Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc70bbd87c | ||
|
|
be3736c230 | ||
|
|
aef03e9633 | ||
|
|
cb7fffd795 | ||
|
|
9cabdca3f5 | ||
|
|
00da3d6a18 | ||
|
|
6482c79644 | ||
|
|
00822d6556 | ||
|
|
8d811f09cc | ||
|
|
36687ce76d | ||
|
|
5e5aa71cfe | ||
|
|
d090df6820 |
+11
-13
@@ -6,30 +6,28 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
## [0.6.12-oled-edition] — 2026-06-07
|
||||
|
||||
---
|
||||
> **Supersedes the withdrawn v0.6.11.** v0.6.11 bundled the two OLED quality-of-life features below with two regressions (constant haptics, issue #11; an audio retiming change, issue #12) and is not recommended. v0.6.12 is built on the verified-working native-trigger firmware and folds in **only** the two separable good features — `CtrlWake` and brightness persistence — leaving the trigger and audio code paths byte-identical to the tested build. The v0.6.11 regression changes (the `state_mgr.cpp` trigger-FFB allow-bit mirror and the `audio.cpp` resampler retiming) are intentionally **not** carried over.
|
||||
|
||||
## [0.6.11-oled-edition] — 2026-06-02
|
||||
Headline: **native DualSense adaptive triggers now fire in real PC games on Linux/Proton — through the dongle, 1:1 with a wired controller.** For weeks the triggers only ever worked in the on-dongle OLED self-test, never in a game: the dongle was *recognised*, but games fell back to a generic/Xbox pad and never sent trigger effects. The root cause was three ways the dongle's USB presentation differed from genuine Sony hardware — each one accepted by Linux's `hid_playstation` (which only checks size + CRC) but **rejected by a game's native DualSense detection, which validates the actual content**. Closing all three makes the dongle byte-for-byte indistinguishable from a real DS5 to the game.
|
||||
|
||||
Headline: **adaptive triggers now actually work through the dongle** (#6) — host trigger force-feedback was being forwarded with its enable bits cleared, so the DualSense silently discarded it; this is the fix. Also adds an opt-in **`CtrlWake`** setting so the OLED can sleep while you play (#8/#9), persists the **OLED brightness** choice across power cycles (#9), and ships an in-progress **speaker-audio retiming** pass aimed at the periodic crackle (#7). UF2s attached to [the GitHub release](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases/tag/v0.6.11-oled-edition) (built by `.github/workflows/release.yml`).
|
||||
**Host-side recipe (required alongside this firmware):** run the game on a Proton that carries the Wine `winebus.sys` **#9034** fix (Wine 11 / current Proton-GE — the bug suppresses the SDL gamepad device when a hidraw device exists for the same VID/PID, so the pad shows up everywhere *except* in-game) and **disable Steam Input** (native path). **No launch option is needed** — Wine 11 enables the hidraw native path by default. Works on **both Steam and Heroic** (on Heroic, fully quit Steam so it can't grab the pad, and keep any global `PROTON_PREFER_SDL` off). The game must natively support DualSense — XInput-only titles give rumble but no adaptive triggers. With a real DS5 this "just works"; the firmware changes below make the dongle match.
|
||||
|
||||
### Added
|
||||
|
||||
- **`CtrlWake` setting — let the OLED sleep while the controller is in use (issues #8, #9).** The idle power-ladder (dim → off) previously never fired during gameplay, because every controller input bumped the activity timer and kept the panel awake — so a configured dim/off timeout effectively did nothing while playing. New Settings item `CtrlWake` (default **on**, preserving the old behavior); set it **off** and controller input no longer wakes the screen — only the OLED's KEY0/KEY1 do — so the dim/off timers count down during play and the panel can sleep, requiring a button press to wake.
|
||||
- **Native adaptive triggers & haptics in games on Linux/Proton, through the dongle.** Games with native DualSense support (Cyberpunk 2077, etc.) now drive the controller's adaptive triggers via the dongle, identical to a directly-wired DualSense — verified 1:1 by A/B against the same controller plugged in over USB-C. No Steam Input, no per-game hacks beyond the host recipe above. The firmware already proxies the host's trigger output reports to the controller (unchanged); the missing piece was getting games to *recognise* the dongle as a genuine DS5 in the first place (below).
|
||||
- **`CtrlWake` setting — let the OLED sleep while you play.** New Settings toggle (default **on**, preserving the old behaviour). With it **off**, controller input no longer keeps the panel awake — only the OLED's own KEY0/KEY1 do — so the auto-dim / auto-off timers actually count down during gameplay and the screen can sleep while the controller is in active use. (folded from v0.6.11; issues #8/#9)
|
||||
- **OLED brightness now persists across a power cycle.** The KEY1-long-press brightness choice is saved to config and restored on boot, instead of resetting to full every power-on. (folded from v0.6.11; issue #9)
|
||||
|
||||
### Changed
|
||||
|
||||
- **Speaker audio path retimed onto a true 10 ms / 100 Hz grid (issue #7, experimental).** The old path resampled 512→480 samples and shipped a 480-sample Opus frame on the haptic-gated cadence (~every 10.667 ms ≈ 45 kHz into the DS5's free-running 48 kHz DAC), a ~6.25 % underrun that produced a periodic gap/crackle. The resampler is removed and the `0x36` frame now carries exactly one native 10 ms / 48 kHz Opus frame (`SAMPLE_SIZE` 64→60, 480-sample buffer), with the speaker sub-report offset computed from the haptic block length rather than hard-coded. This is an in-progress change aimed at the long-standing crackle; the audio path also carries debug counters (inert in the production UF2, visible only with `ENABLE_SERIAL`).
|
||||
- **`audio_buffer_length` now defaults to 16** (was 64) — a lower buffer avoids the DS5's periodic re-buffer gap. Existing saved configs keep their value; this only affects fresh flashes / Reset-to-defaults.
|
||||
- **`auto_haptics_enable` now defaults to Off** (was Fallback) — the speaker-derived rumble fallback could produce erratic haptics, so it is opt-in.
|
||||
- **OLED SPI flush is now chunked / non-blocking.** The framebuffer is sent in row-chunks across main-loop iterations instead of one ~1.1 ms blocking transfer, so the OLED no longer stalls `tud_task` / `audio_loop` / BTstack polling while it refreshes (the OLED-on side of the #7 audio distortion).
|
||||
- **Battery percentage shows the mid-point of each 10 % band** (5, 15, …, 95, 100 %), matching the kernel `hid-playstation` driver and Steam, instead of the band floor.
|
||||
- **The DualSense HID report descriptor is now byte-identical to a real DS5 (289 bytes, was 321).** The OLED Edition declared four extra feature reports — `0xF6`–`0xF9`, the WebHID config/remap IDs — inside the gamepad collection, making the descriptor 32 bytes longer than genuine hardware. Games' native DualSense parser rejects that mismatch and falls back to a generic pad (no triggers); `hid_playstation` didn't care, which is why the OLED self-test always worked but games never did. The four declarations are removed (plus the runtime `wDescriptorLength` patch that overrode the static array, `0x41`→`0x21`). The firmware **still handles** `0xF6`–`0xF9`; they're simply undeclared now and work over Linux `hidraw` exactly like the existing `0xFD` diagnostic report.
|
||||
- **Feature reports `0x09` (pairing), `0x20` (firmware) and `0x05` (calibration) now return the controller's real cached data, not zeros.** The earlier native-recognition fix answered these with zeros + a valid CRC — enough for `hid_playstation` to bind, but games validate the *content*: Cyberpunk GET-read `0x20`/`0x09` ~156× in a retry storm rejecting the zeros, never completed the DualSense handshake, and showed no controller. The dongle already fetches and caches the genuine reports from the connected controller over BT (`init_feature()`); `tud_hid_get_report_cb` now serves that cache (real firmware string / pairing / calibration), keeping the CRC-valid synthetic stub only as a fallback for the USB-enumeration probe before the BT link is up (so `hid_playstation` still binds then). Builds on the `hid_playstation` binding fix and the serial-=-MAC identity fix.
|
||||
|
||||
### Fixed
|
||||
### Known tradeoff
|
||||
|
||||
- **Adaptive trigger effects now work through the dongle (issue #6).** Host-sent trigger force-feedback was silently dropped: `state_update()` copied the 11-byte `RightTriggerFFB`/`LeftTriggerFFB` parameter blocks into the outgoing controller-state but never set the `AllowRightTriggerFFB`/`AllowLeftTriggerFFB` "apply" bits in byte 0, so the DualSense received the data with the enable flags cleared and discarded it. Affected **both** output paths (standalone `0x31` and the `0x36` audio-frame fold), which is why triggers felt absent through the dongle but worked on direct USB. The on-dongle Trigger Test screen always set those bits directly (`0x0C`), which is why it worked. Now the host's two allow-flags are mirrored into the state alongside the FFB data, like the rumble flags already were.
|
||||
- **OLED brightness now persists across a power cycle (issue #9).** The KEY1-long-press brightness level was a runtime-only value that reset to full on every boot; it is now stored in config and restored at init.
|
||||
- **The browser (WebHID) Config and Remap tabs no longer reach the dongle.** They drove config over reports `0xF6`/`0xF7`, which are no longer *declared* in the HID descriptor — and WebHID refuses undeclared report IDs (declaring them is exactly what broke game triggers). Mitigations, no firmware change needed: **on-dongle OLED config still works**, and because undeclared IDs are fine over Linux `hidraw`, a small `hidraw` CLI/script can read & write all of `0xF6`–`0xF9` (the same mechanism `scripts/mic_diag.sh` uses for `0xFD`). Restoring the *browser* tool would require moving the custom reports into a separate vendor HID collection — which risks the descriptor diverging from a real DS5 again, re-breaking triggers, so it's deliberately not done here.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -97,6 +97,8 @@ These are non-obvious from the code; they cost time when forgotten.
|
||||
- **Inactivity-disconnect uses `packet[3..12]`** in the L2CAP interrupt data path (`src/bt.cpp:l2cap_packet_handler`). It's looking at sticks and DPad/buttons to decide "idle." Don't touch those bytes' layout without updating the heuristic.
|
||||
- **The 0x36 BT audio packet layout is load-bearing — speaker + HD haptic actuators silently die without the SetStateData sub-report.** Upstream commit `3a31bd7` (May 2026, "refactor: add SetStateData and audio send priority") moved the `0x10` SetStateData block out of every audio frame and into a one-time L2CAP-open setup. The DualSense hardware requires that sub-report (specifically the `0x7f 0x7f` Headphones + Speaker volume bytes) at `pkt[11..75]` of every `0x36` frame, or the actuators stop producing output even though USB and BT byte counts look fine. Our fork keeps `state_data[63]` in `src/audio.cpp` and re-asserts it on every frame; the pre-3a31bd7 packet layout is: state_data at `pkt[11..75]`, haptic at `pkt[76..141]`, speaker format at `pkt[142]`, opus payload at `pkt[144..343]`. If you rebase onto an upstream that has the refactor and don't preserve this restoration, speaker + HD haptics silently break. The `scripts/test_speaker.sh` helper + the `USB aud / BT 0x32` counters on the OLED Diagnostics screen are the regression tripwire — if bytes are flowing but you hear nothing, look at packet contents not flow. Upstream PR #93 is tracking a proper unified fix; ours is the pre-refactor revert applied just to `audio.cpp`. Same fix was shipped independently by `loteran/DS5Dongle` (commit `c7a8d3c`).
|
||||
|
||||
- **The DualSense HID descriptor must stay byte-identical to a real DS5 (289 bytes), and `0x09`/`0x20`/`0x05` feature reports must return the controller's *real* cached data — or native game triggers silently break.** A game's native DualSense detection (Cyberpunk, etc.) validates *both* the descriptor shape and the feature-report *content*, then drives adaptive triggers over the native hidraw path. If either differs from genuine hardware the game falls back to a generic/Xbox pad and triggers never fire. Linux's `hid_playstation` is far more lenient (size + CRC only), so the dongle still *binds* and the on-dongle OLED trigger-test still works — which masked this break for weeks. Two load-bearing things: (1) **don't re-declare config reports `0xF6`–`0xF9`** in `desc_hid_report_ds` (or bump its `wDescriptorLength` runtime patch in `tud_descriptor_configuration_cb` back to `0x41`) — that's what made the descriptor 321 bytes and killed triggers; (2) keep `tud_hid_get_report_cb` serving the **real `init_feature()` cache** for `0x09`/`0x20`/`0x05`, with the CRC-valid synthetic stub only as the pre-BT-link probe fallback. `desc_hid_report_dse` (DSE mode) still declares F6-F9 and needs the same cut. Host side requires a Proton with the Wine `winebus.sys` #9034 fix + `PROTON_ENABLE_HIDRAW=0x054c/0x0ce6` + Steam Input off; diff a dongle vs real-DS5 `PROTON_LOG=+hid` capture to debug (a GET retry storm on `0x20`/`0x09` = the dongle's feature content is being rejected).
|
||||
|
||||
## Versioning — single source of truth
|
||||
|
||||
The release tag is the **only** place the version is written. Everything else flows from it:
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# DualSense adaptive-trigger games — owned + tested through the dongle
|
||||
|
||||
Cross-referenced across Steam + Epic + GOG (~1,271 owned games). Native adaptive triggers work
|
||||
on **both Steam and Heroic**, flag-free, with **runner = GE-Proton-DualSense (Wine 11)**. Verified
|
||||
2026-06-07 by running Cyberpunk on *both* launchers — identical trigger behaviour.
|
||||
|
||||
- **Steam:** GE-Proton-DualSense + **Steam Input off**. No launch option needed (Wine 11 enables
|
||||
the hidraw native path by default).
|
||||
- **Heroic (Epic / GOG):** GE-Proton-DualSense + **Steam fully quit** + **no `PROTON_PREFER_SDL`**.
|
||||
No launch option needed. (Earlier belief that Heroic needs `PROTON_ENABLE_HIDRAW` was wrong —
|
||||
it was masked by the two gotchas below.)
|
||||
|
||||
### ⚠️ Linux gotchas that masquerade as "the dongle is broken" (all host-side)
|
||||
|
||||
- **Steam running in the background** grabs the pad from non-Steam (Heroic) games. Fully quit Steam,
|
||||
or disable Settings → Controller → PlayStation controller support.
|
||||
- **A global `PROTON_PREFER_SDL=1`** forces the SDL/Xbox path and *suppresses* native triggers. Keep
|
||||
it off (or per-game only). It's how you get a generic Xbox pad when a game lacks native DualSense.
|
||||
- **XInput-only games can't do adaptive triggers — period.** XInput has no trigger-resistance API, so
|
||||
titles that read the pad via XInput (e.g. Ghostrunner, Control on PC) give rumble only, on any OS,
|
||||
through any tool. Not a dongle limit.
|
||||
|
||||
Legend: ✅ = confirmed working **flag-free** through the dongle (this session, 2026-06-07).
|
||||
|
||||
## 🎯 Full adaptive triggers
|
||||
|
||||
| Game | Where you own it | Status |
|
||||
|---|---|---|
|
||||
| Cyberpunk 2077 | Steam · GOG | ✅ tested flag-free — **confirmed on BOTH Steam and Heroic/GOG** |
|
||||
| The Last of Us Part I | Steam | ✅ tested flag-free |
|
||||
| Marvel's Spider-Man Remastered | Steam | ✅ tested flag-free |
|
||||
| Uncharted: Legacy of Thieves Collection | Steam | ✅ tested flag-free |
|
||||
| Ghost of Tsushima Director's Cut | Steam | recognized — bow draw 🏹 (earlier) |
|
||||
| Hogwarts Legacy | Steam · Epic | ✅ tested flag-free (native recog + rumble; triggers moderate, in spellcasting) |
|
||||
| Ghostrunner | Epic · GOG | ⚠️ XInput-only on PC — rumble, **no adaptive triggers** (not the dongle) |
|
||||
| Avatar: Frontiers of Pandora | Steam | ✅ tested flag-free |
|
||||
| Assassin's Creed Shadows | Steam | |
|
||||
| Indiana Jones and the Great Circle | Steam | ✅ tested flag-free |
|
||||
| Star Wars Jedi: Fallen Order | Steam | |
|
||||
| LEGO Star Wars: The Skywalker Saga | Steam | |
|
||||
| Metro Exodus (Enhanced) | Steam | |
|
||||
| Marvel's Guardians of the Galaxy | Epic | |
|
||||
| F.I.S.T.: Forged In Shadow Torch | Epic | |
|
||||
| Dakar Desert Rally | Epic | racing-trigger feel |
|
||||
|
||||
## 〰️ Lighter / haptics-leaning (subtle trigger use)
|
||||
|
||||
- Baldur's Gate 3 — Steam — ✅ great experience; haptics-forward, light trigger use (dice rolls / ranged)
|
||||
- Sifu — Epic — haptics only; combat is O/Triangle face-buttons, **no adaptive triggers** (not a trigger test)
|
||||
- The Witcher 3: Wild Hunt (next-gen) — Steam · GOG
|
||||
- Control Ultimate Edition — GOG — XInput-only on PC (rumble, no adaptive triggers)
|
||||
- Forza Horizon 5 — Steam
|
||||
- Hellblade: Senua's Sacrifice — Steam
|
||||
- Star Wars: Squadrons — Epic
|
||||
- Maneater — Epic
|
||||
|
||||
**Caveats:** matched the *well-known* trigger titles across a 600+ game library — there may be a
|
||||
few more not flagged. ~18 games with real triggers, all owned, all working through the dongle.
|
||||
@@ -17,12 +17,14 @@ The web tool is a one-stop shop — **no installs, no command line, no `picotool
|
||||
- **Flash Firmware tab** — put the Pico in **BOOTSEL mode**, then click *Connect to Pico* in the browser and *Flash now*. The site bundles the latest release UF2, or you can load a local `.uf2` you've built yourself. Powered by WebUSB.
|
||||
|
||||
> **What is BOOTSEL mode?** It's the Pico's built-in flashing mode. To enter it: press and hold the small white button labeled **BOOTSEL** on the Pico, *then* plug the USB cable in (or, if it's already plugged in, briefly disconnect and reconnect while holding BOOTSEL). The Pico will appear to your computer as a removable drive — that's how you know it's in BOOTSEL mode. After the web tool flashes the firmware, the Pico auto-reboots into normal mode and is ready to use.
|
||||
- **Config tab** — once the dongle is flashed and reconnected, edit haptics gain, speaker volume, polling rate, audio auto-haptics mode, and the rest of the persistent settings; save to the dongle's flash with one click. Powered by WebHID.
|
||||
- **Remap tab** — visual button remapper: click a button on a live DualSense diagram to reassign it to any other (the shoulders/triggers float to the corners as labeled glyphs). The map is stored on the dongle and applied before the host sees the report, so it works in every game and on every OS. Powered by WebHID.
|
||||
- **Config tab** — once the dongle is flashed and reconnected, edit haptics gain, speaker volume, polling rate, audio auto-haptics mode, and the rest of the persistent settings; save to the dongle's flash with one click. Powered by WebHID. **⚠️ Does not work on the native-trigger firmware** — see the note below.
|
||||
- **Remap tab** — visual button remapper: click a button on a live DualSense diagram to reassign it to any other (the shoulders/triggers float to the corners as labeled glyphs). The map is stored on the dongle and applied before the host sees the report, so it works in every game and on every OS. Powered by WebHID. **⚠️ Does not work on the native-trigger firmware** — see the note below.
|
||||
- **OLED Preview tab** — pixel-perfect emulation of all 11 OLED screens. Use the in-page KEY0/KEY1 buttons (or the controller's △ / R1 / D-pad when a DualSense is paired) to navigate. Adaptive triggers actually fire on the controller when you cycle the Trigger Test preset.
|
||||
|
||||
Works in any Chromium-based browser (Chrome, Edge, Brave, Opera). Firefox + Safari don't expose WebHID or WebUSB, so flashing and live config aren't available there — the OLED Preview still renders with mock data.
|
||||
|
||||
> **⚠️ Native-trigger firmware (Unreleased) and the WebHID tabs.** To make games on Linux/Proton recognise the dongle as a genuine DualSense and fire **native adaptive triggers** (see [CHANGELOG.md](./CHANGELOG.md)), the firmware's HID descriptor is now byte-identical to a real DS5 — which meant *un-declaring* the `0xF6`/`0xF7` config reports the browser **Config** and **Remap** tabs depend on. On that firmware those two tabs can't reach the dongle (WebHID refuses undeclared report IDs). **Flash Firmware and OLED Preview still work**, on-dongle OLED config still works, and the same settings can be driven over Linux `hidraw`. Earlier releases are unaffected.
|
||||
|
||||
> Source for the web tool: **[MarcelineVPQ/DS5Dongle-OLED-Config-Web](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Config-Web)** (fork of [awalol/ds5dongle-config-web](https://github.com/awalol/ds5dongle-config-web)).
|
||||
|
||||
---
|
||||
@@ -43,6 +45,7 @@ This project enables the Raspberry Pi Pico2W to function as a Bluetooth bridge f
|
||||
|
||||
**OLED Edition additions:**
|
||||
|
||||
- **Native DualSense adaptive triggers in PC games on Linux/Proton — through the dongle.** Games with native DualSense support drive the controller's adaptive triggers wirelessly via the dongle, **1:1 with a directly-wired DualSense**. Requires a Proton carrying the Wine `winebus.sys` #9034 fix (Wine 11 / current Proton-GE) with Steam Input disabled — **no launch option needed** (Wine 11 enables the hidraw native path by default). Works on **both Steam and Heroic** (Epic/GOG). The firmware makes the dongle byte-for-byte indistinguishable from a real DS5 so the game accepts it (full write-up in [CHANGELOG.md](./CHANGELOG.md)). Trade-off: the browser Config/Remap tabs are disabled on this firmware (see the note above).
|
||||
- Optional Pico-OLED-1.3 status display with **11 screens** (status, slots, lightbar, trigger test, gyro tilt, touchpad, diagnostics, CPU/clock, RSSI, VU meters, settings)
|
||||
- **Button remapping** — reassign any of the 16 digital controls (face buttons, D-pad, shoulders/triggers, stick clicks, Create/Options) to any other. Stored on the dongle and applied before the host sees the report, so it works in **every game and OS with no host-side software**; identity (no remap) is the default. Edit it visually in the [web config tool](#️-web-config-tool)'s **Remap tab**, or headlessly via `scripts/remap_test.py`. Persisted in its own flash sector, survives reboot.
|
||||
- **4-slot persistent multi-controller pairing** — bond up to four DualSenses, switch between them from the OLED, slot 0 reconnects automatically on boot
|
||||
@@ -52,6 +55,44 @@ This project enables the Raspberry Pi Pico2W to function as a Bluetooth bridge f
|
||||
- **Soft-reboot** without unplugging USB via DS5 `PS + Mute` hold (works headless) or **KEY0 + KEY1 held together for 1 s** on the OLED add-on (replaces the older KEY0 double-click gesture, which was easy to fire by accident while paging quickly)
|
||||
- **Audit pass on the core bridge** — critical stack-overflow fix in the audio path (resolves long-standing "audio stuttering"), security hardening, watchdog, length validation across HID/L2CAP boundaries (see [CHANGELOG.md](./CHANGELOG.md))
|
||||
|
||||
## 🐧 Linux: native adaptive triggers (Proton)
|
||||
|
||||
Getting a game to drive the controller's **adaptive triggers through the dongle** on Linux needs a Proton that carries the Wine `winebus.sys` **#9034** fix. **As of this writing no official Proton-GE release includes it yet** — it's merged upstream but unreleased, and `GE-Proton10-34` (the current release) and earlier do **not** have it. So a build that does is provided: **`GE-Proton-DualSense`** (see [Releases](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases)). Once an official GE-Proton ships the fix, that will work too.
|
||||
|
||||
> Why: the #9034 bug suppresses the SDL gamepad device when a hidraw device exists for the same VID/PID, so the pad is seen *everywhere except in-game*. The fix lets the native (trigger-capable) path and a working gamepad coexist.
|
||||
|
||||
**One-time setup**
|
||||
|
||||
1. **Install the Proton build.** Download `GE-Proton-DualSense.tar.gz` and extract it to:
|
||||
- **Steam:** `~/.steam/root/compatibilitytools.d/`
|
||||
- **Heroic:** `~/.config/heroic/tools/proton/`
|
||||
|
||||
Then **fully restart Steam** so it detects the new compatibility tool.
|
||||
2. **Disable Steam Input** so the game talks to the controller natively — Steam → game **Properties → Controller → Disable Steam Input** (or globally: Settings → Controller → turn off PlayStation Controller support).
|
||||
|
||||
**Per game**
|
||||
|
||||
3. **Force the Proton:** Steam → right-click the game → **Properties → Compatibility → Force the use of a specific Steam Play compatibility tool → `GE-Proton-DualSense`**. (Heroic: set the game's *Wine version* to it.)
|
||||
|
||||
**No launch option is needed** — Wine 11 enables the hidraw native path by default, so the dongle is handed to the game automatically. Just launch — native adaptive triggers fire through the dongle, 1:1 with a wired DualSense (verified flag-free on Cyberpunk 2077, Uncharted, Spider-Man Remastered, The Last of Us Part I, Avatar, Indiana Jones — and on **both Steam and Heroic**). The game must have **native DualSense support** (XInput-only games give rumble but no adaptive triggers — that's an engine limit, not the dongle). First launch under a new Proton does locale/prefix setup and can sit "Not Responding" for a minute — that's setup, not a crash; let it finish.
|
||||
|
||||
### Heroic (Epic / GOG) — same recipe, two extra requirements
|
||||
|
||||
Native triggers work identically on Heroic, but non-Steam launchers are sensitive to two host-side things that silently steal the controller:
|
||||
|
||||
1. **Fully quit Steam** — a background Steam grabs the DualSense from non-Steam games (or disable Settings → Controller → PlayStation controller support).
|
||||
2. **No global `PROTON_PREFER_SDL`** — that env var forces the SDL/Xbox path and *suppresses* native triggers. Use it per-game only, as a deliberate generic-pad fallback.
|
||||
|
||||
**Launch-option reference**
|
||||
|
||||
| Variable | Value | Purpose |
|
||||
|---|---|---|
|
||||
| `PROTON_ENABLE_HIDRAW` | `0x054c/0x0ce6` | *Legacy / optional — **not needed** on Wine 11*, which enables the hidraw native path by default. Harmless if set. Only relevant on older Proton where hidraw was opt-in (but those also lack the #9034 fix, so triggers won't work there anyway). |
|
||||
| `PROTON_PREFER_SDL` | `1` | *Alternative, input-only.* Forces the SDL gamepad path — gives working input + rumble but as a generic/Xbox pad (**no triggers**). Use only when a game lacks native DualSense support and `ENABLE_HIDRAW` doesn't help. Do **not** combine with `ENABLE_HIDRAW` for trigger games. |
|
||||
| `PROTON_LOG` | `+hid` | *Diagnostic only* (not for normal play). Writes a HID enumeration trace — Steam lands it at `~/steam-<appid>.log`; Heroic sets `PROTON_LOG_DIR` to your home, so it lands at `~/steam-0.log`. Useful for confirming the game opened the native HID path. |
|
||||
|
||||
> **Heroic vs Steam:** Native triggers work on **both** — and neither needs a launch option on Wine 11. Steam: set the runner + disable Steam Input. Heroic: set the runner, **fully quit Steam** (a background Steam steals the pad from non-Steam games), and keep `PROTON_PREFER_SDL` off. Either way the game must natively support DualSense — XInput-only games (e.g. Ghostrunner, Control) give rumble but no adaptive triggers, on any OS.
|
||||
|
||||
## Hardware
|
||||
|
||||
### Required
|
||||
@@ -388,6 +429,7 @@ Some features and design ideas in this fork are borrowed from other forks of ups
|
||||
|
||||
- **[zurce/DS5Dongle-OLED](https://github.com/zurce/DS5Dongle-OLED)** — pixel-art icons in the OLED status header (visual approach), the "hold for factory reset" UX pattern used by the Settings screen's "Reset to defaults" item (hold △ for 2 s to confirm), and the multi-slot persistent BT pairing system on the new Slots screen (4 bonded controllers, D-pad to navigate, △ to switch slots, □ hold to wipe a slot, plus "Wipe all slots" in the Settings menu).
|
||||
- **[loteran/DS5Dongle](https://github.com/loteran/DS5Dongle)** — independent rediscovery of the upstream `3a31bd7` regression that broke speaker / HD haptic output (commit `c7a8d3c`); the fix in our `src/audio.cpp` restores the same SetStateData sub-report. Also the source of the Audio Auto Haptics DSP (1-pole LP + envelope follower, see Settings → Auto Haptics) and the "don't sync USB-side UAC1 volume to the persistent config" fix.
|
||||
- **[SundayMoments/DS5_Bridge](https://github.com/SundayMoments/DS5_Bridge)** — a sibling Pico DualSense bridge. The button-remapping apply logic and digital-control set in `src/remap.{h,cpp}` are ported from it. Its audio architecture (on-device Opus encoding vs an optional host-encoding companion app) was also the reference for diagnosing our speaker-cadence ("warble") work. Thanks for the groundwork.
|
||||
- **[awalol/ds5dongle-config-web](https://github.com/awalol/ds5dongle-config-web)** — base for our forked web config app at [MarcelineVPQ/DS5Dongle-OLED-Config-Web](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Config-Web). The fork adapts upstream's 13-byte Config_body layout to our 19-byte one and adds UI for our additions (multi-slot pairing, Auto Haptics).
|
||||
|
||||
## Roadmap
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Decode raw Opus frames captured from firmware serial output.
|
||||
|
||||
Reads [OPUS_FRAME_N] hex lines from stdin or a file, decodes each frame
|
||||
with libopus, and writes the result as a WAV file + prints a summary
|
||||
(peak amplitude, zero-crossing rate) to diagnose encoder output quality.
|
||||
"""
|
||||
import ctypes
|
||||
import struct
|
||||
import sys
|
||||
import wave
|
||||
import re
|
||||
|
||||
lib = ctypes.cdll.LoadLibrary("libopus.so.0")
|
||||
|
||||
SAMPLE_RATE = 48000
|
||||
CHANNELS = 2
|
||||
FRAME_SIZE = 480 # 10ms at 48kHz
|
||||
|
||||
# Create decoder
|
||||
err = ctypes.c_int(0)
|
||||
decoder = lib.opus_decoder_create(SAMPLE_RATE, CHANNELS, ctypes.byref(err))
|
||||
if err.value != 0:
|
||||
print(f"opus_decoder_create failed: {err.value}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
infile = sys.argv[1] if len(sys.argv) > 1 else "/tmp/ds5_opus.log"
|
||||
with open(infile) as f:
|
||||
lines = f.readlines()
|
||||
|
||||
frames = []
|
||||
for line in lines:
|
||||
m = re.match(r'\[OPUS_FRAME_\d+\]\s+([0-9a-fA-F]+)', line.strip())
|
||||
if m:
|
||||
frames.append(bytes.fromhex(m.group(1)))
|
||||
|
||||
if not frames:
|
||||
print("No [OPUS_FRAME_N] lines found in input.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Found {len(frames)} Opus frames, decoding...")
|
||||
|
||||
all_pcm = b""
|
||||
for i, frame_data in enumerate(frames):
|
||||
pcm = (ctypes.c_int16 * (FRAME_SIZE * CHANNELS))()
|
||||
ret = lib.opus_decode(
|
||||
decoder,
|
||||
frame_data, len(frame_data),
|
||||
pcm, FRAME_SIZE,
|
||||
0 # no FEC
|
||||
)
|
||||
if ret < 0:
|
||||
errstr = lib.opus_strerror(ret)
|
||||
print(f" Frame {i+1}: DECODE ERROR {ret} ({ctypes.string_at(errstr).decode()})")
|
||||
continue
|
||||
|
||||
samples = list(pcm)
|
||||
peak = max(abs(s) for s in samples)
|
||||
nonzero = sum(1 for s in samples if s != 0)
|
||||
print(f" Frame {i+1}: {ret} samples decoded, peak={peak}, nonzero={nonzero}/{len(samples)}")
|
||||
print(f" TOC=0x{frame_data[0]:02x} first 8 bytes: {frame_data[:8].hex()}")
|
||||
all_pcm += struct.pack(f"<{ret * CHANNELS}h", *samples[:ret * CHANNELS])
|
||||
|
||||
outpath = "/tmp/ds5_opus_decoded.wav"
|
||||
with wave.open(outpath, "w") as wf:
|
||||
wf.setnchannels(CHANNELS)
|
||||
wf.setsampwidth(2)
|
||||
wf.setframerate(SAMPLE_RATE)
|
||||
wf.writeframes(all_pcm)
|
||||
|
||||
print(f"\nDecoded audio written to {outpath}")
|
||||
print(f"Play with: aplay {outpath}")
|
||||
print(f"View spectrogram: sox {outpath} -n spectrogram -o /tmp/ds5_opus_spectrogram.png")
|
||||
@@ -1,146 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Send a 440 Hz sine to the DualSense speaker over Bluetooth from a Linux host
|
||||
(Raspberry Pi), via raw /dev/hidraw writes — replicating the DS5Dongle firmware's
|
||||
0x36 audio report BYTE-FOR-BYTE (same Opus settings, same SetStateData, same CRC).
|
||||
|
||||
Purpose: test whether a NON-tunneled BT path (Pi's BlueZ over a real UART) delivers
|
||||
clean audio to the DS5 speaker, vs the Pico 2 W's BT-over-gSPI tunnel. If clean here,
|
||||
the Pico's tunnel is the crackle culprit.
|
||||
|
||||
Pair the DS5 first (bluetoothctl), then run as root (hidraw needs RW):
|
||||
sudo python3 pi_ds5_audio_test.py [seconds] [--headset] [--audbuf N]
|
||||
"""
|
||||
import ctypes, struct, math, sys, os, glob, time
|
||||
|
||||
# ---- libopus via ctypes (same loader style as decode_opus_dump.py) ----
|
||||
opus = ctypes.cdll.LoadLibrary("libopus.so.0")
|
||||
opus.opus_encoder_create.restype = ctypes.c_void_p
|
||||
opus.opus_encoder_create.argtypes = [ctypes.c_int32, ctypes.c_int, ctypes.c_int,
|
||||
ctypes.POINTER(ctypes.c_int)]
|
||||
opus.opus_encode_float.restype = ctypes.c_int32
|
||||
opus.opus_encode_float.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_float),
|
||||
ctypes.c_int, ctypes.POINTER(ctypes.c_ubyte),
|
||||
ctypes.c_int32]
|
||||
# opus_encoder_ctl is variadic; leave argtypes unset and pass c_int values.
|
||||
OPUS_APPLICATION_AUDIO = 2049
|
||||
OPUS_SET_BITRATE_REQUEST = 4002
|
||||
OPUS_SET_VBR_REQUEST = 4006
|
||||
OPUS_SET_COMPLEXITY_REQUEST = 4010
|
||||
|
||||
RATE, CH, FRAME, OPUS_BYTES = 48000, 2, 480, 200 # 480 = 10 ms; 200 B = 160 kbps CBR
|
||||
|
||||
def make_encoder():
|
||||
err = ctypes.c_int(0)
|
||||
enc = opus.opus_encoder_create(RATE, CH, OPUS_APPLICATION_AUDIO, ctypes.byref(err))
|
||||
if err.value != 0 or not enc:
|
||||
sys.exit(f"opus_encoder_create failed: {err.value}")
|
||||
enc_p = ctypes.c_void_p(enc)
|
||||
# match firmware core1_entry(): 160 kbps, CBR, complexity 0
|
||||
opus.opus_encoder_ctl(enc_p, OPUS_SET_BITRATE_REQUEST, ctypes.c_int(OPUS_BYTES*8*100))
|
||||
opus.opus_encoder_ctl(enc_p, OPUS_SET_VBR_REQUEST, ctypes.c_int(0))
|
||||
opus.opus_encoder_ctl(enc_p, OPUS_SET_COMPLEXITY_REQUEST, ctypes.c_int(0))
|
||||
return enc_p
|
||||
|
||||
# ---- 63-byte SetStateData, verbatim from firmware src/state_mgr.cpp ----
|
||||
STATE_DATA = bytes([
|
||||
0xfd, 0xf7, 0x00, 0x00,
|
||||
0x7f, 0x64, # VolHeadphonesMax, VolSpeaker
|
||||
0x40, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a,
|
||||
0x07, 0x00, 0x00, 0x02, 0x01,
|
||||
0x00,
|
||||
0xff, 0xd7, 0x00, # RGB
|
||||
]) + bytes(16) # trailing zeros -> 63 total
|
||||
assert len(STATE_DATA) == 63
|
||||
|
||||
# ---- CRC-32, 0xA2-seeded, verbatim from firmware src/utils.h ----
|
||||
def ds_crc32(data):
|
||||
crc = (~0xEADA2D49) & 0xFFFFFFFF
|
||||
for b in data:
|
||||
crc ^= b
|
||||
for _ in range(8):
|
||||
if crc & 1: crc = (crc >> 1) ^ 0xEDB88320
|
||||
else: crc >>= 1
|
||||
return (~crc) & 0xFFFFFFFF
|
||||
|
||||
REPORT_SIZE, SAMPLE_SIZE = 398, 64 # original firmware layout
|
||||
|
||||
def build_packet(payload, seq, counter, headset, audbuf):
|
||||
pkt = bytearray(REPORT_SIZE)
|
||||
pkt[0] = 0x36
|
||||
pkt[1] = (seq & 0x0F) << 4
|
||||
pkt[2] = 0x11 | 0x80 # 0x91
|
||||
pkt[3] = 7
|
||||
pkt[4] = 0xFE # audio enable, mic OFF
|
||||
for i in range(5, 10): pkt[i] = audbuf & 0xFF
|
||||
pkt[10] = counter & 0xFF
|
||||
pkt[11] = 0x10 | 0x80 # 0x90 SetStateData
|
||||
pkt[12] = 63
|
||||
pkt[13:76] = STATE_DATA
|
||||
pkt[76] = 0x12 | 0x80 # 0x92 haptic
|
||||
pkt[77] = SAMPLE_SIZE # haptic data pkt[78..141] = 0 (silent)
|
||||
pkt[142] = (0x16 if headset else 0x13) | 0x80 # 0x96 headset / 0x93 speaker
|
||||
pkt[143] = OPUS_BYTES
|
||||
pkt[144:144+OPUS_BYTES] = payload
|
||||
struct.pack_into("<I", pkt, REPORT_SIZE-4, ds_crc32(pkt[:REPORT_SIZE-4]))
|
||||
return bytes(pkt)
|
||||
|
||||
def find_ds5_hidraw():
|
||||
for ue in glob.glob("/sys/class/hidraw/hidraw*/device/uevent"):
|
||||
txt = open(ue).read().upper()
|
||||
if "054C" in txt and ("0CE6" in txt or "0DF2" in txt):
|
||||
return "/dev/" + ue.split("/")[4]
|
||||
return None
|
||||
|
||||
def main():
|
||||
headset = "--headset" in sys.argv
|
||||
audbuf = int(sys.argv[sys.argv.index("--audbuf")+1]) if "--audbuf" in sys.argv else 64
|
||||
pos = [a for a in sys.argv[1:] if not a.startswith("--") and a.isdigit()]
|
||||
secs = int(pos[0]) if pos else 15
|
||||
|
||||
node = find_ds5_hidraw()
|
||||
if not node:
|
||||
sys.exit("DualSense hidraw node not found — paired & connected? (bluetoothctl)")
|
||||
print(f"DS5 hidraw: {node} | output: {'HEADSET' if headset else 'SPEAKER'} | "
|
||||
f"AudBuf={audbuf} | {secs}s")
|
||||
|
||||
enc = make_encoder()
|
||||
pcm = (ctypes.c_float * (FRAME*CH))()
|
||||
out = (ctypes.c_ubyte * OPUS_BYTES)()
|
||||
|
||||
# Pre-build every packet so the timed send loop is pure I/O (no compute jitter).
|
||||
n_frames = secs * 100
|
||||
packets, seq, counter, samp = [], 0, 0, 0
|
||||
first_nb = None
|
||||
for _ in range(n_frames):
|
||||
for i in range(FRAME):
|
||||
v = 0.5 * math.sin(2*math.pi*440.0*samp/RATE)
|
||||
pcm[i*2] = v; pcm[i*2+1] = v; samp += 1
|
||||
nb = opus.opus_encode_float(enc, pcm, FRAME, out, OPUS_BYTES)
|
||||
if nb < 0: sys.exit(f"opus_encode_float error {nb}")
|
||||
if first_nb is None: first_nb = nb
|
||||
payload = bytes(out[:nb]) + bytes(OPUS_BYTES - nb) if nb < OPUS_BYTES else bytes(out[:OPUS_BYTES])
|
||||
packets.append(build_packet(payload, seq, counter, headset, audbuf))
|
||||
seq = (seq+1) & 0x0F; counter = (counter+1) & 0xFF
|
||||
print(f"Encoded {len(packets)} frames (first frame {first_nb} B — expect ~200 for CBR). Streaming...")
|
||||
|
||||
fd = os.open(node, os.O_RDWR)
|
||||
short = 0
|
||||
t0 = time.monotonic()
|
||||
for n, pkt in enumerate(packets):
|
||||
try:
|
||||
w = os.write(fd, pkt)
|
||||
except OSError as e:
|
||||
print(f"write failed at frame {n}: {e}"); break
|
||||
if w != REPORT_SIZE: short += 1
|
||||
target = t0 + (n+1)*0.01 # 10 ms grid -> 100 pkt/s
|
||||
dt = target - time.monotonic()
|
||||
if dt > 0: time.sleep(dt)
|
||||
os.close(fd)
|
||||
print(f"Done — {n+1} frames sent (~{(n+1)/100:.1f}s).", end="")
|
||||
print(f" WARNING: {short} short writes (hidraw truncating the 398 B report!)" if short else " All writes full-length.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,45 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Send a 440 Hz sine wave on channels 1+2 only (speaker), silence on 3+4 (haptic).
|
||||
|
||||
Usage: python3 scripts/sine_ch12.py [seconds]
|
||||
"""
|
||||
import subprocess, struct, math, sys, re
|
||||
|
||||
RATE = 48000
|
||||
FREQ = 440
|
||||
DURATION = int(sys.argv[1]) if len(sys.argv) > 1 else 5
|
||||
CHANNELS = 4
|
||||
SAMPLES = RATE * DURATION
|
||||
|
||||
# Generate 4-channel S16_LE: sine on ch1+ch2, silence on ch3+ch4
|
||||
data = bytearray()
|
||||
for i in range(SAMPLES):
|
||||
val = int(6000 * math.sin(2 * math.pi * FREQ * i / RATE)) # ~-15 dBFS (was 32767 / 0 dBFS) — clipping test
|
||||
s = struct.pack('<h', val)
|
||||
data += s + s + b'\x00\x00' + b'\x00\x00' # L, R, hapL=0, hapR=0
|
||||
|
||||
# Auto-detect the dongle's ALSA card — it enumerates as "DualSense Wireless
|
||||
# Controller", and the card number shifts across reboots, so never hardcode it.
|
||||
def find_dualsense_card():
|
||||
out = subprocess.check_output(['aplay', '-l'], text=True)
|
||||
for line in out.splitlines():
|
||||
m = re.match(r'card (\d+):', line)
|
||||
if m and 'DualSense' in line:
|
||||
return int(m.group(1))
|
||||
return None
|
||||
|
||||
card = find_dualsense_card()
|
||||
if card is None:
|
||||
print("DualSense dongle not found in `aplay -l` — paired and enumerated?", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
device = f'hw:{card},0'
|
||||
print(f"Playing to {device} (DualSense dongle)")
|
||||
|
||||
proc = subprocess.Popen(
|
||||
['aplay', '-D', device, '-f', 'S16_LE', '-c', '4', '-r', '48000', '-'],
|
||||
stdin=subprocess.PIPE
|
||||
)
|
||||
proc.stdin.write(data)
|
||||
proc.stdin.close()
|
||||
proc.wait()
|
||||
print(f"Played {DURATION}s of {FREQ} Hz sine on ch1+ch2 (speaker only)")
|
||||
+25
-51
@@ -20,7 +20,7 @@
|
||||
|
||||
#define INPUT_CHANNELS 4
|
||||
#define OUTPUT_CHANNELS 2
|
||||
#define SAMPLE_SIZE 60 // 60 B = 30 haptic frames @ 3 kHz = 10.00 ms/packet → true 100 Hz cadence
|
||||
#define SAMPLE_SIZE 64
|
||||
#define REPORT_SIZE 398
|
||||
#define REPORT_ID 0x36
|
||||
// #define VOLUME_GAIN 2
|
||||
@@ -80,7 +80,7 @@ static volatile uint32_t g_mic_plc_frames = 0; // concealed frames genera
|
||||
uint32_t audio_mic_plc_frames() { return g_mic_plc_frames; }
|
||||
|
||||
struct audio_raw_element {
|
||||
float data[480 * 2]; // exactly one 10 ms Opus frame (480 stereo samples)
|
||||
float data[512 * 2];
|
||||
};
|
||||
|
||||
void set_headset(bool state) {
|
||||
@@ -96,10 +96,6 @@ uint32_t opus_fifo_drops() { return 0; }
|
||||
// emulator's USB / BT rate display. Updated below.
|
||||
static volatile uint32_t g_usb_frames = 0;
|
||||
static volatile uint32_t g_bt_packets = 0;
|
||||
static volatile int32_t g_opus_last_ret = 0;
|
||||
static volatile uint32_t g_fifo_drops = 0;
|
||||
static volatile uint32_t g_opus_encodes = 0;
|
||||
static volatile bool g_opus_ready = false;
|
||||
uint32_t audio_usb_frames() { return g_usb_frames; }
|
||||
uint32_t audio_bt_packets() { return g_bt_packets; }
|
||||
|
||||
@@ -262,7 +258,7 @@ void audio_loop() {
|
||||
}
|
||||
g_usb_frames += (uint32_t)frames;
|
||||
|
||||
static float audio_buf[480 * 2];
|
||||
static float audio_buf[512 * 2];
|
||||
static uint audio_buf_pos = 0;
|
||||
// 2. 从4ch中提取ch3/ch4,转换为float输入重采样器
|
||||
WDL_ResampleSample *in_buf;
|
||||
@@ -310,12 +306,11 @@ void audio_loop() {
|
||||
#if !DISABLE_SPEAKER_PROC
|
||||
audio_buf[audio_buf_pos++] = raw[i * INPUT_CHANNELS] / 32768.0f * audio_gain;
|
||||
audio_buf[audio_buf_pos++] = raw[i * INPUT_CHANNELS + 1] / 32768.0f * audio_gain;
|
||||
if (audio_buf_pos == 480 * 2) {
|
||||
if (audio_buf_pos == 512 * 2) {
|
||||
static audio_raw_element element{};
|
||||
memcpy(element.data, audio_buf, 480 * 2 * 4);
|
||||
memcpy(element.data, audio_buf, 512 * 2 * 4);
|
||||
if (queue_is_full(&audio_fifo)) {
|
||||
queue_try_remove(&audio_fifo,NULL);
|
||||
g_fifo_drops++;
|
||||
}
|
||||
if (!queue_try_add(&audio_fifo, &element)) {
|
||||
printf("[Audio] Warning: audio_fifo add failed\n");
|
||||
@@ -407,45 +402,20 @@ void audio_loop() {
|
||||
pkt[77] = SAMPLE_SIZE;
|
||||
memcpy(pkt + 78, haptic_buf, SAMPLE_SIZE);
|
||||
#if !DISABLE_SPEAKER_PROC
|
||||
// Speaker Audio Data — MUST immediately follow the haptic block. The DS5
|
||||
// parses sub-reports sequentially (header + len + data), so this offset is
|
||||
// 78 + SAMPLE_SIZE, NOT a fixed 142. At SAMPLE_SIZE 64 that worked out to
|
||||
// 142; shrinking the haptic block to 60 without moving this is what
|
||||
// silenced the speaker (controller couldn't locate the speaker sub-report).
|
||||
constexpr int kSpkOff = 78 + SAMPLE_SIZE; // = 138 at SAMPLE_SIZE 60
|
||||
pkt[kSpkOff] = (plug_headset ? 0x16 : 0x13) | 0 << 6 | 1 << 7; // Speaker: 0x13
|
||||
// Speaker Audio Data
|
||||
pkt[142] = (plug_headset ? 0x16 : 0x13) | 0 << 6 | 1 << 7; // Speaker: 0x13
|
||||
// L Headset Mono: 0x14
|
||||
// L Headset R Speaker: 0x15
|
||||
// Headset: 0x16
|
||||
pkt[kSpkOff + 1] = 200;
|
||||
pkt[143] = 200;
|
||||
critical_section_enter_blocking(&opus_cs);
|
||||
memcpy(pkt + kSpkOff + 2, opus_buf, 200);
|
||||
memcpy(pkt + 144, opus_buf, 200);
|
||||
critical_section_exit(&opus_cs);
|
||||
#endif
|
||||
|
||||
bt_write(pkt, sizeof(pkt));
|
||||
g_bt_packets++;
|
||||
haptic_buf_pos = 0;
|
||||
|
||||
// Debug: dump 5 consecutive Opus frames (skip first 10 to let encoder settle)
|
||||
{
|
||||
static int dump_count = 0;
|
||||
if (g_bt_packets > 10 && dump_count < 5) {
|
||||
dump_count++;
|
||||
printf("[OPUS_FRAME_%d] ", dump_count);
|
||||
critical_section_enter_blocking(&opus_cs);
|
||||
for (int di = 0; di < 200; di++) printf("%02x", opus_buf[di]);
|
||||
critical_section_exit(&opus_cs);
|
||||
printf("\n");
|
||||
}
|
||||
if ((g_bt_packets % 94) == 0) {
|
||||
printf("[AUD] usb=%lu enc=%lu bt=%lu opus_ret=%ld fifo_drop=%lu hs=%d\n",
|
||||
(unsigned long)g_usb_frames, (unsigned long)g_opus_encodes,
|
||||
(unsigned long)g_bt_packets, (long)g_opus_last_ret,
|
||||
(unsigned long)g_fifo_drops,
|
||||
plug_headset ? 1 : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,6 +443,7 @@ void audio_init() {
|
||||
}
|
||||
|
||||
static OpusEncoder *encoder;
|
||||
static WDL_Resampler resampler_audio;
|
||||
|
||||
void core1_entry() {
|
||||
int error = 0;
|
||||
@@ -484,25 +455,28 @@ void core1_entry() {
|
||||
opus_encoder_ctl(encoder,OPUS_SET_EXPERT_FRAME_DURATION(OPUS_FRAMESIZE_10_MS));
|
||||
opus_encoder_ctl(encoder,OPUS_SET_BITRATE(200 * 8 * 100));
|
||||
opus_encoder_ctl(encoder,OPUS_SET_VBR(false));
|
||||
opus_encoder_ctl(encoder,OPUS_SET_COMPLEXITY(0)); // 5 overloaded core1 (stale frames -> worse); 0 keeps up
|
||||
opus_encoder_ctl(encoder,OPUS_SET_COMPLEXITY(0)); // max 4
|
||||
resampler_audio.SetMode(true, 0, false);
|
||||
resampler_audio.SetRates(51200, 48000);
|
||||
resampler_audio.SetFeedMode(true);
|
||||
resampler_audio.Prealloc(2, 512, 480);
|
||||
|
||||
while (true) {
|
||||
static audio_raw_element audio_element{};
|
||||
queue_remove_blocking(&audio_fifo, &audio_element);
|
||||
// audio_element is exactly 480 stereo frames (10 ms @ 48 kHz) = one native
|
||||
// Opus frame, so encode it directly. The old 512→480 (51200→48000) resample
|
||||
// only existed to coerce a 512-sample buffer into a legal Opus frame size; it
|
||||
// shipped 480 samples every 10.667 ms (haptic-gated cadence) = 45 kHz into the
|
||||
// DS5's free-running 48 kHz DAC → ~6.25% underrun = the periodic gaps/crackle.
|
||||
// SAMPLE_SIZE 60 + a 480-sample buffer put the whole 0x36 frame on a true
|
||||
// 10 ms / 100 Hz grid: 100 × 480 = 48000 samples/s, matched, no gaps.
|
||||
// 将 512 frames 重采样成 480 frames 以解决噪音问题。感谢 @Junhoo
|
||||
WDL_ResampleSample *in_buf;
|
||||
int nframes = resampler_audio.ResamplePrepare(512, 2, &in_buf);
|
||||
for (int i = 0; i < nframes * 2; i++) {
|
||||
in_buf[i] = audio_element.data[i];
|
||||
}
|
||||
static WDL_ResampleSample out_buf[480 * 2];
|
||||
resampler_audio.ResampleOut(out_buf, nframes, 480, 2);
|
||||
|
||||
static uint8_t out[200];
|
||||
int enc_ret = opus_encode_float(encoder, audio_element.data, 480, out, 200);
|
||||
g_opus_last_ret = enc_ret;
|
||||
g_opus_encodes++;
|
||||
(void) opus_encode_float(encoder, out_buf, 480, out, 200);
|
||||
critical_section_enter_blocking(&opus_cs);
|
||||
memcpy(opus_buf, out, 200);
|
||||
critical_section_exit(&opus_cs);
|
||||
g_opus_ready = true;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -72,7 +72,7 @@ void config_valid() {
|
||||
printf("[Config] polling_rate_mode is invalid\n");
|
||||
}
|
||||
if (body->audio_buffer_length < 16 || body->audio_buffer_length > 128) {
|
||||
body->audio_buffer_length = 16; // low buffer avoids the DS5's periodic re-buffer gap
|
||||
body->audio_buffer_length = 64;
|
||||
printf("[Config] haptics_buffer_length is invalid\n");
|
||||
}
|
||||
if (body->controller_mode > 2) {
|
||||
@@ -84,8 +84,8 @@ void config_valid() {
|
||||
printf("[Config] current_slot is invalid\n");
|
||||
}
|
||||
if (body->auto_haptics_enable > 3) {
|
||||
body->auto_haptics_enable = 0; // default OFF (was 1/Fallback) — it derives erratic rumble from the speaker
|
||||
printf("[Config] auto_haptics_enable invalid, defaulting to 0 (Off)\n");
|
||||
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;
|
||||
|
||||
@@ -251,6 +251,47 @@ uint16_t tud_hid_get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t
|
||||
(void) buffer;
|
||||
(void) reqlen;
|
||||
|
||||
// --- DualSense feature reports that Linux's hid_playstation reads at probe ---
|
||||
// Without valid answers the kernel never creates a gamepad device, so games
|
||||
// outside Steam Input (Heroic/Proton/native) see no controller. The host asks
|
||||
// only for the report DATA (reqlen = report_size - 1); usbhid prepends the
|
||||
// report-id byte itself. The two CRC'd reports validate crc32 over
|
||||
// [0xA3 feature-seed, report_id, data...] in the last 4 bytes.
|
||||
// hid_playstation (kernel) AND the game's native DualSense detection both read
|
||||
// 0x09 (pairing), 0x20 (firmware) and 0x05 (calibration). The KERNEL only checks
|
||||
// size + crc, but the GAME validates the actual CONTENT — so synthesized zeros
|
||||
// pass the kernel yet get rejected by the game (a ~156x GET retry storm, and no
|
||||
// native adaptive triggers). Serve the REAL controller data, which init_feature()
|
||||
// caches from the controller over BT (get_feature_data returns it incl. the
|
||||
// report-id at [0]). Fall back to a crc-valid synthetic answer ONLY when the
|
||||
// controller isn't linked yet (USB-enumeration probe before the BT link), so the
|
||||
// kernel still binds at that moment.
|
||||
if (report_id == 0x09 || report_id == 0x20 || report_id == 0x05) {
|
||||
if (reqlen == 0) return 0;
|
||||
std::vector<uint8_t> real = get_feature_data(report_id, reqlen);
|
||||
if (real.size() > 1) { // real cached response present
|
||||
uint16_t n = (uint16_t)(real.size() - 1);
|
||||
if (n > reqlen) n = reqlen;
|
||||
memcpy(buffer, real.data() + 1, n);
|
||||
return n;
|
||||
}
|
||||
memset(buffer, 0, reqlen);
|
||||
if (report_id == 0x09) { // not linked yet: MAC-only stub
|
||||
if (reqlen >= 6) bt_get_addr(buffer);
|
||||
return reqlen;
|
||||
}
|
||||
if (reqlen < 5) return 0; // 0x20 / 0x05 stub: zeros + valid crc32
|
||||
uint8_t tmp[2 + 64];
|
||||
tmp[0] = 0xA3; tmp[1] = report_id;
|
||||
memcpy(tmp + 2, buffer, reqlen - 4);
|
||||
uint32_t crc = crc32_seeded(tmp, (size_t)(2 + (reqlen - 4)), 0);
|
||||
buffer[reqlen - 4] = (uint8_t)(crc);
|
||||
buffer[reqlen - 3] = (uint8_t)(crc >> 8);
|
||||
buffer[reqlen - 2] = (uint8_t)(crc >> 16);
|
||||
buffer[reqlen - 1] = (uint8_t)(crc >> 24);
|
||||
return reqlen;
|
||||
}
|
||||
|
||||
if (is_pico_cmd(report_id)) {
|
||||
return pico_cmd_get(report_id, buffer, reqlen);
|
||||
}
|
||||
|
||||
+9
-57
@@ -52,10 +52,6 @@ constexpr int kRowBytes = kW / 8;
|
||||
constexpr int kFbBytes = kRowBytes * kH;
|
||||
|
||||
uint8_t fb[kFbBytes];
|
||||
uint8_t fb_tx[kFbBytes];
|
||||
uint8_t reverse_lut[256];
|
||||
int flush_progress = -1;
|
||||
constexpr int kFlushChunkRows = 8;
|
||||
|
||||
uint32_t last_render_us = 0;
|
||||
constexpr uint32_t kFrameUs = 100000;
|
||||
@@ -241,9 +237,7 @@ void sh1107_init() {
|
||||
// lives near the other text-drawing helpers below.
|
||||
void draw_button_chrome();
|
||||
|
||||
// Blocking SPI flush — used only during boot splash before the main loop
|
||||
// starts (no audio/BT to service yet, so blocking is fine).
|
||||
void flush_fb_raw_blocking() {
|
||||
void flush_fb_raw() {
|
||||
cmd(0xB0);
|
||||
for (int j = 0; j < kH; j++) {
|
||||
const uint8_t col = kH - 1 - j;
|
||||
@@ -255,45 +249,11 @@ void flush_fb_raw_blocking() {
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare a chunked (non-blocking) SPI flush. Pre-reverses the framebuffer
|
||||
// into fb_tx via the LUT and sets flush_progress = 0. The actual SPI
|
||||
// transfer is driven by flush_chunk(), called from oled_loop() on
|
||||
// subsequent iterations — each chunk flushes kFlushChunkRows rows (~130 µs)
|
||||
// then yields back to the main loop so audio_loop / tud_task /
|
||||
// cyw43_arch_poll stay serviced. This eliminates the ~1.1 ms blocking
|
||||
// window that caused audio distortion when the OLED was active (issue #7).
|
||||
void flush_prepare(bool chrome) {
|
||||
if (chrome) draw_button_chrome();
|
||||
for (int i = 0; i < kFbBytes; i++) fb_tx[i] = reverse_lut[fb[i]];
|
||||
flush_progress = 0;
|
||||
void flush_fb() {
|
||||
draw_button_chrome();
|
||||
flush_fb_raw();
|
||||
}
|
||||
|
||||
bool flush_chunk() {
|
||||
if (flush_progress < 0) return true;
|
||||
if (flush_progress == 0) cmd(0xB0);
|
||||
const int end = (flush_progress + kFlushChunkRows < kH)
|
||||
? flush_progress + kFlushChunkRows : kH;
|
||||
for (int j = flush_progress; j < end; j++) {
|
||||
const uint8_t col = kH - 1 - j;
|
||||
cmd(0x00 + (col & 0x0F));
|
||||
cmd(0x10 + (col >> 4));
|
||||
gpio_put(kPinDC, 1);
|
||||
gpio_put(kPinCS, 0);
|
||||
spi_write_blocking(spi1, &fb_tx[j * kRowBytes], kRowBytes);
|
||||
gpio_put(kPinCS, 1);
|
||||
}
|
||||
flush_progress = end;
|
||||
if (flush_progress >= kH) {
|
||||
flush_progress = -1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void flush_fb_raw() { flush_prepare(false); }
|
||||
|
||||
void flush_fb() { flush_prepare(true); }
|
||||
|
||||
void fb_clear() { memset(fb, 0, sizeof(fb)); }
|
||||
|
||||
void px(int x, int y, bool on) {
|
||||
@@ -582,10 +542,8 @@ void handle_buttons() {
|
||||
|
||||
// --- Charge ETA tracker --------------------------------------------------
|
||||
// The DS5 only reports battery in 10% steps (interrupt_in_data[52] low
|
||||
// nibble, 0..10; high nibble is power-state, 1 == charging). We display the
|
||||
// midpoint of each band (+5), matching the kernel hid-playstation driver and
|
||||
// Steam: 5, 15, 25, … 95, 100%. We can't read a finer percentage over BT,
|
||||
// so a smooth countdown is impossible. Instead we
|
||||
// nibble, 0..10; high nibble is power-state, 1 == charging). We can't read a
|
||||
// finer percentage over BT, so a smooth countdown is impossible. Instead we
|
||||
// time how long each 10% step takes while charging and extrapolate the
|
||||
// remaining steps. Sampled once per frame from oled_loop (continuously, so
|
||||
// the estimate stays current even while the panel is dimmed/off and even when
|
||||
@@ -743,8 +701,8 @@ __attribute__((noinline)) void render_screen() {
|
||||
draw_text(kContentX, 9, buf);
|
||||
|
||||
const uint8_t pwr = interrupt_in_data[52];
|
||||
int raw = pwr & 0x0F;
|
||||
int pct = (raw >= 10) ? 100 : raw * 10 + 5;
|
||||
int pct = (pwr & 0x0F) * 10;
|
||||
if (pct > 100) pct = 100;
|
||||
const uint8_t pstate = pwr >> 4;
|
||||
char marker = ' ';
|
||||
if (pstate == 1) marker = '+'; // Charging
|
||||
@@ -1849,15 +1807,13 @@ void boot_splash() {
|
||||
draw_text(cx_for(l1), 16, l1);
|
||||
draw_text(cx_for(l2), 30, l2);
|
||||
draw_text(cx_for(l3), 44, l3);
|
||||
draw_button_chrome();
|
||||
flush_fb_raw_blocking();
|
||||
flush_fb();
|
||||
sleep_ms(1500);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void oled_init() {
|
||||
for (int i = 0; i < 256; i++) reverse_lut[i] = reverse_byte((uint8_t)i);
|
||||
spi_init(spi1, 10 * 1000 * 1000);
|
||||
gpio_set_function(kPinCLK, GPIO_FUNC_SPI);
|
||||
gpio_set_function(kPinMOSI, GPIO_FUNC_SPI);
|
||||
@@ -1918,10 +1874,6 @@ void oled_loop() {
|
||||
handle_buttons();
|
||||
const uint32_t now = time_us_32();
|
||||
rumble_burst_tick(now);
|
||||
if (flush_progress >= 0) {
|
||||
flush_chunk();
|
||||
return;
|
||||
}
|
||||
if ((now - last_render_us) < kFrameUs) return;
|
||||
last_render_us = now;
|
||||
// Track charge progress every frame — before the power-ladder early-returns
|
||||
|
||||
@@ -82,16 +82,6 @@ void state_update(const uint8_t *data, const uint8_t size) {
|
||||
|
||||
set_bit(state[0], 0, update.EnableRumbleEmulation);
|
||||
set_bit(state[0], 1, update.UseRumbleNotHaptics);
|
||||
// Mirror the host's adaptive-trigger "apply" flags into the outgoing state.
|
||||
// Without these, copy_if_allowed below writes the 11-byte FFB params into
|
||||
// state[] but the DS5 receives them with AllowRight/LeftTriggerFFB cleared
|
||||
// and discards them — so adaptive triggers were dead through the dongle on
|
||||
// BOTH the standalone 0x31 path and the 0x36 audio fold, while direct USB
|
||||
// (which carries the host's flags verbatim) worked. The on-device Trigger
|
||||
// Test screen sets these same bits (0x0C) directly, which is why it worked.
|
||||
// (issue #6)
|
||||
set_bit(state[0], 2, update.AllowRightTriggerFFB);
|
||||
set_bit(state[0], 3, update.AllowLeftTriggerFFB);
|
||||
set_bit(state[38], 2, update.EnableImprovedRumbleEmulation);
|
||||
copy_if_allowed(
|
||||
update.UseRumbleNotHaptics || update.EnableRumbleEmulation,
|
||||
|
||||
+1
-1
@@ -127,7 +127,7 @@
|
||||
#define CFG_TUD_AUDIO_FUNC_1_EP_OUT_SZ_MAX CFG_TUD_AUDIO_FUNC_1_FORMAT_1_EP_SZ_OUT
|
||||
#define CFG_TUD_AUDIO_FUNC_1_EP_IN_SZ_MAX CFG_TUD_AUDIO_FUNC_1_FORMAT_1_EP_SZ_IN
|
||||
|
||||
#define CFG_TUD_AUDIO_FUNC_1_EP_OUT_SW_BUF_SZ (3 * CFG_TUD_AUDIO_FUNC_1_EP_OUT_SZ_MAX)
|
||||
#define CFG_TUD_AUDIO_FUNC_1_EP_OUT_SW_BUF_SZ (16 * CFG_TUD_AUDIO_FUNC_1_EP_OUT_SZ_MAX)
|
||||
#define CFG_TUD_AUDIO_FUNC_1_EP_IN_SW_BUF_SZ (4 * CFG_TUD_AUDIO_FUNC_1_EP_IN_SZ_MAX)
|
||||
|
||||
// Enable OUT EP (speaker) and IN EP (mic)
|
||||
|
||||
+41
-22
@@ -26,6 +26,8 @@
|
||||
#include "bsp/board_api.h"
|
||||
#include "tusb.h"
|
||||
#include "config.h"
|
||||
#include "bt.h"
|
||||
#include "slots.h"
|
||||
|
||||
#ifndef ENABLE_SERIAL
|
||||
#define ENABLE_SERIAL 0
|
||||
@@ -370,7 +372,7 @@ uint8_t descriptor_configuration[] = {
|
||||
0x00, // bCountryCode: Not localized
|
||||
0x01, // bNumDescriptors: 1 report descriptor
|
||||
0x22, // bDescriptorType: Report
|
||||
0x41, 0x01, // wDescriptorLength: 321 (0x0141) DS
|
||||
0x21, 0x01, // wDescriptorLength: 289 (0x0121) DS (F6-F9 removed to byte-match a real DS5)
|
||||
// 0xB5, 0x01, // wDescriptorLength: 437 (0x01B5) DSE
|
||||
|
||||
// Endpoint Descriptor (HID IN: EP4)
|
||||
@@ -416,7 +418,7 @@ uint8_t const *tud_descriptor_configuration_cb(uint8_t index) {
|
||||
descriptor_configuration[offset - 1] = bInterval;
|
||||
descriptor_configuration[offset - 8] = bInterval;
|
||||
if (ds_mode()) {
|
||||
descriptor_configuration[offset - 16] = 0x41;
|
||||
descriptor_configuration[offset - 16] = 0x21; // wDescriptorLength lo = 289 (0x0121); F6-F9 removed to byte-match a real DS5
|
||||
}else {
|
||||
descriptor_configuration[offset - 16] = 0xB5;
|
||||
}
|
||||
@@ -569,26 +571,17 @@ uint8_t const desc_hid_report_ds[] = {
|
||||
0x09, 0x36, // Usage (0x36)
|
||||
0x95, 0x03, // Report Count (3)
|
||||
0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
|
||||
0x85, 0xF6, // Report ID (-10)
|
||||
0x09, 0x37, // Usage (Vendor 0x37)
|
||||
0x95, 0x3F, // Report Count (63)
|
||||
0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
|
||||
0x85, 0xF7, // Report ID (-9)
|
||||
0x09, 0x38, // Usage (Vendor 0x38)
|
||||
0x95, 0x3F, // Report Count (63)
|
||||
0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
|
||||
0x85, 0xF8, // Report ID (-8)
|
||||
0x09, 0x39, // Usage (Vendor 0x39)
|
||||
0x95, 0x3F, // Report Count (63)
|
||||
0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
|
||||
0x85, 0xF9, // Report ID (-7)
|
||||
0x09, 0x3A, // Usage (Vendor 0x3A)
|
||||
0x95, 0x3F, // Report Count (63)
|
||||
0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
|
||||
// NOTE: web-config feature reports 0xF6-0xF9 are intentionally NOT declared
|
||||
// here. Declaring them made this descriptor 321 bytes vs a genuine DS5's 289,
|
||||
// and games' native DualSense parser rejected the mismatch -> no adaptive
|
||||
// triggers in-game (the long-standing "triggers only work in the OLED test"
|
||||
// bug). The firmware still HANDLES 0xF6-0xF9 (src/cmd.cpp); on Linux they
|
||||
// work fine undeclared over hidraw, exactly like 0xFD. Trade-off: the browser
|
||||
// WebHID config tool can't reach them (OLED + hidraw config still work).
|
||||
0xC0, // End Collection
|
||||
// 321 bytes
|
||||
// 289 bytes — byte-identical to a real DualSense
|
||||
};
|
||||
static_assert(sizeof(desc_hid_report_ds) == 0x0141);
|
||||
static_assert(sizeof(desc_hid_report_ds) == 0x0121);
|
||||
|
||||
uint8_t const desc_hid_report_dse[] = {
|
||||
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
|
||||
@@ -858,9 +851,35 @@ uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
|
||||
chr_count = 1;
|
||||
break;
|
||||
|
||||
case STRID_SERIAL:
|
||||
chr_count = board_usb_get_serial(_desc_str + 1, 32);
|
||||
case STRID_SERIAL: {
|
||||
// Present like a REAL DualSense: USB serial == controller MAC, the
|
||||
// same value the 0x09 pairing-info feature report returns. Before
|
||||
// this the serial was the Pico flash unique id, so the host saw two
|
||||
// identities for one controller (flash-id over USB, MAC over 0x09)
|
||||
// and Wine/Steam device-matching choked. Prefer the live connected
|
||||
// MAC; fall back to the current slot's stored MAC (known at boot,
|
||||
// before BT connects); finally fall back to the flash id if never
|
||||
// paired so the descriptor is always valid.
|
||||
uint8_t mac[6] = {0};
|
||||
bool have = false;
|
||||
bt_get_addr(mac);
|
||||
for (int i = 0; i < 6; ++i) if (mac[i]) { have = true; break; }
|
||||
if (!have) {
|
||||
slot_get_addr(get_config().current_slot, mac);
|
||||
for (int i = 0; i < 6; ++i) if (mac[i]) { have = true; break; }
|
||||
}
|
||||
if (have) {
|
||||
static const char hexd[] = "0123456789ABCDEF";
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
_desc_str[1 + i * 2] = hexd[(mac[i] >> 4) & 0x0F];
|
||||
_desc_str[1 + i * 2 + 1] = hexd[mac[i] & 0x0F];
|
||||
}
|
||||
chr_count = 12;
|
||||
} else {
|
||||
chr_count = board_usb_get_serial(_desc_str + 1, 32);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// Note: the 0xEE index string is a Microsoft OS 1.0 Descriptors.
|
||||
|
||||
Reference in New Issue
Block a user