feat: composite HID mouse+keyboard with WakingHost statechart
Refactor the boot wake sequence into a WakingHost parent state with
WakingWithKeyboard (default, taps Left Shift 4×) timing out after 500 ms
into WakingWithMouse (existing shake). macOS does not reliably wake from
raw HID mouse motion; a keyboard event does — Shift is a no-op modifier
so it has no character side effect. Parent catches WakeDone (emitted by
the mouse path) and exits to Settling.
USB identity: 046d:c07d "G502 Mouse" → 046d:c52b "USB Receiver"
(Logitech Unifying Receiver — a real composite mouse+keyboard device).
Implementation uses two HidWriters on one embassy_usb::Builder; standard
USB composite, no hub simulation.
Naming cleanup throughout the statechart:
- BootSweep → Booting; SweepDone → BootDone; led_rgb_sweep → boot_sweep
- WakingDisplay → WakingHost{WakingWithKeyboard, WakingWithMouse}
- ShowingRunning → Spinning; RunDone → SpinDone; animate_running → animate_spinner
- Ending::ShutdownAnim → Ending::Spiraling; ShutdownAnimDone → SpiralDone
- animate_shutdown → animate_final_spiral; SHUTDOWN_* → FINAL_SPIRAL_*
- Ctx.writer/Writer → Ctx.mouse/MouseHid (+ new Ctx.kbd/KbdHid)
- send → send_mouse (+ new send_kbd)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+36
-7
@@ -9,11 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- **Composite USB device — mouse + keyboard HID** under one VID/PID. A new
|
||||||
|
`WakingHost` parent state wraps two substates: `WakingWithKeyboard`
|
||||||
|
(`default`) taps Left Shift 4× then idles, and parent's
|
||||||
|
`KBD_WAKE_DURATION` timeout drives the transition to `WakingWithMouse`
|
||||||
|
which runs the existing shake animation. Reason: macOS does not reliably
|
||||||
|
wake from raw HID mouse motion alone; a keyboard event does. Shift is a
|
||||||
|
modifier with no character side effect, so it's safe even with focus on a
|
||||||
|
text field at the moment of wake. 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
|
- `hsmc` 0.5.1 statechart drives the entire device lifecycle. The control
|
||||||
flow (boot → wake-up animation → settle → running animation → active
|
flow (boot → host wake (kbd → mouse) → settle → spinner → active
|
||||||
jiggle/flash → end-of-day shutdown spiral → power-down) is expressed
|
jiggle/flash → end-of-day spiral → power-down) is expressed declaratively
|
||||||
declaratively in one `statechart!` block; no more atomic flags, no more
|
in one `statechart!` block; no more atomic flags, no more per-tick
|
||||||
per-tick `Phase` enum, no hand-rolled main loop.
|
`Phase` enum, no hand-rolled main loop.
|
||||||
- Mouse animations driven by the chart's `during:` activities:
|
- Mouse animations driven by the chart's `during:` activities:
|
||||||
- **Wake-up** (`WakingDisplay`): 10 frantic horizontal sin-shaped sweeps
|
- **Wake-up** (`WakingDisplay`): 10 frantic horizontal sin-shaped sweeps
|
||||||
(~12 Hz, ±60 px, ~640 ms) — simulates "shake the mouse to wake the
|
(~12 Hz, ±60 px, ~640 ms) — simulates "shake the mouse to wake the
|
||||||
@@ -38,6 +48,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- **USB descriptor: Logitech G502 (`046d:c07d`, mouse-only) → Logitech
|
||||||
|
Unifying Receiver (`046d:c52b`, real composite mouse+keyboard).** Product
|
||||||
|
string `"G502 Mouse"` → `"USB Receiver"`. Bumping the PID also invalidates
|
||||||
|
the host's cached HID descriptor on first plug after reflash.
|
||||||
|
- **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` from 8 hours to 3 h 50 m**. Math-optimal under the
|
- **`RUN_DURATION` from 8 hours to 3 h 50 m**. Math-optimal under the
|
||||||
user's workday distribution (start `triangular(8.0, mode=8.5, 9.5)`,
|
user's 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)`) for "screen
|
lunch 12:00–13:00, end `triangular(16.0, mode=17.5, 19.0)`) for "screen
|
||||||
@@ -46,12 +75,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- HID `poll_ms` 60 → 8 (125 Hz). At the previous 60 ms poll the host
|
- 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.
|
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
|
- 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.
|
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
|
- Watchdog feeding moved out of the imperative main loop into its own
|
||||||
`#[embassy_executor::task]`; the chart owns user-visible state, the
|
`#[embassy_executor::task]`; the chart owns user-visible state, the
|
||||||
watchdog task owns the periodic kick.
|
watchdog task owns the periodic kick.
|
||||||
- USB descriptor unchanged (still spoofs Logitech G502 VID `0x046d` /
|
|
||||||
PID `0xc07d`).
|
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
|
|||||||
+135
-58
@@ -26,7 +26,7 @@ use libm::{cosf, powf, roundf, sinf};
|
|||||||
use panic_reset as _;
|
use panic_reset as _;
|
||||||
use smart_leds::RGB8;
|
use smart_leds::RGB8;
|
||||||
use static_cell::StaticCell;
|
use static_cell::StaticCell;
|
||||||
use usbd_hid::descriptor::{MouseReport, SerializedDescriptor};
|
use usbd_hid::descriptor::{KeyboardReport, MouseReport, SerializedDescriptor};
|
||||||
|
|
||||||
bind_interrupts!(struct Irqs {
|
bind_interrupts!(struct Irqs {
|
||||||
USBCTRL_IRQ => UsbInterruptHandler<USB>;
|
USBCTRL_IRQ => UsbInterruptHandler<USB>;
|
||||||
@@ -36,7 +36,8 @@ bind_interrupts!(struct Irqs {
|
|||||||
|
|
||||||
type UsbDriver = Driver<'static, USB>;
|
type UsbDriver = Driver<'static, USB>;
|
||||||
type Neo = PioWs2812<'static, PIO0, 0, 1, Grb>;
|
type Neo = PioWs2812<'static, PIO0, 0, 1, Grb>;
|
||||||
type Writer = HidWriter<'static, UsbDriver, 5>;
|
type MouseHid = HidWriter<'static, UsbDriver, 5>;
|
||||||
|
type KbdHid = HidWriter<'static, UsbDriver, 8>;
|
||||||
|
|
||||||
// ── Lifecycle timing (statechart Durations) ────────────────────────
|
// ── Lifecycle timing (statechart Durations) ────────────────────────
|
||||||
// 3h50m: math-optimal under the user's schedule for "screen sleeps during
|
// 3h50m: math-optimal under the user's schedule for "screen sleeps during
|
||||||
@@ -83,10 +84,24 @@ const WAKE_AMPLITUDE: f32 = 60.0;
|
|||||||
const WAKE_JITTER: f32 = 1.0;
|
const WAKE_JITTER: f32 = 1.0;
|
||||||
|
|
||||||
// 2 s pause after the shake so the display has time to actually wake before
|
// 2 s pause after the shake so the display has time to actually wake before
|
||||||
// we draw the "running" circles. The user wants this delay to live *here*,
|
// we draw the spinner. The user wants this delay to live *here*, not before
|
||||||
// not before the shake.
|
// the shake.
|
||||||
const SETTLING_DELAY: Duration = Duration::from_secs(2);
|
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 a keyboard event. Tap Left Shift four times before the mouse shake;
|
||||||
|
// Shift is a modifier, so it produces no visible character even if focus is
|
||||||
|
// on a text field at the moment of wake.
|
||||||
|
const KBD_WAKE_TAPS: u32 = 4;
|
||||||
|
const KBD_TAP_HOLD: EDuration = EDuration::from_millis(30);
|
||||||
|
const KBD_TAP_GAP: EDuration = EDuration::from_millis(50);
|
||||||
|
// Total parent dwell: 4 × (30 + 50) = 320 ms of taps + ~180 ms slack so the
|
||||||
|
// host has a chance to start coming up before the mouse shake begins.
|
||||||
|
const KBD_WAKE_DURATION: Duration = Duration::from_millis(500);
|
||||||
|
// USB HID Boot Keyboard modifier byte: bit 1 = Left Shift.
|
||||||
|
const KBD_MOD_LEFT_SHIFT: u8 = 0x02;
|
||||||
|
|
||||||
// Three quick clockwise circles read more clearly as "spinner / running"
|
// Three quick clockwise circles read more clearly as "spinner / running"
|
||||||
// than one slow lap. 25 frames per circle × 3 × 8 ms = 600 ms total.
|
// than one slow lap. 25 frames per circle × 3 × 8 ms = 600 ms total.
|
||||||
const RUN_RADIUS: f32 = 40.0;
|
const RUN_RADIUS: f32 = 40.0;
|
||||||
@@ -100,10 +115,10 @@ const RUN_CIRCLES: u32 = 3;
|
|||||||
const EASE_POW: f32 = 2.5;
|
const EASE_POW: f32 = 2.5;
|
||||||
const SPIRAL_RADIUS_END: f32 = 2.0;
|
const SPIRAL_RADIUS_END: f32 = 2.0;
|
||||||
|
|
||||||
// Final shutdown — the dramatic full version, 30 s before USB goes silent.
|
// Final spiral — the dramatic full version, 30 s before USB goes silent.
|
||||||
const SHUTDOWN_RADIUS_START: f32 = 80.0;
|
const FINAL_SPIRAL_RADIUS_START: f32 = 80.0;
|
||||||
const SHUTDOWN_TURNS: f32 = 5.0;
|
const FINAL_SPIRAL_TURNS: f32 = 5.0;
|
||||||
const SHUTDOWN_FRAMES: u32 = 625; // 5.0 s @ 8 ms/frame
|
const FINAL_SPIRAL_FRAMES: u32 = 625; // 5.0 s @ 8 ms/frame
|
||||||
|
|
||||||
// 5-min warning — medium escalation.
|
// 5-min warning — medium escalation.
|
||||||
const WARN5_RADIUS_START: f32 = 50.0;
|
const WARN5_RADIUS_START: f32 = 50.0;
|
||||||
@@ -137,7 +152,8 @@ const BOOT_SWEEP_STEP: EDuration = EDuration::from_millis(60);
|
|||||||
const SHUTDOWN_FLASH_STEP: EDuration = EDuration::from_millis(400);
|
const SHUTDOWN_FLASH_STEP: EDuration = EDuration::from_millis(400);
|
||||||
|
|
||||||
pub struct Ctx {
|
pub struct Ctx {
|
||||||
pub writer: Writer,
|
pub mouse: MouseHid,
|
||||||
|
pub kbd: KbdHid,
|
||||||
pub neo: Neo,
|
pub neo: Neo,
|
||||||
pub neo_pwr: Output<'static>,
|
pub neo_pwr: Output<'static>,
|
||||||
pub active_start: Option<Instant>,
|
pub active_start: Option<Instant>,
|
||||||
@@ -145,39 +161,52 @@ pub struct Ctx {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Ev {
|
pub enum Ev {
|
||||||
SweepDone,
|
BootDone,
|
||||||
WakeDone,
|
WakeDone,
|
||||||
RunDone,
|
SpinDone,
|
||||||
Jiggled,
|
Jiggled,
|
||||||
WarnDone,
|
WarnDone,
|
||||||
ShutdownAnimDone,
|
SpiralDone,
|
||||||
}
|
}
|
||||||
|
|
||||||
statechart! {
|
statechart! {
|
||||||
Jiggly {
|
Jiggly {
|
||||||
context: Ctx;
|
context: Ctx;
|
||||||
events: Ev;
|
events: Ev;
|
||||||
default(BootSweep);
|
default(Booting);
|
||||||
|
|
||||||
state BootSweep {
|
state Booting {
|
||||||
during: led_rgb_sweep(neo, neo_pwr);
|
during: boot_sweep(neo, neo_pwr);
|
||||||
on(SweepDone) => WakingDisplay;
|
on(BootDone) => WakingHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
state WakingDisplay {
|
// Wake the host using whichever input the host actually responds to.
|
||||||
during: animate_wake(writer);
|
// Keyboard first (more reliable on macOS), then a mouse shake as
|
||||||
|
// belt-and-suspenders. The parent state catches WakeDone (emitted by the
|
||||||
|
// mouse path) and exits to Settling.
|
||||||
|
state WakingHost {
|
||||||
|
default(WakingWithKeyboard);
|
||||||
on(WakeDone) => Settling;
|
on(WakeDone) => Settling;
|
||||||
|
|
||||||
|
state WakingWithKeyboard {
|
||||||
|
during: wake_with_keyboard(kbd);
|
||||||
|
on(after KBD_WAKE_DURATION) => WakingWithMouse;
|
||||||
|
}
|
||||||
|
|
||||||
|
state WakingWithMouse {
|
||||||
|
during: wake_with_mouse(mouse);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quiet pause so the display has time to come out of sleep before the
|
// Quiet pause so the display has time to come out of sleep before the
|
||||||
// cursor starts drawing the "running" circles.
|
// cursor starts drawing the spinner.
|
||||||
state Settling {
|
state Settling {
|
||||||
on(after SETTLING_DELAY) => ShowingRunning;
|
on(after SETTLING_DELAY) => Spinning;
|
||||||
}
|
}
|
||||||
|
|
||||||
state ShowingRunning {
|
state Spinning {
|
||||||
during: animate_running(writer);
|
during: animate_spinner(mouse);
|
||||||
on(RunDone) => Active;
|
on(SpinDone) => Active;
|
||||||
}
|
}
|
||||||
|
|
||||||
state Active {
|
state Active {
|
||||||
@@ -199,23 +228,23 @@ Jiggly {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state Warning10 {
|
state Warning10 {
|
||||||
during: animate_warning_10(writer);
|
during: animate_warning_10(mouse);
|
||||||
on(WarnDone) => Breathing;
|
on(WarnDone) => Breathing;
|
||||||
}
|
}
|
||||||
|
|
||||||
state Warning5 {
|
state Warning5 {
|
||||||
during: animate_warning_5(writer);
|
during: animate_warning_5(mouse);
|
||||||
on(WarnDone) => Breathing;
|
on(WarnDone) => Breathing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state Ending {
|
state Ending {
|
||||||
during: blink_fast_red(neo);
|
during: blink_fast_red(neo);
|
||||||
default(ShutdownAnim);
|
default(Spiraling);
|
||||||
|
|
||||||
state ShutdownAnim {
|
state Spiraling {
|
||||||
during: animate_shutdown(writer);
|
during: animate_final_spiral(mouse);
|
||||||
on(ShutdownAnimDone) => Quiet;
|
on(SpiralDone) => Quiet;
|
||||||
}
|
}
|
||||||
|
|
||||||
state Quiet {
|
state Quiet {
|
||||||
@@ -241,9 +270,9 @@ impl JigglyActions for JigglyActionContext<'_> {
|
|||||||
} else {
|
} else {
|
||||||
(0, 1)
|
(0, 1)
|
||||||
};
|
};
|
||||||
send(&mut self.writer, dx, dy).await;
|
send_mouse(&mut self.mouse, dx, dy).await;
|
||||||
Timer::after(PIXEL_DWELL).await;
|
Timer::after(PIXEL_DWELL).await;
|
||||||
send(&mut self.writer, -dx, -dy).await;
|
send_mouse(&mut self.mouse, -dx, -dy).await;
|
||||||
let _ = self.emit(Ev::Jiggled);
|
let _ = self.emit(Ev::Jiggled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +296,7 @@ impl JigglyActions for JigglyActionContext<'_> {
|
|||||||
|
|
||||||
// ── During activities (free async fns) ─────────────────────────────
|
// ── During activities (free async fns) ─────────────────────────────
|
||||||
|
|
||||||
async fn led_rgb_sweep(neo: &mut Neo, neo_pwr: &mut Output<'static>) -> Ev {
|
async fn boot_sweep(neo: &mut Neo, neo_pwr: &mut Output<'static>) -> Ev {
|
||||||
neo_pwr.set_high();
|
neo_pwr.set_high();
|
||||||
Timer::after_millis(2).await;
|
Timer::after_millis(2).await;
|
||||||
|
|
||||||
@@ -278,10 +307,28 @@ async fn led_rgb_sweep(neo: &mut Neo, neo_pwr: &mut Output<'static>) -> Ev {
|
|||||||
paint(neo, 0, 0, BREATHE_PEAK).await;
|
paint(neo, 0, 0, BREATHE_PEAK).await;
|
||||||
Timer::after(BOOT_SWEEP_STEP).await;
|
Timer::after(BOOT_SWEEP_STEP).await;
|
||||||
paint(neo, 0, 0, 0).await;
|
paint(neo, 0, 0, 0).await;
|
||||||
Ev::SweepDone
|
Ev::BootDone
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn animate_wake(writer: &mut Writer) -> Ev {
|
// Tap Left Shift 4 times — macOS reliably wakes from a keyboard event but is
|
||||||
|
// inconsistent about waking from raw mouse motion. The during loop ends after
|
||||||
|
// the taps and idles in a long sleep; the parent state's KBD_WAKE_DURATION
|
||||||
|
// timeout drives the transition out, not a completion event from this fn.
|
||||||
|
async fn wake_with_keyboard(kbd: &mut KbdHid) -> Ev {
|
||||||
|
for _ in 0..KBD_WAKE_TAPS {
|
||||||
|
send_kbd(kbd, KBD_MOD_LEFT_SHIFT, [0; 6]).await;
|
||||||
|
Timer::after(KBD_TAP_HOLD).await;
|
||||||
|
send_kbd(kbd, 0, [0; 6]).await;
|
||||||
|
Timer::after(KBD_TAP_GAP).await;
|
||||||
|
}
|
||||||
|
// Idle until the parent timeout fires. The hsmc runtime cancels this
|
||||||
|
// future on transition, so the long sleep just parks us.
|
||||||
|
loop {
|
||||||
|
Timer::after(EDuration::from_secs(60)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wake_with_mouse(mouse: &mut MouseHid) -> Ev {
|
||||||
let period_frames = (WAKE_FRAMES_PER_HALF * 2) as f32;
|
let period_frames = (WAKE_FRAMES_PER_HALF * 2) as f32;
|
||||||
let total_frames = WAKE_OSCILLATIONS * WAKE_FRAMES_PER_HALF * 2;
|
let total_frames = WAKE_OSCILLATIONS * WAKE_FRAMES_PER_HALF * 2;
|
||||||
let mut prev_x: f32 = 0.0;
|
let mut prev_x: f32 = 0.0;
|
||||||
@@ -296,7 +343,7 @@ async fn animate_wake(writer: &mut Writer) -> Ev {
|
|||||||
let (dx, dy, used_x, used_y) = step_delta(prev_x, prev_y, next_x, next_y, acc_x, acc_y);
|
let (dx, dy, used_x, used_y) = step_delta(prev_x, prev_y, next_x, next_y, acc_x, acc_y);
|
||||||
acc_x = used_x;
|
acc_x = used_x;
|
||||||
acc_y = used_y;
|
acc_y = used_y;
|
||||||
send(writer, dx, dy).await;
|
send_mouse(mouse, dx, dy).await;
|
||||||
prev_x = next_x;
|
prev_x = next_x;
|
||||||
prev_y = next_y;
|
prev_y = next_y;
|
||||||
Timer::after(ANIM_FRAME).await;
|
Timer::after(ANIM_FRAME).await;
|
||||||
@@ -304,7 +351,7 @@ async fn animate_wake(writer: &mut Writer) -> Ev {
|
|||||||
Ev::WakeDone
|
Ev::WakeDone
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn animate_running(writer: &mut Writer) -> Ev {
|
async fn animate_spinner(mouse: &mut MouseHid) -> Ev {
|
||||||
let total_frames = RUN_CIRCLES * RUN_FRAMES_PER_CIRCLE;
|
let total_frames = RUN_CIRCLES * RUN_FRAMES_PER_CIRCLE;
|
||||||
let period_frames = RUN_FRAMES_PER_CIRCLE as f32;
|
let period_frames = RUN_FRAMES_PER_CIRCLE as f32;
|
||||||
let mut prev_x: f32 = 0.0;
|
let mut prev_x: f32 = 0.0;
|
||||||
@@ -318,16 +365,16 @@ async fn animate_running(writer: &mut Writer) -> Ev {
|
|||||||
let (dx, dy, used_x, used_y) = step_delta(prev_x, prev_y, next_x, next_y, acc_x, acc_y);
|
let (dx, dy, used_x, used_y) = step_delta(prev_x, prev_y, next_x, next_y, acc_x, acc_y);
|
||||||
acc_x = used_x;
|
acc_x = used_x;
|
||||||
acc_y = used_y;
|
acc_y = used_y;
|
||||||
send(writer, dx, dy).await;
|
send_mouse(mouse, dx, dy).await;
|
||||||
prev_x = next_x;
|
prev_x = next_x;
|
||||||
prev_y = next_y;
|
prev_y = next_y;
|
||||||
Timer::after(ANIM_FRAME).await;
|
Timer::after(ANIM_FRAME).await;
|
||||||
}
|
}
|
||||||
Ev::RunDone
|
Ev::SpinDone
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn animate_spiral(
|
async fn animate_spiral(
|
||||||
writer: &mut Writer,
|
mouse: &mut MouseHid,
|
||||||
radius_start: f32,
|
radius_start: f32,
|
||||||
radius_end: f32,
|
radius_end: f32,
|
||||||
turns: f32,
|
turns: f32,
|
||||||
@@ -348,28 +395,28 @@ async fn animate_spiral(
|
|||||||
let (dx, dy, used_x, used_y) = step_delta(prev_x, prev_y, next_x, next_y, acc_x, acc_y);
|
let (dx, dy, used_x, used_y) = step_delta(prev_x, prev_y, next_x, next_y, acc_x, acc_y);
|
||||||
acc_x = used_x;
|
acc_x = used_x;
|
||||||
acc_y = used_y;
|
acc_y = used_y;
|
||||||
send(writer, dx, dy).await;
|
send_mouse(mouse, dx, dy).await;
|
||||||
prev_x = next_x;
|
prev_x = next_x;
|
||||||
prev_y = next_y;
|
prev_y = next_y;
|
||||||
Timer::after(ANIM_FRAME).await;
|
Timer::after(ANIM_FRAME).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn animate_shutdown(writer: &mut Writer) -> Ev {
|
async fn animate_final_spiral(mouse: &mut MouseHid) -> Ev {
|
||||||
animate_spiral(
|
animate_spiral(
|
||||||
writer,
|
mouse,
|
||||||
SHUTDOWN_RADIUS_START,
|
FINAL_SPIRAL_RADIUS_START,
|
||||||
SPIRAL_RADIUS_END,
|
SPIRAL_RADIUS_END,
|
||||||
SHUTDOWN_TURNS,
|
FINAL_SPIRAL_TURNS,
|
||||||
SHUTDOWN_FRAMES,
|
FINAL_SPIRAL_FRAMES,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
Ev::ShutdownAnimDone
|
Ev::SpiralDone
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn animate_warning_5(writer: &mut Writer) -> Ev {
|
async fn animate_warning_5(mouse: &mut MouseHid) -> Ev {
|
||||||
animate_spiral(
|
animate_spiral(
|
||||||
writer,
|
mouse,
|
||||||
WARN5_RADIUS_START,
|
WARN5_RADIUS_START,
|
||||||
SPIRAL_RADIUS_END,
|
SPIRAL_RADIUS_END,
|
||||||
WARN5_TURNS,
|
WARN5_TURNS,
|
||||||
@@ -379,9 +426,9 @@ async fn animate_warning_5(writer: &mut Writer) -> Ev {
|
|||||||
Ev::WarnDone
|
Ev::WarnDone
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn animate_warning_10(writer: &mut Writer) -> Ev {
|
async fn animate_warning_10(mouse: &mut MouseHid) -> Ev {
|
||||||
animate_spiral(
|
animate_spiral(
|
||||||
writer,
|
mouse,
|
||||||
WARN10_RADIUS_START,
|
WARN10_RADIUS_START,
|
||||||
SPIRAL_RADIUS_END,
|
SPIRAL_RADIUS_END,
|
||||||
WARN10_TURNS,
|
WARN10_TURNS,
|
||||||
@@ -481,7 +528,7 @@ fn step_delta(
|
|||||||
|
|
||||||
// ── HID + LED primitives ───────────────────────────────────────────
|
// ── HID + LED primitives ───────────────────────────────────────────
|
||||||
|
|
||||||
async fn send(writer: &mut Writer, x: i8, y: i8) {
|
async fn send_mouse(mouse: &mut MouseHid, x: i8, y: i8) {
|
||||||
let report = MouseReport {
|
let report = MouseReport {
|
||||||
buttons: 0,
|
buttons: 0,
|
||||||
x,
|
x,
|
||||||
@@ -489,7 +536,17 @@ async fn send(writer: &mut Writer, x: i8, y: i8) {
|
|||||||
wheel: 0,
|
wheel: 0,
|
||||||
pan: 0,
|
pan: 0,
|
||||||
};
|
};
|
||||||
let _ = with_timeout(EDuration::from_secs(3), writer.write_serialize(&report)).await;
|
let _ = with_timeout(EDuration::from_secs(3), mouse.write_serialize(&report)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_kbd(kbd: &mut KbdHid, modifier: u8, keycodes: [u8; 6]) {
|
||||||
|
let report = KeyboardReport {
|
||||||
|
modifier,
|
||||||
|
reserved: 0,
|
||||||
|
leds: 0,
|
||||||
|
keycodes,
|
||||||
|
};
|
||||||
|
let _ = with_timeout(EDuration::from_secs(3), kbd.write_serialize(&report)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn paint(neo: &mut Neo, r: u8, g: u8, b: u8) {
|
async fn paint(neo: &mut Neo, r: u8, g: u8, b: u8) {
|
||||||
@@ -537,9 +594,12 @@ async fn main(spawner: Spawner) {
|
|||||||
|
|
||||||
let driver = Driver::new(p.USB, Irqs);
|
let driver = Driver::new(p.USB, Irqs);
|
||||||
|
|
||||||
let mut config = UsbConfig::new(0x046d, 0xc07d);
|
// Spoof a Logitech Unifying Receiver — a real-world composite device that
|
||||||
|
// exposes both mouse and keyboard HID interfaces under one VID/PID, which
|
||||||
|
// matches what this firmware now does.
|
||||||
|
let mut config = UsbConfig::new(0x046d, 0xc52b);
|
||||||
config.manufacturer = Some("Logitech");
|
config.manufacturer = Some("Logitech");
|
||||||
config.product = Some("G502 Mouse");
|
config.product = Some("USB Receiver");
|
||||||
config.max_power = 100;
|
config.max_power = 100;
|
||||||
config.max_packet_size_0 = 64;
|
config.max_packet_size_0 = 64;
|
||||||
|
|
||||||
@@ -547,7 +607,8 @@ async fn main(spawner: Spawner) {
|
|||||||
static BOS_DESCRIPTOR: StaticCell<[u8; 256]> = StaticCell::new();
|
static BOS_DESCRIPTOR: StaticCell<[u8; 256]> = StaticCell::new();
|
||||||
static MSOS_DESCRIPTOR: StaticCell<[u8; 256]> = StaticCell::new();
|
static MSOS_DESCRIPTOR: StaticCell<[u8; 256]> = StaticCell::new();
|
||||||
static CONTROL_BUF: StaticCell<[u8; 64]> = StaticCell::new();
|
static CONTROL_BUF: StaticCell<[u8; 64]> = StaticCell::new();
|
||||||
static HID_STATE: StaticCell<State> = StaticCell::new();
|
static MOUSE_HID_STATE: StaticCell<State> = StaticCell::new();
|
||||||
|
static KBD_HID_STATE: StaticCell<State> = StaticCell::new();
|
||||||
|
|
||||||
let mut builder = Builder::new(
|
let mut builder = Builder::new(
|
||||||
driver,
|
driver,
|
||||||
@@ -558,7 +619,7 @@ async fn main(spawner: Spawner) {
|
|||||||
CONTROL_BUF.init([0; 64]),
|
CONTROL_BUF.init([0; 64]),
|
||||||
);
|
);
|
||||||
|
|
||||||
let hid_config = HidConfig {
|
let mouse_config = HidConfig {
|
||||||
report_descriptor: MouseReport::desc(),
|
report_descriptor: MouseReport::desc(),
|
||||||
request_handler: None,
|
request_handler: None,
|
||||||
// 8 ms (125 Hz) — standard for full-speed mice. At the previous 60 ms
|
// 8 ms (125 Hz) — standard for full-speed mice. At the previous 60 ms
|
||||||
@@ -568,7 +629,22 @@ async fn main(spawner: Spawner) {
|
|||||||
hid_subclass: HidSubclass::Boot,
|
hid_subclass: HidSubclass::Boot,
|
||||||
hid_boot_protocol: HidBootProtocol::Mouse,
|
hid_boot_protocol: HidBootProtocol::Mouse,
|
||||||
};
|
};
|
||||||
let writer = HidWriter::<_, 5>::new(&mut builder, HID_STATE.init(State::new()), hid_config);
|
let mouse = HidWriter::<_, 5>::new(
|
||||||
|
&mut builder,
|
||||||
|
MOUSE_HID_STATE.init(State::new()),
|
||||||
|
mouse_config,
|
||||||
|
);
|
||||||
|
|
||||||
|
let kbd_config = HidConfig {
|
||||||
|
report_descriptor: KeyboardReport::desc(),
|
||||||
|
request_handler: None,
|
||||||
|
// The keyboard only fires once at boot; no need for fast polling.
|
||||||
|
poll_ms: 10,
|
||||||
|
max_packet_size: 8,
|
||||||
|
hid_subclass: HidSubclass::Boot,
|
||||||
|
hid_boot_protocol: HidBootProtocol::Keyboard,
|
||||||
|
};
|
||||||
|
let kbd = HidWriter::<_, 8>::new(&mut builder, KBD_HID_STATE.init(State::new()), kbd_config);
|
||||||
|
|
||||||
let usb = builder.build();
|
let usb = builder.build();
|
||||||
spawner.spawn(usb_task(usb).unwrap());
|
spawner.spawn(usb_task(usb).unwrap());
|
||||||
@@ -577,7 +653,8 @@ async fn main(spawner: Spawner) {
|
|||||||
static EVENT_CHAN: Channel<CriticalSectionRawMutex, Ev, 8> = Channel::new();
|
static EVENT_CHAN: Channel<CriticalSectionRawMutex, Ev, 8> = Channel::new();
|
||||||
|
|
||||||
let ctx = Ctx {
|
let ctx = Ctx {
|
||||||
writer,
|
mouse,
|
||||||
|
kbd,
|
||||||
neo,
|
neo,
|
||||||
neo_pwr,
|
neo_pwr,
|
||||||
active_start: None,
|
active_start: None,
|
||||||
|
|||||||
Reference in New Issue
Block a user