# GameSir Cyclone 2 — Linux 控制应用
这是一款适用于 GameSir Cyclone 2 手柄的 Linux GUI,通过手柄的
vendor (hidraw) 接口进行通信,协议完全从零开始逆向工程得出。涵盖功能:
- **实时输入视图** — 摇杆、扳机、所有按键(包括 L4/R4/M/Home/
Share 额外按键)、方向键、电池电量及充电状态、固件版本,以及模式警告。
- **配置文件** — 读取活动配置文件并进行切换 (1–4);震动测试。
- **灯光** — 独立 RGB 控制、捕捉到的预设效果、亮度/速度,
音频响应 / 拿起唤醒 / 休眠超时,以及**自定义关键帧
动画编辑器**(添加/删除关键帧、随机化、播放/暂停)。
- **配置编辑器** — 死区、抗死区、摇杆轨迹、灵敏度
曲线(预设**及**可拖动的自定义曲线编辑器)、扳机调节
(微动扳机 + 响应曲线)、震动、回报率以及按键重映射。
- **备份 / 恢复** — 将所有 4 个配置文件及灯光设置快照为 JSON 文件,
并可在日后写回。
- **鼠标模式切换** — 直接在应用中关闭(普通手柄)或开启
KDE/KWin 的“手柄控制光标”行为
(摇杆作为光标的“客厅模式”),此外还提供适用于非 KDE 环境的
EVIOCGRAB 备选方案(Wayland;见状态)。
![status: input, battery, profiles, RGB + keyframes, full config editor, remap, and JSON backup/restore working]
**版本:** `0.1.0-alpha.1` — 一个已知稳定的基线版本(git tag `v0.1.0-alpha.1`)。
剩余的 bug、提议的更改以及待解决的逆向工程问题位于
**[TODO.md](TODO.md)** — 一个持续更新的清单。这是一个业余的逆向工程
项目;你可以随意 fork 并进行自定义。
## 环境要求
- Python 3
- [`hidapi`](https://pypi.org/project/hidapi/) (`import hid`)
- [`dearpygui`](https://pypi.org/project/dearpygui/)
- `xrandr` (可选;仅用于将窗口放置在主显示器上)
```
pip install hidapi dearpygui
```
## 运行说明
手柄必须处于 **Xbox / XInput 模式(按住绿色按键约 2 秒)** —
该 vendor 协议在 PS4/DS4 和 Switch 模式下是无效的。当手柄未处于 Xbox 模式时,
应用会在顶部标题栏发出警告。
**推荐方式 — 无需 `sudo`。** 安装随附的 udev 规则(仅需一次),以便您的
用户可以直接打开手柄的 `hidraw` 节点(及其 `input` 事件
节点,用于下文的鼠标模式修复)— 该规则专门限定于 GameSir 的 USB vendor id,不涉及
其他设备。请**在仓库目录下**(即 `70-gamesir.rules` 所在位置)运行此命令:
```
sudo cp 70-gamesir.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm trigger
```
然后运行(无需重新插拔 — 触发器会重新应用访问 ACL):
```
python3 gamesir_gui.py
```
该规则使用 `TAG+="uaccess"`,这将授予权限给
本地桌面登录的用户。**前缀 `70-` 非常重要**:udev 会按文件名顺序执行规则,
而实际应用 `uaccess` ACL 的是 `73-seat-late.rules` — 编号为
`73`+ 的规则设置 tag 太晚,导致 ACL 永远无法静默授予。在
没有本地 seat 的无头/远程机器上,`uaccess` 不起作用;请改用 group
(`MODE="0660", GROUP="input"`)并将自己添加到该组中。
要确认是否生效,手柄的节点应显示您的 ACL:
`getfacl /dev/hidraw0` → 应包含一行 `user:
:rw-`。
**备选方式 — 使用 `sudo`。** 如果您不想安装规则,由于 `hidraw`
节点默认属于 root 用户,请执行:
```
sudo python3 gamesir_gui.py
```
请注意,在 `sudo` 下,`~` 会解析为 `/root`,因此默认的备份路径会
落入 `/root/` 中 — 这也是推荐使用 udev 规则方式的另一个原因。
## 工作原理(协议说明)
该手柄暴露了一个 **vendor HID 接口**(USB VID `0x3537`),承载着
64 字节的命令/报告通道。以下所有操作均需在 **Xbox 模式**并保持
**持续的心跳信号**(每约 0.5 秒发送一次 `0x0F 0xF2`)。
- **输入**:增强型报告 `0x12` 流式传输摇杆、扳机、IMU、电池状态
(第 36 字节为百分比,第 35 字节 bit0 = 充电状态),以及标准 PS4 报告
无法读取的额外按键(第 60 字节中的 L4/R4/M/Home/Share)。
- **命令**(输出报告 `0x0F`,填充至 64 字节):
- 心跳 `0F F2`
- 获取/设置配置文件 `0F 0B` → `10 0C ` / `0F 07
`
- 震动 `0F 20 66 55 `
- 读取寄存器 `0F 04 ` → `10 05 `
- 写入寄存器 `0F 03 `
- **灯光**位于寄存器 **bank `0x20`**:
- `0x0000` = 活动槽位选择器 (0–3;同时也是 M+摇杆手势的可靠实时回读)
- 槽位记录位于 `0x0001 + slot*0x7c` (124 字节):`[type, 05, param, brightness]`
随后是一系列 RGB 三元组调色板,渲染为重复的 **5-元组帧**
- 帧位置 → 灯光:**0=左侧握把, 1=右侧握把, 2=(无 LED), 3=Profile,
4=Home**。纯色/单灯光颜色 = 类型 `0x01`,在整个记录中平铺一个相同的帧
(将尾部清零会使 Profile LED 熄灭)。
- 动态**效果预设**是从应用中捕获的特殊 `type` 字节
(`rgb_profiles_test.pcapng`):`0x05` Flow, `0x08` Rainbow, `0x02` Pulse,
`0x06` Alarm, `0x01`+调色板 Standoff。原样存储在 `gamesir_led.py`
(`PATTERNS`)中;`set_pattern` 会将其写入活动槽位,
并使用滑块覆盖亮度字节。
- **自定义关键帧动画**重用了 `0x05` 调色板引擎:记录
头部为 `[count, 0x05, speed, brightness]` — **字节 0 为关键帧
数量** (1–8),因此编辑器会在回读时恢复它。每个关键帧对应一帧
5-元组;`decode_record` 是 `set_keyframes` 的逆操作。
`gamesir_kf_cache.py` 会保存一份关于确切颜色/数量的本地副本,使得我们写入的槽位
能够完美往返,即使设备实际上只能存储 8 个平铺帧。
- **播放 / 暂停**当前运行动画的 vendor 命令为 `0F 0D
`(从应用中捕获):字节 2 = `1` 播放 / `0` 暂停,字节 3 =
**用于冻结的基于 1 的关键帧索引** — 因此暂停会停留在您当前查看的帧,
而不是跳转到第 1 帧。
- **固件版本**直接来自于 USB 设备描述符的
`bcdDevice`(hidapi 的 `release_number`),采用 BCD 编码的 `JJ.MN` 格式 — 无需 USB 命令或
网络调用。官方应用的 Info 按钮读取的也是相同的字段。
- **模式切换** (Xbox ↔ Switch ↔ PlayStation) 属于硬件按键组合,
会触发完整的 USB **重新枚举**,而不是可通过发送命令实现的操作。只有 Xbox
模式暴露了 vendor 通道;在其他模式下 `0x12` 流会全部变为零,
应用会将其检测为“未处于 Xbox 模式”。
注意事项:手柄**会丢弃紧随其他命令之后发送的命令** —
请拉开周期性查询的时间间隔(GUI 会交替执行它们)。
完整的硬件笔记保存在助手记忆库文件
`gamesir-vendor-interface-findings.md` 中。
## 文件布局
**应用(运行时)** — GUI 被拆分为专注的模块:
- `gamesir_gui.py` — 视图层:面板构建、逐帧更新、回调
- `gs_state.py` — 共享的实时 `state` 字典(零依赖)
- `gamesir_reader.py` — 填充 `state` 的后台连接/读取循环
- `gamesir_control.py` — 命令通道:`send_cmd`、profile、rumble、寄存器读/写
- `gamesir_led.py` — 灯光域(bank `0x20`):`set_lights`、槽位选择、关键帧、恢复出厂设置
- `gamesir_config.py` — 每个配置文件的配置寄存器映射(死区 / 曲线 / 震动 / 回报率 …)
- `gamesir_backup.py` — 全设置导出/恢复:读取所有 profile + 灯光信息,(反)序列化 JSON
- `gamesir_kf_cache.py` — 每个槽位确切关键帧颜色/数量的本地缓存(在配置文件切换期间作为权威来源)
- `gamesir_mousegrab.py` — 抑制模拟的鼠标/键盘 (EVIOCGRAB),用于鼠标模式修复
- `gamesir_window.py` — 视口放置(xrandr 主显示器几何形状;X11/XWayland)
- `gs_common.py` — vendor 接口发现 + 助手功能(包括固件/`bcdDevice` 读取)
- `gamesir_enhanced.py` — `0x12` 增强型报告解析器
- `gamesir_led_factory.py` — 捕获的“恢复预设”灯光基线
**工具:**
- `gamesir_regdump.py` — 转储/对比寄存器范围 (`sudo python3 gamesir_regdump.py `; bank 0x20 = `32`)
- `gamesir_regread.py` — 读取单个寄存器
- `gamesir_regwrite_test.py` — 安全的写入寄存器验证器(对单个字节执行读-改-读回-恢复)
- `gamesir_profile_axis.py` — 关于配置文件如何映射到 bank 的只读探测
- `gamesir_parse_capture.py` — 将 USBPcap `.pcapng` 解码为 vendor 命令(无依赖)。`--writes` 仅过滤 WRITE-REG 并打印每个地址的摘要 — 非常适合嘈杂的设置更改捕获
- `gamesir_input_diag.py` — 鼠标模式隔离器:逐一抓取手柄的 evdev 节点,以便您查看合成器正在读取哪一个(摇杆节点)来控制光标 (`sudo python3 gamesir_input_diag.py`)
**`USBPcap Controller Tests/`** — 配置映射所基于的官方应用捕获
逆向工程自(连接-同步、持久化、重映射、死区、曲线、
回报率、震动)。可使用 `gamesir_parse_capture.py` 解析其中任何一个。
**`archive/`** — 一次性探测、按键/LED 发现脚本、原始的
PS4 模式读取器、重构前的单体 GUI (`gamesir_gui_monolithic.py`)、
过时的交接文档以及 LED USB 捕获 (`gamesir_led.pcapng`)。保留以供
参考;这些脚本要求仓库根目录在导入路径上
(`from gs_common import …`)。
## 状态与后续步骤
已实现:实时输入、电池、固件读数、Xbox 模式警告、配置文件
读取/切换、震动、完整的独立 RGB、**效果预设**、灯光电源
设置(音频响应 / 拿起唤醒 / 休眠)、**自定义关键帧
动画编辑器**(基于槽位,添加/删除 1–8 帧、随机化、播放/暂停)、
**配置编辑器**(死区、抗死区、摇杆轨迹 + 灵敏度
曲线,包括**可拖动的自定义曲线编辑器**、机死区 +
微动扳机 + 响应曲线、震动 L/R、回报率)以及**按键重映射** —
所有操作均会读取活动配置文件的当前值并实时写入编辑内容。
**备份 / 恢复:** 导出会将所有 4 个配置文件及灯光设置快照到一个带标签的
JSON 文件中;恢复会将其写回。两者均已**在硬件上进行了端到端验证。**
恢复采用**写入-验证-重试**机制:每个区块在写入后都会被读回,
如果未生效则会重新发送(手柄会静默丢弃连续的命令,
因此盲目写入会丢失区块——例如第一个灯光记录),最多会重试几次,
并报告明确的通过/失败状态。选择恢复文件时会显示一个内联的
**“将加载的备份写入手柄”** 按钮(没有模态弹窗——这
在窗口管理器中被证明是不可靠的)。注意:bank `0x02`-`0x04`(已存储的、
非活动的配置文件)在此手柄上显示为只读,因此仅能保证对
**活动配置文件 + 灯光** 进行恢复;如果无法确认存储的配置文件,
状态行会予以提示。
**鼠标模式修复 (KDE/KWin Game Controller 插件):** 在 2.4GHz 接收器
重新配对/重新插拔后,移动摇杆会开始控制桌面光标(并且
按键会触发点击)。这**不是**手柄模拟鼠标 — 而是 **KWin 的
“Game Controller” 插件** (Plasma 6.7,一项 2025 年 GSoC 功能) 将摇杆映射为 →
指针,扳机映射为 → 点击,**直接**读取摇杆 evdev 节点。
(通过逐一抓取手柄的 evdev 节点进行了诊断 — 只有
*摇杆*节点会停止光标移动;模拟的鼠标/键盘节点读取值为零。
`fuser` 确认 `kwin_wayland` 占用了摇杆节点,并且测试了
`LIBINPUT_IGNORE_DEVICE` 规则,发现它**没有**帮助 — KWin 读取该节点是
带外传输的,而不是通过 libinput。)
**推荐修复 — 禁用插件**(永久有效,并且不影响游戏,
因为游戏会直接读取 evdev;当其他应用
使用手柄时,该插件甚至会自动禁用):
```
kwriteconfig6 --file kwinrc --group Plugins --key gamecontrollerEnabled false
qdbus6 org.kde.KWin /KWin reconfigure # or just log out/in
```
`gamecontrollerEnabled true/false` 实际上就是鼠标模式的开启/关闭开关
(还有一个 **系统设置 → Game Controller** 的开关)。
**应用内备选方案(非 KDE / 按需):** **停止鼠标模式** 开关会对摇杆节点采取独占式的
`EVIOCGRAB`,使得合成器无法读取它,并且会在重新插拔后
重新应用抓取。它是桌面环境无关的,但在开启时,
evdev 游戏(Steam/SDL)也无法看到手柄(旧版的 `/dev/input/jsN`
节点和 hidraw 应用仍然可以工作) — 因此在 KDE 上建议优先使用插件开关。(运行
`gamesir_input_diag.py` 以自行复现这种逐一节点的隔离测试。)
**配置架构(由 USB 捕获确定):** 读/写命令的字节 2 是一个
**bank** 选择器。编辑目标指向 **bank `0x01`,即*活动*配置文件的实时工作副本** —
官方应用会将每个配置/重映射更改写入那里,
而不管配置文件编号是多少。bank `0x02`–`0x05` 是仅供应用读取的存储/辅助区
(`0x02`–`0x04` 是默认配置文件存储;`0x05` 是另一个不同的辅助 bank),而
`0x20` 是灯光。寄存器偏移量(震动 `0x20`/`0x21`,回报率 `0x2e`,
扳机区块 `~0x1f1`,摇杆区块 `~0x227`,下方的重映射记录)位于
`gamesir_config.py` 和助手记忆库文件中。**写入会自动持久化到闪存 —
无需提交命令。**
**重映射:** 每个输入都有一个 7 字节的记录;只有 `[enabled, target_code]`
是重要的(清除 = `[00 00]`)。源地址和目标代码映射在
`gamesir_config.py` (`REMAP_SLOTS` / `REMAP_TARGETS`) 中。
**右摇杆区块** 已被**捕获** (`15_rs_testing.pcapng`):它是
镜像于 `+0x20` 处的左摇杆区块(轨迹 `0x0247`,死区最小值/最大值
`0x0249`/`0x024a`,抗死区最小值/最大值 `0x024b`/`0x024c`,曲线 `0x024e`)。该
捕获还揭示了**左摇杆具有一个我们一直遗漏的死区*最大值***,位于 `0x022a` 处
— 编辑器现在会显示两个摇杆的死区最小值+最大值。
**RT 扳机区块**暴露为镜像于 `+0x1c` 处的 LT(RT 重映射
地址证实了该步长) — 属于**推断得出,尚待验证**,需等待对
RT 设置更改的捕获。
待办项目:验证推断的 RT 区块;View/Menu/L4/R4 的*目标*代码(尚未
作为目标被捕获);配置文件切换如何将 bank `0x01` 同步到一个存储中(一个
`SET-PROFILE` + 重读测试);以及 PS4/Switch 模式的输入解析。
## 许可与免责声明
基于 [MIT License](LICENSE) 发布 — 可自由使用、修改和重新分发。
这是一个独立的业余逆向工程项目。它**与 GameSir 没有任何关联、认可或
支持**,“GameSir” 和
“Cyclone 2” 是其各自所有者的商标。该协议是为了
互操作性而进行逆向工程的;本仓库仅包含原创
代码(无 vendor 固件、USB 捕获或第三方资产)。按“原样”提供,
**不提供任何保证** — 您使用它并测试手柄寄存器的
风险由您自行承担。