Node.js Modbus开发实战完整指南:modbus-serial库 TCP/RTU客户端 数据解析与MQTT上云网关

freeFree Technical Resource

This content is free to read, suitable for basic learning and search traffic.

Node.js以其异步非阻塞、事件驱动的特性,非常适合开发工业数据采集和Modbus通信网关。modbus-serial是Node.js生态中最流行、最活跃的Modbus库,纯JavaScript实现,支持Modbus RTU(시리얼 포트)、TCP和RTU-over-TCP,同时提供客户端和服务端API。本文从安装配置、TCP/RTU客户端、数据类型解析、服务端模拟、批量采集、MQTT上云到工业级网关实战,全面讲解Node.js Modbus开发。

一、modbus-serial库概述

1.1 什么是modbus-serial

modbus-serial是一个纯JavaScript实现的Modbus协议库,由Yaacov Zamir维护,在npm上每周下载量超过10万次,是Node.js生态中最成熟的Modbus 솔루션。它同时支持Modbus RTU(通过串口)、Modbus TCP和RTU-over-TCP三种传输方式,提供Promise和回调两种API风格,并内置了Modbus服务端(slave)模拟功能。

1.2 核心特性

  • 多协议支持:Modbus RTU(시리얼 포트)、Modbus TCP、RTU-over-TCP
  • 客户端+服务端:既可以做主站采集数据,也可以做从站模拟设备
  • Promise API:所有操作返回Promise,支持async/await
  • 数据类型解析:内置32位整数、浮点数、字符串等多寄存器数据解析
  • 串口支持:基于node-serialport,支持Windows/Linux/macOS
  • 连接管理:自动重连、超时控制、事务ID管理
  • 纯JavaScript:无需编译原生模块(TCP模式),跨平台
  • 活跃维护:持续更新,issue响应快,社区活跃

1.3 支持的功能码

기능 코드方法说明
01readCoils(addr, count)读线圈
02readDiscreteInputs(addr, count)read discrete inputs
03readHolding회원가입s(addr, count)read holding registers
04readInput회원가입s(addr, count)read input registers
05writeCoil(addr, value)write single coil
06write회원가입(addr, value)write single register
15writeCoils(addr, values)write multiple coils
16write회원가입s(addr, values)write multiple registers
43readDeviceIdentification()读设备标识(MEI)

二、安装与快速开始

# 安装modbus-serial
npm install modbus-serial

# 如果需要RTU串口支持,还需安装serialport
npm install serialport

# 验证安装
node -e "const ModbusRTU = require('modbus-serial'); console.log('modbus-serial已安装');"
// quick_시작.js - 5分钟快速上手
const ModbusRTU = require("modbus-serial");

// 创建客户端实例
const client = new ModbusRTU();

// 设置从站地址(单元ID)
client.setID(1);

// 设置超时(毫秒)
client.setTimeout(5000);

// connectModbus TCPdevice
async function main() {
    try {
        // 连接到Modbus TCPslave
        await client.connectTCP("192.168.1.100", { port: 502 });
        console.log("已连接到Modbus TCPdevice");

        // read10个保持寄存器(从地址0开始)
        const result = await client.readHolding회원가입s(0, 10);
        console.log("保持레지스터:", result.데이터);

        // Write to a single register(주소100,值1234)
        await client.write회원가입(100, 1234);
        console.log("已写入寄存器100=1234");

        // read8个线圈
        const coils = await client.readCoils(0, 8);
        console.log("Coil status:", coils.데이터);

        // 关闭连接
        client.close();
        console.log("连接已关闭");
    } catch (err) {
        console.error("操作失败:", err.message);
    }
}

main();

三、Modbus TCP客户端详解

// tcp_client.js - Modbus TCP客户端完整示例
const ModbusRTU = require("modbus-serial");

class ModbusTcpClient {
    constructor(host, port = 502, unitId = 1, timeout = 5000) {
        this.client = new ModbusRTU();
        this.host = host;
        this.port = port;
        this.unitId = unitId;
        this.timeout = timeout;
        this.connected = false;
    }

