TinyPilot:花不到 100 美元打造一台 KVM Over IP
TinyPilot 是我开发的一款廉价的开源设备,用于远程控制计算机。它甚至在操作系统启动之前就能工作,因此我用 TinyPilot 在我的裸机家庭实验室服务器上安装新操作系统和调试启动故障。
本文详细介绍了我的 TinyPilot 开发经历,并展示如何用 Raspberry Pi 以不到 100 美元的成本打造你自己的设备。

用 TinyPilot 在 Microsoft Surface 的 Chrome 浏览器中控制我的 Ubuntu 笔记本电脑
我不想听你的长篇大论,直接告诉我怎么装
如果你是个不想听我讲述开发 TinyPilot 过程中那些悲喜交加的精彩故事的“扫兴鬼”,可以直接跳到“如何打造你自己的 TinyPilot”一节。
演示
为什么要做 TinyPilot?
几年前,我自己搭建了一台家用服务器来测试软件。这是一笔很值得的投资,我每天都在用它。

我在 2017 年搭建的、用来托管虚拟机的家庭实验室服务器
这台服务器没有连接键盘或显示器,因为我通过 ssh 或 Web 界面访问它。这种配置很方便,但也会把小问题变成巨大的麻烦。
每隔几个月,我就会把什么东西搞砸,导致服务器无法启动或无法加入网络,实际上把我锁在了机器外面。要让它重新跑起来,我得把所有线都拔掉,把服务器拖到书桌上,再手忙脚乱地接上一堆线缆,把它连到我桌面上的键盘和显示器。
商业解决方案
朋友们向我极力推荐 iDRAC 的使用体验。它是 Dell 服务器中的一颗芯片,从系统通电那一刻起就提供虚拟控制台。我曾短暂考虑过在下一台家用服务器上配一个 iDRAC,但它高昂的价格很快打消了这个念头。光是许可证就要 300 美元,而且还需要昂贵的定制硬件。

Dell 的 iDRAC 技术许可证每台机器要 300 美元,还不算硬件成本
接着,我看了一些商业 KVM Over IP 解决方案。它们提供的功能与 Dell 的 iDRAC 类似,但它们是外置设备,连接到计算机的键盘、视频和鼠标端口(KVM 这个名字由此而来)。遗憾的是,它们更贵,每台价格在 500 到 1000 美元之间。

商业 KVM Over IP 设备的价格在 500 到 1000 美元之间。
虽然我在拖动服务器这件事上已经很懒了,但每年才换几次线缆的麻烦,还不足以让我心甘情愿地掏出 500 美元。
于是,我做了任何一个“恰到好处地不理性”的程序员都会做的事:花几百个小时自己造一台 KVM Over IP。
用 Raspberry Pi 打造 KVM Over IP
Raspberry Pi 是一种小巧廉价的单板计算机。这些设备的性能足以运行完整的桌面操作系统,因此其 30 到 60 美元的价格使它们成为爱好者和程序员的热门工具。

Raspberry Pi 是一台功能齐全的计算机,只有一块芯片大小,价格仅 30 到 60 美元。
较新版本的 Pi 支持 USB OTG(USB On-The-Go),这让 Pi 可以伪装成键盘、U 盘、麦克风等 USB 设备。
作为“Pi 当作 KVM”这一想法的概念验证,我做了一个叫 Key Mime Pi 的简单 Web 应用。

Key Mime Pi,TinyPilot 的早期前身,当时只支持键盘转发。
Key Mime Pi 通过 USB 连接到另一台计算机,并注册为一个 USB 键盘。它同时提供一个网页并监听 JavaScript 按键事件。当用户打字时,Key Mime Pi 会捕获这些按键事件,并通过它的虚拟 USB 键盘把它们转换成击键信号。这样,击键就会出现在目标计算机上。我在之前的文章里详细描述过这个行为。
捕获视频的挑战
如果看不到屏幕上发生了什么,键盘转发就没多大用处。显而易见的下一步,就是想办法在 Pi 上捕获服务器的显示输出并在浏览器里呈现出来。
我第一次尝试视频捕获用的是 Lenkeng LKV373A HDMI 延长器。Daniel Kučera(aka danman)对这个设备做了出色的逆向工程。它在 eBay 上由中国卖家出售,价格约 40 美元,看起来是我的最佳选择。

