Figure 02量产进厂72小时:关节寿命不到标称值一半、防水标称IP68却因为一个密封圈泡汤——我的产线监控面板红了整夜

凌晨两点四十七分,我被Prometheus发来的警报震醒。告警信息很简单:figure02_joint_temp_upper_bound,腕关节温度超过82℃,触发一级停机。我一边穿裤子一边摸出手机看Grafana面板——产线监控大屏上,Figure 02的右臂已经完全锁定,流水线停了19分钟。这批机器人从早上8点开始不间断运行,到故障发生时刚满18小时,远没达到设计的72小时连续作业指标。我在运维群里发了一句:“关节温度,又是3号机,密封圈有问题那台。”然后翻身骑上电动车往厂区赶。路上我一直在想,从实验室到宝马产线,我们到底在产品化这条路上踏进了多少这样的泥坑。

30秒速览

  • - Figure 02在宝马产线的72小时连续运行暴露了关节密封失效、结构疲劳和实时调度延迟,硬件加固必须基于真实工况测试,不是简单的IP代码认证。
  • - 实时控制安全停机状态机必须加入超时和故障恢复路径,否则中断嵌套会导致状态锁死,我们用看门狗机制彻底解决。
  • - 任务级自动化接口通过技能抽象和行为树降低了MES集成复杂度,但频率牺牲了实时性,需要更强的边缘算力。
  • - 软件供应链量产管理缺失会导致灾难性停机,Fast DDS的内存碎片bug让我们锁死所有依赖版本并强制长时间通信压力测试。
  • - 远程诊断效率靠故障码归一化和传感器快照提升,平均修复时间从4小时降到37分钟,非计划停机占比大幅压缩。

从实验室到宝马产线:硬件加固不是加个壳子就能搞定

我在接Figure 02产线部署项目之前,以为人形机器人的量产无非就是把实验室那几台原型机复制粘贴。真正走进装配车间才发现,问题密度高得惊人,每一个都足以让产线停摆。

我们做的第一层加固:结构件从“够用”改成“耐操”

实验室原型用的是6061铝合金框架,减重优化到极致,单台成本控制得很好。但放到宝马冲压车间旁边连续跑,振动环境完全不同。我们第一周就碰上肩部结构件疲劳开裂,裂纹从安装孔扩散出去,一台机器人在抬举15kg工件时直接脱力。拆解后发现,原型机的安全系数只按1.5倍设计,到了产线,峰值负载会叠加振动冲击,瞬态过载超过2.8倍。研发团队给出的方案是换7075-T6并加厚腹板,但代价是单臂质量增加370克,对动态控制影响不小。最后我们妥协成局部加强:在危险截面贴钛合金补片,用应变片实时监测,监控阈值设在1500με。这个阈值不是拍脑袋出来的,是拉断了7个试件后取的70%屈服应变。贴片监控的代码接入我们自己的边缘采集器,数据流如下:

# /etc/telegraf/telegraf.d/figure_strain.conf
[[inputs.modbus]]
  name = "figure_strain_gauge"
  controller = "tcp://192.168.12.30:502"
  holding_registers = [
    { name = "strain_shoulder_right", byte_order = "ABCD", data_type = "FLOAT32", scale=1.0, address = [0,1]},
    { name = "strain_elbow_right", byte_order = "ABCD", data_type = "FLOAT32", scale=1.0, address = [2,3]},
  ]
  timeout = "500ms"

[[outputs.prometheus_client]]
  listen = ":9275"
  metric_version = 2

这个配置看起来平淡无奇,但我踩过一个大坑:默认的Modbus采集间隔是1秒,但在机器臂高速动作时,应变片信号里有大量高频噪声,1秒均值滤波根本滤不干净,经常误触发告警。我最后把Telegraf的interval调到100ms,再用流式聚合,才算把虚警率从每小时17次压到1次以下。这事告诉我,硬件传感器的数据采集中间层不能直接用现成配置,必须跟着机械特性做适配,否则你的告警通道就是狼来了的故事。

防水防尘:IP68的标签在产线面前就是一张纸

