
做无人机和机器人健康预警这个需求我是被一次“毫无预兆”的返航逼出来的。当时一台六旋翼在物流巡检途中直接触发低电量保护迫降落地后发现其中一组电池已经轻微鼓包。问题不在电池本身而在于监控后台只看了剩余电量完全没有关注单体压差、电池温度和充放电循环次数。后来我基于 Vue 和 Node.js 从零搭了一套健康预警系统把无人机、机器人上报的实时参数统一接入起飞前做体检、作业中做趋势研判发现异常提前报警。这篇文章就是把这套系统的设计思路、后端判定逻辑、前端展示和实际运行中踩过的坑完整复盘一遍。这套系统不局限于某一类设备凡是能上报遥测数据的移动机器人基本都能接入。适合正在做设备运维平台、无人机管理后台或机器人监控大屏的开发者参考也适合那种“传感器数据和告警逻辑还没想清楚就急着写前端”的团队。下面我按自己的真实落地顺序来讲。1. 先想清楚要监控什么健康预警不是只看“还有没有电”很多团队做无人机监控系统最容易犯的错就是把剩余电量、飞行高度、GPS 坐标画几个大屏就算完事。但健康预警的核心目的是“提前发现隐患”不是“记录事故”。所以第一步不是写代码而是把设备可能出现的故障模式拆出来确定哪些信号能够反映这些故障。1.1 五大类指标从动力、电池到通信链路我把无人机和机器人的健康指标分成五类每一类对应不同的故障模式动力系统包含电机转速、电调温度、电机温度、母线电流、螺旋桨振动。主要用来发现电机堵转、电调过热、螺旋桨损伤等问题。高速旋转下振动特征变化往往比温度变化更早。电源系统包含总电压、单体电压、单体压差、电池温度、充放电循环次数。锂电池的健康状态很难靠单一电压判断压差和温度的组合能提前预警鼓包风险。通信链路包含信号强度、上行丢包率、下行延迟。这类指标不直接代表机械故障但通信质量差会导致遥控指令延迟甚至失联同样属于安全风险。传感器状态包含 IMU 加速度、陀螺仪零偏漂移、GPS 定位精度、气压计高度。传感器漂移会造成姿态解算错误这类问题不监控的话飞控自检也未必能发现。结构状态包含机身振动烈度、机臂连接松动特征、云台电机负载。实际环境中机臂固定螺丝松动是常见故障通过振动谱中特定频段的能量变化可以识别。这里有一个容易忽略的点别把指标粒度设计得太细。比如电机有三相电流如果你把每一相电流都单独做成一条告警规则后台会被大量重复告警淹没。我会在采集端先做一次聚合把同一设备同一时间片的指标合成一个 JSON 对象后端只按“设备 指标名”处理。1.2 上报粒度原始报文、特征值、健康分三层设备上报的数据如果直接拿来存库会造成两个问题一是原始报文格式不统一二是噪声太大。我的做法是分成三层原始报文层设备通过私有协议、CAN 总线或类似 MAVLink 的通用协议上报采集网关负责解析并补包头信息。特征值层把原始值换算成有物理意义的指标比如电压转成百分比、振动数据经 FFT 后提取特定频段能量。健康评分层由后端规则引擎定时计算把一组指标映射成 0 到 100 的整数分用于起飞前体检和全局排序。举个例子电调上报的可能是一串寄存器值经过采集网关解析后变成这样一条统一 JSON{ deviceId: hex-0042, ts: 1736236800000, metrics: { motorTemp: 62.5, escTemp: 58.3, current: 28.6, voltage: 22.4, cellMaxVoltage: 4.19, cellMinVoltage: 4.11, signalRssi: -67, packetLoss: 0.02 } }存储层尽量只存特征值层和健康分原始报文落到日志系统即可。这样告警查询、图表检索都很快不需要每次从一堆二进制里解析。2. 数据怎么走到前端Node.js 的接入链路系统后端我用的是 Node.js版本选了长期支持版框架用的 Express 处理业务接口实时通道用 WebSocket。为什么这么选我在实际对比中主要有三个理由设备接入侧的 MQTT 生态成熟Node.js 的 MQTT 客户端库很稳定前端本身是 Vue前后端共用 JavaScript 技术栈调试成本低规则引擎逻辑不复杂不需要引入重型流计算框架单机 Node.js 足够扛住几十台设备同时上报。2.1 为什么用 WebSocket 而不是轮询无人机遥测数据的实时性要求不高每秒一次足够但要求低延迟推送。如果前端每隔几秒用 HTTP 轮询一次设备列表几十台设备同时刷新时接口压力很大而且告警无法做到秒级触发。所以我采用了两条通道业务查询走 HTTP REST 接口实时遥测和告警推送走 WebSocket。设备端如果本身支持 MQTT 协议后端可以挂一个 MQTT Broker 的客户端订阅主题如果设备端只支持私有 TCP 协议那就先经过协议网关转成内部消息再推给前端。这里要注意 WebSocket 的连接生命周期。前端页面切换路由时不应断开连接应该把连接挂在全局状态里统一管理。我一开始放在单个组件里创建 WebSocket结果每次离开页面就断线重新进去又要握手飞行动态数据丢失严重。后来改成全局单例连接配合自动重连问题才解决。2.2 Node.js 内部的分层与代码骨架Node.js 服务端我分了四层避免所有逻辑堆在一个文件里接入层负责 WebSocket 连接管理、心跳检测、设备鉴权。解析层统一处理各种协议报文转换成内部指标模型。判定层也就是规则引擎根据指标实时计算是否触发告警。通知层把告警推送给前端同时写入告警记录表。接入层的心跳检测值得多说一句。网络环境差时设备可能已经离线但 TCP 连接还没断开如果只监听 close 事件会延迟很久才感知掉线。我每 15 秒检查一次设备最近上报时间超过 45 秒没数据就主动标记离线并触发离线告警。后端核心代码大概是这个结构const WebSocket require(ws); const express require(express); const http require(http); const app express(); const server http.createServer(app); const wss new WebSocket.Server({ server, path: /ws/telemetry }); const ruleEngine require(./ruleEngine); wss.on(connection, (ws) { ws.on(message, (payload) { const message parseTelemetry(payload.toString()); if (!message) return; const alarms ruleEngine.evaluate(message); if (alarms.length 0) { broadcastAlarm(alarms); } broadcastTelemetry(message); }); }); function broadcastTelemetry(message) { wss.clients.forEach((client) { if (client.readyState WebSocket.OPEN) { client.send(JSON.stringify({ type: telemetry, data: message })); } }); } function broadcastAlarm(alarms) { wss.clients.forEach((client) { if (client.readyState WebSocket.OPEN) { client.send(JSON.stringify({ type: alarm, data: alarms })); } }); } server.listen(3000, () { console.log(telemetry server running on 3000); });实际生产环境里我不会让设备直接连接这个 WebSocket 端口而是前面加一层鉴权中间件校验设备编号和密钥。这里省略鉴权逻辑只保留链路结构方便理解。2.3 遥测数据与告警记录的表结构数据存储我用的是 MySQL实时性要求很高的场景也可以用时序数据库但小型项目直接上 MySQL 就够。关键是表结构要设计好不能把每个指标做成一列否则新增指标就要改表。我维护两张核心表遥测原始表和告警记录表。遥测表把动态指标放 JSON 字段里查询时按设备编号和时间索引告警表单独记录每次触发和恢复。CREATE TABLE telemetry_raw ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_id VARCHAR(64) NOT NULL, ts TIMESTAMP NOT NULL, data JSON NOT NULL, INDEX idx_device_time (device_id, ts) ); CREATE TABLE alarm_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_id VARCHAR(64) NOT NULL, rule_key VARCHAR(64) NOT NULL, level TINYINT NOT NULL, trigger_value DOUBLE, threshold_value DOUBLE, created_at TIMESTAMP NOT NULL, recover_at TIMESTAMP NULL, ack_by VARCHAR(64) NULL, INDEX idx_device_time (device_id, created_at) );这种设计的好处是采集端扩展新指标时后端解析层加一个字段映射就行不用动数据库。坏处是 JSON 字段没法建高效索引所以设备列表页的大盘数据不要直接查原始表而是定期把最新遥测写入一个device_latest小表列表页只读这张小表。3. 预警不靠单点阈值规则引擎的三种判定逻辑规则引擎是整个系统的核心。如果只是“超过阈值就告警”会出现两个很尴尬的场景临界值上下抖动导致告警反复触发温度缓慢上升时没有达到阈值就没人发现。这两种情况我都遇到过所以在这块下了不少功夫。3.1 滞后比较器先解决告警频繁抖动先说说最基础的阈值告警。无人机飞行中电机温度可能在 80 度上下波动如果阈值设为 80超过就告警、低于 80 就恢复前端会不停收到“告警-恢复-告警”的消息完全没法看。我采用的方案是滞后比较器也叫回滞判断。简单说就是进入告警状态的触发阈值和退出告警状态的恢复阈值分开设。比如电机温度超过 85 度才触发高温告警回落到 75 度以下才解除告警。中间的 75 到 85 度区间就是一个缓冲区用来吸收正常波动。核心代码可以抽成一个通用类class ThresholdJudge { constructor(ruleKey, level, high, recover) { this.ruleKey ruleKey; this.level level; this.high high; this.recover recover; this.alerting false; } update(value) { if (!this.alerting value this.high) { this.alerting true; return { triggered: true, level: this.level, value }; } if (this.alerting value this.recover) { this.alerting false; return { recovered: true, ruleKey: this.ruleKey, value }; } return this.alerting ? { ongoing: true, level: this.level, value } : null; } }这里有个参数标定心得阈值不能拍脑袋定要结合机型满载飞行、悬停、带载爬升等不同工况跑几轮数据。我当时的做法是收集一周的正常飞行日志取每项指标的 95 分位值作为告警触发线取 80 分位值作为恢复线效果明显比拍脑袋好。3.2 变化率与短窗口平均发现“温水煮青蛙”单点阈值还有个盲区温度从 50 度缓慢升到 90 度整个过程没有触发一次告警直到电机烧了才暴露。健康预警必须关注变化趋势。我在规则引擎里加了两类趋势判定。一类是短窗口变化率计算最近 60 秒内指标的变化斜率如果每分钟上升超过设定值就告警。比如电调温度每分钟升高超过 8 度即使当前温度还没到阈值也要提示“温度快速上升”。另一类是短窗口平均值取最近 5 个数据点的均值过滤偶发毛刺再判断阈值。趋势判定适合电量类指标。电池从满电掉到低电量是正常过程不能只看单次电压值要看放电曲线的斜率是否异常。如果飞行状态没变电压下降速度突然变快说明电池内阻增大需要提前返航。变化率判定的示例逻辑function calcSlope(samples) { if (samples.length 2) return 0; const first samples[0]; const last samples[samples.length - 1]; const dt (last.ts - first.ts) / 1000; if (dt 0) return 0; return (last.value - first.value) / dt; } // 每收到一条新遥测就把该设备的温度指标推进滑动数组 function evaluateTrend(deviceId, metric, value) { const queue trendQueues.get(${deviceId}:${metric}) || []; queue.push({ ts: Date.now(), value }); while (queue.length 0 Date.now() - queue[0].ts 60000) { queue.shift(); } trendQueues.set(${deviceId}:${metric}, queue); const slope calcSlope(queue); if (slope 0.13) { // 约每分钟上升 8 度 return { triggered: true, level: 2, message: temperature rising too fast }; } return null; }这个计算要放在内存里做不要每次都查数据库。单机内存里维护几万个队列没问题但要注意清理长时间离线设备的队列不然内存泄漏。3.3 健康评分把多维度指标收敛成一个分数告警只是离散事件但运维人员还想对上百台设备做排序哪台最该立刻停机检修哪台还能继续作业。这就需要健康评分。我给每个设备算一个 0 到 100 的综合分初始 100根据偏差程度扣分。不同指标的扣分权重不一样而且要设置“一票否决”项比如电池压差超过安全范围扣分直接拉满。电池健康分可以这样算function batteryHealthScore(metrics) { let score 100; const deltaV metrics.cellMaxVoltage - metrics.cellMinVoltage; // 压差是最重要的电池隐患指标 if (deltaV 0.5) { score 0; } else if (deltaV 0.3) { score - (deltaV - 0.3) * 100; } // 电池温度偏高逐步扣分 if (metrics.batteryTemp 45) { score - (metrics.batteryTemp - 45) * 1.5; } // 循环次数多代表老化 if (metrics.cycles 200) { score - Math.min(30, (metrics.cycles - 200) * 0.05); } return Math.max(0, Math.min(100, Math.round(score))); }健康分的用途很明确起飞前任何设备健康分低于 80 就禁止起飞飞行中健康分降至 60 以下强制返航看板页按健康分降序排列运维人员一眼看到最危险的设备。评分规则也要支持配置化不同机型、不同电池批次可以单独调权重。4. Vue 端如何把预警展示得“快而不慌”前端我用的是 Vue 3配合状态管理库做全局数据共享图表用 ECharts。实时监控页面要同时解决数据频次高、设备数量多、告警干扰大这三个问题。4.1 实时通道的缓冲与节流渲染设备端每秒上报一次遥测几十台设备同时在线时前端每秒钟可能要收到几十条消息。如果每条消息都立刻操作 DOM页面必卡无疑。我的做法是WebSocket 收到数据后只写入内存缓冲区然后启动一个每秒执行一次的定时器统一从缓冲区读取最新数据并更新界面。缓冲区只保留最近 500 条消息超过就丢弃最老的。对于图表数据同样控制点数超过一定数量就进行聚合降采样。Vue 里的核心逻辑可以简化成下面这样// telemetryStore.js const telemetryBuffer []; const latestByDevice reactive({}); function initWebSocket() { const ws new WebSocket(/ws/telemetry); ws.onmessage (event) { const msg JSON.parse(event.data); telemetryBuffer.push(msg); if (telemetryBuffer.length 500) { telemetryBuffer.shift(); } }; } setInterval(() { for (const item of telemetryBuffer) { latestByDevice[item.deviceId] item; } telemetryBuffer.length 0; updateRender(); }, 1000);这里有个细节不要每次清空整个缓冲区后重新遍历所有设备而是维护一份“脏设备列表”只更新当前页面正在展示的那部分设备。这样即使后台有上百台设备在线前端也不会一次性渲染全部数据。4.2 告警分级与交互不是每条告警都值得弹窗告警见得太多之后反而会麻痹。我一开始把所有级别的告警都做全屏弹窗结果每次飞行任务都有三四条三级告警弹出维护人员直接点掉真正严重告警反而被忽略。后来我把告警分成四级提示、一般、严重、致命。提示和一般级只更新侧边栏折叠面板不打断操作严重级在右下角弹出气泡伴随短暂声音提示只有致命级才做全屏阻断式弹窗必须人工确认。确认之后还要填写处理意见否则不算闭环。告警交互里还有一个容易被忽略的功能静默。无人机在某个区域长时间执行任务时同一条指标反复触发告警运维人员已经知道情况但不方便立即返航这时需要“静默 30 分钟”之类的操作期间不再重复推送该规则。静默要有上限不能无限期静默导致漏报。4.3 设备看板与历史回放设备详情页要展示三类信息当前实时指标卡片、最近 10 分钟的走势图、历史告警时间线。实时指标卡片按健康分排列异常指标用醒目颜色标出下降趋势。走势图默认展示电池温度、电调温度、单体电压三条曲线用户可以手动增加指标对比。历史回放功能对故障定位很重要。飞机落地后需要回看告警触发前 5 分钟的所有指标变化判断是突发异常还是渐变累积。我的做法是把历史遥测数据按秒粒度重新推送到前端图表加一个播放进度条速度可以调 1 倍、5 倍、10 倍。回放时的数据不是从 WebSocket 实时推的而是从后端历史接口分批拉取避免 WebSocket 通道被回放流量占用。5. 实际运行中踩过的坑和对应修正系统从开发到稳定运行走了不少弯路。这里挑四个最有代表性的坑都是我实际遇到并修正过的比任何“最佳实践”都管用。5.1 弱网环境下的断连与重连无人机在户外飞行时4G 信号经常不稳定。设备端和服务器之间的 WebSocket 连接可能在飞行途中断开几秒如果重连机制写得不好这段时间的遥测数据就丢了。一开始我只在连接关闭后 1 秒重连一次结果弱网环境下频繁重连服务器日志刷屏设备状态时有时无。后来改成指数退避重连从 1 秒开始连续失败则 2 秒、4 秒、8 秒递增最多不超 30 秒。同时在服务端做“最后上报时间”检查超过 45 秒没收到数据就标记离线而不是依赖连接是否真的断开。还要处理好丢失数据的补偿。无人机恢复通信后设备端通常会把缓存的历史遥测补传上来这些补传数据的原始时间戳是过去的时间。存储层要支持按设备时间戳写入避免因为乱序导致图表里最新数据覆盖旧数据。5.2 告警风暴5 分钟聚合一次告警风暴是我遇到过最让人头大的问题。一次任务中一台无人机的通信丢包率超过阈值触发告警后由于丢包率持续超标每隔 10 秒又触发一次几分钟内告警记录表里多了一百多条相同规则告警。告警推送必须做聚合。我的方案是同一设备、同一规则在 5 分钟内只推送一次后续触发只更新当前告警状态和触发值不新增记录。如果 5 分钟后仍然异常则以“持续告警”状态继续显示但推送频率降到每 5 分钟一次。告警恢复时只记录一次恢复时间。实现上可以加一层简单缓存const alarmWindow new Map(); function shouldPushAlarm(deviceId, ruleKey) { const key ${deviceId}:${ruleKey}; const now Date.now(); const last alarmWindow.get(key) || 0; if (now - last 5 * 60 * 1000) { return false; } alarmWindow.set(key, now); return true; }这个缓存在内存里就够了重启后丢失最多也就是重新采集一轮状态影响不大。5.3 时间戳不统一导致的误判无人机飞控、机载电脑、服务器各自的时间基准可能有偏差。如果规则引擎拿设备时间戳做变化率计算设备时钟快了 30 秒趋势判断就会出错甚至出现“未来时间”的遥测记录。我统一以服务器接收时间作为权威时间戳。设备上报的时间戳单独存一个字段仅用于排查设备端时钟问题。规则引擎计算变化率、滑动窗口时一律使用服务器时间。还有一个夏令时和时区的坑。如果前端直接展示设备本地时间用户在不同时区会看到混乱的时间轴。后端接口全部返回 UTC 毫秒时间戳前端在展示层统一做本地化格式化这是最稳妥的方案。5.4 前端长列表和图表卡顿设备超过 50 台之后列表页的渲染性能明显下降。输入框搜索时每敲一个字都要过滤几百条设备记录卡顿感很强。我用了虚拟滚动组件只渲染当前视口内的设备行滚动时动态替换。对状态卡片里的实时数字改用局部文本节点更新不整行重建 DOM。图表的卡顿主要来自数据点过多。ECharts 在数据点超过一万个时渲染效率会显著下降尤其还要开着动画。我的做法是默认展示 10 分钟窗口数据点超过 600 个时按时间聚合每 5 秒取一个采样点采样时保留区间内的最大值和最小值这样温度峰值和电压低谷都能保留不会因为降采样丢掉关键尖峰。健康评分的颜色变化也要注意最好不要用闪烁动画。运维现场光线复杂闪烁的告警灯反而不利于快速定位我用的是稳定的高饱和度色块加粗体数字配合顶部固定置顶的致命告警区。系统运行到现在我最大的感受是预警效果好不好不取决于用了多复杂的算法而取决于规则参数是否和具体机型、任务场景做过联动标定。先用朴素的阈值加趋势加评分跑通把数据质量和告警闭环做干净比一开始就上机器学习模型靠谱得多。后续如果要扩展可以考虑在健康评分基础上叠加异常检测模型或者结合任务载荷数据做寿命预测但前提始终是先把实时链路和告警治理这两件事做扎实。