Wireless DualSense-to-PC bridge on a Raspberry Pi Pico 2 W: audio-derived haptics for games that have none, adaptive triggers, gyro aiming, right-stick inversion, and a browser-based configuration portal.
Version 1.22.0
▶️ Configure in your browser — the config portal can run as a web page, no download required. Needs Chrome or Edge, with the dongle plugged in.
📥 Shared game profiles · 🎮 Trigger effects — ready-to-import per-game configs and adaptive-trigger effects captured from real games; download one and load it in the portal above.
A firmware modification for the DS5Dongle (a Raspberry Pi Pico 2W-based wireless DualSense dongle) that adds audio-derived haptics for games without native DualSense support, full DS4Windows compatibility, converted-rumble blending, a controller-speaker effect leak for added immersion, and custom captured trigger effects — record a real adaptive-trigger effect from a game that has one and replay it in games that don’t — all configurable from a web-based portal.
⚠️ Hardware requirement — an RP2350 board. Two are supported:
- Raspberry Pi Pico 2 W — the released
.uf2is built for this board. Flash it and you’re done.Waveshare RP2350B-Plus-W (USB-C, 16 MB flash, RM2 wireless) — a prebuilt
ds5-v1.22.0-waveshare.uf2now ships with each release; flash that and you’re done. It is built against pico-sdk 2.2.0, as this board requires. It has not yet been confirmed on hardware by anyone — if you have this board, a report either way is very welcome. To build it yourself instead, one command:Windows: powershell -ExecutionPolicy Bypass -File tools\build-windows.ps1 -Variant waveshare macOS: ./tools/build-macos.sh --waveshareThese fetch the right toolchain and SDK for you and write
ds5-bridge-waveshare.uf2into its own build directory. This board needs pico-sdk 2.2.0, newer than the 2.1.1 used for the Pico 2 W build — and the difference matters: against 2.1.1 this target still compiles, but the resulting firmware does not work properly, because the RM2 wireless needs 2.2.0. If you build by hand withboards/build_waveshare_rp2350b_plus_w.sh, it now checks your SDK version and refuses rather than handing you a silently broken build.The original Raspberry Pi Pico W (RP2040) is not supported — it is a different chip (Cortex-M0+ vs Cortex-M33, less memory, no FPU), so a binary built for one cannot run on the other, and this firmware’s floating-point audio DSP (auto-haptics, the effect leak, resampling) is designed around the RP2350’s FPU and larger RAM. Check which board you have before flashing.
Built on awalol/DS5Dongle v0.7.0, which relocates the entire BT/USB/audio path to RAM so native fine haptics and controller audio work without overclocking.
R3 + D-pad Up) or a touchpad swipe to a
keyboard combo, recorded by pressing the actual buttons and typing the actual
keys. Up to 32, with tap-vs-hold and captured release order. Definitions are
shared; which ones are live is per-profile, so Playnite switches macro sets per
game.Game = slot 3 in profile-overrides.txt), and applied
atomically so a game launch can never leave the dongle half-configured.The Wake PC on PS Button feature (USB remote wakeup) originates in the awalol v0.7.0 base and is exposed here as a portal toggle, substantially reworked. It is off by default; enable it only if you want the controller’s PS button to wake the PC from sleep.
With wake enabled the bridge stays on the USB bus whenever the controller is switched off — it has to, or it could never tell that the PC went to sleep — but it does so under a separate USB identity, so nothing appears as a controller that is not there. The details, and what you will see in Windows, are in the callout below.
Wake is a per-profile setting — on for auto-haptics games, off for native ones. Enabling wake changes the controller’s USB descriptor (it advertises USB 2.1 with a BOS descriptor and adds a keyboard interface, which the wake mechanism requires). The altered descriptor no longer looks like a “pure” DualSense, so games with native DualSense support can stop recognising it — a game such as Ratchet & Clank may fall back to Xbox-style rumble instead of native haptics, and the controller’s speaker audio can stop working.
Because wake is one of the settings that requires re-enumeration, a profile that switches it takes the dongle off USB for a second or two. Anything that talks to the dongle right afterwards has to wait for it to come back — the Playnite automation handles this, see
automation/AUTOMATION-README.md.That only matters for games that use the controller natively. For non-native games driven by an auto-haptics profile, nothing is relying on native recognition — the haptics are synthesised from game audio — so wake can safely stay on there.
With wake on, the dongle stays present on USB even when the controller is switched off — it has to, or there would be nothing on the bus for the PC to suspend, and it could never tell the PC had slept. This is what lets the PS button wake the PC even when you switched the controller off before putting the PC to sleep — the usual case, and the one that fails outright if the dongle leaves the bus. In that state it appears under a different USB identity — different vendor and product IDs and its own name — so no controller shows up in DS4Windows or anywhere else; its normal identity returns when the controller reconnects.
What you will see while the controller is off. Windows treats the idle identity as its own device, listed as DS5Dongle (controller off). It is visible in
joy.cpl(Game Controllers) and under Settings → Bluetooth & devices → Devices. This is expected and is not a phantom controller: it is the device the PC will be woken from. It also has its own Allow this device to wake the computer setting — usually granted automatically, and the portal’s wake diagnostics will tell you if it was not.First appearance switches your audio output. Because the idle identity is a new device carrying the same audio interfaces, Windows discovers it as a fresh sound device and, by default, makes it the active output the first time it appears — so PC audio goes silent (or to the wrong endpoint) until you switch back. Set your normal output device again in the volume flyout or Sound settings; Windows remembers the choice, so this is a one-time correction rather than something that recurs.
DS4Windows does not pick it up, by design. The idle identity does not match the controller IDs DS4Windows looks for, so it is not captured — which means DS4Windows stays free to auto-load profiles for any other controller you connect while the DualSense is off. If you would rather not see it at all, it can be hidden with HidHide like any other device, and it then disappears from games as well. Hiding it does not affect waking, which is handled by Windows at the USB level rather than by anything that reads the device.
With wake off the dongle removes itself from USB entirely when the controller disconnects.
Wake PC on PS Buttonis an ordinary configuration field, so every profile and every on-dongle slot carries its own value. The practical setup is:
- auto-haptics profiles → wake on
- native-game profiles → wake off
and the automation switches it per game along with everything else.
One caveat about how the switch is applied. Changing wake forces a USB re-enumeration. Slot activation (
slot Ninprofile-overrides.txt) sends a single command and the firmware applies the whole configuration at once, which handles this cleanly. A field-by-field.htmlprofile applies settings one at a time over the live connection, so a re-enumeration mid-apply can interrupt it — prefer slots for any profile that changes wake.
ds5-v1.22.0.uf2 (Pico 2 W) or ds5-v1.22.0-waveshare.uf2 (Waveshare) to the
RPI-RP2 drive that appears.
flash_nuke.uf2 (the one supplied is built for
the Pico 2 W). Settings and saved profile
slots survive an upgrade — new options are appended to the stored layout, so
old values keep their meaning and anything new lands on its default. Only
run it if a release note explicitly says to, or if the portal shows settings
that are clearly nonsense. It erases every setting and all 32 profile
slots, so back up your slots first (Slots tab → Back up all slots).ds5-config-portal.html and open the
downloaded file in Chrome or Edge. (WebHID needs a secure context — opening it
directly from a website host or file:// that the browser flags will fail with
a permissions error. Downloading it and opening the local file works, as does
serving it from http://localhost.)Decide what you need — this determines whether there is anything left to do.
Mix / Replace only — install and run the audio bridge. Auto-haptics are derived from your PC’s game audio, so a small helper has to capture that audio and feed it to the dongle. Windows only, since it uses WASAPI:
Install its one dependency:
pip install PyAudioWPatch
Run it, and leave it running while you play:
python automation/ds5audio.py
No device flags are needed for most setups — it finds the DualSense on its own
(--list, --in-index and --out-index are there for when it picks wrong).
In Mix or Replace the audio bridge is not optional — and it is not part of the Playnite automation. Without
ds5audio.pyrunning there is no audio for the firmware to derive from, so auto-haptics will appear to do nothing however they are tuned. That is the single most common reason auto-haptics “doesn’t work”. In Off (native) mode it isn’t needed at all.
(Not sure it’s running? Since 1.18.25 the Haptics tab shows an “Audio bridge: active / not detected” line right under the auto-haptics settings, plus two meters — the audio arriving on the dongle against the haptic the DSP is deriving from it — so you can watch “Haptics out” respond as you tune, and see at a glance whether audio is reaching the dongle at all.)
The firmware ships with safe stock defaults (auto-haptics off, standard buffer), so a fresh flash is safe and native passthrough works immediately. For a tuned auto-haptics + immersion setup, see Suggested setup below.
Games with native DualSense support drive the controller themselves and the dongle passes their haptics straight through. Whether that actually happens is decided by two things outside this project, and both catch people out.
1. Steam: set the controller override to Disabled for that game. Right-click the game in Steam → Properties → Controller, and set the override to Disabled. Not Default, and not Enabled — either of those leaves Steam Input in front of the controller, the game stops seeing a real DualSense, and native haptics never arrive at all.
2. DS4Windows may have to be closed. If you use DS4Windows to present an Xbox 360 or DS4 virtual pad, Steam can pick up that virtual controller instead of the DualSense and refuse to treat the game as native. Some titles need DS4Windows closed entirely before launch — Doom: The Dark Ages is one. If a game that should have native haptics gives you nothing, close DS4Windows and relaunch it before changing any other setting.
You can stop the second problem happening at all by hiding the DualSense from everything except your native games, so the two worlds never collide. See Mixing native DualSense and virtual controllers in
automation/AUTOMATION-README.md.
Also worth knowing for native games: keep Wake PC on PS Button off in those profiles — see the wake note above for why.
These are good starting values for using auto-haptics with audio passthrough and the effect leak, tuned on a real DualSense over Bluetooth. They are suggestions — adjust to taste — but they give a working, balanced configuration without trial and error. (The firmware does not ship with these as defaults, so there’s no flashing surprise; apply them in the portal and save.)
Auto-Haptics & Speaker Effect Leak
| Setting | Value |
|---|---|
| Mode | Off (switch to Mix or Replace per game) |
| Intensity (%) | 80 Scales the DERIVED auto-haptics only. Native passthrough and converted rumble have their own levels (see ds5audio --map note) |
| Smoothness | 40 |
| Noise Gate | 20 |
| LP Cutoff (Hz) | 100 |
| Frequency Split Crossover (Hz) | 0 (off) |
| Low Band Gain | 100 |
| High Band Gain | 100 |
| Filter Slope | 12 dB/oct |
| Auto-mute Speaker (Replace) | Yes |
| Auto-mute Speaker (Mix) | Yes |
| Lightbar Off in Replace Mode | Yes |
| Converted Rumble Strength (Mix) | 50 (range goes to 200; left/heavy renders at 60 Hz, right/light at 160 Hz) |
| Effect Leak Volume (0=off) | 0 (raise to enable) |
| Effect Leak Sensitivity | 50 |
| Effect Leak Decay/Fade-out | 80 |
| Effect Leak Attack/Responsiveness | 50 |
| Effect Leak Output High-pass (Hz) | 1000 |
| Effect Leak Output Low-pass (Hz) | 8000 |
| Effect Leak Gate Hold (x5 ms) | 20 |
| Effect Leak Max Burst (x5 ms) | 0 (off; try 30) |
| Effect Leak Detection Band (Hz) | 2500 |
General Haptics & Audio
| Setting | Value |
|---|---|
| Native Haptics Gain | 1.00 |
| Speaker Volume | 100 |
| Headset Volume | 100 |
| Speaker Gain | 2 |
| Sync Speaker & Headset Volume | Yes |
| Lock Volume | No |
| Disable Mic | No |
| Disable Speaker | No |
Device & Connection
| Setting | Value |
|---|---|
| Polling Rate | Real-time |
| Audio Buffer Length | 16 |
| Inactive Time (min) | 12 |
| Disable Inactive Disconnect | No |
| Disable Pico LED | No |
| Wake PC on PS Button | On for auto-haptics profiles, off for native games |
Advanced — BT Latency (experimental)
| Setting | Value |
|---|---|
| BT Flush Timeout | Off (reliable) |
| BT QoS Latency | Off |
Notes on the suggested values:
The portal groups settings into the sections below.
| Setting | Range | Default | Notes |
|---|---|---|---|
| Mode | Off / Mix / Replace | Off | Off = native/rumble passthrough; Mix = native + derived; Replace = derived only |
| Intensity (%) | 0–200 | 100 | Strength of the audio-derived haptics only (curved response). It does NOT scale the native passthrough or converted rumble — those have their own levels, so the three components are set independently |
| Smoothness | 0–100 | 40 | Higher = smoother/longer decay; lower = snappier |
| Noise Gate | 0–100 | 20 | Suppresses quiet content (dialog/ambience) below a threshold |
| Native Passthrough in Mix (%) | 0–100 | 100 | MIX MODE ONLY: level of the native ch3/4 haptic stream mixed under the derived haptics. See “Choosing the passthrough level” below |
| Filter Native Passthrough in Mix | Filtered / Raw | Filtered | MIX MODE ONLY: whether the ch3/4 passthrough is low-passed at the LP Cutoff before mixing. See “Filtered or Raw?” below |
| Auto-Haptics DSP Source | ch0/1 / ch2/3 | ch0/1 | Which channel pair the derived haptics are generated from. The speaker and effect leak always read ch0/1. See “Separating the script feed from game audio” below |
| LP Cutoff (Hz) | 30–200 | 60 | Upper edge of the haptics band — only audio below this drives haptics (content above never reaches the actuators) |
| Frequency Split Crossover (Hz) | 0 / 30–200 | 0 (off) | Divides the haptics band in two at this frequency; 0 = single-band (identical to pre-split firmware). Must sit below LP Cutoff |
| Low Band Gain | 0–100 | 100 | Contribution of content below the crossover (impacts, explosions, engine weight) |
| High Band Gain | 0–100 | 100 | Contribution of the crossover..cutoff range (music bass, voice fundamentals) — lower it to tame music/dialog-driven buzz |
| Filter Slope | 6 / 12 / 24 dB/oct | 12 | Steeper rejects voice above the cutoff more aggressively |
In Mix mode the actuators receive three components: the native ch3/4 stream (scaled by this fader), the derived auto-haptics (scaled by Intensity, shaped by the frequency split and gate), and converted rumble (its own strength knob). The fader exists because ch3/4 mean different things in different setups:
| Scenario | Mode | Set passthrough to | Why |
|---|---|---|---|
| Native game, passthrough profile | Off | (ignored) | With auto-haptics Off, ch3/4 always pass at full — the game’s own HD haptics. The fader has no effect in Off |
| Native game + auto-haptics on top | Mix | 100 | Classic Mix: the game’s real haptics plus derived augmentation |
| Non-native game (DS4Windows / XB360 / DS4) | Mix | 0 (or taste) | ds5audio’s default duplicate mapping copies the game audio onto ch3/4 — an uncontrollable shadow of the derived haptics. At 0, Intensity/split/gate control the whole output; game rumble stays on Converted Rumble Strength. With the fader at 0 the ds5audio --map choice no longer matters in Mix |
| Any game | Replace | (ignored) | Replace discards ch3/4 by definition — derived only |
Rule of thumb: the fader answers “is there anything REAL on ch3/4?” Native game = yes, keep 100. Non-native = no (it’s a duplicate), set 0.
The passthrough has always been low-passed at the LP Cutoff before mixing. That is right for a duplicate feed — in ds5audio’s default mapping (and in VoiceMeeter-style routings) ch3/4 carry a copy of the full-band stereo, and passing that raw sends dialogue and treble straight to the actuators. It is wrong for a game that renders its own haptics, because real haptic content lives well above a typical 80 Hz cutoff: filtering removes exactly the effects the passthrough exists to carry, and no amount of passthrough level brings them back.
| Your ch3/4 carries | Set to | Why |
|---|---|---|
| A game’s own native haptics | Raw | Keeps the game’s effects intact. Pair with --map front so the script does not also write to ch3/4 |
A duplicate of the game audio (default --map duplicate) |
Filtered | Blocks dialogue/treble leaking to the actuators. Usually better still: set passthrough level to 0 |
| The script feed, because DSP Source is ch2/3 | Filtered (or level 0) | ch3/4 is raw captured audio here, not haptics — see below |
The derived haptics are always filtered by the LP Cutoff, split and gate regardless of this setting; it applies only to the passthrough component.
The speaker and the effect leak always read ch0/1. With the default DSP source that pair must also carry the script feed the derived haptics are generated from — so anything ds5audio sends is also what the speaker plays and what the leak passes. For a game that outputs its own audio effects through the controller speaker (Ratchet & Clank: Rift Apart is the obvious example, with its weapon and gadget sounds sent to the pad), that means the game’s speaker effects arrive mixed with the whole captured PC soundtrack, and there is no way to hear only the former.
Setting DSP Source to ch2/3 breaks that coupling:
ds5audio.py --map rear # script capture -> ch2/3 only, ch0/1 left clear
| Component | Reads | Result |
|---|---|---|
| Derived auto-haptics | ch2/3 | Generated from the script feed, as before |
| Controller speaker | ch0/1 | Only the game’s own native speaker audio |
| Effect leak | ch0/1 | Only the game’s native effects pass through |
Set Native Passthrough to 0 in this configuration: ch3/4 now carries raw script audio rather than native haptics. On a 2-channel stream the DSP falls back to ch0/1 automatically.
| Game type | ds5audio --map |
Mode | DSP Source | Passthrough | Filter | Rumble |
|---|---|---|---|---|---|---|
| Native haptics (e.g. Returnal, Ragnarök) | front |
Mix | ch0/1 | 100 | Raw | as taste |
| Native speaker audio you want isolated (e.g. Ratchet & Clank) | rear |
Mix | ch2/3 | 0 | (n/a) | as taste |
| Non-native, DS4Windows / XB360 | duplicate |
Mix | ch0/1 | 0 | (n/a at 0) | 50–100 |
| Native haptics, no augmentation wanted | any | Off | — | (full) | (n/a) | (native) |
Every one of these is an ordinary configuration field, so each row can live in
its own dongle slot and be selected per game — but note the --map value is a
ds5audio argument, not a dongle setting, so profiles that need a different
mapping also need the matching launch argument. With the optional Playnite
automation this is handled per game in profile-overrides.txt, which passes
anything after a further comma to the capture script:
Resident Evil 4 = slot 5, audio, --map front
Ratchet = slot 6, audio, --map rear
A game listed in native-games.txt needs the explicit , audio shown above,
or the capture is skipped — the native list excludes exactly those games by
default. See automation/AUTOMATION-README.md.
The Device tab reports Rumble from host — the peak motor values seen since the last read, plus which rumble flags the game requested. Non-zero while a game vibrates means it sends motor values, which Mix re-creates through Converted Rumble Strength. Zeros while you can still feel vibration with auto-haptics Off mean the game delivers its vibration as haptic audio on ch3/4 instead, which is the passthrough’s job — set the filter to Raw. The values are peak-held, so a short burst between polls still registers.
Continuing the Auto-Haptics & Speaker Effect Leak settings:
| Setting | Range | Default | Notes |
|---|---|---|---|
| Auto-mute Speaker (Replace) | on/off | on | Mute controller speaker in Replace mode |
| Auto-mute Speaker (Mix) | on/off | off | Mute controller speaker in Mix mode |
| Lightbar Off in Replace Mode | on/off | off | Kills the lightbar glow in Replace (e.g. blue in Xbox360 mode) |
| Converted Rumble Strength (Mix) | 0–200 | 50 | Strength of a game’s rumble re-created on the actuators in Mix mode. Left/heavy renders at 60 Hz, right/light at 160 Hz. Above 100 deliberately overdrives into the limiter for games whose motor values sit low |
| Effect Leak Volume | 0–100 | 0 (off) | Volume of the transient effect leak through the speaker when auto-muted |
| Effect Leak Sensitivity | 0–100 | 50 | How sudden a level jump counts as an effect (higher = more leaks through) |
| Effect Leak Decay/Fade-out | 0–100 | 40 | How gradually effects fade after triggering (~50 ms .. 500 ms) |
| Effect Leak Attack/Responsiveness | 0–100 | 50 | How fast the gate opens (higher = more immediate, less delay) |
| Effect Leak Output High-pass (Hz) | 50–2000 | 200 | Low wall of the leak window (12 dB/oct) — removes deep bass that pops the speaker |
| Effect Leak Output Low-pass (Hz) | 500–12000 | 8000 | High wall of the leak window (12 dB/oct) — cuts treble sizzle/crackle. With the high-pass forms a band-pass “capture window”: only sound inside it leaks. Automatic make-up gain keeps loudness constant as you move the walls |
| Effect Leak Gate Hold (×5 ms) | 0–100 | 20 (100 ms) | Minimum gate-open time per transient + hysteresis; stops the gate chattering (the “choppy/poppy” leak artifact) |
| Effect Leak Max Burst (×5 ms) | 0–100 | 0 (off) | MAXIMUM gate-open time: cuts sustained sounds (dialogue, music) at the cap with a no-retrigger refractory — one short accent instead of duplicating the room audio; shots end within the cap naturally. Try 30 (150 ms) and raise leak volume: the leak becomes punctuation, not a second speaker |
| Effect Leak Detection Band (Hz) | 100–5000 | 800 | Frequency band the transient detector listens to |
The DualSense haptic actuator is a voice coil that cannot render a near-DC signal, so a naive low-pass of the audio produces no motion. This firmware instead uses the bass envelope of the game audio to amplitude-modulate a 90 Hz carrier that sits in the actuator’s responsive band — turning “how much bass” into felt rumble. A noise gate and a steep, configurable low-pass keep dialog and music from triggering the haptics. Under DS4Windows, the firmware keeps the controller in actuator mode (rather than letting rumble reports force it into motor mode) so the derived haptics keep playing.
The effect leak uses transient detection: it tracks fast and slow envelopes of the high-frequency content and opens the speaker only when the level jumps sharply (an onset), so discrete impacts pass while sustained sound stays muted. The output is high-passed to protect the small speaker from low-frequency popping.
Applies only to native haptics — the signal a game with real DualSense support sends. It does nothing to derived auto-haptics.
| Setting | Range | Default | Notes |
|---|---|---|---|
| Smoothing (anti-alias filter) | Off / Light / Strong | Light | Off = raw and gritty (the pre-1.0.4 texture); Light removes the grit while keeping transient snap; Strong is smoothest but can feel muted |
Guide: leave this on Light. Reach for Off only if you want the older, harsher texture back, and Strong if a particular game’s native haptics sound buzzy or harsh on your pad.
| Setting | Range | Default | Notes |
|---|---|---|---|
| Native Haptics Gain | 1.0–2.0 | 1.0 | Multiplier on native haptic channels |
| Speaker Volume | 0–127 | 100 | Controller speaker volume (also scales haptic strength) |
| Headset Volume | 0–127 | 100 | Headset jack volume |
| Speaker Gain | 0–7 | 2 | Controller speaker gain stage |
| Sync Speaker & Headset Volume | on/off | on | Tie the two volumes together |
| Lock Volume | on/off | off | Ignore in-game volume changes |
| Disable Mic | on/off | off | Disable the controller microphone |
| Disable Speaker | on/off | off | Disable the controller speaker |
Routes the rumble signal into the trigger actuators as a buzz.
| Setting | Range | Default | Notes |
|---|---|---|---|
| R2T Mode | Off / Left / Right / Both | Off | Which trigger(s) buzz from rumble |
| Only vibrate when trigger pressed | on/off | off | on = buzz only when the trigger is pulled past ~25%; off = buzz whenever there’s rumble |
| Strength | 0–100 | 100 | Amplitude multiplier on the rumble value |
| Frequency | 1–255 | 60 | Buzz frequency of the trigger effect (higher = finer/tighter buzz) |
Guide: enable Both with Only While Pressed = on for a subtle “feel the rumble in your triggers only when you’re using them” effect. With off, the triggers buzz continuously whenever the game sends rumble. If a game drives its own trigger effects, R2T yields to it by default (see Force Override below).
L2-gated constant resistance on R2 — hold the aim trigger and R2 stiffens. On top of that, the push-back kick delivers recoil: while resistance is engaged, each rumble/haptics burst momentarily switches the kicking trigger(s) to a low-frequency vibration thump (or a Bow-effect snap, see Kick style) that knocks the trigger back against your finger, then resistance resumes as the burst fades (hysteresis prevents chatter at the threshold).
R2 and L2 are shown side by side in the portal, as two aligned columns of the same settings — the list below applies to each. The one exception is Kick follows (the kick’s envelope source): it is a single signal both triggers listen to, so it sits in its own full-width row beneath the columns rather than in either one.
| Setting (per trigger) | Range | Default | Notes |
|---|---|---|---|
| Mode | Off / Gated by trigger / Always / Gated by shoulder | Off | “Gated by trigger” = the OPPOSITE trigger arms it, analog (L2 arms R2, R2 arms L2). “Gated by shoulder” = the opposite bumper arms it, digital (L1→R2, R1→L2). Any R2/L2 combination is valid |
| Resistance strength | 0–100 | 70 | Resistance intensity (mapped to the effect’s 0–7 range) |
| Arming threshold | 1–255 | 30 | In “Gated by trigger” mode, how far the arming trigger must be pulled (~12% at default). Ignored in shoulder-gated mode (digital on/off) |
| Resistance start position | 0–9 | 0 | Trigger-travel zone where resistance begins (0 = from the start) |
| Resistance shape | Constant / Ramp / Two-stage / Weapon break | Constant | Constant = flat Strength A. Ramp = linear A→B across the pull (racing: light→heavy gas, heavy→light brake). Two-stage detent = Strength A with a wall of Strength B at the detent zone — a tactile bump marking half-press from full-press (fire/alt-fire games). Weapon break = rigid wall then hardware snap-through at the break point — the semi-auto shot break (Strength B unused) |
| Strength B | 0–100 | 70 | Second strength: the ramp’s end value, or the detent wall |
| — Strength 0 in shapes | In Ramp/Two-stage, a strength of 0 means genuinely FREE travel (zone excluded): Ramp A=0 = free at rest building to B; Detent A=0 = a pure bump with free travel around it | ||
| Detent zone / break point | 0–9 | 5 | Two-stage: the wall’s zone. Weapon break: the snap-through point (hw 3–8, forced above start) |
| Activation dead zone | 0–9 | 0 (off) | Below this zone the GAME sees the trigger untouched (no analog, no press bit) — the shot registers only past the zone. Aligns hair-trigger games with the resistance/detent/bow feel; internal effects always see the raw trigger. Not for analog gas/brake inputs |
| Push-back kick strength | 0–100 | 0 (off) | Recoil intensity; scales the thump amplitude with the vibration envelope. 0 = no kick on that trigger (pure resistance) |
| Kick style | Thump / Bow snap | Thump | Thump = vibration buzz (0x26). Bow snap = mechanical push-back via the Bow effect (0x22): the snap force presses the trigger back against the finger — sharper recoil, experimental (feel varies with hold depth) |
| Kick thump frequency | 10–200 | 35 | Vibration frequency of the kick; lower = heavier knock, higher = buzzier (R2T’s default buzz is 60 for comparison) |
| Shared setting | Range | Default | Notes |
|---|---|---|---|
| Kick follows | Rumble / Audio / Both | Both | Envelope source for both triggers’ kicks: game rumble (incl. converted DS4Windows rumble), the auto-haptics audio envelope, or the strongest of the two. Unless the trigger is gated, prefer Rumble only — otherwise the generated haptics keep the audio envelope alive and the trigger moves almost constantly |
Guide: L2-gated gives a shooter-style “aim to feel the trigger tension” effect without needing native adaptive-trigger support. Resistance wins over R2T vibration while engaged, so you can run R2T Both + AT L2-gated together: the triggers buzz with rumble normally, and R2 stiffens the moment you aim. For recoil, start around kick 60–80, frequency 35, source Both: aim, fire, and each shot thumps R2. Audio as a source means even games with zero rumble kick on gunfire via the auto-haptics envelope. The diagnostics box shows the live push-back envelope (0–255) and a KICK flag — the kick fires at envelope ≥ 32, so if the number stays 0 while the game rumbles, the selected source isn’t producing signal.
A second action part-way through the trigger’s travel. Pull to the boundary and the game sees the trigger as normal; push past it and a button press is added. Pair it with a weapon-break effect and the wall you feel is the boundary, so the second stage lands at a point your finger can find.
| Setting | Range | Default | Notes |
|---|---|---|---|
| Two-stage trigger | Off / Add / Add + rescale / Swap / Swap + rescale | Off | Add keeps the trigger held past the boundary; Swap releases it so only the button remains |
| Second-stage boundary | 0–254 | 0 (off) | Trigger position where the second stage engages |
| Second-stage button | any face, shoulder, stick or trigger button | None | What the second stage presses |
Add suits anything where the trigger must stay held — a racing throttle that also engages a boost. Swap suits a soft-press / hard-press split where the two actions are alternatives rather than layers.
Rescale stretches the travel below the boundary over the full range, so the shortened first stage keeps its full analog resolution instead of being clipped.
The second stage can press the other trigger — R2 can drive L2 and vice versa. That moves the analog axis, not just the digital bit, since games read the triggers as axes. A trigger is never offered as its own second-stage button.
If the button you pick is also what gates that trigger’s resistance, the portal warns: pressing through the detent would open the gate arming the resistance you are pressing against, changing the feel under the finger making the press.
There is hysteresis on the boundary, so holding at exactly the crossing point gives one press rather than a stream of them.
Capture a real adaptive-trigger effect from a game that has one, and replay it on any trigger in a game that doesn’t. Effects are stored as the exact 11-byte commands the game sent — they are not decoded into the sliders above (a game’s force curves don’t survive that round-trip), so what you feel is the original effect, not an approximation.
See what an effect looks like. Every effect — whether you build it, load it, or read it back off a trigger — is drawn as a diagram in the portal: trigger travel along the bottom, force up the side, with resistance shown as per-zone steps, a weapon-break wall marking where it gives way, a bow’s ramp and snap point, and vibration as a band over the part of the pull where it plays. Sequenced stages show their hand-off points, and a wall or bow the sequencer cuts short before it fires is faded and marked ✕ — so a truncated effect is obvious at a glance. Since a trigger effect can’t really be conveyed in words, the diagram can be screenshotted and shared. (New in 1.18.25.)
What can and can’t be captured
If the monitor stays empty after you perform the action in-game, that game isn’t sending trigger effects.
1. Capture — Trigger Effect Monitor
Enable and state count are set for you; state count is never set by hand.
2. How this differs from the trigger sliders above
The two systems work on completely different principles, which is why they are mutually exclusive on a trigger:
| Trigger sliders (Adaptive Triggers) | Custom captured effects | |
|---|---|---|
| Where the effect comes from | Synthesised from your parameters (strength, shape, start position, detent) | Replayed from stored raw effect bytes |
| How many effects | One, continuously recomposed | Up to 5 states, one active at a time |
| What changes the feel | Game rumble and audio (kick, R2T, auto-haptics) plus trigger position | Trigger position only |
| Editable | Yes — every parameter is a slider | No — the bytes are fixed (but you can build new ones) |
| Multi-stage pulls | Not possible (one effect slot; the detent gives one bump, not a second wall) | Yes — that is the point |
The sliders are a live synthesiser: they rebuild one effect every cycle in response to what the game is doing, which is why they can react to rumble and audio. Custom effects are a player: the states are fixed, and the only thing that decides which one is active is where your finger is on the trigger. Neither can do the other’s job — the sliders can’t stage two walls in one pull, and a captured effect can’t respond to a game explosion.
3. How assigned effects replay — the mode is chosen automatically by type:
How far each stage runs. A stage hands over to the next one shortly before the next stage’s region begins, so the next effect is always armed ahead of your finger rather than snapping into place under it. The consequence is worth knowing when you design a sequence:
So there are three distinct replay behaviours, picked for you:
A multi-stage effect only sequences under the While held condition. Stages are stepped through by trigger position, which needs the effect armed for the whole pull. Set that trigger to On press or On release and you get a single one-shot of the first stage instead — the later stages simply never play, with no error to tell you why. Assigning from the builder therefore sets the condition to While held (and the zone to 0) for you; assigning from the monitor, a timeline or a file leaves the condition as you have it, so check it if you are building a sequence that way.
4. Timeline recording — for vibration rhythm
The monitor’s history carries no timing. When a vibration’s character is its rhythm (switch-then-hold, pulse patterns), use Timeline recording (Triggers tab, under the effect monitor): press ● Record, perform the action in-game, then Stop & fetch. Every state change is listed with the milliseconds it was held; tick the loopable core (up to 5) and Assign ticked timeline. Replay holds each state for its recorded duration and loops — no rate dialling needed.
Durations are ignored for mechanical states (a wall’s recorded duration is just how long you happened to hold it, not part of the effect), so walls and resistances always sequence positionally even if you assign them from a timeline or load a file that contains durations.
5. Getting a custom VIBRATION to work
This is where the trigger condition matters:
Settings (per trigger)
| Setting | Range | Default | Notes |
|---|---|---|---|
| Custom captured effect — enable | Off / On | Off | On = the custom effect owns this trigger’s effect output (see below) |
| Custom effect trigger condition | While held / On press / On release | While held | While held = armed continuously — required for a multi-stage effect to sequence, and used for a sustained vibration. On press / On release = a one-shot as the trigger crosses the zone, so only the first stage of a sequence would play |
| Custom effect zone | 0–9 | 0 | While held: the re-arm zone — walls reset when the trigger returns below it; 0 = only at full release (finger off, nothing to push against, no click). On press / On release: the engage point — must be ≥ 1 |
| Custom effect A↔B toggle rate | 1–100 | 40 | Only for 2-state vibration actions (~2–40 Hz). Ignored for mechanical and timeline effects |
| Custom effect state count | 1–5 | — | Auto-set by Assign / file load; never set manually |
| Custom effect vs sliders | 3 options | Custom effect always owns | Shares the trigger with the slider stack, gated by the opposite trigger or shoulder button — see Sharing a trigger with the sliders below |
Sharing a trigger with the sliders (gate hand-off)
By default a custom effect owns its trigger outright. The Custom effect vs sliders setting lets the two share instead. The gate is the physical gate input — the opposite trigger pressed past that trigger’s threshold, or the shoulder button if its resistance mode is the shoulder-gated one:
| Setting | Gate engaged | Gate not engaged |
|---|---|---|
| Custom effect always owns (default) | custom effect | custom effect |
| Sliders while gated, custom otherwise | synthesized slider effect | custom effect |
| Custom effect while gated, sliders otherwise | custom effect | synthesized slider effect |
With R2’s resistance set to L2-gated, the middle option gives the classic split: hip fire plays your captured multi-stage weapon effect, and the moment L2 passes its threshold R2 switches to the synthesized resistance, kick and all. The third option inverts it — aim down sights to get the captured effect.
Notes:
Recipe: a custom effect and Rumble → Trigger on the same trigger
Only one effect can occupy a trigger at a time, so a custom effect and the rumble-driven vibration cannot both be present at once. They can take turns, though, and the hand-off is what schedules it. The result is a trigger that resists while you aim and buzzes when the shot lands:
| Setting | Value |
|---|---|
| Custom effect on L2 | your resistance, enabled |
| Custom effect vs sliders on L2 | Sliders while gated, custom effect otherwise |
| L2 resistance mode | Off |
| L2 threshold | the R2 depth at which the swap happens |
| Rumble → Trigger mode | Left trigger only (or Both) |
Why it works: the trigger’s order of precedence is game effects → custom effect → slider resistance → rumble → trigger. Rumble sits below the custom effect, so you don’t gate the rumble at all — you gate the custom effect, and rumble becomes what L2 falls back to. With the L2 resistance mode off, there is nothing in between.
In use: hold L2 and you feel your custom resistance. Pull R2 past the L2 threshold and L2 hands over to the rumble vibration, so the shot is felt in the hand that was holding the aim. Release R2 and the resistance comes back, ready for the next shot. Reverse the two triggers for the mirror-image setup.
Two things make or break the feel:
Worth knowing: because only one effect fits, you get one tactile event when the gate engages and another when it releases — not a click, buzz and click, since the resistance ending and the vibration starting are the same transition. How well the window is covered depends on how long the game’s rumble lasts; a very short burst can leave a quiet gap before you release R2.
What works — and what doesn’t — while a custom effect owns the trigger
While the custom effect owns the trigger (always, by default — or only on one side of the gate, if you set the hand-off above), these have no effect on that trigger:
These still work:
The exclusivity is deliberate: the resistance/kick path engages on mode and position even with its strength at 0, so allowing it to run alongside leaked stray kicks (a bow snap on the next press) between custom-effect engagements.
Tip: to keep normal R2T/resistance in one game and captured effects in another, put them in separate profile slots — the automation switches slots per game.
Sharing effects as files
Save ticked to file writes the effect as readable JSON (raw bytes, plus durations for timeline recordings). Load custom effect file → R2 / L2 loads one onto a trigger and sets enable and state count automatically. The portal’s Back up all slots JSON includes each slot’s custom-effect states as well as its settings, so a backup restores a slot complete. (Backups taken before 1.16.0 predate this and contain settings only.)
Building an effect by hand
When there is nothing to capture, the Manual effect builder authors the same kind of effect from scratch. Add stages one at a time and set each one up:
| Stage | What it is | Set |
|---|---|---|
| Weapon break | A wall the trigger resists, then gives way past | Start and end zone, strength |
| Bow | Rising tension that snaps back at the release point | Start and end zone, and the two force levels |
| Vibration | A buzz over a range of the pull | Zones, frequency, strength |
Stages are stacked in pull order and encoded exactly as the firmware’s own writers would emit them, so a hand-built stage is byte-identical to what the sliders — or a game — would have sent for the same values. Built effects behave like captured ones everywhere else: they sit in the same list, play in sequence as you pull, save to the same JSON files, and are stored in slots the same way. Trigger positions are expressed in the same nine zones used throughout the trigger sections, so a wall at zones 2-3 sits where the same numbers put it in a captured effect.
Ready-to-import adaptive-trigger effects captured from real games live on the Trigger effects page — break-through walls, bows, resistance curves and vibration you can load straight onto L2 or R2 in the portal, then Save to a slot.
| Setting | Range | Default | Notes |
|---|---|---|---|
| Force Override | on/off | off | on = force R2T/AT even when a game/app is sending its own trigger effects (off = yield to the game) |
Maps controller motion onto the right stick for motion aiming.
| Setting | Range | Default | Notes |
|---|---|---|---|
| Gyro Mode | Off / L2-held / Always / Touch-enables / Ratchet | Off | When motion aiming is active (see below) |
| Sensitivity | 1–100 | 50 | Motion-to-stick gain (50 ≈ raw) |
| Horizontal source | Yaw / Roll | Yaw | Yaw = turn the controller; Roll = tilt it sideways |
| Invert gyro aim | X / Y / both | off | Per-axis inversion (bit0 = X, bit1 = Y) |
Gyro modes:
Guide: L2-held + Yaw is the most natural starting point for shooters — turn the controller to fine-tune aim only when aiming down sights. Raise sensitivity if the motion feels sluggish; use invert if the direction feels backwards.
Inverts the physical right stick in the input report the PC sees — independent of gyro aiming, and active in any game with no PC-side software.
| Setting | Range | Default | Notes |
|---|---|---|---|
| Invert Right Stick | X / Y / both | off | Per-axis inversion of the physical stick (bit0 = X, bit1 = Y). Applies whether or not gyro is on. |
Useful for inverted-look setups, or games that only offer inversion on one axis. Because it rewrites the stick values in the report itself it works everywhere, and it composes with gyro aiming — the stick is inverted first, then the gyro delta is added on top with its own invert. (New in 1.18.21.)
Bind a controller button press or a touchpad swipe to a keyboard combo. For example you can press R3 + D-pad Up and map to Ctrl+J, if you use the RivaTuner OSD; swipe left across the touchpad and it receives whatever you assigned. No PC-side software is involved — the dongle sends the keystrokes itself over the same HID keyboard interface the wake feature uses.
Everything is edited on the Macros tab.
| Column | What it does |
|---|---|
| Checkbox | Enables this macro for the current profile. Per-slot — see below. |
| Name | Up to 15 characters, stored on the dongle so your names survive a different PC or a cleared browser. |
| Record input | Press it, then hold the buttons you want or swipe the touchpad, then press Stop. |
| Record output | Press it, then type the key combo on your real keyboard, then press Stop. |
| + / − | Adds or removes a macro row. (The checkbox enables; the minus deletes.) |
Up to 32 macros.
While recording the input, the duration you hold the controller button(s) sets its mode:
R3 + D-pad Up (hold 0.75s).You can bind the same controller button twice, once short and once long, exactly the way the PS button gives you one action on a tap and another on a hold.
One consequence worth knowing: if a button press is bound to both a short and a long macro, the short one can only fire when you release, because until then there is no way to tell a short press from the start of a long one. A button press with only a short-time macro fires immediately on press.
Recording Alt+Tab stores that you released Tab before Alt, and playback reproduces that order. This matters for combos where the modifier must outlive the key.
Some combos cannot be captured in a browser, because Windows or the browser takes them first — anything with the Win key, Alt+Tab itself, and Ctrl+W / Ctrl+T / F11. The recorder tells you when it saw something it could not read; use the Pick button on that row to set the combo by hand — modifier tick-boxes plus a key list, writing exactly what a recording would.
Four directions, distinguished by which half of the pad the swipe starts on and whether you use one or two fingers. A swipe has no release, so it always fires a single pulse — tap-versus-hold does not apply.
Matching is exact: a macro recorded as swipe right, left half only fires on a swipe that starts on the left half.
| Where it lives | Scope | |
|---|---|---|
| The macro definitions | Their own area of the dongle’s flash | Shared by every profile |
| Which macros are enabled | The profile itself | Per-slot |
So you define a macro once and then choose, per game, whether it is live. Save a slot and it captures the current set of ticked boxes; the Playnite automation then switches macro sets per game with no extra setup.
Press Save macros to device to store both the definitions and the enable state. The panel warns you when the enable state has changed but not yet been saved.
Enabling the first macro re-enumerates the controller. Macros share the keyboard interface with the wake feature, so switching from “no macros” to “at least one” changes the USB descriptor and the device briefly disconnects and reappears. Switching which macros are on costs nothing, and if Enable Wake is already on the interface is present anyway, so there is no reconnect at all. This is the same constraint as wake: a game with native DualSense support may stop recognising the controller while the keyboard interface is present.
A third way to trigger a macro: hold a button and flick your wrists with the controller. Hold L2, flick down then up, release — and the macro fires.
The held button is a gate, not a modifier. It marks when you are “drawing”, so the dongle is not watching your wrist the whole time you play. Recording starts when the gate goes down (button is pressed) and the gesture is matched when you let go.
| Strokes per gesture | 1–4 (up to 8 are stored) |
| Directions | up, down, left, right |
| Gate | any button or combination, held while you flick |
| Fires | on release of the gate |
While a gate is held, gyro aiming is suspended — the same wrist movement that makes the gesture would otherwise swing your aim. It resumes the moment you let go.
Recording calibrates to you. Press Record motion, hold the gate, flick the controller, release. The portal measures how far you actually moved and stores a step size with that macro, so a small flick and a broad sweep are both recognised as what you performed. A fixed threshold chosen without your hardware in front of it does not survive contact with a real wrist.
Two behaviours worth knowing, both learned the hard way:
Gestures are macros like any other, so they carry names, live in the same 32 rows, and are enabled per profile.
Definitions and enablement travel separately, because they are stored separately.
| Action | Carries | Asks |
|---|---|---|
| Export macros / Import macros (Macros tab) | the definitions | import confirms — it replaces all of them |
| Export Profile / Import Profile | which macros are enabled | import confirms, and names what would fire |
| Export HTML and the Playnite auto-apply page | nothing about macros | n/a |
| Back up all slots / Restore | both | asks about definitions, both ways |
Import macros replaces every macro on the dongle, for all profiles. It loads into the editor rather than writing straight to the device, so nothing is committed until you press Save macros to device.
Importing a profile asks before changing your macro selection, and lists what that selection would switch on. It has to: a profile carries only the enable mask, so it turns on your macros in those rows — which may be nothing like what the profile’s author had there. Decline and your current selection is left alone while every other setting still imports.
The auto-apply page carries no macro information at all. Two reasons, either
sufficient. It runs unattended on every game launch, so there is nobody to answer
that question — and enabling the first macro re-enumerates the controller, which
is the same mid-apply interruption hazard that makes wake unsuitable for a
field-by-field .html profile. Per-game macro sets belong on the slot path, where
the whole configuration is applied in one command.
R3 and
R3 + D-pad Up enabled, pressing R3 first fires the R3 macro and the longer
combo never fires — at the moment R3 goes down the firmware cannot know whether
a second button is coming. The portal warns you when it spots this.| Setting | Range | Default | Notes |
|---|---|---|---|
| Controller Type | DualSense (DS5) / DualSense Edge (DSE) / Auto-detect | Auto-detect | Which pad identity the dongle presents to the PC. Auto-detect follows the controller actually paired; force one if a game only recognises a specific model. Changing this re-enumerates the device. |
| Polling Rate | 250 / 500 / Real-time | Real-time | USB report rate |
| Audio Buffer Length | 16–128 | 64 | Lower = snappier haptics/lower latency; higher = more audio stability |
| Inactive Time (min) | 5–60 | 30 | Idle timeout before disconnect |
| Disable Inactive Disconnect | on/off | off | Never auto-disconnect when idle |
| Disable Pico LED | on/off | off | Turn off the Pico’s onboard LED |
| Wake PC on PS Button | on/off | off | Assert USB remote wakeup on PS press to wake the host |
| Setting | Default | Notes |
|---|---|---|
| BT Flush Timeout | Off | Drop stale packets instead of retransmitting. No clear benefit on a strong link; left in for tinkering. |
| BT QoS Latency | Off | Request a tighter poll interval. Inconclusive in testing; left in for tinkering. |
python ds5audio.py manually — use pythonw to
avoid one) is enough to trip it, which makes the crash look automation-related
when the real fragility is the display mode. Diagnostic: launch the game bare
and alt-tab — if it crashes the same way, it’s the game. Fix: set the game to
Borderless/Windowed — no exclusive mode switches, focus changes become
harmless, and the automation works unmodified (borderless has effectively no
performance cost on modern Windows).flash_nuke.uf2 if the config appears corrupted or a
release explicitly calls for it. After a layout change it’s still worth opening
the portal to check any new settings and re-saving slots that use them.joy.cpl and under Settings → Bluetooth & devices
→ Devices, has its own Allow this device to wake the computer setting, and can be
hidden with HidHide without affecting waking. With wake off, the bridge leaves
the bus entirely when the controller disconnects, as before.Requires the Pico SDK (2.x) with the Pico 2W board support.
Important: pin TinyUSB to 0.20.0 inside the SDK (
cd pico-sdk/lib/tinyusb && git checkout 0.20.0). The SDK’s bundled 0.18.0 fails to build the audio interface (TUD_AUDIO_EP_SIZE). Also rungit submodule update --init --recursivesolib/WDLandlib/opusare present.
git clone https://github.com/awalol/DS5Dongle.git
cd DS5Dongle
git checkout v0.7.0
# apply the changes:
git apply /path/to/ds5dongle-v1.0.9.patch
# or copy the files from src/ over the originals
mkdir build && cd build
cmake .. -DPICO_SDK_PATH=/path/to/pico-sdk -DPICO_BOARD=pico2_w
make -j
The resulting ds5-bridge.uf2 is the firmware.
src/audio.cpp — auto-haptics DSP (channel detection, filter cascade, envelope,
noise gate, carrier modulation, converted-rumble blend, transient effect leak,
speaker mute)src/state_mgr.cpp — DS4Windows rumble-mode fix, rumble value capture,
lightbar-off-in-Replace, state_set in RAMsrc/bt.cpp — BT flush timeout / QoS controls, RSSI signal strength readout, clean controller disconnect when the host is awake (clears DS4Windows even with wake on), suspend-aware connect/disconnectsrc/config.h / src/config.cpp — config fields, validation, defaultssrc/cmd.cpp — config field-ID read/write handlers, diagnostics, reboot-to-bootloadersrc/main.cpp / src/state_mgr.h — stuck-rumble fix (send state to the controller when it changes even while the speaker is active)src/wake.cpp / src/wake.h — USB suspend/wake hardening: debounced controller power-off (ride out hub-induced suspends), and a grace window so a deliberate USB reconnect is not treated as a host sleep (ported from upstream PR #186)src/usb.cpp — suspend-callback gate so the controller’s Bluetooth is left alone on a USB suspend when wake is offsrc/macro.cpp / src/macro.h — macro engine: flash-backed table, button presses and
gesture matching, ordered keyboard playback (new in 1.19.0)src/input_buttons.h — logical button decode shared by the macro engine and the
portal; expands the D-pad hat into discrete direction bits (new in 1.19.0)src/flash_map.h — one place defining every flash region, with static_asserts
reserving room for slot growth above the macro table (new in 1.19.0)CMakeLists.txt — adds src/macro.cpp to the buildThis is a derivative of awalol/DS5Dongle, which in turn builds on earlier community work on the DualSense dongle concept.
Auto-haptics origin — thanks to @loteran. The audio-derived auto-haptics in this
project were inspired by @loteran’s earlier auto-haptics experiments on the
DS5Dongle. The single most important insight came from loteran: relocating
state_set to RAM, which is what allows the haptic actuators to fire at stock clock
speeds (150 MHz) instead of requiring an overclock. The DSP and supporting code here
were rewritten from scratch (carrier modulation, channel detection, the noise gate,
the DS4Windows handling, the effect leak, and the config protocol are new), but
loteran’s groundwork is what made the whole feature possible. This release would not
exist without it — thank you.
Thanks also to the broader DS5Dongle contributors and to awalol for the complete RAM relocation in v0.7.0 that keeps native haptics and controller audio working without overclocking, and for the wake-on-PS-button implementation. (@awalol)
Upstream fixes incorporated. The stuck-rumble fix is based on mik9’s (GitHub handle to confirm) upstream “Fix stuck rumble” commit. The USB suspend/wake hardening (debounced power-off to ride out hub-induced suspends, plus the reconnect grace window) is based on @up2urheadlights‘ upstream pull request #186. Both were adapted to the v0.7.0 base used here. Thank you.
Licensed under the MIT License — see LICENSE. The original awalol copyright notice is preserved as required.
Watch releases, not commits. Development happens as a steady stream of commits;
those are work in progress and aren’t meant to be flashed. Every tested build is
published as a release with its own .uf2, portal and changelog entry. On
GitHub, Watch → Custom → Releases notifies you about those and nothing else.
Updating is drag-and-drop. Hold BOOTSEL while plugging the board in (or
triple-click BOOTSEL on a running one) and copy the new .uf2 to the RPI-RP2
drive. Take the portal from the same release — the portal and firmware are released
together and are tested as a pair.
Your settings survive. Configuration and all saved profile slots carry across an
upgrade: new options are appended to the stored layout, so existing values keep
their meaning and anything new starts at its default. There is nothing to back up,
nothing to re-enter, and no need for flash_nuke.uf2 unless a release note says so.
How often is up to you. There’s no auto-update and nothing nags you. If your setup works, staying on it is perfectly fine — upgrade when a release mentions a fix or feature you actually want. Release notes say plainly when a change is firmware-only, portal-only or documentation-only, so you can skip the ones that don’t affect you.
ds5-v1.22.0.uf2 — the firmware for the Raspberry Pi Pico 2 W (flash this;
reports version 1.22.0)ds5-v1.22.0-waveshare.uf2 — the same firmware for the Waveshare
RP2350B-Plus-W (built against pico-sdk 2.2.0)ds5-config-portal.html — the web configuration portal (download and open)flash_nuke.uf2 — config-reset utility. Not needed for a normal upgrade —
only when a release note says so, or to recover from clearly corrupted settings.
Erases all settings and every profile slot.src/ — the modified source filesLICENSE — MIT licenseREADME.md — this fileCHANGELOG.md — version historytools/ — one-command builders for Windows and macOS, plus the portal test
harness and the host-side macro engine testsboards/ — board support, including the Waveshare RP2350B-Plus-W build scriptautomation/ — optional Playnite integration (see below)The automation/ folder adds hands-off Playnite integration: it auto-applies a
profile per game and routes game audio to the dongle for audio-driven auto-haptics,
switching automatically between native-haptics games and everything else.
Need per-game configs to switch between? The Shared game profiles page collects ready-to-import profiles — import one into a slot and the automation can activate it per game.
Quick start:
native-games.txt it applies the native profile and starts
no audio capture, so a native-only setup needs no Python — slot switching,
wake and per-game profiles all still work.
pip install PyAudioWPatch.
numpy is optional — it gives cleaner resampling, and is only required
by the separate ds5audio_downmix.py helper.automation\ds5-setup.bat — it detects its own folder and generates the
Playnite scripts with correct paths, then prints the exact lines to paste into
Playnite’s script settings.automation\ds5-policy.bat once (self-elevates). It pre-grants the dongle
to the profile pages via browser policy, so automated applies never wait for a
Connect click and the grant survives browser restarts. Fully reversible with
ds5-policy-remove.bat (Edge/Chrome show “Managed by your organization” while
the policy is installed).ds5-automation.log (each launch logs game: '...') into
native-games.txt — no need to type them from memory.
Per-game custom profiles are supported too: save a profile into a dongle
slot in the portal and add a game = slot 3 rule to profile-overrides.txt
(or the file-based form, game = file.html, for exported profiles).automation\AUTOMATION-README.md for the full walkthrough.This is entirely optional — the firmware and config portal work on their own without it. The automation just removes the manual steps if you use Playnite.
Terms used throughout this document and in the portal.
Actuator — the voice coil in each grip of a DualSense. It replaces the rumble motors of older pads and can reproduce audio-rate signals, which is why haptics on this controller are delivered as sound rather than as motor speeds.
Derived (auto-)haptics — haptic feedback this firmware generates from game audio, for games that have none of their own.
Native haptics — haptic feedback a game renders itself and sends to the controller. Nothing is generated in that case; the firmware only passes it through.
Envelope — how loud a signal is over time, ignoring the waveform itself. The bass envelope of the game audio is what drives the derived haptics.
Carrier — a steady tone the envelope is applied to. The actuators cannot render very low frequencies, so “how much bass” is turned into felt motion by modulating a 90 Hz tone rather than sending the bass itself.
Noise gate — a threshold below which the derived haptics stay silent, so room tone, dialogue and quiet music do not buzz continuously.
LP cutoff (low-pass) — the frequency above which content is discarded when looking for bass. Lower = only deep rumble drives the haptics.
Crossover / split — dividing the audio into a low band and a high band so each grip can be driven by a different part of the spectrum.
Effect leak — with the speaker muted, letting sharp one-off sounds (shots, impacts, clinks) through to it anyway, while sustained dialogue and music stay muted.
Transient / onset — a sudden jump in level, i.e. the start of a sound. What the effect leak detects.
Converted rumble — a game’s rumble instructions re-created on the actuators as vibration, used when the actuators are busy with haptics and cannot be handed to the controller’s own rumble emulation.
Motor values — the two numbers a game sends to request rumble. Left is the heavy/low-frequency motor, right the light/high-frequency one, a convention inherited from pads that really had two different motors.
ch0/1 and ch2/3 — the four audio channels the dongle receives. ch0/1 feed the controller speaker and are what the derived haptics are generated from by default; ch2/3 carry native haptics from a game that sends them.
Native passthrough — how much of ch2/3 is passed to the actuators in Mix mode.
Loopback capture — recording what the PC is playing (rather than a
microphone), which is how ds5audio.py gets game audio to send to the dongle.
The DualSense triggers contain a motor and a clutch that can push back against your finger at chosen points in the pull. The vocabulary below describes what that mechanism is doing.
Zone — a position along the trigger pull, numbered 0 (released) to 9 (fully pressed). Every trigger effect is defined by the zones it acts on.
Resistance — constant opposing force across a range of zones. Feels like the trigger is stiffer, or like pulling through treacle.
Wall — a point in the pull where the trigger suddenly resists much harder, so it feels like it has stopped. It may be a hard stop or something you can push past with more force.
Weapon break — a wall you do push through, the way a real trigger breaks when the sear releases: resistance builds, then gives way suddenly. Defined by where it starts, where it gives, and how hard it holds.
Bow — tension that rises the further you pull, then snaps back when released, like drawing and loosing a bowstring. Defined by a start and end zone and two force levels.
Vibration (trigger) — a buzz felt in the trigger itself over a range of zones, at a chosen frequency and strength. Used for engines, chainsaws, automatic fire.
Push-back kick (Stage 2) — a short vibration burst fired while resistance is engaged, so each shot knocks the trigger back against your finger before resistance resumes. Its “bow-snap” variant uses the bow mechanism for a more mechanical snap instead of a thump.
R2T (Trigger-to-Rumble) — rumble generated from how far you pull a trigger, rather than sent by the game.
Force Override — take control of the triggers even while a game is sending its own effects, instead of yielding to the game.
Custom captured effect — a real effect recorded from a game that has one, stored as the exact bytes the game sent and replayed in a game that has none.
Timeline capture — recording not just an effect but its rhythm, so a sequence of vibrations replays with the original timing.
Enumeration / re-enumeration — the process where the PC discovers a USB device and reads what it is. Settings that change the device’s description (wake, polling rate, audio buffer, mic/speaker) require the dongle to detach and re-attach so the PC re-reads it, which briefly disconnects the controller.
Idle identity — the separate USB identity the dongle presents while the controller is switched off and wake is enabled: DS5Dongle (controller off), with its own vendor and product IDs, so nothing appears to the PC as a controller that is not connected.
Remote wakeup — the USB mechanism a device uses to wake a sleeping PC. It needs the device to still be attached, and needs permission in Windows Device Manager.
Keyboard interface — a second HID device the dongle presents to the PC alongside the gamepad, used to send keystrokes. It is shared by Wake, the PS button shortcut and Macros: it appears when any one of them is enabled and disappears when none is. That appearing and disappearing is what re-enumerates the controller, which is why enabling your first macro briefly disconnects the pad but enabling a second one does not.
Profile slot — a complete configuration stored on the dongle itself, so switching setups is one instant command rather than writing every setting.
Passthrough mode (DS4Windows) — DS4Windows forwarding a game’s rumble to the real controller rather than emulating a different pad.
Macro — a controller input bound to a keyboard combination. The dongle sends the keystrokes itself over its keyboard interface, so nothing runs on the PC and it works in any application, not only games.
Combo — two or more buttons held together and treated as one trigger, such as
R3 + D-pad Up. A combo only fires once every one of its buttons is down, so
holding the first one on its own does nothing however long you hold it.
Modifier — Ctrl, Shift, Alt or the Windows key. Macros store modifiers in the same list as ordinary keys rather than in a separate field, which is what allows the order they are pressed and released to be recorded exactly as you performed it.
Release order — which key of a combo lifts first. It matters more than it sounds: Alt+Tab expects Tab to be released while Alt is still held. Recording captures the real order; the Pick button assumes the usual one, last pressed released first.
Tap / long press — whether a macro fires the moment the buttons go down, or only after they have been held past a threshold. You set this by how long you hold the buttons while recording rather than by typing a number.
Swipe — a touchpad gesture used as a macro trigger. Identified by its direction, which half of the pad it starts on, and whether one or two fingers are used, so a swipe recorded on the left half only fires when it starts there.
Macro table — the set of macro definitions, stored once on the dongle and shared by every profile slot. Only which macros are enabled is stored per slot, which is what lets one definition be live in one game and dormant in another.