switch-pico/switch_pro_driver.cpp
Joey Yakimowich-Payne 731e1d8d15
fix(firmware): correct SPI 0x6080 horizontal offsets to match bridge output
The SPI horizontal offsets at 0x6080 tell the Switch what accelerometer
values to expect when the controller is held in normal gaming position.
The Switch uses this as a gravity reference for its sensor fusion.

Old values (-688, 0, 4038) were from a real Pro Controller's physical
IMU chip. Our bridge sends ~(0, 0, 4096) through the axis reversal
pipeline. The 388-count mismatch on X (0.095G = 5.4° tilt error) caused
the Switch's sensor fusion to continuously fight the gyro data, trying
to correct toward the wrong reference orientation → camera swinging.

New values (0, 0, 4096) match the bridge's output for a still controller
after SDL axis reversal, matching the zeroed calibration origins at 0x6020.
2026-03-16 19:18:37 -06:00

927 lines
33 KiB
C++

#include "switch_pro_driver.h"
#include <algorithm>
#include <cstring>
#include <map>
#include <stdio.h>
#include "pico/rand.h"
#include "pico/time.h"
#include "tusb.h"
#ifdef SWITCH_PICO_LOG
#define LOG_PRINTF(...) printf(__VA_ARGS__)
#else
#define LOG_PRINTF(...) ((void)0)
#endif
// force a report to be sent every X ms
#define SWITCH_PRO_KEEPALIVE_TIMER 5
static SwitchInputState g_input_state{
false, false, false, false,
false, false, false, false, false, false, false, false,
false, false, false, false, false, false,
SWITCH_PRO_JOYSTICK_MID, SWITCH_PRO_JOYSTICK_MID,
SWITCH_PRO_JOYSTICK_MID, SWITCH_PRO_JOYSTICK_MID};
static uint8_t report_buffer[SWITCH_PRO_ENDPOINT_SIZE] = {};
static uint8_t last_report[SWITCH_PRO_ENDPOINT_SIZE] = {};
static SwitchProReport switch_report{};
static uint8_t last_report_counter = 0;
static uint32_t last_report_timer = 0;
static uint32_t last_host_activity_ms = 0;
static bool is_ready = false;
static bool is_initialized = false;
static bool is_report_queued = false;
static bool report_sent = false;
static uint8_t queued_report_id = 0;
static bool forced_ready = false;
static uint8_t handshake_counter = 0;
static SwitchDeviceInfo device_info{};
static uint8_t player_id = 0;
static uint8_t input_mode = 0x30;
static bool is_imu_enabled = false;
static bool is_vibration_enabled = false;
// Optional compile-time colour override (body/buttons/grips).
#if __has_include("controller_color_config.h")
#include "controller_color_config.h"
#endif
#ifndef SWITCH_COLOR_BODY_R
#define SWITCH_COLOR_BODY_R 0x1B
#define SWITCH_COLOR_BODY_G 0x1B
#define SWITCH_COLOR_BODY_B 0x1D
#endif
#ifndef SWITCH_COLOR_BUTTON_R
#define SWITCH_COLOR_BUTTON_R 0xFF
#define SWITCH_COLOR_BUTTON_G 0xFF
#define SWITCH_COLOR_BUTTON_B 0xFF
#endif
#ifndef SWITCH_COLOR_LEFT_GRIP_R
#define SWITCH_COLOR_LEFT_GRIP_R 0xEC
#define SWITCH_COLOR_LEFT_GRIP_G 0x00
#define SWITCH_COLOR_LEFT_GRIP_B 0x8C
#endif
#ifndef SWITCH_COLOR_RIGHT_GRIP_R
#define SWITCH_COLOR_RIGHT_GRIP_R 0xEC
#define SWITCH_COLOR_RIGHT_GRIP_G 0x00
#define SWITCH_COLOR_RIGHT_GRIP_B 0x8C
#endif
static uint16_t leftMinX, leftMinY;
static uint16_t leftCenX, leftCenY;
static uint16_t leftMaxX, leftMaxY;
static uint16_t rightMinX, rightMinY;
static uint16_t rightCenX, rightCenY;
static uint16_t rightMaxX, rightMaxY;
static SwitchRumbleCallback rumble_callback = nullptr;
static const uint8_t factory_config_data[0xEFF] = {
// serial number
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF,
// device type
SWITCH_TYPE_PRO_CONTROLLER,
// unknown
0xA0,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
// color options
0x02,
0xFF, 0xFF, 0xFF, 0xFF,
// config & calibration 1 (6-axis IMU, SPI 0x6020-0x6037)
// Accel origin (0,0,0): bridge pre-corrects for bias, so Switch must not
// apply a second origin offset. Real controllers have hardware DC offsets
// here, but our emulated sensor sends bias-corrected values.
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Accel sensitivity coeff: 0x4000 = 16384 → 4096 LSB/G (matches bridge)
0x00, 0x40, 0x00, 0x40, 0x00, 0x40,
// Gyro origin (0,0,0): bridge removes hardware bias before sending.
// Original values (9, -22, -95) caused phantom 6.7 dps yaw when still.
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Gyro sensitivity coeff: 0x343B = 13371 → 818.5 LSB/rad_s (matches bridge)
0x3B, 0x34, 0x3B, 0x34, 0x3B, 0x34,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
// config & calibration 2
// left stick
0xa4, 0x46, 0x6a, 0x00, 0x08, 0x80, 0xa4, 0x46,
0x6a,
// right stick
0x00, 0x08, 0x80, 0xa4, 0x46, 0x6a, 0xa4, 0x46,
0x6a,
0xFF,
// body color
SWITCH_COLOR_BODY_R, SWITCH_COLOR_BODY_G, SWITCH_COLOR_BODY_B,
// button color
SWITCH_COLOR_BUTTON_R, SWITCH_COLOR_BUTTON_G, SWITCH_COLOR_BUTTON_B,
// left grip color
SWITCH_COLOR_LEFT_GRIP_R, SWITCH_COLOR_LEFT_GRIP_G, SWITCH_COLOR_LEFT_GRIP_B,
// right grip color
SWITCH_COLOR_RIGHT_GRIP_R, SWITCH_COLOR_RIGHT_GRIP_G, SWITCH_COLOR_RIGHT_GRIP_B,
0x01,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF,
// Six-Axis horizontal offsets (SPI 0x6080): expected accel when held in
// gaming position. Must match the bridge's actual output for a still
// controller. Old values (-688,0,4038) were for a real Pro Controller's
// physical IMU; our bridge sends (~0,~0,~4096). The 388-count mismatch
// on X caused the Switch's sensor fusion to fight the gyro → camera swing.
0x00, 0x00, 0x00, 0x00, 0x00, 0x10, // (0, 0, 4096) = 1G on Z
0x0F, 0x30, 0x61, 0xAE, 0x90, 0xD9, 0xD4, 0x14,
0x54, 0x41, 0x15, 0x54, 0xC7, 0x79, 0x9C, 0x33,
0x36, 0x63,
0x0F, 0x30, 0x61, 0xAE, 0x90, 0xD9, 0xD4, 0x14,
0x54, 0x41, 0x15, 0x54,
0xC7,
0x79,
0x9C,
0x33,
0x36,
0x63, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF
};
static const uint8_t user_calibration_data[0x3F] = {
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
// Left Stick
0xB2, 0xA1, 0xa4, 0x46, 0x6a, 0x00, 0x08, 0x80,
0xa4, 0x46, 0x6a,
// Right Stick
0xB2, 0xA1, 0x00, 0x08, 0x80, 0xa4, 0x46, 0x6a,
0xa4, 0x46, 0x6a,
// Motion
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
};
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 std::map<uint32_t, const uint8_t*> spi_flash_data = {
{0x6000, factory_config_data},
{0x8000, user_calibration_data}
};
static inline uint16_t scale16To12(uint16_t pos) { return pos >> 4; }
// Default "at rest" IMU sample: zero gyro, ~1G on accel Z (face-up).
// Written to imuData at boot and whenever no fresh UART data is available,
// so the Switch never sees all-zero IMU (which it interprets as free-fall).
static const uint8_t DEFAULT_IMU_SAMPLE[12] = {
0x00, 0x00, // accel_x = 0
0x00, 0x00, // accel_y = 0
0x00, 0x10, // accel_z = 0x1000 = 4096 = 1G
0x00, 0x00, // gyro_x = 0
0x00, 0x00, // gyro_y = 0
0x00, 0x00, // gyro_z = 0
};
static void fill_imu_report_data(const SwitchInputState& state) {
if (state.imu_sample_count == 0) {
// No new IMU data — fill with default "at rest" sample.
// This prevents the Switch from seeing all-zero accel (free-fall)
// during startup or when the bridge hasn't sent IMU yet.
for (int i = 0; i < 3; ++i) {
memcpy(switch_report.imuData + i * 12, DEFAULT_IMU_SAMPLE, 12);
}
return;
}
uint8_t sample_count = state.imu_sample_count > 3 ? 3 : state.imu_sample_count;
// If fewer than 3 samples, duplicate the last one to fill all 3 slots
uint8_t* dst = switch_report.imuData;
for (uint8_t i = 0; i < 3; ++i) {
const SwitchImuSample& s = (i < sample_count) ? state.imu_samples[i] : state.imu_samples[sample_count - 1];
dst[0] = static_cast<uint8_t>(s.accel_x & 0xFF);
dst[1] = static_cast<uint8_t>((s.accel_x >> 8) & 0xFF);
dst[2] = static_cast<uint8_t>(s.accel_y & 0xFF);
dst[3] = static_cast<uint8_t>((s.accel_y >> 8) & 0xFF);
dst[4] = static_cast<uint8_t>(s.accel_z & 0xFF);
dst[5] = static_cast<uint8_t>((s.accel_z >> 8) & 0xFF);
dst[6] = static_cast<uint8_t>(s.gyro_x & 0xFF);
dst[7] = static_cast<uint8_t>((s.gyro_x >> 8) & 0xFF);
dst[8] = static_cast<uint8_t>(s.gyro_y & 0xFF);
dst[9] = static_cast<uint8_t>((s.gyro_y >> 8) & 0xFF);
dst[10] = static_cast<uint8_t>(s.gyro_z & 0xFF);
dst[11] = static_cast<uint8_t>((s.gyro_z >> 8) & 0xFF);
dst += 12;
}
}
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;
report_buffer[1] = IDENTIFY;
report_buffer[2] = 0x00;
report_buffer[3] = device_info.controllerType;
for (uint8_t i = 0; i < 6; i++) {
report_buffer[4 + i] = device_info.macAddress[5 - i];
}
}
static bool send_report(uint8_t reportID, const void* reportData, uint16_t reportLength) {
bool result = tud_hid_report(reportID, reportData, reportLength);
if (last_report_counter < 255) {
last_report_counter++;
} else {
last_report_counter = 0;
}
if (!result) {
LOG_PRINTF("[HID] send_report failed id=%u len=%u\n", reportID, reportLength);
}
return result;
}
static void read_spi_flash(uint8_t* dest, uint32_t address, uint8_t size) {
uint32_t addressBank = address & 0xFFFFFF00;
uint32_t addressOffset = address & 0x000000FF;
auto it = spi_flash_data.find(addressBank);
if (it != spi_flash_data.end()) {
const uint8_t* data = it->second;
memcpy(dest, data + addressOffset, size);
} else {
memset(dest, 0xFF, size);
}
}
static void forward_rumble_to_host(const uint8_t* report, uint16_t length) {
// Output reports 0x10/0x21 include 8 rumble bytes starting at offset 2.
if (!rumble_callback || length < 10) {
return;
}
uint8_t rumble[8];
memcpy(rumble, report + 2, sizeof(rumble));
rumble_callback(rumble);
}
static void handle_config_report(uint8_t switchReportID, uint8_t switchReportSubID, const uint8_t *reportData, uint16_t reportLength) {
bool canSend = false;
last_host_activity_ms = to_ms_since_boot(get_absolute_time());
switch (switchReportSubID) {
case IDENTIFY:
send_identify();
canSend = true;
LOG_PRINTF("[HID] CONFIG IDENTIFY\n");
break;
case HANDSHAKE:
report_buffer[0] = REPORT_USB_INPUT_81;
report_buffer[1] = HANDSHAKE;
canSend = true;
LOG_PRINTF("[HID] CONFIG HANDSHAKE\n");
break;
case BAUD_RATE:
report_buffer[0] = REPORT_USB_INPUT_81;
report_buffer[1] = BAUD_RATE;
canSend = true;
LOG_PRINTF("[HID] CONFIG BAUD_RATE\n");
break;
case DISABLE_USB_TIMEOUT:
report_buffer[0] = REPORT_OUTPUT_30;
report_buffer[1] = switchReportSubID;
//if (handshakeCounter < 4) {
// handshakeCounter++;
//} else {
is_ready = true;
//}
canSend = true;
LOG_PRINTF("[HID] CONFIG DISABLE_USB_TIMEOUT -> ready\n");
break;
case ENABLE_USB_TIMEOUT:
report_buffer[0] = REPORT_OUTPUT_30;
report_buffer[1] = switchReportSubID;
canSend = true;
LOG_PRINTF("[HID] CONFIG ENABLE_USB_TIMEOUT\n");
break;
default:
report_buffer[0] = REPORT_OUTPUT_30;
report_buffer[1] = switchReportSubID;
canSend = true;
LOG_PRINTF("[HID] CONFIG unknown subid=0x%02x\n", switchReportSubID);
break;
}
if (canSend) is_report_queued = true;
}
static void handle_feature_report(uint8_t switchReportID, uint8_t switchReportSubID, const uint8_t *reportData, uint16_t reportLength) {
uint8_t commandID = reportData[10];
uint32_t spiReadAddress = 0;
uint8_t spiReadSize = 0;
bool canSend = false;
last_host_activity_ms = to_ms_since_boot(get_absolute_time());
LOG_PRINTF("[HID] handle_feature rid=0x%02x cmd=0x%02x imu_enabled=%d\n",
switchReportID, commandID, is_imu_enabled);
report_buffer[0] = REPORT_OUTPUT_21;
report_buffer[1] = last_report_counter;
memcpy(report_buffer + 2, &switch_report.inputs, sizeof(SwitchInputReport));
switch (commandID) {
case GET_CONTROLLER_STATE:
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
report_buffer[15] = 0x03;
canSend = true;
LOG_PRINTF("[HID] FEATURE GET_CONTROLLER_STATE\n");
break;
case BLUETOOTH_PAIR_REQUEST:
report_buffer[13] = 0x81;
report_buffer[14] = commandID;
report_buffer[15] = 0x03;
canSend = true;
LOG_PRINTF("[HID] FEATURE BLUETOOTH_PAIR_REQUEST\n");
break;
case REQUEST_DEVICE_INFO:
report_buffer[13] = 0x82;
report_buffer[14] = 0x02;
memcpy(&report_buffer[15], &device_info, sizeof(device_info));
canSend = true;
LOG_PRINTF("[HID] FEATURE REQUEST_DEVICE_INFO\n");
break;
case SET_MODE:
input_mode = reportData[11];
report_buffer[13] = 0x80;
report_buffer[14] = 0x03;
report_buffer[15] = input_mode;
canSend = true;
LOG_PRINTF("[HID] FEATURE SET_MODE 0x%02x\n", input_mode);
break;
case TRIGGER_BUTTONS:
report_buffer[13] = 0x83;
report_buffer[14] = 0x04;
canSend = true;
LOG_PRINTF("[HID] FEATURE TRIGGER_BUTTONS\n");
break;
case SET_SHIPMENT:
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
canSend = true;
LOG_PRINTF("[HID] FEATURE SET_SHIPMENT\n");
break;
case SPI_READ:
spiReadAddress = (reportData[14] << 24) | (reportData[13] << 16) | (reportData[12] << 8) | (reportData[11]);
spiReadSize = reportData[15];
report_buffer[13] = 0x90;
report_buffer[14] = reportData[10];
report_buffer[15] = reportData[11];
report_buffer[16] = reportData[12];
report_buffer[17] = reportData[13];
report_buffer[18] = reportData[14];
report_buffer[19] = reportData[15];
read_spi_flash(&report_buffer[20], spiReadAddress, spiReadSize);
canSend = true;
LOG_PRINTF("[HID] FEATURE SPI_READ addr=0x%08lx size=%u\n", (unsigned long)spiReadAddress, spiReadSize);
break;
case SET_NFC_IR_CONFIG:
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
canSend = true;
LOG_PRINTF("[HID] FEATURE SET_NFC_IR_CONFIG\n");
break;
case SET_NFC_IR_STATE:
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
canSend = true;
LOG_PRINTF("[HID] FEATURE SET_NFC_IR_STATE\n");
break;
case SET_PLAYER_LIGHTS:
player_id = reportData[11];
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
canSend = true;
LOG_PRINTF("[HID] FEATURE SET_PLAYER_LIGHTS player=%u\n", player_id);
break;
case GET_PLAYER_LIGHTS:
player_id = reportData[11];
report_buffer[13] = 0xB0;
report_buffer[14] = commandID;
report_buffer[15] = player_id;
canSend = true;
LOG_PRINTF("[HID] FEATURE GET_PLAYER_LIGHTS player=%u\n", player_id);
break;
case COMMAND_UNKNOWN_33:
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
report_buffer[15] = 0x03;
canSend = true;
LOG_PRINTF("[HID] FEATURE COMMAND_UNKNOWN_33\n");
break;
case SET_HOME_LIGHT:
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
report_buffer[15] = 0x00;
canSend = true;
LOG_PRINTF("[HID] FEATURE SET_HOME_LIGHT\n");
break;
case TOGGLE_IMU:
is_imu_enabled = reportData[11];
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
report_buffer[15] = 0x00;
canSend = true;
LOG_PRINTF("[HID] FEATURE TOGGLE_IMU %u\n", is_imu_enabled);
break;
case IMU_SENSITIVITY:
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
canSend = true;
LOG_PRINTF("[HID] FEATURE IMU_SENSITIVITY\n");
break;
case ENABLE_VIBRATION:
is_vibration_enabled = reportData[11];
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
report_buffer[15] = 0x00;
canSend = true;
LOG_PRINTF("[HID] FEATURE ENABLE_VIBRATION %u\n", is_vibration_enabled);
break;
case READ_IMU:
report_buffer[13] = 0xC0;
report_buffer[14] = commandID;
report_buffer[15] = reportData[11];
report_buffer[16] = reportData[12];
canSend = true;
LOG_PRINTF("[HID] FEATURE READ_IMU addr=%u size=%u\n", reportData[11], reportData[12]);
break;
case GET_VOLTAGE:
report_buffer[13] = 0xD0;
report_buffer[14] = 0x50;
report_buffer[15] = 0x83;
report_buffer[16] = 0x06;
canSend = true;
LOG_PRINTF("[HID] FEATURE GET_VOLTAGE\n");
break;
default:
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
report_buffer[15] = 0x03;
canSend = true;
LOG_PRINTF("[HID] FEATURE unknown cmd=0x%02x\n", commandID);
break;
}
if (canSend) is_report_queued = true;
}
static void update_switch_report_from_state() {
switch_report.inputs.dpadUp = g_input_state.dpad_up;
switch_report.inputs.dpadDown = g_input_state.dpad_down;
switch_report.inputs.dpadLeft = g_input_state.dpad_left;
switch_report.inputs.dpadRight = g_input_state.dpad_right;
switch_report.inputs.chargingGrip = 1;
switch_report.inputs.buttonY = g_input_state.button_y;
switch_report.inputs.buttonX = g_input_state.button_x;
switch_report.inputs.buttonB = g_input_state.button_b;
switch_report.inputs.buttonA = g_input_state.button_a;
switch_report.inputs.buttonRightSR = 0;
switch_report.inputs.buttonRightSL = 0;
switch_report.inputs.buttonR = g_input_state.button_r;
switch_report.inputs.buttonZR = g_input_state.button_zr;
switch_report.inputs.buttonMinus = g_input_state.button_minus;
switch_report.inputs.buttonPlus = g_input_state.button_plus;
switch_report.inputs.buttonThumbR = g_input_state.button_r3;
switch_report.inputs.buttonThumbL = g_input_state.button_l3;
switch_report.inputs.buttonHome = g_input_state.button_home;
switch_report.inputs.buttonCapture = g_input_state.button_capture;
switch_report.inputs.buttonLeftSR = 0;
switch_report.inputs.buttonLeftSL = 0;
switch_report.inputs.buttonL = g_input_state.button_l;
switch_report.inputs.buttonZL = g_input_state.button_zl;
uint16_t scaleLeftStickX = scale16To12(g_input_state.lx);
uint16_t scaleLeftStickY = scale16To12(g_input_state.ly);
uint16_t scaleRightStickX = scale16To12(g_input_state.rx);
uint16_t scaleRightStickY = scale16To12(g_input_state.ry);
switch_report.inputs.leftStick.setX(std::min(std::max(scaleLeftStickX,leftMinX), leftMaxX));
switch_report.inputs.leftStick.setY(-std::min(std::max(scaleLeftStickY,leftMinY), leftMaxY));
switch_report.inputs.rightStick.setX(std::min(std::max(scaleRightStickX,rightMinX), rightMaxX));
switch_report.inputs.rightStick.setY(-std::min(std::max(scaleRightStickY,rightMinY), rightMaxY));
fill_imu_report_data(g_input_state);
switch_report.rumbleReport = 0x09;
}
void switch_pro_init() {
player_id = 0;
last_report_counter = 0;
handshake_counter = 0;
is_ready = false;
is_initialized = false;
is_report_queued = false;
report_sent = false;
forced_ready = false;
forced_ready = true;
is_ready = true;
is_initialized = true;
last_report_timer = 0;
device_info = {
.majorVersion = 0x04,
.minorVersion = 0x91,
.controllerType = SWITCH_TYPE_PRO_CONTROLLER,
.unknown00 = 0x02,
.macAddress = {0x7c, 0xbb, 0x8a, static_cast<uint8_t>(get_rand_32() % 0xff), static_cast<uint8_t>(get_rand_32() % 0xff), static_cast<uint8_t>(get_rand_32() % 0xff)},
.unknown01 = 0x01,
.storedColors = 0x02,
};
switch_report = {
.reportID = 0x30,
.timestamp = 0,
.inputs {
.connectionInfo = 0x08, // wired connection
.batteryLevel = 0x0F, // full battery
.buttonY = 0,
.buttonX = 0,
.buttonB = 0,
.buttonA = 0,
.buttonRightSR = 0,
.buttonRightSL = 0,
.buttonR = 0,
.buttonZR = 0,
.buttonMinus = 0,
.buttonPlus = 0,
.buttonThumbR = 0,
.buttonThumbL = 0,
.buttonHome = 0,
.buttonCapture = 0,
.dummy = 0,
.chargingGrip = 0,
.dpadDown = 0,
.dpadUp = 0,
.dpadRight = 0,
.dpadLeft = 0,
.buttonLeftSL = 0,
.buttonLeftSR = 0,
.buttonL = 0,
.buttonZL = 0,
.leftStick = {0xFF, 0xF7, 0x7F},
.rightStick = {0xFF, 0xF7, 0x7F},
},
.rumbleReport = 0,
.imuData = {0x00},
.padding = {0x00}
};
last_report_timer = to_ms_since_boot(get_absolute_time());
last_host_activity_ms = last_report_timer;
factory_config->leftStickCalibration.getRealMin(leftMinX, leftMinY);
factory_config->leftStickCalibration.getCenter(leftCenX, leftCenY);
factory_config->leftStickCalibration.getRealMax(leftMaxX, leftMaxY);
factory_config->rightStickCalibration.getRealMin(rightMinX, rightMinY);
factory_config->rightStickCalibration.getCenter(rightCenX, rightCenY);
factory_config->rightStickCalibration.getRealMax(rightMaxX, rightMaxY);
}
void switch_pro_set_input(const SwitchInputState& state) {
g_input_state = state;
}
void switch_pro_task() {
uint32_t now = to_ms_since_boot(get_absolute_time());
report_sent = false;
update_switch_report_from_state();
if (tud_suspended()) {
tud_remote_wakeup();
}
if (is_report_queued) {
if ((now - last_report_timer) > SWITCH_PRO_KEEPALIVE_TIMER) {
if (tud_hid_ready() && send_report(queued_report_id, report_buffer, 64) == true ) {
is_report_queued = false;
last_report_timer = now;
}
}
report_sent = true;
}
if (is_ready && !report_sent) {
if ((now - last_report_timer) > SWITCH_PRO_KEEPALIVE_TIMER) {
switch_report.timestamp = last_report_counter;
void * inputReport = &switch_report;
uint16_t report_size = sizeof(switch_report);
if (tud_hid_ready() && send_report(0, inputReport, report_size) == true ) {
memcpy(last_report, inputReport, report_size);
// Log IMU data being sent (throttled to ~4Hz to avoid flooding UART0)
static uint32_t last_imu_log = 0;
if (now - last_imu_log > 250) {
last_imu_log = now;
int16_t ax = (int16_t)(switch_report.imuData[0] | (switch_report.imuData[1] << 8));
int16_t ay = (int16_t)(switch_report.imuData[2] | (switch_report.imuData[3] << 8));
int16_t az = (int16_t)(switch_report.imuData[4] | (switch_report.imuData[5] << 8));
int16_t gx = (int16_t)(switch_report.imuData[6] | (switch_report.imuData[7] << 8));
int16_t gy = (int16_t)(switch_report.imuData[8] | (switch_report.imuData[9] << 8));
int16_t gz = (int16_t)(switch_report.imuData[10] | (switch_report.imuData[11] << 8));
LOG_PRINTF("[IMU_OUT] a=(%d,%d,%d) g=(%d,%d,%d) cnt=%d\n",
ax, ay, az, gx, gy, gz, g_input_state.imu_sample_count);
}
g_input_state.imu_sample_count = 0;
report_sent = true;
}
last_report_timer = now;
}
} else {
if (!is_initialized) {
send_identify();
if (tud_hid_ready() && tud_hid_report(0, report_buffer, 64) == true) {
is_initialized = true;
report_sent = true;
}
last_report_timer = now;
}
}
}
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;
}
bool switch_pro_is_ready() {
return is_ready;
}
// HID callbacks
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) {
(void)instance;
LOG_PRINTF("[HID] get_report id=%u type=%u len=%u\n", report_id, report_type, reqlen);
if (!buffer) return 0;
// Serve the current input report for any GET_REPORT request.
uint16_t report_size = sizeof(switch_report);
if (reqlen < report_size) report_size = reqlen;
memcpy(buffer, &switch_report, report_size);
return report_size;
}
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;
memset(report_buffer, 0x00, bufsize);
uint8_t switchReportID = buffer[0];
uint8_t switchReportSubID = buffer[1];
LOG_PRINTF("[HID] set_report type=%d id=%u switchRID=0x%02x sub=0x%02x len=%u\n",
report_type, report_id, switchReportID, switchReportSubID, bufsize);
if (switchReportID == REPORT_OUTPUT_10 || switchReportID == REPORT_OUTPUT_21) {
forward_rumble_to_host(buffer, bufsize);
}
if (switchReportID == REPORT_OUTPUT_00) {
// No-op, just acknowledge to clear any stalls.
return;
} else if (switchReportID == REPORT_FEATURE) {
queued_report_id = report_id;
handle_feature_report(switchReportID, switchReportSubID, buffer, bufsize);
} else if (switchReportID == REPORT_CONFIGURATION) {
queued_report_id = report_id;
handle_config_report(switchReportID, switchReportSubID, buffer, bufsize);
}
}
void tud_hid_report_received_cb(uint8_t instance, uint8_t report_id, uint8_t const* buffer, uint16_t bufsize) {
(void)instance;
// Host sent data on interrupt OUT; mirror the control path handling.
memset(report_buffer, 0x00, bufsize);
uint8_t switchReportID = buffer[0];
uint8_t switchReportSubID = buffer[1];
LOG_PRINTF("[HID] report_received id=%u switchRID=0x%02x sub=0x%02x len=%u\n",
report_id, switchReportID, switchReportSubID, bufsize);
if (switchReportID == REPORT_OUTPUT_10 || switchReportID == REPORT_OUTPUT_21) {
forward_rumble_to_host(buffer, bufsize);
}
if (switchReportID == REPORT_OUTPUT_00) {
return;
} else if (switchReportID == REPORT_FEATURE) {
queued_report_id = report_id;
handle_feature_report(switchReportID, switchReportSubID, buffer, bufsize);
} else if (switchReportID == REPORT_CONFIGURATION) {
queued_report_id = report_id;
handle_config_report(switchReportID, switchReportSubID, buffer, bufsize);
}
}
uint8_t const * tud_hid_descriptor_report_cb(uint8_t itf) {
(void)itf;
return switch_pro_report_descriptor;
}
uint8_t const * tud_descriptor_device_cb(void) {
return switch_pro_device_descriptor;
}
uint8_t const * tud_descriptor_configuration_cb(uint8_t index) {
(void)index;
return switch_pro_configuration_descriptor;
}
bool tud_control_request_cb(uint8_t rhport, tusb_control_request_t const * request) {
(void)rhport;
LOG_PRINTF("[CTRL] bmReq=0x%02x bReq=0x%02x wValue=0x%04x wIndex=0x%04x wLen=%u\n",
request->bmRequestType, request->bRequest, request->wValue, request->wIndex, request->wLength);
return false; // let TinyUSB handle it normally
}
void tud_mount_cb(void) {
LOG_PRINTF("[USB] mount_cb\n");
last_host_activity_ms = to_ms_since_boot(get_absolute_time());
forced_ready = false;
is_ready = false;
is_initialized = false;
}
void tud_umount_cb(void) {
LOG_PRINTF("[USB] umount_cb\n");
forced_ready = false;
is_ready = false;
is_initialized = false;
}
static uint16_t desc_str[32];
uint16_t const * tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
(void)langid;
uint8_t chr_count;
if ( index == 0 ) {
memcpy(&desc_str[1], switch_pro_string_language, 2);
chr_count = 1;
} else {
if ( index >= sizeof(switch_pro_string_descriptors)/sizeof(switch_pro_string_descriptors[0]) ) return nullptr;
const uint8_t *str = switch_pro_string_descriptors[index];
chr_count = 0;
while ( str[chr_count] ) chr_count++;
if ( chr_count > 31 ) chr_count = 31;
for(uint8_t i=0; i<chr_count; i++) {
desc_str[1+i] = str[i];
}
}
desc_str[0] = (uint16_t) ((0x03 << 8 ) | (2*chr_count + 2));
return desc_str;
}