Assign colors per controller slot
This commit is contained in:
parent
4a73680bd2
commit
23a3cbade5
12 changed files with 330 additions and 51 deletions
33
README.md
33
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:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
17
build.py
17
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue