Skip to content

Commit 3783a53

Browse files
Marcel Dütscherclaude
andcommitted
Pip: real deep sleep after 3 min idle, woken by power button
Replaces the old light-sleep-with-1.5s-timer-poll approach with proper ESP32 deep sleep, which drops standby current from ~1 mA to ~10–150 µA. Idle ramp on Pip is now: 30 s → display dims to BRIGHT_DIM 60 s → BRIGHT_DARK + Sleeping UI (unchanged) 3 min → M5.Power.deepSleep(0, true) — wakes only on the power button (GPIO35 on M5StickC Plus 2) Force-sleep (BtnB / BtnA-hold) gets a FORCE_SLEEP_GRACE_MS = 3 s window during which BtnA still cancels the sleep, then drops into deep sleep the same way. NVS is saved and the speaker is torn down before the deepSleep() call so the buzzer doesn't pop on the way down. Drops LIGHT_SLEEP_MS and DISPLAY_OFF_AFTER_MS from pip_tuning.h. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a318302 commit 3783a53

2 files changed

Lines changed: 59 additions & 42 deletions

File tree

src/main_pip.cpp

Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@
1010
// State machine:
1111
// Idle → display shows treat + hint, ready for input
1212
// Throwing → ~800 ms post-shake animation (treat flies up + fades)
13-
// Sleeping → 30 s idle → Zzz screen (auto-dims separately)
13+
// Sleeping → 60 s idle → Zzz screen
14+
//
15+
// Power ramp:
16+
// 30 s → display dims to BRIGHT_DIM
17+
// 60 s → BRIGHT_DARK + Sleeping UI
18+
// 3 min → ESP32 deep sleep, woken by the power button (GPIO35)
1419
//
1520
// Inputs:
1621
// BtnA — cycle Apple → Carrot → Bone → Apple
17-
// BtnB — force sleep
22+
// BtnB — force sleep (3 s grace, then deep sleep)
1823
// shake — throw the currently selected treat
1924
//
2025
// Compiled only in [env:pip] / [env:pip-s3].
@@ -23,7 +28,6 @@
2328
#include <M5Unified.h>
2429
#include <Preferences.h>
2530
#include <math.h>
26-
#include <esp_sleep.h>
2731
#include <esp32-hal-cpu.h>
2832

