Node-RED编程基础
【Node-RED与IoT开发交流】785381620 ,欢迎加入!
前言
Node-RED是一款低代码编程的平台, 可以通过可视化编程的方式实现某些特定功能. 但对于许多初次接触该应用的用户来说, 使用Node-RED编程仍存在一些障碍, 个人认为主要是在以下方面:
- 消息模型msg
- 上下文context
- 函数节点function.
故在此将以上三点进行详细的说明, 希望对各位有所帮助.
在学习使用任何软件/平台时, 官方文档永远是第一选择, 你遇到的几乎所有的问题都可以在官方找到答案, 此外, 对于一些节点, 你可很方便的从info窗口看到最基础的指引.
消息模型msg
在Node-RED中, 我们通过连线将不同功能的节点连接起来.
有些节点只能接入线, 如debug节点. 这类节点只可以接收其它节点提供的数据, 而不能直接产生任何数据.只可以作为输出使用, 类比扬声器.
有些节点只可输出线, 如inject节点. 这类节点, 只可以产生数据, 而不能接收任何数据, 只能做输入使用, 类比麦克风.
有些节点即可输出线, 也可以输入线, 如function节点. 此类节点既可以接收数据, 也可以输出数据, 它一般对输入数据进行处理, 处理完成后, 再从输出端口输出.
当我们用线将三个节点进行连接后, 这便成为了一个流.
每一个流通过msg
对象进行传递消息, 对象可以理解为一包数据, 其结构一般如下:
{"_msgid":"e701ad8b.c7bb1","payload":1626087545399,"topic":""}
其中_msgid
指明了消息的ID, payload
指明了消息主体, topic
指明了消息主题.
在这个流中, 我们使用inject节点产生一个时间戳1626087545399
并将其放到payload的位置(msg.payload = 1626087545399
). 此时消息已经变为
{"_msgid":"e701ad8b.c7bb1","payload":1626087545399,"topic":""}
该消息作为输入, 输入函数节点. 函数
节点接收到该数据后, 进行处理. 函数
的具体逻辑由我们指定, 在上述流中, 函数
节点代码如下:
return msg;
函数
节点中使用JavaScript进行编程, 在该节点中, 直接将msg
返回, 不做任何修改. 此时消息仍为
{"_msgid":"e701ad8b.c7bb1","payload":1626087545399,"topic":""}
该消息作为函数
节点的处理结果, 输入debug
节点. debug节点在接收到该数据后, 将其在调试窗口输出.
以上是最为基础的一个流, 主要是想要说明一点: msg
对象为消息传递的载体. 如果想要对数据进行处理, 操作这个msg
就可以了.
例如, 我们想要改变上述流程, 通过时间戳获取当前的时间. 我们只需要对函数
节点进行修改即可.
例如, 我们将函数
节点的内容修改如下:
let date = new Date(msg.payload) // 根据时间戳生成Date对象
let hh = date.getHours() // 通过date对象获取时分秒
let mm = date.getMinutes()
let ss = date.getSeconds()
msg.payload = hh + ':' + mm + ':' + ss // 拼接时分秒
return msg;
这样, 时间戳作为输入, 经过以上的处理后就得到对应的时分秒了.
但有些时候, 你不是对传进来的msg
对象进行处理, 或者传入的msg
不符合你的需要, 那需要对它进行修改.
你可以创造一个新的对象将它返回, 它叫什么名字无所谓.
a = {
a: ' ',
b: ' ',
c: ' '
}
return a;
也可以对原有的msg对象修修补补.
msg.a = 'a' // msg['a'] = 'a' 两种写法的作用是相同的
return msg;
那么以一个案例结束这一部分吧. 我们做过将时间戳转化为dd:mm:ss的格式, 但如果我们也需要返回小时, 分钟, 秒以供后面的节点调用呢. 可以在脑子中简单的过一下想一下答案. 答案很简单, 在msg
对象上增添3个键值对就可以了.
let date = new Date(msg.payload); // 根据时间戳生成Date对象
let hh = date.getHours() // 通过date对象获取时分秒
let mm = date.getMinutes()
let ss = date.getSeconds()
msg.payload = hh + ':' + mm + ':' + ss // 拼接时分秒
msg.hh = hh
msg.mm = mm
msg.ss = ss
return msg;
触发后, 你会发现, 我们后面添加的hh,mm,ss并没有输出, 这是为什么呢. 这是因为debug
节点默认输出msg
对象中的payload
键值对, 其余的并没有显示. 可以通过双击debug
节点修改配置.
这样就得到我们想要结果了.
Node-RED的消息模型, 大概就是这样了, 希望你有所收获吧, 如果有问题也欢迎在下方提出你的疑问.
上下文Context
对于学习过计算机编程的同学来说, “上下文” 应该是个非常非常常见的术语, 通常用来存储当前操作所处的状态. 在Node-RED也如此, 并且, Node-RED还将上下文分为了3种作用域, Node/Flow/Global.
Node的作用域是在当前节点, Flow是当前流, Global则是全局. 下面展开介绍
Node
你也许会问, 在一个节点内部, 使用Context意义在哪? 直接使用变量不就好了.
我们设想一个场景, 我们需要记录某个函数
节点的执行次数, 当达到一定次数后就不再输出. 如果用最基础的变量, 将永远不会结束, 因为每次通过函数
节点后, 这个节点的所有变量都会被清空, 再次执行使仍是最初始的状态, 可以理解为这个节点是无状态的. 这时Context的作用就体现出来了, 我们需要Context去记录这个节点的状态.
具体实现如下:
// 若Context中有count则使用count, 无count使用0
let count = context.get('count')||0;
if(count > 5)
return null
count += 1;
context.set('count',count); // 将count放入Context
msg.count = count;
return msg;
执行结果如下:
在执行5次过后, 即使再次使用inject节点触发, 也不输出任何消息.
Flow
与Node类似, Flow存储整个流的状态, 这里所指的流并不是几个节点串成一条的流, 而是一个面板算作一个流, 例如, 以下5个节点串成两条线, 但处一个Tab中, 那么这5个节点共享同一个Flow的Context.
Flow的应用场景很多, 例如, 我们通过MQTT接收到温湿度传感器传来的数据, 同时, 如果外界通过HTTP协议访问温湿度数据时, 我们就可以通过如下Flow去实现.
MQTT收到数据后, 将会把数据放入Flow中, 收到HTTP请求后, 会去Flow中查询响应的数据并做返回.
完整的流如下, 可以自行导入查看:
[{"id":"5ae2bdf8.c1b644","type":"mqtt in","z":"77db09d1.403ba8","name":"","topic":"sensor","qos":"2","datatype":"auto","broker":"c97be055.a659d","nl":false,"rap":true,"rh":0,"x":150,"y":4660,"wires":[["16b09f20.3b9281"]]},{"id":"c1aec982.63b988","type":"function","z":"77db09d1.403ba8","name":"","func":"flow.set('humi', msg.payload.humi)\nflow.set('temp', msg.payload.temp)\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":490,"y":4660,"wires":[[]]},{"id":"46e11606.fcc408","type":"http in","z":"77db09d1.403ba8","name":"","url":"/sensor","method":"get","upload":false,"swaggerDoc":"","x":170,"y":4720,"wires":[["66aa5722.6b53f8"]]},{"id":"66aa5722.6b53f8","type":"function","z":"77db09d1.403ba8","name":"","func":"let humi = flow.get('humi') || 0\nlet temp = flow.get('temp') || 0\nmsg.payload = {\n humi, temp\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":330,"y":4720,"wires":[["c24d1af.43c51e8"]]},{"id":"c24d1af.43c51e8","type":"http response","z":"77db09d1.403ba8","name":"","statusCode":"","headers":{},"x":490,"y":4720,"wires":[]},{"id":"16b09f20.3b9281","type":"json","z":"77db09d1.403ba8","name":"","property":"payload","action":"","pretty":false,"x":330,"y":4660,"wires":[["c1aec982.63b988"]]},{"id":"c97be055.a659d","type":"mqtt-broker","name":"","broker":"www.carwasher.com.cn","port":"1883","clientid":"","usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"sessionExpiry":""}]
Global
与前两种类似, Global是解决跨流数据共享的. 在函数
节点中主要是以下两个API
let bar = global.get('foo') || 0 //有则获取, 无则为0
global.set('foo', 'bar') // 将foo写入值'bar'
除了在函数
节点中通过代码的方式使用Context, 也可以在inject
节点, change
节点等节点中使用.
Node-RED的上下文Context, 为我们的编程提供了很大的便利, 它是实现复杂应用必不可少的一部分, 就如同其它编程语言中的局部变量/全局变量, 少了这个特性, 许许多多的功能几乎实现不了.
函数节点function
在Node-RED中, 很少使用代码, 所以要写代码的地方就成了比较困难的一部分. 函数节点使用JavaScript编程, 支持大部分API, 上面有穿插的使用, 但并不系统, 下面就对这个节点进行一个较为系统的梳理. 官方文档中也有很细致的说明, 喜欢看官方文档的可以直接参考.
消息是通过msg对象进行传递的, 通常会有一个payload的键值对包含消息的主体, 其他节点会msg对象上添加新的键值对.
上文提到, 直接将msg返回, 不做任何处理. 同时这个对象并无强制命名.
return msg;
// 以下等价.
// let message = msg;
// return message;
如果返回null
, 则停止传递.
return null;
// 以下等价.
// return ;
并且, 函数节点必须返回一个对象或者null, 如果返回字符串/数字等, 会产生一个错误.
多端口输出
我们可以看到, 函数节点是可以设置多输出的, 此时返回的对象需要是一个与输出数相同长度的数组. 输出端口数也可以通过node.outputCount
获取
返回的数组按序依次从对应的端口输出.例如:
if (msg.topic === "banana") {
return [ null, msg ];
} else {
return [ msg, null ];
}
如果msg.topic
为banana
则从2号口输出, 非banana
则从1号口输出
异步发送消息
通常我们使用节点发送消息都是同步的, 何为同步呢? 这是一个比较直观的概念, 单向一车道堵车了, 你只能同步的等待, 前面不走我们就没办法走, 这也许损耗了极大的性能. 而现在应用更广的是异步模式, 我们可以不用等待处理结果, 处理结束了通知我即可, 就像你安排别人做个事情, 安排完了, 就去忙别的事了, 他做好事情再来通知你. 例如:
setTimeout(()=>{
msg.payload = 'notify'
node.send(msg)
node.done()
}, 1000)
return;
我们使用setTimeout
出启动一个延时任务, 消息不直接返回, 在1000ms后通过node.send
返回.
同时, 我们还经常遇到一个非常常见的需求, 我们需要将一个数组的内容配合MySQL节点插入到数据库中, 我们就可以使用node.send
来实现这个功能.
data = [25.0, 25.3, 25.1, 25.4, 25.6]
data.forEach((item)=>{
sql = `insert into sensors (temp) values (${item}) `
msg.topic = sql
node.send(msg)
})
return ;
执行结果:
在异步任务中, 还有一个较为重要的概念则是回调, 回调即是返回调用, 别人干完事了回来通知. 但别人通知总需要一个入口, 这个入口称之为event
, 对event
的处理成为event handler
或者callback
, 此处并不严谨, 仅供理解. 例如, 我们在使用Context存储数据的时候, 如果其存储在文件系统上, IO操作要慢许多, 如果使用同步的方式获取, 会极大地影响性能, 此时我们就会通过异步的方式获取. 实现如下:
flow.get(['humi', 'temp'], (err, humi, temp) => { // 获取多个context
if(err) return
node.send({
payload: {
humi: humi || 0,
temp: temp || 0
}
})
node.done()
})
return ;
其中的arrow function就是一个回调函数.
节点状态
Node-RED也提供了状态显示的API, 例如调用
node.status({fill:"green",shape:"dot",text:"完成"});
就会出现如下效果:
具体的API可以参考官方文档
使用外部模块
[注] 需要Node-RED版本>=1.3.0
当我们遇到一些需求需要使用别人造好的轮子时, 我们就不得不使用外部模块. 函数节点也提供了该功能. 首先需要去.node-red
文件夹下找到settings.js
文件, 如果不知道该文件在哪里, 可以从启动日志找到, 如下:
在文件中添加functionExternalModules: true,
如图:
此时启动Node-RED, 双击函数节点, 进入Setup就可以看到如下画面:
你可以通过左下角添加
按钮, 添加需要使用的模块, 第三方模块就会被自动安装到.node-red/externalModules/
中. 以一个小例子结束本部分内容: 有时我们需要获取网卡信息, 很多人都通过安装第三方节点实现, 但其实, 并不需要第三方节点就可以实现.
我们引入了os模块, os模块提供了networkInterfaces
API, 我们可以通过该模块获取网卡信息.
en0 = os.networkInterfaces().en0
ipv4 = en0[1] // en0[0]为ipv6, 需要根据自身环境修改
ip = ipv4.address
mac = ipv4.mac
msg.payload = {
ip, mac
}
return msg;
执行结果如下:
有了这个feature后, Node-RED的功能得到了极大的增强, 举个例子, 我们可以通过引入johnny-five
, 通过函数节点直接对硬件进行编程. 等等.
如果需要全局引入某个模块可以通过修改functionGlobalContext
实现.
例如, 我们同样在settings.js
中引入os
模块, 重启Node-RED, 我们就不需要再函数节点的setup中引入os, 仅需要在函数中通过os = global.get('os')
引入即可.
事件记录
在函数节点中可以使用node.log
/node.warn
/ node.error
记录某些事件, 方便调试.文章来源:https://www.toymoban.com/news/detail-409035.html
期待与您成为朋友文章来源地址https://www.toymoban.com/news/detail-409035.html
到了这里,关于Node-RED编程基础的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!