UNPKG

modbus-serial

Version:

A pure JavaScript implemetation of MODBUS-RTU (Serial and TCP) for NodeJS.

353 lines (303 loc) 12.3 kB
"use strict"; /** * Copyright (c) 2017, Yaacov Zamir <kobi.zamir@gmail.com> * * 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"); const HOST = "127.0.0.1"; const UNIT_ID = 255; // listen to all adresses const MODBUS_PORT = 502; // Not really its official length, but we parse UnitID as part of PDU const MBAP_LEN = 6; /* Get Handlers */ const handlers = require("./servertcp_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 _serverDebug("server response", unitID, functionCode, responseBuffer); 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 < MBAP_LEN) { 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 ServerTCP extends EventEmitter { /** * Class making ModbusTCP 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.on("error", function(error) { modbus.emit("serverError", error); }); 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 > MBAP_LEN) { const transactionsId = recvBuffer.readUInt16BE(0); const pduLen = recvBuffer.readUInt16BE(4); // Check the presence of the full request (MBAP + PDU) if(recvBuffer.length - MBAP_LEN < pduLen) break; // remove mbap and add crc16 const requestBuffer = Buffer.alloc(pduLen + 2); recvBuffer.copy(requestBuffer, 0, MBAP_LEN, MBAP_LEN + pduLen); // Move receive buffer on recvBuffer = recvBuffer.slice(MBAP_LEN + pduLen); 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 outTcp = Buffer.alloc(responseBuffer.length + 6 - 2); outTcp.writeUInt16BE(transactionsId, 0); outTcp.writeUInt16BE(0, 2); outTcp.writeUInt16BE(responseBuffer.length - 2, 4); responseBuffer.copy(outTcp, 6); modbusSerialDebug({ action: "send", data: responseBuffer }); modbusSerialDebug(JSON.stringify({ action: "send string", data: responseBuffer })); // write to port sock.write(outTcp); } }; // 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" }); } } } /** * ServerTCP interface export. * @type {ServerTCP} */ module.exports = ServerTCP;