njs-modbus
Version:
A pure JavaScript implemetation of Modbus for NodeJS.
1,391 lines (1,374 loc) • 85 kB
JavaScript
'use strict';
var EventEmitter = require('node:events');
var serialport = require('serialport');
var node_net = require('node:net');
var node_dgram = require('node:dgram');
exports.ErrorCode = void 0;
(function (ErrorCode) {
ErrorCode[ErrorCode["ILLEGAL_FUNCTION"] = 1] = "ILLEGAL_FUNCTION";
ErrorCode[ErrorCode["ILLEGAL_DATA_ADDRESS"] = 2] = "ILLEGAL_DATA_ADDRESS";
ErrorCode[ErrorCode["ILLEGAL_DATA_VALUE"] = 3] = "ILLEGAL_DATA_VALUE";
ErrorCode[ErrorCode["SERVER_DEVICE_FAILURE"] = 4] = "SERVER_DEVICE_FAILURE";
ErrorCode[ErrorCode["ACKNOWLEDGE"] = 5] = "ACKNOWLEDGE";
ErrorCode[ErrorCode["SERVER_DEVICE_BUSY"] = 6] = "SERVER_DEVICE_BUSY";
ErrorCode[ErrorCode["MEMORY_PARITY_ERROR"] = 8] = "MEMORY_PARITY_ERROR";
ErrorCode[ErrorCode["GATEWAY_PATH_UNAVAILABLE"] = 10] = "GATEWAY_PATH_UNAVAILABLE";
ErrorCode[ErrorCode["GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND"] = 11] = "GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND";
})(exports.ErrorCode || (exports.ErrorCode = {}));
const PREFIX = 'MODBUS_ERROR_CODE_';
function getErrorByCode(code) {
return new Error(PREFIX + code);
}
function getCodeByError(err) {
if (err.message.startsWith(PREFIX)) {
return Number(err.message.slice(PREFIX.length));
}
return exports.ErrorCode.SERVER_DEVICE_FAILURE;
}
class AbstractPhysicalLayer extends EventEmitter {
}
class SerialPhysicalLayer extends AbstractPhysicalLayer {
get isOpen() {
return this._serialport.isOpen;
}
get destroyed() {
return this._destroyed;
}
get baudRate() {
return this._baudRate;
}
constructor(options) {
super();
Object.defineProperty(this, "TYPE", {
enumerable: true,
configurable: true,
writable: true,
value: 'SERIAL'
});
Object.defineProperty(this, "_serialport", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_destroyed", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
Object.defineProperty(this, "_baudRate", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
this._serialport = new serialport.SerialPort(Object.assign(Object.assign({}, options), { autoOpen: false }));
this._baudRate = options.baudRate;
}
open() {
if (this.destroyed) {
return Promise.reject(new Error('Port is destroyed'));
}
return new Promise((resolve, reject) => {
this._serialport.open((error) => {
if (error) {
reject(error);
}
else {
this._serialport.on('data', (data) => {
this.emit('data', data, (data) => this.write(data));
});
this._serialport.on('error', (error) => {
this.emit('error', error);
});
this._serialport.on('close', () => {
this._serialport.removeAllListeners();
this.emit('close');
});
resolve();
}
});
});
}
write(data) {
return new Promise((resolve, reject) => {
if (this.isOpen) {
this._serialport.write(data, (error) => {
if (error) {
reject(error);
}
else {
this.emit('write', data);
resolve();
}
});
}
else {
reject(new Error('Port is not open'));
}
});
}
close() {
return new Promise((resolve) => {
this._serialport.removeAllListeners();
this._serialport.close(() => {
resolve();
});
});
}
destroy() {
this._destroyed = true;
this.removeAllListeners();
return this.close();
}
}
class TcpClientPhysicalLayer extends AbstractPhysicalLayer {
get isOpen() {
return this._isOpen;
}
get destroyed() {
return this._destroyed;
}
constructor(options) {
super();
Object.defineProperty(this, "TYPE", {
enumerable: true,
configurable: true,
writable: true,
value: 'NET'
});
Object.defineProperty(this, "_socket", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_isOpen", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
Object.defineProperty(this, "_destroyed", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
this._socket = new node_net.Socket(options);
}
open(options) {
if (this.destroyed) {
return Promise.reject(new Error('Port is destroyed'));
}
return new Promise((resolve, reject) => {
let called = false;
this._socket.connect(options !== null && options !== void 0 ? options : { port: 502 }, () => {
called = true;
this._isOpen = true;
this._socket.on('data', (data) => {
this.emit('data', data, (data) => this.write(data));
});
this._socket.on('close', () => {
this._isOpen = false;
this._socket.removeAllListeners();
this.emit('close');
});
resolve();
});
this._socket.on('error', (error) => {
if (called) {
this.emit('error', error);
}
else {
reject(error);
}
});
});
}
write(data) {
return new Promise((resolve, reject) => {
if (this.isOpen) {
this._socket.write(data, (error) => {
if (error) {
reject(error);
}
else {
this.emit('write', data);
resolve();
}
});
}
else {
reject(new Error('Port is not open'));
}
});
}
close() {
return new Promise((resolve) => {
this._isOpen = false;
this._socket.removeAllListeners();
this._socket.destroy();
resolve();
});
}
destroy() {
this._destroyed = true;
this.removeAllListeners();
return this.close();
}
}
class TcpServerPhysicalLayer extends AbstractPhysicalLayer {
get isOpen() {
return this._isOpen;
}
get destroyed() {
return this._destroyed;
}
constructor(options) {
super();
Object.defineProperty(this, "TYPE", {
enumerable: true,
configurable: true,
writable: true,
value: 'NET'
});
Object.defineProperty(this, "_server", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_isOpen", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
Object.defineProperty(this, "_destroyed", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
Object.defineProperty(this, "_sockets", {
enumerable: true,
configurable: true,
writable: true,
value: new Set()
});
this._server = node_net.createServer(options, (socket) => {
this._sockets.add(socket);
socket.on('data', (data) => {
this.emit('data', data, (data) => new Promise((resolve, reject) => {
socket.write(data, (error) => {
if (error) {
reject(error);
}
else {
resolve();
}
});
}));
});
socket.once('close', () => {
socket.removeAllListeners();
this._sockets.delete(socket);
});
});
}
open(options) {
if (this.destroyed) {
return Promise.reject(new Error('Port is destroyed'));
}
return new Promise((resolve, reject) => {
var _a;
let called = false;
this._server.listen(Object.assign(Object.assign({}, options), { port: (_a = options === null || options === void 0 ? void 0 : options.port) !== null && _a !== void 0 ? _a : 502 }), () => {
called = true;
this._isOpen = true;
this._sockets.clear();
this._server.on('close', () => {
this._isOpen = false;
this._server.removeAllListeners();
for (const socket of this._sockets) {
socket.removeAllListeners();
}
this.emit('close');
});
resolve();
});
this._server.on('error', (error) => {
if (called) {
this.emit('error', error);
}
else {
reject(error);
}
});
});
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
write(data) {
return new Promise((resolve, reject) => {
reject(new Error('Not supported'));
});
}
close() {
return new Promise((resolve) => {
this._isOpen = false;
this._server.removeAllListeners();
for (const socket of this._sockets) {
socket.removeAllListeners();
}
this._server.close(() => {
resolve();
});
});
}
destroy() {
this._destroyed = true;
this.removeAllListeners();
return this.close();
}
}
class UdpPhysicalLayer extends AbstractPhysicalLayer {
get isOpen() {
return this._isOpen;
}
get destroyed() {
return this._destroyed;
}
/**
*
* @param options
* @param remote If omitted, as server.
* Otherwise as client.
*/
constructor(options, remote) {
var _a, _b;
super();
Object.defineProperty(this, "TYPE", {
enumerable: true,
configurable: true,
writable: true,
value: 'NET'
});
Object.defineProperty(this, "_socket", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_isOpen", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
Object.defineProperty(this, "_destroyed", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
Object.defineProperty(this, "_port", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_address", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "isServer", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
this._socket = node_dgram.createSocket(Object.assign(Object.assign({}, options), { type: (_a = options === null || options === void 0 ? void 0 : options.type) !== null && _a !== void 0 ? _a : 'udp4' }), (msg, rinfo) => {
this.emit('data', msg, (data) => new Promise((resolve, reject) => {
this._socket.send(data, rinfo.port, rinfo.address, (error) => {
if (error) {
reject(error);
}
else {
resolve();
}
});
}));
});
this.isServer = !remote;
this._port = (_b = remote === null || remote === void 0 ? void 0 : remote.port) !== null && _b !== void 0 ? _b : 502;
this._address = remote === null || remote === void 0 ? void 0 : remote.address;
}
open(options) {
if (this.destroyed) {
return Promise.reject(new Error('Port is destroyed'));
}
return new Promise((resolve, reject) => {
var _a;
if (this.isServer) {
let called = false;
this._socket.bind(Object.assign(Object.assign({}, options), { port: (_a = options === null || options === void 0 ? void 0 : options.port) !== null && _a !== void 0 ? _a : 502 }), () => {
called = true;
this._isOpen = true;
this._socket.on('close', () => {
this._isOpen = false;
this._socket.removeAllListeners();
this.emit('close');
});
resolve();
});
this._socket.on('error', (error) => {
if (called) {
this.emit('error', error);
}
else {
reject(error);
}
});
}
else {
this._isOpen = true;
resolve();
}
});
}
write(data) {
return new Promise((resolve, reject) => {
if (this.isOpen) {
this._socket.send(data, this._port, this._address, (error) => {
if (error) {
reject(error);
}
else {
this.emit('write', data);
resolve();
}
});
}
else {
reject(new Error('Port is not open'));
}
});
}
close() {
return new Promise((resolve) => {
this._isOpen = false;
this._socket.removeAllListeners();
this._socket.close(() => {
resolve();
});
});
}
destroy() {
this._destroyed = true;
this.removeAllListeners();
return this.close();
}
}
class AbstractApplicationLayer extends EventEmitter {
}
function checkRange(value, range) {
if (range) {
if (typeof range[0] === 'number' && typeof range[1] === 'number') {
if (range[0] < range[1]) {
return (Array.isArray(value) ? value : [value]).every((n) => n >= range[0] && n <= range[1]);
}
}
else if (range.length > 0) {
for (const r of range) {
if (r[0] < r[1]) {
if ((Array.isArray(value) ? value : [value]).every((n) => n >= r[0] && n <= r[1])) {
return true;
}
}
}
return false;
}
}
return true;
}
const TABLE = [
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, 0xcc01,
0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, 0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, 0xd801, 0x18c0,
0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581,
0x1540, 0xd701, 0x17c0, 0x1680, 0xd641, 0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040, 0xf001, 0x30c0, 0x3180, 0xf141,
0x3300, 0xf3c1, 0xf281, 0x3240, 0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441, 0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01,
0x3fc0, 0x3e80, 0xfe41, 0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840, 0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0,
0x2a80, 0xea41, 0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40, 0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681,
0x2640, 0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041, 0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441, 0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41, 0xaa01,
0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840, 0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41, 0xbe01, 0x7ec0,
0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40, 0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640, 0x7200, 0xb2c1, 0xb381,
0x7340, 0xb101, 0x71c0, 0x7080, 0xb041, 0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241, 0x9601, 0x56c0, 0x5780, 0x9741,
0x5500, 0x95c1, 0x9481, 0x5440, 0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40, 0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901,
0x59c0, 0x5880, 0x9841, 0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0,
0x4c80, 0x8c41, 0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081,
0x4040,
];
function crc(data) {
let crc = 0xffff;
for (let index = 0; index < data.length; index++) {
crc = (TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8)) & 0xffff;
}
return crc;
}
/**
* Get time interval between message frames witch well-known as 3.5T.
* @param baudRate Serial port baud rate.
* @param {number} [approximation=48] Approximate number of bits corresponding to 3.5T.
* @returns `ms`.
*/
function getThreePointFiveT(baudRate, approximation = 48) {
return (approximation * 1000) / baudRate;
}
function lrc(data) {
return (~data.reduce((sum, n) => sum + n, 0) + 1) & 0xff;
}
const MAX_FRAME_LENGTH = 256;
class RtuApplicationLayer extends AbstractApplicationLayer {
constructor(physicalLayer,
/**
* The time interval between two frames, support two formats:
* - bit: `48bit` as default
* - millisecond: `20ms`
*/
intervalBetweenFrames) {
super();
Object.defineProperty(this, "_waitingResponse", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_timerThreePointFive", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_bufferRx", {
enumerable: true,
configurable: true,
writable: true,
value: Buffer.alloc(0)
});
Object.defineProperty(this, "_removeAllListeners", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
let threePointFiveT = 0;
if (physicalLayer.TYPE === 'SERIAL') {
if (intervalBetweenFrames && intervalBetweenFrames.endsWith('ms')) {
threePointFiveT = Number(intervalBetweenFrames.slice(0, -2));
}
else {
threePointFiveT = Math.ceil(physicalLayer.baudRate > 19200
? 1.8
: getThreePointFiveT(physicalLayer.baudRate, intervalBetweenFrames ? Number(intervalBetweenFrames.slice(0, -3)) : 48));
}
}
const handleData = (data, response) => {
this._bufferRx = Buffer.concat([this._bufferRx, data]);
clearTimeout(this._timerThreePointFive);
const handleData = () => {
this.framing(this._bufferRx, (error, frame) => {
if (this._waitingResponse) {
if (error && error.message === 'Insufficient data length') {
return;
}
this._waitingResponse.callback(error, frame);
this._bufferRx = Buffer.alloc(0);
}
else {
if (!error) {
this.emit('framing', frame, response);
}
this._bufferRx = Buffer.alloc(0);
}
});
};
if (this._bufferRx.length >= MAX_FRAME_LENGTH) {
handleData();
}
else {
if (threePointFiveT) {
this._timerThreePointFive = setTimeout(handleData, threePointFiveT);
}
else {
handleData();
}
}
};
physicalLayer.on('data', handleData);
this._removeAllListeners.push(() => {
physicalLayer.removeListener('data', handleData);
});
const handleClose = () => {
clearTimeout(this._timerThreePointFive);
this._bufferRx = Buffer.alloc(0);
};
physicalLayer.on('close', handleClose);
this._removeAllListeners.push(() => {
physicalLayer.removeListener('close', handleClose);
});
}
framing(buffer, callback) {
if (buffer.length >= 4) {
const frame = {
unit: buffer[0],
fc: buffer[1],
data: Array.from(buffer.subarray(2, buffer.length - 2)),
buffer,
};
if (this._waitingResponse) {
for (const check of this._waitingResponse.preCheck) {
const res = check(frame);
if (typeof res === 'undefined') {
callback(new Error('Insufficient data length'));
return;
}
if (typeof res === 'number') {
if (frame.data.length < res) {
callback(new Error('Insufficient data length'));
return;
}
if (frame.data.length !== res) {
callback(new Error('Invalid response'));
return;
}
}
if (!res) {
callback(new Error('Invalid response'));
return;
}
}
}
const crcPassed = buffer.readUInt16LE(buffer.length - 2) === crc(buffer.subarray(0, buffer.length - 2));
if (crcPassed) {
callback(null, frame);
}
else {
callback(new Error('CRC check failed'));
}
}
else {
callback(new Error('Insufficient data length'));
}
}
startWaitingResponse(preCheck, callback) {
this._waitingResponse = { preCheck, callback };
clearTimeout(this._timerThreePointFive);
this._bufferRx = Buffer.alloc(0);
}
stopWaitingResponse() {
this._waitingResponse = undefined;
}
encode(data) {
const buffer = Buffer.alloc(data.data.length + 4);
buffer.writeUInt8(data.unit, 0);
buffer.writeUInt8(data.fc, 1);
data.data.forEach((num, index) => {
buffer.writeUInt8(num, 2 + index);
});
buffer.writeUInt16LE(crc(buffer.subarray(0, -2)), buffer.length - 2);
return buffer;
}
destroy() {
this.removeAllListeners();
for (const removeAllListener of this._removeAllListeners) {
removeAllListener();
}
clearTimeout(this._timerThreePointFive);
}
}
const CHAR_CODE = {
COLON: ':'.charCodeAt(0),
CR: '\r'.charCodeAt(0),
LF: '\n'.charCodeAt(0),
};
class AsciiApplicationLayer extends AbstractApplicationLayer {
constructor(physicalLayer) {
super();
Object.defineProperty(this, "_waitingResponse", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_status", {
enumerable: true,
configurable: true,
writable: true,
value: 'idle'
});
Object.defineProperty(this, "_frame", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
Object.defineProperty(this, "_removeAllListeners", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
const handleData = (data, response) => {
data.forEach((value) => {
switch (this._status) {
case 'idle': {
if (value === CHAR_CODE.COLON) {
this._status = 'reception';
this._frame = [];
}
break;
}
case 'reception': {
if (value === CHAR_CODE.COLON) {
this._frame = [];
}
else if (value === CHAR_CODE.CR) {
this._status = 'waiting end';
}
else {
this._frame.push(value);
}
break;
}
case 'waiting end': {
if (value === CHAR_CODE.COLON) {
this._status = 'reception';
this._frame = [];
}
else {
this._status = 'idle';
if (value === CHAR_CODE.LF) {
this.framing(Buffer.from(this._frame), (error, frame) => {
if (this._waitingResponse) {
this._waitingResponse.callback(error, frame);
}
else if (!error) {
this.emit('framing', frame, response);
}
});
}
}
break;
}
}
});
};
physicalLayer.on('data', handleData);
this._removeAllListeners.push(() => {
physicalLayer.removeListener('data', handleData);
});
const handleClose = () => {
this._status = 'idle';
this._frame = [];
};
physicalLayer.on('close', handleClose);
this._removeAllListeners.push(() => {
physicalLayer.removeListener('close', handleClose);
});
}
framing(_buffer, callback) {
if (_buffer.length >= 6) {
if (_buffer.length % 2 === 0) {
const ascii = [];
let num = '';
for (const value of _buffer) {
num += String.fromCharCode(value);
if (num.length === 2) {
ascii.push(Number('0x' + num));
num = '';
}
}
const buffer = Buffer.from(ascii);
const frame = {
unit: buffer[0],
fc: buffer[1],
data: Array.from(buffer.subarray(2, buffer.length - 1)),
buffer: _buffer,
};
if (this._waitingResponse) {
for (const check of this._waitingResponse.preCheck) {
const res = check(frame);
if (typeof res === 'undefined') {
callback(new Error('Insufficient data length'));
return;
}
if (typeof res === 'number') {
if (frame.data.length < res) {
callback(new Error('Insufficient data length'));
return;
}
if (frame.data.length !== res) {
callback(new Error('Invalid response'));
return;
}
}
if (!res) {
callback(new Error('Invalid response'));
return;
}
}
}
const lrcPassed = buffer[buffer.length - 1] === lrc(buffer.subarray(0, buffer.length - 1));
if (lrcPassed) {
callback(null, frame);
}
else {
callback(new Error('LRC check failed'));
}
}
else {
callback(new Error('Invalid data'));
}
}
else {
callback(new Error('Insufficient data length'));
}
}
startWaitingResponse(preCheck, callback) {
this._waitingResponse = { preCheck, callback };
this._status = 'idle';
this._frame = [];
}
stopWaitingResponse() {
this._waitingResponse = undefined;
}
encode(data) {
const buffer = Buffer.alloc(data.data.length + 3);
buffer.writeUInt8(data.unit, 0);
buffer.writeUInt8(data.fc, 1);
data.data.forEach((num, index) => {
buffer.writeUInt8(num, 2 + index);
});
buffer.writeUInt8(lrc(buffer.subarray(0, -1)), buffer.length - 1);
let frame = ':';
for (const value of buffer) {
frame += value.toString(16).toUpperCase().padStart(2, '0');
}
frame += '\r\n';
return Buffer.from(frame);
}
destroy() {
this.removeAllListeners();
for (const removeAllListener of this._removeAllListeners) {
removeAllListener();
}
}
}
class TcpApplicationLayer extends AbstractApplicationLayer {
constructor(physicalLayer) {
super();
Object.defineProperty(this, "_waitingResponse", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_transactionId", {
enumerable: true,
configurable: true,
writable: true,
value: 1
});
Object.defineProperty(this, "_removeAllListeners", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
const handleData = (data, response) => {
this.framing(data, (error, frame) => {
if (this._waitingResponse) {
this._waitingResponse.callback(error, frame);
}
else if (!error) {
this.emit('framing', frame, response);
}
});
};
physicalLayer.on('data', handleData);
this._removeAllListeners.push(() => {
physicalLayer.removeListener('data', handleData);
});
}
framing(buffer, callback) {
if (buffer.length >= 8) {
if (buffer[2] === 0 && buffer[3] === 0 && buffer.readUInt16BE(4) === buffer.length - 6) {
const frame = {
transaction: buffer.readUInt16BE(0),
unit: buffer[6],
fc: buffer[7],
data: Array.from(buffer.subarray(8)),
buffer,
};
if (this._waitingResponse) {
for (const check of this._waitingResponse.preCheck) {
const res = check(frame);
if (typeof res === 'undefined') {
callback(new Error('Insufficient data length'));
return;
}
if (typeof res === 'number') {
if (frame.data.length < res) {
callback(new Error('Insufficient data length'));
return;
}
if (frame.data.length !== res) {
callback(new Error('Invalid response'));
return;
}
}
if (!res) {
callback(new Error('Invalid response'));
return;
}
}
}
callback(null, frame);
}
else {
callback(new Error('Invalid data'));
}
}
else {
callback(new Error('Insufficient data length'));
}
}
startWaitingResponse(preCheck, callback) {
this._waitingResponse = { preCheck, callback };
}
stopWaitingResponse() {
this._waitingResponse = undefined;
}
encode(data) {
var _a;
const buffer = Buffer.alloc(data.data.length + 8);
buffer.writeUInt16BE((_a = data.transaction) !== null && _a !== void 0 ? _a : this._transactionId, 0);
buffer.writeUInt16BE(0, 2);
buffer.writeUInt16BE(data.data.length + 2, 4);
buffer.writeUInt8(data.unit, 6);
buffer.writeUInt8(data.fc, 7);
data.data.forEach((num, index) => {
buffer.writeUInt8(num, 8 + index);
});
this._transactionId = (this._transactionId + 1) % 256 || 1;
return buffer;
}
destroy() {
this.removeAllListeners();
for (const removeAllListener of this._removeAllListeners) {
removeAllListener();
}
}
}
class ModbusMaster extends EventEmitter {
get isOpen() {
return this.physicalLayer.isOpen;
}
get destroyed() {
return this.physicalLayer.destroyed;
}
constructor(applicationLayer, physicalLayer, timeout = 1000) {
super();
Object.defineProperty(this, "applicationLayer", {
enumerable: true,
configurable: true,
writable: true,
value: applicationLayer
});
Object.defineProperty(this, "physicalLayer", {
enumerable: true,
configurable: true,
writable: true,
value: physicalLayer
});
Object.defineProperty(this, "timeout", {
enumerable: true,
configurable: true,
writable: true,
value: timeout
});
Object.defineProperty(this, "writeFC1", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "writeFC2", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "writeFC3", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "writeFC4", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "writeFC5", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "writeFC6", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "writeFC15", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "writeFC16", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "handleFC17", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "handleFC22", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "handleFC23", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "handleFC43_14", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
this.writeFC1 = this.readCoils;
this.writeFC2 = this.readDiscreteInputs;
this.writeFC3 = this.readHoldingRegisters;
this.writeFC4 = this.readInputRegisters;
this.writeFC5 = this.writeSingleCoil;
this.writeFC6 = this.writeSingleRegister;
this.writeFC15 = this.writeMultipleCoils;
this.writeFC16 = this.writeMultipleRegisters;
this.handleFC17 = this.reportServerId;
this.handleFC22 = this.maskWriteRegister;
this.handleFC23 = this.readAndWriteMultipleRegisters;
this.handleFC43_14 = this.readDeviceIdentification;
physicalLayer.on('error', (error) => {
this.emit('error', error);
});
physicalLayer.on('close', () => {
this.emit('close');
});
}
waitResponse(request, response, timeout) {
return new Promise((resolve, reject) => {
this.physicalLayer
.write(request.data)
.then(() => {
if (request.broadcast) {
resolve();
}
else {
const tid = setTimeout(() => {
this.applicationLayer.stopWaitingResponse();
reject(new Error('Timeout'));
}, timeout);
this.applicationLayer.startWaitingResponse(response.preCheck, (error, frame) => {
clearTimeout(tid);
this.applicationLayer.stopWaitingResponse();
if (error) {
reject(error);
}
else {
resolve(frame);
}
});
}
})
.catch((error) => {
reject(error);
});
});
}
writeFC1Or2(unit, fc, address, length, timeout) {
const byteCount = Math.ceil(length / 8);
const bufferTx = Buffer.alloc(4);
bufferTx.writeUInt16BE(address, 0);
bufferTx.writeUInt16BE(length, 2);
return this.waitResponse({
data: this.applicationLayer.encode({
unit,
fc,
data: Array.from(bufferTx),
}),
broadcast: unit === 0,
}, {
preCheck: [(frame) => frame.unit === unit && frame.fc === fc, () => 1 + byteCount, (frame) => frame.data[0] === byteCount],
}, timeout).then((frame) => {
if (frame) {
return Object.assign(Object.assign({}, frame), { data: Array.from({ length }).map((_, index) => (frame.data[1 + ~~(index / 8)] & (1 << index % 8)) > 0) });
}
});
}
readCoils(unit, address, length, timeout = this.timeout) {
return this.writeFC1Or2(unit, 0x01, address, length, timeout);
}
readDiscreteInputs(unit, address, length, timeout = this.timeout) {
return this.writeFC1Or2(unit, 0x02, address, length, timeout);
}
writeFC3Or4(unit, fc, address, length, timeout) {
const byteCount = length * 2;
const bufferTx = Buffer.alloc(4);
bufferTx.writeUInt16BE(address, 0);
bufferTx.writeUInt16BE(length, 2);
return this.waitResponse({
data: this.applicationLayer.encode({
unit,
fc,
data: Array.from(bufferTx),
}),
broadcast: unit === 0,
}, {
preCheck: [(frame) => frame.unit === unit && frame.fc === fc, () => 1 + byteCount, (frame) => frame.data[0] === byteCount],
}, timeout).then((frame) => {
if (frame) {
const bufferRx = Buffer.from(frame.data.slice(1));
return Object.assign(Object.assign({}, frame), { data: Array.from({ length }).map((_, index) => bufferRx.readUInt16BE(index * 2)) });
}
});
}
readHoldingRegisters(unit, address, length, timeout = this.timeout) {
return this.writeFC3Or4(unit, 0x03, address, length, timeout);
}
readInputRegisters(unit, address, length, timeout = this.timeout) {
return this.writeFC3Or4(unit, 0x04, address, length, timeout);
}
writeSingleCoil(unit, address, value, timeout = this.timeout) {
const fc = 0x05;
const bufferTx = Buffer.alloc(4);
bufferTx.writeUInt16BE(address, 0);
bufferTx.writeUInt16BE(value ? 0xff00 : 0x0000, 2);
return this.waitResponse({
data: this.applicationLayer.encode({
unit,
fc,
data: Array.from(bufferTx),
}),
broadcast: unit === 0,
}, {
preCheck: [
(frame) => frame.unit === unit && frame.fc === fc,
() => bufferTx.length,
(frame) => frame.data.every((v, i) => v === bufferTx[i]),
],
}, timeout).then((frame) => {
if (frame) {
return Object.assign(Object.assign({}, frame), { data: value });
}
});
}
writeSingleRegister(unit, address, value, timeout = this.timeout) {
const fc = 0x06;
const bufferTx = Buffer.alloc(4);
bufferTx.writeUInt16BE(address, 0);
bufferTx.writeUInt16BE(value, 2);
return this.waitResponse({
data: this.applicationLayer.encode({
unit,
fc,
data: Array.from(bufferTx),
}),
broadcast: unit === 0,
}, {
preCheck: [
(frame) => frame.unit === unit && frame.fc === fc,
() => bufferTx.length,
(frame) => frame.data.every((v, i) => v === bufferTx[i]),
],
}, timeout).then((frame) => {
if (frame) {
return Object.assign(Object.assign({}, frame), { data: value });
}
});
}
writeMultipleCoils(unit, address, value, timeout = this.timeout) {
const fc = 0x0f;
const byteCount = Math.ceil(value.length / 8);
const bufferTx = Buffer.alloc(5 + byteCount);
bufferTx.writeUInt16BE(address, 0);
bufferTx.writeUInt16BE(value.length, 2);
bufferTx.writeUInt8(byteCount, 4);
value.forEach((v, i) => {
if (v) {
bufferTx[5 + ~~(i / 8)] |= 1 << i % 8;
}
});
return this.waitResponse({
data: this.applicationLayer.encode({
unit,
fc,
data: Array.from(bufferTx),
}),
broadcast: unit === 0,
}, {
preCheck: [(frame) => frame.unit === unit && frame.fc === fc, () => 4, (frame) => frame.data.every((v, i) => v === bufferTx[i])],
}, timeout).then((frame) => {
if (frame) {
return Object.assign(Object.assign({}, frame), { data: value });
}
});
}
writeMultipleRegisters(unit, address, value, timeout = this.timeout) {
const fc = 0x10;
const byteCount = value.length * 2;
const bufferTx = Buffer.alloc(5 + byteCount);
bufferTx.writeUInt16BE(address, 0);
bufferTx.writeUInt16BE(value.length, 2);
bufferTx.writeUInt8(byteCount, 4);
value.forEach((v, i) => {
bufferTx.writeUInt16BE(v, 5 + i * 2);
});
return this.waitResponse({
data: this.applicationLayer.encode({
unit,
fc,
data: Array.from(bufferTx),
}),
broadcast: unit === 0,
}, {
preCheck: [(frame) => frame.unit === unit && frame.fc === fc, () => 4, (frame) => frame.data.every((v, i) => v === bufferTx[i])],
}, timeout).then((frame) => {
if (frame) {
return Object.assign(Object.assign({}, frame), { data: value });
}
});
}
reportServerId(unit, timeout = this.timeout) {
const fc = 0x11;
return this.waitResponse({
data: this.applicationLayer.encode({
unit,
fc,
data: [],
}),
broadcast: unit === 0,
}, {
preCheck: [
(frame) => frame.unit === unit && frame.fc === fc,
(frame) => {
if (frame.data.length >= 3) {
return 1 + frame.data[0];
}
},
],
}, timeout).then((frame) => {
if (frame) {
return Object.assign(Object.assign({}, frame), { data: {
serverId: frame.data[1],
runIndicatorStatus: frame.data[2] === 0xff,
additionalData: frame.data.slice(3),
} });
}
});
}
maskWriteRegister(unit, address, andMask, orMask, timeout = this.timeout) {
const fc = 0x16;
const bufferTx = Buffer.alloc(6);
bufferTx.writeUInt16BE(address, 0);
bufferTx.writeUInt16BE(andMask, 2);
bufferTx.writeUInt16BE(orMask, 4);
return this.waitResponse({
data: this.applicationLayer.encode({
unit,
fc,
data: Array.from(bufferTx),
}),
broadcast: unit === 0,
}, {
preCheck: [(frame) => frame.unit === unit && frame.fc === fc, () => 6, (frame) => frame.data.every((v, i) => v === bufferTx[i])],
}, timeout).then((frame) => {
if (frame) {
return Object.assign(Object.assign({}, frame), { data: { andMask, orMask } });
}
});
}
readAndWriteMultipleRegisters(unit, read, write, timeout = this.timeout) {
const fc = 0x17;
const byteCount = write.value.length * 2;
const bufferTx = Buffer.alloc(9 + byteCount);
bufferTx.writeUInt16BE(read.address, 0);
bufferTx.writeUInt16BE(read.length, 2);
bufferTx.writeUInt16BE(write.address, 4);
bufferTx.writeUInt16BE(write.value.length, 6);
bufferTx.writeUInt8(byteCount, 8);
write.value.forEach((v, i) => {
bufferTx.writeUInt16BE(v, 9 + i * 2);
});
return this.waitResponse({
data: this.applicationLayer.encode({
unit,
fc,
data: Array.from(bufferTx),
}),
broadcast: unit === 0,
}, {
preCheck: [(frame) => frame.unit === unit && frame.fc === fc, () => 1 + byteCount, (frame) => frame.data[0] === byteCount],
}, timeout).then((frame) => {
if (frame) {
const bufferRx = Buffer.from(frame.data.slice(1));
return Object.assign(Object.assign({}, frame), { data: Array.from({ length: read.length }).map((_, index) => bufferRx.readUInt16BE(index * 2)) });
}
});
}
readDeviceIdentification(unit, readDeviceIDCode, objectId, timeout = this.timeout) {
const fc = 0x2b;
return this.waitResponse({
data: this.applicationLayer.encode({
unit,
fc,
data: [0x0e, readDeviceIDCode, objectId],
}),
broadcast: unit === 0,
}, {
preCheck: [
(frame) => frame.unit === unit && frame.fc === fc,
(frame) => {
if (frame.data.length >= 6) {
if (frame.data[0] === 0x0e && frame.data[1] === readDeviceIDCode) {
const objects = [];
let object = [];
for (const v of frame.data.slice(6)) {
switch (object.length) {