Plus queue item 11: suspend-resume FTS calibration race (dt2w wake from s2idle wedges the touch controller; dpms serialization didn't cover it).
6 KiB
Lock / DPMS / wake — the lessons doc
The hard-won rules for everything between "screen goes dark" and "user is back in." Doctrine lives in SESSION-AUTHORITY-DOCTRINE.md; the boot choreography lives in session-authority-boot-order.md; the raw archaeology lives in DUMP-power-idle-lock-2026-07-15.md. This page is the distilled operating knowledge — what we know is true on blueline, with the receipts.
1. Ordering: lock, then off
Blanking an unlocked session leaks the desktop for a frame on wake, and a wake that races the lock can land input on an unlocked surface.
- Power button (
blueline-power-button): screen on →session lockIPC → wait ≤2s for the compositor-acked"locked": true→ blank. If the lock IPC fails, blank anyway — dark-but-unlocked is recoverable, lit-and- unlocked in a pocket is not. - Suspend:
PrepareForSleep→ lock secure → release the delay inhibitor. Fail-closed, ~5s budget. - Idle: the native IdleCoordinator locks (300s) well before hypridle's dpms-off (600s) and suspend (900s), so idle blanking always finds the session already locked.
2. One DPMS authority
blueline-screen-toggle serializes every panel power transition. Nothing
else calls hyprctl dispatch dpms — when hypridle still had a raw dpms
call after the toggle became authority, double-tap wake raced on-resume
and the touch controller calibrated against a half-ramped panel
(Pixel3Arch 73d0cd0). Any new wake source (power button, dt2w, proximity
someday) goes through the toggle, or it will reintroduce the race.
3. The FTS touch controller has opinions
- On panel-unprepare it drops to gesture mode (that is what makes dt2w work while the screen is off).
- On sense-on it self-calibrates against the panel. If the panel is still
ramping, calibration is garbage and touch goes deaf or erratic. The
kernel signature is a continuous spew of
fts_status_event_handler: … invalid … Strength soft Force cal. A single line after a wake is noise; a stream that keeps flowing minutes later is a wedged controller. - The serialized DPMS path fixed this for screen-off/on cycles. The suspend-resume path still races (found 2026-07-17): s2idle exit re-runs panel-prepare → FTS reset → Sense ON with nobody pacing it, and a dt2w wake from suspend produced exactly the wedged-controller spew and a dead swipe-to-unlock on the lockscreen. Open gap; candidate fixes are kernel-side (pace sense-on after panel ramp — check what Android does, Android is the reference) or a post-resume rebind hook, which needs care because rebinding while the panel is off may drop the dt2w feature flags.
- Recovery without reboot:
echo 2-0049 > /sys/bus/i2c/drivers/fts/unbind && sleep 1 && echo 2-0049 > /sys/bus/i2c/drivers/fts/bind(root). Firmware re-verifies, sense restarts. Expect a line or two of invalid-cal immediately after; continuous spew means it did not take.
4. Suspend truths
- Suspend is s2idle. The USB gadget (usb0 NCM/ACM) dies with it — an ssh
session dropping and
usb0: Lost carrierin the journal during suspend entry is normal, not a crash. Do not chase gadget "flaps" without first checkingjournalctlforPM: suspend entry. - dt2w wakes the phone from s2idle (gesture mode survives suspend).
- NetworkManager/ModemManager sleep-monitor churn around suspend entry is choreography, not failure.
- Warm reboots poison TZ/remoteproc state on blueline — recovery from a
bad state is a true cold boot (unplugged power-off), never
reboot.
5. Lock architecture (who holds what)
- souveraine-sessiond takes ext-session-lock at boot before the shell
exists; no desktop surface can precede the lock. The shell inherits the
lock later over the bridge socket (
misc.allow_session_lock_restore). - The sessiond↔shell handoff necessarily drops one lock client for ~1.05s.
Hyprland's
lockdead_screen_delaydefault (1000ms) painted a red "lockscreen crashed" flash in that gap; it is 4000ms in the tracked hyprland.lua. The session stays locked throughout — the flash was cosmetic. - Shell heartbeat loss → sessiond fail-closes: re-acquires the lock and presents its bare fallback PIN surface. Proven in anger 2026-07-17 when the shell died silently post-unlock. PAM at the fallback surface is a real unlock.
screenLocked(request) andscreenLockSecure(compositor ack) are different facts. Personal-tier gates key off secure. The compositor can end a lock unilaterally (ext_session_lock_v1_finished); the shell resyncsscreenLockedwhen that happens (LockScreen.qml onLockStateChanged) — before that fix, a stalelockRequested:trueboth lied to the gates and blocked re-lock.- Unlock is never an agent verb.
6. Shell surface rules
- Lock-path QML deploys only while the phone is UNLOCKED.
WlSessionLock.surfaceComponentcannot change while the lock is active; hot-deploying over an active lock wedges the lockscreen until reboot. - Two-stage lock: glance first, PIN pad on swipe-up (60px) / tap / any key; retreats on swipe-down (80px, empty PIN only) or 25s idle.
- hypridle's
after_sleep_cmdfires thelockFocusglobal shortcut on EVERY wake. It must only refocus the (hidden) PIN field — refocus must never imply PIN reveal, or every wake would skip the glance stage. - The shell runs under
souveraine-shell.service(journal keeps stderr, restart on failure, non-clean exits append to~/.local/state/souveraine/crashes.log, gives up after 8 tries and leaves the fail-closed lock in charge). It was a raw exec once; the crash reason died with it and unlock landed on a black desktop.
7. Open gaps
- Suspend-resume FTS calibration race (§3) — the one live regression.
- Crash reporting (queue item 10): crashes.log + coredumps + failed units surfaced as banner/sensoria, not just survived.
- Proximity wake must never touch lock state (DUMP-power-idle-lock-2026-07-15.md §2) — same doctrine as dt2w: wake sources move the panel, only sessiond moves the lock.