Compare commits
10 commits
master
...
feat/imu-g
| Author | SHA1 | Date | |
|---|---|---|---|
| f722a9dcbb | |||
| 4f7e4a8de6 | |||
|
731e1d8d15 |
|||
|
6884f25121 |
|||
|
0fbb187068 |
|||
|
e7c01d1116 |
|||
|
3c60841d23 |
|||
|
30e22c210c |
|||
|
9d33bc4be7 |
|||
|
12dc3d58ee |
32 changed files with 660 additions and 2275 deletions
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
58
README.md
58
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.
|
||||
|
|
|
|||
121
build.py
Executable file → Normal file
121
build.py
Executable file → Normal file
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
#include "switch2_commands.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
// 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<std::size_t>(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;
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
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;
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
#include "switch_protocol.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#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<uint8_t, kMaxVendorResponseLength> 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<uint32_t>(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<uint32_t>(
|
||||
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<uint8_t>(report.id),
|
||||
report.payload.data(),
|
||||
static_cast<uint16_t>(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<uint8_t>(Switch2InputReportId::Common)) {
|
||||
id = Switch2InputReportId::Common;
|
||||
} else if (report_id == static_cast<uint8_t>(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<uint16_t>(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<uint8_t, kMaxVendorRequestLength> 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<uint16_t>(switch2_string_language[0]) |
|
||||
(static_cast<uint16_t>(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<uint16_t>(0x0300 | (2 * length + 2));
|
||||
return descriptor;
|
||||
}
|
||||
|
|
@ -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<uint8_t>(packed_x & 0xFF);
|
||||
destination[1] = static_cast<uint8_t>(
|
||||
((packed_x >> 8) & 0x0F) | ((packed_y & 0x0F) << 4));
|
||||
destination[2] = static_cast<uint8_t>(packed_y >> 4);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Switch2InputReport switch2_build_input_report(
|
||||
Switch2InputReportId id,
|
||||
const SwitchInputState& state,
|
||||
uint32_t counter) {
|
||||
Switch2InputReport report{id, {}};
|
||||
std::array<uint8_t, 63>& payload = report.payload;
|
||||
|
||||
switch (id) {
|
||||
case Switch2InputReportId::Common:
|
||||
payload[0] = static_cast<uint8_t>(counter);
|
||||
payload[1] = static_cast<uint8_t>(counter >> 8);
|
||||
payload[2] = static_cast<uint8_t>(counter >> 16);
|
||||
payload[3] = static_cast<uint8_t>(counter >> 24);
|
||||
payload[4] = static_cast<uint8_t>(
|
||||
(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<uint8_t>(
|
||||
(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<uint8_t>(
|
||||
(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<uint8_t>(counter);
|
||||
// USB external power is known; charging and battery bits stay zero.
|
||||
payload[1] = 0x01;
|
||||
payload[2] = static_cast<uint8_t>(
|
||||
(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<uint8_t>(
|
||||
(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<uint8_t>(
|
||||
(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;
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
#include "switch_input.h"
|
||||
|
||||
enum class Switch2InputReportId : uint8_t {
|
||||
Common = 0x05,
|
||||
Pro = 0x09,
|
||||
};
|
||||
|
||||
struct Switch2InputReport {
|
||||
Switch2InputReportId id;
|
||||
std::array<uint8_t, 63> payload;
|
||||
};
|
||||
|
||||
Switch2InputReport switch2_build_input_report(
|
||||
Switch2InputReportId id,
|
||||
const SwitchInputState& state,
|
||||
uint32_t counter);
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
@ -9,10 +9,39 @@
|
|||
|
||||
#include <stdint.h>
|
||||
|
||||
#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,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
#include "switch_pro_driver.h"
|
||||
#include "switch_pro_bounds.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
|
@ -7,7 +6,6 @@
|
|||
#include <stdio.h>
|
||||
#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<const SwitchFactoryConfig*>(factory_config_data);
|
||||
static const SwitchUserCalibration* user_calibration [[maybe_unused]] = reinterpret_cast<const SwitchUserCalibration*>(user_calibration_data);
|
||||
|
||||
struct SpiFlashRegion {
|
||||
const uint8_t* data;
|
||||
std::size_t size;
|
||||
};
|
||||
|
||||
static const std::map<uint32_t, SpiFlashRegion> spi_flash_data = {
|
||||
{0x6000, {factory_config_data, sizeof(factory_config_data)}},
|
||||
{0x8000, {user_calibration_data, sizeof(user_calibration_data)}}
|
||||
static std::map<uint32_t, const uint8_t*> 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<uint16_t>(packet[3]) | (static_cast<uint16_t>(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<uint16_t>(8u + static_cast<uint16_t>(imu_count) * 12u);
|
||||
if (payload_len < required_payload_len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto expand_axis = [](uint8_t v) -> uint16_t {
|
||||
return static_cast<uint16_t>(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<int16_t>(static_cast<uint16_t>(src[0]) | (static_cast<uint16_t>(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) {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,46 @@
|
|||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#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);
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#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);
|
||||
|
|
@ -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<uint16_t>(payload_len) + 4u != length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t sum = 0;
|
||||
for (uint16_t index = 0; index < static_cast<uint16_t>(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<uint16_t>(packet[3]) |
|
||||
(static_cast<uint16_t>(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<uint16_t>(8u + static_cast<uint16_t>(imu_count) * 12u);
|
||||
if (payload_len < required_payload_len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto expand_axis = [](uint8_t value) -> uint16_t {
|
||||
return static_cast<uint16_t>(value) << 8 | value;
|
||||
};
|
||||
const auto read_int16 = [](const uint8_t* source) -> int16_t {
|
||||
return static_cast<int16_t>(
|
||||
static_cast<uint16_t>(source[0]) |
|
||||
(static_cast<uint16_t>(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;
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "switch_input.h"
|
||||
|
||||
bool switch_uart_decode_input_frame(
|
||||
const uint8_t* packet,
|
||||
uint8_t length,
|
||||
SwitchInputState* out_state);
|
||||
|
|
@ -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)
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
#include "test_support.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include "../../switch_pro_descriptors.h"
|
||||
#include "../../switch_pro_bounds.h"
|
||||
|
||||
namespace {
|
||||
|
||||
template <std::size_t ActualSize, std::size_t ExpectedSize>
|
||||
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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
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);
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
#include "test_support.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#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<uint8_t, 12> 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<uint8_t, sizeof(expected)> 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<uint8_t, sizeof(valid) + 1> 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<uint8_t, sizeof(valid)> 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<uint8_t, sizeof(valid)> 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<uint8_t, 12> 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<uint8_t, 16> 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<uint8_t, 12> 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);
|
||||
}
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
#include "test_support.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#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<uint32_t>(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);
|
||||
}
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
#include "test_support.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#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<uint8_t, 63>& 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<uint8_t>(common.id) == 0x05);
|
||||
CHECK(static_cast<uint8_t>(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);
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
#include "test_support.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#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);
|
||||
}
|
||||
|
|
@ -1,261 +0,0 @@
|
|||
#include "test_support.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "../../switch_input.h"
|
||||
#include "../../switch_uart_protocol.h"
|
||||
|
||||
namespace {
|
||||
|
||||
using ImuSamples = std::vector<SwitchImuSample>;
|
||||
|
||||
void append_int16(std::vector<uint8_t>& bytes, int16_t value) {
|
||||
const uint16_t encoded = static_cast<uint16_t>(value);
|
||||
bytes.push_back(static_cast<uint8_t>(encoded & 0xFF));
|
||||
bytes.push_back(static_cast<uint8_t>(encoded >> 8));
|
||||
}
|
||||
|
||||
std::vector<uint8_t> make_frame(
|
||||
uint16_t buttons = 0,
|
||||
uint8_t hat = SWITCH_PRO_HAT_NOTHING,
|
||||
uint8_t imu_count = 0,
|
||||
const ImuSamples& samples = {}) {
|
||||
std::vector<uint8_t> frame = {
|
||||
0xAA, 0x02, 0x00,
|
||||
static_cast<uint8_t>(buttons & 0xFF),
|
||||
static_cast<uint8_t>(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<uint8_t>(frame.size() - 3);
|
||||
uint8_t checksum = 0;
|
||||
for (uint8_t byte : frame) {
|
||||
checksum = static_cast<uint8_t>(checksum + byte);
|
||||
}
|
||||
frame.push_back(checksum);
|
||||
return frame;
|
||||
}
|
||||
|
||||
bool decode(const std::vector<uint8_t>& frame, SwitchInputState& state) {
|
||||
return switch_uart_decode_input_frame(
|
||||
frame.data(), static_cast<uint8_t>(frame.size()), &state);
|
||||
}
|
||||
|
||||
bool uart_decoder_rejects_short_frame() {
|
||||
// Given: a frame shorter than the legacy 12-byte minimum.
|
||||
const std::array<uint8_t, 11> 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<uint8_t> wrong_header = make_frame();
|
||||
std::vector<uint8_t> 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<uint8_t> wrong_length = make_frame();
|
||||
std::vector<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t>(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<uint8_t> capped = make_frame(0, SWITCH_PRO_HAT_NOTHING, 4, {sample, sample, sample});
|
||||
const std::vector<uint8_t> 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<uint8_t> 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);
|
||||
}
|
||||
205
tools/debug_imu_raw.py
Executable file
205
tools/debug_imu_raw.py
Executable file
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue