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

@ -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",
):