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

FBP 流程

图片

流程主要分为三部分,经典的 IPO 的程序模型

  1. PLC 获取数据并拆包
  2. 数据映射
  3. 格式化 SQL 插入语句并配置数据库

分解步骤

input

  1. PLC 的 IP 及端口
  2. 点位配置
  3. 拆包

因为 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

  1. 数据映射

此部分需严格参考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;
}
  1. 日志保存

保存日志时按照以下方式进行记录,需注意需要对获取的时间进行偏移

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

  1. 本地 txt 格式保存

使用 【写入文件】模块即可

  1. 格式化 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;
  1. 数据库配置

可以参考另一篇文章 https://www.cnblogs.com/moluomei/articles/19044548 在此不做赘述

小结

此 flows 的思路应该可以适用于大多数的应用场景。获取、收集数据是因为这部分数据可能有使用价值,所以在收集的过程中就要对数据进行意义明确的编码,为了上层应用系统的方便调用。node-red 有许多的应用场景,此例程主要针对于部署在边缘网关的场景。

posted @ 2025-08-19 09:50  麻洛美  阅读(199)  评论(0)    收藏  举报