fix: reconnect Joy-Con mates and balance mixed Bluetooth links
This commit is contained in:
parent
006b573ee8
commit
8dd11c17a6
15 changed files with 780 additions and 75 deletions
|
|
@ -160,6 +160,14 @@ including an additional 8BitDo Ultimate, require opening the BOOTSEL pairing
|
|||
window while another controller is active. Do not restore continuous inquiry
|
||||
as a convenience feature; it causes gameplay latency.
|
||||
|
||||
A narrowly scoped exception now permits low-duty passive BLE discovery while
|
||||
a ready solo Joy-Con 2 awaits a remembered opposite half. It requires free
|
||||
physical capacity and no pending controller setup, accepts only remembered
|
||||
opposite-half candidates, and stops when the pair completes. It neither
|
||||
starts Classic inquiry nor opens new authentication. Lifecycle regressions
|
||||
cover setup failure, capacity exhaustion, pairing-window transitions and
|
||||
unrelated-controller isolation.
|
||||
|
||||
## Implementation principles
|
||||
|
||||
### Protocol-neutral state
|
||||
|
|
@ -523,8 +531,8 @@ Target hardware families:
|
|||
| DualShock 4 | input, Switch motion, rumble, lightbar |
|
||||
| Switch Pro | input, motion, rumble, player LED |
|
||||
| Joy-Con L/R | each half as the standalone controller exposed by Bluepad32 |
|
||||
| Switch 2 Pro | BLE setup, sticks/motion and C/GL/GR observed; rumble feel/reconnect still need qualification |
|
||||
| Joy-Con 2 L/R | solo and merged-pair software support; both-order/disconnect regressions pass; hardware qualification pending |
|
||||
| Switch 2 Pro | BLE setup, sticks/motion and C/GL/GR observed; native HD frequencies and timing physically measured; motion accuracy/endurance need wider qualification |
|
||||
| Joy-Con 2 L/R | paired stereo HD rumble physically confirmed; remembered reconnect and mixed-radio interval transitions exercised; long-duration qualification remains |
|
||||
| Wii Remote | buttons, accelerometer, rumble |
|
||||
| Wii Remote + Classic Controller | extension controls |
|
||||
| Wii U Pro | buttons, sticks, rumble |
|
||||
|
|
@ -728,8 +736,7 @@ Set B delivery evidence:
|
|||
The flashed real Switch 2 Pro delivered sticks, motion and independent
|
||||
C/GL/GR combinations; schema-7 extra mappings saved/read/restored.
|
||||
All 32 pre-existing profiles, metadata and active indices survived.
|
||||
Joy-Con hardware, physical rumble feel, reconnect endurance and mixed-radio
|
||||
qualification remain separate from those software results.
|
||||
Subsequent native-rumble and mixed-radio measurements are summarized below.
|
||||
- Full operation and limitations: [README.md](README.md#switch-2-controller-input).
|
||||
|
||||
Switch 2 native HD output now preserves decoded stereo frequency/amplitude
|
||||
|
|
@ -745,8 +752,24 @@ The final Pro-only native run sustained 512 changing one-subframe commands at
|
|||
recorded zero drops. Deliberate three-subframe saturation at 125.11 Hz
|
||||
discarded/superseded 69 output-stage commands out of 128 host updates and
|
||||
recovered to a clean stop. No mixed-radio lossless claim is made.
|
||||
Verification is now 289 passing tests and all five firmware builds; all
|
||||
40 stored profiles and metadata survived. See the
|
||||
Paired Joy-Con 2 stereo/subframe patterns produced 394 physical dispatches
|
||||
from 197 host requests without queue drops; the user confirmed both sides.
|
||||
Normal GATT-client busy responses now retain queued output instead of
|
||||
disconnecting the controller.
|
||||
|
||||
Two Switch 2 BLE links plus Classic now select 30 ms Switch 2 connection
|
||||
intervals automatically; other topologies retain 7.5 ms. This trades Switch 2
|
||||
delivery latency for Classic airtime without changing HD encoding or bonds.
|
||||
Physical DualSense power-off/reconnect verified both interval transitions
|
||||
without losing the Joy-Con links. A 30-second production mixed-rumble run
|
||||
kept native DualSense streaming, with no send failures or ingress drops,
|
||||
14 Joy-Con output-stage discards and nine PCM skips. It is not lossless.
|
||||
|
||||
Verification is now 289 passing tests and all five firmware builds. All
|
||||
40 pre-existing profiles, metadata and adapter configuration remain unchanged;
|
||||
the current inventory contains 56 profiles across seven identities. Temporary
|
||||
measurement hooks were removed, and the final production image passed another
|
||||
simultaneous-rumble exercise after normal controller reconnect. See the
|
||||
[native HD contract and measurements](README.md#switch-2-native-hd-rumble).
|
||||
|
||||
### Native Switch-family HD rumble — Implemented, qualification incomplete
|
||||
|
|
|
|||
|
|
@ -197,6 +197,8 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
|||
set(BLUEPAD32_SOURCE ${CMAKE_CURRENT_LIST_DIR}/external/bluepad32)
|
||||
set(BLUEPAD32_PATCH ${CMAKE_CURRENT_LIST_DIR}/patches/bluepad32-sdl3-imu.patch)
|
||||
set(BLUEPAD32_ROOT ${CMAKE_CURRENT_BINARY_DIR}/_deps/bluepad32-src)
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
|
||||
${BLUEPAD32_PREP_SCRIPT} ${BLUEPAD32_PATCH})
|
||||
execute_process(
|
||||
COMMAND ${Python3_EXECUTABLE} ${BLUEPAD32_PREP_SCRIPT}
|
||||
--bluepad32 ${BLUEPAD32_SOURCE}
|
||||
|
|
|
|||
43
README.md
43
README.md
|
|
@ -185,6 +185,8 @@ Pairing order determines the initial USB slot assignment. Up to four physical Bl
|
|||
|
||||
With no active controller, the Pico runs Bluepad32 discovery and autoconnect. After any controller becomes active, active discovery pauses to protect input, motion, and rumble latency; bonded controllers may still initiate incoming reconnects. Pairing keys persist across Pico power cycles, so reconnect a previously paired controller by pressing its normal Home, PS, or Xbox power button. Hold BOOTSEL for the bounded pairing window before pairing a new controller or a controller that requires host-side discovery. Outside that window, BTstack remains non-bondable and rejects new Classic and BLE authentication.
|
||||
|
||||
Exception: a ready solo Joy-Con 2 keeps a low-duty passive BLE scan running for a remembered opposite half while physical capacity remains and no controller setup is pending. This reconnect does not require BOOTSEL, start Classic inquiry, or enable fresh pairing. Scanning stops when the pair completes; an explicit pairing window restores normal discovery.
|
||||
|
||||
To clear every stored Classic, BLE and proprietary Switch 2 pairing without a PC, hold BOOTSEL continuously for 10 seconds. The normal pairing window opens after two seconds; continuing to hold until the LED changes to a rapid blink clears remembered controllers, disconnects active controllers, publishes neutral state to every slot, and closes new authentication. Release BOOTSEL, open a new pairing window, and pair controllers again. A persistent-storage failure is reported rather than acknowledging a successful clear.
|
||||
|
||||
|
||||
|
|
@ -204,7 +206,7 @@ The Pico 2 W onboard LED reports the overall Bluetooth state:
|
|||
- **Reconnect a paired controller**: power it on normally with its Home, PS, or Xbox button.
|
||||
- **8BitDo Ultimate Bluetooth reconnect**: leave its selector in Bluetooth mode, press Home once, then shake it. After an abrupt controller power-off, the Pico can remain solid for up to four seconds while Bluetooth link supervision confirms the disconnect; scanning restarts immediately afterward.
|
||||
- **Pair a new controller**: hold BOOTSEL until the LED double-blinks, then put the controller into its explicit Bluetooth pairing mode.
|
||||
- **Pairing window expires**: new authentication and active discovery stop while a controller is active; remembered controllers may still initiate reconnects.
|
||||
- **Pairing window expires**: new authentication and full discovery stop while a controller is active; remembered controllers may still initiate reconnects, and a solo Joy-Con 2 resumes low-duty discovery of its remembered opposite half.
|
||||
- **Clear all pairings**: hold BOOTSEL continuously for 10 seconds, through the initial double blink, until the rapid confirmation blink starts. All controllers are disconnected and must be paired again.
|
||||
|
||||
### Managing configuration, profiles, and pairings from a PC
|
||||
|
|
@ -392,6 +394,8 @@ uses the current valid final sample, not a replay of masked history.
|
|||
Keepalives likewise send only the final sample with count 1. Pending ATT
|
||||
write-request buffers remain immutable; late completion cannot resurrect an
|
||||
old epoch after stop, reconnect or Joy-Con topology change.
|
||||
Normal GATT-client busy responses are treated as transient backpressure, not
|
||||
as controller disconnects.
|
||||
|
||||
Final single-Pro hardware runs:
|
||||
|
||||
|
|
@ -401,12 +405,43 @@ Final single-Pro hardware runs:
|
|||
| 512 changing one-subframe commands at 125.14 Hz | Zero ingress/output drops; clean stop |
|
||||
| 128 changing three-subframe commands at 125.11 Hz | Zero ingress drops; 69 output-stage commands discarded/superseded; clean stop and empty ingress |
|
||||
|
||||
Paired Joy-Con 2 hardware runs exercised left-only, right-only, stereo and
|
||||
ordered-subframe effects: 197 host requests produced 394 physical dispatches
|
||||
with zero ingress/output drops. The user confirmed both sides vibrated as
|
||||
intended. Held one-subframe commands at 125 Hz, changing one-subframe commands
|
||||
at 62.5 Hz, and three-subframe commands at 50 Hz completed without drops.
|
||||
Higher changing workloads discarded output-stage commands, but the pair
|
||||
remained connected after the GATT backpressure fix.
|
||||
|
||||
Three-subframe commands at 125 Hz exceed the native playback budget. These
|
||||
results do **not** establish lossless arbitrary workloads or mixed-controller
|
||||
radio performance. Joy-Con hardware and perceptual equivalence remain separate
|
||||
qualification items. All 40 profiles, names, active selections and adapter
|
||||
results do **not** establish lossless arbitrary workloads or perceptual
|
||||
equivalence. All 40 profiles, names, active selections and adapter
|
||||
configuration were preserved during this upgrade.
|
||||
|
||||
**Automatic mixed-controller scheduling:** when at least two physical Switch 2
|
||||
BLE links coexist with a Classic Bluetooth link, the firmware requests a
|
||||
30 ms connection interval for the Switch 2 links. Otherwise it requests
|
||||
7.5 ms. A merged Joy-Con pair counts as two links. Unrelated BLE controllers
|
||||
are not retimed; bonds, profiles, HD encoding and the DualSense timeout stay
|
||||
unchanged. The policy runs during connection setup, before native DualSense
|
||||
attachment, and reconciles asynchronous negotiation and topology changes.
|
||||
|
||||
This trades additional Switch 2 input, gyro and rumble delivery latency for
|
||||
Classic radio time. Multiple reports can travel per connection event, so
|
||||
30 ms does not impose a 33-report/s limit. Two Joy-Con 2 links at 7.5 ms caused
|
||||
native DualSense PCM timeouts in hardware testing. With the automatic policy,
|
||||
both negotiated 30 ms and DualSense started without manual re-arming.
|
||||
A production-firmware 30-second simultaneous rumble run retained all three
|
||||
connections and native streaming: zero ingress drops or send failures,
|
||||
14 Joy-Con output-stage discards and nine DualSense PCM skips. This is not
|
||||
a lossless or long-duration qualification. Lifecycle regressions cover
|
||||
Classic arrival/departure, a missing half, handle reuse, late negotiation
|
||||
completion, clock wrap and transient request rejection.
|
||||
In the physical power-off/reconnect check, both Joy-Cons negotiated 7.5 ms
|
||||
after DualSense shut down, then 30 ms after it reconnected with PS. Their
|
||||
Bluetooth handles stayed unchanged; native DualSense streaming restarted
|
||||
automatically and sent 1,715 packets without skips at the observation point.
|
||||
|
||||
### Rumble per controller
|
||||
|
||||
Commands remain bound to a USB slot and Bluetooth connection generation. Compatibility output uses a latest-value mailbox; native output keeps a bounded timestamped command history instead of collapsing substeps.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
#include <btstack.h>
|
||||
#include "bt/uni_bt_defines.h"
|
||||
#include "bt/uni_bt_le.h"
|
||||
#include "parser/uni_hid_parser_imu.h"
|
||||
#include "parser/uni_switch2_pairing.h"
|
||||
#include "sdkconfig.h"
|
||||
|
|
@ -251,7 +252,8 @@ static void sw2_subscribe(sw2_instance_t* ins, bool input) {
|
|||
}
|
||||
|
||||
static bool sw2_transient_write_error(uint8_t status) {
|
||||
return status == BTSTACK_ACL_BUFFERS_FULL || status == GATT_CLIENT_IN_WRONG_STATE;
|
||||
return status == BTSTACK_ACL_BUFFERS_FULL || status == GATT_CLIENT_BUSY ||
|
||||
status == GATT_CLIENT_IN_WRONG_STATE;
|
||||
}
|
||||
|
||||
static void sw2_try_command(sw2_instance_t* ins) {
|
||||
|
|
@ -715,8 +717,6 @@ bool uni_bt_le_switch2_handle_advertisement(const uint8_t* packet, uint16_t size
|
|||
return true;
|
||||
uint8_t rssi = gap_event_advertising_report_get_rssi(packet);
|
||||
const uint16_t cod = UNI_BT_COD_MAJOR_PERIPHERAL | UNI_BT_COD_MINOR_GAMEPAD;
|
||||
if (uni_hid_device_on_device_discovered(address, "Switch2", cod, rssi) != UNI_ERROR_SUCCESS)
|
||||
return true;
|
||||
sw2_instance_t* ins = NULL;
|
||||
for (unsigned i = 0; i < CONFIG_BLUEPAD32_MAX_DEVICES; ++i) {
|
||||
if (sw2_instances[i].state == SW2_OFF &&
|
||||
|
|
@ -754,10 +754,18 @@ bool uni_bt_le_switch2_handle_advertisement(const uint8_t* packet, uint16_t size
|
|||
uni_hid_device_set_name(d, "Switch2");
|
||||
uni_bt_conn_set_state(&d->conn, UNI_BT_CONN_STATE_DEVICE_DISCOVERED);
|
||||
d->conn.rssi = rssi;
|
||||
// Platform admission needs the validated transport and identity metadata.
|
||||
// Keep generic allowlist/RSSI filtering authoritative before touching scan.
|
||||
if (uni_hid_device_on_device_discovered(address, "Switch2", cod, rssi) != UNI_ERROR_SUCCESS) {
|
||||
uni_hid_device_delete(d);
|
||||
return true;
|
||||
}
|
||||
gap_stop_scan(); // Existing BLE lifecycle owns resuming scan under policy.
|
||||
uint8_t status = gap_connect(address, address_type);
|
||||
if (status != ERROR_CODE_SUCCESS)
|
||||
if (status != ERROR_CODE_SUCCESS) {
|
||||
sw2_fail(ins, "BLE connection request failed", status);
|
||||
uni_bt_le_resume_scanning_if_enabled();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -58,6 +58,15 @@ diff --git a/src/components/bluepad32/bt/uni_bt_le.c b/src/components/bluepad32/
|
|||
#include "uni_common.h"
|
||||
#include "uni_config.h"
|
||||
#include "uni_hid_device.h"
|
||||
@@ -93,7 +94,7 @@
|
||||
gap_connect(addr, addr_type);
|
||||
}
|
||||
|
||||
-static void resume_scanning_hint(void) {
|
||||
+void uni_bt_le_resume_scanning_if_enabled(void) {
|
||||
// Resume scanning, only if it was scanning before connecting
|
||||
if (is_scanning) {
|
||||
gap_start_scan();
|
||||
@@ -107,7 +108,7 @@
|
||||
uni_hid_device_t* device;
|
||||
|
||||
|
|
@ -67,6 +76,24 @@ diff --git a/src/components/bluepad32/bt/uni_bt_le.c b/src/components/bluepad32/
|
|||
status = hids_client_disconnect(device->hids_cid);
|
||||
if (status != ERROR_CODE_SUCCESS) {
|
||||
loge("Failed to disconnect HIDS client for hids_cid=%d, status=%d\n", device->hids_cid, status);
|
||||
@@ -118,7 +119,7 @@
|
||||
if (gap_get_connection_type(con_handle) != GAP_CONNECTION_INVALID)
|
||||
gap_disconnect(con_handle);
|
||||
|
||||
- resume_scanning_hint();
|
||||
+ uni_bt_le_resume_scanning_if_enabled();
|
||||
}
|
||||
|
||||
static void get_advertisement_data(const uint8_t* adv_data, uint8_t adv_size, uint16_t* appearance, char* name) {
|
||||
@@ -297,7 +298,7 @@
|
||||
uni_hid_device_connect(device);
|
||||
uni_hid_device_set_ready(device);
|
||||
|
||||
- resume_scanning_hint();
|
||||
+ uni_bt_le_resume_scanning_if_enabled();
|
||||
break;
|
||||
default:
|
||||
loge("HID service client connection failed, err 0x%02x.\n", status);
|
||||
@@ -560,10 +561,22 @@
|
||||
type = hci_event_packet_get_type(packet);
|
||||
switch (type) {
|
||||
|
|
@ -122,7 +149,7 @@ diff --git a/src/components/bluepad32/bt/uni_bt_le.c b/src/components/bluepad32/
|
|||
+ hci_subevent_le_connection_complete_get_status(packet) != ERROR_CODE_SUCCESS) {
|
||||
+ // A failed event has no valid connection handle to disconnect.
|
||||
+ uni_hid_device_delete(device);
|
||||
+ resume_scanning_hint();
|
||||
+ uni_bt_le_resume_scanning_if_enabled();
|
||||
+ break;
|
||||
+ }
|
||||
uni_hid_device_set_connection_handle(device, con_handle);
|
||||
|
|
@ -155,6 +182,36 @@ diff --git a/src/components/bluepad32/bt/uni_bt_le.c b/src/components/bluepad32/
|
|||
|
||||
gap_event_advertising_report_get_address(packet, addr);
|
||||
if (uni_hid_device_get_instance_for_address(addr)) {
|
||||
@@ -826,7 +866,7 @@
|
||||
ARG_UNUSED(packet);
|
||||
ARG_UNUSED(size);
|
||||
|
||||
- resume_scanning_hint();
|
||||
+ uni_bt_le_resume_scanning_if_enabled();
|
||||
}
|
||||
|
||||
void uni_bt_le_list_bonded_keys(void) {
|
||||
@@ -872,6 +912,11 @@
|
||||
gap_delete_bonding((bd_addr_type_t)entry_address_type, entry_address);
|
||||
}
|
||||
logi(".\n");
|
||||
+}
|
||||
+
|
||||
+void uni_bt_le_set_background_scan(bool enabled) {
|
||||
+ gap_set_scan_parameters(0 /* type: passive */, enabled ? 160 : 48 /* interval */,
|
||||
+ enabled ? 16 : 48 /* window */);
|
||||
}
|
||||
|
||||
void uni_bt_le_setup(void) {
|
||||
@@ -921,7 +966,7 @@
|
||||
// scan_parameters_service_client_init();
|
||||
device_information_service_client_init();
|
||||
|
||||
- gap_set_scan_parameters(0 /* type: passive */, 48 /* interval */, 48 /* window */);
|
||||
+ uni_bt_le_set_background_scan(false);
|
||||
}
|
||||
|
||||
void uni_bt_le_scan_start(void) {
|
||||
diff --git a/src/components/bluepad32/controller/uni_gamepad.c b/src/components/bluepad32/controller/uni_gamepad.c
|
||||
--- a/src/components/bluepad32/controller/uni_gamepad.c
|
||||
+++ b/src/components/bluepad32/controller/uni_gamepad.c
|
||||
|
|
@ -168,6 +225,18 @@ diff --git a/src/components/bluepad32/controller/uni_gamepad.c b/src/components/
|
|||
{CONTROLLER_TYPE_MobileTouch, "Mobile Touch"},
|
||||
{CONTROLLER_TYPE_XInputSwitchController, "XInput Switch"},
|
||||
{CONTROLLER_TYPE_PS5Controller, "DualSense"},
|
||||
diff --git a/src/components/bluepad32/include/bt/uni_bt_le.h b/src/components/bluepad32/include/bt/uni_bt_le.h
|
||||
--- a/src/components/bluepad32/include/bt/uni_bt_le.h
|
||||
+++ b/src/components/bluepad32/include/bt/uni_bt_le.h
|
||||
@@ -25,6 +25,8 @@
|
||||
|
||||
void uni_bt_le_scan_start(void);
|
||||
void uni_bt_le_scan_stop(void);
|
||||
+void uni_bt_le_set_background_scan(bool enabled);
|
||||
+void uni_bt_le_resume_scanning_if_enabled(void);
|
||||
|
||||
// Called from uni_hid_device_disconnect()
|
||||
void uni_bt_le_disconnect(uni_hid_device_t* d);
|
||||
diff --git a/src/components/bluepad32/include/controller/uni_controller_list.h b/src/components/bluepad32/include/controller/uni_controller_list.h
|
||||
--- a/src/components/bluepad32/include/controller/uni_controller_list.h
|
||||
+++ b/src/components/bluepad32/include/controller/uni_controller_list.h
|
||||
|
|
|
|||
|
|
@ -48,6 +48,10 @@ constexpr uint32_t kDefaultPairingWindowDurationMs =
|
|||
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. Two fast Switch 2 links starve Classic native PCM.
|
||||
constexpr uint16_t kSwitch2FastInterval = 6;
|
||||
constexpr uint16_t kSwitch2MixedInterval = 24;
|
||||
constexpr uint32_t kSwitch2IntervalSettleMs = 1000;
|
||||
constexpr uint8_t kAllBlePairingMethods =
|
||||
SM_STK_GENERATION_METHOD_JUST_WORKS |
|
||||
SM_STK_GENERATION_METHOD_OOB |
|
||||
|
|
@ -229,6 +233,13 @@ 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]{};
|
||||
uint32_t g_pairing_window_deadline_ms = 0;
|
||||
uint32_t g_pairing_window_duration_ms =
|
||||
kDefaultPairingWindowDurationMs;
|
||||
|
|
@ -359,6 +370,84 @@ int joycon_side(const uni_hid_device_t* device) {
|
|||
return device->product_id == UNI_SW2_JOYCON_R_PID ? 1 : 0;
|
||||
}
|
||||
|
||||
bool waiting_for_joycon_mate(int side = 0) {
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
unsigned physical_count = 0;
|
||||
bool pending = false;
|
||||
bool solo_mate = false;
|
||||
for (const BackendSlot& slot : g_slots) {
|
||||
physical_count += slot.device != nullptr;
|
||||
physical_count += slot.companion != nullptr;
|
||||
pending = pending || (slot.device != nullptr && !slot.active);
|
||||
if (slot.active && slot.companion == nullptr) {
|
||||
const int candidate_side = joycon_side(slot.device);
|
||||
solo_mate = solo_mate ||
|
||||
(candidate_side != 0 && (side == 0 || candidate_side == -side));
|
||||
}
|
||||
}
|
||||
critical_section_exit(&g_state_lock);
|
||||
return physical_count < kSlotCount && !pending && solo_mate;
|
||||
}
|
||||
|
||||
void stop_background_scan() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
// Core 1 only. Count physical links, including Classic setup, rather than
|
||||
// logical players: a merged Joy-Con pair still consumes two LE connections.
|
||||
void apply_radio_connection_policy() {
|
||||
unsigned switch2_links = 0;
|
||||
bool classic_link = false;
|
||||
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)) {
|
||||
++switch2_links;
|
||||
// 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;
|
||||
} else if (type == GAP_CONNECTION_ACL) {
|
||||
classic_link = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
const uint16_t desired = switch2_links >= 2 && classic_link
|
||||
? kSwitch2MixedInterval
|
||||
: kSwitch2FastInterval;
|
||||
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 reversing it.
|
||||
// API success alone does not prove that negotiation completed.
|
||||
continue;
|
||||
}
|
||||
request.interval = 0;
|
||||
if (actual == desired) continue;
|
||||
gap_update_connection_parameters(handle, desired, desired, 0, 600);
|
||||
// Reconcile negotiated state on the configuration timer. Rejected or
|
||||
// incomplete requests are retried at most once per second per link.
|
||||
request = {handle, desired, now_ms};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Caller holds the cross-core state lock. Only Core 1 resets parser state.
|
||||
void clear_switch2_ingress(BackendSlot& slot) {
|
||||
Switch2Ingress& ingress = slot.switch2_ingress;
|
||||
|
|
@ -827,6 +916,7 @@ void publish_all_neutral() {
|
|||
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;
|
||||
|
|
@ -1529,6 +1619,7 @@ void process_clear_pairings(uint32_t now_ms) {
|
|||
}
|
||||
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;
|
||||
|
|
@ -1561,23 +1652,28 @@ 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 = 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) {
|
||||
if (g_connection_policy_state == desired_state &&
|
||||
g_background_scan_active == background_scan) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Classic inquiry and BLE scanning consume radio time and measurably delay
|
||||
// active controller HID traffic. Stop them before every policy transition.
|
||||
// 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) {
|
||||
|
|
@ -1586,15 +1682,20 @@ void apply_connection_policy() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Passive mode still accepts controller-initiated reconnects without
|
||||
// running inquiry. Active discovery is reserved for zero-controller idle
|
||||
// state and the explicit BOOTSEL pairing window.
|
||||
// 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(true);
|
||||
if (active_scan) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1673,6 +1774,7 @@ void process_configuration_timer(btstack_timer_source_t* timer) {
|
|||
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);
|
||||
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
|
||||
|
|
@ -2062,7 +2164,6 @@ void platform_on_init_complete() {
|
|||
hci_add_event_handler(&g_pairing_event_callback);
|
||||
switch2_wake_initialize();
|
||||
refresh_pairing_snapshot();
|
||||
// Keep Bluepad32 autoconnect active whenever at least one slot is free.
|
||||
btstack_run_loop_set_timer_handler(&g_rumble_timer, process_rumble_timer);
|
||||
btstack_run_loop_set_timer(&g_rumble_timer, kRumblePollIntervalMs);
|
||||
btstack_run_loop_add_timer(&g_rumble_timer);
|
||||
|
|
@ -2079,12 +2180,26 @@ void platform_on_init_complete() {
|
|||
|
||||
uni_error_t platform_on_device_discovered(bd_addr_t addr, const char* name,
|
||||
uint16_t cod, uint8_t rssi) {
|
||||
(void)addr;
|
||||
(void)name;
|
||||
(void)cod;
|
||||
(void)rssi;
|
||||
return has_free_slot() &&
|
||||
g_connection_policy_state == ConnectionPolicyState::Open
|
||||
if (!has_free_slot()) {
|
||||
return UNI_ERROR_IGNORE_DEVICE;
|
||||
}
|
||||
if (g_connection_policy_state == ConnectionPolicyState::Open) {
|
||||
return UNI_ERROR_SUCCESS;
|
||||
}
|
||||
if (g_connection_policy_state != ConnectionPolicyState::Passive) {
|
||||
return UNI_ERROR_IGNORE_DEVICE;
|
||||
}
|
||||
const uni_hid_device_t* candidate =
|
||||
uni_hid_device_get_instance_for_address(addr);
|
||||
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;
|
||||
}
|
||||
|
|
@ -2103,6 +2218,7 @@ void platform_on_device_connected(uni_hid_device_t* device) {
|
|||
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] = {};
|
||||
}
|
||||
const int slot_index = reserve_device_slot(device);
|
||||
if (slot_index >= 0) {
|
||||
|
|
@ -2167,7 +2283,9 @@ void platform_on_device_disconnected(uni_hid_device_t* device) {
|
|||
}
|
||||
}
|
||||
// Losing a half frees transport capacity, not another logical player.
|
||||
g_connection_policy_state = ConnectionPolicyState::Uninitialized;
|
||||
if (g_connection_policy_state != ConnectionPolicyState::FailedClosed) {
|
||||
g_connection_policy_state = ConnectionPolicyState::Uninitialized;
|
||||
}
|
||||
recompute_connection_status();
|
||||
}
|
||||
|
||||
|
|
@ -2246,6 +2364,7 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) {
|
|||
connection_identity = current.identity;
|
||||
critical_section_exit(&g_state_lock);
|
||||
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),
|
||||
|
|
@ -2519,6 +2638,7 @@ void bluepad32_input_backend_init() {
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -19,11 +19,9 @@ namespace {
|
|||
|
||||
|
||||
bool incoming_connections = false;
|
||||
int scan_starts = 0;
|
||||
int scan_stops = 0;
|
||||
bool scanning_enabled = false;
|
||||
int classic_scan_starts = 0;
|
||||
int classic_scan_stops = 0;
|
||||
bool aggregate_scanning_enabled = false;
|
||||
bool background_scan_parameters = false;
|
||||
bool classic_scanning_enabled = false;
|
||||
uni_platform* installed_platform = nullptr;
|
||||
bool observed_status_led_on = false;
|
||||
|
|
@ -67,6 +65,11 @@ void require_clear_completion_pending();
|
|||
void require_clear_snapshot_published();
|
||||
void request_repeated_clear_during_disconnect();
|
||||
gap_connection_type_t gap_connection_types[256]{};
|
||||
uint16_t negotiated_intervals[256]{};
|
||||
uint16_t pending_intervals[256]{};
|
||||
unsigned interval_requests[256]{};
|
||||
bool defer_interval_updates = false;
|
||||
bool reject_interval_updates = false;
|
||||
uint8_t xbox_left_trigger = 0;
|
||||
uint8_t xbox_right_trigger = 0;
|
||||
unsigned xbox_quad_calls = 0;
|
||||
|
|
@ -127,6 +130,8 @@ uni_hid_device_t device(
|
|||
result.gamepad = gamepad;
|
||||
result.conn.protocol = protocol;
|
||||
result.conn.handle = static_cast<hci_con_handle_t>(0x40 + idx);
|
||||
negotiated_intervals[result.conn.handle] = 6;
|
||||
pending_intervals[result.conn.handle] = 0;
|
||||
result.conn.btaddr[5] = static_cast<uint8_t>(idx + 1);
|
||||
result.vendor_id = static_cast<uint16_t>(0x1000 + idx);
|
||||
result.product_id = static_cast<uint16_t>(0x2000 + idx);
|
||||
|
|
@ -179,6 +184,17 @@ extern "C" bool uni_switch2_pairing_get(
|
|||
return true;
|
||||
}
|
||||
|
||||
extern "C" bool uni_switch2_pairing_known(
|
||||
uint8_t address_type, const uint8_t address[6]) {
|
||||
for (uint8_t index = 0; index < switch2_pairing_count; ++index) {
|
||||
if (switch2_pairing_types[index] == address_type &&
|
||||
memcmp(switch2_pairings[index], address, sizeof(bd_addr_t)) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
extern "C" bool uni_switch2_pairing_clear(void) {
|
||||
if (!switch2_clear_succeeds) {
|
||||
return false;
|
||||
|
|
@ -268,6 +284,15 @@ uni_hid_device_t* uni_hid_device_get_instance_for_connection_handle(
|
|||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uni_hid_device_t* uni_hid_device_get_instance_for_address(const bd_addr_t address) {
|
||||
for (size_t index = 0; index < lookup_device_count; ++index) {
|
||||
if (memcmp(lookup_devices[index]->conn.btaddr, address, sizeof(bd_addr_t)) == 0) {
|
||||
return lookup_devices[index];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
void uni_hid_device_disconnect(uni_hid_device_t* device) {
|
||||
require_clear_completion_pending();
|
||||
request_repeated_clear_during_disconnect();
|
||||
|
|
@ -281,37 +306,42 @@ void uni_bt_allow_incoming_connections(bool enabled) {
|
|||
|
||||
|
||||
void uni_bt_bredr_scan_start() {
|
||||
++classic_scan_starts;
|
||||
classic_scanning_enabled = true;
|
||||
}
|
||||
|
||||
void uni_bt_bredr_scan_stop() {
|
||||
if (classic_scanning_enabled) {
|
||||
++classic_scan_stops;
|
||||
}
|
||||
classic_scanning_enabled = false;
|
||||
}
|
||||
|
||||
void uni_bt_le_scan_start() {
|
||||
++scan_starts;
|
||||
scanning_enabled = true;
|
||||
}
|
||||
|
||||
void uni_bt_le_scan_stop() {
|
||||
if (scanning_enabled) {
|
||||
++scan_stops;
|
||||
}
|
||||
scanning_enabled = false;
|
||||
}
|
||||
|
||||
void uni_bt_le_set_background_scan(bool enabled) {
|
||||
require(!scanning_enabled, "LE scan timing must change only while scanning is stopped");
|
||||
background_scan_parameters = enabled;
|
||||
}
|
||||
|
||||
void uni_bt_start_scanning_and_autoconnect_unsafe() {
|
||||
require_clear_completion_pending();
|
||||
require_clear_snapshot_published();
|
||||
if (aggregate_scanning_enabled) {
|
||||
return;
|
||||
}
|
||||
aggregate_scanning_enabled = true;
|
||||
uni_bt_bredr_scan_start();
|
||||
uni_bt_le_scan_start();
|
||||
}
|
||||
|
||||
void uni_bt_stop_scanning_unsafe() {
|
||||
if (!aggregate_scanning_enabled) {
|
||||
return;
|
||||
}
|
||||
aggregate_scanning_enabled = false;
|
||||
uni_bt_bredr_scan_stop();
|
||||
uni_bt_le_scan_stop();
|
||||
}
|
||||
|
|
@ -331,6 +361,25 @@ gap_connection_type_t gap_get_connection_type(
|
|||
: GAP_CONNECTION_INVALID;
|
||||
}
|
||||
|
||||
uint16_t gap_le_connection_interval(hci_con_handle_t handle) {
|
||||
require(handle < 256, "interval read must target a valid fake connection");
|
||||
return negotiated_intervals[handle];
|
||||
}
|
||||
|
||||
int gap_update_connection_parameters(hci_con_handle_t handle, uint16_t minimum,
|
||||
uint16_t maximum, uint16_t latency,
|
||||
uint16_t supervision_timeout) {
|
||||
require(handle < 256 && gap_get_connection_type(handle) == GAP_CONNECTION_LE,
|
||||
"interval policy must not update a Classic or invalid link");
|
||||
require(minimum == maximum && latency == 0 && supervision_timeout >= 100,
|
||||
"interval request must retain valid supervision and peripheral latency");
|
||||
++interval_requests[handle];
|
||||
if (reject_interval_updates) return 0x0c;
|
||||
if (defer_interval_updates) pending_intervals[handle] = minimum;
|
||||
else negotiated_intervals[handle] = minimum;
|
||||
return ERROR_CODE_SUCCESS;
|
||||
}
|
||||
|
||||
int gap_link_key_iterator_init(btstack_link_key_iterator_t* iterator) {
|
||||
iterator->index = 0;
|
||||
return 1;
|
||||
|
|
@ -749,7 +798,7 @@ void start_backend() {
|
|||
bluepad32_input_backend_init();
|
||||
platform_on_init_complete();
|
||||
require(incoming_connections && scanning_enabled &&
|
||||
classic_scanning_enabled && scan_starts == 1 &&
|
||||
classic_scanning_enabled &&
|
||||
link_supervision_timeout ==
|
||||
kClassicLinkSupervisionTimeout &&
|
||||
!bondable && accepted_stk_methods == 0 &&
|
||||
|
|
@ -905,6 +954,14 @@ void ready_switch2(uni_hid_device_t& controller) {
|
|||
"Switch2 physical device must become ready");
|
||||
}
|
||||
|
||||
void remember_switch2(const uni_hid_device_t& controller) {
|
||||
require(switch2_pairing_count < UNI_SWITCH2_PAIRING_CAPACITY,
|
||||
"test proprietary trust inventory overflow");
|
||||
switch2_pairing_types[switch2_pairing_count] = controller.switch2_identity_address_type;
|
||||
memcpy(switch2_pairings[switch2_pairing_count], controller.conn.btaddr, sizeof(bd_addr_t));
|
||||
++switch2_pairing_count;
|
||||
}
|
||||
|
||||
Bluepad32SlotSnapshot slot_snapshot(uint8_t index) {
|
||||
Bluepad32SlotSnapshot result{};
|
||||
bluepad32_input_backend_snapshot(index, &result);
|
||||
|
|
@ -1534,6 +1591,256 @@ void test_switch2_admission() {
|
|||
"Pro2 must preserve vertical normal controls and ingest remappable extras");
|
||||
}
|
||||
|
||||
void test_switch2_radio_policy() {
|
||||
start_backend();
|
||||
auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID);
|
||||
auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID);
|
||||
auto classic = device(2, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
auto other_ble = device(3, true, UNI_BT_CONN_PROTOCOL_BLE);
|
||||
ready_switch2(left);
|
||||
ready_switch2(right);
|
||||
platform_on_device_connected(&other_ble);
|
||||
require(platform_on_device_ready(&other_ble) == UNI_ERROR_SUCCESS,
|
||||
"unrelated BLE controller must join");
|
||||
require(negotiated_intervals[left.conn.handle] == 6 &&
|
||||
negotiated_intervals[right.conn.handle] == 6,
|
||||
"a pair without Classic must retain fast intervals");
|
||||
platform_on_device_connected(&classic);
|
||||
require(negotiated_intervals[left.conn.handle] == 24 &&
|
||||
negotiated_intervals[right.conn.handle] == 24 &&
|
||||
negotiated_intervals[other_ble.conn.handle] == 6,
|
||||
"Classic setup must relax both physical halves before native attachment, not unrelated BLE");
|
||||
require(platform_on_device_ready(&classic) == UNI_ERROR_SUCCESS,
|
||||
"Classic controller must complete setup alongside the pair");
|
||||
const auto pair = slot_snapshot(0);
|
||||
platform_on_device_disconnected(&classic);
|
||||
require(negotiated_intervals[left.conn.handle] == 6 &&
|
||||
negotiated_intervals[right.conn.handle] == 6 &&
|
||||
slot_snapshot(0).connection_generation == pair.connection_generation,
|
||||
"Classic departure must restore fast intervals without rebinding the pair");
|
||||
platform_on_device_connected(&classic);
|
||||
require(platform_on_device_ready(&classic) == UNI_ERROR_SUCCESS,
|
||||
"Classic reconnect must complete");
|
||||
platform_on_device_disconnected(&left);
|
||||
require(negotiated_intervals[right.conn.handle] == 6,
|
||||
"a single surviving Switch2 link must return to fast scheduling even with Classic");
|
||||
left = switch2_device(0, UNI_SW2_JOYCON_L_PID);
|
||||
ready_switch2(left);
|
||||
require(negotiated_intervals[left.conn.handle] == 24 &&
|
||||
negotiated_intervals[right.conn.handle] == 24,
|
||||
"physical index and handle reuse must negotiate mixed intervals on the new connection");
|
||||
}
|
||||
|
||||
void test_switch2_radio_settling() {
|
||||
start_backend();
|
||||
auto classic = device(2, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID);
|
||||
auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID);
|
||||
platform_on_device_connected(&classic);
|
||||
require(platform_on_device_ready(&classic) == UNI_ERROR_SUCCESS,
|
||||
"Classic-first connection must become ready");
|
||||
ready_switch2(left);
|
||||
defer_interval_updates = true;
|
||||
now_ms = UINT32_MAX - 10;
|
||||
platform_on_device_connected(&right);
|
||||
require(interval_requests[right.conn.handle] == 0,
|
||||
"pending Switch2 setup must retain ownership of its initial interval");
|
||||
require(platform_on_device_ready(&right) == UNI_ERROR_SUCCESS,
|
||||
"second Switch2 connection must become ready");
|
||||
platform_on_device_disconnected(&classic);
|
||||
// The controller completes the old update after the topology reversed.
|
||||
for (auto* half : {&left, &right}) {
|
||||
negotiated_intervals[half->conn.handle] = pending_intervals[half->conn.handle];
|
||||
}
|
||||
now_ms += 50;
|
||||
process_configuration_timer(&g_configuration_timer);
|
||||
for (auto* half : {&left, &right}) {
|
||||
negotiated_intervals[half->conn.handle] = pending_intervals[half->conn.handle];
|
||||
require(negotiated_intervals[half->conn.handle] == 6,
|
||||
"late mixed-mode completion must not strand a pair at slow intervals across clock wrap");
|
||||
}
|
||||
process_configuration_timer(&g_configuration_timer);
|
||||
defer_interval_updates = false;
|
||||
reject_interval_updates = true;
|
||||
platform_on_device_connected(&classic);
|
||||
const unsigned attempts = interval_requests[left.conn.handle];
|
||||
now_ms += 999;
|
||||
process_configuration_timer(&g_configuration_timer);
|
||||
require(interval_requests[left.conn.handle] == attempts,
|
||||
"rejected negotiation must not flood the HCI command queue");
|
||||
reject_interval_updates = false;
|
||||
++now_ms;
|
||||
process_configuration_timer(&g_configuration_timer);
|
||||
require(negotiated_intervals[left.conn.handle] == 24 &&
|
||||
negotiated_intervals[right.conn.handle] == 24,
|
||||
"transiently rejected negotiation must recover without reconnect or pairing");
|
||||
}
|
||||
|
||||
void test_switch2_mate_reconnect() {
|
||||
start_backend();
|
||||
auto right = switch2_device(0, UNI_SW2_JOYCON_R_PID);
|
||||
auto left = switch2_device(1, UNI_SW2_JOYCON_L_PID);
|
||||
left.conn.btaddr[0] = 0xc1;
|
||||
left.switch2_identity_address_type = BD_ADDR_TYPE_LE_RANDOM;
|
||||
remember_switch2(right);
|
||||
remember_switch2(left);
|
||||
register_lookup_device(&right);
|
||||
register_lookup_device(&left);
|
||||
ready_switch2(right);
|
||||
require(scanning_enabled && background_scan_parameters &&
|
||||
!classic_scanning_enabled && incoming_connections &&
|
||||
!bondable && accepted_stk_methods == 0 &&
|
||||
!switch_pico_switch2_pairing_allowed(),
|
||||
"first remembered half must seek its mate with low-duty BLE and authentication closed");
|
||||
dispatch_pairing_event(HCI_EVENT_USER_CONFIRMATION_REQUEST);
|
||||
dispatch_pairing_event(HCI_EVENT_USER_PASSKEY_REQUEST);
|
||||
require(confirmation_accepts == 0 && passkey_accepts == 0 &&
|
||||
confirmation_rejections == 1 && passkey_rejections == 1,
|
||||
"mate scanning must not authorize new Classic authentication");
|
||||
|
||||
bd_addr_t missing = {1, 2, 3, 4, 5, 6};
|
||||
require(platform_on_device_discovered(missing, "Joy-Con 2 (L)", 0, 0) ==
|
||||
UNI_ERROR_IGNORE_DEVICE,
|
||||
"a device name without a prepared parser candidate cannot authorize passive discovery");
|
||||
const auto reject_left = [&]() {
|
||||
require(platform_on_device_discovered(left.conn.btaddr, "Joy-Con 2 (L)", 0, 0) ==
|
||||
UNI_ERROR_IGNORE_DEVICE &&
|
||||
scanning_enabled && !classic_scanning_enabled &&
|
||||
!switch_pico_switch2_pairing_allowed(),
|
||||
"rejected mate candidates must leave the bounded passive policy unchanged");
|
||||
};
|
||||
++left.conn.btaddr[5];
|
||||
reject_left();
|
||||
--left.conn.btaddr[5];
|
||||
left.product_id = UNI_SW2_JOYCON_R_PID;
|
||||
reject_left();
|
||||
left.product_id = UNI_SW2_PRO_PID;
|
||||
reject_left();
|
||||
left.product_id = UNI_SW2_JOYCON_L_PID;
|
||||
left.conn.protocol = UNI_BT_CONN_PROTOCOL_BR_EDR;
|
||||
reject_left();
|
||||
left.conn.protocol = UNI_BT_CONN_PROTOCOL_BLE;
|
||||
++left.vendor_id;
|
||||
reject_left();
|
||||
--left.vendor_id;
|
||||
left.switch2_identity_valid = false;
|
||||
reject_left();
|
||||
left.switch2_identity_valid = true;
|
||||
left.switch2_identity_address_type = BD_ADDR_TYPE_LE_PUBLIC;
|
||||
reject_left();
|
||||
left.switch2_identity_address_type = BD_ADDR_TYPE_LE_RANDOM;
|
||||
require(platform_on_device_discovered(left.conn.btaddr, nullptr, 0, 0) ==
|
||||
UNI_ERROR_SUCCESS,
|
||||
"remembered opposite static-random half must be admitted outside the pairing window");
|
||||
|
||||
// The parser stops GAP scan before it has a backend slot to reserve.
|
||||
uni_bt_le_scan_stop();
|
||||
platform_on_device_connected(&left);
|
||||
require(!scanning_enabled && !classic_scanning_enabled,
|
||||
"remembered mate setup must pause scan even when GAP already stopped it");
|
||||
platform_on_device_disconnected(&left);
|
||||
require(scanning_enabled && background_scan_parameters && !classic_scanning_enabled,
|
||||
"failed setup must resume remembered mate scanning");
|
||||
uni_bt_le_scan_stop();
|
||||
platform_on_device_connected(&left);
|
||||
require(!scanning_enabled && !classic_scanning_enabled && incoming_connections,
|
||||
"mate setup must pause background scanning while reserving physical capacity");
|
||||
require(platform_on_device_ready(&left) == UNI_ERROR_SUCCESS &&
|
||||
slot_snapshot(0).active && !slot_snapshot(1).active &&
|
||||
controller_identity_equal(slot_snapshot(0).identity, identity_for_device(&left)) &&
|
||||
!scanning_enabled && !classic_scanning_enabled,
|
||||
"ready remembered mate must merge without a pairing window and stop background scanning");
|
||||
auto ordinary = device(2, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
platform_on_device_connected(&ordinary);
|
||||
require(platform_on_device_ready(&ordinary) == UNI_ERROR_SUCCESS &&
|
||||
!scanning_enabled && !classic_scanning_enabled,
|
||||
"a complete pair plus incoming Classic controller must not keep LE scanning");
|
||||
platform_on_device_disconnected(&left);
|
||||
require(scanning_enabled && background_scan_parameters &&
|
||||
!classic_scanning_enabled && slot_snapshot(2).active,
|
||||
"losing a half must resume its mate scan without disturbing an ordinary controller");
|
||||
}
|
||||
|
||||
void test_switch2_mate_pending() {
|
||||
start_backend();
|
||||
auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID);
|
||||
auto right = switch2_device(3, UNI_SW2_JOYCON_R_PID);
|
||||
remember_switch2(left);
|
||||
remember_switch2(right);
|
||||
register_lookup_device(&right);
|
||||
ready_switch2(left);
|
||||
auto first = device(1, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
auto second = device(2, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
auto third = device(3, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
platform_on_device_connected(&first);
|
||||
platform_on_device_connected(&second);
|
||||
require(!scanning_enabled && !classic_scanning_enabled && incoming_connections &&
|
||||
platform_on_device_discovered(right.conn.btaddr, nullptr, 0, 0) ==
|
||||
UNI_ERROR_IGNORE_DEVICE,
|
||||
"any pending setup must pause scanning and reject otherwise valid mates");
|
||||
require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS &&
|
||||
!scanning_enabled,
|
||||
"one resolved setup must not resume mate scanning while another is pending");
|
||||
platform_on_device_disconnected(&second);
|
||||
require(scanning_enabled && background_scan_parameters &&
|
||||
!classic_scanning_enabled &&
|
||||
platform_on_device_discovered(right.conn.btaddr, nullptr, 0, 0) ==
|
||||
UNI_ERROR_SUCCESS,
|
||||
"last pending setup failure must resume passive mate discovery and admission");
|
||||
platform_on_device_connected(&second);
|
||||
require(!scanning_enabled &&
|
||||
platform_on_device_ready(&second) == UNI_ERROR_SUCCESS &&
|
||||
scanning_enabled && !classic_scanning_enabled,
|
||||
"last pending setup becoming ready must resume the unmatched half scan");
|
||||
platform_on_device_connected(&third);
|
||||
require(platform_on_device_ready(&third) == UNI_ERROR_SUCCESS &&
|
||||
!scanning_enabled && !classic_scanning_enabled && !incoming_connections &&
|
||||
platform_on_device_discovered(right.conn.btaddr, nullptr, 0, 0) ==
|
||||
UNI_ERROR_IGNORE_DEVICE,
|
||||
"four physical controllers must stop mate scanning even with an unmatched half");
|
||||
platform_on_device_disconnected(&third);
|
||||
require(scanning_enabled && background_scan_parameters && incoming_connections,
|
||||
"free physical capacity must restore the waiting half scan");
|
||||
|
||||
switch2_clear_succeeds = false;
|
||||
bluepad32_input_backend_clear_pairings();
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(!scanning_enabled && !classic_scanning_enabled && !incoming_connections,
|
||||
"failed trust deletion must explicitly stop a direct background LE scan");
|
||||
platform_on_device_disconnected(&right);
|
||||
platform_on_device_disconnected(&left);
|
||||
bluepad32_input_backend_open_pairing_window();
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(!scanning_enabled && !classic_scanning_enabled && !incoming_connections &&
|
||||
!switch_pico_switch2_pairing_allowed() &&
|
||||
platform_on_device_discovered(right.conn.btaddr, nullptr, 0, 0) ==
|
||||
UNI_ERROR_IGNORE_DEVICE,
|
||||
"late disconnects and pairing requests must not clear the failed-closed latch");
|
||||
}
|
||||
|
||||
void test_switch2_mate_pairing_window() {
|
||||
start_backend();
|
||||
auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID);
|
||||
ready_switch2(left);
|
||||
require(scanning_enabled && background_scan_parameters && !classic_scanning_enabled,
|
||||
"solo half must enter background discovery before opening pairing");
|
||||
bluepad32_input_backend_open_pairing_window();
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(scanning_enabled && !background_scan_parameters && classic_scanning_enabled &&
|
||||
bondable && accepted_stk_methods == kAllBlePairingMethods,
|
||||
"explicit pairing must restore full discovery timing and the existing authentication window");
|
||||
now_ms = g_pairing_window_deadline_ms;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(scanning_enabled && background_scan_parameters && !classic_scanning_enabled &&
|
||||
!bondable && accepted_stk_methods == 0 &&
|
||||
!switch_pico_switch2_pairing_allowed(),
|
||||
"pairing expiry must restore low-duty mate discovery without extending authentication");
|
||||
platform_on_device_disconnected(&left);
|
||||
require(scanning_enabled && !background_scan_parameters && classic_scanning_enabled &&
|
||||
!bondable && accepted_stk_methods == 0,
|
||||
"last controller loss must restore idle discovery defaults without opening pairing");
|
||||
}
|
||||
|
||||
void test_switch2_pairing_inventory() {
|
||||
start_pairing_backend();
|
||||
switch2_pairing_count = 2;
|
||||
|
|
@ -1630,23 +1937,21 @@ void test_ready_order(bool reverse) {
|
|||
}
|
||||
|
||||
if (position + 1 < kSlotCount) {
|
||||
require(scan_stops == 0,
|
||||
"scanning must continue while any slot remains free");
|
||||
require(scanning_enabled && classic_scanning_enabled,
|
||||
"pairing-window discovery must continue while a physical slot remains free");
|
||||
require(incoming_connections,
|
||||
"incoming connections must remain enabled before all slots are ready");
|
||||
}
|
||||
}
|
||||
|
||||
require(scan_stops == 1,
|
||||
"scanning must stop exactly when all four slots are ready");
|
||||
require(!scanning_enabled && !classic_scanning_enabled,
|
||||
"discovery must stop when all four physical slots are full");
|
||||
require(!incoming_connections,
|
||||
"incoming connections must be disabled only when all slots are full");
|
||||
|
||||
for (int slot = 0; slot < kSlotCount; ++slot) {
|
||||
const int starts_before_disconnect = scan_starts;
|
||||
platform_on_device_disconnected(&devices[slot]);
|
||||
require(scan_starts == starts_before_disconnect + 1 &&
|
||||
scanning_enabled && incoming_connections,
|
||||
require(scanning_enabled && classic_scanning_enabled && incoming_connections,
|
||||
"disconnecting any slot must resume connection policy");
|
||||
|
||||
for (int candidate = 0; candidate < kSlotCount; ++candidate) {
|
||||
|
|
@ -1768,12 +2073,6 @@ void test_independent_lifecycle() {
|
|||
require(g_slots[0].device == &aborted && !g_slots[0].active,
|
||||
"connected device must remain identifiable while becoming ready");
|
||||
|
||||
const int starts_before_aborted_disconnect = scan_starts;
|
||||
const int classic_starts_before_aborted_disconnect =
|
||||
classic_scan_starts;
|
||||
const int stops_before_aborted_disconnect = scan_stops;
|
||||
const int classic_stops_before_aborted_disconnect =
|
||||
classic_scan_stops;
|
||||
platform_on_device_disconnected(&aborted);
|
||||
require(g_slots[0].device == nullptr && !g_slots[0].active,
|
||||
"pre-ready disconnect must clear its pending slot identity");
|
||||
|
|
@ -1781,13 +2080,7 @@ void test_independent_lifecycle() {
|
|||
"pre-ready disconnect must invalidate its connection generation");
|
||||
require(g_connection_status == ConnectionStatus::Scanning &&
|
||||
scanning_enabled && classic_scanning_enabled &&
|
||||
incoming_connections &&
|
||||
scan_starts == starts_before_aborted_disconnect + 1 &&
|
||||
classic_scan_starts ==
|
||||
classic_starts_before_aborted_disconnect + 1 &&
|
||||
scan_stops == stops_before_aborted_disconnect + 1 &&
|
||||
classic_scan_stops ==
|
||||
classic_stops_before_aborted_disconnect + 1,
|
||||
incoming_connections,
|
||||
"pre-ready disconnect must restart Classic and BLE scans");
|
||||
|
||||
uni_hid_device_t devices[kSlotCount] = {
|
||||
|
|
@ -1850,7 +2143,6 @@ void test_independent_lifecycle() {
|
|||
|
||||
const uint32_t first_pending_generation =
|
||||
g_slots[0].connection_generation;
|
||||
const int starts_before_first_pending_disconnect = scan_starts;
|
||||
platform_on_device_disconnected(&devices[0]);
|
||||
require(g_slots[0].device == nullptr && !g_slots[0].active,
|
||||
"pre-ready disconnect must clear only its own pending identity");
|
||||
|
|
@ -1864,8 +2156,7 @@ void test_independent_lifecycle() {
|
|||
"pending disconnect beside peers must invalidate its generation");
|
||||
require(g_connection_status == ConnectionStatus::Connecting &&
|
||||
scanning_enabled && classic_scanning_enabled &&
|
||||
incoming_connections &&
|
||||
scan_starts == starts_before_first_pending_disconnect + 1,
|
||||
incoming_connections,
|
||||
"open pairing slot must preserve pending peers and resume scanning");
|
||||
|
||||
for (int slot = 1; slot < kSlotCount; ++slot) {
|
||||
|
|
@ -2025,10 +2316,8 @@ void test_independent_lifecycle() {
|
|||
bluepad32_input_backend_queue_rumble(3, ControllerRumbleOutput{55, 66});
|
||||
const uint32_t disconnected_generation =
|
||||
baseline_connection_generations[3];
|
||||
const int starts_before_slot_three_disconnect = scan_starts;
|
||||
platform_on_device_disconnected(&devices[3]);
|
||||
require(scan_starts == starts_before_slot_three_disconnect + 1 &&
|
||||
scanning_enabled && incoming_connections,
|
||||
require(scanning_enabled && classic_scanning_enabled && incoming_connections,
|
||||
"slot 3 disconnect must resume scanning and incoming connections");
|
||||
require(!read_controller_state(3, &states[3]) &&
|
||||
!states[3].button_north && states[3].left_stick_x == 0,
|
||||
|
|
@ -2162,10 +2451,8 @@ void test_independent_lifecycle() {
|
|||
uni_hid_device_t replacements[kSlotCount] = {
|
||||
device(0), device(1), device(2), device(3)};
|
||||
for (int slot = 0; slot < 3; ++slot) {
|
||||
const int starts_before_disconnect = scan_starts;
|
||||
platform_on_device_disconnected(&devices[slot]);
|
||||
require(scan_starts == starts_before_disconnect + 1 &&
|
||||
scanning_enabled && incoming_connections,
|
||||
require(scanning_enabled && classic_scanning_enabled && incoming_connections,
|
||||
"disconnecting slots 0-2 must resume connection policy");
|
||||
require(!read_controller_state(slot, &states[slot]) &&
|
||||
states[slot].left_stick_x == 0,
|
||||
|
|
@ -3369,14 +3656,14 @@ void test_wake_identity_gates_connections() {
|
|||
require(switch2_wake_initializations == 1 &&
|
||||
g_connection_policy_state ==
|
||||
ConnectionPolicyState::Uninitialized &&
|
||||
scan_starts == 0 && classic_scan_starts == 0 &&
|
||||
!scanning_enabled && !classic_scanning_enabled &&
|
||||
!incoming_connections,
|
||||
"controller discovery started before wake identity was ready");
|
||||
|
||||
switch2_connections_ready = true;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(g_connection_policy_state == ConnectionPolicyState::Open &&
|
||||
scan_starts == 1 && classic_scan_starts == 1 &&
|
||||
scanning_enabled && classic_scanning_enabled &&
|
||||
incoming_connections,
|
||||
"controller discovery did not start after wake identity setup");
|
||||
}
|
||||
|
|
@ -3677,6 +3964,16 @@ int main(int argc, char** argv) {
|
|||
test_switch2_multiple_pairs();
|
||||
} else if (scenario == "switch2-admission") {
|
||||
test_switch2_admission();
|
||||
} else if (scenario == "switch2-radio-policy") {
|
||||
test_switch2_radio_policy();
|
||||
} else if (scenario == "switch2-radio-settling") {
|
||||
test_switch2_radio_settling();
|
||||
} else if (scenario == "switch2-mate-reconnect") {
|
||||
test_switch2_mate_reconnect();
|
||||
} else if (scenario == "switch2-mate-pending") {
|
||||
test_switch2_mate_pending();
|
||||
} else if (scenario == "switch2-mate-pairing-window") {
|
||||
test_switch2_mate_pairing_window();
|
||||
} else if (scenario == "switch2-pairing-inventory") {
|
||||
test_switch2_pairing_inventory();
|
||||
} else if (scenario == "ready-forward") {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ struct btstack_link_key_iterator_t {
|
|||
|
||||
enum {
|
||||
ERROR_CODE_SUCCESS = 0,
|
||||
HCI_CON_HANDLE_INVALID = 0xffff,
|
||||
HCI_EVENT_PACKET = 4,
|
||||
HCI_EVENT_USER_CONFIRMATION_REQUEST = 0x33,
|
||||
HCI_EVENT_USER_PASSKEY_REQUEST = 0x34,
|
||||
|
|
@ -198,6 +199,7 @@ int uni_hid_device_get_idx_for_instance(const uni_hid_device_t* device);
|
|||
void uni_hid_device_disconnect(uni_hid_device_t* device);
|
||||
uni_hid_device_t* uni_hid_device_get_instance_for_connection_handle(
|
||||
hci_con_handle_t handle);
|
||||
uni_hid_device_t* uni_hid_device_get_instance_for_address(const bd_addr_t address);
|
||||
void uni_bt_allow_incoming_connections(bool enabled);
|
||||
void uni_bt_start_scanning_and_autoconnect_unsafe();
|
||||
void uni_bt_stop_scanning_unsafe();
|
||||
|
|
@ -205,9 +207,14 @@ void uni_bt_bredr_scan_start();
|
|||
void uni_bt_bredr_scan_stop();
|
||||
void uni_bt_le_scan_start();
|
||||
void uni_bt_le_scan_stop();
|
||||
void uni_bt_le_set_background_scan(bool enabled);
|
||||
void uni_bt_del_keys_unsafe();
|
||||
gap_connection_type_t gap_get_connection_type(
|
||||
hci_con_handle_t connection_handle);
|
||||
uint16_t gap_le_connection_interval(hci_con_handle_t handle);
|
||||
int gap_update_connection_parameters(hci_con_handle_t handle, uint16_t minimum,
|
||||
uint16_t maximum, uint16_t latency,
|
||||
uint16_t supervision_timeout);
|
||||
int gap_link_key_iterator_init(btstack_link_key_iterator_t* iterator);
|
||||
int gap_link_key_iterator_get_next(
|
||||
btstack_link_key_iterator_t* iterator, bd_addr_t address,
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@
|
|||
#include "protocol_fixture.h"
|
||||
#include "parser/uni_hid_parser_switch2.h"
|
||||
#include "parser/uni_switch2_pairing.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#define PEERS 4
|
||||
#define PEERS CONFIG_BLUEPAD32_MAX_DEVICES
|
||||
#define SERVICE_START 0x100
|
||||
#define INPUT_HANDLE 0x104
|
||||
#define RESPONSE_HANDLE 0x114
|
||||
|
|
@ -17,9 +18,14 @@ static struct fixture_peer peers[PEERS];
|
|||
static btstack_timer_source_t* timers[16];
|
||||
static unsigned timer_count, connected, ready, disconnected, emitted, remembered, listeners;
|
||||
static unsigned connected_events, disconnected_events;
|
||||
static unsigned scan_stops, discovery_calls;
|
||||
static uint16_t expected_discovery_pid;
|
||||
static uint8_t expected_discovery_address_type;
|
||||
static uint32_t now_ms;
|
||||
static bool pairing_allowed, trusted, storage_ok, request_writes, admit, reject_connected;
|
||||
static uint8_t next_write_error;
|
||||
static uint8_t next_connect_error;
|
||||
static bool scan_enabled, scan_running;
|
||||
static const bd_addr_t host_address = {0x10, 0x21, 0x32, 0x43, 0x54, 0x65};
|
||||
static const bd_addr_t controller_address = {0xc0, 0x22, 0x33, 0x44, 0x55, 0x66};
|
||||
static const uint8_t service_uuid[16] = {0xab,0x7d,0xe9,0xbe,0x89,0xfe,0x49,0xad,0x82,0x8f,0x11,0x8f,0x09,0xdf,0x7f,0xd0};
|
||||
|
|
@ -87,8 +93,14 @@ bool uni_switch2_pairing_remember(uint8_t type, const uint8_t address[6]) {
|
|||
return storage_ok;
|
||||
}
|
||||
void gap_local_bd_addr(bd_addr_t address) { memcpy(address, host_address, 6); }
|
||||
void gap_stop_scan(void) {}
|
||||
uint8_t gap_connect(const bd_addr_t address, bd_addr_type_t type) { (void)address; (void)type; ++connected; return 0; }
|
||||
void gap_stop_scan(void) { ++scan_stops; scan_running = false; }
|
||||
void uni_bt_le_resume_scanning_if_enabled(void) { if (scan_enabled) scan_running = true; }
|
||||
uint8_t gap_connect(const bd_addr_t address, bd_addr_type_t type) {
|
||||
(void)address; (void)type; ++connected;
|
||||
uint8_t status = next_connect_error;
|
||||
next_connect_error = 0;
|
||||
return status;
|
||||
}
|
||||
int gap_update_connection_parameters(hci_con_handle_t handle, uint16_t min, uint16_t max, uint16_t latency, uint16_t timeout) {
|
||||
(void)handle; (void)min; (void)max; (void)latency; (void)timeout; return 0;
|
||||
}
|
||||
|
|
@ -121,7 +133,17 @@ uni_hid_device_t* uni_hid_device_get_instance_for_connection_handle(hci_con_hand
|
|||
return peer ? &peer->device : NULL;
|
||||
}
|
||||
uni_error_t uni_hid_device_on_device_discovered(bd_addr_t address, const char* name, uint16_t cod, uint8_t rssi) {
|
||||
(void)address; (void)name; (void)cod; (void)rssi;
|
||||
++discovery_calls;
|
||||
uni_hid_device_t* d = uni_hid_device_get_instance_for_address(address);
|
||||
uint8_t type = 0xff;
|
||||
assert(d && uni_hid_parser_switch2_is_ble_device(d));
|
||||
assert(uni_hid_parser_switch2_identity_address_type(d, &type));
|
||||
(void)name;
|
||||
assert(d->cod == cod && d->conn.rssi == rssi);
|
||||
if (expected_discovery_pid) {
|
||||
assert(d->product_id == expected_discovery_pid);
|
||||
assert(type == expected_discovery_address_type);
|
||||
}
|
||||
return admit ? UNI_ERROR_SUCCESS : (uni_error_t)1;
|
||||
}
|
||||
void uni_hid_device_set_vendor_id(uni_hid_device_t* d, uint16_t value) { d->vendor_id = value; }
|
||||
|
|
@ -419,8 +441,13 @@ static void reset(void) {
|
|||
memset(peers, 0, sizeof(peers));
|
||||
connected = ready = disconnected = emitted = remembered = 0;
|
||||
connected_events = disconnected_events = 0;
|
||||
scan_stops = discovery_calls = 0;
|
||||
expected_discovery_pid = 0;
|
||||
expected_discovery_address_type = BD_ADDR_TYPE_LE_PUBLIC;
|
||||
now_ms = 0;
|
||||
next_write_error = 0;
|
||||
next_connect_error = 0;
|
||||
scan_enabled = scan_running = true;
|
||||
pairing_allowed = trusted = storage_ok = admit = true;
|
||||
request_writes = false;
|
||||
reject_connected = false;
|
||||
|
|
@ -457,6 +484,7 @@ static void test_advertisement_bounds_and_admission(void) {
|
|||
packet[3] = BD_ADDR_TYPE_LE_RANDOM;
|
||||
packet[9] = 0x40; // Resolving private address, not static identity.
|
||||
assert(uni_bt_le_switch2_handle_advertisement(packet, size) && connected == 0);
|
||||
assert(discovery_calls == 0 && scan_stops == 0);
|
||||
advertisement(packet, UNI_SW2_PRO_PID, false);
|
||||
admit = false;
|
||||
assert(uni_bt_le_switch2_handle_advertisement(packet, size) && connected == 0);
|
||||
|
|
@ -466,6 +494,62 @@ static void test_advertisement_bounds_and_admission(void) {
|
|||
assert(uni_hid_parser_switch2_identity_address_type(&peers[0].device, &type) && type == BD_ADDR_TYPE_LE_PUBLIC);
|
||||
}
|
||||
|
||||
static void test_discovery_metadata_and_rejection_isolation(void) {
|
||||
reset();
|
||||
expected_discovery_pid = UNI_SW2_JOYCON_L_PID;
|
||||
struct fixture_peer* mate = connect_peer(UNI_SW2_JOYCON_L_PID, false);
|
||||
discover(mate);
|
||||
subscribe_response(mate);
|
||||
finish_setup(mate);
|
||||
assert(ready == 1 && discovery_calls == 1);
|
||||
const unsigned mate_timers = timer_count;
|
||||
const unsigned mate_listeners = listeners;
|
||||
|
||||
uint8_t packet[64];
|
||||
size_t size = advertisement(packet, UNI_SW2_JOYCON_R_PID, false);
|
||||
packet[3] = BD_ADDR_TYPE_LE_RANDOM;
|
||||
packet[4] ^= 1; // A different static identity from the active mate.
|
||||
bd_addr_t candidate_address;
|
||||
gap_event_advertising_report_get_address(packet, candidate_address);
|
||||
expected_discovery_pid = UNI_SW2_JOYCON_R_PID;
|
||||
expected_discovery_address_type = BD_ADDR_TYPE_LE_RANDOM;
|
||||
admit = false;
|
||||
// More rejections than slots must not exhaust either device or parser storage.
|
||||
for (unsigned i = 0; i <= CONFIG_BLUEPAD32_MAX_DEVICES; ++i) {
|
||||
assert(uni_bt_le_switch2_handle_advertisement(packet, size));
|
||||
assert(discovery_calls == i + 2);
|
||||
assert(!uni_hid_device_get_instance_for_address(candidate_address));
|
||||
for (unsigned slot = 1; slot < PEERS; ++slot) {
|
||||
uint8_t type;
|
||||
assert(!peers[slot].used);
|
||||
assert(!uni_hid_parser_switch2_identity_address_type(&peers[slot].device, &type));
|
||||
}
|
||||
assert(connected == 1 && scan_stops == 1 && disconnected == 0);
|
||||
assert(timer_count == mate_timers && listeners == mate_listeners);
|
||||
assert(mate->used && mate->link_alive && mate->device.conn.state == UNI_BT_CONN_STATE_DEVICE_READY);
|
||||
}
|
||||
|
||||
// A rejected candidate must remain discoverable once policy permits it.
|
||||
admit = true;
|
||||
assert(uni_bt_le_switch2_handle_advertisement(packet, size));
|
||||
assert(uni_hid_device_get_instance_for_address(candidate_address));
|
||||
assert(connected == 2 && scan_stops == 2 && disconnected == 0);
|
||||
unsigned admitted_calls = discovery_calls;
|
||||
assert(uni_bt_le_switch2_handle_advertisement(packet, size));
|
||||
assert(discovery_calls == admitted_calls && connected == 2 && scan_stops == 2);
|
||||
assert(mate->used && mate->link_alive);
|
||||
|
||||
reset();
|
||||
size = advertisement(packet, UNI_SW2_JOYCON_R_PID, false);
|
||||
admit = false;
|
||||
assert(uni_bt_le_switch2_handle_advertisement(packet, size));
|
||||
advance(3000);
|
||||
assert(discovery_calls == 1 && connected == 0 && scan_stops == 0 && disconnected == 0);
|
||||
assert(timer_count == 0 && listeners == 0);
|
||||
for (unsigned slot = 0; slot < PEERS; ++slot)
|
||||
assert(!peers[slot].used);
|
||||
}
|
||||
|
||||
static void test_missing_descriptor_and_setup_timeout(void) {
|
||||
reset();
|
||||
struct fixture_peer* peer = connect_peer(UNI_SW2_PRO_PID, false);
|
||||
|
|
@ -1024,9 +1108,52 @@ static void test_expired_history_does_not_starve_fresh_sequences(void) {
|
|||
assert(uni_hid_parser_switch2_haptics_dropped() == drops + 1);
|
||||
}
|
||||
|
||||
static void test_gatt_busy_retries_without_disconnect_or_sequence_loss(void) {
|
||||
struct fixture_peer* peer = ready_peer(UNI_SW2_PRO_PID, false);
|
||||
uni_switch2_haptics_frame_t frame = native_frame(3, 2, 24);
|
||||
uint32_t drops = uni_hid_parser_switch2_haptics_dropped();
|
||||
next_write_error = GATT_CLIENT_BUSY;
|
||||
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &frame, now_ms));
|
||||
advance(1);
|
||||
assert(!disconnected && peer->rumbles == 0);
|
||||
advance(13);
|
||||
assert(!disconnected && peer->rumbles == 1);
|
||||
assert((peer->rumble[1] & 15) == 0);
|
||||
assert_block(peer, 0, &frame.sides[0]);
|
||||
assert_block(peer, 1, &frame.sides[1]);
|
||||
|
||||
// The SDK uses the same busy status on command writes such as player LEDs.
|
||||
next_write_error = GATT_CLIENT_BUSY;
|
||||
uni_hid_parser_switch2_set_player_leds(&peer->device, 3);
|
||||
assert(!disconnected);
|
||||
advance(13);
|
||||
assert(peer->command[0] == 0x09 && peer->command[8] == 3);
|
||||
acknowledge(peer, false);
|
||||
assert(!disconnected && uni_hid_parser_switch2_haptics_dropped() == drops);
|
||||
}
|
||||
|
||||
static void test_immediate_connection_failure_preserves_reconnect_discovery(void) {
|
||||
reset();
|
||||
uint8_t packet[64];
|
||||
size_t size = advertisement(packet, UNI_SW2_JOYCON_R_PID, false);
|
||||
next_connect_error = ERROR_CODE_COMMAND_DISALLOWED;
|
||||
assert(uni_bt_le_switch2_handle_advertisement(packet, size));
|
||||
assert(scan_running && !peers[0].used && !timer_count && !listeners);
|
||||
assert(uni_bt_le_switch2_handle_advertisement(packet, size));
|
||||
assert(peers[0].used && connected == 2); // The same remembered peer can retry.
|
||||
|
||||
reset();
|
||||
scan_enabled = scan_running = false;
|
||||
next_connect_error = ERROR_CODE_COMMAND_DISALLOWED;
|
||||
assert(uni_bt_le_switch2_handle_advertisement(packet, size));
|
||||
assert(!scan_running && !peers[0].used); // Explicit stop must not be overridden.
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_connected_callback_rejection();
|
||||
test_advertisement_bounds_and_admission();
|
||||
test_discovery_metadata_and_rejection_isolation();
|
||||
test_immediate_connection_failure_preserves_reconnect_discovery();
|
||||
test_missing_descriptor_and_setup_timeout();
|
||||
test_pairing_gate_and_write_ack_order();
|
||||
test_calibration_physical_inputs_and_sensor_units();
|
||||
|
|
@ -1043,6 +1170,7 @@ int main(void) {
|
|||
test_identical_hold_coalescing_refreshes_borrowed_head();
|
||||
test_hold_coalescing_requires_identical_samples_and_side_masks();
|
||||
test_expired_history_does_not_starve_fresh_sequences();
|
||||
test_gatt_busy_retries_without_disconnect_or_sequence_loss();
|
||||
reset();
|
||||
puts("Switch2 protocol boundaries, setup failure, pairing, calibration, physical input, motion and rumble passed");
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -98,6 +98,11 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
|||
"switch2-reverse",
|
||||
"switch2-multiple-pairs",
|
||||
"switch2-admission",
|
||||
"switch2-radio-policy",
|
||||
"switch2-radio-settling",
|
||||
"switch2-mate-reconnect",
|
||||
"switch2-mate-pending",
|
||||
"switch2-mate-pairing-window",
|
||||
"switch2-pairing-inventory",
|
||||
"switch2-hd-pro",
|
||||
"switch2-hd-solo-left",
|
||||
|
|
|
|||
|
|
@ -3,10 +3,15 @@ from __future__ import annotations
|
|||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
|
||||
|
||||
from prepare_bluepad32 import prepare_bluepad32
|
||||
|
||||
|
||||
def test_switch2_parser_protocol_and_lifecycle(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
|
|
@ -25,6 +30,12 @@ def test_switch2_parser_protocol_and_lifecycle(tmp_path: Path) -> None:
|
|||
)
|
||||
if btstack is None:
|
||||
pytest.skip("Pico SDK BTstack headers required; configure firmware or set PICO_SDK_PATH")
|
||||
prepared = prepare_bluepad32(
|
||||
root / "external" / "bluepad32",
|
||||
root / "patches" / "bluepad32-sdl3-imu.patch",
|
||||
tmp_path / "bluepad32-src",
|
||||
)
|
||||
component = prepared / "src" / "components" / "bluepad32"
|
||||
executable = tmp_path / "switch2_parser_native_test"
|
||||
subprocess.run(
|
||||
[
|
||||
|
|
@ -39,7 +50,7 @@ def test_switch2_parser_protocol_and_lifecycle(tmp_path: Path) -> None:
|
|||
"-DENABLE_CLASSIC",
|
||||
f"-I{root / 'tests' / 'switch2_parser_native_stubs'}",
|
||||
f"-I{root / 'bluepad32_config'}",
|
||||
f"-I{root / 'external' / 'bluepad32' / 'src' / 'components' / 'bluepad32' / 'include'}",
|
||||
f"-I{component / 'include'}",
|
||||
f"-I{btstack}",
|
||||
f"-I{btstack.parent / '3rd-party' / 'bluedroid' / 'encoder' / 'include'}",
|
||||
f"-I{btstack.parent / '3rd-party' / 'bluedroid' / 'decoder' / 'include'}",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue