chore: cut 0.3.0 — new tuning crate, retuned lifecycle constants

- 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>
This commit is contained in:
2026-05-07 06:32:17 -06:00
co-authored by Claude Opus 4.7
parent 7effaa293b
commit c45fceaf7a
15 changed files with 1123 additions and 322 deletions
+1 -2
View File
@@ -14,8 +14,7 @@ use crate::config::{
};
use crate::kbd::{KbdHid, send_kbd, wake_with_keyboard};
use crate::led::{
Neo, blink_fast_red, boot_sweep, breathe_color, fade_to_green, paint, pulse_blue,
pulse_white,
Neo, blink_fast_red, boot_sweep, breathe_color, fade_to_green, paint, pulse_blue, pulse_white,
};
use crate::mouse::{
MouseHid, animate_final_spiral, animate_spinner, animate_warning_5, animate_warning_10,
+13 -15
View File
@@ -1,20 +1,19 @@
//! Compile-time tuning constants — timing, geometry, brightness.
//!
//! Every magic number lives here so the rest of the firmware reads as
//! pure behaviour. Several values are joint-tuned by `scripts/tune_runtime.py`
//! — see the README's "Why four hours…" section for the rationale.
//! pure behaviour. Several values are joint-tuned by the `tuning/` crate
//! — see the README's "Why these timings…" section for the rationale.
use embassy_time::Duration as EDuration;
use hsmc::Duration;
// ── Lifecycle timing (statechart Durations) ────────────────────────
// 4h00m: optimum from the 4-D Monte Carlo (RUN_DURATION × YELLOW_AT ×
// RED_AT × FAST_RED_AT) over a typical office workday distribution
// with a per-minute press-on-warning user model. Lands the screen-sleep
// in the 12:1512:45 sweet spot on ~52 % of days and somewhere in
// lunch on ~74 %. See the README's "Why four hours…" section and
// `scripts/tune_runtime.py` for the simulation.
pub(crate) const RUN_DURATION: Duration = Duration::from_hours(4);
// 3h51m: a-posteriori pick from a 4-objective NSGA-III Pareto search
// over (RUN_DURATION, YELLOW_AT, RED_AT, FAST_RED_AT) — minimize work-
// time failures, maximize lunch sleep, minimize button presses, minimize
// after-hours waste. See the README's "Why these timings?" section and
// `tuning/` for the search.
pub(crate) const RUN_DURATION: Duration = Duration::from_mins(3 * 60 + 51);
pub(crate) const SHUTDOWN_LEAD: Duration = Duration::from_secs(30);
pub(crate) const RUN_BEFORE_SHUTDOWN: Duration = RUN_DURATION.saturating_sub(SHUTDOWN_LEAD);
pub(crate) const SHUTDOWN_ANIM_BUDGET: Duration = Duration::from_secs(5);
@@ -23,12 +22,11 @@ pub(crate) const JIGGLE_PERIOD: Duration = Duration::from_secs(270);
pub(crate) const FLASH_DURATION: Duration = Duration::from_millis(100);
// Phase boundaries — compared against time *remaining* in Active.
// Joint optimum from `scripts/tune_runtime.py`. Yellow and red are
// kept deliberately short (5 min each); the long phase is fast-red.
// See the README's "Why four hours…" section for the rationale.
pub(crate) const YELLOW_AT: EDuration = EDuration::from_secs(30 * 60);
pub(crate) const RED_AT: EDuration = EDuration::from_secs(25 * 60);
pub(crate) const FAST_RED_AT: EDuration = EDuration::from_secs(20 * 60);
// Joint pick from the NSGA-III Pareto search; see the README's
// "Why these timings…" section for the rationale.
pub(crate) const YELLOW_AT: EDuration = EDuration::from_secs(22 * 60);
pub(crate) const RED_AT: EDuration = EDuration::from_secs(11 * 60);
pub(crate) const FAST_RED_AT: EDuration = EDuration::from_secs(4 * 60);
// LED breathing math
pub(crate) const LED_TICK: EDuration = EDuration::from_millis(20);
+14 -4
View File
@@ -15,8 +15,8 @@ use smart_leds::RGB8;
use crate::chart::Ev;
use crate::config::{
BOOT_SWEEP_STEP, BREATHE_FLOOR, BREATHE_PEAK, FAST_RED_AT, FAST_RED_PERIOD, LED_TICK,
RED_AT, RED_PERIOD, RUN_BEFORE_SHUTDOWN, SETTLING_PULSE_PERIOD, SLOW_GREEN_PERIOD,
BOOT_SWEEP_STEP, BREATHE_FLOOR, BREATHE_PEAK, FAST_RED_AT, FAST_RED_PERIOD, LED_TICK, RED_AT,
RED_PERIOD, RUN_BEFORE_SHUTDOWN, SETTLING_PULSE_PERIOD, SLOW_GREEN_PERIOD,
SPINNER_FADE_DURATION, WAKING_PULSE_PERIOD, YELLOW_AT, YELLOW_PERIOD,
};
@@ -68,7 +68,12 @@ pub(crate) async fn blink_fast_red(neo: &mut Neo) -> Ev {
pub(crate) async fn pulse_blue(neo: &mut Neo) -> Ev {
let start = Instant::now();
loop {
let level = sin_breath(WAKING_PULSE_PERIOD, start.elapsed(), BREATHE_FLOOR, BREATHE_PEAK);
let level = sin_breath(
WAKING_PULSE_PERIOD,
start.elapsed(),
BREATHE_FLOOR,
BREATHE_PEAK,
);
paint(neo, 0, 0, level).await;
Timer::after(LED_TICK).await;
}
@@ -77,7 +82,12 @@ pub(crate) async fn pulse_blue(neo: &mut Neo) -> Ev {
pub(crate) async fn pulse_white(neo: &mut Neo) -> Ev {
let start = Instant::now();
loop {
let level = sin_breath(SETTLING_PULSE_PERIOD, start.elapsed(), BREATHE_FLOOR, BREATHE_PEAK);
let level = sin_breath(
SETTLING_PULSE_PERIOD,
start.elapsed(),
BREATHE_FLOOR,
BREATHE_PEAK,
);
paint(neo, level, level, level).await;
Timer::after(LED_TICK).await;
}
+1 -1
View File
@@ -87,7 +87,7 @@ async fn main(spawner: Spawner) {
#[cfg(feature = "defmt")]
defmt::info!("usb serial: {}", serial);
config.serial_number = Some(serial);
config.device_release = 0x0200; // matches firmware version 0.2.0
config.device_release = 0x0300; // matches firmware version 0.3.0
config.max_power = 100;
config.max_packet_size_0 = 64;