Assign colors per controller slot

This commit is contained in:
Joey Yakimowich-Payne 2026-08-31 09:06:00 -06:00
commit 23a3cbade5
12 changed files with 330 additions and 51 deletions

View file

@ -3,10 +3,10 @@
Raspberry Pi Pico firmware that emulates one or more Switch Pro controllers over USB. Input can come from the SDL3-to-UART computer bridge or, on Pico 2 W, directly from Bluetooth controllers through Bluepad32.
## What you get
- **Firmware** (`switch-pico.cpp` + `switch_pro_driver.*`): acts as a Switch Pro controller (one on standard Pico, two on Pico 2 W AIO), accepting either UART bridge reports or the optional Pico 2 W Bluepad32 backend.
- **Firmware** (`switch-pico.cpp` + `switch_pro_driver.*`): acts as a Switch Pro controller (one on standard Pico, four on Pico 2 W AIO), accepting either UART bridge reports or the optional Pico 2 W Bluepad32 backend.
- **Python bridge** (`switch_pico_bridge.controller_uart_bridge` / CLI `controller-uart-bridge`): reads SDL3 controllers on the host, sends reports over UART, and applies rumble locally. Hotplug friendly and crossplatform (macOS/Windows/Linux).
- **Colour override** (`controller_color_config.h`): compiletime RGB overrides for body/buttons/grips as seen by the Switch.
- **Pico 2 W AIO firmware** (`firmware/switch-pico-aio.uf2`): hosts two concurrent Bluetooth controllers and sends their controls, calibrated motion, and rumble through two separate Switch Pro USB interfaces without a computer.
- **Color configuration** (`controller_color_config.h`): compile-time RGB colors for emulated controller grips and supported Bluetooth controller LEDs.
- **Pico 2 W AIO firmware** (`firmware/switch-pico-aio.uf2`): hosts four concurrent Bluetooth controllers and sends their controls, calibrated motion, rumble, and slot identity through four separate Switch Pro USB interfaces without a computer.
## Quick start
1. Flash the Pico with `firmware/switch-pico.uf2` (or build your own) using BOOTSEL drag-and-drop (see “Manual UF2 flashing” below).
@ -79,6 +79,18 @@ The Pico 2 W onboard LED reports the overall Bluetooth state:
- **Pair a new controller**: hold BOOTSEL until the LED double-blinks, then put the controller into its explicit Bluetooth pairing mode.
- **Pairing window expires**: scanning and incoming connections stop; already connected controllers remain connected.
### Per-slot controller colors
Each AIO slot has one color shared by its emulated Switch Pro grips and its physical Bluetooth controller:
1. Blue `#0089EB`
2. Red `#E63946`
3. Yellow `#F6C945`
4. Green `#2ECC71`
When a controller becomes ready, RGB-capable devices such as DualSense and DualShock 4 receive a darker, more saturated RGB value derived automatically from the slot's Switch grip color. Controllers without an RGB light use player indicator 1, 2, 3, or 4 when Bluepad32 exposes player-LED control. Devices without either capability are left unchanged. Edit only the four grip colors in `controller_color_config.h`; rebuilding automatically recalibrates their lightbar colors.
### Controller capabilities
| Controller | Buttons/sticks | Rumble | Motion |
@ -236,20 +248,21 @@ The generated files are:
- `firmware/switch-pico.elf` and `firmware/switch-pico.uf2`, refreshed from the
corresponding `build/` artifacts after every successful build.
To customize the controller grip color while building, pass one of these mutually
exclusive options:
To assign one color to every emulated controller slot while building, pass one
of these mutually exclusive options:
```sh
# Use a random color for both grips
# Use one random color for all slots
python3 build.py --random-grip-color
# Use a specific six-digit RGB color for both grips
# Use one specific six-digit RGB color for all slots
python3 build.py --grip-color FF00AA
```
Both options update `controller_color_config.h` before building. With no color
option, that file is left unchanged. Run `python3 build.py --help` to see the
available command-line options.
Both options update all four slot definitions in
`controller_color_config.h` before building. With no color option, the
per-slot blue/red/yellow/green palette is left unchanged. Run
`python3 build.py --help` to see the available command-line options.
If the tools or artifacts are in non-default locations, use these environment
variables:

