DeElderly养老机器人需要同时处理设备端长连接、传感器高频上报、家属远程探视和护理工单流转,后端如果采用传统的每连接一线程模型,内存和上下文切换成本会随并发量快速上升。Node.js的事件循环和异步非阻塞I/O可以从代码层面把这些不同来源的消息统一成事件流,用回调或Promise驱动处理逻辑。

一、Node.js事件驱动与养老机器人场景的匹配
养老机器人不是简单的Web应用,它更像一个持续在线的物联网网关。机器人端通过MQTT上报传感器与状态,服务端需要把这些数据按规则转发给家属App、护士站大屏或第三方呼叫中心。如果每个连接都占用一个线程,几百台机器人同时在线就会让服务器资源吃紧。Node.js底层基于libuv,网络I/O和定时器都通过事件循环调度,主线程只负责执行JavaScript逻辑,不会因为等待数据库或网络响应而阻塞其他连接。这意味着同一台2核4G的云主机可以轻松承载数千个WebSocket和MQTT会话。
DeElderly内部大量使用EventEmitter来解耦模块。例如传感器数据到达后,由sensorEvent事件触发跌倒检测、心率异常分析和数据落库三个订阅者,彼此之间没有直接调用关系。后续新增一个病情趋势预测模块时,只需要增加一个监听器,不需要改动原有代码。事件驱动还让测试变得简单,可以在测试用例中手动emit一个假数据,观察对应处理函数是否被正确触发。
使用事件驱动也要注意两个问题:一是回调地狱,DeElderly统一使用async/await封装异步流程;二是未捕获异常可能导致进程退出,需要为unhandledRejection和uncaughtException注册兜底处理。下面是一个简单的传感器事件分发示例。
const EventEmitter = require('events');
const emitter = new EventEmitter();
emitter.on('sensorData', async function(payload) {
if (payload.type === 'fall') {
await handleFallAlert(payload);
}
if (payload.heartRate < 45 || payload.heartRate > 130) {
await handleHeartRateAlert(payload);
}
});
function handleFallAlert(data) {
console.log('跌倒告警触发:', data.deviceId);
return Promise.resolve();
}
function handleHeartRateAlert(data) {
console.log('心率异常:', data.heartRate);
return Promise.resolve();
}
emitter.emit('sensorData', {
deviceId: 'robot-001',
type: 'fall',
heartRate: 42
});
上面的代码中,条件判断里的<和>在代码块里做了HTML转义,实际运行时仍是小于和大于号。EventEmitter的监听器默认按注册顺序同步执行,如果处理函数返回Promise,事件循环不会等待Promise完成,所以这里用async/await只是为了在监听器内部串行处理多个判断。
二、DeElderly通信层设计:MQTT与WebSocket协同
机器人处在养老院或家庭网络环境里,上行带宽和稳定性通常不如机房服务器。MQTT协议本身是为低带宽、不稳定网络设计的,支持QoS等级、遗嘱消息和会话保持,非常适合作为设备上报通道。DeElderly使用EMQX作为MQTT Broker,机器人所有传感器数据发布到deederly/device/{deviceId}/telemetry主题,服务端订阅deederly/device/+/telemetry即可拿到全部设备数据。主题中使用加号通配符能减少订阅数量,也便于按设备维度做授权。
WebSocket则负责服务端与浏览器、家属App之间的实时下行推送。为什么不直接让App订阅MQTT?主要原因是移动端长连接对功耗和系统限制敏感,WebSocket走HTTP升级通道,在浏览器和小程序里兼容性更好。DeElderly在Node.js服务中同时维护MQTT客户端和WebSocket服务器,收到机器人告警后立即广播给对应房间的家属和值班护士。下面是核心通信代码。
const mqtt = require('mqtt');
const WebSocket = require('ws');
const mqttClient = mqtt.connect('mqtt://127.0.0.1:1883', {
clientId: 'deederly-backend',
clean: true,
reconnectPeriod: 1000
});
const wss = new WebSocket.Server({ port: 8080 });
mqttClient.on('connect', function() {
mqttClient.subscribe('deederly/device/+/telemetry', { qos: 1 });
});
mqttClient.on('message', function(topic, message) {
const payload = JSON.parse(message.toString());
const deviceId = topic.split('/')[2];
broadcast(deviceId, payload);
});
function broadcast(deviceId, data) {
const text = JSON.stringify({ deviceId: deviceId, data: data });
wss.clients.forEach(function(client) {
if (client.readyState === WebSocket.OPEN) {
client.send(text);
}
});
}
这段代码中mqtt.connect的地址用的是环回地址,生产环境应替换为内网Broker地址。reconnectPeriod设置为1000毫秒,网络抖动后可以自动重连。WebSocket服务监听8080端口,实际部署时通常放在Nginx后面,用反向代理统一处理TLS。需要注意broadcast函数目前是广播给所有客户端,如果后续要按房间或角色隔离消息,需要在WebSocket连接建立时记录客户端身份,并在广播前过滤。
QoS的选择也影响实时性和可靠性。DeElderly对常规遥测数据使用QoS 0,尽量减少重传开销;对跌倒、紧急呼叫等关键告警使用QoS 1,保证至少送达一次。EMQX侧开启共享订阅时,多个Node.js实例可以分摊消息处理压力,避免单实例成为瓶颈。消息体统一用JSON,字段命名保持小驼峰,便于前后端和数据库字段映射。
三、关键业务实现:跌倒检测与语音呼叫处理
跌倒检测是养老机器人最核心的功能,但传感器误报率很高。如果每次加速度突变都直接推送告警,护士站很快会告警疲劳。DeElderly在Node.js中实现了两级确认机制:第一次触发跌倒候选后,启动一个10秒的确认窗口,窗口内如果设备没有上报取消事件,再升级为正式告警。这个逻辑用setTimeout和Map即可实现,不需要引入复杂的状态机库。
const pendingFalls = new Map();
function onFallCandidate(deviceId) {
if (pendingFalls.has(deviceId)) {
return;
}
const timer = setTimeout(function() {
pendingFalls.delete(deviceId);
escalateFallAlert(deviceId);
}, 10000);
pendingFalls.set(deviceId, timer);
}
function onFallCancel(deviceId) {
const timer = pendingFalls.get(deviceId);
if (timer) {
clearTimeout(timer);
pendingFalls.delete(deviceId);
console.log('跌倒候选已取消:', deviceId);
}
}
function escalateFallAlert(deviceId) {
console.log('升级为正式跌倒告警:', deviceId);
broadcast(deviceId, { type: 'fall_alert', level: 'critical' });
}
这个简单的防抖逻辑可以过滤大部分因为弯腰、坐下过重造成的瞬时加速度尖峰。实际项目中还可以结合机器人视觉识别结果做二次确认,比如在确认窗口内调用摄像头抓拍,用目标检测模型判断是否真的有人倒地。Node.js服务不负责推理,通过gRPC或HTTP调用Python推理服务,拿到结果后再决定是否升级告警。这样Node.js专注于事件编排,把计算密集型的模型推理留给更适合的运行时。
语音呼叫的处理稍有不同。用户按下机器人上的呼叫按钮或说出唤醒词后,需要快速建立与值班护士的双向通话通道。DeElderly没有在Node.js里直接处理WebRTC信令,而是把Node.js作为信令中转,机器人端和护士站App通过WebSocket交换SDP和ICE候选。Node.js只做房间管理和消息转发,媒体流走点对点或TURN服务器,避免音视频数据占用主服务带宽。值班护士接听前,系统先播放一段预设的安抚语音,这个语音文件放在对象存储中,Node.js返回临时URL给机器人端。
四、稳定性保障:断线重连、集群与压测
养老场景对系统可用性要求很高,夜间如果服务挂掉,机器人就无法上报紧急呼叫。DeElderly在进程管理上采用PM2的cluster模式启动多个Node.js实例,共享同一个端口。WebSocket的粘性会话需要额外处理,如果使用ws库,客户端重连后可能落到不同实例,导致状态丢失。DeElderly最终选择Socket.IO作为WebSocket封装,配合@socket.io/redis-adapter把消息广播到集群所有节点。
const { createServer } = require('http');
const { Server } = require('socket.io');
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');
const httpServer = createServer();
const io = new Server(httpServer);
const pubClient = createClient({ url: 'redis://127.0.0.1:6379' });
const subClient = pubClient.duplicate();
Promise.all([pubClient.connect(), subClient.connect()]).then(function() {
io.adapter(createAdapter(pubClient, subClient));
httpServer.listen(3000);
});
io.on('connection', function(socket) {
socket.on('join:room', function(roomId) {
socket.join(roomId);
});
socket.on('signal', function(message) {
socket.to(message.roomId).emit('signal', message);
});
});
这段代码中redis://127.0.0.1:6379同样属于环回地址,生产环境需要替换为独立的Redis服务。使用Redis适配器后,任何实例收到的signal事件都能转发到目标房间,即使连接在不同实例上也不影响。如果不想引入Redis,也可以用Nginx的ip_hash保持同一客户端始终连接到同一实例,但扩展性不如Redis适配器。
断线重连不只发生在服务端,机器人端也要处理网络切换。MQTT客户端设置reconnectPeriod后会自动重连,但重连成功后需要重新订阅主题。代码中在connect事件里订阅可以保证每次连接成功后都执行订阅动作。WebSocket客户端也要监听断线事件,并在指数退避后重新建立连接。DeElderly在前端封装了一个带重连逻辑的Socket.IO客户端,最大重试间隔30秒,服务端通过心跳检测清理超过60秒无响应的僵尸连接。
压测时使用mqtt-bench模拟500台机器人每秒上报一条消息,Node.js单实例CPU占用稳定在40%左右,内存约180MB。WebSocket侧用k6模拟2000个家属端连接,广播消息端到端延迟P99小于300毫秒。集群双实例压测时,消息丢失率为零。这些数据说明事件驱动模型在养老机器人这种长连接、多消息源的场景下具备明确的性能优势,而且开发成本比Java或Go的完整微服务框架更低。上线前建议再用混沌工具随机杀掉Node.js进程,观察PM2自动拉起后集群是否能在10秒内恢复正常。
从DeElderly的实现可以看出,Node.js不只适合做短平快的Web API,在物联网和机器人后端同样能发挥事件模型的优势。关键是把MQTT、WebSocket和业务状态机放在同一套事件循环里统一处理,同时用集群和消息队列补齐多核利用和单点故障短板。