mirror of
https://github.com/78/xiaozhi-esp32.git
synced 2025-05-17 15:20:29 +08:00
Hello, Xiaozhi.
This commit is contained in:
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
tmp/
|
||||
components/
|
||||
managed_components/
|
||||
build/
|
||||
.vscode/
|
||||
.devcontainer/
|
||||
sdkconfig.old
|
||||
sdkconfig
|
||||
dependencies.lock
|
8
CMakeLists.txt
Executable file
8
CMakeLists.txt
Executable file
@ -0,0 +1,8 @@
|
||||
# For more information about build system see
|
||||
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
|
||||
# The following five lines of boilerplate have to be in your project's
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(xiaozhi)
|
6
README.md
Executable file
6
README.md
Executable file
@ -0,0 +1,6 @@
|
||||
# 你好,小智
|
||||
|
||||
【ESP32+SenseVoice+Qwen72B打造你的AI聊天伴侣!】
|
||||
|
||||
https://www.bilibili.com/video/BV11msTenEH3/?share_source=copy_web&vd_source=ee1aafe19d6e60cf22e60a93881faeba
|
||||
|
430
main/Application.cc
Normal file
430
main/Application.cc
Normal file
@ -0,0 +1,430 @@
|
||||
#include "Application.h"
|
||||
#include <cstring>
|
||||
#include "esp_log.h"
|
||||
#include "model_path.h"
|
||||
#include "SystemInfo.h"
|
||||
#include "cJSON.h"
|
||||
#include "silk_resampler.h"
|
||||
|
||||
#define TAG "application"
|
||||
#define INPUT_SAMPLE_RATE 16000
|
||||
#define DECODE_SAMPLE_RATE 24000
|
||||
#define OUTPUT_SAMPLE_RATE 24000
|
||||
|
||||
|
||||
Application::Application() {
|
||||
event_group_ = xEventGroupCreate();
|
||||
audio_encode_queue_ = xQueueCreate(100, sizeof(AudioEncoderData));
|
||||
audio_decode_queue_ = xQueueCreate(100, sizeof(AudioPacket*));
|
||||
|
||||
srmodel_list_t *models = esp_srmodel_init("model");
|
||||
for (int i = 0; i < models->num; i++) {
|
||||
ESP_LOGI(TAG, "Model %d: %s", i, models->model_name[i]);
|
||||
if (strstr(models->model_name[i], ESP_WN_PREFIX) != NULL) {
|
||||
wakenet_model_ = models->model_name[i];
|
||||
} else if (strstr(models->model_name[i], ESP_NSNET_PREFIX) != NULL) {
|
||||
nsnet_model_ = models->model_name[i];
|
||||
}
|
||||
}
|
||||
|
||||
opus_encoder_.Configure(INPUT_SAMPLE_RATE, 1);
|
||||
opus_decoder_ = opus_decoder_create(DECODE_SAMPLE_RATE, 1, NULL);
|
||||
if (DECODE_SAMPLE_RATE != OUTPUT_SAMPLE_RATE) {
|
||||
assert(0 == silk_resampler_init(&resampler_state_, DECODE_SAMPLE_RATE, OUTPUT_SAMPLE_RATE, 1));
|
||||
}
|
||||
}
|
||||
|
||||
Application::~Application() {
|
||||
if (afe_detection_data_ != nullptr) {
|
||||
esp_afe_sr_v1.destroy(afe_detection_data_);
|
||||
}
|
||||
|
||||
if (afe_communication_data_ != nullptr) {
|
||||
esp_afe_vc_v1.destroy(afe_communication_data_);
|
||||
}
|
||||
|
||||
if (opus_decoder_ != nullptr) {
|
||||
opus_decoder_destroy(opus_decoder_);
|
||||
}
|
||||
if (audio_encode_task_stack_ != nullptr) {
|
||||
free(audio_encode_task_stack_);
|
||||
}
|
||||
if (audio_decode_task_stack_ != nullptr) {
|
||||
free(audio_decode_task_stack_);
|
||||
}
|
||||
vQueueDelete(audio_decode_queue_);
|
||||
vQueueDelete(audio_encode_queue_);
|
||||
|
||||
vEventGroupDelete(event_group_);
|
||||
}
|
||||
|
||||
void Application::Start() {
|
||||
audio_device_.Start(INPUT_SAMPLE_RATE, OUTPUT_SAMPLE_RATE);
|
||||
audio_device_.OnStateChanged([this]() {
|
||||
if (audio_device_.playing()) {
|
||||
SetChatState(kChatStateSpeaking);
|
||||
} else {
|
||||
// Check if communication is still running
|
||||
if (xEventGroupGetBits(event_group_) & COMMUNICATION_RUNNING) {
|
||||
SetChatState(kChatStateListening);
|
||||
} else {
|
||||
SetChatState(kChatStateIdle);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// OPUS encoder / decoder use a lot of stack memory
|
||||
audio_encode_task_stack_ = (StackType_t*)malloc(4096 * 8);
|
||||
xTaskCreateStatic([](void* arg) {
|
||||
Application* app = (Application*)arg;
|
||||
app->AudioEncodeTask();
|
||||
}, "opus_encode", 4096 * 8, this, 1, audio_encode_task_stack_, &audio_encode_task_buffer_);
|
||||
audio_decode_task_stack_ = (StackType_t*)malloc(4096 * 8);
|
||||
xTaskCreateStatic([](void* arg) {
|
||||
Application* app = (Application*)arg;
|
||||
app->AudioDecodeTask();
|
||||
}, "opus_decode", 4096 * 8, this, 1, audio_decode_task_stack_, &audio_decode_task_buffer_);
|
||||
|
||||
wifi_station_.Start();
|
||||
|
||||
StartCommunication();
|
||||
StartDetection();
|
||||
|
||||
xEventGroupSetBits(event_group_, DETECTION_RUNNING);
|
||||
}
|
||||
|
||||
void Application::SetChatState(ChatState state) {
|
||||
chat_state_ = state;
|
||||
switch (chat_state_) {
|
||||
case kChatStateIdle:
|
||||
ESP_LOGI(TAG, "Chat state: idle");
|
||||
builtin_led_.TurnOff();
|
||||
break;
|
||||
case kChatStateConnecting:
|
||||
ESP_LOGI(TAG, "Chat state: connecting");
|
||||
builtin_led_.SetBlue();
|
||||
builtin_led_.TurnOn();
|
||||
break;
|
||||
case kChatStateListening:
|
||||
ESP_LOGI(TAG, "Chat state: listening");
|
||||
builtin_led_.SetRed();
|
||||
builtin_led_.TurnOn();
|
||||
break;
|
||||
case kChatStateSpeaking:
|
||||
ESP_LOGI(TAG, "Chat state: speaking");
|
||||
builtin_led_.SetGreen();
|
||||
builtin_led_.TurnOn();
|
||||
break;
|
||||
}
|
||||
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
if (ws_client_ && ws_client_->IsConnected()) {
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(root, "type", "state");
|
||||
cJSON_AddStringToObject(root, "state", chat_state_ == kChatStateListening ? "listening" : "speaking");
|
||||
char* json = cJSON_PrintUnformatted(root);
|
||||
ws_client_->Send(json);
|
||||
cJSON_Delete(root);
|
||||
free(json);
|
||||
}
|
||||
}
|
||||
|
||||
void Application::StartCommunication() {
|
||||
afe_config_t afe_config = {
|
||||
.aec_init = false,
|
||||
.se_init = true,
|
||||
.vad_init = false,
|
||||
.wakenet_init = false,
|
||||
.voice_communication_init = true,
|
||||
.voice_communication_agc_init = true,
|
||||
.voice_communication_agc_gain = 10,
|
||||
.vad_mode = VAD_MODE_3,
|
||||
.wakenet_model_name = NULL,
|
||||
.wakenet_model_name_2 = NULL,
|
||||
.wakenet_mode = DET_MODE_90,
|
||||
.afe_mode = SR_MODE_HIGH_PERF,
|
||||
.afe_perferred_core = 0,
|
||||
.afe_perferred_priority = 5,
|
||||
.afe_ringbuf_size = 50,
|
||||
.memory_alloc_mode = AFE_MEMORY_ALLOC_MORE_PSRAM,
|
||||
.afe_linear_gain = 1.0,
|
||||
.agc_mode = AFE_MN_PEAK_AGC_MODE_2,
|
||||
.pcm_config = {
|
||||
.total_ch_num = 1,
|
||||
.mic_num = 1,
|
||||
.ref_num = 0,
|
||||
.sample_rate = INPUT_SAMPLE_RATE
|
||||
},
|
||||
.debug_init = false,
|
||||
.debug_hook = {{ AFE_DEBUG_HOOK_MASE_TASK_IN, NULL }, { AFE_DEBUG_HOOK_FETCH_TASK_IN, NULL }},
|
||||
.afe_ns_mode = NS_MODE_SSP,
|
||||
.afe_ns_model_name = NULL,
|
||||
.fixed_first_channel = true,
|
||||
};
|
||||
|
||||
afe_communication_data_ = esp_afe_vc_v1.create_from_config(&afe_config);
|
||||
|
||||
xTaskCreate([](void* arg) {
|
||||
Application* app = (Application*)arg;
|
||||
app->AudioCommunicationTask();
|
||||
}, "audio_communication", 4096 * 2, this, 5, NULL);
|
||||
}
|
||||
|
||||
void Application::StartDetection() {
|
||||
afe_config_t afe_config = {
|
||||
.aec_init = false,
|
||||
.se_init = true,
|
||||
.vad_init = false,
|
||||
.wakenet_init = true,
|
||||
.voice_communication_init = false,
|
||||
.voice_communication_agc_init = false,
|
||||
.voice_communication_agc_gain = 10,
|
||||
.vad_mode = VAD_MODE_3,
|
||||
.wakenet_model_name = wakenet_model_,
|
||||
.wakenet_model_name_2 = NULL,
|
||||
.wakenet_mode = DET_MODE_90,
|
||||
.afe_mode = SR_MODE_HIGH_PERF,
|
||||
.afe_perferred_core = 0,
|
||||
.afe_perferred_priority = 5,
|
||||
.afe_ringbuf_size = 50,
|
||||
.memory_alloc_mode = AFE_MEMORY_ALLOC_MORE_PSRAM,
|
||||
.afe_linear_gain = 1.0,
|
||||
.agc_mode = AFE_MN_PEAK_AGC_MODE_2,
|
||||
.pcm_config = {
|
||||
.total_ch_num = 1,
|
||||
.mic_num = 1,
|
||||
.ref_num = 0,
|
||||
.sample_rate = INPUT_SAMPLE_RATE
|
||||
},
|
||||
.debug_init = false,
|
||||
.debug_hook = {{ AFE_DEBUG_HOOK_MASE_TASK_IN, NULL }, { AFE_DEBUG_HOOK_FETCH_TASK_IN, NULL }},
|
||||
.afe_ns_mode = NS_MODE_SSP,
|
||||
.afe_ns_model_name = NULL,
|
||||
.fixed_first_channel = true,
|
||||
};
|
||||
|
||||
afe_detection_data_ = esp_afe_sr_v1.create_from_config(&afe_config);
|
||||
xTaskCreate([](void* arg) {
|
||||
Application* app = (Application*)arg;
|
||||
app->AudioFeedTask();
|
||||
}, "audio_feed", 4096 * 2, this, 5, NULL);
|
||||
|
||||
xTaskCreate([](void* arg) {
|
||||
Application* app = (Application*)arg;
|
||||
app->AudioDetectionTask();
|
||||
}, "audio_detection", 4096 * 2, this, 5, NULL);
|
||||
}
|
||||
|
||||
void Application::AudioFeedTask() {
|
||||
int chunk_size = esp_afe_vc_v1.get_feed_chunksize(afe_detection_data_);
|
||||
int16_t buffer[chunk_size];
|
||||
ESP_LOGI(TAG, "Audio feed task started, chunk size: %d", chunk_size);
|
||||
|
||||
while (true) {
|
||||
audio_device_.Read(buffer, chunk_size);
|
||||
|
||||
auto event_bits = xEventGroupGetBits(event_group_);
|
||||
if (event_bits & DETECTION_RUNNING) {
|
||||
esp_afe_sr_v1.feed(afe_detection_data_, buffer);
|
||||
} else if (event_bits & COMMUNICATION_RUNNING) {
|
||||
esp_afe_vc_v1.feed(afe_communication_data_, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void Application::AudioDetectionTask() {
|
||||
auto chunk_size = esp_afe_sr_v1.get_fetch_chunksize(afe_detection_data_);
|
||||
ESP_LOGI(TAG, "Audio detection task started, chunk size: %d", chunk_size);
|
||||
|
||||
while (true) {
|
||||
xEventGroupWaitBits(event_group_, DETECTION_RUNNING, pdFALSE, pdTRUE, portMAX_DELAY);
|
||||
|
||||
auto res = esp_afe_sr_v1.fetch(afe_detection_data_);
|
||||
if (res == nullptr || res->ret_value == ESP_FAIL) {
|
||||
ESP_LOGE(TAG, "Error in fetch");
|
||||
if (res != nullptr) {
|
||||
ESP_LOGI(TAG, "Error code: %d", res->ret_value);
|
||||
}
|
||||
continue;;
|
||||
}
|
||||
|
||||
if (res->wakeup_state == WAKENET_DETECTED) {
|
||||
ESP_LOGI(TAG, "Wakenet detected");
|
||||
|
||||
xEventGroupClearBits(event_group_, DETECTION_RUNNING);
|
||||
SetChatState(kChatStateConnecting);
|
||||
StartWebSocketClient();
|
||||
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
if (ws_client_ && ws_client_->IsConnected()) {
|
||||
// If connected, the hello message is already sent, so we can start communication
|
||||
xEventGroupSetBits(event_group_, COMMUNICATION_RUNNING);
|
||||
} else {
|
||||
SetChatState(kChatStateIdle);
|
||||
xEventGroupSetBits(event_group_, DETECTION_RUNNING);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Application::AudioCommunicationTask() {
|
||||
int chunk_size = esp_afe_vc_v1.get_fetch_chunksize(afe_communication_data_);
|
||||
ESP_LOGI(TAG, "Audio communication task started, chunk size: %d", chunk_size);
|
||||
|
||||
while (true) {
|
||||
xEventGroupWaitBits(event_group_, COMMUNICATION_RUNNING, pdFALSE, pdTRUE, portMAX_DELAY);
|
||||
|
||||
auto res = esp_afe_vc_v1.fetch(afe_communication_data_);
|
||||
if (res == nullptr || res->ret_value == ESP_FAIL) {
|
||||
ESP_LOGE(TAG, "Error in fetch");
|
||||
if (res != nullptr) {
|
||||
ESP_LOGI(TAG, "Error code: %d", res->ret_value);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the websocket client is disconnected by the server
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
if (ws_client_ == nullptr || !ws_client_->IsConnected()) {
|
||||
if (ws_client_ != nullptr) {
|
||||
delete ws_client_;
|
||||
ws_client_ = nullptr;
|
||||
}
|
||||
if (audio_device_.playing()) {
|
||||
audio_device_.Break();
|
||||
}
|
||||
SetChatState(kChatStateIdle);
|
||||
xEventGroupSetBits(event_group_, DETECTION_RUNNING);
|
||||
xEventGroupClearBits(event_group_, COMMUNICATION_RUNNING);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (chat_state_ == kChatStateListening) {
|
||||
// Send audio data to server
|
||||
AudioEncoderData data;
|
||||
data.size = res->data_size;
|
||||
data.data = malloc(data.size);
|
||||
memcpy((void*)data.data, res->data, data.size);
|
||||
xQueueSend(audio_encode_queue_, &data, portMAX_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Application::AudioEncodeTask() {
|
||||
ESP_LOGI(TAG, "Audio encode task started");
|
||||
while (true) {
|
||||
AudioEncoderData data;
|
||||
xQueueReceive(audio_encode_queue_, &data, portMAX_DELAY);
|
||||
|
||||
// Encode audio data
|
||||
opus_encoder_.Encode(data.data, data.size, [this](const void* data, size_t size) {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||
if (ws_client_ && ws_client_->IsConnected()) {
|
||||
ws_client_->Send((const char*)data, size, true);
|
||||
}
|
||||
});
|
||||
free((void*)data.data);
|
||||
}
|
||||
}
|
||||
|
||||
void Application::AudioDecodeTask() {
|
||||
int frame_size = DECODE_SAMPLE_RATE / 1000 * opus_duration_ms_;
|
||||
|
||||
while (true) {
|
||||
AudioPacket* packet;
|
||||
xQueueReceive(audio_decode_queue_, &packet, portMAX_DELAY);
|
||||
packet->pcm.resize(frame_size);
|
||||
|
||||
int ret = opus_decode(opus_decoder_, packet->opus.data(), packet->opus.size(), packet->pcm.data(), frame_size, 0);
|
||||
if (ret < 0) {
|
||||
ESP_LOGE(TAG, "Failed to decode audio, error code: %d", ret);
|
||||
free(packet);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (DECODE_SAMPLE_RATE != OUTPUT_SAMPLE_RATE) {
|
||||
int target_size = frame_size * OUTPUT_SAMPLE_RATE / DECODE_SAMPLE_RATE;
|
||||
std::vector<int16_t> resampled(target_size);
|
||||
assert(0 == silk_resampler(&resampler_state_, resampled.data(), packet->pcm.data(), frame_size));
|
||||
packet->pcm = std::move(resampled);
|
||||
}
|
||||
|
||||
audio_device_.QueueAudioPacket(packet);
|
||||
}
|
||||
}
|
||||
|
||||
void Application::StartWebSocketClient() {
|
||||
if (ws_client_ != nullptr) {
|
||||
delete ws_client_;
|
||||
}
|
||||
|
||||
std::string token = "Bearer " + std::string(CONFIG_WEBSOCKET_ACCESS_TOKEN);
|
||||
ws_client_ = new WebSocketClient();
|
||||
ws_client_->SetHeader("Authorization", token.c_str());
|
||||
ws_client_->SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str());
|
||||
|
||||
ws_client_->OnConnected([this]() {
|
||||
ESP_LOGI(TAG, "Websocket connected");
|
||||
|
||||
// Send hello message to describe the client
|
||||
// keys: message type, version, wakeup_model, audio_params (format, sample_rate, channels)
|
||||
std::string message = "{";
|
||||
message += "\"type\":\"hello\", \"version\":\"1.0\",";
|
||||
message += "\"wakeup_model\":\"" + std::string(wakenet_model_) + "\",";
|
||||
message += "\"audio_params\":{";
|
||||
message += "\"format\":\"opus\", \"sample_rate\":" + std::to_string(INPUT_SAMPLE_RATE) + ", \"channels\":1";
|
||||
message += "}}";
|
||||
ws_client_->Send(message);
|
||||
});
|
||||
|
||||
ws_client_->OnData([this](const char* data, size_t len, bool binary) {
|
||||
auto packet = new AudioPacket();
|
||||
if (binary) {
|
||||
auto header = (AudioDataHeader*)data;
|
||||
packet->type = kAudioPacketTypeData;
|
||||
packet->timestamp = ntohl(header->timestamp);
|
||||
|
||||
auto payload_size = ntohl(header->payload_size);
|
||||
packet->opus.resize(payload_size);
|
||||
memcpy(packet->opus.data(), data + sizeof(AudioDataHeader), payload_size);
|
||||
} else {
|
||||
// Parse JSON data
|
||||
auto root = cJSON_Parse(data);
|
||||
auto type = cJSON_GetObjectItem(root, "type");
|
||||
if (type != NULL) {
|
||||
if (strcmp(type->valuestring, "tts") == 0) {
|
||||
auto state = cJSON_GetObjectItem(root, "state");
|
||||
if (strcmp(state->valuestring, "start") == 0) {
|
||||
packet->type = kAudioPacketTypeStart;
|
||||
} else if (strcmp(state->valuestring, "stop") == 0) {
|
||||
packet->type = kAudioPacketTypeStop;
|
||||
} else if (strcmp(state->valuestring, "sentence_end") == 0) {
|
||||
packet->type = kAudioPacketTypeSentenceEnd;
|
||||
} else if (strcmp(state->valuestring, "sentence_start") == 0) {
|
||||
packet->type = kAudioPacketTypeSentenceStart;
|
||||
packet->text = cJSON_GetObjectItem(root, "text")->valuestring;
|
||||
}
|
||||
}
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
xQueueSend(audio_decode_queue_, &packet, portMAX_DELAY);
|
||||
});
|
||||
|
||||
ws_client_->OnError([this](int error) {
|
||||
ESP_LOGE(TAG, "Websocket error: %d", error);
|
||||
});
|
||||
|
||||
ws_client_->OnClosed([this]() {
|
||||
ESP_LOGI(TAG, "Websocket closed");
|
||||
});
|
||||
|
||||
if (!ws_client_->Connect(CONFIG_WEBSOCKET_URL)) {
|
||||
ESP_LOGE(TAG, "Failed to connect to websocket server");
|
||||
return;
|
||||
}
|
||||
}
|
83
main/Application.h
Normal file
83
main/Application.h
Normal file
@ -0,0 +1,83 @@
|
||||
#ifndef _APPLICATION_H_
|
||||
#define _APPLICATION_H_
|
||||
|
||||
#include "WifiStation.h"
|
||||
#include "AudioDevice.h"
|
||||
#include "OpusEncoder.h"
|
||||
#include "WebSocketClient.h"
|
||||
#include "BuiltinLed.h"
|
||||
|
||||
#include "opus.h"
|
||||
#include "resampler_structs.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_afe_sr_models.h"
|
||||
#include "esp_nsn_models.h"
|
||||
#include <mutex>
|
||||
|
||||
#define DETECTION_RUNNING 1
|
||||
#define COMMUNICATION_RUNNING 2
|
||||
|
||||
struct AudioEncoderData {
|
||||
const void* data;
|
||||
size_t size;
|
||||
};
|
||||
|
||||
enum ChatState {
|
||||
kChatStateIdle,
|
||||
kChatStateConnecting,
|
||||
kChatStateListening,
|
||||
kChatStateSpeaking,
|
||||
};
|
||||
|
||||
class Application {
|
||||
public:
|
||||
Application();
|
||||
~Application();
|
||||
void Start();
|
||||
|
||||
private:
|
||||
WifiStation wifi_station_;
|
||||
AudioDevice audio_device_;
|
||||
BuiltinLed builtin_led_;
|
||||
|
||||
std::recursive_mutex mutex_;
|
||||
WebSocketClient* ws_client_ = nullptr;
|
||||
esp_afe_sr_data_t* afe_detection_data_ = nullptr;
|
||||
esp_afe_sr_data_t* afe_communication_data_ = nullptr;
|
||||
EventGroupHandle_t event_group_;
|
||||
char* wakenet_model_ = NULL;
|
||||
char* nsnet_model_ = NULL;
|
||||
volatile ChatState chat_state_ = kChatStateIdle;
|
||||
|
||||
// Audio encode / decode
|
||||
TaskHandle_t audio_feed_task_ = nullptr;
|
||||
StaticTask_t audio_encode_task_buffer_;
|
||||
StackType_t* audio_encode_task_stack_ = nullptr;
|
||||
QueueHandle_t audio_encode_queue_ = nullptr;
|
||||
|
||||
TaskHandle_t audio_decode_task_ = nullptr;
|
||||
StaticTask_t audio_decode_task_buffer_;
|
||||
StackType_t* audio_decode_task_stack_ = nullptr;
|
||||
QueueHandle_t audio_decode_queue_ = nullptr;
|
||||
|
||||
OpusEncoder opus_encoder_;
|
||||
OpusDecoder* opus_decoder_ = nullptr;
|
||||
|
||||
int opus_duration_ms_ = 60;
|
||||
silk_resampler_state_struct resampler_state_;
|
||||
|
||||
void SetChatState(ChatState state);
|
||||
void StartDetection();
|
||||
void StartCommunication();
|
||||
void StartWebSocketClient();
|
||||
|
||||
void AudioFeedTask();
|
||||
void AudioDetectionTask();
|
||||
void AudioCommunicationTask();
|
||||
void AudioEncodeTask();
|
||||
void AudioDecodeTask();
|
||||
};
|
||||
|
||||
#endif // _APPLICATION_H_
|
235
main/AudioDevice.cc
Normal file
235
main/AudioDevice.cc
Normal file
@ -0,0 +1,235 @@
|
||||
#include "AudioDevice.h"
|
||||
#include "esp_log.h"
|
||||
#include <cstring>
|
||||
|
||||
#define TAG "AudioDevice"
|
||||
#define SPEAKING BIT0
|
||||
|
||||
AudioDevice::AudioDevice() {
|
||||
audio_play_queue_ = xQueueCreate(100, sizeof(AudioPacket*));
|
||||
}
|
||||
|
||||
AudioDevice::~AudioDevice() {
|
||||
vQueueDelete(audio_play_queue_);
|
||||
|
||||
if (audio_play_task_ != nullptr) {
|
||||
vTaskDelete(audio_play_task_);
|
||||
}
|
||||
if (rx_handle_ != nullptr) {
|
||||
ESP_ERROR_CHECK(i2s_channel_disable(rx_handle_));
|
||||
}
|
||||
if (tx_handle_ != nullptr) {
|
||||
ESP_ERROR_CHECK(i2s_channel_disable(tx_handle_));
|
||||
}
|
||||
}
|
||||
|
||||
void AudioDevice::Start(int input_sample_rate, int output_sample_rate) {
|
||||
assert(input_sample_rate == 16000);
|
||||
input_sample_rate_ = input_sample_rate;
|
||||
output_sample_rate_ = output_sample_rate;
|
||||
|
||||
if (output_sample_rate == 16000) {
|
||||
CreateDuplexChannels();
|
||||
} else {
|
||||
CreateSimplexChannels();
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_));
|
||||
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle_));
|
||||
|
||||
xTaskCreate([](void* arg) {
|
||||
auto audio_device = (AudioDevice*)arg;
|
||||
audio_device->AudioPlayTask();
|
||||
}, "audio_play", 4096 * 4, this, 5, &audio_play_task_);
|
||||
}
|
||||
|
||||
void AudioDevice::CreateDuplexChannels() {
|
||||
duplex_ = true;
|
||||
|
||||
i2s_chan_config_t chan_cfg = {
|
||||
.id = I2S_NUM_0,
|
||||
.role = I2S_ROLE_MASTER,
|
||||
.dma_desc_num = 6,
|
||||
.dma_frame_num = 240,
|
||||
.auto_clear_after_cb = false,
|
||||
.auto_clear_before_cb = false,
|
||||
.intr_priority = 0,
|
||||
};
|
||||
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));
|
||||
|
||||
i2s_std_config_t std_cfg = {
|
||||
.clk_cfg = {
|
||||
.sample_rate_hz = (uint32_t)output_sample_rate_,
|
||||
.clk_src = I2S_CLK_SRC_DEFAULT,
|
||||
.ext_clk_freq_hz = 0,
|
||||
.mclk_multiple = I2S_MCLK_MULTIPLE_256
|
||||
},
|
||||
.slot_cfg = {
|
||||
.data_bit_width = I2S_DATA_BIT_WIDTH_32BIT,
|
||||
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
|
||||
.slot_mode = I2S_SLOT_MODE_MONO,
|
||||
.slot_mask = I2S_STD_SLOT_LEFT,
|
||||
.ws_width = I2S_DATA_BIT_WIDTH_32BIT,
|
||||
.ws_pol = false,
|
||||
.bit_shift = true,
|
||||
.left_align = true,
|
||||
.big_endian = false,
|
||||
.bit_order_lsb = false
|
||||
},
|
||||
.gpio_cfg = {
|
||||
.mclk = I2S_GPIO_UNUSED,
|
||||
.bclk = GPIO_NUM_5,
|
||||
.ws = GPIO_NUM_4,
|
||||
.dout = GPIO_NUM_6,
|
||||
.din = GPIO_NUM_3,
|
||||
.invert_flags = {
|
||||
.mclk_inv = false,
|
||||
.bclk_inv = false,
|
||||
.ws_inv = false
|
||||
}
|
||||
}
|
||||
};
|
||||
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
|
||||
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
|
||||
ESP_LOGI(TAG, "Duplex channels created");
|
||||
}
|
||||
|
||||
void AudioDevice::CreateSimplexChannels() {
|
||||
// Create a new channel for speaker
|
||||
i2s_chan_config_t chan_cfg = {
|
||||
.id = I2S_NUM_0,
|
||||
.role = I2S_ROLE_MASTER,
|
||||
.dma_desc_num = 6,
|
||||
.dma_frame_num = 240,
|
||||
.auto_clear_after_cb = false,
|
||||
.auto_clear_before_cb = false,
|
||||
.intr_priority = 0,
|
||||
};
|
||||
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, nullptr));
|
||||
|
||||
i2s_std_config_t std_cfg = {
|
||||
.clk_cfg = {
|
||||
.sample_rate_hz = (uint32_t)output_sample_rate_,
|
||||
.clk_src = I2S_CLK_SRC_DEFAULT,
|
||||
.ext_clk_freq_hz = 0,
|
||||
.mclk_multiple = I2S_MCLK_MULTIPLE_256
|
||||
},
|
||||
.slot_cfg = {
|
||||
.data_bit_width = I2S_DATA_BIT_WIDTH_32BIT,
|
||||
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
|
||||
.slot_mode = I2S_SLOT_MODE_MONO,
|
||||
.slot_mask = I2S_STD_SLOT_LEFT,
|
||||
.ws_width = I2S_DATA_BIT_WIDTH_32BIT,
|
||||
.ws_pol = false,
|
||||
.bit_shift = true,
|
||||
.left_align = true,
|
||||
.big_endian = false,
|
||||
.bit_order_lsb = false
|
||||
},
|
||||
.gpio_cfg = {
|
||||
.mclk = I2S_GPIO_UNUSED,
|
||||
.bclk = GPIO_NUM_5,
|
||||
.ws = GPIO_NUM_4,
|
||||
.dout = GPIO_NUM_6,
|
||||
.din = I2S_GPIO_UNUSED,
|
||||
.invert_flags = {
|
||||
.mclk_inv = false,
|
||||
.bclk_inv = false,
|
||||
.ws_inv = false
|
||||
}
|
||||
}
|
||||
};
|
||||
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
|
||||
|
||||
// Create a new channel for MIC
|
||||
chan_cfg.id = I2S_NUM_1;
|
||||
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, nullptr, &rx_handle_));
|
||||
std_cfg.clk_cfg.sample_rate_hz = (uint32_t)input_sample_rate_;
|
||||
std_cfg.gpio_cfg.bclk = GPIO_NUM_11;
|
||||
std_cfg.gpio_cfg.ws = GPIO_NUM_10;
|
||||
std_cfg.gpio_cfg.dout = I2S_GPIO_UNUSED;
|
||||
std_cfg.gpio_cfg.din = GPIO_NUM_3;
|
||||
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
|
||||
ESP_LOGI(TAG, "Simplex channels created");
|
||||
}
|
||||
|
||||
void AudioDevice::Write(const int16_t* data, int samples) {
|
||||
int32_t buffer[samples];
|
||||
for (int i = 0; i < samples; i++) {
|
||||
buffer[i] = int32_t(data[i]) << 15;
|
||||
}
|
||||
|
||||
size_t bytes_written;
|
||||
ESP_ERROR_CHECK(i2s_channel_write(tx_handle_, buffer, samples * sizeof(int32_t), &bytes_written, portMAX_DELAY));
|
||||
}
|
||||
|
||||
int AudioDevice::Read(int16_t* dest, int samples) {
|
||||
size_t bytes_read;
|
||||
|
||||
int32_t bit32_buffer_[samples];
|
||||
if (i2s_channel_read(rx_handle_, bit32_buffer_, samples * sizeof(int32_t), &bytes_read, portMAX_DELAY) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Read Failed!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
samples = bytes_read / sizeof(int32_t);
|
||||
for (int i = 0; i < samples; i++) {
|
||||
int32_t value = bit32_buffer_[i] >> 12;
|
||||
dest[i] = (value > INT16_MAX) ? INT16_MAX : (value < -INT16_MAX) ? -INT16_MAX : (int16_t)value;
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
void AudioDevice::QueueAudioPacket(AudioPacket* packet) {
|
||||
xQueueSend(audio_play_queue_, &packet, portMAX_DELAY);
|
||||
}
|
||||
|
||||
void AudioDevice::AudioPlayTask() {
|
||||
while (true) {
|
||||
AudioPacket* packet;
|
||||
xQueueReceive(audio_play_queue_, &packet, portMAX_DELAY);
|
||||
|
||||
switch (packet->type)
|
||||
{
|
||||
case kAudioPacketTypeStart:
|
||||
playing_ = true;
|
||||
breaked_ = false;
|
||||
if (on_state_changed_) {
|
||||
on_state_changed_();
|
||||
}
|
||||
break;
|
||||
case kAudioPacketTypeStop:
|
||||
playing_ = false;
|
||||
if (on_state_changed_) {
|
||||
on_state_changed_();
|
||||
}
|
||||
break;
|
||||
case kAudioPacketTypeSentenceStart:
|
||||
ESP_LOGI(TAG, "Playing sentence: %s", packet->text.c_str());
|
||||
break;
|
||||
case kAudioPacketTypeSentenceEnd:
|
||||
if (breaked_) { // Clear the queue
|
||||
AudioPacket* p;
|
||||
while (xQueueReceive(audio_play_queue_, &p, 0) == pdTRUE) {
|
||||
delete p;
|
||||
}
|
||||
breaked_ = false;
|
||||
}
|
||||
break;
|
||||
case kAudioPacketTypeData:
|
||||
Write(packet->pcm.data(), packet->pcm.size());
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Unknown audio packet type: %d", packet->type);
|
||||
}
|
||||
delete packet;
|
||||
}
|
||||
}
|
||||
|
||||
void AudioDevice::OnStateChanged(std::function<void()> callback) {
|
||||
on_state_changed_ = callback;
|
||||
}
|
||||
|
||||
void AudioDevice::Break() {
|
||||
breaked_ = true;
|
||||
}
|
76
main/AudioDevice.h
Normal file
76
main/AudioDevice.h
Normal file
@ -0,0 +1,76 @@
|
||||
#ifndef _AUDIO_DEVICE_H
|
||||
#define _AUDIO_DEVICE_H
|
||||
|
||||
#include "opus.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "driver/i2s_std.h"
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
|
||||
enum AudioPacketType {
|
||||
kAudioPacketTypeUnkonwn = 0,
|
||||
kAudioPacketTypeStart,
|
||||
kAudioPacketTypeStop,
|
||||
kAudioPacketTypeData,
|
||||
kAudioPacketTypeSentenceStart,
|
||||
kAudioPacketTypeSentenceEnd
|
||||
};
|
||||
|
||||
struct AudioPacket {
|
||||
AudioPacketType type = kAudioPacketTypeUnkonwn;
|
||||
std::string text;
|
||||
std::vector<uint8_t> opus;
|
||||
std::vector<int16_t> pcm;
|
||||
uint32_t timestamp;
|
||||
};
|
||||
|
||||
struct AudioDataHeader {
|
||||
uint32_t version;
|
||||
uint32_t reserved;
|
||||
uint32_t timestamp;
|
||||
uint32_t payload_size;
|
||||
} __attribute__((packed));
|
||||
|
||||
class AudioDevice {
|
||||
public:
|
||||
AudioDevice();
|
||||
~AudioDevice();
|
||||
|
||||
void Start(int input_sample_rate, int output_sample_rate);
|
||||
int Read(int16_t* dest, int samples);
|
||||
void Write(const int16_t* data, int samples);
|
||||
void QueueAudioPacket(AudioPacket* packet);
|
||||
void OnStateChanged(std::function<void()> callback);
|
||||
void Break();
|
||||
|
||||
int input_sample_rate() const { return input_sample_rate_; }
|
||||
int output_sample_rate() const { return output_sample_rate_; }
|
||||
bool duplex() const { return duplex_; }
|
||||
bool playing() const { return playing_; }
|
||||
|
||||
private:
|
||||
bool playing_ = false;
|
||||
bool breaked_ = false;
|
||||
bool duplex_ = false;
|
||||
int input_sample_rate_ = 0;
|
||||
int output_sample_rate_ = 0;
|
||||
|
||||
i2s_chan_handle_t tx_handle_ = nullptr;
|
||||
i2s_chan_handle_t rx_handle_ = nullptr;
|
||||
|
||||
QueueHandle_t audio_play_queue_ = nullptr;
|
||||
TaskHandle_t audio_play_task_ = nullptr;
|
||||
|
||||
EventGroupHandle_t event_group_;
|
||||
std::function<void()> on_state_changed_;
|
||||
|
||||
void CreateDuplexChannels();
|
||||
void CreateSimplexChannels();
|
||||
void AudioPlayTask();
|
||||
};
|
||||
|
||||
#endif // _AUDIO_DEVICE_H
|
89
main/BuiltinLed.cc
Normal file
89
main/BuiltinLed.cc
Normal file
@ -0,0 +1,89 @@
|
||||
#include "BuiltinLed.h"
|
||||
#include <cstring>
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#define TAG "builtin_led"
|
||||
|
||||
BuiltinLed::BuiltinLed() {
|
||||
mutex_ = xSemaphoreCreateMutex();
|
||||
|
||||
Configure();
|
||||
SetGreen();
|
||||
}
|
||||
|
||||
BuiltinLed::~BuiltinLed() {
|
||||
if (blink_task_ != nullptr) {
|
||||
vTaskDelete(blink_task_);
|
||||
}
|
||||
if (led_strip_ != nullptr) {
|
||||
led_strip_del(led_strip_);
|
||||
}
|
||||
vSemaphoreDelete(mutex_);
|
||||
}
|
||||
|
||||
void BuiltinLed::Configure() {
|
||||
/* LED strip initialization with the GPIO and pixels number*/
|
||||
led_strip_config_t strip_config;
|
||||
bzero(&strip_config, sizeof(strip_config));
|
||||
strip_config.strip_gpio_num = CONFIG_BUILTIN_LED_GPIO;
|
||||
strip_config.max_leds = 1;
|
||||
|
||||
led_strip_rmt_config_t rmt_config;
|
||||
bzero(&rmt_config, sizeof(rmt_config));
|
||||
rmt_config.resolution_hz = 10 * 1000 * 1000; // 10MHz
|
||||
|
||||
ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip_));
|
||||
/* Set all LED off to clear all pixels */
|
||||
led_strip_clear(led_strip_);
|
||||
}
|
||||
|
||||
void BuiltinLed::SetColor(uint8_t r, uint8_t g, uint8_t b) {
|
||||
r_ = r;
|
||||
g_ = g;
|
||||
b_ = b;
|
||||
}
|
||||
|
||||
void BuiltinLed::TurnOn() {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
led_strip_set_pixel(led_strip_, 0, r_, g_, b_);
|
||||
led_strip_refresh(led_strip_);
|
||||
xSemaphoreGive(mutex_);
|
||||
}
|
||||
|
||||
void BuiltinLed::TurnOff() {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
led_strip_clear(led_strip_);
|
||||
xSemaphoreGive(mutex_);
|
||||
}
|
||||
|
||||
void BuiltinLed::BlinkOnce() {
|
||||
Blink(1, 100);
|
||||
}
|
||||
|
||||
void BuiltinLed::Blink(int times, int interval_ms) {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
struct BlinkTaskArgs {
|
||||
BuiltinLed* self;
|
||||
int times;
|
||||
int interval_ms;
|
||||
};
|
||||
auto args = new BlinkTaskArgs {this, times, interval_ms};
|
||||
|
||||
xTaskCreate([](void* obj) {
|
||||
auto args = (BlinkTaskArgs*) obj;
|
||||
auto this_ = args->self;
|
||||
for (int i = 0; i < args->times; i++) {
|
||||
this_->TurnOn();
|
||||
vTaskDelay(args->interval_ms / portTICK_PERIOD_MS);
|
||||
this_->TurnOff();
|
||||
vTaskDelay(args->interval_ms / portTICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
delete args;
|
||||
this_->blink_task_ = nullptr;
|
||||
vTaskDelete(NULL);
|
||||
}, "blink", 4096, args, tskIDLE_PRIORITY, &blink_task_);
|
||||
|
||||
xSemaphoreGive(mutex_);
|
||||
}
|
33
main/BuiltinLed.h
Normal file
33
main/BuiltinLed.h
Normal file
@ -0,0 +1,33 @@
|
||||
#ifndef _BUILTIN_LED_H_
|
||||
#define _BUILTIN_LED_H_
|
||||
|
||||
#include "led_strip.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
class BuiltinLed {
|
||||
public:
|
||||
BuiltinLed();
|
||||
~BuiltinLed();
|
||||
|
||||
void BlinkOnce();
|
||||
void Blink(int times, int interval_ms);
|
||||
void TurnOn();
|
||||
void TurnOff();
|
||||
void SetColor(uint8_t r, uint8_t g, uint8_t b);
|
||||
void SetWhite() { SetColor(128, 128, 128); }
|
||||
void SetGrey() { SetColor(32, 32, 32); }
|
||||
void SetRed() { SetColor(128, 0, 0); }
|
||||
void SetGreen() { SetColor(0, 128, 0); }
|
||||
void SetBlue() { SetColor(0, 0, 128); }
|
||||
|
||||
private:
|
||||
SemaphoreHandle_t mutex_;
|
||||
TaskHandle_t blink_task_ = nullptr;
|
||||
led_strip_handle_t led_strip_ = nullptr;
|
||||
uint8_t r_ = 0, g_ = 0, b_ = 0;
|
||||
|
||||
void Configure();
|
||||
};
|
||||
|
||||
#endif // _BUILTIN_LED_H_
|
15
main/CMakeLists.txt
Executable file
15
main/CMakeLists.txt
Executable file
@ -0,0 +1,15 @@
|
||||
set(SOURCES "AudioDevice.cc"
|
||||
"SystemInfo.cc"
|
||||
"WebSocketClient.cc"
|
||||
"OpusEncoder.cc"
|
||||
"BuiltinLed.cc"
|
||||
"Application.cc"
|
||||
"WifiConfigurationAp.cc"
|
||||
"main.cc"
|
||||
"WifiStation.cc"
|
||||
)
|
||||
|
||||
idf_component_register(SRCS ${SOURCES}
|
||||
INCLUDE_DIRS "."
|
||||
EMBED_TXTFILES "assets/wifi_configuration_ap.html"
|
||||
)
|
22
main/Kconfig.projbuild
Normal file
22
main/Kconfig.projbuild
Normal file
@ -0,0 +1,22 @@
|
||||
menu "Xiaozhi Assistant"
|
||||
|
||||
config WEBSOCKET_URL
|
||||
string "Websocket URL"
|
||||
default "ws://"
|
||||
help
|
||||
Communication with the server through websocket after wake up.
|
||||
|
||||
config WEBSOCKET_ACCESS_TOKEN
|
||||
string "Websocket Access Token"
|
||||
default ""
|
||||
help
|
||||
Access token for websocket communication.
|
||||
|
||||
|
||||
config BUILTIN_LED_GPIO
|
||||
int "Builtin LED GPIO"
|
||||
default 48
|
||||
help
|
||||
GPIO number of the builtin LED.
|
||||
|
||||
endmenu
|
69
main/OpusEncoder.cc
Normal file
69
main/OpusEncoder.cc
Normal file
@ -0,0 +1,69 @@
|
||||
#include "OpusEncoder.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#define TAG "OpusEncoder"
|
||||
|
||||
OpusEncoder::OpusEncoder() {
|
||||
}
|
||||
|
||||
OpusEncoder::~OpusEncoder() {
|
||||
if (out_buffer_ != nullptr) {
|
||||
free(out_buffer_);
|
||||
}
|
||||
|
||||
if (audio_enc_ != nullptr) {
|
||||
opus_encoder_destroy(audio_enc_);
|
||||
}
|
||||
}
|
||||
|
||||
void OpusEncoder::Configure(int sample_rate, int channels, int duration_ms) {
|
||||
if (audio_enc_ != nullptr) {
|
||||
opus_encoder_destroy(audio_enc_);
|
||||
audio_enc_ = nullptr;
|
||||
}
|
||||
if (out_buffer_ != nullptr) {
|
||||
free(out_buffer_);
|
||||
out_buffer_ = nullptr;
|
||||
}
|
||||
|
||||
int error;
|
||||
audio_enc_ = opus_encoder_create(sample_rate, channels, OPUS_APPLICATION_VOIP, &error);
|
||||
if (audio_enc_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create audio encoder, error code: %d", error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set DTX
|
||||
opus_encoder_ctl(audio_enc_, OPUS_SET_DTX(1));
|
||||
// Set complexity to 5
|
||||
opus_encoder_ctl(audio_enc_, OPUS_SET_COMPLEXITY(5));
|
||||
|
||||
frame_size_ = sample_rate / 1000 * duration_ms;
|
||||
out_size_ = sample_rate * channels * sizeof(int16_t);
|
||||
out_buffer_ = (uint8_t*)malloc(out_size_);
|
||||
assert(out_buffer_ != nullptr);
|
||||
}
|
||||
|
||||
void OpusEncoder::Encode(const void* pcm, size_t pcm_len, std::function<void(const void*, size_t)> handler) {
|
||||
if (audio_enc_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Audio encoder is not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
in_buffer_.append((const char*)pcm, pcm_len);
|
||||
|
||||
while (in_buffer_.size() >= frame_size_ * sizeof(int16_t)) {
|
||||
auto ret = opus_encode(audio_enc_, (const opus_int16*)in_buffer_.data(), frame_size_, out_buffer_, out_size_);
|
||||
if (ret < 0) {
|
||||
ESP_LOGE(TAG, "Failed to encode audio, error code: %ld", ret);
|
||||
return;
|
||||
}
|
||||
|
||||
if (handler != nullptr) {
|
||||
handler(out_buffer_, ret);
|
||||
}
|
||||
|
||||
in_buffer_.erase(0, frame_size_ * sizeof(int16_t));
|
||||
}
|
||||
}
|
25
main/OpusEncoder.h
Normal file
25
main/OpusEncoder.h
Normal file
@ -0,0 +1,25 @@
|
||||
#ifndef _OPUS_ENCODER_H_
|
||||
#define _OPUS_ENCODER_H_
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include "opus.h"
|
||||
|
||||
class OpusEncoder {
|
||||
public:
|
||||
OpusEncoder();
|
||||
~OpusEncoder();
|
||||
|
||||
void Configure(int sample_rate, int channels, int duration_ms = 60);
|
||||
void Encode(const void* pcm, size_t pcm_len, std::function<void(const void*, size_t)> handler);
|
||||
bool IsBufferEmpty() const { return in_buffer_.empty(); }
|
||||
|
||||
private:
|
||||
struct OpusEncoder* audio_enc_ = nullptr;
|
||||
int frame_size_;
|
||||
int out_size_;
|
||||
uint8_t* out_buffer_ = nullptr;
|
||||
std::string in_buffer_;
|
||||
};
|
||||
|
||||
#endif // _OPUS_ENCODER_H_
|
211
main/SystemInfo.cc
Normal file
211
main/SystemInfo.cc
Normal file
@ -0,0 +1,211 @@
|
||||
#include "SystemInfo.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_flash.h"
|
||||
#include "esp_mac.h"
|
||||
#include "esp_chip_info.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_partition.h"
|
||||
#include "esp_app_desc.h"
|
||||
#include "esp_psram.h"
|
||||
|
||||
|
||||
#define TAG "SystemInfo"
|
||||
|
||||
size_t SystemInfo::GetFlashSize() {
|
||||
uint32_t flash_size;
|
||||
if (esp_flash_get_size(NULL, &flash_size) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to get flash size");
|
||||
return 0;
|
||||
}
|
||||
return (size_t)flash_size;
|
||||
}
|
||||
|
||||
size_t SystemInfo::GetMinimumFreeHeapSize() {
|
||||
return esp_get_minimum_free_heap_size();
|
||||
}
|
||||
|
||||
size_t SystemInfo::GetFreeHeapSize() {
|
||||
return esp_get_free_heap_size();
|
||||
}
|
||||
|
||||
std::string SystemInfo::GetMacAddress() {
|
||||
uint8_t mac[6];
|
||||
esp_read_mac(mac, ESP_MAC_WIFI_STA);
|
||||
char mac_str[18];
|
||||
snprintf(mac_str, sizeof(mac_str), "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
return std::string(mac_str);
|
||||
}
|
||||
|
||||
std::string SystemInfo::GetChipModelName() {
|
||||
return std::string(CONFIG_IDF_TARGET);
|
||||
}
|
||||
|
||||
std::string SystemInfo::GetJsonString() {
|
||||
/*
|
||||
{
|
||||
"flash_size": 4194304,
|
||||
"psram_size": 0,
|
||||
"minimum_free_heap_size": 123456,
|
||||
"mac_address": "00:00:00:00:00:00",
|
||||
"chip_model_name": "esp32s3",
|
||||
"chip_info": {
|
||||
"model": 1,
|
||||
"cores": 2,
|
||||
"revision": 0,
|
||||
"features": 0
|
||||
},
|
||||
"application": {
|
||||
"name": "my-app",
|
||||
"version": "1.0.0",
|
||||
"compile_time": "2021-01-01T00:00:00Z"
|
||||
"idf_version": "4.2-dev"
|
||||
"elf_sha256": ""
|
||||
},
|
||||
"partition_table": {
|
||||
"app": {
|
||||
"label": "app",
|
||||
"type": 1,
|
||||
"subtype": 2,
|
||||
"address": 0x10000,
|
||||
"size": 0x100000
|
||||
},
|
||||
}
|
||||
}
|
||||
*/
|
||||
std::string json = "{";
|
||||
json += "\"flash_size\":" + std::to_string(GetFlashSize()) + ",";
|
||||
json += "\"psram_size\":" + std::to_string(esp_psram_get_size()) + ",";
|
||||
json += "\"minimum_free_heap_size\":" + std::to_string(GetMinimumFreeHeapSize()) + ",";
|
||||
json += "\"mac_address\":\"" + GetMacAddress() + "\",";
|
||||
json += "\"chip_model_name\":\"" + GetChipModelName() + "\",";
|
||||
json += "\"chip_info\":{";
|
||||
|
||||
esp_chip_info_t chip_info;
|
||||
esp_chip_info(&chip_info);
|
||||
json += "\"model\":" + std::to_string(chip_info.model) + ",";
|
||||
json += "\"cores\":" + std::to_string(chip_info.cores) + ",";
|
||||
json += "\"revision\":" + std::to_string(chip_info.revision) + ",";
|
||||
json += "\"features\":" + std::to_string(chip_info.features);
|
||||
json += "},";
|
||||
|
||||
json += "\"application\":{";
|
||||
auto app_desc = esp_app_get_description();
|
||||
json += "\"name\":\"" + std::string(app_desc->project_name) + "\",";
|
||||
json += "\"version\":\"" + std::string(app_desc->version) + "\",";
|
||||
json += "\"compile_time\":\"" + std::string(app_desc->date) + "T" + std::string(app_desc->time) + "Z\",";
|
||||
json += "\"idf_version\":\"" + std::string(app_desc->idf_ver) + "\",";
|
||||
|
||||
char sha256_str[65];
|
||||
for (int i = 0; i < 32; i++) {
|
||||
snprintf(sha256_str + i * 2, sizeof(sha256_str) - i * 2, "%02x", app_desc->app_elf_sha256[i]);
|
||||
}
|
||||
json += "\"elf_sha256\":\"" + std::string(sha256_str) + "\"";
|
||||
json += "},";
|
||||
|
||||
json += "\"partition_table\": [";
|
||||
esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL);
|
||||
while (it) {
|
||||
const esp_partition_t *partition = esp_partition_get(it);
|
||||
json += "{";
|
||||
json += "\"label\":\"" + std::string(partition->label) + "\",";
|
||||
json += "\"type\":" + std::to_string(partition->type) + ",";
|
||||
json += "\"subtype\":" + std::to_string(partition->subtype) + ",";
|
||||
json += "\"address\":" + std::to_string(partition->address) + ",";
|
||||
json += "\"size\":" + std::to_string(partition->size);
|
||||
json += "},";
|
||||
it = esp_partition_next(it);
|
||||
}
|
||||
json.pop_back(); // Remove the last comma
|
||||
json += "]";
|
||||
|
||||
// Close the JSON object
|
||||
json += "}";
|
||||
return json;
|
||||
}
|
||||
|
||||
esp_err_t SystemInfo::PrintRealTimeStats(TickType_t xTicksToWait) {
|
||||
#define ARRAY_SIZE_OFFSET 5
|
||||
TaskStatus_t *start_array = NULL, *end_array = NULL;
|
||||
UBaseType_t start_array_size, end_array_size;
|
||||
configRUN_TIME_COUNTER_TYPE start_run_time, end_run_time;
|
||||
esp_err_t ret;
|
||||
uint32_t total_elapsed_time;
|
||||
|
||||
//Allocate array to store current task states
|
||||
start_array_size = uxTaskGetNumberOfTasks() + ARRAY_SIZE_OFFSET;
|
||||
start_array = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * start_array_size);
|
||||
if (start_array == NULL) {
|
||||
ret = ESP_ERR_NO_MEM;
|
||||
goto exit;
|
||||
}
|
||||
//Get current task states
|
||||
start_array_size = uxTaskGetSystemState(start_array, start_array_size, &start_run_time);
|
||||
if (start_array_size == 0) {
|
||||
ret = ESP_ERR_INVALID_SIZE;
|
||||
goto exit;
|
||||
}
|
||||
|
||||
vTaskDelay(xTicksToWait);
|
||||
|
||||
//Allocate array to store tasks states post delay
|
||||
end_array_size = uxTaskGetNumberOfTasks() + ARRAY_SIZE_OFFSET;
|
||||
end_array = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * end_array_size);
|
||||
if (end_array == NULL) {
|
||||
ret = ESP_ERR_NO_MEM;
|
||||
goto exit;
|
||||
}
|
||||
//Get post delay task states
|
||||
end_array_size = uxTaskGetSystemState(end_array, end_array_size, &end_run_time);
|
||||
if (end_array_size == 0) {
|
||||
ret = ESP_ERR_INVALID_SIZE;
|
||||
goto exit;
|
||||
}
|
||||
|
||||
//Calculate total_elapsed_time in units of run time stats clock period.
|
||||
total_elapsed_time = (end_run_time - start_run_time);
|
||||
if (total_elapsed_time == 0) {
|
||||
ret = ESP_ERR_INVALID_STATE;
|
||||
goto exit;
|
||||
}
|
||||
|
||||
printf("| Task | Run Time | Percentage\n");
|
||||
//Match each task in start_array to those in the end_array
|
||||
for (int i = 0; i < start_array_size; i++) {
|
||||
int k = -1;
|
||||
for (int j = 0; j < end_array_size; j++) {
|
||||
if (start_array[i].xHandle == end_array[j].xHandle) {
|
||||
k = j;
|
||||
//Mark that task have been matched by overwriting their handles
|
||||
start_array[i].xHandle = NULL;
|
||||
end_array[j].xHandle = NULL;
|
||||
break;
|
||||
}
|
||||
}
|
||||
//Check if matching task found
|
||||
if (k >= 0) {
|
||||
uint32_t task_elapsed_time = end_array[k].ulRunTimeCounter - start_array[i].ulRunTimeCounter;
|
||||
uint32_t percentage_time = (task_elapsed_time * 100UL) / (total_elapsed_time * CONFIG_FREERTOS_NUMBER_OF_CORES);
|
||||
printf("| %-16s | %8lu | %4lu%%\n", start_array[i].pcTaskName, task_elapsed_time, percentage_time);
|
||||
}
|
||||
}
|
||||
|
||||
//Print unmatched tasks
|
||||
for (int i = 0; i < start_array_size; i++) {
|
||||
if (start_array[i].xHandle != NULL) {
|
||||
printf("| %s | Deleted\n", start_array[i].pcTaskName);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < end_array_size; i++) {
|
||||
if (end_array[i].xHandle != NULL) {
|
||||
printf("| %s | Created\n", end_array[i].pcTaskName);
|
||||
}
|
||||
}
|
||||
ret = ESP_OK;
|
||||
|
||||
exit: //Common return path
|
||||
free(start_array);
|
||||
free(end_array);
|
||||
return ret;
|
||||
}
|
||||
|
20
main/SystemInfo.h
Normal file
20
main/SystemInfo.h
Normal file
@ -0,0 +1,20 @@
|
||||
#ifndef _SYSTEM_INFO_H_
|
||||
#define _SYSTEM_INFO_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
|
||||
class SystemInfo {
|
||||
public:
|
||||
static size_t GetFlashSize();
|
||||
static size_t GetMinimumFreeHeapSize();
|
||||
static size_t GetFreeHeapSize();
|
||||
static std::string GetMacAddress();
|
||||
static std::string GetChipModelName();
|
||||
static std::string GetJsonString();
|
||||
static esp_err_t PrintRealTimeStats(TickType_t xTicksToWait);
|
||||
};
|
||||
|
||||
#endif // _SYSTEM_INFO_H_
|
130
main/WebSocketClient.cc
Normal file
130
main/WebSocketClient.cc
Normal file
@ -0,0 +1,130 @@
|
||||
#include "WebSocketClient.h"
|
||||
#include <cstring>
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
|
||||
#define TAG "WebSocket"
|
||||
#define TIMEOUT_TICKS pdMS_TO_TICKS(3000)
|
||||
|
||||
WebSocketClient::WebSocketClient(bool auto_reconnect) {
|
||||
event_group_ = xEventGroupCreate();
|
||||
|
||||
esp_websocket_client_config_t config = {};
|
||||
config.task_prio = 1;
|
||||
config.disable_auto_reconnect = !auto_reconnect;
|
||||
client_ = esp_websocket_client_init(&config);
|
||||
assert(client_ != NULL);
|
||||
|
||||
esp_websocket_register_events(client_, WEBSOCKET_EVENT_ANY, [](void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
|
||||
WebSocketClient* ws = (WebSocketClient*)arg;
|
||||
esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data;
|
||||
switch (event_id)
|
||||
{
|
||||
case WEBSOCKET_EVENT_BEFORE_CONNECT:
|
||||
break;
|
||||
case WEBSOCKET_EVENT_CONNECTED:
|
||||
if (ws->on_connected_) {
|
||||
ws->on_connected_();
|
||||
}
|
||||
xEventGroupSetBits(ws->event_group_, WEBSOCKET_CONNECTED_BIT);
|
||||
break;
|
||||
case WEBSOCKET_EVENT_DISCONNECTED:
|
||||
xEventGroupSetBits(ws->event_group_, WEBSOCKET_DISCONNECTED_BIT);
|
||||
break;
|
||||
case WEBSOCKET_EVENT_DATA:
|
||||
if (data->data_len != data->payload_len) {
|
||||
ESP_LOGE(TAG, "Payload segmentating is not supported, data_len: %d, payload_len: %d", data->data_len, data->payload_len);
|
||||
break;
|
||||
}
|
||||
if (data->op_code == 8) { // Websocket close
|
||||
ESP_LOGI(TAG, "Websocket closed");
|
||||
if (ws->on_closed_) {
|
||||
ws->on_closed_();
|
||||
}
|
||||
} else if (data->op_code == 9) {
|
||||
// Websocket ping
|
||||
} else if (data->op_code == 10) {
|
||||
// Websocket pong
|
||||
} else if (data->op_code == 1) {
|
||||
// Websocket text
|
||||
if (ws->on_data_) {
|
||||
ws->on_data_(data->data_ptr, data->data_len, false);
|
||||
}
|
||||
} else if (data->op_code == 2) {
|
||||
// Websocket binary
|
||||
if (ws->on_data_) {
|
||||
ws->on_data_(data->data_ptr, data->data_len, true);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Unknown opcode: %d", data->op_code);
|
||||
}
|
||||
break;
|
||||
case WEBSOCKET_EVENT_ERROR:
|
||||
if (ws->on_error_) {
|
||||
ws->on_error_(data->error_handle.error_type);
|
||||
}
|
||||
xEventGroupSetBits(ws->event_group_, WEBSOCKET_ERROR_BIT);
|
||||
break;
|
||||
case WEBSOCKET_EVENT_CLOSED:
|
||||
break;
|
||||
default:
|
||||
ESP_LOGI(TAG, "Event %ld", event_id);
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
|
||||
WebSocketClient::~WebSocketClient() {
|
||||
esp_websocket_client_close(client_, TIMEOUT_TICKS);
|
||||
ESP_LOGI(TAG, "Destroying websocket client");
|
||||
esp_websocket_client_destroy(client_);
|
||||
}
|
||||
|
||||
void WebSocketClient::SetHeader(const char* key, const char* value) {
|
||||
esp_websocket_client_append_header(client_, key, value);
|
||||
}
|
||||
|
||||
bool WebSocketClient::Connect(const char* uri) {
|
||||
esp_websocket_client_set_uri(client_, uri);
|
||||
esp_websocket_client_start(client_);
|
||||
|
||||
// Wait for the connection to be established or an error
|
||||
EventBits_t bits = xEventGroupWaitBits(event_group_, WEBSOCKET_CONNECTED_BIT | WEBSOCKET_ERROR_BIT, pdFALSE, pdFALSE, TIMEOUT_TICKS);
|
||||
return bits & WEBSOCKET_CONNECTED_BIT;
|
||||
}
|
||||
|
||||
void WebSocketClient::Send(const char* data, size_t len, bool binary) {
|
||||
if (binary) {
|
||||
esp_websocket_client_send_bin(client_, data, len, portMAX_DELAY);
|
||||
} else {
|
||||
esp_websocket_client_send_text(client_, data, len, portMAX_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketClient::Send(const std::string& data) {
|
||||
Send(data.c_str(), data.size(), false);
|
||||
}
|
||||
|
||||
void WebSocketClient::OnClosed(std::function<void()> callback) {
|
||||
on_closed_ = callback;
|
||||
}
|
||||
|
||||
void WebSocketClient::OnData(std::function<void(const char*, size_t, bool binary)> callback) {
|
||||
on_data_ = callback;
|
||||
}
|
||||
|
||||
void WebSocketClient::OnError(std::function<void(int)> callback) {
|
||||
on_error_ = callback;
|
||||
}
|
||||
|
||||
void WebSocketClient::OnConnected(std::function<void()> callback) {
|
||||
on_connected_ = callback;
|
||||
}
|
||||
|
||||
void WebSocketClient::OnDisconnected(std::function<void()> callback) {
|
||||
on_disconnected_ = callback;
|
||||
}
|
||||
|
||||
bool WebSocketClient::IsConnected() const {
|
||||
return esp_websocket_client_is_connected(client_);
|
||||
}
|
40
main/WebSocketClient.h
Normal file
40
main/WebSocketClient.h
Normal file
@ -0,0 +1,40 @@
|
||||
#ifndef _WEBSOCKET_CLIENT_H_
|
||||
#define _WEBSOCKET_CLIENT_H_
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include "esp_websocket_client.h"
|
||||
#include "freertos/event_groups.h"
|
||||
|
||||
#define WEBSOCKET_CONNECTED_BIT BIT0
|
||||
#define WEBSOCKET_DISCONNECTED_BIT BIT1
|
||||
#define WEBSOCKET_ERROR_BIT BIT2
|
||||
|
||||
class WebSocketClient {
|
||||
public:
|
||||
WebSocketClient(bool auto_reconnect = false);
|
||||
~WebSocketClient();
|
||||
|
||||
void SetHeader(const char* key, const char* value);
|
||||
bool IsConnected() const;
|
||||
bool Connect(const char* uri);
|
||||
void Send(const std::string& data);
|
||||
void Send(const char* data, size_t len, bool binary = false);
|
||||
|
||||
void OnConnected(std::function<void()> callback);
|
||||
void OnDisconnected(std::function<void()> callback);
|
||||
void OnData(std::function<void(const char*, size_t, bool binary)> callback);
|
||||
void OnError(std::function<void(int)> callback);
|
||||
void OnClosed(std::function<void()> callback);
|
||||
|
||||
private:
|
||||
esp_websocket_client_handle_t client_ = NULL;
|
||||
EventGroupHandle_t event_group_;
|
||||
std::function<void(const char*, size_t, bool binary)> on_data_;
|
||||
std::function<void(int)> on_error_;
|
||||
std::function<void()> on_closed_;
|
||||
std::function<void()> on_connected_;
|
||||
std::function<void()> on_disconnected_;
|
||||
};
|
||||
|
||||
#endif // _WEBSOCKET_CLIENT_H_
|
259
main/WifiConfigurationAp.cc
Normal file
259
main/WifiConfigurationAp.cc
Normal file
@ -0,0 +1,259 @@
|
||||
#include "WifiConfigurationAp.h"
|
||||
#include <cstdio>
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_mac.h"
|
||||
#include "esp_netif.h"
|
||||
#include "lwip/ip_addr.h"
|
||||
#include "nvs.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#define TAG "WifiConfigurationAp"
|
||||
|
||||
extern const char index_html_start[] asm("_binary_wifi_configuration_ap_html_start");
|
||||
|
||||
#define WIFI_CONNECTED_BIT BIT0
|
||||
#define WIFI_FAIL_BIT BIT1
|
||||
|
||||
|
||||
WifiConfigurationAp::WifiConfigurationAp()
|
||||
{
|
||||
event_group_ = xEventGroupCreate();
|
||||
}
|
||||
|
||||
std::string WifiConfigurationAp::GetSsid()
|
||||
{
|
||||
// Get MAC and use it to generate a unique SSID
|
||||
uint8_t mac[6];
|
||||
ESP_ERROR_CHECK(esp_read_mac(mac, ESP_MAC_WIFI_SOFTAP));
|
||||
char ssid[32];
|
||||
snprintf(ssid, sizeof(ssid), "ESP32-%02X%02X%02X", mac[3], mac[4], mac[5]);
|
||||
return std::string(ssid);
|
||||
}
|
||||
|
||||
void WifiConfigurationAp::StartAccessPoint()
|
||||
{
|
||||
// Get the SSID
|
||||
std::string ssid = GetSsid();
|
||||
|
||||
// Register the WiFi event handler
|
||||
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
|
||||
[](void *ctx, esp_event_base_t event_base, int32_t event_id, void *event_data) {
|
||||
if (event_id == WIFI_EVENT_AP_STACONNECTED) {
|
||||
wifi_event_ap_staconnected_t *event = (wifi_event_ap_staconnected_t *)event_data;
|
||||
ESP_LOGI(TAG, "Station connected: " MACSTR, MAC2STR(event->mac));
|
||||
} else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
|
||||
wifi_event_ap_stadisconnected_t *event = (wifi_event_ap_stadisconnected_t *)event_data;
|
||||
ESP_LOGI(TAG, "Station disconnected: " MACSTR, MAC2STR(event->mac));
|
||||
} else if (event_id == WIFI_EVENT_STA_CONNECTED) {
|
||||
xEventGroupSetBits(static_cast<WifiConfigurationAp *>(ctx)->event_group_, WIFI_CONNECTED_BIT);
|
||||
} else if (event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
xEventGroupSetBits(static_cast<WifiConfigurationAp *>(ctx)->event_group_, WIFI_FAIL_BIT);
|
||||
}
|
||||
}, this));
|
||||
|
||||
// Initialize the TCP/IP stack
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
|
||||
// Create the default event loop
|
||||
auto netif = esp_netif_create_default_wifi_ap();
|
||||
|
||||
// Set the router IP address to 192.168.4.1
|
||||
esp_netif_ip_info_t ip_info;
|
||||
IP4_ADDR(&ip_info.ip, 192, 168, 4, 1);
|
||||
IP4_ADDR(&ip_info.gw, 192, 168, 4, 1);
|
||||
IP4_ADDR(&ip_info.netmask, 255, 255, 255, 0);
|
||||
esp_netif_dhcps_stop(netif);
|
||||
esp_netif_set_ip_info(netif, &ip_info);
|
||||
esp_netif_dhcps_start(netif);
|
||||
|
||||
// Initialize the WiFi stack in Access Point mode
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
// Set the WiFi configuration
|
||||
wifi_config_t wifi_config = {};
|
||||
strcpy((char *)wifi_config.ap.ssid, ssid.c_str());
|
||||
wifi_config.ap.ssid_len = ssid.length();
|
||||
wifi_config.ap.max_connection = 4;
|
||||
wifi_config.ap.authmode = WIFI_AUTH_OPEN;
|
||||
|
||||
// Start the WiFi Access Point
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
ESP_LOGI(TAG, "Access Point started with SSID %s", ssid.c_str());
|
||||
}
|
||||
|
||||
void WifiConfigurationAp::StartWebServer()
|
||||
{
|
||||
// Start the web server
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.uri_match_fn = httpd_uri_match_wildcard;
|
||||
ESP_ERROR_CHECK(httpd_start(&server_, &config));
|
||||
|
||||
// Register the index.html file
|
||||
httpd_uri_t index_html = {
|
||||
.uri = "/",
|
||||
.method = HTTP_GET,
|
||||
.handler = [](httpd_req_t *req) -> esp_err_t {
|
||||
httpd_resp_send(req, index_html_start, strlen(index_html_start));
|
||||
return ESP_OK;
|
||||
},
|
||||
.user_ctx = NULL
|
||||
};
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(server_, &index_html));
|
||||
|
||||
// Register the /scan URI
|
||||
httpd_uri_t scan = {
|
||||
.uri = "/scan",
|
||||
.method = HTTP_GET,
|
||||
.handler = [](httpd_req_t *req) -> esp_err_t {
|
||||
esp_wifi_scan_start(nullptr, true);
|
||||
uint16_t ap_num = 0;
|
||||
esp_wifi_scan_get_ap_num(&ap_num);
|
||||
wifi_ap_record_t *ap_records = (wifi_ap_record_t *)malloc(ap_num * sizeof(wifi_ap_record_t));
|
||||
esp_wifi_scan_get_ap_records(&ap_num, ap_records);
|
||||
|
||||
// Send the scan results as JSON
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_sendstr_chunk(req, "[");
|
||||
for (int i = 0; i < ap_num; i++) {
|
||||
ESP_LOGI(TAG, "SSID: %s, RSSI: %d, Authmode: %d",
|
||||
(char *)ap_records[i].ssid, ap_records[i].rssi, ap_records[i].authmode);
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "{\"ssid\":\"%s\",\"rssi\":%d,\"authmode\":%d}",
|
||||
(char *)ap_records[i].ssid, ap_records[i].rssi, ap_records[i].authmode);
|
||||
httpd_resp_sendstr_chunk(req, buf);
|
||||
if (i < ap_num - 1) {
|
||||
httpd_resp_sendstr_chunk(req, ",");
|
||||
}
|
||||
}
|
||||
httpd_resp_sendstr_chunk(req, "]");
|
||||
httpd_resp_sendstr_chunk(req, NULL);
|
||||
free(ap_records);
|
||||
return ESP_OK;
|
||||
},
|
||||
.user_ctx = NULL
|
||||
};
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(server_, &scan));
|
||||
|
||||
// Register the form submission
|
||||
httpd_uri_t form_submit = {
|
||||
.uri = "/submit",
|
||||
.method = HTTP_POST,
|
||||
.handler = [](httpd_req_t *req) -> esp_err_t {
|
||||
char buf[128];
|
||||
int ret = httpd_req_recv(req, buf, sizeof(buf));
|
||||
if (ret <= 0) {
|
||||
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
|
||||
httpd_resp_send_408(req);
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
buf[ret] = '\0';
|
||||
ESP_LOGI(TAG, "Received form data: %s", buf);
|
||||
|
||||
// Parse the form data
|
||||
char ssid[32], password[64];
|
||||
if (sscanf(buf, "ssid=%32[^&]&password=%64s", ssid, password) != 2) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid form data");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// Get this object from the user context
|
||||
auto *this_ = static_cast<WifiConfigurationAp *>(req->user_ctx);
|
||||
if (!this_->ConnectToWifi(ssid, password)) {
|
||||
char error[] = "Failed to connect to WiFi";
|
||||
char location[128];
|
||||
snprintf(location, sizeof(location), "/?error=%s&ssid=%s", error, ssid);
|
||||
|
||||
httpd_resp_set_status(req, "302 Found");
|
||||
httpd_resp_set_hdr(req, "Location", location);
|
||||
httpd_resp_send(req, NULL, 0);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// Set HTML response
|
||||
httpd_resp_set_status(req, "200 OK");
|
||||
httpd_resp_set_type(req, "text/html");
|
||||
httpd_resp_send(req, "<h1>Done!</h1>", -1);
|
||||
|
||||
this_->Save(ssid, password);
|
||||
return ESP_OK;
|
||||
},
|
||||
.user_ctx = this
|
||||
};
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(server_, &form_submit));
|
||||
|
||||
ESP_LOGI(TAG, "Web server started");
|
||||
}
|
||||
|
||||
void WifiConfigurationAp::Start()
|
||||
{
|
||||
builtin_led_.SetBlue();
|
||||
builtin_led_.Blink(1000, 500);
|
||||
|
||||
StartAccessPoint();
|
||||
StartWebServer();
|
||||
}
|
||||
|
||||
bool WifiConfigurationAp::ConnectToWifi(const std::string &ssid, const std::string &password)
|
||||
{
|
||||
// auto esp_netif = esp_netif_create_default_wifi_sta();
|
||||
|
||||
wifi_config_t wifi_config;
|
||||
bzero(&wifi_config, sizeof(wifi_config));
|
||||
strcpy((char *)wifi_config.sta.ssid, ssid.c_str());
|
||||
strcpy((char *)wifi_config.sta.password, password.c_str());
|
||||
wifi_config.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
|
||||
wifi_config.sta.failure_retry_cnt = 1;
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
auto ret = esp_wifi_connect();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to connect to WiFi: %d", ret);
|
||||
return false;
|
||||
}
|
||||
ESP_LOGI(TAG, "Connecting to WiFi %s", ssid.c_str());
|
||||
|
||||
// Wait for the connection to complete for 5 seconds
|
||||
EventBits_t bits = xEventGroupWaitBits(event_group_, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdTRUE, pdFALSE, pdMS_TO_TICKS(10000));
|
||||
if (bits & WIFI_CONNECTED_BIT) {
|
||||
ESP_LOGI(TAG, "Connected to WiFi %s", ssid.c_str());
|
||||
return true;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to connect to WiFi %s", ssid.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void WifiConfigurationAp::Save(const std::string &ssid, const std::string &password)
|
||||
{
|
||||
// Open the NVS flash
|
||||
nvs_handle_t nvs_handle;
|
||||
ESP_ERROR_CHECK(nvs_open("wifi", NVS_READWRITE, &nvs_handle));
|
||||
|
||||
// Write the SSID and password to the NVS flash
|
||||
ESP_ERROR_CHECK(nvs_set_str(nvs_handle, "ssid", ssid.c_str()));
|
||||
ESP_ERROR_CHECK(nvs_set_str(nvs_handle, "password", password.c_str()));
|
||||
|
||||
// Commit the changes
|
||||
ESP_ERROR_CHECK(nvs_commit(nvs_handle));
|
||||
|
||||
// Close the NVS flash
|
||||
nvs_close(nvs_handle);
|
||||
|
||||
ESP_LOGI(TAG, "WiFi configuration saved");
|
||||
// Use xTaskCreate to create a new task that restarts the ESP32
|
||||
xTaskCreate([](void *ctx) {
|
||||
ESP_LOGI(TAG, "Restarting the ESP32 in 3 second");
|
||||
vTaskDelay(pdMS_TO_TICKS(3000));
|
||||
esp_restart();
|
||||
}, "restart_task", 4096, NULL, 5, NULL);
|
||||
}
|
25
main/WifiConfigurationAp.h
Normal file
25
main/WifiConfigurationAp.h
Normal file
@ -0,0 +1,25 @@
|
||||
#ifndef _WIFI_CONFIGURATION_AP_H_
|
||||
#define _WIFI_CONFIGURATION_AP_H_
|
||||
|
||||
#include <string>
|
||||
#include "esp_http_server.h"
|
||||
#include "BuiltinLed.h"
|
||||
|
||||
class WifiConfigurationAp {
|
||||
public:
|
||||
WifiConfigurationAp();
|
||||
void Start();
|
||||
|
||||
private:
|
||||
BuiltinLed builtin_led_;
|
||||
httpd_handle_t server_ = NULL;
|
||||
EventGroupHandle_t event_group_;
|
||||
|
||||
std::string GetSsid();
|
||||
void StartAccessPoint();
|
||||
void StartWebServer();
|
||||
bool ConnectToWifi(const std::string &ssid, const std::string &password);
|
||||
void Save(const std::string &ssid, const std::string &password);
|
||||
};
|
||||
|
||||
#endif // _WIFI_CONFIGURATION_AP_H_
|
100
main/WifiStation.cc
Normal file
100
main/WifiStation.cc
Normal file
@ -0,0 +1,100 @@
|
||||
#include "WifiStation.h"
|
||||
#include <cstring>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "nvs.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_system.h"
|
||||
|
||||
|
||||
#define TAG "wifi"
|
||||
#define WIFI_EVENT_CONNECTED BIT0
|
||||
#define WIFI_EVENT_FAILED BIT1
|
||||
#define MAX_RECONNECT_COUNT 5
|
||||
|
||||
WifiStation::WifiStation() {
|
||||
// Get ssid and password from NVS
|
||||
nvs_handle_t nvs_handle;
|
||||
ESP_ERROR_CHECK(nvs_open("wifi", NVS_READONLY, &nvs_handle));
|
||||
char ssid[32], password[64];
|
||||
size_t length = sizeof(ssid);
|
||||
ESP_ERROR_CHECK(nvs_get_str(nvs_handle, "ssid", ssid, &length));
|
||||
length = sizeof(password);
|
||||
ESP_ERROR_CHECK(nvs_get_str(nvs_handle, "password", password, &length));
|
||||
nvs_close(nvs_handle);
|
||||
|
||||
ssid_ = std::string(ssid);
|
||||
password_ = std::string(password);
|
||||
|
||||
// Create the event group
|
||||
event_group_ = xEventGroupCreate();
|
||||
|
||||
// Register event handler
|
||||
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
|
||||
[](void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
|
||||
auto this_ = static_cast<WifiStation*>(event_handler_arg);
|
||||
if (event_id == WIFI_EVENT_STA_START) {
|
||||
esp_wifi_connect();
|
||||
} else if (event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
xEventGroupClearBits(this_->event_group_, WIFI_EVENT_CONNECTED);
|
||||
if (this_->reconnect_count_ < MAX_RECONNECT_COUNT) {
|
||||
esp_wifi_connect();
|
||||
this_->reconnect_count_++;
|
||||
ESP_LOGI(TAG, "Reconnecting to WiFi (attempt %d)", this_->reconnect_count_);
|
||||
} else {
|
||||
xEventGroupSetBits(this_->event_group_, WIFI_EVENT_FAILED);
|
||||
ESP_LOGI(TAG, "Failed to connect to WiFi");
|
||||
}
|
||||
}
|
||||
}, this));
|
||||
|
||||
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
|
||||
[](void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
|
||||
auto this_ = static_cast<WifiStation*>(event_handler_arg);
|
||||
auto event = static_cast<ip_event_got_ip_t*>(event_data);
|
||||
|
||||
char ip_address[16];
|
||||
esp_ip4addr_ntoa(&event->ip_info.ip, ip_address, sizeof(ip_address));
|
||||
this_->ip_address_ = ip_address;
|
||||
ESP_LOGI(TAG, "Got IP: %s", this_->ip_address_.c_str());
|
||||
xEventGroupSetBits(this_->event_group_, WIFI_EVENT_CONNECTED);
|
||||
}, this));
|
||||
}
|
||||
|
||||
|
||||
void WifiStation::Start() {
|
||||
// Initialize the TCP/IP stack
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
|
||||
// Create the default event loop
|
||||
esp_netif_create_default_wifi_sta();
|
||||
|
||||
// Initialize the WiFi stack in station mode
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||
|
||||
ESP_LOGI(TAG, "Connecting to WiFi ssid=%s password=%s", ssid_.c_str(), password_.c_str());
|
||||
wifi_config_t wifi_config;
|
||||
bzero(&wifi_config, sizeof(wifi_config));
|
||||
strcpy((char *)wifi_config.sta.ssid, ssid_.c_str());
|
||||
strcpy((char *)wifi_config.sta.password, password_.c_str());
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
|
||||
// Start the WiFi stack
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
// Wait for the WiFi stack to start
|
||||
auto bits = xEventGroupWaitBits(event_group_, WIFI_EVENT_CONNECTED | WIFI_EVENT_FAILED, pdFALSE, pdFALSE, portMAX_DELAY);
|
||||
if (bits & WIFI_EVENT_FAILED) {
|
||||
ESP_LOGE(TAG, "WifiStation start failed");
|
||||
} else {
|
||||
ESP_LOGI(TAG, "WifiStation started");
|
||||
}
|
||||
}
|
||||
|
||||
bool WifiStation::IsConnected() {
|
||||
return xEventGroupGetBits(event_group_) & WIFI_EVENT_CONNECTED;
|
||||
}
|
23
main/WifiStation.h
Normal file
23
main/WifiStation.h
Normal file
@ -0,0 +1,23 @@
|
||||
#ifndef _WIFI_STATION_H_
|
||||
#define _WIFI_STATION_H_
|
||||
|
||||
#include <string>
|
||||
#include "esp_event.h"
|
||||
|
||||
class WifiStation {
|
||||
public:
|
||||
WifiStation();
|
||||
void Start();
|
||||
bool IsConnected();
|
||||
std::string ssid() { return ssid_; }
|
||||
std::string ip_address() { return ip_address_; }
|
||||
|
||||
private:
|
||||
EventGroupHandle_t event_group_;
|
||||
std::string ssid_;
|
||||
std::string password_;
|
||||
std::string ip_address_;
|
||||
int reconnect_count_ = 0;
|
||||
};
|
||||
|
||||
#endif // _WIFI_STATION_H_
|
137
main/assets/wifi_configuration_ap.html
Normal file
137
main/assets/wifi_configuration_ap.html
Normal file
@ -0,0 +1,137 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>WiFi Configuration</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
<style type="text/css">
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-top: 50px;
|
||||
}
|
||||
form {
|
||||
width: 300px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
input[type="submit"] {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type="submit"]:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
input[type="submit"]:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#ap_list {
|
||||
margin-top: 20px;
|
||||
border-top: 1px solid #ccc;
|
||||
padding-top: 10px;
|
||||
}
|
||||
#ap_list a {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
#ap_list a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>WiFi Configuration</h1>
|
||||
<form action="/submit" method="post" onsubmit="button.disabled = true;">
|
||||
<p class="error" style="color: red; text-align: center;" id="error">
|
||||
</p>
|
||||
<p>
|
||||
<label for="ssid">SSID:</label>
|
||||
<input type="text" id="ssid" name="ssid" required>
|
||||
</p>
|
||||
<p>
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</p>
|
||||
<p style="text-align: center;">
|
||||
<input type="submit" value="Submit" id="button">
|
||||
</p>
|
||||
<p id="ap_list">
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<script type="text/javascript">
|
||||
const button = document.getElementById('button');
|
||||
const error = document.getElementById('error');
|
||||
const ssid = document.getElementById('ssid');
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.has('error')) {
|
||||
error.textContent = params.get('error');
|
||||
}
|
||||
if (params.has('ssid')) {
|
||||
ssid.value = params.get('ssid');
|
||||
}
|
||||
|
||||
// Load AP list from /scan
|
||||
function loadAPList() {
|
||||
if (button.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/scan')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const apList = document.getElementById('ap_list');
|
||||
apList.innerHTML = '<p>Select an AP from the list below: </p>';
|
||||
data.forEach(ap => {
|
||||
// Create a link for each AP
|
||||
const link = document.createElement('a');
|
||||
link.href = '#';
|
||||
link.textContent = ap.ssid + ' (' + ap.rssi + ' dBm)';
|
||||
if (ap.authmode === 0) {
|
||||
link.textContent += ' 🌐';
|
||||
} else {
|
||||
link.textContent += ' 🔒';
|
||||
}
|
||||
link.addEventListener('click', () => {
|
||||
ssid.value = ap.ssid;
|
||||
});
|
||||
apList.appendChild(link);
|
||||
});
|
||||
setTimeout(loadAPList, 5000);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
loadAPList();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
20
main/idf_component.yml
Normal file
20
main/idf_component.yml
Normal file
@ -0,0 +1,20 @@
|
||||
## IDF Component Manager Manifest File
|
||||
dependencies:
|
||||
78/esp-opus: "*"
|
||||
espressif/esp_websocket_client: "^1.2.3"
|
||||
espressif/led_strip: "*"
|
||||
espressif/esp-sr: "^1.9.0"
|
||||
## Required IDF version
|
||||
idf:
|
||||
version: ">=4.1.0"
|
||||
# # Put list of dependencies here
|
||||
# # For components maintained by Espressif:
|
||||
# component: "~1.0.0"
|
||||
# # For 3rd party components:
|
||||
# username/component: ">=1.0.0,<2.0.0"
|
||||
# username2/component2:
|
||||
# version: "~1.0.0"
|
||||
# # For transient dependencies `public` flag can be set.
|
||||
# # `public` flag doesn't have an effect dependencies of the `main` component.
|
||||
# # All dependencies of `main` are public by default.
|
||||
# public: true
|
66
main/main.cc
Executable file
66
main/main.cc
Executable file
@ -0,0 +1,66 @@
|
||||
#include <cstdio>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
#include "nvs.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "driver/gpio.h"
|
||||
|
||||
#include "WifiConfigurationAp.h"
|
||||
#include "Application.h"
|
||||
#include "SystemInfo.h"
|
||||
|
||||
#define TAG "main"
|
||||
#define STATS_TICKS pdMS_TO_TICKS(1000)
|
||||
|
||||
extern "C" void app_main(void)
|
||||
{
|
||||
// Initialize the default event loop
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
|
||||
// Configure GPIO1 as INPUT, reset NVS flash if the button is pressed
|
||||
gpio_config_t io_conf;
|
||||
io_conf.intr_type = GPIO_INTR_DISABLE;
|
||||
io_conf.mode = GPIO_MODE_INPUT;
|
||||
io_conf.pin_bit_mask = 1ULL << 1;
|
||||
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
|
||||
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
|
||||
gpio_config(&io_conf);
|
||||
|
||||
if (gpio_get_level(GPIO_NUM_1) == 0) {
|
||||
ESP_LOGI(TAG, "Button is pressed, reset NVS flash");
|
||||
nvs_flash_erase();
|
||||
}
|
||||
|
||||
// Initialize NVS flash for WiFi configuration
|
||||
esp_err_t ret = nvs_flash_init();
|
||||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
ret = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(ret);
|
||||
|
||||
// Get the WiFi configuration
|
||||
nvs_handle_t nvs_handle;
|
||||
ret = nvs_open("wifi", NVS_READONLY, &nvs_handle);
|
||||
|
||||
// If the WiFi configuration is not found, launch the WiFi configuration AP
|
||||
if (ret != ESP_OK) {
|
||||
auto app = new WifiConfigurationAp();
|
||||
app->Start();
|
||||
return;
|
||||
}
|
||||
nvs_close(nvs_handle);
|
||||
|
||||
// Otherwise, launch the application
|
||||
auto app = new Application();
|
||||
app->Start();
|
||||
|
||||
// Dump CPU usage every 1 second
|
||||
while (true) {
|
||||
vTaskDelay(2000 / portTICK_PERIOD_MS);
|
||||
SystemInfo::PrintRealTimeStats(STATS_TICKS);
|
||||
int free_sram = heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL);
|
||||
ESP_LOGI(TAG, "Free heap size: %u minimal internal: %u", SystemInfo::GetFreeHeapSize(), free_sram);
|
||||
}
|
||||
}
|
60
main/resampler_structs.h
Normal file
60
main/resampler_structs.h
Normal file
@ -0,0 +1,60 @@
|
||||
/***********************************************************************
|
||||
Copyright (c) 2006-2011, Skype Limited. All rights reserved.
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
- Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
- Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
- Neither the name of Internet Society, IETF or IETF Trust, nor the
|
||||
names of specific contributors, may be used to endorse or promote
|
||||
products derived from this software without specific prior written
|
||||
permission.
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
***********************************************************************/
|
||||
|
||||
#ifndef SILK_RESAMPLER_STRUCTS_H
|
||||
#define SILK_RESAMPLER_STRUCTS_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define SILK_RESAMPLER_MAX_FIR_ORDER 36
|
||||
#define SILK_RESAMPLER_MAX_IIR_ORDER 6
|
||||
|
||||
typedef struct _silk_resampler_state_struct{
|
||||
opus_int32 sIIR[ SILK_RESAMPLER_MAX_IIR_ORDER ]; /* this must be the first element of this struct */
|
||||
union{
|
||||
opus_int32 i32[ SILK_RESAMPLER_MAX_FIR_ORDER ];
|
||||
opus_int16 i16[ SILK_RESAMPLER_MAX_FIR_ORDER ];
|
||||
} sFIR;
|
||||
opus_int16 delayBuf[ 48 ];
|
||||
opus_int resampler_function;
|
||||
opus_int batchSize;
|
||||
opus_int32 invRatio_Q16;
|
||||
opus_int FIR_Order;
|
||||
opus_int FIR_Fracs;
|
||||
opus_int Fs_in_kHz;
|
||||
opus_int Fs_out_kHz;
|
||||
opus_int inputDelay;
|
||||
const opus_int16 *Coefs;
|
||||
} silk_resampler_state_struct;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* SILK_RESAMPLER_STRUCTS_H */
|
||||
|
25
main/silk_resampler.h
Normal file
25
main/silk_resampler.h
Normal file
@ -0,0 +1,25 @@
|
||||
#ifndef _SILK_RESAMPLER_H_
|
||||
#define _SILK_RESAMPLER_H_
|
||||
|
||||
|
||||
/*!
|
||||
* Initialize/reset the resampler state for a given pair of input/output sampling rates
|
||||
*/
|
||||
extern "C" opus_int silk_resampler_init(
|
||||
silk_resampler_state_struct *S, /* I/O Resampler state */
|
||||
opus_int32 Fs_Hz_in, /* I Input sampling rate (Hz) */
|
||||
opus_int32 Fs_Hz_out, /* I Output sampling rate (Hz) */
|
||||
opus_int forEnc /* I If 1: encoder; if 0: decoder */
|
||||
);
|
||||
|
||||
/*!
|
||||
* Resampler: convert from one sampling rate to another
|
||||
*/
|
||||
extern "C" opus_int silk_resampler(
|
||||
silk_resampler_state_struct *S, /* I/O Resampler state */
|
||||
opus_int16 out[], /* O Output signal */
|
||||
const opus_int16 in[], /* I Input signal */
|
||||
opus_int32 inLen /* I Number of input samples */
|
||||
);
|
||||
|
||||
#endif // _SILK_RESAMPLER_H_
|
9
partitions.csv
Normal file
9
partitions.csv
Normal file
@ -0,0 +1,9 @@
|
||||
# ESP-IDF Partition Table
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x4000,
|
||||
otadata, data, ota, 0xd000, 0x2000,
|
||||
phy_init, data, phy, 0xf000, 0x1000,
|
||||
factory, app, factory, 0x10000, 2M,
|
||||
ota_0, app, ota_0, , 2M,
|
||||
ota_1, app, ota_1, , 2M,
|
||||
model, data, spiffs, , 1M,
|
|
34
sdkconfig.defaults
Normal file
34
sdkconfig.defaults
Normal file
@ -0,0 +1,34 @@
|
||||
CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_PERF=y
|
||||
CONFIG_BOOTLOADER_LOG_LEVEL_NONE=y
|
||||
CONFIG_BOOTLOADER_SKIP_VALIDATE_ALWAYS=y
|
||||
|
||||
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
|
||||
CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB=y
|
||||
CONFIG_ESP32S3_DATA_CACHE_64KB=y
|
||||
CONFIG_ESP32S3_DATA_CACHE_LINE_64B=y
|
||||
|
||||
CONFIG_FLASHMODE_QIO=y
|
||||
|
||||
CONFIG_ESP32S3_SPIRAM_SUPPORT=y
|
||||
CONFIG_SPIRAM_MODE_OCT=y
|
||||
CONFIG_SPIRAM_SPEED_80M=y
|
||||
CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=4096
|
||||
CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y
|
||||
CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=16384
|
||||
CONFIG_SPIRAM_MEMTEST=n
|
||||
|
||||
CONFIG_HTTPD_MAX_REQ_HDR_LEN=2048
|
||||
CONFIG_HTTPD_MAX_URI_LEN=2048
|
||||
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
|
||||
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
|
||||
CONFIG_PARTITION_TABLE_OFFSET=0x8000
|
||||
|
||||
CONFIG_MODEL_IN_SPIFFS=y
|
||||
CONFIG_USE_WAKENET=y
|
||||
CONFIG_SR_WN_WN9_NIHAOXIAOZHI_TTS=y
|
||||
CONFIG_USE_MULTINET=n
|
||||
|
||||
CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y
|
||||
CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS=y
|
Reference in New Issue
Block a user