#include "bluepad32_input_backend.h" #include #include #include #include #include #include #include #include namespace { constexpr uint16_t kStickMidpoint = 32768; constexpr int32_t kAxisMinimum = -512; constexpr int32_t kAxisMaximum = 511; constexpr int32_t kTriggerMaximum = 1023; constexpr int32_t kTriggerThreshold = (kTriggerMaximum * 35) / 100; constexpr uint16_t kRumbleDurationMs = 50; constexpr uint32_t kRumblePollIntervalMs = 5; constexpr uint8_t kSlotCount = BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; static_assert(kSlotCount == 2); static_assert(SWITCH_PICO_HID_INSTANCE_COUNT == kSlotCount); enum class ConnectionStatus { Initializing, Scanning, Connecting, Ready, }; struct RumbleEnvelope { uint8_t slot; uint32_t connection_generation; SwitchRumbleOutput rumble; }; struct BackendSlot { SwitchInputState state; uni_hid_device_t* device; uint32_t state_generation; uint32_t connection_generation; bool active; bool rumble_pending; RumbleEnvelope pending_rumble; }; critical_section_t g_state_lock; BackendSlot g_slots[kSlotCount]; // These acknowledgement generations are only read or written by Core 0. uint32_t g_consumed_generation[kSlotCount]{}; uint32_t g_last_snapshot_generation[kSlotCount]{}; bool g_initialized = false; bool g_started = false; // The timer and connection status are only read or written by Core 1 / BTstack. btstack_timer_source_t g_rumble_timer{}; ConnectionStatus g_connection_status = ConnectionStatus::Initializing; uint16_t g_status_led_tick = 0; bool g_status_led_on = false; SwitchInputState make_neutral_state() { SwitchInputState state{}; state.lx = kStickMidpoint; state.ly = kStickMidpoint; state.rx = kStickMidpoint; state.ry = kStickMidpoint; return state; } bool valid_slot(uint8_t slot) { return slot < kSlotCount; } int slot_for_device(const uni_hid_device_t* device) { if (device == nullptr) { return -1; } const int slot = uni_hid_device_get_idx_for_instance(device); return slot >= 0 && slot < kSlotCount ? slot : -1; } bool all_slots_ready() { critical_section_enter_blocking(&g_state_lock); bool ready = true; for (const BackendSlot& slot : g_slots) { ready = ready && slot.active && slot.device != nullptr; } critical_section_exit(&g_state_lock); return ready; } void publish_device_state(uint8_t slot, uni_hid_device_t* device, const SwitchInputState& state) { critical_section_enter_blocking(&g_state_lock); BackendSlot& target = g_slots[slot]; if (target.active && target.device == device) { target.state = state; ++target.state_generation; } critical_section_exit(&g_state_lock); } void publish_all_neutral() { critical_section_enter_blocking(&g_state_lock); for (BackendSlot& slot : g_slots) { slot.state = make_neutral_state(); slot.device = nullptr; slot.active = false; slot.rumble_pending = false; ++slot.state_generation; ++slot.connection_generation; } critical_section_exit(&g_state_lock); } constexpr int32_t clamp_axis(int32_t value) { if (value < kAxisMinimum) { return kAxisMinimum; } if (value > kAxisMaximum) { return kAxisMaximum; } return value; } constexpr uint16_t scale_stick(int32_t value) { value = clamp_axis(value); if (value <= 0) { return static_cast( (static_cast(value - kAxisMinimum) * kStickMidpoint) / -kAxisMinimum); } return static_cast( kStickMidpoint + (static_cast(value) * (UINT16_MAX - kStickMidpoint)) / kAxisMaximum); } constexpr int16_t clamp_int16(int64_t value) { if (value < INT16_MIN) { return INT16_MIN; } if (value > INT16_MAX) { return INT16_MAX; } return static_cast(value); } constexpr int64_t divide_round_nearest(int64_t numerator, int64_t denominator) { if (numerator >= 0) { return (numerator + denominator / 2) / denominator; } return -((-numerator + denominator / 2) / denominator); } constexpr int16_t convert_accel(int64_t q13_value) { return clamp_int16(q13_value / 2); } constexpr int16_t convert_gyro(int64_t q10_value) { constexpr int64_t kNumeratorScale = 13371; constexpr int64_t kDenominator = 1024 * 936; return clamp_int16(divide_round_nearest(q10_value * kNumeratorScale, kDenominator)); } static_assert(scale_stick(-512) == 0); static_assert(scale_stick(0) == 32768); static_assert(scale_stick(511) == UINT16_MAX); static_assert(convert_accel(8192) == 4096); static_assert(convert_accel(-8192) == -4096); static_assert(convert_gyro(1024) == 14); static_assert(convert_gyro(-1024) == -14); bool has_motion(const uni_gamepad_t& gamepad) { for (size_t i = 0; i < 3; ++i) { if (gamepad.accel[i] != 0 || gamepad.gyro[i] != 0) { return true; } } return false; } SwitchInputState map_gamepad(const uni_gamepad_t& gamepad) { SwitchInputState state = make_neutral_state(); state.dpad_up = (gamepad.dpad & DPAD_UP) != 0; state.dpad_down = (gamepad.dpad & DPAD_DOWN) != 0; state.dpad_left = (gamepad.dpad & DPAD_LEFT) != 0; state.dpad_right = (gamepad.dpad & DPAD_RIGHT) != 0; // Bluepad32's A/B/X/Y are positional: south/east/west/north. state.button_b = (gamepad.buttons & BUTTON_A) != 0; state.button_a = (gamepad.buttons & BUTTON_B) != 0; state.button_y = (gamepad.buttons & BUTTON_X) != 0; state.button_x = (gamepad.buttons & BUTTON_Y) != 0; state.button_l = (gamepad.buttons & BUTTON_SHOULDER_L) != 0; state.button_r = (gamepad.buttons & BUTTON_SHOULDER_R) != 0; state.button_zl = (gamepad.buttons & BUTTON_TRIGGER_L) != 0 || gamepad.brake >= kTriggerThreshold; state.button_zr = (gamepad.buttons & BUTTON_TRIGGER_R) != 0 || gamepad.throttle >= kTriggerThreshold; state.button_l3 = (gamepad.buttons & BUTTON_THUMB_L) != 0; state.button_r3 = (gamepad.buttons & BUTTON_THUMB_R) != 0; state.button_minus = (gamepad.misc_buttons & MISC_BUTTON_SELECT) != 0; state.button_plus = (gamepad.misc_buttons & MISC_BUTTON_START) != 0; state.button_home = (gamepad.misc_buttons & MISC_BUTTON_SYSTEM) != 0; state.button_capture = (gamepad.misc_buttons & MISC_BUTTON_CAPTURE) != 0; state.lx = scale_stick(gamepad.axis_x); state.ly = scale_stick(gamepad.axis_y); state.rx = scale_stick(gamepad.axis_rx); state.ry = scale_stick(gamepad.axis_ry); if (has_motion(gamepad)) { // Dependency patches normalize both arrays to SDL3 PlayStation axes. SwitchImuSample sample{}; sample.accel_x = convert_accel(-static_cast(gamepad.accel[2])); sample.accel_y = convert_accel(-static_cast(gamepad.accel[0])); sample.accel_z = convert_accel(gamepad.accel[1]); sample.gyro_x = convert_gyro(-static_cast(gamepad.gyro[2])); sample.gyro_y = convert_gyro(-static_cast(gamepad.gyro[0])); sample.gyro_z = convert_gyro(gamepad.gyro[1]); state.imu_sample_count = 3; for (SwitchImuSample& destination : state.imu_samples) { destination = sample; } } return state; } void update_status_led() { ++g_status_led_tick; bool led_on = false; switch (g_connection_status) { case ConnectionStatus::Initializing: case ConnectionStatus::Ready: led_on = true; break; case ConnectionStatus::Scanning: led_on = (g_status_led_tick % 200) < 100; break; case ConnectionStatus::Connecting: led_on = (g_status_led_tick % 40) < 20; break; } if (led_on != g_status_led_on) { cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, led_on); g_status_led_on = led_on; } } void process_rumble_timer(btstack_timer_source_t* timer) { for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) { RumbleEnvelope envelope{}; uni_hid_device_t* device = nullptr; bool dispatch = false; critical_section_enter_blocking(&g_state_lock); BackendSlot& slot = g_slots[slot_index]; if (slot.rumble_pending) { envelope = slot.pending_rumble; slot.rumble_pending = false; dispatch = envelope.slot == slot_index && slot.active && slot.device != nullptr && envelope.connection_generation == slot.connection_generation; if (dispatch) { device = slot.device; } } critical_section_exit(&g_state_lock); if (dispatch && device->report_parser.play_dual_rumble != nullptr) { device->report_parser.play_dual_rumble( device, 0, kRumbleDurationMs, envelope.rumble.high_frequency_magnitude, envelope.rumble.low_frequency_magnitude); } } update_status_led(); btstack_run_loop_set_timer(timer, kRumblePollIntervalMs); btstack_run_loop_add_timer(timer); } void resume_connections() { uni_bt_allow_incoming_connections(true); uni_bt_start_scanning_and_autoconnect_unsafe(); } void platform_init(int argc, const char** argv) { (void)argc; (void)argv; } void platform_on_init_complete() { btstack_run_loop_set_timer_handler(&g_rumble_timer, process_rumble_timer); btstack_run_loop_set_timer(&g_rumble_timer, kRumblePollIntervalMs); btstack_run_loop_add_timer(&g_rumble_timer); g_connection_status = ConnectionStatus::Scanning; g_status_led_tick = 0; resume_connections(); } uni_error_t platform_on_device_discovered(bd_addr_t addr, const char* name, uint16_t cod, uint8_t rssi) { (void)addr; (void)name; (void)cod; (void)rssi; return all_slots_ready() ? UNI_ERROR_IGNORE_DEVICE : UNI_ERROR_SUCCESS; } void platform_on_device_connected(uni_hid_device_t* device) { (void)device; if (!all_slots_ready()) { g_connection_status = ConnectionStatus::Connecting; g_status_led_tick = 0; } } void platform_on_device_disconnected(uni_hid_device_t* device) { const int slot_index = slot_for_device(device); if (slot_index < 0) { return; } bool disconnected_active_slot = false; critical_section_enter_blocking(&g_state_lock); BackendSlot& slot = g_slots[slot_index]; if (slot.active && slot.device == device) { slot.state = make_neutral_state(); slot.device = nullptr; slot.active = false; slot.rumble_pending = false; ++slot.state_generation; ++slot.connection_generation; disconnected_active_slot = true; } critical_section_exit(&g_state_lock); if (disconnected_active_slot) { g_connection_status = ConnectionStatus::Scanning; g_status_led_tick = 0; resume_connections(); } } uni_error_t platform_on_device_ready(uni_hid_device_t* device) { if (device == nullptr || !uni_hid_device_is_gamepad(device)) { return UNI_ERROR_INVALID_CONTROLLER; } const int slot_index = slot_for_device(device); if (slot_index < 0) { return UNI_ERROR_NO_SLOTS; } bool occupied_mismatch = false; critical_section_enter_blocking(&g_state_lock); BackendSlot& slot = g_slots[slot_index]; occupied_mismatch = slot.active && slot.device != device; if (!occupied_mismatch && !slot.active) { slot.state = make_neutral_state(); slot.device = device; slot.active = true; slot.rumble_pending = false; ++slot.state_generation; } critical_section_exit(&g_state_lock); if (occupied_mismatch) { return UNI_ERROR_NO_SLOTS; } g_status_led_tick = 0; if (all_slots_ready()) { g_connection_status = ConnectionStatus::Ready; uni_bt_stop_scanning_unsafe(); uni_bt_allow_incoming_connections(false); } else { g_connection_status = ConnectionStatus::Scanning; resume_connections(); } return UNI_ERROR_SUCCESS; } void platform_on_controller_data(uni_hid_device_t* device, uni_controller_t* controller) { const int slot_index = slot_for_device(device); if (slot_index < 0 || controller == nullptr || controller->klass != UNI_CONTROLLER_CLASS_GAMEPAD) { return; } publish_device_state(static_cast(slot_index), device, map_gamepad(controller->gamepad)); } const uni_property_t* platform_get_property(uni_property_idx_t index) { (void)index; return nullptr; } void platform_on_oob_event(uni_platform_oob_event_t event, void* data) { (void)event; (void)data; } uni_platform* get_platform() { static uni_platform platform = { "Switch Pico", platform_init, platform_on_init_complete, platform_on_device_discovered, platform_on_device_connected, platform_on_device_disconnected, platform_on_device_ready, nullptr, platform_on_controller_data, platform_get_property, platform_on_oob_event, nullptr, nullptr, }; return &platform; } [[noreturn]] void core1_main() { if (cyw43_arch_init() != 0) { publish_all_neutral(); while (true) { tight_loop_contents(); } } cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, true); g_status_led_on = true; uni_platform_set_custom(get_platform()); if (uni_init(0, nullptr) != 0) { publish_all_neutral(); while (true) { tight_loop_contents(); } } btstack_run_loop_execute(); while (true) { tight_loop_contents(); } } } // namespace void bluepad32_input_backend_init() { if (g_initialized) { return; } critical_section_init(&g_state_lock); for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) { BackendSlot& slot = g_slots[slot_index]; slot = {}; slot.state = make_neutral_state(); slot.pending_rumble.slot = slot_index; g_consumed_generation[slot_index] = 0; g_last_snapshot_generation[slot_index] = 0; } g_initialized = true; } void bluepad32_input_backend_start() { if (!g_initialized) { bluepad32_input_backend_init(); } if (g_started) { return; } g_started = true; multicore_launch_core1(core1_main); } bool bluepad32_input_backend_snapshot(uint8_t slot_index, SwitchInputState* out) { if (out == nullptr || !valid_slot(slot_index)) { return false; } if (!g_initialized) { *out = make_neutral_state(); return false; } critical_section_enter_blocking(&g_state_lock); *out = g_slots[slot_index].state; const bool controller_active = g_slots[slot_index].active; const uint32_t generation = g_slots[slot_index].state_generation; critical_section_exit(&g_state_lock); if (generation == g_consumed_generation[slot_index]) { out->imu_sample_count = 0; } g_last_snapshot_generation[slot_index] = generation; return controller_active; } void bluepad32_input_backend_report_sent(uint8_t slot_index) { if (!g_initialized || !valid_slot(slot_index)) { return; } g_consumed_generation[slot_index] = g_last_snapshot_generation[slot_index]; } void bluepad32_input_backend_queue_rumble(uint8_t slot_index, const SwitchRumbleOutput& rumble) { if (!g_initialized || !valid_slot(slot_index)) { return; } critical_section_enter_blocking(&g_state_lock); BackendSlot& slot = g_slots[slot_index]; if (slot.active && slot.device != nullptr) { slot.pending_rumble = {slot_index, slot.connection_generation, rumble}; slot.rumble_pending = true; } critical_section_exit(&g_state_lock); }