IoT 场景示例
# IoT 场景示例
IoT 组件与 RuleGo 标准组件/端点的典型组合,展示完整数据链路。
# 场景一:定时采集 → 告警 → 通知
温度超阈值时发邮件告警。
endpoint/schedule(每30秒) → x/iotRead(S7) → jsFilter(温度>80?) → sendEmail(告警)
→ x/tsdbWrite(正常也落盘)
2
{
"ruleChain": {"name": "temp-alarm", "root": true},
"metadata": {
"nodes": [
{"id": "sch", "type": "endpoint/schedule", "configuration": {"interval": "@every 30s"}},
{"id": "read", "type": "x/iotRead", "configuration": {
"driver": "s7", "server": "192.168.1.10:102",
"points": [
{"name": "炉温", "addr": "DB1.DBD0", "type": "FLOAT32"},
{"name": "压力", "addr": "DB1.DBD4", "type": "FLOAT32"}
]
}},
{"id": "filter", "type": "jsFilter", "configuration": {
"jsScript": "var data = JSON.parse(msg.data || '[]'); return data.some(function(d){return d.name==='炉温' && d.value > 80;});"
}},
{"id": "email", "type": "sendEmail", "configuration": {
"smtpHost": "smtp.example.com", "smtpPort": 465,
"from": "alarm@example.com", "to": "ops@example.com",
"subject": "【告警】炉温超限", "isHtml": false,
"body": "炉温超过 80°C,当前数据:${msg.data}"
}},
{"id": "tsdb", "type": "x/tsdbWrite", "configuration": {
"driver": "tdengine", "dsn": "root:taosdata@http(localhost:6041)/", "db": "iot",
"measurement": "device_data",
"tags": [{"key": "device_id", "value": "s7-01"}],
"fields": [{"key": "temp", "source": "炉温"}, {"key": "pressure", "source": "压力"}]
}}
],
"connections": [
{"fromId": "sch", "toId": "read", "type": "ip"},
{"fromId": "read", "toId": "filter", "type": "Success"},
{"fromId": "read", "toId": "tsdb", "type": "Success"},
{"fromId": "filter", "toId": "email", "type": "True"}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
TDengine 落盘需预建超级表(子表写入时自动创建):
CREATE STABLE iot.device_data (ts TIMESTAMP, temp DOUBLE, pressure DOUBLE) TAGS (device_id NCHAR(32));
# 场景二:HTTP API 按需读取 → 返回 JSON
前端调 REST API 实时读 PLC 数据。
endpoint/http(POST /api/read) → x/iotRead(Modbus) → 响应
{
"ruleChain": {"name": "api-read", "root": true},
"metadata": {
"nodes": [
{"id": "http", "type": "endpoint/http", "configuration": {
"server": ":9090", "certFile": "", "certKeyFile": ""
}},
{"id": "read", "type": "x/iotRead", "configuration": {
"driver": "modbus", "server": "tcp://192.168.1.100:502",
"points": [
{"name": "temperature", "addr": "40001", "type": "INT16", "scale": 0.1},
{"name": "humidity", "addr": "40002", "type": "INT16", "scale": 0.1}
]
}}
],
"connections": [
{"fromId": "http", "toId": "read", "type": "POST /api/read"}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 场景三:MQTT 命令 → 写 PLC → 反馈
通过 MQTT 下发控制指令到 PLC,写结果回传 MQTT。
endpoint/mqtt(订阅 cmd/+) → jsTransform(解析命令) → x/iotWrite(S7) → mqttClient(发布结果)
{
"ruleChain": {"name": "mqtt-control", "root": true},
"metadata": {
"nodes": [
{"id": "mqtt_in", "type": "endpoint/mqtt", "configuration": {
"server": "tcp://localhost:1883", "topic": "device/cmd/#"
}},
{"id": "parse", "type": "jsTransform", "configuration": {
"jsScript": "var cmd = JSON.parse(msg.data); msg.data = JSON.stringify([{name:'设定温度', addr:'DB1.DBD0', type:'FLOAT32', value: String(cmd.targetTemp)}]); return {msg:msg, metadata:metadata, msgType:msgType};"
}},
{"id": "write", "type": "x/iotWrite", "configuration": {
"driver": "s7", "server": "192.168.1.10:102"
}},
{"id": "mqtt_out", "type": "mqttClient", "configuration": {
"server": "tcp://localhost:1883", "topic": "device/status/${metadata.deviceId}"
}}
],
"connections": [
{"fromId": "mqtt_in", "toId": "parse", "type": "mqtt/device/cmd/#"},
{"fromId": "parse", "toId": "write", "type": "Success"},
{"fromId": "write", "toId": "mqtt_out", "type": "Success"}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 场景四:多协议并行采集 → 合并 → 落盘
同一规则链采集 Modbus 电表 + S7 PLC,合并后统一落盘。
endpoint/schedule → x/iotRead(Modbus电表) ─┐
→ x/iotRead(S7 PLC) ─┤→ join(合并) → x/tsdbWrite(配置 measurement)
2
{
"ruleChain": {"name": "multi-protocol", "root": true},
"metadata": {
"nodes": [
{"id": "sch", "type": "endpoint/schedule", "configuration": {"interval": "@every 1m"}},
{"id": "meter", "type": "x/iotRead", "configuration": {
"driver": "modbus", "server": "tcp://192.168.1.50:502",
"points": [{"name": "power", "addr": "40013", "type": "FLOAT32", "scale": 0.1}]
}},
{"id": "plc", "type": "x/iotRead", "configuration": {
"driver": "s7", "server": "192.168.1.10:102",
"points": [{"name": "speed", "addr": "DB1.DBD0", "type": "FLOAT32"}]
}},
{"id": "merge", "type": "join", "configuration": {"joinInterval": 5}},
{"id": "tsdb", "type": "x/tsdbWrite", "configuration": {
"driver": "opengemini", "host": "127.0.0.1:8086", "database": "iot",
"measurement": "factory", "tags": [{"key": "line", "value": "1"}]
}}
],
"connections": [
{"fromId": "sch", "toId": "meter", "type": "ip"},
{"fromId": "sch", "toId": "plc", "type": "ip"},
{"fromId": "meter", "toId": "merge", "type": "Success"},
{"fromId": "plc", "toId": "merge", "type": "Success"},
{"fromId": "merge", "toId": "tsdb", "type": "Success"}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 场景五:Modbus 从站 → 数据转换 → REST 推送
SCADA 写 Modbus 寄存器 → 转换格式 → 推送到第三方 HTTP 接口。
endpoint/modbusServer → jsTransform(格式化) → restApiCall(POST 推送)
{
"ruleChain": {"name": "scada-bridge", "root": true},
"metadata": {
"nodes": [
{"id": "slave", "type": "endpoint/modbusServer", "configuration": {
"server": "tcp://:5020", "unitId": 1
}},
{"id": "format", "type": "jsTransform", "configuration": {
"jsScript": "var d = JSON.parse(msg.data); msg.data = JSON.stringify({device: 'PLC-01', register: d.addr, value: d.values[0], time: new Date().toISOString()}); return {msg:msg, metadata:metadata, msgType:msgType};"
}},
{"id": "push", "type": "restApiCall", "configuration": {
"restEndpointUrlPattern": "http://third-party:8080/api/data",
"requestMethod": "POST",
"headers": {"Content-Type": "application/json"}
}}
],
"connections": [
{"fromId": "slave", "toId": "format", "type": "ip"},
{"fromId": "format", "toId": "push", "type": "Success"}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 场景六:SNMP Trap 告警 → 写数据库 → 企业微信通知
网络设备 Trap → 解析 → 告警记录入库 + 企微通知。
endpoint/snmp(Trap) → jsTransform(解析) → dbClient(写告警表)
→ restApiCall(企微 webhook)
2
{
"ruleChain": {"name": "snmp-alarm", "root": true},
"metadata": {
"nodes": [
{"id": "trap", "type": "endpoint/snmp", "configuration": {
"server": "0.0.0.0:162", "version": "v2c", "community": "public"
}},
{"id": "parse", "type": "jsTransform", "configuration": {
"jsScript": "var d = JSON.parse(msg.data); msg.data = JSON.stringify({source: d.from, oid: metadata.trapOID, time: Date.now()}); return {msg:msg, metadata:metadata, msgType:msgType};"
}},
{"id": "db", "type": "dbClient", "configuration": {
"driverName": "postgres", "dsn": "postgres://user:pass@localhost:5432/alarm?sslmode=disable",
"sql": "INSERT INTO alerts(source, oid, created_at) VALUES('${source}', '${oid}', NOW())"
}},
{"id": "wecom", "type": "restApiCall", "configuration": {
"restEndpointUrlPattern": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY",
"requestMethod": "POST",
"headers": {"Content-Type": "application/json"}
}}
],
"connections": [
{"fromId": "trap", "toId": "parse", "type": "ip"},
{"fromId": "parse", "toId": "db", "type": "Success"},
{"fromId": "parse", "toId": "wecom", "type": "Success"}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 场景七:采集 → 窗口聚合 → 降采样落盘
高频采集(每秒一次),窗口聚合成每分钟均值后再落盘,存储量降为 1/60。x/iotRead 输出的点位数组直连聚合节点,无需转换节点。
endpoint/schedule(每1秒) → x/iotRead(Modbus) → x/streamAggregator(1min窗口) → x/tsdbWrite
{
"ruleChain": {"name": "downsample", "root": true},
"metadata": {
"nodes": [
{"id": "sch", "type": "endpoint/schedule", "configuration": {"interval": "@every 1s"}},
{"id": "read", "type": "x/iotRead", "configuration": {
"driver": "modbus", "server": "tcp://192.168.1.100:502",
"points": [
{"name": "temperature", "addr": "40001", "type": "FLOAT32", "scale": 0.1},
{"name": "humidity", "addr": "40002", "type": "FLOAT32", "scale": 0.1}
]
}},
{"id": "agg", "type": "x/streamAggregator", "configuration": {
"sql": "SELECT name, AVG(value) AS value FROM stream WHERE error IS NULL GROUP BY name, TumblingWindow('1m')"
}},
{"id": "tsdb", "type": "x/tsdbWrite", "configuration": {
"driver": "timescaledb", "dsn": "postgres://user:pass@localhost:5432/iot?sslmode=disable",
"measurement": "device_minute"
}}
],
"connections": [
{"fromId": "sch", "toId": "read", "type": "ip"},
{"fromId": "read", "toId": "agg", "type": "Success"},
{"fromId": "agg", "toId": "tsdb", "type": "stream_event"}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- 默认纵表模式:每个点位一行(
name/value/timestamp列),SQL 用GROUP BY name逐点统计;聚合结果是[{name,value}]形态的数组,x/tsdbWrite配置measurement后自动透视为一条时序记录落盘。- 需要跨点位计算(如
temperature + humidity)时,给agg节点配置"inputFormat": "columns":点位数组透视为宽表行后进流,SQL 直接写SELECT AVG(temperature)...,聚合输出的整个 map 成为一条记录的 fields。- 聚合结果走
stream_event关系链;Success链透传原始消息,如需原始数据同时落盘可从read再接一路x/tsdbWrite。- 组件文档:x/streamAggregator、StreamSQL 概述。
# 软PLC式逻辑控制
x/control/timer(定时器)与 x/control/watchdog(看门狗)是协议无关的逻辑组件,在规则链里与 9 协议的读写节点串接,即可实现软PLC式的延时动作与失联保护。组件文档:控制定时器、看门狗。
读侧注意:x/iotRead 输出是点位数组 [{name,value,timestamp,error}]。流聚合可直接消费该数组(见场景七);进定时器前需先摊平成 {name:value}(一行 jsTransform):
var d = JSON.parse(msg.data || '[]'); var out = {}; d.forEach(function(p){ if(!p.error) out[p.name] = p.value; }); msg.data = JSON.stringify(out); return {msg:msg, metadata:metadata, msgType:msgType};
# 场景八:罐体超温延时关阀(持续条件)
罐温连续 5 秒全程高于 80°C 才关闭进料阀。窗口聚合 HAVING MIN>80 = 全程过高,天然防抖(单次瞬时尖峰不触发)。
endpoint/schedule(每1秒) → x/iotRead(S7) → jsTransform(摊平) → x/streamAggregator(5s窗口) → x/iotWrite(关阀)
{
"ruleChain": {"name": "tank-overtemp-shutoff", "root": true},
"metadata": {
"nodes": [
{"id": "sch", "type": "endpoint/schedule", "configuration": {"interval": "@every 1s"}},
{"id": "read", "type": "x/iotRead", "configuration": {
"driver": "s7", "server": "192.168.1.10:102",
"points": [{"name": "temperature", "addr": "DB1.DBD0", "type": "REAL"}]
}},
{"id": "flat", "type": "jsTransform", "configuration": {
"jsScript": "var d = JSON.parse(msg.data || '[]'); var out = {}; d.forEach(function(p){ if(!p.error) out[p.name] = p.value; }); msg.data = JSON.stringify(out); return {msg:msg, metadata:metadata, msgType:msgType};"
}},
{"id": "agg", "type": "x/streamAggregator", "configuration": {
"sql": "SELECT MIN(temperature) AS min_temp FROM stream GROUP BY TumblingWindow('5s') HAVING min_temp > 80"
}},
{"id": "write", "type": "x/iotWrite", "configuration": {
"driver": "s7", "server": "192.168.1.10:102",
"points": [{"name": "valve", "addr": "DB1.DBX4.0", "type": "BOOL", "value": "false"}]
}}
],
"connections": [
{"fromId": "sch", "toId": "read", "type": "ip"},
{"fromId": "read", "toId": "flat", "type": "Success"},
{"fromId": "flat", "toId": "agg", "type": "Success"},
{"fromId": "agg", "toId": "write", "type": "stream_event"}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
HAVING min_temp > 80:窗口内只要有一笔采样跌回 80 以下,整个窗口被过滤;只有全程超温的窗口才产出,无窗口满足时聚合节点不输出、写节点不动作。HAVING须引用 SELECT 别名(min_temp),不能复述聚合函数;聚合结果走stream_event关系链(Success透传原始消息)。- 写节点用配置点位(固定关阀
false),聚合输出只决定"是否触发"。
# 场景九:电机延时启动、中途可取消
启动信号上升沿 → 延时 3 秒 → 电机得电;延时期间信号消失则取消(TON 接通延时语义)。
endpoint/schedule(每1秒) → x/iotRead(读启动信号) → jsTransform(摊平并写 metadata.start) → x/control/timer(TON,3s) → x/iotWrite(电机)
{
"ruleChain": {"name": "motor-delay-start", "root": true},
"metadata": {
"nodes": [
{"id": "sch", "type": "endpoint/schedule", "configuration": {"interval": "@every 1s"}},
{"id": "read", "type": "x/iotRead", "configuration": {
"driver": "s7", "server": "192.168.1.10:102",
"points": [{"name": "启动", "addr": "DB1.DBX0.0", "type": "BOOL"}]
}},
{"id": "flat", "type": "jsTransform", "configuration": {
"jsScript": "var d = JSON.parse(msg.data || '[]'); var out = {}; d.forEach(function(p){ if(!p.error) out[p.name] = p.value; }); metadata.start = out['启动']; msg.data = JSON.stringify(out); return {msg:msg, metadata:metadata, msgType:msgType};"
}},
{"id": "timer", "type": "x/control/timer", "configuration": {
"mode": "TON", "pt": "3s", "in": "${metadata.start}", "out": "q"
}},
{"id": "write", "type": "x/iotWrite", "configuration": {
"driver": "s7", "server": "192.168.1.10:102",
"points": [{"name": "电机", "addr": "Q0.0", "type": "BOOL", "value": "${metadata.q}"}]
}}
],
"connections": [
{"fromId": "sch", "toId": "read", "type": "ip"},
{"fromId": "read", "toId": "flat", "type": "Success"},
{"fromId": "flat", "toId": "timer", "type": "Success"},
{"fromId": "timer", "toId": "write", "type": "Success"}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- 上升沿(false→true)开始计时,持续
pt=3s后metadata.q置true;输入提前变回false则计时取消、q复位,下次上升沿重新触发。- 定时器结果写入
metadata.q(out字段),写节点以${metadata.q}引用。
# 场景十:上位机失联 → 安全停机
上位机每隔几秒发心跳,连续 10 秒未收到则看门狗下发故障安全 JSON,关阀停电机。
endpoint/mqtt(订阅 scada/heartbeat/#) → x/control/watchdog(10s) → x/iotWrite(阀/电机)
{
"ruleChain": {"name": "heartbeat-failsafe", "root": true},
"metadata": {
"nodes": [
{"id": "hb", "type": "endpoint/mqtt", "configuration": {
"server": "tcp://localhost:1883", "topic": "scada/heartbeat/#"
}},
{"id": "wd", "type": "x/control/watchdog", "configuration": {
"timeout": "10s",
"failsafe": {"valve": 0, "motor": 0}
}},
{"id": "write", "type": "x/iotWrite", "configuration": {
"driver": "modbus", "server": "tcp://192.168.1.100:502",
"points": [
{"name": "阀门", "addr": "00001", "type": "BOOL", "value": "${msg.valve}"},
{"name": "电机", "addr": "00002", "type": "BOOL", "value": "${msg.motor}"}
]
}}
],
"connections": [
{"fromId": "hb", "toId": "wd", "type": "mqtt/scada/heartbeat/#"},
{"fromId": "wd", "toId": "write", "type": "Success"}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- 心跳正常到达:看门狗透传
msg.Data(心跳携带的阀/电机状态)到下游,并重新武装计时;timeout内无消息则经Success链下发failsafe{"valve":0,"motor":0}。- 心跳载荷与
failsafe使用相同valve/motor字段名,写节点统一以${msg.xx}引用。
# 场景十一:带死区迟滞的越限告警(防抖 + 边沿通知)
温度 >80°C 告警,但传感器在边界抖动——用死区(迟滞):≥80 进入告警,需回落到 ≤78 才恢复,中间区间保持原态,挡住抖动;且只在状态翻转时发一次通知(边沿),不发告警风暴。
场景一的 jsFilter(温度>80) 无死区、每条都判断,边界抖动会告警风暴。本场景用 streamsql 的 hysteresis(死区状态机)+ changed_col(边沿)根治。
endpoint/schedule → x/iotRead(温度) → x/streamTransform(hysteresis 死区电平) → x/streamTransform(changed_col 边沿) → restApiCall(钉钉)
→ x/tsdbWrite(落盘)
2
{
"ruleChain": {"name": "deadband-alarm", "root": true},
"metadata": {
"nodes": [
{"id": "sch", "type": "endpoint/schedule", "configuration": {"interval": "@every 5s"}},
{"id": "read", "type": "x/iotRead", "configuration": {
"driver": "modbus", "server": "tcp://192.168.1.100:502",
"points": [{"name": "temp", "addr": "40001", "type": "FLOAT32", "scale": 0.1}]
}},
{"id": "alarm", "type": "x/streamTransform", "configuration": {
"inputFormat": "columns",
"sql": "SELECT hysteresis(temp, 80, 78) AS alarm FROM stream"
}},
{"id": "edge", "type": "x/streamTransform", "configuration": {
"sql": "SELECT changed_col(true, alarm) AS edge FROM stream"
}},
{"id": "notify", "type": "restApiCall", "configuration": {
"restEndpointUrlPattern": "https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN",
"requestMethod": "POST",
"headers": {"Content-Type": "application/json"}
}},
{"id": "tsdb", "type": "x/tsdbWrite", "configuration": {
"driver": "tdengine", "dsn": "root:taosdata@http(localhost:6041)/", "db": "iot",
"measurement": "device_data"
}}
],
"connections": [
{"fromId": "sch", "toId": "read", "type": "ip"},
{"fromId": "read", "toId": "alarm", "type": "Success"},
{"fromId": "alarm", "toId": "edge", "type": "Success"},
{"fromId": "edge", "toId": "notify", "type": "Success"},
{"fromId": "read", "toId": "tsdb", "type": "Success"}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
- 死区
hysteresis(temp, 80, 78):temp≥80进入告警(alarm=true),temp≤78才恢复(alarm=false),80→78 之间保持原态——传感器在 79~80 抖动不会反复翻转。下越限把两阈值对调即可(如低温告警hysteresis(temp, 10, 15):≤10进、≥15出)。多点采集配OVER (PARTITION BY name),每个点位独立死区。- 边沿为何要两个节点:streamsql 不允许分析函数嵌套(
changed_col(hysteresis(...))会报错),所以串联两个x/streamTransform:alarm节点算死区电平,edge节点用changed_col取翻转。changed_col作为唯一 SELECT 输出时,值未变化整行被抑制走 False 链(不通知),翻转才走 Success → 进入告警、恢复正常各发一次,杜绝风暴。- 落盘与通知分离:
read → tsdb每次采样都落盘(含正常态);通知只在edge翻转时触发。- 需 streamsql
v1.1.3+(内置hysteresis/latch)。函数参考:分析函数。
# 组件组合速查
| 场景 | 触发 | 采集/接收 | 处理 | 输出 |
|---|---|---|---|---|
| 定时采集落盘 | endpoint/schedule | x/iotRead | — | x/tsdbWrite(配置 measurement) |
| 降采样落盘 | endpoint/schedule | x/iotRead | x/streamAggregator(窗口聚合) | x/tsdbWrite |
| 超限告警 | 同上 | 同上 | jsFilter | sendEmail / restApiCall |
| 死区告警 | endpoint/schedule | x/iotRead | x/streamTransform(hysteresis+changed_col) | restApiCall |
| API 按需读 | endpoint/http | x/iotRead | — | 响应 |
| MQTT 控制 | endpoint/mqtt | — | jsTransform | x/iotWrite + mqttClient |
| 多协议合并 | endpoint/schedule | 多个 x/iotRead | join | x/tsdbWrite |
| SCADA 桥接 | endpoint/modbusServer | — | jsTransform | restApiCall / x/tsdbWrite |
| Trap 告警 | endpoint/snmp | — | jsTransform | dbClient + restApiCall |
| 环保数采 | endpoint/hj212 | — | — | x/tsdbWrite(配置 measurement) |