记录一下这次把向日葵控控 A2 改成 OpenKVM-A2 的过程。

设备就是那个 OrayKVM A2,本来硬件能力是够的:HDMI 输入采集画面,USB HID 模拟键鼠,网口/Wi-Fi 访问管理端。但是原厂方案绕不开向日葵客户端和云绑定。我想要的是很简单的东西:局域网里直接打开网页就能看屏幕、打键盘、动鼠标,不依赖向日葵客户端,不走云。

这次比较特殊,整个过程基本都是 Codex 做的。我给了目标、设备 SSH、现场拔插线配合和一些判断,剩下从备份、分析、写代码、刷机、测试、开源、发 GitHub Release,到最后写这篇文章,都是 Codex 操作完成的。

仓库在这里:

https://github.com/glzjin/openkvm-a2

固件 Release 在这里:

https://github.com/glzjin/openkvm-a2/releases/tag/v0.1.0

先说结论

最后做出来的是一个只支持 OrayKVM A2 的本地 KVM 固件。功能包括:

  • 网页看 HDMI 输入画面;
  • 网页控制键盘、绝对鼠标、相对鼠标、滚轮;
  • 支持 Ctrl+Alt+Delete、Alt+Tab、Win+L 等快捷键;
  • HTTP Basic Auth 登录;
  • Web 里可以改管理密码;
  • SSH 下可以 openkvm –set-password 改密码;
  • 可以 openkvm-mode official/openkvm/toggle 在官方和 OpenKVM 之间切换;
  • 只刷 mtd2,不动 bootloader、kernel、配置分区和序列号/校准分区;
  • GitHub 上放了源码和实验固件;
  • 写了一个一键刷机脚本。

一键刷机大概就是这样:

sh scripts/flash-a2.sh --yes admin@<device-ip> 44022 ./openkvm-a2-rootfs.squashfs

脚本会先上传镜像到 /tmp,在设备端校验大小和 SHA-256,然后只执行:

flashcp -v /tmp/openkvm-a2-rootfs.squashfs /dev/mtd2
sync
reboot

重点:只写 /dev/mtd2。不要写 mtd0、mtd1、mtd3、mtd4。

为什么没直接移植 PiKVM

一开始确实想过,既然目标是 KVM,那能不能直接把 PiKVM 搬过来。

后来 Codex 看了一圈,结论是不值得。

PiKVM 的 kvmd 依赖比较现代的环境,Python、systemd、nginx、Janus 这些都不是 A2 这种设备喜欢的东西。A2 只有 128MB 内存,Linux 3.4,而且视频链路不是标准 V4L2 摄像头,而是 HiSilicon 自己那套 MPP。

强行移植完整 PiKVM,大概率会变成:

  • 系统依赖塞不下;
  • 运行内存不够;
  • 视频链路对不上;
  • 调起来很痛苦;
  • 以后维护也麻烦。

所以最后方案改成:不移植完整 PiKVM,只参考 PiKVM 的体验。底层用 A2 原生能力重写。

工程底座

底座选了 OpenIPC Divinus。

原因很简单,它本来就是一个轻量 C 写的视频服务,已经有嵌入式 SoC 的 HAL 和 MPP 相关结构。相比从零开始写一个视频服务器,改 Divinus 更现实。

最后工程变成这样:

  • 保留 Divinus Git 历史;
  • 保留 Divinus MIT 声明;
  • 整个 OpenKVM-A2 因为参考 PiKVM GPLv3 体验和前端思路,按 GPL-3.0-only 发布;
  • HiSilicon MPP 闭源库不进 Git;
  • 原厂 rootfs/kernel/mtd 备份不进 Git;
  • 设备密码、SSH 密钥、抓包文件不进 Git。

仓库里只放源码、脚本、文档、测试和最小打包逻辑。

备份

这个是第一步,也是最重要的一步。

Codex 先通过 SSH 把设备上的 mtd0 到 mtd4 全部备份下来,并做 SHA-256。后面所有操作都以这个备份作为恢复基线。

