- Adds an in-repo `tuning/` crate that solves the four-knob LED-threshold tuning problem as a 4-objective Pareto search using published `heuropt` 0.8 (NSGA-III + a-posteriori weighted ranking), replacing `scripts/tune_runtime.py`'s single-composite-score grid. `just tune` runs it; 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`. - Retunes the shipping defaults from the new Pareto front: `RUN_DURATION` 4h00m → 3h51m, `YELLOW_AT` 30 → 22, `RED_AT` 25 → 11, `FAST_RED_AT` 20 → 4 (LED thresholds in minutes-remaining). Across 1,000 simulated workdays the new combination averages 26 minutes of lunch sleep and 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. - Bumps `config.device_release` 0x0200 → 0x0300 to match firmware version 0.3.0. - README "Why four hours…" → "Why these timings…", rewritten for the new methodology with the actual run statistics. `src/config.rs` module-level + lifecycle/phase comments updated accordingly. - Picks up a small `cargo fmt` drift in `src/chart.rs` and `src/led.rs` that had crept in under the 0.2.0 module split. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
10 KiB
10 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.3.0 - 2026-05-07
Added
tuning/crate — a host-side multi-objective NSGA-III tuner for jiggly's four lifecycle constants. Runs against publishedheuropt0.8 (with theparallelrayon 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.tomloverriding the firmware's inheritedthumbv6m-none-eabibuild target so it can usestd. Invoked viajust tuneorcargo run --releasefrom insidetuning/.
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_DURATION4h00m → 3h51m,YELLOW_AT30 → 22,RED_AT25 → 11,FAST_RED_AT20 → 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_release0x0200 → 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-repotuning/crate's NSGA-III multi-objective search. The new tuner ships with the firmware, builds reproducibly throughcargo/mise, and is just a normal Rust dependency onheuropt(no separateuvinvocation).
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.