使用 Node-red 从 PLC 获取数据后进行映射并插入到 TDengine 时序数据库【边缘网关】
FBP 流程

流程主要分为三部分,经典的 IPO 的程序模型
- PLC 获取数据并拆包
- 数据映射
- 格式化 SQL 插入语句并配置数据库
分解步骤
input
- PLC 的 IP 及端口
- 点位配置
- 拆包
因为 PLC 获取的数据的方式有三种输出,为了方便调试和差错,选择第二种然后对输出的 json 进行拆解。
| Single Variable | 单个点位值 |
| All variables, one permessage | 全部值,一个 json 输出 |
| All variables | 全部值,分开输出 |
// 检查 msg.payload 是否为对象
if (typeof msg.payload === 'object' && msg.payload !== null && !Array.isArray(msg.payload)) {
// 获取所有的键值对
var entries = Object.entries(msg.payload);
// 遍历所有的键值对
entries.forEach(function(entry) {
// 创建一个新的 msg 对象
var newMsg = {
payload: entry[0] + ':' + entry[1], // 将键和值连接成字符串
topic: 'data' // 可以设置一个统一的 topic 或者根据需要自定义
};
// 发送新的 msg 对象到输出
node.send(newMsg);
});
} else {
// 如果 msg.payload 不是对象,发送原始 msg 对象
node.send(msg);
}
拆包完成的数据格式,点位:值
Q2,Q1:228
Q2,Q0:false
process
- 数据映射
此部分需严格参考TDengine 的表结构,对原始的点位信息进行编码,有助于后期的维护
| 名称 | 类型 | 长度 | 字段标签 | 备注 |
|---|---|---|---|---|
| ts | TIMESTAMP | 8 | 字段 | 时间戳 |
| val | VARCHAR | 20 | 字段 | 值 |
| device | VARCHAR | 20 | 标签 | 设备 |
| location | VARCHAR | 10 | 标签 | 位置 |
| attribute | VARCHAR | 30 | 标签 | 属性 |
// 定义一个映射对象,将 DB 块和位移映射到变量名
var mapping = {
"Q2,Q1": "mixing_451_stop",
"Q2,Q0": "mixing_451_start"
};
// 解析 msg.payload
if (typeof msg.payload === 'string') {
// 分割字符串,获取 DB 块,地址 和 值
var parts = msg.payload.split(':');
var dbInfo = parts[0].trim(); // "DB22,R26"
var value = parts[1].trim(); // "0" 或 "false" 或 "51.41"
// 提取 DB 块和地址
var dbBlock = dbInfo.split(',')[0] + ',' + dbInfo.split(',')[1]; // "DB22,R26"
// 获取映射后的变量名
var variableName = mapping[dbBlock];
// 检查变量名是否存在
if (variableName) {
// 尝试解析为数字,如果失败则保留原始字符串
var parsedValue = isNaN(parseFloat(value)) ? value : parseFloat(value);
// 创建一个新的 msg 对象
var newMsg = {
payload: {
[variableName]: parsedValue
},
topic: variableName // 设置 topic 为映射后的变量名
};
// 发送新的 msg 对象到输出
return newMsg;
} else {
// 如果没有找到映射关系,可以发送原始 msg 对象或者错误消息
node.error('No mapping found for ' + dbBlock, msg);
return null;
}
} else {
// 如果 msg.payload 不是字符串,可以发送原始 msg 对象或者错误消息
node.error('Payload is not a string', msg);
return null;
}
- 日志保存
保存日志时按照以下方式进行记录,需注意需要对获取的时间进行偏移
mixing_451_stop: true*2025-07-28 15:35:58
mixing_451_start: false*2025-07-28 15:35:58
let data = msg.payload;
// 获取当前时间并转换为东八区时间
let now = new Date();
let timestamp = new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString().replace('T', ' ').substring(0, 19);
// 初始化输出字符串
let output = "";
// 遍历 JSON 数据
for (let key in data) {
// 获取对应的值
let value = data[key];
// 格式化输出,用逗号分隔每个字段
output += `${key}: ${value}*${timestamp}, `;
}
// 去掉最后一个多余的逗号和空格
output = output.slice(0, -2);
// 将结果存储到 msg.payload 中
msg.payload = output;
// 返回结果
return msg;
output
- 本地 txt 格式保存
使用 【写入文件】模块即可
- 格式化 SQL 语句
// 获取原始数据
const data = msg.payload;
// 定义一个数组,用于存储生成的 SQL 插入语句
let sqlStatements = [];
// 遍历数据中的每个字段
for (const key in data) {
if (key !== "timestamp") {
// 提取设备名称、位置编号和属性名称
const parts = key.split("_"); // 使用下划线分隔字段名
if (parts.length !== 3) {
// 如果字段名不符合预期格式,跳过
console.warn(`Field name format is incorrect: ${key}. Expected format: device_location_attribute`);
continue;
}
const device = parts[0]; // 设备名称
const location = parts[1]; // 位置编号
const attribute = parts[2]; // 属性名称
// 获取字段值
const value = data[key];
// 根据数据类型处理值
let formattedValue;
if (typeof value === "string" || typeof value === "number") {
// 如果值是字符串或数字,直接使用(TDengine 的 VARCHAR 类型可以接受字符串或数字)
formattedValue = typeof value === "string" ? `'${value}'` : value; // 数字类型不加引号
} else {
console.warn(`Unsupported data type for value: ${typeof value} (key: ${key})`);
continue; // 跳过不支持的数据类型
}
// 构建 SQL 插入语句
// 使用原始数据中的键名作为表名
const tableName = key; // 表名直接使用原始键名
const sql = `INSERT INTO ${tableName} USING JYdetected_data TAGS ('${device}', '${location}', '${attribute}') VALUES (NOW(), ${formattedValue})`;
sqlStatements.push(sql);
}
}
// 将所有 SQL 语句合并为一个字符串,用分号分隔
let result = sqlStatements.join(";");
// 如果 result 为空,则填充默认值为 0
if (!result.trim()) {
result = `INSERT INTO 'null' USING JYdetected_data TAGS ('null', 'null', 'null') VALUES (NOW(), null)`;
}
// 将结果赋值给 msg.payload
msg.payload = result;
//msg.topic = result;
return msg;
- 数据库配置
可以参考另一篇文章 https://www.cnblogs.com/moluomei/articles/19044548 在此不做赘述
小结
此 flows 的思路应该可以适用于大多数的应用场景。获取、收集数据是因为这部分数据可能有使用价值,所以在收集的过程中就要对数据进行意义明确的编码,为了上层应用系统的方便调用。node-red 有许多的应用场景,此例程主要针对于部署在边缘网关的场景。

浙公网安备 33010602011771号