Lenkeng LKV373A HDMI 延长器是我第一次尝试 HDMI 视频捕获。
捕获视频很棘手,因为 LKV373A 发送端并不是视频采集设备。它的设计用途是与一个 LKV373A 接收端配对,由接收端把网络流转换回 HDMI 输出。danman 在研究中发现了一种拦截并捕获视频流的方法,但 LKV373A 使用的是一种非标准变体的 RTP 协议,几乎没有视频工具能识别。
幸运的是,danman 向 ffmpeg 提交了一个补丁,可以处理 LKV377A 那种古怪的行为,于是我能用 ffmpeg 的播放器渲染这个流:
ffplay -i udp://239.255.42.42:5004
用 ffplay 渲染来自 LKV373A 的视频流
正是在这里,我第一次尝到了贯穿整个项目的难题的滋味:延迟。目标计算机与我桌面上的视频播放之间有将近一秒的延迟。

LKV373A 在任何重编码之前就引入了 838 毫秒的延迟。
我试着调整 ffplay 的众多命令行参数来加快流的速度,但始终没能突破 800 毫秒。而且这还是在我那配有高端 GPU 和 CPU 的桌面上测的。对我那台寒酸的小 Raspberry Pi 来说,性能前景不容乐观。
幸运的是,我完全靠巧合找到了更好的方案。
HDMI 转 USB 小棒
有一次漫无目的地刷 Twitter 时,我碰巧看到 Arsenio Dev(阿塞尼奥·德夫)发的一条推文,介绍他刚买的一个低成本 HDMI 转 USB 小棒:

Arsenio Dev 的一条推文让我发现了更好的视频采集方案。
以 1080p 分辨率、30 帧每秒采集视频,听起来好得有点不真实,所以我在 eBay 上订了一个。只要 11 美元,含运费。我都不知道这东西该叫什么——它没有品牌名,我就姑且叫它“HDMI 小棒”。市面上有好几种变体,但它们都只是同一颗 MacroSilicon MS2109 芯片套上了不同的外壳而已。

eBay 上有售的 HDMI 转 USB 小棒,11.20 美元包邮
几天后设备到手时,我被震撼到了。不用任何折腾,插到 Raspberry Pi 上它就直接被识别为 UVC 视频采集设备。
$ sudo v4l2-ctl --list-devices
bcm2835-codec-decode (platform:bcm2835-codec):
/dev/video10
/dev/video11
/dev/video12
UVC Camera (534d:2109): USB Vid (usb-0000:01:00.0-1.4): <<< HDMI capture dongle
/dev/video0
/dev/video1几分钟之内,我就能采集并转发 HDMI 视频了:
# On the Pi
ffmpeg \
-re \
-f v4l2 \
-i /dev/video0 \
-vcodec libx264 \
-f mpegts udp://10.0.0.100:1234/stream
# On my Windows desktop
ffplay.exe -i udp://@10.0.0.100:1234/stream而且它实在太方便了。LKV373A 几乎有一块砖那么大,还需要自己的电源和网线。而 HDMI 小棒只有一个 U 盘大小,只需要一个 USB 口就够了。

Lenkeng LKV373A HDMI 延长器(左)比 HDMI 小棒(右)更大,需要更多接线。
唯一的问题还是那个老毛病:延迟。Pi 转发的视频流比源计算机滞后 7 到 10 秒。

用 ffmpeg 从我的 Pi 流式传输视频时,视频延迟高达 10 秒。
我不确定这个延迟是来自小棒本身、Pi 上的 ffmpeg,还是我桌面上的 ffplay。Arsenio Dev 报告的延迟只有 20 毫秒,所以看来只要钻研一下 ffmpeg 那些晦涩神秘的命令行参数,更快的性能是有可能的。
又一次好运让我免去了这项苦差事。
借鉴类似项目
当我发布关于 Key Mime Pi 的上一篇博文后,Max Devaev(马克斯·德瓦耶夫)给我留了条评论,鼓励我去看看他的项目 Pi-KVM。

Max Devaev 把我引向他已有的 Pi-KVM 项目。

我以前玩面包板的经历包括不小心把它熔化了。
我之前粗略看过 Pi-KVM,但它对面包板和焊接的要求把我吓退了。
在 Max 的建议下,我又看了一遍 Pi-KVM,特别想知道他是怎么解决视频延迟问题的。我注意到他通过一个叫 uStreamer 的工具来采集视频。
注:经过与 Max 的进一步交流,我了解到 Pi-KVM 也支持无需焊接和面包板的构建方式。
uStreamer:超快的视频流工具
你有没有遇到过那种好到能解决你根本没预料到的问题的工具?
uStreamer 开箱即用就把我的延迟从 8 秒降到了 500 至 600 毫秒。但它还省掉了一整串额外的工作。

