CPU/Clock OLED diagnostics screen (PR #2) + battery_led disconnect fix, on top of v0.6.0-oled-edition. Promotes the [Unreleased] section to [0.6.1-oled-edition] with today's date. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
19 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]
[0.6.1-oled-edition] — 2026-05-18
Tagged release of the v0.6.0-oled-edition follow-up. UF2s attached to the GitHub release (built by .github/workflows/release.yml).
Added
- CPU / Clock diagnostics screen (
kScreenCpu, inserted between Diagnostics and BT Signal in the K0 cycle). Shows the configured system clock (SYS_CLOCK_KHZ— the overclock target), the actually runningclk_sysmeasured live by the RP2350 on-chip frequency counter against the crystal reference, the core voltage read back from the regulator (vreg_get_voltage(), not the compile-time constant), and the RP2350 on-die temperature (ADC input 4). Pure read-only instrumentation; one-time ADC bring-up and no other code path uses the ADC, so it is conflict-free.render_screen_cpu()isnoinlinelike the other render functions (Thumb literal-pool reach). Addshardware_adctotarget_link_libraries. The frequency-counter measurement (a multi-ms busy-wait) runs once on screen entry and is cached —clk_sysis fixed at boot, so only the temperature refreshes per frame, avoiding a per-frame BT/audio hitch while the screen is visible.oled_loop()gained a genericscreen_enteredflag for this. Hardware-verified on Pico 2 W + OLED. Also exported over a new HID feature report0xfc(src/cmd.cpp) — 11 bytes: set_khz, cached real_khz, vreg code, ADC ch4 raw — so the web config emulator can show live CPU telemetry (volts/temp math done web-side to keep the firmware HID path float-free).
Fixed (web telemetry — latent since the slots/diag reports landed)
- CPU/Clock temperature was a single noisy ADC sample. The RP2350 temp sensor has a shallow slope (−1.721 mV/°C, ~1 LSB ≈ 0.47 °C) so a lone 12-bit reading swings several tenths of a degree per frame — the displayed value just mirrored the latest noisy sample instead of the true die temperature. New
cpu_temp_raw_smoothed()insrc/cmd.cppaverages a 256-sample block then runs a slow EMA (α=0.15, seeded on first call). It is the single source of truth: bothrender_screen_cpu()and the0xfcweb telemetry call it, and the duplicated per-site ADC bring-up was removed (ADC now initialised in exactly one place).oled.cppno longer touches the ADC directly (dropshardware/adc.h, addscmd.h). - Live web telemetry over WebHID: not feasible on the target setup; abandoned. A browser-side read-only diagnostic proved Chrome WebHID returns
NotAllowedErrorfor any report ID not declared in the parsed HID report descriptor (declared0xF7/0xF8/0xF9read fine; undeclared0xFA/0xFB/0xFCfail). Declaring them is therefore mandatory for the web read — but doing so (even applied atomically with a matchingwDescriptorLength, correct bytes identical in shape to the working0xF6–0xF9, and abcdDevicecache-bust bump) made the device fail to enumerate as a usable HID device on the user's real Windows machine in two independent attempts (Device Manager showed it; WebHID and the PlayStation Accessories app did not). The cloned DualSense HID report descriptor cannot be safely extended on this environment. Reverted to the original descriptor (0x0141/0x01B5, no vendor feature reports,bcdDevice 0x0100). Retained with no USB impact: the0xfcfirmware handler and the temperature smoothing/cpu_temp_raw_smoothed(). The on-device CPU/Clock OLED screen is fully working and hardware-verified; the web preview's CPU screen stays on representative mock values (the slots/diagnostics web screens were never readable for the same root cause and are likewise mock-only when connected).
Fixed
- Low-battery LED keeps blinking after controller disconnect (
fb68ea5). When the DualSense's battery dropped low enough to triggerbattery_led_tick's blink and the controller subsequently disconnected (typically: battery fully depletes and the BT link drops), the Pico's onboard LED stayed frozen in whichever half-cycle it was in at the moment of disconnect; reconnect-retry windows could even briefly resume blinking. Newbattery_led_on_disconnect()clears blink state, forces LED off, and zeroslast_report_usso the stale-check early-return blocks any new blink until a fresh 0x31 report arrives on the next connection. Stale-check in the tick also now forces LED off when it fires mid-blink (defense in depth for ungraceful disconnects). Reported by Sura Academy on Discord. Same bug present in upstream — sent back as awalol/DS5Dongle#101.
[0.6.0-oled-edition] — 2026-05-17
Tagged release of the rebase below. UF2s attached to the GitHub release (built by .github/workflows/release.yml). No code changes vs the rebase; tag exists so users can install from a stable artifact.
[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.