Embedded firmware for the Adafruit Feather RP2040 USB Host, Type A, product 5723. The firmware accepts exactly one wired Sony DualSense or DualSense Edge controller on the USB host port and exposes a profile-specific USB HID device to the PC:
- USB HID keyboard
- USB HID mouse
- Compile-time selected USB HID gamepad backend: Google Stadia Controller by default (VID
0x18D1, PID0x9400) or DualShock 4 (VID0x054C, PID0x09CC) - Optional vendor-defined status HID interface for overlays, enabled by default
The active profile determines which HID reports the Feather sends:
- KBM profile – maps controller buttons to keyboard and mouse reports
- Gamepad profile – forwards controller state as gamepad reports
- Hybrid profile – forwards gamepad reports and adds touch-activated gyro mouse
- Gyro Stick profile – optional profile that forwards gamepad reports and maps touch-activated gyro to the right stick
Perform a full-width touchpad swipe (single finger, edge to edge) to switch profiles: left→right selects the next profile, right→left selects the previous profile. The default profile order is KBM → Gamepad → Hybrid → KBM. If FEATHER_GYRO_STICK_PROFILE=ON is configured, the order is KBM → Gamepad → Hybrid → Gyro Stick → KBM. Profile switches are written to flash and reboot the board so USB re-enumerates with the selected profile's HID interface set. Empty or invalid flash storage falls back to KBM profile.
Configuration is intentionally compile-time only. Mappings live in src/mapping.h; there is no runtime configuration UI or script.
The project started from the idea of making a wired DualSense or DualSense Edge feel like an Input Labs Alpakka-style gyro controller, using the Adafruit Feather RP2040 USB Host as a small standalone adapter. In this repository, "DualPakka" refers to that idea: PlayStation controller hardware with touch-activated gyro-to-mouse output and keyboard/mouse button mappings.
In KBM mode, touching the DualSense touchpad enables gyro aiming and sends gyro movement as relative mouse input. The rest of the controller is mapped to keyboard and mouse actions, so games see a regular keyboard and mouse instead of a controller. This avoids depending on PC-side mapper software such as Steam Input or reWASD.
Responsiveness is a core design goal. The firmware forces the DualSense input endpoint to 1 ms and exposes 1 ms HID output reports, targeting a direct 1000 Hz input-to-output path.
A separate gamepad mode is included for games where native controller input works better and gyro is not useful, such as racing games. By default, it emulates a Google Stadia Controller because that profile works reliably for the author's macOS cloud gaming setup with Shadow PC and GeForce Now. It avoids PlayStation-controller mapping issues in Shadow PC when USB forwarding is not used, and avoids XInput-style duplicate-controller detection through SDL2. A DualShock 4 backend can be selected at compile time for testing. Rumble and DualSense-specific features are intentionally out of scope.
This project is intended to be used with a conductive tape mod on the DualSense touchpad, inspired by community Dualpakka examples such as this r/GyroGaming post. The tape extends touch activation to a more natural finger position near the face buttons, while the firmware still sees a normal touchpad touch and arms gyro in the usual way.
Reliable detection needs enough conductive area on the touchpad itself. A very narrow strip can be inconsistent; a wider vertical contact patch on the touchpad improved recognition in testing. Use thin, flexible conductive or Faraday fabric tape with conductive adhesive.
This project is inspired by the Input Labs Alpakka controller, especially its firmware-level approach to gyro aiming as mouse-like primary input without requiring PC-side remapping software. The goal is not to recreate Alpakka exactly, but to bring a similar control philosophy to wired DualSense and DualSense Edge controllers.
It also builds on ideas explored in the earlier DualSense Gyro Mouse Profile (Dualpakka), a HID Remapper profile that mapped DualSense Edge gyro to mouse movement, activated gyro through touchpad contact, and used expression-based logic for Alpakka-style FPS controls.
Dualpakka tape mod variants shared by the Gyro Gaming community helped shape the touch-activation ergonomics, including exposed conductive-tape layouts and cleaner internal-tape versions such as My Dualpakka!.
HID Remapper and jfedor2/hid-remapper also showed the potential of using a small standalone adapter to translate input on the device side, expose standard HID outputs to the host, and avoid keeping mapper software running on the PC. Seeing that potential, I used AI-assisted iterative development to build this dedicated DualSense/DualSense Edge firmware around the same standalone-adapter idea.
Feather DualSense HID Remapper turns that profile experiment into dedicated firmware for the Adafruit Feather RP2040 USB Host, with profile-specific USB descriptors, 1000 Hz input/output handling, touch-activated gyro mouse and gyro stick modes, selectable gamepad backends, persistent profile switching, and a Status HID interface for overlays.
Useful communities and related project spaces:
- Target board: Adafruit Feather RP2040 USB Host, Type A, 5723
- Pico board variable:
PICO_BOARD=feather_host - Host wiring: PIO USB D+ on GPIO 16, D− on GPIO 17, VBUS enable on GPIO 18
- Input: wired USB controller on the Feather USB host port
- Output: profile-specific HID device to the PC
Only these VID/PID pairs are accepted:
| Controller | VID | PID |
|---|---|---|
| Sony DualSense | 0x054C |
0x0CE6 |
| Sony DualSense Edge | 0x054C |
0x0CE7 |
| Sony DualSense Edge (alt) | 0x054C |
0x0DF2 |
All other USB devices are ignored.
The Feather enumerates keyboard and mouse HID interfaces:
- Keyboard: 14KRO keyboard report
- Mouse: relative mouse with 16-bit signed X/Y axes
The Feather enumerates only a gamepad HID interface that mimics the selected gamepad backend. Stadia Controller is the default backend; DualShock 4 can be selected at compile time. Analog sticks, D-pad hat, and all digital buttons are forwarded.
Hybrid profile enumerates keyboard, mouse, and gamepad HID interfaces. It uses the same gamepad mapping as Gamepad profile and additionally sends touch-activated gyro movement as relative mouse X/Y. Pressing the Mute button sends F12 in this profile for screenshot shortcuts. Other controller buttons are not mapped to keyboard actions yet, but the keyboard interface is present for future hybrid mappings.
On macOS, Hybrid profile currently works with the Stadia backend only. The DualShock 4 firmware can still be built, but its mixed mouse+gamepad Hybrid profile does not work reliably on macOS at this time.
Gyro Stick profile is enabled with -DFEATHER_GYRO_STICK_PROFILE=ON, which sets GYRO_STICK_PROFILE_ENABLE at compile time. It uses the same gamepad mapping as Gamepad profile. While the touchpad is touched, gyro movement is mapped to the right analog stick instead of mouse X/Y. When the touchpad is not touched, the physical right stick is forwarded normally.
The firmware exposes a vendor-defined Status HID interface in every profile by default. It is intended for browser overlays or small bridge tools that need controller state without parsing the gamepad, keyboard, or mouse outputs. The status interface uses vendor usage page 0xFF42, report ID 0x7E, and sends a fixed binary input report at 60 Hz. The USB interrupt packet is 64 bytes total: 1 byte report ID plus 63 bytes of payload.
All multi-byte fields are little-endian. The report payload excludes the report ID:
| Byte | Type | Field |
|---|---|---|
| 0 | uint8 |
Report version, currently 2 |
| 1 | uint8 |
Controller type: 0 unknown, 1 DualSense, 2 DualSense Edge |
| 2-3 | uint16 |
Sequence counter, incremented after each accepted status report |
| 4-11 | uint64 |
Raw controller button bitmask using the mapping::Button order in src/mapping.h |
| 12 | uint8 |
Left stick X, raw DualSense value |
| 13 | uint8 |
Left stick Y, raw DualSense value |
| 14 | uint8 |
Right stick X, raw DualSense value |
| 15 | uint8 |
Right stick Y, raw DualSense value |
| 16 | uint8 |
L2 analog, raw DualSense value |
| 17 | uint8 |
R2 analog, raw DualSense value |
| 18-19 | uint16 |
Flags |
| 20 | uint8 |
Touch point 0 ID |
| 21 | uint8 |
Touch point 1 ID |
| 22-23 | uint16 |
Touch point 0 X |
| 24-25 | uint16 |
Touch point 0 Y |
| 26-27 | uint16 |
Touch point 1 X |
| 28-29 | uint16 |
Touch point 1 Y |
| 30-31 | int16 |
Gyro X, raw DualSense value |
| 32-33 | int16 |
Gyro Y, raw DualSense value |
| 34-35 | int16 |
Gyro Z, raw DualSense value |
| 36-37 | int16 |
Accelerometer X, raw DualSense value |
| 38-39 | int16 |
Accelerometer Y, raw DualSense value |
| 40-41 | int16 |
Accelerometer Z, raw DualSense value |
| 42-43 | int16 |
Neutralized lean roll angle in centidegrees, gravity/gyro fused |
| 44-45 | uint16 |
Last accepted DualSense input interval, microseconds |
| 46-47 | uint16 |
Maximum accepted DualSense input interval since the last status report |
| 48-49 | uint16 |
Last HID input callback processing duration, microseconds |
| 50-51 | uint16 |
Maximum HID input callback processing duration since the last status report |
| 52-53 | uint16 |
Count of accepted input intervals above 1500 us since the last status report |
| 54-55 | uint16 |
Count of busy Status HID sends since the last successful status report |
| 56-62 | uint8[7] |
Reserved, currently zero |
Status flags:
| Bit | Meaning |
|---|---|
| 0 | Controller connected |
| 1 | Touch point 0 active |
| 2 | Touch point 1 active |
| 3 | Gyro mouse armed |
| 4 | Gyro stick armed |
gyro_mouse_armed is set while the touchpad is touched in KBM or Hybrid profile. gyro_stick_armed is set while the touchpad is touched in Gyro Stick profile. Gyro and accelerometer fields are raw DualSense values. lean_roll_centideg is the exception: it is a firmware-estimated, neutralized lean angle for testing future mapping behavior. 2D/3D rendering and any additional display conversion belong in the overlay.
A WebHID status overlay is available at tools/status_overlay.html. It reads the vendor-defined Status HID report directly from the browser and visualizes the controller state without any native helper process. The overlay currently provides:
- A live 3D DualSense Edge view based on a local
tools/dualsenseende.objmodel - Controller pose visualization from raw gyro data, with a very slow permanent return-to-center correction
- Button highlighting for named OBJ parts, including fallback-rendered Fn and rear paddle buttons when those parts are not present in the OBJ
- Stick tilt visualization and active-stick rings
- Two-finger touchpad visualization with moving touch markers on the 3D touchpad surface
- Raw report, stick, trigger, touch, gyro, accelerometer, and status flag readouts
- Configurable background and highlight colors stored in browser local storage
- A fullscreen 3D view for OBS or streaming overlays
- Automatic WebHID reconnect attempts after profile switches, because the board reboots and re-enumerates USB when the active profile changes
Serve the repository through a local web server before opening the page, because the overlay uses ES module imports for Three.js:
The DualSense Edge OBJ model is not included in this repository. Place a local model at tools/dualsenseende.obj if you want the 3D controller view; the status readouts still work without committing that local asset.
python3 -m http.server 8000Then open http://localhost:8000/tools/status_overlay.html in a WebHID-capable browser.
OBS Browser Source does not provide WebHID access. For OBS, use the local Go Status Bridge, which reads the same Status HID interface and streams reports to the overlay with Server-Sent Events:
cd tools/status_bridge
go run .Then use this OBS Browser Source URL:
http://127.0.0.1:8765/tools/status_overlay.html?input=sse
The SSE overlay starts in fullscreen 3D mode and uses a transparent background for OBS. Override the default red highlight color with a highlight query parameter, for example ?input=sse&highlight=00ff00.
For safety, the bridge only serves status_overlay.html, dualsenseende.obj, and the /events SSE endpoint. Chrome/WebHID usage remains unchanged when the input=sse query parameter is not used.
The bridge uses a native non-exclusive IOKit HID reader on macOS. Windows and Linux builds use HIDAPI and select the Status HID interface by vendor usage when available, with known DualPakka status interface numbers as a fallback. On Linux, HID access may require udev permissions depending on the distribution.
| Controller Input | Output |
|---|---|
| Cross | F |
| Circle | V |
| Square | R |
| Triangle | T |
| L1 | Q |
| R1 | E |
| L2 | Right mouse button |
| R2 | Left mouse button |
| Options | Escape |
| Create / Share | Tab |
| L3 | J |
| R3 | 0 |
| Touchpad click (left third) | M |
| Touchpad click (middle third) | Middle mouse button |
| Touchpad click (right third) | N |
| PS button | Enter |
| Mute | F12 |
| D-Pad Up | Arrow Up |
| D-Pad Right | Arrow Right |
| D-Pad Down | Arrow Down |
| D-Pad Left | Arrow Left |
| Left stick (WASD zone) | W / A / S / D |
| Left stick (inner ring) | L |
| Right stick | Numpad 1–8 |
| Gyro (while touching touchpad) | Relative mouse X/Y |
| Touchpad vertical swipe | Scroll wheel |
| Touchpad full-width swipe left→right / right→left (single finger) | Next / previous profile |
| Controller Input | Output |
|---|---|
| Left rear paddle | Left Shift |
| Right rear paddle | Space |
| Fn1 | X |
| Fn2 | Y |
Controller buttons are translated to the equivalent Stadia Controller buttons:
| DualSense | Stadia |
|---|---|
| Cross | A |
| Circle | B |
| Square | X |
| Triangle | Y |
| L1 | LB |
| R1 | RB |
| L2 | LT |
| R2 | RT |
| L3 | L3 |
| R3 | R3 |
| Create | Back |
| Options | Options |
| PS | Guide |
| D-Pad | D-Pad |
| Left stick | Left stick |
| Right stick | Right stick |
| Edge Fn1 | Assistant |
| Edge Fn2 | Capture |
Mute, Touchpad click, and Touchpad touch are not forwarded in gamepad mode.
Gyro movement is mapped to relative mouse X/Y while at least one finger touches the touchpad. When no touch is active the gyro output is suppressed and the sub-pixel accumulator is reset.
Bias correction — While the controller is held still (all three gyro axes below threshold) and the touchpad is not being touched, a per-axis bias estimate is updated via an EMA (α ≈ 0.0025, ~0.4 s time constant at 1000 Hz). This continuously compensates for sensor drift without interfering with aiming.
Soft suppression — Instead of a hard deadzone, small residual values after bias subtraction are shaped with a quadratic fade-in (output = v × |v| / threshold, threshold = 15 raw units). Micro-movements are preserved rather than cut off.
1000 Hz polling — On mount, the firmware patches the DualSense interrupt IN endpoint interval to 1 ms (overriding the 4 ms default). The sensitivity constant is tuned accordingly.
Base sensitivity: GYRO_MOUSE_SENSITIVITY_Q16 = -171 (~−0.0026 per raw unit at 1000 Hz).
Axis scale factors: X = 1.0, Y = 0.7.
Perform a full-width touchpad swipe (single finger from one edge to the other, ≥ ~80 % of pad width) to switch profiles. Left→right selects the next profile; right→left selects the previous profile. The default order is KBM, Gamepad, and Hybrid. If FEATHER_GYRO_STICK_PROFILE is enabled at configure time, Gyro Stick profile is added after Hybrid. The selected profile is saved to flash and the board reboots so the USB HID interfaces match the profile.
The swipe gesture works in all profiles. A second finger on the pad at any point during the swipe cancels it.
Install the ARM toolchain, CMake, Make, the Pico SDK, and Pico-PIO-USB. Either set PICO_SDK_PATH or check out pico-sdk at the repository root. Pico-PIO-USB must be available at pico-sdk/lib/tinyusb/hw/mcu/raspberry_pi/Pico-PIO-USB.
macOS: Homebrew's arm-none-eabi-gcc does not include nosys.specs. Install the official ARM toolchain instead:
brew install --cask gcc-arm-embedded
sudo installer -pkg /opt/homebrew/Caskroom/gcc-arm-embedded/15.2.rel1/arm-gnu-toolchain-15.2.rel1-darwin-arm64-arm-none-eabi.pkg -target /
export PATH="/Applications/ArmGNUToolchain/15.2.rel1/arm-none-eabi/bin:$PATH"
# Add the export to ~/.zshrc to make it permanentgit submodule update --init --recursive
mkdir -p pico-sdk/lib/tinyusb/hw/mcu/raspberry_pi
git clone --depth 1 https://github.com/sekigon-gonnoc/Pico-PIO-USB.git \
pico-sdk/lib/tinyusb/hw/mcu/raspberry_pi/Pico-PIO-USB
mkdir build
cd build
PICO_BOARD=feather_host cmake ..
make -j$(nproc) # Linux
make -j$(sysctl -n hw.logicalcpu) # macOSThe build produces one flashable artifact per gamepad backend:
build/feather_remapper_stadia.uf2
build/feather_remapper_ds4.uf2Put the Feather into BOOTSEL mode and copy the UF2 file to the mounted RP2040 mass-storage volume.
Check that the Feather enumerates correctly:
# Stadia backend — expect profile-dependent HID interfaces
lsusb -v -d 18d1:9400
# DualShock 4 backend — expect profile-dependent HID interfaces
lsusb -v -d 054c:09ccCheck the connected controller:
lsusb -d 054c:UART debug logging is disabled by default. Enable it for development:
PICO_BOARD=feather_host cmake -DCMAKE_BUILD_TYPE=Debug -DFEATHER_REMAPPER_DEBUG=1 ..
make -j$(nproc)Debug builds print raw HID reports to UART. Do not use debug logging for latency testing.
Edit src/mapping.h. The mapping is a static compile-time table of Action entries indexed by logical controller buttons. No JSON, EEPROM, filesystem, or runtime configuration is used.
The DualSense wired USB report parser targets the common 0x01 input report layout. If a future controller firmware changes button positions, the whitelist will still prevent unsupported devices, but the parser may need adjustment.




