diff --git a/ADAPTER_PARITY_PLAN.md b/ADAPTER_PARITY_PLAN.md index 3fc5d11..1fe00ed 100644 --- a/ADAPTER_PARITY_PLAN.md +++ b/ADAPTER_PARITY_PLAN.md @@ -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. diff --git a/adapter_host_probe.h b/adapter_host_probe.h index 77629f3..c01f0a2 100644 --- a/adapter_host_probe.h +++ b/adapter_host_probe.h @@ -2,15 +2,11 @@ #include +#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); diff --git a/adapter_usb_mode.h b/adapter_usb_mode.h new file mode 100644 index 0000000..f64e442 --- /dev/null +++ b/adapter_usb_mode.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +enum class AdapterUsbMode : uint8_t { + kSwitchProbe, + kXInput, +}; + +AdapterUsbMode adapter_host_probe_mode(); diff --git a/bluepad32_input_backend.cpp b/bluepad32_input_backend.cpp index ee84646..ccaa78a 100644 --- a/bluepad32_input_backend.cpp +++ b/bluepad32_input_backend.cpp @@ -12,21 +12,20 @@ #include #include #include +#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( - (static_cast(value - kAxisMinimum) * kStickMidpoint) / -kAxisMinimum); + return static_cast( + (static_cast(value) * -INT16_MIN) / + -kAxisMinimum); + } + return static_cast( + (static_cast(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( - kStickMidpoint + (static_cast(value) * (UINT16_MAX - kStickMidpoint)) / kAxisMaximum); + (static_cast(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(gamepad.accel[2])); sample.accel_y = convert_accel(-static_cast(gamepad.accel[0])); sample.accel_z = convert_accel(gamepad.accel[1]); sample.gyro_x = convert_gyro(-static_cast(gamepad.gyro[2])); sample.gyro_y = convert_gyro(-static_cast(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; } diff --git a/bluepad32_input_backend.h b/bluepad32_input_backend.h index 74ed309..d7bdaa6 100644 --- a/bluepad32_input_backend.h +++ b/bluepad32_input_backend.h @@ -2,8 +2,9 @@ #include +#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); diff --git a/build.py b/build.py index 5bb0a45..a46585d 100644 --- a/build.py +++ b/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() diff --git a/controller_color.h b/controller_color.h new file mode 100644 index 0000000..37149ee --- /dev/null +++ b/controller_color.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +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(maximum - minimum); + const uint16_t peak = + static_cast((static_cast(maximum) * 2u + 1u) / + 3u); + if (chroma == 0) { + const uint8_t gray = static_cast(peak); + return {gray, gray, gray}; + } + + const auto calibrate = [minimum, chroma, peak](uint8_t component) { + const uint32_t delta = + static_cast(component - minimum); + return static_cast( + (static_cast(peak) * delta * delta) / + (static_cast(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); diff --git a/controller_state.h b/controller_state.h new file mode 100644 index 0000000..b2b8392 --- /dev/null +++ b/controller_state.h @@ -0,0 +1,92 @@ +#pragma once + +#include + +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_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( + static_cast(value) - INT16_MIN); +} + +constexpr int16_t controller_axis_from_unsigned(uint16_t value) { + return static_cast( + static_cast(value) + INT16_MIN); +} + +constexpr uint8_t controller_trigger_to_u8(uint16_t value) { + return static_cast( + 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); diff --git a/firmware/switch-pico-adapter-feasibility.elf b/firmware/switch-pico-adapter-feasibility.elf new file mode 100755 index 0000000..0b6642e Binary files /dev/null and b/firmware/switch-pico-adapter-feasibility.elf differ diff --git a/firmware/switch-pico-adapter-feasibility.uf2 b/firmware/switch-pico-adapter-feasibility.uf2 index ec4a41e..f5f6109 100644 Binary files a/firmware/switch-pico-adapter-feasibility.uf2 and b/firmware/switch-pico-adapter-feasibility.uf2 differ diff --git a/firmware/switch-pico-aio.elf b/firmware/switch-pico-aio.elf index c490d09..ed48956 100755 Binary files a/firmware/switch-pico-aio.elf and b/firmware/switch-pico-aio.elf differ diff --git a/firmware/switch-pico-aio.uf2 b/firmware/switch-pico-aio.uf2 index 1fd9861..a3a4786 100644 Binary files a/firmware/switch-pico-aio.uf2 and b/firmware/switch-pico-aio.uf2 differ diff --git a/firmware/switch-pico.elf b/firmware/switch-pico.elf index 25a3b3d..00900a6 100755 Binary files a/firmware/switch-pico.elf and b/firmware/switch-pico.elf differ diff --git a/firmware/switch-pico.uf2 b/firmware/switch-pico.uf2 index 4593404..69063a8 100644 Binary files a/firmware/switch-pico.uf2 and b/firmware/switch-pico.uf2 differ diff --git a/patches/bluepad32-sdl3-imu.patch b/patches/bluepad32-sdl3-imu.patch index 5ba7ef7..3b03c1f 100644 --- a/patches/bluepad32-sdl3-imu.patch +++ b/patches/bluepad32-sdl3-imu.patch @@ -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); } diff --git a/switch-pico.cpp b/switch-pico.cpp index 3a18b30..361021f 100644 --- a/switch-pico.cpp +++ b/switch-pico.cpp @@ -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 diff --git a/switch_haptics.cpp b/switch_haptics.cpp index 84d9b96..8bade37 100644 --- a/switch_haptics.cpp +++ b/switch_haptics.cpp @@ -289,7 +289,7 @@ uint8_t SwitchHapticsDecoder::amplitude_to_magnitude(uint8_t amplitude_index) { return static_cast(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}, diff --git a/switch_haptics.h b/switch_haptics.h index 15ae502..9d8c75a 100644 --- a/switch_haptics.h +++ b/switch_haptics.h @@ -4,10 +4,13 @@ #include #include -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 { diff --git a/switch_pro_driver.cpp b/switch_pro_driver.cpp index 9cc7490..631ad2b 100644 --- a/switch_pro_driver.cpp +++ b/switch_pro_driver.cpp @@ -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(packet[3]) | (static_cast(packet[4]) << 8); + out.buttons = static_cast(packet[3]) | + (static_cast(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(8u + static_cast(imu_count) * 12u); + uint16_t required_payload_len = static_cast( + 8u + static_cast(motion_count) * 12u); if (payload_len < required_payload_len) { return false; } - auto expand_axis = [](uint8_t v) -> uint16_t { - return static_cast(v) << 8 | v; + auto expand_axis = [](uint8_t value) -> int16_t { + const uint16_t expanded = + static_cast(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(static_cast(src[0]) | (static_cast(src[1]) << 8)); + return static_cast( + static_cast(src[0]) | + (static_cast(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; diff --git a/switch_pro_driver.h b/switch_pro_driver.h index 74e4346..bb1d9ba 100644 --- a/switch_pro_driver.h +++ b/switch_pro_driver.h @@ -8,95 +8,27 @@ #include #include +#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( + (static_cast(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(maximum - minimum); - const uint16_t peak = - static_cast((static_cast(maximum) * 2u + 1u) / - 3u); - if (chroma == 0) { - const uint8_t gray = static_cast(peak); - return {gray, gray, gray}; - } - - const auto calibrate = [minimum, chroma, peak](uint8_t component) { - const uint32_t delta = - static_cast(component - minimum); - return static_cast( - (static_cast(peak) * delta * delta) / - (static_cast(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); \ No newline at end of file diff --git a/tests/bluepad32_backend_lifecycle_test.cpp b/tests/bluepad32_backend_lifecycle_test.cpp index 66a043c..e539272 100644 --- a/tests/bluepad32_backend_lifecycle_test.cpp +++ b/tests/bluepad32_backend_lifecycle_test.cpp @@ -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(100 + slot), - static_cast(110 + slot)}); + slot, ControllerRumbleOutput{static_cast(100 + slot), + static_cast(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") { diff --git a/tests/switch_haptics_test.cpp b/tests/switch_haptics_test.cpp index 96d6e22..bec8a15 100644 --- a/tests/switch_haptics_test.cpp +++ b/tests/switch_haptics_test.cpp @@ -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) { diff --git a/tests/switch_pro_driver_context_test.cpp b/tests/switch_pro_driver_context_test.cpp index baa62b4..3130f38 100644 --- a/tests/switch_pro_driver_context_test.cpp +++ b/tests/switch_pro_driver_context_test.cpp @@ -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(-static_cast(packed_mid)) & 0x0fffu; expect(report.inputs.leftStick.getX() == packed_mid && @@ -195,7 +196,7 @@ std::array 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 states{}; + std::array states{}; for (uint8_t instance = 0; instance < kInstanceCount; ++instance) { - SwitchInputState& state = states[instance]; - state.lx = static_cast(0x1111u * (instance + 1u)); - state.ly = static_cast(0x2222u + 0x1111u * instance); - state.rx = static_cast(0x5555u + 0x1111u * instance); - state.ry = static_cast(0x8888u + 0x1111u * instance); + ControllerState& state = states[instance]; + state.left_stick_x = controller_axis_from_unsigned( + static_cast(0x1111u * (instance + 1u))); + state.left_stick_y = controller_axis_from_unsigned( + static_cast(0x2222u + 0x1111u * instance)); + state.right_stick_x = controller_axis_from_unsigned( + static_cast(0x5555u + 0x1111u * instance)); + state.right_stick_y = controller_axis_from_unsigned( + static_cast(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 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(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(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 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"; diff --git a/tests/test_bluepad32_backend_lifecycle_native.py b/tests/test_bluepad32_backend_lifecycle_native.py index 65c0285..7484f39 100644 --- a/tests/test_bluepad32_backend_lifecycle_native.py +++ b/tests/test_bluepad32_backend_lifecycle_native.py @@ -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", diff --git a/tests/xinput_feasibility_test.cpp b/tests/xinput_feasibility_test.cpp index c56131a..41ce9da 100644 --- a/tests/xinput_feasibility_test.cpp +++ b/tests/xinput_feasibility_test.cpp @@ -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"); diff --git a/xinput_feasibility_driver.cpp b/xinput_feasibility_driver.cpp index 2a9f485..bc0b27e 100644 --- a/xinput_feasibility_driver.cpp +++ b/xinput_feasibility_driver.cpp @@ -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; diff --git a/xinput_feasibility_driver.h b/xinput_feasibility_driver.h index b1db301..33e15b8 100644 --- a/xinput_feasibility_driver.h +++ b/xinput_feasibility_driver.h @@ -2,12 +2,13 @@ #include -#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); diff --git a/xinput_feasibility_protocol.h b/xinput_feasibility_protocol.h index 75059c6..43315ca 100644 --- a/xinput_feasibility_protocol.h +++ b/xinput_feasibility_protocol.h @@ -2,7 +2,8 @@ #include -#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(static_cast(value) - 32768); +constexpr int16_t invert_axis(int16_t value) { + return value == INT16_MIN ? INT16_MAX + : static_cast(-value); } -constexpr int16_t vertical_axis(uint16_t value) { - const int16_t horizontal = horizontal_axis(value); - return horizontal == INT16_MIN ? INT16_MAX - : static_cast(-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;