从 iGame Center 到 Linux:Colorful CVN Z890 ARK FROZEN RGB 控制器逆向与 Qt 控制台实现
摘要
这次折腾的是 Colorful CVN Z890 ARK FROZEN 的主板 RGB 和 5V ARGB。Windows 下 iGame Center 能识别并控制,Linux 里却只剩一块亮着、看得见但改不了颜色的主板。目标不是套一层通用 RGB 软件,而是把官方模块实际走的 HID 输出链路找出来,再做一个普通用户可运行的 Qt 控制台。
最后确认控制器的 USB VID/PID 是 2f4c:1000,真正负责 RGB 输出的是 USB interface 02,而不是一开始误以为的 interface 01。输出帧固定为 10 个 RGB 数据包加一个提交包,共覆盖 200 个槽位。基于这条链路实现了静态、呼吸、彩虹、波浪等主机侧效果,也把板载 RGB、ARGB 1、ARGB 2 的物理映射交给用户自己绑定。
一开始的问题:同一个 USB 设备不等于同一个控制接口
用 hidapi 枚举 2f4c:1000 后,设备会出现多个 HID interface。最早按照接口 01 的厂商用途页 FF00:0001 去写,hid_write() 虽然返回成功,主板的颜色却完全没有变化。这种“写入成功但硬件没反应”不能当成协议正确,只能说明内核接受了这次 write。
重新检查 sysfs 里的 report descriptor 后,接口 01 是 FF00 的厂商端点;接口 02 才有 64 字节输出报告,对应观察到的 FF01:0001。官方 iGameMBoard.dll 中的写入函数也会构造 65 字节的 HID 报告,因此最终选择条件不能只看 VID/PID,而必须同时约束 interface number。
hid_device_info* list = hid_enumerate(0x2f4c, 0x1000);
for (hid_device_info* item = list; item; item = item->next) {
if (item->interface_number == 2) {
controller = item;
break;
}
}尤其是 hidraw 编号会在 USB 重新枚举后变化。开发时输出节点从 /dev/hidraw2 变成过 /dev/hidraw8,因此程序只保存设备路径用于界面展示,重新扫描时仍然以 VID、PID 和 interface 02 选择输出端点。
真正生效的帧格式
恢复官方模块的写入过程后,关键头部是 00 01 00 88,第 5 个字节是包序号。前 10 包每包写 20 组 RGB 三元组,最后发送一个 00 01 00 88 FF 提交包。也就是说,一帧是 11 个 65 字节报告,实际可寻址槽位为 200。
00 01 00 88 00 <20 RGB triples>
00 01 00 88 01 <20 RGB triples>
...
00 01 00 88 09 <20 RGB triples>
00 01 00 88 FFconstexpr int kLedCount = 200;
constexpr int kLedsPerPacket = 20;
constexpr int kPacketCount = 10;
report[0] = 0x00;
report[1] = 0x01;
report[2] = 0x00;
report[3] = 0x88;
report[4] = packet;
// report[5..64] = 20 组 R/G/B这也解释了早期只有整板单色测试时为什么很难判断效果是否正确:传输层已经支持 200 个独立颜色,真正未知的是这 200 个槽位分别落在板载灯、ARGB 1 和 ARGB 2 的哪个物理位置。
不是复刻硬件效果,而是在 Linux 端渲染帧
确认帧传输后,没有继续猜 iGame Center 的私有“彩虹模式”命令,而是直接在 Linux 端生成每一帧颜色。静态效果一次性发送;呼吸、循环、彩虹、波浪、闪烁和星点由 QTimer 驱动,按速度档位以约 40 到 139ms 的间隔写入 HID。
const double position = mappedPositionForSlot(led);
const double bands = spectrumDensitySlider->value();
frame[led] = hsv(position * bands + phase * 0.06 * direction());彩虹密度单独暴露为 1 到 8 轮色环。这个参数很重要:只做一个颜色沿 200 个槽位缓慢移动,视觉上会像单色渐变;增加色环数后,同一时刻能看到多段红绿蓝等颜色,才接近原来“同时存在很多颜色”的观感。波浪效果则使用主色与辅色混合,而不是把所有槽位一起做同一亮度变化。
ARGB 位置不能猜,必须让用户绑定
主板并不会在这条协议里直接告诉软件“哪些槽位属于 ARGB 1”。不同灯带的灯珠数量、插入方向和串联方式也不同。把 200 槽位硬拆成三个固定区段只适合演示,不适合作为真实配置。
因此界面为板载 RGB、ARGB 1、ARGB 2 分别保存起始槽位、灯珠数量、反向顺序。点击“定位区段”时,程序只点亮这一段并使用不同颜色,确认实际亮起的设备后再保存。彩虹和波浪会按照每一个逻辑区域的局部位置计算颜色,反向灯带也能获得正确的流动方向。
ZoneConfig { start, count, reverse }
slot 属于某个 zone 时:
position = (slot - start) / count;
if (reverse) position = 1.0 - position;权限:不需要让 GUI 常驻 root
RGB 输出节点默认属于 root。最初尝试在 GUI 内用 pkexec 调 helper,但桌面上没有可用的认证代理时不会出现预期的弹窗,而且动态效果每一帧都提权显然也不合理。最终方案是让正常桌面用户持久获得目标 hidraw 节点的读写权限,GUI 本身始终以普通用户运行。
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", \
ATTRS{idVendor}=="2f4c", ATTRS{idProduct}=="1000", \
MODE="0660", GROUP="wheel"安装规则后重新加载 udev,再用旁边的 helper 做一次 --check。静态颜色、全红定位和一键关闭仍保留了单次 pkexec fallback;动态效果则要求直接打开 interface 02,避免把 root 权限变成后台常驻服务。
顺手补齐的可用性:关闭、脚本和 KDE 启动器
“停止动画”只会停在最后一帧,并不等于熄灯,所以增加了“关闭灯光”操作:停止定时器后写入 200 个黑色槽位。自定义灯光没有额外嵌入 Lua 运行时,而是支持手动执行本地 .sh 脚本。脚本输出一个 #RRGGBB 时填满全局;输出恰好 200 个颜色时按槽位写入。脚本以当前用户身份运行、最长两秒、不自动启动、不常驻。
cmake --install build --prefix "$HOME/.local" --component Runtime
cmake --install build --prefix "$HOME/.local" --component Desktop桌面条目在安装时写入实际二进制绝对路径,避免 KDE 会话的 PATH 没有 ~/.local/bin 导致启动器找不到程序。项目源码与完整中文说明已经放在 GitHub。
最后的边界
这个项目只控制已经验证的主板 RGB 端点,不是完整 iGame Center。风扇、监控、超频和固件更新不在范围内;板载与 ARGB 的对应关系也需要先实际定位。反过来看,明确这些边界反而比“看起来支持很多主板”的通用工具更可靠:每一次写入都有已验证的接口、已验证的报告结构和可复现的权限路径。