2933
#include "target_caps.h"
@@ -42,6 +46,10 @@ struct PipState {
4246
pip::UiState state = pip::UiState::Idle;
4347
pip::MenuPage page = pip::MenuPage::Empty;
4448
bool forceSleep = false;
49+
// Set when forceSleep flips from false to true. Used to gate the
50+
// grace period before we actually drop into deep sleep, so an
51+
// accidental BtnB press can be cancelled with a BtnA click.
52+
uint32_t forceSleepStartMs = 0;
4553
uint32_t lastInteractionMs = 0;
4654
uint32_t throwStartMs = 0;
4755
uint32_t lastSavedMs = 0;
@@ -224,22 +232,27 @@ void updateUiState(uint32_t now) {
224232
// the threshold is crossed during the press.
225233
void handleButtons(uint32_t now) {
226234
if (M5.BtnA.wasHold()) {
227-
g_pet.forceSleep = true;
235+
g_pet.forceSleep = true;
236+
g_pet.forceSleepStartMs = now;
228237
pip::play(pip::Sound::Sleepy);
229238
return;
230239
}
231240
if (M5.BtnA.wasClicked()) {
232241
if (g_pet.forceSleep) {
233-
// Wake from forced sleep instead of cycling pages.
234-
g_pet.forceSleep = false;
242+
// Wake from forced sleep instead of cycling pages — only
243+
// works while we're still inside the grace window before
244+
// deep sleep kicks in.
245+
g_pet.forceSleep = false;
246+
g_pet.forceSleepStartMs = 0;
235247
g_pet.lastInteractionMs = now;
236248
pip::play(pip::Sound::Wake);
237249
return;
238250
}
239251
cyclePage(now);
240252
}
241253
if (M5.BtnB.wasClicked()) {
242-
g_pet.forceSleep = true;
254+
g_pet.forceSleep = true;
255+
g_pet.forceSleepStartMs = now;
243256
pip::play(pip::Sound::Sleepy);
244257
}
245258
}
@@ -272,30 +285,44 @@ void saveState() {
272285
}
273286

274287
// ── Display power (mobile strategy) ────────────────────────────────────────
288+
//
289+
// Two-step idle ramp:
290+
// 30 s → BRIGHT_DIM
291+
// 60 s → BRIGHT_DARK + Sleeping UI (set by updateUiState)
292+
// 3 min → ESP32 deep sleep, woken by the power button
293+
//
294+
// Force-sleep (BtnB / BtnA-hold) skips the ramp: it shows the Zzz UI for
295+
// FORCE_SLEEP_GRACE_MS so an accidental press can be undone with BtnA,
296+
// then drops into deep sleep too.
275297
uint8_t g_brightness = pip::BRIGHT_NORMAL;
276298
uint8_t g_curBrightness = 0xFF;
277-
bool g_displayAsleep = false;
299+
300+
void enterDeepSleep() {
301+
Serial.println(F("[pip] entering deep sleep"));
302+
saveState();
303+
M5.Speaker.end();
304+
// M5.Power.deepSleep(0, true) handles M5.Display.sleep(), enables
305+
// ext0/ext1 wakeup on the board's _wakeupPin (GPIO35 = power button
306+
// on StickC Plus 2), then calls esp_deep_sleep_start(). 0 = no timer
307+
// wakeup; the device only comes back via the power button.
308+
M5.Power.deepSleep(0, true);
309+
}
278310

279311
void applyDisplayPower(uint32_t now) {
280312
uint32_t idle = now - g_pet.lastInteractionMs;
281-
uint8_t target;
282-
bool wantSleep = false;
283-
if (idle >= pip::DISPLAY_OFF_AFTER_MS) { target = 0; wantSleep = true; }
284-
else if (idle >= pip::DARK_AFTER_MS) { target = pip::BRIGHT_DARK; }
285-
else if (idle >= pip::DIM_AFTER_MS) { target = pip::BRIGHT_DIM; }
286-
else { target = g_brightness; }
287-
288-
if (wantSleep && !g_displayAsleep) {
289-
M5.Display.setBrightness(0);
290-
M5.Display.sleep();
291-
g_displayAsleep = true;
292-
g_curBrightness = 0;
293-
return;
294-
}
295-
if (!wantSleep && g_displayAsleep) {
296-
M5.Display.wakeup();
297-
g_displayAsleep = false;
313+
314+
bool forceSleepRipe = g_pet.forceSleep
315+
&& (now - g_pet.forceSleepStartMs) >= pip::FORCE_SLEEP_GRACE_MS;
316+
if (idle >= pip::DEEP_SLEEP_AFTER_MS || forceSleepRipe) {
317+
enterDeepSleep();
318+
// unreachable
298319
}
320+
321+
uint8_t target;
322+
if (idle >= pip::DARK_AFTER_MS) target = pip::BRIGHT_DARK;
323+
else if (idle >= pip::DIM_AFTER_MS) target = pip::BRIGHT_DIM;
324+
else target = g_brightness;
325+
299326
if (target != g_curBrightness) {
300327
M5.Display.setBrightness(target);
301328
g_curBrightness = target;
@@ -500,20 +527,8 @@ void loop() {
500527
saveState();
501528
}
502529

503-
// Display power (auto-dim).
530+
// Display power (auto-dim, then deep sleep).
504531
applyDisplayPower(now);
505-
if (g_displayAsleep) {
506-
// Tear down the speaker before light-sleep: the buzzer's LEDC
507-
// channel pops audibly when it stops/restarts at sleep entry/exit
508-
// every ~1.5 s, which manifests as a slow "click click" while Pip
509-
// is supposed to be silent. Re-init on wake; the cost is well
510-
// under the per-cycle wake overhead and silence wins.
511-
M5.Speaker.end();
512-
esp_sleep_enable_timer_wakeup((uint64_t)pip::LIGHT_SLEEP_MS * 1000ULL);
513-
esp_light_sleep_start();
514-
M5.Speaker.begin();
515-
return;
516-
}
517532

518533
// Render.
519534
pip::PipView v{};

src/pip/pip_tuning.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,16 @@ constexpr uint32_t BTN_HOLD_SLEEP_MS = 600;
3838
// Mobile power strategy.
3939
constexpr uint32_t DIM_AFTER_MS = 30000; // 30 s → half brightness
4040
constexpr uint32_t DARK_AFTER_MS = 60000; // 60 s → 5 % brightness
41-
constexpr uint32_t DISPLAY_OFF_AFTER_MS = 300000; // 5 min → display off
41+
constexpr uint32_t DEEP_SLEEP_AFTER_MS = 180000; // 3 min → ESP32 deep sleep
4242
constexpr uint8_t BRIGHT_NORMAL = 150;
4343
constexpr uint8_t BRIGHT_DIM = 60;
4444
constexpr uint8_t BRIGHT_DARK = 12;
4545

46-
// Light-sleep period while the display is off. Wake every X ms for an
47-
// IMU check ("pet taken out of pocket" → display back on).
48-
constexpr uint32_t LIGHT_SLEEP_MS = 1500;
46+
// Grace period after a force-sleep button press before we actually enter
47+
// deep sleep. Long enough that the user can cancel via BtnA if they hit
48+
// BtnB by accident, short enough that "I want to save power now" still
49+
// feels responsive.
50+
constexpr uint32_t FORCE_SLEEP_GRACE_MS = 3000;
4951

5052
// Battery threshold at boot. Below this percent we don't go into full
5153
// operation — show "Low Battery" briefly and deep-sleep, otherwise the

0 commit comments

Comments
 (0)