diff --git a/README.md b/README.md index 7e32b40..9941696 100644 --- a/README.md +++ b/README.md @@ -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. 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. -- **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: diff --git a/bluepad32_input_backend.cpp b/bluepad32_input_backend.cpp index 1b84169..cbc913c 100644 --- a/bluepad32_input_backend.cpp +++ b/bluepad32_input_backend.cpp @@ -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(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(slot_index), device); + } + recompute_connection_status(); return UNI_ERROR_SUCCESS; diff --git a/build.py b/build.py index 8b728d3..5bb0a45 100644 --- a/build.py +++ b/build.py @@ -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) diff --git a/controller_color_config.h b/controller_color_config.h index 4abdf51..cc9316a 100644 --- a/controller_color_config.h +++ b/controller_color_config.h @@ -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 diff --git a/firmware/switch-pico-aio.elf b/firmware/switch-pico-aio.elf index eb9df86..61d4d09 100755 Binary files a/firmware/switch-pico-aio.elf and b/firmware/switch-pico-aio.elf differ diff --git a/firmware/switch-pico-aio.uf2 b/firmware/switch-pico-aio.uf2 index e177a55..aed1452 100644 Binary files a/firmware/switch-pico-aio.uf2 and b/firmware/switch-pico-aio.uf2 differ diff --git a/switch_pro_driver.cpp b/switch_pro_driver.cpp index bdeb62e..3cb8a2f 100644 --- a/switch_pro_driver.cpp +++ b/switch_pro_driver.cpp @@ -1,6 +1,7 @@ #include "switch_pro_driver.h" #include +#include #include #include #include @@ -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(&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(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, diff --git a/switch_pro_driver.h b/switch_pro_driver.h index 91072f7..74e4346 100644 --- a/switch_pro_driver.h +++ b/switch_pro_driver.h @@ -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(maximum - minimum); + const uint16_t peak = + static_cast((static_cast(maximum) * 2u + 1u) / + 3u); + if (chroma == 0) { + const uint8_t gray = static_cast(peak); + return {gray, gray, gray}; + } + + const auto calibrate = [minimum, chroma, peak](uint8_t component) { + const uint32_t delta = + static_cast(component - minimum); + return static_cast( + (static_cast(peak) * delta * delta) / + (static_cast(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); diff --git a/tests/bluepad32_backend_lifecycle_test.cpp b/tests/bluepad32_backend_lifecycle_test.cpp index 253b74c..6b4da7a 100644 --- a/tests/bluepad32_backend_lifecycle_test.cpp +++ b/tests/bluepad32_backend_lifecycle_test.cpp @@ -4,6 +4,7 @@ #include #include +#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") { diff --git a/tests/bluepad32_native_stubs/uni.h b/tests/bluepad32_native_stubs/uni.h index b229a86..adaa018 100644 --- a/tests/bluepad32_native_stubs/uni.h +++ b/tests/bluepad32_native_stubs/uni.h @@ -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 { diff --git a/tests/switch_pro_driver_context_test.cpp b/tests/switch_pro_driver_context_test.cpp index 576f645..baa62b4 100644 --- a/tests/switch_pro_driver_context_test.cpp +++ b/tests/switch_pro_driver_context_test.cpp @@ -1,8 +1,10 @@ #include "switch_pro_driver.h" +#include "controller_color_config.h" #include "tusb.h" #include "pico/time.h" #include +#include #include #include #include @@ -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 report{}; + report[0] = REPORT_FEATURE; + report[10] = SPI_READ; + report[11] = static_cast(address); + report[12] = static_cast(address >> 8u); + report[13] = static_cast(address >> 16u); + report[14] = static_cast(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(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) { diff --git a/tests/test_bluepad32_backend_lifecycle_native.py b/tests/test_bluepad32_backend_lifecycle_native.py index b9fdb8d..d10eeca 100644 --- a/tests/test_bluepad32_backend_lifecycle_native.py +++ b/tests/test_bluepad32_backend_lifecycle_native.py @@ -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", ):