switch-pico/tools/debug_imu_raw.py

205 lines
7.7 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Raw SDL3 IMU diagnostic tool.
Prints every gyro/accel sensor event directly from SDL3, bypassing all
bridge logic. Use this to confirm SDL3 is delivering sensor events before
debugging conversion or axis mapping issues.
Usage:
uv run python tools/debug_imu_raw.py
uv run python tools/debug_imu_raw.py --count 500 # stop after N gyro events
uv run python tools/debug_imu_raw.py --no-bias # skip bias calibration window
"""
import argparse
import ctypes
import math
import sys
import time
import sdl3
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
SDL_SENSOR_ACCEL = 1
SDL_SENSOR_GYRO = 2
GRAVITY = 9.80665 # m/s²
LSB_PER_G = 4096.0 # Nintendo accel scale
LSB_PER_RAD_S = 818.5 # Nintendo gyro scale
BIAS_SAMPLES = 200 # ~1 second at 200 Hz
def main():
parser = argparse.ArgumentParser(description="Raw SDL3 IMU diagnostic")
parser.add_argument("--count", type=int, default=0,
help="Stop after this many gyro events (0 = run forever)")
parser.add_argument("--no-bias", action="store_true",
help="Skip bias calibration window, print raw values immediately")
parser.add_argument("--raw", action="store_true",
help="Also print converted Nintendo-native raw counts")
args = parser.parse_args()
# Init SDL3 with gamepad + sensor support
if not sdl3.SDL_Init(sdl3.SDL_INIT_GAMEPAD | sdl3.SDL_INIT_EVENTS):
print(f"SDL_Init failed: {sdl3.SDL_GetError().decode()}", file=sys.stderr)
sys.exit(1)
sdl3.SDL_SetGamepadEventsEnabled(True)
# Find first gamepad
count = ctypes.c_int(0)
ids = sdl3.SDL_GetJoysticks(ctypes.byref(count))
if not ids or count.value == 0:
print("No joysticks/gamepads found.", file=sys.stderr)
sdl3.SDL_Quit()
sys.exit(1)
gamepad = None
instance_id = None
for i in range(count.value):
if sdl3.SDL_IsGamepad(ids[i]):
gamepad = sdl3.SDL_OpenGamepad(ids[i])
instance_id = ids[i]
break
sdl3.SDL_free(ids)
if not gamepad:
print("No gamepad found (only non-gamepad joysticks detected).", file=sys.stderr)
sdl3.SDL_Quit()
sys.exit(1)
name = sdl3.SDL_GetGamepadName(gamepad)
print(f"Gamepad: {name.decode() if name else 'unknown'} (instance_id={instance_id})")
# Check sensor support
has_accel = bool(sdl3.SDL_GamepadHasSensor(gamepad, SDL_SENSOR_ACCEL))
has_gyro = bool(sdl3.SDL_GamepadHasSensor(gamepad, SDL_SENSOR_GYRO))
print(f" Accelerometer supported: {has_accel}")
print(f" Gyroscope supported: {has_gyro}")
if not (has_accel and has_gyro):
print("\nThis controller does not expose IMU sensors to SDL3.")
print("Possible reasons:")
print(" - Controller doesn't have IMU (Xbox, generic gamepads)")
print(" - Missing kernel driver (Linux: hid-nintendo not loaded)")
print(" - SDL3 HIDAPI disabled for this controller")
sdl3.SDL_CloseGamepad(gamepad)
sdl3.SDL_Quit()
sys.exit(1)
# Enable sensors
ok_accel = bool(sdl3.SDL_SetGamepadSensorEnabled(gamepad, SDL_SENSOR_ACCEL, True))
ok_gyro = bool(sdl3.SDL_SetGamepadSensorEnabled(gamepad, SDL_SENSOR_GYRO, True))
print(f" Accelerometer enabled: {ok_accel}")
print(f" Gyroscope enabled: {ok_gyro}")
if not (ok_accel and ok_gyro):
print(f"\nFailed to enable sensors: {sdl3.SDL_GetError().decode()}")
sdl3.SDL_CloseGamepad(gamepad)
sdl3.SDL_Quit()
sys.exit(1)
print()
if args.no_bias:
print("Skipping bias calibration. Showing raw values immediately.")
else:
print(f"Hold controller STILL — collecting {BIAS_SAMPLES} gyro samples for bias calibration...")
print("Press Ctrl+C to stop.\n")
print(f"{'EVENT':<8} {'AX':>8} {'AY':>8} {'AZ':>8} {'GX':>8} {'GY':>8} {'GZ':>8} {'STATUS'}")
print("-" * 80)
# State
last_accel = (0.0, 0.0, 0.0)
bias = [0.0, 0.0, 0.0]
bias_count = 0
bias_locked = args.no_bias
gyro_events = 0
last_print = time.monotonic()
event = sdl3.SDL_Event()
try:
while True:
while sdl3.SDL_PollEvent(ctypes.byref(event)):
t = event.type
if t == sdl3.SDL_EVENT_GAMEPAD_SENSOR_UPDATE:
gs = event.gsensor
# Only handle events from our gamepad
if gs.which != instance_id:
continue
sensor_type = gs.sensor
d = gs.data # c_float_Array_3
if sensor_type == SDL_SENSOR_ACCEL:
last_accel = (float(d[0]), float(d[1]), float(d[2]))
continue
if sensor_type != SDL_SENSOR_GYRO:
continue
gx, gy, gz = float(d[0]), float(d[1]), float(d[2])
# Bias accumulation
if not bias_locked:
if bias_count < BIAS_SAMPLES:
bias[0] += gx
bias[1] += gy
bias[2] += gz
bias_count += 1
if bias_count >= BIAS_SAMPLES:
bias = [b / BIAS_SAMPLES for b in bias]
bias_locked = True
print(f" [BIAS LOCKED] bias_rad_s=({bias[0]:.5f}, {bias[1]:.5f}, {bias[2]:.5f})\n")
continue # Don't print during calibration
gyro_events += 1
ax, ay, az = last_accel
ux, uy, uz = gx - bias[0], gy - bias[1], gz - bias[2]
now = time.monotonic()
if now - last_print >= 0.1: # 10 Hz display update
last_print = now
# In m/s² and rad/s (SDL values)
status = f"events={gyro_events}"
if args.raw:
# Nintendo-native counts (reversed SDL axis mapping)
nx = int(-uz * LSB_PER_RAD_S)
ny = int(-ux * LSB_PER_RAD_S)
nz = int( uy * LSB_PER_RAD_S)
nax = int(-az / GRAVITY * LSB_PER_G)
nay = int(-ax / GRAVITY * LSB_PER_G)
naz = int( ay / GRAVITY * LSB_PER_G)
status += f" raw_g=({nax},{nay},{naz}) raw_gyro=({nx},{ny},{nz})"
print(
f"{'GYRO':<8} "
f"{ax:>8.3f} {ay:>8.3f} {az:>8.3f} "
f"{ux:>8.4f} {uy:>8.4f} {uz:>8.4f} "
f"{status}"
)
elif t == sdl3.SDL_EVENT_GAMEPAD_REMOVED:
print("\nGamepad disconnected.")
break
if args.count and gyro_events >= args.count:
print(f"\nReached {args.count} gyro events. Done.")
break
time.sleep(0.001)
except KeyboardInterrupt:
print("\n\nStopped.")
print(f"\nTotal gyro events received: {gyro_events}")
if bias_locked:
print(f"Final bias (rad/s): ({bias[0]:.5f}, {bias[1]:.5f}, {bias[2]:.5f})")
print(f"Bias magnitude: {math.sqrt(sum(b**2 for b in bias)):.5f} rad/s "
f"= {math.sqrt(sum(b**2 for b in bias)) * 180/math.pi:.3f} deg/s")
sdl3.SDL_CloseGamepad(gamepad)
sdl3.SDL_Quit()
if __name__ == "__main__":
main()