readme
约 5018 字大约 17 分钟
2025-10-09
一、准备工作
- 联犀物联网平台账号(使用体验账号或者购买账号)
- 创建应用(在联犀控制台创建应用)
- 安装Node-RED(确保已安装并运行)
二、联犀平台配置
1. 创建产品
登录联犀物联网平台控制台
联犀物联网平台:https://demo.unitedrhino.com/
进入 设备管理 > 产品管理 → 点击【创建产品】
填写产品信息:
产品名称:
自定义
主协议:
联犀MQTT协议
产品ID:
系统自动生成或者自定义
产品描述:
自定义
设备物模型:
建议设备上报自动创建,因为兼容手动创建物模型和上报自动创建物模型
设备物模型4个选项解释
1. 手动管理
- 作用:手动定义设备的物模型(包括属性、服务和事件)。
- 适用场景:对设备物模型有特定需求或复杂配置的场景。
- 特点:灵活性最高,但需开发者深入理解物模型结构。
2. 设备自动创建
- 作用:系统根据预设模板自动为设备生成物模型。
- 适用场景:物模型结构简单且标准化的场景。
- 特点:配置快速简单,但灵活性较低(物模型由系统预设决定)。
3. 设备上报自动创建
- 作用:系统根据设备实际上报的数据动态创建物模型。
- 适用场景:设备类型多样、上报数据结构不固定的场景。
- 特点:能自动适应设备实际数据,可手动配置属性也可上报数据自动创建。
4. 设备上报自动创建(数字类型只使用浮点)
- 作用:基于“设备上报自动创建”逻辑,但强制将所有数字类型数据统一为浮点格式。
- 适用场景:对数据精度有严格要求的场景(如传感器数值统一处理)。
- 特点:在自动创建基础上规范数字类型,确保数据一致性。
点击【确定】 → 产品创建成功
2. 创建设备
进入 设备管理 > 新增
产品名称:绑定刚刚创建的产品
设备名称:
自定义
设备ID:
自定义
点击【确定】 → 设备创建成功
进入刚创建的设备的详情页
3.创建物模型
例如我们本次实验的物模型有以下参数
属性名称 | 属性标识 | 数据类型 | 读写权限 | 说明 |
---|---|---|---|---|
温度 | wendu | float | 只读 | 环境温度 |
湿度 | shidu | float | 只读 | 环境湿度 |
开关 | switch | int | 读写 | 设备开关状态 |
人体传感器 | renti | int | 只读 | 人体状态 |
进入物模型栏目,点击新增自定义功能
根据以上的操作和图表,依次添加物模型。
4. 生成MQTT三元组
进入 基础信息 > 设备连接参数
点击【MQTT三元组】→ 【生成三元组】
系统生成:
clientId(客户端ID):
自动生成
username(用户名):
自动生成
password(密码):
自动生成
保存三元组信息(用于Node-RED配置)
三、Node-RED 配置
MQTT服务通信
侧边栏拉入MQTT节点 → 双击配置连接:
- Broker:
demo.ithings.net.cn
(联犀MQTT地址) - Client ID:
三元组中生成的客户端ID
(与三元组一致) - Username:
三元组中生成的用户名
- Password:
三元组中生成的密码
- Port:
1883
- Broker:
填写完成后,点击更新,随后点击右上角部署,看MQTT节点是否通信成功。
查看到MQTT节点为已连接状态即可。
四、Node-RED 流程设计
1. 推送数据到平台(上报)
上报的结构
A[定时触发] --> B[生成随机数据]
B --> C[构建上报消息]
C --> D[MQTT节点]
D --> E[联犀平台]
当设备需要向云端上报设备运行状态的变化时,以通知应用端小程序、App 实时展示或云端业务系统接收设备上报属性数据,物联网开发平台为设备设定了默认的 Topic:
- 设备属性上行请求 Topic:
$thing/up/property/{ProductID}/{DeviceName}
首先模拟一下数据的更新
例如触发节点初始化触发一次,随后循环10秒钟触发一次。
// 生成 0-100 保留 2 位小数的随机数
msg.payload = parseFloat((Math.random() * 100).toFixed(2));
// 保存到上下文
flow.set("wendu", msg.payload);
// 正确返回 msg 对象
return msg;
在函数节点中复制以上代码。意思就是每被触发一次,就模拟出来0-100且带2位小数点的数值,保存到上下文,在当前流程中可以使用。
侧边栏中即可看到数据打印
这里也可以看到数据变动
根据以上的做法,模拟生成出来湿度,人体的状态变化
设计一个手动切换switch的开关状态
MQTT的推送主题格式:$thing/up/property/{product_id}/{device_id}(推送消息至云平台)
双击MQTT OUT节点,配置主题。
已知推送主题是 $thing/up/property/{product_id}/{device_id}
,所以我们就需要查找{product_id}和
回到联犀物联网云平台,可以看到设备ID和产品ID,直接复制粘贴即可
所以我的推送主题是 $thing/up/property/80/NR123
例如设定个15秒推送一次MQTT消息。拉入inject节点每15秒循环触发一次。
下面是MQTT推送的消息格式
{
"method": "report",
"msgToken": UUID,
"params": {
"wendu": 45.6,
"shidu": 67.2,
"switch": 1
}
}
所以拉入一个函数节点,手动组帧个JSON数据上报
// 生成符合UUID v4格式的唯一字符串(无需依赖外部库,兼容所有Node-RED环境)
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
.replace(/[xy]/g, function (c) {
// 生成0-15的随机整数
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// 生成唯一消息令牌(用于平台消息追踪)
const msgToken = generateUUID(); // 例如: "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
// 构建上报消息(联犀平台要求格式)
msg.payload = {
method: "report", // 固定值,表示这是设备上报数据
msgToken: msgToken, // 唯一消息标识(平台用于匹配请求/响应)
params: { // 实际上报的设备数据
wendu: flow.get("wendu") ?? 0, // 从flow上下文中获取温度值(默认0)
shidu: flow.get("shidu") ?? 0, // 从flow上下文中获取湿度值(默认0)
switch: flow.get("switch") ?? 0, // 从flow上下文中获取开关状态(默认0)
renti: flow.get("renti") ?? 0 // 从flow上下文中获取人体状态(默认0)
}
};
return msg; // 将处理后的消息返回给MQTT节点
点击部署后即可在联犀云平台看板中看到数值上报成功
2. 订阅平台指令(接收控制)
graph TD
A[联犀平台] --> B[MQTT节点]
B --> C[判断指令]
C --> D[执行开关操作]
节点配置:
MQTT节点(订阅主题):
- 使用物模型协议的设备,当需要通过云端控制设备时,设备需订阅下发 Topic 接收云端指令:
- 下发 Topic:
$thing/down/property/{ProductID}/{DeviceName}
- 响应 Topic:
$thing/up/property/{ProductID}/{DeviceName}
- 下发 Topic:
- 使用物模型协议的设备,当需要通过云端控制设备时,设备需订阅下发 Topic 接收云端指令:
拉入MQTT IN节点
双击MQTT IN节点,配置主题
$thing/down/property/{product_id}/{device_id}
可以先拉入个Debug节点,然后手动下发一下
在设备调试中,点击开关后设定数值,再点击发送。
点击发送后,即可在Node-RED中调试到下发的信息。
{ "method": "control", "msgToken": "系统生成的消息" "params": { "switch": 1 } }
当MQTT节点接收到平台下发的控制指令时,系统将首先验证消息的
method
字段是否为control
。若为控制指令,系统会遍历params
对象中的所有参数,将每个参数的值存储至流程变量中,以便后续执行相应的设备控制操作。Function节点(指令处理):
// 检查是否是控制指令 if (msg.payload && msg.payload.method === "control") { // 遍历 params 中的所有属性 for (const key in msg.payload.params) { if (msg.payload.params.hasOwnProperty(key)) { // 将键名作为 flow 变量名,值作为存储值 const value = msg.payload.params[key]; flow.set(key, value); msg.payload=(`已保存控制指令: ${key} = ${value}`); return msg; } } }
此代码的功能作用
使用
for...in
循环- 会遍历
params
对象中所有可枚举属性(包括多个键) - 例如:
params
有 3 个键 → 循环执行 3 次
- 会遍历
动态键名处理
key
变量会依次获取到switch
、wendu
、renti
等键名flow.set(key, value)
会动态创建变量
零配置扩展
- 无需修改代码,平台随时可增加新参数(如
light_level
) - 新参数会自动被
flow.set
保存
- 无需修改代码,平台随时可增加新参数(如
与物模型严格匹配
例如:平台下发
renti
→ 代码自动保存为flow.get("renti")
测试下发
3.响应平台指令(控制成功响应)
详细平台下发响应的格式可以看此文档。联犀下发消息及响应
使用物模型协议的设备,当需要通过云端控制设备时,设备需订阅下发 Topic 接收云端指令:
- 下发 Topic:
$thing/down/property/{ProductID}/{DeviceName}
- 响应 Topic:
$thing/up/property/{ProductID}/{DeviceName}
现在需要将函数节点输出2个端口,端口1输出平台下发的控制,端口2输出响应的指令
响应输出的格式
{ "method":"controlReply", "msgToken":"拷贝下发控制的msgToken", "code":200 }
以下是函数节点的代码
// 检查是否是控制指令 if (msg.payload && msg.payload.method === "control") { // 遍历 params 中的所有属性 for (const key in msg.payload.params) { if (msg.payload.params.hasOwnProperty(key)) { const value = msg.payload.params[key]; // 保存到flow flow.set(key, value); // 创建端口1的消息:日志字符串 const logMsg = { payload: `已保存控制指令: ${key} = ${value}` }; // 发送到端口1(第一个输出端口) node.send([logMsg,null]); } } // 构造控制回复消息(用于端口2) const replyMsg = { payload: { method: "controlReply", msgToken: msg.payload.msgToken, // 拷贝下发控制的msgToken code: 200 } }; // 发送到端口2(第二个输出端口),同时第一个端口发送null node.send([null, replyMsg]); }
将以上的代码复制粘贴到函数节点中,随后点击设置,将输出端口设置为2
随后拉入MQTT OUT复制推送主题的MQTT节点,跟函数节点的端口2连接即可。
4.测试下发控制
在设备调试中,点击开关后设定数值,再点击发送。
在Node-RED的调试窗口中即可看到函数的端口1输出下发控制的指令,端口2输出响应到云平台表示控制成功的指令
随后在上下文数据中,即可看到上下文变量的数值改变
在云平台的日志界面中,选择侧边栏的命令日志即可看到控制成功的提示。
最后会被打包成数据帧,循环上报至云平台。
这构成了一个完整的物联网数据闭环:Node-RED采集设备数据并上报至云平台,云平台解析数据后下发控制指令,设备执行控制后自动重新采集数据并再次上报,形成持续的双向数据交互。
- 下发 Topic:
五、关键消息格式说明
1. 上报消息(设备→平台)
{
"method": "report",
"msgToken": UUID,
"params": {
"wendu": 45.6,
"shidu": 67.2,
"switch": 1
}
}
2. 控制指令(平台→设备)
{
"method": "control",
"msgToken": "系统生成的消息"
"params": {
"switch": 1
}
}
3. 响应指令(设备→平台)
{
"method":"controlReply",
"msgToken":"拷贝下发控制的msgToken",
"code":200
}
响应失败的指令
{
"method":"controlReply",
"msgToken":"123",
"code":200,
"msg":"some message where error"
}
- 请求参数说明:
参数 | 类型 | 说明 |
---|---|---|
method | String | 表示设备向云端下发的控制指令的请求响应 |
msgToken | String | 用于上下行消息配对标识 |
code | Integer | 200表示设备成功接收到云端下发的控制指令 |
msg | String | 当 code 非200的时候,提示错误信息 |
六、测试步骤
- 启动Node-RED → 部署流程
- 查看联犀平台:
- 设备状态 → 实时查看温度/湿度数据
- 指令下发 → 在平台控制台发送开关指令
- 验证:
- 设备是否正确上报数据
- 是否能正确响应平台下发的指令
七、常见问题解决
问题 | 解决方案 |
---|---|
连接失败 | 检查MQTT三元组是否正确,确认产品ID/设备ID |
数据不显示 | 检查物模型属性标识是否与上报参数一致 |
指令不响应 | 确认订阅主题格式是否正确,检查Function节点逻辑 |
重要提示:
- 所有
{product_id}
和{device_id}
需替换为实际值- 上报消息中
params
的键名必须与物模型定义的属性标识符完全一致
配置项 | 查找处 |
---|---|
产品ID | 设备列表 > 设备详情 |
设备ID | 设备列表 > 设备详情 |
物模型 | 设备详情 > 物模型 |
MQTT三元组 | 设备详情 > MQTT三元组* |
八、Node-RED节点流
节点json
[
{
"id": "c33e0a85c146fb4b",
"type": "tab",
"label": "MQTT对接联犀物联网云平台",
"disabled": false,
"info": "",
"env": [
]
},
{
"id": "7904e7db21977f56",
"type": "group",
"z": "c33e0a85c146fb4b",
"style": {
"stroke": "#999999",
"stroke-opacity": "1",
"fill": "none",
"fill-opacity": "1",
"label": true,
"label-position": "nw",
"color": "#a4a4a4"
},
"nodes": [
"7c375555c38e4124",
"61257ddb39b5a4b9",
"d5944d4cf3756481",
"16599b7f47d3d2b6",
"947c09e2b74ae1c2",
"fdb9579583e2b744",
"cf762ea378db3b01",
"ad79ac8ce3a6a7c4",
"e3260a2481b8418f",
"75b16f0061873255",
"3a9ec27beb2cac9c",
"a75e9a90dca8c006",
"a11616162371e833"
],
"x": 54,
"y": 39,
"w": 1012,
"h": 462
},
{
"id": "fa4e05f19a1eef08",
"type": "group",
"z": "c33e0a85c146fb4b",
"style": {
"stroke": "#999999",
"stroke-opacity": "1",
"fill": "none",
"fill-opacity": "1",
"label": true,
"label-position": "nw",
"color": "#a4a4a4"
},
"nodes": [
"972ff6cefcf24e5f",
"18ace1e5fac849c5",
"340a4929c7276e71",
"fbe1415d5f83a249",
"de804f20af4207db"
],
"x": 54,
"y": 559,
"w": 872,
"h": 182
},
{
"id": "9ef38a1c3bdb9ec9",
"type": "group",
"z": "c33e0a85c146fb4b",
"style": {
"stroke": "#999999",
"stroke-opacity": "1",
"fill": "none",
"fill-opacity": "1",
"label": true,
"label-position": "nw",
"color": "#a4a4a4"
},
"nodes": [
"fc0d1a49babc8caf",
"44a694a13b8e0cbd",
"9a685c4bd15ae901",
"7a2272d563e9514c",
"8c41e9a28c833d0b",
"2fd46c71b1c1d9d6",
"5c43ae7a8bb184df"
],
"x": 54,
"y": 779,
"w": 752,
"h": 202
},
{
"id": "972ff6cefcf24e5f",
"type": "mqtt out",
"z": "c33e0a85c146fb4b",
"g": "fa4e05f19a1eef08",
"name": "MQTT推送",
"topic": "$thing/up/property/80/NR123",
"qos": "1",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "faf05a8513dab521",
"x": 810,
"y": 640,
"wires": [
]
},
{
"id": "7c375555c38e4124",
"type": "inject",
"z": "c33e0a85c146fb4b",
"g": "7904e7db21977f56",
"name": "10秒触发模拟一次温度数值",
"props": [
],
"repeat": "10",
"crontab": "",
"once": true,
"onceDelay": 0.1,
"topic": "",
"x": 220,
"y": 120,
"wires": [
[
"61257ddb39b5a4b9"
]
]
},
{
"id": "61257ddb39b5a4b9",
"type": "function",
"z": "c33e0a85c146fb4b",
"g": "7904e7db21977f56",
"name": "将模拟出来的温度数值保存至上下文存储",
"func": "// 生成 0-100 保留 2 位小数的随机数\nmsg.payload = parseFloat((Math.random() * 100).toFixed(2));\n\n// 保存到上下文\nflow.set(\"wendu\", msg.payload);\n\n// 正确返回 msg 对象\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [
],
"x": 580,
"y": 120,
"wires": [
[
"d5944d4cf3756481"
]
]
},
{
"id": "d5944d4cf3756481",
"type": "debug",
"z": "c33e0a85c146fb4b",
"g": "7904e7db21977f56",
"name": "debug 155",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 950,
"y": 120,
"wires": [
]
},
{
"id": "16599b7f47d3d2b6",
"type": "inject",
"z": "c33e0a85c146fb4b",
"g": "7904e7db21977f56",
"name": "10秒触发模拟一次湿度数值",
"props": [
],
"repeat": "10",
"crontab": "",
"once": true,
"onceDelay": 0.1,
"topic": "",
"x": 220,
"y": 200,
"wires": [
[
"947c09e2b74ae1c2"
]
]
},
{
"id": "947c09e2b74ae1c2",
"type": "function",
"z": "c33e0a85c146fb4b",
"g": "7904e7db21977f56",
"name": "将模拟出来的湿度数值保存至上下文存储",
"func": "// 生成 0-100 保留 2 位小数的随机数\nmsg.payload = parseFloat((Math.random() * 100).toFixed(2));\n\n// 保存到上下文\nflow.set(\"shidu\", msg.payload);\n\n// 正确返回 msg 对象\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [
],
"x": 580,
"y": 200,
"wires": [
[
"fdb9579583e2b744"
]
]
},
{
"id": "fdb9579583e2b744",
"type": "debug",
"z": "c33e0a85c146fb4b",
"g": "7904e7db21977f56",
"name": "debug 156",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 950,
"y": 200,
"wires": [
]
},
{
"id": "cf762ea378db3b01",
"type": "inject",
"z": "c33e0a85c146fb4b",
"g": "7904e7db21977f56",
"name": "10秒触发模拟一次人体数值",
"props": [
],
"repeat": "10",
"crontab": "",
"once": true,
"onceDelay": 0.1,
"topic": "",
"x": 220,
"y": 280,
"wires": [
[
"ad79ac8ce3a6a7c4"
]
]
},
{
"id": "ad79ac8ce3a6a7c4",
"type": "function",
"z": "c33e0a85c146fb4b",
"g": "7904e7db21977f56",
"name": "将模拟出来的人体数值保存至上下文存储",
"func": "// 生成 0-1的随机数\nmsg.payload = Math.floor(Math.random() * 2);\n\n// 保存到上下文\nflow.set(\"renti\", msg.payload);\n\n// 正确返回 msg 对象\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [
],
"x": 580,
"y": 280,
"wires": [
[
"e3260a2481b8418f"
]
]
},
{
"id": "e3260a2481b8418f",
"type": "debug",
"z": "c33e0a85c146fb4b",
"g": "7904e7db21977f56",
"name": "debug 157",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 950,
"y": 280,
"wires": [
]
},
{
"id": "75b16f0061873255",
"type": "inject",
"z": "c33e0a85c146fb4b",
"g": "7904e7db21977f56",
"name": "",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": 0.1,
"topic": "",
"payload": "0",
"payloadType": "num",
"x": 170,
"y": 380,
"wires": [
[
"a75e9a90dca8c006"
]
]
},
{
"id": "3a9ec27beb2cac9c",
"type": "inject",
"z": "c33e0a85c146fb4b",
"g": "7904e7db21977f56",
"name": "",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "1",
"payloadType": "num",
"x": 170,
"y": 460,
"wires": [
[
"a75e9a90dca8c006"
]
]
},
{
"id": "a75e9a90dca8c006",
"type": "change",
"z": "c33e0a85c146fb4b",
"g": "7904e7db21977f56",
"name": "手动切换开关状态",
"rules": [
{
"t": "set",
"p": "switch",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 430,
"y": 420,
"wires": [
[
]
]
},
{
"id": "18ace1e5fac849c5",
"type": "inject",
"z": "c33e0a85c146fb4b",
"g": "fa4e05f19a1eef08",
"name": "每15秒循环推送一次消息",
"props": [
],
"repeat": "15",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"x": 220,
"y": 640,
"wires": [
[
"340a4929c7276e71"
]
]
},
{
"id": "340a4929c7276e71",
"type": "function",
"z": "c33e0a85c146fb4b",
"g": "fa4e05f19a1eef08",
"name": "MQTT数据推送",
"func": "// 生成符合UUID v4格式的唯一字符串(无需依赖外部库,兼容所有Node-RED环境)\nfunction generateUUID() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'\n .replace(/[xy]/g, function (c) {\n // 生成0-15的随机整数\n const r = Math.random() * 16 | 0;\n const v = c === 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n// 生成唯一消息令牌(用于平台消息追踪)\nconst msgToken = generateUUID(); // 例如: \"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d\"\n\n// 构建上报消息(联犀平台要求格式)\nmsg.payload = {\n method: \"report\", // 固定值,表示这是设备上报数据\n msgToken: msgToken, // 唯一消息标识(平台用于匹配请求/响应)\n params: { // 实际上报的设备数据\n wendu: flow.get(\"wendu\") ?? 0, // 从flow上下文中获取温度值(默认0)\n shidu: flow.get(\"shidu\") ?? 0, // 从flow上下文中获取湿度值(默认0)\n switch: flow.get(\"switch\") ?? 0, // 从flow上下文中获取开关状态(默认0)\n renti: flow.get(\"renti\") ?? 0 // 从flow上下文中获取人体状态(默认0)\n }\n};\n\nreturn msg; // 将处理后的消息返回给MQTT节点",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [
],
"x": 520,
"y": 640,
"wires": [
[
"fbe1415d5f83a249",
"972ff6cefcf24e5f"
]
]
},
{
"id": "fbe1415d5f83a249",
"type": "debug",
"z": "c33e0a85c146fb4b",
"g": "fa4e05f19a1eef08",
"name": "debug 158",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 810,
"y": 700,
"wires": [
]
},
{
"id": "fc0d1a49babc8caf",
"type": "mqtt in",
"z": "c33e0a85c146fb4b",
"g": "9ef38a1c3bdb9ec9",
"name": "MQTT接收",
"topic": "$thing/down/property/80/NR123",
"qos": "1",
"datatype": "auto-detect",
"broker": "faf05a8513dab521",
"nl": false,
"rap": true,
"rh": 0,
"inputs": 0,
"x": 140,
"y": 880,
"wires": [
[
"9a685c4bd15ae901",
"8c41e9a28c833d0b"
]
]
},
{
"id": "44a694a13b8e0cbd",
"type": "debug",
"z": "c33e0a85c146fb4b",
"g": "9ef38a1c3bdb9ec9",
"name": "debug 159",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 690,
"y": 820,
"wires": [
]
},
{
"id": "9a685c4bd15ae901",
"type": "function",
"z": "c33e0a85c146fb4b",
"g": "9ef38a1c3bdb9ec9",
"name": "MQTT下发判断",
"func": "// 检查是否是控制指令\nif (msg.payload && msg.payload.method === \"control\") {\n // 遍历 params 中的所有属性\n for (const key in msg.payload.params) {\n if (msg.payload.params.hasOwnProperty(key)) {\n const value = msg.payload.params[key];\n // 保存到flow\n flow.set(key, value);\n // 创建端口1的消息:日志字符串\n const logMsg = {\n payload: `已保存控制指令: ${key} = ${value}`\n };\n // 发送到端口1(第一个输出端口)\n node.send([logMsg,null]);\n }\n }\n\n // 构造控制回复消息(用于端口2)\n const replyMsg = {\n payload: {\n method: \"controlReply\",\n msgToken: msg.payload.msgToken, // 拷贝下发控制的msgToken\n code: 200\n }\n };\n // 发送到端口2(第二个输出端口),同时第一个端口发送null\n node.send([null, replyMsg]);\n}",
"outputs": 2,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [
],
"x": 440,
"y": 880,
"wires": [
[
"44a694a13b8e0cbd"
],
[
"5c43ae7a8bb184df",
"2fd46c71b1c1d9d6"
]
]
},
{
"id": "a11616162371e833",
"type": "comment",
"z": "c33e0a85c146fb4b",
"g": "7904e7db21977f56",
"name": "模拟数据",
"info": "",
"x": 140,
"y": 80,
"wires": [
]
},
{
"id": "de804f20af4207db",
"type": "comment",
"z": "c33e0a85c146fb4b",
"g": "fa4e05f19a1eef08",
"name": "上报消息",
"info": "",
"x": 140,
"y": 600,
"wires": [
]
},
{
"id": "7a2272d563e9514c",
"type": "comment",
"z": "c33e0a85c146fb4b",
"g": "9ef38a1c3bdb9ec9",
"name": "下发消息",
"info": "",
"x": 140,
"y": 840,
"wires": [
]
},
{
"id": "8c41e9a28c833d0b",
"type": "debug",
"z": "c33e0a85c146fb4b",
"g": "9ef38a1c3bdb9ec9",
"name": "debug 160",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 370,
"y": 820,
"wires": [
]
},
{
"id": "5c43ae7a8bb184df",
"type": "debug",
"z": "c33e0a85c146fb4b",
"g": "9ef38a1c3bdb9ec9",
"name": "debug 161",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 690,
"y": 900,
"wires": [
]
},
{
"id": "2fd46c71b1c1d9d6",
"type": "mqtt out",
"z": "c33e0a85c146fb4b",
"g": "9ef38a1c3bdb9ec9",
"name": "MQTT响应",
"topic": "$thing/up/property/80/NR123",
"qos": "1",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "faf05a8513dab521",
"x": 690,
"y": 940,
"wires": [
]
},
{
"id": "faf05a8513dab521",
"type": "mqtt-broker",
"name": "",
"broker": "demo.ithings.net.cn",
"port": "1883",
"clientid": "80&NR123",
"autoConnect": true,
"usetls": false,
"protocolVersion": "4",
"keepalive": "60",
"cleansession": true,
"autoUnsubscribe": true,
"birthTopic": "",
"birthQos": "0",
"birthRetain": "false",
"birthPayload": "",
"birthMsg": {
},
"closeTopic": "",
"closeQos": "0",
"closeRetain": "false",
"closePayload": "",
"closeMsg": {
},
"willTopic": "",
"willQos": "0",
"willRetain": "false",
"willPayload": "",
"willMsg": {
},
"userProps": "",
"sessionExpiry": ""
}
]
说明:下载后,可在Node-RED中通过"Import" -> "JSON"功能导入配置。