Wire protocol modes

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Joey Yakimowich-Payne 2026-08-11 12:23:07 +09:00
commit 05e04b2632
6 changed files with 53 additions and 203 deletions

View file

@ -24,8 +24,29 @@ if (EXISTS ${picoVscode})
endif()
# ====================================================================================
option(SWITCH_PICO_LOG "Enable UART debug logging" OFF)
set(SWITCH_PICO_PROTOCOL "legacy" CACHE STRING "USB protocol: legacy or switch2")
set_property(CACHE SWITCH_PICO_PROTOCOL PROPERTY STRINGS legacy switch2)
set(PICO_BOARD pico CACHE STRING "Board type")
if (SWITCH_PICO_PROTOCOL STREQUAL "legacy")
set(SWITCH_PICO_PROTOCOL_SOURCES
switch_pro_driver.cpp
switch_legacy_protocol.cpp
)
set(SWITCH_PICO_PROTOCOL_DEFINITION SWITCH_PICO_PROTOCOL_LEGACY=1)
elseif (SWITCH_PICO_PROTOCOL STREQUAL "switch2")
set(SWITCH_PICO_PROTOCOL_SOURCES
switch2_driver.cpp
switch2_descriptors.cpp
switch2_reports.cpp
switch2_commands.cpp
)
set(SWITCH_PICO_PROTOCOL_DEFINITION SWITCH_PICO_PROTOCOL_SWITCH2=1)
else()
message(FATAL_ERROR
"Invalid SWITCH_PICO_PROTOCOL='${SWITCH_PICO_PROTOCOL}'. Expected legacy or switch2.")
endif()
# Pull in Raspberry Pi Pico SDK (must be before project)
include(pico_sdk_import.cmake)
@ -38,9 +59,12 @@ pico_sdk_init()
add_executable(switch-pico
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_version(switch-pico "0.1")

View file

@ -4,7 +4,9 @@
#include "hardware/uart.h"
#include "pico/stdlib.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
#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);
}
// 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 uint8_t buffer[64];
static uint8_t index = 0;
@ -103,7 +105,7 @@ static bool poll_uart_frames() {
if (expected_len > 0 && index >= expected_len) {
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;
new_data = true;
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() {
bool mounted = tud_mounted();
bool ready = switch_pro_is_ready();
bool ready = switch_protocol_is_ready();
if (mounted != g_last_mounted) {
g_last_mounted = mounted;
@ -156,10 +158,10 @@ int main() {
init_uart_input();
tusb_init();
switch_pro_init();
switch_pro_set_rumble_callback(on_rumble_from_switch);
switch_protocol_init();
switch_protocol_set_rumble_callback(on_rumble_from_switch);
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("[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
(void)new_data;
SwitchInputState state = g_user_state;
switch_pro_set_input(state);
switch_pro_task(); // Push state to the Switch host
switch_protocol_set_input(state);
switch_protocol_task(); // Push state to the Switch host
log_usb_state();
}
}

View file

@ -9,39 +9,10 @@
#include <stdint.h>
#include "switch_input.h"
#define SWITCH_PRO_ENDPOINT_SIZE 64
// HAT report (4 bits)
#define SWITCH_PRO_HAT_UP 0x00
#define SWITCH_PRO_HAT_UPRIGHT 0x01
#define SWITCH_PRO_HAT_RIGHT 0x02
#define SWITCH_PRO_HAT_DOWNRIGHT 0x03
#define SWITCH_PRO_HAT_DOWN 0x04
#define SWITCH_PRO_HAT_DOWNLEFT 0x05
#define SWITCH_PRO_HAT_LEFT 0x06
#define SWITCH_PRO_HAT_UPLEFT 0x07
#define SWITCH_PRO_HAT_NOTHING 0x08
#define SWITCH_PRO_MASK_Y (1U << 0)
#define SWITCH_PRO_MASK_B (1U << 1)
#define SWITCH_PRO_MASK_A (1U << 2)
#define SWITCH_PRO_MASK_X (1U << 3)
#define SWITCH_PRO_MASK_L (1U << 4)
#define SWITCH_PRO_MASK_R (1U << 5)
#define SWITCH_PRO_MASK_ZL (1U << 6)
#define SWITCH_PRO_MASK_ZR (1U << 7)
#define SWITCH_PRO_MASK_MINUS (1U << 8)
#define SWITCH_PRO_MASK_PLUS (1U << 9)
#define SWITCH_PRO_MASK_L3 (1U << 10)
#define SWITCH_PRO_MASK_R3 (1U << 11)
#define SWITCH_PRO_MASK_HOME (1U << 12)
#define SWITCH_PRO_MASK_CAPTURE (1U << 13)
#define SWITCH_PRO_JOYSTICK_MIN 0x0000
#define SWITCH_PRO_JOYSTICK_MID 0x7FFF
#define SWITCH_PRO_JOYSTICK_MAX 0xFFFF
typedef enum {
REPORT_OUTPUT_00 = 0x00,
REPORT_FEATURE = 0x01,

View file

@ -6,6 +6,7 @@
#include <stdio.h>
#include "pico/rand.h"
#include "pico/time.h"
#include "switch_pro_descriptors.h"
#include "tusb.h"
#ifdef SWITCH_PICO_LOG
@ -210,16 +211,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() {
memset(report_buffer, 0x00, sizeof(report_buffer));
report_buffer[0] = REPORT_USB_INPUT_81;
@ -643,112 +634,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) {
rumble_callback = cb;
}

View file

@ -8,46 +8,8 @@
#include <stdbool.h>
#include <stdint.h>
#include "switch_pro_descriptors.h"
typedef struct {
int16_t accel_x;
int16_t accel_y;
int16_t accel_z;
int16_t gyro_x;
int16_t gyro_y;
int16_t gyro_z;
} SwitchImuSample;
typedef struct {
bool dpad_up;
bool dpad_down;
bool dpad_left;
bool dpad_right;
bool button_a;
bool button_b;
bool button_x;
bool button_y;
bool button_l;
bool button_r;
bool button_zl;
bool button_zr;
bool button_plus;
bool button_minus;
bool button_home;
bool button_capture;
bool button_l3;
bool button_r3;
uint16_t lx; // 0-65535
uint16_t ly;
uint16_t rx;
uint16_t ry;
uint8_t imu_sample_count; // 0-3
SwitchImuSample imu_samples[3];
} SwitchInputState;
#include "switch_protocol.h"
// Initialize USB state and calibration before entering the main loop.
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.
void switch_pro_task();
// Convert a packed UART message into controller state (returns true if parsed).
// If out_state is null the parsed state is written directly to the driver.
bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length, SwitchInputState* out_state = nullptr);
// Driver state helpers
bool switch_pro_is_ready();
// Optional callback fired when the host sends a rumble payload (the raw 8 rumble bytes).
typedef void (*SwitchRumbleCallback)(const uint8_t rumble_data[8]);
void switch_pro_set_rumble_callback(SwitchRumbleCallback cb);

View file

@ -1,8 +1,11 @@
// TinyUSB configuration tailored for a single Switch Pro style HID interface.
// Data is derived from TinyUSB examples and tuned for a 64-byte HID endpoint.
// TinyUSB configuration for the selected 64-byte controller protocol.
#ifndef _TUSB_CONFIG_H_
#define _TUSB_CONFIG_H_
#if (defined(SWITCH_PICO_PROTOCOL_LEGACY) + defined(SWITCH_PICO_PROTOCOL_SWITCH2)) != 1
#error "Define exactly one Switch Pico USB protocol"
#endif
#ifdef __cplusplus
extern "C" {
#endif
@ -27,7 +30,15 @@ extern "C" {
#define CFG_TUD_CDC 0
#define CFG_TUD_MSC 0
#define CFG_TUD_MIDI 0
#define CFG_TUD_AUDIO 0
#if defined(SWITCH_PICO_PROTOCOL_SWITCH2)
#define CFG_TUD_VENDOR 1
#define CFG_TUD_VENDOR_EPSIZE 64
#define CFG_TUD_VENDOR_RX_BUFSIZE 64
#define CFG_TUD_VENDOR_TX_BUFSIZE 64
#else
#define CFG_TUD_VENDOR 0
#endif
// Always enable TinyUSB debug at level 2; LOG_PRINTF controls user-facing logs.
#ifdef CFG_TUSB_DEBUG
#undef CFG_TUSB_DEBUG