diff --git a/README.md b/README.md index 7d300d1..2d39c63 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,12 @@ Config Layer (holding CONFIG button) - Activity colors: green (active), blue (virtual throttle/calibration), orange (holds) - Warning/error tones (red) and bootloader purple - Idle heartbeat flashes at half speed once inputs settle + - LED turns off during USB suspend for power savings +- Power management for USB suspend/resume + - Automatic power saving when USB host suspends device + - Reduced input scanning frequency (10x slower) during suspend + - Wake-on-input detection for gimbals and buttons + - Immediate resume response when inputs detected ## Low-latency firmware path diff --git a/rp2040/src/main.rs b/rp2040/src/main.rs index 10df3ee..b47b86d 100644 --- a/rp2040/src/main.rs +++ b/rp2040/src/main.rs @@ -63,6 +63,7 @@ use rp2040_hal::{ use status::{StatusLed, StatusMode, SystemState}; use usb_device::class_prelude::*; use usb_device::prelude::*; +use usb_device::device::UsbDeviceState; use usb_joystick_device::JoystickConfig; use usb_report::get_joystick_report; use usbd_human_interface_device::prelude::*; @@ -230,10 +231,12 @@ fn main() -> ! { let mut usb_activity: bool = false; let mut usb_active: bool = false; let mut usb_initialized: bool = false; + let mut usb_suspended: bool = false; let mut usb_send_pending: bool = false; let mut vt_enable: bool = false; let mut idle_mode: bool = false; let mut usb_activity_timeout_count: u32 = 0; + let mut wake_on_input: bool = false; let mut axis_manager = AxisManager::new(); let mut button_manager = ButtonManager::new(); @@ -298,7 +301,47 @@ fn main() -> ! { usb_active = true; } - if scan_count_down.wait().is_ok() { + // Check USB device state for suspend/resume handling + let usb_state = usb_dev.state(); + let was_suspended = usb_suspended; + usb_suspended = usb_state == UsbDeviceState::Suspend; + + // Handle USB resume transition + if was_suspended && !usb_suspended { + // Device was suspended and is now resumed + usb_activity = true; + idle_mode = false; + usb_activity_timeout_count = 0; + usb_send_pending = true; + wake_on_input = false; + } + + // Handle USB suspend transition + if !was_suspended && usb_suspended { + // Device has just been suspended - enter power saving mode + idle_mode = true; + usb_activity = false; + usb_send_pending = false; + wake_on_input = true; + + // Reduce LED update frequency to save power when suspended + // LED will be off anyway (Suspended mode), so slow updates are fine + } + + // Skip high-frequency scanning when suspended to save power + // Only scan periodically to detect wake-up inputs + let should_scan = if usb_suspended { + // When suspended, reduce scan frequency by factor of 10 (every ~2ms instead of 200μs) + static mut SUSPENDED_SCAN_COUNTER: u8 = 0; + unsafe { + SUSPENDED_SCAN_COUNTER = (SUSPENDED_SCAN_COUNTER + 1) % 10; + SUSPENDED_SCAN_COUNTER == 0 + } + } else { + true + }; + + if scan_count_down.wait().is_ok() && should_scan { // ## High-Frequency Input Sampling (~5 kHz) // // Sample all inputs at high frequency for responsive control: @@ -398,6 +441,12 @@ fn main() -> ! { usb_activity_timeout_count = 0; // Reset timeout on real input activity idle_mode = false; usb_send_pending = true; + + // Wake from USB suspend if input detected + if wake_on_input && usb_suspended { + // TODO: Implement remote wakeup if supported by host + wake_on_input = false; + } } // Update virtual axes based on front button states @@ -406,6 +455,12 @@ fn main() -> ! { usb_activity_timeout_count = 0; // Reset timeout on real input activity idle_mode = false; usb_send_pending = true; + + // Wake from USB suspend if input detected + if wake_on_input && usb_suspended { + // TODO: Implement remote wakeup if supported by host + wake_on_input = false; + } } // Process button logic (press types, timing, USB mapping) @@ -414,6 +469,12 @@ fn main() -> ! { usb_activity_timeout_count = 0; // Reset timeout on real input activity idle_mode = false; usb_send_pending = true; + + // Wake from USB suspend if input detected + if wake_on_input && usb_suspended { + // TODO: Implement remote wakeup if supported by host + wake_on_input = false; + } } } @@ -430,6 +491,7 @@ fn main() -> ! { let system_state = SystemState { usb_active, usb_initialized, + usb_suspended, idle_mode, calibration_active: calibration_manager.is_active(), throttle_hold_enable: axis_manager.throttle_hold_enable, @@ -452,9 +514,9 @@ fn main() -> ! { // - 8-direction HAT switch state // - Virtual throttle mode handling - // Only transmit USB reports when input activity is detected + // Only transmit USB reports when input activity is detected and not suspended let usb_tick = usb_update_count_down.wait().is_ok(); - if usb_activity && (usb_tick || usb_send_pending) { + if usb_activity && (usb_tick || usb_send_pending) && !usb_suspended { let mut send_report = || { let virtual_ry_value = axis_manager.get_virtual_ry_value(&expo_lut_virtual); let virtual_rz_value = axis_manager.get_virtual_rz_value(&expo_lut_virtual); @@ -489,7 +551,8 @@ fn main() -> ! { } else { send_report(); } - } else if usb_tick && usb_active { + } else if usb_tick && usb_active && !usb_suspended { + // Only update idle mode for non-suspended devices idle_mode = true; } } diff --git a/rp2040/src/status.rs b/rp2040/src/status.rs index d9e675d..0a20933 100644 --- a/rp2040/src/status.rs +++ b/rp2040/src/status.rs @@ -34,6 +34,7 @@ pub enum StatusMode { Error = 9, Bootloader = 10, Power = 11, + Suspended = 12, } /// Aggregate system state for LED status indication. @@ -41,6 +42,7 @@ pub enum StatusMode { pub struct SystemState { pub usb_active: bool, pub usb_initialized: bool, + pub usb_suspended: bool, pub idle_mode: bool, pub calibration_active: bool, pub throttle_hold_enable: bool, @@ -187,6 +189,10 @@ const fn descriptor_for(mode: StatusMode, base_mode: StatusMode) -> ModeDescript period_ms: HEARTBEAT_POWER_MS, }, }, + StatusMode::Suspended => ModeDescriptor { + color: COLOR_OFF, + effect: LedEffect::Solid, + }, } } @@ -200,7 +206,9 @@ fn scale_color(base: RGB8, brightness: u8) -> RGB8 { } fn determine_base_mode(system_state: SystemState) -> StatusMode { - if system_state.calibration_active { + if system_state.usb_suspended { + StatusMode::Suspended + } else if system_state.calibration_active { StatusMode::ActivityFlash } else if !system_state.usb_initialized { StatusMode::Power @@ -349,6 +357,7 @@ mod tests { let state = SystemState { usb_active: false, usb_initialized: true, + usb_suspended: false, idle_mode: true, calibration_active: false, throttle_hold_enable: false, @@ -381,6 +390,7 @@ mod tests { let state = SystemState { usb_active: true, usb_initialized: true, + usb_suspended: false, idle_mode: true, calibration_active: true, throttle_hold_enable: false, @@ -428,6 +438,7 @@ mod tests { let state = SystemState { usb_active: false, usb_initialized: false, + usb_suspended: false, idle_mode: false, calibration_active: false, throttle_hold_enable: false, @@ -436,4 +447,19 @@ mod tests { assert_eq!(determine_base_mode(state), StatusMode::Power); } + + #[test] + fn usb_suspend_takes_priority() { + let state = SystemState { + usb_active: true, + usb_initialized: true, + usb_suspended: true, + idle_mode: false, + calibration_active: true, // Even with calibration active + throttle_hold_enable: false, + vt_enable: false, + }; + + assert_eq!(determine_base_mode(state), StatusMode::Suspended); + } }