UNPKG

modbus-server

Version:
408 lines 19.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ModbusTCPServer = void 0; const net = __importStar(require("net")); const modbus_types_1 = require("../types/modbus-types"); const modbus_data_store_1 = require("../memory/modbus-data-store"); class ModbusTCPServer { constructor(config = {}) { // Merge provided config with defaults this.config = { ...modbus_types_1.DEFAULT_MODBUS_CONFIG, ...config }; this.dataStore = new modbus_data_store_1.ModbusDataStore(this.config); this.server = net.createServer(); this.setupServer(); } setupServer() { this.server.on('connection', (socket) => { console.log(`[SERVER] New client connected: ${socket.remoteAddress}:${socket.remotePort}`); socket.on('data', (data) => { try { this.handleModbusRequest(socket, data); } catch (error) { console.error('[SERVER] Error handling request:', error); socket.destroy(); } }); socket.on('close', () => { console.log(`[SERVER] Client disconnected: ${socket.remoteAddress}:${socket.remotePort}`); }); socket.on('error', error => { console.error('[SERVER] Socket error:', error); }); }); this.server.on('error', error => { console.error('[SERVER] Server error:', error); }); } handleModbusRequest(socket, data) { if (data.length < 8) { // console.error('[SERVER] Invalid Modbus request - too short'); return; } // Parse Modbus TCP header const transactionId = data.readUInt16BE(0); const protocolId = data.readUInt16BE(2); // const length = data.readUInt16BE(4); // Not used in current implementation const unitId = data.readUInt8(6); const functionCode = data.readUInt8(7); // console.log(`[SERVER] Request - TID: ${transactionId}, Function: ${functionCode}, Unit: ${unitId}`); if (protocolId !== 0) { // console.error('[SERVER] Invalid protocol ID'); return; } try { const response = this.processModbusFunction(functionCode, data.slice(8), transactionId, unitId); socket.write(response); } catch (error) { // console.error(`[SERVER] Critical error processing request:`, error); const errorResponse = this.createErrorResponse(transactionId, unitId, functionCode, modbus_types_1.ModbusExceptionCode.SERVER_DEVICE_FAILURE); socket.write(errorResponse); } } processModbusFunction(functionCode, pdu, transactionId, unitId) { try { switch (functionCode) { case modbus_types_1.ModbusFunctionCode.READ_HOLDING_REGISTERS: return this.readHoldingRegisters(pdu, transactionId, unitId); case modbus_types_1.ModbusFunctionCode.READ_INPUT_REGISTERS: { const result = this.readInputRegisters(pdu, transactionId, unitId); return result; } case modbus_types_1.ModbusFunctionCode.READ_COILS: return this.readCoils(pdu, transactionId, unitId); case modbus_types_1.ModbusFunctionCode.READ_DISCRETE_INPUTS: return this.readDiscreteInputs(pdu, transactionId, unitId); case modbus_types_1.ModbusFunctionCode.WRITE_SINGLE_REGISTER: return this.writeSingleRegister(pdu, transactionId, unitId); case modbus_types_1.ModbusFunctionCode.WRITE_MULTIPLE_REGISTERS: return this.writeMultipleRegisters(pdu, transactionId, unitId); case modbus_types_1.ModbusFunctionCode.WRITE_SINGLE_COIL: return this.writeSingleCoil(pdu, transactionId, unitId); case modbus_types_1.ModbusFunctionCode.WRITE_MULTIPLE_COILS: return this.writeMultipleCoils(pdu, transactionId, unitId); default: throw new Error(`Unsupported function code: ${functionCode}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`[SERVER] Function ${functionCode} error: ${errorMessage}`); // Determine appropriate Modbus exception code let exceptionCode = modbus_types_1.ModbusExceptionCode.ILLEGAL_FUNCTION; // Default: Illegal Function if (errorMessage.includes('Address') && errorMessage.includes('outside valid range')) { exceptionCode = modbus_types_1.ModbusExceptionCode.ILLEGAL_DATA_ADDRESS; // Illegal Data Address } else if (errorMessage.includes('Quantity')) { exceptionCode = modbus_types_1.ModbusExceptionCode.ILLEGAL_DATA_VALUE; // Illegal Data Value } else if (errorMessage.includes('Byte count')) { exceptionCode = modbus_types_1.ModbusExceptionCode.ILLEGAL_DATA_VALUE; // Illegal Data Value } return this.createErrorResponse(transactionId, unitId, functionCode, exceptionCode); } } readHoldingRegisters(pdu, transactionId, unitId) { const startAddress = pdu.readUInt16BE(0); const quantity = pdu.readUInt16BE(2); // Validate quantity range (1-125 for read holding registers) if (quantity < 1 || quantity > 125) { throw new Error(`Read holding registers failed: Quantity ${quantity} is outside valid range (1-125)`); } const registers = this.dataStore.getHoldingRegisters(startAddress, quantity); const byteCount = quantity * 2; const responseBuffer = Buffer.alloc(9 + byteCount); responseBuffer.writeUInt16BE(transactionId, 0); responseBuffer.writeUInt16BE(0, 2); // Protocol ID responseBuffer.writeUInt16BE(3 + byteCount, 4); // Length responseBuffer.writeUInt8(unitId, 6); responseBuffer.writeUInt8(modbus_types_1.ModbusFunctionCode.READ_HOLDING_REGISTERS, 7); responseBuffer.writeUInt8(byteCount, 8); for (let i = 0; i < quantity; i++) { responseBuffer.writeUInt16BE(registers[i], 9 + i * 2); } return responseBuffer; } readInputRegisters(pdu, transactionId, unitId) { const startAddress = pdu.readUInt16BE(0); const quantity = pdu.readUInt16BE(2); // console.log(`[SERVER] Read input registers - Address: ${startAddress}, Quantity: ${quantity}`); // Validate quantity range (1-125 for read input registers) if (quantity < 1 || quantity > 125) { throw new Error(`Read input registers failed: Quantity ${quantity} is outside valid range (1-125)`); } const registers = this.dataStore.getInputRegisters(startAddress, quantity); const byteCount = quantity * 2; const responseBuffer = Buffer.alloc(9 + byteCount); responseBuffer.writeUInt16BE(transactionId, 0); responseBuffer.writeUInt16BE(0, 2); responseBuffer.writeUInt16BE(3 + byteCount, 4); responseBuffer.writeUInt8(unitId, 6); responseBuffer.writeUInt8(modbus_types_1.ModbusFunctionCode.READ_INPUT_REGISTERS, 7); responseBuffer.writeUInt8(byteCount, 8); for (let i = 0; i < quantity; i++) { responseBuffer.writeUInt16BE(registers[i], 9 + i * 2); } return responseBuffer; } readCoils(pdu, transactionId, unitId) { const startAddress = pdu.readUInt16BE(0); const quantity = pdu.readUInt16BE(2); // console.log(`[SERVER] Read coils - Address: ${startAddress}, Quantity: ${quantity}`); const coils = this.dataStore.getCoils(startAddress, quantity); const byteCount = Math.ceil(quantity / 8); const responseBuffer = Buffer.alloc(9 + byteCount); responseBuffer.writeUInt16BE(transactionId, 0); responseBuffer.writeUInt16BE(0, 2); responseBuffer.writeUInt16BE(3 + byteCount, 4); responseBuffer.writeUInt8(unitId, 6); responseBuffer.writeUInt8(modbus_types_1.ModbusFunctionCode.READ_COILS, 7); responseBuffer.writeUInt8(byteCount, 8); // Pack coils into bytes for (let byteIndex = 0; byteIndex < byteCount; byteIndex++) { let byte = 0; for (let bitIndex = 0; bitIndex < 8; bitIndex++) { const coilIndex = byteIndex * 8 + bitIndex; if (coilIndex < quantity && coils[coilIndex]) { byte |= 1 << bitIndex; } } responseBuffer.writeUInt8(byte, 9 + byteIndex); } return responseBuffer; } readDiscreteInputs(pdu, transactionId, unitId) { const startAddress = pdu.readUInt16BE(0); const quantity = pdu.readUInt16BE(2); // console.log(`[SERVER] Read discrete inputs - Address: ${startAddress}, Quantity: ${quantity}`); const inputs = this.dataStore.getDiscreteInputs(startAddress, quantity); const byteCount = Math.ceil(quantity / 8); const responseBuffer = Buffer.alloc(9 + byteCount); responseBuffer.writeUInt16BE(transactionId, 0); responseBuffer.writeUInt16BE(0, 2); responseBuffer.writeUInt16BE(3 + byteCount, 4); responseBuffer.writeUInt8(unitId, 6); responseBuffer.writeUInt8(modbus_types_1.ModbusFunctionCode.READ_DISCRETE_INPUTS, 7); responseBuffer.writeUInt8(byteCount, 8); // Pack inputs into bytes for (let byteIndex = 0; byteIndex < byteCount; byteIndex++) { let byte = 0; for (let bitIndex = 0; bitIndex < 8; bitIndex++) { const inputIndex = byteIndex * 8 + bitIndex; if (inputIndex < quantity && inputs[inputIndex]) { byte |= 1 << bitIndex; } } responseBuffer.writeUInt8(byte, 9 + byteIndex); } return responseBuffer; } writeSingleRegister(pdu, transactionId, unitId) { const address = pdu.readUInt16BE(0); const value = pdu.readUInt16BE(2); // console.log(`[SERVER] Write single register - Address: ${address}, Value: ${value}`); this.dataStore.setHoldingRegister(address, value); const responseBuffer = Buffer.alloc(12); responseBuffer.writeUInt16BE(transactionId, 0); responseBuffer.writeUInt16BE(0, 2); responseBuffer.writeUInt16BE(6, 4); responseBuffer.writeUInt8(unitId, 6); responseBuffer.writeUInt8(modbus_types_1.ModbusFunctionCode.WRITE_SINGLE_REGISTER, 7); responseBuffer.writeUInt16BE(address, 8); responseBuffer.writeUInt16BE(value, 10); return responseBuffer; } writeMultipleRegisters(pdu, transactionId, unitId) { const startAddress = pdu.readUInt16BE(0); const quantity = pdu.readUInt16BE(2); const byteCount = pdu.readUInt8(4); // console.log(`[SERVER] Write multiple registers - Address: ${startAddress}, Quantity: ${quantity}`); // Validate quantity range (1-123 for write multiple registers) if (quantity < 1 || quantity > 123) { throw new Error(`Write multiple registers failed: Quantity ${quantity} is outside valid range (1-123)`); } // Validate byte count if (byteCount !== quantity * 2) { throw new Error(`Write multiple registers failed: Byte count ${byteCount} doesn't match quantity ${quantity}`); } const values = []; for (let i = 0; i < quantity; i++) { values.push(pdu.readUInt16BE(5 + i * 2)); } this.dataStore.setHoldingRegisters(startAddress, values); const responseBuffer = Buffer.alloc(12); responseBuffer.writeUInt16BE(transactionId, 0); responseBuffer.writeUInt16BE(0, 2); responseBuffer.writeUInt16BE(6, 4); responseBuffer.writeUInt8(unitId, 6); responseBuffer.writeUInt8(modbus_types_1.ModbusFunctionCode.WRITE_MULTIPLE_REGISTERS, 7); responseBuffer.writeUInt16BE(startAddress, 8); responseBuffer.writeUInt16BE(quantity, 10); return responseBuffer; } writeSingleCoil(pdu, transactionId, unitId) { const address = pdu.readUInt16BE(0); const value = pdu.readUInt16BE(2) === 0xff00; // console.log(`[SERVER] Write single coil - Address: ${address}, Value: ${value}`); this.dataStore.setCoil(address, value); const responseBuffer = Buffer.alloc(12); responseBuffer.writeUInt16BE(transactionId, 0); responseBuffer.writeUInt16BE(0, 2); responseBuffer.writeUInt16BE(6, 4); responseBuffer.writeUInt8(unitId, 6); responseBuffer.writeUInt8(modbus_types_1.ModbusFunctionCode.WRITE_SINGLE_COIL, 7); responseBuffer.writeUInt16BE(address, 8); responseBuffer.writeUInt16BE(value ? 0xff00 : 0x0000, 10); return responseBuffer; } writeMultipleCoils(pdu, transactionId, unitId) { const startAddress = pdu.readUInt16BE(0); const quantity = pdu.readUInt16BE(2); const byteCount = pdu.readUInt8(4); // console.log(`[SERVER] Write multiple coils - Address: ${startAddress}, Quantity: ${quantity}`); const values = []; for (let byteIndex = 0; byteIndex < byteCount; byteIndex++) { const byte = pdu.readUInt8(5 + byteIndex); for (let bitIndex = 0; bitIndex < 8; bitIndex++) { const coilIndex = byteIndex * 8 + bitIndex; if (coilIndex < quantity) { values.push((byte & (1 << bitIndex)) !== 0); } } } this.dataStore.setCoils(startAddress, values); const responseBuffer = Buffer.alloc(12); responseBuffer.writeUInt16BE(transactionId, 0); responseBuffer.writeUInt16BE(0, 2); responseBuffer.writeUInt16BE(6, 4); responseBuffer.writeUInt8(unitId, 6); responseBuffer.writeUInt8(modbus_types_1.ModbusFunctionCode.WRITE_MULTIPLE_COILS, 7); responseBuffer.writeUInt16BE(startAddress, 8); responseBuffer.writeUInt16BE(quantity, 10); return responseBuffer; } createErrorResponse(transactionId, unitId, functionCode, errorCode) { const responseBuffer = Buffer.alloc(9); responseBuffer.writeUInt16BE(transactionId, 0); responseBuffer.writeUInt16BE(0, 2); responseBuffer.writeUInt16BE(3, 4); responseBuffer.writeUInt8(unitId, 6); responseBuffer.writeUInt8(functionCode | 0x80, 7); // Set error bit responseBuffer.writeUInt8(errorCode, 8); return responseBuffer; } start() { return new Promise((resolve, reject) => { this.server.listen(this.config.port, this.config.host, () => { console.log(`[SERVER] Modbus TCP Server listening on ${this.config.host}:${this.config.port}`); resolve(); }); this.server.on('error', error => { reject(error); }); }); } stop() { return new Promise(resolve => { // Set a timeout to force close if graceful shutdown takes too long const timeout = setTimeout(() => { console.log('[SERVER] Force closing server due to timeout'); this.server.close(); resolve(); }, 5000); // 5 second timeout this.server.close(() => { clearTimeout(timeout); console.log('[SERVER] Modbus TCP Server stopped'); resolve(); }); // Handle errors during close this.server.on('error', error => { clearTimeout(timeout); console.error('[SERVER] Error during shutdown:', error); resolve(); // Still resolve to allow process to exit }); }); } getDataStore() { return this.dataStore; } getConfig() { return this.config; } // Monitoring/Read functions for users to access data directly getHoldingRegisterValue(address) { return this.dataStore.getHoldingRegister(address); } getHoldingRegisterValues(startAddress, quantity) { return this.dataStore.getHoldingRegisters(startAddress, quantity); } getInputRegisterValue(address) { return this.dataStore.getInputRegister(address); } getInputRegisterValues(startAddress, quantity) { return this.dataStore.getInputRegisters(startAddress, quantity); } getCoilValue(address) { return this.dataStore.getCoil(address); } getCoilValues(startAddress, quantity) { return this.dataStore.getCoils(startAddress, quantity); } getDiscreteInputValue(address) { return this.dataStore.getDiscreteInput(address); } getDiscreteInputValues(startAddress, quantity) { return this.dataStore.getDiscreteInputs(startAddress, quantity); } getAllData() { return this.dataStore.getAllRegisters(); } getDataSummary() { const allData = this.dataStore.getAllRegisters(); return { holdingRegisters: Object.keys(allData.holding).length, inputRegisters: Object.keys(allData.input).length, coils: Object.keys(allData.coils).length, discreteInputs: Object.keys(allData.discreteInputs).length }; } clearAllData() { this.dataStore.clearAllData(); } } exports.ModbusTCPServer = ModbusTCPServer; //# sourceMappingURL=modbus-server.js.map