diff --git a/ADAPTER_PARITY_PLAN.md b/ADAPTER_PARITY_PLAN.md index 1fe00ed..3fc5d11 100644 --- a/ADAPTER_PARITY_PLAN.md +++ b/ADAPTER_PARITY_PLAN.md @@ -51,14 +51,12 @@ The AIO firmware currently has: - Switch Pro input, motion, colors, and rumble per slot - per-controller ABXY and motion hotkeys -The Bluetooth, UART, Switch, and XInput paths now share `ControllerState`: +The current Bluetooth backend converts controller input directly into `SwitchInputState`. This is the principal architectural blocker for other USB modes because it: -- 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 +- 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 ## Progress and gap matrix @@ -74,7 +72,6 @@ The Bluetooth, UART, Switch, and XInput paths now share `ControllerState`: | 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. | @@ -83,7 +80,7 @@ The Bluetooth, UART, Switch, and XInput paths now share `ControllerState`: | 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 | Partial | Full analog values are preserved; profile-configurable lower/upper range, curve, and digital threshold remain. | +| Trigger ranges | Missing | Preserve analog values, then add lower/upper range, curve, and digital threshold. | | 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. | @@ -130,36 +127,6 @@ 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 @@ -219,7 +186,7 @@ Do not use heap allocation or virtual dispatch. Select one descriptor family bef ## Delivery phases -### Phase 1 — Protocol-neutral controller state — Complete +### Phase 1 — Protocol-neutral controller state Changes: @@ -237,15 +204,6 @@ 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. @@ -511,17 +469,14 @@ Do not mark a host/controller combination complete from descriptor inspection or ## Next action -Begin Phase 2: generalize endpoint-zero management into a versioned persistent -configuration protocol while preserving pairing commands and Switch -enumeration. +Begin Phase 1: introduce the protocol-neutral controller state while preserving the current Switch and UART behavior. -The first Phase 2 delivery should remain narrow: +The first change should be deliberately narrow: -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 +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 -Do not begin profiles, macros, or tuning transforms until the storage and USB -transaction boundary is proven. +Do not begin profiles, macros, or additional output modes until this boundary is proven. They all depend on it. diff --git a/adapter_host_probe.h b/adapter_host_probe.h index c01f0a2..77629f3 100644 --- a/adapter_host_probe.h +++ b/adapter_host_probe.h @@ -2,11 +2,15 @@ #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 deleted file mode 100644 index f64e442..0000000 --- a/adapter_usb_mode.h +++ /dev/null @@ -1,10 +0,0 @@ -#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 ccaa78a..12d4f29 100644 --- a/bluepad32_input_backend.cpp +++ b/bluepad32_input_backend.cpp @@ -12,20 +12,21 @@ #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 uint16_t kSwitchHostRumbleDurationMs = 50; +constexpr int32_t kTriggerThreshold = (kTriggerMaximum * 35) / 100; #ifdef SWITCH_PICO_ADAPTER_FEASIBILITY -// XInput vibration is stateful and remains active until XInputSetState sends -// a new magnitude. -constexpr uint16_t kXInputHostRumbleDurationMs = UINT16_MAX; +// 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; #endif constexpr uint32_t kRumblePollIntervalMs = 5; constexpr uint8_t kSlotCount = BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; @@ -91,7 +92,6 @@ enum class ConnectionStatus { enum class ConnectionPolicyState { Uninitialized, Open, - Passive, Paused, FailedClosed, }; @@ -99,7 +99,7 @@ enum class ConnectionPolicyState { struct RumbleEnvelope { uint8_t slot; uint32_t connection_generation; - ControllerRumbleOutput rumble; + SwitchRumbleOutput rumble; }; struct FeedbackEnvelope { uint32_t connection_generation; @@ -110,7 +110,7 @@ struct FeedbackEnvelope { struct BackendSlot { - ControllerState state; + SwitchInputState state; // Non-null with active=false is a connected device still becoming ready. uni_hid_device_t* device; uint32_t state_generation; @@ -153,17 +153,13 @@ bool g_pairing_window_open = false; bool g_status_led_on = false; Bluepad32PairingSnapshot g_pairing_snapshot{}; -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(); +SwitchInputState make_neutral_state() { + SwitchInputState state{}; + state.lx = kStickMidpoint; + state.ly = kStickMidpoint; + state.rx = kStickMidpoint; + state.ry = kStickMidpoint; + return state; } bool valid_slot(uint8_t slot) { @@ -229,7 +225,7 @@ ConnectionStatus compute_connection_status() { } void publish_device_state(uint8_t slot, uni_hid_device_t* device, - const ControllerState& state) { + const SwitchInputState& state) { critical_section_enter_blocking(&g_state_lock); BackendSlot& target = g_slots[slot]; if (target.active && target.device == device) { @@ -266,28 +262,14 @@ constexpr int32_t clamp_axis(int32_t value) { return value; } -constexpr int16_t scale_axis(int32_t value) { +constexpr uint16_t scale_stick(int32_t value) { value = clamp_axis(value); if (value <= 0) { - 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( + (static_cast(value - kAxisMinimum) * kStickMidpoint) / -kAxisMinimum); } return static_cast( - (static_cast(value) * UINT16_MAX) / - kTriggerMaximum); + kStickMidpoint + (static_cast(value) * (UINT16_MAX - kStickMidpoint)) / kAxisMaximum); } constexpr int16_t clamp_int16(int64_t value) { @@ -318,11 +300,9 @@ constexpr int16_t convert_gyro(int64_t q10_value) { return clamp_int16(divide_round_nearest(q10_value * kNumeratorScale, kDenominator)); } -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(scale_stick(-512) == 0); +static_assert(scale_stick(0) == 32768); +static_assert(scale_stick(511) == UINT16_MAX); static_assert(convert_accel(8192) == 4096); static_assert(convert_accel(-8192) == -4096); static_assert(convert_gyro(1024) == 14); @@ -337,10 +317,10 @@ bool has_motion(const uni_gamepad_t& gamepad) { return false; } -ControllerState map_gamepad(const uni_gamepad_t& gamepad, - bool swap_abxy, - bool motion_enabled) { - ControllerState state = make_neutral_state(); +SwitchInputState map_gamepad(const uni_gamepad_t& gamepad, + bool swap_abxy, + bool motion_enabled) { + SwitchInputState state = make_neutral_state(); state.dpad_up = (gamepad.dpad & DPAD_UP) != 0; state.dpad_down = (gamepad.dpad & DPAD_DOWN) != 0; @@ -348,52 +328,46 @@ ControllerState 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_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; + 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; if (swap_abxy) { - 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; + 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; } - 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_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_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_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_capture = (gamepad.misc_buttons & MISC_BUTTON_CAPTURE) != 0; - 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); + 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); if (motion_enabled && has_motion(gamepad)) { // Dependency patches normalize both arrays to SDL3 PlayStation axes. - ControllerMotionSample sample{}; + SwitchImuSample 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.motion_sample_count = 3; - for (ControllerMotionSample& destination : state.motion_samples) { + state.imu_sample_count = 3; + for (SwitchImuSample& destination : state.imu_samples) { destination = sample; } } @@ -657,40 +631,26 @@ void process_clear_pairings(uint32_t now_ms) { void apply_connection_policy() { const bool free_slot = has_free_slot(); - const bool active_controller = has_active_controller(); - const bool pairing_open = - pairing_window_active_at(btstack_run_loop_get_time_ms()); - const bool active_scan = - free_slot && (!active_controller || pairing_open); - const ConnectionPolicyState desired_state = - !free_slot - ? ConnectionPolicyState::Paused - : (active_scan ? ConnectionPolicyState::Open - : ConnectionPolicyState::Passive); - if (g_connection_policy_state == desired_state) { + if ((!free_slot && + g_connection_policy_state == ConnectionPolicyState::Paused) || + (free_slot && + g_connection_policy_state == ConnectionPolicyState::Open)) { return; } - // Classic inquiry and BLE scanning consume radio time and measurably delay - // active controller HID traffic. Stop them before every policy transition. + uni_bt_allow_incoming_connections(false); uni_bt_stop_scanning_unsafe(); if (!free_slot) { - uni_bt_allow_incoming_connections(false); g_connection_policy_state = ConnectionPolicyState::Paused; return; } - // Passive mode still accepts controller-initiated reconnects without - // running inquiry. Active discovery is reserved for zero-controller idle - // state and the explicit BOOTSEL pairing window. + // Bluepad32's normal scan/autoconnect path handles both remembered + // controllers powering on and controllers in explicit pairing mode. uni_bt_allow_incoming_connections(true); - if (active_scan) { - uni_bt_start_scanning_and_autoconnect_unsafe(); - g_connection_policy_state = ConnectionPolicyState::Open; - } else { - g_connection_policy_state = ConnectionPolicyState::Passive; - } + uni_bt_start_scanning_and_autoconnect_unsafe(); + g_connection_policy_state = ConnectionPolicyState::Open; } void update_status_led() { @@ -723,9 +683,7 @@ void process_rumble_timer(btstack_timer_source_t* timer) { const uint32_t now_ms = btstack_run_loop_get_time_ms(); process_clear_pairings(now_ms); process_pairing_snapshot_request(); - if (update_pairing_window(now_ms)) { - apply_connection_policy(); - } + update_pairing_window(now_ms); for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) { RumbleEnvelope envelope{}; @@ -778,7 +736,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 : host_rumble_duration_ms(), + device, 0, stop ? 0 : kRumbleDurationMs, envelope.rumble.high_frequency_magnitude, envelope.rumble.low_frequency_magnitude); } @@ -833,8 +791,7 @@ void platform_on_device_connected(uni_hid_device_t* device) { if (device == nullptr) { return; } - if (g_connection_policy_state != ConnectionPolicyState::Open && - g_connection_policy_state != ConnectionPolicyState::Passive) { + if (g_connection_policy_state != ConnectionPolicyState::Open) { uni_hid_device_disconnect(device); return; } @@ -886,9 +843,9 @@ void platform_on_device_disconnected(uni_hid_device_t* device) { critical_section_exit(&g_state_lock); if (disconnected_tracked_device) { - // Re-evaluate from scratch: resume discovery only after the final - // active controller disconnects; otherwise keep passive incoming - // reconnect support without inquiry-induced latency. + // A controller can disconnect while the policy is already Open. + // Restart both scans so host-initiated reconnect controllers such as + // 8BitDo Ultimate become reachable without rebooting the Pico. g_connection_policy_state = ConnectionPolicyState::Uninitialized; recompute_connection_status(); } @@ -1109,8 +1066,7 @@ void bluepad32_input_backend_pairing_snapshot( } -bool bluepad32_input_backend_snapshot(uint8_t slot_index, - ControllerState* out) { +bool bluepad32_input_backend_snapshot(uint8_t slot_index, SwitchInputState* out) { if (out == nullptr || !valid_slot(slot_index)) { return false; } @@ -1126,7 +1082,7 @@ bool bluepad32_input_backend_snapshot(uint8_t slot_index, critical_section_exit(&g_state_lock); if (generation == g_consumed_generation[slot_index]) { - out->motion_sample_count = 0; + out->imu_sample_count = 0; } g_last_snapshot_generation[slot_index] = generation; return controller_active; @@ -1139,8 +1095,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 ControllerRumbleOutput& rumble) { +void bluepad32_input_backend_queue_rumble(uint8_t slot_index, + const SwitchRumbleOutput& rumble) { if (!g_initialized || !valid_slot(slot_index)) { return; } diff --git a/bluepad32_input_backend.h b/bluepad32_input_backend.h index d7bdaa6..74ed309 100644 --- a/bluepad32_input_backend.h +++ b/bluepad32_input_backend.h @@ -2,9 +2,8 @@ #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; @@ -38,10 +37,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, ControllerState* out); +bool bluepad32_input_backend_snapshot(uint8_t slot, SwitchInputState* 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 ControllerRumbleOutput& rumble); +void bluepad32_input_backend_queue_rumble(uint8_t slot, + const SwitchRumbleOutput& rumble); diff --git a/build.py b/build.py index a46585d..5bb0a45 100644 --- a/build.py +++ b/build.py @@ -13,18 +13,11 @@ 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() @@ -41,17 +34,11 @@ def parse_args(): formatter_class=argparse.RawDescriptionHelpFormatter, epilog="Default behavior leaves controller_color_config.h unchanged.", ) - mode_group = parser.add_mutually_exclusive_group() - mode_group.add_argument( + parser.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", @@ -131,22 +118,19 @@ def resolve_picotool(): def build( aio, - adapter_feasibility, build_dir, elf_path, uf2_path, firmware_elf_path, firmware_uf2_path, ): - if aio or adapter_feasibility: + if aio: 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", @@ -212,13 +196,7 @@ def main(): update_grip_colors(color) print(f"Grip color set to #{color} in {CONFIG_FILE.name}") - 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: + if args.aio: build_dir = AIO_BUILD_DIR elf_path = AIO_BUILD_DIR / "switch-pico.elf" uf2_path = AIO_BUILD_DIR / "switch-pico.uf2" @@ -233,17 +211,13 @@ 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 and not args.adapter_feasibility, - ) + flash(elf_path, allow_elf_override=not args.aio) if __name__ == "__main__": main() diff --git a/controller_color.h b/controller_color.h deleted file mode 100644 index 37149ee..0000000 --- a/controller_color.h +++ /dev/null @@ -1,42 +0,0 @@ -#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 deleted file mode 100644 index b2b8392..0000000 --- a/controller_state.h +++ /dev/null @@ -1,92 +0,0 @@ -#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 deleted file mode 100755 index 0b6642e..0000000 Binary files a/firmware/switch-pico-adapter-feasibility.elf and /dev/null differ diff --git a/firmware/switch-pico-adapter-feasibility.uf2 b/firmware/switch-pico-adapter-feasibility.uf2 index f5f6109..ec4a41e 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 ed48956..f0fa733 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 a3a4786..6f93f85 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 00900a6..25a3b3d 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 69063a8..4593404 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 3b03c1f..5ba7ef7 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..0105c53 100644 +index 599fc35..9f073b4 100644 --- a/src/components/bluepad32/parser/uni_hid_parser_switch.c +++ b/src/components/bluepad32/parser/uni_hid_parser_switch.c -@@ -51,13 +51,22 @@ static const int16_t DEFAULT_ACCEL_OFFSET = 0; +@@ -51,13 +51,15 @@ 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,17 +276,10 @@ index 599fc35..0105c53 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 +81,7 @@ enum switch_state { +@@ -72,6 +74,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 @@ -294,7 +287,7 @@ index 599fc35..0105c53 100644 STATE_DUMP_FLASH, // Dump SPI Flash memory STATE_UPDATE_LED, // Update LEDs STATE_READY, // Gamepad setup ready! -@@ -111,6 +121,7 @@ enum switch_subcmd { +@@ -111,6 +114,7 @@ enum switch_subcmd { SUBCMD_SPI_FLASH_READ = 0x10, SUBCMD_SET_PLAYER_LEDS = 0x30, SUBCMD_ENABLE_IMU = 0x40, @@ -302,7 +295,7 @@ index 599fc35..0105c53 100644 }; typedef enum { -@@ -137,6 +148,7 @@ typedef struct switch_instance_s { +@@ -137,6 +141,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; @@ -310,7 +303,7 @@ index 599fc35..0105c53 100644 switch_state_rumble_t rumble_state; btstack_timer_source_t setup_timer; -@@ -322,6 +334,7 @@ static void fsm_read_user_stick_calibration(struct uni_hid_device_s* d); +@@ -322,6 +327,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); @@ -318,7 +311,7 @@ index 599fc35..0105c53 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 +346,16 @@ static void process_reply_set_report_mode(struct uni_hid_device_s* d, const stru +@@ -333,11 +339,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); @@ -335,7 +328,7 @@ index 599fc35..0105c53 100644 static void switch_play_dual_rumble_now(uni_hid_device_t* d, uint16_t duration_ms, uint8_t weak_magnitude, -@@ -451,6 +469,10 @@ static void process_fsm(struct uni_hid_device_s* d) { +@@ -451,6 +462,10 @@ static void process_fsm(struct uni_hid_device_s* d) { break; case STATE_ENABLE_IMU: logd("STATE_ENABLE_IMU\n"); @@ -346,7 +339,7 @@ index 599fc35..0105c53 100644 fsm_dump_rom(d); break; case STATE_DUMP_FLASH: -@@ -725,6 +747,12 @@ static void process_reply_enable_imu(struct uni_hid_device_s* d, const struct sw +@@ -725,6 +740,12 @@ static void process_reply_enable_imu(struct uni_hid_device_s* d, const struct sw ARG_UNUSED(r); ARG_UNUSED(len); } @@ -359,7 +352,7 @@ index 599fc35..0105c53 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 +780,9 @@ static void process_input_subcmd_reply(struct uni_hid_device_s* d, const uint8_t +@@ -752,6 +773,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; @@ -369,7 +362,7 @@ index 599fc35..0105c53 100644 default: loge("Switch: Error, unexpected subcmd_id=0x%02x in report 0x21\n", r->subcmd_id); break; -@@ -823,19 +854,26 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) { +@@ -823,19 +847,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; @@ -405,7 +398,7 @@ index 599fc35..0105c53 100644 if (ins->controller_type == SWITCH_CONTROLLER_TYPE_JCR) { accel[1] = -accel[1]; accel[2] = -accel[2]; -@@ -843,10 +881,13 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) { +@@ -843,10 +874,13 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) { gyro[2] = -gyro[2]; } @@ -423,7 +416,7 @@ index 599fc35..0105c53 100644 } // Process 0x30 input report: SWITCH_INPUT_IMU_DATA -@@ -1172,6 +1213,18 @@ static void fsm_enable_imu(struct uni_hid_device_s* d) { +@@ -1172,6 +1206,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)); } @@ -442,21 +435,18 @@ index 599fc35..0105c53 100644 static void fsm_update_led(struct uni_hid_device_s* d) { switch_instance_t* ins = get_switch_instance(d); -@@ -1203,6 +1256,13 @@ static struct switch_rumble_freq_data find_rumble_freq(uint16_t freq) { +@@ -1203,6 +1249,10 @@ 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 * SWITCH_RUMBLE_AMPLITUDE_MAX + -+ SWITCH_RUMBLE_AMPLITUDE_ROUNDING_BIAS) / -+ SWITCH_RUMBLE_MAGNITUDE_MAX); ++ return (uint16_t)(((uint32_t)magnitude * 1003 + 127) / 255); +} + static struct switch_rumble_amp_data find_rumble_amp(uint16_t amp) { unsigned int i = 0; if (amp > rumble_amps[0].amp) { -@@ -1259,6 +1319,7 @@ void uni_hid_parser_switch_play_dual_rumble(struct uni_hid_device_s* d, +@@ -1259,6 +1309,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); @@ -464,7 +454,7 @@ index 599fc35..0105c53 100644 break; default: // Do nothing -@@ -1366,6 +1427,7 @@ static void switch_stop_rumble_now(uni_hid_device_t* d) { +@@ -1366,6 +1417,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); @@ -472,7 +462,7 @@ index 599fc35..0105c53 100644 ins->rumble_state = SWITCH_STATE_RUMBLE_DISABLED; struct switch_subcmd_request req = {0}; -@@ -1379,6 +1441,24 @@ static void switch_stop_rumble_now(uni_hid_device_t* d) { +@@ -1379,6 +1431,22 @@ static void switch_stop_rumble_now(uni_hid_device_t* d) { send_subcmd(d, (struct switch_subcmd_request*)&req, sizeof(req) - 1); } @@ -484,11 +474,9 @@ index 599fc35..0105c53 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, SWITCH_RUMBLE_LOW_FREQUENCY_HZ, -+ SWITCH_RUMBLE_LEFT_HIGH_FREQUENCY_HZ, ++ switch_encode_rumble(req.rumble_left, 453, 135, + switch_magnitude_to_amp(weak_magnitude)); -+ switch_encode_rumble(req.rumble_right, SWITCH_RUMBLE_LOW_FREQUENCY_HZ, -+ SWITCH_RUMBLE_RIGHT_HIGH_FREQUENCY_HZ, ++ switch_encode_rumble(req.rumble_right, 453, 99, + 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); @@ -497,7 +485,7 @@ index 599fc35..0105c53 100644 static void switch_play_dual_rumble_now(uni_hid_device_t* d, uint16_t duration_ms, uint8_t weak_magnitude, -@@ -1391,14 +1471,17 @@ static void switch_play_dual_rumble_now(uni_hid_device_t* d, +@@ -1391,14 +1459,17 @@ static void switch_play_dual_rumble_now(uni_hid_device_t* d, return; } @@ -522,7 +510,7 @@ index 599fc35..0105c53 100644 // Set timer to turn off rumble ins->rumble_timer_duration.process = &on_switch_set_rumble_off; -@@ -1414,6 +1497,20 @@ static void on_switch_set_rumble_on(btstack_timer_source_t* ts) { +@@ -1414,6 +1485,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 361021f..3a18b30 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 ControllerState +static SwitchInputState g_user_states[BLUEPAD32_INPUT_BACKEND_SLOT_COUNT]{}; #else static constexpr uint8_t SWITCH_HID_INSTANCE = 0; static bool g_last_ready = false; -static ControllerState g_user_state; +static SwitchInputState g_user_state; #endif static bool g_last_mounted = false; @@ -53,12 +53,17 @@ static void init_uart_input() { } #endif -static ControllerState neutral_input() { - return controller_neutral_state(); +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; } #ifndef SWITCH_PICO_BLUEPAD32 -static void send_rumble_uart_frame(const ControllerRumbleOutput& rumble) { +static void send_rumble_uart_frame(const SwitchRumbleOutput& rumble) { uint8_t frame[5] = { UART_RUMBLE_HEADER, UART_RUMBLE_TYPE, @@ -75,7 +80,7 @@ static void send_rumble_uart_frame(const ControllerRumbleOutput& rumble) { #endif static void on_rumble_from_switch(uint8_t instance, - const ControllerRumbleOutput& rumble) { + const SwitchRumbleOutput& rumble) { #ifdef SWITCH_PICO_BLUEPAD32 if (instance >= BLUEPAD32_INPUT_BACKEND_SLOT_COUNT) { return; @@ -132,33 +137,30 @@ static bool poll_uart_frames() { } if (expected_len > 0 && index >= expected_len) { - ControllerState parsed{}; + SwitchInputState 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_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_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_capture?SWITCH_PRO_MASK_CAPTURE:0) | - (parsed.button_left_stick ? SWITCH_PRO_MASK_L3 : 0) | - (parsed.button_right_stick ? SWITCH_PRO_MASK_R3 : 0), + (parsed.button_l3 ? SWITCH_PRO_MASK_L3 : 0) | + (parsed.button_r3 ? 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, - 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); + parsed.lx >> 8, parsed.ly >> 8, parsed.rx >> 8, parsed.ry >> 8); } index = 0; expected_len = 0; @@ -315,7 +317,7 @@ int main() { #else bool new_data = poll_uart_frames(); // Pull controller state from UART1 (void)new_data; - ControllerState state = g_user_state; + SwitchInputState 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 8bade37..84d9b96 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); } -ControllerRumbleOutput SwitchHapticsDecoder::decode(const uint8_t payload[8]) { +SwitchRumbleOutput 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 9d8c75a..15ae502 100644 --- a/switch_haptics.h +++ b/switch_haptics.h @@ -4,13 +4,10 @@ #include #include -struct ControllerRumbleOutput { +struct SwitchRumbleOutput { 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, @@ -22,7 +19,7 @@ public: SwitchHapticsDecoder(); void reset(); - ControllerRumbleOutput decode(const uint8_t payload[8]); + SwitchRumbleOutput decode(const uint8_t payload[8]); private: struct ActuatorState { diff --git a/switch_pro_driver.cpp b/switch_pro_driver.cpp index 631ad2b..9cc7490 100644 --- a/switch_pro_driver.cpp +++ b/switch_pro_driver.cpp @@ -25,6 +25,7 @@ // (~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, @@ -42,7 +43,7 @@ struct MotionQuaternion { }; struct SwitchProContext { - ControllerState input_state{}; + SwitchInputState input_state{}; uint8_t report_buffer[SWITCH_PRO_ENDPOINT_SIZE]{}; SwitchProReport switch_report{}; uint8_t last_report_counter = 0; @@ -64,7 +65,7 @@ struct SwitchProContext { uint16_t right_min_y = 0; uint16_t right_max_x = 0; uint16_t right_max_y = 0; - ControllerRumbleCallback rumble_callback = nullptr; + SwitchRumbleCallback rumble_callback = nullptr; SwitchHapticsDecoder rumble_decoder{}; MotionQuaternion motion_quaternion{0.0f, 0.0f, 0.0f, 1.0f, 0, 0, 0}; }; @@ -261,7 +262,7 @@ static void write_bits_le(uint8_t* dst, uint16_t bit_offset, uint32_t value, } static void integrate_motion_sample(SwitchProContext& context, - const ControllerMotionSample& sample) { + const SwitchImuSample& sample) { constexpr float sample_dt = 0.005f; constexpr float gyro_rad_per_lsb = 1.0f / 818.5f; MotionQuaternion& quaternion = context.motion_quaternion; @@ -309,21 +310,19 @@ static void integrate_motion_sample(SwitchProContext& context, } static void fill_raw_imu_report_data(SwitchProContext& context, - const ControllerState& state) { - if (state.motion_sample_count == 0) { + const SwitchInputState& state) { + if (state.imu_sample_count == 0) { memset(context.switch_report.imuData, 0x00, sizeof(context.switch_report.imuData)); return; } uint8_t sample_count = - state.motion_sample_count > CONTROLLER_MOTION_SAMPLE_CAPACITY - ? CONTROLLER_MOTION_SAMPLE_CAPACITY - : state.motion_sample_count; + state.imu_sample_count > 3 ? 3 : state.imu_sample_count; uint8_t* dst = context.switch_report.imuData; - 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]; + for (uint8_t i = 0; i < 3; ++i) { + const SwitchImuSample& sample = + (i < sample_count) ? state.imu_samples[i] + : state.imu_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); @@ -334,18 +333,16 @@ static void fill_raw_imu_report_data(SwitchProContext& context, } } -static void fill_quaternion_imu_report_data( - SwitchProContext& context, const ControllerState& state, - uint32_t now_ms) { - if (state.motion_sample_count > 0) { +static void fill_quaternion_imu_report_data(SwitchProContext& context, + const SwitchInputState& state, + uint32_t now_ms) { + if (state.imu_sample_count > 0) { uint8_t sample_count = - 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]; + 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]; integrate_motion_sample(context, sample); } } @@ -394,7 +391,7 @@ static void fill_quaternion_imu_report_data( } static void fill_imu_report_data(SwitchProContext& context, - const ControllerState& state, + const SwitchInputState& state, uint32_t now_ms) { switch (context.imu_mode) { case SwitchImuMode::Raw: @@ -413,8 +410,14 @@ static void fill_imu_report_data(SwitchProContext& context, static void update_switch_report_from_state(SwitchProContext& context); -static ControllerState make_neutral_state() { - return controller_neutral_state(); +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 void reset_context_runtime(SwitchProContext& context, uint32_t now, @@ -519,8 +522,7 @@ static void forward_decoded_rumble(uint8_t instance, return; } - ControllerRumbleOutput rumble = - context.rumble_decoder.decode(report + 2); + SwitchRumbleOutput rumble = context.rumble_decoder.decode(report + 2); if (context.rumble_callback != nullptr) { context.rumble_callback(instance, rumble); } @@ -732,42 +734,36 @@ static void handle_feature_report(SwitchProContext& context, } static void update_switch_report_from_state(SwitchProContext& context) { - const ControllerState& state = context.input_state; + const SwitchInputState& 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_west; - inputs.buttonX = state.button_north; - inputs.buttonB = state.button_south; - inputs.buttonA = state.button_east; + inputs.buttonY = state.button_y; + inputs.buttonX = state.button_x; + inputs.buttonB = state.button_b; + inputs.buttonA = state.button_a; inputs.buttonRightSR = 0; inputs.buttonRightSL = 0; - 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.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.buttonCapture = state.button_capture; inputs.buttonLeftSR = 0; inputs.buttonLeftSL = 0; - inputs.buttonL = state.button_left_shoulder; - inputs.buttonZL = - state.left_trigger >= SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD; + inputs.buttonL = state.button_l; + inputs.buttonZL = state.button_zl; - 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)); + 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); inputs.leftStick.setX( std::min(std::max(left_x, context.left_min_x), context.left_max_x)); @@ -813,7 +809,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 ControllerState& state) { +void switch_pro_set_input(uint8_t instance, const SwitchInputState& state) { SwitchProContext* context = context_for(instance); if (context != nullptr) { context->input_state = state; @@ -858,7 +854,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.motion_sample_count = 0; + context->input_state.imu_sample_count = 0; regular_report_sent = true; } context->last_report_timer = now; @@ -882,12 +878,18 @@ bool switch_pro_task(uint8_t instance) { } bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length, - ControllerState& out_state) { + SwitchInputState& out_state) { if (packet == nullptr) { return false; } // v2 format: 0xAA + 0x02 + payload_len + payload... + checksum - if (length < 12 || packet[0] != 0xAA || packet[1] != 0x02) { + if (length < 12) { + return false; + } + if (packet[0] != 0xAA) { + return false; + } + if (packet[1] != 0x02) { return false; } @@ -904,115 +906,86 @@ bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length, return false; } - // payload: buttons(2 LE), hat, lx, ly, rx, ry, motion_count, - // [motion_samples...] + // payload: buttons(2 LE), hat, lx, ly, rx, ry, imu_count, [imu_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 motion_count = packet[10]; - if (motion_count > CONTROLLER_MOTION_SAMPLE_CAPACITY) { - motion_count = CONTROLLER_MOTION_SAMPLE_CAPACITY; + uint8_t imu_count = packet[10]; + if (imu_count > 3) { + imu_count = 3; } - uint16_t required_payload_len = static_cast( - 8u + static_cast(motion_count) * 12u); + uint16_t required_payload_len = static_cast(8u + static_cast(imu_count) * 12u); if (payload_len < required_payload_len) { return false; } - auto expand_axis = [](uint8_t value) -> int16_t { - const uint16_t expanded = - static_cast(value) << 8 | value; - return controller_axis_from_unsigned(expanded); - }; - auto read_int16 = [](const uint8_t* src) -> int16_t { - return static_cast( - static_cast(src[0]) | - (static_cast(src[1]) << 8)); + auto expand_axis = [](uint8_t v) -> uint16_t { + return static_cast(v) << 8 | v; }; - ControllerState state = make_neutral_state(); - state.motion_sample_count = motion_count; - for (uint8_t i = 0; i < motion_count; ++i) { + 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)); + }; + for (uint8_t i = 0; i < imu_count; ++i) { const uint8_t* base = &packet[11 + i * 12]; - 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); + 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); } 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_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.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.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); + state.lx = expand_axis(out.lx); + state.ly = expand_axis(out.ly); + state.rx = expand_axis(out.rx); + state.ry = expand_axis(out.ry); out_state = state; return true; } -void switch_pro_set_rumble_callback( - uint8_t instance, ControllerRumbleCallback callback) { +void switch_pro_set_rumble_callback(uint8_t instance, + SwitchRumbleCallback 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 bb1d9ba..74e4346 100644 --- a/switch_pro_driver.h +++ b/switch_pro_driver.h @@ -8,27 +8,95 @@ #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 ControllerState& state); +void switch_pro_set_input(uint8_t instance, const SwitchInputState& state); // Drive one Switch Pro USB state machine; returns true only when a regular // 0x30 input report was successfully queued. @@ -36,10 +104,14 @@ 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, - ControllerState& out_state); + SwitchInputState& 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, - ControllerRumbleCallback callback); \ No newline at end of file + SwitchRumbleCallback callback); diff --git a/tests/bluepad32_backend_lifecycle_test.cpp b/tests/bluepad32_backend_lifecycle_test.cpp index e539272..db668ed 100644 --- a/tests/bluepad32_backend_lifecycle_test.cpp +++ b/tests/bluepad32_backend_lifecycle_test.cpp @@ -285,13 +285,6 @@ 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, @@ -362,7 +355,7 @@ void test_ready_order(bool reverse) { "ready device must bind to its Bluepad index"); for (int candidate = 0; candidate < kSlotCount; ++candidate) { - ControllerState snapshot{}; + SwitchInputState snapshot{}; bool expected_active = false; for (int ready = 0; ready <= position; ++ready) { expected_active = expected_active || order[ready] == candidate; @@ -393,7 +386,7 @@ void test_ready_order(bool reverse) { "disconnecting any slot must resume connection policy"); for (int candidate = 0; candidate < kSlotCount; ++candidate) { - ControllerState snapshot{}; + SwitchInputState snapshot{}; require(bluepad32_input_backend_snapshot(candidate, &snapshot) == (candidate != slot), "disconnect must preserve every surviving slot"); @@ -434,10 +427,10 @@ void test_rejections() { collision_data.klass = UNI_CONTROLLER_CLASS_GAMEPAD; collision_data.gamepad.buttons = BUTTON_B; platform_on_controller_data(&collision, &collision_data); - ControllerState snapshot{}; + SwitchInputState snapshot{}; require(bluepad32_input_backend_snapshot(0, &snapshot), "occupied slot must stay active"); - require(!snapshot.button_east, + require(!snapshot.button_a, "mismatched device input must not enter the occupied slot"); uni_controller_t slot_zero_data{}; @@ -445,15 +438,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.motion_sample_count == 3, + snapshot.imu_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.motion_sample_count == 3, + snapshot.imu_sample_count == 3, "slot 4 acknowledgement must not consume slot 0 IMU"); - bluepad32_input_backend_queue_rumble(4, ControllerRumbleOutput{1, 2}); + bluepad32_input_backend_queue_rumble(4, SwitchRumbleOutput{1, 2}); process_rumble_timer(&g_rumble_timer); require(slot_zero.rumble_calls == 0, "slot 4 rumble must not reach a valid controller"); @@ -541,39 +534,39 @@ void test_independent_lifecycle() { platform_on_controller_data(&devices[slot], &data[slot]); } - ControllerState states[kSlotCount]{}; + SwitchInputState states[kSlotCount]{}; for (int slot = 0; slot < kSlotCount; ++slot) { require(bluepad32_input_backend_snapshot(slot, &states[slot]) && - states[slot].motion_sample_count == 3, + states[slot].imu_sample_count == 3, "every slot must expose independent input and IMU"); } - require(states[0].button_east && !states[0].button_south && - !states[0].button_west && !states[0].button_north, + require(states[0].button_a && !states[0].button_b && + !states[0].button_y && !states[0].button_x, "slot 0 must contain only slot 0 input"); - require(states[1].button_south && !states[1].button_east && - !states[1].button_west && !states[1].button_north, + require(states[1].button_b && !states[1].button_a && + !states[1].button_y && !states[1].button_x, "slot 1 must contain only slot 1 input"); - require(states[2].button_west && !states[2].button_east && - !states[2].button_south && !states[2].button_north, + require(states[2].button_y && !states[2].button_a && + !states[2].button_b && !states[2].button_x, "slot 2 must contain only slot 2 input"); - require(states[3].button_north && !states[3].button_east && - !states[3].button_south && !states[3].button_west, + require(states[3].button_x && !states[3].button_a && + !states[3].button_b && !states[3].button_y, "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].motion_sample_count == (slot == 3 ? 0 : 3), + states[slot].imu_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].motion_sample_count == 0, + states[slot].imu_sample_count == 0, "each slot acknowledgement must consume only its own IMU"); } - const ControllerRumbleOutput initial_rumble[kSlotCount] = { + const SwitchRumbleOutput 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]); @@ -584,17 +577,17 @@ void test_independent_lifecycle() { devices[slot].last_low == 11 + slot && devices[slot].last_high == 21 + slot && devices[slot].last_rumble_duration_ms == - host_rumble_duration_ms(), + kRumbleDurationMs, "each slot rumble must reach only its indexed controller"); } - bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{0, 0}); + bluepad32_input_backend_queue_rumble(0, SwitchRumbleOutput{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, ControllerRumbleOutput{55, 66}); + bluepad32_input_backend_queue_rumble(3, SwitchRumbleOutput{55, 66}); const uint32_t disconnected_generation = g_slots[3].connection_generation; const int starts_before_slot_three_disconnect = scan_starts; @@ -603,21 +596,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_north && states[3].left_stick_x == 0, - "slot 3 disconnect must publish protocol-neutral state"); + !states[3].button_x && states[3].lx == 32768, + "slot 3 disconnect must neutralize only slot 3"); require(bluepad32_input_backend_snapshot(0, &states[0]) && - states[0].button_east && + states[0].button_a && bluepad32_input_backend_snapshot(1, &states[1]) && - states[1].button_south && + states[1].button_b && bluepad32_input_backend_snapshot(2, &states[2]) && - states[2].button_west, + states[2].button_y, "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_east, + states[0].button_a, "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, ControllerRumbleOutput{115, 116}); + bluepad32_input_backend_queue_rumble(0, SwitchRumbleOutput{115, 116}); tick_backend_timer(99); require(devices[0].rumble_calls == slot_zero_calls_while_scanning + 1 && devices[0].last_low == 115 && @@ -633,7 +626,7 @@ void test_independent_lifecycle() { "slot 3 replacement must not receive disconnected device rumble"); g_slots[3].pending_rumble = { - 3, disconnected_generation, ControllerRumbleOutput{77, 88}}; + 3, disconnected_generation, SwitchRumbleOutput{77, 88}}; g_slots[3].rumble_pending = true; process_rumble_timer(&g_rumble_timer); require(slot_three_replacement.rumble_calls == 0, @@ -645,21 +638,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_north && states[3].motion_sample_count == 3, + states[3].button_x && states[3].imu_sample_count == 3, "replacement input and IMU must populate only slot 3"); require(bluepad32_input_backend_snapshot(0, &states[0]) && - states[0].button_east && + states[0].button_a && bluepad32_input_backend_snapshot(1, &states[1]) && - states[1].button_south && + states[1].button_b && bluepad32_input_backend_snapshot(2, &states[2]) && - states[2].button_west, + states[2].button_y, "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, ControllerRumbleOutput{90, 91}); + bluepad32_input_backend_queue_rumble(3, SwitchRumbleOutput{90, 91}); process_rumble_timer(&g_rumble_timer); require(slot_three_replacement.rumble_calls == 1 && slot_three_replacement.last_low == 90 && @@ -676,8 +669,8 @@ void test_independent_lifecycle() { slot_three_replacement.rumble_calls}; for (int slot = 0; slot < kSlotCount; ++slot) { bluepad32_input_backend_queue_rumble( - slot, ControllerRumbleOutput{static_cast(100 + slot), - static_cast(110 + slot)}); + slot, SwitchRumbleOutput{static_cast(100 + slot), + static_cast(110 + slot)}); } process_rumble_timer(&g_rumble_timer); for (int slot = 0; slot < kSlotCount; ++slot) { @@ -698,8 +691,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].left_stick_x == 0, - "disconnect must publish protocol-neutral state"); + states[slot].lx == 32768, + "disconnect must neutralize its indexed slot"); for (int survivor = 0; survivor < kSlotCount; ++survivor) { if (survivor == slot) { continue; @@ -719,9 +712,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, ControllerRumbleOutput{119, 120}); - bluepad32_input_backend_queue_rumble(3, ControllerRumbleOutput{121, 122}); - bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{123, 124}); + 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}); process_rumble_timer(&g_rumble_timer); require(slot_three_replacement.rumble_calls == slot_three_calls_before_mailboxes + 1 && @@ -815,23 +808,13 @@ void test_pairing_window_policy() { g_connection_policy_state == ConnectionPolicyState::Paused, "Classic and BLE pairing authentication must close at the deadline"); platform_on_device_disconnected(&devices[3]); - require(g_connection_policy_state == ConnectionPolicyState::Passive && - !classic_scanning_enabled && !scanning_enabled && - incoming_connections, - "a freed slot with active controllers must remain passive"); - require(platform_on_device_discovered(address, "controller", 0, 0) == - UNI_ERROR_IGNORE_DEVICE, - "passive policy must reject inquiry discoveries"); - - bluepad32_input_backend_open_pairing_window(); - process_rumble_timer(&g_rumble_timer); require(g_connection_policy_state == ConnectionPolicyState::Open && classic_scanning_enabled && scanning_enabled && incoming_connections, - "explicit BOOTSEL window must resume active discovery"); + "a freed slot must resume autoconnect after pairing indication expires"); require(platform_on_device_discovered(address, "controller", 0, 0) == UNI_ERROR_SUCCESS, - "pairing window discovery must accept a controller"); + "resumed autoconnect must accept a discovered controller"); } @@ -868,11 +851,11 @@ void test_slot_lighting() { "controller without RGB support did not receive its slot LED"); } -void require_south_button_mapping(const ControllerState& state, +void require_south_button_mapping(const SwitchInputState& state, bool swapped, const char* message) { - require(state.button_east == swapped && state.button_south == !swapped && - !state.button_north && !state.button_west, + require(state.button_a == swapped && state.button_b == !swapped && + !state.button_x && !state.button_y, message); } @@ -888,7 +871,7 @@ void test_abxy_hotkey() { input.klass = UNI_CONTROLLER_CLASS_GAMEPAD; input.gamepad.buttons = BUTTON_A; platform_on_controller_data(&slot_zero, &input); - ControllerState snapshot{}; + SwitchInputState snapshot{}; require(bluepad32_input_backend_snapshot(0, &snapshot), "slot 0 ABXY state was not published"); require_south_button_mapping( @@ -904,12 +887,12 @@ void test_abxy_hotkey() { require_south_button_mapping( snapshot, !kDefaultSwapAbxy, "hotkey did not toggle slot 0 ABXY mapping"); - require(!snapshot.button_left_shoulder && !snapshot.button_right_shoulder && - !snapshot.button_select && !snapshot.button_start, + require(!snapshot.button_l && !snapshot.button_r && + !snapshot.button_minus && !snapshot.button_plus, "hotkey chord leaked into the Switch report"); bluepad32_input_backend_queue_rumble( - 0, ControllerRumbleOutput{0x11, 0x22}); + 0, SwitchRumbleOutput{0x11, 0x22}); process_rumble_timer(&g_rumble_timer); require(slot_zero.rumble_calls == 1 && slot_zero.last_high == kAbxyFeedbackWeakMagnitude && @@ -978,9 +961,9 @@ void test_motion_hotkey() { input.klass = UNI_CONTROLLER_CLASS_GAMEPAD; input.gamepad.accel[0] = 8192; platform_on_controller_data(&slot_zero, &input); - ControllerState snapshot{}; + SwitchInputState snapshot{}; require(bluepad32_input_backend_snapshot(0, &snapshot) && - snapshot.motion_sample_count == + snapshot.imu_sample_count == (kDefaultMotionEnabled ? 3 : 0), "slot 0 did not start with configured motion state"); @@ -989,10 +972,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.motion_sample_count == + snapshot.imu_sample_count == (kDefaultMotionEnabled ? 0 : 3) && - !snapshot.dpad_up && !snapshot.button_right_shoulder && - !snapshot.button_start, + !snapshot.dpad_up && !snapshot.button_r && + !snapshot.button_plus, "motion chord did not toggle motion or suppress its inputs"); process_rumble_timer(&g_rumble_timer); @@ -1022,7 +1005,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.motion_sample_count == + snapshot.imu_sample_count == (kDefaultMotionEnabled ? 3 : 0), "slot 0 motion chord changed slot 1 motion state"); @@ -1034,7 +1017,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.motion_sample_count == + snapshot.imu_sample_count == (kDefaultMotionEnabled ? 3 : 0), "released motion chord did not re-arm or restore motion"); process_rumble_timer(&g_rumble_timer); @@ -1051,76 +1034,6 @@ 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; @@ -1144,7 +1057,7 @@ void test_clear_pairings() { "pairing reset controller did not become ready"); } bluepad32_input_backend_queue_rumble( - 0, ControllerRumbleOutput{100, 101}); + 0, SwitchRumbleOutput{100, 101}); bluepad32_input_backend_clear_pairings(); require(g_clear_pairings_requested && delete_key_calls == 0 && @@ -1163,11 +1076,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.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, + slot.state.lx == kStickMidpoint && + slot.state.ly == kStickMidpoint && + slot.state.rx == kStickMidpoint && + slot.state.ry == kStickMidpoint && + slot.state.imu_sample_count == 0, "pairing reset must publish neutral empty slots"); } require(!g_pairing_window_open && !bondable && @@ -1240,10 +1153,6 @@ 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 bec8a15..96d6e22 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, ControllerRumbleOutput actual, +void expect_output(const char* scenario, SwitchRumbleOutput 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 3130f38..baa62b4 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; - ControllerRumbleOutput output{}; + SwitchRumbleOutput output{}; }; uint64_t now_ms = 0; @@ -95,8 +95,7 @@ SwitchProReport get_current_report(uint8_t instance, void expect_neutral_sticks(SwitchProReport& report, const char* state_failure) { - constexpr uint16_t packed_mid = - CONTROLLER_AXIS_UNSIGNED_CENTER >> 4u; + constexpr uint16_t packed_mid = SWITCH_PRO_JOYSTICK_MID >> 4u; constexpr uint16_t packed_inverted_mid = static_cast(-static_cast(packed_mid)) & 0x0fffu; expect(report.inputs.leftStick.getX() == packed_mid && @@ -196,7 +195,7 @@ std::array complete_rumble_report( return report; } -void rumble_callback(uint8_t instance, const ControllerRumbleOutput& output) { +void rumble_callback(uint8_t instance, const SwitchRumbleOutput& output) { expect(instance < rumble_events.size(), "rumble callback received an invalid instance"); if (instance >= rumble_events.size()) { @@ -271,22 +270,18 @@ 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) { - 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)); + 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); } - states[0].button_east = true; - states[1].button_south = true; - states[2].button_north = true; - states[3].button_west = true; + states[0].button_a = true; + states[1].button_b = true; + states[2].button_x = true; + states[3].button_y = true; for (uint8_t instance = 0; instance < kInstanceCount; ++instance) { switch_pro_set_input(instance, states[instance]); @@ -325,9 +320,9 @@ void test_input_reports_and_timers_are_isolated() { } } - ControllerState changed_zero = states[0]; - changed_zero.button_east = false; - changed_zero.button_system = true; + SwitchInputState changed_zero = states[0]; + changed_zero.button_a = false; + changed_zero.button_home = true; switch_pro_set_input(0, changed_zero); now_ms = 30; expect(switch_pro_task(0), @@ -338,8 +333,8 @@ void test_input_reports_and_timers_are_isolated() { !unchanged_three.inputs.buttonHome, "instance 0 input change leaked into instance 3"); - ControllerState changed_three = states[3]; - changed_three.button_west = false; + SwitchInputState changed_three = states[3]; + changed_three.button_y = false; changed_three.button_capture = true; switch_pro_set_input(3, changed_three); now_ms = 45; @@ -362,14 +357,13 @@ void test_callback_send_and_imu_modes_are_isolated() { expect(reports_for_instance(1) == 0, "feature callback queued a reply on instance 1"); - 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}; + 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}; switch_pro_set_input(0, zero); switch_pro_set_input(1, one); now_ms = 21; @@ -390,16 +384,15 @@ void test_callback_send_and_imu_modes_are_isolated() { now_ms = 6; switch_pro_task(0); switch_pro_task(1); - 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}; + 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}; switch_pro_set_input(0, moving); switch_pro_set_input(1, stationary); now_ms = 21; @@ -575,8 +568,8 @@ void test_lifecycle_and_invalid_instances() { "unmount did not reset every configured context"); } - ControllerState ignored{}; - ignored.button_system = true; + SwitchInputState ignored{}; + ignored.button_home = true; switch_pro_init(kInvalidInstance); switch_pro_set_input(kInvalidInstance, ignored); switch_pro_set_rumble_callback(kInvalidInstance, rumble_callback); @@ -586,41 +579,18 @@ 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(); - 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; + SwitchInputState driver_state{}; + driver_state.lx = driver_state.ly = driver_state.rx = driver_state.ry = + SWITCH_PRO_JOYSTICK_MID; + driver_state.button_x = true; switch_pro_set_input(0, driver_state); now_ms = 15; switch_pro_task(0); @@ -640,20 +610,14 @@ void test_uart_parser_is_pure() { for (unsigned i = 0; i < packet.size() - 1; ++i) { packet.back() = static_cast(packet.back() + packet[i]); } - ControllerState parsed{}; + SwitchInputState parsed{}; expect(switch_pro_apply_uart_packet(packet.data(), packet.size(), parsed), "valid UART packet was rejected"); - expect(parsed.button_east && parsed.button_left_shoulder && parsed.dpad_down && + expect(parsed.button_a && parsed.button_l && parsed.dpad_down && parsed.dpad_left, "UART buttons or hat were parsed incorrectly"); - 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), + expect(parsed.lx == 0x1212 && parsed.ly == 0x3434 && + parsed.rx == 0x5656 && parsed.ry == 0x7878, "UART axes were parsed incorrectly"); std::array current{}; @@ -664,14 +628,14 @@ void test_uart_parser_is_pure() { expect(current_report.inputs.buttonX && !current_report.inputs.buttonA, "UART parsing mutated driver context state"); - ControllerState unchanged{}; - unchanged.button_system = true; - unchanged.left_stick_x = 123; + SwitchInputState unchanged{}; + unchanged.button_home = true; + unchanged.lx = 123; packet.back() ^= 0xffu; expect(!switch_pro_apply_uart_packet(packet.data(), packet.size(), unchanged), "invalid UART checksum was accepted"); - expect(unchanged.button_system && unchanged.left_stick_x == 123, + expect(unchanged.button_home && unchanged.lx == 123, "failed UART parse modified its output reference"); } @@ -723,7 +687,6 @@ 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 7484f39..65c0285 100644 --- a/tests/test_bluepad32_backend_lifecycle_native.py +++ b/tests/test_bluepad32_backend_lifecycle_native.py @@ -44,8 +44,6 @@ 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 41ce9da..c56131a 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,7 +113,8 @@ void test_microsoft_compatible_id_descriptor() { } void test_input_report_mapping() { - ControllerState state{}; + SwitchInputState state{}; + state.lx = state.ly = state.rx = state.ry = 32768; auto report = XInputFeasibility::build_input_report(state); expect(report.report_id == 0 && report.report_size == 20, "neutral report header mismatch"); @@ -125,19 +126,19 @@ void test_input_report_mapping() { "neutral axes mismatch"); state.dpad_up = true; - 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; + 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; report = XInputFeasibility::build_input_report(state); expect((report.buttons & XInputFeasibility::kDpadUp) != 0, "D-pad mapping missing"); @@ -147,22 +148,15 @@ void test_input_report_mapping() { (report.buttons & XInputFeasibility::kButtonY) != 0, "positional face-button mapping mismatch"); expect(report.left_trigger == 0xff && report.right_trigger == 0xff, - "full analog trigger mapping mismatch"); + "digital 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}; - ControllerRumbleOutput output{}; + SwitchRumbleOutput 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 bc0b27e..2a9f485 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 { - ControllerState input{}; + SwitchInputState input{}; XInputFeasibility::InputReport input_report{}; uint8_t output_report[kEndpointBufferSize]{}; - ControllerRumbleCallback rumble_callback = nullptr; + SwitchRumbleCallback 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 ControllerRumbleCallback callback = context.rumble_callback; + const SwitchRumbleCallback 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) { - ControllerRumbleOutput rumble{}; + SwitchRumbleOutput rumble{}; if (XInputFeasibility::parse_rumble_report(context->output_report, transferred, &rumble) && context->rumble_callback != nullptr) { @@ -168,14 +168,15 @@ void xinput_feasibility_init(uint8_t instance) { } void xinput_feasibility_set_rumble_callback(uint8_t instance, - ControllerRumbleCallback callback) { + SwitchRumbleCallback callback) { XInputContext *context = context_for(instance); if (context != nullptr) { context->rumble_callback = callback; } } + void xinput_feasibility_set_input(uint8_t instance, - const ControllerState& state) { + const SwitchInputState &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 33e15b8..b1db301 100644 --- a/xinput_feasibility_driver.h +++ b/xinput_feasibility_driver.h @@ -2,13 +2,12 @@ #include -#include "controller_state.h" -#include "switch_haptics.h" +#include "switch_pro_driver.h" void xinput_feasibility_init(uint8_t instance); void xinput_feasibility_set_rumble_callback(uint8_t instance, - ControllerRumbleCallback callback); + SwitchRumbleCallback callback); void xinput_feasibility_set_input(uint8_t instance, - const ControllerState& state); + const SwitchInputState &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 43315ca..75059c6 100644 --- a/xinput_feasibility_protocol.h +++ b/xinput_feasibility_protocol.h @@ -2,8 +2,7 @@ #include -#include "controller_state.h" -#include "switch_haptics.h" +#include "switch_pro_driver.h" namespace XInputFeasibility { @@ -40,41 +39,42 @@ struct InputReport { static_assert(sizeof(InputReport) == 20); -constexpr int16_t invert_axis(int16_t value) { - return value == INT16_MIN ? INT16_MAX - : static_cast(-value); +constexpr int16_t horizontal_axis(uint16_t value) { + return static_cast(static_cast(value) - 32768); } -inline InputReport build_input_report(const ControllerState& state) { +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) { 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_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); + (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); return report; } inline bool parse_rumble_report(const uint8_t *data, uint32_t size, - ControllerRumbleOutput *output) { + SwitchRumbleOutput *output) { if (data == nullptr || output == nullptr || size < 5 || data[0] != 0x00 || data[1] != 0x08) { return false;