modbus-server
Version:
TypeScript Modbus TCP Server Implementation
408 lines • 19.2 kB
JavaScript
;
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