diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac90eb1..a4cd9b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [10.x, 12.x, 14.x, 18.x] + node-version: [14.x, 18.x] steps: - uses: actions/checkout@v2 diff --git a/ModbusRTU.d.ts b/ModbusRTU.d.ts index 02c682e..708a9e1 100644 --- a/ModbusRTU.d.ts +++ b/ModbusRTU.d.ts @@ -45,8 +45,8 @@ export class ModbusRTU { linkTcpRTUBuffered(socket: Socket, options: TcpRTUPortOptions): Promise; linkTelnet(socket: Socket, options: TelnetPortOptions, next: Function): void; linkTelnet(socket: Socket, options: TelnetPortOptions): Promise; - connectRTUSocket(socket: Socket, next: Function): void; - connectRTUSocket(socket: Socket): Promise; + connectRTUSocket(socket: Socket, options: SocketOptions, next: Function): void; + connectRTUSocket(socket: Socket, options: SocketOptions): Promise; // Promise API setID(id: number): void; @@ -152,6 +152,9 @@ export interface TcpRTUPortOptions { family?: number; } +export interface SocketOptions { +} + export interface TelnetPortOptions { port?: number; } diff --git a/README.md b/README.md index a3476ab..1a2cc57 100644 --- a/README.md +++ b/README.md @@ -61,14 +61,15 @@ This module has not been tested on every single version of NodeJS. For best resu |-------|----------| | FC1 "Read Coil Status" | `readCoils(coil, len)` | | FC2 "Read Input Status" | `readDiscreteInputs(addr, arg)` | -| FC3 "Read Holding Registers" | `readHoldingRegisters(addr, len) ` | +| FC3 "Read Holding Registers" | `readHoldingRegisters(addr, len)`, `readRegistersEnron(addr, len)`* | | FC4 "Read Input Registers" | `readInputRegisters(addr, len) ` | | FC5 "Force Single Coil" | `writeCoil(coil, binary) //NOT setCoil` | | FC6 "Preset Single Register" | `writeRegister(addr, value)` | - | FC15 "Force Multiple Coil" | `writeCoils(addr, valueAry)` | -| FC16 "Preset Multiple Registers" | `writeRegisters(addr, valueAry)` | +| FC15 "Force Multiple Coil" | `writeCoils(addr, valueAry)` | +| FC16 "Preset Multiple Registers" | `writeRegisters(addr, valueAry)`, `writeRegistersEnron(addr, valueAry)`* | | FC43/14 "Read Device Identification" (supported ports: TCP, RTU) | `readDeviceIdentification(id, obj)` | +\* See examples/server_enron.js for enron configuration example. ###### Client Serial: * modbus-RTU (SerialPort): Over serial line [require node serialport]. @@ -84,8 +85,9 @@ This module has not been tested on every single version of NodeJS. For best resu ###### Server -* modbus-TCP (ServerTCP): Over TCP/IP line. - +* modbus-TCP ServerTCP(): Encapsulated TCP/IP. +* modbus-RTU Server(): RTU over TCP/IP Socket. +* modbus-Serial ServerSerial(): Over serial. #### Examples @@ -227,7 +229,7 @@ setInterval(function() { }, 1000); ``` ---- -###### ModbusTCP Server +###### Modbus TCP Server ``` javascript // create an empty modbus client const ModbusRTU = require("modbus-serial"); @@ -283,6 +285,91 @@ serverTCP.on("socketError", function(err){ }); ``` ---- +###### Modbus Serial Server +``` javascript +const ModbusRTU = require(".."); + +const holdingRegisters = {}; +const coils = {}; +const inputRegisters = {}; +const discreteInputs = {}; + +const vector = { + getInputRegister: function(addr) { + return inputRegisters[addr]; + }, + getMultipleInputRegisters: function(startAddr, length) { + const values = []; + for (let i = 0; i < length; i++) { + values[i] = inputRegisters[startAddr + i]; + } + return values; + }, + getDiscreteInput: function(addr) { + return discreteInputs[addr]; + }, + getHoldingRegister: function(addr) { + return holdingRegisters[addr]; + }, + setRegister: function(addr, value) { + holdingRegisters[addr] = value; + return; + }, + getMultipleHoldingRegisters: function(startAddr, length) { + const values = []; + for (let i = 0; i < length; i++) { + values[i] = holdingRegisters[startAddr + i]; + } + return values; + }, + getCoil: function(addr) { + return coils[addr]; + }, + setCoil: function(addr, value) { + coils[addr] = value; + return coils[addr]; + }, + readDeviceIdentification: function() { + return { + 0x00: "MyVendorName", + 0x01: "MyProductCode", + 0x02: "MyMajorMinorRevision", + 0x05: "MyModelName", + 0x97: "MyExtendedObject1", + 0xab: "MyExtendedObject2" + }; + } +}; + +// set the server to answer for modbus requests +const serverSerial = new ModbusRTU.ServerSerial( + vector, + { + port: "/tmp/ttyp0", + debug: true, + unitID: 1 + // enron: true, + // enronTables: { + // booleanRange: [1001, 1999], + // shortRange: [3001, 3999], + // longRange: [5001, 5999], + // floatRange: [7001, 7999] + // } + }, + { + baudRate: 9600, + dataBits: 8, + stopBits: 1, + parity: "even" + } +); + +serverSerial.on("error", function(err) { + // Handle socket error if needed, can be ignored + console.error(err); +}); +``` +---- ###### Read and Write Modbus ASCII ``` javascript // create an empty modbus client diff --git a/apis/connection.js b/apis/connection.js index 7f2acd1..65369f4 100644 --- a/apis/connection.js +++ b/apis/connection.js @@ -60,6 +60,7 @@ const addConnctionAPI = function(Modbus) { if (options) { this._enron = options.enron; this._enronTables = options.enronTables; + this._encapsulatedRTU = options.encapsulatedRTU; } // check if we have options @@ -97,6 +98,7 @@ const addConnctionAPI = function(Modbus) { if (options) { this._enron = options.enron; this._enronTables = options.enronTables; + this._encapsulatedRTU = options.encapsulatedRTU; } // check if we have options @@ -325,6 +327,7 @@ const addConnctionAPI = function(Modbus) { if (options) { this._enron = options.enron; this._enronTables = options.enronTables; + this._encapsulatedRTU = options.encapsulatedRTU; } // check if we have options @@ -382,7 +385,24 @@ const addConnctionAPI = function(Modbus) { * @param {socket} socket the socket to connect to - required. * @param {Function} next the function to call next. */ - cl.connectRTUSocket = function(socket, next) { + cl.connectRTUSocket = function(socket, options, next) { + if (options) { + this._enron = options.enron; + this._enronTables = options.enronTables; + this._encapsulatedRTU = options.encapsulatedRTU; + } + + // check if we have options + if (typeof next === "undefined" && typeof options === "function") { + next = options; + options = {}; + } + + // check if we have options + if (typeof options === "undefined") { + options = {}; + } + const thisModbus = this; this._port = socket; this._port.open = function(callback) { diff --git a/examples/server_serial.js b/examples/server_serial.js new file mode 100644 index 0000000..292224b --- /dev/null +++ b/examples/server_serial.js @@ -0,0 +1,81 @@ +const ModbusRTU = require(".."); + +const holdingRegisters = {}; +const coils = {}; +const inputRegisters = {}; +const discreteInputs = {}; + +const vector = { + getInputRegister: function(addr) { + return inputRegisters[addr]; + }, + getMultipleInputRegisters: function(startAddr, length) { + const values = []; + for (let i = 0; i < length; i++) { + values[i] = inputRegisters[startAddr + i]; + } + return values; + }, + getDiscreteInput: function(addr) { + return discreteInputs[addr]; + }, + getHoldingRegister: function(addr) { + return holdingRegisters[addr]; + }, + setRegister: function(addr, value) { + holdingRegisters[addr] = value; + return; + }, + getMultipleHoldingRegisters: function(startAddr, length) { + const values = []; + for (let i = 0; i < length; i++) { + values[i] = holdingRegisters[startAddr + i]; + } + return values; + }, + getCoil: function(addr) { + return coils[addr]; + }, + setCoil: function(addr, value) { + coils[addr] = value; + return coils[addr]; + }, + readDeviceIdentification: function() { + return { + 0x00: "MyVendorName", + 0x01: "MyProductCode", + 0x02: "MyMajorMinorRevision", + 0x05: "MyModelName", + 0x97: "MyExtendedObject1", + 0xab: "MyExtendedObject2" + }; + } +}; + +// set the server to answer for modbus requests +const serverSerial = new ModbusRTU.ServerSerial( + vector, + { + port: "/tmp/ttyp0", + debug: true, + unitID: 1 + // enron: true, + // enronTables: { + // booleanRange: [1001, 1999], + // shortRange: [3001, 3999], + // longRange: [5001, 5999], + // floatRange: [7001, 7999] + // } + }, + { + baudRate: 9600, + dataBits: 8, + stopBits: 1, + parity: "even" + } +); + +serverSerial.on("error", function(err) { + // Handle socket error if needed, can be ignored + console.error(err); +}); diff --git a/index.js b/index.js index d0d3de5..24d098c 100644 --- a/index.js +++ b/index.js @@ -351,191 +351,301 @@ function _onReceive(data) { const modbus = this; let error; - // set locale helpers variables - const transaction = modbus._transactions[modbus._port._transactionIdRead]; + let next; - // the _transactionIdRead can be missing, ignore wrong transaction it's - if (!transaction) { - return; - } + /* check enron options are valid + */ + function validateEnron() { + if (modbus._enron) { + const example = { + enronTables: { + booleanRange: [1001, 1999], + shortRange: [3001, 3999], + longRange: [5001, 5999], + floatRange: [7001, 7999] + } + }; - if (transaction.responses) { - /* Stash what we received */ - transaction.responses.push(Uint8Array.prototype.slice.call(data)); + if (typeof modbus._enronTables === "undefined" || + modbus._enronTables.shortRange.length !== 2 || + modbus._enronTables.shortRange[0] >= modbus._enronTables.shortRange[1]) { + next(new Error("Enron table definition missing from options. Example: " + JSON.stringify(example))); + return; + } + } } - /* What do we do next? */ - const next = function(err, res) { - if (transaction.next) { - /* Include request/response data if enabled */ - if (transaction.request && transaction.responses) { - if (err) { - err.modbusRequest = transaction.request; - err.modbusResponses = transaction.responses; - } + if (modbus._encapsulatedRTU === true) { + let finished = false; + let dataLength = data.readUInt8(2); + let transaction = modbus._transactions[modbus._port._transactionIdRead]; + modbus._port._transactionIdRead += 1; - if (res) { - res.request = transaction.request; - res.responses = transaction.responses; - } + /* What do we do next? */ + next = function(err, res) { + if (transaction && transaction.next) { + return transaction.next(err, res); } + }; - /* Pass the data on */ - return transaction.next(err, res); - } - }; + validateEnron(); - /* cancel the timeout */ - _cancelTimeout(transaction._timeoutHandle); - transaction._timeoutHandle = undefined; + let messageBuf; + let offset = 0; - /* check if the timeout fired */ - if (transaction._timeoutFired === true) { - // we have already called back with an error, so don't generate a new callback - return; - } + while (!finished) { + // do stuff + messageBuf = Buffer.alloc(dataLength + 5); - /* check incoming data - */ + data.copy(messageBuf, 0, offset, offset + dataLength + 5); - /* check minimal length - */ - if (!transaction.lengthUnknown && data.length < 5) { - error = "Data length error, expected " + - transaction.nextLength + " got " + data.length; - next(new Error(error)); - return; - } + const crcIn = messageBuf.readUInt16LE(messageBuf.length - 2); - /* check message CRC - * if CRC is bad raise an error - */ - const crcIn = data.readUInt16LE(data.length - 2); - if (crcIn !== crc16(data.slice(0, -2))) { - error = "CRC error"; - next(new Error(error)); - return; - } + if (crcIn !== crc16(messageBuf.slice(0, -2))) { + error = "CRC error"; + return; + } - // if crc is OK, read address and function code - const address = data.readUInt8(0); - const code = data.readUInt8(1); + // if crc is OK, read address and function code + const address = messageBuf.readUInt8(0); + const code = messageBuf.readUInt8(1); + + /* check for modbus exception + */ + if (messageBuf.length >= 5 && + code === (0x80 | code)) { + const errorCode = messageBuf.readUInt8(2); + error = new Error("Modbus exception " + errorCode + ": " + (modbusErrorMessages[errorCode] || "Unknown error")); + error.modbusCode = errorCode; + return; + } - /* check for modbus exception - */ - if (data.length >= 5 && - code === (0x80 | transaction.nextCode)) { - const errorCode = data.readUInt8(2); - if (transaction.next) { - error = new Error("Modbus exception " + errorCode + ": " + (modbusErrorMessages[errorCode] || "Unknown error")); - error.modbusCode = errorCode; - next(error); + switch (code) { + case 1: + case 2: + // Read Coil Status (FC=01) + // Read Input Status (FC=02) + _readFC2(messageBuf, next); + break; + case 3: + case 4: + // Read Input Registers (FC=04) + // Read Holding Registers (FC=03) + if (modbus._enron && !(address >= modbus._enronTables.shortRange[0] && address <= modbus._enronTables.shortRange[1])) { + _readFC3or4Enron(messageBuf, next); + } else { + _readFC3or4(messageBuf, next); + } + break; + case 5: + // Force Single Coil + _readFC5(messageBuf, next); + break; + case 6: + // Preset Single Register + if (modbus._enron && !(address >= modbus._enronTables.shortRange[0] && address <= modbus._enronTables.shortRange[1])) { + _readFC6Enron(messageBuf, next); + } else { + _readFC6(messageBuf, next); + } + break; + case 15: + case 16: + // Force Multiple Coils + // Preset Multiple Registers + _readFC16(messageBuf, next); + break; + case 17: + _readFC17(messageBuf, next); + break; + case 20: + _readFC20(messageBuf, transaction.next); + break; + case 43: + // read device identification + _readFC43(messageBuf, modbus, next); + } + + // move on to next + offset += 5 + dataLength; + if (offset + 2 > data.length) { + finished = true; + } else { + dataLength = data.readUInt8(offset + 2); + + transaction = modbus._transactions[modbus._port._transactionIdRead]; + modbus._port._transactionIdRead += 1; + } } - return; - } + } else { + // set locale helpers variables + const transaction = modbus._transactions[modbus._port._transactionIdRead]; - /* check enron options are valid - */ - if (modbus._enron) { - const example = { - enronTables: { - booleanRange: [1001, 1999], - shortRange: [3001, 3999], - longRange: [5001, 5999], - floatRange: [7001, 7999] + // the _transactionIdRead can be missing, ignore wrong transaction it's + if (!transaction) { + return; + } + + if (transaction.responses) { + /* Stash what we received */ + transaction.responses.push(Uint8Array.prototype.slice.call(data)); + } + + /* What do we do next? */ + next = function(err, res) { + if (transaction.next) { + /* Include request/response data if enabled */ + if (transaction.request && transaction.responses) { + if (err) { + err.modbusRequest = transaction.request; + err.modbusResponses = transaction.responses; + } + + if (res) { + res.request = transaction.request; + res.responses = transaction.responses; + } + } + + /* Pass the data on */ + return transaction.next(err, res); } }; - if (typeof modbus._enronTables === "undefined" || - modbus._enronTables.shortRange.length !== 2 || - modbus._enronTables.shortRange[0] >= modbus._enronTables.shortRange[1]) { - next(new Error("Enron table definition missing from options. Example: " + JSON.stringify(example))); + validateEnron(); + + /* cancel the timeout */ + _cancelTimeout(transaction._timeoutHandle); + transaction._timeoutHandle = undefined; + + /* check if the timeout fired */ + if (transaction._timeoutFired === true) { + // we have already called back with an error, so don't generate a new callback return; } - } - /* check message length - * if we do not expect this data - * raise an error - */ - if (!transaction.lengthUnknown && data.length !== transaction.nextLength) { - error = "Data length error, expected " + - transaction.nextLength + " got " + data.length; - next(new Error(error)); - return; - } + /* check incoming data + */ - /* check message address - * if we do not expect this message - * raise an error - */ - if (address !== transaction.nextAddress) { - error = "Unexpected data error, expected " + - "address " + transaction.nextAddress + " got " + address; - if (transaction.next) + /* check minimal length + */ + if (!transaction.lengthUnknown && data.length < 5) { + error = "Data length error, expected " + + transaction.nextLength + " got " + data.length; next(new Error(error)); - return; - } + return; + } - /* check message code - * if we do not expect this message - * raise an error - */ - if (code !== transaction.nextCode) { - error = "Unexpected data error, expected " + - "code " + transaction.nextCode + " got " + code; - if (transaction.next) + /* check message CRC + * if CRC is bad raise an error + */ + const crcIn = data.readUInt16LE(data.length - 2); + if (crcIn !== crc16(data.slice(0, -2))) { + error = "CRC error"; next(new Error(error)); - return; - } - - /* parse incoming data - */ + return; + } - switch (code) { - case 1: - case 2: - // Read Coil Status (FC=01) - // Read Input Status (FC=02) - _readFC2(data, next); - break; - case 3: - case 4: - // Read Input Registers (FC=04) - // Read Holding Registers (FC=03) - if (modbus._enron && !(transaction.nextDataAddress >= modbus._enronTables.shortRange[0] && transaction.nextDataAddress <= modbus._enronTables.shortRange[1])) { - _readFC3or4Enron(data, next); - } else { - _readFC3or4(data, next); - } - break; - case 5: - // Force Single Coil - _readFC5(data, next); - break; - case 6: - // Preset Single Register - if (modbus._enron && !(transaction.nextDataAddress >= modbus._enronTables.shortRange[0] && transaction.nextDataAddress <= modbus._enronTables.shortRange[1])) { - _readFC6Enron(data, next); - } else { - _readFC6(data, next); + // if crc is OK, read address and function code + const address = data.readUInt8(0); + const code = data.readUInt8(1); + + /* check for modbus exception + */ + if (data.length >= 5 && + code === (0x80 | transaction.nextCode)) { + const errorCode = data.readUInt8(2); + if (transaction.next) { + error = new Error("Modbus exception " + errorCode + ": " + (modbusErrorMessages[errorCode] || "Unknown error")); + error.modbusCode = errorCode; + next(error); } - break; - case 15: - case 16: - // Force Multiple Coils - // Preset Multiple Registers - _readFC16(data, next); - break; - case 17: - _readFC17(data, next); - break; - case 20: - _readFC20(data, transaction.next); - break; - case 43: - // read device identification - _readFC43(data, modbus, next); + return; + } + + /* check message length + * if we do not expect this data + * raise an error + */ + if (!transaction.lengthUnknown && data.length !== transaction.nextLength) { + error = "Data length error, expected " + + transaction.nextLength + " got " + data.length; + next(new Error(error)); + return; + } + + /* check message address + * if we do not expect this message + * raise an error + */ + if (address !== transaction.nextAddress) { + error = "Unexpected data error, expected " + + "address " + transaction.nextAddress + " got " + address; + if (transaction.next) + next(new Error(error)); + return; + } + + /* check message code + * if we do not expect this message + * raise an error + */ + if (code !== transaction.nextCode) { + error = "Unexpected data error, expected " + + "code " + transaction.nextCode + " got " + code; + if (transaction.next) + next(new Error(error)); + return; + } + + /* parse incoming data + */ + + switch (code) { + case 1: + case 2: + // Read Coil Status (FC=01) + // Read Input Status (FC=02) + _readFC2(data, next); + break; + case 3: + case 4: + // Read Input Registers (FC=04) + // Read Holding Registers (FC=03) + if (modbus._enron && !(transaction.nextDataAddress >= modbus._enronTables.shortRange[0] && transaction.nextDataAddress <= modbus._enronTables.shortRange[1])) { + _readFC3or4Enron(data, next); + } else { + _readFC3or4(data, next); + } + break; + case 5: + // Force Single Coil + _readFC5(data, next); + break; + case 6: + // Preset Single Register + if (modbus._enron && !(transaction.nextDataAddress >= modbus._enronTables.shortRange[0] && transaction.nextDataAddress <= modbus._enronTables.shortRange[1])) { + _readFC6Enron(data, next); + } else { + _readFC6(data, next); + } + break; + case 15: + case 16: + // Force Multiple Coils + // Preset Multiple Registers + _readFC16(data, next); + break; + case 17: + _readFC17(data, next); + break; + case 20: + _readFC20(data, transaction.next); + break; + case 43: + // read device identification + _readFC43(data, modbus, next); + } } } @@ -728,6 +838,10 @@ class ModbusRTU extends EventEmitter { // write buffer to serial port _writeBufferToPort.call(this, buf, this._port._transactionIdWrite); + + if (this._encapsulatedRTU) { + this._port._transactionIdWrite += 1; + } } /** @@ -770,13 +884,12 @@ class ModbusRTU extends EventEmitter { if (this._enron && !(dataAddress >= this._enronTables.shortRange[0] && dataAddress <= this._enronTables.shortRange[1])) { valueSize = 4; } - // set state variables this._transactions[this._port._transactionIdWrite] = { nextAddress: address, nextDataAddress: dataAddress, nextCode: code, - nextLength: 3 + (valueSize * length) + 2, + nextLength: 3 + (valueSize * length) + 2, // response size: unitID, FC, length, data, CRC next: next }; @@ -793,6 +906,10 @@ class ModbusRTU extends EventEmitter { // write buffer to serial port _writeBufferToPort.call(this, buf, this._port._transactionIdWrite); + + if (this._encapsulatedRTU) { + this._port._transactionIdWrite += 1; + } } /** @@ -969,6 +1086,10 @@ class ModbusRTU extends EventEmitter { // write buffer to serial port _writeBufferToPort.call(this, buf, this._port._transactionIdWrite); + + if (this._encapsulatedRTU) { + this._port._transactionIdWrite += 1; + } } /** @@ -1031,6 +1152,10 @@ class ModbusRTU extends EventEmitter { // write buffer to serial port _writeBufferToPort.call(this, buf, this._port._transactionIdWrite); + + if (this._encapsulatedRTU) { + this._port._transactionIdWrite += 1; + } } /** @@ -1067,6 +1192,10 @@ class ModbusRTU extends EventEmitter { // write buffer to serial port _writeBufferToPort.call(this, buf, this._port._transactionIdWrite); + + if (this._encapsulatedRTU) { + this._port._transactionIdWrite += 1; + } } @@ -1107,6 +1236,10 @@ class ModbusRTU extends EventEmitter { buf.writeUInt8(chunck, 9); buf.writeUInt16LE(crc16(buf.slice(0, -2)), codeLength); _writeBufferToPort.call(this, buf, this._port._transactionIdWrite); + + if (this._encapsulatedRTU) { + this._port._transactionIdWrite += 1; + } } /** @@ -1144,6 +1277,10 @@ class ModbusRTU extends EventEmitter { buf.writeUInt16LE(crc16(buf.slice(0, -2)), codeLength); // write buffer to serial port _writeBufferToPort.call(this, buf, this._port._transactionIdWrite); + + if (this._encapsulatedRTU) { + this._port._transactionIdWrite += 1; + } } } @@ -1172,6 +1309,7 @@ module.exports.TcpRTUBufferedPort = require("./ports/tcprtubufferedport"); module.exports.TelnetPort = require("./ports/telnetport"); module.exports.C701Port = require("./ports/c701port"); +module.exports.Server = require("./servers/server"); module.exports.ServerTCP = require("./servers/servertcp"); module.exports.ServerSerial = require("./servers/serverserial"); module.exports.default = module.exports; diff --git a/package.json b/package.json index fefe93a..ac1d7df 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,6 @@ }, "dependencies": { "debug": "^4.1.1", - "serialport": "^10.4.0" + "serialport": "^11.0.0" } } diff --git a/servers/server.js b/servers/server.js new file mode 100644 index 0000000..851e45a --- /dev/null +++ b/servers/server.js @@ -0,0 +1,342 @@ +"use strict"; +/** + * Copyright (c) 2017, Yaacov Zamir + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +const events = require("events"); +const EventEmitter = events.EventEmitter || events; +const net = require("net"); +const modbusSerialDebug = require("debug")("modbus-serial"); // modbusSerialDebug + +const HOST = "127.0.0.1"; +const UNIT_ID = 255; // listen to all adresses +const MODBUS_PORT = 502; + +const RTU_META_LENGTH = 3; // UnitID[1] and CRC[2] + +/* Get Handlers + */ +const handlers = require("./server_handler"); + +/* Add bit operation functions to Buffer + */ +require("../utils/buffer_bit")(); +const crc16 = require("../utils/crc16"); + +/** + * Helper function for sending debug objects. + * + * @param {string} text - text of message, an error or an action + * @param {int} unitID - Id of the requesting unit + * @param {int} functionCode - a modbus function code. + * @param {Buffer} requestBuffer - request Buffer from client + * @returns undefined + * @private + */ +function _serverDebug(text, unitID, functionCode, responseBuffer) { + // If no responseBuffer, then assume this is an error + // o/w assume an action + if (typeof responseBuffer === "undefined") { + modbusSerialDebug({ + error: text, + unitID: unitID, + functionCode: functionCode + }); + + } else { + modbusSerialDebug({ + action: text, + unitID: unitID, + functionCode: functionCode, + responseBuffer: responseBuffer.toString("hex") + }); + } +} + +/** + * Helper function for creating callback functions. + * + * @param {int} unitID - Id of the requesting unit + * @param {int} functionCode - a modbus function code + * @param {function} sockWriter - write buffer (or error) to tcp socket + * @returns {function} - a callback function + * @private + */ +function _callbackFactory(unitID, functionCode, sockWriter) { + return function cb(err, responseBuffer) { + // If we have an error. + if (err) { + let errorCode = 0x04; // slave device failure + if (!isNaN(err.modbusErrorCode)) { + errorCode = err.modbusErrorCode; + } + + // Set an error response + functionCode = parseInt(functionCode) | 0x80; + responseBuffer = Buffer.alloc(3 + 2); + responseBuffer.writeUInt8(errorCode, 2); + + _serverDebug("error processing response", unitID, functionCode); + } + + // If we do not have a responseBuffer + if (!responseBuffer) { + _serverDebug("no response buffer", unitID, functionCode); + return sockWriter(null, responseBuffer); + } + + // add unit number and function code + responseBuffer.writeUInt8(unitID, 0); + responseBuffer.writeUInt8(functionCode, 1); + + // Add crc + const crc = crc16(responseBuffer.slice(0, -2)); + responseBuffer.writeUInt16LE(crc, responseBuffer.length - 2); + + // Call callback function + return sockWriter(null, responseBuffer); + }; +} + +/** + * Parse a ModbusRTU buffer and return an answer buffer. + * + * @param {Buffer} requestBuffer - request Buffer from client + * @param {object} vector - vector of functions for read and write + * @param {function} callback - callback to be invoked passing {Buffer} response + * @param {int} serverUnitID - the server's unitID + * @param {function} sockWriter - write buffer (or error) to tcp socket + * @param {object} options - the options object + * @returns undefined + * @private + */ +function _parseModbusBuffer(requestBuffer, vector, serverUnitID, sockWriter, options) { + // Check requestBuffer length + if (!requestBuffer || requestBuffer.length < RTU_META_LENGTH + 5) { + modbusSerialDebug("wrong size of request Buffer " + requestBuffer.length); + return; + } + + const unitID = requestBuffer[0]; + let functionCode = requestBuffer[1]; + const crc = requestBuffer[requestBuffer.length - 2] + requestBuffer[requestBuffer.length - 1] * 0x100; + + // if crc is bad, ignore message + if (crc !== crc16(requestBuffer.slice(0, -2))) { + modbusSerialDebug("wrong CRC of request Buffer"); + return; + } + + // if crc is bad, ignore message + if (serverUnitID !== 255 && serverUnitID !== unitID) { + modbusSerialDebug("wrong unitID"); + return; + } + + modbusSerialDebug("request for function code " + functionCode); + const cb = _callbackFactory(unitID, functionCode, sockWriter); + + switch (parseInt(functionCode)) { + case 1: + case 2: + handlers.readCoilsOrInputDiscretes(requestBuffer, vector, unitID, cb, functionCode); + break; + case 3: + if (options.enron) { + handlers.readMultipleRegistersEnron(requestBuffer, vector, unitID, options.enronTables, cb); + } else { + handlers.readMultipleRegisters(requestBuffer, vector, unitID, cb); + } + break; + case 4: + handlers.readInputRegisters(requestBuffer, vector, unitID, cb); + break; + case 5: + handlers.writeCoil(requestBuffer, vector, unitID, cb); + break; + case 6: + if (options.enron) { + handlers.writeSingleRegisterEnron(requestBuffer, vector, unitID, options.enronTables, cb); + } else { + handlers.writeSingleRegister(requestBuffer, vector, unitID, cb); + } + break; + case 15: + handlers.forceMultipleCoils(requestBuffer, vector, unitID, cb); + break; + case 16: + handlers.writeMultipleRegisters(requestBuffer, vector, unitID, cb); + break; + case 43: + handlers.handleMEI(requestBuffer, vector, unitID, cb); + break; + default: { + const errorCode = 0x01; // illegal function + + // set an error response + functionCode = parseInt(functionCode) | 0x80; + const responseBuffer = Buffer.alloc(3 + 2); + responseBuffer.writeUInt8(errorCode, 2); + + modbusSerialDebug({ + error: "Illegal function", + functionCode: functionCode + }); + + cb({ modbusErrorCode: errorCode }, responseBuffer); + } + } +} + +class Server extends EventEmitter { + /** + * Class making Modbus server. + * + * @param vector - vector of server functions (see examples/server.js) + * @param options - server options (host (IP), port, debug (true/false), unitID, enron? (true/false), enronTables? (object)) + * @constructor + */ + constructor(vector, options) { + super(); + + const modbus = this; + options = options || {}; + + // create a tcp server + modbus._server = net.createServer(); + modbus._server.listen({ + port: options.port || MODBUS_PORT, + host: options.host || HOST + }, function() { + modbus.emit("initialized"); + }); + + // create a server unit id + const serverUnitID = options.unitID || UNIT_ID; + + // remember open sockets + modbus.socks = new Map(); + + modbus._server.on("connection", function(sock) { + let recvBuffer = Buffer.from([]); + modbus.socks.set(sock, 0); + + modbusSerialDebug({ + action: "connected", + address: sock.address(), + remoteAddress: sock.remoteAddress, + localPort: sock.localPort + }); + + sock.once("close", function() { + modbusSerialDebug({ + action: "closed" + }); + modbus.socks.delete(sock); + }); + + sock.on("data", function(data) { + modbusSerialDebug({ action: "socket data", data: data }); + recvBuffer = Buffer.concat([recvBuffer, data], recvBuffer.length + data.length); + + while(recvBuffer.length > RTU_META_LENGTH) { + // Check the presence of the full message, unitID, FC, Data Address, Data payload + if(recvBuffer.length < RTU_META_LENGTH + 5) + break; + + const requestBuffer = Buffer.alloc(RTU_META_LENGTH + 5); + recvBuffer.copy(requestBuffer, 0, 0, RTU_META_LENGTH + 5); + + // Move receive buffer on + recvBuffer = recvBuffer.slice(RTU_META_LENGTH + 5); // TODO depends on request type? + + const crc = crc16(requestBuffer.slice(0, -2)); + requestBuffer.writeUInt16LE(crc, requestBuffer.length - 2); + + modbusSerialDebug({ action: "receive", data: requestBuffer, requestBufferLength: requestBuffer.length }); + modbusSerialDebug(JSON.stringify({ action: "receive", data: requestBuffer })); + + const sockWriter = function(err, responseBuffer) { + if (err) { + modbus.emit("error", err); + return; + } + + // send data back + if (responseBuffer) { + // remove crc and add mbap + const outRtu = Buffer.alloc(responseBuffer.length); // Add UnitID + // outRtu.writeUInt16BE(transactionsId, 0); + // outRtu.writeUInt16BE(0, 2); + // outRtu.writeUInt16BE(responseBuffer.length, 3); + responseBuffer.copy(outRtu, 0); + + modbusSerialDebug(JSON.stringify({ action: "send string", data: responseBuffer })); + + // write to port + sock.write(outRtu); + } + }; + + // parse the modbusRTU buffer + setTimeout( + _parseModbusBuffer.bind(this, + requestBuffer, + vector, + serverUnitID, + sockWriter, + options + ), + 0 + ); + } + }); + + sock.on("error", function(err) { + modbusSerialDebug(JSON.stringify({ action: "socket error", data: err })); + + modbus.emit("socketError", err); + }); + }); + } + + /** + * Delegate the close server method to backend. + * + * @param callback + */ + close(callback) { + const modbus = this; + + // close the net port if exist + if (modbus._server) { + modbus._server.removeAllListeners("data"); + modbus._server.close(callback); + + modbus.socks.forEach(function(e, sock) { + sock.destroy(); + }); + + modbusSerialDebug({ action: "close server" }); + } else { + modbusSerialDebug({ action: "close server", warning: "server already closed" }); + } + } +} + +/** + * Server interface export. + * @type {Server} + */ +module.exports = Server; diff --git a/servers/server_handler.js b/servers/server_handler.js new file mode 100644 index 0000000..1099dca --- /dev/null +++ b/servers/server_handler.js @@ -0,0 +1,1217 @@ +/* eslint-disable no-var */ +"use strict"; +/** + * Copyright (c) 2017, Yaacov Zamir + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +const modbusSerialDebug = require("debug")("modbus-serial"); + +/** + * Check the length of request Buffer for length of 8. + * + * @param requestBuffer - request Buffer from client + * @returns {boolean} - if error it is true, otherwise false + * @private + */ +function _errorRequestBufferLength(requestBuffer) { + + if (requestBuffer.length !== 8) { + modbusSerialDebug("request Buffer length " + requestBuffer.length + " is wrong - has to be == 8"); + return true; + } + + return false; // length is okay - no error +} + +/** + * Check the length of request Buffer for length of 10. + * + * @param requestBuffer - request Buffer from client + * @returns {boolean} - if error it is true, otherwise false + * @private + */ +function _errorRequestBufferLengthEnron(requestBuffer) { + + if (requestBuffer.length !== 10) { + modbusSerialDebug("request (Enron) Buffer length " + requestBuffer.length + " is wrong - has to be == 10"); + return true; + } + + return false; // length is okay - no error +} + +/** + * Handle the callback invocation for Promises or synchronous values + * + * @param promiseOrValue - the Promise to be resolved or value to be returned + * @param cb - the callback to be invoked + * @returns undefined + * @private + */ +function _handlePromiseOrValue(promiseOrValue, cb) { + if (promiseOrValue && promiseOrValue.then && typeof promiseOrValue.then === "function") { + promiseOrValue + .then(function(value) { + cb(null, value); + }) + .catch(function(err) { + cb(err); + }); + } else { + cb(null, promiseOrValue); + } +} + + +/** + * Function to handle FC1 or FC2 request. + * + * @param requestBuffer - request Buffer from client + * @param vector - vector of functions for read and write + * @param unitID - Id of the requesting unit + * @param {function} callback - callback to be invoked passing {Buffer} response + * @returns undefined + * @private + */ +function _handleReadCoilsOrInputDiscretes(requestBuffer, vector, unitID, callback, fc) { + const address = requestBuffer.readUInt16BE(2); + const length = requestBuffer.readUInt16BE(4); + + if (_errorRequestBufferLength(requestBuffer)) { + return; + } + + // build answer + const dataBytes = parseInt((length - 1) / 8 + 1); + const responseBuffer = Buffer.alloc(3 + dataBytes + 2); + try { + responseBuffer.writeUInt8(dataBytes, 2); + } + catch (err) { + callback(err); + return; + } + + const isGetCoil = (fc === 1 && vector.getCoil); + const isGetDiscreteInpupt = (fc === 2 && vector.getDiscreteInput); + + // read coils + if (isGetCoil || isGetDiscreteInpupt) { + let callbackInvoked = false; + let cbCount = 0; + const buildCb = function(i) { + return function(err, value) { + if (err) { + if (!callbackInvoked) { + callbackInvoked = true; + callback(err); + } + + return; + } + + cbCount = cbCount + 1; + + responseBuffer.writeBit(value, i % 8, 3 + parseInt(i / 8)); + + if (cbCount === length && !callbackInvoked) { + modbusSerialDebug({ action: "FC" + fc + " response", responseBuffer: responseBuffer }); + + callbackInvoked = true; + callback(null, responseBuffer); + } + }; + }; + + if (length === 0) + callback({ + modbusErrorCode: 0x02, // Illegal address + msg: "Invalid length" + }); + + let i = 0; + let cb = null; + let promiseOrValue = null; + + if (isGetCoil && vector.getCoil.length === 3) { + for (i = 0; i < length; i++) { + cb = buildCb(i); + try { + vector.getCoil(address + i, unitID, cb); + } + catch(err) { + cb(err); + } + } + } + else if (isGetDiscreteInpupt && vector.getDiscreteInput.length === 3) { + for (i = 0; i < length; i++) { + cb = buildCb(i); + try { + vector.getDiscreteInput(address + i, unitID, cb); + } + catch(err) { + cb(err); + } + } + } + else if (isGetCoil) { + for (i = 0; i < length; i++) { + cb = buildCb(i); + try { + promiseOrValue = vector.getCoil(address + i, unitID); + _handlePromiseOrValue(promiseOrValue, cb); + } + catch(err) { + cb(err); + } + } + } + else if (isGetDiscreteInpupt) { + for (i = 0; i < length; i++) { + cb = buildCb(i); + try { + promiseOrValue = vector.getDiscreteInput(address + i, unitID); + _handlePromiseOrValue(promiseOrValue, cb); + } + catch(err) { + cb(err); + } + } + } + } +} + +/** + * Function to handle FC3 request. + * + * @param requestBuffer - request Buffer from client + * @param vector - vector of functions for read and write + * @param unitID - Id of the requesting unit + * @param {function} callback - callback to be invoked passing {Buffer} response + * @returns undefined + * @private + */ +function _handleReadMultipleRegisters(requestBuffer, vector, unitID, callback) { + const valueSize = 2; + const address = requestBuffer.readUInt16BE(2); + const length = requestBuffer.readUInt16BE(4); + + if (_errorRequestBufferLength(requestBuffer)) { + return; + } + + // build answer + const responseBuffer = Buffer.alloc(3 + (length * valueSize) + 2); + try { + responseBuffer.writeUInt8(length * valueSize, 2); + } + catch (err) { + callback(err); + return; + } + + let callbackInvoked = false; + let cbCount = 0; + const buildCb = function(i) { + return function(err, value) { + if (err) { + if (!callbackInvoked) { + callbackInvoked = true; + callback(err); + } + + return; + } + + cbCount = cbCount + 1; + + responseBuffer.writeUInt16BE(value, 3 + (i * valueSize)); + + if (cbCount === length && !callbackInvoked) { + modbusSerialDebug({ action: "FC3 response", responseBuffer: responseBuffer }); + + callbackInvoked = true; + callback(null, responseBuffer); + } + }; + }; + + if (length === 0) + callback({ + modbusErrorCode: 0x02, // Illegal address + msg: "Invalid length" + }); + + // read registers + function tryAndHandlePromiseOrValue(i, values) { + const cb = buildCb(i); + try { + const promiseOrValue = values[i]; + _handlePromiseOrValue(promiseOrValue, cb); + } + catch (err) { + cb(err); + } + } + + if (vector.getMultipleHoldingRegisters && length > 1) { + + if (vector.getMultipleHoldingRegisters.length === 4) { + vector.getMultipleHoldingRegisters(address, length, unitID, function(err, values) { + if (!err && values.length !== length) { + const error = new Error("Requested address length and response length do not match"); + callback(error); + } else if (err) { + const cb = buildCb(i); + try { + cb(err); // no need to use value array if there is an error + } + catch (ex) { + cb(ex); + } + } + else { + for (var i = 0; i < length; i++) { + const cb = buildCb(i); + try { + cb(err, values[i]); + } + catch (ex) { + cb(ex); + } + } + } + }); + } else { + const values = vector.getMultipleHoldingRegisters(address, length, unitID); + if (values.length === length) { + for (i = 0; i < length; i++) { + tryAndHandlePromiseOrValue(i, values); + } + } else { + const error = new Error("Requested address length and response length do not match"); + callback(error); + } + } + + } + else if (vector.getHoldingRegister) { + for (var i = 0; i < length; i++) { + const cb = buildCb(i); + try { + if (vector.getHoldingRegister.length === 3) { + vector.getHoldingRegister(address + i, unitID, cb); + } else { + const promiseOrValue = vector.getHoldingRegister(address + i, unitID); + _handlePromiseOrValue(promiseOrValue, cb); + } + } + catch (err) { + cb(err); + } + } + } +} + +/** + * Function to handle FC3 request. + * + * @param requestBuffer - request Buffer from client + * @param vector - vector of functions for read and write + * @param unitID - Id of the requesting unit + * @param enronTables - The enron tables definition + * @param {function} callback - callback to be invoked passing {Buffer} response + * @returns undefined + * @private + */ +function _handleReadMultipleRegistersEnron(requestBuffer, vector, unitID, enronTables, callback) { + const valueSize = 4; + const address = requestBuffer.readUInt16BE(2); + const length = requestBuffer.readUInt16BE(4); + + // Fall back to 16 bit for short integer variables + if (address >= enronTables.shortRange[0] && address <= enronTables.shortRange[1]) { + return _handleReadMultipleRegisters(requestBuffer, vector, unitID, callback); + } + + if (_errorRequestBufferLength(requestBuffer)) { + return; + } + + // build answer + const responseBuffer = Buffer.alloc(3 + (length * valueSize) + 2); + try { + responseBuffer.writeUInt8(length * valueSize, 2); + } + catch (err) { + callback(err); + return; + } + + let callbackInvoked = false; + let cbCount = 0; + const buildCb = function(i) { + return function(err, value) { + if (err) { + if (!callbackInvoked) { + callbackInvoked = true; + callback(err); + } + + return; + } + + cbCount = cbCount + 1; + + responseBuffer.writeUInt32BE(value, 3 + (i * valueSize)); + + if (cbCount === length && !callbackInvoked) { + modbusSerialDebug({ action: "FC3 response", responseBuffer: responseBuffer }); + + callbackInvoked = true; + callback(null, responseBuffer); + } + }; + }; + + if (length === 0) + callback({ + modbusErrorCode: 0x02, // Illegal address + msg: "Invalid length" + }); + + // read registers + function tryAndHandlePromiseOrValue(i, values) { + const cb = buildCb(i); + try { + const promiseOrValue = values[i]; + _handlePromiseOrValue(promiseOrValue, cb); + } + catch (err) { + cb(err); + } + } + + if (vector.getMultipleHoldingRegisters && length > 1) { + + if (vector.getMultipleHoldingRegisters.length === 4) { + vector.getMultipleHoldingRegisters(address, length, unitID, function(err, values) { + if (!err && values.length !== length) { + const error = new Error("Requested address length and response length do not match"); + callback(error); + } else if (err) { + const cb = buildCb(i); + try { + cb(err); // no need to use value array if there is an error + } + catch (ex) { + cb(ex); + } + } + else { + for (var i = 0; i < length; i++) { + const cb = buildCb(i); + try { + cb(err, values[i]); + } + catch (ex) { + cb(ex); + } + } + } + }); + } else { + const values = vector.getMultipleHoldingRegisters(address, length, unitID); + if (values.length === length) { + for (i = 0; i < length; i++) { + tryAndHandlePromiseOrValue(i, values); + } + } else { + const error = new Error("Requested address length and response length do not match"); + callback(error); + } + } + + } + else if (vector.getHoldingRegister) { + for (var i = 0; i < length; i++) { + const cb = buildCb(i); + try { + if (vector.getHoldingRegister.length === 3) { + vector.getHoldingRegister(address + i, unitID, cb); + } else { + const promiseOrValue = vector.getHoldingRegister(address + i, unitID); + _handlePromiseOrValue(promiseOrValue, cb); + } + } + catch (err) { + cb(err); + } + } + } +} + +/** + * Function to handle FC4 request. + * + * @param requestBuffer - request Buffer from client + * @param vector - vector of functions for read and write + * @param unitID - Id of the requesting unit + * @param {function} callback - callback to be invoked passing {Buffer} response + * @returns undefined + * @private + */ +function _handleReadInputRegisters(requestBuffer, vector, unitID, callback) { + const address = requestBuffer.readUInt16BE(2); + const length = requestBuffer.readUInt16BE(4); + + if (_errorRequestBufferLength(requestBuffer)) { + return; + } + + // build answer + const responseBuffer = Buffer.alloc(3 + length * 2 + 2); + try { + responseBuffer.writeUInt8(length * 2, 2); + } + catch (err) { + callback(err); + return; + } + + let callbackInvoked = false; + let cbCount = 0; + const buildCb = function(i) { + return function(err, value) { + if (err) { + if (!callbackInvoked) { + callbackInvoked = true; + callback(err); + } + + return; + } + + cbCount = cbCount + 1; + + responseBuffer.writeUInt16BE(value, 3 + i * 2); + + if (cbCount === length && !callbackInvoked) { + modbusSerialDebug({ action: "FC4 response", responseBuffer: responseBuffer }); + + callbackInvoked = true; + callback(null, responseBuffer); + } + }; + }; + + if (length === 0) + callback({ + modbusErrorCode: 0x02, // Illegal address + msg: "Invalid length" + }); + + function tryAndHandlePromiseOrValues(i, values) { + const cb = buildCb(i); + try { + const promiseOrValue = values[i]; + _handlePromiseOrValue(promiseOrValue, cb); + } + catch (err) { + cb(err); + } + } + + if (vector.getMultipleInputRegisters && length > 1) { + + if (vector.getMultipleInputRegisters.length === 4) { + vector.getMultipleInputRegisters(address, length, unitID, function(err, values) { + if (!err && values.length !== length) { + const error = new Error("Requested address length and response length do not match"); + callback(error); + } else { + for (let i = 0; i < length; i++) { + const cb = buildCb(i); + try { + cb(err, values[i]); + } + catch (ex) { + cb(ex); + } + } + } + }); + } else { + const values = vector.getMultipleInputRegisters(address, length, unitID); + if (values.length === length) { + for (var i = 0; i < length; i++) { + tryAndHandlePromiseOrValues(i, values); + } + } else { + const error = new Error("Requested address length and response length do not match"); + callback(error); + } + } + + } + else if (vector.getInputRegister) { + + for (i = 0; i < length; i++) { + const cb = buildCb(i); + try { + if (vector.getInputRegister.length === 3) { + vector.getInputRegister(address + i, unitID, cb); + } + else { + const promiseOrValue = vector.getInputRegister(address + i, unitID); + _handlePromiseOrValue(promiseOrValue, cb); + } + } + catch (ex) { + cb(ex); + } + } + } +} + +/** + * Function to handle FC5 request. + * + * @param requestBuffer - request Buffer from client + * @param vector - vector of functions for read and write + * @param unitID - Id of the requesting unit + * @param {function} callback - callback to be invoked passing {Buffer} response + * @returns undefined + * @private + */ +function _handleWriteCoil(requestBuffer, vector, unitID, callback) { + const address = requestBuffer.readUInt16BE(2); + const state = requestBuffer.readUInt16BE(4); + + if (_errorRequestBufferLength(requestBuffer)) { + return; + } + + // build answer + const responseBuffer = Buffer.alloc(8); + responseBuffer.writeUInt16BE(address, 2); + responseBuffer.writeUInt16BE(state, 4); + + if (vector.setCoil) { + let callbackInvoked = false; + const cb = function(err) { + if (err) { + if (!callbackInvoked) { + callbackInvoked = true; + callback(err); + } + + return; + } + + if (!callbackInvoked) { + modbusSerialDebug({ action: "FC5 response", responseBuffer: responseBuffer }); + + callbackInvoked = true; + callback(null, responseBuffer); + } + }; + + try { + if (vector.setCoil.length === 4) { + vector.setCoil(address, state === 0xff00, unitID, cb); + } + else { + const promiseOrValue = vector.setCoil(address, state === 0xff00, unitID); + _handlePromiseOrValue(promiseOrValue, cb); + } + } + catch(err) { + cb(err); + } + } +} + +/** + * Function to handle FC6 request. + * + * @param requestBuffer - request Buffer from client + * @param vector - vector of functions for read and write + * @param unitID - Id of the requesting unit + * @param {function} callback - callback to be invoked passing {Buffer} response + * @returns undefined + * @private + */ +function _handleWriteSingleRegister(requestBuffer, vector, unitID, callback) { + const address = requestBuffer.readUInt16BE(2); + const value = requestBuffer.readUInt16BE(4); + + if (_errorRequestBufferLength(requestBuffer)) { + return; + } + + // build answer + const responseBuffer = Buffer.alloc(8); + responseBuffer.writeUInt16BE(address, 2); + responseBuffer.writeUInt16BE(value, 4); + + if (vector.setRegister) { + let callbackInvoked = false; + const cb = function(err) { + if (err) { + if (!callbackInvoked) { + callbackInvoked = true; + callback(err); + } + + return; + } + + if (!callbackInvoked) { + modbusSerialDebug({ action: "FC6 response", responseBuffer: responseBuffer }); + + callbackInvoked = true; + callback(null, responseBuffer); + } + }; + + try { + if (vector.setRegister.length === 4) { + vector.setRegister(address, value, unitID, cb); + } + else { + const promiseOrValue = vector.setRegister(address, value, unitID); + _handlePromiseOrValue(promiseOrValue, cb); + } + } catch(err) { + cb(err); + } + } +} + +/** + * Function to handle FC6 (Enron) request. + * + * @param requestBuffer - request Buffer from client + * @param vector - vector of functions for read and write + * @param unitID - Id of the requesting unit + * @param enronTables - The enron tables definition + * @param {function} callback - callback to be invoked passing {Buffer} response + * @returns undefined + * @private + */ +function _handleWriteSingleRegisterEnron(requestBuffer, vector, unitID, enronTables, callback) { + const address = requestBuffer.readUInt16BE(2); + const value = requestBuffer.readUInt32BE(4); + + // Fall back to 16 bit for short integer variables + if (address >= enronTables.shortRange[0] && address <= enronTables.shortRange[1]) { + return _handleWriteSingleRegister(requestBuffer, vector, unitID, callback); + } + + if (_errorRequestBufferLengthEnron(requestBuffer)) { + return; + } + + // build answer + const responseBuffer = Buffer.alloc(10); + responseBuffer.writeUInt16BE(address, 2); + responseBuffer.writeUInt32BE(value, 4); + + if (vector.setRegister) { + let callbackInvoked = false; + const cb = function(err) { + if (err) { + if (!callbackInvoked) { + callbackInvoked = true; + callback(err); + } + + return; + } + + if (!callbackInvoked) { + modbusSerialDebug({ action: "FC6 response", responseBuffer: responseBuffer }); + + callbackInvoked = true; + callback(null, responseBuffer); + } + }; + + try { + if (vector.setRegister.length === 4) { + vector.setRegister(address, value, unitID, cb); + } + else { + const promiseOrValue = vector.setRegister(address, value, unitID); + _handlePromiseOrValue(promiseOrValue, cb); + } + } catch(err) { + cb(err); + } + } +} + +/** + * Function to handle FC15 request. + * + * @param requestBuffer - request Buffer from client + * @param vector - vector of functions for read and write + * @param unitID - Id of the requesting unit + * @param {function} callback - callback to be invoked passing {Buffer} response + * @returns undefined + * @private + */ +function _handleForceMultipleCoils(requestBuffer, vector, unitID, callback) { + const address = requestBuffer.readUInt16BE(2); + const length = requestBuffer.readUInt16BE(4); + + // if length is bad, ignore message + if (requestBuffer.length !== 7 + Math.ceil(length / 8) + 2) { + return; + } + + // build answer + const responseBuffer = Buffer.alloc(8); + responseBuffer.writeUInt16BE(address, 2); + responseBuffer.writeUInt16BE(length, 4); + + let callbackInvoked = false; + let cbCount = 0; + const buildCb = function(/* i - not used at the moment */) { + return function(err) { + if (err) { + if (!callbackInvoked) { + callbackInvoked = true; + callback(err); + } + + return; + } + + cbCount = cbCount + 1; + + if (cbCount === length && !callbackInvoked) { + modbusSerialDebug({ action: "FC15 response", responseBuffer: responseBuffer }); + + callbackInvoked = true; + callback(null, responseBuffer); + } + }; + }; + + if (length === 0) + callback({ + modbusErrorCode: 0x02, // Illegal address + msg: "Invalid length" + }); + + if (vector.setCoilArray) { + const state = []; + + for (i = 0; i < length; i++) { + cb = buildCb(i); + state.push(requestBuffer.readBit(i, 7)); + _handlePromiseOrValue(promiseOrValue, cb); + } + + try { + if (vector.setCoilArray.length === 4) { + vector.setCoilArray(address, state, unitID, cb); + } + else { + vector.setCoilArray(address, state, unitID); + } + } + catch(err) { + cb(err); + } + } else if (vector.setCoil) { + let state; + + for (var i = 0; i < length; i++) { + var cb = buildCb(i); + state = requestBuffer.readBit(i, 7); + + try { + if (vector.setCoil.length === 4) { + vector.setCoil(address + i, state !== false, unitID, cb); + } + else { + var promiseOrValue = vector.setCoil(address + i, state !== false, unitID); + _handlePromiseOrValue(promiseOrValue, cb); + } + } + catch(err) { + cb(err); + } + } + } +} +/** + * Function to handle FC16 request. + * + * @param requestBuffer - request Buffer from client + * @param vector - vector of functions for read and write + * @param unitID - Id of the requesting unit + * @param {function} callback - callback to be invoked passing {Buffer} response + * @returns undefined + * @private + */ +function _handleWriteMultipleRegisters(requestBuffer, vector, unitID, callback) { + const address = requestBuffer.readUInt16BE(2); + const length = requestBuffer.readUInt16BE(4); + + // if length is bad, ignore message + if (requestBuffer.length !== (7 + length * 2 + 2)) { + return; + } + + // build answer + const responseBuffer = Buffer.alloc(8); + responseBuffer.writeUInt16BE(address, 2); + responseBuffer.writeUInt16BE(length, 4); + + // write registers + let callbackInvoked = false; + const cb = function(err) { + if (err) { + if (!callbackInvoked) { + callbackInvoked = true; + callback(err); + } + + return; + } + + if (!callbackInvoked) { + modbusSerialDebug({ action: "FC16 response", responseBuffer: responseBuffer }); + + callbackInvoked = true; + callback(null, responseBuffer); + } + }; + + if (length === 0) + callback({ + modbusErrorCode: 0x02, // Illegal address + msg: "Invalid length" + }); + if (vector.setRegisterArray) { + value = []; + + try { + for (i = 0; i < length; i++) { + value.push(requestBuffer.readUInt16BE(7 + i * 2)); + } + + if (vector.setRegisterArray.length === 4) { + vector.setRegisterArray(address, value, unitID, cb); + } + else { + var promiseOrValue = vector.setRegisterArray(address, value, unitID); + _handlePromiseOrValue(promiseOrValue, cb); + } + } + catch (err) { + cb(err); + } + } else if (vector.setRegister) { + var value; + + for (var i = 0; i < length; i++) { + try { + value = requestBuffer.readUInt16BE(7 + i * 2); + + if (vector.setRegister.length === 4) { + vector.setRegister(address + i, value, unitID, cb); + } + else { + const promiseOrValue = vector.setRegister(address + i, value, unitID); + _handlePromiseOrValue(promiseOrValue, cb); + } + } + catch(err) { + cb(err); + } + } + } +} + +/** + * Function to handle FC17 request. + * + * @param requestBuffer - request Buffer from client + * @param vector - vector of functions for read and write + * @param unitID - Id of the requesting unit + * @param {function} callback - callback to be invoked passing {Buffer} response + * @returns undefined + * @private + */ +function _handleReportServerID(requestBuffer, vector, unitID, callback) { + if(!vector.reportServerID) { + callback({ modbusErrorCode: 0x01 }); + return; + } + + // build answer + const promiseOrValue = vector.reportServerID(unitID); + _handlePromiseOrValue(promiseOrValue, function(err, value) { + if(err) { + callback(err); + return; + } + if (!value) { + callback({ modbusErrorCode: 0x01, msg: "Report Server ID not supported by device" }); + return; + } + if (!value.id || !value.running) { + callback({ modbusErrorCode: 0x04, msg: "Invalid content provided for Report Server ID: " + JSON.stringify(value) }); + return; + } + const id = value.id; + const running = value.running; + const additionalData = value.additionalData; + let contentLength = 2; // serverID + Running + if (additionalData) { + contentLength += additionalData.length; + } + const totalLength = 3 + contentLength + 2; // UnitID + FC + Byte-Count + Content-Length + CRC + + let i = 2; + const responseBuffer = Buffer.alloc(totalLength); + i = responseBuffer.writeUInt8(contentLength, i); + i = responseBuffer.writeUInt8(id, i); + if (running === true) { + i = responseBuffer.writeUInt8(0xFF, i); + } else { + i += 1; + } + if (additionalData) { + additionalData.copy(responseBuffer, i); + } + callback(null, responseBuffer); + }); +} + +/** + * Function to handle FC43 request. + * + * @param requestBuffer - request Buffer from client + * @param vector - vector of functions for read and write + * @param unitID - Id of the requesting unit + * @param {function} callback - callback to be invoked passing {Buffer} response + * @returns undefined + * @private + */ +function _handleMEI(requestBuffer, vector, unitID, callback) { + const MEIType = requestBuffer[2]; + switch(parseInt(MEIType)) { + case 14: + _handleReadDeviceIdentification(requestBuffer, vector, unitID, callback); + break; + default: + callback({ modbusErrorCode: 0x01 }); // illegal MEI type + } +} + +/** + * Function to handle FC43/14 MEI request. + * + * @param requestBuffer - request Buffer from client + * @param vector - vector of functions for read and write + * @param unitID - Id of the requesting unit + * @param {function} callback - callback to be invoked passing {Buffer} response + * @returns undefined + * @private + */ +function _handleReadDeviceIdentification(requestBuffer, vector, unitID, callback) { + const PDULenMax = 253; + const MEI14HeaderLen = 6; + const stringLengthMax = PDULenMax - MEI14HeaderLen - 2; + + if(!vector.readDeviceIdentification) { + callback({ modbusErrorCode: 0x01 }); + return; + } + + const readDeviceIDCode = requestBuffer.readUInt8(3); + let objectID = requestBuffer.readUInt8(4); + + // Basic request parameters checks + switch(readDeviceIDCode) { + case 0x01: + if(objectID > 0x02 || (objectID > 0x06 && objectID < 0x80)) + objectID = 0x00; + break; + + case 0x02: + if(objectID >= 0x80 || (objectID > 0x06 && objectID < 0x80)) + objectID = 0x00; + break; + + case 0x03: + if(objectID > 0x06 && objectID < 0x80) + objectID = 0x00; + break; + + case 0x04: + if(objectID > 0x06 && objectID < 0x80) { + callback({ modbusErrorCode: 0x02 }); + return; + } + break; + + default: + callback({ modbusErrorCode: 0x03 }); + return; + } + + // Filling mandatory basic device identification objects + const objects = { + 0x00: "undefined", + 0x01: "undefined", + 0x02: "undefined" + }; + + const pkg = require("../package.json"); + if(pkg) { + if(pkg.author) + objects[0x00] = pkg.author; + if(pkg.name) + objects[0x01] = pkg.name; + if(pkg.version) + objects[0x02] = pkg.version; + } + + const promiseOrValue = vector.readDeviceIdentification(unitID); + _handlePromiseOrValue(promiseOrValue, function(err, value) { + if(err) { + callback(err); + return; + } + + const userObjects = value; + + for(const o of Object.keys(userObjects)) { + const i = parseInt(o); + if(!isNaN(i) && i >= 0 && i <= 255) + objects[i] = userObjects[o]; + } + + // Checking the existence of the requested objectID + if(!objects[objectID]) { + if(readDeviceIDCode === 0x04) { + callback({ modbusErrorCode: 0x02 }); + return; + } + + objectID = 0x00; + } + + const ids = []; + let totalLength = 2 + MEI14HeaderLen + 2; // UnitID + FC + MEI14Header + CRC + let lastID = 0; + let conformityLevel = 0x81; + + const supportedIDs = Object.keys(objects); + + // Filtering of objects and Conformity level determination + for(var id of supportedIDs) { + id = parseInt(id); + + if(isNaN(id)) + continue; + + // Enforcing valid object IDs from the user + if(id < 0x00 || (id > 0x06 && id < 0x80) || id > 0xFF) { + callback({ modbusErrorCode: 0x04, msg: "Invalid Object ID provided for Read Device Identification: " + id }); + } + + if(id > 0x02) + conformityLevel = 0x82; + if(id > 0x80) + conformityLevel = 0x83; + + // Starting from requested object ID + if(objectID > id) + continue; + + // Enforcing maximum string length + if(objects[id].length > stringLengthMax) { + callback({ modbusErrorCode: 0x04, + msg: "Read Device Identification string size can be maximum " + + stringLengthMax }); + } + + if(lastID !== 0) + continue; + + if(objects[id].length + 2 > PDULenMax - totalLength) { + if(lastID === 0) + lastID = id; + } + else { + totalLength += objects[id].length + 2; + ids.push(id); + + // Requested a single object + if(readDeviceIDCode === 0x04) + break; + } + } + + ids.sort((a, b) => parseInt(a) - parseInt(b)); + const responseBuffer = Buffer.alloc(totalLength); + + let i = 2; + i = responseBuffer.writeUInt8(14, i); // MEI type + i = responseBuffer.writeUInt8(readDeviceIDCode, i); + i = responseBuffer.writeUInt8(conformityLevel, i); + if(lastID === 0) // More follows + i = responseBuffer.writeUInt8(0x00, i); + else + i = responseBuffer.writeUInt8(0xFF, i); + + i = responseBuffer.writeUInt8(lastID, i); // Next Object Id + i = responseBuffer.writeUInt8(ids.length, i); // Number of objects + + for(id of ids) { + i = responseBuffer.writeUInt8(id, i); // Object id + i = responseBuffer.writeUInt8(objects[id].length, i); // Object length + i += responseBuffer.write(objects[id], i, objects[id].length); // Object value + } + + callback(null, responseBuffer); + }); +} + +/** + * Exports + */ +module.exports = { + readCoilsOrInputDiscretes: _handleReadCoilsOrInputDiscretes, + readMultipleRegisters: _handleReadMultipleRegisters, + readMultipleRegistersEnron: _handleReadMultipleRegistersEnron, + readInputRegisters: _handleReadInputRegisters, + writeCoil: _handleWriteCoil, + writeSingleRegister: _handleWriteSingleRegister, + writeSingleRegisterEnron: _handleWriteSingleRegisterEnron, + forceMultipleCoils: _handleForceMultipleCoils, + writeMultipleRegisters: _handleWriteMultipleRegisters, + reportServerID: _handleReportServerID, + handleMEI: _handleMEI +}; diff --git a/test/servers/server.test.js b/test/servers/server.test.js new file mode 100644 index 0000000..8411c97 --- /dev/null +++ b/test/servers/server.test.js @@ -0,0 +1,241 @@ +"use strict"; +/* eslint-disable no-undef, no-console */ + +const expect = require("chai").expect; +const net = require("net"); +const Server = require("./../../servers/server"); + +describe("Modbus RTU Server (no serverID)", function() { + let server; // eslint-disable-line no-unused-vars + + before(function() { + const vector = { + getInputRegister: function(addr) { + return addr; + }, + getHoldingRegister: function(addr) { + console.log("getHoldingRegister", addr); + if (addr === 62) + throw new Error(); + + console.log("\tHolding register: ", addr); + + return addr + 8000; + }, + getCoil: function(addr) { + console.log("\tHolding register: ", addr); + return (addr % 2) === 0; + }, + setRegister: function(addr, value) { + console.log("\tset register", addr, value); + return; + }, + setCoil: function(addr, value) { + console.log("\tset coil", addr, value); + return; + } + }; + server = new Server(vector, { host: "0.0.0.0", port: 8512, debug: true }); + }); + + after(function() { + server.close(); + }); + + describe("function code handler", function() { + it("should receive a valid Modbus RTU message", function(done) { + const client = net.connect({ host: "0.0.0.0", port: 8512 }, function() { + // FC05 - force single coil, to on 0xff00 + console.log("connect"); + client.write(Buffer.from("01050005ff009c3b", "hex")); + }); + + client.once("data", function(data) { + // FC05 - valid responce + expect(data.toString("hex")).to.equal("01050005ff009c3b"); + + client.end(); + done(); + }); + }); + + // TODO: FC1 - FCX tests + }); + + describe("modbus exception handler", function() { + it("should receive a valid unhandled function Modbus RTU message", function(done) { + const client = net.connect({ host: "0.0.0.0", port: 8512 }, function() { + // FC07 - unhandled function + client.write(Buffer.from("0107000000000000", "hex")); + }); + + client.once("data", function(data) { + // A valid error message, code 0x01 - Illegal function + expect(data.toString("hex")).to.equal("0187018230"); + + client.end(); + done(); + }); + }); + + it("should receive a valid slave failure Modbus RTU message", function(done) { + const client = net.connect({ host: "0.0.0.0", port: 8512 }, function() { + // FC03 to error triggering address + client.write(Buffer.from("0103003E00010000", "hex")); + }); + + client.once("data", function(data) { + // A valid error message, code 0x04 - Slave failure + expect(data.toString("hex")).to.equal("01830440f3"); + + client.end(); + done(); + }); + }); + + // TODO: exceptions + }); + + describe("socket connection error", function() { + it("should receive an error event on socket closed by client", function(done) { + const client = net.connect({ host: "0.0.0.0", port: 8512 }, function() { + client.destroy(); + + server.emit("socketError"); + }); + + server.on("socketError", function() { + // Error handled correctly + client.end(); + done(); + }); + }); + + // TODO: exceptions + }); + + describe("large client request", function() { + it("should handle a large request without crash successfully (FC1)", function(done) { + const client = net.connect({ host: "0.0.0.0", port: 8512 }, function() { + // request 65535 registers at once + client.write(Buffer.from("0101003EFFFF0000", "hex")); + }); + + client.once("data", function(data) { + // A valid error message, code 0x04 - Slave failure + expect(data.toString("hex")).to.equal("0181044193"); + + client.end(); + done(); + }); + }); + + it("should handle a large request without crash successfully (FC3)", function(done) { + const client = net.connect({ host: "0.0.0.0", port: 8512 }, function() { + // request 65535 registers at once + client.write(Buffer.from("0103003EFFFF0000", "hex")); + }); + + client.once("data", function(data) { + // A valid error message, code 0x04 - Slave failure + expect(data.toString("hex")).to.equal("01830440f3"); + + client.end(); + done(); + }); + }); + + it("should handle a large request without crash successfully (FC4)", function(done) { + const client = net.connect({ host: "0.0.0.0", port: 8512 }, function() { + // request 65535 registers at once + client.write(Buffer.from("0104003EFFFF0000", "hex")); + }); + + client.once("data", function(data) { + // A valid error message, code 0x04 - Slave failure + expect(data.toString("hex")).to.equal("01840442c3"); + + client.end(); + done(); + }); + }); + + // TODO: exceptions + }); +}); + +describe("Modbus RTU Server (serverID = requestID)", function() { + let server; // eslint-disable-line no-unused-vars + + before(function() { + const vector = { + setCoil: function(addr, value) { + console.log("\tset coil", addr, value); + return; + } + }; + server = new Server(vector, { host: "0.0.0.0", port: 8512, debug: true, unitID: 0x04 }); + }); + + after(function() { + server.close(); + }); + + describe("function code handler", function() { + it("should receive a valid Modbus RTU message", function(done) { + const client = net.connect({ host: "0.0.0.0", port: 8512 }, function() { + // FC05 - force single coil, to on 0xff00 + client.write(Buffer.from("04050005ff000000", "hex")); + }); + + client.once("data", function(data) { + // FC05 - valid responce + expect(data.toString("hex")).to.equal("04050005ff009c6e"); + + client.end(); + done(); + }); + }); + }); +}); + +describe("Modbus RTU Server (serverID != requestID)", function() { + let server; // eslint-disable-line no-unused-vars + + before(function() { + const vector = { + setCoil: function(addr, value) { + console.log("\tset coil", addr, value); + return; + } + }; + server = new Server(vector, { host: "0.0.0.0", port: 8512, debug: true, unitID: 0x04 }); + }); + + after(function() { + server.close(); + }); + + describe("function code handler", function() { + it("should receive a no Modbus RTU message for wrong unitID", function(done) { + let timeout; + this.timeout(1000 + 100); + + const client = net.connect({ host: "0.0.0.0", port: 8512 }, function() { + // FC05 - force single coil, to on 0xff00 + client.write(Buffer.from("03050005ff00", "hex")); + timeout = setTimeout(done, 1000); + }); + + client.once("data", function(data) { + clearTimeout(timeout); + + // FC05 - we expect no data for wrong unitID + expect(data.toString("hex")).to.equal("NO DATA"); + + client.end(); + done(); + }); + }); + }); +}); diff --git a/test/servers/serverserial.test.js b/test/servers/serverserial.test.js index a081f88..1dde037 100644 --- a/test/servers/serverserial.test.js +++ b/test/servers/serverserial.test.js @@ -68,6 +68,9 @@ describe("Modbus Serial Server (no serverID)", function() { port: "/dev/server", portResponse: clientSerial, debug: true + }, + { + baudRate: 9600 }); }); @@ -258,6 +261,9 @@ describe("Modbus Serial Server (serverID = requestID)", function() { portResponse: clientSerial, debug: true, unitID: 4 + }, + { + baudRate: 9600 }); }); @@ -318,6 +324,9 @@ describe("Modbus Serial Server (serverID != requestID)", function() { portResponse: clientSerial, debug: true, unitID: 4 + }, + { + baudRate: 9600 }); });