Files
xiaozhi-esp32/main/boards/waveshare/esp32-s3-touch-amoled-1.8/esp32-s3-touch-amoled-1.8.cc
Aleksei Savin d7a9b619eb fix(esp32-s3-touch-amoled-1.8): keep PowerSaveTimer ticking while charging
The previous patch disabled PowerSaveTimer entirely on the charger,
which also disabled the screen dim and sleep emoji that fire from
OnEnterSleepMode after 60s of idle. Since this board constructs the
timer with cpu_max_freq=-1, it never enters CPU light sleep nor
disables wake word detection in OnEnterSleepMode anyway — the only
charger-incompatible action is the 5-minute pmic_->PowerOff() in
OnShutdownRequest.

Keep the timer running unconditionally and gate just the shutdown
callback on last_discharging_. Also call WakeUp() on the
charging→battery transition so an unplug after 5+ idle minutes
doesn't trigger an immediate power-off.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 11:11:16 +03:00

367 lines
13 KiB
C++

#include "wifi_board.h"
#include "display/lcd_display.h"
#include "esp_lcd_sh8601.h"
#include "codecs/es8311_audio_codec.h"
#include "application.h"
#include "button.h"
#include "led/single_led.h"
#include "mcp_server.h"
#include "config.h"
#include "power_save_timer.h"
#include "axp2101.h"
#include "i2c_device.h"
#include <esp_log.h>
#include <esp_lcd_panel_vendor.h>
#include <driver/i2c_master.h>
#include <driver/spi_master.h>
#include "esp_io_expander_tca9554.h"
#include "settings.h"
#include <esp_lcd_touch_ft5x06.h>
#include <esp_lvgl_port.h>
#include <lvgl.h>
#define TAG "WaveshareEsp32s3TouchAMOLED1inch8"
class Pmic : public Axp2101 {
public:
Pmic(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : Axp2101(i2c_bus, addr) {
WriteReg(0x22, 0b110); // PWRON > OFFLEVEL as POWEROFF Source enable
WriteReg(0x27, 0x10); // hold 4s to power off
// Disable All DCs but DC1
WriteReg(0x80, 0x01);
// Disable All LDOs
WriteReg(0x90, 0x00);
WriteReg(0x91, 0x00);
// Set DC1 to 3.3V
WriteReg(0x82, (3300 - 1500) / 100);
// Set ALDO1 to 3.3V
WriteReg(0x92, (3300 - 500) / 100);
// Enable ALDO1(MIC)
WriteReg(0x90, 0x01);
WriteReg(0x64, 0x02); // CV charger voltage setting to 4.1V
WriteReg(0x61, 0x02); // set Main battery precharge current to 50mA
WriteReg(0x62, 0x08); // set Main battery charger current to 400mA ( 0x08-200mA, 0x09-300mA, 0x0A-400mA )
WriteReg(0x63, 0x01); // set Main battery term charge current to 25mA
}
};
#define LCD_OPCODE_WRITE_CMD (0x02ULL)
#define LCD_OPCODE_READ_CMD (0x03ULL)
#define LCD_OPCODE_WRITE_COLOR (0x32ULL)
static const sh8601_lcd_init_cmd_t vendor_specific_init[] = {
{0x11, (uint8_t[]){0x00}, 0, 120},
{0x44, (uint8_t[]){0x01, 0xD1}, 2, 0},
{0x35, (uint8_t[]){0x00}, 1, 0},
{0x53, (uint8_t[]){0x20}, 1, 10},
{0x2A, (uint8_t[]){0x00, 0x00, 0x01, 0x6F}, 4, 0},
{0x2B, (uint8_t[]){0x00, 0x00, 0x01, 0xBF}, 4, 0},
{0x51, (uint8_t[]){0x00}, 1, 10},
{0x29, (uint8_t[]){0x00}, 0, 10}
};
// 在waveshare_amoled_1_8类之前添加新的显示类
class CustomLcdDisplay : public SpiLcdDisplay {
public:
CustomLcdDisplay(esp_lcd_panel_io_handle_t io_handle,
esp_lcd_panel_handle_t panel_handle,
int width,
int height,
int offset_x,
int offset_y,
bool mirror_x,
bool mirror_y,
bool swap_xy)
: SpiLcdDisplay(io_handle, panel_handle,
width, height, offset_x, offset_y, mirror_x, mirror_y, swap_xy) {
// Note: UI customization should be done in SetupUI(), not in constructor
// to ensure lvgl objects are created before accessing them
}
virtual void SetupUI() override {
// Call parent SetupUI() first to create all lvgl objects
SpiLcdDisplay::SetupUI();
DisplayLockGuard lock(this);
lv_obj_set_style_pad_left(status_bar_, LV_HOR_RES * 0.1, 0);
lv_obj_set_style_pad_right(status_bar_, LV_HOR_RES * 0.1, 0);
}
};
class CustomBacklight : public Backlight {
public:
CustomBacklight(esp_lcd_panel_io_handle_t panel_io) : Backlight(), panel_io_(panel_io) {}
protected:
esp_lcd_panel_io_handle_t panel_io_;
virtual void SetBrightnessImpl(uint8_t brightness) override {
auto display = Board::GetInstance().GetDisplay();
DisplayLockGuard lock(display);
uint8_t data[1] = {((uint8_t)((255 * brightness) / 100))};
int lcd_cmd = 0x51;
lcd_cmd &= 0xff;
lcd_cmd <<= 8;
lcd_cmd |= LCD_OPCODE_WRITE_CMD << 24;
esp_lcd_panel_io_tx_param(panel_io_, lcd_cmd, &data, sizeof(data));
}
};
class WaveshareEsp32s3TouchAMOLED1inch8 : public WifiBoard {
private:
i2c_master_bus_handle_t codec_i2c_bus_;
Pmic* pmic_ = nullptr;
Button boot_button_;
CustomLcdDisplay* display_;
CustomBacklight* backlight_;
esp_io_expander_handle_t io_expander = NULL;
PowerSaveTimer* power_save_timer_;
int last_discharging_ = -1; // -1 = unknown, 0 = charging (always-on), 1 = discharging
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(20);
});
power_save_timer_->OnExitSleepMode([this]() {
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->OnShutdownRequest([this]() {
// Block auto power-off while plugged into the charger so the device
// stays "always-on" for wake word detection. Display dim and the
// sleep emoji from OnEnterSleepMode still kick in after 60s as usual.
if (last_discharging_ == 0) {
return;
}
pmic_->PowerOff();
});
power_save_timer_->SetEnabled(true);
}
void InitializeCodecI2c() {
// Initialize I2C peripheral
i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = I2C_NUM_0,
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.trans_queue_depth = 0,
.flags = {
.enable_internal_pullup = 1,
},
};
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &codec_i2c_bus_));
}
void InitializeTca9554(void) {
esp_err_t ret = esp_io_expander_new_i2c_tca9554(codec_i2c_bus_, I2C_ADDRESS, &io_expander);
if(ret != ESP_OK)
ESP_LOGE(TAG, "TCA9554 create returned error");
ret = esp_io_expander_set_dir(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1 |IO_EXPANDER_PIN_NUM_2, IO_EXPANDER_OUTPUT);
ret |= esp_io_expander_set_dir(io_expander, IO_EXPANDER_PIN_NUM_4, IO_EXPANDER_INPUT);
ESP_ERROR_CHECK(ret);
ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1|IO_EXPANDER_PIN_NUM_2, 1);
ESP_ERROR_CHECK(ret);
vTaskDelay(pdMS_TO_TICKS(100));
ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1|IO_EXPANDER_PIN_NUM_2, 0);
ESP_ERROR_CHECK(ret);
vTaskDelay(pdMS_TO_TICKS(300));
ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1|IO_EXPANDER_PIN_NUM_2, 1);
ESP_ERROR_CHECK(ret);
}
void InitializeAxp2101() {
ESP_LOGI(TAG, "Init AXP2101");
pmic_ = new Pmic(codec_i2c_bus_, 0x34);
}
void InitializeSpi() {
spi_bus_config_t buscfg = {};
buscfg.sclk_io_num = GPIO_NUM_11;
buscfg.data0_io_num = GPIO_NUM_4;
buscfg.data1_io_num = GPIO_NUM_5;
buscfg.data2_io_num = GPIO_NUM_6;
buscfg.data3_io_num = GPIO_NUM_7;
buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
buscfg.flags = SPICOMMON_BUSFLAG_QUAD;
ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO));
}
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode();
return;
}
app.ToggleChatState();
});
}
void InitializeSH8601Display() {
esp_lcd_panel_io_handle_t panel_io = nullptr;
esp_lcd_panel_handle_t panel = nullptr;
// 液晶屏控制IO初始化
ESP_LOGD(TAG, "Install panel IO");
esp_lcd_panel_io_spi_config_t io_config = SH8601_PANEL_IO_QSPI_CONFIG(
EXAMPLE_PIN_NUM_LCD_CS,
nullptr,
nullptr
);
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io));
// 初始化液晶屏驱动芯片
ESP_LOGD(TAG, "Install LCD driver");
const sh8601_vendor_config_t vendor_config = {
.init_cmds = &vendor_specific_init[0],
.init_cmds_size = sizeof(vendor_specific_init) / sizeof(sh8601_lcd_init_cmd_t),
.flags ={
.use_qspi_interface = 1,
}
};
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = GPIO_NUM_NC;
panel_config.flags.reset_active_high = 1,
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB;
panel_config.bits_per_pixel = 16;
panel_config.vendor_config = (void *)&vendor_config;
ESP_ERROR_CHECK(esp_lcd_new_panel_sh8601(panel_io, &panel_config, &panel));
esp_lcd_panel_reset(panel);
esp_lcd_panel_init(panel);
esp_lcd_panel_invert_color(panel, false);
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
esp_lcd_panel_disp_on_off(panel, true);
display_ = new CustomLcdDisplay(panel_io, panel,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
backlight_ = new CustomBacklight(panel_io);
backlight_->RestoreBrightness();
}
void InitializeTouch()
{
esp_lcd_touch_handle_t tp;
esp_lcd_touch_config_t tp_cfg = {
.x_max = DISPLAY_WIDTH,
.y_max = DISPLAY_HEIGHT,
.rst_gpio_num = GPIO_NUM_NC,
.int_gpio_num = GPIO_NUM_21,
.levels = {
.reset = 0,
.interrupt = 0,
},
.flags = {
.swap_xy = 0,
.mirror_x = 0,
.mirror_y = 0,
},
};
esp_lcd_panel_io_handle_t tp_io_handle = NULL;
esp_lcd_panel_io_i2c_config_t tp_io_config = {
.dev_addr = ESP_LCD_TOUCH_IO_I2C_FT5x06_ADDRESS,
.control_phase_bytes = 1,
.dc_bit_offset = 0,
.lcd_cmd_bits = 8,
.flags =
{
.disable_control_phase = 1,
}
};
tp_io_config.scl_speed_hz = 400 * 1000;
ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c(codec_i2c_bus_, &tp_io_config, &tp_io_handle));
ESP_LOGI(TAG, "Initialize touch controller");
ESP_ERROR_CHECK(esp_lcd_touch_new_i2c_ft5x06(tp_io_handle, &tp_cfg, &tp));
const lvgl_port_touch_cfg_t touch_cfg = {
.disp = lv_display_get_default(),
.handle = tp,
};
lvgl_port_add_touch(&touch_cfg);
ESP_LOGI(TAG, "Touch panel initialized successfully");
}
// 初始化工具
void InitializeTools() {
auto &mcp_server = McpServer::GetInstance();
mcp_server.AddTool("self.system.reconfigure_wifi",
"End this conversation and enter WiFi configuration mode.\n"
"**CAUTION** You must ask the user to confirm this action.",
PropertyList(), [this](const PropertyList& properties) {
EnterWifiConfigMode();
return true;
});
}
public:
WaveshareEsp32s3TouchAMOLED1inch8() :
boot_button_(BOOT_BUTTON_GPIO) {
InitializePowerSaveTimer();
InitializeCodecI2c();
InitializeTca9554();
InitializeAxp2101();
InitializeSpi();
InitializeSH8601Display();
InitializeTouch();
InitializeButtons();
InitializeTools();
}
virtual AudioCodec* GetAudioCodec() override {
static Es8311AudioCodec audio_codec(codec_i2c_bus_, I2C_NUM_0, AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_GPIO_MCLK, AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN,
AUDIO_CODEC_PA_PIN, AUDIO_CODEC_ES8311_ADDR);
return &audio_codec;
}
virtual Display* GetDisplay() override {
return display_;
}
virtual Backlight* GetBacklight() override {
return backlight_;
}
virtual bool GetBatteryLevel(int &level, bool& charging, bool& discharging) override {
charging = pmic_->IsCharging();
discharging = pmic_->IsDischarging();
int discharging_state = discharging ? 1 : 0;
if (discharging_state != last_discharging_) {
if (discharging) {
ESP_LOGI(TAG, "Always-on disabled (battery, auto power-off armed)");
// Reset the timer so we don't shut down immediately if the
// charger is unplugged after the device has been idle a while.
power_save_timer_->WakeUp();
} else {
ESP_LOGI(TAG, "Always-on enabled (charging, auto power-off blocked)");
}
last_discharging_ = discharging_state;
}
level = pmic_->GetBatteryLevel();
return true;
}
virtual void SetPowerSaveLevel(PowerSaveLevel level) override {
if (level != PowerSaveLevel::LOW_POWER) {
power_save_timer_->WakeUp();
}
WifiBoard::SetPowerSaveLevel(level);
}
};
DECLARE_BOARD(WaveshareEsp32s3TouchAMOLED1inch8);