Skip to content

Power button short press when sleeping refreshes TRMNL#4

Open
mcmphd wants to merge 8 commits into
yazdipour:developmentfrom
mcmphd:feature/trmnl-power-button-refresh
Open

Power button short press when sleeping refreshes TRMNL#4
mcmphd wants to merge 8 commits into
yazdipour:developmentfrom
mcmphd:feature/trmnl-power-button-refresh

Conversation

@mcmphd

@mcmphd mcmphd commented Jul 2, 2026

Copy link
Copy Markdown

Problem

Short-pressing the power button while the device is deep-asleep with the TRMNL
sleep screen active did nothing; a long press fully booted into
Home/Reader — there was no way to refresh the TRMNL sleep screen and immediately go back to sleep.

Design

SHORT_PWRBTN (the existing "what does a short press do" enum) only dispatches
once the device has already fully booted — it never fires while still asleep.
What actually gates a press while asleep is HalGPIO::verifyPowerButtonWakeup(),
called from setup() before any activity is pushed. This feature hooks in there
instead, reusing enterDeepSleep()SleepActivity::renderTrmnlSleepScreen()
(the same call the periodic TRMNL refresh already uses) rather than duplicating it.

Off by default: Settings > System > TRMNL Settings > Refresh on Power Button.
Long-press wake is completely unchanged.

