Compare commits

...

17 commits

Author SHA1 Message Date
e2a7635f2f Harden legacy bounds
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-11 12:51:36 +09:00
6f18b65217 Document protocol modes
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-11 12:23:07 +09:00
05790b4f99 Update build helper
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-11 12:23:07 +09:00
05e04b2632 Wire protocol modes
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-11 12:23:07 +09:00
01cf6ed26f Enable firmware tests
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-11 12:23:07 +09:00
f3067bfd2e Add legacy contracts
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-11 12:23:07 +09:00
95b0576d1d Add Switch2 driver
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-11 12:23:07 +09:00
3251e49191 Implement Switch2 descriptors
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-11 12:23:07 +09:00
0ec06676c3 Define Switch2 descriptors
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-11 12:23:07 +09:00
f8de986717 Implement Switch2 reports
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-11 12:23:07 +09:00
f79394cd80 Define Switch2 reports
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-11 12:23:07 +09:00
950c5de1ea Implement Switch2 commands
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-11 12:23:07 +09:00
1b65893a69 Define Switch2 commands
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-11 12:23:07 +09:00
1a6859855e Add protocol facade
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-11 12:23:07 +09:00
a7f07c67b8 Implement UART decoder
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-11 12:23:07 +09:00
05098bbdac Define UART decoder
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-11 12:23:07 +09:00
a1dd3af692 Share input model
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-11 12:23:07 +09:00
30 changed files with 2221 additions and 287 deletions

View file

@ -24,8 +24,29 @@ if (EXISTS ${picoVscode})
endif() endif()
# ==================================================================================== # ====================================================================================
option(SWITCH_PICO_LOG "Enable UART debug logging" OFF) 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") 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) # Pull in Raspberry Pi Pico SDK (must be before project)
include(pico_sdk_import.cmake) include(pico_sdk_import.cmake)
@ -38,9 +59,12 @@ pico_sdk_init()
add_executable(switch-pico add_executable(switch-pico
switch-pico.cpp switch-pico.cpp
switch_pro_driver.cpp switch_uart_protocol.cpp
${SWITCH_PICO_PROTOCOL_SOURCES}
) )
target_compile_definitions(switch-pico PRIVATE ${SWITCH_PICO_PROTOCOL_DEFINITION})
pico_set_program_name(switch-pico "switch-pico") pico_set_program_name(switch-pico "switch-pico")
pico_set_program_version(switch-pico "0.1") pico_set_program_version(switch-pico "0.1")

View file

@ -103,19 +103,62 @@ Filters you can use:
## Building and flashing firmware ## Building and flashing firmware
Prereqs: Pico SDK + CMake toolchain set up. Prereqs: Pico SDK + CMake toolchain set up.
### One-shot build + flash (picotool) ### 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:
```sh ```sh
python3 build.py 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. - 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`. - Set `ELF_PATH` to override the default `build/switch-pico.elf`.
### Manual build ### Experimental Switch 2 scope
```sh
cmake -S . -B build -DSWITCH_PICO_LOG=OFF - 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.
cmake --build build -j - 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.
This produces a `.uf2` you can flash (typically `build/switch-pico.uf2`). - 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 UF2 flashing (BOOTSEL, no tools) ### 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: If you already have a built (or use the pre-built one in `firmware/`) `.uf2`, you can flash it without rebuilding:
@ -135,6 +178,7 @@ Flags:
`build.py` can optionally update the **grip** colours in `controller_color_config.h` before building/flashing (default leaves the file unchanged): `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` - Random grip colours: `python3 build.py --random-grip-color`
- Set grip colours: `python3 build.py --grip-color FF00AA` - 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) ## Python bridge (recommended)
Works on macOS, Windows, Linux. Uses SDL2 + pyserial. Works on macOS, Windows, Linux. Uses SDL2 + pyserial.

121
build.py Normal file → Executable file
View file

