design\project\Node-Red 读书笔记(IOT)(一)

1,006 阅读7分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Node-Red(一):编辑器的使用

一、是什么

设备采集类数据,建议用二级制, 如果是供多个上层业务应用再考虑json

node-red 是一个基于 NodeJs 的应用广泛的物联网接入平台

  • 支持基于浏览器的拖拽,动态生成事件驱动架构应用
  • 支持部署于边缘设备(树莓派 jess 已内置)和云端
  • 提供一种(面向数据流)可视化编程技术,减少代码量

2b7c76826a0689475ae32f87d2a43978910c3cad.png

🙄博主的很菜申明流程

二、能干什么?

node-red 广泛应用于

  • 健康管理
  • 农业
  • 智能家居
  • 工业自动化
  • 其他无限的可能

三、Node-Red Editor 的使用

1. 编辑器的框架

NodeRed 插件安装2.png

四、功能说明

1. 常用快捷键

  • 按住 ctrl (command)进入快速连接模式

  • 按住 ctrl (command)点击空白地区,快速添加节点(会优先展示最近使用的节点)

  • 一直按住 ctrl (command)一直快

  • 按住 ctrl(command)点击连线,快速加入中间节点

  • 按住 shift 修改连线。如果有多个连线则,批量移动端点

  • 按住 shift 移动到空白区域,删除连线

  • 双击节点,编辑节点属性

    • 给节点起一个好的名字
    • 每个节点可以有 markdown 格式的说明
    • 可以修改节点的 icon 图标
    • 可以给 inPort 和 outPort 添加 tooltip 注释
    • 可以使节点无效(界面上显示为虚线,临时禁用一般用于测试)

2. 部署

node-red 支持 4 种部署策略

  • 部署 --> 全面(默认选项)

    • runtime 运行时将停止所有节点,然后加载新配置重启
  • 部署-->已修改的流程

    • 未受修改影响的流程继续运行
  • 部署-->已更改的节点

    • 未修改的节点将继续运行
  • 部署-->重启

3. 导航

  • 右侧的工具栏可以导航全部流程

4. 上下文

上下文是一个在流程之外的存储信息的地方,上下文有作用域。RedNode 的上下文分为:

  • 全局上下文(作用范围:全局,所有流程,所有页签)

    • 所以一定要取一个好一点的名字
  • 节点上下文(作用范围:本节点)

  • 流程上下文(作用范围:相同的流程 或者相同的页签内的节点可以访问)

    • 含子流程的环境变量
  • 子流程上下文

    • 子流程的流程上下文的作于域只存在于子流程节点的子流程内
    • 子流程中能拿到父流程流程参数(使用 $parent.xxx)

如何确定上下文的作用域

  • 决定于需要在哪些节点使用这个数据

上下文中的变量默认情况下只保存在内存中。

  • 将上下文保存到文件时,上下文也会同步到内存中,加快读取速度

  • 上下文存储在 node-red 中做了插件化设计

  • 你可以选择性的持久化某些文件到磁盘,使用 Multi-Context-Storage

    • 在 settings.js 中新增配置 contextStorage:{store: { module: "localfilesystem"},default: { module: "memory" }}
    • flow.set("ceshi", "12345678", "store");
  • 使用 功能节点下的 change 节点可以快捷的设置上下文变量,右边可以选择存储策略(如果配置了文件存储的话)

  • 如果使用 context.flow() 等,来设置上下文变量,

  • myObject 加属性 myObject.property = xx 时,需要调用 context.flow.set('xx', 'xxx') 进行回写

  • 如果修改了 myObject 又不想修改流程变量,建议使用 RED.util.cloneMessage(myObject)

  • 你可以通过编辑器右侧的 context 侧边栏来方便的管理 上下文

使用上下文的时机

  • 上下文表示,你有一些和流程无关的变量
  • 如果你考虑到可扩展性,也就是会启动多个,实栗,做并行处理,考虑上下文同步问题

5. 子流程

子流程将一系列的节点并成一个聚合节点

  • 降低流程的复杂度
  • 为了复用
  • 子流程创建后,将出现在左边的工具箱,成为一个独立的实栗

如何创建子流程?

  • 选择要合并的流程
  • 选择右上角的主菜单 > 子流程 > 将选择部分更改为子流程
  • 左边工具箱将出现子流程分组和刚添加的子流程

