diff --git a/.gitignore b/.gitignore index 38023be..b95424e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ Switch-Fightstick GP2040-CE build -build-aio debug .pycache *.egg-info diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 18b03e8..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "external/bluepad32"] - path = external/bluepad32 - url = https://github.com/ricardoquesada/bluepad32.git diff --git a/CMakeLists.txt b/CMakeLists.txt index aea6efe..10a816f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,20 +24,7 @@ if (EXISTS ${picoVscode}) endif() # ==================================================================================== option(SWITCH_PICO_LOG "Enable UART debug logging" OFF) -set(SWITCH_PICO_INPUT_BACKEND "UART" CACHE STRING "Controller input backend") -set_property(CACHE SWITCH_PICO_INPUT_BACKEND PROPERTY STRINGS UART BLUEPAD32) -if(NOT SWITCH_PICO_INPUT_BACKEND STREQUAL "UART" - AND NOT SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32") - message(FATAL_ERROR - "Unknown SWITCH_PICO_INPUT_BACKEND='${SWITCH_PICO_INPUT_BACKEND}'. " - "Expected UART or BLUEPAD32.") -endif() set(PICO_BOARD pico CACHE STRING "Board type") -if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32" - AND NOT PICO_BOARD STREQUAL "pico2_w") - message(FATAL_ERROR - "SWITCH_PICO_INPUT_BACKEND=BLUEPAD32 requires PICO_BOARD=pico2_w") -endif() # Pull in Raspberry Pi Pico SDK (must be before project) include(pico_sdk_import.cmake) @@ -47,51 +34,12 @@ project(switch-pico C CXX ASM) # Initialise the Raspberry Pi Pico SDK pico_sdk_init() -# Configure BLUEPAD32 input backend if selected -if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32") - # Ensure Python3 is available and execute patch preparation - find_package(Python3 REQUIRED COMPONENTS Interpreter) - - set(BLUEPAD32_PREP_SCRIPT ${CMAKE_CURRENT_LIST_DIR}/tools/prepare_bluepad32.py) - execute_process( - COMMAND ${Python3_EXECUTABLE} ${BLUEPAD32_PREP_SCRIPT} - WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} - RESULT_VARIABLE BLUEPAD32_PREP_RESULT - OUTPUT_VARIABLE BLUEPAD32_PREP_OUTPUT - ERROR_VARIABLE BLUEPAD32_PREP_ERROR - ) - - if(NOT BLUEPAD32_PREP_RESULT EQUAL 0) - message(FATAL_ERROR - "Failed to prepare Bluepad32: Patch application or validation failed. " - "Details: ${BLUEPAD32_PREP_ERROR}") - endif() - - # Configure Bluepad32 include paths and subdirectory - set(BLUEPAD32_ROOT ${CMAKE_CURRENT_LIST_DIR}/external/bluepad32) - set(BTSTACK_ROOT ${PICO_SDK_PATH}/lib/btstack) - include_directories( - ${CMAKE_CURRENT_LIST_DIR}/bluepad32_config - ${BTSTACK_ROOT}/3rd-party/bluedroid/encoder/include - ${BTSTACK_ROOT}/3rd-party/bluedroid/decoder/include - ${BTSTACK_ROOT}/src - ) - add_subdirectory( - ${BLUEPAD32_ROOT}/src/components/bluepad32 - ${CMAKE_CURRENT_BINARY_DIR}/libbluepad32 - ) -endif() - # Add executable. Default name is the project name, version 0.1 add_executable(switch-pico switch-pico.cpp switch_pro_driver.cpp ) -if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32") - target_sources(switch-pico PRIVATE bluepad32_input_backend.cpp) - target_compile_definitions(switch-pico PRIVATE SWITCH_PICO_BLUEPAD32=1) -endif() pico_set_program_name(switch-pico "switch-pico") pico_set_program_version(switch-pico "0.1") @@ -109,16 +57,6 @@ target_link_libraries(switch-pico hardware_uart pico_rand ) -if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32") - target_link_libraries(switch-pico - bluepad32 - pico_cyw43_arch_none - pico_btstack_ble - pico_btstack_classic - pico_btstack_cyw43 - pico_multicore - ) -endif() if (SWITCH_PICO_LOG) target_compile_definitions(switch-pico PRIVATE SWITCH_PICO_LOG=1) diff --git a/README.md b/README.md index 5303c0a..f6b6a38 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,11 @@ # Switch Pico Controller Bridge -Raspberry Pi Pico firmware that emulates a Switch Pro controller over USB. Input can come from the SDL3-to-UART computer bridge or, on Pico 2 W, directly from a Bluetooth controller through Bluepad32. +Raspberry Pi Pico firmware that emulates a Switch Pro controller over USB and a host bridge that forwards real gamepad input over UART (with rumble round-trip). ## What you get -- **Firmware** (`switch-pico.cpp` + `switch_pro_driver.*`): acts as a wired Switch Pro, accepting either UART bridge reports or the optional Pico 2 W Bluepad32 backend. +- **Firmware** (`switch-pico.cpp` + `switch_pro_driver.*`): acts as a wired Switch Pro. Takes controller reports over UART1 and passes rumble from the Switch back over UART. - **Python bridge** (`switch_pico_bridge.controller_uart_bridge` / CLI `controller-uart-bridge`): reads SDL3 controllers on the host, sends reports over UART, and applies rumble locally. Hot‑plug friendly and cross‑platform (macOS/Windows/Linux). - **Colour override** (`controller_color_config.h`): compile‑time RGB overrides for body/buttons/grips as seen by the Switch. -- **Pico 2 W AIO firmware** (`firmware/switch-pico-aio.uf2`): hosts one Bluetooth controller and sends its controls, calibrated motion, and rumble through the same Switch Pro USB device without a computer. ## Quick start 1. Flash the Pico with `firmware/switch-pico.uf2` (or build your own) using BOOTSEL drag-and-drop (see “Manual UF2 flashing” below). @@ -15,66 +14,12 @@ Raspberry Pi Pico firmware that emulates a Switch Pro controller over USB. Input 4. Install the Python bridge (see “Python bridge”) and run `controller-uart-bridge --interactive`. 5. Connect the Pico to the Switch (dock USB-A or USB-C OTG); the Switch should see it as a wired Pro Controller. -## Pico 2 W all-in-one Bluetooth option - -The AIO build runs TinyUSB and Switch report generation on Core 0 while Bluepad32, BTstack, and the CYW43439 radio run on Core 1. A fixed state snapshot and bounded rumble queue are the only cross-core interfaces. - -### Build and flash - -Initialize the pinned Bluepad32 dependency once: - -```sh -git submodule update --init external/bluepad32 -``` - -Build and flash a Pico 2 W in BOOTSEL mode: - -```sh -python3 build.py --aio -``` - -This uses an isolated `build-aio/` CMake cache and publishes: - -- `firmware/switch-pico-aio.elf` -- `firmware/switch-pico-aio.uf2` - -The default `python3 build.py` command and `firmware/switch-pico.*` artifacts remain the UART/Pico build. The AIO build requires `PICO_BOARD=pico2_w`; it is not interchangeable with the original non-wireless Pico firmware. - -Both `build.py --aio` and direct AIO CMake configuration apply `patches/bluepad32-sdl3-imu.patch` idempotently before compiling Bluepad32. The patch makes supported motion controllers use SDL3-equivalent axes and fixed-point units before conversion to Nintendo samples. It intentionally leaves the dependency worktree dirty; the committed submodule revision remains Bluepad32 4.2.0. - -### Pair a controller - -1. Flash and connect the Pico 2 W to the Switch. -2. Enable `System Settings → Controllers and Sensors → Pro Controller Wired Communication`. -3. Put one controller into Bluetooth pairing mode: - - DualSense: hold Create + PS. - - DualShock 4: hold Share + PS. - - Switch Pro: press its sync button. - - Xbox Bluetooth controller: hold its pair button. - - 8BitDo: use a Bluetooth mode supported by Bluepad32; use Switch/S mode when motion is required. -4. Wait for the controller to connect. Pairing keys persist across Pico reboots. - -Only one wireless controller owns the emulated Pro Controller. Turn off or disconnect it before pairing another; scanning resumes automatically after disconnect. A disconnect immediately publishes neutral buttons, sticks, and motion. - -### Controller capabilities - -| Controller | Buttons/sticks | Rumble | Motion | -|---|---:|---:|---:| -| DualSense / DualShock 4 | Yes | Yes | Yes | -| Switch Pro | Yes | Yes | Yes | -| 8BitDo in Switch-compatible Bluetooth mode | Yes | Model-dependent | Yes when the mode exposes IMU | -| Xbox Bluetooth controller | Yes | Yes | No hardware IMU | - -Motion is normalized to 1024 units per degree/second and 8192 units per g in SDL3 axes, then converted to Nintendo axes and raw counts. The latest normalized sample is duplicated across the report's three nominal 5 ms slots; it remains pending until a regular `0x30` USB report successfully consumes it. - -Bluepad32 is Apache-2.0. BTstack use on Pico W/Pico 2 W is covered by Raspberry Pi's BTstack license. - ## Planned features ## Limitations - No NFC/amiibo/IR support. -- Rumble is best-effort: the UART build depends on SDL3 haptics; the AIO build depends on the connected controller's Bluepad32 rumble implementation. -- The UART firmware requires a host computer running the bridge. The Pico 2 W AIO firmware does not; it hosts controllers over Bluetooth, not USB. +- Rumble is best-effort: it depends on the Switch sending rumble and SDL3 being able to drive haptics on your specific controller. +- Requires a host computer running the bridge; the Pico is not a Bluetooth/USB host for controllers. ## Uses - **Remote couch co-op**: friends connect via Parsec while the host streams the Switch via a low-latency capture device (e.g., Magewell Pro Capture) and runs the bridge (see setup below). diff --git a/bluepad32_config/btstack_config.h b/bluepad32_config/btstack_config.h deleted file mode 100644 index 95072bf..0000000 --- a/bluepad32_config/btstack_config.h +++ /dev/null @@ -1,82 +0,0 @@ -#ifndef _PICO_BTSTACK_BTSTACK_CONFIG_H -#define _PICO_BTSTACK_BTSTACK_CONFIG_H - -// Based on Bluepad32's official Pico W configuration. ENABLE_BLE and -// ENABLE_CLASSIC are supplied by the corresponding Pico SDK BTstack targets. -#define ENABLE_LOG_INFO -#define ENABLE_LOG_ERROR -#define ENABLE_PRINTF_HEXDUMP -#define ENABLE_SCO_OVER_HCI - -#ifdef ENABLE_BLE -#define ENABLE_GATT_CLIENT_PAIRING -#define ENABLE_L2CAP_LE_CREDIT_BASED_FLOW_CONTROL_MODE -#define ENABLE_LE_CENTRAL -#define ENABLE_LE_DATA_LENGTH_EXTENSION -#define ENABLE_LE_PERIPHERAL -#define ENABLE_LE_PRIVACY_ADDRESS_RESOLUTION -#define ENABLE_LE_SECURE_CONNECTIONS -#else -#error "BP32: ENABLE_BLE should be defined" -#endif - -#ifdef ENABLE_CLASSIC -#define ENABLE_L2CAP_ENHANCED_RETRANSMISSION_MODE -#define ENABLE_GOEP_L2CAP -#else -#error "BP32: ENABLE_CLASSIC should be defined" -#endif - -#if defined(ENABLE_CLASSIC) && defined(ENABLE_BLE) -#define ENABLE_CROSS_TRANSPORT_KEY_DERIVATION -#endif - -#define HCI_OUTGOING_PRE_BUFFER_SIZE 4 -#define HCI_ACL_PAYLOAD_SIZE (1691 + 4) -#define HCI_ACL_CHUNK_SIZE_ALIGNMENT 4 -#define MAX_NR_AVDTP_CONNECTIONS 1 -#define MAX_NR_AVDTP_STREAM_ENDPOINTS 1 -#define MAX_NR_AVRCP_CONNECTIONS 2 -#define MAX_NR_BNEP_CHANNELS 1 -#define MAX_NR_BNEP_SERVICES 1 -#define MAX_NR_BTSTACK_LINK_KEY_DB_MEMORY_ENTRIES 2 -#define MAX_NR_GATT_CLIENTS 1 -#define MAX_NR_HCI_CONNECTIONS 4 -#define MAX_NR_HID_HOST_CONNECTIONS 1 -#define MAX_NR_HIDS_CLIENTS 1 -#define MAX_NR_HFP_CONNECTIONS 1 -#define MAX_NR_L2CAP_CHANNELS 6 -#define MAX_NR_L2CAP_SERVICES 5 -#define MAX_NR_RFCOMM_CHANNELS 1 -#define MAX_NR_RFCOMM_MULTIPLEXERS 1 -#define MAX_NR_RFCOMM_SERVICES 1 -#define MAX_NR_SERVICE_RECORD_ITEMS 4 -#define MAX_NR_SM_LOOKUP_ENTRIES 3 -#define MAX_NR_WHITELIST_ENTRIES 16 -#define MAX_NR_LE_DEVICE_DB_ENTRIES 16 - -// Keep controller buffers and controller-to-host flow control enabled to avoid -// overrunning the shared CYW43 bus. -#define MAX_NR_CONTROLLER_ACL_BUFFERS 3 -#define MAX_NR_CONTROLLER_SCO_PACKETS 3 -#define ENABLE_HCI_CONTROLLER_TO_HOST_FLOW_CONTROL -#define HCI_HOST_ACL_PACKET_LEN 1024 -#define HCI_HOST_ACL_PACKET_NUM 3 -#define HCI_HOST_SCO_PACKET_LEN 120 -#define HCI_HOST_SCO_PACKET_NUM 3 - -// Persistent Classic and BLE pairing databases use Pico flash-backed TLV. -#define NVM_NUM_DEVICE_DB_ENTRIES 16 -#define NVM_NUM_LINK_KEYS 16 - -// Bluepad32 does not provide malloc to BTstack. -#define MAX_ATT_DB_SIZE 512 - -#define HAVE_EMBEDDED_TIME_MS -#define HAVE_ASSERT -#define HCI_RESET_RESEND_TIMEOUT_MS 1000 -#define ENABLE_SOFTWARE_AES128 -#define ENABLE_MICRO_ECC_FOR_LE_SECURE_CONNECTIONS -#define HAVE_BTSTACK_STDIN - -#endif // _PICO_BTSTACK_BTSTACK_CONFIG_H diff --git a/bluepad32_config/sdkconfig.h b/bluepad32_config/sdkconfig.h deleted file mode 100644 index 83002a9..0000000 --- a/bluepad32_config/sdkconfig.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -// Bluepad32's Pico W example configuration, limited to one live controller. -#define CONFIG_BLUEPAD32_MAX_DEVICES 1 -#define CONFIG_BLUEPAD32_MAX_ALLOWLIST 1 -#define CONFIG_BLUEPAD32_GAP_SECURITY 1 -#define CONFIG_BLUEPAD32_ENABLE_BLE_BY_DEFAULT 1 - -#define CONFIG_BLUEPAD32_PLATFORM_CUSTOM -#define CONFIG_TARGET_PICO_W - -// 2 == Info -#define CONFIG_BLUEPAD32_LOG_LEVEL 2 diff --git a/bluepad32_input_backend.cpp b/bluepad32_input_backend.cpp deleted file mode 100644 index 6d60a69..0000000 --- a/bluepad32_input_backend.cpp +++ /dev/null @@ -1,402 +0,0 @@ -#include "bluepad32_input_backend.h" - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace { - -constexpr uint16_t kStickMidpoint = 32768; -constexpr int32_t kAxisMinimum = -512; -constexpr int32_t kAxisMaximum = 511; -constexpr int32_t kTriggerMaximum = 1023; -constexpr int32_t kTriggerThreshold = (kTriggerMaximum * 35) / 100; -constexpr uint16_t kRumbleDurationMs = 50; -constexpr uint32_t kRumblePollIntervalMs = 5; -constexpr uint kRumbleQueueDepth = 8; - -struct RumblePacket { - uint8_t bytes[8]; -}; - -critical_section_t g_state_lock; -queue_t g_rumble_queue; -SwitchInputState g_shared_state; -bool g_shared_controller_active = false; -uint32_t g_shared_generation = 0; - -// These generations are only read or written by Core 0. -uint32_t g_consumed_generation = 0; -uint32_t g_last_snapshot_generation = 0; -bool g_initialized = false; -bool g_started = false; - -// This pointer and the timer are only read or written by Core 1 / BTstack. -uni_hid_device_t* g_active_device = nullptr; -btstack_timer_source_t g_rumble_timer{}; - -SwitchInputState make_neutral_state() { - SwitchInputState state{}; - state.lx = kStickMidpoint; - state.ly = kStickMidpoint; - state.rx = kStickMidpoint; - state.ry = kStickMidpoint; - return state; -} - -void publish_state(const SwitchInputState& state, bool controller_active) { - critical_section_enter_blocking(&g_state_lock); - g_shared_state = state; - g_shared_controller_active = controller_active; - ++g_shared_generation; - critical_section_exit(&g_state_lock); -} - -constexpr int32_t clamp_axis(int32_t value) { - if (value < kAxisMinimum) { - return kAxisMinimum; - } - if (value > kAxisMaximum) { - return kAxisMaximum; - } - return value; -} - -constexpr uint16_t scale_stick(int32_t value) { - value = clamp_axis(value); - if (value <= 0) { - return static_cast( - (static_cast(value - kAxisMinimum) * kStickMidpoint) / -kAxisMinimum); - } - return static_cast( - kStickMidpoint + (static_cast(value) * (UINT16_MAX - kStickMidpoint)) / kAxisMaximum); -} - -constexpr int16_t clamp_int16(int64_t value) { - if (value < INT16_MIN) { - return INT16_MIN; - } - if (value > INT16_MAX) { - return INT16_MAX; - } - return static_cast(value); -} - -constexpr int64_t divide_round_nearest(int64_t numerator, int64_t denominator) { - if (numerator >= 0) { - return (numerator + denominator / 2) / denominator; - } - return -((-numerator + denominator / 2) / denominator); -} - -constexpr int16_t convert_accel(int64_t q13_value) { - return clamp_int16(q13_value / 2); -} - -constexpr int16_t convert_gyro(int64_t q10_value) { - constexpr int64_t kNumeratorScale = 13371; - constexpr int64_t kDenominator = 1024 * 936; - return clamp_int16(divide_round_nearest(q10_value * kNumeratorScale, kDenominator)); -} - -static_assert(scale_stick(-512) == 0); -static_assert(scale_stick(0) == 32768); -static_assert(scale_stick(511) == UINT16_MAX); -static_assert(convert_accel(8192) == 4096); -static_assert(convert_accel(-8192) == -4096); -static_assert(convert_gyro(1024) == 14); -static_assert(convert_gyro(-1024) == -14); - -bool has_motion(const uni_gamepad_t& gamepad) { - for (size_t i = 0; i < 3; ++i) { - if (gamepad.accel[i] != 0 || gamepad.gyro[i] != 0) { - return true; - } - } - return false; -} - -SwitchInputState map_gamepad(const uni_gamepad_t& gamepad) { - SwitchInputState state = make_neutral_state(); - - state.dpad_up = (gamepad.dpad & DPAD_UP) != 0; - state.dpad_down = (gamepad.dpad & DPAD_DOWN) != 0; - state.dpad_left = (gamepad.dpad & DPAD_LEFT) != 0; - state.dpad_right = (gamepad.dpad & DPAD_RIGHT) != 0; - - // Bluepad32's A/B/X/Y are positional: south/east/west/north. - state.button_b = (gamepad.buttons & BUTTON_A) != 0; - state.button_a = (gamepad.buttons & BUTTON_B) != 0; - state.button_y = (gamepad.buttons & BUTTON_X) != 0; - state.button_x = (gamepad.buttons & BUTTON_Y) != 0; - state.button_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_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.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 (has_motion(gamepad)) { - // Dependency patches normalize both arrays to SDL3 PlayStation axes. - 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.imu_sample_count = 3; - for (SwitchImuSample& destination : state.imu_samples) { - destination = sample; - } - } - - return state; -} - -void decode_rumble(const uint8_t bytes[8], uint8_t* left_magnitude, uint8_t* right_magnitude) { - static constexpr uint8_t kNeutralPacket[8] = {0x00, 0x01, 0x40, 0x40, 0x00, 0x01, 0x40, 0x40}; - if (memcmp(bytes, kNeutralPacket, sizeof(kNeutralPacket)) == 0) { - *left_magnitude = 0; - *right_magnitude = 0; - return; - } - - uint16_t right_raw = static_cast(((bytes[1] & 0x03) << 8) | bytes[0]); - uint16_t left_raw = static_cast(((bytes[5] & 0x03) << 8) | bytes[4]); - if (left_raw < 8 && right_raw < 8) { - left_raw = 0; - right_raw = 0; - } - - *left_magnitude = static_cast((left_raw * UINT8_MAX + 511) / 1023); - *right_magnitude = static_cast((right_raw * UINT8_MAX + 511) / 1023); -} - -void process_rumble_timer(btstack_timer_source_t* timer) { - RumblePacket packet{}; - RumblePacket latest{}; - bool have_packet = false; - while (queue_try_remove(&g_rumble_queue, &packet)) { - latest = packet; - have_packet = true; - } - - if (have_packet && g_active_device != nullptr && - g_active_device->report_parser.play_dual_rumble != nullptr) { - uint8_t left_magnitude = 0; - uint8_t right_magnitude = 0; - decode_rumble(latest.bytes, &left_magnitude, &right_magnitude); - // Bluepad orders the weak (high-frequency) motor before the strong - // (low-frequency) motor; the project decoder names those right/left. - g_active_device->report_parser.play_dual_rumble( - g_active_device, 0, kRumbleDurationMs, right_magnitude, left_magnitude); - } - - btstack_run_loop_set_timer(timer, kRumblePollIntervalMs); - btstack_run_loop_add_timer(timer); -} - -void platform_init(int argc, const char** argv) { - (void)argc; - (void)argv; -} - -void platform_on_init_complete() { - btstack_run_loop_set_timer_handler(&g_rumble_timer, process_rumble_timer); - btstack_run_loop_set_timer(&g_rumble_timer, kRumblePollIntervalMs); - btstack_run_loop_add_timer(&g_rumble_timer); - - uni_bt_allow_incoming_connections(true); - uni_bt_start_scanning_and_autoconnect_unsafe(); -} - -uni_error_t platform_on_device_discovered(bd_addr_t addr, const char* name, uint16_t cod, uint8_t rssi) { - (void)addr; - (void)name; - (void)cod; - (void)rssi; - return g_active_device == nullptr ? UNI_ERROR_SUCCESS : UNI_ERROR_IGNORE_DEVICE; -} - -void platform_on_device_connected(uni_hid_device_t* device) { - (void)device; -} - -void resume_connections() { - uni_bt_allow_incoming_connections(true); - uni_bt_start_scanning_and_autoconnect_unsafe(); -} - -void platform_on_device_disconnected(uni_hid_device_t* device) { - if (device == g_active_device) { - g_active_device = nullptr; - publish_state(make_neutral_state(), false); - resume_connections(); - } else if (g_active_device == nullptr) { - resume_connections(); - } -} - -uni_error_t platform_on_device_ready(uni_hid_device_t* device) { - if (!uni_hid_device_is_gamepad(device)) { - return UNI_ERROR_INVALID_CONTROLLER; - } - if (g_active_device != nullptr && g_active_device != device) { - return UNI_ERROR_NO_SLOTS; - } - - g_active_device = device; - publish_state(make_neutral_state(), true); - uni_bt_stop_scanning_unsafe(); - uni_bt_allow_incoming_connections(false); - return UNI_ERROR_SUCCESS; -} - -void platform_on_controller_data(uni_hid_device_t* device, uni_controller_t* controller) { - if (device != g_active_device || controller == nullptr || controller->klass != UNI_CONTROLLER_CLASS_GAMEPAD) { - return; - } - publish_state(map_gamepad(controller->gamepad), true); -} - -const uni_property_t* platform_get_property(uni_property_idx_t index) { - (void)index; - return nullptr; -} - -void platform_on_oob_event(uni_platform_oob_event_t event, void* data) { - (void)event; - (void)data; -} - -uni_platform* get_platform() { - static uni_platform platform = { - "Switch Pico", - platform_init, - platform_on_init_complete, - platform_on_device_discovered, - platform_on_device_connected, - platform_on_device_disconnected, - platform_on_device_ready, - nullptr, - platform_on_controller_data, - platform_get_property, - platform_on_oob_event, - nullptr, - nullptr, - }; - return &platform; -} - -[[noreturn]] void core1_main() { - if (cyw43_arch_init() != 0) { - publish_state(make_neutral_state(), false); - while (true) { - tight_loop_contents(); - } - } - - uni_platform_set_custom(get_platform()); - if (uni_init(0, nullptr) != 0) { - publish_state(make_neutral_state(), false); - while (true) { - tight_loop_contents(); - } - } - - btstack_run_loop_execute(); - while (true) { - tight_loop_contents(); - } -} - -} // namespace - -void bluepad32_input_backend_init() { - if (g_initialized) { - return; - } - - critical_section_init(&g_state_lock); - queue_init(&g_rumble_queue, sizeof(RumblePacket), kRumbleQueueDepth); - g_shared_state = make_neutral_state(); - g_shared_controller_active = false; - g_shared_generation = 0; - g_consumed_generation = 0; - g_last_snapshot_generation = 0; - g_initialized = true; -} - -void bluepad32_input_backend_start() { - if (!g_initialized) { - bluepad32_input_backend_init(); - } - if (g_started) { - return; - } - - g_started = true; - multicore_launch_core1(core1_main); -} - -bool bluepad32_input_backend_snapshot(SwitchInputState* out) { - if (out == nullptr) { - return false; - } - if (!g_initialized) { - *out = make_neutral_state(); - return false; - } - - critical_section_enter_blocking(&g_state_lock); - *out = g_shared_state; - const bool controller_active = g_shared_controller_active; - const uint32_t generation = g_shared_generation; - critical_section_exit(&g_state_lock); - - if (generation == g_consumed_generation) { - out->imu_sample_count = 0; - } - g_last_snapshot_generation = generation; - return controller_active; -} - -void bluepad32_input_backend_report_sent() { - if (!g_initialized) { - return; - } - g_consumed_generation = g_last_snapshot_generation; -} - -void bluepad32_input_backend_queue_rumble(const uint8_t rumble[8]) { - if (!g_initialized || rumble == nullptr) { - return; - } - - RumblePacket packet{}; - memcpy(packet.bytes, rumble, sizeof(packet.bytes)); - if (!queue_try_add(&g_rumble_queue, &packet)) { - RumblePacket discarded{}; - (void)queue_try_remove(&g_rumble_queue, &discarded); - (void)queue_try_add(&g_rumble_queue, &packet); - } -} diff --git a/bluepad32_input_backend.h b/bluepad32_input_backend.h deleted file mode 100644 index df5cc56..0000000 --- a/bluepad32_input_backend.h +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once - -#include - -#include "switch_pro_driver.h" - -void bluepad32_input_backend_init(); -void bluepad32_input_backend_start(); -bool bluepad32_input_backend_snapshot(SwitchInputState* out); -void bluepad32_input_backend_report_sent(); -void bluepad32_input_backend_queue_rumble(const uint8_t rumble[8]); diff --git a/build.py b/build.py index 8b728d3..95c537c 100644 --- a/build.py +++ b/build.py @@ -12,12 +12,9 @@ from pathlib import Path 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" 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" 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() @@ -37,11 +34,6 @@ def parse_args(): formatter_class=argparse.RawDescriptionHelpFormatter, epilog="Default behavior leaves controller_color_config.h unchanged.", ) - parser.add_argument( - "--aio", - action="store_true", - help="Build and flash the Pico 2 W Bluepad32 all-in-one firmware.", - ) group = parser.add_mutually_exclusive_group() group.add_argument( "--random-grip-color", @@ -119,68 +111,41 @@ def resolve_picotool(): sys.stderr.write("Error: picotool not found. Put it on your PATH or set PICOTOOL_PATH.\n") sys.exit(1) -def build( - aio, - build_dir, - elf_path, - uf2_path, - firmware_elf_path, - firmware_uf2_path, -): - 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", - ] - else: - definitions = [ - "-DSWITCH_PICO_LOG=OFF", - "-DPICO_BOARD=pico", - "-DSWITCH_PICO_INPUT_BACKEND=UART", - ] - +def build(): run_cmd( [ "cmake", "-S", str(SCRIPT_DIR), "-B", - str(build_dir), - *definitions, + str(BUILD_DIR), + "-DSWITCH_PICO_LOG=OFF", ] ) - run_cmd(["cmake", "--build", str(build_dir)]) + run_cmd(["cmake", "--build", str(BUILD_DIR)]) - missing_artifacts = [ - path for path in (elf_path, uf2_path) if not path.is_file() - ] + missing_artifacts = [path for path in (ELF_PATH, UF2_PATH) if not path.is_file()] if missing_artifacts: missing = ", ".join(str(path) for path in missing_artifacts) sys.stderr.write(f"Error: Build did not produce required artifact(s): {missing}\n") sys.exit(1) FIRMWARE_DIR.mkdir(parents=True, exist_ok=True) - shutil.copy2(elf_path, firmware_elf_path) - shutil.copy2(uf2_path, firmware_uf2_path) + shutil.copy2(ELF_PATH, FIRMWARE_ELF_PATH) + shutil.copy2(UF2_PATH, FIRMWARE_UF2_PATH) - print(f"Built ELF: {elf_path}") - print(f"Built UF2: {uf2_path}") - print(f"Copied ELF: {firmware_elf_path}") - print(f"Copied UF2: {firmware_uf2_path}") + print(f"Built ELF: {ELF_PATH}") + print(f"Built UF2: {UF2_PATH}") + print(f"Copied ELF: {FIRMWARE_ELF_PATH}") + print(f"Copied UF2: {FIRMWARE_UF2_PATH}") - -def flash(elf_path, allow_elf_override): +def flash(): picotool = resolve_picotool() - if not elf_path.exists(): - if allow_elf_override: - sys.stderr.write( - f"Error: Cannot find ELF at {elf_path}. Set ELF_PATH to override.\n" - ) - else: - sys.stderr.write(f"Error: Cannot find ELF at {elf_path}.\n") + if not ELF_PATH.exists(): + sys.stderr.write( + f"Error: Cannot find ELF at {ELF_PATH}. Set ELF_PATH to override.\n" + ) sys.exit(1) - run_cmd([str(picotool), "load", str(elf_path), "-fx"]) + run_cmd([str(picotool), "load", str(ELF_PATH), "-fx"]) def main(): args = parse_args() @@ -199,28 +164,8 @@ def main(): update_grip_colors(color) print(f"Grip color set to #{color} in {CONFIG_FILE.name}") - if args.aio: - build_dir = AIO_BUILD_DIR - elf_path = AIO_BUILD_DIR / "switch-pico.elf" - uf2_path = AIO_BUILD_DIR / "switch-pico.uf2" - firmware_elf_path = AIO_FIRMWARE_ELF_PATH - firmware_uf2_path = AIO_FIRMWARE_UF2_PATH - else: - build_dir = BUILD_DIR - elf_path = ELF_PATH - uf2_path = UF2_PATH - firmware_elf_path = FIRMWARE_ELF_PATH - firmware_uf2_path = FIRMWARE_UF2_PATH - - build( - args.aio, - build_dir, - elf_path, - uf2_path, - firmware_elf_path, - firmware_uf2_path, - ) - flash(elf_path, allow_elf_override=not args.aio) + build() + flash() if __name__ == "__main__": main() diff --git a/external/bluepad32 b/external/bluepad32 deleted file mode 160000 index 6efa712..0000000 --- a/external/bluepad32 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6efa7123fe8badf5a40ad1205743a80b31c00ea4 diff --git a/firmware/switch-pico-aio.elf b/firmware/switch-pico-aio.elf deleted file mode 100755 index cf7fdf6..0000000 Binary files a/firmware/switch-pico-aio.elf and /dev/null differ diff --git a/firmware/switch-pico-aio.uf2 b/firmware/switch-pico-aio.uf2 deleted file mode 100644 index 8e4968d..0000000 Binary files a/firmware/switch-pico-aio.uf2 and /dev/null differ diff --git a/firmware/switch-pico.elf b/firmware/switch-pico.elf index 2f0103b..6d314ee 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 6ef09f0..113f9a5 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 deleted file mode 100644 index 7060f78..0000000 --- a/patches/bluepad32-sdl3-imu.patch +++ /dev/null @@ -1,154 +0,0 @@ -diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds4.c b/src/components/bluepad32/parser/uni_hid_parser_ds4.c -index ea063b8..7670caf 100644 ---- a/src/components/bluepad32/parser/uni_hid_parser_ds4.c -+++ b/src/components/bluepad32/parser/uni_hid_parser_ds4.c -@@ -297,17 +297,17 @@ void uni_hid_parser_ds4_parse_feature_report(uni_hid_device_t* d, const uint8_t* - // Set gyroscope calibration and normalization parameters. - // Data values will be normalized to 1/DS_GYRO_RES_PER_DEG_S degree/s. - speed_2x = r->gyro_speed_plus + r->gyro_speed_minus; -- ins->gyro_calib_data[0].bias = 0; -+ ins->gyro_calib_data[0].bias = r->gyro_pitch_bias; - ins->gyro_calib_data[0].sens_numer = speed_2x * DS4_GYRO_RES_PER_DEG_S; - ins->gyro_calib_data[0].sens_denom = - abs(r->gyro_pitch_plus - r->gyro_pitch_bias) + abs(r->gyro_pitch_minus + r->gyro_pitch_bias); - -- ins->gyro_calib_data[1].bias = 0; -+ ins->gyro_calib_data[1].bias = r->gyro_yaw_bias; - ins->gyro_calib_data[1].sens_numer = speed_2x * DS4_GYRO_RES_PER_DEG_S; - ins->gyro_calib_data[1].sens_denom = - abs(r->gyro_yaw_plus - r->gyro_yaw_bias) + abs(r->gyro_yaw_minus - r->gyro_yaw_bias); - -- ins->gyro_calib_data[2].bias = 0; -+ ins->gyro_calib_data[2].bias = r->gyro_roll_bias; - ins->gyro_calib_data[2].sens_numer = speed_2x * DS4_GYRO_RES_PER_DEG_S; - ins->gyro_calib_data[2].sens_denom = - abs(r->gyro_roll_plus - r->gyro_roll_bias) + abs(r->gyro_roll_minus - r->gyro_roll_bias); -@@ -476,7 +476,7 @@ static void ds4_parse_input_report_11(uni_hid_device_t* d, const ds4_input_repor - - // Gyro - for (size_t i = 0; i < ARRAY_SIZE(r->gyro); i++) { -- int32_t raw_data = (int16_t)r->gyro[i]; -+ int32_t raw_data = (int16_t)r->gyro[i] - ins->gyro_calib_data[i].bias; - int32_t calib_data = - mult_frac(ins->gyro_calib_data[i].sens_numer, raw_data, ins->gyro_calib_data[i].sens_denom); - ctl->gamepad.gyro[i] = calib_data; -@@ -484,7 +484,7 @@ static void ds4_parse_input_report_11(uni_hid_device_t* d, const ds4_input_repor - - // Accel - for (size_t i = 0; i < ARRAY_SIZE(r->accel); i++) { -- int32_t raw_data = (int16_t)r->accel[i]; -+ int32_t raw_data = (int16_t)r->accel[i] - ins->accel_calib_data[i].bias; - int32_t calib_data = - mult_frac(ins->accel_calib_data[i].sens_numer, raw_data, ins->accel_calib_data[i].sens_denom); - ctl->gamepad.accel[i] = calib_data; -diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds5.c b/src/components/bluepad32/parser/uni_hid_parser_ds5.c -index a22ef26..3d5ecef 100644 ---- a/src/components/bluepad32/parser/uni_hid_parser_ds5.c -+++ b/src/components/bluepad32/parser/uni_hid_parser_ds5.c -@@ -487,17 +487,17 @@ void uni_hid_parser_ds5_parse_feature_report(uni_hid_device_t* d, const uint8_t* - // Set gyroscope calibration and normalization parameters. - // Data values will be normalized to 1/DS_GYRO_RES_PER_DEG_S degree/s. - speed_2x = r->gyro_speed_plus + r->gyro_speed_minus; -- ins->gyro_calib_data[0].bias = 0; -+ ins->gyro_calib_data[0].bias = r->gyro_pitch_bias; - ins->gyro_calib_data[0].sens_numer = speed_2x * DS5_GYRO_RES_PER_DEG_S; - ins->gyro_calib_data[0].sens_denom = - abs(r->gyro_pitch_plus - r->gyro_pitch_bias) + abs(r->gyro_pitch_minus + r->gyro_pitch_bias); - -- ins->gyro_calib_data[1].bias = 0; -+ ins->gyro_calib_data[1].bias = r->gyro_yaw_bias; - ins->gyro_calib_data[1].sens_numer = speed_2x * DS5_GYRO_RES_PER_DEG_S; - ins->gyro_calib_data[1].sens_denom = - abs(r->gyro_yaw_plus - r->gyro_yaw_bias) + abs(r->gyro_yaw_minus - r->gyro_yaw_bias); - -- ins->gyro_calib_data[2].bias = 0; -+ ins->gyro_calib_data[2].bias = r->gyro_roll_bias; - ins->gyro_calib_data[2].sens_numer = speed_2x * DS5_GYRO_RES_PER_DEG_S; - ins->gyro_calib_data[2].sens_denom = - abs(r->gyro_roll_plus - r->gyro_roll_bias) + abs(r->gyro_roll_minus - r->gyro_roll_bias); -@@ -622,7 +622,7 @@ void uni_hid_parser_ds5_parse_input_report(uni_hid_device_t* d, const uint8_t* r - - // Gyro - for (size_t i = 0; i < ARRAY_SIZE(r->gyro); i++) { -- int32_t raw_data = (int16_t)r->gyro[i]; -+ int32_t raw_data = (int16_t)r->gyro[i] - ins->gyro_calib_data[i].bias; - int32_t calib_data = - mult_frac(ins->gyro_calib_data[i].sens_numer, raw_data, ins->gyro_calib_data[i].sens_denom); - ctl->gamepad.gyro[i] = calib_data; -@@ -630,7 +630,7 @@ void uni_hid_parser_ds5_parse_input_report(uni_hid_device_t* d, const uint8_t* r - - // Accel - for (size_t i = 0; i < ARRAY_SIZE(r->accel); i++) { -- int32_t raw_data = (int16_t)r->accel[i]; -+ int32_t raw_data = (int16_t)r->accel[i] - ins->accel_calib_data[i].bias; - int32_t calib_data = - mult_frac(ins->accel_calib_data[i].sens_numer, raw_data, ins->accel_calib_data[i].sens_denom); - ctl->gamepad.accel[i] = calib_data; -diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/components/bluepad32/parser/uni_hid_parser_switch.c -index 599fc35..c72f056 100644 ---- a/src/components/bluepad32/parser/uni_hid_parser_switch.c -+++ b/src/components/bluepad32/parser/uni_hid_parser_switch.c -@@ -51,7 +51,8 @@ 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; --#define SWITCH_IMU_PREC_RANGE_SCALE 1000 -+#define SWITCH_IMU_GYRO_RES_PER_DEG_S 1024 -+#define SWITCH_IMU_ACCEL_RES_PER_G 8192 - - #define SWITCH_FACTORY_IMU_CAL_DATA_SIZE 24 - static const uint16_t SWITCH_FACTORY_IMU_CAL_DATA_ADDR = 0x6020; -@@ -823,19 +824,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; - -- int accel[3]; -- int gyro[3]; -+ int32_t accel[3]; -+ int32_t gyro[3]; - - for (int i = 0; i < 3; i++) { -- if (ins->imu_cal_accel_divisor[i] == 0) -- accel[i] = r->accel[i]; -- else -- accel[i] = (r->accel[i] * ins->cal_accel.scale[i]) / ins->imu_cal_accel_divisor[i]; -- gyro[i] = mult_frac((SWITCH_IMU_PREC_RANGE_SCALE * (r->gyro[i] - ins->cal_gyro.offset[i])), -- ins->cal_gyro.scale[i], ins->imu_cal_gyro_divisor[i]); -+ if (ins->imu_cal_accel_divisor[i] == 0) { -+ accel[i] = r->accel[i] * 2; -+ } else { -+ accel[i] = mult_frac(r->accel[i], 4 * SWITCH_IMU_ACCEL_RES_PER_G, ins->imu_cal_accel_divisor[i]); -+ } -+ -+ if (ins->imu_cal_gyro_divisor[i] == 0) { -+ gyro[i] = mult_frac(r->gyro[i], 936 * SWITCH_IMU_GYRO_RES_PER_DEG_S, DEFAULT_GYRO_SCALE); -+ } else { -+ gyro[i] = mult_frac(r->gyro[i] - ins->cal_gyro.offset[i], -+ 936 * SWITCH_IMU_GYRO_RES_PER_DEG_S, -+ ins->imu_cal_gyro_divisor[i]); -+ } - } - -- // Right joycon has Y and Z axes negated. -+ // Right Joy-Con has native Y and Z axes negated. - if (ins->controller_type == SWITCH_CONTROLLER_TYPE_JCR) { - accel[1] = -accel[1]; - accel[2] = -accel[2]; -@@ -843,10 +851,13 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) { - gyro[2] = -gyro[2]; - } - -- for (int i = 0; i < 3; i++) { -- ctl->gamepad.accel[i] = accel[i]; -- ctl->gamepad.gyro[i] = gyro[i]; -- } -+ // Match SDL3's PlayStation-oriented sensor coordinate convention. -+ ctl->gamepad.accel[0] = -accel[1]; -+ ctl->gamepad.accel[1] = accel[2]; -+ ctl->gamepad.accel[2] = -accel[0]; -+ ctl->gamepad.gyro[0] = -gyro[1]; -+ ctl->gamepad.gyro[1] = gyro[2]; -+ ctl->gamepad.gyro[2] = -gyro[0]; - } - - // Process 0x30 input report: SWITCH_INPUT_IMU_DATA diff --git a/switch-pico.cpp b/switch-pico.cpp index 22a3083..22beaef 100644 --- a/switch-pico.cpp +++ b/switch-pico.cpp @@ -1,14 +1,10 @@ #include #include #include "bsp/board.h" +#include "hardware/uart.h" #include "pico/stdlib.h" #include "tusb.h" #include "switch_pro_driver.h" -#ifndef SWITCH_PICO_BLUEPAD32 -#include "hardware/uart.h" -#else -#include "bluepad32_input_backend.h" -#endif #ifdef SWITCH_PICO_LOG #define LOG_PRINTF(...) printf(__VA_ARGS__) @@ -16,7 +12,6 @@ #define LOG_PRINTF(...) ((void)0) #endif -#ifndef SWITCH_PICO_BLUEPAD32 // UART1 is reserved for external input frames from the host PC. #define UART_ID uart1 #define BAUD_RATE 921600 @@ -24,7 +19,6 @@ #define UART_RX_PIN 5 #define UART_RUMBLE_HEADER 0xBB #define UART_RUMBLE_RUMBLE_TYPE 0x01 -#endif static bool g_last_mounted = false; static bool g_last_ready = false; @@ -32,14 +26,12 @@ static bool g_last_ready = false; // Track the latest state provided by UART or the autopilot. static SwitchInputState g_user_state; -#ifndef SWITCH_PICO_BLUEPAD32 static void init_uart_input() { uart_init(UART_ID, BAUD_RATE); gpio_set_function(UART_TX_PIN, GPIO_FUNC_UART); gpio_set_function(UART_RX_PIN, GPIO_FUNC_UART); uart_set_format(UART_ID, 8, 1, UART_PARITY_NONE); } -#endif static SwitchInputState neutral_input() { SwitchInputState state{}; @@ -50,7 +42,6 @@ static SwitchInputState neutral_input() { return state; } -#ifndef SWITCH_PICO_BLUEPAD32 static void send_rumble_uart_frame(const uint8_t rumble[8]) { uint8_t frame[11]; frame[0] = UART_RUMBLE_HEADER; @@ -64,17 +55,11 @@ static void send_rumble_uart_frame(const uint8_t rumble[8]) { frame[10] = checksum; uart_write_blocking(UART_ID, frame, sizeof(frame)); } -#endif static void on_rumble_from_switch(const uint8_t rumble[8]) { -#ifdef SWITCH_PICO_BLUEPAD32 - bluepad32_input_backend_queue_rumble(rumble); -#else send_rumble_uart_frame(rumble); -#endif } -#ifndef SWITCH_PICO_BLUEPAD32 // Consume UART bytes and forward complete frames to the Switch Pro driver. static bool poll_uart_frames() { static uint8_t buffer[64]; @@ -149,7 +134,6 @@ static bool poll_uart_frames() { return new_data; } -#endif static void log_usb_state() { bool mounted = tud_mounted(); @@ -169,11 +153,7 @@ int main() { board_init(); stdio_init_all(); -#ifdef SWITCH_PICO_BLUEPAD32 - bluepad32_input_backend_init(); -#else init_uart_input(); -#endif tusb_init(); switch_pro_init(); @@ -181,32 +161,17 @@ int main() { g_user_state = neutral_input(); switch_pro_set_input(g_user_state); -#ifdef SWITCH_PICO_BLUEPAD32 - bluepad32_input_backend_start(); - LOG_PRINTF("[BOOT] switch-pico starting (Bluepad32 wireless @ 115200)\n"); -#else LOG_PRINTF("[BOOT] switch-pico starting (UART0 log @ 115200)\n"); LOG_PRINTF("[INFO] UART1 pins TX=%d RX=%d baud=%d\n", UART_TX_PIN, UART_RX_PIN, BAUD_RATE); -#endif while (true) { tud_task(); // USB device tasks -#ifdef SWITCH_PICO_BLUEPAD32 - bluepad32_input_backend_snapshot(&g_user_state); -#else bool new_data = poll_uart_frames(); // Pull controller state from UART1 (void)new_data; -#endif SwitchInputState state = g_user_state; switch_pro_set_input(state); -#ifdef SWITCH_PICO_BLUEPAD32 - if (switch_pro_task()) { - bluepad32_input_backend_report_sent(); - } -#else - (void)switch_pro_task(); -#endif + switch_pro_task(); // Push state to the Switch host log_usb_state(); } } diff --git a/switch_pro_driver.cpp b/switch_pro_driver.cpp index 6417c68..4d26922 100644 --- a/switch_pro_driver.cpp +++ b/switch_pro_driver.cpp @@ -753,10 +753,9 @@ void switch_pro_set_input(const SwitchInputState& state) { g_input_state = state; } -bool switch_pro_task() { +void switch_pro_task() { uint32_t now = to_ms_since_boot(get_absolute_time()); report_sent = false; - bool regular_report_sent = false; update_switch_report_from_state(); @@ -785,7 +784,6 @@ bool switch_pro_task() { memcpy(last_report, inputReport, report_size); g_input_state.imu_sample_count = 0; report_sent = true; - regular_report_sent = true; } last_report_timer = now; @@ -801,7 +799,6 @@ bool switch_pro_task() { last_report_timer = now; } } - return regular_report_sent; } bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length, SwitchInputState* out_state) { diff --git a/switch_pro_driver.h b/switch_pro_driver.h index 6a4f8bb..951d68b 100644 --- a/switch_pro_driver.h +++ b/switch_pro_driver.h @@ -55,9 +55,8 @@ void switch_pro_init(); // Update the desired controller state for the next USB report. void switch_pro_set_input(const SwitchInputState& state); -// Drive the Switch Pro USB state machine; returns true only when a regular -// 0x30 input report was successfully queued. -bool switch_pro_task(); +// Drive the Switch Pro USB state machine; call this frequently in the main loop. +void switch_pro_task(); // Convert a packed UART message into controller state (returns true if parsed). // If out_state is null the parsed state is written directly to the driver. diff --git a/tests/test_prepare_bluepad32.py b/tests/test_prepare_bluepad32.py deleted file mode 100644 index 3e4673f..0000000 --- a/tests/test_prepare_bluepad32.py +++ /dev/null @@ -1,339 +0,0 @@ -""" -Tests for prepare_bluepad32.py patch preparation tool. - -Tests cover: -- Fresh patch application -- Idempotence (second invocation succeeds without changing content) -- Missing paths validation -- Diverged/ambiguous repository states -""" - -import subprocess -import tempfile -import pytest -from pathlib import Path -from unittest.mock import patch as mock_patch - -import sys -sys.path.insert(0, str(Path(__file__).parent.parent / "tools")) - -from prepare_bluepad32 import ( - PatchError, - resolve_paths, - check_paths, - is_patch_applied, - apply_patch, - prepare_bluepad32, -) - - -@pytest.fixture -def temp_repo_structure(): - """Create a temporary directory structure with git repositories.""" - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - - # Create bluepad32 repo - bp_dir = root / "external" / "bluepad32" - bp_dir.mkdir(parents=True) - subprocess.run(["git", "init"], cwd=bp_dir, check=True, capture_output=True) - subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=bp_dir, check=True, capture_output=True) - subprocess.run(["git", "config", "user.name", "Test User"], cwd=bp_dir, check=True, capture_output=True) - - # Create a file to patch - test_file = bp_dir / "test.txt" - test_file.write_text("line 1\n") - subprocess.run(["git", "add", "test.txt"], cwd=bp_dir, check=True, capture_output=True) - subprocess.run(["git", "commit", "-m", "initial"], cwd=bp_dir, check=True, capture_output=True) - - # Create patches dir - patches_dir = root / "patches" - patches_dir.mkdir() - - yield root, bp_dir, patches_dir - - -def create_simple_patch(repo_path: Path, patch_path: Path, file_to_patch: str = "test.txt") -> str: - """ - Create a simple patch file that modifies a file in the repository. - - Returns the patch content as a string. - """ - # Create the modification - test_file = repo_path / file_to_patch - original_content = test_file.read_text() - modified_content = original_content + "line 2\n" - - # Generate patch using git diff - test_file.write_text(modified_content) - result = subprocess.run( - ["git", "diff", file_to_patch], - cwd=repo_path, - capture_output=True, - text=True, - check=True, - ) - patch_content = result.stdout - - # Reset the file to original state - test_file.write_text(original_content) - - # Write patch to file - patch_path.write_text(patch_content) - return patch_content - - -def test_resolve_paths_with_defaults(): - """Test that resolve_paths returns expected default paths.""" - with tempfile.TemporaryDirectory() as tmpdir: - bp_path, patch_path = resolve_paths(Path(tmpdir)) - assert bp_path == Path(tmpdir) / "external" / "bluepad32" - assert patch_path == Path(tmpdir) / "patches" / "bluepad32-sdl3-imu.patch" - - -def test_resolve_paths_no_root(): - """Test resolve_paths with no root uses current directory.""" - bp_path, patch_path = resolve_paths() - assert bp_path.is_absolute() - assert patch_path.is_absolute() - - -def test_check_paths_missing_bluepad32(temp_repo_structure): - """Test that check_paths fails if bluepad32 dir is missing.""" - root, bp_dir, patches_dir = temp_repo_structure - - # Remove bluepad32 - import shutil - shutil.rmtree(bp_dir) - - patch_file = patches_dir / "test.patch" - patch_file.write_text("dummy") - - with pytest.raises(PatchError, match="bluepad32 directory does not exist"): - check_paths(bp_dir, patch_file) - - -def test_check_paths_missing_patch(temp_repo_structure): - """Test that check_paths fails if patch file is missing.""" - root, bp_dir, patches_dir = temp_repo_structure - - patch_file = patches_dir / "nonexistent.patch" - - with pytest.raises(PatchError, match="patch file does not exist"): - check_paths(bp_dir, patch_file) - - -def test_check_paths_bluepad32_not_git_repo(temp_repo_structure): - """Test that check_paths fails if bluepad32 is not a git repo.""" - root, bp_dir, patches_dir = temp_repo_structure - - # Remove .git to make it not a git repo - import shutil - shutil.rmtree(bp_dir / ".git") - - patch_file = patches_dir / "test.patch" - patch_file.write_text("dummy") - - with pytest.raises(PatchError, match="not a git repository"): - check_paths(bp_dir, patch_file) - - -def test_fresh_patch_application(temp_repo_structure): - """Test applying a fresh patch to a clean repository.""" - root, bp_dir, patches_dir = temp_repo_structure - - patch_file = patches_dir / "test.patch" - create_simple_patch(bp_dir, patch_file, "test.txt") - - # Verify test.txt before patch - test_file = bp_dir / "test.txt" - original = test_file.read_text() - assert "line 2" not in original - - # Apply patch - apply_patch(bp_dir, patch_file) - - # Verify test.txt after patch - patched = test_file.read_text() - assert "line 2" in patched - - -def test_idempotent_patch_application(temp_repo_structure): - """Test that applying the same patch twice succeeds (idempotence).""" - root, bp_dir, patches_dir = temp_repo_structure - - patch_file = patches_dir / "test.patch" - create_simple_patch(bp_dir, patch_file, "test.txt") - - # First application - apply_patch(bp_dir, patch_file) - test_file = bp_dir / "test.txt" - after_first = test_file.read_text() - - # Second application should succeed without changing content - apply_patch(bp_dir, patch_file) - after_second = test_file.read_text() - - assert after_first == after_second - - -def test_is_patch_applied_not_applied(temp_repo_structure): - """Test is_patch_applied returns False for unapplied patch.""" - root, bp_dir, patches_dir = temp_repo_structure - - patch_file = patches_dir / "test.patch" - create_simple_patch(bp_dir, patch_file, "test.txt") - - # Patch not applied yet - assert is_patch_applied(bp_dir, patch_file) is False - - -def test_is_patch_applied_already_applied(temp_repo_structure): - """Test is_patch_applied returns True for already applied patch.""" - root, bp_dir, patches_dir = temp_repo_structure - - patch_file = patches_dir / "test.patch" - create_simple_patch(bp_dir, patch_file, "test.txt") - - # Apply patch first - apply_patch(bp_dir, patch_file) - - # Now check should detect it's applied - assert is_patch_applied(bp_dir, patch_file) is True - - -def test_diverged_repository_state(temp_repo_structure): - """Test that diverged repository (patch doesn't apply cleanly) is rejected.""" - root, bp_dir, patches_dir = temp_repo_structure - - patch_file = patches_dir / "test.patch" - create_simple_patch(bp_dir, patch_file, "test.txt") - - # Diverge the repository by modifying the file such that the patch conflicts - test_file = bp_dir / "test.txt" - test_file.write_text("completely different line 1\n") - subprocess.run(["git", "add", "test.txt"], cwd=bp_dir, check=True, capture_output=True) - subprocess.run(["git", "commit", "-m", "divergence"], cwd=bp_dir, check=True, capture_output=True) - - # Try to apply patch - should fail because file content doesn't match - with pytest.raises(PatchError, match="Patch validation failed"): - apply_patch(bp_dir, patch_file) - - -def test_missing_bluepad32_path(temp_repo_structure): - """Test prepare_bluepad32 fails gracefully with missing bluepad32.""" - root, bp_dir, patches_dir = temp_repo_structure - - import shutil - shutil.rmtree(bp_dir) - - patch_file = patches_dir / "test.patch" - patch_file.write_text("dummy") - - with pytest.raises(PatchError, match="bluepad32 directory does not exist"): - prepare_bluepad32(bp_dir, patch_file) - - -def test_missing_patch_file(temp_repo_structure): - """Test prepare_bluepad32 fails gracefully with missing patch file.""" - root, bp_dir, patches_dir = temp_repo_structure - - patch_file = patches_dir / "nonexistent.patch" - - with pytest.raises(PatchError, match="patch file does not exist"): - prepare_bluepad32(bp_dir, patch_file) - - -def test_prepare_bluepad32_full_workflow(temp_repo_structure): - """Test complete prepare_bluepad32 workflow: apply then idempotent re-apply.""" - root, bp_dir, patches_dir = temp_repo_structure - - patch_file = patches_dir / "test.patch" - create_simple_patch(bp_dir, patch_file, "test.txt") - - test_file = bp_dir / "test.txt" - original = test_file.read_text() - - # First prepare (should apply patch) - prepare_bluepad32(bp_dir, patch_file) - after_first = test_file.read_text() - assert after_first != original - assert "line 2" in after_first - - # Second prepare (should be idempotent) - prepare_bluepad32(bp_dir, patch_file) - after_second = test_file.read_text() - assert after_first == after_second - - -def test_prepare_bluepad32_with_defaults(temp_repo_structure): - """Test prepare_bluepad32 uses correct defaults when paths not provided.""" - root, bp_dir, patches_dir = temp_repo_structure - - patch_file = patches_dir / "bluepad32-sdl3-imu.patch" - create_simple_patch(bp_dir, patch_file, "test.txt") - - # Change to root directory and call with defaults - import os - original_cwd = os.getcwd() - try: - os.chdir(root) - prepare_bluepad32() # Use defaults - finally: - os.chdir(original_cwd) - - # Verify patch was applied - test_file = bp_dir / "test.txt" - assert "line 2" in test_file.read_text() - - -def test_patch_application_with_conflicting_content(temp_repo_structure): - """Test that patch with conflicting content is rejected.""" - root, bp_dir, patches_dir = temp_repo_structure - - # Create a patch that adds a specific change - patch_content = """--- a/test.txt -+++ b/test.txt -@@ -1 +1,3 @@ - line 1 -+line 2 -+line 3 -""" - - patch_file = patches_dir / "conflict.patch" - patch_file.write_text(patch_content) - - # Modify the file to have different content that won't match the patch context - test_file = bp_dir / "test.txt" - test_file.write_text("modified line 1\n") - subprocess.run(["git", "add", "test.txt"], cwd=bp_dir, check=True, capture_output=True) - subprocess.run(["git", "commit", "-m", "modify"], cwd=bp_dir, check=True, capture_output=True) - - # Try to apply patch - should fail due to context mismatch - with pytest.raises(PatchError): - apply_patch(bp_dir, patch_file) - - -def test_cli_with_explicit_paths(temp_repo_structure): - """Test CLI argument parsing with explicit paths.""" - root, bp_dir, patches_dir = temp_repo_structure - - patch_file = patches_dir / "test.patch" - create_simple_patch(bp_dir, patch_file, "test.txt") - - # Simulate CLI call - sys.argv = [ - "prepare_bluepad32.py", - "--bluepad32", str(bp_dir), - "--patch", str(patch_file), - ] - - from prepare_bluepad32 import main - - # Should not raise - try: - main() - except SystemExit as e: - # main() calls sys.exit on success, which we need to catch - if e.code != 0: - raise diff --git a/tools/prepare_bluepad32.py b/tools/prepare_bluepad32.py deleted file mode 100755 index 981d40f..0000000 --- a/tools/prepare_bluepad32.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env python3 -"""Apply the project Bluepad32 patch exactly once.""" -from __future__ import annotations - -import argparse -import subprocess -import sys -from pathlib import Path - - -class PatchError(RuntimeError): - pass - - -def resolve_paths(repo_root: Path | None = None) -> tuple[Path, Path]: - root = Path.cwd() if repo_root is None else Path(repo_root) - return ( - root / "external" / "bluepad32", - root / "patches" / "bluepad32-sdl3-imu.patch", - ) - - -def check_paths(bluepad32_path: Path, patch_path: Path) -> None: - if not bluepad32_path.is_dir(): - raise PatchError(f"bluepad32 directory does not exist: {bluepad32_path}") - if not (bluepad32_path / ".git").exists(): - raise PatchError(f"Bluepad32 is not a git repository: {bluepad32_path}") - if not patch_path.is_file(): - raise PatchError(f"patch file does not exist: {patch_path}") - - -def git_apply(bluepad32_path: Path, patch_path: Path, *args: str) -> subprocess.CompletedProcess[str]: - return subprocess.run( - ["git", "-C", str(bluepad32_path), "apply", *args, str(patch_path)], - capture_output=True, - text=True, - check=False, - ) - - -def is_patch_applied(bluepad32_path: Path, patch_path: Path) -> bool: - return git_apply(bluepad32_path, patch_path, "--reverse", "--check").returncode == 0 - - -def apply_patch(bluepad32_path: Path, patch_path: Path) -> None: - if is_patch_applied(bluepad32_path, patch_path): - return - - check = git_apply(bluepad32_path, patch_path, "--check") - if check.returncode != 0: - detail = check.stderr.strip() or "patch does not apply" - raise PatchError(f"Patch validation failed (repository may be diverged):\n{detail}") - - result = git_apply(bluepad32_path, patch_path) - if result.returncode != 0: - detail = result.stderr.strip() or "git apply failed" - raise PatchError(f"Could not patch Bluepad32: {detail}") - - -def prepare_bluepad32( - bluepad32_path: Path | None = None, - patch_path: Path | None = None, -) -> None: - default_bluepad32, default_patch = resolve_paths() - dependency = Path(bluepad32_path or default_bluepad32) - patch = Path(patch_path or default_patch) - check_paths(dependency, patch) - apply_patch(dependency, patch) - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--bluepad32", type=Path) - parser.add_argument("--patch", type=Path) - args = parser.parse_args() - try: - prepare_bluepad32(args.bluepad32, args.patch) - except PatchError as exc: - print(f"error: {exc}", file=sys.stderr) - return 1 - return 0 - - -if __name__ == "__main__": - raise SystemExit(main())