关于Node-RED的任何项目、技术咨询都可联系 vx: _0xffffffffffff, qq: 924219829
前言
Node-RED是一个基于流的可视化编程工具,但目前在数据采集、智能家居自动化用的多一些。
但这个工具应该有更多的用处,基于流应该是一个很好的工业自动化工具,但确实很少可看到类似的应用。
博主尝试将Node-RED应用到现实场景中,并且项目也取得不错进展。
从个人视角来看,Node-RED完全可以胜任简单自动化场景,过于复杂的场景可以尝试,也许是个不错的选择。
演示视频:
Node-RED非标自动化尝试
移液称重模块总览
模块包含多个Node-RED自定义节点
其中包括
- 总线配置节点(RS485)
- 电机节点
- 夹爪节点
- 自定义Modbus节点
- 移液器节点
- IO模块节点
Node-RED自定义节点结构
Node-RED中自定义的节点包含三个文件
- 根目录的
package.json
文件指定模块所有的节点索引及配置 - 模块内部的
*.html
文件,描述了节点的配置项(颜色,大小,输入输出数目等) - 模块内部的
*.js
文件,描述了节点的实际功能,当消息到来时会回调input
事件(该事件应该在节点构造时注册回调函数)
下对各个节点一一说明
总线配置节点
该节点一共提供一个配置项:串口选择(例如:com3)
由于多台设备挂在一路总线上分站号访问,在软件层面映射为一个串口设备。
由于各个功能节点均需要访问该串口,而串口又是独占设备,因此,通过Event-BUS的技术,共享串口设备,提供多节点的访问。
此处,由于工程全部串行执行,不存在并发问题,未过多考虑并发互斥问题。
但引入了消息队列,虽然是未加锁的数据结构,但在很大程度上减少了并发问题,并且提供了数据的有序性。
// event-bus监听command事件,收到command事件后,将需要的指令放入队列
evBus.on('command', (callerId, buf) => {
// ...
que.push({
callerId, buf, ts: Date.now()
});
});
// 每隔100ms从队列中取值,顺序发出,
setInterval(()=>{
if(que.length <= 0) return ;
let head = que[0];
if(Date.now() - head.ts > 1000) {
// 超时没有返回,踢出队列
que.shift();
return;
}
port.write(head.buf, (err) => {
if(err) {
return node.error('Error on write: ' + err.message);
}
});
}, 100);
// 串口收到数据后,讲数据简单处理后返回调用者
parser.on('data', function (raw) {
// 取出队头(最先调用,但没有返回的请求)
let head = que.shift();
if(!head) return ;
// 数据太短,异常响应
if(raw.length < 3) return console.error();
// 返回调用者,并去除crc16校验
evBus.emit(head.callerId, null, raw.slice(0, raw.length-2));
});
所有需要用到总线的节点,比如电机节点,会把总线节点当做配置项引入。
当引入总线节点后,可以获取到总线的Event-Bus。
通过节点自身id进行划分,实现统一的总线使用。
let bus = RED.nodes.getNode(config.bus);
let evBus = bus.evBus;
// 使用总线发布数据时,带上自身节点id
node.evBus.emit('command', null, node.id, buf);
// 节点内部监听对应id的事件,收到串口端回应的时候进行回调
evBus.on(node.id, (data)=> {
// ...
});
电机模块节点
电机作为最重要的节点,对系统具有非常重要的影响。
每个电机可以看作是一个运动,绝对移动或相对移动等,主要是完成了对大肯RS485协议的封装,提供易用性。
电机提供了多种配置项
- Name 节点名称,用于备注节点,例如:皿盘出
- 总线 所使用的总线,见总线配置节点
- 站号 每个设备都有不同的站号,此处用于区分
- 功能 该节点需要执行的功能,对应电机的通讯协议
- 位置详情(当功能选择为绝对移动)
- 速度详情(当功能选择为设置速度)
- 相对位置详情(当功能选择为相对移动)
- 使能 该节点是否有效,可以通过消息动态改变,主要用于急停
- 输出原始报文 主要用于调试查看指令是否异常
当需要添加参数时,参数的来源可以有多种
- 固定参数 - 直接在配置界面配置完成
- 动态参数 - 从消息中取值
当电机移动时,有两种模式
- 同步移动 - 电机在未到达目标位置时,不产生任何消息,节点假阻塞等待
- 异步移动 - 电机给出目标位之后,直接返回,不等待其运动到指定位置
同步运动模式使用的较多。
具体实现:
function sendCmd(node, config, buf) {
node.errCount = 0;
if(config.func === 'D' && config.sync) {
// 第一次检查
checkStatus(node, config, 'd');
} // ...
node.evBus.emit('command', null, node.id, buf);
}
// 收到第一次检查状态的结果后,进行解析
// 如果未到达位置,延时再次检查
if(config.func === 'D' && config.sync) {
// 绝对值运动
let status = parseResp('d', data);
if(status === statusMapping['00']) {
// 运行中持续监测
node.status({fill:"yellow",shape:"dot",text:status});
// 当检查电机状态
checkStatus(node, config, recvFunc);
} // ...
}
// 200ms后发起状态检查, 此处的延时为了避免出现数据风暴
function checkStatus(node, config, ins) {
setTimeout(() => {
node.evBus.emit(
'command', null, node.id,
addChecksum(config.station, ins)
);
}, 200);
}
夹爪节点
夹爪节点底层使用Modbus协议,因此该节点主要是根据Modbus协议基于底层的总线节点进行封装。
大致框架与电机节点类似,不赘述。
实现上,将预定义的指令(或部分指令)当做select的value,之后对value进行解析,拼接出需要发送的modbus指令。
function genCmd(msg) {
// # 定义为站号,替换为16进制配置好的站号
let withSta = func.replace('#', station.toString(16).padStart(2, '0'));
// 获取定义的参数
let arg = parseInt( config.speed || config.torque || config.position || (config.enable ? 1 : 0) || msg.payload);
// ??被定义为参数, 替换为真实的参数
withSta = withSta.replace('??', arg.toString(16).padStart(2, '0'));
// 通过下划线分割 将string转换为Buffer
let buf = withSta.split('_').map((item)=> parseInt(item, 16));
// 添加crc16校验
buf = addCrc16(buf, false);
return buf;
}