#include "switch_pro_driver.h" #include #include #include #include #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(factory_config_data); static const SwitchUserCalibration* user_calibration [[maybe_unused]] = reinterpret_cast(user_calibration_data); static std::map 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(s.accel_x & 0xFF); dst[1] = static_cast((s.accel_x >> 8) & 0xFF); dst[2] = static_cast(s.accel_y & 0xFF); dst[3] = static_cast((s.accel_y >> 8) & 0xFF); dst[4] = static_cast(s.accel_z & 0xFF); dst[5] = static_cast((s.accel_z >> 8) & 0xFF); dst[6] = static_cast(s.gyro_x & 0xFF); dst[7] = static_cast((s.gyro_x >> 8) & 0xFF); dst[8] = static_cast(s.gyro_y & 0xFF); dst[9] = static_cast((s.gyro_y >> 8) & 0xFF); dst[10] = static_cast(s.gyro_z & 0xFF); dst[11] = static_cast((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(get_rand_32() % 0xff), static_cast(get_rand_32() % 0xff), static_cast(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(packet[3]) | (static_cast(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(8u + static_cast(imu_count) * 12u); if (payload_len < required_payload_len) { return false; } auto expand_axis = [](uint8_t v) -> uint16_t { return static_cast(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(static_cast(src[0]) | (static_cast(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