# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.3.0] - 2026-05-07 ### Added - **`tuning/` crate** — a host-side multi-objective NSGA-III tuner for jiggly's four lifecycle constants. Runs against published [`heuropt`][heuropt] 0.8 (with the `parallel` rayon feature) and prints the Pareto front, extreme tradeoffs per objective, the firmware's current shipping defaults, and a single weighted-rank recommendation. The crate is its own workspace root with a local `.cargo/config.toml` overriding the firmware's inherited `thumbv6m-none-eabi` build target so it can use `std`. Invoked via `just tune` or `cargo run --release` from inside `tuning/`. ### Changed - **Lifecycle timings retuned** via the new tuner's 4-objective (work-time failure, lunch sleep, presses, after-hours waste) Pareto search, then collapsed by explicit decision weights. New shipping values: **`RUN_DURATION` 4h00m → 3h51m**, **`YELLOW_AT` 30 → 22**, **`RED_AT` 25 → 11**, **`FAST_RED_AT` 20 → 4** (LED thresholds in minutes-remaining). The 0.2.0 single-composite-score grid had baked the user's weight choices into the search itself; the new approach surfaces the legitimate tradeoffs first and applies preferences afterward. Across 1,000 simulated workdays the new combination averages 26 minutes of lunch sleep, lands in the 12:15–12:45 sweet spot on ~57 % of days, with zero mean work-time failure and ~2 min/day of after-hours waste. The 0.2.0 shipping defaults survive on the new Pareto front but rank well below the new pick under the same weights. - **`config.device_release` 0x0200 → 0x0300** — matches firmware version 0.3.0. - **README section heading "Why four hours…" → "Why these timings…"**, rewritten to describe the new methodology, the four objectives, the explicit decision weights, and the actual run statistics. ### Removed - **`scripts/tune_runtime.py`** — the Python single-composite-score grid search is superseded by the in-repo `tuning/` crate's NSGA-III multi-objective search. The new tuner ships with the firmware, builds reproducibly through `cargo`/`mise`, and is just a normal Rust dependency on `heuropt` (no separate `uv` invocation). [heuropt]: https://crates.io/crates/heuropt ## [0.2.0] - 2026-05-01 ### Added - **Composite USB device — mouse + keyboard HID** under one VID/PID. A new `WakingHost` parent state wraps two substates: `WakingWithKeyboard` taps **F13** 4× then idles, and a chart timer transitions to `WakingWithMouse` which 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 intuitive `Shift` because 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_wake` always sends an all-keys-released report after its work loop, regardless of which side of the deadline won. Adds a second `HidWriter` against the same `embassy_usb::Builder` (no hub simulation; standard USB composite), plus `KbdHid` / `KBD_HID_STATE` machinery and a `send_kbd` helper. - `hsmc` 0.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 one `statechart!` block; no more atomic flags, no more per-tick `Phase` enum, 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 phase `u(t) = t^2.5` drives 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. - 2 s `Settling` state 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 `i8` HID 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's `hid-logitech-dj` kernel 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 lets `hid-generic` bind immediately on Linux. - **`config.device_release = 0x0200`** (was unset / default `0x0010`). Matches firmware version `0.2.0`. - **`config.serial_number`** now derived from the RP2040's 64-bit unique chip ID, rendered as a 16-hex-char `&'static str` in a `StaticCell`. 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) → `WakingHost` parent with `WakingWithKeyboard` + `WakingWithMouse` substates; `animate_wake` → `wake_with_mouse`, matched by new `wake_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 new `Ctx.kbd: KbdHid`); `send` → `send_mouse`. - **`RUN_DURATION` 8 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 (start `triangular(8.0, mode=8.5, 9.5)`, lunch 12:00–13:00, end `triangular(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-picked `60/30/10` was almost exactly 50ᵗʰ-percentile; pushing the warning thresholds much closer to death lifts `P(any lunch sleep)` from 53 % → 74 % and `P(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. See `scripts/tune_runtime.py` for the simulation. - HID `poll_ms` 60 → 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→B` step 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: AtomicBool` and all `swap`/`store` calls. - `Phase` enum and the per-tick `phase_for(remaining)` switch — the breathing color is now read directly from elapsed time inside the `breathe_color` `during:`. - `led_task` and `sleep_chunked` helper — both subsumed by the chart. - `RUN_DURATION = Duration::from_secs(8 * 60 * 60)` and similar raw-integer-millisecond constants. All timing is now `Duration`-typed with named constructors (`from_hours`, `from_mins`, `from_millis`). ## [0.1.0] - 2026-04-27 ### Added - Initial Embassy-based `no_std` firmware for the Seeed Studio Xiao RP2040. - USB HID Boot Mouse, masquerading as a Dell MS116 (VID `0x413c`, PID `0x301a`). - 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.toml` pinning rust 1.95.0 with explicit components (`cargo`, `clippy`, `llvm-tools`, `rust-src`, `rust-std`, `rustc`, `rustfmt`) and the `thumbv6m-none-eabi` target, plus cargo helpers (`cargo-binutils`, `uf2conv`, `cargo-watch`, `cargo-bloat`, `cargo-expand`). - `justfile` recipes for `build`, `release`, `check`, `clippy`, `fmt`, `lint`, `ci`, `bin`, `uf2`, `flash`, `size`, `bloat`, `expand`, and `bootstrap`. All recipes execute inside `mise exec -- sh -eu -c` so the pinned toolchain is used regardless of shell activation state. [Unreleased]: https://git.swaits.com/swaits/jiggly/compare/v0.3.0...main [0.3.0]: https://git.swaits.com/swaits/jiggly/compare/v0.2.0...v0.3.0 [0.2.0]: https://git.swaits.com/swaits/jiggly/compare/v0.1.0...v0.2.0 [0.1.0]: https://git.swaits.com/swaits/jiggly/releases/tag/v0.1.0