    // connect
    async connect() {
        this.client.setID(this.unitId);
        this.client.setTimeout(this.timeout);

        await this.client.connectTCP(this.host, {
            port: this.port,
            // 可选:自动重连
            // autoReconnect: true,
            // reconnectInterval: 1000,
        });
        this.connected = true;
        console.log(`Connected ${this.host}:${this.port}, slaveID=${this.unitId}`);
    }

    // read holding registers
    async readHolding(주소, count) {
        const result = await this.client.readHolding회원가입s(주소, count);
        return result.데이터;  // 返回number数组
    }

    // read input registers
    async readInput(주소, count) {
        const result = await this.client.readInput회원가입s(주소, count);
        return result.데이터;
    }

    // 读线圈
    async readCoils(주소, count) {
        const result = await this.client.readCoils(주소, count);
        return result.데이터;  // 返回boolean数组
    }

    // read discrete inputs
    async readDiscrete(주소, count) {
        const result = await this.client.readDiscreteInputs(주소, count);
        return result.데이터;
    }

    // write single register
    async write회원가입(주소, value) {
        await this.client.write회원가입(주소, value);
    }

    // write multiple registers
    async write회원가입s(주소, values) {
        await this.client.write회원가입s(주소, values);
    }

    // write single coil
    async writeCoil(주소, value) {
        await this.client.writeCoil(주소, value);  // value: true/false
    }

    // 닫기
    close() {
        this.client.close();
        this.connected = false;
    }
}

// 使用示例
async function main() {
    const mb = new ModbusTcpClient("192.168.1.100", 502, 1);
    await mb.connect();

    // 读取传感器数据
    const temp = await mb.readHolding(0, 4);
    console.log("temperature:", temp[0] / 10, "°C");
    console.log("湿度:", temp[1] / 10, "%");
    console.log("电压:", temp[2] / 10, "V");
    console.log("电流:", temp[3] / 100, "A");

    // 控制设备
    await mb.writeCoil(0, true);   // 启动电机
    await mb.write회원가입(10, 50); // 设置频率50Hz

    mb.close();
}

main().catch(console.error);

四、Modbus RTU串口客户端

// rtu_client.js - Modbus RTU串口客户端
const ModbusRTU = require("modbus-serial");

async function main() {
    const client = new ModbusRTU();
    client.setID(1);        // slave 주소
    client.setTimeout(5000); // 超时5秒

    // Connect 시리얼 포트(Linux: /dev/ttyUSB0, Windows: COM3, macOS: /dev/tty.usbserial-xxx)
    await client.connectRTU("/dev/ttyUSB0", {
        baudRate: 9600,      // Baud rate
        데이터Bits: 8,         // 데이터 bit
        parity: "none",      // check digit: none/even/odd
        stopBits: 1,         // stop bit
        // 可选:RS485自动收发控制
        // rtsControl: "hardware",
    });
    console.log("RTU串口已连接");

    // Read and hold register
    const result = await client.readHolding회원가입s(0, 10);
    console.log("register 데이터:", result.데이터);

    // 读取多个从站(总线上多个设备)
    for (let id = 1; id <= 5; id++) {
        client.setID(id);
        try {
            const 데이터 = await client.readHolding회원가입s(0, 4);
            console.log(`slave${id}:`, 데이터.데이터);
        } catch (err) {
            console.log(`slave${id}无响应:`, err.message);
        }
    }

    client.close();
}

main().catch(console.error);

4.1 串口设备查找

// list_ports.js - 查找可用串口
const SerialPort = require("serialport");

async function listSerialPorts() {
    const ports = await SerialPort.list();
    console.log("可用串口:");
    ports.forEach(port => {
        console.log(`  路径: ${port.path}`);
        console.log(`  制造商: ${port.manufacturer || "Unknown"}`);
        console.log(`  序列号: ${port.serialNumber || "无"}`);
        console.log(`  pnpId: ${port.pnpId || "无"}`);
        console.log("---");
    });
}

listSerialPorts();

// 常见串口路径:
// Linux:   /dev/ttyUSB0 (USB转RS485), /dev/ttyS0 (板载串口)
// Windows: COM3, COM4
// macOS:   /dev/tty.usbserial-A123456, /dev/cu.usbserial-A123456

五、数据类型解析

Modbus寄存器是16位的,但实际设备经常用多个寄存器组合存储32位整数、浮点数、字符串等数据。modbus-serial提供了buffer工具类来解析这些数据。

