#include "switch_pro_driver.h" #include #include #include #include #include #include "pico/rand.h" #include "pico/time.h" #include "tusb.h" #ifdef SWITCH_PICO_ADAPTER_FEASIBILITY #include "adapter_host_probe.h" #include "xinput_feasibility_descriptors.h" #endif #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 // Real Pro Controller cadence: 3 IMU frames per report, 5ms/frame => 15ms/report // (~66.7Hz). Emitting the 3-frame 0x30 faster makes the console over-integrate // gyro (3 frames assumed 5ms apart delivered too often) => wild camera swing. #define SWITCH_PRO_IMU_REPORT_TIMER 15 enum class SwitchImuMode : uint8_t { Off = 0, Raw = 1, Quaternion = 2, }; struct MotionQuaternion { float x; float y; float z; float w; int16_t accel_x; int16_t accel_y; int16_t accel_z; }; struct SwitchProContext { SwitchInputState input_state{}; uint8_t report_buffer[SWITCH_PRO_ENDPOINT_SIZE]{}; SwitchProReport switch_report{}; uint8_t last_report_counter = 0; uint32_t last_report_timer = 0; bool is_ready = false; bool is_initialized = false; bool is_report_queued = false; SwitchDeviceInfo device_info{}; SwitchRgbColor grip_color{}; uint8_t player_id = 0; uint8_t input_mode = 0x30; SwitchImuMode imu_mode = SwitchImuMode::Off; bool is_vibration_enabled = false; uint16_t left_min_x = 0; uint16_t left_min_y = 0; uint16_t left_max_x = 0; uint16_t left_max_y = 0; uint16_t right_min_x = 0; uint16_t right_min_y = 0; uint16_t right_max_x = 0; uint16_t right_max_y = 0; SwitchRumbleCallback rumble_callback = nullptr; SwitchHapticsDecoder rumble_decoder{}; MotionQuaternion motion_quaternion{0.0f, 0.0f, 0.0f, 1.0f, 0, 0, 0}; }; static_assert(SWITCH_PICO_HID_INSTANCE_COUNT > 0, "at least one HID instance is required"); static SwitchProContext contexts[SWITCH_PICO_HID_INSTANCE_COUNT]{}; static SwitchProContext* context_for(uint8_t instance) { if (instance >= SWITCH_PICO_HID_INSTANCE_COUNT) { return nullptr; } return &contexts[instance]; } // Optional compile-time color palette shared with Bluetooth controller LEDs. #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_SLOT_1_R #define SWITCH_COLOR_SLOT_1_R 0x00 #define SWITCH_COLOR_SLOT_1_G 0x89 #define SWITCH_COLOR_SLOT_1_B 0xEB #define SWITCH_COLOR_SLOT_2_R 0xE6 #define SWITCH_COLOR_SLOT_2_G 0x39 #define SWITCH_COLOR_SLOT_2_B 0x46 #define SWITCH_COLOR_SLOT_3_R 0xF6 #define SWITCH_COLOR_SLOT_3_G 0xC9 #define SWITCH_COLOR_SLOT_3_B 0x45 #define SWITCH_COLOR_SLOT_4_R 0x2E #define SWITCH_COLOR_SLOT_4_G 0xCC #define SWITCH_COLOR_SLOT_4_B 0x71 #endif static constexpr SwitchRgbColor slot_colors[] = { {SWITCH_COLOR_SLOT_1_R, SWITCH_COLOR_SLOT_1_G, SWITCH_COLOR_SLOT_1_B}, {SWITCH_COLOR_SLOT_2_R, SWITCH_COLOR_SLOT_2_G, SWITCH_COLOR_SLOT_2_B}, {SWITCH_COLOR_SLOT_3_R, SWITCH_COLOR_SLOT_3_G, SWITCH_COLOR_SLOT_3_B}, {SWITCH_COLOR_SLOT_4_R, SWITCH_COLOR_SLOT_4_G, SWITCH_COLOR_SLOT_4_B}, }; static_assert(SWITCH_PICO_HID_INSTANCE_COUNT <= sizeof(slot_colors) / sizeof(slot_colors[0])); SwitchRgbColor switch_pro_get_slot_color(uint8_t instance) { if (instance >= sizeof(slot_colors) / sizeof(slot_colors[0])) { return {}; } return slot_colors[instance]; } SwitchRgbColor switch_pro_get_slot_light_color(uint8_t instance) { if (instance >= sizeof(slot_colors) / sizeof(slot_colors[0])) { return {}; } return switch_pro_calibrate_light_color(slot_colors[instance]); } 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 // IMU calibration @ 0x6020: zero offsets, 4096 LSB/g, and // 818.5 LSB/(rad/s), matching the bridge's raw-value conversion. 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 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_SLOT_1_R, SWITCH_COLOR_SLOT_1_G, SWITCH_COLOR_SLOT_1_B, // right grip color SWITCH_COLOR_SLOT_1_R, SWITCH_COLOR_SLOT_1_G, SWITCH_COLOR_SLOT_1_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* const factory_config = reinterpret_cast(factory_config_data); static inline uint16_t scale16To12(uint16_t pos) { return pos >> 4; } static void reset_motion_quaternion(SwitchProContext& context) { context.motion_quaternion = {0.0f, 0.0f, 0.0f, 1.0f, 0, 0, 0}; } static void write_int16_le(uint8_t* dst, int16_t value) { dst[0] = static_cast(value & 0xFF); dst[1] = static_cast((value >> 8) & 0xFF); } static void write_bits_le(uint8_t* dst, uint16_t bit_offset, uint32_t value, uint8_t width) { for (uint8_t bit = 0; bit < width; ++bit) { if ((value & (1u << bit)) != 0) { uint16_t output_bit = static_cast(bit_offset + bit); dst[output_bit >> 3] |= static_cast(1u << (output_bit & 7u)); } } } static void integrate_motion_sample(SwitchProContext& context, const SwitchImuSample& sample) { constexpr float sample_dt = 0.005f; constexpr float gyro_rad_per_lsb = 1.0f / 818.5f; MotionQuaternion& quaternion = context.motion_quaternion; // Nintendo mode 2 uses Y, X, Z sensor order for quaternion axes. float angle_x = static_cast(sample.gyro_y) * gyro_rad_per_lsb * sample_dt; float angle_y = static_cast(sample.gyro_x) * gyro_rad_per_lsb * sample_dt; float angle_z = static_cast(sample.gyro_z) * gyro_rad_per_lsb * sample_dt; float norm = sqrtf(angle_x * angle_x + angle_y * angle_y + angle_z * angle_z); float half = 0.5f * norm; float vector_scale = norm > 1e-12f ? sinf(half) / norm : 0.5f; float scalar = norm > 1e-12f ? cosf(half) : 1.0f; float dx = angle_x * vector_scale; float dy = angle_y * vector_scale; float dz = angle_z * vector_scale; float dw = scalar; float x = quaternion.w * dx + quaternion.x * dw + quaternion.y * dz - quaternion.z * dy; float y = quaternion.w * dy - quaternion.x * dz + quaternion.y * dw + quaternion.z * dx; float z = quaternion.w * dz + quaternion.x * dy - quaternion.y * dx + quaternion.z * dw; float w = quaternion.w * dw - quaternion.x * dx - quaternion.y * dy - quaternion.z * dz; float magnitude = sqrtf(x * x + y * y + z * z + w * w); if (magnitude > 1e-12f) { float inverse = 1.0f / magnitude; quaternion.x = x * inverse; quaternion.y = y * inverse; quaternion.z = z * inverse; quaternion.w = w * inverse; } else { reset_motion_quaternion(context); } quaternion.accel_x = sample.accel_x; quaternion.accel_y = sample.accel_y; quaternion.accel_z = sample.accel_z; } static void fill_raw_imu_report_data(SwitchProContext& context, const SwitchInputState& state) { if (state.imu_sample_count == 0) { memset(context.switch_report.imuData, 0x00, sizeof(context.switch_report.imuData)); return; } uint8_t sample_count = state.imu_sample_count > 3 ? 3 : state.imu_sample_count; uint8_t* dst = context.switch_report.imuData; for (uint8_t i = 0; i < 3; ++i) { const SwitchImuSample& sample = (i < sample_count) ? state.imu_samples[i] : state.imu_samples[sample_count - 1]; write_int16_le(dst + 0, sample.accel_x); write_int16_le(dst + 2, sample.accel_y); write_int16_le(dst + 4, sample.accel_z); write_int16_le(dst + 6, sample.gyro_x); write_int16_le(dst + 8, sample.gyro_y); write_int16_le(dst + 10, sample.gyro_z); dst += 12; } } static void fill_quaternion_imu_report_data(SwitchProContext& context, const SwitchInputState& state, uint32_t now_ms) { if (state.imu_sample_count > 0) { uint8_t sample_count = state.imu_sample_count > 3 ? 3 : state.imu_sample_count; for (uint8_t i = 0; i < 3; ++i) { const SwitchImuSample& sample = (i < sample_count) ? state.imu_samples[i] : state.imu_samples[sample_count - 1]; integrate_motion_sample(context, sample); } } MotionQuaternion& quaternion = context.motion_quaternion; uint8_t* dst = context.switch_report.imuData; memset(dst, 0x00, sizeof(context.switch_report.imuData)); // Mode 2 accelerometer vectors are encoded in Y, X, Z order. write_int16_le(dst + 0, quaternion.accel_y); write_int16_le(dst + 2, quaternion.accel_x); write_int16_le(dst + 4, quaternion.accel_z); float components[4] = { quaternion.x, quaternion.y, quaternion.z, quaternion.w, }; uint8_t max_index = 0; for (uint8_t i = 1; i < 4; ++i) { if (fabsf(components[i]) > fabsf(components[max_index])) { max_index = i; } } uint32_t packed_component[3]{}; float sign = components[max_index] < 0.0f ? -1.0f : 1.0f; for (uint8_t i = 0; i < 3; ++i) { int32_t component = static_cast( components[(max_index + i + 1) & 3] * 1073741824.0f * sign); packed_component[i] = static_cast(component >> 10) & 0x1FFFFFu; } write_bits_le(dst, 48, 2, 2); write_bits_le(dst, 50, max_index, 2); write_bits_le(dst, 52, packed_component[0], 21); write_bits_le(dst, 73, packed_component[1], 21); write_bits_le(dst, 94, packed_component[2] & 0x3u, 2); write_bits_le(dst, 144, packed_component[2] >> 2, 19); uint16_t timestamp = static_cast(now_ms & 0x7FFu); write_bits_le(dst, 271, timestamp, 11); write_bits_le(dst, 282, 3, 6); } static void fill_imu_report_data(SwitchProContext& context, const SwitchInputState& state, uint32_t now_ms) { switch (context.imu_mode) { case SwitchImuMode::Raw: fill_raw_imu_report_data(context, state); break; case SwitchImuMode::Quaternion: fill_quaternion_imu_report_data(context, state, now_ms); break; case SwitchImuMode::Off: default: memset(context.switch_report.imuData, 0x00, sizeof(context.switch_report.imuData)); break; } } static void update_switch_report_from_state(SwitchProContext& context); 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 reset_context_runtime(SwitchProContext& context, uint32_t now, bool ready_before_mount) { context.input_state = make_neutral_state(); memset(context.report_buffer, 0x00, sizeof(context.report_buffer)); context.switch_report = {}; context.switch_report.reportID = 0x30; context.switch_report.inputs.connectionInfo = 0x01; context.switch_report.inputs.batteryLevel = 0x08; update_switch_report_from_state(context); context.last_report_counter = 0; context.last_report_timer = now; context.is_ready = ready_before_mount; context.is_initialized = ready_before_mount; context.is_report_queued = false; context.player_id = 0; context.input_mode = 0x30; context.imu_mode = SwitchImuMode::Off; context.is_vibration_enabled = false; context.rumble_decoder.reset(); reset_motion_quaternion(context); } static void reset_all_contexts(bool ready_before_mount) { uint32_t now = to_ms_since_boot(get_absolute_time()); for (uint8_t instance = 0; instance < SWITCH_PICO_HID_INSTANCE_COUNT; ++instance) { reset_context_runtime(contexts[instance], now, ready_before_mount); } } static void send_identify(SwitchProContext& context) { memset(context.report_buffer, 0x00, sizeof(context.report_buffer)); context.report_buffer[0] = REPORT_USB_INPUT_81; context.report_buffer[1] = IDENTIFY; context.report_buffer[2] = 0x00; context.report_buffer[3] = context.device_info.controllerType; for (uint8_t i = 0; i < 6; ++i) { context.report_buffer[4 + i] = context.device_info.macAddress[5 - i]; } } static bool send_report(uint8_t instance, SwitchProContext& context, uint8_t report_id, const void* report_data, uint16_t report_length) { bool result = tud_hid_n_report(instance, report_id, report_data, report_length); ++context.last_report_counter; if (!result) { LOG_PRINTF("[HID %u] send_report failed id=%u len=%u\n", instance, report_id, report_length); } return result; } static void read_spi_flash(const SwitchProContext& context, uint8_t* dest, uint32_t address, uint8_t size) { uint32_t address_bank = address & 0xFFFFFF00u; uint32_t address_offset = address & 0x000000FFu; const uint8_t* data = nullptr; if (address_bank == 0x6000u) { data = factory_config_data; } else if (address_bank == 0x8000u) { data = user_calibration_data; } if (data == nullptr) { memset(dest, 0xFF, size); return; } memcpy(dest, data + address_offset, size); if (address_bank != 0x6000u) { return; } static_assert(sizeof(SwitchRgbColor) == sizeof(SwitchColorDefinition)); const uint8_t* color = reinterpret_cast(&context.grip_color); constexpr size_t left_offset = offsetof(SwitchFactoryConfig, leftGripColor); constexpr size_t right_offset = offsetof(SwitchFactoryConfig, rightGripColor); for (uint8_t index = 0; index < size; ++index) { const size_t offset = address_offset + index; if (offset >= left_offset && offset < left_offset + sizeof(SwitchRgbColor)) { dest[index] = color[offset - left_offset]; } else if (offset >= right_offset && offset < right_offset + sizeof(SwitchRgbColor)) { dest[index] = color[offset - right_offset]; } } } static void forward_decoded_rumble(uint8_t instance, SwitchProContext& context, const uint8_t* report, uint16_t length) { // Output reports 0x10/0x01 include 8 rumble bytes starting at offset 2. if (length < 10) { return; } SwitchRumbleOutput rumble = context.rumble_decoder.decode(report + 2); if (context.rumble_callback != nullptr) { context.rumble_callback(instance, rumble); } } static void handle_config_report(SwitchProContext& context, uint8_t switch_report_sub_id) { switch (switch_report_sub_id) { case IDENTIFY: send_identify(context); LOG_PRINTF("[HID] CONFIG IDENTIFY\n"); break; case HANDSHAKE: context.report_buffer[0] = REPORT_USB_INPUT_81; context.report_buffer[1] = HANDSHAKE; LOG_PRINTF("[HID] CONFIG HANDSHAKE\n"); break; case BAUD_RATE: context.report_buffer[0] = REPORT_USB_INPUT_81; context.report_buffer[1] = BAUD_RATE; LOG_PRINTF("[HID] CONFIG BAUD_RATE\n"); break; case DISABLE_USB_TIMEOUT: context.report_buffer[0] = REPORT_OUTPUT_30; context.report_buffer[1] = switch_report_sub_id; context.is_ready = true; LOG_PRINTF("[HID] CONFIG DISABLE_USB_TIMEOUT -> ready\n"); break; case ENABLE_USB_TIMEOUT: context.report_buffer[0] = REPORT_OUTPUT_30; context.report_buffer[1] = switch_report_sub_id; LOG_PRINTF("[HID] CONFIG ENABLE_USB_TIMEOUT\n"); break; default: context.report_buffer[0] = REPORT_OUTPUT_30; context.report_buffer[1] = switch_report_sub_id; LOG_PRINTF("[HID] CONFIG unknown subid=0x%02x\n", switch_report_sub_id); break; } context.is_report_queued = true; } static void handle_feature_report(SwitchProContext& context, const uint8_t* report_data) { uint8_t command_id = report_data[10]; uint32_t spi_read_address = 0; uint8_t spi_read_size = 0; context.report_buffer[0] = REPORT_OUTPUT_21; context.report_buffer[1] = context.last_report_counter; memcpy(context.report_buffer + 2, &context.switch_report.inputs, sizeof(SwitchInputReport)); switch (command_id) { case GET_CONTROLLER_STATE: context.report_buffer[13] = 0x80; context.report_buffer[14] = command_id; context.report_buffer[15] = 0x03; LOG_PRINTF("[HID] FEATURE GET_CONTROLLER_STATE\n"); break; case BLUETOOTH_PAIR_REQUEST: context.report_buffer[13] = 0x81; context.report_buffer[14] = command_id; context.report_buffer[15] = 0x03; LOG_PRINTF("[HID] FEATURE BLUETOOTH_PAIR_REQUEST\n"); break; case REQUEST_DEVICE_INFO: context.report_buffer[13] = 0x82; context.report_buffer[14] = 0x02; memcpy(&context.report_buffer[15], &context.device_info, sizeof(context.device_info)); LOG_PRINTF("[HID] FEATURE REQUEST_DEVICE_INFO\n"); break; case SET_MODE: context.input_mode = report_data[11]; context.report_buffer[13] = 0x80; context.report_buffer[14] = 0x03; context.report_buffer[15] = context.input_mode; LOG_PRINTF("[HID] FEATURE SET_MODE 0x%02x\n", context.input_mode); break; case TRIGGER_BUTTONS: context.report_buffer[13] = 0x83; context.report_buffer[14] = 0x04; LOG_PRINTF("[HID] FEATURE TRIGGER_BUTTONS\n"); break; case SET_SHIPMENT: context.report_buffer[13] = 0x80; context.report_buffer[14] = command_id; LOG_PRINTF("[HID] FEATURE SET_SHIPMENT\n"); break; case SPI_READ: spi_read_address = (static_cast(report_data[14]) << 24u) | (static_cast(report_data[13]) << 16u) | (static_cast(report_data[12]) << 8u) | static_cast(report_data[11]); spi_read_size = report_data[15]; context.report_buffer[13] = 0x90; context.report_buffer[14] = report_data[10]; context.report_buffer[15] = report_data[11]; context.report_buffer[16] = report_data[12]; context.report_buffer[17] = report_data[13]; context.report_buffer[18] = report_data[14]; context.report_buffer[19] = report_data[15]; read_spi_flash(context, &context.report_buffer[20], spi_read_address, spi_read_size); LOG_PRINTF("[HID] FEATURE SPI_READ addr=0x%08lx size=%u\n", static_cast(spi_read_address), spi_read_size); break; case SET_NFC_IR_CONFIG: context.report_buffer[13] = 0x80; context.report_buffer[14] = command_id; LOG_PRINTF("[HID] FEATURE SET_NFC_IR_CONFIG\n"); break; case SET_NFC_IR_STATE: context.report_buffer[13] = 0x80; context.report_buffer[14] = command_id; LOG_PRINTF("[HID] FEATURE SET_NFC_IR_STATE\n"); break; case SET_PLAYER_LIGHTS: context.player_id = report_data[11]; context.report_buffer[13] = 0x80; context.report_buffer[14] = command_id; LOG_PRINTF("[HID] FEATURE SET_PLAYER_LIGHTS player=%u\n", context.player_id); break; case GET_PLAYER_LIGHTS: context.player_id = report_data[11]; context.report_buffer[13] = 0xB0; context.report_buffer[14] = command_id; context.report_buffer[15] = context.player_id; LOG_PRINTF("[HID] FEATURE GET_PLAYER_LIGHTS player=%u\n", context.player_id); break; case COMMAND_UNKNOWN_33: context.report_buffer[13] = 0x80; context.report_buffer[14] = command_id; context.report_buffer[15] = 0x03; LOG_PRINTF("[HID] FEATURE COMMAND_UNKNOWN_33\n"); break; case SET_HOME_LIGHT: context.report_buffer[13] = 0x80; context.report_buffer[14] = command_id; context.report_buffer[15] = 0x00; LOG_PRINTF("[HID] FEATURE SET_HOME_LIGHT\n"); break; case TOGGLE_IMU: { SwitchImuMode requested_mode = SwitchImuMode::Off; if (report_data[11] == static_cast(SwitchImuMode::Raw)) { requested_mode = SwitchImuMode::Raw; } else if (report_data[11] == static_cast(SwitchImuMode::Quaternion)) { requested_mode = SwitchImuMode::Quaternion; } if (requested_mode == SwitchImuMode::Quaternion && context.imu_mode != requested_mode) { reset_motion_quaternion(context); } context.imu_mode = requested_mode; context.report_buffer[13] = 0x80; context.report_buffer[14] = command_id; context.report_buffer[15] = 0x00; LOG_PRINTF("[HID] FEATURE TOGGLE_IMU %u\n", static_cast(context.imu_mode)); break; } case IMU_SENSITIVITY: context.report_buffer[13] = 0x80; context.report_buffer[14] = command_id; LOG_PRINTF("[HID] FEATURE IMU_SENSITIVITY\n"); break; case ENABLE_VIBRATION: context.is_vibration_enabled = report_data[11] != 0; context.report_buffer[13] = 0x80; context.report_buffer[14] = command_id; context.report_buffer[15] = 0x00; LOG_PRINTF("[HID] FEATURE ENABLE_VIBRATION %u\n", context.is_vibration_enabled); break; case READ_IMU: context.report_buffer[13] = 0xC0; context.report_buffer[14] = command_id; context.report_buffer[15] = report_data[11]; context.report_buffer[16] = report_data[12]; LOG_PRINTF("[HID] FEATURE READ_IMU addr=%u size=%u\n", report_data[11], report_data[12]); break; case GET_VOLTAGE: context.report_buffer[13] = 0xD0; context.report_buffer[14] = 0x50; context.report_buffer[15] = 0x83; context.report_buffer[16] = 0x06; LOG_PRINTF("[HID] FEATURE GET_VOLTAGE\n"); break; default: context.report_buffer[13] = 0x80; context.report_buffer[14] = command_id; context.report_buffer[15] = 0x03; LOG_PRINTF("[HID] FEATURE unknown cmd=0x%02x\n", command_id); break; } context.is_report_queued = true; } static void update_switch_report_from_state(SwitchProContext& context) { const SwitchInputState& state = context.input_state; SwitchInputReport& inputs = context.switch_report.inputs; inputs.dpadUp = state.dpad_up; inputs.dpadDown = state.dpad_down; inputs.dpadLeft = state.dpad_left; inputs.dpadRight = state.dpad_right; inputs.chargingGrip = 1; inputs.buttonY = state.button_y; inputs.buttonX = state.button_x; inputs.buttonB = state.button_b; inputs.buttonA = state.button_a; inputs.buttonRightSR = 0; inputs.buttonRightSL = 0; inputs.buttonR = state.button_r; inputs.buttonZR = state.button_zr; inputs.buttonMinus = state.button_minus; inputs.buttonPlus = state.button_plus; inputs.buttonThumbR = state.button_r3; inputs.buttonThumbL = state.button_l3; inputs.buttonHome = state.button_home; inputs.buttonCapture = state.button_capture; inputs.buttonLeftSR = 0; inputs.buttonLeftSL = 0; inputs.buttonL = state.button_l; inputs.buttonZL = state.button_zl; uint16_t left_x = scale16To12(state.lx); uint16_t left_y = scale16To12(state.ly); uint16_t right_x = scale16To12(state.rx); uint16_t right_y = scale16To12(state.ry); inputs.leftStick.setX( std::min(std::max(left_x, context.left_min_x), context.left_max_x)); inputs.leftStick.setY(-std::min( std::max(left_y, context.left_min_y), context.left_max_y)); inputs.rightStick.setX(std::min( std::max(right_x, context.right_min_x), context.right_max_x)); inputs.rightStick.setY(-std::min( std::max(right_y, context.right_min_y), context.right_max_y)); context.switch_report.rumbleReport = 0x09; } void switch_pro_init(uint8_t instance) { SwitchProContext* context = context_for(instance); if (context == nullptr) { return; } context->grip_color = switch_pro_get_slot_color(instance); context->device_info = { 0x03, 0x48, SWITCH_TYPE_PRO_CONTROLLER, 0x02, {0x7c, 0xbb, 0x8a, static_cast(get_rand_32() % 0xff), static_cast(get_rand_32() % 0xff), static_cast(get_rand_32() % 0xff)}, 0x01, 0x02, }; factory_config->leftStickCalibration.getRealMin( context->left_min_x, context->left_min_y); factory_config->leftStickCalibration.getRealMax( context->left_max_x, context->left_max_y); factory_config->rightStickCalibration.getRealMin( context->right_min_x, context->right_min_y); factory_config->rightStickCalibration.getRealMax( context->right_max_x, context->right_max_y); reset_context_runtime(*context, to_ms_since_boot(get_absolute_time()), true); } void switch_pro_set_input(uint8_t instance, const SwitchInputState& state) { SwitchProContext* context = context_for(instance); if (context != nullptr) { context->input_state = state; } } bool switch_pro_task(uint8_t instance) { SwitchProContext* context = context_for(instance); if (context == nullptr) { return false; } uint32_t now = to_ms_since_boot(get_absolute_time()); bool report_sent = false; bool regular_report_sent = false; update_switch_report_from_state(*context); if (tud_suspended()) { tud_remote_wakeup(); } if (context->is_report_queued) { if ((now - context->last_report_timer) > SWITCH_PRO_KEEPALIVE_TIMER) { if (tud_hid_n_ready(instance) && send_report(instance, *context, 0, context->report_buffer, SWITCH_PRO_ENDPOINT_SIZE)) { context->is_report_queued = false; context->last_report_timer = now; } } report_sent = true; } if (context->is_ready && !report_sent) { if ((now - context->last_report_timer) >= SWITCH_PRO_IMU_REPORT_TIMER) { // One timer tick per 5ms IMU frame; three frames per report. fill_imu_report_data(*context, context->input_state, now); context->switch_report.timestamp += 3; if (tud_hid_n_ready(instance) && send_report(instance, *context, 0, &context->switch_report, sizeof(context->switch_report))) { context->input_state.imu_sample_count = 0; regular_report_sent = true; } context->last_report_timer = now; } } else if (!context->is_initialized) { send_identify(*context); if (tud_hid_n_ready(instance)) { bool result = tud_hid_n_report( instance, 0, context->report_buffer, SWITCH_PRO_ENDPOINT_SIZE); if (result) { context->is_initialized = true; } else { LOG_PRINTF("[HID %u] send_report failed id=0 len=%u\n", instance, SWITCH_PRO_ENDPOINT_SIZE); } } context->last_report_timer = now; } return regular_report_sent; } bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length, SwitchInputState& out_state) { if (packet == nullptr) { return false; } // 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); out_state = state; return true; } void switch_pro_set_rumble_callback(uint8_t instance, SwitchRumbleCallback callback) { SwitchProContext* context = context_for(instance); if (context != nullptr) { context->rumble_callback = callback; } } bool switch_pro_is_ready(uint8_t instance) { const SwitchProContext* context = context_for(instance); return context != nullptr && context->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 requested_length) { (void)report_id; (void)report_type; SwitchProContext* context = context_for(instance); LOG_PRINTF("[HID %u] get_report id=%u type=%u len=%u\n", instance, report_id, report_type, requested_length); if (context == nullptr || buffer == nullptr) { return 0; } // Serve the addressed instance's current input report. uint16_t report_size = sizeof(context->switch_report); if (requested_length < report_size) { report_size = requested_length; } memcpy(buffer, &context->switch_report, report_size); return report_size; } static void process_output_report(uint8_t instance, SwitchProContext& context, uint8_t callback_report_id, const uint8_t* payload, uint16_t payload_size) { uint8_t normalized[SWITCH_PRO_ENDPOINT_SIZE]{}; size_t normalized_size = normalize_switch_output_report( callback_report_id, payload, payload_size, normalized); if (normalized_size < 2) { return; } memset(context.report_buffer, 0x00, sizeof(context.report_buffer)); uint8_t switch_report_id = normalized[0]; uint8_t switch_report_sub_id = normalized[1]; LOG_PRINTF( "[HID %u] output id=%u switchRID=0x%02x sub=0x%02x len=%u\n", instance, callback_report_id, switch_report_id, switch_report_sub_id, static_cast(normalized_size)); if (switch_report_id == REPORT_OUTPUT_10 || switch_report_id == REPORT_FEATURE) { forward_decoded_rumble( instance, context, normalized, static_cast(normalized_size)); } if (switch_report_id == REPORT_OUTPUT_00) { return; } if (switch_report_id == REPORT_FEATURE) { if (normalized_size >= 16) { handle_feature_report(context, normalized); } } else if (switch_report_id == REPORT_CONFIGURATION) { handle_config_report(context, switch_report_sub_id); } } void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, const uint8_t* buffer, uint16_t buffer_size) { SwitchProContext* context = context_for(instance); if (context == nullptr || report_type != HID_REPORT_TYPE_OUTPUT) { return; } process_output_report(instance, *context, report_id, buffer, buffer_size); } void tud_hid_report_received_cb(uint8_t instance, uint8_t report_id, const uint8_t* buffer, uint16_t buffer_size) { SwitchProContext* context = context_for(instance); if (context == nullptr) { return; } process_output_report(instance, *context, report_id, buffer, buffer_size); } uint8_t const* tud_hid_descriptor_report_cb(uint8_t instance) { if (context_for(instance) == nullptr) { return nullptr; } return switch_pro_report_descriptor; } uint8_t const* tud_descriptor_device_cb(void) { #ifdef SWITCH_PICO_ADAPTER_FEASIBILITY if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) { return XInputFeasibility::kDeviceDescriptor; } return XInputFeasibility::kSwitchProbeDeviceDescriptor; #else return switch_pro_device_descriptor; #endif } uint8_t const* tud_descriptor_configuration_cb(uint8_t index) { (void)index; #ifdef SWITCH_PICO_ADAPTER_FEASIBILITY if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) { return XInputFeasibility::kConfigurationDescriptor; } #endif return switch_pro_configuration_descriptor; } bool tud_control_request_cb(uint8_t rhport, tusb_control_request_t const* request) { (void)rhport; (void)request; 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"); reset_all_contexts(false); } void tud_umount_cb(void) { LOG_PRINTF("[USB] umount_cb\n"); reset_all_contexts(false); } static uint16_t desc_str[32]; uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) { (void)langid; #ifdef SWITCH_PICO_ADAPTER_FEASIBILITY adapter_host_probe_note_string_descriptor(index); if (index == 0xee) { static constexpr char kSignature[] = "MSFT100"; for (uint8_t i = 0; i < sizeof(kSignature) - 1; ++i) { desc_str[1 + i] = kSignature[i]; } desc_str[8] = XInputFeasibility::kMsVendorRequest; desc_str[0] = static_cast((0x03 << 8) | 18); return desc_str; } #endif uint8_t chr_count = 0; if (index == 0) { memcpy(&desc_str[1], switch_pro_string_language, 2); chr_count = 1; } else { const uint8_t* str = nullptr; #ifdef SWITCH_PICO_ADAPTER_FEASIBILITY static const uint8_t kManufacturer[] = "Switch Pico"; static const uint8_t kProduct[] = "XInput Feasibility"; static const uint8_t kSerial[] = "XINPUT-PROTOTYPE"; static const uint8_t* const kXInputStrings[] = { nullptr, kManufacturer, kProduct, kSerial}; if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) { if (index >= sizeof(kXInputStrings) / sizeof(kXInputStrings[0])) { return nullptr; } str = kXInputStrings[index]; } else #endif { if (index >= sizeof(switch_pro_string_descriptors) / sizeof(switch_pro_string_descriptors[0])) { return nullptr; } str = switch_pro_string_descriptors[index]; } while (str[chr_count] != 0) { ++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] = static_cast((0x03 << 8) | (2 * chr_count + 2)); return desc_str; }