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 a27c2313..3632661c 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 94326571..cdb93216 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 4082d4e1..f695a16f 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 e1b3aab8..3d06d396 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 fbc6795f..d9229022 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 00000000..f46da1b7 --- /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 00000000..9b9c6143 --- /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 00000000..9d4a3edf --- /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 00000000..07dc99f2 --- /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 d459b449..690ce9f6 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 00000000..684f5190 --- /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 00000000..b0744d2b --- /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 61364426..b85c6d51 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 49ccf3b4..9ea9b5e1 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 1e291fad..9f69ed5d 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