Add selectable BLE-only and Classic-only radio modes
This commit is contained in:
parent
7c1c37a84e
commit
595518fe3d
13 changed files with 951 additions and 95 deletions
|
|
@ -90,6 +90,29 @@ if(NOT SWITCH_PICO_INPUT_BACKEND STREQUAL "UART"
|
|||
"Unknown SWITCH_PICO_INPUT_BACKEND='${SWITCH_PICO_INPUT_BACKEND}'. "
|
||||
"Expected UART or BLUEPAD32.")
|
||||
endif()
|
||||
set(SWITCH_PICO_BLUETOOTH_MODE "MIXED" CACHE STRING "Bluetooth radio mode: MIXED, BLE, or CLASSIC")
|
||||
set_property(CACHE SWITCH_PICO_BLUETOOTH_MODE PROPERTY STRINGS MIXED BLE CLASSIC)
|
||||
if(NOT SWITCH_PICO_BLUETOOTH_MODE MATCHES "^(MIXED|BLE|CLASSIC)$")
|
||||
message(FATAL_ERROR "SWITCH_PICO_BLUETOOTH_MODE must be MIXED, BLE, or CLASSIC")
|
||||
endif()
|
||||
if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
||||
if(SWITCH_PICO_BLUETOOTH_MODE STREQUAL "CLASSIC")
|
||||
set(SWITCH_PICO_ENABLE_BLE 0)
|
||||
else()
|
||||
set(SWITCH_PICO_ENABLE_BLE 1)
|
||||
endif()
|
||||
if(SWITCH_PICO_BLUETOOTH_MODE STREQUAL "BLE")
|
||||
set(SWITCH_PICO_ENABLE_CLASSIC 0)
|
||||
else()
|
||||
set(SWITCH_PICO_ENABLE_CLASSIC 1)
|
||||
endif()
|
||||
# Directory scope also covers Bluepad32 and its build-local SDK sources.
|
||||
add_compile_definitions(
|
||||
SWITCH_PICO_ENABLE_BLE=${SWITCH_PICO_ENABLE_BLE}
|
||||
SWITCH_PICO_ENABLE_CLASSIC=${SWITCH_PICO_ENABLE_CLASSIC})
|
||||
elseif(NOT SWITCH_PICO_BLUETOOTH_MODE STREQUAL "MIXED")
|
||||
message(FATAL_ERROR "Single-transport Bluetooth modes require the BLUEPAD32 backend")
|
||||
endif()
|
||||
set(PICO_BOARD pico CACHE STRING "Board type")
|
||||
if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32"
|
||||
AND NOT PICO_BOARD STREQUAL "pico2_w")
|
||||
|
|
|
|||
47
README.md
47
README.md
|
|
@ -66,6 +66,53 @@ 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.
|
||||
|
||||
### Bluetooth transport selection
|
||||
|
||||
`SWITCH_PICO_BLUETOOTH_MODE` selects the active radio transports at build time:
|
||||
|
||||
| CMake value | Active radio behavior |
|
||||
| --- | --- |
|
||||
| `MIXED` (default) | Bluetooth Classic and BLE |
|
||||
| `BLE` | BLE only; no Classic inquiry, page scanning, or controller admission |
|
||||
| `CLASSIC` | Classic only; no BLE scanning, controller admission, or wake advertising |
|
||||
|
||||
To **build without flashing**, use a separate directory for each mode:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build-aio-ble \
|
||||
-DPICO_BOARD=pico2_w \
|
||||
-DSWITCH_PICO_INPUT_BACKEND=BLUEPAD32 \
|
||||
-DSWITCH_PICO_BLUETOOTH_MODE=BLE
|
||||
cmake --build build-aio-ble
|
||||
```
|
||||
|
||||
Use `CLASSIC` or `MIXED` and a matching build directory for the other modes.
|
||||
Add `-DSWITCH_PICO_ADAPTER_FEASIBILITY=ON` for automatic Switch/XInput output.
|
||||
Invalid modes and single-transport selections with the UART backend are rejected.
|
||||
|
||||
The build helper also supports the selector; these commands **build and flash**:
|
||||
|
||||
```sh
|
||||
python3 build.py --aio --bluetooth-mode ble
|
||||
python3 build.py --aio --bluetooth-mode classic
|
||||
python3 build.py --adapter-feasibility --bluetooth-mode ble
|
||||
```
|
||||
|
||||
The helper defaults explicitly to `mixed`. Single-transport build directories and
|
||||
published `.elf`/`.uf2` names get `-ble` or `-classic` suffixes, so they do not
|
||||
overwrite mixed artifacts—for example, `build-aio-ble/` and
|
||||
`firmware/switch-pico-aio-ble.uf2`.
|
||||
|
||||
These flags select radio activity, not complete removal of the unused host stack:
|
||||
shared BTstack code and both pairing databases remain available. Switching modes
|
||||
does not erase bonds or profiles; pairing lists still include inactive-transport
|
||||
bonds and explicit clear-all still clears both stores. A configured stable public
|
||||
Bluetooth address is retained in every mode, including Classic-only, so changing
|
||||
modes does not silently change the host identity used by existing bonds.
|
||||
Classic-only disables Switch 2 wake advertising regardless of the saved wake
|
||||
configuration. Each build remains a standalone USB adapter; no inter-Pico link
|
||||
is introduced.
|
||||
|
||||
### Switch 2 wake from L + R + Home, PS, or Xbox
|
||||
|
||||
The AIO firmware can wake a sleeping Switch 2 when a connected controller's
|
||||
|
|
|
|||
17
bluepad32_config/bluetooth_transport_config.h
Normal file
17
bluepad32_config/bluetooth_transport_config.h
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#pragma once
|
||||
|
||||
// Radio policy, independent of the BTstack libraries linked for shared services.
|
||||
#ifndef SWITCH_PICO_ENABLE_BLE
|
||||
#define SWITCH_PICO_ENABLE_BLE 1
|
||||
#endif
|
||||
#ifndef SWITCH_PICO_ENABLE_CLASSIC
|
||||
#define SWITCH_PICO_ENABLE_CLASSIC 1
|
||||
#endif
|
||||
|
||||
#if (SWITCH_PICO_ENABLE_BLE != 0 && SWITCH_PICO_ENABLE_BLE != 1) || \
|
||||
(SWITCH_PICO_ENABLE_CLASSIC != 0 && SWITCH_PICO_ENABLE_CLASSIC != 1)
|
||||
#error "Bluetooth transport selections must be 0 or 1"
|
||||
#endif
|
||||
#if !SWITCH_PICO_ENABLE_BLE && !SWITCH_PICO_ENABLE_CLASSIC
|
||||
#error "At least one Bluetooth transport must be enabled"
|
||||
#endif
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
#include "bluetooth_transport_config.h"
|
||||
|
||||
// The AIO firmware exposes one fixed Bluepad32 device slot per USB interface.
|
||||
#define CONFIG_BLUEPAD32_MAX_DEVICES 4
|
||||
#define CONFIG_BLUEPAD32_MAX_ALLOWLIST 4
|
||||
#define CONFIG_BLUEPAD32_GAP_SECURITY 1
|
||||
#define CONFIG_BLUEPAD32_ENABLE_BLE_BY_DEFAULT 1
|
||||
#define CONFIG_BLUEPAD32_ENABLE_BLE_BY_DEFAULT SWITCH_PICO_ENABLE_BLE
|
||||
|
||||
#define CONFIG_BLUEPAD32_PLATFORM_CUSTOM
|
||||
#define CONFIG_TARGET_PICO_W
|
||||
|
|
|
|||
25
build.py
25
build.py
|
|
@ -44,6 +44,11 @@ CMAKE_CACHE_PATHS = tuple(
|
|||
AIO_BUILD_DIR,
|
||||
FEASIBILITY_BUILD_DIR,
|
||||
WAKE_CAPTURE_BUILD_DIR,
|
||||
*(
|
||||
base.with_name(f"{base.name}-{mode}")
|
||||
for base in (AIO_BUILD_DIR, FEASIBILITY_BUILD_DIR)
|
||||
for mode in ("ble", "classic")
|
||||
),
|
||||
)
|
||||
)
|
||||
TOOLCHAIN_COMPILER = "arm-none-eabi-gcc.exe" if os.name == "nt" else "arm-none-eabi-gcc"
|
||||
|
|
@ -287,6 +292,12 @@ def parse_args():
|
|||
action="store_true",
|
||||
help="Build and flash the automatic Switch 2 wake capture firmware.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--bluetooth-mode",
|
||||
choices=("mixed", "ble", "classic"),
|
||||
default="mixed",
|
||||
help="Select active Bluetooth transports for --aio or --adapter-feasibility.",
|
||||
)
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument(
|
||||
"--random-grip-color",
|
||||
|
|
@ -301,6 +312,8 @@ def 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")
|
||||
if args.bluetooth_mode != "mixed" and not (args.aio or args.adapter_feasibility):
|
||||
parser.error("--bluetooth-mode requires --aio or --adapter-feasibility")
|
||||
return args
|
||||
|
||||
|
||||
|
|
@ -385,6 +398,8 @@ def build(
|
|||
uf2_path,
|
||||
firmware_elf_path,
|
||||
firmware_uf2_path,
|
||||
*,
|
||||
bluetooth_mode="mixed",
|
||||
):
|
||||
if aio or adapter_feasibility:
|
||||
definitions = [
|
||||
|
|
@ -405,6 +420,7 @@ def build(
|
|||
native = "ON" if aio or adapter_feasibility else "OFF"
|
||||
definitions.extend(
|
||||
[
|
||||
f"-DSWITCH_PICO_BLUETOOTH_MODE={bluetooth_mode.upper()}",
|
||||
f"-DSWITCH_PICO_HAPTICS_EXPERIMENT={native}",
|
||||
f"-DSWITCH_PICO_HD_RUMBLE={native}",
|
||||
f"-DSWITCH_PICO_SYS_CLOCK_MHZ={300 if native == 'ON' else 150}",
|
||||
|
|
@ -532,6 +548,14 @@ def main():
|
|||
firmware_elf_path = FIRMWARE_ELF_PATH
|
||||
firmware_uf2_path = FIRMWARE_UF2_PATH
|
||||
|
||||
if args.bluetooth_mode != "mixed":
|
||||
suffix = f"-{args.bluetooth_mode}"
|
||||
build_dir = build_dir.with_name(build_dir.name + suffix)
|
||||
elf_path = build_dir / elf_path.name
|
||||
uf2_path = build_dir / uf2_path.name
|
||||
firmware_elf_path = firmware_elf_path.with_stem(firmware_elf_path.stem + suffix)
|
||||
firmware_uf2_path = firmware_uf2_path.with_stem(firmware_uf2_path.stem + suffix)
|
||||
|
||||
build(
|
||||
args.aio,
|
||||
args.adapter_feasibility,
|
||||
|
|
@ -540,6 +564,7 @@ def main():
|
|||
uf2_path,
|
||||
firmware_elf_path,
|
||||
firmware_uf2_path,
|
||||
bluetooth_mode=args.bluetooth_mode,
|
||||
)
|
||||
flash(
|
||||
elf_path,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,49 @@ diff --git a/src/components/bluepad32/bt/uni_bt.c b/src/components/bluepad32/bt/
|
|||
// globals
|
||||
bd_addr_t uni_local_bd_addr;
|
||||
|
||||
@@ -473,6 +479,10 @@
|
||||
@@ -100,17 +106,21 @@
|
||||
};
|
||||
|
||||
static void bluetooth_del_keys(void) {
|
||||
- if (IS_ENABLED(UNI_ENABLE_BREDR))
|
||||
- uni_bt_bredr_delete_bonded_keys();
|
||||
- if (IS_ENABLED(UNI_ENABLE_BLE))
|
||||
- uni_bt_le_delete_bonded_keys();
|
||||
+#ifdef ENABLE_CLASSIC
|
||||
+ uni_bt_bredr_delete_bonded_keys();
|
||||
+#endif
|
||||
+#ifdef ENABLE_BLE
|
||||
+ uni_bt_le_delete_bonded_keys();
|
||||
+#endif
|
||||
}
|
||||
|
||||
static void bluetooth_list_keys(void) {
|
||||
- if (IS_ENABLED(UNI_ENABLE_BREDR))
|
||||
- uni_bt_bredr_list_bonded_keys();
|
||||
- if (IS_ENABLED(UNI_ENABLE_BLE))
|
||||
- uni_bt_le_list_bonded_keys();
|
||||
+#ifdef ENABLE_CLASSIC
|
||||
+ uni_bt_bredr_list_bonded_keys();
|
||||
+#endif
|
||||
+#ifdef ENABLE_BLE
|
||||
+ uni_bt_le_list_bonded_keys();
|
||||
+#endif
|
||||
}
|
||||
|
||||
static void start_scan(void) {
|
||||
@@ -172,9 +182,9 @@
|
||||
// Device cannot be used after delete.
|
||||
d = NULL;
|
||||
|
||||
- if (IS_ENABLED(UNI_ENABLE_BLE) && type == GAP_CONNECTION_LE)
|
||||
+ if (IS_ENABLED(ENABLE_BLE) && type == GAP_CONNECTION_LE)
|
||||
uni_bt_le_on_hci_disconnection_complete(channel, packet, size);
|
||||
- else if (IS_ENABLED(UNI_ENABLE_BREDR) && type == GAP_CONNECTION_ACL)
|
||||
+ else if (IS_ENABLED(ENABLE_CLASSIC) && type == GAP_CONNECTION_ACL)
|
||||
uni_bt_bredr_on_hci_disconnection_complete(channel, packet, size);
|
||||
else
|
||||
loge("on_hci_disconnection_complete: Unknown GAP connection type: %d\n", type);
|
||||
@@ -473,6 +483,10 @@
|
||||
if (device == NULL) {
|
||||
loge("--->>> CANNOT FIND DEVICE");
|
||||
} else {
|
||||
|
|
@ -28,7 +70,43 @@ diff --git a/src/components/bluepad32/bt/uni_bt.c b/src/components/bluepad32/bt/
|
|||
diff --git a/src/components/bluepad32/bt/uni_bt_bredr.c b/src/components/bluepad32/bt/uni_bt_bredr.c
|
||||
--- a/src/components/bluepad32/bt/uni_bt_bredr.c
|
||||
+++ b/src/components/bluepad32/bt/uni_bt_bredr.c
|
||||
@@ -423,13 +423,14 @@
|
||||
@@ -28,7 +28,7 @@
|
||||
#define INQUIRY_REMOTE_NAME_TIMEOUT_MS 4500
|
||||
_Static_assert(INQUIRY_REMOTE_NAME_TIMEOUT_MS < HID_DEVICE_CONNECTION_TIMEOUT_MS, "Timeout too big");
|
||||
|
||||
-static bool bt_bredr_enabled = true;
|
||||
+static bool bt_bredr_enabled = IS_ENABLED(UNI_ENABLE_BREDR);
|
||||
|
||||
static void l2cap_create_control_connection(uni_hid_device_t* d) {
|
||||
uint8_t status;
|
||||
@@ -62,6 +62,8 @@
|
||||
}
|
||||
|
||||
void uni_bt_bredr_scan_start(void) {
|
||||
+ if (!bt_bredr_enabled)
|
||||
+ return;
|
||||
uint8_t status;
|
||||
|
||||
status = gap_inquiry_periodic_start(uni_bt_get_gap_inquiry_length(), uni_bt_get_gap_max_periodic_length(),
|
||||
@@ -147,6 +149,8 @@
|
||||
}
|
||||
|
||||
void uni_bt_bredr_setup(void) {
|
||||
+ if (!bt_bredr_enabled)
|
||||
+ return;
|
||||
int security_level = uni_bt_get_gap_security_level();
|
||||
gap_set_security_level(security_level);
|
||||
|
||||
@@ -183,7 +187,7 @@
|
||||
}
|
||||
|
||||
void uni_bt_bredr_set_enabled(bool enabled) {
|
||||
- bt_bredr_enabled = enabled;
|
||||
+ bt_bredr_enabled = enabled && IS_ENABLED(UNI_ENABLE_BREDR);
|
||||
}
|
||||
|
||||
bool uni_bt_bredr_is_enabled(void) {
|
||||
@@ -423,13 +427,14 @@
|
||||
status = l2cap_event_channel_opened_get_status(packet);
|
||||
if (status) {
|
||||
logi("L2CAP Connection failed: 0x%02x.\n", status);
|
||||
|
|
@ -191,19 +269,22 @@ diff --git a/src/components/bluepad32/bt/uni_bt_le.c b/src/components/bluepad32/
|
|||
}
|
||||
|
||||
void uni_bt_le_list_bonded_keys(void) {
|
||||
@@ -872,6 +912,11 @@
|
||||
gap_delete_bonding((bd_addr_type_t)entry_address_type, entry_address);
|
||||
}
|
||||
@@ -874,7 +914,14 @@
|
||||
logi(".\n");
|
||||
+}
|
||||
+
|
||||
}
|
||||
|
||||
+void uni_bt_le_set_background_scan(bool enabled) {
|
||||
+ gap_set_scan_parameters(0 /* type: passive */, enabled ? 160 : 48 /* interval */,
|
||||
+ enabled ? 16 : 48 /* window */);
|
||||
}
|
||||
|
||||
+}
|
||||
+
|
||||
void uni_bt_le_setup(void) {
|
||||
@@ -921,7 +966,7 @@
|
||||
+ if (!IS_ENABLED(UNI_ENABLE_BLE))
|
||||
+ return;
|
||||
// register for events from Security Manager
|
||||
sm_event_callback_registration.callback = &uni_sm_packet_handler;
|
||||
sm_add_event_handler(&sm_event_callback_registration);
|
||||
@@ -921,11 +968,11 @@
|
||||
// scan_parameters_service_client_init();
|
||||
device_information_service_client_init();
|
||||
|
||||
|
|
@ -212,6 +293,58 @@ diff --git a/src/components/bluepad32/bt/uni_bt_le.c b/src/components/bluepad32/
|
|||
}
|
||||
|
||||
void uni_bt_le_scan_start(void) {
|
||||
- if (!ble_enabled)
|
||||
+ if (!IS_ENABLED(UNI_ENABLE_BLE) || !ble_enabled)
|
||||
return;
|
||||
|
||||
gap_start_scan();
|
||||
@@ -950,6 +997,9 @@
|
||||
|
||||
void uni_bt_le_set_enabled(bool enabled) {
|
||||
// Called from different Task. Don't call BTstack functions.
|
||||
+ // A build-time radio exclusion must not rewrite the saved mixed-mode preference.
|
||||
+ if (!IS_ENABLED(UNI_ENABLE_BLE))
|
||||
+ return;
|
||||
uni_property_value_t val;
|
||||
|
||||
val.u8 = enabled;
|
||||
@@ -959,6 +1009,8 @@
|
||||
}
|
||||
|
||||
bool uni_bt_le_is_enabled() {
|
||||
+ if (!IS_ENABLED(UNI_ENABLE_BLE))
|
||||
+ return false;
|
||||
// Expensive call. Avoid calling it from this same file.
|
||||
// Called from "uni_bt_setup"
|
||||
uni_property_value_t val;
|
||||
diff --git a/src/components/bluepad32/bt/uni_bt_service.c b/src/components/bluepad32/bt/uni_bt_service.c
|
||||
--- a/src/components/bluepad32/bt/uni_bt_service.c
|
||||
+++ b/src/components/bluepad32/bt/uni_bt_service.c
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "bt/uni_bt_service.gatt.h"
|
||||
#include "controller/uni_gamepad.h"
|
||||
#include "uni_common.h"
|
||||
+#include "uni_config.h"
|
||||
#include "uni_log.h"
|
||||
#include "uni_system.h"
|
||||
#include "uni_version.h"
|
||||
@@ -433,6 +434,8 @@
|
||||
* Finally, it configures the advertisements.
|
||||
*/
|
||||
void uni_bt_service_init(void) {
|
||||
+ if (!IS_ENABLED(UNI_ENABLE_BLE))
|
||||
+ return;
|
||||
logi("Starting Bluepad32 BLE service UUID: 4627C4A4-AC00-46B9-B688-AFC5C1BF7F63\n");
|
||||
|
||||
// Setup ATT server.
|
||||
@@ -464,6 +467,7 @@
|
||||
}
|
||||
|
||||
void uni_bt_service_set_enabled(bool enabled) {
|
||||
+ enabled = enabled && IS_ENABLED(UNI_ENABLE_BLE);
|
||||
if (enabled == service_enabled)
|
||||
return;
|
||||
|
||||
diff --git a/src/components/bluepad32/controller/uni_gamepad.c b/src/components/bluepad32/controller/uni_gamepad.c
|
||||
--- a/src/components/bluepad32/controller/uni_gamepad.c
|
||||
+++ b/src/components/bluepad32/controller/uni_gamepad.c
|
||||
|
|
@ -322,6 +455,27 @@ diff --git a/src/components/bluepad32/include/uni.h b/src/components/bluepad32/i
|
|||
#include "parser/uni_hid_parser_xboxone.h"
|
||||
#include "platform/uni_platform.h"
|
||||
#include "uni_circular_buffer.h"
|
||||
diff --git a/src/components/bluepad32/include/uni_config.h b/src/components/bluepad32/include/uni_config.h
|
||||
--- a/src/components/bluepad32/include/uni_config.h
|
||||
+++ b/src/components/bluepad32/include/uni_config.h
|
||||
@@ -7,8 +7,15 @@
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
-#if defined(CONFIG_TARGET_POSIX) || defined(CONFIG_TARGET_PICO_W) || defined(CONFIG_IDF_TARGET_ESP32)
|
||||
-// Pico W, original ESP32 and Posix all support both BR/EDR and BLE
|
||||
+#if defined(CONFIG_TARGET_PICO_W)
|
||||
+#if SWITCH_PICO_ENABLE_CLASSIC
|
||||
+#define UNI_ENABLE_BREDR 1
|
||||
+#endif
|
||||
+#if SWITCH_PICO_ENABLE_BLE
|
||||
+#define UNI_ENABLE_BLE 1
|
||||
+#endif
|
||||
+#elif defined(CONFIG_TARGET_POSIX) || defined(CONFIG_IDF_TARGET_ESP32)
|
||||
+// Original ESP32 and Posix support both BR/EDR and BLE.
|
||||
#define UNI_ENABLE_BREDR 1
|
||||
#define UNI_ENABLE_BLE 1
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || \
|
||||
diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds4.c b/src/components/bluepad32/parser/uni_hid_parser_ds4.c
|
||||
--- a/src/components/bluepad32/parser/uni_hid_parser_ds4.c
|
||||
+++ b/src/components/bluepad32/parser/uni_hid_parser_ds4.c
|
||||
|
|
@ -2341,6 +2495,18 @@ diff --git a/src/components/bluepad32/uni_hid_device.c b/src/components/bluepad3
|
|||
// Disconnect child first
|
||||
if (d->child)
|
||||
uni_hid_device_disconnect(d->child);
|
||||
@@ -465,9 +471,9 @@
|
||||
// Cleanup
|
||||
if (!uni_hid_device_is_virtual_device(d)) {
|
||||
type = gap_get_connection_type(d->conn.handle);
|
||||
- if (IS_ENABLED(UNI_ENABLE_BLE) && type == GAP_CONNECTION_LE)
|
||||
+ if (IS_ENABLED(ENABLE_BLE) && type == GAP_CONNECTION_LE)
|
||||
uni_bt_le_disconnect(d);
|
||||
- else if (IS_ENABLED(UNI_ENABLE_BREDR) && type == GAP_CONNECTION_ACL)
|
||||
+ else if (IS_ENABLED(ENABLE_CLASSIC) && type == GAP_CONNECTION_ACL)
|
||||
uni_bt_bredr_disconnect(d);
|
||||
else
|
||||
loge("uni_hid_device_disconnect: Unknown GAP connection type: %d\n", type);
|
||||
@@ -490,6 +496,11 @@
|
||||
loge("uni_hid_device_delete: invalid hid device: NULL\n");
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "input/bluepad32_input_backend.h"
|
||||
#include "bluetooth_transport_config.h"
|
||||
#include "input/controller_hotkey_config.h"
|
||||
#include "input/switch2_wake.h"
|
||||
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
|
||||
|
|
@ -595,6 +596,9 @@ bool waiting_for_joycon_mate(int side = 0) {
|
|||
}
|
||||
|
||||
void stop_background_scan() {
|
||||
if (!SWITCH_PICO_ENABLE_BLE) {
|
||||
return;
|
||||
}
|
||||
if (g_background_scan_active) {
|
||||
// Direct LE scans do not update Bluepad32's aggregate scanning flag.
|
||||
uni_bt_le_scan_stop();
|
||||
|
|
@ -604,6 +608,9 @@ void stop_background_scan() {
|
|||
// Core 1 only. Reconcile every ready physical Switch 2 link to the fast interval,
|
||||
// independently of player grouping, controller count, or Classic connections.
|
||||
void apply_radio_connection_policy() {
|
||||
if (!SWITCH_PICO_ENABLE_BLE) {
|
||||
return;
|
||||
}
|
||||
uni_hid_device_t* ready[kSlotCount]{};
|
||||
for (const BackendSlot& slot : g_slots) {
|
||||
uni_hid_device_t* targets[] = {slot.device, slot.companion};
|
||||
|
|
@ -1586,19 +1593,19 @@ void handle_btstack_event(uint8_t packet_type, uint16_t channel,
|
|||
pairing_window_active_at(btstack_run_loop_get_time_ms());
|
||||
switch (hci_event_packet_get_type(packet)) {
|
||||
case SM_EVENT_IDENTITY_RESOLVING_STARTED:
|
||||
if (size >= 11) {
|
||||
if (SWITCH_PICO_ENABLE_BLE && size >= 11) {
|
||||
clear_ble_identity_for_handle(
|
||||
sm_event_identity_resolving_started_get_handle(packet));
|
||||
}
|
||||
break;
|
||||
case SM_EVENT_IDENTITY_RESOLVING_FAILED:
|
||||
if (size >= 11) {
|
||||
if (SWITCH_PICO_ENABLE_BLE && size >= 11) {
|
||||
clear_ble_identity_for_handle(
|
||||
sm_event_identity_resolving_failed_get_handle(packet));
|
||||
}
|
||||
break;
|
||||
case SM_EVENT_IDENTITY_RESOLVING_SUCCEEDED:
|
||||
if (size >= 20) {
|
||||
if (SWITCH_PICO_ENABLE_BLE && size >= 20) {
|
||||
connection_handle =
|
||||
sm_event_identity_resolving_succeeded_get_handle(packet);
|
||||
sm_event_identity_resolving_succeeded_get_address(
|
||||
|
|
@ -1613,7 +1620,7 @@ void handle_btstack_event(uint8_t packet_type, uint16_t channel,
|
|||
}
|
||||
break;
|
||||
case SM_EVENT_IDENTITY_CREATED:
|
||||
if (size >= 20) {
|
||||
if (SWITCH_PICO_ENABLE_BLE && size >= 20) {
|
||||
connection_handle =
|
||||
sm_event_identity_created_get_handle(packet);
|
||||
sm_event_identity_created_get_address(packet, address);
|
||||
|
|
@ -1628,7 +1635,7 @@ void handle_btstack_event(uint8_t packet_type, uint16_t channel,
|
|||
}
|
||||
break;
|
||||
case SM_EVENT_REENCRYPTION_STARTED:
|
||||
if (size >= 11) {
|
||||
if (SWITCH_PICO_ENABLE_BLE && size >= 11) {
|
||||
connection_handle =
|
||||
sm_event_reencryption_started_get_handle(packet);
|
||||
sm_event_reencryption_started_get_address(
|
||||
|
|
@ -1643,7 +1650,7 @@ void handle_btstack_event(uint8_t packet_type, uint16_t channel,
|
|||
}
|
||||
break;
|
||||
case SM_EVENT_REENCRYPTION_COMPLETE:
|
||||
if (size >= 12) {
|
||||
if (SWITCH_PICO_ENABLE_BLE && size >= 12) {
|
||||
connection_handle =
|
||||
sm_event_reencryption_complete_get_handle(packet);
|
||||
if (sm_event_reencryption_complete_get_status(packet) ==
|
||||
|
|
@ -1663,7 +1670,7 @@ void handle_btstack_event(uint8_t packet_type, uint16_t channel,
|
|||
}
|
||||
break;
|
||||
case HCI_EVENT_USER_CONFIRMATION_REQUEST:
|
||||
if (size < 8) {
|
||||
if (!SWITCH_PICO_ENABLE_CLASSIC || size < 8) {
|
||||
break;
|
||||
}
|
||||
hci_event_user_confirmation_request_get_bd_addr(packet, address);
|
||||
|
|
@ -1674,7 +1681,7 @@ void handle_btstack_event(uint8_t packet_type, uint16_t channel,
|
|||
}
|
||||
break;
|
||||
case HCI_EVENT_USER_PASSKEY_REQUEST:
|
||||
if (size < 8) {
|
||||
if (!SWITCH_PICO_ENABLE_CLASSIC || size < 8) {
|
||||
break;
|
||||
}
|
||||
hci_event_user_passkey_request_get_bd_addr(packet, address);
|
||||
|
|
@ -1709,13 +1716,17 @@ bool update_pairing_window(uint32_t now_ms) {
|
|||
g_pairing_window_deadline_ms =
|
||||
now_ms + g_pairing_window_duration_ms;
|
||||
gap_set_bondable_mode(true);
|
||||
sm_set_accepted_stk_generation_methods(kAllBlePairingMethods);
|
||||
if (SWITCH_PICO_ENABLE_BLE) {
|
||||
sm_set_accepted_stk_generation_methods(kAllBlePairingMethods);
|
||||
}
|
||||
g_status_led_tick = 0;
|
||||
return true;
|
||||
}
|
||||
if (g_pairing_window_open && !pairing_window_active_at(now_ms)) {
|
||||
g_pairing_window_open = false;
|
||||
sm_set_accepted_stk_generation_methods(0);
|
||||
if (SWITCH_PICO_ENABLE_BLE) {
|
||||
sm_set_accepted_stk_generation_methods(0);
|
||||
}
|
||||
g_status_led_tick = 0;
|
||||
gap_set_bondable_mode(false);
|
||||
return true;
|
||||
|
|
@ -1839,7 +1850,9 @@ void process_clear_pairings(uint32_t now_ms) {
|
|||
|
||||
g_pairing_window_open = false;
|
||||
gap_set_bondable_mode(false);
|
||||
sm_set_accepted_stk_generation_methods(0);
|
||||
if (SWITCH_PICO_ENABLE_BLE) {
|
||||
sm_set_accepted_stk_generation_methods(0);
|
||||
}
|
||||
const bool proprietary_cleared = uni_switch2_pairing_clear();
|
||||
uni_bt_del_keys_unsafe();
|
||||
for (uni_hid_device_t* device : devices) {
|
||||
|
|
@ -1895,8 +1908,8 @@ void apply_connection_policy() {
|
|||
pairing_window_active_at(btstack_run_loop_get_time_ms());
|
||||
const bool active_scan =
|
||||
free_slot && (!active_controller || pairing_open);
|
||||
const bool background_scan = free_slot && !active_scan &&
|
||||
waiting_for_joycon_mate();
|
||||
const bool background_scan = SWITCH_PICO_ENABLE_BLE &&
|
||||
free_slot && !active_scan && waiting_for_joycon_mate();
|
||||
const ConnectionPolicyState desired_state =
|
||||
!free_slot
|
||||
? ConnectionPolicyState::Paused
|
||||
|
|
@ -1920,9 +1933,11 @@ void apply_connection_policy() {
|
|||
|
||||
// Passive mode permits incoming Classic reconnects, with LE discovery
|
||||
// limited to a remembered opposite half for a ready solo Joy-Con2.
|
||||
uni_bt_allow_incoming_connections(true);
|
||||
uni_bt_allow_incoming_connections(SWITCH_PICO_ENABLE_CLASSIC != 0);
|
||||
if (active_scan) {
|
||||
uni_bt_le_set_background_scan(false);
|
||||
if (SWITCH_PICO_ENABLE_BLE) {
|
||||
uni_bt_le_set_background_scan(false);
|
||||
}
|
||||
uni_bt_start_scanning_and_autoconnect_unsafe();
|
||||
g_connection_policy_state = ConnectionPolicyState::Open;
|
||||
} else {
|
||||
|
|
@ -2796,14 +2811,19 @@ void platform_init(int argc, const char** argv) {
|
|||
}
|
||||
|
||||
void platform_on_init_complete() {
|
||||
gap_set_link_supervision_timeout(kClassicLinkSupervisionTimeout);
|
||||
if (SWITCH_PICO_ENABLE_CLASSIC) {
|
||||
gap_set_link_supervision_timeout(kClassicLinkSupervisionTimeout);
|
||||
gap_ssp_set_auto_accept(false);
|
||||
g_pairing_event_callback.callback = handle_btstack_event;
|
||||
hci_add_event_handler(&g_pairing_event_callback);
|
||||
}
|
||||
gap_set_bondable_mode(false);
|
||||
sm_set_accepted_stk_generation_methods(0);
|
||||
gap_ssp_set_auto_accept(false);
|
||||
g_pairing_event_callback.callback = handle_btstack_event;
|
||||
g_identity_event_callback.callback = handle_btstack_event;
|
||||
sm_add_event_handler(&g_identity_event_callback);
|
||||
hci_add_event_handler(&g_pairing_event_callback);
|
||||
if (SWITCH_PICO_ENABLE_BLE) {
|
||||
// Bluepad32 does not initialize SM in Classic-only mode.
|
||||
sm_set_accepted_stk_generation_methods(0);
|
||||
g_identity_event_callback.callback = handle_btstack_event;
|
||||
sm_add_event_handler(&g_identity_event_callback);
|
||||
}
|
||||
switch2_wake_initialize();
|
||||
refresh_pairing_snapshot();
|
||||
btstack_run_loop_set_timer_handler(&g_rumble_timer, process_rumble_timer);
|
||||
|
|
@ -2823,6 +2843,35 @@ void platform_on_init_complete() {
|
|||
}
|
||||
}
|
||||
|
||||
bool device_transport_enabled(const uni_hid_device_t* device) {
|
||||
if (device == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (SWITCH_PICO_ENABLE_BLE && SWITCH_PICO_ENABLE_CLASSIC) {
|
||||
return true;
|
||||
}
|
||||
// GAP describes the actual live link. The protocol hint is also available
|
||||
// before an outgoing connection has an HCI handle.
|
||||
switch (gap_get_connection_type(device->conn.handle)) {
|
||||
case GAP_CONNECTION_ACL:
|
||||
return SWITCH_PICO_ENABLE_CLASSIC != 0;
|
||||
case GAP_CONNECTION_LE:
|
||||
return SWITCH_PICO_ENABLE_BLE != 0;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch (device->conn.protocol) {
|
||||
case UNI_BT_CONN_PROTOCOL_BR_EDR:
|
||||
return SWITCH_PICO_ENABLE_CLASSIC != 0;
|
||||
case UNI_BT_CONN_PROTOCOL_BLE:
|
||||
return SWITCH_PICO_ENABLE_BLE != 0;
|
||||
default:
|
||||
// Preserve mixed-mode admission; single-transport builds cannot
|
||||
// safely admit a connection whose transport is still unknown.
|
||||
return SWITCH_PICO_ENABLE_BLE && SWITCH_PICO_ENABLE_CLASSIC;
|
||||
}
|
||||
}
|
||||
|
||||
uni_error_t platform_on_device_discovered(bd_addr_t addr, const char* name,
|
||||
uint16_t cod, uint8_t rssi) {
|
||||
(void)name;
|
||||
|
|
@ -2831,14 +2880,23 @@ uni_error_t platform_on_device_discovered(bd_addr_t addr, const char* name,
|
|||
if (!has_free_slot()) {
|
||||
return UNI_ERROR_IGNORE_DEVICE;
|
||||
}
|
||||
const uni_hid_device_t* candidate =
|
||||
(g_connection_policy_state == ConnectionPolicyState::Passive ||
|
||||
!SWITCH_PICO_ENABLE_BLE || !SWITCH_PICO_ENABLE_CLASSIC)
|
||||
? uni_hid_device_get_instance_for_address(addr)
|
||||
: nullptr;
|
||||
if (candidate != nullptr && !device_transport_enabled(candidate)) {
|
||||
return UNI_ERROR_IGNORE_DEVICE;
|
||||
}
|
||||
// First discovery can precede device creation, so the transport-specific
|
||||
// Bluepad32 discovery handlers must enforce the mode before this callback.
|
||||
if (g_connection_policy_state == ConnectionPolicyState::Open) {
|
||||
return UNI_ERROR_SUCCESS;
|
||||
}
|
||||
if (g_connection_policy_state != ConnectionPolicyState::Passive) {
|
||||
if (!SWITCH_PICO_ENABLE_BLE ||
|
||||
g_connection_policy_state != ConnectionPolicyState::Passive) {
|
||||
return UNI_ERROR_IGNORE_DEVICE;
|
||||
}
|
||||
const uni_hid_device_t* candidate =
|
||||
uni_hid_device_get_instance_for_address(addr);
|
||||
const int side = joycon_side(candidate);
|
||||
uint8_t address_type = BD_ADDR_TYPE_UNKNOWN;
|
||||
return side != 0 && waiting_for_joycon_mate(side) &&
|
||||
|
|
@ -2853,8 +2911,9 @@ void platform_on_device_connected(uni_hid_device_t* device) {
|
|||
if (device == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (g_connection_policy_state != ConnectionPolicyState::Open &&
|
||||
g_connection_policy_state != ConnectionPolicyState::Passive) {
|
||||
if (!device_transport_enabled(device) ||
|
||||
(g_connection_policy_state != ConnectionPolicyState::Open &&
|
||||
g_connection_policy_state != ConnectionPolicyState::Passive)) {
|
||||
uni_hid_device_disconnect(device);
|
||||
return;
|
||||
}
|
||||
|
|
@ -2937,7 +2996,8 @@ void platform_on_device_disconnected(uni_hid_device_t* device) {
|
|||
}
|
||||
|
||||
uni_error_t platform_on_device_ready(uni_hid_device_t* device) {
|
||||
if (device == nullptr || !uni_hid_device_is_gamepad(device)) {
|
||||
if (!device_transport_enabled(device) ||
|
||||
!uni_hid_device_is_gamepad(device)) {
|
||||
return UNI_ERROR_INVALID_CONTROLLER;
|
||||
}
|
||||
if (g_connection_policy_state == ConnectionPolicyState::FailedClosed) {
|
||||
|
|
@ -3145,7 +3205,7 @@ uni_platform* get_platform() {
|
|||
} // namespace
|
||||
|
||||
extern "C" bool switch_pico_switch2_pairing_allowed(void) {
|
||||
return g_initialized &&
|
||||
return SWITCH_PICO_ENABLE_BLE && g_initialized &&
|
||||
pairing_window_active_at(btstack_run_loop_get_time_ms());
|
||||
}
|
||||
|
||||
|
|
@ -3153,6 +3213,12 @@ extern "C" void __real_sm_request_pairing(hci_con_handle_t handle);
|
|||
extern "C" void __wrap_sm_request_pairing(hci_con_handle_t handle) {
|
||||
uni_hid_device_t* device =
|
||||
uni_hid_device_get_instance_for_connection_handle(handle);
|
||||
if (!SWITCH_PICO_ENABLE_BLE) {
|
||||
if (device != nullptr) {
|
||||
uni_hid_device_disconnect(device);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (uni_hid_parser_switch2_is_ble_device(device)) {
|
||||
// GATT's implicit authentication retry must not enter standard SMP for
|
||||
// this proprietary protocol. Retain storage until HCI teardown.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "input/switch2_wake.h"
|
||||
#include "bluetooth_transport_config.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
|
|
@ -235,7 +236,7 @@ void handle_command_complete(uint8_t* packet, uint16_t size) {
|
|||
}
|
||||
#endif
|
||||
g_identity_ready = true;
|
||||
g_phase = Phase::kIdle;
|
||||
g_phase = SWITCH_PICO_ENABLE_BLE ? Phase::kIdle : Phase::kDisabled;
|
||||
break;
|
||||
case Phase::kSetParameters:
|
||||
g_phase = Phase::kSetData;
|
||||
|
|
@ -298,7 +299,9 @@ void switch2_wake_initialize() {
|
|||
++g_failures;
|
||||
return;
|
||||
}
|
||||
g_configured = true;
|
||||
// Classic bonds also depend on the public address: preserve it across modes,
|
||||
// even when BLE wake advertising is unavailable.
|
||||
g_configured = SWITCH_PICO_ENABLE_BLE;
|
||||
g_event_registration.callback = handle_hci_event;
|
||||
hci_add_event_handler(&g_event_registration);
|
||||
btstack_run_loop_set_timer_handler(&g_timer, task);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#endif
|
||||
|
||||
#include <uni.h>
|
||||
#include "bluetooth_transport_config.h"
|
||||
#include "parser/uni_hid_parser_switch2.h"
|
||||
#include "parser/uni_switch2_haptics.h"
|
||||
#include "parser/uni_switch2_pairing.h"
|
||||
|
|
@ -192,6 +193,7 @@ void uni_hid_parser_xboxone_play_dual_rumble(
|
|||
|
||||
|
||||
extern "C" void __real_sm_request_pairing(hci_con_handle_t) {
|
||||
require(SWITCH_PICO_ENABLE_BLE, "disabled BLE transport entered SMP");
|
||||
++ordinary_smp_requests;
|
||||
}
|
||||
|
||||
|
|
@ -322,27 +324,34 @@ void uni_hid_device_disconnect(uni_hid_device_t* device) {
|
|||
}
|
||||
|
||||
void uni_bt_allow_incoming_connections(bool enabled) {
|
||||
require(!enabled || SWITCH_PICO_ENABLE_CLASSIC,
|
||||
"disabled Classic transport enabled incoming page scanning");
|
||||
incoming_connections = enabled;
|
||||
}
|
||||
|
||||
|
||||
void uni_bt_bredr_scan_start() {
|
||||
require(SWITCH_PICO_ENABLE_CLASSIC, "disabled Classic inquiry started");
|
||||
classic_scanning_enabled = true;
|
||||
}
|
||||
|
||||
void uni_bt_bredr_scan_stop() {
|
||||
require(SWITCH_PICO_ENABLE_CLASSIC, "disabled Classic inquiry was accessed");
|
||||
classic_scanning_enabled = false;
|
||||
}
|
||||
|
||||
void uni_bt_le_scan_start() {
|
||||
require(SWITCH_PICO_ENABLE_BLE, "disabled BLE scanning started");
|
||||
scanning_enabled = true;
|
||||
}
|
||||
|
||||
void uni_bt_le_scan_stop() {
|
||||
require(SWITCH_PICO_ENABLE_BLE, "disabled BLE scanning was accessed");
|
||||
scanning_enabled = false;
|
||||
}
|
||||
|
||||
void uni_bt_le_set_background_scan(bool enabled) {
|
||||
require(SWITCH_PICO_ENABLE_BLE, "disabled BLE scan parameters were accessed");
|
||||
require(!scanning_enabled, "LE scan timing must change only while scanning is stopped");
|
||||
background_scan_parameters = enabled;
|
||||
}
|
||||
|
|
@ -354,8 +363,8 @@ void uni_bt_start_scanning_and_autoconnect_unsafe() {
|
|||
return;
|
||||
}
|
||||
aggregate_scanning_enabled = true;
|
||||
uni_bt_bredr_scan_start();
|
||||
uni_bt_le_scan_start();
|
||||
if (SWITCH_PICO_ENABLE_CLASSIC) uni_bt_bredr_scan_start();
|
||||
if (SWITCH_PICO_ENABLE_BLE) uni_bt_le_scan_start();
|
||||
}
|
||||
|
||||
void uni_bt_stop_scanning_unsafe() {
|
||||
|
|
@ -363,8 +372,8 @@ void uni_bt_stop_scanning_unsafe() {
|
|||
return;
|
||||
}
|
||||
aggregate_scanning_enabled = false;
|
||||
uni_bt_bredr_scan_stop();
|
||||
uni_bt_le_scan_stop();
|
||||
if (SWITCH_PICO_ENABLE_CLASSIC) uni_bt_bredr_scan_stop();
|
||||
if (SWITCH_PICO_ENABLE_BLE) uni_bt_le_scan_stop();
|
||||
}
|
||||
void uni_bt_del_keys_unsafe() {
|
||||
require_clear_completion_pending();
|
||||
|
|
@ -444,33 +453,40 @@ void gap_set_bondable_mode(int enabled) {
|
|||
}
|
||||
|
||||
void gap_set_link_supervision_timeout(uint16_t timeout) {
|
||||
require(SWITCH_PICO_ENABLE_CLASSIC, "disabled Classic link policy was accessed");
|
||||
link_supervision_timeout = timeout;
|
||||
}
|
||||
|
||||
void gap_ssp_set_auto_accept(int auto_accept) {
|
||||
require(SWITCH_PICO_ENABLE_CLASSIC, "disabled Classic SSP was accessed");
|
||||
ssp_auto_accept = auto_accept != 0;
|
||||
}
|
||||
void sm_set_accepted_stk_generation_methods(uint8_t methods) {
|
||||
require(SWITCH_PICO_ENABLE_BLE, "uninitialized SM pairing policy was accessed");
|
||||
accepted_stk_methods = methods;
|
||||
}
|
||||
|
||||
|
||||
int gap_ssp_confirmation_response(const bd_addr_t) {
|
||||
require(SWITCH_PICO_ENABLE_CLASSIC, "disabled Classic confirmation was accepted");
|
||||
++confirmation_accepts;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int gap_ssp_confirmation_negative(const bd_addr_t) {
|
||||
require(SWITCH_PICO_ENABLE_CLASSIC, "disabled Classic confirmation was accessed");
|
||||
++confirmation_rejections;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int gap_ssp_passkey_response(const bd_addr_t, uint32_t) {
|
||||
require(SWITCH_PICO_ENABLE_CLASSIC, "disabled Classic passkey was accepted");
|
||||
++passkey_accepts;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int gap_ssp_passkey_negative(const bd_addr_t) {
|
||||
require(SWITCH_PICO_ENABLE_CLASSIC, "disabled Classic passkey was accessed");
|
||||
++passkey_rejections;
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -482,6 +498,7 @@ void hci_add_event_handler(
|
|||
|
||||
void sm_add_event_handler(
|
||||
btstack_packet_callback_registration_t* callback_handler) {
|
||||
require(SWITCH_PICO_ENABLE_BLE, "event handler added to uninitialized SM");
|
||||
identity_event_handler = callback_handler->callback;
|
||||
}
|
||||
|
||||
|
|
@ -654,6 +671,7 @@ uint32_t time_us_32() {
|
|||
|
||||
void switch2_wake_initialize() {
|
||||
++switch2_wake_initializations;
|
||||
if (!SWITCH_PICO_ENABLE_BLE) switch2_connections_ready = true;
|
||||
}
|
||||
bool switch2_wake_ready_for_connections() {
|
||||
return switch2_connections_ready;
|
||||
|
|
@ -950,23 +968,35 @@ bool read_controller_state(uint8_t slot, ControllerState* output) {
|
|||
void start_backend() {
|
||||
bluepad32_input_backend_init();
|
||||
platform_on_init_complete();
|
||||
require(incoming_connections && scanning_enabled &&
|
||||
classic_scanning_enabled &&
|
||||
link_supervision_timeout ==
|
||||
kClassicLinkSupervisionTimeout &&
|
||||
!bondable && accepted_stk_methods == 0 &&
|
||||
!ssp_auto_accept && pairing_event_handler != nullptr &&
|
||||
identity_event_handler != nullptr &&
|
||||
switch2_wake_initializations == 1,
|
||||
"initialization must register Classic and BLE identity policy");
|
||||
require(incoming_connections == (SWITCH_PICO_ENABLE_CLASSIC != 0) &&
|
||||
scanning_enabled == (SWITCH_PICO_ENABLE_BLE != 0) &&
|
||||
classic_scanning_enabled == (SWITCH_PICO_ENABLE_CLASSIC != 0) &&
|
||||
!bondable && switch2_wake_initializations == 1,
|
||||
"initialization must scan and admit only the selected transports");
|
||||
if (SWITCH_PICO_ENABLE_CLASSIC) {
|
||||
require(link_supervision_timeout == kClassicLinkSupervisionTimeout &&
|
||||
!ssp_auto_accept && pairing_event_handler != nullptr,
|
||||
"Classic initialization must retain its reconnect and SSP policy");
|
||||
} else {
|
||||
require(pairing_event_handler == nullptr,
|
||||
"BLE-only initialization registered a Classic pairing handler");
|
||||
}
|
||||
if (SWITCH_PICO_ENABLE_BLE) {
|
||||
require(accepted_stk_methods == 0 && identity_event_handler != nullptr,
|
||||
"BLE initialization must reject pairing until its window opens");
|
||||
} else {
|
||||
require(identity_event_handler == nullptr,
|
||||
"Classic-only initialization registered an uninitialized SM handler");
|
||||
}
|
||||
}
|
||||
void start_pairing_backend() {
|
||||
start_backend();
|
||||
bluepad32_input_backend_open_pairing_window();
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(g_connection_policy_state == ConnectionPolicyState::Open &&
|
||||
scanning_enabled && classic_scanning_enabled &&
|
||||
incoming_connections,
|
||||
scanning_enabled == (SWITCH_PICO_ENABLE_BLE != 0) &&
|
||||
classic_scanning_enabled == (SWITCH_PICO_ENABLE_CLASSIC != 0) &&
|
||||
incoming_connections == (SWITCH_PICO_ENABLE_CLASSIC != 0),
|
||||
"test connection setup requires an open pairing window");
|
||||
}
|
||||
void dispatch_pairing_event(uint8_t event_type) {
|
||||
|
|
@ -5673,11 +5703,230 @@ void test_wii_orientation_races() {
|
|||
"coalescing back to the current mapping must not apply an obsolete intermediate choice");
|
||||
}
|
||||
|
||||
void require_selected_radio(bool active_scan, bool free_slot) {
|
||||
require(scanning_enabled == (active_scan && SWITCH_PICO_ENABLE_BLE) &&
|
||||
classic_scanning_enabled ==
|
||||
(active_scan && SWITCH_PICO_ENABLE_CLASSIC) &&
|
||||
incoming_connections ==
|
||||
(free_slot && SWITCH_PICO_ENABLE_CLASSIC),
|
||||
"radio policy activated a disabled transport or lost an enabled one");
|
||||
}
|
||||
|
||||
void test_transport_mode_policy() {
|
||||
classic_bond_count = 1;
|
||||
classic_bonds[0][5] = 0xa1;
|
||||
ble_bond_count = 1;
|
||||
ble_bond_types[0] = BD_ADDR_TYPE_LE_RANDOM;
|
||||
ble_bonds[0][5] = 0xb1;
|
||||
auto remembered_switch2 = switch2_device(3, UNI_SW2_PRO_PID);
|
||||
remember_switch2(remembered_switch2);
|
||||
initialize_runtime_profile_storage();
|
||||
ControllerIdentity saved_identities[2]{};
|
||||
for (unsigned index = 0; index < 2; ++index) {
|
||||
auto& identity = saved_identities[index];
|
||||
identity.stable = true;
|
||||
identity.transport = index == 0 ? ControllerTransport::kClassic
|
||||
: ControllerTransport::kBle;
|
||||
identity.address_type = index == 0 ? BD_ADDR_TYPE_UNKNOWN
|
||||
: BD_ADDR_TYPE_LE_RANDOM;
|
||||
memcpy(identity.address, index == 0 ? classic_bonds[0] : ble_bonds[0],
|
||||
sizeof(bd_addr_t));
|
||||
identity.vendor_id = 0x1234;
|
||||
identity.product_id = 0x5678;
|
||||
auto profile = controller_profile_default(identity, 2 + index);
|
||||
profile.weak_rumble_scale = 37 + index;
|
||||
require(runtime_profile_storage.set(identity, 2 + index, profile) ==
|
||||
ProfileStorageResult::kOk &&
|
||||
runtime_profile_storage.activate(identity, 2 + index) ==
|
||||
ProfileStorageResult::kOk,
|
||||
"mode fixture must persist distinct Classic and BLE profiles");
|
||||
}
|
||||
if (!SWITCH_PICO_ENABLE_BLE) switch2_connections_ready = false;
|
||||
start_backend();
|
||||
Bluepad32PairingSnapshot bonds{};
|
||||
bluepad32_input_backend_pairing_snapshot(&bonds);
|
||||
require(bonds.record_count == 3 &&
|
||||
bonds.records[0].transport == Bluepad32PairingTransport::kClassic &&
|
||||
bonds.records[1].transport == Bluepad32PairingTransport::kBle &&
|
||||
bonds.records[2].transport == Bluepad32PairingTransport::kBle &&
|
||||
memcmp(bonds.records[0].address, classic_bonds[0], sizeof(bd_addr_t)) == 0 &&
|
||||
memcmp(bonds.records[1].address, ble_bonds[0], sizeof(bd_addr_t)) == 0 &&
|
||||
memcmp(bonds.records[2].address, remembered_switch2.conn.btaddr,
|
||||
sizeof(bd_addr_t)) == 0 && delete_key_calls == 0,
|
||||
"boot must expose remembered keys from both transports without deleting them");
|
||||
|
||||
for (bool ble : {false, true}) {
|
||||
const bool enabled = ble ? SWITCH_PICO_ENABLE_BLE
|
||||
: SWITCH_PICO_ENABLE_CLASSIC;
|
||||
auto candidate = device(0, true, ble ? UNI_BT_CONN_PROTOCOL_BLE
|
||||
: UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
lookup_devices[0] = &candidate;
|
||||
lookup_device_count = 1;
|
||||
// A discovery candidate need not have a live GAP handle yet.
|
||||
const auto handle = candidate.conn.handle;
|
||||
candidate.conn.handle = HCI_CON_HANDLE_INVALID;
|
||||
require(platform_on_device_discovered(candidate.conn.btaddr, "candidate", 0, 0) ==
|
||||
(enabled ? UNI_ERROR_SUCCESS : UNI_ERROR_IGNORE_DEVICE),
|
||||
"discovery admitted a known disabled-transport candidate");
|
||||
candidate.conn.handle = handle;
|
||||
// A stale protocol hint must never overrule the actual live GAP link.
|
||||
candidate.conn.protocol = ble ? UNI_BT_CONN_PROTOCOL_BR_EDR
|
||||
: UNI_BT_CONN_PROTOCOL_BLE;
|
||||
const int disconnected_before = device_disconnect_calls;
|
||||
platform_on_device_connected(&candidate);
|
||||
require(device_disconnect_calls == disconnected_before + !enabled,
|
||||
"connected admission failed to tear down a disabled live transport");
|
||||
require(platform_on_device_ready(&candidate) ==
|
||||
(enabled ? UNI_ERROR_SUCCESS : UNI_ERROR_INVALID_CONTROLLER) &&
|
||||
slot_snapshot(0).active == enabled,
|
||||
"ready callback bypassed transport admission");
|
||||
uni_controller_t report{};
|
||||
report.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
report.gamepad.buttons = BUTTON_A;
|
||||
platform_on_controller_data(&candidate, &report);
|
||||
require(slot_snapshot(0).state.button_south == enabled,
|
||||
"rejected transport published controller input");
|
||||
if (enabled) platform_on_device_disconnected(&candidate);
|
||||
require_selected_radio(true, true);
|
||||
}
|
||||
auto ordinary_ble = device(0, true, UNI_BT_CONN_PROTOCOL_BLE);
|
||||
lookup_devices[0] = &ordinary_ble;
|
||||
__wrap_sm_request_pairing(ordinary_ble.conn.handle);
|
||||
__wrap_sm_request_pairing(HCI_CON_HANDLE_INVALID);
|
||||
require(ordinary_smp_requests == (SWITCH_PICO_ENABLE_BLE ? 2u : 0u),
|
||||
"Classic-only mode forwarded an SMP authentication request");
|
||||
lookup_device_count = 0;
|
||||
if (!SWITCH_PICO_ENABLE_BLE || !SWITCH_PICO_ENABLE_CLASSIC) {
|
||||
auto unknown = device(0);
|
||||
const int disconnected_before = device_disconnect_calls;
|
||||
platform_on_device_connected(&unknown);
|
||||
require(device_disconnect_calls == disconnected_before + 1 &&
|
||||
platform_on_device_ready(&unknown) == UNI_ERROR_INVALID_CONTROLLER &&
|
||||
!slot_snapshot(0).active,
|
||||
"single-transport mode admitted an unidentified connection");
|
||||
}
|
||||
|
||||
uint8_t confirmation[] = {HCI_EVENT_USER_CONFIRMATION_REQUEST, 6, 1, 2, 3, 4, 5, 6};
|
||||
uint8_t passkey[] = {HCI_EVENT_USER_PASSKEY_REQUEST, 6, 1, 2, 3, 4, 5, 6};
|
||||
handle_btstack_event(HCI_EVENT_PACKET, 0, confirmation, sizeof(confirmation));
|
||||
handle_btstack_event(HCI_EVENT_PACKET, 0, passkey, sizeof(passkey));
|
||||
require(confirmation_accepts == 0 && passkey_accepts == 0 &&
|
||||
confirmation_rejections == SWITCH_PICO_ENABLE_CLASSIC &&
|
||||
passkey_rejections == SWITCH_PICO_ENABLE_CLASSIC,
|
||||
"closed pairing policy must reject only the enabled Classic authentication");
|
||||
bluepad32_input_backend_open_pairing_window();
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(bondable &&
|
||||
switch_pico_switch2_pairing_allowed() == (SWITCH_PICO_ENABLE_BLE != 0) &&
|
||||
(!SWITCH_PICO_ENABLE_BLE || accepted_stk_methods == kAllBlePairingMethods),
|
||||
"pairing window did not enable only the selected authentication policy");
|
||||
handle_btstack_event(HCI_EVENT_PACKET, 0, confirmation, sizeof(confirmation));
|
||||
handle_btstack_event(HCI_EVENT_PACKET, 0, passkey, sizeof(passkey));
|
||||
require(confirmation_accepts == SWITCH_PICO_ENABLE_CLASSIC &&
|
||||
passkey_accepts == SWITCH_PICO_ENABLE_CLASSIC,
|
||||
"pairing window accepted disabled Classic authentication");
|
||||
|
||||
uni_hid_device_t controllers[kSlotCount]{};
|
||||
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
||||
const bool ble = SWITCH_PICO_ENABLE_BLE &&
|
||||
(!SWITCH_PICO_ENABLE_CLASSIC || (index & 1u));
|
||||
controllers[index] = device(index, true, ble ? UNI_BT_CONN_PROTOCOL_BLE
|
||||
: UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
platform_on_device_connected(&controllers[index]);
|
||||
require(platform_on_device_ready(&controllers[index]) == UNI_ERROR_SUCCESS,
|
||||
"enabled transport failed to fill the available slots");
|
||||
}
|
||||
require_selected_radio(false, false);
|
||||
require(platform_on_device_discovered(controllers[0].conn.btaddr, "full", 0, 0) ==
|
||||
UNI_ERROR_IGNORE_DEVICE,
|
||||
"full slots admitted another discovery");
|
||||
now_ms = g_pairing_window_deadline_ms;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(!bondable && !switch_pico_switch2_pairing_allowed() &&
|
||||
(!SWITCH_PICO_ENABLE_BLE || accepted_stk_methods == 0),
|
||||
"pairing authentication remained enabled after the deadline");
|
||||
platform_on_device_disconnected(&controllers[3]);
|
||||
require_selected_radio(false, true);
|
||||
bluepad32_input_backend_open_pairing_window();
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require_selected_radio(true, true);
|
||||
for (uint8_t index = 0; index < 3; ++index) {
|
||||
platform_on_device_disconnected(&controllers[index]);
|
||||
}
|
||||
require_selected_radio(true, true);
|
||||
require(classic_bond_count == 1 && ble_bond_count == 1 &&
|
||||
switch2_pairing_count == 1 && delete_key_calls == 0,
|
||||
"pairing windows or disconnects erased inactive-mode bonds");
|
||||
for (unsigned index = 0; index < 2; ++index) {
|
||||
require_active_profile(saved_identities[index], 2 + index, 37 + index);
|
||||
}
|
||||
const uint32_t token = bluepad32_input_backend_clear_pairings();
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
bluepad32_input_backend_pairing_snapshot(&bonds);
|
||||
require(bluepad32_input_backend_clear_pairings_completed(bonds, token) &&
|
||||
bonds.record_count == 0 && classic_bond_count == 0 &&
|
||||
ble_bond_count == 0 && switch2_pairing_count == 0,
|
||||
"explicit clearing must still cover all stored transports");
|
||||
for (unsigned index = 0; index < 2; ++index) {
|
||||
require_active_profile(saved_identities[index], 2 + index, 37 + index);
|
||||
}
|
||||
require_selected_radio(true, true);
|
||||
}
|
||||
|
||||
void test_transport_background_scan() {
|
||||
start_backend();
|
||||
auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID);
|
||||
auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID);
|
||||
remember_switch2(right);
|
||||
register_lookup_device(&right);
|
||||
negotiated_intervals[left.conn.handle] = 24;
|
||||
ready_switch2(left);
|
||||
require(scanning_enabled && background_scan_parameters &&
|
||||
!classic_scanning_enabled &&
|
||||
incoming_connections == (SWITCH_PICO_ENABLE_CLASSIC != 0) &&
|
||||
negotiated_intervals[left.conn.handle] == 6 &&
|
||||
platform_on_device_discovered(right.conn.btaddr, "mate", 0, 0) ==
|
||||
UNI_ERROR_SUCCESS,
|
||||
"ready solo must find its remembered mate using only low-duty BLE");
|
||||
platform_on_device_connected(&right);
|
||||
require(!scanning_enabled && !classic_scanning_enabled,
|
||||
"pending mate setup must stop discovery");
|
||||
require(platform_on_device_ready(&right) == UNI_ERROR_SUCCESS &&
|
||||
!scanning_enabled && !classic_scanning_enabled,
|
||||
"a complete pair must not continue background discovery");
|
||||
platform_on_device_disconnected(&right);
|
||||
require(scanning_enabled && background_scan_parameters &&
|
||||
!classic_scanning_enabled,
|
||||
"departed mate must restore low-duty BLE discovery");
|
||||
bluepad32_input_backend_open_pairing_window();
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require_selected_radio(true, true);
|
||||
require(!background_scan_parameters,
|
||||
"explicit pairing must replace background scan timing");
|
||||
now_ms = g_pairing_window_deadline_ms;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(scanning_enabled && background_scan_parameters &&
|
||||
!classic_scanning_enabled,
|
||||
"pairing expiry must restore only remembered-mate BLE discovery");
|
||||
platform_on_device_disconnected(&left);
|
||||
require_selected_radio(true, true);
|
||||
require(!background_scan_parameters,
|
||||
"last disconnect must restore foreground scan timing");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
require(argc == 2, "scenario argument required");
|
||||
const std::string scenario = argv[1];
|
||||
if (scenario == "transport-policy") {
|
||||
test_transport_mode_policy();
|
||||
return 0;
|
||||
}
|
||||
if (scenario == "transport-background") {
|
||||
test_transport_background_scan();
|
||||
return 0;
|
||||
}
|
||||
#if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) && defined(SWITCH_PICO_USB_OUTPUT_MODES)
|
||||
if (scenario == "native-stateful") {
|
||||
test_native_stateful_routing();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include <iostream>
|
||||
|
||||
#include <btstack.h>
|
||||
#include "bluetooth_transport_config.h"
|
||||
|
||||
namespace {
|
||||
|
||||
|
|
@ -240,10 +241,44 @@ void test_failed_wake_keeps_stable_identity() {
|
|||
"wake failure disrupted the stable controller identity");
|
||||
}
|
||||
|
||||
void test_disabled_transport_wake() {
|
||||
switch2_wake_initialize();
|
||||
switch2_wake_initialize();
|
||||
Switch2WakeDiagnostics diagnostics{};
|
||||
switch2_wake_diagnostics(&diagnostics);
|
||||
require(!diagnostics.configured && !switch2_wake_ready_for_connections() &&
|
||||
!switch2_wake_request(),
|
||||
"Classic-only wake must be unavailable while preserving the paired host identity");
|
||||
run_task();
|
||||
require_opcode(0, 0xfc01);
|
||||
const uint8_t paired_address[] = SWITCH2_WAKE_SOURCE_ADDRESS_BYTES;
|
||||
require(memcmp(submitted[0].address, paired_address, sizeof(paired_address)) == 0,
|
||||
"Classic-only startup changed the address used by existing bonds");
|
||||
complete(0xfc01);
|
||||
run_task();
|
||||
require_opcode(1, 0x1009);
|
||||
complete(0x1009, 0, paired_address);
|
||||
require(switch2_wake_ready_for_connections() && !switch2_wake_request(),
|
||||
"Classic controller startup did not resume after identity verification");
|
||||
now_ms = 10000;
|
||||
run_task();
|
||||
switch2_wake_diagnostics(&diagnostics);
|
||||
require(!diagnostics.configured && !diagnostics.busy &&
|
||||
submitted_count == 2 && command_available &&
|
||||
diagnostics.accepted_requests == 0 &&
|
||||
diagnostics.completed_bursts == 0 && diagnostics.failures == 0 &&
|
||||
!switch2_wake_request(),
|
||||
"Classic-only wake submitted BLE advertising or remained busy");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
test_stable_identity_wake();
|
||||
test_failed_wake_keeps_stable_identity();
|
||||
if (SWITCH_PICO_ENABLE_BLE) {
|
||||
test_stable_identity_wake();
|
||||
test_failed_wake_keeps_stable_identity();
|
||||
} else {
|
||||
test_disabled_transport_wake();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
|||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
|
||||
for adapter_feasibility, native, short_packets in (
|
||||
(False, False, False),
|
||||
(True, False, False),
|
||||
(True, True, False),
|
||||
(True, True, True),
|
||||
for bluetooth_mode, adapter_feasibility, native, short_packets in (
|
||||
("mixed", False, False, False),
|
||||
("mixed", True, False, False),
|
||||
("mixed", True, True, False),
|
||||
("mixed", True, True, True),
|
||||
("ble", False, False, False),
|
||||
("classic", False, False, False),
|
||||
):
|
||||
suffix = (
|
||||
"_native32"
|
||||
|
|
@ -25,7 +27,9 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
|||
if adapter_feasibility
|
||||
else ""
|
||||
)
|
||||
executable = tmp_path / f"bluepad32_backend_lifecycle_test{suffix}"
|
||||
executable = (
|
||||
tmp_path / f"bluepad32_backend_lifecycle_test_{bluetooth_mode}{suffix}"
|
||||
)
|
||||
command = [
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
|
|
@ -34,6 +38,8 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
|||
"-Werror",
|
||||
"-pedantic",
|
||||
"-DSWITCH_PICO_HID_INSTANCE_COUNT=4",
|
||||
f"-DSWITCH_PICO_ENABLE_BLE={int(bluetooth_mode != 'classic')}",
|
||||
f"-DSWITCH_PICO_ENABLE_CLASSIC={int(bluetooth_mode != 'ble')}",
|
||||
]
|
||||
if adapter_feasibility:
|
||||
command.append("-DSWITCH_PICO_USB_OUTPUT_MODES=1")
|
||||
|
|
@ -84,9 +90,27 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
|||
f"-I{root / 'bluepad32_config'}",
|
||||
str(root / "tests" / "bluepad32_backend_lifecycle_test.cpp"),
|
||||
str(root / "src" / "firmware" / "profile" / "controller_profile.cpp"),
|
||||
str(root / "src" / "firmware" / "profile" / "controller_profile_transform.cpp"),
|
||||
str(root / "src" / "firmware" / "profile" / "controller_synthetic_input.cpp"),
|
||||
str(root / "src" / "firmware" / "profile" / "controller_profile_runtime.cpp"),
|
||||
str(
|
||||
root
|
||||
/ "src"
|
||||
/ "firmware"
|
||||
/ "profile"
|
||||
/ "controller_profile_transform.cpp"
|
||||
),
|
||||
str(
|
||||
root
|
||||
/ "src"
|
||||
/ "firmware"
|
||||
/ "profile"
|
||||
/ "controller_synthetic_input.cpp"
|
||||
),
|
||||
str(
|
||||
root
|
||||
/ "src"
|
||||
/ "firmware"
|
||||
/ "profile"
|
||||
/ "controller_profile_runtime.cpp"
|
||||
),
|
||||
str(root / "src" / "firmware" / "profile" / "profile_storage.cpp"),
|
||||
str(root / "bluepad32_config" / "parser" / "uni_switch2_haptics.c"),
|
||||
str(
|
||||
|
|
@ -97,6 +121,15 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
|||
]
|
||||
)
|
||||
subprocess.run(command, check=True, cwd=root)
|
||||
subprocess.run([str(executable), "transport-policy"], check=True, cwd=root)
|
||||
if bluetooth_mode != "classic":
|
||||
subprocess.run(
|
||||
[str(executable), "transport-background"], check=True, cwd=root
|
||||
)
|
||||
# The existing lifecycle matrix intentionally exercises both transports.
|
||||
# Single-radio builds exercise their admission/radio policy above.
|
||||
if bluetooth_mode != "mixed":
|
||||
continue
|
||||
subprocess.run([str(executable), "xbox-rumble"], check=True, cwd=root)
|
||||
for scenario in (
|
||||
"wii-orientation",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SPEC = importlib.util.spec_from_file_location("switch_pico_build", ROOT / "build.py")
|
||||
build_script = importlib.util.module_from_spec(SPEC)
|
||||
|
|
@ -88,9 +87,7 @@ def test_explicit_environment_wins_over_cache_and_fallback(tmp_path):
|
|||
),
|
||||
],
|
||||
)
|
||||
def test_stale_explicit_environment_is_an_error(
|
||||
tmp_path, variable, invalid, expected
|
||||
):
|
||||
def test_stale_explicit_environment_is_an_error(tmp_path, variable, invalid, expected):
|
||||
sdk = make_sdk(tmp_path / "sdk")
|
||||
toolchain = make_toolchain(tmp_path / "toolchain")
|
||||
environ = {
|
||||
|
|
@ -201,8 +198,197 @@ def test_missing_dependencies_name_only_actionable_overrides():
|
|||
which=no_compiler,
|
||||
)
|
||||
|
||||
assert str(error.value) == (
|
||||
"Missing build prerequisite(s): Pico SDK (set PICO_SDK_PATH); "
|
||||
"Arm GNU toolchain (set PICO_TOOLCHAIN_PATH or add "
|
||||
"arm-none-eabi-gcc to PATH)."
|
||||
assert "PICO_SDK_PATH" in str(error.value)
|
||||
assert "PICO_TOOLCHAIN_PATH" in str(error.value)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def build_cli(tmp_path, monkeypatch):
|
||||
for name in (
|
||||
"SCRIPT_DIR",
|
||||
"CONFIG_FILE",
|
||||
"BUILD_DIR",
|
||||
"AIO_BUILD_DIR",
|
||||
"FEASIBILITY_BUILD_DIR",
|
||||
"FIRMWARE_DIR",
|
||||
"FIRMWARE_ELF_PATH",
|
||||
"FIRMWARE_UF2_PATH",
|
||||
"AIO_FIRMWARE_ELF_PATH",
|
||||
"AIO_FIRMWARE_UF2_PATH",
|
||||
"FEASIBILITY_FIRMWARE_ELF_PATH",
|
||||
"FEASIBILITY_FIRMWARE_UF2_PATH",
|
||||
):
|
||||
original = getattr(build_script, name)
|
||||
monkeypatch.setattr(build_script, name, tmp_path / original.relative_to(ROOT))
|
||||
monkeypatch.setattr(
|
||||
build_script, "ELF_PATH", tmp_path / "build" / "switch-pico.elf"
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
build_script, "UF2_PATH", tmp_path / "build" / "switch-pico.uf2"
|
||||
)
|
||||
sdk = make_sdk(tmp_path / "sdk")
|
||||
toolchain = make_toolchain(tmp_path / "toolchain")
|
||||
monkeypatch.setenv("PICO_SDK_PATH", str(sdk))
|
||||
monkeypatch.setenv("PICO_TOOLCHAIN_PATH", str(toolchain))
|
||||
monkeypatch.setattr(build_script, "resolve_picotool", lambda: Path("picotool"))
|
||||
commands = []
|
||||
|
||||
def run_cmd(command):
|
||||
commands.append(command)
|
||||
if command[:2] == ["cmake", "--build"]:
|
||||
build_dir = Path(command[2])
|
||||
build_dir.mkdir(parents=True, exist_ok=True)
|
||||
for extension in ("elf", "uf2"):
|
||||
(build_dir / f"switch-pico.{extension}").write_bytes(
|
||||
f"{build_dir.name}:{extension}".encode()
|
||||
)
|
||||
|
||||
monkeypatch.setattr(build_script, "run_cmd", run_cmd)
|
||||
return commands
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("variant", "directory", "artifact"),
|
||||
[
|
||||
("--aio", "build-aio", "switch-pico-aio"),
|
||||
(
|
||||
"--adapter-feasibility",
|
||||
"build-feasibility",
|
||||
"switch-pico-adapter-feasibility",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_bluetooth_modes_configure_and_publish_isolated_artifacts(
|
||||
tmp_path, monkeypatch, build_cli, variant, directory, artifact
|
||||
):
|
||||
# Switch away from mixed and back, leaving every other image untouched.
|
||||
published = {}
|
||||
for mode in (None, "ble", "classic", "mixed"):
|
||||
arguments = ["build.py", variant]
|
||||
if mode is not None:
|
||||
arguments.extend(["--bluetooth-mode", mode])
|
||||
monkeypatch.setattr(build_script.sys, "argv", arguments)
|
||||
build_script.main()
|
||||
|
||||
selected = mode or "mixed"
|
||||
suffix = "" if selected == "mixed" else f"-{selected}"
|
||||
build_dir = tmp_path / f"{directory}{suffix}"
|
||||
configure, compile_command, flash_command = build_cli[-3:]
|
||||
assert configure[:5] == ["cmake", "-S", str(tmp_path), "-B", str(build_dir)]
|
||||
assert f"-DSWITCH_PICO_BLUETOOTH_MODE={selected.upper()}" in configure
|
||||
assert "-DSWITCH_PICO_INPUT_BACKEND=BLUEPAD32" in configure
|
||||
assert ("-DSWITCH_PICO_ADAPTER_FEASIBILITY=ON" in configure) == (
|
||||
variant == "--adapter-feasibility"
|
||||
)
|
||||
assert compile_command == ["cmake", "--build", str(build_dir)]
|
||||
assert flash_command == [
|
||||
"picotool",
|
||||
"load",
|
||||
str(build_dir / "switch-pico.elf"),
|
||||
"-fx",
|
||||
]
|
||||
for extension in ("elf", "uf2"):
|
||||
destination = tmp_path / "firmware" / f"{artifact}{suffix}.{extension}"
|
||||
published[destination] = f"{directory}{suffix}:{extension}".encode()
|
||||
for destination, expected in published.items():
|
||||
assert destination.read_bytes() == expected
|
||||
|
||||
|
||||
def test_uart_default_preserves_paths_and_explicit_mixed_configuration(
|
||||
tmp_path, monkeypatch, build_cli
|
||||
):
|
||||
monkeypatch.setattr(build_script.sys, "argv", ["build.py"])
|
||||
build_script.main()
|
||||
|
||||
configure, compile_command, flash_command = build_cli
|
||||
assert configure[:5] == [
|
||||
"cmake",
|
||||
"-S",
|
||||
str(tmp_path),
|
||||
"-B",
|
||||
str(tmp_path / "build"),
|
||||
]
|
||||
assert "-DSWITCH_PICO_INPUT_BACKEND=UART" in configure
|
||||
assert "-DSWITCH_PICO_BLUETOOTH_MODE=MIXED" in configure
|
||||
assert compile_command == ["cmake", "--build", str(tmp_path / "build")]
|
||||
assert flash_command == [
|
||||
"picotool",
|
||||
"load",
|
||||
str(tmp_path / "build" / "switch-pico.elf"),
|
||||
"-fx",
|
||||
]
|
||||
for extension in ("elf", "uf2"):
|
||||
assert (tmp_path / "firmware" / f"switch-pico.{extension}").read_bytes() == (
|
||||
f"build:{extension}".encode()
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"arguments",
|
||||
[
|
||||
["--bluetooth-mode", "ble"],
|
||||
["--bluetooth-mode", "classic"],
|
||||
["--wake-capture", "--bluetooth-mode", "ble"],
|
||||
["--wake-capture", "--bluetooth-mode", "classic"],
|
||||
["--aio", "--bluetooth-mode", "invalid"],
|
||||
],
|
||||
)
|
||||
def test_invalid_bluetooth_selection_exits_before_build_setup(monkeypatch, arguments):
|
||||
monkeypatch.setattr(build_script.sys, "argv", ["build.py", *arguments])
|
||||
monkeypatch.setattr(
|
||||
build_script,
|
||||
"configure_pico_environment",
|
||||
lambda: pytest.fail("invalid selection reached build setup"),
|
||||
)
|
||||
with pytest.raises(SystemExit) as error:
|
||||
build_script.main()
|
||||
assert error.value.code == 2
|
||||
|
||||
|
||||
def test_uart_artifact_overrides_remain_effective(tmp_path, monkeypatch, build_cli):
|
||||
elf_path = tmp_path / "custom.elf"
|
||||
uf2_path = tmp_path / "custom.uf2"
|
||||
elf_path.write_bytes(b"custom ELF")
|
||||
uf2_path.write_bytes(b"custom UF2")
|
||||
monkeypatch.setenv("ELF_PATH", str(elf_path))
|
||||
monkeypatch.setenv("UF2_PATH", str(uf2_path))
|
||||
overridden = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(overridden)
|
||||
monkeypatch.setattr(build_script, "ELF_PATH", overridden.ELF_PATH)
|
||||
monkeypatch.setattr(build_script, "UF2_PATH", overridden.UF2_PATH)
|
||||
monkeypatch.setattr(build_script.sys, "argv", ["build.py"])
|
||||
|
||||
build_script.main()
|
||||
|
||||
assert (tmp_path / "firmware" / "switch-pico.elf").read_bytes() == b"custom ELF"
|
||||
assert (tmp_path / "firmware" / "switch-pico.uf2").read_bytes() == b"custom UF2"
|
||||
assert build_cli[-1] == ["picotool", "load", str(elf_path), "-fx"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("color_option", ["--grip-color", "--random-grip-color"])
|
||||
def test_ble_build_preserves_grip_color_options(
|
||||
tmp_path, monkeypatch, build_cli, color_option
|
||||
):
|
||||
config = build_script.CONFIG_FILE
|
||||
config.parent.mkdir(parents=True)
|
||||
config.write_text(
|
||||
"".join(f"#define {macro} 0x00\n" for macro in build_script.MACROS),
|
||||
encoding="utf-8",
|
||||
)
|
||||
arguments = ["build.py", "--aio", "--bluetooth-mode", "ble", color_option]
|
||||
if color_option == "--grip-color":
|
||||
arguments.append("A1B2C3")
|
||||
else:
|
||||
monkeypatch.setattr(build_script, "random_hex_color", lambda: "A1B2C3")
|
||||
monkeypatch.setattr(build_script.sys, "argv", arguments)
|
||||
|
||||
build_script.main()
|
||||
|
||||
assert config.read_text(encoding="utf-8") == "".join(
|
||||
f"#define SWITCH_COLOR_SLOT_{slot}_{component} 0x{value}\n"
|
||||
for slot in range(1, 5)
|
||||
for component, value in zip(("R", "G", "B"), ("A1", "B2", "C3"))
|
||||
)
|
||||
assert (tmp_path / "firmware" / "switch-pico-aio-ble.uf2").read_bytes() == (
|
||||
b"build-aio-ble:uf2"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,22 +7,26 @@ 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)
|
||||
for mode in ("mixed", "ble", "classic"):
|
||||
executable = tmp_path / f"switch2_wake_test_{mode}"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-DSWITCH_PICO_ENABLE_BLE={int(mode != 'classic')}",
|
||||
f"-DSWITCH_PICO_ENABLE_CLASSIC={int(mode != 'ble')}",
|
||||
f"-I{root / 'tests' / 'switch2_wake_native_stubs'}",
|
||||
f"-I{root / 'src' / 'firmware'}",
|
||||
f"-I{root / 'bluepad32_config'}",
|
||||
str(root / "tests" / "switch2_wake_test.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue