From 8d9bb74c7d4ab092fa2eb9da5fd5c1c61ab785ee Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 22 Sep 2026 08:21:03 -0600 Subject: [PATCH] Add --debug-rumble frame tracing to UART bridge --- .../controller_uart_bridge.py | 33 ++++++++++++++++--- tests/test_uart_rumble.py | 15 +++++---- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/switch_pico_bridge/controller_uart_bridge.py b/src/switch_pico_bridge/controller_uart_bridge.py index 8a29484..aaee4eb 100644 --- a/src/switch_pico_bridge/controller_uart_bridge.py +++ b/src/switch_pico_bridge/controller_uart_bridge.py @@ -202,12 +202,15 @@ def apply_rumble( controller: sdl3.SDL_Gamepad, low_frequency: float, high_frequency: float, -) -> bool: - """Apply normalized low/high rumble magnitudes to an SDL controller.""" +) -> Tuple[bool, bool]: + """Apply normalized low/high rumble magnitudes to an SDL controller. + + Returns (motor active, SDL accepted the request). + """ low = int(max(0.0, min(1.0, low_frequency)) * 0xFFFF) high = int(max(0.0, min(1.0, high_frequency)) * 0xFFFF) - sdl3.SDL_RumbleGamepad(controller, low, high, RUMBLE_DURATION_MS) - return low != 0 or high != 0 + accepted = bool(sdl3.SDL_RumbleGamepad(controller, low, high, RUMBLE_DURATION_MS)) + return low != 0 or high != 0, accepted @dataclass @@ -247,6 +250,8 @@ class ControllerContext: gyro_bias_samples: int = 0 gyro_bias_locked: bool = False last_debug_imu_print: float = 0.0 + last_debug_rumble: Optional[Tuple[float, float]] = None + debug_rumble_frames: int = 0 def capture_stick_offsets(controller: sdl3.SDL_Gamepad) -> Dict[int, int]: @@ -758,6 +763,11 @@ def build_arg_parser() -> argparse.ArgumentParser: action="store_true", help="Print raw IMU sensor readings and converted values for debugging.", ) + parser.add_argument( + "--debug-rumble", + action="store_true", + help="Print every decoded rumble frame received from the Pico and whether SDL accepted it.", + ) parser.add_argument( "--no-imu", action="store_true", @@ -815,6 +825,7 @@ class BridgeConfig: swap_abxy_ids: set[str] swap_abxy_global: bool debug_imu: bool = False + debug_rumble: bool = False no_imu: bool = False gyro_scale: float = 1.0 @@ -907,6 +918,7 @@ def build_bridge_config(console: Console, args: argparse.Namespace) -> BridgeCon swap_abxy_ids=set(swap_abxy_guids), # filled later once stable IDs are known swap_abxy_global=bool(args.swap_abxy), debug_imu=bool(args.debug_imu), + debug_rumble=bool(args.debug_rumble), no_imu=bool(args.no_imu), gyro_scale=float(args.gyro_scale), ) @@ -1602,13 +1614,24 @@ def service_contexts( if rumble is None: break latest_rumble = rumble + ctx.debug_rumble_frames += 1 if latest_rumble is not None: # Apply only the freshest rumble command seen during this tick. - ctx.rumble_active = apply_rumble( + ctx.rumble_active, accepted = apply_rumble( ctx.controller, latest_rumble[0], latest_rumble[1] ) ctx.last_rumble_at = now + if config.debug_rumble and ( + latest_rumble != ctx.last_debug_rumble or not accepted + ): + ctx.last_debug_rumble = latest_rumble + error = "" if accepted else f" sdl_error={sdl3.SDL_GetError().decode(errors='ignore')!r}" + print( + f"[RUMBLE idx={ctx.controller_index}] frame#{ctx.debug_rumble_frames} " + f"low={latest_rumble[0]:.3f} high={latest_rumble[1]:.3f} " + f"accepted={accepted}{error}" + ) elif ( ctx.rumble_active and (now - ctx.last_rumble_at) > RUMBLE_IDLE_TIMEOUT diff --git a/tests/test_uart_rumble.py b/tests/test_uart_rumble.py index cdce9c6..e5bb475 100644 --- a/tests/test_uart_rumble.py +++ b/tests/test_uart_rumble.py @@ -46,17 +46,18 @@ def test_apply_rumble_maps_low_and_high_with_50ms_duration( monkeypatch: pytest.MonkeyPatch, ) -> None: calls: list[tuple[int, int, int]] = [] - monkeypatch.setattr( - bridge.sdl3, - "SDL_RumbleGamepad", - lambda _controller, low, high, duration: calls.append((low, high, duration)), - ) + + def fake_rumble(_controller, low, high, duration): + calls.append((low, high, duration)) + return True + + monkeypatch.setattr(bridge.sdl3, "SDL_RumbleGamepad", fake_rumble) controller = cast(sdl3.SDL_Gamepad, object()) - assert bridge.apply_rumble(controller, 1.0, 0.5) + assert bridge.apply_rumble(controller, 1.0, 0.5) == (True, True) assert calls[-1] == (0xFFFF, 0x7FFF, 50) - assert not bridge.apply_rumble(controller, 0.0, 0.0) + assert bridge.apply_rumble(controller, 0.0, 0.0) == (False, True) assert calls[-1] == (0, 0, 50)