UNPKG

modbus-serial

Version:

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

281 lines (236 loc) 8.94 kB
"use strict"; /* eslint-disable no-ternary */ const events = require("events"); const EventEmitter = events.EventEmitter || events; const SerialPort = require("serialport").SerialPort; const modbusSerialDebug = require("debug")("modbus-serial"); const crc16 = require("../utils/crc16"); const calculateLrc = require("./../utils/lrc"); /* TODO: const should be set once, maybe */ const MIN_DATA_LENGTH = 6; /** * Ascii encode a 'request' buffer and return it. This includes removing * the CRC bytes and replacing them with an LRC. * * @param {Buffer} buf the data buffer to encode. * @return {Buffer} the ascii encoded buffer * @private */ function _asciiEncodeRequestBuffer(buf) { // replace the 2 byte crc16 with a single byte lrc buf.writeUInt8(calculateLrc(buf.slice(0, -2)), buf.length - 2); // create a new buffer of the correct size const bufAscii = Buffer.alloc(buf.length * 2 + 1); // 1 byte start delimit + x2 data as ascii encoded + 2 lrc + 2 end delimit // create the ascii payload // start with the single start delimiter bufAscii.write(":", 0); // encode the data, with the new single byte lrc bufAscii.write(buf.toString("hex", 0, buf.length - 1).toUpperCase(), 1); // end with the two end delimiters bufAscii.write("\r", bufAscii.length - 2); bufAscii.write("\n", bufAscii.length - 1); return bufAscii; } /** * Ascii decode a 'response' buffer and return it. * * @param {Buffer} bufAscii the ascii data buffer to decode. * @return {Buffer} the decoded buffer, or null if decode error * @private */ function _asciiDecodeResponseBuffer(bufAscii) { // create a new buffer of the correct size (based on ascii encoded buffer length) const bufDecoded = Buffer.alloc((bufAscii.length - 1) / 2); // decode into new buffer (removing delimiters at start and end) for (let i = 0; i < (bufAscii.length - 3) / 2; i++) { bufDecoded.write(String.fromCharCode(bufAscii.readUInt8(i * 2 + 1), bufAscii.readUInt8(i * 2 + 2)), i, 1, "hex"); } // check the lrc is true const lrcIn = bufDecoded.readUInt8(bufDecoded.length - 2); if(calculateLrc(bufDecoded.slice(0, -2)) !== lrcIn) { // return null if lrc error const calcLrc = calculateLrc(bufDecoded.slice(0, -2)); modbusSerialDebug({ action: "LRC error", LRC: lrcIn.toString(16), calcLRC: calcLrc.toString(16) }); return null; } // replace the 1 byte lrc with a two byte crc16 bufDecoded.writeUInt16LE(crc16(bufDecoded.slice(0, -2)), bufDecoded.length - 2); return bufDecoded; } /** * check if a buffer chunk can be a modbus answer * or modbus exception * * @param {AsciiPort} modbus * @param {Buffer} buf the buffer to check. * @return {boolean} if the buffer can be an answer * @private */ function _checkData(modbus, buf) { // check buffer size if (buf.length !== modbus._length && buf.length !== 5) { modbusSerialDebug({ action: "length error", recive: buf.length, expected: modbus._length }); return false; } // check buffer unit-id and command return (buf[0] === modbus._id && (0x7f & buf[1]) === modbus._cmd); } class AsciiPort extends EventEmitter { /** * Simulate a modbus-ascii port using serial connection. * * @param path * @param options * @constructor */ constructor(path, options) { super(); const modbus = this; // options options = options || {}; // select char for start of slave frame (usually :) this._startOfSlaveFrameChar = (options.startOfSlaveFrameChar === undefined) ? 0x3A : options.startOfSlaveFrameChar; // disable auto open, as we handle the open options.autoOpen = false; // internal buffer this._buffer = Buffer.from(""); this._id = 0; this._cmd = 0; this._length = 0; // create the SerialPort this._client = new SerialPort(Object.assign({}, { path }, options)); // register the port data event this._client.on("data", function(data) { // add new data to buffer modbus._buffer = Buffer.concat([modbus._buffer, data]); modbusSerialDebug({ action: "receive serial ascii port", data: data, buffer: modbus._buffer }); modbusSerialDebug(JSON.stringify({ action: "receive serial ascii port strings", data: data, buffer: modbus._buffer })); // check buffer for start delimiter const sdIndex = modbus._buffer.indexOf(modbus._startOfSlaveFrameChar); if(sdIndex === -1) { // if not there, reset the buffer and return modbus._buffer = Buffer.from(""); return; } // if there is data before the start delimiter, remove it if(sdIndex > 0) { modbus._buffer = modbus._buffer.slice(sdIndex); } // do we have the complete message (i.e. are the end delimiters there) if(modbus._buffer.includes("\r\n", 1, "ascii") === true) { // check there is no excess data after end delimiters const edIndex = modbus._buffer.indexOf(0x0A); // ascii for '\n' if(edIndex !== modbus._buffer.length - 1) { // if there is, remove it modbus._buffer = modbus._buffer.slice(0, edIndex + 1); } // we have what looks like a complete ascii encoded response message, so decode const _data = _asciiDecodeResponseBuffer(modbus._buffer); modbusSerialDebug({ action: "got EOM", data: _data, buffer: modbus._buffer }); if(_data !== null) { // check if this is the data we are waiting for if (_checkData(modbus, _data)) { modbusSerialDebug({ action: "emit data serial ascii port", data: data, buffer: modbus._buffer }); modbusSerialDebug(JSON.stringify({ action: "emit data serial ascii port strings", data: data, buffer: modbus._buffer })); // emit a data signal modbus.emit("data", _data); } } // reset the buffer now its been used modbus._buffer = Buffer.from(""); } else { // otherwise just wait for more data to arrive } }); } /** * Check if port is open. * * @returns {boolean} */ get isOpen() { return this._client.isOpen; } /** * Simulate successful port open. * * @param callback */ open(callback) { this._client.open(callback); } /** * Simulate successful close port. * * @param callback */ close(callback) { this._client.close(callback); this.removeAllListeners(); } /** * Send data to a modbus slave. * * @param 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 (this is checked after ascii decoding) 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 ? modbusSerialDebug({ action: "unknown command", id: this._id.toString(16), command: this._cmd.toString(16) }); this._length = 0; break; } // ascii encode buffer const _encodedData = _asciiEncodeRequestBuffer(data); // send buffer to slave this._client.write(_encodedData); modbusSerialDebug({ action: "send serial ascii port", data: _encodedData, unitid: this._id, functionCode: this._cmd }); modbusSerialDebug(JSON.stringify({ action: "send serial ascii port", data: _encodedData, unitid: this._id, functionCode: this._cmd })); } } /** * ASCII port for Modbus. * * @type {AsciiPort} */ module.exports = AsciiPort;