Consolidate firmware variants and publish AIO radio-mode builds

This commit is contained in:
Joey Yakimowich-Payne 2026-09-09 11:53:14 -06:00
commit e2c8e0d860
12 changed files with 38 additions and 83 deletions

View file

@ -119,7 +119,8 @@ The final Windows fix uses a distinct probe device revision because Windows cach
Current test artifact: Current test artifact:
- `firmware/switch-pico-adapter-feasibility.uf2` - `firmware/switch-pico-aio.uf2` (includes the automatic Switch/XInput behavior;
the separate feasibility build has been retired).
Windows test command: Windows test command:

View file

@ -24,8 +24,6 @@ if (EXISTS ${picoVscode})
endif() endif()
# ==================================================================================== # ====================================================================================
option(SWITCH_PICO_LOG "Enable UART debug logging" OFF) option(SWITCH_PICO_LOG "Enable UART debug logging" OFF)
option(SWITCH_PICO_ADAPTER_FEASIBILITY
"Build the automatic Switch/XInput feasibility prototype" OFF)
if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32") if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
set(SWITCH_PICO_NATIVE_DEFAULT ON) set(SWITCH_PICO_NATIVE_DEFAULT ON)
set(SWITCH_PICO_CLOCK_DEFAULT 300) set(SWITCH_PICO_CLOCK_DEFAULT 300)
@ -119,11 +117,6 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32"
message(FATAL_ERROR message(FATAL_ERROR
"SWITCH_PICO_INPUT_BACKEND=BLUEPAD32 requires PICO_BOARD=pico2_w") "SWITCH_PICO_INPUT_BACKEND=BLUEPAD32 requires PICO_BOARD=pico2_w")
endif() endif()
if(SWITCH_PICO_ADAPTER_FEASIBILITY
AND NOT SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
message(FATAL_ERROR
"SWITCH_PICO_ADAPTER_FEASIBILITY requires the BLUEPAD32 backend")
endif()
if(SWITCH_PICO_HAPTICS_EXPERIMENT if(SWITCH_PICO_HAPTICS_EXPERIMENT
AND NOT SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32") AND NOT SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
message(FATAL_ERROR message(FATAL_ERROR

View file

@ -57,10 +57,18 @@ Build and flash a Pico 2 W in BOOTSEL mode:
python3 build.py --aio python3 build.py --aio
``` ```
This uses an isolated `build-aio/` CMake cache and publishes: This uses an isolated `build-aio/` CMake cache. The main firmware variants are:
- `firmware/switch-pico-aio.elf` | Variant | UF2 image |
- `firmware/switch-pico-aio.uf2` | --- | --- |
| Regular Pico, UART input | [switch-pico.uf2](firmware/switch-pico.uf2) |
| Pico 2 W AIO, mixed Bluetooth | [switch-pico-aio.uf2](firmware/switch-pico-aio.uf2) |
| Pico 2 W AIO, BLE only | [switch-pico-aio-ble.uf2](firmware/switch-pico-aio-ble.uf2) |
| Pico 2 W AIO, Classic only | [switch-pico-aio-classic.uf2](firmware/switch-pico-aio-classic.uf2) |
Each image has a matching `.elf` in `firmware/`. All AIO variants include the same
automatic Switch/XInput and manual USB output modes; no separate feasibility
firmware is needed. The wake-capture image remains a separate setup utility.
The default `python3 build.py` command and `firmware/switch-pico.*` artifacts remain the UART/Pico build. The AIO build requires `PICO_BOARD=pico2_w`; it is not interchangeable with the original non-wireless Pico firmware. The default `python3 build.py` command and `firmware/switch-pico.*` artifacts remain the UART/Pico build. The AIO build requires `PICO_BOARD=pico2_w`; it is not interchangeable with the original non-wireless Pico firmware.
@ -87,7 +95,7 @@ cmake --build build-aio-ble
``` ```
Use `CLASSIC` or `MIXED` and a matching build directory for the other modes. Use `CLASSIC` or `MIXED` and a matching build directory for the other modes.
Add `-DSWITCH_PICO_ADAPTER_FEASIBILITY=ON` for automatic Switch/XInput output. Every AIO build includes automatic Switch/XInput and manual USB output modes.
Invalid modes and single-transport selections with the UART backend are rejected. Invalid modes and single-transport selections with the UART backend are rejected.
The build helper also supports the selector; these commands **build and flash**: The build helper also supports the selector; these commands **build and flash**:
@ -95,7 +103,7 @@ The build helper also supports the selector; these commands **build and flash**:
```sh ```sh
python3 build.py --aio --bluetooth-mode ble python3 build.py --aio --bluetooth-mode ble
python3 build.py --aio --bluetooth-mode classic python3 build.py --aio --bluetooth-mode classic
python3 build.py --adapter-feasibility --bluetooth-mode ble python3 build.py --aio --bluetooth-mode mixed
``` ```
The helper defaults explicitly to `mixed`. Single-transport build directories and The helper defaults explicitly to `mixed`. Single-transport build directories and
@ -812,7 +820,7 @@ were created, copies the release artifacts into `firmware/`, and flashes the ELF
with `picotool`. with `picotool`.
`build.py` automatically locates the Pico SDK and Arm GNU toolchain from valid `build.py` automatically locates the Pico SDK and Arm GNU toolchain from valid
existing `build/`, `build-aio/`, or `build-feasibility/` CMake caches, then from existing regular, AIO (including BLE/Classic), and wake-capture CMake caches, then from
project-local `build/_deps/pico_sdk-src` and `build/toolchain` installs, and project-local `build/_deps/pico_sdk-src` and `build/toolchain` installs, and
finally from conventional user and system locations. A compiler already on finally from conventional user and system locations. A compiler already on
`PATH` is used without setting a toolchain override. Explicit `PICO_SDK_PATH` `PATH` is used without setting a toolchain override. Explicit `PICO_SDK_PATH`

View file

@ -15,7 +15,6 @@ FIRMWARE_SOURCE_DIR = SCRIPT_DIR / "src" / "firmware"
CONFIG_FILE = FIRMWARE_SOURCE_DIR / "platform" / "pico" / "controller_color_config.h" CONFIG_FILE = FIRMWARE_SOURCE_DIR / "platform" / "pico" / "controller_color_config.h"
BUILD_DIR = SCRIPT_DIR / "build" BUILD_DIR = SCRIPT_DIR / "build"
AIO_BUILD_DIR = SCRIPT_DIR / "build-aio" 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_SOURCE_DIR = SCRIPT_DIR / "tools" / "switch2_wake_capture"
WAKE_CAPTURE_BUILD_DIR = SCRIPT_DIR / "build-wake-capture" WAKE_CAPTURE_BUILD_DIR = SCRIPT_DIR / "build-wake-capture"
FIRMWARE_DIR = SCRIPT_DIR / "firmware" FIRMWARE_DIR = SCRIPT_DIR / "firmware"
@ -23,8 +22,6 @@ FIRMWARE_ELF_PATH = FIRMWARE_DIR / "switch-pico.elf"
FIRMWARE_UF2_PATH = FIRMWARE_DIR / "switch-pico.uf2" FIRMWARE_UF2_PATH = FIRMWARE_DIR / "switch-pico.uf2"
AIO_FIRMWARE_ELF_PATH = FIRMWARE_DIR / "switch-pico-aio.elf" AIO_FIRMWARE_ELF_PATH = FIRMWARE_DIR / "switch-pico-aio.elf"
AIO_FIRMWARE_UF2_PATH = FIRMWARE_DIR / "switch-pico-aio.uf2" AIO_FIRMWARE_UF2_PATH = FIRMWARE_DIR / "switch-pico-aio.uf2"
FEASIBILITY_FIRMWARE_ELF_PATH = FIRMWARE_DIR / "switch-pico-adapter-feasibility.elf"
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_ELF_PATH = FIRMWARE_DIR / "switch-pico-wake-capture.elf"
WAKE_CAPTURE_FIRMWARE_UF2_PATH = FIRMWARE_DIR / "switch-pico-wake-capture.uf2" WAKE_CAPTURE_FIRMWARE_UF2_PATH = FIRMWARE_DIR / "switch-pico-wake-capture.uf2"
@ -42,11 +39,9 @@ CMAKE_CACHE_PATHS = tuple(
for build_dir in ( for build_dir in (
BUILD_DIR, BUILD_DIR,
AIO_BUILD_DIR, AIO_BUILD_DIR,
FEASIBILITY_BUILD_DIR,
WAKE_CAPTURE_BUILD_DIR, WAKE_CAPTURE_BUILD_DIR,
*( *(
base.with_name(f"{base.name}-{mode}") AIO_BUILD_DIR.with_name(f"{AIO_BUILD_DIR.name}-{mode}")
for base in (AIO_BUILD_DIR, FEASIBILITY_BUILD_DIR)
for mode in ("ble", "classic") for mode in ("ble", "classic")
), ),
) )
@ -282,11 +277,6 @@ def parse_args():
action="store_true", action="store_true",
help="Build and flash the Pico 2 W Bluepad32 all-in-one firmware.", help="Build and flash the Pico 2 W Bluepad32 all-in-one firmware.",
) )
mode_group.add_argument(
"--adapter-feasibility",
action="store_true",
help="Build and flash the Pico 2 W automatic Switch/XInput prototype.",
)
mode_group.add_argument( mode_group.add_argument(
"--wake-capture", "--wake-capture",
action="store_true", action="store_true",
@ -296,7 +286,7 @@ def parse_args():
"--bluetooth-mode", "--bluetooth-mode",
choices=("mixed", "ble", "classic"), choices=("mixed", "ble", "classic"),
default="mixed", default="mixed",
help="Select active Bluetooth transports for --aio or --adapter-feasibility.", help="Select active Bluetooth transports for --aio.",
) )
group = parser.add_mutually_exclusive_group() group = parser.add_mutually_exclusive_group()
group.add_argument( group.add_argument(
@ -312,8 +302,8 @@ def parse_args():
args = parser.parse_args() args = parser.parse_args()
if args.wake_capture and (args.random_grip_color or args.grip_color): if args.wake_capture and (args.random_grip_color or args.grip_color):
parser.error("wake capture firmware does not use grip-color options") parser.error("wake capture firmware does not use grip-color options")
if args.bluetooth_mode != "mixed" and not (args.aio or args.adapter_feasibility): if args.bluetooth_mode != "mixed" and not args.aio:
parser.error("--bluetooth-mode requires --aio or --adapter-feasibility") parser.error("--bluetooth-mode requires --aio")
return args return args
@ -392,7 +382,6 @@ def resolve_picotool():
def build( def build(
aio, aio,
adapter_feasibility,
build_dir, build_dir,
elf_path, elf_path,
uf2_path, uf2_path,
@ -401,23 +390,21 @@ def build(
*, *,
bluetooth_mode="mixed", bluetooth_mode="mixed",
): ):
if aio or adapter_feasibility: if aio:
definitions = [ definitions = [
"-DSWITCH_PICO_LOG=OFF", "-DSWITCH_PICO_LOG=OFF",
"-DPICO_BOARD=pico2_w", "-DPICO_BOARD=pico2_w",
"-DSWITCH_PICO_INPUT_BACKEND=BLUEPAD32", "-DSWITCH_PICO_INPUT_BACKEND=BLUEPAD32",
] ]
if adapter_feasibility:
definitions.append("-DSWITCH_PICO_ADAPTER_FEASIBILITY=ON")
else: else:
definitions = [ definitions = [
"-DSWITCH_PICO_LOG=OFF", "-DSWITCH_PICO_LOG=OFF",
"-DPICO_BOARD=pico", "-DPICO_BOARD=pico",
"-DSWITCH_PICO_INPUT_BACKEND=UART", "-DSWITCH_PICO_INPUT_BACKEND=UART",
] ]
# AIO and XInput images use the qualified native transport by default. # AIO includes all USB output modes and uses the native transport by default.
# UART remains unchanged; explicit flags also prevent stale cache choices. # UART remains unchanged; explicit flags also prevent stale cache choices.
native = "ON" if aio or adapter_feasibility else "OFF" native = "ON" if aio else "OFF"
definitions.extend( definitions.extend(
[ [
f"-DSWITCH_PICO_BLUETOOTH_MODE={bluetooth_mode.upper()}", f"-DSWITCH_PICO_BLUETOOTH_MODE={bluetooth_mode.upper()}",
@ -529,13 +516,7 @@ def main():
update_grip_colors(color) update_grip_colors(color)
print(f"Grip color set to #{color} in {CONFIG_FILE.name}") print(f"Grip color set to #{color} in {CONFIG_FILE.name}")
if args.adapter_feasibility: if args.aio:
build_dir = FEASIBILITY_BUILD_DIR
elf_path = FEASIBILITY_BUILD_DIR / "switch-pico.elf"
uf2_path = FEASIBILITY_BUILD_DIR / "switch-pico.uf2"
firmware_elf_path = FEASIBILITY_FIRMWARE_ELF_PATH
firmware_uf2_path = FEASIBILITY_FIRMWARE_UF2_PATH
elif args.aio:
build_dir = AIO_BUILD_DIR build_dir = AIO_BUILD_DIR
elf_path = AIO_BUILD_DIR / "switch-pico.elf" elf_path = AIO_BUILD_DIR / "switch-pico.elf"
uf2_path = AIO_BUILD_DIR / "switch-pico.uf2" uf2_path = AIO_BUILD_DIR / "switch-pico.uf2"
@ -558,7 +539,6 @@ def main():
build( build(
args.aio, args.aio,
args.adapter_feasibility,
build_dir, build_dir,
elf_path, elf_path,
uf2_path, uf2_path,
@ -568,7 +548,7 @@ def main():
) )
flash( flash(
elf_path, elf_path,
allow_elf_override=not args.aio and not args.adapter_feasibility, allow_elf_override=not args.aio,
) )

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -10,23 +10,14 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
compiler = shutil.which("c++") or shutil.which("g++") compiler = shutil.which("c++") or shutil.which("g++")
assert compiler is not None, "a host C++ compiler is required" assert compiler is not None, "a host C++ compiler is required"
for bluetooth_mode, adapter_feasibility, native, short_packets in ( for bluetooth_mode, native, short_packets in (
("mixed", False, False, False), ("mixed", False, False),
("mixed", True, False, False), ("mixed", True, False),
("mixed", True, True, False), ("mixed", True, True),
("mixed", True, True, True), ("ble", False, False),
("ble", False, False, False), ("classic", False, False),
("classic", False, False, False),
): ):
suffix = ( suffix = "_native32" if short_packets else "_native64" if native else ""
"_native32"
if short_packets
else "_native64"
if native
else "_adapter"
if adapter_feasibility
else ""
)
executable = ( executable = (
tmp_path / f"bluepad32_backend_lifecycle_test_{bluetooth_mode}{suffix}" tmp_path / f"bluepad32_backend_lifecycle_test_{bluetooth_mode}{suffix}"
) )
@ -38,11 +29,10 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
"-Werror", "-Werror",
"-pedantic", "-pedantic",
"-DSWITCH_PICO_HID_INSTANCE_COUNT=4", "-DSWITCH_PICO_HID_INSTANCE_COUNT=4",
"-DSWITCH_PICO_USB_OUTPUT_MODES=1",
f"-DSWITCH_PICO_ENABLE_BLE={int(bluetooth_mode != 'classic')}", f"-DSWITCH_PICO_ENABLE_BLE={int(bluetooth_mode != 'classic')}",
f"-DSWITCH_PICO_ENABLE_CLASSIC={int(bluetooth_mode != 'ble')}", f"-DSWITCH_PICO_ENABLE_CLASSIC={int(bluetooth_mode != 'ble')}",
] ]
if adapter_feasibility:
command.append("-DSWITCH_PICO_USB_OUTPUT_MODES=1")
if native: if native:
command.extend( command.extend(
[ [

View file

@ -209,14 +209,11 @@ def build_cli(tmp_path, monkeypatch):
"CONFIG_FILE", "CONFIG_FILE",
"BUILD_DIR", "BUILD_DIR",
"AIO_BUILD_DIR", "AIO_BUILD_DIR",
"FEASIBILITY_BUILD_DIR",
"FIRMWARE_DIR", "FIRMWARE_DIR",
"FIRMWARE_ELF_PATH", "FIRMWARE_ELF_PATH",
"FIRMWARE_UF2_PATH", "FIRMWARE_UF2_PATH",
"AIO_FIRMWARE_ELF_PATH", "AIO_FIRMWARE_ELF_PATH",
"AIO_FIRMWARE_UF2_PATH", "AIO_FIRMWARE_UF2_PATH",
"FEASIBILITY_FIRMWARE_ELF_PATH",
"FEASIBILITY_FIRMWARE_UF2_PATH",
): ):
original = getattr(build_script, name) original = getattr(build_script, name)
monkeypatch.setattr(build_script, name, tmp_path / original.relative_to(ROOT)) monkeypatch.setattr(build_script, name, tmp_path / original.relative_to(ROOT))
@ -247,24 +244,13 @@ def build_cli(tmp_path, monkeypatch):
return commands 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( def test_bluetooth_modes_configure_and_publish_isolated_artifacts(
tmp_path, monkeypatch, build_cli, variant, directory, artifact tmp_path, monkeypatch, build_cli
): ):
# Switch away from mixed and back, leaving every other image untouched. # Switch away from mixed and back, leaving every other image untouched.
published = {} published = {}
for mode in (None, "ble", "classic", "mixed"): for mode in (None, "ble", "classic", "mixed"):
arguments = ["build.py", variant] arguments = ["build.py", "--aio"]
if mode is not None: if mode is not None:
arguments.extend(["--bluetooth-mode", mode]) arguments.extend(["--bluetooth-mode", mode])
monkeypatch.setattr(build_script.sys, "argv", arguments) monkeypatch.setattr(build_script.sys, "argv", arguments)
@ -272,14 +258,11 @@ def test_bluetooth_modes_configure_and_publish_isolated_artifacts(
selected = mode or "mixed" selected = mode or "mixed"
suffix = "" if selected == "mixed" else f"-{selected}" suffix = "" if selected == "mixed" else f"-{selected}"
build_dir = tmp_path / f"{directory}{suffix}" build_dir = tmp_path / f"build-aio{suffix}"
configure, compile_command, flash_command = build_cli[-3:] configure, compile_command, flash_command = build_cli[-3:]
assert configure[:5] == ["cmake", "-S", str(tmp_path), "-B", str(build_dir)] assert configure[:5] == ["cmake", "-S", str(tmp_path), "-B", str(build_dir)]
assert f"-DSWITCH_PICO_BLUETOOTH_MODE={selected.upper()}" in configure assert f"-DSWITCH_PICO_BLUETOOTH_MODE={selected.upper()}" in configure
assert "-DSWITCH_PICO_INPUT_BACKEND=BLUEPAD32" 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 compile_command == ["cmake", "--build", str(build_dir)]
assert flash_command == [ assert flash_command == [
"picotool", "picotool",
@ -288,8 +271,8 @@ def test_bluetooth_modes_configure_and_publish_isolated_artifacts(
"-fx", "-fx",
] ]
for extension in ("elf", "uf2"): for extension in ("elf", "uf2"):
destination = tmp_path / "firmware" / f"{artifact}{suffix}.{extension}" destination = tmp_path / "firmware" / f"switch-pico-aio{suffix}.{extension}"
published[destination] = f"{directory}{suffix}:{extension}".encode() published[destination] = f"build-aio{suffix}:{extension}".encode()
for destination, expected in published.items(): for destination, expected in published.items():
assert destination.read_bytes() == expected assert destination.read_bytes() == expected