// 데이터_types.js - 多寄存器数据类型解析
const ModbusRTU = require("modbus-serial");

// 方法一:使用modbus-serial内置的bufferTools
async function read데이터Types(client) {
    // read8个寄存器(足够存放各种数据类型)
    const result = await client.readHolding회원가입s(0, 8);
    const regs = result.데이터;  // [r0, r1, r2, r3, r4, r5, r6, r7]

    // ====== 16位整数(单个寄存器) ======
    const uint16 = regs[0];           // 无符号16位
    const int16 = regs[0] > 32767 ? regs[0] - 65536 : regs[0];  // 有符号16位

    // ====== 32位整数(两个寄存器) ======
    // 大端模式(高字在前):value = (r0 << 16) | r1
    const uint32_be = (regs[0] << 16) | regs[1];
    // 小端模式(低字在前):value = (r1 << 16) | r0
    const uint32_le = (regs[1] << 16) | regs[0];

    // ====== 32位浮点数(两个寄存器,IEEE 754) ======
    // 使用Buffer转换
    const buf = Buffer.alloc(4);
    buf.writeUInt16BE(regs[2], 0);  // 高字
    buf.writeUInt16BE(regs[3], 2);  // 低字
    const float32 = buf.readFloatBE(0);  // 大端浮点数

    // ====== 64位浮点数(四个寄存器) ======
    const buf64 = Buffer.alloc(8);
    for (let i = 0; i < 4; i++) {
        buf64.writeUInt16BE(regs[4 + i], i * 2);
    }
    const double64 = buf64.readDoubleBE(0);

    // ====== 字符串(多个寄存器,每个寄存器存2个ASCIIcharacters) ======
    let str = "";
    for (let i = 0; i < 4; i++) {
        str += String.fromCharCode((regs[i] >> 8) & 0xFF);
        str += String.fromCharCode(regs[i] & 0xFF);
    }
    str = str.replace(/\0/g, "").trim();  // 去掉空字符

    console.log("16位无符号:", uint16);
    console.log("16位有符号:", int16);
    console.log("32位无符号(大端):", uint32_be);
    console.log("32位浮点数:", float32);
    console.log("64位浮点数:", double64);
    console.log("字符串:", str);
}

// 方法二:使用modbus-serial的readHolding회원가입s返回的buffer
// result.buffer 是一个Buffer对象,可以直接用标准方法读取
async function readWithBuffer(client) {
    const result = await client.readHolding회원가입s(0, 8);
    const buf = result.buffer;  // Buffer对象

    const uint16 = buf.readUInt16BE(0);
    const uint32 = buf.readUInt32BE(2);
    const float = buf.readFloatBE(6);

    console.log("使用Bufferread:", uint16, uint32, float);
}

六、Modbus服务端(slave)模拟

modbus-serial不仅可以做客户端,还可以创建Modbus TCP服务端(slave),用于模拟设备或搭建数据网关。

// modbus_server.js - Modbus TCP服务端(slave)模拟
const ModbusRTU = require("modbus-serial");

// 创建服务端
const server = new ModbusRTU.Server();

// ====== 定义数据向量(回调函数) ======
// 当主站读取时,这些回调函数被调用,返回对应数据
const vector = {
    // read holding registers(기능 코드03)
    getHolding회원가입: function(addr, unitID, callback) {
        // addr: register 주소
        // unitID: slave 주소
        // callback(err, value): 返回寄存器值
        const holdingRegs = [250, 650, 2200, 135, 0, 0, 0, 0];
        if (addr >= 0 && addr < holdingRegs.length) {
            callback(null, holdingRegs[addr]);
        } else {
            callback({
                exceptionCode: 2,  // 非法地址
            });
        }
    },

    // read input registers(기능 코드04)
    getInput회원가입: function(addr, unitID, callback) {
        const inputRegs = [100, 200, 300, 400];
        if (addr >= 0 && addr < inputRegs.length) {
            callback(null, inputRegs[addr]);
        } else {
            callback({ exceptionCode: 2 });
        }
    },

    // 读线圈(기능 코드01)
    getCoil: function(addr, unitID, callback) {
        const coils = [true, false, true, true, false, false, false, false];
        if (addr >= 0 && addr < coils.length) {
            callback(null, coils[addr]);
        } else {
            callback({ exceptionCode: 2 });
        }
    },

    // read discrete inputs(기능 코드02)
    getDiscreteInput: function(addr, unitID, callback) {
        callback(null, addr % 2 === 0);
    },

    // write single register(기능 코드06)
    set회원가입: function(addr, value, unitID, callback) {
        console.log(`写레지스터: 주소=${addr}, 值=${value}, slave=${unitID}`);
        // 在这里执行实际动作,如控制设备
        callback(null);  // 写入成功
    },

    // write multiple registers(기능 코드16)
    set회원가입Array: function(addr, values, unitID, callback) {
        console.log(`批量写레지스터: 주소=${addr}, 值=${values}`);
        callback(null);
    },

    // write single coil(기능 코드05)
    setCoil: function(addr, value, unitID, callback) {
        console.log(`写Coil: 주소=${addr}, 值=${value}`);
        callback(null);
    },
};

