Add --debug-rumble frame tracing to UART bridge

This commit is contained in:
Joey Yakimowich-Payne 2026-09-22 08:21:03 -06:00
commit 8d9bb74c7d
2 changed files with 36 additions and 12 deletions

View file

@ -202,12 +202,15 @@ def apply_rumble(
controller: sdl3.SDL_Gamepad, controller: sdl3.SDL_Gamepad,
low_frequency: float, low_frequency: float,
high_frequency: float, high_frequency: float,
) -> bool: ) -> Tuple[bool, bool]:
"""Apply normalized low/high rumble magnitudes to an SDL controller.""" """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) low = int(max(0.0, min(1.0, low_frequency)) * 0xFFFF)
high = int(max(0.0, min(1.0, high_frequency)) * 0xFFFF) high = int(max(0.0, min(1.0, high_frequency)) * 0xFFFF)
sdl3.SDL_RumbleGamepad(controller, low, high, RUMBLE_DURATION_MS) accepted = bool(sdl3.SDL_RumbleGamepad(controller, low, high, RUMBLE_DURATION_MS))
return low != 0 or high != 0 return low != 0 or high != 0, accepted
@dataclass @dataclass
@ -247,6 +250,8 @@ class ControllerContext:
gyro_bias_samples: int = 0 gyro_bias_samples: int = 0
gyro_bias_locked: bool = False gyro_bias_locked: bool = False
last_debug_imu_print: float = 0.0 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]: def capture_stick_offsets(controller: sdl3.SDL_Gamepad) -> Dict[int, int]:
@ -758,6 +763,11 @@ def build_arg_parser() -> argparse.ArgumentParser:
action="store_true", action="store_true",
help="Print raw IMU sensor readings and converted values for debugging.", 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( parser.add_argument(
"--no-imu", "--no-imu",
action="store_true", action="store_true",
@ -815,6 +825,7 @@ class BridgeConfig:
swap_abxy_ids: set[str] swap_abxy_ids: set[str]
swap_abxy_global: bool swap_abxy_global: bool
debug_imu: bool = False debug_imu: bool = False
debug_rumble: bool = False
no_imu: bool = False no_imu: bool = False
gyro_scale: float = 1.0 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_ids=set(swap_abxy_guids), # filled later once stable IDs are known
swap_abxy_global=bool(args.swap_abxy), swap_abxy_global=bool(args.swap_abxy),
debug_imu=bool(args.debug_imu), debug_imu=bool(args.debug_imu),
debug_rumble=bool(args.debug_rumble),
no_imu=bool(args.no_imu), no_imu=bool(args.no_imu),
gyro_scale=float(args.gyro_scale), gyro_scale=float(args.gyro_scale),
) )
@ -1602,13 +1614,24 @@ def service_contexts(
if rumble is None: if rumble is None:
break break
latest_rumble = rumble latest_rumble = rumble
ctx.debug_rumble_frames += 1
if latest_rumble is not None: if latest_rumble is not None:
# Apply only the freshest rumble command seen during this tick. # 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.controller, latest_rumble[0], latest_rumble[1]
) )
ctx.last_rumble_at = now 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 ( elif (
ctx.rumble_active ctx.rumble_active
and (now - ctx.last_rumble_at) > RUMBLE_IDLE_TIMEOUT and (now - ctx.last_rumble_at) > RUMBLE_IDLE_TIMEOUT

View file

@ -46,17 +46,18 @@ def test_apply_rumble_maps_low_and_high_with_50ms_duration(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
) -> None: ) -> None:
calls: list[tuple[int, int, int]] = [] calls: list[tuple[int, int, int]] = []
monkeypatch.setattr(
bridge.sdl3, def fake_rumble(_controller, low, high, duration):
"SDL_RumbleGamepad", calls.append((low, high, duration))
lambda _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()) 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 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) assert calls[-1] == (0, 0, 50)