Gate controller pairing behind BOOTSEL
This commit is contained in:
parent
fac919a798
commit
4a73680bd2
24 changed files with 934 additions and 145 deletions
|
|
@ -90,10 +90,14 @@ add_executable(switch-pico
|
||||||
switch_haptics.cpp
|
switch_haptics.cpp
|
||||||
)
|
)
|
||||||
if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
||||||
target_sources(switch-pico PRIVATE bluepad32_input_backend.cpp)
|
target_sources(switch-pico PRIVATE
|
||||||
|
bluepad32_input_backend.cpp
|
||||||
|
bootsel_pairing_button.cpp
|
||||||
|
)
|
||||||
target_compile_definitions(switch-pico PRIVATE
|
target_compile_definitions(switch-pico PRIVATE
|
||||||
SWITCH_PICO_BLUEPAD32=1
|
SWITCH_PICO_BLUEPAD32=1
|
||||||
SWITCH_PICO_HID_INSTANCE_COUNT=4
|
SWITCH_PICO_HID_INSTANCE_COUNT=4
|
||||||
|
PICO_FLASH_ASSUME_CORE1_SAFE=0
|
||||||
)
|
)
|
||||||
else()
|
else()
|
||||||
target_compile_definitions(switch-pico PRIVATE
|
target_compile_definitions(switch-pico PRIVATE
|
||||||
|
|
@ -125,6 +129,7 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
||||||
pico_btstack_classic
|
pico_btstack_classic
|
||||||
pico_btstack_cyw43
|
pico_btstack_cyw43
|
||||||
pico_multicore
|
pico_multicore
|
||||||
|
pico_flash
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
|
||||||
46
README.md
46
README.md
|
|
@ -46,40 +46,38 @@ The default `python3 build.py` command and `firmware/switch-pico.*` artifacts re
|
||||||
|
|
||||||
Both `build.py --aio` and direct AIO CMake configuration apply `patches/bluepad32-sdl3-imu.patch` idempotently before compiling Bluepad32. The patch makes supported motion controllers use SDL3-equivalent axes and fixed-point units before conversion to Nintendo samples. It intentionally leaves the dependency worktree dirty; the committed submodule revision remains Bluepad32 4.2.0.
|
Both `build.py --aio` and direct AIO CMake configuration apply `patches/bluepad32-sdl3-imu.patch` idempotently before compiling Bluepad32. The patch makes supported motion controllers use SDL3-equivalent axes and fixed-point units before conversion to Nintendo samples. It intentionally leaves the dependency worktree dirty; the committed submodule revision remains Bluepad32 4.2.0.
|
||||||
|
|
||||||
### Pairing two controllers
|
### Pairing up to four controllers
|
||||||
|
|
||||||
1. Flash and connect the Pico 2 W to the Switch.
|
1. Flash and connect the Pico 2 W to the Switch.
|
||||||
2. Enable `System Settings → Controllers and Sensors → Pro Controller Wired Communication`.
|
2. Enable `System Settings → Controllers and Sensors → Pro Controller Wired Communication`.
|
||||||
3. Put the first controller into Bluetooth pairing mode:
|
3. Hold the Pico's BOOTSEL button for about two seconds, until the onboard LED starts double-blinking. This opens a 60-second pairing window.
|
||||||
|
4. Put a controller into Bluetooth pairing mode:
|
||||||
- DualSense: hold Create + PS.
|
- DualSense: hold Create + PS.
|
||||||
- DualShock 4: hold Share + PS.
|
- DualShock 4: hold Share + PS.
|
||||||
- Switch Pro: press its sync button.
|
- Switch Pro: press its sync button.
|
||||||
- Xbox Bluetooth controller: hold its pair button.
|
- Xbox Bluetooth controller: hold its pair button.
|
||||||
- 8BitDo: use a Bluetooth mode supported by Bluepad32; use Switch/S mode when motion is required.
|
- 8BitDo: use a Bluetooth mode supported by Bluepad32; use Switch/S mode when motion is required.
|
||||||
4. Wait for the first controller to connect and become ready. The LED continues slow-blinking because the second slot remains open. Pairing keys persist across Pico reboots.
|
5. Wait for the controller's player light to settle. Repeat step 4 for additional controllers while the window remains open. Holding BOOTSEL again extends the window by 60 seconds from that point.
|
||||||
5. Put the second controller into pairing mode and wait for it to connect and become ready. The LED turns solid only after both controllers are active.
|
|
||||||
|
|
||||||
During initial setup, pairing order determines the initial slot assignment: the first controller paired occupies slot 0, and the second occupies slot 1. Pairing keys persist, so both controllers can reconnect after a Pico reboot without re-pairing. Slot numbers are not permanently bound to physical controllers: while one controller remains connected, a returning controller fills the other open slot; after a reboot or whenever both slots are empty, whichever persisted controller reconnects first receives slot 0, so the physical controllers can swap USB interfaces.
|
Pairing order determines the initial USB slot assignment. Up to four controllers map 1:1 to the four emulated Switch Pro Controller interfaces.
|
||||||
|
|
||||||
|
Outside the BOOTSEL-open window, Bluetooth discovery and incoming connections are disabled. The Pico does not scan for or reconnect disconnected controllers while locked. Pairing keys still persist, but reconnecting a previously paired controller also requires opening the BOOTSEL window before pressing its normal power button.
|
||||||
|
|
||||||
### LED meanings and device state
|
### LED meanings and device state
|
||||||
|
|
||||||
The Pico 2 W onboard LED reports the overall Bluetooth state:
|
The Pico 2 W onboard LED reports the overall Bluetooth state:
|
||||||
- **Slow blink (0.5 s period)**: at least one slot is open and scanning for a Bluetooth controller.
|
- **Double blink**: the bounded pairing window is open.
|
||||||
- **Fast blink (0.1 s period)**: at least one controller is connected but not yet ready (handshake in progress).
|
- **Fast blink**: a controller connection is still completing its handshake.
|
||||||
- **Solid**: both slots are filled and both controllers are ready for input.
|
- **Solid**: at least one controller is active.
|
||||||
- **Solid immediately after boot that never starts blinking**: Bluepad32 initialization did not complete; check firmware flashing and UART logs.
|
- **Slow blink**: no controller is active and pairing is locked.
|
||||||
|
- **Solid immediately after boot that never transitions**: Bluepad32 initialization did not complete; check firmware flashing and UART logs.
|
||||||
The LED transitions to slow blink as soon as any slot becomes empty (e.g., a controller is turned off or unpaired). Scanning resumes automatically.
|
|
||||||
|
|
||||||
### Managing controller disconnect and reconnect
|
### Managing controller disconnect and reconnect
|
||||||
|
|
||||||
Controllers can disconnect and reconnect independently:
|
- **Disconnect a controller**: its slot immediately publishes neutral buttons, sticks, and motion. Other connected controllers are unaffected.
|
||||||
- **Disconnect one controller**: that controller's slot becomes empty. The LED transitions to slow blink if both slots are no longer filled. The other controller continues sending input.
|
- **Reconnect a paired controller**: hold BOOTSEL until the LED double-blinks, then power on the controller normally.
|
||||||
- **Reconnect while the other controller remains connected**: the returning controller fills the only open slot, preserving the current assignment. The LED transitions through fast blink and back to solid.
|
- **Pair a new controller**: hold BOOTSEL until the LED double-blinks, then put the controller into its explicit Bluetooth pairing mode.
|
||||||
- **Reconnect after both slots become empty or after reboot**: reconnection/autoconnect order determines the assignments. The physical controllers can swap USB interfaces if their order changes.
|
- **Pairing window expires**: scanning and incoming connections stop; already connected controllers remain connected.
|
||||||
- **Turn off or unpair a controller**: delete it from Bluetooth settings on the Pico or reset pairing entirely using Bluepad32 commands. It will no longer auto-reconnect; the slot remains open for a new controller.
|
|
||||||
|
|
||||||
When a controller disconnects, the Pico immediately publishes neutral buttons, sticks, and motion for that slot. The other controller is unaffected.
|
|
||||||
|
|
||||||
### Controller capabilities
|
### Controller capabilities
|
||||||
|
|
||||||
|
|
@ -98,15 +96,15 @@ Rumble effects are per-slot and independent. The Switch sends rumble commands to
|
||||||
|
|
||||||
### Hardware validation
|
### Hardware validation
|
||||||
|
|
||||||
The dual-interface AIO build has been verified on a real Switch with two DualSense controllers: the Switch assigned two controller slots; buttons, sticks, calibrated motion, and rumble remained independent; disconnecting either controller left the other working; scanning resumed and the disconnected controller reconnected to the open slot.
|
The four-interface AIO build has been verified on a real Switch with two DualSense controllers: the Switch assigned independent controller slots, and buttons, sticks, calibrated motion, rumble, and disconnect isolation worked per controller. Fresh DualSense pairing through the BOOTSEL-open window has also been verified on hardware.
|
||||||
|
|
||||||
To reproduce the validation:
|
To reproduce the validation:
|
||||||
|
|
||||||
1. **Verify USB enumeration**: Connect the Pico 2 W to a USB host (PC, Mac, or USB analyzer). Confirm that two HID devices are present (e.g., `lsusb -v` on Linux shows interface 0 and interface 1, both with Product ID 0x2009).
|
1. **Verify USB enumeration**: Connect the Pico 2 W to a USB host or analyzer. Confirm that four HID interfaces are present, using IN/OUT endpoint pairs `0x81/0x01` through `0x84/0x04`.
|
||||||
2. **Verify Bluetooth pairing**: Pair two controllers via Bluepad32. Confirm the LED transitions from scanning → fast blink → solid.
|
2. **Verify Bluetooth pairing**: Hold BOOTSEL until the LED double-blinks, put a controller into explicit pairing mode, and confirm its player light settles.
|
||||||
3. **Verify input on one controller**: Move sticks, press buttons, and check that the controller paired first during initial setup appears in slot 0.
|
3. **Verify input on one controller**: Move sticks and press buttons; confirm only its assigned Switch slot changes.
|
||||||
4. **Verify input on two controllers**: Move sticks on the controller paired second during initial setup, and confirm its inputs appear in slot 1 while the first controller is unaffected.
|
4. **Verify input on two controllers**: Move the second controller independently and confirm the first controller's slot is unaffected.
|
||||||
5. **Verify disconnect and reconnect**: Turn off one controller while leaving the other connected. The LED reverts to slow blink. Turn the disconnected controller back on; it reconnects to the only open slot. Verify the occupied slot continues reporting the other controller's input.
|
5. **Verify the pairing gate**: Disconnect a controller and confirm it does not reconnect while locked. Open the BOOTSEL window, power it on, and confirm it can connect.
|
||||||
6. **Verify rumble per slot**: Send rumble to interface 0 and confirm only the slot 0 controller vibrates. Send rumble to interface 1 and confirm only the slot 1 controller vibrates.
|
6. **Verify rumble per slot**: Send rumble to interface 0 and confirm only the slot 0 controller vibrates. Send rumble to interface 1 and confirm only the slot 1 controller vibrates.
|
||||||
7. **Verify motion**: Enable gyro/accel on both controllers. Rotate each controller independently and confirm that motion is per-slot (rotating controller 0 does not affect controller 1's IMU output).
|
7. **Verify motion**: Enable gyro/accel on both controllers. Rotate each controller independently and confirm that motion is per-slot (rotating controller 0 does not affect controller 1's IMU output).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
#define ENABLE_LE_DATA_LENGTH_EXTENSION
|
#define ENABLE_LE_DATA_LENGTH_EXTENSION
|
||||||
#define ENABLE_LE_PERIPHERAL
|
#define ENABLE_LE_PERIPHERAL
|
||||||
#define ENABLE_LE_PRIVACY_ADDRESS_RESOLUTION
|
#define ENABLE_LE_PRIVACY_ADDRESS_RESOLUTION
|
||||||
|
#define ENABLE_LE_RESOLVING_LIST
|
||||||
#define ENABLE_LE_SECURE_CONNECTIONS
|
#define ENABLE_LE_SECURE_CONNECTIONS
|
||||||
#else
|
#else
|
||||||
#error "BP32: ENABLE_BLE should be defined"
|
#error "BP32: ENABLE_BLE should be defined"
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@
|
||||||
|
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
#include <btstack_run_loop.h>
|
#include <btstack_run_loop.h>
|
||||||
#include <pico/critical_section.h>
|
#include <pico/critical_section.h>
|
||||||
#include <pico/cyw43_arch.h>
|
#include <pico/cyw43_arch.h>
|
||||||
|
#include <pico/flash.h>
|
||||||
#include <pico/multicore.h>
|
#include <pico/multicore.h>
|
||||||
#include <pico/stdlib.h>
|
#include <pico/stdlib.h>
|
||||||
#include <uni.h>
|
#include <uni.h>
|
||||||
|
|
@ -20,6 +22,7 @@ constexpr int32_t kTriggerThreshold = (kTriggerMaximum * 35) / 100;
|
||||||
constexpr uint16_t kRumbleDurationMs = 50;
|
constexpr uint16_t kRumbleDurationMs = 50;
|
||||||
constexpr uint32_t kRumblePollIntervalMs = 5;
|
constexpr uint32_t kRumblePollIntervalMs = 5;
|
||||||
constexpr uint8_t kSlotCount = BLUEPAD32_INPUT_BACKEND_SLOT_COUNT;
|
constexpr uint8_t kSlotCount = BLUEPAD32_INPUT_BACKEND_SLOT_COUNT;
|
||||||
|
constexpr uint32_t kPairingWindowDurationMs = 60000;
|
||||||
|
|
||||||
static_assert(kSlotCount == 4);
|
static_assert(kSlotCount == 4);
|
||||||
static_assert(SWITCH_PICO_HID_INSTANCE_COUNT == kSlotCount);
|
static_assert(SWITCH_PICO_HID_INSTANCE_COUNT == kSlotCount);
|
||||||
|
|
@ -31,6 +34,14 @@ enum class ConnectionStatus {
|
||||||
Ready,
|
Ready,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class ConnectionPolicyState {
|
||||||
|
Uninitialized,
|
||||||
|
Open,
|
||||||
|
Locked,
|
||||||
|
Paused,
|
||||||
|
FailedClosed,
|
||||||
|
};
|
||||||
|
|
||||||
struct RumbleEnvelope {
|
struct RumbleEnvelope {
|
||||||
uint8_t slot;
|
uint8_t slot;
|
||||||
uint32_t connection_generation;
|
uint32_t connection_generation;
|
||||||
|
|
@ -51,16 +62,22 @@ struct BackendSlot {
|
||||||
critical_section_t g_state_lock;
|
critical_section_t g_state_lock;
|
||||||
BackendSlot g_slots[kSlotCount];
|
BackendSlot g_slots[kSlotCount];
|
||||||
|
|
||||||
// These acknowledgement generations are only read or written by Core 0.
|
// These acknowledgement generations and the pairing request producer are only
|
||||||
|
// used by Core 0. The request is transferred under the cross-core state lock.
|
||||||
uint32_t g_consumed_generation[kSlotCount]{};
|
uint32_t g_consumed_generation[kSlotCount]{};
|
||||||
uint32_t g_last_snapshot_generation[kSlotCount]{};
|
uint32_t g_last_snapshot_generation[kSlotCount]{};
|
||||||
|
bool g_pairing_window_requested = false;
|
||||||
bool g_initialized = false;
|
bool g_initialized = false;
|
||||||
bool g_started = false;
|
bool g_started = false;
|
||||||
|
|
||||||
// The timer and connection status are only read or written by Core 1 / BTstack.
|
// These fields are only read or written by Core 1 / BTstack.
|
||||||
btstack_timer_source_t g_rumble_timer{};
|
btstack_timer_source_t g_rumble_timer{};
|
||||||
ConnectionStatus g_connection_status = ConnectionStatus::Initializing;
|
ConnectionStatus g_connection_status = ConnectionStatus::Initializing;
|
||||||
|
ConnectionPolicyState g_connection_policy_state =
|
||||||
|
ConnectionPolicyState::Uninitialized;
|
||||||
|
uint32_t g_pairing_window_deadline_ms = 0;
|
||||||
uint16_t g_status_led_tick = 0;
|
uint16_t g_status_led_tick = 0;
|
||||||
|
bool g_pairing_window_open = false;
|
||||||
bool g_status_led_on = false;
|
bool g_status_led_on = false;
|
||||||
|
|
||||||
SwitchInputState make_neutral_state() {
|
SwitchInputState make_neutral_state() {
|
||||||
|
|
@ -76,6 +93,26 @@ bool valid_slot(uint8_t slot) {
|
||||||
return slot < kSlotCount;
|
return slot < kSlotCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool has_free_slot() {
|
||||||
|
critical_section_enter_blocking(&g_state_lock);
|
||||||
|
bool free_slot = false;
|
||||||
|
for (const BackendSlot& slot : g_slots) {
|
||||||
|
free_slot = free_slot || slot.device == nullptr;
|
||||||
|
}
|
||||||
|
critical_section_exit(&g_state_lock);
|
||||||
|
return free_slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has_active_controller() {
|
||||||
|
critical_section_enter_blocking(&g_state_lock);
|
||||||
|
bool active_controller = false;
|
||||||
|
for (const BackendSlot& slot : g_slots) {
|
||||||
|
active_controller = active_controller || slot.active;
|
||||||
|
}
|
||||||
|
critical_section_exit(&g_state_lock);
|
||||||
|
return active_controller;
|
||||||
|
}
|
||||||
|
|
||||||
int slot_for_device(const uni_hid_device_t* device) {
|
int slot_for_device(const uni_hid_device_t* device) {
|
||||||
if (device == nullptr) {
|
if (device == nullptr) {
|
||||||
return -1;
|
return -1;
|
||||||
|
|
@ -125,6 +162,8 @@ void publish_all_neutral() {
|
||||||
}
|
}
|
||||||
critical_section_exit(&g_state_lock);
|
critical_section_exit(&g_state_lock);
|
||||||
g_connection_status = ConnectionStatus::Initializing;
|
g_connection_status = ConnectionStatus::Initializing;
|
||||||
|
g_connection_policy_state = ConnectionPolicyState::FailedClosed;
|
||||||
|
g_pairing_window_open = false;
|
||||||
g_status_led_tick = 0;
|
g_status_led_tick = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,21 +279,79 @@ SwitchInputState map_gamepad(const uni_gamepad_t& gamepad) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool update_pairing_window(uint32_t now_ms) {
|
||||||
|
critical_section_enter_blocking(&g_state_lock);
|
||||||
|
const bool requested = g_pairing_window_requested;
|
||||||
|
g_pairing_window_requested = false;
|
||||||
|
critical_section_exit(&g_state_lock);
|
||||||
|
|
||||||
|
if (requested) {
|
||||||
|
g_pairing_window_open = true;
|
||||||
|
g_pairing_window_deadline_ms = now_ms + kPairingWindowDurationMs;
|
||||||
|
g_status_led_tick = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (g_pairing_window_open && !pairing_window_active_at(now_ms)) {
|
||||||
|
g_pairing_window_open = false;
|
||||||
|
g_status_led_tick = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_connection_policy(uint32_t now_ms) {
|
||||||
|
const bool free_slot = has_free_slot();
|
||||||
|
const bool pairing_open = pairing_window_active_at(now_ms);
|
||||||
|
if ((!free_slot &&
|
||||||
|
g_connection_policy_state == ConnectionPolicyState::Paused) ||
|
||||||
|
(free_slot && pairing_open &&
|
||||||
|
g_connection_policy_state == ConnectionPolicyState::Open) ||
|
||||||
|
(free_slot && !pairing_open &&
|
||||||
|
g_connection_policy_state == ConnectionPolicyState::Locked)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uni_bt_allow_incoming_connections(false);
|
||||||
|
uni_bt_stop_scanning_unsafe();
|
||||||
|
|
||||||
|
if (!free_slot) {
|
||||||
|
g_connection_policy_state = ConnectionPolicyState::Paused;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pairing_open) {
|
||||||
|
g_connection_policy_state = ConnectionPolicyState::Locked;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Bluepad32's normal pairing/autoconnect path while the physical
|
||||||
|
// BOOTSEL gesture has explicitly opened the pairing window.
|
||||||
|
uni_bt_allow_incoming_connections(true);
|
||||||
|
uni_bt_start_scanning_and_autoconnect_unsafe();
|
||||||
|
g_connection_policy_state = ConnectionPolicyState::Open;
|
||||||
|
}
|
||||||
|
|
||||||
void update_status_led() {
|
void update_status_led() {
|
||||||
++g_status_led_tick;
|
++g_status_led_tick;
|
||||||
bool led_on = false;
|
bool led_on = false;
|
||||||
switch (g_connection_status) {
|
|
||||||
case ConnectionStatus::Initializing:
|
if (pairing_window_active_at(btstack_run_loop_get_time_ms())) {
|
||||||
case ConnectionStatus::Ready:
|
const uint16_t phase = g_status_led_tick % 200;
|
||||||
led_on = true;
|
led_on = phase < 20 || (phase >= 40 && phase < 60);
|
||||||
break;
|
} else if (g_connection_status == ConnectionStatus::Connecting) {
|
||||||
case ConnectionStatus::Scanning:
|
led_on = (g_status_led_tick % 40) < 20;
|
||||||
led_on = (g_status_led_tick % 200) < 100;
|
} else if (g_connection_status == ConnectionStatus::Initializing ||
|
||||||
break;
|
has_active_controller()) {
|
||||||
case ConnectionStatus::Connecting:
|
led_on = true;
|
||||||
led_on = (g_status_led_tick % 40) < 20;
|
} else {
|
||||||
break;
|
led_on = (g_status_led_tick % 200) < 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (led_on != g_status_led_on) {
|
if (led_on != g_status_led_on) {
|
||||||
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, led_on);
|
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, led_on);
|
||||||
g_status_led_on = led_on;
|
g_status_led_on = led_on;
|
||||||
|
|
@ -262,6 +359,11 @@ void update_status_led() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void process_rumble_timer(btstack_timer_source_t* timer) {
|
void process_rumble_timer(btstack_timer_source_t* timer) {
|
||||||
|
const uint32_t now_ms = btstack_run_loop_get_time_ms();
|
||||||
|
if (update_pairing_window(now_ms)) {
|
||||||
|
apply_connection_policy(now_ms);
|
||||||
|
}
|
||||||
|
|
||||||
for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) {
|
for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) {
|
||||||
RumbleEnvelope envelope{};
|
RumbleEnvelope envelope{};
|
||||||
uni_hid_device_t* device = nullptr;
|
uni_hid_device_t* device = nullptr;
|
||||||
|
|
@ -274,14 +376,16 @@ void process_rumble_timer(btstack_timer_source_t* timer) {
|
||||||
slot.rumble_pending = false;
|
slot.rumble_pending = false;
|
||||||
dispatch = envelope.slot == slot_index && slot.active &&
|
dispatch = envelope.slot == slot_index && slot.active &&
|
||||||
slot.device != nullptr &&
|
slot.device != nullptr &&
|
||||||
envelope.connection_generation == slot.connection_generation;
|
envelope.connection_generation ==
|
||||||
|
slot.connection_generation;
|
||||||
if (dispatch) {
|
if (dispatch) {
|
||||||
device = slot.device;
|
device = slot.device;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
critical_section_exit(&g_state_lock);
|
critical_section_exit(&g_state_lock);
|
||||||
|
|
||||||
if (dispatch && device->report_parser.play_dual_rumble != nullptr) {
|
if (dispatch &&
|
||||||
|
device->report_parser.play_dual_rumble != nullptr) {
|
||||||
device->report_parser.play_dual_rumble(
|
device->report_parser.play_dual_rumble(
|
||||||
device, 0, kRumbleDurationMs,
|
device, 0, kRumbleDurationMs,
|
||||||
envelope.rumble.high_frequency_magnitude,
|
envelope.rumble.high_frequency_magnitude,
|
||||||
|
|
@ -294,19 +398,12 @@ void process_rumble_timer(btstack_timer_source_t* timer) {
|
||||||
btstack_run_loop_add_timer(timer);
|
btstack_run_loop_add_timer(timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
void resume_connections() {
|
|
||||||
uni_bt_allow_incoming_connections(true);
|
|
||||||
uni_bt_start_scanning_and_autoconnect_unsafe();
|
|
||||||
}
|
|
||||||
void recompute_connection_status() {
|
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_connection_status = compute_connection_status();
|
||||||
g_status_led_tick = 0;
|
g_status_led_tick = 0;
|
||||||
if (g_connection_status == ConnectionStatus::Ready) {
|
apply_connection_policy(now_ms);
|
||||||
uni_bt_stop_scanning_unsafe();
|
|
||||||
uni_bt_allow_incoming_connections(false);
|
|
||||||
} else {
|
|
||||||
resume_connections();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void platform_init(int argc, const char** argv) {
|
void platform_init(int argc, const char** argv) {
|
||||||
|
|
@ -315,23 +412,35 @@ void platform_init(int argc, const char** argv) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void platform_on_init_complete() {
|
void platform_on_init_complete() {
|
||||||
|
// Discovery and incoming connections remain disabled until BOOTSEL opens
|
||||||
|
// the bounded pairing window.
|
||||||
btstack_run_loop_set_timer_handler(&g_rumble_timer, process_rumble_timer);
|
btstack_run_loop_set_timer_handler(&g_rumble_timer, process_rumble_timer);
|
||||||
btstack_run_loop_set_timer(&g_rumble_timer, kRumblePollIntervalMs);
|
btstack_run_loop_set_timer(&g_rumble_timer, kRumblePollIntervalMs);
|
||||||
btstack_run_loop_add_timer(&g_rumble_timer);
|
btstack_run_loop_add_timer(&g_rumble_timer);
|
||||||
recompute_connection_status();
|
recompute_connection_status();
|
||||||
}
|
}
|
||||||
|
|
||||||
uni_error_t platform_on_device_discovered(bd_addr_t addr, const char* name, uint16_t cod, uint8_t rssi) {
|
uni_error_t platform_on_device_discovered(bd_addr_t addr, const char* name,
|
||||||
|
uint16_t cod, uint8_t rssi) {
|
||||||
(void)addr;
|
(void)addr;
|
||||||
(void)name;
|
(void)name;
|
||||||
(void)cod;
|
(void)cod;
|
||||||
(void)rssi;
|
(void)rssi;
|
||||||
return compute_connection_status() == ConnectionStatus::Ready
|
return has_free_slot() &&
|
||||||
? UNI_ERROR_IGNORE_DEVICE
|
g_connection_policy_state == ConnectionPolicyState::Open
|
||||||
: UNI_ERROR_SUCCESS;
|
? UNI_ERROR_SUCCESS
|
||||||
|
: UNI_ERROR_IGNORE_DEVICE;
|
||||||
}
|
}
|
||||||
|
|
||||||
void platform_on_device_connected(uni_hid_device_t* device) {
|
void platform_on_device_connected(uni_hid_device_t* device) {
|
||||||
|
if (device == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (g_connection_policy_state != ConnectionPolicyState::Open) {
|
||||||
|
uni_hid_device_disconnect(device);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const int slot_index = slot_for_device(device);
|
const int slot_index = slot_for_device(device);
|
||||||
if (slot_index < 0) {
|
if (slot_index < 0) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -453,22 +562,26 @@ uni_platform* get_platform() {
|
||||||
return &platform;
|
return &platform;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[noreturn]] void halt_wireless_backend() {
|
||||||
|
publish_all_neutral();
|
||||||
|
while (true) {
|
||||||
|
tight_loop_contents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[[noreturn]] void core1_main() {
|
[[noreturn]] void core1_main() {
|
||||||
|
if (!flash_safe_execute_core_init()) {
|
||||||
|
halt_wireless_backend();
|
||||||
|
}
|
||||||
if (cyw43_arch_init() != 0) {
|
if (cyw43_arch_init() != 0) {
|
||||||
publish_all_neutral();
|
halt_wireless_backend();
|
||||||
while (true) {
|
|
||||||
tight_loop_contents();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, true);
|
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, true);
|
||||||
g_status_led_on = true;
|
g_status_led_on = true;
|
||||||
|
|
||||||
uni_platform_set_custom(get_platform());
|
uni_platform_set_custom(get_platform());
|
||||||
if (uni_init(0, nullptr) != 0) {
|
if (uni_init(0, nullptr) != 0) {
|
||||||
publish_all_neutral();
|
halt_wireless_backend();
|
||||||
while (true) {
|
|
||||||
tight_loop_contents();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
btstack_run_loop_execute();
|
btstack_run_loop_execute();
|
||||||
|
|
@ -493,6 +606,11 @@ void bluepad32_input_backend_init() {
|
||||||
g_consumed_generation[slot_index] = 0;
|
g_consumed_generation[slot_index] = 0;
|
||||||
g_last_snapshot_generation[slot_index] = 0;
|
g_last_snapshot_generation[slot_index] = 0;
|
||||||
}
|
}
|
||||||
|
g_pairing_window_requested = false;
|
||||||
|
g_connection_status = ConnectionStatus::Initializing;
|
||||||
|
g_connection_policy_state = ConnectionPolicyState::Uninitialized;
|
||||||
|
g_pairing_window_deadline_ms = 0;
|
||||||
|
g_pairing_window_open = false;
|
||||||
g_initialized = true;
|
g_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -503,11 +621,28 @@ void bluepad32_input_backend_start() {
|
||||||
if (g_started) {
|
if (g_started) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Core 0 services USB from flash while Core 1 owns BTstack. Register both
|
||||||
|
// cores before either side can initiate a flash-backed BTstack TLV write.
|
||||||
|
if (!flash_safe_execute_core_init()) {
|
||||||
|
g_connection_policy_state = ConnectionPolicyState::FailedClosed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
g_started = true;
|
g_started = true;
|
||||||
multicore_launch_core1(core1_main);
|
multicore_launch_core1(core1_main);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void bluepad32_input_backend_open_pairing_window() {
|
||||||
|
if (!g_initialized) {
|
||||||
|
bluepad32_input_backend_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
critical_section_enter_blocking(&g_state_lock);
|
||||||
|
g_pairing_window_requested = true;
|
||||||
|
critical_section_exit(&g_state_lock);
|
||||||
|
}
|
||||||
|
|
||||||
bool bluepad32_input_backend_snapshot(uint8_t slot_index, SwitchInputState* out) {
|
bool bluepad32_input_backend_snapshot(uint8_t slot_index, SwitchInputState* out) {
|
||||||
if (out == nullptr || !valid_slot(slot_index)) {
|
if (out == nullptr || !valid_slot(slot_index)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ constexpr uint8_t BLUEPAD32_INPUT_BACKEND_SLOT_COUNT = 4;
|
||||||
|
|
||||||
void bluepad32_input_backend_init();
|
void bluepad32_input_backend_init();
|
||||||
void bluepad32_input_backend_start();
|
void bluepad32_input_backend_start();
|
||||||
|
void bluepad32_input_backend_open_pairing_window();
|
||||||
bool bluepad32_input_backend_snapshot(uint8_t slot, SwitchInputState* out);
|
bool bluepad32_input_backend_snapshot(uint8_t slot, SwitchInputState* out);
|
||||||
void bluepad32_input_backend_report_sent(uint8_t slot);
|
void bluepad32_input_backend_report_sent(uint8_t slot);
|
||||||
void bluepad32_input_backend_queue_rumble(uint8_t slot,
|
void bluepad32_input_backend_queue_rumble(uint8_t slot,
|
||||||
|
|
|
||||||
99
bootsel_pairing_button.cpp
Normal file
99
bootsel_pairing_button.cpp
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
#include "bootsel_pairing_button.h"
|
||||||
|
|
||||||
|
#include "hardware/gpio.h"
|
||||||
|
#include "hardware/structs/ioqspi.h"
|
||||||
|
#include "hardware/structs/sio.h"
|
||||||
|
#include "pico/flash.h"
|
||||||
|
#include "pico/time.h"
|
||||||
|
#if PICO_RP2350
|
||||||
|
#include "hardware/regs/sio.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr uint32_t kPollIntervalMs = 100;
|
||||||
|
constexpr uint32_t kFlashSafeTimeoutMs = 100;
|
||||||
|
constexpr uint32_t kQspiCsPinIndex = 1;
|
||||||
|
|
||||||
|
BootselPairingButtonHoldFsm g_hold_fsm;
|
||||||
|
uint32_t g_last_sample_ms = 0;
|
||||||
|
|
||||||
|
// QSPI CSn sampling adapted from awalol/DS5Dongle's button_functions.cpp:
|
||||||
|
// https://github.com/awalol/DS5Dongle/blob/master/src/button_functions.cpp
|
||||||
|
// Copyright (c) 2026 awalol; used under the MIT License.
|
||||||
|
//
|
||||||
|
// This callback and everything it executes while CSn is floated must remain in
|
||||||
|
// SRAM or be an inlined hardware-register operation. In particular, do not add
|
||||||
|
// logging or ordinary flash-backed data access here.
|
||||||
|
void __no_inline_not_in_flash_func(read_bootsel_callback)(void* parameter) {
|
||||||
|
auto* pressed = static_cast<bool*>(parameter);
|
||||||
|
|
||||||
|
hw_write_masked(
|
||||||
|
&ioqspi_hw->io[kQspiCsPinIndex].ctrl,
|
||||||
|
GPIO_OVERRIDE_LOW << IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_LSB,
|
||||||
|
IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_BITS);
|
||||||
|
|
||||||
|
for (volatile uint32_t delay = 0; delay < 1000; ++delay) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#if PICO_RP2350
|
||||||
|
*pressed =
|
||||||
|
(sio_hw->gpio_hi_in & SIO_GPIO_HI_IN_QSPI_CSN_BITS) == 0;
|
||||||
|
#else
|
||||||
|
*pressed = (sio_hw->gpio_hi_in & (1u << kQspiCsPinIndex)) == 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
hw_write_masked(
|
||||||
|
&ioqspi_hw->io[kQspiCsPinIndex].ctrl,
|
||||||
|
GPIO_OVERRIDE_NORMAL << IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_LSB,
|
||||||
|
IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_BITS);
|
||||||
|
}
|
||||||
|
|
||||||
|
BootselPairingButtonSample sample_bootsel() {
|
||||||
|
bool pressed = false;
|
||||||
|
const int result = flash_safe_execute(read_bootsel_callback, &pressed,
|
||||||
|
kFlashSafeTimeoutMs);
|
||||||
|
if (result != PICO_OK) {
|
||||||
|
return BootselPairingButtonSample::kUnread;
|
||||||
|
}
|
||||||
|
return pressed ? BootselPairingButtonSample::kPressed
|
||||||
|
: BootselPairingButtonSample::kReleased;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool BootselPairingButtonHoldFsm::update(
|
||||||
|
BootselPairingButtonSample sample) {
|
||||||
|
if (sample == BootselPairingButtonSample::kUnread) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sample == BootselPairingButtonSample::kReleased) {
|
||||||
|
pressed_samples_ = 0;
|
||||||
|
hold_reported_ = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hold_reported_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
++pressed_samples_;
|
||||||
|
if (pressed_samples_ < kHoldSamples) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hold_reported_ = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bootsel_pairing_button_task() {
|
||||||
|
const uint32_t now_ms =
|
||||||
|
static_cast<uint32_t>(to_ms_since_boot(get_absolute_time()));
|
||||||
|
if (now_ms - g_last_sample_ms < kPollIntervalMs) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
g_last_sample_ms = now_ms;
|
||||||
|
|
||||||
|
return g_hold_fsm.update(sample_bootsel());
|
||||||
|
}
|
||||||
23
bootsel_pairing_button.h
Normal file
23
bootsel_pairing_button.h
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
enum class BootselPairingButtonSample : uint8_t {
|
||||||
|
kUnread,
|
||||||
|
kReleased,
|
||||||
|
kPressed,
|
||||||
|
};
|
||||||
|
|
||||||
|
class BootselPairingButtonHoldFsm {
|
||||||
|
public:
|
||||||
|
static constexpr uint8_t kHoldSamples = 20;
|
||||||
|
|
||||||
|
bool update(BootselPairingButtonSample sample);
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint8_t pressed_samples_ = 0;
|
||||||
|
bool hold_reported_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Polls BOOTSEL at 10 Hz. Returns true once when a 20-sample hold completes.
|
||||||
|
bool bootsel_pairing_button_task();
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -7,6 +7,7 @@
|
||||||
#include "hardware/uart.h"
|
#include "hardware/uart.h"
|
||||||
#else
|
#else
|
||||||
#include "bluepad32_input_backend.h"
|
#include "bluepad32_input_backend.h"
|
||||||
|
#include "bootsel_pairing_button.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef SWITCH_PICO_LOG
|
#ifdef SWITCH_PICO_LOG
|
||||||
|
|
@ -232,6 +233,9 @@ int main() {
|
||||||
while (true) {
|
while (true) {
|
||||||
tud_task(); // USB device tasks
|
tud_task(); // USB device tasks
|
||||||
#ifdef SWITCH_PICO_BLUEPAD32
|
#ifdef SWITCH_PICO_BLUEPAD32
|
||||||
|
if (bootsel_pairing_button_task()) {
|
||||||
|
bluepad32_input_backend_open_pairing_window();
|
||||||
|
}
|
||||||
for (uint8_t instance = 0;
|
for (uint8_t instance = 0;
|
||||||
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
|
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
|
||||||
bluepad32_input_backend_snapshot(instance,
|
bluepad32_input_backend_snapshot(instance,
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,29 @@
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
|
||||||
bool incoming_connections = false;
|
bool incoming_connections = false;
|
||||||
int scan_starts = 0;
|
int scan_starts = 0;
|
||||||
int scan_stops = 0;
|
int scan_stops = 0;
|
||||||
bool scanning_enabled = false;
|
bool scanning_enabled = false;
|
||||||
|
int classic_scan_starts = 0;
|
||||||
|
int classic_scan_stops = 0;
|
||||||
|
bool classic_scanning_enabled = false;
|
||||||
uni_platform* installed_platform = nullptr;
|
uni_platform* installed_platform = nullptr;
|
||||||
bool observed_status_led_on = false;
|
bool observed_status_led_on = false;
|
||||||
int observed_status_led_writes = 0;
|
int observed_status_led_writes = 0;
|
||||||
|
uint32_t now_ms = 0;
|
||||||
|
|
||||||
|
bool flash_core_init_result = true;
|
||||||
|
int flash_core_init_calls = 0;
|
||||||
|
int core1_launch_calls = 0;
|
||||||
|
int cyw43_init_calls = 0;
|
||||||
|
int uni_init_calls = 0;
|
||||||
|
int device_disconnect_calls = 0;
|
||||||
|
uni_hid_device_t* last_disconnected_device = nullptr;
|
||||||
|
|
||||||
|
struct CoreStopped {};
|
||||||
|
|
||||||
|
|
||||||
void require(bool condition, const char* message) {
|
void require(bool condition, const char* message) {
|
||||||
if (!condition) {
|
if (!condition) {
|
||||||
|
|
@ -29,10 +45,14 @@ void play_rumble(uni_hid_device_t* device, uint16_t, uint16_t,
|
||||||
device->last_low = low;
|
device->last_low = low;
|
||||||
}
|
}
|
||||||
|
|
||||||
uni_hid_device_t device(int idx, bool gamepad = true) {
|
uni_hid_device_t device(
|
||||||
|
int idx, bool gamepad = true,
|
||||||
|
uni_bt_conn_protocol_t protocol = UNI_BT_CONN_PROTOCOL_NONE) {
|
||||||
uni_hid_device_t result{};
|
uni_hid_device_t result{};
|
||||||
result.idx = idx;
|
result.idx = idx;
|
||||||
result.gamepad = gamepad;
|
result.gamepad = gamepad;
|
||||||
|
result.conn.protocol = protocol;
|
||||||
|
result.conn.btaddr[5] = static_cast<uint8_t>(idx + 1);
|
||||||
result.report_parser.play_dual_rumble = play_rumble;
|
result.report_parser.play_dual_rumble = play_rumble;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -47,29 +67,66 @@ int uni_hid_device_get_idx_for_instance(const uni_hid_device_t* device) {
|
||||||
return device == nullptr ? -1 : device->idx;
|
return device == nullptr ? -1 : device->idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void uni_hid_device_disconnect(uni_hid_device_t* device) {
|
||||||
|
++device_disconnect_calls;
|
||||||
|
last_disconnected_device = device;
|
||||||
|
}
|
||||||
|
|
||||||
void uni_bt_allow_incoming_connections(bool enabled) {
|
void uni_bt_allow_incoming_connections(bool enabled) {
|
||||||
incoming_connections = enabled;
|
incoming_connections = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
void uni_bt_start_scanning_and_autoconnect_unsafe() {
|
|
||||||
|
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;
|
++scan_starts;
|
||||||
scanning_enabled = true;
|
scanning_enabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void uni_bt_stop_scanning_unsafe() {
|
void uni_bt_le_scan_stop() {
|
||||||
++scan_stops;
|
if (scanning_enabled) {
|
||||||
|
++scan_stops;
|
||||||
|
}
|
||||||
scanning_enabled = false;
|
scanning_enabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void uni_bt_start_scanning_and_autoconnect_unsafe() {
|
||||||
|
uni_bt_bredr_scan_start();
|
||||||
|
uni_bt_le_scan_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void uni_bt_stop_scanning_unsafe() {
|
||||||
|
uni_bt_bredr_scan_stop();
|
||||||
|
uni_bt_le_scan_stop();
|
||||||
|
}
|
||||||
|
|
||||||
void uni_platform_set_custom(uni_platform* platform) {
|
void uni_platform_set_custom(uni_platform* platform) {
|
||||||
installed_platform = platform;
|
installed_platform = platform;
|
||||||
}
|
}
|
||||||
|
|
||||||
int uni_init(int, const char**) {
|
int uni_init(int, const char**) {
|
||||||
|
++uni_init_calls;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool flash_safe_execute_core_init() {
|
||||||
|
++flash_core_init_calls;
|
||||||
|
return flash_core_init_result;
|
||||||
|
}
|
||||||
|
|
||||||
int cyw43_arch_init() {
|
int cyw43_arch_init() {
|
||||||
|
++cyw43_init_calls;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,7 +135,18 @@ void cyw43_arch_gpio_put(int, bool enabled) {
|
||||||
++observed_status_led_writes;
|
++observed_status_led_writes;
|
||||||
}
|
}
|
||||||
|
|
||||||
void multicore_launch_core1(void (*)()) {}
|
void multicore_launch_core1(void (*)()) {
|
||||||
|
++core1_launch_calls;
|
||||||
|
}
|
||||||
|
|
||||||
|
void tight_loop_contents() {
|
||||||
|
throw CoreStopped{};
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t btstack_run_loop_get_time_ms() {
|
||||||
|
return now_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#include "../bluepad32_input_backend.cpp"
|
#include "../bluepad32_input_backend.cpp"
|
||||||
|
|
||||||
|
|
@ -87,9 +155,22 @@ namespace {
|
||||||
void start_backend() {
|
void start_backend() {
|
||||||
bluepad32_input_backend_init();
|
bluepad32_input_backend_init();
|
||||||
platform_on_init_complete();
|
platform_on_init_complete();
|
||||||
require(incoming_connections, "initialization must allow connections");
|
require(!incoming_connections,
|
||||||
require(scan_starts == 1, "initialization must start scanning");
|
"initialization must keep incoming connections closed");
|
||||||
|
require(scan_starts == 0,
|
||||||
|
"initialization must not scan before a BOOTSEL request");
|
||||||
}
|
}
|
||||||
|
void start_pairing_backend() {
|
||||||
|
start_backend();
|
||||||
|
bluepad32_input_backend_open_pairing_window();
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(g_connection_policy_state == ConnectionPolicyState::Open &&
|
||||||
|
scanning_enabled && classic_scanning_enabled &&
|
||||||
|
incoming_connections,
|
||||||
|
"test connection setup requires an open pairing window");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void tick_backend_timer(int ticks) {
|
void tick_backend_timer(int ticks) {
|
||||||
for (int tick = 0; tick < ticks; ++tick) {
|
for (int tick = 0; tick < ticks; ++tick) {
|
||||||
|
|
@ -98,7 +179,7 @@ void tick_backend_timer(int ticks) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_ready_order(bool reverse) {
|
void test_ready_order(bool reverse) {
|
||||||
start_backend();
|
start_pairing_backend();
|
||||||
uni_hid_device_t devices[kSlotCount] = {
|
uni_hid_device_t devices[kSlotCount] = {
|
||||||
device(0), device(1), device(2), device(3)};
|
device(0), device(1), device(2), device(3)};
|
||||||
uni_hid_device_t replacements[kSlotCount] = {
|
uni_hid_device_t replacements[kSlotCount] = {
|
||||||
|
|
@ -107,23 +188,7 @@ void test_ready_order(bool reverse) {
|
||||||
const int backward[kSlotCount] = {3, 2, 1, 0};
|
const int backward[kSlotCount] = {3, 2, 1, 0};
|
||||||
const int* order = reverse ? backward : forward;
|
const int* order = reverse ? backward : forward;
|
||||||
|
|
||||||
tick_backend_timer(99);
|
|
||||||
require(observed_status_led_on,
|
|
||||||
"scanning LED must stay on for the first slow-blink half-cycle");
|
|
||||||
tick_backend_timer(1);
|
|
||||||
require(!observed_status_led_on,
|
|
||||||
"scanning LED must turn off at the slow-blink half-cycle");
|
|
||||||
|
|
||||||
platform_on_device_connected(&devices[order[0]]);
|
platform_on_device_connected(&devices[order[0]]);
|
||||||
tick_backend_timer(19);
|
|
||||||
require(observed_status_led_on,
|
|
||||||
"connecting LED must stay on for the first fast-blink half-cycle");
|
|
||||||
tick_backend_timer(1);
|
|
||||||
require(!observed_status_led_on,
|
|
||||||
"connecting LED must turn off at the fast-blink half-cycle");
|
|
||||||
tick_backend_timer(20);
|
|
||||||
require(observed_status_led_on,
|
|
||||||
"connecting LED must turn on for the next fast-blink cycle");
|
|
||||||
|
|
||||||
for (int position = 0; position < kSlotCount; ++position) {
|
for (int position = 0; position < kSlotCount; ++position) {
|
||||||
const int slot = order[position];
|
const int slot = order[position];
|
||||||
|
|
@ -146,14 +211,6 @@ void test_ready_order(bool reverse) {
|
||||||
"scanning must continue while any slot remains free");
|
"scanning must continue while any slot remains free");
|
||||||
require(incoming_connections,
|
require(incoming_connections,
|
||||||
"incoming connections must remain enabled before all slots are ready");
|
"incoming connections must remain enabled before all slots are ready");
|
||||||
if (position == 0) {
|
|
||||||
tick_backend_timer(99);
|
|
||||||
require(observed_status_led_on,
|
|
||||||
"a partially full backend must use the scanning LED on half-cycle");
|
|
||||||
tick_backend_timer(1);
|
|
||||||
require(!observed_status_led_on,
|
|
||||||
"a partially full backend must use the scanning LED off half-cycle");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,14 +218,6 @@ void test_ready_order(bool reverse) {
|
||||||
"scanning must stop exactly when all four slots are ready");
|
"scanning must stop exactly when all four slots are ready");
|
||||||
require(!incoming_connections,
|
require(!incoming_connections,
|
||||||
"incoming connections must be disabled only when all slots are full");
|
"incoming connections must be disabled only when all slots are full");
|
||||||
tick_backend_timer(1);
|
|
||||||
require(observed_status_led_on,
|
|
||||||
"four ready slots must turn the status LED on");
|
|
||||||
const int ready_led_writes = observed_status_led_writes;
|
|
||||||
tick_backend_timer(200);
|
|
||||||
require(observed_status_led_on &&
|
|
||||||
observed_status_led_writes == ready_led_writes,
|
|
||||||
"four ready slots must keep the status LED solid");
|
|
||||||
|
|
||||||
for (int slot = 0; slot < kSlotCount; ++slot) {
|
for (int slot = 0; slot < kSlotCount; ++slot) {
|
||||||
const int starts_before_disconnect = scan_starts;
|
const int starts_before_disconnect = scan_starts;
|
||||||
|
|
@ -245,19 +294,13 @@ void test_rejections() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_independent_lifecycle() {
|
void test_independent_lifecycle() {
|
||||||
start_backend();
|
start_pairing_backend();
|
||||||
|
|
||||||
uni_hid_device_t aborted = device(0);
|
uni_hid_device_t aborted = device(0);
|
||||||
const uint32_t aborted_generation = g_slots[0].connection_generation;
|
const uint32_t aborted_generation = g_slots[0].connection_generation;
|
||||||
platform_on_device_connected(&aborted);
|
platform_on_device_connected(&aborted);
|
||||||
require(g_slots[0].device == &aborted && !g_slots[0].active,
|
require(g_slots[0].device == &aborted && !g_slots[0].active,
|
||||||
"connected device must remain identifiable while becoming ready");
|
"connected device must remain identifiable while becoming ready");
|
||||||
tick_backend_timer(19);
|
|
||||||
require(observed_status_led_on,
|
|
||||||
"a lone pending connection must use the fast LED on half-cycle");
|
|
||||||
tick_backend_timer(1);
|
|
||||||
require(!observed_status_led_on,
|
|
||||||
"a lone pending connection must use the fast LED off half-cycle");
|
|
||||||
|
|
||||||
const int starts_before_aborted_disconnect = scan_starts;
|
const int starts_before_aborted_disconnect = scan_starts;
|
||||||
platform_on_device_disconnected(&aborted);
|
platform_on_device_disconnected(&aborted);
|
||||||
|
|
@ -266,15 +309,10 @@ void test_independent_lifecycle() {
|
||||||
require(g_slots[0].connection_generation == aborted_generation + 1,
|
require(g_slots[0].connection_generation == aborted_generation + 1,
|
||||||
"pre-ready disconnect must invalidate its connection generation");
|
"pre-ready disconnect must invalidate its connection generation");
|
||||||
require(g_connection_status == ConnectionStatus::Scanning &&
|
require(g_connection_status == ConnectionStatus::Scanning &&
|
||||||
scanning_enabled && incoming_connections &&
|
scanning_enabled && classic_scanning_enabled &&
|
||||||
scan_starts == starts_before_aborted_disconnect + 1,
|
incoming_connections &&
|
||||||
"pre-ready disconnect with no peer must resume scanning");
|
scan_starts == starts_before_aborted_disconnect,
|
||||||
tick_backend_timer(99);
|
"pre-ready disconnect must preserve the open pairing scan");
|
||||||
require(observed_status_led_on,
|
|
||||||
"pre-ready disconnect must restore the slow LED on half-cycle");
|
|
||||||
tick_backend_timer(1);
|
|
||||||
require(!observed_status_led_on,
|
|
||||||
"pre-ready disconnect must restore the slow LED off half-cycle");
|
|
||||||
|
|
||||||
uni_hid_device_t devices[kSlotCount] = {
|
uni_hid_device_t devices[kSlotCount] = {
|
||||||
device(0), device(1), device(2), device(3)};
|
device(0), device(1), device(2), device(3)};
|
||||||
|
|
@ -284,12 +322,6 @@ void test_independent_lifecycle() {
|
||||||
!g_slots[slot].active,
|
!g_slots[slot].active,
|
||||||
"each pending device must retain its indexed identity");
|
"each pending device must retain its indexed identity");
|
||||||
}
|
}
|
||||||
tick_backend_timer(19);
|
|
||||||
require(observed_status_led_on,
|
|
||||||
"concurrent pending devices must use the fast LED on half-cycle");
|
|
||||||
tick_backend_timer(1);
|
|
||||||
require(!observed_status_led_on,
|
|
||||||
"concurrent pending devices must use the fast LED off half-cycle");
|
|
||||||
|
|
||||||
const uint32_t first_pending_generation =
|
const uint32_t first_pending_generation =
|
||||||
g_slots[0].connection_generation;
|
g_slots[0].connection_generation;
|
||||||
|
|
@ -306,15 +338,10 @@ void test_independent_lifecycle() {
|
||||||
first_pending_generation + 1,
|
first_pending_generation + 1,
|
||||||
"pending disconnect beside peers must invalidate its generation");
|
"pending disconnect beside peers must invalidate its generation");
|
||||||
require(g_connection_status == ConnectionStatus::Connecting &&
|
require(g_connection_status == ConnectionStatus::Connecting &&
|
||||||
scanning_enabled && incoming_connections &&
|
scanning_enabled && classic_scanning_enabled &&
|
||||||
|
incoming_connections &&
|
||||||
scan_starts == starts_before_first_pending_disconnect + 1,
|
scan_starts == starts_before_first_pending_disconnect + 1,
|
||||||
"open slot must scan while other slots remain connecting");
|
"open pairing slot must preserve pending peers and resume scanning");
|
||||||
tick_backend_timer(19);
|
|
||||||
require(observed_status_led_on,
|
|
||||||
"surviving pending devices must retain the fast LED on half-cycle");
|
|
||||||
tick_backend_timer(1);
|
|
||||||
require(!observed_status_led_on,
|
|
||||||
"surviving pending devices must retain the fast LED off half-cycle");
|
|
||||||
|
|
||||||
for (int slot = 1; slot < kSlotCount; ++slot) {
|
for (int slot = 1; slot < kSlotCount; ++slot) {
|
||||||
require(platform_on_device_ready(&devices[slot]) == UNI_ERROR_SUCCESS,
|
require(platform_on_device_ready(&devices[slot]) == UNI_ERROR_SUCCESS,
|
||||||
|
|
@ -412,11 +439,6 @@ void test_independent_lifecycle() {
|
||||||
devices[0].last_low == 115 &&
|
devices[0].last_low == 115 &&
|
||||||
devices[0].last_high == 116,
|
devices[0].last_high == 116,
|
||||||
"slot 0 rumble must continue while slot 3 is disconnected");
|
"slot 0 rumble must continue while slot 3 is disconnected");
|
||||||
require(observed_status_led_on,
|
|
||||||
"disconnect scanning must use the slow LED on half-cycle");
|
|
||||||
tick_backend_timer(1);
|
|
||||||
require(!observed_status_led_on,
|
|
||||||
"disconnect scanning must reach the slow LED off half-cycle");
|
|
||||||
|
|
||||||
uni_hid_device_t slot_three_replacement = device(3);
|
uni_hid_device_t slot_three_replacement = device(3);
|
||||||
require(platform_on_device_ready(&slot_three_replacement) ==
|
require(platform_on_device_ready(&slot_three_replacement) ==
|
||||||
|
|
@ -425,13 +447,6 @@ void test_independent_lifecycle() {
|
||||||
process_rumble_timer(&g_rumble_timer);
|
process_rumble_timer(&g_rumble_timer);
|
||||||
require(slot_three_replacement.rumble_calls == 0,
|
require(slot_three_replacement.rumble_calls == 0,
|
||||||
"slot 3 replacement must not receive disconnected device rumble");
|
"slot 3 replacement must not receive disconnected device rumble");
|
||||||
require(observed_status_led_on,
|
|
||||||
"slot 3 replacement must restore the solid ready LED");
|
|
||||||
const int replacement_ready_led_writes = observed_status_led_writes;
|
|
||||||
tick_backend_timer(100);
|
|
||||||
require(observed_status_led_on &&
|
|
||||||
observed_status_led_writes == replacement_ready_led_writes,
|
|
||||||
"replacement quartet must keep the ready LED solid");
|
|
||||||
|
|
||||||
g_slots[3].pending_rumble = {
|
g_slots[3].pending_rumble = {
|
||||||
3, disconnected_generation, SwitchRumbleOutput{77, 88}};
|
3, disconnected_generation, SwitchRumbleOutput{77, 88}};
|
||||||
|
|
@ -536,6 +551,128 @@ void test_independent_lifecycle() {
|
||||||
"slot 0 activity must not evict the slot 3 mailbox");
|
"slot 0 activity must not evict the slot 3 mailbox");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void test_pairing_window_policy() {
|
||||||
|
bd_addr_t address = {1, 2, 3, 4, 5, 6};
|
||||||
|
|
||||||
|
bluepad32_input_backend_init();
|
||||||
|
require(platform_on_device_discovered(address, "controller", 0, 0) ==
|
||||||
|
UNI_ERROR_IGNORE_DEVICE,
|
||||||
|
"discovery must remain closed before backend initialization");
|
||||||
|
|
||||||
|
start_backend();
|
||||||
|
require(g_connection_policy_state == ConnectionPolicyState::Locked &&
|
||||||
|
!classic_scanning_enabled && !scanning_enabled &&
|
||||||
|
!incoming_connections,
|
||||||
|
"boot must disable all discovery and incoming connections");
|
||||||
|
require(platform_on_device_discovered(address, "controller", 0, 0) ==
|
||||||
|
UNI_ERROR_IGNORE_DEVICE,
|
||||||
|
"locked policy must reject every discovery");
|
||||||
|
|
||||||
|
uni_hid_device_t rejected = device(0);
|
||||||
|
platform_on_device_connected(&rejected);
|
||||||
|
require(device_disconnect_calls == 1 &&
|
||||||
|
last_disconnected_device == &rejected &&
|
||||||
|
g_slots[0].device == nullptr,
|
||||||
|
"locked policy must disconnect every incoming controller");
|
||||||
|
|
||||||
|
bluepad32_input_backend_open_pairing_window();
|
||||||
|
require(!g_pairing_window_open,
|
||||||
|
"Core0 request must wait for Core1 consumption");
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(g_pairing_window_open &&
|
||||||
|
g_pairing_window_deadline_ms == 60000 &&
|
||||||
|
g_connection_policy_state == ConnectionPolicyState::Open &&
|
||||||
|
classic_scanning_enabled && scanning_enabled &&
|
||||||
|
incoming_connections,
|
||||||
|
"BOOTSEL window must run Bluepad32's normal pairing scan");
|
||||||
|
require(platform_on_device_discovered(address, "controller", 0, 0) ==
|
||||||
|
UNI_ERROR_SUCCESS,
|
||||||
|
"open pairing window must accept a discovered controller");
|
||||||
|
|
||||||
|
uni_hid_device_t paired = device(0);
|
||||||
|
platform_on_device_connected(&paired);
|
||||||
|
require(device_disconnect_calls == 1 &&
|
||||||
|
g_slots[0].device == &paired,
|
||||||
|
"open pairing window must retain a connected controller");
|
||||||
|
platform_on_device_disconnected(&paired);
|
||||||
|
|
||||||
|
tick_backend_timer(20);
|
||||||
|
require(!observed_status_led_on,
|
||||||
|
"pairing double blink must finish its first pulse");
|
||||||
|
tick_backend_timer(20);
|
||||||
|
require(observed_status_led_on,
|
||||||
|
"pairing double blink must start its second pulse");
|
||||||
|
tick_backend_timer(20);
|
||||||
|
require(!observed_status_led_on,
|
||||||
|
"pairing double blink must finish its second pulse");
|
||||||
|
|
||||||
|
now_ms = 30000;
|
||||||
|
bluepad32_input_backend_open_pairing_window();
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(g_pairing_window_deadline_ms == 90000,
|
||||||
|
"pairing request must extend deadline from current Core1 time");
|
||||||
|
|
||||||
|
uni_hid_device_t devices[kSlotCount] = {
|
||||||
|
device(0), device(1), device(2), device(3)};
|
||||||
|
for (int slot = 0; slot < kSlotCount; ++slot) {
|
||||||
|
require(platform_on_device_ready(&devices[slot]) == UNI_ERROR_SUCCESS,
|
||||||
|
"policy test devices must fill all slots");
|
||||||
|
}
|
||||||
|
require(g_connection_policy_state == ConnectionPolicyState::Paused &&
|
||||||
|
g_pairing_window_open && !scanning_enabled &&
|
||||||
|
!classic_scanning_enabled && !incoming_connections,
|
||||||
|
"full slots must pause pairing without closing the deadline");
|
||||||
|
|
||||||
|
now_ms = 90000;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(!g_pairing_window_open &&
|
||||||
|
g_connection_policy_state == ConnectionPolicyState::Paused,
|
||||||
|
"deadline must expire while slots remain full");
|
||||||
|
platform_on_device_disconnected(&devices[3]);
|
||||||
|
require(g_connection_policy_state == ConnectionPolicyState::Locked &&
|
||||||
|
!classic_scanning_enabled && !scanning_enabled &&
|
||||||
|
!incoming_connections,
|
||||||
|
"a freed slot after expiry must remain closed");
|
||||||
|
require(platform_on_device_discovered(address, "controller", 0, 0) ==
|
||||||
|
UNI_ERROR_IGNORE_DEVICE,
|
||||||
|
"expired pairing policy must reject discovery");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void test_flash_core_start_contract() {
|
||||||
|
bluepad32_input_backend_init();
|
||||||
|
flash_core_init_result = false;
|
||||||
|
bluepad32_input_backend_start();
|
||||||
|
require(flash_core_init_calls == 1 && core1_launch_calls == 0 &&
|
||||||
|
g_connection_policy_state ==
|
||||||
|
ConnectionPolicyState::FailedClosed,
|
||||||
|
"Core0 flash-safe init failure must prevent Core1 launch");
|
||||||
|
|
||||||
|
flash_core_init_result = true;
|
||||||
|
bluepad32_input_backend_start();
|
||||||
|
require(flash_core_init_calls == 2 && core1_launch_calls == 1,
|
||||||
|
"Core0 must register as a flash-safe victim before Core1 launch");
|
||||||
|
bluepad32_input_backend_start();
|
||||||
|
require(flash_core_init_calls == 2 && core1_launch_calls == 1,
|
||||||
|
"backend start must remain idempotent");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_flash_core_init_fatal() {
|
||||||
|
bluepad32_input_backend_init();
|
||||||
|
flash_core_init_result = false;
|
||||||
|
bool stopped = false;
|
||||||
|
try {
|
||||||
|
core1_main();
|
||||||
|
} catch (const CoreStopped&) {
|
||||||
|
stopped = true;
|
||||||
|
}
|
||||||
|
require(stopped && flash_core_init_calls == 1 &&
|
||||||
|
cyw43_init_calls == 0 && uni_init_calls == 0 &&
|
||||||
|
g_connection_policy_state ==
|
||||||
|
ConnectionPolicyState::FailedClosed,
|
||||||
|
"flash-safe Core1 init failure must halt before CYW43 init");
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
|
|
@ -549,6 +686,12 @@ int main(int argc, char** argv) {
|
||||||
test_rejections();
|
test_rejections();
|
||||||
} else if (scenario == "lifecycle") {
|
} else if (scenario == "lifecycle") {
|
||||||
test_independent_lifecycle();
|
test_independent_lifecycle();
|
||||||
|
} else if (scenario == "pairing-policy") {
|
||||||
|
test_pairing_window_policy();
|
||||||
|
} else if (scenario == "flash-core-start") {
|
||||||
|
test_flash_core_start_contract();
|
||||||
|
} else if (scenario == "flash-core-failure") {
|
||||||
|
test_flash_core_init_fatal();
|
||||||
} else {
|
} else {
|
||||||
require(false, "unknown scenario");
|
require(false, "unknown scenario");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,4 +19,5 @@ inline void btstack_run_loop_set_timer(btstack_timer_source_t* timer,
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void btstack_run_loop_add_timer(btstack_timer_source_t*) {}
|
inline void btstack_run_loop_add_timer(btstack_timer_source_t*) {}
|
||||||
|
uint32_t btstack_run_loop_get_time_ms();
|
||||||
inline void btstack_run_loop_execute() {}
|
inline void btstack_run_loop_execute() {}
|
||||||
|
|
|
||||||
3
tests/bluepad32_native_stubs/pico/flash.h
Normal file
3
tests/bluepad32_native_stubs/pico/flash.h
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
bool flash_safe_execute_core_init();
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
inline void tight_loop_contents() {}
|
void tight_loop_contents();
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,30 @@
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
typedef uint8_t bd_addr_t[6];
|
typedef uint8_t bd_addr_t[6];
|
||||||
|
typedef uint8_t link_key_t[16];
|
||||||
|
typedef uint8_t sm_key_t[16];
|
||||||
|
typedef int link_key_type_t;
|
||||||
|
|
||||||
|
enum bd_addr_type_t {
|
||||||
|
BD_ADDR_TYPE_LE_PUBLIC = 0,
|
||||||
|
BD_ADDR_TYPE_LE_RANDOM = 1,
|
||||||
|
BD_ADDR_TYPE_LE_PUBLIC_IDENTITY = 2,
|
||||||
|
BD_ADDR_TYPE_LE_RANDOM_IDENTITY = 3,
|
||||||
|
BD_ADDR_TYPE_UNKNOWN = 0xfe,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum hci_link_type_t {
|
||||||
|
HCI_LINK_TYPE_SCO = 0,
|
||||||
|
HCI_LINK_TYPE_ACL = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct btstack_link_key_iterator_t {
|
||||||
|
int index;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum {
|
||||||
|
ERROR_CODE_SUCCESS = 0,
|
||||||
|
};
|
||||||
typedef int uni_property_idx_t;
|
typedef int uni_property_idx_t;
|
||||||
typedef int uni_platform_oob_event_t;
|
typedef int uni_platform_oob_event_t;
|
||||||
struct uni_property_t {};
|
struct uni_property_t {};
|
||||||
|
|
@ -63,7 +87,20 @@ struct uni_report_parser_t {
|
||||||
uni_play_dual_rumble_t play_dual_rumble;
|
uni_play_dual_rumble_t play_dual_rumble;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum uni_bt_conn_protocol_t {
|
||||||
|
UNI_BT_CONN_PROTOCOL_NONE,
|
||||||
|
UNI_BT_CONN_PROTOCOL_BR_EDR,
|
||||||
|
UNI_BT_CONN_PROTOCOL_BLE,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
struct uni_bt_conn_t {
|
||||||
|
bd_addr_t btaddr;
|
||||||
|
uni_bt_conn_protocol_t protocol;
|
||||||
|
};
|
||||||
|
|
||||||
struct uni_hid_device_t {
|
struct uni_hid_device_t {
|
||||||
|
uni_bt_conn_t conn;
|
||||||
int idx;
|
int idx;
|
||||||
bool gamepad;
|
bool gamepad;
|
||||||
uni_report_parser_t report_parser;
|
uni_report_parser_t report_parser;
|
||||||
|
|
@ -91,8 +128,14 @@ struct uni_platform {
|
||||||
|
|
||||||
bool uni_hid_device_is_gamepad(const uni_hid_device_t* device);
|
bool uni_hid_device_is_gamepad(const uni_hid_device_t* device);
|
||||||
int uni_hid_device_get_idx_for_instance(const uni_hid_device_t* device);
|
int uni_hid_device_get_idx_for_instance(const uni_hid_device_t* device);
|
||||||
|
void uni_hid_device_disconnect(uni_hid_device_t* device);
|
||||||
void uni_bt_allow_incoming_connections(bool enabled);
|
void uni_bt_allow_incoming_connections(bool enabled);
|
||||||
void uni_bt_start_scanning_and_autoconnect_unsafe();
|
void uni_bt_start_scanning_and_autoconnect_unsafe();
|
||||||
void uni_bt_stop_scanning_unsafe();
|
void uni_bt_stop_scanning_unsafe();
|
||||||
|
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_platform_set_custom(uni_platform* platform);
|
void uni_platform_set_custom(uni_platform* platform);
|
||||||
int uni_init(int argc, const char** argv);
|
int uni_init(int argc, const char** argv);
|
||||||
|
|
||||||
|
|
|
||||||
27
tests/bootsel_native_stubs/hardware/gpio.h
Normal file
27
tests/bootsel_native_stubs/hardware/gpio.h
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
using io_rw_32 = volatile uint32_t;
|
||||||
|
|
||||||
|
enum gpio_override {
|
||||||
|
GPIO_OVERRIDE_NORMAL = 0,
|
||||||
|
GPIO_OVERRIDE_LOW = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
void bootsel_test_masked_write(io_rw_32* address, uint32_t values,
|
||||||
|
uint32_t mask);
|
||||||
|
|
||||||
|
inline void hw_write_masked(io_rw_32* address, uint32_t values,
|
||||||
|
uint32_t mask) {
|
||||||
|
*address = (*address & ~mask) | (values & mask);
|
||||||
|
bootsel_test_masked_write(address, values, mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if PICO_RP2350
|
||||||
|
#define IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_LSB 14u
|
||||||
|
#define IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_BITS 0x0000c000u
|
||||||
|
#else
|
||||||
|
#define IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_LSB 12u
|
||||||
|
#define IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_BITS 0x00003000u
|
||||||
|
#endif
|
||||||
3
tests/bootsel_native_stubs/hardware/regs/sio.h
Normal file
3
tests/bootsel_native_stubs/hardware/regs/sio.h
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define SIO_GPIO_HI_IN_QSPI_CSN_BITS 0x08000000u
|
||||||
14
tests/bootsel_native_stubs/hardware/structs/ioqspi.h
Normal file
14
tests/bootsel_native_stubs/hardware/structs/ioqspi.h
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "hardware/gpio.h"
|
||||||
|
|
||||||
|
struct ioqspi_status_ctrl_hw_t {
|
||||||
|
io_rw_32 status;
|
||||||
|
io_rw_32 ctrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ioqspi_hw_t {
|
||||||
|
ioqspi_status_ctrl_hw_t io[6];
|
||||||
|
};
|
||||||
|
|
||||||
|
extern ioqspi_hw_t* ioqspi_hw;
|
||||||
10
tests/bootsel_native_stubs/hardware/structs/sio.h
Normal file
10
tests/bootsel_native_stubs/hardware/structs/sio.h
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
struct sio_hw_t {
|
||||||
|
volatile uint32_t gpio_in;
|
||||||
|
volatile uint32_t gpio_hi_in;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern sio_hw_t* sio_hw;
|
||||||
10
tests/bootsel_native_stubs/pico/flash.h
Normal file
10
tests/bootsel_native_stubs/pico/flash.h
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#define __no_inline_not_in_flash_func(function_name) function_name
|
||||||
|
|
||||||
|
constexpr int PICO_OK = 0;
|
||||||
|
|
||||||
|
int flash_safe_execute(void (*function)(void*), void* parameter,
|
||||||
|
uint32_t enter_exit_timeout_ms);
|
||||||
8
tests/bootsel_native_stubs/pico/time.h
Normal file
8
tests/bootsel_native_stubs/pico/time.h
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
using absolute_time_t = uint64_t;
|
||||||
|
|
||||||
|
absolute_time_t get_absolute_time();
|
||||||
|
uint64_t to_ms_since_boot(absolute_time_t time);
|
||||||
224
tests/bootsel_pairing_button_test.cpp
Normal file
224
tests/bootsel_pairing_button_test.cpp
Normal file
|
|
@ -0,0 +1,224 @@
|
||||||
|
#include "bootsel_pairing_button.h"
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iostream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "hardware/gpio.h"
|
||||||
|
#include "hardware/regs/sio.h"
|
||||||
|
#include "hardware/structs/ioqspi.h"
|
||||||
|
#include "hardware/structs/sio.h"
|
||||||
|
#include "pico/flash.h"
|
||||||
|
#include "pico/time.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
#if PICO_RP2350
|
||||||
|
constexpr uint32_t kBootselInputMask = SIO_GPIO_HI_IN_QSPI_CSN_BITS;
|
||||||
|
#else
|
||||||
|
constexpr uint32_t kBootselInputMask = 1u << 1u;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct FlashResponse {
|
||||||
|
int result;
|
||||||
|
bool pressed;
|
||||||
|
};
|
||||||
|
|
||||||
|
ioqspi_hw_t qspi_registers{};
|
||||||
|
sio_hw_t sio_registers{};
|
||||||
|
uint64_t now_ms = 0;
|
||||||
|
std::vector<FlashResponse> flash_responses;
|
||||||
|
std::size_t next_flash_response = 0;
|
||||||
|
std::vector<uint32_t> qspi_override_writes;
|
||||||
|
int flash_safe_calls = 0;
|
||||||
|
bool inside_flash_safe_callback = false;
|
||||||
|
|
||||||
|
void require(bool condition, const char* message) {
|
||||||
|
if (!condition) {
|
||||||
|
std::cerr << message << '\n';
|
||||||
|
std::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int apply_pressed(BootselPairingButtonHoldFsm& fsm, int count) {
|
||||||
|
int events = 0;
|
||||||
|
for (int sample = 0; sample < count; ++sample) {
|
||||||
|
if (fsm.update(BootselPairingButtonSample::kPressed)) {
|
||||||
|
++events;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_short_press() {
|
||||||
|
BootselPairingButtonHoldFsm fsm;
|
||||||
|
require(apply_pressed(fsm, 19) == 0,
|
||||||
|
"a 19-sample press must not complete the hold");
|
||||||
|
require(!fsm.update(BootselPairingButtonSample::kReleased),
|
||||||
|
"a short-press release must not report a hold");
|
||||||
|
require(apply_pressed(fsm, 19) == 0,
|
||||||
|
"a release must discard the previous short press");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_exact_and_long_hold_once() {
|
||||||
|
BootselPairingButtonHoldFsm fsm;
|
||||||
|
require(apply_pressed(fsm, 19) == 0,
|
||||||
|
"the hold must not fire before sample 20");
|
||||||
|
require(fsm.update(BootselPairingButtonSample::kPressed),
|
||||||
|
"the hold must fire on exactly sample 20");
|
||||||
|
require(apply_pressed(fsm, 100) == 0,
|
||||||
|
"a continuously held button must not repeat");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_release_and_rearm() {
|
||||||
|
BootselPairingButtonHoldFsm fsm;
|
||||||
|
require(apply_pressed(fsm, 20) == 1,
|
||||||
|
"the initial hold must fire once");
|
||||||
|
require(!fsm.update(BootselPairingButtonSample::kReleased),
|
||||||
|
"release must rearm without reporting an event");
|
||||||
|
require(apply_pressed(fsm, 20) == 1,
|
||||||
|
"a valid release must permit one later hold");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_unread_samples_do_not_transition() {
|
||||||
|
BootselPairingButtonHoldFsm fsm;
|
||||||
|
require(apply_pressed(fsm, 10) == 0,
|
||||||
|
"the first half of a hold must not fire");
|
||||||
|
for (int sample = 0; sample < 8; ++sample) {
|
||||||
|
require(!fsm.update(BootselPairingButtonSample::kUnread),
|
||||||
|
"unread press samples must not report or reset a hold");
|
||||||
|
}
|
||||||
|
require(apply_pressed(fsm, 9) == 0,
|
||||||
|
"valid pressed samples must resume after unread samples");
|
||||||
|
require(fsm.update(BootselPairingButtonSample::kPressed),
|
||||||
|
"20 valid pressed samples must fire despite unread samples");
|
||||||
|
|
||||||
|
require(!fsm.update(BootselPairingButtonSample::kUnread),
|
||||||
|
"an unread release must not rearm a completed hold");
|
||||||
|
require(apply_pressed(fsm, 20) == 0,
|
||||||
|
"the held state must persist until a valid release");
|
||||||
|
require(!fsm.update(BootselPairingButtonSample::kReleased),
|
||||||
|
"a valid release must only rearm");
|
||||||
|
require(apply_pressed(fsm, 20) == 1,
|
||||||
|
"the FSM must fire after the eventual valid release");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool run_sample(uint64_t sample_time_ms, int result, bool pressed) {
|
||||||
|
flash_responses.push_back({result, pressed});
|
||||||
|
now_ms = sample_time_ms;
|
||||||
|
const std::size_t expected_consumed = flash_responses.size();
|
||||||
|
const bool event = bootsel_pairing_button_task();
|
||||||
|
require(next_flash_response == expected_consumed,
|
||||||
|
"a due poll must invoke flash_safe_execute exactly once");
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_sampler_cadence_and_callback_failure() {
|
||||||
|
now_ms = 0;
|
||||||
|
require(!bootsel_pairing_button_task(),
|
||||||
|
"the sampler must wait for its first 100 ms cadence");
|
||||||
|
now_ms = 99;
|
||||||
|
require(!bootsel_pairing_button_task(),
|
||||||
|
"the sampler must not poll before 100 ms");
|
||||||
|
require(flash_safe_calls == 0,
|
||||||
|
"sub-cadence task calls must not enter flash-safe execution");
|
||||||
|
|
||||||
|
require(!run_sample(100, PICO_OK, true),
|
||||||
|
"the first valid pressed sample must only start the hold");
|
||||||
|
require(flash_safe_calls == 1 && qspi_override_writes.size() == 2,
|
||||||
|
"a successful sample must float and restore QSPI CSn once");
|
||||||
|
const uint32_t disabled =
|
||||||
|
GPIO_OVERRIDE_LOW << IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_LSB;
|
||||||
|
require(qspi_override_writes[0] == disabled,
|
||||||
|
"the callback must float QSPI CSn before reading BOOTSEL");
|
||||||
|
require(qspi_override_writes[1] == 0,
|
||||||
|
"the callback must restore normal QSPI CSn control");
|
||||||
|
|
||||||
|
now_ms = 199;
|
||||||
|
require(!bootsel_pairing_button_task(),
|
||||||
|
"the sampler must remain gated between 10 Hz polls");
|
||||||
|
require(flash_safe_calls == 1,
|
||||||
|
"an early task call must not sample BOOTSEL");
|
||||||
|
|
||||||
|
const std::size_t writes_before_failure = qspi_override_writes.size();
|
||||||
|
require(!run_sample(200, -1, true),
|
||||||
|
"flash-safe failure must be treated as unread");
|
||||||
|
require(qspi_override_writes.size() == writes_before_failure,
|
||||||
|
"a failed flash-safe entry must not invoke the callback");
|
||||||
|
|
||||||
|
for (uint64_t time = 300; time < 2100; time += 100) {
|
||||||
|
require(!run_sample(time, PICO_OK, true),
|
||||||
|
"the sampler must wait for 20 valid pressed samples");
|
||||||
|
}
|
||||||
|
require(run_sample(2100, PICO_OK, true),
|
||||||
|
"a failed sample must not reset the valid pressed count");
|
||||||
|
require(!run_sample(2200, PICO_OK, true),
|
||||||
|
"a held button must not repeat after firing");
|
||||||
|
|
||||||
|
require(!run_sample(2300, -1, false),
|
||||||
|
"a failed release sample must remain unread");
|
||||||
|
require(!run_sample(2400, PICO_OK, true),
|
||||||
|
"an unread release must not rearm the sampler FSM");
|
||||||
|
require(!run_sample(2500, PICO_OK, false),
|
||||||
|
"a valid release must rearm without firing");
|
||||||
|
|
||||||
|
for (uint64_t time = 2600; time < 4500; time += 100) {
|
||||||
|
require(!run_sample(time, PICO_OK, true),
|
||||||
|
"the rearmed sampler must count a fresh hold");
|
||||||
|
}
|
||||||
|
require(run_sample(4500, PICO_OK, true),
|
||||||
|
"a valid release must permit a second completed hold");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ioqspi_hw_t* ioqspi_hw = &qspi_registers;
|
||||||
|
sio_hw_t* sio_hw = &sio_registers;
|
||||||
|
|
||||||
|
absolute_time_t get_absolute_time() {
|
||||||
|
return now_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t to_ms_since_boot(absolute_time_t time) {
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|
||||||
|
void bootsel_test_masked_write(io_rw_32* address, uint32_t, uint32_t mask) {
|
||||||
|
require(inside_flash_safe_callback,
|
||||||
|
"QSPI override writes must occur inside flash_safe_execute");
|
||||||
|
require(address == &ioqspi_hw->io[1].ctrl,
|
||||||
|
"the callback must only override QSPI CSn");
|
||||||
|
qspi_override_writes.push_back(*address & mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_safe_execute(void (*function)(void*), void* parameter,
|
||||||
|
uint32_t enter_exit_timeout_ms) {
|
||||||
|
require(enter_exit_timeout_ms == 100,
|
||||||
|
"BOOTSEL sampling must use the 100 ms flash-safe timeout");
|
||||||
|
require(next_flash_response < flash_responses.size(),
|
||||||
|
"flash-safe execution requires a queued test response");
|
||||||
|
++flash_safe_calls;
|
||||||
|
const FlashResponse response = flash_responses[next_flash_response++];
|
||||||
|
if (response.result != PICO_OK) {
|
||||||
|
return response.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
sio_hw->gpio_hi_in = response.pressed ? 0 : kBootselInputMask;
|
||||||
|
inside_flash_safe_callback = true;
|
||||||
|
function(parameter);
|
||||||
|
inside_flash_safe_callback = false;
|
||||||
|
require((ioqspi_hw->io[1].ctrl &
|
||||||
|
IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_BITS) == 0,
|
||||||
|
"the callback must restore QSPI CSn before returning");
|
||||||
|
return PICO_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
test_short_press();
|
||||||
|
test_exact_and_long_hold_once();
|
||||||
|
test_release_and_rearm();
|
||||||
|
test_unread_samples_do_not_transition();
|
||||||
|
test_sampler_cadence_and_callback_failure();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -35,5 +35,8 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
||||||
"ready-reverse",
|
"ready-reverse",
|
||||||
"rejections",
|
"rejections",
|
||||||
"lifecycle",
|
"lifecycle",
|
||||||
|
"pairing-policy",
|
||||||
|
"flash-core-start",
|
||||||
|
"flash-core-failure",
|
||||||
):
|
):
|
||||||
subprocess.run([str(executable), scenario], check=True, cwd=root)
|
subprocess.run([str(executable), scenario], check=True, cwd=root)
|
||||||
|
|
|
||||||
34
tests/test_bootsel_pairing_button_native.py
Normal file
34
tests/test_bootsel_pairing_button_native.py
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def test_bootsel_pairing_button_native(tmp_path: Path) -> None:
|
||||||
|
root = Path(__file__).resolve().parents[1]
|
||||||
|
compiler = shutil.which("c++") or shutil.which("g++")
|
||||||
|
assert compiler is not None, "a host C++ compiler is required"
|
||||||
|
|
||||||
|
for platform, rp2350 in (("rp2350", 1), ("rp2040", 0)):
|
||||||
|
executable = tmp_path / f"bootsel_pairing_button_test_{platform}"
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
compiler,
|
||||||
|
"-std=c++17",
|
||||||
|
"-Wall",
|
||||||
|
"-Wextra",
|
||||||
|
"-Werror",
|
||||||
|
"-pedantic",
|
||||||
|
f"-DPICO_RP2350={rp2350}",
|
||||||
|
f"-I{root / 'tests' / 'bootsel_native_stubs'}",
|
||||||
|
f"-I{root}",
|
||||||
|
str(root / "bootsel_pairing_button.cpp"),
|
||||||
|
str(root / "tests" / "bootsel_pairing_button_test.cpp"),
|
||||||
|
"-o",
|
||||||
|
str(executable),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
cwd=root,
|
||||||
|
)
|
||||||
|
subprocess.run([str(executable)], check=True, cwd=root)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue