modbus-serial
Version:
A pure JavaScript implemetation of MODBUS-RTU (Serial and TCP) for NodeJS.
280 lines (244 loc) • 9.18 kB
JavaScript
"use strict";
const events = require("events");
const EventEmitter = events.EventEmitter || events;
const net = require("net");
const modbusSerialDebug = require("debug")("modbus-serial");
const crc16 = require("../utils/crc16");
/* TODO: const should be set once, maybe */
const MODBUS_PORT = 502; // modbus port
const MAX_TRANSACTIONS = 256; // maximum transaction to wait for
const MIN_DATA_LENGTH = 6;
const MIN_MBAP_LENGTH = 6;
const CRC_LENGTH = 2;
class TcpPort extends EventEmitter {
/**
* Simulate a modbus-RTU port using modbus-TCP connection.
*
* @param {string} ip - IP address of Modbus slave.
* @param {{
* port?: number,
* localAddress?: string,
* family?: 0|4|6,
* timeout?: number,
* socket?: net.Socket
* socketOpts?: {
* fd: number,
* allowHalfOpen?: boolean,
* readable?: boolean,
* writable?: boolean,
* signal?: AbortSignal
* },
* } & net.TcpSocketConnectOpts} options - Options object.
* options.port: Nonstandard Modbus port (default is 502).
* options.localAddress: Local IP address to bind to, default is any.
* options.family: 4 = IPv4-only, 6 = IPv6-only, 0 = either (default).
* @constructor
*/
constructor(ip, options) {
super();
const self = this;
/** @type {boolean} Flag to indicate if port is open */
this.openFlag = false;
/** @type {(err?: Error) => void} */
this.callback = null;
this._transactionIdWrite = 1;
/** @type {net.Socket?} - Optional custom socket */
this._externalSocket = null;
if (typeof ip === "object") {
options = ip;
ip = undefined;
}
if (typeof options === "undefined") options = {};
this.socketOpts = undefined;
if (options.socketOpts) {
this.socketOpts = options.socketOpts;
delete options.socketOpts;
}
/** @type {net.TcpSocketConnectOpts} - Options for net.connect(). */
this.connectOptions = {
// Default options
...{
host: ip || options.ip,
port: MODBUS_PORT
},
// User options
...options
};
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");
}
}
// handle callback - call a callback function only once, for the first event
// it will trigger
const handleCallback = function(had_error) {
if (self.callback) {
self.callback(had_error);
self.callback = null;
}
};
// init a socket
this._client = this._externalSocket || new net.Socket(this.socketOpts);
this._writeCompleted = Promise.resolve();
if (options.timeout) this._client.setTimeout(options.timeout);
// register events handlers
this._client.on("data", function(data) {
let buffer;
let crc;
let length;
// data recived
modbusSerialDebug({ action: "receive tcp port strings", data: data });
// check data length
while (data.length > MIN_MBAP_LENGTH) {
// parse tcp header length
length = data.readUInt16BE(4);
// cut 6 bytes of mbap and copy pdu
buffer = Buffer.alloc(length + CRC_LENGTH);
data.copy(buffer, 0, MIN_MBAP_LENGTH);
// add crc to message
crc = crc16(buffer.slice(0, -CRC_LENGTH));
buffer.writeUInt16LE(crc, buffer.length - CRC_LENGTH);
// update transaction id and emit data
self._transactionIdRead = data.readUInt16BE(0);
self.emit("data", buffer);
// debug
modbusSerialDebug({ action: "parsed tcp port", buffer: buffer, transactionId: self._transactionIdRead });
// reset data
data = data.slice(length + MIN_MBAP_LENGTH);
}
});
this._client.on("connect", function() {
self.openFlag = true;
self._writeCompleted = Promise.resolve();
modbusSerialDebug("TCP port: signal connect");
self._client.setNoDelay();
handleCallback();
});
this._client.on("close", function(had_error) {
if (self.openFlag) {
self.openFlag = false;
modbusSerialDebug("TCP port: signal close: " + had_error);
handleCallback(had_error);
self.emit("close");
self.removeAllListeners();
}
});
this._client.on("error", function(had_error) {
self.openFlag = false;
modbusSerialDebug("TCP port: signal error: " + had_error);
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("TCP port: TimedOut");
handleCallback(new Error("TCP Connection Timed Out"));
});
}
/**
* Check if port is open.
*
* @returns {boolean}
*/
get isOpen() {
return this.openFlag;
}
/**
* Simulate successful port open.
*
* @param {(err?: Error) => void} callback
*/
open(callback) {
if (this._externalSocket === null) {
this.callback = callback;
this._client.connect(this.connectOptions);
} else if (this.openFlag) {
modbusSerialDebug("TCP port: external socket is opened");
callback(); // go ahead to setup existing socket
} else {
callback(new Error("TCP port: external socket is not opened"));
}
}
/**
* Simulate successful close port.
*
* @param {(err?: Error) => void} callback
*/
close(callback) {
this.callback = callback;
// DON'T pass callback to `end()` here, it will be handled by client.on('close') handler
this._client.end();
}
/**
* Simulate successful destroy port.
*
* @param {(err?: Error) => void} callback
*/
destroy(callback) {
this.callback = callback;
if (!this._client.destroyed) {
this._client.destroy();
}
}
/**
* Send data to a modbus-tcp slave.
*
* @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;
}
// remember current unit and command
this._id = data[0];
this._cmd = data[1];
// remove crc and add mbap
const buffer = Buffer.alloc(data.length + MIN_MBAP_LENGTH - CRC_LENGTH);
buffer.writeUInt16BE(this._transactionIdWrite, 0);
buffer.writeUInt16BE(0, 2);
buffer.writeUInt16BE(data.length - CRC_LENGTH, 4);
data.copy(buffer, MIN_MBAP_LENGTH);
modbusSerialDebug({
action: "send tcp port",
data: data,
buffer: buffer,
unitid: this._id,
functionCode: this._cmd,
transactionsId: this._transactionIdWrite
});
// send buffer to slave
const previousWritePromise = this._writeCompleted;
const newWritePromise = new Promise((resolveNewWrite, rejectNewWrite) => {
// Wait for the completion of any write that happened before.
previousWritePromise.finally(() => {
try {
// The previous write succeeded, write the new buffer.
if (this._client.write(buffer)) {
// Mark this write as complete.
resolveNewWrite();
} else {
// Wait for one `drain` event to mark this write as complete.
this._client.once("drain", resolveNewWrite);
}
} catch (error) {
rejectNewWrite(error);
}
});
});
// Overwrite `_writeCompleted` so that the next call to `TcpPort.write` will have to wait on our write to complete.
this._writeCompleted = newWritePromise;
// set next transaction id
this._transactionIdWrite = (this._transactionIdWrite + 1) % MAX_TRANSACTIONS;
}
}
/**
* TCP port for Modbus.
*
* @type {TcpPort}
*/
module.exports = TcpPort;