Esodland/EyeToyPSVita

GitHub: Esodland/EyeToyPSVita

PS Vita / PS TV 自制软件项目,实现了基于摄像头的动作检测游戏引擎以及端到端的 PlayStation Move 手柄蓝牙驱动,并附带了详尽的硬件逆向文档。

Stars: 0 | Forks: 0

# EyeToy PS Vita — 以及适用于 PS Vita / PS TV 的可用 PS Move 驱动 PS Vita / PS TV Homebrew:一个基于摄像头动作捕捉的游戏引擎, 秉承 *EyeToy: Play* 的理念,完全 *from scratch*(从零开始)编写 —— 外加对 原版 PS2 游戏的研究。 个人学习项目。发布以下内容是因为**多处硬件探测结果在其他地方从未有过记录**,并且 可能会对其他人有所帮助,特别是关于 PS Move 和 Vita 的 Bluetooth 的部分。 ## 状态 | 阶段 | 主题 | 状态 | |---|---|---| | **1** | 前置摄像头的动作捕捉 | **已完成**,在主机上验证通过 | | **2.1** | USB Host 内核插件 (PS TV) | 待办 | | **2.2** | PS Eye 驱动 (OV534) | 待办 | | **2.3** | PS Move | **可用** — 配对,HID 数据流,传感器以 g 和 °/s 为单位 | | **3** | *EyeToy: Play* (PS2) 的逆向工程 | 待办 | **阶段 1** 以稳定的 60 fps 运行:640×480 的摄像头画面以零拷贝(zero-copy)方式显示 (YUV 转换由 GPU 完成),在 160×120 的网格上进行动作检测耗时约 3 ms,具有自适应环境噪声阈值,以及一个“Bubble Pop”游戏原型。 **阶段 2.3 — PS Move 手柄在 PS Vita 上实现了端到端的运行**,据我们所知, 这是前所未有的: - 取得了 Bluetooth 配对,随后**由 `SceBt` 自身进行记录** —— 此后 连接无需任何 hook 即可成功; - 完整的 HID 数据流:178 字节的 descriptor,连续接收 input report `0x01`,sphere(光球)和振动器可控; - 传感器已解码并在**主机上通过物理学验证**,而不仅仅是在 PC 上(见下表); - **解码了出厂校准**(*feature* report `0x10`):手柄提供的是 **g** 和 **°/s**,而不是任意单位; - 一个暴露给应用程序的 API —— `psmove_lire_etat`、 `psmove_couleur_sphere`、`psmove_vibreur`、`psmove_lire_calibration`、 `psmove_remesurer_zeros` —— 提供整合后的按键、扳机、电池、 加速度计、陀螺仪和磁力计数据; - 手柄的地址是在**运行时**从主机上的一个文本文件中读取的: 使用一个已编译好的、自带手柄地址的 `.skprx` 不需要 VitaSDK。 这方面剩下的工作:在摄像头画面中对光球进行追踪,这将最终使项目的两 部分交汇。 ## 可能对其他人有帮助的内容 ### PS Move 手柄的 HID 协议 完全通过测量记录,未借用任何现有文档 — — 参见 **[`docs/psmove_protocole.md`](docs/psmove_protocole.md)**。 - Input report `0x01`(49 字节):按键、trigger、加速度计、 陀螺仪、磁力计、电池、时间戳。 - Output report `0x02`:光球的 RGB 颜色和振动器。 - *Feature* report `0x04` / `0x05`:在 vendor collection `0xff02` 上读取和 **写入**已配对主机的地址。 - 传感器以 **120 Hz** 采样(每个 report 包含两组测量数据)。 - **USB 不会传输任何数据**:所有数据流都通过 Bluetooth。因此, 配对是不可或缺的,没有任何线缆可以绕过它。 - ⚠️ 字节 `[4]` 混合了两个按键(第 7-6 位)和一个**序列计数器** (第 3-0 位)。直接读取原始数据会在每次递增时产生幽灵按压。 验证方法:与其盲目相信假定的位拆分,不如验证一个**物理不变量** —— 在静止状态下,加速度计矢量的模长必须为 1 g,而磁力计矢量的模长必须为地磁场强度。 在 **0.25%** 和 **0.65%** 的误差下测量保持稳定,这绝非错误的字节分组 所能产生的结果。正是这个测试揭示了磁力计的 Y 轴打包方式与 另外两个轴完全相反。 同样的测试在 **Vita** 上,跨越整个 Bluetooth 链路,手柄保持静止的状态下重新进行了 —— 并且给出了相同的结论: | 物理量 | 在 PC 上 | 在 Vita 上 | |---|---|---| | 加速度模长稳定性 | 0.25% | **0.30%** | | 磁力计模长稳定性 | 0.65% | **1.17%** | | 静止状态下的陀螺仪零偏 | (−23, +55, −3) | **(−26, +53, +7)** | 在两个完全不同的软件栈上,精确到单位地重新找出了该传感器的特定硬件偏差,这为解码问题画上了句号。 ### 出厂校准 — *feature* report `0x10` 完全通过测量进行解码([协议的第 8 节](docs/psmove_protocole.md)), 正是它将整数转换为 g 和 °/s。 - **三个 49 字节的数据块**,以**轮询方式**提供:连续两次读取 会得到 `00 01 82`,随后是 `01 82 00`。根据字节 `[1]` 进行排序,其 第 7 位标记了最后一个数据块。按照到达顺序拼接会产生 混合的 payload,而且没有任何标识指出这一点。 - 141 个连续字节:六个加速度计矢量(每个面对应一个)、静止状态下的陀螺仪、 以及标准旋转状态、温度,接着是**八个作用尚未确定的浮点数** —— 如实标记,而不是生搬硬套某种解释。 - 标准速率为 **480 °/s**(80 转/分)。单位是通过 饱和度来确定的:如果单位是十分之一度,满量程将降至 330 °/s, 手柄稍微一动就会达到饱和。 - ❌ **无法从 Vita 读取此 report。** 带有 类型 2 和 3 的 `ksceBtHidTransfer` 接受了请求,发出了一个 `0x0C` 事件 —— 在其他情况下从未见过 —— 而且**从不写入缓冲区**。收到回执并不代表 有了响应。因此,校准只能在 PC 上通过 USB 获取一次。 **最有用的结果是由测量强加的一种划分:** | | 结论 | |---|---| | **出厂增益** | 良好 —— 在超定测试中残差为 0.57% | | **出厂零点** | 已失效 —— 需要在运行时重新测量 | 陀螺仪毫不含糊地证明了这一点,将手柄平放即可直接读出其零点: 该数据块显示为 (−27, −125, +105),PC 测量值为 (−23, +55, −3),而 Vita 为 (−24, +52, +6)。两次独立的测量相互吻合,但 两者都偏离了数据块。公认的合理解释 —— **漂移**:比例因子在 时间内是稳定的,而 offset 则不然,何况这个手柄已经有十四年的历史了。 因此,插件的工作原理是:**增益来自出厂设置,零点在运行中测量**。 陀螺仪在静止一秒钟后即可归零; 加速度计则需要将每个轴在两个方向上分别呈现,其零点位于 两个极值的中点。获取到的零点保存在 `ux0:data/psmove_zeros.txt` 中,并在下次启动时重新读取。 在十二个姿态下测得的效果:模长范围 **10.84% → 1.72%**,平均值 **0.9996**。该平均值验证了 *增益* —— 调整后的零点缩小了范围 但并未重新修正中心。 ⚠️ **覆盖率安全限制必须严格收紧。** 第一个版本允许 10% 的偏差: 一个轴的增益达到了 90.4%,而其中一个方向从未被呈现过, 发布了一个带有 300 个单位误差的虚假零点,表面看起来却很好。采用的阈值 是 **98%**,这是可以论证的:静止时没有任何一个分量 超过 1 g,因此要求半振幅 ≥ 98% 会迫使两个极值 都超过 96%。*过于宽松的限制只会起到安抚作用,而不能提供保护。* ### PS Vita 的 Bluetooth (`SceBt`, kernel) - **`mac0`/`mac1` 上的地址拆分**:48 位地址被切成 两部分 —— `mac1` = 高 16 位,`mac0` = 低 32 位。 文档中未记载,但缺了它什么都无法运行。 - **事件表**:`0x01` 搜索结果,`0x02` 搜索结束, `0x04` 请求 link key,`0x05` 连接被接受,`0x06` 断开连接,`0x08` 请求连接,`0x09` 未经配对的连接请求,`0x0A`–`0x0C` HID 响应。 - **`ksceBtReadEvent` 不会阻塞**:当无事发生时,它会返回成功 并带有一个空事件。一个简单的 naïve 循环会使主机死机。 - **一个 callback 属于注册它的线程。** 如果在其他地方注册, `ksceBtRegisterCallback` 会返回 `0`,但随后的每次读取都会失败并返回 `CB_NOT_REGISTERED` —— 这种无声的故障表现得就像 设备不存在一样。 - **正在进行的 inquiry 会阻止 `ksceBtStartConnect`** (`CONNECT_START_BUSY`)。 - **主机的 Bluetooth 地址等于其 Wi-Fi MAC + 1**,没有任何 API 能在 主机上获取它:必须从外部读取。 - **配对数据库位于 registry 中**, 即 `/CONFIG/BT/NN/info`,`ksceRegMgrSetKeyBin` 可向其写入 —— 即使在读取被拒绝时也是如此。**但是千万不要** **使用这种方法**,请看紧接着的下文。 ### ⚠️ 让我们白白浪费了两天时间的陷阱 自己手动构造一个配对条目**表面上看起来可行,但实际上会彻底破坏一切**。 固件接受了它,手柄出现在了 Bluetooth 设置中,连接得以建立 —— 但之后什么也 无法正常工作。 因为此时 SceBt 看到的是一个“已知设备”,它会**跳过 SDP 查询**,导致既没有 descriptor 也没有 HID 通道。所有的这些 症状,全都是由这一个条目引起的: - `ksceBtHidTransfer` 返回 **`HID_NO_CAP`**; - `ksceBtHidGetReportDescriptor` 返回 0 且缓冲区为空; - `ksceBtGetVidPid` 返回 **`0006:0001`** 而不是 `054C:03D5`; - 配对条目显示为 **`vid=0000 pid=0000`**; - interrupt 通道 `0x13` 不存在,control 通道 `0x11` 容量为零, 而 vendor API `ksceBtHidVu*` 返回 **`0x802F4006`** / **`0x802F4007`**。 **解决方法全靠一个调用:`ksceBtDeleteRegisteredInfo(mac0, mac1)`。** 这五个症状随之消失。SDP 得以执行,真实身份显现, 178 字节的 descriptor 被成功读取,input report 随之开始涌入。 推论:不要试图通过 `taiInjectDataForKernel` 来绕过那些检测 HID 容量缺失的校验。我们试过,它掩盖了症状但并未修复任何问题 —— 并且我们在 `SceBt+0x12DA6` 处移除大小检查的 patch 引入了一个 **主机上的内核缓冲区溢出漏洞,这影响了主机上所有** Bluetooth 设备的共同调用路径。 ### 首次配对真正必需的条件 - 通过 USB 使用 *feature* report `0x05`(文档的第 4 节), 将主机的 Bluetooth 地址写入手柄。Vita 的地址等于其 **Wi-Fi MAC + 1**。 - 两个针对 `SceBt` 的 taiHEN hook,借鉴自 `ds3vita` / `ds4vita` (`SceBt+0x199C8` 和 `SceBt+0x147E4`),否则连接会被拒绝并返回 `NO_REG`。这些 offset 专属于固件 3.60。 - **一旦取得了合法的配对,这些 hook 就不再需要了**:SceBt 会自行记录设备,没有它们连接也能成功。因此, 只要不存在有效条目,插件就会安装它们,这会自动关闭主机处于 宽松状态的窗口期。 - 位于 `SceBt0x147E4` 处函数的第三个参数是 **设备的 Bluetooth 地址**(高位为 `mac1`),这就允许将绕过行为限制在 特定的手柄上,而不是接受任何传入连接 —— 原版就是这样做的。 ### 从应用程序中调用内核插件 - 应用程序可以在运行时加载 `.skprx` (`taiLoadStartKernelModuleForUser`),**但在同一会话中将无法调用其导出的 任何内容**:应用程序的导入是在进程启动时解析的。 调用未解析的 symbol 会导致应用程序崩溃 (`psp2dmp`)。 - **weak linkage 无法解决这种情况**:即使模块确实已驻留, `lib*_stub_weak.a` 中的 symbol 仍然保持为 null。 - 可靠的信号是**加载的返回代码**: `SCE_KERNEL_ERROR_MODULEMGR_OLD_LIB` (**`0x8002D013`**) 表示该 库已注册,因此该模块先于进程存在, 所以 API 是可用的。 - 加载内核模块需要 eboot 为 **`UNSAFE`** 属性:否则 taiHEN 会返回 `TAI_ERROR_NOT_ALLOWED` (**`0x90010009`**)。 - ⚠️ **`taiLoadStartKernelModule` 无法读取 `app0:`** —— 它返回 **`0x80010013`** (ENODEV)。事后想想这很符合逻辑:`app0:` 是专属于 调用进程的挂载点,内核端完全没有理由知道如何解析它。因此,一个 VPK **可以** 携带自己的插件,但应用程序必须先将其**复制**到 `ux0:data/` —— 它有权读取 `app0:`,而内核则没有。这就是 `src/drivers/psmove_plugin.cpp` 所做的操作,它将插件重写为一个独立的名称, 并且每次启动都会重新写入,以便 VPK 的更新始终能被考虑在内,而 永远不会覆盖开发版本。 - 实际操作中的必然结果:**由于内核模块永远不会被卸载,新的 `.skprx` 只有在重启后才会生效。** 覆盖文件并不能替换内存中的模块 —— 这时你以为在测试新二进制文件,实际上测试的还是旧的。 - 内核插件的线程**寿命长于加载它的应用程序**。因此它 即使在没有任何程序打开时也能提供服务:在这里,它会检测一个 标记文件并调用 `kscePowerRequestColdReset()`,这样可以实现重启的 自动化,**而无需启动任何其他程序**。 ### Vita 的前置摄像头 - 640×480 的 `YUV422_PACKED` 格式:实际的字节顺序是 **YVYU** (`SCE_GXM_TEXTURE_FORMAT_YVYU422_CSC0`)。 - 摄像头缓冲区位于**非缓存**内存中:CPU 进行 YUV → RGBA 转换的代价 是 **113 ms 每 frame**。解决方法是让 GPU 通过 `sceGxmMapMemory` 读取缓冲区 —— 零拷贝,零 CPU 开销。 - 图像设置(对比度、亮度、清晰度)**会被接受但没有实际效果**, 并且 `sceCameraGetGain` 会失败。只有曝光起作用。 ## 构建 前提条件:[VitaSDK](https://vitasdk.org/),CMake ≥ 3.16,Ninja。 ``` cmake -B build -S . -G Ninja cmake --build build ``` VPK 输出在 `build/EyeToyPSVita.vpk` 中,并且**它内置了内核插件**: 只需安装它即可,无需通过 FTP 发送 `.skprx`。应用程序会首 先在 `ux0:data/` 中查找 —— `deploy.ps1` 会将正在开发的版本放在这里 —— 然后查找 `app0:`,这是正常安装 后的路径。 内核插件需单独构建 (`plugin/`),并且在应用程序**之前**构建: 它既会生成应用程序嵌入的 `.skprx`,也会生成应用程序所链接的 导入库 (`plugin/build/stubs/`)。 ``` cmake -B plugin/build -S plugin -G Ninja cmake --build plugin/build ``` **手柄的地址无需在编译时知晓**:插件会在启动时读取主机上的 `ux0:data/psmove_adresse.txt` —— 类似于 `00:06:F7:11:22:33` 这样的一行。根目录下的 `.env` 可以提供一个备选值, 但它是可选的。 ⚠️ **插件没有列在 `config.txt` 中**:由应用程序负责加载, 这可以避免有缺陷的模块导致主机进入 bootloop —— 我们 已经遇到过两次这种情况。相关的代价如前文所述:重启后, 第一次启动会装载插件,第二次启动才会使用它。 ### 开发周期 `tools/deploy.ps1` 依次执行 构建 → 关闭 → 发送 → 启动,无需经过 VPK 的安装过程。加上 `-Reboot` 参数,它还会重启主机并 连续执行两次必要的启动 —— 只要 `.skprx` 发生了改变,这就是 **必须的**,否则你测试的还是旧模块。 ``` .\tools\deploy.ps1 # application seule .\tools\deploy.ps1 -Reboot # après une modification du plugin ``` 在游戏中,手柄上有两个命令:**按住 SELECT 键 1 秒** 会清除零点并 重新启动校准向导,**按住 START 键 2 秒** 会重启主机 (松开则取消)。 ## 组织结构 ``` src/ moteur : caméra, détection de mouvement, rendu, jeu plugin/ plugin kernel .skprx (Bluetooth PS Move) plugin/psmove_api.h contrat partagé noyau <-> application tools/ sondes et scripts d'analyse (Python, PowerShell) docs/ relevés de protocole re/ reverse engineering d'EyeToy: Play (Voie B) ``` 主机上所需的文件: | 路径 | 作用 | 写入者 | |---|---|---| | `ux0:data/psmove_adresse.txt` | 手柄的 Bluetooth 地址,单独一行 | **您** | | `ux0:data/psmove_calib.bin` | 出厂校准,通过 PC 上的 USB 获取 | 您 | | `ux0:data/psmove_zeros.txt` | 测量到的零点,可在不同会话间复用 | 插件 | | `ux0:data/psmove_bt.log` | 插件日志,上限 400 行 | 插件 | | `ux0:data/psmove_bt_app.skprx` | 嵌入的插件副本,每次启动都会重写 | 应用程序 | | `ux0:data/psmove_bt.skprx` | 开发版插件,优先级高于前者 | `deploy.ps1` | **只有第一个是必需的** —— 插件会随 VPK 一起发布。如果没有 校准,传感器将保持原始状态;如果没有零点,它们将在屏幕向导的 指引下在大约三十秒内重新测量完毕。要生成校准文件,请使用 USB 线连接到 PC: ``` python tools/psmove_hid.py calibration usb # -> captures/psmove_calib.bin ``` 零点文件除了包含零点外,还包含增益,**作为手柄的指纹**: 如果插上另一个手柄,在校准文件名称不变的情况下会改变校准结果, 而且在其他地方测量的零点如果不加提示直接使用将会是错误的。 `CLAUDE.md` 包含了详细的工作笔记:测得的 frame 预算、 遇到的陷阱以及技术决策的历史记录。 ## 方法论 项目依靠测量来推进,而不是依靠直觉。走不通的死胡同与 成功的方法被同等记录 —— 一个负面结果连同尝试过的内容,可以避免 其他人重蹈覆辙。 几条非常有用的原则: 1. **在未知中寻找已知。** 如果去追踪两个已经在其他地方确立的数值,解码 一个格式的速度要比盲目猜测其结构快得多。 2. **在构建理论之前先记录返回代码。** 有好几次,被认为是硬件导致的 沉默,实际上是因为一个失败的调用没有被记录下来。 3. **绝对不要用推导出模型的数据来验证模型本身。** 仅仅因为 这个陷阱,校准工作就连续遭遇了三次虚假的成功:一个 0.012% 的极小范围其实什么也没测量到,因为独立变量在其中 的权重只有 5·10⁻⁵ ;一个在六个姿态上进行六参数拟合的模型,在构造上 本身就是精确的;还有一个探针,使用它 希望读到的响应预填充了它自己的缓冲区。**计算自由度,并在 完全不相关的数据集上进行交叉验证。** 4. **检查测量是否在假定的条件下进行的。** 一个结论 —— “出厂 偏差导致性能下降” —— 被证明与事实完全相反,因为它来自于*在运动* 状态下采集的姿态,而在运动状态下,加速度的模长根本没有 理由等于 1 g。 5. **当预期行为无法理解时,你缺的是一块仪表盘,而不是一句话。** 手动 校准从一个晦涩的练习变成了只需三十秒的操作,这一切就发生在有两根进度条 直接显示对齐和静止状态的那一天。 ## 许可证和内容 专为该项目编写的代码。此处未对任何来自原版游戏的素材、二进制文件或反编译代码 进行版本控制 —— 逆向工程部分仅包含 笔记和重写的代码。
标签:PS Vita, 外设逆向工程, 嵌入式开发, 摄像头动作检测, 游戏开发, 自制软件, 蓝牙驱动