switch-pico/tests/test_native_hub_management_native.py
Joey Yakimowich-Payne 19358a0fff feat(wake): add USB-triggered Switch 2 wake with Windows interface support
Queue correlated, volatile wake requests for the BTstack owner and expose
read-only completion state. Keep the controller chord, captured identity,
pairings, profiles, HD settings and existing wake burst unchanged.

Add Python API and wake CLI with bounded waits, explicit failure outcomes
and no automatic broadcast retries. Avoid importing SDL just to obtain
fixed sensor IDs so USB automation has clean output and no SDL dependency.

Windows libusb cannot control hub roots. Expose only INFO/WAKE on each
native child's existing vendor Interface 1 with isolated control state;
retain hub/HID drivers, descriptors and identities. Add safe sibling-aware
Windows discovery and a packaged Windows libusb runtime. Document manual
WinUSB binding to Interface 1 only and migrate examples to find_wake_pico.

Validate with 713 tests, 10 firmware/probe builds, Windows x64 wheel
resolution on Python 3.9/3.11, and a real child-interface wake burst on
Linux (2.03 s, no controller, no failures). Device/configuration descriptors,
serials and persistent state matched before/after. Physical Windows
hardware/driver operation remains unverified on this Linux workstation.
2026-09-19 20:19:46 -06:00

158 lines
5.1 KiB
Python

import shutil
import subprocess
from pathlib import Path
import pytest
@pytest.mark.parametrize("clock_mhz", [240, 300], ids=["240MHz", "300MHz"])
@pytest.mark.parametrize(
("controller_count", "neutral_input"),
[(2, False), (4, False), (2, True), (4, True)],
ids=[
"native-management",
"native-two-pair",
"neutral-one-pair",
"neutral-two-pair",
],
)
def test_native_hub_management_native(
tmp_path: Path, controller_count: int, neutral_input: bool, clock_mhz: int
) -> None:
root = Path(__file__).resolve().parents[1]
cc = shutil.which("cc") or shutil.which("gcc")
cxx = shutil.which("c++") or shutil.which("g++")
assert cc is not None and cxx is not None, "host C and C++ compilers are required"
includes = [
f"-I{root / 'tests' / 'native_hub_stubs'}",
f"-I{root / 'tests' / 'bluepad32_native_stubs'}",
f"-I{root / 'src' / 'firmware'}",
f"-I{root / 'tools' / 'pico_usb_address_probe'}",
f"-I{root / 'tools' / 'switch2_usb_probe'}",
]
flags = [
"-Wall",
"-Wextra",
"-Werror",
"-pedantic",
"-ffunction-sections",
"-fdata-sections",
"-DSWITCH2_PROBE_HUB=1",
f"-DPROBE_CONTROLLER_COUNT={controller_count}",
f"-DSWITCH_PICO_SYS_CLOCK_MHZ={clock_mhz}",
]
flags.append(f"-DSWITCH2_PROBE_NEUTRAL_INPUT={int(neutral_input)}")
if controller_count == 4 and not neutral_input:
flags.append("-DSWITCH2_BRIDGE_FULL_INPUT=1")
transport = tmp_path / "native_hub_transport.o"
executable = tmp_path / "native_hub_management_test"
subprocess.run(
[
cc,
"-std=c11",
*flags,
*includes,
"-c",
str(root / "tests" / "native_hub_transport_fixture.c"),
"-o",
str(transport),
],
check=True,
cwd=root,
)
sources = [
"tests/native_hub_management_test.cpp",
"src/firmware/usb/usb_configuration_management.cpp",
"tools/switch2_usb_probe/bootsel.cpp",
"src/firmware/configuration/adapter_configuration.cpp",
"src/firmware/core/controller_identity.cpp",
"src/firmware/profile/controller_profile.cpp",
"src/firmware/profile/profile_storage.cpp",
"src/firmware/profile/profile_service.cpp",
]
subprocess.run(
[
cxx,
"-std=c++17",
*flags,
*includes,
"-Wl,--gc-sections",
*(str(root / path) for path in sources),
str(transport),
"-o",
str(executable),
],
check=True,
cwd=root,
)
for reboot_slot in ("root", "child"):
subprocess.run([str(executable), reboot_slot], check=True, cwd=root)
for trace in (False, True):
router_executable = tmp_path / f"native_hub_router_test_{int(trace)}"
trace_flags = ["-DSWITCH2_PROBE_TRACE_NATIVE_INPUT=1"] if trace else []
subprocess.run(
[
cc,
"-std=c11",
*flags,
*trace_flags,
*includes,
str(root / "tests" / "native_hub_router_test.c"),
"-o",
str(router_executable),
],
check=True,
cwd=root,
)
subprocess.run([str(router_executable)], check=True, cwd=root)
@pytest.mark.parametrize("clock_mhz", [240, 300], ids=["240MHz", "300MHz"])
@pytest.mark.parametrize("controller_count", [2, 4], ids=["one-pair", "two-pair"])
@pytest.mark.parametrize("trace_enabled", [False, True], ids=["plain", "trace"])
def test_native_hub_cold_startup(
tmp_path: Path, controller_count: int, trace_enabled: bool, clock_mhz: int
) -> None:
root = Path(__file__).resolve().parents[1]
cc = shutil.which("cc") or shutil.which("gcc")
assert cc is not None, "a host C compiler is required"
executable = tmp_path / "native_hub_startup_test"
flags = [
f"-DPROBE_CONTROLLER_COUNT={controller_count}",
f"-DSWITCH_PICO_SYS_CLOCK_MHZ={clock_mhz}",
]
if trace_enabled:
flags.append("-DSWITCH2_PROBE_TRACE_NATIVE_INPUT=1")
subprocess.run(
[
cc,
"-std=c11",
"-Wall",
"-Wextra",
"-Werror",
"-pedantic",
"-ffunction-sections",
"-fdata-sections",
"-DSWITCH2_PROBE_HUB=1",
*flags,
f"-I{root / 'tests' / 'native_hub_stubs'}",
f"-I{root / 'src' / 'firmware'}",
f"-I{root / 'tools' / 'pico_usb_address_probe'}",
f"-I{root / 'tools' / 'switch2_usb_probe'}",
str(root / "tests" / "native_hub_startup_test.c"),
"-Wl,--gc-sections",
"-o",
str(executable),
],
check=True,
cwd=root,
)
for scenario in (
"ready",
"delayed",
"timeout",
"mismatched-clock",
"unsupported-clock",
"inexact-clock",
):
subprocess.run([str(executable), scenario], check=True, cwd=root)