如何编辑和配置子流程?

  • 双击子流程打开属性面板

  • 点击属性面板左上角“编辑流程模板”

  • 就可以在子流程做编辑了

    • 可以编辑子流程的输入、输出数量
    • 可以编辑子流程内的环境变量
    • 在流程中使用 `${变量名}`` 使用该环境变量

来自博主的补充:

  • node-red 导出的时候对子流程的处理

    • 没有像 activiti 一样使用嵌套,而是平铺
    • 这点给 node-red 点赞

6. 消息对象 msg

node-red 的工作方式就是,在节点之间传递 msg 对象

  • msg 是一个 JavaScript 对象,可以有任意个属性,支持任意类型
  • msg.payload 默认的数据主体(好像 payload 的toString() 方法能自动进行 mqtt 数据转码)

功能节点下的 change 节点支持 JSONata

  • 栗如:(payload.temperature-32)*5/9

msg 序列

  • 可以将 csv 等多个数据通过 split 节点,将一条消息拆分为多条
  • 这是使用 debugger 节点,将有多次输出
  • 这时候使用函数节点操作一条数据,将会修改 msg 序列中的所有数据
  • 可以通过 join 节点,将多条消息重新合并为一条消息
  • 栗如:[1, 2, 3, 4] 调用 split 节点 将生成 msg 序列:4,3,2,1
  • 栗如:“123 换行 456 换行 789”调用 split 节点,将生成 msg 序列:“789”“456”“123”
  • 栗如:{"a":1,"b":2,"c":3} 调用 split 节点,将生成 msg 序列:“c 3”“b 2”“a 1”

每个 msg 包含一个 msg parts 属性

  • msg.parts.id:消息序列唯一ID

  • msg.parts.index:msg 在消息序列中的位置

  • msg.parts.count:消息序列的长度(如果能提前获取)

  • 其他节点特有的元数据(metadata)

    • split 拆分节点特有的:join 规则

7. 路由节点

如果不使用路由节点,出口连接多个分支,则每个分支都是同样的数据

功能节点下的路由节点(switch)可以根据属性做选择分支

  • 每新增一个 condition 表达式,节点就会在相应的位置新增一个出口
  • condition 表达式中,可以使用流程上下文做为参数,实现参数化流程
  • condition 表达式同样支持 JSONata
  • 你还可以使用 函数节点做动态分发 node.send()

8. 延迟节点,触发器节点

如果想要限制消息发送频率,你可以使用延迟节点(delay)或者触发器节点(trigger)

  • 延迟节点

    • 每个流经的消息都将延迟等待指定时间

    • 支持等待随机时间

    • 支持等待 msg.delay 属性的值

    • 延迟节点可以选择:限制消息速率模式

      • 可以配置将多余的消息压入队列
      • 丢弃多余的消息
      • 和将多余的消息拼接到下一条消息
  • 触发器节点(好像是重试机制)

    • 处理完本条消息后,等待一定时间,然后重发

      • 勾选:如有新信息,延长延迟,可以用来做断线检测
      • 栗如:mqtt 的遗言
    • 典型应用,双 trigger 节点组成

      • 在正常业务下并行的创建两个 trigger 节点
      • 第一个 节点做成断线检测
      • 第二个节点会每 5 秒重发指定内容
      • 第一个节点在重新收到消息后,会发送一个 reset 消息
      • 第二个 trigger 节点在接收到 reset 消息后停止尝试重发
    • 处理完本条消息后,一定时间内,如果没有接收到任何消息,发送一条消息

9. 节点合并

  • 使用 join 节点合并消息

    • 可以使用 key/value 模式合并

      • 栗如:合并两个传感器数据:
      • 传感器1 topic = internal,payload = ["23.94", "24.24","20.00"]
      • 传感器2 topic = external,payload =["22.68", "24.24", "20.00"]
      • 用 topic 做为key,当 message 部分到达量为 2 合并为
      • {internal: "23.94", external: "22.68"}
    • 直接合并

    • 合并成一个对象

  • 使用 函数节点合并消息

    • 当 join 节点不符合要求的时候,可以通过函数节点来处理
// 设置 context 的值
context.set(msg.topic, msg.payload);


// 获得最新的 context 的值
let internal = context.get("internal");
let external = context.get("external");

// 检查
if (internal !== undefined && external !== undefined) {
    return {
        payload: {
            internal: internal,
            external: external
        }
    }
}
return;

10. 节点选择组

节点可以聚合起来成为一个选择组,一个选择组可以在编辑器中同时移动,拷贝,删除

  • 节点选择组提供了一种组织节点的方式
  • 节点选择组提供更好的展现方式(添加文字提示)

如何添加选择组

  • 创建组:选择多个节点,选择右上角菜单,组,选择组
  • 加入组:将节点拖拽到选择组框内,就能将节点加入该选择组
  • 移出组:选择一个节点,选择右上角菜单,组,从组中移除
  • 双击选择组,可以编辑选择组的外观、名称
  • ctrl + shift + c 快速复制一个分组的样式
  • ctrl + shift + v 快速粘贴一个分组的样式

11. JSONata 语法

  • JSONata 能做什么?
Address.City

FirstName & ' ' & Surname

Phone[type = 'mobile'].number

$sum(Order.Product.(Price * Quantity))

(Numbers[2] != 0) and (Numbers[5] != Numbers[1])
  • 属性拼接
firstName & ' ' & lastName
  • 属性获取
xx: { Phone: [ { type: 'xxx', number: 'xxxxxx' }, { type: 'xxxx', number: 'yyyyyy' }, { type: 'xxxxx', number: 'zzzzzzz' } ] }

/** 属性获取 */ Phone.number

/** 返回 */ [ "xxxxxx", "yyyyyy", "zzzzzzz" ]

/** 属性获取 */ Phone[0].number

/** 返回 */ "xxxxxx"

/** 属性获取 */ Phone[type='xxxx'].number

/** 返回 */ "yyyyyy"
  • 属性计算(查询 + 抽取数据)
/** 属性获取 */
Account.Order.Product.(Price * Quantity)

/** 返回 */
[
    68.9,
    21.67,
    137.8,
    107.99
]


/** 属性获取 */
$sum(Account.Order.Product.(Price * Quantity))

/** 返回 */
336.36
  • 内置函数

    • String 类

      • $string(5) => "5"
      • $length("Hello World") => 11
      • $substring("Hello World", -4) => "orld"
      • $substringBefore("Hello World", " ") => "Hello"
      • $substringAfter("Hello World", " ") => "World"
      • $uppercase("Hello World") => "HELLO WORLD"
      • $lowercase("Hello World") => "hello world"
      • $trim(" Hello \n World ") => "Hello World"
      • $pad("foo", 5) => "foo "
      • $pad("foo", -5, "#") => "##foo"
      • $contains("abracadabra", "bra") => true
      • $split("so many words", " ") => [ "so", "many", "words" ]
      • $join(['a','b','c']) => "abc"
      • $match("ababbabbcc",/a(b+)/) => ?
      • $replace("John Smith and John Jones", "John", "Mr") => "Mr Smith and Mr Jones"
      • $eval("[1,2,3]") => [1, 2, 3]
      • $base64encode("myuser:mypass") => "bXl1c2VyOm15cGFzcw=="
      • $base64decode("bXl1c2VyOm15cGFzcw==") => "myuser:mypass"
      • $encodeUrlComponent("?x=test") => "%3Fx%3Dtest"
      • $encodeUrl("https://mozilla.org/?x=шеллы") => "https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B"
      • $decodeUrlComponent("%3Fx%3Dtest") => "?x=test"
      • $decodeUrl("https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B") =>
    • Number 类

      • $number("5") => 5
      • ["1", "2", "3", "4", "5"].$number() => [1, 2, 3, 4, 5]
      • $abs(-5) => 5
      • $floor(5.8) => 5
      • $ceil(5.8) => 6
      • $round(123.456, -1) => 120
      • $power(2, 0.5) => 1.414213562373
      • $sqrt(2) => 1.414213562373
      • $random() => 0.4029142127028
      • $formatNumber(12345.6, '#,###.00') => "12,345.60"
      • $formatBase(100, 2) => "1100100"
      • $formatInteger(1999, 'I') => "MCMXCIX"
      • $parseInteger('12,345,678', '#,##0') => 12345678
    • 聚合类

      • $sum([5,1,3,7,4]) => 20
      • $max([5,1,3,7,4]) => 7
      • $min([5,1,3,7,4]) => 1
      • $average([5,1,3,7,4]) => 4
    • 布尔类

      • $boolean(arg)
      • $not()
      • $exists(arg)
    • 数组类

      • $count([1,2,3,1]) => 4
      • $append([1,2,3], [4,5,6]) => [1,2,3,4,5,6]
      $sort(Account.Order.Product, function($l, $r) { $l.Description.Weight > $r.Description.Weight })
      
      • [1..5] ~> $reverse() => [5, 4, 3, 2, 1]
      • $shuffle([1..9]) => [6, 8, 2, 3, 9, 5, 1, 4, 7]
      • $distinct([1,2,3,3,4,3,5]) => [1, 2, 3, 4, 5]
      • $zip([1,2,3],[4,5],[7,8,9]) => [[1,4,7], [2,5,8]]
    • 对象类

      • $keys(object)
      • $lookup(object, key)
      • $spread(object)
      • $merge(array<object>)
      • $sift(object, function)
      • $each(object, function)
      • $error(message)
      • $assert(condition, message)
      • $type(value)
    • 时间日期类

      • $now() => "2017-05-15T15:12:59.152Z"
      • $millis() => 1502700297574
      • $fromMillis(1510067557121) => "2017-11-07T15:12:37.121Z"
      • $toMillis("2017-11-07T15:07:54.972Z") => 1510067274972
    • ???

      • $map([1..5], $string) => ["1", "2", "3", "4", "5"]
      • $filter(array, function)
      • $single(array, function)
      • $reduce(array, function [, init])
      • $sift(object, function)
  • 你可以自己扩展其内置的函数