// 启动TCP服务端,监听502端口
server.listenTCP(502, vector, {
    host: "0.0.0.0",
    // 可选:允许的从站ID
    // unitID: 1,
});

console.log("Modbus TCP服务端已启动,监听端口502");
console.log("保持寄存器0-3: temperature25.0°C, 湿度65.0%, 电压220.0V, 电流1.35A");
console.log("按 Ctrl+C Stop");

// 模拟传感器数据实时更新
setInterval(() => {
    // 可以在这里更新内部数据
    // 例如从真实传感器读取数据
}, 1000);

七、批量数据采集与轮询

// poller.js - 工业级批量数据采集器
const ModbusRTU = require("modbus-serial");

class ModbusPoller {
    constructor(config) {
        this.client = new ModbusRTU();
        this.config = config;
        this.데이터 = {};        // 采集到的数据
        this.running = false;
        this.timer = null;
    }

    // connect
    async connect() {
        this.client.setID(this.config.unitId || 1);
        this.client.setTimeout(this.config.timeout || 5000);

        if (this.config.type === "tcp") {
            await this.client.connectTCP(this.config.host, {
                port: this.config.port || 502,
            });
        } else if (this.config.type === "rtu") {
            await this.client.connectRTU(this.config.port, {
                baudRate: this.config.baudRate || 9600,
                parity: this.config.parity || "none",
            });
        }
        console.log("已连接到Modbusdevice");
    }

    // 采集单个寄存器组
    async readGroup(group) {
        try {
            const result = await this.client.readHolding회원가입s(
                group.주소, group.count
            );
            // 按配置解析数据
            group.points.forEach((point, idx) => {
                let value = result.데이터[idx];
                // 应用缩放系数
                if (point.scale) value = value * point.scale;
                // 应用偏移
                if (point.offset) value = value + point.offset;
                // 保留小数位
                if (point.decimals !== undefined) {
                    value = Number(value.toFixed(point.decimals));
                }
                this.데이터[point.name] = value;
            });
            return true;
        } catch (err) {
            console.error(`采集 ${group.name} 失败:`, err.message);
            return false;
        }
    }

    // 启动周期性采集
    시작(intervalMs = 1000) {
        this.running = true;
        const poll = async () => {
            if (!this.running) return;
            for (const group of this.config.groups) {
                await this.readGroup(group);
            }
            // 输出当前数据
            console.log(JSON.stringify(this.데이터, null, 2));
            this.timer = setTimeout(poll, intervalMs);
        };
        poll();
    }

    // Stop
    stop() {
        this.running = false;
        if (this.timer) clearTimeout(this.timer);
        this.client.close();
    }
}

// 配置示例:采集一个智能电表
const config = {
    type: "tcp",
    host: "192.168.1.100",
    port: 502,
    unitId: 1,
    groups: [
        {
            name: "电参数",
            주소: 0,
            count: 6,
            points: [
                { name: "电压", scale: 0.1, decimals: 1, unit: "V" },
                { name: "电流", scale: 0.01, decimals: 2, unit: "A" },
                { name: "有功功率", scale: 0.001, decimals: 3, unit: "kW" },
                { name: "无功功率", scale: 0.001, decimals: 3, unit: "kvar" },
                { name: "功率因数", scale: 0.001, decimals: 3 },
                { name: "频率", scale: 0.01, decimals: 2, unit: "Hz" },
            ],
        },
        {
            name: "电能",
            주소: 100,
            count: 2,
            points: [
                { name: "正向有功电能", scale: 0.01, decimals: 2, unit: "kWh" },
                { name: "反向有功电能", scale: 0.01, decimals: 2, unit: "kWh" },
            ],
        },
    ],
};

