diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 88c1435c..ba798668 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -5,6 +5,7 @@ set(SOURCES "audio/audio_codec.cc" "audio/codecs/es8311_audio_codec.cc" "audio/codecs/es8374_audio_codec.cc" "audio/codecs/es8388_audio_codec.cc" + "audio/codecs/es8389_audio_codec.cc" "audio/codecs/dummy_audio_codec.cc" "audio/processors/audio_debugger.cc" "led/single_led.cc" @@ -151,6 +152,10 @@ elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX) set(BOARD_TYPE "atk-dnesp32s3-box") elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX0) set(BOARD_TYPE "atk-dnesp32s3-box0") +elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX2_WIFI) + set(BOARD_TYPE "atk-dnesp32s3-box2-wifi") +elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX2_4G) + set(BOARD_TYPE "atk-dnesp32s3-box2-4g") elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3M_WIFI) set(BOARD_TYPE "atk-dnesp32s3m-wifi") elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3M_4G) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 297515f9..8bec4299 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -214,6 +214,12 @@ choice BOARD_TYPE config BOARD_TYPE_ATK_DNESP32S3_BOX0 bool "正点原子DNESP32S3-BOX0" depends on IDF_TARGET_ESP32S3 + config BOARD_TYPE_ATK_DNESP32S3_BOX2_WIFI + bool "正点原子DNESP32S3-BOX2-WIFI" + depends on IDF_TARGET_ESP32S3 + config BOARD_TYPE_ATK_DNESP32S3_BOX2_4G + bool "正点原子DNESP32S3-BOX2-4G" + depends on IDF_TARGET_ESP32S3 config BOARD_TYPE_ATK_DNESP32S3M_WIFI bool "正点原子DNESP32S3M-WIFI" depends on IDF_TARGET_ESP32S3 diff --git a/main/audio/codecs/es8389_audio_codec.cc b/main/audio/codecs/es8389_audio_codec.cc new file mode 100644 index 00000000..9e30627c --- /dev/null +++ b/main/audio/codecs/es8389_audio_codec.cc @@ -0,0 +1,201 @@ +#include "es8389_audio_codec.h" + +#include + +static const char TAG[] = "Es8389AudioCodec"; + +Es8389AudioCodec::Es8389AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, + gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, + gpio_num_t pa_pin, uint8_t es8389_addr, bool use_mclk) { + duplex_ = true; // 是否双工 + input_reference_ = false; // 是否使用参考输入,实现回声消除 + input_channels_ = 1; // 输入通道数 + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; + pa_pin_ = pa_pin; + CreateDuplexChannels(mclk, bclk, ws, dout, din); + + // Do initialize of related interface: data_if, ctrl_if and gpio_if + audio_codec_i2s_cfg_t i2s_cfg = { + .port = I2S_NUM_0, + .rx_handle = rx_handle_, + .tx_handle = tx_handle_, + }; + data_if_ = audio_codec_new_i2s_data(&i2s_cfg); + assert(data_if_ != NULL); + + // Output + audio_codec_i2c_cfg_t i2c_cfg = { + .port = i2c_port, + .addr = es8389_addr, + .bus_handle = i2c_master_handle, + }; + ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); + assert(ctrl_if_ != NULL); + + gpio_if_ = audio_codec_new_gpio(); + assert(gpio_if_ != NULL); + + es8389_codec_cfg_t es8389_cfg = {}; + es8389_cfg.ctrl_if = ctrl_if_; + es8389_cfg.gpio_if = gpio_if_; + es8389_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH; + es8389_cfg.pa_pin = pa_pin; + es8389_cfg.use_mclk = use_mclk; + es8389_cfg.hw_gain.pa_voltage = 5.0; + es8389_cfg.hw_gain.codec_dac_voltage = 3.3; + codec_if_ = es8389_codec_new(&es8389_cfg); + + assert(codec_if_ != NULL); + + esp_codec_dev_cfg_t outdev_cfg = { + .dev_type = ESP_CODEC_DEV_TYPE_OUT, + .codec_if = codec_if_, + .data_if = data_if_, + }; + output_dev_ = esp_codec_dev_new(&outdev_cfg); + assert(output_dev_ != NULL); + + esp_codec_dev_cfg_t indev_cfg = { + .dev_type = ESP_CODEC_DEV_TYPE_IN, + .codec_if = codec_if_, + .data_if = data_if_, + }; + input_dev_ = esp_codec_dev_new(&indev_cfg); + assert(input_dev_ != NULL); + esp_codec_set_disable_when_closed(output_dev_, false); + esp_codec_set_disable_when_closed(input_dev_, false); + ESP_LOGI(TAG, "Es8389AudioCodec initialized"); +} + +Es8389AudioCodec::~Es8389AudioCodec() { + ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); + esp_codec_dev_delete(output_dev_); + ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); + esp_codec_dev_delete(input_dev_); + + audio_codec_delete_codec_if(codec_if_); + audio_codec_delete_ctrl_if(ctrl_if_); + audio_codec_delete_gpio_if(gpio_if_); + audio_codec_delete_data_if(data_if_); +} + +void Es8389AudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) { + assert(input_sample_rate_ == output_sample_rate_); + + i2s_chan_config_t chan_cfg = { + .id = I2S_NUM_0, + .role = I2S_ROLE_MASTER, + .dma_desc_num = 6, + .dma_frame_num = 240, + .auto_clear_after_cb = true, + .auto_clear_before_cb = false, + .intr_priority = 0, + }; + ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); + + i2s_std_config_t std_cfg = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)output_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, +#ifdef I2S_HW_VERSION_2 + .ext_clk_freq_hz = 0, +#endif + }, + .slot_cfg = { + .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, + .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, + .slot_mode = I2S_SLOT_MODE_STEREO, + .slot_mask = I2S_STD_SLOT_BOTH, + .ws_width = I2S_DATA_BIT_WIDTH_16BIT, + .ws_pol = false, + .bit_shift = true, + .left_align = true, + .big_endian = false, + .bit_order_lsb = false + }, + .gpio_cfg = { + .mclk = mclk, + .bclk = bclk, + .ws = ws, + .dout = dout, + .din = din, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); + ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); + ESP_LOGI(TAG, "Duplex channels created"); +} + +void Es8389AudioCodec::SetOutputVolume(int volume) { + ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume)); + AudioCodec::SetOutputVolume(volume); +} + +void Es8389AudioCodec::EnableInput(bool enable) { + if (enable == input_enabled_) { + return; + } + if (enable) { + esp_codec_dev_sample_info_t fs = { + .bits_per_sample = 16, + .channel = 1, + .channel_mask = 0, + .sample_rate = (uint32_t)input_sample_rate_, + .mclk_multiple = 0, + }; + ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs)); + ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 40.0)); + } else { + ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); + } + AudioCodec::EnableInput(enable); +} + +void Es8389AudioCodec::EnableOutput(bool enable) { + if (enable == output_enabled_) { + return; + } + if (enable) { + // Play 16bit 1 channel + esp_codec_dev_sample_info_t fs = { + .bits_per_sample = 16, + .channel = 1, + .channel_mask = 0, + .sample_rate = (uint32_t)output_sample_rate_, + .mclk_multiple = 0, + }; + ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs)); + ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_)); + if (pa_pin_ != GPIO_NUM_NC) { + gpio_set_level(pa_pin_, 1); + } + } else { + ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); + if (pa_pin_ != GPIO_NUM_NC) { + gpio_set_level(pa_pin_, 0); + } + } + AudioCodec::EnableOutput(enable); +} + +int Es8389AudioCodec::Read(int16_t* dest, int samples) { + if (input_enabled_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t))); + } + return samples; +} + +int Es8389AudioCodec::Write(const int16_t* data, int samples) { + if (output_enabled_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t))); + } + return samples; +} \ No newline at end of file diff --git a/main/audio/codecs/es8389_audio_codec.h b/main/audio/codecs/es8389_audio_codec.h new file mode 100644 index 00000000..de213a3d --- /dev/null +++ b/main/audio/codecs/es8389_audio_codec.h @@ -0,0 +1,38 @@ +#ifndef _ES8389_AUDIO_CODEC_H +#define _ES8389_AUDIO_CODEC_H + +#include "audio_codec.h" + +#include +#include +#include +#include + +class Es8389AudioCodec : public AudioCodec { +private: + const audio_codec_data_if_t* data_if_ = nullptr; + const audio_codec_ctrl_if_t* ctrl_if_ = nullptr; + const audio_codec_if_t* codec_if_ = nullptr; + const audio_codec_gpio_if_t* gpio_if_ = nullptr; + + esp_codec_dev_handle_t output_dev_ = nullptr; + esp_codec_dev_handle_t input_dev_ = nullptr; + gpio_num_t pa_pin_ = GPIO_NUM_NC; + + void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din); + + virtual int Read(int16_t* dest, int samples) override; + virtual int Write(const int16_t* data, int samples) override; + +public: + Es8389AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, + gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, + gpio_num_t pa_pin, uint8_t es8389_addr, bool use_mclk = true); + virtual ~Es8389AudioCodec(); + + virtual void SetOutputVolume(int volume) override; + virtual void EnableInput(bool enable) override; + virtual void EnableOutput(bool enable) override; +}; + +#endif // _ES8389_AUDIO_CODEC_H diff --git a/main/boards/atk-dnesp32s3-box2-4g/atk_dnesp32s3_box2.cc b/main/boards/atk-dnesp32s3-box2-4g/atk_dnesp32s3_box2.cc new file mode 100644 index 00000000..ad4631b0 --- /dev/null +++ b/main/boards/atk-dnesp32s3-box2-4g/atk_dnesp32s3_box2.cc @@ -0,0 +1,487 @@ +#include "dual_network_board.h" +#include "codecs/es8389_audio_codec.h" +#include "display/lcd_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "power_save_timer.h" +#include "led/single_led.h" +#include "assets/lang_config.h" +#include "power_manager.h" + +#include "i2c_device.h" +#include +#include +#include + +#include +#include +#include "esp_io_expander_tca95xx_16bit.h" + +#define TAG "atk_dnesp32s3_box2_4g" + +LV_FONT_DECLARE(font_puhui_20_4); +LV_FONT_DECLARE(font_awesome_20_4); + +class atk_dnesp32s3_box2_4g : public DualNetworkBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + LcdDisplay* display_; + esp_io_expander_handle_t io_exp_handle; + button_handle_t btns; + button_driver_t* btn_driver_ = nullptr; + static atk_dnesp32s3_box2_4g* instance_; + PowerSaveTimer* power_save_timer_; + PowerManager* power_manager_; + PowerSupply power_status_; + esp_timer_handle_t wake_timer_handle_; + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + int ticks_ = 0; + const int kChgCtrlInterval = 5; + + void InitializeBoardPowerManager() { + instance_ = this; + + if (IoExpanderGetLevel(XIO_CHRG) == 0) { + power_status_ = kDeviceTypecSupply; + } else { + power_status_ = kDeviceBatterySupply; + } + + esp_timer_create_args_t wake_display_timer_args = { + .callback = [](void *arg) { + atk_dnesp32s3_box2_4g* self = static_cast(arg); + + self->ticks_ ++; + if (self->ticks_ % self->kChgCtrlInterval == 0) { + if (self->IoExpanderGetLevel(XIO_CHRG) == 0) { + self->power_status_ = kDeviceTypecSupply; + } else { + self->power_status_ = kDeviceBatterySupply; + } + + /* 低于某个电量,会自动关机 */ + if (self->power_manager_->low_voltage_ < 2630 && self->power_status_ == kDeviceBatterySupply) { + esp_timer_stop(self->power_manager_->timer_handle_); + + esp_io_expander_set_dir(self->io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT); + esp_io_expander_set_level(self->io_exp_handle, XIO_CHG_CTRL, 0); + vTaskDelay(pdMS_TO_TICKS(100)); + + esp_io_expander_set_dir(self->io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_INPUT); + esp_io_expander_set_level(self->io_exp_handle, XIO_CHG_CTRL, 0); + vTaskDelay(pdMS_TO_TICKS(100)); + } + } + }, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "wake_update_timer", + .skip_unhandled_events = true, + }; + ESP_ERROR_CHECK(esp_timer_create(&wake_display_timer_args, &wake_timer_handle_)); + ESP_ERROR_CHECK(esp_timer_start_periodic(wake_timer_handle_, 100000)); + } + + void InitializePowerManager() { + power_manager_ = new PowerManager(io_exp_handle); + 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]() { + display_->SetChatMessage("system", ""); + display_->SetEmotion("sleepy"); + GetBacklight()->SetBrightness(1); + }); + power_save_timer_->OnExitSleepMode([this]() { + display_->SetChatMessage("system", ""); + display_->SetEmotion("neutral"); + GetBacklight()->RestoreBrightness(); + }); + power_save_timer_->OnShutdownRequest([this]() { + if (power_status_ == kDeviceBatterySupply) { + GetBacklight()->SetBrightness(0); + esp_timer_stop(power_manager_->timer_handle_); + esp_io_expander_set_dir( io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT); + esp_io_expander_set_level(io_exp_handle, XIO_CHG_CTRL, 0); + vTaskDelay(pdMS_TO_TICKS(100)); + esp_io_expander_set_level(io_exp_handle, XIO_SYS_POW, 0); + } + }); + + power_save_timer_->SetEnabled(true); + } + + void audio_volume_change(bool direction) { + auto codec = GetAudioCodec(); + auto volume = codec->output_volume(); + + if (direction) { + volume += 10; + if (volume > 100) { + volume = 100; + } + codec->SetOutputVolume(volume); + } else { + volume -= 10; + if (volume < 0) { + volume = 0; + } + codec->SetOutputVolume(volume); + } + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + } + + void audio_volume_minimum(){ + GetAudioCodec()->SetOutputVolume(0); + GetDisplay()->ShowNotification(Lang::Strings::MUTED); + } + + void audio_volume_maxmum(){ + GetAudioCodec()->SetOutputVolume(100); + GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME); + } + + esp_err_t IoExpanderSetLevel(uint16_t pin_mask, uint8_t level) { + return esp_io_expander_set_level(io_exp_handle, pin_mask, level); + } + + uint8_t IoExpanderGetLevel(uint16_t pin_mask) { + uint32_t pin_val = 0; + esp_io_expander_get_level(io_exp_handle, DRV_IO_EXP_INPUT_MASK, &pin_val); + pin_mask &= DRV_IO_EXP_INPUT_MASK; + return (uint8_t)((pin_val & pin_mask) ? 1 : 0); + } + + void InitializeIoExpander() { + esp_err_t ret = ESP_OK; + esp_io_expander_new_i2c_tca95xx_16bit(i2c_bus_, ESP_IO_EXPANDER_I2C_TCA9555_ADDRESS_000, &io_exp_handle); + + ret |= esp_io_expander_set_dir(io_exp_handle, DRV_IO_EXP_OUTPUT_MASK, IO_EXPANDER_OUTPUT); + ret |= esp_io_expander_set_dir(io_exp_handle, DRV_IO_EXP_INPUT_MASK, IO_EXPANDER_INPUT); + + ret |= esp_io_expander_set_level(io_exp_handle, XIO_SYS_POW, 1); + ret |= esp_io_expander_set_level(io_exp_handle, XIO_EN_3V3A, 1); + ret |= esp_io_expander_set_level(io_exp_handle, XIO_EN_4G, 1); + ret |= esp_io_expander_set_level(io_exp_handle, XIO_SPK_EN, 1); + ret |= esp_io_expander_set_level(io_exp_handle, XIO_USB_SEL, 1); + ret |= esp_io_expander_set_level(io_exp_handle, XIO_VBUS_EN, 0); + + assert(ret == ESP_OK); + } + + void InitializeI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = (i2c_port_t)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 InitializeButtons() { + instance_ = this; + + button_config_t l_btn_cfg = { + .long_press_time = 800, + .short_press_time = 500 + }; + + button_config_t m_btn_cfg = { + .long_press_time = 800, + .short_press_time = 500 + }; + + button_config_t r_btn_cfg = { + .long_press_time = 800, + .short_press_time = 500 + }; + + button_driver_t* xio_l_btn_driver_ = nullptr; + button_driver_t* xio_m_btn_driver_ = nullptr; + + button_handle_t l_btn_handle = NULL; + button_handle_t m_btn_handle = NULL; + button_handle_t r_btn_handle = NULL; + + xio_l_btn_driver_ = (button_driver_t*)calloc(1, sizeof(button_driver_t)); + xio_l_btn_driver_->enable_power_save = false; + xio_l_btn_driver_->get_key_level = [](button_driver_t *button_driver) -> uint8_t { + return !instance_->IoExpanderGetLevel(XIO_KEY_L); + }; + ESP_ERROR_CHECK(iot_button_create(&l_btn_cfg, xio_l_btn_driver_, &l_btn_handle)); + + xio_m_btn_driver_ = (button_driver_t*)calloc(1, sizeof(button_driver_t)); + xio_m_btn_driver_->enable_power_save = false; + xio_m_btn_driver_->get_key_level = [](button_driver_t *button_driver) -> uint8_t { + return instance_->IoExpanderGetLevel(XIO_KEY_M); + }; + ESP_ERROR_CHECK(iot_button_create(&m_btn_cfg, xio_m_btn_driver_, &m_btn_handle)); + + button_gpio_config_t r_cfg = { + .gpio_num = R_BUTTON_GPIO, + .active_level = BUTTON_INACTIVE, + .enable_power_save = false, + .disable_pull = false + }; + ESP_ERROR_CHECK(iot_button_new_gpio_device(&r_btn_cfg, &r_cfg, &r_btn_handle)); + + iot_button_register_cb(l_btn_handle, BUTTON_PRESS_DOWN, nullptr, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + self->power_save_timer_->WakeUp(); + self->audio_volume_change(false); + }, this); + + iot_button_register_cb(l_btn_handle, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + self->power_save_timer_->WakeUp(); + self->audio_volume_minimum(); + }, this); + + iot_button_register_cb(m_btn_handle, BUTTON_PRESS_DOWN, nullptr, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + self->power_save_timer_->WakeUp(); + auto& app = Application::GetInstance(); + if (self->GetNetworkType() == NetworkType::WIFI) { + if (app.GetDeviceState() == kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) { + } + else { + app.ToggleChatState(); + } + } else { + app.ToggleChatState(); + } + }, this); + + iot_button_register_cb(m_btn_handle, BUTTON_DOUBLE_CLICK, nullptr, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + self->power_save_timer_->WakeUp(); + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) { + self->SwitchNetworkType(); + } + }, this); + + iot_button_register_cb(m_btn_handle, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + + auto& app = Application::GetInstance(); + if (self->GetNetworkType() == NetworkType::WIFI) { + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + auto& wifi_board = static_cast(self->GetCurrentBoard()); + wifi_board.ResetWifiConfiguration(); + } + } + + if (self->power_status_ == kDeviceBatterySupply) { + auto backlight = Board::GetInstance().GetBacklight(); + backlight->SetBrightness(0); + esp_timer_stop(self->power_manager_->timer_handle_); + esp_io_expander_set_dir(self->io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT); + esp_io_expander_set_level(self->io_exp_handle, XIO_CHG_CTRL, 0); + vTaskDelay(pdMS_TO_TICKS(100)); + esp_io_expander_set_level(self->io_exp_handle, XIO_SYS_POW, 0); + vTaskDelay(pdMS_TO_TICKS(100)); + } + }, this); + + iot_button_register_cb(r_btn_handle, BUTTON_PRESS_DOWN, nullptr, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + self->power_save_timer_->WakeUp(); + self->audio_volume_change(true); + }, this); + + iot_button_register_cb(r_btn_handle, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + self->power_save_timer_->WakeUp(); + self->audio_volume_maxmum(); + }, this); + } + + void InitializeSt7789Display() { + ESP_LOGI(TAG, "Install panel IO"); + + /*RD PIN */ + gpio_config_t gpio_init_struct; + gpio_init_struct.intr_type = GPIO_INTR_DISABLE; + gpio_init_struct.mode = GPIO_MODE_INPUT_OUTPUT; + gpio_init_struct.pin_bit_mask = 1ull << LCD_PIN_RD; + gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; + gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; + gpio_config(&gpio_init_struct); + gpio_set_level(LCD_PIN_RD, 1); + + /* BL PIN */ + gpio_init_struct.pin_bit_mask = 1ull << DISPLAY_BACKLIGHT_PIN; + gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; + gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; + gpio_config(&gpio_init_struct); + + esp_lcd_i80_bus_handle_t i80_bus = NULL; + esp_lcd_i80_bus_config_t bus_config = { + .dc_gpio_num = LCD_PIN_DC, + .wr_gpio_num = LCD_PIN_WR, + .clk_src = LCD_CLK_SRC_DEFAULT, + .data_gpio_nums = { + LCD_PIN_D0, + LCD_PIN_D1, + LCD_PIN_D2, + LCD_PIN_D3, + LCD_PIN_D4, + LCD_PIN_D5, + LCD_PIN_D6, + LCD_PIN_D7, + }, + .bus_width = 8, + .max_transfer_bytes = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t), + .psram_trans_align = 64, + .sram_trans_align = 4, + }; + ESP_ERROR_CHECK(esp_lcd_new_i80_bus(&bus_config, &i80_bus)); + + esp_lcd_panel_io_i80_config_t io_config = { + .cs_gpio_num = LCD_PIN_CS, + .pclk_hz = (20 * 1000 * 1000), + .trans_queue_depth = 7, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .dc_levels = { + .dc_idle_level = 1, + .dc_cmd_level = 0, + .dc_dummy_level = 0, + .dc_data_level = 1, + }, + .flags = { + .cs_active_high = 0, + .pclk_active_neg = 0, + .pclk_idle_low = 0, + }, + }; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &panel_io)); + + esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = LCD_PIN_RST, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, + .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, true); + esp_lcd_panel_set_gap(panel, 0, 0); + esp_lcd_panel_io_tx_param(panel_io, 0xCF, (uint8_t[]) {0x00,0x83,0x30}, 3); + esp_lcd_panel_io_tx_param(panel_io, 0xED, (uint8_t[]) {0x64,0x03,0x12,0x81}, 4); + esp_lcd_panel_io_tx_param(panel_io, 0xE8, (uint8_t[]) {0x85,0x01,0x79}, 3); + esp_lcd_panel_io_tx_param(panel_io, 0xCB, (uint8_t[]) {0x39,0x2C,0x00,0x34,0x02}, 5); + esp_lcd_panel_io_tx_param(panel_io, 0xF7, (uint8_t[]) {0x20}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0xEA, (uint8_t[]) {0x00,0x00}, 2); + esp_lcd_panel_io_tx_param(panel_io, 0xbb, (uint8_t[]) {0x20}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0xc3, (uint8_t[]) {0x00}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0xC4, (uint8_t[]) {0x20}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0xC5, (uint8_t[]) {0x20}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0xC6, (uint8_t[]) {0x10}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0xC7, (uint8_t[]) {0xB0}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0x36, (uint8_t[]) {0x60}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0x3A, (uint8_t[]) {0x55}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0xB1, (uint8_t[]) {0x00,0x1B}, 2); + esp_lcd_panel_io_tx_param(panel_io, 0xF2, (uint8_t[]) {0x08}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0x26, (uint8_t[]) {0x01}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0xE0, (uint8_t[]) {0xD0,0x00,0x02,0x07,0x0A,0x28,0x32,0x44,0x42,0x06,0x0E,0x12,0x14,0x17}, 14); + esp_lcd_panel_io_tx_param(panel_io, 0xE1, (uint8_t[]) {0xD0,0x00,0x02,0x07,0x0A,0x28,0x31,0x54,0x47,0x0E,0x1C,0x17,0x1B,0x1E}, 14); + esp_lcd_panel_io_tx_param(panel_io, 0xB7, (uint8_t[]) {0x07}, 1); + 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, + { + .text_font = &font_puhui_20_4, + .icon_font = &font_awesome_20_4, + .emoji_font = DISPLAY_HEIGHT >= 240 ? font_emoji_64_init() : font_emoji_32_init(), + }); + } + +public: + atk_dnesp32s3_box2_4g() : + DualNetworkBoard(Module_4G_TX_PIN, Module_4G_RX_PIN) { + InitializeI2c(); + InitializeIoExpander(); + InitializePowerSaveTimer(); + InitializePowerManager(); + InitializeSt7789Display(); + InitializeButtons(); + GetBacklight()->RestoreBrightness(); + InitializeBoardPowerManager(); + } + + virtual AudioCodec* GetAudioCodec() override { + static Es8389AudioCodec 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, + GPIO_NUM_NC, + AUDIO_CODEC_ES8389_ADDR + ); + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } + + virtual Backlight* GetBacklight() override { + static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT); + return &backlight; + } + + 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(); + } + DualNetworkBoard::SetPowerSaveMode(enabled); + } +}; + +DECLARE_BOARD(atk_dnesp32s3_box2_4g); + +// 定义静态成员变量 +atk_dnesp32s3_box2_4g* atk_dnesp32s3_box2_4g::instance_ = nullptr; diff --git a/main/boards/atk-dnesp32s3-box2-4g/config.h b/main/boards/atk-dnesp32s3-box2-4g/config.h new file mode 100644 index 00000000..70e9d4d8 --- /dev/null +++ b/main/boards/atk-dnesp32s3-box2-4g/config.h @@ -0,0 +1,80 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + + +#include + +enum PowerSupply { + kDeviceTypecSupply, + kDeviceBatterySupply, +}; + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 16000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_38 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_42 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_40 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_39 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_41 + +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_48 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_47 +#define AUDIO_CODEC_ES8389_ADDR ES8389_CODEC_DEFAULT_ADDR + +#define SPISD_PIN_MOSI GPIO_NUM_16 +#define SPISD_PIN_MISO GPIO_NUM_18 +#define SPISD_PIN_CLK GPIO_NUM_17 +#define SPISD_PIN_TS GPIO_NUM_15 + +#define R_BUTTON_GPIO GPIO_NUM_0 + +#define XL9555_INT_GPIO GPIO_NUM_2 +#define XIO_IO_SBU2 (IO_EXPANDER_PIN_NUM_3) +#define XIO_IO_SBU1 (IO_EXPANDER_PIN_NUM_4) +#define XIO_KEY_L (IO_EXPANDER_PIN_NUM_5) +#define XIO_KEY_Q (IO_EXPANDER_PIN_NUM_6) +#define XIO_KEY_M (IO_EXPANDER_PIN_NUM_7) +#define XIO_USB_SEL (IO_EXPANDER_PIN_NUM_8) +#define XIO_SPK_EN (IO_EXPANDER_PIN_NUM_9) +#define XIO_SYS_POW (IO_EXPANDER_PIN_NUM_10) +#define XIO_VBUS_EN (IO_EXPANDER_PIN_NUM_11) +#define XIO_EN_4G (IO_EXPANDER_PIN_NUM_12) +#define XIO_EN_3V3A (IO_EXPANDER_PIN_NUM_13) +#define XIO_CHG_CTRL (IO_EXPANDER_PIN_NUM_14) +#define XIO_CHRG (IO_EXPANDER_PIN_NUM_15) + +#define DRV_IO_EXP_OUTPUT_MASK 0x3F18 +#define DRV_IO_EXP_INPUT_MASK 0xC0E7 + +#define LCD_PIN_CS GPIO_NUM_14 +#define LCD_PIN_DC GPIO_NUM_12 +#define LCD_PIN_RD GPIO_NUM_10 +#define LCD_PIN_WR GPIO_NUM_11 +#define LCD_PIN_RST GPIO_NUM_NC +#define LCD_PIN_D0 GPIO_NUM_13 +#define LCD_PIN_D1 GPIO_NUM_9 +#define LCD_PIN_D2 GPIO_NUM_8 +#define LCD_PIN_D3 GPIO_NUM_7 +#define LCD_PIN_D4 GPIO_NUM_6 +#define LCD_PIN_D5 GPIO_NUM_5 +#define LCD_PIN_D6 GPIO_NUM_4 +#define LCD_PIN_D7 GPIO_NUM_3 + +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_21 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#define Module_4G_RX_PIN GPIO_NUM_44 +#define Module_4G_TX_PIN GPIO_NUM_43 + +#endif // _BOARD_CONFIG_H_ + diff --git a/main/boards/atk-dnesp32s3-box2-4g/config.json b/main/boards/atk-dnesp32s3-box2-4g/config.json new file mode 100644 index 00000000..076253a3 --- /dev/null +++ b/main/boards/atk-dnesp32s3-box2-4g/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "atk-dnesp32s3-box2-4g", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/atk-dnesp32s3-box2-4g/power_manager.h b/main/boards/atk-dnesp32s3-box2-4g/power_manager.h new file mode 100644 index 00000000..103ce888 --- /dev/null +++ b/main/boards/atk-dnesp32s3-box2-4g/power_manager.h @@ -0,0 +1,195 @@ +#pragma once +#include +#include +#include "esp_io_expander_tca95xx_16bit.h" +#include +#include +#include + + +class PowerManager { +private: + std::function on_charging_status_changed_; + std::function on_low_battery_status_changed_; + esp_io_expander_handle_t xl9555_; + uint32_t pin_val = 0; + 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_; + + void CheckBatteryStatus() { + // Get charging status + esp_io_expander_get_level(xl9555_, DRV_IO_EXP_INPUT_MASK, &pin_val); + bool new_charging_status = ((uint8_t)((pin_val & XIO_CHRG) ? 1 : 0)) == 0; + 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; + uint32_t temp_val = 0; + + esp_io_expander_set_dir(xl9555_, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT); + esp_io_expander_set_level(xl9555_, XIO_CHG_CTRL, 0); + vTaskDelay(pdMS_TO_TICKS(500)); + + for(int t = 0; t < 10; t ++) { + ESP_ERROR_CHECK(adc_oneshot_read(adc_handle_, ADC_CHANNEL_0, &adc_value)); + temp_val += adc_value; + } + + esp_io_expander_set_dir(xl9555_, XIO_CHG_CTRL, IO_EXPANDER_INPUT); + + adc_value = temp_val / 10; + + // 将 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[] = { + {2696, 0}, /* 3.48V -屏幕闪屏 */ + {2724, 20}, /* 3.53V */ + {2861, 40}, /* 3.7V */ + {3038, 60}, /* 3.90V */ + {3150, 80}, /* 4.02V */ + {3280, 100} /* 4.14V */ + }; + + // 低于最低值时 + 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_); + } + } + } + + low_voltage_ = adc_value; + + // ESP_LOGI("PowerManager", "ADC value: %d average: %ld level: %ld", adc_value, average_adc, battery_level_); + } + +public: + esp_timer_handle_t timer_handle_; + uint16_t low_voltage_ = 2630; + PowerManager(esp_io_expander_handle_t xl9555) : xl9555_(xl9555) { + // 创建电池电量检查定时器 + 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_0, &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; + } +}; diff --git a/main/boards/atk-dnesp32s3-box2-wifi/atk_dnesp32s3_box2.cc b/main/boards/atk-dnesp32s3-box2-wifi/atk_dnesp32s3_box2.cc new file mode 100644 index 00000000..433ffb77 --- /dev/null +++ b/main/boards/atk-dnesp32s3-box2-wifi/atk_dnesp32s3_box2.cc @@ -0,0 +1,466 @@ +#include "wifi_board.h" +#include "codecs/es8389_audio_codec.h" +#include "display/lcd_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "power_save_timer.h" +#include "led/single_led.h" +#include "assets/lang_config.h" +#include "power_manager.h" + +#include "i2c_device.h" +#include +#include +#include + +#include +#include +#include "esp_io_expander_tca95xx_16bit.h" + +#define TAG "atk_dnesp32s3_box2_wifi" + +LV_FONT_DECLARE(font_puhui_20_4); +LV_FONT_DECLARE(font_awesome_20_4); + +class atk_dnesp32s3_box2_wifi : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + LcdDisplay* display_; + esp_io_expander_handle_t io_exp_handle; + button_handle_t btns; + button_driver_t* btn_driver_ = nullptr; + static atk_dnesp32s3_box2_wifi* instance_; + PowerSaveTimer* power_save_timer_; + PowerManager* power_manager_; + PowerSupply power_status_; + esp_timer_handle_t wake_timer_handle_; + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + int ticks_ = 0; + const int kChgCtrlInterval = 5; + + void InitializeBoardPowerManager() { + instance_ = this; + + if (IoExpanderGetLevel(XIO_CHRG) == 0) { + power_status_ = kDeviceTypecSupply; + } else { + power_status_ = kDeviceBatterySupply; + } + + esp_timer_create_args_t wake_display_timer_args = { + .callback = [](void *arg) { + atk_dnesp32s3_box2_wifi* self = static_cast(arg); + + self->ticks_ ++; + if (self->ticks_ % self->kChgCtrlInterval == 0) { + if (self->IoExpanderGetLevel(XIO_CHRG) == 0) { + self->power_status_ = kDeviceTypecSupply; + } else { + self->power_status_ = kDeviceBatterySupply; + } + + /* 低于某个电量,会自动关机 */ + if (self->power_manager_->low_voltage_ < 2630 && self->power_status_ == kDeviceBatterySupply) { + esp_timer_stop(self->power_manager_->timer_handle_); + + esp_io_expander_set_dir(self->io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT); + esp_io_expander_set_level(self->io_exp_handle, XIO_CHG_CTRL, 0); + vTaskDelay(pdMS_TO_TICKS(100)); + + esp_io_expander_set_dir(self->io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_INPUT); + esp_io_expander_set_level(self->io_exp_handle, XIO_CHG_CTRL, 0); + vTaskDelay(pdMS_TO_TICKS(100)); + } + } + }, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "wake_update_timer", + .skip_unhandled_events = true, + }; + ESP_ERROR_CHECK(esp_timer_create(&wake_display_timer_args, &wake_timer_handle_)); + ESP_ERROR_CHECK(esp_timer_start_periodic(wake_timer_handle_, 100000)); + } + + void InitializePowerManager() { + power_manager_ = new PowerManager(io_exp_handle); + 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]() { + display_->SetChatMessage("system", ""); + display_->SetEmotion("sleepy"); + GetBacklight()->SetBrightness(1); + }); + power_save_timer_->OnExitSleepMode([this]() { + display_->SetChatMessage("system", ""); + display_->SetEmotion("neutral"); + GetBacklight()->RestoreBrightness(); + }); + power_save_timer_->OnShutdownRequest([this]() { + if (power_status_ == kDeviceBatterySupply) { + GetBacklight()->SetBrightness(0); + esp_timer_stop(power_manager_->timer_handle_); + esp_io_expander_set_dir( io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT); + esp_io_expander_set_level(io_exp_handle, XIO_CHG_CTRL, 0); + vTaskDelay(pdMS_TO_TICKS(100)); + esp_io_expander_set_level(io_exp_handle, XIO_SYS_POW, 0); + } + }); + + power_save_timer_->SetEnabled(true); + } + + void audio_volume_change(bool direction) { + auto codec = GetAudioCodec(); + auto volume = codec->output_volume(); + + if (direction) { + volume += 10; + if (volume > 100) { + volume = 100; + } + codec->SetOutputVolume(volume); + } else { + volume -= 10; + if (volume < 0) { + volume = 0; + } + codec->SetOutputVolume(volume); + } + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + } + + void audio_volume_minimum(){ + GetAudioCodec()->SetOutputVolume(0); + GetDisplay()->ShowNotification(Lang::Strings::MUTED); + } + + void audio_volume_maxmum(){ + GetAudioCodec()->SetOutputVolume(100); + GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME); + } + + esp_err_t IoExpanderSetLevel(uint16_t pin_mask, uint8_t level) { + return esp_io_expander_set_level(io_exp_handle, pin_mask, level); + } + + uint8_t IoExpanderGetLevel(uint16_t pin_mask) { + uint32_t pin_val = 0; + esp_io_expander_get_level(io_exp_handle, DRV_IO_EXP_INPUT_MASK, &pin_val); + pin_mask &= DRV_IO_EXP_INPUT_MASK; + return (uint8_t)((pin_val & pin_mask) ? 1 : 0); + } + + void InitializeIoExpander() { + esp_err_t ret = ESP_OK; + esp_io_expander_new_i2c_tca95xx_16bit(i2c_bus_, ESP_IO_EXPANDER_I2C_TCA9555_ADDRESS_000, &io_exp_handle); + + ret |= esp_io_expander_set_dir(io_exp_handle, DRV_IO_EXP_OUTPUT_MASK, IO_EXPANDER_OUTPUT); + ret |= esp_io_expander_set_dir(io_exp_handle, DRV_IO_EXP_INPUT_MASK, IO_EXPANDER_INPUT); + + ret |= esp_io_expander_set_level(io_exp_handle, XIO_SYS_POW, 1); + ret |= esp_io_expander_set_level(io_exp_handle, XIO_EN_3V3A, 1); + ret |= esp_io_expander_set_level(io_exp_handle, XIO_EN_4G, 1); + ret |= esp_io_expander_set_level(io_exp_handle, XIO_SPK_EN, 1); + ret |= esp_io_expander_set_level(io_exp_handle, XIO_USB_SEL, 1); + ret |= esp_io_expander_set_level(io_exp_handle, XIO_VBUS_EN, 0); + + assert(ret == ESP_OK); + } + + // Initialize I2C peripheral + void InitializeI2c() { + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = (i2c_port_t)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 InitializeButtons() { + instance_ = this; + + button_config_t l_btn_cfg = { + .long_press_time = 800, + .short_press_time = 500 + }; + + button_config_t m_btn_cfg = { + .long_press_time = 800, + .short_press_time = 500 + }; + + button_config_t r_btn_cfg = { + .long_press_time = 800, + .short_press_time = 500 + }; + + button_driver_t* xio_l_btn_driver_ = nullptr; + button_driver_t* xio_m_btn_driver_ = nullptr; + + button_handle_t l_btn_handle = NULL; + button_handle_t m_btn_handle = NULL; + button_handle_t r_btn_handle = NULL; + + xio_l_btn_driver_ = (button_driver_t*)calloc(1, sizeof(button_driver_t)); + xio_l_btn_driver_->enable_power_save = false; + xio_l_btn_driver_->get_key_level = [](button_driver_t *button_driver) -> uint8_t { + return !instance_->IoExpanderGetLevel(XIO_KEY_L); + }; + ESP_ERROR_CHECK(iot_button_create(&l_btn_cfg, xio_l_btn_driver_, &l_btn_handle)); + + xio_m_btn_driver_ = (button_driver_t*)calloc(1, sizeof(button_driver_t)); + xio_m_btn_driver_->enable_power_save = false; + xio_m_btn_driver_->get_key_level = [](button_driver_t *button_driver) -> uint8_t { + return instance_->IoExpanderGetLevel(XIO_KEY_M); + }; + ESP_ERROR_CHECK(iot_button_create(&m_btn_cfg, xio_m_btn_driver_, &m_btn_handle)); + + button_gpio_config_t r_cfg = { + .gpio_num = R_BUTTON_GPIO, + .active_level = BUTTON_INACTIVE, + .enable_power_save = false, + .disable_pull = false + }; + ESP_ERROR_CHECK(iot_button_new_gpio_device(&r_btn_cfg, &r_cfg, &r_btn_handle)); + + iot_button_register_cb(l_btn_handle, BUTTON_PRESS_DOWN, nullptr, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + self->power_save_timer_->WakeUp(); + self->audio_volume_change(false); + }, this); + + iot_button_register_cb(l_btn_handle, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + self->power_save_timer_->WakeUp(); + self->audio_volume_minimum(); + }, this); + + iot_button_register_cb(m_btn_handle, BUTTON_PRESS_DOWN, nullptr, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + self->power_save_timer_->WakeUp(); + auto& app = Application::GetInstance(); + app.ToggleChatState(); + }, this); + + iot_button_register_cb(m_btn_handle, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + self->ResetWifiConfiguration(); + } + + if (self->power_status_ == kDeviceBatterySupply) { + auto backlight = Board::GetInstance().GetBacklight(); + backlight->SetBrightness(0); + esp_timer_stop(self->power_manager_->timer_handle_); + esp_io_expander_set_dir(self->io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT); + esp_io_expander_set_level(self->io_exp_handle, XIO_CHG_CTRL, 0); + vTaskDelay(pdMS_TO_TICKS(100)); + esp_io_expander_set_level(self->io_exp_handle, XIO_SYS_POW, 0); + vTaskDelay(pdMS_TO_TICKS(100)); + } + }, this); + + iot_button_register_cb(r_btn_handle, BUTTON_PRESS_DOWN, nullptr, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + self->power_save_timer_->WakeUp(); + self->audio_volume_change(true); + }, this); + + iot_button_register_cb(r_btn_handle, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + self->power_save_timer_->WakeUp(); + self->audio_volume_maxmum(); + }, this); + } + + void InitializeSt7789Display() { + ESP_LOGI(TAG, "Install panel IO"); + + /* RD PIN */ + gpio_config_t gpio_init_struct; + gpio_init_struct.intr_type = GPIO_INTR_DISABLE; + gpio_init_struct.mode = GPIO_MODE_INPUT_OUTPUT; + gpio_init_struct.pin_bit_mask = 1ull << LCD_PIN_RD; + gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; + gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; + gpio_config(&gpio_init_struct); + gpio_set_level(LCD_PIN_RD, 1); + + /* BL PIN */ + gpio_init_struct.pin_bit_mask = 1ull << DISPLAY_BACKLIGHT_PIN; + gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; + gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; + gpio_config(&gpio_init_struct); + + esp_lcd_i80_bus_handle_t i80_bus = NULL; + esp_lcd_i80_bus_config_t bus_config = { + .dc_gpio_num = LCD_PIN_DC, + .wr_gpio_num = LCD_PIN_WR, + .clk_src = LCD_CLK_SRC_DEFAULT, + .data_gpio_nums = { + LCD_PIN_D0, + LCD_PIN_D1, + LCD_PIN_D2, + LCD_PIN_D3, + LCD_PIN_D4, + LCD_PIN_D5, + LCD_PIN_D6, + LCD_PIN_D7, + }, + .bus_width = 8, + .max_transfer_bytes = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t), + .psram_trans_align = 64, + .sram_trans_align = 4, + }; + ESP_ERROR_CHECK(esp_lcd_new_i80_bus(&bus_config, &i80_bus)); + + esp_lcd_panel_io_i80_config_t io_config = { + .cs_gpio_num = LCD_PIN_CS, + .pclk_hz = (20 * 1000 * 1000), + .trans_queue_depth = 7, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .dc_levels = { + .dc_idle_level = 1, + .dc_cmd_level = 0, + .dc_dummy_level = 0, + .dc_data_level = 1, + }, + .flags = { + .cs_active_high = 0, + .pclk_active_neg = 0, + .pclk_idle_low = 0, + }, + }; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &panel_io)); + + esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = LCD_PIN_RST, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, + .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, true); + esp_lcd_panel_set_gap(panel, 0, 0); + esp_lcd_panel_io_tx_param(panel_io, 0xCF, (uint8_t[]) {0x00,0x83,0x30}, 3); + esp_lcd_panel_io_tx_param(panel_io, 0xED, (uint8_t[]) {0x64,0x03,0x12,0x81}, 4); + esp_lcd_panel_io_tx_param(panel_io, 0xE8, (uint8_t[]) {0x85,0x01,0x79}, 3); + esp_lcd_panel_io_tx_param(panel_io, 0xCB, (uint8_t[]) {0x39,0x2C,0x00,0x34,0x02}, 5); + esp_lcd_panel_io_tx_param(panel_io, 0xF7, (uint8_t[]) {0x20}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0xEA, (uint8_t[]) {0x00,0x00}, 2); + esp_lcd_panel_io_tx_param(panel_io, 0xbb, (uint8_t[]) {0x20}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0xc3, (uint8_t[]) {0x00}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0xC4, (uint8_t[]) {0x20}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0xC5, (uint8_t[]) {0x20}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0xC6, (uint8_t[]) {0x10}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0xC7, (uint8_t[]) {0xB0}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0x36, (uint8_t[]) {0x60}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0x3A, (uint8_t[]) {0x55}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0xB1, (uint8_t[]) {0x00,0x1B}, 2); + esp_lcd_panel_io_tx_param(panel_io, 0xF2, (uint8_t[]) {0x08}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0x26, (uint8_t[]) {0x01}, 1); + esp_lcd_panel_io_tx_param(panel_io, 0xE0, (uint8_t[]) {0xD0,0x00,0x02,0x07,0x0A,0x28,0x32,0x44,0x42,0x06,0x0E,0x12,0x14,0x17}, 14); + esp_lcd_panel_io_tx_param(panel_io, 0xE1, (uint8_t[]) {0xD0,0x00,0x02,0x07,0x0A,0x28,0x31,0x54,0x47,0x0E,0x1C,0x17,0x1B,0x1E}, 14); + esp_lcd_panel_io_tx_param(panel_io, 0xB7, (uint8_t[]) {0x07}, 1); + 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, + { + .text_font = &font_puhui_20_4, + .icon_font = &font_awesome_20_4, + .emoji_font = DISPLAY_HEIGHT >= 240 ? font_emoji_64_init() : font_emoji_32_init(), + }); + } + +public: + atk_dnesp32s3_box2_wifi() { + InitializeI2c(); + InitializeIoExpander(); + InitializePowerSaveTimer(); + InitializePowerManager(); + InitializeSt7789Display(); + InitializeButtons(); + GetBacklight()->RestoreBrightness(); + InitializeBoardPowerManager(); + } + + virtual AudioCodec* GetAudioCodec() override { + static Es8389AudioCodec 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, + GPIO_NUM_NC, + AUDIO_CODEC_ES8389_ADDR, + false); + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } + + virtual Backlight* GetBacklight() override { + static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT); + return &backlight; + } + + 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(atk_dnesp32s3_box2_wifi); + +// 定义静态成员变量 +atk_dnesp32s3_box2_wifi* atk_dnesp32s3_box2_wifi::instance_ = nullptr; diff --git a/main/boards/atk-dnesp32s3-box2-wifi/config.h b/main/boards/atk-dnesp32s3-box2-wifi/config.h new file mode 100644 index 00000000..a12384c1 --- /dev/null +++ b/main/boards/atk-dnesp32s3-box2-wifi/config.h @@ -0,0 +1,71 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +enum PowerSupply { + kDeviceTypecSupply, + kDeviceBatterySupply, +}; + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 16000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_38 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_42 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_40 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_39 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_41 + +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_48 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_47 +#define AUDIO_CODEC_ES8389_ADDR ES8389_CODEC_DEFAULT_ADDR + +#define R_BUTTON_GPIO GPIO_NUM_0 + +#define XL9555_INT_GPIO GPIO_NUM_2 +#define XIO_IO_SBU2 (IO_EXPANDER_PIN_NUM_3) +#define XIO_IO_SBU1 (IO_EXPANDER_PIN_NUM_4) +#define XIO_KEY_L (IO_EXPANDER_PIN_NUM_5) +#define XIO_KEY_Q (IO_EXPANDER_PIN_NUM_6) +#define XIO_KEY_M (IO_EXPANDER_PIN_NUM_7) +#define XIO_USB_SEL (IO_EXPANDER_PIN_NUM_8) +#define XIO_SPK_EN (IO_EXPANDER_PIN_NUM_9) +#define XIO_SYS_POW (IO_EXPANDER_PIN_NUM_10) +#define XIO_VBUS_EN (IO_EXPANDER_PIN_NUM_11) +#define XIO_EN_4G (IO_EXPANDER_PIN_NUM_12) +#define XIO_EN_3V3A (IO_EXPANDER_PIN_NUM_13) +#define XIO_CHG_CTRL (IO_EXPANDER_PIN_NUM_14) +#define XIO_CHRG (IO_EXPANDER_PIN_NUM_15) + +#define DRV_IO_EXP_OUTPUT_MASK 0x3F18 +#define DRV_IO_EXP_INPUT_MASK 0xC0E7 + +#define LCD_PIN_CS GPIO_NUM_14 +#define LCD_PIN_DC GPIO_NUM_12 +#define LCD_PIN_RD GPIO_NUM_10 +#define LCD_PIN_WR GPIO_NUM_11 +#define LCD_PIN_RST GPIO_NUM_NC +#define LCD_PIN_D0 GPIO_NUM_13 +#define LCD_PIN_D1 GPIO_NUM_9 +#define LCD_PIN_D2 GPIO_NUM_8 +#define LCD_PIN_D3 GPIO_NUM_7 +#define LCD_PIN_D4 GPIO_NUM_6 +#define LCD_PIN_D5 GPIO_NUM_5 +#define LCD_PIN_D6 GPIO_NUM_4 +#define LCD_PIN_D7 GPIO_NUM_3 + +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_21 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#endif // _BOARD_CONFIG_H_ + diff --git a/main/boards/atk-dnesp32s3-box2-wifi/config.json b/main/boards/atk-dnesp32s3-box2-wifi/config.json new file mode 100644 index 00000000..2add4715 --- /dev/null +++ b/main/boards/atk-dnesp32s3-box2-wifi/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "atk-dnesp32s3-box2-wifi", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/atk-dnesp32s3-box2-wifi/power_manager.h b/main/boards/atk-dnesp32s3-box2-wifi/power_manager.h new file mode 100644 index 00000000..103ce888 --- /dev/null +++ b/main/boards/atk-dnesp32s3-box2-wifi/power_manager.h @@ -0,0 +1,195 @@ +#pragma once +#include +#include +#include "esp_io_expander_tca95xx_16bit.h" +#include +#include +#include + + +class PowerManager { +private: + std::function on_charging_status_changed_; + std::function on_low_battery_status_changed_; + esp_io_expander_handle_t xl9555_; + uint32_t pin_val = 0; + 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_; + + void CheckBatteryStatus() { + // Get charging status + esp_io_expander_get_level(xl9555_, DRV_IO_EXP_INPUT_MASK, &pin_val); + bool new_charging_status = ((uint8_t)((pin_val & XIO_CHRG) ? 1 : 0)) == 0; + 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; + uint32_t temp_val = 0; + + esp_io_expander_set_dir(xl9555_, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT); + esp_io_expander_set_level(xl9555_, XIO_CHG_CTRL, 0); + vTaskDelay(pdMS_TO_TICKS(500)); + + for(int t = 0; t < 10; t ++) { + ESP_ERROR_CHECK(adc_oneshot_read(adc_handle_, ADC_CHANNEL_0, &adc_value)); + temp_val += adc_value; + } + + esp_io_expander_set_dir(xl9555_, XIO_CHG_CTRL, IO_EXPANDER_INPUT); + + adc_value = temp_val / 10; + + // 将 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[] = { + {2696, 0}, /* 3.48V -屏幕闪屏 */ + {2724, 20}, /* 3.53V */ + {2861, 40}, /* 3.7V */ + {3038, 60}, /* 3.90V */ + {3150, 80}, /* 4.02V */ + {3280, 100} /* 4.14V */ + }; + + // 低于最低值时 + 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_); + } + } + } + + low_voltage_ = adc_value; + + // ESP_LOGI("PowerManager", "ADC value: %d average: %ld level: %ld", adc_value, average_adc, battery_level_); + } + +public: + esp_timer_handle_t timer_handle_; + uint16_t low_voltage_ = 2630; + PowerManager(esp_io_expander_handle_t xl9555) : xl9555_(xl9555) { + // 创建电池电量检查定时器 + 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_0, &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; + } +};