uStreamer 把我的延迟降低了 15 倍。
在 uStreamer 出现之前,我不知道该怎么把 ffmpeg 输出的视频送到用户的浏览器里,但我知道总有办法做到。我测试过一篇大体靠谱的教程,讲的是用 HLS 把 ffmpeg 的视频管道接到 nginx 上,但这反而增加了更多延迟。而且它仍然留下一堆问题,比如如何在 HDMI 线插拔时启动和停止流式传输,以及如何把视频转换成浏览器友好的格式。
uStreamer 解决了所有这些问题。它自带一个极简的 HTTP 服务器,以 Motion JPEG 格式提供视频,这是浏览器原生就能播放的格式。我再也不用折腾 HLS 流,也不用让 ffmpeg 和 nginx 互相通信了。
这个工具功能如此完备,我以为 Max 只是从某个更成熟的项目 fork 过来的,但我错了。这位狂人用 C 语言自己写了一个视频编码器,只为从 Pi 硬件上榨取最大性能。我立刻给 Max 捐了款,也邀请所有使用他软件的人这样做。
改善视频延迟
uStreamer 把我的延迟从 10 秒降到了约 600 毫秒。这是巨大的一步,但延迟依然明显。我告诉 Max,如果他能找到提升性能的办法,我愿意资助 uStreamer 的进一步开发,于是我们聊了起来。
Max 对我用的那个 HDMI 小棒很感兴趣,因为他从没见过这种设备。他邀请我用 tmate 创建一个共享 shell 会话,以便远程访问我的 Pi。

Max 表示要么帮我改善延迟,要么陷害我犯下联邦罪行。幸运的是,他最终选择了前者。
测试了几分钟 uStreamer 在我硬件上的运行情况后,Max 运行了 v4l2-ctl 工具,看到一行让他着迷却完全超出我理解范围的内容:
$ sudo v4l2-ctl --all
Driver Info:
Driver name : uvcvideo
Card type : UVC Camera (534d:2109): USB Vid
...
Format Video Capture:
Width/Height : 1280/720
Pixel Format : 'MJPG' (Motion-JPEG)
...
Streaming Parameters Video Capture:
Capabilities : timeperframe
Frames per second: 30.000 (30/1)HDMI 小棒输出的视频流竟然是 Motion JPEG 格式!uStreamer 的硬件辅助编码速度很快,但其实完全没有必要,因为现代浏览器原生就能播放 Motion JPEG。
我们把 uStreamer 配置成跳过重编码,直接原样透传视频流。

跳过 Pi 上多余的重编码步骤后,延迟从 600 毫秒降到了 200 毫秒。
延迟从 600 毫秒一路降到 200 毫秒。虽然算不上即时,但用几分钟之后你就感觉不到延迟的存在了。
TinyPilot 实战
还记得本文开头我说过想要 TinyPilot,是为了能在无头 VM 服务器启动之前访问它吗?嗯,它真的做到了,我也真的在用!
我在 Key Mime Pi 的基础上迭代出了一个集成视频采集功能的新网页界面:

今年我搭了一台新的无头 VM 服务器,并用 TinyPilot 安装了 Proxmox,这是一个开源的 hypervisor(虚拟机管理程序),带有管理 VM 的网页界面。
TinyPilot 让我可以全程在浏览器里完成整个安装过程。这绝对比我过去拖着机器到处跑、来回换线缆的老办法舒服多了。
如何打造你自己的 TinyPilot
零件清单
- Raspberry Pi 4(所有型号都可以)
- USB-C 转 USB-A 数据线(公对公)
- HDMI 转 USB 采集小棒
- 奇怪的是,这些东西没有品牌名,但你可以凭外观认出它们。
- 它们通常在 eBay 上卖 11 到 15 美元。
- microSD 卡(Class 10,8 GB 或更大)
- HDMI 转 HDMI 线
- 或者 [其他接口] 转 HDMI,取决于你的目标机器用什么显示输出。
- (可选)USB-C OTG 分流接头
- 需要另外两根 USB-A 转 microUSB 线和一个 3 安培电源适配器。
- (可选)散热外壳、散热片或风扇
- 选择的外壳要能露出 Pi 的 GPIO 引脚。
- 我用的是这款极简的无源散热外壳。
安装 Raspberry Pi OS Lite
首先,把 Raspberry Pi OS lite(旧称 Raspbian)安装到 microSD 卡上。

我用 Rufus 写入 Pi 的 microSD 卡,不过任何整盘镜像工具都可以。
在 microSD 卡的 boot 分区放一个名为 ssh 的文件即可启用 SSH 访问。如果你要通过无线网络连接,还需要一个 wpa_supplicant.conf 文件。
准备好 microSD 卡之后,把它插入你的 Pi 设备。
安装外壳(可选)
Raspberry Pi 4 以发热量大而闻名。不加散热也能正常跑,但随着时间推移很可能遇到稳定性问题。
我喜欢这款极简外壳,因为它便宜,而且无需带风扇的复杂结构就能被动散热:

