Add Switch 2 wake capture and replay
This commit is contained in:
parent
8818f1f6e9
commit
6f58d345e0
22 changed files with 1436 additions and 3 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -14,3 +14,4 @@ __pycache__
|
|||
pytest-*/*
|
||||
build-*/*
|
||||
.ignore
|
||||
src/firmware/platform/pico/switch2_wake_config.h
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
|||
${SWITCH_PICO_SOURCE_DIR}/configuration/configuration_transaction.cpp
|
||||
${SWITCH_PICO_SOURCE_DIR}/core/controller_identity.cpp
|
||||
${SWITCH_PICO_SOURCE_DIR}/input/bluepad32_input_backend.cpp
|
||||
${SWITCH_PICO_SOURCE_DIR}/input/switch2_wake.cpp
|
||||
${SWITCH_PICO_SOURCE_DIR}/platform/pico/bootsel_pairing_button.cpp
|
||||
${SWITCH_PICO_SOURCE_DIR}/platform/pico/pico_configuration_storage.cpp
|
||||
${SWITCH_PICO_SOURCE_DIR}/platform/pico/pico_profile_storage.cpp
|
||||
|
|
|
|||
96
README.md
96
README.md
|
|
@ -66,6 +66,102 @@ The default `python3 build.py` command and `firmware/switch-pico.*` artifacts re
|
|||
|
||||
Both `build.py --aio` and direct AIO CMake configuration copy the pinned Bluepad32 source into the active build directory and apply `patches/bluepad32-sdl3-imu.patch` there before compiling. The patch makes supported motion controllers use SDL3-equivalent axes and fixed-point units before conversion to Nintendo samples. The `external/bluepad32` submodule remains pristine; patch or source-revision drift fails configuration.
|
||||
|
||||
### Switch 2 wake from L + R + Home, PS, or Xbox
|
||||
|
||||
The AIO firmware can wake a sleeping Switch 2 when a connected controller's
|
||||
physical **L + R + System** chord becomes held: L + R + Home on
|
||||
Nintendo-style controllers, L1 + R1 + PS on PlayStation controllers, or
|
||||
LB + RB + Xbox on Xbox controllers. Plain Home, PS, or Xbox remains a normal
|
||||
button and does not start wake advertising. Setup needs one wake advertisement
|
||||
captured from a Joy-Con 2 already paired with that Switch 2. The generated
|
||||
configuration is console-specific and is intentionally ignored by Git.
|
||||
|
||||
The implementation replays the captured, unencrypted Switch 2 BLE wake
|
||||
advertisement for two seconds at a 20 ms base interval. The CYW43439 has one
|
||||
controller-wide public Bluetooth address, so the firmware temporarily changes
|
||||
from the Pico's normal identity to the captured Joy-Con identity for the wake
|
||||
burst and restores it afterward. The explicit chord confines the resulting
|
||||
input-controller disconnect to an intentional wake attempt. This follows the
|
||||
packet format documented by
|
||||
[`ndeadly/switch2_controller_research`](https://github.com/ndeadly/switch2_controller_research/blob/master/bluetooth_interface.md)
|
||||
and the capture/replay approach demonstrated by
|
||||
[`alexvnesta/switch2controller`](https://github.com/alexvnesta/switch2controller)
|
||||
and the MIT-licensed
|
||||
[`Switch2-Wake-Beacon-ESPHome`](https://github.com/sickyj/Switch2-Wake-Beacon-ESPHome).
|
||||
|
||||
Back up the complete Pico flash before replacing the AIO firmware with the
|
||||
temporary capture image:
|
||||
|
||||
```sh
|
||||
picotool save -a -v switch-pico-before-wake-capture.uf2
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
1. Connect the Pico 2 W to the computer and build/flash the one-shot capture
|
||||
firmware:
|
||||
|
||||
```sh
|
||||
python3 build.py --wake-capture
|
||||
```
|
||||
|
||||
This also publishes `firmware/switch-pico-wake-capture.elf` and
|
||||
`firmware/switch-pico-wake-capture.uf2`.
|
||||
|
||||
2. Start the configuration tool. It auto-detects a single Pico USB serial
|
||||
port; use `--port /dev/ttyACM0` when more than one Pico is attached:
|
||||
|
||||
```sh
|
||||
python3 tools/configure_switch2_wake.py
|
||||
```
|
||||
|
||||
3. Detach a Joy-Con 2 that is already paired with the target Switch 2, put the
|
||||
console to sleep, and press that Joy-Con's Home button. Do not press its
|
||||
sync button. The capture firmware accepts the first public `ADV_IND` packet
|
||||
with Nintendo's Switch 2 wake flag and nonzero console address, stops
|
||||
scanning automatically, lights the onboard LED solid, and repeats the
|
||||
captured record until the tool receives it.
|
||||
|
||||
4. The tool validates the packet and atomically writes
|
||||
`src/firmware/platform/pico/switch2_wake_config.h`.
|
||||
|
||||
5. Restore the AIO firmware with the generated wake configuration:
|
||||
|
||||
```sh
|
||||
python3 build.py --aio
|
||||
```
|
||||
|
||||
|
||||
The capture tool also accepts a saved serial log:
|
||||
|
||||
```sh
|
||||
python3 tools/configure_switch2_wake.py --input switch2-joycon-capture.log
|
||||
```
|
||||
|
||||
With the configured AIO firmware powered while the console sleeps, first turn
|
||||
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.
|
||||
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
|
||||
not disturb the radio.
|
||||
|
||||
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
|
||||
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
|
||||
two-second wake burst to avoid two radios using one address.
|
||||
|
||||
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
|
||||
firmware. Restore the full-flash backup only if you need to recover the exact
|
||||
pre-capture firmware and persistent state.
|
||||
|
||||
### Pairing up to four controllers
|
||||
|
||||
1. Flash and connect the Pico 2 W to the Switch.
|
||||
|
|
|
|||
62
build.py
62
build.py
|
|
@ -15,6 +15,8 @@ CONFIG_FILE = FIRMWARE_SOURCE_DIR / "platform" / "pico" / "controller_color_conf
|
|||
BUILD_DIR = SCRIPT_DIR / "build"
|
||||
AIO_BUILD_DIR = SCRIPT_DIR / "build-aio"
|
||||
FEASIBILITY_BUILD_DIR = SCRIPT_DIR / "build-feasibility"
|
||||
WAKE_CAPTURE_SOURCE_DIR = SCRIPT_DIR / "tools" / "switch2_wake_capture"
|
||||
WAKE_CAPTURE_BUILD_DIR = SCRIPT_DIR / "build-wake-capture"
|
||||
FIRMWARE_DIR = SCRIPT_DIR / "firmware"
|
||||
FIRMWARE_ELF_PATH = FIRMWARE_DIR / "switch-pico.elf"
|
||||
FIRMWARE_UF2_PATH = FIRMWARE_DIR / "switch-pico.uf2"
|
||||
|
|
@ -26,6 +28,12 @@ FEASIBILITY_FIRMWARE_ELF_PATH = (
|
|||
FEASIBILITY_FIRMWARE_UF2_PATH = (
|
||||
FIRMWARE_DIR / "switch-pico-adapter-feasibility.uf2"
|
||||
)
|
||||
WAKE_CAPTURE_FIRMWARE_ELF_PATH = (
|
||||
FIRMWARE_DIR / "switch-pico-wake-capture.elf"
|
||||
)
|
||||
WAKE_CAPTURE_FIRMWARE_UF2_PATH = (
|
||||
FIRMWARE_DIR / "switch-pico-wake-capture.uf2"
|
||||
)
|
||||
|
||||
ELF_PATH = Path(os.environ.get("ELF_PATH", BUILD_DIR / "switch-pico.elf")).expanduser()
|
||||
UF2_PATH = Path(os.environ.get("UF2_PATH", BUILD_DIR / "switch-pico.uf2")).expanduser()
|
||||
|
|
@ -38,7 +46,12 @@ MACROS = tuple(
|
|||
|
||||
CMAKE_CACHE_PATHS = tuple(
|
||||
build_dir / "CMakeCache.txt"
|
||||
for build_dir in (BUILD_DIR, AIO_BUILD_DIR, FEASIBILITY_BUILD_DIR)
|
||||
for build_dir in (
|
||||
BUILD_DIR,
|
||||
AIO_BUILD_DIR,
|
||||
FEASIBILITY_BUILD_DIR,
|
||||
WAKE_CAPTURE_BUILD_DIR,
|
||||
)
|
||||
)
|
||||
TOOLCHAIN_COMPILER = (
|
||||
"arm-none-eabi-gcc.exe" if os.name == "nt" else "arm-none-eabi-gcc"
|
||||
|
|
@ -279,6 +292,11 @@ def parse_args():
|
|||
action="store_true",
|
||||
help="Build and flash the Pico 2 W automatic Switch/XInput prototype.",
|
||||
)
|
||||
mode_group.add_argument(
|
||||
"--wake-capture",
|
||||
action="store_true",
|
||||
help="Build and flash the automatic Switch 2 wake capture firmware.",
|
||||
)
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument(
|
||||
"--random-grip-color",
|
||||
|
|
@ -290,7 +308,10 @@ def parse_args():
|
|||
metavar="RRGGBB",
|
||||
help="Set every emulated controller slot to the provided hex color.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
args = parser.parse_args()
|
||||
if args.wake_capture and (args.random_grip_color or args.grip_color):
|
||||
parser.error("wake capture firmware does not use grip-color options")
|
||||
return args
|
||||
|
||||
def random_hex_color():
|
||||
return "".join(f"{random.randrange(256):02X}" for _ in range(3))
|
||||
|
|
@ -409,6 +430,39 @@ def build(
|
|||
print(f"Copied UF2: {firmware_uf2_path}")
|
||||
|
||||
|
||||
def build_wake_capture():
|
||||
elf_path = WAKE_CAPTURE_BUILD_DIR / "switch2-wake-capture.elf"
|
||||
uf2_path = WAKE_CAPTURE_BUILD_DIR / "switch2-wake-capture.uf2"
|
||||
run_cmd(
|
||||
[
|
||||
"cmake",
|
||||
"-S",
|
||||
str(WAKE_CAPTURE_SOURCE_DIR),
|
||||
"-B",
|
||||
str(WAKE_CAPTURE_BUILD_DIR),
|
||||
"-DPICO_BOARD=pico2_w",
|
||||
]
|
||||
)
|
||||
run_cmd(["cmake", "--build", str(WAKE_CAPTURE_BUILD_DIR)])
|
||||
missing_artifacts = [
|
||||
path for path in (elf_path, uf2_path) if not path.is_file()
|
||||
]
|
||||
if missing_artifacts:
|
||||
missing = ", ".join(str(path) for path in missing_artifacts)
|
||||
sys.stderr.write(
|
||||
f"Error: Wake capture build did not produce: {missing}\n"
|
||||
)
|
||||
sys.exit(1)
|
||||
FIRMWARE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(elf_path, WAKE_CAPTURE_FIRMWARE_ELF_PATH)
|
||||
shutil.copy2(uf2_path, WAKE_CAPTURE_FIRMWARE_UF2_PATH)
|
||||
print(f"Built wake capture ELF: {elf_path}")
|
||||
print(f"Built wake capture UF2: {uf2_path}")
|
||||
print(f"Copied ELF: {WAKE_CAPTURE_FIRMWARE_ELF_PATH}")
|
||||
print(f"Copied UF2: {WAKE_CAPTURE_FIRMWARE_UF2_PATH}")
|
||||
return elf_path
|
||||
|
||||
|
||||
def flash(elf_path, allow_elf_override):
|
||||
picotool = resolve_picotool()
|
||||
if not elf_path.exists():
|
||||
|
|
@ -428,6 +482,10 @@ def main():
|
|||
except BuildEnvironmentError as exc:
|
||||
sys.stderr.write(f"Error: {exc}\n")
|
||||
sys.exit(1)
|
||||
if args.wake_capture:
|
||||
wake_capture_elf = build_wake_capture()
|
||||
flash(wake_capture_elf, allow_elf_override=False)
|
||||
return
|
||||
|
||||
color = None
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
BIN
firmware/switch-pico-wake-capture.elf
Executable file
BIN
firmware/switch-pico-wake-capture.elf
Executable file
Binary file not shown.
BIN
firmware/switch-pico-wake-capture.uf2
Normal file
BIN
firmware/switch-pico-wake-capture.uf2
Normal file
Binary file not shown.
|
|
@ -1,5 +1,6 @@
|
|||
#include "input/bluepad32_input_backend.h"
|
||||
#include "input/controller_hotkey_config.h"
|
||||
#include "input/switch2_wake.h"
|
||||
#include "configuration/configuration_service.h"
|
||||
#include "profile/profile_service.h"
|
||||
#include <limits.h>
|
||||
|
|
@ -500,6 +501,8 @@ ConnectionStatus compute_connection_status() {
|
|||
: ConnectionStatus::Scanning;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void publish_device_state(uint8_t slot, uni_hid_device_t* device,
|
||||
uint16_t pre_hotkey_button_mask,
|
||||
const ControllerState& state) {
|
||||
|
|
@ -632,6 +635,21 @@ constexpr uint16_t logical_button_bit(
|
|||
return static_cast<uint16_t>(
|
||||
1u << static_cast<uint8_t>(button));
|
||||
}
|
||||
bool wake_chord_rising_edge(uint8_t slot, uni_hid_device_t* device,
|
||||
uint16_t button_mask) {
|
||||
const uint16_t chord =
|
||||
logical_button_bit(ControllerProfileLogicalButton::kLeftShoulder) |
|
||||
logical_button_bit(ControllerProfileLogicalButton::kRightShoulder) |
|
||||
logical_button_bit(ControllerProfileLogicalButton::kSystem);
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
const BackendSlot& previous = g_slots[slot];
|
||||
const bool rising =
|
||||
previous.active && previous.device == device &&
|
||||
(button_mask & chord) == chord &&
|
||||
(previous.pre_hotkey_button_mask & chord) != chord;
|
||||
critical_section_exit(&g_state_lock);
|
||||
return rising;
|
||||
}
|
||||
|
||||
constexpr uint16_t logical_button_mask(
|
||||
uint32_t dpad, uint32_t buttons, uint32_t misc_buttons) {
|
||||
|
|
@ -1462,6 +1480,7 @@ void platform_on_init_complete() {
|
|||
g_identity_event_callback.callback = handle_btstack_event;
|
||||
sm_add_event_handler(&g_identity_event_callback);
|
||||
hci_add_event_handler(&g_pairing_event_callback);
|
||||
switch2_wake_initialize();
|
||||
refresh_pairing_snapshot();
|
||||
// Keep Bluepad32 autoconnect active whenever at least one slot is free.
|
||||
btstack_run_loop_set_timer_handler(&g_rumble_timer, process_rumble_timer);
|
||||
|
|
@ -1624,6 +1643,11 @@ void platform_on_controller_data(uni_hid_device_t* device,
|
|||
uni_gamepad_t gamepad = controller->gamepad;
|
||||
const uint16_t pre_hotkey_button_mask =
|
||||
logical_button_mask(gamepad);
|
||||
if (wake_chord_rising_edge(
|
||||
static_cast<uint8_t>(slot_index), device,
|
||||
pre_hotkey_button_mask)) {
|
||||
switch2_wake_request();
|
||||
}
|
||||
const HotkeyDecision hotkeys = update_controller_hotkeys(
|
||||
static_cast<uint8_t>(slot_index), device);
|
||||
const uint16_t output_button_mask = pre_hotkey_button_mask;
|
||||
|
|
|
|||
316
src/firmware/input/switch2_wake.cpp
Normal file
316
src/firmware/input/switch2_wake.cpp
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
#include "input/switch2_wake.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <btstack.h>
|
||||
|
||||
#if !defined(SWITCH2_WAKE_CONFIGURED)
|
||||
#if __has_include("platform/pico/switch2_wake_config.h")
|
||||
#include "platform/pico/switch2_wake_config.h"
|
||||
#else
|
||||
#define SWITCH2_WAKE_CONFIGURED 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kRetryIntervalMs = 5;
|
||||
constexpr uint32_t kCommandTimeoutMs = 1000;
|
||||
constexpr uint32_t kBurstDurationMs = 2000;
|
||||
constexpr uint16_t kAdvertisingIntervalUnits = 0x0020; // 20 ms + BLE delay.
|
||||
constexpr uint8_t kAdvertisingTypeNonConnectable = 3;
|
||||
constexpr uint16_t kWritePublicAddressOpcode = 0xfc01;
|
||||
|
||||
#if SWITCH2_WAKE_CONFIGURED
|
||||
const uint8_t kWakeAddress[6] = SWITCH2_WAKE_SOURCE_ADDRESS_BYTES;
|
||||
uint8_t kWakeAdvertisement[31] = SWITCH2_WAKE_ADVERTISEMENT_DATA_BYTES;
|
||||
#endif
|
||||
|
||||
const hci_cmd_t kWritePublicAddress = {kWritePublicAddressOpcode, "B"};
|
||||
const bd_addr_t kUnusedPeerAddress{};
|
||||
|
||||
enum class Phase : uint8_t {
|
||||
kDisabled,
|
||||
kIdle,
|
||||
kSetWakeAddress,
|
||||
kSetParameters,
|
||||
kSetData,
|
||||
kEnableAdvertising,
|
||||
kAdvertising,
|
||||
kDisableAdvertising,
|
||||
kRestoreAddress,
|
||||
kFailed,
|
||||
};
|
||||
|
||||
Phase g_phase = Phase::kDisabled;
|
||||
bd_addr_t g_original_address{};
|
||||
btstack_packet_callback_registration_t g_event_registration{};
|
||||
btstack_timer_source_t g_timer{};
|
||||
uint16_t g_pending_opcode = 0;
|
||||
uint32_t g_command_deadline_ms = 0;
|
||||
uint32_t g_burst_deadline_ms = 0;
|
||||
uint32_t g_accepted_requests = 0;
|
||||
uint32_t g_completed_bursts = 0;
|
||||
uint32_t g_failures = 0;
|
||||
bool g_initialized = false;
|
||||
bool g_configured = false;
|
||||
bool g_timer_armed = false;
|
||||
bool g_address_changed = false;
|
||||
bool g_advertising = false;
|
||||
|
||||
bool deadline_reached(uint32_t now, uint32_t deadline) {
|
||||
return static_cast<int32_t>(now - deadline) >= 0;
|
||||
}
|
||||
|
||||
bool phase_needs_work() {
|
||||
return g_phase != Phase::kDisabled &&
|
||||
g_phase != Phase::kIdle &&
|
||||
g_phase != Phase::kFailed;
|
||||
}
|
||||
|
||||
void cancel_task() {
|
||||
if (g_timer_armed) {
|
||||
btstack_run_loop_remove_timer(&g_timer);
|
||||
g_timer_armed = false;
|
||||
}
|
||||
}
|
||||
|
||||
void schedule_task(uint32_t delay_ms) {
|
||||
cancel_task();
|
||||
btstack_run_loop_set_timer(&g_timer, delay_ms);
|
||||
btstack_run_loop_add_timer(&g_timer);
|
||||
g_timer_armed = true;
|
||||
}
|
||||
|
||||
void schedule_for_phase(uint32_t now_ms) {
|
||||
if (g_pending_opcode != 0) {
|
||||
schedule_task(
|
||||
deadline_reached(now_ms, g_command_deadline_ms)
|
||||
? 0
|
||||
: g_command_deadline_ms - now_ms);
|
||||
} else if (g_phase == Phase::kAdvertising) {
|
||||
schedule_task(
|
||||
deadline_reached(now_ms, g_burst_deadline_ms)
|
||||
? 0
|
||||
: g_burst_deadline_ms - now_ms);
|
||||
} else if (phase_needs_work()) {
|
||||
schedule_task(kRetryIntervalMs);
|
||||
} else {
|
||||
cancel_task();
|
||||
}
|
||||
}
|
||||
|
||||
#if SWITCH2_WAKE_CONFIGURED
|
||||
bool configured_packet_valid() {
|
||||
static const uint8_t prefix[] = {
|
||||
0x02, 0x01, 0x06, 0x1b, 0xff, 0x53, 0x05,
|
||||
0x01, 0x00, 0x03, 0x7e, 0x05,
|
||||
};
|
||||
static const uint8_t suffix[] = {
|
||||
0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
uint8_t address_any = 0;
|
||||
uint8_t target_any = 0;
|
||||
for (uint8_t value : kWakeAddress) {
|
||||
address_any |= value;
|
||||
}
|
||||
for (size_t index = 17; index < 23; ++index) {
|
||||
target_any |= kWakeAdvertisement[index];
|
||||
}
|
||||
return address_any != 0 && target_any != 0 &&
|
||||
memcmp(kWakeAdvertisement, prefix, sizeof(prefix)) == 0 &&
|
||||
kWakeAdvertisement[14] == 0x00 &&
|
||||
kWakeAdvertisement[15] == 0x01 &&
|
||||
kWakeAdvertisement[16] == 0x81 &&
|
||||
memcmp(&kWakeAdvertisement[23], suffix, sizeof(suffix)) == 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
void recover_from_failure() {
|
||||
++g_failures;
|
||||
g_pending_opcode = 0;
|
||||
if (g_advertising) {
|
||||
g_phase = Phase::kDisableAdvertising;
|
||||
} else if (g_address_changed) {
|
||||
g_phase = Phase::kRestoreAddress;
|
||||
} else {
|
||||
g_phase = Phase::kIdle;
|
||||
}
|
||||
}
|
||||
|
||||
void begin_command(uint16_t opcode, uint32_t now_ms) {
|
||||
g_pending_opcode = opcode;
|
||||
g_command_deadline_ms = now_ms + kCommandTimeoutMs;
|
||||
}
|
||||
|
||||
void submit_phase_command(uint32_t now_ms) {
|
||||
if (g_pending_opcode != 0 || !hci_can_send_command_packet_now()) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t result = ERROR_CODE_SUCCESS;
|
||||
switch (g_phase) {
|
||||
case Phase::kSetWakeAddress:
|
||||
begin_command(kWritePublicAddress.opcode, now_ms);
|
||||
#if SWITCH2_WAKE_CONFIGURED
|
||||
result = hci_send_cmd(&kWritePublicAddress, kWakeAddress);
|
||||
#endif
|
||||
break;
|
||||
case Phase::kSetParameters:
|
||||
begin_command(hci_le_set_advertising_parameters.opcode, now_ms);
|
||||
result = hci_send_cmd(
|
||||
&hci_le_set_advertising_parameters,
|
||||
kAdvertisingIntervalUnits, kAdvertisingIntervalUnits,
|
||||
kAdvertisingTypeNonConnectable,
|
||||
BD_ADDR_TYPE_LE_PUBLIC, BD_ADDR_TYPE_LE_PUBLIC,
|
||||
kUnusedPeerAddress, 7, 0);
|
||||
break;
|
||||
case Phase::kSetData:
|
||||
begin_command(hci_le_set_advertising_data.opcode, now_ms);
|
||||
#if SWITCH2_WAKE_CONFIGURED
|
||||
result = hci_send_cmd(
|
||||
&hci_le_set_advertising_data,
|
||||
static_cast<uint8_t>(sizeof(kWakeAdvertisement)),
|
||||
kWakeAdvertisement);
|
||||
#endif
|
||||
break;
|
||||
case Phase::kEnableAdvertising:
|
||||
case Phase::kDisableAdvertising:
|
||||
begin_command(hci_le_set_advertise_enable.opcode, now_ms);
|
||||
result = hci_send_cmd(
|
||||
&hci_le_set_advertise_enable,
|
||||
g_phase == Phase::kEnableAdvertising ? 1 : 0);
|
||||
break;
|
||||
case Phase::kRestoreAddress:
|
||||
begin_command(kWritePublicAddress.opcode, now_ms);
|
||||
result = hci_send_cmd(&kWritePublicAddress, g_original_address);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (result != ERROR_CODE_SUCCESS) {
|
||||
recover_from_failure();
|
||||
}
|
||||
}
|
||||
|
||||
void handle_command_complete(uint8_t* packet, uint16_t size) {
|
||||
if (size < 6 || g_pending_opcode == 0) {
|
||||
return;
|
||||
}
|
||||
const uint16_t opcode =
|
||||
hci_event_command_complete_get_command_opcode(packet);
|
||||
if (opcode != g_pending_opcode) {
|
||||
return;
|
||||
}
|
||||
const uint8_t status =
|
||||
hci_event_command_complete_get_return_parameters(packet)[0];
|
||||
g_pending_opcode = 0;
|
||||
if (status != ERROR_CODE_SUCCESS) {
|
||||
recover_from_failure();
|
||||
schedule_for_phase(btstack_run_loop_get_time_ms());
|
||||
return;
|
||||
}
|
||||
|
||||
switch (g_phase) {
|
||||
case Phase::kSetWakeAddress:
|
||||
g_address_changed = true;
|
||||
g_phase = Phase::kSetParameters;
|
||||
break;
|
||||
case Phase::kSetParameters:
|
||||
g_phase = Phase::kSetData;
|
||||
break;
|
||||
case Phase::kSetData:
|
||||
g_phase = Phase::kEnableAdvertising;
|
||||
break;
|
||||
case Phase::kEnableAdvertising:
|
||||
g_advertising = true;
|
||||
g_burst_deadline_ms =
|
||||
btstack_run_loop_get_time_ms() + kBurstDurationMs;
|
||||
g_phase = Phase::kAdvertising;
|
||||
break;
|
||||
case Phase::kDisableAdvertising:
|
||||
g_advertising = false;
|
||||
g_phase = Phase::kRestoreAddress;
|
||||
break;
|
||||
case Phase::kRestoreAddress:
|
||||
g_address_changed = false;
|
||||
++g_completed_bursts;
|
||||
g_phase = Phase::kIdle;
|
||||
break;
|
||||
default:
|
||||
recover_from_failure();
|
||||
break;
|
||||
}
|
||||
schedule_for_phase(btstack_run_loop_get_time_ms());
|
||||
}
|
||||
|
||||
void handle_hci_event(uint8_t packet_type, uint16_t,
|
||||
uint8_t* packet, uint16_t size) {
|
||||
if (packet_type == HCI_EVENT_PACKET && size >= 2 &&
|
||||
hci_event_packet_get_type(packet) == HCI_EVENT_COMMAND_COMPLETE) {
|
||||
handle_command_complete(packet, size);
|
||||
}
|
||||
}
|
||||
|
||||
void task(btstack_timer_source_t*) {
|
||||
g_timer_armed = false;
|
||||
const uint32_t now_ms = btstack_run_loop_get_time_ms();
|
||||
if (g_pending_opcode != 0 &&
|
||||
deadline_reached(now_ms, g_command_deadline_ms)) {
|
||||
recover_from_failure();
|
||||
}
|
||||
if (g_phase == Phase::kAdvertising &&
|
||||
deadline_reached(now_ms, g_burst_deadline_ms)) {
|
||||
g_phase = Phase::kDisableAdvertising;
|
||||
}
|
||||
submit_phase_command(now_ms);
|
||||
schedule_for_phase(now_ms);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void switch2_wake_initialize() {
|
||||
if (g_initialized) {
|
||||
return;
|
||||
}
|
||||
g_initialized = true;
|
||||
#if SWITCH2_WAKE_CONFIGURED
|
||||
if (!configured_packet_valid()) {
|
||||
g_phase = Phase::kFailed;
|
||||
++g_failures;
|
||||
return;
|
||||
}
|
||||
g_configured = true;
|
||||
gap_local_bd_addr(g_original_address);
|
||||
g_event_registration.callback = handle_hci_event;
|
||||
hci_add_event_handler(&g_event_registration);
|
||||
btstack_run_loop_set_timer_handler(&g_timer, task);
|
||||
g_phase = Phase::kIdle;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
bool switch2_wake_request() {
|
||||
if (g_phase != Phase::kIdle) {
|
||||
return false;
|
||||
}
|
||||
++g_accepted_requests;
|
||||
g_phase = Phase::kSetWakeAddress;
|
||||
schedule_task(0);
|
||||
return true;
|
||||
}
|
||||
|
||||
void switch2_wake_diagnostics(Switch2WakeDiagnostics* out) {
|
||||
if (out == nullptr) {
|
||||
return;
|
||||
}
|
||||
*out = {
|
||||
g_configured,
|
||||
g_phase != Phase::kDisabled && g_phase != Phase::kIdle &&
|
||||
g_phase != Phase::kFailed,
|
||||
g_accepted_requests,
|
||||
g_completed_bursts,
|
||||
g_failures,
|
||||
};
|
||||
}
|
||||
20
src/firmware/input/switch2_wake.h
Normal file
20
src/firmware/input/switch2_wake.h
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
struct Switch2WakeDiagnostics {
|
||||
bool configured;
|
||||
bool busy;
|
||||
uint32_t accepted_requests;
|
||||
uint32_t completed_bursts;
|
||||
uint32_t failures;
|
||||
};
|
||||
|
||||
// Installs BTstack callbacks and remembers the Pico's normal public identity.
|
||||
void switch2_wake_initialize();
|
||||
|
||||
|
||||
// Starts one wake burst when configured and idle. Calls while busy coalesce.
|
||||
bool switch2_wake_request();
|
||||
|
||||
void switch2_wake_diagnostics(Switch2WakeDiagnostics* out);
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
#include <uni.h>
|
||||
#include "platform/pico/controller_color_config.h"
|
||||
#include "input/switch2_wake.h"
|
||||
|
||||
namespace {
|
||||
|
||||
|
|
@ -44,6 +45,8 @@ bool expect_configuration_timer_prearmed = false;
|
|||
uint32_t expected_configuration_timer_add_count = 0;
|
||||
int cyw43_init_calls = 0;
|
||||
int uni_init_calls = 0;
|
||||
int switch2_wake_initializations = 0;
|
||||
int switch2_wake_requests = 0;
|
||||
int device_disconnect_calls = 0;
|
||||
uni_hid_device_t* last_disconnected_device = nullptr;
|
||||
uni_hid_device_t* lookup_devices[8]{};
|
||||
|
|
@ -442,6 +445,19 @@ uint32_t btstack_run_loop_get_time_ms() {
|
|||
}
|
||||
|
||||
|
||||
void switch2_wake_initialize() {
|
||||
++switch2_wake_initializations;
|
||||
}
|
||||
|
||||
|
||||
bool switch2_wake_request() {
|
||||
++switch2_wake_requests;
|
||||
return true;
|
||||
}
|
||||
|
||||
void switch2_wake_diagnostics(Switch2WakeDiagnostics*) {
|
||||
}
|
||||
|
||||
#include "core/controller_identity.cpp"
|
||||
#include "input/bluepad32_input_backend.cpp"
|
||||
|
||||
|
|
@ -548,7 +564,8 @@ void start_backend() {
|
|||
kClassicLinkSupervisionTimeout &&
|
||||
!bondable && accepted_stk_methods == 0 &&
|
||||
!ssp_auto_accept && pairing_event_handler != nullptr &&
|
||||
identity_event_handler != nullptr,
|
||||
identity_event_handler != nullptr &&
|
||||
switch2_wake_initializations == 1,
|
||||
"initialization must register Classic and BLE identity policy");
|
||||
}
|
||||
void start_pairing_backend() {
|
||||
|
|
@ -2373,6 +2390,51 @@ void test_flash_core_start_contract() {
|
|||
"backend start must remain idempotent");
|
||||
}
|
||||
|
||||
|
||||
|
||||
void test_system_button_wake_trigger() {
|
||||
start_pairing_backend();
|
||||
uni_hid_device_t controller = device(0);
|
||||
require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS,
|
||||
"wake trigger controller did not become ready");
|
||||
|
||||
uni_controller_t input{};
|
||||
input.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
platform_on_controller_data(&controller, &input);
|
||||
require(switch2_wake_requests == 0,
|
||||
"neutral input requested a wake burst");
|
||||
|
||||
input.gamepad.misc_buttons = MISC_BUTTON_SYSTEM;
|
||||
platform_on_controller_data(&controller, &input);
|
||||
require(switch2_wake_requests == 0,
|
||||
"plain system button requested a wake burst");
|
||||
|
||||
input.gamepad.buttons = BUTTON_SHOULDER_L | BUTTON_SHOULDER_R;
|
||||
platform_on_controller_data(&controller, &input);
|
||||
platform_on_controller_data(&controller, &input);
|
||||
require(switch2_wake_requests == 1,
|
||||
"held L+R+System chord did not produce exactly one wake request");
|
||||
|
||||
input.gamepad.buttons = 0;
|
||||
input.gamepad.misc_buttons = 0;
|
||||
platform_on_controller_data(&controller, &input);
|
||||
input.gamepad.buttons = BUTTON_SHOULDER_L | BUTTON_SHOULDER_R;
|
||||
platform_on_controller_data(&controller, &input);
|
||||
require(switch2_wake_requests == 1,
|
||||
"L+R without System requested wake");
|
||||
input.gamepad.misc_buttons = MISC_BUTTON_SYSTEM;
|
||||
platform_on_controller_data(&controller, &input);
|
||||
require(switch2_wake_requests == 2,
|
||||
"second L+R+System chord edge did not request wake");
|
||||
|
||||
input.gamepad.buttons = 0;
|
||||
input.gamepad.misc_buttons = MISC_BUTTON_CAPTURE;
|
||||
platform_on_controller_data(&controller, &input);
|
||||
require(switch2_wake_requests == 2,
|
||||
"non-system misc button requested wake");
|
||||
}
|
||||
|
||||
|
||||
void test_flash_core_init_fatal() {
|
||||
bluepad32_input_backend_init();
|
||||
flash_core_init_result = false;
|
||||
|
|
@ -2424,6 +2486,8 @@ int main(int argc, char** argv) {
|
|||
test_configuration_timer_rearms_before_storage_work();
|
||||
} else if (scenario == "flash-core-start") {
|
||||
test_flash_core_start_contract();
|
||||
} else if (scenario == "system-wake") {
|
||||
test_system_button_wake_trigger();
|
||||
} else if (scenario == "flash-core-failure") {
|
||||
test_flash_core_init_fatal();
|
||||
} else {
|
||||
|
|
|
|||
57
tests/switch2_wake_native_stubs/btstack.h
Normal file
57
tests/switch2_wake_native_stubs/btstack.h
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
using bd_addr_t = uint8_t[6];
|
||||
using btstack_packet_handler_t = void (*)(uint8_t, uint16_t, uint8_t*, uint16_t);
|
||||
struct btstack_packet_callback_registration_t {
|
||||
void* item;
|
||||
btstack_packet_handler_t callback;
|
||||
};
|
||||
struct btstack_timer_source_t {
|
||||
void (*handler)(btstack_timer_source_t*);
|
||||
uint32_t timeout_ms;
|
||||
uint32_t add_count;
|
||||
};
|
||||
struct hci_cmd_t {
|
||||
uint16_t opcode;
|
||||
const char* format;
|
||||
};
|
||||
|
||||
enum {
|
||||
ERROR_CODE_SUCCESS = 0,
|
||||
HCI_EVENT_PACKET = 4,
|
||||
HCI_EVENT_COMMAND_COMPLETE = 0x0e,
|
||||
BD_ADDR_TYPE_LE_PUBLIC = 0,
|
||||
};
|
||||
|
||||
extern const hci_cmd_t hci_read_bd_addr;
|
||||
extern const hci_cmd_t hci_le_set_advertising_parameters;
|
||||
extern const hci_cmd_t hci_le_set_advertising_data;
|
||||
extern const hci_cmd_t hci_le_set_advertise_enable;
|
||||
|
||||
bool hci_can_send_command_packet_now();
|
||||
uint8_t hci_send_cmd(const hci_cmd_t* command, ...);
|
||||
void hci_add_event_handler(btstack_packet_callback_registration_t* registration);
|
||||
void gap_local_bd_addr(bd_addr_t output);
|
||||
uint32_t btstack_run_loop_get_time_ms();
|
||||
void btstack_run_loop_set_timer_handler(
|
||||
btstack_timer_source_t* timer,
|
||||
void (*handler)(btstack_timer_source_t*));
|
||||
void btstack_run_loop_set_timer(btstack_timer_source_t* timer, uint32_t timeout_ms);
|
||||
void btstack_run_loop_add_timer(btstack_timer_source_t* timer);
|
||||
int btstack_run_loop_remove_timer(btstack_timer_source_t* timer);
|
||||
|
||||
inline uint8_t hci_event_packet_get_type(const uint8_t* event) {
|
||||
return event[0];
|
||||
}
|
||||
inline uint16_t hci_event_command_complete_get_command_opcode(
|
||||
const uint8_t* event) {
|
||||
return static_cast<uint16_t>(event[3]) |
|
||||
static_cast<uint16_t>(event[4] << 8);
|
||||
}
|
||||
inline const uint8_t* hci_event_command_complete_get_return_parameters(
|
||||
const uint8_t* event) {
|
||||
return &event[5];
|
||||
}
|
||||
249
tests/switch2_wake_test.cpp
Normal file
249
tests/switch2_wake_test.cpp
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
#include <cstdarg>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
#include <btstack.h>
|
||||
|
||||
namespace {
|
||||
|
||||
struct SubmittedCommand {
|
||||
uint16_t opcode;
|
||||
uint8_t address[6];
|
||||
uint16_t interval_min;
|
||||
uint16_t interval_max;
|
||||
uint8_t advertising_type;
|
||||
uint8_t own_address_type;
|
||||
uint8_t channel_map;
|
||||
uint8_t filter_policy;
|
||||
uint8_t data[31];
|
||||
uint8_t data_length;
|
||||
uint8_t enabled;
|
||||
};
|
||||
|
||||
uint32_t now_ms;
|
||||
bd_addr_t original_address = {0xA8, 0x59, 0x5F, 0x9E, 0x37, 0x65};
|
||||
btstack_packet_handler_t event_handler;
|
||||
btstack_timer_source_t* installed_timer;
|
||||
SubmittedCommand submitted[16]{};
|
||||
size_t submitted_count;
|
||||
bool command_available = true;
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const hci_cmd_t hci_read_bd_addr = {0x1009, "read-address"};
|
||||
const hci_cmd_t hci_le_set_advertising_parameters = {0x2006, "params"};
|
||||
const hci_cmd_t hci_le_set_advertising_data = {0x2008, "data"};
|
||||
const hci_cmd_t hci_le_set_advertise_enable = {0x200a, "enable"};
|
||||
|
||||
bool hci_can_send_command_packet_now() {
|
||||
return command_available;
|
||||
}
|
||||
|
||||
uint8_t hci_send_cmd(const hci_cmd_t* command, ...) {
|
||||
require(command_available, "submitted without an HCI command credit");
|
||||
require(submitted_count < 16, "command capture overflow");
|
||||
command_available = false;
|
||||
SubmittedCommand& output = submitted[submitted_count++];
|
||||
output.opcode = command->opcode;
|
||||
va_list arguments;
|
||||
va_start(arguments, command);
|
||||
if (command->opcode == 0xfc01) {
|
||||
const uint8_t* address = va_arg(arguments, const uint8_t*);
|
||||
memcpy(output.address, address, sizeof(output.address));
|
||||
} else if (command->opcode == 0x2006) {
|
||||
output.interval_min = static_cast<uint16_t>(va_arg(arguments, int));
|
||||
output.interval_max = static_cast<uint16_t>(va_arg(arguments, int));
|
||||
output.advertising_type = static_cast<uint8_t>(va_arg(arguments, int));
|
||||
output.own_address_type = static_cast<uint8_t>(va_arg(arguments, int));
|
||||
(void)va_arg(arguments, int);
|
||||
(void)va_arg(arguments, const uint8_t*);
|
||||
output.channel_map = static_cast<uint8_t>(va_arg(arguments, int));
|
||||
output.filter_policy = static_cast<uint8_t>(va_arg(arguments, int));
|
||||
} else if (command->opcode == 0x2008) {
|
||||
output.data_length = static_cast<uint8_t>(va_arg(arguments, int));
|
||||
const uint8_t* data = va_arg(arguments, const uint8_t*);
|
||||
memcpy(output.data, data, output.data_length);
|
||||
} else if (command->opcode == 0x200a) {
|
||||
output.enabled = static_cast<uint8_t>(va_arg(arguments, int));
|
||||
}
|
||||
va_end(arguments);
|
||||
return ERROR_CODE_SUCCESS;
|
||||
}
|
||||
|
||||
void hci_add_event_handler(
|
||||
btstack_packet_callback_registration_t* registration) {
|
||||
event_handler = registration->callback;
|
||||
}
|
||||
|
||||
void gap_local_bd_addr(bd_addr_t output) {
|
||||
memcpy(output, original_address, sizeof(original_address));
|
||||
}
|
||||
|
||||
uint32_t btstack_run_loop_get_time_ms() {
|
||||
return now_ms;
|
||||
}
|
||||
|
||||
void btstack_run_loop_set_timer_handler(
|
||||
btstack_timer_source_t* timer,
|
||||
void (*handler)(btstack_timer_source_t*)) {
|
||||
timer->handler = handler;
|
||||
installed_timer = timer;
|
||||
}
|
||||
|
||||
void btstack_run_loop_set_timer(
|
||||
btstack_timer_source_t* timer, uint32_t timeout_ms) {
|
||||
timer->timeout_ms = timeout_ms;
|
||||
}
|
||||
|
||||
void btstack_run_loop_add_timer(btstack_timer_source_t* timer) {
|
||||
++timer->add_count;
|
||||
}
|
||||
int btstack_run_loop_remove_timer(btstack_timer_source_t*) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
#define SWITCH2_WAKE_CONFIGURED 1
|
||||
#define SWITCH2_WAKE_SOURCE_ADDRESS_BYTES \
|
||||
{0x98, 0xE2, 0x55, 0x07, 0xDF, 0x00}
|
||||
#define SWITCH2_WAKE_ADVERTISEMENT_DATA_BYTES \
|
||||
{0x02, 0x01, 0x06, 0x1B, 0xFF, 0x53, 0x05, 0x01, 0x00, 0x03, 0x7E, \
|
||||
0x05, 0x66, 0x20, 0x00, 0x01, 0x81, 0xBD, 0xD6, 0xF7, 0xEB, 0xF1, \
|
||||
0x48, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
#include "input/switch2_wake.cpp"
|
||||
|
||||
namespace {
|
||||
|
||||
void complete(uint16_t opcode, uint8_t status = 0,
|
||||
const uint8_t* address = nullptr) {
|
||||
uint8_t event[12] = {
|
||||
HCI_EVENT_COMMAND_COMPLETE,
|
||||
static_cast<uint8_t>(address == nullptr ? 4 : 10),
|
||||
1,
|
||||
static_cast<uint8_t>(opcode), static_cast<uint8_t>(opcode >> 8),
|
||||
status,
|
||||
};
|
||||
if (address != nullptr) {
|
||||
for (size_t index = 0; index < 6; ++index) {
|
||||
event[6 + index] = address[5 - index];
|
||||
}
|
||||
}
|
||||
command_available = true;
|
||||
event_handler(
|
||||
HCI_EVENT_PACKET, 0, event,
|
||||
static_cast<uint16_t>(address == nullptr ? 6 : 12));
|
||||
}
|
||||
|
||||
void run_task() {
|
||||
require(installed_timer != nullptr && installed_timer->handler != nullptr,
|
||||
"wake timer was not installed");
|
||||
installed_timer->handler(installed_timer);
|
||||
}
|
||||
|
||||
void require_opcode(size_t index, uint16_t opcode) {
|
||||
require(index < submitted_count && submitted[index].opcode == opcode,
|
||||
"unexpected HCI command sequence");
|
||||
}
|
||||
|
||||
void test_temporary_identity_wake_and_restore() {
|
||||
switch2_wake_initialize();
|
||||
Switch2WakeDiagnostics diagnostics{};
|
||||
switch2_wake_diagnostics(&diagnostics);
|
||||
require(diagnostics.configured && !diagnostics.busy && !g_timer_armed,
|
||||
"configured wake module did not initialize dormant");
|
||||
require(switch2_wake_request() && !switch2_wake_request(),
|
||||
"wake requests were not bounded while busy");
|
||||
|
||||
run_task();
|
||||
require_opcode(0, 0xfc01);
|
||||
const uint8_t wake_address[] = {0x98, 0xE2, 0x55, 0x07, 0xDF, 0x00};
|
||||
require(memcmp(submitted[0].address, wake_address, 6) == 0,
|
||||
"wake address did not reach the radio command");
|
||||
complete(0xfc01);
|
||||
|
||||
run_task();
|
||||
require_opcode(1, 0x2006);
|
||||
require(submitted[1].interval_min == 0x20 &&
|
||||
submitted[1].interval_max == 0x20 &&
|
||||
submitted[1].advertising_type == 3 &&
|
||||
submitted[1].own_address_type == 0 &&
|
||||
submitted[1].channel_map == 7 &&
|
||||
submitted[1].filter_policy == 0,
|
||||
"known-working advertising parameters changed");
|
||||
complete(0x2006);
|
||||
|
||||
run_task();
|
||||
require_opcode(2, 0x2008);
|
||||
require(submitted[2].data_length == 31 &&
|
||||
submitted[2].data[16] == 0x81,
|
||||
"captured wake payload was not submitted intact");
|
||||
complete(0x2008);
|
||||
|
||||
run_task();
|
||||
require_opcode(3, 0x200a);
|
||||
require(submitted[3].enabled == 1,
|
||||
"wake advertising was not enabled");
|
||||
complete(0x200a);
|
||||
require(g_timer_armed && installed_timer->timeout_ms == 2000,
|
||||
"wake burst did not schedule one exact stop deadline");
|
||||
now_ms = 1999;
|
||||
run_task();
|
||||
require(submitted_count == 4,
|
||||
"wake burst stopped before two seconds");
|
||||
now_ms = 2000;
|
||||
run_task();
|
||||
require_opcode(4, 0x200a);
|
||||
require(submitted[4].enabled == 0,
|
||||
"wake advertising was not disabled");
|
||||
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);
|
||||
require(!diagnostics.busy && diagnostics.accepted_requests == 1 &&
|
||||
diagnostics.completed_bursts == 1 &&
|
||||
diagnostics.failures == 0 && !g_timer_armed,
|
||||
"completed wake did not restore a dormant gameplay state");
|
||||
}
|
||||
|
||||
void test_failed_setup_restores_gameplay_identity() {
|
||||
require(switch2_wake_request(),
|
||||
"idle module rejected a second wake request");
|
||||
run_task();
|
||||
require_opcode(6, 0xfc01);
|
||||
complete(0xfc01);
|
||||
run_task();
|
||||
require_opcode(7, 0x2006);
|
||||
complete(0x2006, 0x12);
|
||||
run_task();
|
||||
require_opcode(8, 0xfc01);
|
||||
require(memcmp(submitted[8].address, original_address, 6) == 0,
|
||||
"wake setup failure did not restore the gameplay identity");
|
||||
complete(0xfc01);
|
||||
|
||||
Switch2WakeDiagnostics diagnostics{};
|
||||
switch2_wake_diagnostics(&diagnostics);
|
||||
require(!diagnostics.busy && diagnostics.failures == 1 &&
|
||||
!g_timer_armed,
|
||||
"wake failure did not recover to dormant gameplay");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
test_temporary_identity_wake_and_restore();
|
||||
test_failed_setup_restores_gameplay_identity();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -51,6 +51,7 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
|||
"clear-pairings",
|
||||
"configuration-timer",
|
||||
"flash-core-start",
|
||||
"system-wake",
|
||||
"flash-core-failure",
|
||||
):
|
||||
subprocess.run([str(executable), scenario], check=True, cwd=root)
|
||||
|
|
|
|||
70
tests/test_configure_switch2_wake.py
Normal file
70
tests/test_configure_switch2_wake.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SPEC = importlib.util.spec_from_file_location(
|
||||
"configure_switch2_wake",
|
||||
ROOT / "tools" / "configure_switch2_wake.py",
|
||||
)
|
||||
configure = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(configure)
|
||||
|
||||
RAW_HEX = "0201061BFF53050100037E056620000181BDD6F7EBF1480F00000000000000"
|
||||
RECORD = {
|
||||
"version": 1,
|
||||
"advertiser": "98:E2:55:07:DF:00",
|
||||
"address_type": 0,
|
||||
"event_type": 0,
|
||||
"rssi": -67,
|
||||
"pid": "2066",
|
||||
"console": "48:F1:EB:F7:D6:BD",
|
||||
"raw_hex": RAW_HEX,
|
||||
"esphome_payload_hex": RAW_HEX[14:],
|
||||
}
|
||||
|
||||
|
||||
def test_valid_capture_generates_build_configuration(tmp_path: Path) -> None:
|
||||
capture_log = tmp_path / "capture.log"
|
||||
capture_log.write_text(
|
||||
"scanner startup\nSWITCH2_WAKE_CAPTURE " + json.dumps(RECORD) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
output = tmp_path / "switch2_wake_config.h"
|
||||
|
||||
assert configure.main(
|
||||
["--input", str(capture_log), "--output", str(output)]
|
||||
) == 0
|
||||
generated = output.read_text(encoding="utf-8")
|
||||
assert "#define SWITCH2_WAKE_CONFIGURED 1" in generated
|
||||
assert "0x98, 0xE2, 0x55, 0x07, 0xDF, 0x00" in generated
|
||||
assert "0x02, 0x01, 0x06, 0x1B, 0xFF" in generated
|
||||
assert "Target Switch 2: 48:F1:EB:F7:D6:BD" in generated
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("field", "value", "message"),
|
||||
[
|
||||
("address_type", 1, "public advertiser"),
|
||||
("event_type", 3, "ADV_IND"),
|
||||
("console", "48:F1:EB:F7:D6:BE", "console address"),
|
||||
("pid", "2067", "PID"),
|
||||
],
|
||||
)
|
||||
def test_inconsistent_capture_is_rejected(field, value, message) -> None:
|
||||
record = dict(RECORD)
|
||||
record[field] = value
|
||||
with pytest.raises(configure.CaptureError, match=message):
|
||||
configure.validate_capture(record)
|
||||
|
||||
|
||||
def test_non_wake_packet_is_rejected() -> None:
|
||||
record = dict(RECORD)
|
||||
raw = bytearray.fromhex(RAW_HEX)
|
||||
raw[16] = 0
|
||||
record["raw_hex"] = raw.hex()
|
||||
with pytest.raises(configure.CaptureError, match="wake flag"):
|
||||
configure.validate_capture(record)
|
||||
28
tests/test_switch2_wake_native.py
Normal file
28
tests/test_switch2_wake_native.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_switch2_wake_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"
|
||||
executable = tmp_path / "switch2_wake_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-I{root / 'tests' / 'switch2_wake_native_stubs'}",
|
||||
f"-I{root / 'src' / 'firmware'}",
|
||||
str(root / "tests" / "switch2_wake_test.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
213
tools/configure_switch2_wake.py
Executable file
213
tools/configure_switch2_wake.py
Executable file
|
|
@ -0,0 +1,213 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Capture a Switch 2 wake record and generate the AIO firmware header."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_OUTPUT = (
|
||||
ROOT / "src" / "firmware" / "platform" / "pico" /
|
||||
"switch2_wake_config.h"
|
||||
)
|
||||
CAPTURE_PREFIX = "SWITCH2_WAKE_CAPTURE "
|
||||
ADDRESS_PATTERN = re.compile(r"(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")
|
||||
EXPECTED_PREFIX = bytes.fromhex("0201061BFF53050100037E05")
|
||||
EXPECTED_SUFFIX = bytes.fromhex("0F00000000000000")
|
||||
|
||||
|
||||
class CaptureError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def normalize_address(value, field):
|
||||
if not isinstance(value, str) or ADDRESS_PATTERN.fullmatch(value) is None:
|
||||
raise CaptureError(f"{field} must be a six-byte Bluetooth address.")
|
||||
normalized = value.upper()
|
||||
if normalized == "00:00:00:00:00:00":
|
||||
raise CaptureError(f"{field} must not be all zeroes.")
|
||||
return normalized
|
||||
|
||||
|
||||
def parse_capture_line(line):
|
||||
line = line.strip()
|
||||
if line.startswith(CAPTURE_PREFIX):
|
||||
line = line[len(CAPTURE_PREFIX):]
|
||||
if not line.startswith("{"):
|
||||
return None
|
||||
try:
|
||||
record = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
if not isinstance(record, dict) or "raw_hex" not in record:
|
||||
return None
|
||||
return validate_capture(record)
|
||||
|
||||
|
||||
def validate_capture(record):
|
||||
if record.get("version", 1) != 1:
|
||||
raise CaptureError("unsupported capture version")
|
||||
if record.get("address_type") != 0:
|
||||
raise CaptureError("capture must use a public advertiser address")
|
||||
if record.get("event_type") != 0:
|
||||
raise CaptureError("capture must be an ADV_IND wake advertisement")
|
||||
advertiser = normalize_address(record.get("advertiser"), "advertiser")
|
||||
console = normalize_address(record.get("console"), "console")
|
||||
raw_hex = record.get("raw_hex")
|
||||
if not isinstance(raw_hex, str) or re.fullmatch(r"[0-9A-Fa-f]{62}", raw_hex) is None:
|
||||
raise CaptureError("raw_hex must contain exactly 31 bytes")
|
||||
raw = bytes.fromhex(raw_hex)
|
||||
if raw[:12] != EXPECTED_PREFIX:
|
||||
raise CaptureError("capture is not a Nintendo Switch 2 advertisement")
|
||||
if raw[14:16] != b"\x00\x01":
|
||||
raise CaptureError("capture has an unexpected reconnect marker")
|
||||
if raw[16] != 0x81:
|
||||
raise CaptureError("capture does not contain the Switch 2 wake flag")
|
||||
if raw[23:] != EXPECTED_SUFFIX:
|
||||
raise CaptureError("capture has an unexpected reserved suffix")
|
||||
decoded_console = ":".join(f"{byte:02X}" for byte in reversed(raw[17:23]))
|
||||
if decoded_console != console:
|
||||
raise CaptureError("console address does not match raw wake data")
|
||||
pid = int.from_bytes(raw[12:14], "little")
|
||||
if record.get("pid", f"{pid:04X}").upper() != f"{pid:04X}":
|
||||
raise CaptureError("PID does not match raw wake data")
|
||||
payload = raw[7:].hex().upper()
|
||||
supplied_payload = record.get("esphome_payload_hex", payload)
|
||||
if supplied_payload.upper() != payload:
|
||||
raise CaptureError("manufacturer payload does not match raw wake data")
|
||||
return {
|
||||
"advertiser": advertiser,
|
||||
"console": console,
|
||||
"pid": pid,
|
||||
"raw": raw,
|
||||
}
|
||||
|
||||
|
||||
def byte_initializer(data):
|
||||
return ", ".join(f"0x{byte:02X}" for byte in data)
|
||||
|
||||
|
||||
def render_header(capture):
|
||||
source = bytes.fromhex(capture["advertiser"].replace(":", ""))
|
||||
return (
|
||||
"#pragma once\n\n"
|
||||
"// Generated by tools/configure_switch2_wake.py. Do not commit this file.\n"
|
||||
f"// Captured source controller: {capture['advertiser']}\n"
|
||||
f"// Target Switch 2: {capture['console']}; PID: 0x{capture['pid']:04X}\n"
|
||||
"#define SWITCH2_WAKE_CONFIGURED 1\n"
|
||||
"#define SWITCH2_WAKE_SOURCE_ADDRESS_BYTES \\\n"
|
||||
f" {{{byte_initializer(source)}}}\n"
|
||||
"#define SWITCH2_WAKE_ADVERTISEMENT_DATA_BYTES \\\n"
|
||||
f" {{{byte_initializer(capture['raw'])}}}\n"
|
||||
)
|
||||
|
||||
|
||||
def capture_from_stream(stream, timeout):
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
line = stream.readline()
|
||||
if not line:
|
||||
continue
|
||||
if isinstance(line, bytes):
|
||||
line = line.decode("utf-8", errors="replace")
|
||||
print(line.rstrip())
|
||||
capture = parse_capture_line(line)
|
||||
if capture is not None:
|
||||
return capture
|
||||
raise CaptureError("timed out waiting for a Switch 2 wake capture")
|
||||
|
||||
|
||||
def pico_serial_ports():
|
||||
try:
|
||||
from serial.tools import list_ports
|
||||
except ImportError as exc:
|
||||
raise CaptureError(
|
||||
"pyserial is required; install project dependencies first"
|
||||
) from exc
|
||||
return [port.device for port in list_ports.comports() if port.vid == 0x2E8A]
|
||||
|
||||
|
||||
def capture_from_pico(port, timeout):
|
||||
try:
|
||||
import serial
|
||||
except ImportError as exc:
|
||||
raise CaptureError(
|
||||
"pyserial is required; install project dependencies first"
|
||||
) from exc
|
||||
if port is None:
|
||||
ports = pico_serial_ports()
|
||||
if len(ports) != 1:
|
||||
detail = ", ".join(ports) if ports else "none"
|
||||
raise CaptureError(
|
||||
f"expected one Pico USB serial port, found {detail}; use --port"
|
||||
)
|
||||
port = ports[0]
|
||||
print(f"Waiting for automatic wake capture on {port}...")
|
||||
with serial.Serial(port, 115200, timeout=0.25, write_timeout=1) as device:
|
||||
deadline = time.monotonic() + timeout
|
||||
next_request = 0.0
|
||||
while time.monotonic() < deadline:
|
||||
now = time.monotonic()
|
||||
if now >= next_request:
|
||||
device.write(b"p\n")
|
||||
next_request = now + 1.0
|
||||
line = device.readline()
|
||||
if not line:
|
||||
continue
|
||||
text = line.decode("utf-8", errors="replace").strip()
|
||||
print(text)
|
||||
capture = parse_capture_line(text)
|
||||
if capture is not None:
|
||||
return capture
|
||||
raise CaptureError("timed out waiting for a Switch 2 wake capture")
|
||||
|
||||
|
||||
def write_header(path, capture):
|
||||
path = Path(path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = path.with_suffix(path.suffix + ".tmp")
|
||||
temporary.write_text(render_header(capture), encoding="utf-8")
|
||||
temporary.replace(path)
|
||||
|
||||
|
||||
def parse_args(argv=None):
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Read the one-shot Pico wake capture and configure the AIO build."
|
||||
)
|
||||
)
|
||||
parser.add_argument("--port", help="Pico USB serial port; auto-detected by default")
|
||||
parser.add_argument(
|
||||
"--input",
|
||||
type=Path,
|
||||
help="read a saved capture log instead of a live Pico",
|
||||
)
|
||||
parser.add_argument("--timeout", type=float, default=90.0)
|
||||
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
args = parse_args(argv)
|
||||
try:
|
||||
if args.timeout <= 0:
|
||||
raise CaptureError("timeout must be positive")
|
||||
if args.input is None:
|
||||
capture = capture_from_pico(args.port, args.timeout)
|
||||
else:
|
||||
with args.input.open("r", encoding="utf-8") as stream:
|
||||
capture = capture_from_stream(stream, args.timeout)
|
||||
write_header(args.output, capture)
|
||||
except (CaptureError, OSError) as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
print(f"Configured Switch 2 wake: {args.output}")
|
||||
print("Build and flash it with: python3 build.py --aio")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
19
tools/switch2_wake_capture/CMakeLists.txt
Normal file
19
tools/switch2_wake_capture/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
cmake_minimum_required(VERSION 3.13)
|
||||
set(PICO_BOARD pico2_w CACHE STRING "Target board")
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/../../pico_sdk_import.cmake)
|
||||
project(switch2_wake_capture C CXX ASM)
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
pico_sdk_init()
|
||||
add_executable(switch2-wake-capture main.c)
|
||||
target_include_directories(switch2-wake-capture PRIVATE ${CMAKE_CURRENT_LIST_DIR})
|
||||
target_link_libraries(switch2-wake-capture PRIVATE
|
||||
pico_stdlib
|
||||
pico_cyw43_arch_none
|
||||
pico_btstack_cyw43
|
||||
pico_btstack_ble
|
||||
)
|
||||
pico_enable_stdio_usb(switch2-wake-capture 1)
|
||||
pico_enable_stdio_uart(switch2-wake-capture 0)
|
||||
pico_set_program_name(switch2-wake-capture "Switch 2 wake capture")
|
||||
pico_set_program_version(switch2-wake-capture "1.0")
|
||||
pico_add_extra_outputs(switch2-wake-capture)
|
||||
30
tools/switch2_wake_capture/btstack_config.h
Normal file
30
tools/switch2_wake_capture/btstack_config.h
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#ifndef SWITCH2_WAKE_CAPTURE_BTSTACK_CONFIG_H
|
||||
#define SWITCH2_WAKE_CAPTURE_BTSTACK_CONFIG_H
|
||||
|
||||
#define ENABLE_LE_CENTRAL
|
||||
#define ENABLE_LE_PERIPHERAL
|
||||
#define ENABLE_LOG_ERROR
|
||||
#define ENABLE_PRINTF_HEXDUMP
|
||||
#define HAVE_EMBEDDED_TIME_MS
|
||||
#define HAVE_ASSERT
|
||||
#define HCI_OUTGOING_PRE_BUFFER_SIZE 4
|
||||
#define HCI_ACL_PAYLOAD_SIZE (1691 + 4)
|
||||
#define HCI_ACL_CHUNK_SIZE_ALIGNMENT 4
|
||||
#define MAX_NR_HCI_CONNECTIONS 1
|
||||
#define MAX_NR_GATT_CLIENTS 1
|
||||
#define MAX_NR_L2CAP_CHANNELS 2
|
||||
#define MAX_NR_L2CAP_SERVICES 2
|
||||
#define MAX_NR_SM_LOOKUP_ENTRIES 1
|
||||
#define MAX_NR_WHITELIST_ENTRIES 1
|
||||
#define MAX_NR_LE_DEVICE_DB_ENTRIES 16
|
||||
#define NVM_NUM_DEVICE_DB_ENTRIES 16
|
||||
#define MAX_ATT_DB_SIZE 128
|
||||
#define MAX_NR_CONTROLLER_ACL_BUFFERS 3
|
||||
#define ENABLE_HCI_CONTROLLER_TO_HOST_FLOW_CONTROL
|
||||
#define HCI_HOST_ACL_PACKET_LEN 1024
|
||||
#define HCI_HOST_ACL_PACKET_NUM 3
|
||||
#define HCI_HOST_SCO_PACKET_LEN 120
|
||||
#define HCI_HOST_SCO_PACKET_NUM 3
|
||||
#define HCI_RESET_RESEND_TIMEOUT_MS 1000
|
||||
|
||||
#endif
|
||||
152
tools/switch2_wake_capture/main.c
Normal file
152
tools/switch2_wake_capture/main.c
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "pico/cyw43_arch.h"
|
||||
#include "pico/stdio_usb.h"
|
||||
#include "pico/stdlib.h"
|
||||
#include "btstack.h"
|
||||
#include "nintendo_wake.h"
|
||||
|
||||
typedef struct {
|
||||
bd_addr_t advertiser;
|
||||
uint8_t address_type;
|
||||
uint8_t event_type;
|
||||
int8_t rssi;
|
||||
uint8_t data[31];
|
||||
} wake_capture_t;
|
||||
|
||||
static wake_capture_t capture;
|
||||
static bool captured;
|
||||
static bool scan_started;
|
||||
static bool usb_was_connected;
|
||||
static uint32_t last_print_ms;
|
||||
static btstack_packet_callback_registration_t registration;
|
||||
static btstack_timer_source_t output_timer;
|
||||
|
||||
static void to_hex(const uint8_t* bytes, size_t size, char* output) {
|
||||
static const char digits[] = "0123456789ABCDEF";
|
||||
for (size_t index = 0; index < size; ++index) {
|
||||
output[index * 2] = digits[bytes[index] >> 4];
|
||||
output[index * 2 + 1] = digits[bytes[index] & 0x0f];
|
||||
}
|
||||
output[size * 2] = 0;
|
||||
}
|
||||
|
||||
static void print_capture(void) {
|
||||
const uint8_t* manufacturer = &capture.data[5];
|
||||
char raw_hex[63];
|
||||
char payload_hex[49];
|
||||
to_hex(capture.data, sizeof(capture.data), raw_hex);
|
||||
// ESPHome-style payload excludes the two-byte Nintendo company ID.
|
||||
to_hex(&manufacturer[2], 24, payload_hex);
|
||||
printf("SWITCH2_WAKE_CAPTURE {\"version\":1,\"advertiser\":"
|
||||
"\"%02X:%02X:%02X:%02X:%02X:%02X\",\"address_type\":%u,"
|
||||
"\"event_type\":%u,\"rssi\":%d,\"pid\":\"%04X\","
|
||||
"\"console\":\"%02X:%02X:%02X:%02X:%02X:%02X\","
|
||||
"\"raw_hex\":\"%s\",\"esphome_payload_hex\":\"%s\"}\n",
|
||||
capture.advertiser[0], capture.advertiser[1],
|
||||
capture.advertiser[2], capture.advertiser[3],
|
||||
capture.advertiser[4], capture.advertiser[5],
|
||||
capture.address_type, capture.event_type, (int)capture.rssi,
|
||||
(unsigned)(manufacturer[7] | (manufacturer[8] << 8)),
|
||||
manufacturer[17], manufacturer[16], manufacturer[15],
|
||||
manufacturer[14], manufacturer[13], manufacturer[12],
|
||||
raw_hex, payload_hex);
|
||||
}
|
||||
|
||||
static void output_task(btstack_timer_source_t* timer) {
|
||||
const uint32_t now_ms = btstack_run_loop_get_time_ms();
|
||||
const bool usb_connected = stdio_usb_connected();
|
||||
if (usb_connected && !usb_was_connected) {
|
||||
puts("Switch 2 wake capture ready. Put the console to sleep, then wake it with a paired Joy-Con 2 HOME button.");
|
||||
puts("Scanning stops automatically after one valid public ADV_IND wake packet.");
|
||||
if (captured) {
|
||||
print_capture();
|
||||
last_print_ms = now_ms;
|
||||
}
|
||||
}
|
||||
if (usb_connected) {
|
||||
int key;
|
||||
while ((key = getchar_timeout_us(0)) != PICO_ERROR_TIMEOUT) {
|
||||
if ((key == 'p' || key == 'P') && captured) {
|
||||
print_capture();
|
||||
last_print_ms = now_ms;
|
||||
}
|
||||
}
|
||||
// Repeat for a generator attached after capture; scanning remains stopped.
|
||||
if (captured && now_ms - last_print_ms >= 1000) {
|
||||
print_capture();
|
||||
last_print_ms = now_ms;
|
||||
}
|
||||
}
|
||||
usb_was_connected = usb_connected;
|
||||
static bool blink;
|
||||
blink = !blink;
|
||||
cyw43_arch_gpio_put(
|
||||
CYW43_WL_GPIO_LED_PIN, captured || (scan_started && blink));
|
||||
btstack_run_loop_set_timer(timer, 250);
|
||||
btstack_run_loop_add_timer(timer);
|
||||
}
|
||||
|
||||
static void handle_packet(uint8_t packet_type, uint16_t,
|
||||
uint8_t* packet, uint16_t size) {
|
||||
if (packet_type != HCI_EVENT_PACKET || size < 2 || captured) {
|
||||
return;
|
||||
}
|
||||
const uint8_t event_type = hci_event_packet_get_type(packet);
|
||||
if (event_type == BTSTACK_EVENT_STATE && size >= 3 &&
|
||||
btstack_event_state_get_state(packet) == HCI_STATE_WORKING) {
|
||||
gap_set_scan_params(0, 96, 96, 0); // Passive, continuous, accept all.
|
||||
gap_set_scan_duplicate_filter(false);
|
||||
gap_start_scan();
|
||||
scan_started = true;
|
||||
return;
|
||||
}
|
||||
if (event_type != GAP_EVENT_ADVERTISING_REPORT || size < 12) {
|
||||
return;
|
||||
}
|
||||
const uint8_t length =
|
||||
gap_event_advertising_report_get_data_length(packet);
|
||||
if (length != sizeof(capture.data) || size < 12u + length ||
|
||||
gap_event_advertising_report_get_address_type(packet) !=
|
||||
BD_ADDR_TYPE_LE_PUBLIC ||
|
||||
gap_event_advertising_report_get_advertising_event_type(packet) != 0) {
|
||||
return;
|
||||
}
|
||||
const uint8_t* data =
|
||||
gap_event_advertising_report_get_data(packet);
|
||||
if (switch2_wake_manufacturer(data, length) == NULL) {
|
||||
return;
|
||||
}
|
||||
gap_event_advertising_report_get_address(packet, capture.advertiser);
|
||||
capture.address_type = BD_ADDR_TYPE_LE_PUBLIC;
|
||||
capture.event_type = 0;
|
||||
capture.rssi =
|
||||
(int8_t)gap_event_advertising_report_get_rssi(packet);
|
||||
memcpy(capture.data, data, sizeof(capture.data));
|
||||
captured = true;
|
||||
scan_started = false;
|
||||
gap_stop_scan();
|
||||
if (stdio_usb_connected()) {
|
||||
print_capture();
|
||||
last_print_ms = btstack_run_loop_get_time_ms();
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
stdio_init_all();
|
||||
if (cyw43_arch_init() != 0) {
|
||||
while (true) {
|
||||
puts("ERROR: CYW43 initialization failed");
|
||||
sleep_ms(1000);
|
||||
}
|
||||
}
|
||||
registration.callback = handle_packet;
|
||||
hci_add_event_handler(®istration);
|
||||
btstack_run_loop_set_timer_handler(&output_timer, output_task);
|
||||
btstack_run_loop_set_timer(&output_timer, 250);
|
||||
btstack_run_loop_add_timer(&output_timer);
|
||||
hci_power_control(HCI_POWER_ON);
|
||||
btstack_run_loop_execute();
|
||||
return 0;
|
||||
}
|
||||
34
tools/switch2_wake_capture/nintendo_wake.h
Normal file
34
tools/switch2_wake_capture/nintendo_wake.h
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#ifndef SWITCH2_WAKE_CAPTURE_NINTENDO_WAKE_H
|
||||
#define SWITCH2_WAKE_CAPTURE_NINTENDO_WAKE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
static inline const uint8_t* switch2_wake_manufacturer(
|
||||
const uint8_t* data, size_t size) {
|
||||
static const uint8_t prefix[] = {
|
||||
0x53, 0x05, 0x01, 0x00, 0x03, 0x7e, 0x05,
|
||||
};
|
||||
static const uint8_t suffix[] = {
|
||||
0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
if (size != 31 || memcmp(data, "\x02\x01\x06\x1b\xff", 5) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
const uint8_t* manufacturer = &data[5];
|
||||
if (memcmp(manufacturer, prefix, sizeof(prefix)) != 0 ||
|
||||
manufacturer[9] != 0x00 || manufacturer[10] != 0x01 ||
|
||||
manufacturer[11] != 0x81 ||
|
||||
memcmp(&manufacturer[18], suffix, sizeof(suffix)) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
uint8_t target_any = 0;
|
||||
for (size_t index = 12; index < 18; ++index) {
|
||||
target_any |= manufacturer[index];
|
||||
}
|
||||
return target_any == 0 ? NULL : manufacturer;
|
||||
}
|
||||
|
||||
#endif
|
||||
Loading…
Add table
Add a link
Reference in a new issue