分区边界大概按这个原则处理:

  • mtd0:不动;
  • mtd1:kernel,不动;
  • mtd2:rootfs,最终只写这个;
  • mtd3:配置分区,只通过正常文件操作写 /config/openkvm/;
  • mtd4:序列号/校准之类,不动。

这也是后面所有脚本的原则:能不写 Flash 就不写,必须写也只写 mtd2。

视频

视频这块踩坑比较多。

A2 的 HDMI 输入不是走普通摄像头接口。链路大概是:

ADV7611 -> BT.1120 -> VI -> VPSS -> VENC

也就是 ADV7611 收 HDMI,BT.1120 进 HiSilicon VI,VPSS 处理,VENC 编 H.264。

Divinus 原来更像 IPC 摄像头那套,会初始化 sensor、ISP、AE、AWB。A2 这里不能这么搞,因为输入源不是 sensor,是 HDMI receiver。所以 Codex 加了一个 A2 专用模式,跳过 sensor/ISP,直接起 BT.1120 VI。

一开始探针能跑,但整机重启后视频没帧。ADV7611 显示有信号,VI 的 IntCnt 是 0。

最后发现是 /dev/hi_mipi 的 combo input mode 没设。原厂启动流程里会设 BT.1120 input mode,OpenKVM 自己独立启动时漏了。补上 input mode 6 后,VI 开始进帧。

这个坑比较典型:不是 ADV7611 没信号,也不是 VENC 不工作,是 VI 前面的输入模式没被硬件切到 BT.1120。

现在提供三个视频接口:

  • /video.mp4:主路径,分片 MP4;
  • /video.264:裸 H.264;
  • /mjpeg:降级路径。

网页默认走 MP4。浏览器不支持或者播放失败,就降级到 MJPEG。

这里也踩了一个前端坑。MediaSource 要创建 blob: URL,但 CSP 里一开始没放 media-src blob:,Chrome 直接把自己的视频 URL 拦了。修完之后,Chrome 长时间播放 MP4 正常。

HID

HID 是另一条线。

A2 不是 Linux gadget HID,而是一个 STM32 MCU 负责 USB HID。主控这边通过 /dev/ttyAMA1 给 STM32 发串口包,然后 STM32 再把键鼠事件发给目标主机。

这个协议没有直接复制原厂代码。Codex 用几种方式还原:

  • 看社区资料和按键表;
  • 看真机串口行为;
  • 观察 STM32 ACK;
  • 写黄金向量测试;
  • 再用真实 Windows 画面验证键鼠效果。

实现后支持:

  • 普通键盘按键;
  • 左右 Ctrl/Alt/Shift/Meta;
  • F1-F12;
  • 方向键和编辑键;
  • 绝对鼠标坐标;
  • 相对鼠标移动;
  • 左键、中键、右键;
  • 滚轮;
  • 断线释放;
  • 快捷键面板。

之前遇到过“刚开始能用,过一会儿不动”的情况。Codex 加了 ACK 统计和回包排空,连续发了很多正反抵消鼠标报告,确认不是 UART 回包堆积。后面有一次 HID MCU status=1,最终判断更像是目标主机 USB HID 枚举侧没准备好,重插 A2 到目标主机的 USB 后恢复。

所以文档里也写了:UART 在线不等于目标主机已经接受 HID。status=1 先检查 USB 线、目标主机枚举和官方模式行为。

Web 控制台

Web UI 没做成完整 PiKVM。

保留的功能:

  • 视频画面;
  • 键盘捕获;
  • 绝对/相对鼠标模式;
  • 滚轮;
  • 全屏;
  • 连接状态;
  • HID 状态;
  • 快捷键;
  • 管理密码修改。

删掉或者没做的功能:

  • ATX;
  • 虚拟介质;
  • GPIO;
  • OCR;
  • 录像;
  • 宏;
  • 终端;
  • 完整 kvmd API;
  • 云中继。

理由也简单:首版目标是可用,不是把 PiKVM 全家桶搬进 128MB 的老设备。

认证和配置

认证是 HTTP Basic Auth。

密码文件不存明文,只存:

sha256:salt:hash

盐随机生成,hash 是 SHA-256(salt || password)。

密码可以两种方式改:

openkvm --set-password

或者在 Web 管理弹窗里改。

持久配置放在:

/config/openkvm/

里面主要是 openkvm.yaml、openkvm.passwd 和 mode。重启、刷 rootfs、切换官方/OpenKVM 都不会覆盖已有密码。

去云化

OpenKVM 模式下关闭这些东西:

  • sunlogin_client;
  • kvm_lan_ctrl;
  • 原厂 uhttpd 30080 页面;
  • oray_daemon.sh 守护;
  • 原厂 OTA/文件传输相关脚本;
  • LED 网络探测脚本。

这里也有一个小坑:一开始以为停掉 sunlogin 就没有外连了,后来查网络发现 LED 脚本还在周期性访问公网网站做连通性探测。Codex 把它也纳入了模式守卫:OpenKVM 模式停掉,official 模式恢复。

双模式

这个功能我觉得很有必要。

如果直接把官方东西全删了,调试时很难对照,也不方便回滚。所以最终 rootfs 里保留官方栈,但不默认启动。通过 openkvm-mode 切换:

openkvm-mode status
openkvm-mode official
openkvm-mode openkvm
openkvm-mode toggle

官方模式下,原厂 30080 页面和客户端起来;OpenKVM 模式下,只保留 80 Web 和 44022 SSH。

从 44022 chroot 会话里切换时,用:

/bin/busybox chroot /proc/1/root /usr/sbin/openkvm-mode toggle

这条后面也写进文档了。

SSH

SSH 也顺手加固了一下。

原厂有 44022 和 44033。OpenKVM 最终保留 44022,关掉 44033。Dropbear 参数也改了,禁用不需要的转发,设置 idle timeout,去掉 DSA host key。

另外重新设置了更随机的 SSH 密码,并且只保存在本地 dist 里的 ignored 文件,不进 Git。

测试

测试分几类。

主机端测试:

make -C tests clean test sanitize

覆盖 HID 帧、鼠标布局、按键映射、Basic Auth、WebSocket 控制权、非法帧、断线释放、配置持久化、模式切换等。

真机测试:

  • MPP ABI 探针;
  • SYS/VB 初始化;
  • BT.1120 VI;
  • VPSS;
  • H.264 VENC;
  • /video.mp4;
  • /video.264;
  • /mjpeg;
  • Chrome/Edge 真实播放;
  • 键盘输入;
  • 绝对鼠标;
  • 相对鼠标;
  • 左右键;
  • Ctrl+Alt+Delete;
  • 断线释放;
  • 单控制端;
  • 多观看端;
  • 官方/OpenKVM 往返;
  • 端口检查;
  • 10 分钟 soak。

原计划是跑 2 小时,我嫌太久,改成了 10 分钟。600 秒测试通过,RSS 很低,没看到 panic/Oops/segfault。

HDMI out

HDMI out 也折腾了一下。

现象是物理显示器接 A2 的 HDMI out,会隔一会儿黑一下。开始怀疑 OpenKVM 改坏了什么,后来切回官方固件也闪。

这就说明不是 OpenKVM 独有问题。

最后做了一个保守处理:把 EDID/输出目标降到 1080p30。这个不是完美解决所有显示器兼容性,只是当前设备上更稳的策略。

rootfs 制作

rootfs 构建脚本做的事大概是:

  • 从用户自己的原厂 mtd2 解包;
  • 保留官方客户端和旧 Web,作为 official 模式;
  • 加入 OpenKVM 程序、配置和私有运行库;
  • 加入 S84openkvm 和模式守卫脚本;
  • 移除 OTA/文件传输相关组件;
  • 删掉没用的多 ABI C++ 运行库副本,省空间;
  • 重建 SquashFS;
  • 检查大小,保留至少 512KiB 余量;
  • 生成 rootfs 镜像、恢复 U 盘镜像、SHA-256 清单和初始密码包。