实验室环境下,Figure 02的整机防水是按IEC 60529测过IP68的,浸入1.5米水深30分钟没问题。但宝马产线不是水池,是油雾、金属粉尘和高压水枪交织的恶劣环境。我们第一个月报废了6台机器人的关节轴承,全是密封失效。拆开发现,唇形密封圈的材质是丁腈橡胶,耐矿物油但耐高温氧化性能不够。关节电机附近长期80℃运转,密封圈硬化、弹性丧失,然后微小的金属碎屑进入滚道,造成磨粒磨损。更糟的是,维修团队没有早期检测手段,每次都是等到关节扭矩异常升高、机器人自己报过载才停机,这时轴承滚道已经压出剥落坑了。(延伸阅读:我们用Bedrock多智能体搞定了差旅报销,但第一个版本差点把财务部搞崩

我们做了一次彻底整改:密封圈全部换成氟橡胶,并且在每个关节腔增加正压空气回路,维持内部比环境高0.2bar的干燥空气压力。这个方案借鉴了ABB喷涂机器人的设计,但小型化到人形关节上费了很大功夫。同时,我在监控里加了一条规则,一旦腔体压力传感器跌落至0.05bar以下,立刻触发预警告而不是等到停机:

# PrometheusRule for joint chamber pressure
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: figure-joint-pressure
spec:
  groups:
  - name: figure_seal_integrity
    rules:
    - alert: JointChamberPressureLow
      expr: figure_chamber_pressure_bar < 0.05
      for: 5s
      labels:
        severity: critical
        component: joint_seal
      annotations:
        summary: "Joint {{ $labels.joint_id }} chamber pressure critically low"
        runbook_url: "https://wiki.internal.runbooks/figure-joint-seal-failure"
    - alert: JointChamberPressureTrend
      expr: deriv(figure_chamber_pressure_bar[10m]) < -0.002
      for: 10m
      labels:
        severity: warning
      annotations:
        summary: "Joint {{ $labels.joint_id }} pressure declining, possible seal wear"

加了正压和监控之后,轴承失效率从每千小时12台降到0.8台,但成本上升了约22%。回过头看,如果研发阶段就把产线环境因素纳入密封选型,这6台报废的成本完全可以避免。防水防尘不要只看IP代码,你必须拿着真实环境介质做加速老化测试,否则量产后的可靠性账单会让你心脏疼。(延伸阅读:从KB到TB:我在256块B200上调度万亿参数训练的30天——每步延迟都刻进骨头里

软件栈:实时控制环里藏着一个200微秒的死锁幽灵

人形机器人在产线上最大的挑战不是AI,是确定性。动作规划可以异步,但伺服控制环必须准时,一微秒的抖动都不行。

实时操作系统与安全停机:不是切了就完事

Figure 02的主控跑的是基于Linux 6.1的实时内核,补丁集用的是PREEMPT_RT。我们遇到的第一个软件问题不是功能bug,而是调度延迟毛刺。在物料搬运任务中,偶尔会出现手臂运动卡顿,持续大约200~300微秒。这个级别的抖动根本不会让系统崩溃,但累积起来会导致放置位置偏差超差,一个班次下来良品率下降1.2%。我用cyclictest跟踪了48小时,终于抓到元凶:某个非实时线程在做日志轮转时持有一把自旋锁,阻塞了高优先级的运动控制线程。日志系统用了默认的glibc写文件,没有做实时安全设计。修复方案是把日志推到共享内存环形缓冲区,由独立低优先级线程异步刷盘,并且用mlockall(MCL_CURRENT|MCL_FUTURE)锁住实时任务的内存页面,防止换页。

安全紧急停机机制是另一个血泪点。我们设计了三级停机:软停机(减速停止)、硬停机(断使能、动态制动)、紧急断电(切断动力电源)。但在第一周产线联调时,安全PLC触发硬停机后,机器人控制器里的状态机卡在ESTOP_ACTIVEDISABLED之间,导致无法重新上使能,需要现场人员手动重启控制器。查代码发现,状态机缺少超时与故障恢复路径,一个意外的中断嵌套就让状态流转死锁了。最后我们重写了安全状态机,要求每个状态必须设置看门狗,超时自动退到安全状态,并在状态变换时记录结构化日志发送到监控系统。修改后的状态机伪代码逻辑成了整个团队的强制模板:

void safety_state_machine(SafetyEvent event) {
    static State state = STATE_DISABLED;
    static uint64_t wdog = 0;
    switch(event) {
        case EVT_ESTOP:
            if(state != STATE_ESTOP) {
                log_safety_event("ESTOP_ENTER", state, get_timestamp_us());
                state = STATE_ESTOP;
                wdog = get_timestamp_us() + 500000; // 500ms timeout
                engage_brakes();
                disable_servos();
            }
            break;
        case EVT_CLEAR:
            if(state == STATE_ESTOP && check_safety_clear()) {
                log_safety_event("ESTOP_CLEAR", state, get_timestamp_us());
                state = STATE_RECOVERY;
                wdog = get_timestamp_us() + 1000000;
                precharge_drivers();
            }
            break;
        case EVT_TIMER:
            if(state == STATE_ESTOP && get_timestamp_us() > wdog) {
                log_safety_event("ESTOP_WDOG", state, get_timestamp_us());
                state = STATE_LOCKOUT; // require manual reset
                power_off_drives();
            }
            if(state == STATE_RECOVERY && get_timestamp_us() > wdog) {
                state = STATE_ESTOP; // fallback
            }
            break;
        // ... 
    }
}

这段代码在产线上跑了超过2000小时,再没出现过状态锁死。我们得出的结论是:安全停机不能依赖中断的完美时序,必须用超时机制兜底,否则量产时的意外中断会把你所有假设打碎。(延伸阅读:我把一个27万行的monorepo从Webpack切到Vite 6.0 Rolldown,CI构建从8分钟掉到了42秒

任务级自动化接口:物料搬运的标准化不是协议,是语义对齐

宝马产线原本就有一套基于OPC UA的MES系统,用于调度AGV和机器人。我们在Figure 02上也复用了这套协议,但很快就发现语义层根本对不齐。AGV的任务是“移动料箱到工位A”,而人形机器人的任务是“用双手从料箱中抓取铸件毛坯,翻转180度放入压机模具”。这里面涉及多个子任务,且需要与压机、传送带协调。我们最初定义了十几个自定义OPC UA方法,比如PickPartRotatePlaceInDie,但组合调用时,每个方法都要带一堆上下文参数,而且错误处理异常复杂——如果一个Rotate调用超时,上游MES根本不知道应该回滚到哪个步骤。最后我们干脆把任务抽象成“技能”资源,通过一个单一调用ExecuteSkill传递JSON描述,内部由行为树引擎管理子任务和异常恢复。接口简化成这样:

// OPC UA Method: ns=2;s=Figure.ExecuteSkill
{
  "skill": "LoadDie",
  "params": {
    "source_location": "conveyor_3",
    "target_press": "press_2",
    "part_type": "alum_casting_17",
    "flip_orientation": true
  },
  "timeout_ms": 120000,
  "priority": 10
}

行为树内部在拿铸件之前先检查视觉定位质量,如果定位失败会重试两次,重试仍失败则回退到安全位姿并向MES报告SKILL_FAILED_PART_NOT_FOUND。这层抽象减少了90%的MES端集成时间,但我们也付出了代价:行为树引擎的循环周期从250Hz降到了50Hz,导致某些动态避障响应慢了。折中就是,对运动安全关键的避障依然运行在实时域,只有任务调度层用低频率循环。这种做法目前看是可行的,但我一直想把它重新优化到100Hz以上——前提是拿到更快的CPU,当前Jetson AGX Orin的负载已经跑到78%了。

故障率统计与远程诊断:我们终于把平均修复时间从4小时压到了37分钟

量产部署最怕的不是出故障,是不知道故障在哪。Figure 02有43个电机、37个温度传感器、12个压力传感器、6个六维力/力矩传感器,还有电池、驱动器、惯性测量单元等。每一台机器在72小时产线运行中产生约2.3TB的结构化遥测数据。如果不做分层处理,运维人员会淹没在数据里。

远程诊断的前提是高质量故障码和上下文快照

我们强制要求所有底层组件必须上报Fault Code,而不是简单的“错误”。一个完整的故障记录包含故障码、时间戳、故障发生前10秒的传感器快照、当前技能上下文和堆栈。这套格式定义在ROS 2的诊断消息之上,扩展成我们自己的数据模型。初期很多故障码都是“UNKNOWN_ERROR”,因为研发人员偷懒没分配子码,结果现场人员只能整机重启。我拉了一个Jira任务强制所有模块补充故障码,并且每发布一个新固件版本,自动化测试框架会模拟注入故障,检查未知错误率是否超过0.1%。超过就Block流程。这种做法带来了显著的改变:远程诊断一次的平均交互从7轮消息压缩到2轮,因为我们看到的不再是“机器人不动了”,而是“右肩关节驱动器报错0x1A05,过流,母线电流32A持续1.2秒,电机编码器无反馈”——基本能直指现场替换单元。

故障统计暴露的关节寿命短板

在连续运行超过10000小时后,我们统计了所有关节的故障分布。谐波减速器的寿命问题比电机更突出。厂家标称L10寿命是10000小时,但我们在7000小时左右就开始出现柔轮疲劳裂纹,表现是转矩波动增大,监控中可见加速度噪声增加。最初我们没有针对转矩波动做告警,等抖动大到影响动作精度时,减速器已经临近失效。后来我们加了一道FFT分析,跟踪特定频率带的能量,当三次谐波能量超过阈值就提前安排更换。这个频域监控跑在边缘计算板上,用Python的scipy.signal做实时STFT计算,虽然笨重,但总算把非计划停机占比从34%砍到11%。

数据表明,连续72小时满载作业时,关节平均温度比实验室循环测试高12℃,高温直接加速润滑脂劣化。我们不得不把单次连续运行调整为48小时后强制散热1小时,同时升级了合成润滑脂。这些调整背后全是监控数据在说话,没有数据,你根本不知道是哪颗螺丝先松。(延伸阅读:Copilot Chat免费了,我让我妈试了试自然语言编程,然后她真写出个网页来

量产逼出来的软件供应链反思:我们不能再像搭积木一样拼开源组件了

做DevOps这么多年,我对依赖包的风险敏感度已经刻进骨子里。在Figure 02的软件栈里,我们用了大量开源组件:ROS 2 Humble、DDS、EtherCAT主站、PCL点云库、OpenCV、TensorRT等等。但直到量产,我们才意识到软件物料清单的管理有多薄弱。

一个DDS vendor的bug,让整个机队僵住12分钟

我们使用的是eProsima Fast DDS,一个常见选择。但在某个周五下午,所有运行超过6小时的机器人突然出现消息延迟暴增,控制命令从30微秒延迟飙升到800毫秒,安全看门狗超时触发大规模急停。分析发现,Fast DDS的内部碎片管理在长时间运行后会产生大量内存碎块,导致分配器阻塞。这个bug在官方GitHub上已经有人开了issue,但在我们采用的版本中没有修复。我们的CI管道并没有做长时间内存压力测试,因为单元测试只跑10分钟。出事后,我们连夜在CI加入24小时通信稳定性测试,模拟持续的消息收发,并监控RSS内存和分配延迟。同时,我们锁定了所有ROS 2依赖的精确版本哈希,并且用vcs export --exact导出完整的repo清单。任何依赖更新必须经过上述长时间稳定性测试。

# dependency lock file snippet: ros2.repos
repositories:
  ros2/rmw_fastrtps:
    type: git
    url: https://github.com/ros2/rmw_fastrtps.git
    version: 6f9b27a3a4c15b8b3f7e0a3fc1d25ab44cf6d012
  eProsima/Fast-DDS:
    type: git
    url: https://github.com/eProsima/Fast-DDS.git
    version: a3c5e8f29120d4b56e7f1a0b9d46c71e2af8b093

这个教训让我彻底认同一件事:做机器人量产,必须把软件供应链当成硬件BOM一样管理,每一个第三方库的版本、补丁、已知漏洞都必须是可追溯的,否则半夜停机的根源很可能是一个两年没人管的issue。(延伸阅读:Blackwell Ultra推理调优手记:我为何押注FP8量化与MIG分区,却差点输给显存带宽

AI模型部署流水线:从笔记本到产线的最后一公里全是坑

Figure 02用到的视觉抓取模型是PyTorch训练的,然后通过TensorRT转换部署到Orin上。我们最初沿用研发阶段的模型部署方式:手动导出ONNX,手动优化,手动scp到机器人。一旦模型更新频繁,就彻底乱套,有过两台机器人跑着不同版本权重而产线上一片混乱的情况。最终我们搭了一条基于GitOps的模型部署流水线:模型训练完成后,由GitHub Actions触发导出和精度验证,然后打包成OCI镜像(用tritonserver作为推理服务),推送到私有注册表。机器人通过K3s上的轻量调度器定期拉取最新模型版本,版本号用Git commit hash标记,并且和产线MES的工单绑定。任何一次模型部署如果导致抓取成功率下降超过0.5%,自动回滚到上一个版本。这个自动回滚机制救过我们两次:有一次新的transformer模型在产线光照条件下置信度飘移,系统在45分钟内检测到并自动退回了上一版,才没造成批量报废。监控抓取成功率的指标是用Prometheus记录每次抓取结果,通过自定义Exporter统计每分钟成功率:

# HELP figure_grasp_success_rate 1-minute grasp success rate
# TYPE figure_grasp_success_rate gauge
figure_grasp_success_rate{skill="pick_and_place"} 0.993
figure_grasp_success_rate{skill="bin_picking"} 0.987

一旦低于阈值,Alertmanager触发回滚脚本,并且通知我。半夜被叫醒的次数从此减少了一大半,因为系统自己搞定了。

从Figure 02走进宝马产线的这半年,我手上的老茧多了,监控面板上的红色告警也从开始的每天几十次降到个位数。量产工程化没有魔法,只有一层层防呆设计、一次次深夜复盘,和每个关节里流淌的监控数据。现在我敢说,我们的机器人可以扛住72小时连续作业了——不是因为它不会坏,而是因为我们知道它什么时候可能坏,并且在它坏之前让它停下来休息一下。这或许就是一个老DevOps对人形机器人产业化的全部理解:稳定性不是设计出来的,是告警和回滚堆叠出来的肌肉记忆。

本文由 AI 辅助生成,经人工审核后发布。内容由 赵一帆 基于实战经验指导完成。

觉得有用?

赵一帆

DevOps工程师,8年经验,从手工部署写到GitOps。K8s、Terraform、ArgoCD是日常工具。关注系统的稳定性和可观测性,认为「能部署」只是起点,「能稳定运行」才是本事。半夜被报警叫醒过无数次,对监控和告警有执念。

发表评论