rebase: Phase B — trivial files for the v0.6.0 base
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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
0ed05d3f56
commit
4b6a72b70a
@@ -86,7 +86,7 @@ jobs:
|
|||||||
-DVERSION="$FIRMWARE_VERSION"
|
-DVERSION="$FIRMWARE_VERSION"
|
||||||
cmake --build build/standard --target ds5-bridge
|
cmake --build build/standard --target ds5-bridge
|
||||||
mkdir -p artifacts
|
mkdir -p artifacts
|
||||||
cp build/standard/ds5-bridge.uf2 artifacts/ds5-bridge.uf2
|
cp build/standard/ds5-bridge-oled.uf2 artifacts/ds5-bridge-oled.uf2
|
||||||
|
|
||||||
- name: Build Debug firmware
|
- name: Build Debug firmware
|
||||||
run: |
|
run: |
|
||||||
@@ -97,7 +97,7 @@ jobs:
|
|||||||
-DENABLE_VERBOSE=ON \
|
-DENABLE_VERBOSE=ON \
|
||||||
-DVERSION="$FIRMWARE_VERSION"
|
-DVERSION="$FIRMWARE_VERSION"
|
||||||
cmake --build build/debug --target ds5-bridge
|
cmake --build build/debug --target ds5-bridge
|
||||||
cp build/debug/ds5-bridge.uf2 artifacts/ds5-bridge-debug.uf2
|
cp build/debug/ds5-bridge-oled.uf2 artifacts/ds5-bridge-oled-debug.uf2
|
||||||
|
|
||||||
- name: Build no-batt-led firmware (compile check only)
|
- name: Build no-batt-led firmware (compile check only)
|
||||||
run: |
|
run: |
|
||||||
@@ -116,25 +116,25 @@ jobs:
|
|||||||
-DPICO_W_BUILD=ON \
|
-DPICO_W_BUILD=ON \
|
||||||
-DVERSION="$FIRMWARE_VERSION"
|
-DVERSION="$FIRMWARE_VERSION"
|
||||||
cmake --build build/picow --target ds5-bridge
|
cmake --build build/picow --target ds5-bridge
|
||||||
cp build/picow/ds5-bridge.uf2 artifacts/ds5-bridge-picow.uf2
|
cp build/picow/ds5-bridge-oled.uf2 artifacts/ds5-bridge-oled-picow.uf2
|
||||||
|
|
||||||
- name: Upload standard UF2 artifact
|
- name: Upload standard UF2 artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
path: artifacts/ds5-bridge.uf2
|
path: artifacts/ds5-bridge-oled.uf2
|
||||||
archive: false
|
archive: false
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Debug UF2 artifact
|
- name: Upload Debug UF2 artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
path: artifacts/ds5-bridge-debug.uf2
|
path: artifacts/ds5-bridge-oled-debug.uf2
|
||||||
archive: false
|
archive: false
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload PicoW UF2 artifact
|
- name: Upload PicoW UF2 artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
path: artifacts/ds5-bridge-picow.uf2
|
path: artifacts/ds5-bridge-oled-picow.uf2
|
||||||
archive: false
|
archive: false
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ jobs:
|
|||||||
-DVERSION="$FIRMWARE_VERSION"
|
-DVERSION="$FIRMWARE_VERSION"
|
||||||
cmake --build build/standard --target ds5-bridge
|
cmake --build build/standard --target ds5-bridge
|
||||||
mkdir -p artifacts
|
mkdir -p artifacts
|
||||||
cp build/standard/ds5-bridge.uf2 "artifacts/ds5-bridge-${{ github.event.release.tag_name }}.uf2"
|
cp build/standard/ds5-bridge-oled.uf2 "artifacts/ds5-bridge-oled-${{ github.event.release.tag_name }}.uf2"
|
||||||
|
|
||||||
- name: Build Debug firmware
|
- name: Build Debug firmware
|
||||||
run: |
|
run: |
|
||||||
@@ -102,7 +102,7 @@ jobs:
|
|||||||
-DENABLE_VERBOSE=ON \
|
-DENABLE_VERBOSE=ON \
|
||||||
-DVERSION="$FIRMWARE_VERSION"
|
-DVERSION="$FIRMWARE_VERSION"
|
||||||
cmake --build build/debug --target ds5-bridge
|
cmake --build build/debug --target ds5-bridge
|
||||||
cp build/debug/ds5-bridge.uf2 "artifacts/ds5-bridge-debug-${{ github.event.release.tag_name }}.uf2"
|
cp build/debug/ds5-bridge-oled.uf2 "artifacts/ds5-bridge-oled-debug-${{ github.event.release.tag_name }}.uf2"
|
||||||
|
|
||||||
- name: Upload UF2 files to release
|
- name: Upload UF2 files to release
|
||||||
env:
|
env:
|
||||||
|
|||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to **Pico2W DualSense 5 Bridge — OLED Edition** are documented here. This fork tracks [awalol/DS5Dongle](https://github.com/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](https://keepachangelog.com/en/1.1.0/). Versioning approximates [SemVer](https://semver.org/) 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 the `0x10` SetStateData sub-report out of every `0x36` audio packet and into a one-time L2CAP-open setup. The DS5 hardware requires that sub-report to be re-asserted on every audio frame (the `0x7f 0x7f` Headphones+Speaker volume bytes specifically) or the speaker and HD haptic actuators silently stop producing output. Restored in `src/audio.cpp` with the pre-3a31bd7 packet layout (state_data at `pkt[11..75]`, haptic at `pkt[76..141]`, speaker at `pkt[142..343]`). Same fix shipped independently by loteran/DS5Dongle as commit `c7a8d3c` ~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 commit `03fa1e4`.
|
||||||
|
|
||||||
|
### 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|)`, avoids `tanhf` on Cortex-M33), borrowed from loteran/DS5Dongle commit `5d6bc2f`. 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) and `BT 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](https://www.waveshare.com/wiki/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):
|
||||||
|
1. **Status** — connection state, paired BD address, battery % with pixel-icon battery, live stick / D-pad / face-button / L1-R1 / L2-R2 trigger visualization.
|
||||||
|
2. **Slots** — persistent 4-slot multi-controller pairing (see below).
|
||||||
|
3. **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).
|
||||||
|
4. **Trigger Test** — cycles 7 DS5 adaptive trigger effects (Off / Feedback / Weapon / Vibration / Bow / Galloping / Machine Gun) on both L2 and R2, bitpacked per [dualsensectl](https://github.com/nowrep/dualsensectl)'s reverse-engineering.
|
||||||
|
5. **Gyro Tilt** — live X/Y/Z accelerometer values + 40×40 crosshair box that tracks tilt in real time.
|
||||||
|
6. **Touchpad** — live render of finger positions on the touchpad surface, with a finger count.
|
||||||
|
7. **Diagnostics** — uptime, BT state, HCI / audio-FIFO / opus-FIFO counter stubs (kept for future wiring).
|
||||||
|
8. **RSSI** — live BT signal strength of the active link, dBm + bar.
|
||||||
|
9. **VU Meters** — live peak meters for the speaker + haptic audio paths.
|
||||||
|
10. **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 + Mute` held for 2 seconds → `watchdog_reboot` (works without the OLED).
|
||||||
|
- **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_RESULT` enforces 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](https://github.com/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`'s `out_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_data` sized by host-controlled length (potential stack blowup). Replaced with a bounded fixed buffer + length validation; CRC bounds tightened so `len < 4` no longer wraps to a huge `size_t`.
|
||||||
|
- **C3**: Unbounded `memcpy` into a 78-byte stack buffer in `tud_hid_set_report_cb` for HID report 0x02 (overflow if `bufsize > 76`). Bounded.
|
||||||
|
- **H1**: `tud_hid_get_report_cb` wrote `feature_data.size() - 1` bytes into the host-supplied buffer without clamping to `reqlen` (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 `uint16` instead of signed `int16` — non-conformant hosts could crank haptic gain to 256× and saturate everything. Fixed sign extension.
|
||||||
|
- **H7**: BT report sequence counter in `main.cpp` cycled with period 16 (incremented as `int`, written as `(c<<4)`). Replaced with the correct `(c+1) & 0x0F` wrap used in `audio.cpp`.
|
||||||
|
- **N1**: Pairing-failure recovery: three `gap_inquiry_start(30)` calls were commented out in `bt.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_checksum` underflowed `size_t` and wrote at negative offsets on `len < 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.0` pin requirement (the 0.18.0 bundled with Pico SDK 2.2.0 lacks the 4-arg form of `TUD_AUDIO_EP_SIZE` used by this project's `tusb_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 in `config.h` but never defined upstream (latent "undefined symbol" → linker "dangerous relocation" if anything called it). Implemented (fills body with `0xFF`, runs `config_valid()` — same path as a freshly-erased flash sector).
|
||||||
|
- GitHub Actions workflow `cp` paths corrected to match the `ds5-bridge-oled.uf2` produced after the CMake `OUTPUT_NAME` rename. (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_Rel1` validated. Pico 2 W (RP2350) is the primary target.
|
||||||
|
- `CMakeLists.txt` adds `src/oled.cpp` + `src/slots.cpp` to the executable target, links `hardware_spi`.
|
||||||
|
- `OUTPUT_NAME` set to `ds5-bridge-oled` so the UF2 reflects the project name.
|
||||||
|
|
||||||
|
### Acknowledgements
|
||||||
|
|
||||||
|
- **[awalol/DS5Dongle](https://github.com/awalol/DS5Dongle)** — upstream base. This fork is a strict superset that tracks upstream and layers add-on features.
|
||||||
|
- **[zurce/DS5Dongle-OLED](https://github.com/zurce/DS5Dongle-OLED)** — pixel-art icon approach, the "hold for factory reset" UX pattern, and the multi-slot persistent pairing model.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[0.5.4]: https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases/tag/v0.5.4
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## What this is
|
||||||
|
|
||||||
|
A Raspberry Pi Pico 2 W (RP2350) firmware that bridges a Sony DualSense (DS5) controller over Bluetooth Classic to a USB host. Presents itself to the host as a USB HID + UAC1 audio composite device. This repo is `MarcelineVPQ/DS5Dongle-OLED-Edition`, a personal fork of upstream `awalol/DS5Dongle` that adds an optional Waveshare Pico-OLED-1.3 status display (10 screens) and a 4-slot persistent multi-controller pairing layer.
|
||||||
|
|
||||||
|
`README.md` is authoritative for user-facing capability; `CHANGELOG.md` tracks what shipped per release. Read both before proposing changes — much of the design is non-obvious from the code alone (audit history, BT pairing-posture rationale, overclock requirement).
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
This is firmware — there are no tests. Verification is hardware-in-the-loop (see "Hardware-in-the-loop workflow" below).
|
||||||
|
|
||||||
|
The build is **non-trivial to set up** because it requires a specific TinyUSB version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pico SDK 2.2.0 must be installed; export PICO_SDK_PATH.
|
||||||
|
# CRITICAL: TinyUSB must be pinned to 0.20.0. The 0.18.0 that ships with
|
||||||
|
# Pico SDK 2.2.0 lacks the 4-arg form of TUD_AUDIO_EP_SIZE used by
|
||||||
|
# src/tusb_config.h — compile errors with no clear pointer to the cause.
|
||||||
|
cd "$PICO_SDK_PATH/lib/tinyusb"
|
||||||
|
git fetch --tags && git checkout 0.20.0
|
||||||
|
|
||||||
|
# Build
|
||||||
|
cd /path/to/DS5Dongle
|
||||||
|
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DPICO_SDK_PATH="$PICO_SDK_PATH"
|
||||||
|
cmake --build build --target ds5-bridge
|
||||||
|
# Produces build/ds5-bridge-oled.uf2 (output name set in CMakeLists.txt, not the default)
|
||||||
|
```
|
||||||
|
|
||||||
|
Common build flags (set via `-D` on the cmake invocation):
|
||||||
|
|
||||||
|
- `ENABLE_SERIAL=ON` — route printf to USB CDC for debugging. Default OFF (production).
|
||||||
|
- `ENABLE_VERBOSE=ON` — chatty BT/HID logging. Default OFF.
|
||||||
|
- `ENABLE_BATT_LED=OFF` — disable the low-battery LED blink. Default ON.
|
||||||
|
- `PICO_W_BUILD=ON` — build for the original Pico W (drops audio, lower clock). Default targets Pico 2 W.
|
||||||
|
|
||||||
|
CI runs four build variants in `.github/workflows/build.yml` (standard / debug / no-batt-led / Pico W). When changing build flags or CMake, update the workflow accordingly.
|
||||||
|
|
||||||
|
## Hardware-in-the-loop workflow
|
||||||
|
|
||||||
|
There are no software tests. To verify a change:
|
||||||
|
|
||||||
|
1. Build → produces `build/ds5-bridge-oled.uf2`.
|
||||||
|
2. Put Pico into bootloader: hold BOOTSEL, plug USB. It mounts as `/run/media/$USER/RP2350` (Linux) or `RPI-RP2` (others).
|
||||||
|
3. Copy the UF2 to the mount: `cp build/ds5-bridge-oled.uf2 /run/media/$USER/RP2350/` — Pico auto-reboots into the new firmware.
|
||||||
|
4. Pair a DualSense: hold Create + PS on the controller until the lightbar pulses; the dongle inquiry picks it up.
|
||||||
|
5. Verify enumeration host-side: `lsusb | grep 054c:0ce6` should show `Sony Corp. DualSense wireless controller (PS5)`.
|
||||||
|
|
||||||
|
The development cadence is **one feature per UF2 + checkpoint with the user before moving on** — the user has hardware in front of them, regressions are caught the same minute, and large multi-feature commits make root-causing hard.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
**Two cores, asymmetric workload:**
|
||||||
|
|
||||||
|
- **Core 0** runs everything: `main()` infinite loop in `src/main.cpp` drives `cyw43_arch_poll()` → BTstack, `tud_task()` → TinyUSB, `audio_loop()` (USB → BT haptic path), `interrupt_loop()` (BT → USB HID path), and `oled_loop()`.
|
||||||
|
- **Core 1** runs only `core1_entry()` in `src/audio.cpp` — the Opus speaker encoder. The two cores communicate via Pico SDK `queue_t` (`audio_fifo`) and a critical section (`opus_cs`).
|
||||||
|
|
||||||
|
**Two parallel data paths through the firmware:**
|
||||||
|
|
||||||
|
1. **HID** (DS5 input → host, host output → DS5): `src/bt.cpp` registers `on_bt_data()` → `src/main.cpp:on_bt_data()` → updates `interrupt_in_data[63]` → `tud_hid_report()`. Output reports come the other way through `tud_hid_set_report_cb`.
|
||||||
|
2. **Audio** (host USB audio → DS5 speaker + haptics): `src/audio.cpp:audio_loop()` reads 4-channel USB UAC1, splits ch1/2 (speaker → Opus encode on core1 → BT 0x32 report) and ch3/4 (haptic → resample 48k→3k → BT 0x32 report).
|
||||||
|
|
||||||
|
**BT layer:** Uses Pico SDK's bundled BTstack (no separate clone). `src/bt.cpp` is the only file that touches HCI/L2CAP. PSMs are `PSM_HID_CONTROL` and `PSM_HID_INTERRUPT`. Link keys live in BTstack's TLV NVM with `NVM_NUM_LINK_KEYS=4` (configured in `src/btstack_config.h`).
|
||||||
|
|
||||||
|
**OLED add-on is optional and self-contained:** All 10 screens, the SH1107 SPI driver, the 5×7 font, the icon table, and the input handling live in `src/oled.cpp` (~30 KB). The only outward dependencies are read-only accessors (`bt_is_connected`, `bt_get_addr`, `audio_peak_*`, `bt_get_signal_strength`, the new slot accessors) and the global `interrupt_in_data[63]` (read-only for input visualization). When no OLED is wired, the SPI writes go nowhere — no init check needed.
|
||||||
|
|
||||||
|
**Multi-slot pairing (Phase G):** Storage is two-tier:
|
||||||
|
|
||||||
|
- **Link keys** stay in BTstack's TLV NVM (4 slots, unchanged from upstream).
|
||||||
|
- **bd_addrs + occupancy** in a custom flash sector owned by `src/slots.cpp` — sector below the config sector (`PICO_FLASH_SIZE_BYTES - 2*FLASH_SECTOR_SIZE`).
|
||||||
|
|
||||||
|
Slot routing is enforced in `src/bt.cpp`'s `HCI_EVENT_INQUIRY_RESULT` handler: skip devices owned by other slots, accept only the current slot's bd_addr if occupied, auto-assign on first `L2CAP_EVENT_CHANNEL_OPENED` for HID_CONTROL when the current slot is empty. Switching slots flows: `bt_set_slot()` → persist new index → `bt_disconnect()` → `HCI_EVENT_DISCONNECTION_COMPLETE` → restart inquiry under new filter.
|
||||||
|
|
||||||
|
**Flash layout** (top of flash, growing downward):
|
||||||
|
|
||||||
|
| Sector | Owner | Contents |
|
||||||
|
|---|---|---|
|
||||||
|
| `PICO_FLASH_SIZE_BYTES - 1*FLASH_SECTOR_SIZE` | `src/config.cpp` | `Config{magic, version, crc32, size, body}` — 9 user-tunable fields incl. `current_slot` |
|
||||||
|
| `PICO_FLASH_SIZE_BYTES - 2*FLASH_SECTOR_SIZE` | `src/slots.cpp` | `SlotsData{magic, addrs[4][6], occupied[4]}` |
|
||||||
|
|
||||||
|
`config_save()` and `save_slots_to_flash()` both use `save_and_disable_interrupts()` + `flash_range_erase` + `flash_range_program`. `PICO_FLASH_ASSUME_CORE1_SAFE=1` is set in CMakeLists.txt — flash writes happen while core1 may be running, but core1's hot path (Opus encode) does not access XIP during the brief erase/program window.
|
||||||
|
|
||||||
|
## Critical gotchas
|
||||||
|
|
||||||
|
These are non-obvious from the code; they cost time when forgotten.
|
||||||
|
|
||||||
|
- **Overclock 320 MHz @ 1.20 V is load-bearing.** Dropping voltage to 1.10 V *or* clock to stock breaks the CYW43 PIO SPI bus (BT chip becomes unreachable). Tested. Don't "optimize" `src/main.cpp:main()`'s `vreg_set_voltage(VREG_VOLTAGE_1_20)` + `set_sys_clock_khz(SYS_CLOCK_KHZ, true)` block.
|
||||||
|
- **Render functions are `__attribute__((noinline))` on purpose.** With aggressive inlining the compiler folds all 10 `render_screen_*` into `oled_loop()`, which then exceeds Thumb's 4 KB literal-pool reach and the linker emits `dangerous relocation: unsupported relocation`. If you remove the noinline attributes or merge two render functions into a single huge one, the build will fail at link time. The symbol size grows over time as screens get richer — the lambda inside `render_screen_settings` was already hoisted to a noinline `format_settings_item()` for the same reason.
|
||||||
|
- **`config_default()` is implemented in this fork**, not upstream. Upstream declares it in `config.h:30` but never defines it. The implementation in `src/config.cpp` fills the body with `0xFF` and re-runs `config_valid()` (matches a freshly-erased flash sector). Don't assume any header declaration is implemented just because it compiles — check the .cpp.
|
||||||
|
- **Screen indices are symbolic.** `kScreenStatus`, `kScreenSlots`, `kScreenLightbar`, … are defined in a single block near the top of `src/oled.cpp`. `oled_loop()`'s switch and `handle_buttons()`' KEY1 contextual checks (Trigger preset cycle, Lightbar mode cycle) refer to them by name. Never hard-code integers — reorder = one-block edit.
|
||||||
|
- **OLED `kPin*` pinout is hardcoded** to the Waveshare Pico-OLED-1.3 standard (`kPinMOSI=11`, `kPinCLK=10`, `kPinCS=9`, `kPinDC=8`, `kPinRST=12`, `kPinKey0=15`, `kPinKey1=17`). SPI1 instance. Conflicts with anything else on those pins. Pico's BT chip uses internal SPI to CYW43 (different bus), and USB / UART pins (GP0/GP1) are also free — no conflicts in default config.
|
||||||
|
- **DS5 PS+Mute hold-2-seconds → `watchdog_reboot(0,0,0)`** is the headless soft-reboot path (see `src/main.cpp:interrupt_loop()`). It checks `interrupt_in_data[9]` bits 0+2. If you change input report layout, this combo breaks silently.
|
||||||
|
- **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`).
|
||||||
|
|
||||||
|
## Git / branch model
|
||||||
|
|
||||||
|
- **`master` (origin)** = `MarcelineVPQ/DS5Dongle-OLED-Edition` (this fork's primary branding, what users download).
|
||||||
|
- **`upstream` / `upstream-real`** = `awalol/DS5Dongle` (read-only mirror for `git fetch upstream && git rebase` cycles).
|
||||||
|
- **`upstream-fork`** = `MarcelineVPQ/DS5Dongle-upstream-fork` (a separate cleanroom fork retained from earlier PR contributions; not the active development branch).
|
||||||
|
|
||||||
|
Commits in this repo include a `Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>` trailer for AI-assisted work.
|
||||||
|
|
||||||
|
## Where features live
|
||||||
|
|
||||||
|
When asked to modify behavior, the *first* file to read is usually one of:
|
||||||
|
|
||||||
|
- New BT pairing / connection state behavior → `src/bt.cpp` (HCI + L2CAP event handlers).
|
||||||
|
- New OLED screen or change to existing one → `src/oled.cpp`.
|
||||||
|
- New persistent config field → `src/config.h` (struct), `src/config.cpp:config_valid()` (defaults + clamping), `src/oled.cpp:format_settings_item()` (UI), `src/oled.cpp:settings_adjust()` (D-pad ▶◀ behavior). Update `CHANGELOG.md`.
|
||||||
|
- USB descriptor or interface change → `src/usb_descriptors.cpp` + `src/tusb_config.h`.
|
||||||
|
- Audio / haptic path → `src/audio.cpp`. **Don't** add stack arrays sized smaller than the resampler / Opus expects (this is the C1 bug that caused the long-standing "audio stuttering" issue — fix landed in upstream `5b04cbd`, but the lesson stands).
|
||||||
|
|
||||||
|
## Don't do these
|
||||||
|
|
||||||
|
- **Don't disable the watchdog** without explicit user authorization. `watchdog_enable(1000, true)` in `src/main.cpp:main()` is the safety net for hangs.
|
||||||
|
- **Don't add fallbacks for missing OLED** beyond what `oled.cpp` already does (no init checks, SPI writes harmlessly to nothing) — the firmware must boot identically with or without the display.
|
||||||
|
- **Don't write a new flash sector backend** without updating the `static_assert(SECTOR_OFFSET % FLASH_SECTOR_SIZE == 0)` pattern from `slots.cpp` and confirming no overlap with `config.cpp`'s sector.
|
||||||
|
- **Don't push to `upstream` or `upstream-fork`** by accident — only `origin/master` is the personal fork's deployment target.
|
||||||
+5
-1
@@ -88,6 +88,8 @@ add_executable(ds5-bridge
|
|||||||
src/config.cpp
|
src/config.cpp
|
||||||
src/cmd.cpp
|
src/cmd.cpp
|
||||||
src/state_mgr.cpp
|
src/state_mgr.cpp
|
||||||
|
src/oled.cpp
|
||||||
|
src/slots.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
if (ENABLE_BATT_LED)
|
if (ENABLE_BATT_LED)
|
||||||
@@ -144,7 +146,8 @@ if(WAKE_DEBUG)
|
|||||||
endif()
|
endif()
|
||||||
set(VERSION "dev" CACHE STRING "Program version string")
|
set(VERSION "dev" CACHE STRING "Program version string")
|
||||||
|
|
||||||
pico_set_program_name(ds5-bridge "ds5-bridge")
|
set_target_properties(ds5-bridge PROPERTIES OUTPUT_NAME "ds5-bridge-oled")
|
||||||
|
pico_set_program_name(ds5-bridge "ds5-bridge-oled")
|
||||||
pico_set_program_version(ds5-bridge "${VERSION}")
|
pico_set_program_version(ds5-bridge "${VERSION}")
|
||||||
|
|
||||||
# Modify the below lines to enable/disable output over UART/USB
|
# Modify the below lines to enable/disable output over UART/USB
|
||||||
@@ -167,6 +170,7 @@ target_link_libraries(ds5-bridge
|
|||||||
hardware_watchdog
|
hardware_watchdog
|
||||||
hardware_timer
|
hardware_timer
|
||||||
hardware_flash
|
hardware_flash
|
||||||
|
hardware_spi
|
||||||
pico_btstack_classic
|
pico_btstack_classic
|
||||||
# pico_cyw43_arch_threadsafe_background
|
# pico_cyw43_arch_threadsafe_background
|
||||||
pico_cyw43_arch_poll
|
pico_cyw43_arch_poll
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 427 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 362 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 352 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 284 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 406 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 393 KiB |
Executable
+254
@@ -0,0 +1,254 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
DS5Dongle (OLED Edition) configuration tool.
|
||||||
|
|
||||||
|
Reads and writes the firmware's persistent config over USB HID feature
|
||||||
|
reports — no browser, no WebHID, works in any terminal. Ported from
|
||||||
|
loteran/DS5Dongle's scripts/set_ds5.py and extended for this fork's
|
||||||
|
Config_body layout (adds `config_version` at the start and `current_slot`
|
||||||
|
between `controller_mode` and `auto_haptics_*`).
|
||||||
|
|
||||||
|
Requires EITHER `cython-hidapi` (preferred — `pip install hidapi`) OR
|
||||||
|
the `hid` package (`pip install hid`). The script auto-detects which is
|
||||||
|
installed; both expose `hid.device` somewhere with slightly different
|
||||||
|
constructor APIs, and we handle both.
|
||||||
|
|
||||||
|
Quick usage:
|
||||||
|
scripts/set_ds5.py # print current config
|
||||||
|
scripts/set_ds5.py --auto-haptics fallback --auto-haptics-gain 120
|
||||||
|
scripts/set_ds5.py --haptics-gain 1.5 --speaker-volume -10
|
||||||
|
scripts/set_ds5.py --slot 2 # switch active pairing slot
|
||||||
|
scripts/set_ds5.py --version # print firmware version
|
||||||
|
scripts/set_ds5.py --rssi # print live BT RSSI in dBm
|
||||||
|
|
||||||
|
Credit: protocol + script structure from loteran/DS5Dongle commit 5d6bc2f.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
import hid
|
||||||
|
except ImportError:
|
||||||
|
print("[ERROR] Missing dependency: install with pip install hidapi", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _open_hid(vid, pid):
|
||||||
|
"""Open a HID device, abstracting over cython-hidapi vs apmorton/pyhidapi."""
|
||||||
|
if hasattr(hid, 'Device'): # apmorton/pyhidapi: hid.Device(vid, pid)
|
||||||
|
return hid.Device(vid, pid)
|
||||||
|
if hasattr(hid, 'device'): # cython-hidapi: hid.device() + .open(vid, pid)
|
||||||
|
d = hid.device()
|
||||||
|
d.open(vid, pid)
|
||||||
|
return d
|
||||||
|
raise RuntimeError("installed `hid` module has neither Device nor device — unknown variant")
|
||||||
|
|
||||||
|
SONY_VID = 0x054C
|
||||||
|
DS5_PID = 0x0CE6
|
||||||
|
DSE_PID = 0x0DF2
|
||||||
|
|
||||||
|
# Our Config_body wire layout — 19 bytes, little-endian, packed struct.
|
||||||
|
# uint8 config_version [0] (firmware-set, ignored on write)
|
||||||
|
# float haptics_gain [1:5]
|
||||||
|
# float speaker_volume [5:9]
|
||||||
|
# uint8 inactive_time [9]
|
||||||
|
# uint8 disable_inactive_disc [10]
|
||||||
|
# uint8 disable_pico_led [11]
|
||||||
|
# uint8 polling_rate_mode [12]
|
||||||
|
# uint8 audio_buffer_length [13]
|
||||||
|
# uint8 controller_mode [14]
|
||||||
|
# uint8 current_slot [15] (fork-specific: multi-slot pairing)
|
||||||
|
# uint8 auto_haptics_enable [16] 0=off 1=fallback 2=mix 3=replace
|
||||||
|
# uint8 auto_haptics_gain [17] [0..200] percent
|
||||||
|
# uint8 auto_haptics_lowpass [18] 0=80Hz 1=160Hz 2=250Hz 3=400Hz
|
||||||
|
CONFIG_FMT = '<BffBBBBBBBBBB'
|
||||||
|
CONFIG_SIZE = struct.calcsize(CONFIG_FMT)
|
||||||
|
assert CONFIG_SIZE == 19, f"unexpected CONFIG_SIZE {CONFIG_SIZE}"
|
||||||
|
|
||||||
|
POLLING_MODES = {0: "250 Hz", 1: "500 Hz", 2: "Real-time (1000 Hz)"}
|
||||||
|
CONTROLLER_MODES = {0: "DS5", 1: "DSE (Edge)", 2: "Auto"}
|
||||||
|
AUTO_HAP_MODES = {
|
||||||
|
0: "Off",
|
||||||
|
1: "Fallback (only when game sends no native haptic)",
|
||||||
|
2: "Mix (native + audio-derived)",
|
||||||
|
3: "Replace (audio-derived only, override native)",
|
||||||
|
}
|
||||||
|
LOWPASS_MODES = {0: "80 Hz", 1: "160 Hz", 2: "250 Hz", 3: "400 Hz"}
|
||||||
|
|
||||||
|
|
||||||
|
def open_device():
|
||||||
|
last_err = None
|
||||||
|
for pid, label in [(DS5_PID, "DualSense"), (DSE_PID, "DualSense Edge")]:
|
||||||
|
try:
|
||||||
|
d = _open_hid(SONY_VID, pid)
|
||||||
|
print(f"[INFO] Connected to {label} (VID:PID 0x{SONY_VID:04X}:0x{pid:04X})")
|
||||||
|
return d
|
||||||
|
except Exception as e:
|
||||||
|
last_err = e
|
||||||
|
continue
|
||||||
|
print("[ERROR] No DS5/DSE device found. Pair the controller with the Pico first.", file=sys.stderr)
|
||||||
|
if last_err is not None:
|
||||||
|
print(f" (last open() error: {type(last_err).__name__}: {last_err})", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def get_config(device):
|
||||||
|
raw = bytes(device.get_feature_report(0xF7, 64))
|
||||||
|
# raw[0] is the report ID echo; the actual Config_body starts at raw[1].
|
||||||
|
body = raw[1:1 + CONFIG_SIZE]
|
||||||
|
if len(body) < CONFIG_SIZE:
|
||||||
|
print(f"[ERROR] Config too short ({len(body)} bytes, expected {CONFIG_SIZE}). "
|
||||||
|
"Flash a newer firmware.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
(
|
||||||
|
config_version,
|
||||||
|
haptics_gain, speaker_volume,
|
||||||
|
inactive_time, disable_inactive_disconnect, disable_pico_led,
|
||||||
|
polling_rate_mode, audio_buffer_length, controller_mode,
|
||||||
|
current_slot,
|
||||||
|
auto_haptics_enable, auto_haptics_gain, auto_haptics_lowpass,
|
||||||
|
) = struct.unpack(CONFIG_FMT, body)
|
||||||
|
return {
|
||||||
|
'config_version': config_version,
|
||||||
|
'haptics_gain': haptics_gain,
|
||||||
|
'speaker_volume': speaker_volume,
|
||||||
|
'inactive_time': inactive_time,
|
||||||
|
'disable_inactive_disconnect': disable_inactive_disconnect,
|
||||||
|
'disable_pico_led': disable_pico_led,
|
||||||
|
'polling_rate_mode': polling_rate_mode,
|
||||||
|
'audio_buffer_length': audio_buffer_length,
|
||||||
|
'controller_mode': controller_mode,
|
||||||
|
'current_slot': current_slot,
|
||||||
|
'auto_haptics_enable': auto_haptics_enable,
|
||||||
|
'auto_haptics_gain': auto_haptics_gain,
|
||||||
|
'auto_haptics_lowpass': auto_haptics_lowpass,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def write_config(device, cfg):
|
||||||
|
body = struct.pack(
|
||||||
|
CONFIG_FMT,
|
||||||
|
cfg['config_version'] & 0xFF,
|
||||||
|
cfg['haptics_gain'], cfg['speaker_volume'],
|
||||||
|
cfg['inactive_time'] & 0xFF, cfg['disable_inactive_disconnect'] & 0xFF,
|
||||||
|
cfg['disable_pico_led'] & 0xFF,
|
||||||
|
cfg['polling_rate_mode'] & 0xFF, cfg['audio_buffer_length'] & 0xFF,
|
||||||
|
cfg['controller_mode'] & 0xFF,
|
||||||
|
cfg['current_slot'] & 0xFF,
|
||||||
|
cfg['auto_haptics_enable'] & 0xFF, cfg['auto_haptics_gain'] & 0xFF,
|
||||||
|
cfg['auto_haptics_lowpass'] & 0xFF,
|
||||||
|
)
|
||||||
|
# 0xF6 set protocol: [0x01, ...body...] = update in-memory → [0x02] = persist to flash.
|
||||||
|
device.send_feature_report(b'\xf6\x01' + body)
|
||||||
|
device.send_feature_report(b'\xf6\x02')
|
||||||
|
|
||||||
|
|
||||||
|
def get_version(device):
|
||||||
|
raw = bytes(device.get_feature_report(0xF8, 64))
|
||||||
|
return raw[1:].rstrip(b'\x00').decode('ascii', errors='replace')
|
||||||
|
|
||||||
|
|
||||||
|
def get_rssi(device):
|
||||||
|
raw = bytes(device.get_feature_report(0xF9, 64))
|
||||||
|
if len(raw) < 2:
|
||||||
|
return None
|
||||||
|
val = raw[1]
|
||||||
|
return val - 256 if val >= 128 else val # int8
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_cfg(c):
|
||||||
|
return (
|
||||||
|
f" config_version {c['config_version']}\n"
|
||||||
|
f" haptics_gain {c['haptics_gain']:.3f}\n"
|
||||||
|
f" speaker_volume {c['speaker_volume']:.1f} dB\n"
|
||||||
|
f" inactive_time {c['inactive_time']} min\n"
|
||||||
|
f" inactive_disc {'disabled' if c['disable_inactive_disconnect'] else 'enabled'}\n"
|
||||||
|
f" pico_led {'off' if c['disable_pico_led'] else 'on'}\n"
|
||||||
|
f" polling_rate {c['polling_rate_mode']} ({POLLING_MODES.get(c['polling_rate_mode'], '?')})\n"
|
||||||
|
f" audio_buffer {c['audio_buffer_length']}\n"
|
||||||
|
f" controller_mode {c['controller_mode']} ({CONTROLLER_MODES.get(c['controller_mode'], '?')})\n"
|
||||||
|
f" current_slot {c['current_slot']}\n"
|
||||||
|
f" auto_haptics {c['auto_haptics_enable']} ({AUTO_HAP_MODES.get(c['auto_haptics_enable'], '?')})\n"
|
||||||
|
f" auto_haptics_gain {c['auto_haptics_gain']}%\n"
|
||||||
|
f" auto_haptics_lp {c['auto_haptics_lowpass']} ({LOWPASS_MODES.get(c['auto_haptics_lowpass'], '?')})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
AUTO_HAP_ARG = {'off': 0, 'fallback': 1, 'mix': 2, 'replace': 3}
|
||||||
|
LOWPASS_ARG = {'80': 0, '160': 1, '250': 2, '400': 3}
|
||||||
|
CTRL_MODE_ARG = {'ds5': 0, 'dse': 1, 'auto': 2}
|
||||||
|
POLL_ARG = {'250': 0, '500': 1, 'realtime': 2, 'rt': 2}
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser():
|
||||||
|
p = argparse.ArgumentParser(description="DS5Dongle (OLED Edition) config tool.")
|
||||||
|
p.add_argument('--version', action='store_true', help="print firmware version and exit")
|
||||||
|
p.add_argument('--rssi', action='store_true', help="print live BT RSSI (dBm) and exit")
|
||||||
|
p.add_argument('--haptics-gain', type=float, help="float [1.0, 2.0]")
|
||||||
|
p.add_argument('--speaker-volume', type=float, help="dB [-100, 0]")
|
||||||
|
p.add_argument('--inactive-time', type=int, help="minutes [10, 60]")
|
||||||
|
p.add_argument('--inactive-disc', choices=['on', 'off'], help="silent disconnect on idle")
|
||||||
|
p.add_argument('--pico-led', choices=['on', 'off'])
|
||||||
|
p.add_argument('--polling', choices=POLL_ARG.keys(), help="USB HID polling rate")
|
||||||
|
p.add_argument('--audio-buffer', type=int, help="haptic buffer length [16, 128]")
|
||||||
|
p.add_argument('--controller-mode', choices=CTRL_MODE_ARG.keys())
|
||||||
|
p.add_argument('--slot', type=int, choices=[0, 1, 2, 3], help="active multi-pairing slot")
|
||||||
|
p.add_argument('--auto-haptics', choices=AUTO_HAP_ARG.keys(), help="Auto Haptics mode")
|
||||||
|
p.add_argument('--auto-haptics-gain', type=int, help="percent [0, 200]")
|
||||||
|
p.add_argument('--auto-haptics-lp', choices=LOWPASS_ARG.keys(), help="LP cutoff Hz")
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = build_parser().parse_args()
|
||||||
|
device = open_device()
|
||||||
|
|
||||||
|
if args.version:
|
||||||
|
print(f"Firmware: {get_version(device)}")
|
||||||
|
return
|
||||||
|
if args.rssi:
|
||||||
|
rssi = get_rssi(device)
|
||||||
|
print(f"BT RSSI: {rssi} dBm" if rssi is not None else "RSSI unavailable")
|
||||||
|
return
|
||||||
|
|
||||||
|
cfg = get_config(device)
|
||||||
|
changes = []
|
||||||
|
|
||||||
|
def set_kv(key, val, label=None):
|
||||||
|
if cfg[key] != val:
|
||||||
|
changes.append(f" {label or key}: {cfg[key]} → {val}")
|
||||||
|
cfg[key] = val
|
||||||
|
|
||||||
|
if args.haptics_gain is not None: set_kv('haptics_gain', args.haptics_gain)
|
||||||
|
if args.speaker_volume is not None: set_kv('speaker_volume', args.speaker_volume)
|
||||||
|
if args.inactive_time is not None: set_kv('inactive_time', args.inactive_time)
|
||||||
|
if args.inactive_disc is not None:
|
||||||
|
set_kv('disable_inactive_disconnect', 1 if args.inactive_disc == 'off' else 0,
|
||||||
|
label='inactive_disc')
|
||||||
|
if args.pico_led is not None:
|
||||||
|
set_kv('disable_pico_led', 1 if args.pico_led == 'off' else 0, label='pico_led')
|
||||||
|
if args.polling is not None: set_kv('polling_rate_mode', POLL_ARG[args.polling])
|
||||||
|
if args.audio_buffer is not None: set_kv('audio_buffer_length', args.audio_buffer)
|
||||||
|
if args.controller_mode is not None: set_kv('controller_mode', CTRL_MODE_ARG[args.controller_mode])
|
||||||
|
if args.slot is not None: set_kv('current_slot', args.slot)
|
||||||
|
if args.auto_haptics is not None: set_kv('auto_haptics_enable', AUTO_HAP_ARG[args.auto_haptics])
|
||||||
|
if args.auto_haptics_gain is not None: set_kv('auto_haptics_gain', args.auto_haptics_gain)
|
||||||
|
if args.auto_haptics_lp is not None: set_kv('auto_haptics_lowpass', LOWPASS_ARG[args.auto_haptics_lp])
|
||||||
|
|
||||||
|
if changes:
|
||||||
|
print("Updating config:")
|
||||||
|
print("\n".join(changes))
|
||||||
|
write_config(device, cfg)
|
||||||
|
print("Saved to flash.")
|
||||||
|
# Re-read to show what stuck after firmware validation
|
||||||
|
cfg = get_config(device)
|
||||||
|
print("\nNew config:")
|
||||||
|
else:
|
||||||
|
print("Current config:")
|
||||||
|
print(fmt_cfg(cfg))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Executable
+68
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# test_speaker.sh — play audio through the DualSense speaker via the Pico bridge.
|
||||||
|
#
|
||||||
|
# The Pico exposes itself as a 4-channel UAC1 audio device (VID:PID 054c:0ce6).
|
||||||
|
# Channels 0+1 route to the DS5 speaker; channels 2+3 drive the HD haptic motors.
|
||||||
|
# Stereo audio fills 0+1 and leaves 2+3 silent (no haptic noise).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/test_speaker.sh # 3-second 440 Hz sine
|
||||||
|
# scripts/test_speaker.sh --tone 880 5 # 5-second 880 Hz sine
|
||||||
|
# scripts/test_speaker.sh path/to/audio # play a file (anything paplay accepts)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
find_sink() {
|
||||||
|
pactl list short sinks 2>/dev/null \
|
||||||
|
| grep -iE 'dualsense|sony|054c' \
|
||||||
|
| awk '{print $2}' \
|
||||||
|
| head -n1
|
||||||
|
}
|
||||||
|
|
||||||
|
play_tone() {
|
||||||
|
local freq="$1" dur="$2"
|
||||||
|
echo "Playing ${dur}s sine at ${freq} Hz on $SINK ..."
|
||||||
|
ffmpeg -hide_banner -loglevel error -nostdin \
|
||||||
|
-f lavfi -i "sine=frequency=${freq}:duration=${dur}" \
|
||||||
|
-ac 2 -ar 48000 -f wav - \
|
||||||
|
| paplay --device="$SINK"
|
||||||
|
}
|
||||||
|
|
||||||
|
play_file() {
|
||||||
|
local path="$1"
|
||||||
|
[[ -f "$path" ]] || { echo "Error: file not found: $path" >&2; exit 1; }
|
||||||
|
echo "Playing $path on $SINK ..."
|
||||||
|
paplay --device="$SINK" "$path"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1-}" == -h || "${1-}" == --help ]]; then
|
||||||
|
sed -n '2,11p' "$0"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SINK="$(find_sink)"
|
||||||
|
if [[ -z "$SINK" ]]; then
|
||||||
|
echo "Error: no DualSense audio sink found." >&2
|
||||||
|
echo " Check the DS5 is paired: lsusb | grep 054c:0ce6" >&2
|
||||||
|
echo " Check the sink shows up: pactl list short sinks" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${1-}" in
|
||||||
|
--tone)
|
||||||
|
play_tone "${2:-440}" "${3:-3}"
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
play_tone 440 3
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
play_file "$1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "If you heard nothing on the DS5 speaker, sanity checks:"
|
||||||
|
echo " - DS5 is paired and connected? (lsusb | grep 054c:0ce6)"
|
||||||
|
echo " - Sink not muted? pactl get-sink-mute $SINK"
|
||||||
|
echo " - DS5 hardware speaker switch / mute LED not on?"
|
||||||
|
echo " - OLED Settings -> Spk Vol not set to -100 dB (silent floor)?"
|
||||||
+27
-2
@@ -52,8 +52,8 @@ void config_valid() {
|
|||||||
printf("[Config] Haptics Gain value is invalid\n");
|
printf("[Config] Haptics Gain value is invalid\n");
|
||||||
}
|
}
|
||||||
if (std::isnan(body->speaker_volume) || body->speaker_volume < -100 || body->speaker_volume > 0) {
|
if (std::isnan(body->speaker_volume) || body->speaker_volume < -100 || body->speaker_volume > 0) {
|
||||||
body->speaker_volume = -100;
|
body->speaker_volume = 0; // OLED Edition: 0 dB default (unity); -100 would be silent
|
||||||
printf("[Config] Speaker Volume is invalid\n");
|
printf("[Config] Speaker Volume is invalid, defaulting to 0 dB\n");
|
||||||
}
|
}
|
||||||
if (body->inactive_time < 5 || body->inactive_time > 60) {
|
if (body->inactive_time < 5 || body->inactive_time > 60) {
|
||||||
body->inactive_time = 30;
|
body->inactive_time = 30;
|
||||||
@@ -79,6 +79,22 @@ void config_valid() {
|
|||||||
body->controller_mode = 2;
|
body->controller_mode = 2;
|
||||||
printf("[Config] controller_mode is invalid\n");
|
printf("[Config] controller_mode is invalid\n");
|
||||||
}
|
}
|
||||||
|
if (body->current_slot >= 4) {
|
||||||
|
body->current_slot = 0;
|
||||||
|
printf("[Config] current_slot is invalid\n");
|
||||||
|
}
|
||||||
|
if (body->auto_haptics_enable > 3) {
|
||||||
|
body->auto_haptics_enable = 1; // Fallback default
|
||||||
|
printf("[Config] auto_haptics_enable invalid, defaulting to 1 (Fallback)\n");
|
||||||
|
}
|
||||||
|
if (body->auto_haptics_gain > 200) {
|
||||||
|
body->auto_haptics_gain = 100;
|
||||||
|
printf("[Config] auto_haptics_gain invalid, defaulting to 100\n");
|
||||||
|
}
|
||||||
|
if (body->auto_haptics_lowpass > 3) {
|
||||||
|
body->auto_haptics_lowpass = 1; // 160 Hz
|
||||||
|
printf("[Config] auto_haptics_lowpass invalid, defaulting to 1 (160 Hz)\n");
|
||||||
|
}
|
||||||
if (body->config_version != CONFIG_VERSION) {
|
if (body->config_version != CONFIG_VERSION) {
|
||||||
body->config_version = CONFIG_VERSION;
|
body->config_version = CONFIG_VERSION;
|
||||||
printf("[Config] Warning: Config may breaking change\n");
|
printf("[Config] Warning: Config may breaking change\n");
|
||||||
@@ -91,6 +107,15 @@ void config_load() {
|
|||||||
config_valid();
|
config_valid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset RAM-resident config body to firmware defaults. Caller must
|
||||||
|
// config_save() to persist. Filling with 0xFF mirrors the byte pattern
|
||||||
|
// of a freshly-erased flash sector, so every field fails validity and
|
||||||
|
// gets re-initialized to its documented default by config_valid().
|
||||||
|
void config_default() {
|
||||||
|
memset(&config.body, 0xFF, sizeof(config.body));
|
||||||
|
config_valid();
|
||||||
|
}
|
||||||
|
|
||||||
bool config_save() {
|
bool config_save() {
|
||||||
config.crc32 = calc_config_crc(config);
|
config.crc32 = calc_config_crc(config);
|
||||||
alignas(4) uint8_t page[FLASH_PAGE_SIZE];
|
alignas(4) uint8_t page[FLASH_PAGE_SIZE];
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ struct __attribute__((packed)) Config_body {
|
|||||||
uint8_t polling_rate_mode; // 0: 250Hz, 1: 500Hz, 2: real-time
|
uint8_t polling_rate_mode; // 0: 250Hz, 1: 500Hz, 2: real-time
|
||||||
uint8_t audio_buffer_length; // [16,128]
|
uint8_t audio_buffer_length; // [16,128]
|
||||||
uint8_t controller_mode; // 0: DS5, 1: DSE, 2: Auto
|
uint8_t controller_mode; // 0: DS5, 1: DSE, 2: Auto
|
||||||
|
uint8_t current_slot; // [0..3] active multi-pairing slot (OLED Edition Phase G)
|
||||||
|
// Audio Auto Haptics — derive haptic feedback from speaker audio for games that
|
||||||
|
// send no per-frame native haptic data. DSP borrowed from loteran/DS5Dongle 5d6bc2f.
|
||||||
|
uint8_t auto_haptics_enable; // 0=Off, 1=Fallback (default), 2=Mix, 3=Replace
|
||||||
|
uint8_t auto_haptics_gain; // [0,200] percent, default 100
|
||||||
|
uint8_t auto_haptics_lowpass; // 0=80Hz, 1=160Hz (default), 2=250Hz, 3=400Hz
|
||||||
};
|
};
|
||||||
|
|
||||||
struct __attribute__((packed)) Config {
|
struct __attribute__((packed)) Config {
|
||||||
|
|||||||
+1219
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
|||||||
|
#ifndef DS5_BRIDGE_OLED_H
|
||||||
|
#define DS5_BRIDGE_OLED_H
|
||||||
|
|
||||||
|
void oled_init();
|
||||||
|
void oled_loop();
|
||||||
|
|
||||||
|
#endif // DS5_BRIDGE_OLED_H
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
// 5x7 ASCII font, ASCII 0x20-0x7E. 5 column bytes per glyph, LSB = top row.
|
||||||
|
// Source: public-domain font from the Adafruit / SSD1306 ecosystem.
|
||||||
|
#ifndef DS5_BRIDGE_OLED_FONT_H
|
||||||
|
#define DS5_BRIDGE_OLED_FONT_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
static constexpr int kFontW = 5;
|
||||||
|
static constexpr int kFontH = 7;
|
||||||
|
|
||||||
|
static const uint8_t kFont5x7[95][5] = {
|
||||||
|
{0x00, 0x00, 0x00, 0x00, 0x00}, // ' '
|
||||||
|
{0x00, 0x00, 0x5F, 0x00, 0x00}, // '!'
|
||||||
|
{0x00, 0x03, 0x00, 0x03, 0x00}, // '"'
|
||||||
|
{0x14, 0x3E, 0x14, 0x3E, 0x14}, // '#'
|
||||||
|
{0x24, 0x2A, 0x7F, 0x2A, 0x12}, // '$'
|
||||||
|
{0x43, 0x33, 0x08, 0x66, 0x61}, // '%'
|
||||||
|
{0x36, 0x49, 0x55, 0x22, 0x50}, // '&'
|
||||||
|
{0x00, 0x05, 0x03, 0x00, 0x00}, // '''
|
||||||
|
{0x00, 0x1C, 0x22, 0x41, 0x00}, // '('
|
||||||
|
{0x00, 0x41, 0x22, 0x1C, 0x00}, // ')'
|
||||||
|
{0x14, 0x08, 0x3E, 0x08, 0x14}, // '*'
|
||||||
|
{0x08, 0x08, 0x3E, 0x08, 0x08}, // '+'
|
||||||
|
{0x00, 0x50, 0x30, 0x00, 0x00}, // ','
|
||||||
|
{0x08, 0x08, 0x08, 0x08, 0x08}, // '-'
|
||||||
|
{0x00, 0x60, 0x60, 0x00, 0x00}, // '.'
|
||||||
|
{0x20, 0x10, 0x08, 0x04, 0x02}, // '/'
|
||||||
|
{0x3E, 0x51, 0x49, 0x45, 0x3E}, // '0'
|
||||||
|
{0x00, 0x04, 0x02, 0x7F, 0x00}, // '1'
|
||||||
|
{0x42, 0x61, 0x51, 0x49, 0x46}, // '2'
|
||||||
|
{0x22, 0x41, 0x49, 0x49, 0x36}, // '3'
|
||||||
|
{0x18, 0x14, 0x12, 0x7F, 0x10}, // '4'
|
||||||
|
{0x27, 0x45, 0x45, 0x45, 0x39}, // '5'
|
||||||
|
{0x3E, 0x49, 0x49, 0x49, 0x32}, // '6'
|
||||||
|
{0x01, 0x01, 0x71, 0x09, 0x07}, // '7'
|
||||||
|
{0x36, 0x49, 0x49, 0x49, 0x36}, // '8'
|
||||||
|
{0x26, 0x49, 0x49, 0x49, 0x3E}, // '9'
|
||||||
|
{0x00, 0x36, 0x36, 0x00, 0x00}, // ':'
|
||||||
|
{0x00, 0x56, 0x36, 0x00, 0x00}, // ';'
|
||||||
|
{0x08, 0x14, 0x22, 0x41, 0x00}, // '<'
|
||||||
|
{0x14, 0x14, 0x14, 0x14, 0x14}, // '='
|
||||||
|
{0x00, 0x41, 0x22, 0x14, 0x08}, // '>'
|
||||||
|
{0x02, 0x01, 0x51, 0x09, 0x06}, // '?'
|
||||||
|
{0x3E, 0x41, 0x59, 0x55, 0x5E}, // '@'
|
||||||
|
{0x7E, 0x09, 0x09, 0x09, 0x7E}, // 'A'
|
||||||
|
{0x7F, 0x49, 0x49, 0x49, 0x36}, // 'B'
|
||||||
|
{0x3E, 0x41, 0x41, 0x41, 0x22}, // 'C'
|
||||||
|
{0x7F, 0x41, 0x41, 0x41, 0x3E}, // 'D'
|
||||||
|
{0x7F, 0x49, 0x49, 0x49, 0x41}, // 'E'
|
||||||
|
{0x7F, 0x09, 0x09, 0x09, 0x01}, // 'F'
|
||||||
|
{0x3E, 0x41, 0x41, 0x49, 0x3A}, // 'G'
|
||||||
|
{0x7F, 0x08, 0x08, 0x08, 0x7F}, // 'H'
|
||||||
|
{0x00, 0x41, 0x7F, 0x41, 0x00}, // 'I'
|
||||||
|
{0x30, 0x40, 0x40, 0x40, 0x3F}, // 'J'
|
||||||
|
{0x7F, 0x08, 0x14, 0x22, 0x41}, // 'K'
|
||||||
|
{0x7F, 0x40, 0x40, 0x40, 0x40}, // 'L'
|
||||||
|
{0x7F, 0x02, 0x0C, 0x02, 0x7F}, // 'M'
|
||||||
|
{0x7F, 0x02, 0x04, 0x08, 0x7F}, // 'N'
|
||||||
|
{0x3E, 0x41, 0x41, 0x41, 0x3E}, // 'O'
|
||||||
|
{0x7F, 0x09, 0x09, 0x09, 0x06}, // 'P'
|
||||||
|
{0x1E, 0x21, 0x21, 0x21, 0x5E}, // 'Q'
|
||||||
|
{0x7F, 0x09, 0x09, 0x09, 0x76}, // 'R'
|
||||||
|
{0x26, 0x49, 0x49, 0x49, 0x32}, // 'S'
|
||||||
|
{0x01, 0x01, 0x7F, 0x01, 0x01}, // 'T'
|
||||||
|
{0x3F, 0x40, 0x40, 0x40, 0x3F}, // 'U'
|
||||||
|
{0x1F, 0x20, 0x40, 0x20, 0x1F}, // 'V'
|
||||||
|
{0x7F, 0x20, 0x10, 0x20, 0x7F}, // 'W'
|
||||||
|
{0x41, 0x22, 0x1C, 0x22, 0x41}, // 'X'
|
||||||
|
{0x07, 0x08, 0x70, 0x08, 0x07}, // 'Y'
|
||||||
|
{0x61, 0x51, 0x49, 0x45, 0x43}, // 'Z'
|
||||||
|
{0x00, 0x7F, 0x41, 0x00, 0x00}, // '['
|
||||||
|
{0x02, 0x04, 0x08, 0x10, 0x20}, // '\\'
|
||||||
|
{0x00, 0x00, 0x41, 0x7F, 0x00}, // ']'
|
||||||
|
{0x04, 0x02, 0x01, 0x02, 0x04}, // '^'
|
||||||
|
{0x40, 0x40, 0x40, 0x40, 0x40}, // '_'
|
||||||
|
{0x00, 0x01, 0x02, 0x04, 0x00}, // '`'
|
||||||
|
{0x20, 0x54, 0x54, 0x54, 0x78}, // 'a'
|
||||||
|
{0x7F, 0x44, 0x44, 0x44, 0x38}, // 'b'
|
||||||
|
{0x38, 0x44, 0x44, 0x44, 0x44}, // 'c'
|
||||||
|
{0x38, 0x44, 0x44, 0x44, 0x7F}, // 'd'
|
||||||
|
{0x38, 0x54, 0x54, 0x54, 0x18}, // 'e'
|
||||||
|
{0x04, 0x04, 0x7E, 0x05, 0x05}, // 'f'
|
||||||
|
{0x08, 0x54, 0x54, 0x54, 0x3C}, // 'g'
|
||||||
|
{0x7F, 0x08, 0x04, 0x04, 0x78}, // 'h'
|
||||||
|
{0x00, 0x44, 0x7D, 0x40, 0x00}, // 'i'
|
||||||
|
{0x20, 0x40, 0x44, 0x3D, 0x00}, // 'j'
|
||||||
|
{0x7F, 0x10, 0x28, 0x44, 0x00}, // 'k'
|
||||||
|
{0x00, 0x41, 0x7F, 0x40, 0x00}, // 'l'
|
||||||
|
{0x7C, 0x04, 0x78, 0x04, 0x78}, // 'm'
|
||||||
|
{0x7C, 0x08, 0x04, 0x04, 0x78}, // 'n'
|
||||||
|
{0x38, 0x44, 0x44, 0x44, 0x38}, // 'o'
|
||||||
|
{0x7C, 0x14, 0x14, 0x14, 0x08}, // 'p'
|
||||||
|
{0x08, 0x14, 0x14, 0x14, 0x7C}, // 'q'
|
||||||
|
{0x00, 0x7C, 0x08, 0x04, 0x04}, // 'r'
|
||||||
|
{0x48, 0x54, 0x54, 0x54, 0x20}, // 's'
|
||||||
|
{0x04, 0x04, 0x3F, 0x44, 0x44}, // 't'
|
||||||
|
{0x3C, 0x40, 0x40, 0x20, 0x7C}, // 'u'
|
||||||
|
{0x1C, 0x20, 0x40, 0x20, 0x1C}, // 'v'
|
||||||
|
{0x3C, 0x40, 0x30, 0x40, 0x3C}, // 'w'
|
||||||
|
{0x44, 0x28, 0x10, 0x28, 0x44}, // 'x'
|
||||||
|
{0x0C, 0x50, 0x50, 0x50, 0x3C}, // 'y'
|
||||||
|
{0x44, 0x64, 0x54, 0x4C, 0x44}, // 'z'
|
||||||
|
{0x00, 0x08, 0x36, 0x41, 0x41}, // '{'
|
||||||
|
{0x00, 0x00, 0x7F, 0x00, 0x00}, // '|'
|
||||||
|
{0x41, 0x41, 0x36, 0x08, 0x00}, // '}'
|
||||||
|
{0x02, 0x01, 0x02, 0x04, 0x02}, // '~'
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DS5_BRIDGE_OLED_FONT_H
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
// Flash-backed slot table for 4-slot persistent BT pairing.
|
||||||
|
// Modeled on zurce/DS5Dongle-OLED (bt.cpp:29-115). Credit to zurce.
|
||||||
|
|
||||||
|
#include "slots.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
#include "hardware/flash.h"
|
||||||
|
#include "hardware/sync.h"
|
||||||
|
|
||||||
|
constexpr uint32_t SLOTS_MAGIC = 0x44533502u; // "DS5\x02"
|
||||||
|
constexpr uint32_t SLOTS_FLASH_OFFSET = PICO_FLASH_SIZE_BYTES - 2u * FLASH_SECTOR_SIZE;
|
||||||
|
|
||||||
|
struct __attribute__((packed)) SlotsData {
|
||||||
|
uint32_t magic;
|
||||||
|
uint8_t addrs[kNumSlots][6];
|
||||||
|
uint8_t occupied[kNumSlots];
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(SlotsData) <= FLASH_PAGE_SIZE);
|
||||||
|
static_assert(SLOTS_FLASH_OFFSET % FLASH_SECTOR_SIZE == 0);
|
||||||
|
|
||||||
|
static SlotsData g_slots{};
|
||||||
|
|
||||||
|
static const SlotsData *flash_slots() {
|
||||||
|
return reinterpret_cast<const SlotsData *>(XIP_BASE + SLOTS_FLASH_OFFSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool save_slots_to_flash() {
|
||||||
|
alignas(4) uint8_t page[FLASH_PAGE_SIZE];
|
||||||
|
memset(page, 0xff, sizeof(page));
|
||||||
|
memcpy(page, &g_slots, sizeof(g_slots));
|
||||||
|
|
||||||
|
const uint32_t interrupts = save_and_disable_interrupts();
|
||||||
|
flash_range_erase(SLOTS_FLASH_OFFSET, FLASH_SECTOR_SIZE);
|
||||||
|
flash_range_program(SLOTS_FLASH_OFFSET, page, sizeof(page));
|
||||||
|
restore_interrupts(interrupts);
|
||||||
|
|
||||||
|
SlotsData verify{};
|
||||||
|
memcpy(&verify, flash_slots(), sizeof(verify));
|
||||||
|
if (memcmp(&verify, &g_slots, sizeof(g_slots)) == 0) {
|
||||||
|
printf("[Slots] flash write verified\n");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
printf("[Slots] flash write VERIFY FAILED\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void slots_load() {
|
||||||
|
memcpy(&g_slots, flash_slots(), sizeof(g_slots));
|
||||||
|
if (g_slots.magic != SLOTS_MAGIC) {
|
||||||
|
printf("[Slots] flash sector empty/invalid, initializing\n");
|
||||||
|
memset(&g_slots, 0, sizeof(g_slots));
|
||||||
|
g_slots.magic = SLOTS_MAGIC;
|
||||||
|
save_slots_to_flash();
|
||||||
|
}
|
||||||
|
for (int i = 0; i < kNumSlots; i++) {
|
||||||
|
if (g_slots.occupied[i]) {
|
||||||
|
printf("[Slots] %d: %02X:%02X:%02X:%02X:%02X:%02X\n", i,
|
||||||
|
g_slots.addrs[i][0], g_slots.addrs[i][1], g_slots.addrs[i][2],
|
||||||
|
g_slots.addrs[i][3], g_slots.addrs[i][4], g_slots.addrs[i][5]);
|
||||||
|
} else {
|
||||||
|
printf("[Slots] %d: (empty)\n", i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool slot_occupied(int slot) {
|
||||||
|
if (slot < 0 || slot >= kNumSlots) return false;
|
||||||
|
return g_slots.occupied[slot] != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void slot_get_addr(int slot, uint8_t out[6]) {
|
||||||
|
if (slot < 0 || slot >= kNumSlots) {
|
||||||
|
memset(out, 0, 6);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
memcpy(out, g_slots.addrs[slot], 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
int slot_owner_of(const uint8_t addr[6]) {
|
||||||
|
for (int i = 0; i < kNumSlots; i++) {
|
||||||
|
if (g_slots.occupied[i] && memcmp(g_slots.addrs[i], addr, 6) == 0) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void slot_assign(int slot, const uint8_t addr[6]) {
|
||||||
|
if (slot < 0 || slot >= kNumSlots) return;
|
||||||
|
memcpy(g_slots.addrs[slot], addr, 6);
|
||||||
|
g_slots.occupied[slot] = 1;
|
||||||
|
save_slots_to_flash();
|
||||||
|
}
|
||||||
|
|
||||||
|
void slot_forget(int slot) {
|
||||||
|
if (slot < 0 || slot >= kNumSlots) return;
|
||||||
|
memset(g_slots.addrs[slot], 0, 6);
|
||||||
|
g_slots.occupied[slot] = 0;
|
||||||
|
save_slots_to_flash();
|
||||||
|
}
|
||||||
|
|
||||||
|
void slots_wipe_all() {
|
||||||
|
for (int i = 0; i < kNumSlots; i++) {
|
||||||
|
memset(g_slots.addrs[i], 0, 6);
|
||||||
|
g_slots.occupied[i] = 0;
|
||||||
|
}
|
||||||
|
save_slots_to_flash();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool slots_any_empty() {
|
||||||
|
for (int i = 0; i < kNumSlots; i++) {
|
||||||
|
if (!g_slots.occupied[i]) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
// Persistent 4-slot multi-controller pairing storage. Stores the 4 bonded
|
||||||
|
// DS5 bd_addrs in a custom flash sector (BTstack's NVM keeps the link_keys).
|
||||||
|
//
|
||||||
|
// Multi-slot pairing modeled on zurce/DS5Dongle-OLED. Credit to zurce.
|
||||||
|
|
||||||
|
#ifndef DS5_BRIDGE_SLOTS_H
|
||||||
|
#define DS5_BRIDGE_SLOTS_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
constexpr int kNumSlots = 4;
|
||||||
|
|
||||||
|
void slots_load();
|
||||||
|
bool slot_occupied(int slot);
|
||||||
|
void slot_get_addr(int slot, uint8_t out[6]);
|
||||||
|
int slot_owner_of(const uint8_t addr[6]);
|
||||||
|
void slot_assign(int slot, const uint8_t addr[6]);
|
||||||
|
void slot_forget(int slot);
|
||||||
|
void slots_wipe_all();
|
||||||
|
bool slots_any_empty();
|
||||||
|
|
||||||
|
#endif // DS5_BRIDGE_SLOTS_H
|
||||||
+6
-5
@@ -67,11 +67,12 @@ static bool audio10_set_req_entity(tusb_control_request_t const *p_request, uint
|
|||||||
TU_VERIFY(p_request->wLength == 2);
|
TU_VERIFY(p_request->wLength == 2);
|
||||||
|
|
||||||
volume[index] = static_cast<float>(*reinterpret_cast<int16_t const *>(pBuff)) / 256;
|
volume[index] = static_cast<float>(*reinterpret_cast<int16_t const *>(pBuff)) / 256;
|
||||||
if (entityID == UAC1_ENTITY_SPK_FEATURE_UNIT) {
|
// Do NOT sync the USB-side UAC1 volume control into our
|
||||||
auto config = get_config();
|
// flash-persisted config. The host (PipeWire/Pulse) re-applies
|
||||||
config.speaker_volume = volume[index];
|
// its last-known UAC1 volume on every device reconnect, which
|
||||||
set_config(config);
|
// would silently override the user's saved speaker_volume.
|
||||||
}
|
// Fix borrowed from loteran/DS5Dongle commit 03fa1e4.
|
||||||
|
(void)entityID;
|
||||||
|
|
||||||
TU_LOG2(" Set Volume: %d dB of entity: %u\r\n", volume[index], entityID);
|
TU_LOG2(" Set Volume: %d dB of entity: %u\r\n", volume[index], entityID);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -102,7 +102,15 @@ tusb_desc_device_t desc_device =
|
|||||||
|
|
||||||
.iManufacturer = 0x01,
|
.iManufacturer = 0x01,
|
||||||
.iProduct = 0x02,
|
.iProduct = 0x02,
|
||||||
.iSerialNumber = 0x00,
|
// OLED Edition restores the per-board serial number that upstream stripped
|
||||||
|
// in commit e79c762 ("remove usb serialnumber #32"). Without it, Windows
|
||||||
|
// treats the dongle as a NEW device for each USB port (issue awalol#100):
|
||||||
|
// users lose their per-device volume / app preferences when they move
|
||||||
|
// ports. The trade-off (re-introducing SpecialK incompatibility from #32)
|
||||||
|
// affects only Windows users with that specific tool; the broader Windows
|
||||||
|
// population gets stable device identity back. The serial string itself
|
||||||
|
// is produced by board_usb_get_serial() from the flash chip's unique ID.
|
||||||
|
.iSerialNumber = STRID_SERIAL,
|
||||||
|
|
||||||
.bNumConfigurations = 0x01
|
.bNumConfigurations = 0x01
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user