diff --git a/CMakeLists.txt b/CMakeLists.txt index ebca586..10a816f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,29 +24,8 @@ if (EXISTS ${picoVscode}) endif() # ==================================================================================== option(SWITCH_PICO_LOG "Enable UART debug logging" OFF) -set(SWITCH_PICO_PROTOCOL "legacy" CACHE STRING "USB protocol: legacy or switch2") -set_property(CACHE SWITCH_PICO_PROTOCOL PROPERTY STRINGS legacy switch2) set(PICO_BOARD pico CACHE STRING "Board type") -if (SWITCH_PICO_PROTOCOL STREQUAL "legacy") - set(SWITCH_PICO_PROTOCOL_SOURCES - switch_pro_driver.cpp - switch_legacy_protocol.cpp - ) - set(SWITCH_PICO_PROTOCOL_DEFINITION SWITCH_PICO_PROTOCOL_LEGACY=1) -elseif (SWITCH_PICO_PROTOCOL STREQUAL "switch2") - set(SWITCH_PICO_PROTOCOL_SOURCES - switch2_driver.cpp - switch2_descriptors.cpp - switch2_reports.cpp - switch2_commands.cpp - ) - set(SWITCH_PICO_PROTOCOL_DEFINITION SWITCH_PICO_PROTOCOL_SWITCH2=1) -else() - message(FATAL_ERROR - "Invalid SWITCH_PICO_PROTOCOL='${SWITCH_PICO_PROTOCOL}'. Expected legacy or switch2.") -endif() - # Pull in Raspberry Pi Pico SDK (must be before project) include(pico_sdk_import.cmake) @@ -59,12 +38,9 @@ pico_sdk_init() add_executable(switch-pico switch-pico.cpp - switch_uart_protocol.cpp - ${SWITCH_PICO_PROTOCOL_SOURCES} + switch_pro_driver.cpp ) -target_compile_definitions(switch-pico PRIVATE ${SWITCH_PICO_PROTOCOL_DEFINITION}) - pico_set_program_name(switch-pico "switch-pico") pico_set_program_version(switch-pico "0.1") diff --git a/README.md b/README.md index ddcba0d..01b46e6 100644 --- a/README.md +++ b/README.md @@ -103,62 +103,19 @@ Filters you can use: ## Building and flashing firmware Prereqs: Pico SDK + CMake toolchain set up. -### Protocol selection - -The default `legacy` protocol is the existing wired Switch Pro implementation. Omitting `--protocol` from `build.py` or `SWITCH_PICO_PROTOCOL` from CMake preserves that default. - -Build and flash the default legacy firmware with picotool: - +### One-shot build + flash (picotool) ```sh python3 build.py ``` - -Build without flashing, either implicitly or explicitly selecting legacy: - -```sh -python3 build.py --build-only -python3 build.py --protocol legacy --build-only -``` - -Manual legacy build: - -```sh -cmake -S . -B build -DSWITCH_PICO_PROTOCOL=legacy -DSWITCH_PICO_LOG=OFF -cmake --build build -j -``` - -Build the experimental Switch 2 protocol without flashing: - -```sh -python3 build.py --protocol switch2 --build-only -``` - -Manual experimental Switch 2 build: - -```sh -cmake -S . -B build -DSWITCH_PICO_PROTOCOL=switch2 -DSWITCH_PICO_LOG=OFF -cmake --build build -j -``` - -To build and flash that experimental image, omit `--build-only`: - -```sh -python3 build.py --protocol switch2 -``` - -`--build-only` confirms and prints `build/switch-pico.elf` and `build/switch-pico.uf2`, then exits without invoking `picotool load` or flashing hardware. Pico SDK may still use picotool internally while generating build outputs. Without that flag, existing build-and-flash behavior is unchanged. - - Requires `picotool` on your `PATH` (or set `PICOTOOL_PATH=/path/to/picotool`) and a connected Pico in BOOTSEL mode to automatically flash. - Set `ELF_PATH` to override the default `build/switch-pico.elf`. -### Experimental Switch 2 scope - -- Uses VID:PID `057E:2069` and a reduced two-interface USB configuration: interface 0 is HID with 64-byte interrupt IN/OUT endpoints, and interface 1 is vendor-specific with 64-byte bulk IN/OUT endpoints. -- Omits the captured audio interfaces, so it is not the full five-interface controller topology. -- Supports only USB initialization and selection of input reports `0x05` and `0x09`. Other vendor command families are unsupported. -- Packs buttons and sticks only. Switch 2 IMU data is not packed, and native HD-rumble output is not mapped to the legacy UART rumble path. -- Has been validated only for compilation and descriptor consistency. PC enumeration and Nintendo Switch 2 console compatibility remain unverified. -- Changes only the USB protocol facade. Existing UART1 wiring (GPIO4 TX, GPIO5 RX), 921600 baud rate, and UART report framing are unchanged. +### Manual build +```sh +cmake -S . -B build -DSWITCH_PICO_LOG=OFF +cmake --build build -j +``` +This produces a `.uf2` you can flash (typically `build/switch-pico.uf2`). ### Manual UF2 flashing (BOOTSEL, no tools) If you already have a built (or use the pre-built one in `firmware/`) `.uf2`, you can flash it without rebuilding: @@ -178,7 +135,6 @@ Flags: `build.py` can optionally update the **grip** colours in `controller_color_config.h` before building/flashing (default leaves the file unchanged): - Random grip colours: `python3 build.py --random-grip-color` - Set grip colours: `python3 build.py --grip-color FF00AA` -- Grip colour overrides apply only to `legacy`; `build.py` rejects them when `--protocol switch2` is selected. ## Python bridge (recommended) Works on macOS, Windows, Linux. Uses SDL2 + pyserial. diff --git a/build.py b/build.py old mode 100755 new mode 100644 index 190bee6..dabefe8 --- a/build.py +++ b/build.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 """Build and flash the project with optional grip color overrides.""" - import argparse import os import random @@ -9,15 +8,12 @@ import shutil import subprocess import sys from pathlib import Path -from typing import Final, Literal, TypeAlias, final SCRIPT_DIR = Path(__file__).resolve().parent CONFIG_FILE = SCRIPT_DIR / "controller_color_config.h" BUILD_DIR = SCRIPT_DIR / "build" -BUILD_ELF_PATH = BUILD_DIR / "switch-pico.elf" -BUILD_UF2_PATH = BUILD_DIR / "switch-pico.uf2" -ELF_PATH = Path(os.environ.get("ELF_PATH", str(BUILD_ELF_PATH))).expanduser() +ELF_PATH = Path(os.environ.get("ELF_PATH", BUILD_DIR / "switch-pico.elf")).expanduser() MACROS = ( "SWITCH_COLOR_LEFT_GRIP_R", @@ -28,73 +24,36 @@ MACROS = ( "SWITCH_COLOR_RIGHT_GRIP_B", ) -BuildProtocol: TypeAlias = Literal["legacy", "switch2"] -PROTOCOL_CHOICES: Final[tuple[BuildProtocol, BuildProtocol]] = ("legacy", "switch2") - - -@final -class BuildArguments(argparse.Namespace): - def __init__(self) -> None: - super().__init__() - self.protocol: BuildProtocol = "legacy" - self.build_only: bool = False - self.random_grip_color: bool = False - self.grip_color: str = "" - - -def parse_args() -> BuildArguments: +def parse_args(): parser = argparse.ArgumentParser( description="Build and flash the project, optionally setting grip colors.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="Default behavior leaves controller_color_config.h unchanged.", ) - _ = parser.add_argument( - "--protocol", - choices=PROTOCOL_CHOICES, - default="legacy", - help="USB protocol to build (default: legacy).", - ) - _ = parser.add_argument( - "--build-only", - action="store_true", - help="Build and print ELF/UF2 paths without flashing.", - ) group = parser.add_mutually_exclusive_group() - _ = group.add_argument( + group.add_argument( "--random-grip-color", action="store_true", help="Randomize both grip colors before building.", ) - _ = group.add_argument( + group.add_argument( "--grip-color", metavar="RRGGBB", help="Set both grip colors to the provided hex value.", ) - args = BuildArguments() - _ = parser.parse_args(namespace=args) - if args.protocol == "switch2" and (args.random_grip_color or bool(args.grip_color)): - parser.error( - "Switch 2 builds do not consume legacy grip colors; omit --random-grip-color " - + "and --grip-color." - ) - return args + return parser.parse_args() - -def random_hex_color() -> str: +def random_hex_color(): return "".join(f"{random.randrange(256):02X}" for _ in range(3)) - -def validate_custom_color(value: str) -> str: +def validate_custom_color(value): if not re.fullmatch(r"[0-9A-Fa-f]{6}", value): - raise argparse.ArgumentTypeError( - "Color must be a 6-digit hex value like FF8800." - ) + raise ValueError("Color must be a 6-digit hex value like FF8800.") return value - -def update_grip_colors(rgb_hex: str) -> None: +def update_grip_colors(rgb_hex): if not CONFIG_FILE.exists(): - _ = sys.stderr.write(f"Error: Cannot find {CONFIG_FILE}\n") + sys.stderr.write(f"Error: Cannot find {CONFIG_FILE}\n") sys.exit(1) r, g, b = rgb_hex[:2], rgb_hex[2:4], rgb_hex[4:6] @@ -102,14 +61,14 @@ def update_grip_colors(rgb_hex: str) -> None: try: text = CONFIG_FILE.read_text(encoding="utf-8") except OSError as exc: - _ = sys.stderr.write(f"Error reading {CONFIG_FILE}: {exc}\n") + sys.stderr.write(f"Error reading {CONFIG_FILE}: {exc}\n") sys.exit(1) - def replace(name: str, val: str, data: str) -> str: + def replace(name, val, data): pattern = rf"(?m)^(#define\s+{name}\s+)0x[0-9A-Fa-f]{{2}}" updated, count = re.subn(pattern, rf"\g<1>0x{val.upper()}", data) if count == 0: - _ = sys.stderr.write(f"Error: Could not find {name} in {CONFIG_FILE}\n") + sys.stderr.write(f"Error: Could not find {name} in {CONFIG_FILE}\n") sys.exit(1) return updated @@ -118,30 +77,26 @@ def update_grip_colors(rgb_hex: str) -> None: text = replace(macro, val, text) try: - _ = CONFIG_FILE.write_text(text, encoding="utf-8") + CONFIG_FILE.write_text(text, encoding="utf-8") except OSError as exc: - _ = sys.stderr.write(f"Error writing {CONFIG_FILE}: {exc}\n") + sys.stderr.write(f"Error writing {CONFIG_FILE}: {exc}\n") sys.exit(1) - -def run_cmd(command: list[str]) -> None: +def run_cmd(command): try: - _ = subprocess.run(command, cwd=SCRIPT_DIR, check=True) + subprocess.run(command, cwd=SCRIPT_DIR, check=True) except FileNotFoundError as exc: - _ = sys.stderr.write(f"Error running {command[0]}: {exc}\n") + sys.stderr.write(f"Error running {command[0]}: {exc}\n") sys.exit(1) except subprocess.CalledProcessError as exc: sys.exit(exc.returncode) - -def resolve_picotool() -> Path: +def resolve_picotool(): env_val = os.environ.get("PICOTOOL_PATH") if env_val: env_path = Path(env_val).expanduser() if not env_path.exists(): - _ = sys.stderr.write( - f"Error: PICOTOOL_PATH set to {env_path}, but it does not exist.\n" - ) + sys.stderr.write(f"Error: PICOTOOL_PATH set to {env_path}, but it does not exist.\n") sys.exit(1) return env_path @@ -149,13 +104,10 @@ def resolve_picotool() -> Path: if found: return Path(found) - _ = sys.stderr.write( - "Error: picotool not found. Put it on your PATH or set PICOTOOL_PATH.\n" - ) + sys.stderr.write("Error: picotool not found. Put it on your PATH or set PICOTOOL_PATH.\n") sys.exit(1) - -def build(protocol: BuildProtocol) -> None: +def build(): run_cmd( [ "cmake", @@ -164,23 +116,20 @@ def build(protocol: BuildProtocol) -> None: "-B", str(BUILD_DIR), "-DSWITCH_PICO_LOG=OFF", - f"-DSWITCH_PICO_PROTOCOL={protocol}", ] ) run_cmd(["cmake", "--build", str(BUILD_DIR)]) - -def flash() -> None: +def flash(): picotool = resolve_picotool() if not ELF_PATH.exists(): - _ = sys.stderr.write( + sys.stderr.write( f"Error: Cannot find ELF at {ELF_PATH}. Set ELF_PATH to override.\n" ) sys.exit(1) run_cmd([str(picotool), "load", str(ELF_PATH), "-fx"]) - -def main() -> None: +def main(): args = parse_args() color = None @@ -189,30 +138,16 @@ def main() -> None: elif args.grip_color: try: color = validate_custom_color(args.grip_color) - except argparse.ArgumentTypeError as exc: - _ = sys.stderr.write(f"Error: {exc}\n") + except ValueError as exc: + sys.stderr.write(f"Error: {exc}\n") sys.exit(1) if color: update_grip_colors(color) print(f"Grip color set to #{color} in {CONFIG_FILE.name}") - build(args.protocol) - if args.build_only: - outputs = (BUILD_ELF_PATH, BUILD_UF2_PATH) - for output in outputs: - if not output.is_file(): - _ = sys.stderr.write( - f"Error: Expected build output not found: {output}\n" - ) - sys.exit(1) - print("Build outputs:") - for output in outputs: - print(f" {output}") - return - + build() flash() - if __name__ == "__main__": main() diff --git a/src/switch_pico_bridge/controller_uart_bridge.py b/src/switch_pico_bridge/controller_uart_bridge.py index c1db5b1..3721407 100644 --- a/src/switch_pico_bridge/controller_uart_bridge.py +++ b/src/switch_pico_bridge/controller_uart_bridge.py @@ -59,8 +59,12 @@ RUMBLE_MIN_ACTIVE = 0.40 # below this, rumble is treated as off/noise RUMBLE_SCALE = 1.0 CONTROLLER_DB_URL_DEFAULT = "https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt" SDL_TRUE = True -SDL_EVENT_GAMEPAD_SENSOR_UPDATE = getattr(sdl3, "SDL_EVENT_GAMEPAD_SENSOR_UPDATE", 0x658) -GYRO_BIAS_SAMPLES = 200 +SDL_EVENT_GAMEPAD_SENSOR_UPDATE = getattr( + sdl3, "SDL_EVENT_GAMEPAD_SENSOR_UPDATE", 0x658 +) +GYRO_BIAS_SAMPLES = 100 # samples to collect for bias (~0.5 s at 200 Hz) +GYRO_BIAS_WARMUP_S = 1.5 # seconds to wait before starting calibration +GYRO_BIAS_TIMEOUT_S = 10.0 # force-lock after this many seconds even if still moving IMU_BUFFER_SIZE = 32 @@ -249,12 +253,16 @@ class ControllerContext: sensors_supported: bool = False sensors_enabled: bool = False imu_samples: List[IMUSample] = field(default_factory=list) - last_accel: Tuple[float, float, float] = (0.0, 0.0, 0.0) + # Default: gravity on SDL Y axis (~+9.8 m/s²) — controller held horizontally. + # This prevents the first IMU sample from having zero accel before the first + # accel event arrives. + last_accel: Tuple[float, float, float] = (0.0, 9.80665, 0.0) gyro_bias_x: float = 0.0 gyro_bias_y: float = 0.0 gyro_bias_z: float = 0.0 gyro_bias_samples: int = 0 gyro_bias_locked: bool = False + gyro_bias_start_time: float = 0.0 # monotonic time when calibration began last_debug_imu_print: float = 0.0 @@ -322,7 +330,9 @@ def initialize_controller_sensors(ctx: ControllerContext, console: Console) -> N accel_enabled = sdl3.SDL_SetGamepadSensorEnabled( ctx.controller, SENSOR_ACCEL, SDL_TRUE ) - gyro_enabled = sdl3.SDL_SetGamepadSensorEnabled(ctx.controller, SENSOR_GYRO, SDL_TRUE) + gyro_enabled = sdl3.SDL_SetGamepadSensorEnabled( + ctx.controller, SENSOR_GYRO, SDL_TRUE + ) ctx.sensors_enabled = accel_enabled and gyro_enabled if not ctx.sensors_enabled: console.print( @@ -819,7 +829,7 @@ class BridgeConfig: swap_abxy_ids: set[str] swap_abxy_global: bool debug_imu: bool = False - no_imu: bool = False + no_imu: bool = True gyro_scale: float = 1.0 @@ -853,7 +863,9 @@ class PairingState: ignore_port_desc: List[str] = field(default_factory=list) include_port_desc: List[str] = field(default_factory=list) include_port_mfr: List[str] = field(default_factory=list) - display_index_alloc: DisplayIndexAllocator = field(default_factory=DisplayIndexAllocator) + display_index_alloc: DisplayIndexAllocator = field( + default_factory=DisplayIndexAllocator + ) def load_button_maps( @@ -944,7 +956,11 @@ def detect_controllers( if sdl3.SDL_IsGamepad(instance_id): name = sdl3.SDL_GetGamepadNameForID(instance_id) name_str = ( - name.decode() if isinstance(name, bytes) else str(name) if name else "Unknown" + name.decode() + if isinstance(name, bytes) + else str(name) + if name + else "Unknown" ) if include_controller_name and all( substr not in name_str.lower() for substr in include_controller_name @@ -953,14 +969,20 @@ def detect_controllers( f"[yellow]Skipping controller ({name_str}) due to name filter[/yellow]" ) continue - console.print(f"[cyan]Detected controller {display_counter}: ({name_str})[/cyan]") + console.print( + f"[cyan]Detected controller {display_counter}: ({name_str})[/cyan]" + ) display_counter += 1 controller_ids.append(instance_id) controller_names[instance_id] = name_str else: name = sdl3.SDL_GetJoystickNameForID(instance_id) name_str = ( - name.decode() if isinstance(name, bytes) else str(name) if name else "Unknown" + name.decode() + if isinstance(name, bytes) + else str(name) + if name + else "Unknown" ) if include_controller_name and all( substr not in name_str.lower() for substr in include_controller_name @@ -1003,10 +1025,19 @@ def list_controllers_with_guids( if is_gc else sdl3.SDL_GetJoystickNameForID(instance_id) ) - name_str = name.decode() if isinstance(name, bytes) else str(name) if name else "Unknown" + name_str = ( + name.decode() + if isinstance(name, bytes) + else str(name) + if name + else "Unknown" + ) guid_str = guid_string_for_instance_id(instance_id) table.add_row( - str(instance_id), "GameController" if is_gc else "Joystick", name_str, guid_str + str(instance_id), + "GameController" if is_gc else "Joystick", + name_str, + guid_str, ) sdl3.SDL_free(joystick_ids) console.print(table) @@ -1098,7 +1129,9 @@ def assign_port_for_index( return port_choice -def ports_in_use(pairing: PairingState, contexts: Dict[int, ControllerContext]) -> set[str]: +def ports_in_use( + pairing: PairingState, contexts: Dict[int, ControllerContext] +) -> set[str]: """Return a set of UART paths currently reserved or mapped.""" used = set(pairing.mapping_by_index.values()) used.update(ctx.port for ctx in contexts.values() if ctx.port) @@ -1217,7 +1250,13 @@ def open_initial_contexts( for instance_id in controller_indices: if not sdl3.SDL_IsGamepad(instance_id): name = sdl3.SDL_GetJoystickNameForID(instance_id) - name_str = name.decode() if isinstance(name, bytes) else str(name) if name else "Unknown" + name_str = ( + name.decode() + if isinstance(name, bytes) + else str(name) + if name + else "Unknown" + ) console.print( f"[yellow]ID {instance_id} is not a GameController ({name_str}). Trying raw open failed.[/yellow]" ) @@ -1326,22 +1365,55 @@ def handle_sensor_update( gx, gy, gz = float(data[0]), float(data[1]), float(data[2]) if not ctx.gyro_bias_locked: + now = time.monotonic() + + # Track when the first gyro event arrived so we can enforce the warmup. + if ctx.gyro_bias_start_time == 0.0: + ctx.gyro_bias_start_time = now + print( + f"[IMU idx={ctx.controller_index}] Gyro bias calibration started — " + f"hold controller still for {GYRO_BIAS_WARMUP_S:.0f}s..." + ) + + elapsed = now - ctx.gyro_bias_start_time + + # Phase 1: warmup — discard all samples, just wait. + if elapsed < GYRO_BIAS_WARMUP_S: + return + + # Phase 2: collect samples unconditionally. + # Timeout: after GYRO_BIAS_TIMEOUT_S total, force-lock with whatever we have. if ctx.gyro_bias_samples < GYRO_BIAS_SAMPLES: ctx.gyro_bias_x += gx ctx.gyro_bias_y += gy ctx.gyro_bias_z += gz ctx.gyro_bias_samples += 1 - if ctx.gyro_bias_samples >= GYRO_BIAS_SAMPLES: - n = ctx.gyro_bias_samples + + force_lock = elapsed > GYRO_BIAS_TIMEOUT_S and ctx.gyro_bias_samples > 10 + + if ctx.gyro_bias_samples >= GYRO_BIAS_SAMPLES or force_lock: + n = max(ctx.gyro_bias_samples, 1) ctx.gyro_bias_x /= n ctx.gyro_bias_y /= n ctx.gyro_bias_z /= n ctx.gyro_bias_locked = True + import math - if not ctx.gyro_bias_locked: - bx, by, bz = 0.0, 0.0, 0.0 - else: - bx, by, bz = ctx.gyro_bias_x, ctx.gyro_bias_y, ctx.gyro_bias_z + mag = math.sqrt( + ctx.gyro_bias_x**2 + ctx.gyro_bias_y**2 + ctx.gyro_bias_z**2 + ) + quality = ( + "OK" if mag < 0.05 else "WARN: controller was moving during calibration" + ) + print( + f"[IMU idx={ctx.controller_index}] Bias locked{' (timeout)' if force_lock else ''}: " + f"({ctx.gyro_bias_x:.5f}, {ctx.gyro_bias_y:.5f}, {ctx.gyro_bias_z:.5f}) rad/s " + f"magnitude={mag:.4f} rad/s = {mag * 180 / math.pi:.2f} deg/s [{quality}]" + ) + # Don't send IMU until bias is locked — raw unbiased values cause drift. + return + + bx, by, bz = ctx.gyro_bias_x, ctx.gyro_bias_y, ctx.gyro_bias_z ux, uy, uz = gx, gy, gz ux -= bx @@ -1385,31 +1457,10 @@ def handle_sensor_update( ) -ABXY_SWAP_COMBO = frozenset({ - sdl3.SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, - sdl3.SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, - sdl3.SDL_GAMEPAD_BUTTON_BACK, - sdl3.SDL_GAMEPAD_BUTTON_START, -}) - - -def _check_abxy_swap_combo( - ctx: ControllerContext, - config: BridgeConfig, - console: Console, -) -> None: - """Toggle ABXY layout when LB+RB+SELECT+START are all held.""" - if not all(ctx.button_state.get(b) for b in ABXY_SWAP_COMBO): - return - toggle_abxy_for_context(ctx, config, console) - sdl3.SDL_RumbleGamepad(ctx.controller, 0xAAAA, 0xAAAA, 200) - - def handle_button_event( event: sdl3.SDL_Event, config: BridgeConfig, contexts: Dict[int, ControllerContext], - console: Console, ) -> None: """Process button events into report/dpad state.""" ctx = contexts.get(event.gbutton.which) @@ -1432,8 +1483,6 @@ def handle_button_event( elif button in DPAD_BUTTONS: ctx.dpad[DPAD_BUTTONS[button]] = pressed ctx.report.hat = str_to_dpad(ctx.dpad) - if pressed and button in ABXY_SWAP_COMBO: - _check_abxy_swap_combo(ctx, config, console) def handle_device_added( @@ -1451,7 +1500,13 @@ def handle_device_added( return if not sdl3.SDL_IsGamepad(sdl_id): name = sdl3.SDL_GetJoystickNameForID(sdl_id) - name_str = name.decode() if isinstance(name, bytes) else str(name) if name else "Unknown" + name_str = ( + name.decode() + if isinstance(name, bytes) + else str(name) + if name + else "Unknown" + ) console.print( f"[yellow]Device {sdl_id} is not a GameController ({name_str}).[/yellow]" ) @@ -1464,11 +1519,15 @@ def handle_device_added( try: controller, instance_id, guid = open_controller(sdl_id) except Exception as exc: - console.print(f"[red]Hotplug open failed for controller {display_idx}: {exc}[/red]") + console.print( + f"[red]Hotplug open failed for controller {display_idx}: {exc}[/red]" + ) pairing.display_index_alloc.release(display_idx) return stable_id = guid - should_swap = display_idx in config.swap_abxy_indices or stable_id in config.swap_abxy_ids + should_swap = ( + display_idx in config.swap_abxy_indices or stable_id in config.swap_abxy_ids + ) uart = open_uart_or_warn(port, args.baud, console) if port else None if uart: uarts.append(uart) @@ -1569,12 +1628,23 @@ def service_contexts( if ctx.sensors_enabled and not config.no_imu: count = min(len(ctx.imu_samples), IMU_SAMPLES_PER_REPORT) if count > 0: - ctx.report.imu_samples = ctx.imu_samples[:count] - ctx.imu_samples = ctx.imu_samples[count:] + # Take the NEWEST samples, discard stale ones. + # Previously took from front (oldest) which caused + # 145ms latency when FIFO was full at 29-32 samples. + ctx.report.imu_samples = ctx.imu_samples[-count:] + ctx.imu_samples.clear() else: ctx.report.imu_samples = [] else: ctx.report.imu_samples = [] + # Debug: log actual IMU values being sent via UART + if config.debug_imu and ctx.report.imu_samples: + s = ctx.report.imu_samples[0] + if abs(s.gyro_x) > 50 or abs(s.gyro_y) > 50 or abs(s.gyro_z) > 50: + print( + f"[UART_SEND] LARGE GYRO a=({s.accel_x},{s.accel_y},{s.accel_z}) " + f"g=({s.gyro_x},{s.gyro_y},{s.gyro_z}) fifo_remaining={len(ctx.imu_samples)}" + ) ctx.uart.send_report(ctx.report) ctx.last_send = now @@ -1645,7 +1715,7 @@ def run_bridge_loop( sdl3.SDL_EVENT_GAMEPAD_BUTTON_DOWN, sdl3.SDL_EVENT_GAMEPAD_BUTTON_UP, ): - handle_button_event(event, config, contexts, console) + handle_button_event(event, config, contexts) elif event.type == SDL_EVENT_GAMEPAD_SENSOR_UPDATE: handle_sensor_update(event, contexts, config) elif event.type == sdl3.SDL_EVENT_GAMEPAD_ADDED: diff --git a/switch-pico.cpp b/switch-pico.cpp index dc93a67..22beaef 100644 --- a/switch-pico.cpp +++ b/switch-pico.cpp @@ -4,9 +4,7 @@ #include "hardware/uart.h" #include "pico/stdlib.h" #include "tusb.h" -#include "switch_input.h" -#include "switch_protocol.h" -#include "switch_uart_protocol.h" +#include "switch_pro_driver.h" #ifdef SWITCH_PICO_LOG #define LOG_PRINTF(...) printf(__VA_ARGS__) @@ -62,7 +60,7 @@ static void on_rumble_from_switch(const uint8_t rumble[8]) { send_rumble_uart_frame(rumble); } -// Consume UART bytes and decode complete input frames. +// Consume UART bytes and forward complete frames to the Switch Pro driver. static bool poll_uart_frames() { static uint8_t buffer[64]; static uint8_t index = 0; @@ -105,7 +103,7 @@ static bool poll_uart_frames() { if (expected_len > 0 && index >= expected_len) { SwitchInputState parsed{}; - if (switch_uart_decode_input_frame(buffer, expected_len, &parsed)) { + if (switch_pro_apply_uart_packet(buffer, expected_len, &parsed)) { g_user_state = parsed; new_data = true; LOG_PRINTF("[UART] packet buttons=0x%04x hat=%u lx=%u ly=%u rx=%u ry=%u\n", @@ -139,7 +137,7 @@ static bool poll_uart_frames() { static void log_usb_state() { bool mounted = tud_mounted(); - bool ready = switch_protocol_is_ready(); + bool ready = switch_pro_is_ready(); if (mounted != g_last_mounted) { g_last_mounted = mounted; @@ -158,10 +156,10 @@ int main() { init_uart_input(); tusb_init(); - switch_protocol_init(); - switch_protocol_set_rumble_callback(on_rumble_from_switch); + switch_pro_init(); + switch_pro_set_rumble_callback(on_rumble_from_switch); g_user_state = neutral_input(); - switch_protocol_set_input(g_user_state); + switch_pro_set_input(g_user_state); LOG_PRINTF("[BOOT] switch-pico starting (UART0 log @ 115200)\n"); LOG_PRINTF("[INFO] UART1 pins TX=%d RX=%d baud=%d\n", @@ -172,8 +170,8 @@ int main() { bool new_data = poll_uart_frames(); // Pull controller state from UART1 (void)new_data; SwitchInputState state = g_user_state; - switch_protocol_set_input(state); - switch_protocol_task(); // Push state to the Switch host + switch_pro_set_input(state); + switch_pro_task(); // Push state to the Switch host log_usb_state(); } } diff --git a/switch2_commands.cpp b/switch2_commands.cpp deleted file mode 100644 index de8716f..0000000 --- a/switch2_commands.cpp +++ /dev/null @@ -1,79 +0,0 @@ -#include "switch2_commands.h" - -#include - -// Captured command vectors and acknowledgements: -// https://github.com/ndeadly/switch2_controller_research/blob/d1c5a7f7ba298f83017fae84952a4e6d2ef8fc92/commands.md -namespace { - -constexpr uint8_t kSelectReportResponse[] = { - 0x03, 0x01, 0x00, 0x0A, 0x00, 0xF8, 0x00, 0x00, -}; -constexpr uint8_t kInitializeUsbResponse[] = { - 0x03, 0x01, 0x00, 0x0D, 0x00, 0xF8, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, -}; - -} // namespace - -Switch2VendorCommand switch2_classify_vendor_request( - const uint8_t* data, - std::size_t length) { - if (data == nullptr || length < 8) { - return Switch2VendorCommand::Unsupported; - } - if (data[0] != 0x03 || data[1] != 0x91 || data[2] != 0x00 || - data[4] != 0x00 || data[6] != 0x00 || data[7] != 0x00) { - return Switch2VendorCommand::Unsupported; - } - if (length != static_cast(8 + data[5])) { - return Switch2VendorCommand::Unsupported; - } - - switch (data[3]) { - case 0x0A: - if (data[5] != 4 || data[9] != 0 || data[10] != 0 || data[11] != 0) { - return Switch2VendorCommand::Unsupported; - } - if (data[8] == 0x05) return Switch2VendorCommand::SelectReport05; - if (data[8] == 0x09) return Switch2VendorCommand::SelectReport09; - return Switch2VendorCommand::Unsupported; - - case 0x0D: - if (data[5] == 8 && data[8] == 0x01) { - return Switch2VendorCommand::InitializeUsb; - } - return Switch2VendorCommand::Unsupported; - - default: - return Switch2VendorCommand::Unsupported; - } -} - -std::size_t switch2_build_vendor_response( - Switch2VendorCommand command, - uint8_t* output, - std::size_t capacity) { - const uint8_t* response = nullptr; - std::size_t response_length = 0; - - switch (command) { - case Switch2VendorCommand::Unsupported: - return 0; - case Switch2VendorCommand::SelectReport05: - case Switch2VendorCommand::SelectReport09: - response = kSelectReportResponse; - response_length = sizeof(kSelectReportResponse); - break; - case Switch2VendorCommand::InitializeUsb: - response = kInitializeUsbResponse; - response_length = sizeof(kInitializeUsbResponse); - break; - } - - if (output == nullptr || capacity < response_length) { - return 0; - } - std::memcpy(output, response, response_length); - return response_length; -} diff --git a/switch2_commands.h b/switch2_commands.h deleted file mode 100644 index bd2cfcd..0000000 --- a/switch2_commands.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include -#include - -enum class Switch2VendorCommand { - Unsupported, - SelectReport05, - SelectReport09, - InitializeUsb, -}; - -Switch2VendorCommand switch2_classify_vendor_request( - const uint8_t* data, - std::size_t length); - -std::size_t switch2_build_vendor_response( - Switch2VendorCommand command, - uint8_t* output, - std::size_t capacity); diff --git a/switch2_descriptors.cpp b/switch2_descriptors.cpp deleted file mode 100644 index 1de4fa6..0000000 --- a/switch2_descriptors.cpp +++ /dev/null @@ -1,64 +0,0 @@ -#include "switch2_descriptors.h" - -// Captured Pro Controller 2 descriptors: -// https://github.com/ndeadly/switch2_controller_research/blob/d1c5a7f7ba298f83017fae84952a4e6d2ef8fc92/descriptors.md -const uint8_t switch2_device_descriptor[] = { - 0x12, 0x01, 0x00, 0x02, 0xEF, 0x02, 0x01, 0x40, 0x7E, - 0x05, 0x69, 0x20, 0x00, 0x02, 0x01, 0x02, 0x03, 0x01, -}; -const size_t switch2_device_descriptor_length = sizeof(switch2_device_descriptor); - -// The capture has five interfaces and a 268-byte configuration. This explicit -// experimental subset keeps captured HID/vendor interfaces 0-1 only, changes -// wTotalLength to 80 and bNumInterfaces to 2, and zeros uncaptured string -// indices iConfiguration and iInterface. Audio interfaces 2-4 are omitted. -const uint8_t switch2_configuration_descriptor[] = { - 0x09, 0x02, 0x50, 0x00, 0x02, 0x01, 0x00, 0xC0, 0xFA, - - 0x08, 0x0B, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, - 0x09, 0x04, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, - 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x61, 0x00, - 0x07, 0x05, 0x81, 0x03, 0x40, 0x00, 0x04, - 0x07, 0x05, 0x01, 0x03, 0x40, 0x00, 0x04, - - 0x08, 0x0B, 0x01, 0x01, 0xFF, 0x00, 0x00, 0x00, - 0x09, 0x04, 0x01, 0x00, 0x02, 0xFF, 0x00, 0x00, 0x00, - 0x07, 0x05, 0x02, 0x02, 0x40, 0x00, 0x00, - 0x07, 0x05, 0x82, 0x02, 0x40, 0x00, 0x00, -}; -const size_t switch2_configuration_descriptor_length = - sizeof(switch2_configuration_descriptor); - -const uint8_t switch2_hid_report_descriptor[] = { - 0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x05, - 0x05, 0xFF, 0x09, 0x01, 0x15, 0x00, 0x26, 0xFF, 0x00, - 0x95, 0x3F, 0x75, 0x08, 0x81, 0x02, - - 0x85, 0x09, 0x09, 0x01, 0x95, 0x02, 0x81, 0x02, - 0x05, 0x09, 0x19, 0x01, 0x29, 0x15, 0x25, 0x01, - 0x95, 0x15, 0x75, 0x01, 0x81, 0x02, - 0x95, 0x01, 0x75, 0x03, 0x81, 0x03, - 0x05, 0x01, 0x09, 0x01, 0xA1, 0x00, - 0x09, 0x30, 0x09, 0x31, 0x09, 0x33, 0x09, 0x35, - 0x26, 0xFF, 0x0F, 0x95, 0x04, 0x75, 0x0C, 0x81, 0x02, 0xC0, - 0x05, 0xFF, 0x09, 0x02, 0x26, 0xFF, 0x00, - 0x95, 0x34, 0x75, 0x08, 0x81, 0x02, - - 0x85, 0x02, 0x09, 0x01, 0x95, 0x3F, 0x91, 0x02, 0xC0, -}; -const size_t switch2_hid_report_descriptor_length = - sizeof(switch2_hid_report_descriptor); - -const uint8_t switch2_string_language[] = {0x09, 0x04}; -const size_t switch2_string_language_length = sizeof(switch2_string_language); -const uint8_t switch2_string_manufacturer[] = "Nintendo"; -const size_t switch2_string_manufacturer_length = - sizeof(switch2_string_manufacturer) - 1; -const uint8_t switch2_string_product[] = "Switch 2 Pro Controller"; -const size_t switch2_string_product_length = sizeof(switch2_string_product) - 1; -const uint8_t switch2_string_serial[] = "00"; -const size_t switch2_string_serial_length = sizeof(switch2_string_serial) - 1; - -static_assert(sizeof(switch2_device_descriptor) == 18); -static_assert(sizeof(switch2_configuration_descriptor) == 80); -static_assert(sizeof(switch2_hid_report_descriptor) == 97); diff --git a/switch2_descriptors.h b/switch2_descriptors.h deleted file mode 100644 index b8848aa..0000000 --- a/switch2_descriptors.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include -#include - -extern const uint8_t switch2_device_descriptor[]; -extern const size_t switch2_device_descriptor_length; - -extern const uint8_t switch2_configuration_descriptor[]; -extern const size_t switch2_configuration_descriptor_length; - -extern const uint8_t switch2_hid_report_descriptor[]; -extern const size_t switch2_hid_report_descriptor_length; - -extern const uint8_t switch2_string_language[]; -extern const size_t switch2_string_language_length; -extern const uint8_t switch2_string_manufacturer[]; -extern const size_t switch2_string_manufacturer_length; -extern const uint8_t switch2_string_product[]; -extern const size_t switch2_string_product_length; -extern const uint8_t switch2_string_serial[]; -extern const size_t switch2_string_serial_length; diff --git a/switch2_driver.cpp b/switch2_driver.cpp deleted file mode 100644 index e8d3133..0000000 --- a/switch2_driver.cpp +++ /dev/null @@ -1,237 +0,0 @@ -#include "switch_protocol.h" - -#include -#include -#include -#include - -#include "pico/time.h" -#include "switch2_commands.h" -#include "switch2_descriptors.h" -#include "switch2_reports.h" -#include "tusb.h" - -namespace { - -constexpr uint32_t kReportIntervalMs = 4; -constexpr std::size_t kMaxVendorRequestLength = 64; -constexpr std::size_t kMaxVendorResponseLength = 12; - -SwitchInputState input_state{}; -Switch2InputReportId selected_report = Switch2InputReportId::Pro; -uint32_t report_counter = 0; -uint32_t last_report_time = 0; -bool mounted = false; -bool initialized = false; -std::array pending_response{}; -std::size_t pending_response_length = 0; -SwitchRumbleCallback rumble_callback = nullptr; - -void reset_connection_state(bool is_mounted) { - input_state = {}; - selected_report = Switch2InputReportId::Pro; - report_counter = 0; - last_report_time = 0; - mounted = is_mounted; - initialized = false; - pending_response_length = 0; -} - -void flush_vendor_response() { - if (pending_response_length == 0) return; - - const uint32_t written = tud_vendor_write( - pending_response.data(), static_cast(pending_response_length)); - if (written == 0) return; - - const std::size_t consumed = written; - pending_response_length -= consumed; - if (pending_response_length > 0) { - std::memmove( - pending_response.data(), - pending_response.data() + consumed, - pending_response_length); - } - tud_vendor_write_flush(); -} - -} // namespace - -void switch_protocol_init() { - rumble_callback = nullptr; - reset_connection_state(false); -} - -void switch_protocol_set_input(const SwitchInputState& state) { - input_state = state; -} - -void switch_protocol_task() { - if (!mounted) return; - - const bool had_pending_response = pending_response_length > 0; - flush_vendor_response(); - if (had_pending_response || !initialized || pending_response_length > 0) return; - - const uint32_t now = static_cast( - to_ms_since_boot(get_absolute_time())); - if ((now - last_report_time) < kReportIntervalMs || !tud_hid_ready()) return; - - const Switch2InputReport report = - switch2_build_input_report(selected_report, input_state, report_counter); - if (tud_hid_report( - static_cast(report.id), - report.payload.data(), - static_cast(report.payload.size()))) { - ++report_counter; - last_report_time = now; - } -} - -bool switch_protocol_is_ready() { - return mounted && initialized; -} - -void switch_protocol_set_rumble_callback(SwitchRumbleCallback callback) { - rumble_callback = callback; -} - -uint8_t const* tud_descriptor_device_cb() { - return switch2_device_descriptor; -} - -uint8_t const* tud_descriptor_configuration_cb(uint8_t index) { - return index == 0 ? switch2_configuration_descriptor : nullptr; -} - -uint8_t const* tud_hid_descriptor_report_cb(uint8_t instance) { - return instance == 0 ? switch2_hid_report_descriptor : nullptr; -} - -uint16_t tud_hid_get_report_cb( - uint8_t instance, - uint8_t report_id, - hid_report_type_t report_type, - uint8_t* buffer, - uint16_t reqlen) { - if (instance != 0 || report_type != HID_REPORT_TYPE_INPUT || buffer == nullptr || - !mounted || !initialized) { - return 0; - } - - Switch2InputReportId id; - if (report_id == static_cast(Switch2InputReportId::Common)) { - id = Switch2InputReportId::Common; - } else if (report_id == static_cast(Switch2InputReportId::Pro)) { - id = Switch2InputReportId::Pro; - } else { - return 0; - } - - const Switch2InputReport report = - switch2_build_input_report(id, input_state, report_counter); - const uint16_t length = reqlen < report.payload.size() - ? reqlen - : static_cast(report.payload.size()); - std::memcpy(buffer, report.payload.data(), length); - return length; -} - -void tud_hid_set_report_cb( - uint8_t instance, - uint8_t report_id, - hid_report_type_t report_type, - uint8_t const* buffer, - uint16_t bufsize) { - if (instance != 0 || report_type != HID_REPORT_TYPE_OUTPUT || buffer == nullptr) return; - - uint8_t const* payload = nullptr; - if (report_id == 0 && bufsize == 64 && buffer[0] == 0x02) { - payload = buffer + 1; - } else if (report_id == 0x02 && bufsize == 63) { - payload = buffer; - } else { - return; - } - - // Report 0x02 carries two native HD-rumble payloads. No legacy UART mapping exists. - (void)payload; -} - -void tud_vendor_rx_cb(uint8_t itf, uint8_t const* buffer, uint16_t bufsize) { - if (itf != 0) return; - - std::array request{}; - const std::size_t request_length = bufsize < request.size() ? bufsize : request.size(); - if (buffer != nullptr) { - std::memcpy(request.data(), buffer, request_length); - } - tud_vendor_read_flush(); - if (!mounted || buffer == nullptr) return; - - const Switch2VendorCommand command = - switch2_classify_vendor_request(request.data(), request_length); - - switch (command) { - case Switch2VendorCommand::Unsupported: - return; - case Switch2VendorCommand::SelectReport05: - selected_report = Switch2InputReportId::Common; - break; - case Switch2VendorCommand::SelectReport09: - selected_report = Switch2InputReportId::Pro; - break; - case Switch2VendorCommand::InitializeUsb: - initialized = true; - break; - } - - pending_response_length = switch2_build_vendor_response( - command, pending_response.data(), pending_response.size()); -} - -void tud_mount_cb() { - reset_connection_state(true); -} - -void tud_umount_cb() { - reset_connection_state(false); -} - -uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) { - (void)langid; - static uint16_t descriptor[32]; - - if (index == 0) { - descriptor[1] = static_cast(switch2_string_language[0]) | - (static_cast(switch2_string_language[1]) << 8); - descriptor[0] = 0x0304; - return descriptor; - } - - const uint8_t* string = nullptr; - std::size_t length = 0; - switch (index) { - case 1: - string = switch2_string_manufacturer; - length = switch2_string_manufacturer_length; - break; - case 2: - string = switch2_string_product; - length = switch2_string_product_length; - break; - case 3: - string = switch2_string_serial; - length = switch2_string_serial_length; - break; - default: - return nullptr; - } - - if (length > 31) length = 31; - for (std::size_t position = 0; position < length; ++position) { - descriptor[1 + position] = string[position]; - } - descriptor[0] = static_cast(0x0300 | (2 * length + 2)); - return descriptor; -} diff --git a/switch2_reports.cpp b/switch2_reports.cpp deleted file mode 100644 index 70b171c..0000000 --- a/switch2_reports.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include "switch2_reports.h" - -// Report layouts are pinned to: -// https://github.com/ndeadly/switch2_controller_research/blob/d1c5a7f7ba298f83017fae84952a4e6d2ef8fc92/hid_reports.md -namespace { - -void pack_stick(uint8_t* destination, uint16_t x, uint16_t y) { - const uint16_t packed_x = x >> 4; - const uint16_t packed_y = y >> 4; - destination[0] = static_cast(packed_x & 0xFF); - destination[1] = static_cast( - ((packed_x >> 8) & 0x0F) | ((packed_y & 0x0F) << 4)); - destination[2] = static_cast(packed_y >> 4); -} - -} // namespace - -Switch2InputReport switch2_build_input_report( - Switch2InputReportId id, - const SwitchInputState& state, - uint32_t counter) { - Switch2InputReport report{id, {}}; - std::array& payload = report.payload; - - switch (id) { - case Switch2InputReportId::Common: - payload[0] = static_cast(counter); - payload[1] = static_cast(counter >> 8); - payload[2] = static_cast(counter >> 16); - payload[3] = static_cast(counter >> 24); - payload[4] = static_cast( - (state.button_zr ? 0x80 : 0) | - (state.button_r ? 0x40 : 0) | - (state.button_a ? 0x08 : 0) | - (state.button_b ? 0x04 : 0) | - (state.button_x ? 0x02 : 0) | - (state.button_y ? 0x01 : 0)); - payload[5] = static_cast( - (state.button_capture ? 0x20 : 0) | - (state.button_home ? 0x10 : 0) | - (state.button_l3 ? 0x08 : 0) | - (state.button_r3 ? 0x04 : 0) | - (state.button_plus ? 0x02 : 0) | - (state.button_minus ? 0x01 : 0)); - payload[6] = static_cast( - (state.button_zl ? 0x80 : 0) | - (state.button_l ? 0x40 : 0) | - (state.dpad_left ? 0x08 : 0) | - (state.dpad_right ? 0x04 : 0) | - (state.dpad_up ? 0x02 : 0) | - (state.dpad_down ? 0x01 : 0)); - pack_stick(&payload[10], state.lx, state.ly); - pack_stick(&payload[13], state.rx, state.ry); - payload[0x29] = 0x01; - break; - - case Switch2InputReportId::Pro: - payload[0] = static_cast(counter); - // USB external power is known; charging and battery bits stay zero. - payload[1] = 0x01; - payload[2] = static_cast( - (state.button_r3 ? 0x80 : 0) | - (state.button_plus ? 0x40 : 0) | - (state.button_zr ? 0x20 : 0) | - (state.button_r ? 0x10 : 0) | - (state.button_x ? 0x08 : 0) | - (state.button_y ? 0x04 : 0) | - (state.button_a ? 0x02 : 0) | - (state.button_b ? 0x01 : 0)); - payload[3] = static_cast( - (state.button_l3 ? 0x80 : 0) | - (state.button_minus ? 0x40 : 0) | - (state.button_zl ? 0x20 : 0) | - (state.button_l ? 0x10 : 0) | - (state.dpad_up ? 0x08 : 0) | - (state.dpad_left ? 0x04 : 0) | - (state.dpad_right ? 0x02 : 0) | - (state.dpad_down ? 0x01 : 0)); - payload[4] = static_cast( - (state.button_capture ? 0x02 : 0) | - (state.button_home ? 0x01 : 0)); - pack_stick(&payload[5], state.lx, state.ly); - pack_stick(&payload[8], state.rx, state.ry); - payload[11] = 0x30; - break; - } - - return report; -} diff --git a/switch2_reports.h b/switch2_reports.h deleted file mode 100644 index ae1a08b..0000000 --- a/switch2_reports.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include -#include - -#include "switch_input.h" - -enum class Switch2InputReportId : uint8_t { - Common = 0x05, - Pro = 0x09, -}; - -struct Switch2InputReport { - Switch2InputReportId id; - std::array payload; -}; - -Switch2InputReport switch2_build_input_report( - Switch2InputReportId id, - const SwitchInputState& state, - uint32_t counter); diff --git a/switch_input.h b/switch_input.h deleted file mode 100644 index 93b65e0..0000000 --- a/switch_input.h +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#include -#include - -#define SWITCH_PRO_HAT_UP 0x00 -#define SWITCH_PRO_HAT_UPRIGHT 0x01 -#define SWITCH_PRO_HAT_RIGHT 0x02 -#define SWITCH_PRO_HAT_DOWNRIGHT 0x03 -#define SWITCH_PRO_HAT_DOWN 0x04 -#define SWITCH_PRO_HAT_DOWNLEFT 0x05 -#define SWITCH_PRO_HAT_LEFT 0x06 -#define SWITCH_PRO_HAT_UPLEFT 0x07 -#define SWITCH_PRO_HAT_NOTHING 0x08 - -#define SWITCH_PRO_MASK_Y (1U << 0) -#define SWITCH_PRO_MASK_B (1U << 1) -#define SWITCH_PRO_MASK_A (1U << 2) -#define SWITCH_PRO_MASK_X (1U << 3) -#define SWITCH_PRO_MASK_L (1U << 4) -#define SWITCH_PRO_MASK_R (1U << 5) -#define SWITCH_PRO_MASK_ZL (1U << 6) -#define SWITCH_PRO_MASK_ZR (1U << 7) -#define SWITCH_PRO_MASK_MINUS (1U << 8) -#define SWITCH_PRO_MASK_PLUS (1U << 9) -#define SWITCH_PRO_MASK_L3 (1U << 10) -#define SWITCH_PRO_MASK_R3 (1U << 11) -#define SWITCH_PRO_MASK_HOME (1U << 12) -#define SWITCH_PRO_MASK_CAPTURE (1U << 13) - -#define SWITCH_PRO_JOYSTICK_MIN 0x0000 -#define SWITCH_PRO_JOYSTICK_MID 0x7FFF -#define SWITCH_PRO_JOYSTICK_MAX 0xFFFF - -typedef struct { - int16_t accel_x; - int16_t accel_y; - int16_t accel_z; - int16_t gyro_x; - int16_t gyro_y; - int16_t gyro_z; -} SwitchImuSample; - -typedef struct { - bool dpad_up; - bool dpad_down; - bool dpad_left; - bool dpad_right; - - bool button_a; - bool button_b; - bool button_x; - bool button_y; - bool button_l; - bool button_r; - bool button_zl; - bool button_zr; - bool button_plus; - bool button_minus; - bool button_home; - bool button_capture; - bool button_l3; - bool button_r3; - - uint16_t lx; - uint16_t ly; - uint16_t rx; - uint16_t ry; - - uint8_t imu_sample_count; - SwitchImuSample imu_samples[3]; -} SwitchInputState; diff --git a/switch_legacy_protocol.cpp b/switch_legacy_protocol.cpp deleted file mode 100644 index 9f21073..0000000 --- a/switch_legacy_protocol.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "switch_protocol.h" - -#include "switch_pro_driver.h" - -void switch_protocol_init() { - switch_pro_init(); -} - -void switch_protocol_set_input(const SwitchInputState& state) { - switch_pro_set_input(state); -} - -void switch_protocol_task() { - switch_pro_task(); -} - -bool switch_protocol_is_ready() { - return switch_pro_is_ready(); -} - -void switch_protocol_set_rumble_callback(SwitchRumbleCallback callback) { - switch_pro_set_rumble_callback(callback); -} diff --git a/switch_pro_bounds.h b/switch_pro_bounds.h deleted file mode 100644 index 183fb36..0000000 --- a/switch_pro_bounds.h +++ /dev/null @@ -1,71 +0,0 @@ -#pragma once - -#include -#include - -enum class SwitchProOutputReportKind : uint8_t { - Ignore, - Noop, - Rumble, - Feature, - Configuration, -}; - -inline SwitchProOutputReportKind switch_pro_classify_output_report( - const uint8_t* report, - std::size_t length) { - if (report == nullptr || length < 2 || length > 64) { - return SwitchProOutputReportKind::Ignore; - } - - switch (report[0]) { - case 0x00: - return SwitchProOutputReportKind::Noop; - case 0x01: - return length >= 16 - ? SwitchProOutputReportKind::Feature - : SwitchProOutputReportKind::Ignore; - case 0x10: - case 0x21: - return length >= 10 - ? SwitchProOutputReportKind::Rumble - : SwitchProOutputReportKind::Ignore; - case 0x80: - return SwitchProOutputReportKind::Configuration; - default: - return SwitchProOutputReportKind::Ignore; - } -} - -inline bool switch_pro_spi_read_size_fits(std::size_t size) { - return size <= 64 - 20; -} - -inline std::size_t switch_pro_fill_flash_read( - uint8_t* destination, - std::size_t destination_capacity, - const uint8_t* source, - std::size_t source_size, - std::size_t source_offset, - std::size_t requested) { - if (destination == nullptr) { - return 0; - } - - const std::size_t produced = requested < destination_capacity - ? requested - : destination_capacity; - for (std::size_t index = 0; index < produced; ++index) { - destination[index] = 0xFF; - } - - if (source != nullptr && source_offset < source_size) { - const std::size_t available = source_size - source_offset; - const std::size_t copied = available < produced ? available : produced; - for (std::size_t index = 0; index < copied; ++index) { - destination[index] = source[source_offset + index]; - } - } - - return produced; -} diff --git a/switch_pro_descriptors.h b/switch_pro_descriptors.h index 236b8b3..2ec3444 100644 --- a/switch_pro_descriptors.h +++ b/switch_pro_descriptors.h @@ -9,10 +9,39 @@ #include -#include "switch_input.h" - #define SWITCH_PRO_ENDPOINT_SIZE 64 +// HAT report (4 bits) +#define SWITCH_PRO_HAT_UP 0x00 +#define SWITCH_PRO_HAT_UPRIGHT 0x01 +#define SWITCH_PRO_HAT_RIGHT 0x02 +#define SWITCH_PRO_HAT_DOWNRIGHT 0x03 +#define SWITCH_PRO_HAT_DOWN 0x04 +#define SWITCH_PRO_HAT_DOWNLEFT 0x05 +#define SWITCH_PRO_HAT_LEFT 0x06 +#define SWITCH_PRO_HAT_UPLEFT 0x07 +#define SWITCH_PRO_HAT_NOTHING 0x08 + +#define SWITCH_PRO_MASK_Y (1U << 0) +#define SWITCH_PRO_MASK_B (1U << 1) +#define SWITCH_PRO_MASK_A (1U << 2) +#define SWITCH_PRO_MASK_X (1U << 3) +#define SWITCH_PRO_MASK_L (1U << 4) +#define SWITCH_PRO_MASK_R (1U << 5) +#define SWITCH_PRO_MASK_ZL (1U << 6) +#define SWITCH_PRO_MASK_ZR (1U << 7) + +#define SWITCH_PRO_MASK_MINUS (1U << 8) +#define SWITCH_PRO_MASK_PLUS (1U << 9) +#define SWITCH_PRO_MASK_L3 (1U << 10) +#define SWITCH_PRO_MASK_R3 (1U << 11) +#define SWITCH_PRO_MASK_HOME (1U << 12) +#define SWITCH_PRO_MASK_CAPTURE (1U << 13) + +#define SWITCH_PRO_JOYSTICK_MIN 0x0000 +#define SWITCH_PRO_JOYSTICK_MID 0x7FFF +#define SWITCH_PRO_JOYSTICK_MAX 0xFFFF + typedef enum { REPORT_OUTPUT_00 = 0x00, REPORT_FEATURE = 0x01, diff --git a/switch_pro_driver.cpp b/switch_pro_driver.cpp index a199e67..7e835e3 100644 --- a/switch_pro_driver.cpp +++ b/switch_pro_driver.cpp @@ -1,5 +1,4 @@ #include "switch_pro_driver.h" -#include "switch_pro_bounds.h" #include #include @@ -7,7 +6,6 @@ #include #include "pico/rand.h" #include "pico/time.h" -#include "switch_pro_descriptors.h" #include "tusb.h" #ifdef SWITCH_PICO_LOG @@ -99,10 +97,18 @@ static const uint8_t factory_config_data[0xEFF] = { 0xFF, 0xFF, 0xFF, 0xFF, - // config & calibration 1 - 0xE3, 0xFF, 0x39, 0xFF, 0xED, 0x01, 0x00, 0x40, - 0x00, 0x40, 0x00, 0x40, 0x09, 0x00, 0xEA, 0xFF, - 0xA1, 0xFF, 0x3B, 0x34, 0x3B, 0x34, 0x3B, 0x34, + // config & calibration 1 (6-axis IMU, SPI 0x6020-0x6037) + // Accel origin (0,0,0): bridge pre-corrects for bias, so Switch must not + // apply a second origin offset. Real controllers have hardware DC offsets + // here, but our emulated sensor sends bias-corrected values. + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Accel sensitivity coeff: 0x4000 = 16384 → 4096 LSB/G (matches bridge) + 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, + // Gyro origin (0,0,0): bridge removes hardware bias before sending. + // Original values (9, -22, -95) caused phantom 6.7 dps yaw when still. + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Gyro sensitivity coeff: 0x343B = 13371 → 818.5 LSB/rad_s (matches bridge) + 0x3B, 0x34, 0x3B, 0x34, 0x3B, 0x34, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, @@ -137,7 +143,12 @@ static const uint8_t factory_config_data[0xEFF] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0x50, 0xFD, 0x00, 0x00, 0xC6, 0x0F, + // Six-Axis horizontal offsets (SPI 0x6080): expected accel when held in + // gaming position. Must match the bridge's actual output for a still + // controller. Old values (-688,0,4038) were for a real Pro Controller's + // physical IMU; our bridge sends (~0,~0,~4096). The 388-count mismatch + // on X caused the Switch's sensor fusion to fight the gyro → camera swing. + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, // (0, 0, 4096) = 1G on Z 0x0F, 0x30, 0x61, 0xAE, 0x90, 0xD9, 0xD4, 0x14, 0x54, 0x41, 0x15, 0x54, 0xC7, 0x79, 0x9C, 0x33, 0x36, 0x63, @@ -179,21 +190,33 @@ static const uint8_t user_calibration_data[0x3F] = { static const SwitchFactoryConfig* factory_config = reinterpret_cast(factory_config_data); static const SwitchUserCalibration* user_calibration [[maybe_unused]] = reinterpret_cast(user_calibration_data); -struct SpiFlashRegion { - const uint8_t* data; - std::size_t size; -}; - -static const std::map spi_flash_data = { - {0x6000, {factory_config_data, sizeof(factory_config_data)}}, - {0x8000, {user_calibration_data, sizeof(user_calibration_data)}} +static std::map spi_flash_data = { + {0x6000, factory_config_data}, + {0x8000, user_calibration_data} }; static inline uint16_t scale16To12(uint16_t pos) { return pos >> 4; } +// Default "at rest" IMU sample: zero gyro, ~1G on accel Z (face-up). +// Written to imuData at boot and whenever no fresh UART data is available, +// so the Switch never sees all-zero IMU (which it interprets as free-fall). +static const uint8_t DEFAULT_IMU_SAMPLE[12] = { + 0x00, 0x00, // accel_x = 0 + 0x00, 0x00, // accel_y = 0 + 0x00, 0x10, // accel_z = 0x1000 = 4096 = 1G + 0x00, 0x00, // gyro_x = 0 + 0x00, 0x00, // gyro_y = 0 + 0x00, 0x00, // gyro_z = 0 +}; + static void fill_imu_report_data(const SwitchInputState& state) { if (state.imu_sample_count == 0) { - memset(switch_report.imuData, 0x00, sizeof(switch_report.imuData)); + // No new IMU data — fill with default "at rest" sample. + // This prevents the Switch from seeing all-zero accel (free-fall) + // during startup or when the bridge hasn't sent IMU yet. + for (int i = 0; i < 3; ++i) { + memcpy(switch_report.imuData + i * 12, DEFAULT_IMU_SAMPLE, 12); + } return; } uint8_t sample_count = state.imu_sample_count > 3 ? 3 : state.imu_sample_count; @@ -217,6 +240,16 @@ static void fill_imu_report_data(const SwitchInputState& state) { } } +static SwitchInputState make_neutral_state() { + SwitchInputState s{}; + s.lx = SWITCH_PRO_JOYSTICK_MID; + s.ly = SWITCH_PRO_JOYSTICK_MID; + s.rx = SWITCH_PRO_JOYSTICK_MID; + s.ry = SWITCH_PRO_JOYSTICK_MID; + s.imu_sample_count = 0; + return s; +} + static void send_identify() { memset(report_buffer, 0x00, sizeof(report_buffer)); report_buffer[0] = REPORT_USB_INPUT_81; @@ -241,32 +274,16 @@ static bool send_report(uint8_t reportID, const void* reportData, uint16_t repor return result; } -static void read_spi_flash( - uint8_t* destination, - std::size_t destination_capacity, - uint32_t address, - uint8_t size) { +static void read_spi_flash(uint8_t* dest, uint32_t address, uint8_t size) { uint32_t addressBank = address & 0xFFFFFF00; uint32_t addressOffset = address & 0x000000FF; auto it = spi_flash_data.find(addressBank); if (it != spi_flash_data.end()) { - const SpiFlashRegion& region = it->second; - switch_pro_fill_flash_read( - destination, - destination_capacity, - region.data, - region.size, - addressOffset, - size); + const uint8_t* data = it->second; + memcpy(dest, data + addressOffset, size); } else { - switch_pro_fill_flash_read( - destination, - destination_capacity, - nullptr, - 0, - 0, - size); + memset(dest, 0xFF, size); } } @@ -336,6 +353,8 @@ static void handle_feature_report(uint8_t switchReportID, uint8_t switchReportSu uint8_t spiReadSize = 0; bool canSend = false; last_host_activity_ms = to_ms_since_boot(get_absolute_time()); + LOG_PRINTF("[HID] handle_feature rid=0x%02x cmd=0x%02x imu_enabled=%d\n", + switchReportID, commandID, is_imu_enabled); report_buffer[0] = REPORT_OUTPUT_21; report_buffer[1] = last_report_counter; @@ -393,11 +412,7 @@ static void handle_feature_report(uint8_t switchReportID, uint8_t switchReportSu report_buffer[17] = reportData[13]; report_buffer[18] = reportData[14]; report_buffer[19] = reportData[15]; - read_spi_flash( - &report_buffer[20], - sizeof(report_buffer) - 20, - spiReadAddress, - spiReadSize); + read_spi_flash(&report_buffer[20], spiReadAddress, spiReadSize); canSend = true; LOG_PRINTF("[HID] FEATURE SPI_READ addr=0x%08lx size=%u\n", (unsigned long)spiReadAddress, spiReadSize); break; @@ -641,6 +656,19 @@ void switch_pro_task() { uint16_t report_size = sizeof(switch_report); if (tud_hid_ready() && send_report(0, inputReport, report_size) == true ) { memcpy(last_report, inputReport, report_size); + // Log IMU data being sent (throttled to ~4Hz to avoid flooding UART0) + static uint32_t last_imu_log = 0; + if (now - last_imu_log > 250) { + last_imu_log = now; + int16_t ax = (int16_t)(switch_report.imuData[0] | (switch_report.imuData[1] << 8)); + int16_t ay = (int16_t)(switch_report.imuData[2] | (switch_report.imuData[3] << 8)); + int16_t az = (int16_t)(switch_report.imuData[4] | (switch_report.imuData[5] << 8)); + int16_t gx = (int16_t)(switch_report.imuData[6] | (switch_report.imuData[7] << 8)); + int16_t gy = (int16_t)(switch_report.imuData[8] | (switch_report.imuData[9] << 8)); + int16_t gz = (int16_t)(switch_report.imuData[10] | (switch_report.imuData[11] << 8)); + LOG_PRINTF("[IMU_OUT] a=(%d,%d,%d) g=(%d,%d,%d) cnt=%d\n", + ax, ay, az, gx, gy, gz, g_input_state.imu_sample_count); + } g_input_state.imu_sample_count = 0; report_sent = true; } @@ -660,6 +688,112 @@ void switch_pro_task() { } } +bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length, SwitchInputState* out_state) { + // v2 format: 0xAA + 0x02 + payload_len + payload... + checksum + if (length < 12) { + return false; + } + if (packet[0] != 0xAA) { + return false; + } + if (packet[1] != 0x02) { + return false; + } + + uint8_t payload_len = packet[2]; + if ((uint16_t)payload_len + 4u != length) { + return false; + } + + uint16_t sum = 0; + for (uint16_t i = 0; i < (uint16_t)(3u + payload_len); ++i) { + sum += packet[i]; + } + if ((sum & 0xFF) != packet[length - 1]) { + return false; + } + + // payload: buttons(2 LE), hat, lx, ly, rx, ry, imu_count, [imu_samples...] + if (payload_len < 8) { + return false; + } + + SwitchProOutReport out{}; + out.buttons = static_cast(packet[3]) | (static_cast(packet[4]) << 8); + out.hat = packet[5]; + out.lx = packet[6]; + out.ly = packet[7]; + out.rx = packet[8]; + out.ry = packet[9]; + uint8_t imu_count = packet[10]; + if (imu_count > 3) { + imu_count = 3; + } + + uint16_t required_payload_len = static_cast(8u + static_cast(imu_count) * 12u); + if (payload_len < required_payload_len) { + return false; + } + + auto expand_axis = [](uint8_t v) -> uint16_t { + return static_cast(v) << 8 | v; + }; + + SwitchInputState state = make_neutral_state(); + state.imu_sample_count = imu_count; + + auto read_int16 = [](const uint8_t* src) -> int16_t { + return static_cast(static_cast(src[0]) | (static_cast(src[1]) << 8)); + }; + for (uint8_t i = 0; i < imu_count; ++i) { + const uint8_t* base = &packet[11 + i * 12]; + state.imu_samples[i].accel_x = read_int16(base + 0); + state.imu_samples[i].accel_y = read_int16(base + 2); + state.imu_samples[i].accel_z = read_int16(base + 4); + state.imu_samples[i].gyro_x = read_int16(base + 6); + state.imu_samples[i].gyro_y = read_int16(base + 8); + state.imu_samples[i].gyro_z = read_int16(base + 10); + } + + switch (out.hat) { + case SWITCH_PRO_HAT_UP: state.dpad_up = true; break; + case SWITCH_PRO_HAT_UPRIGHT: state.dpad_up = true; state.dpad_right = true; break; + case SWITCH_PRO_HAT_RIGHT: state.dpad_right = true; break; + case SWITCH_PRO_HAT_DOWNRIGHT: state.dpad_down = true; state.dpad_right = true; break; + case SWITCH_PRO_HAT_DOWN: state.dpad_down = true; break; + case SWITCH_PRO_HAT_DOWNLEFT: state.dpad_down = true; state.dpad_left = true; break; + case SWITCH_PRO_HAT_LEFT: state.dpad_left = true; break; + case SWITCH_PRO_HAT_UPLEFT: state.dpad_up = true; state.dpad_left = true; break; + default: break; + } + + state.button_y = out.buttons & SWITCH_PRO_MASK_Y; + state.button_x = out.buttons & SWITCH_PRO_MASK_X; + state.button_b = out.buttons & SWITCH_PRO_MASK_B; + state.button_a = out.buttons & SWITCH_PRO_MASK_A; + state.button_r = out.buttons & SWITCH_PRO_MASK_R; + state.button_zr = out.buttons & SWITCH_PRO_MASK_ZR; + state.button_plus = out.buttons & SWITCH_PRO_MASK_PLUS; + state.button_minus = out.buttons & SWITCH_PRO_MASK_MINUS; + state.button_r3 = out.buttons & SWITCH_PRO_MASK_R3; + state.button_l3 = out.buttons & SWITCH_PRO_MASK_L3; + state.button_home = out.buttons & SWITCH_PRO_MASK_HOME; + state.button_capture = out.buttons & SWITCH_PRO_MASK_CAPTURE; + state.button_zl = out.buttons & SWITCH_PRO_MASK_ZL; + state.button_l = out.buttons & SWITCH_PRO_MASK_L; + + state.lx = expand_axis(out.lx); + state.ly = expand_axis(out.ly); + state.rx = expand_axis(out.rx); + state.ry = expand_axis(out.ry); + + if (!out_state) { + return false; + } + *out_state = state; + return true; +} + void switch_pro_set_rumble_callback(SwitchRumbleCallback cb) { rumble_callback = cb; } @@ -668,52 +802,6 @@ bool switch_pro_is_ready() { return is_ready; } -static void dispatch_output_report( - uint8_t instance, - uint8_t report_id, - const uint8_t* buffer, - uint16_t length) { - if (instance != 0) { - return; - } - - const SwitchProOutputReportKind kind = - switch_pro_classify_output_report(buffer, length); - if (kind == SwitchProOutputReportKind::Ignore) { - return; - } - if (kind == SwitchProOutputReportKind::Feature && - buffer[10] == SPI_READ && - !switch_pro_spi_read_size_fits(buffer[15])) { - return; - } - - memset(report_buffer, 0x00, sizeof(report_buffer)); - - const uint8_t switchReportID = buffer[0]; - const uint8_t switchReportSubID = buffer[1]; - LOG_PRINTF("[HID] output_report id=%u switchRID=0x%02x sub=0x%02x len=%u\n", - report_id, switchReportID, switchReportSubID, length); - - switch (kind) { - case SwitchProOutputReportKind::Noop: - return; - case SwitchProOutputReportKind::Rumble: - forward_rumble_to_host(buffer, length); - return; - case SwitchProOutputReportKind::Feature: - queued_report_id = report_id; - handle_feature_report(switchReportID, switchReportSubID, buffer, length); - return; - case SwitchProOutputReportKind::Configuration: - queued_report_id = report_id; - handle_config_report(switchReportID, switchReportSubID, buffer, length); - return; - case SwitchProOutputReportKind::Ignore: - return; - } -} - // HID callbacks uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) { (void)instance; @@ -728,12 +816,50 @@ uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_t } void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize) { + (void)instance; if (report_type != HID_REPORT_TYPE_OUTPUT) return; - dispatch_output_report(instance, report_id, buffer, bufsize); + + memset(report_buffer, 0x00, bufsize); + + uint8_t switchReportID = buffer[0]; + uint8_t switchReportSubID = buffer[1]; + LOG_PRINTF("[HID] set_report type=%d id=%u switchRID=0x%02x sub=0x%02x len=%u\n", + report_type, report_id, switchReportID, switchReportSubID, bufsize); + if (switchReportID == REPORT_OUTPUT_10 || switchReportID == REPORT_OUTPUT_21) { + forward_rumble_to_host(buffer, bufsize); + } + if (switchReportID == REPORT_OUTPUT_00) { + // No-op, just acknowledge to clear any stalls. + return; + } else if (switchReportID == REPORT_FEATURE) { + queued_report_id = report_id; + handle_feature_report(switchReportID, switchReportSubID, buffer, bufsize); + } else if (switchReportID == REPORT_CONFIGURATION) { + queued_report_id = report_id; + handle_config_report(switchReportID, switchReportSubID, buffer, bufsize); + } } void tud_hid_report_received_cb(uint8_t instance, uint8_t report_id, uint8_t const* buffer, uint16_t bufsize) { - dispatch_output_report(instance, report_id, buffer, bufsize); + (void)instance; + // Host sent data on interrupt OUT; mirror the control path handling. + memset(report_buffer, 0x00, bufsize); + uint8_t switchReportID = buffer[0]; + uint8_t switchReportSubID = buffer[1]; + LOG_PRINTF("[HID] report_received id=%u switchRID=0x%02x sub=0x%02x len=%u\n", + report_id, switchReportID, switchReportSubID, bufsize); + if (switchReportID == REPORT_OUTPUT_10 || switchReportID == REPORT_OUTPUT_21) { + forward_rumble_to_host(buffer, bufsize); + } + if (switchReportID == REPORT_OUTPUT_00) { + return; + } else if (switchReportID == REPORT_FEATURE) { + queued_report_id = report_id; + handle_feature_report(switchReportID, switchReportSubID, buffer, bufsize); + } else if (switchReportID == REPORT_CONFIGURATION) { + queued_report_id = report_id; + handle_config_report(switchReportID, switchReportSubID, buffer, bufsize); + } } uint8_t const * tud_hid_descriptor_report_cb(uint8_t itf) { diff --git a/switch_pro_driver.h b/switch_pro_driver.h index fe3c4f5..951d68b 100644 --- a/switch_pro_driver.h +++ b/switch_pro_driver.h @@ -8,8 +8,46 @@ #include #include +#include "switch_pro_descriptors.h" -#include "switch_protocol.h" +typedef struct { + int16_t accel_x; + int16_t accel_y; + int16_t accel_z; + int16_t gyro_x; + int16_t gyro_y; + int16_t gyro_z; +} SwitchImuSample; + +typedef struct { + bool dpad_up; + bool dpad_down; + bool dpad_left; + bool dpad_right; + + bool button_a; + bool button_b; + bool button_x; + bool button_y; + bool button_l; + bool button_r; + bool button_zl; + bool button_zr; + bool button_plus; + bool button_minus; + bool button_home; + bool button_capture; + bool button_l3; + bool button_r3; + + uint16_t lx; // 0-65535 + uint16_t ly; + uint16_t rx; + uint16_t ry; + + uint8_t imu_sample_count; // 0-3 + SwitchImuSample imu_samples[3]; +} SwitchInputState; // Initialize USB state and calibration before entering the main loop. void switch_pro_init(); @@ -20,8 +58,13 @@ void switch_pro_set_input(const SwitchInputState& state); // Drive the Switch Pro USB state machine; call this frequently in the main loop. void switch_pro_task(); +// Convert a packed UART message into controller state (returns true if parsed). +// If out_state is null the parsed state is written directly to the driver. +bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length, SwitchInputState* out_state = nullptr); + // Driver state helpers bool switch_pro_is_ready(); // Optional callback fired when the host sends a rumble payload (the raw 8 rumble bytes). +typedef void (*SwitchRumbleCallback)(const uint8_t rumble_data[8]); void switch_pro_set_rumble_callback(SwitchRumbleCallback cb); diff --git a/switch_protocol.h b/switch_protocol.h deleted file mode 100644 index 28b4c8a..0000000 --- a/switch_protocol.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include - -#include "switch_input.h" - -using SwitchRumbleCallback = void (*)(const uint8_t rumble_data[8]); - -void switch_protocol_init(); -void switch_protocol_set_input(const SwitchInputState& state); -void switch_protocol_task(); -bool switch_protocol_is_ready(); -void switch_protocol_set_rumble_callback(SwitchRumbleCallback callback); diff --git a/switch_uart_protocol.cpp b/switch_uart_protocol.cpp deleted file mode 100644 index 58bb6e9..0000000 --- a/switch_uart_protocol.cpp +++ /dev/null @@ -1,124 +0,0 @@ -#include "switch_uart_protocol.h" - -namespace { - -SwitchInputState make_neutral_state() { - SwitchInputState state{}; - state.lx = SWITCH_PRO_JOYSTICK_MID; - state.ly = SWITCH_PRO_JOYSTICK_MID; - state.rx = SWITCH_PRO_JOYSTICK_MID; - state.ry = SWITCH_PRO_JOYSTICK_MID; - state.imu_sample_count = 0; - return state; -} - -} // namespace - -bool switch_uart_decode_input_frame( - const uint8_t* packet, - uint8_t length, - SwitchInputState* out_state) { - if (length < 12) { - return false; - } - if (packet[0] != 0xAA) { - return false; - } - if (packet[1] != 0x02) { - return false; - } - - const uint8_t payload_len = packet[2]; - if (static_cast(payload_len) + 4u != length) { - return false; - } - - uint16_t sum = 0; - for (uint16_t index = 0; index < static_cast(3u + payload_len); ++index) { - sum += packet[index]; - } - if ((sum & 0xFF) != packet[length - 1]) { - return false; - } - - if (payload_len < 8) { - return false; - } - - const uint16_t buttons = static_cast(packet[3]) | - (static_cast(packet[4]) << 8); - const uint8_t hat = packet[5]; - const uint8_t lx = packet[6]; - const uint8_t ly = packet[7]; - const uint8_t rx = packet[8]; - const uint8_t ry = packet[9]; - uint8_t imu_count = packet[10]; - if (imu_count > 3) { - imu_count = 3; - } - - const uint16_t required_payload_len = - static_cast(8u + static_cast(imu_count) * 12u); - if (payload_len < required_payload_len) { - return false; - } - - const auto expand_axis = [](uint8_t value) -> uint16_t { - return static_cast(value) << 8 | value; - }; - const auto read_int16 = [](const uint8_t* source) -> int16_t { - return static_cast( - static_cast(source[0]) | - (static_cast(source[1]) << 8)); - }; - - SwitchInputState state = make_neutral_state(); - state.imu_sample_count = imu_count; - for (uint8_t index = 0; index < imu_count; ++index) { - const uint8_t* base = &packet[11 + index * 12]; - state.imu_samples[index].accel_x = read_int16(base); - state.imu_samples[index].accel_y = read_int16(base + 2); - state.imu_samples[index].accel_z = read_int16(base + 4); - state.imu_samples[index].gyro_x = read_int16(base + 6); - state.imu_samples[index].gyro_y = read_int16(base + 8); - state.imu_samples[index].gyro_z = read_int16(base + 10); - } - - switch (hat) { - case SWITCH_PRO_HAT_UP: state.dpad_up = true; break; - case SWITCH_PRO_HAT_UPRIGHT: state.dpad_up = true; state.dpad_right = true; break; - case SWITCH_PRO_HAT_RIGHT: state.dpad_right = true; break; - case SWITCH_PRO_HAT_DOWNRIGHT: state.dpad_down = true; state.dpad_right = true; break; - case SWITCH_PRO_HAT_DOWN: state.dpad_down = true; break; - case SWITCH_PRO_HAT_DOWNLEFT: state.dpad_down = true; state.dpad_left = true; break; - case SWITCH_PRO_HAT_LEFT: state.dpad_left = true; break; - case SWITCH_PRO_HAT_UPLEFT: state.dpad_up = true; state.dpad_left = true; break; - default: break; - } - - state.button_y = buttons & SWITCH_PRO_MASK_Y; - state.button_x = buttons & SWITCH_PRO_MASK_X; - state.button_b = buttons & SWITCH_PRO_MASK_B; - state.button_a = buttons & SWITCH_PRO_MASK_A; - state.button_r = buttons & SWITCH_PRO_MASK_R; - state.button_zr = buttons & SWITCH_PRO_MASK_ZR; - state.button_plus = buttons & SWITCH_PRO_MASK_PLUS; - state.button_minus = buttons & SWITCH_PRO_MASK_MINUS; - state.button_r3 = buttons & SWITCH_PRO_MASK_R3; - state.button_l3 = buttons & SWITCH_PRO_MASK_L3; - state.button_home = buttons & SWITCH_PRO_MASK_HOME; - state.button_capture = buttons & SWITCH_PRO_MASK_CAPTURE; - state.button_zl = buttons & SWITCH_PRO_MASK_ZL; - state.button_l = buttons & SWITCH_PRO_MASK_L; - - state.lx = expand_axis(lx); - state.ly = expand_axis(ly); - state.rx = expand_axis(rx); - state.ry = expand_axis(ry); - - if (!out_state) { - return false; - } - *out_state = state; - return true; -} diff --git a/switch_uart_protocol.h b/switch_uart_protocol.h deleted file mode 100644 index 8fc8d79..0000000 --- a/switch_uart_protocol.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include - -#include "switch_input.h" - -bool switch_uart_decode_input_frame( - const uint8_t* packet, - uint8_t length, - SwitchInputState* out_state); diff --git a/tests/firmware/CMakeLists.txt b/tests/firmware/CMakeLists.txt deleted file mode 100644 index 2d26b4d..0000000 --- a/tests/firmware/CMakeLists.txt +++ /dev/null @@ -1,24 +0,0 @@ -cmake_minimum_required(VERSION 3.13) - -project(switch-pico-firmware-tests LANGUAGES CXX) - -enable_testing() - -add_executable(switch-pico-firmware-tests - ../../switch2_commands.cpp - ../../switch2_descriptors.cpp - ../../switch2_reports.cpp - ../../switch_uart_protocol.cpp - test_main.cpp - test_legacy_descriptors.cpp - test_switch2_commands.cpp - test_switch2_descriptors.cpp - test_switch2_reports.cpp - test_switch_input.cpp - test_switch_uart_protocol.cpp -) - -target_compile_features(switch-pico-firmware-tests PRIVATE cxx_std_17) -target_include_directories(switch-pico-firmware-tests PRIVATE ../..) - -add_test(NAME switch-pico-firmware-tests COMMAND switch-pico-firmware-tests) diff --git a/tests/firmware/test_legacy_descriptors.cpp b/tests/firmware/test_legacy_descriptors.cpp deleted file mode 100644 index 47c0064..0000000 --- a/tests/firmware/test_legacy_descriptors.cpp +++ /dev/null @@ -1,173 +0,0 @@ -#include "test_support.h" - -#include -#include -#include - -#include "../../switch_pro_descriptors.h" -#include "../../switch_pro_bounds.h" - -namespace { - -template -bool bytes_equal( - const uint8_t (&actual)[ActualSize], - const uint8_t (&expected)[ExpectedSize]) { - return ActualSize == ExpectedSize && - std::memcmp(actual, expected, ExpectedSize) == 0; -} - -bool legacy_device_descriptor_matches_exact_bytes() { - // Given: the captured legacy device descriptor bytes. - static constexpr uint8_t expected[] = { - 0x12, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x40, 0x7E, - 0x05, 0x09, 0x20, 0x10, 0x02, 0x01, 0x02, 0x03, 0x01, - }; - - // When: the compiled legacy descriptor is inspected. - // Then: its identity and all 18 bytes remain unchanged. - CHECK(sizeof(switch_pro_device_descriptor) == 18); - CHECK(bytes_equal(switch_pro_device_descriptor, expected)); - return true; -} - -bool legacy_configuration_descriptor_matches_exact_bytes() { - // Given: the captured single-interface legacy configuration. - static constexpr uint8_t expected[] = { - 0x09, 0x02, 0x29, 0x00, 0x01, 0x01, 0x00, 0xA0, 0xFA, - 0x09, 0x04, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, - 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0xCB, 0x00, - 0x07, 0x05, 0x81, 0x03, 0x40, 0x00, 0x08, - 0x07, 0x05, 0x01, 0x03, 0x40, 0x00, 0x08, - }; - - // When: the compiled legacy configuration is inspected. - // Then: the interface and both interrupt endpoints remain byte-identical. - CHECK(sizeof(switch_pro_configuration_descriptor) == 41); - CHECK(bytes_equal(switch_pro_configuration_descriptor, expected)); - return true; -} - -bool legacy_hid_report_descriptor_matches_exact_bytes() { - // Given: the complete captured 203-byte legacy HID report descriptor. - static constexpr uint8_t expected[] = { - 0x05, 0x01, 0x15, 0x00, 0x09, 0x04, 0xA1, 0x01, 0x85, 0x30, 0x05, 0x01, 0x05, 0x09, 0x19, 0x01, - 0x29, 0x0A, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x0A, 0x55, 0x00, 0x65, 0x00, 0x81, 0x02, - 0x05, 0x09, 0x19, 0x0B, 0x29, 0x0E, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x04, 0x81, 0x02, - 0x75, 0x01, 0x95, 0x02, 0x81, 0x03, 0x0B, 0x01, 0x00, 0x01, 0x00, 0xA1, 0x00, 0x0B, 0x30, 0x00, - 0x01, 0x00, 0x0B, 0x31, 0x00, 0x01, 0x00, 0x0B, 0x32, 0x00, 0x01, 0x00, 0x0B, 0x35, 0x00, 0x01, - 0x00, 0x15, 0x00, 0x27, 0xFF, 0xFF, 0x00, 0x00, 0x75, 0x10, 0x95, 0x04, 0x81, 0x02, 0xC0, 0x0B, - 0x39, 0x00, 0x01, 0x00, 0x15, 0x00, 0x25, 0x07, 0x35, 0x00, 0x46, 0x3B, 0x01, 0x65, 0x14, 0x75, - 0x04, 0x95, 0x01, 0x81, 0x02, 0x05, 0x09, 0x19, 0x0F, 0x29, 0x12, 0x15, 0x00, 0x25, 0x01, 0x75, - 0x01, 0x95, 0x04, 0x81, 0x02, 0x75, 0x08, 0x95, 0x34, 0x81, 0x03, 0x06, 0x00, 0xFF, 0x85, 0x21, - 0x09, 0x01, 0x75, 0x08, 0x95, 0x3F, 0x81, 0x03, 0x85, 0x81, 0x09, 0x02, 0x75, 0x08, 0x95, 0x3F, - 0x81, 0x03, 0x85, 0x01, 0x09, 0x03, 0x75, 0x08, 0x95, 0x3F, 0x91, 0x83, 0x85, 0x10, 0x09, 0x04, - 0x75, 0x08, 0x95, 0x3F, 0x91, 0x83, 0x85, 0x80, 0x09, 0x05, 0x75, 0x08, 0x95, 0x3F, 0x91, 0x83, - 0x85, 0x82, 0x09, 0x06, 0x75, 0x08, 0x95, 0x3F, 0x91, 0x83, 0xC0, - }; - - // When: the compiled legacy HID descriptor is inspected. - // Then: every report item remains byte-identical. - CHECK(sizeof(switch_pro_report_descriptor) == 203); - CHECK(bytes_equal(switch_pro_report_descriptor, expected)); - return true; -} - -bool legacy_string_descriptors_match_exact_bytes() { - // Given: the legacy language, manufacturer, product, and serial strings. - static constexpr uint8_t language[] = {0x09, 0x04}; - static constexpr uint8_t manufacturer[] = "Nintendo Co., Ltd."; - static constexpr uint8_t product[] = "Pro Controller"; - static constexpr uint8_t version[] = "000000000001"; - - // When: the compiled string tables are inspected. - // Then: every string byte and terminator remains unchanged. - CHECK(bytes_equal(switch_pro_string_language, language)); - CHECK(bytes_equal(switch_pro_string_manufacturer, manufacturer)); - CHECK(bytes_equal(switch_pro_string_product, product)); - CHECK(bytes_equal(switch_pro_string_version, version)); - return true; -} - -bool legacy_output_classifier_rejects_invalid_report_framing() { - const uint8_t report[] = {0x01, 0x00}; - CHECK(switch_pro_classify_output_report(nullptr, 2) == SwitchProOutputReportKind::Ignore); - CHECK(switch_pro_classify_output_report(report, 0) == SwitchProOutputReportKind::Ignore); - CHECK(switch_pro_classify_output_report(report, 1) == SwitchProOutputReportKind::Ignore); - CHECK(switch_pro_classify_output_report(report, 65) == SwitchProOutputReportKind::Ignore); - return true; -} - -bool legacy_feature_reports_reject_short_payloads_and_accept_bounds() { - const uint8_t report[] = {0x01, 0x00}; - CHECK(switch_pro_classify_output_report(report, 15) == SwitchProOutputReportKind::Ignore); - CHECK(switch_pro_classify_output_report(report, 16) == SwitchProOutputReportKind::Feature); - CHECK(switch_pro_classify_output_report(report, 64) == SwitchProOutputReportKind::Feature); - return true; -} - -bool legacy_configuration_and_rumble_reports_reject_short_payloads() { - const uint8_t configuration[] = {0x80, 0x00}; - const uint8_t rumble[] = {0x10, 0x00}; - const uint8_t noop[] = {0x00, 0x00}; - CHECK(switch_pro_classify_output_report(configuration, 1) == SwitchProOutputReportKind::Ignore); - CHECK(switch_pro_classify_output_report(configuration, 2) == SwitchProOutputReportKind::Configuration); - CHECK(switch_pro_classify_output_report(rumble, 9) == SwitchProOutputReportKind::Ignore); - CHECK(switch_pro_classify_output_report(rumble, 10) == SwitchProOutputReportKind::Rumble); - CHECK(switch_pro_classify_output_report(noop, 2) == SwitchProOutputReportKind::Noop); - return true; -} - -bool legacy_spi_read_rejects_payload_overflow_at_forty_five_bytes() { - CHECK(switch_pro_spi_read_size_fits(0)); - CHECK(switch_pro_spi_read_size_fits(44)); - CHECK(!switch_pro_spi_read_size_fits(45)); - CHECK(!switch_pro_spi_read_size_fits(255)); - return true; -} - -bool legacy_flash_read_copies_in_range_data_through_exact_end() { - const uint8_t source[] = {1, 2, 3, 4}; - uint8_t destination[] = {0xAA, 0xAA, 0xAA, 0xAA}; - CHECK(switch_pro_fill_flash_read(destination, 4, source, 4, 0, 4) == 4); - CHECK(destination[0] == 1 && destination[3] == 4); - return true; -} - -bool legacy_flash_read_prefills_partial_source_end_with_ff() { - const uint8_t source[] = {1, 2}; - uint8_t destination[] = {0xAA, 0xAA, 0xAA, 0xAA}; - CHECK(switch_pro_fill_flash_read(destination, 4, source, 2, 0, 4) == 4); - CHECK(destination[0] == 1 && destination[1] == 2); - CHECK(destination[2] == 0xFF && destination[3] == 0xFF); - return true; -} - -bool legacy_flash_read_leaves_canaries_on_invalid_range_or_null_source() { - const uint8_t source[] = {1, 2}; - uint8_t destination[] = {0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}; - CHECK(switch_pro_fill_flash_read(destination + 1, 4, source, 2, 3, 4) == 4); - CHECK(destination[0] == 0xAA && destination[5] == 0xAA); - CHECK(destination[1] == 0xFF && destination[4] == 0xFF); - CHECK(switch_pro_fill_flash_read(destination + 1, 4, nullptr, 2, 0, 4) == 4); - CHECK(destination[0] == 0xAA && destination[5] == 0xAA); - CHECK(destination[1] == 0xFF && destination[4] == 0xFF); - CHECK(switch_pro_fill_flash_read(nullptr, 4, source, 2, 0, 4) == 0); - return true; -} - -} // namespace - -void run_legacy_descriptor_tests(TestRunner& runner) { - runner.run("legacy device descriptor exact bytes", legacy_device_descriptor_matches_exact_bytes); - runner.run("legacy configuration descriptor exact bytes", legacy_configuration_descriptor_matches_exact_bytes); - runner.run("legacy HID report descriptor exact bytes", legacy_hid_report_descriptor_matches_exact_bytes); - runner.run("legacy string descriptors exact bytes", legacy_string_descriptors_match_exact_bytes); - runner.run("legacy output classifier rejects invalid report framing", legacy_output_classifier_rejects_invalid_report_framing); - runner.run("legacy feature reports reject short payloads and accept bounds", legacy_feature_reports_reject_short_payloads_and_accept_bounds); - runner.run("legacy configuration and rumble reports reject short payloads", legacy_configuration_and_rumble_reports_reject_short_payloads); - runner.run("legacy SPI read rejects payload overflow at 45 bytes", legacy_spi_read_rejects_payload_overflow_at_forty_five_bytes); - runner.run("legacy flash read copies in-range data through exact end", legacy_flash_read_copies_in_range_data_through_exact_end); - runner.run("legacy flash read prefills partial source end with FF", legacy_flash_read_prefills_partial_source_end_with_ff); - runner.run("legacy flash read leaves canaries on invalid range or null source", legacy_flash_read_leaves_canaries_on_invalid_range_or_null_source); -} diff --git a/tests/firmware/test_main.cpp b/tests/firmware/test_main.cpp deleted file mode 100644 index d8b7ba8..0000000 --- a/tests/firmware/test_main.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include "test_support.h" - -int main() { - TestRunner runner; - run_legacy_descriptor_tests(runner); - run_switch2_command_tests(runner); - run_switch2_descriptor_tests(runner); - run_switch2_report_tests(runner); - run_switch_input_tests(runner); - run_switch_uart_protocol_tests(runner); - return runner.result(); -} diff --git a/tests/firmware/test_support.h b/tests/firmware/test_support.h deleted file mode 100644 index 8b47685..0000000 --- a/tests/firmware/test_support.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include - -class TestRunner { -public: - void run(const char* name, bool (*test)()) { - if (test()) { - std::printf("PASS %s\n", name); - return; - } - - ++failures_; - std::printf("FAIL %s\n", name); - } - - int result() const { - return failures_ == 0 ? 0 : 1; - } - -private: - int failures_ = 0; -}; - -#define CHECK(condition) \ - do { \ - if (!(condition)) { \ - std::fprintf(stderr, " %s:%d: %s\n", __FILE__, __LINE__, #condition); \ - return false; \ - } \ - } while (false) - -void run_legacy_descriptor_tests(TestRunner& runner); -void run_switch2_command_tests(TestRunner& runner); -void run_switch2_descriptor_tests(TestRunner& runner); -void run_switch2_report_tests(TestRunner& runner); -void run_switch_input_tests(TestRunner& runner); -void run_switch_uart_protocol_tests(TestRunner& runner); diff --git a/tests/firmware/test_switch2_commands.cpp b/tests/firmware/test_switch2_commands.cpp deleted file mode 100644 index c9bb6d2..0000000 --- a/tests/firmware/test_switch2_commands.cpp +++ /dev/null @@ -1,185 +0,0 @@ -#include "test_support.h" - -#include -#include -#include -#include - -#include "../../switch2_commands.h" - -namespace { - -bool switch2_classifies_captured_report_selection_requests() { - // Given: captured common and Pro report-selection request vectors. - static constexpr uint8_t select_05[] = { - 0x03, 0x91, 0x00, 0x0A, 0x00, 0x04, 0x00, 0x00, - 0x05, 0x00, 0x00, 0x00, - }; - static constexpr uint8_t select_09[] = { - 0x03, 0x91, 0x00, 0x0A, 0x00, 0x04, 0x00, 0x00, - 0x09, 0x00, 0x00, 0x00, - }; - - // When/Then: each captured vector selects only its represented report. - CHECK(switch2_classify_vendor_request(select_05, sizeof(select_05)) == - Switch2VendorCommand::SelectReport05); - CHECK(switch2_classify_vendor_request(select_09, sizeof(select_09)) == - Switch2VendorCommand::SelectReport09); - return true; -} - -bool switch2_builds_exact_report_selection_ack() { - // Given: the two supported report-selection classifications. - static constexpr uint8_t expected[] = { - 0x03, 0x01, 0x00, 0x0A, 0x00, 0xF8, 0x00, 0x00, - }; - std::array output{}; - - // When: either response is built. Then: both equal the captured ACK. - CHECK(switch2_build_vendor_response( - Switch2VendorCommand::SelectReport05, output.data(), output.size()) == - sizeof(expected)); - CHECK(std::memcmp(output.data(), expected, sizeof(expected)) == 0); - output.fill(0); - CHECK(switch2_build_vendor_response( - Switch2VendorCommand::SelectReport09, output.data(), output.size()) == - sizeof(expected)); - CHECK(std::memcmp(output.data(), expected, sizeof(expected)) == 0); - return true; -} - -bool switch2_classifies_captured_and_opaque_usb_init_requests() { - // Given: the pinned vector and another opaque host-address payload. - static constexpr uint8_t captured[] = { - 0x03, 0x91, 0x00, 0x0D, 0x00, 0x08, 0x00, 0x00, - 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - }; - static constexpr uint8_t opaque_address[] = { - 0x03, 0x91, 0x00, 0x0D, 0x00, 0x08, 0x00, 0x00, - 0x01, 0xA5, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, - }; - - // When/Then: both structurally valid requests initialize USB. - CHECK(switch2_classify_vendor_request(captured, sizeof(captured)) == - Switch2VendorCommand::InitializeUsb); - CHECK(switch2_classify_vendor_request(opaque_address, sizeof(opaque_address)) == - Switch2VendorCommand::InitializeUsb); - return true; -} - -bool switch2_builds_exact_usb_init_ack() { - // Given: the captured initialization acknowledgement. - static constexpr uint8_t expected[] = { - 0x03, 0x01, 0x00, 0x0D, 0x00, 0xF8, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, - }; - std::array output{}; - - // When: the initialization response is built. Then: every byte is exact. - CHECK(switch2_build_vendor_response( - Switch2VendorCommand::InitializeUsb, output.data(), output.size()) == - sizeof(expected)); - CHECK(std::memcmp(output.data(), expected, sizeof(expected)) == 0); - return true; -} - -bool switch2_rejects_malformed_header_and_length_fields() { - // Given: a structurally valid selection request as the mutation baseline. - static constexpr uint8_t valid[] = { - 0x03, 0x91, 0x00, 0x0A, 0x00, 0x04, 0x00, 0x00, - 0x05, 0x00, 0x00, 0x00, - }; - - // When/Then: truncation, extension, and every fixed header violation reject. - CHECK(switch2_classify_vendor_request(nullptr, 0) == Switch2VendorCommand::Unsupported); - for (std::size_t length = 0; length < sizeof(valid); ++length) { - CHECK(switch2_classify_vendor_request(valid, length) == - Switch2VendorCommand::Unsupported); - } - std::array extended{}; - std::memcpy(extended.data(), valid, sizeof(valid)); - CHECK(switch2_classify_vendor_request(extended.data(), extended.size()) == - Switch2VendorCommand::Unsupported); - for (uint8_t offset : {0, 1, 2, 4, 6, 7}) { - std::array malformed{}; - std::memcpy(malformed.data(), valid, sizeof(valid)); - ++malformed[offset]; - CHECK(switch2_classify_vendor_request(malformed.data(), malformed.size()) == - Switch2VendorCommand::Unsupported); - } - for (uint8_t declared_length : {0x03, 0x05}) { - std::array malformed{}; - std::memcpy(malformed.data(), valid, sizeof(valid)); - malformed[5] = declared_length; - CHECK(switch2_classify_vendor_request(malformed.data(), malformed.size()) == - Switch2VendorCommand::Unsupported); - } - return true; -} - -bool switch2_rejects_unsupported_commands_and_payloads() { - // Given: valid framing mutated to unsupported subcommands and payloads. - std::array request = { - 0x03, 0x91, 0x00, 0x0A, 0x00, 0x04, 0x00, 0x00, - 0x05, 0x00, 0x00, 0x00, - }; - - // When/Then: no command outside the captured subset is classified. - request[3] = 0x03; - CHECK(switch2_classify_vendor_request(request.data(), request.size()) == - Switch2VendorCommand::Unsupported); - request[3] = 0x0A; - request[8] = 0x08; - CHECK(switch2_classify_vendor_request(request.data(), request.size()) == - Switch2VendorCommand::Unsupported); - request[8] = 0x05; - request[9] = 0x01; - CHECK(switch2_classify_vendor_request(request.data(), request.size()) == - Switch2VendorCommand::Unsupported); - - std::array init = { - 0x03, 0x91, 0x00, 0x0D, 0x00, 0x08, 0x00, 0x00, - 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, - }; - CHECK(switch2_classify_vendor_request(init.data(), init.size()) == - Switch2VendorCommand::Unsupported); - return true; -} - -bool switch2_response_builder_rejects_unsupported_or_small_outputs() { - // Given: a sentinel output buffer and every unsupported capacity. - std::array output{}; - - // When/Then: unsupported, null, and short outputs produce no bytes or writes. - output.fill(0xA5); - CHECK(switch2_build_vendor_response( - Switch2VendorCommand::Unsupported, output.data(), output.size()) == 0); - for (uint8_t byte : output) CHECK(byte == 0xA5); - CHECK(switch2_build_vendor_response( - Switch2VendorCommand::InitializeUsb, nullptr, output.size()) == 0); - for (std::size_t capacity = 0; capacity < 8; ++capacity) { - output.fill(0xA5); - CHECK(switch2_build_vendor_response( - Switch2VendorCommand::SelectReport05, output.data(), capacity) == 0); - for (uint8_t byte : output) CHECK(byte == 0xA5); - } - for (std::size_t capacity = 0; capacity < 12; ++capacity) { - output.fill(0xA5); - CHECK(switch2_build_vendor_response( - Switch2VendorCommand::InitializeUsb, output.data(), capacity) == 0); - for (uint8_t byte : output) CHECK(byte == 0xA5); - } - return true; -} - -} // namespace - -void run_switch2_command_tests(TestRunner& runner) { - runner.run("Switch 2 captured report selections", switch2_classifies_captured_report_selection_requests); - runner.run("Switch 2 report selection ACK", switch2_builds_exact_report_selection_ack); - runner.run("Switch 2 captured USB initialization", switch2_classifies_captured_and_opaque_usb_init_requests); - runner.run("Switch 2 USB initialization ACK", switch2_builds_exact_usb_init_ack); - runner.run("Switch 2 malformed command framing", switch2_rejects_malformed_header_and_length_fields); - runner.run("Switch 2 unsupported commands", switch2_rejects_unsupported_commands_and_payloads); - runner.run("Switch 2 response capacity", switch2_response_builder_rejects_unsupported_or_small_outputs); -} diff --git a/tests/firmware/test_switch2_descriptors.cpp b/tests/firmware/test_switch2_descriptors.cpp deleted file mode 100644 index 2530ca5..0000000 --- a/tests/firmware/test_switch2_descriptors.cpp +++ /dev/null @@ -1,166 +0,0 @@ -#include "test_support.h" - -#include -#include -#include - -#include "../../switch2_descriptors.h" - -namespace { - -bool bytes_equal( - const uint8_t* actual, - std::size_t actual_length, - const uint8_t* expected, - std::size_t expected_length) { - return actual_length == expected_length && - std::memcmp(actual, expected, expected_length) == 0; -} - -bool switch2_descriptor_bytes_match_pinned_capture_subset() { - // Given: captured Pro Controller 2 bytes with the documented subset edits. - static constexpr uint8_t expected_device[] = { - 0x12, 0x01, 0x00, 0x02, 0xEF, 0x02, 0x01, 0x40, 0x7E, - 0x05, 0x69, 0x20, 0x00, 0x02, 0x01, 0x02, 0x03, 0x01, - }; - static constexpr uint8_t expected_configuration[] = { - 0x09, 0x02, 0x50, 0x00, 0x02, 0x01, 0x00, 0xC0, 0xFA, - 0x08, 0x0B, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, - 0x09, 0x04, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, - 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x61, 0x00, - 0x07, 0x05, 0x81, 0x03, 0x40, 0x00, 0x04, - 0x07, 0x05, 0x01, 0x03, 0x40, 0x00, 0x04, - 0x08, 0x0B, 0x01, 0x01, 0xFF, 0x00, 0x00, 0x00, - 0x09, 0x04, 0x01, 0x00, 0x02, 0xFF, 0x00, 0x00, 0x00, - 0x07, 0x05, 0x02, 0x02, 0x40, 0x00, 0x00, - 0x07, 0x05, 0x82, 0x02, 0x40, 0x00, 0x00, - }; - static constexpr uint8_t expected_hid[] = { - 0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x05, 0x05, 0xFF, 0x09, 0x01, 0x15, 0x00, 0x26, 0xFF, - 0x00, 0x95, 0x3F, 0x75, 0x08, 0x81, 0x02, 0x85, 0x09, 0x09, 0x01, 0x95, 0x02, 0x81, 0x02, 0x05, - 0x09, 0x19, 0x01, 0x29, 0x15, 0x25, 0x01, 0x95, 0x15, 0x75, 0x01, 0x81, 0x02, 0x95, 0x01, 0x75, - 0x03, 0x81, 0x03, 0x05, 0x01, 0x09, 0x01, 0xA1, 0x00, 0x09, 0x30, 0x09, 0x31, 0x09, 0x33, 0x09, - 0x35, 0x26, 0xFF, 0x0F, 0x95, 0x04, 0x75, 0x0C, 0x81, 0x02, 0xC0, 0x05, 0xFF, 0x09, 0x02, 0x26, - 0xFF, 0x00, 0x95, 0x34, 0x75, 0x08, 0x81, 0x02, 0x85, 0x02, 0x09, 0x01, 0x95, 0x3F, 0x91, 0x02, - 0xC0, - }; - - // When/Then: all exported descriptor bytes and lengths match exactly. - CHECK(bytes_equal(switch2_device_descriptor, switch2_device_descriptor_length, - expected_device, sizeof(expected_device))); - CHECK(bytes_equal(switch2_configuration_descriptor, switch2_configuration_descriptor_length, - expected_configuration, sizeof(expected_configuration))); - CHECK(bytes_equal(switch2_hid_report_descriptor, switch2_hid_report_descriptor_length, - expected_hid, sizeof(expected_hid))); - return true; -} - -bool switch2_configuration_iterates_two_interfaces_and_four_endpoints() { - // Given: the deliberately reduced configuration descriptor. - std::size_t offset = 0; - uint8_t interface_count = 0; - uint8_t endpoint_count = 0; - const uint8_t expected_endpoints[][5] = { - {0x81, 0x03, 0x40, 0x00, 0x04}, - {0x01, 0x03, 0x40, 0x00, 0x04}, - {0x02, 0x02, 0x40, 0x00, 0x00}, - {0x82, 0x02, 0x40, 0x00, 0x00}, - }; - - // When: every USB descriptor is iterated by bLength. - while (offset < switch2_configuration_descriptor_length) { - const uint8_t length = switch2_configuration_descriptor[offset]; - CHECK(length >= 2); - CHECK(offset + length <= switch2_configuration_descriptor_length); - const uint8_t type = switch2_configuration_descriptor[offset + 1]; - if (type == 0x04) { - CHECK(length == 9); - CHECK(switch2_configuration_descriptor[offset + 5] != 0x01); - CHECK(switch2_configuration_descriptor[offset + 8] == 0x00); - ++interface_count; - } else if (type == 0x05) { - CHECK(length == 7); - CHECK(endpoint_count < 4); - CHECK(std::memcmp(&switch2_configuration_descriptor[offset + 2], - expected_endpoints[endpoint_count], 5) == 0); - ++endpoint_count; - } - offset += length; - } - - // Then: iteration is exact, contains only HID/vendor, and omits audio. - CHECK(offset == switch2_configuration_descriptor_length); - CHECK(switch2_configuration_descriptor[6] == 0x00); - CHECK(interface_count == 2); - CHECK(endpoint_count == 4); - return true; -} - -bool switch2_hid_reports_are_exactly_sixty_three_payload_bytes() { - // Given: the captured HID report descriptor. - uint32_t report_size = 0; - uint32_t report_count = 0; - uint32_t report_id = 0; - uint32_t input_05_bits = 0; - uint32_t input_09_bits = 0; - uint32_t output_02_bits = 0; - - // When: HID short items are parsed and report fields accumulated. - for (std::size_t offset = 0; offset < switch2_hid_report_descriptor_length;) { - const uint8_t prefix = switch2_hid_report_descriptor[offset++]; - CHECK(prefix != 0xFE); - const uint8_t size_code = prefix & 0x03; - const uint8_t data_size = size_code == 3 ? 4 : size_code; - CHECK(offset + data_size <= switch2_hid_report_descriptor_length); - uint32_t value = 0; - for (uint8_t index = 0; index < data_size; ++index) { - value |= static_cast(switch2_hid_report_descriptor[offset + index]) << (8 * index); - } - offset += data_size; - const uint8_t type = (prefix >> 2) & 0x03; - const uint8_t tag = (prefix >> 4) & 0x0F; - if (type == 1 && tag == 7) report_size = value; - if (type == 1 && tag == 8) report_id = value; - if (type == 1 && tag == 9) report_count = value; - if (type == 0 && tag == 8 && report_id == 0x05) input_05_bits += report_size * report_count; - if (type == 0 && tag == 8 && report_id == 0x09) input_09_bits += report_size * report_count; - if (type == 0 && tag == 9 && report_id == 0x02) output_02_bits += report_size * report_count; - } - - // Then: each supported transfer is 63 payload bytes plus its report ID. - CHECK(input_05_bits == 63 * 8); - CHECK(input_09_bits == 63 * 8); - CHECK(output_02_bits == 63 * 8); - return true; -} - -bool switch2_string_bytes_match_pinned_capture() { - // Given/When: the known language and ASCII strings are inspected. - static constexpr uint8_t language[] = {0x09, 0x04}; - static constexpr uint8_t manufacturer[] = "Nintendo"; - static constexpr uint8_t product[] = "Switch 2 Pro Controller"; - static constexpr uint8_t serial[] = "00"; - - // Then: exported lengths exclude the C terminator and bytes remain exact. - CHECK(bytes_equal(switch2_string_language, switch2_string_language_length, - language, sizeof(language))); - CHECK(bytes_equal(switch2_string_manufacturer, switch2_string_manufacturer_length, - manufacturer, sizeof(manufacturer) - 1)); - CHECK(bytes_equal(switch2_string_product, switch2_string_product_length, - product, sizeof(product) - 1)); - CHECK(bytes_equal(switch2_string_serial, switch2_string_serial_length, - serial, sizeof(serial) - 1)); - CHECK(switch2_string_manufacturer[switch2_string_manufacturer_length] == 0); - CHECK(switch2_string_product[switch2_string_product_length] == 0); - CHECK(switch2_string_serial[switch2_string_serial_length] == 0); - return true; -} - -} // namespace - -void run_switch2_descriptor_tests(TestRunner& runner) { - runner.run("Switch 2 descriptor exact bytes", switch2_descriptor_bytes_match_pinned_capture_subset); - runner.run("Switch 2 configuration iteration", switch2_configuration_iterates_two_interfaces_and_four_endpoints); - runner.run("Switch 2 HID report sizes", switch2_hid_reports_are_exactly_sixty_three_payload_bytes); - runner.run("Switch 2 string bytes", switch2_string_bytes_match_pinned_capture); -} diff --git a/tests/firmware/test_switch2_reports.cpp b/tests/firmware/test_switch2_reports.cpp deleted file mode 100644 index 1496bc7..0000000 --- a/tests/firmware/test_switch2_reports.cpp +++ /dev/null @@ -1,186 +0,0 @@ -#include "test_support.h" - -#include -#include - -#include "../../switch2_reports.h" - -namespace { - -SwitchInputState neutral_state() { - SwitchInputState state{}; - state.lx = SWITCH_PRO_JOYSTICK_MID; - state.ly = SWITCH_PRO_JOYSTICK_MID; - state.rx = SWITCH_PRO_JOYSTICK_MID; - state.ry = SWITCH_PRO_JOYSTICK_MID; - return state; -} - -bool bytes_are_zero( - const std::array& payload, - std::size_t begin, - std::size_t end) { - for (std::size_t index = begin; index < end; ++index) { - if (payload[index] != 0) return false; - } - return true; -} - -bool switch2_neutral_reports_pack_documented_constants() { - // Given: neutral normalized input and distinct counters. - const SwitchInputState state = neutral_state(); - - // When: common and Pro reports are built. - const Switch2InputReport common = switch2_build_input_report( - Switch2InputReportId::Common, state, 0x78563412); - const Switch2InputReport pro = switch2_build_input_report( - Switch2InputReportId::Pro, state, 0x1234); - - // Then: IDs, counters, neutral sticks, and documented constants are exact. - CHECK(static_cast(common.id) == 0x05); - CHECK(static_cast(pro.id) == 0x09); - CHECK(common.payload.size() + 1 == 64 && pro.payload.size() + 1 == 64); - CHECK(common.payload[0] == 0x12 && common.payload[1] == 0x34); - CHECK(common.payload[2] == 0x56 && common.payload[3] == 0x78); - CHECK(common.payload[10] == 0xFF && common.payload[11] == 0xF7 && common.payload[12] == 0x7F); - CHECK(common.payload[13] == 0xFF && common.payload[14] == 0xF7 && common.payload[15] == 0x7F); - CHECK(common.payload[0x29] == 0x01); - CHECK(pro.payload[0] == 0x34); - CHECK(pro.payload[1] == 0x01); - CHECK(pro.payload[5] == 0xFF && pro.payload[6] == 0xF7 && pro.payload[7] == 0x7F); - CHECK(pro.payload[8] == 0xFF && pro.payload[9] == 0xF7 && pro.payload[10] == 0x7F); - CHECK(pro.payload[11] == 0x30); - return true; -} - -bool switch2_report_counters_roll_over_deterministically() { - // Given: counters at and beyond the Pro report's eight-bit boundary. - const SwitchInputState state = neutral_state(); - - // When: reports are built at 255 and 256. - const Switch2InputReport common = switch2_build_input_report( - Switch2InputReportId::Common, state, 0xFFFFFFFF); - const Switch2InputReport pro_255 = switch2_build_input_report( - Switch2InputReportId::Pro, state, 255); - const Switch2InputReport pro_256 = switch2_build_input_report( - Switch2InputReportId::Pro, state, 256); - - // Then: common remains LE32 while Pro uses the low eight bits. - CHECK(common.payload[0] == 0xFF && common.payload[1] == 0xFF); - CHECK(common.payload[2] == 0xFF && common.payload[3] == 0xFF); - CHECK(pro_255.payload[0] == 0xFF); - CHECK(pro_256.payload[0] == 0x00); - return true; -} - -bool switch2_reports_map_every_shared_button() { - struct ButtonCase { - bool SwitchInputState::*field; - uint8_t common_offset; - uint8_t common_mask; - uint8_t pro_offset; - uint8_t pro_mask; - }; - static constexpr ButtonCase cases[] = { - {&SwitchInputState::button_y, 4, 0x01, 2, 0x04}, - {&SwitchInputState::button_b, 4, 0x04, 2, 0x01}, - {&SwitchInputState::button_a, 4, 0x08, 2, 0x02}, - {&SwitchInputState::button_x, 4, 0x02, 2, 0x08}, - {&SwitchInputState::button_r, 4, 0x40, 2, 0x10}, - {&SwitchInputState::button_zr, 4, 0x80, 2, 0x20}, - {&SwitchInputState::button_minus, 5, 0x01, 3, 0x40}, - {&SwitchInputState::button_plus, 5, 0x02, 2, 0x40}, - {&SwitchInputState::button_r3, 5, 0x04, 2, 0x80}, - {&SwitchInputState::button_l3, 5, 0x08, 3, 0x80}, - {&SwitchInputState::button_home, 5, 0x10, 4, 0x01}, - {&SwitchInputState::button_capture, 5, 0x20, 4, 0x02}, - {&SwitchInputState::dpad_down, 6, 0x01, 3, 0x01}, - {&SwitchInputState::dpad_up, 6, 0x02, 3, 0x08}, - {&SwitchInputState::dpad_right, 6, 0x04, 3, 0x02}, - {&SwitchInputState::dpad_left, 6, 0x08, 3, 0x04}, - {&SwitchInputState::button_l, 6, 0x40, 3, 0x10}, - {&SwitchInputState::button_zl, 6, 0x80, 3, 0x20}, - }; - - // Given/When: every shared button is built independently in both reports. - for (const ButtonCase& button : cases) { - SwitchInputState state = neutral_state(); - state.*(button.field) = true; - const Switch2InputReport common = switch2_build_input_report( - Switch2InputReportId::Common, state, 0); - const Switch2InputReport pro = switch2_build_input_report( - Switch2InputReportId::Pro, state, 0); - - // Then: only the captured byte and bit for that button is set. - for (uint8_t offset = 4; offset <= 7; ++offset) { - CHECK(common.payload[offset] == - (offset == button.common_offset ? button.common_mask : 0)); - } - for (uint8_t offset = 2; offset <= 4; ++offset) { - CHECK(pro.payload[offset] == - (offset == button.pro_offset ? button.pro_mask : 0)); - } - } - return true; -} - -bool switch2_reports_pack_twelve_bit_stick_extremes() { - // Given: four distinct normalized axis values. - SwitchInputState state{}; - state.lx = 0x0000; - state.ly = 0xFFFF; - state.rx = 0x1234; - state.ry = 0xABCD; - - // When: both report formats are built. - const Switch2InputReport common = switch2_build_input_report( - Switch2InputReportId::Common, state, 0); - const Switch2InputReport pro = switch2_build_input_report( - Switch2InputReportId::Pro, state, 0); - - // Then: axes are reduced and packed in captured 12-bit little-endian form. - const uint8_t expected_left[] = {0x00, 0xF0, 0xFF}; - const uint8_t expected_right[] = {0x23, 0xC1, 0xAB}; - for (uint8_t index = 0; index < 3; ++index) { - CHECK(common.payload[10 + index] == expected_left[index]); - CHECK(common.payload[13 + index] == expected_right[index]); - CHECK(pro.payload[5 + index] == expected_left[index]); - CHECK(pro.payload[8 + index] == expected_right[index]); - } - return true; -} - -bool switch2_reports_zero_unknowns_and_ignore_imu() { - // Given: populated IMU input that has no known Switch 2 packing. - SwitchInputState state = neutral_state(); - state.imu_sample_count = 3; - state.imu_samples[0] = {1, 2, 3, 4, 5, 6}; - state.imu_samples[1] = {7, 8, 9, 10, 11, 12}; - state.imu_samples[2] = {13, 14, 15, 16, 17, 18}; - - // When: both reports are built twice from identical input. - const Switch2InputReport common = switch2_build_input_report( - Switch2InputReportId::Common, state, 7); - const Switch2InputReport common_again = switch2_build_input_report( - Switch2InputReportId::Common, state, 7); - const Switch2InputReport pro = switch2_build_input_report( - Switch2InputReportId::Pro, state, 7); - - // Then: unknown power/sensor/motion fields stay deterministic and zero. - CHECK(common.payload == common_again.payload); - CHECK(bytes_are_zero(common.payload, 8, 10)); - CHECK(bytes_are_zero(common.payload, 16, 0x29)); - CHECK(bytes_are_zero(common.payload, 0x2A, 63)); - CHECK(bytes_are_zero(pro.payload, 12, 63)); - return true; -} - -} // namespace - -void run_switch2_report_tests(TestRunner& runner) { - runner.run("Switch 2 neutral report constants", switch2_neutral_reports_pack_documented_constants); - runner.run("Switch 2 report counter rollover", switch2_report_counters_roll_over_deterministically); - runner.run("Switch 2 report button mapping", switch2_reports_map_every_shared_button); - runner.run("Switch 2 report stick packing", switch2_reports_pack_twelve_bit_stick_extremes); - runner.run("Switch 2 report unknown fields", switch2_reports_zero_unknowns_and_ignore_imu); -} diff --git a/tests/firmware/test_switch_input.cpp b/tests/firmware/test_switch_input.cpp deleted file mode 100644 index 11781e0..0000000 --- a/tests/firmware/test_switch_input.cpp +++ /dev/null @@ -1,52 +0,0 @@ -#include "test_support.h" - -#include - -#include "../../switch_input.h" - -namespace { - -bool switch_input_constants_match_legacy_values() { - // Given/When: the shared input constants are compiled after extraction. - // Then: every UART-visible legacy value remains unchanged. - CHECK(SWITCH_PRO_HAT_UP == 0x00 && SWITCH_PRO_HAT_UPRIGHT == 0x01); - CHECK(SWITCH_PRO_HAT_RIGHT == 0x02 && SWITCH_PRO_HAT_DOWNRIGHT == 0x03); - CHECK(SWITCH_PRO_HAT_DOWN == 0x04 && SWITCH_PRO_HAT_DOWNLEFT == 0x05); - CHECK(SWITCH_PRO_HAT_LEFT == 0x06 && SWITCH_PRO_HAT_UPLEFT == 0x07); - CHECK(SWITCH_PRO_HAT_NOTHING == 0x08); - CHECK(SWITCH_PRO_MASK_Y == (1U << 0) && SWITCH_PRO_MASK_B == (1U << 1)); - CHECK(SWITCH_PRO_MASK_A == (1U << 2) && SWITCH_PRO_MASK_X == (1U << 3)); - CHECK(SWITCH_PRO_MASK_L == (1U << 4) && SWITCH_PRO_MASK_R == (1U << 5)); - CHECK(SWITCH_PRO_MASK_ZL == (1U << 6) && SWITCH_PRO_MASK_ZR == (1U << 7)); - CHECK(SWITCH_PRO_MASK_MINUS == (1U << 8) && SWITCH_PRO_MASK_PLUS == (1U << 9)); - CHECK(SWITCH_PRO_MASK_L3 == (1U << 10) && SWITCH_PRO_MASK_R3 == (1U << 11)); - CHECK(SWITCH_PRO_MASK_HOME == (1U << 12) && SWITCH_PRO_MASK_CAPTURE == (1U << 13)); - CHECK(SWITCH_PRO_JOYSTICK_MIN == 0x0000); - CHECK(SWITCH_PRO_JOYSTICK_MID == 0x7FFF); - CHECK(SWITCH_PRO_JOYSTICK_MAX == 0xFFFF); - return true; -} - -bool switch_input_layout_matches_legacy_layout() { - // Given/When: the shared input types are compiled after extraction. - // Then: field offsets and aggregate sizes match the former driver-owned layout. - CHECK(sizeof(SwitchImuSample) == 12); - CHECK(offsetof(SwitchImuSample, accel_x) == 0); - CHECK(offsetof(SwitchImuSample, gyro_z) == 10); - CHECK(offsetof(SwitchInputState, dpad_up) == 0); - CHECK(offsetof(SwitchInputState, button_a) == 4); - CHECK(offsetof(SwitchInputState, button_r3) == 17); - CHECK(offsetof(SwitchInputState, lx) == 18); - CHECK(offsetof(SwitchInputState, ry) == 24); - CHECK(offsetof(SwitchInputState, imu_sample_count) == 26); - CHECK(offsetof(SwitchInputState, imu_samples) == 28); - CHECK(sizeof(SwitchInputState) == 64); - return true; -} - -} // namespace - -void run_switch_input_tests(TestRunner& runner) { - runner.run("shared input constants preserve legacy values", switch_input_constants_match_legacy_values); - runner.run("shared input types preserve legacy layout", switch_input_layout_matches_legacy_layout); -} diff --git a/tests/firmware/test_switch_uart_protocol.cpp b/tests/firmware/test_switch_uart_protocol.cpp deleted file mode 100644 index 0dd1edd..0000000 --- a/tests/firmware/test_switch_uart_protocol.cpp +++ /dev/null @@ -1,261 +0,0 @@ -#include "test_support.h" - -#include -#include -#include -#include - -#include "../../switch_input.h" -#include "../../switch_uart_protocol.h" - -namespace { - -using ImuSamples = std::vector; - -void append_int16(std::vector& bytes, int16_t value) { - const uint16_t encoded = static_cast(value); - bytes.push_back(static_cast(encoded & 0xFF)); - bytes.push_back(static_cast(encoded >> 8)); -} - -std::vector make_frame( - uint16_t buttons = 0, - uint8_t hat = SWITCH_PRO_HAT_NOTHING, - uint8_t imu_count = 0, - const ImuSamples& samples = {}) { - std::vector frame = { - 0xAA, 0x02, 0x00, - static_cast(buttons & 0xFF), - static_cast(buttons >> 8), - hat, 0x80, 0x80, 0x80, 0x80, imu_count, - }; - for (const SwitchImuSample& sample : samples) { - append_int16(frame, sample.accel_x); - append_int16(frame, sample.accel_y); - append_int16(frame, sample.accel_z); - append_int16(frame, sample.gyro_x); - append_int16(frame, sample.gyro_y); - append_int16(frame, sample.gyro_z); - } - frame[2] = static_cast(frame.size() - 3); - uint8_t checksum = 0; - for (uint8_t byte : frame) { - checksum = static_cast(checksum + byte); - } - frame.push_back(checksum); - return frame; -} - -bool decode(const std::vector& frame, SwitchInputState& state) { - return switch_uart_decode_input_frame( - frame.data(), static_cast(frame.size()), &state); -} - -bool uart_decoder_rejects_short_frame() { - // Given: a frame shorter than the legacy 12-byte minimum. - const std::array frame{}; - SwitchInputState state{}; - - // When: the frame is decoded. Then: it is rejected. - CHECK(!switch_uart_decode_input_frame(frame.data(), frame.size(), &state)); - return true; -} - -bool uart_decoder_rejects_wrong_header_and_version() { - // Given: otherwise-valid frames with invalid framing bytes. - std::vector wrong_header = make_frame(); - std::vector wrong_version = make_frame(); - wrong_header[0] = 0xAB; - wrong_version[1] = 0x01; - SwitchInputState state{}; - - // When: either frame is decoded. Then: both are rejected before payload use. - CHECK(!decode(wrong_header, state)); - CHECK(!decode(wrong_version, state)); - return true; -} - -bool uart_decoder_rejects_declared_length_and_checksum_mismatch() { - // Given: valid frames corrupted independently at length and checksum. - std::vector wrong_length = make_frame(); - std::vector wrong_checksum = make_frame(); - ++wrong_length[2]; - ++wrong_checksum.back(); - SwitchInputState state{}; - state.lx = 0x1234; - - // When: either frame is decoded. Then: both validation failures are rejected. - CHECK(!decode(wrong_length, state)); - CHECK(!decode(wrong_checksum, state)); - CHECK(state.lx == 0x1234); - return true; -} - -bool uart_decoder_decodes_neutral_frame() { - // Given: the canonical 12-byte neutral frame. - const std::vector frame = make_frame(); - SwitchInputState state{}; - - // When: the frame is decoded. Then: buttons/hat/IMU are clear and sticks expand exactly. - CHECK(decode(frame, state)); - CHECK(!state.dpad_up && !state.dpad_down && !state.dpad_left && !state.dpad_right); - CHECK(!state.button_a && !state.button_b && !state.button_x && !state.button_y); - CHECK(!state.button_l && !state.button_r && !state.button_zl && !state.button_zr); - CHECK(!state.button_plus && !state.button_minus && !state.button_home && !state.button_capture); - CHECK(!state.button_l3 && !state.button_r3); - CHECK(state.lx == 0x8080 && state.ly == 0x8080); - CHECK(state.rx == 0x8080 && state.ry == 0x8080); - CHECK(state.imu_sample_count == 0); - return true; -} - -bool uart_decoder_maps_every_button_bit() { - struct ButtonCase { - uint16_t mask; - bool SwitchInputState::*field; - }; - static constexpr ButtonCase cases[] = { - {SWITCH_PRO_MASK_Y, &SwitchInputState::button_y}, - {SWITCH_PRO_MASK_B, &SwitchInputState::button_b}, - {SWITCH_PRO_MASK_A, &SwitchInputState::button_a}, - {SWITCH_PRO_MASK_X, &SwitchInputState::button_x}, - {SWITCH_PRO_MASK_L, &SwitchInputState::button_l}, - {SWITCH_PRO_MASK_R, &SwitchInputState::button_r}, - {SWITCH_PRO_MASK_ZL, &SwitchInputState::button_zl}, - {SWITCH_PRO_MASK_ZR, &SwitchInputState::button_zr}, - {SWITCH_PRO_MASK_MINUS, &SwitchInputState::button_minus}, - {SWITCH_PRO_MASK_PLUS, &SwitchInputState::button_plus}, - {SWITCH_PRO_MASK_L3, &SwitchInputState::button_l3}, - {SWITCH_PRO_MASK_R3, &SwitchInputState::button_r3}, - {SWITCH_PRO_MASK_HOME, &SwitchInputState::button_home}, - {SWITCH_PRO_MASK_CAPTURE, &SwitchInputState::button_capture}, - }; - - // Given/When: each legacy button bit is decoded independently. - for (const ButtonCase& button : cases) { - SwitchInputState state{}; - CHECK(decode(make_frame(button.mask), state)); - - // Then: the corresponding shared input field is set. - CHECK(state.*(button.field)); - const int pressed_count = - state.button_y + state.button_b + state.button_a + state.button_x + - state.button_l + state.button_r + state.button_zl + state.button_zr + - state.button_minus + state.button_plus + state.button_l3 + state.button_r3 + - state.button_home + state.button_capture; - CHECK(pressed_count == 1); - } - return true; -} - -bool uart_decoder_maps_every_hat_value() { - struct HatCase { - uint8_t hat; - bool up; - bool down; - bool left; - bool right; - }; - static constexpr HatCase cases[] = { - {SWITCH_PRO_HAT_UP, true, false, false, false}, - {SWITCH_PRO_HAT_UPRIGHT, true, false, false, true}, - {SWITCH_PRO_HAT_RIGHT, false, false, false, true}, - {SWITCH_PRO_HAT_DOWNRIGHT, false, true, false, true}, - {SWITCH_PRO_HAT_DOWN, false, true, false, false}, - {SWITCH_PRO_HAT_DOWNLEFT, false, true, true, false}, - {SWITCH_PRO_HAT_LEFT, false, false, true, false}, - {SWITCH_PRO_HAT_UPLEFT, true, false, true, false}, - {SWITCH_PRO_HAT_NOTHING, false, false, false, false}, - {0xFF, false, false, false, false}, - }; - - // Given/When: every legacy hat value is decoded. - for (const HatCase& hat : cases) { - SwitchInputState state{}; - CHECK(decode(make_frame(0, hat.hat), state)); - - // Then: its exact cardinal/diagonal field combination is produced. - CHECK(state.dpad_up == hat.up && state.dpad_down == hat.down); - CHECK(state.dpad_left == hat.left && state.dpad_right == hat.right); - } - return true; -} - -bool uart_decoder_expands_stick_bytes() { - // Given: a valid frame with distinct byte values on every axis. - std::vector frame = make_frame(); - frame[6] = 0x00; - frame[7] = 0x7F; - frame[8] = 0x80; - frame[9] = 0xFF; - frame.back() = 0; - for (std::size_t index = 0; index + 1 < frame.size(); ++index) { - frame.back() = static_cast(frame.back() + frame[index]); - } - SwitchInputState state{}; - - // When: the frame is decoded. Then: each byte is duplicated into 16 bits. - CHECK(decode(frame, state)); - CHECK(state.lx == 0x0000 && state.ly == 0x7F7F); - CHECK(state.rx == 0x8080 && state.ry == 0xFFFF); - return true; -} - -bool uart_decoder_decodes_one_and_three_imu_samples() { - // Given: one-sample and three-sample frames with signed extrema and distinct values. - const SwitchImuSample first{-32768, -2, -1, 0, 1, 32767}; - const SwitchImuSample second{10, 20, 30, 40, 50, 60}; - const SwitchImuSample third{-10, -20, -30, -40, -50, -60}; - SwitchInputState one{}; - SwitchInputState three{}; - - // When: both frames are decoded. - CHECK(decode(make_frame(0, SWITCH_PRO_HAT_NOTHING, 1, {first}), one)); - CHECK(decode(make_frame(0, SWITCH_PRO_HAT_NOTHING, 3, {first, second, third}), three)); - - // Then: counts and little-endian signed sample fields remain exact. - CHECK(one.imu_sample_count == 1 && one.imu_samples[0].accel_x == -32768); - CHECK(one.imu_samples[0].gyro_z == 32767); - CHECK(three.imu_sample_count == 3); - CHECK(three.imu_samples[1].accel_z == 30 && three.imu_samples[1].gyro_y == 50); - CHECK(three.imu_samples[2].accel_y == -20 && three.imu_samples[2].gyro_z == -60); - return true; -} - -bool uart_decoder_caps_imu_count_and_rejects_truncation() { - // Given: a count of four backed by three samples, and a count of one with none. - const SwitchImuSample sample{1, 2, 3, 4, 5, 6}; - const std::vector capped = make_frame(0, SWITCH_PRO_HAT_NOTHING, 4, {sample, sample, sample}); - const std::vector truncated = make_frame(0, SWITCH_PRO_HAT_NOTHING, 1); - SwitchInputState state{}; - - // When: both frames are decoded. Then: three samples are accepted and truncation is rejected. - CHECK(decode(capped, state)); - CHECK(state.imu_sample_count == 3); - CHECK(!decode(truncated, state)); - return true; -} - -bool uart_decoder_rejects_null_output() { - // Given: an otherwise-valid frame. When: no output state is supplied. - const std::vector frame = make_frame(); - - // Then: the legacy parser returns false rather than mutating driver state. - CHECK(!switch_uart_decode_input_frame(frame.data(), frame.size(), nullptr)); - return true; -} - -} // namespace - -void run_switch_uart_protocol_tests(TestRunner& runner) { - runner.run("UART rejects short frame", uart_decoder_rejects_short_frame); - runner.run("UART rejects header and version", uart_decoder_rejects_wrong_header_and_version); - runner.run("UART rejects length and checksum", uart_decoder_rejects_declared_length_and_checksum_mismatch); - runner.run("UART decodes neutral frame", uart_decoder_decodes_neutral_frame); - runner.run("UART maps every button", uart_decoder_maps_every_button_bit); - runner.run("UART maps every hat", uart_decoder_maps_every_hat_value); - runner.run("UART expands stick bytes", uart_decoder_expands_stick_bytes); - runner.run("UART decodes IMU samples", uart_decoder_decodes_one_and_three_imu_samples); - runner.run("UART caps and validates IMU count", uart_decoder_caps_imu_count_and_rejects_truncation); - runner.run("UART rejects null output", uart_decoder_rejects_null_output); -} diff --git a/tools/debug_imu_raw.py b/tools/debug_imu_raw.py new file mode 100755 index 0000000..ad9b4cd --- /dev/null +++ b/tools/debug_imu_raw.py @@ -0,0 +1,205 @@ +#!/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() diff --git a/tusb_config.h b/tusb_config.h index 3c25f26..7f291ec 100644 --- a/tusb_config.h +++ b/tusb_config.h @@ -1,11 +1,8 @@ -// TinyUSB configuration for the selected 64-byte controller protocol. +// TinyUSB configuration tailored for a single Switch Pro style HID interface. +// Data is derived from TinyUSB examples and tuned for a 64-byte HID endpoint. #ifndef _TUSB_CONFIG_H_ #define _TUSB_CONFIG_H_ -#if (defined(SWITCH_PICO_PROTOCOL_LEGACY) + defined(SWITCH_PICO_PROTOCOL_SWITCH2)) != 1 -#error "Define exactly one Switch Pico USB protocol" -#endif - #ifdef __cplusplus extern "C" { #endif @@ -30,15 +27,7 @@ extern "C" { #define CFG_TUD_CDC 0 #define CFG_TUD_MSC 0 #define CFG_TUD_MIDI 0 -#define CFG_TUD_AUDIO 0 -#if defined(SWITCH_PICO_PROTOCOL_SWITCH2) -#define CFG_TUD_VENDOR 1 -#define CFG_TUD_VENDOR_EPSIZE 64 -#define CFG_TUD_VENDOR_RX_BUFSIZE 64 -#define CFG_TUD_VENDOR_TX_BUFSIZE 64 -#else #define CFG_TUD_VENDOR 0 -#endif // Always enable TinyUSB debug at level 2; LOG_PRINTF controls user-facing logs. #ifdef CFG_TUSB_DEBUG #undef CFG_TUSB_DEBUG