从 Windows HID 到 Linux 常驻服务:FK360 EL ARGB 数显驱动复盘
最近给电脑换了一套 FK360 EL ARGB 水冷。
硬件安装没有问题,但冷头数显只支持 Windows 官方软件 SystemTemperatureMonitoring CHS V2.6。我的主力系统是 Arch Linux,数显在 Linux 下不会自己更新,因此这次目标不是“让 RGB 亮起来”,而是完整复现官方程序通过 USB HID 发送的监控数据。
最终实现是一个纯 Python 标准库的 Linux 用户态驱动,确认可用于 USB ID 0145:1001。它负责自动定位 hidraw、组装 65 字节报告、读取 hwmon、/proc、NVIDIA SMI 和 Intel RAPL,并通过 systemd 以 root 常驻运行。
0. 最终项目与当前默认参数
项目地址:
https://github.com/LuorixDev/fk360el-linux
当前默认配置是:
[fk360el]
device =
interval = 1
repeats = 1
repeat_delay = 0.02
display_mode = 0
power_smoothing = 0.6
verbose = falsedevice:留空时按 VID/PID 自动定位,也可以写死为/dev/hidraw7。interval:完整采样与显示更新周期,单位秒,默认 1 秒。repeats:同一份 65 字节报告连续发送次数,默认 1。repeat_delay:重复报告之间的间隔,单位秒。只有repeats > 1时有效。display_mode:原始模式字节,范围 0–255,目前默认 0。power_smoothing:功耗 EMA 中“新值”的权重,1 表示不平滑,默认 0.6。
官方 Windows DLL 的发送节奏是 interval=0.5、repeats=15、repeat_delay=0.02。Linux 实机测试不需要每帧重复 15 次,因此最后把默认值改为 1 秒采样、每帧只写一次,降低无意义的 USB 写入。
1. 问题一:到底哪个接口负责数显
现象
水冷头有 ARGB 接口也有 USB 接口。ARGB 能由主板控制,不代表数显数据也走 ARGB。
排查
lsusb -d 0145:1001
ls -l /dev/hidraw*
# 查看 hidraw 对应的 USB/HID 身份
for dev in /sys/class/hidraw/hidraw*; do
echo "=== $dev ==="
cat "$dev/device/uevent" 2>/dev/null
done实机确认数显设备为:
VID 0145
PID 1001
HID 输出节点 /dev/hidrawX根因与修复
ARGB 的 3 针 5V 接口只负责灯珠,USB HID 才负责冷头屏幕。驱动扫描 /sys/class/hidraw/hidraw*,逐级读取父目录的 uevent,同时匹配两种内核格式:
HID_ID=...:00000145:00001001
PRODUCT=145/1001/...匹配成功后,取同名节点 /dev/hidrawX,用 O_RDWR | O_CLOEXEC 打开;若设备拒绝读写模式,再回退为 O_WRONLY | O_CLOEXEC。
因此不能把 hidraw7 写死。编号在重启或插拔后可能变化,VID/PID 才是稳定身份。
2. 问题二:Windows 程序到底写了什么
现象
官方安装包能正确刷新温度,但 Linux 下没有协议文档,也没有可直接调用的库。
排查方式
拆开 SystemTemperatureMonitoring CHS V2.6 后,顺着 HID 写入逻辑和数据组装顺序,确认动态数据使用固定 65 字节输出报告。Windows 程序并不是上传图片,而是周期性发送一组紧凑的数值字段。
65 字节报告结构
下面是当前实机验证过的偏移。偏移从写入 /dev/hidraw 的第一个字节开始计数:
16 位字段统一按大端写入:
report[offset] = number >> 8
report[offset + 1] = number & 0xFF例如 CPU 功耗 65.4W 先乘 10 得到 654,即 0x028E,29、30 偏移分别写入 0x02 和 0x8E。
写设备时强制检查 os.write() 的返回值必须是 65。少写任何字节都直接报 short HID write,避免把半份报告当成成功。
3. 问题三:Linux 没有一个接口能给齐所有数据
CPU 温度与风扇
遍历 /sys/class/hwmon/hwmon*,通过 name 识别设备。当前 CPU 名称集合包括:
coretemp
k10temp
zenpower
cpu_thermal
acpitztemp*_input 是毫摄氏度,读取后除以 1000。温度标签按 package → tctl → tdie → cpu → core 0 排优先级,避免随便取到某个核心或主板环境温度。
风扇读取 fan*_input。带有 cpu 标签的通道优先,其他通道作为低优先级回退。
CPU 占用率
/proc/stat 的第一行只有累计 tick,不能直接当百分比。驱动保存上一次 total 和 idle:
total = 所有 CPU tick 之和
idle = idle + iowait
load = 100 × (1 - Δidle / Δtotal)启动后先预采样一次并等待最多 150ms,再生成第一帧。否则第一次没有差值,CPU 占用率必然是 0。
CPU 频率与内存
CPU 频率读取所有核心:
/sys/devices/system/cpu/cpu[0-9]*/cpufreq/scaling_cur_freq原始单位为 kHz,除以 1000 后取所有可用核心的平均 MHz。
内存读取 /proc/meminfo,使用:
memory_load = 100 × (MemTotal - MemAvailable) / MemTotal这里使用 MemAvailable 而不是 MemFree,否则 Linux 文件缓存会被错误算成“已用内存”。
GPU
AMD、nouveau、nvidia hwmon 会尝试读取:
gpu_busy_percent
freq*_input
power*_input
fan*_inputLinux hwmon 的 power*_input 单位是 μW,最终需要除以 1,000,000 得到 W。代码内部先由通用读取器除 1000,再在 GPU/CPU 分支除 1000。
NVIDIA 额外调用:
nvidia-smi \
--query-gpu=temperature.gpu,utilization.gpu,clocks.current.graphics,fan.speed,power.draw \
--format=csv,noheader,nounits超时设置为 2 秒。命令不存在、驱动通信失败、字段为非数字或没有输出时全部回退到 hwmon,不会中断主循环。
当前明确的限制
缺失的传感器会按 0 写入,不会阻止其他字段更新。当前 CPU/GPU 电压字段虽然协议位置已确认,但采集器没有稳定的跨平台来源,因此通常仍为 0。NVIDIA 的 fan.speed 返回百分比,不是 RPM,当前也没有把它伪装成 RPM 写入。
4. 问题四:CPU 功耗字段为什么一直是 0
现象
最初报告偏移 29–30 已经实现,但 Sample.cpu_watts 没有任何真实赋值,所以冷头始终显示 0W。
排查
find -L /sys/class/powercap -type f \
\( -name name -o -name energy_uj -o -name max_energy_range_uj \) -print
ls -l /sys/class/powercap/intel-rapl:0/energy_uj
cat /sys/class/powercap/intel-rapl:0/nameIntel 平台暴露的不是实时 W,而是累计能量 μJ。实机同时存在:
package-0 CPU Package
core Package 子域
uncore Package 子域
psys 整个平台如果把这些全部相加,会把同一份能量重复计算。正确做法是只选顶层名称以 package- 开头、且目录后缀只有一个数字的节点,例如 intel-rapl:0,排除 intel-rapl:0:0 这类子域。
计算逻辑
delta_uj = current_energy_uj - previous_energy_uj
如果 delta_uj < 0:
delta_uj += max_energy_range_uj
watts = delta_uj / 1,000,000 / elapsed_seconds同一时刻读取所有 Package,多路 CPU 时把各 Package 功耗相加。优先级为:
/sys/class/powercap/intel-rapl:*:MSR 后端。/sys/class/powercap/intel-rapl-mmio:*:MSR 不可读时回退。AMD
k10temp/zenpower的power*_input。
为了防止休眠、驱动重置或错误计数器产生巨大假值,单 Package 超过 2000W 的结果直接丢弃。
权限问题
实机 energy_uj 权限是:
-r-------- root root ... energy_uj普通用户运行 --dry-run 时 CPU 功耗显示 0,不代表计算错误,而是根本读不到能量计数器。安装后的 systemd 单元明确设置:
User=root
Group=rootroot 服务是为了读取 RAPL,不是为了访问 hidraw。hidraw 本身已经有 udev 规则处理。
5. 问题五:功耗每秒跳动,屏幕看起来很乱
RAPL 和 GPU 功耗都是瞬时采样。1 秒更新时,负载变化会让数值在几十瓦范围内快速跳动。
最后对 CPU/GPU 功耗分别维护 EMA 状态:
smooth = α × current + (1 - α) × previous
α = power_smoothing = 0.6第一份有效值直接采用,不做从 0 缓慢爬升。之后若功耗从 20W 跳到 100W,结果为:
第 1 帧:68.0W
第 2 帧:87.2W
第 3 帧:94.9W这个权重只压掉视觉抖动,不会产生几秒钟的严重延迟。配置为 0.8 更灵敏,配置为 1 完全关闭平滑。允许范围为 0.05–1。
6. 权限与常驻服务
udev
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0145", ATTRS{idProduct}=="1001", MODE="0660", TAG+="uaccess"这条规则让当前桌面登录用户可以访问对应 hidraw。安装后执行:
sudo udevadm control --reload-rules
sudo udevadm trigger --subsystem-match=hidrawsystemd
[Unit]
Description=FK360 EL ARGB USB display monitor
After=local-fs.target
[Service]
Type=simple
User=root
Group=root
ExecStart=/usr/local/lib/fk360el/fk360el.py --config /etc/fk360el.conf
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
RestrictAddressFamilies=AF_UNIX
[Install]
WantedBy=multi-user.targetRestart=on-failure 用于设备瞬时断开或传感器异常退出;ProtectSystem=strict、ProtectHome=true 等限制 root 服务的可写范围。没有启用 PrivateDevices=true,因为那会把真正需要写入的 hidraw 一起隔离。
7. 一键安装、升级和卸载
git clone https://github.com/LuorixDev/fk360el-linux.git
cd fk360el-linux
sudo ./install.sh安装目标固定为:
安装器只在 /etc/fk360el.conf 不存在时写默认配置,所以升级不会覆盖自己的参数。每次安装都会执行 daemon-reload、enable 和 restart,确保新代码立刻生效。
安装成功后源文件夹可以删除。卸载命令已被独立复制:
sudo fk360el-uninstall卸载会停止并禁用服务,删除程序、udev 规则、systemd 单元以及 /etc/fk360el.conf。
8. TUI 与非交互配置
sudo fk360el-configTUI 使用 Python 标准库 curses,不需要 textual、rich 等 pip 依赖。上下选择,左右切换预设值,E 手动编辑,Enter 执行保存、启动、停用、测试或重启。
脚本环境直接用子命令:
fk360el-config show
sudo fk360el-config set interval 1
sudo fk360el-config set repeats 1
sudo fk360el-config set repeat-delay 0.02
sudo fk360el-config set display-mode 0
sudo fk360el-config set power-smoothing 0.6
sudo fk360el-config set device auto
sudo fk360el-config test
sudo fk360el-config restart
sudo fk360el-config enable
sudo fk360el-config disabletest 会先判断服务是否运行:运行中则临时 stop,发送一帧测试报告,最后在 finally 中重新 start,避免两个进程同时写 hidraw。
9. 最终验证顺序
# 1. USB 身份
lsusb -d 0145:1001
# 2. root 下确认 RAPL 可读
sudo /usr/local/lib/fk360el/fk360el.py --list-sensors
# 3. 单帧硬件测试
sudo fk360el-config test
# 4. 服务状态
systemctl status fk360el.service
# 5. 临时打开详细日志
sudo fk360el-config set verbose true
sudo fk360el-config restart
journalctl -u fk360el.service -f正常日志现在会同时输出功耗:
CPU 47C 6% 3643MHz 23.8W; GPU 42C 10% 2542MHz 67.5W; RAM 52%自动化测试目前覆盖 65 字节固定布局、字段钳位、配置优先级、新默认参数、RAPL 正常差分、RAPL 回绕、MMIO 回退、功耗平滑和日志功耗字段。
总结
这次真正困难的部分不是 os.write(fd, report),而是确定 65 字节每个偏移的含义、找对 Linux 数据源、处理单位和计数器回绕,再把 root-only RAPL 与普通 hidraw 权限拆开考虑。
最后形成的结构很简单:Python 标准库负责采集和 HID,udev 负责设备权限,systemd 负责 root 常驻与失败重启,INI 配置和 curses TUI 负责用户侧调整。
目前只确认 FK360 EL ARGB、VID:PID 0145:1001。其他外观相似的冷头不应直接假定协议相同,至少先检查 USB ID,再用单帧测试确认报告结构,之后再交给 systemd 常驻运行。