From 908c9d5708349a0955200595ef494e88df4f10cd Mon Sep 17 00:00:00 2001 From: laride <198868291+laride@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:52:49 +0800 Subject: [PATCH] feat: add esp-spot c5 (#1462) * feat: add esp-spot c5 * fix: fix table custom filename --- main/CMakeLists.txt | 4 +- main/Kconfig.projbuild | 3 + main/boards/esp-spot-s3/config.h | 35 -- main/boards/esp-spot-s3/esp_spot_s3_board.cc | 242 ---------- .../{esp-spot-s3 => esp-spot}/README.md | 18 +- main/boards/esp-spot/config.h | 79 ++++ main/boards/esp-spot/config.json | 14 + main/boards/esp-spot/esp_spot_board.cc | 437 ++++++++++++++++++ main/idf_component.yml | 5 + sdkconfig.defaults.esp32c5 | 3 + 10 files changed, 557 insertions(+), 283 deletions(-) delete mode 100644 main/boards/esp-spot-s3/config.h delete mode 100644 main/boards/esp-spot-s3/esp_spot_s3_board.cc rename main/boards/{esp-spot-s3 => esp-spot}/README.md (66%) create mode 100644 main/boards/esp-spot/config.h create mode 100644 main/boards/esp-spot/config.json create mode 100644 main/boards/esp-spot/esp_spot_board.cc diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index ed0db50a..017a3c32 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -200,7 +200,9 @@ elseif(CONFIG_BOARD_TYPE_ESP_SPARKBOT) set(BUILTIN_ICON_FONT font_awesome_20_4) set(DEFAULT_EMOJI_COLLECTION twemoji_64) elseif(CONFIG_BOARD_TYPE_ESP_SPOT_S3) - set(BOARD_TYPE "esp-spot-s3") + set(BOARD_TYPE "esp-spot") +elseif(CONFIG_BOARD_TYPE_ESP_SPOT_C5) + set(BOARD_TYPE "esp-spot") elseif(CONFIG_BOARD_TYPE_ESP_HI) set(BOARD_TYPE "esp-hi") # Set ESP_HI emoji directory for DEFAULT_ASSETS_EXTRA_FILES diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 2a1ddff9..690b7718 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -153,6 +153,9 @@ choice BOARD_TYPE config BOARD_TYPE_ESP_SPOT_S3 bool "Espressif Spot-S3" depends on IDF_TARGET_ESP32S3 + config BOARD_TYPE_ESP_SPOT_C5 + bool "Espressif Spot-C5" + depends on IDF_TARGET_ESP32C5 config BOARD_TYPE_ESP_HI bool "Espressif ESP-HI" depends on IDF_TARGET_ESP32C3 diff --git a/main/boards/esp-spot-s3/config.h b/main/boards/esp-spot-s3/config.h deleted file mode 100644 index 0acf05cc..00000000 --- a/main/boards/esp-spot-s3/config.h +++ /dev/null @@ -1,35 +0,0 @@ -#ifndef _BOARD_CONFIG_H_ -#define _BOARD_CONFIG_H_ - -#include - -#define AUDIO_INPUT_SAMPLE_RATE 16000 -#define AUDIO_OUTPUT_SAMPLE_RATE 16000 - -#define AUDIO_INPUT_REFERENCE false - -#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_8 -#define AUDIO_I2S_GPIO_WS GPIO_NUM_17 -#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_16 -#define AUDIO_I2S_GPIO_DIN GPIO_NUM_15 -#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_18 - -#define AUDIO_CODEC_PA_PIN GPIO_NUM_40 -#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_2 -#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_1 -#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR - -#define BOOT_BUTTON_GPIO GPIO_NUM_0 -#define KEY_BUTTON_GPIO GPIO_NUM_12 -#define LED_PIN GPIO_NUM_11 - -#define VBAT_ADC_CHANNEL ADC_CHANNEL_9 // S3: IO10 -#define MCU_VCC_CTL GPIO_NUM_4 // set 1 to power on MCU -#define PERP_VCC_CTL GPIO_NUM_6 // set 1 to power on peripherals - -#define ADC_ATTEN ADC_ATTEN_DB_12 -#define ADC_WIDTH ADC_BITWIDTH_DEFAULT -#define FULL_BATTERY_VOLTAGE 4100 -#define EMPTY_BATTERY_VOLTAGE 3200 - -#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/esp-spot-s3/esp_spot_s3_board.cc b/main/boards/esp-spot-s3/esp_spot_s3_board.cc deleted file mode 100644 index 0ff61397..00000000 --- a/main/boards/esp-spot-s3/esp_spot_s3_board.cc +++ /dev/null @@ -1,242 +0,0 @@ -#include "wifi_board.h" -#include "codecs/es8311_audio_codec.h" -#include "application.h" -#include "button.h" -#include "config.h" -#include "sdkconfig.h" - -#include -#include -#include -#include -#include "esp_adc/adc_oneshot.h" -#include "esp_adc/adc_cali.h" -#include "esp_adc/adc_cali_scheme.h" - -#include -#include "esp_timer.h" -#include "led/circular_strip.h" - -#define TAG "esp_spot_s3" - -bool button_released_ = false; -bool shutdown_ready_ = false; -esp_timer_handle_t shutdown_timer; - -class EspSpotS3Bot : public WifiBoard { -private: - i2c_master_bus_handle_t i2c_bus_; - Button boot_button_; - Button key_button_; - adc_oneshot_unit_handle_t adc1_handle; - adc_cali_handle_t adc1_cali_handle; - bool do_calibration = false; - bool key_long_pressed = false; - int64_t last_key_press_time = 0; - static const int64_t LONG_PRESS_TIMEOUT_US = 5 * 1000000ULL; - - void InitializeI2c() { - // 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, &i2c_bus_)); - } - - void InitializeADC() { - adc_oneshot_unit_init_cfg_t init_config1 = { - .unit_id = ADC_UNIT_1 - }; - ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config1, &adc1_handle)); - - adc_oneshot_chan_cfg_t chan_config = { - .atten = ADC_ATTEN, - .bitwidth = ADC_WIDTH, - }; - ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, VBAT_ADC_CHANNEL, &chan_config)); - - adc_cali_handle_t handle = NULL; - esp_err_t ret = ESP_FAIL; - -#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED - adc_cali_curve_fitting_config_t cali_config = { - .unit_id = ADC_UNIT_1, - .atten = ADC_ATTEN, - .bitwidth = ADC_WIDTH, - }; - ret = adc_cali_create_scheme_curve_fitting(&cali_config, &handle); - if (ret == ESP_OK) { - do_calibration = true; - adc1_cali_handle = handle; - ESP_LOGI(TAG, "ADC Curve Fitting calibration succeeded"); - } -#endif // ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED - } - - void InitializeButtons() { - boot_button_.OnClick([this]() { - auto& app = Application::GetInstance(); - ResetWifiConfiguration(); - }); - - key_button_.OnClick([this]() { - auto& app = Application::GetInstance(); - app.ToggleChatState(); - key_long_pressed = false; - }); - - key_button_.OnLongPress([this]() { - int64_t now = esp_timer_get_time(); - auto* led = static_cast(this->GetLed()); - - if (key_long_pressed) { - if ((now - last_key_press_time) < LONG_PRESS_TIMEOUT_US) { - ESP_LOGW(TAG, "Key button long pressed the second time within 5s, shutting down..."); - led->SetSingleColor(0, {0, 0, 0}); - - gpio_hold_dis(MCU_VCC_CTL); - gpio_set_level(MCU_VCC_CTL, 0); - - } else { - last_key_press_time = now; - BlinkGreenFor5s(); - } - key_long_pressed = true; - } else { - ESP_LOGW(TAG, "Key button first long press! Waiting second within 5s to shutdown..."); - last_key_press_time = now; - key_long_pressed = true; - - BlinkGreenFor5s(); - } - }); - } - - void InitializePowerCtl() { - InitializeGPIO(); - - gpio_set_level(MCU_VCC_CTL, 1); - gpio_hold_en(MCU_VCC_CTL); - - gpio_set_level(PERP_VCC_CTL, 1); - gpio_hold_en(PERP_VCC_CTL); - } - - void InitializeGPIO() { - gpio_config_t io_pa = { - .pin_bit_mask = (1ULL << AUDIO_CODEC_PA_PIN), - .mode = GPIO_MODE_OUTPUT, - .pull_up_en = GPIO_PULLUP_DISABLE, - .pull_down_en = GPIO_PULLDOWN_DISABLE, - .intr_type = GPIO_INTR_DISABLE - }; - gpio_config(&io_pa); - gpio_set_level(AUDIO_CODEC_PA_PIN, 0); - - gpio_config_t io_conf_1 = { - .pin_bit_mask = (1ULL << MCU_VCC_CTL), - .mode = GPIO_MODE_OUTPUT, - .pull_up_en = GPIO_PULLUP_DISABLE, - .pull_down_en = GPIO_PULLDOWN_DISABLE, - .intr_type = GPIO_INTR_DISABLE - }; - gpio_config(&io_conf_1); - - gpio_config_t io_conf_2 = { - .pin_bit_mask = (1ULL << PERP_VCC_CTL), - .mode = GPIO_MODE_OUTPUT, - .pull_up_en = GPIO_PULLUP_DISABLE, - .pull_down_en = GPIO_PULLDOWN_DISABLE, - .intr_type = GPIO_INTR_DISABLE - }; - gpio_config(&io_conf_2); - } - - void BlinkGreenFor5s() { - auto* led = static_cast(GetLed()); - if (!led) { - return; - } - - led->Blink({50, 25, 0}, 100); - - esp_timer_create_args_t timer_args = { - .callback = [](void* arg) { - auto* self = static_cast(arg); - auto* led = static_cast(self->GetLed()); - if (led) { - led->SetSingleColor(0, {0, 0, 0}); - } - }, - .arg = this, - .dispatch_method = ESP_TIMER_TASK, - .name = "blinkGreenFor5s_timer" - }; - - esp_timer_handle_t blink_timer = nullptr; - ESP_ERROR_CHECK(esp_timer_create(&timer_args, &blink_timer)); - ESP_ERROR_CHECK(esp_timer_start_once(blink_timer, LONG_PRESS_TIMEOUT_US)); - } - -public: - EspSpotS3Bot() : boot_button_(BOOT_BUTTON_GPIO), key_button_(KEY_BUTTON_GPIO, true) { - InitializePowerCtl(); - InitializeADC(); - InitializeI2c(); - InitializeButtons(); - } - - virtual Led* GetLed() override { - static CircularStrip led(LED_PIN, 1); - return &led; - } - - virtual AudioCodec* GetAudioCodec() override { - static Es8311AudioCodec audio_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, false); - return &audio_codec; - } - - virtual bool GetBatteryLevel(int &level, bool &charging, bool &discharging) { - if (!adc1_handle) { - InitializeADC(); - } - - int raw_value = 0; - int voltage = 0; - - ESP_ERROR_CHECK(adc_oneshot_read(adc1_handle, VBAT_ADC_CHANNEL, &raw_value)); - - if (do_calibration) { - ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc1_cali_handle, raw_value, &voltage)); - voltage = voltage * 3 / 2; // compensate for voltage divider - ESP_LOGI(TAG, "Calibrated voltage: %d mV", voltage); - } else { - ESP_LOGI(TAG, "Raw ADC value: %d", raw_value); - voltage = raw_value; - } - - voltage = voltage < EMPTY_BATTERY_VOLTAGE ? EMPTY_BATTERY_VOLTAGE : voltage; - voltage = voltage > FULL_BATTERY_VOLTAGE ? FULL_BATTERY_VOLTAGE : voltage; - - // 计算电量百分比 - level = (voltage - EMPTY_BATTERY_VOLTAGE) * 100 / (FULL_BATTERY_VOLTAGE - EMPTY_BATTERY_VOLTAGE); - - charging = gpio_get_level(MCU_VCC_CTL); - ESP_LOGI(TAG, "Battery Level: %d%%, Charging: %s", level, charging ? "Yes" : "No"); - return true; - } -}; - -DECLARE_BOARD(EspSpotS3Bot); diff --git a/main/boards/esp-spot-s3/README.md b/main/boards/esp-spot/README.md similarity index 66% rename from main/boards/esp-spot-s3/README.md rename to main/boards/esp-spot/README.md index b63ef3a8..c13054f4 100644 --- a/main/boards/esp-spot-s3/README.md +++ b/main/boards/esp-spot/README.md @@ -1,4 +1,4 @@ -# ESP-Spot S3 +# ESP-Spot ## 简介 @@ -10,15 +10,17 @@ ESP-Spot 是 ESP Friends 开源的一款智能语音交互盒子,内置麦克风、扬声器、IMU 惯性传感器,可使用电池供电。ESP-Spot 不带屏幕,带有一个 RGB 指示灯和两个按钮。硬件详情可查看[立创开源项目](https://oshwhub.com/esp-college/esp-spot)。 -ESP-Spot 开源项目采用 ESP32-S3-WROOM-1-N16R8 模组。如在复刻时使用了其他大小的 Flash,需修改对应的参数。 +ESP-Spot 开源项目采用 ESP32-S3-WROOM-1-N16R8 模组或 ESP32-C5-WROOM-1-N8R8。如在复刻时使用了其他大小的 Flash,需修改对应的参数。 ## 配置、编译命令 -**配置编译目标为 ESP32S3** +**配置编译目标** ```bash -idf.py set-target esp32s3 +idf.py set-target esp32s3 # Spot S3 +# or +idf.py set-target esp32c5 # Spot C5 ``` **打开 menuconfig 并配置** @@ -29,7 +31,7 @@ idf.py menuconfig 分别配置如下选项: -- `Xiaozhi Assistant` → `Board Type` → 选择 `ESP-Spot-S3` +- `Xiaozhi Assistant` → `Board Type` → 选择 `ESP-Spot-S3` / `ESP-Spot-C5` 按 `S` 保存,按 `Q` 退出。 @@ -53,3 +55,9 @@ idf.py flash > 3. 按住 BOOT 同时插回 PCB 版,注意不要颠倒; > > 此时, ESP-Spot 应当已进入下载模式。在烧录完成后,可能需要重新插拔 PCB 板。 + +## 低功耗 + +ESP-Spot 支持 Deep Sleep 低功耗模式。 + +当处于 idle 状态 10 分钟后,ESP-Spot 会自动进入 Deep Sleep 模式,按 Key 键或摇晃 ESP-Spot 即可唤醒。 diff --git a/main/boards/esp-spot/config.h b/main/boards/esp-spot/config.h new file mode 100644 index 00000000..df6427d4 --- /dev/null +++ b/main/boards/esp-spot/config.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include "sdkconfig.h" + +/* Audio configuration */ +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 16000 +#define AUDIO_INPUT_REFERENCE false +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR + +/* ADC configuration */ +#define ADC_ATTEN ADC_ATTEN_DB_12 +#define ADC_WIDTH ADC_BITWIDTH_DEFAULT +#define FULL_BATTERY_VOLTAGE 4100 +#define EMPTY_BATTERY_VOLTAGE 3200 + +/* I2C configuration */ +#define I2C_MASTER_FREQ_HZ (400 * 1000) + +/* Button configuration */ +#define LONG_PRESS_TIMEOUT_US (5 * 1000000ULL) + +#ifdef CONFIG_IDF_TARGET_ESP32S3 + +/* Audio I2S GPIOs */ +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_NC +#define AUDIO_I2S_GPIO_WS GPIO_NUM_17 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_16 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_15 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_18 + +/* Audio CODEC GPIOs */ +#define AUDIO_CODEC_PA_PIN GPIO_NUM_40 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_2 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_1 + +/* Button GPIOs */ +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define KEY_BUTTON_GPIO GPIO_NUM_12 +#define LED_GPIO GPIO_NUM_11 + +/* ADC GPIOs */ +#define VBAT_ADC_CHANNEL ADC_CHANNEL_9 // S3: IO10 +#define MCU_VCC_CTL GPIO_NUM_4 // set 1 to power on MCU +#define PERP_VCC_CTL GPIO_NUM_6 // set 1 to power on peripherals + +/* IMU GPIOs */ +#define IMU_INT_GPIO GPIO_NUM_5 + +#elif defined(CONFIG_IDF_TARGET_ESP32C5) + +/* Audio I2S GPIOs */ +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_NC +#define AUDIO_I2S_GPIO_WS GPIO_NUM_8 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_7 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_6 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_9 + +/* Audio CODEC GPIOs */ +#define AUDIO_CODEC_PA_PIN GPIO_NUM_23 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_25 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_26 + +/* Button GPIOs */ +#define BOOT_BUTTON_GPIO GPIO_NUM_28 +#define KEY_BUTTON_GPIO GPIO_NUM_5 +#define LED_GPIO GPIO_NUM_27 + +/* ADC GPIOs */ +#define VBAT_ADC_CHANNEL ADC_CHANNEL_3 // C5: IO4 +#define MCU_VCC_CTL GPIO_NUM_2 // set 1 to power on MCU +#define PERP_VCC_CTL GPIO_NUM_0 // set 1 to power on peripherals + +/* IMU GPIOs */ +#define IMU_INT_GPIO GPIO_NUM_3 + +#endif // CONFIG_IDF_TARGET + diff --git a/main/boards/esp-spot/config.json b/main/boards/esp-spot/config.json new file mode 100644 index 00000000..350171e9 --- /dev/null +++ b/main/boards/esp-spot/config.json @@ -0,0 +1,14 @@ +{ + "target": "esp32c5", + "builds": [ + { + "name": "esp-spot-c5", + "sdkconfig_append": [ + "CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y", + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\"", + "CONFIG_PM_ENABLE=y", + "CONFIG_FREERTOS_USE_TICKLESS_IDLE=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/esp-spot/esp_spot_board.cc b/main/boards/esp-spot/esp_spot_board.cc new file mode 100644 index 00000000..9cc62947 --- /dev/null +++ b/main/boards/esp-spot/esp_spot_board.cc @@ -0,0 +1,437 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "esp_idf_version.h" +#include "led/circular_strip.h" +#include "sdkconfig.h" + +#include "application.h" +#include "button.h" +#include "codecs/es8311_audio_codec.h" +#include "config.h" +#include "sleep_timer.h" +#include "wifi_board.h" +#include "wifi_station.h" + +#ifdef IMU_INT_GPIO +#include + +#include "bmi270_api.h" +#include "i2c_bus.h" +#endif // IMU_INT_GPIO + +#ifdef CONFIG_IDF_TARGET_ESP32S3 +#define TAG "esp_spot_s3" +#elif defined(CONFIG_IDF_TARGET_ESP32C5) +#define TAG "esp_spot_c5" +#else // target +#error "Unsupported target" +#endif // target + +#ifdef IMU_INT_GPIO +namespace Bmi270Imu { + +static bmi270_handle_t bmi_handle_ = nullptr; + +esp_err_t Initialize(i2c_bus_handle_t i2c_bus, uint8_t addr = BMI270_I2C_ADDRESS) { + if (bmi_handle_) { + return ESP_OK; + } + + if (!i2c_bus) { + ESP_LOGE(TAG, "Invalid I2C bus for BMI270"); + return ESP_ERR_INVALID_ARG; + } + + esp_err_t ret = bmi270_sensor_create(i2c_bus, &bmi_handle_, bmi270_config_file, + BMI2_GYRO_CROSS_SENS_ENABLE | BMI2_CRT_RTOSK_ENABLE); + if (ret != ESP_OK || !bmi_handle_) { + ESP_LOGE(TAG, "BMI270 create failed: %s", esp_err_to_name(ret)); + return ret == ESP_OK ? ESP_FAIL : ret; + } + ESP_LOGI(TAG, "BMI270 initialized"); + return ESP_OK; +} + +// Only used for deep sleep wakeup with wrist gesture interrupt +esp_err_t EnableImuIntForWakeup() { + if (!bmi_handle_) { + return ESP_ERR_INVALID_STATE; + } + + const uint8_t sens_list[] = {BMI2_ACCEL, BMI2_WRIST_GESTURE}; + int8_t rslt = bmi270_sensor_enable(sens_list, 2, bmi_handle_); + if (rslt != BMI2_OK) { + ESP_LOGE(TAG, "Failed to enable BMI270 sensors: %d", rslt); + return ESP_FAIL; + } + + struct bmi2_sens_config config = {.type = BMI2_WRIST_GESTURE}; + rslt = bmi270_get_sensor_config(&config, 1, bmi_handle_); + if (rslt != BMI2_OK) { + ESP_LOGE(TAG, "Failed to get wrist gesture config: %d", rslt); + return ESP_FAIL; + } + config.cfg.wrist_gest.wearable_arm = BMI2_ARM_RIGHT; + rslt = bmi270_set_sensor_config(&config, 1, bmi_handle_); + if (rslt != BMI2_OK) { + ESP_LOGE(TAG, "Failed to set wrist gesture config: %d", rslt); + return ESP_FAIL; + } + + struct bmi2_int_pin_config pin_config = {}; + pin_config.pin_type = BMI2_INT1; + pin_config.pin_cfg[0].input_en = BMI2_INT_INPUT_DISABLE; + pin_config.pin_cfg[0].lvl = BMI2_INT_ACTIVE_HIGH; + pin_config.pin_cfg[0].od = BMI2_INT_PUSH_PULL; + pin_config.pin_cfg[0].output_en = BMI2_INT_OUTPUT_ENABLE; + pin_config.int_latch = BMI2_INT_NON_LATCH; + rslt = bmi2_set_int_pin_config(&pin_config, bmi_handle_); + if (rslt != BMI2_OK) { + ESP_LOGE(TAG, "Failed to set BMI270 INT pin: %d", rslt); + return ESP_FAIL; + } + + struct bmi2_sens_int_config int_config = {.type = BMI2_WRIST_GESTURE, .hw_int_pin = BMI2_INT1}; + rslt = bmi270_map_feat_int(&int_config, 1, bmi_handle_); + if (rslt != BMI2_OK) { + ESP_LOGE(TAG, "Failed to map BMI270 interrupt: %d", rslt); + return ESP_FAIL; + } + + return ESP_OK; +} + +} // namespace Bmi270Imu + +#endif // IMU_INT_GPIO + +class EspSpot : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_ = nullptr; + Button boot_button_; + Button key_button_; + adc_oneshot_unit_handle_t adc1_handle_; + adc_cali_handle_t adc1_cali_handle_; + bool adc_calibration_lock_ = false; + bool key_long_pressed_ = false; + int64_t last_key_press_time = 0; + SleepTimer* sleep_timer_ = nullptr; +#ifdef IMU_INT_GPIO + i2c_bus_handle_t shared_i2c_bus_handle_ = nullptr; + static constexpr int kDeepSleepTimeoutSeconds = 10 * 60; // 10 minutes + bool imu_ready_ = false; +#endif + +#ifdef IMU_INT_GPIO + void InitializeI2c() { + // Initialize I2C peripheral + i2c_config_t i2c_bus_cfg = { + .mode = I2C_MODE_MASTER, + .sda_io_num = AUDIO_CODEC_I2C_SDA_PIN, + .scl_io_num = AUDIO_CODEC_I2C_SCL_PIN, + .sda_pullup_en = true, + .scl_pullup_en = true, + .master = + { + .clk_speed = I2C_MASTER_FREQ_HZ, + }, + .clk_flags = 0, + }; + shared_i2c_bus_handle_ = i2c_bus_create(I2C_NUM_0, &i2c_bus_cfg); + if (!shared_i2c_bus_handle_) { + ESP_LOGE(TAG, "Failed to create shared I2C bus"); + ESP_ERROR_CHECK(ESP_FAIL); + } + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) && !CONFIG_I2C_BUS_BACKWARD_CONFIG + i2c_bus_ = i2c_bus_get_internal_bus_handle(shared_i2c_bus_handle_); +#else +#error "ESP-Spot board requires i2c_bus_get_internal_bus_handle() support" +#endif + if (!i2c_bus_) { + ESP_LOGE(TAG, "Failed to obtain master bus handle"); + ESP_ERROR_CHECK(ESP_FAIL); + } + + esp_err_t imu_ret = Bmi270Imu::Initialize(shared_i2c_bus_handle_); + if (imu_ret != ESP_OK) { + ESP_LOGW(TAG, "BMI270 initialization failed, deep sleep disabled (%s)", esp_err_to_name(imu_ret)); + } else { + imu_ready_ = true; + } + } +#else + void InitializeI2c() { + 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, &i2c_bus_)); + } +#endif // IMU_INT_GPIO + + void InitializeADC() { + adc_oneshot_unit_init_cfg_t init_config1 = {.unit_id = ADC_UNIT_1}; + ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config1, &adc1_handle_)); + + adc_oneshot_chan_cfg_t chan_config = { + .atten = ADC_ATTEN, + .bitwidth = ADC_WIDTH, + }; + ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle_, VBAT_ADC_CHANNEL, &chan_config)); + +#ifdef ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED + adc_cali_handle_t handle = nullptr; + esp_err_t ret = ESP_FAIL; + + adc_cali_curve_fitting_config_t cali_config = { + .unit_id = ADC_UNIT_1, + .atten = ADC_ATTEN, + .bitwidth = ADC_WIDTH, + }; + ret = adc_cali_create_scheme_curve_fitting(&cali_config, &handle); + if (ret == ESP_OK) { + adc_calibration_lock_ = true; + adc1_cali_handle_ = handle; + ESP_LOGI(TAG, "ADC Curve Fitting calibration succeeded"); + } +#endif // ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + HandleUserActivity(); + ResetWifiConfiguration(); + }); + + key_button_.OnClick([this]() { + HandleUserActivity(); + auto& app = Application::GetInstance(); + app.ToggleChatState(); + key_long_pressed_ = false; + }); + + key_button_.OnLongPress([this]() { + HandleUserActivity(); + int64_t now = esp_timer_get_time(); + auto* led = static_cast(this->GetLed()); + + if (key_long_pressed_) { + if ((now - last_key_press_time) < LONG_PRESS_TIMEOUT_US) { + ESP_LOGW(TAG, "Key button long pressed the second time within 5s, shutting down..."); + led->SetSingleColor(0, {0, 0, 0}); + + gpio_hold_dis(MCU_VCC_CTL); + gpio_set_level(MCU_VCC_CTL, 0); + + } else { + last_key_press_time = now; + BlinkGreenFor5s(); + } + key_long_pressed_ = true; + } else { + ESP_LOGW(TAG, "Key button first long press! Waiting second within 5s to shutdown..."); + last_key_press_time = now; + key_long_pressed_ = true; + + BlinkGreenFor5s(); + } + }); + } + + void InitializePowerCtl() { + InitializeGPIO(); + + gpio_set_level(MCU_VCC_CTL, 1); + gpio_hold_en(MCU_VCC_CTL); + + gpio_set_level(PERP_VCC_CTL, 1); + gpio_hold_en(PERP_VCC_CTL); + } + + void InitializeGPIO() { + gpio_config_t io_pa = {.pin_bit_mask = (1ULL << AUDIO_CODEC_PA_PIN), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE}; + gpio_config(&io_pa); + gpio_set_level(AUDIO_CODEC_PA_PIN, 0); + + gpio_config_t io_conf_1 = {.pin_bit_mask = (1ULL << MCU_VCC_CTL), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE}; + gpio_config(&io_conf_1); + + gpio_config_t io_conf_2 = {.pin_bit_mask = (1ULL << PERP_VCC_CTL), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE}; + gpio_config(&io_conf_2); + +#ifdef IMU_INT_GPIO + gpio_config_t io_conf_imu_int = { + .pin_bit_mask = (1ULL << IMU_INT_GPIO), + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_ENABLE, + .intr_type = GPIO_INTR_NEGEDGE, + }; + gpio_config(&io_conf_imu_int); + gpio_install_isr_service(0); +#endif // IMU_INT_GPIO + } + + void HandleUserActivity() { + if (sleep_timer_) { + sleep_timer_->WakeUp(); + } + } + +#ifdef IMU_INT_GPIO + void InitializePowerSaveTimer() { + if (!imu_ready_) { + ESP_LOGW(TAG, "IMU not ready, skip deep sleep timer"); + return; + } + if (sleep_timer_) { + return; + } + sleep_timer_ = new SleepTimer(-1, kDeepSleepTimeoutSeconds); + sleep_timer_->OnEnterDeepSleepMode([this]() { EnterDeepSleep(); }); + sleep_timer_->SetEnabled(true); + ESP_LOGI(TAG, "Deep sleep timer enabled, timeout=%ds", kDeepSleepTimeoutSeconds); + } + + void EnterDeepSleep() { + if (!imu_ready_) { + ESP_LOGW(TAG, "Skip deep sleep because IMU is not ready"); + return; + } + + auto* led = static_cast(GetLed()); + if (led) { + led->SetSingleColor(0, {0, 0, 0}); + } + + if (Bmi270Imu::EnableImuIntForWakeup() != ESP_OK) { + ESP_LOGE(TAG, "IMU wakeup configuration failed, abort deep sleep"); + return; + } + + const uint64_t wakeup_mask = (1ULL << KEY_BUTTON_GPIO) | (1ULL << IMU_INT_GPIO); + ESP_ERROR_CHECK(esp_sleep_enable_ext1_wakeup(wakeup_mask, ESP_EXT1_WAKEUP_ANY_HIGH)); + ESP_LOGI(TAG, "Entering deep sleep, waiting for key or wrist gesture"); + esp_deep_sleep_start(); + } +#endif // IMU_INT_GPIO + + void BlinkGreenFor5s() { + auto* led = static_cast(GetLed()); + if (!led) { + return; + } + + led->Blink({50, 25, 0}, 100); + + esp_timer_create_args_t timer_args = {.callback = + [](void* arg) { + auto* self = static_cast(arg); + auto* led = static_cast(self->GetLed()); + if (led) { + led->SetSingleColor(0, {0, 0, 0}); + } + }, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "green_blink_timer"}; + + esp_timer_handle_t green_blink_timer = nullptr; + ESP_ERROR_CHECK(esp_timer_create(&timer_args, &green_blink_timer)); + ESP_ERROR_CHECK(esp_timer_start_once(green_blink_timer, LONG_PRESS_TIMEOUT_US)); + } + +public: + EspSpot() : boot_button_(BOOT_BUTTON_GPIO), key_button_(KEY_BUTTON_GPIO, true) { + InitializePowerCtl(); + InitializeADC(); + InitializeI2c(); + InitializeButtons(); +#ifdef IMU_INT_GPIO + InitializePowerSaveTimer(); +#endif // IMU_INT_GPIO + } + + virtual Led* GetLed() override { + static CircularStrip led(LED_GPIO, 1); + return &led; + } + + virtual AudioCodec* GetAudioCodec() override { + static Es8311AudioCodec audio_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, false); + return &audio_codec; + } + + virtual void SetPowerSaveMode(bool enabled) override { + if (sleep_timer_) { + sleep_timer_->SetEnabled(enabled); + } + WifiBoard::SetPowerSaveMode(enabled); + } + + virtual bool GetBatteryLevel(int& level, bool& charging, bool& discharging) override { + if (!adc1_handle_) { + InitializeADC(); + } + + int raw_value = 0; + int voltage = 0; + + ESP_ERROR_CHECK(adc_oneshot_read(adc1_handle_, VBAT_ADC_CHANNEL, &raw_value)); + + if (adc_calibration_lock_) { + ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc1_cali_handle_, raw_value, &voltage)); + voltage = voltage * 3 / 2; // compensate for voltage divider + ESP_LOGI(TAG, "Calibrated voltage: %d mV", voltage); + } else { + ESP_LOGI(TAG, "Raw ADC value: %d", raw_value); + voltage = raw_value; + } + + voltage = voltage < EMPTY_BATTERY_VOLTAGE ? EMPTY_BATTERY_VOLTAGE : voltage; + voltage = voltage > FULL_BATTERY_VOLTAGE ? FULL_BATTERY_VOLTAGE : voltage; + + // Calculate battery level percentage + level = (voltage - EMPTY_BATTERY_VOLTAGE) * 100 / (FULL_BATTERY_VOLTAGE - EMPTY_BATTERY_VOLTAGE); + + // ESP-Spot does not support charging detection, so we use MCU_VCC_CTL to determine charging status + charging = gpio_get_level(MCU_VCC_CTL); + discharging = !charging; + ESP_LOGI(TAG, "Battery Level: %d%%, Charging: %s", level, charging ? "Yes" : "No"); + return true; + } +}; + +DECLARE_BOARD(EspSpot); diff --git a/main/idf_component.yml b/main/idf_component.yml index 31d36946..658d51fb 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -99,6 +99,11 @@ dependencies: rules: - if: target in [esp32p4] + espressif/bmi270_sensor: + version: ^0.1.0 + rules: + - if: target in [esp32s3, esp32c5] + ## Required IDF version idf: version: '>=5.4.0' diff --git a/sdkconfig.defaults.esp32c5 b/sdkconfig.defaults.esp32c5 index abc4ffdf..d3322dfd 100644 --- a/sdkconfig.defaults.esp32c5 +++ b/sdkconfig.defaults.esp32c5 @@ -9,3 +9,6 @@ CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=6 CONFIG_ESP_WIFI_RX_BA_WIN=3 CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=16 CONFIG_MBEDTLS_DYNAMIC_FREE_CONFIG_DATA=y + +CONFIG_SR_WN_WN9S_NIHAOXIAOZHI=y +CONFIG_USE_ESP_WAKE_WORD=y