@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Build and flash the project with optional grip color overrides.""" """Build and flash the project with optional grip color overrides."""
import argparse import argparse
import os import os
import random import random
@ -8,12 +9,15 @@ import shutil
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Final, Literal, TypeAlias, final
SCRIPT_DIR = Path(__file__).resolve().parent SCRIPT_DIR = Path(__file__).resolve().parent
CONFIG_FILE = SCRIPT_DIR / "controller_color_config.h" CONFIG_FILE = SCRIPT_DIR / "controller_color_config.h"
BUILD_DIR = SCRIPT_DIR / "build" 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", BUILD_DIR / "switch-pico.elf")).expanduser() ELF_PATH = Path(os.environ.get("ELF_PATH", str(BUILD_ELF_PATH))).expanduser()
MACROS = ( MACROS = (
"SWITCH_COLOR_LEFT_GRIP_R", "SWITCH_COLOR_LEFT_GRIP_R",
@ -24,36 +28,73 @@ MACROS = (
"SWITCH_COLOR_RIGHT_GRIP_B", "SWITCH_COLOR_RIGHT_GRIP_B",
) )
def parse_args(): 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:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Build and flash the project, optionally setting grip colors.", description="Build and flash the project, optionally setting grip colors.",
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="Default behavior leaves controller_color_config.h unchanged.", 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 = parser.add_mutually_exclusive_group()
group.add_argument( _ = group.add_argument(
"--random-grip-color", "--random-grip-color",
action="store_true", action="store_true",
help="Randomize both grip colors before building.", help="Randomize both grip colors before building.",
) )
group.add_argument( _ = group.add_argument(
"--grip-color", "--grip-color",
metavar="RRGGBB", metavar="RRGGBB",
help="Set both grip colors to the provided hex value.", help="Set both grip colors to the provided hex value.",
) )
return parser.parse_args() 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
def random_hex_color():
def random_hex_color() -> str:
return "".join(f"{random.randrange(256):02X}" for _ in range(3)) return "".join(f"{random.randrange(256):02X}" for _ in range(3))
def validate_custom_color(value):
def validate_custom_color(value: str) -> str:
if not re.fullmatch(r"[0-9A-Fa-f]{6}", value): if not re.fullmatch(r"[0-9A-Fa-f]{6}", value):
raise ValueError("Color must be a 6-digit hex value like FF8800.") raise argparse.ArgumentTypeError(
"Color must be a 6-digit hex value like FF8800."
)
return value return value
def update_grip_colors(rgb_hex):
def update_grip_colors(rgb_hex: str) -> None:
if not CONFIG_FILE.exists(): 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) sys.exit(1)
r, g, b = rgb_hex[:2], rgb_hex[2:4], rgb_hex[4:6] r, g, b = rgb_hex[:2], rgb_hex[2:4], rgb_hex[4:6]
@ -61,14 +102,14 @@ def update_grip_colors(rgb_hex):
try: try:
text = CONFIG_FILE.read_text(encoding="utf-8") text = CONFIG_FILE.read_text(encoding="utf-8")
except OSError as exc: 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) sys.exit(1)
def replace(name, val, data): def replace(name: str, val: str, data: str) -> str:
pattern = rf"(?m)^(#define\s+{name}\s+)0x[0-9A-Fa-f]{{2}}" 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) updated, count = re.subn(pattern, rf"\g<1>0x{val.upper()}", data)
if count == 0: 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) sys.exit(1)
return updated return updated
@ -77,26 +118,30 @@ def update_grip_colors(rgb_hex):
text = replace(macro, val, text) text = replace(macro, val, text)
try: try:
CONFIG_FILE.write_text(text, encoding="utf-8") _ = CONFIG_FILE.write_text(text, encoding="utf-8")
except OSError as exc: 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) sys.exit(1)
def run_cmd(command):
def run_cmd(command: list[str]) -> None:
try: try:
subprocess.run(command, cwd=SCRIPT_DIR, check=True) _ = subprocess.run(command, cwd=SCRIPT_DIR, check=True)
except FileNotFoundError as exc: 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) sys.exit(1)
except subprocess.CalledProcessError as exc: except subprocess.CalledProcessError as exc:
sys.exit(exc.returncode) sys.exit(exc.returncode)
def resolve_picotool():
def resolve_picotool() -> Path:
env_val = os.environ.get("PICOTOOL_PATH") env_val = os.environ.get("PICOTOOL_PATH")
if env_val: if env_val:
env_path = Path(env_val).expanduser() env_path = Path(env_val).expanduser()
if not env_path.exists(): 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) sys.exit(1)
return env_path return env_path
@ -104,10 +149,13 @@ def resolve_picotool():
if found: if found:
return Path(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) sys.exit(1)
def build():
def build(protocol: BuildProtocol) -> None:
run_cmd( run_cmd(
[ [
"cmake", "cmake",
@ -116,20 +164,23 @@ def build():
"-B", "-B",
str(BUILD_DIR), str(BUILD_DIR),
"-DSWITCH_PICO_LOG=OFF", "-DSWITCH_PICO_LOG=OFF",
f"-DSWITCH_PICO_PROTOCOL={protocol}",
] ]
) )
run_cmd(["cmake", "--build", str(BUILD_DIR)]) run_cmd(["cmake", "--build", str(BUILD_DIR)])
def flash():
def flash() -> None:
picotool = resolve_picotool() picotool = resolve_picotool()
if not ELF_PATH.exists(): 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" f"Error: Cannot find ELF at {ELF_PATH}. Set ELF_PATH to override.\n"
) )
sys.exit(1) sys.exit(1)
run_cmd([str(picotool), "load", str(ELF_PATH), "-fx"]) run_cmd([str(picotool), "load", str(ELF_PATH), "-fx"])
def main():
def main() -> None:
args = parse_args() args = parse_args()
color = None color = None
@ -138,16 +189,30 @@ def main():
elif args.grip_color: elif args.grip_color:
try: try:
color = validate_custom_color(args.grip_color) color = validate_custom_color(args.grip_color)
except ValueError as exc: except argparse.ArgumentTypeError as exc:
sys.stderr.write(f"Error: {exc}\n") _ = sys.stderr.write(f"Error: {exc}\n")
sys.exit(1) sys.exit(1)
if color: if color:
update_grip_colors(color) update_grip_colors(color)
print(f"Grip color set to #{color} in {CONFIG_FILE.name}") print(f"Grip color set to #{color} in {CONFIG_FILE.name}")
build() 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
flash() flash()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View file

@ -4,7 +4,9 @@
#include "hardware/uart.h" #include "hardware/uart.h"
#include "pico/stdlib.h" #include "pico/stdlib.h"
#include "tusb.h" #include "tusb.h"
#include "switch_pro_driver.h" #include "switch_input.h"
#include "switch_protocol.h"
#include "switch_uart_protocol.h"
#ifdef SWITCH_PICO_LOG #ifdef SWITCH_PICO_LOG
#define LOG_PRINTF(...) printf(__VA_ARGS__) #define LOG_PRINTF(...) printf(__VA_ARGS__)
@ -60,7 +62,7 @@ static void on_rumble_from_switch(const uint8_t rumble[8]) {
send_rumble_uart_frame(rumble); send_rumble_uart_frame(rumble);
} }
// Consume UART bytes and forward complete frames to the Switch Pro driver. // Consume UART bytes and decode complete input frames.
static bool poll_uart_frames() { static bool poll_uart_frames() {
static uint8_t buffer[64]; static uint8_t buffer[64];
static uint8_t index = 0; static uint8_t index = 0;
@ -103,7 +105,7 @@ static bool poll_uart_frames() {
if (expected_len > 0 && index >= expected_len) { if (expected_len > 0 && index >= expected_len) {
SwitchInputState parsed{}; SwitchInputState parsed{};
if (switch_pro_apply_uart_packet(buffer, expected_len, &parsed)) { if (switch_uart_decode_input_frame(buffer, expected_len, &parsed)) {
g_user_state = parsed; g_user_state = parsed;
new_data = true; new_data = true;
LOG_PRINTF("[UART] packet buttons=0x%04x hat=%u lx=%u ly=%u rx=%u ry=%u\n", LOG_PRINTF("[UART] packet buttons=0x%04x hat=%u lx=%u ly=%u rx=%u ry=%u\n",
@ -137,7 +139,7 @@ static bool poll_uart_frames() {
static void log_usb_state() { static void log_usb_state() {
bool mounted = tud_mounted(); bool mounted = tud_mounted();
bool ready = switch_pro_is_ready(); bool ready = switch_protocol_is_ready();
if (mounted != g_last_mounted) { if (mounted != g_last_mounted) {
g_last_mounted = mounted; g_last_mounted = mounted;
@ -156,10 +158,10 @@ int main() {
init_uart_input(); init_uart_input();
tusb_init(); tusb_init();
switch_pro_init(); switch_protocol_init();
switch_pro_set_rumble_callback(on_rumble_from_switch); switch_protocol_set_rumble_callback(on_rumble_from_switch);
g_user_state = neutral_input(); g_user_state = neutral_input();
switch_pro_set_input(g_user_state); switch_protocol_set_input(g_user_state);
LOG_PRINTF("[BOOT] switch-pico starting (UART0 log @ 115200)\n"); LOG_PRINTF("[BOOT] switch-pico starting (UART0 log @ 115200)\n");
LOG_PRINTF("[INFO] UART1 pins TX=%d RX=%d baud=%d\n", LOG_PRINTF("[INFO] UART1 pins TX=%d RX=%d baud=%d\n",
@ -170,8 +172,8 @@ int main() {
bool new_data = poll_uart_frames(); // Pull controller state from UART1 bool new_data = poll_uart_frames(); // Pull controller state from UART1
(void)new_data; (void)new_data;
SwitchInputState state = g_user_state; SwitchInputState state = g_user_state;
switch_pro_set_input(state); switch_protocol_set_input(state);
switch_pro_task(); // Push state to the Switch host switch_protocol_task(); // Push state to the Switch host
log_usb_state(); log_usb_state();
} }
} }

79
switch2_commands.cpp Normal file
View file

@ -0,0 +1,79 @@
#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;
}

20
switch2_commands.h Normal file
View file

@ -0,0 +1,20 @@
#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);

64
switch2_descriptors.cpp Normal file
View file

@ -0,0 +1,64 @@
#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);

22
switch2_descriptors.h Normal file
View file

@ -0,0 +1,22 @@
#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;

237
switch2_driver.cpp Normal file
View file

@ -0,0 +1,237 @@
#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;
}

89
switch2_reports.cpp Normal file
View file

@ -0,0 +1,89 @@
#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;
}

21
switch2_reports.h Normal file
View file

@ -0,0 +1,21 @@
#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);

72
switch_input.h Normal file
View file

@ -0,0 +1,72 @@
#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;

View file

@ -0,0 +1,23 @@
#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);
}

71
switch_pro_bounds.h Normal file
View file

@ -0,0 +1,71 @@
#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;
}

View file

@ -9,39 +9,10 @@
#include <stdint.h> #include <stdint.h>
#include "switch_input.h"
#define SWITCH_PRO_ENDPOINT_SIZE 64 #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 { typedef enum {
REPORT_OUTPUT_00 = 0x00, REPORT_OUTPUT_00 = 0x00,
REPORT_FEATURE = 0x01, REPORT_FEATURE = 0x01,

View file

@ -1,4 +1,5 @@
#include "switch_pro_driver.h" #include "switch_pro_driver.h"
#include "switch_pro_bounds.h"
#include <algorithm> #include <algorithm>
#include <cstring> #include <cstring>
@ -6,6 +7,7 @@
#include <stdio.h> #include <stdio.h>
#include "pico/rand.h" #include "pico/rand.h"
#include "pico/time.h" #include "pico/time.h"
#include "switch_pro_descriptors.h"
#include "tusb.h" #include "tusb.h"
#ifdef SWITCH_PICO_LOG #ifdef SWITCH_PICO_LOG
@ -177,9 +179,14 @@ static const uint8_t user_calibration_data[0x3F] = {
static const SwitchFactoryConfig* factory_config = reinterpret_cast<const SwitchFactoryConfig*>(factory_config_data); 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); static const SwitchUserCalibration* user_calibration [[maybe_unused]] = reinterpret_cast<const SwitchUserCalibration*>(user_calibration_data);
static std::map<uint32_t, const uint8_t*> spi_flash_data = { struct SpiFlashRegion {
{0x6000, factory_config_data}, const uint8_t* data;
{0x8000, user_calibration_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 inline uint16_t scale16To12(uint16_t pos) { return pos >> 4; } static inline uint16_t scale16To12(uint16_t pos) { return pos >> 4; }
@ -210,16 +217,6 @@ 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() { static void send_identify() {
memset(report_buffer, 0x00, sizeof(report_buffer)); memset(report_buffer, 0x00, sizeof(report_buffer));
report_buffer[0] = REPORT_USB_INPUT_81; report_buffer[0] = REPORT_USB_INPUT_81;
@ -244,16 +241,32 @@ static bool send_report(uint8_t reportID, const void* reportData, uint16_t repor
return result; return result;
} }
static void read_spi_flash(uint8_t* dest, uint32_t address, uint8_t size) { static void read_spi_flash(
uint8_t* destination,
std::size_t destination_capacity,
uint32_t address,
uint8_t size) {
uint32_t addressBank = address & 0xFFFFFF00; uint32_t addressBank = address & 0xFFFFFF00;
uint32_t addressOffset = address & 0x000000FF; uint32_t addressOffset = address & 0x000000FF;
auto it = spi_flash_data.find(addressBank); auto it = spi_flash_data.find(addressBank);
if (it != spi_flash_data.end()) { if (it != spi_flash_data.end()) {
const uint8_t* data = it->second; const SpiFlashRegion& region = it->second;
memcpy(dest, data + addressOffset, size); switch_pro_fill_flash_read(
destination,
destination_capacity,
region.data,
region.size,
addressOffset,
size);
} else { } else {
memset(dest, 0xFF, size); switch_pro_fill_flash_read(
destination,
destination_capacity,
nullptr,
0,
0,
size);
} }
} }
@ -380,7 +393,11 @@ static void handle_feature_report(uint8_t switchReportID, uint8_t switchReportSu
report_buffer[17] = reportData[13]; report_buffer[17] = reportData[13];
report_buffer[18] = reportData[14]; report_buffer[18] = reportData[14];
report_buffer[19] = reportData[15]; report_buffer[19] = reportData[15];
read_spi_flash(&report_buffer[20], spiReadAddress, spiReadSize); read_spi_flash(
&report_buffer[20],
sizeof(report_buffer) - 20,
spiReadAddress,
spiReadSize);
canSend = true; canSend = true;
LOG_PRINTF("[HID] FEATURE SPI_READ addr=0x%08lx size=%u\n", (unsigned long)spiReadAddress, spiReadSize); LOG_PRINTF("[HID] FEATURE SPI_READ addr=0x%08lx size=%u\n", (unsigned long)spiReadAddress, spiReadSize);
break; break;
@ -643,112 +660,6 @@ 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) { void switch_pro_set_rumble_callback(SwitchRumbleCallback cb) {
rumble_callback = cb; rumble_callback = cb;
} }
@ -757,6 +668,52 @@ bool switch_pro_is_ready() {
return 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 // 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) { 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; (void)instance;
@ -771,51 +728,12 @@ 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 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; 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);
} else {
}
} }
void tud_hid_report_received_cb(uint8_t instance, uint8_t report_id, uint8_t const* buffer, uint16_t bufsize) { void tud_hid_report_received_cb(uint8_t instance, uint8_t report_id, uint8_t const* buffer, uint16_t bufsize) {
(void)instance; dispatch_output_report(instance, report_id, buffer, bufsize);
// 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) { uint8_t const * tud_hid_descriptor_report_cb(uint8_t itf) {

View file

@ -8,46 +8,8 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include "switch_pro_descriptors.h"
typedef struct { #include "switch_protocol.h"
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. // Initialize USB state and calibration before entering the main loop.
void switch_pro_init(); void switch_pro_init();
@ -58,13 +20,8 @@ void switch_pro_set_input(const SwitchInputState& state);
// Drive the Switch Pro USB state machine; call this frequently in the main loop. // Drive the Switch Pro USB state machine; call this frequently in the main loop.
void switch_pro_task(); 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 // Driver state helpers
bool switch_pro_is_ready(); bool switch_pro_is_ready();
// Optional callback fired when the host sends a rumble payload (the raw 8 rumble bytes). // 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); void switch_pro_set_rumble_callback(SwitchRumbleCallback cb);

13
switch_protocol.h Normal file
View file

@ -0,0 +1,13 @@
#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);

124
switch_uart_protocol.cpp Normal file
View file

@ -0,0 +1,124 @@
#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;
}

10
switch_uart_protocol.h Normal file
View file

@ -0,0 +1,10 @@
#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);

View file

@ -0,0 +1,24 @@
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)

View file

@ -0,0 +1,173 @@
#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);
}

View file

@ -0,0 +1,12 @@
#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();
}

View file

@ -0,0 +1,38 @@
#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);

View file

@ -0,0 +1,185 @@
#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);
}

View file

@ -0,0 +1,166 @@
#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);
}

View file

@ -0,0 +1,186 @@
#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);
}

View file

@ -0,0 +1,52 @@
#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);
}

View file

@ -0,0 +1,261 @@
#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);
}

View file

@ -1,8 +1,11 @@
// TinyUSB configuration tailored for a single Switch Pro style HID interface. // TinyUSB configuration for the selected 64-byte controller protocol.
// Data is derived from TinyUSB examples and tuned for a 64-byte HID endpoint.
#ifndef _TUSB_CONFIG_H_ #ifndef _TUSB_CONFIG_H_
#define _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 #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
@ -27,7 +30,15 @@ extern "C" {
#define CFG_TUD_CDC 0 #define CFG_TUD_CDC 0
#define CFG_TUD_MSC 0 #define CFG_TUD_MSC 0
#define CFG_TUD_MIDI 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 #define CFG_TUD_VENDOR 0
#endif
// Always enable TinyUSB debug at level 2; LOG_PRINTF controls user-facing logs. // Always enable TinyUSB debug at level 2; LOG_PRINTF controls user-facing logs.
#ifdef CFG_TUSB_DEBUG #ifdef CFG_TUSB_DEBUG
#undef CFG_TUSB_DEBUG #undef CFG_TUSB_DEBUG