switch-pico/switch_pro_driver.cpp

896 lines
31 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 (roughly matches Pro Controller cadence)
#define SWITCH_PRO_KEEPALIVE_TIMER 15
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
0xE3, 0xFF, 0x39, 0xFF, 0xED, 0x01, 0x00, 0x40,
0x00, 0x40, 0x00, 0x40, 0x09, 0x00, 0xEA, 0xFF,
0xA1, 0xFF, 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,
0x50, 0xFD, 0x00, 0x00, 0xC6, 0x0F,
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; }
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 fill_imu_report_data(const SwitchInputState& state) {
// Include IMU data when the host provided samples; otherwise zero.
if (state.imu_sample_count == 0) {
memset(switch_report.imuData, 0x00, sizeof(switch_report.imuData));
return;
}
uint8_t sample_count = state.imu_sample_count > 3 ? 3 : state.imu_sample_count;
uint8_t* dst = switch_report.imuData;
// Map host IMU axes (host already sends Pro-oriented X,Z,Y) into report layout:
// Report order per sample: accel_x, accel_y, accel_z, gyro_x, gyro_y, gyro_z.
for (uint8_t i = 0; i < 3; ++i) {
SwitchImuSample sample{};
if (i < sample_count) {
sample = state.imu_samples[i];
}
dst[0] = static_cast<uint8_t>(sample.accel_x & 0xFF);
dst[1] = static_cast<uint8_t>((sample.accel_x >> 8) & 0xFF);
dst[2] = static_cast<uint8_t>(sample.accel_y & 0xFF);
dst[3] = static_cast<uint8_t>((sample.accel_y >> 8) & 0xFF);
dst[4] = static_cast<uint8_t>(sample.accel_z & 0xFF);
dst[5] = static_cast<uint8_t>((sample.accel_z >> 8) & 0xFF);
dst[6] = static_cast<uint8_t>(sample.gyro_x & 0xFF);
dst[7] = static_cast<uint8_t>((sample.gyro_x >> 8) & 0xFF);
dst[8] = static_cast<uint8_t>(sample.gyro_y & 0xFF);
dst[9] = static_cast<uint8_t>((sample.gyro_y >> 8) & 0xFF);
dst[10] = static_cast<uint8_t>(sample.gyro_z & 0xFF);
dst[11] = static_cast<uint8_t>((sample.gyro_z >> 8) & 0xFF);
dst += 12;
}
}
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());
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_imu_enabled = true; // default on to allow IMU during host bring-up/debug
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);
report_sent = true;
// Clear IMU samples so they aren't repeated in the next report
// if no new data arrives.
g_input_state.imu_sample_count = 0;
}
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) {
// Packet v2 format:
// 0:0xAA header
// 1:version (0x02)
// 2:payload_len (bytes 3..3+len-1)
// 3-4: buttons LE
// 5: hat
// 6-9: lx, ly, rx, ry (0-255)
// 10: imu_sample_count (0-3)
// 11-46: up to 3 samples of accel/gyro (int16 LE each axis)
// 47: checksum (sum of bytes 0..46) & 0xFF
if (length < 8 || packet[0] != 0xAA) {
return false;
}
if (packet[1] != 0x02) {
return false;
}
uint8_t payload_len = packet[2];
uint16_t expected_len = static_cast<uint16_t>(payload_len) + 4; // header+version+len+checksum
if (length < expected_len) {
return false;
}
uint16_t checksum_end = static_cast<uint16_t>(3 + payload_len - 1); // last payload byte
uint16_t checksum_index = static_cast<uint16_t>(3 + payload_len);
if (checksum_index >= length) {
return false;
}
uint16_t sum = 0;
for (uint16_t i = 0; i <= checksum_end; ++i) {
sum = static_cast<uint16_t>(sum + packet[i]);
}
if ((sum & 0xFF) != packet[checksum_index]) {
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];
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 = std::min<uint8_t>(packet[10], 3);
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));
};
const uint8_t* imu_base = &packet[11];
for (uint8_t i = 0; i < state.imu_sample_count; ++i) {
const uint8_t* sample_ptr = imu_base + (i * 12);
state.imu_samples[i].accel_x = read_int16(sample_ptr + 0);
state.imu_samples[i].accel_y = read_int16(sample_ptr + 2);
state.imu_samples[i].accel_z = read_int16(sample_ptr + 4);
state.imu_samples[i].gyro_x = read_int16(sample_ptr + 6);
state.imu_samples[i].gyro_y = read_int16(sample_ptr + 8);
state.imu_samples[i].gyro_z = read_int16(sample_ptr + 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) {
*out_state = state;
} else {
switch_pro_set_input(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);
} else {
}
}
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 = true;
is_ready = true;
is_initialized = true;
}
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;
}