这里注意,GitHub 仓库不提交 dist、不提交 .vendor、不提交 toolchain、不提交设备密码、不提交原厂备份。

一键刷机脚本

后来又加了 scripts/flash-a2.sh。

这个脚本故意写得很简单,不做自动发现,不做复杂菜单,不自动选择设备。必须显式 –yes,必须传 admin@device-ip。

sh scripts/flash-a2.sh --yes admin@<device-ip> 44022 ./openkvm-a2-rootfs.squashfs

流程就是:

  • 检查本地镜像存在;
  • 算本地大小和 SHA-256;
  • SSH 检查设备有 /dev/mtd2 和 flashcp;
  • SCP 上传到 /tmp;
  • 设备端重新算大小和 SHA-256;
  • 一致才 flashcp -v 写 /dev/mtd2;
  • sync;
  • reboot。

没有自动写其它分区,也没有自动回滚。嵌入式设备上自动写 Flash 这件事,越少越好。

开源发布

Codex 最后把仓库整理了一遍。

  • README 改成中英双语;
  • 写了 doc/FLASHING.md;
  • 写了架构、HID、rootfs、设备闸门、安全策略文档;
  • 补了 LICENSE、NOTICE、Divinus MIT 声明;
  • GitHub Actions 改成只跑 host tests 和 public guard;
  • public guard 检查真实 IP、私钥、设备密码、固件镜像是否误提交;
  • 创建 GitHub 仓库;
  • 推送源码;
  • 创建 v0.1.0 Release;
  • 上传 rootfs、recovery USB 镜像、SHA-256 文件和初始密码包。

GitHub 仓库:

https://github.com/glzjin/openkvm-a2

Release:

https://github.com/glzjin/openkvm-a2/releases/tag/v0.1.0

怎么刷

如果直接用 Release 里的二进制,流程是:

curl -LO https://github.com/glzjin/openkvm-a2/releases/download/v0.1.0/openkvm-a2-rootfs.squashfs
curl -LO https://github.com/glzjin/openkvm-a2/releases/download/v0.1.0/openkvm-a2-rootfs.squashfs.sha256
sha256sum -c openkvm-a2-rootfs.squashfs.sha256
sh scripts/flash-a2.sh --yes admin@<device-ip> 44022 ./openkvm-a2-rootfs.squashfs

如果设备里之前没有 /config/openkvm/,初始 Web 密码在 Release 的 rootfs-credentials.tar.gz 里。第一次登录后应该马上改密码。

当前限制

  • 只支持 OrayKVM A2;
  • 不支持 A2-300/Pro;
  • 没有音频;
  • 没有虚拟介质;
  • 没有 ATX 电源控制;
  • 没有完整 kvmd API;
  • 没有 VNC/IPMI;
  • 没有云中继;
  • 公网不要直接映射设备端口,建议 VPN 或 HTTPS 反代。

总结

这次最有意思的不是某一段代码,而是整个链路都跑通了。

从一台只能按原厂逻辑绑定云客户端的 A2,到一个能本地打开网页控制、能切回官方、能刷回原厂、能开源构建、能 GitHub Release 下载固件的 OpenKVM-A2,中间所有实际操作都是 Codex 完成的。

Codex 做的事情包括:方案判断、固件备份、MPP ABI 验证、视频链路、HID 协议、Web 控制台、认证、SSH 加固、模式切换、rootfs 重制、真机刷写、测试、GitHub 发布、Release 上传和博客发布。

我这边主要负责给目标、给设备、现场拔插线,以及在关键地方确认现象。

这个项目算是一个比较典型的例子:AI 不只是补全代码,也可以在有权限、有工具、有反馈的情况下,把一个真实硬件项目从想法一路推到可发布。