Introduce protocol-neutral controller state

This commit is contained in:
Joey Yakimowich-Payne 2026-09-02 14:53:56 -06:00
commit b11ae076a8
28 changed files with 833 additions and 501 deletions

View file

@ -51,12 +51,14 @@ The AIO firmware currently has:
- Switch Pro input, motion, colors, and rumble per slot
- per-controller ABXY and motion hotkeys
The current Bluetooth backend converts controller input directly into `SwitchInputState`. This is the principal architectural blocker for other USB modes because it:
The Bluetooth, UART, Switch, and XInput paths now share `ControllerState`:
- turns analog triggers into digital ZL/ZR early
- assigns Switch-specific button labels before output selection
- couples input normalization to Switch report semantics
- gives mapping, profiles, and macros no protocol-neutral state on which to operate
- face buttons use positional names instead of protocol labels
- sticks use signed full-range axes with zero at rest
- triggers retain their full 16-bit analog values
- motion samples and generation-based consumption remain per slot
- normalized conventional rumble uses `ControllerRumbleOutput`
- Switch and XInput serializers own their protocol-specific mappings
## Progress and gap matrix
@ -72,6 +74,7 @@ The current Bluetooth backend converts controller input directly into `SwitchInp
| XInput descriptors and reports | Feasibility complete | Four-interface prototype works on Windows and is covered by native descriptor/report tests. |
| Automatic Windows/Switch selection | Feasibility complete | Windows enumeration fix is in `db4a860`; real Windows transition and rumble were reported working. |
| Windows feasibility test | Complete | `tools/Test-AdapterFeasibility.ps1` checks transition, PnP health, four XInput slots, controls, and rumble isolation. |
| Protocol-neutral controller state | Complete | `ControllerState` is shared by Bluetooth, UART, Switch, and XInput paths; analog trigger precision is retained. |
| Production USB VID/PID | Missing | Prototype uses `CAFE:4010`; obtain an appropriate project VID/PID and repeat Windows binding tests. |
| DInput output | Missing | Add generic HID descriptor and report driver. |
| Mac output mode | Missing | Capture/define compatible descriptor and report semantics. |
@ -80,7 +83,7 @@ The current Bluetooth backend converts controller input directly into `SwitchInp
| Manual output-mode selection | Missing | Add persistent PC command and controller chord. |
| General button remapping | Partial | Only per-controller ABXY swap exists. |
| Stick sensitivity | Missing in AIO | Add inner deadzone, outer saturation, curve, inversion, and center calibration. |
| Trigger ranges | Missing | Preserve analog values, then add lower/upper range, curve, and digital threshold. |
| Trigger ranges | Partial | Full analog values are preserved; profile-configurable lower/upper range, curve, and digital threshold remain. |
| Vibration intensity | Missing as configuration | Transport works; add per-profile weak/strong scaling. |
| Macros | Missing | Add a bounded deterministic macro engine. |
| Turbo and Auto Burst | Missing | Add exact 15 Hz behavior and cancellation rules. |
@ -127,6 +130,36 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass `
The script should be started with the Pico disconnected. It records the Switch-to-XInput transition, checks all four XInput API slots, requires D-pad/face/shoulder/trigger/stick activity for each requested physical controller, tests per-slot rumble, and writes a JSON report to `%TEMP%`.
### Bluetooth discovery latency regression
Hardware bisect established `edf6eca` (`Add dual-controller AIO USB
transport`) as the first commit with delayed IMU and rumble. The preceding
`7941294` build was responsive. The extra USB interface was not the cause.
`edf6eca` changed the Bluetooth policy so Classic inquiry and BLE scanning
continued whenever any controller slot was free. With one controller active
and another slot empty, discovery consumed CYW43439 radio time and delayed HID
input and output traffic.
Commit `7449d6d` fixes the regression with three connection-policy states:
- **Open:** active discovery and incoming connections; used with zero active
controllers or while the explicit BOOTSEL pairing window is open.
- **Passive:** active discovery stopped, incoming connections allowed; used
whenever at least one controller is active and a slot remains free.
- **Paused:** discovery and incoming connections stopped because all slots are
occupied.
The diagnostic proof kept the dual-controller USB build unchanged and only
stopped discovery after the first controller became ready; IMU responsiveness
immediately returned. The production policy was then verified on the current
four-controller master build, with both IMU and rumble reported good.
Important tradeoff: controllers that initiate their own reconnect can join
while the firmware is passive. Controllers that require host-side discovery,
including an additional 8BitDo Ultimate, require opening the BOOTSEL pairing
window while another controller is active. Do not restore continuous inquiry
as a convenience feature; it causes gameplay latency.
## Implementation principles
### Protocol-neutral state
@ -186,7 +219,7 @@ Do not use heap allocation or virtual dispatch. Select one descriptor family bef
## Delivery phases
### Phase 1 — Protocol-neutral controller state
### Phase 1 — Protocol-neutral controller state — Complete
Changes:
@ -204,6 +237,15 @@ Acceptance:
- Current pairing, motion, rumble, and descriptor tests pass.
- UART and AIO firmware both build.
Completion evidence:
- 49 native tests passed, including analog trigger and Switch threshold boundaries
- UART, AIO, and feasibility firmware built successfully
- XInput hardware exposed full-range sticks and 8-bit analog triggers
- XInput rumble passed on the 8BitDo Ultimate
- Switch buttons, sticks, ZL/ZR, motion, and rumble matched the fixed master baseline
- the scan-latency regression was bisected, fixed, documented, and retested before acceptance
### Phase 2 — Persistent configuration protocol
Generalize endpoint-zero management beyond pairing while keeping Switch USB enumeration unchanged.
@ -469,14 +511,17 @@ Do not mark a host/controller combination complete from descriptor inspection or
## Next action
Begin Phase 1: introduce the protocol-neutral controller state while preserving the current Switch and UART behavior.
Begin Phase 2: generalize endpoint-zero management into a versioned persistent
configuration protocol while preserving pairing commands and Switch
enumeration.
The first change should be deliberately narrow:
The first Phase 2 delivery should remain narrow:
1. define the neutral state and invariants
2. make Bluepad32 publish it without losing analog triggers
3. adapt the existing Switch path to consume it
4. migrate all four slots in one clean cutover
5. run existing tests and real Switch input/motion/rumble smoke tests
1. define version, size, CRC, generation, and atomic-commit invariants
2. reserve storage that cannot overlap Bluepad32 bonds
3. add read/write/reset operations for one small configuration object
4. migrate pairing management into the versioned envelope
5. verify malformed requests, interrupted writes, rollback, and power-cycle persistence
Do not begin profiles, macros, or additional output modes until this boundary is proven. They all depend on it.
Do not begin profiles, macros, or tuning transforms until the storage and USB
transaction boundary is proven.

View file

@ -2,15 +2,11 @@
#include <stdint.h>
#include "adapter_usb_mode.h"
#include "tusb.h"
enum class AdapterUsbMode : uint8_t {
kSwitchProbe,
kXInput,
};
void adapter_host_probe_init();
AdapterUsbMode adapter_host_probe_mode();
void adapter_host_probe_note_string_descriptor(uint8_t index);
bool adapter_host_probe_vendor_control(uint8_t rhport, uint8_t stage,
tusb_control_request_t const *request);

10
adapter_usb_mode.h Normal file
View file

@ -0,0 +1,10 @@
#pragma once
#include <stdint.h>
enum class AdapterUsbMode : uint8_t {
kSwitchProbe,
kXInput,
};
AdapterUsbMode adapter_host_probe_mode();

View file

@ -12,21 +12,20 @@
#include <pico/multicore.h>
#include <pico/stdlib.h>
#include <uni.h>
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#include "adapter_usb_mode.h"
#endif
namespace {
constexpr uint16_t kStickMidpoint = 32768;
constexpr int32_t kAxisMinimum = -512;
constexpr int32_t kAxisMaximum = 511;
constexpr int32_t kTriggerMaximum = 1023;
constexpr int32_t kTriggerThreshold = (kTriggerMaximum * 35) / 100;
constexpr uint16_t kSwitchHostRumbleDurationMs = 50;
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
// XInput vibration is stateful: it remains active until XInputSetState sends
// a new magnitude. Use the longest Bluepad32 duration and stop explicitly on
// the zero-magnitude packet.
constexpr uint16_t kRumbleDurationMs = UINT16_MAX;
#else
constexpr uint16_t kRumbleDurationMs = 50;
// XInput vibration is stateful and remains active until XInputSetState sends
// a new magnitude.
constexpr uint16_t kXInputHostRumbleDurationMs = UINT16_MAX;
#endif
constexpr uint32_t kRumblePollIntervalMs = 5;
constexpr uint8_t kSlotCount = BLUEPAD32_INPUT_BACKEND_SLOT_COUNT;
@ -100,7 +99,7 @@ enum class ConnectionPolicyState {
struct RumbleEnvelope {
uint8_t slot;
uint32_t connection_generation;
SwitchRumbleOutput rumble;
ControllerRumbleOutput rumble;
};
struct FeedbackEnvelope {
uint32_t connection_generation;
@ -111,7 +110,7 @@ struct FeedbackEnvelope {
struct BackendSlot {
SwitchInputState state;
ControllerState state;
// Non-null with active=false is a connected device still becoming ready.
uni_hid_device_t* device;
uint32_t state_generation;
@ -154,13 +153,17 @@ bool g_pairing_window_open = false;
bool g_status_led_on = false;
Bluepad32PairingSnapshot g_pairing_snapshot{};
SwitchInputState make_neutral_state() {
SwitchInputState state{};
state.lx = kStickMidpoint;
state.ly = kStickMidpoint;
state.rx = kStickMidpoint;
state.ry = kStickMidpoint;
return state;
uint16_t host_rumble_duration_ms() {
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) {
return kXInputHostRumbleDurationMs;
}
#endif
return kSwitchHostRumbleDurationMs;
}
ControllerState make_neutral_state() {
return controller_neutral_state();
}
bool valid_slot(uint8_t slot) {
@ -226,7 +229,7 @@ ConnectionStatus compute_connection_status() {
}
void publish_device_state(uint8_t slot, uni_hid_device_t* device,
const SwitchInputState& state) {
const ControllerState& state) {
critical_section_enter_blocking(&g_state_lock);
BackendSlot& target = g_slots[slot];
if (target.active && target.device == device) {
@ -263,14 +266,28 @@ constexpr int32_t clamp_axis(int32_t value) {
return value;
}
constexpr uint16_t scale_stick(int32_t value) {
constexpr int16_t scale_axis(int32_t value) {
value = clamp_axis(value);
if (value <= 0) {
return static_cast<uint16_t>(
(static_cast<int64_t>(value - kAxisMinimum) * kStickMidpoint) / -kAxisMinimum);
return static_cast<int16_t>(
(static_cast<int64_t>(value) * -INT16_MIN) /
-kAxisMinimum);
}
return static_cast<int16_t>(
(static_cast<int64_t>(value) * INT16_MAX) /
kAxisMaximum);
}
constexpr uint16_t scale_trigger(int32_t value) {
if (value <= 0) {
return 0;
}
if (value >= kTriggerMaximum) {
return UINT16_MAX;
}
return static_cast<uint16_t>(
kStickMidpoint + (static_cast<int64_t>(value) * (UINT16_MAX - kStickMidpoint)) / kAxisMaximum);
(static_cast<int64_t>(value) * UINT16_MAX) /
kTriggerMaximum);
}
constexpr int16_t clamp_int16(int64_t value) {
@ -301,9 +318,11 @@ constexpr int16_t convert_gyro(int64_t q10_value) {
return clamp_int16(divide_round_nearest(q10_value * kNumeratorScale, kDenominator));
}
static_assert(scale_stick(-512) == 0);
static_assert(scale_stick(0) == 32768);
static_assert(scale_stick(511) == UINT16_MAX);
static_assert(scale_axis(-512) == INT16_MIN);
static_assert(scale_axis(0) == 0);
static_assert(scale_axis(511) == INT16_MAX);
static_assert(scale_trigger(0) == 0);
static_assert(scale_trigger(1023) == UINT16_MAX);
static_assert(convert_accel(8192) == 4096);
static_assert(convert_accel(-8192) == -4096);
static_assert(convert_gyro(1024) == 14);
@ -318,10 +337,10 @@ bool has_motion(const uni_gamepad_t& gamepad) {
return false;
}
SwitchInputState map_gamepad(const uni_gamepad_t& gamepad,
bool swap_abxy,
bool motion_enabled) {
SwitchInputState state = make_neutral_state();
ControllerState map_gamepad(const uni_gamepad_t& gamepad,
bool swap_abxy,
bool motion_enabled) {
ControllerState state = make_neutral_state();
state.dpad_up = (gamepad.dpad & DPAD_UP) != 0;
state.dpad_down = (gamepad.dpad & DPAD_DOWN) != 0;
@ -329,46 +348,52 @@ SwitchInputState map_gamepad(const uni_gamepad_t& gamepad,
state.dpad_right = (gamepad.dpad & DPAD_RIGHT) != 0;
// Bluepad32's A/B/X/Y are positional: south/east/west/north.
state.button_b = (gamepad.buttons & BUTTON_A) != 0;
state.button_a = (gamepad.buttons & BUTTON_B) != 0;
state.button_y = (gamepad.buttons & BUTTON_X) != 0;
state.button_x = (gamepad.buttons & BUTTON_Y) != 0;
state.button_south = (gamepad.buttons & BUTTON_A) != 0;
state.button_east = (gamepad.buttons & BUTTON_B) != 0;
state.button_west = (gamepad.buttons & BUTTON_X) != 0;
state.button_north = (gamepad.buttons & BUTTON_Y) != 0;
if (swap_abxy) {
bool temporary = state.button_a;
state.button_a = state.button_b;
state.button_b = temporary;
temporary = state.button_x;
state.button_x = state.button_y;
state.button_y = temporary;
bool temporary = state.button_east;
state.button_east = state.button_south;
state.button_south = temporary;
temporary = state.button_north;
state.button_north = state.button_west;
state.button_west = temporary;
}
state.button_l = (gamepad.buttons & BUTTON_SHOULDER_L) != 0;
state.button_r = (gamepad.buttons & BUTTON_SHOULDER_R) != 0;
state.button_zl = (gamepad.buttons & BUTTON_TRIGGER_L) != 0 || gamepad.brake >= kTriggerThreshold;
state.button_zr = (gamepad.buttons & BUTTON_TRIGGER_R) != 0 || gamepad.throttle >= kTriggerThreshold;
state.button_l3 = (gamepad.buttons & BUTTON_THUMB_L) != 0;
state.button_r3 = (gamepad.buttons & BUTTON_THUMB_R) != 0;
state.button_left_shoulder = (gamepad.buttons & BUTTON_SHOULDER_L) != 0;
state.button_right_shoulder = (gamepad.buttons & BUTTON_SHOULDER_R) != 0;
state.left_trigger =
(gamepad.buttons & BUTTON_TRIGGER_L) != 0
? UINT16_MAX
: scale_trigger(gamepad.brake);
state.right_trigger =
(gamepad.buttons & BUTTON_TRIGGER_R) != 0
? UINT16_MAX
: scale_trigger(gamepad.throttle);
state.button_left_stick = (gamepad.buttons & BUTTON_THUMB_L) != 0;
state.button_right_stick = (gamepad.buttons & BUTTON_THUMB_R) != 0;
state.button_minus = (gamepad.misc_buttons & MISC_BUTTON_SELECT) != 0;
state.button_plus = (gamepad.misc_buttons & MISC_BUTTON_START) != 0;
state.button_home = (gamepad.misc_buttons & MISC_BUTTON_SYSTEM) != 0;
state.button_select = (gamepad.misc_buttons & MISC_BUTTON_SELECT) != 0;
state.button_start = (gamepad.misc_buttons & MISC_BUTTON_START) != 0;
state.button_system = (gamepad.misc_buttons & MISC_BUTTON_SYSTEM) != 0;
state.button_capture = (gamepad.misc_buttons & MISC_BUTTON_CAPTURE) != 0;
state.lx = scale_stick(gamepad.axis_x);
state.ly = scale_stick(gamepad.axis_y);
state.rx = scale_stick(gamepad.axis_rx);
state.ry = scale_stick(gamepad.axis_ry);
state.left_stick_x = scale_axis(gamepad.axis_x);
state.left_stick_y = scale_axis(gamepad.axis_y);
state.right_stick_x = scale_axis(gamepad.axis_rx);
state.right_stick_y = scale_axis(gamepad.axis_ry);
if (motion_enabled && has_motion(gamepad)) {
// Dependency patches normalize both arrays to SDL3 PlayStation axes.
SwitchImuSample sample{};
ControllerMotionSample sample{};
sample.accel_x = convert_accel(-static_cast<int64_t>(gamepad.accel[2]));
sample.accel_y = convert_accel(-static_cast<int64_t>(gamepad.accel[0]));
sample.accel_z = convert_accel(gamepad.accel[1]);
sample.gyro_x = convert_gyro(-static_cast<int64_t>(gamepad.gyro[2]));
sample.gyro_y = convert_gyro(-static_cast<int64_t>(gamepad.gyro[0]));
sample.gyro_z = convert_gyro(gamepad.gyro[1]);
state.imu_sample_count = 3;
for (SwitchImuSample& destination : state.imu_samples) {
state.motion_sample_count = 3;
for (ControllerMotionSample& destination : state.motion_samples) {
destination = sample;
}
}
@ -753,7 +778,7 @@ void process_rumble_timer(btstack_timer_source_t* timer) {
envelope.rumble.low_frequency_magnitude == 0 &&
envelope.rumble.high_frequency_magnitude == 0;
device->report_parser.play_dual_rumble(
device, 0, stop ? 0 : kRumbleDurationMs,
device, 0, stop ? 0 : host_rumble_duration_ms(),
envelope.rumble.high_frequency_magnitude,
envelope.rumble.low_frequency_magnitude);
}
@ -1084,7 +1109,8 @@ void bluepad32_input_backend_pairing_snapshot(
}
bool bluepad32_input_backend_snapshot(uint8_t slot_index, SwitchInputState* out) {
bool bluepad32_input_backend_snapshot(uint8_t slot_index,
ControllerState* out) {
if (out == nullptr || !valid_slot(slot_index)) {
return false;
}
@ -1100,7 +1126,7 @@ bool bluepad32_input_backend_snapshot(uint8_t slot_index, SwitchInputState* out)
critical_section_exit(&g_state_lock);
if (generation == g_consumed_generation[slot_index]) {
out->imu_sample_count = 0;
out->motion_sample_count = 0;
}
g_last_snapshot_generation[slot_index] = generation;
return controller_active;
@ -1113,8 +1139,8 @@ void bluepad32_input_backend_report_sent(uint8_t slot_index) {
g_consumed_generation[slot_index] = g_last_snapshot_generation[slot_index];
}
void bluepad32_input_backend_queue_rumble(uint8_t slot_index,
const SwitchRumbleOutput& rumble) {
void bluepad32_input_backend_queue_rumble(
uint8_t slot_index, const ControllerRumbleOutput& rumble) {
if (!g_initialized || !valid_slot(slot_index)) {
return;
}

View file

@ -2,8 +2,9 @@
#include <stdint.h>
#include "controller_color.h"
#include "controller_state.h"
#include "switch_haptics.h"
#include "switch_pro_driver.h"
constexpr uint8_t BLUEPAD32_INPUT_BACKEND_SLOT_COUNT = 4;
constexpr uint8_t BLUEPAD32_PAIRING_RECORD_CAPACITY = 16;
@ -37,10 +38,10 @@ void bluepad32_input_backend_init();
void bluepad32_input_backend_start();
void bluepad32_input_backend_open_pairing_window();
void bluepad32_input_backend_clear_pairings();
bool bluepad32_input_backend_snapshot(uint8_t slot, SwitchInputState* out);
bool bluepad32_input_backend_snapshot(uint8_t slot, ControllerState* out);
void bluepad32_input_backend_request_pairing_snapshot();
void bluepad32_input_backend_pairing_snapshot(
Bluepad32PairingSnapshot* out);
void bluepad32_input_backend_report_sent(uint8_t slot);
void bluepad32_input_backend_queue_rumble(uint8_t slot,
const SwitchRumbleOutput& rumble);
void bluepad32_input_backend_queue_rumble(
uint8_t slot, const ControllerRumbleOutput& rumble);

View file

@ -13,11 +13,18 @@ SCRIPT_DIR = Path(__file__).resolve().parent
CONFIG_FILE = SCRIPT_DIR / "controller_color_config.h"
BUILD_DIR = SCRIPT_DIR / "build"
AIO_BUILD_DIR = SCRIPT_DIR / "build-aio"
FEASIBILITY_BUILD_DIR = SCRIPT_DIR / "build-feasibility"
FIRMWARE_DIR = SCRIPT_DIR / "firmware"
FIRMWARE_ELF_PATH = FIRMWARE_DIR / "switch-pico.elf"
FIRMWARE_UF2_PATH = FIRMWARE_DIR / "switch-pico.uf2"
AIO_FIRMWARE_ELF_PATH = FIRMWARE_DIR / "switch-pico-aio.elf"
AIO_FIRMWARE_UF2_PATH = FIRMWARE_DIR / "switch-pico-aio.uf2"
FEASIBILITY_FIRMWARE_ELF_PATH = (
FIRMWARE_DIR / "switch-pico-adapter-feasibility.elf"
)
FEASIBILITY_FIRMWARE_UF2_PATH = (
FIRMWARE_DIR / "switch-pico-adapter-feasibility.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()
@ -34,11 +41,17 @@ def parse_args():
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="Default behavior leaves controller_color_config.h unchanged.",
)
parser.add_argument(
mode_group = parser.add_mutually_exclusive_group()
mode_group.add_argument(
"--aio",
action="store_true",
help="Build and flash the Pico 2 W Bluepad32 all-in-one firmware.",
)
mode_group.add_argument(
"--adapter-feasibility",
action="store_true",
help="Build and flash the Pico 2 W automatic Switch/XInput prototype.",
)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"--random-grip-color",
@ -118,19 +131,22 @@ def resolve_picotool():
def build(
aio,
adapter_feasibility,
build_dir,
elf_path,
uf2_path,
firmware_elf_path,
firmware_uf2_path,
):
if aio:
if aio or adapter_feasibility:
run_cmd([sys.executable, str(SCRIPT_DIR / "tools" / "prepare_bluepad32.py")])
definitions = [
"-DSWITCH_PICO_LOG=OFF",
"-DPICO_BOARD=pico2_w",
"-DSWITCH_PICO_INPUT_BACKEND=BLUEPAD32",
]
if adapter_feasibility:
definitions.append("-DSWITCH_PICO_ADAPTER_FEASIBILITY=ON")
else:
definitions = [
"-DSWITCH_PICO_LOG=OFF",
@ -196,7 +212,13 @@ def main():
update_grip_colors(color)
print(f"Grip color set to #{color} in {CONFIG_FILE.name}")
if args.aio:
if args.adapter_feasibility:
build_dir = FEASIBILITY_BUILD_DIR
elf_path = FEASIBILITY_BUILD_DIR / "switch-pico.elf"
uf2_path = FEASIBILITY_BUILD_DIR / "switch-pico.uf2"
firmware_elf_path = FEASIBILITY_FIRMWARE_ELF_PATH
firmware_uf2_path = FEASIBILITY_FIRMWARE_UF2_PATH
elif args.aio:
build_dir = AIO_BUILD_DIR
elf_path = AIO_BUILD_DIR / "switch-pico.elf"
uf2_path = AIO_BUILD_DIR / "switch-pico.uf2"
@ -211,13 +233,17 @@ def main():
build(
args.aio,
args.adapter_feasibility,
build_dir,
elf_path,
uf2_path,
firmware_elf_path,
firmware_uf2_path,
)
flash(elf_path, allow_elf_override=not args.aio)
flash(
elf_path,
allow_elf_override=not args.aio and not args.adapter_feasibility,
)
if __name__ == "__main__":
main()

42
controller_color.h Normal file
View file

@ -0,0 +1,42 @@
#pragma once
#include <stdint.h>
struct SwitchRgbColor {
uint8_t red;
uint8_t green;
uint8_t blue;
};
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)};
}
SwitchRgbColor switch_pro_get_slot_color(uint8_t instance);
SwitchRgbColor switch_pro_get_slot_light_color(uint8_t instance);

92
controller_state.h Normal file
View file

@ -0,0 +1,92 @@
#pragma once
#include <stdint.h>
constexpr uint8_t CONTROLLER_MOTION_SAMPLE_CAPACITY = 3;
constexpr uint16_t CONTROLLER_AXIS_UNSIGNED_MIN = 0;
constexpr uint16_t CONTROLLER_AXIS_UNSIGNED_CENTER =
static_cast<uint16_t>(UINT16_MAX / 2u + 1u);
constexpr uint16_t CONTROLLER_AXIS_UNSIGNED_MAX = UINT16_MAX;
constexpr uint16_t CONTROLLER_TRIGGER_MIN = 0;
constexpr uint16_t CONTROLLER_TRIGGER_MAX = UINT16_MAX;
constexpr uint8_t CONTROLLER_TRIGGER_TO_U8_SHIFT = 8;
// Motion uses the existing fixed-point units carried by the UART protocol and
// consumed by the Switch serializer. Axis names are controller-relative.
struct ControllerMotionSample {
int16_t accel_x;
int16_t accel_y;
int16_t accel_z;
int16_t gyro_x;
int16_t gyro_y;
int16_t gyro_z;
};
// Protocol-neutral controller state. Face buttons are positional, sticks are
// signed with zero at rest, and triggers retain their full analog range.
struct ControllerState {
bool dpad_up;
bool dpad_down;
bool dpad_left;
bool dpad_right;
bool button_south;
bool button_east;
bool button_west;
bool button_north;
bool button_left_shoulder;
bool button_right_shoulder;
bool button_select;
bool button_start;
bool button_system;
bool button_capture;
bool button_left_stick;
bool button_right_stick;
uint16_t left_trigger;
uint16_t right_trigger;
int16_t left_stick_x;
int16_t left_stick_y;
int16_t right_stick_x;
int16_t right_stick_y;
uint8_t motion_sample_count;
ControllerMotionSample
motion_samples[CONTROLLER_MOTION_SAMPLE_CAPACITY];
};
constexpr uint16_t controller_axis_to_unsigned(int16_t value) {
return static_cast<uint16_t>(
static_cast<int32_t>(value) - INT16_MIN);
}
constexpr int16_t controller_axis_from_unsigned(uint16_t value) {
return static_cast<int16_t>(
static_cast<int32_t>(value) + INT16_MIN);
}
constexpr uint8_t controller_trigger_to_u8(uint16_t value) {
return static_cast<uint8_t>(
value >> CONTROLLER_TRIGGER_TO_U8_SHIFT);
}
constexpr ControllerState controller_neutral_state() {
return {};
}
static_assert(controller_axis_to_unsigned(INT16_MIN) ==
CONTROLLER_AXIS_UNSIGNED_MIN);
static_assert(controller_axis_to_unsigned(0) ==
CONTROLLER_AXIS_UNSIGNED_CENTER);
static_assert(controller_axis_to_unsigned(INT16_MAX) ==
CONTROLLER_AXIS_UNSIGNED_MAX);
static_assert(controller_axis_from_unsigned(
CONTROLLER_AXIS_UNSIGNED_MIN) == INT16_MIN);
static_assert(controller_axis_from_unsigned(
CONTROLLER_AXIS_UNSIGNED_CENTER) == 0);
static_assert(controller_axis_from_unsigned(
CONTROLLER_AXIS_UNSIGNED_MAX) == INT16_MAX);
static_assert(controller_trigger_to_u8(0x7fff) == 0x7f);
static_assert(controller_trigger_to_u8(0x8000) == 0x80);
static_assert(controller_trigger_to_u8(CONTROLLER_TRIGGER_MAX) == UINT8_MAX);

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -259,10 +259,10 @@ index 0265f93..5c0f2bb 100644
return (psmove_instance_t*)&d->parser_data[0];
}
diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/components/bluepad32/parser/uni_hid_parser_switch.c
index 599fc35..9f073b4 100644
index 599fc35..0105c53 100644
--- a/src/components/bluepad32/parser/uni_hid_parser_switch.c
+++ b/src/components/bluepad32/parser/uni_hid_parser_switch.c
@@ -51,13 +51,15 @@ static const int16_t DEFAULT_ACCEL_OFFSET = 0;
@@ -51,13 +51,22 @@ static const int16_t DEFAULT_ACCEL_OFFSET = 0;
static const int16_t DEFAULT_ACCEL_SCALE = 16384;
static const int16_t DEFAULT_GYRO_OFFSET = 0;
static const int16_t DEFAULT_GYRO_SCALE = 13371;
@ -276,10 +276,17 @@ index 599fc35..9f073b4 100644
#define SWITCH_DUMP_ROM_DATA_SIZE 24 // Max size is 24
#define SWITCH_SETUP_TIMEOUT_MS 800
+#define SWITCH_RUMBLE_REFRESH_MS 40
+#define SWITCH_RUMBLE_LOW_FREQUENCY_HZ 453
+#define SWITCH_RUMBLE_LEFT_HIGH_FREQUENCY_HZ 135
+#define SWITCH_RUMBLE_RIGHT_HIGH_FREQUENCY_HZ 99
+#define SWITCH_RUMBLE_AMPLITUDE_MAX 1003
+#define SWITCH_RUMBLE_MAGNITUDE_MAX UINT8_MAX
+#define SWITCH_RUMBLE_AMPLITUDE_ROUNDING_BIAS \
+ (SWITCH_RUMBLE_MAGNITUDE_MAX / 2)
#if ENABLE_SPI_FLASH_DUMP
static const uint32_t SWITCH_DUMP_ROM_DATA_ADDR_START = 0x20000;
static const uint32_t SWITCH_DUMP_ROM_DATA_ADDR_END = 0x30000;
@@ -72,6 +74,7 @@ enum switch_state {
@@ -72,6 +81,7 @@ enum switch_state {
STATE_READ_FACTORY_IMU_CALIBRATION, // Factory IMU calibration info
STATE_SET_FULL_REPORT, // Request report 0x30
STATE_ENABLE_IMU, // Enable/Disable gyro/accel
@ -287,7 +294,7 @@ index 599fc35..9f073b4 100644
STATE_DUMP_FLASH, // Dump SPI Flash memory
STATE_UPDATE_LED, // Update LEDs
STATE_READY, // Gamepad setup ready!
@@ -111,6 +114,7 @@ enum switch_subcmd {
@@ -111,6 +121,7 @@ enum switch_subcmd {
SUBCMD_SPI_FLASH_READ = 0x10,
SUBCMD_SET_PLAYER_LEDS = 0x30,
SUBCMD_ENABLE_IMU = 0x40,
@ -295,7 +302,7 @@ index 599fc35..9f073b4 100644
};
typedef enum {
@@ -137,6 +141,7 @@ typedef struct switch_instance_s {
@@ -137,6 +148,7 @@ typedef struct switch_instance_s {
// Although technically, we can use one timer for delay and duration, easier to debug/maintain if we have two.
btstack_timer_source_t rumble_timer_duration;
btstack_timer_source_t rumble_timer_delayed_start;
@ -303,7 +310,7 @@ index 599fc35..9f073b4 100644
switch_state_rumble_t rumble_state;
btstack_timer_source_t setup_timer;
@@ -322,6 +327,7 @@ static void fsm_read_user_stick_calibration(struct uni_hid_device_s* d);
@@ -322,6 +334,7 @@ static void fsm_read_user_stick_calibration(struct uni_hid_device_s* d);
static void fsm_read_factory_imu_calibration(struct uni_hid_device_s* d);
static void fsm_set_full_report(struct uni_hid_device_s* d);
static void fsm_enable_imu(struct uni_hid_device_s* d);
@ -311,7 +318,7 @@ index 599fc35..9f073b4 100644
static void fsm_update_led(struct uni_hid_device_s* d);
static void fsm_ready(struct uni_hid_device_s* d);
static void process_reply_read_spi_dump(struct uni_hid_device_s* d, const uint8_t* data, int len);
@@ -333,11 +339,16 @@ static void process_reply_set_report_mode(struct uni_hid_device_s* d, const stru
@@ -333,11 +346,16 @@ static void process_reply_set_report_mode(struct uni_hid_device_s* d, const stru
static void process_reply_spi_flash_read(struct uni_hid_device_s* d, const struct switch_report_21_s* r, int len);
static void process_reply_set_player_leds(struct uni_hid_device_s* d, const struct switch_report_21_s* r, int len);
static void process_reply_enable_imu(struct uni_hid_device_s* d, const struct switch_report_21_s* r, int len);
@ -328,7 +335,7 @@ index 599fc35..9f073b4 100644
static void switch_play_dual_rumble_now(uni_hid_device_t* d,
uint16_t duration_ms,
uint8_t weak_magnitude,
@@ -451,6 +462,10 @@ static void process_fsm(struct uni_hid_device_s* d) {
@@ -451,6 +469,10 @@ static void process_fsm(struct uni_hid_device_s* d) {
break;
case STATE_ENABLE_IMU:
logd("STATE_ENABLE_IMU\n");
@ -339,7 +346,7 @@ index 599fc35..9f073b4 100644
fsm_dump_rom(d);
break;
case STATE_DUMP_FLASH:
@@ -725,6 +740,12 @@ static void process_reply_enable_imu(struct uni_hid_device_s* d, const struct sw
@@ -725,6 +747,12 @@ static void process_reply_enable_imu(struct uni_hid_device_s* d, const struct sw
ARG_UNUSED(r);
ARG_UNUSED(len);
}
@ -352,7 +359,7 @@ index 599fc35..9f073b4 100644
// Process 0x21 input report: SWITCH_INPUT_SUBCMD_REPLY
static void process_input_subcmd_reply(struct uni_hid_device_s* d, const uint8_t* report, int len) {
@@ -752,6 +773,9 @@ static void process_input_subcmd_reply(struct uni_hid_device_s* d, const uint8_t
@@ -752,6 +780,9 @@ static void process_input_subcmd_reply(struct uni_hid_device_s* d, const uint8_t
case SUBCMD_ENABLE_IMU:
process_reply_enable_imu(d, r, len);
break;
@ -362,7 +369,7 @@ index 599fc35..9f073b4 100644
default:
loge("Switch: Error, unexpected subcmd_id=0x%02x in report 0x21\n", r->subcmd_id);
break;
@@ -823,19 +847,26 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) {
@@ -823,19 +854,26 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) {
switch_instance_t* ins = get_switch_instance(d);
uni_controller_t* ctl = &d->controller;
@ -398,7 +405,7 @@ index 599fc35..9f073b4 100644
if (ins->controller_type == SWITCH_CONTROLLER_TYPE_JCR) {
accel[1] = -accel[1];
accel[2] = -accel[2];
@@ -843,10 +874,13 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) {
@@ -843,10 +881,13 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) {
gyro[2] = -gyro[2];
}
@ -416,7 +423,7 @@ index 599fc35..9f073b4 100644
}
// Process 0x30 input report: SWITCH_INPUT_IMU_DATA
@@ -1172,6 +1206,18 @@ static void fsm_enable_imu(struct uni_hid_device_s* d) {
@@ -1172,6 +1213,18 @@ static void fsm_enable_imu(struct uni_hid_device_s* d) {
req->data[0] = (ins->mode == SWITCH_MODE_IMU);
send_subcmd(d, req, sizeof(out));
}
@ -435,18 +442,21 @@ index 599fc35..9f073b4 100644
static void fsm_update_led(struct uni_hid_device_s* d) {
switch_instance_t* ins = get_switch_instance(d);
@@ -1203,6 +1249,10 @@ static struct switch_rumble_freq_data find_rumble_freq(uint16_t freq) {
@@ -1203,6 +1256,13 @@ static struct switch_rumble_freq_data find_rumble_freq(uint16_t freq) {
return rumble_freqs[i];
}
+static uint16_t switch_magnitude_to_amp(uint8_t magnitude) {
+ return (uint16_t)(((uint32_t)magnitude * 1003 + 127) / 255);
+ return (uint16_t)(
+ ((uint32_t)magnitude * SWITCH_RUMBLE_AMPLITUDE_MAX +
+ SWITCH_RUMBLE_AMPLITUDE_ROUNDING_BIAS) /
+ SWITCH_RUMBLE_MAGNITUDE_MAX);
+}
+
static struct switch_rumble_amp_data find_rumble_amp(uint16_t amp) {
unsigned int i = 0;
if (amp > rumble_amps[0].amp) {
@@ -1259,6 +1309,7 @@ void uni_hid_parser_switch_play_dual_rumble(struct uni_hid_device_s* d,
@@ -1259,6 +1319,7 @@ void uni_hid_parser_switch_play_dual_rumble(struct uni_hid_device_s* d,
break;
case SWITCH_STATE_RUMBLE_IN_PROGRESS:
btstack_run_loop_remove_timer(&ins->rumble_timer_duration);
@ -454,7 +464,7 @@ index 599fc35..9f073b4 100644
break;
default:
// Do nothing
@@ -1366,6 +1417,7 @@ static void switch_stop_rumble_now(uni_hid_device_t* d) {
@@ -1366,6 +1427,7 @@ static void switch_stop_rumble_now(uni_hid_device_t* d) {
// No need to protect it with a mutex since it runs in the same main thread
assert(ins->rumble_state == SWITCH_STATE_RUMBLE_IN_PROGRESS);
@ -462,7 +472,7 @@ index 599fc35..9f073b4 100644
ins->rumble_state = SWITCH_STATE_RUMBLE_DISABLED;
struct switch_subcmd_request req = {0};
@@ -1379,6 +1431,22 @@ static void switch_stop_rumble_now(uni_hid_device_t* d) {
@@ -1379,6 +1441,24 @@ static void switch_stop_rumble_now(uni_hid_device_t* d) {
send_subcmd(d, (struct switch_subcmd_request*)&req, sizeof(req) - 1);
}
@ -474,9 +484,11 @@ index 599fc35..9f073b4 100644
+ };
+ // Fixed frequencies match the standard Switch LRA envelope and the
+ // 8BitDo Switch-mode implementation. Magnitudes control amplitude only.
+ switch_encode_rumble(req.rumble_left, 453, 135,
+ switch_encode_rumble(req.rumble_left, SWITCH_RUMBLE_LOW_FREQUENCY_HZ,
+ SWITCH_RUMBLE_LEFT_HIGH_FREQUENCY_HZ,
+ switch_magnitude_to_amp(weak_magnitude));
+ switch_encode_rumble(req.rumble_right, 453, 99,
+ switch_encode_rumble(req.rumble_right, SWITCH_RUMBLE_LOW_FREQUENCY_HZ,
+ SWITCH_RUMBLE_RIGHT_HIGH_FREQUENCY_HZ,
+ switch_magnitude_to_amp(strong_magnitude));
+ // Rumble request don't include the last byte of "switch_subcmd_request": subcmd_id
+ send_subcmd(d, &req, sizeof(req) - 1);
@ -485,7 +497,7 @@ index 599fc35..9f073b4 100644
static void switch_play_dual_rumble_now(uni_hid_device_t* d,
uint16_t duration_ms,
uint8_t weak_magnitude,
@@ -1391,14 +1459,17 @@ static void switch_play_dual_rumble_now(uni_hid_device_t* d,
@@ -1391,14 +1471,17 @@ static void switch_play_dual_rumble_now(uni_hid_device_t* d,
return;
}
@ -510,7 +522,7 @@ index 599fc35..9f073b4 100644
// Set timer to turn off rumble
ins->rumble_timer_duration.process = &on_switch_set_rumble_off;
@@ -1414,6 +1485,20 @@ static void on_switch_set_rumble_on(btstack_timer_source_t* ts) {
@@ -1414,6 +1497,20 @@ static void on_switch_set_rumble_on(btstack_timer_source_t* ts) {
switch_play_dual_rumble_now(d, ins->rumble_duration_ms, ins->rumble_weak_magnitude, ins->rumble_strong_magnitude);
}

View file

@ -34,12 +34,12 @@
static_assert(SWITCH_PICO_HID_INSTANCE_COUNT ==
BLUEPAD32_INPUT_BACKEND_SLOT_COUNT);
static bool g_last_ready[BLUEPAD32_INPUT_BACKEND_SLOT_COUNT]{};
static SwitchInputState
static ControllerState
g_user_states[BLUEPAD32_INPUT_BACKEND_SLOT_COUNT]{};
#else
static constexpr uint8_t SWITCH_HID_INSTANCE = 0;
static bool g_last_ready = false;
static SwitchInputState g_user_state;
static ControllerState g_user_state;
#endif
static bool g_last_mounted = false;
@ -53,17 +53,12 @@ static void init_uart_input() {
}
#endif
static SwitchInputState neutral_input() {
SwitchInputState state{};
state.lx = SWITCH_PRO_JOYSTICK_MID;
state.ly = SWITCH_PRO_JOYSTICK_MID;
state.rx = SWITCH_PRO_JOYSTICK_MID;
state.ry = SWITCH_PRO_JOYSTICK_MID;
return state;
static ControllerState neutral_input() {
return controller_neutral_state();
}
#ifndef SWITCH_PICO_BLUEPAD32
static void send_rumble_uart_frame(const SwitchRumbleOutput& rumble) {
static void send_rumble_uart_frame(const ControllerRumbleOutput& rumble) {
uint8_t frame[5] = {
UART_RUMBLE_HEADER,
UART_RUMBLE_TYPE,
@ -80,7 +75,7 @@ static void send_rumble_uart_frame(const SwitchRumbleOutput& rumble) {
#endif
static void on_rumble_from_switch(uint8_t instance,
const SwitchRumbleOutput& rumble) {
const ControllerRumbleOutput& rumble) {
#ifdef SWITCH_PICO_BLUEPAD32
if (instance >= BLUEPAD32_INPUT_BACKEND_SLOT_COUNT) {
return;
@ -137,30 +132,33 @@ static bool poll_uart_frames() {
}
if (expected_len > 0 && index >= expected_len) {
SwitchInputState parsed{};
ControllerState parsed{};
if (switch_pro_apply_uart_packet(buffer, expected_len, parsed)) {
g_user_state = parsed;
new_data = true;
LOG_PRINTF("[UART] packet buttons=0x%04x hat=%u lx=%u ly=%u rx=%u ry=%u\n",
(parsed.button_a ? SWITCH_PRO_MASK_A : 0) |
(parsed.button_b ? SWITCH_PRO_MASK_B : 0) |
(parsed.button_x ? SWITCH_PRO_MASK_X : 0) |
(parsed.button_y ? SWITCH_PRO_MASK_Y : 0) |
(parsed.button_l ? SWITCH_PRO_MASK_L : 0) |
(parsed.button_r ? SWITCH_PRO_MASK_R : 0) |
(parsed.button_zl ? SWITCH_PRO_MASK_ZL : 0) |
(parsed.button_zr ? SWITCH_PRO_MASK_ZR : 0) |
(parsed.button_plus? SWITCH_PRO_MASK_PLUS: 0) |
(parsed.button_minus?SWITCH_PRO_MASK_MINUS:0) |
(parsed.button_home?SWITCH_PRO_MASK_HOME:0) |
(parsed.button_east ? SWITCH_PRO_MASK_A : 0) |
(parsed.button_south ? SWITCH_PRO_MASK_B : 0) |
(parsed.button_north ? SWITCH_PRO_MASK_X : 0) |
(parsed.button_west ? SWITCH_PRO_MASK_Y : 0) |
(parsed.button_left_shoulder ? SWITCH_PRO_MASK_L : 0) |
(parsed.button_right_shoulder ? SWITCH_PRO_MASK_R : 0) |
(parsed.left_trigger ? SWITCH_PRO_MASK_ZL : 0) |
(parsed.right_trigger ? SWITCH_PRO_MASK_ZR : 0) |
(parsed.button_start? SWITCH_PRO_MASK_PLUS: 0) |
(parsed.button_select?SWITCH_PRO_MASK_MINUS:0) |
(parsed.button_system?SWITCH_PRO_MASK_HOME:0) |
(parsed.button_capture?SWITCH_PRO_MASK_CAPTURE:0) |
(parsed.button_l3 ? SWITCH_PRO_MASK_L3 : 0) |
(parsed.button_r3 ? SWITCH_PRO_MASK_R3 : 0),
(parsed.button_left_stick ? SWITCH_PRO_MASK_L3 : 0) |
(parsed.button_right_stick ? SWITCH_PRO_MASK_R3 : 0),
parsed.dpad_up ? SWITCH_PRO_HAT_UP :
parsed.dpad_down ? SWITCH_PRO_HAT_DOWN :
parsed.dpad_left ? SWITCH_PRO_HAT_LEFT :
parsed.dpad_right ? SWITCH_PRO_HAT_RIGHT : SWITCH_PRO_HAT_NOTHING,
parsed.lx >> 8, parsed.ly >> 8, parsed.rx >> 8, parsed.ry >> 8);
controller_axis_to_unsigned(parsed.left_stick_x) >> 8,
controller_axis_to_unsigned(parsed.left_stick_y) >> 8,
controller_axis_to_unsigned(parsed.right_stick_x) >> 8,
controller_axis_to_unsigned(parsed.right_stick_y) >> 8);
}
index = 0;
expected_len = 0;
@ -317,7 +315,7 @@ int main() {
#else
bool new_data = poll_uart_frames(); // Pull controller state from UART1
(void)new_data;
SwitchInputState state = g_user_state;
ControllerState state = g_user_state;
switch_pro_set_input(SWITCH_HID_INSTANCE, state);
(void)switch_pro_task(SWITCH_HID_INSTANCE);
#endif

View file

@ -289,7 +289,7 @@ uint8_t SwitchHapticsDecoder::amplitude_to_magnitude(uint8_t amplitude_index) {
return static_cast<uint8_t>(magnitude);
}
SwitchRumbleOutput SwitchHapticsDecoder::decode(const uint8_t payload[8]) {
ControllerRumbleOutput SwitchHapticsDecoder::decode(const uint8_t payload[8]) {
AmplitudePeak peaks[2] = {
{actuators_[0].low_amplitude, actuators_[0].high_amplitude},
{actuators_[1].low_amplitude, actuators_[1].high_amplitude},

View file

@ -4,10 +4,13 @@
#include <stddef.h>
#include <stdint.h>
struct SwitchRumbleOutput {
struct ControllerRumbleOutput {
uint8_t low_frequency_magnitude;
uint8_t high_frequency_magnitude;
};
typedef void (*ControllerRumbleCallback)(
uint8_t instance, const ControllerRumbleOutput& rumble);
size_t normalize_switch_output_report(uint8_t report_id,
const uint8_t* payload,
@ -19,7 +22,7 @@ public:
SwitchHapticsDecoder();
void reset();
SwitchRumbleOutput decode(const uint8_t payload[8]);
ControllerRumbleOutput decode(const uint8_t payload[8]);
private:
struct ActuatorState {

View file

@ -25,7 +25,6 @@
// (~66.7Hz). Emitting the 3-frame 0x30 faster makes the console over-integrate
// gyro (3 frames assumed 5ms apart delivered too often) => wild camera swing.
#define SWITCH_PRO_IMU_REPORT_TIMER 15
enum class SwitchImuMode : uint8_t {
Off = 0,
Raw = 1,
@ -43,7 +42,7 @@ struct MotionQuaternion {
};
struct SwitchProContext {
SwitchInputState input_state{};
ControllerState input_state{};
uint8_t report_buffer[SWITCH_PRO_ENDPOINT_SIZE]{};
SwitchProReport switch_report{};
uint8_t last_report_counter = 0;
@ -65,7 +64,7 @@ struct SwitchProContext {
uint16_t right_min_y = 0;
uint16_t right_max_x = 0;
uint16_t right_max_y = 0;
SwitchRumbleCallback rumble_callback = nullptr;
ControllerRumbleCallback rumble_callback = nullptr;
SwitchHapticsDecoder rumble_decoder{};
MotionQuaternion motion_quaternion{0.0f, 0.0f, 0.0f, 1.0f, 0, 0, 0};
};
@ -262,7 +261,7 @@ static void write_bits_le(uint8_t* dst, uint16_t bit_offset, uint32_t value,
}
static void integrate_motion_sample(SwitchProContext& context,
const SwitchImuSample& sample) {
const ControllerMotionSample& sample) {
constexpr float sample_dt = 0.005f;
constexpr float gyro_rad_per_lsb = 1.0f / 818.5f;
MotionQuaternion& quaternion = context.motion_quaternion;
@ -310,19 +309,21 @@ static void integrate_motion_sample(SwitchProContext& context,
}
static void fill_raw_imu_report_data(SwitchProContext& context,
const SwitchInputState& state) {
if (state.imu_sample_count == 0) {
const ControllerState& state) {
if (state.motion_sample_count == 0) {
memset(context.switch_report.imuData, 0x00,
sizeof(context.switch_report.imuData));
return;
}
uint8_t sample_count =
state.imu_sample_count > 3 ? 3 : state.imu_sample_count;
state.motion_sample_count > CONTROLLER_MOTION_SAMPLE_CAPACITY
? CONTROLLER_MOTION_SAMPLE_CAPACITY
: state.motion_sample_count;
uint8_t* dst = context.switch_report.imuData;
for (uint8_t i = 0; i < 3; ++i) {
const SwitchImuSample& sample =
(i < sample_count) ? state.imu_samples[i]
: state.imu_samples[sample_count - 1];
for (uint8_t i = 0; i < CONTROLLER_MOTION_SAMPLE_CAPACITY; ++i) {
const ControllerMotionSample& sample =
(i < sample_count) ? state.motion_samples[i]
: state.motion_samples[sample_count - 1];
write_int16_le(dst + 0, sample.accel_x);
write_int16_le(dst + 2, sample.accel_y);
write_int16_le(dst + 4, sample.accel_z);
@ -333,16 +334,18 @@ static void fill_raw_imu_report_data(SwitchProContext& context,
}
}
static void fill_quaternion_imu_report_data(SwitchProContext& context,
const SwitchInputState& state,
uint32_t now_ms) {
if (state.imu_sample_count > 0) {
static void fill_quaternion_imu_report_data(
SwitchProContext& context, const ControllerState& state,
uint32_t now_ms) {
if (state.motion_sample_count > 0) {
uint8_t sample_count =
state.imu_sample_count > 3 ? 3 : state.imu_sample_count;
for (uint8_t i = 0; i < 3; ++i) {
const SwitchImuSample& sample =
(i < sample_count) ? state.imu_samples[i]
: state.imu_samples[sample_count - 1];
state.motion_sample_count > CONTROLLER_MOTION_SAMPLE_CAPACITY
? CONTROLLER_MOTION_SAMPLE_CAPACITY
: state.motion_sample_count;
for (uint8_t i = 0; i < CONTROLLER_MOTION_SAMPLE_CAPACITY; ++i) {
const ControllerMotionSample& sample =
(i < sample_count) ? state.motion_samples[i]
: state.motion_samples[sample_count - 1];
integrate_motion_sample(context, sample);
}
}
@ -391,7 +394,7 @@ static void fill_quaternion_imu_report_data(SwitchProContext& context,
}
static void fill_imu_report_data(SwitchProContext& context,
const SwitchInputState& state,
const ControllerState& state,
uint32_t now_ms) {
switch (context.imu_mode) {
case SwitchImuMode::Raw:
@ -410,14 +413,8 @@ static void fill_imu_report_data(SwitchProContext& context,
static void update_switch_report_from_state(SwitchProContext& context);
static SwitchInputState make_neutral_state() {
SwitchInputState s{};
s.lx = SWITCH_PRO_JOYSTICK_MID;
s.ly = SWITCH_PRO_JOYSTICK_MID;
s.rx = SWITCH_PRO_JOYSTICK_MID;
s.ry = SWITCH_PRO_JOYSTICK_MID;
s.imu_sample_count = 0;
return s;
static ControllerState make_neutral_state() {
return controller_neutral_state();
}
static void reset_context_runtime(SwitchProContext& context, uint32_t now,
@ -522,7 +519,8 @@ static void forward_decoded_rumble(uint8_t instance,
return;
}
SwitchRumbleOutput rumble = context.rumble_decoder.decode(report + 2);
ControllerRumbleOutput rumble =
context.rumble_decoder.decode(report + 2);
if (context.rumble_callback != nullptr) {
context.rumble_callback(instance, rumble);
}
@ -734,36 +732,42 @@ static void handle_feature_report(SwitchProContext& context,
}
static void update_switch_report_from_state(SwitchProContext& context) {
const SwitchInputState& state = context.input_state;
const ControllerState& state = context.input_state;
SwitchInputReport& inputs = context.switch_report.inputs;
inputs.dpadUp = state.dpad_up;
inputs.dpadDown = state.dpad_down;
inputs.dpadLeft = state.dpad_left;
inputs.dpadRight = state.dpad_right;
inputs.chargingGrip = 1;
inputs.buttonY = state.button_y;
inputs.buttonX = state.button_x;
inputs.buttonB = state.button_b;
inputs.buttonA = state.button_a;
inputs.buttonY = state.button_west;
inputs.buttonX = state.button_north;
inputs.buttonB = state.button_south;
inputs.buttonA = state.button_east;
inputs.buttonRightSR = 0;
inputs.buttonRightSL = 0;
inputs.buttonR = state.button_r;
inputs.buttonZR = state.button_zr;
inputs.buttonMinus = state.button_minus;
inputs.buttonPlus = state.button_plus;
inputs.buttonThumbR = state.button_r3;
inputs.buttonThumbL = state.button_l3;
inputs.buttonHome = state.button_home;
inputs.buttonR = state.button_right_shoulder;
inputs.buttonZR =
state.right_trigger >= SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
inputs.buttonMinus = state.button_select;
inputs.buttonPlus = state.button_start;
inputs.buttonThumbR = state.button_right_stick;
inputs.buttonThumbL = state.button_left_stick;
inputs.buttonHome = state.button_system;
inputs.buttonCapture = state.button_capture;
inputs.buttonLeftSR = 0;
inputs.buttonLeftSL = 0;
inputs.buttonL = state.button_l;
inputs.buttonZL = state.button_zl;
inputs.buttonL = state.button_left_shoulder;
inputs.buttonZL =
state.left_trigger >= SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
uint16_t left_x = scale16To12(state.lx);
uint16_t left_y = scale16To12(state.ly);
uint16_t right_x = scale16To12(state.rx);
uint16_t right_y = scale16To12(state.ry);
uint16_t left_x =
scale16To12(controller_axis_to_unsigned(state.left_stick_x));
uint16_t left_y =
scale16To12(controller_axis_to_unsigned(state.left_stick_y));
uint16_t right_x =
scale16To12(controller_axis_to_unsigned(state.right_stick_x));
uint16_t right_y =
scale16To12(controller_axis_to_unsigned(state.right_stick_y));
inputs.leftStick.setX(
std::min(std::max(left_x, context.left_min_x), context.left_max_x));
@ -809,7 +813,7 @@ void switch_pro_init(uint8_t instance) {
to_ms_since_boot(get_absolute_time()), true);
}
void switch_pro_set_input(uint8_t instance, const SwitchInputState& state) {
void switch_pro_set_input(uint8_t instance, const ControllerState& state) {
SwitchProContext* context = context_for(instance);
if (context != nullptr) {
context->input_state = state;
@ -854,7 +858,7 @@ bool switch_pro_task(uint8_t instance) {
if (tud_hid_n_ready(instance) &&
send_report(instance, *context, 0, &context->switch_report,
sizeof(context->switch_report))) {
context->input_state.imu_sample_count = 0;
context->input_state.motion_sample_count = 0;
regular_report_sent = true;
}
context->last_report_timer = now;
@ -878,18 +882,12 @@ bool switch_pro_task(uint8_t instance) {
}
bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length,
SwitchInputState& out_state) {
ControllerState& out_state) {
if (packet == nullptr) {
return false;
}
// v2 format: 0xAA + 0x02 + payload_len + payload... + checksum
if (length < 12) {
return false;
}
if (packet[0] != 0xAA) {
return false;
}
if (packet[1] != 0x02) {
if (length < 12 || packet[0] != 0xAA || packet[1] != 0x02) {
return false;
}
@ -906,86 +904,115 @@ bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length,
return false;
}
// payload: buttons(2 LE), hat, lx, ly, rx, ry, imu_count, [imu_samples...]
// payload: buttons(2 LE), hat, lx, ly, rx, ry, motion_count,
// [motion_samples...]
if (payload_len < 8) {
return false;
}
SwitchProOutReport out{};
out.buttons = static_cast<uint16_t>(packet[3]) | (static_cast<uint16_t>(packet[4]) << 8);
out.buttons = static_cast<uint16_t>(packet[3]) |
(static_cast<uint16_t>(packet[4]) << 8);
out.hat = packet[5];
out.lx = packet[6];
out.ly = packet[7];
out.rx = packet[8];
out.ry = packet[9];
uint8_t imu_count = packet[10];
if (imu_count > 3) {
imu_count = 3;
uint8_t motion_count = packet[10];
if (motion_count > CONTROLLER_MOTION_SAMPLE_CAPACITY) {
motion_count = CONTROLLER_MOTION_SAMPLE_CAPACITY;
}
uint16_t required_payload_len = static_cast<uint16_t>(8u + static_cast<uint16_t>(imu_count) * 12u);
uint16_t required_payload_len = static_cast<uint16_t>(
8u + static_cast<uint16_t>(motion_count) * 12u);
if (payload_len < required_payload_len) {
return false;
}
auto expand_axis = [](uint8_t v) -> uint16_t {
return static_cast<uint16_t>(v) << 8 | v;
auto expand_axis = [](uint8_t value) -> int16_t {
const uint16_t expanded =
static_cast<uint16_t>(value) << 8 | value;
return controller_axis_from_unsigned(expanded);
};
SwitchInputState state = make_neutral_state();
state.imu_sample_count = imu_count;
auto read_int16 = [](const uint8_t* src) -> int16_t {
return static_cast<int16_t>(static_cast<uint16_t>(src[0]) | (static_cast<uint16_t>(src[1]) << 8));
return static_cast<int16_t>(
static_cast<uint16_t>(src[0]) |
(static_cast<uint16_t>(src[1]) << 8));
};
for (uint8_t i = 0; i < imu_count; ++i) {
ControllerState state = make_neutral_state();
state.motion_sample_count = motion_count;
for (uint8_t i = 0; i < motion_count; ++i) {
const uint8_t* base = &packet[11 + i * 12];
state.imu_samples[i].accel_x = read_int16(base + 0);
state.imu_samples[i].accel_y = read_int16(base + 2);
state.imu_samples[i].accel_z = read_int16(base + 4);
state.imu_samples[i].gyro_x = read_int16(base + 6);
state.imu_samples[i].gyro_y = read_int16(base + 8);
state.imu_samples[i].gyro_z = read_int16(base + 10);
state.motion_samples[i].accel_x = read_int16(base + 0);
state.motion_samples[i].accel_y = read_int16(base + 2);
state.motion_samples[i].accel_z = read_int16(base + 4);
state.motion_samples[i].gyro_x = read_int16(base + 6);
state.motion_samples[i].gyro_y = read_int16(base + 8);
state.motion_samples[i].gyro_z = read_int16(base + 10);
}
switch (out.hat) {
case SWITCH_PRO_HAT_UP: state.dpad_up = true; break;
case SWITCH_PRO_HAT_UPRIGHT: state.dpad_up = true; state.dpad_right = true; break;
case SWITCH_PRO_HAT_RIGHT: state.dpad_right = true; break;
case SWITCH_PRO_HAT_DOWNRIGHT: state.dpad_down = true; state.dpad_right = true; break;
case SWITCH_PRO_HAT_DOWN: state.dpad_down = true; break;
case SWITCH_PRO_HAT_DOWNLEFT: state.dpad_down = true; state.dpad_left = true; break;
case SWITCH_PRO_HAT_LEFT: state.dpad_left = true; break;
case SWITCH_PRO_HAT_UPLEFT: state.dpad_up = true; state.dpad_left = true; break;
default: break;
case SWITCH_PRO_HAT_UP:
state.dpad_up = true;
break;
case SWITCH_PRO_HAT_UPRIGHT:
state.dpad_up = true;
state.dpad_right = true;
break;
case SWITCH_PRO_HAT_RIGHT:
state.dpad_right = true;
break;
case SWITCH_PRO_HAT_DOWNRIGHT:
state.dpad_down = true;
state.dpad_right = true;
break;
case SWITCH_PRO_HAT_DOWN:
state.dpad_down = true;
break;
case SWITCH_PRO_HAT_DOWNLEFT:
state.dpad_down = true;
state.dpad_left = true;
break;
case SWITCH_PRO_HAT_LEFT:
state.dpad_left = true;
break;
case SWITCH_PRO_HAT_UPLEFT:
state.dpad_up = true;
state.dpad_left = true;
break;
default:
break;
}
state.button_y = out.buttons & SWITCH_PRO_MASK_Y;
state.button_x = out.buttons & SWITCH_PRO_MASK_X;
state.button_b = out.buttons & SWITCH_PRO_MASK_B;
state.button_a = out.buttons & SWITCH_PRO_MASK_A;
state.button_r = out.buttons & SWITCH_PRO_MASK_R;
state.button_zr = out.buttons & SWITCH_PRO_MASK_ZR;
state.button_plus = out.buttons & SWITCH_PRO_MASK_PLUS;
state.button_minus = out.buttons & SWITCH_PRO_MASK_MINUS;
state.button_r3 = out.buttons & SWITCH_PRO_MASK_R3;
state.button_l3 = out.buttons & SWITCH_PRO_MASK_L3;
state.button_home = out.buttons & SWITCH_PRO_MASK_HOME;
state.button_capture = out.buttons & SWITCH_PRO_MASK_CAPTURE;
state.button_zl = out.buttons & SWITCH_PRO_MASK_ZL;
state.button_l = out.buttons & SWITCH_PRO_MASK_L;
state.button_west = (out.buttons & SWITCH_PRO_MASK_Y) != 0;
state.button_north = (out.buttons & SWITCH_PRO_MASK_X) != 0;
state.button_south = (out.buttons & SWITCH_PRO_MASK_B) != 0;
state.button_east = (out.buttons & SWITCH_PRO_MASK_A) != 0;
state.button_right_shoulder = (out.buttons & SWITCH_PRO_MASK_R) != 0;
state.right_trigger =
(out.buttons & SWITCH_PRO_MASK_ZR) != 0 ? UINT16_MAX : 0;
state.button_start = (out.buttons & SWITCH_PRO_MASK_PLUS) != 0;
state.button_select = (out.buttons & SWITCH_PRO_MASK_MINUS) != 0;
state.button_right_stick = (out.buttons & SWITCH_PRO_MASK_R3) != 0;
state.button_left_stick = (out.buttons & SWITCH_PRO_MASK_L3) != 0;
state.button_system = (out.buttons & SWITCH_PRO_MASK_HOME) != 0;
state.button_capture = (out.buttons & SWITCH_PRO_MASK_CAPTURE) != 0;
state.left_trigger =
(out.buttons & SWITCH_PRO_MASK_ZL) != 0 ? UINT16_MAX : 0;
state.button_left_shoulder = (out.buttons & SWITCH_PRO_MASK_L) != 0;
state.lx = expand_axis(out.lx);
state.ly = expand_axis(out.ly);
state.rx = expand_axis(out.rx);
state.ry = expand_axis(out.ry);
state.left_stick_x = expand_axis(out.lx);
state.left_stick_y = expand_axis(out.ly);
state.right_stick_x = expand_axis(out.rx);
state.right_stick_y = expand_axis(out.ry);
out_state = state;
return true;
}
void switch_pro_set_rumble_callback(uint8_t instance,
SwitchRumbleCallback callback) {
void switch_pro_set_rumble_callback(
uint8_t instance, ControllerRumbleCallback callback) {
SwitchProContext* context = context_for(instance);
if (context != nullptr) {
context->rumble_callback = callback;

View file

@ -8,95 +8,27 @@
#include <stdbool.h>
#include <stdint.h>
#include "controller_color.h"
#include "controller_state.h"
#include "switch_haptics.h"
#include "switch_pro_descriptors.h"
// Preserve the pre-neutral-state 35%-of-1023 digital trigger boundary.
constexpr uint32_t SWITCH_PRO_LEGACY_TRIGGER_RANGE_MAXIMUM = 1023;
constexpr uint32_t SWITCH_PRO_LEGACY_TRIGGER_PRESS_THRESHOLD = 358;
constexpr uint16_t SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD =
static_cast<uint16_t>(
(static_cast<uint32_t>(CONTROLLER_TRIGGER_MAX) *
SWITCH_PRO_LEGACY_TRIGGER_PRESS_THRESHOLD) /
SWITCH_PRO_LEGACY_TRIGGER_RANGE_MAXIMUM);
typedef struct {
int16_t accel_x;
int16_t accel_y;
int16_t accel_z;
int16_t gyro_x;
int16_t gyro_y;
int16_t gyro_z;
} SwitchImuSample;
typedef struct {
bool dpad_up;
bool dpad_down;
bool dpad_left;
bool dpad_right;
bool button_a;
bool button_b;
bool button_x;
bool button_y;
bool button_l;
bool button_r;
bool button_zl;
bool button_zr;
bool button_plus;
bool button_minus;
bool button_home;
bool button_capture;
bool button_l3;
bool button_r3;
uint16_t lx; // 0-65535
uint16_t ly;
uint16_t rx;
uint16_t ry;
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);
// Update the desired controller state for one HID instance.
void switch_pro_set_input(uint8_t instance, const SwitchInputState& state);
void switch_pro_set_input(uint8_t instance, const ControllerState& state);
// Drive one Switch Pro USB state machine; returns true only when a regular
// 0x30 input report was successfully queued.
@ -104,14 +36,10 @@ bool switch_pro_task(uint8_t instance);
// Convert a packed UART message into controller state (returns true if parsed).
bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length,
SwitchInputState& out_state);
ControllerState& out_state);
// Driver state helpers
bool switch_pro_is_ready(uint8_t instance);
// Optional callback fired with decoded rumble intensities from one host
// interface.
typedef void (*SwitchRumbleCallback)(uint8_t instance,
const SwitchRumbleOutput& rumble);
void switch_pro_set_rumble_callback(uint8_t instance,
SwitchRumbleCallback callback);
ControllerRumbleCallback callback);

View file

@ -285,6 +285,13 @@ uint32_t btstack_run_loop_get_time_ms() {
#include "../bluepad32_input_backend.cpp"
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
AdapterUsbMode test_adapter_mode = AdapterUsbMode::kXInput;
AdapterUsbMode adapter_host_probe_mode() {
return test_adapter_mode;
}
#endif
SwitchRgbColor switch_pro_get_slot_light_color(uint8_t instance) {
static constexpr SwitchRgbColor grips[] = {
{SWITCH_COLOR_SLOT_1_R, SWITCH_COLOR_SLOT_1_G,
@ -355,7 +362,7 @@ void test_ready_order(bool reverse) {
"ready device must bind to its Bluepad index");
for (int candidate = 0; candidate < kSlotCount; ++candidate) {
SwitchInputState snapshot{};
ControllerState snapshot{};
bool expected_active = false;
for (int ready = 0; ready <= position; ++ready) {
expected_active = expected_active || order[ready] == candidate;
@ -386,7 +393,7 @@ void test_ready_order(bool reverse) {
"disconnecting any slot must resume connection policy");
for (int candidate = 0; candidate < kSlotCount; ++candidate) {
SwitchInputState snapshot{};
ControllerState snapshot{};
require(bluepad32_input_backend_snapshot(candidate, &snapshot) ==
(candidate != slot),
"disconnect must preserve every surviving slot");
@ -427,10 +434,10 @@ void test_rejections() {
collision_data.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
collision_data.gamepad.buttons = BUTTON_B;
platform_on_controller_data(&collision, &collision_data);
SwitchInputState snapshot{};
ControllerState snapshot{};
require(bluepad32_input_backend_snapshot(0, &snapshot),
"occupied slot must stay active");
require(!snapshot.button_a,
require(!snapshot.button_east,
"mismatched device input must not enter the occupied slot");
uni_controller_t slot_zero_data{};
@ -438,15 +445,15 @@ void test_rejections() {
slot_zero_data.gamepad.accel[0] = 8192;
platform_on_controller_data(&slot_zero, &slot_zero_data);
require(bluepad32_input_backend_snapshot(0, &snapshot) &&
snapshot.imu_sample_count == 3,
snapshot.motion_sample_count == 3,
"valid slot input must remain observable");
require(!bluepad32_input_backend_snapshot(4, &snapshot),
"public snapshot must reject slot 4");
bluepad32_input_backend_report_sent(4);
require(bluepad32_input_backend_snapshot(0, &snapshot) &&
snapshot.imu_sample_count == 3,
snapshot.motion_sample_count == 3,
"slot 4 acknowledgement must not consume slot 0 IMU");
bluepad32_input_backend_queue_rumble(4, SwitchRumbleOutput{1, 2});
bluepad32_input_backend_queue_rumble(4, ControllerRumbleOutput{1, 2});
process_rumble_timer(&g_rumble_timer);
require(slot_zero.rumble_calls == 0,
"slot 4 rumble must not reach a valid controller");
@ -534,39 +541,39 @@ void test_independent_lifecycle() {
platform_on_controller_data(&devices[slot], &data[slot]);
}
SwitchInputState states[kSlotCount]{};
ControllerState states[kSlotCount]{};
for (int slot = 0; slot < kSlotCount; ++slot) {
require(bluepad32_input_backend_snapshot(slot, &states[slot]) &&
states[slot].imu_sample_count == 3,
states[slot].motion_sample_count == 3,
"every slot must expose independent input and IMU");
}
require(states[0].button_a && !states[0].button_b &&
!states[0].button_y && !states[0].button_x,
require(states[0].button_east && !states[0].button_south &&
!states[0].button_west && !states[0].button_north,
"slot 0 must contain only slot 0 input");
require(states[1].button_b && !states[1].button_a &&
!states[1].button_y && !states[1].button_x,
require(states[1].button_south && !states[1].button_east &&
!states[1].button_west && !states[1].button_north,
"slot 1 must contain only slot 1 input");
require(states[2].button_y && !states[2].button_a &&
!states[2].button_b && !states[2].button_x,
require(states[2].button_west && !states[2].button_east &&
!states[2].button_south && !states[2].button_north,
"slot 2 must contain only slot 2 input");
require(states[3].button_x && !states[3].button_a &&
!states[3].button_b && !states[3].button_y,
require(states[3].button_north && !states[3].button_east &&
!states[3].button_south && !states[3].button_west,
"slot 3 must contain only slot 3 input");
bluepad32_input_backend_report_sent(3);
for (int slot = 0; slot < kSlotCount; ++slot) {
require(bluepad32_input_backend_snapshot(slot, &states[slot]) &&
states[slot].imu_sample_count == (slot == 3 ? 0 : 3),
states[slot].motion_sample_count == (slot == 3 ? 0 : 3),
"slot 3 acknowledgement must not consume slots 0-2 IMU");
}
for (int slot = 0; slot < 3; ++slot) {
bluepad32_input_backend_report_sent(slot);
require(bluepad32_input_backend_snapshot(slot, &states[slot]) &&
states[slot].imu_sample_count == 0,
states[slot].motion_sample_count == 0,
"each slot acknowledgement must consume only its own IMU");
}
const SwitchRumbleOutput initial_rumble[kSlotCount] = {
const ControllerRumbleOutput initial_rumble[kSlotCount] = {
{11, 21}, {12, 22}, {13, 23}, {14, 24}};
for (int slot = 0; slot < kSlotCount; ++slot) {
bluepad32_input_backend_queue_rumble(slot, initial_rumble[slot]);
@ -577,17 +584,17 @@ void test_independent_lifecycle() {
devices[slot].last_low == 11 + slot &&
devices[slot].last_high == 21 + slot &&
devices[slot].last_rumble_duration_ms ==
kRumbleDurationMs,
host_rumble_duration_ms(),
"each slot rumble must reach only its indexed controller");
}
bluepad32_input_backend_queue_rumble(0, SwitchRumbleOutput{0, 0});
bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{0, 0});
process_rumble_timer(&g_rumble_timer);
require(devices[0].rumble_calls == 2 &&
devices[0].last_rumble_duration_ms == 0,
"zero XInput magnitude must stop rumble immediately");
bluepad32_input_backend_queue_rumble(3, SwitchRumbleOutput{55, 66});
bluepad32_input_backend_queue_rumble(3, ControllerRumbleOutput{55, 66});
const uint32_t disconnected_generation =
g_slots[3].connection_generation;
const int starts_before_slot_three_disconnect = scan_starts;
@ -596,21 +603,21 @@ void test_independent_lifecycle() {
scanning_enabled && incoming_connections,
"slot 3 disconnect must resume scanning and incoming connections");
require(!bluepad32_input_backend_snapshot(3, &states[3]) &&
!states[3].button_x && states[3].lx == 32768,
"slot 3 disconnect must neutralize only slot 3");
!states[3].button_north && states[3].left_stick_x == 0,
"slot 3 disconnect must publish protocol-neutral state");
require(bluepad32_input_backend_snapshot(0, &states[0]) &&
states[0].button_a &&
states[0].button_east &&
bluepad32_input_backend_snapshot(1, &states[1]) &&
states[1].button_b &&
states[1].button_south &&
bluepad32_input_backend_snapshot(2, &states[2]) &&
states[2].button_y,
states[2].button_west,
"slot 3 disconnect must preserve slots 0-2");
platform_on_controller_data(&devices[0], &data[0]);
require(bluepad32_input_backend_snapshot(0, &states[0]) &&
states[0].button_a,
states[0].button_east,
"slot 0 input must continue while slot 3 is disconnected");
const int slot_zero_calls_while_scanning = devices[0].rumble_calls;
bluepad32_input_backend_queue_rumble(0, SwitchRumbleOutput{115, 116});
bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{115, 116});
tick_backend_timer(99);
require(devices[0].rumble_calls == slot_zero_calls_while_scanning + 1 &&
devices[0].last_low == 115 &&
@ -626,7 +633,7 @@ void test_independent_lifecycle() {
"slot 3 replacement must not receive disconnected device rumble");
g_slots[3].pending_rumble = {
3, disconnected_generation, SwitchRumbleOutput{77, 88}};
3, disconnected_generation, ControllerRumbleOutput{77, 88}};
g_slots[3].rumble_pending = true;
process_rumble_timer(&g_rumble_timer);
require(slot_three_replacement.rumble_calls == 0,
@ -638,21 +645,21 @@ void test_independent_lifecycle() {
replacement_data.gamepad.accel[0] = 9000;
platform_on_controller_data(&slot_three_replacement, &replacement_data);
require(bluepad32_input_backend_snapshot(3, &states[3]) &&
states[3].button_x && states[3].imu_sample_count == 3,
states[3].button_north && states[3].motion_sample_count == 3,
"replacement input and IMU must populate only slot 3");
require(bluepad32_input_backend_snapshot(0, &states[0]) &&
states[0].button_a &&
states[0].button_east &&
bluepad32_input_backend_snapshot(1, &states[1]) &&
states[1].button_b &&
states[1].button_south &&
bluepad32_input_backend_snapshot(2, &states[2]) &&
states[2].button_y,
states[2].button_west,
"slot 3 replacement must not disturb slots 0-2");
const int survivor_calls[kSlotCount - 1] = {
devices[0].rumble_calls,
devices[1].rumble_calls,
devices[2].rumble_calls};
bluepad32_input_backend_queue_rumble(3, SwitchRumbleOutput{90, 91});
bluepad32_input_backend_queue_rumble(3, ControllerRumbleOutput{90, 91});
process_rumble_timer(&g_rumble_timer);
require(slot_three_replacement.rumble_calls == 1 &&
slot_three_replacement.last_low == 90 &&
@ -669,8 +676,8 @@ void test_independent_lifecycle() {
slot_three_replacement.rumble_calls};
for (int slot = 0; slot < kSlotCount; ++slot) {
bluepad32_input_backend_queue_rumble(
slot, SwitchRumbleOutput{static_cast<uint8_t>(100 + slot),
static_cast<uint8_t>(110 + slot)});
slot, ControllerRumbleOutput{static_cast<uint8_t>(100 + slot),
static_cast<uint8_t>(110 + slot)});
}
process_rumble_timer(&g_rumble_timer);
for (int slot = 0; slot < kSlotCount; ++slot) {
@ -691,8 +698,8 @@ void test_independent_lifecycle() {
scanning_enabled && incoming_connections,
"disconnecting slots 0-2 must resume connection policy");
require(!bluepad32_input_backend_snapshot(slot, &states[slot]) &&
states[slot].lx == 32768,
"disconnect must neutralize its indexed slot");
states[slot].left_stick_x == 0,
"disconnect must publish protocol-neutral state");
for (int survivor = 0; survivor < kSlotCount; ++survivor) {
if (survivor == slot) {
continue;
@ -712,9 +719,9 @@ void test_independent_lifecycle() {
const int slot_zero_calls_before_mailboxes = replacements[0].rumble_calls;
const int slot_three_calls_before_mailboxes =
slot_three_replacement.rumble_calls;
bluepad32_input_backend_queue_rumble(3, SwitchRumbleOutput{119, 120});
bluepad32_input_backend_queue_rumble(3, SwitchRumbleOutput{121, 122});
bluepad32_input_backend_queue_rumble(0, SwitchRumbleOutput{123, 124});
bluepad32_input_backend_queue_rumble(3, ControllerRumbleOutput{119, 120});
bluepad32_input_backend_queue_rumble(3, ControllerRumbleOutput{121, 122});
bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{123, 124});
process_rumble_timer(&g_rumble_timer);
require(slot_three_replacement.rumble_calls ==
slot_three_calls_before_mailboxes + 1 &&
@ -861,11 +868,11 @@ void test_slot_lighting() {
"controller without RGB support did not receive its slot LED");
}
void require_south_button_mapping(const SwitchInputState& state,
void require_south_button_mapping(const ControllerState& state,
bool swapped,
const char* message) {
require(state.button_a == swapped && state.button_b == !swapped &&
!state.button_x && !state.button_y,
require(state.button_east == swapped && state.button_south == !swapped &&
!state.button_north && !state.button_west,
message);
}
@ -881,7 +888,7 @@ void test_abxy_hotkey() {
input.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
input.gamepad.buttons = BUTTON_A;
platform_on_controller_data(&slot_zero, &input);
SwitchInputState snapshot{};
ControllerState snapshot{};
require(bluepad32_input_backend_snapshot(0, &snapshot),
"slot 0 ABXY state was not published");
require_south_button_mapping(
@ -897,12 +904,12 @@ void test_abxy_hotkey() {
require_south_button_mapping(
snapshot, !kDefaultSwapAbxy,
"hotkey did not toggle slot 0 ABXY mapping");
require(!snapshot.button_l && !snapshot.button_r &&
!snapshot.button_minus && !snapshot.button_plus,
require(!snapshot.button_left_shoulder && !snapshot.button_right_shoulder &&
!snapshot.button_select && !snapshot.button_start,
"hotkey chord leaked into the Switch report");
bluepad32_input_backend_queue_rumble(
0, SwitchRumbleOutput{0x11, 0x22});
0, ControllerRumbleOutput{0x11, 0x22});
process_rumble_timer(&g_rumble_timer);
require(slot_zero.rumble_calls == 1 &&
slot_zero.last_high == kAbxyFeedbackWeakMagnitude &&
@ -971,9 +978,9 @@ void test_motion_hotkey() {
input.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
input.gamepad.accel[0] = 8192;
platform_on_controller_data(&slot_zero, &input);
SwitchInputState snapshot{};
ControllerState snapshot{};
require(bluepad32_input_backend_snapshot(0, &snapshot) &&
snapshot.imu_sample_count ==
snapshot.motion_sample_count ==
(kDefaultMotionEnabled ? 3 : 0),
"slot 0 did not start with configured motion state");
@ -982,10 +989,10 @@ void test_motion_hotkey() {
input.gamepad.misc_buttons = kMotionHotkeyMiscMask;
platform_on_controller_data(&slot_zero, &input);
require(bluepad32_input_backend_snapshot(0, &snapshot) &&
snapshot.imu_sample_count ==
snapshot.motion_sample_count ==
(kDefaultMotionEnabled ? 0 : 3) &&
!snapshot.dpad_up && !snapshot.button_r &&
!snapshot.button_plus,
!snapshot.dpad_up && !snapshot.button_right_shoulder &&
!snapshot.button_start,
"motion chord did not toggle motion or suppress its inputs");
process_rumble_timer(&g_rumble_timer);
@ -1015,7 +1022,7 @@ void test_motion_hotkey() {
peer_input.gamepad.accel[0] = 8192;
platform_on_controller_data(&slot_one, &peer_input);
require(bluepad32_input_backend_snapshot(1, &snapshot) &&
snapshot.imu_sample_count ==
snapshot.motion_sample_count ==
(kDefaultMotionEnabled ? 3 : 0),
"slot 0 motion chord changed slot 1 motion state");
@ -1027,7 +1034,7 @@ void test_motion_hotkey() {
input.gamepad.misc_buttons = kMotionHotkeyMiscMask;
platform_on_controller_data(&slot_zero, &input);
require(bluepad32_input_backend_snapshot(0, &snapshot) &&
snapshot.imu_sample_count ==
snapshot.motion_sample_count ==
(kDefaultMotionEnabled ? 3 : 0),
"released motion chord did not re-arm or restore motion");
process_rumble_timer(&g_rumble_timer);
@ -1044,6 +1051,76 @@ void test_motion_hotkey() {
"disconnect did not reset slot 0 motion hotkey state");
}
void test_protocol_neutral_analog_state() {
start_pairing_backend();
uni_hid_device_t controller = device(0);
platform_on_device_connected(&controller);
require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS,
"analog-state controller did not become ready");
uni_controller_t input{};
input.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
input.gamepad.axis_x = -512;
input.gamepad.axis_y = 0;
input.gamepad.axis_rx = 511;
input.gamepad.axis_ry = -256;
input.gamepad.brake = 1;
input.gamepad.throttle = 512;
platform_on_controller_data(&controller, &input);
ControllerState state{};
require(bluepad32_input_backend_snapshot(0, &state),
"analog state was not published");
require(state.left_stick_x == INT16_MIN &&
state.left_stick_y == 0 &&
state.right_stick_x == INT16_MAX &&
state.right_stick_y == -16384,
"stick axes were not normalized to signed full range");
require(state.left_trigger == scale_trigger(1) &&
state.left_trigger > 0 &&
state.right_trigger == scale_trigger(512) &&
state.right_trigger < UINT16_MAX,
"analog trigger precision was discarded");
input.gamepad.brake = 0;
input.gamepad.throttle = 0;
input.gamepad.buttons =
BUTTON_TRIGGER_L | BUTTON_TRIGGER_R;
platform_on_controller_data(&controller, &input);
require(bluepad32_input_backend_snapshot(0, &state) &&
state.left_trigger == UINT16_MAX &&
state.right_trigger == UINT16_MAX,
"digital trigger buttons did not map to full analog range");
}
void test_host_rumble_mode_duration() {
start_pairing_backend();
uni_hid_device_t controller = device(0);
platform_on_device_connected(&controller);
require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS,
"rumble-mode controller did not become ready");
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
test_adapter_mode = AdapterUsbMode::kSwitchProbe;
#endif
bluepad32_input_backend_queue_rumble(
0, ControllerRumbleOutput{100, 101});
process_rumble_timer(&g_rumble_timer);
require(controller.last_rumble_duration_ms ==
kSwitchHostRumbleDurationMs,
"Switch mode did not use bounded host rumble");
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
test_adapter_mode = AdapterUsbMode::kXInput;
bluepad32_input_backend_queue_rumble(
0, ControllerRumbleOutput{102, 103});
process_rumble_timer(&g_rumble_timer);
require(controller.last_rumble_duration_ms ==
kXInputHostRumbleDurationMs,
"XInput mode did not retain stateful host rumble");
#endif
}
void test_clear_pairings() {
classic_bond_count = 1;
classic_bonds[0][0] = 0x10;
@ -1067,7 +1144,7 @@ void test_clear_pairings() {
"pairing reset controller did not become ready");
}
bluepad32_input_backend_queue_rumble(
0, SwitchRumbleOutput{100, 101});
0, ControllerRumbleOutput{100, 101});
bluepad32_input_backend_clear_pairings();
require(g_clear_pairings_requested && delete_key_calls == 0 &&
@ -1086,11 +1163,11 @@ void test_clear_pairings() {
for (const BackendSlot& slot : g_slots) {
require(slot.device == nullptr && !slot.active &&
!slot.rumble_pending && !slot.feedback_pending &&
slot.state.lx == kStickMidpoint &&
slot.state.ly == kStickMidpoint &&
slot.state.rx == kStickMidpoint &&
slot.state.ry == kStickMidpoint &&
slot.state.imu_sample_count == 0,
slot.state.left_stick_x == 0 &&
slot.state.left_stick_y == 0 &&
slot.state.right_stick_x == 0 &&
slot.state.right_stick_y == 0 &&
slot.state.motion_sample_count == 0,
"pairing reset must publish neutral empty slots");
}
require(!g_pairing_window_open && !bondable &&
@ -1163,6 +1240,10 @@ int main(int argc, char** argv) {
test_abxy_hotkey();
} else if (scenario == "motion-hotkey") {
test_motion_hotkey();
} else if (scenario == "analog-state") {
test_protocol_neutral_analog_state();
} else if (scenario == "rumble-mode") {
test_host_rumble_mode_duration();
} else if (scenario == "clear-pairings") {
test_clear_pairings();
} else if (scenario == "flash-core-start") {

View file

@ -8,7 +8,7 @@ namespace {
int failures = 0;
void expect_output(const char* scenario, SwitchRumbleOutput actual,
void expect_output(const char* scenario, ControllerRumbleOutput actual,
uint8_t expected_low, uint8_t expected_high) {
if (actual.low_frequency_magnitude == expected_low &&
actual.high_frequency_magnitude == expected_high) {

View file

@ -26,7 +26,7 @@ struct SentReport {
struct RumbleEvent {
unsigned count = 0;
uint8_t instance = 0xff;
SwitchRumbleOutput output{};
ControllerRumbleOutput output{};
};
uint64_t now_ms = 0;
@ -95,7 +95,8 @@ SwitchProReport get_current_report(uint8_t instance,
void expect_neutral_sticks(SwitchProReport& report,
const char* state_failure) {
constexpr uint16_t packed_mid = SWITCH_PRO_JOYSTICK_MID >> 4u;
constexpr uint16_t packed_mid =
CONTROLLER_AXIS_UNSIGNED_CENTER >> 4u;
constexpr uint16_t packed_inverted_mid =
static_cast<uint16_t>(-static_cast<int32_t>(packed_mid)) & 0x0fffu;
expect(report.inputs.leftStick.getX() == packed_mid &&
@ -195,7 +196,7 @@ std::array<uint8_t, 10> complete_rumble_report(
return report;
}
void rumble_callback(uint8_t instance, const SwitchRumbleOutput& output) {
void rumble_callback(uint8_t instance, const ControllerRumbleOutput& output) {
expect(instance < rumble_events.size(),
"rumble callback received an invalid instance");
if (instance >= rumble_events.size()) {
@ -270,18 +271,22 @@ void test_failed_startup_identify_retries_preserve_counter() {
void test_input_reports_and_timers_are_isolated() {
initialize_contexts();
std::array<SwitchInputState, kInstanceCount> states{};
std::array<ControllerState, kInstanceCount> states{};
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
SwitchInputState& state = states[instance];
state.lx = static_cast<uint16_t>(0x1111u * (instance + 1u));
state.ly = static_cast<uint16_t>(0x2222u + 0x1111u * instance);
state.rx = static_cast<uint16_t>(0x5555u + 0x1111u * instance);
state.ry = static_cast<uint16_t>(0x8888u + 0x1111u * instance);
ControllerState& state = states[instance];
state.left_stick_x = controller_axis_from_unsigned(
static_cast<uint16_t>(0x1111u * (instance + 1u)));
state.left_stick_y = controller_axis_from_unsigned(
static_cast<uint16_t>(0x2222u + 0x1111u * instance));
state.right_stick_x = controller_axis_from_unsigned(
static_cast<uint16_t>(0x5555u + 0x1111u * instance));
state.right_stick_y = controller_axis_from_unsigned(
static_cast<uint16_t>(0x8888u + 0x1111u * instance));
}
states[0].button_a = true;
states[1].button_b = true;
states[2].button_x = true;
states[3].button_y = true;
states[0].button_east = true;
states[1].button_south = true;
states[2].button_north = true;
states[3].button_west = true;
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
switch_pro_set_input(instance, states[instance]);
@ -320,9 +325,9 @@ void test_input_reports_and_timers_are_isolated() {
}
}
SwitchInputState changed_zero = states[0];
changed_zero.button_a = false;
changed_zero.button_home = true;
ControllerState changed_zero = states[0];
changed_zero.button_east = false;
changed_zero.button_system = true;
switch_pro_set_input(0, changed_zero);
now_ms = 30;
expect(switch_pro_task(0),
@ -333,8 +338,8 @@ void test_input_reports_and_timers_are_isolated() {
!unchanged_three.inputs.buttonHome,
"instance 0 input change leaked into instance 3");
SwitchInputState changed_three = states[3];
changed_three.button_y = false;
ControllerState changed_three = states[3];
changed_three.button_west = false;
changed_three.button_capture = true;
switch_pro_set_input(3, changed_three);
now_ms = 45;
@ -357,13 +362,14 @@ void test_callback_send_and_imu_modes_are_isolated() {
expect(reports_for_instance(1) == 0,
"feature callback queued a reply on instance 1");
SwitchInputState zero{};
zero.lx = zero.ly = zero.rx = zero.ry = SWITCH_PRO_JOYSTICK_MID;
zero.imu_sample_count = 1;
zero.imu_samples[0] = {101, 202, 303, 404, 505, 606};
SwitchInputState one = zero;
one.button_x = true;
one.imu_samples[0] = {1001, 2002, 3003, 4004, 5005, 6006};
ControllerState zero{};
zero.left_stick_x = zero.left_stick_y =
zero.right_stick_x = zero.right_stick_y = 0;
zero.motion_sample_count = 1;
zero.motion_samples[0] = {101, 202, 303, 404, 505, 606};
ControllerState one = zero;
one.button_north = true;
one.motion_samples[0] = {1001, 2002, 3003, 4004, 5005, 6006};
switch_pro_set_input(0, zero);
switch_pro_set_input(1, one);
now_ms = 21;
@ -384,15 +390,16 @@ void test_callback_send_and_imu_modes_are_isolated() {
now_ms = 6;
switch_pro_task(0);
switch_pro_task(1);
SwitchInputState moving{};
moving.lx = moving.ly = moving.rx = moving.ry = SWITCH_PRO_JOYSTICK_MID;
moving.imu_sample_count = 1;
moving.imu_samples[0] = {100, 200, 300, 20000, 0, 0};
SwitchInputState stationary{};
stationary.lx = stationary.ly = stationary.rx = stationary.ry =
SWITCH_PRO_JOYSTICK_MID;
stationary.imu_sample_count = 1;
stationary.imu_samples[0] = {1000, 2000, 3000, 0, 0, 0};
ControllerState moving{};
moving.left_stick_x = moving.left_stick_y =
moving.right_stick_x = moving.right_stick_y = 0;
moving.motion_sample_count = 1;
moving.motion_samples[0] = {100, 200, 300, 20000, 0, 0};
ControllerState stationary{};
stationary.left_stick_x = stationary.left_stick_y =
stationary.right_stick_x = stationary.right_stick_y = 0;
stationary.motion_sample_count = 1;
stationary.motion_samples[0] = {1000, 2000, 3000, 0, 0, 0};
switch_pro_set_input(0, moving);
switch_pro_set_input(1, stationary);
now_ms = 21;
@ -568,8 +575,8 @@ void test_lifecycle_and_invalid_instances() {
"unmount did not reset every configured context");
}
SwitchInputState ignored{};
ignored.button_home = true;
ControllerState ignored{};
ignored.button_system = true;
switch_pro_init(kInvalidInstance);
switch_pro_set_input(kInvalidInstance, ignored);
switch_pro_set_rumble_callback(kInvalidInstance, rumble_callback);
@ -579,18 +586,41 @@ void test_lifecycle_and_invalid_instances() {
"invalid instance reported ready");
std::array<uint8_t, SWITCH_PRO_ENDPOINT_SIZE> buffer{};
expect(tud_hid_get_report_cb(kInvalidInstance, 0, HID_REPORT_TYPE_INPUT,
buffer.data(), buffer.size()) == 0,
"invalid instance served GET_REPORT data");
expect(tud_hid_descriptor_report_cb(kInvalidInstance) == nullptr,
"invalid instance served a report descriptor");
}
void test_protocol_neutral_trigger_threshold() {
initialize_contexts();
ControllerState state{};
state.left_trigger =
static_cast<uint16_t>(SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD - 1u);
state.right_trigger = SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
switch_pro_set_input(0, state);
now_ms = 15;
expect(switch_pro_task(0), "trigger threshold report was not sent");
SwitchProReport report = copy_switch_report(latest_regular_report(0));
expect(!report.inputs.buttonZL && report.inputs.buttonZR,
"Switch trigger threshold changed at the lower boundary");
state.left_trigger = SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
state.right_trigger = CONTROLLER_TRIGGER_MIN;
switch_pro_set_input(0, state);
now_ms = 30;
expect(switch_pro_task(0), "second trigger threshold report was not sent");
report = copy_switch_report(latest_regular_report(0));
expect(report.inputs.buttonZL && !report.inputs.buttonZR,
"Switch trigger threshold changed at the upper boundary");
}
void test_uart_parser_is_pure() {
initialize_contexts();
SwitchInputState driver_state{};
driver_state.lx = driver_state.ly = driver_state.rx = driver_state.ry =
SWITCH_PRO_JOYSTICK_MID;
driver_state.button_x = true;
ControllerState driver_state{};
driver_state.left_stick_x = driver_state.left_stick_y =
driver_state.right_stick_x = driver_state.right_stick_y = 0;
driver_state.button_north = true;
switch_pro_set_input(0, driver_state);
now_ms = 15;
switch_pro_task(0);
@ -610,14 +640,20 @@ void test_uart_parser_is_pure() {
for (unsigned i = 0; i < packet.size() - 1; ++i) {
packet.back() = static_cast<uint8_t>(packet.back() + packet[i]);
}
SwitchInputState parsed{};
ControllerState parsed{};
expect(switch_pro_apply_uart_packet(packet.data(), packet.size(), parsed),
"valid UART packet was rejected");
expect(parsed.button_a && parsed.button_l && parsed.dpad_down &&
expect(parsed.button_east && parsed.button_left_shoulder && parsed.dpad_down &&
parsed.dpad_left,
"UART buttons or hat were parsed incorrectly");
expect(parsed.lx == 0x1212 && parsed.ly == 0x3434 &&
parsed.rx == 0x5656 && parsed.ry == 0x7878,
expect(parsed.left_stick_x ==
controller_axis_from_unsigned(0x1212) &&
parsed.left_stick_y ==
controller_axis_from_unsigned(0x3434) &&
parsed.right_stick_x ==
controller_axis_from_unsigned(0x5656) &&
parsed.right_stick_y ==
controller_axis_from_unsigned(0x7878),
"UART axes were parsed incorrectly");
std::array<uint8_t, SWITCH_PRO_ENDPOINT_SIZE> current{};
@ -628,14 +664,14 @@ void test_uart_parser_is_pure() {
expect(current_report.inputs.buttonX && !current_report.inputs.buttonA,
"UART parsing mutated driver context state");
SwitchInputState unchanged{};
unchanged.button_home = true;
unchanged.lx = 123;
ControllerState unchanged{};
unchanged.button_system = true;
unchanged.left_stick_x = 123;
packet.back() ^= 0xffu;
expect(!switch_pro_apply_uart_packet(packet.data(), packet.size(),
unchanged),
"invalid UART checksum was accepted");
expect(unchanged.button_home && unchanged.lx == 123,
expect(unchanged.button_system && unchanged.left_stick_x == 123,
"failed UART parse modified its output reference");
}
@ -687,6 +723,7 @@ int main() {
test_rumble_callbacks_and_decoders_are_isolated();
test_grip_colors_are_isolated();
test_lifecycle_and_invalid_instances();
test_protocol_neutral_trigger_threshold();
test_uart_parser_is_pure();
if (failures != 0) {
std::cerr << failures << " driver context test(s) failed\n";

View file

@ -44,6 +44,8 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
"slot-lighting",
"abxy-hotkey",
"motion-hotkey",
"analog-state",
"rumble-mode",
"clear-pairings",
"flash-core-start",
"flash-core-failure",

View file

@ -99,8 +99,8 @@ void test_microsoft_compatible_id_descriptor() {
"Microsoft descriptor index mismatch");
expect(kMsCompatIdDescriptor[8] == SWITCH_PICO_HID_INSTANCE_COUNT,
"Microsoft function count mismatch");
for (uint8_t instance = 0; instance < SWITCH_PICO_HID_INSTANCE_COUNT;
++instance) {
for (uint8_t instance = 0;
instance < SWITCH_PICO_HID_INSTANCE_COUNT; ++instance) {
const uint8_t *function = &kMsCompatIdDescriptor[16 + instance * 24];
expect(function[0] == instance,
"Microsoft descriptor interface mismatch");
@ -113,8 +113,7 @@ void test_microsoft_compatible_id_descriptor() {
}
void test_input_report_mapping() {
SwitchInputState state{};
state.lx = state.ly = state.rx = state.ry = 32768;
ControllerState state{};
auto report = XInputFeasibility::build_input_report(state);
expect(report.report_id == 0 && report.report_size == 20,
"neutral report header mismatch");
@ -126,19 +125,19 @@ void test_input_report_mapping() {
"neutral axes mismatch");
state.dpad_up = true;
state.button_b = true;
state.button_a = true;
state.button_y = true;
state.button_x = true;
state.button_plus = true;
state.button_minus = true;
state.button_home = true;
state.button_zl = true;
state.button_zr = true;
state.lx = 0;
state.ly = 0;
state.rx = UINT16_MAX;
state.ry = UINT16_MAX;
state.button_south = true;
state.button_east = true;
state.button_west = true;
state.button_north = true;
state.button_start = true;
state.button_select = true;
state.button_system = true;
state.left_trigger = UINT16_MAX;
state.right_trigger = UINT16_MAX;
state.left_stick_x = INT16_MIN;
state.left_stick_y = INT16_MIN;
state.right_stick_x = INT16_MAX;
state.right_stick_y = INT16_MAX;
report = XInputFeasibility::build_input_report(state);
expect((report.buttons & XInputFeasibility::kDpadUp) != 0,
"D-pad mapping missing");
@ -148,15 +147,22 @@ void test_input_report_mapping() {
(report.buttons & XInputFeasibility::kButtonY) != 0,
"positional face-button mapping mismatch");
expect(report.left_trigger == 0xff && report.right_trigger == 0xff,
"digital trigger mapping mismatch");
"full analog trigger mapping mismatch");
expect(report.left_x == INT16_MIN && report.left_y == INT16_MAX &&
report.right_x == INT16_MAX && report.right_y == -INT16_MAX,
report.right_x == INT16_MAX &&
report.right_y == -INT16_MAX,
"axis endpoint mapping mismatch");
state.left_trigger = 0x8000;
state.right_trigger = 0x7fff;
report = XInputFeasibility::build_input_report(state);
expect(report.left_trigger == 0x80 && report.right_trigger == 0x7f,
"analog trigger precision was discarded");
}
void test_rumble_report() {
const uint8_t packet[8] = {0x00, 0x08, 0x00, 0xa5, 0x5a, 0x00, 0x00, 0x00};
SwitchRumbleOutput output{};
ControllerRumbleOutput output{};
expect(
XInputFeasibility::parse_rumble_report(packet, sizeof(packet), &output),
"valid rumble report rejected");

View file

@ -14,10 +14,10 @@ constexpr uint8_t kRhport = 0;
constexpr uint8_t kEndpointBufferSize = 32;
struct XInputContext {
SwitchInputState input{};
ControllerState input{};
XInputFeasibility::InputReport input_report{};
uint8_t output_report[kEndpointBufferSize]{};
SwitchRumbleCallback rumble_callback = nullptr;
ControllerRumbleCallback rumble_callback = nullptr;
uint8_t endpoint_in = 0;
uint8_t endpoint_out = 0;
bool configured = false;
@ -43,7 +43,7 @@ XInputContext *context_for_endpoint(uint8_t endpoint) {
}
void reset_context(XInputContext &context) {
const SwitchRumbleCallback callback = context.rumble_callback;
const ControllerRumbleCallback callback = context.rumble_callback;
context = {};
context.rumble_callback = callback;
}
@ -138,7 +138,7 @@ bool driver_transfer(uint8_t rhport, uint8_t endpoint, xfer_result_t result,
return false;
}
if (endpoint == context->endpoint_out) {
SwitchRumbleOutput rumble{};
ControllerRumbleOutput rumble{};
if (XInputFeasibility::parse_rumble_report(context->output_report,
transferred, &rumble) &&
context->rumble_callback != nullptr) {
@ -168,15 +168,14 @@ void xinput_feasibility_init(uint8_t instance) {
}
void xinput_feasibility_set_rumble_callback(uint8_t instance,
SwitchRumbleCallback callback) {
ControllerRumbleCallback callback) {
XInputContext *context = context_for(instance);
if (context != nullptr) {
context->rumble_callback = callback;
}
}
void xinput_feasibility_set_input(uint8_t instance,
const SwitchInputState &state) {
const ControllerState& state) {
XInputContext *context = context_for(instance);
if (context != nullptr) {
context->input = state;

View file

@ -2,12 +2,13 @@
#include <stdint.h>
#include "switch_pro_driver.h"
#include "controller_state.h"
#include "switch_haptics.h"
void xinput_feasibility_init(uint8_t instance);
void xinput_feasibility_set_rumble_callback(uint8_t instance,
SwitchRumbleCallback callback);
ControllerRumbleCallback callback);
void xinput_feasibility_set_input(uint8_t instance,
const SwitchInputState &state);
const ControllerState& state);
bool xinput_feasibility_task(uint8_t instance);
bool xinput_feasibility_is_ready(uint8_t instance);

View file

@ -2,7 +2,8 @@
#include <stdint.h>
#include "switch_pro_driver.h"
#include "controller_state.h"
#include "switch_haptics.h"
namespace XInputFeasibility {
@ -39,42 +40,41 @@ struct InputReport {
static_assert(sizeof(InputReport) == 20);
constexpr int16_t horizontal_axis(uint16_t value) {
return static_cast<int16_t>(static_cast<int32_t>(value) - 32768);
constexpr int16_t invert_axis(int16_t value) {
return value == INT16_MIN ? INT16_MAX
: static_cast<int16_t>(-value);
}
constexpr int16_t vertical_axis(uint16_t value) {
const int16_t horizontal = horizontal_axis(value);
return horizontal == INT16_MIN ? INT16_MAX
: static_cast<int16_t>(-horizontal);
}
inline InputReport build_input_report(const SwitchInputState &state) {
inline InputReport build_input_report(const ControllerState& state) {
InputReport report{};
report.report_size = sizeof(report);
report.buttons =
(state.dpad_up ? kDpadUp : 0) | (state.dpad_down ? kDpadDown : 0) |
(state.dpad_up ? kDpadUp : 0) |
(state.dpad_down ? kDpadDown : 0) |
(state.dpad_left ? kDpadLeft : 0) |
(state.dpad_right ? kDpadRight : 0) | (state.button_plus ? kStart : 0) |
(state.button_minus ? kBack : 0) | (state.button_l3 ? kLeftThumb : 0) |
(state.button_r3 ? kRightThumb : 0) |
(state.button_l ? kLeftShoulder : 0) |
(state.button_r ? kRightShoulder : 0) |
(state.button_home ? kGuide : 0) |
// Switch labels are positional opposites of XInput labels.
(state.button_b ? kButtonA : 0) | (state.button_a ? kButtonB : 0) |
(state.button_y ? kButtonX : 0) | (state.button_x ? kButtonY : 0);
report.left_trigger = state.button_zl ? 0xff : 0x00;
report.right_trigger = state.button_zr ? 0xff : 0x00;
report.left_x = horizontal_axis(state.lx);
report.left_y = vertical_axis(state.ly);
report.right_x = horizontal_axis(state.rx);
report.right_y = vertical_axis(state.ry);
(state.dpad_right ? kDpadRight : 0) |
(state.button_start ? kStart : 0) |
(state.button_select ? kBack : 0) |
(state.button_left_stick ? kLeftThumb : 0) |
(state.button_right_stick ? kRightThumb : 0) |
(state.button_left_shoulder ? kLeftShoulder : 0) |
(state.button_right_shoulder ? kRightShoulder : 0) |
(state.button_system ? kGuide : 0) |
(state.button_south ? kButtonA : 0) |
(state.button_east ? kButtonB : 0) |
(state.button_west ? kButtonX : 0) |
(state.button_north ? kButtonY : 0);
report.left_trigger = controller_trigger_to_u8(state.left_trigger);
report.right_trigger = controller_trigger_to_u8(state.right_trigger);
report.left_x = state.left_stick_x;
report.left_y = invert_axis(state.left_stick_y);
report.right_x = state.right_stick_x;
report.right_y = invert_axis(state.right_stick_y);
return report;
}
inline bool parse_rumble_report(const uint8_t *data, uint32_t size,
SwitchRumbleOutput *output) {
ControllerRumbleOutput *output) {
if (data == nullptr || output == nullptr || size < 5 || data[0] != 0x00 ||
data[1] != 0x08) {
return false;