From d26e8d25ffd96181dcfef723b4e20971bc588819 Mon Sep 17 00:00:00 2001 From: Terrence Date: Tue, 1 Oct 2024 14:16:12 +0800 Subject: [PATCH] support ML307, new version 0.3.0 --- CMakeLists.txt | 2 +- README.md | 67 +++++------ docs/wiring2.jpg | Bin 0 -> 74192 bytes main/Application.cc | 201 ++++++++++++++++++++++++++----- main/Application.h | 25 +++- main/AudioDevice.cc | 16 +-- main/CMakeLists.txt | 3 + main/Display.cc | 139 +++++++++++++++++++++ main/Display.h | 32 +++++ main/FirmwareUpgrade.cc | 259 ++++++++++++++++++++++++++++++++++++++++ main/FirmwareUpgrade.h | 37 ++++++ main/Kconfig.projbuild | 89 ++++++++++---- main/SystemInfo.cc | 221 ++++++++++++++++++++++++++++++++++ main/SystemInfo.h | 20 ++++ main/idf_component.yml | 21 +--- main/main.cc | 19 +-- sdkconfig.defaults | 1 + 17 files changed, 1020 insertions(+), 132 deletions(-) create mode 100644 docs/wiring2.jpg create mode 100644 main/Display.cc create mode 100644 main/Display.h create mode 100644 main/FirmwareUpgrade.cc create mode 100644 main/FirmwareUpgrade.h create mode 100644 main/SystemInfo.cc create mode 100644 main/SystemInfo.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a27c231..3632661 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ # CMakeLists in this exact order for cmake to work correctly cmake_minimum_required(VERSION 3.16) -set(PROJECT_VER "0.2.0") +set(PROJECT_VER "0.3.0") include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(xiaozhi) diff --git a/README.md b/README.md index 9432657..cdb9321 100755 --- a/README.md +++ b/README.md @@ -2,50 +2,41 @@ BiliBili 视频介绍 [【ESP32+SenseVoice+Qwen72B打造你的AI聊天伴侣!】](https://www.bilibili.com/video/BV11msTenEH3/?share_source=copy_web&vd_source=ee1aafe19d6e60cf22e60a93881faeba) +这是虾哥的第一个硬件作品。 + +## 项目目的 + +本项目基于乐鑫的 ESP-IDF 进行开发。 + +本项目是一个开源项目,主要用于教学目的。我们希望通过这个项目,能够帮助更多人入门 AI 硬件开发,了解如何将当下飞速发展的大语言模型应用到实际的硬件设备中。无论你是对 AI 感兴趣的学生,还是想要探索新技术的开发者,都可以通过这个项目获得宝贵的学习经验。 + +欢迎所有人参与到项目的开发和改进中来。如果你有任何想法或建议,请随时提出 issue 或加入群聊。 + 学习交流 QQ 群:946599635 +## 已实现功能 + +- Wi-Fi 配网 +- 离线语音唤醒(使用乐鑫方案) +- 流式语音对话(WebSocket 协议) +- 支持国语、粤语、英语、日语、韩语 5 种语言识别(使用 SenseVoice 方案) +- 声纹识别(识别是谁在喊 AI 的名字,[3D Speaker 项目](https://github.com/modelscope/3D-Speaker)) +- 使用大模型 TTS(火山引擎方案,阿里云接入中) +- 支持可配置的提示词和音色(自定义角色) +- 免费提供 Qwen2.5 72B 和 豆包模型(受限于性能和额度,人多后可能会限额) +- 支持每轮对话后自我总结,生成记忆体 +- 扩展液晶显示屏,显示信号强弱(后面可以显示中文字幕) +- 支持 ML307 Cat.1 4G 模块(可选) + ## 硬件部分 -### DIY 所需硬件 +为方便协作,目前所有硬件资料都放在飞书文档中: -- 开发板:ESP32-S3-DevKitC-1 -- 麦克风:INMP441 -- 功放:MAX98357 -- 喇叭:8Ω 3W -- 400 孔面包板 2 块 -- 导线若干 +[《小智 AI 聊天机器人百科全书》](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink) +第二版接线图如下: -### GPIO 接线指引 - -以下是默认接线方案,如果你的接线跟默认不一样,请在项目配置中同步修改。 - -![接线图](./docs/wiring.jpg) - -注意,MAX98357 的 GND 和 VIN 接线隐藏在元件下方。INMP441 的 VDD 和 GND 不能接反,否则会烧毁麦克风。 - -#### MAX98357 功放 - -``` -LRC -> GPIO 4 -BCLK -> GPIO 5 -DIN -> GPIO 6 -GAIN -> GND(如果音量太大,请将 GAIN 接到 3.3V) -SD -> 3.3V -GND -> GND -VIN -> 3.3V 或 5V(如果你的喇叭需要 5V,应该将 VIN 接到 5V) -``` - -#### INMP441 麦克风 - -``` -L/R -> GND -WS -> GPIO 10 -SCK -> GPIO 11 -SD -> GPIO 3 -VDD -> 3.3V -GND -> GND -``` +![第二版接线图](docs/wiring2.jpg) ## 固件部分 @@ -73,7 +64,7 @@ GND -> GND - 配置完成后,编译固件 -## 配置 Wi-Fi +## 配置 Wi-Fi (4G 版本跳过) 按照上述接线,烧录固件,设备上电后,开发板上的 RGB 会闪烁蓝灯(部分开发板需要焊接 RGB 灯的开关才会亮),进入配网状态。 diff --git a/docs/wiring2.jpg b/docs/wiring2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..910e2f5948a8a9e0c0ab0898474ee44a7d33677b GIT binary patch literal 74192 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 #include "esp_log.h" #include "model_path.h" @@ -10,7 +15,19 @@ #define TAG "Application" -Application::Application() { +Application::Application() +#ifdef CONFIG_USE_ML307 + : ml307_at_modem_(CONFIG_ML307_TX_PIN, CONFIG_ML307_RX_PIN, 4096), + http_(ml307_at_modem_), + firmware_upgrade_(http_) +#else + : http_(), + firmware_upgrade_(http_) +#endif +#ifdef CONFIG_USE_DISPLAY + , display_(CONFIG_DISPLAY_SDA_PIN, CONFIG_DISPLAY_SCL_PIN) +#endif +{ event_group_ = xEventGroupCreate(); audio_encode_queue_ = xQueueCreate(100, sizeof(iovec)); audio_decode_queue_ = xQueueCreate(100, sizeof(AudioPacket*)); @@ -20,8 +37,6 @@ Application::Application() { 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]; } } @@ -32,6 +47,8 @@ Application::Application() { } firmware_upgrade_.SetCheckVersionUrl(CONFIG_OTA_VERSION_URL); + firmware_upgrade_.SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str()); + firmware_upgrade_.SetPostData(SystemInfo::GetJsonString()); } Application::~Application() { @@ -49,9 +66,6 @@ Application::~Application() { for (auto& pcm : wake_word_pcm_) { free(pcm.iov_base); } - for (auto& packet : wake_word_opus_) { - heap_caps_free(packet); - } if (opus_decoder_ != nullptr) { opus_decoder_destroy(opus_decoder_); @@ -77,16 +91,119 @@ void Application::CheckNewVersion() { vTaskDelay(100); } SetChatState(kChatStateUpgrading); - firmware_upgrade_.StartUpgrade(); + firmware_upgrade_.StartUpgrade([this](int progress, size_t speed) { +#ifdef CONFIG_USE_DISPLAY + char buffer[64]; + snprintf(buffer, sizeof(buffer), "Upgrading...\n %d%% %zuKB/s", progress, speed / 1024); + display_.SetText(buffer); +#endif + }); // If upgrade success, the device will reboot and never reach here ESP_LOGI(TAG, "Firmware upgrade failed..."); SetChatState(kChatStateIdle); } else { - firmware_upgrade_.MarkValid(); + firmware_upgrade_.MarkCurrentVersionValid(); } } +#ifdef CONFIG_USE_DISPLAY + +#ifdef CONFIG_USE_ML307 +static std::string csq_to_string(int csq) { + if (csq == -1) { + return "No network"; + } else if (csq >= 0 && csq <= 9) { + return "Very bad"; + } else if (csq >= 10 && csq <= 14) { + return "Bad"; + } else if (csq >= 15 && csq <= 19) { + return "Fair"; + } else if (csq >= 20 && csq <= 24) { + return "Good"; + } else if (csq >= 25 && csq <= 31) { + return "Very good"; + } + return "Invalid"; +} +#else +static std::string rssi_to_string(int rssi) { + if (rssi >= -55) { + return "Very good"; + } else if (rssi >= -65) { + return "Good"; + } else if (rssi >= -75) { + return "Fair"; + } else if (rssi >= -85) { + return "Poor"; + } else { + return "No network"; + } +} +#endif + +void Application::UpdateDisplay() { + while (true) { + if (chat_state_ == kChatStateIdle) { +#ifdef CONFIG_USE_ML307 + std::string network_name = ml307_at_modem_.GetCarrierName(); + int signal_quality = ml307_at_modem_.GetCsq(); + if (signal_quality == -1) { + network_name = "No network"; + } else { + ESP_LOGI(TAG, "%s CSQ: %d", network_name.c_str(), signal_quality); + display_.SetText(network_name + "\n" + csq_to_string(signal_quality) + " (" + std::to_string(signal_quality) + ")"); + } +#else + auto& wifi_station = WifiStation::GetInstance(); + int8_t rssi = wifi_station.GetRssi(); + display_.SetText(wifi_station.GetSsid() + "\n" + rssi_to_string(rssi) + " (" + std::to_string(rssi) + ")"); +#endif + } + vTaskDelay(pdMS_TO_TICKS(10 * 1000)); + } +} +#endif + void Application::Start() { + auto& builtin_led = BuiltinLed::GetInstance(); +#ifdef CONFIG_USE_ML307 + builtin_led.SetBlue(); + builtin_led.StartContinuousBlink(100); + ml307_at_modem_.SetDebug(false); + ml307_at_modem_.SetBaudRate(921600); + // Print the ML307 modem information + std::string module_name = ml307_at_modem_.GetModuleName(); + ESP_LOGI(TAG, "ML307 Module: %s", module_name.c_str()); +#ifdef CONFIG_USE_DISPLAY + display_.SetText(std::string("Wait for network\n") + module_name); +#endif + ml307_at_modem_.ResetConnections(); + ml307_at_modem_.WaitForNetworkReady(); + + ESP_LOGI(TAG, "ML307 IMEI: %s", ml307_at_modem_.GetImei().c_str()); + ESP_LOGI(TAG, "ML307 ICCID: %s", ml307_at_modem_.GetIccid().c_str()); +#else + // Try to connect to WiFi, if failed, launch the WiFi configuration AP + auto& wifi_station = WifiStation::GetInstance(); +#ifdef CONFIG_USE_DISPLAY + display_.SetText(std::string("Connect to WiFi\n") + wifi_station.GetSsid()); +#endif + builtin_led.SetBlue(); + builtin_led.StartContinuousBlink(100); + wifi_station.Start(); + if (!wifi_station.IsConnected()) { + builtin_led.SetBlue(); + builtin_led.Blink(1000, 500); + auto& wifi_ap = WifiConfigurationAp::GetInstance(); + wifi_ap.SetSsidPrefix("Xiaozhi"); +#ifdef CONFIG_USE_DISPLAY + display_.SetText(wifi_ap.GetSsid() + "\n" + wifi_ap.GetWebServerUrl()); +#endif + wifi_ap.Start(); + return; + } +#endif + // Initialize the audio device audio_device_.Start(CONFIG_AUDIO_INPUT_SAMPLE_RATE, CONFIG_AUDIO_OUTPUT_SAMPLE_RATE); audio_device_.OnStateChanged([this]() { @@ -108,18 +225,19 @@ void Application::Start() { xTaskCreateStatic([](void* arg) { Application* app = (Application*)arg; app->AudioEncodeTask(); + vTaskDelete(NULL); }, "opus_encode", opus_stack_size, this, 1, audio_encode_task_stack_, &audio_encode_task_buffer_); audio_decode_task_stack_ = (StackType_t*)malloc(opus_stack_size); xTaskCreateStatic([](void* arg) { Application* app = (Application*)arg; app->AudioDecodeTask(); + vTaskDelete(NULL); }, "opus_decode", opus_stack_size, this, 1, audio_decode_task_stack_, &audio_decode_task_buffer_); StartCommunication(); StartDetection(); // Blink the LED to indicate the device is running - auto& builtin_led = BuiltinLed::GetInstance(); builtin_led.SetGreen(); builtin_led.BlinkOnce(); xEventGroupSetBits(event_group_, DETECTION_RUNNING); @@ -130,6 +248,15 @@ void Application::Start() { app->CheckNewVersion(); vTaskDelete(NULL); }, "check_new_version", 4096 * 2, this, 1, NULL); + +#ifdef CONFIG_USE_DISPLAY + // Launch a task to update the display + xTaskCreate([](void* arg) { + Application* app = (Application*)arg; + app->UpdateDisplay(); + vTaskDelete(NULL); + }, "update_display", 4096, this, 1, NULL); +#endif } void Application::SetChatState(ChatState state) { @@ -227,6 +354,7 @@ void Application::StartCommunication() { xTaskCreate([](void* arg) { Application* app = (Application*)arg; app->AudioCommunicationTask(); + vTaskDelete(NULL); }, "audio_communication", 4096 * 2, this, 5, NULL); } @@ -267,11 +395,13 @@ void Application::StartDetection() { xTaskCreate([](void* arg) { Application* app = (Application*)arg; app->AudioFeedTask(); + vTaskDelete(NULL); }, "audio_feed", 4096 * 2, this, 5, NULL); xTaskCreate([](void* arg) { Application* app = (Application*)arg; app->AudioDetectionTask(); + vTaskDelete(NULL); }, "audio_detection", 4096 * 2, this, 5, NULL); } @@ -303,14 +433,13 @@ void Application::StoreWakeWordData(uint8_t* data, size_t size) { memcpy(iov.iov_base, data, size); wake_word_pcm_.push_back(iov); // keep about 2 seconds of data, detect duration is 32ms (sample_rate == 16000, chunksize == 512) - if (wake_word_pcm_.size() > 2000 / 32) { + while (wake_word_pcm_.size() > 2000 / 32) { heap_caps_free(wake_word_pcm_.front().iov_base); wake_word_pcm_.pop_front(); } } void Application::EncodeWakeWordData() { - wake_word_opus_.clear(); if (wake_word_encode_task_stack_ == nullptr) { wake_word_encode_task_stack_ = (StackType_t*)malloc(4096 * 8); } @@ -321,18 +450,30 @@ void Application::EncodeWakeWordData() { OpusEncoder* encoder = new OpusEncoder(); encoder->Configure(CONFIG_AUDIO_INPUT_SAMPLE_RATE, 1, 60); encoder->SetComplexity(0); + app->wake_word_opus_.resize(4096 * 4); + size_t offset = 0; for (auto& pcm: app->wake_word_pcm_) { - encoder->Encode(pcm, [app](const iovec opus) { - auto protocol = app->AllocateBinaryProtocol(opus.iov_base, opus.iov_len); - app->wake_word_opus_.push_back(protocol); + encoder->Encode(pcm, [app, &offset](const iovec opus) { + size_t protocol_size = sizeof(BinaryProtocol) + opus.iov_len; + if (offset + protocol_size < app->wake_word_opus_.size()) { + auto protocol = (BinaryProtocol*)(&app->wake_word_opus_[offset]); + protocol->version = htons(PROTOCOL_VERSION); + protocol->type = htons(0); + protocol->reserved = 0; + protocol->timestamp = htonl(app->audio_device_.playing() ? app->audio_device_.last_timestamp() : 0); + protocol->payload_size = htonl(opus.iov_len); + memcpy(protocol->payload, opus.iov_base, opus.iov_len); + offset += protocol_size; + } }); heap_caps_free(pcm.iov_base); } app->wake_word_pcm_.clear(); + app->wake_word_opus_.resize(offset); auto end_time = esp_timer_get_time(); - ESP_LOGI(TAG, "Encode wake word data opus packets: %d in %lld ms", app->wake_word_opus_.size(), (end_time - start_time) / 1000); + ESP_LOGI(TAG, "Encode wake word opus: %zu bytes in %lld ms", app->wake_word_opus_.size(), (end_time - start_time) / 1000); xEventGroupSetBits(app->event_group_, WAKE_WORD_ENCODED); delete encoder; vTaskDelete(NULL); @@ -340,10 +481,7 @@ void Application::EncodeWakeWordData() { } void Application::SendWakeWordData() { - for (auto& protocol: wake_word_opus_) { - ws_client_->Send(protocol, sizeof(BinaryProtocol) + ntohl(protocol->payload_size), true); - heap_caps_free(protocol); - } + ws_client_->Send(wake_word_opus_.data(), wake_word_opus_.size(), true); wake_word_opus_.clear(); } @@ -412,7 +550,7 @@ void Application::AudioDetectionTask() { auto res = esp_afe_sr_v1.fetch(afe_detection_data_); if (res == nullptr || res->ret_value == ESP_FAIL) { - ESP_LOGE(TAG, "Error in fetch"); + ESP_LOGE(TAG, "Error in AudioDetectionTask"); if (res != nullptr) { ESP_LOGI(TAG, "Error code: %d", res->ret_value); } @@ -461,6 +599,7 @@ void Application::AudioDetectionTask() { opus_encoder_.ResetState(); // If connected, the hello message is already sent, so we can start communication xEventGroupSetBits(event_group_, COMMUNICATION_RUNNING); + ESP_LOGI(TAG, "Communication running"); } else { SetChatState(kChatStateIdle); xEventGroupSetBits(event_group_, DETECTION_RUNNING); @@ -478,7 +617,7 @@ void Application::AudioCommunicationTask() { auto res = esp_afe_vc_v1.fetch(afe_communication_data_); if (res == nullptr || res->ret_value == ESP_FAIL) { - ESP_LOGE(TAG, "Error in fetch"); + ESP_LOGE(TAG, "Error in AudioCommunicationTask"); if (res != nullptr) { ESP_LOGI(TAG, "Error code: %d", res->ret_value); } @@ -489,16 +628,16 @@ void Application::AudioCommunicationTask() { { std::lock_guard lock(mutex_); if (ws_client_ == nullptr || !ws_client_->IsConnected()) { - if (ws_client_ != nullptr) { - delete ws_client_; - ws_client_ = nullptr; - } + xEventGroupClearBits(event_group_, COMMUNICATION_RUNNING); if (audio_device_.playing()) { audio_device_.Break(); } SetChatState(kChatStateIdle); + if (ws_client_ != nullptr) { + delete ws_client_; + ws_client_ = nullptr; + } xEventGroupSetBits(event_group_, DETECTION_RUNNING); - xEventGroupClearBits(event_group_, COMMUNICATION_RUNNING); continue; } } @@ -591,7 +730,11 @@ void Application::StartWebSocketClient() { } std::string token = "Bearer " + std::string(CONFIG_WEBSOCKET_ACCESS_TOKEN); - ws_client_ = new WebSocketClient(); +#ifdef CONFIG_USE_ML307 + ws_client_ = new WebSocket(new Ml307SslTransport(ml307_at_modem_, 0)); +#else + ws_client_ = new WebSocket(new TlsTransport()); +#endif ws_client_->SetHeader("Authorization", token.c_str()); ws_client_->SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str()); ws_client_->SetHeader("Protocol-Version", std::to_string(PROTOCOL_VERSION).c_str()); @@ -659,6 +802,10 @@ void Application::StartWebSocketClient() { ESP_LOGE(TAG, "Websocket error: %d", error); }); + ws_client_->OnDisconnected([this]() { + ESP_LOGI(TAG, "Websocket disconnected"); + }); + if (!ws_client_->Connect(CONFIG_WEBSOCKET_URL)) { ESP_LOGE(TAG, "Failed to connect to websocket server"); return; diff --git a/main/Application.h b/main/Application.h index 4082d4e..f695a16 100644 --- a/main/Application.h +++ b/main/Application.h @@ -4,8 +4,12 @@ #include "AudioDevice.h" #include "OpusEncoder.h" #include "OpusResampler.h" -#include "WebSocketClient.h" +#include "WebSocket.h" +#include "Display.h" +#include "Ml307AtModem.h" #include "FirmwareUpgrade.h" +#include "Ml307Http.h" +#include "EspHttp.h" #include "opus.h" #include "resampler_structs.h" @@ -60,15 +64,23 @@ private: ~Application(); AudioDevice audio_device_; +#ifdef CONFIG_USE_ML307 + Ml307AtModem ml307_at_modem_; + Ml307Http http_; +#else + EspHttp http_; +#endif FirmwareUpgrade firmware_upgrade_; +#ifdef CONFIG_USE_DISPLAY + Display display_; +#endif std::recursive_mutex mutex_; - WebSocketClient* ws_client_ = nullptr; + WebSocket* 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 @@ -95,7 +107,11 @@ private: StaticTask_t wake_word_encode_task_buffer_; StackType_t* wake_word_encode_task_stack_ = nullptr; std::list wake_word_pcm_; - std::vector wake_word_opus_; + std::string wake_word_opus_; + + TaskHandle_t check_new_version_task_ = nullptr; + StaticTask_t check_new_version_task_buffer_; + StackType_t* check_new_version_task_stack_ = nullptr; BinaryProtocol* AllocateBinaryProtocol(void* payload, size_t payload_size); void SetDecodeSampleRate(int sample_rate); @@ -109,6 +125,7 @@ private: void CheckTestButton(); void PlayTestAudio(); void CheckNewVersion(); + void UpdateDisplay(); void AudioFeedTask(); void AudioDetectionTask(); diff --git a/main/AudioDevice.cc b/main/AudioDevice.cc index e1b3aab..3d06d39 100644 --- a/main/AudioDevice.cc +++ b/main/AudioDevice.cc @@ -76,10 +76,10 @@ void AudioDevice::CreateDuplexChannels() { }, .gpio_cfg = { .mclk = I2S_GPIO_UNUSED, - .bclk = (gpio_num_t)CONFIG_AUDIO_DEVICE_I2S_GPIO_BCLK, - .ws = (gpio_num_t)CONFIG_AUDIO_DEVICE_I2S_GPIO_WS, - .dout = (gpio_num_t)CONFIG_AUDIO_DEVICE_I2S_GPIO_DOUT, - .din = (gpio_num_t)CONFIG_AUDIO_DEVICE_I2S_GPIO_DIN, + .bclk = (gpio_num_t)CONFIG_AUDIO_DEVICE_I2S_MIC_GPIO_BCLK, + .ws = (gpio_num_t)CONFIG_AUDIO_DEVICE_I2S_MIC_GPIO_WS, + .dout = (gpio_num_t)CONFIG_AUDIO_DEVICE_I2S_SPK_GPIO_DOUT, + .din = (gpio_num_t)CONFIG_AUDIO_DEVICE_I2S_MIC_GPIO_DIN, .invert_flags = { .mclk_inv = false, .bclk_inv = false, @@ -127,9 +127,9 @@ void AudioDevice::CreateSimplexChannels() { }, .gpio_cfg = { .mclk = I2S_GPIO_UNUSED, - .bclk = (gpio_num_t)CONFIG_AUDIO_DEVICE_I2S_GPIO_BCLK, - .ws = (gpio_num_t)CONFIG_AUDIO_DEVICE_I2S_GPIO_WS, - .dout = (gpio_num_t)CONFIG_AUDIO_DEVICE_I2S_GPIO_DOUT, + .bclk = (gpio_num_t)CONFIG_AUDIO_DEVICE_I2S_SPK_GPIO_BCLK, + .ws = (gpio_num_t)CONFIG_AUDIO_DEVICE_I2S_SPK_GPIO_WS, + .dout = (gpio_num_t)CONFIG_AUDIO_DEVICE_I2S_SPK_GPIO_DOUT, .din = I2S_GPIO_UNUSED, .invert_flags = { .mclk_inv = false, @@ -147,7 +147,7 @@ void AudioDevice::CreateSimplexChannels() { std_cfg.gpio_cfg.bclk = (gpio_num_t)CONFIG_AUDIO_DEVICE_I2S_MIC_GPIO_BCLK; std_cfg.gpio_cfg.ws = (gpio_num_t)CONFIG_AUDIO_DEVICE_I2S_MIC_GPIO_WS; std_cfg.gpio_cfg.dout = I2S_GPIO_UNUSED; - std_cfg.gpio_cfg.din = (gpio_num_t)CONFIG_AUDIO_DEVICE_I2S_GPIO_DIN; + std_cfg.gpio_cfg.din = (gpio_num_t)CONFIG_AUDIO_DEVICE_I2S_MIC_GPIO_DIN; ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); ESP_LOGI(TAG, "Simplex channels created"); } diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index fbc6795..d922902 100755 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,6 +1,9 @@ set(SOURCES "AudioDevice.cc" + "FirmwareUpgrade.cc" + "SystemInfo.cc" "SystemReset.cc" "Application.cc" + "Display.cc" "main.cc" ) diff --git a/main/Display.cc b/main/Display.cc new file mode 100644 index 0000000..f46da1b --- /dev/null +++ b/main/Display.cc @@ -0,0 +1,139 @@ + +#include "Display.h" + +#include "esp_log.h" +#include "esp_err.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lcd_panel_vendor.h" +#include "esp_lvgl_port.h" +#include +#include + +#define TAG "Display" + +#ifdef CONFIG_USE_DISPLAY + +Display::Display(int sda_pin, int scl_pin) : sda_pin_(sda_pin), scl_pin_(scl_pin) { + ESP_LOGI(TAG, "Display Pins: %d, %d", sda_pin_, scl_pin_); + + i2c_master_bus_config_t bus_config = { + .i2c_port = I2C_NUM_0, + .sda_io_num = (gpio_num_t)sda_pin_, + .scl_io_num = (gpio_num_t)scl_pin_, + .clk_source = I2C_CLK_SRC_DEFAULT, + .glitch_ignore_cnt = 7, + .intr_priority = 1, + .trans_queue_depth = 0, + .flags = { + .enable_internal_pullup = 1, + }, + }; + + ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &i2c_bus_)); + + // SSD1306 config + esp_lcd_panel_io_i2c_config_t io_config = { + .dev_addr = 0x3C, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .control_phase_bytes = 1, + .dc_bit_offset = 6, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .flags = { + .dc_low_on_data = 0, + .disable_control_phase = 0, + }, + .scl_speed_hz = 400 * 1000, + }; + + ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c_v2(i2c_bus_, &io_config, &panel_io_)); + + ESP_LOGI(TAG, "Install SSD1306 driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = -1; + panel_config.bits_per_pixel = 1; + + esp_lcd_panel_ssd1306_config_t ssd1306_config = { + .height = CONFIG_DISPLAY_HEIGHT + }; + panel_config.vendor_config = &ssd1306_config; + + ESP_ERROR_CHECK(esp_lcd_new_panel_ssd1306(panel_io_, &panel_config, &panel_)); + ESP_LOGI(TAG, "SSD1306 driver installed"); + + // Reset the display + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_)); + if (esp_lcd_panel_init(panel_) != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize display"); + return; + } + + ESP_LOGI(TAG, "Initialize LVGL"); + lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG(); + lvgl_port_init(&port_cfg); + + const lvgl_port_display_cfg_t display_cfg = { + .io_handle = panel_io_, + .panel_handle = panel_, + .buffer_size = 128 * CONFIG_DISPLAY_HEIGHT, + .double_buffer = true, + .hres = 128, + .vres = CONFIG_DISPLAY_HEIGHT, + .monochrome = true, + .rotation = { + .swap_xy = 0, + .mirror_x = 0, + .mirror_y = 0, + }, + .flags = { + .buff_dma = 0, + .buff_spiram = 0, + }, + }; + disp_ = lvgl_port_add_disp(&display_cfg); + lv_disp_set_rotation(disp_, LV_DISP_ROT_180); + + // Set the display to on + ESP_LOGI(TAG, "Turning display on"); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_, true)); + + ESP_LOGI(TAG, "Display Loading..."); + if (lvgl_port_lock(0)) { + label_ = lv_label_create(lv_disp_get_scr_act(disp_)); + lv_label_set_text(label_, "Initializing..."); + lv_obj_set_width(label_, disp_->driver->hor_res); + lv_obj_set_height(label_, disp_->driver->ver_res); + lv_obj_set_style_text_line_space(label_, 0, 0); + lv_obj_set_style_pad_all(label_, 0, 0); + lv_obj_set_style_outline_pad(label_, 0, 0); + lvgl_port_unlock(); + } +} + +Display::~Display() { + if (label_ != nullptr) { + lvgl_port_lock(0); + lv_obj_del(label_); + lvgl_port_unlock(); + } + + if (disp_ != nullptr) { + lvgl_port_deinit(); + esp_lcd_panel_del(panel_); + esp_lcd_panel_io_del(panel_io_); + i2c_master_bus_reset(i2c_bus_); + } +} + +void Display::SetText(const std::string &text) { + if (label_ != nullptr) { + text_ = text; + lvgl_port_lock(0); + // Change the text of the label + lv_label_set_text(label_, text_.c_str()); + lvgl_port_unlock(); + } +} + +#endif diff --git a/main/Display.h b/main/Display.h new file mode 100644 index 0000000..9b9c614 --- /dev/null +++ b/main/Display.h @@ -0,0 +1,32 @@ +#ifndef DISPLAY_H +#define DISPLAY_H + +#include "driver/i2c_master.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_panel_ops.h" +#include "lvgl.h" + +#include + +class Display { +public: + Display(int sda_pin, int scl_pin); + ~Display(); + + void SetText(const std::string &text); + +private: + int sda_pin_; + int scl_pin_; + + i2c_master_bus_handle_t i2c_bus_ = nullptr; + + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + lv_disp_t *disp_ = nullptr; + lv_obj_t *label_ = nullptr; + + std::string text_; +}; + +#endif diff --git a/main/FirmwareUpgrade.cc b/main/FirmwareUpgrade.cc new file mode 100644 index 0000000..9d4a3ed --- /dev/null +++ b/main/FirmwareUpgrade.cc @@ -0,0 +1,259 @@ +#include "FirmwareUpgrade.h" +#include "SystemInfo.h" +#include "cJSON.h" +#include "esp_log.h" +#include "esp_partition.h" +#include "esp_http_client.h" +#include "esp_ota_ops.h" +#include "esp_app_format.h" +#include "Ml307Http.h" +#include +#include +#include + +#define TAG "FirmwareUpgrade" + + +FirmwareUpgrade::FirmwareUpgrade(Http& http) : http_(http) { +} + +FirmwareUpgrade::~FirmwareUpgrade() { +} + +void FirmwareUpgrade::SetCheckVersionUrl(std::string check_version_url) { + check_version_url_ = check_version_url; +} + +void FirmwareUpgrade::SetPostData(const std::string& post_data) { + post_data_ = post_data; +} + +void FirmwareUpgrade::SetHeader(const std::string& key, const std::string& value) { + headers_[key] = value; +} + +void FirmwareUpgrade::CheckVersion() { + std::string current_version = esp_app_get_description()->version; + ESP_LOGI(TAG, "Current version: %s", current_version.c_str()); + + if (check_version_url_.length() < 10) { + ESP_LOGE(TAG, "Check version URL is not properly set"); + return; + } + + for (const auto& header : headers_) { + http_.SetHeader(header.first, header.second); + } + + if (post_data_.empty()) { + http_.Open("GET", check_version_url_); + } else { + http_.SetHeader("Content-Type", "application/json"); + http_.SetContent(post_data_); + http_.Open("POST", check_version_url_); + } + + auto response = http_.GetBody(); + http_.Close(); + + // Response: { "firmware": { "version": "1.0.0", "url": "http://" } } + // Parse the JSON response and check if the version is newer + // If it is, set has_new_version_ to true and store the new version and URL + + cJSON *root = cJSON_Parse(response.c_str()); + if (root == NULL) { + ESP_LOGE(TAG, "Failed to parse JSON response"); + return; + } + cJSON *firmware = cJSON_GetObjectItem(root, "firmware"); + if (firmware == NULL) { + ESP_LOGE(TAG, "Failed to get firmware object"); + cJSON_Delete(root); + return; + } + cJSON *version = cJSON_GetObjectItem(firmware, "version"); + if (version == NULL) { + ESP_LOGE(TAG, "Failed to get version object"); + cJSON_Delete(root); + return; + } + cJSON *url = cJSON_GetObjectItem(firmware, "url"); + if (url == NULL) { + ESP_LOGE(TAG, "Failed to get url object"); + cJSON_Delete(root); + return; + } + + firmware_version_ = version->valuestring; + firmware_url_ = url->valuestring; + cJSON_Delete(root); + + // Check if the version is newer, for example, 0.1.0 is newer than 0.0.1 + has_new_version_ = IsNewVersionAvailable(current_version, firmware_version_); + if (has_new_version_) { + ESP_LOGI(TAG, "New version available: %s", firmware_version_.c_str()); + } else { + ESP_LOGI(TAG, "Current is the latest version"); + } +} + +void FirmwareUpgrade::MarkCurrentVersionValid() { + auto partition = esp_ota_get_running_partition(); + if (strcmp(partition->label, "factory") == 0) { + ESP_LOGI(TAG, "Running from factory partition, skipping"); + return; + } + + ESP_LOGI(TAG, "Running partition: %s", partition->label); + esp_ota_img_states_t state; + if (esp_ota_get_state_partition(partition, &state) != ESP_OK) { + ESP_LOGE(TAG, "Failed to get state of partition"); + return; + } + + if (state == ESP_OTA_IMG_PENDING_VERIFY) { + ESP_LOGI(TAG, "Marking firmware as valid"); + esp_ota_mark_app_valid_cancel_rollback(); + } +} + +void FirmwareUpgrade::Upgrade(const std::string& firmware_url) { + ESP_LOGI(TAG, "Upgrading firmware from %s", firmware_url.c_str()); + esp_ota_handle_t update_handle = 0; + auto update_partition = esp_ota_get_next_update_partition(NULL); + if (update_partition == NULL) { + ESP_LOGE(TAG, "Failed to get update partition"); + return; + } + + ESP_LOGI(TAG, "Writing to partition %s at offset 0x%lx", update_partition->label, update_partition->address); + bool image_header_checked = false; + std::string image_header; + + if (!http_.Open("GET", firmware_url)) { + ESP_LOGE(TAG, "Failed to open HTTP connection"); + return; + } + + size_t content_length = http_.GetBodyLength(); + if (content_length == 0) { + ESP_LOGE(TAG, "Failed to get content length"); + http_.Close(); + return; + } + + char buffer[4096]; + size_t total_read = 0, recent_read = 0; + auto last_calc_time = esp_timer_get_time(); + while (true) { + int ret = http_.Read(buffer, sizeof(buffer)); + if (ret < 0) { + ESP_LOGE(TAG, "Failed to read HTTP data: %s", esp_err_to_name(ret)); + http_.Close(); + return; + } + + // Calculate speed and progress every second + recent_read += ret; + total_read += ret; + if (esp_timer_get_time() - last_calc_time >= 1000000 || ret == 0) { + size_t progress = total_read * 100 / content_length; + ESP_LOGI(TAG, "Progress: %zu%% (%zu/%zu), Speed: %zuB/s", progress, total_read, content_length, recent_read); + if (upgrade_callback_) { + upgrade_callback_(progress, recent_read); + } + last_calc_time = esp_timer_get_time(); + recent_read = 0; + } + + if (ret == 0) { + break; + } + + + if (!image_header_checked) { + image_header.append(buffer, ret); + if (image_header.size() >= sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t)) { + esp_app_desc_t new_app_info; + memcpy(&new_app_info, image_header.data() + sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t), sizeof(esp_app_desc_t)); + ESP_LOGI(TAG, "New firmware version: %s", new_app_info.version); + + auto current_version = esp_app_get_description()->version; + if (memcmp(new_app_info.version, current_version, sizeof(new_app_info.version)) == 0) { + ESP_LOGE(TAG, "Firmware version is the same, skipping upgrade"); + http_.Close(); + return; + } + + if (esp_ota_begin(update_partition, OTA_WITH_SEQUENTIAL_WRITES, &update_handle)) { + esp_ota_abort(update_handle); + http_.Close(); + ESP_LOGE(TAG, "Failed to begin OTA"); + return; + } + + image_header_checked = true; + } + } + auto err = esp_ota_write(update_handle, buffer, ret); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err)); + esp_ota_abort(update_handle); + http_.Close(); + return; + } + } + http_.Close(); + + esp_err_t err = esp_ota_end(update_handle); + if (err != ESP_OK) { + if (err == ESP_ERR_OTA_VALIDATE_FAILED) { + ESP_LOGE(TAG, "Image validation failed, image is corrupted"); + } else { + ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(err)); + } + return; + } + + err = esp_ota_set_boot_partition(update_partition); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to set boot partition: %s", esp_err_to_name(err)); + return; + } + + ESP_LOGI(TAG, "Firmware upgrade successful, rebooting in 3 seconds..."); + vTaskDelay(pdMS_TO_TICKS(3000)); + esp_restart(); +} + +void FirmwareUpgrade::StartUpgrade(std::function callback) { + upgrade_callback_ = callback; + Upgrade(firmware_url_); +} + +std::vector FirmwareUpgrade::ParseVersion(const std::string& version) { + std::vector versionNumbers; + std::stringstream ss(version); + std::string segment; + + while (std::getline(ss, segment, '.')) { + versionNumbers.push_back(std::stoi(segment)); + } + + return versionNumbers; +} + +bool FirmwareUpgrade::IsNewVersionAvailable(const std::string& currentVersion, const std::string& newVersion) { + std::vector current = ParseVersion(currentVersion); + std::vector newer = ParseVersion(newVersion); + + for (size_t i = 0; i < std::min(current.size(), newer.size()); ++i) { + if (newer[i] > current[i]) { + return true; + } else if (newer[i] < current[i]) { + return false; + } + } + + return newer.size() > current.size(); +} diff --git a/main/FirmwareUpgrade.h b/main/FirmwareUpgrade.h new file mode 100644 index 0000000..07dc99f --- /dev/null +++ b/main/FirmwareUpgrade.h @@ -0,0 +1,37 @@ +#ifndef _FIRMWARE_UPGRADE_H +#define _FIRMWARE_UPGRADE_H + +#include +#include +#include +#include "Http.h" + +class FirmwareUpgrade { +public: + FirmwareUpgrade(Http& http); + ~FirmwareUpgrade(); + + void SetCheckVersionUrl(std::string check_version_url); + void SetPostData(const std::string& post_data); + void SetHeader(const std::string& key, const std::string& value); + void CheckVersion(); + bool HasNewVersion() { return has_new_version_; } + void StartUpgrade(std::function callback); + void MarkCurrentVersionValid(); + +private: + Http& http_; + std::string check_version_url_; + bool has_new_version_ = false; + std::string firmware_version_; + std::string firmware_url_; + std::string post_data_; + std::map headers_; + + void Upgrade(const std::string& firmware_url); + std::function upgrade_callback_; + std::vector ParseVersion(const std::string& version); + bool IsNewVersionAvailable(const std::string& currentVersion, const std::string& newVersion); +}; + +#endif // _FIRMWARE_UPGRADE_H diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index d459b44..690ce9f 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -30,29 +30,29 @@ config AUDIO_OUTPUT_SAMPLE_RATE help Audio output sample rate. -config AUDIO_DEVICE_I2S_GPIO_BCLK - int "I2S GPIO BCLK" - default 5 - help - GPIO number of the I2S BCLK. - -config AUDIO_DEVICE_I2S_GPIO_WS +config AUDIO_DEVICE_I2S_MIC_GPIO_WS int "I2S GPIO WS" default 4 help GPIO number of the I2S WS. -config AUDIO_DEVICE_I2S_GPIO_DOUT - int "I2S GPIO DOUT" +config AUDIO_DEVICE_I2S_MIC_GPIO_BCLK + int "I2S GPIO BCLK" + default 5 + help + GPIO number of the I2S BCLK. + +config AUDIO_DEVICE_I2S_MIC_GPIO_DIN + int "I2S GPIO DIN" default 6 - help - GPIO number of the I2S DOUT. - -config AUDIO_DEVICE_I2S_GPIO_DIN - int "I2S GPIO DIN" - default 3 help GPIO number of the I2S DIN. + +config AUDIO_DEVICE_I2S_SPK_GPIO_DOUT + int "I2S GPIO DOUT" + default 7 + help + GPIO number of the I2S DOUT. config AUDIO_DEVICE_I2S_SIMPLEX bool "I2S Simplex" @@ -60,18 +60,65 @@ config AUDIO_DEVICE_I2S_SIMPLEX help Enable I2S Simplex mode. -config AUDIO_DEVICE_I2S_MIC_GPIO_BCLK - int "I2S MIC GPIO BCLK" - default 11 +config AUDIO_DEVICE_I2S_SPK_GPIO_BCLK + int "I2S SPK GPIO BCLK" + default 15 depends on AUDIO_DEVICE_I2S_SIMPLEX help GPIO number of the I2S MIC BCLK. -config AUDIO_DEVICE_I2S_MIC_GPIO_WS - int "I2S MIC GPIO WS" - default 10 +config AUDIO_DEVICE_I2S_SPK_GPIO_WS + int "I2S SPK GPIO WS" + default 16 depends on AUDIO_DEVICE_I2S_SIMPLEX help GPIO number of the I2S MIC WS. +config USE_ML307 + bool "Use ML307" + default n + help + Use ML307 as the modem. + +config ML307_RX_PIN + int "ML307 RX Pin" + default 11 + depends on USE_ML307 + help + GPIO number of the ML307 RX. + +config ML307_TX_PIN + int "ML307 TX Pin" + default 12 + depends on USE_ML307 + help + GPIO number of the ML307 TX. + +config USE_DISPLAY + bool "Use Display" + default n + help + Use Display. + +config DISPLAY_HEIGHT + int "Display Height" + default 64 + depends on USE_DISPLAY + help + Display height in pixels. + +config DISPLAY_SDA_PIN + int "Display SDA Pin" + default 41 + depends on USE_DISPLAY + help + GPIO number of the Display SDA. + +config DISPLAY_SCL_PIN + int "Display SCL Pin" + default 42 + depends on USE_DISPLAY + help + GPIO number of the Display SCL. + endmenu diff --git a/main/SystemInfo.cc b/main/SystemInfo.cc new file mode 100644 index 0000000..684f519 --- /dev/null +++ b/main/SystemInfo.cc @@ -0,0 +1,221 @@ +#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" +#include "esp_wifi.h" +#include "esp_ota_ops.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 + } + ], + "ota": { + "label": "ota_0" + } + } + */ + 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 += "],"; + + json += "\"ota\":{"; + auto ota_partition = esp_ota_get_running_partition(); + json += "\"label\":\"" + std::string(ota_partition->label) + "\""; + 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; +} + diff --git a/main/SystemInfo.h b/main/SystemInfo.h new file mode 100644 index 0000000..b0744d2 --- /dev/null +++ b/main/SystemInfo.h @@ -0,0 +1,20 @@ +#ifndef _SYSTEM_INFO_H_ +#define _SYSTEM_INFO_H_ + +#include + +#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_ diff --git a/main/idf_component.yml b/main/idf_component.yml index 6136442..b85c6d5 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -1,24 +1,13 @@ ## IDF Component Manager Manifest File dependencies: 78/esp-builtin-led: "^1.0.1" - 78/esp-wifi-connect: "^1.0.1" - 78/esp-ota: "^1.0.1" - 78/esp-websocket: "^1.0.0" + 78/esp-wifi-connect: "^1.1.0" 78/esp-opus-encoder: "^1.0.1" + 78/esp-ml307: "^1.1.0" espressif/esp-sr: "^1.9.0" + lvgl/lvgl: "^8.4.0" + esp_lvgl_port: "^1.4.0" ## Required IDF version idf: version: ">=5.3" - # # 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 -description: "An AI voice assistant for ESP32" -url: "https://github.com/78/xiaozhi-esp32" + diff --git a/main/main.cc b/main/main.cc index 49ccf3b..9ea9b5e 100755 --- a/main/main.cc +++ b/main/main.cc @@ -5,13 +5,11 @@ #include "nvs.h" #include "nvs_flash.h" #include "driver/gpio.h" +#include "esp_event.h" -#include "WifiConfigurationAp.h" #include "Application.h" #include "SystemInfo.h" #include "SystemReset.h" -#include "BuiltinLed.h" -#include "WifiStation.h" #define TAG "main" #define STATS_TICKS pdMS_TO_TICKS(1000) @@ -33,19 +31,6 @@ extern "C" void app_main(void) } ESP_ERROR_CHECK(ret); - // Try to connect to WiFi, if failed, launch the WiFi configuration AP - auto& builtin_led = BuiltinLed::GetInstance(); - auto& wifi_station = WifiStation::GetInstance(); - builtin_led.SetBlue(); - builtin_led.StartContinuousBlink(100); - wifi_station.Start(); - if (!wifi_station.IsConnected()) { - builtin_led.SetBlue(); - builtin_led.Blink(1000, 500); - WifiConfigurationAp::GetInstance().Start("Xiaozhi"); - return; - } - // Otherwise, launch the application Application::GetInstance().Start(); @@ -54,6 +39,6 @@ extern "C" void app_main(void) vTaskDelay(10000 / portTICK_PERIOD_MS); // SystemInfo::PrintRealTimeStats(STATS_TICKS); int free_sram = heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL); - ESP_LOGD(TAG, "Free heap size: %u minimal internal: %u", SystemInfo::GetFreeHeapSize(), free_sram); + ESP_LOGI(TAG, "Free heap size: %u minimal internal: %u", SystemInfo::GetFreeHeapSize(), free_sram); } } diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 1e291fa..9f69ed5 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -12,6 +12,7 @@ CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=4096 CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=32768 CONFIG_SPIRAM_MEMTEST=n +CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y CONFIG_HTTPD_MAX_REQ_HDR_LEN=2048 CONFIG_HTTPD_MAX_URI_LEN=2048