DS5Dongle 支持 DualSense Edge:DSE Profile 解锁握手协议深度解析(附时序与状态机)
【免费下载链接】DS5DongleTurn your Pico 2 W into a DualSense 5 dongle.项目地址: https://gitcode.com/gh_mirrors/ds/DS5Dongle
DS5Dongle 是一款把 Raspberry Pi Pico 2 W 变成 DualSense 5 无线手柄适配器的开源固件项目,它不仅能让 DS5 手柄无线连接电脑,如今还新增了DualSense Edge(DSE)手柄支持:通过复刻 Play Station 配件应用的"解锁握手"协议,让手柄里保存的 4 个自定义 Profile 在首次打开 PS Accessories 应用时就能被正确读取。本文将带你读懂这套 DSE Profile 握手协议的完整时序与状态机设计。
⚡ 为什么 Edge 手柄的 Profile 这么难读?
普通 DualSense 手柄接入 USB 后,主机随时可以读取它的各种状态报告。但 DualSense Edge 的自定义 Profile(手柄内置的 4 个按键/摇杆配置档位)并不直接开放:
- PS Accessories 应用通过0x70–0x7B共 12 个 feature 报告读取 Profile 快照;
- 但手柄只在收到一条特殊的 SET 0x80 解锁命令并处理完毕后,才会把这些报告"填充"好;
- 这条解锁命令的处理耗时约3.5 秒,且命令必须携带合法的CRC32 校验尾,否则会被手柄以
HANDSHAKE 0x04(参数无效)拒绝。
这就形成了一个两难:等 3.5 秒再连 USB 会拖慢设备识别;不等又会让应用读到空快照,Profile 显示"未分配"。DS5Dongle 的 DSE 模块(src/dse.h 中有完整协议注释)的解法是——先连 USB,边等边预取,用"NAK 重试"骗过应用的缓存逻辑。
🔍 第一步:如何识别 DualSense Edge
手柄蓝牙配对成功后,固件会读取0x20 固件信息报告(src/bt.cpp):
若报告第 23 字节为0x44,则判定为 DSE 手柄(日志打印
Connected DSE Controller),随即触发dse_on_connect()启动解锁流程。
识别结果还影响 USB 端点描述符:src/usb_descriptors.cpp 中会根据is_dse把设备串名上报为 "DualSense Edge Wireless Controller",让主机系统正确显示手柄型号。
⏱ 解锁握手时序:从连接就绪到快照可读
下面是 DSE 模块复刻的完整握手时序(源码见 src/dse.cpp):
Pico 2 W(DS5Dongle) DualSense Edge 手柄 |--------------------------------->| ① SET 0x65:原样回显 0x20 报告体(63 字节) |<----------------------------------| ② 收到 SET 0x80 {0x70,0x01}(带 CRC32) | (同时 USB 立即枚举,游戏可玩)| ③ 手柄后台处理解锁,约 3.5 秒 |--------------------------------->| ④ 4 秒后开始节奏预取:GET 0x70 |<----------------------------------| ⑤ 每 80ms 一个 GET,直到 0x7B | ⑥ profiles_ready 置位,应用读取成功三个关键动作:
- SET 0x65 回显:把手柄刚上报的 0x20 固件报告体原样回显回去(原生主机也是这么做的),完成"握手确认";
- SET 0x80 解锁:报文
{0x70, 0x01, ...}由set_feature_data()自动追加 CRC32 校验后发出(src/bt.cpp),缺失校验会被手柄拒收; - USB 立即连接:不阻塞设备枚举,游戏可以马上玩,Profile 读取被门控住。
🚦 状态机:profiles_ready 门控与 NAK 重试
DSE 模块内部只有几个非常简洁的状态变量(src/dse.cpp):
| 状态变量 | 取值 | 含义 |
|---|---|---|
unlock_phase | 0 | 空闲,解锁流程未启动 |
unlock_phase | 1 | 已发出 SET 0x80,等待手柄准备快照 |
profiles_ready | false | 快照未就绪,USB 读 0x70–0x7B 一律返回 0(NAK) |
profiles_ready | true | 快照就绪,应用读取返回真实数据 |
门控逻辑在 USB 的 GET_REPORT 回调里(src/main.cpp):
应用读取 Profile 报告时,若
dse_profiles_ready()为 false,固件故意返回 0(NAK),同时仍在后台发起蓝牙 GET。PS Accessories 应用在加载时本来就反复轮询这些报告,所以它只会安静地重试——直到 4 秒后预取完成、门控放开,应用拿到的就是完整快照,而不会把空数据缓存下来。
这是整套协议最巧妙的地方:利用应用自身的重试机制,把"等待"的代价转移给了应用侧,用户无感知。
🔄 保存 Profile 后的快照再生周期
手柄的 Profile 快照有个"坑":你在应用里保存一个新 Profile,写入了存储区(SET 0x60–0x62),但读回用的快照并不会自动更新——原生情况下要等下次打开应用才生效。
DSE 模块在检测到 Profile 写入后,会复刻 PS 应用自己的刷新周期(src/dse.cpp):
t=0 转发 SET 0x60~0x62(保存 Profile) t≈500ms 重新发送 SET 0x80 解锁命令 t≈1s~2.5s 轮询 GET 0x81 状态报告 ×6 次(间隔 250ms) t≈5.5s 以 80ms 节奏重新预取 0x70–0x7B 快照于是保存立即生效,无需重启手柄或重新打开应用。
⚠️ 为什么是 80ms 一拍?——节奏预取的设计考量
源码注释(src/dse.cpp)记录了一个实测现象:
一次性突发发出全部 12 个 Profile GET 请求会撑爆 L2CAP 控制通道的流控窗口,导致部分请求被静默丢弃——表现为某个 Profile 在应用里显示"not assigned"。改为每 80ms 发一个 GET,读取才稳定可靠。
12 个报告 × 80ms ≈ 1 秒,加上解锁等待的 4 秒,用户从插上手柄到 Profile 可读大约需要 5 秒,且游戏连接完全不受影响。
🧭 核心模块速查
| 模块 | 职责 |
|---|---|
| src/dse.h | DSE 协议总览与对外接口定义 |
| src/dse.cpp | 解锁握手、节奏预取、保存后再生周期 |
| src/bt.cpp | 蓝牙控制通道分发、Edge 识别(0x44) |
| src/main.cpp | USB GET_REPORT 回调与 NAK 门控 |
| src/usb_descriptors.cpp | Edge 串名上报 |
| tools/wireshark_dualsense_setstate.lua | 抓包辅助脚本,可辅助分析 SET 报文 |
🎯 总结
DS5Dongle 的 DSE 支持展示了嵌入式固件里一个漂亮的工程范式:逆向出主机协议 → 逐字节复刻握手 → 用应用重试机制掩盖异步延迟。整套状态机仅用寥寥百行代码(src/dse.cpp 全文约 160 行),就让 PS Accessories 应用"以为"自己直连着原生蓝牙主机。
如果你也想在自己的 Edge 手柄上体验这套能力,获取固件的方式见 README.md 中的Getting Started章节:下载预编译.uf2拖入 BOOTSEL 模式即可,无需任何额外工具。
【免费下载链接】DS5DongleTurn your Pico 2 W into a DualSense 5 dongle.项目地址: https://gitcode.com/gh_mirrors/ds/DS5Dongle
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考