Keep native backend locking IRQ-permitting and service USB hardware from SRAM during BOOTSEL sampling. Add bounded input/control flight recording, root IN and EP0 snapshots, and masked-window handoff coverage. Record PC qualification and the remaining Switch stability limitations.
5097 lines
193 KiB
C++
5097 lines
193 KiB
C++
#include "input/bluepad32_input_backend.h"
|
|
#include "bluetooth_transport_config.h"
|
|
#include "input/controller_hotkey_config.h"
|
|
#include "input/switch2_wake.h"
|
|
#ifdef SWITCH_PICO_WII_IR
|
|
#include "input/wii_ir_pointer.h"
|
|
#include "parser/uni_hid_parser_wii_ir.h"
|
|
#endif
|
|
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
|
|
#include "input/switch_native_output.h"
|
|
#endif
|
|
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
|
#include "input/haptics_experiment.h"
|
|
#endif
|
|
#if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) || defined(SWITCH_PICO_NATIVE_SWITCH_RUMBLE)
|
|
#include "input/native_output_scheduler.h"
|
|
#endif
|
|
#include "configuration/configuration_service.h"
|
|
#include "profile/profile_service.h"
|
|
#include <limits.h>
|
|
#include <math.h>
|
|
#include <stddef.h>
|
|
#include <string.h>
|
|
|
|
#include <btstack_run_loop.h>
|
|
#if SWITCH2_PROBE_HUB
|
|
#include <pico/async_context.h>
|
|
#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
|
#include "usb/native_hub/native_hub_trace.h"
|
|
#endif
|
|
#endif
|
|
#include <pico/critical_section.h>
|
|
#include <pico/cyw43_arch.h>
|
|
#include <pico/flash.h>
|
|
#if !SWITCH2_PROBE_HUB
|
|
#include <pico/multicore.h>
|
|
#endif
|
|
#include <pico/stdlib.h>
|
|
#include <uni.h>
|
|
extern "C" {
|
|
#include "parser/uni_hid_parser_wii.h"
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
#include "parser/uni_hid_parser_ds5.h"
|
|
#include "parser/uni_hid_parser_native_motion.h"
|
|
#endif
|
|
}
|
|
#include "parser/uni_hid_parser_switch2.h"
|
|
#include "parser/uni_switch2_haptics.h"
|
|
#include "parser/uni_switch2_pairing.h"
|
|
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
|
|
#include "adapter/adapter_usb_mode.h"
|
|
#endif
|
|
|
|
#if SWITCH2_PROBE_HUB && !PICO_CYW43_ARCH_POLL
|
|
#error "Native hub Bluetooth requires pico_cyw43_arch_poll on Core 0"
|
|
#endif
|
|
#if SWITCH2_PROBE_HUB && !PICO_FLASH_ASSUME_CORE1_SAFE
|
|
#error "Native hub flash writes require its IRQ-disabled SRAM-only Core 1 transport"
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
constexpr int32_t kAxisMinimum = -512;
|
|
constexpr int32_t kAxisMaximum = 511;
|
|
constexpr int32_t kTriggerMaximum = 1023;
|
|
constexpr int32_t kTriggerFullScaleMinimum = 1020;
|
|
constexpr uint16_t kSwitchHostRumbleDurationMs = 50;
|
|
// XInput vibration is stateful and remains active until XInputSetState sends
|
|
// a new magnitude.
|
|
constexpr uint16_t kXInputHostRumbleDurationMs = UINT16_MAX;
|
|
constexpr uint32_t kRumblePollIntervalMs = 5;
|
|
constexpr uint32_t kConfigurationPollIntervalMs = 50;
|
|
constexpr uint8_t kSlotCount = BLUEPAD32_INPUT_BACKEND_SLOT_COUNT;
|
|
constexpr uint32_t kDefaultPairingWindowDurationMs =
|
|
ADAPTER_PAIRING_WINDOW_SECONDS_DEFAULT * 1000u;
|
|
constexpr uint32_t kPairingResetFeedbackDurationMs = 2000;
|
|
// Bluetooth Classic units are 0.625 ms: 0x1900 = 4 seconds.
|
|
constexpr uint16_t kClassicLinkSupervisionTimeout = 0x1900;
|
|
// LE units are 1.25 ms. All Switch 2 links request the 7.5 ms minimum.
|
|
constexpr uint16_t kSwitch2FastInterval = 6;
|
|
constexpr uint32_t kSwitch2IntervalSettleMs = 1000;
|
|
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 uint16_t kProfileFeedbackPhaseDurationMs = 75;
|
|
constexpr uint8_t kProfileFeedbackWeakMagnitude = UINT8_MAX;
|
|
constexpr uint8_t kProfileFeedbackStrongMagnitude = UINT8_MAX;
|
|
constexpr uint32_t kJoyConGestureHoldMs = 2000;
|
|
constexpr uint32_t kJoyConGestureFreshMs = 250;
|
|
constexpr uint16_t kJoyConGestureFeedbackMs = 75;
|
|
#ifdef SWITCH_PICO_WII_IR_GYRO
|
|
constexpr uint32_t kWiiAimChordHoldUs = 2000000;
|
|
constexpr uint32_t kWiiAimChordFreshUs = 150000;
|
|
constexpr uint16_t kWiiAimChordButtons = 0x0002 | 0x0001;
|
|
#endif
|
|
// One initial indication can be followed by one committed switch before the
|
|
// BTstack timer drains the queue. Profile commits are rate-limited well beyond
|
|
// the longest feedback sequence.
|
|
constexpr uint8_t kProfileFeedbackQueueCapacity = 2;
|
|
constexpr SwitchRgbColor kProfileLightbarPalette[CONTROLLER_PROFILE_COUNT] = {
|
|
{0x00, 0x55, 0xff},
|
|
{0x00, 0xcc, 0x66},
|
|
{0xff, 0xaa, 0x00},
|
|
{0xcc, 0x33, 0xff},
|
|
{0xff, 0x44, 0x44},
|
|
{0x00, 0xdd, 0xdd},
|
|
{0xff, 0x66, 0xbb},
|
|
{0xcc, 0xff, 0x33},
|
|
};
|
|
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(kProfileFeedbackPhaseDurationMs == 75);
|
|
static_assert(CONTROLLER_PROFILE_COUNT == 8);
|
|
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;
|
|
uint16_t duration_ms;
|
|
uint32_t received_ms = 0;
|
|
#if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) || defined(SWITCH_PICO_NATIVE_SWITCH_RUMBLE)
|
|
uint64_t received_us = 0;
|
|
#endif
|
|
};
|
|
|
|
constexpr uint8_t kSwitch2IngressCapacity = 16;
|
|
struct Switch2HostCommand {
|
|
RumbleEnvelope envelope;
|
|
uint32_t generation;
|
|
uint8_t accepted_halves;
|
|
};
|
|
struct Switch2Ingress {
|
|
Switch2HostCommand commands[kSwitch2IngressCapacity];
|
|
uint32_t generation;
|
|
uint8_t host_mode;
|
|
uint8_t head;
|
|
uint8_t count;
|
|
bool reset_pending;
|
|
};
|
|
struct FeedbackEnvelope {
|
|
uint32_t connection_generation;
|
|
uint16_t duration_ms;
|
|
uint8_t weak_magnitude;
|
|
uint8_t strong_magnitude;
|
|
};
|
|
struct ProfileFeedbackEnvelope {
|
|
uint32_t connection_generation;
|
|
uint8_t active_profile_number;
|
|
ControllerProfileConfirmationPolicy policy;
|
|
};
|
|
|
|
struct ProfileFeedbackSequence {
|
|
uint32_t connection_generation;
|
|
uint32_t phase_deadline_ms;
|
|
uint8_t pulse_count;
|
|
uint8_t pulses_started;
|
|
bool active;
|
|
bool on;
|
|
bool rumble_enabled;
|
|
bool led_enabled;
|
|
};
|
|
struct WiiOrientationRequest {
|
|
ControllerIdentity identity;
|
|
uint32_t connection_generation;
|
|
bool vertical;
|
|
};
|
|
#ifdef SWITCH_PICO_WII_IR_GYRO
|
|
struct WiiAimSource {
|
|
uint32_t sequence;
|
|
uint32_t last_report_us;
|
|
uint32_t started_us;
|
|
bool have_sequence;
|
|
bool infrared;
|
|
bool holding;
|
|
bool masked;
|
|
bool latched;
|
|
bool reposition_masked;
|
|
};
|
|
#endif
|
|
|
|
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
|
struct WiiMotionIngress {
|
|
uint32_t received_us = 0;
|
|
uint32_t accel_sequence = 0;
|
|
uint32_t gyro_sequence = 0;
|
|
uint32_t nunchuk_sequence = 0;
|
|
uint32_t accel_received_us = 0;
|
|
uint32_t gyro_received_us = 0;
|
|
int32_t accel_q13[3]{};
|
|
int32_t gyro_q10[3]{};
|
|
bool accel_valid = false;
|
|
bool gyro_valid = false;
|
|
};
|
|
|
|
struct WiiCue {
|
|
uint64_t token = 0;
|
|
uint32_t connection_generation = 0;
|
|
uint32_t requested_ms = 0;
|
|
uint32_t started_ms = 0;
|
|
uint8_t slot = 0xff;
|
|
uint8_t sample_id = 0;
|
|
uint8_t dispatched_phase = 0xff;
|
|
int result = -1;
|
|
bool consumed = false;
|
|
bool active = false;
|
|
bool stop_pending = false;
|
|
bool in_flight = false;
|
|
};
|
|
|
|
// Wii has a fixed-strength ERM motor: "soft"/"strong" are approximated only
|
|
// through pulse length, not invented HD frequencies or amplitude control.
|
|
struct WiiCuePattern {
|
|
uint16_t phases_ms[7]; // Alternating on/gap, starting and ending on.
|
|
uint8_t count;
|
|
};
|
|
constexpr WiiCuePattern kWiiCuePatterns[8] = {
|
|
{{0}, 0},
|
|
{{1000}, 1},
|
|
{{100, 180, 100, 180, 100, 180, 100}, 7},
|
|
{{25, 90, 25}, 3},
|
|
{{100, 140, 100}, 3},
|
|
{{70, 120, 70}, 3},
|
|
{{60}, 1},
|
|
{{120}, 1},
|
|
};
|
|
constexpr uint32_t kWiiCueDeadlineMs = 2000;
|
|
#endif
|
|
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
struct NativeGamepadIngress {
|
|
uint32_t received_us = 0;
|
|
uint32_t accel_sequence = 0;
|
|
uint32_t gyro_sequence = 0;
|
|
uint32_t accel_received_us = 0;
|
|
uint32_t gyro_received_us = 0;
|
|
bool has_report = false;
|
|
bool accel_valid = false;
|
|
bool gyro_valid = false;
|
|
int32_t accel_q13[3]{};
|
|
int32_t gyro_q10[3]{};
|
|
};
|
|
|
|
// Physical parser counters survive logical pairing/reselection epochs. They
|
|
// are retired only with the Bluetooth connection, never by a snapshot getter.
|
|
struct NativeGamepadReportIngress {
|
|
uni_hid_device_t* device = nullptr;
|
|
uint32_t report_sequence = 0;
|
|
uint32_t accel_sequence = 0;
|
|
uint32_t gyro_sequence = 0;
|
|
};
|
|
|
|
struct NativeGamepadCue {
|
|
uint64_t token = 0;
|
|
uint32_t connection_generation = 0;
|
|
uint32_t requested_ms = 0;
|
|
uint32_t started_ms = 0;
|
|
uint8_t slot = 0xff;
|
|
uint8_t sample_id = 0;
|
|
int result = -1;
|
|
bool active = false;
|
|
bool consumed = false;
|
|
bool in_flight = false;
|
|
};
|
|
|
|
struct NativeGamepadMotorOutput {
|
|
uint32_t deadline_ms = 0;
|
|
uint8_t magnitude[2]{};
|
|
bool owned = false;
|
|
};
|
|
|
|
// Same bounded pulse vocabulary as Wii, with source-driver motor magnitudes.
|
|
// These are compatibility-vibration approximations, not uploaded HD waveforms.
|
|
struct NativeGamepadCuePattern {
|
|
uint16_t phases_ms[7];
|
|
uint8_t count;
|
|
uint8_t magnitude;
|
|
};
|
|
constexpr NativeGamepadCuePattern kNativeGamepadCuePatterns[8] = {
|
|
{{0}, 0, 0},
|
|
{{1000}, 1, 160},
|
|
{{100, 180, 100, 180, 100, 180, 100}, 7, 200},
|
|
{{25, 90, 25}, 3, 96},
|
|
{{100, 140, 100}, 3, 220},
|
|
{{70, 120, 70}, 3, 160},
|
|
{{60}, 1, 96},
|
|
{{120}, 1, 220},
|
|
};
|
|
constexpr uint32_t kNativeGamepadCueDeadlineMs = 2000;
|
|
#endif
|
|
|
|
|
|
|
|
// Security Manager identity events arrive before Bluepad32 publishes a ready
|
|
// device. Retain only the four live handle/address associations so a BLE RPA
|
|
// is never promoted to a stable identity on its own.
|
|
struct BleIdentityMapping {
|
|
bool used;
|
|
hci_con_handle_t connection_handle;
|
|
bd_addr_t connection_address;
|
|
uint8_t identity_address_type;
|
|
bd_addr_t identity_address;
|
|
};
|
|
|
|
|
|
struct BackendSlot {
|
|
ControllerState state;
|
|
WiiAccelerometerSample accelerometer{};
|
|
WiiAccelerometerSample nunchuk_accelerometer{};
|
|
uint16_t pre_hotkey_button_mask;
|
|
ControllerIdentity identity;
|
|
// Non-null with active=false is a connected device still becoming ready.
|
|
uni_hid_device_t* device;
|
|
// A pair occupies one logical profile owner and output, but still consumes
|
|
// two Bluepad32 physical device indices. device is always the left half.
|
|
uni_hid_device_t* companion;
|
|
uni_gamepad_t gamepad;
|
|
uni_gamepad_t companion_gamepad;
|
|
uint8_t extra_buttons;
|
|
uint8_t companion_extra_buttons;
|
|
uint32_t state_generation;
|
|
uint32_t connection_generation;
|
|
bool active;
|
|
bool wii_orientation_pending;
|
|
WiiOrientationRequest pending_wii_orientation;
|
|
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
|
WiiMotionIngress wii_motion;
|
|
#endif
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
NativeGamepadIngress native_motion;
|
|
NativeGamepadMotorOutput native_output;
|
|
#endif
|
|
#ifdef SWITCH_PICO_WII_IR_GYRO
|
|
WiiAimSource wii_aim;
|
|
#endif
|
|
bool rumble_pending;
|
|
bool motion_enabled;
|
|
bool feedback_pending;
|
|
uint32_t feedback_until_ms;
|
|
uint8_t pending_profile_feedback_count;
|
|
RumbleEnvelope pending_rumble;
|
|
bool retained_host_rumble_valid;
|
|
RumbleEnvelope retained_host_rumble;
|
|
Switch2Ingress switch2_ingress;
|
|
FeedbackEnvelope pending_feedback;
|
|
ProfileFeedbackEnvelope
|
|
pending_profile_feedback[kProfileFeedbackQueueCapacity];
|
|
ProfileFeedbackSequence profile_feedback;
|
|
};
|
|
|
|
#if SWITCH2_PROBE_HUB
|
|
spin_lock_t* g_state_lock;
|
|
static __force_inline void backend_state_lock_enter() {
|
|
// Hub Bluetooth is polled on Core0, and USB IRQs only enqueue transport
|
|
// events. Neither IRQs nor Core1 may enter backend state. Keep USB IRQs
|
|
// serviceable while holding this lock; preserve any caller-owned masking.
|
|
if (get_core_num() != 0 || __get_current_exception() != 0) {
|
|
panic("Native hub backend state requires Core0 foreground");
|
|
}
|
|
spin_lock_unsafe_blocking(g_state_lock);
|
|
}
|
|
#define backend_state_lock_exit() spin_unlock_unsafe(g_state_lock)
|
|
#else
|
|
critical_section_t g_state_lock;
|
|
#define backend_state_lock_enter() critical_section_enter_blocking(&g_state_lock)
|
|
#define backend_state_lock_exit() critical_section_exit(&g_state_lock)
|
|
#endif
|
|
#if SWITCH2_PROBE_HUB && defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
|
static uint32_t g_state_lock_trace_parent;
|
|
static __force_inline void trace_state_lock_enter(uint32_t line) {
|
|
backend_state_lock_enter();
|
|
g_state_lock_trace_parent = native_hub_trace_phase(
|
|
NATIVE_HUB_TRACE_PHASE_BACKEND_LOCK | line);
|
|
}
|
|
static __force_inline void trace_state_lock_exit() {
|
|
native_hub_trace_phase(g_state_lock_trace_parent);
|
|
backend_state_lock_exit();
|
|
}
|
|
#define state_lock_enter() trace_state_lock_enter(__LINE__)
|
|
#define state_lock_exit() trace_state_lock_exit()
|
|
#else
|
|
#define state_lock_enter() backend_state_lock_enter()
|
|
#define state_lock_exit() backend_state_lock_exit()
|
|
#endif
|
|
uni_hid_device_t* g_retired_devices[kSlotCount]{};
|
|
BackendSlot g_slots[kSlotCount];
|
|
ControllerMacroCapture g_macro_capture;
|
|
#if !SWITCH2_PROBE_HUB
|
|
// Catalog migration/compaction needs more than the 4 KiB scratch bank.
|
|
// Supply a dedicated static stack in main SRAM rather than overflowing it.
|
|
alignas(8) uint32_t g_core1_stack[4096];
|
|
#else
|
|
bool g_poll_ready = false;
|
|
#endif
|
|
BleIdentityMapping g_ble_identity_mappings[kSlotCount]{};
|
|
|
|
// These acknowledgement generations and request producers are only used by
|
|
// Core 0. Requests are 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;
|
|
uint32_t g_clear_pairings_requested_token = 0;
|
|
uint32_t g_clear_pairings_in_progress_token = 0;
|
|
uint32_t g_next_clear_pairings_request_token = 1;
|
|
bool g_pairing_snapshot_requested = false;
|
|
bool g_initialized = false;
|
|
bool g_started = false;
|
|
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
|
bool g_wii_source_selected = false;
|
|
uint8_t g_wii_source_address[6]{};
|
|
Bluepad32WiiBridgeSnapshot g_wii_snapshot{};
|
|
WiiCue g_wii_cue{};
|
|
uint64_t g_next_wii_cue_token = 1;
|
|
|
|
// All native Wii state shares the backend lock. Only Core 1 dereferences a
|
|
// parser or dispatches transport; Core 0 sees a copied, published snapshot.
|
|
bool is_selected_wii(const BackendSlot& slot) {
|
|
return g_wii_source_selected && slot.active && slot.device != nullptr &&
|
|
slot.companion == nullptr &&
|
|
slot.device->controller_type == CONTROLLER_TYPE_WiiController &&
|
|
memcmp(slot.device->conn.btaddr, g_wii_source_address, 6) == 0;
|
|
}
|
|
|
|
void retire_wii_motion(WiiMotionIngress& motion) {
|
|
// Keep the observed counters across logical epochs: a cached parser sample
|
|
// must not acquire a new receipt timestamp after orientation/reselection.
|
|
const uint32_t accel_sequence = motion.accel_sequence;
|
|
const uint32_t gyro_sequence = motion.gyro_sequence;
|
|
const uint32_t nunchuk_sequence = motion.nunchuk_sequence;
|
|
motion = {};
|
|
motion.accel_sequence = accel_sequence;
|
|
motion.gyro_sequence = gyro_sequence;
|
|
motion.nunchuk_sequence = nunchuk_sequence;
|
|
}
|
|
|
|
void cancel_wii_cue_locked() {
|
|
g_wii_cue.stop_pending = g_wii_cue.stop_pending ||
|
|
g_wii_cue.active || g_wii_cue.in_flight;
|
|
g_wii_cue.active = false;
|
|
g_wii_cue.result = -1;
|
|
}
|
|
|
|
void retire_wii_slot(uint8_t slot_index) {
|
|
if (g_wii_snapshot.slot == slot_index) g_wii_snapshot = {};
|
|
if (g_wii_cue.slot == slot_index) {
|
|
// The connection is gone or its owner is being reset on Core 1.
|
|
// Never send a deferred stop into a replacement generation.
|
|
g_wii_cue = {};
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
bool g_native_explicit_address = false;
|
|
uint8_t g_native_address[6]{};
|
|
uint8_t g_native_slot = 0xff;
|
|
uint32_t g_native_generation = 0;
|
|
Bluepad32NativeGamepadSnapshot g_native_snapshot{};
|
|
NativeGamepadCue g_native_cues[2]{};
|
|
uint64_t g_next_native_token = 1;
|
|
uni_hid_device_t* g_native_pending_devices[kSlotCount]{};
|
|
NativeGamepadReportIngress g_native_reports[kSlotCount]{};
|
|
|
|
bool native_device_allowed(const uni_hid_device_t* device) {
|
|
if (device == nullptr || !uni_hid_device_is_gamepad(device)) return false;
|
|
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
|
return device->controller_type == CONTROLLER_TYPE_PS5Controller &&
|
|
device->report_parser.parse_input_report == uni_hid_parser_ds5_parse_input_report;
|
|
#else
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
bool native_address_matches(const uni_hid_device_t* device) {
|
|
return device != nullptr && memcmp(device->conn.btaddr, g_native_address, 6) == 0;
|
|
}
|
|
|
|
bool eligible_native_gamepad(const BackendSlot& slot) {
|
|
return slot.active && native_device_allowed(slot.device) &&
|
|
(slot.companion == nullptr || native_device_allowed(slot.companion)) &&
|
|
(!g_native_explicit_address || native_address_matches(slot.device) ||
|
|
native_address_matches(slot.companion));
|
|
}
|
|
|
|
uni_hid_device_t* native_rumble_target(const BackendSlot& slot, uint8_t side) {
|
|
// Paired Joy-Cons have a real left owner and right companion. A solo pad's
|
|
// driver owns its motor topology, including mono OR/max mixing.
|
|
return slot.companion != nullptr && side == 0 ? slot.companion : slot.device;
|
|
}
|
|
|
|
bool native_rumble_capable(const BackendSlot& slot, uint8_t side) {
|
|
const uni_hid_device_t* device = native_rumble_target(slot, side);
|
|
return device != nullptr && device->report_parser.play_dual_rumble != nullptr;
|
|
}
|
|
|
|
void cancel_native_cue_locked(NativeGamepadCue& cue) {
|
|
cue.active = false;
|
|
cue.result = -1;
|
|
// The slot's last motor output remains owned until the timer replaces it.
|
|
}
|
|
|
|
void refresh_native_source_locked(bool reselection = false) {
|
|
uint8_t selected = 0xff;
|
|
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
|
if (!eligible_native_gamepad(g_slots[index])) continue;
|
|
if (selected != 0xff) {
|
|
selected = 0xff; // Never blend or choose by connection order.
|
|
break;
|
|
}
|
|
selected = index;
|
|
}
|
|
if (!reselection && selected == g_native_slot &&
|
|
(selected == 0xff ||
|
|
g_slots[selected].connection_generation == g_native_generation)) return;
|
|
for (NativeGamepadCue& cue : g_native_cues) cancel_native_cue_locked(cue);
|
|
g_native_snapshot = {};
|
|
g_native_slot = selected;
|
|
g_native_generation = 0;
|
|
if (selected != 0xff) {
|
|
BackendSlot& slot = g_slots[selected];
|
|
g_macro_capture.disconnect(selected, slot.connection_generation, time_us_32());
|
|
// A missed inactive snapshot must still retire the adapter's old epoch.
|
|
g_native_generation = ++slot.connection_generation;
|
|
++slot.state_generation;
|
|
slot.native_motion = {};
|
|
}
|
|
}
|
|
|
|
void retire_native_slot(uint8_t index) {
|
|
if (g_native_slot == index) {
|
|
g_native_slot = 0xff;
|
|
g_native_generation = 0;
|
|
g_native_snapshot = {};
|
|
}
|
|
for (NativeGamepadCue& cue : g_native_cues)
|
|
if (cue.slot == index) cue = {};
|
|
g_slots[index].native_motion = {};
|
|
g_slots[index].native_output = {};
|
|
}
|
|
|
|
bool native_cue_current(const NativeGamepadCue& cue) {
|
|
return cue.slot < kSlotCount && cue.slot == g_native_slot &&
|
|
cue.connection_generation == g_native_generation &&
|
|
cue.connection_generation == g_slots[cue.slot].connection_generation;
|
|
}
|
|
#endif
|
|
|
|
// These fields are only read or written by the BTstack execution context.
|
|
btstack_timer_source_t g_rumble_timer{};
|
|
btstack_timer_source_t g_configuration_timer{};
|
|
ConnectionStatus g_connection_status = ConnectionStatus::Initializing;
|
|
btstack_packet_callback_registration_t g_pairing_event_callback{};
|
|
btstack_packet_callback_registration_t g_identity_event_callback{};
|
|
ConnectionPolicyState g_connection_policy_state =
|
|
ConnectionPolicyState::Uninitialized;
|
|
bool g_background_scan_active = false;
|
|
struct Switch2IntervalRequest {
|
|
hci_con_handle_t handle = HCI_CON_HANDLE_INVALID;
|
|
uint16_t interval = 0;
|
|
uint32_t requested_ms = 0;
|
|
};
|
|
Switch2IntervalRequest g_switch2_interval_requests[kSlotCount]{};
|
|
JoyConMode g_joycon_mode = JoyConMode::kPaired;
|
|
bool g_joycon_reconcile_requested = false;
|
|
// Live-link hints only: splitting two pairs must not exchange their members
|
|
// when the next Paired preference is applied.
|
|
struct JoyConPairHint {
|
|
uni_hid_device_t* mate = nullptr;
|
|
uint8_t owner_slot = 0;
|
|
};
|
|
JoyConPairHint g_joycon_pair_hints[kSlotCount]{};
|
|
|
|
enum class JoyConGroupingOverride : uint8_t {
|
|
Default,
|
|
Individual,
|
|
Paired,
|
|
};
|
|
struct JoyConConnectionOverride {
|
|
JoyConGroupingOverride mode = JoyConGroupingOverride::Default;
|
|
uni_hid_device_t* mate = nullptr;
|
|
};
|
|
JoyConConnectionOverride g_joycon_overrides[kSlotCount]{};
|
|
|
|
// BTstack physical-link state survives logical slot moves. Raw reports stay in
|
|
// BackendSlot; only the derived logical view consumes the reserved buttons.
|
|
struct JoyConGesture {
|
|
uni_hid_device_t* device = nullptr;
|
|
uint32_t last_report_ms = 0;
|
|
uint32_t started_ms = 0;
|
|
uint8_t participants = 0;
|
|
bool held = false;
|
|
bool released = true;
|
|
bool masked = false;
|
|
bool blocked = false;
|
|
bool joining = false;
|
|
};
|
|
JoyConGesture g_joycon_gestures[kSlotCount]{};
|
|
uint32_t g_pairing_window_deadline_ms = 0;
|
|
uint32_t g_pairing_window_duration_ms =
|
|
kDefaultPairingWindowDurationMs;
|
|
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{};
|
|
uint32_t g_initialization_stage = 0;
|
|
uint32_t g_rumble_timer_ticks = 0;
|
|
uint32_t g_configuration_timer_ticks = 0;
|
|
uint32_t g_controller_reports = 0;
|
|
uint32_t g_host_rumble_requests = 0;
|
|
uint32_t g_local_feedback_requests = 0;
|
|
uint32_t g_rumble_dispatches = 0;
|
|
uint32_t g_switch2_ingress_drops = 0;
|
|
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
|
uint32_t g_seeded_native_run_id = 0;
|
|
#endif
|
|
|
|
uint16_t host_rumble_duration_ms() {
|
|
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
|
|
if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) {
|
|
return kXInputHostRumbleDurationMs;
|
|
}
|
|
#endif
|
|
return kSwitchHostRumbleDurationMs;
|
|
}
|
|
|
|
uint8_t switch2_host_mode() {
|
|
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
|
|
return static_cast<uint8_t>(adapter_host_probe_mode());
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
ControllerState make_neutral_state() {
|
|
return controller_neutral_state();
|
|
}
|
|
|
|
bool valid_slot(uint8_t slot) {
|
|
return slot < kSlotCount;
|
|
}
|
|
|
|
bool has_free_slot() {
|
|
state_lock_enter();
|
|
unsigned physical_count = 0;
|
|
for (const BackendSlot& slot : g_slots) {
|
|
physical_count += slot.device != nullptr;
|
|
physical_count += slot.companion != nullptr;
|
|
}
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
for (const auto* pending : g_native_pending_devices) physical_count += pending != nullptr;
|
|
#endif
|
|
state_lock_exit();
|
|
return physical_count < kSlotCount;
|
|
}
|
|
|
|
bool has_active_controller() {
|
|
state_lock_enter();
|
|
bool active_controller = false;
|
|
for (const BackendSlot& slot : g_slots) {
|
|
active_controller = active_controller || slot.active;
|
|
}
|
|
state_lock_exit();
|
|
return active_controller;
|
|
}
|
|
|
|
int physical_index_for_device(const uni_hid_device_t* device) {
|
|
if (device == nullptr) {
|
|
return -1;
|
|
}
|
|
const int index = uni_hid_device_get_idx_for_instance(device);
|
|
return index >= 0 && index < kSlotCount ? index : -1;
|
|
}
|
|
|
|
int slot_for_device(const uni_hid_device_t* device) {
|
|
if (device != nullptr) {
|
|
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
|
if (g_slots[index].device == device ||
|
|
g_slots[index].companion == device) {
|
|
return index;
|
|
}
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Called under the state lock. Bluepad32's index is a transport resource, not
|
|
// an output index once two Joy-Cons merge. Never evict an unrelated output.
|
|
int reserve_device_slot(uni_hid_device_t* device) {
|
|
const int physical_index = physical_index_for_device(device);
|
|
if (physical_index < 0) {
|
|
return -1;
|
|
}
|
|
const int tracked = slot_for_device(device);
|
|
if (g_retired_devices[physical_index] == device) {
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
return -1;
|
|
#else
|
|
if (uni_hid_parser_switch2_is_ble_device(device)) return -1;
|
|
#endif
|
|
}
|
|
if (tracked >= 0) {
|
|
return tracked;
|
|
}
|
|
for (const BackendSlot& slot : g_slots) {
|
|
if ((slot.device != nullptr &&
|
|
physical_index_for_device(slot.device) == physical_index) ||
|
|
(slot.companion != nullptr &&
|
|
physical_index_for_device(slot.companion) == physical_index)) {
|
|
return -1;
|
|
}
|
|
}
|
|
#if !SWITCH2_BRIDGE_FULL_INPUT
|
|
if (g_slots[physical_index].device == nullptr) {
|
|
return physical_index;
|
|
}
|
|
#endif
|
|
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
|
if (g_slots[index].device == nullptr) {
|
|
return index;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
int joycon_side(const uni_hid_device_t* device) {
|
|
if (!uni_hid_parser_switch2_is_ble_device(device)) {
|
|
return 0;
|
|
}
|
|
if (device->product_id == UNI_SW2_JOYCON_L_PID) {
|
|
return -1;
|
|
}
|
|
return device->product_id == UNI_SW2_JOYCON_R_PID ? 1 : 0;
|
|
}
|
|
|
|
bool joycon_default_pairing_allowed(const uni_hid_device_t* device) {
|
|
const int index = physical_index_for_device(device);
|
|
return g_joycon_mode == JoyConMode::kPaired && index >= 0 &&
|
|
g_joycon_overrides[index].mode == JoyConGroupingOverride::Default;
|
|
}
|
|
|
|
// Include the entire old attempt when another held solo makes selection
|
|
// ambiguous. None of its members may retry until all have released.
|
|
void block_joycon_gesture(uint8_t participants) {
|
|
for (uint8_t pass = 0; pass < kSlotCount; ++pass) {
|
|
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
|
if (participants & (1u << index)) {
|
|
participants |= g_joycon_gestures[index].participants;
|
|
}
|
|
}
|
|
}
|
|
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
|
if (!(participants & (1u << index))) continue;
|
|
g_joycon_gestures[index].participants = participants;
|
|
g_joycon_gestures[index].blocked = true;
|
|
}
|
|
}
|
|
|
|
bool joycon_gesture_live(uint8_t index) {
|
|
const JoyConGesture& gesture = g_joycon_gestures[index];
|
|
const int slot = slot_for_device(gesture.device);
|
|
return gesture.device != nullptr && slot >= 0 && g_slots[slot].active &&
|
|
physical_index_for_device(gesture.device) == index &&
|
|
joycon_side(gesture.device) != 0;
|
|
}
|
|
|
|
void arm_joycon_gesture(uint8_t left, uint8_t right, uint32_t now_ms,
|
|
bool joining) {
|
|
JoyConGesture& l = g_joycon_gestures[left];
|
|
JoyConGesture& r = g_joycon_gestures[right];
|
|
if (!l.held || !r.held || l.blocked || r.blocked ||
|
|
l.participants != (1u << left) ||
|
|
r.participants != (1u << right)) return;
|
|
l.participants = r.participants = (1u << left) | (1u << right);
|
|
l.started_ms = r.started_ms = now_ms;
|
|
l.joining = r.joining = joining;
|
|
}
|
|
|
|
// Caller holds the state lock. Reports perform admission; the timer also
|
|
// expires attempts, but elapsed cached input alone can never complete a hold.
|
|
void refresh_joycon_gestures(uint32_t now_ms) {
|
|
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
|
JoyConGesture& gesture = g_joycon_gestures[index];
|
|
if (gesture.participants == 0 || gesture.blocked) continue;
|
|
if (!joycon_gesture_live(index) || !gesture.held ||
|
|
now_ms - gesture.last_report_ms > kJoyConGestureFreshMs) {
|
|
block_joycon_gesture(gesture.participants);
|
|
}
|
|
}
|
|
for (JoyConGesture& gesture : g_joycon_gestures) {
|
|
if (gesture.participants == 0) continue;
|
|
bool released = true;
|
|
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
|
if ((gesture.participants & (1u << index)) &&
|
|
!g_joycon_gestures[index].released) released = false;
|
|
}
|
|
if (!released) continue;
|
|
const uint8_t participants = gesture.participants;
|
|
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
|
if (!(participants & (1u << index))) continue;
|
|
g_joycon_gestures[index].participants = 0;
|
|
g_joycon_gestures[index].blocked = false;
|
|
}
|
|
}
|
|
|
|
int left = -1;
|
|
int right = -1;
|
|
uint8_t solos = 0;
|
|
bool ambiguous = false;
|
|
for (const BackendSlot& slot : g_slots) {
|
|
if (!slot.active || joycon_side(slot.device) == 0) continue;
|
|
const int index = physical_index_for_device(slot.device);
|
|
if (index < 0) continue;
|
|
if (slot.companion != nullptr) {
|
|
const int mate = physical_index_for_device(slot.companion);
|
|
if (mate >= 0) arm_joycon_gesture(index, mate, now_ms, false);
|
|
continue;
|
|
}
|
|
const JoyConGesture& gesture = g_joycon_gestures[index];
|
|
if (gesture.device != slot.device || !gesture.held || gesture.blocked) continue;
|
|
solos |= 1u << index;
|
|
int& side = joycon_side(slot.device) < 0 ? left : right;
|
|
ambiguous = ambiguous || side >= 0;
|
|
side = index;
|
|
}
|
|
if (ambiguous) {
|
|
block_joycon_gesture(solos);
|
|
} else if (left >= 0 && right >= 0) {
|
|
arm_joycon_gesture(left, right, now_ms, true);
|
|
}
|
|
}
|
|
|
|
void observe_joycon_gesture(uni_hid_device_t* device,
|
|
const uni_gamepad_t& raw, uint32_t now_ms) {
|
|
const int side = joycon_side(device);
|
|
const int index = physical_index_for_device(device);
|
|
if (side == 0 || index < 0) return;
|
|
// Expire before updating the timestamp: a returning stale report must not
|
|
// hide a gap, even when no timer ran during it.
|
|
refresh_joycon_gestures(now_ms);
|
|
JoyConGesture& gesture = g_joycon_gestures[index];
|
|
gesture.device = device;
|
|
const bool trigger = (raw.buttons &
|
|
(side < 0 ? BUTTON_TRIGGER_L : BUTTON_TRIGGER_R)) != 0;
|
|
const bool menu = (raw.misc_buttons &
|
|
(side < 0 ? MISC_BUTTON_SELECT : MISC_BUTTON_START)) != 0;
|
|
gesture.last_report_ms = now_ms;
|
|
gesture.held = trigger && menu;
|
|
gesture.released = !trigger && !menu;
|
|
if (gesture.held) {
|
|
gesture.masked = true;
|
|
if (gesture.participants == 0) gesture.participants = 1u << index;
|
|
} else if (gesture.released) {
|
|
gesture.masked = false;
|
|
}
|
|
refresh_joycon_gestures(now_ms);
|
|
}
|
|
|
|
bool joycon_gesture_masked(const uni_hid_device_t* device) {
|
|
const int index = physical_index_for_device(device);
|
|
return index >= 0 && g_joycon_gestures[index].device == device &&
|
|
g_joycon_gestures[index].masked;
|
|
}
|
|
|
|
void mask_joycon_gesture(uni_gamepad_t& gamepad,
|
|
const uni_hid_device_t* device) {
|
|
if (!joycon_gesture_masked(device)) return;
|
|
if (joycon_side(device) < 0) {
|
|
gamepad.buttons &= ~BUTTON_TRIGGER_L;
|
|
gamepad.misc_buttons &= ~MISC_BUTTON_SELECT;
|
|
gamepad.brake = 0;
|
|
} else {
|
|
gamepad.buttons &= ~BUTTON_TRIGGER_R;
|
|
gamepad.misc_buttons &= ~MISC_BUTTON_START;
|
|
gamepad.throttle = 0;
|
|
}
|
|
}
|
|
|
|
bool waiting_for_joycon_mate(int side = 0) {
|
|
state_lock_enter();
|
|
unsigned physical_count = 0;
|
|
bool pending = false;
|
|
unsigned left_count = 0;
|
|
unsigned right_count = 0;
|
|
for (const BackendSlot& slot : g_slots) {
|
|
physical_count += slot.device != nullptr;
|
|
physical_count += slot.companion != nullptr;
|
|
pending = pending || (slot.device != nullptr && !slot.active);
|
|
// An explicit solo choice is complete, not a request for another
|
|
// default-paired mate. Individual defaults retain balanced reconnects.
|
|
if (slot.active && slot.companion == nullptr &&
|
|
(g_joycon_mode == JoyConMode::kIndividual ||
|
|
joycon_default_pairing_allowed(slot.device))) {
|
|
const int candidate_side = joycon_side(slot.device);
|
|
left_count += candidate_side < 0;
|
|
right_count += candidate_side > 0;
|
|
}
|
|
}
|
|
state_lock_exit();
|
|
// Individual players still reconnect their remembered opposite half, but
|
|
// a balanced set is complete even though no logical pair was created.
|
|
const bool missing_left = g_joycon_mode == JoyConMode::kIndividual
|
|
? right_count > left_count
|
|
: right_count != 0;
|
|
const bool missing_right = g_joycon_mode == JoyConMode::kIndividual
|
|
? left_count > right_count
|
|
: left_count != 0;
|
|
return physical_count < kSlotCount && !pending &&
|
|
((side <= 0 && missing_left) || (side >= 0 && missing_right));
|
|
}
|
|
|
|
void stop_background_scan() {
|
|
if (!SWITCH_PICO_ENABLE_BLE) {
|
|
return;
|
|
}
|
|
if (g_background_scan_active) {
|
|
// Direct LE scans do not update Bluepad32's aggregate scanning flag.
|
|
uni_bt_le_scan_stop();
|
|
g_background_scan_active = false;
|
|
}
|
|
}
|
|
// BTstack only. Reconcile every ready physical Switch 2 link to the fast interval,
|
|
// independently of player grouping, controller count, or Classic connections.
|
|
void apply_radio_connection_policy() {
|
|
if (!SWITCH_PICO_ENABLE_BLE) {
|
|
return;
|
|
}
|
|
uni_hid_device_t* ready[kSlotCount]{};
|
|
for (const BackendSlot& slot : g_slots) {
|
|
uni_hid_device_t* targets[] = {slot.device, slot.companion};
|
|
for (uni_hid_device_t* target : targets) {
|
|
const int index = physical_index_for_device(target);
|
|
if (index < 0) continue;
|
|
const auto type = gap_get_connection_type(target->conn.handle);
|
|
if (type == GAP_CONNECTION_LE &&
|
|
uni_hid_parser_switch2_is_ble_device(target)) {
|
|
// The parser requests its initial interval during setup.
|
|
// Do not race that request by changing a pending device here.
|
|
if (slot.active) ready[index] = target;
|
|
}
|
|
}
|
|
}
|
|
const uint32_t now_ms = btstack_run_loop_get_time_ms();
|
|
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
|
auto& request = g_switch2_interval_requests[index];
|
|
if (ready[index] == nullptr) {
|
|
request = {};
|
|
continue;
|
|
}
|
|
const auto handle = ready[index]->conn.handle;
|
|
if (request.handle != handle) request = {};
|
|
const uint16_t actual = gap_le_connection_interval(handle);
|
|
if (request.interval != 0 && actual != request.interval &&
|
|
now_ms - request.requested_ms < kSwitch2IntervalSettleMs) {
|
|
// Let an accepted asynchronous update settle before retrying it.
|
|
// API success alone does not prove that negotiation completed.
|
|
continue;
|
|
}
|
|
request.interval = 0;
|
|
if (actual == kSwitch2FastInterval) continue;
|
|
gap_update_connection_parameters(
|
|
handle, kSwitch2FastInterval, kSwitch2FastInterval, 0, 600);
|
|
// Reconcile negotiated state on the configuration timer. Rejected or
|
|
// incomplete requests are retried at most once per second per link.
|
|
request = {handle, kSwitch2FastInterval, now_ms};
|
|
}
|
|
}
|
|
|
|
|
|
// Caller holds the state lock. Only the BTstack context resets parser state.
|
|
void clear_switch2_ingress(BackendSlot& slot) {
|
|
Switch2Ingress& ingress = slot.switch2_ingress;
|
|
__atomic_add_fetch(&g_switch2_ingress_drops, ingress.count, __ATOMIC_RELAXED);
|
|
ingress.head = 0;
|
|
ingress.count = 0;
|
|
++ingress.generation;
|
|
ingress.reset_pending = true;
|
|
}
|
|
|
|
void reset_switch2_outputs(BackendSlot& slot) {
|
|
uni_hid_device_t* targets[] = {slot.device, slot.companion};
|
|
for (uni_hid_device_t* target : targets) {
|
|
if (uni_hid_parser_switch2_is_ble_device(target)) {
|
|
uni_hid_parser_switch2_reset_haptics(target);
|
|
}
|
|
}
|
|
slot.switch2_ingress.reset_pending = false;
|
|
}
|
|
|
|
bool switch2_has_hd(const ControllerRumbleOutput& rumble) {
|
|
return rumble.hd.actuators[0].sample_count != 0 ||
|
|
rumble.hd.actuators[1].sample_count != 0;
|
|
}
|
|
|
|
bool switch2_host_stop(const ControllerRumbleOutput& rumble) {
|
|
if (!switch2_has_hd(rumble)) {
|
|
return (rumble.low_frequency_magnitude | rumble.high_frequency_magnitude) == 0;
|
|
}
|
|
for (const SwitchHapticsActuatorFrame& side : rumble.hd.actuators) {
|
|
if (side.sample_count == 0 || side.sample_count > 3) return false;
|
|
for (uint8_t index = 0; index < side.sample_count; ++index) {
|
|
if (side.samples[index].low_amplitude_q15 != 0 ||
|
|
side.samples[index].high_amplitude_q15 != 0) return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void encode_switch2_side(uni_switch2_haptics_side_t& output,
|
|
const SwitchHapticsActuatorFrame& input) {
|
|
output.count = input.sample_count;
|
|
for (uint8_t index = 0; index < input.sample_count; ++index) {
|
|
const SwitchHapticsSample& sample = input.samples[index];
|
|
uni_switch2_haptics_encode_sample(
|
|
output.samples[index], sample.low_frequency_index,
|
|
sample.high_frequency_index, sample.low_amplitude_q15,
|
|
sample.high_amplitude_q15);
|
|
}
|
|
}
|
|
|
|
uni_switch2_haptics_frame_t switch2_physical_frame(
|
|
const ControllerRumbleOutput& rumble, const uni_hid_device_t* target,
|
|
bool paired) {
|
|
uni_switch2_haptics_frame_t frame{};
|
|
const SwitchHapticsActuatorFrame& left = rumble.hd.actuators[0];
|
|
const SwitchHapticsActuatorFrame& right = rumble.hd.actuators[1];
|
|
const int side = joycon_side(target);
|
|
if (side == 0) {
|
|
encode_switch2_side(frame.sides[0], left);
|
|
encode_switch2_side(frame.sides[1], right);
|
|
} else if (paired) {
|
|
encode_switch2_side(frame.sides[0], side < 0 ? left : right);
|
|
} else {
|
|
// Mono chooses each band's louder source independently. A short side
|
|
// holds its final substep; an absent side contributes no update.
|
|
frame.sides[0].count =
|
|
left.sample_count > right.sample_count ? left.sample_count : right.sample_count;
|
|
for (uint8_t index = 0; index < frame.sides[0].count; ++index) {
|
|
const SwitchHapticsSample* l = left.sample_count == 0 ? nullptr :
|
|
&left.samples[index < left.sample_count ? index : left.sample_count - 1];
|
|
const SwitchHapticsSample* r = right.sample_count == 0 ? nullptr :
|
|
&right.samples[index < right.sample_count ? index : right.sample_count - 1];
|
|
const SwitchHapticsSample* low = !r || (l && l->low_amplitude_q15 >= r->low_amplitude_q15) ? l : r;
|
|
const SwitchHapticsSample* high = !r || (l && l->high_amplitude_q15 >= r->high_amplitude_q15) ? l : r;
|
|
uni_switch2_haptics_encode_sample(
|
|
frame.sides[0].samples[index], low->low_frequency_index,
|
|
high->high_frequency_index, low->low_amplitude_q15,
|
|
high->high_amplitude_q15);
|
|
}
|
|
}
|
|
return frame;
|
|
}
|
|
|
|
void drain_switch2_ingress(uint8_t slot_index, uint32_t now_ms) {
|
|
state_lock_enter();
|
|
BackendSlot& slot = g_slots[slot_index];
|
|
if (!slot.active || !uni_hid_parser_switch2_is_ble_device(slot.device)) {
|
|
state_lock_exit();
|
|
return;
|
|
}
|
|
Switch2Ingress& ingress = slot.switch2_ingress;
|
|
const uint16_t duration_ms = host_rumble_duration_ms();
|
|
const uint8_t host_mode = switch2_host_mode();
|
|
if (ingress.host_mode != host_mode) {
|
|
clear_switch2_ingress(slot);
|
|
ingress.host_mode = host_mode;
|
|
}
|
|
if (ingress.reset_pending) reset_switch2_outputs(slot);
|
|
for (uint8_t budget = 0; budget < kSwitch2IngressCapacity && ingress.count != 0; ++budget) {
|
|
Switch2HostCommand& command = ingress.commands[ingress.head];
|
|
const RumbleEnvelope& envelope = command.envelope;
|
|
const bool hd = switch2_has_hd(envelope.rumble);
|
|
const bool stop = switch2_host_stop(envelope.rumble);
|
|
const bool stale = command.generation != ingress.generation ||
|
|
envelope.connection_generation != slot.connection_generation ||
|
|
envelope.duration_ms != duration_ms ||
|
|
(!stop && (hd || duration_ms != kXInputHostRumbleDurationMs) &&
|
|
static_cast<uint32_t>(now_ms - envelope.received_ms) >= UNI_SWITCH2_HAPTICS_WATCHDOG_MS);
|
|
const bool invalid = envelope.rumble.hd.actuators[0].sample_count > 3 ||
|
|
envelope.rumble.hd.actuators[1].sample_count > 3;
|
|
if (stale || invalid) {
|
|
__atomic_add_fetch(&g_switch2_ingress_drops, 1, __ATOMIC_RELAXED);
|
|
} else {
|
|
uni_hid_device_t* targets[] = {slot.device, slot.companion};
|
|
const uint8_t target_mask = slot.companion == nullptr ? 1 : 3;
|
|
for (uint8_t half = 0; half < 2; ++half) {
|
|
const uint8_t bit = 1u << half;
|
|
if (!(target_mask & bit) || (command.accepted_halves & bit)) continue;
|
|
bool accepted;
|
|
if (hd) {
|
|
const uni_switch2_haptics_frame_t frame =
|
|
switch2_physical_frame(envelope.rumble, targets[half], slot.companion != nullptr);
|
|
if (frame.sides[0].count == 0 && frame.sides[1].count == 0) {
|
|
command.accepted_halves |= bit;
|
|
continue;
|
|
}
|
|
accepted = uni_hid_parser_switch2_queue_haptics(
|
|
targets[half], &frame, envelope.received_ms);
|
|
} else {
|
|
accepted = uni_hid_parser_switch2_queue_rumble(
|
|
targets[half], envelope.rumble.high_frequency_magnitude,
|
|
envelope.rumble.low_frequency_magnitude,
|
|
stop ? 0 : envelope.duration_ms, envelope.received_ms);
|
|
}
|
|
if (accepted) {
|
|
command.accepted_halves |= bit;
|
|
__atomic_add_fetch(&g_rumble_dispatches, 1, __ATOMIC_RELAXED);
|
|
}
|
|
}
|
|
if (command.accepted_halves != target_mask) break;
|
|
}
|
|
ingress.head = (ingress.head + 1u) % kSwitch2IngressCapacity;
|
|
--ingress.count;
|
|
}
|
|
state_lock_exit();
|
|
}
|
|
|
|
bool addresses_equal(const bd_addr_t first, const bd_addr_t second) {
|
|
return memcmp(first, second, sizeof(bd_addr_t)) == 0;
|
|
}
|
|
|
|
BleIdentityMapping* find_ble_identity_mapping(
|
|
hci_con_handle_t connection_handle,
|
|
const bd_addr_t connection_address) {
|
|
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
|
|
if (mapping.used &&
|
|
mapping.connection_handle == connection_handle &&
|
|
addresses_equal(mapping.connection_address,
|
|
connection_address)) {
|
|
return &mapping;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
BleIdentityMapping* find_ble_identity_mapping_for_handle(
|
|
hci_con_handle_t connection_handle) {
|
|
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
|
|
if (mapping.used &&
|
|
mapping.connection_handle == connection_handle) {
|
|
return &mapping;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
BleIdentityMapping* reserve_ble_identity_mapping(
|
|
hci_con_handle_t connection_handle) {
|
|
BleIdentityMapping* available = nullptr;
|
|
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
|
|
if (mapping.used &&
|
|
mapping.connection_handle == connection_handle) {
|
|
return &mapping;
|
|
}
|
|
if (!mapping.used && available == nullptr) {
|
|
available = &mapping;
|
|
}
|
|
}
|
|
return available;
|
|
}
|
|
|
|
ControllerIdentity make_ble_identity(
|
|
const BleIdentityMapping& mapping, const uni_hid_device_t* device) {
|
|
ControllerIdentity identity{};
|
|
identity.stable = true;
|
|
identity.transport = ControllerTransport::kBle;
|
|
identity.address_type = mapping.identity_address_type;
|
|
memcpy(identity.address, mapping.identity_address,
|
|
sizeof(identity.address));
|
|
identity.vendor_id = device->vendor_id;
|
|
identity.product_id = device->product_id;
|
|
return identity;
|
|
}
|
|
|
|
ControllerIdentity identity_for_device(const uni_hid_device_t* device) {
|
|
if (device == nullptr) {
|
|
return controller_identity_global();
|
|
}
|
|
switch (gap_get_connection_type(device->conn.handle)) {
|
|
case GAP_CONNECTION_ACL: {
|
|
ControllerIdentity identity{};
|
|
identity.stable = true;
|
|
identity.transport = ControllerTransport::kClassic;
|
|
identity.address_type = BD_ADDR_TYPE_UNKNOWN;
|
|
memcpy(identity.address, device->conn.btaddr,
|
|
sizeof(identity.address));
|
|
identity.vendor_id = device->vendor_id;
|
|
identity.product_id = device->product_id;
|
|
return identity;
|
|
}
|
|
case GAP_CONNECTION_LE: {
|
|
const BleIdentityMapping* mapping = find_ble_identity_mapping(
|
|
device->conn.handle, device->conn.btaddr);
|
|
if (mapping != nullptr) {
|
|
return make_ble_identity(*mapping, device);
|
|
}
|
|
uint8_t address_type = BD_ADDR_TYPE_UNKNOWN;
|
|
if (uni_hid_parser_switch2_identity_address_type(
|
|
device, &address_type)) {
|
|
BleIdentityMapping proprietary{};
|
|
proprietary.identity_address_type = address_type;
|
|
memcpy(proprietary.identity_address, device->conn.btaddr,
|
|
sizeof(proprietary.identity_address));
|
|
return make_ble_identity(proprietary, device);
|
|
}
|
|
break;
|
|
}
|
|
case GAP_CONNECTION_INVALID:
|
|
case GAP_CONNECTION_SCO:
|
|
break;
|
|
}
|
|
return controller_identity_global();
|
|
}
|
|
|
|
void publish_ble_identity(const BleIdentityMapping& mapping) {
|
|
ControllerIdentity observed_identity{};
|
|
bool observe_identity = false;
|
|
bool joycon_identity_changed = false;
|
|
state_lock_enter();
|
|
for (BackendSlot& slot : g_slots) {
|
|
if (slot.device != nullptr && slot.companion == nullptr &&
|
|
gap_get_connection_type(slot.device->conn.handle) ==
|
|
GAP_CONNECTION_LE &&
|
|
slot.device->conn.handle == mapping.connection_handle &&
|
|
addresses_equal(slot.device->conn.btaddr,
|
|
mapping.connection_address)) {
|
|
const ControllerIdentity identity = make_ble_identity(mapping, slot.device);
|
|
joycon_identity_changed = slot.active && joycon_side(slot.device) != 0 &&
|
|
!controller_identity_equal(slot.identity, identity);
|
|
slot.identity = identity;
|
|
if (slot.active) {
|
|
observed_identity = slot.identity;
|
|
observe_identity = true;
|
|
}
|
|
}
|
|
}
|
|
state_lock_exit();
|
|
if (observe_identity) {
|
|
profile_service_observe_identity_on_storage_core(
|
|
observed_identity);
|
|
if (joycon_identity_changed && g_joycon_mode == JoyConMode::kPaired) {
|
|
g_joycon_reconcile_requested = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void record_ble_identity(hci_con_handle_t connection_handle,
|
|
const bd_addr_t connection_address,
|
|
uint8_t identity_address_type,
|
|
const bd_addr_t identity_address) {
|
|
BleIdentityMapping* mapping =
|
|
reserve_ble_identity_mapping(connection_handle);
|
|
if (mapping == nullptr) {
|
|
return;
|
|
}
|
|
*mapping = {};
|
|
mapping->used = true;
|
|
mapping->connection_handle = connection_handle;
|
|
memcpy(mapping->connection_address, connection_address,
|
|
sizeof(mapping->connection_address));
|
|
mapping->identity_address_type = identity_address_type;
|
|
memcpy(mapping->identity_address, identity_address,
|
|
sizeof(mapping->identity_address));
|
|
publish_ble_identity(*mapping);
|
|
}
|
|
|
|
void clear_ble_identity_for_handle(hci_con_handle_t connection_handle) {
|
|
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
|
|
if (mapping.used &&
|
|
mapping.connection_handle == connection_handle) {
|
|
mapping = {};
|
|
}
|
|
}
|
|
|
|
state_lock_enter();
|
|
for (BackendSlot& slot : g_slots) {
|
|
if (slot.device != nullptr && slot.companion == nullptr &&
|
|
gap_get_connection_type(slot.device->conn.handle) ==
|
|
GAP_CONNECTION_LE &&
|
|
slot.device->conn.handle == connection_handle) {
|
|
slot.identity = controller_identity_global();
|
|
}
|
|
}
|
|
state_lock_exit();
|
|
}
|
|
|
|
void clear_ble_identity_for_device(const uni_hid_device_t* device) {
|
|
if (device == nullptr) {
|
|
return;
|
|
}
|
|
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
|
|
if (mapping.used &&
|
|
mapping.connection_handle == device->conn.handle &&
|
|
addresses_equal(mapping.connection_address,
|
|
device->conn.btaddr)) {
|
|
mapping = {};
|
|
}
|
|
}
|
|
}
|
|
|
|
void connection_address_for_handle(hci_con_handle_t connection_handle,
|
|
const bd_addr_t fallback,
|
|
bd_addr_t output) {
|
|
const uni_hid_device_t* device =
|
|
uni_hid_device_get_instance_for_connection_handle(
|
|
connection_handle);
|
|
if (device != nullptr) {
|
|
memcpy(output, device->conn.btaddr, sizeof(bd_addr_t));
|
|
return;
|
|
}
|
|
const BleIdentityMapping* mapping =
|
|
find_ble_identity_mapping_for_handle(connection_handle);
|
|
if (mapping != nullptr) {
|
|
memcpy(output, mapping->connection_address, sizeof(bd_addr_t));
|
|
return;
|
|
}
|
|
memcpy(output, fallback, sizeof(bd_addr_t));
|
|
}
|
|
|
|
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<uint8_t>(1u << slot_index));
|
|
}
|
|
}
|
|
bool valid_confirmation_policy(
|
|
ControllerProfileConfirmationPolicy policy) {
|
|
return static_cast<uint8_t>(policy) <=
|
|
static_cast<uint8_t>(
|
|
ControllerProfileConfirmationPolicy::kRumbleAndLed);
|
|
}
|
|
|
|
void apply_profile_lighting(
|
|
uint8_t active_profile_number, uni_hid_device_t* device) {
|
|
if (device == nullptr || active_profile_number == 0 ||
|
|
active_profile_number > CONTROLLER_PROFILE_COUNT) {
|
|
return;
|
|
}
|
|
if (device->report_parser.set_lightbar_color != nullptr) {
|
|
const SwitchRgbColor color =
|
|
kProfileLightbarPalette[active_profile_number - 1u];
|
|
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<uint8_t>(
|
|
(1u << active_profile_number) - 1u));
|
|
}
|
|
}
|
|
|
|
bool lighting_target_is_current(
|
|
uint8_t slot_index, uint32_t connection_generation,
|
|
const uni_hid_device_t* device) {
|
|
state_lock_enter();
|
|
const bool current =
|
|
device != nullptr && slot_index < kSlotCount &&
|
|
g_slots[slot_index].active &&
|
|
(g_slots[slot_index].device == device ||
|
|
g_slots[slot_index].companion == device) &&
|
|
g_slots[slot_index].connection_generation ==
|
|
connection_generation;
|
|
state_lock_exit();
|
|
return current;
|
|
}
|
|
|
|
Bluepad32ControllerLayout controller_layout(const BackendSlot& slot) {
|
|
if (!slot.active || slot.device == nullptr)
|
|
return Bluepad32ControllerLayout::kUnspecified;
|
|
const int side = joycon_side(slot.device);
|
|
if (side != 0) {
|
|
return slot.companion != nullptr
|
|
? Bluepad32ControllerLayout::kJoyCon2MergedPair
|
|
: side < 0 ? Bluepad32ControllerLayout::kJoyCon2LeftSolo
|
|
: Bluepad32ControllerLayout::kJoyCon2RightSolo;
|
|
}
|
|
switch (slot.device->controller_subtype) {
|
|
case CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL:
|
|
case CONTROLLER_SUBTYPE_WIIMOTE_ACCEL:
|
|
return Bluepad32ControllerLayout::kWiiHorizontal;
|
|
case CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL:
|
|
return Bluepad32ControllerLayout::kWiiVertical;
|
|
case CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK:
|
|
case CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL:
|
|
return Bluepad32ControllerLayout::kWiiNunchuk;
|
|
default:
|
|
return Bluepad32ControllerLayout::kUnspecified;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
ConnectionStatus compute_connection_status() {
|
|
state_lock_enter();
|
|
bool all_ready = true;
|
|
bool any_connecting = false;
|
|
unsigned physical_count = 0;
|
|
for (const BackendSlot& slot : g_slots) {
|
|
const bool has_device = slot.device != nullptr;
|
|
physical_count += has_device;
|
|
physical_count += slot.companion != nullptr;
|
|
all_ready = all_ready && (!has_device || slot.active);
|
|
any_connecting = any_connecting || (!slot.active && has_device);
|
|
}
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
for (const auto* pending : g_native_pending_devices) {
|
|
if (pending != nullptr) {
|
|
++physical_count;
|
|
all_ready = false;
|
|
any_connecting = true;
|
|
}
|
|
}
|
|
#endif
|
|
state_lock_exit();
|
|
|
|
if (all_ready && physical_count == kSlotCount) {
|
|
return ConnectionStatus::Ready;
|
|
}
|
|
return any_connecting ? ConnectionStatus::Connecting
|
|
: ConnectionStatus::Scanning;
|
|
}
|
|
|
|
|
|
|
|
void publish_device_state(uint8_t slot, uni_hid_device_t* device,
|
|
uint16_t pre_hotkey_button_mask,
|
|
const ControllerState& state) {
|
|
state_lock_enter();
|
|
BackendSlot& target = g_slots[slot];
|
|
if (target.active && target.device == device) {
|
|
target.state = state;
|
|
target.pre_hotkey_button_mask = pre_hotkey_button_mask;
|
|
#ifdef SWITCH_PICO_WII_IR_GYRO
|
|
if (device->controller_type == CONTROLLER_TYPE_WiiController) {
|
|
const ControllerMotionSample sample =
|
|
state.motion_sample_count != 0
|
|
? state.motion_samples[0] : ControllerMotionSample{};
|
|
wii_ir_gyro_update_motion(
|
|
slot, target.connection_generation, target.motion_enabled, sample);
|
|
if (!target.motion_enabled) {
|
|
target.state.motion_sample_count = 0;
|
|
}
|
|
}
|
|
#endif
|
|
++target.state_generation;
|
|
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
|
if (is_selected_wii(target)) {
|
|
const WiiMotionIngress& motion = target.wii_motion;
|
|
g_wii_snapshot.slot = slot;
|
|
g_wii_snapshot.controller = {
|
|
target.active, target.connection_generation, target.identity,
|
|
target.pre_hotkey_button_mask, target.state,
|
|
target.accelerometer, target.nunchuk_accelerometer};
|
|
g_wii_snapshot.layout = controller_layout(target);
|
|
g_wii_snapshot.state_generation = target.state_generation;
|
|
g_wii_snapshot.received_us = motion.received_us;
|
|
g_wii_snapshot.battery = device->controller.battery;
|
|
g_wii_snapshot.accel_valid = motion.accel_valid;
|
|
g_wii_snapshot.gyro_valid = motion.gyro_valid;
|
|
g_wii_snapshot.accel_sequence = motion.accel_sequence;
|
|
g_wii_snapshot.gyro_sequence = motion.gyro_sequence;
|
|
g_wii_snapshot.accel_received_us = motion.accel_received_us;
|
|
g_wii_snapshot.gyro_received_us = motion.gyro_received_us;
|
|
memcpy(g_wii_snapshot.accel_q13, motion.accel_q13, sizeof(motion.accel_q13));
|
|
memcpy(g_wii_snapshot.gyro_q10, motion.gyro_q10, sizeof(motion.gyro_q10));
|
|
}
|
|
#endif
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
if (slot == g_native_slot && target.native_motion.has_report &&
|
|
target.connection_generation == g_native_generation) {
|
|
const NativeGamepadIngress& motion = target.native_motion;
|
|
g_native_snapshot.slot = slot;
|
|
g_native_snapshot.controller = {
|
|
target.active, target.connection_generation, target.identity,
|
|
target.pre_hotkey_button_mask, target.state,
|
|
target.accelerometer, target.nunchuk_accelerometer};
|
|
g_native_snapshot.state_generation = target.state_generation;
|
|
g_native_snapshot.received_us = motion.received_us;
|
|
g_native_snapshot.battery = device->controller.battery;
|
|
g_native_snapshot.track_stationary_bias =
|
|
device->controller_type == CONTROLLER_TYPE_WiiController;
|
|
g_native_snapshot.accel_valid = motion.accel_valid;
|
|
g_native_snapshot.gyro_valid = motion.gyro_valid;
|
|
g_native_snapshot.accel_sequence = motion.accel_sequence;
|
|
g_native_snapshot.gyro_sequence = motion.gyro_sequence;
|
|
g_native_snapshot.accel_received_us = motion.accel_received_us;
|
|
g_native_snapshot.gyro_received_us = motion.gyro_received_us;
|
|
memcpy(g_native_snapshot.accel_q13, motion.accel_q13, sizeof(motion.accel_q13));
|
|
memcpy(g_native_snapshot.gyro_q10, motion.gyro_q10, sizeof(motion.gyro_q10));
|
|
}
|
|
#endif
|
|
g_macro_capture.observe(slot, target.connection_generation,
|
|
time_us_32(), target.state);
|
|
}
|
|
state_lock_exit();
|
|
}
|
|
|
|
void publish_all_neutral() {
|
|
state_lock_enter();
|
|
#ifdef SWITCH_PICO_WII_IR
|
|
wii_ir_pointer_reset();
|
|
#endif
|
|
for (BackendSlot& slot : g_slots) {
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
retire_native_slot(static_cast<uint8_t>(&slot - g_slots));
|
|
#endif
|
|
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
|
retire_wii_slot(static_cast<uint8_t>(&slot - g_slots));
|
|
retire_wii_motion(slot.wii_motion);
|
|
#endif
|
|
clear_switch2_ingress(slot);
|
|
reset_switch2_outputs(slot);
|
|
slot.state = make_neutral_state();
|
|
slot.accelerometer = {};
|
|
slot.nunchuk_accelerometer = {};
|
|
slot.pre_hotkey_button_mask = 0;
|
|
slot.identity = controller_identity_global();
|
|
slot.device = nullptr;
|
|
slot.companion = nullptr;
|
|
slot.gamepad = {};
|
|
slot.companion_gamepad = {};
|
|
slot.extra_buttons = 0;
|
|
slot.companion_extra_buttons = 0;
|
|
slot.active = false;
|
|
slot.wii_orientation_pending = false;
|
|
slot.pending_wii_orientation = {};
|
|
#ifdef SWITCH_PICO_WII_IR_GYRO
|
|
slot.wii_aim = {};
|
|
#endif
|
|
slot.rumble_pending = false;
|
|
slot.retained_host_rumble_valid = false;
|
|
slot.retained_host_rumble = {};
|
|
slot.feedback_pending = false;
|
|
slot.feedback_until_ms = 0;
|
|
slot.pending_profile_feedback_count = 0;
|
|
for (ProfileFeedbackEnvelope& feedback :
|
|
slot.pending_profile_feedback) {
|
|
feedback = {};
|
|
}
|
|
slot.profile_feedback = {};
|
|
++slot.state_generation;
|
|
++slot.connection_generation;
|
|
}
|
|
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
|
g_joycon_gestures[index] = {};
|
|
g_joycon_overrides[index] = {};
|
|
g_joycon_pair_hints[index] = {};
|
|
}
|
|
state_lock_exit();
|
|
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
|
|
mapping = {};
|
|
}
|
|
g_connection_status = ConnectionStatus::Initializing;
|
|
stop_background_scan();
|
|
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<int16_t>(
|
|
(static_cast<int64_t>(value) * -INT16_MIN) /
|
|
-kAxisMinimum);
|
|
}
|
|
return static_cast<int16_t>(
|
|
(static_cast<int64_t>(value) * INT16_MAX) /
|
|
kAxisMaximum);
|
|
}
|
|
|
|
constexpr uint16_t scale_trigger(int32_t value) {
|
|
if (value <= 0) {
|
|
return 0;
|
|
}
|
|
if (value >= kTriggerFullScaleMinimum) {
|
|
return UINT16_MAX;
|
|
}
|
|
return static_cast<uint16_t>(
|
|
(static_cast<int64_t>(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<int16_t>(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(1016) == 65086);
|
|
static_assert(scale_trigger(1020) == UINT16_MAX);
|
|
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;
|
|
}
|
|
constexpr uint16_t logical_button_bit(
|
|
ControllerProfileLogicalButton button) {
|
|
return static_cast<uint16_t>(
|
|
1u << static_cast<uint8_t>(button));
|
|
}
|
|
bool wake_chord_rising_edge(uint8_t slot, uni_hid_device_t* device,
|
|
uint16_t button_mask) {
|
|
const uint16_t chord =
|
|
logical_button_bit(ControllerProfileLogicalButton::kLeftShoulder) |
|
|
logical_button_bit(ControllerProfileLogicalButton::kRightShoulder) |
|
|
logical_button_bit(ControllerProfileLogicalButton::kSystem);
|
|
state_lock_enter();
|
|
const BackendSlot& previous = g_slots[slot];
|
|
const bool rising =
|
|
previous.active && previous.device == device &&
|
|
(button_mask & chord) == chord &&
|
|
(previous.pre_hotkey_button_mask & chord) != chord;
|
|
state_lock_exit();
|
|
return rising;
|
|
}
|
|
|
|
constexpr uint16_t logical_button_mask(
|
|
uint32_t dpad, uint32_t buttons, uint32_t misc_buttons) {
|
|
return static_cast<uint16_t>(
|
|
((buttons & BUTTON_A) != 0
|
|
? logical_button_bit(
|
|
ControllerProfileLogicalButton::kSouth)
|
|
: 0u) |
|
|
((buttons & BUTTON_B) != 0
|
|
? logical_button_bit(
|
|
ControllerProfileLogicalButton::kEast)
|
|
: 0u) |
|
|
((buttons & BUTTON_X) != 0
|
|
? logical_button_bit(
|
|
ControllerProfileLogicalButton::kWest)
|
|
: 0u) |
|
|
((buttons & BUTTON_Y) != 0
|
|
? logical_button_bit(
|
|
ControllerProfileLogicalButton::kNorth)
|
|
: 0u) |
|
|
((buttons & BUTTON_SHOULDER_L) != 0
|
|
? logical_button_bit(
|
|
ControllerProfileLogicalButton::kLeftShoulder)
|
|
: 0u) |
|
|
((buttons & BUTTON_SHOULDER_R) != 0
|
|
? logical_button_bit(
|
|
ControllerProfileLogicalButton::kRightShoulder)
|
|
: 0u) |
|
|
((misc_buttons & MISC_BUTTON_SELECT) != 0
|
|
? logical_button_bit(
|
|
ControllerProfileLogicalButton::kSelect)
|
|
: 0u) |
|
|
((misc_buttons & MISC_BUTTON_START) != 0
|
|
? logical_button_bit(
|
|
ControllerProfileLogicalButton::kStart)
|
|
: 0u) |
|
|
((misc_buttons & MISC_BUTTON_SYSTEM) != 0
|
|
? logical_button_bit(
|
|
ControllerProfileLogicalButton::kSystem)
|
|
: 0u) |
|
|
((misc_buttons & MISC_BUTTON_CAPTURE) != 0
|
|
? logical_button_bit(
|
|
ControllerProfileLogicalButton::kCapture)
|
|
: 0u) |
|
|
((buttons & BUTTON_THUMB_L) != 0
|
|
? logical_button_bit(
|
|
ControllerProfileLogicalButton::kLeftStick)
|
|
: 0u) |
|
|
((buttons & BUTTON_THUMB_R) != 0
|
|
? logical_button_bit(
|
|
ControllerProfileLogicalButton::kRightStick)
|
|
: 0u) |
|
|
((dpad & DPAD_UP) != 0
|
|
? logical_button_bit(
|
|
ControllerProfileLogicalButton::kDpadUp)
|
|
: 0u) |
|
|
((dpad & DPAD_DOWN) != 0
|
|
? logical_button_bit(
|
|
ControllerProfileLogicalButton::kDpadDown)
|
|
: 0u) |
|
|
((dpad & DPAD_LEFT) != 0
|
|
? logical_button_bit(
|
|
ControllerProfileLogicalButton::kDpadLeft)
|
|
: 0u) |
|
|
((dpad & DPAD_RIGHT) != 0
|
|
? logical_button_bit(
|
|
ControllerProfileLogicalButton::kDpadRight)
|
|
: 0u));
|
|
}
|
|
|
|
|
|
uint16_t logical_button_mask(const uni_gamepad_t& gamepad) {
|
|
return logical_button_mask(
|
|
gamepad.dpad, gamepad.buttons, gamepad.misc_buttons);
|
|
}
|
|
|
|
ControllerState map_gamepad(const uni_gamepad_t& gamepad,
|
|
bool motion_enabled,
|
|
uint16_t button_mask) {
|
|
ControllerState state = make_neutral_state();
|
|
|
|
state.dpad_up =
|
|
(button_mask & logical_button_bit(
|
|
ControllerProfileLogicalButton::kDpadUp)) != 0;
|
|
state.dpad_down =
|
|
(button_mask & logical_button_bit(
|
|
ControllerProfileLogicalButton::kDpadDown)) != 0;
|
|
state.dpad_left =
|
|
(button_mask & logical_button_bit(
|
|
ControllerProfileLogicalButton::kDpadLeft)) != 0;
|
|
state.dpad_right =
|
|
(button_mask & logical_button_bit(
|
|
ControllerProfileLogicalButton::kDpadRight)) != 0;
|
|
|
|
// Bluepad32's A/B/X/Y are positional: south/east/west/north. Persistent
|
|
// profile mappings are the only button remapping layer.
|
|
state.button_south =
|
|
(button_mask & logical_button_bit(
|
|
ControllerProfileLogicalButton::kSouth)) != 0;
|
|
state.button_east =
|
|
(button_mask & logical_button_bit(
|
|
ControllerProfileLogicalButton::kEast)) != 0;
|
|
state.button_west =
|
|
(button_mask & logical_button_bit(
|
|
ControllerProfileLogicalButton::kWest)) != 0;
|
|
state.button_north =
|
|
(button_mask & logical_button_bit(
|
|
ControllerProfileLogicalButton::kNorth)) != 0;
|
|
state.button_left_shoulder =
|
|
(button_mask & logical_button_bit(
|
|
ControllerProfileLogicalButton::kLeftShoulder)) != 0;
|
|
state.button_right_shoulder =
|
|
(button_mask & logical_button_bit(
|
|
ControllerProfileLogicalButton::kRightShoulder)) != 0;
|
|
state.left_trigger = scale_trigger(gamepad.brake);
|
|
if (gamepad.brake == 0 &&
|
|
(gamepad.buttons & BUTTON_TRIGGER_L) != 0) {
|
|
state.left_trigger = UINT16_MAX;
|
|
}
|
|
state.right_trigger = scale_trigger(gamepad.throttle);
|
|
if (gamepad.throttle == 0 &&
|
|
(gamepad.buttons & BUTTON_TRIGGER_R) != 0) {
|
|
state.right_trigger = UINT16_MAX;
|
|
}
|
|
state.button_left_stick =
|
|
(button_mask & logical_button_bit(
|
|
ControllerProfileLogicalButton::kLeftStick)) != 0;
|
|
state.button_right_stick =
|
|
(button_mask & logical_button_bit(
|
|
ControllerProfileLogicalButton::kRightStick)) != 0;
|
|
|
|
state.button_select =
|
|
(button_mask & logical_button_bit(
|
|
ControllerProfileLogicalButton::kSelect)) != 0;
|
|
state.button_start =
|
|
(button_mask & logical_button_bit(
|
|
ControllerProfileLogicalButton::kStart)) != 0;
|
|
state.button_system =
|
|
(button_mask & logical_button_bit(
|
|
ControllerProfileLogicalButton::kSystem)) != 0;
|
|
state.button_capture =
|
|
(button_mask & logical_button_bit(
|
|
ControllerProfileLogicalButton::kCapture)) != 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<int64_t>(gamepad.accel[2]));
|
|
sample.accel_y = convert_accel(-static_cast<int64_t>(gamepad.accel[0]));
|
|
sample.accel_z = convert_accel(gamepad.accel[1]);
|
|
sample.gyro_x = convert_gyro(-static_cast<int64_t>(gamepad.gyro[2]));
|
|
sample.gyro_y = convert_gyro(-static_cast<int64_t>(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;
|
|
}
|
|
int32_t negate_motion_axis(int32_t value) {
|
|
return value == INT32_MIN ? INT32_MAX : -value;
|
|
}
|
|
|
|
void rotate_solo_joycon(uni_gamepad_t& gamepad, int side,
|
|
uint8_t extras) {
|
|
const uint32_t buttons = gamepad.buttons;
|
|
if (side < 0) {
|
|
const int32_t x = gamepad.axis_x;
|
|
gamepad.axis_x = clamp_axis(gamepad.axis_y);
|
|
gamepad.axis_y = clamp_axis(-clamp_axis(x));
|
|
gamepad.buttons &= ~(BUTTON_A | BUTTON_B | BUTTON_X | BUTTON_Y);
|
|
gamepad.buttons |=
|
|
((gamepad.dpad & DPAD_LEFT) ? uint32_t{BUTTON_A} : 0u) |
|
|
((gamepad.dpad & DPAD_DOWN) ? uint32_t{BUTTON_B} : 0u) |
|
|
((gamepad.dpad & DPAD_UP) ? uint32_t{BUTTON_X} : 0u) |
|
|
((gamepad.dpad & DPAD_RIGHT) ? uint32_t{BUTTON_Y} : 0u) |
|
|
((extras & UNI_SW2_BUTTON_LEFT_SL) ? uint32_t{BUTTON_SHOULDER_L} : 0u) |
|
|
((extras & UNI_SW2_BUTTON_LEFT_SR) ? uint32_t{BUTTON_SHOULDER_R} : 0u);
|
|
} else {
|
|
gamepad.axis_x = clamp_axis(-clamp_axis(gamepad.axis_ry));
|
|
gamepad.axis_y = clamp_axis(gamepad.axis_rx);
|
|
gamepad.buttons &=
|
|
~(BUTTON_A | BUTTON_B | BUTTON_X | BUTTON_Y | BUTTON_THUMB_R);
|
|
gamepad.buttons |=
|
|
((buttons & BUTTON_B) ? uint32_t{BUTTON_A} : 0u) |
|
|
((buttons & BUTTON_Y) ? uint32_t{BUTTON_B} : 0u) |
|
|
((buttons & BUTTON_A) ? uint32_t{BUTTON_X} : 0u) |
|
|
((buttons & BUTTON_X) ? uint32_t{BUTTON_Y} : 0u) |
|
|
((buttons & BUTTON_THUMB_R) ? uint32_t{BUTTON_THUMB_L} : 0u) |
|
|
((extras & UNI_SW2_BUTTON_RIGHT_SL) ? uint32_t{BUTTON_SHOULDER_L} : 0u) |
|
|
((extras & UNI_SW2_BUTTON_RIGHT_SR) ? uint32_t{BUTTON_SHOULDER_R} : 0u);
|
|
}
|
|
gamepad.dpad = 0;
|
|
gamepad.axis_rx = 0;
|
|
gamepad.axis_ry = 0;
|
|
int32_t* motion_axes[] = {gamepad.accel, gamepad.gyro};
|
|
for (int32_t* axes : motion_axes) {
|
|
const int32_t x = axes[0];
|
|
axes[0] = side < 0 ? negate_motion_axis(axes[1]) : axes[1];
|
|
axes[1] = side < 0 ? x : negate_motion_axis(x);
|
|
}
|
|
}
|
|
#ifdef SWITCH_PICO_WII_IR_GYRO
|
|
uint32_t wii_aim_chord_button_mask(const uni_hid_device_t* device) {
|
|
if (device == nullptr ||
|
|
device->controller_type != CONTROLLER_TYPE_WiiController) {
|
|
return 0;
|
|
}
|
|
switch (device->controller_subtype) {
|
|
case CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL:
|
|
case CONTROLLER_SUBTYPE_WIIMOTE_ACCEL:
|
|
return BUTTON_A | BUTTON_B;
|
|
case CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL:
|
|
return BUTTON_X | BUTTON_Y;
|
|
case CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK:
|
|
case CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL:
|
|
return BUTTON_SHOULDER_L | BUTTON_SHOULDER_R;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
uni_gamepad_t logical_gamepad(const BackendSlot& slot) {
|
|
uni_gamepad_t gamepad = slot.gamepad;
|
|
mask_joycon_gesture(gamepad, slot.device);
|
|
#ifdef SWITCH_PICO_WII_IR_GYRO
|
|
if (slot.wii_aim.masked) {
|
|
gamepad.buttons &= ~wii_aim_chord_button_mask(slot.device);
|
|
}
|
|
if (slot.wii_aim.reposition_masked) {
|
|
gamepad.buttons &= ~(BUTTON_X | BUTTON_SHOULDER_L);
|
|
}
|
|
#endif
|
|
if (slot.companion != nullptr) {
|
|
const uni_gamepad_t& right = slot.companion_gamepad;
|
|
gamepad.dpad |= right.dpad;
|
|
const bool masked = joycon_gesture_masked(slot.companion);
|
|
gamepad.buttons |=
|
|
right.buttons & ~(masked ? uint32_t{BUTTON_TRIGGER_R} : 0u);
|
|
gamepad.misc_buttons |=
|
|
right.misc_buttons & ~(masked ? uint32_t{MISC_BUTTON_START} : 0u);
|
|
gamepad.axis_rx = right.axis_rx;
|
|
gamepad.axis_ry = right.axis_ry;
|
|
gamepad.throttle = masked ? 0 : right.throttle;
|
|
// The right half is the sole aim source. A left report must not
|
|
// republish an already consumed right-hand motion sample.
|
|
memcpy(gamepad.accel, right.accel, sizeof(gamepad.accel));
|
|
memcpy(gamepad.gyro, right.gyro, sizeof(gamepad.gyro));
|
|
} else {
|
|
const int side = joycon_side(slot.device);
|
|
if (side != 0) {
|
|
rotate_solo_joycon(gamepad, side, slot.extra_buttons);
|
|
}
|
|
}
|
|
return gamepad;
|
|
}
|
|
|
|
void refresh_topology_input(BackendSlot& slot) {
|
|
const uni_gamepad_t gamepad = logical_gamepad(slot);
|
|
slot.pre_hotkey_button_mask = logical_button_mask(gamepad);
|
|
// Topology changes release the lost half immediately; motion stays neutral
|
|
// until a fresh report from the newly selected source arrives.
|
|
slot.state = map_gamepad(gamepad, false, slot.pre_hotkey_button_mask);
|
|
slot.state.extra_buttons =
|
|
slot.extra_buttons | slot.companion_extra_buttons;
|
|
}
|
|
struct HotkeyDecision {
|
|
bool motion_enabled;
|
|
};
|
|
|
|
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};
|
|
__atomic_add_fetch(&g_local_feedback_requests, 1, __ATOMIC_RELAXED);
|
|
}
|
|
|
|
void queue_profile_feedback(BackendSlot& slot,
|
|
const ProfileFeedbackEnvelope& feedback) {
|
|
if (slot.pending_profile_feedback_count < kProfileFeedbackQueueCapacity) {
|
|
slot.pending_profile_feedback[
|
|
slot.pending_profile_feedback_count++] = feedback;
|
|
} else {
|
|
slot.pending_profile_feedback[kProfileFeedbackQueueCapacity - 1u] =
|
|
feedback;
|
|
}
|
|
}
|
|
|
|
#ifdef SWITCH_PICO_WII_IR_GYRO
|
|
void observe_wii_aim_chord(BackendSlot& slot, uni_hid_device_t* device,
|
|
const uni_gamepad_t& gamepad,
|
|
const uni_wii_ir_snapshot_t* infrared,
|
|
uint32_t now_us) {
|
|
const uint32_t mapped_buttons = wii_aim_chord_button_mask(device);
|
|
if (mapped_buttons == 0) return;
|
|
WiiAimSource& aim = slot.wii_aim;
|
|
const uint8_t slot_index = static_cast<uint8_t>(&slot - g_slots);
|
|
if (mapped_buttons == (BUTTON_SHOULDER_L | BUTTON_SHOULDER_R)) {
|
|
const uint32_t controls = BUTTON_X | BUTTON_SHOULDER_L; // Nunchuk C + 1.
|
|
const uint32_t pressed = gamepad.buttons & controls;
|
|
if (aim.infrared && pressed == controls) {
|
|
aim.reposition_masked = true;
|
|
} else if (pressed == 0) {
|
|
aim.reposition_masked = false;
|
|
}
|
|
} else {
|
|
aim.reposition_masked = false;
|
|
}
|
|
|
|
// Expire before accepting a returning packet. Cached snapshots cannot
|
|
// extend or complete a hold, and a gap requires a full release to retry.
|
|
// A transport gap cancels the gesture, not the user's selected source.
|
|
// The pointer's freshness guard stops IR output without falling back.
|
|
if (aim.have_sequence &&
|
|
now_us - aim.last_report_us >= kWiiAimChordFreshUs) {
|
|
aim.holding = false;
|
|
aim.latched = aim.latched || aim.masked;
|
|
}
|
|
const uint32_t pressed_buttons = gamepad.buttons & mapped_buttons;
|
|
if (pressed_buttons == mapped_buttons) {
|
|
aim.masked = true;
|
|
} else {
|
|
aim.holding = false;
|
|
if (pressed_buttons == 0) {
|
|
aim.masked = false;
|
|
aim.latched = false;
|
|
} else {
|
|
aim.latched = aim.latched || aim.masked;
|
|
}
|
|
}
|
|
if (infrared == nullptr ||
|
|
(aim.have_sequence && infrared->sequence == aim.sequence) ||
|
|
(!aim.have_sequence && infrared->sequence == 0)) {
|
|
return;
|
|
}
|
|
aim.have_sequence = true;
|
|
aim.sequence = infrared->sequence;
|
|
aim.last_report_us = now_us;
|
|
const uint16_t buttons = infrared->buttons & kWiiAimChordButtons;
|
|
if (buttons != kWiiAimChordButtons || pressed_buttons != mapped_buttons) {
|
|
aim.holding = false;
|
|
aim.latched = aim.latched || aim.masked;
|
|
return;
|
|
}
|
|
if (aim.latched) return;
|
|
if (!aim.holding) {
|
|
aim.started_us = now_us;
|
|
aim.holding = true;
|
|
return;
|
|
}
|
|
if (now_us - aim.started_us < kWiiAimChordHoldUs) return;
|
|
aim.latched = true;
|
|
if (!wii_ir_gyro_select(
|
|
slot_index, slot.connection_generation, !aim.infrared)) {
|
|
return;
|
|
}
|
|
aim.infrared = !aim.infrared;
|
|
queue_profile_feedback(
|
|
slot, {slot.connection_generation,
|
|
static_cast<uint8_t>(aim.infrared ? 2 : 1),
|
|
ControllerProfileConfirmationPolicy::kRumble});
|
|
__atomic_add_fetch(&g_local_feedback_requests, 1, __ATOMIC_RELAXED);
|
|
}
|
|
#endif
|
|
|
|
void reset_slot_hotkeys(BackendSlot& slot) {
|
|
g_macro_capture.disconnect(static_cast<uint8_t>(&slot - g_slots),
|
|
slot.connection_generation, time_us_32());
|
|
slot.wii_orientation_pending = false;
|
|
slot.pending_wii_orientation = {};
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
retire_native_slot(static_cast<uint8_t>(&slot - g_slots));
|
|
#endif
|
|
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
|
retire_wii_slot(static_cast<uint8_t>(&slot - g_slots));
|
|
retire_wii_motion(slot.wii_motion);
|
|
#endif
|
|
#ifdef SWITCH_PICO_WII_IR_GYRO
|
|
slot.wii_aim = {};
|
|
#endif
|
|
slot.motion_enabled = kDefaultMotionEnabled;
|
|
slot.pre_hotkey_button_mask = 0;
|
|
slot.accelerometer = {};
|
|
slot.nunchuk_accelerometer = {};
|
|
slot.feedback_pending = false;
|
|
slot.feedback_until_ms = 0;
|
|
slot.pending_feedback = {};
|
|
slot.pending_profile_feedback_count = 0;
|
|
for (ProfileFeedbackEnvelope& feedback :
|
|
slot.pending_profile_feedback) {
|
|
feedback = {};
|
|
}
|
|
slot.profile_feedback = {};
|
|
slot.retained_host_rumble_valid = false;
|
|
slot.retained_host_rumble = {};
|
|
}
|
|
|
|
void invalidate_slot(BackendSlot& slot) {
|
|
#ifdef SWITCH_PICO_WII_IR
|
|
wii_ir_pointer_disconnect(static_cast<uint8_t>(&slot - g_slots));
|
|
#endif
|
|
clear_switch2_ingress(slot);
|
|
reset_switch2_outputs(slot);
|
|
reset_slot_hotkeys(slot);
|
|
slot.rumble_pending = false;
|
|
slot.pending_rumble = {};
|
|
slot.state = make_neutral_state();
|
|
++slot.state_generation;
|
|
++slot.connection_generation;
|
|
}
|
|
|
|
void release_slot(BackendSlot& slot) {
|
|
invalidate_slot(slot);
|
|
slot.identity = controller_identity_global();
|
|
slot.device = nullptr;
|
|
slot.companion = nullptr;
|
|
slot.gamepad = {};
|
|
slot.companion_gamepad = {};
|
|
slot.extra_buttons = 0;
|
|
slot.companion_extra_buttons = 0;
|
|
slot.active = false;
|
|
}
|
|
|
|
bool is_solo_wii_remote(const BackendSlot& slot) {
|
|
if (!slot.active || slot.device == nullptr || slot.companion != nullptr ||
|
|
slot.device->controller_type != CONTROLLER_TYPE_WiiController) {
|
|
return false;
|
|
}
|
|
switch (slot.device->controller_subtype) {
|
|
case CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL:
|
|
case CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL:
|
|
case CONTROLLER_SUBTYPE_WIIMOTE_ACCEL:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
HotkeyDecision update_controller_hotkeys(
|
|
uint8_t slot_index, uni_hid_device_t* device) {
|
|
HotkeyDecision decision{kDefaultMotionEnabled};
|
|
state_lock_enter();
|
|
const BackendSlot& slot = g_slots[slot_index];
|
|
if (slot.active && slot.device == device) {
|
|
decision.motion_enabled = slot.motion_enabled;
|
|
}
|
|
state_lock_exit();
|
|
return decision;
|
|
}
|
|
|
|
|
|
bool pairing_window_active_at(uint32_t now_ms) {
|
|
return g_pairing_window_open &&
|
|
static_cast<int32_t>(now_ms - g_pairing_window_deadline_ms) < 0;
|
|
}
|
|
|
|
void handle_btstack_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 < 2) {
|
|
return;
|
|
}
|
|
|
|
bd_addr_t address{};
|
|
bd_addr_t identity_address{};
|
|
bd_addr_t connection_address{};
|
|
hci_con_handle_t connection_handle = 0;
|
|
const bool pairing_open =
|
|
pairing_window_active_at(btstack_run_loop_get_time_ms());
|
|
switch (hci_event_packet_get_type(packet)) {
|
|
case SM_EVENT_IDENTITY_RESOLVING_STARTED:
|
|
if (SWITCH_PICO_ENABLE_BLE && size >= 11) {
|
|
clear_ble_identity_for_handle(
|
|
sm_event_identity_resolving_started_get_handle(packet));
|
|
}
|
|
break;
|
|
case SM_EVENT_IDENTITY_RESOLVING_FAILED:
|
|
if (SWITCH_PICO_ENABLE_BLE && size >= 11) {
|
|
clear_ble_identity_for_handle(
|
|
sm_event_identity_resolving_failed_get_handle(packet));
|
|
}
|
|
break;
|
|
case SM_EVENT_IDENTITY_RESOLVING_SUCCEEDED:
|
|
if (SWITCH_PICO_ENABLE_BLE && size >= 20) {
|
|
connection_handle =
|
|
sm_event_identity_resolving_succeeded_get_handle(packet);
|
|
sm_event_identity_resolving_succeeded_get_address(
|
|
packet, connection_address);
|
|
sm_event_identity_resolving_succeeded_get_identity_address(
|
|
packet, identity_address);
|
|
record_ble_identity(
|
|
connection_handle, connection_address,
|
|
sm_event_identity_resolving_succeeded_get_identity_addr_type(
|
|
packet),
|
|
identity_address);
|
|
}
|
|
break;
|
|
case SM_EVENT_IDENTITY_CREATED:
|
|
if (SWITCH_PICO_ENABLE_BLE && size >= 20) {
|
|
connection_handle =
|
|
sm_event_identity_created_get_handle(packet);
|
|
sm_event_identity_created_get_address(packet, address);
|
|
sm_event_identity_created_get_identity_address(
|
|
packet, identity_address);
|
|
connection_address_for_handle(
|
|
connection_handle, address, connection_address);
|
|
record_ble_identity(
|
|
connection_handle, connection_address,
|
|
sm_event_identity_created_get_identity_addr_type(packet),
|
|
identity_address);
|
|
}
|
|
break;
|
|
case SM_EVENT_REENCRYPTION_STARTED:
|
|
if (SWITCH_PICO_ENABLE_BLE && size >= 11) {
|
|
connection_handle =
|
|
sm_event_reencryption_started_get_handle(packet);
|
|
sm_event_reencryption_started_get_address(
|
|
packet, identity_address);
|
|
connection_address_for_handle(
|
|
connection_handle, identity_address,
|
|
connection_address);
|
|
record_ble_identity(
|
|
connection_handle, connection_address,
|
|
sm_event_reencryption_started_get_addr_type(packet),
|
|
identity_address);
|
|
}
|
|
break;
|
|
case SM_EVENT_REENCRYPTION_COMPLETE:
|
|
if (SWITCH_PICO_ENABLE_BLE && size >= 12) {
|
|
connection_handle =
|
|
sm_event_reencryption_complete_get_handle(packet);
|
|
if (sm_event_reencryption_complete_get_status(packet) ==
|
|
ERROR_CODE_SUCCESS) {
|
|
sm_event_reencryption_complete_get_address(
|
|
packet, identity_address);
|
|
connection_address_for_handle(
|
|
connection_handle, identity_address,
|
|
connection_address);
|
|
record_ble_identity(
|
|
connection_handle, connection_address,
|
|
sm_event_reencryption_complete_get_addr_type(packet),
|
|
identity_address);
|
|
} else {
|
|
clear_ble_identity_for_handle(connection_handle);
|
|
}
|
|
}
|
|
break;
|
|
case HCI_EVENT_USER_CONFIRMATION_REQUEST:
|
|
if (!SWITCH_PICO_ENABLE_CLASSIC || size < 8) {
|
|
break;
|
|
}
|
|
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:
|
|
if (!SWITCH_PICO_ENABLE_CLASSIC || size < 8) {
|
|
break;
|
|
}
|
|
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) {
|
|
state_lock_enter();
|
|
const bool requested = g_pairing_window_requested;
|
|
g_pairing_window_requested = false;
|
|
state_lock_exit();
|
|
if (g_connection_policy_state == ConnectionPolicyState::FailedClosed) {
|
|
return false;
|
|
}
|
|
|
|
if (requested) {
|
|
ConfigurationServiceSnapshot configuration{};
|
|
configuration_service_snapshot(&configuration);
|
|
g_pairing_window_duration_ms =
|
|
static_cast<uint32_t>(
|
|
configuration.configuration.pairing_window_seconds) *
|
|
1000u;
|
|
g_pairing_window_open = true;
|
|
g_pairing_window_deadline_ms =
|
|
now_ms + g_pairing_window_duration_ms;
|
|
gap_set_bondable_mode(true);
|
|
if (SWITCH_PICO_ENABLE_BLE) {
|
|
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;
|
|
if (SWITCH_PICO_ENABLE_BLE) {
|
|
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<uint8_t>(address_type), address);
|
|
}
|
|
for (uint8_t index = 0; index < UNI_SWITCH2_PAIRING_CAPACITY; ++index) {
|
|
uint8_t address_type = BD_ADDR_TYPE_UNKNOWN;
|
|
bd_addr_t address{};
|
|
if (uni_switch2_pairing_get(index, &address_type, address)) {
|
|
append_pairing_record(
|
|
snapshot, Bluepad32PairingTransport::kBle, address_type, address);
|
|
}
|
|
}
|
|
|
|
state_lock_enter();
|
|
if (g_pairing_snapshot.status == Bluepad32PairingSnapshotStatus::kFailed) {
|
|
snapshot.status = Bluepad32PairingSnapshotStatus::kFailed;
|
|
}
|
|
snapshot.generation = g_pairing_snapshot.generation + 1;
|
|
snapshot.completed_clear_pairings_token =
|
|
g_pairing_snapshot.completed_clear_pairings_token;
|
|
g_pairing_snapshot = snapshot;
|
|
g_pairing_snapshot_requested = false;
|
|
state_lock_exit();
|
|
}
|
|
|
|
void process_pairing_snapshot_request() {
|
|
state_lock_enter();
|
|
const bool requested = g_pairing_snapshot_requested;
|
|
state_lock_exit();
|
|
if (requested) {
|
|
refresh_pairing_snapshot();
|
|
}
|
|
}
|
|
|
|
void apply_connection_policy();
|
|
void recompute_connection_status();
|
|
|
|
void process_clear_pairings(uint32_t now_ms) {
|
|
uni_hid_device_t* devices[kSlotCount]{};
|
|
uint8_t device_count = 0;
|
|
state_lock_enter();
|
|
const uint32_t request_token =
|
|
g_clear_pairings_requested_token;
|
|
if (request_token != 0) {
|
|
g_clear_pairings_requested_token = 0;
|
|
g_clear_pairings_in_progress_token = request_token;
|
|
}
|
|
if (request_token != 0) {
|
|
g_pairing_window_requested = false;
|
|
for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) {
|
|
BackendSlot& slot = g_slots[slot_index];
|
|
if (slot.device != nullptr) {
|
|
g_retired_devices[physical_index_for_device(slot.device)] =
|
|
slot.device;
|
|
devices[device_count++] = slot.device;
|
|
}
|
|
if (slot.companion != nullptr) {
|
|
g_retired_devices[physical_index_for_device(slot.companion)] =
|
|
slot.companion;
|
|
devices[device_count++] = slot.companion;
|
|
}
|
|
release_slot(slot);
|
|
g_joycon_gestures[slot_index] = {};
|
|
g_joycon_overrides[slot_index] = {};
|
|
g_joycon_pair_hints[slot_index] = {};
|
|
}
|
|
}
|
|
state_lock_exit();
|
|
if (request_token == 0) {
|
|
return;
|
|
}
|
|
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
|
|
mapping = {};
|
|
}
|
|
|
|
g_pairing_window_open = false;
|
|
gap_set_bondable_mode(false);
|
|
if (SWITCH_PICO_ENABLE_BLE) {
|
|
sm_set_accepted_stk_generation_methods(0);
|
|
}
|
|
const bool proprietary_cleared = uni_switch2_pairing_clear();
|
|
uni_bt_del_keys_unsafe();
|
|
for (uni_hid_device_t* device : devices) {
|
|
if (device != nullptr) {
|
|
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
|
|
switch_native_output_detach(device);
|
|
#endif
|
|
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
|
haptics_experiment_detach(device);
|
|
#endif
|
|
uni_hid_device_disconnect(device);
|
|
}
|
|
}
|
|
refresh_pairing_snapshot();
|
|
if (!proprietary_cleared) {
|
|
stop_background_scan();
|
|
uni_bt_stop_scanning_unsafe();
|
|
uni_bt_allow_incoming_connections(false);
|
|
g_connection_policy_state = ConnectionPolicyState::FailedClosed;
|
|
state_lock_enter();
|
|
g_pairing_snapshot.status = Bluepad32PairingSnapshotStatus::kFailed;
|
|
g_clear_pairings_in_progress_token = 0;
|
|
g_clear_pairings_requested_token = 0;
|
|
g_pairing_window_requested = false;
|
|
state_lock_exit();
|
|
return;
|
|
}
|
|
|
|
g_connection_status = ConnectionStatus::Scanning;
|
|
g_status_led_tick = 0;
|
|
g_pairing_reset_feedback_deadline_ms =
|
|
now_ms + kPairingResetFeedbackDurationMs;
|
|
if (g_connection_policy_state == ConnectionPolicyState::FailedClosed) {
|
|
g_connection_policy_state = ConnectionPolicyState::Uninitialized;
|
|
}
|
|
apply_connection_policy();
|
|
state_lock_enter();
|
|
g_pairing_snapshot.completed_clear_pairings_token =
|
|
request_token;
|
|
g_clear_pairings_in_progress_token = 0;
|
|
state_lock_exit();
|
|
}
|
|
|
|
|
|
void apply_connection_policy() {
|
|
if (g_connection_policy_state == ConnectionPolicyState::FailedClosed) {
|
|
return;
|
|
}
|
|
apply_radio_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 bool background_scan = SWITCH_PICO_ENABLE_BLE &&
|
|
free_slot && !active_scan && waiting_for_joycon_mate();
|
|
const ConnectionPolicyState desired_state =
|
|
!free_slot
|
|
? ConnectionPolicyState::Paused
|
|
: (active_scan ? ConnectionPolicyState::Open
|
|
: ConnectionPolicyState::Passive);
|
|
if (g_connection_policy_state == desired_state &&
|
|
g_background_scan_active == background_scan) {
|
|
return;
|
|
}
|
|
|
|
// Leave low-duty LE explicitly before the aggregate stop, which otherwise
|
|
// does nothing when its own scanning flag is already clear.
|
|
stop_background_scan();
|
|
uni_bt_stop_scanning_unsafe();
|
|
|
|
if (!free_slot) {
|
|
uni_bt_allow_incoming_connections(false);
|
|
g_connection_policy_state = ConnectionPolicyState::Paused;
|
|
return;
|
|
}
|
|
|
|
// Passive mode permits incoming Classic reconnects, with LE discovery
|
|
// limited to a remembered opposite half for a ready solo Joy-Con2.
|
|
uni_bt_allow_incoming_connections(SWITCH_PICO_ENABLE_CLASSIC != 0);
|
|
if (active_scan) {
|
|
if (SWITCH_PICO_ENABLE_BLE) {
|
|
uni_bt_le_set_background_scan(false);
|
|
}
|
|
uni_bt_start_scanning_and_autoconnect_unsafe();
|
|
g_connection_policy_state = ConnectionPolicyState::Open;
|
|
} else {
|
|
g_connection_policy_state = ConnectionPolicyState::Passive;
|
|
if (background_scan) {
|
|
uni_bt_le_set_background_scan(true);
|
|
uni_bt_le_scan_start();
|
|
g_background_scan_active = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool deadline_reached(uint32_t now_ms, uint32_t deadline_ms) {
|
|
return static_cast<int32_t>(now_ms - deadline_ms) >= 0;
|
|
}
|
|
|
|
bool advance_profile_feedback(ProfileFeedbackSequence* sequence,
|
|
uint32_t now_ms) {
|
|
bool rumble_dispatch = false;
|
|
for (uint8_t transition = 0;
|
|
transition < CONTROLLER_PROFILE_COUNT * 2u &&
|
|
sequence->active &&
|
|
deadline_reached(now_ms, sequence->phase_deadline_ms);
|
|
++transition) {
|
|
sequence->phase_deadline_ms +=
|
|
kProfileFeedbackPhaseDurationMs;
|
|
if (sequence->on) {
|
|
sequence->on = false;
|
|
rumble_dispatch = false;
|
|
} else if (sequence->pulses_started >=
|
|
sequence->pulse_count) {
|
|
sequence->active = false;
|
|
} else {
|
|
sequence->on = true;
|
|
++sequence->pulses_started;
|
|
rumble_dispatch = sequence->rumble_enabled;
|
|
}
|
|
}
|
|
return rumble_dispatch;
|
|
}
|
|
|
|
void update_status_led() {
|
|
++g_status_led_tick;
|
|
const uint32_t now_ms = btstack_run_loop_get_time_ms();
|
|
bool profile_led_override = false;
|
|
bool profile_led_on = false;
|
|
state_lock_enter();
|
|
for (const BackendSlot& slot : g_slots) {
|
|
if (slot.profile_feedback.active &&
|
|
slot.profile_feedback.led_enabled) {
|
|
profile_led_override = true;
|
|
profile_led_on =
|
|
profile_led_on || slot.profile_feedback.on;
|
|
}
|
|
}
|
|
state_lock_exit();
|
|
|
|
bool led_on = false;
|
|
if (profile_led_override) {
|
|
led_on = profile_led_on;
|
|
} else if (static_cast<int32_t>(
|
|
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 apply_joycon_configuration(const ConfigurationServiceSnapshot& configuration);
|
|
void process_joycon_gestures(uint32_t now_ms);
|
|
|
|
void process_configuration_timer(btstack_timer_source_t* timer) {
|
|
__atomic_add_fetch(
|
|
&g_configuration_timer_ticks, 1, __ATOMIC_RELAXED);
|
|
btstack_run_loop_set_timer(timer, kConfigurationPollIntervalMs);
|
|
btstack_run_loop_add_timer(timer);
|
|
const uint32_t now_ms = btstack_run_loop_get_time_ms();
|
|
apply_radio_connection_policy();
|
|
configuration_service_task_on_storage_core(now_ms);
|
|
profile_service_task_on_storage_core(now_ms);
|
|
ConfigurationServiceSnapshot configuration{};
|
|
configuration_service_snapshot(&configuration);
|
|
apply_joycon_configuration(configuration);
|
|
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
|
|
if (configuration.state == ConfigurationServiceState::kReady) {
|
|
uint8_t previously_owned = 0;
|
|
for (uint8_t i = 0; i < kSlotCount; ++i)
|
|
if (switch_native_output_owns(g_slots[i].device)) previously_owned |= 1u << i;
|
|
switch_native_output_configure(configuration.configuration, configuration.generation);
|
|
for (uint8_t i = 0; i < kSlotCount; ++i) {
|
|
if ((previously_owned & (1u << i)) || !switch_native_output_owns(g_slots[i].device))
|
|
continue;
|
|
RumbleEnvelope retained{};
|
|
state_lock_enter();
|
|
const BackendSlot& current = g_slots[i];
|
|
retained = current.pending_rumble;
|
|
const bool valid = current.active && retained.slot == i &&
|
|
retained.connection_generation == current.connection_generation &&
|
|
retained.duration_ms == host_rumble_duration_ms();
|
|
state_lock_exit();
|
|
if (valid) switch_native_output_submit(i, retained.connection_generation,
|
|
retained.received_us, retained.rumble,
|
|
retained.duration_ms == kXInputHostRumbleDurationMs);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void dispatch_rumble(uni_hid_device_t* device, uint16_t duration_ms,
|
|
uint8_t weak, uint8_t strong) {
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
if (device->controller_type == CONTROLLER_TYPE_PS5Controller &&
|
|
device->report_parser.parse_input_report == uni_hid_parser_ds5_parse_input_report) {
|
|
// Local feedback shares the bounded writer. Its stale compatibility
|
|
// packets must not remain queued to overtake a later native cue.
|
|
uni_hid_parser_ds5_bridge_rumble(
|
|
device, duration_ms > 1000 ? 1000 : duration_ms, weak, strong);
|
|
return;
|
|
}
|
|
#endif
|
|
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
|
|
if (switch_native_output_feedback(device, strong, weak, duration_ms)) return;
|
|
#endif
|
|
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
|
if (haptics_experiment_feedback(device, strong, weak, duration_ms)) {
|
|
return;
|
|
}
|
|
#endif
|
|
device->report_parser.play_dual_rumble(device, 0, duration_ms, weak, strong);
|
|
}
|
|
|
|
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
|
struct WiiCueDispatch {
|
|
uni_hid_device_t* device = nullptr;
|
|
uint64_t token = 0;
|
|
uint32_t connection_generation = 0;
|
|
uint8_t slot = 0xff;
|
|
uint8_t phase = 0;
|
|
uint16_t duration_ms = 0;
|
|
bool cancellation_stop = false;
|
|
};
|
|
|
|
bool wii_cue_target_current() {
|
|
return g_wii_cue.slot < kSlotCount &&
|
|
is_selected_wii(g_slots[g_wii_cue.slot]) &&
|
|
g_slots[g_wii_cue.slot].connection_generation ==
|
|
g_wii_cue.connection_generation;
|
|
}
|
|
|
|
void restore_wii_host_rumble(BackendSlot& slot) {
|
|
if (slot.retained_host_rumble_valid) {
|
|
slot.pending_rumble = slot.retained_host_rumble;
|
|
slot.rumble_pending = true;
|
|
}
|
|
}
|
|
|
|
// Called under the backend lock, after profile/local arbitration. A waiting
|
|
// cue yields to local feedback; a playing cue is canceled rather than replayed
|
|
// after an interruption. A finite driver timer bounds even a stalled poller.
|
|
bool prepare_wii_cue(uint8_t slot_index, uint32_t now_ms,
|
|
bool local_active, bool local_dispatch,
|
|
WiiCueDispatch* output) {
|
|
WiiCue& cue = g_wii_cue;
|
|
if (cue.slot != slot_index) return false;
|
|
BackendSlot& slot = g_slots[slot_index];
|
|
if (!wii_cue_target_current() ||
|
|
slot.device->report_parser.play_dual_rumble == nullptr) {
|
|
cue = {};
|
|
return false;
|
|
}
|
|
if ((cue.result == 0 &&
|
|
static_cast<uint32_t>(now_ms - cue.requested_ms) >= kWiiCueDeadlineMs) ||
|
|
(cue.active &&
|
|
static_cast<uint32_t>(now_ms - cue.started_ms) >= kWiiCueDeadlineMs)) {
|
|
cancel_wii_cue_locked();
|
|
}
|
|
if (local_active || local_dispatch) {
|
|
if (cue.active) cancel_wii_cue_locked();
|
|
// A local rumble command replaces our finite pulse; a later stop must
|
|
// not cut that higher-priority feedback short.
|
|
if (local_dispatch) cue.stop_pending = false;
|
|
return cue.result == 0 || cue.active || cue.stop_pending;
|
|
}
|
|
if (cue.in_flight) return true;
|
|
if (cue.result != 0 && !cue.active && !cue.stop_pending) return false;
|
|
uint8_t phase = 0;
|
|
uint16_t duration_ms = 0;
|
|
if (!cue.stop_pending && cue.sample_id != 0) {
|
|
const WiiCuePattern& pattern = kWiiCuePatterns[cue.sample_id];
|
|
uint32_t elapsed = cue.active ? now_ms - cue.started_ms : 0;
|
|
while (phase < pattern.count && elapsed >= pattern.phases_ms[phase]) {
|
|
elapsed -= pattern.phases_ms[phase++];
|
|
}
|
|
if (phase == pattern.count) {
|
|
cue.active = false;
|
|
restore_wii_host_rumble(slot);
|
|
return false;
|
|
}
|
|
// The driver's duration timer supplies the gaps. Skip missed phases,
|
|
// never replay a burst of old pulses to catch up after a scheduling gap.
|
|
if ((phase & 1u) != 0 || phase == cue.dispatched_phase) return true;
|
|
duration_ms = static_cast<uint16_t>(pattern.phases_ms[phase] - elapsed);
|
|
}
|
|
*output = {slot.device, cue.token, cue.connection_generation,
|
|
slot_index, phase, duration_ms, cue.stop_pending};
|
|
cue.in_flight = true;
|
|
return true;
|
|
}
|
|
|
|
void dispatch_wii_cue(const WiiCueDispatch& command) {
|
|
if (command.device == nullptr) return;
|
|
state_lock_enter();
|
|
const bool current = g_wii_cue.token == command.token &&
|
|
g_wii_cue.in_flight && wii_cue_target_current() &&
|
|
g_slots[command.slot].device == command.device &&
|
|
(command.cancellation_stop ? g_wii_cue.stop_pending
|
|
: g_wii_cue.result != -1);
|
|
state_lock_exit();
|
|
// Lifecycle/parser callbacks are serialized on Core 1. The readiness check
|
|
// excludes the Wii void hook's early-return path during topology setup.
|
|
const bool dispatched = current &&
|
|
command.device->report_parser.play_dual_rumble != nullptr &&
|
|
uni_hid_parser_wii_rumble_ready(command.device);
|
|
if (dispatched) {
|
|
__atomic_add_fetch(&g_rumble_dispatches, 1, __ATOMIC_RELAXED);
|
|
command.device->report_parser.play_dual_rumble(
|
|
command.device, 0, command.duration_ms, UINT8_MAX, UINT8_MAX);
|
|
}
|
|
state_lock_enter();
|
|
WiiCue& cue = g_wii_cue;
|
|
if (cue.token == command.token && cue.connection_generation == command.connection_generation) {
|
|
cue.in_flight = false;
|
|
if (!wii_cue_target_current()) {
|
|
cue = {};
|
|
} else if (command.cancellation_stop) {
|
|
if (dispatched) {
|
|
cue.stop_pending = false;
|
|
restore_wii_host_rumble(g_slots[command.slot]);
|
|
}
|
|
} else if (cue.result != -1 && dispatched) {
|
|
const uint32_t now_ms = btstack_run_loop_get_time_ms();
|
|
if (cue.result == 0 && now_ms - cue.requested_ms >= kWiiCueDeadlineMs) {
|
|
cue.stop_pending = command.duration_ms != 0;
|
|
cancel_wii_cue_locked();
|
|
} else {
|
|
if (cue.result == 0) {
|
|
cue.started_ms = now_ms;
|
|
cue.result = 1; // Driver dispatch, not an application ACK.
|
|
}
|
|
cue.active = cue.sample_id != 0;
|
|
cue.dispatched_phase = command.phase;
|
|
if (!cue.active) restore_wii_host_rumble(g_slots[command.slot]);
|
|
}
|
|
}
|
|
}
|
|
state_lock_exit();
|
|
}
|
|
#endif
|
|
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
struct NativeGamepadCueDispatch {
|
|
uni_hid_device_t* device = nullptr;
|
|
uni_hid_device_t* companion = nullptr;
|
|
uint32_t connection_generation = 0;
|
|
uint32_t prepared_ms = 0;
|
|
uint64_t token[2]{};
|
|
uint16_t duration_ms = 0;
|
|
uint8_t magnitude[2]{};
|
|
uint8_t slot = 0xff;
|
|
};
|
|
|
|
// Source drivers use a shared finite timer (or one per paired half). Recompute
|
|
// both contributions at each boundary; the shortest ON remainder protects a
|
|
// mono actuator too. Absolute timelines skip missed pulses, never queue them.
|
|
bool prepare_native_cues(uint8_t index, uint32_t now_ms,
|
|
bool local_active, bool local_dispatch,
|
|
NativeGamepadCueDispatch* command) {
|
|
BackendSlot& slot = g_slots[index];
|
|
NativeGamepadMotorOutput& previous = slot.native_output;
|
|
bool busy = false;
|
|
bool pending = false;
|
|
uint16_t duration = UINT16_MAX;
|
|
uint8_t magnitude[2]{};
|
|
for (uint8_t side = 0; side < 2; ++side) {
|
|
NativeGamepadCue& cue = g_native_cues[side];
|
|
if (cue.slot != index) continue;
|
|
if (!native_cue_current(cue) ||
|
|
(cue.result == 0 && now_ms - cue.requested_ms >= kNativeGamepadCueDeadlineMs) ||
|
|
(cue.active && now_ms - cue.started_ms >= kNativeGamepadCueDeadlineMs))
|
|
cancel_native_cue_locked(cue);
|
|
if (local_active || local_dispatch) {
|
|
if (cue.active) cancel_native_cue_locked(cue);
|
|
busy |= cue.result == 0;
|
|
continue;
|
|
}
|
|
if (cue.in_flight) return true;
|
|
if (cue.result != 0 && !cue.active) continue;
|
|
command->token[side] = cue.token;
|
|
pending |= cue.result == 0;
|
|
if (cue.sample_id == 0) continue;
|
|
const NativeGamepadCuePattern& pattern = kNativeGamepadCuePatterns[cue.sample_id];
|
|
uint32_t elapsed = cue.active ? now_ms - cue.started_ms : 0;
|
|
uint8_t phase = 0;
|
|
while (phase < pattern.count && elapsed >= pattern.phases_ms[phase])
|
|
elapsed -= pattern.phases_ms[phase++];
|
|
if (phase == pattern.count) {
|
|
cue.active = false;
|
|
command->token[side] = 0;
|
|
continue;
|
|
}
|
|
busy = true;
|
|
if ((phase & 1u) == 0) {
|
|
magnitude[side] = pattern.magnitude;
|
|
const uint16_t remaining = static_cast<uint16_t>(pattern.phases_ms[phase] - elapsed);
|
|
if (remaining < duration) duration = remaining;
|
|
}
|
|
}
|
|
if (local_active || local_dispatch) {
|
|
// Higher-priority feedback replaces our finite timer; do not stop it.
|
|
if (local_dispatch) previous = {};
|
|
return busy || previous.owned;
|
|
}
|
|
if (duration == UINT16_MAX) duration = 0;
|
|
const uint32_t deadline = duration == 0 ? 0 : now_ms + duration;
|
|
const bool changed = magnitude[0] != previous.magnitude[0] ||
|
|
magnitude[1] != previous.magnitude[1] ||
|
|
(duration != 0 && deadline != previous.deadline_ms);
|
|
if (!pending && !changed) return busy || previous.owned;
|
|
if (!slot.active || slot.device == nullptr ||
|
|
(!native_rumble_capable(slot, 0) && !native_rumble_capable(slot, 1))) {
|
|
for (NativeGamepadCue& cue : g_native_cues)
|
|
if (cue.slot == index) cancel_native_cue_locked(cue);
|
|
previous = {};
|
|
return false;
|
|
}
|
|
command->device = slot.device;
|
|
command->companion = slot.companion;
|
|
command->connection_generation = slot.connection_generation;
|
|
command->prepared_ms = now_ms;
|
|
command->duration_ms = duration;
|
|
command->magnitude[0] = magnitude[0];
|
|
command->magnitude[1] = magnitude[1];
|
|
command->slot = index;
|
|
for (uint8_t side = 0; side < 2; ++side)
|
|
if (command->token[side] != 0) g_native_cues[side].in_flight = true;
|
|
return true;
|
|
}
|
|
|
|
bool submit_native_rumble(uni_hid_device_t* device, uint16_t duration,
|
|
uint8_t right, uint8_t left) {
|
|
if (device == nullptr || device->report_parser.play_dual_rumble == nullptr)
|
|
return false;
|
|
if (device->controller_type == CONTROLLER_TYPE_PS5Controller &&
|
|
device->report_parser.parse_input_report == uni_hid_parser_ds5_parse_input_report) {
|
|
return uni_hid_parser_ds5_bridge_rumble(device, duration, right, left);
|
|
}
|
|
// Existing finite-duration dispatch is the strongest observable result
|
|
// most drivers expose. It is not transport acceptance or a remote ACK.
|
|
dispatch_rumble(device, duration, right, left);
|
|
return true;
|
|
}
|
|
|
|
void dispatch_native_cues(const NativeGamepadCueDispatch& command) {
|
|
if (command.device == nullptr) return;
|
|
BackendSlot& slot = g_slots[command.slot];
|
|
const bool paired = command.companion != nullptr;
|
|
bool submitted[2]{};
|
|
uint32_t dispatch_ms = btstack_run_loop_get_time_ms();
|
|
for (uint8_t target = 0; target < (paired ? 2 : 1); ++target) {
|
|
state_lock_enter();
|
|
bool current = slot.active && slot.device == command.device &&
|
|
slot.companion == command.companion &&
|
|
slot.connection_generation == command.connection_generation;
|
|
for (uint8_t side = 0; side < 2; ++side) {
|
|
if (paired && side != target) continue;
|
|
const NativeGamepadCue& cue = g_native_cues[side];
|
|
if (command.token[side] != 0)
|
|
current &= cue.token == command.token[side] && cue.in_flight &&
|
|
cue.result != -1 && native_cue_current(cue);
|
|
}
|
|
state_lock_exit();
|
|
// No backend lock crosses a driver call. Recheck every real target:
|
|
// cancel/reselection or a stall during R dispatch must not send stale L.
|
|
dispatch_ms = btstack_run_loop_get_time_ms();
|
|
const uint32_t delay = dispatch_ms - command.prepared_ms;
|
|
const uint16_t duration = command.duration_ms > delay
|
|
? static_cast<uint16_t>(command.duration_ms - delay) : 0;
|
|
current &= delay < kRumblePollIntervalMs &&
|
|
(command.duration_ms == 0 || duration != 0);
|
|
if (!current) continue;
|
|
uni_hid_device_t* device = paired && target == 0
|
|
? command.companion : command.device;
|
|
const uint8_t right = command.magnitude[paired ? target : 0];
|
|
const uint8_t left = command.magnitude[paired ? target : 1];
|
|
if (!submit_native_rumble(
|
|
device, (right | left) == 0 ? 0 : duration, right, left)) continue;
|
|
__atomic_add_fetch(&g_rumble_dispatches, 1, __ATOMIC_RELAXED);
|
|
if (paired) submitted[target] = true;
|
|
else submitted[0] = submitted[1] = true;
|
|
|
|
state_lock_enter();
|
|
if (slot.active && slot.device == command.device &&
|
|
slot.companion == command.companion) {
|
|
// Retain each actual submission even if USB canceled/reselected
|
|
// during its driver call, or the other half cannot be submitted.
|
|
NativeGamepadMotorOutput& output = slot.native_output;
|
|
if (paired) output.magnitude[target] = command.magnitude[target];
|
|
else {
|
|
output.magnitude[0] = command.magnitude[0];
|
|
output.magnitude[1] = command.magnitude[1];
|
|
}
|
|
output.owned = (output.magnitude[0] | output.magnitude[1]) != 0;
|
|
output.deadline_ms = output.owned
|
|
? command.prepared_ms + command.duration_ms : 0;
|
|
}
|
|
state_lock_exit();
|
|
}
|
|
state_lock_enter();
|
|
for (uint8_t side = 0; side < 2; ++side) {
|
|
NativeGamepadCue& cue = g_native_cues[side];
|
|
if (command.token[side] == 0 || cue.token != command.token[side]) continue;
|
|
cue.in_flight = false;
|
|
if (!native_cue_current(cue) ||
|
|
(cue.result == 0 && dispatch_ms - cue.requested_ms >= kNativeGamepadCueDeadlineMs)) {
|
|
cancel_native_cue_locked(cue);
|
|
} else if (submitted[side] && cue.result == 0) {
|
|
cue.result = 1; // Accepted source submission, never a native ACK.
|
|
cue.started_ms = command.prepared_ms;
|
|
cue.active = cue.sample_id != 0;
|
|
}
|
|
}
|
|
state_lock_exit();
|
|
}
|
|
#endif
|
|
|
|
// Core 1 only. The mailbox carries values, never a parser pointer supplied by
|
|
// Core 0. Revalidate after lifecycle/topology work and before touching the parser.
|
|
void process_wii_orientation(uint8_t slot_index) {
|
|
state_lock_enter();
|
|
BackendSlot& slot = g_slots[slot_index];
|
|
if (!slot.wii_orientation_pending) {
|
|
state_lock_exit();
|
|
return;
|
|
}
|
|
const WiiOrientationRequest request = slot.pending_wii_orientation;
|
|
slot.wii_orientation_pending = false;
|
|
slot.pending_wii_orientation = {};
|
|
if (!is_solo_wii_remote(slot) ||
|
|
slot.connection_generation != request.connection_generation ||
|
|
!controller_identity_equal(slot.identity, request.identity)) {
|
|
state_lock_exit();
|
|
return;
|
|
}
|
|
uni_hid_device_t* device = slot.device;
|
|
// Subtype publication can lag set_mode during extension discovery. Even a
|
|
// reselection must reach the parser to replace a deferred opposite choice.
|
|
// A new logical epoch retires profile macros, hotkey holds, capture and
|
|
// feedback without touching this connection's identity or saved profiles.
|
|
invalidate_slot(slot);
|
|
slot.gamepad = {};
|
|
slot.extra_buttons = 0;
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
refresh_native_source_locked();
|
|
#endif
|
|
state_lock_exit();
|
|
|
|
// The setter can synchronously re-enter the platform ready callback, so
|
|
// release the lock first. Lifecycle and parser callbacks share this core.
|
|
if (device->report_parser.play_dual_rumble != nullptr) {
|
|
dispatch_rumble(device, 0, 0, 0);
|
|
}
|
|
uni_hid_parser_wii_set_mode(
|
|
device, request.vertical ? WII_MODE_VERTICAL : WII_MODE_HORIZONTAL);
|
|
apply_slot_lighting(slot_index, device);
|
|
}
|
|
|
|
uint8_t xbox_trigger_magnitude(const SwitchHapticsActuatorFrame& frame) {
|
|
uint16_t peak = 0;
|
|
for (uint8_t i = 0; i < frame.sample_count && i < 3; ++i) {
|
|
if (frame.samples[i].high_amplitude_q15 > peak)
|
|
peak = frame.samples[i].high_amplitude_q15;
|
|
}
|
|
// Impulse triggers are amplitude-only ERMs, not HD actuators. Keep their
|
|
// extra response at half scale, including after profile amplification.
|
|
if (peak > 32767) peak = 32767;
|
|
return static_cast<uint8_t>((static_cast<uint32_t>(peak) * 127u) / 32767u);
|
|
}
|
|
|
|
void dispatch_host_rumble(uni_hid_device_t* device, uint16_t duration_ms,
|
|
const ControllerRumbleOutput& rumble) {
|
|
const uint8_t weak = rumble.high_frequency_magnitude;
|
|
const uint8_t strong = rumble.low_frequency_magnitude;
|
|
if (device->vendor_id == 0x045e &&
|
|
device->report_parser.play_dual_rumble ==
|
|
uni_hid_parser_xboxone_play_dual_rumble) {
|
|
const bool hd = rumble.hd.actuators[0].sample_count != 0 ||
|
|
rumble.hd.actuators[1].sample_count != 0;
|
|
const uint8_t left = hd ? xbox_trigger_magnitude(rumble.hd.actuators[0])
|
|
: weak / 2u;
|
|
const uint8_t right = hd ? xbox_trigger_magnitude(rumble.hd.actuators[1])
|
|
: weak / 2u;
|
|
const bool stop = (weak | strong | left | right) == 0;
|
|
xboxone_play_quad_rumble(device, 0, stop ? 0 : duration_ms,
|
|
left, right, weak, strong);
|
|
return;
|
|
}
|
|
dispatch_rumble(device, (weak | strong) == 0 ? 0 : duration_ms, weak, strong);
|
|
}
|
|
|
|
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
|
void seed_native_host_rumble() {
|
|
HapticsExperimentDiagnostics native;
|
|
haptics_experiment_snapshot(&native);
|
|
if (native.mode != 1 || native.slot >= kSlotCount ||
|
|
native.run_id == g_seeded_native_run_id ||
|
|
(native.state != HapticsExperimentState::kPending &&
|
|
native.state != HapticsExperimentState::kRunning)) {
|
|
return;
|
|
}
|
|
g_seeded_native_run_id = native.run_id;
|
|
RumbleEnvelope retained{};
|
|
state_lock_enter();
|
|
BackendSlot& slot = g_slots[native.slot];
|
|
const bool valid = slot.active && slot.device != nullptr &&
|
|
slot.retained_host_rumble_valid &&
|
|
slot.connection_generation == native.connection_generation &&
|
|
slot.retained_host_rumble.connection_generation ==
|
|
native.connection_generation &&
|
|
slot.retained_host_rumble.slot == native.slot &&
|
|
slot.retained_host_rumble.duration_ms ==
|
|
kXInputHostRumbleDurationMs;
|
|
if (valid) {
|
|
retained = slot.retained_host_rumble;
|
|
// Arming cancels compatibility output even when its mailbox was
|
|
// already consumed. Keep that held state available for the next Stop.
|
|
slot.pending_rumble = retained;
|
|
slot.rumble_pending = true;
|
|
}
|
|
state_lock_exit();
|
|
if (valid) {
|
|
// Replay once on arm, not on a watchdog cadence. The original timestamp
|
|
// keeps a raced newer USB command authoritative in the host timeline.
|
|
haptics_experiment_submit_rumble(
|
|
native.slot, native.connection_generation, retained.received_us,
|
|
retained.rumble.low_frequency_magnitude,
|
|
retained.rumble.high_frequency_magnitude);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void process_rumble_timer(btstack_timer_source_t* timer) {
|
|
__atomic_add_fetch(&g_rumble_timer_ticks, 1, __ATOMIC_RELAXED);
|
|
uint32_t now_ms = btstack_run_loop_get_time_ms();
|
|
|
|
process_clear_pairings(now_ms);
|
|
process_pairing_snapshot_request();
|
|
process_joycon_gestures(now_ms);
|
|
now_ms = btstack_run_loop_get_time_ms();
|
|
const bool wake_identity_ready =
|
|
switch2_wake_ready_for_connections();
|
|
if (g_connection_policy_state ==
|
|
ConnectionPolicyState::Uninitialized &&
|
|
wake_identity_ready) {
|
|
recompute_connection_status();
|
|
}
|
|
if (update_pairing_window(now_ms) && wake_identity_ready) {
|
|
apply_connection_policy();
|
|
}
|
|
const bool xinput_host_mode =
|
|
host_rumble_duration_ms() == kXInputHostRumbleDurationMs;
|
|
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
|
if (xinput_host_mode) seed_native_host_rumble();
|
|
haptics_experiment_poll();
|
|
#endif
|
|
|
|
for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) {
|
|
process_wii_orientation(slot_index);
|
|
drain_switch2_ingress(slot_index, now_ms);
|
|
RumbleEnvelope envelope{};
|
|
FeedbackEnvelope feedback{};
|
|
ProfileFeedbackEnvelope profile_feedback{};
|
|
uni_hid_device_t* device = nullptr;
|
|
uni_hid_device_t* profile_lighting_device = nullptr;
|
|
uint32_t profile_lighting_generation = 0;
|
|
uni_hid_device_t* companion = nullptr;
|
|
uint32_t dispatch_generation = 0;
|
|
bool profile_lighting_dispatch = false;
|
|
bool profile_lighting_restore = false;
|
|
bool profile_rumble_dispatch = false;
|
|
bool feedback_dispatch = false;
|
|
bool host_dispatch = false;
|
|
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
|
WiiCueDispatch wii_cue_dispatch{};
|
|
#endif
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
NativeGamepadCueDispatch native_dispatch{};
|
|
#endif
|
|
|
|
state_lock_enter();
|
|
BackendSlot& slot = g_slots[slot_index];
|
|
if (slot.retained_host_rumble_valid &&
|
|
(!xinput_host_mode ||
|
|
slot.retained_host_rumble.duration_ms !=
|
|
kXInputHostRumbleDurationMs ||
|
|
slot.retained_host_rumble.slot != slot_index ||
|
|
slot.retained_host_rumble.connection_generation !=
|
|
slot.connection_generation ||
|
|
!slot.active || slot.device == nullptr)) {
|
|
slot.retained_host_rumble_valid = false;
|
|
slot.retained_host_rumble = {};
|
|
}
|
|
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
|
if (haptics_experiment_owns(slot.device) &&
|
|
!haptics_experiment_gameplay_owns(slot.device)) {
|
|
// Fixture/startup/restoration exclusively own output. Preserve
|
|
// stateful XInput requests until compatibility restoration ends.
|
|
if (!xinput_host_mode) slot.rumble_pending = false;
|
|
state_lock_exit();
|
|
continue;
|
|
}
|
|
#endif
|
|
if (slot.profile_feedback.active &&
|
|
slot.profile_feedback.connection_generation !=
|
|
slot.connection_generation) {
|
|
slot.profile_feedback = {};
|
|
}
|
|
const bool profile_feedback_was_active =
|
|
slot.profile_feedback.active;
|
|
const bool completed_feedback_had_rumble =
|
|
slot.profile_feedback.rumble_enabled;
|
|
const bool completed_feedback_had_led =
|
|
slot.profile_feedback.led_enabled;
|
|
const uint32_t completed_feedback_generation =
|
|
slot.profile_feedback.connection_generation;
|
|
profile_rumble_dispatch =
|
|
advance_profile_feedback(&slot.profile_feedback, now_ms);
|
|
if (profile_feedback_was_active &&
|
|
!slot.profile_feedback.active &&
|
|
completed_feedback_had_rumble &&
|
|
slot.retained_host_rumble_valid) {
|
|
slot.pending_rumble = slot.retained_host_rumble;
|
|
slot.rumble_pending = true;
|
|
}
|
|
if (profile_feedback_was_active &&
|
|
!slot.profile_feedback.active &&
|
|
completed_feedback_had_led && slot.active &&
|
|
slot.device != nullptr &&
|
|
completed_feedback_generation ==
|
|
slot.connection_generation) {
|
|
profile_lighting_device = slot.device;
|
|
profile_lighting_generation =
|
|
completed_feedback_generation;
|
|
profile_lighting_restore = true;
|
|
}
|
|
if (profile_rumble_dispatch) {
|
|
device = slot.device;
|
|
}
|
|
|
|
const bool feedback_active =
|
|
static_cast<int32_t>(now_ms - slot.feedback_until_ms) < 0;
|
|
if (!slot.profile_feedback.active && !feedback_active &&
|
|
slot.pending_profile_feedback_count != 0) {
|
|
profile_feedback = slot.pending_profile_feedback[0];
|
|
if (slot.pending_profile_feedback_count == 2) {
|
|
slot.pending_profile_feedback[0] =
|
|
slot.pending_profile_feedback[1];
|
|
}
|
|
--slot.pending_profile_feedback_count;
|
|
slot.pending_profile_feedback[
|
|
slot.pending_profile_feedback_count] = {};
|
|
const uint8_t policy =
|
|
static_cast<uint8_t>(profile_feedback.policy);
|
|
if (slot.active && slot.device != nullptr &&
|
|
profile_feedback.connection_generation ==
|
|
slot.connection_generation &&
|
|
profile_feedback.active_profile_number != 0 &&
|
|
profile_feedback.active_profile_number <=
|
|
CONTROLLER_PROFILE_COUNT &&
|
|
valid_confirmation_policy(profile_feedback.policy) &&
|
|
profile_feedback.policy !=
|
|
ControllerProfileConfirmationPolicy::kNone) {
|
|
slot.profile_feedback = {
|
|
slot.connection_generation,
|
|
now_ms + kProfileFeedbackPhaseDurationMs,
|
|
profile_feedback.active_profile_number,
|
|
1,
|
|
true,
|
|
true,
|
|
(policy & static_cast<uint8_t>(
|
|
ControllerProfileConfirmationPolicy::
|
|
kRumble)) != 0,
|
|
(policy & static_cast<uint8_t>(
|
|
ControllerProfileConfirmationPolicy::
|
|
kLed)) != 0,
|
|
};
|
|
device = slot.device;
|
|
profile_lighting_device = slot.device;
|
|
profile_lighting_generation =
|
|
slot.profile_feedback.connection_generation;
|
|
profile_lighting_dispatch =
|
|
slot.profile_feedback.led_enabled;
|
|
profile_rumble_dispatch =
|
|
slot.profile_feedback.rumble_enabled;
|
|
}
|
|
}
|
|
|
|
if (!slot.profile_feedback.active &&
|
|
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 local_feedback_active =
|
|
slot.profile_feedback.active ||
|
|
static_cast<int32_t>(
|
|
now_ms - slot.feedback_until_ms) < 0;
|
|
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
|
const bool wii_cue_owns_rumble = prepare_wii_cue(
|
|
slot_index, now_ms, local_feedback_active,
|
|
profile_rumble_dispatch || feedback_dispatch, &wii_cue_dispatch);
|
|
#endif
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
const bool native_owns_rumble = prepare_native_cues(
|
|
slot_index, now_ms, local_feedback_active,
|
|
profile_rumble_dispatch || feedback_dispatch, &native_dispatch);
|
|
#endif
|
|
if (!profile_rumble_dispatch && !feedback_dispatch &&
|
|
!local_feedback_active && slot.rumble_pending
|
|
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
|
&& !wii_cue_owns_rumble
|
|
#endif
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
&& !native_owns_rumble
|
|
#endif
|
|
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
|
&& !(xinput_host_mode &&
|
|
haptics_experiment_gameplay_owns(slot.device))
|
|
#endif
|
|
) {
|
|
envelope = slot.pending_rumble;
|
|
slot.rumble_pending = false;
|
|
host_dispatch =
|
|
envelope.slot == slot_index && slot.active &&
|
|
envelope.duration_ms == host_rumble_duration_ms() &&
|
|
slot.device != nullptr &&
|
|
envelope.connection_generation ==
|
|
slot.connection_generation;
|
|
if (host_dispatch) {
|
|
device = slot.device;
|
|
}
|
|
}
|
|
companion = slot.companion;
|
|
dispatch_generation = slot.connection_generation;
|
|
state_lock_exit();
|
|
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
|
|
if (host_dispatch && switch_native_output_owns(device))
|
|
host_dispatch = false; // The timestamped native queue already owns this command.
|
|
#endif
|
|
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
|
if (host_dispatch && haptics_experiment_gameplay_owns(device)) {
|
|
// Switch commands have already entered the timestamped timeline.
|
|
// Consume their finite fallback, never turn it into a PCM overlay
|
|
// or emit compatibility reports while gameplay owns the device.
|
|
host_dispatch = false;
|
|
}
|
|
#endif
|
|
|
|
uni_hid_device_t* lighting_targets[] = {
|
|
profile_lighting_device, companion};
|
|
for (uni_hid_device_t* target : lighting_targets) {
|
|
if (!lighting_target_is_current(
|
|
slot_index, profile_lighting_generation, target)) {
|
|
continue;
|
|
}
|
|
if (profile_lighting_restore) {
|
|
apply_slot_lighting(slot_index, target);
|
|
}
|
|
if (profile_lighting_dispatch) {
|
|
apply_profile_lighting(
|
|
profile_feedback.active_profile_number, target);
|
|
}
|
|
}
|
|
uni_hid_device_t* rumble_targets[] = {device, companion};
|
|
for (uni_hid_device_t* target : rumble_targets) {
|
|
if (!lighting_target_is_current(
|
|
slot_index, dispatch_generation, target) ||
|
|
target->report_parser.play_dual_rumble == nullptr) {
|
|
continue;
|
|
}
|
|
if (profile_rumble_dispatch) {
|
|
__atomic_add_fetch(
|
|
&g_rumble_dispatches, 1, __ATOMIC_RELAXED);
|
|
dispatch_rumble(
|
|
target, kProfileFeedbackPhaseDurationMs,
|
|
kProfileFeedbackWeakMagnitude, kProfileFeedbackStrongMagnitude);
|
|
} else if (feedback_dispatch) {
|
|
__atomic_add_fetch(
|
|
&g_rumble_dispatches, 1, __ATOMIC_RELAXED);
|
|
dispatch_rumble(
|
|
target, feedback.duration_ms,
|
|
feedback.weak_magnitude, feedback.strong_magnitude);
|
|
} else if (host_dispatch) {
|
|
__atomic_add_fetch(
|
|
&g_rumble_dispatches, 1, __ATOMIC_RELAXED);
|
|
dispatch_host_rumble(target, envelope.duration_ms, envelope.rumble);
|
|
}
|
|
}
|
|
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
|
dispatch_wii_cue(wii_cue_dispatch);
|
|
#endif
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
dispatch_native_cues(native_dispatch);
|
|
#endif
|
|
}
|
|
|
|
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 forget_joycon_pair_hint(uni_hid_device_t* device) {
|
|
const int physical_index = physical_index_for_device(device);
|
|
if (physical_index < 0) return;
|
|
for (JoyConPairHint& hint : g_joycon_pair_hints) {
|
|
if (hint.mate == device) hint = {};
|
|
}
|
|
g_joycon_pair_hints[physical_index] = {};
|
|
}
|
|
|
|
// Replacing an explicit association does not revoke its former partner's solo
|
|
// choice. Only the new current association participates in disconnect reset.
|
|
void set_joycon_override(uni_hid_device_t* left, uni_hid_device_t* right,
|
|
JoyConGroupingOverride mode) {
|
|
uni_hid_device_t* devices[] = {left, right};
|
|
for (uni_hid_device_t* device : devices) {
|
|
for (JoyConConnectionOverride& current : g_joycon_overrides) {
|
|
if (current.mate == device) current.mate = nullptr;
|
|
}
|
|
}
|
|
g_joycon_overrides[physical_index_for_device(left)] = {mode, right};
|
|
g_joycon_overrides[physical_index_for_device(right)] = {mode, left};
|
|
}
|
|
|
|
// Caller holds the state lock; used for disconnect and physical index reuse.
|
|
void reset_joycon_connection(uni_hid_device_t* device) {
|
|
const int index = physical_index_for_device(device);
|
|
if (index < 0) return;
|
|
bool changed = g_joycon_overrides[index].mode !=
|
|
JoyConGroupingOverride::Default;
|
|
for (JoyConConnectionOverride& current : g_joycon_overrides) {
|
|
if (current.mate == device) {
|
|
current = {};
|
|
changed = true;
|
|
}
|
|
}
|
|
g_joycon_overrides[index] = {};
|
|
if (changed) g_joycon_reconcile_requested = true;
|
|
block_joycon_gesture(g_joycon_gestures[index].participants);
|
|
g_joycon_gestures[index] = {};
|
|
for (JoyConGesture& gesture : g_joycon_gestures) {
|
|
gesture.participants &= ~(1u << index);
|
|
}
|
|
forget_joycon_pair_hint(device);
|
|
}
|
|
|
|
bool joycon_gesture_mature(uni_hid_device_t* first,
|
|
uni_hid_device_t* second, bool joining,
|
|
uint32_t now_ms) {
|
|
const int first_index = physical_index_for_device(first);
|
|
const int second_index = physical_index_for_device(second);
|
|
if (first_index < 0 || second_index < 0 || first_index == second_index ||
|
|
joycon_side(first) == 0 || joycon_side(first) != -joycon_side(second)) {
|
|
return false;
|
|
}
|
|
const uint8_t participants = (1u << first_index) | (1u << second_index);
|
|
const JoyConGesture& a = g_joycon_gestures[first_index];
|
|
const JoyConGesture& b = g_joycon_gestures[second_index];
|
|
if (a.device != first || b.device != second ||
|
|
a.started_ms != b.started_ms) return false;
|
|
const int indices[] = {first_index, second_index};
|
|
for (int index : indices) {
|
|
const JoyConGesture& gesture = g_joycon_gestures[index];
|
|
if (!joycon_gesture_live(index) ||
|
|
gesture.participants != participants ||
|
|
gesture.joining != joining || !gesture.held ||
|
|
now_ms - gesture.last_report_ms > kJoyConGestureFreshMs ||
|
|
static_cast<int32_t>(gesture.last_report_ms - gesture.started_ms) <
|
|
static_cast<int32_t>(kJoyConGestureHoldMs)) return false;
|
|
}
|
|
const int a_slot = slot_for_device(first);
|
|
const int b_slot = slot_for_device(second);
|
|
return joining
|
|
? a_slot != b_slot && g_slots[a_slot].companion == nullptr &&
|
|
g_slots[b_slot].companion == nullptr
|
|
: a_slot == b_slot && g_slots[a_slot].companion != nullptr;
|
|
}
|
|
|
|
// Caller holds the state lock. Prefer the last live pair's exact members,
|
|
// otherwise preserve the existing first-ready / lowest-slot admission order.
|
|
int joycon_partner_slot(uni_hid_device_t* device, int slot_index) {
|
|
const int side = joycon_side(device);
|
|
if (side == 0 || !joycon_default_pairing_allowed(device)) return -1;
|
|
const auto& hint = g_joycon_pair_hints[physical_index_for_device(device)];
|
|
int first = -1;
|
|
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
|
const BackendSlot& candidate = g_slots[index];
|
|
if (index == slot_index || !candidate.active ||
|
|
candidate.companion != nullptr ||
|
|
!joycon_default_pairing_allowed(candidate.device) ||
|
|
joycon_side(candidate.device) != -side) continue;
|
|
if (candidate.device == hint.mate) return index;
|
|
const auto& candidate_hint =
|
|
g_joycon_pair_hints[physical_index_for_device(candidate.device)];
|
|
if (candidate_hint.mate != nullptr && candidate_hint.mate != device) continue;
|
|
if (first < 0) first = index;
|
|
}
|
|
return first;
|
|
}
|
|
|
|
void stop_joycon_output(uni_hid_device_t* device) {
|
|
if (device->report_parser.play_dual_rumble != nullptr) {
|
|
dispatch_rumble(device, 0, 0, 0);
|
|
}
|
|
}
|
|
|
|
// BTstack only; shared by ready admission, saved defaults and explicit gestures.
|
|
// Pair enrollment is atomic and idempotent, and always precedes topology
|
|
// changes with no cross-core input lock held during storage I/O.
|
|
bool merge_joycon_slots(int owner_index, int joining_index,
|
|
uni_hid_device_t* joining_device,
|
|
bool gesture = false) {
|
|
state_lock_enter();
|
|
BackendSlot& owner = g_slots[owner_index];
|
|
BackendSlot& joining = g_slots[joining_index];
|
|
uni_hid_device_t* const owner_device = owner.device;
|
|
const uint32_t owner_generation = owner.connection_generation;
|
|
const uint32_t joining_generation = joining.connection_generation;
|
|
const bool joining_active = joining.active;
|
|
const int side = joycon_side(joining_device);
|
|
const bool admission = gesture
|
|
? joining.active && joycon_gesture_mature(
|
|
owner_device, joining_device, true, btstack_run_loop_get_time_ms())
|
|
: joycon_default_pairing_allowed(owner_device) &&
|
|
joycon_default_pairing_allowed(joining_device);
|
|
const bool eligible = admission &&
|
|
owner.active && owner.companion == nullptr &&
|
|
joining.companion == nullptr && side != 0 &&
|
|
joycon_side(owner_device) == -side &&
|
|
reserve_device_slot(joining_device) == joining_index;
|
|
const ControllerIdentity owner_identity = identity_for_device(owner_device);
|
|
const ControllerIdentity joining_identity = identity_for_device(joining_device);
|
|
state_lock_exit();
|
|
ControllerIdentity pair_identity{};
|
|
if (!eligible ||
|
|
!controller_identity_make_joycon_pair(
|
|
side < 0 ? joining_identity : owner_identity,
|
|
side < 0 ? owner_identity : joining_identity, &pair_identity) ||
|
|
!profile_service_observe_joycon_pair_on_storage_core(pair_identity)) {
|
|
return false;
|
|
}
|
|
|
|
state_lock_enter();
|
|
const bool still_admitted = gesture
|
|
? joycon_gesture_mature(
|
|
owner_device, joining_device, true, btstack_run_loop_get_time_ms())
|
|
: joycon_default_pairing_allowed(owner_device) &&
|
|
joycon_default_pairing_allowed(joining_device);
|
|
if (!still_admitted ||
|
|
!owner.active || owner.device != owner_device ||
|
|
owner.companion != nullptr ||
|
|
owner.connection_generation != owner_generation ||
|
|
joining.active != joining_active || joining.companion != nullptr ||
|
|
joining.connection_generation != joining_generation ||
|
|
reserve_device_slot(joining_device) != joining_index) {
|
|
state_lock_exit();
|
|
return false;
|
|
}
|
|
invalidate_slot(owner);
|
|
invalidate_slot(joining);
|
|
// Clear both parser epochs and local motor feedback before publishing
|
|
// either the new pair or its neutral retired output.
|
|
stop_joycon_output(owner_device);
|
|
stop_joycon_output(joining_device);
|
|
if (side < 0) {
|
|
owner.companion = owner.device;
|
|
owner.companion_gamepad = owner.gamepad;
|
|
owner.companion_extra_buttons = owner.extra_buttons;
|
|
owner.device = joining_device;
|
|
owner.gamepad = joining.gamepad;
|
|
owner.extra_buttons = joining.extra_buttons;
|
|
} else {
|
|
owner.companion = joining_device;
|
|
owner.companion_gamepad = joining.gamepad;
|
|
owner.companion_extra_buttons = joining.extra_buttons;
|
|
}
|
|
owner.identity = pair_identity;
|
|
refresh_topology_input(owner);
|
|
joining.identity = controller_identity_global();
|
|
joining.device = nullptr;
|
|
joining.gamepad = {};
|
|
joining.extra_buttons = 0;
|
|
joining.active = false;
|
|
forget_joycon_pair_hint(owner.device);
|
|
forget_joycon_pair_hint(owner.companion);
|
|
if (gesture) {
|
|
set_joycon_override(owner.device, owner.companion,
|
|
JoyConGroupingOverride::Paired);
|
|
queue_local_feedback(owner, kJoyConGestureFeedbackMs,
|
|
kProfileFeedbackWeakMagnitude,
|
|
kProfileFeedbackStrongMagnitude);
|
|
}
|
|
g_joycon_pair_hints[physical_index_for_device(owner.device)] =
|
|
{owner.companion, static_cast<uint8_t>(owner_index)};
|
|
g_joycon_pair_hints[physical_index_for_device(owner.companion)] =
|
|
{owner.device, static_cast<uint8_t>(owner_index)};
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
refresh_native_source_locked();
|
|
#endif
|
|
state_lock_exit();
|
|
apply_slot_lighting(static_cast<uint8_t>(owner_index), owner.device);
|
|
apply_slot_lighting(static_cast<uint8_t>(owner_index), owner.companion);
|
|
return true;
|
|
}
|
|
|
|
bool split_joycon_slot(uint8_t owner_index, bool gesture = false) {
|
|
state_lock_enter();
|
|
BackendSlot& owner = g_slots[owner_index];
|
|
const int physical_index = physical_index_for_device(owner.device);
|
|
if (!owner.active || owner.companion == nullptr || physical_index < 0 ||
|
|
(!gesture && g_joycon_overrides[physical_index].mode ==
|
|
JoyConGroupingOverride::Paired) ||
|
|
(gesture && !joycon_gesture_mature(
|
|
owner.device, owner.companion, false,
|
|
btstack_run_loop_get_time_ms()))) {
|
|
state_lock_exit();
|
|
return false;
|
|
}
|
|
// Keep the pair's left member at its existing player index. Prefer the
|
|
// right member's physical index, falling back to the lowest free output.
|
|
int right_index = physical_index_for_device(owner.companion);
|
|
if (right_index < 0 || g_slots[right_index].device != nullptr) {
|
|
right_index = -1;
|
|
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
|
if (g_slots[index].device == nullptr) {
|
|
right_index = index;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (right_index < 0) {
|
|
state_lock_exit();
|
|
return false;
|
|
}
|
|
BackendSlot& right = g_slots[right_index];
|
|
ControllerIdentity left_identity{};
|
|
ControllerIdentity right_identity{};
|
|
// The enrolled owner is authoritative even if a later identity-resolution
|
|
// event has temporarily cleared a member's transport mapping.
|
|
if (!controller_identity_joycon_pair_members(
|
|
owner.identity, &left_identity, &right_identity)) {
|
|
state_lock_exit();
|
|
return false;
|
|
}
|
|
invalidate_slot(owner);
|
|
invalidate_slot(right);
|
|
stop_joycon_output(owner.device);
|
|
stop_joycon_output(owner.companion);
|
|
right.device = owner.companion;
|
|
right.identity = right_identity;
|
|
right.gamepad = owner.companion_gamepad;
|
|
right.extra_buttons = owner.companion_extra_buttons;
|
|
right.active = true;
|
|
owner.identity = left_identity;
|
|
owner.companion = nullptr;
|
|
owner.companion_gamepad = {};
|
|
owner.companion_extra_buttons = 0;
|
|
refresh_topology_input(owner);
|
|
refresh_topology_input(right);
|
|
if (gesture) {
|
|
set_joycon_override(owner.device, right.device,
|
|
JoyConGroupingOverride::Individual);
|
|
queue_local_feedback(owner, kJoyConGestureFeedbackMs,
|
|
kProfileFeedbackWeakMagnitude,
|
|
kProfileFeedbackStrongMagnitude);
|
|
queue_local_feedback(right, kJoyConGestureFeedbackMs,
|
|
kProfileFeedbackWeakMagnitude,
|
|
kProfileFeedbackStrongMagnitude);
|
|
}
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
refresh_native_source_locked();
|
|
#endif
|
|
state_lock_exit();
|
|
apply_slot_lighting(owner_index, owner.device);
|
|
apply_slot_lighting(static_cast<uint8_t>(right_index), right.device);
|
|
return true;
|
|
}
|
|
|
|
void process_joycon_gestures(uint32_t now_ms) {
|
|
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
|
now_ms = btstack_run_loop_get_time_ms();
|
|
state_lock_enter();
|
|
refresh_joycon_gestures(now_ms);
|
|
const JoyConGesture& gesture = g_joycon_gestures[index];
|
|
if (gesture.blocked || joycon_side(gesture.device) >= 0) {
|
|
state_lock_exit();
|
|
continue;
|
|
}
|
|
uni_hid_device_t* right = nullptr;
|
|
for (uint8_t mate = 0; mate < kSlotCount; ++mate) {
|
|
if (mate != index && (gesture.participants & (1u << mate))) {
|
|
right = g_joycon_gestures[mate].device;
|
|
}
|
|
}
|
|
const bool joining = gesture.joining;
|
|
const int owner_slot = slot_for_device(gesture.device);
|
|
const int right_slot = slot_for_device(right);
|
|
const bool mature = joycon_gesture_mature(
|
|
gesture.device, right, joining, now_ms);
|
|
if (mature) block_joycon_gesture(gesture.participants);
|
|
state_lock_exit();
|
|
if (!mature) continue;
|
|
// Latch success AND failure before any enrollment I/O. A failed seed
|
|
// must not retry at the timer cadence or undo either participant.
|
|
// Player-slot ownership is independent of physical handedness.
|
|
// Keep the lower occupied slot, even when the left half owns the higher one.
|
|
const bool keep_left_slot = owner_slot < right_slot;
|
|
const bool changed = joining
|
|
? merge_joycon_slots(
|
|
keep_left_slot ? owner_slot : right_slot,
|
|
keep_left_slot ? right_slot : owner_slot,
|
|
keep_left_slot ? right : gesture.device, true)
|
|
: split_joycon_slot(static_cast<uint8_t>(owner_slot), true);
|
|
if (changed) recompute_connection_status();
|
|
}
|
|
}
|
|
|
|
void apply_joycon_configuration(const ConfigurationServiceSnapshot& configuration) {
|
|
if (configuration.state != ConfigurationServiceState::kReady) return;
|
|
const JoyConMode requested = configuration.configuration.joycon_mode;
|
|
if (requested != g_joycon_mode) {
|
|
state_lock_enter();
|
|
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
|
g_joycon_overrides[index] = {};
|
|
block_joycon_gesture(g_joycon_gestures[index].participants);
|
|
}
|
|
g_joycon_mode = requested;
|
|
g_joycon_reconcile_requested = true;
|
|
state_lock_exit();
|
|
}
|
|
if (!g_joycon_reconcile_requested) return;
|
|
g_joycon_reconcile_requested = false;
|
|
if (g_joycon_mode == JoyConMode::kIndividual) {
|
|
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
|
split_joycon_slot(index);
|
|
}
|
|
} else {
|
|
uint8_t attempted = 0;
|
|
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
|
state_lock_enter();
|
|
const BackendSlot& slot = g_slots[index];
|
|
int partner = slot.active && slot.companion == nullptr &&
|
|
!(attempted & (1u << index))
|
|
? joycon_partner_slot(slot.device, index) : -1;
|
|
if (partner < 0 || (attempted & (1u << partner))) {
|
|
state_lock_exit();
|
|
continue;
|
|
}
|
|
int owner = index;
|
|
int joining = partner;
|
|
const auto& hint =
|
|
g_joycon_pair_hints[physical_index_for_device(slot.device)];
|
|
if (hint.mate == g_slots[partner].device && hint.owner_slot == partner) {
|
|
owner = partner;
|
|
joining = index;
|
|
}
|
|
uni_hid_device_t* joining_device = g_slots[joining].device;
|
|
attempted |= (1u << index) | (1u << partner);
|
|
state_lock_exit();
|
|
// Failed seeds/invalid identities leave both live solos intact.
|
|
// Retry only on a mode change or fresh identity/ready event, never
|
|
// at the 50 ms poll cadence or for unrelated configuration edits.
|
|
merge_joycon_slots(owner, joining, joining_device);
|
|
}
|
|
}
|
|
recompute_connection_status();
|
|
}
|
|
|
|
void platform_init(int argc, const char** argv) {
|
|
(void)argc;
|
|
(void)argv;
|
|
}
|
|
|
|
void platform_on_init_complete() {
|
|
if (SWITCH_PICO_ENABLE_CLASSIC) {
|
|
gap_set_link_supervision_timeout(kClassicLinkSupervisionTimeout);
|
|
gap_ssp_set_auto_accept(false);
|
|
g_pairing_event_callback.callback = handle_btstack_event;
|
|
hci_add_event_handler(&g_pairing_event_callback);
|
|
}
|
|
gap_set_bondable_mode(false);
|
|
if (SWITCH_PICO_ENABLE_BLE) {
|
|
// Bluepad32 does not initialize SM in Classic-only mode.
|
|
sm_set_accepted_stk_generation_methods(0);
|
|
g_identity_event_callback.callback = handle_btstack_event;
|
|
sm_add_event_handler(&g_identity_event_callback);
|
|
}
|
|
switch2_wake_initialize();
|
|
refresh_pairing_snapshot();
|
|
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);
|
|
btstack_run_loop_set_timer_handler(
|
|
&g_configuration_timer, process_configuration_timer);
|
|
btstack_run_loop_set_timer(
|
|
&g_configuration_timer, kConfigurationPollIntervalMs);
|
|
btstack_run_loop_add_timer(&g_configuration_timer);
|
|
__atomic_store_n(&g_initialization_stage, 6, __ATOMIC_RELEASE);
|
|
ConfigurationServiceSnapshot configuration{};
|
|
configuration_service_snapshot(&configuration);
|
|
apply_joycon_configuration(configuration);
|
|
if (switch2_wake_ready_for_connections()) {
|
|
recompute_connection_status();
|
|
}
|
|
}
|
|
|
|
bool device_transport_enabled(const uni_hid_device_t* device) {
|
|
if (device == nullptr) {
|
|
return false;
|
|
}
|
|
if (SWITCH_PICO_ENABLE_BLE && SWITCH_PICO_ENABLE_CLASSIC) {
|
|
return true;
|
|
}
|
|
// GAP describes the actual live link. The protocol hint is also available
|
|
// before an outgoing connection has an HCI handle.
|
|
switch (gap_get_connection_type(device->conn.handle)) {
|
|
case GAP_CONNECTION_ACL:
|
|
return SWITCH_PICO_ENABLE_CLASSIC != 0;
|
|
case GAP_CONNECTION_LE:
|
|
return SWITCH_PICO_ENABLE_BLE != 0;
|
|
default:
|
|
break;
|
|
}
|
|
switch (device->conn.protocol) {
|
|
case UNI_BT_CONN_PROTOCOL_BR_EDR:
|
|
return SWITCH_PICO_ENABLE_CLASSIC != 0;
|
|
case UNI_BT_CONN_PROTOCOL_BLE:
|
|
return SWITCH_PICO_ENABLE_BLE != 0;
|
|
default:
|
|
// Preserve mixed-mode admission; single-transport builds cannot
|
|
// safely admit a connection whose transport is still unknown.
|
|
return SWITCH_PICO_ENABLE_BLE && SWITCH_PICO_ENABLE_CLASSIC;
|
|
}
|
|
}
|
|
|
|
uni_error_t platform_on_device_discovered(bd_addr_t addr, const char* name,
|
|
uint16_t cod, uint8_t rssi) {
|
|
(void)name;
|
|
(void)cod;
|
|
(void)rssi;
|
|
if (!has_free_slot()) {
|
|
return UNI_ERROR_IGNORE_DEVICE;
|
|
}
|
|
const uni_hid_device_t* candidate =
|
|
(g_connection_policy_state == ConnectionPolicyState::Passive ||
|
|
!SWITCH_PICO_ENABLE_BLE || !SWITCH_PICO_ENABLE_CLASSIC)
|
|
? uni_hid_device_get_instance_for_address(addr)
|
|
: nullptr;
|
|
if (candidate != nullptr && !device_transport_enabled(candidate)) {
|
|
return UNI_ERROR_IGNORE_DEVICE;
|
|
}
|
|
// First discovery can precede device creation, so the transport-specific
|
|
// Bluepad32 discovery handlers must enforce the mode before this callback.
|
|
if (g_connection_policy_state == ConnectionPolicyState::Open) {
|
|
return UNI_ERROR_SUCCESS;
|
|
}
|
|
if (!SWITCH_PICO_ENABLE_BLE ||
|
|
g_connection_policy_state != ConnectionPolicyState::Passive) {
|
|
return UNI_ERROR_IGNORE_DEVICE;
|
|
}
|
|
const int side = joycon_side(candidate);
|
|
uint8_t address_type = BD_ADDR_TYPE_UNKNOWN;
|
|
return side != 0 && waiting_for_joycon_mate(side) &&
|
|
uni_hid_parser_switch2_identity_address_type(
|
|
candidate, &address_type) &&
|
|
uni_switch2_pairing_known(address_type, addr)
|
|
? UNI_ERROR_SUCCESS
|
|
: UNI_ERROR_IGNORE_DEVICE;
|
|
}
|
|
|
|
void platform_on_device_connected(uni_hid_device_t* device) {
|
|
if (device == nullptr) {
|
|
return;
|
|
}
|
|
if (!device_transport_enabled(device) ||
|
|
(g_connection_policy_state != ConnectionPolicyState::Open &&
|
|
g_connection_policy_state != ConnectionPolicyState::Passive)) {
|
|
uni_hid_device_disconnect(device);
|
|
return;
|
|
}
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
// Classification completes after connection. Reserve transport capacity,
|
|
// not a player/color slot, until a supported source reaches ready.
|
|
const int pending_index = physical_index_for_device(device);
|
|
if (pending_index < 0) {
|
|
uni_hid_device_disconnect(device);
|
|
return;
|
|
}
|
|
state_lock_enter();
|
|
g_retired_devices[pending_index] = nullptr;
|
|
g_switch2_interval_requests[pending_index] = {};
|
|
if (slot_for_device(device) < 0) {
|
|
g_native_reports[pending_index] = {};
|
|
reset_joycon_connection(device);
|
|
}
|
|
if (slot_for_device(device) < 0) g_native_pending_devices[pending_index] = device;
|
|
state_lock_exit();
|
|
recompute_connection_status();
|
|
#else
|
|
const ControllerIdentity connection_identity = identity_for_device(device);
|
|
state_lock_enter();
|
|
const int physical_index = physical_index_for_device(device);
|
|
if (physical_index >= 0) {
|
|
g_retired_devices[physical_index] = nullptr;
|
|
g_switch2_interval_requests[physical_index] = {};
|
|
if (slot_for_device(device) < 0) reset_joycon_connection(device);
|
|
}
|
|
const int slot_index = reserve_device_slot(device);
|
|
if (slot_index >= 0) {
|
|
BackendSlot& slot = g_slots[slot_index];
|
|
if (slot.device == nullptr) {
|
|
slot.device = device;
|
|
slot.identity = connection_identity;
|
|
slot.rumble_pending = false;
|
|
reset_slot_hotkeys(slot);
|
|
}
|
|
}
|
|
state_lock_exit();
|
|
if (slot_index >= 0) {
|
|
recompute_connection_status();
|
|
} else {
|
|
uni_hid_device_disconnect(device);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void platform_on_device_disconnected(uni_hid_device_t* device) {
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
const int pending_index = physical_index_for_device(device);
|
|
if (pending_index >= 0) {
|
|
state_lock_enter();
|
|
if (g_native_pending_devices[pending_index] == device)
|
|
g_native_pending_devices[pending_index] = nullptr;
|
|
g_retired_devices[pending_index] = device;
|
|
state_lock_exit();
|
|
}
|
|
#endif
|
|
const int slot_index = slot_for_device(device);
|
|
if (slot_index < 0) {
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
recompute_connection_status();
|
|
#endif
|
|
return;
|
|
}
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
// DS4/PSMove and other finite-rumble drivers keep timers in parser_data.
|
|
// Retire those timers before Bluepad32 reuses that memory. Call the real
|
|
// driver directly: a feedback scheduler must not defer this local teardown.
|
|
if (device->report_parser.play_dual_rumble != nullptr)
|
|
device->report_parser.play_dual_rumble(device, 0, 0, 0, 0);
|
|
#endif
|
|
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
|
|
switch_native_output_detach(device);
|
|
#endif
|
|
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
|
haptics_experiment_detach(device);
|
|
#endif
|
|
uni_hid_device_t* survivor = nullptr;
|
|
ControllerIdentity survivor_identity{};
|
|
state_lock_enter();
|
|
BackendSlot& slot = g_slots[slot_index];
|
|
g_retired_devices[physical_index_for_device(device)] = device;
|
|
reset_joycon_connection(device);
|
|
if (slot.companion != nullptr) {
|
|
invalidate_slot(slot);
|
|
if (slot.device == device) {
|
|
slot.device = slot.companion;
|
|
slot.gamepad = slot.companion_gamepad;
|
|
slot.extra_buttons = slot.companion_extra_buttons;
|
|
}
|
|
slot.companion = nullptr;
|
|
slot.companion_gamepad = {};
|
|
slot.companion_extra_buttons = 0;
|
|
survivor = slot.device;
|
|
slot.identity = identity_for_device(survivor);
|
|
survivor_identity = slot.identity;
|
|
refresh_topology_input(slot);
|
|
} else {
|
|
release_slot(slot);
|
|
}
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
refresh_native_source_locked();
|
|
#endif
|
|
state_lock_exit();
|
|
clear_ble_identity_for_device(device);
|
|
if (survivor != nullptr) {
|
|
if (survivor->report_parser.play_dual_rumble != nullptr) {
|
|
dispatch_rumble(survivor, 0, 0, 0);
|
|
}
|
|
apply_slot_lighting(static_cast<uint8_t>(slot_index), survivor);
|
|
if (survivor_identity.stable) {
|
|
profile_service_observe_identity_on_storage_core(survivor_identity);
|
|
}
|
|
}
|
|
// Losing a half frees transport capacity, not another logical player.
|
|
if (g_connection_policy_state != ConnectionPolicyState::FailedClosed) {
|
|
g_connection_policy_state = ConnectionPolicyState::Uninitialized;
|
|
}
|
|
recompute_connection_status();
|
|
}
|
|
|
|
uni_error_t platform_on_device_ready(uni_hid_device_t* device) {
|
|
if (!device_transport_enabled(device) ||
|
|
!uni_hid_device_is_gamepad(device)) {
|
|
return UNI_ERROR_INVALID_CONTROLLER;
|
|
}
|
|
if (g_connection_policy_state == ConnectionPolicyState::FailedClosed) {
|
|
return UNI_ERROR_NO_SLOTS;
|
|
}
|
|
|
|
bool became_active = false;
|
|
bool paired = false;
|
|
uint32_t lighting_generation = 0;
|
|
uni_hid_device_t* owner = device;
|
|
uni_hid_device_t* companion = nullptr;
|
|
ControllerIdentity connection_identity = identity_for_device(device);
|
|
state_lock_enter();
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
if (!native_device_allowed(device)) {
|
|
state_lock_exit();
|
|
return UNI_ERROR_INVALID_CONTROLLER;
|
|
}
|
|
#endif
|
|
int slot_index = reserve_device_slot(device);
|
|
if (slot_index < 0) {
|
|
state_lock_exit();
|
|
return UNI_ERROR_NO_SLOTS;
|
|
}
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
const int pending_index = physical_index_for_device(device);
|
|
if (g_native_pending_devices[pending_index] == device)
|
|
g_native_pending_devices[pending_index] = nullptr;
|
|
#endif
|
|
BackendSlot& pending = g_slots[slot_index];
|
|
if (!pending.active) {
|
|
const int partner_index = joycon_partner_slot(device, slot_index);
|
|
if (partner_index >= 0) {
|
|
state_lock_exit();
|
|
if (!merge_joycon_slots(partner_index, slot_index, device)) {
|
|
return UNI_ERROR_INIT_FAILED;
|
|
}
|
|
state_lock_enter();
|
|
slot_index = partner_index;
|
|
paired = true;
|
|
} else {
|
|
pending.identity = connection_identity;
|
|
pending.device = device;
|
|
pending.state = make_neutral_state();
|
|
pending.active = true;
|
|
pending.rumble_pending = false;
|
|
reset_slot_hotkeys(pending);
|
|
++pending.state_generation;
|
|
}
|
|
became_active = true;
|
|
}
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
refresh_native_source_locked();
|
|
#endif
|
|
const BackendSlot& current = g_slots[slot_index];
|
|
owner = current.device;
|
|
companion = current.companion;
|
|
lighting_generation = current.connection_generation;
|
|
connection_identity = current.identity;
|
|
state_lock_exit();
|
|
if (became_active) {
|
|
apply_radio_connection_policy();
|
|
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
|
|
if (!paired) {
|
|
switch_native_output_attach(static_cast<uint8_t>(slot_index),
|
|
lighting_generation, device, connection_identity);
|
|
}
|
|
#endif
|
|
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
|
if (!paired && !uni_hid_parser_switch2_is_ble_device(device)) {
|
|
haptics_experiment_attach(
|
|
static_cast<uint8_t>(slot_index), lighting_generation, device);
|
|
}
|
|
#ifdef SWITCH_PICO_HD_RUMBLE
|
|
if (connection_identity.vendor_id == 0x054c &&
|
|
(connection_identity.product_id == 0x0ce6 ||
|
|
connection_identity.product_id == 0x0df2)) {
|
|
haptics_experiment_request(2, static_cast<uint8_t>(slot_index));
|
|
}
|
|
#endif
|
|
#endif
|
|
if (!paired && lighting_target_is_current(
|
|
static_cast<uint8_t>(slot_index), lighting_generation, owner)) {
|
|
apply_slot_lighting(static_cast<uint8_t>(slot_index), owner);
|
|
if (companion != nullptr) {
|
|
apply_slot_lighting(static_cast<uint8_t>(slot_index), companion);
|
|
}
|
|
}
|
|
if (!paired && connection_identity.stable) {
|
|
profile_service_observe_identity_on_storage_core(
|
|
connection_identity);
|
|
}
|
|
if (!paired && joycon_side(device) != 0 &&
|
|
g_joycon_mode == JoyConMode::kPaired) {
|
|
g_joycon_reconcile_requested = true;
|
|
}
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
__atomic_add_fetch(&g_controller_reports, 1, __ATOMIC_RELAXED);
|
|
|
|
// Parser state belongs to this serialized Bluepad32 callback, not to
|
|
// g_state_lock. Read it before masking IRQs so USB completions can run
|
|
// while snapshot helpers execute (including cold XIP fetches).
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
const int physical_index = physical_index_for_device(device);
|
|
if (physical_index < 0) return;
|
|
uni_native_motion_snapshot_t sensor{};
|
|
uni_hid_parser_native_motion_snapshot(device, &sensor);
|
|
#endif
|
|
const uint8_t extras = uni_hid_parser_switch2_extra_buttons(device);
|
|
const bool is_wii = device->controller_type == CONTROLLER_TYPE_WiiController;
|
|
int32_t acceleration[3];
|
|
uint32_t sequence = 0;
|
|
const bool have_acceleration = is_wii &&
|
|
uni_hid_parser_wii_accel_snapshot(device, acceleration, &sequence);
|
|
int32_t nunchuk_acceleration[3];
|
|
uint32_t nunchuk_sequence = 0;
|
|
const bool have_nunchuk_acceleration = is_wii &&
|
|
uni_hid_parser_wii_nunchuk_accel_snapshot(
|
|
device, nunchuk_acceleration, &nunchuk_sequence);
|
|
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
|
int32_t gyro[3];
|
|
uint32_t gyro_sequence = 0;
|
|
const bool have_gyro = is_wii &&
|
|
uni_hid_parser_wii_gyro_snapshot(device, gyro, &gyro_sequence);
|
|
#endif
|
|
|
|
const uint32_t report_ms = btstack_run_loop_get_time_ms();
|
|
state_lock_enter();
|
|
BackendSlot& slot = g_slots[slot_index];
|
|
if (!slot.active) {
|
|
state_lock_exit();
|
|
return;
|
|
}
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
NativeGamepadReportIngress& ingress = g_native_reports[physical_index];
|
|
if (ingress.device != device) ingress = {device, 0, 0, 0};
|
|
if (sensor.report_tracked &&
|
|
(!sensor.report_valid || sensor.report_sequence == ingress.report_sequence)) {
|
|
state_lock_exit();
|
|
return;
|
|
}
|
|
ingress.report_sequence = sensor.report_sequence;
|
|
NativeGamepadIngress& motion = slot.native_motion;
|
|
motion.has_report = true;
|
|
motion.received_us = time_us_32();
|
|
// Read only the reporting physical device. The right half is the existing
|
|
// pair's motion owner; left controls must not refresh or invalidate its IMU.
|
|
if (slot.companion == nullptr || slot.companion == device) {
|
|
if (!sensor.accel_valid) motion.accel_valid = false;
|
|
else if (sensor.accel_sequence != ingress.accel_sequence) {
|
|
motion.accel_valid = true;
|
|
motion.accel_sequence = sensor.accel_sequence;
|
|
motion.accel_received_us = motion.received_us;
|
|
memcpy(motion.accel_q13, sensor.accel_q13, sizeof(motion.accel_q13));
|
|
}
|
|
if (!sensor.gyro_valid) motion.gyro_valid = false;
|
|
else if (sensor.gyro_sequence != ingress.gyro_sequence) {
|
|
motion.gyro_valid = true;
|
|
motion.gyro_sequence = sensor.gyro_sequence;
|
|
motion.gyro_received_us = motion.received_us;
|
|
memcpy(motion.gyro_q10, sensor.gyro_q10, sizeof(motion.gyro_q10));
|
|
}
|
|
}
|
|
if (sensor.accel_valid) ingress.accel_sequence = sensor.accel_sequence;
|
|
if (sensor.gyro_valid) ingress.gyro_sequence = sensor.gyro_sequence;
|
|
#endif
|
|
#ifdef SWITCH_PICO_WII_IR
|
|
uni_wii_ir_snapshot_t infrared{};
|
|
const bool have_infrared =
|
|
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
|
is_selected_wii(slot) && slot.device == device &&
|
|
#endif
|
|
uni_hid_parser_wii_ir_snapshot(device, &infrared);
|
|
#ifdef SWITCH_PICO_WII_IR_GYRO
|
|
observe_wii_aim_chord(
|
|
slot, device, controller->gamepad,
|
|
have_infrared ? &infrared : nullptr, time_us_32());
|
|
#endif
|
|
#endif
|
|
if (slot.companion == device) {
|
|
slot.companion_gamepad = controller->gamepad;
|
|
slot.companion_extra_buttons = extras;
|
|
} else {
|
|
slot.gamepad = controller->gamepad;
|
|
slot.extra_buttons = extras;
|
|
}
|
|
observe_joycon_gesture(
|
|
device, controller->gamepad, report_ms);
|
|
if (is_wii) {
|
|
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
|
WiiMotionIngress& motion = slot.wii_motion;
|
|
motion.received_us = time_us_32();
|
|
#endif
|
|
if (!have_acceleration) {
|
|
slot.accelerometer = {};
|
|
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
|
motion.accel_valid = false;
|
|
#endif
|
|
} else if (
|
|
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
|
sequence != motion.accel_sequence
|
|
#else
|
|
!slot.accelerometer.valid || sequence != slot.accelerometer.sequence
|
|
#endif
|
|
) {
|
|
slot.accelerometer = {
|
|
convert_accel(-static_cast<int64_t>(acceleration[2])),
|
|
convert_accel(-static_cast<int64_t>(acceleration[0])),
|
|
convert_accel(acceleration[1]), sequence,
|
|
report_ms, true};
|
|
}
|
|
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
|
if (slot.accelerometer.valid && sequence != motion.accel_sequence) {
|
|
motion.accel_sequence = sequence;
|
|
motion.accel_received_us = motion.received_us;
|
|
motion.accel_valid = true;
|
|
memcpy(motion.accel_q13, acceleration, sizeof(motion.accel_q13));
|
|
}
|
|
if (!have_gyro) {
|
|
motion.gyro_valid = false;
|
|
} else if (gyro_sequence != motion.gyro_sequence) {
|
|
motion.gyro_sequence = gyro_sequence;
|
|
motion.gyro_received_us = motion.received_us;
|
|
motion.gyro_valid = true;
|
|
memcpy(motion.gyro_q10, gyro, sizeof(motion.gyro_q10));
|
|
}
|
|
#endif
|
|
if (!have_nunchuk_acceleration) {
|
|
slot.nunchuk_accelerometer = {};
|
|
} else if (
|
|
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
|
nunchuk_sequence != motion.nunchuk_sequence
|
|
#else
|
|
!slot.nunchuk_accelerometer.valid ||
|
|
nunchuk_sequence != slot.nunchuk_accelerometer.sequence
|
|
#endif
|
|
) {
|
|
slot.nunchuk_accelerometer = {
|
|
convert_accel(-static_cast<int64_t>(nunchuk_acceleration[2])),
|
|
convert_accel(-static_cast<int64_t>(nunchuk_acceleration[0])),
|
|
convert_accel(nunchuk_acceleration[1]), nunchuk_sequence,
|
|
report_ms, true};
|
|
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
|
motion.nunchuk_sequence = nunchuk_sequence;
|
|
#endif
|
|
}
|
|
}
|
|
#ifdef SWITCH_PICO_WII_IR
|
|
if (have_infrared) {
|
|
const bool nunchuk_c =
|
|
(device->controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK ||
|
|
device->controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL) &&
|
|
(controller->gamepad.buttons & BUTTON_X) != 0;
|
|
float gravity_roll = 0.0f;
|
|
bool gravity_valid = false;
|
|
#if SWITCH2_BRIDGE_WII_INPUT
|
|
const WiiMotionIngress& motion = slot.wii_motion;
|
|
if (motion.accel_valid && time_us_32() - motion.accel_received_us < 150000) {
|
|
const float x = static_cast<float>(motion.accel_q13[0]) / 8192.0f;
|
|
const float y = static_cast<float>(motion.accel_q13[1]) / 8192.0f;
|
|
const float z = static_cast<float>(motion.accel_q13[2]) / 8192.0f;
|
|
const float magnitude = x * x + y * y + z * z;
|
|
gravity_valid = magnitude >= 0.85f * 0.85f && magnitude <= 1.15f * 1.15f &&
|
|
x * x + y * y >= 0.25f;
|
|
if (gravity_valid) gravity_roll = atan2f(-x, y);
|
|
}
|
|
#endif
|
|
wii_ir_pointer_observe(static_cast<uint8_t>(slot_index),
|
|
slot.connection_generation, infrared.sequence,
|
|
infrared.buttons, infrared.x, infrared.y,
|
|
infrared.valid_mask, nunchuk_c, gravity_roll, gravity_valid);
|
|
}
|
|
#endif
|
|
const uni_gamepad_t gamepad = logical_gamepad(slot);
|
|
uni_hid_device_t* owner = slot.device;
|
|
const bool fresh_motion =
|
|
slot.companion == nullptr || slot.companion == device;
|
|
const uint8_t merged_extras =
|
|
slot.extra_buttons | slot.companion_extra_buttons;
|
|
state_lock_exit();
|
|
const uint16_t pre_hotkey_button_mask = logical_button_mask(gamepad);
|
|
if (wake_chord_rising_edge(
|
|
static_cast<uint8_t>(slot_index), owner, pre_hotkey_button_mask)) {
|
|
switch2_wake_request();
|
|
}
|
|
const HotkeyDecision hotkeys = update_controller_hotkeys(
|
|
static_cast<uint8_t>(slot_index), owner);
|
|
ControllerState state = map_gamepad(
|
|
gamepad, hotkeys.motion_enabled && fresh_motion, pre_hotkey_button_mask);
|
|
state.extra_buttons = merged_extras;
|
|
publish_device_state(
|
|
static_cast<uint8_t>(slot_index), owner, pre_hotkey_button_mask, state);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
#if !SWITCH2_PROBE_HUB
|
|
[[noreturn]] void halt_wireless_backend() {
|
|
publish_all_neutral();
|
|
while (true) {
|
|
tight_loop_contents();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Called on the Bluetooth/storage owner after flash-safe registration.
|
|
bool initialize_wireless_backend() {
|
|
__atomic_store_n(&g_initialization_stage, 2, __ATOMIC_RELEASE);
|
|
configuration_service_initialize_on_storage_core();
|
|
profile_service_initialize_on_storage_core();
|
|
ConfigurationServiceSnapshot configuration{};
|
|
configuration_service_snapshot(&configuration);
|
|
if (configuration.state == ConfigurationServiceState::kReady) {
|
|
// Load before uni_init can deliver even the first ready callback.
|
|
g_joycon_mode = configuration.configuration.joycon_mode;
|
|
}
|
|
__atomic_store_n(&g_initialization_stage, 3, __ATOMIC_RELEASE);
|
|
if (cyw43_arch_init() != 0) {
|
|
return false;
|
|
}
|
|
__atomic_store_n(&g_initialization_stage, 4, __ATOMIC_RELEASE);
|
|
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) {
|
|
return false;
|
|
}
|
|
__atomic_store_n(&g_initialization_stage, 5, __ATOMIC_RELEASE);
|
|
return true;
|
|
}
|
|
|
|
#if !SWITCH2_PROBE_HUB
|
|
[[noreturn]] void core1_main() {
|
|
if (!flash_safe_execute_core_init() || !initialize_wireless_backend()) {
|
|
halt_wireless_backend();
|
|
}
|
|
|
|
btstack_run_loop_execute();
|
|
while (true) {
|
|
tight_loop_contents();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
} // namespace
|
|
|
|
extern "C" bool switch_pico_switch2_pairing_allowed(void) {
|
|
return SWITCH_PICO_ENABLE_BLE && g_initialized &&
|
|
pairing_window_active_at(btstack_run_loop_get_time_ms());
|
|
}
|
|
|
|
extern "C" void __real_sm_request_pairing(hci_con_handle_t handle);
|
|
extern "C" void __wrap_sm_request_pairing(hci_con_handle_t handle) {
|
|
uni_hid_device_t* device =
|
|
uni_hid_device_get_instance_for_connection_handle(handle);
|
|
if (!SWITCH_PICO_ENABLE_BLE) {
|
|
if (device != nullptr) {
|
|
uni_hid_device_disconnect(device);
|
|
}
|
|
return;
|
|
}
|
|
if (uni_hid_parser_switch2_is_ble_device(device)) {
|
|
// GATT's implicit authentication retry must not enter standard SMP for
|
|
// this proprietary protocol. Retain storage until HCI teardown.
|
|
uni_hid_device_disconnect(device);
|
|
return;
|
|
}
|
|
__real_sm_request_pairing(handle);
|
|
}
|
|
|
|
#if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) || defined(SWITCH_PICO_NATIVE_SWITCH_RUMBLE)
|
|
extern "C" bool uni_platform_on_l2cap_can_send_now(
|
|
uni_hid_device_t* device, uint16_t cid) {
|
|
bool block_generic = false;
|
|
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
|
block_generic = haptics_experiment_blocks_generic(device);
|
|
#endif
|
|
const bool consumed = native_output_scheduler_on_can_send_now(device, cid);
|
|
return consumed || block_generic;
|
|
}
|
|
#endif
|
|
|
|
bool bluepad32_input_backend_capture_start(
|
|
uint8_t slot, uint32_t connection_generation, const CaptureOptions& options) {
|
|
if (!g_initialized || slot >= kSlotCount) return false;
|
|
state_lock_enter();
|
|
const BackendSlot& current = g_slots[slot];
|
|
const bool accepted = current.active &&
|
|
current.connection_generation == connection_generation &&
|
|
g_macro_capture.start(slot, connection_generation, options,
|
|
time_us_32(), current.state);
|
|
state_lock_exit();
|
|
return accepted;
|
|
}
|
|
|
|
bool bluepad32_input_backend_capture_stop(uint32_t run_id) {
|
|
if (!g_initialized || run_id == 0) return false;
|
|
state_lock_enter();
|
|
const bool matches = run_id == g_macro_capture.run_id();
|
|
if (matches) g_macro_capture.stop(time_us_32());
|
|
state_lock_exit();
|
|
return matches;
|
|
}
|
|
|
|
bool bluepad32_input_backend_capture_page(
|
|
uint32_t run_id, uint16_t first_index, Bluepad32CaptureSnapshot* output) {
|
|
if (!g_initialized || output == nullptr) return false;
|
|
state_lock_enter();
|
|
g_macro_capture.tick(time_us_32());
|
|
if ((run_id != 0 && run_id != g_macro_capture.run_id()) ||
|
|
first_index > g_macro_capture.event_count()) {
|
|
state_lock_exit();
|
|
return false;
|
|
}
|
|
*output = {};
|
|
output->run_id = g_macro_capture.run_id();
|
|
output->connection_generation = g_macro_capture.generation();
|
|
output->elapsed_us = g_macro_capture.elapsed_us(time_us_32());
|
|
output->slot = g_macro_capture.slot();
|
|
output->state = g_macro_capture.state();
|
|
output->options = g_macro_capture.options();
|
|
output->total_events = g_macro_capture.event_count();
|
|
output->first_index = first_index;
|
|
const uint16_t remaining = output->total_events - first_index;
|
|
output->event_count = remaining < BLUEPAD32_CAPTURE_PAGE_EVENTS
|
|
? remaining : BLUEPAD32_CAPTURE_PAGE_EVENTS;
|
|
for (uint8_t index = 0; index < output->event_count; ++index) {
|
|
g_macro_capture.event(first_index + index, &output->events[index]);
|
|
}
|
|
state_lock_exit();
|
|
return true;
|
|
}
|
|
|
|
void bluepad32_input_backend_init() {
|
|
if (g_initialized) {
|
|
return;
|
|
}
|
|
|
|
#if SWITCH2_PROBE_HUB
|
|
g_state_lock = spin_lock_init(spin_lock_claim_unused(true));
|
|
#else
|
|
critical_section_init(&g_state_lock);
|
|
#endif
|
|
#ifdef SWITCH_PICO_WII_IR
|
|
wii_ir_pointer_init();
|
|
#endif
|
|
configuration_service_prepare();
|
|
profile_service_prepare();
|
|
#if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) || defined(SWITCH_PICO_NATIVE_SWITCH_RUMBLE)
|
|
native_output_scheduler_prepare();
|
|
#endif
|
|
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
|
|
switch_native_output_prepare();
|
|
#endif
|
|
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
|
haptics_experiment_prepare();
|
|
#endif
|
|
for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) {
|
|
BackendSlot& slot = g_slots[slot_index];
|
|
slot = {};
|
|
slot.state = make_neutral_state();
|
|
slot.identity = controller_identity_global();
|
|
slot.pending_rumble.slot = slot_index;
|
|
reset_slot_hotkeys(slot);
|
|
g_consumed_generation[slot_index] = 0;
|
|
g_last_snapshot_generation[slot_index] = 0;
|
|
g_ble_identity_mappings[slot_index] = {};
|
|
g_joycon_pair_hints[slot_index] = {};
|
|
g_joycon_gestures[slot_index] = {};
|
|
g_joycon_overrides[slot_index] = {};
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
g_native_pending_devices[slot_index] = nullptr;
|
|
g_native_reports[slot_index] = {};
|
|
#endif
|
|
}
|
|
g_joycon_mode = JoyConMode::kPaired;
|
|
g_joycon_reconcile_requested = false;
|
|
g_pairing_window_requested = false;
|
|
g_pairing_snapshot_requested = false;
|
|
g_pairing_snapshot = {};
|
|
g_pairing_snapshot.status =
|
|
Bluepad32PairingSnapshotStatus::kPending;
|
|
g_clear_pairings_requested_token = 0;
|
|
g_clear_pairings_in_progress_token = 0;
|
|
g_next_clear_pairings_request_token = 1;
|
|
g_connection_status = ConnectionStatus::Initializing;
|
|
g_connection_policy_state = ConnectionPolicyState::Uninitialized;
|
|
g_background_scan_active = false;
|
|
g_pairing_window_deadline_ms = 0;
|
|
g_pairing_window_duration_ms =
|
|
kDefaultPairingWindowDurationMs;
|
|
g_pairing_reset_feedback_deadline_ms = 0;
|
|
g_pairing_window_open = false;
|
|
g_initialized = true;
|
|
__atomic_store_n(&g_initialization_stage, 1, __ATOMIC_RELEASE);
|
|
__atomic_store_n(&g_rumble_timer_ticks, 0, __ATOMIC_RELAXED);
|
|
__atomic_store_n(&g_configuration_timer_ticks, 0, __ATOMIC_RELAXED);
|
|
__atomic_store_n(&g_controller_reports, 0, __ATOMIC_RELAXED);
|
|
__atomic_store_n(&g_host_rumble_requests, 0, __ATOMIC_RELAXED);
|
|
__atomic_store_n(&g_local_feedback_requests, 0, __ATOMIC_RELAXED);
|
|
__atomic_store_n(&g_rumble_dispatches, 0, __ATOMIC_RELAXED);
|
|
}
|
|
|
|
void bluepad32_input_backend_start() {
|
|
#if SWITCH2_PROBE_HUB
|
|
if (get_core_num() != 0) {
|
|
panic("native hub Bluetooth must start on Core 0");
|
|
}
|
|
#endif
|
|
if (!g_initialized) {
|
|
bluepad32_input_backend_init();
|
|
}
|
|
if (g_started) {
|
|
return;
|
|
}
|
|
// Non-hub builds register both cores before BTstack can write flash.
|
|
// Hub builds keep Core 1 in IRQ-disabled SRAM code, so the SDK's
|
|
// PICO_FLASH_ASSUME_CORE1_SAFE path only disables Core 0 interrupts.
|
|
if (!flash_safe_execute_core_init()) {
|
|
g_connection_policy_state = ConnectionPolicyState::FailedClosed;
|
|
return;
|
|
}
|
|
|
|
g_started = true;
|
|
#if SWITCH2_PROBE_HUB
|
|
g_poll_ready = initialize_wireless_backend();
|
|
if (!g_poll_ready) {
|
|
g_connection_policy_state = ConnectionPolicyState::FailedClosed;
|
|
publish_all_neutral();
|
|
}
|
|
#else
|
|
multicore_launch_core1_with_stack(
|
|
core1_main, g_core1_stack, sizeof(g_core1_stack));
|
|
#endif
|
|
}
|
|
|
|
void bluepad32_input_backend_poll() {
|
|
#if SWITCH2_PROBE_HUB
|
|
if (!g_poll_ready) return;
|
|
async_context_t* context = cyw43_arch_async_context();
|
|
// The SDK checks both the owning core and non-IRQ context. Polling invokes
|
|
// the existing BTstack workers/timers; no second scheduler or wait loop.
|
|
async_context_lock_check(context);
|
|
async_context_poll(context);
|
|
#endif
|
|
}
|
|
|
|
void bluepad32_input_backend_open_pairing_window() {
|
|
if (!g_initialized) {
|
|
bluepad32_input_backend_init();
|
|
}
|
|
|
|
state_lock_enter();
|
|
g_pairing_window_requested = true;
|
|
state_lock_exit();
|
|
}
|
|
uint32_t bluepad32_input_backend_clear_pairings() {
|
|
if (!g_initialized) {
|
|
bluepad32_input_backend_init();
|
|
}
|
|
|
|
state_lock_enter();
|
|
uint32_t request_token = g_clear_pairings_requested_token;
|
|
if (request_token == 0) {
|
|
request_token = g_clear_pairings_in_progress_token;
|
|
}
|
|
if (request_token == 0) {
|
|
request_token = g_next_clear_pairings_request_token;
|
|
g_next_clear_pairings_request_token =
|
|
request_token == UINT32_MAX ? 1 : request_token + 1;
|
|
g_clear_pairings_requested_token = request_token;
|
|
g_pairing_snapshot.status =
|
|
Bluepad32PairingSnapshotStatus::kPending;
|
|
}
|
|
state_lock_exit();
|
|
return request_token;
|
|
}
|
|
|
|
void bluepad32_input_backend_request_pairing_snapshot() {
|
|
if (!g_initialized) {
|
|
bluepad32_input_backend_init();
|
|
}
|
|
|
|
state_lock_enter();
|
|
g_pairing_snapshot_requested = true;
|
|
if (g_pairing_snapshot.status != Bluepad32PairingSnapshotStatus::kFailed) {
|
|
g_pairing_snapshot.status = Bluepad32PairingSnapshotStatus::kPending;
|
|
}
|
|
state_lock_exit();
|
|
}
|
|
|
|
void bluepad32_input_backend_pairing_snapshot(
|
|
Bluepad32PairingSnapshot* out) {
|
|
if (out == nullptr) {
|
|
return;
|
|
}
|
|
if (!g_initialized) {
|
|
bluepad32_input_backend_init();
|
|
}
|
|
|
|
state_lock_enter();
|
|
*out = g_pairing_snapshot;
|
|
state_lock_exit();
|
|
}
|
|
|
|
|
|
void bluepad32_input_backend_diagnostics(
|
|
Bluepad32BackendDiagnostics* out) {
|
|
if (out == nullptr) {
|
|
return;
|
|
}
|
|
*out = {};
|
|
out->initialization_stage =
|
|
__atomic_load_n(&g_initialization_stage, __ATOMIC_ACQUIRE);
|
|
out->rumble_timer_ticks =
|
|
__atomic_load_n(&g_rumble_timer_ticks, __ATOMIC_RELAXED);
|
|
out->configuration_timer_ticks =
|
|
__atomic_load_n(&g_configuration_timer_ticks, __ATOMIC_RELAXED);
|
|
out->controller_reports =
|
|
__atomic_load_n(&g_controller_reports, __ATOMIC_RELAXED);
|
|
out->host_rumble_requests =
|
|
__atomic_load_n(&g_host_rumble_requests, __ATOMIC_RELAXED);
|
|
out->local_feedback_requests =
|
|
__atomic_load_n(&g_local_feedback_requests, __ATOMIC_RELAXED);
|
|
out->rumble_dispatches =
|
|
__atomic_load_n(&g_rumble_dispatches, __ATOMIC_RELAXED);
|
|
out->switch2_ingress_drops =
|
|
__atomic_load_n(&g_switch2_ingress_drops, __ATOMIC_RELAXED);
|
|
out->switch2_output_drops = uni_hid_parser_switch2_haptics_dropped();
|
|
|
|
state_lock_enter();
|
|
for (const BackendSlot& slot : g_slots) {
|
|
if (slot.active) {
|
|
++out->active_slots;
|
|
}
|
|
if (slot.active && slot.device != nullptr &&
|
|
slot.device->report_parser.play_dual_rumble != nullptr) {
|
|
++out->rumble_capable_slots;
|
|
}
|
|
if (slot.feedback_pending || slot.profile_feedback.active ||
|
|
slot.pending_profile_feedback_count != 0) {
|
|
++out->feedback_pending_slots;
|
|
}
|
|
if (slot.rumble_pending || slot.switch2_ingress.count != 0) {
|
|
++out->rumble_pending_slots;
|
|
}
|
|
}
|
|
state_lock_exit();
|
|
}
|
|
|
|
void bluepad32_input_backend_snapshot(uint8_t slot_index,
|
|
Bluepad32SlotSnapshot* out) {
|
|
if (out == nullptr) {
|
|
return;
|
|
}
|
|
*out = {};
|
|
if (!valid_slot(slot_index) || !g_initialized) {
|
|
return;
|
|
}
|
|
|
|
state_lock_enter();
|
|
const BackendSlot& slot = g_slots[slot_index];
|
|
out->active = slot.active;
|
|
out->connection_generation = slot.connection_generation;
|
|
out->identity = slot.identity;
|
|
out->pre_hotkey_button_mask =
|
|
slot.pre_hotkey_button_mask;
|
|
out->state = slot.state;
|
|
out->accelerometer = slot.accelerometer;
|
|
out->nunchuk_accelerometer = slot.nunchuk_accelerometer;
|
|
const uint32_t state_generation = slot.state_generation;
|
|
state_lock_exit();
|
|
|
|
if (state_generation == g_consumed_generation[slot_index]) {
|
|
out->state.motion_sample_count = 0;
|
|
}
|
|
g_last_snapshot_generation[slot_index] = state_generation;
|
|
}
|
|
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
void bluepad32_input_backend_select_native_source(const uint8_t address[6]) {
|
|
if (!g_initialized) return;
|
|
state_lock_enter();
|
|
g_native_explicit_address = address != nullptr;
|
|
if (address != nullptr) memcpy(g_native_address, address, 6);
|
|
else memset(g_native_address, 0, sizeof(g_native_address));
|
|
refresh_native_source_locked(true);
|
|
state_lock_exit();
|
|
}
|
|
|
|
void bluepad32_input_backend_native_snapshot(Bluepad32NativeGamepadSnapshot* output) {
|
|
if (output == nullptr) return;
|
|
*output = {};
|
|
if (!g_initialized) return;
|
|
state_lock_enter();
|
|
*output = g_native_snapshot;
|
|
state_lock_exit();
|
|
}
|
|
|
|
bool bluepad32_input_backend_native_sample_request(
|
|
uint8_t instance, uint8_t sample_id, uint64_t* token) {
|
|
if (token == nullptr) return false;
|
|
*token = 0;
|
|
if (!g_initialized || instance >= 2 || sample_id >= 8) return false;
|
|
state_lock_enter();
|
|
NativeGamepadCue& cue = g_native_cues[instance];
|
|
const uint8_t index = g_native_slot;
|
|
const bool accepted = index < kSlotCount && g_next_native_token != 0 &&
|
|
native_rumble_capable(g_slots[index], instance) &&
|
|
!cue.in_flight && (sample_id == 0 || (cue.result != 0 && !cue.active));
|
|
if (accepted) {
|
|
cue = {};
|
|
cue.token = g_next_native_token++;
|
|
cue.slot = index;
|
|
cue.connection_generation = g_native_generation;
|
|
cue.requested_ms = btstack_run_loop_get_time_ms();
|
|
cue.sample_id = sample_id;
|
|
cue.result = 0;
|
|
*token = cue.token;
|
|
}
|
|
state_lock_exit();
|
|
return accepted;
|
|
}
|
|
|
|
int bluepad32_input_backend_native_sample_result(uint8_t instance, uint64_t token) {
|
|
if (!g_initialized || instance >= 2 || token == 0) return -1;
|
|
state_lock_enter();
|
|
NativeGamepadCue& cue = g_native_cues[instance];
|
|
int result = -1;
|
|
if (cue.token == token && !cue.consumed) {
|
|
if (!native_cue_current(cue) ||
|
|
(cue.result == 0 &&
|
|
btstack_run_loop_get_time_ms() - cue.requested_ms >= kNativeGamepadCueDeadlineMs))
|
|
cancel_native_cue_locked(cue);
|
|
result = cue.result;
|
|
if (result != 0) cue.consumed = true;
|
|
}
|
|
state_lock_exit();
|
|
return result;
|
|
}
|
|
|
|
void bluepad32_input_backend_native_sample_cancel(uint8_t instance) {
|
|
if (!g_initialized || instance >= 2) return;
|
|
state_lock_enter();
|
|
cancel_native_cue_locked(g_native_cues[instance]);
|
|
state_lock_exit();
|
|
}
|
|
#endif
|
|
|
|
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
|
void bluepad32_input_backend_select_wii_source(const uint8_t address[6]) {
|
|
// Selection is configuration, not a live Core 0 parser mutation.
|
|
if (!g_initialized || g_started) return;
|
|
state_lock_enter();
|
|
g_wii_source_selected = address != nullptr;
|
|
if (address != nullptr) memcpy(g_wii_source_address, address, 6);
|
|
else memset(g_wii_source_address, 0, sizeof(g_wii_source_address));
|
|
g_wii_snapshot = {};
|
|
g_wii_cue = {};
|
|
for (BackendSlot& slot : g_slots) retire_wii_motion(slot.wii_motion);
|
|
#ifdef SWITCH_PICO_WII_IR
|
|
// Lock order: backend exclusive, then pointer striped.
|
|
wii_ir_pointer_reset();
|
|
#endif
|
|
state_lock_exit();
|
|
}
|
|
|
|
void bluepad32_input_backend_wii_snapshot(Bluepad32WiiBridgeSnapshot* output) {
|
|
if (output == nullptr) return;
|
|
*output = {};
|
|
if (!g_initialized) return;
|
|
state_lock_enter();
|
|
const uint8_t slot = g_wii_snapshot.slot;
|
|
if (slot < kSlotCount && is_selected_wii(g_slots[slot]) &&
|
|
g_slots[slot].connection_generation ==
|
|
g_wii_snapshot.controller.connection_generation) {
|
|
*output = g_wii_snapshot;
|
|
}
|
|
state_lock_exit();
|
|
}
|
|
|
|
bool bluepad32_input_backend_wii_sample_request(uint8_t sample_id, uint64_t* token) {
|
|
if (token == nullptr) return false;
|
|
*token = 0;
|
|
if (!g_initialized || sample_id >= 8) return false;
|
|
state_lock_enter();
|
|
bool accepted = false;
|
|
// Do not overwrite a command already being dispatched. Sample zero can
|
|
// replace a queued/running pattern; ordinary cues serialize until it ends.
|
|
if (g_next_wii_cue_token != 0 && !g_wii_cue.in_flight &&
|
|
(sample_id == 0 || (g_wii_cue.result != 0 &&
|
|
!g_wii_cue.active && !g_wii_cue.stop_pending))) {
|
|
for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) {
|
|
const BackendSlot& slot = g_slots[slot_index];
|
|
if (!is_selected_wii(slot) ||
|
|
slot.device->report_parser.play_dual_rumble == nullptr) continue;
|
|
g_wii_cue = {};
|
|
g_wii_cue.token = g_next_wii_cue_token++;
|
|
g_wii_cue.slot = slot_index;
|
|
g_wii_cue.connection_generation = slot.connection_generation;
|
|
g_wii_cue.sample_id = sample_id;
|
|
g_wii_cue.requested_ms = btstack_run_loop_get_time_ms();
|
|
g_wii_cue.result = 0;
|
|
*token = g_wii_cue.token;
|
|
accepted = true;
|
|
break;
|
|
}
|
|
}
|
|
state_lock_exit();
|
|
return accepted;
|
|
}
|
|
|
|
int bluepad32_input_backend_wii_sample_result(uint64_t token) {
|
|
if (!g_initialized || token == 0) return -1;
|
|
state_lock_enter();
|
|
int result = -1;
|
|
if (g_wii_cue.token == token && !g_wii_cue.consumed) {
|
|
if (!wii_cue_target_current() ||
|
|
g_slots[g_wii_cue.slot].device->report_parser.play_dual_rumble == nullptr ||
|
|
(g_wii_cue.result == 0 &&
|
|
btstack_run_loop_get_time_ms() - g_wii_cue.requested_ms >= kWiiCueDeadlineMs)) {
|
|
cancel_wii_cue_locked();
|
|
}
|
|
result = g_wii_cue.result;
|
|
if (result != 0) g_wii_cue.consumed = true;
|
|
}
|
|
state_lock_exit();
|
|
return result;
|
|
}
|
|
|
|
void bluepad32_input_backend_wii_sample_cancel() {
|
|
if (!g_initialized) return;
|
|
state_lock_enter();
|
|
cancel_wii_cue_locked();
|
|
state_lock_exit();
|
|
}
|
|
#endif
|
|
|
|
void bluepad32_input_backend_playtest_snapshot(
|
|
uint8_t slot_index, Bluepad32PlaytestSnapshot* out) {
|
|
if (out == nullptr) {
|
|
return;
|
|
}
|
|
*out = {};
|
|
if (!valid_slot(slot_index) || !g_initialized) {
|
|
return;
|
|
}
|
|
|
|
state_lock_enter();
|
|
const BackendSlot& slot = g_slots[slot_index];
|
|
out->active = slot.active;
|
|
out->connection_generation = slot.connection_generation;
|
|
out->state_generation = slot.state_generation;
|
|
out->identity = slot.identity;
|
|
out->physical_button_mask = slot.pre_hotkey_button_mask;
|
|
out->state = slot.state;
|
|
if (slot.device != nullptr) {
|
|
out->battery = slot.device->controller.battery;
|
|
out->capabilities =
|
|
(slot.device->report_parser.play_dual_rumble != nullptr ? 1u : 0u) |
|
|
(slot.device->report_parser.set_lightbar_color != nullptr ? 2u : 0u) |
|
|
(slot.device->report_parser.set_player_leds != nullptr ? 4u : 0u) |
|
|
(slot.state.motion_sample_count != 0 ? 8u : 0u);
|
|
out->controller_layout = controller_layout(slot);
|
|
}
|
|
state_lock_exit();
|
|
}
|
|
|
|
bool bluepad32_input_backend_toggle_motion(
|
|
uint8_t slot_index, uint32_t connection_generation) {
|
|
if (!g_initialized || !valid_slot(slot_index)) {
|
|
return false;
|
|
}
|
|
bool toggled = false;
|
|
state_lock_enter();
|
|
BackendSlot& slot = g_slots[slot_index];
|
|
if (slot.active &&
|
|
slot.connection_generation == connection_generation) {
|
|
slot.motion_enabled = !slot.motion_enabled;
|
|
#ifdef SWITCH_PICO_WII_IR_GYRO
|
|
if (slot.device != nullptr &&
|
|
slot.device->controller_type == CONTROLLER_TYPE_WiiController) {
|
|
const ControllerMotionSample sample =
|
|
slot.state.motion_sample_count != 0
|
|
? slot.state.motion_samples[0] : ControllerMotionSample{};
|
|
wii_ir_gyro_update_motion(
|
|
slot_index, connection_generation, slot.motion_enabled, sample);
|
|
if (!slot.motion_enabled) {
|
|
slot.state.motion_sample_count = 0;
|
|
}
|
|
}
|
|
#endif
|
|
if (slot.motion_enabled) {
|
|
queue_local_feedback(
|
|
slot, kMotionEnabledFeedbackDurationMs,
|
|
kMotionEnabledFeedbackWeakMagnitude,
|
|
kMotionEnabledFeedbackStrongMagnitude);
|
|
} else {
|
|
queue_local_feedback(
|
|
slot, kMotionDisabledFeedbackDurationMs,
|
|
kMotionDisabledFeedbackWeakMagnitude,
|
|
kMotionDisabledFeedbackStrongMagnitude);
|
|
}
|
|
toggled = true;
|
|
}
|
|
state_lock_exit();
|
|
return toggled;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
#if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) || defined(SWITCH_PICO_NATIVE_SWITCH_RUMBLE)
|
|
const uint64_t received_us = time_us_64();
|
|
uint32_t native_generation = 0;
|
|
bool native_candidate = false;
|
|
#endif
|
|
const uint16_t duration_ms = host_rumble_duration_ms();
|
|
const uint32_t received_ms = btstack_run_loop_get_time_ms();
|
|
state_lock_enter();
|
|
BackendSlot& slot = g_slots[slot_index];
|
|
if (slot.active && slot.device != nullptr) {
|
|
if (uni_hid_parser_switch2_is_ble_device(slot.device)) {
|
|
Switch2Ingress& ingress = slot.switch2_ingress;
|
|
const uint8_t host_mode = switch2_host_mode();
|
|
if (ingress.host_mode != host_mode) {
|
|
clear_switch2_ingress(slot);
|
|
ingress.host_mode = host_mode;
|
|
}
|
|
if (switch2_host_stop(rumble)) {
|
|
// Host stop is a barrier, not a local-feedback cancellation.
|
|
ingress.head = 0;
|
|
ingress.count = 0;
|
|
} else if (ingress.count == kSwitch2IngressCapacity) {
|
|
ingress.head = (ingress.head + 1u) % kSwitch2IngressCapacity;
|
|
--ingress.count;
|
|
__atomic_add_fetch(&g_switch2_ingress_drops, 1, __ATOMIC_RELAXED);
|
|
}
|
|
Switch2HostCommand& command =
|
|
ingress.commands[(ingress.head + ingress.count) % kSwitch2IngressCapacity];
|
|
command = {};
|
|
command.envelope.slot = slot_index;
|
|
command.envelope.connection_generation = slot.connection_generation;
|
|
command.envelope.rumble = rumble;
|
|
command.envelope.duration_ms = duration_ms;
|
|
command.envelope.received_ms = received_ms;
|
|
command.generation = ingress.generation;
|
|
++ingress.count;
|
|
__atomic_add_fetch(&g_host_rumble_requests, 1, __ATOMIC_RELAXED);
|
|
state_lock_exit();
|
|
return;
|
|
}
|
|
#if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) || defined(SWITCH_PICO_NATIVE_SWITCH_RUMBLE)
|
|
native_generation = slot.connection_generation;
|
|
native_candidate = true;
|
|
#endif
|
|
const RumbleEnvelope envelope{
|
|
slot_index, slot.connection_generation, rumble,
|
|
duration_ms, received_ms
|
|
#if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) || defined(SWITCH_PICO_NATIVE_SWITCH_RUMBLE)
|
|
, received_us
|
|
#endif
|
|
};
|
|
slot.pending_rumble = envelope;
|
|
slot.rumble_pending = true;
|
|
__atomic_add_fetch(
|
|
&g_host_rumble_requests, 1, __ATOMIC_RELAXED);
|
|
if (duration_ms == kXInputHostRumbleDurationMs) {
|
|
slot.retained_host_rumble = envelope;
|
|
slot.retained_host_rumble_valid = true;
|
|
} else {
|
|
slot.retained_host_rumble = {};
|
|
slot.retained_host_rumble_valid = false;
|
|
}
|
|
}
|
|
state_lock_exit();
|
|
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
|
|
if (native_candidate)
|
|
switch_native_output_submit(slot_index, native_generation, received_us, rumble,
|
|
duration_ms == kXInputHostRumbleDurationMs);
|
|
#endif
|
|
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
|
if (native_candidate) {
|
|
if (duration_ms == kXInputHostRumbleDurationMs) {
|
|
haptics_experiment_submit_rumble(
|
|
slot_index, native_generation, received_us,
|
|
rumble.low_frequency_magnitude, rumble.high_frequency_magnitude);
|
|
} else {
|
|
haptics_experiment_submit(
|
|
slot_index, native_generation, received_us, rumble.hd);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
bool bluepad32_input_backend_set_wii_orientation(
|
|
const ControllerIdentity& identity, uint32_t connection_generation,
|
|
bool vertical) {
|
|
if (!g_initialized || !identity.stable ||
|
|
controller_identity_is_global(identity)) {
|
|
return false;
|
|
}
|
|
bool queued = false;
|
|
state_lock_enter();
|
|
for (BackendSlot& slot : g_slots) {
|
|
if (!is_solo_wii_remote(slot) ||
|
|
slot.connection_generation != connection_generation ||
|
|
!controller_identity_equal(slot.identity, identity)) {
|
|
continue;
|
|
}
|
|
slot.pending_wii_orientation = {identity, connection_generation, vertical};
|
|
slot.wii_orientation_pending = true;
|
|
queued = true;
|
|
break;
|
|
}
|
|
state_lock_exit();
|
|
return queued;
|
|
}
|
|
|
|
bool bluepad32_input_backend_identify(
|
|
const ControllerIdentity& identity) {
|
|
if (!g_initialized || !identity.stable ||
|
|
controller_identity_is_global(identity)) {
|
|
return false;
|
|
}
|
|
bool queued = false;
|
|
state_lock_enter();
|
|
for (BackendSlot& slot : g_slots) {
|
|
if (!slot.active ||
|
|
!controller_identity_equal(slot.identity, identity)) {
|
|
continue;
|
|
}
|
|
const ProfileFeedbackEnvelope feedback{
|
|
slot.connection_generation, 1,
|
|
ControllerProfileConfirmationPolicy::kRumbleAndLed};
|
|
queue_profile_feedback(slot, feedback);
|
|
queued = true;
|
|
break;
|
|
}
|
|
state_lock_exit();
|
|
return queued;
|
|
}
|
|
|
|
void bluepad32_input_backend_queue_profile_feedback(
|
|
uint8_t slot_index, uint32_t connection_generation,
|
|
uint8_t active_profile_number,
|
|
ControllerProfileConfirmationPolicy policy) {
|
|
if (!g_initialized || !valid_slot(slot_index) ||
|
|
active_profile_number == 0 ||
|
|
active_profile_number > CONTROLLER_PROFILE_COUNT ||
|
|
!valid_confirmation_policy(policy) ||
|
|
policy == ControllerProfileConfirmationPolicy::kNone) {
|
|
return;
|
|
}
|
|
|
|
state_lock_enter();
|
|
BackendSlot& slot = g_slots[slot_index];
|
|
if (slot.active && slot.device != nullptr &&
|
|
slot.connection_generation == connection_generation) {
|
|
const ProfileFeedbackEnvelope feedback{
|
|
connection_generation, active_profile_number, policy};
|
|
queue_profile_feedback(slot, feedback);
|
|
}
|
|
state_lock_exit();
|
|
}
|