While refreshing, the screen shows a hint ("Short press power to refresh" / "Long
press power to wake up") with an arrow pointing at the physical power button, and
the usual "Going to sleep" popup reads "Refreshing" for this path specifically —
every other sleep trigger/mode is unaffected.

Verification

pio run -e simulator builds clean. The simulator can't measure press duration
at all (confirmed via the external crosspoint-simulator dependency's stubbed
verifyPowerButtonWakeup), so measurePowerButtonPressWasShort is compiled only
for real hardware (#ifndef SIMULATOR). Wake routing, the settings toggle, the
popup/hint text swap, the arrow, and a lastSleepFromReader preservation fix
this path needed were verified in the simulator by directly driving
WakeupReason::PowerButton via CROSSPOINT_SIM_WAKE_REASON=power and
temporarily forcing the refresh branch on to confirm no crashes end-to-end.

Confirmed on a real X4 device:

  • Short press refreshes the TRMNL sleep screen in place, without opening the
    reader
  • Long press still wakes normally and opens the reader, unchanged
  • The power-button arrow points in the correct direction/position
  • The "Refresh on Power Button" toggle actually gates the behavior (off by
    default, only takes effect once enabled)

Summary by Sourcery

Enable short power-button presses during TRMNL sleep to refresh the TRMNL screen in place and immediately return to sleep, gated by a new user setting.

New Features:

  • Add an optional TRMNL setting that lets a short power-button press while the TRMNL sleep screen is active refresh the screen in place instead of fully waking.

Bug Fixes:

  • Preserve the lastSleepFromReader flag when performing a TRMNL refresh-in-place so reader resume behavior is not corrupted.

Enhancements:

  • Introduce a dedicated TRMNL refresh status screen with guidance text and an arrow pointing to the physical power button, along with a context-specific "Refreshing" popup during this path.
  • Refine power-button wake handling by separating short-press measurement from wake rejection logic and allowing configuration of short-press handling when waking from sleep.

Documentation:

  • Add English translations for the new TRMNL refresh setting, hint text, and "Refreshing" status label.

yazdipour and others added 7 commits June 27, 2026 21:40
… place

Short-pressing the power button while the device is deep-asleep with the
TRMNL sleep screen active now re-fetches and re-renders the TRMNL image
and goes straight back to sleep, instead of either silently doing nothing
(the previous IGNORE default) or fully booting into Home/Reader. A long
press continues to wake normally, unchanged.

Root cause / design: SHORT_PWRBTN (the existing configurable "what does a
short press do" enum, e.g. FORCE_REFRESH) only dispatches from the main
loop, once the device has *already* fully booted -- it never fires while
still asleep. What actually gates a press while asleep is
HalGPIO::verifyPowerButtonWakeup(), called from setup() before any
activity is pushed. Wiring this feature through SHORT_PWRBTN as originally
scoped would still flash Home/Reader before refreshing; the correct hook
is the wake gate itself.

verifyPowerButtonWakeup is refactored (behavior-preserving) to delegate
its calibrated press-duration polling to a new
HalGPIO::measurePowerButtonPressWasShort(), which the new TRMNL path also
calls -- always allowing the boot to continue (shortPressAllowed=true)
when sleepScreen==TRMNL, then measuring short vs. long against the
existing long-press threshold to decide refresh-in-place vs. normal wake.
The refresh itself reuses enterDeepSleep() -> activityManager.goToSleep()
-> SleepActivity::renderTrmnlSleepScreen(), the same call the periodic
TRMNL refresh already uses, rather than duplicating it.

enterDeepSleep() gained a preserveLastSleepFromReader parameter: called
this early (before Home/Reader/Boot is ever pushed),
activityManager.isReaderActivity() would wrongly report false and corrupt
APP_STATE.lastSleepFromReader, breaking reader-resume on the next real
wake. All other call sites are unaffected (default false).

A new "Refreshing" status string (STR_REFRESHING) is shown in place of
the "BOOTING" splash during this path, which skips the splash entirely.

Not gated behind a new SHORT_PWRBTN option: that enum doesn't govern this
code path at all (see above), so adding one wouldn't be reachable here.
Instead this activates automatically whenever sleepScreen==TRMNL, which
is itself an explicit, existing user choice.

Verification: pio run -e simulator builds clean. The simulator's wake
path (external crosspoint-simulator dependency, not part of this repo)
cannot measure press duration at all -- confirmed via
.pio/libdeps/simulator/simulator/src/HalGPIO.cpp, where
verifyPowerButtonWakeup is a no-op and any keypress triggers an immediate
reboot regardless of duration -- so measurePowerButtonPressWasShort is
only compiled for real hardware (#ifndef SIMULATOR) and the short/long
distinction itself needs a real device flash to confirm. The rest of the
new logic (wake routing, display setup, "Refreshing" render, TRMNL fetch
with graceful fallback on missing config, SleepActivity entry, clean
re-sleep, and the lastSleepFromReader preservation fix) was verified by
directly driving the simulator's WakeupReason::PowerButton path via
CROSSPOINT_SIM_WAKE_REASON=power against a local settings file with
sleepScreen=TRMNL, temporarily forcing the refresh branch on to confirm
it runs end-to-end with no crashes, and confirming lastSleepFromReader
survives the refresh when set to true beforehand. The temporary override
is not part of this commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The short-press-refreshes-TRMNL-in-place behavior from the previous commit
was unconditional whenever sleepScreen==TRMNL, with no way to opt out.
Add Settings > System > TRMNL Settings > Refresh on Power Button, off by
default (matching the Extended Wi-Fi Timeout toggle's convention of
preserving prior behavior unless explicitly enabled).

- CrossPointSettings: new trmnlRefreshOnPowerButton field, registered in
  getSettingsList() for JSON persistence (same pattern as trmnlOrientation
  and trmnlExtendedWifiTimeout).
- TrmnlSettingsActivity: new list item, toggled and saved the same way as
  the other TRMNL settings.
- main.cpp: trmnlSleepScreenActive now also requires the toggle, which
  gates both the wake-gate fast-path (shortPressAllowed) and the
  press-duration measurement -- off means byte-for-byte the pre-feature
  wake behavior.

Verified: pio run -e simulator builds clean. Confirmed via
CROSSPOINT_SIM_WAKE_REASON=power against a local settings file with
sleepScreen=TRMNL that shortAllowed logs 0 with the new field absent/0
(default) and 1 once set to 1, matching the gate's intent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…fresh

Two text tweaks to the refresh-in-place flow, requested after trying it
conceptually: the transient full-screen status shown at the very start
now reads as a usage hint rather than a bare status word, and the "Going
to sleep" popup that SleepActivity always shows on the way down now says
"Refreshing" specifically for this path (unchanged for every other sleep
trigger/mode).

- showTrmnlRefreshingStatus() now shows two lines ("Short press power to
  refresh" / "Long press power to wake up") instead of just "Refreshing".
- SleepActivity gains an isPowerButtonRefresh constructor flag (threaded
  through ActivityManager::goToSleep() and enterDeepSleep(), replacing
  the narrower preserveLastSleepFromReader flag added previously -- this
  is the more accurate name for what that flag actually represents, and
  now drives two behaviors instead of one). onEnter() swaps its "Going to
  sleep" popup for "Refreshing" only when this flag is set; every other
  sleep-screen mode and sleep trigger (timeout, manual sleep, etc.) is
  unaffected.
- STR_REFRESHING moves from the BOOTING-style all-caps splash convention
  to the ENTERING_SLEEP-style sentence-case popup convention, since it's
  no longer used for the former.

Verified: pio run -e simulator builds clean. Re-ran the same
CROSSPOINT_SIM_WAKE_REASON=power + temporarily-forced-refresh-branch
check used for the original feature commit; confirmed the same
crash-free activity sequence (status screen -> SleepActivity -> TRMNL
fetch fallback -> deep sleep) with the new strings compiled in. Exact
on-device text rendering (line wrap, popup sizing) still needs a real
device flash to see visually -- not verifiable in this simulator per
the original commit's limitations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The refresh-in-place status screen (showTrmnlRefreshingStatus) explains
what the buttons do but didn't say where the power button physically is.
Add a simple up-pointing chevron-and-shaft arrow in the top-left corner
of the screen, matching the X4's physical power button placement
(top edge, offset left) as confirmed by the user.

Reuses the existing stacked-widening-lines chevron technique already
used for BaseTheme's list scroll arrows (src/components/themes/
BaseTheme.cpp) rather than introducing a new drawing primitive or a
bitmap asset -- there were no existing button-hint diagrams or arrow
assets anywhere in the repo to reuse (confirmed by search).

Text casing left unchanged (sentence-style, matching Apple HIG guidance:
capitalize only the first word, no title-case/all-caps for generic
hardware terms like "power").

Verified: pio run -e simulator builds clean. Re-ran the same forced-
refresh-branch trace used for prior commits on this branch to confirm
the arrow-drawing code executes without crashing. Pixel-level visual
verification (arrow proportions, exact on-screen position) was not
possible in this environment -- the simulator binary isn't a registered
macOS app bundle, so GUI screenshot tooling can't target its window, and
hand-decoding the raw rotated 1bpp framebuffer risked a wrong read more
than it was worth for a cosmetic check. Geometric placement was sanity-
checked against the known 480x800 logical portrait canvas (arrow head
spans roughly x=33-47, y=24-32; shaft to y=52; well clear of the text
centered around y=400, no clipping or overlap). Real device flash will
be the actual visual confirmation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous commit placed the arrow at the top-left corner based on an
initial rough description. Corrected per an actual physical measurement:
the power button is on the device's right edge, about 20% of the way
down from the top (~22mm).

Replaces the up-pointing chevron with a right-pointing arrow (shaft plus
a two-line diagonal head, mirroring BaseTheme's keyboard backspace arrow
rotated 180 degrees) positioned at the right edge, y = pageHeight/5.

Verified: pio run -e simulator builds clean. Re-ran the same forced-
refresh-branch trace as prior commits to confirm no crash. Geometry
sanity-checked against the 480x800 logical portrait canvas: arrow tip at
x=456, head spans y=152-168, shaft to x=432 -- within bounds, clear of
the text block centered around y=400.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements an optional feature where a short power-button press while the TRMNL sleep screen is active refreshes the screen in place and immediately re-enters deep sleep, including input-duration measurement on real hardware, new settings plumbing, and sleep/boot UI updates, while preserving existing long-press wake behavior.

Sequence diagram for short power-button TRMNL refresh-in-place boot path

sequenceDiagram
    actor User
    participant HalGPIO as HalGPIO
    participant Main as setup
    participant Renderer as GfxRenderer
    participant ActivityMgr as ActivityManager
    participant SleepAct as SleepActivity

    User->>HalGPIO: Power button press (device in TRMNL sleep)
    HalGPIO-->>Main: getWakeupReason() == PowerButton
    Main->>HalGPIO: verifyPowerButtonWakeup(getPowerButtonWakeDuration, shortPressAllowed)
    Note over Main,HalGPIO: Short press allowed when TRMNL sleep screen active
    Main->>HalGPIO: measurePowerButtonPressWasShort(getPowerButtonLongPressDuration)
    HalGPIO-->>Main: true (short press)
    Main->>Main: powerButtonWakeIsTrmnlRefresh = true

    Main->>Renderer: setupDisplayAndFonts(seamless=true)
    Main->>Renderer: showTrmnlRefreshingStatus()
    Main->>ActivityMgr: enterDeepSleep(fromTimeout=false, isPowerButtonRefresh=true)
    ActivityMgr->>SleepAct: SleepActivity(..., fromTimeout=false, isPowerButtonRefresh=true)
    SleepAct->>Renderer: GUI.drawPopup(STR_REFRESHING)
    SleepAct->>SleepAct: renderTrmnlSleepScreen()
    SleepAct->>HalGPIO: esp_deep_sleep_start() (via enterDeepSleep)
    HalGPIO-->>User: Device returns to deep sleep with refreshed TRMNL screen
Loading

File-Level Changes

Change Details Files
Route power-button short press during TRMNL sleep into a refresh-in-place path instead of a full boot, including wake-duration handling and deep-sleep entry adjustments.
  • Extend enterDeepSleep and ActivityManager::goToSleep to accept an isPowerButtonRefresh flag, using it to avoid clobbering APP_STATE.lastSleepFromReader and to propagate the mode into SleepActivity.
  • In setup(), detect power-button wakeups, decide whether short presses are allowed based on TRMNL sleep settings and SHORT_PWRBTN mode, and on real hardware use HalGPIO::measurePowerButtonPressWasShort to decide between refresh-in-place vs normal boot.
  • On a short-press TRMNL refresh wake, initialize display/fonts, render a transient TRMNL refresh status screen, and immediately re-enter deep sleep via the new refresh path instead of continuing normal boot.
  • Update SleepActivity::onEnter to choose between "Going to sleep" and the new "Refreshing" popup text based on the isPowerButtonRefresh flag.
src/main.cpp
src/activities/ActivityManager.cpp
src/activities/ActivityManager.h
src/activities/boot_sleep/SleepActivity.cpp
src/activities/boot_sleep/SleepActivity.h
src/GlobalActions.h
Refactor power-button wake verification to support both wake rejection and short/long press classification, with simulator-aware behavior.
  • Modify HalGPIO::verifyPowerButtonWakeup to delegate short-press detection to a new helper and immediately re-enter deep sleep on short presses.
  • Introduce HalGPIO::measurePowerButtonPressWasShort, which calibrates against boot time, samples input until timeout or release, and returns whether the press duration was below the long-press threshold without initiating sleep itself.
  • Guard calls to measurePowerButtonPressWasShort with #ifndef SIMULATOR and document simulator limitations around measuring press duration.
lib/hal/HalGPIO.cpp
lib/hal/HalGPIO.h
src/main.cpp
Add a user-facing setting to enable TRMNL refresh-on-power-button and surface it in the TRMNL settings UI.
  • Add trmnlRefreshOnPowerButton to CrossPointSettings with default off and include it in the settings list as a toggle under TRMNL settings.
  • Extend TrmnlSettingsActivity to include a fifth menu item for the new setting, toggling the backing field and reflecting its ON/OFF state in the menu.
  • Wire the new setting into wakeup logic to gate short-press refresh behavior to TRMNL sleep-screen mode when enabled.
src/CrossPointSettings.h
src/SettingsList.h
src/activities/settings/TrmnlSettingsActivity.cpp
src/main.cpp
Update UI and localization to support the TRMNL refresh flow, including a visual power-button hint screen.
  • Add showTrmnlRefreshingStatus in main.cpp to clear the screen, draw a right-pointing arrow toward the physical power button, and render two centered hint lines before deep sleep during a TRMNL refresh.
  • Extend English translations with strings for "Refreshing", the new TRMNL setting label, and the short/long-press hint text.
  • Use the new STR_REFRESHING string when entering SleepActivity in the power-button refresh path.
src/main.cpp
lib/I18n/translations/english.yaml
src/activities/boot_sleep/SleepActivity.cpp

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In TrmnlSettingsActivity, the hard-coded index 4 for the new "Refresh on Power Button" option is brittle; consider introducing a named constant or deriving indices from the menuNames array to keep the selection/render logic in sync if items are reordered.
  • HalGPIO::measurePowerButtonPressWasShort is now used with both the wake-duration and the long-press duration thresholds; it would be clearer to document this explicitly or split into two helpers (e.g., isShortPressForWake vs isShortPressForLongPressDetection) so the differing semantics are obvious at call sites.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `TrmnlSettingsActivity`, the hard-coded index `4` for the new "Refresh on Power Button" option is brittle; consider introducing a named constant or deriving indices from the `menuNames` array to keep the selection/render logic in sync if items are reordered.
- `HalGPIO::measurePowerButtonPressWasShort` is now used with both the wake-duration and the long-press duration thresholds; it would be clearer to document this explicitly or split into two helpers (e.g., `isShortPressForWake` vs `isShortPressForLongPressDetection`) so the differing semantics are obvious at call sites.

## Individual Comments

### Comment 1
<location path="lib/hal/HalGPIO.cpp" line_range="252" />
<code_context>
+void HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed) {
</code_context>
<issue_to_address>
**issue (bug_risk):** Short presses now always force deep sleep, ignoring `shortPressAllowed` and changing `verifyPowerButtonWakeup` semantics.

Previously this function distinguished between short presses that should be rejected and short presses that were explicitly allowed (via `shortPressAllowed`, e.g. `SHORT_PWRBTN::SLEEP`). With the new `measurePowerButtonPressWasShort(requiredDurationMs)` call and unconditional `startDeepSleep()` on `true`, all short presses now trigger deep sleep, ignoring `shortPressAllowed`.

This means:
- Configurations that allow short presses will still be put back to sleep, breaking the existing API contract.
- TRMNL refresh short presses are likely to be consumed here before `setup()` can handle them, making `powerButtonWakeIsTrmnlRefresh` hard or impossible to reach.

To preserve behavior while reusing the timing logic, you could:
- Use `measurePowerButtonPressWasShort` only to obtain `bool isShort`, then apply the previous decision tree using `shortPressAllowed`, or
- Keep `verifyPowerButtonWakeup` as the wake gatekeeper and move the TRMNL-refresh decision (using `measurePowerButtonPressWasShort`) into `setup()` only.

Without such changes, this is likely to regress power‑button behavior and conflict with the new refresh-in-place feature.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread lib/hal/HalGPIO.cpp
- TrmnlSettingsActivity: replace the hard-coded index 4 with a named
  MENU_INDEX_REFRESH_ON_POWER_BUTTON constant, matching the review's
  suggestion. Keeps handleSelection()/render() in sync if items are ever
  reordered.
- HalGPIO::measurePowerButtonPressWasShort is a single generic duration-
  vs-threshold primitive deliberately reused from two call sites with
  different thresholds and meanings (noise-filter gate inside
  verifyPowerButtonWakeup vs. short/long classification in main.cpp's
  TRMNL routing). Rather than splitting into two identically-implemented
  wrapper functions, documented this explicitly at the declaration and at
  both call sites -- the review offered either as an acceptable fix, and
  the docs option avoids adding two functions whose only difference is
  their name. Renamed the parameter from longPressDurationMs to the more
  accurate durationMs since it isn't always a long-press duration.

Investigated but did NOT act on the review's third comment (bug_risk on
HalGPIO.cpp:252): it claimed verifyPowerButtonWakeup now ignores
shortPressAllowed and always forces deep sleep on a short press,
breaking the TRMNL refresh path. That's not what the code does -- the
`if (shortPressAllowed) { return; }` guard is unchanged from before this
feature (confirmed against the original commit's diff, which only
touched code after that guard) and still short-circuits before
measurePowerButtonPressWasShort/startDeepSleep are ever reached. Since
shortPressAllowed is true precisely when the TRMNL refresh path is
active, the claimed regression can't occur. Restructuring working code
to fix a bug that doesn't exist would add risk for no benefit, so left
the logic as-is and instead strengthened the guard's comment (see above)
so the same misread doesn't happen again on a future review.

Verified: pio run -e simulator builds clean. Re-ran the same forced-
refresh-branch trace used for every commit on this branch to confirm
behavior is unchanged (doc/naming-only change, no logic touched).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants