[PR #234] [CLOSED] Add MODE_WAKE: smart-power-on wake personality (Lenovo Alt+P) #250

Closed
opened 2026-10-22 05:08:16 +00:00 by roman · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/safijari/openpuck/pull/234
Author: @Saffsanity
Created: 8/15/2026
Status: ❌ Closed

Base: main ← Head: mode-wake


📝 Commits (10+)

  • 4dbe150 Add MODE_WAKE: boot-keyboard personality for firmware smart power on
  • 95e3cef Surface MODE_WAKE in the panel, README, and CODE_MAP
  • 2103639 Shape the wake press like a human keystroke (fixes M90q Gen 6 S5 wake)
  • bc90a33 Keep the triggering controller alive through the post-fire boot
  • 040fbd7 Document wake mode: setup guide for smart-power-on hosts
  • 46ac644 Harden the wake flow against every review finding
  • 8b147c9 Auto-rearm: tell sleep from shutdown by the host's remote-wakeup arming
  • 7b36141 Auto-rearm: sample sleep-vs-shutdown once per down-episode, not per edge
  • 4b17fee Fix second-review findings: dead-bus truths, episode tracking, docs
  • 014e617 Retry the suspend power-off relay while the controller stays linked

📊 Changes

14 files changed (+982 additions, -40 deletions)

View changed files

📝 CODE_MAP.md (+30 -2)
📝 OpenPuck/OpenPuck.ino (+35 -15)
📝 OpenPuck/config.cpp (+35 -6)
📝 OpenPuck/config.h (+28 -1)
📝 OpenPuck/controllers.cpp (+3 -0)
📝 OpenPuck/haptics.cpp (+147 -3)
➕ OpenPuck/mode_wake.cpp (+339 -0)
➕ OpenPuck/mode_wake.h (+159 -0)
📝 OpenPuck/rf_link.cpp (+39 -7)
📝 OpenPuck/rf_link.h (+5 -0)
📝 OpenPuck/serial_console.cpp (+1 -1)
📝 README.md (+1 -0)
➕ docs/WAKE_MODE.md (+151 -0)
📝 docs/index.html (+9 -5)

📄 Description

Summary

Adds MODE_WAKE (mode 10): a bare boot-protocol keyboard personality that powers on a fully shut-down (S5) host through firmware "smart power on" (Lenovo: Alt+P from the always-on USB port). On the first controller connect after a 3 s link-quiet arming period it types the hotkey and reboots back into the puck — by the time the OS is up, the puck is a normal puck and the controller has reconnected by itself. USB remote wakeup only covers S3; this covers real power-on.

Hardware-validated on SteamOS (Lenovo ThinkCentre M90q Gen 6), including the optional WAKE_AUTO_REARM=1 build: shut down → puck re-arms itself → one controller power-button press → machine boots → controller is connected and working in SteamOS. Not yet exercised on a Windows host. A subsequent adversarial-review hardening pass (46ac644) tightened the arming/trigger/grace paths and was re-validated end-to-end on the same hardware. Should apply to any Lenovo/Dell-class box with a keyboard-hotkey power-on feature — hotkey/press-shaping/return mode are #defines at the top of mode_wake.h.

Default behavior is unchanged for everyone who doesn't use it. Mode 10 is only entered explicitly (panel / console x10 / assignable chord); WAKE_AUTO_REARM is compile-time off; the new suspend-policy gates are inert unless the puck is in wake mode or booting off a wake fire; the cfg layout is byte-identical (a dead reserved byte is repurposed, old cfg.bin files load unchanged).

User docs: docs/WAKE_MODE.md — BIOS setup, the designated port, LED diagnostics, auto-rearm trade-offs, adapting to other vendors.

Design notes

  • Dedicated personality, not an extra puck interface. The S5 embedded controller runs a minimal host stack written against real keyboards, so it sees exactly one boot keyboard and nothing else (reuses the clean-PS bareHid gating in setup(); keeps remote-wakeup in bmAttributes like a real keyboard). Also endpoint budget — the puck uses 6 of 7 IN.
  • Trigger = the anySlotLinkUp() connect up-edge (F-type replies only, so a neighbor puck's shared-address beacon can't false-fire), latched 2 s so an EC that suspends the port between enumeration and the press still fires — but a controller that connected against a dead port can't fire hours later into a manually booted OS. Arming requires ~5 s of observed link silence (the quiet clock runs only while RF is on air) so chording in with a connected controller doesn't type Alt+P into the live session.
  • Press shaping (hardware finding): the M90q EC reads but ignores a minimal combined Alt+P report; it accepts a human-shaped press — modifier alone first, long held chord restated while held, repeated. A real keyboard on the same port works either way, so the shaping is just "what a real keyboard produces".
  • Post-fire handoff grace (hardware finding): after the hotkey lands, the EC hands the port to the chipset and the bus suspends — during the press sequence and through POST. Without a grace, the suspend-persisted controller power-off kills the controller that just triggered the wake, and WAKE_AUTO_REARM re-enters wake mode mid-boot. The grace is a one-shot persisted in cfg (Cfg.wakeHandoff, the dead rsvd1 byte — this board class's bootloader wipes .noinit RAM every reset, and the return mode-switch already rewrites cfg, so persistence is free); it is deadline-only (90 s — a POST-time BIOS SET_CONFIGURATION reads mounted() long before the OS is up, so ending on mount would re-expose the kill during the BIOS→kernel gap), and a machine that never boots still gets the battery-saving power-off via an expiry fallback. The grace boot also skips setup()'s 3 s mount wait and opens the RF connect cooldown, putting beacons on air on the first loop() pass — the triggering controller relinks in 0.424 s (measured) instead of giving up during POST.
  • WAKE_SEQ_DEADLINE_MS (10 s) bails to settle-and-reboot if the EC deconfigures us mid-sequence, so a booted OS never receives a stray Alt+P.
  • WAKE_AUTO_REARM — compile-time, off by default. Persistent suspend in any other mode reboots into MODE_WAKE so it re-arms itself (the smart-power-on port keeps VBUS in S5, so the MCU never cold-boots). Sleep-aware: the OS arms DEVICE_REMOTE_WAKEUP before S3 (that's what makes wake-from-S3 work), a shutdown never does — sampled once at the first suspend edge of a down-episode, so the Smart Power On EC re-arming the feature during its S5 port takeover (a real M90q behavior caught by black-box tracing) can't masquerade as a sleeping OS. Sleeping leaves the puck a puck and controller-wake-from-S3 keeps working; shutdown re-arms. Still off by default: a host with device wakeup disabled in sysfs reads sleep as shutdown (degrades to re-arm-on-sleep). It intentionally bypasses the "no mode switch while suspended" rule the chord/console paths follow — rationale in mode_wake.cpp.

Commits

  1. mode_wake.{h,cpp} + plumbing (config.h, controllers.cpp, OpenPuck.ino, serial_console.cpp); also fixes a hardcoded <= 9 bound in the MODE_SUFFIX lookup that the MODE_MAX bump would otherwise have desynced.
  2. Surfacing: panel mode button + MODE_NAMES, README row, CODE_MAP section.
  3. Press shaping (from S5 hardware testing — see PR comments for the full campaign).
  4. Handoff grace + fast post-fire RF bring-up (ditto).
  5. docs/WAKE_MODE.md user guide.
  6. Review hardening: two independent adversarial review passes over the branch, all nine confirmed findings fixed (arming race, edge-latched trigger, escape-chord suspend exemption, persist-mode preservation, deadline-only grace, narrowed suspend power-off gate + never-booted fallback, LED contract, panel dialog, anySlotLinkUp millis-wrap guard).

Open question for maintainer

Identity is 28DE:574B — Valve VID with an invented PID, following the ReversePuck 28DE:1302 precedent. Happy to change if you'd prefer something else.

Status

  • clang-format-18 clean; trailing-comment lint adds zero new violations (a handful pre-exist upstream in touched files)
  • Both WAKE_AUTO_REARM variants compile clean (arduino-cli, adafruit:nrf52 1.7.0)
  • Hardware-validated end-to-end on the M90q Gen 6, including re-validation of the review-hardening commit 46ac644 (full S5 auto-rearm cycle + arming-race negative test — see the hardware comments)
  • CI has not run on this PR yet (first-time contributor) — maintainer: the Format/Build workflow runs need approval

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/safijari/openpuck/pull/234 **Author:** [@Saffsanity](https://github.com/Saffsanity) **Created:** 8/15/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `mode-wake` --- ### 📝 Commits (10+) - [`4dbe150`](https://github.com/safijari/openpuck/commit/4dbe150a8d7f8d6482dcf558b311cda61c2f9240) Add MODE_WAKE: boot-keyboard personality for firmware smart power on - [`95e3cef`](https://github.com/safijari/openpuck/commit/95e3cefeef81f6a563f8741cebd3ef20406c8b0d) Surface MODE_WAKE in the panel, README, and CODE_MAP - [`2103639`](https://github.com/safijari/openpuck/commit/210363980c7bca2b4e2d07b747f704da10c1587a) Shape the wake press like a human keystroke (fixes M90q Gen 6 S5 wake) - [`bc90a33`](https://github.com/safijari/openpuck/commit/bc90a33eabe96dcc0eb57a70807bb2903d76f865) Keep the triggering controller alive through the post-fire boot - [`040fbd7`](https://github.com/safijari/openpuck/commit/040fbd7944457cbfe45bb3801afd451c79970d64) Document wake mode: setup guide for smart-power-on hosts - [`46ac644`](https://github.com/safijari/openpuck/commit/46ac644b157b480cb6f62ff685f7b8b086960e01) Harden the wake flow against every review finding - [`8b147c9`](https://github.com/safijari/openpuck/commit/8b147c990cac8856c4589909ae2115c43cc941c5) Auto-rearm: tell sleep from shutdown by the host's remote-wakeup arming - [`7b36141`](https://github.com/safijari/openpuck/commit/7b361418309de55a000b19a02eca99e293ba9e1d) Auto-rearm: sample sleep-vs-shutdown once per down-episode, not per edge - [`4b17fee`](https://github.com/safijari/openpuck/commit/4b17feed4741f2f5da06f6ab8150ede8e869218e) Fix second-review findings: dead-bus truths, episode tracking, docs - [`014e617`](https://github.com/safijari/openpuck/commit/014e6175f95b1a9a1437847d5d7d0c9068bee3db) Retry the suspend power-off relay while the controller stays linked ### 📊 Changes **14 files changed** (+982 additions, -40 deletions) <details> <summary>View changed files</summary> 📝 `CODE_MAP.md` (+30 -2) 📝 `OpenPuck/OpenPuck.ino` (+35 -15) 📝 `OpenPuck/config.cpp` (+35 -6) 📝 `OpenPuck/config.h` (+28 -1) 📝 `OpenPuck/controllers.cpp` (+3 -0) 📝 `OpenPuck/haptics.cpp` (+147 -3) ➕ `OpenPuck/mode_wake.cpp` (+339 -0) ➕ `OpenPuck/mode_wake.h` (+159 -0) 📝 `OpenPuck/rf_link.cpp` (+39 -7) 📝 `OpenPuck/rf_link.h` (+5 -0) 📝 `OpenPuck/serial_console.cpp` (+1 -1) 📝 `README.md` (+1 -0) ➕ `docs/WAKE_MODE.md` (+151 -0) 📝 `docs/index.html` (+9 -5) </details> ### 📄 Description ## Summary Adds **MODE_WAKE** (mode 10): a bare boot-protocol keyboard personality that powers on a fully shut-down (S5) host through firmware "smart power on" (Lenovo: Alt+P from the always-on USB port). On the first controller connect after a 3 s link-quiet arming period it types the hotkey and reboots back into the puck — by the time the OS is up, the puck is a normal puck and the controller has reconnected by itself. USB remote wakeup only covers S3; this covers real power-on. **Hardware-validated on SteamOS** (Lenovo ThinkCentre M90q Gen 6), including the optional `WAKE_AUTO_REARM=1` build: shut down → puck re-arms itself → one controller power-button press → machine boots → controller is connected and working in SteamOS. Not yet exercised on a Windows host. A subsequent adversarial-review hardening pass (`46ac644`) tightened the arming/trigger/grace paths and was re-validated end-to-end on the same hardware. Should apply to any Lenovo/Dell-class box with a keyboard-hotkey power-on feature — hotkey/press-shaping/return mode are `#define`s at the top of `mode_wake.h`. **Default behavior is unchanged for everyone who doesn't use it.** Mode 10 is only entered explicitly (panel / console `x10` / assignable chord); `WAKE_AUTO_REARM` is compile-time off; the new suspend-policy gates are inert unless the puck is in wake mode or booting off a wake fire; the cfg layout is byte-identical (a dead reserved byte is repurposed, old `cfg.bin` files load unchanged). **User docs:** [docs/WAKE_MODE.md](https://github.com/Saffsanity/openpuck/blob/mode-wake/docs/WAKE_MODE.md) — BIOS setup, the designated port, LED diagnostics, auto-rearm trade-offs, adapting to other vendors. ## Design notes - **Dedicated personality, not an extra puck interface.** The S5 embedded controller runs a minimal host stack written against real keyboards, so it sees exactly one boot keyboard and nothing else (reuses the clean-PS `bareHid` gating in `setup()`; keeps remote-wakeup in bmAttributes like a real keyboard). Also endpoint budget — the puck uses 6 of 7 IN. - **Trigger** = the `anySlotLinkUp()` connect up-edge (F-type replies only, so a neighbor puck's shared-address beacon can't false-fire), latched 2 s so an EC that suspends the port between enumeration and the press still fires — but a controller that connected against a dead port can't fire hours later into a manually booted OS. Arming requires ~5 s of *observed* link silence (the quiet clock runs only while RF is on air) so chording in with a connected controller doesn't type Alt+P into the live session. - **Press shaping** (hardware finding): the M90q EC reads but *ignores* a minimal combined Alt+P report; it accepts a human-shaped press — modifier alone first, long held chord restated while held, repeated. A real keyboard on the same port works either way, so the shaping is just "what a real keyboard produces". - **Post-fire handoff grace** (hardware finding): after the hotkey lands, the EC hands the port to the chipset and the bus suspends — during the press sequence *and* through POST. Without a grace, the suspend-persisted controller power-off kills the controller that just triggered the wake, and `WAKE_AUTO_REARM` re-enters wake mode mid-boot. The grace is a one-shot **persisted in cfg** (`Cfg.wakeHandoff`, the dead `rsvd1` byte — this board class's bootloader wipes `.noinit` RAM every reset, and the return mode-switch already rewrites cfg, so persistence is free); it is deadline-only (90 s — a POST-time BIOS SET_CONFIGURATION reads `mounted()` long before the OS is up, so ending on mount would re-expose the kill during the BIOS→kernel gap), and a machine that never boots still gets the battery-saving power-off via an expiry fallback. The grace boot also skips `setup()`'s 3 s mount wait and opens the RF connect cooldown, putting beacons on air on the first `loop()` pass — the triggering controller relinks in 0.424 s (measured) instead of giving up during POST. - **`WAKE_SEQ_DEADLINE_MS`** (10 s) bails to settle-and-reboot if the EC deconfigures us mid-sequence, so a booted OS never receives a stray Alt+P. - **`WAKE_AUTO_REARM`** — compile-time, **off by default**. Persistent suspend in any other mode reboots into MODE_WAKE so it re-arms itself (the smart-power-on port keeps VBUS in S5, so the MCU never cold-boots). **Sleep-aware**: the OS arms `DEVICE_REMOTE_WAKEUP` before S3 (that's what makes wake-from-S3 work), a shutdown never does — sampled once at the first suspend edge of a down-episode, so the Smart Power On EC re-arming the feature during its S5 port takeover (a real M90q behavior caught by black-box tracing) can't masquerade as a sleeping OS. Sleeping leaves the puck a puck and controller-wake-from-S3 keeps working; shutdown re-arms. Still off by default: a host with device wakeup disabled in sysfs reads sleep as shutdown (degrades to re-arm-on-sleep). It intentionally bypasses the "no mode switch while suspended" rule the chord/console paths follow — rationale in `mode_wake.cpp`. ## Commits 1. `mode_wake.{h,cpp}` + plumbing (`config.h`, `controllers.cpp`, `OpenPuck.ino`, `serial_console.cpp`); also fixes a hardcoded `<= 9` bound in the `MODE_SUFFIX` lookup that the `MODE_MAX` bump would otherwise have desynced. 2. Surfacing: panel mode button + `MODE_NAMES`, README row, CODE_MAP section. 3. Press shaping (from S5 hardware testing — see PR comments for the full campaign). 4. Handoff grace + fast post-fire RF bring-up (ditto). 5. `docs/WAKE_MODE.md` user guide. 6. Review hardening: two independent adversarial review passes over the branch, all nine confirmed findings fixed (arming race, edge-latched trigger, escape-chord suspend exemption, persist-mode preservation, deadline-only grace, narrowed suspend power-off gate + never-booted fallback, LED contract, panel dialog, `anySlotLinkUp` millis-wrap guard). ## Open question for maintainer Identity is `28DE:574B` — Valve VID with an invented PID, following the ReversePuck `28DE:1302` precedent. Happy to change if you'd prefer something else. ## Status - [x] clang-format-18 clean; trailing-comment lint adds zero new violations (a handful pre-exist upstream in touched files) - [x] Both `WAKE_AUTO_REARM` variants compile clean (arduino-cli, adafruit:nrf52 1.7.0) - [x] Hardware-validated end-to-end on the M90q Gen 6, including re-validation of the review-hardening commit `46ac644` (full S5 auto-rearm cycle + arming-race negative test — see the hardware comments) - [ ] CI has not run on this PR yet (first-time contributor) — maintainer: the Format/Build workflow runs need approval --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
roman 2026-10-22 05:08:16 +00:00
Sign in to join this conversation.
No labels
pull-request
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
safijari/openpuck#250
No description provided.