UNPKG

modbus-connect

Version:

Modbus RTU over Web Serial and Node.js SerialPort

1,360 lines (1,250 loc) 78.5 kB
// slave-emulator/slave-emulator.ts import Logger from '../logger.js'; import { LoggerInstance, LogContext as LoggerContext, InfinityChangeParams, StopInfinityChangeParams, SlaveEmulatorOptions, RegisterDefinitions, } from '../types/modbus-types.js'; import { ModbusFunctionCode, ModbusExceptionCode } from '../constants/constants.js'; import { ModbusExceptionError, ModbusInvalidAddressError, ModbusInvalidQuantityError, ModbusIllegalDataValueError, ModbusIllegalDataAddressError, ModbusSlaveDeviceFailureError, ModbusCRCError, ModbusResponseError, ModbusTimeoutError, ModbusFlushError, ModbusDataConversionError, ModbusBufferOverflowError, ModbusBufferUnderrunError, ModbusMemoryError, ModbusStackOverflowError, ModbusInvalidFunctionCodeError, ModbusSlaveBusyError, ModbusAcknowledgeError, ModbusMemoryParityError, ModbusGatewayPathUnavailableError, ModbusGatewayTargetDeviceError, ModbusGatewayBusyError, ModbusDataOverrunError, ModbusBroadcastError, ModbusConfigError, ModbusBaudRateError, ModbusSyncError, ModbusFrameBoundaryError, ModbusLRCError, ModbusChecksumError, ModbusParityError, ModbusNoiseError, ModbusFramingError, ModbusOverrunError, ModbusCollisionError, ModbusTooManyEmptyReadsError, ModbusInterFrameTimeoutError, ModbusSilentIntervalError, ModbusInvalidStartingAddressError, ModbusMalformedFrameError, ModbusInvalidFrameLengthError, ModbusInvalidTransactionIdError, ModbusUnexpectedFunctionCodeError, ModbusConnectionRefusedError, ModbusConnectionTimeoutError, ModbusNotConnectedError, ModbusAlreadyConnectedError, ModbusInsufficientDataError, } from '../errors.js'; import { crc16Modbus } from '../utils/crc.js'; class SlaveEmulator { private slaveAddress: number; private coils: Map<number, boolean>; private discreteInputs: Map<number, boolean>; private holdingRegisters: Map<number, number>; private inputRegisters: Map<number, number>; private exceptions: Map<string, number>; private _infinityTasks: Map<string, NodeJS.Timeout>; private loggerEnabled: boolean; private logger: LoggerInstance; public connected: boolean; constructor(slaveAddress: number = 1, options: SlaveEmulatorOptions = {}) { if (typeof slaveAddress !== 'number' || slaveAddress < 0 || slaveAddress > 247) { throw new ModbusInvalidAddressError(slaveAddress); } this.slaveAddress = slaveAddress; this.coils = new Map(); this.discreteInputs = new Map(); this.holdingRegisters = new Map(); this.inputRegisters = new Map(); this.exceptions = new Map(); this._infinityTasks = new Map(); this.loggerEnabled = !!options.loggerEnabled; const loggerInstance = new Logger(); this.logger = loggerInstance.createLogger('SlaveEmulator'); if (!this.loggerEnabled) { this.logger.setLevel('error'); } else { this.logger.setLevel('info'); } this.connected = false; } enableLogger(): void { if (!this.loggerEnabled) { this.loggerEnabled = true; this.logger.setLevel('info'); } } disableLogger(): void { if (this.loggerEnabled) { this.loggerEnabled = false; this.logger.setLevel('error'); } } async connect(): Promise<void> { this.logger.info('Connecting to emulator...', { slaveAddress: this.slaveAddress, } as LoggerContext); this.connected = true; this.logger.info('Connected', { slaveAddress: this.slaveAddress } as LoggerContext); } async disconnect(): Promise<void> { this.logger.info('Disconnecting from emulator...', { slaveAddress: this.slaveAddress, } as LoggerContext); this.connected = false; this.logger.info('Disconnected', { slaveAddress: this.slaveAddress } as LoggerContext); } private _validateAddress(address: number): void { if (typeof address !== 'number' || address < 0 || address > 0xffff) { throw new ModbusInvalidAddressError(address); } } private _validateQuantity(quantity: number, max: number = 125): void { if (typeof quantity !== 'number' || quantity <= 0 || quantity > max) { throw new ModbusInvalidQuantityError(quantity, 1, max); } } private _validateValue(value: unknown, isRegister: boolean = false): void { if (isRegister) { if (typeof value !== 'number') { throw new ModbusIllegalDataValueError(String(value), 'number between 0 and 65535'); } if (value < 0 || value > 0xffff) { throw new ModbusIllegalDataValueError(value, 'between 0 and 65535'); } } else { if (typeof value !== 'boolean') { throw new ModbusIllegalDataValueError(String(value), 'boolean'); } } } infinityChange(params: InfinityChangeParams): void { const { typeRegister, register, range, interval } = params; if ( !typeRegister || typeof register !== 'number' || !Array.isArray(range) || range.length !== 2 ) { throw new ModbusDataConversionError(params, 'valid InfinityChangeParams'); } if (typeof interval !== 'number' || interval <= 0) { throw new ModbusDataConversionError(interval, 'positive number'); } const key = `${typeRegister}:${register}`; this.stopInfinityChange({ typeRegister, register }); const [min, max] = range; const setters = { Holding: (addr: number, val: number | boolean) => this.setHoldingRegister(addr, val as number), Input: (addr: number, val: number | boolean) => this.setInputRegister(addr, val as number), Coil: (addr: number, val: number | boolean) => this.setCoil(addr, val as boolean), Discrete: (addr: number, val: number | boolean) => this.setDiscreteInput(addr, val as boolean), }; const setter = setters[typeRegister]; if (!setter) { throw new ModbusDataConversionError(typeRegister, 'valid register type'); } if (min > max) { throw new ModbusDataConversionError(range, 'valid range (min <= max)'); } const intervalId = setInterval(() => { try { const value = typeRegister === 'Holding' || typeRegister === 'Input' ? Math.floor(Math.random() * (max - min + 1)) + min : Math.random() < 0.5; setter(register, value); this.logger.debug('Infinity change updated', { typeRegister, register, value, slaveAddress: this.slaveAddress, } as LoggerContext); } catch (error: any) { if (error instanceof ModbusExceptionError) { this.logger.error('Modbus exception in infinity change task', { error: error.message, functionCode: error.functionCode, exceptionCode: error.exceptionCode, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusInvalidAddressError) { this.logger.error('Invalid address in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusInvalidQuantityError) { this.logger.error('Invalid quantity in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusIllegalDataValueError) { this.logger.error('Illegal data value in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusIllegalDataAddressError) { this.logger.error('Illegal data address in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusSlaveDeviceFailureError) { this.logger.error('Slave device failure in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusCRCError) { this.logger.error('CRC error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusResponseError) { this.logger.error('Response error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusTimeoutError) { this.logger.error('Timeout error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusFlushError) { this.logger.error('Flush error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusDataConversionError) { this.logger.error('Data conversion error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusBufferOverflowError) { this.logger.error('Buffer overflow error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusBufferUnderrunError) { this.logger.error('Buffer underrun error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusMemoryError) { this.logger.error('Memory error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusStackOverflowError) { this.logger.error('Stack overflow error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusInvalidFunctionCodeError) { this.logger.error('Invalid function code error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusSlaveBusyError) { this.logger.error('Slave busy error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusAcknowledgeError) { this.logger.error('Acknowledge error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusMemoryParityError) { this.logger.error('Memory parity error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusGatewayPathUnavailableError) { this.logger.error('Gateway path unavailable error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusGatewayTargetDeviceError) { this.logger.error('Gateway target device error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusGatewayBusyError) { this.logger.error('Gateway busy error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusDataOverrunError) { this.logger.error('Data overrun error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusBroadcastError) { this.logger.error('Broadcast error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusConfigError) { this.logger.error('Config error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusBaudRateError) { this.logger.error('Baud rate error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusSyncError) { this.logger.error('Sync error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusFrameBoundaryError) { this.logger.error('Frame boundary error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusLRCError) { this.logger.error('LRC error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusChecksumError) { this.logger.error('Checksum error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusParityError) { this.logger.error('Parity error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusNoiseError) { this.logger.error('Noise error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusFramingError) { this.logger.error('Framing error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusOverrunError) { this.logger.error('Overrun error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusCollisionError) { this.logger.error('Collision error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusTooManyEmptyReadsError) { this.logger.error('Too many empty reads error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusInterFrameTimeoutError) { this.logger.error('Inter-frame timeout error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusSilentIntervalError) { this.logger.error('Silent interval error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusInvalidStartingAddressError) { this.logger.error('Invalid starting address error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusMalformedFrameError) { this.logger.error('Malformed frame error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusInvalidFrameLengthError) { this.logger.error('Invalid frame length error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusInvalidTransactionIdError) { this.logger.error('Invalid transaction ID error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusUnexpectedFunctionCodeError) { this.logger.error('Unexpected function code error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusConnectionRefusedError) { this.logger.error('Connection refused error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusConnectionTimeoutError) { this.logger.error('Connection timeout error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusNotConnectedError) { this.logger.error('Not connected error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusAlreadyConnectedError) { this.logger.error('Already connected error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusInsufficientDataError) { this.logger.error('Insufficient data error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } else { this.logger.error('Error in infinity change task', { error: error.message, typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } } }, interval); this._infinityTasks.set(key, intervalId); this.logger.info('Infinity change started', { typeRegister, register, interval, slaveAddress: this.slaveAddress, } as LoggerContext); } stopInfinityChange(params: StopInfinityChangeParams): void { const { typeRegister, register } = params; const key = `${typeRegister}:${register}`; if (this._infinityTasks.has(key)) { const intervalId = this._infinityTasks.get(key); if (intervalId) { clearInterval(intervalId); } this._infinityTasks.delete(key); this.logger.debug('Infinity change stopped', { typeRegister, register, slaveAddress: this.slaveAddress, } as LoggerContext); } } setException(functionCode: number, address: number, exceptionCode: number): void { this._validateAddress(address); this.exceptions.set(`${functionCode}_${address}`, exceptionCode); this.logger.info( `Exception set: functionCode=0x${functionCode.toString(16)}, address=${address}, exceptionCode=0x${exceptionCode.toString(16)}`, { functionCode: `0x${functionCode.toString(16)}`, address, exceptionCode, slaveAddress: this.slaveAddress, } as LoggerContext ); } private _checkException(functionCode: number, address: number): void { this._validateAddress(address); const key = `${functionCode}_${address}`; if (this.exceptions.has(key)) { const exCode = this.exceptions.get(key)!; this.logger.warn( `Throwing exception for function 0x${functionCode.toString(16)} at address ${address}: code 0x${exCode.toString(16)}`, { functionCode: `0x${functionCode.toString(16)}`, address, exceptionCode: exCode, slaveAddress: this.slaveAddress, } as LoggerContext ); throw new ModbusExceptionError(functionCode, exCode); } } addRegisters(definitions: RegisterDefinitions): void { if (!definitions || typeof definitions !== 'object') { throw new ModbusDataConversionError(definitions, 'valid RegisterDefinitions object'); } const stats = { coils: 0, discrete: 0, holding: 0, input: 0 }; try { if (Array.isArray(definitions.coils)) { for (const { start, value } of definitions.coils) { this.setCoil(start, value as boolean); stats.coils++; } } if (Array.isArray(definitions.discrete)) { for (const { start, value } of definitions.discrete) { this.setDiscreteInput(start, value as boolean); stats.discrete++; } } if (Array.isArray(definitions.holding)) { for (const { start, value } of definitions.holding) { this.setHoldingRegister(start, value as number); stats.holding++; } } if (Array.isArray(definitions.input)) { for (const { start, value } of definitions.input) { this.setInputRegister(start, value as number); stats.input++; } } this.logger.info('Registers added successfully', { ...stats, slaveAddress: this.slaveAddress, } as LoggerContext); } catch (error: any) { if (error instanceof ModbusExceptionError) { this.logger.error('Modbus exception adding registers', { error: error.message, functionCode: error.functionCode, exceptionCode: error.exceptionCode, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusInvalidAddressError) { this.logger.error('Invalid address adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusInvalidQuantityError) { this.logger.error('Invalid quantity adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusIllegalDataValueError) { this.logger.error('Illegal data value adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusIllegalDataAddressError) { this.logger.error('Illegal data address adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusSlaveDeviceFailureError) { this.logger.error('Slave device failure adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusCRCError) { this.logger.error('CRC error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusResponseError) { this.logger.error('Response error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusTimeoutError) { this.logger.error('Timeout error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusFlushError) { this.logger.error('Flush error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusDataConversionError) { this.logger.error('Data conversion error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusBufferOverflowError) { this.logger.error('Buffer overflow error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusBufferUnderrunError) { this.logger.error('Buffer underrun error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusMemoryError) { this.logger.error('Memory error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusStackOverflowError) { this.logger.error('Stack overflow error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusInvalidFunctionCodeError) { this.logger.error('Invalid function code error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusSlaveBusyError) { this.logger.error('Slave busy error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusAcknowledgeError) { this.logger.error('Acknowledge error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusMemoryParityError) { this.logger.error('Memory parity error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusGatewayPathUnavailableError) { this.logger.error('Gateway path unavailable error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusGatewayTargetDeviceError) { this.logger.error('Gateway target device error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusGatewayBusyError) { this.logger.error('Gateway busy error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusDataOverrunError) { this.logger.error('Data overrun error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusBroadcastError) { this.logger.error('Broadcast error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusConfigError) { this.logger.error('Config error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusBaudRateError) { this.logger.error('Baud rate error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusSyncError) { this.logger.error('Sync error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusFrameBoundaryError) { this.logger.error('Frame boundary error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusLRCError) { this.logger.error('LRC error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusChecksumError) { this.logger.error('Checksum error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusParityError) { this.logger.error('Parity error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusNoiseError) { this.logger.error('Noise error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusFramingError) { this.logger.error('Framing error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusOverrunError) { this.logger.error('Overrun error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusCollisionError) { this.logger.error('Collision error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusTooManyEmptyReadsError) { this.logger.error('Too many empty reads error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusInterFrameTimeoutError) { this.logger.error('Inter-frame timeout error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusSilentIntervalError) { this.logger.error('Silent interval error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusInvalidStartingAddressError) { this.logger.error('Invalid starting address error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusMalformedFrameError) { this.logger.error('Malformed frame error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusInvalidFrameLengthError) { this.logger.error('Invalid frame length error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusInvalidTransactionIdError) { this.logger.error('Invalid transaction ID error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusUnexpectedFunctionCodeError) { this.logger.error('Unexpected function code error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusConnectionRefusedError) { this.logger.error('Connection refused error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusConnectionTimeoutError) { this.logger.error('Connection timeout error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusNotConnectedError) { this.logger.error('Not connected error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusAlreadyConnectedError) { this.logger.error('Already connected error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else if (error instanceof ModbusInsufficientDataError) { this.logger.error('Insufficient data error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } else { this.logger.error('Error adding registers', { error: error.message, definitions: JSON.stringify(definitions), slaveAddress: this.slaveAddress, } as LoggerContext); } throw error; } } setCoil(address: number, value: boolean): void { this._validateAddress(address); this._validateValue(value, false); this.coils.set(address, !!value); this.logger.debug('Coil set', { address, value: !!value, slaveAddress: this.slaveAddress, } as LoggerContext); } getCoil(address: number): boolean { this._validateAddress(address); return this.coils.get(address) || false; } readCoils(startAddress: number, quantity: number): boolean[] { this._validateAddress(startAddress); this._validateQuantity(quantity, 2000); if (startAddress + quantity > 0x10000) { throw new ModbusIllegalDataAddressError(startAddress, quantity); } this.logger.info('readCoils', { startAddress, quantity, slaveAddress: this.slaveAddress, } as LoggerContext); for (let addr = startAddress; addr < startAddress + quantity; addr++) { this._checkException(ModbusFunctionCode.READ_COILS, addr); } const result: boolean[] = []; for (let i = 0; i < quantity; i++) { result.push(this.getCoil(startAddress + i)); } return result; } writeSingleCoil(address: number, value: boolean): void { this._validateAddress(address); this._validateValue(value, false); this._checkException(ModbusFunctionCode.WRITE_SINGLE_COIL, address); this.setCoil(address, value); this.logger.info('writeSingleCoil', { address, value: !!value, slaveAddress: this.slaveAddress, } as LoggerContext); } writeMultipleCoils(startAddress: number, values: boolean[]): void { this._validateAddress(startAddress); this._validateQuantity(values.length, 1968); if (!Array.isArray(values)) { throw new ModbusDataConversionError(values, 'array'); } if (startAddress + values.length > 0x10000) { throw new ModbusIllegalDataAddressError(startAddress, values.length); } values.forEach((val, idx) => { this._validateValue(val, false); this._checkException(ModbusFunctionCode.WRITE_MULTIPLE_COILS, startAddress + idx); }); values.forEach((val, idx) => { this.setCoil(startAddress + idx, val); }); this.logger.info('writeMultipleCoils', { startAddress, values: values.length, slaveAddress: this.slaveAddress, } as LoggerContext); } setDiscreteInput(address: number, value: boolean): void { this._validateAddress(address); this._validateValue(value, false); this.discreteInputs.set(address, !!value); this.logger.debug('Discrete Input set', { address, value: !!value, slaveAddress: this.slaveAddress, } as LoggerContext); } getDiscreteInput(address: number): boolean { this._validateAddress(address); return this.discreteInputs.get(address) || false; } readDiscreteInputs(startAddress: number, quantity: number): boolean[] { this._validateAddress(startAddress); this._validateQuantity(quantity, 2000); if (startAddress + quantity > 0x10000) { throw new ModbusIllegalDataAddressError(startAddress, quantity); } this.logger.info('readDiscreteInputs', { startAddress, quantity, slaveAddress: this.slaveAddress, } as LoggerContext); for (let addr = startAddress; addr < startAddress + quantity; addr++) { this._checkException(ModbusFunctionCode.READ_DISCRETE_INPUTS, addr); } const result: boolean[] = []; for (let i = 0; i < quantity; i++) { result.push(this.getDiscreteInput(startAddress + i)); } return result; } setHoldingRegister(address: number, value: number): void { this._validateAddress(address); this._validateValue(value, true); const maskedValue = value & 0xffff; this.holdingRegisters.set(address, maskedValue); this.logger.debug('Holding Register set', { address, value: maskedValue, slaveAddress: this.slaveAddress, } as LoggerContext); } getHoldingRegister(address: number): number { this._validateAddress(address); return this.holdingRegisters.get(address) || 0; } readHoldingRegisters(startAddress: number, quantity: number): number[] { this._validateAddress(startAddress); this._validateQuantity(quantity, 125); if (startAddress + quantity > 0x10000) { throw new ModbusIllegalDataAddressError(startAddress, quantity); } this.logger.info('readHoldingRegisters', { startAddress, quantity, slaveAddress: this.slaveAddress, } as LoggerContext); for (let addr = startAddress; addr < startAddress + quantity; addr++) { this._checkException(ModbusFunctionCode.READ_HOLDING_REGISTERS, addr); } const result: number[] = []; for (let i = 0; i < quantity; i++) { result.push(this.getHoldingRegister(startAddress + i)); } return result; } writeSingleRegister(address: number, value: number): void { this._validateAddress(address); this._validateValue(value, true); this._checkException(ModbusFunctionCode.WRITE_SINGLE_REGISTER, address); this.setHoldingRegister(address, value); this.logger.info('writeSingleRegister', { address, value, slaveAddress: this.slaveAddress, } as LoggerContext); } writeMultipleRegisters(startAddress: number, values: number[]): void { this._validateAddress(startAddress); this._validateQuantity(values.length, 123); if (!Array.isArray(values)) { throw new ModbusDataConversionError(values, 'array'); } if (startAddress + values.length > 0x10000) { throw new ModbusIllegalDataAddressError(startAddress, values.length); } values.forEach((val, idx) => { this._validateValue(val, true); this._checkException(ModbusFunctionCode.WRITE_MULTIPLE_REGISTERS, startAddress + idx); }); values.forEach((val, idx) => { this.setHoldingRegister(startAddress + idx, val); }); this.logger.info('writeMultipleRegisters', { startAddress, values: values.length, slaveAddress: this.slaveAddress, } as LoggerContext); } setInputRegister(address: number, value: number): void { this._validateAddress(address); this._validateValue(value, true); const maskedValue = value & 0xffff; this.inputRegisters.set(address, maskedValue); this.logger.debug('Input Register set', { address, value: maskedValue, slaveAddress: this.slaveAddress, } as LoggerContext); } getInputRegister(address: number): number { this._validateAddress(address); return this.inputRegisters.get(address) || 0; } readInputRegisters(startAddress: number, quantity: number): number[] { this._validateAddress(startAddress); this._validateQuantity(quantity, 125); if (startAddress + quantity > 0x10000) { throw new ModbusIllegalDataAddressError(startAddress, quantity); } this.logger.info('readInputRegisters', { startAddress, quantity, slaveAddress: this.slaveAddress, } as LoggerContext); for (let addr = startAddress; addr < startAddress + quantity; addr++) { this._checkException(ModbusFunctionCode.READ_INPUT_REGISTERS, addr); } const result: number[] = []; for (let i = 0; i < quantity; i++) { result.push(this.getInputRegister(startAddress + i)); } return result; } // --- Прямые методы (без RTU) --- readHolding(start: number, quantity: number): number[] { this._validateAddress(start); this._validateQuantity(quantity, 125); if (start + quantity > 0x10000) { throw new ModbusIllegalDataAddressError(start, quantity); } const result: number[] = []; for (let i = 0; i < quantity; i++) { const addr = start + i; this._checkException(ModbusFunctionCode.READ_HOLDING_REGISTERS, addr); result.push(this.getHoldingRegister(addr)); } return result; } readInput(start: number, quantity: number): number[] { this._validateAddress(start); this._validateQuantity(quantity, 125); if (start + quantity > 0x10000) { throw new ModbusIllegalDataAddressError(start, quantity); } const result: number[] = []; for (let i = 0; i < quantity; i++) { const addr = start + i; this._checkException(ModbusFunctionCode.READ_INPUT_REGISTERS, addr); result.push(this.getInputRegister(addr)); } return result; } // --- Методы диагностики и мониторинга --- getRegisterStats(): { coils: number; discreteInputs: number; holdingRegisters: number; inputRegisters: number; exceptions: number; infinityTasks: number; } { return { coils: this.coils.size, discreteInputs: this.discreteInputs.size, holdingRegisters: this.holdingRegisters.size, inputRegisters: this.inputRegisters.size, exceptions: this.exceptions.size, infinityTasks: this._infinityTasks.size, }; } getRegisterDump(): { coils: { [key: number]: boolean }; discreteInputs: { [key: number]: boolean }; holdingRegisters: { [key: number]: number }; inputRegisters: { [key: number]: number }; } { return { coils: Object.fromEntries(this.coils), discreteInputs: Object.fromEntries(this.discreteInputs), holdingRegisters: Object.fromEntries(this.holdingRegisters), inputRegisters: Object.fromEntries(this.inputRegisters), }; } getInfinityTasks(): string[] { return Array.from(this._infinityTasks.keys()); } clearAllRegisters(): void { this.coils.clear(); this.discreteInputs.clear(); this.holdingRegisters.clear(); this.inputRegisters.clear(); this.logger.info('All registers cleared', { slaveAddress: this.slaveAddress } as LoggerContext); } clearExceptions(): void { this.exceptions.clear(); this.logger.info('All exceptions cleared', { slaveAddress: this.slaveAddress, } as LoggerContext); } clearInfinityTasks(): void { for (const intervalId of this._infinityTasks.values()) { clearInterval(intervalId); } this._infinityTasks.clear(); this.logger.info('All infinity tasks cleared', { slaveAddress: this.slaveAddress, } as LoggerContext); } // --- Graceful shutdown --- async destroy(): Promise<void> { this.logger.info('Destroying SlaveEmulator', { slaveAddress: this.slaveAddress, } as LoggerContext); this.clearInfinityTasks(); if (this.connected) { await this.disconnect(); } this.clearAllRegisters(); this.clearExceptions(); this.logger.info('SlaveEmulator destroyed', { slaveAddress: this.slaveAddress, } as LoggerContext); } // --- Modbus RTU Frame handler --- handleRequest(buffer: Uint8Array): Uint8Array | null { try { if (!this.connected) { this.logger.warn('Received request but emulator not connected', { slaveAddress: this.slaveAddress, } as LoggerContext); return null; } if (!(buffer instanceof Uint8Array)) { throw new ModbusDataConversionError(buffer, 'Uint8Array'); } if (buffer.length < 5) { throw new ModbusResponseError('Invalid Modbus RTU frame: too short'); } const crcReceived = (buffer[buffer.length - 2]! | (buffer[buffer.length - 1]! << 8)) & 0xffff; const dataForCrc = buffer.subarray(0, buffer.length - 2); const crcCalculatedBuffer = crc16Modbus(dataForCrc); if (crcCalculatedBuffer.length < 2) { throw new ModbusCRCError('cr