Node.js以其异步非阻塞、事件驱动的特性,非常适合开发工业データの収集和Modbus通信ゲートウェイ。modbus-serial是Node.js生态中最流行、最活跃的Modbus库,纯JavaScript実装,サポートModbus RTU(serial port)、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 solution。它同时サポートModbus RTU(経由シリアルポート)、Modbus TCP和RTU-over-TCP3つの伝送方式,提供Promise和回调两种API风格,并内置了Modbusサーバー側。(slave)模拟功能。
1.2 核心特性
- 多协议サポート:Modbus RTU(serial port)、Modbus TCP、RTU-over-TCP
- クライアント側は+サーバー側。:既可以做マスター采集データ,也可以做ステーションからのシミュレーションデバイス
- Promise API:すべての操作戻るPromise,サポートasync/await
- データ型解析:内置32ビット整数、浮動小数点数、ストリング·ストリング等多レジスタのデータの解析
- シリアルポートサポート:基于node-serialport,サポートWindows/Linux/macOS
- 连接管理:自動再接続。、タイムアウト控制、トランザクションID管理
- 纯JavaScript:无需编译原生モジュール(TCPモード),跨プラットフォーム
- 活跃维护:継続的な更新,issueレスポンス快,社区活跃
1.3 サポート的機能コード
| function code | 方法 | 説明 |
|---|---|---|
| 01 | readCoils(addr, count) | リードコイルを読む |
| 02 | readDiscreteInputs(addr, count) | read discrete inputs |
| 03 | readHoldingRegisters(addr, count) | read holding registers |
| 04 | readInputRegisters(addr, count) | read input registers |
| 05 | writeCoil(addr, value) | write single coil |
| 06 | writeRegister(addr, value) | write single register |
| 15 | writeCoils(addr, values) | write multiple coils |
| 16 | writeRegisters(addr, values) | write multiple registers |
| 43 | readDeviceIdentification() | 读デバイス标识(MEI) |
二、インストール与早速スタート。
# インストールmodbus-serial
npm install modbus-serial
# もし需要RTUシリアルポートサポート,还需インストールserialport
npm install serialport
# 验证インストール
node -e "const ModbusRTU = require('modbus-serial'); console.log('modbus-serial已インストール');"
// quick_start.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.readHoldingRegisters(0, 10);
console.log("保持Register:", result.data);
// Write to a single register(address100,值1234)
await client.writeRegister(100, 1234);
console.log("書き込み済み。レジスタ100=1234");
// read81つのコイル
const coils = await client.readCoils(0, 8);
console.log("Coil status:", coils.data);
// 接続を閉じる
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(address, count) {
const result = await this.client.readHoldingRegisters(address, count);
return result.data; // 戻るnumber配列
}
// read input registers
async readInput(address, count) {
const result = await this.client.readInputRegisters(address, count);
return result.data;
}
// リードコイルを読む
async readCoils(address, count) {
const result = await this.client.readCoils(address, count);
return result.data; // 戻るboolean配列
}
// read discrete inputs
async readDiscrete(address, count) {
const result = await this.client.readDiscreteInputs(address, count);
return result.data;
}
// write single register
async writeRegister(address, value) {
await this.client.writeRegister(address, value);
}
// write multiple registers
async writeRegisters(address, values) {
await this.client.writeRegisters(address, values);
}
// write single coil
async writeCoil(address, value) {
await this.client.writeCoil(address, value); // value: true/false
}
// Close
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.writeRegister(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 address
client.setTimeout(5000); // タイムアウト5秒
// Connect serial port(Linux: /dev/ttyUSB0, Windows: COM3, macOS: /dev/tty.usbserial-xxx)
await client.connectRTU("/dev/ttyUSB0", {
baudRate: 9600, // Baud rate
dataBits: 8, // data 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.readHoldingRegisters(0, 10);
console.log("register data:", result.data);
// 読み取り多个スレーブ(バス上多个デバイス)
for (let id = 1; id <= 5; id++) {
client.setID(id);
try {
const data = await client.readHoldingRegisters(0, 4);
console.log(`slave${id}:`, data.data);
} 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位的,但实际デバイス经常用多1つのレジスタ组合存储32ビット整数、浮動小数点数、ストリング·ストリング等データ。modbus-serial提供了buffer工具类来解析这些データ。
// data_types.js - 多レジスタのデータ型解析
const ModbusRTU = require("modbus-serial");
// 方法一:使用modbus-serial内置的bufferTools
async function readDataTypes(client) {
// read81つのレジスタ(足够存放各种データ型)
const result = await client.readHoldingRegisters(0, 8);
const regs = result.data; // [r0, r1, r2, r3, r4, r5, r6, r7]
// ====== 16ビット整数(单1つのレジスタ) ======
const uint16 = regs[0]; // 符号なし。16位
const int16 = regs[0] > 32767 ? regs[0] - 65536 : regs[0]; // 記号付き。16位
// ====== 32ビット整数(两1つのレジスタ) ======
// ビッグエンディアンモード(前の単語。):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ビット浮動小数点数(两1つのレジスタ,IEEE 754) ======
// 使用Buffer转换
const buf = Buffer.alloc(4);
buf.writeUInt16BE(regs[2], 0); // 高字
buf.writeUInt16BE(regs[3], 2); // 低字
const float32 = buf.readFloatBE(0); // ビッグエンディアン浮動小数点数
// ====== 64ビット浮動小数点数(四1つのレジスタ) ======
const buf64 = Buffer.alloc(8);
for (let i = 0; i < 4; i++) {
buf64.writeUInt16BE(regs[4 + i], i * 2);
}
const double64 = buf64.readDoubleBE(0);
// ====== ストリング·ストリング(多1つのレジスタ,すべてのレジスタ。存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的readHoldingRegisters戻る的buffer
// result.buffer 是一个Buffer对象,可以直接使用する。標準方法読み取り
async function readWithBuffer(client) {
const result = await client.readHoldingRegisters(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(function code03)
getHoldingRegister: function(addr, unitID, callback) {
// addr: register address
// unitID: slave address
// 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(function code04)
getInputRegister: function(addr, unitID, callback) {
const inputRegs = [100, 200, 300, 400];
if (addr >= 0 && addr < inputRegs.length) {
callback(null, inputRegs[addr]);
} else {
callback({ exceptionCode: 2 });
}
},
// リードコイルを読む(function code01)
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(function code02)
getDiscreteInput: function(addr, unitID, callback) {
callback(null, addr % 2 === 0);
},
// write single register(function code06)
setRegister: function(addr, value, unitID, callback) {
console.log(`写Register: address=${addr}, 值=${value}, slave=${unitID}`);
// ここで执行实际动作,如控制デバイス
callback(null); // 写入成功
},
// write multiple registers(function code16)
setRegisterArray: function(addr, values, unitID, callback) {
console.log(`一括書き込みき込みRegister: address=${addr}, 值=${values}`);
callback(null);
},
// write single coil(function code05)
setCoil: function(addr, value, unitID, callback) {
console.log(`写Coil: address=${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.data = {}; // 采集到的データ
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");
}
// 采集单1つのレジスタ组
async readGroup(group) {
try {
const result = await this.client.readHoldingRegisters(
group.address, group.count
);
// 按設定解析データ
group.points.forEach((point, idx) => {
let value = result.data[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.data[point.name] = value;
});
return true;
} catch (err) {
console.error(`采集 ${group.name} 失敗:`, err.message);
return false;
}
}
// 启动周期性采集
start(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.data, 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: "电参数",
address: 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: "电能",
address: 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.start(2000); // 每2秒采集一次
}
main().catch(console.error);
八、Modbus转MQTT上云実践
// modbus_mqtt_gateway.js - Modbus转MQTTIoTゲートウェイ
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 DATA_POINTS = [
{ name: "temperature", address: 0, scale: 0.1, unit: "°C" },
{ name: "humidity", address: 1, scale: 0.1, unit: "%" },
{ name: "voltage", address: 2, scale: 0.1, unit: "V" },
{ name: "current", address: 3, scale: 0.01, unit: "A" },
{ name: "power", address: 4, scale: 0.001, unit: "kW" },
{ name: "energy", address: 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.writeRegister(cmd.address, cmd.value);
console.log(`書き込み済み。レジスタ${cmd.address}=${cmd.value}`);
} else if (cmd.type === "write_coil") {
await mbClient.writeCoil(cmd.address, cmd.value);
console.log(`書き込み済み。コイル${cmd.address}=${cmd.value}`);
}
} catch (err) {
console.error("控制指令执行失敗:", err.message);
}
});
// 4. 周期性采集并上报(Modbus → MQTT)
async function pollAndPublish() {
try {
const payload = {
deviceId: "device001",
timestamp: new Date().toISOString(),
data: {},
};
for (const point of DATA_POINTS) {
const result = await mbClient.readHoldingRegisters(point.address, 1);
let value = result.data[0];
if (point.scale) value = value * point.scale;
payload.data[point.name] = {
value: Number(value.toFixed(3)),
unit: point.unit,
};
}
// 发布到MQTT
mqttClient.publish(
MQTT_CONFIG.topic + "/data",
JSON.stringify(payload),
{ qos: 1 }
);
console.log("已上报:", JSON.stringify(payload.data));
} 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 | 読み取り数超出範囲 | 减少読み取り数,最大のシングル。1251つのレジスタ |
| データ值正しくない | endianness/缩放系数エラー | 確認ビッグエンディアン/リトルエンディアンモード,核对scale和offset |
| シリアルポート打不开 | ポート被占用或权限不足 | Linux需sudo或加入dialout组,チェック是否被其他程序占用 |
| RTU応答なし。 | Baud rate/チェックビット不マッチング | 核对デバイス通信パラメータ,チェックA/B线是否接反 |
| 频繁断连 | 网络不稳定或デバイス连接数限制 | 增加再接続机制,使用长连接,デバイスの最大接続数の |
| 并发リクエスト冲突 | 同时送信多个リクエスト | 使用队列串行送信,待機中レスポンス后再发下一个 |
| 浮動小数点数解析エラー | バイトオーダー不マッチング | 尝试readFloatBE/readFloatLE,機器の確認ドキュメント |
十一、パフォーマンス最適化ベストプラクティス
- バッチ読み取り:尽量一次読み取り连续アドレス的多1つのレジスタ,减少リクエスト次数
- 长连接复用:不要频繁作成/接続を閉じる,保持TCP长连接
- 串行リクエスト:Modbus是ハーフデュプレックス协议,必须待機中レスポンス后再发下一个リクエスト
- 合理ポーリング間隔:根据データ变化频率設定采集周期,不要过于频繁
- アドレス优化:将需要频繁読み取り的レジスタ安排在连续アドレス,便于バッチ読み取り
- タイムアウトの設定:TCP建议3-5秒,RTU建议根据ボーレート計算(9600ボーレート约2秒)
- エラー重试:单次失敗不要立即报错,重试1-2次后再上报異常
- 连接池:多デバイス场景使用连接池,避免重复握手开销
- データ缓存:对变化慢的データ(如电能累计值)降低読み取り频率,缓存结果
- 内存管理:长时间运行的采集程序注意内存泄漏,定期再起動或モニタリング
十二、其他Node.js Modbus库对比
| 库名 | Protocol | クライアント側は/サーバー側。 | 特点 | おすすめ场景 |
|---|---|---|---|---|
| modbus-serial | RTU/TCP/RTU-over-TCP | 都サポート | 最流行,API简洁,ドキュメント完善 | 通用场景,首选 |
| modbus-rtu | RTU/TCP | クライアント側は | サポートデータ型自动转换 | 需要复杂データの解析 |
| modbus-stream | RTU/TCP | 都サポート | 基于Stream,灵活 | 需要底层控制 |
| jsmodbus | TCP | 都サポート | 纯TCP,TypeScriptサポート | 仅TCPScenarios |
| node-modbus | TCP | クライアント側は | 轻量 | 简单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)和多Bytesのデータ的バイトオーダーマッチング这三个よくある穴点。
VIP专属:Node.js Modbus完全なコードパッケージの開発
modbus-serial库TCP/RTUクライアント側は、サーバ側シミュレーション、データの解析、MQTTトップクラウドゲートウェイ、強力な再接続メカニズム。
Activate VIP即可下载完全代码,同时解锁 30+ 工程実践パッケージの内容:スクリプトのデバッグ、クイックチェック·テーブル、项目模板、トラブルシューティング案例……
前往VIP资料库下载 → 月费仅9.9元 / 年费199元