Verify and publish dual-controller AIO firmware
This commit is contained in:
parent
edf6ecaae1
commit
18c27b06ae
5 changed files with 297 additions and 48 deletions
61
README.md
61
README.md
|
|
@ -1,12 +1,12 @@
|
|||
# Switch Pico Controller Bridge
|
||||
|
||||
Raspberry Pi Pico firmware that emulates a Switch Pro controller over USB. Input can come from the SDL3-to-UART computer bridge or, on Pico 2 W, directly from a Bluetooth controller through Bluepad32.
|
||||
Raspberry Pi Pico firmware that emulates one or more Switch Pro controllers over USB. Input can come from the SDL3-to-UART computer bridge or, on Pico 2 W, directly from Bluetooth controllers through Bluepad32.
|
||||
|
||||
## What you get
|
||||
- **Firmware** (`switch-pico.cpp` + `switch_pro_driver.*`): acts as a wired Switch Pro, accepting either UART bridge reports or the optional Pico 2 W Bluepad32 backend.
|
||||
- **Firmware** (`switch-pico.cpp` + `switch_pro_driver.*`): acts as a Switch Pro controller (one on standard Pico, two on Pico 2 W AIO), accepting either UART bridge reports or the optional Pico 2 W Bluepad32 backend.
|
||||
- **Python bridge** (`switch_pico_bridge.controller_uart_bridge` / CLI `controller-uart-bridge`): reads SDL3 controllers on the host, sends reports over UART, and applies rumble locally. Hot‑plug friendly and cross‑platform (macOS/Windows/Linux).
|
||||
- **Colour override** (`controller_color_config.h`): compile‑time RGB overrides for body/buttons/grips as seen by the Switch.
|
||||
- **Pico 2 W AIO firmware** (`firmware/switch-pico-aio.uf2`): hosts one Bluetooth controller and sends its controls, calibrated motion, and rumble through the same Switch Pro USB device without a computer.
|
||||
- **Pico 2 W AIO firmware** (`firmware/switch-pico-aio.uf2`): hosts two concurrent Bluetooth controllers and sends their controls, calibrated motion, and rumble through two separate Switch Pro USB interfaces without a computer.
|
||||
|
||||
## Quick start
|
||||
1. Flash the Pico with `firmware/switch-pico.uf2` (or build your own) using BOOTSEL drag-and-drop (see “Manual UF2 flashing” below).
|
||||
|
|
@ -17,7 +17,11 @@ Raspberry Pi Pico firmware that emulates a Switch Pro controller over USB. Input
|
|||
|
||||
## Pico 2 W all-in-one Bluetooth option
|
||||
|
||||
The AIO build runs TinyUSB and Switch report generation on Core 0 while Bluepad32, BTstack, and the CYW43439 radio run on Core 1. A fixed state snapshot and bounded rumble queue are the only cross-core interfaces.
|
||||
### Architecture
|
||||
|
||||
The AIO build accepts two concurrent Bluetooth controllers on a single Pico 2 W. The device runs TinyUSB and Switch report generation on Core 0, while Bluepad32, BTstack, and the CYW43439 radio run on Core 1. Core 0 maintains two USB Pro HID interfaces (slots 0 and 1), and each Bluetooth connection is isolated in the Bluepad32 slot assigned when that connection becomes active. A fixed state snapshot and per-slot bounded rumble queue are the only cross-core synchronization points.
|
||||
|
||||
Both USB interfaces are always present to the Switch. The Switch enumerates them as two separate Pro Controllers on the same physical device. Inputs and rumble are independent per controller.
|
||||
|
||||
### Build and flash
|
||||
|
||||
|
|
@ -42,21 +46,40 @@ 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.
|
||||
|
||||
### Pair a controller
|
||||
### Pairing two controllers
|
||||
|
||||
1. Flash and connect the Pico 2 W to the Switch.
|
||||
2. Enable `System Settings → Controllers and Sensors → Pro Controller Wired Communication`.
|
||||
3. Put one controller into Bluetooth pairing mode:
|
||||
3. Put the first controller into Bluetooth pairing mode:
|
||||
- DualSense: hold Create + PS.
|
||||
- DualShock 4: hold Share + PS.
|
||||
- Switch Pro: press its sync button.
|
||||
- Xbox Bluetooth controller: hold its pair button.
|
||||
- 8BitDo: use a Bluetooth mode supported by Bluepad32; use Switch/S mode when motion is required.
|
||||
4. Wait for the controller to connect. Pairing keys persist across Pico reboots.
|
||||
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. 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.
|
||||
|
||||
The Pico 2 W onboard LED reports Bluetooth state: a slow 0.5-second blink means scanning, a fast 0.1-second blink means a controller connected but is not ready, and solid means the controller is ready. A solid LED immediately after boot that never starts blinking indicates Bluepad32 initialization did not complete.
|
||||
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.
|
||||
|
||||
Only one wireless controller owns the emulated Pro Controller. Turn off or disconnect it before pairing another; scanning resumes automatically after disconnect. A disconnect immediately publishes neutral buttons, sticks, and motion.
|
||||
### LED meanings and device 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.
|
||||
- **Fast blink (0.1 s period)**: at least one controller is connected but not yet ready (handshake in progress).
|
||||
- **Solid**: both slots are filled and both controllers are ready for input.
|
||||
- **Solid immediately after boot that never starts blinking**: 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
|
||||
|
||||
Controllers can disconnect and reconnect independently:
|
||||
- **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 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.
|
||||
- **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.
|
||||
- **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
|
||||
|
||||
|
|
@ -69,6 +92,26 @@ Only one wireless controller owns the emulated Pro Controller. Turn off or disco
|
|||
|
||||
Motion is normalized to 1024 units per degree/second and 8192 units per g in SDL3 axes, then converted to Nintendo axes and raw counts. The latest normalized sample is duplicated across the report's three nominal 5 ms slots; it remains pending until a regular `0x30` USB report successfully consumes it.
|
||||
|
||||
### Rumble per controller
|
||||
|
||||
Rumble effects are per-slot and independent. The Switch sends rumble commands to a specific USB interface, and the Pico routes each command to the Bluetooth controller in the matching slot. Each slot has a critical-section-protected latest-value mailbox tagged with its connection generation; a newer pending command replaces the older one, and disconnect invalidates commands from the prior controller.
|
||||
|
||||
### 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.
|
||||
|
||||
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).
|
||||
2. **Verify Bluetooth pairing**: Pair two controllers via Bluepad32. Confirm the LED transitions from scanning → fast blink → solid.
|
||||
3. **Verify input on one controller**: Move sticks, press buttons, and check that the controller paired first during initial setup appears in slot 0.
|
||||
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.
|
||||
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.
|
||||
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).
|
||||
|
||||
On the tested Linux host, both HID interfaces enumerated (`lsusb -t` showed interface 0 and 1), but `hid-nintendo` probes timed out (`-110`) while requesting controller information from this composite device and removed their transient hidraw nodes. This is an observed, undiagnosed composite interoperability limitation; its root cause has not been established. The timeout was not observed on the Switch, so successful `hid-nintendo` binding is not the release criterion for dual-interface AIO firmware.
|
||||
|
||||
Bluepad32 is Apache-2.0. BTstack use on Pico W/Pico 2 W is covered by Raspberry Pi's BTstack license.
|
||||
|
||||
## Planned features
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ struct RumbleEnvelope {
|
|||
|
||||
struct BackendSlot {
|
||||
SwitchInputState state;
|
||||
// Non-null with active=false is a connected device still becoming ready.
|
||||
uni_hid_device_t* device;
|
||||
uint32_t state_generation;
|
||||
uint32_t connection_generation;
|
||||
|
|
@ -83,14 +84,22 @@ int slot_for_device(const uni_hid_device_t* device) {
|
|||
return slot >= 0 && slot < kSlotCount ? slot : -1;
|
||||
}
|
||||
|
||||
bool all_slots_ready() {
|
||||
ConnectionStatus compute_connection_status() {
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
bool ready = true;
|
||||
bool all_ready = true;
|
||||
bool any_connecting = false;
|
||||
for (const BackendSlot& slot : g_slots) {
|
||||
ready = ready && slot.active && slot.device != nullptr;
|
||||
const bool has_device = slot.device != nullptr;
|
||||
all_ready = all_ready && slot.active && has_device;
|
||||
any_connecting = any_connecting || (!slot.active && has_device);
|
||||
}
|
||||
critical_section_exit(&g_state_lock);
|
||||
return ready;
|
||||
|
||||
if (all_ready) {
|
||||
return ConnectionStatus::Ready;
|
||||
}
|
||||
return any_connecting ? ConnectionStatus::Connecting
|
||||
: ConnectionStatus::Scanning;
|
||||
}
|
||||
|
||||
void publish_device_state(uint8_t slot, uni_hid_device_t* device,
|
||||
|
|
@ -115,6 +124,8 @@ void publish_all_neutral() {
|
|||
++slot.connection_generation;
|
||||
}
|
||||
critical_section_exit(&g_state_lock);
|
||||
g_connection_status = ConnectionStatus::Initializing;
|
||||
g_status_led_tick = 0;
|
||||
}
|
||||
|
||||
constexpr int32_t clamp_axis(int32_t value) {
|
||||
|
|
@ -287,6 +298,16 @@ void resume_connections() {
|
|||
uni_bt_allow_incoming_connections(true);
|
||||
uni_bt_start_scanning_and_autoconnect_unsafe();
|
||||
}
|
||||
void recompute_connection_status() {
|
||||
g_connection_status = compute_connection_status();
|
||||
g_status_led_tick = 0;
|
||||
if (g_connection_status == ConnectionStatus::Ready) {
|
||||
uni_bt_stop_scanning_unsafe();
|
||||
uni_bt_allow_incoming_connections(false);
|
||||
} else {
|
||||
resume_connections();
|
||||
}
|
||||
}
|
||||
|
||||
void platform_init(int argc, const char** argv) {
|
||||
(void)argc;
|
||||
|
|
@ -297,9 +318,7 @@ void platform_on_init_complete() {
|
|||
btstack_run_loop_set_timer_handler(&g_rumble_timer, process_rumble_timer);
|
||||
btstack_run_loop_set_timer(&g_rumble_timer, kRumblePollIntervalMs);
|
||||
btstack_run_loop_add_timer(&g_rumble_timer);
|
||||
g_connection_status = ConnectionStatus::Scanning;
|
||||
g_status_led_tick = 0;
|
||||
resume_connections();
|
||||
recompute_connection_status();
|
||||
}
|
||||
|
||||
uni_error_t platform_on_device_discovered(bd_addr_t addr, const char* name, uint16_t cod, uint8_t rssi) {
|
||||
|
|
@ -307,14 +326,31 @@ uni_error_t platform_on_device_discovered(bd_addr_t addr, const char* name, uint
|
|||
(void)name;
|
||||
(void)cod;
|
||||
(void)rssi;
|
||||
return all_slots_ready() ? UNI_ERROR_IGNORE_DEVICE : UNI_ERROR_SUCCESS;
|
||||
return compute_connection_status() == ConnectionStatus::Ready
|
||||
? UNI_ERROR_IGNORE_DEVICE
|
||||
: UNI_ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
void platform_on_device_connected(uni_hid_device_t* device) {
|
||||
(void)device;
|
||||
if (!all_slots_ready()) {
|
||||
g_connection_status = ConnectionStatus::Connecting;
|
||||
g_status_led_tick = 0;
|
||||
const int slot_index = slot_for_device(device);
|
||||
if (slot_index < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool tracked_connection = false;
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
BackendSlot& slot = g_slots[slot_index];
|
||||
if (!slot.active && slot.device == nullptr) {
|
||||
slot.device = device;
|
||||
slot.rumble_pending = false;
|
||||
tracked_connection = true;
|
||||
} else {
|
||||
tracked_connection = slot.device == device;
|
||||
}
|
||||
critical_section_exit(&g_state_lock);
|
||||
|
||||
if (tracked_connection) {
|
||||
recompute_connection_status();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -324,24 +360,24 @@ void platform_on_device_disconnected(uni_hid_device_t* device) {
|
|||
return;
|
||||
}
|
||||
|
||||
bool disconnected_active_slot = false;
|
||||
bool disconnected_tracked_device = false;
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
BackendSlot& slot = g_slots[slot_index];
|
||||
if (slot.active && slot.device == device) {
|
||||
slot.state = make_neutral_state();
|
||||
if (slot.device == device) {
|
||||
if (slot.active) {
|
||||
slot.state = make_neutral_state();
|
||||
++slot.state_generation;
|
||||
}
|
||||
slot.device = nullptr;
|
||||
slot.active = false;
|
||||
slot.rumble_pending = false;
|
||||
++slot.state_generation;
|
||||
++slot.connection_generation;
|
||||
disconnected_active_slot = true;
|
||||
disconnected_tracked_device = true;
|
||||
}
|
||||
critical_section_exit(&g_state_lock);
|
||||
|
||||
if (disconnected_active_slot) {
|
||||
g_connection_status = ConnectionStatus::Scanning;
|
||||
g_status_led_tick = 0;
|
||||
resume_connections();
|
||||
if (disconnected_tracked_device) {
|
||||
recompute_connection_status();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -358,13 +394,15 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) {
|
|||
bool occupied_mismatch = false;
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
BackendSlot& slot = g_slots[slot_index];
|
||||
occupied_mismatch = slot.active && slot.device != device;
|
||||
if (!occupied_mismatch && !slot.active) {
|
||||
slot.state = make_neutral_state();
|
||||
occupied_mismatch = slot.device != nullptr && slot.device != device;
|
||||
if (!occupied_mismatch) {
|
||||
slot.device = device;
|
||||
slot.active = true;
|
||||
slot.rumble_pending = false;
|
||||
++slot.state_generation;
|
||||
if (!slot.active) {
|
||||
slot.state = make_neutral_state();
|
||||
slot.active = true;
|
||||
slot.rumble_pending = false;
|
||||
++slot.state_generation;
|
||||
}
|
||||
}
|
||||
critical_section_exit(&g_state_lock);
|
||||
|
||||
|
|
@ -372,15 +410,7 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) {
|
|||
return UNI_ERROR_NO_SLOTS;
|
||||
}
|
||||
|
||||
g_status_led_tick = 0;
|
||||
if (all_slots_ready()) {
|
||||
g_connection_status = ConnectionStatus::Ready;
|
||||
uni_bt_stop_scanning_unsafe();
|
||||
uni_bt_allow_incoming_connections(false);
|
||||
} else {
|
||||
g_connection_status = ConnectionStatus::Scanning;
|
||||
resume_connections();
|
||||
}
|
||||
recompute_connection_status();
|
||||
return UNI_ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -10,7 +10,10 @@ namespace {
|
|||
bool incoming_connections = false;
|
||||
int scan_starts = 0;
|
||||
int scan_stops = 0;
|
||||
bool scanning_enabled = false;
|
||||
uni_platform* installed_platform = nullptr;
|
||||
bool observed_status_led_on = false;
|
||||
int observed_status_led_writes = 0;
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
|
|
@ -50,10 +53,12 @@ void uni_bt_allow_incoming_connections(bool enabled) {
|
|||
|
||||
void uni_bt_start_scanning_and_autoconnect_unsafe() {
|
||||
++scan_starts;
|
||||
scanning_enabled = true;
|
||||
}
|
||||
|
||||
void uni_bt_stop_scanning_unsafe() {
|
||||
++scan_stops;
|
||||
scanning_enabled = false;
|
||||
}
|
||||
|
||||
void uni_platform_set_custom(uni_platform* platform) {
|
||||
|
|
@ -68,7 +73,11 @@ int cyw43_arch_init() {
|
|||
return 0;
|
||||
}
|
||||
|
||||
void cyw43_arch_gpio_put(int, bool) {}
|
||||
void cyw43_arch_gpio_put(int, bool enabled) {
|
||||
observed_status_led_on = enabled;
|
||||
++observed_status_led_writes;
|
||||
}
|
||||
|
||||
void multicore_launch_core1(void (*)()) {}
|
||||
|
||||
#include "../bluepad32_input_backend.cpp"
|
||||
|
|
@ -82,10 +91,33 @@ void start_backend() {
|
|||
require(scan_starts == 1, "initialization must start scanning");
|
||||
}
|
||||
|
||||
void tick_backend_timer(int ticks) {
|
||||
for (int tick = 0; tick < ticks; ++tick) {
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
}
|
||||
}
|
||||
|
||||
void test_ready_order(int first_slot) {
|
||||
start_backend();
|
||||
uni_hid_device_t devices[2] = {device(0), device(1)};
|
||||
const int second_slot = 1 - first_slot;
|
||||
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[first_slot]);
|
||||
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");
|
||||
|
||||
require(platform_on_device_ready(&devices[first_slot]) ==
|
||||
UNI_ERROR_SUCCESS,
|
||||
|
|
@ -94,6 +126,12 @@ void test_ready_order(int first_slot) {
|
|||
"scanning must continue while one slot remains free");
|
||||
require(incoming_connections,
|
||||
"incoming connections must remain enabled with one ready slot");
|
||||
tick_backend_timer(99);
|
||||
require(observed_status_led_on,
|
||||
"one ready slot must leave the LED in the slow scanning cycle");
|
||||
tick_backend_timer(1);
|
||||
require(!observed_status_led_on,
|
||||
"one open slot must produce the scanning LED off transition");
|
||||
|
||||
SwitchInputState first{};
|
||||
SwitchInputState second{};
|
||||
|
|
@ -109,6 +147,14 @@ void test_ready_order(int first_slot) {
|
|||
"scanning must stop exactly when both slots are ready");
|
||||
require(!incoming_connections,
|
||||
"incoming connections must be disabled only when full");
|
||||
tick_backend_timer(1);
|
||||
require(observed_status_led_on,
|
||||
"both 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,
|
||||
"both ready slots must keep the status LED solid");
|
||||
|
||||
bd_addr_t address{};
|
||||
require(platform_on_device_discovered(address, "extra", 0, 0) ==
|
||||
|
|
@ -146,12 +192,76 @@ void test_rejections() {
|
|||
|
||||
void test_independent_lifecycle() {
|
||||
start_backend();
|
||||
|
||||
uni_hid_device_t aborted = device(0);
|
||||
const uint32_t aborted_generation = g_slots[0].connection_generation;
|
||||
platform_on_device_connected(&aborted);
|
||||
require(g_slots[0].device == &aborted && !g_slots[0].active,
|
||||
"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;
|
||||
platform_on_device_disconnected(&aborted);
|
||||
require(g_slots[0].device == nullptr && !g_slots[0].active,
|
||||
"pre-ready disconnect must clear its pending slot identity");
|
||||
require(g_slots[0].connection_generation == aborted_generation + 1,
|
||||
"pre-ready disconnect must invalidate its connection generation");
|
||||
require(g_connection_status == ConnectionStatus::Scanning &&
|
||||
scanning_enabled && incoming_connections &&
|
||||
scan_starts == starts_before_aborted_disconnect + 1,
|
||||
"pre-ready disconnect with no peer must resume scanning");
|
||||
tick_backend_timer(99);
|
||||
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 first = device(0);
|
||||
uni_hid_device_t survivor = device(1);
|
||||
platform_on_device_connected(&first);
|
||||
platform_on_device_connected(&survivor);
|
||||
require(g_slots[0].device == &first && !g_slots[0].active &&
|
||||
g_slots[1].device == &survivor && !g_slots[1].active,
|
||||
"concurrent pending devices must retain independent identities");
|
||||
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 =
|
||||
g_slots[0].connection_generation;
|
||||
const int starts_before_first_pending_disconnect = scan_starts;
|
||||
platform_on_device_disconnected(&first);
|
||||
require(g_slots[0].device == nullptr && !g_slots[0].active &&
|
||||
g_slots[1].device == &survivor && !g_slots[1].active,
|
||||
"pre-ready disconnect must preserve the other pending identity");
|
||||
require(g_slots[0].connection_generation ==
|
||||
first_pending_generation + 1,
|
||||
"pending disconnect beside a peer must invalidate its generation");
|
||||
require(g_connection_status == ConnectionStatus::Connecting &&
|
||||
scanning_enabled && incoming_connections &&
|
||||
scan_starts == starts_before_first_pending_disconnect + 1,
|
||||
"open slot must scan while another slot remains connecting");
|
||||
tick_backend_timer(19);
|
||||
require(observed_status_led_on,
|
||||
"surviving pending device must retain the fast LED on half-cycle");
|
||||
tick_backend_timer(1);
|
||||
require(!observed_status_led_on,
|
||||
"surviving pending device must retain the fast LED off half-cycle");
|
||||
|
||||
require(platform_on_device_ready(&survivor) == UNI_ERROR_SUCCESS,
|
||||
"slot 1 must be accepted before slot 0");
|
||||
"surviving pending device must still become ready");
|
||||
platform_on_device_connected(&first);
|
||||
require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS,
|
||||
"slot 0 must complete the pair");
|
||||
"reconnected slot 0 device must complete the pair");
|
||||
|
||||
uni_controller_t data0{};
|
||||
data0.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
|
|
@ -239,6 +349,72 @@ void test_independent_lifecycle() {
|
|||
require(survivor.rumble_calls == 2 && survivor.last_low == 92 &&
|
||||
survivor.last_high == 93,
|
||||
"survivor rumble must continue after peer replacement");
|
||||
|
||||
const int starts_before_slot_one_disconnect = scan_starts;
|
||||
platform_on_device_disconnected(&survivor);
|
||||
require(scan_starts == starts_before_slot_one_disconnect + 1 &&
|
||||
incoming_connections,
|
||||
"slot 1 disconnect must resume scanning for its open slot");
|
||||
require(bluepad32_input_backend_snapshot(0, &state0) && state0.button_x,
|
||||
"slot 1 disconnect must preserve slot 0 state and activity");
|
||||
require(!bluepad32_input_backend_snapshot(1, &state1) &&
|
||||
!state1.button_b && state1.lx == 32768,
|
||||
"slot 1 disconnect must neutralize only slot 1");
|
||||
|
||||
uni_controller_t continuing_slot_zero_data{};
|
||||
continuing_slot_zero_data.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
continuing_slot_zero_data.gamepad.buttons = BUTTON_B;
|
||||
platform_on_controller_data(&replacement, &continuing_slot_zero_data);
|
||||
require(bluepad32_input_backend_snapshot(0, &state0) && state0.button_a,
|
||||
"slot 0 input must continue while slot 1 is disconnected");
|
||||
|
||||
const int slot_zero_calls_while_scanning = replacement.rumble_calls;
|
||||
bluepad32_input_backend_queue_rumble(0, SwitchRumbleOutput{115, 116});
|
||||
tick_backend_timer(99);
|
||||
require(replacement.rumble_calls == slot_zero_calls_while_scanning + 1 &&
|
||||
replacement.last_low == 115 && replacement.last_high == 116,
|
||||
"slot 0 rumble must continue while slot 1 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 first_slot_one_replacement = device(1);
|
||||
require(platform_on_device_ready(&first_slot_one_replacement) ==
|
||||
UNI_ERROR_SUCCESS,
|
||||
"slot 1 replacement must bind without disturbing slot 0");
|
||||
tick_backend_timer(1);
|
||||
require(observed_status_led_on,
|
||||
"replacing the open slot must return the LED to solid ready");
|
||||
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 pair must keep the ready LED solid");
|
||||
|
||||
bluepad32_input_backend_queue_rumble(1, SwitchRumbleOutput{117, 118});
|
||||
platform_on_device_disconnected(&first_slot_one_replacement);
|
||||
uni_hid_device_t second_slot_one_replacement = device(1);
|
||||
require(platform_on_device_ready(&second_slot_one_replacement) ==
|
||||
UNI_ERROR_SUCCESS,
|
||||
"a subsequent slot 1 replacement must bind to the freed slot");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(second_slot_one_replacement.rumble_calls == 0,
|
||||
"slot 1 replacement must not receive prior-generation rumble");
|
||||
|
||||
const int slot_zero_calls_before_mailboxes = replacement.rumble_calls;
|
||||
bluepad32_input_backend_queue_rumble(1, SwitchRumbleOutput{119, 120});
|
||||
bluepad32_input_backend_queue_rumble(1, SwitchRumbleOutput{121, 122});
|
||||
bluepad32_input_backend_queue_rumble(0, SwitchRumbleOutput{123, 124});
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(second_slot_one_replacement.rumble_calls == 1 &&
|
||||
second_slot_one_replacement.last_low == 121 &&
|
||||
second_slot_one_replacement.last_high == 122,
|
||||
"slot 1 mailbox must dispatch only its latest queued value");
|
||||
require(replacement.rumble_calls == slot_zero_calls_before_mailboxes + 1 &&
|
||||
replacement.last_low == 123 && replacement.last_high == 124,
|
||||
"slot 0 activity must not evict the slot 1 mailbox");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue