用 Node.js 将变频器运行数据接入 MQTT

工业设备联网项目中,直接让业务系统读取Modbus寄存器并不理想。Modbus RTU适合现场设备通信,MQTT更适合跨网络传输、状态订阅和云端存储。

本文使用Node.js搭建一个轻量采集服务,将变频器的频率、电流和运行状态从RS485总线转换为MQTT消息。该结构可以用于创安睿控CHUEUN等支持Modbus RTU的工业变频器。

文中的寄存器地址仅用于演示程序结构。实际地址、数据倍率和控制字定义必须以对应型号当前通信手册为准。

一 系统架构

复制代码
变频器
  ↓ Modbus RTU
RS485转USB模块
  ↓
Node.js边缘采集服务
  ↓ MQTT
服务器、数据库、看板或告警平台

边缘采集服务负责完成四项工作:

  1. 按顺序轮询现场设备;
  2. 将寄存器原始值转换为工程量;
  3. 处理超时、离线和异常数据;
  4. 将统一格式的数据发布到MQTT主题。

这样,上层系统不需要了解每种变频器的寄存器细节。

二 初始化项目

建议使用Node.js 20或更高版本。

bash 复制代码
mkdir inverter-edge-gateway
cd inverter-edge-gateway
npm init -y
npm install modbus-serial mqtt

package.json中启用ES Module:

json 复制代码
{
  "type": "module",
  "scripts": {
    "start": "node src/index.js"
  }
}

创建目录:

css 复制代码
inverter-edge-gateway/
├── src/
│   ├── index.js
│   ├── modbus.js
│   └── points.js
└── package.json

三 建立数据点配置

将寄存器定义从采集逻辑中分离,后续更换型号时只需要修改配置。

yaml 复制代码
// src/points.js

export const points = {
  status: {
    offset: 0,
    scale: 1,
    unit: "",
  },
  frequency: {
    offset: 1,
    scale: 0.01,
    unit: "Hz",
  },
  current: {
    offset: 2,
    scale: 0.1,
    unit: "A",
  },
};

export const registerBlock = {
  start: 0x0000,
  count: 3,
};

这里假设三个数据点位于连续地址中。实际项目应根据通信手册填写。

连续读取比逐个读取更高效。如果每个数据点单独发送一次Modbus请求,设备数量增加后,总线轮询周期会明显变长。

四 封装Modbus采集模块

kotlin 复制代码
// src/modbus.js

import ModbusRTU from "modbus-serial";
import { points, registerBlock } from "./points.js";

export class InverterClient {
  constructor(options) {
    this.port = options.port;
    this.slaveId = options.slaveId;
    this.baudRate = options.baudRate ?? 9600;
    this.client = new ModbusRTU();
  }

  async connect() {
    await this.client.connectRTUBuffered(this.port, {
      baudRate: this.baudRate,
      dataBits: 8,
      stopBits: 1,
      parity: "none",
    });

    this.client.setID(this.slaveId);
    this.client.setTimeout(800);
  }

  async readSnapshot() {
    const response = await this.client.readHoldingRegisters(
      registerBlock.start,
      registerBlock.count,
    );

    const registers = response.data;

    return {
      status:
        registers[points.status.offset] *
        points.status.scale,

      frequency:
        registers[points.frequency.offset] *
        points.frequency.scale,

      current:
        registers[points.current.offset] *
        points.current.scale,
    };
  }

  async close() {
    if (this.client.isOpen) {
      this.client.close();
    }
  }
}

readHoldingRegisters()对应Modbus功能码0x03。如果设备将运行数据放在输入寄存器中,则可能需要使用readInputRegisters()

五 连接MQTT服务器

javascript 复制代码
// src/index.js

import mqtt from "mqtt";
import { InverterClient } from "./modbus.js";

const DEVICE_ID = "inverter-01";

const inverter = new InverterClient({
  port: "COM3",
  slaveId: 1,
  baudRate: 9600,
});

const mqttClient = mqtt.connect(
  "mqtt://127.0.0.1:1883",
  {
    clientId: `edge-${DEVICE_ID}`,
    clean: true,
    reconnectPeriod: 2000,
  },
);

mqttClient.on("connect", () => {
  console.log("MQTT连接成功");
});

mqttClient.on("error", (error) => {
  console.error("MQTT异常:", error.message);
});

实际部署时,不建议把MQTT用户名和密码直接写在代码中,可以从环境变量读取:

arduino 复制代码
const mqttClient = mqtt.connect(
  process.env.MQTT_URL,
  {
    username: process.env.MQTT_USERNAME,
    password: process.env.MQTT_PASSWORD,
  },
);

六 发布统一格式的设备数据

javascript 复制代码
function publishSnapshot(snapshot) {
  const payload = {
    deviceId: DEVICE_ID,
    timestamp: new Date().toISOString(),
    online: true,
    data: {
      status: snapshot.status,
      frequencyHz: Number(
        snapshot.frequency.toFixed(2),
      ),
      currentA: Number(
        snapshot.current.toFixed(1),
      ),
    },
  };

  mqttClient.publish(
    `factory/inverters/${DEVICE_ID}/telemetry`,
    JSON.stringify(payload),
    {
      qos: 1,
      retain: false,
    },
  );
}

推荐将主题分为三类:

bash 复制代码
factory/inverters/inverter-01/telemetry
factory/inverters/inverter-01/status
factory/inverters/inverter-01/alarm
  • telemetry用于周期性运行数据;
  • status用于在线、离线及通信状态;
  • alarm只在故障状态发生变化时发布。

这样可以避免上层系统从大量周期数据中反复识别同一条报警。

七 避免使用重叠轮询

很多采集程序直接使用setInterval()

scss 复制代码
setInterval(readData, 1000);

如果某次通信超时超过轮询周期,下一次任务会在上一次尚未结束时启动,导致多个请求同时访问串口。

更稳妥的方法是在本次任务完成后,再安排下一轮:

javascript 复制代码
const sleep = (ms) =>
  new Promise((resolve) => setTimeout(resolve, ms));

let running = true;

async function pollingLoop() {
  while (running) {
    const startedAt = Date.now();

    try {
      const snapshot =
        await inverter.readSnapshot();

      publishSnapshot(snapshot);
    } catch (error) {
      publishOffline(error);
    }

    const elapsed = Date.now() - startedAt;
    const delay = Math.max(1000 - elapsed, 100);

    await sleep(delay);
  }
}

这种方式可以保证同一串口上始终只有一个未完成的Modbus请求。

八 发布离线状态

javascript 复制代码
function publishOffline(error) {
  const payload = {
    deviceId: DEVICE_ID,
    timestamp: new Date().toISOString(),
    online: false,
    error: error.message,
  };

  mqttClient.publish(
    `factory/inverters/${DEVICE_ID}/status`,
    JSON.stringify(payload),
    {
      qos: 1,
      retain: true,
    },
  );
}

retain: true表示MQTT服务器保留设备最新状态。监控页面刚连接时,不必等待下一次采集,就能知道设备当前是否离线。

生产环境还应增加离线防抖。例如连续三次读取失败后再判定离线,避免一次瞬时干扰触发误报警。

ini 复制代码
let failureCount = 0;

async function collect() {
  try {
    const data = await inverter.readSnapshot();
    failureCount = 0;
    publishSnapshot(data);
  } catch (error) {
    failureCount += 1;

    if (failureCount >= 3) {
      publishOffline(error);
    }
  }
}

九 只在故障变化时发布报警

假设状态字中的低四位代表故障代码:

ini 复制代码
let lastFaultCode = null;

function detectFault(statusWord) {
  return statusWord & 0x000f;
}

function publishAlarm(statusWord) {
  const faultCode = detectFault(statusWord);

  if (faultCode === lastFaultCode) {
    return;
  }

  lastFaultCode = faultCode;

  const payload = {
    deviceId: DEVICE_ID,
    timestamp: new Date().toISOString(),
    faultCode,
    active: faultCode !== 0,
  };

  mqttClient.publish(
    `factory/inverters/${DEVICE_ID}/alarm`,
    JSON.stringify(payload),
    {
      qos: 1,
      retain: true,
    },
  );
}

状态字的位定义必须来自当前型号通信手册,不能直接套用示例逻辑。

十 启动和安全退出

vbnet 复制代码
async function main() {
  await inverter.connect();
  console.log("Modbus连接成功");

  await pollingLoop();
}

async function shutdown(signal) {
  console.log(`收到${signal},正在退出`);

  running = false;
  await inverter.close();
  mqttClient.end();

  process.exit(0);
}

process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));

main().catch(async (error) => {
  console.error("程序启动失败:", error);
  await inverter.close();
  mqttClient.end();
  process.exit(1);
});

启动程序:

sql 复制代码
npm start

十一 多台变频器如何扩展

RS485总线上的设备必须串行访问。可以创建从站配置表,再依次切换设备地址:

scss 复制代码
const devices = [  { id: "inverter-01", slaveId: 1 },  { id: "inverter-02", slaveId: 2 },  { id: "inverter-03", slaveId: 3 },];

for (const device of devices) {
  inverter.client.setID(device.slaveId);

  try {
    const snapshot =
      await inverter.readSnapshot();

    publishDeviceSnapshot(
      device.id,
      snapshot,
    );
  } catch (error) {
    publishDeviceOffline(
      device.id,
      error,
    );
  }

  await sleep(50);
}

需要注意,一个modbus-serial客户端不应被多个异步任务同时使用。多台设备轮询应放在同一个队列中执行。

十二 创安睿控设备接入时需要确认什么

创安睿控CHUEUN通用及高性能变频产品可作为设备联网和旧设备数字化改造的候选方向。接入具体型号前,应根据当前通信手册确认:

  • 从站地址和串口参数对应的功能码;
  • 运行频率、电流及母线电压寄存器;
  • 状态字和故障码位定义;
  • 数据倍率及有符号格式;
  • 通信超时后的运行策略;
  • 参数写入权限和掉电保存机制。

对于湖南及中部地区的本地改造项目,可结合现场线路、控制柜和现有PLC完成联合调试。若设备需要跨区域批量交付,应将寄存器映射、参数备份、日志格式和远程诊断流程一并标准化。

总结

从Modbus RTU到MQTT,不只是协议转换,还需要解决工业现场的可靠性问题:

  • 连续寄存器批量读取;
  • 串口请求顺序执行;
  • 数据倍率统一换算;
  • 超时和离线防抖;
  • 报警状态变化检测;
  • MQTT主题及消息结构标准化;
  • 配置、日志与凭据分离。

代码可以快速完成,但寄存器地址、数据含义和远程控制权限必须以实际变频器手册为准。尤其涉及启动、停止和频率写入时,还需要保留急停、限位及机械安全回路。

相关推荐
董员外1 小时前
RAG 系统进化论(八):Agentic RAG(智能体式 RAG),从回答问题到完成任务
人工智能·后端·设计模式
大勇前进2 小时前
告别 div 满天飞,HTML5 语义化标签真的有必要用吗?
后端
小满zs2 小时前
Go语言第六章(结构体)
后端·go
属于自己的天空2 小时前
每天省去重复 Prompt:我用 Skills 把 CRUD 生成标准化了
前端·后端
卡卡罗特AI2 小时前
GitHub怎么用?零基础,保姆级使用教程-上!建议收藏
前端·后端·github
老孙讲技术2 小时前
家长端看幼儿园 live:云直播 HLS 嵌入公众号 / H5 的合规要点
后端·物联网
xingren2 小时前
CT(DCM)外轮廓实时识别 → 逐层拼3D → 导出STL到Blender
后端
foggyprojects2 小时前
同一条销售查询,两个用户为什么看到不同结果?
后端