Build-system, config, usb, and CI wiring on top of upstream's
v0.6.0-hotfix tree. Build will still fail until Phase C+D land bt.h
accessors that oled.cpp references; that's expected mid-rebase.
CMakeLists.txt: add src/oled.cpp + src/slots.cpp to add_executable,
link hardware_spi, restore OUTPUT_NAME ds5-bridge-oled.
src/config.h: append the 4 OLED Edition Config_body fields
(current_slot + auto_haptics_enable/gain/lowpass). Preserves
upstream's field order for the first 8 fields.
src/config.cpp:
- speaker_volume validity default -100 dB → 0 dB (footgun fix)
- 4 new validity guards for the appended fields with sensible
defaults (Fallback / 100 % / 160 Hz / slot 0)
- config_default() impl (was declared in upstream's config.h since
v0.5.4 but never defined; the OLED Edition's "Reset to defaults"
Settings item calls it)
src/usb.cpp: drop the UAC1 SET_CUR Volume → flash-config sync that
let PipeWire/Pulse silently override the user's saved Spk Vol on
every device reconnect. Borrowed from loteran/DS5Dongle commit
03fa1e4.
src/usb_descriptors.cpp: restore iSerialNumber = STRID_SERIAL.
Upstream's commit e79c762 zeroed it to work around SpecialK (#32),
but that broke Windows device identity (#100) — users lost
per-device volume / app preferences when moving USB ports. The
OLED Edition prioritizes Windows device-identity stability for
the broader user base over SpecialK compat for a narrower one.
.github/workflows/{build,release}.yml: artifact filenames use
ds5-bridge-oled.uf2 (and -debug / -picow variants) per the
OUTPUT_NAME rebrand.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 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.
[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.