UNPKG

modbus-serial

Version:

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

337 lines (283 loc) 9.94 kB
/* eslint-disable class-methods-use-this */ "use strict"; const events = require("events"); const EventEmitter = events.EventEmitter || events; const modbusSerialDebug = require("debug")("modbus-serial"); /* Add bit operation functions to Buffer */ require("../utils/buffer_bit")(); const crc16 = require("../utils/crc16"); const MIN_DATA_LENGTH = 7; class TestPort extends EventEmitter { /** * Simulate a serial port with 4 modbus-rtu slaves connected. * * 1 - a modbus slave working correctly * 2 - a modbus slave that answer short replays * 3 - a modbus slave that answer with bad crc * 4 - a modbus slave that answer with bad unit number * 5 - a modbus slave that answer with an exception * 6 - a modbus slave that times out (does not answer) */ constructor() { super(); // simulate 11 input registers this._registers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // simulate 11 holding registers this._holding_registers = [0, 0, 0, 0, 0, 0, 0, 0, 0xa12b, 0xffff, 0xb21a]; // simulate 16 coils / digital inputs this._coils = 0x0000; // TODO 0xa12b, 1010 0001 0010 1011 } /** * Check if port is open. * * @returns {boolean} */ get isOpen() { return true; } /** * Simulate successful port open. * * @param callback */ open(callback) { if (callback) callback(null); } /** * Simulate successful close port. * * @param callback */ close(callback) { if (callback) callback(null); } /** * Simulate successful/failure port requests and replays. * * @param {Buffer} data */ write(data) { let buffer = null; let length = null; let address = null; let value = null; let state = null; let i = null; if(data.length < MIN_DATA_LENGTH) { modbusSerialDebug("expected length of data is to small - minimum is " + MIN_DATA_LENGTH); return; } const unitNumber = data[0]; const functionCode = data[1]; let crc = data[data.length - 2] + data[data.length - 1] * 0x100; // if crc is bad, ignore message if (crc !== crc16(data.slice(0, -2))) { return; } // function code 1 and 2 if (functionCode === 1 || functionCode === 2) { address = data.readUInt16BE(2); length = data.readUInt16BE(4); // if length is bad, ignore message if (data.length !== 8) { return; } // build answer buffer = Buffer.alloc(3 + parseInt((length - 1) / 8 + 1) + 2); buffer.writeUInt8(parseInt((length - 1) / 8 + 1), 2); // read coils buffer.writeUInt16LE(this._coils >> address, 3); } // function code 3 if (functionCode === 3) { address = data.readUInt16BE(2); length = data.readUInt16BE(4); // if length is bad, ignore message if (data.length !== 8) { return; } // build answer buffer = Buffer.alloc(3 + length * 2 + 2); buffer.writeUInt8(length * 2, 2); // read registers for (i = 0; i < length; i++) { buffer.writeUInt16BE(this._holding_registers[address + i], 3 + i * 2); } } // function code 4 if (functionCode === 4) { address = data.readUInt16BE(2); length = data.readUInt16BE(4); // if length is bad, ignore message if (data.length !== 8) { return; } // build answer buffer = Buffer.alloc(3 + length * 2 + 2); buffer.writeUInt8(length * 2, 2); // read registers for (i = 0; i < length; i++) { buffer.writeUInt16BE(this._registers[address + i], 3 + i * 2); } } // function code 5 if (functionCode === 5) { address = data.readUInt16BE(2); state = data.readUInt16BE(4); // if length is bad, ignore message if (data.length !== 8) { return; } // build answer buffer = Buffer.alloc(8); buffer.writeUInt16BE(address, 2); buffer.writeUInt16BE(state, 4); // write coil if (state === 0xff00) { this._coils |= (1 << address); } else { this._coils &= ~(1 << address); } } // function code 6 if (functionCode === 6) { address = data.readUInt16BE(2); value = data.readUInt16BE(4); // if length is bad, ignore message if (data.length !== (6 + 2)) { return; } // build answer buffer = Buffer.alloc(8); buffer.writeUInt16BE(address, 2); buffer.writeUInt16BE(value, 4); this._holding_registers[address] = value; } // function code 15 if (functionCode === 15) { address = data.readUInt16BE(2); length = data.readUInt16BE(4); // if length is bad, ignore message if (data.length !== 7 + Math.ceil(length / 8) + 2) { return; } // build answer buffer = Buffer.alloc(8); buffer.writeUInt16BE(address, 2); buffer.writeUInt16BE(length, 4); // write coils for (i = 0; i < length; i++) { state = data.readBit(i, 7); if (state) { this._coils |= (1 << (address + i)); } else { this._coils &= ~(1 << (address + i)); } } } // function code 16 if (functionCode === 16) { address = data.readUInt16BE(2); length = data.readUInt16BE(4); // if length is bad, ignore message if (data.length !== (7 + length * 2 + 2)) { return; } // build answer buffer = Buffer.alloc(8); buffer.writeUInt16BE(address, 2); buffer.writeUInt16BE(length, 4); // write registers for (i = 0; i < length; i++) { this._holding_registers[address + i] = data.readUInt16BE(7 + i * 2); } } if (functionCode === 22) { address = data.readUInt16BE(2); const andMask = data.readUInt16BE(4); const orMask = data.readUInt16BE(6); if (data.length !== 10) { return; } const oldValue = this._holding_registers[address] || 0; this._holding_registers[address] = (oldValue & andMask) | (orMask & ~andMask); buffer = Buffer.alloc(10); buffer.writeUInt16BE(address, 2); buffer.writeUInt16BE(andMask, 4); buffer.writeUInt16BE(orMask, 6); } if (functionCode === 43) { const productCode = "MyProductCode1234"; buffer = Buffer.alloc(12 + productCode.length); buffer.writeUInt8(16, 2); // MEI Type buffer.writeUInt8(data.readInt8(3), 3); // read device ID code buffer.writeUInt8(0x01, 4); // conformity level buffer.writeUInt8(0, 5); // number of follows left buffer.writeUInt8(0, 6); // next object ID buffer.writeUInt8(1, 7); // number of objects buffer.writeUInt8(data.readInt8(4), 8); buffer.writeUInt8(productCode.length, 9); buffer.write(productCode, 10, productCode.length, "ascii"); } // send data back if (buffer) { // add unit number and function code buffer.writeUInt8(unitNumber, 0); buffer.writeUInt8(functionCode, 1); // corrupt the answer switch (unitNumber) { case 1: // unit 1: answers correctly break; case 2: // unit 2: answers short data buffer = buffer.slice(0, buffer.length - 5); break; case 4: // unit 4: answers with bad unit number buffer[0] = unitNumber + 2; break; case 5: // unit 5: answers with exception buffer.writeUInt8(functionCode + 128, 1); buffer.writeUInt8(4, 2); buffer = buffer.slice(0, 5); break; case 6: // unit 6: does not answer return; } // add crc crc = crc16(buffer.slice(0, -2)); buffer.writeUInt16LE(crc, buffer.length - 2); // unit 3: answers with bad crc if (unitNumber === 3) { buffer.writeUInt16LE(crc + 1, buffer.length - 2); } this.emit("data", buffer); modbusSerialDebug({ action: "send test port", data: data, buffer: buffer, unitid: unitNumber, functionCode: functionCode }); modbusSerialDebug(JSON.stringify({ action: "send test port strings", data: data, buffer: buffer, unitid: unitNumber, functionCode: functionCode })); } } } /** * Test port for Modbus. * * @type {TestPort} */ module.exports = TestPort;