Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c879b24364 | ||
|
|
c7e6c5b914 | ||
|
|
7e21a1fbea | ||
|
|
3556cdedb3 | ||
|
|
a7f20cfd5a |
@@ -10,6 +10,28 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [0.6.10-oled-edition] — 2026-05-25
|
||||||
|
|
||||||
|
Headline: the **Gyro Tilt screen** is now actually usable — it applies the controller's per-unit factory **IMU calibration**, the dot **centres when the controller lies flat**, and tilt **tracks the direction** you move it. Also adds an **L3 / R3 stick-click indicator** on the Status screen and makes the **charge-ETA** robust so it no longer over-reports off a single slow charge step. Everything here is on-dongle display only — **what games receive is unchanged** (the full gyro/accel stream is still forwarded byte-for-byte). UF2s attached to [the GitHub release](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases/tag/v0.6.10-oled-edition) (built by `.github/workflows/release.yml`).
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **L3 / R3 click indicator on the OLED Status screen.** Clicking a stick in now flashes its analog-stick box inverse (white box, black dot) for as long as it's held — previously the stick clicks had no on-screen feedback. Mirrored in the web config tool's OLED Preview.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Gyro Tilt screen reworked: per-unit IMU calibration, a tilt dot that centres when flat, and intuitive direction.** Three things, all display-only — the host input report (all gyro + accel axes) is still forwarded byte-for-byte, so in-game motion is unaffected:
|
||||||
|
- **Calibration.** The DualSense ships factory gyro/accel bias + sensitivity in feature report `0x05` (which the dongle already fetches and caches at connect). Previously the tilt visuals used raw accel counts; now the cached `0x05` is parsed once per connection (re-read per controller, so it stays correct across the 4 pairing slots) and applied as `(raw − bias) × sensitivity`, keeping the same ±8192 ≈ 1 g scale. A bad/short read is rejected (same sanity gate SDL uses) and it falls back to raw — no regression when calibration is unavailable. The tilt→RGB lightbar mode uses the corrected accel too. Parse/apply mirror SDL's `SDL_hidapi_ps5.c` (zlib-licensed; credit).
|
||||||
|
- **Centred when flat.** The dot is now driven by the X (roll) and **Z** (pitch) axes — the two that read ~0 when the controller lies flat — instead of X/Y. Gravity rests on Y when flat, so the old Y mapping pegged the dot to the bottom edge at rest; it now sits centred.
|
||||||
|
- **Direction follows the controller.** Both axes are negated so tilting left moves the dot left and tilting forward moves it up, instead of mirrored.
|
||||||
|
- The dot centring + direction are mirrored in the web config tool's OLED Preview (its mock IMU now also rests gravity on Y to match real hardware).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Charge-ETA no longer balloons off a single slow 10% step.** The Status-screen `~Nm` charge estimate timed each 10% battery step and projected the rest, but one anomalously slow step (e.g. ~47 min — observed reading `~222m` at 70% on the dock) used to drag the whole projection up because the rate was a mean over only 3 steps. Each timed step's bulk-equivalent is now clamped to a 30-min ceiling and the rate is taken as the **median** over the last 5 steps, so a single under-load/anomalous reading can't dominate. Mirrored in the web OLED Preview emulator.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.6.9-oled-edition] — 2026-05-24
|
## [0.6.9-oled-edition] — 2026-05-24
|
||||||
|
|
||||||
Headline feature: **on-dongle button remapping** — reassign any of the 16 digital controls, stored on the dongle so it works in every game and on every OS with no host-side software, edited visually in the web config tool's new **Remap** tab (a click-the-controller diagram built on Zacksly's CC BY 3.0 DualSense art). Also ships **packet-loss concealment** for the BT microphone. UF2s attached to [the GitHub release](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases/tag/v0.6.9-oled-edition) (built by `.github/workflows/release.yml`); this is the first release without the debug UF2 as a download.
|
Headline feature: **on-dongle button remapping** — reassign any of the 16 digital controls, stored on the dongle so it works in every game and on every OS with no host-side software, edited visually in the web config tool's new **Remap** tab (a click-the-controller diagram built on Zacksly's CC BY 3.0 DualSense art). Also ships **packet-loss concealment** for the BT microphone. UF2s attached to [the GitHub release](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases/tag/v0.6.9-oled-edition) (built by `.github/workflows/release.yml`); this is the first release without the debug UF2 as a download.
|
||||||
|
|||||||
+388
-37
@@ -1,53 +1,404 @@
|
|||||||
# Pico2W DualSense 5 Bridge
|
# Pico2W DualSense 5 Bridge — OLED Edition
|
||||||
|
|
||||||
[English](./README.md)
|
[English](./README.md)
|
||||||
> 将 Pico2W 变成 DS5 手柄的无线适配器
|
|
||||||
|
|
||||||
# 功能特点
|
> 将 Raspberry Pi Pico2W 变成 DualSense (DS5) 手柄的无线适配器 —— 并可选配板载状态显示屏。
|
||||||
- 支持HD震动
|
|
||||||
|
|
||||||
# 使用方法
|
> **OLED 版**是 **[awalol/DS5Dongle](https://github.com/awalol/DS5Dongle)**(上游)的一个分支,增加了可选的 Pico-OLED-1.3 128×64 显示屏插件,提供 11 个屏幕(状态、4 槽多手柄配对、带收藏与特效预设的灯条调色器、扳机测试、陀螺仪倾斜、触摸板、诊断、CPU/时钟、蓝牙信号强度、音频 VU 表,以及一个持久化设置菜单),外加 DS5 按键组合软重启。核心桥接固件以上游为权威来源;本分支跟踪上游并在其之上叠加插件功能。
|
||||||
1. 按住 Pico 上的BOOTSEL进入刷机
|
|
||||||
2. 将 .uf2 文件拖入进去
|
|
||||||
3. 将 DS5 手柄进入蓝牙配对模式
|
|
||||||
4. Enjoy it
|
|
||||||
|
|
||||||
***你可能需要在控制器处于匹配模式时重新插拔 pico***
|
---
|
||||||
|
|
||||||
- 手柄连接到pico以后,系统才会显示设备
|
## 🛠️ 网页配置工具
|
||||||
|
|
||||||
# Pico 配置调整
|
**[→ 打开 OLED 版网页配置](https://marcelinevpq.github.io/DS5Dongle-OLED-Config-Web/#config)**
|
||||||
你可以通过网页调整Pico的内部设置
|
|
||||||
|
|
||||||
- 用于正式固件: https://ds5.awalol.eu.org
|
网页工具是一站式方案 —— **无需安装、无需命令行、无需 `picotool`**。一块全新的 Pico 2 W 可以全程在浏览器里从"刚开箱"做到完整刷写并配置完成:
|
||||||
- 用于测试固件: https://ds5-dev.awalol.eu.org
|
|
||||||
|
|
||||||
### Pico W 版本
|
- **刷写固件标签页** —— 让 Pico 进入 **BOOTSEL 模式**,然后在浏览器中点击 *Connect to Pico*,再点 *Flash now*。站点会捆绑最新发布的 UF2,你也可以载入自己编译的本地 `.uf2`。基于 WebUSB。
|
||||||
|
|
||||||
Pico W 由于性能问题,只能支持震动,不支持扬声器。
|
> **什么是 BOOTSEL 模式?** 这是 Pico 内置的刷写模式。进入方法:按住 Pico 上标有 **BOOTSEL** 的白色小按钮,*然后*插入 USB 线(若已插好,则在按住 BOOTSEL 的同时短暂拔插一次)。Pico 会作为可移动磁盘出现在电脑上 —— 看到它就说明已进入 BOOTSEL 模式。网页工具刷完固件后,Pico 会自动重启进入正常模式即可使用。
|
||||||
你可以通过开启 `-DPICO_W_BUILD=ON` 编译项去开启 Pico W 固件编译,或者在 Github Action 下载预编译的固件
|
- **配置标签页** —— 设备刷好并重新连接后,可编辑振动增益、扬声器音量、轮询率、音频自动触感模式以及其余持久化设置;一键保存到设备闪存。基于 WebHID。
|
||||||
|
- **重映射标签页** —— 可视化按键重映射器:在实时 DualSense 示意图上点击某个按键即可把它重新指派为任意其他按键(肩键/扳机会以带标签的图标浮动到角落)。映射保存在设备上并在主机看到报告之前应用,因此在任何游戏、任何操作系统中都生效。基于 WebHID。
|
||||||
|
- **OLED 预览标签页** —— 像素级精确模拟全部 11 个 OLED 屏幕。用页面内的 KEY0/KEY1 按钮(或在 DualSense 配对时用手柄的 △ / R1 / 方向键)来导航。循环切换扳机测试预设时,自适应扳机会在手柄上真实触发。
|
||||||
|
|
||||||
### USB 唤醒支持
|
可在任何基于 Chromium 的浏览器中使用(Chrome、Edge、Brave、Opera)。Firefox 与 Safari 不提供 WebHID 或 WebUSB,因此那里无法刷写和实时配置 —— 但 OLED 预览仍会用模拟数据渲染。
|
||||||
这是一项实验性的功能。如果你需要该功能,请前往 feat/usb-wake 分支进行编译,或者使用该分支对应的 Github Action 预编译固件。`ds5-bridge-wake.uf2` 为该功能的固件
|
|
||||||
|
|
||||||
极为建议在使用该功能前阅读 #60 和 #61
|
> 网页工具源码:**[MarcelineVPQ/DS5Dongle-OLED-Config-Web](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Config-Web)**([awalol/ds5dongle-config-web](https://github.com/awalol/ds5dongle-config-web) 的分支)。
|
||||||
|
|
||||||
### 社区分支
|
---
|
||||||
https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition
|
|
||||||
https://github.com/zurce/DS5Dongle-OLED
|
|
||||||
|
|
||||||
# 当前问题:
|
## 概述
|
||||||
- 声音可能有点小卡顿
|
|
||||||
- 由于编码需要,需要对pico进行超频,当前的参数是1.2V 320MHz。
|
|
||||||
- 若您的pico使用该超频参数无法启动,请自行增加电压或者降低频率
|
|
||||||
|
|
||||||
# 未来计划
|
本项目让 Raspberry Pi Pico2W 作为 DualSense 手柄的蓝牙桥接器,实现无线连接并增强触感支持。
|
||||||
请查看[DS5Dongle plan](https://github.com/users/awalol/projects/5)
|
|
||||||
|
|
||||||
# 编译
|
## 功能特点
|
||||||
需要将pico sdk里面的tinyusb版本升级到最新
|
|
||||||
|
|
||||||
# 致谢
|
**核心桥接(来自上游):**
|
||||||
- [rafaelvaloto/Pico_W-Dualsense](https://github.com/rafaelvaloto/Pico_W-Dualsense) - 灵感来源
|
|
||||||
- [egormanga/SAxense](https://github.com/egormanga/SAxense) - 震动报文
|
- 通过 Pico2W 完整连接 DualSense
|
||||||
- [https://controllers.fandom.com/wiki/Sony_DualSense](https://controllers.fandom.com/wiki/Sony_DualSense) - 数据报文结构
|
- HD 触感(高级振动反馈)
|
||||||
- [Paliverse/DualSenseX](https://github.com/Paliverse/DualSenseX) - 扬声器数据包报文
|
- 无线蓝牙桥接
|
||||||
|
- 通过麦克风音量调节触感增益
|
||||||
|
- 可配置的 LED 与断连行为
|
||||||
|
|
||||||
|
**OLED 版新增:**
|
||||||
|
|
||||||
|
- 可选的 Pico-OLED-1.3 状态显示屏,含 **11 个屏幕**(状态、配对槽、灯条、扳机测试、陀螺仪倾斜、触摸板、诊断、CPU/时钟、RSSI、VU 表、设置)
|
||||||
|
- **按键重映射** —— 把 16 个数字控件(面板按键、方向键、肩键/扳机、摇杆按下、Create/Options)中的任意一个重新指派为其他按键。映射保存在设备上并在主机看到报告之前应用,因此在**任何游戏和操作系统中都无需任何主机端软件**即可生效;默认为恒等映射(无重映射)。可在[网页配置工具](#️-网页配置工具)的**重映射标签页**中可视化编辑,或通过 `scripts/remap_test.py` 无界面编辑。保存在专属闪存扇区,重启后保留。
|
||||||
|
- **4 槽持久化多手柄配对** —— 可绑定至多四个 DualSense,在 OLED 上切换,开机时自动重连槽 0
|
||||||
|
- **灯条调色器**,含 4 个用户收藏槽 + 呼吸 / 彩虹 / 渐变特效预设
|
||||||
|
- **持久化设置菜单**,涵盖 8 个固件配置字段(振动增益、扬声器音量、轮询率等),并提供按住确认的"重置"与"清除全部槽"操作
|
||||||
|
- **OLED 空闲省电阶梯** —— 手动亮度循环(长按 KEY1)、空闲 2 分钟自动深度变暗并显示一个呼吸小点、空闲 15 分钟完全熄屏。按键、手柄配对或输入时立即唤醒。是真正的防烧屏,而不只是调对比度。
|
||||||
|
- **软重启**,无需拔 USB:长按 DS5 的 `PS + Mute`(可无界面工作),或在 OLED 插件上**同时按住 KEY0 + KEY1 持续 1 秒**(取代了旧的 KEY0 双击手势 —— 快速翻页时容易误触发)
|
||||||
|
- **核心桥接的审计修复** —— 修复音频路径中关键的栈溢出(解决长期存在的"音频卡顿")、安全加固、看门狗、HID/L2CAP 边界处的长度校验(见 [CHANGELOG.md](./CHANGELOG.md))
|
||||||
|
|
||||||
|
## 硬件
|
||||||
|
|
||||||
|
### 必需
|
||||||
|
|
||||||
|
| 物品 | 说明 | 大致价格 |
|
||||||
|
|---|---|---|
|
||||||
|
| **Raspberry Pi Pico 2 W** | 搭载 RP2350 MCU,板载 CYW43 蓝牙/WiFi。[官方产品页](https://www.raspberrypi.com/products/raspberry-pi-pico-2/) | 约 $7 USD |
|
||||||
|
| **索尼 DualSense 手柄** | 任何标准 PS5 DualSense(VID `054C:0CE6`)。 | — |
|
||||||
|
| **USB-C 线缆** | 将 Pico 2 W 连接到主机 PC。 | — |
|
||||||
|
|
||||||
|
### 可选(强烈推荐)
|
||||||
|
|
||||||
|
| 物品 | 说明 | 大致价格 |
|
||||||
|
|---|---|---|
|
||||||
|
| **Waveshare Pico-OLED-1.3** | 128×64 SH1107 OLED 插件板(SKU HIPI1798)。直接插到 Pico 2 W 排针上。固件检测到时自动驱动,缺失时优雅地不做任何动作。[产品页](https://www.waveshare.com/pico-oled-1.3.htm) · [Wiki](https://www.waveshare.com/wiki/Pico-OLED-1.3) | 约 $6 USD |
|
||||||
|
| **小散热片**(用于 RP2350) | 固件将 MCU 超频到 320 MHz @ 1.20 V(见[性能 / 超频](#性能--超频))。持续游玩时小散热片或导热垫有帮助。 | $1–3 USD |
|
||||||
|
|
||||||
|
### 哪里购买
|
||||||
|
|
||||||
|
Pico 2 W 与 Waveshare Pico-OLED-1.3 在全球都很容易买到:
|
||||||
|
|
||||||
|
- **Adafruit**、**Pimoroni**、**The Pi Hut**、**DigiKey**、**Mouser** —— 主要电子元件分销商(美/欧)
|
||||||
|
- **Waveshare 自营商店** 购买 OLED 插件
|
||||||
|
- 各地区 **Amazon** 店面 —— 搜索 `Raspberry Pi Pico 2 W` 与 `Waveshare Pico-OLED-1.3`(或 SKU `HIPI1798`)
|
||||||
|
- **AliExpress(速卖通)** —— 原装 Waveshare 与 Pico 货源以及克隆品;注意查看卖家评分
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 刷写固件
|
||||||
|
|
||||||
|
1. 按住 Pico2W 上的 BOOTSEL 按钮
|
||||||
|
2. 通过 USB 将 Pico2W 连接到电脑
|
||||||
|
3. 设备会挂载为 USB 存储设备
|
||||||
|
4. 将 .uf2 固件文件拖放到该设备上
|
||||||
|
|
||||||
|
### 配对手柄
|
||||||
|
|
||||||
|
1. 让 DualSense 手柄进入蓝牙配对模式
|
||||||
|
2. 等待 Pico2W 检测并连接
|
||||||
|
3. 连接后,设备会出现在主机系统中
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
有四种方式配置固件:
|
||||||
|
|
||||||
|
**网页配置(推荐,任何基于 Chromium 的浏览器):** 在 Chrome、Edge、Vivaldi、Brave 或 Opera 中打开 **[DS5 Bridge Config — OLED Edition](https://marcelinevpq.github.io/DS5Dongle-OLED-Config-Web/)**(不支持 Firefox —— Mozilla 拒绝实现 WebHID)。点击 **Connect**,在浏览器对话框中选择 DualSense,然后用熟悉的表单界面编辑任意字段。页面通过 WebHID 直接与 Pico 通信 —— 无驱动、无安装,数据不离开你的机器。源码见 [MarcelineVPQ/DS5Dongle-OLED-Config-Web](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Config-Web)。
|
||||||
|
|
||||||
|
**设备端(已装 OLED 插件):** 使用屏上的**设置**菜单(第 11 个屏幕)。方向键 ▲▼ 移动选择,▶◀ 调整数值,△ 保存到闪存。
|
||||||
|
|
||||||
|
**终端 CLI(任何系统、任何浏览器):** 安装 hidapi,然后使用 `scripts/set_ds5.py`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install hidapi
|
||||||
|
scripts/set_ds5.py # 显示当前配置
|
||||||
|
scripts/set_ds5.py --auto-haptics fallback # 修改某字段并持久化到闪存
|
||||||
|
scripts/set_ds5.py --speaker-volume -10 --haptics-gain 1.5
|
||||||
|
scripts/set_ds5.py --slot 2 # 切换活动多槽配对
|
||||||
|
scripts/set_ds5.py --version # 固件版本
|
||||||
|
scripts/set_ds5.py --rssi # 实时蓝牙 RSSI(dBm)
|
||||||
|
scripts/set_ds5.py --help # 完整参数列表
|
||||||
|
```
|
||||||
|
|
||||||
|
该脚本通过 USB HID 特性报告 `0xF6`/`0xF7`/`0xF8`/`0xF9` 与固件通信 —— 在 Linux、macOS、Windows 的任何终端下都可用,与你用哪个浏览器无关。移植自 [loteran/DS5Dongle](https://github.com/loteran/DS5Dongle) 并为本分支的 `current_slot` 字段做了扩展。
|
||||||
|
|
||||||
|
**DualSense 手柄按键(旧式后备方案,无 OLED、无 CLI):**
|
||||||
|
|
||||||
|
### 麦克风音量
|
||||||
|
|
||||||
|
控制触感增益倍率。范围:`[1.0 – 2.0]`。
|
||||||
|
|
||||||
|
### 扬声器静音
|
||||||
|
|
||||||
|
禁用 LED 连接指示灯。手柄重连后生效。
|
||||||
|
|
||||||
|
### 麦克风静音
|
||||||
|
|
||||||
|
禁用静默断连行为。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
只有在手柄连接之后,Pico 设备才会对系统可见
|
||||||
|
|
||||||
|
某些行为需要经过重连周期才会生效
|
||||||
|
|
||||||
|
### 低电量 LED 指示
|
||||||
|
|
||||||
|
当所连 DualSense 报告其电量等于或低于 10%(且未在充电)时,Pico 板载 LED 会从常亮切换为 1 Hz 闪烁,让你一眼看到警告。一旦手柄插上充电或其报告电量回升,LED 恢复常亮。即使设置了 `disable_pico_led`,该闪烁也会触发 —— 该警告被视为关键,会覆盖 LED 关闭偏好;电量恢复或手柄开始充电后,LED 回到其禁用(熄灭)状态。
|
||||||
|
|
||||||
|
如需在编译期退出此功能,用 `-DENABLE_BATT_LED=OFF` 配置。默认为 ON。
|
||||||
|
|
||||||
|
## 蓝牙 DualSense 麦克风
|
||||||
|
|
||||||
|
**DualSense 的内置麦克风可通过适配器的蓝牙配对工作**(自 v0.6.8 起)。手柄将麦克风以 Opus 音频流式传输;适配器解码后作为标准 DualSense USB 采集设备呈现给主机,因此任何应用(Discord、OBS、游戏内语音)都能像普通麦克风一样使用它。
|
||||||
|
|
||||||
|
这一度被认为不可能实现 —— 本分支早期版本曾将其记录为索尼固件的硬性限制(Linux 的 `hid-playstation` 内核驱动至今仍不支持)。结果发现它取决于适配器外发音频报告中的一个使能位。**完全归功于 [awalol](https://github.com/awalol/DS5Dongle)(上游)发现了它。** 更正后的调查记录见 [BLUETOOTH_AUDIO_NOTES.md](./BLUETOOTH_AUDIO_NOTES.md)。
|
||||||
|
|
||||||
|
**使用方法:**
|
||||||
|
|
||||||
|
- **默认开启。** 配对手柄后一两秒内麦克风便开始流式传输 —— 无需游戏音频(适配器会自行保活该流)。
|
||||||
|
- **在主机端调高采集音量** —— 它默认很低。在 Linux 上,用 `arecord -l` 找到声卡,然后例如 `amixer -c <DualSense 声卡> sset 'Headset' 90%`。用 `scripts/mic_diag.sh capture` 验证采集。
|
||||||
|
- **关闭它以节省手柄电量** —— OLED **设置 → BT Mic**,或[网页配置工具](#️-网页配置工具)中的 **BT microphone** 开关。常开麦克风会让 DS5 的音频子系统保持唤醒,明显加快耗电,所以不用语音时请关闭。
|
||||||
|
|
||||||
|
**注意点:**
|
||||||
|
|
||||||
|
- 麦克风音频为**单声道**(解码为单声道,在立体声采集端点上复制)。
|
||||||
|
- 会话中途关闭会立即停止主机端的馈送,但手柄会持续流式传输直到下次重连(目前没有已知的"停止"命令);以关闭状态全新连接则永远不会启用它。
|
||||||
|
- OLED **诊断**屏幕的 `Mic in:` 计数器在麦克风流式传输时约为 ~100/s —— 是确认它在工作的快捷方式。
|
||||||
|
- **丢包隐藏:** 丢失的麦克风帧(蓝牙链路弱、距离远、干扰)会用 Opus PLC 隐藏,使语音保持连续而不卡顿/中断,代价是少量抖动缓冲延迟(~30 ms)。诊断屏的 `Mic PLC:` 计数器仅在隐藏帧时增长 —— 实质上是一个实时链路质量计。
|
||||||
|
|
||||||
|
## 已知问题
|
||||||
|
|
||||||
|
- 超频到 320 MHz @ 1.20 V 是稳定蓝牙配对所**必需**的。把电压降到 1.10 V 或时钟降回默认会破坏 CYW43 PIO SPI 总线,蓝牙将停止工作。持续游玩时建议在 RP2350 上加小散热片。
|
||||||
|
- 在 Linux + Steam 上,HD 触感未必在每个游戏中都触发;这是游戏侧问题(部分作品仅在 Windows 专用 API 下发送 HD 触感音频)。在《漫威蜘蛛侠:重制版》中测试可用;在《对马岛之魂》中未送达 —— 同一固件、同一手柄。
|
||||||
|
- **USB 3.0 端口可能干扰配对** —— 手柄可能卡在常亮的琥珀/黄色灯条上始终连不上,而同一适配器在 USB 2.0 端口上工作正常。这是射频干扰,并非固件 bug;见下文 [USB 3.0 端口与蓝牙干扰](#usb-30-端口与蓝牙干扰)。(自 v0.6.8 起,固件会自动重试卡住的连接而不是一直挂着,能恢复许多 —— 但非全部 —— 边缘情况。)
|
||||||
|
|
||||||
|
## USB 3.0 端口与蓝牙干扰
|
||||||
|
|
||||||
|
如果适配器在某个端口能用而在另一个不能用,**请先试试 USB 2.0 端口。** USB 3.0 端口(尤其是 USB 3.0 延长线)会发出以 2.4 GHz 附近为中心的宽带射频噪声 —— 正是适配器蓝牙电台与手柄通信所用的频段。这是业界有充分记录的问题(Intel,*"USB 3.0 Radio Frequency Interference Impact on 2.4 GHz Wireless Devices"*),并非本固件特有。该噪声会降低适配器蓝牙接收机的灵敏度,于是手柄可以开始连接(琥珀灯条)但链路太吵无法完成 —— 卡在黄色上。
|
||||||
|
|
||||||
|
缓解措施,大致按效果排序:
|
||||||
|
|
||||||
|
1. **把适配器插到 USB 2.0 端口**(往往是最简单的修复 —— 许多主板/机箱两者都有)。
|
||||||
|
2. **用一根短的 USB 2.0 延长线** 让适配器离 USB 3.0 端口/金属机箱几英寸远,改善对手柄的视线。要特别避免 USB 3.0 延长线。
|
||||||
|
3. **使用插在 USB 3.0 端口上的有源 USB 2.0 集线器** —— 集线器会把链路降速并增加距离。
|
||||||
|
4. **在靠近适配器处的线缆上夹一个磁环。**
|
||||||
|
5. **配对时让手柄更靠近 / 与适配器保持视线。**
|
||||||
|
|
||||||
|
固件会自行持续重试卡住的连接,因此灯条变琥珀后让它插着等约 10–20 秒,也许无需拔插即可恢复。
|
||||||
|
|
||||||
|
## 性能 / 超频
|
||||||
|
|
||||||
|
**你无需为此做任何事 —— 超频已内置在固件中。** 当你刷入本仓库的 UF2 时,Pico 2 W 会自动以下列设置启动。没有单独要运行的工具、没有要编辑的配置文件、没有要烧的熔丝。
|
||||||
|
|
||||||
|
内置设置:
|
||||||
|
|
||||||
|
- **电压:1.20 V**(`vreg_set_voltage(VREG_VOLTAGE_1_20)`)
|
||||||
|
- **时钟:320 MHz**(`set_sys_clock_khz(SYS_CLOCK_KHZ, true)`)
|
||||||
|
|
||||||
|
为何必需:在默认时钟/电压下,CYW43 PIO SPI 总线(固件用来与板载蓝牙芯片通信的通路)不可靠,配对会失败。320 MHz @ 1.20 V 是我们验证过能在本板上产生稳定蓝牙链路的最低组合。
|
||||||
|
|
||||||
|
如果你自己编译的构建无法启动(不常见 —— 仅当你改过源码时才相关),可尝试在 `src/main.cpp` 中略微提高电压或降低时钟。运行官方 UF2 发布版的最终用户无需改动此处。
|
||||||
|
|
||||||
|
持续游玩**建议**在 RP2350 上加小散热片,但配对或短时使用并不需要。
|
||||||
|
|
||||||
|
## 编译说明
|
||||||
|
|
||||||
|
从源码编译本项目:
|
||||||
|
|
||||||
|
1. 安装 Pico SDK 2.2.0(或更新版本)。编译使用 `pico_sdk_import.cmake`。
|
||||||
|
2. **在 Pico SDK 内将 TinyUSB 固定到 0.20.0**(`$PICO_SDK_PATH/lib/tinyusb`)。本项目的 `tusb_config.h` 使用了 `TUD_AUDIO_EP_SIZE` 的四参数形式,而 Pico SDK 2.2.0 捆绑的 0.18.0 版本没有它:
|
||||||
|
```bash
|
||||||
|
cd "$PICO_SDK_PATH/lib/tinyusb"
|
||||||
|
git fetch --tags
|
||||||
|
git checkout 0.20.0
|
||||||
|
```
|
||||||
|
3. 用 CMake + Ninja(或 Make)配置并编译:
|
||||||
|
```bash
|
||||||
|
cd /path/to/DS5Dongle
|
||||||
|
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DPICO_SDK_PATH="$PICO_SDK_PATH"
|
||||||
|
cmake --build build --target ds5-bridge
|
||||||
|
```
|
||||||
|
4. UF2 会生成在 `build/ds5-bridge-oled.uf2`。照常用 BOOTSEL 刷写。
|
||||||
|
|
||||||
|
值得了解的编译标志:
|
||||||
|
|
||||||
|
- `-DENABLE_BATT_LED=ON`(默认)—— DS5 低电量时闪烁 Pico LED。
|
||||||
|
- `-DENABLE_SERIAL=ON` —— 将 printf 路由到 USB CDC 用于调试(默认 OFF;生产构建释放 UART)。
|
||||||
|
- `-DPICO_W_BUILD=ON` —— 为原版 Pico W 编译(去掉音频、降低时钟)。默认面向 Pico 2 W。
|
||||||
|
|
||||||
|
## 诊断与调试工具
|
||||||
|
|
||||||
|
排查桥接问题有两种方式 —— 设备端通过 OLED 诊断屏幕,主机端通过 `scripts/mic_diag.sh`(Linux)。主机端更快:无需切屏、无需刷写周期,可在手柄正在使用时运行。
|
||||||
|
|
||||||
|
```
|
||||||
|
# 一次性快照 —— 适配器在 USB 上吗?ALSA 枚举到了吗?采集流在跑吗?当前有手柄配对吗?
|
||||||
|
scripts/mic_diag.sh status
|
||||||
|
|
||||||
|
# 对麦克风 IN 端点做 3 秒 arecord —— 报告峰值 / RMS / 非零计数,
|
||||||
|
# 以便区分"流是静音的"和"流在产生音频"。
|
||||||
|
scripts/mic_diag.sh capture 3
|
||||||
|
|
||||||
|
# 同 `status` 但循环运行,仅在状态变化时打印。可用于
|
||||||
|
# 捕捉配对完成或音频流开/关的确切时刻。
|
||||||
|
scripts/mic_diag.sh watch
|
||||||
|
|
||||||
|
# 实时读取固件的 0xFD 厂商特性报告(经由 /dev/hidraw):
|
||||||
|
# 蓝牙输入计数与速率、最近见到的非 0x31 ID、字节前缀,以及
|
||||||
|
# 扳机流计数器(收到的主机 0x02 / 置了 AllowTriggerFFB 的 /
|
||||||
|
# 转发到蓝牙的)。bt-trace 会给出结论 —— "主机驱动没有发送
|
||||||
|
# 扳机 Allow 位" 还是 "已转发但手柄未触动" —— 这原本需要
|
||||||
|
# USB 协议分析仪才能判断。
|
||||||
|
scripts/mic_diag.sh bt-trace
|
||||||
|
```
|
||||||
|
|
||||||
|
最初是为排查被搁置的 DS5 蓝牙麦克风调查而写的(见 [BLUETOOTH_AUDIO_NOTES.md](./BLUETOOTH_AUDIO_NOTES.md))。`0xFD` 特性报告与 `bt-trace` 解码器现在还携带为 [issue #3](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/issues/3)(部分游戏缺少自适应扳机张力)新增的扳机流计数器。
|
||||||
|
|
||||||
|
## OLED 显示插件(可选)
|
||||||
|
|
||||||
|
如果你把 [Waveshare Pico-OLED-1.3](#硬件) 插到 Pico2W 的排针上,固件会自动将其作为实时状态显示屏驱动。无需任何配置 —— 没有 OLED 时固件会优雅地不做动作。
|
||||||
|
|
||||||
|
### 开机启动画面(上电后 1.5 秒)
|
||||||
|
|
||||||
|
空屏居中显示固件版本 1.5 秒,然后跳转到状态屏幕。
|
||||||
|
|
||||||
|
### 11 个屏幕,用插件上的 KEY0 循环
|
||||||
|
|
||||||
|
循环顺序:**状态 → 配对槽 → 灯条 → 扳机测试 → 陀螺仪倾斜 → 触摸板 → 诊断 → CPU/时钟 → 蓝牙信号 → VU 表 → 设置 →** 回绕。**短按 KEY0 前进;短按 KEY1 后退** —— 在*每个*屏幕上都如此。各屏幕内的交互(循环扳机预设、循环灯条模式、移动设置光标、切换槽)都放在 **DualSense 手柄按键**上,绝不放在 KEY0/KEY1 上,因此 OLED 插件上这两个物理按键始终表示同一含义。
|
||||||
|
|
||||||
|
每个屏幕还会在左上边缘(KEY0 旁)画 **`>`**、在左下边缘(KEY1 旁)画 **`<`**,让屏上标签与按键在物理上配对。
|
||||||
|
|
||||||
|
#### 1. 状态
|
||||||
|
|
||||||
|
连接状态、已配对 DualSense 的蓝牙地址、带条形的电量百分比(`+` 充电中 / `*` 已充满 / `!` 错误)、实时摇杆位置、方向键、面板按键(△ ◯ ✕ □)、L1/R1,以及 L2/R2 模拟扳机填充条。链路指示与电量使用小像素图标。
|
||||||
|
|
||||||
|
<img src="./assets/oled/oled_sc01.jpg" alt="Status screen on the OLED" width="420">
|
||||||
|
|
||||||
|
#### 2. 配对槽
|
||||||
|
|
||||||
|
持久化 4 槽多手柄配对。浏览已存手柄、切换活动槽,或清除单个槽。`>` 是光标,`*` 标记当前活动槽。
|
||||||
|
|
||||||
|
<img src="./assets/oled/oled_sc02.jpg" alt="Slots screen on the OLED" width="420">
|
||||||
|
|
||||||
|
- **方向键 ▲▼** —— 在槽 0–3 间移动光标
|
||||||
|
- **△** —— 切换到光标所在槽(断开当前,重连到该槽存储的手柄)
|
||||||
|
- **□ 按住 1.5 秒** —— 清除光标所在槽(删除 bd_addr + BTstack 链路密钥)
|
||||||
|
- 活动槽会被持久化;适配器下次开机时重连到它
|
||||||
|
|
||||||
|
#### 3. 灯条调色器
|
||||||
|
|
||||||
|
在各轴上倾斜手柄来调出 R / G / B;固件以 10 Hz 把得到的颜色发送到 DualSense 实际的灯条,因此灯条本身就是可视预览(OLED 是单色的)。
|
||||||
|
|
||||||
|
<img src="./assets/oled/oled_sc03.jpg" alt="Lightbar color picker on the OLED" width="420">
|
||||||
|
|
||||||
|
- 在手柄上按 **△ ◯ ✕ □** 把当前颜色**保存**到收藏槽 0 / 1 / 2 / 3
|
||||||
|
- 在手柄上按 **R1** 循环模式标签:`[LIVE]` → `[FAV0]` → `[FAV1]` → `[FAV2]` → `[FAV3]` → 特效(呼吸 / 彩虹 / 渐变)→ 回到 `[LIVE]`
|
||||||
|
- 默认收藏:红、绿、蓝、白
|
||||||
|
|
||||||
|
#### 4. 扳机测试
|
||||||
|
|
||||||
|
在手柄上按 **△** 循环七种应用到 L2 和 R2 的自适应扳机效果。扣动各扳机来感受效果。
|
||||||
|
|
||||||
|
<img src="./assets/oled/oled_sc04.jpg" alt="Trigger Test screen on the OLED" width="420">
|
||||||
|
|
||||||
|
循环顺序:**关 → 反馈 → 武器 → 振动 → 弓 → 疾驰 → 机枪 → 关 …** 效果参数依据 [dualsensectl](https://github.com/nowrep/dualsensectl) 的逆向工程按位打包,全部为最大强度。
|
||||||
|
|
||||||
|
#### 5. 陀螺仪倾斜
|
||||||
|
|
||||||
|
实时 X/Y/Z 加速度计数值,配 40×40 十字准线框。倾斜手柄,点会实时跟随。
|
||||||
|
|
||||||
|
<img src="./assets/oled/oled_sc05.jpg" alt="Gyro Tilt screen on the OLED" width="420">
|
||||||
|
|
||||||
|
#### 6. 触摸板
|
||||||
|
|
||||||
|
触摸板表面的实时渲染。当前手指位置处出现圆点;计数随手指触碰/离开而更新。
|
||||||
|
|
||||||
|
<img src="./assets/oled/oled_sc06.jpg" alt="Touchpad screen on the OLED" width="420">
|
||||||
|
|
||||||
|
#### 7. 诊断
|
||||||
|
|
||||||
|
可滚动的实时计数器列表 —— 运行时间、蓝牙状态、主机 → 蓝牙扳机流(`host02` / `trig` / `tx`)、蓝牙 0x31 输入速率、USB 音频帧/秒、蓝牙 0x32 包/秒,底部还有被搁置的麦克风调查计数器。手柄方向键 ▲/▼ 滚动;右边缘的小 `^` / `v` 标记"上方/下方还有更多"。只读,所以没有光标。无需 UART 线即可验证桥接器在搬运字节。
|
||||||
|
|
||||||
|
同一批计数器也通过 HID 特性报告 `0xFD` 导出给主机端工具 —— 见下文 `scripts/mic_diag.sh bt-trace`。
|
||||||
|
|
||||||
|
<img src="./assets/oled/oled_sc07.jpg" alt="Diagnostics screen on the OLED" width="420">
|
||||||
|
|
||||||
|
#### 8. CPU / 时钟
|
||||||
|
|
||||||
|
实时 RP2350 指标:配置的(`Set`)与实际运行的(`Real`)系统时钟(以晶振参考测量)、从稳压器回读的核心电压,以及片上温度(256 次采样平均 + 慢速 EMA,使数值跟踪真实芯片温度而非 ADC 噪声)。
|
||||||
|
|
||||||
|
<img src="./assets/oled/oled_sc08.jpg" alt="CPU / Clock diagnostics screen on the OLED" width="420">
|
||||||
|
|
||||||
|
同样的遥测也通过 HID 特性报告 `0xFC` 导出给工具。
|
||||||
|
|
||||||
|
#### 9. 蓝牙信号
|
||||||
|
|
||||||
|
活动链路的实时蓝牙信号强度,以 dBm 显示并带条形。越接近 0 dBm 越强;−90 dBm 为弱。含定性标签(差 / 一般 / 好 / 极佳)。
|
||||||
|
|
||||||
|
<img src="./assets/oled/oled_sc09.jpg" alt="BT Signal screen on the OLED" width="420">
|
||||||
|
|
||||||
|
#### 10. VU 表
|
||||||
|
|
||||||
|
扬声器与触感音频路径的实时峰值表。可在手柄未插入主机时验证音频路由。
|
||||||
|
|
||||||
|
<img src="./assets/oled/oled_sc10.jpg" alt="VU Meters screen on the OLED" width="420">
|
||||||
|
|
||||||
|
#### 11. 设置
|
||||||
|
|
||||||
|
持久化配置编辑器。方向键 ▲▼ 移动选择,▶◀ 调整数值,△ 保存到闪存。包含固件配置字段(振动增益、扬声器音量、空闲超时、轮询率)、音频自动触感控件,以及两个按住确认的操作:
|
||||||
|
|
||||||
|
<img src="./assets/oled/oled_sc11.jpg" alt="Settings screen on the OLED" width="420">
|
||||||
|
|
||||||
|
- **AutoHap Off / Fallback / Mix / Replace** —— 选择音频自动触感模式。默认 `Fallback` 仅在游戏不发送原生触感数据时(如 Linux 上的《对马岛之魂》)才触发派生震动;确实发送原生触感的游戏(《漫威蜘蛛侠:重制版》)则原样通过。`Mix` 在原生之上叠加派生,`Replace` 完全忽略原生,`Off` 禁用。
|
||||||
|
- **AH Gain N%** —— 派生信号增益,0–200%,10% 步进。默认 100%。
|
||||||
|
- **AH LP 80/160/250/400 Hz** —— 在包络跟随之前应用到扬声器音频的低通截止。越低越偏重低音,越高越偏临场感。默认 160 Hz。
|
||||||
|
- **Reset to defaults** —— 按住 △ 2 秒恢复所有配置字段
|
||||||
|
- **Wipe all slots** —— 按住 △ 2 秒删除全部 4 个已配对手柄 + 全部 BTstack 链路密钥
|
||||||
|
|
||||||
|
### 按键参考
|
||||||
|
|
||||||
|
OLED 插件上的两个物理按键**严格用于导航**:
|
||||||
|
|
||||||
|
| 按键 | 动作 |
|
||||||
|
|---|---|
|
||||||
|
| **KEY0** 短按 | 下一屏(前进) |
|
||||||
|
| **KEY1** 短按 | 上一屏(后退) |
|
||||||
|
| **KEY1** 长按(≥ 1.5 秒) | 循环 OLED 亮度等级 |
|
||||||
|
| **KEY0 + KEY1** 同时按住 ≥ 1 秒 | `watchdog_reboot` —— 无需拔 USB 的软重启 |
|
||||||
|
|
||||||
|
各屏幕内的状态变更(循环扳机预设、循环灯条模式、移动设置光标、切换槽、把颜色保存到收藏槽)全部发生在 **DualSense 手柄按键**上 —— 绝不在 KEY0 / KEY1 上 —— 因此这两个物理按键在每个屏幕上始终表示同一含义。各按键对应哪个手柄操作,见上文每个屏幕的小节。
|
||||||
|
|
||||||
|
### 引脚定义(标准 Waveshare Pico HAT 布局)
|
||||||
|
|
||||||
|
| 功能 | GPIO |
|
||||||
|
|---|---|
|
||||||
|
| MOSI | 11 |
|
||||||
|
| SCK | 10 |
|
||||||
|
| CS | 9 |
|
||||||
|
| DC | 8 |
|
||||||
|
| RST | 12 |
|
||||||
|
| KEY0 | 15 |
|
||||||
|
| KEY1 | 17 |
|
||||||
|
|
||||||
|
### 软重启恢复
|
||||||
|
|
||||||
|
两种无需拔 USB 重启适配器的方式 —— 配对卡住或想要干净状态时很方便:
|
||||||
|
|
||||||
|
- **OLED 上同时按住 KEY0 + KEY1 ≥ 1 秒** → `watchdog_reboot`。取代了早期版本的"KEY0 双击"手势,因为快速前进导航总会误触双击计时器。
|
||||||
|
- **按住 DualSense 的 `PS + Mute` 2 秒** → `watchdog_reboot`(无论是否装有 OLED 都可用 —— 无界面后备)。
|
||||||
|
|
||||||
|
## 致谢
|
||||||
|
|
||||||
|
本分支中的部分功能与设计思路借鉴自上游的其他分支,并予以致谢:
|
||||||
|
|
||||||
|
- **[zurce/DS5Dongle-OLED](https://github.com/zurce/DS5Dongle-OLED)** —— OLED 状态头部的像素图标(视觉方案)、设置屏 "Reset to defaults" 项使用的"按住以恢复出厂"交互模式(按住 △ 2 秒确认),以及新增配对槽屏上的多槽持久化蓝牙配对系统(4 个已绑定手柄、方向键导航、△ 切换槽、□ 按住清除某槽,外加设置菜单中的 "Wipe all slots")。
|
||||||
|
- **[loteran/DS5Dongle](https://github.com/loteran/DS5Dongle)** —— 独立地重新发现了上游 `3a31bd7` 破坏扬声器/HD 触感输出的回归(提交 `c7a8d3c`);我们 `src/audio.cpp` 中的修复恢复了同样的 SetStateData 子报告。也是音频自动触感 DSP(1 极点 LP + 包络跟随器,见 设置 → Auto Haptics)以及"不要把 USB 侧 UAC1 音量同步到持久化配置"修复的来源。
|
||||||
|
- **[awalol/ds5dongle-config-web](https://github.com/awalol/ds5dongle-config-web)** —— 我们分支版网页配置应用 [MarcelineVPQ/DS5Dongle-OLED-Config-Web](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Config-Web) 的基础。该分支把上游的 Config_body 布局适配为我们的版本,并为我们的新增功能(多槽配对、Auto Haptics)添加了 UI。
|
||||||
|
- **[PS5 Button Icons and Controls](https://zacksly.itch.io/ps5-button-icons-and-controls)**(作者 **Zacksly**)—— 网页配置工具重映射标签页所用的 DualSense 控制器轮廓与按键图标,采用 [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/) 授权(已重新着色为 `currentColor` 并裁剪以适配主题)。
|
||||||
|
|
||||||
|
## 路线图
|
||||||
|
- 请查看 [DS5Dongle plan](https://github.com/users/awalol/projects/5)
|
||||||
|
|
||||||
|
## 社区
|
||||||
|
- 加入 Discord 服务器:[Discord Server](https://discord.gg/hM4ntchGCa)
|
||||||
|
- 如果你遇到 bug,请改为开 issue。
|
||||||
|
|
||||||
|
## 参考
|
||||||
|
- [rafaelvaloto/Pico_W-Dualsense](https://github.com/rafaelvaloto/Pico_W-Dualsense) —— 项目灵感
|
||||||
|
- [egormanga/SAxense](https://github.com/egormanga/SAxense) —— 蓝牙触感 POC
|
||||||
|
- [https://controllers.fandom.com/wiki/Sony_DualSense](https://controllers.fandom.com/wiki/Sony_DualSense) —— DualSense 数据报告结构文档
|
||||||
|
- [Paliverse/DualSenseX](https://github.com/Paliverse/DualSenseX) —— 扬声器报告数据包
|
||||||
|
|||||||
@@ -748,6 +748,11 @@ vector<uint8_t> get_feature_data(uint8_t reportId, uint16_t len) {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> bt_peek_feature(uint8_t reportId) {
|
||||||
|
auto it = feature_data.find(reportId);
|
||||||
|
return (it != feature_data.end()) ? it->second : std::vector<uint8_t>{};
|
||||||
|
}
|
||||||
|
|
||||||
void set_feature_data(uint8_t reportId, uint8_t *data, uint16_t len) {
|
void set_feature_data(uint8_t reportId, uint8_t *data, uint16_t len) {
|
||||||
if (hid_control_cid != 0) {
|
if (hid_control_cid != 0) {
|
||||||
uint8_t get_feature[len + 2];
|
uint8_t get_feature[len + 2];
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ void bt_send_control(uint8_t *data, uint16_t len);
|
|||||||
void bt_write(const uint8_t *data, uint16_t len);
|
void bt_write(const uint8_t *data, uint16_t len);
|
||||||
void bt_get_signal_strength(int8_t *rssi);
|
void bt_get_signal_strength(int8_t *rssi);
|
||||||
std::vector<uint8_t> get_feature_data(uint8_t reportId,uint16_t len);
|
std::vector<uint8_t> get_feature_data(uint8_t reportId,uint16_t len);
|
||||||
|
// Side-effect-free read of an already-cached feature report (empty vector if it
|
||||||
|
// hasn't arrived yet). Unlike get_feature_data(), never issues an L2CAP request,
|
||||||
|
// so it is safe to poll every frame — used by the OLED IMU-calibration parse.
|
||||||
|
std::vector<uint8_t> bt_peek_feature(uint8_t reportId);
|
||||||
void init_feature();
|
void init_feature();
|
||||||
void set_feature_data(uint8_t reportId, uint8_t* data,uint16_t len);
|
void set_feature_data(uint8_t reportId, uint8_t* data,uint16_t len);
|
||||||
|
|
||||||
|
|||||||
+136
-7
@@ -273,6 +273,16 @@ void rect_filled(int x, int y, int w, int h) {
|
|||||||
px(x + i, y + j, true);
|
px(x + i, y + j, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XOR-invert every pixel in a region (used to flash a control "pressed").
|
||||||
|
void rect_invert(int x, int y, int w, int h) {
|
||||||
|
for (int j = 0; j < h; j++)
|
||||||
|
for (int i = 0; i < w; i++) {
|
||||||
|
const int xx = x + i, yy = y + j;
|
||||||
|
if (xx < 0 || xx >= kW || yy < 0 || yy >= kH) continue;
|
||||||
|
fb[yy * kRowBytes + (xx / 8)] ^= 1 << (7 - (xx % 8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void draw_char(int x, int y, char c) {
|
void draw_char(int x, int y, char c) {
|
||||||
if (c < 0x20 || c > 0x7E) return;
|
if (c < 0x20 || c > 0x7E) return;
|
||||||
const uint8_t *g = kFont5x7[c - 0x20];
|
const uint8_t *g = kFont5x7[c - 0x20];
|
||||||
@@ -550,6 +560,15 @@ ChargeEta g_charge_eta{};
|
|||||||
// the "?") as soon as the first clean step completes.
|
// the "?") as soon as the first clean step completes.
|
||||||
constexpr float kDefaultStepUs = 15.0f * 60.0f * 1000000.0f;
|
constexpr float kDefaultStepUs = 15.0f * 60.0f * 1000000.0f;
|
||||||
|
|
||||||
|
// Ceiling on a single timed step's bulk-equivalent duration. A genuine idle 10%
|
||||||
|
// step on this dongle is ~15 min; anything past ~30 min is almost always an
|
||||||
|
// anomalous/under-load sample (e.g. the controller in use while charging, or a
|
||||||
|
// battery-nibble bounce) that would otherwise balloon the projection — observed
|
||||||
|
// reading ~222m at 70% off one ~47-min step. We clamp such samples instead of
|
||||||
|
// trusting them, and pair that with a median over kRing steps so one bad reading
|
||||||
|
// can't dominate the estimate.
|
||||||
|
constexpr float kMaxStepUs = 30.0f * 60.0f * 1000000.0f;
|
||||||
|
|
||||||
// Relative time the step *ending* at `to_level` (10% units, 1..10) takes vs a
|
// Relative time the step *ending* at `to_level` (10% units, 1..10) takes vs a
|
||||||
// bulk step. Tuned to the Li-ion CV taper: ~80% onward stretches out.
|
// bulk step. Tuned to the Li-ion CV taper: ~80% onward stretches out.
|
||||||
static float charge_step_weight(int to_level) {
|
static float charge_step_weight(int to_level) {
|
||||||
@@ -559,7 +578,7 @@ static float charge_step_weight(int to_level) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void sample_charge_eta() {
|
void sample_charge_eta() {
|
||||||
constexpr int kRing = 3; // average the last few steps
|
constexpr int kRing = 5; // median over the last few steps
|
||||||
static float ring[kRing] = {0}; // bulk-equivalent step durations (us)
|
static float ring[kRing] = {0}; // bulk-equivalent step durations (us)
|
||||||
static int ring_count = 0;
|
static int ring_count = 0;
|
||||||
static int ring_head = 0;
|
static int ring_head = 0;
|
||||||
@@ -598,7 +617,9 @@ void sample_charge_eta() {
|
|||||||
if (first_step_pending) {
|
if (first_step_pending) {
|
||||||
first_step_pending = false;
|
first_step_pending = false;
|
||||||
} else {
|
} else {
|
||||||
ring[ring_head] = dur / charge_step_weight(step);
|
float be = dur / charge_step_weight(step);
|
||||||
|
if (be > kMaxStepUs) be = kMaxStepUs; // clamp under-load/anomalous outliers
|
||||||
|
ring[ring_head] = be;
|
||||||
ring_head = (ring_head + 1) % kRing;
|
ring_head = (ring_head + 1) % kRing;
|
||||||
if (ring_count < kRing) ring_count++;
|
if (ring_count < kRing) ring_count++;
|
||||||
}
|
}
|
||||||
@@ -620,9 +641,18 @@ void sample_charge_eta() {
|
|||||||
const bool measured = (ring_count > 0);
|
const bool measured = (ring_count > 0);
|
||||||
float bulk;
|
float bulk;
|
||||||
if (measured) {
|
if (measured) {
|
||||||
bulk = 0.0f;
|
// Median of the timed steps — robust to a single slow/fast outlier
|
||||||
for (int i = 0; i < ring_count; i++) bulk += ring[i];
|
// in a way the old mean wasn't (one 47-min under-load step used to
|
||||||
bulk /= (float)ring_count;
|
// drag the whole projection up). kRing is tiny, so insertion-sort.
|
||||||
|
float tmp[kRing];
|
||||||
|
for (int i = 0; i < ring_count; i++) tmp[i] = ring[i];
|
||||||
|
for (int i = 1; i < ring_count; i++) {
|
||||||
|
const float v = tmp[i];
|
||||||
|
int j = i - 1;
|
||||||
|
while (j >= 0 && tmp[j] > v) { tmp[j + 1] = tmp[j]; j--; }
|
||||||
|
tmp[j + 1] = v;
|
||||||
|
}
|
||||||
|
bulk = tmp[ring_count / 2];
|
||||||
} else {
|
} else {
|
||||||
bulk = kDefaultStepUs;
|
bulk = kDefaultStepUs;
|
||||||
}
|
}
|
||||||
@@ -694,11 +724,15 @@ __attribute__((noinline)) void render_screen() {
|
|||||||
int lx = (kContentX + 2) + (interrupt_in_data[0] * 27) / 255;
|
int lx = (kContentX + 2) + (interrupt_in_data[0] * 27) / 255;
|
||||||
int ly = 32 + (interrupt_in_data[1] * 27) / 255;
|
int ly = 32 + (interrupt_in_data[1] * 27) / 255;
|
||||||
rect_filled(lx - 1, ly - 1, 3, 3);
|
rect_filled(lx - 1, ly - 1, 3, 3);
|
||||||
|
// L3 (left stick click) — invert the whole box as a pressed indicator.
|
||||||
|
if (interrupt_in_data[8] & 0x40) rect_invert(kContentX, 30, 32, 32);
|
||||||
|
|
||||||
rect_outline(96, 30, 32, 32);
|
rect_outline(96, 30, 32, 32);
|
||||||
int rx = 98 + (interrupt_in_data[2] * 27) / 255;
|
int rx = 98 + (interrupt_in_data[2] * 27) / 255;
|
||||||
int ry = 32 + (interrupt_in_data[3] * 27) / 255;
|
int ry = 32 + (interrupt_in_data[3] * 27) / 255;
|
||||||
rect_filled(rx - 1, ry - 1, 3, 3);
|
rect_filled(rx - 1, ry - 1, 3, 3);
|
||||||
|
// R3 (right stick click) — invert the whole box.
|
||||||
|
if (interrupt_in_data[8] & 0x80) rect_invert(96, 30, 32, 32);
|
||||||
|
|
||||||
// L2/R2 analog trigger bars (vertical, fill from bottom). L2 sits
|
// L2/R2 analog trigger bars (vertical, fill from bottom). L2 sits
|
||||||
// just right of the shifted left stick box.
|
// just right of the shifted left stick box.
|
||||||
@@ -1019,6 +1053,85 @@ __attribute__((noinline)) void render_screen_triggers() {
|
|||||||
flush_fb();
|
flush_fb();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- IMU calibration (DS5 feature report 0x05) ---------------------------
|
||||||
|
// The DualSense ships per-unit gyro/accel calibration in feature report 0x05,
|
||||||
|
// which bt.cpp already fetches and caches at connect (init_feature). Parsing it
|
||||||
|
// lets the Gyro Tilt screen and the tilt->RGB lightbar mode use bias- and
|
||||||
|
// sensitivity-corrected accel instead of raw counts, so the tilt dot recenters
|
||||||
|
// per controller. Parse + apply mirror SDL's SDL_hidapi_ps5.c (zlib-licensed)
|
||||||
|
// LoadCalibrationData/ApplyCalibrationData (credit); feature_data[0x05]'s byte
|
||||||
|
// layout matches SDL's data[] (index 0 = report id, calibration words from 1).
|
||||||
|
//
|
||||||
|
// imu_apply keeps accel in the same +-8192 == 1g count space the callers already
|
||||||
|
// scale by, so existing /8192 (gyro screen) and +-8192 (lightbar) math is
|
||||||
|
// unchanged — calibration only removes the per-axis zero offset and corrects gain.
|
||||||
|
struct ImuCal { int16_t bias; float sens; }; // 0..2 gyro P/Y/R, 3..5 accel X/Y/Z
|
||||||
|
ImuCal g_imu_cal[6];
|
||||||
|
bool g_imu_cal_valid = false; // a plausible calibration was loaded
|
||||||
|
bool g_imu_cal_tried = false; // 0x05 has been seen this connection (good or bad)
|
||||||
|
|
||||||
|
constexpr float kGyroResPerDeg = 1024.0f;
|
||||||
|
constexpr float kAccelResPerG = 8192.0f;
|
||||||
|
|
||||||
|
inline int16_t cal_ld16(const std::vector<uint8_t>& d, int i) {
|
||||||
|
return (int16_t)((uint16_t)d[i] | ((uint16_t)d[i + 1] << 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((noinline))
|
||||||
|
void imu_cal_parse(const std::vector<uint8_t>& d) {
|
||||||
|
g_imu_cal_valid = false;
|
||||||
|
if (d.size() < 35) return; // SDL requires >= 35 calibration bytes
|
||||||
|
|
||||||
|
const int16_t gPB = cal_ld16(d, 1), gYB = cal_ld16(d, 3), gRB = cal_ld16(d, 5);
|
||||||
|
const int16_t gPp = cal_ld16(d, 7), gPm = cal_ld16(d, 9);
|
||||||
|
const int16_t gYp = cal_ld16(d, 11), gYm = cal_ld16(d, 13);
|
||||||
|
const int16_t gRp = cal_ld16(d, 15), gRm = cal_ld16(d, 17);
|
||||||
|
const int16_t gSp = cal_ld16(d, 19), gSm = cal_ld16(d, 21);
|
||||||
|
const int16_t aXp = cal_ld16(d, 23), aXm = cal_ld16(d, 25);
|
||||||
|
const int16_t aYp = cal_ld16(d, 27), aYm = cal_ld16(d, 29);
|
||||||
|
const int16_t aZp = cal_ld16(d, 31), aZm = cal_ld16(d, 33);
|
||||||
|
|
||||||
|
const float num = (float)(gSp + gSm) * kGyroResPerDeg;
|
||||||
|
g_imu_cal[0] = { gPB, num / (float)(gPp - gPm) };
|
||||||
|
g_imu_cal[1] = { gYB, num / (float)(gYp - gYm) };
|
||||||
|
g_imu_cal[2] = { gRB, num / (float)(gRp - gRm) };
|
||||||
|
|
||||||
|
int16_t r;
|
||||||
|
r = aXp - aXm; g_imu_cal[3] = { (int16_t)(aXp - r / 2), 2.0f * kAccelResPerG / (float)r };
|
||||||
|
r = aYp - aYm; g_imu_cal[4] = { (int16_t)(aYp - r / 2), 2.0f * kAccelResPerG / (float)r };
|
||||||
|
r = aZp - aZm; g_imu_cal[5] = { (int16_t)(aZp - r / 2), 2.0f * kAccelResPerG / (float)r };
|
||||||
|
|
||||||
|
// Sanity gate (same as SDL): a wild bias or a gain off by >50% means a bad
|
||||||
|
// factory cal or a short/garbled read — fall back to raw rather than amplify it.
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
const float divisor = (i < 3) ? 64.0f : 1.0f;
|
||||||
|
const int ab = g_imu_cal[i].bias < 0 ? -g_imu_cal[i].bias : g_imu_cal[i].bias;
|
||||||
|
float gain = 1.0f - g_imu_cal[i].sens / divisor;
|
||||||
|
if (gain < 0) gain = -gain;
|
||||||
|
if (ab > 1024 || gain > 0.5f) return; // leave g_imu_cal_valid = false
|
||||||
|
}
|
||||||
|
g_imu_cal_valid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll once per frame from oled_loop: parse 0x05 the first time it is available
|
||||||
|
// for this controller, and reset on disconnect so the next controller re-reads.
|
||||||
|
void imu_cal_service() {
|
||||||
|
if (!bt_is_connected()) { g_imu_cal_valid = false; g_imu_cal_tried = false; return; }
|
||||||
|
if (g_imu_cal_tried) return;
|
||||||
|
auto d = bt_peek_feature(0x05);
|
||||||
|
if (d.size() < 35) return; // not arrived yet — retry next frame
|
||||||
|
imu_cal_parse(d);
|
||||||
|
g_imu_cal_tried = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// index 0..2 gyro, 3..5 accel. Returns the calibrated value in the same count
|
||||||
|
// scale the raw value used (+-8192 == 1g for accel); identity when no valid
|
||||||
|
// calibration is loaded, so behaviour matches the pre-calibration firmware.
|
||||||
|
inline int16_t imu_apply(int index, int16_t raw) {
|
||||||
|
if (!g_imu_cal_valid) return raw;
|
||||||
|
return (int16_t)((float)(raw - g_imu_cal[index].bias) * g_imu_cal[index].sens);
|
||||||
|
}
|
||||||
|
|
||||||
__attribute__((noinline)) void render_screen_gyro() {
|
__attribute__((noinline)) void render_screen_gyro() {
|
||||||
fb_clear();
|
fb_clear();
|
||||||
draw_text(kContentX, 0, "Gyro Tilt");
|
draw_text(kContentX, 0, "Gyro Tilt");
|
||||||
@@ -1027,6 +1140,9 @@ __attribute__((noinline)) void render_screen_gyro() {
|
|||||||
memcpy(&ax, &interrupt_in_data[21], 2);
|
memcpy(&ax, &interrupt_in_data[21], 2);
|
||||||
memcpy(&ay, &interrupt_in_data[23], 2);
|
memcpy(&ay, &interrupt_in_data[23], 2);
|
||||||
memcpy(&az, &interrupt_in_data[25], 2);
|
memcpy(&az, &interrupt_in_data[25], 2);
|
||||||
|
ax = imu_apply(3, ax); // bias/sensitivity-corrected accel (identity if no cal)
|
||||||
|
ay = imu_apply(4, ay);
|
||||||
|
az = imu_apply(5, az);
|
||||||
char buf[16];
|
char buf[16];
|
||||||
snprintf(buf, sizeof(buf), "X%+5d", ax); draw_text(kContentX, 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), "Y%+5d", ay); draw_text(50, 10, buf);
|
||||||
@@ -1036,8 +1152,15 @@ __attribute__((noinline)) void render_screen_gyro() {
|
|||||||
rect_outline(bx, by, bw, bh);
|
rect_outline(bx, by, bw, bh);
|
||||||
for (int x = bx + 1; x < bx + bw - 1; x++) px(x, by + bh / 2, true);
|
for (int x = bx + 1; x < bx + bw - 1; x++) px(x, by + bh / 2, true);
|
||||||
for (int y = by + 1; y < by + bh - 1; y++) px(bx + bw / 2, y, true);
|
for (int y = by + 1; y < by + bh - 1; y++) px(bx + bw / 2, y, true);
|
||||||
int dx = ((int)ax * (bw / 2 - 3)) / 8192;
|
// Plot the two axes that read ~0 when the controller lies flat: X (roll,
|
||||||
int dy = ((int)ay * (bh / 2 - 3)) / 8192;
|
// left/right) and Z (pitch, fwd/back). Gravity rests on Y when flat, so
|
||||||
|
// driving the dot from Y pegged it to the bottom edge at rest — using Z
|
||||||
|
// keeps the dot centred flat and it tracks as you tilt. (Readout above
|
||||||
|
// still shows all three raw axes.)
|
||||||
|
// Negated so the dot follows the tilt direction: tilt left -> dot left,
|
||||||
|
// tilt forward -> dot up (gravity pulls the opposite way on the axis).
|
||||||
|
int dx = -((int)ax * (bw / 2 - 3)) / 8192;
|
||||||
|
int dy = -((int)az * (bh / 2 - 3)) / 8192;
|
||||||
int cx = bx + bw / 2 + dx;
|
int cx = bx + bw / 2 + dx;
|
||||||
int cy = by + bh / 2 + dy;
|
int cy = by + bh / 2 + dy;
|
||||||
if (cx < bx + 2) cx = bx + 2;
|
if (cx < bx + 2) cx = bx + 2;
|
||||||
@@ -1216,6 +1339,9 @@ void lightbar_compute_mode(int mode, uint32_t now_ms) {
|
|||||||
memcpy(&ax, &interrupt_in_data[21], 2);
|
memcpy(&ax, &interrupt_in_data[21], 2);
|
||||||
memcpy(&ay, &interrupt_in_data[23], 2);
|
memcpy(&ay, &interrupt_in_data[23], 2);
|
||||||
memcpy(&az, &interrupt_in_data[25], 2);
|
memcpy(&az, &interrupt_in_data[25], 2);
|
||||||
|
ax = imu_apply(3, ax); // calibrated accel keeps the +-8192 == 1g scale below
|
||||||
|
ay = imu_apply(4, ay);
|
||||||
|
az = imu_apply(5, az);
|
||||||
const int rr = ((int)ax + 8192) * 255 / 16384;
|
const int rr = ((int)ax + 8192) * 255 / 16384;
|
||||||
const int gg = ((int)ay + 8192) * 255 / 16384;
|
const int gg = ((int)ay + 8192) * 255 / 16384;
|
||||||
const int bb = ((int)az + 8192) * 255 / 16384;
|
const int bb = ((int)az + 8192) * 255 / 16384;
|
||||||
@@ -1737,6 +1863,9 @@ void oled_loop() {
|
|||||||
// Track charge progress every frame — before the power-ladder early-returns
|
// Track charge progress every frame — before the power-ladder early-returns
|
||||||
// below, so step timing stays correct even while the panel is dimmed/off.
|
// below, so step timing stays correct even while the panel is dimmed/off.
|
||||||
sample_charge_eta();
|
sample_charge_eta();
|
||||||
|
// Parse the DS5's per-unit IMU calibration once it lands (no-op until then),
|
||||||
|
// so the tilt screen + tilt->RGB lightbar use corrected accel. See imu_apply().
|
||||||
|
imu_cal_service();
|
||||||
// Drive the controller LED every frame (any screen / power state): charging
|
// Drive the controller LED every frame (any screen / power state): charging
|
||||||
// pulse, selected OLED mode, or hand-off to the host. See lightbar_service().
|
// pulse, selected OLED mode, or hand-off to the host. See lightbar_service().
|
||||||
lightbar_service();
|
lightbar_service();
|
||||||
|
|||||||
Reference in New Issue
Block a user