量产设备的远程可达与出厂流程
结论先行:OTA 和远程入网是两条独立链路。OTA 只要求设备能主动出网拉取,不需要 VPN、不需要公网 IP、不需要在客户路由器上开端口; 只有「运维要够得着这台机器」才需要入网方案。把 VPN 塞进 OTA 链路是最常见的架构错误。 本页把入网选型、费用红线、金镜像容量事故、分区级救砖、出厂品牌后置合成一条可执行的产线流程。
问题背景
设备进客户现场后,网络环境完全不可控:没有公网 IP,多数落在运营商 NAT/CGNAT 后面,现场也未必有人能配合开端口。 这类设备一旦失联,只能派人跑现场,运维成本立刻失控。
同时量产阶段有几个只在批量时才暴露的问题:金镜像比新批次 eMMC 大一百多兆、改设备树把机器刷成砖、 每台机要打客户自己的开机画面。这三件事没做成流程,返工率会远高于装配环节。
一、两条链路先分开
| 链路 | 谁发起连接 | 前提条件 | 用途 |
|---|---|---|---|
| 状态上报 / 配置 OTA | 设备主动出网拉 | 能访问分发地址 | 心跳、printer.cfg 与主题下发、固件升级 |
| 远程入网(mesh VPN) | 设备加入虚拟内网 | 能出 443 + STUN | SSH、抓日志、看屏、临时排障 |
判据很简单:如果一台设备永远不需要人去操作它,它就不需要入网。绝大多数量产机的日常只需要第一种。 反过来,只要有人需要 SSH 进去,就要有第二种,而且这一层必须在做控制台和 OTA 之前就位 —— 客户现场网络千奇百怪, 没有「网络永远可达」这一层,后面的控制台、批量执行、远程升级全是空谈。
推进顺序不能乱:状态总览 → 一页 dashboard → 配置 OTA(风险最低、收益最大)→ 固件 OTA → 系统 OTA。 每一步都应当独立可用,不要一次上大改造。
自建时这六层分别由什么承担
| 层 | 干什么 | 自建选型 |
|---|---|---|
| ① 网络层 | 每台机永远可达(不用公网 IP、不在客户路由器开端口) | Headscale(设备无上限)或官方 Tailscale + 自建 DERP |
| ② 接入运维 | 批量执行命令 / 传文件 | Tailscale SSH(免密钥分发)+ Ansible(无 agent) |
| ③ 控制台 | 看每台机状态、点按钮 | Headscale 的 Web UI + 自己拼一页 dashboard |
| ④ 配置 OTA(最实用) | 推 printer.cfg / 触屏主题 / 宏 |
git 或自建 HTTP + 版本清单 manifest:每台机定时拉取 → 比对 → 应用 → 重启(留上一版可回滚) |
| ⑤ 固件 / 系统 OTA | MCU 固件 / 整机系统 | 固件复用出厂初始化的烧录能力;系统级 = RAUC A/B 分区 + 签名 + 回滚(要改金镜像分区表,放最后做) |
| ⑥ 监控告警 | 掉线 / 异常主动通知 | 探活(Uptime Kuma 一类)+ 设备侧上报脚本 |
第 ④ 层之所以「最实用」,是因为它只动文本与资源文件,出错可以回滚到上一版,风险远低于系统级 OTA。 第 ⑤ 层的系统升级会改分区结构,一旦和「金镜像缩容」的成果冲突,返工成本极高 —— 所以顺序上必须排在最后。
二、批量入网:三种做法,只有两种能上量产
| 做法 | 机器侧行为 | 机器上有长期密钥吗 | 产线要动手吗 | 适合量产 |
|---|---|---|---|---|
| ① 出厂注入一次性 key | 首启服务读 key 文件 → 入网 → 成功后自删 | 只在「出厂到首启」这个窗口存在 | 要(主机侧一条命令) | 可以,够简单 |
| ② 板上自取(通网即连) | 首启服务向内网签发端点要 key → 入网 → 用完即弃 | 零长期密钥(OAuth secret 只在签发端点上) | 不用 | 上百台首选 |
| ③ 镜像里塞 reusable key | 直接用镜像里的 | 在镜像里,永久 | 不用 | 绝对不行 |
第 ③ 种不是「不安全一点」,而是根本性错误:镜像流到客户手里,等于把整个内网的入场券一起交出去, 任何拿到镜像的人都能入网。上游文档本身就把 reusable key 标成「很危险,被偷了很糟糕」。
第 ② 种之所以是量产正解,是因为它把「谁有资格发 key」这个判断留在了你的服务端: 设备上没有任何东西可以被拷走,产线也不需要为每台机执行一条命令 —— 一台机刷完、联网、自己入网,流程上零人工。 缺点是依赖一个始终在线的签发端点,这个端点挂了,新机就入不了网(但已入网的老机不受影响)。
官方对第 ② 种的推荐写法是 OAuth client + auth_keys scope:
tailscale up \
--auth-key='<OAuth secret>?ephemeral=false&preauthorized=true' \
--advertise-tags=tag:prod \
--hostname=<机型>-<序号>
后台一次性准备:建 tag:prod 与 ACL(只允许自己的设备互相访问)→ 在控制台建 OAuth client
(scope 给 auth_keys 写权限、tag 绑 tag:prod)→ secret 只放运维主机(keychain 或环境变量),
不进镜像、不进 git、不进部署脚本的明文日志。
镜像侧只需要两样:tailscale 二进制 + 一个 enroll 服务。服务本体用 ConditionPathExists 盯一个 key 文件,
没有 key 就什么都不做 —— 这样同一个镜像既能用于「一次性注入」的批次,也能用于「板上自取」的批次。
三个必备件,缺一个就连不上
- 等网络 + 重试:
After=network-online.target,Restart=on-failure+RestartSec=120,脚本内部再做退避。 客户现场首启常常没网,服务必须能在「联网之后自己连上」,并且已入网则跳过(幂等,不能每次开机都重新注册一台)。 - 出网端口只要 443 + STUN。客户网络抽风时靠 DERP 中继兜底,不要指望 UDP 直连永远成功。
- 命名唯一:
--hostname=<机型>-<序号>,序号由量产脚本传入。否则后台会堆出一排同名节点, 分不清哪台是哪台 —— 这和镜像内主机名重复是同一类问题,只是发生在网络层。
三条缺一个都会「连不上」,且失败形态各不相同,可以按现象反查:
| 现象 | 缺的是哪一条 | 处置 |
|---|---|---|
| 节点一直不出现 | 首启无网,服务退出且不重试 | 补 network-online 依赖 + 重试与退避 |
| 出现一排同名节点 | 没传 hostname 序号 | 量产脚本必须带序号参数 |
| 入网了但连不通 / 看不到 | ACL 未放行或 tag 不匹配 | 核对 tag:prod 的 ACL 与签发时绑的 tag |
| 每次开机后台都多一台 | 入网判断非幂等 | 先检测状态,已入网直接退出 |
| 直连很慢、时通时断 | 只靠默认境外中继 | 自建 DERP,或配 OmitDefaultRegions |
三、开源边界与费用:选型先看这两张表
| 组件 | 开源情况 | 结论 |
|---|---|---|
客户端 tailscaled / CLI |
全开源(BSD-3) | 任意 Linux/ARM 可自编译;支持 --tun=userspace-networking(内核无 tun 也能跑);有 tsnet Go 库可把 mesh 嵌进自己的程序 |
| DERP 中继 | 可自建(go install tailscale.com/cmd/derper@latest,官方标 alpha) |
默认中继都在境外,自建(国内 VPS)能明显降时延;可配 OmitDefaultRegions 只走自家中继;需要域名 + 443/80 + STUN + ICMP,不能躲在 NAT 或负载均衡后面 |
| 控制面 | 官方闭源(closed source coordination server) | 要自建控制面只能用 Headscale |
Headscale 的能力边界要提前确认,别以为「自建 = 全都有」:
- 有:MagicDNS、Taildrop、Tags、子网路由、出口节点、ACL、自动批准、Tailscale SSH、内嵌 DERP、OIDC 注册。
- 没有:Funnel;OIDC 组不能用于 ACL;企业功能(审批后台、MDM、SSO 管理)。
费用上有一条最实际的红线:个人免费版对 user devices 是无限(≤6 用户、≤3 组),
但 tagged resources 只免费 50 个,超出按量计费。而量产机队几乎必然要用 tag:prod 做 ACL 隔离 ——
所以超过 50 台就要花钱,不 tag 才是免费无限。选型结论:
| 规模 | 方案 |
|---|---|
| ≤50 台 | 官方免费版,省事 |
| 上百台 | 官方付费,或 Headscale + 自建 DERP(设备无上限、零月费、数据自控) |
若要自建,按阶段推进,每一阶段都能独立用起来:
| 阶段 | 做什么 |
|---|---|
| 0 | 用一台常开的 Mac 当「服务器」,官方免费 tailnet 先接 3~5 台验证流程 |
| 1 | 一台轻量 VPS + 域名 → Headscale + 签发端点 + dashboard + OTA 分发 |
| 2 | 国内节点做自建 DERP,压时延 |
VPS 选香港/新加坡轻量:免备案、国内时延 30–60 ms。国内轻量最快但域名必须备案(1~2 周),会拖住整个排期。 规格 2 vCPU / 2 GB / 40 GB SSD / Debian 12 足够 —— 控制面只走信令(几十 KB 级),只有做 DERP 才吃带宽。
一个国内特有的坑:临时匿名隧道(trycloudflare.com 这类)在国内不可用 —— 公共 DNS 有记录或直接 NXDOMAIN,
板子本地解析也失败。量产只能走具名隧道或自建,不要把它当应急通道写进流程。
四、金镜像 eMMC 缩容:99% 处 Write LBA failed 的真因
这是量产时最容易被误判成「线材/接口接触不良」的一类故障。实际连卡两轮,根因是新板 eMMC 比源板小 118 MiB, 镜像尾部的 rootfs 装不下。
| 现象 | 数据 |
|---|---|
| 两轮烧录都停在 99% | 两份 8.3 MB、238,568 行进度点的日志,错误同一句 Write LBA failed! |
| 金镜像体积 | 15,758,000,128 B(14.676 GiB) |
| 99% 失败点 | 15,600,420,126 B,剩余未写入 ≈ 150.3 MiB |
| rootfs 分区 | LBA 2957312 → 30777310,结束于 15,757,983,232 B |
| 新板 eMMC(SAMSUNG) | 30,535,680 扇区 = 15,634,268,160 B |
| 超出量 | ≈ 118 MiB(源板 ~15028 MiB vs 新板 ~14910 MiB) |
判据:两端都停在同一个百分比、同一个错误 ⇒ 确定性问题(容量边界),不是概率性硬件故障。 概率故障的表现是每次停在不同位置、错误码会变。
不要直接 truncate 截断:末尾 8 MB 里有 806 万个非零字节,一截 rootfs 立刻损坏,机器能烧进去但起不来。
能不能缩,先读 ext4 超级块,看已用量:
| 项 | 值 |
|---|---|
| rootfs 文件系统 | 13.266 GiB |
| 实际已用 | 仅 4.77 GiB |
| 空闲 | 8.38 GiB |
余量极大 ⇒ 缩到 12 GiB 就能让整盘落进 15.63 GB 以内。全程在副本上操作,原件不动:
E=/opt/homebrew/opt/e2fsprogs/sbin # 主机侧 e2fsprogs
# ① 按偏移提取 rootfs 分区(偏移 1,514,143,744 / 长度 14,243,839,488),约 32.7 s
# ② 修复:运行时 dd 出来的镜像必然有脏计数/checksum,属正常
$E/e2fsck -f -y rootfs-shrink.img
# ③ 缩容 + 截断
$E/resize2fs rootfs-shrink.img 12G
truncate -s 12G rootfs-shrink.img
# ④ 只读复核
$E/e2fsck -f -n rootfs-shrink.img # 期望 clean、零错误;已用 4.87 GiB
# ⑤ 重拼整盘镜像 + 修正 GPT 分区表(重算 CRC32)→ 精确 15,634,268,160 B
五项校验全过才允许烧:
- 新镜像字节数正好等于新板 eMMC 全容量(不是「小于」,要相等);
- GPT primary + backup 两份分区表逐字节一致,CRC32 全对;
- 9 个分区全部落在可用区内;
- rootfs
clean; - 原始完整镜像完好未动(有 md5 可比)。
结果:得到 15,634,268,160 B 的镜像,烧录 0% → 100% 零 Write LBA failed,耗时 15 分 50 秒;
收尾 Reset Device OK,随后 USB 设备名从 Maskrom 阶段的通用名变成 SoC 的 rk3xxx 名 ⇒ 系统已从 eMMC 正常引导。
烧录侧的三条硬教训
- 下载 loader 只能做一次:板子进入 Loader 模式后再发一次
db会直接挂死(Download boot failed!), 之后连读信息、重启的命令都无响应,只能断电重进 Maskrom。 烧录脚本必须一把连续跑「下 loader → 写 → 重启」,中间不插任何探测命令。 - 板子 USB 直插主机机身口。走两级 hub 的扩展坞时 Maskrom 阶段不稳定;
注意
LocationID变化只代表地址变了,不代表真的换了物理口。 - eMMC 供应商与批次会变 ⇒ 每批板先读一次真实容量再烧,不要假设与金样机同容量。这条是本节的根因预防。
五、DTB 改错刷砖后的分区级恢复
设备树里触摸控制器的 irq-gpios 是三格属性:0x45000000 / 0 / 0x08000000,依次是 gpio、pin、flags。
flags 写成 8 时,INT 引脚带上偏置且被配成电平触发 ⇒ 中断线持续有效 ⇒ 永久误触发,
实测 719–722 次/秒,启动日志里有 Failed get int-trigger-type from dts, set default 这条线索。
一族板子上救过,于是在同型号另一批机子上照搬「8 改成 0」⇒ 直接卡在第一屏,ARP 都不回。 教训是硬的:同 SoC、同型号、不同批次,DTB 不能整套照搬,pinconf 的偏置方向与板级走线是绑定的。
恢复不需要全盘重烧。整盘 15.6 GB 重烧约 40 分钟,而且会丢掉现场配置与校准;只还原承载 DTB 的那个分区就够:
| 步骤 | 命令 | 结果 |
|---|---|---|
| 读分区表 | rkdeveloptool ppt |
boot 分区 = LBA 0x8000 = mmcblk0p3(64 MB,DTB 就在里面) |
| 进 MASKROM | 按住板上 MASKROM 键(或短接 eMMC CLK 测试点)+ USB 接主机,2–3 秒后松手 | loader 就绪 |
| 下 loader | rkdeveloptool db <loader> |
成功 |
| 只写回受损分区 | rkdeveloptool wl 0x8000 p3_backup.img |
100% |
| 重启 | rkdeveloptool rd |
3 秒后 ping 通 |
实测全程 2 分钟,系统、printer.cfg、标定值全部保留。
支撑这条流程的是备份纪律,它比恢复命令本身更重要:动 DTB 之前先把 p3(64 MB)整块 dump 出来,记 md5, 板上与运维主机各存一份并交叉校验。有了这个 64 MB 文件,「砖」就从事故降级成一次两分钟内可控的实验; 没有它,同样的动作就是一次需要返厂的故障。
两个读 DTB 时的实操细节:
- 三格必须一起解码。有人事后用
awk取错列(取到中间的 gpio number),据此得出「flag 已经是 0、板上没留改动」的 错误结论 —— 单看一格一定会读错。 - 恢复后要核回三格原值,不是核「中间那格变成什么」。
当前口径:生产机上不再试 DTB。 触摸风暴白烧约 10% CPU、把 load 抬到 6 上下,但打印本身不受影响,
不在紧急路径上。要治就两条路:在备用板上验 flags=16(另一种偏置)这个候选值,或找主板方案商要原始 DTS 源码重编。
此外,「换更轻的 rootfs / 更轻的发行版能治触摸」是错的 —— DTB 与内核驱动原样保留,中断速率会一模一样地继续。
六、出厂品牌后置:镜像通用 + 每台机打标
镜像保持出厂态不动,客户的品牌与界面在每台机上做。好处是镜像通用、一条流水线能吃多个客户; 风险是「某台机网络不通就漏做」,所以必须配巡检。
标准流程:
烧机(金镜像)→ 重启 → 自动连产线 WiFi 取 DHCP → 扫网定位 →
① SSH 替换开机画面(U-Boot 与 kernel 两份)
② 装触屏主题(默认主题指向客户方案)
③ 初始化 MCU(烧 bootloader + 写 [mcu] serial + 按机型套 printer.cfg)
→ 板上先备份原件 → reboot → 回网后回读 md5 校验 → 记日志
自动化侧用定时任务每 10 分钟扫一遍网段,新板一上线就处理;日志按行追加(时间戳 + 地址 + 动作 + md5 + 结果)。 只有真的改了板子才推一条通知,跳过的不推 —— 否则巡检消息会淹没真正需要人处理的异常。
开机画面的位置与格式
| 项 | 值 |
|---|---|
| 载体 | GPT 第 6 分区 logo_rk(vfat,板内挂 /mnt/logo) |
| 文件 | logo.bmp + logo_kernel.bmp,U-Boot 与 kernel 各读一份,内容必须一致 |
| 格式 | 1280×800 / 24-bit BMP / 3,072,054 B |
| 关键约束 | 同画幅、同字节数替换 ⇒ 不动 FAT、不动分区表,风险最低 |
| 生成 | 源图去白边 → 等比缩到内容高 620 px → 居中贴到 1280×800 纯白画布 |
| 回滚 | 镜像侧留两原文件 + 整分区 68 MiB;板内 /mnt/logo/provision_orig/ 留原件 |
画幅由板内 U-Boot 日期决定:较新的 U-Boot 会多转 90°,要用 1280×800;较老的一批要用 800×1280。 写死一个方向,另一批机出来的画面就是躺倒的。
⚠️ 查现役画面不能用「搜 BMP 签名」:FAT 的空闲簇里躺着历史版本,按签名扫会扫到副本上, 改完一重启又变回去。必须按 FAT 根目录项 + 簇链回读真正的现役文件。
五条安全守卫(防误伤在产机器)
- 只认指定 MAC 前缀的板(或「特定端口开着且型号匹配」),不碰任何远程/内网节点。 扫描范围限定在产线网段,这条是防止把客户在跑的机器当成新板。
- 正在打印绝不重启:读到打印状态为 printing/paused 或任务活跃 → 跳过并交回队列重排。
- 现役画面不在出厂白名单 → 拒绝。说明这台机被人改过,不是新板,交人工判断。
- 已是目标画面 → 跳过(幂等)。重复扫不会反复重启。
- 改完必须回读校验:重启回网后重算 md5,与写入值比对,一致才算完成。
地址冲突这件小事
有一次板子从 DHCP 拿到一个与现场 AP 自身相同的地址,两边互相抢答,SSH 与 Moonraker 都打不通, 看起来像「板子没起来」。根治办法二选一:把该地址从上游 DHCP 池排除,或量产时给每台机固定分配高位段的地址。 不要靠「拔了再插」,那是把确定性配置问题当成偶发故障。
七、可复用的三条铁律
- 镜像出厂态 + 每台机后置品牌化:镜像通用、客户隔离,代价是必须有巡检兜住「漏做的那台」。
- 改动前先备份、改完回读校验 + 重启复验,并把「已是目标态 → 跳过」做成幂等。
- 品牌只落在开机画面这一帧。Web 前端与触屏 UI 里没有客户品牌,不要为了「看起来更定制」去改前端 —— 那是把一次性打标变成长期维护负担。