UNPKG

njs-modbus

Version:

A pure JavaScript implemetation of Modbus for NodeJS.

1,391 lines (1,374 loc) 85 kB
'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) {