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].
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.
225233void 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.
275297uint8_t g_brightness = pip::BRIGHT_NORMAL ;
276298uint8_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
279311void 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{};
0 commit comments