Compare commits

..
Author SHA1 Message Date
MarcelineVPQandClaude Opus 4.8 be3736c230 Merge native-trigger firmware as v0.6.12 — supersedes the withdrawn v0.6.11
v0.6.12 = the verified-working native-adaptive-trigger firmware (off v0.6.10)
plus the two separable good v0.6.11 features (CtrlWake, brightness persistence).
The v0.6.11 regressions are intentionally NOT carried over: the state_mgr.cpp
trigger-FFB allow-bit mirror (#11 constant haptics) and the audio.cpp resampler
retiming (#12). This merge takes the branch's tree wholesale; v0.6.11 remains in
history (and its tag) but its file changes are superseded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 18:28:31 -06:00
MarcelineVPQandClaude Opus 4.7 aef03e9633 docs(readme): document gyro-tilt rework + L3/R3 indicator (EN + CN)
Bring both READMEs up to v0.6.10's user-facing capability:
- Status screen: note the L3/R3 stick-click inverse-flash indicator and
  the ~Nm charge-time token next to the battery.
- Gyro Tilt screen: per-unit factory IMU calibration, dot centres when the
  controller lies flat, and tilt moves the dot in the matching direction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 18:28:31 -06:00
MarcelineVPQandClaude Opus 4.8 cb7fffd795 docs(linux): native adaptive triggers confirmed on Steam AND Heroic, flag-free
Verified 2026-06-07: Cyberpunk on GOG/Heroic fires adaptive triggers in
real gameplay, 1:1 with Steam. Corrects the recipe across README,
CHANGELOG, and the new DUALSENSE_GAME_LIST.md:
  - no launch option needed on Wine 11 (PROTON_ENABLE_HIDRAW now legacy)
  - works on BOTH Steam and Heroic; Heroic needs Steam fully quit + no
    global PROTON_PREFER_SDL
  - XInput-only games (Ghostrunner, Control) give rumble but no triggers
    by engine design, not a dongle limit
  - OpenRGB confirmed a non-issue

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 18:25:25 -06:00
MarcelineVPQandClaude Opus 4.8 9cabdca3f5 feat(oled): fold CtrlWake + brightness persistence onto native-trigger firmware (v0.6.12)
Cherry-picks ONLY the two separable good features from the withdrawn
v0.6.11 (commit 71cead4) — the `controller_wakes_display` (CtrlWake /
OLED-sleep-while-playing) toggle and `screen_brightness` persistence —
onto the verified-working native-trigger build.

The v0.6.11 regressions are intentionally NOT carried over:
  - 71cead4's state_mgr.cpp trigger-FFB allow-bit mirror (#11 suspect)
  - all of 84393c0's audio resampler retiming + config audio-defaults
    + chunked-SPI flush (#12 suspect)

Touches only src/config.{h,cpp} and src/oled.cpp, so the trigger and
audio code paths compile byte-identical to the tested firmware and
cannot regress. Two new Config_body fields append at the struct tail
(erased flash -> clamped to defaults by config_valid, clean upgrade).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 12:23:21 -06:00
MarcelineVPQandClaude Opus 4.8 00da3d6a18 docs(readme): flag the WebHID Config/Remap tabs as non-functional on the native-trigger firmware
Inline warnings on the Config and Remap tab descriptions (in addition to the
existing caveat block) so users on the native-trigger firmware don't try the
browser config tool and hit a silent failure. The reports those tabs use
(0xF6/0xF7) are undeclared on this firmware so WebHID can't reach them; OLED
on-device config and a Linux hidraw path still work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:53:22 -06:00
MarcelineVPQandClaude Opus 4.8 6482c79644 fix(audio): fold the crackle fix (RX buffer 3x->16x) into the native-trigger firmware
The speaker-crackle root cause -- USB-audio OUT endpoint software buffer overflow
at 3x the max packet size -- was fixed on the audio branch (de37e00) but never on
this branch. Bumping CFG_TUD_AUDIO_FUNC_1_EP_OUT_SW_BUF_SZ from 3x to 16x removes
the crackle; verified by A/B tone test (440 Hz) through the DS5 speaker. A faint
residual cadence "warble" remains (a separate root cause, not the buffer) and is
left for a later pass. Net: one firmware with BOTH native adaptive triggers and a
crackle-free speaker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:50:37 -06:00
MarcelineVPQandClaude Opus 4.8 00822d6556 docs: document native adaptive triggers in games (Linux/Proton) + the descriptor/feature-data fixes
CHANGELOG: headline + Added/Changed/Known-tradeoff for the native-trigger work.
README: a Features bullet, a "Linux: native adaptive triggers (Proton)" setup
section with every launch option in a reference table, and a caveat that the
WebHID Config/Remap tabs are disabled on this firmware. CLAUDE.md: a load-bearing
gotcha -- the HID descriptor must stay byte-identical to a real DS5 (289 bytes)
and feature reports 0x09/0x20/0x05 must serve the real cached controller data, or
native game triggers silently break.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 10:44:31 -06:00
MarcelineVPQandClaude Opus 4.8 8d811f09cc fix(hid): serve the controller's REAL 0x09/0x20/0x05 feature data to the host
The native-recognition shortcut answered 0x09 (pairing), 0x20 (firmware) and 0x05
(calibration) with zeros + a valid crc. hid_playstation accepted that (it checks
size+crc only), but games validate the actual CONTENT: Cyberpunk GET-read 0x20/0x09
~156x in a retry storm rejecting the zeros, never completed the DualSense handshake,
and showed no controller / no adaptive triggers.

init_feature() already caches the real reports from the controller over BT, so serve
those (via get_feature_data) for 0x09/0x20/0x05, keeping the crc-valid synthetic stub
only as a fallback for the USB-enumeration probe before the BT link (so the kernel
still binds then). The dongle now returns the genuine firmware string / pairing /
calibration, and native adaptive triggers work in games through the dongle, 1:1 with
a wired DualSense.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 10:36:10 -06:00
MarcelineVPQandClaude Opus 4.8 36687ce76d fix(usb): strip web-config reports 0xF6-0xF9 so the HID descriptor byte-matches a real DS5
The OLED Edition declared four extra feature reports (0xF6-0xF9, the WebHID config
IDs) in the DualSense HID descriptor, making it 321 bytes vs a genuine DS5's 289.
Games' native DualSense parser (Cyberpunk, etc.) rejects the mismatch and falls
back to generic/Xbox, so adaptive triggers never fire in a game -- the long-standing
"triggers only work in the OLED self-test" bug.

Remove the four declarations and the runtime wDescriptorLength patch that overrode
them (0x41->0x21) so the descriptor is byte-identical to a real DS5 (289 bytes). The
firmware still handles 0xF6-0xF9; they're just undeclared now and work over hidraw on
Linux exactly like 0xFD. Trade-off: the browser WebHID config tool can't reach them
(OLED + hidraw config still work).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 10:36:10 -06:00
MarcelineVPQandClaude Opus 4.8 5e5aa71cfe fix(usb): use the controller MAC as the USB serial, matching the 0x09 report
The serial was the Pico flash unique id (board_usb_get_serial), while the 0x09
pairing-info feature report returns the controller's BT MAC. The host saw two
identities for one controller (flash-id over USB, MAC over 0x09), and Wine/Steam
device-matching choked — "recognized but no controller" in-game.

Set STRID_SERIAL to the controller MAC (live bt_get_addr, falling back to the
current slot's stored addr, then the flash id if never paired) so the dongle
presents one consistent identity like a real DualSense. Steam now recognises
the dongle as a native DualSense.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 00:39:46 -06:00
MarcelineVPQandClaude Opus 4.8 d090df6820 feat(hid): answer DS5 0x09/0x20/0x05 feature reports so Linux binds hid_playstation
Linux's hid_playstation driver reads three feature reports at probe — 0x09
(pairing info / controller MAC), 0x20 (firmware info) and 0x05 (calibration),
the latter two CRC-checked. Without valid answers the kernel never creates a
gamepad, so games outside Steam Input (Heroic/Proton/native) see no controller.

Synthesize them in tud_hid_get_report_cb: 0x09 returns the BT MAC at [0..5];
0x20/0x05 fill zeros plus a valid trailing crc32 over [0xA3 seed, report_id,
data]. Confirmed: the dongle now binds hid_playstation — a real gamepad on Linux.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 00:39:46 -06:00
MarcelineVPQandClaude Opus 4.8 878a742bfc docs(changelog): cut v0.6.11 — triggers fix + CtrlWake + audio retiming
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 19:19:16 -06:00
MarcelineVPQandClaude Opus 4.8 84393c00e8 wip: park audio-crackle investigation + haptic/OLED defaults + debug tooling
Preserves in-progress work that had been sitting uncommitted in the working
tree (predates this session) so it is protected in git history. This is NOT a
verified/shipped fix — it is parked. Audio path is unverified on hardware and
still carries debug instrumentation.

- src/audio.cpp: drop the 512->480 resampler and retime the 0x36 frame to a
  true 10ms/100Hz grid (SAMPLE_SIZE 64->60, 480-sample buffer) to chase the
  ~45kHz-vs-48kHz speaker underrun theory behind the crackle; dynamic speaker
  sub-report offset; debug printf + Opus-frame-dump instrumentation.
- src/config.cpp: default audio_buffer_length 64->16; auto_haptics_enable
  default 1 (Fallback) -> 0 (Off).
- src/oled.cpp: chunked non-blocking SPI flush (issue #7 OLED-stall angle) and
  a battery-% midpoint display tweak.
- scripts/: audio debug helpers (opus-dump decode, pi audio test, sine ch1/2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 19:11:39 -06:00
MarcelineVPQandClaude Opus 4.8 71cead401d fix: adaptive triggers + opt-in OLED sleep + brightness persistence
Closes the three non-audio user issues (audio path untouched):

- #6: adaptive trigger FFB was silently dropped. state_update() copied the
  RightTriggerFFB/LeftTriggerFFB params into the outgoing state but never set
  the Allow{Right,Left}TriggerFFB apply-bits in byte 0, so the DS5 discarded
  them on BOTH the standalone 0x31 path and the 0x36 audio-frame fold, while
  direct USB worked. Mirror the host's two allow-flags in, like the rumble
  flags already were (matches what the on-device Trigger Test screen does).

- #8/#9: new CtrlWake setting (default on = unchanged behavior). Set off and
  controller input no longer keeps the OLED awake, so the dim/off timeouts run
  during gameplay and the panel can sleep while the controller is in use; only
  KEY0/KEY1 wake it.

- #9: OLED brightness (screen_brightness) now persists across a power cycle
  instead of resetting to full on every boot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 18:58:33 -06:00
MarcelineVPQandClaude Opus 4.7 8fc369b1af docs(readme): document gyro-tilt rework + L3/R3 indicator (EN + CN)
Bring both READMEs up to v0.6.10's user-facing capability:
- Status screen: note the L3/R3 stick-click inverse-flash indicator and
  the ~Nm charge-time token next to the battery.
- Gyro Tilt screen: per-unit factory IMU calibration, dot centres when the
  controller lies flat, and tilt moves the dot in the matching direction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 08:45:49 -06:00
11 changed files with 257 additions and 36 deletions
+22 -1
View File
@@ -6,7 +6,28 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
---
## [Unreleased]
## [0.6.12-oled-edition] — 2026-06-07
> **Supersedes the withdrawn v0.6.11.** v0.6.11 bundled the two OLED quality-of-life features below with two regressions (constant haptics, issue #11; an audio retiming change, issue #12) and is not recommended. v0.6.12 is built on the verified-working native-trigger firmware and folds in **only** the two separable good features — `CtrlWake` and brightness persistence — leaving the trigger and audio code paths byte-identical to the tested build. The v0.6.11 regression changes (the `state_mgr.cpp` trigger-FFB allow-bit mirror and the `audio.cpp` resampler retiming) are intentionally **not** carried over.
Headline: **native DualSense adaptive triggers now fire in real PC games on Linux/Proton — through the dongle, 1:1 with a wired controller.** For weeks the triggers only ever worked in the on-dongle OLED self-test, never in a game: the dongle was *recognised*, but games fell back to a generic/Xbox pad and never sent trigger effects. The root cause was three ways the dongle's USB presentation differed from genuine Sony hardware — each one accepted by Linux's `hid_playstation` (which only checks size + CRC) but **rejected by a game's native DualSense detection, which validates the actual content**. Closing all three makes the dongle byte-for-byte indistinguishable from a real DS5 to the game.
**Host-side recipe (required alongside this firmware):** run the game on a Proton that carries the Wine `winebus.sys` **#9034** fix (Wine 11 / current Proton-GE — the bug suppresses the SDL gamepad device when a hidraw device exists for the same VID/PID, so the pad shows up everywhere *except* in-game) and **disable Steam Input** (native path). **No launch option is needed** — Wine 11 enables the hidraw native path by default. Works on **both Steam and Heroic** (on Heroic, fully quit Steam so it can't grab the pad, and keep any global `PROTON_PREFER_SDL` off). The game must natively support DualSense — XInput-only titles give rumble but no adaptive triggers. With a real DS5 this "just works"; the firmware changes below make the dongle match.
### Added
- **Native adaptive triggers & haptics in games on Linux/Proton, through the dongle.** Games with native DualSense support (Cyberpunk 2077, etc.) now drive the controller's adaptive triggers via the dongle, identical to a directly-wired DualSense — verified 1:1 by A/B against the same controller plugged in over USB-C. No Steam Input, no per-game hacks beyond the host recipe above. The firmware already proxies the host's trigger output reports to the controller (unchanged); the missing piece was getting games to *recognise* the dongle as a genuine DS5 in the first place (below).
- **`CtrlWake` setting — let the OLED sleep while you play.** New Settings toggle (default **on**, preserving the old behaviour). With it **off**, controller input no longer keeps the panel awake — only the OLED's own KEY0/KEY1 do — so the auto-dim / auto-off timers actually count down during gameplay and the screen can sleep while the controller is in active use. (folded from v0.6.11; issues #8/#9)
- **OLED brightness now persists across a power cycle.** The KEY1-long-press brightness choice is saved to config and restored on boot, instead of resetting to full every power-on. (folded from v0.6.11; issue #9)
### Changed
- **The DualSense HID report descriptor is now byte-identical to a real DS5 (289 bytes, was 321).** The OLED Edition declared four extra feature reports — `0xF6``0xF9`, the WebHID config/remap IDs — inside the gamepad collection, making the descriptor 32 bytes longer than genuine hardware. Games' native DualSense parser rejects that mismatch and falls back to a generic pad (no triggers); `hid_playstation` didn't care, which is why the OLED self-test always worked but games never did. The four declarations are removed (plus the runtime `wDescriptorLength` patch that overrode the static array, `0x41``0x21`). The firmware **still handles** `0xF6``0xF9`; they're simply undeclared now and work over Linux `hidraw` exactly like the existing `0xFD` diagnostic report.
- **Feature reports `0x09` (pairing), `0x20` (firmware) and `0x05` (calibration) now return the controller's real cached data, not zeros.** The earlier native-recognition fix answered these with zeros + a valid CRC — enough for `hid_playstation` to bind, but games validate the *content*: Cyberpunk GET-read `0x20`/`0x09` ~156× in a retry storm rejecting the zeros, never completed the DualSense handshake, and showed no controller. The dongle already fetches and caches the genuine reports from the connected controller over BT (`init_feature()`); `tud_hid_get_report_cb` now serves that cache (real firmware string / pairing / calibration), keeping the CRC-valid synthetic stub only as a fallback for the USB-enumeration probe before the BT link is up (so `hid_playstation` still binds then). Builds on the `hid_playstation` binding fix and the serial-=-MAC identity fix.
### Known tradeoff
- **The browser (WebHID) Config and Remap tabs no longer reach the dongle.** They drove config over reports `0xF6`/`0xF7`, which are no longer *declared* in the HID descriptor — and WebHID refuses undeclared report IDs (declaring them is exactly what broke game triggers). Mitigations, no firmware change needed: **on-dongle OLED config still works**, and because undeclared IDs are fine over Linux `hidraw`, a small `hidraw` CLI/script can read & write all of `0xF6``0xF9` (the same mechanism `scripts/mic_diag.sh` uses for `0xFD`). Restoring the *browser* tool would require moving the custom reports into a separate vendor HID collection — which risks the descriptor diverging from a real DS5 again, re-breaking triggers, so it's deliberately not done here.
---
+2
View File
@@ -97,6 +97,8 @@ These are non-obvious from the code; they cost time when forgotten.
- **Inactivity-disconnect uses `packet[3..12]`** in the L2CAP interrupt data path (`src/bt.cpp:l2cap_packet_handler`). It's looking at sticks and DPad/buttons to decide "idle." Don't touch those bytes' layout without updating the heuristic.
- **The 0x36 BT audio packet layout is load-bearing — speaker + HD haptic actuators silently die without the SetStateData sub-report.** Upstream commit `3a31bd7` (May 2026, "refactor: add SetStateData and audio send priority") moved the `0x10` SetStateData block out of every audio frame and into a one-time L2CAP-open setup. The DualSense hardware requires that sub-report (specifically the `0x7f 0x7f` Headphones + Speaker volume bytes) at `pkt[11..75]` of every `0x36` frame, or the actuators stop producing output even though USB and BT byte counts look fine. Our fork keeps `state_data[63]` in `src/audio.cpp` and re-asserts it on every frame; the pre-3a31bd7 packet layout is: state_data at `pkt[11..75]`, haptic at `pkt[76..141]`, speaker format at `pkt[142]`, opus payload at `pkt[144..343]`. If you rebase onto an upstream that has the refactor and don't preserve this restoration, speaker + HD haptics silently break. The `scripts/test_speaker.sh` helper + the `USB aud / BT 0x32` counters on the OLED Diagnostics screen are the regression tripwire — if bytes are flowing but you hear nothing, look at packet contents not flow. Upstream PR #93 is tracking a proper unified fix; ours is the pre-refactor revert applied just to `audio.cpp`. Same fix was shipped independently by `loteran/DS5Dongle` (commit `c7a8d3c`).
- **The DualSense HID descriptor must stay byte-identical to a real DS5 (289 bytes), and `0x09`/`0x20`/`0x05` feature reports must return the controller's *real* cached data — or native game triggers silently break.** A game's native DualSense detection (Cyberpunk, etc.) validates *both* the descriptor shape and the feature-report *content*, then drives adaptive triggers over the native hidraw path. If either differs from genuine hardware the game falls back to a generic/Xbox pad and triggers never fire. Linux's `hid_playstation` is far more lenient (size + CRC only), so the dongle still *binds* and the on-dongle OLED trigger-test still works — which masked this break for weeks. Two load-bearing things: (1) **don't re-declare config reports `0xF6``0xF9`** in `desc_hid_report_ds` (or bump its `wDescriptorLength` runtime patch in `tud_descriptor_configuration_cb` back to `0x41`) — that's what made the descriptor 321 bytes and killed triggers; (2) keep `tud_hid_get_report_cb` serving the **real `init_feature()` cache** for `0x09`/`0x20`/`0x05`, with the CRC-valid synthetic stub only as the pre-BT-link probe fallback. `desc_hid_report_dse` (DSE mode) still declares F6-F9 and needs the same cut. Host side requires a Proton with the Wine `winebus.sys` #9034 fix + `PROTON_ENABLE_HIDRAW=0x054c/0x0ce6` + Steam Input off; diff a dongle vs real-DS5 `PROTON_LOG=+hid` capture to debug (a GET retry storm on `0x20`/`0x09` = the dongle's feature content is being rejected).
## Versioning — single source of truth
The release tag is the **only** place the version is written. Everything else flows from it:
+58
View File
@@ -0,0 +1,58 @@
# DualSense adaptive-trigger games — owned + tested through the dongle
Cross-referenced across Steam + Epic + GOG (~1,271 owned games). Native adaptive triggers work
on **both Steam and Heroic**, flag-free, with **runner = GE-Proton-DualSense (Wine 11)**. Verified
2026-06-07 by running Cyberpunk on *both* launchers — identical trigger behaviour.
- **Steam:** GE-Proton-DualSense + **Steam Input off**. No launch option needed (Wine 11 enables
the hidraw native path by default).
- **Heroic (Epic / GOG):** GE-Proton-DualSense + **Steam fully quit** + **no `PROTON_PREFER_SDL`**.
No launch option needed. (Earlier belief that Heroic needs `PROTON_ENABLE_HIDRAW` was wrong —
it was masked by the two gotchas below.)
### ⚠️ Linux gotchas that masquerade as "the dongle is broken" (all host-side)
- **Steam running in the background** grabs the pad from non-Steam (Heroic) games. Fully quit Steam,
or disable Settings → Controller → PlayStation controller support.
- **A global `PROTON_PREFER_SDL=1`** forces the SDL/Xbox path and *suppresses* native triggers. Keep
it off (or per-game only). It's how you get a generic Xbox pad when a game lacks native DualSense.
- **XInput-only games can't do adaptive triggers — period.** XInput has no trigger-resistance API, so
titles that read the pad via XInput (e.g. Ghostrunner, Control on PC) give rumble only, on any OS,
through any tool. Not a dongle limit.
Legend: ✅ = confirmed working **flag-free** through the dongle (this session, 2026-06-07).
## 🎯 Full adaptive triggers
| Game | Where you own it | Status |
|---|---|---|
| Cyberpunk 2077 | Steam · GOG | ✅ tested flag-free — **confirmed on BOTH Steam and Heroic/GOG** |
| The Last of Us Part I | Steam | ✅ tested flag-free |
| Marvel's Spider-Man Remastered | Steam | ✅ tested flag-free |
| Uncharted: Legacy of Thieves Collection | Steam | ✅ tested flag-free |
| Ghost of Tsushima Director's Cut | Steam | recognized — bow draw 🏹 (earlier) |
| Hogwarts Legacy | Steam · Epic | ✅ tested flag-free (native recog + rumble; triggers moderate, in spellcasting) |
| Ghostrunner | Epic · GOG | ⚠️ XInput-only on PC — rumble, **no adaptive triggers** (not the dongle) |
| Avatar: Frontiers of Pandora | Steam | ✅ tested flag-free |
| Assassin's Creed Shadows | Steam | |
| Indiana Jones and the Great Circle | Steam | ✅ tested flag-free |
| Star Wars Jedi: Fallen Order | Steam | |
| LEGO Star Wars: The Skywalker Saga | Steam | |
| Metro Exodus (Enhanced) | Steam | |
| Marvel's Guardians of the Galaxy | Epic | |
| F.I.S.T.: Forged In Shadow Torch | Epic | |
| Dakar Desert Rally | Epic | racing-trigger feel |
## 〰️ Lighter / haptics-leaning (subtle trigger use)
- Baldur's Gate 3 — Steam — ✅ great experience; haptics-forward, light trigger use (dice rolls / ranged)
- Sifu — Epic — haptics only; combat is O/Triangle face-buttons, **no adaptive triggers** (not a trigger test)
- The Witcher 3: Wild Hunt (next-gen) — Steam · GOG
- Control Ultimate Edition — GOG — XInput-only on PC (rumble, no adaptive triggers)
- Forza Horizon 5 — Steam
- Hellblade: Senua's Sacrifice — Steam
- Star Wars: Squadrons — Epic
- Maneater — Epic
**Caveats:** matched the *well-known* trigger titles across a 600+ game library — there may be a
few more not flagged. ~18 games with real triggers, all owned, all working through the dongle.
+2 -2
View File
@@ -264,7 +264,7 @@ scripts/mic_diag.sh bt-trace
#### 1. 状态
连接状态、已配对 DualSense 的蓝牙地址、带条形的电量百分比(`+` 充电中 / `*` 已充满 / `!` 错误)、实时摇杆位置、方向键、面板按键(△ ◯ ✕ □)、L1/R1,以及 L2/R2 模拟扳机填充条。链路指示与电量使用小像素图标。
连接状态、已配对 DualSense 的蓝牙地址、带条形的电量百分比(`+` 充电中 / `*` 已充满 / `!` 错误)、实时摇杆位置(按下 **L3 / R3** 时,对应的摇杆框会反色闪烁 —— 白底黑点 —— 直到松开)、方向键、面板按键(△ ◯ ✕ □)、L1/R1,以及 L2/R2 模拟扳机填充条。链路指示与电量使用小像素图标。充电时,电池旁会显示到 100% 的预计时间 `~Nm`。
<img src="./assets/oled/oled_sc01.jpg" alt="Status screen on the OLED" width="420">
@@ -299,7 +299,7 @@ scripts/mic_diag.sh bt-trace
#### 5. 陀螺仪倾斜
实时 X/Y/Z 加速度计数值,配 40×40 十字准线框。倾斜手柄,点会实时跟随。
实时 X/Y/Z 加速度计数值,配 40×40 十字准线框。点会实时跟随手柄的倾斜,并在手柄**平放时居中** —— 它使用手柄自带的逐台出厂 IMU 校准(从特性报告 `0x05` 解析),因此每个手柄的静止位置与增益都准确。向左/右、向前/后倾斜时,点会朝相应方向移动
<img src="./assets/oled/oled_sc05.jpg" alt="Gyro Tilt screen on the OLED" width="420">
+45 -4
View File
@@ -17,12 +17,14 @@ The web tool is a one-stop shop — **no installs, no command line, no `picotool
- **Flash Firmware tab** — put the Pico in **BOOTSEL mode**, then click *Connect to Pico* in the browser and *Flash now*. The site bundles the latest release UF2, or you can load a local `.uf2` you've built yourself. Powered by WebUSB.
> **What is BOOTSEL mode?** It's the Pico's built-in flashing mode. To enter it: press and hold the small white button labeled **BOOTSEL** on the Pico, *then* plug the USB cable in (or, if it's already plugged in, briefly disconnect and reconnect while holding BOOTSEL). The Pico will appear to your computer as a removable drive — that's how you know it's in BOOTSEL mode. After the web tool flashes the firmware, the Pico auto-reboots into normal mode and is ready to use.
- **Config tab** — once the dongle is flashed and reconnected, edit haptics gain, speaker volume, polling rate, audio auto-haptics mode, and the rest of the persistent settings; save to the dongle's flash with one click. Powered by WebHID.
- **Remap tab** — visual button remapper: click a button on a live DualSense diagram to reassign it to any other (the shoulders/triggers float to the corners as labeled glyphs). The map is stored on the dongle and applied before the host sees the report, so it works in every game and on every OS. Powered by WebHID.
- **Config tab** — once the dongle is flashed and reconnected, edit haptics gain, speaker volume, polling rate, audio auto-haptics mode, and the rest of the persistent settings; save to the dongle's flash with one click. Powered by WebHID. **⚠️ Does not work on the native-trigger firmware** — see the note below.
- **Remap tab** — visual button remapper: click a button on a live DualSense diagram to reassign it to any other (the shoulders/triggers float to the corners as labeled glyphs). The map is stored on the dongle and applied before the host sees the report, so it works in every game and on every OS. Powered by WebHID. **⚠️ Does not work on the native-trigger firmware** — see the note below.
- **OLED Preview tab** — pixel-perfect emulation of all 11 OLED screens. Use the in-page KEY0/KEY1 buttons (or the controller's △ / R1 / D-pad when a DualSense is paired) to navigate. Adaptive triggers actually fire on the controller when you cycle the Trigger Test preset.
Works in any Chromium-based browser (Chrome, Edge, Brave, Opera). Firefox + Safari don't expose WebHID or WebUSB, so flashing and live config aren't available there — the OLED Preview still renders with mock data.
> **⚠️ Native-trigger firmware (Unreleased) and the WebHID tabs.** To make games on Linux/Proton recognise the dongle as a genuine DualSense and fire **native adaptive triggers** (see [CHANGELOG.md](./CHANGELOG.md)), the firmware's HID descriptor is now byte-identical to a real DS5 — which meant *un-declaring* the `0xF6`/`0xF7` config reports the browser **Config** and **Remap** tabs depend on. On that firmware those two tabs can't reach the dongle (WebHID refuses undeclared report IDs). **Flash Firmware and OLED Preview still work**, on-dongle OLED config still works, and the same settings can be driven over Linux `hidraw`. Earlier releases are unaffected.
> Source for the web tool: **[MarcelineVPQ/DS5Dongle-OLED-Config-Web](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Config-Web)** (fork of [awalol/ds5dongle-config-web](https://github.com/awalol/ds5dongle-config-web)).
---
@@ -43,6 +45,7 @@ This project enables the Raspberry Pi Pico2W to function as a Bluetooth bridge f
**OLED Edition additions:**
- **Native DualSense adaptive triggers in PC games on Linux/Proton — through the dongle.** Games with native DualSense support drive the controller's adaptive triggers wirelessly via the dongle, **1:1 with a directly-wired DualSense**. Requires a Proton carrying the Wine `winebus.sys` #9034 fix (Wine 11 / current Proton-GE) with Steam Input disabled — **no launch option needed** (Wine 11 enables the hidraw native path by default). Works on **both Steam and Heroic** (Epic/GOG). The firmware makes the dongle byte-for-byte indistinguishable from a real DS5 so the game accepts it (full write-up in [CHANGELOG.md](./CHANGELOG.md)). Trade-off: the browser Config/Remap tabs are disabled on this firmware (see the note above).
- Optional Pico-OLED-1.3 status display with **11 screens** (status, slots, lightbar, trigger test, gyro tilt, touchpad, diagnostics, CPU/clock, RSSI, VU meters, settings)
- **Button remapping** — reassign any of the 16 digital controls (face buttons, D-pad, shoulders/triggers, stick clicks, Create/Options) to any other. Stored on the dongle and applied before the host sees the report, so it works in **every game and OS with no host-side software**; identity (no remap) is the default. Edit it visually in the [web config tool](#-web-config-tool)'s **Remap tab**, or headlessly via `scripts/remap_test.py`. Persisted in its own flash sector, survives reboot.
- **4-slot persistent multi-controller pairing** — bond up to four DualSenses, switch between them from the OLED, slot 0 reconnects automatically on boot
@@ -52,6 +55,44 @@ This project enables the Raspberry Pi Pico2W to function as a Bluetooth bridge f
- **Soft-reboot** without unplugging USB via DS5 `PS + Mute` hold (works headless) or **KEY0 + KEY1 held together for 1 s** on the OLED add-on (replaces the older KEY0 double-click gesture, which was easy to fire by accident while paging quickly)
- **Audit pass on the core bridge** — critical stack-overflow fix in the audio path (resolves long-standing "audio stuttering"), security hardening, watchdog, length validation across HID/L2CAP boundaries (see [CHANGELOG.md](./CHANGELOG.md))
## 🐧 Linux: native adaptive triggers (Proton)
Getting a game to drive the controller's **adaptive triggers through the dongle** on Linux needs a Proton that carries the Wine `winebus.sys` **#9034** fix. **As of this writing no official Proton-GE release includes it yet** — it's merged upstream but unreleased, and `GE-Proton10-34` (the current release) and earlier do **not** have it. So a build that does is provided: **`GE-Proton-DualSense`** (see [Releases](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases)). Once an official GE-Proton ships the fix, that will work too.
> Why: the #9034 bug suppresses the SDL gamepad device when a hidraw device exists for the same VID/PID, so the pad is seen *everywhere except in-game*. The fix lets the native (trigger-capable) path and a working gamepad coexist.
**One-time setup**
1. **Install the Proton build.** Download `GE-Proton-DualSense.tar.gz` and extract it to:
- **Steam:** `~/.steam/root/compatibilitytools.d/`
- **Heroic:** `~/.config/heroic/tools/proton/`
Then **fully restart Steam** so it detects the new compatibility tool.
2. **Disable Steam Input** so the game talks to the controller natively — Steam → game **Properties → Controller → Disable Steam Input** (or globally: Settings → Controller → turn off PlayStation Controller support).
**Per game**
3. **Force the Proton:** Steam → right-click the game → **Properties → Compatibility → Force the use of a specific Steam Play compatibility tool → `GE-Proton-DualSense`**. (Heroic: set the game's *Wine version* to it.)
**No launch option is needed** — Wine 11 enables the hidraw native path by default, so the dongle is handed to the game automatically. Just launch — native adaptive triggers fire through the dongle, 1:1 with a wired DualSense (verified flag-free on Cyberpunk 2077, Uncharted, Spider-Man Remastered, The Last of Us Part I, Avatar, Indiana Jones — and on **both Steam and Heroic**). The game must have **native DualSense support** (XInput-only games give rumble but no adaptive triggers — that's an engine limit, not the dongle). First launch under a new Proton does locale/prefix setup and can sit "Not Responding" for a minute — that's setup, not a crash; let it finish.
### Heroic (Epic / GOG) — same recipe, two extra requirements
Native triggers work identically on Heroic, but non-Steam launchers are sensitive to two host-side things that silently steal the controller:
1. **Fully quit Steam** — a background Steam grabs the DualSense from non-Steam games (or disable Settings → Controller → PlayStation controller support).
2. **No global `PROTON_PREFER_SDL`** — that env var forces the SDL/Xbox path and *suppresses* native triggers. Use it per-game only, as a deliberate generic-pad fallback.
**Launch-option reference**
| Variable | Value | Purpose |
|---|---|---|
| `PROTON_ENABLE_HIDRAW` | `0x054c/0x0ce6` | *Legacy / optional — **not needed** on Wine 11*, which enables the hidraw native path by default. Harmless if set. Only relevant on older Proton where hidraw was opt-in (but those also lack the #9034 fix, so triggers won't work there anyway). |
| `PROTON_PREFER_SDL` | `1` | *Alternative, input-only.* Forces the SDL gamepad path — gives working input + rumble but as a generic/Xbox pad (**no triggers**). Use only when a game lacks native DualSense support and `ENABLE_HIDRAW` doesn't help. Do **not** combine with `ENABLE_HIDRAW` for trigger games. |
| `PROTON_LOG` | `+hid` | *Diagnostic only* (not for normal play). Writes a HID enumeration trace — Steam lands it at `~/steam-<appid>.log`; Heroic sets `PROTON_LOG_DIR` to your home, so it lands at `~/steam-0.log`. Useful for confirming the game opened the native HID path. |
> **Heroic vs Steam:** Native triggers work on **both** — and neither needs a launch option on Wine 11. Steam: set the runner + disable Steam Input. Heroic: set the runner, **fully quit Steam** (a background Steam steals the pad from non-Steam games), and keep `PROTON_PREFER_SDL` off. Either way the game must natively support DualSense — XInput-only games (e.g. Ghostrunner, Control) give rumble but no adaptive triggers, on any OS.
## Hardware
### Required
@@ -265,7 +306,7 @@ Every screen also paints **`>`** at the top-left edge (next to KEY0) and **`<`**
#### 1. Status
Connection state, paired DualSense BD address, battery % with bar (`+` charging / `*` complete / `!` error), live analog stick positions, D-pad, face buttons (△ ◯ ✕ □), L1/R1, and L2/R2 analog trigger fill bars. The link indicator and battery use small pixel icons.
Connection state, paired DualSense BD address, battery % with bar (`+` charging / `*` complete / `!` error), live analog stick positions (each stick box flashes inverse — a black dot on a white box — while its **L3 / R3** is clicked in), D-pad, face buttons (△ ◯ ✕ □), L1/R1, and L2/R2 analog trigger fill bars. The link indicator and battery use small pixel icons. While charging, a `~Nm` estimate of the time to 100% appears next to the battery.
<img src="./assets/oled/oled_sc01.jpg" alt="Status screen on the OLED" width="420">
@@ -300,7 +341,7 @@ Cycle order: **Off → Feedback → Weapon → Vibration → Bow → Gallop →
#### 5. Gyro Tilt
Live X/Y/Z accelerometer values with a 40×40 crosshair box. Tilt the controller and the dot tracks in real time.
Live X/Y/Z accelerometer values with a 40×40 crosshair box. The dot tracks the controller's tilt in real time and **sits centered when the controller lies flat** — it's driven by the controller's own per-unit factory IMU calibration (parsed from feature report `0x05`), so the rest position and gain are correct on every controller. Tilting left/right and forward/back moves the dot in the matching direction.
<img src="./assets/oled/oled_sc05.jpg" alt="Gyro Tilt screen on the OLED" width="420">
+8
View File
@@ -113,6 +113,14 @@ void config_valid() {
body->bt_mic_enable = 1;
printf("[Config] bt_mic_enable invalid, defaulting to 1 (on)\n");
}
if (body->screen_brightness > 3) { // kBrightLevels has 4 entries (0..3)
body->screen_brightness = 0; // full brightness
printf("[Config] screen_brightness invalid, defaulting to 0 (full)\n");
}
if (body->controller_wakes_display > 1) { // 0xFF erased / upgrade → default ON
body->controller_wakes_display = 1;
printf("[Config] controller_wakes_display invalid, defaulting to 1 (on)\n");
}
if (body->config_version != CONFIG_VERSION) {
body->config_version = CONFIG_VERSION;
printf("[Config] Warning: Config may breaking change\n");
+10
View File
@@ -44,6 +44,16 @@ struct __attribute__((packed)) Config_body {
// over BT and the dongle decodes it to the USB capture endpoint. Costs extra
// DS5 battery (keeps its audio subsystem awake), hence the toggle.
uint8_t bt_mic_enable;
// OLED brightness, as an index into kBrightLevels[] (src/oled.cpp). Persisted
// so the KEY1-long-press brightness choice survives a power cycle. Erased
// flash (0xFF) → clamped to 0 (full brightness) by config_valid. Issue #9.
uint8_t screen_brightness;
// When 0, controller input no longer keeps the OLED awake — only the OLED's
// own KEY0/KEY1 do — so the dim/off timers actually count down during
// gameplay and the panel can sleep while the controller is in use. Default 1
// preserves the original "any controller activity wakes the screen"
// behavior. Issues #8 (dim timeout never fired during play) and #9.
uint8_t controller_wakes_display;
};
struct __attribute__((packed)) Config {
+41
View File
@@ -251,6 +251,47 @@ uint16_t tud_hid_get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t
(void) buffer;
(void) reqlen;
// --- DualSense feature reports that Linux's hid_playstation reads at probe ---
// Without valid answers the kernel never creates a gamepad device, so games
// outside Steam Input (Heroic/Proton/native) see no controller. The host asks
// only for the report DATA (reqlen = report_size - 1); usbhid prepends the
// report-id byte itself. The two CRC'd reports validate crc32 over
// [0xA3 feature-seed, report_id, data...] in the last 4 bytes.
// hid_playstation (kernel) AND the game's native DualSense detection both read
// 0x09 (pairing), 0x20 (firmware) and 0x05 (calibration). The KERNEL only checks
// size + crc, but the GAME validates the actual CONTENT — so synthesized zeros
// pass the kernel yet get rejected by the game (a ~156x GET retry storm, and no
// native adaptive triggers). Serve the REAL controller data, which init_feature()
// caches from the controller over BT (get_feature_data returns it incl. the
// report-id at [0]). Fall back to a crc-valid synthetic answer ONLY when the
// controller isn't linked yet (USB-enumeration probe before the BT link), so the
// kernel still binds at that moment.
if (report_id == 0x09 || report_id == 0x20 || report_id == 0x05) {
if (reqlen == 0) return 0;
std::vector<uint8_t> real = get_feature_data(report_id, reqlen);
if (real.size() > 1) { // real cached response present
uint16_t n = (uint16_t)(real.size() - 1);
if (n > reqlen) n = reqlen;
memcpy(buffer, real.data() + 1, n);
return n;
}
memset(buffer, 0, reqlen);
if (report_id == 0x09) { // not linked yet: MAC-only stub
if (reqlen >= 6) bt_get_addr(buffer);
return reqlen;
}
if (reqlen < 5) return 0; // 0x20 / 0x05 stub: zeros + valid crc32
uint8_t tmp[2 + 64];
tmp[0] = 0xA3; tmp[1] = report_id;
memcpy(tmp + 2, buffer, reqlen - 4);
uint32_t crc = crc32_seeded(tmp, (size_t)(2 + (reqlen - 4)), 0);
buffer[reqlen - 4] = (uint8_t)(crc);
buffer[reqlen - 3] = (uint8_t)(crc >> 8);
buffer[reqlen - 2] = (uint8_t)(crc >> 16);
buffer[reqlen - 1] = (uint8_t)(crc >> 24);
return reqlen;
}
if (is_pico_cmd(report_id)) {
return pico_cmd_get(report_id, buffer, reqlen);
}
+27 -6
View File
@@ -126,15 +126,16 @@ constexpr int kLbModeHost = 8;
constexpr int kNumLbModes = 9;
// Settings screen state
constexpr int kNumSettingsItems = 16; // 8 fields + 3 auto-haptic + 2 screen-timeout + BT mic + Reset + Wipe
constexpr int kNumSettingsItems = 17; // 8 fields + 3 auto-haptic + 2 screen-timeout + BT mic + Ctrl-wake + Reset + Wipe
constexpr int kSettingsAutoHapEnaIdx = 8;
constexpr int kSettingsAutoHapGainIdx = 9;
constexpr int kSettingsAutoHapLpIdx = 10;
constexpr int kSettingsScrDimIdx = 11;
constexpr int kSettingsScrOffIdx = 12;
constexpr int kSettingsBtMicIdx = 13;
constexpr int kSettingsResetIdx = 14;
constexpr int kSettingsWipeSlotsIdx = 15;
constexpr int kSettingsCtrlWakeIdx = 14;
constexpr int kSettingsResetIdx = 15;
constexpr int kSettingsWipeSlotsIdx = 16;
Config_body settings_local{};
int settings_sel = 0;
bool settings_dirty = false;
@@ -521,6 +522,14 @@ void handle_buttons() {
last_activity_us = time_us_64();
if (held > kLongPressUs) {
bright_idx = (bright_idx + 1) % kNumBrightLevels;
// Persist so the choice survives a power cycle (issue #9). Keep
// settings_local in sync too, so a later Settings-screen save can't
// clobber screen_brightness with its stale snapshot.
Config_body b = get_config();
b.screen_brightness = (uint8_t)bright_idx;
set_config(b);
config_save();
settings_local.screen_brightness = (uint8_t)bright_idx;
} else {
current_screen = (current_screen - 1 + kNumScreens) % kNumScreens;
last_render_us = 0;
@@ -1547,6 +1556,7 @@ void settings_adjust(int delta) {
break;
}
case 13: c.bt_mic_enable ^= 1; break; // BT mic on/off
case 14: c.controller_wakes_display ^= 1; break; // controller activity wakes OLED on/off
}
}
@@ -1649,8 +1659,9 @@ __attribute__((noinline)) void format_settings_item(int idx, char* line, size_t
else snprintf(line, n, "%s ScrOff %umin", cur, c.screen_off_timeout);
break;
case 13: snprintf(line, n, "%s BT Mic %s", cur, c.bt_mic_enable ? "on" : "off"); break;
case 14: snprintf(line, n, "%s Reset to defaults", cur); break;
case 15: snprintf(line, n, "%s Wipe all slots", cur); break;
case 14: snprintf(line, n, "%s CtrlWake %s", cur, c.controller_wakes_display ? "on" : "off"); break;
case 15: snprintf(line, n, "%s Reset to defaults", cur); break;
case 16: snprintf(line, n, "%s Wipe all slots", cur); break;
}
}
@@ -1822,6 +1833,11 @@ void oled_init() {
// Restore the persisted lightbar mode + favorites (config_load() already ran
// in main() before this). Defaults to HOST passthrough on a fresh flash.
lightbar_load_config();
// Restore the persisted OLED brightness (KEY1-long-press choice). config_valid
// clamps screen_brightness to a legal kBrightLevels index, so this is safe to
// use directly. Fresh flash → 0 (full brightness). Issue #9.
bright_idx = get_config().screen_brightness;
}
// Dim-tier renderer: blank the panel and draw a tiny "I'm alive" dot that
@@ -1885,7 +1901,12 @@ void oled_loop() {
}
if (hash != last_input_hash) {
last_input_hash = hash;
last_activity_us = time_us_64();
// Controller input only keeps the panel awake when the user has left
// "CtrlWake" on (the default). With it off, the dim/off timers count
// down during gameplay and only KEY0/KEY1 wake the screen — see
// handle_buttons(), which bumps last_activity_us unconditionally.
// Issues #8 / #9.
if (get_config().controller_wakes_display) last_activity_us = time_us_64();
}
// Rising-edge: BT-connect itself counts as activity, so the screen wakes
// the moment a controller pairs rather than waiting for the first input.
+1 -1
View File
@@ -127,7 +127,7 @@
#define CFG_TUD_AUDIO_FUNC_1_EP_OUT_SZ_MAX CFG_TUD_AUDIO_FUNC_1_FORMAT_1_EP_SZ_OUT
#define CFG_TUD_AUDIO_FUNC_1_EP_IN_SZ_MAX CFG_TUD_AUDIO_FUNC_1_FORMAT_1_EP_SZ_IN
#define CFG_TUD_AUDIO_FUNC_1_EP_OUT_SW_BUF_SZ (3 * CFG_TUD_AUDIO_FUNC_1_EP_OUT_SZ_MAX)
#define CFG_TUD_AUDIO_FUNC_1_EP_OUT_SW_BUF_SZ (16 * CFG_TUD_AUDIO_FUNC_1_EP_OUT_SZ_MAX)
#define CFG_TUD_AUDIO_FUNC_1_EP_IN_SW_BUF_SZ (4 * CFG_TUD_AUDIO_FUNC_1_EP_IN_SZ_MAX)
// Enable OUT EP (speaker) and IN EP (mic)
+41 -22
View File
@@ -26,6 +26,8 @@
#include "bsp/board_api.h"
#include "tusb.h"
#include "config.h"
#include "bt.h"
#include "slots.h"
#ifndef ENABLE_SERIAL
#define ENABLE_SERIAL 0
@@ -370,7 +372,7 @@ uint8_t descriptor_configuration[] = {
0x00, // bCountryCode: Not localized
0x01, // bNumDescriptors: 1 report descriptor
0x22, // bDescriptorType: Report
0x41, 0x01, // wDescriptorLength: 321 (0x0141) DS
0x21, 0x01, // wDescriptorLength: 289 (0x0121) DS (F6-F9 removed to byte-match a real DS5)
// 0xB5, 0x01, // wDescriptorLength: 437 (0x01B5) DSE
// Endpoint Descriptor (HID IN: EP4)
@@ -416,7 +418,7 @@ uint8_t const *tud_descriptor_configuration_cb(uint8_t index) {
descriptor_configuration[offset - 1] = bInterval;
descriptor_configuration[offset - 8] = bInterval;
if (ds_mode()) {
descriptor_configuration[offset - 16] = 0x41;
descriptor_configuration[offset - 16] = 0x21; // wDescriptorLength lo = 289 (0x0121); F6-F9 removed to byte-match a real DS5
}else {
descriptor_configuration[offset - 16] = 0xB5;
}
@@ -569,26 +571,17 @@ uint8_t const desc_hid_report_ds[] = {
0x09, 0x36, // Usage (0x36)
0x95, 0x03, // Report Count (3)
0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x85, 0xF6, // Report ID (-10)
0x09, 0x37, // Usage (Vendor 0x37)
0x95, 0x3F, // Report Count (63)
0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x85, 0xF7, // Report ID (-9)
0x09, 0x38, // Usage (Vendor 0x38)
0x95, 0x3F, // Report Count (63)
0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x85, 0xF8, // Report ID (-8)
0x09, 0x39, // Usage (Vendor 0x39)
0x95, 0x3F, // Report Count (63)
0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x85, 0xF9, // Report ID (-7)
0x09, 0x3A, // Usage (Vendor 0x3A)
0x95, 0x3F, // Report Count (63)
0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
// NOTE: web-config feature reports 0xF6-0xF9 are intentionally NOT declared
// here. Declaring them made this descriptor 321 bytes vs a genuine DS5's 289,
// and games' native DualSense parser rejected the mismatch -> no adaptive
// triggers in-game (the long-standing "triggers only work in the OLED test"
// bug). The firmware still HANDLES 0xF6-0xF9 (src/cmd.cpp); on Linux they
// work fine undeclared over hidraw, exactly like 0xFD. Trade-off: the browser
// WebHID config tool can't reach them (OLED + hidraw config still work).
0xC0, // End Collection
// 321 bytes
// 289 bytes — byte-identical to a real DualSense
};
static_assert(sizeof(desc_hid_report_ds) == 0x0141);
static_assert(sizeof(desc_hid_report_ds) == 0x0121);
uint8_t const desc_hid_report_dse[] = {
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
@@ -858,9 +851,35 @@ uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
chr_count = 1;
break;
case STRID_SERIAL:
chr_count = board_usb_get_serial(_desc_str + 1, 32);
case STRID_SERIAL: {
// Present like a REAL DualSense: USB serial == controller MAC, the
// same value the 0x09 pairing-info feature report returns. Before
// this the serial was the Pico flash unique id, so the host saw two
// identities for one controller (flash-id over USB, MAC over 0x09)
// and Wine/Steam device-matching choked. Prefer the live connected
// MAC; fall back to the current slot's stored MAC (known at boot,
// before BT connects); finally fall back to the flash id if never
// paired so the descriptor is always valid.
uint8_t mac[6] = {0};
bool have = false;
bt_get_addr(mac);
for (int i = 0; i < 6; ++i) if (mac[i]) { have = true; break; }
if (!have) {
slot_get_addr(get_config().current_slot, mac);
for (int i = 0; i < 6; ++i) if (mac[i]) { have = true; break; }
}
if (have) {
static const char hexd[] = "0123456789ABCDEF";
for (int i = 0; i < 6; ++i) {
_desc_str[1 + i * 2] = hexd[(mac[i] >> 4) & 0x0F];
_desc_str[1 + i * 2 + 1] = hexd[mac[i] & 0x0F];
}
chr_count = 12;
} else {
chr_count = board_usb_get_serial(_desc_str + 1, 32);
}
break;
}
default:
// Note: the 0xEE index string is a Microsoft OS 1.0 Descriptors.