| .vscode | ||
| bluepad32_config | ||
| examples | ||
| external | ||
| firmware | ||
| patches | ||
| src/switch_pico_bridge | ||
| tests | ||
| tools | ||
| .gitignore | ||
| .gitmodules | ||
| bluepad32_input_backend.cpp | ||
| bluepad32_input_backend.h | ||
| build.py | ||
| CMakeLists.txt | ||
| controller_color_config.h | ||
| LICENSE | ||
| pico_sdk_import.cmake | ||
| pyproject.toml | ||
| README.md | ||
| requirements.txt | ||
| switch-pico.cpp | ||
| switch_haptics.cpp | ||
| switch_haptics.h | ||
| switch_pro_descriptors.h | ||
| switch_pro_driver.cpp | ||
| switch_pro_driver.h | ||
| tusb_config.h | ||
| uv.lock | ||
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.
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. - Python bridge (
switch_pico_bridge.controller_uart_bridge/ CLIcontroller-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
- Flash the Pico with
firmware/switch-pico.uf2(or build your own) using BOOTSEL drag-and-drop (see “Manual UF2 flashing” below). - Wire Pico UART1 to a USB↔UART adapter (GPIO4 TX, GPIO5 RX, GND) and plug that adapter into your host PC.
- Enable
System Settings → Controllers and Sensors → Pro Controller Wired Communicationon the Switch. - Install the Python bridge (see “Python bridge”) and run
controller-uart-bridge --interactive. - 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:
git submodule update --init external/bluepad32
Build and flash a Pico 2 W in BOOTSEL mode:
python3 build.py --aio
This uses an isolated build-aio/ CMake cache and publishes:
firmware/switch-pico-aio.elffirmware/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
- Flash and connect the Pico 2 W to the Switch.
- Enable
System Settings → Controllers and Sensors → Pro Controller Wired Communication. - 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.
- Wait for the controller to connect. Pairing keys persist across Pico reboots.
The Pico 2 W onboard LED reports Bluetooth state: a slow 0.5-second blink means scanning, a fast 0.1-second blink means a controller connected but is not ready, and solid means the controller is ready. A solid LED immediately after boot that never starts blinking indicates Bluepad32 initialization did not complete.
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.
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).
- Switch automation (Python): write scripts/bots that drive the Pico directly using
switch_pico_bridge.switch_pico_uart(seeexamples/example_switch_macro.py). - Twitch chat plays: translate chat messages into controller actions on the host, then forward them over UART to the Pico.
Remote couch co-op setup (example)
- Connect the Switch to a low-latency capture device on the host PC; view it in OBS (or your preferred viewer).
- Run
controller-uart-bridgeon the host PC and connect the Pico to the Switch for input. - Have friends connect to the host PC using Parsec; they use their controllers on their end, which Parsec forwards to the host (SDL3 sees them).
- Optional audio routing: Voicemeeter Potato + a virtual audio cable can help manage capture/voice/game audio mixing:
- Voicemeeter Potato: https://vb-audio.com/Voicemeeter/potato.htm
- VB-CABLE: https://vb-audio.com/Cable/index.htm
End-to-end data flow (input + rumble)
INPUT (buttons/sticks)
[Any controller] -> [Host OS HID] -> [SDL3 Gamepad] -> [controller-uart-bridge]
-> [USB↔UART adapter + UART serial] -> [Pico firmware] -> [USB (Switch Pro)]
-> [Nintendo Switch]
RUMBLE (force feedback)
[Nintendo Switch] -> [USB rumble output report] -> [Pico firmware]
-> [UART serial + USB↔UART adapter] -> [controller-uart-bridge]
-> [SDL3 haptics] -> [Any controller motors]
HD rumble translation
Nintendo sends two stateful four-byte HD-rumble actuator words. Each word can carry full or relative high/low frequency and amplitude commands with up to three subsamples; amplitude uses a logarithmic curve. The Pico decodes both words once in SwitchHapticsDecoder, retains actuator state across packets, and reduces the result to conventional low/strong and high/weak motor magnitudes. SDL3 and Bluepad32 cannot reproduce the original linear-actuator frequencies or left/right spatial effects, but they receive the correct nonlinear band amplitudes.
The UART return frame carries the decoded result rather than raw HD-rumble bytes:
0xBB, 0x02, low-frequency magnitude, high-frequency magnitude, checksum
The checksum is the sum of the first four bytes modulo 256. Firmware and Python bridge versions from before this change are not rumble-protocol compatible; controller input framing remains unchanged.
Hardware wiring (Pico)
- UART1 pins (fixed in firmware):
- TX: GPIO4 (Pico pin 6) → RX of your USB-serial adapter.
- RX: GPIO5 (Pico pin 7) → TX of your USB-serial adapter.
- GND: common ground between Pico and adapter.
- Baud rate: 921600 (default). Some adapters only handle 500,000; both bridges accept a
--baudflag. - Keep logic at 3.3V; do not feed 5V UART into the Pico.
Full hookup checklist
-
Gather the hardware
- Raspberry Pi Pico flashed with the provided firmware.
- USB-A-to-micro USB cable (or USB-C if you use a Pico W) to connect the Pico to the Switch or a PC for testing.
- USB-to-UART adapter capable of 3.3 V logic at 921600 baud (FT232, CP2102, CH340, etc.).
- Three dupont wires (TX, RX, GND). Optionally add heat-shrink or a small proto board if you want something more permanent.
-
Wire the Pico to the USB-to-UART adapter
- Pico GPIO4 → adapter RX (sometimes labelled RXD, DI, or R).
- Pico GPIO5 → adapter TX (TXD, DO, or T).
- Pico GND → adapter GND. Tie grounds even if the adapter is already USB-powered.
- Leave VBUS/VCC unconnected unless your adapter explicitly supports 3.3 V power output and you intend to power the Pico from it (the bridge expects the Pico to be powered from USB instead).
-
Connect everything to the host and Switch
- Plug the USB-to-UART adapter into the computer that will run the Python bridge. Note the COM port (
Device Manager > Ports) on Windows or/dev/cu.*//dev/ttyUSB*path on macOS/Linux; pass it via--map/--ports. - Connect the Pico's micro USB port to the Nintendo Switch (via the dock's USB-A port, a USB-C OTG adapter, or a PC if you are only testing). The Pico enumerates as a Switch Pro Controller over USB.
- On the Switch, enable
System Settings → Controllers and Sensors → Pro Controller Wired Communication. - Any SDL-compatible gamepads you want to use should also be plugged into (or paired with) the same host computer that runs the Python bridge; the bridge is the one reading them.
- Plug the USB-to-UART adapter into the computer that will run the Python bridge. Note the COM port (
Finding your USB↔UART adapter “description” (port filtering)
If you have multiple serial/COM devices, you can filter which ports the bridge will consider using the port description (or vendor/product text) shown by the OS.
- macOS/Linux (terminal):
- Quick list with descriptions:
python -m serial.tools.list_ports -v - Then run the bridge with a filter, for example:
controller-uart-bridge --interactive --include-port-desc CP210
- Quick list with descriptions:
- Windows:
- Device Manager → Ports (COM & LPT) → open your adapter → copy the device name/vendor text.
- Then run:
controller-uart-bridge --interactive --include-port-desc "USB-SERIAL CH340"
Filters you can use:
--include-port-desc SUBSTR(repeatable): only consider ports whose description contains the substring.--ignore-port-desc SUBSTR(repeatable): exclude ports whose description contains the substring.--all-ports: include non-USB serial devices in discovery (useful if your adapter isn’t tagged as USB by the OS).
-
Power-on order and sanity checks
- Power the Switch/dock so the Pico gets 5 V over USB; its USB stack must stay alive while the bridge streams data.
- On the host computer, run
controller-uart-bridge --list-controllersto make sure SDL sees your pads, then start the bridge with--map/--ports(or--interactive) referencing the adapter path you found earlier. - Watch the Rich console output: you should see each controller paired with a UART port and the rumble loop logging reconnects if cables are unplugged.
-
Common pitfalls
- A flipped TX/RX pair results in silence (no button presses); swap them if the Pico never shows input.
- Some adapters default to 5 V logic—move the jumper to 3.3 V before touching the Pico.
- If you use multiple adapters, label each cable; COM port numbers can change between boots.
- When testing on a PC before plugging into a Switch, you can verify activity with the lightweight
switch_pico_bridge.switch_pico_uarthelper or the Windows "Game Controllers" panel.
Building and flashing firmware
Prereqs: Pico SDK + CMake toolchain set up.
Using build.py
build.py configures CMake, builds the firmware, checks that both output formats
were created, copies the release artifacts into firmware/, and flashes the ELF
with picotool.
Before running it:
- Install the Pico SDK, CMake toolchain, and
picotool. - Connect the Pico in BOOTSEL mode.
- From the repository root, run:
python3 build.py
The generated files are:
build/switch-pico.elf, whichbuild.pypasses topicotool.build/switch-pico.uf2, which can also be copied to the Pico manually.firmware/switch-pico.elfandfirmware/switch-pico.uf2, refreshed from the correspondingbuild/artifacts after every successful build.
To customize the controller grip color while building, pass one of these mutually exclusive options:
# Use a random color for both grips
python3 build.py --random-grip-color
# Use a specific six-digit RGB color for both grips
python3 build.py --grip-color FF00AA
Both options update controller_color_config.h before building. With no color
option, that file is left unchanged. Run python3 build.py --help to see the
available command-line options.
If the tools or artifacts are in non-default locations, use these environment variables:
PICOTOOL_PATH=/path/to/picotool \
ELF_PATH=/path/to/switch-pico.elf \
UF2_PATH=/path/to/switch-pico.uf2 \
python3 build.py
PICOTOOL_PATH selects the flashing tool, ELF_PATH selects the ELF that is
checked and flashed, and UF2_PATH selects the UF2 that is checked after the
build. Their defaults are picotool from PATH, build/switch-pico.elf, and
build/switch-pico.uf2, respectively.
Manual build
cmake -S . -B build -DSWITCH_PICO_LOG=OFF
cmake --build build -j
This produces both build/switch-pico.elf and a flashable build/switch-pico.uf2.
Manual UF2 flashing (BOOTSEL, no tools)
If you already have a built (or use the pre-built one in firmware/) .uf2, you can flash it without rebuilding:
- Unplug the Pico.
- Hold the BOOTSEL button.
- While holding BOOTSEL, plug the Pico into your computer over USB (not the Switch), then release BOOTSEL.
- A USB mass-storage drive (usually
RPI-RP2) will appear. Copy the.uf2onto it (drag-and-drop). - The Pico will reboot automatically and the
RPI-RP2drive will disappear when flashing completes.
Tip: if you don’t see RPI-RP2, try a different USB cable (some are charge-only) or a different USB port/hub.
Flash alternatives: bootsel + drag-drop or picotool load.
Flags:
SWITCH_PICO_LOG: enable/disable UART logging on the Pico.
Python bridge (recommended)
Works on macOS, Windows, Linux. Uses SDL3 + pyserial.
Install dependencies (pyproject-enabled)
The repository now includes a pyproject.toml, so you can install the bridge and helper scripts as an editable package:
# from repo root
uv venv .venv
source .venv/bin/activate # or .venv\Scripts\activate on Windows
uv pip install -e .
Prefer stock pip?
python -m venv .venv
source .venv/bin/activate # or .venv\Scripts\activate on Windows
pip install -e .
- SDL3 runtime: install via your OS package manager (macOS:
brew install sdl3; Windows: placeSDL3.dllon PATH or next to the script; Linux: installlibsdl3-0or your distribution's equivalent).
Run
source .venv/bin/activate # or .venv\Scripts\activate on Windows
controller-uart-bridge --interactive
# or, equivalently
python -m switch_pico_bridge.controller_uart_bridge --interactive
Options:
--map index:PORT(repeatable) to pin controller index to serial (e.g.,--map 0:/dev/cu.usbserial-0001or--map 0:COM5).--ports PORTS...or--interactivefor auto/interactive pairing.--all-portsto include non-USB serial devices in discovery.--ignore-port-desc SUBSTR/--include-port-desc SUBSTRto filter serial ports by description (repeatable).--include-controller-name SUBSTRto only open controllers whose name matches (repeatable).--list-controllersto print detected controllers and their GUIDs, then exit (useful for GUID-based options).--baud 921600(default 921600; use500000if your adapter can’t do 900K).--frequency 1000to send at 1 kHz.--deadzone 0.08to change stick deadzone (0.0-1.0).--zero-sticksto sample the current stick positions on connect and treat them as neutral (cancel drift).--zero-hotkey zto choose the terminal hotkey that re-zeroes all connected controllers on demand (presszby default; pass an empty string to disable).--update-controller-dbto download the latest SDL GameController database before launching (defaults to the bundled copy inswitch_pico_bridge/controller_db/).--controller-db-url URLto override the source URL when updating the controller database (defaults to the official mdqinc repo).--trigger-threshold 0.35to change analog trigger press threshold (0.0-1.0).--swap-abxyto flip AB/XY globally.--swap-abxy-index N(repeatable) to flip AB/XY for controllers first seen at index N (auto-converts to a stable GUID).--swap-abxy-guid GUID(repeatable) to flip AB/XY for a specific physical controller (GUID is stable across runs).--swap-hotkey xto pick the runtime hotkey that prompts you to toggle ABXY layout for a specific connected controller (defaultx; empty string disables).--sdl-mapping path/to/gamecontrollerdb.txtto load extra SDL mappings (defaults toswitch_pico_bridge/controller_db/gamecontrollerdb.txt).--debug-imuto print raw gyroscope and accelerometer readings every ~200ms (useful for verifying sensor data and troubleshooting).--no-imuto disable sensor reading entirely (useful for controllers without gyro, or if motion causes issues).--gyro-scale FLOATto adjust gyroscope sensitivity (default 1.0; reduce below 1.0 if camera rotates too fast; increase above 1.0 for more sensitivity).
Runtime hotkeys
- By default, pressing
zin the terminal re-samples every connected controller's sticks and re-applies neutral offsets. Change/disable with--zero-hotkey. - Press
x(configurable via--swap-hotkey) to open an in-CLI prompt and toggle the ABXY layout for a specific connected controller. This updates the controller's stable GUID list immediately; press again to revert. - Hotkeys work only when the bridge is started from a TTY/console that currently has focus. Pass an empty string to either flag to disable that shortcut (useful when running unattended).
- If you launch the bridge with
--swap-abxy(global swap), the per-controller toggle hotkey will show that the layout is enforced globally and will not override it.
Updating SDL controller mappings
- The bridge ships with a pinned
switch_pico_bridge/controller_db/gamecontrollerdb.txt. Runcontroller-uart-bridge --update-controller-db ...to download the latest database from the official upstream (mdqinc/SDL_GameControllerDB). - The download only touches
switch_pico_bridge/controller_db/gamecontrollerdb.txt; add--controller-db-url https://.../custom.txtif you maintain your own fork. - If the file is missing, the bridge will automatically attempt a download on startup.
Hot-plugging: controllers and UARTs can be plugged/unplugged while running; the bridge will auto reconnect when possible.
Using the lightweight UART helper (no SDL needed)
For simple scripts or tests you can skip SDL and drive the Pico directly with switch_pico_bridge.switch_pico_uart:
from switch_pico_bridge import SwitchUARTClient, SwitchButton, SwitchDpad
with SwitchUARTClient("/dev/cu.usbserial-0001") as client:
client.press(SwitchButton.A)
client.release(SwitchButton.A)
client.move_left_stick(0.0, -1.0) # push up
client.set_hat(SwitchDpad.UP_RIGHT)
print(client.poll_rumble()) # returns (left, right) amplitudes 0.0-1.0 or None
SwitchButtonis anIntFlag(bitwise friendly) andSwitchDpadis anIntEnumfor the DPAD/hat values (aliasSwitchHatremains for older scripts).- The helper only depends on
pyserial; SDL is not required.
macOS tips
- Ensure the USB‑serial adapter shows up (use
/dev/cu.usb*for TX). - Some controllers’ Guide/Home buttons are intercepted by macOS; using XInput/DInput mode or disabling Steam’s controller handling helps.
Windows tips
- Use
COMxfor ports (e.g.,COM5). Auto‑detect lists COM ports. - Ensure SDL3.dll is on PATH or alongside the script.
Linux tips
- You may need udev permissions for
/dev/ttyUSB*//dev/ttyACM*(add user todialout/uucpor useudevrules).
IMU / Motion Controls
The bridge supports gyroscope and accelerometer passthrough from controllers that have motion sensors (e.g. the Nintendo Switch Pro Controller and DualSense). Motion data is forwarded to the Pico as a rolling three-sample window; the Pico emits standard 0x30 reports at 15 ms intervals and supports both raw IMU mode 1 and packed quaternion mode 2.
Requirements
- A controller with gyro/accelerometer support that SDL3 can enable.
- The Switch will automatically use motion data once the controller is recognised as a Pro Controller.
Gyro bias calibration
On startup, the bridge collects the first 200 gyro readings while the controller is stationary and averages them to compute a per-axis bias (zero-rate offset). The bias is subtracted from subsequent readings. Keep the controller still during startup for best results.
CLI flags
--debug-imu: Print raw sensor values (m/s² and rad/s) and converted Switch integer counts every ~200ms. Useful for verifying the sensor is detected and producing sensible data.--no-imu: Disable IMU entirely. The bridge sends zero motion data to the Pico, which sends zero-filled IMU bytes to the Switch. Buttons and sticks are unaffected.--gyro-scale FLOAT(default 1.0): Multiply all gyro values by this factor before sending. Reduce below 1.0 if the camera moves too fast; increase above 1.0 for more sensitivity.
Troubleshooting
- Gyro not detected: Run with
--debug-imu. If no IMU readings appear, SDL3 cannot see sensors on the controller. On Linux, thehid-nintendokernel driver may expose Nintendo controller motion differently; DualSense motion is supported by SDL3's PlayStation HID driver. - Wild camera swinging: Rebuild and flash the current Pico firmware. Older builds acknowledged quaternion IMU mode 2 but emitted raw mode-1 bytes, which Zelda interpreted as random quaternion data. Keep the controller still during startup, then use
--gyro-scaleonly for deliberate sensitivity adjustment. - Verifying Pico output: Use
uv run python tools/read_pro_imu.py --vid 0x057E --pid 0x2009to read raw IMU bytes directly from the Pico's USB HID output. A stationary controller should show gyro values near zero and three non-empty, non-duplicated samples per report.
Implementation notes for maintainers
The failure
Nintendo subcommand 0x40 is a mode selector, not a Boolean enable:
| Value | Meaning | Required bytes 13-48 in report 0x30 |
|---|---|---|
0 |
IMU off | Zero-filled |
1 |
Raw IMU | Three 12-byte accelerometer/gyro samples |
2 |
Quaternion | Nintendo's packed 36-byte mode-2 structure |
The previous firmware stored the argument in bool is_imu_enabled. A mode-2 request therefore enabled the raw mode-1 packer. Zelda then decoded raw sensor bytes as mode bits, compressed quaternion components, deltas, and timestamps, producing apparently random camera rotation. The fake also advertised firmware 4.91, while the genuine wired Pro Controller used during diagnosis reported 3.48.
Keep SwitchImuMode as a three-state value. Never acknowledge mode 2 and then emit mode-1 bytes.
Mode-1 implementation
- Emit one
0x30report every 15 ms. - Advance the report timer by 3: one timer tick for each nominal 5 ms IMU sample.
- Pack three chronological samples as signed little-endian
accel X/Y/Z, thengyro X/Y/Z. - The host bridge must retain and republish its latest three-sample window. Do not drain it at the faster UART rate; that previously produced empty and duplicated USB reports.
- With the advertised factory calibration, 1g is approximately 4096 counts and 1 rad/s is approximately 818.5 gyro counts.
Mode-2 implementation
switch_pro_driver.cpp implements this in integrate_motion_sample() and fill_quaternion_imu_report_data():
- Reset quaternion state to
(0, 0, 0, 1)when transitioning into mode 2. - Integrate each report's three gyro samples at 5 ms per sample. The Nintendo quaternion axes use sensor
Y, X, Z, notX, Y, Z. - Build a delta quaternion from the angular rotation vector, multiply it into the current orientation, and normalize after every sample.
- Select the largest absolute quaternion component. Its index and sign represent the omitted component; encode the other three signed components at 21-bit precision.
- Pack accelerometer data in
Y, X, Zorder, set the mode field to2, write the 11-bit millisecond timestamp, and set the timestamp/sample count to3. - Integrate and repack only when transmitting the next 15 ms USB report. Calling the integrator from the unrestricted main loop over-integrates the same UART samples.
The mode-2 wire format is bit-packed and fields cross byte boundaries. Use write_bits_le() rather than C/C++ bitfields so layout does not depend on compiler bitfield rules.
Regression and hardware verification
After changing any IMU conversion, calibration, timing, or report packing:
- Run
uv run --with pytest pytest -q. - Build with
cmake --build build -j. - Capture at least 200 raw
0x30reports. Stationary gyro should remain near zero; there should be no empty windows, duplicated three-sample windows, or timer-step errors. - Send subcommand
0x40with value2. Every resulting report must have mode bits2and timestamp count3. - Inject a known single-axis gyro rate and decode the packed quaternion. The corresponding component must change smoothly with the expected sign.
- Perform the decisive end-to-end check: genuine Pro Controller → SDL3 bridge → UART → emulated Pico → Zelda. This path was confirmed correct after the mode-2 fix.
References
- GP2040-CE (controller firmware ecosystem): https://github.com/OpenStickCommunity/GP2040-CE
- nxbt (Switch controller research/tools): https://github.com/Brikwerk/nxbt
- Nintendo Switch Reverse Engineering notes: https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
hid-nintendodriver reference: https://github.com/DanielOgorchock/linux/blob/ogorchock/drivers/hid/hid-nintendo.c
Troubleshooting
- No input on Switch: verify UART wiring (Pico GPIO4/5), baud matches both sides, Pico flashed with current firmware, and
Pro Controller Wired Communicationis enabled on the Switch. - Constant buzzing rumble: the bridge filters small rumble payloads; ensure baud isn’t dropping bytes. Try lowering rumble scale in
switch_pico_bridge.controller_uart_bridgeif needed. - Guide/Home triggers system menu (macOS): try different controller mode (XInput/DInput), disable Steam overlay/controller support, or connect wired.
- SDL can’t see controller: load
switch_pico_bridge/controller_db/gamecontrollerdb.txt(default), add your own mapping, or try a different mode on the pad (e.g., XInput).