UNPKG

modbus-serial

Version:

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

287 lines (251 loc) 8.31 kB
"use strict"; const events = require("events"); const EventEmitter = events.EventEmitter || events; const net = require("net"); const modbusSerialDebug = require("debug")("modbus-serial"); /* TODO: const should be set once, maybe */ const EXCEPTION_LENGTH = 5; const MIN_DATA_LENGTH = 6; const TELNET_PORT = 2217; class TelnetPort extends EventEmitter { /** * Simulate a modbus-RTU port using Telent connection. * * @param ip * @param options * @constructor */ constructor(ip, options) { super(); const self = this; this.ip = ip; this.openFlag = false; this.callback = null; this._externalSocket = null; // options if(typeof ip === "object") { options = ip; this.ip = options.ip; } if (typeof options === "undefined") options = {}; this.port = options.port || TELNET_PORT; // telnet server port // internal buffer this._buffer = Buffer.alloc(0); this._id = 0; this._cmd = 0; this._length = 0; // handle callback - call a callback function only once, for the first event // it will triger const handleCallback = function(had_error) { if (self.callback) { self.callback(had_error); self.callback = null; } }; if(options.socket) { if(options.socket instanceof net.Socket) { this._externalSocket = options.socket; this.openFlag = this._externalSocket.readyState === "opening" || this._externalSocket.readyState === "open"; } else { throw new Error("invalid socket provided"); } } // create a socket this._client = this._externalSocket || new net.Socket(); if (options.timeout) this._client.setTimeout(options.timeout); // register the port data event this._client.on("data", function onData(data) { // add data to buffer self._buffer = Buffer.concat([self._buffer, data]); // check if buffer include a complete modbus answer const expectedLength = self._length; const bufferLength = self._buffer.length; modbusSerialDebug( "on data expected length:" + expectedLength + " buffer length:" + bufferLength ); modbusSerialDebug({ action: "receive tcp telnet port", data: data, buffer: self._buffer }); modbusSerialDebug( JSON.stringify({ action: "receive tcp telnet port strings", data: data, buffer: self._buffer }) ); // check data length if (expectedLength < 6 || bufferLength < EXCEPTION_LENGTH) return; // loop and check length-sized buffer chunks const maxOffset = bufferLength - EXCEPTION_LENGTH; for (let i = 0; i <= maxOffset; i++) { const unitId = self._buffer[i]; const functionCode = self._buffer[i + 1]; if (unitId !== self._id) continue; if ( functionCode === self._cmd && i + expectedLength <= bufferLength ) { self._emitData(i, expectedLength); return; } if ( functionCode === (0x80 | self._cmd) && i + EXCEPTION_LENGTH <= bufferLength ) { self._emitData(i, EXCEPTION_LENGTH); return; } // frame header matches, but still missing bytes pending if (functionCode === (0x7f & self._cmd)) break; } }); this._client.on("connect", function() { self.openFlag = true; handleCallback(); }); this._client.on("close", function(had_error) { self.openFlag = false; handleCallback(had_error); self.emit("close"); }); this._client.on("error", function(had_error) { self.openFlag = false; handleCallback(had_error); }); this._client.on("timeout", function() { // modbus.openFlag is left in its current state as it reflects two types of timeouts, // i.e. 'false' for "TCP connection timeout" and 'true' for "Modbus response timeout" // (this allows to continue Modbus request re-tries without reconnecting TCP). modbusSerialDebug("TelnetPort port: TimedOut"); handleCallback(new Error("TelnetPort Connection Timed Out.")); }); } /** * Check if port is open. * * @returns {boolean} */ get isOpen() { return this.openFlag; } /** * Emit the received response, cut the buffer and reset the internal vars. * * @param {number} start the start index of the response within the buffer * @param {number} length the length of the response * @private */ _emitData(start, length) { this.emit("data", this._buffer.slice(start, start + length)); this._buffer = this._buffer.slice(start + length); // reset internal vars this._id = 0; this._cmd = 0; this._length = 0; } /** * Simulate successful port open. * * @param callback */ open(callback) { if(this._externalSocket === null) { this.callback = callback; this._client.connect(this.port, this.ip); } else if(this.openFlag) { modbusSerialDebug("telnet port: external socket is opened"); callback(); // go ahead to setup existing socket } else { callback(new Error("telnet port: external socket is not opened")); } } /** * Simulate successful close port. * * @param callback */ close(callback) { this.callback = callback; this._client.end(); this.removeAllListeners(); } /** * Simulate successful destroy port. * * @param callback */ destroy(callback) { this.callback = callback; if (!this._client.destroyed) { this._client.destroy(); } } /** * Send data to a modbus slave via telnet server. * * @param {Buffer} data */ write(data) { if (data.length < MIN_DATA_LENGTH) { modbusSerialDebug( "expected length of data is to small - minimum is " + MIN_DATA_LENGTH ); return; } let length = null; // remember current unit and command this._id = data[0]; this._cmd = data[1]; // calculate expected answer length switch (this._cmd) { case 1: case 2: length = data.readUInt16BE(4); this._length = 3 + parseInt((length - 1) / 8 + 1) + 2; break; case 3: case 4: length = data.readUInt16BE(4); this._length = 3 + 2 * length + 2; break; case 5: case 6: case 15: case 16: this._length = 6 + 2; break; default: // raise and error ? this._length = 0; break; } // send buffer to slave this._client.write(data); modbusSerialDebug({ action: "send tcp telnet port", data: data, unitid: this._id, functionCode: this._cmd }); modbusSerialDebug( JSON.stringify({ action: "send tcp telnet port strings", data: data, unitid: this._id, functionCode: this._cmd }) ); } } /** * Telnet port for Modbus. * * @type {TelnetPort} */ module.exports = TelnetPort;