View file

@ -120,6 +120,18 @@ int slot_for_device(const uni_hid_device_t* device) {
const int slot = uni_hid_device_get_idx_for_instance(device);
return slot >= 0 && slot < kSlotCount ? slot : -1;
}
void apply_slot_lighting(uint8_t slot_index, uni_hid_device_t* device) {
const SwitchRgbColor color =
switch_pro_get_slot_light_color(slot_index);
if (device->report_parser.set_lightbar_color != nullptr) {
device->report_parser.set_lightbar_color(
device, color.red, color.green, color.blue);
} else if (device->report_parser.set_player_leds != nullptr) {
device->report_parser.set_player_leds(
device, static_cast<uint8_t>(1u << slot_index));
}
}
ConnectionStatus compute_connection_status() {
critical_section_enter_blocking(&g_state_lock);
@ -501,6 +513,7 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) {
}
bool occupied_mismatch = false;
bool became_active = false;
critical_section_enter_blocking(&g_state_lock);
BackendSlot& slot = g_slots[slot_index];
occupied_mismatch = slot.device != nullptr && slot.device != device;
@ -511,6 +524,7 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) {
slot.active = true;
slot.rumble_pending = false;
++slot.state_generation;
became_active = true;
}
}
critical_section_exit(&g_state_lock);
@ -518,6 +532,10 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) {
if (occupied_mismatch) {
return UNI_ERROR_NO_SLOTS;
}
if (became_active) {
apply_slot_lighting(static_cast<uint8_t>(slot_index), device);
}
recompute_connection_status();
return UNI_ERROR_SUCCESS;

View file

@ -22,13 +22,10 @@ AIO_FIRMWARE_UF2_PATH = FIRMWARE_DIR / "switch-pico-aio.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()
MACROS = (
"SWITCH_COLOR_LEFT_GRIP_R",
"SWITCH_COLOR_LEFT_GRIP_G",
"SWITCH_COLOR_LEFT_GRIP_B",
"SWITCH_COLOR_RIGHT_GRIP_R",
"SWITCH_COLOR_RIGHT_GRIP_G",
"SWITCH_COLOR_RIGHT_GRIP_B",
MACROS = tuple(
f"SWITCH_COLOR_SLOT_{slot}_{component}"
for slot in range(1, 5)
for component in ("R", "G", "B")
)
def parse_args():
@ -46,12 +43,12 @@ def parse_args():
group.add_argument(
"--random-grip-color",
action="store_true",
help="Randomize both grip colors before building.",
help="Assign one random color to every emulated controller slot.",
)
group.add_argument(
"--grip-color",
metavar="RRGGBB",
help="Set both grip colors to the provided hex value.",
help="Set every emulated controller slot to the provided hex color.",
)
return parser.parse_args()
@ -84,7 +81,7 @@ def update_grip_colors(rgb_hex):
sys.exit(1)
return updated
values = (r, g, b, r, g, b)
values = (r, g, b) * 4
for macro, val in zip(MACROS, values):
text = replace(macro, val, text)

View file

@ -1,25 +1,31 @@
// Optional override for Switch Pro colour fields.
// Copy/modify the values below and rebuild to change how the controller appears on the Switch.
// Each value is an 8-bit RGB component.
// Compile-time Switch grip colors. Physical controller lightbar values are
// derived automatically; each value here is an 8-bit RGB component.
#pragma once
// Body shell colour
// Body shell color
#define SWITCH_COLOR_BODY_R 0x1B
#define SWITCH_COLOR_BODY_G 0x1B
#define SWITCH_COLOR_BODY_B 0x1D
// Face/button cluster colour
// Face/button cluster color
#define SWITCH_COLOR_BUTTON_R 0xFF
#define SWITCH_COLOR_BUTTON_G 0xFF
#define SWITCH_COLOR_BUTTON_B 0xFF
// Left grip colour
#define SWITCH_COLOR_LEFT_GRIP_R 0x00
#define SWITCH_COLOR_LEFT_GRIP_G 0x89
#define SWITCH_COLOR_LEFT_GRIP_B 0xEB
// Per-slot Switch grip colors: blue, red, yellow, green.
#define SWITCH_COLOR_SLOT_1_R 0x00
#define SWITCH_COLOR_SLOT_1_G 0x89
#define SWITCH_COLOR_SLOT_1_B 0xEB
// Right grip colour
#define SWITCH_COLOR_RIGHT_GRIP_R 0x00
#define SWITCH_COLOR_RIGHT_GRIP_G 0x89
#define SWITCH_COLOR_RIGHT_GRIP_B 0xEB
#define SWITCH_COLOR_SLOT_2_R 0xE6
#define SWITCH_COLOR_SLOT_2_G 0x39
#define SWITCH_COLOR_SLOT_2_B 0x46
#define SWITCH_COLOR_SLOT_3_R 0xF6
#define SWITCH_COLOR_SLOT_3_G 0xC9
#define SWITCH_COLOR_SLOT_3_B 0x45
#define SWITCH_COLOR_SLOT_4_R 0x2E
#define SWITCH_COLOR_SLOT_4_G 0xCC
#define SWITCH_COLOR_SLOT_4_B 0x71

Binary file not shown.

Binary file not shown.

View file

@ -1,6 +1,7 @@
#include "switch_pro_driver.h"
#include <algorithm>
#include <cstddef>
#include <cmath>
#include <cstring>
#include <stdio.h>
@ -47,6 +48,7 @@ struct SwitchProContext {
bool is_initialized = false;
bool is_report_queued = false;
SwitchDeviceInfo device_info{};
SwitchRgbColor grip_color{};
uint8_t player_id = 0;
uint8_t input_mode = 0x30;
SwitchImuMode imu_mode = SwitchImuMode::Off;
@ -75,7 +77,7 @@ static SwitchProContext* context_for(uint8_t instance) {
return &contexts[instance];
}
// Optional compile-time colour override (body/buttons/grips).
// Optional compile-time color palette shared with Bluetooth controller LEDs.
#if __has_include("controller_color_config.h")
#include "controller_color_config.h"
#endif
@ -89,18 +91,47 @@ static SwitchProContext* context_for(uint8_t instance) {
#define SWITCH_COLOR_BUTTON_G 0xFF
#define SWITCH_COLOR_BUTTON_B 0xFF
#endif
#ifndef SWITCH_COLOR_LEFT_GRIP_R
#define SWITCH_COLOR_LEFT_GRIP_R 0xEC
#define SWITCH_COLOR_LEFT_GRIP_G 0x00
#define SWITCH_COLOR_LEFT_GRIP_B 0x8C
#endif
#ifndef SWITCH_COLOR_RIGHT_GRIP_R
#define SWITCH_COLOR_RIGHT_GRIP_R 0xEC
#define SWITCH_COLOR_RIGHT_GRIP_G 0x00
#define SWITCH_COLOR_RIGHT_GRIP_B 0x8C
#ifndef SWITCH_COLOR_SLOT_1_R
#define SWITCH_COLOR_SLOT_1_R 0x00
#define SWITCH_COLOR_SLOT_1_G 0x89
#define SWITCH_COLOR_SLOT_1_B 0xEB
#define SWITCH_COLOR_SLOT_2_R 0xE6
#define SWITCH_COLOR_SLOT_2_G 0x39
#define SWITCH_COLOR_SLOT_2_B 0x46
#define SWITCH_COLOR_SLOT_3_R 0xF6
#define SWITCH_COLOR_SLOT_3_G 0xC9
#define SWITCH_COLOR_SLOT_3_B 0x45
#define SWITCH_COLOR_SLOT_4_R 0x2E
#define SWITCH_COLOR_SLOT_4_G 0xCC
#define SWITCH_COLOR_SLOT_4_B 0x71
#endif
static constexpr SwitchRgbColor slot_colors[] = {
{SWITCH_COLOR_SLOT_1_R, SWITCH_COLOR_SLOT_1_G, SWITCH_COLOR_SLOT_1_B},
{SWITCH_COLOR_SLOT_2_R, SWITCH_COLOR_SLOT_2_G, SWITCH_COLOR_SLOT_2_B},
{SWITCH_COLOR_SLOT_3_R, SWITCH_COLOR_SLOT_3_G, SWITCH_COLOR_SLOT_3_B},
{SWITCH_COLOR_SLOT_4_R, SWITCH_COLOR_SLOT_4_G, SWITCH_COLOR_SLOT_4_B},
};
static_assert(SWITCH_PICO_HID_INSTANCE_COUNT <=
sizeof(slot_colors) / sizeof(slot_colors[0]));
SwitchRgbColor switch_pro_get_slot_color(uint8_t instance) {
if (instance >= sizeof(slot_colors) / sizeof(slot_colors[0])) {
return {};
}
return slot_colors[instance];
}
SwitchRgbColor switch_pro_get_slot_light_color(uint8_t instance) {
if (instance >= sizeof(slot_colors) / sizeof(slot_colors[0])) {
return {};
}
return switch_pro_calibrate_light_color(slot_colors[instance]);
}
static const uint8_t factory_config_data[0xEFF] = {
// serial number
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
@ -148,10 +179,10 @@ static const uint8_t factory_config_data[0xEFF] = {
SWITCH_COLOR_BUTTON_R, SWITCH_COLOR_BUTTON_G, SWITCH_COLOR_BUTTON_B,
// left grip color
SWITCH_COLOR_LEFT_GRIP_R, SWITCH_COLOR_LEFT_GRIP_G, SWITCH_COLOR_LEFT_GRIP_B,
SWITCH_COLOR_SLOT_1_R, SWITCH_COLOR_SLOT_1_G, SWITCH_COLOR_SLOT_1_B,
// right grip color
SWITCH_COLOR_RIGHT_GRIP_R, SWITCH_COLOR_RIGHT_GRIP_G, SWITCH_COLOR_RIGHT_GRIP_B,
SWITCH_COLOR_SLOT_1_R, SWITCH_COLOR_SLOT_1_G, SWITCH_COLOR_SLOT_1_B,
0x01,
@ -440,7 +471,8 @@ static bool send_report(uint8_t instance, SwitchProContext& context,
return result;
}
static void read_spi_flash(uint8_t* dest, uint32_t address, uint8_t size) {
static void read_spi_flash(const SwitchProContext& context, uint8_t* dest,
uint32_t address, uint8_t size) {
uint32_t address_bank = address & 0xFFFFFF00u;
uint32_t address_offset = address & 0x000000FFu;
const uint8_t* data = nullptr;
@ -450,10 +482,31 @@ static void read_spi_flash(uint8_t* dest, uint32_t address, uint8_t size) {
data = user_calibration_data;
}
if (data != nullptr) {
memcpy(dest, data + address_offset, size);
} else {
if (data == nullptr) {
memset(dest, 0xFF, size);
return;
}
memcpy(dest, data + address_offset, size);
if (address_bank != 0x6000u) {
return;
}
static_assert(sizeof(SwitchRgbColor) == sizeof(SwitchColorDefinition));
const uint8_t* color =
reinterpret_cast<const uint8_t*>(&context.grip_color);
constexpr size_t left_offset =
offsetof(SwitchFactoryConfig, leftGripColor);
constexpr size_t right_offset =
offsetof(SwitchFactoryConfig, rightGripColor);
for (uint8_t index = 0; index < size; ++index) {
const size_t offset = address_offset + index;
if (offset >= left_offset &&
offset < left_offset + sizeof(SwitchRgbColor)) {
dest[index] = color[offset - left_offset];
} else if (offset >= right_offset &&
offset < right_offset + sizeof(SwitchRgbColor)) {
dest[index] = color[offset - right_offset];
}
}
}
@ -573,8 +626,8 @@ static void handle_feature_report(SwitchProContext& context,
context.report_buffer[17] = report_data[13];
context.report_buffer[18] = report_data[14];
context.report_buffer[19] = report_data[15];
read_spi_flash(&context.report_buffer[20], spi_read_address,
spi_read_size);
read_spi_flash(context, &context.report_buffer[20],
spi_read_address, spi_read_size);
LOG_PRINTF("[HID] FEATURE SPI_READ addr=0x%08lx size=%u\n",
static_cast<unsigned long>(spi_read_address),
spi_read_size);
@ -725,6 +778,7 @@ void switch_pro_init(uint8_t instance) {
return;
}
context->grip_color = switch_pro_get_slot_color(instance);
context->device_info = {
0x03,
0x48,

View file

@ -50,6 +50,47 @@ typedef struct {
uint8_t imu_sample_count; // 0-3
SwitchImuSample imu_samples[3];
} SwitchInputState;
typedef struct {
uint8_t red;
uint8_t green;
uint8_t blue;
} SwitchRgbColor;
constexpr SwitchRgbColor switch_pro_calibrate_light_color(
SwitchRgbColor grip) {
const uint8_t minimum =
grip.red < grip.green
? (grip.red < grip.blue ? grip.red : grip.blue)
: (grip.green < grip.blue ? grip.green : grip.blue);
const uint8_t maximum =
grip.red > grip.green
? (grip.red > grip.blue ? grip.red : grip.blue)
: (grip.green > grip.blue ? grip.green : grip.blue);
const uint16_t chroma = static_cast<uint16_t>(maximum - minimum);
const uint16_t peak =
static_cast<uint16_t>((static_cast<uint16_t>(maximum) * 2u + 1u) /
3u);
if (chroma == 0) {
const uint8_t gray = static_cast<uint8_t>(peak);
return {gray, gray, gray};
}
const auto calibrate = [minimum, chroma, peak](uint8_t component) {
const uint32_t delta =
static_cast<uint32_t>(component - minimum);
return static_cast<uint8_t>(
(static_cast<uint32_t>(peak) * delta * delta) /
(static_cast<uint32_t>(chroma) * chroma));
};
return {calibrate(grip.red), calibrate(grip.green),
calibrate(grip.blue)};
}
// Return the configured Switch grip color and its automatically calibrated
// physical LED color for one HID/controller slot.
SwitchRgbColor switch_pro_get_slot_color(uint8_t instance);
SwitchRgbColor switch_pro_get_slot_light_color(uint8_t instance);
// Initialize one HID instance before entering the main loop.
void switch_pro_init(uint8_t instance);

View file

@ -4,6 +4,7 @@
#include <string>
#include <uni.h>
#include "../controller_color_config.h"
namespace {
@ -44,6 +45,19 @@ void play_rumble(uni_hid_device_t* device, uint16_t, uint16_t,
device->last_high = high;
device->last_low = low;
}
void set_lightbar(uni_hid_device_t* device, uint8_t red, uint8_t green,
uint8_t blue) {
++device->lightbar_calls;
device->lightbar_red = red;
device->lightbar_green = green;
device->lightbar_blue = blue;
}
void set_player_leds(uni_hid_device_t* device, uint8_t leds) {
++device->player_led_calls;
device->player_leds = leds;
}
uni_hid_device_t device(
int idx, bool gamepad = true,
@ -59,6 +73,7 @@ uni_hid_device_t device(
} // namespace
bool uni_hid_device_is_gamepad(const uni_hid_device_t* device) {
return device != nullptr && device->gamepad;
}
@ -149,6 +164,21 @@ uint32_t btstack_run_loop_get_time_ms() {
#include "../bluepad32_input_backend.cpp"
SwitchRgbColor switch_pro_get_slot_light_color(uint8_t instance) {
static constexpr SwitchRgbColor grips[] = {
{SWITCH_COLOR_SLOT_1_R, SWITCH_COLOR_SLOT_1_G,
SWITCH_COLOR_SLOT_1_B},
{SWITCH_COLOR_SLOT_2_R, SWITCH_COLOR_SLOT_2_G,
SWITCH_COLOR_SLOT_2_B},
{SWITCH_COLOR_SLOT_3_R, SWITCH_COLOR_SLOT_3_G,
SWITCH_COLOR_SLOT_3_B},
{SWITCH_COLOR_SLOT_4_R, SWITCH_COLOR_SLOT_4_G,
SWITCH_COLOR_SLOT_4_B},
};
return instance < sizeof(grips) / sizeof(grips[0])
? switch_pro_calibrate_light_color(grips[instance])
: SwitchRgbColor{};
}
namespace {
@ -639,6 +669,39 @@ void test_pairing_window_policy() {
}
void test_slot_lighting() {
start_pairing_backend();
uni_hid_device_t devices[kSlotCount] = {
device(0), device(1), device(2), device(3)};
for (uint8_t slot = 0; slot < kSlotCount; ++slot) {
devices[slot].report_parser.set_lightbar_color = set_lightbar;
devices[slot].report_parser.set_player_leds = set_player_leds;
require(platform_on_device_ready(&devices[slot]) == UNI_ERROR_SUCCESS,
"color-capable controller did not become ready");
const SwitchRgbColor expected =
switch_pro_get_slot_light_color(slot);
require(devices[slot].lightbar_calls == 1 &&
devices[slot].lightbar_red == expected.red &&
devices[slot].lightbar_green == expected.green &&
devices[slot].lightbar_blue == expected.blue &&
devices[slot].player_led_calls == 0,
"slot color did not reach the controller lightbar");
require(platform_on_device_ready(&devices[slot]) == UNI_ERROR_SUCCESS &&
devices[slot].lightbar_calls == 1,
"duplicate ready event rewrote controller lighting");
}
platform_on_device_disconnected(&devices[2]);
uni_hid_device_t fallback = device(2);
fallback.report_parser.set_player_leds = set_player_leds;
require(platform_on_device_ready(&fallback) == UNI_ERROR_SUCCESS &&
fallback.lightbar_calls == 0 &&
fallback.player_led_calls == 1 &&
fallback.player_leds == (1u << 2u),
"controller without RGB support did not receive its slot LED");
}
void test_flash_core_start_contract() {
bluepad32_input_backend_init();
flash_core_init_result = false;
@ -688,6 +751,8 @@ int main(int argc, char** argv) {
test_independent_lifecycle();
} else if (scenario == "pairing-policy") {
test_pairing_window_policy();
} else if (scenario == "slot-lighting") {
test_slot_lighting();
} else if (scenario == "flash-core-start") {
test_flash_core_start_contract();
} else if (scenario == "flash-core-failure") {

View file

@ -82,8 +82,13 @@ struct uni_controller_t {
struct uni_hid_device_t;
typedef void (*uni_play_dual_rumble_t)(uni_hid_device_t*, uint16_t,
uint16_t, uint8_t, uint8_t);
typedef void (*uni_set_player_leds_t)(uni_hid_device_t*, uint8_t);
typedef void (*uni_set_lightbar_color_t)(uni_hid_device_t*, uint8_t, uint8_t,
uint8_t);
struct uni_report_parser_t {
uni_set_player_leds_t set_player_leds;
uni_set_lightbar_color_t set_lightbar_color;
uni_play_dual_rumble_t play_dual_rumble;
};
@ -107,6 +112,12 @@ struct uni_hid_device_t {
int rumble_calls;
uint8_t last_high;
uint8_t last_low;
int lightbar_calls;
uint8_t lightbar_red;
uint8_t lightbar_green;
uint8_t lightbar_blue;
int player_led_calls;
uint8_t player_leds;
};
struct uni_platform {

View file

@ -1,8 +1,10 @@
#include "switch_pro_driver.h"
#include "controller_color_config.h"
#include "tusb.h"
#include "pico/time.h"
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <iostream>
@ -139,6 +141,18 @@ void send_feature(uint8_t instance, uint8_t command, uint8_t value) {
report[11] = value;
tud_hid_report_received_cb(instance, 0, report.data(), report.size());
}
void send_spi_read(uint8_t instance, uint32_t address, uint8_t size) {
std::array<uint8_t, SWITCH_PRO_ENDPOINT_SIZE> report{};
report[0] = REPORT_FEATURE;
report[10] = SPI_READ;
report[11] = static_cast<uint8_t>(address);
report[12] = static_cast<uint8_t>(address >> 8u);
report[13] = static_cast<uint8_t>(address >> 16u);
report[14] = static_cast<uint8_t>(address >> 24u);
report[15] = size;
tud_hid_report_received_cb(instance, 0, report.data(), report.size());
}
void send_config(uint8_t instance, uint8_t subtype) {
const uint8_t report[] = {REPORT_CONFIGURATION, subtype};
@ -406,6 +420,64 @@ void test_callback_send_and_imu_modes_are_isolated() {
"instance 1 quaternion accelerometer state was overwritten");
}
void test_grip_colors_are_isolated() {
initialize_contexts();
constexpr uint32_t grip_address =
0x6000u + offsetof(SwitchFactoryConfig, leftGripColor);
constexpr uint8_t grip_bytes =
sizeof(SwitchColorDefinition) * 2u;
constexpr SwitchRgbColor calibrated_blue =
switch_pro_calibrate_light_color({0x00, 0x89, 0xEB});
constexpr SwitchRgbColor calibrated_gray =
switch_pro_calibrate_light_color({0x96, 0x96, 0x96});
static_assert(calibrated_blue.red == 0x00 &&
calibrated_blue.green == 0x35 &&
calibrated_blue.blue == 0x9D);
static_assert(calibrated_gray.red == 0x64 &&
calibrated_gray.green == 0x64 &&
calibrated_gray.blue == 0x64);
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
send_spi_read(instance, grip_address, grip_bytes);
now_ms += 6;
expect(!switch_pro_task(instance),
"grip color SPI reply counted as regular input");
expect(sent_report_count == static_cast<unsigned>(instance + 1u),
"grip color SPI reply was not sent");
const SentReport& response = sent_reports[sent_report_count - 1u];
const SwitchRgbColor expected =
switch_pro_get_slot_color(instance);
const uint8_t expected_bytes[] = {
expected.red, expected.green, expected.blue,
expected.red, expected.green, expected.blue,
};
expect(response.instance == instance &&
response.data[13] == 0x90 &&
response.data[14] == SPI_READ &&
std::memcmp(response.data.data() + 20, expected_bytes,
sizeof(expected_bytes)) == 0,
"Switch grip color did not match its HID slot");
const SwitchRgbColor light =
switch_pro_get_slot_light_color(instance);
const SwitchRgbColor calibrated =
switch_pro_calibrate_light_color(expected);
expect(light.red == calibrated.red &&
light.green == calibrated.green &&
light.blue == calibrated.blue,
"physical controller light was not derived from its grip");
}
const SwitchRgbColor invalid_grip =
switch_pro_get_slot_color(kInvalidInstance);
const SwitchRgbColor invalid_light =
switch_pro_get_slot_light_color(kInvalidInstance);
expect(invalid_grip.red == 0 && invalid_grip.green == 0 &&
invalid_grip.blue == 0 && invalid_light.red == 0 &&
invalid_light.green == 0 && invalid_light.blue == 0,
"invalid HID slot returned a configured color");
}
void test_rumble_callbacks_and_decoders_are_isolated() {
initialize_contexts();
rumble_events = {};
@ -613,6 +685,7 @@ int main() {
test_input_reports_and_timers_are_isolated();
test_callback_send_and_imu_modes_are_isolated();
test_rumble_callbacks_and_decoders_are_isolated();
test_grip_colors_are_isolated();
test_lifecycle_and_invalid_instances();
test_uart_parser_is_pure();
if (failures != 0) {

View file

@ -36,6 +36,7 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
"rejections",
"lifecycle",
"pairing-policy",
"slot-lighting",
"flash-core-start",
"flash-core-failure",
):