README: checkout from our master since our content (10-screen docs, web-config link, multi-slot docs, Auto Haptics docs, Acknowledgements, etc.) is the source of truth for the fork; upstream's v0.6.0 README updates are mostly about their new web config tool which we don't surface (we link our own fork's web config instead). CHANGELOG: new [0.6.0-rebase] entry on top documenting (a) the state_mgr adoption + speaker-fix hack drop, (b) the discoverable-rule loosening for multi-slot, (c) the iSerialNumber restore that fixes #100, and (d) verification results from the mid-rebase flash. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
14 KiB
Changelog
All notable changes to Pico2W DualSense 5 Bridge — OLED Edition are documented here. This fork tracks awalol/DS5Dongle (upstream) and adds an optional OLED status display plus a security/correctness audit pass on the core bridge.
Format follows Keep a Changelog. Versioning approximates SemVer with the upstream version stream — the fork shares a major.minor with whatever upstream tag it is rebased on.
[0.6.0-rebase] — 2026-05-17
Rebased onto upstream awalol/DS5Dongle v0.6.0-hotfix. All OLED Edition features preserved with no user-visible regression.
Changed
- Adopted upstream's
state_mgrrefactor (awalol/DS5Dongle#93) as the new base for controller-state and audio-packet construction. The local speaker / HD-haptic regression fix we shipped inbff65d6(re-introducing a 63-bytestate_datablock into every audio packet) is dropped — upstream'sstate_mgr.cppis the proper architectural fix and supersedes the hack. Speaker, HD haptics, and basic rumble all verified working on the rebased firmware. Same fix loteran/DS5Dongle shipped asc7a8d3c~6 h before ours. - All upstream commits between our prior base (
a2e3a33) andv0.6.0-hotfixare now in our tree:0a33aaePR#93 merge,b1019fbREADME update,54a4b69config struct comment typo,f882ff1adjust default state-init volume,77bbee5rumble transfer fix,c2e0d84state init after connected,7bbe37bstate_mgr refactor itself,9a2c2b4discoverable / connectable off when connected,508a841DISABLE_SPEAKER_PROC option,b308545audio.cpp comment,0ed05d3rumble hotfix. - Our
update_discoverable()helper (gatesgap_discoverable_controlonslots_any_empty()) replaces upstream's strictergap_*_control(false)pair on L2CAP connect. Effect: dongle stays discoverable while at least one slot is empty (needed for slot-1/2/3 pairing); only goes dark when all 4 are full. Strictly looser than upstream's rule, but correct for the multi-slot use case.
Fixed
awalol/DS5Dongle#100"(v0.6.0) Dongle is detected as a new device on Windows when using different USB ports" — upstream'se79c762 remove usb serialnumber #32zeroed the device descriptor'siSerialNumberto work around a SpecialK compat issue, but that broke Windows device identity: users lost per-device volume / app settings every time they moved the dongle to a different USB port. The OLED Edition restoresiSerialNumber = STRID_SERIAL(per-board unique serial from flash chip ID viaboard_usb_get_serial). Trade-off documented insrc/usb_descriptors.cpp: re-introduces SpecialK incompatibility (#32) for the narrower set of Windows users with that specific tool, in exchange for stable device identity for the broader Windows population.
Verification
End-to-end on user's hardware after the rebase:
- DS5 pairs cleanly.
- Speaker audio via
scripts/test_speaker.sh --tone 440 3— audible (load-bearing check that upstream's state_mgr does what we expect). - Basic rumble works in games (validates we didn't disturb upstream's
0ed05d3hotfix). - All 10 OLED screens render correctly with live data.
- Multi-slot pairing: switch between slots, slot_assign on empty, wipe-from-Settings all work.
- PS + Mute hold-2s combo reboot triggers
watchdog_reboot(validates ourinterrupt_loopaddition coexists with upstream'sstate_updateflow).
Unchanged
The full prior CHANGELOG history for [0.5.4] and earlier sessions remains accurate and below.
[Unreleased]
Fixed
- DualSense speaker + HD haptic actuator regression — upstream commit
3a31bd7(2026-05-12, "refactor: add SetStateData and audio send priority") moved the0x10SetStateData sub-report out of every0x36audio packet and into a one-time L2CAP-open setup. The DS5 hardware requires that sub-report to be re-asserted on every audio frame (the0x7f 0x7fHeadphones+Speaker volume bytes specifically) or the speaker and HD haptic actuators silently stop producing output. Restored insrc/audio.cppwith the pre-3a31bd7 packet layout (state_data atpkt[11..75], haptic atpkt[76..141], speaker atpkt[142..343]). Same fix shipped independently by loteran/DS5Dongle as commitc7a8d3c~6 h before ours; credit to loteran for the clearer hardware-side explanation in their commit message. - USB UAC1 SET_CUR Volume request no longer overrides flash-persisted speaker_volume. PipeWire / PulseAudio re-apply their last-known UAC1 volume on every device reconnect, which had silently overridden whatever the user had saved in the OLED Settings menu. The in-memory
volume[]array still tracks live host volume; only the flash sync was removed. Fix borrowed from loteran/DS5Dongle commit03fa1e4.
Added
- Audio Auto Haptics — derive haptic feedback from the speaker audio (UAC channels 0/1) for games that send sound but no per-frame haptic data, e.g. Ghost of Tsushima on Linux + Steam. DSP is a 1-pole low-pass + envelope follower + modulation + soft-clip (
x / (1 + |x|), avoidstanhfon Cortex-M33), borrowed from loteran/DS5Dongle commit5d6bc2f. Four modes selectable from the OLED Settings menu: Off / Fallback / Mix / Replace. Default is Fallback — derived rumble fires only after the game has been silent on the native haptic path (channels 2/3) for ~1 s, so games that send native HD haptics (Spider-Man Remastered) are not overridden, while games that don't (Ghost of Tsushima) get derived haptics out of the box. Gain (0–200 %) and LP cutoff (80 / 160 / 250 / 400 Hz) are also tunable from the Settings menu. - Audio Diagnostics counters on the OLED Diagnostics screen:
USB aud N/s(UAC1 frames per second arriving from the host) andBT 0x32 N/s(audio packets emitted to the DS5 per second). Lets the user verify the speaker/haptic path is actually moving bytes without needing a UART cable. Used to triage the speaker regression above.
0.5.4 — 2026-05-16
First full OLED Edition release. Includes upstream's v0.5.4 base plus the audit pass and the entire OLED add-on feature set.
Added — OLED display add-on
Requires a Waveshare Pico-OLED-1.3 (128×64 SH1107). Firmware drives it automatically when present and no-ops gracefully when absent.
- Boot splash (1.5 s) showing firmware version on power-on.
- 10 screens, cycled with KEY0 (forward) / KEY1 (back, except where contextual):
- Status — connection state, paired BD address, battery % with pixel-icon battery, live stick / D-pad / face-button / L1-R1 / L2-R2 trigger visualization.
- Slots — persistent 4-slot multi-controller pairing (see below).
- Lightbar Color Picker — tilt-to-RGB live preview on the controller's lightbar, with 4 user-savable favorite slots (△ ○ ✕ □) and three effect presets (Breathing, Rainbow, Fade).
- Trigger Test — cycles 7 DS5 adaptive trigger effects (Off / Feedback / Weapon / Vibration / Bow / Galloping / Machine Gun) on both L2 and R2, bitpacked per dualsensectl's reverse-engineering.
- Gyro Tilt — live X/Y/Z accelerometer values + 40×40 crosshair box that tracks tilt in real time.
- Touchpad — live render of finger positions on the touchpad surface, with a finger count.
- Diagnostics — uptime, BT state, HCI / audio-FIFO / opus-FIFO counter stubs (kept for future wiring).
- RSSI — live BT signal strength of the active link, dBm + bar.
- VU Meters — live peak meters for the speaker + haptic audio paths.
- Settings — persistent on-device editor for the 8 firmware config fields, plus "Reset to defaults" (hold △ 2 s) and "Wipe all slots" (hold △ 2 s).
- OLED brightness control — KEY1 long-press cycles brightness levels.
- Auto-dim after 5 minutes of input idle (lifespan / burn-in protection).
- Soft-reboot recovery without unplugging USB:
- OLED KEY0 double-click (~400 ms window) →
watchdog_reboot. - DS5
PS + Muteheld for 2 seconds →watchdog_reboot(works without the OLED).
- OLED KEY0 double-click (~400 ms window) →
- Pixel-art icons in the Status screen header (link indicator + battery icon).
Added — 4-slot persistent multi-controller pairing
- Bond up to 4 DualSenses; switch between them from the Slots OLED screen.
- Active slot persisted in the existing flash-backed config; dongle reconnects to the last-used controller on boot.
- D-pad ▲▼ to move cursor, △ to switch to the cursor slot (disconnect current ACL, restart inquiry filtered to the new slot's bd_addr), □ hold 1.5 s to wipe a single slot.
- "Wipe all slots" item in the Settings menu drops all 4 stored bd_addrs + all BTstack link keys in one shot.
- Inquiry filter on
HCI_EVENT_INQUIRY_RESULTenforces slot ownership: devices stored in other slots are skipped; empty slots auto-assign on the first L2CAP HID_CONTROL channel open. - Dongle stops being BT-discoverable once all 4 slots are full (security tightening; was permanently discoverable in upstream).
- Storage: BTstack's TLV link-key DB holds the keys (
NVM_NUM_LINK_KEYS=4, unchanged from upstream); a new dedicated flash sector holds the 4 bd_addrs + occupancy bits + magic word. - Multi-slot UX modeled on zurce/DS5Dongle-OLED. Credit to zurce.
Added — security / correctness audit pass
Critical and high-severity fixes on the core bridge code. Many of these have since landed upstream independently; this changelog captures what this fork shipped.
- C1: 4.8× stack-overflow in
core1_entry'sout_buf(200 floats vs. 960 floats the resampler/encoder actually writes/reads). Root cause of the long-standing "audio may experience slight stuttering" known issue. Buffer resized and Opus return value now checked. - C2 + H5: Variable-length stack array in
set_feature_datasized by host-controlled length (potential stack blowup). Replaced with a bounded fixed buffer + length validation; CRC bounds tightened solen < 4no longer wraps to a hugesize_t. - C3: Unbounded
memcpyinto a 78-byte stack buffer intud_hid_set_report_cbfor HID report 0x02 (overflow ifbufsize > 76). Bounded. - H1:
tud_hid_get_report_cbwrotefeature_data.size() - 1bytes into the host-supplied buffer without clamping toreqlen(host-buffer overflow). Clamped. - H2: OOB read in
on_bt_data(data[56]/memcpy(.., data+3, 63)with no length check on the incoming BT frame). Bounded. - H3: Same pattern in
l2cap_packet_handler(packet[3]read with no size check). Bounded. - H4: UAC1 volume parsed as
uint16instead of signedint16— non-conformant hosts could crank haptic gain to 256× and saturate everything. Fixed sign extension. - H7: BT report sequence counter in
main.cppcycled with period 16 (incremented asint, written as(c<<4)). Replaced with the correct(c+1) & 0x0Fwrap used inaudio.cpp. - N1: Pairing-failure recovery: three
gap_inquiry_start(30)calls were commented out inbt.cpp's error paths (HCI command status, connection complete, authentication complete). Dongle would soft-brick after any pairing failure that didn't end in a disconnect. Uncommented. - N2: Removed printf of the bonding link-key to UART on every reconnect.
- N3: CRC-helper bounds:
fill_output_report_checksum/fill_feature_report_checksumunderflowedsize_tand wrote at negative offsets onlen < 4. Guarded. - N4: HCI command-send return values logged so a future stuck-dongle report has observability.
- N5: Watchdog enabled (8 s). Hangs now recover automatically instead of needing a power-cycle.
Added — pairing posture hardening
- Tightened
gap_discoverable_control(1)— the dongle now only advertises as pairable when at least one slot is empty. Once all 4 slots are full, it goes non-discoverable (still connectable to bonded controllers). - (Carried over from upstream's own hardening; documented here for completeness.)
Changed
- Project rebranded as OLED Edition to differentiate from upstream. UF2 artifact renamed:
ds5-bridge.uf2→ds5-bridge-oled.uf2. - README rewritten with full hardware section (Pico 2 W + Pico-OLED-1.3 SKUs, vendor links, prices), all 10 OLED screen mockups in the new cycle order, and explicit
TINYUSB 0.20.0pin requirement (the 0.18.0 bundled with Pico SDK 2.2.0 lacks the 4-arg form ofTUD_AUDIO_EP_SIZEused by this project'stusb_config.h). - KEY1 short-press: was a 250 ms test-rumble burst on most screens; now cycles backward through screens (mirror of KEY0). Still cycles trigger preset on Trigger Test and lightbar mode on Lightbar (their primary in-screen interactions).
- Screen cycle order reorganized: Status → Slots → Lightbar → Trigger Test → Gyro Tilt → Touchpad → Diagnostics → RSSI → VU Meters → Settings. Settings last; the three "test" stimulus screens grouped together; diagnostic screens grouped together. Screen indices are symbolic constants (
kScreenStatus,kScreenSlots, …) so future reorders are a one-block edit.
Fixed
config_default()was declared inconfig.hbut never defined upstream (latent "undefined symbol" → linker "dangerous relocation" if anything called it). Implemented (fills body with0xFF, runsconfig_valid()— same path as a freshly-erased flash sector).- GitHub Actions workflow
cppaths corrected to match theds5-bridge-oled.uf2produced after the CMakeOUTPUT_NAMErename. (Workflows had failed on every push since the rebrand commit.)
Build
- TinyUSB pinned to
0.20.0(required; see Build Instructions in README). - Pico SDK 2.2.0 + toolchain
14_2_Rel1validated. Pico 2 W (RP2350) is the primary target. CMakeLists.txtaddssrc/oled.cpp+src/slots.cppto the executable target, linkshardware_spi.OUTPUT_NAMEset tods5-bridge-oledso the UF2 reflects the project name.
Acknowledgements
- awalol/DS5Dongle — upstream base. This fork is a strict superset that tracks upstream and layers add-on features.
- zurce/DS5Dongle-OLED — pixel-art icon approach, the "hold for factory reset" UX pattern, and the multi-slot persistent pairing model.