njs-modbus
v2.1.0
Published
A pure JavaScript implemetation of Modbus for NodeJS.
Readme
njs-modbus
A pure JavaScript implementation of MODBUS for NodeJS.
Introduction
njs-modbus is designed as a layered architecture, including the physical layer and the application layer:
- Physical layer implements Serial Port, TCP/IP and UDP/IP.
- Application layer implements RTU, ASCII and TCP.
njs-modbus provide both client and server.
Features
- Full modbus standard protocol implementation
- Support for custom function codes
- Support broadcasting
- Very lightweight project
- Full typescript
Supported function codes
| Code | | | ----- | ----------------------------- | | 01 | Read Coils | | 02 | Read Discrete Inputs | | 03 | Read Holding Registers | | 04 | Read Input Register | | 05 | Write Single Coil | | 06 | Write Single Register | | 15 | Write Multiple Coils | | 16 | Write Multiple Registers | | 17 | Report Server ID | | 22 | Mask Write Register | | 23 | Read/Write Multiple Registers | | 43/14 | Read device Identification |
Supported protocols
- Modbus RTU
- Modbus ASCII
- Modbus TCP/IP
- Modbus UDP/IP
- Modbus RTU/ASCII Over TCP/IP
- Modbus RTU/ASCII Over UDP/IP
Installation
npm install njs-modbusExamples
Modbus RTU Master
import { SerialPhysicalLayer, RtuApplicationLayer, ModbusMaster } from 'njs-modbus';
const physicalLayer = new SerialPhysicalLayer({ path: 'COM1', baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1 });
const applicationLayer = new RtuApplicationLayer(physicalLayer);
const modbusMaster = new ModbusMaster(applicationLayer, physicalLayer);
modbusMaster
.open()
.then(() => {
console.log('opened');
modbusMaster.readHoldingRegisters(1, 0, 10).then((res) => {
console.log(res);
});
})
.catch((error) => {
console.log(error);
});Modbus RTU Slave
import type { ModbusSlaveModel } from 'njs-modbus';
import { SerialPhysicalLayer, RtuApplicationLayer, ModbusSlave } from 'njs-modbus';
const physicalLayer = new SerialPhysicalLayer({ path: 'COM1', baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1 });
const applicationLayer = new RtuApplicationLayer(physicalLayer);
const slave1Data = {
discreteInputs: new Map<number, boolean>(),
coils: new Map<number, boolean>(),
inputRegisters: new Map<number, number>(),
holdingRegisters: new Map<number, number>(),
};
const slave1: ModbusSlaveModel = {
readDiscreteInputs: (address, length) => {
return Array.from({ length }).map((_, i) => {
const discreteInput = slave1Data.discreteInputs.get(address + i);
if (typeof discreteInput === 'undefined') {
return false;
}
return discreteInput;
});
},
readCoils: (address, length) => {
return Array.from({ length }).map((_, i) => {
const coil = slave1Data.coils.get(address + i);
if (typeof coil === 'undefined') {
return false;
}
return coil;
});
},
writeSingleCoil: (address, value) => {
slave1Data.coils.set(address, value);
},
readInputRegisters: (address, length) => {
return Array.from({ length }).map((_, i) => {
const inputRegister = slave1Data.inputRegisters.get(address + i);
if (typeof inputRegister === 'undefined') {
return 0;
}
return inputRegister;
});
},
readHoldingRegisters: (address, length) => {
return Array.from({ length }).map((_, i) => {
const holdingRegister = slave1Data.holdingRegisters.get(address + i);
if (typeof holdingRegister === 'undefined') {
return 0;
}
return holdingRegister;
});
},
writeSingleRegister: (address, value) => {
slave1Data.holdingRegisters.set(address, value);
},
reportServerId: () => ({ additionalData: [1, 2, 3] }),
readDeviceIdentification: () => ({
0x00: 'Basic:VendorName',
0x01: 'Basic:ProductCode',
0x02: 'Basic:MajorMinorRevision',
0x03: 'Regular:VendorUrl',
0x04: 'Regular:ProductName',
0x05: 'Regular:ModelName',
0x06: 'Regular:UserApplicationName',
0x80: 'Extended:Extended',
0xff: 'Extended:Extended',
}),
};
const modbusSlave = new ModbusSlave(applicationLayer, physicalLayer);
modbusSlave.add(slave1);
modbusSlave
.open()
.then(() => {
console.log('opened');
})
.catch((error) => {
console.log(error);
});For more advanced examples, check out examples included in the repository. If you have created any utilities that meet a specific need, feel free to submit them so others can benefit.
Broadcasts (unit = 0)
Slaves never respond to broadcast requests, so the master's write*(0, ...) Promise resolves as soon as the bytes are flushed to the wire.
If you broadcast over serial (RTU or ASCII), per Modbus over Serial Line V1.02 §2.4.1 you must wait a turnaround delay before sending the next request — slow slaves need time to apply the broadcast write that produced no response. The library does not insert this delay automatically because the right value is workload-specific (fast sensors vs. PLCs writing to flash differ by orders of magnitude). Insert it yourself in your call site:
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
await master.writeSingleRegister(0, 0x0000, 0x1234); // broadcast
await sleep(100); // turnaround — tune per devices
await master.writeSingleRegister(1, 0x0000, 0x5678); // unicast to next slaveA safe lower bound is the RTU t3.5 inter-frame silence (e.g. ~4 ms at 9600 baud, ~1.75 ms above 19200 baud). Many real-world PLCs need 50–100 ms after a broadcast write. Modbus TCP/UDP do not require this delay (TCP gives synchronous acks; broadcasting on TCP is uncommon anyway).
Contributing
Please read our contributing guide first.
