#include "bluepad32_input_backend.h" #include "controller_hotkey_config.h" #include #include #include #include #include #include #include #include #include #include #ifdef SWITCH_PICO_ADAPTER_FEASIBILITY #include "adapter_usb_mode.h" #endif namespace { constexpr int32_t kAxisMinimum = -512; constexpr int32_t kAxisMaximum = 511; constexpr int32_t kTriggerMaximum = 1023; constexpr uint16_t kSwitchHostRumbleDurationMs = 50; #ifdef SWITCH_PICO_ADAPTER_FEASIBILITY // XInput vibration is stateful and remains active until XInputSetState sends // a new magnitude. constexpr uint16_t kXInputHostRumbleDurationMs = UINT16_MAX; #endif constexpr uint32_t kRumblePollIntervalMs = 5; constexpr uint8_t kSlotCount = BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; constexpr uint32_t kPairingWindowDurationMs = 60000; constexpr uint32_t kPairingResetFeedbackDurationMs = 2000; // Bluetooth Classic units are 0.625 ms: 0x1900 = 4 seconds. constexpr uint16_t kClassicLinkSupervisionTimeout = 0x1900; constexpr uint8_t kAllBlePairingMethods = SM_STK_GENERATION_METHOD_JUST_WORKS | SM_STK_GENERATION_METHOD_OOB | SM_STK_GENERATION_METHOD_PASSKEY | SM_STK_GENERATION_METHOD_NUMERIC_COMPARISON; constexpr uint32_t kAbxyHotkeyButtonMask = SWITCH_ABXY_HOTKEY_BUTTON_MASK; constexpr uint32_t kAbxyHotkeyMiscMask = SWITCH_ABXY_HOTKEY_MISC_MASK; constexpr bool kDefaultSwapAbxy = SWITCH_ABXY_DEFAULT_SWAPPED != 0; constexpr uint32_t kAbxyFeedbackDurationMs = SWITCH_ABXY_FEEDBACK_DURATION_MS; constexpr uint8_t kAbxyFeedbackWeakMagnitude = SWITCH_ABXY_FEEDBACK_WEAK_MAGNITUDE; constexpr uint8_t kAbxyFeedbackStrongMagnitude = SWITCH_ABXY_FEEDBACK_STRONG_MAGNITUDE; constexpr uint32_t kMotionHotkeyDpadMask = SWITCH_MOTION_HOTKEY_DPAD_MASK; constexpr uint32_t kMotionHotkeyButtonMask = SWITCH_MOTION_HOTKEY_BUTTON_MASK; constexpr uint32_t kMotionHotkeyMiscMask = SWITCH_MOTION_HOTKEY_MISC_MASK; constexpr bool kDefaultMotionEnabled = SWITCH_MOTION_DEFAULT_ENABLED != 0; constexpr uint16_t kMotionDisabledFeedbackDurationMs = SWITCH_MOTION_DISABLED_FEEDBACK_DURATION_MS; constexpr uint8_t kMotionDisabledFeedbackWeakMagnitude = SWITCH_MOTION_DISABLED_FEEDBACK_WEAK_MAGNITUDE; constexpr uint8_t kMotionDisabledFeedbackStrongMagnitude = SWITCH_MOTION_DISABLED_FEEDBACK_STRONG_MAGNITUDE; constexpr uint16_t kMotionEnabledFeedbackDurationMs = SWITCH_MOTION_ENABLED_FEEDBACK_DURATION_MS; constexpr uint8_t kMotionEnabledFeedbackWeakMagnitude = SWITCH_MOTION_ENABLED_FEEDBACK_WEAK_MAGNITUDE; constexpr uint8_t kMotionEnabledFeedbackStrongMagnitude = SWITCH_MOTION_ENABLED_FEEDBACK_STRONG_MAGNITUDE; static_assert(kAbxyHotkeyButtonMask != 0); static_assert(kAbxyHotkeyMiscMask != 0); static_assert(kAbxyFeedbackDurationMs > 0); static_assert(kMotionHotkeyDpadMask != 0); static_assert(kMotionHotkeyButtonMask != 0); static_assert(kMotionHotkeyMiscMask != 0); static_assert(kMotionDisabledFeedbackDurationMs > 0); static_assert(kMotionEnabledFeedbackDurationMs > 0); static_assert(kSlotCount == 4); static_assert(SWITCH_PICO_HID_INSTANCE_COUNT == kSlotCount); enum class ConnectionStatus { Initializing, Scanning, Connecting, Ready, }; enum class ConnectionPolicyState { Uninitialized, Open, Passive, Paused, FailedClosed, }; struct RumbleEnvelope { uint8_t slot; uint32_t connection_generation; ControllerRumbleOutput rumble; }; struct FeedbackEnvelope { uint32_t connection_generation; uint16_t duration_ms; uint8_t weak_magnitude; uint8_t strong_magnitude; }; struct BackendSlot { ControllerState state; // Non-null with active=false is a connected device still becoming ready. uni_hid_device_t* device; uint32_t state_generation; uint32_t connection_generation; bool active; bool rumble_pending; bool swap_abxy; bool abxy_hotkey_latched; bool motion_enabled; bool motion_hotkey_latched; bool feedback_pending; uint32_t feedback_until_ms; RumbleEnvelope pending_rumble; FeedbackEnvelope pending_feedback; }; critical_section_t g_state_lock; BackendSlot g_slots[kSlotCount]; // These acknowledgement generations and the pairing request producer are only // used by Core 0. The request is transferred under the cross-core state lock. uint32_t g_consumed_generation[kSlotCount]{}; uint32_t g_last_snapshot_generation[kSlotCount]{}; bool g_pairing_window_requested = false; bool g_clear_pairings_requested = false; bool g_pairing_snapshot_requested = false; bool g_initialized = false; bool g_started = false; // These fields are only read or written by Core 1 / BTstack. btstack_timer_source_t g_rumble_timer{}; ConnectionStatus g_connection_status = ConnectionStatus::Initializing; btstack_packet_callback_registration_t g_pairing_event_callback{}; ConnectionPolicyState g_connection_policy_state = ConnectionPolicyState::Uninitialized; uint32_t g_pairing_window_deadline_ms = 0; uint32_t g_pairing_reset_feedback_deadline_ms = 0; uint16_t g_status_led_tick = 0; bool g_pairing_window_open = false; bool g_status_led_on = false; Bluepad32PairingSnapshot g_pairing_snapshot{}; uint16_t host_rumble_duration_ms() { #ifdef SWITCH_PICO_ADAPTER_FEASIBILITY if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) { return kXInputHostRumbleDurationMs; } #endif return kSwitchHostRumbleDurationMs; } ControllerState make_neutral_state() { return controller_neutral_state(); } bool valid_slot(uint8_t slot) { return slot < kSlotCount; } bool has_free_slot() { critical_section_enter_blocking(&g_state_lock); bool free_slot = false; for (const BackendSlot& slot : g_slots) { free_slot = free_slot || slot.device == nullptr; } critical_section_exit(&g_state_lock); return free_slot; } bool has_active_controller() { critical_section_enter_blocking(&g_state_lock); bool active_controller = false; for (const BackendSlot& slot : g_slots) { active_controller = active_controller || slot.active; } critical_section_exit(&g_state_lock); return active_controller; } 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; } void apply_slot_lighting(uint8_t slot_index, uni_hid_device_t* device) { const SwitchRgbColor color = switch_pro_get_slot_light_color(slot_index); if (device->report_parser.set_lightbar_color != nullptr) { device->report_parser.set_lightbar_color( device, color.red, color.green, color.blue); } else if (device->report_parser.set_player_leds != nullptr) { device->report_parser.set_player_leds( device, static_cast(1u << slot_index)); } } ConnectionStatus compute_connection_status() { critical_section_enter_blocking(&g_state_lock); bool all_ready = true; bool any_connecting = false; for (const BackendSlot& slot : g_slots) { const bool has_device = slot.device != nullptr; all_ready = all_ready && slot.active && has_device; any_connecting = any_connecting || (!slot.active && has_device); } critical_section_exit(&g_state_lock); if (all_ready) { return ConnectionStatus::Ready; } return any_connecting ? ConnectionStatus::Connecting : ConnectionStatus::Scanning; } void publish_device_state(uint8_t slot, uni_hid_device_t* device, const ControllerState& 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); g_connection_status = ConnectionStatus::Initializing; g_connection_policy_state = ConnectionPolicyState::FailedClosed; g_pairing_window_open = false; g_status_led_tick = 0; } constexpr int32_t clamp_axis(int32_t value) { if (value < kAxisMinimum) { return kAxisMinimum; } if (value > kAxisMaximum) { return kAxisMaximum; } return value; } constexpr int16_t scale_axis(int32_t value) { value = clamp_axis(value); if (value <= 0) { return static_cast( (static_cast(value) * -INT16_MIN) / -kAxisMinimum); } return static_cast( (static_cast(value) * INT16_MAX) / kAxisMaximum); } constexpr uint16_t scale_trigger(int32_t value) { if (value <= 0) { return 0; } if (value >= kTriggerMaximum) { return UINT16_MAX; } return static_cast( (static_cast(value) * UINT16_MAX) / kTriggerMaximum); } 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_axis(-512) == INT16_MIN); static_assert(scale_axis(0) == 0); static_assert(scale_axis(511) == INT16_MAX); static_assert(scale_trigger(0) == 0); static_assert(scale_trigger(1023) == 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; } ControllerState map_gamepad(const uni_gamepad_t& gamepad, bool swap_abxy, bool motion_enabled) { ControllerState 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_south = (gamepad.buttons & BUTTON_A) != 0; state.button_east = (gamepad.buttons & BUTTON_B) != 0; state.button_west = (gamepad.buttons & BUTTON_X) != 0; state.button_north = (gamepad.buttons & BUTTON_Y) != 0; if (swap_abxy) { bool temporary = state.button_east; state.button_east = state.button_south; state.button_south = temporary; temporary = state.button_north; state.button_north = state.button_west; state.button_west = temporary; } state.button_left_shoulder = (gamepad.buttons & BUTTON_SHOULDER_L) != 0; state.button_right_shoulder = (gamepad.buttons & BUTTON_SHOULDER_R) != 0; state.left_trigger = (gamepad.buttons & BUTTON_TRIGGER_L) != 0 ? UINT16_MAX : scale_trigger(gamepad.brake); state.right_trigger = (gamepad.buttons & BUTTON_TRIGGER_R) != 0 ? UINT16_MAX : scale_trigger(gamepad.throttle); state.button_left_stick = (gamepad.buttons & BUTTON_THUMB_L) != 0; state.button_right_stick = (gamepad.buttons & BUTTON_THUMB_R) != 0; state.button_select = (gamepad.misc_buttons & MISC_BUTTON_SELECT) != 0; state.button_start = (gamepad.misc_buttons & MISC_BUTTON_START) != 0; state.button_system = (gamepad.misc_buttons & MISC_BUTTON_SYSTEM) != 0; state.button_capture = (gamepad.misc_buttons & MISC_BUTTON_CAPTURE) != 0; state.left_stick_x = scale_axis(gamepad.axis_x); state.left_stick_y = scale_axis(gamepad.axis_y); state.right_stick_x = scale_axis(gamepad.axis_rx); state.right_stick_y = scale_axis(gamepad.axis_ry); if (motion_enabled && has_motion(gamepad)) { // Dependency patches normalize both arrays to SDL3 PlayStation axes. ControllerMotionSample 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.motion_sample_count = 3; for (ControllerMotionSample& destination : state.motion_samples) { destination = sample; } } return state; } struct HotkeyDecision { bool swap_abxy; bool motion_enabled; uint32_t suppress_dpad; uint32_t suppress_buttons; uint32_t suppress_misc_buttons; }; void queue_local_feedback(BackendSlot& slot, uint16_t duration_ms, uint8_t weak_magnitude, uint8_t strong_magnitude) { slot.feedback_pending = true; slot.pending_feedback = { slot.connection_generation, duration_ms, weak_magnitude, strong_magnitude}; } void reset_slot_hotkeys(BackendSlot& slot) { slot.swap_abxy = kDefaultSwapAbxy; slot.abxy_hotkey_latched = false; slot.motion_enabled = kDefaultMotionEnabled; slot.motion_hotkey_latched = false; slot.feedback_pending = false; slot.feedback_until_ms = 0; slot.pending_feedback = {}; } HotkeyDecision update_controller_hotkeys( uint8_t slot_index, uni_hid_device_t* device, const uni_gamepad_t& gamepad) { const bool abxy_pressed = (gamepad.buttons & kAbxyHotkeyButtonMask) == kAbxyHotkeyButtonMask && (gamepad.misc_buttons & kAbxyHotkeyMiscMask) == kAbxyHotkeyMiscMask; const bool motion_pressed = (gamepad.dpad & kMotionHotkeyDpadMask) == kMotionHotkeyDpadMask && (gamepad.buttons & kMotionHotkeyButtonMask) == kMotionHotkeyButtonMask && (gamepad.misc_buttons & kMotionHotkeyMiscMask) == kMotionHotkeyMiscMask; HotkeyDecision decision{ kDefaultSwapAbxy, kDefaultMotionEnabled, 0, 0, 0}; critical_section_enter_blocking(&g_state_lock); BackendSlot& slot = g_slots[slot_index]; if (slot.active && slot.device == device) { if (abxy_pressed && !slot.abxy_hotkey_latched) { slot.swap_abxy = !slot.swap_abxy; queue_local_feedback( slot, static_cast(kAbxyFeedbackDurationMs), kAbxyFeedbackWeakMagnitude, kAbxyFeedbackStrongMagnitude); } else if (motion_pressed && !slot.motion_hotkey_latched) { slot.motion_enabled = !slot.motion_enabled; if (slot.motion_enabled) { queue_local_feedback( slot, kMotionEnabledFeedbackDurationMs, kMotionEnabledFeedbackWeakMagnitude, kMotionEnabledFeedbackStrongMagnitude); } else { queue_local_feedback( slot, kMotionDisabledFeedbackDurationMs, kMotionDisabledFeedbackWeakMagnitude, kMotionDisabledFeedbackStrongMagnitude); } } slot.abxy_hotkey_latched = abxy_pressed; slot.motion_hotkey_latched = motion_pressed; decision.swap_abxy = slot.swap_abxy; decision.motion_enabled = slot.motion_enabled; if (abxy_pressed) { decision.suppress_buttons |= kAbxyHotkeyButtonMask; decision.suppress_misc_buttons |= kAbxyHotkeyMiscMask; } if (motion_pressed) { decision.suppress_dpad |= kMotionHotkeyDpadMask; decision.suppress_buttons |= kMotionHotkeyButtonMask; decision.suppress_misc_buttons |= kMotionHotkeyMiscMask; } } critical_section_exit(&g_state_lock); return decision; } bool pairing_window_active_at(uint32_t now_ms) { return g_pairing_window_open && static_cast(now_ms - g_pairing_window_deadline_ms) < 0; } void handle_pairing_hci_event(uint8_t packet_type, uint16_t channel, uint8_t* packet, uint16_t size) { (void)channel; if (packet_type != HCI_EVENT_PACKET || packet == nullptr || size < 8) { return; } bd_addr_t address{}; const bool pairing_open = pairing_window_active_at(btstack_run_loop_get_time_ms()); switch (hci_event_packet_get_type(packet)) { case HCI_EVENT_USER_CONFIRMATION_REQUEST: hci_event_user_confirmation_request_get_bd_addr(packet, address); if (pairing_open) { gap_ssp_confirmation_response(address); } else { gap_ssp_confirmation_negative(address); } break; case HCI_EVENT_USER_PASSKEY_REQUEST: hci_event_user_passkey_request_get_bd_addr(packet, address); if (pairing_open) { gap_ssp_passkey_response(address, 0); } else { gap_ssp_passkey_negative(address); } break; default: break; } } bool update_pairing_window(uint32_t now_ms) { critical_section_enter_blocking(&g_state_lock); const bool requested = g_pairing_window_requested; g_pairing_window_requested = false; critical_section_exit(&g_state_lock); if (requested) { g_pairing_window_open = true; g_pairing_window_deadline_ms = now_ms + kPairingWindowDurationMs; gap_set_bondable_mode(true); sm_set_accepted_stk_generation_methods(kAllBlePairingMethods); g_status_led_tick = 0; return true; } if (g_pairing_window_open && !pairing_window_active_at(now_ms)) { g_pairing_window_open = false; sm_set_accepted_stk_generation_methods(0); g_status_led_tick = 0; gap_set_bondable_mode(false); return true; } return false; } void append_pairing_record( Bluepad32PairingSnapshot& snapshot, Bluepad32PairingTransport transport, uint8_t address_type, const bd_addr_t address) { if (snapshot.record_count >= BLUEPAD32_PAIRING_RECORD_CAPACITY) { snapshot.overflow = true; return; } Bluepad32PairingRecord& record = snapshot.records[snapshot.record_count++]; record.transport = transport; record.address_type = address_type; memcpy(record.address, address, sizeof(record.address)); } void refresh_pairing_snapshot() { Bluepad32PairingSnapshot snapshot{}; snapshot.status = Bluepad32PairingSnapshotStatus::kReady; btstack_link_key_iterator_t iterator{}; if (gap_link_key_iterator_init(&iterator)) { bd_addr_t address{}; link_key_t link_key{}; link_key_type_t link_key_type{}; while (gap_link_key_iterator_get_next( &iterator, address, link_key, &link_key_type)) { append_pairing_record( snapshot, Bluepad32PairingTransport::kClassic, BD_ADDR_TYPE_UNKNOWN, address); } gap_link_key_iterator_done(&iterator); } for (int index = 0; index < le_device_db_max_count(); ++index) { int address_type = BD_ADDR_TYPE_UNKNOWN; bd_addr_t address{}; le_device_db_info(index, &address_type, address, nullptr); if (address_type == BD_ADDR_TYPE_UNKNOWN) { continue; } append_pairing_record( snapshot, Bluepad32PairingTransport::kBle, static_cast(address_type), address); } critical_section_enter_blocking(&g_state_lock); snapshot.generation = g_pairing_snapshot.generation + 1; g_pairing_snapshot = snapshot; g_pairing_snapshot_requested = false; critical_section_exit(&g_state_lock); } void process_pairing_snapshot_request() { critical_section_enter_blocking(&g_state_lock); const bool requested = g_pairing_snapshot_requested; critical_section_exit(&g_state_lock); if (requested) { refresh_pairing_snapshot(); } } void apply_connection_policy(); void process_clear_pairings(uint32_t now_ms) { uni_hid_device_t* devices[kSlotCount]{}; critical_section_enter_blocking(&g_state_lock); const bool requested = g_clear_pairings_requested; g_clear_pairings_requested = false; if (requested) { g_pairing_window_requested = false; for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) { BackendSlot& slot = g_slots[slot_index]; devices[slot_index] = slot.device; slot.state = make_neutral_state(); slot.device = nullptr; slot.active = false; slot.rumble_pending = false; slot.feedback_pending = false; slot.feedback_until_ms = 0; reset_slot_hotkeys(slot); ++slot.state_generation; ++slot.connection_generation; } } critical_section_exit(&g_state_lock); if (!requested) { return; } g_pairing_window_open = false; gap_set_bondable_mode(false); sm_set_accepted_stk_generation_methods(0); uni_bt_del_keys_unsafe(); for (uni_hid_device_t* device : devices) { if (device != nullptr) { uni_hid_device_disconnect(device); } } refresh_pairing_snapshot(); g_connection_status = ConnectionStatus::Scanning; g_status_led_tick = 0; g_pairing_reset_feedback_deadline_ms = now_ms + kPairingResetFeedbackDurationMs; apply_connection_policy(); } void apply_connection_policy() { const bool free_slot = has_free_slot(); const bool active_controller = has_active_controller(); const bool pairing_open = pairing_window_active_at(btstack_run_loop_get_time_ms()); const bool active_scan = free_slot && (!active_controller || pairing_open); const ConnectionPolicyState desired_state = !free_slot ? ConnectionPolicyState::Paused : (active_scan ? ConnectionPolicyState::Open : ConnectionPolicyState::Passive); if (g_connection_policy_state == desired_state) { return; } // Classic inquiry and BLE scanning consume radio time and measurably delay // active controller HID traffic. Stop them before every policy transition. uni_bt_stop_scanning_unsafe(); if (!free_slot) { uni_bt_allow_incoming_connections(false); g_connection_policy_state = ConnectionPolicyState::Paused; return; } // Passive mode still accepts controller-initiated reconnects without // running inquiry. Active discovery is reserved for zero-controller idle // state and the explicit BOOTSEL pairing window. uni_bt_allow_incoming_connections(true); if (active_scan) { uni_bt_start_scanning_and_autoconnect_unsafe(); g_connection_policy_state = ConnectionPolicyState::Open; } else { g_connection_policy_state = ConnectionPolicyState::Passive; } } void update_status_led() { ++g_status_led_tick; const uint32_t now_ms = btstack_run_loop_get_time_ms(); bool led_on = false; if (static_cast( now_ms - g_pairing_reset_feedback_deadline_ms) < 0) { led_on = (g_status_led_tick % 20) < 10; } else if (pairing_window_active_at(now_ms)) { const uint16_t phase = g_status_led_tick % 200; led_on = phase < 20 || (phase >= 40 && phase < 60); } else if (g_connection_status == ConnectionStatus::Connecting) { led_on = (g_status_led_tick % 40) < 20; } else if (g_connection_status == ConnectionStatus::Initializing || has_active_controller()) { led_on = true; } else { led_on = (g_status_led_tick % 200) < 100; } 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) { const uint32_t now_ms = btstack_run_loop_get_time_ms(); process_clear_pairings(now_ms); process_pairing_snapshot_request(); if (update_pairing_window(now_ms)) { apply_connection_policy(); } for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) { RumbleEnvelope envelope{}; FeedbackEnvelope feedback{}; uni_hid_device_t* device = nullptr; bool feedback_dispatch = false; bool host_dispatch = false; critical_section_enter_blocking(&g_state_lock); BackendSlot& slot = g_slots[slot_index]; if (slot.feedback_pending) { feedback = slot.pending_feedback; feedback_dispatch = slot.active && slot.device != nullptr && feedback.connection_generation == slot.connection_generation && slot.device->report_parser.play_dual_rumble != nullptr; slot.feedback_pending = false; if (feedback_dispatch) { device = slot.device; slot.feedback_until_ms = now_ms + feedback.duration_ms; } } const bool feedback_active = static_cast(now_ms - slot.feedback_until_ms) < 0; if (!feedback_dispatch && !feedback_active && slot.rumble_pending) { envelope = slot.pending_rumble; slot.rumble_pending = false; host_dispatch = envelope.slot == slot_index && slot.active && slot.device != nullptr && envelope.connection_generation == slot.connection_generation; if (host_dispatch) { device = slot.device; } } critical_section_exit(&g_state_lock); if (feedback_dispatch) { device->report_parser.play_dual_rumble( device, 0, feedback.duration_ms, feedback.weak_magnitude, feedback.strong_magnitude); } else if (host_dispatch && device->report_parser.play_dual_rumble != nullptr) { const bool stop = envelope.rumble.low_frequency_magnitude == 0 && envelope.rumble.high_frequency_magnitude == 0; device->report_parser.play_dual_rumble( device, 0, stop ? 0 : host_rumble_duration_ms(), 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 recompute_connection_status() { const uint32_t now_ms = btstack_run_loop_get_time_ms(); update_pairing_window(now_ms); g_connection_status = compute_connection_status(); g_status_led_tick = 0; apply_connection_policy(); } void platform_init(int argc, const char** argv) { (void)argc; (void)argv; } void platform_on_init_complete() { gap_set_link_supervision_timeout(kClassicLinkSupervisionTimeout); gap_set_bondable_mode(false); sm_set_accepted_stk_generation_methods(0); gap_ssp_set_auto_accept(false); g_pairing_event_callback.callback = handle_pairing_hci_event; hci_add_event_handler(&g_pairing_event_callback); refresh_pairing_snapshot(); // Keep Bluepad32 autoconnect active whenever at least one slot is free. 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); recompute_connection_status(); } 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 has_free_slot() && g_connection_policy_state == ConnectionPolicyState::Open ? UNI_ERROR_SUCCESS : UNI_ERROR_IGNORE_DEVICE; } void platform_on_device_connected(uni_hid_device_t* device) { if (device == nullptr) { return; } if (g_connection_policy_state != ConnectionPolicyState::Open && g_connection_policy_state != ConnectionPolicyState::Passive) { uni_hid_device_disconnect(device); return; } const int slot_index = slot_for_device(device); if (slot_index < 0) { return; } bool tracked_connection = false; critical_section_enter_blocking(&g_state_lock); BackendSlot& slot = g_slots[slot_index]; if (!slot.active && slot.device == nullptr) { slot.device = device; slot.rumble_pending = false; reset_slot_hotkeys(slot); tracked_connection = true; } else { tracked_connection = slot.device == device; } critical_section_exit(&g_state_lock); if (tracked_connection) { recompute_connection_status(); } } 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_tracked_device = false; critical_section_enter_blocking(&g_state_lock); BackendSlot& slot = g_slots[slot_index]; if (slot.device == device) { if (slot.active) { slot.state = make_neutral_state(); ++slot.state_generation; } slot.device = nullptr; slot.active = false; slot.rumble_pending = false; reset_slot_hotkeys(slot); ++slot.connection_generation; disconnected_tracked_device = true; } critical_section_exit(&g_state_lock); if (disconnected_tracked_device) { // Re-evaluate from scratch: resume discovery only after the final // active controller disconnects; otherwise keep passive incoming // reconnect support without inquiry-induced latency. g_connection_policy_state = ConnectionPolicyState::Uninitialized; recompute_connection_status(); } } 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; bool became_active = false; critical_section_enter_blocking(&g_state_lock); BackendSlot& slot = g_slots[slot_index]; occupied_mismatch = slot.device != nullptr && slot.device != device; if (!occupied_mismatch) { slot.device = device; if (!slot.active) { slot.state = make_neutral_state(); slot.active = true; slot.rumble_pending = false; reset_slot_hotkeys(slot); ++slot.state_generation; became_active = true; } } critical_section_exit(&g_state_lock); if (occupied_mismatch) { return UNI_ERROR_NO_SLOTS; } if (became_active) { apply_slot_lighting(static_cast(slot_index), device); } recompute_connection_status(); 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; } uni_gamepad_t gamepad = controller->gamepad; const HotkeyDecision hotkeys = update_controller_hotkeys( static_cast(slot_index), device, gamepad); gamepad.dpad &= ~hotkeys.suppress_dpad; gamepad.buttons &= ~hotkeys.suppress_buttons; gamepad.misc_buttons &= ~hotkeys.suppress_misc_buttons; publish_device_state( static_cast(slot_index), device, map_gamepad(gamepad, hotkeys.swap_abxy, hotkeys.motion_enabled)); } 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 halt_wireless_backend() { publish_all_neutral(); while (true) { tight_loop_contents(); } } [[noreturn]] void core1_main() { if (!flash_safe_execute_core_init()) { halt_wireless_backend(); } if (cyw43_arch_init() != 0) { halt_wireless_backend(); } 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) { halt_wireless_backend(); } 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; reset_slot_hotkeys(slot); g_consumed_generation[slot_index] = 0; g_last_snapshot_generation[slot_index] = 0; } g_pairing_window_requested = false; g_pairing_snapshot_requested = false; g_pairing_snapshot = {}; g_pairing_snapshot.status = Bluepad32PairingSnapshotStatus::kPending; g_clear_pairings_requested = false; g_connection_status = ConnectionStatus::Initializing; g_connection_policy_state = ConnectionPolicyState::Uninitialized; g_pairing_window_deadline_ms = 0; g_pairing_reset_feedback_deadline_ms = 0; g_pairing_window_open = false; g_initialized = true; } void bluepad32_input_backend_start() { if (!g_initialized) { bluepad32_input_backend_init(); } if (g_started) { return; } // Core 0 services USB from flash while Core 1 owns BTstack. Register both // cores before either side can initiate a flash-backed BTstack TLV write. if (!flash_safe_execute_core_init()) { g_connection_policy_state = ConnectionPolicyState::FailedClosed; return; } g_started = true; multicore_launch_core1(core1_main); } void bluepad32_input_backend_open_pairing_window() { if (!g_initialized) { bluepad32_input_backend_init(); } critical_section_enter_blocking(&g_state_lock); g_pairing_window_requested = true; critical_section_exit(&g_state_lock); } void bluepad32_input_backend_clear_pairings() { if (!g_initialized) { bluepad32_input_backend_init(); } critical_section_enter_blocking(&g_state_lock); g_clear_pairings_requested = true; g_pairing_snapshot.status = Bluepad32PairingSnapshotStatus::kPending; critical_section_exit(&g_state_lock); } void bluepad32_input_backend_request_pairing_snapshot() { if (!g_initialized) { bluepad32_input_backend_init(); } critical_section_enter_blocking(&g_state_lock); g_pairing_snapshot_requested = true; g_pairing_snapshot.status = Bluepad32PairingSnapshotStatus::kPending; critical_section_exit(&g_state_lock); } void bluepad32_input_backend_pairing_snapshot( Bluepad32PairingSnapshot* out) { if (out == nullptr) { return; } if (!g_initialized) { bluepad32_input_backend_init(); } critical_section_enter_blocking(&g_state_lock); *out = g_pairing_snapshot; critical_section_exit(&g_state_lock); } bool bluepad32_input_backend_snapshot(uint8_t slot_index, ControllerState* 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->motion_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 ControllerRumbleOutput& 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); }