From 89674f883837791fd6c225e1cd25e9b6ace1c31d Mon Sep 17 00:00:00 2001 From: Xiaoxia Date: Mon, 19 Jan 2026 21:46:21 +0800 Subject: [PATCH] v2.2.0: Add bread-compact-nt26 board (#1663) * Refactor application error handling and improve network task logic - Updated error handling for modem initialization failure in Application::Initialize(). - Added new error message for modem initialization in English and Chinese language files. - Simplified lambda captures in NetworkTask to avoid unnecessary references. - Set main task priority in Application::Run() for better performance. * Add support for Bread Compact NT26 board - Introduced new board configuration for Bread Compact NT26 in CMakeLists.txt and Kconfig. - Added board-specific implementation in compact_nt26_board.cc and nt26_board.cc. - Created configuration files for NT26, including config.h and config.json. - Updated dependencies in idf_component.yml to include uart-eth-modem. - Translated error messages in config.h for OLED display type selection to English. - Enhanced display and button initialization logic for NT26 board. * Update project version and improve build configuration - Updated project version from 2.1.0 to 2.2.0 in CMakeLists.txt. - Enabled minimal build configuration to include only essential components. - Updated README files to replace QQ group links with Discord links for community engagement. * Update Bread Compact NT26 board configuration name in config.json * fix compile errors * Update uart-eth-modem dependency format in idf_component.yml * fix esp32 compiling errors * Update CMakeLists.txt to change component dependency from REQUIRES to PRIV_REQUIRES for esp_pm, esp_psram, and esp_driver_gpio * Refactor CMakeLists.txt to explicitly list board common source files and update include directories for better clarity and organization. * Add esp_driver_ppa as a dependency in CMakeLists.txt --- CMakeLists.txt | 11 +- README.md | 2 +- README_ja.md | 2 +- README_zh.md | 2 +- main/CMakeLists.txt | 45 ++- main/Kconfig.projbuild | 5 +- main/application.cc | 12 +- main/assets/locales/en-US/language.json | 1 + main/assets/locales/zh-CN/language.json | 1 + main/boards/bread-compact-esp32/config.h | 2 +- main/boards/bread-compact-ml307/config.h | 2 +- .../bread-compact-nt26/compact_nt26_board.cc | 181 ++++++++++++ main/boards/bread-compact-nt26/config.h | 61 ++++ main/boards/bread-compact-nt26/config.json | 11 + main/boards/bread-compact-wifi/config.h | 2 +- main/boards/common/ml307_board.cc | 7 +- main/boards/common/nt26_board.cc | 266 ++++++++++++++++++ main/boards/common/nt26_board.h | 62 ++++ main/idf_component.yml | 6 +- 19 files changed, 654 insertions(+), 27 deletions(-) create mode 100644 main/boards/bread-compact-nt26/compact_nt26_board.cc create mode 100644 main/boards/bread-compact-nt26/config.h create mode 100644 main/boards/bread-compact-nt26/config.json create mode 100644 main/boards/common/nt26_board.cc create mode 100644 main/boards/common/nt26_board.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8dcd6df0..c6392e1a 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,14 +1,13 @@ -# 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 +# The following 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) -set(PROJECT_VER "2.1.0") - # Add this line to disable the specific warning add_compile_options(-Wno-missing-field-initializers) include($ENV{IDF_PATH}/tools/cmake/project.cmake) -project(xiaozhi) +# "Trim" the build. Include the minimal set of components, main, and anything it depends on. +idf_build_set_property(MINIMAL_BUILD ON) +set(PROJECT_VER "2.2.0") +project(xiaozhi) diff --git a/README.md b/README.md index 9e8c70d6..d092e488 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ This is an open-source ESP32 project, released under the MIT license, allowing a We hope this project helps everyone understand AI hardware development and apply rapidly evolving large language models to real hardware devices. -If you have any ideas or suggestions, please feel free to raise Issues or join the QQ group: 1011329060 +If you have any ideas or suggestions, please feel free to raise Issues or join our [Discord](https://discord.gg/x3S4jgXHk3) or QQ group: 994694848 ## Star History diff --git a/README_ja.md b/README_ja.md index ea7f70f0..9b1da9e3 100644 --- a/README_ja.md +++ b/README_ja.md @@ -155,7 +155,7 @@ Feishuドキュメントチュートリアルをご覧ください: このプロジェクトを通じて、AIハードウェア開発を理解し、急速に進化する大規模言語モデルを実際のハードウェアデバイスに応用できるようになることを目指しています。 -ご意見やご提案があれば、いつでもIssueを提出するか、QQグループ:1011329060 にご参加ください。 +ご意見やご提案があれば、いつでもIssueを提出するか、[Discord](https://discord.gg/x3S4jgXHk3) または QQグループ:1011329060 にご参加ください。 ## スター履歴 diff --git a/README_zh.md b/README_zh.md index e7ad1fad..2b65cc6f 100644 --- a/README_zh.md +++ b/README_zh.md @@ -155,7 +155,7 @@ v1 的稳定版本为 1.9.2,可以通过 `git checkout v1` 来切换到 v1 版 我们希望通过这个项目,能够帮助大家了解 AI 硬件开发,将当下飞速发展的大语言模型应用到实际的硬件设备中。 -如果你有任何想法或建议,请随时提出 Issues 或加入 QQ 群:1011329060 +如果你有任何想法或建议,请随时提出 Issues 或加入 [Discord](https://discord.gg/x3S4jgXHk3) 或 QQ 群:1011329060 ## Star History diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index d380ae72..ba9db4db 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -41,10 +41,29 @@ set(SOURCES "audio/audio_codec.cc" set(INCLUDE_DIRS "." "display" "display/lvgl_display" "display/lvgl_display/jpg" "audio" "protocols") # Add board common files -file(GLOB BOARD_COMMON_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/boards/common/*.cc) -list(APPEND SOURCES ${BOARD_COMMON_SOURCES}) +list(APPEND SOURCES + "boards/common/board.cc" + "boards/common/wifi_board.cc" + "boards/common/ml307_board.cc" + "boards/common/nt26_board.cc" + "boards/common/dual_network_board.cc" + "boards/common/adc_battery_monitor.cc" + "boards/common/afsk_demod.cc" + "boards/common/axp2101.cc" + "boards/common/backlight.cc" + "boards/common/button.cc" + "boards/common/esp32_camera.cc" + "boards/common/i2c_device.cc" + "boards/common/knob.cc" + "boards/common/power_save_timer.cc" + "boards/common/press_to_talk_mcp_tool.cc" + "boards/common/sleep_timer.cc" + "boards/common/sy6970.cc" + "boards/common/system_reset.cc" +) list(APPEND INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/boards/common) + idf_build_get_property(build_components BUILD_COMPONENTS) # Function to find component dynamically by pattern function(find_component_by_pattern PATTERN COMPONENT_VAR PATH_VAR) @@ -74,6 +93,10 @@ elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ML307) set(BOARD_TYPE "bread-compact-ml307") set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) set(BUILTIN_ICON_FONT font_awesome_14_1) +elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_NT26) + set(BOARD_TYPE "bread-compact-nt26") + set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) + set(BUILTIN_ICON_FONT font_awesome_14_1) elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ESP32) set(BOARD_TYPE "bread-compact-esp32") elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ESP32_LCD) @@ -732,9 +755,10 @@ if(CONFIG_IDF_TARGET_ESP32) "audio/codecs/es8388_audio_codec.cc" "audio/codecs/es8389_audio_codec.cc" "led/gpio_led.cc" - "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/esp32_camera.cc" "display/lvgl_display/jpg/image_to_jpeg.cpp" "display/lvgl_display/jpg/jpeg_to_image.c" + "boards/common/esp32_camera.cc" + "boards/common/nt26_board.cc" ) endif() @@ -742,6 +766,21 @@ idf_component_register(SRCS ${SOURCES} EMBED_FILES ${LANG_SOUNDS} ${COMMON_SOUNDS} INCLUDE_DIRS ${INCLUDE_DIRS} WHOLE_ARCHIVE + PRIV_REQUIRES + esp_pm + esp_psram + esp_driver_gpio + esp_driver_uart + esp_driver_spi + esp_driver_i2c + esp_driver_i2s + esp_driver_jpeg + esp_driver_ppa + esp_app_format + app_update + spi_flash + console + efuse ) # Use target_compile_definitions to define BOARD_TYPE, BOARD_NAME diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index ed86440f..4189da67 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -134,6 +134,9 @@ choice BOARD_TYPE config BOARD_TYPE_BREAD_COMPACT_ML307 bool "Bread Compact ML307/EC801E (面包板 4G)" depends on IDF_TARGET_ESP32S3 + config BOARD_TYPE_BREAD_COMPACT_NT26 + bool "Bread Compact NT26 (面包板 4G)" + depends on IDF_TARGET_ESP32S3 config BOARD_TYPE_BREAD_COMPACT_ESP32 bool "Bread Compact ESP32 DevKit (面包板)" depends on IDF_TARGET_ESP32 @@ -497,7 +500,7 @@ choice ESP_S3_LCD_EV_Board_Version_TYPE endchoice choice DISPLAY_OLED_TYPE - depends on BOARD_TYPE_BREAD_COMPACT_WIFI || BOARD_TYPE_BREAD_COMPACT_ML307 || BOARD_TYPE_BREAD_COMPACT_ESP32 || BOARD_TYPE_HU_087 + depends on BOARD_TYPE_BREAD_COMPACT_WIFI || BOARD_TYPE_BREAD_COMPACT_ML307 || BOARD_TYPE_BREAD_COMPACT_NT26 || BOARD_TYPE_BREAD_COMPACT_ESP32 || BOARD_TYPE_HU_087 prompt "OLED Type" default OLED_SSD1306_128X32 help diff --git a/main/application.cc b/main/application.cc index 09803862..a8c30aa5 100644 --- a/main/application.cc +++ b/main/application.cc @@ -147,8 +147,7 @@ void Application::Initialize() { Alert(Lang::Strings::ERROR, Lang::Strings::REG_ERROR, "triangle_exclamation", Lang::Sounds::OGG_ERR_REG); break; case NetworkEvent::ModemErrorInitFailed: - display->SetStatus(Lang::Strings::DETECTING_MODULE); - display->SetChatMessage("system", Lang::Strings::DETECTING_MODULE); + Alert(Lang::Strings::ERROR, Lang::Strings::MODEM_INIT_ERROR, "triangle_exclamation", Lang::Sounds::OGG_EXCLAMATION); break; case NetworkEvent::ModemErrorTimeout: display->SetStatus(Lang::Strings::REGISTERING_NETWORK); @@ -164,6 +163,9 @@ void Application::Initialize() { } void Application::Run() { + // Set the priority of the main task to 10 + vTaskPrioritySet(nullptr, 10); + const EventBits_t ALL_EVENTS = MAIN_EVENT_SCHEDULE | MAIN_EVENT_SEND_AUDIO | @@ -538,7 +540,7 @@ void Application::InitializeProtocol() { auto text = cJSON_GetObjectItem(root, "text"); if (cJSON_IsString(text)) { ESP_LOGI(TAG, "<< %s", text->valuestring); - Schedule([this, display, message = std::string(text->valuestring)]() { + Schedule([display, message = std::string(text->valuestring)]() { display->SetChatMessage("assistant", message.c_str()); }); } @@ -547,14 +549,14 @@ void Application::InitializeProtocol() { auto text = cJSON_GetObjectItem(root, "text"); if (cJSON_IsString(text)) { ESP_LOGI(TAG, ">> %s", text->valuestring); - Schedule([this, display, message = std::string(text->valuestring)]() { + Schedule([display, message = std::string(text->valuestring)]() { display->SetChatMessage("user", message.c_str()); }); } } else if (strcmp(type->valuestring, "llm") == 0) { auto emotion = cJSON_GetObjectItem(root, "emotion"); if (cJSON_IsString(emotion)) { - Schedule([this, display, emotion_str = std::string(emotion->valuestring)]() { + Schedule([display, emotion_str = std::string(emotion->valuestring)]() { display->SetEmotion(emotion_str.c_str()); }); } diff --git a/main/assets/locales/en-US/language.json b/main/assets/locales/en-US/language.json index 1758d34c..426015e5 100644 --- a/main/assets/locales/en-US/language.json +++ b/main/assets/locales/en-US/language.json @@ -11,6 +11,7 @@ "INITIALIZING": "Initializing...", "PIN_ERROR": "Please insert SIM card", "REG_ERROR": "Unable to access network, please check SIM card status", + "MODEM_INIT_ERROR": "Modem initialization failed", "DETECTING_MODULE": "Detecting module...", "REGISTERING_NETWORK": "Waiting for network...", "CHECKING_NEW_VERSION": "Checking for new version...", diff --git a/main/assets/locales/zh-CN/language.json b/main/assets/locales/zh-CN/language.json index 9eb619d1..83ad54a4 100644 --- a/main/assets/locales/zh-CN/language.json +++ b/main/assets/locales/zh-CN/language.json @@ -11,6 +11,7 @@ "INITIALIZING": "正在初始化...", "PIN_ERROR": "请插入 SIM 卡", "REG_ERROR": "无法接入网络,请检查流量卡状态", + "MODEM_INIT_ERROR": "模组初始化失败", "DETECTING_MODULE": "检测模组...", "REGISTERING_NETWORK": "等待网络...", "CHECKING_NEW_VERSION": "检查新版本...", diff --git a/main/boards/bread-compact-esp32/config.h b/main/boards/bread-compact-esp32/config.h index 0d5ab75d..8f480053 100644 --- a/main/boards/bread-compact-esp32/config.h +++ b/main/boards/bread-compact-esp32/config.h @@ -45,7 +45,7 @@ #elif CONFIG_OLED_SSD1306_128X64 #define DISPLAY_HEIGHT 64 #else -#error "未选择 OLED 屏幕类型" +#error "OLED display type is not selected" #endif #define DISPLAY_MIRROR_X true diff --git a/main/boards/bread-compact-ml307/config.h b/main/boards/bread-compact-ml307/config.h index de9ef7f2..aaa67f0f 100644 --- a/main/boards/bread-compact-ml307/config.h +++ b/main/boards/bread-compact-ml307/config.h @@ -42,7 +42,7 @@ #elif CONFIG_OLED_SSD1306_128X64 #define DISPLAY_HEIGHT 64 #else -#error "未选择 OLED 屏幕类型" +#error "OLED display type is not selected" #endif #define DISPLAY_MIRROR_X true diff --git a/main/boards/bread-compact-nt26/compact_nt26_board.cc b/main/boards/bread-compact-nt26/compact_nt26_board.cc new file mode 100644 index 00000000..88128efd --- /dev/null +++ b/main/boards/bread-compact-nt26/compact_nt26_board.cc @@ -0,0 +1,181 @@ +#include "board.h" +#include "nt26_board.h" +#include "codecs/no_audio_codec.h" +#include "display/oled_display.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "lamp_controller.h" +#include "led/single_led.h" +#include "assets/lang_config.h" + +#include +#include +#include +#include + +#define TAG "CompactNt26Board" + +class CompactNt26Board : public Nt26Board { +private: + i2c_master_bus_handle_t display_i2c_bus_; + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + Display* display_ = nullptr; + Button boot_button_; + Button touch_button_; + Button volume_up_button_; + Button volume_down_button_; + + void InitializeDisplayI2c() { + i2c_master_bus_config_t bus_config = { + .i2c_port = (i2c_port_t)0, + .sda_io_num = DISPLAY_SDA_PIN, + .scl_io_num = DISPLAY_SCL_PIN, + .clk_source = I2C_CLK_SRC_DEFAULT, + .glitch_ignore_cnt = 7, + .intr_priority = 0, + .trans_queue_depth = 0, + .flags = { + .enable_internal_pullup = 1, + }, + }; + ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &display_i2c_bus_)); + } + + void InitializeSsd1306Display() { + // 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(display_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 = static_cast(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"); + display_ = new NoDisplay(); + return; + } + + // Set the display to on + ESP_LOGI(TAG, "Turning display on"); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_, true)); + + display_ = new OledDisplay(panel_io_, panel_, DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); + } + + void InitializeButtons() { + boot_button_.OnClick([]() { + Application::GetInstance().ToggleChatState(); + }); + + touch_button_.OnPressDown([]() { + Application::GetInstance().StartListening(); + }); + touch_button_.OnPressUp([]() { + Application::GetInstance().StopListening(); + }); + + volume_up_button_.OnClick([this]() { + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() + 10; + if (volume > 100) { + volume = 100; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + volume_up_button_.OnLongPress([this]() { + GetAudioCodec()->SetOutputVolume(100); + GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME); + }); + + volume_down_button_.OnClick([this]() { + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() - 10; + if (volume < 0) { + volume = 0; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + volume_down_button_.OnLongPress([this]() { + GetAudioCodec()->SetOutputVolume(0); + GetDisplay()->ShowNotification(Lang::Strings::MUTED); + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeTools() { + static LampController lamp(LAMP_GPIO); + } + +public: + CompactNt26Board() : + Nt26Board(NT26_TX_PIN, NT26_RX_PIN, NT26_DTR_PIN, NT26_RI_PIN), + boot_button_(BOOT_BUTTON_GPIO), + touch_button_(TOUCH_BUTTON_GPIO), + volume_up_button_(VOLUME_UP_BUTTON_GPIO), + volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) { + + InitializeDisplayI2c(); + InitializeSsd1306Display(); + InitializeButtons(); + InitializeTools(); + } + + virtual void StartNetwork() override { + GetDisplay()->SetStatus(Lang::Strings::DETECTING_MODULE); + Nt26Board::StartNetwork(); + } + + virtual Led* GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + return &led; + } + + virtual AudioCodec* GetAudioCodec() override { +#ifdef AUDIO_I2S_METHOD_SIMPLEX + static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN); +#else + static NoAudioCodecDuplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN); +#endif + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } +}; + +DECLARE_BOARD(CompactNt26Board); diff --git a/main/boards/bread-compact-nt26/config.h b/main/boards/bread-compact-nt26/config.h new file mode 100644 index 00000000..74169c16 --- /dev/null +++ b/main/boards/bread-compact-nt26/config.h @@ -0,0 +1,61 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +// 如果使用 Duplex I2S 模式,请注释下面一行 +#define AUDIO_I2S_METHOD_SIMPLEX + +#ifdef AUDIO_I2S_METHOD_SIMPLEX + +#define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_4 +#define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_5 +#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_6 +#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_7 +#define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_15 +#define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_16 + +#else + +#define AUDIO_I2S_GPIO_WS GPIO_NUM_4 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_5 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_6 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_7 + +#endif + +#define BUILTIN_LED_GPIO GPIO_NUM_48 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define TOUCH_BUTTON_GPIO GPIO_NUM_47 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_40 +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_39 + +#define DISPLAY_SDA_PIN GPIO_NUM_41 +#define DISPLAY_SCL_PIN GPIO_NUM_42 +#define DISPLAY_WIDTH 128 + +#if CONFIG_OLED_SSD1306_128X32 +#define DISPLAY_HEIGHT 32 +#elif CONFIG_OLED_SSD1306_128X64 +#define DISPLAY_HEIGHT 64 +#else +#error "OLED display type is not selected" +#endif + +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y true + + +#define NT26_DTR_PIN GPIO_NUM_9 +#define NT26_RI_PIN GPIO_NUM_10 +#define NT26_RX_PIN GPIO_NUM_11 +#define NT26_TX_PIN GPIO_NUM_12 + + +// A MCP Test: Control a lamp +#define LAMP_GPIO GPIO_NUM_18 + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/bread-compact-nt26/config.json b/main/boards/bread-compact-nt26/config.json new file mode 100644 index 00000000..e0c370e8 --- /dev/null +++ b/main/boards/bread-compact-nt26/config.json @@ -0,0 +1,11 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "bread-compact-nt26", + "sdkconfig_append": [ + "CONFIG_OLED_SSD1306_128X32=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/bread-compact-wifi/config.h b/main/boards/bread-compact-wifi/config.h index 7bb0982b..c1910d4d 100644 --- a/main/boards/bread-compact-wifi/config.h +++ b/main/boards/bread-compact-wifi/config.h @@ -46,7 +46,7 @@ #define DISPLAY_HEIGHT 64 #define SH1106 #else -#error "未选择 OLED 屏幕类型" +#error "OLED display type is not selected" #endif #define DISPLAY_MIRROR_X true diff --git a/main/boards/common/ml307_board.cc b/main/boards/common/ml307_board.cc index a75ff1c2..d204bd7e 100644 --- a/main/boards/common/ml307_board.cc +++ b/main/boards/common/ml307_board.cc @@ -1,8 +1,7 @@ #include "ml307_board.h" -#include "application.h" +#include "audio_codec.h" #include "display.h" -#include "assets/lang_config.h" #include #include @@ -66,8 +65,6 @@ void Ml307Board::OnNetworkEvent(NetworkEvent event, const std::string& data) { } void Ml307Board::NetworkTask() { - auto& application = Application::GetInstance(); - // Notify modem detection started OnNetworkEvent(NetworkEvent::ModemDetecting); @@ -92,7 +89,7 @@ void Ml307Board::NetworkTask() { // Set up network state change callback // Note: Don't call GetCarrierName() here as it sends AT command and will block ReceiveTask - modem_->OnNetworkStateChanged([this, &application](bool network_ready) { + modem_->OnNetworkStateChanged([this](bool network_ready) { if (network_ready) { OnNetworkEvent(NetworkEvent::Connected); } else { diff --git a/main/boards/common/nt26_board.cc b/main/boards/common/nt26_board.cc new file mode 100644 index 00000000..024d6a0b --- /dev/null +++ b/main/boards/common/nt26_board.cc @@ -0,0 +1,266 @@ +#include "nt26_board.h" +#include "display.h" +#include "application.h" +#include "audio_codec.h" +#include +#include +#include + +#define TAG "Nt26Board" + +Nt26Board::Nt26Board(gpio_num_t tx_pin, gpio_num_t rx_pin, gpio_num_t dtr_pin, gpio_num_t ri_pin, gpio_num_t reset_pin) + : tx_pin_(tx_pin), rx_pin_(rx_pin), dtr_pin_(dtr_pin), ri_pin_(ri_pin), reset_pin_(reset_pin) { + + gpio_install_isr_service(ESP_INTR_FLAG_IRAM); + esp_event_loop_create_default(); + esp_netif_init(); + + // Create PM lock handle + esp_pm_lock_create(ESP_PM_CPU_FREQ_MAX, 0, "nt26_cpu", &pm_lock_cpu_max_); + + // Create network ready timeout timer + esp_timer_create_args_t timer_args = { + .callback = OnNetworkReadyTimeout, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "nt26_net_timer", + .skip_unhandled_events = true + }; + esp_timer_create(&timer_args, &network_ready_timer_); +} + +Nt26Board::~Nt26Board() { + if (current_power_level_ != PowerSaveLevel::LOW_POWER) { + SetPowerSaveLevel(PowerSaveLevel::LOW_POWER); + } + + if (network_ready_timer_) { + esp_timer_stop(network_ready_timer_); + esp_timer_delete(network_ready_timer_); + } + + if (modem_) { + modem_->Stop(); + } + + if (pm_lock_cpu_max_) { + esp_pm_lock_delete(pm_lock_cpu_max_); + } +} + +std::string Nt26Board::GetBoardType() { + return "nt26"; +} + +void Nt26Board::OnNetworkEvent(NetworkEvent event, const std::string& data) { + if (network_event_callback_) { + network_event_callback_(event, data); + } +} + +void Nt26Board::OnNetworkReadyTimeout(void* arg) { + auto* self = static_cast(arg); + ESP_LOGW(TAG, "Network ready timeout"); + self->OnNetworkEvent(NetworkEvent::ModemErrorTimeout, "网络连接超时"); +} + +void Nt26Board::StartNetwork() { + OnNetworkEvent(NetworkEvent::ModemDetecting); + + UartEthModem::Config config = { + .uart_num = UART_NUM_1, + .baud_rate = 3000000, + .tx_pin = tx_pin_, + .rx_pin = rx_pin_, + .mrdy_pin = dtr_pin_, + .srdy_pin = ri_pin_ + }; + + modem_ = std::make_unique(config); + modem_->SetDebug(false); + + modem_->SetNetworkEventCallback([this](UartEthModem::UartEthModemEvent event) { + switch (event) { + case UartEthModem::UartEthModemEvent::Connected: + esp_timer_stop(network_ready_timer_); + OnNetworkEvent(NetworkEvent::Connected); + break; + case UartEthModem::UartEthModemEvent::Disconnected: + OnNetworkEvent(NetworkEvent::Disconnected); + break; + case UartEthModem::UartEthModemEvent::ErrorNoSim: + esp_timer_stop(network_ready_timer_); + ScheduleAsyncStop(); + OnNetworkEvent(NetworkEvent::ModemErrorNoSim); + break; + case UartEthModem::UartEthModemEvent::ErrorRegistrationDenied: + esp_timer_stop(network_ready_timer_); + ScheduleAsyncStop(); + OnNetworkEvent(NetworkEvent::ModemErrorRegDenied); + break; + case UartEthModem::UartEthModemEvent::Connecting: + OnNetworkEvent(NetworkEvent::Connecting); + break; + case UartEthModem::UartEthModemEvent::ErrorInitFailed: + case UartEthModem::UartEthModemEvent::ErrorNoCarrier: + esp_timer_stop(network_ready_timer_); + ScheduleAsyncStop(); + OnNetworkEvent(NetworkEvent::ModemErrorInitFailed); + break; + } + }); + + if (modem_->Start() != ESP_OK) { + OnNetworkEvent(NetworkEvent::ModemErrorInitFailed); + return; + } + + esp_timer_start_once(network_ready_timer_, 30000 * 1000ULL); + OnNetworkEvent(NetworkEvent::Connecting); +} + +void Nt26Board::ScheduleAsyncStop() { + Application::GetInstance().Schedule([this]() { + if (modem_) { + modem_->Stop(); + } + }); +} + +void Nt26Board::SetNetworkEventCallback(NetworkEventCallback callback) { + network_event_callback_ = std::move(callback); +} + +NetworkInterface* Nt26Board::GetNetwork() { + static EspNetwork network; + return &network; +} + +const char* Nt26Board::GetNetworkStateIcon() { + if (modem_ == nullptr || !modem_->IsInitialized()) { + return FONT_AWESOME_SIGNAL_OFF; + } + int csq = modem_->GetSignalStrength(); + if (csq == 99 || csq == -1) { + return FONT_AWESOME_SIGNAL_OFF; + } else if (csq >= 0 && csq <= 9) { + return FONT_AWESOME_SIGNAL_WEAK; + } else if (csq >= 10 && csq <= 14) { + return FONT_AWESOME_SIGNAL_FAIR; + } else if (csq >= 15 && csq <= 19) { + return FONT_AWESOME_SIGNAL_GOOD; + } else if (csq >= 20 && csq <= 31) { + return FONT_AWESOME_SIGNAL_STRONG; + } + return FONT_AWESOME_SIGNAL_OFF; +} + +void Nt26Board::SetPowerSaveLevel(PowerSaveLevel level) { + if (level == current_power_level_) return; + + if (current_power_level_ == PowerSaveLevel::BALANCED || + current_power_level_ == PowerSaveLevel::PERFORMANCE) { + if (pm_lock_cpu_max_) { + esp_pm_lock_release(pm_lock_cpu_max_); + } + } + + if (level == PowerSaveLevel::BALANCED || level == PowerSaveLevel::PERFORMANCE) { + if (pm_lock_cpu_max_) { + esp_pm_lock_acquire(pm_lock_cpu_max_); + } + } + + current_power_level_ = level; +} + +std::string Nt26Board::GetBoardJson() { + // Set the board type for OTA + std::string board_json = std::string("{\"type\":\"" BOARD_TYPE "\","); + board_json += "\"name\":\"" BOARD_NAME "\","; + if (modem_) { + board_json += "\"revision\":\"" + modem_->GetModuleRevision() + "\","; + board_json += "\"carrier\":\"" + modem_->GetCarrierName() + "\","; + board_json += "\"csq\":\"" + std::to_string(modem_->GetSignalStrength()) + "\","; + board_json += "\"imei\":\"" + modem_->GetImei() + "\","; + board_json += "\"iccid\":\"" + modem_->GetIccid() + "\","; + board_json += "\"cereg\":" + GetRegistrationState().ToString() + "}"; + } else { + board_json += "\"status\":\"offline\"}"; + } + return board_json; +} + +Nt26CeregState Nt26Board::GetRegistrationState() { + Nt26CeregState state; + if (modem_) { + auto cell_info = modem_->GetCellInfo(); + state.stat = cell_info.stat; + state.tac = cell_info.tac; + state.ci = cell_info.ci; + state.AcT = cell_info.act; + } + return state; +} + +std::string Nt26Board::GetDeviceStatusJson() { + auto& board = Board::GetInstance(); + auto root = cJSON_CreateObject(); + + // Audio speaker + auto audio_speaker = cJSON_CreateObject(); + auto audio_codec = board.GetAudioCodec(); + if (audio_codec) { + cJSON_AddNumberToObject(audio_speaker, "volume", audio_codec->output_volume()); + } + cJSON_AddItemToObject(root, "audio_speaker", audio_speaker); + + // Screen + auto backlight = board.GetBacklight(); + auto screen = cJSON_CreateObject(); + if (backlight) { + cJSON_AddNumberToObject(screen, "brightness", backlight->brightness()); + } + auto display = board.GetDisplay(); + if (display && display->height() > 64) { + auto theme = display->GetTheme(); + if (theme != nullptr) { + cJSON_AddStringToObject(screen, "theme", theme->name().c_str()); + } + } + cJSON_AddItemToObject(root, "screen", screen); + + // Battery + int battery_level = 0; + bool charging = false, discharging = false; + if (board.GetBatteryLevel(battery_level, charging, discharging)) { + auto battery = cJSON_CreateObject(); + cJSON_AddNumberToObject(battery, "level", battery_level); + cJSON_AddBoolToObject(battery, "charging", charging); + cJSON_AddItemToObject(root, "battery", battery); + } + + // Network + auto network = cJSON_CreateObject(); + cJSON_AddStringToObject(network, "type", "cellular"); + if (modem_) { + cJSON_AddStringToObject(network, "carrier", modem_->GetCarrierName().c_str()); + int csq = modem_->GetSignalStrength(); + if (csq == 99 || csq == -1) { + cJSON_AddStringToObject(network, "signal", "unknown"); + } else if (csq >= 0 && csq <= 14) { + cJSON_AddStringToObject(network, "signal", "weak"); + } else if (csq >= 15 && csq <= 24) { + cJSON_AddStringToObject(network, "signal", "medium"); + } else if (csq >= 25 && csq <= 31) { + cJSON_AddStringToObject(network, "signal", "strong"); + } + } + cJSON_AddItemToObject(root, "network", network); + + auto json_str = cJSON_PrintUnformatted(root); + std::string json(json_str); + cJSON_free(json_str); + cJSON_Delete(root); + return json; +} diff --git a/main/boards/common/nt26_board.h b/main/boards/common/nt26_board.h new file mode 100644 index 00000000..f897fe9d --- /dev/null +++ b/main/boards/common/nt26_board.h @@ -0,0 +1,62 @@ +#ifndef NT26_BOARD_H +#define NT26_BOARD_H + +#include +#include +#include +#include +#include +#include "board.h" + +struct Nt26CeregState { + int stat = 0; + std::string tac; + std::string ci; + int AcT = -1; + + std::string ToString() const { + std::string json = "{"; + json += "\"stat\":" + std::to_string(stat); + if (!tac.empty()) json += ",\"tac\":\"" + tac + "\""; + if (!ci.empty()) json += ",\"ci\":\"" + ci + "\""; + if (AcT >= 0) json += ",\"AcT\":" + std::to_string(AcT); + json += "}"; + return json; + } +}; + +class Nt26Board : public Board { +protected: + std::unique_ptr modem_; + gpio_num_t tx_pin_; + gpio_num_t rx_pin_; + gpio_num_t dtr_pin_; // mrdy_pin + gpio_num_t ri_pin_; // srdy_pin + gpio_num_t reset_pin_; + + NetworkEventCallback network_event_callback_; + esp_pm_lock_handle_t pm_lock_cpu_max_ = nullptr; + PowerSaveLevel current_power_level_ = PowerSaveLevel::LOW_POWER; + esp_timer_handle_t network_ready_timer_ = nullptr; + + virtual std::string GetBoardJson() override; + + void OnNetworkEvent(NetworkEvent event, const std::string& data = ""); + static void OnNetworkReadyTimeout(void* arg); + void ScheduleAsyncStop(); + +public: + Nt26Board(gpio_num_t tx_pin, gpio_num_t rx_pin, gpio_num_t dtr_pin, gpio_num_t ri_pin, gpio_num_t reset_pin = GPIO_NUM_NC); + virtual ~Nt26Board(); + virtual std::string GetBoardType() override; + virtual void StartNetwork() override; + virtual void SetNetworkEventCallback(NetworkEventCallback callback) override; + virtual NetworkInterface* GetNetwork() override; + virtual const char* GetNetworkStateIcon() override; + virtual void SetPowerSaveLevel(PowerSaveLevel level) override; + virtual AudioCodec* GetAudioCodec() override { return nullptr; } + virtual std::string GetDeviceStatusJson() override; + Nt26CeregState GetRegistrationState(); +}; + +#endif // NT26_BOARD_H diff --git a/main/idf_component.yml b/main/idf_component.yml index df225162..24d7f163 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -22,6 +22,10 @@ dependencies: espressif/esp_audio_effects: ~1.2.0 espressif/esp_audio_codec: ~2.4.0 78/esp-ml307: ~3.5.3 + 78/uart-eth-modem: + version: ~0.1.3 + rules: + - if: target not in [esp32] 78/xiaozhi-fonts: ~1.5.5 espressif/led_strip: ~3.0.1 espressif/esp_codec_dev: ~1.5 @@ -107,5 +111,5 @@ dependencies: ## Required IDF version idf: - version: '>=5.4.0' + version: '>=5.5.2' espressif/esp_lcd_touch_st7123: ^1.0.0