Go to file
Krzysztof kuhy Rudnicki cb50e58a56 Phase 4/3: NFC room proof, runtime permissions, non-blocking step reads
Three defects found by reviewing the emulator run, all on the dismiss path:

1. ACTIVITY_RECOGNITION was declared but never requested at runtime (API 29+).
   Denied, TYPE_STEP_COUNTER delivers nothing, so checkSteps() reports "sensor
   unavailable" forever. With steps the only wired proof, that made the alarm
   *undismissable*, not merely degraded. MainActivity now requests it
   alongside POST_NOTIFICATIONS.

2. checkNfc/checkQr were dead code — no reader, no channel method, no
   registration — so steps was the only reachable proof. That inverts the
   decision ("NFC or QR qualifies; steps alone does NOT") and removes exactly
   the redundancy whose purpose is that one failing mechanism at 06:00 must
   never block dismissal. Added NfcReader (hand-written on the existing
   channel, NfcAdapter.enableReaderMode so the tap does not restart the
   activity and reset the challenge) plus RoomProofSettings for the registered
   tag/QR. mobile_scanner stays the droppable extra.

3. StepCounter blocked the platform main thread up to 3s per read, called from
   the MethodChannel handler — jank-to-ANR on the ring screen. Now registers a
   listener and serves the last sample; reads never block.

Also: raiseAlarmVolume() is guarded. setStreamVolume can throw under a zen
rule, and by then startForeground() has already posted — a failed volume bump
must not take the ring down with it.

Verified on the API 36 AVD with the real app widget and route (the earlier
probe rendered its own UI, so it proved the Kotlin path but nothing about
/ring): screen Asleep -> Awake, AlarmActivity top resumed over the lockscreen,
RingScreen showing the room-proof gate. New widget tests cover the /ring route
resolving and the challenge staying gated behind the room proof.

flutter test: 57 pass. dart analyze: clean. Zero Python files touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EckWmyfetYq3Mi8MAnUudh
2026-08-27 21:28:24 +02:00
.github/workflows Adopt the four markdown namespaces and gate them 2026-08-24 15:53:51 +02:00
docs/explanations Add change explanation: phone-driven-alarm-timer 2026-07-20 09:17:12 +02:00
phone_app Phase 4/3: NFC room proof, runtime permissions, non-blocking step reads 2026-08-27 21:28:24 +02:00
prompts Add the removal marker to every TODO file 2026-08-24 15:56:24 +02:00
scripts Phase 6: vmbox becomes the gate for the system scripts (item 10) 2026-08-27 20:49:16 +02:00
wake_alarm Phase 7: curtains open on the alarm, no-op until hardware exists (item 5) 2026-08-27 21:00:11 +02:00
.gitignore Add CI-mirror pre-push gate + requirements-sync check 2026-07-11 17:51:16 +02:00
.pre-commit-config.yaml Phase 0: fix dependency pin mismatch, stale docs, add speaker research 2026-08-26 22:53:01 +02:00
AGENTS.md Phase 4/3: the phone's Dart half — ring screen, challenges, room proofs 2026-08-27 21:22:07 +02:00
CLAUDE.md Phase 0: fix dependency pin mismatch, stale docs, add speaker research 2026-08-26 22:53:01 +02:00
DOCS-architecture.md Add the continuation prompt for the remaining hardening phases 2026-08-27 20:05:14 +02:00
DOCS-curtains-hardware.md Phase 7: curtains open on the alarm, no-op until hardware exists (item 5) 2026-08-27 21:00:11 +02:00
DOCS-speaker-recommendation.md Phase 0: fix dependency pin mismatch, stale docs, add speaker research 2026-08-26 22:53:01 +02:00
DOCS-vmbox.md Phase 6: vmbox becomes the gate for the system scripts (item 10) 2026-08-27 20:49:16 +02:00
install.sh Phase 6: vmbox becomes the gate for the system scripts (item 10) 2026-08-27 20:49:16 +02:00
pyproject.toml Bump gatelock to v0.4.1: keep the dismiss box focused after a monitor blip 2026-08-12 22:17:31 +02:00
README.md Phase 6: vmbox becomes the gate for the system scripts (item 10) 2026-08-27 20:49:16 +02:00
requirements.txt Bump gatelock to v0.4.1: keep the dismiss box focused after a monitor blip 2026-08-12 22:17:31 +02:00
shutdown-wrapper.sh Read the phone's alarm from Firebase, keeping GitHub as the mirror 2026-08-05 09:09:22 +02:00
sleep-hook.sh Extract wake_alarm from testsAndMisc as a standalone repo 2026-06-22 12:31:40 +02:00
TODO-file-length-250.md Add the removal marker to every TODO file 2026-08-24 15:56:24 +02:00
TODO-hardening-continue.md Record item 5 done, and two traps the split cascade cost 2026-08-27 21:00:30 +02:00
TODO-phone-alarm-session.md Add the handoff prompt for the phone-alarm session 2026-08-27 21:03:20 +02:00
wake-alarm-trigger.service Fire the alarm at the phone's time via a per-minute timer 2026-07-19 20:21:17 +02:00
wake-alarm-trigger.timer Fire the alarm at the phone's time via a per-minute timer 2026-07-19 20:21:17 +02:00
wake-alarm.service Fire the alarm at the phone's time via a per-minute timer 2026-07-19 20:21:17 +02:00

wake_alarm

A phone-driven wake alarm for an always-on PC. The alarm time is whatever the companion phone app last synced (via the shared crdt_sync transport over a private GitHub repo); a per-minute systemd user timer fires at exactly that time, showing a fullscreen, challenge-dismissed alarm with escalating audio and optional TP-Link Tapo P110 smart-plug control.

Change the alarm on the phone at any hour and the PC follows within ~5 minutes. If the PC does happen to be powered down, a shutdown wrapper hibernates with rtcwake set to the same synced time, as a fallback.

Install

bash install.sh

This installs the package + dependencies into system Python's user site-packages (the systemd units run /usr/bin/python directly, not a venv — see CLAUDE.md), installs and enables the per-minute trigger timer, the systemd-sleep resume hook, the shutdown wrapper that hibernates with rtcwake as a fallback, and (optionally) python-kasa for smart-plug control.

Reading the phone's alarm needs a GitHub token with repo scope, at ~/.config/wake_alarm/sync.json as {"token": "..."}. If that file is absent, a sibling app's ~/.config/{diet_guard,screen_locker}/sync_token is used instead, so one token covers every app syncing through kuhyx/syncs.

Usage

python -m wake_alarm._alarm --demo     # test the alarm window (safe, closeable)
python -m wake_alarm._sync --show      # what alarm time the PC resolves
python -m wake_alarm._trigger --dry-run  # what the next tick would do
journalctl --user -fu wake-alarm-trigger.service  # watch it live

The alarm fires automatically once installed; no manual invocation is needed in normal operation.

Development

python -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/pre-commit install && .venv/bin/pre-commit install --hook-type pre-push
.venv/bin/python -m pytest wake_alarm/tests/ --cov=wake_alarm --cov-branch --cov-fail-under=100

See CLAUDE.md for scheduling details, the hibernate/rtcwake mechanism, the cross-repo workout_log.json read, and production deployment gotchas.