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. 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 ## 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. Hot‑plug friendly and cross‑platform (macOS/Windows/Linux). - **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. Hot‑plug friendly and cross‑platform (macOS/Windows/Linux).
- **Colour override** (`controller_color_config.h`): compile‑time RGB overrides for body/buttons/grips as seen by the Switch. - **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 two concurrent Bluetooth controllers and sends their controls, calibrated motion, and rumble through two separate Switch Pro USB interfaces without a computer. - **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 ## 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). 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. - **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. - **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 capabilities
| Controller | Buttons/sticks | Rumble | Motion | | 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 - `firmware/switch-pico.elf` and `firmware/switch-pico.uf2`, refreshed from the
corresponding `build/` artifacts after every successful build. corresponding `build/` artifacts after every successful build.
To customize the controller grip color while building, pass one of these mutually To assign one color to every emulated controller slot while building, pass one
exclusive options: of these mutually exclusive options:
```sh ```sh
# Use a random color for both grips # Use one random color for all slots
python3 build.py --random-grip-color 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 python3 build.py --grip-color FF00AA
``` ```
Both options update `controller_color_config.h` before building. With no color Both options update all four slot definitions in
option, that file is left unchanged. Run `python3 build.py --help` to see the `controller_color_config.h` before building. With no color option, the
available command-line options. 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 If the tools or artifacts are in non-default locations, use these environment
variables: 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); const int slot = uni_hid_device_get_idx_for_instance(device);
return slot >= 0 && slot < kSlotCount ? slot : -1; 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() { ConnectionStatus compute_connection_status() {
critical_section_enter_blocking(&g_state_lock); 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 occupied_mismatch = false;
bool became_active = false;
critical_section_enter_blocking(&g_state_lock); critical_section_enter_blocking(&g_state_lock);
BackendSlot& slot = g_slots[slot_index]; BackendSlot& slot = g_slots[slot_index];
occupied_mismatch = slot.device != nullptr && slot.device != device; 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.active = true;
slot.rumble_pending = false; slot.rumble_pending = false;
++slot.state_generation; ++slot.state_generation;
became_active = true;
} }
} }
critical_section_exit(&g_state_lock); 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) { if (occupied_mismatch) {
return UNI_ERROR_NO_SLOTS; return UNI_ERROR_NO_SLOTS;
} }
if (became_active) {
apply_slot_lighting(static_cast<uint8_t>(slot_index), device);
}
recompute_connection_status(); recompute_connection_status();
return UNI_ERROR_SUCCESS; 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() 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() UF2_PATH = Path(os.environ.get("UF2_PATH", BUILD_DIR / "switch-pico.uf2")).expanduser()
MACROS = ( MACROS = tuple(
"SWITCH_COLOR_LEFT_GRIP_R", f"SWITCH_COLOR_SLOT_{slot}_{component}"
"SWITCH_COLOR_LEFT_GRIP_G", for slot in range(1, 5)
"SWITCH_COLOR_LEFT_GRIP_B", for component in ("R", "G", "B")
"SWITCH_COLOR_RIGHT_GRIP_R",
"SWITCH_COLOR_RIGHT_GRIP_G",
"SWITCH_COLOR_RIGHT_GRIP_B",
) )
def parse_args(): def parse_args():
@ -46,12 +43,12 @@ def parse_args():
group.add_argument( group.add_argument(
"--random-grip-color", "--random-grip-color",
action="store_true", action="store_true",
help="Randomize both grip colors before building.", help="Assign one random color to every emulated controller slot.",
) )
group.add_argument( group.add_argument(
"--grip-color", "--grip-color",
metavar="RRGGBB", 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() return parser.parse_args()
@ -84,7 +81,7 @@ def update_grip_colors(rgb_hex):
sys.exit(1) sys.exit(1)
return updated return updated
values = (r, g, b, r, g, b) values = (r, g, b) * 4
for macro, val in zip(MACROS, values): for macro, val in zip(MACROS, values):
text = replace(macro, val, text) text = replace(macro, val, text)

View file

@ -1,25 +1,31 @@
// Optional override for Switch Pro colour fields. // Compile-time Switch grip colors. Physical controller lightbar values are
// Copy/modify the values below and rebuild to change how the controller appears on the Switch. // derived automatically; each value here is an 8-bit RGB component.
// Each value is an 8-bit RGB component.
#pragma once #pragma once
// Body shell colour // Body shell color
#define SWITCH_COLOR_BODY_R 0x1B #define SWITCH_COLOR_BODY_R 0x1B
#define SWITCH_COLOR_BODY_G 0x1B #define SWITCH_COLOR_BODY_G 0x1B
#define SWITCH_COLOR_BODY_B 0x1D #define SWITCH_COLOR_BODY_B 0x1D
// Face/button cluster colour // Face/button cluster color
#define SWITCH_COLOR_BUTTON_R 0xFF #define SWITCH_COLOR_BUTTON_R 0xFF
#define SWITCH_COLOR_BUTTON_G 0xFF #define SWITCH_COLOR_BUTTON_G 0xFF
#define SWITCH_COLOR_BUTTON_B 0xFF #define SWITCH_COLOR_BUTTON_B 0xFF
// Left grip colour // Per-slot Switch grip colors: blue, red, yellow, green.
#define SWITCH_COLOR_LEFT_GRIP_R 0x00 #define SWITCH_COLOR_SLOT_1_R 0x00
#define SWITCH_COLOR_LEFT_GRIP_G 0x89 #define SWITCH_COLOR_SLOT_1_G 0x89
#define SWITCH_COLOR_LEFT_GRIP_B 0xEB #define SWITCH_COLOR_SLOT_1_B 0xEB
// Right grip colour #define SWITCH_COLOR_SLOT_2_R 0xE6
#define SWITCH_COLOR_RIGHT_GRIP_R 0x00 #define SWITCH_COLOR_SLOT_2_G 0x39
#define SWITCH_COLOR_RIGHT_GRIP_G 0x89 #define SWITCH_COLOR_SLOT_2_B 0x46
#define SWITCH_COLOR_RIGHT_GRIP_B 0xEB
#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 "switch_pro_driver.h"
#include <algorithm> #include <algorithm>
#include <cstddef>
#include <cmath> #include <cmath>
#include <cstring> #include <cstring>
#include <stdio.h> #include <stdio.h>
@ -47,6 +48,7 @@ struct SwitchProContext {
bool is_initialized = false; bool is_initialized = false;
bool is_report_queued = false; bool is_report_queued = false;
SwitchDeviceInfo device_info{}; SwitchDeviceInfo device_info{};
SwitchRgbColor grip_color{};
uint8_t player_id = 0; uint8_t player_id = 0;
uint8_t input_mode = 0x30; uint8_t input_mode = 0x30;
SwitchImuMode imu_mode = SwitchImuMode::Off; SwitchImuMode imu_mode = SwitchImuMode::Off;
@ -75,7 +77,7 @@ static SwitchProContext* context_for(uint8_t instance) {
return &contexts[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") #if __has_include("controller_color_config.h")
#include "controller_color_config.h" #include "controller_color_config.h"
#endif #endif
@ -89,18 +91,47 @@ static SwitchProContext* context_for(uint8_t instance) {
#define SWITCH_COLOR_BUTTON_G 0xFF #define SWITCH_COLOR_BUTTON_G 0xFF
#define SWITCH_COLOR_BUTTON_B 0xFF #define SWITCH_COLOR_BUTTON_B 0xFF
#endif #endif
#ifndef SWITCH_COLOR_LEFT_GRIP_R #ifndef SWITCH_COLOR_SLOT_1_R
#define SWITCH_COLOR_LEFT_GRIP_R 0xEC #define SWITCH_COLOR_SLOT_1_R 0x00
#define SWITCH_COLOR_LEFT_GRIP_G 0x00 #define SWITCH_COLOR_SLOT_1_G 0x89
#define SWITCH_COLOR_LEFT_GRIP_B 0x8C #define SWITCH_COLOR_SLOT_1_B 0xEB
#endif #define SWITCH_COLOR_SLOT_2_R 0xE6
#ifndef SWITCH_COLOR_RIGHT_GRIP_R #define SWITCH_COLOR_SLOT_2_G 0x39
#define SWITCH_COLOR_RIGHT_GRIP_R 0xEC #define SWITCH_COLOR_SLOT_2_B 0x46
#define SWITCH_COLOR_RIGHT_GRIP_G 0x00 #define SWITCH_COLOR_SLOT_3_R 0xF6
#define SWITCH_COLOR_RIGHT_GRIP_B 0x8C #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 #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] = { static const uint8_t factory_config_data[0xEFF] = {
// serial number // serial number
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 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, SWITCH_COLOR_BUTTON_R, SWITCH_COLOR_BUTTON_G, SWITCH_COLOR_BUTTON_B,
// left grip color // 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 // 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, 0x01,
@ -440,7 +471,8 @@ static bool send_report(uint8_t instance, SwitchProContext& context,
return result; 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_bank = address & 0xFFFFFF00u;
uint32_t address_offset = address & 0x000000FFu; uint32_t address_offset = address & 0x000000FFu;
const uint8_t* data = nullptr; 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; data = user_calibration_data;
} }
if (data != nullptr) { if (data == nullptr) {
memcpy(dest, data + address_offset, size);
} else {
memset(dest, 0xFF, size); 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[17] = report_data[13];
context.report_buffer[18] = report_data[14]; context.report_buffer[18] = report_data[14];
context.report_buffer[19] = report_data[15]; context.report_buffer[19] = report_data[15];
read_spi_flash(&context.report_buffer[20], spi_read_address, read_spi_flash(context, &context.report_buffer[20],
spi_read_size); spi_read_address, spi_read_size);
LOG_PRINTF("[HID] FEATURE SPI_READ addr=0x%08lx size=%u\n", LOG_PRINTF("[HID] FEATURE SPI_READ addr=0x%08lx size=%u\n",
static_cast<unsigned long>(spi_read_address), static_cast<unsigned long>(spi_read_address),
spi_read_size); spi_read_size);
@ -725,6 +778,7 @@ void switch_pro_init(uint8_t instance) {
return; return;
} }
context->grip_color = switch_pro_get_slot_color(instance);
context->device_info = { context->device_info = {
0x03, 0x03,
0x48, 0x48,

View file

@ -50,6 +50,47 @@ typedef struct {
uint8_t imu_sample_count; // 0-3 uint8_t imu_sample_count; // 0-3
SwitchImuSample imu_samples[3]; SwitchImuSample imu_samples[3];
} SwitchInputState; } 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. // Initialize one HID instance before entering the main loop.
void switch_pro_init(uint8_t instance); void switch_pro_init(uint8_t instance);

View file

@ -4,6 +4,7 @@
#include <string> #include <string>
#include <uni.h> #include <uni.h>
#include "../controller_color_config.h"
namespace { namespace {
@ -44,6 +45,19 @@ void play_rumble(uni_hid_device_t* device, uint16_t, uint16_t,
device->last_high = high; device->last_high = high;
device->last_low = low; 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( uni_hid_device_t device(
int idx, bool gamepad = true, int idx, bool gamepad = true,
@ -59,6 +73,7 @@ uni_hid_device_t device(
} // namespace } // namespace
bool uni_hid_device_is_gamepad(const uni_hid_device_t* device) { bool uni_hid_device_is_gamepad(const uni_hid_device_t* device) {
return device != nullptr && device->gamepad; return device != nullptr && device->gamepad;
} }
@ -149,6 +164,21 @@ uint32_t btstack_run_loop_get_time_ms() {
#include "../bluepad32_input_backend.cpp" #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 { 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() { void test_flash_core_start_contract() {
bluepad32_input_backend_init(); bluepad32_input_backend_init();
flash_core_init_result = false; flash_core_init_result = false;
@ -688,6 +751,8 @@ int main(int argc, char** argv) {
test_independent_lifecycle(); test_independent_lifecycle();
} else if (scenario == "pairing-policy") { } else if (scenario == "pairing-policy") {
test_pairing_window_policy(); test_pairing_window_policy();
} else if (scenario == "slot-lighting") {
test_slot_lighting();
} else if (scenario == "flash-core-start") { } else if (scenario == "flash-core-start") {
test_flash_core_start_contract(); test_flash_core_start_contract();
} else if (scenario == "flash-core-failure") { } else if (scenario == "flash-core-failure") {

View file

@ -82,8 +82,13 @@ struct uni_controller_t {
struct uni_hid_device_t; struct uni_hid_device_t;
typedef void (*uni_play_dual_rumble_t)(uni_hid_device_t*, uint16_t, typedef void (*uni_play_dual_rumble_t)(uni_hid_device_t*, uint16_t,
uint16_t, uint8_t, uint8_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 { 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; uni_play_dual_rumble_t play_dual_rumble;
}; };
@ -107,6 +112,12 @@ struct uni_hid_device_t {
int rumble_calls; int rumble_calls;
uint8_t last_high; uint8_t last_high;
uint8_t last_low; 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 { struct uni_platform {

View file

@ -1,8 +1,10 @@
#include "switch_pro_driver.h" #include "switch_pro_driver.h"
#include "controller_color_config.h"
#include "tusb.h" #include "tusb.h"
#include "pico/time.h" #include "pico/time.h"
#include <array> #include <array>
#include <cstddef>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <iostream> #include <iostream>
@ -139,6 +141,18 @@ void send_feature(uint8_t instance, uint8_t command, uint8_t value) {
report[11] = value; report[11] = value;
tud_hid_report_received_cb(instance, 0, report.data(), report.size()); 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) { void send_config(uint8_t instance, uint8_t subtype) {
const uint8_t report[] = {REPORT_CONFIGURATION, 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"); "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() { void test_rumble_callbacks_and_decoders_are_isolated() {
initialize_contexts(); initialize_contexts();
rumble_events = {}; rumble_events = {};
@ -613,6 +685,7 @@ int main() {
test_input_reports_and_timers_are_isolated(); test_input_reports_and_timers_are_isolated();
test_callback_send_and_imu_modes_are_isolated(); test_callback_send_and_imu_modes_are_isolated();
test_rumble_callbacks_and_decoders_are_isolated(); test_rumble_callbacks_and_decoders_are_isolated();
test_grip_colors_are_isolated();
test_lifecycle_and_invalid_instances(); test_lifecycle_and_invalid_instances();
test_uart_parser_is_pure(); test_uart_parser_is_pure();
if (failures != 0) { if (failures != 0) {

View file

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