fix(esp32-s3-touch-amoled-1.8): keep wake word always-on while charging (#1981)

* fix(esp32-s3-touch-amoled-1.8): keep wake word always-on while charging

The previous logic in GetBatteryLevel() used a static last_discharging
initialized to false. If the device booted on the charger (discharging
already false), the equality check never fired and PowerSaveTimer kept
running, so the device entered light sleep after 60s and stopped wake
word detection — defeating the always-on-while-charging behavior.

Move the tracker to a member with a tri-state initial value (-1 =
unknown) so the very first GetBatteryLevel() tick reconciles the timer
state regardless of boot conditions. Add ESP_LOGI on each transition
so the always-on state is visible in the logs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 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>

* Revert "fix(esp32-s3-touch-amoled-1.8): keep PowerSaveTimer ticking while charging"

This reverts commit d7a9b619eb.

* feat(esp32-s3-touch-amoled-1.8): add wake word heartbeat while charging

Brings back the display dim + sleepy emoji UX while plugged in by
keeping PowerSaveTimer running and gating just the shutdown callback
on charging state. To work around the wake word stalling after long
idle (~22 min observed in testing), add a periodic bounce timer that
restarts wake word detection every 5 minutes while idle on the
charger. Skips the bounce mid-conversation, on battery, or when wake
word is already disabled.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(esp32-s3-touch-amoled-1.8): bounce wake word on sleep mode entry

Hardware testing shows wake word detection stops responding shortly
after PowerSaveTimer enters sleep mode (display dim + sleepy emoji).
The 5-minute periodic heartbeat doesn't help — the failure happens
well before the first bounce.

Add a preemptive bounce in OnEnterSleepMode while charging, and
shorten the periodic heartbeat to 2 minutes as a safety net for any
later stalls.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(esp32-s3-touch-amoled-1.8): use standalone dim timer on charger

Hardware testing showed that the previous approach (keeping
PowerSaveTimer active with wake-word bounces on the charger) still
left wake word unresponsive after sleep mode entered — even with a
preemptive bounce in OnEnterSleepMode and a 2-min periodic heartbeat,
the AFE / audio pipeline kept stalling, and the user had to wake the
device with the boot button.

Switch to a different split:

- On charger: PowerSaveTimer is fully disabled. Its sleep callbacks
  never fire, so wake word detection is undisturbed. A standalone
  dim_timer (1s tick) handles the brightness + sleepy-emoji UX
  independently — on idle 60s it dims, on activity (conversation,
  button) it restores via Application::CanEnterSleepMode() polling.
- On battery: PowerSaveTimer runs as before (60s sleep callbacks,
  5min auto-shutdown), dim_timer stays stopped.

Also drops the wake-word bounce timer entirely — without
PowerSaveTimer's sleep mode firing on the charger there's nothing to
recover from.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Alxy Savin
2026-05-25 18:01:02 +03:00
committed by GitHub
parent f929f2f142
commit a596121acf

View File

@@ -125,6 +125,17 @@ private:
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
// Standalone dim-only timer used while charging. PowerSaveTimer is
// disabled on the charger so wake word detection isn't disturbed by its
// sleep callbacks; this lightweight timer applies just the visual dim
// (low brightness + sleepy emoji) on its own.
esp_timer_handle_t dim_timer_ = nullptr;
int dim_ticks_ = 0;
bool dimmed_ = false;
bool dim_timer_running_ = false;
static constexpr int kDimSeconds = 60;
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
@@ -142,6 +153,67 @@ private:
power_save_timer_->SetEnabled(true);
}
void InitializeDimTimer() {
esp_timer_create_args_t args = {
.callback = [](void* arg) {
auto self = static_cast<WaveshareEsp32s3TouchAMOLED1inch8*>(arg);
self->DimTick();
},
.arg = this,
.dispatch_method = ESP_TIMER_TASK,
.name = "dim_tick",
.skip_unhandled_events = true,
};
ESP_ERROR_CHECK(esp_timer_create(&args, &dim_timer_));
}
void ApplyDim(bool on) {
Application::GetInstance().Schedule([this, on]() {
if (on) {
ESP_LOGI(TAG, "Dim mode (always-on)");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(20);
} else {
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
}
});
}
void DimTick() {
if (!Application::GetInstance().CanEnterSleepMode()) {
// Activity (conversation, button) — restore display and reset.
dim_ticks_ = 0;
if (dimmed_) {
dimmed_ = false;
ApplyDim(false);
}
return;
}
dim_ticks_++;
if (dim_ticks_ >= kDimSeconds && !dimmed_) {
dimmed_ = true;
ApplyDim(true);
}
}
void StartDimTimer() {
if (dim_timer_running_) return;
dim_ticks_ = 0;
esp_timer_start_periodic(dim_timer_, 1000000);
dim_timer_running_ = true;
}
void StopDimTimer() {
if (!dim_timer_running_) return;
esp_timer_stop(dim_timer_);
dim_timer_running_ = false;
if (dimmed_) {
dimmed_ = false;
ApplyDim(false);
}
}
void InitializeCodecI2c() {
// Initialize I2C peripheral
i2c_master_bus_config_t i2c_bus_cfg = {
@@ -303,6 +375,7 @@ public:
WaveshareEsp32s3TouchAMOLED1inch8() :
boot_button_(BOOT_BUTTON_GPIO) {
InitializePowerSaveTimer();
InitializeDimTimer();
InitializeCodecI2c();
InitializeTca9554();
InitializeAxp2101();
@@ -329,12 +402,24 @@ public:
}
virtual bool GetBatteryLevel(int &level, bool& charging, bool& discharging) override {
static bool last_discharging = false;
charging = pmic_->IsCharging();
discharging = pmic_->IsDischarging();
if (discharging != last_discharging) {
int discharging_state = discharging ? 1 : 0;
if (discharging_state != last_discharging_) {
// On charger: disable PowerSaveTimer so its sleep-mode callbacks
// (which on this board appear to disrupt the AFE / audio pipeline)
// never fire. Run the standalone dim timer instead for the
// brightness + sleepy-emoji UX. On battery: reverse — disable dim
// timer, re-enable PowerSaveTimer (60s sleep, 5min auto-shutdown).
power_save_timer_->SetEnabled(discharging);
last_discharging = discharging;
if (discharging) {
StopDimTimer();
ESP_LOGI(TAG, "Always-on disabled (battery)");
} else {
StartDimTimer();
ESP_LOGI(TAG, "Always-on enabled (charging)");
}
last_discharging_ = discharging_state;
}
level = pmic_->GetBatteryLevel();