Release-prep work for a publish-worthy 0.2.0: - LICENSE: MIT, © 2026 Stephen Waits. - README.md: tagline, what-it-does, hardware, build/flash recipes, ASCII statechart overview, "Why four hours…" runtime-tuning rationale, USB identity section. - Cargo.toml: 0.1.0 → 0.2.0; description, license, repository, readme, keywords, categories; publish = false (firmware, not a library); release profile tightened (lto = "fat", opt-level = "z", panic = "abort"). Flashed binary stays 47 KB; the size knobs are explicit rather than relying on defaults. - CHANGELOG.md: collapse Unreleased → [0.2.0] - 2026-05-01. - scripts/tune_runtime.py: 4-D Monte Carlo over (RUN_DURATION, YELLOW_AT, RED_AT, FAST_RED_AT). PEP 723 inline deps so `uv run` just works. Runtime + LED thresholds re-derived from a typical office workday distribution with a per-minute press-on-warning user model. Joint optimum: RUN_DURATION: 4h00m YELLOW_AT / RED_AT / FAST_RED_AT (min remaining): 30 / 25 / 20 USB identity: VID/PID: 046d:c07d (G502) → 1209:b0b0 (pid.codes) manufacturer: "Logitech" → "swaits.com" product: "G502 Mouse" → "jiggly" bcdDevice: default 0x0010 → 0x0200 (matches firmware version) serial: (none) → RP2040 chip ID as 16 hex chars Bug fix in the descriptor change: an interim version spoofed the Logitech Unifying Receiver (046d:c52b). On Linux, `hid-logitech-dj` matches that exact PID and tries to talk Logitech's HID++ protocol to enumerate paired wireless devices. The firmware doesn't speak HID++, so the driver waits through ~10–20 s of control-transfer timeouts on every plug before unbinding and letting `hid-generic` actually start polling. macOS has no such driver and was always fast. Moving to a pid.codes VID routes the device straight to `hid-generic`. Wake key: tapped Left Shift in early versions to wake the host. In practice that turned out to be exactly the nightmare scenario it sounds like — if the deadline preempted the loop between a Shift-down report and its Shift-up, the host would silently capitalise every keystroke from the user's real keyboard until the device was unplugged. Switched to F13: still wakes any modern OS, but no mainstream OS maps F13 by default, so a stuck F13 has zero visible effect. Also added an unconditional all-keys-released cleanup report at the end of the wake action, bounded by KBD_RELEASE_DEADLINE = 100 ms, so even a deadline that fires mid-press can't leave anything held. Wake refactor: WakingWithKeyboard and WakingWithMouse use oneshot `entry:` actions that race their work against an internal deadline via `embassy_futures::select`; the chart timer (`on(after KBD_PHASE_DURATION)` / `on(after MOUSE_PHASE_DURATION)`) advances. Removes a class of "slow USB ⇒ chart stalls" failure modes from the wake path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.1 KiB
8.1 KiB
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
0.2.0 - 2026-05-01
Added
- Composite USB device — mouse + keyboard HID under one VID/PID. A new
WakingHostparent state wraps two substates:WakingWithKeyboardtaps F13 4× then idles, and a chart timer transitions toWakingWithMousewhich runs the existing shake animation. Reason: macOS does not reliably wake from raw HID mouse motion alone; a keyboard event does. F13 was picked over the more intuitiveShiftbecause mainstream OSes don't map F13 by default — if a key ever gets stuck on the host (e.g. the wake deadline preempts the loop between a key-down and key-up report), nothing visible happens. Earlier versions used Left Shift and exhibited exactly that nightmare scenario in practice (host typing was capitalised until the device was unplugged). Belt-and-suspenders:keyboard_wakealways sends an all-keys-released report after its work loop, regardless of which side of the deadline won. Adds a secondHidWriteragainst the sameembassy_usb::Builder(no hub simulation; standard USB composite), plusKbdHid/KBD_HID_STATEmachinery and asend_kbdhelper. hsmc0.5.1 statechart drives the entire device lifecycle. The control flow (boot → host wake (kbd → mouse) → settle → spinner → active jiggle/flash → end-of-day spiral → power-down) is expressed declaratively in onestatechart!block; no more atomic flags, no more per-tickPhaseenum, no hand-rolled main loop.- Mouse animations driven by the chart's
during:activities:- Wake-up (
WakingDisplay): 10 frantic horizontal sin-shaped sweeps (~12 Hz, ±60 px, ~640 ms) — simulates "shake the mouse to wake the display." - Running (
ShowingRunning): 3 quick clockwise circles (radius 40 px, ~600 ms) — universal "spinner / in progress" cue. - Pre-shutdown (
Ending → ShutdownAnim): eased inward spiral 80 → 2 px over 5 turns (~5 s), starting 30 s before lifetime end. A shared phaseu(t) = t^2.5drives both radius and angle, so the rotation accelerates as the radius collapses — the visual signature of a coin/Euler-disk spinning down. - Warnings (
Active → Warning10 / Warning5): mini versions of the same spiral fire 10 min and 5 min before shutdown — 30 → 2 px over 2 turns (~1.5 s) and 50 → 2 px over 3 turns (~2.5 s) — so the user gets escalating kinetic foreshadowing of the death gesture that's coming.
- Wake-up (
- 2 s
Settlingstate between wake-up and running animations so the display has time to come out of sleep before the spinner draws. - All animations carry sub-pixel residue forward across
i8HID delta reports so the rendered shape matches the intended geometry rather than losing ~half its motion to truncation.
Changed
- USB descriptor: Logitech G502 (
046d:c07d, mouse-only) → pid.codes hobby slot (1209:b0b0). Product string"G502 Mouse"→"jiggly", manufacturer"Logitech"→"swaits.com". Briefly spoofed the Logitech Unifying Receiver (046d:c52b) on the way from G502 → final, but Linux'shid-logitech-djkernel driver matches that PID and runs ~10–20 s of HID++ control-transfer probes the firmware doesn't answer, blocking actual endpoint polling for that long on every plug. macOS doesn't have the driver and was unaffected. Switching to a pid.codes VID letshid-genericbind immediately on Linux. config.device_release = 0x0200(was unset / default0x0010). Matches firmware version0.2.0.config.serial_numbernow derived from the RP2040's 64-bit unique chip ID, rendered as a 16-hex-char&'static strin aStaticCell. Hosts now treat each replug as the same device, and two boards have distinct identities.- Statechart names cleaned up for symmetry:
BootSweep→Booting;Ev::SweepDone→Ev::BootDone;led_rgb_sweep→boot_sweep.WakingDisplay(flat) →WakingHostparent withWakingWithKeyboardWakingWithMousesubstates;animate_wake→wake_with_mouse, matched by newwake_with_keyboard.
ShowingRunning→Spinning;Ev::RunDone→Ev::SpinDone;animate_running→animate_spinner.Ending::ShutdownAnim→Ending::Spiraling;Ev::ShutdownAnimDone→Ev::SpiralDone;animate_shutdown→animate_final_spiral;SHUTDOWN_RADIUS_START/SHUTDOWN_TURNS/SHUTDOWN_FRAMES→FINAL_SPIRAL_*.Ctx.writer: Writer→Ctx.mouse: MouseHid(parallel to newCtx.kbd: KbdHid);send→send_mouse.
RUN_DURATION8 h → 4 h 00 m and LED phase boundaries 60 / 30 / 10 → 30 / 25 / 20 (YELLOW_AT/RED_AT/FAST_RED_AT, in minutes-remaining). Joint optimum from a 4-D Monte Carlo over a typical office workday distribution (starttriangular(8.0, mode=8.5, 9.5), lunch 12:00–13:00, endtriangular(16.0, mode=17.5, 19.0)) with a per-minute press-on-warning user model (yellow ~1.5 %/min, red ~4 %/min, fast-red ~6 %/min, plus small bumps at the 10'/5' spiral animations). The hand-picked60/30/10was almost exactly 50ᵗʰ-percentile; pushing the warning thresholds much closer to death liftsP(any lunch sleep)from 53 % → 74 % andP(sweet 12:15–12:45 spot)from 34 % → 52 %. The mechanism is non-obvious — long visible warnings cause more accidental morning RESET-presses that extend the cycle into the afternoon, which is the opposite of what the user wants. Seescripts/tune_runtime.pyfor the simulation.- HID
poll_ms60 → 8 (125 Hz). At the previous 60 ms poll the host was discarding ~7 of every 8 animation frames the firmware emitted. - Boot LED
R→G→Bstep 180 ms → 60 ms; 1.5 s pre-animation USB-settle delay removed. Reset → wake-shake latency dropped from ~2 s to ~240 ms (mouse-shake now ~740 ms after reset since 500 ms of keyboard-wake taps precede it). - Watchdog feeding moved out of the imperative main loop into its own
#[embassy_executor::task]; the chart owns user-visible state, the watchdog task owns the periodic kick.
Removed
JIGGLE_FLAG: AtomicBooland allswap/storecalls.Phaseenum and the per-tickphase_for(remaining)switch — the breathing color is now read directly from elapsed time inside thebreathe_colorduring:.led_taskandsleep_chunkedhelper — both subsumed by the chart.RUN_DURATION = Duration::from_secs(8 * 60 * 60)and similar raw-integer-millisecond constants. All timing is nowDuration-typed with named constructors (from_hours,from_mins,from_millis).
0.1.0 - 2026-04-27
Added
- Initial Embassy-based
no_stdfirmware for the Seeed Studio Xiao RP2040. - USB HID Boot Mouse, masquerading as a Dell MS116 (VID
0x413c, PID0x301a). - Jiggle pattern: pick a random axis from the RP2040 ROSC
RANDOMBIT, nudge +1 px, dwell 25 ms, nudge −1 px, then sleep 4.5 minutes before the next cycle. - 8-hour workday window after boot or
RESET (R)press, then silent idle (watchdog still fed) until the next manual reset. - 8-second hardware watchdog, fed in 5-second chunks during long sleeps.
mise.tomlpinning rust 1.95.0 with explicit components (cargo,clippy,llvm-tools,rust-src,rust-std,rustc,rustfmt) and thethumbv6m-none-eabitarget, plus cargo helpers (cargo-binutils,uf2conv,cargo-watch,cargo-bloat,cargo-expand).justfilerecipes forbuild,release,check,clippy,fmt,lint,ci,bin,uf2,flash,size,bloat,expand, andbootstrap. All recipes execute insidemise exec -- sh -eu -cso the pinned toolchain is used regardless of shell activation state.