Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2209f9b8c7 | ||
|
|
a7835ac156 | ||
|
|
ac035fcb25 | ||
|
|
5b9df0596b | ||
|
|
bd2f33bf0d | ||
|
|
3eb0e76508 | ||
|
|
c5759dfce8 |
@@ -1,4 +1,11 @@
|
||||
build
|
||||
build-pr
|
||||
.vscode
|
||||
.idea
|
||||
example
|
||||
|
||||
# Local/per-user scratch
|
||||
.claude/
|
||||
.directory
|
||||
*.code-workspace
|
||||
pr-review-*.md
|
||||
|
||||
@@ -10,6 +10,52 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
|
||||
|
||||
---
|
||||
|
||||
## [0.6.3-oled-edition] — 2026-05-18
|
||||
|
||||
Small follow-up to v0.6.2. UF2s attached to [the GitHub release](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases/tag/v0.6.3-oled-edition) (built by `.github/workflows/release.yml`).
|
||||
|
||||
### Fixed
|
||||
|
||||
- **OLED Status header was stuck on `"DS5 Bridge v0.6.0"`.** The string was hardcoded in `src/oled.cpp` and never got bumped per release, so v0.6.1 and v0.6.2 both shipped with stale text on the Status screen. Now driven by a compile-time `FIRMWARE_VERSION` macro set from `CMakeLists.txt`'s `${VERSION}` (which `release.yml` already passes as `-DVERSION="$FIRMWARE_VERSION"`). Single source of truth: the release tag. Local builds without `-DVERSION` show `"dev"` so an untagged build is obvious at a glance.
|
||||
- **Web preview's Status header had the same bug.** `src/oled/screens.ts` hardcoded `"v0.5.4"`. Now reads `firmware-latest.json` (already CI-bundled from the GitHub API) at runtime in `OledEmulator.tsx` and writes the short tag (suffix `-oled-edition` stripped) into `state.firmwareVersionLabel`, which `renderStatus()` consumes.
|
||||
|
||||
### Documentation
|
||||
|
||||
- New `CLAUDE.md` "Versioning — single source of truth" section documents the release ritual (CHANGELOG bump → tag → push → `gh release create`) and the single-source-of-truth flow from tag → CMake → C++ macro → web `firmware-latest.json`. Includes a note about the still-pending `WEB_REPO_DISPATCH_PAT` secret on the firmware repo.
|
||||
|
||||
---
|
||||
|
||||
## [0.6.2-oled-edition] — 2026-05-18
|
||||
|
||||
OLED button-model + visual chrome refactor on top of v0.6.1. UF2s attached to [the GitHub release](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases/tag/v0.6.2-oled-edition) (built by `.github/workflows/release.yml`).
|
||||
|
||||
### Changed (button model)
|
||||
|
||||
- **KEY0 / KEY1 are now strictly navigation on every screen.** KEY0 short-press = next screen, KEY1 short-press = previous screen. KEY1 long-press still cycles OLED brightness (unchanged). The old contextual K1=cycle behavior on Trigger Test (cycle trigger preset) and Lightbar (cycle lightbar mode) moved to **DualSense controller buttons** — Triangle on Trigger Test, R1 on Lightbar. Source of "the Mode label didn't change when I clicked K1" confusion eliminated.
|
||||
- **KEY0 double-click reboot → KEY0 + KEY1 simultaneous hold (≥ 1 s).** Rapid forward-navigation kept tripping the double-click timer by accident, soft-rebooting the dongle mid-session. The new two-button chord can't be fat-fingered. `kDoubleClickUs` + `key0_pending_single` state removed; new `chord_held_since_us` + `kChordHoldUs = 1 s`. DS5 PS+Mute hold-2 s remains the headless backup.
|
||||
- **Per-screen contextual actions on the controller** mirror the existing Slots / Settings conventions (where Triangle has always meant "commit / switch / save"):
|
||||
- **Trigger Test** — △ rising edge cycles `trigger_preset` and re-applies via `send_trigger_effect()`.
|
||||
- **Lightbar** — R1 rising edge cycles `lb_mode`. (Triangle stays as "save current RGB to favorite slot 0" — the existing favorite-save UX.)
|
||||
|
||||
### Changed (visual chrome)
|
||||
|
||||
- **Arrow chrome on the left edge of every screen.** `flush_fb()` now paints `>` at `(0, 8)` and `<` at `(0, 49)` so the on-screen labels physically pair with the KEY0 (top) and KEY1 (bottom) buttons. The horizontal `"K0=next K1=back"` footer at y=56 is removed from all 11 screens. Trigger Test footer = `"Tri=cycle"`; Lightbar footer = `"R1=mode"`; Slots and Settings keep their existing contextual hints (`"Tri=switch Sq hold=wipe"`, `"DP nav/adj Tri=save"`). New `kContentX = 6` shifts every screen's content right by 6 px to clear the chrome strip; rectangles, sticks, and the L1/L2 column on Status all repositioned to avoid the chrome `<` glyph painting inside the live left-stick area.
|
||||
|
||||
### Added (web preview parity)
|
||||
|
||||
- **`src/protocol/ds5BridgeHid.ts` `sendTriggerPreset(preset)`** — builds the DS5 SetStateData payload byte-for-byte from `src/oled.cpp send_trigger_effect()` and pushes via `device.sendReport(0x02, ...)`. The dongle relays it over BT to the paired controller, so cycling Trigger Test in the web preview actually drives the real adaptive triggers.
|
||||
- **Web preview mirrors the firmware refactor.** `key1Action()` collapsed to back-nav-only. New rising-edge handlers in `OledEmulator.tsx` detect Triangle / R1 / D-pad from the live controller's input report and dispatch to the appropriate per-screen action. `drawButtonChrome(fb)` paints the `>` / `<` arrows after every render. `flush()` accepts an optional tint color: Slots / Diagnostics / CPU/Clock render in **orange** (`#f59e0b`) when a controller is connected — Chrome WebHID can't expose those reports on a stock DualSense descriptor, so the orange tint + an explanatory paragraph below the canvas flag the values as mock. KEY0/KEY1 buttons in the UI moved to sit visually next to the rendered Pico-OLED-1.3, mirroring the physical board.
|
||||
- **Settings cursor on the web** — new `settingsSel` state, `>` cursor mark on the selected row; D-pad up/down on the connected controller moves the cursor (web-preview-only — actual edits + save happen via the dedicated Config tab on the website).
|
||||
- **Mock-data temperature tweak** — web Preview's CPU/Clock screen no longer drifts 41–47 °C; jitter is now ±0.4 °C around 33.6 °C (realistic Pico 2 W idle).
|
||||
|
||||
### Documentation
|
||||
|
||||
- **README "Web Config Tool" section** added near the top, linking https://marcelinevpq.github.io/DS5Dongle-OLED-Config-Web/#config and explaining the three tabs (Flash / Config / OLED Preview). Includes a BOOTSEL mode primer for first-time flashers.
|
||||
- **OLED Display Add-on section rewritten.** Screen count 10 → 11 (CPU/Clock added). Cycle order updated. New "Button reference" table covers the strict K0/K1 nav, K1 long-press brightness, and K0+K1 chord reboot. ASCII mockups dropped in favor of consistent web-preview screenshots under `assets/oled/`.
|
||||
- **Performance / Overclocking section reworded** to lead with "you don't need to do anything — the overclock is baked into the firmware". The "raise voltage / lower clock if it fails to boot" line is scoped to users compiling from source.
|
||||
|
||||
---
|
||||
|
||||
## [0.6.1-oled-edition] — 2026-05-18
|
||||
|
||||
Tagged release of the v0.6.0-oled-edition follow-up. UF2s attached to [the GitHub release](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases/tag/v0.6.1-oled-edition) (built by `.github/workflows/release.yml`).
|
||||
|
||||
@@ -95,6 +95,33 @@ 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`).
|
||||
|
||||
## Versioning — single source of truth
|
||||
|
||||
The release tag is the **only** place the version is written. Everything else flows from it:
|
||||
|
||||
- **Release tag** (e.g. `v0.6.2-oled-edition`) → created with `git tag` then `gh release create`.
|
||||
- **`.github/workflows/release.yml`** picks the tag up as `$FIRMWARE_VERSION` and passes it to CMake via `-DVERSION="$FIRMWARE_VERSION"`.
|
||||
- **`CMakeLists.txt`** exposes it to C++ as a compile-time macro:
|
||||
```cmake
|
||||
target_compile_definitions(ds5-bridge PRIVATE FIRMWARE_VERSION="${VERSION}")
|
||||
```
|
||||
Local builds without `-DVERSION=...` get the default `"dev"` — that's a deliberate visual signal so an untagged build is obvious on the OLED Status header.
|
||||
- **`src/oled.cpp` `render_screen()`** renders `"DS5 Bridge " FIRMWARE_VERSION` for the Status screen header. No string literal for the version exists anywhere else in the firmware source.
|
||||
- **Web preview** (`DS5Dongle-OLED-Config-Web`) reads the same tag at runtime from `public/firmware-latest.json`, which CI bundles in `.github/workflows/deploy.yml`'s "Bundle latest firmware UF2 from GitHub releases" step (it pulls from `MarcelineVPQ/DS5Dongle-OLED-Edition/releases/latest` via the GitHub API). `OledEmulator.tsx` fetches that JSON on mount and writes the short form (suffix `-oled-edition` stripped) into `state.firmwareVersionLabel`, which `screens.ts:renderStatus` consumes.
|
||||
|
||||
**The release ritual** is therefore:
|
||||
|
||||
1. Update `CHANGELOG.md` — move `[Unreleased]` content into a new `[X.Y.Z-oled-edition]` section dated today.
|
||||
2. Commit the CHANGELOG bump.
|
||||
3. `git tag -a vX.Y.Z-oled-edition -m "..."`
|
||||
4. `git push origin master && git push origin vX.Y.Z-oled-edition`
|
||||
5. `gh release create vX.Y.Z-oled-edition -R MarcelineVPQ/DS5Dongle-OLED-Edition --title "vX.Y.Z — OLED Edition" --notes "..."`
|
||||
6. CI builds the UF2s (~5–7 min), uploads them with SHA256SUMS, edits the release notes to append checksums, and (when `WEB_REPO_DISPATCH_PAT` is configured on the firmware repo — currently unset, see below) fires a `repository_dispatch` event to the web repo to refresh `firmware-latest.json`. Without the secret, the next push to the web repo's `master` does the refresh instead.
|
||||
|
||||
There is **no other place** to edit the version. If you find a hardcoded version string in source (`"v0.6.0"`, `"v0.5.4"`, etc.), it's a bug — replace it with the macro / JSON lookup.
|
||||
|
||||
**Known follow-up:** `WEB_REPO_DISPATCH_PAT` secret is unset on the firmware repo, so the firmware-release → web-rebuild dispatch is currently silently no-op'd (peter-evans/repository-dispatch with continue-on-error). The web bundle still updates via push events to the web repo's master, but not automatically on every firmware release.
|
||||
|
||||
## Git / branch model
|
||||
|
||||
- **`master` (origin)** = `MarcelineVPQ/DS5Dongle-OLED-Edition` (this fork's primary branding, what users download).
|
||||
|
||||
@@ -146,6 +146,14 @@ if(WAKE_DEBUG)
|
||||
endif()
|
||||
set(VERSION "dev" CACHE STRING "Program version string")
|
||||
|
||||
# Expose VERSION to firmware C++ as FIRMWARE_VERSION so src/oled.cpp's
|
||||
# Status header always reflects the release tag (set by release.yml via
|
||||
# -DVERSION="$FIRMWARE_VERSION"). Local builds without -DVERSION show
|
||||
# "dev" — visible signal that you're not on a tagged release.
|
||||
target_compile_definitions(ds5-bridge PRIVATE
|
||||
FIRMWARE_VERSION="${VERSION}"
|
||||
)
|
||||
|
||||
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}")
|
||||
|
||||
@@ -6,6 +6,26 @@
|
||||
|
||||
> **OLED Edition** is a fork of **[awalol/DS5Dongle](https://github.com/awalol/DS5Dongle)** (upstream) that adds an optional Pico-OLED-1.3 128×64 display add-on with 11 screens (status, 4-slot multi-controller pairing, lightbar color picker with favorites and effect presets, trigger test, gyro tilt, touchpad, diagnostics, CPU/clock, BT signal strength, audio VU meters, and a persistent settings menu), plus a DS5 button-combo soft-reboot. Upstream is the authoritative source for the core bridge firmware; this fork tracks it and layers add-on features on top.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Web Config Tool
|
||||
|
||||
**[→ Open the OLED Edition Web Config](https://marcelinevpq.github.io/DS5Dongle-OLED-Config-Web/#config)**
|
||||
|
||||
The web tool is a one-stop shop — **no installs, no command line, no `picotool`**. A brand-new Pico 2 W can go from "just out of the box" to fully flashed and configured without ever leaving the browser:
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
> 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)).
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This project enables the Raspberry Pi Pico2W to function as a Bluetooth bridge for the DualSense controller, allowing wireless connectivity with enhanced haptics support.
|
||||
@@ -27,7 +47,7 @@ This project enables the Raspberry Pi Pico2W to function as a Bluetooth bridge f
|
||||
- **Lightbar color picker** with 4 user favorite slots + breathing / rainbow / fade effect presets
|
||||
- **Persistent settings menu** for the 8 firmware config fields (haptics gain, speaker volume, polling rate, etc.) with hold-to-confirm Reset and Wipe-all-slots actions
|
||||
- **OLED brightness control + auto-dim** after 5 min idle (extends OLED life)
|
||||
- **Soft-reboot** without unplugging USB via DS5 `PS + Mute` hold (works headless) or OLED KEY0 double-click
|
||||
- **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))
|
||||
|
||||
## Hardware
|
||||
@@ -127,16 +147,18 @@ To opt out at build time, configure with `-DENABLE_BATT_LED=OFF`. Default is ON.
|
||||
|
||||
## Performance / Overclocking
|
||||
|
||||
Due to encoding requirements, the Pico2W must be overclocked:
|
||||
**You don't need to do anything for this — the overclock is baked into the firmware.** When you flash a UF2 from this repo, the Pico 2 W boots at the settings below automatically. There is no separate tool to run, no config file to edit, no fuses to blow.
|
||||
|
||||
Current settings:
|
||||
Baked-in settings:
|
||||
|
||||
- Voltage: 1.2V
|
||||
- Frequency: 320 MHz
|
||||
- **Voltage: 1.20 V** (`vreg_set_voltage(VREG_VOLTAGE_1_20)`)
|
||||
- **Clock: 320 MHz** (`set_sys_clock_khz(SYS_CLOCK_KHZ, true)`)
|
||||
|
||||
If your device fails to boot:
|
||||
Why it's required: at stock clock/voltage the CYW43 PIO SPI bus (the path the firmware uses to talk to the on-board Bluetooth chip) is unreliable and pairing fails. 320 MHz @ 1.20 V is the lowest combination we've verified to produce a stable BT link on this board.
|
||||
|
||||
- Increase voltage slightly or Reduce CPU frequency
|
||||
If your device fails to boot a build you compiled yourself (unusual — only relevant when you've changed source), try increasing voltage slightly or lowering the clock in `src/main.cpp`. End users running official UF2 releases should not need to touch this.
|
||||
|
||||
A small heatsink on the RP2350 is **recommended for sustained gameplay** but not required for pairing or short sessions.
|
||||
|
||||
## Build Instructions
|
||||
|
||||
@@ -169,53 +191,25 @@ If you plug a [Waveshare Pico-OLED-1.3](#hardware) onto the Pico2W's headers, th
|
||||
|
||||
### Boot splash (1.5 s on power-on)
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ │
|
||||
│ DS5 Bridge │
|
||||
│ Pico2W + OLED │
|
||||
│ │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
Centered firmware version on a blank screen for 1.5 seconds, then jumps to the Status screen.
|
||||
|
||||
### Ten screens, cycled with KEY0 on the add-on
|
||||
### Eleven screens, cycled with KEY0 on the add-on
|
||||
|
||||
Cycle order: **Status → Slots → Lightbar → Trigger Test → Gyro Tilt → Touchpad → Diagnostics → RSSI → VU Meters → Settings →** wrap. KEY0 short-press steps forward. KEY1 short-press steps backward (with two contextual exceptions, see the table at the end).
|
||||
Cycle order: **Status → Slots → Lightbar → Trigger Test → Gyro Tilt → Touchpad → Diagnostics → CPU/Clock → BT Signal → VU Meters → Settings →** wrap. **KEY0 short-press steps forward; KEY1 short-press steps backward** — on *every* screen. Per-screen interactions (cycling the trigger preset, cycling the lightbar mode, navigating the Settings cursor, switching slots) live on the **DualSense controller buttons**, never on KEY0/KEY1, so the two physical buttons on the OLED add-on always mean the same thing.
|
||||
|
||||
Every screen also paints **`>`** at the top-left edge (next to KEY0) and **`<`** at the bottom-left edge (next to KEY1) so the on-screen labels physically pair with the buttons.
|
||||
|
||||
#### 1. Status
|
||||
|
||||
Connection state, paired DualSense BD address, battery % with bar (`+` charging / `*` complete / `!` error), live analog stick positions, D-pad, face buttons (△ ◯ ✕ □), L1/R1, and L2/R2 analog trigger fill bars. The link indicator and battery use small pixel icons.
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ DS5 Bridge v0.6.0 ● │
|
||||
│ 14:3A:9A:FF:D9:F9 │
|
||||
│ 87%+ ╔══════════════╗ │
|
||||
│ ║██████░░░░░░░░║▌ │
|
||||
│ ┌────┐ L1 △ R1 ┌────┐│
|
||||
│ │ ·• │ L2 ○ □ R2 │ ·• ││
|
||||
│ │ │ ▌ ✕ ▌ │ ││
|
||||
│ └────┘ └────┘│
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
<img src="./assets/main_screen_01.jpeg" alt="Status screen on the OLED" width="420">
|
||||
<img src="./assets/oled/oled_sc01.jpg" alt="Status screen on the OLED" width="420">
|
||||
|
||||
#### 2. Slots
|
||||
|
||||
Persistent 4-slot multi-controller pairing. Browse stored controllers, switch active slot, or wipe a single slot. `>` is the cursor, `*` marks the currently active slot.
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ Slots [s0 ON] │
|
||||
│ >0* 14:3A:9A:FF:D9:F9 │
|
||||
│ 1 (empty) │
|
||||
│ 2 (empty) │
|
||||
│ 3 (empty) │
|
||||
│ │
|
||||
│ Tri=switch Sq hold=wipe │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
<img src="./assets/oled/oled_sc02.jpg" alt="Slots screen on the OLED" width="420">
|
||||
|
||||
- **D-pad ▲▼** — move cursor across slots 0–3
|
||||
- **△** — switch to the cursor slot (disconnect current, reconnect to slot's stored controller)
|
||||
@@ -226,42 +220,17 @@ Persistent 4-slot multi-controller pairing. Browse stored controllers, switch ac
|
||||
|
||||
Tilt the controller on each axis to dial in R / G / B; the firmware sends the resulting color to the DualSense's actual lightbar at 10 Hz, so the lightbar IS the visual preview (the OLED is monochrome).
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ Lightbar [LIVE] │
|
||||
│ R:128 G: 77 B:200 │
|
||||
│ ████░░░ ██░░░░░ ██████░░ │
|
||||
│ (R) (G) (B) │
|
||||
│ Sv: T=0 C=1 X=2 S=3 │
|
||||
│ Tilt = R/G/B │
|
||||
│ K0=next K1=cycle │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
<img src="./assets/lightbar_01.jpeg" alt="Lightbar color picker on the OLED" width="420">
|
||||
<img src="./assets/oled/oled_sc03.jpg" alt="Lightbar color picker on the OLED" width="420">
|
||||
|
||||
- Press **△ ◯ ✕ □** on the controller to **save** the current color into favorite slot 0 / 1 / 2 / 3
|
||||
- Press **KEY1** to cycle the mode tag: `[LIVE]` → `[FAV0]` → `[FAV1]` → `[FAV2]` → `[FAV3]` → effects (Breathing / Rainbow / Fade) → back to `[LIVE]`
|
||||
- Press **R1** on the controller to cycle the mode tag: `[LIVE]` → `[FAV0]` → `[FAV1]` → `[FAV2]` → `[FAV3]` → effects (Breathing / Rainbow / Fade) → back to `[LIVE]`
|
||||
- Default favorites: Red, Green, Blue, White
|
||||
|
||||
#### 4. Trigger Test
|
||||
|
||||
KEY1 cycles seven adaptive trigger effects applied to both L2 and R2. Pull each trigger to feel the effect.
|
||||
Press **△** on the controller to cycle seven adaptive trigger effects applied to both L2 and R2. Pull each trigger to feel the effect.
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ Trigger Test │
|
||||
│ Mode: Weapon │
|
||||
│ L2: 127 R2: 42 │
|
||||
│ │
|
||||
│ ████░░░░░░ ██░░░░░░░░ │
|
||||
│ (L2 pull) (R2 pull) │
|
||||
│ │
|
||||
│ K0=next K1=cycle │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
<img src="./assets/triggertest_01.jpeg" alt="Trigger Test screen on the OLED" width="420">
|
||||
<img src="./assets/oled/oled_sc04.jpg" alt="Trigger Test screen on the OLED" width="420">
|
||||
|
||||
Cycle order: **Off → Feedback → Weapon → Vibration → Bow → Gallop → Machine Gun → Off …** Effect parameters bitpacked per [dualsensectl](https://github.com/nowrep/dualsensectl)'s reverse-engineering, all at max strength.
|
||||
|
||||
@@ -269,69 +238,45 @@ Cycle order: **Off → Feedback → Weapon → Vibration → Bow → Gallop →
|
||||
|
||||
Live X/Y/Z accelerometer values with a 40×40 crosshair box. Tilt the controller and the dot tracks in real time.
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ Gyro Tilt │
|
||||
│ X +123 Y -456 Z +8123 │
|
||||
│ ┌────────┐ │
|
||||
│ │ │ │ │
|
||||
│ │───•────│ │
|
||||
│ │ │ │ │
|
||||
│ └────────┘ │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
<img src="./assets/gyro_tilt_01.jpeg" alt="Gyro Tilt screen on the OLED" width="420">
|
||||
<img src="./assets/oled/oled_sc05.jpg" alt="Gyro Tilt screen on the OLED" width="420">
|
||||
|
||||
#### 6. Touchpad
|
||||
|
||||
Live render of the touchpad surface. Dots appear at current finger positions; the count updates as fingers touch / leave.
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ Touchpad │
|
||||
│ ┌──────────────────────────┐ │
|
||||
│ │ • • │ │
|
||||
│ │ │ │
|
||||
│ └──────────────────────────┘ │
|
||||
│ Fingers: 2 │
|
||||
│ │
|
||||
│ K0=next │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
<img src="./assets/touchpad_01.jpeg" alt="Touchpad screen on the OLED" width="420">
|
||||
<img src="./assets/oled/oled_sc06.jpg" alt="Touchpad screen on the OLED" width="420">
|
||||
|
||||
#### 7. Diagnostics
|
||||
|
||||
Uptime, BT state, and stub counters for HCI errors / audio FIFO drops / opus FIFO drops (always 0 in this build — kept for future wiring).
|
||||
Uptime, BT state, USB-audio frames/sec, BT 0x32 packets/sec, and HCI error counter — live values for verifying the audio path is moving bytes without needing a UART cable.
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ Diagnostics │
|
||||
│ Up: 0h 14m 22s │
|
||||
│ HCI errs: 0 │
|
||||
│ Aud drops: 0 │
|
||||
│ Opus drops: 0 │
|
||||
│ BT: connected │
|
||||
│ │
|
||||
│ K0=next │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
<img src="./assets/oled/oled_sc07.jpg" alt="Diagnostics screen on the OLED" width="420">
|
||||
|
||||
<img src="./assets/diagnostics_01.jpeg" alt="Diagnostics screen on the OLED" width="420">
|
||||
#### 8. CPU / Clock
|
||||
|
||||
#### 8. RSSI
|
||||
Live RP2350 vitals: configured (`Set`) and actually-running (`Real`) system clock measured against the crystal reference, core voltage read back from the regulator, and on-die temperature (256-sample average + slow EMA so the value tracks the real die temp rather than ADC noise).
|
||||
|
||||
Live Bluetooth signal strength of the active link, in dBm with a bar. Closer to 0 dBm is stronger; −90 dBm is weak.
|
||||
<img src="./assets/oled/oled_sc08.jpg" alt="CPU / Clock diagnostics screen on the OLED" width="420">
|
||||
|
||||
#### 9. VU Meters
|
||||
The same telemetry is also exported on HID feature report `0xFC` for tooling.
|
||||
|
||||
#### 9. BT Signal
|
||||
|
||||
Live Bluetooth signal strength of the active link, in dBm with a bar. Closer to 0 dBm is stronger; −90 dBm is weak. Includes a qualitative label (Poor / Fair / Good / Excellent).
|
||||
|
||||
<img src="./assets/oled/oled_sc09.jpg" alt="BT Signal screen on the OLED" width="420">
|
||||
|
||||
#### 10. VU Meters
|
||||
|
||||
Live peak meters for the speaker and haptic audio paths. Useful for verifying audio routing without the controller being plugged in to a host.
|
||||
|
||||
#### 10. Settings
|
||||
<img src="./assets/oled/oled_sc10.jpg" alt="VU Meters screen on the OLED" width="420">
|
||||
|
||||
Persistent config editor. D-pad ▲▼ moves the selection, ▶◀ adjusts values, △ saves to flash. Includes the 8 firmware-config fields (haptics gain, speaker volume, inactive timeout, etc.), the Audio Auto Haptics controls, and two hold-to-confirm actions:
|
||||
#### 11. Settings
|
||||
|
||||
Persistent config editor. D-pad ▲▼ moves the selection, ▶◀ adjusts values, △ saves to flash. Includes the firmware-config fields (haptics gain, speaker volume, inactive timeout, polling rate), the Audio Auto Haptics controls, and two hold-to-confirm actions:
|
||||
|
||||
<img src="./assets/oled/oled_sc11.jpg" alt="Settings screen on the OLED" width="420">
|
||||
|
||||
- **AutoHap Off / Fallback / Mix / Replace** — selects the Audio Auto Haptics mode. Default `Fallback` fires derived rumble only when the game sends no native haptic data (e.g. Ghost of Tsushima on Linux); games that *do* send native haptics (Spider-Man Remastered) pass through unchanged. `Mix` adds derived on top of native, `Replace` ignores native entirely, `Off` disables.
|
||||
- **AH Gain N%** — derived-signal gain, 0–200 % in 10 % steps. Default 100 %.
|
||||
@@ -339,15 +284,18 @@ Persistent config editor. D-pad ▲▼ moves the selection, ▶◀ adjusts value
|
||||
- **Reset to defaults** — hold △ for 2 s to revert all config fields
|
||||
- **Wipe all slots** — hold △ for 2 s to drop all 4 paired controllers + all BTstack link keys
|
||||
|
||||
### KEY1 behavior by screen
|
||||
### Button reference
|
||||
|
||||
| Screen | KEY1 short-press action |
|
||||
The two physical buttons on the OLED add-on are **strictly navigation**:
|
||||
|
||||
| Button | Action |
|
||||
|---|---|
|
||||
| Status, Slots, Gyro Tilt, Touchpad, Diagnostics, RSSI, VU Meters, Settings | Step **backward** one screen (mirror of KEY0's forward step) |
|
||||
| Trigger Test | Cycle the trigger effect preset |
|
||||
| Lightbar Color Picker | Cycle between LIVE preview, the 4 favorite slots, and the effect presets |
|
||||
| **KEY0** short-press | Next screen (forward) |
|
||||
| **KEY1** short-press | Previous screen (backward) |
|
||||
| **KEY1** long-press (≥ 1.5 s) | Cycle OLED brightness level |
|
||||
| **KEY0 + KEY1** held together ≥ 1 s | `watchdog_reboot` — soft-reboot without unplugging USB |
|
||||
|
||||
KEY1 long-press (≥1.5 s) on any screen cycles the OLED brightness level.
|
||||
Per-screen state changes (cycling the trigger preset, cycling the lightbar mode, navigating Settings, switching slots, saving colors to favorite slots) all happen on the **DualSense controller buttons** — never on KEY0 / KEY1 — so the two physical buttons always mean the same thing across every screen. See each screen's section above for which controller button does what.
|
||||
|
||||
### Pinout (standard Waveshare Pico HAT layout)
|
||||
|
||||
@@ -365,8 +313,8 @@ KEY1 long-press (≥1.5 s) on any screen cycles the OLED brightness level.
|
||||
|
||||
Two ways to reboot the dongle without unplugging USB — handy if pairing gets stuck or you want a clean state:
|
||||
|
||||
- **OLED KEY0 double-click** within ~400 ms → `watchdog_reboot`
|
||||
- **DualSense `PS + Mute` held for 2 seconds** → `watchdog_reboot` (works whether or not the OLED is attached)
|
||||
- **OLED KEY0 + KEY1 held simultaneously for ≥ 1 s** → `watchdog_reboot`. Replaces the older "KEY0 double-click" gesture from earlier versions, since rapid forward-navigation kept tripping the double-click timer by accident.
|
||||
- **DualSense `PS + Mute` held for 2 seconds** → `watchdog_reboot` (works whether or not the OLED is attached — headless backup).
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 50 KiB |
@@ -42,15 +42,21 @@ uint32_t key0_t_us = 0;
|
||||
uint32_t key1_t_us = 0;
|
||||
constexpr uint32_t kDebounceUs = 20000;
|
||||
|
||||
uint32_t key0_first_press_us = 0;
|
||||
bool key0_pending_single = false;
|
||||
constexpr uint32_t kDoubleClickUs = 400000;
|
||||
// Single-press latch — armed on rising edge, fired on release. KEY0 was
|
||||
// previously a double-click reboot trigger; that gesture moved to the
|
||||
// KEY0+KEY1 chord below because rapid forward-navigation kept tripping it.
|
||||
bool key0_armed = false;
|
||||
|
||||
// KEY1 long-press detection (for brightness cycling)
|
||||
uint32_t key1_press_us = 0;
|
||||
bool key1_was_pressed = false;
|
||||
constexpr uint32_t kLongPressUs = 1500000;
|
||||
|
||||
// KEY0 + KEY1 simultaneous hold → watchdog_reboot. 1 s hold is long enough
|
||||
// to filter accidental two-button taps but short enough to feel responsive.
|
||||
uint32_t chord_held_since_us = 0;
|
||||
constexpr uint32_t kChordHoldUs = 1000000;
|
||||
|
||||
// Brightness levels (SH1107 contrast register 0x81). User cycles via KEY1 long-press.
|
||||
constexpr uint8_t kBrightLevels[] = {0xFF, 0x7F, 0x3F, 0x10};
|
||||
constexpr int kNumBrightLevels = sizeof(kBrightLevels) / sizeof(kBrightLevels[0]);
|
||||
@@ -119,6 +125,13 @@ constexpr uint32_t kRumbleBurstUs = 250000;
|
||||
|
||||
int trigger_preset = 0;
|
||||
const char* const kTrigPresetNames[] = {"Off", "Feedback", "Weapon", "Vibration", "Bow", "Gallop", "Machine"};
|
||||
|
||||
// Rising-edge trackers for the screens whose K1=cycle action moved to a
|
||||
// controller button. Trigger Test uses △ (byte 7 bit 7); Lightbar uses R1
|
||||
// (byte 8 bit 1) because △ is already taken on Lightbar for "save current
|
||||
// RGB to favorite slot 0".
|
||||
uint8_t triggers_last_face = 0;
|
||||
uint8_t lb_last_buttons = 0;
|
||||
constexpr int kNumTrigPresets = 7;
|
||||
|
||||
void cmd(uint8_t c) {
|
||||
@@ -175,7 +188,13 @@ void sh1107_init() {
|
||||
cmd(0xAF);
|
||||
}
|
||||
|
||||
// Forward-declared so flush_fb can paint the per-button arrows on top of
|
||||
// the rendered framebuffer just before SPI sends it to the OLED. Body
|
||||
// lives near the other text-drawing helpers below.
|
||||
void draw_button_chrome();
|
||||
|
||||
void flush_fb() {
|
||||
draw_button_chrome();
|
||||
cmd(0xB0);
|
||||
for (int j = 0; j < kH; j++) {
|
||||
const uint8_t col = kH - 1 - j;
|
||||
@@ -225,6 +244,17 @@ void draw_text(int x, int y, const char *s) {
|
||||
}
|
||||
}
|
||||
|
||||
// Button-chrome strip on the left edge of every screen. KEY0 (top button)
|
||||
// shows '>' at y=8; KEY1 (bottom button) shows '<' at y=49. Painted by
|
||||
// flush_fb() on top of the rendered framebuffer so it never gets clobbered.
|
||||
// Per-screen renderers reserve x ∈ [0..5] (5-wide glyph + 1 padding) and
|
||||
// start main content at kContentX.
|
||||
constexpr int kContentX = 6;
|
||||
void draw_button_chrome() {
|
||||
draw_char(0, 8, '>');
|
||||
draw_char(0, 49, '<');
|
||||
}
|
||||
|
||||
// Pixel-art icon support. Visual approach inspired by zurce/DS5Dongle-OLED
|
||||
// (https://github.com/zurce/DS5Dongle-OLED) — credit to zurce for the idea
|
||||
// of decorating the OLED with small bitmaps instead of bare text/shapes.
|
||||
@@ -387,22 +417,41 @@ void handle_buttons() {
|
||||
const uint32_t now = time_us_32();
|
||||
const bool k0 = gpio_get(kPinKey0);
|
||||
const bool k1 = gpio_get(kPinKey1);
|
||||
|
||||
// KEY0 + KEY1 chord — both held >= kChordHoldUs triggers watchdog_reboot.
|
||||
// Pre-empts the per-key handlers so a chord cancels any armed single
|
||||
// press (whichever key gets released first won't also navigate).
|
||||
const bool chord = !k0 && !k1;
|
||||
if (chord) {
|
||||
if (chord_held_since_us == 0) chord_held_since_us = now;
|
||||
key0_armed = false;
|
||||
key1_was_pressed = false;
|
||||
if ((now - chord_held_since_us) >= kChordHoldUs) {
|
||||
watchdog_reboot(0, 0, 0);
|
||||
}
|
||||
} else {
|
||||
chord_held_since_us = 0;
|
||||
}
|
||||
|
||||
// KEY0: arm on debounced rising edge, fire "next screen" on release.
|
||||
// Releasing without a chord during the hold = pure forward-nav.
|
||||
if (!k0 && key0_prev && (now - key0_t_us) > kDebounceUs) {
|
||||
key0_t_us = now;
|
||||
if (key0_pending_single && (now - key0_first_press_us) < kDoubleClickUs) {
|
||||
key0_pending_single = false;
|
||||
watchdog_reboot(0, 0, 0);
|
||||
} else {
|
||||
key0_pending_single = true;
|
||||
key0_first_press_us = now;
|
||||
key0_armed = true;
|
||||
last_activity_us = now;
|
||||
}
|
||||
}
|
||||
if (key0_pending_single && (now - key0_first_press_us) > kDoubleClickUs) {
|
||||
key0_pending_single = false;
|
||||
if (k0 && !key0_prev && key0_armed) {
|
||||
key0_armed = false;
|
||||
current_screen = (current_screen + 1) % kNumScreens;
|
||||
last_render_us = 0;
|
||||
last_activity_us = now;
|
||||
}
|
||||
// KEY1: track press time, decide on release whether it was short or long
|
||||
|
||||
// KEY1: arm on press, fire on release. Short press = back; long press
|
||||
// = brightness cycle (unchanged). Trigger-preset / lightbar-mode cycle
|
||||
// moved to the DualSense △ button — see triggers_handle_input() and
|
||||
// lightbar_handle_input(). The chord above clears key1_was_pressed so
|
||||
// a released-after-chord K1 doesn't navigate back.
|
||||
if (!k1 && key1_prev && (now - key1_t_us) > kDebounceUs) {
|
||||
key1_t_us = now;
|
||||
key1_press_us = now;
|
||||
@@ -414,24 +463,13 @@ void handle_buttons() {
|
||||
const uint32_t held = now - key1_press_us;
|
||||
last_activity_us = now;
|
||||
if (held > kLongPressUs) {
|
||||
// Long press: cycle brightness level
|
||||
bright_idx = (bright_idx + 1) % kNumBrightLevels;
|
||||
} else {
|
||||
// Short press: contextual per screen. On Trigger Test / Lightbar
|
||||
// KEY1 stays as the primary in-screen cycle. Everywhere else it
|
||||
// acts as a "back" button — cycles to the previous screen so
|
||||
// KEY0/KEY1 form a natural forward/back pair.
|
||||
if (current_screen == kScreenTriggers) {
|
||||
trigger_preset = (trigger_preset + 1) % kNumTrigPresets;
|
||||
send_trigger_effect(trigger_preset);
|
||||
} else if (current_screen == kScreenLightbar) {
|
||||
lb_mode = (lb_mode + 1) % kNumLbModes;
|
||||
} else {
|
||||
current_screen = (current_screen - 1 + kNumScreens) % kNumScreens;
|
||||
last_render_us = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
key0_prev = k0;
|
||||
key1_prev = k1;
|
||||
}
|
||||
@@ -441,7 +479,10 @@ __attribute__((noinline)) void render_screen() {
|
||||
|
||||
const bool connected = bt_is_connected();
|
||||
|
||||
draw_text(0, 0, "DS5 Bridge v0.6.0");
|
||||
// FIRMWARE_VERSION is set via CMake from -DVERSION=... on the build
|
||||
// command line (release.yml passes the tag name). Local builds get
|
||||
// "dev" so a non-tagged build is visible at a glance.
|
||||
draw_text(kContentX, 0, "DS5 Bridge " FIRMWARE_VERSION);
|
||||
draw_icon(120, 0, connected ? kIconLinkOn : kIconLinkOff, 8, 8);
|
||||
|
||||
if (connected) {
|
||||
@@ -450,7 +491,7 @@ __attribute__((noinline)) void render_screen() {
|
||||
char buf[24];
|
||||
snprintf(buf, sizeof(buf), "%02X:%02X:%02X:%02X:%02X:%02X",
|
||||
a[0], a[1], a[2], a[3], a[4], a[5]);
|
||||
draw_text(0, 9, buf);
|
||||
draw_text(kContentX, 9, buf);
|
||||
|
||||
const uint8_t pwr = interrupt_in_data[52];
|
||||
int pct = (pwr & 0x0F) * 10;
|
||||
@@ -462,11 +503,13 @@ __attribute__((noinline)) void render_screen() {
|
||||
else if (pstate >= 0xA) marker = '!'; // Error
|
||||
char bbuf[16];
|
||||
snprintf(bbuf, sizeof(bbuf), "%3d%%%c", pct, marker);
|
||||
draw_text(0, 18, bbuf);
|
||||
draw_battery_icon(30, 18, pct);
|
||||
draw_text(kContentX, 18, bbuf);
|
||||
draw_battery_icon(36, 18, pct);
|
||||
|
||||
rect_outline(0, 30, 32, 32);
|
||||
int lx = 2 + (interrupt_in_data[0] * 27) / 255;
|
||||
// Left-half visuals are shifted right by kContentX so the < button
|
||||
// chrome at (x=0, y=49) doesn't paint over the live stick dot.
|
||||
rect_outline(kContentX, 30, 32, 32);
|
||||
int lx = (kContentX + 2) + (interrupt_in_data[0] * 27) / 255;
|
||||
int ly = 32 + (interrupt_in_data[1] * 27) / 255;
|
||||
rect_filled(lx - 1, ly - 1, 3, 3);
|
||||
|
||||
@@ -475,10 +518,11 @@ __attribute__((noinline)) void render_screen() {
|
||||
int ry = 32 + (interrupt_in_data[3] * 27) / 255;
|
||||
rect_filled(rx - 1, ry - 1, 3, 3);
|
||||
|
||||
// L2/R2 analog trigger bars (vertical, fill from bottom)
|
||||
rect_outline(32, 33, 4, 29);
|
||||
// L2/R2 analog trigger bars (vertical, fill from bottom). L2 sits
|
||||
// just right of the shifted left stick box.
|
||||
rect_outline(kContentX + 32, 33, 4, 29);
|
||||
const int l2_fill = (interrupt_in_data[4] * 27) / 255;
|
||||
if (l2_fill > 0) rect_filled(33, 61 - l2_fill, 2, l2_fill);
|
||||
if (l2_fill > 0) rect_filled(kContentX + 33, 61 - l2_fill, 2, l2_fill);
|
||||
rect_outline(92, 33, 4, 29);
|
||||
const int r2_fill = (interrupt_in_data[5] * 27) / 255;
|
||||
if (r2_fill > 0) rect_filled(93, 61 - r2_fill, 2, r2_fill);
|
||||
@@ -486,13 +530,14 @@ __attribute__((noinline)) void render_screen() {
|
||||
const uint8_t b7 = interrupt_in_data[7];
|
||||
const uint8_t b8 = interrupt_in_data[8];
|
||||
|
||||
// D-pad indicator (4 directions; lit for primary + diagonals)
|
||||
// D-pad indicator (4 directions; lit for primary + diagonals).
|
||||
// Centered between the left stick column and the face-button cluster.
|
||||
const int dp = b7 & 0x0F;
|
||||
const bool dp_n = (dp == 7 || dp == 0 || dp == 1);
|
||||
const bool dp_e = (dp == 1 || dp == 2 || dp == 3);
|
||||
const bool dp_s = (dp == 3 || dp == 4 || dp == 5);
|
||||
const bool dp_w = (dp == 5 || dp == 6 || dp == 7);
|
||||
const int dcx = 46, dcy = 46;
|
||||
const int dcx = 52, dcy = 46;
|
||||
auto dot = [&](int dx, int dy, bool on) {
|
||||
if (on) rect_filled(dcx + dx - 2, dcy + dy - 2, 5, 5);
|
||||
else rect_outline(dcx + dx - 2, dcy + dy - 2, 5, 5);
|
||||
@@ -514,13 +559,14 @@ __attribute__((noinline)) void render_screen() {
|
||||
sq(fcx_off + 0, 8, b7 & 0x20); // Cross
|
||||
sq(fcx_off - 8, 0, b7 & 0x10); // Square
|
||||
|
||||
if (b8 & 0x01) rect_filled(36, 30, 12, 3); else rect_outline(36, 30, 12, 3); // L1
|
||||
// L1 bar shifted to sit between the L2 trigger column and the d-pad.
|
||||
if (b8 & 0x01) rect_filled(42, 30, 8, 3); else rect_outline(42, 30, 8, 3); // L1
|
||||
if (b8 & 0x02) rect_filled(80, 30, 12, 3); else rect_outline(80, 30, 12, 3); // R1
|
||||
} else {
|
||||
draw_text(0, 14, "Pair your DualSense:");
|
||||
draw_text(0, 26, "1. Hold Create + PS");
|
||||
draw_text(0, 36, "2. Wait for light bar");
|
||||
draw_text(0, 46, " to flash blue");
|
||||
draw_text(kContentX, 14, "Pair your DualSense:");
|
||||
draw_text(kContentX, 26, "1. Hold Create + PS");
|
||||
draw_text(kContentX, 36, "2. Wait for light bar");
|
||||
draw_text(kContentX, 46, " to flash blue");
|
||||
}
|
||||
|
||||
flush_fb();
|
||||
@@ -528,41 +574,40 @@ __attribute__((noinline)) void render_screen() {
|
||||
|
||||
__attribute__((noinline)) void render_screen_rssi() {
|
||||
fb_clear();
|
||||
draw_text(0, 0, "BT Signal");
|
||||
draw_text(kContentX, 0, "BT Signal");
|
||||
if (bt_is_connected()) {
|
||||
int8_t rssi = 0;
|
||||
bt_get_signal_strength(&rssi);
|
||||
char buf[24];
|
||||
snprintf(buf, sizeof(buf), "RSSI: %d dBm", (int)rssi);
|
||||
draw_text(0, 12, buf);
|
||||
draw_text(kContentX, 12, buf);
|
||||
|
||||
// Map RSSI range -90..-40 dBm to 0..100% bar
|
||||
int pct = ((int)rssi + 90) * 100 / 50;
|
||||
if (pct < 0) pct = 0;
|
||||
if (pct > 100) pct = 100;
|
||||
snprintf(buf, sizeof(buf), "Quality: %d%%", pct);
|
||||
draw_text(0, 22, buf);
|
||||
rect_outline(0, 34, 128, 10);
|
||||
int fill = (pct * 124) / 100;
|
||||
if (fill > 0) rect_filled(2, 36, fill, 6);
|
||||
draw_text(kContentX, 22, buf);
|
||||
rect_outline(kContentX, 34, 122, 10);
|
||||
int fill = (pct * 118) / 100;
|
||||
if (fill > 0) rect_filled(kContentX + 2, 36, fill, 6);
|
||||
|
||||
const char *label = "Poor";
|
||||
if (rssi > -55) label = "Excellent";
|
||||
else if (rssi > -65) label = "Good";
|
||||
else if (rssi > -75) label = "Fair";
|
||||
snprintf(buf, sizeof(buf), "Link: %s", label);
|
||||
draw_text(0, 48, buf);
|
||||
draw_text(kContentX, 48, buf);
|
||||
} else {
|
||||
draw_text(0, 30, "(no controller)");
|
||||
draw_text(kContentX, 30, "(no controller)");
|
||||
}
|
||||
draw_text(0, 56, "K0=next");
|
||||
flush_fb();
|
||||
}
|
||||
|
||||
__attribute__((noinline)) void render_screen_diag() {
|
||||
fb_clear();
|
||||
|
||||
draw_text(0, 0, "Diagnostics");
|
||||
draw_text(kContentX, 0, "Diagnostics");
|
||||
|
||||
const uint32_t uptime_s = time_us_32() / 1000000u;
|
||||
const uint32_t h = uptime_s / 3600u;
|
||||
@@ -570,7 +615,7 @@ __attribute__((noinline)) void render_screen_diag() {
|
||||
const uint32_t s = uptime_s % 60u;
|
||||
char buf[24];
|
||||
snprintf(buf, sizeof(buf), "Up:%luh %02lum %02lus", (unsigned long)h, (unsigned long)m, (unsigned long)s);
|
||||
draw_text(0, 9, buf);
|
||||
draw_text(kContentX, 9, buf);
|
||||
|
||||
// Per-second rates for the audio path counters — recompute every render.
|
||||
static uint32_t prev_us_frames = 0, prev_bt_packets = 0;
|
||||
@@ -591,23 +636,21 @@ __attribute__((noinline)) void render_screen_diag() {
|
||||
prev_sample_us = now_us;
|
||||
|
||||
snprintf(buf, sizeof(buf), "USB aud %lu/s", (unsigned long)usb_rate);
|
||||
draw_text(0, 18, buf);
|
||||
draw_text(kContentX, 18, buf);
|
||||
snprintf(buf, sizeof(buf), "BT 0x32 %lu/s", (unsigned long)bt_rate);
|
||||
draw_text(0, 27, buf);
|
||||
draw_text(kContentX, 27, buf);
|
||||
snprintf(buf, sizeof(buf), "HCI errs: %lu", (unsigned long)bt_hci_err_count());
|
||||
draw_text(0, 36, buf);
|
||||
draw_text(kContentX, 36, buf);
|
||||
|
||||
snprintf(buf, sizeof(buf), "BT: %s", bt_is_connected() ? "connected" : "waiting");
|
||||
draw_text(0, 45, buf);
|
||||
|
||||
draw_text(0, 56, "K0=next K1=back");
|
||||
draw_text(kContentX, 45, buf);
|
||||
|
||||
flush_fb();
|
||||
}
|
||||
|
||||
__attribute__((noinline)) void render_screen_cpu(bool entered) {
|
||||
fb_clear();
|
||||
draw_text(0, 0, "CPU / Clock");
|
||||
draw_text(kContentX, 0, "CPU / Clock");
|
||||
|
||||
char buf[24];
|
||||
|
||||
@@ -615,7 +658,7 @@ __attribute__((noinline)) void render_screen_cpu(bool entered) {
|
||||
// via set_sys_clock_khz(). This is the *target*.
|
||||
const uint32_t set_khz = (uint32_t)SYS_CLOCK_KHZ;
|
||||
snprintf(buf, sizeof(buf), "Set : %lu MHz", (unsigned long)(set_khz / 1000u));
|
||||
draw_text(0, 12, buf);
|
||||
draw_text(kContentX, 12, buf);
|
||||
|
||||
// Actually running clk_sys, measured by the on-chip frequency counter
|
||||
// against the crystal reference (not just what we asked for). The counter
|
||||
@@ -631,7 +674,7 @@ __attribute__((noinline)) void render_screen_cpu(bool entered) {
|
||||
snprintf(buf, sizeof(buf), "Real: %lu.%01lu MHz",
|
||||
(unsigned long)(real_khz / 1000u),
|
||||
(unsigned long)((real_khz % 1000u) / 100u));
|
||||
draw_text(0, 22, buf);
|
||||
draw_text(kContentX, 22, buf);
|
||||
|
||||
// Core voltage actually programmed into the regulator, read back (not the
|
||||
// compile-time constant). Codes 0..15 are linear 0.05 V steps from 0.55 V.
|
||||
@@ -642,7 +685,7 @@ __attribute__((noinline)) void render_screen_cpu(bool entered) {
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "Vcore: code %d", vcode);
|
||||
}
|
||||
draw_text(0, 32, buf);
|
||||
draw_text(kContentX, 32, buf);
|
||||
|
||||
// RP2350 on-die temperature sensor. Smoothed + averaged in cmd.cpp
|
||||
// (single source of truth shared with the 0xfc web telemetry) so the
|
||||
@@ -653,52 +696,67 @@ __attribute__((noinline)) void render_screen_cpu(bool entered) {
|
||||
const int t10 = (int)(temp_c * 10.0f + (temp_c >= 0 ? 0.5f : -0.5f));
|
||||
snprintf(buf, sizeof(buf), "Temp : %d.%d C", t10 / 10,
|
||||
(t10 < 0 ? -t10 : t10) % 10);
|
||||
draw_text(0, 42, buf);
|
||||
draw_text(kContentX, 42, buf);
|
||||
|
||||
draw_text(0, 56, "K0=next K1=back");
|
||||
flush_fb();
|
||||
}
|
||||
|
||||
// △ rising edge on the Trigger Test screen cycles trigger_preset and
|
||||
// re-applies the new effect to the paired controller. KEY1 used to do
|
||||
// this; moving it to the controller frees K0/K1 for navigation only.
|
||||
void triggers_handle_input() {
|
||||
if (!bt_is_connected()) { triggers_last_face = 0; return; }
|
||||
const uint8_t face = interrupt_in_data[7] & 0xF0;
|
||||
const bool tri_now = (face & 0x80) != 0;
|
||||
const bool tri_prev = (triggers_last_face & 0x80) != 0;
|
||||
if (tri_now && !tri_prev) {
|
||||
trigger_preset = (trigger_preset + 1) % kNumTrigPresets;
|
||||
send_trigger_effect(trigger_preset);
|
||||
}
|
||||
triggers_last_face = face;
|
||||
}
|
||||
|
||||
__attribute__((noinline)) void render_screen_triggers() {
|
||||
triggers_handle_input();
|
||||
fb_clear();
|
||||
draw_text(0, 0, "Trigger Test");
|
||||
draw_text(kContentX, 0, "Trigger Test");
|
||||
|
||||
char buf[24];
|
||||
snprintf(buf, sizeof(buf), "Mode: %s", kTrigPresetNames[trigger_preset]);
|
||||
draw_text(0, 12, buf);
|
||||
draw_text(kContentX, 12, buf);
|
||||
|
||||
if (bt_is_connected()) {
|
||||
const uint8_t l2 = interrupt_in_data[4];
|
||||
const uint8_t r2 = interrupt_in_data[5];
|
||||
snprintf(buf, sizeof(buf), "L2:%3d R2:%3d", l2, r2);
|
||||
draw_text(0, 24, buf);
|
||||
draw_text(kContentX, 24, buf);
|
||||
|
||||
rect_outline(0, 35, 60, 9);
|
||||
int lfill = (l2 * 56) / 255;
|
||||
if (lfill > 0) rect_filled(2, 37, lfill, 5);
|
||||
rect_outline(68, 35, 60, 9);
|
||||
int rfill = (r2 * 56) / 255;
|
||||
if (rfill > 0) rect_filled(70, 37, rfill, 5);
|
||||
rect_outline(kContentX, 35, 56, 9);
|
||||
int lfill = (l2 * 52) / 255;
|
||||
if (lfill > 0) rect_filled(kContentX + 2, 37, lfill, 5);
|
||||
rect_outline(72, 35, 56, 9);
|
||||
int rfill = (r2 * 52) / 255;
|
||||
if (rfill > 0) rect_filled(74, 37, rfill, 5);
|
||||
} else {
|
||||
draw_text(0, 24, "(no controller)");
|
||||
draw_text(kContentX, 24, "(no controller)");
|
||||
}
|
||||
|
||||
draw_text(0, 56, "K0=next K1=cycle");
|
||||
draw_text(kContentX, 56, "Tri=cycle");
|
||||
flush_fb();
|
||||
}
|
||||
|
||||
__attribute__((noinline)) void render_screen_gyro() {
|
||||
fb_clear();
|
||||
draw_text(0, 0, "Gyro Tilt");
|
||||
draw_text(kContentX, 0, "Gyro Tilt");
|
||||
if (bt_is_connected()) {
|
||||
int16_t ax, ay, az;
|
||||
memcpy(&ax, &interrupt_in_data[21], 2);
|
||||
memcpy(&ay, &interrupt_in_data[23], 2);
|
||||
memcpy(&az, &interrupt_in_data[25], 2);
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "X%+5d", ax); draw_text(0, 10, buf);
|
||||
snprintf(buf, sizeof(buf), "Y%+5d", ay); draw_text(44, 10, buf);
|
||||
snprintf(buf, sizeof(buf), "Z%+5d", az); draw_text(88, 10, buf);
|
||||
snprintf(buf, sizeof(buf), "X%+5d", ax); draw_text(kContentX, 10, buf);
|
||||
snprintf(buf, sizeof(buf), "Y%+5d", ay); draw_text(50, 10, buf);
|
||||
snprintf(buf, sizeof(buf), "Z%+5d", az); draw_text(94, 10, buf);
|
||||
|
||||
const int bx = 44, by = 22, bw = 40, bh = 40;
|
||||
rect_outline(bx, by, bw, bh);
|
||||
@@ -714,16 +772,16 @@ __attribute__((noinline)) void render_screen_gyro() {
|
||||
if (cy > by + bh - 3) cy = by + bh - 3;
|
||||
rect_filled(cx - 1, cy - 1, 3, 3);
|
||||
} else {
|
||||
draw_text(0, 30, "(no controller)");
|
||||
draw_text(kContentX, 30, "(no controller)");
|
||||
}
|
||||
flush_fb();
|
||||
}
|
||||
|
||||
__attribute__((noinline)) void render_screen_touchpad() {
|
||||
fb_clear();
|
||||
draw_text(0, 0, "Touchpad");
|
||||
draw_text(kContentX, 0, "Touchpad");
|
||||
if (bt_is_connected()) {
|
||||
rect_outline(4, 12, 120, 30);
|
||||
rect_outline(kContentX + 2, 12, 116, 30);
|
||||
int active = 0;
|
||||
for (int finger = 0; finger < 2; finger++) {
|
||||
const int off = 32 + finger * 4;
|
||||
@@ -735,9 +793,9 @@ __attribute__((noinline)) void render_screen_touchpad() {
|
||||
if (not_touching) continue;
|
||||
const uint16_t fx = (f >> 8) & 0xFFFu;
|
||||
const uint16_t fy = (f >> 20) & 0xFFFu;
|
||||
int sx = 5 + ((int)fx * 114) / 1919;
|
||||
int sx = (kContentX + 3) + ((int)fx * 110) / 1919;
|
||||
int sy = 13 + ((int)fy * 26) / 1079;
|
||||
if (sx < 5) sx = 5;
|
||||
if (sx < kContentX + 3) sx = kContentX + 3;
|
||||
if (sx > 122) sx = 122;
|
||||
if (sy < 13) sy = 13;
|
||||
if (sy > 40) sy = 40;
|
||||
@@ -746,11 +804,10 @@ __attribute__((noinline)) void render_screen_touchpad() {
|
||||
}
|
||||
char buf[20];
|
||||
snprintf(buf, sizeof(buf), "Fingers: %d", active);
|
||||
draw_text(0, 46, buf);
|
||||
draw_text(kContentX, 46, buf);
|
||||
} else {
|
||||
draw_text(0, 30, "(no controller)");
|
||||
draw_text(kContentX, 30, "(no controller)");
|
||||
}
|
||||
draw_text(0, 56, "K0=next");
|
||||
flush_fb();
|
||||
}
|
||||
|
||||
@@ -803,9 +860,25 @@ const char* lb_mode_tag(int mode) {
|
||||
}
|
||||
}
|
||||
|
||||
// R1 rising edge on Lightbar cycles lb_mode. Used to be KEY1; that moved
|
||||
// to back-nav. Triangle on this screen stays as "save current RGB to
|
||||
// favorite slot 0" (the existing favorite-save UX), so R1 is the next
|
||||
// free button that doesn't break a mental model.
|
||||
void lightbar_handle_input() {
|
||||
if (!bt_is_connected()) { lb_last_buttons = 0; return; }
|
||||
const uint8_t btns = interrupt_in_data[8];
|
||||
const bool r1_now = (btns & 0x02) != 0;
|
||||
const bool r1_prev = (lb_last_buttons & 0x02) != 0;
|
||||
if (r1_now && !r1_prev) {
|
||||
lb_mode = (lb_mode + 1) % kNumLbModes;
|
||||
}
|
||||
lb_last_buttons = btns;
|
||||
}
|
||||
|
||||
__attribute__((noinline)) void render_screen_lightbar() {
|
||||
lightbar_handle_input();
|
||||
fb_clear();
|
||||
draw_text(0, 0, "Lightbar");
|
||||
draw_text(kContentX, 0, "Lightbar");
|
||||
draw_text(86, 0, lb_mode_tag(lb_mode));
|
||||
|
||||
if (bt_is_connected()) {
|
||||
@@ -853,14 +926,14 @@ __attribute__((noinline)) void render_screen_lightbar() {
|
||||
}
|
||||
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "R:%3u", lb_r); draw_text(0, 12, buf);
|
||||
snprintf(buf, sizeof(buf), "G:%3u", lb_g); draw_text(44, 12, buf);
|
||||
snprintf(buf, sizeof(buf), "B:%3u", lb_b); draw_text(88, 12, buf);
|
||||
snprintf(buf, sizeof(buf), "R:%3u", lb_r); draw_text(kContentX, 12, buf);
|
||||
snprintf(buf, sizeof(buf), "G:%3u", lb_g); draw_text(48, 12, buf);
|
||||
snprintf(buf, sizeof(buf), "B:%3u", lb_b); draw_text(90, 12, buf);
|
||||
|
||||
const int by = 22, bh = 8;
|
||||
rect_outline(0, by, 40, bh); int rf = (lb_r * 36) / 255; if (rf > 0) rect_filled(2, by + 2, rf, bh - 4);
|
||||
rect_outline(44, by, 40, bh); int gf = (lb_g * 36) / 255; if (gf > 0) rect_filled(46, by + 2, gf, bh - 4);
|
||||
rect_outline(88, by, 40, bh); int bf = (lb_b * 36) / 255; if (bf > 0) rect_filled(90, by + 2, bf, bh - 4);
|
||||
rect_outline(kContentX, by, 38, bh); int rf = (lb_r * 34) / 255; if (rf > 0) rect_filled(kContentX + 2, by + 2, rf, bh - 4);
|
||||
rect_outline(48, by, 38, bh); int gf = (lb_g * 34) / 255; if (gf > 0) rect_filled(50, by + 2, gf, bh - 4);
|
||||
rect_outline(90, by, 38, bh); int bf = (lb_b * 34) / 255; if (bf > 0) rect_filled(92, by + 2, bf, bh - 4);
|
||||
|
||||
// Face button rising-edge -> save current color to slot 0..3
|
||||
const uint8_t face = interrupt_in_data[7] & 0xF0;
|
||||
@@ -877,47 +950,46 @@ __attribute__((noinline)) void render_screen_lightbar() {
|
||||
lb_fav_b[save_slot] = lb_b;
|
||||
}
|
||||
|
||||
draw_text(0, 38, "Sv:T=0 C=1 X=2 S=3");
|
||||
draw_text(kContentX, 38, "Sv:T=0 C=1 X=2 S=3");
|
||||
const char* hint =
|
||||
(lb_mode == 0) ? "Tilt = R/G/B" :
|
||||
(lb_mode == 5) ? "Breathing FAV0" :
|
||||
(lb_mode == 6) ? "Rainbow sweep" :
|
||||
(lb_mode == 7) ? "Fade thru FAVs" :
|
||||
"Locked to fav";
|
||||
draw_text(0, 48, hint);
|
||||
draw_text(kContentX, 48, hint);
|
||||
|
||||
send_lightbar_color(lb_r, lb_g, lb_b);
|
||||
} else {
|
||||
draw_text(0, 30, "(no controller)");
|
||||
draw_text(kContentX, 30, "(no controller)");
|
||||
}
|
||||
draw_text(0, 56, "K0=next K1=cycle");
|
||||
draw_text(kContentX, 56, "R1=mode");
|
||||
flush_fb();
|
||||
}
|
||||
|
||||
__attribute__((noinline)) void render_screen_vu() {
|
||||
fb_clear();
|
||||
draw_text(0, 0, "Audio Meters");
|
||||
draw_text(kContentX, 0, "Audio Meters");
|
||||
if (bt_is_connected()) {
|
||||
const uint8_t spk = audio_peak_speaker();
|
||||
const uint8_t hap = audio_peak_haptic();
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "SPK %3u", spk);
|
||||
draw_text(0, 14, buf);
|
||||
draw_text(kContentX, 14, buf);
|
||||
rect_outline(48, 14, 80, 8);
|
||||
int sfill = (spk * 76) / 255;
|
||||
if (sfill > 0) rect_filled(50, 16, sfill, 4);
|
||||
|
||||
snprintf(buf, sizeof(buf), "HAP %3u", hap);
|
||||
draw_text(0, 28, buf);
|
||||
draw_text(kContentX, 28, buf);
|
||||
rect_outline(48, 28, 80, 8);
|
||||
int hfill = (hap * 76) / 255;
|
||||
if (hfill > 0) rect_filled(50, 30, hfill, 4);
|
||||
|
||||
draw_text(0, 42, "Live USB audio peaks");
|
||||
draw_text(kContentX, 42, "Live USB audio peaks");
|
||||
} else {
|
||||
draw_text(0, 30, "(no controller)");
|
||||
draw_text(kContentX, 30, "(no controller)");
|
||||
}
|
||||
draw_text(0, 56, "K0=next");
|
||||
flush_fb();
|
||||
}
|
||||
|
||||
@@ -1088,7 +1160,7 @@ __attribute__((noinline)) void render_screen_settings() {
|
||||
fb_clear();
|
||||
char buf[24];
|
||||
snprintf(buf, sizeof(buf), "Settings %s", settings_dirty ? "(*)" : " ");
|
||||
draw_text(0, 0, buf);
|
||||
draw_text(kContentX, 0, buf);
|
||||
if (settings_save_status[0]) {
|
||||
draw_text(86, 0, settings_save_status);
|
||||
}
|
||||
@@ -1099,15 +1171,15 @@ __attribute__((noinline)) void render_screen_settings() {
|
||||
char line[28];
|
||||
for (int i = 0; i < kVisible && top + i < kNumSettingsItems; i++) {
|
||||
format_settings_item(top + i, line, sizeof(line));
|
||||
draw_text(0, 9 + i * 9, line);
|
||||
draw_text(kContentX, 9 + i * 9, line);
|
||||
}
|
||||
|
||||
if (settings_sel == kSettingsResetIdx) {
|
||||
draw_text(0, 56, "Hold Tri 2s = RESET");
|
||||
draw_text(kContentX, 56, "Hold Tri 2s = RESET");
|
||||
} else if (settings_sel == kSettingsWipeSlotsIdx) {
|
||||
draw_text(0, 56, "Hold Tri 2s = WIPE");
|
||||
draw_text(kContentX, 56, "Hold Tri 2s = WIPE");
|
||||
} else {
|
||||
draw_text(0, 56, "DP nav/adj Tri=save");
|
||||
draw_text(kContentX, 56, "DP nav/adj Tri=save");
|
||||
}
|
||||
flush_fb();
|
||||
}
|
||||
@@ -1183,7 +1255,7 @@ __attribute__((noinline)) void render_screen_slots() {
|
||||
const int active = bt_get_slot();
|
||||
const bool conn = bt_is_connected();
|
||||
snprintf(hdr, sizeof(hdr), "Slots [s%d %s]", active, conn ? "ON" : "--");
|
||||
draw_text(0, 0, hdr);
|
||||
draw_text(kContentX, 0, hdr);
|
||||
|
||||
if (slots_status[0] && (uint32_t)time_us_32() < slots_status_until_us) {
|
||||
draw_text(80, 0, slots_status);
|
||||
@@ -1201,10 +1273,10 @@ __attribute__((noinline)) void render_screen_slots() {
|
||||
} else {
|
||||
snprintf(line, sizeof(line), "%s%d%s (empty)", cursor_mark, i, active_mark);
|
||||
}
|
||||
draw_text(0, 9 + i * 9, line);
|
||||
draw_text(kContentX, 9 + i * 9, line);
|
||||
}
|
||||
|
||||
draw_text(0, 56, "Tri=switch Sq hold=wipe");
|
||||
draw_text(kContentX, 56, "Tri=switch Sq hold=wipe");
|
||||
flush_fb();
|
||||
}
|
||||
|
||||
|
||||