Files
jiggly/src/config.rs
T
swaits 7effaa293b refactor: split 832-line main.rs into focused modules
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.
2026-05-04 18:56:27 -06:00

153 lines
9.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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: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);
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.
// F13F24 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);