这款极简铝制外壳不需要风扇的复杂结构就能很好地给你的 Pi 散热。
通过 USB 连接到目标机器
要让 TinyPilot 充当虚拟键盘,请把 Pi 的 USB-C 口连接到目标机器上的一个 USB-A 口:


用一根 USB-C 转 USB-A 线,把 USB-C 一端接到 Pi 的 USB-C 口,USB-A 一端接到目标计算机。
注:优先使用 USB 3.0 端口,因为它们能给 Pi 提供更多电力。
接入 HDMI 采集小棒
完成物理组装的最后一步:把 HDMI 小棒插入 Pi 的某个 USB 口。然后,将一根 HDMI 线接到小棒上,另一端插入目标计算机的显示输出。


把目标计算机的显示输出接到 HDMI 小棒上,再把小棒插入 Pi 的 USB 口。
注:如果你要连接的计算机没有 HDMI 输出,应该可以使用 DisplayPort 转 HDMI 线或 DVI 转 HDMI 线,不过我没有亲自测试过这些。
连接以太网线
如果你通过有线局域网连接 Pi,请把网线接到 Pi 的以太网口:

把一根以太网线接到你的 Pi 上。
注:如果你已经按照上文所述添加了 wpa_supplicant.conf 文件配置无线访问,可以跳过这一步。
安装 TinyPilot 软件
SSH 登录你的 Pi 设备(Raspberry Pi OS 的默认凭据是 pi / raspberry),然后运行以下命令:
curl -sS https://raw.githubusercontent.com/tiny-pilot/tinypilot/master/quick-install \
| bash -
sudo reboot如果你对把网上的随机脚本直接灌进 shell 保持应有的警惕,我鼓励你去查看源码。
该脚本会引导搭建一个自包含的 Ansible 环境,其中包含我的 TinyPilot Ansible role。它会安装四个每次开机都会运行的服务:
- nginx:流行的开源 Web 服务器
- ustreamer:轻量级 HTTP 视频流服务器
- usb-gadget:启用 Pi “USB gadget 模式”的脚本,让 Pi 可以伪装成 USB 设备
- tinypilot:我为 TinyPilot 创建的网页界面
使用 TinyPilot
运行安装脚本后,TinyPilot 将可以通过以下地址访问:

设置完成后,你可以在本地网络中通过 http://raspberrypi/ 访问 TinyPilot 的网页界面。
供电问题
这套方案最大的局限在于供电。依赖目标计算机供电意味着,当目标计算机关机时,Pi 会遭遇突然断电。
此外,Pi 4 要稳定运行需要 3 安培电流,尽管它在更低功率下也能运行。计算机的 USB 3.0 口只能提供 0.9 安培,USB 2.0 只有 0.5 安培,这就是为什么你可能会在 Pi 的系统日志里看到这些警告:
$ sudo journalctl -xe | grep "Under-voltage"
Jun 28 06:23:15 tinypilot kernel: Under-voltage detected! (0x00050005)为了解决这个问题,我与一家工程公司合作设计了一块定制电路板,把 Pi 的 USB-C 口一分为二。第一个口接受 USB 供电,这样你仍能给 Pi 提供 3 安培的完整电流。第二个口接受 USB 数据输出,因此 Pi 仍能伪装成 USB 键盘。


TinyPilot Power Connector 让 Pi 能通过 USB-C 口获得 3 安培的供电,同时不丢失 USB OTG 功能。
重要的是,电源接头的数据口不含 USB 电源线路。这确保了计算机电源与 Pi 电源之间的电压差不会造成不良的电流回流。
注:如果没有合适的接头,在 Pi 连接着计算机的同时又用外部电源给它供电,存在损坏硬件的风险。详情请参阅 TinyPilot wiki。
源代码
TinyPilot 的软件以宽松的 MIT 许可证开源:
- tinypilot:TinyPilot 的网页界面与后端。
- ansible-role-tinypilot:用于把 TinyPilot 及其依赖安装为 systemd 服务的 Ansible role。
预装的 TinyPilot 成品设备
成品 TinyPilot 设备可在 TinyPilot 网站购买。我是 TinyPilot 公司的创始人,但已于 2024 年 4 月卖掉了这家公司,如今除了作为一名热情的用户之外,我与该公司已没有任何关系。
特别感谢 Max Devaev 在 uStreamer 上的出色工作以及对 TinyPilot 的贡献。
随机一篇博客