Introduce protocol-neutral controller state
This commit is contained in:
parent
df515934ab
commit
b11ae076a8
28 changed files with 833 additions and 501 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
10
adapter_usb_mode.h
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
enum class AdapterUsbMode : uint8_t {
|
||||
kSwitchProbe,
|
||||
kXInput,
|
||||
};
|
||||
|
||||
AdapterUsbMode adapter_host_probe_mode();
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
34
build.py
34
build.py
|
|
@ -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
42
controller_color.h
Normal 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
92
controller_state.h
Normal 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);
|
||||
BIN
firmware/switch-pico-adapter-feasibility.elf
Executable file
BIN
firmware/switch-pico-adapter-feasibility.elf
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue