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.
|
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:
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
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()
|
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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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") {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
):
|
):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue