Keep wake identity stable during play
This commit is contained in:
parent
6f58d345e0
commit
1f9d7f6824
9 changed files with 147 additions and 87 deletions
37
README.md
37
README.md
|
|
@ -77,12 +77,11 @@ captured from a Joy-Con 2 already paired with that Switch 2. The generated
|
||||||
configuration is console-specific and is intentionally ignored by Git.
|
configuration is console-specific and is intentionally ignored by Git.
|
||||||
|
|
||||||
The implementation replays the captured, unencrypted Switch 2 BLE wake
|
The implementation replays the captured, unencrypted Switch 2 BLE wake
|
||||||
advertisement for two seconds at a 20 ms base interval. The CYW43439 has one
|
advertisement for two seconds at a 20 ms base interval. The configured AIO
|
||||||
controller-wide public Bluetooth address, so the firmware temporarily changes
|
firmware adopts the captured Joy-Con's public Bluetooth address once during
|
||||||
from the Pico's normal identity to the captured Joy-Con identity for the wake
|
startup, before Bluepad32 admits controller connections. Wake bursts then
|
||||||
burst and restores it afterward. The explicit chord confines the resulting
|
require no identity change and do not disconnect the input controller. This
|
||||||
input-controller disconnect to an intentional wake attempt. This follows the
|
follows the packet format documented by
|
||||||
packet format documented by
|
|
||||||
[`ndeadly/switch2_controller_research`](https://github.com/ndeadly/switch2_controller_research/blob/master/bluetooth_interface.md)
|
[`ndeadly/switch2_controller_research`](https://github.com/ndeadly/switch2_controller_research/blob/master/bluetooth_interface.md)
|
||||||
and the capture/replay approach demonstrated by
|
and the capture/replay approach demonstrated by
|
||||||
[`alexvnesta/switch2controller`](https://github.com/alexvnesta/switch2controller)
|
[`alexvnesta/switch2controller`](https://github.com/alexvnesta/switch2controller)
|
||||||
|
|
@ -131,6 +130,10 @@ Then:
|
||||||
python3 build.py --aio
|
python3 build.py --aio
|
||||||
```
|
```
|
||||||
|
|
||||||
|
6. The Pico now has a new stable Bluetooth host address. Clear its old
|
||||||
|
Bluepad32 bonds by holding BOOTSEL for ten seconds, then open a pairing
|
||||||
|
window and pair each input controller again. This is a one-time re-pair.
|
||||||
|
|
||||||
|
|
||||||
The capture tool also accepts a saved serial log:
|
The capture tool also accepts a saved serial log:
|
||||||
|
|
||||||
|
|
@ -143,24 +146,20 @@ on the paired input controller with Home, PS, or Xbox and let it reconnect to
|
||||||
the Pico. Then hold L + R and press its system button to send one wake burst.
|
the Pico. Then hold L + R and press its system button to send one wake burst.
|
||||||
Holding the chord does not retrigger it; release at least one chord button
|
Holding the chord does not retrigger it; release at least one chord button
|
||||||
before another attempt. Plain Home, PS, or Xbox is forwarded normally and does
|
before another attempt. Plain Home, PS, or Xbox is forwarded normally and does
|
||||||
not disturb the radio.
|
not disturb the radio. Because the wake identity is stable from startup, the
|
||||||
|
input controller stays connected through the advertising burst.
|
||||||
The input controller disconnects during the intentional wake burst because
|
|
||||||
the CYW43439 cannot retain its normal public identity while transmitting the
|
|
||||||
captured controller's public identity. It can reconnect after the Pico restores
|
|
||||||
its address. Avoiding that disconnect requires a second BLE radio dedicated to
|
|
||||||
wake transmission; keeping the captured identity throughout gameplay caused
|
|
||||||
severe Classic Bluetooth latency in hardware testing.
|
|
||||||
|
|
||||||
The Pico must remain powered for wireless wake. If the Switch or dock removes
|
The Pico must remain powered for wireless wake. If the Switch or dock removes
|
||||||
USB power during sleep, use a powered USB arrangement that preserves the
|
USB power during sleep, use a powered USB arrangement that preserves the
|
||||||
Pico-to-Switch data connection. Keep the captured Joy-Con inactive during the
|
Pico-to-Switch data connection. The configured Pico continuously owns the
|
||||||
two-second wake burst to avoid two radios using one address.
|
captured Joy-Con's public Bluetooth address, so keep that Joy-Con inactive
|
||||||
|
while the AIO firmware is running to avoid two radios using one address.
|
||||||
|
|
||||||
To target another Switch 2, repeat the capture and configuration steps. To
|
To target another Switch 2, repeat the capture and configuration steps. To
|
||||||
disable wake, delete the generated `switch2_wake_config.h` and rebuild the AIO
|
disable wake, delete the generated `switch2_wake_config.h`, rebuild the AIO
|
||||||
firmware. Restore the full-flash backup only if you need to recover the exact
|
firmware, clear the Pico's bonds, and pair the input controllers to its restored
|
||||||
pre-capture firmware and persistent state.
|
factory address. Restore the full-flash backup only if you need to recover the
|
||||||
|
exact pre-capture firmware and persistent state.
|
||||||
|
|
||||||
### Pairing up to four controllers
|
### Pairing up to four controllers
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -1076,6 +1076,7 @@ void process_pairing_snapshot_request() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void apply_connection_policy();
|
void apply_connection_policy();
|
||||||
|
void recompute_connection_status();
|
||||||
|
|
||||||
void process_clear_pairings(uint32_t now_ms) {
|
void process_clear_pairings(uint32_t now_ms) {
|
||||||
uni_hid_device_t* devices[kSlotCount]{};
|
uni_hid_device_t* devices[kSlotCount]{};
|
||||||
|
|
@ -1258,7 +1259,14 @@ void process_rumble_timer(btstack_timer_source_t* timer) {
|
||||||
|
|
||||||
process_clear_pairings(now_ms);
|
process_clear_pairings(now_ms);
|
||||||
process_pairing_snapshot_request();
|
process_pairing_snapshot_request();
|
||||||
if (update_pairing_window(now_ms)) {
|
const bool wake_identity_ready =
|
||||||
|
switch2_wake_ready_for_connections();
|
||||||
|
if (g_connection_policy_state ==
|
||||||
|
ConnectionPolicyState::Uninitialized &&
|
||||||
|
wake_identity_ready) {
|
||||||
|
recompute_connection_status();
|
||||||
|
}
|
||||||
|
if (update_pairing_window(now_ms) && wake_identity_ready) {
|
||||||
apply_connection_policy();
|
apply_connection_policy();
|
||||||
}
|
}
|
||||||
const bool xinput_host_mode =
|
const bool xinput_host_mode =
|
||||||
|
|
@ -1492,7 +1500,9 @@ void platform_on_init_complete() {
|
||||||
&g_configuration_timer, kConfigurationPollIntervalMs);
|
&g_configuration_timer, kConfigurationPollIntervalMs);
|
||||||
btstack_run_loop_add_timer(&g_configuration_timer);
|
btstack_run_loop_add_timer(&g_configuration_timer);
|
||||||
__atomic_store_n(&g_initialization_stage, 6, __ATOMIC_RELEASE);
|
__atomic_store_n(&g_initialization_stage, 6, __ATOMIC_RELEASE);
|
||||||
recompute_connection_status();
|
if (switch2_wake_ready_for_connections()) {
|
||||||
|
recompute_connection_status();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uni_error_t platform_on_device_discovered(bd_addr_t addr, const char* name,
|
uni_error_t platform_on_device_discovered(bd_addr_t addr, const char* name,
|
||||||
|
|
|
||||||
|
|
@ -31,19 +31,18 @@ const bd_addr_t kUnusedPeerAddress{};
|
||||||
|
|
||||||
enum class Phase : uint8_t {
|
enum class Phase : uint8_t {
|
||||||
kDisabled,
|
kDisabled,
|
||||||
|
kSetStableAddress,
|
||||||
|
kVerifyStableAddress,
|
||||||
kIdle,
|
kIdle,
|
||||||
kSetWakeAddress,
|
|
||||||
kSetParameters,
|
kSetParameters,
|
||||||
kSetData,
|
kSetData,
|
||||||
kEnableAdvertising,
|
kEnableAdvertising,
|
||||||
kAdvertising,
|
kAdvertising,
|
||||||
kDisableAdvertising,
|
kDisableAdvertising,
|
||||||
kRestoreAddress,
|
|
||||||
kFailed,
|
kFailed,
|
||||||
};
|
};
|
||||||
|
|
||||||
Phase g_phase = Phase::kDisabled;
|
Phase g_phase = Phase::kDisabled;
|
||||||
bd_addr_t g_original_address{};
|
|
||||||
btstack_packet_callback_registration_t g_event_registration{};
|
btstack_packet_callback_registration_t g_event_registration{};
|
||||||
btstack_timer_source_t g_timer{};
|
btstack_timer_source_t g_timer{};
|
||||||
uint16_t g_pending_opcode = 0;
|
uint16_t g_pending_opcode = 0;
|
||||||
|
|
@ -54,8 +53,8 @@ uint32_t g_completed_bursts = 0;
|
||||||
uint32_t g_failures = 0;
|
uint32_t g_failures = 0;
|
||||||
bool g_initialized = false;
|
bool g_initialized = false;
|
||||||
bool g_configured = false;
|
bool g_configured = false;
|
||||||
|
bool g_identity_ready = false;
|
||||||
bool g_timer_armed = false;
|
bool g_timer_armed = false;
|
||||||
bool g_address_changed = false;
|
|
||||||
bool g_advertising = false;
|
bool g_advertising = false;
|
||||||
|
|
||||||
bool deadline_reached(uint32_t now, uint32_t deadline) {
|
bool deadline_reached(uint32_t now, uint32_t deadline) {
|
||||||
|
|
@ -129,10 +128,13 @@ bool configured_packet_valid() {
|
||||||
void recover_from_failure() {
|
void recover_from_failure() {
|
||||||
++g_failures;
|
++g_failures;
|
||||||
g_pending_opcode = 0;
|
g_pending_opcode = 0;
|
||||||
if (g_advertising) {
|
if (g_phase == Phase::kSetStableAddress ||
|
||||||
|
g_phase == Phase::kVerifyStableAddress) {
|
||||||
|
// Wake stays disabled, but controller input must remain available.
|
||||||
|
g_identity_ready = true;
|
||||||
|
g_phase = Phase::kFailed;
|
||||||
|
} else if (g_advertising) {
|
||||||
g_phase = Phase::kDisableAdvertising;
|
g_phase = Phase::kDisableAdvertising;
|
||||||
} else if (g_address_changed) {
|
|
||||||
g_phase = Phase::kRestoreAddress;
|
|
||||||
} else {
|
} else {
|
||||||
g_phase = Phase::kIdle;
|
g_phase = Phase::kIdle;
|
||||||
}
|
}
|
||||||
|
|
@ -150,12 +152,16 @@ void submit_phase_command(uint32_t now_ms) {
|
||||||
|
|
||||||
uint8_t result = ERROR_CODE_SUCCESS;
|
uint8_t result = ERROR_CODE_SUCCESS;
|
||||||
switch (g_phase) {
|
switch (g_phase) {
|
||||||
case Phase::kSetWakeAddress:
|
case Phase::kSetStableAddress:
|
||||||
begin_command(kWritePublicAddress.opcode, now_ms);
|
begin_command(kWritePublicAddress.opcode, now_ms);
|
||||||
#if SWITCH2_WAKE_CONFIGURED
|
#if SWITCH2_WAKE_CONFIGURED
|
||||||
result = hci_send_cmd(&kWritePublicAddress, kWakeAddress);
|
result = hci_send_cmd(&kWritePublicAddress, kWakeAddress);
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
|
case Phase::kVerifyStableAddress:
|
||||||
|
begin_command(hci_read_bd_addr.opcode, now_ms);
|
||||||
|
result = hci_send_cmd(&hci_read_bd_addr);
|
||||||
|
break;
|
||||||
case Phase::kSetParameters:
|
case Phase::kSetParameters:
|
||||||
begin_command(hci_le_set_advertising_parameters.opcode, now_ms);
|
begin_command(hci_le_set_advertising_parameters.opcode, now_ms);
|
||||||
result = hci_send_cmd(
|
result = hci_send_cmd(
|
||||||
|
|
@ -181,10 +187,6 @@ void submit_phase_command(uint32_t now_ms) {
|
||||||
&hci_le_set_advertise_enable,
|
&hci_le_set_advertise_enable,
|
||||||
g_phase == Phase::kEnableAdvertising ? 1 : 0);
|
g_phase == Phase::kEnableAdvertising ? 1 : 0);
|
||||||
break;
|
break;
|
||||||
case Phase::kRestoreAddress:
|
|
||||||
begin_command(kWritePublicAddress.opcode, now_ms);
|
|
||||||
result = hci_send_cmd(&kWritePublicAddress, g_original_address);
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -213,9 +215,27 @@ void handle_command_complete(uint8_t* packet, uint16_t size) {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (g_phase) {
|
switch (g_phase) {
|
||||||
case Phase::kSetWakeAddress:
|
case Phase::kSetStableAddress:
|
||||||
g_address_changed = true;
|
g_phase = Phase::kVerifyStableAddress;
|
||||||
g_phase = Phase::kSetParameters;
|
break;
|
||||||
|
case Phase::kVerifyStableAddress:
|
||||||
|
#if SWITCH2_WAKE_CONFIGURED
|
||||||
|
if (size < 12) {
|
||||||
|
recover_from_failure();
|
||||||
|
schedule_for_phase(btstack_run_loop_get_time_ms());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (size_t index = 0; index < sizeof(kWakeAddress); ++index) {
|
||||||
|
if (packet[6 + index] !=
|
||||||
|
kWakeAddress[sizeof(kWakeAddress) - 1 - index]) {
|
||||||
|
recover_from_failure();
|
||||||
|
schedule_for_phase(btstack_run_loop_get_time_ms());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
g_identity_ready = true;
|
||||||
|
g_phase = Phase::kIdle;
|
||||||
break;
|
break;
|
||||||
case Phase::kSetParameters:
|
case Phase::kSetParameters:
|
||||||
g_phase = Phase::kSetData;
|
g_phase = Phase::kSetData;
|
||||||
|
|
@ -231,10 +251,6 @@ void handle_command_complete(uint8_t* packet, uint16_t size) {
|
||||||
break;
|
break;
|
||||||
case Phase::kDisableAdvertising:
|
case Phase::kDisableAdvertising:
|
||||||
g_advertising = false;
|
g_advertising = false;
|
||||||
g_phase = Phase::kRestoreAddress;
|
|
||||||
break;
|
|
||||||
case Phase::kRestoreAddress:
|
|
||||||
g_address_changed = false;
|
|
||||||
++g_completed_bursts;
|
++g_completed_bursts;
|
||||||
g_phase = Phase::kIdle;
|
g_phase = Phase::kIdle;
|
||||||
break;
|
break;
|
||||||
|
|
@ -277,26 +293,32 @@ void switch2_wake_initialize() {
|
||||||
g_initialized = true;
|
g_initialized = true;
|
||||||
#if SWITCH2_WAKE_CONFIGURED
|
#if SWITCH2_WAKE_CONFIGURED
|
||||||
if (!configured_packet_valid()) {
|
if (!configured_packet_valid()) {
|
||||||
|
g_identity_ready = true;
|
||||||
g_phase = Phase::kFailed;
|
g_phase = Phase::kFailed;
|
||||||
++g_failures;
|
++g_failures;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
g_configured = true;
|
g_configured = true;
|
||||||
gap_local_bd_addr(g_original_address);
|
|
||||||
g_event_registration.callback = handle_hci_event;
|
g_event_registration.callback = handle_hci_event;
|
||||||
hci_add_event_handler(&g_event_registration);
|
hci_add_event_handler(&g_event_registration);
|
||||||
btstack_run_loop_set_timer_handler(&g_timer, task);
|
btstack_run_loop_set_timer_handler(&g_timer, task);
|
||||||
g_phase = Phase::kIdle;
|
g_phase = Phase::kSetStableAddress;
|
||||||
|
schedule_task(0);
|
||||||
|
#else
|
||||||
|
g_identity_ready = true;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool switch2_wake_ready_for_connections() {
|
||||||
|
return g_identity_ready;
|
||||||
|
}
|
||||||
|
|
||||||
bool switch2_wake_request() {
|
bool switch2_wake_request() {
|
||||||
if (g_phase != Phase::kIdle) {
|
if (g_phase != Phase::kIdle) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
++g_accepted_requests;
|
++g_accepted_requests;
|
||||||
g_phase = Phase::kSetWakeAddress;
|
g_phase = Phase::kSetParameters;
|
||||||
schedule_task(0);
|
schedule_task(0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,10 @@ struct Switch2WakeDiagnostics {
|
||||||
uint32_t failures;
|
uint32_t failures;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Installs BTstack callbacks and remembers the Pico's normal public identity.
|
// Installs BTstack callbacks and applies the configured stable public identity.
|
||||||
|
// Call once from the BTstack core before admitting controller connections.
|
||||||
void switch2_wake_initialize();
|
void switch2_wake_initialize();
|
||||||
|
bool switch2_wake_ready_for_connections();
|
||||||
|
|
||||||
// Starts one wake burst when configured and idle. Calls while busy coalesce.
|
// Starts one wake burst when configured and idle. Calls while busy coalesce.
|
||||||
bool switch2_wake_request();
|
bool switch2_wake_request();
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ int cyw43_init_calls = 0;
|
||||||
int uni_init_calls = 0;
|
int uni_init_calls = 0;
|
||||||
int switch2_wake_initializations = 0;
|
int switch2_wake_initializations = 0;
|
||||||
int switch2_wake_requests = 0;
|
int switch2_wake_requests = 0;
|
||||||
|
bool switch2_connections_ready = true;
|
||||||
int device_disconnect_calls = 0;
|
int device_disconnect_calls = 0;
|
||||||
uni_hid_device_t* last_disconnected_device = nullptr;
|
uni_hid_device_t* last_disconnected_device = nullptr;
|
||||||
uni_hid_device_t* lookup_devices[8]{};
|
uni_hid_device_t* lookup_devices[8]{};
|
||||||
|
|
@ -448,6 +449,10 @@ uint32_t btstack_run_loop_get_time_ms() {
|
||||||
void switch2_wake_initialize() {
|
void switch2_wake_initialize() {
|
||||||
++switch2_wake_initializations;
|
++switch2_wake_initializations;
|
||||||
}
|
}
|
||||||
|
bool switch2_wake_ready_for_connections() {
|
||||||
|
return switch2_connections_ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
bool switch2_wake_request() {
|
bool switch2_wake_request() {
|
||||||
|
|
@ -2392,6 +2397,26 @@ void test_flash_core_start_contract() {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void test_wake_identity_gates_connections() {
|
||||||
|
switch2_connections_ready = false;
|
||||||
|
bluepad32_input_backend_init();
|
||||||
|
platform_on_init_complete();
|
||||||
|
require(switch2_wake_initializations == 1 &&
|
||||||
|
g_connection_policy_state ==
|
||||||
|
ConnectionPolicyState::Uninitialized &&
|
||||||
|
scan_starts == 0 && classic_scan_starts == 0 &&
|
||||||
|
!incoming_connections,
|
||||||
|
"controller discovery started before wake identity was ready");
|
||||||
|
|
||||||
|
switch2_connections_ready = true;
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(g_connection_policy_state == ConnectionPolicyState::Open &&
|
||||||
|
scan_starts == 1 && classic_scan_starts == 1 &&
|
||||||
|
incoming_connections,
|
||||||
|
"controller discovery did not start after wake identity setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void test_system_button_wake_trigger() {
|
void test_system_button_wake_trigger() {
|
||||||
start_pairing_backend();
|
start_pairing_backend();
|
||||||
uni_hid_device_t controller = device(0);
|
uni_hid_device_t controller = device(0);
|
||||||
|
|
@ -2486,6 +2511,8 @@ int main(int argc, char** argv) {
|
||||||
test_configuration_timer_rearms_before_storage_work();
|
test_configuration_timer_rearms_before_storage_work();
|
||||||
} else if (scenario == "flash-core-start") {
|
} else if (scenario == "flash-core-start") {
|
||||||
test_flash_core_start_contract();
|
test_flash_core_start_contract();
|
||||||
|
} else if (scenario == "wake-identity-gate") {
|
||||||
|
test_wake_identity_gates_connections();
|
||||||
} else if (scenario == "system-wake") {
|
} else if (scenario == "system-wake") {
|
||||||
test_system_button_wake_trigger();
|
test_system_button_wake_trigger();
|
||||||
} else if (scenario == "flash-core-failure") {
|
} else if (scenario == "flash-core-failure") {
|
||||||
|
|
|
||||||
|
|
@ -153,97 +153,97 @@ void require_opcode(size_t index, uint16_t opcode) {
|
||||||
"unexpected HCI command sequence");
|
"unexpected HCI command sequence");
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_temporary_identity_wake_and_restore() {
|
void test_stable_identity_wake() {
|
||||||
switch2_wake_initialize();
|
switch2_wake_initialize();
|
||||||
Switch2WakeDiagnostics diagnostics{};
|
Switch2WakeDiagnostics diagnostics{};
|
||||||
switch2_wake_diagnostics(&diagnostics);
|
switch2_wake_diagnostics(&diagnostics);
|
||||||
require(diagnostics.configured && !diagnostics.busy && !g_timer_armed,
|
require(diagnostics.configured && diagnostics.busy &&
|
||||||
"configured wake module did not initialize dormant");
|
!switch2_wake_ready_for_connections() &&
|
||||||
require(switch2_wake_request() && !switch2_wake_request(),
|
!switch2_wake_request(),
|
||||||
"wake requests were not bounded while busy");
|
"controller connections or wake escaped startup identity setup");
|
||||||
|
|
||||||
run_task();
|
run_task();
|
||||||
require_opcode(0, 0xfc01);
|
require_opcode(0, 0xfc01);
|
||||||
const uint8_t wake_address[] = {0x98, 0xE2, 0x55, 0x07, 0xDF, 0x00};
|
const uint8_t wake_address[] = {0x98, 0xE2, 0x55, 0x07, 0xDF, 0x00};
|
||||||
require(memcmp(submitted[0].address, wake_address, 6) == 0,
|
require(memcmp(submitted[0].address, wake_address, 6) == 0,
|
||||||
"wake address did not reach the radio command");
|
"stable wake address did not reach the radio command");
|
||||||
complete(0xfc01);
|
complete(0xfc01);
|
||||||
|
|
||||||
run_task();
|
run_task();
|
||||||
require_opcode(1, 0x2006);
|
require_opcode(1, 0x1009);
|
||||||
require(submitted[1].interval_min == 0x20 &&
|
complete(0x1009, 0, wake_address);
|
||||||
submitted[1].interval_max == 0x20 &&
|
require(switch2_wake_ready_for_connections() && !g_timer_armed,
|
||||||
submitted[1].advertising_type == 3 &&
|
"verified stable identity did not admit connections and go idle");
|
||||||
submitted[1].own_address_type == 0 &&
|
|
||||||
submitted[1].channel_map == 7 &&
|
switch2_wake_diagnostics(&diagnostics);
|
||||||
submitted[1].filter_policy == 0,
|
require(!diagnostics.busy && switch2_wake_request() &&
|
||||||
|
!switch2_wake_request(),
|
||||||
|
"wake requests were not bounded while busy");
|
||||||
|
run_task();
|
||||||
|
require_opcode(2, 0x2006);
|
||||||
|
require(submitted[2].interval_min == 0x20 &&
|
||||||
|
submitted[2].interval_max == 0x20 &&
|
||||||
|
submitted[2].advertising_type == 3 &&
|
||||||
|
submitted[2].own_address_type == 0 &&
|
||||||
|
submitted[2].channel_map == 7 &&
|
||||||
|
submitted[2].filter_policy == 0,
|
||||||
"known-working advertising parameters changed");
|
"known-working advertising parameters changed");
|
||||||
complete(0x2006);
|
complete(0x2006);
|
||||||
|
|
||||||
run_task();
|
run_task();
|
||||||
require_opcode(2, 0x2008);
|
require_opcode(3, 0x2008);
|
||||||
require(submitted[2].data_length == 31 &&
|
require(submitted[3].data_length == 31 &&
|
||||||
submitted[2].data[16] == 0x81,
|
submitted[3].data[16] == 0x81,
|
||||||
"captured wake payload was not submitted intact");
|
"captured wake payload was not submitted intact");
|
||||||
complete(0x2008);
|
complete(0x2008);
|
||||||
|
|
||||||
run_task();
|
run_task();
|
||||||
require_opcode(3, 0x200a);
|
require_opcode(4, 0x200a);
|
||||||
require(submitted[3].enabled == 1,
|
require(submitted[4].enabled == 1,
|
||||||
"wake advertising was not enabled");
|
"wake advertising was not enabled");
|
||||||
complete(0x200a);
|
complete(0x200a);
|
||||||
require(g_timer_armed && installed_timer->timeout_ms == 2000,
|
require(g_timer_armed && installed_timer->timeout_ms == 2000,
|
||||||
"wake burst did not schedule one exact stop deadline");
|
"wake burst did not schedule one exact stop deadline");
|
||||||
now_ms = 1999;
|
now_ms = 1999;
|
||||||
run_task();
|
run_task();
|
||||||
require(submitted_count == 4,
|
require(submitted_count == 5,
|
||||||
"wake burst stopped before two seconds");
|
"wake burst stopped before two seconds");
|
||||||
now_ms = 2000;
|
now_ms = 2000;
|
||||||
run_task();
|
run_task();
|
||||||
require_opcode(4, 0x200a);
|
require_opcode(5, 0x200a);
|
||||||
require(submitted[4].enabled == 0,
|
require(submitted[5].enabled == 0,
|
||||||
"wake advertising was not disabled");
|
"wake advertising was not disabled");
|
||||||
complete(0x200a);
|
complete(0x200a);
|
||||||
|
|
||||||
run_task();
|
|
||||||
require_opcode(5, 0xfc01);
|
|
||||||
require(memcmp(submitted[5].address, original_address, 6) == 0,
|
|
||||||
"Pico gameplay identity was not restored");
|
|
||||||
complete(0xfc01);
|
|
||||||
|
|
||||||
switch2_wake_diagnostics(&diagnostics);
|
switch2_wake_diagnostics(&diagnostics);
|
||||||
require(!diagnostics.busy && diagnostics.accepted_requests == 1 &&
|
require(!diagnostics.busy && diagnostics.accepted_requests == 1 &&
|
||||||
diagnostics.completed_bursts == 1 &&
|
diagnostics.completed_bursts == 1 &&
|
||||||
diagnostics.failures == 0 && !g_timer_armed,
|
diagnostics.failures == 0 && !g_timer_armed,
|
||||||
"completed wake did not restore a dormant gameplay state");
|
"completed wake did not return to a dormant idle state");
|
||||||
|
require(submitted_count == 6,
|
||||||
|
"wake burst changed the public identity after startup");
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_failed_setup_restores_gameplay_identity() {
|
void test_failed_wake_keeps_stable_identity() {
|
||||||
require(switch2_wake_request(),
|
require(switch2_wake_request(),
|
||||||
"idle module rejected a second wake request");
|
"idle module rejected a second wake request");
|
||||||
run_task();
|
run_task();
|
||||||
require_opcode(6, 0xfc01);
|
require_opcode(6, 0x2006);
|
||||||
complete(0xfc01);
|
|
||||||
run_task();
|
|
||||||
require_opcode(7, 0x2006);
|
|
||||||
complete(0x2006, 0x12);
|
complete(0x2006, 0x12);
|
||||||
run_task();
|
run_task();
|
||||||
require_opcode(8, 0xfc01);
|
require(submitted_count == 7,
|
||||||
require(memcmp(submitted[8].address, original_address, 6) == 0,
|
"wake setup failure issued an address reset");
|
||||||
"wake setup failure did not restore the gameplay identity");
|
|
||||||
complete(0xfc01);
|
|
||||||
|
|
||||||
Switch2WakeDiagnostics diagnostics{};
|
Switch2WakeDiagnostics diagnostics{};
|
||||||
switch2_wake_diagnostics(&diagnostics);
|
switch2_wake_diagnostics(&diagnostics);
|
||||||
require(!diagnostics.busy && diagnostics.failures == 1 &&
|
require(!diagnostics.busy && diagnostics.failures == 1 &&
|
||||||
!g_timer_armed,
|
switch2_wake_ready_for_connections(),
|
||||||
"wake failure did not recover to dormant gameplay");
|
"wake failure disrupted the stable controller identity");
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
test_temporary_identity_wake_and_restore();
|
test_stable_identity_wake();
|
||||||
test_failed_setup_restores_gameplay_identity();
|
test_failed_wake_keeps_stable_identity();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
||||||
"clear-pairings",
|
"clear-pairings",
|
||||||
"configuration-timer",
|
"configuration-timer",
|
||||||
"flash-core-start",
|
"flash-core-start",
|
||||||
|
"wake-identity-gate",
|
||||||
"system-wake",
|
"system-wake",
|
||||||
"flash-core-failure",
|
"flash-core-failure",
|
||||||
):
|
):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue