diff --git a/CMakeLists.txt b/CMakeLists.txt index b884276f..e8f5f384 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 "1.7.6") +set(PROJECT_VER "1.7.7") # Add this line to disable the specific warning add_compile_options(-Wno-missing-field-initializers) diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index ec6781e1..938064e0 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -4,6 +4,7 @@ set(SOURCES "audio_codecs/audio_codec.cc" "audio_codecs/es8311_audio_codec.cc" "audio_codecs/es8374_audio_codec.cc" "audio_codecs/es8388_audio_codec.cc" + "audio_codecs/dummy_audio_codec.cc" "audio_processing/audio_debugger.cc" "led/single_led.cc" "led/circular_strip.cc" @@ -91,6 +92,10 @@ elseif(CONFIG_BOARD_TYPE_ATOMS3R_CAM_M12_ECHO_BASE) set(BOARD_TYPE "atoms3r-cam-m12-echo-base") elseif(CONFIG_BOARD_TYPE_ATOMMATRIX_ECHO_BASE) set(BOARD_TYPE "atommatrix-echo-base") +elseif(CONFIG_BOARD_TYPE_XMINI_C3_V3) + set(BOARD_TYPE "xmini-c3-v3") +elseif(CONFIG_BOARD_TYPE_XMINI_C3_4G) + set(BOARD_TYPE "xmini-c3-4g") elseif(CONFIG_BOARD_TYPE_XMINI_C3) set(BOARD_TYPE "xmini-c3") elseif(CONFIG_BOARD_TYPE_ESP32S3_KORVO2_V3) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 906c3bdd..eac41c8a 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -46,6 +46,12 @@ choice BOARD_TYPE config BOARD_TYPE_BREAD_COMPACT_ESP32_LCD bool "面包板(WiFi+ LCD) ESP32 DevKit" depends on IDF_TARGET_ESP32 + config BOARD_TYPE_XMINI_C3_V3 + bool "虾哥 Mini C3 V3" + depends on IDF_TARGET_ESP32C3 + config BOARD_TYPE_XMINI_C3_4G + bool "虾哥 Mini C3 4G" + depends on IDF_TARGET_ESP32C3 config BOARD_TYPE_XMINI_C3 bool "虾哥 Mini C3" depends on IDF_TARGET_ESP32C3 diff --git a/main/application.cc b/main/application.cc index efbd464b..198bbb0c 100644 --- a/main/application.cc +++ b/main/application.cc @@ -2,7 +2,6 @@ #include "board.h" #include "display.h" #include "system_info.h" -#include "ml307_ssl_transport.h" #include "audio_codec.h" #include "mqtt_protocol.h" #include "websocket_protocol.h" diff --git a/main/audio_codecs/audio_codec.cc b/main/audio_codecs/audio_codec.cc index 6cd114d6..cef61803 100644 --- a/main/audio_codecs/audio_codec.cc +++ b/main/audio_codecs/audio_codec.cc @@ -34,8 +34,13 @@ void AudioCodec::Start() { output_volume_ = 10; } - ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_)); - ESP_ERROR_CHECK(i2s_channel_enable(rx_handle_)); + if (tx_handle_ != nullptr) { + ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_)); + } + + if (rx_handle_ != nullptr) { + ESP_ERROR_CHECK(i2s_channel_enable(rx_handle_)); + } EnableInput(true); EnableOutput(true); diff --git a/main/audio_codecs/dummy_audio_codec.cc b/main/audio_codecs/dummy_audio_codec.cc new file mode 100644 index 00000000..5f646b3e --- /dev/null +++ b/main/audio_codecs/dummy_audio_codec.cc @@ -0,0 +1,20 @@ +#include "dummy_audio_codec.h" + +DummyAudioCodec::DummyAudioCodec(int input_sample_rate, int output_sample_rate) { + duplex_ = true; + input_reference_ = false; + input_channels_ = 1; + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; +} + +DummyAudioCodec::~DummyAudioCodec() { +} + +int DummyAudioCodec::Read(int16_t* dest, int samples) { + return 0; +} + +int DummyAudioCodec::Write(const int16_t* data, int samples) { + return 0; +} diff --git a/main/audio_codecs/dummy_audio_codec.h b/main/audio_codecs/dummy_audio_codec.h new file mode 100644 index 00000000..158f1404 --- /dev/null +++ b/main/audio_codecs/dummy_audio_codec.h @@ -0,0 +1,16 @@ +#ifndef _DUMMY_AUDIO_CODEC_H +#define _DUMMY_AUDIO_CODEC_H + +#include "audio_codec.h" + +class DummyAudioCodec : public AudioCodec { +private: + virtual int Read(int16_t* dest, int samples) override; + virtual int Write(const int16_t* data, int samples) override; + +public: + DummyAudioCodec(int input_sample_rate, int output_sample_rate); + virtual ~DummyAudioCodec(); +}; + +#endif // _DUMMY_AUDIO_CODEC_H \ No newline at end of file diff --git a/main/boards/atk-dnesp32s3m-4g/atk_dnesp32s3m.cc b/main/boards/atk-dnesp32s3m-4g/atk_dnesp32s3m.cc index 344cda8e..2d646e82 100644 --- a/main/boards/atk-dnesp32s3m-4g/atk_dnesp32s3m.cc +++ b/main/boards/atk-dnesp32s3m-4g/atk_dnesp32s3m.cc @@ -184,7 +184,7 @@ private: } public: - atk_dnesp32s3m_4g() : Ml307Board(Module_4G_TX_PIN, Module_4G_RX_PIN, 4096), + atk_dnesp32s3m_4g() : Ml307Board(Module_4G_TX_PIN, Module_4G_RX_PIN), boot_button_(BOOT_BUTTON_GPIO), volume_up_button_(VOLUME_UP_BUTTON_GPIO), volume_down_button_(VOLUME_DOWN_BUTTON_GPIO), diff --git a/main/boards/bread-compact-ml307/compact_ml307_board.cc b/main/boards/bread-compact-ml307/compact_ml307_board.cc index 922d1620..e020cc2b 100644 --- a/main/boards/bread-compact-ml307/compact_ml307_board.cc +++ b/main/boards/bread-compact-ml307/compact_ml307_board.cc @@ -166,7 +166,7 @@ private: } public: - CompactMl307Board() : DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN, 4096), + CompactMl307Board() : DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN, GPIO_NUM_NC), boot_button_(BOOT_BUTTON_GPIO), touch_button_(TOUCH_BUTTON_GPIO), volume_up_button_(VOLUME_UP_BUTTON_GPIO), diff --git a/main/boards/common/board.h b/main/boards/common/board.h index a637d018..a095cf17 100644 --- a/main/boards/common/board.h +++ b/main/boards/common/board.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "led/led.h" #include "backlight.h" @@ -41,10 +42,7 @@ public: virtual bool GetTemperature(float& esp32temp); virtual Display* GetDisplay(); virtual Camera* GetCamera(); - virtual Http* CreateHttp() = 0; - virtual WebSocket* CreateWebSocket() = 0; - virtual Mqtt* CreateMqtt() = 0; - virtual Udp* CreateUdp() = 0; + virtual NetworkInterface* GetNetwork() = 0; virtual void StartNetwork() = 0; virtual const char* GetNetworkStateIcon() = 0; virtual bool GetBatteryLevel(int &level, bool& charging, bool& discharging); diff --git a/main/boards/common/dual_network_board.cc b/main/boards/common/dual_network_board.cc index 41dbef55..e6fe9641 100644 --- a/main/boards/common/dual_network_board.cc +++ b/main/boards/common/dual_network_board.cc @@ -7,11 +7,11 @@ static const char *TAG = "DualNetworkBoard"; -DualNetworkBoard::DualNetworkBoard(gpio_num_t ml307_tx_pin, gpio_num_t ml307_rx_pin, size_t ml307_rx_buffer_size, int32_t default_net_type) +DualNetworkBoard::DualNetworkBoard(gpio_num_t ml307_tx_pin, gpio_num_t ml307_rx_pin, gpio_num_t ml307_dtr_pin, int32_t default_net_type) : Board(), ml307_tx_pin_(ml307_tx_pin), ml307_rx_pin_(ml307_rx_pin), - ml307_rx_buffer_size_(ml307_rx_buffer_size) { + ml307_dtr_pin_(ml307_dtr_pin) { // 从Settings加载网络类型 network_type_ = LoadNetworkTypeFromSettings(default_net_type); @@ -35,7 +35,7 @@ void DualNetworkBoard::SaveNetworkTypeToSettings(NetworkType type) { void DualNetworkBoard::InitializeCurrentBoard() { if (network_type_ == NetworkType::ML307) { ESP_LOGI(TAG, "Initialize ML307 board"); - current_board_ = std::make_unique(ml307_tx_pin_, ml307_rx_pin_, ml307_rx_buffer_size_); + current_board_ = std::make_unique(ml307_tx_pin_, ml307_rx_pin_, ml307_dtr_pin_); } else { ESP_LOGI(TAG, "Initialize WiFi board"); current_board_ = std::make_unique(); @@ -72,20 +72,8 @@ void DualNetworkBoard::StartNetwork() { current_board_->StartNetwork(); } -Http* DualNetworkBoard::CreateHttp() { - return current_board_->CreateHttp(); -} - -WebSocket* DualNetworkBoard::CreateWebSocket() { - return current_board_->CreateWebSocket(); -} - -Mqtt* DualNetworkBoard::CreateMqtt() { - return current_board_->CreateMqtt(); -} - -Udp* DualNetworkBoard::CreateUdp() { - return current_board_->CreateUdp(); +NetworkInterface* DualNetworkBoard::GetNetwork() { + return current_board_->GetNetwork(); } const char* DualNetworkBoard::GetNetworkStateIcon() { diff --git a/main/boards/common/dual_network_board.h b/main/boards/common/dual_network_board.h index 780c877a..efd88a99 100644 --- a/main/boards/common/dual_network_board.h +++ b/main/boards/common/dual_network_board.h @@ -22,7 +22,7 @@ private: // ML307的引脚配置 gpio_num_t ml307_tx_pin_; gpio_num_t ml307_rx_pin_; - size_t ml307_rx_buffer_size_; + gpio_num_t ml307_dtr_pin_; // 从Settings加载网络类型 NetworkType LoadNetworkTypeFromSettings(int32_t default_net_type); @@ -34,7 +34,7 @@ private: void InitializeCurrentBoard(); public: - DualNetworkBoard(gpio_num_t ml307_tx_pin, gpio_num_t ml307_rx_pin, size_t ml307_rx_buffer_size = 4096, int32_t default_net_type = 1); + DualNetworkBoard(gpio_num_t ml307_tx_pin, gpio_num_t ml307_rx_pin, gpio_num_t ml307_dtr_pin = GPIO_NUM_NC, int32_t default_net_type = 1); virtual ~DualNetworkBoard() = default; // 切换网络类型 @@ -49,10 +49,7 @@ public: // 重写Board接口 virtual std::string GetBoardType() override; virtual void StartNetwork() override; - virtual Http* CreateHttp() override; - virtual WebSocket* CreateWebSocket() override; - virtual Mqtt* CreateMqtt() override; - virtual Udp* CreateUdp() override; + virtual NetworkInterface* GetNetwork() override; virtual const char* GetNetworkStateIcon() override; virtual void SetPowerSaveMode(bool enabled) override; virtual std::string GetBoardJson() override; diff --git a/main/boards/common/esp32_camera.cc b/main/boards/common/esp32_camera.cc index 17b5d972..2c9898b8 100644 --- a/main/boards/common/esp32_camera.cc +++ b/main/boards/common/esp32_camera.cc @@ -209,7 +209,8 @@ std::string Esp32Camera::Explain(const std::string& question) { }, jpeg_queue); }); - auto http = Board::GetInstance().CreateHttp(); + auto network = Board::GetInstance().GetNetwork(); + auto http = std::unique_ptr(network->CreateHttp(3)); // 构造multipart/form-data请求体 std::string boundary = "----ESP32_CAMERA_BOUNDARY"; diff --git a/main/boards/common/ml307_board.cc b/main/boards/common/ml307_board.cc index 247e6cb5..5cabd5d4 100644 --- a/main/boards/common/ml307_board.cc +++ b/main/boards/common/ml307_board.cc @@ -7,16 +7,11 @@ #include #include -#include -#include -#include -#include -#include #include static const char *TAG = "Ml307Board"; -Ml307Board::Ml307Board(gpio_num_t tx_pin, gpio_num_t rx_pin, size_t rx_buffer_size) : modem_(tx_pin, rx_pin, rx_buffer_size) { +Ml307Board::Ml307Board(gpio_num_t tx_pin, gpio_num_t rx_pin, gpio_num_t dtr_pin) : tx_pin_(tx_pin), rx_pin_(rx_pin), dtr_pin_(dtr_pin) { } std::string Ml307Board::GetBoardType() { @@ -24,34 +19,39 @@ std::string Ml307Board::GetBoardType() { } void Ml307Board::StartNetwork() { + auto& application = Application::GetInstance(); auto display = Board::GetInstance().GetDisplay(); display->SetStatus(Lang::Strings::DETECTING_MODULE); - modem_.SetDebug(false); - modem_.SetBaudRate(921600); - - auto& application = Application::GetInstance(); - // If low power, the material ready event will be triggered by the modem because of a reset - modem_.OnMaterialReady([this, &application]() { - ESP_LOGI(TAG, "ML307 material ready"); - application.Schedule([this, &application]() { - application.SetDeviceState(kDeviceStateIdle); - WaitForNetworkReady(); - }); - }); - - WaitForNetworkReady(); -} - -void Ml307Board::WaitForNetworkReady() { - auto& application = Application::GetInstance(); - auto display = Board::GetInstance().GetDisplay(); - display->SetStatus(Lang::Strings::REGISTERING_NETWORK); while (true) { - int result = modem_.WaitForNetworkReady(); - if (result == -1) { + modem_ = AtModem::Detect(tx_pin_, rx_pin_, dtr_pin_, 921600); + if (modem_ != nullptr) { + break; + } + vTaskDelay(pdMS_TO_TICKS(1000)); + } + + modem_->OnNetworkStateChanged([this, &application](bool network_ready) { + if (network_ready) { + ESP_LOGI(TAG, "Network is ready"); + } else { + ESP_LOGE(TAG, "Network is down"); + auto device_state = application.GetDeviceState(); + if (device_state == kDeviceStateListening || device_state == kDeviceStateSpeaking) { + application.Schedule([this, &application]() { + application.SetDeviceState(kDeviceStateIdle); + }); + } + } + }); + + // Wait for network ready + display->SetStatus(Lang::Strings::REGISTERING_NETWORK); + while (true) { + auto result = modem_->WaitForNetworkReady(); + if (result == NetworkStatus::ErrorInsertPin) { application.Alert(Lang::Strings::ERROR, Lang::Strings::PIN_ERROR, "sad", Lang::Sounds::P3_ERR_PIN); - } else if (result == -2) { + } else if (result == NetworkStatus::ErrorRegistrationDenied) { application.Alert(Lang::Strings::ERROR, Lang::Strings::REG_ERROR, "sad", Lang::Sounds::P3_ERR_REG); } else { break; @@ -60,41 +60,29 @@ void Ml307Board::WaitForNetworkReady() { } // Print the ML307 modem information - std::string module_name = modem_.GetModuleName(); - std::string imei = modem_.GetImei(); - std::string iccid = modem_.GetIccid(); - ESP_LOGI(TAG, "ML307 Module: %s", module_name.c_str()); + std::string module_revision = modem_->GetModuleRevision(); + std::string imei = modem_->GetImei(); + std::string iccid = modem_->GetIccid(); + ESP_LOGI(TAG, "ML307 Revision: %s", module_revision.c_str()); ESP_LOGI(TAG, "ML307 IMEI: %s", imei.c_str()); ESP_LOGI(TAG, "ML307 ICCID: %s", iccid.c_str()); // Close all previous connections - modem_.ResetConnections(); + modem_->ResetConnections(); // Enable sleep mode - modem_.SetSleepMode(true, 30); + modem_->SetSleepMode(true, 30); } -Http* Ml307Board::CreateHttp() { - return new Ml307Http(modem_); -} - -WebSocket* Ml307Board::CreateWebSocket() { - return new WebSocket(new Ml307SslTransport(modem_, 0)); -} - -Mqtt* Ml307Board::CreateMqtt() { - return new Ml307Mqtt(modem_, 0); -} - -Udp* Ml307Board::CreateUdp() { - return new Ml307Udp(modem_, 0); +NetworkInterface* Ml307Board::GetNetwork() { + return modem_.get(); } const char* Ml307Board::GetNetworkStateIcon() { - if (!modem_.network_ready()) { + if (modem_ == nullptr || !modem_->network_ready()) { return FONT_AWESOME_SIGNAL_OFF; } - int csq = modem_.GetCsq(); + int csq = modem_->GetCsq(); if (csq == -1) { return FONT_AWESOME_SIGNAL_OFF; } else if (csq >= 0 && csq <= 14) { @@ -115,12 +103,12 @@ std::string Ml307Board::GetBoardJson() { // Set the board type for OTA std::string board_json = std::string("{\"type\":\"" BOARD_TYPE "\","); board_json += "\"name\":\"" BOARD_NAME "\","; - board_json += "\"revision\":\"" + modem_.GetModuleName() + "\","; - board_json += "\"carrier\":\"" + modem_.GetCarrierName() + "\","; - board_json += "\"csq\":\"" + std::to_string(modem_.GetCsq()) + "\","; - board_json += "\"imei\":\"" + modem_.GetImei() + "\","; - board_json += "\"iccid\":\"" + modem_.GetIccid() + "\","; - board_json += "\"cereg\":" + modem_.GetRegistrationState().ToString() + "}"; + board_json += "\"revision\":\"" + modem_->GetModuleRevision() + "\","; + board_json += "\"carrier\":\"" + modem_->GetCarrierName() + "\","; + board_json += "\"csq\":\"" + std::to_string(modem_->GetCsq()) + "\","; + board_json += "\"imei\":\"" + modem_->GetImei() + "\","; + board_json += "\"iccid\":\"" + modem_->GetIccid() + "\","; + board_json += "\"cereg\":" + modem_->GetRegistrationState().ToString() + "}"; return board_json; } @@ -189,8 +177,8 @@ std::string Ml307Board::GetDeviceStatusJson() { // Network auto network = cJSON_CreateObject(); cJSON_AddStringToObject(network, "type", "cellular"); - cJSON_AddStringToObject(network, "carrier", modem_.GetCarrierName().c_str()); - int csq = modem_.GetCsq(); + cJSON_AddStringToObject(network, "carrier", modem_->GetCarrierName().c_str()); + int csq = modem_->GetCsq(); if (csq == -1) { cJSON_AddStringToObject(network, "signal", "unknown"); } else if (csq >= 0 && csq <= 14) { diff --git a/main/boards/common/ml307_board.h b/main/boards/common/ml307_board.h index 05321084..22dddf99 100644 --- a/main/boards/common/ml307_board.h +++ b/main/boards/common/ml307_board.h @@ -1,23 +1,25 @@ #ifndef ML307_BOARD_H #define ML307_BOARD_H +#include +#include #include "board.h" -#include + class Ml307Board : public Board { protected: - Ml307AtModem modem_; + std::unique_ptr modem_; + gpio_num_t tx_pin_; + gpio_num_t rx_pin_; + gpio_num_t dtr_pin_; + virtual std::string GetBoardJson() override; - void WaitForNetworkReady(); public: - Ml307Board(gpio_num_t tx_pin, gpio_num_t rx_pin, size_t rx_buffer_size = 4096); + Ml307Board(gpio_num_t tx_pin, gpio_num_t rx_pin, gpio_num_t dtr_pin = GPIO_NUM_NC); virtual std::string GetBoardType() override; virtual void StartNetwork() override; - virtual Http* CreateHttp() override; - virtual WebSocket* CreateWebSocket() override; - virtual Mqtt* CreateMqtt() override; - virtual Udp* CreateUdp() override; + virtual NetworkInterface* GetNetwork() override; virtual const char* GetNetworkStateIcon() override; virtual void SetPowerSaveMode(bool enabled) override; virtual AudioCodec* GetAudioCodec() override { return nullptr; } diff --git a/main/boards/common/wifi_board.cc b/main/boards/common/wifi_board.cc index c8f8deb2..77781adb 100644 --- a/main/boards/common/wifi_board.cc +++ b/main/boards/common/wifi_board.cc @@ -9,12 +9,7 @@ #include #include -#include -#include -#include -#include -#include -#include +#include #include #include @@ -114,27 +109,9 @@ void WifiBoard::StartNetwork() { } } -Http* WifiBoard::CreateHttp() { - return new EspHttp(); -} - -WebSocket* WifiBoard::CreateWebSocket() { - Settings settings("websocket", false); - std::string url = settings.GetString("url"); - if (url.find("wss://") == 0) { - return new WebSocket(new TlsTransport()); - } else { - return new WebSocket(new TcpTransport()); - } - return nullptr; -} - -Mqtt* WifiBoard::CreateMqtt() { - return new EspMqtt(); -} - -Udp* WifiBoard::CreateUdp() { - return new EspUdp(); +NetworkInterface* WifiBoard::GetNetwork() { + static EspNetwork network; + return &network; } const char* WifiBoard::GetNetworkStateIcon() { diff --git a/main/boards/common/wifi_board.h b/main/boards/common/wifi_board.h index 6827b58e..c84cf0f1 100644 --- a/main/boards/common/wifi_board.h +++ b/main/boards/common/wifi_board.h @@ -13,10 +13,7 @@ public: WifiBoard(); virtual std::string GetBoardType() override; virtual void StartNetwork() override; - virtual Http* CreateHttp() override; - virtual WebSocket* CreateWebSocket() override; - virtual Mqtt* CreateMqtt() override; - virtual Udp* CreateUdp() override; + virtual NetworkInterface* GetNetwork() override; virtual const char* GetNetworkStateIcon() override; virtual void SetPowerSaveMode(bool enabled) override; virtual void ResetWifiConfiguration(); diff --git a/main/boards/kevin-box-1/kevin_box_board.cc b/main/boards/kevin-box-1/kevin_box_board.cc index e06c24a4..c76299d6 100644 --- a/main/boards/kevin-box-1/kevin_box_board.cc +++ b/main/boards/kevin-box-1/kevin_box_board.cc @@ -183,7 +183,7 @@ private: } public: - KevinBoxBoard() : Ml307Board(ML307_TX_PIN, ML307_RX_PIN, 4096), + KevinBoxBoard() : Ml307Board(ML307_TX_PIN, ML307_RX_PIN), boot_button_(BOOT_BUTTON_GPIO), volume_up_button_(VOLUME_UP_BUTTON_GPIO), volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) { diff --git a/main/boards/kevin-box-2/kevin_box_board.cc b/main/boards/kevin-box-2/kevin_box_board.cc index 899163f6..428265fd 100644 --- a/main/boards/kevin-box-2/kevin_box_board.cc +++ b/main/boards/kevin-box-2/kevin_box_board.cc @@ -237,7 +237,7 @@ private: } public: - KevinBoxBoard() : DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN, 4096), + KevinBoxBoard() : DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN), boot_button_(BOOT_BUTTON_GPIO), volume_up_button_(VOLUME_UP_BUTTON_GPIO), volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) { diff --git a/main/boards/magiclick-2p5/magiclick_2p5_board.cc b/main/boards/magiclick-2p5/magiclick_2p5_board.cc index 3ad302d4..1379431e 100644 --- a/main/boards/magiclick-2p5/magiclick_2p5_board.cc +++ b/main/boards/magiclick-2p5/magiclick_2p5_board.cc @@ -289,7 +289,7 @@ private: } public: - magiclick_2p5() : DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN, 4096, 0), + magiclick_2p5() : DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN, GPIO_NUM_NC, 0), main_button_(MAIN_BUTTON_GPIO), left_button_(LEFT_BUTTON_GPIO), right_button_(RIGHT_BUTTON_GPIO) { diff --git a/main/boards/minsi-k08-dual/minsi_k08_dual.cc b/main/boards/minsi-k08-dual/minsi_k08_dual.cc index 49da0ad3..44b3f0e2 100644 --- a/main/boards/minsi-k08-dual/minsi_k08_dual.cc +++ b/main/boards/minsi-k08-dual/minsi_k08_dual.cc @@ -210,7 +210,7 @@ private: public: - MINSI_K08_DUAL() : DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN, 4096), + MINSI_K08_DUAL() : DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN), boot_button_(BOOT_BUTTON_GPIO), volume_up_button_(VOLUME_UP_BUTTON_GPIO), volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) { diff --git a/main/boards/sensecap-watcher/sscma_camera.cc b/main/boards/sensecap-watcher/sscma_camera.cc index ede87e64..31c9678a 100644 --- a/main/boards/sensecap-watcher/sscma_camera.cc +++ b/main/boards/sensecap-watcher/sscma_camera.cc @@ -278,7 +278,8 @@ std::string SscmaCamera::Explain(const std::string& question) { return "{\"success\": false, \"message\": \"Image explain URL or token is not set\"}"; } - auto http = Board::GetInstance().CreateHttp(); + auto network = Board::GetInstance().GetNetwork(); + auto http = std::unique_ptr(network->CreateHttp(3)); // 构造multipart/form-data请求体 std::string boundary = "----ESP32_CAMERA_BOUNDARY"; diff --git a/main/boards/tudouzi/kevin_box_board.cc b/main/boards/tudouzi/kevin_box_board.cc index e2a92870..c75d69ce 100644 --- a/main/boards/tudouzi/kevin_box_board.cc +++ b/main/boards/tudouzi/kevin_box_board.cc @@ -68,11 +68,6 @@ private: void InitializePowerSaveTimer() { power_save_timer_ = new PowerSaveTimer(240, 60, -1); power_save_timer_->OnEnterSleepMode([this]() { - ESP_LOGI(TAG, "Enabling sleep mode"); - if (!modem_.Command("AT+MLPMCFG=\"sleepmode\",2,0")) { - ESP_LOGE(TAG, "Failed to enable module sleep mode"); - } - auto display = GetDisplay(); display->SetChatMessage("system", ""); display->SetEmotion("sleepy"); @@ -237,7 +232,7 @@ private: } public: - KevinBoxBoard() : Ml307Board(ML307_TX_PIN, ML307_RX_PIN, 4096), + KevinBoxBoard() : Ml307Board(ML307_TX_PIN, ML307_RX_PIN), boot_button_(BOOT_BUTTON_GPIO), volume_up_button_(VOLUME_UP_BUTTON_GPIO), volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) { diff --git a/main/boards/xingzhi-cube-0.85tft-ml307/xingzhi-cube-0.85tft-ml307.cc b/main/boards/xingzhi-cube-0.85tft-ml307/xingzhi-cube-0.85tft-ml307.cc index f3c0b3dd..c59384b5 100644 --- a/main/boards/xingzhi-cube-0.85tft-ml307/xingzhi-cube-0.85tft-ml307.cc +++ b/main/boards/xingzhi-cube-0.85tft-ml307/xingzhi-cube-0.85tft-ml307.cc @@ -210,7 +210,7 @@ private: } public: - XINGZHI_CUBE_0_85TFT_ML307(): Ml307Board(ML307_TX_PIN, ML307_RX_PIN, 4096), + XINGZHI_CUBE_0_85TFT_ML307(): Ml307Board(ML307_TX_PIN, ML307_RX_PIN), boot_button_(BOOT_BUTTON_GPIO), volume_up_button_(VOLUME_UP_BUTTON_GPIO), volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) { diff --git a/main/boards/xingzhi-cube-0.96oled-ml307/xingzhi-cube-0.96oled-ml307.cc b/main/boards/xingzhi-cube-0.96oled-ml307/xingzhi-cube-0.96oled-ml307.cc index 6acc14ee..394b7792 100644 --- a/main/boards/xingzhi-cube-0.96oled-ml307/xingzhi-cube-0.96oled-ml307.cc +++ b/main/boards/xingzhi-cube-0.96oled-ml307/xingzhi-cube-0.96oled-ml307.cc @@ -202,7 +202,7 @@ private: } public: - XINGZHI_CUBE_0_96OLED_ML307() : DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN, 4096), + XINGZHI_CUBE_0_96OLED_ML307() : DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN), boot_button_(BOOT_BUTTON_GPIO), volume_up_button_(VOLUME_UP_BUTTON_GPIO), volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) { diff --git a/main/boards/xingzhi-cube-1.54tft-ml307/xingzhi-cube-1.54tft-ml307.cc b/main/boards/xingzhi-cube-1.54tft-ml307/xingzhi-cube-1.54tft-ml307.cc index 5f6bd53a..05a7e90d 100644 --- a/main/boards/xingzhi-cube-1.54tft-ml307/xingzhi-cube-1.54tft-ml307.cc +++ b/main/boards/xingzhi-cube-1.54tft-ml307/xingzhi-cube-1.54tft-ml307.cc @@ -186,7 +186,7 @@ private: public: XINGZHI_CUBE_1_54TFT_ML307() : - DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN, 4096), + DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN), boot_button_(BOOT_BUTTON_GPIO), volume_up_button_(VOLUME_UP_BUTTON_GPIO), volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) { diff --git a/main/boards/xmini-c3-4g/config.h b/main/boards/xmini-c3-4g/config.h new file mode 100644 index 00000000..8a38e0fc --- /dev/null +++ b/main/boards/xmini-c3-4g/config.h @@ -0,0 +1,32 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_10 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_7 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_5 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_6 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_8 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_13 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_21 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_20 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR + +#define BUILTIN_LED_GPIO GPIO_NUM_3 +#define BOOT_BUTTON_GPIO GPIO_NUM_9 + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y true + +#define ML307_TX_PIN GPIO_NUM_2 +#define ML307_RX_PIN GPIO_NUM_0 +#define ML307_DTR_PIN GPIO_NUM_1 + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/xmini-c3-4g/config.json b/main/boards/xmini-c3-4g/config.json new file mode 100644 index 00000000..918d7d8c --- /dev/null +++ b/main/boards/xmini-c3-4g/config.json @@ -0,0 +1,15 @@ +{ + "target": "esp32c3", + "builds": [ + { + "name": "xmini-c3-4g", + "sdkconfig_append": [ + "CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y", + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v1/8m.csv\"", + "CONFIG_PM_ENABLE=y", + "CONFIG_FREERTOS_USE_TICKLESS_IDLE=y", + "CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/xmini-c3-4g/power_manager.h b/main/boards/xmini-c3-4g/power_manager.h new file mode 100644 index 00000000..91b4ad45 --- /dev/null +++ b/main/boards/xmini-c3-4g/power_manager.h @@ -0,0 +1,200 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + + +class PowerManager { +private: + esp_timer_handle_t timer_handle_; + std::function on_charging_status_changed_; + std::function on_low_battery_status_changed_; + + gpio_num_t charging_pin_ = GPIO_NUM_NC; + std::vector voltages_; + uint32_t battery_level_ = 0; + bool is_charging_ = false; + bool is_low_battery_ = false; + int ticks_ = 0; + const int kBatteryAdcInterval = 1; + const int kBatteryAdcDataCount = 3; + const int kLowBatteryLevel = 20; + + adc_oneshot_unit_handle_t adc_handle_; + adc_cali_handle_t adc_cali_handle_; + + void CheckBatteryStatus() { + // Get charging status + bool new_charging_status = gpio_get_level(charging_pin_) == 1; + if (new_charging_status != is_charging_) { + is_charging_ = new_charging_status; + if (on_charging_status_changed_) { + on_charging_status_changed_(is_charging_); + } + ReadBatteryAdcData(); + return; + } + + // 如果电池电量数据不足,则读取电池电量数据 + if (voltages_.size() < kBatteryAdcDataCount) { + ReadBatteryAdcData(); + return; + } + + // 如果电池电量数据充足,则每 kBatteryAdcInterval 个 tick 读取一次电池电量数据 + ticks_++; + if (ticks_ % kBatteryAdcInterval == 0) { + ReadBatteryAdcData(); + } + } + + void ReadBatteryAdcData() { + int adc_value, voltage; + ESP_ERROR_CHECK(adc_oneshot_read(adc_handle_, ADC_CHANNEL_4, &adc_value)); + if (adc_value == 0) { + return; + } + + ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc_cali_handle_, adc_value, &voltage)); + + // 将 ADC 值添加到队列中 + voltages_.push_back(voltage); + if (voltages_.size() > kBatteryAdcDataCount) { + voltages_.erase(voltages_.begin()); + } + uint32_t average_voltage = 0; + for (auto value : voltages_) { + average_voltage += value; + } + average_voltage /= voltages_.size(); + + // 定义电池电量区间 + const struct { + uint16_t adc; + uint8_t level; + } levels[] = { + {1600, 0}, + {1700, 20}, + {1800, 40}, + {1900, 60}, + {2000, 80}, + {2100, 100} + }; + + // 低于最低值时 + if (average_voltage < levels[0].adc) { + battery_level_ = 0; + } + // 高于最高值时 + else if (average_voltage >= levels[5].adc) { + battery_level_ = 100; + } else { + // 线性插值计算中间值 + for (int i = 0; i < 5; i++) { + if (average_voltage >= levels[i].adc && average_voltage < levels[i+1].adc) { + float ratio = static_cast(average_voltage - levels[i].adc) / (levels[i+1].adc - levels[i].adc); + battery_level_ = levels[i].level + ratio * (levels[i+1].level - levels[i].level); + break; + } + } + } + + // Check low battery status + if (voltages_.size() >= kBatteryAdcDataCount) { + bool new_low_battery_status = battery_level_ <= kLowBatteryLevel; + if (new_low_battery_status != is_low_battery_) { + is_low_battery_ = new_low_battery_status; + if (on_low_battery_status_changed_) { + on_low_battery_status_changed_(is_low_battery_); + } + } + } + } + +public: + PowerManager(gpio_num_t pin) : charging_pin_(pin) { + // 初始化充电引脚 + gpio_config_t io_conf = {}; + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pin_bit_mask = (1ULL << charging_pin_); + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_conf.pull_up_en = GPIO_PULLUP_DISABLE; + gpio_config(&io_conf); + + // 创建电池电量检查定时器 + esp_timer_create_args_t timer_args = { + .callback = [](void* arg) { + PowerManager* self = static_cast(arg); + self->CheckBatteryStatus(); + }, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "battery_check_timer", + .skip_unhandled_events = true, + }; + ESP_ERROR_CHECK(esp_timer_create(&timer_args, &timer_handle_)); + ESP_ERROR_CHECK(esp_timer_start_periodic(timer_handle_, 1000000)); + + // 初始化 ADC + adc_oneshot_unit_init_cfg_t init_config = { + .unit_id = ADC_UNIT_1, + .ulp_mode = ADC_ULP_MODE_DISABLE, + }; + ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config, &adc_handle_)); + + adc_oneshot_chan_cfg_t chan_config = { + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_12, + }; + ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle_, ADC_CHANNEL_4, &chan_config)); + + adc_cali_curve_fitting_config_t cali_config = { + .unit_id = ADC_UNIT_1, + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_12, + }; + ESP_ERROR_CHECK(adc_cali_create_scheme_curve_fitting(&cali_config, &adc_cali_handle_)); + } + + ~PowerManager() { + if (timer_handle_) { + esp_timer_stop(timer_handle_); + esp_timer_delete(timer_handle_); + } + if (adc_handle_) { + adc_oneshot_del_unit(adc_handle_); + } + } + + bool IsCharging() { + // 如果电量已经满了,则不再显示充电中 + if (battery_level_ == 100) { + return false; + } + return is_charging_; + } + + bool IsDischarging() { + // 没有区分充电和放电,所以直接返回相反状态 + return !is_charging_; + } + + uint8_t GetBatteryLevel() { + return battery_level_; + } + + void OnLowBatteryStatusChanged(std::function callback) { + on_low_battery_status_changed_ = callback; + } + + void OnChargingStatusChanged(std::function callback) { + on_charging_status_changed_ = callback; + } +}; diff --git a/main/boards/xmini-c3-4g/xmini_c3_4g_board.cc b/main/boards/xmini-c3-4g/xmini_c3_4g_board.cc new file mode 100644 index 00000000..c8fc92a2 --- /dev/null +++ b/main/boards/xmini-c3-4g/xmini_c3_4g_board.cc @@ -0,0 +1,257 @@ +#include "ml307_board.h" +#include "audio_codecs/es8311_audio_codec.h" +#include "display/oled_display.h" +#include "application.h" +#include "button.h" +#include "led/single_led.h" +#include "mcp_server.h" +#include "settings.h" +#include "config.h" +#include "power_save_timer.h" +#include "font_awesome_symbols.h" +#include "power_manager.h" + +#include +#include +#include +#include +#include +#include + +#define TAG "XminiC3Board" + +LV_FONT_DECLARE(font_puhui_14_1); +LV_FONT_DECLARE(font_awesome_14_1); + + +class XminiC3Board : public Ml307Board { +private: + i2c_master_bus_handle_t codec_i2c_bus_; + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + Display* display_ = nullptr; + Button boot_button_; + bool press_to_talk_enabled_ = false; + PowerSaveTimer* power_save_timer_ = nullptr; + PowerManager* power_manager_ = nullptr; + + void InitializePowerManager() { + power_manager_ = new PowerManager(GPIO_NUM_12); + power_manager_->OnChargingStatusChanged([this](bool is_charging) { + if (is_charging) { + power_save_timer_->SetEnabled(false); + } else { + power_save_timer_->SetEnabled(true); + } + }); + } + + void InitializePowerSaveTimer() { +#if CONFIG_USE_ESP_WAKE_WORD + power_save_timer_ = new PowerSaveTimer(160, 600); +#else + power_save_timer_ = new PowerSaveTimer(160, 60); +#endif + power_save_timer_->OnEnterSleepMode([this]() { + ESP_LOGI(TAG, "Enabling sleep mode"); + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("sleepy"); + + auto codec = GetAudioCodec(); + codec->EnableInput(false); + }); + power_save_timer_->OnExitSleepMode([this]() { + auto codec = GetAudioCodec(); + codec->EnableInput(true); + + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("neutral"); + }); + power_save_timer_->SetEnabled(true); + } + + void InitializeCodecI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = I2C_NUM_0, + .sda_io_num = AUDIO_CODEC_I2C_SDA_PIN, + .scl_io_num = AUDIO_CODEC_I2C_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(&i2c_bus_cfg, &codec_i2c_bus_)); + + if (i2c_master_probe(codec_i2c_bus_, 0x18, 1000) != ESP_OK) { + while (true) { + ESP_LOGE(TAG, "Failed to probe I2C bus, please check if you have installed the correct firmware"); + vTaskDelay(1000 / portTICK_PERIOD_MS); + } + } + } + + 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(codec_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, + {&font_puhui_14_1, &font_awesome_14_1}); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (!press_to_talk_enabled_) { + app.ToggleChatState(); + } + }); + boot_button_.OnPressDown([this]() { + if (power_save_timer_) { + power_save_timer_->WakeUp(); + } + if (press_to_talk_enabled_) { + Application::GetInstance().StartListening(); + } + }); + boot_button_.OnPressUp([this]() { + if (press_to_talk_enabled_) { + Application::GetInstance().StopListening(); + } + }); + } + + void InitializeTools() { + Settings settings("vendor"); + press_to_talk_enabled_ = settings.GetInt("press_to_talk", 0) != 0; + +#if CONFIG_IOT_PROTOCOL_XIAOZHI +#error "XiaoZhi 协议不支持" +#elif CONFIG_IOT_PROTOCOL_MCP + auto& mcp_server = McpServer::GetInstance(); + mcp_server.AddTool("self.set_press_to_talk", + "Switch between press to talk mode (长按说话) and click to talk mode (单击说话).\n" + "The mode can be `press_to_talk` or `click_to_talk`.", + PropertyList({ + Property("mode", kPropertyTypeString) + }), + [this](const PropertyList& properties) -> ReturnValue { + auto mode = properties["mode"].value(); + if (mode == "press_to_talk") { + SetPressToTalkEnabled(true); + return true; + } else if (mode == "click_to_talk") { + SetPressToTalkEnabled(false); + return true; + } + throw std::runtime_error("Invalid mode: " + mode); + }); +#endif + } + +public: + XminiC3Board() : Ml307Board(ML307_TX_PIN, ML307_RX_PIN, ML307_DTR_PIN), + boot_button_(BOOT_BUTTON_GPIO) { + + InitializePowerManager(); + InitializePowerSaveTimer(); + InitializeCodecI2c(); + InitializeSsd1306Display(); + InitializeButtons(); + InitializeTools(); + } + + virtual Led* GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + return &led; + } + + virtual Display* GetDisplay() override { + return display_; + } + + virtual AudioCodec* GetAudioCodec() override { + static Es8311AudioCodec audio_codec(codec_i2c_bus_, I2C_NUM_0, AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_GPIO_MCLK, AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN, + AUDIO_CODEC_PA_PIN, AUDIO_CODEC_ES8311_ADDR); + return &audio_codec; + } + + virtual bool GetBatteryLevel(int& level, bool& charging, bool& discharging) override { + static bool last_discharging = false; + charging = power_manager_->IsCharging(); + discharging = power_manager_->IsDischarging(); + if (discharging != last_discharging) { + power_save_timer_->SetEnabled(discharging); + last_discharging = discharging; + } + level = power_manager_->GetBatteryLevel(); + return true; + } + + void SetPressToTalkEnabled(bool enabled) { + press_to_talk_enabled_ = enabled; + + Settings settings("vendor", true); + settings.SetInt("press_to_talk", enabled ? 1 : 0); + ESP_LOGI(TAG, "Press to talk enabled: %d", enabled); + } + + bool IsPressToTalkEnabled() { + return press_to_talk_enabled_; + } + + virtual void SetPowerSaveMode(bool enabled) override { + if (!enabled) { + power_save_timer_->WakeUp(); + } + Ml307Board::SetPowerSaveMode(enabled); + } +}; + +DECLARE_BOARD(XminiC3Board); diff --git a/main/boards/xmini-c3-v3/config.h b/main/boards/xmini-c3-v3/config.h new file mode 100644 index 00000000..1f75de69 --- /dev/null +++ b/main/boards/xmini-c3-v3/config.h @@ -0,0 +1,28 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_8 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_5 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_7 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_6 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_4 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_10 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_0 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_1 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR + +#define BUILTIN_LED_GPIO GPIO_NUM_2 +#define BOOT_BUTTON_GPIO GPIO_NUM_9 + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y true + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/xmini-c3-v3/config.json b/main/boards/xmini-c3-v3/config.json new file mode 100644 index 00000000..26851771 --- /dev/null +++ b/main/boards/xmini-c3-v3/config.json @@ -0,0 +1,13 @@ +{ + "target": "esp32c3", + "builds": [ + { + "name": "xmini-c3-v3", + "sdkconfig_append": [ + "CONFIG_PM_ENABLE=y", + "CONFIG_FREERTOS_USE_TICKLESS_IDLE=y", + "CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/xmini-c3-v3/power_manager.h b/main/boards/xmini-c3-v3/power_manager.h new file mode 100644 index 00000000..8f95da6d --- /dev/null +++ b/main/boards/xmini-c3-v3/power_manager.h @@ -0,0 +1,196 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + + +class PowerManager { +private: + esp_timer_handle_t timer_handle_; + std::function on_charging_status_changed_; + std::function on_low_battery_status_changed_; + + gpio_num_t charging_pin_ = GPIO_NUM_NC; + std::vector voltages_; + uint32_t battery_level_ = 0; + bool is_charging_ = false; + bool is_low_battery_ = false; + int ticks_ = 0; + const int kBatteryAdcInterval = 1; + const int kBatteryAdcDataCount = 3; + const int kLowBatteryLevel = 20; + + adc_oneshot_unit_handle_t adc_handle_; + adc_cali_handle_t adc_cali_handle_; + + void CheckBatteryStatus() { + // Get charging status + bool new_charging_status = gpio_get_level(charging_pin_) == 1; + if (new_charging_status != is_charging_) { + is_charging_ = new_charging_status; + if (on_charging_status_changed_) { + on_charging_status_changed_(is_charging_); + } + ReadBatteryAdcData(); + return; + } + + // 如果电池电量数据不足,则读取电池电量数据 + if (voltages_.size() < kBatteryAdcDataCount) { + ReadBatteryAdcData(); + return; + } + + // 如果电池电量数据充足,则每 kBatteryAdcInterval 个 tick 读取一次电池电量数据 + ticks_++; + if (ticks_ % kBatteryAdcInterval == 0) { + ReadBatteryAdcData(); + } + } + + void ReadBatteryAdcData() { + int adc_value, voltage; + ESP_ERROR_CHECK(adc_oneshot_read(adc_handle_, ADC_CHANNEL_3, &adc_value)); + if (adc_value == 0) { + return; + } + + ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc_cali_handle_, adc_value, &voltage)); + + // 将 ADC 值添加到队列中 + voltages_.push_back(voltage); + if (voltages_.size() > kBatteryAdcDataCount) { + voltages_.erase(voltages_.begin()); + } + uint32_t average_voltage = 0; + for (auto value : voltages_) { + average_voltage += value; + } + average_voltage /= voltages_.size(); + + // 定义电池电量区间 + const struct { + uint16_t adc; + uint8_t level; + } levels[] = { + {1600, 0}, + {1700, 20}, + {1800, 40}, + {1900, 60}, + {2000, 80}, + {2100, 100} + }; + + // 低于最低值时 + if (average_voltage < levels[0].adc) { + battery_level_ = 0; + } + // 高于最高值时 + else if (average_voltage >= levels[5].adc) { + battery_level_ = 100; + } else { + // 线性插值计算中间值 + for (int i = 0; i < 5; i++) { + if (average_voltage >= levels[i].adc && average_voltage < levels[i+1].adc) { + float ratio = static_cast(average_voltage - levels[i].adc) / (levels[i+1].adc - levels[i].adc); + battery_level_ = levels[i].level + ratio * (levels[i+1].level - levels[i].level); + break; + } + } + } + + // Check low battery status + if (voltages_.size() >= kBatteryAdcDataCount) { + bool new_low_battery_status = battery_level_ <= kLowBatteryLevel; + if (new_low_battery_status != is_low_battery_) { + is_low_battery_ = new_low_battery_status; + if (on_low_battery_status_changed_) { + on_low_battery_status_changed_(is_low_battery_); + } + } + } + } + +public: + PowerManager(gpio_num_t pin) : charging_pin_(pin) { + // 初始化充电引脚 + gpio_config_t io_conf = {}; + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pin_bit_mask = (1ULL << charging_pin_); + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_conf.pull_up_en = GPIO_PULLUP_DISABLE; + gpio_config(&io_conf); + + // 创建电池电量检查定时器 + esp_timer_create_args_t timer_args = { + .callback = [](void* arg) { + PowerManager* self = static_cast(arg); + self->CheckBatteryStatus(); + }, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "battery_check_timer", + .skip_unhandled_events = true, + }; + ESP_ERROR_CHECK(esp_timer_create(&timer_args, &timer_handle_)); + ESP_ERROR_CHECK(esp_timer_start_periodic(timer_handle_, 1000000)); + + // 初始化 ADC + adc_oneshot_unit_init_cfg_t init_config = { + .unit_id = ADC_UNIT_1, + .ulp_mode = ADC_ULP_MODE_DISABLE, + }; + ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config, &adc_handle_)); + + adc_oneshot_chan_cfg_t chan_config = { + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_12, + }; + ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle_, ADC_CHANNEL_3, &chan_config)); + + adc_cali_curve_fitting_config_t cali_config = { + .unit_id = ADC_UNIT_1, + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_12, + }; + ESP_ERROR_CHECK(adc_cali_create_scheme_curve_fitting(&cali_config, &adc_cali_handle_)); + } + + ~PowerManager() { + if (timer_handle_) { + esp_timer_stop(timer_handle_); + esp_timer_delete(timer_handle_); + } + if (adc_handle_) { + adc_oneshot_del_unit(adc_handle_); + } + } + + bool IsCharging() { + return is_charging_; + } + + bool IsDischarging() { + // 没有区分充电和放电,所以直接返回相反状态 + return !is_charging_; + } + + uint8_t GetBatteryLevel() { + return battery_level_; + } + + void OnLowBatteryStatusChanged(std::function callback) { + on_low_battery_status_changed_ = callback; + } + + void OnChargingStatusChanged(std::function callback) { + on_charging_status_changed_ = callback; + } +}; diff --git a/main/boards/xmini-c3-v3/xmini_c3_board.cc b/main/boards/xmini-c3-v3/xmini_c3_board.cc new file mode 100644 index 00000000..ad0eab52 --- /dev/null +++ b/main/boards/xmini-c3-v3/xmini_c3_board.cc @@ -0,0 +1,258 @@ +#include "wifi_board.h" +#include "audio_codecs/es8311_audio_codec.h" +#include "display/oled_display.h" +#include "application.h" +#include "button.h" +#include "led/single_led.h" +#include "mcp_server.h" +#include "settings.h" +#include "config.h" +#include "power_save_timer.h" +#include "font_awesome_symbols.h" +#include "power_manager.h" + +#include +#include +#include +#include +#include +#include + +#define TAG "XminiC3Board" + +LV_FONT_DECLARE(font_puhui_14_1); +LV_FONT_DECLARE(font_awesome_14_1); + +class XminiC3Board : public WifiBoard { +private: + i2c_master_bus_handle_t codec_i2c_bus_; + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + Display* display_ = nullptr; + Button boot_button_; + bool press_to_talk_enabled_ = false; + PowerSaveTimer* power_save_timer_ = nullptr; + PowerManager* power_manager_ = nullptr; + + void InitializePowerManager() { + power_manager_ = new PowerManager(GPIO_NUM_12); + power_manager_->OnChargingStatusChanged([this](bool is_charging) { + if (is_charging) { + power_save_timer_->SetEnabled(false); + } else { + power_save_timer_->SetEnabled(true); + } + }); + } + + void InitializePowerSaveTimer() { +#if CONFIG_USE_ESP_WAKE_WORD + power_save_timer_ = new PowerSaveTimer(160, 600); +#else + power_save_timer_ = new PowerSaveTimer(160, 60); +#endif + power_save_timer_->OnEnterSleepMode([this]() { + ESP_LOGI(TAG, "Enabling sleep mode"); + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("sleepy"); + + auto codec = GetAudioCodec(); + codec->EnableInput(false); + }); + power_save_timer_->OnExitSleepMode([this]() { + auto codec = GetAudioCodec(); + codec->EnableInput(true); + + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("neutral"); + }); + power_save_timer_->SetEnabled(true); + } + + void InitializeCodecI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = I2C_NUM_0, + .sda_io_num = AUDIO_CODEC_I2C_SDA_PIN, + .scl_io_num = AUDIO_CODEC_I2C_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(&i2c_bus_cfg, &codec_i2c_bus_)); + + // Print I2C bus info + if (i2c_master_probe(codec_i2c_bus_, 0x18, 1000) != ESP_OK) { + while (true) { + ESP_LOGE(TAG, "Failed to probe I2C bus, please check if you have installed the correct firmware"); + vTaskDelay(1000 / portTICK_PERIOD_MS); + } + } + } + + 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(codec_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, + {&font_puhui_14_1, &font_awesome_14_1}); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + if (!press_to_talk_enabled_) { + app.ToggleChatState(); + } + }); + boot_button_.OnPressDown([this]() { + if (power_save_timer_) { + power_save_timer_->WakeUp(); + } + if (press_to_talk_enabled_) { + Application::GetInstance().StartListening(); + } + }); + boot_button_.OnPressUp([this]() { + if (press_to_talk_enabled_) { + Application::GetInstance().StopListening(); + } + }); + } + + void InitializeTools() { + Settings settings("vendor"); + press_to_talk_enabled_ = settings.GetInt("press_to_talk", 0) != 0; + +#if CONFIG_IOT_PROTOCOL_XIAOZHI +#error "XiaoZhi 协议不支持" +#elif CONFIG_IOT_PROTOCOL_MCP + auto& mcp_server = McpServer::GetInstance(); + mcp_server.AddTool("self.set_press_to_talk", + "Switch between press to talk mode (长按说话) and click to talk mode (单击说话).\n" + "The mode can be `press_to_talk` or `click_to_talk`.", + PropertyList({ + Property("mode", kPropertyTypeString) + }), + [this](const PropertyList& properties) -> ReturnValue { + auto mode = properties["mode"].value(); + if (mode == "press_to_talk") { + SetPressToTalkEnabled(true); + return true; + } else if (mode == "click_to_talk") { + SetPressToTalkEnabled(false); + return true; + } + throw std::runtime_error("Invalid mode: " + mode); + }); +#endif + } + +public: + XminiC3Board() : boot_button_(BOOT_BUTTON_GPIO) { + InitializePowerManager(); + InitializePowerSaveTimer(); + InitializeCodecI2c(); + InitializeSsd1306Display(); + InitializeButtons(); + InitializeTools(); + } + + virtual Led* GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + return &led; + } + + virtual Display* GetDisplay() override { + return display_; + } + + virtual AudioCodec* GetAudioCodec() override { + static Es8311AudioCodec audio_codec(codec_i2c_bus_, I2C_NUM_0, AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_GPIO_MCLK, AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN, + AUDIO_CODEC_PA_PIN, AUDIO_CODEC_ES8311_ADDR); + return &audio_codec; + } + + virtual bool GetBatteryLevel(int& level, bool& charging, bool& discharging) override { + static bool last_discharging = false; + charging = power_manager_->IsCharging(); + discharging = power_manager_->IsDischarging(); + if (discharging != last_discharging) { + power_save_timer_->SetEnabled(discharging); + last_discharging = discharging; + } + level = power_manager_->GetBatteryLevel(); + return true; + } + + void SetPressToTalkEnabled(bool enabled) { + press_to_talk_enabled_ = enabled; + + Settings settings("vendor", true); + settings.SetInt("press_to_talk", enabled ? 1 : 0); + ESP_LOGI(TAG, "Press to talk enabled: %d", enabled); + } + + bool IsPressToTalkEnabled() { + return press_to_talk_enabled_; + } + + virtual void SetPowerSaveMode(bool enabled) override { + if (!enabled) { + power_save_timer_->WakeUp(); + } + WifiBoard::SetPowerSaveMode(enabled); + } +}; + +DECLARE_BOARD(XminiC3Board); diff --git a/main/boards/zhengchen-1.54tft-ml307/zhengchen-1.54tft-ml307.cc b/main/boards/zhengchen-1.54tft-ml307/zhengchen-1.54tft-ml307.cc index 304361ba..e73fd548 100644 --- a/main/boards/zhengchen-1.54tft-ml307/zhengchen-1.54tft-ml307.cc +++ b/main/boards/zhengchen-1.54tft-ml307/zhengchen-1.54tft-ml307.cc @@ -176,7 +176,7 @@ private: public: ZHENGCHEN_1_54TFT_ML307() : - DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN, 4096), + DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN), boot_button_(BOOT_BUTTON_GPIO), volume_up_button_(VOLUME_UP_BUTTON_GPIO), volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) { diff --git a/main/idf_component.yml b/main/idf_component.yml index 9663c519..a8f9a4da 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -15,7 +15,7 @@ dependencies: 78/esp_lcd_nv3023: ~1.0.0 78/esp-wifi-connect: ~2.4.3 78/esp-opus-encoder: ~2.3.3 - 78/esp-ml307: ~2.2.1 + 78/esp-ml307: ~3.0.0 78/xiaozhi-fonts: ~1.3.2 espressif/led_strip: ^2.5.5 espressif/esp_codec_dev: ~1.3.2 diff --git a/main/ota.cc b/main/ota.cc index 67fc286a..435d94b7 100644 --- a/main/ota.cc +++ b/main/ota.cc @@ -53,7 +53,8 @@ Http* Ota::SetupHttp() { auto& board = Board::GetInstance(); auto app_desc = esp_app_get_description(); - auto http = board.CreateHttp(); + auto network = board.GetNetwork(); + auto http = network->CreateHttp(0); http->SetHeader("Activation-Version", has_serial_number_ ? "2" : "1"); http->SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str()); http->SetHeader("Client-Id", board.GetUuid()); @@ -272,7 +273,8 @@ void Ota::Upgrade(const std::string& firmware_url) { bool image_header_checked = false; std::string image_header; - auto http = std::unique_ptr(Board::GetInstance().CreateHttp()); + auto network = Board::GetInstance().GetNetwork(); + auto http = std::unique_ptr(network->CreateHttp(0)); if (!http->Open("GET", firmware_url)) { ESP_LOGE(TAG, "Failed to open HTTP connection"); return; diff --git a/main/protocols/mqtt_protocol.cc b/main/protocols/mqtt_protocol.cc index 8ffb60a2..af08b722 100644 --- a/main/protocols/mqtt_protocol.cc +++ b/main/protocols/mqtt_protocol.cc @@ -4,8 +4,6 @@ #include "settings.h" #include -#include -#include #include #include #include "assets/lang_config.h" @@ -53,7 +51,8 @@ bool MqttProtocol::StartMqttClient(bool report_error) { return false; } - mqtt_ = Board::GetInstance().CreateMqtt(); + auto network = Board::GetInstance().GetNetwork(); + mqtt_ = network->CreateMqtt(0); mqtt_->SetKeepAlive(keepalive_interval); mqtt_->OnDisconnected([this]() { @@ -197,7 +196,9 @@ bool MqttProtocol::OpenAudioChannel() { if (udp_ != nullptr) { delete udp_; } - udp_ = Board::GetInstance().CreateUdp(); + + auto network = Board::GetInstance().GetNetwork(); + udp_ = network->CreateUdp(2); udp_->OnMessage([this](const std::string& data) { /* * UDP Encrypted OPUS Packet Format: diff --git a/main/protocols/websocket_protocol.cc b/main/protocols/websocket_protocol.cc index 2d5c7840..088b173a 100644 --- a/main/protocols/websocket_protocol.cc +++ b/main/protocols/websocket_protocol.cc @@ -29,7 +29,7 @@ bool WebsocketProtocol::Start() { } bool WebsocketProtocol::SendAudio(const AudioStreamPacket& packet) { - if (websocket_ == nullptr) { + if (websocket_ == nullptr || !websocket_->IsConnected()) { return false; } @@ -61,7 +61,7 @@ bool WebsocketProtocol::SendAudio(const AudioStreamPacket& packet) { } bool WebsocketProtocol::SendText(const std::string& text) { - if (websocket_ == nullptr) { + if (websocket_ == nullptr || !websocket_->IsConnected()) { return false; } @@ -100,8 +100,9 @@ bool WebsocketProtocol::OpenAudioChannel() { error_occurred_ = false; - websocket_ = Board::GetInstance().CreateWebSocket(); - + auto network = Board::GetInstance().GetNetwork(); + websocket_ = network->CreateWebSocket(1); + if (!token.empty()) { // If token not has a space, add "Bearer " prefix if (token.find(" ") == std::string::npos) {