Node-RED 编程:入门指南

Node-RED编程基础

【Node-RED与IoT开发交流】785381620 ,欢迎加入!

前言

Node-RED是一款低代码编程的平台, 可以通过可视化编程的方式实现某些特定功能. 但对于许多初次接触该应用的用户来说, 使用Node-RED编程仍存在一些障碍, 个人认为主要是在以下方面:

  1. 消息模型msg
  2. 上下文context
  3. 函数节点function.

故在此将以上三点进行详细的说明, 希望对各位有所帮助.

在学习使用任何软件/平台时, 官方文档永远是第一选择, 你遇到的几乎所有的问题都可以在官方找到答案, 此外, 对于一些节点, 你可很方便的从info窗口看到最基础的指引.

image-20210712184750527

消息模型msg

在Node-RED中, 我们通过连线将不同功能的节点连接起来.

有些节点只能接入线, 如debug节点. 这类节点只可以接收其它节点提供的数据, 而不能直接产生任何数据.只可以作为输出使用, 类比扬声器.

image-20210712185019380

有些节点只可输出线, 如inject节点. 这类节点, 只可以产生数据, 而不能接收任何数据, 只能做输入使用, 类比麦克风.

image-20210712185055140

有些节点即可输出线, 也可以输入线, 如function节点. 此类节点既可以接收数据, 也可以输出数据, 它一般对输入数据进行处理, 处理完成后, 再从输出端口输出.

image-20210712185229159

当我们用线将三个节点进行连接后, 这便成为了一个流.

image-20210712185756500

每一个流通过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节点在接收到该数据后, 将其在调试窗口输出.

image-20210712190802456

以上是最为基础的一个流, 主要是想要说明一点: 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;

这样, 时间戳作为输入, 经过以上的处理后就得到对应的时分秒了.

image-20210712191924502

但有些时候, 你不是对传进来的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节点修改配置.

image-20210712200222044

这样就得到我们想要结果了.

image-20210712200315084

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;

执行结果如下:

image-20210712203702701

在执行5次过后, 即使再次使用inject节点触发, 也不输出任何消息.

Flow

与Node类似, Flow存储整个流的状态, 这里所指的流并不是几个节点串成一条的流, 而是一个面板算作一个流, 例如, 以下5个节点串成两条线, 但处一个Tab中, 那么这5个节点共享同一个Flow的Context.

image-20210712204527219

Flow的应用场景很多, 例如, 我们通过MQTT接收到温湿度传感器传来的数据, 同时, 如果外界通过HTTP协议访问温湿度数据时, 我们就可以通过如下Flow去实现.

image-20210712205536343

MQTT收到数据后, 将会把数据放入Flow中, 收到HTTP请求后, 会去Flow中查询响应的数据并做返回.

image-20210712205725324

完整的流如下, 可以自行导入查看:

[{"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节点等节点中使用.

image-20210713085932383

image-20210713090008717

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获取

image-20210713091620256

返回的数组按序依次从对应的端口输出.例如:

if (msg.topic === "banana") {
   return [ null, msg ];
} else {
   return [ msg, null ];
}

如果msg.topicbanana 则从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 ;

执行结果:

image-20210713104613350

在异步任务中, 还有一个较为重要的概念则是回调, 回调即是返回调用, 别人干完事了回来通知. 但别人通知总需要一个入口, 这个入口称之为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:"完成"});

就会出现如下效果:

image-20210713112239333

具体的API可以参考官方文档

使用外部模块

[注] 需要Node-RED版本>=1.3.0

当我们遇到一些需求需要使用别人造好的轮子时, 我们就不得不使用外部模块. 函数节点也提供了该功能. 首先需要去.node-red文件夹下找到settings.js文件, 如果不知道该文件在哪里, 可以从启动日志找到, 如下:

image-20210713121322460

在文件中添加functionExternalModules: true, 如图:

image-20210713121400535

此时启动Node-RED, 双击函数节点, 进入Setup就可以看到如下画面:

image-20210713121530899

你可以通过左下角添加按钮, 添加需要使用的模块, 第三方模块就会被自动安装到.node-red/externalModules/中. 以一个小例子结束本部分内容: 有时我们需要获取网卡信息, 很多人都通过安装第三方节点实现, 但其实, 并不需要第三方节点就可以实现.

我们引入了os模块, os模块提供了networkInterfacesAPI, 我们可以通过该模块获取网卡信息.

en0 = os.networkInterfaces().en0
ipv4 = en0[1] // en0[0]为ipv6, 需要根据自身环境修改
ip = ipv4.address
mac = ipv4.mac
msg.payload = {
    ip, mac
}
return msg;

执行结果如下:

image-20210713122159793

有了这个feature后, Node-RED的功能得到了极大的增强, 举个例子, 我们可以通过引入johnny-five, 通过函数节点直接对硬件进行编程. 等等.

如果需要全局引入某个模块可以通过修改functionGlobalContext实现.

例如, 我们同样在settings.js中引入os模块, 重启Node-RED, 我们就不需要再函数节点的setup中引入os, 仅需要在函数中通过os = global.get('os')引入即可.

事件记录

在函数节点中可以使用node.log/node.warn / node.error 记录某些事件, 方便调试.

期待与您成为朋友

物联沃分享整理
物联沃-IOTWORD物联网 » Node-RED 编程:入门指南

发表评论