From 22da7bce8fc1ca1e699f5cee2a37c91c7e4541ed Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 16 Mar 2026 11:50:46 -0600 Subject: [PATCH] chore: add IMU diagnostic tool - Add tools/read_pro_imu.py from gyrov3 branch - Reads raw 0x30 HID reports from Switch Pro Controller / Pico - Lazy pyhidapi import with helpful error message when missing - Supports --list, --count, --plot, --save-prefix flags --- tools/read_pro_imu.py | 199 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100755 tools/read_pro_imu.py diff --git a/tools/read_pro_imu.py b/tools/read_pro_imu.py new file mode 100755 index 0000000..c9abdf1 --- /dev/null +++ b/tools/read_pro_imu.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +""" +Read raw IMU samples from a Nintendo Switch Pro Controller (or Pico spoof) over USB. + +Uses the `hidapi` (pyhidapi) package. Press Ctrl+C to exit. +""" + +import argparse +import struct +import sys +from typing import List, Tuple + +DEFAULT_VENDOR_ID = 0x057E +DEFAULT_PRODUCT_ID = 0x2009 # Switch Pro Controller (USB) + +try: + import hid # from pyhidapi +except ImportError: + hid = None + + +def list_devices(filter_vid=None, filter_pid=None): + devices = hid.enumerate() + for d in devices: + if filter_vid and d["vendor_id"] != filter_vid: + continue + if filter_pid and d["product_id"] != filter_pid: + continue + print( + f"VID=0x{d['vendor_id']:04X} PID=0x{d['product_id']:04X} " + f"path={d.get('path')} " + f"serial={d.get('serial_number')} " + f"manufacturer={d.get('manufacturer_string')} " + f"product={d.get('product_string')} " + f"interface={d.get('interface_number')}" + ) + return devices + + +def find_device(vendor_id: int, product_id: int): + for dev in hid.enumerate(): + if dev["vendor_id"] == vendor_id and dev["product_id"] == product_id: + return dev + return None + + +def main(): + parser = argparse.ArgumentParser( + description="Read raw 0x30 reports (IMU) from a Switch Pro Controller / Pico." + ) + parser.add_argument( + "--vid", + type=lambda x: int(x, 0), + default=DEFAULT_VENDOR_ID, + help="Vendor ID (default 0x057E)", + ) + parser.add_argument( + "--pid", + type=lambda x: int(x, 0), + default=DEFAULT_PRODUCT_ID, + help="Product ID (default 0x2009)", + ) + parser.add_argument("--path", help="Explicit HID path to open (overrides VID/PID).") + parser.add_argument( + "--count", + type=int, + default=0, + help="Stop after this many 0x30 reports (0 = infinite).", + ) + parser.add_argument( + "--timeout", type=int, default=3000, help="Read timeout ms (default 3000)." + ) + parser.add_argument( + "--list", action="store_true", help="List detected HID devices and exit." + ) + parser.add_argument( + "--plot", + action="store_true", + help="Plot accel/gyro traces after capture (requires matplotlib).", + ) + parser.add_argument( + "--save-prefix", + help="If set, save accel/gyro plots as '_accel.png' and '_gyro.png'.", + ) + args = parser.parse_args() + + if hid is None: + print( + "pyhidapi is required for this tool. Install it with: pip install pyhidapi", + file=sys.stderr, + ) + sys.exit(1) + + if args.list: + list_devices() + return + + if args.path: + dev_info = { + "path": bytes(args.path, encoding="utf-8"), + "vendor_id": args.vid, + "product_id": args.pid, + } + else: + dev_info = find_device(args.vid, args.pid) + if not dev_info: + print( + f"No HID device found for VID=0x{args.vid:04X} PID=0x{args.pid:04X}. " + "Use --list to inspect devices or --path to target a specific one.", + file=sys.stderr, + ) + sys.exit(1) + + device = hid.device() + device.open_path(dev_info["path"]) + device.set_nonblocking(False) + print( + f"Reading raw 0x30 reports from device (VID=0x{args.vid:04X} PID=0x{args.pid:04X})... " + "Ctrl+C to stop." + ) + accel_series: List[Tuple[int, int, int]] = [] + gyro_series: List[Tuple[int, int, int]] = [] + try: + read_count = 0 + while args.count == 0 or read_count < args.count: + data = device.read(64, timeout_ms=args.timeout) + if not data: + print(f"(timeout after {args.timeout} ms, no data)") + continue + if data[0] != 0x30: + print(f"(non-0x30 report id=0x{data[0]:02X}, len={len(data)})") + continue + samples = [] + offset = 13 # accel_x starts at byte 13 + for _ in range(3): + ax, ay, az, gx, gy, gz = struct.unpack_from( + "