diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index c9da16ec3..00aa5c4a4 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -526,6 +526,11 @@ elseif(CONFIG_BOARD_TYPE_WIRELESS_TAG_WTP4C5MP07S) set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) set(BUILTIN_ICON_FONT font_awesome_30_4) set(DEFAULT_EMOJI_COLLECTION twemoji_64) +elseif(CONFIG_BOARD_TYPE_AIPI_LITE) + set(BOARD_TYPE "aipi-lite") + set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) + set(BUILTIN_ICON_FONT font_awesome_14_1) + set(DEFAULT_EMOJI_COLLECTION twemoji_32) endif() file(GLOB BOARD_SOURCES diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 25440f533..b9b1291e1 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -424,6 +424,9 @@ choice BOARD_TYPE config BOARD_TYPE_WIRELESS_TAG_WTP4C5MP07S bool "Wireless-Tag WTP4C5MP07S" depends on IDF_TARGET_ESP32P4 + config BOARD_TYPE_AIPI_LITE + bool "AIPI-Lite" + depends on IDF_TARGET_ESP32S3 endchoice choice diff --git a/main/boards/aipi-lite/README.md b/main/boards/aipi-lite/README.md new file mode 100644 index 000000000..28f566605 --- /dev/null +++ b/main/boards/aipi-lite/README.md @@ -0,0 +1,41 @@ +# 编译命令 + +## 一键编译 + +```bash +python scripts/release.py aipi-lite +``` + +## 手动配置编译 + +```bash +idf.py set-target esp32s3 +``` + +**配置** + +```bash +idf.py menuconfig +``` + +选择板子 + +``` +Xiaozhi Assistant -> Board Type -> AIPI-Lite +``` + +## 编译烧入 + +```bash +idf.py -DBOARD_NAME=aipi-lite build flash +``` + +注意: 如果当前设备出货之前是AiPi-Lite 固件(非小智版本),请特别小心处理闪存固件分区地址,以避免错误擦除 AiPi-Lite 的自身设备信息(EUI 等),否则设备即使恢复成Xorigin固件也无法正确连接到 服务器!所以在刷写固件之前,请务必记录设备的相关必要信息,以确保有恢复的方法! + +您可以使用以下命令备份生产信息 + +```bash +# firstly backup the factory information partition which contains the credentials for connecting the SenseCraft server +esptool.py --chip esp32s3 --baud 2000000 --before default_reset --after hard_reset --no-stub read_flash 0x9000 16384 nvsfactory.bin + +``` \ No newline at end of file diff --git a/main/boards/aipi-lite/README_en.md b/main/boards/aipi-lite/README_en.md new file mode 100644 index 000000000..df9deb5d6 --- /dev/null +++ b/main/boards/aipi-lite/README_en.md @@ -0,0 +1,40 @@ +# Build Instructions + +## One-click Build + +```bash +python scripts/release.py aipi-lite -c config_en.json +``` + +## Manual Configuration and Build + +```bash +idf.py set-target esp32s3 +``` + +**Configuration** + +```bash +idf.py menuconfig +``` + +Select the board: + +``` +Xiaozhi Assistant -> Board Type -> AiPi-Lite +``` + +## Build and Flash + +```bash +idf.py -DBOARD_NAME=aipi-lite build flash +``` + +Note: If your device was previously shipped with the AiPi-Lite firmware (not the Xiaozhi version), please be very careful with the flash partition addresses to avoid accidentally erasing the device information (such as EUI) of the AiPi-Lite. Otherwise, even if you restore the AiPi-Lite firmware, the device may not be able to connect to the Xorigin server correctly! Therefore, before flashing the firmware, be sure to record the necessary device information to ensure you have a way to recover it! + +You can use the following command to back up the factory information: + +```bash +# Firstly backup the factory information partition which contains the credentials for connecting the SenseCraft server +esptool.py --chip esp32s3 --baud 2000000 --before default_reset --after hard_reset --no-stub read_flash 0x9000 16384 nvsfactory.bin +``` diff --git a/main/boards/aipi-lite/aipi-lite.cc b/main/boards/aipi-lite/aipi-lite.cc new file mode 100644 index 000000000..b808812a2 --- /dev/null +++ b/main/boards/aipi-lite/aipi-lite.cc @@ -0,0 +1,247 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "application.h" +#include "button.h" +#include "codecs/es8311_audio_codec.h" +#include "config.h" +#include "display/lcd_display.h" +#include "lamp_controller.h" +#include "led/single_led.h" +#include "mcp_server.h" +#include "power_manager.h" +#include "power_save_timer.h" +#include "system_reset.h" +#include "wifi_board.h" + +#define TAG "AIPI-Lite" + +class AIPILite : public WifiBoard { + private: + i2c_master_bus_handle_t i2c_bus_; + Button boot_button_; + Button power_button_; + LcdDisplay* display_; + PowerManager* power_manager_; + PowerSaveTimer* power_save_timer_; + esp_lcd_panel_handle_t panel_ = nullptr; + + void InitializePowerManager() { + power_manager_ = new PowerManager(POWER_CHARGE_DETECT_PIN); + power_manager_->OnChargingStatusChanged([this](bool is_charging) { + if (is_charging) { + power_save_timer_->SetEnabled(false); + } else { + power_save_timer_->SetEnabled(true); + } + }); + } + + void InitializePowerSaveTimer() { + power_save_timer_ = new PowerSaveTimer(-1, 60, 300); + power_save_timer_->OnEnterSleepMode([this]() { + GetDisplay()->SetPowerSaveMode(true); + GetBacklight()->SetBrightness(1); + }); + power_save_timer_->OnExitSleepMode([this]() { + GetDisplay()->SetPowerSaveMode(false); + GetBacklight()->RestoreBrightness(); + }); + power_save_timer_->OnShutdownRequest([this]() { + ESP_LOGI(TAG, "Shutting down"); + esp_lcd_panel_disp_on_off(panel_, false); // 关闭显示 + rtc_gpio_set_level(POWER_CONTROL_PIN, 0); + rtc_gpio_hold_dis(POWER_CONTROL_PIN); + esp_deep_sleep_start(); + }); + 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 InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_SPI_MOSI_PIN; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = DISPLAY_SPI_SCLK_PIN; + 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 InitializeLcdDisplay() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = DISPLAY_SPI_CS_PIN; + io_config.dc_gpio_num = DISPLAY_SPI_DC_PIN; + io_config.spi_mode = DISPLAY_SPI_MODE; + 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"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = DISPLAY_SPI_RESET_PIN; + panel_config.rgb_ele_order = DISPLAY_RGB_ORDER; + panel_config.bits_per_pixel = 16; + ESP_ERROR_CHECK( + esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel_)); + + esp_lcd_panel_reset(panel_); + + esp_lcd_panel_init(panel_); + esp_lcd_panel_invert_color(panel_, DISPLAY_INVERT_COLOR); + esp_lcd_panel_swap_xy(panel_, DISPLAY_SWAP_XY); + esp_lcd_panel_mirror(panel_, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); + 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 InitializeButtons() { + boot_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && + !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + + // 设置开机按钮的长按事件(直接进入配网模式) + boot_button_.OnLongPress([this]() { + // 唤醒电源保存定时器 + power_save_timer_->WakeUp(); + // 获取应用程序实例 + auto& app = Application::GetInstance(); + + // 进入配网模式 + app.SetDeviceState(kDeviceStateWifiConfiguring); + + // 重置WiFi配置以确保进入配网模式 + ResetWifiConfiguration(); + }); + + power_button_.OnClick([this]() { power_save_timer_->WakeUp(); }); + power_button_.OnLongPress([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() != kDeviceStateStarting && + !(power_manager_->IsCharging() && + power_manager_->GetBatteryLevel() < 100)) { + ESP_LOGI(TAG, "Power button long pressed, shutting down"); + esp_lcd_panel_disp_on_off(panel_, false); // 关闭显示 + rtc_gpio_set_level(POWER_CONTROL_PIN, 0); + rtc_gpio_hold_dis(POWER_CONTROL_PIN); + esp_deep_sleep_start(); + } + }); + } + + void InitializePowerCtl() { + ESP_LOGI(TAG, "Initialize Power Control GPIO"); + rtc_gpio_init(POWER_CONTROL_PIN); + rtc_gpio_set_direction(POWER_CONTROL_PIN, RTC_GPIO_MODE_OUTPUT_ONLY); + rtc_gpio_set_level(POWER_CONTROL_PIN, 1); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeTools() {} + + public: + AIPILite() + : boot_button_(BOOT_BUTTON_GPIO), power_button_(POWER_BUTTON_GPIO) { + InitializePowerCtl(); + InitializePowerManager(); + InitializePowerSaveTimer(); + InitializeI2c(); + InitializeSpi(); + InitializeLcdDisplay(); + InitializeButtons(); + InitializeTools(); + if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) { + GetBacklight()->RestoreBrightness(); + } + } + + virtual Led* GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + 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 Display* GetDisplay() override { return display_; } + + virtual Backlight* GetBacklight() override { + if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) { + static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, + DISPLAY_BACKLIGHT_OUTPUT_INVERT); + return &backlight; + } + return nullptr; + } + + 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 SetPowerSaveMode(bool enabled) override { + if (!enabled) { + power_save_timer_->WakeUp(); + } + WifiBoard::SetPowerSaveMode(enabled); + } +}; + +DECLARE_BOARD(AIPILite); diff --git a/main/boards/aipi-lite/config.h b/main/boards/aipi-lite/config.h new file mode 100644 index 000000000..22aef8ecb --- /dev/null +++ b/main/boards/aipi-lite/config.h @@ -0,0 +1,53 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +// aipi-lite configuration + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_6 // MCLK +#define AUDIO_I2S_GPIO_WS GPIO_NUM_12 // LRCK +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_14 // SCLK +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_13 // DIN +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_11 // DOUT + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_9 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_5 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_4 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR + +#define BUILTIN_LED_GPIO GPIO_NUM_46 +#define BOOT_BUTTON_GPIO GPIO_NUM_42 + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 128 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY true +#define DISPLAY_INVERT_COLOR false +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_3 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#define DISPLAY_SPI_SCLK_PIN GPIO_NUM_16 +#define DISPLAY_SPI_MOSI_PIN GPIO_NUM_17 +#define DISPLAY_SPI_CS_PIN GPIO_NUM_15 +#define DISPLAY_SPI_DC_PIN GPIO_NUM_7 +#define DISPLAY_SPI_RESET_PIN GPIO_NUM_18 +#define DISPLAY_SPI_MODE 0 +#define DISPLAY_SPI_SCLK_HZ (20 * 1000 * 1000) + +#define POWER_BUTTON_GPIO GPIO_NUM_1 +#define POWER_CONTROL_PIN GPIO_NUM_10 +#define POWER_CHARGE_DETECT_PIN GPIO_NUM_8 +#define POWER_ADC_UNIT ADC_UNIT_1 +#define POWER_ADC_CHANNEL ADC_CHANNEL_1 + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/aipi-lite/config.json b/main/boards/aipi-lite/config.json new file mode 100644 index 000000000..9ea016835 --- /dev/null +++ b/main/boards/aipi-lite/config.json @@ -0,0 +1,12 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "aipi-lite", + "sdkconfig_append": [ + "CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y", + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/16m.csv\"" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/aipi-lite/config_en.json b/main/boards/aipi-lite/config_en.json new file mode 100644 index 000000000..6f71bafdf --- /dev/null +++ b/main/boards/aipi-lite/config_en.json @@ -0,0 +1,17 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "aipi-lite_en", + "sdkconfig_append": [ + "CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y", + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/16m.csv\"", + "CONFIG_LANGUAGE_EN_US=y", + "CONFIG_SR_WN_WN9_NIHAOXIAOZHI_TTS=n", + "CONFIG_SR_WN_WN9_JARVIS_TTS=y", + "CONFIG_SR_WN_WN9_SOPHIA_TTS=y" + ] + } + ] +} + diff --git a/main/boards/aipi-lite/power_manager.h b/main/boards/aipi-lite/power_manager.h new file mode 100644 index 000000000..e93b94451 --- /dev/null +++ b/main/boards/aipi-lite/power_manager.h @@ -0,0 +1,187 @@ +#pragma once +#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_ = POWER_CHARGE_DETECT_PIN; + 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_; + + 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_value; + ESP_ERROR_CHECK( + adc_oneshot_read(adc_handle_, POWER_ADC_CHANNEL, &adc_value)); + + // 将 ADC 值添加到队列中 + adc_values_.push_back(adc_value); + if (adc_values_.size() > kBatteryAdcDataCount) { + adc_values_.erase(adc_values_.begin()); + } + uint32_t average_adc = 0; + for (auto value : adc_values_) { + average_adc += value; + } + average_adc /= adc_values_.size(); + + // 定义电池电量区间 + const struct { + uint16_t adc; + uint8_t level; + } levels[] = {{1480, 0}, {1581, 20}, {1663, 40}, + {1750, 60}, {1840, 80}, {1980, 100}}; + + // 低于最低值时 + if (average_adc < levels[0].adc) { + battery_level_ = 0; + } + // 高于最高值时 + else if (average_adc >= levels[5].adc) { + battery_level_ = 100; + } else { + // 线性插值计算中间值 + for (int i = 0; i < 5; i++) { + if (average_adc >= levels[i].adc && + average_adc < levels[i + 1].adc) { + float ratio = + static_cast(average_adc - 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 (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 value: %d average: %ld level: %ld", + adc_value, average_adc, battery_level_); + } + + 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_, 100000)); + + // 初始化 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_, POWER_ADC_CHANNEL, &chan_config)); + } + + ~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; + } +};