// run
async function main() {
    const poller = new ModbusPoller(config);
    await poller.connect();
    poller.시작(2000);  // 每2秒采集一次
}

main().catch(console.error);

八、Modbus转MQTT上云实战

// modbus_mqtt_gateway.js - Modbus转MQTT物联网网关
const ModbusRTU = require("modbus-serial");
const mqtt = require("mqtt");

// ====== 配置 ======
const MODBUS_CONFIG = {
    host: "192.168.1.100",
    port: 502,
    unitId: 1,
};

const MQTT_CONFIG = {
    url: "mqtt://broker.emqx.io",
    port: 1883,
    topic: "modbus/gateway/device001",
    clientId: "modbus_gateway_" + Math.random().toString(16).substr(2, 8),
};

const POLL_INTERVAL = 5000;  // 5秒采集一次

// ====== 数据点配置 ======
const 데이터_POINTS = [
    { name: "temperature", 주소: 0, scale: 0.1, unit: "°C" },
    { name: "humidity", 주소: 1, scale: 0.1, unit: "%" },
    { name: "voltage", 주소: 2, scale: 0.1, unit: "V" },
    { name: "current", 주소: 3, scale: 0.01, unit: "A" },
    { name: "power", 주소: 4, scale: 0.001, unit: "kW" },
    { name: "energy", 주소: 100, scale: 0.01, unit: "kWh" },
];

// ====== 主程序 ======
async function main() {
    // 1. connectModbus
    const mbClient = new ModbusRTU();
    mbClient.setID(MODBUS_CONFIG.unitId);
    mbClient.setTimeout(3000);
    await mbClient.connectTCP(MODBUS_CONFIG.host, { port: MODBUS_CONFIG.port });
    console.log("ModbusConnected");

    // 2. connectMQTT
    const mqttClient = mqtt.connect(MQTT_CONFIG.url, {
        clientId: MQTT_CONFIG.clientId,
        port: MQTT_CONFIG.port,
    });

    mqttClient.on("connect", () => {
        console.log("MQTTConnected");
        // 订阅下行控制主题
        mqttClient.subscribe(MQTT_CONFIG.topic + "/command");
    });

    // 3. 处理下行控制指令(MQTT → Modbus写)
    mqttClient.on("message", async (topic, message) => {
        try {
            const cmd = JSON.parse(message.toString());
            console.log("收到控制指令:", cmd);
            if (cmd.type === "write_register") {
                await mbClient.write회원가입(cmd.주소, cmd.value);
                console.log(`已写入寄存器${cmd.주소}=${cmd.value}`);
            } else if (cmd.type === "write_coil") {
                await mbClient.writeCoil(cmd.주소, cmd.value);
                console.log(`已写入线圈${cmd.주소}=${cmd.value}`);
            }
        } catch (err) {
            console.error("控制指令执行失败:", err.message);
        }
    });

    // 4. 周期性采集并上报(Modbus → MQTT)
    async function pollAndPublish() {
        try {
            const payload = {
                deviceId: "device001",
                timestamp: new Date().toISOString(),
                데이터: {},
            };

            for (const point of 데이터_POINTS) {
                const result = await mbClient.readHolding회원가입s(point.주소, 1);
                let value = result.데이터[0];
                if (point.scale) value = value * point.scale;
                payload.데이터[point.name] = {
                    value: Number(value.toFixed(3)),
                    unit: point.unit,
                };
            }

            // 发布到MQTT
            mqttClient.publish(
                MQTT_CONFIG.topic + "/데이터",
                JSON.stringify(payload),
                { qos: 1 }
            );
            console.log("已上报:", JSON.stringify(payload.데이터));
        } catch (err) {
            console.error("采集上报失败:", err.message);
        }
    }

    // 启动采集
    setInterval(pollAndPublish, POLL_INTERVAL);
    pollAndPublish();  // 立即执行一次
}

