秋天一到,院子里的落叶就成了让人头疼的家务活。能不能自己动手做一台扫叶机器人,让它自动把院子清理干净?DeLeaf就是这样一个小型自主清扫机器人项目:下位机用Arduino控制电机和传感器,上位机则完全用Node.js编写,负责路径决策、任务调度和远程监控。很多前端开发者掌握了JavaScript之后想往硬件方向延伸,这个项目正好是一个门槛不高、乐趣十足的切入点。

为什么选择Node.js做机器人的大脑
传统机器人项目通常用C++、Python或者ROS来写主控逻辑,JavaScript听起来似乎不搭边。但在DeLeaf项目中,Node.js确实是最合适的选择。首先,扫叶机器人的核心工作是对接大量IO:轮询传感器数据、给电机下发指令、把状态推送到手机浏览器,这些全是IO密集型任务,正好是Node.js事件循环最擅长的场景。
其次,Node.js的生态足够丰富。serialport库可以稳定读写串口,express和ws库几行代码就能搭出Web监控面板,johnny-five框架甚至可以直接抽象出Motor、Sensor等硬件对象。对于想快速验证想法的原型项目,JavaScript的灵活性远胜C++。
当然也要客观看到短板:Node.js是单线程的,不适合做高实时性的PWM波形生成和PID闭环控制。所以DeLeaf采用了分工架构:把硬实时任务交给Arduino的定时器中断,Node.js只负责高层决策。这是初次接触机器人开发的朋友最容易忽略的一点——选对任务边界,比选语言本身更重要。
系统架构与硬件通信设计
DeLeaf整体分为两层。下位机是Arduino Mega,通过L298N驱动板控制两个直流减速电机和一个滚刷电机,同时读取前、左、右三个方向的红外避障传感器和地面的树叶检测传感器。上位机是树莓派,运行Node.js主程序,两者之间用USB串口连接,波特率115200。
通信协议采用自定义的JSON行协议,每条消息以换行符结尾。之所以选JSON而不是二进制帧,是因为调试阶段可以直接用串口助手肉眼看数据,排查问题效率极高。数据帧格式示例如下:
// 下位机上报传感器数据
{"type":"sensor","front":0,"left":1,"right":0,"leaf":1,"battery":87}
// 上位机下发运动指令
{"type":"move","left":150,"right":150,"brush":200}
// 下位机回执
{"type":"ack","cmd":"move","status":"ok"}Node.js端使用serialport库加ReadlineParser按行切分数据,避免粘包问题,核心代码如下:
const { SerialPort } = require('serialport');
const { ReadlineParser } = require('@serialport/parser-readline');
const port = new SerialPort({
path: '/dev/ttyUSB0',
baudRate: 115200,
});
const parser = port.pipe(new ReadlineParser({ delimiter: '\n' }));
parser.on('data', (line) => {
try {
const msg = JSON.parse(line);
if (msg.type === 'sensor') {
updateRobotState(msg); // 更新机器人状态
}
} catch (e) {
console.error('无效数据帧:', line);
}
});
function sendCommand(cmd) {
port.write(JSON.stringify(cmd) + '\n');
}这里有个实战细节值得注意:红外传感器偶尔会输出毛刺信号,直接用原始数据会导致机器人无理由转向。解决办法是在Node.js端加一个简单的消抖逻辑——连续三次读到同一个值才认为状态变化。这种软件层面的容错能显著提升行走稳定性,成本几乎为零。
弓字形路径规划与状态机实现
扫叶机器人要覆盖整个院子,最经典的是弓字形(S形)清扫路径:直线前进,碰到障碍物后退、原地转向90度、横移一个车身宽度,再反向直线行进。用状态机来描述这个流程非常清晰,DeLeaf定义了六个状态:FORWARD(前进)、BACKUP(后退)、TURN(转向)、SHIFT(横移)、CLEAN(清扫)、IDLE(待机)。
状态机用纯JavaScript实现,每个传感器事件驱动状态迁移:
const State = { IDLE: 'idle', FORWARD: 'forward', BACKUP: 'backup',
TURN: 'turn', SHIFT: 'shift' };
let state = State.IDLE;
let lastDirection = 1; // 1: 从左向右, -1: 从右向左
function onSensor(msg) {
const blocked = msg.front === 1 || msg.leaf === 0;
switch (state) {
case State.FORWARD:
if (msg.front === 1) { // 前方有障碍
sendCommand({ type: 'move', left: -120, right: -120, brush: 0 });
state = State.BACKUP;
} else { // 正常前进,检测到树叶时提高滚刷转速
const brushSpeed = msg.leaf === 1 ? 255 : 160;
sendCommand({ type: 'move', left: 150, right: 150, brush: brushSpeed });
}
break;
case State.BACKUP:
setTimeout(() => {
const turn = lastDirection === 1 ? { left: 120, right: -120 }
: { left: -120, right: 120 };
sendCommand({ type: 'move', ...turn, brush: 0 });
state = State.TURN;
}, 800); // 后退0.8秒
break;
case State.TURN:
setTimeout(() => {
sendCommand({ type: 'move', left: 150, right: 150, brush: 0 });
state = State.SHIFT;
}, 600); // 转90度约0.6秒,需实测校准
break;
case State.SHIFT:
setTimeout(() => {
lastDirection *= -1; // 掉头,下一轮反向清扫
const turn = lastDirection === 1 ? { left: -120, right: 120 }
: { left: 120, right: -120 };
sendCommand({ type: 'move', ...turn, brush: 0 });
state = State.TURN;
}, 1200); // 横移一个车身宽度
break;
}
}转向和横移的时间参数需要根据实际电机转速校准,这也是开源扫地机器人项目里公认最磨人的环节。更完善的做法是加编码器做里程反馈,把开环控制升级为闭环,DeLeaf后期版本就在主动轮上装了霍尔编码器,转向精度从误差15度降到了3度以内。
另一个实用技巧是清扫力度自适应。上面的代码中,滚刷电机的PWM值会根据地面树叶检测结果动态调整:有叶子时全速运转,没叶子时降速省电。仅这一项优化就让单次清扫续航从40分钟延长到了接近一小时。
用Express和WebSocket搭建远程监控面板
既然主控程序是Node.js,搭一个Web监控界面几乎是顺手的事。用express提供静态页面,用ws库建立WebSocket连接,把机器人状态实时推送到浏览器,手机连上同一个WiFi就能随时查看清扫进度、电量余量和当前状态。
服务端实现非常简洁:
const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const path = require('path');
const app = express();
app.use(express.static(path.join(__dirname, 'public')));
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
wss.on('connection', (ws) => {
console.log('监控客户端已连接');
// 立即推送一次当前状态
ws.send(JSON.stringify(robotState));
});
// 每500毫秒向所有客户端广播机器人状态
setInterval(() => {
const data = JSON.stringify(robotState);
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
}, 500);
server.listen(3000, () => {
console.log('监控面板已启动: http://192.168.0.1:3000');
});前端页面用几个色块展示传感器触发情况,用文字显示当前状态机阶段和电量百分比,再放一个暂停按钮,点击后通过WebSocket发消息回服务端,让机器人进入IDLE状态。整个页面加上CSS不到两百行代码,对于监控用途完全够用。
监控面板还有一个隐藏价值:日志留痕。把每次传感器事件和状态迁移都记录到本地文件,清扫结束后回放日志,就能分析出哪些区域反复触发了避障、路径规划是否需要调整。这种数据驱动的调试方式,比肉眼盯着机器人跑来跑去高效得多。
Node.js做实时控制的注意事项
项目做完之后有几点经验值得总结。第一,凡是硬实时需求(PWM波形、PID闭环)一律交给单片机,Node.js只做决策和调度,千万不要试图用setTimeout去模拟精确时序,事件循环的调度延迟在毫秒到几十毫秒之间波动,对电机控制来说太粗糙了。
第二,主程序中避免引入CPU密集型操作,比如图像识别、大量数据序列化。如果确实需要(例如后续想给机器人加摄像头识别落叶密度),正确做法是spawn一个Python子进程专门跑视觉算法,进程间通过标准输入输出通信,各自互不干扰。
第三,一定要处理串口断连。USB接口接触不良、下位机意外重启都会导致串口报错,如果没有重连机制,主程序会直接崩溃。建议监听port的error和close事件,断开后指数退避重试连接。这类健壮性细节在实验室里感知不到,真放到院子里跑上一下午就会暴露无遗。
总的来说,DeLeaf证明了JavaScript完全可以胜任中小型机器人的主控开发。对前端开发者而言,用熟悉的语言跨入硬件世界,再逐步深入到单片机编程和闭环控制,是一条平滑的学习路径。感兴趣的话,可以从GitHub上找类似的 Arduino 和 Node.js 结合的开源项目参考,动手把第一版跑起来,后面的迭代就会越来越顺。