config (constants), usb (driver + tasks), led (NeoPixel anims), mouse (cursor anims + HID), kbd (F13 wake + HID), chart (statechart + Ctx/Ev + JigglyActions). main.rs is now just Irqs + USB builder + chart.run(). No behavior change — comments, deadlines, and tunables preserved verbatim. Builds clean on default and --features embassy,defmt; clippy -D warnings passes.
153 lines
9.0 KiB
Rust
153 lines
9.0 KiB
Rust
//! 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.
|
||
|
||
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:15–12: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);
|
||
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);
|
||
pub(crate) const QUIET_AFTER_ANIM: Duration = SHUTDOWN_LEAD.saturating_sub(SHUTDOWN_ANIM_BUDGET);
|
||
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);
|
||
|
||
// LED breathing math
|
||
pub(crate) const LED_TICK: EDuration = EDuration::from_millis(20);
|
||
pub(crate) const SLOW_GREEN_PERIOD: EDuration = EDuration::from_secs(4);
|
||
pub(crate) const YELLOW_PERIOD: EDuration = EDuration::from_secs(3);
|
||
pub(crate) const RED_PERIOD: EDuration = EDuration::from_secs(2);
|
||
pub(crate) const FAST_RED_PERIOD: EDuration = EDuration::from_millis(500);
|
||
|
||
// Wake-up LED feedback periods. Blue on WakingHost (kbd+mouse), white on
|
||
// Settling, then a linear white→green fade across the Spinning duration so
|
||
// the LED hands off cleanly to Active's green breathing.
|
||
pub(crate) const WAKING_PULSE_PERIOD: EDuration = EDuration::from_millis(800);
|
||
pub(crate) const SETTLING_PULSE_PERIOD: EDuration = EDuration::from_millis(1200);
|
||
pub(crate) const SPINNER_FADE_DURATION: EDuration = EDuration::from_millis(600);
|
||
|
||
// LED brightness (raw WS2812 PWM, 0..=255).
|
||
pub(crate) const BREATHE_FLOOR: u8 = 1;
|
||
pub(crate) const BREATHE_PEAK: u8 = 16;
|
||
pub(crate) const FLASH_PEAK: u8 = 160;
|
||
|
||
// ── Animation parameters ───────────────────────────────────────────
|
||
// 8ms frames + HID poll_ms=8 means each frame's report actually reaches the
|
||
// host instead of being coalesced — at 16ms frames against a 60ms poll the
|
||
// shake looked sluggish because three reports out of four were dropped on the
|
||
// floor.
|
||
pub(crate) const ANIM_FRAME: EDuration = EDuration::from_millis(8);
|
||
|
||
// Frantic side-to-side, the gesture a person makes to wake a sleeping display.
|
||
// 10 full oscillations × 8 frames of full period = 80 frames × 8 ms = 640 ms,
|
||
// which works out to a ~12 Hz alternation — visibly "shaking", not "sweeping".
|
||
pub(crate) const WAKE_OSCILLATIONS: u32 = 10;
|
||
pub(crate) const WAKE_FRAMES_PER_HALF: u32 = 4;
|
||
pub(crate) const WAKE_AMPLITUDE: f32 = 60.0;
|
||
pub(crate) const WAKE_JITTER: f32 = 1.0;
|
||
|
||
// 2 s pause after the shake so the display has time to actually wake before
|
||
// we draw the spinner. The user wants this delay to live *here*, not before
|
||
// the shake.
|
||
pub(crate) const SETTLING_DELAY: Duration = Duration::from_secs(2);
|
||
|
||
// ── Keyboard wake (host-wake first pass) ───────────────────────────
|
||
// macOS often won't wake from raw HID mouse motion alone, but reliably wakes
|
||
// from any keyboard event. We tap **F13** four times before the mouse shake.
|
||
// F13–F24 are intentionally unmapped on every mainstream OS, so even in the
|
||
// nightmare scenario where the deadline preempts the loop *between* a key-
|
||
// down and key-up report and the host ends up holding F13 forever, nothing
|
||
// visible happens — unlike with Shift, which would silently capitalise every
|
||
// keystroke from the user's real keyboard until they unplug the device.
|
||
// Earlier versions used Left Shift; that turned out to be exactly that
|
||
// nightmare scenario in practice.
|
||
pub(crate) const KBD_WAKE_TAPS: u32 = 4;
|
||
pub(crate) const KBD_TAP_HOLD: EDuration = EDuration::from_millis(30);
|
||
pub(crate) const KBD_TAP_GAP: EDuration = EDuration::from_millis(50);
|
||
// Hard internal deadline on the keyboard-wake entry action. The taps total
|
||
// ~320 ms so they finish well before this; the deadline only kicks in if a
|
||
// USB write blocks (e.g. the host hasn't bound the keyboard endpoint yet).
|
||
pub(crate) const KBD_WAKE_DEADLINE: EDuration = EDuration::from_millis(500);
|
||
// Statechart timer for the WakingWithKeyboard state — chosen above the
|
||
// internal deadline so the chart timer is what drives the transition out.
|
||
pub(crate) const KBD_PHASE_DURATION: Duration = Duration::from_millis(550);
|
||
// HID Keyboard usage page keycode for F13.
|
||
pub(crate) const KBD_KEY_F13: u8 = 0x68;
|
||
// Final-cleanup deadline — the all-keys-released report we send after the
|
||
// main work loop is bounded by this so a misbehaving endpoint can't pin
|
||
// the chart. Best-effort; if it doesn't land we tried.
|
||
pub(crate) const KBD_RELEASE_DEADLINE: EDuration = EDuration::from_millis(100);
|
||
|
||
// ── Mouse wake (host-wake second pass) ─────────────────────────────
|
||
// Internal deadline on the mouse-shake entry action — the shake itself takes
|
||
// ~640 ms; the cap exists so a misbehaving USB endpoint can't pin the chart.
|
||
pub(crate) const MOUSE_WAKE_DEADLINE: EDuration = EDuration::from_millis(1000);
|
||
pub(crate) const MOUSE_PHASE_DURATION: Duration = Duration::from_millis(1050);
|
||
|
||
// Three quick clockwise circles read more clearly as "spinner / running"
|
||
// than one slow lap. 25 frames per circle × 3 × 8 ms = 600 ms total.
|
||
pub(crate) const RUN_RADIUS: f32 = 40.0;
|
||
pub(crate) const RUN_FRAMES_PER_CIRCLE: u32 = 25;
|
||
pub(crate) const RUN_CIRCLES: u32 = 3;
|
||
|
||
// Shared "spin-down" spiral. Both radius and angle are driven by an eased phase
|
||
// u(t) = t^EASE_POW with EASE_POW > 1 — slow at the start, ~EASE_POW× the
|
||
// average rate at the finish. Because radius and angle share u, the inward
|
||
// spiral and the rotation accelerate together: a coin/Euler-disk feel.
|
||
pub(crate) const EASE_POW: f32 = 2.5;
|
||
pub(crate) const SPIRAL_RADIUS_END: f32 = 2.0;
|
||
|
||
// Final spiral — the dramatic full version, 30 s before USB goes silent.
|
||
pub(crate) const FINAL_SPIRAL_RADIUS_START: f32 = 80.0;
|
||
pub(crate) const FINAL_SPIRAL_TURNS: f32 = 5.0;
|
||
pub(crate) const FINAL_SPIRAL_FRAMES: u32 = 625; // 5.0 s @ 8 ms/frame
|
||
|
||
// 5-min warning — medium escalation.
|
||
pub(crate) const WARN5_RADIUS_START: f32 = 50.0;
|
||
pub(crate) const WARN5_TURNS: f32 = 3.0;
|
||
pub(crate) const WARN5_FRAMES: u32 = 312; // ~2.5 s
|
||
|
||
// 10-min warning — small foreshadow.
|
||
pub(crate) const WARN10_RADIUS_START: f32 = 30.0;
|
||
pub(crate) const WARN10_TURNS: f32 = 2.0;
|
||
pub(crate) const WARN10_FRAMES: u32 = 187; // ~1.5 s
|
||
|
||
// Offsets from Active-entry. The hsmc parent timer rule: timers in a parent
|
||
// state start on parent entry and survive sibling-substate transitions, so
|
||
// these three `after`s race concurrently against the same epoch.
|
||
pub(crate) const WARN_10_AT: Duration = RUN_BEFORE_SHUTDOWN.saturating_sub(Duration::from_mins(10));
|
||
pub(crate) const WARN_5_AT: Duration = RUN_BEFORE_SHUTDOWN.saturating_sub(Duration::from_mins(5));
|
||
|
||
// ── Watchdog (independent task) ────────────────────────────────────
|
||
pub(crate) const WATCHDOG_TIMEOUT: EDuration = EDuration::from_secs(8);
|
||
pub(crate) const WATCHDOG_FEED_INTERVAL: EDuration = EDuration::from_secs(5);
|
||
|
||
// ── Jiggle dwell ───────────────────────────────────────────────────
|
||
pub(crate) const PIXEL_DWELL: EDuration = EDuration::from_millis(25);
|
||
|
||
// ── Boot LED sweep ─────────────────────────────────────────────────
|
||
// Brief power-on confirmation. Kept short so the cursor shake — the actual
|
||
// "wake the display" gesture — happens promptly after reset.
|
||
pub(crate) const BOOT_SWEEP_STEP: EDuration = EDuration::from_millis(60);
|
||
|
||
// ── Shutdown LED flashes ───────────────────────────────────────────
|
||
pub(crate) const SHUTDOWN_FLASH_STEP: EDuration = EDuration::from_millis(400);
|