UNPKG

modbus-connect

Version:

Modbus RTU over Web Serial and Node.js SerialPort

442 lines (380 loc) 17.8 kB
// slave-emulator/SlaveEmulator.js const logger = require('../logger.js'); const { FUNCTION_CODES } = require('../constants/constants.js'); const ModbusExceptionError = require('../errors.js'); const crc16Modbus = require('../utils/crc.js'); class SlaveEmulator { constructor(slaveAddress = 1) { this.slaveAddress = slaveAddress; this.coils = new Map(); this.discreteInputs = new Map(); this.holdingRegisters = new Map(); this.inputRegisters = new Map(); this.exceptions = new Map(); this.log = logger.createLogger('SlaveEmulator'); this.connected = false; this._infinityTasks = new Map() } async connect() { this.log.info('Connecting to emulator...'); this.connected = true; this.log.info('Connected'); } async disconnect() { this.log.info('Disconnecting from emulator...'); this.connected = false; this.log.info('Disconnected'); } infinityChange({ typeRegister, register, range, interval }) { const key = `${typeRegister}:${register}` if (this._infinityTasks.has(key)) { clearInterval(this._infinityTasks.get(key)) } const [min, max] = range const setter = { Holding: this.setHoldingRegister.bind(this), Input: this.setInputRegister.bind(this), Coil: this.setCoil.bind(this), Discrete: this.setDiscreteInput.bind(this), }[typeRegister] if (!setter) { throw new Error(`Invalid register type: ${typeRegister}`) } const intervalId = setInterval(() => { const value = typeRegister === 'Holding' || typeRegister === 'Input' ? Math.floor(Math.random() * (max - min + 1)) + min : Math.random() < 0.5 setter(register, value) }, interval) this._infinityTasks.set(key, intervalId) } stopInfinityChange({ typeRegister, register }) { const key = `${typeRegister}:${register}` if (this._infinityTasks.has(key)) { clearInterval(this._infinityTasks.get(key)) this._infinityTasks.delete(key) } } setException(functionCode, address, exceptionCode) { this.exceptions.set(`${functionCode}_${address}`, exceptionCode); this.log.info(`Exception set: functionCode=0x${functionCode.toString(16)}, address=${address}, exceptionCode=0x${exceptionCode.toString(16)}`); } _checkException(functionCode, address) { const key = `${functionCode}_${address}`; if (this.exceptions.has(key)) { const exCode = this.exceptions.get(key); this.log.warn(`Throwing exception for function 0x${functionCode.toString(16)} at address ${address}: code 0x${exCode.toString(16)}`); throw new ModbusExceptionError(functionCode, exCode); } } addRegisters(definitions) { if (definitions.holding) { for (const { start, value } of definitions.holding) { this.setHoldingRegister(start, value); } } if (definitions.input) { for (const { start, value } of definitions.input) { this.setInputRegister(start, value); } } if (definitions.coils) { for (const { start, value } of definitions.coils) { this.setCoil(start, value); } } if (definitions.discrete) { for (const { start, value } of definitions.discrete) { this.setDiscreteInput(start, value); } } console.log('Registers added:', definitions); } setCoil(address, value) { this.coils.set(address, !!value); this.log.debug('Coil set:', address, !!value); } getCoil(address) { return this.coils.get(address) || false; } readCoils(startAddress, quantity) { this.log.info(`readCoils: start=${startAddress}, quantity=${quantity}`); for (let addr = startAddress; addr < startAddress + quantity; addr++) { this._checkException(FUNCTION_CODES.READ_COILS, addr); } const result = []; for (let i = 0; i < quantity; i++) { result.push(this.getCoil(startAddress + i)); } return result; } writeSingleCoil(address, value) { this._checkException(FUNCTION_CODES.WRITE_SINGLE_COIL, address); this.setCoil(address, value); this.log.info(`writeSingleCoil: address=${address}, value=${!!value}`); } writeMultipleCoils(startAddress, values) { values.forEach((val, idx) => { this._checkException(FUNCTION_CODES.WRITE_MULTIPLE_COILS, startAddress + idx); }); values.forEach((val, idx) => { this.setCoil(startAddress + idx, val); }); this.log.info(`writeMultipleCoils: startAddress=${startAddress}, values=${values}`); } setDiscreteInput(address, value) { this.discreteInputs.set(address, !!value); this.log.debug('Discrete Input set:', address, !!value); } getDiscreteInput(address) { return this.discreteInputs.get(address) || false; } readDiscreteInputs(startAddress, quantity) { this.log.info(`readDiscreteInputs: start=${startAddress}, quantity=${quantity}`); for (let addr = startAddress; addr < startAddress + quantity; addr++) { this._checkException(FUNCTION_CODES.READ_DISCRETE_INPUTS, addr); } const result = []; for (let i = 0; i < quantity; i++) { result.push(this.getDiscreteInput(startAddress + i)); } return result; } setHoldingRegister(address, value) { this.holdingRegisters.set(address, value & 0xFFFF); this.log.debug('Holding Register set:', address, value & 0xFFFF); } getHoldingRegister(address) { return this.holdingRegisters.get(address) || 0; } readHoldingRegisters(startAddress, quantity) { this.log.info(`readHoldingRegisters: start=${startAddress}, quantity=${quantity}`); for (let addr = startAddress; addr < startAddress + quantity; addr++) { this._checkException(FUNCTION_CODES.READ_HOLDING_REGISTERS, addr); } const result = []; for (let i = 0; i < quantity; i++) { result.push(this.getHoldingRegister(startAddress + i)); } return result; } writeSingleRegister(address, value) { this._checkException(FUNCTION_CODES.WRITE_SINGLE_REGISTER, address); this.setHoldingRegister(address, value); this.log.info(`writeSingleRegister: address=${address}, value=${value}`); } writeMultipleRegisters(startAddress, values) { values.forEach((val, idx) => { this._checkException(FUNCTION_CODES.WRITE_MULTIPLE_REGISTERS, startAddress + idx); }); values.forEach((val, idx) => { this.setHoldingRegister(startAddress + idx, val); }); this.log.info(`writeMultipleRegisters: startAddress=${startAddress}, values=${values}`); } setInputRegister(address, value) { this.inputRegisters.set(address, value & 0xFFFF); this.log.debug('Input Register set:', address, value & 0xFFFF); } getInputRegister(address) { return this.inputRegisters.get(address) || 0; } readInputRegisters(startAddress, quantity) { this.log.info(`readInputRegisters: start=${startAddress}, quantity=${quantity}`); for (let addr = startAddress; addr < startAddress + quantity; addr++) { this._checkException(FUNCTION_CODES.READ_INPUT_REGISTERS, addr); } const result = []; for (let i = 0; i < quantity; i++) { result.push(this.getInputRegister(startAddress + i)); } return result; } // --- Прямые методы (без RTU) --- readHolding(start, quantity) { const result = []; for (let i = 0; i < quantity; i++) { const addr = start + i; this._checkException(FUNCTION_CODES.READ_HOLDING_REGISTERS, addr); result.push(this.getHoldingRegister(addr)); } return result; } readInput(start, quantity) { const result = []; for (let i = 0; i < quantity; i++) { const addr = start + i; this._checkException(FUNCTION_CODES.READ_INPUT_REGISTERS, addr); result.push(this.getInputRegister(addr)); } return result; } // --- Modbus RTU Frame handler --- handleRequest(buffer) { if (!this.connected) { this.log.warn('Received request but emulator not connected'); return null; } if (!(buffer instanceof Uint8Array)) { throw new Error('Input buffer must be Uint8Array or Buffer'); } if (buffer.length < 5) { throw new Error('Invalid Modbus RTU frame: too short'); } const crcReceived = buffer[buffer.length - 2] | (buffer[buffer.length - 1] << 8); const crcCalculated = crc16Modbus(buffer.subarray(0, buffer.length - 2)); if (crcReceived !== crcCalculated) { this.log.warn(`CRC mismatch: received=0x${crcReceived.toString(16)}, calculated=0x${crcCalculated.toString(16)}`); return null; } const slaveAddr = buffer[0]; if (slaveAddr !== this.slaveAddress && slaveAddr !== 0) { this.log.debug(`Frame for slave ${slaveAddr} ignored (this slave: ${this.slaveAddress})`); return null; } const functionCode = buffer[1]; const data = buffer.subarray(2, buffer.length - 2); this.log.info(`Request: slave=${slaveAddr}, function=0x${functionCode.toString(16)}, data=${Buffer.from(data).toString('hex')}`); try { let responseData; switch (functionCode) { case FUNCTION_CODES.READ_COILS: { if (data.length !== 4) throw new Error('Invalid data length for Read Coils'); const startAddr = (data[0] << 8) | data[1]; const qty = (data[2] << 8) | data[3]; const coils = this.readCoils(startAddr, qty); const byteCount = Math.ceil(qty / 8); const resp = new Uint8Array(1 + byteCount); resp[0] = byteCount; for (let i = 0; i < qty; i++) { if (coils[i]) { resp[1 + Math.floor(i / 8)] |= 1 << (i % 8); } } responseData = resp; break; } case FUNCTION_CODES.READ_DISCRETE_INPUTS: { if (data.length !== 4) throw new Error('Invalid data length for Read Discrete Inputs'); const startAddr = (data[0] << 8) | data[1]; const qty = (data[2] << 8) | data[3]; const inputs = this.readDiscreteInputs(startAddr, qty); const byteCount = Math.ceil(qty / 8); const resp = new Uint8Array(1 + byteCount); resp[0] = byteCount; for (let i = 0; i < qty; i++) { if (inputs[i]) { resp[1 + Math.floor(i / 8)] |= 1 << (i % 8); } } responseData = resp; break; } case FUNCTION_CODES.READ_HOLDING_REGISTERS: { if (data.length !== 4) throw new Error('Invalid data length for Read Holding Registers'); const startAddr = (data[0] << 8) | data[1]; const qty = (data[2] << 8) | data[3]; const regs = this.readHoldingRegisters(startAddr, qty); const byteCount = qty * 2; const resp = new Uint8Array(1 + byteCount); resp[0] = byteCount; for (let i = 0; i < qty; i++) { resp[1 + i * 2] = regs[i] >> 8; resp[2 + i * 2] = regs[i] & 0xFF; } responseData = resp; break; } case FUNCTION_CODES.READ_INPUT_REGISTERS: { if (data.length !== 4) throw new Error('Invalid data length for Read Input Registers'); const startAddr = (data[0] << 8) | data[1]; const qty = (data[2] << 8) | data[3]; const regs = this.readInputRegisters(startAddr, qty); const byteCount = qty * 2; const resp = new Uint8Array(1 + byteCount); resp[0] = byteCount; for (let i = 0; i < qty; i++) { resp[1 + i * 2] = regs[i] >> 8; resp[2 + i * 2] = regs[i] & 0xFF; } responseData = resp; break; } case FUNCTION_CODES.WRITE_SINGLE_COIL: { if (data.length !== 4) throw new Error('Invalid data length for Write Single Coil'); const addr = (data[0] << 8) | data[1]; const val = (data[2] << 8) | data[3]; if (val !== 0x0000 && val !== 0xFF00) throw new Error('Invalid coil value'); this.writeSingleCoil(addr, val === 0xFF00); responseData = data; break; } case FUNCTION_CODES.WRITE_SINGLE_REGISTER: { if (data.length !== 4) throw new Error('Invalid data length for Write Single Register'); const addr = (data[0] << 8) | data[1]; const val = (data[2] << 8) | data[3]; this.writeSingleRegister(addr, val); responseData = data; break; } case FUNCTION_CODES.WRITE_MULTIPLE_COILS: { if (data.length < 5) throw new Error('Invalid data length for Write Multiple Coils'); const startAddr = (data[0] << 8) | data[1]; const qty = (data[2] << 8) | data[3]; const byteCount = data[4]; if (byteCount !== data.length - 5) throw new Error('Byte count mismatch'); const coilValues = []; for (let i = 0; i < qty; i++) { const byteIndex = 5 + Math.floor(i / 8); const bitIndex = i % 8; coilValues.push((data[byteIndex] & (1 << bitIndex)) !== 0); } this.writeMultipleCoils(startAddr, coilValues); responseData = data.subarray(0, 4); break; } case FUNCTION_CODES.WRITE_MULTIPLE_REGISTERS: { if (data.length < 5) throw new Error('Invalid data length for Write Multiple Registers'); const startAddr = (data[0] << 8) | data[1]; const qty = (data[2] << 8) | data[3]; const byteCount = data[4]; if (byteCount !== qty * 2) throw new Error('Byte count mismatch'); const regValues = []; for (let i = 0; i < qty; i++) { regValues.push((data[5 + i * 2] << 8) | data[6 + i * 2]); } this.writeMultipleRegisters(startAddr, regValues); responseData = data.subarray(0, 4); break; } default: this.log.warn(`Unsupported function code 0x${functionCode.toString(16)}`); throw new ModbusExceptionError(functionCode, 0x01); } const respBuf = new Uint8Array(2 + responseData.length + 2); respBuf[0] = this.slaveAddress; respBuf[1] = functionCode; respBuf.set(responseData, 2); const crc = crc16Modbus(respBuf.subarray(0, respBuf.length - 2)); respBuf[respBuf.length - 2] = crc & 0xFF; respBuf[respBuf.length - 1] = crc >> 8; this.log.info(`Response: ${Buffer.from(respBuf).toString('hex')}`); return respBuf; } catch (err) { if (err instanceof ModbusExceptionError) { const excBuf = new Uint8Array(5); excBuf[0] = this.slaveAddress; excBuf[1] = functionCode | 0x80; excBuf[2] = err.exceptionCode; const crc = crc16Modbus(excBuf.subarray(0, 3)); excBuf[3] = crc & 0xFF; excBuf[4] = crc >> 8; this.log.warn(`Exception response: ${Buffer.from(excBuf).toString('hex')}`); return excBuf; } this.log.error(`Unexpected error in handleRequest: ${err.message}`); return null; } } } module.exports = SlaveEmulator