main().catch(console.error);

九、错误处理与重连机制

十、常见问题与排查

问题原因解决方法
连接超时IP/端口错误或网络不通ping测试IP,telnet测试502端口,检查防火墙
返回异常码01功能码不支持确认设备支持该功能码,查阅设备手册
返回异常码02寄存器地址不存在检查地址范围,注意设备地址是0-based还是1-based
返回异常码03读取数量超出范围减少读取数量,单次最多125个寄存器
数据值不对endianness/缩放系数错误确认大端/小端模式,核对scale和offset
串口打不开端口被占用或权限不足Linux需sudo或加入dialout组,检查是否被其他程序占用
RTU无响应Baud rate/校验位不匹配核对设备通信参数,检查A/B线是否接反
频繁断连网络不稳定或设备连接数限制增加重连机制,使用长连接,检查设备最大连接数
并发请求冲突同时发送多个请求使用队列串行发送,等待响应后再发下一个
浮点数解析错误字节序不匹配尝试readFloatBE/readFloatLE,确认设备文档

十一、性能优化最佳实践

  • 批量读取:尽量一次读取连续地址的多个寄存器,减少请求次数
  • 长连接复用:不要频繁创建/关闭连接,保持TCP长连接
  • 串行请求:Modbus是半双工协议,必须等待响应后再发下一个请求
  • 合理轮询间隔:根据数据变化频率设置采集周期,不要过于频繁
  • 地址优化:将需要频繁读取的寄存器安排在连续地址,便于批量读取
  • 超时设置:TCP建议3-5秒,RTU建议根据波特率计算(9600波特率约2秒)
  • 错误重试:单次失败不要立即报错,重试1-2次后再上报异常
  • 连接池:多设备场景使用连接池,避免重复握手开销
  • 数据缓存:对变化慢的数据(如电能累计值)降低读取频率,缓存结果
  • 内存管理:长时间运行的采集程序注意内存泄漏,定期重启或监控

十二、其他Node.js Modbus库对比

库名Protocol客户端/服务端特点推荐场景
modbus-serialRTU/TCP/RTU-over-TCP都支持最流行,API简洁,文档完善通用场景,首选
modbus-rtuRTU/TCP客户端支持数据类型自动转换需要复杂数据解析
modbus-streamRTU/TCP都支持基于Stream,灵活需要底层控制
jsmodbusTCP都支持纯TCP,TypeScript支持仅TCPScenarios
node-modbusTCP客户端轻量简单TCP采集

十三、学习资源

  • npm包地址:https://www.npmjs.com/package/modbus-serial
  • GitHubsource code:https://github.com/yaacov/node-modbus-serial
  • APIDocument:https://yaacov.github.io/node-modbus-serial/
  • Modbus协议规范:https://modbus.org/specs.php
  • node-serialportDocument:https://serialport.io/
  • MQTT.jsDocument:https://github.com/mqttjs/MQTT.js

Node.js凭借异步非阻塞的天然优势和modbus-serial库的成熟API,成为开发工业数据采集网关、Modbus转MQTT桥接器和设备模拟服务的理想选择。本文覆盖了从基础连接、数据解析到批量采集、MQTT上云、错误重连的完整开发流程,代码均可直接运行。关键是注意Modbus半双工特性(请求必须串行)、寄存器地址偏移(0-based vs 1-based)和多字节数据的字节序匹配这三个常见坑点。

📦

VIP专属:Node.js Modbus开发完整代码包

modbus-serial库TCP/RTU客户端、服务端模拟、数据解析、MQTT上云网关、健壮重连机制。

Activate VIP即可下载完整代码,同时解锁 30+ 工程实战资料包:调试脚本、速查表、项目模板、排查案例……

前往VIP资料库下载 → 月费仅9.9元 / 年费199元
Put this resource to use in a real project?

Go to the Tool Center for message parsing, CRC 검증 and device debugging, or submit your requirements for selection and integration advice.

Engineer Membership

Turn this article into actionable debugging resources

After activation, you can use advanced message parsing, resource pack downloads, code examples, engineering cases and priority technical support, suitable for real project delivery.

Unlimited Advanced Tools
Resource & Code Packs
Complete Engineering Case Library
Priority Technical Support