diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index b042262b..d1c71e28 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -220,6 +220,11 @@ elseif(CONFIG_BOARD_TYPE_ESP_KORVO2_V3) set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) set(BUILTIN_ICON_FONT font_awesome_20_4) set(DEFAULT_EMOJI_COLLECTION twemoji_64) +elseif(CONFIG_BOARD_TYPE_ESP_KORVO2_V3_RNDIS) + set(BOARD_TYPE "esp32s3-korvo2-v3-rndis") + set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) + set(BUILTIN_ICON_FONT font_awesome_20_4) + set(DEFAULT_EMOJI_COLLECTION twemoji_64) elseif(CONFIG_BOARD_TYPE_ESP_SPARKBOT) set(BOARD_TYPE "esp-sparkbot") set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 3e80405c..a0270a29 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -155,6 +155,9 @@ choice BOARD_TYPE config BOARD_TYPE_ESP_KORVO2_V3 bool "Espressif Korvo2 V3" depends on IDF_TARGET_ESP32S3 + config BOARD_TYPE_ESP_KORVO2_V3_RNDIS + bool "Espressif Korvo2 V3 RNDIS" + depends on IDF_TARGET_ESP32S3 config BOARD_TYPE_ESP_SPARKBOT bool "Espressif SparkBot" depends on IDF_TARGET_ESP32S3 @@ -568,7 +571,7 @@ choice DISPLAY_LCD_TYPE endchoice choice DISPLAY_ESP32S3_KORVO2_V3 - depends on BOARD_TYPE_ESP_KORVO2_V3 + depends on BOARD_TYPE_ESP_KORVO2_V3 || BOARD_TYPE_ESP_KORVO2_V3_RNDIS prompt "ESP32S3_KORVO2_V3 LCD Type" default ESP32S3_KORVO2_V3_LCD_ST7789 help diff --git a/main/boards/common/rndis_board.cc b/main/boards/common/rndis_board.cc new file mode 100644 index 00000000..c9c5219d --- /dev/null +++ b/main/boards/common/rndis_board.cc @@ -0,0 +1,247 @@ +#include "rndis_board.h" +#include "display.h" +#include "application.h" +#include "system_info.h" +#include "settings.h" +#include "assets/lang_config.h" + +#include +#include +#include +#include +#include +#include + +#if CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S3 + +static const char *TAG = "RndisBoard"; +#define EVENT_GOT_IP_BIT (1 << 0) + +RndisBoard::RndisBoard() { +} + +RndisBoard::~RndisBoard() { +} + +std::string RndisBoard::GetBoardType() { + return "rndis"; +} + +void RndisBoard::StartNetwork() { + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + /* NVS partition was truncated and needs to be erased + * Retry nvs_flash_init */ + ESP_ERROR_CHECK(nvs_flash_erase()); + ESP_ERROR_CHECK(nvs_flash_init()); + } + /* Initialize default TCP/IP stack */ + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + s_event_group = xEventGroupCreate(); + esp_event_handler_register(IOT_ETH_EVENT, ESP_EVENT_ANY_ID, iot_event_handle, this); + esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, iot_event_handle, this); + + // install usbh cdc driver + usbh_cdc_driver_config_t config = { + .task_stack_size = 1024 * 4, + .task_priority = configMAX_PRIORITIES - 1, + .task_coreid = 0, + .skip_init_usb_host_driver = false, + }; + ESP_ERROR_CHECK(usbh_cdc_driver_install(&config)); + + install_rndis(USB_DEVICE_VENDOR_ANY, USB_DEVICE_PRODUCT_ANY, "USB RNDIS0"); + xEventGroupWaitBits(s_event_group, EVENT_GOT_IP_BIT, pdFALSE, pdFALSE, portMAX_DELAY); +} + + +void RndisBoard::iot_event_handle(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) +{ + if (event_base == IOT_ETH_EVENT) { + switch (event_id) { + case IOT_ETH_EVENT_START: + ESP_LOGI(TAG, "IOT_ETH_EVENT_START"); + break; + case IOT_ETH_EVENT_STOP: + ESP_LOGI(TAG, "IOT_ETH_EVENT_STOP"); + break; + case IOT_ETH_EVENT_CONNECTED: + ESP_LOGI(TAG, "IOT_ETH_EVENT_CONNECTED"); + static_cast(arg)->OnNetworkEvent(NetworkEvent::Connected); + break; + case IOT_ETH_EVENT_DISCONNECTED: + ESP_LOGI(TAG, "IOT_ETH_EVENT_DISCONNECTED"); + xEventGroupClearBits(static_cast(arg)->s_event_group, EVENT_GOT_IP_BIT); + static_cast(arg)->OnNetworkEvent(NetworkEvent::Disconnected); + break; + default: + ESP_LOGI(TAG, "IOT_ETH_EVENT_UNKNOWN"); + break; + } + } else if (event_base == IP_EVENT) { + ESP_LOGI(TAG, "GOT_IP"); + xEventGroupSetBits(static_cast(arg)->s_event_group, EVENT_GOT_IP_BIT); + } +} + + +void RndisBoard::OnNetworkEvent(NetworkEvent event, const std::string& data) { + switch (event) { + case NetworkEvent::Connected: + ESP_LOGI(TAG, "Connected to WiFi: %s", data.c_str()); + break; + case NetworkEvent::Scanning: + ESP_LOGI(TAG, "WiFi scanning"); + break; + case NetworkEvent::Connecting: + ESP_LOGI(TAG, "WiFi connecting to %s", data.c_str()); + break; + case NetworkEvent::Disconnected: + ESP_LOGW(TAG, "WiFi disconnected"); + break; + default: + break; + } + + // Notify external callback if set + if (network_event_callback_) { + network_event_callback_(event, data); + } +} + +void RndisBoard::SetNetworkEventCallback(NetworkEventCallback callback) { + network_event_callback_ = std::move(callback); +} + +void RndisBoard::install_rndis(uint16_t idVendor, uint16_t idProduct, const char *netif_name) +{ + esp_err_t ret = ESP_OK; + iot_eth_handle_t eth_handle = nullptr; + iot_eth_netif_glue_handle_t glue = nullptr; + + usb_device_match_id_t *dev_match_id = (usb_device_match_id_t*)calloc(2, sizeof(usb_device_match_id_t)); + dev_match_id[0].match_flags = USB_DEVICE_ID_MATCH_VID_PID; + dev_match_id[0].idVendor = idVendor; + dev_match_id[0].idProduct = idProduct; + memset(&dev_match_id[1], 0, sizeof(usb_device_match_id_t)); // end of list + iot_usbh_rndis_config_t rndis_cfg = { + .match_id_list = dev_match_id, + }; + + ret = iot_eth_new_usb_rndis(&rndis_cfg, &rndis_eth_driver); + if (ret != ESP_OK || rndis_eth_driver == NULL) { + ESP_LOGE(TAG, "Failed to create USB RNDIS driver"); + return; + } + + iot_eth_config_t eth_cfg = { + .driver = rndis_eth_driver, + .stack_input = NULL, + }; + ret = iot_eth_install(ð_cfg, ð_handle); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to install USB RNDIS driver"); + return; + } + + esp_netif_inherent_config_t _inherent_eth_config = ESP_NETIF_INHERENT_DEFAULT_ETH(); + _inherent_eth_config.if_key = netif_name; + _inherent_eth_config.if_desc = netif_name; + esp_netif_config_t netif_cfg = { + .base = &_inherent_eth_config, + .driver = NULL, + .stack = ESP_NETIF_NETSTACK_DEFAULT_ETH, + }; + s_rndis_netif = esp_netif_new(&netif_cfg); + if (s_rndis_netif == NULL) { + ESP_LOGE(TAG, "Failed to create network interface"); + return; + } + + glue = iot_eth_new_netif_glue(eth_handle); + if (glue == NULL) { + ESP_LOGE(TAG, "Failed to create netif glue"); + return; + } + esp_netif_attach(s_rndis_netif, glue); + iot_eth_start(eth_handle); +} + + +NetworkInterface* RndisBoard::GetNetwork() { + static EspNetwork network; + return &network; +} + +const char* RndisBoard::GetNetworkStateIcon() { + return FONT_AWESOME_SIGNAL_STRONG; +} + +std::string RndisBoard::GetBoardJson() { + + std::string json = R"({"type":")" + std::string(BOARD_TYPE) + R"(",)"; + json += R"("name":")" + std::string(BOARD_NAME) + R"(",)"; + + json += R"("mac":")" + SystemInfo::GetMacAddress() + R"("})"; + return json; +} + +void RndisBoard::SetPowerSaveLevel(PowerSaveLevel level) { + +} + +std::string RndisBoard::GetDeviceStatusJson() { + auto& board = Board::GetInstance(); + auto root = cJSON_CreateObject(); + + // Audio speaker + auto audio_speaker = cJSON_CreateObject(); + if (auto codec = board.GetAudioCodec()) { + cJSON_AddNumberToObject(audio_speaker, "volume", codec->output_volume()); + } + cJSON_AddItemToObject(root, "audio_speaker", audio_speaker); + + // Screen + auto screen = cJSON_CreateObject(); + if (auto backlight = board.GetBacklight()) { + cJSON_AddNumberToObject(screen, "brightness", backlight->brightness()); + } + if (auto display = board.GetDisplay(); display && display->height() > 64) { + if (auto theme = display->GetTheme()) { + cJSON_AddStringToObject(screen, "theme", theme->name().c_str()); + } + } + cJSON_AddItemToObject(root, "screen", screen); + + // Battery + int level = 0; + bool charging = false, discharging = false; + if (board.GetBatteryLevel(level, charging, discharging)) { + auto battery = cJSON_CreateObject(); + cJSON_AddNumberToObject(battery, "level", level); + cJSON_AddBoolToObject(battery, "charging", charging); + cJSON_AddItemToObject(root, "battery", battery); + } + + // Network + auto network = cJSON_CreateObject(); + cJSON_AddStringToObject(network, "type", "rndis"); + cJSON_AddItemToObject(root, "network", network); + + // Chip temperature + float temp = 0.0f; + if (board.GetTemperature(temp)) { + auto chip = cJSON_CreateObject(); + cJSON_AddNumberToObject(chip, "temperature", temp); + cJSON_AddItemToObject(root, "chip", chip); + } + + auto str = cJSON_PrintUnformatted(root); + std::string result(str); + cJSON_free(str); + cJSON_Delete(root); + return result; +} +#endif // CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S3 \ No newline at end of file diff --git a/main/boards/common/rndis_board.h b/main/boards/common/rndis_board.h new file mode 100644 index 00000000..7e9f7526 --- /dev/null +++ b/main/boards/common/rndis_board.h @@ -0,0 +1,74 @@ +#ifndef RNDIS_BOARD_H +#define RNDIS_BOARD_H + +#include "sdkconfig.h" + +#if CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S3 +#include "board.h" +#include "iot_eth.h" +#include "iot_usbh_rndis.h" +#include "iot_eth_netif_glue.h" +#include +#include +#include +#include +#include + +class RndisBoard : public Board { +private: + EventGroupHandle_t s_event_group = nullptr; + iot_eth_driver_t *rndis_eth_driver = nullptr; + esp_netif_t *s_rndis_netif = nullptr; + + void install_rndis(uint16_t idVendor, uint16_t idProduct, const char *netif_name); + static void iot_event_handle(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); +protected: + NetworkEventCallback network_event_callback_ = nullptr; + + virtual std::string GetBoardJson() override; + + /** + * Handle network event (called from WiFi manager callbacks) + * @param event The network event type + * @param data Additional data (e.g., SSID for Connecting/Connected events) + */ + void OnNetworkEvent(NetworkEvent event, const std::string& data = ""); + + /** + * Start WiFi connection attempt + */ + void TryWifiConnect(); + + /** + * Enter WiFi configuration mode + */ + void StartWifiConfigMode(); + + /** + * WiFi connection timeout callback + */ + static void OnWifiConnectTimeout(void* arg); + +public: + RndisBoard(); + virtual ~RndisBoard(); + + virtual std::string GetBoardType() override; + + /** + * Start network connection asynchronously + * This function returns immediately. Network events are notified through the callback set by SetNetworkEventCallback(). + */ + virtual void StartNetwork() override; + + virtual NetworkInterface* GetNetwork() override; + virtual void SetNetworkEventCallback(NetworkEventCallback callback) override; + virtual const char* GetNetworkStateIcon() override; + virtual void SetPowerSaveLevel(PowerSaveLevel level) override; + virtual AudioCodec* GetAudioCodec() override { return nullptr; } + virtual std::string GetDeviceStatusJson() override; + +}; +#endif // CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S3 + +#endif // RNDIS_BOARD_H diff --git a/main/boards/esp32s3-korvo2-v3-rndis/config.h b/main/boards/esp32s3-korvo2-v3-rndis/config.h new file mode 100644 index 00000000..afb1dfab --- /dev/null +++ b/main/boards/esp32s3-korvo2-v3-rndis/config.h @@ -0,0 +1,81 @@ + +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_INPUT_REFERENCE true + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_16 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_45 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_9 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_10 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_8 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_48 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_17 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_18 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR +#define AUDIO_CODEC_ES7210_ADDR ES7210_CODEC_DEFAULT_ADDR + +#define BUILTIN_LED_GPIO GPIO_NUM_NC +#define BOOT_BUTTON_GPIO GPIO_NUM_5 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#ifdef CONFIG_ESP32S3_KORVO2_V3_LCD_ST7789 +#define DISPLAY_SDA_PIN GPIO_NUM_NC +#define DISPLAY_SCL_PIN GPIO_NUM_NC +#define DISPLAY_WIDTH 280 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_SWAP_XY true +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y true +#define BACKLIGHT_INVERT false + +#define DISPLAY_OFFSET_X 20 +#define DISPLAY_OFFSET_Y 0 +#endif + +#ifdef CONFIG_ESP32S3_KORVO2_V3_LCD_ILI9341 +#define LCD_TYPE_ILI9341_SERIAL +#define DISPLAY_SDA_PIN GPIO_NUM_NC +#define DISPLAY_SCL_PIN GPIO_NUM_NC +#define DISPLAY_WIDTH 320 +#define DISPLAY_HEIGHT 240 + +#define DISPLAY_SWAP_XY false +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y true +#define BACKLIGHT_INVERT false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#endif + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +/* Camera pins */ +#define CAMERA_PIN_PWDN GPIO_NUM_NC +#define CAMERA_PIN_RESET GPIO_NUM_NC +#define CAMERA_PIN_XCLK GPIO_NUM_40 +#define CAMERA_PIN_SIOD GPIO_NUM_17 +#define CAMERA_PIN_SIOC GPIO_NUM_18 + +#define CAMERA_PIN_D7 GPIO_NUM_39 +#define CAMERA_PIN_D6 GPIO_NUM_41 +#define CAMERA_PIN_D5 GPIO_NUM_42 +#define CAMERA_PIN_D4 GPIO_NUM_12 +#define CAMERA_PIN_D3 GPIO_NUM_3 +#define CAMERA_PIN_D2 GPIO_NUM_14 +#define CAMERA_PIN_D1 GPIO_NUM_47 +#define CAMERA_PIN_D0 GPIO_NUM_13 +#define CAMERA_PIN_VSYNC GPIO_NUM_21 +#define CAMERA_PIN_HREF GPIO_NUM_38 +#define CAMERA_PIN_PCLK GPIO_NUM_11 + +#define XCLK_FREQ_HZ 20000000 +#endif // _BOARD_CONFIG_H_ \ No newline at end of file diff --git a/main/boards/esp32s3-korvo2-v3-rndis/config.json b/main/boards/esp32s3-korvo2-v3-rndis/config.json new file mode 100644 index 00000000..c9605569 --- /dev/null +++ b/main/boards/esp32s3-korvo2-v3-rndis/config.json @@ -0,0 +1,17 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "esp32s3-korvo2-v3-rndis", + "sdkconfig_append": [ + "CONFIG_CAMERA_OV2640=y", + "CONFIG_CAMERA_OV3660=y", + "CONFIG_CAMERA_OV3660_AUTO_DETECT_DVP_INTERFACE_SENSOR=y", + "CONFIG_CAMERA_OV3660_DVP_RGB565_240X240_24FPS=y", + "CONFIG_CAMERA_OV2640_AUTO_DETECT_DVP_INTERFACE_SENSOR=y", + "CONFIG_CAMERA_OV2640_DVP_RGB565_240X240_25FPS=y", + "CONFIG_XIAOZHI_ENABLE_CAMERA_ENDIANNESS_SWAP=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/esp32s3-korvo2-v3-rndis/esp32s3_korvo2_v3_board.cc b/main/boards/esp32s3-korvo2-v3-rndis/esp32s3_korvo2_v3_board.cc new file mode 100644 index 00000000..434482a7 --- /dev/null +++ b/main/boards/esp32s3-korvo2-v3-rndis/esp32s3_korvo2_v3_board.cc @@ -0,0 +1,450 @@ +#include "rndis_board.h" +#include "codecs/box_audio_codec.h" +#include "display/lcd_display.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "i2c_device.h" +#include "assets/lang_config.h" + +#include +#include +#include +#include +#include +#include +#include "esp32_camera.h" +#include "power_manager.h" +#include "power_save_timer.h" + +#define TAG "esp32s3_korvo2_v3_rndis" +/* ADC Buttons */ +typedef enum { + BSP_ADC_BUTTON_REC, + BSP_ADC_BUTTON_VOL_MUTE, + BSP_ADC_BUTTON_PLAY, + BSP_ADC_BUTTON_SET, + BSP_ADC_BUTTON_VOL_DOWN, + BSP_ADC_BUTTON_VOL_UP, + BSP_ADC_BUTTON_NUM +} bsp_adc_button_t; + +// Init ili9341 by custom cmd +static const ili9341_lcd_init_cmd_t vendor_specific_init[] = { + {0xC8, (uint8_t []){0xFF, 0x93, 0x42}, 3, 0}, + {0xC0, (uint8_t []){0x0E, 0x0E}, 2, 0}, + {0xC5, (uint8_t []){0xD0}, 1, 0}, + {0xC1, (uint8_t []){0x02}, 1, 0}, + {0xB4, (uint8_t []){0x02}, 1, 0}, + {0xE0, (uint8_t []){0x00, 0x03, 0x08, 0x06, 0x13, 0x09, 0x39, 0x39, 0x48, 0x02, 0x0a, 0x08, 0x17, 0x17, 0x0F}, 15, 0}, + {0xE1, (uint8_t []){0x00, 0x28, 0x29, 0x01, 0x0d, 0x03, 0x3f, 0x33, 0x52, 0x04, 0x0f, 0x0e, 0x37, 0x38, 0x0F}, 15, 0}, + + {0xB1, (uint8_t []){00, 0x1B}, 2, 0}, + {0x36, (uint8_t []){0x08}, 1, 0}, + {0x3A, (uint8_t []){0x55}, 1, 0}, + {0xB7, (uint8_t []){0x06}, 1, 0}, + + {0x11, (uint8_t []){0}, 0x80, 0}, + {0x29, (uint8_t []){0}, 0x80, 0}, + + {0, (uint8_t []){0}, 0xff, 0}, +}; + +class Esp32S3Korvo2V3Board : public RndisBoard { +private: + Button boot_button_; + Button* adc_button_[BSP_ADC_BUTTON_NUM]; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + adc_oneshot_unit_handle_t bsp_adc_handle = NULL; +#endif + i2c_master_bus_handle_t i2c_bus_; + LcdDisplay* display_; + esp_io_expander_handle_t io_expander_ = NULL; + Esp32Camera* camera_; + PowerSaveTimer* power_save_timer_; + PowerManager* power_manager_; + void InitializePowerManager() { + // PowerManager需要复用按钮的ADC句柄,所以在InitializeButtons之后调用 + // 传入按钮的ADC句柄指针,让PowerManager复用 + power_manager_ = new PowerManager(GPIO_NUM_NC, &bsp_adc_handle); + } + + void InitializePowerSaveTimer() { + power_save_timer_ = new PowerSaveTimer(-1, 60); + power_save_timer_->OnEnterSleepMode([this]() { + GetDisplay()->SetPowerSaveMode(true); + }); + power_save_timer_->OnExitSleepMode([this]() { + GetDisplay()->SetPowerSaveMode(false); + }); + power_save_timer_->SetEnabled(true); + } + + void InitializeI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = (i2c_port_t)1, + .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, &i2c_bus_)); + } + + void I2cDetect() { + uint8_t address; + printf(" 0 1 2 3 4 5 6 7 8 9 a b c d e f\r\n"); + for (int i = 0; i < 128; i += 16) { + printf("%02x: ", i); + for (int j = 0; j < 16; j++) { + fflush(stdout); + address = i + j; + esp_err_t ret = i2c_master_probe(i2c_bus_, address, pdMS_TO_TICKS(200)); + if (ret == ESP_OK) { + printf("%02x ", address); + } else if (ret == ESP_ERR_TIMEOUT) { + printf("UU "); + } else { + printf("-- "); + } + } + printf("\r\n"); + } + } + + void InitializeTca9554() { + esp_err_t ret = esp_io_expander_new_i2c_tca9554(i2c_bus_, ESP_IO_EXPANDER_I2C_TCA9554_ADDRESS_000, &io_expander_); + if(ret != ESP_OK) { + ret = esp_io_expander_new_i2c_tca9554(i2c_bus_, ESP_IO_EXPANDER_I2C_TCA9554A_ADDRESS_000, &io_expander_); + if(ret != ESP_OK) { + ESP_LOGE(TAG, "TCA9554 create returned error"); + return; + } + } + // 配置IO0-IO3为输出模式 + ESP_ERROR_CHECK(esp_io_expander_set_dir(io_expander_, + IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1 | + IO_EXPANDER_PIN_NUM_2 | IO_EXPANDER_PIN_NUM_3, + IO_EXPANDER_OUTPUT)); + + // 复位LCD和TouchPad + ESP_ERROR_CHECK(esp_io_expander_set_level(io_expander_, + IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1 | IO_EXPANDER_PIN_NUM_2, 1)); + vTaskDelay(pdMS_TO_TICKS(300)); + ESP_ERROR_CHECK(esp_io_expander_set_level(io_expander_, + IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1 | IO_EXPANDER_PIN_NUM_2, 0)); + vTaskDelay(pdMS_TO_TICKS(300)); + ESP_ERROR_CHECK(esp_io_expander_set_level(io_expander_, + IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1 | IO_EXPANDER_PIN_NUM_2, 1)); + } + + void EnableLcdCs() { + if(io_expander_ != NULL) { + esp_io_expander_set_level(io_expander_, IO_EXPANDER_PIN_NUM_3, 0);// 置低 LCD CS + } + } + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = GPIO_NUM_0; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = GPIO_NUM_1; + buscfg.quadwp_io_num = GPIO_NUM_NC; + buscfg.quadhd_io_num = GPIO_NUM_NC; + buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t); + ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO)); + } + + void ChangeVol(int val) { + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() + val; + if (volume > 100) { + volume = 100; + } + if (volume < 0) { + volume = 0; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + } + + void MuteVol() { + auto codec = GetAudioCodec(); + auto volume = codec->output_volume(); + if (volume > 1) { + volume = 0; + } else { + volume = 50; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + } + + void InitializeButtons() { + button_adc_config_t adc_cfg = {}; + adc_cfg.adc_channel = ADC_CHANNEL_4; // ADC1 channel 0 is GPIO5 +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + const adc_oneshot_unit_init_cfg_t init_config1 = { + .unit_id = ADC_UNIT_1, + }; + adc_oneshot_new_unit(&init_config1, &bsp_adc_handle); + adc_cfg.adc_handle = &bsp_adc_handle; +#endif + adc_cfg.button_index = BSP_ADC_BUTTON_REC; + adc_cfg.min = 2310; // middle is 2410mV + adc_cfg.max = 2510; + adc_button_[0] = new AdcButton(adc_cfg); + + adc_cfg.button_index = BSP_ADC_BUTTON_VOL_MUTE; + adc_cfg.min = 1880; // middle is 1980mV + adc_cfg.max = 2080; + adc_button_[1] = new AdcButton(adc_cfg); + + adc_cfg.button_index = BSP_ADC_BUTTON_PLAY; + adc_cfg.min = 1550; // middle is 1650mV + adc_cfg.max = 1750; + adc_button_[2] = new AdcButton(adc_cfg); + + adc_cfg.button_index = BSP_ADC_BUTTON_SET; + adc_cfg.min = 1015; // middle is 1115mV + adc_cfg.max = 1215; + adc_button_[3] = new AdcButton(adc_cfg); + + adc_cfg.button_index = BSP_ADC_BUTTON_VOL_DOWN; + adc_cfg.min = 720; // middle is 820mV + adc_cfg.max = 920; + adc_button_[4] = new AdcButton(adc_cfg); + + adc_cfg.button_index = BSP_ADC_BUTTON_VOL_UP; + adc_cfg.min = 280; // middle is 380mV + adc_cfg.max = 480; + adc_button_[5] = new AdcButton(adc_cfg); + + auto volume_up_button = adc_button_[BSP_ADC_BUTTON_VOL_UP]; + volume_up_button->OnClick([this]() {ChangeVol(10);}); + volume_up_button->OnLongPress([this]() { + GetAudioCodec()->SetOutputVolume(100); + GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME); + }); + + auto volume_down_button = adc_button_[BSP_ADC_BUTTON_VOL_DOWN]; + volume_down_button->OnClick([this]() {ChangeVol(-10);}); + volume_down_button->OnLongPress([this]() { + GetAudioCodec()->SetOutputVolume(0); + GetDisplay()->ShowNotification(Lang::Strings::MUTED); + }); + + auto volume_mute_button = adc_button_[BSP_ADC_BUTTON_VOL_MUTE]; + volume_mute_button->OnClick([this]() {MuteVol();}); + + auto play_button = adc_button_[BSP_ADC_BUTTON_PLAY]; + play_button->OnClick([this]() { + ESP_LOGI(TAG, " TODO %s:%d\n", __func__, __LINE__); + }); + + auto set_button = adc_button_[BSP_ADC_BUTTON_SET]; + set_button->OnClick([this]() { + }); + + auto rec_button = adc_button_[BSP_ADC_BUTTON_REC]; + rec_button->OnClick([this]() { + Application::GetInstance().ToggleChatState(); + }); + boot_button_.OnClick([this]() {}); + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + app.ToggleChatState(); + }); + +#if CONFIG_USE_DEVICE_AEC + boot_button_.OnDoubleClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateIdle) { + app.SetAecMode(app.GetAecMode() == kAecOff ? kAecOnDeviceSide : kAecOff); + } + }); +#endif + } + + void InitializeIli9341Display() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = GPIO_NUM_NC; + io_config.dc_gpio_num = GPIO_NUM_2; + io_config.spi_mode = 0; + io_config.pclk_hz = 40 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片 + ESP_LOGD(TAG, "Install LCD driver"); + const ili9341_vendor_config_t vendor_config = { + .init_cmds = &vendor_specific_init[0], + .init_cmds_size = sizeof(vendor_specific_init) / sizeof(ili9341_lcd_init_cmd_t), + }; + + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = GPIO_NUM_NC; + // panel_config.flags.reset_active_high = 0, + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + panel_config.vendor_config = (void *)&vendor_config; + ESP_ERROR_CHECK(esp_lcd_new_panel_ili9341(panel_io, &panel_config, &panel)); + + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel)); + EnableLcdCs(); + ESP_ERROR_CHECK(esp_lcd_panel_init(panel)); + ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY)); + ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y)); + ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel, false)); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel, true)); + display_ = new SpiLcdDisplay(panel_io, panel, + DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); + } + + void InitializeSt7789Display() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = GPIO_NUM_46; + io_config.dc_gpio_num = GPIO_NUM_2; + io_config.spi_mode = 0; + io_config.pclk_hz = 60 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片ST7789 + ESP_LOGD(TAG, "Install LCD driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = GPIO_NUM_NC; + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel)); + EnableLcdCs(); + ESP_ERROR_CHECK(esp_lcd_panel_init(panel)); + ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY)); + ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y)); + ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel, true)); + + display_ = new SpiLcdDisplay(panel_io, panel, + DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); + } + + void InitializeCamera() { + static esp_cam_ctlr_dvp_pin_config_t dvp_pin_config = { + .data_width = CAM_CTLR_DATA_WIDTH_8, + .data_io = { + [0] = CAMERA_PIN_D0, + [1] = CAMERA_PIN_D1, + [2] = CAMERA_PIN_D2, + [3] = CAMERA_PIN_D3, + [4] = CAMERA_PIN_D4, + [5] = CAMERA_PIN_D5, + [6] = CAMERA_PIN_D6, + [7] = CAMERA_PIN_D7, + }, + .vsync_io = CAMERA_PIN_VSYNC, + .de_io = CAMERA_PIN_HREF, + .pclk_io = CAMERA_PIN_PCLK, + .xclk_io = CAMERA_PIN_XCLK, + }; + + esp_video_init_sccb_config_t sccb_config = { + .init_sccb = false, + .i2c_handle = i2c_bus_, + .freq = 100000, + }; + + esp_video_init_dvp_config_t dvp_config = { + .sccb_config = sccb_config, + .reset_pin = CAMERA_PIN_RESET, + .pwdn_pin = CAMERA_PIN_PWDN, + .dvp_pin = dvp_pin_config, + .xclk_freq = XCLK_FREQ_HZ, + }; + + esp_video_init_config_t video_config = { + .dvp = &dvp_config, + }; + + camera_ = new Esp32Camera(video_config); + } + +public: + Esp32S3Korvo2V3Board() : boot_button_(BOOT_BUTTON_GPIO) { + ESP_LOGI(TAG, "Initializing esp32s3_korvo2_v3 Board"); + InitializePowerSaveTimer(); + InitializeI2c(); + I2cDetect(); + InitializeTca9554(); + InitializeCamera(); + InitializeSpi(); + InitializeButtons(); // 先初始化按钮(创建ADC1句柄) + InitializePowerManager(); // 后初始化PowerManager(复用ADC1句柄) + #ifdef LCD_TYPE_ILI9341_SERIAL + InitializeIli9341Display(); + #else + InitializeSt7789Display(); + #endif + } + + virtual AudioCodec* GetAudioCodec() override { + static BoxAudioCodec audio_codec( + i2c_bus_, + 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, + AUDIO_CODEC_ES7210_ADDR, + AUDIO_INPUT_REFERENCE); + return &audio_codec; + } + + virtual Display *GetDisplay() override { + return display_; + } + virtual Camera* GetCamera() override { + return camera_; + } + 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; + } + + virtual void SetPowerSaveLevel(PowerSaveLevel level) override { + if (level != PowerSaveLevel::LOW_POWER) { + power_save_timer_->WakeUp(); + } + } +}; + +DECLARE_BOARD(Esp32S3Korvo2V3Board); diff --git a/main/boards/esp32s3-korvo2-v3-rndis/power_manager.h b/main/boards/esp32s3-korvo2-v3-rndis/power_manager.h new file mode 100644 index 00000000..49a69562 --- /dev/null +++ b/main/boards/esp32s3-korvo2-v3-rndis/power_manager.h @@ -0,0 +1,249 @@ +#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 adc_values_; + uint32_t battery_level_ = 0; + bool is_charging_ = false; + bool is_low_battery_ = false; + int ticks_ = 0; + const int kBatteryAdcInterval = 60; + const int kBatteryAdcDataCount = 3; + const int kLowBatteryLevel = 20; + + adc_oneshot_unit_handle_t adc_handle_; + bool adc_handle_owned_ = false; // 标记ADC句柄是否由本类创建 + adc_cali_handle_t adc_cali_handle_ = nullptr; // ADC校准句柄 + + 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 (adc_values_.size() < kBatteryAdcDataCount) { + ReadBatteryAdcData(); + return; + } + + // 如果电池电量数据充足,则每 kBatteryAdcInterval 个 tick 读取一次电池电量数据 + ticks_++; + if (ticks_ % kBatteryAdcInterval == 0) { + ReadBatteryAdcData(); + } + } + + void ReadBatteryAdcData() { + int adc_raw = 0; + int voltage_mv = 0; // ADC校准后的电压(mV) + + // 多次采样取平均,提高稳定性 + uint32_t adc_sum = 0; + const int sample_count = 10; + for (int i = 0; i < sample_count; i++) { + int temp_raw = 0; + ESP_ERROR_CHECK(adc_oneshot_read(adc_handle_, ADC_CHANNEL_5, &temp_raw)); + adc_sum += temp_raw; + } + adc_raw = adc_sum / sample_count; + + // 使用ADC校准获取准确电压 + if (adc_cali_handle_) { + ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc_cali_handle_, adc_raw, &voltage_mv)); + } else { + // 如果没有校准,使用线性计算 + voltage_mv = (int)(adc_raw * 3300.0f / 4095.0f); + } + + // 根据分压比计算实际电池电压 + // 电路分压比: R21/(R20+R21) = 100K/300K = 1/3 + // 实际电池电压 = ADC测量电压 × 3 + int battery_voltage_mv = voltage_mv * 3; + + // 将电压值添加到队列中用于平滑 + adc_values_.push_back(battery_voltage_mv); + if (adc_values_.size() > kBatteryAdcDataCount) { + adc_values_.erase(adc_values_.begin()); + } + + uint32_t average_voltage = 0; + for (auto value : adc_values_) { + average_voltage += value; + } + average_voltage /= adc_values_.size(); + + // 定义电池电量区间(基于实际电池电压,单位mV) + const struct { + uint16_t voltage_mv; // 电池电压(mV) + uint8_t level; // 电量百分比 + } levels[] = { + {3500, 0}, // 3.5V + {3640, 20}, // 3.64V + {3760, 40}, // 3.76V + {3880, 60}, // 3.88V + {4000, 80}, // 4.0V + {4200, 100} // 4.2V + }; + + // 低于最低值时 + if (average_voltage < levels[0].voltage_mv) { + battery_level_ = 0; + } + // 高于最高值时 + else if (average_voltage >= levels[5].voltage_mv) { + battery_level_ = 100; + } else { + // 线性插值计算中间值 + for (int i = 0; i < 5; i++) { + if (average_voltage >= levels[i].voltage_mv && average_voltage < levels[i+1].voltage_mv) { + float ratio = static_cast(average_voltage - levels[i].voltage_mv) / + (levels[i+1].voltage_mv - levels[i].voltage_mv); + battery_level_ = levels[i].level + ratio * (levels[i+1].level - levels[i].level); + break; + } + } + } + + // Check low battery status + if (adc_values_.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_); + } + } + } + + ESP_LOGI("PowerManager", "ADC raw: %d, ADC voltage: %dmV, Battery: %ldmV (%.2fV), level: %ld%%", + adc_raw, voltage_mv, average_voltage, average_voltage/1000.0f, battery_level_); + } + +public: + // 构造函数:使用外部ADC句柄(用于复用已存在的ADC) + PowerManager(gpio_num_t pin, adc_oneshot_unit_handle_t* external_adc_handle = nullptr) + : charging_pin_(pin), adc_handle_owned_(false) { + if(charging_pin_ != GPIO_NUM_NC){ + // 初始化充电引脚 + 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 + if (external_adc_handle != nullptr && *external_adc_handle != nullptr) { + // 复用外部ADC句柄 + adc_handle_ = *external_adc_handle; + adc_handle_owned_ = false; + } else { + // 创建新的ADC句柄 + adc_oneshot_unit_init_cfg_t init_config = { + .unit_id = ADC_UNIT_1, // GPIO6 对应 ADC1 + .ulp_mode = ADC_ULP_MODE_DISABLE, + }; + ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config, &adc_handle_)); + adc_handle_owned_ = true; + } + + // 配置ADC通道 + 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_5, &chan_config)); // GPIO6 = ADC1_CHANNEL_5 + + // 初始化ADC校准 + adc_cali_curve_fitting_config_t cali_config = { + .unit_id = ADC_UNIT_1, + .chan = ADC_CHANNEL_5, + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_12, + }; + esp_err_t ret = adc_cali_create_scheme_curve_fitting(&cali_config, &adc_cali_handle_); + if (ret == ESP_OK) { + ESP_LOGI("PowerManager", "ADC calibration initialized successfully"); + } else { + ESP_LOGW("PowerManager", "ADC calibration failed, using linear calculation"); + adc_cali_handle_ = nullptr; + } + } + + ~PowerManager() { + if (timer_handle_) { + esp_timer_stop(timer_handle_); + esp_timer_delete(timer_handle_); + } + // 删除ADC校准句柄 + if (adc_cali_handle_) { + adc_cali_delete_scheme_curve_fitting(adc_cali_handle_); + } + // 只有当ADC句柄是本类创建的时候才删除 + if (adc_handle_ && adc_handle_owned_) { + 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/idf_component.yml b/main/idf_component.yml index 4a5a2aad..2256f02e 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -118,3 +118,7 @@ dependencies: idf: version: '>=5.5.1' espressif/esp_lcd_touch_st7123: ^1.0.0 + espressif/iot_usbh_rndis: + version: ^0.3.1 + rules: + - if: target in [esp32s3, esp32p4]