UNPKG

modbus-connect

Version:

Modbus RTU over Web Serial and Node.js SerialPort

1,288 lines 89.3 kB
// src/polling-manager.ts import { Mutex } from 'async-mutex'; import Logger from './logger.js'; import { LogLevel, LogContext, LoggerInstance, PollingManagerConfig, PollingTaskOptions, PollingTaskState, PollingTaskStats, PollingQueueInfo, PollingSystemStats, } from './types/modbus-types.js'; import { ModbusFlushError, ModbusTimeoutError, PollingManagerError, PollingTaskAlreadyExistsError, PollingTaskNotFoundError, PollingTaskValidationError, ModbusCRCError, ModbusParityError, ModbusNoiseError, ModbusFramingError, ModbusOverrunError, ModbusCollisionError, ModbusConfigError, ModbusBaudRateError, ModbusSyncError, ModbusFrameBoundaryError, ModbusLRCError, ModbusChecksumError, ModbusDataConversionError, ModbusBufferOverflowError, ModbusBufferUnderrunError, ModbusMemoryError, ModbusStackOverflowError, ModbusResponseError, ModbusInvalidAddressError, ModbusInvalidFunctionCodeError, ModbusInvalidQuantityError, ModbusIllegalDataAddressError, ModbusIllegalDataValueError, ModbusSlaveBusyError, ModbusAcknowledgeError, ModbusSlaveDeviceFailureError, ModbusMalformedFrameError, ModbusInvalidFrameLengthError, ModbusInvalidTransactionIdError, ModbusUnexpectedFunctionCodeError, ModbusConnectionRefusedError, ModbusConnectionTimeoutError, ModbusNotConnectedError, ModbusAlreadyConnectedError, ModbusInsufficientDataError, ModbusGatewayPathUnavailableError, ModbusGatewayTargetDeviceError, ModbusInvalidStartingAddressError, ModbusMemoryParityError, ModbusBroadcastError, ModbusGatewayBusyError, ModbusDataOverrunError, ModbusTooManyEmptyReadsError, ModbusInterFrameTimeoutError, ModbusSilentIntervalError, } from './errors.js'; function hasTransportProperty(obj: unknown): obj is { transport: unknown } { return typeof obj === 'object' && obj !== null && 'transport' in obj; } function hasFlushMethod(obj: unknown): obj is { flush: () => Promise<void> } { return ( typeof obj === 'object' && obj !== null && 'flush' in obj && typeof (obj as { flush: unknown }).flush === 'function' ); } /** * TaskController управляет логикой одной задачи. */ class TaskController { public id: string; public resourceId?: string; public priority: number; public name: string | null; public fn: Array<() => Promise<unknown>>; public interval: number; public onData?: (data: unknown[]) => void; public onError?: (error: Error, fnIndex: number, retryCount: number) => void; public onStart?: () => void; public onStop?: () => void; public onFinish?: (success: boolean, results: unknown[]) => void; public onBeforeEach?: () => void; public onRetry?: (error: Error, fnIndex: number, retryCount: number) => void; public shouldRun?: () => boolean; public onSuccess?: (result: unknown) => void; public onFailure?: (error: Error) => void; public maxRetries: number; public backoffDelay: number; public taskTimeout: number; public stopped: boolean; public paused: boolean; public loopRunning: boolean; public executionInProgress: boolean; public stats: PollingTaskStats; private transportMutex: Mutex; public logger: LoggerInstance; private pollingManager: PollingManager; constructor(options: PollingTaskOptions, pollingManager: PollingManager) { const { id, resourceId, priority = 0, interval, fn, onData, onError, onStart, onStop, onFinish, onBeforeEach, onRetry, shouldRun, onSuccess, onFailure, name = null, maxRetries = 3, backoffDelay = 1000, taskTimeout = 5000, } = options; this.id = id; this.resourceId = resourceId; this.priority = priority; this.name = name; this.fn = Array.isArray(fn) ? fn : [fn]; this.interval = interval; this.onData = onData; this.onError = onError; this.onStart = onStart; this.onStop = onStop; this.onFinish = onFinish; this.onBeforeEach = onBeforeEach; this.onRetry = onRetry; this.shouldRun = shouldRun; this.onSuccess = onSuccess; this.onFailure = onFailure; this.maxRetries = maxRetries; this.backoffDelay = backoffDelay; this.taskTimeout = taskTimeout; this.stopped = true; this.paused = false; this.loopRunning = false; this.executionInProgress = false; this.pollingManager = pollingManager; this.stats = { totalRuns: 0, totalErrors: 0, lastError: null, lastResult: null, lastRunTime: null, retries: 0, successes: 0, failures: 0, }; this.transportMutex = new Mutex(); this.logger = pollingManager.loggerInstance.createLogger('TaskController'); this.logger.setLevel('error'); this.logger.debug('TaskController created', { id, resourceId: resourceId || undefined, priority, interval, maxRetries, backoffDelay, taskTimeout, } as LogContext); } /** * Запускает задачу. */ async start(): Promise<void> { if (!this.stopped) { this.logger.debug('Task already running'); return; } this.stopped = false; this.loopRunning = true; this.logger.info('Task started', { id: this.id } as LogContext); this.onStart?.(); if (this.resourceId) { this.scheduleRun(); } else { this._runLoop(); } } /** * Останавливает задачу. */ stop(): void { if (this.stopped) { this.logger.debug('Task already stopped', { id: this.id } as LogContext); return; } this.stopped = true; this.loopRunning = false; this.logger.info('Task stopped', { id: this.id } as LogContext); this.onStop?.(); } /** * Ставит задачу на паузу. */ pause(): void { if (this.paused) { this.logger.debug('Task already paused', { id: this.id } as LogContext); return; } this.paused = true; this.logger.info('Task paused', { id: this.id } as LogContext); } /** * Возобновляет задачу. */ resume(): void { if (!this.stopped && this.paused) { this.paused = false; this.logger.info('Task resumed', { id: this.id } as LogContext); this.scheduleRun(); } else { this.logger.debug('Cannot resume task - not paused or stopped', { id: this.id, } as LogContext); } } /** * Планирует запуск задачи. */ scheduleRun(): void { if (this.stopped) { this.logger.debug('Cannot schedule run - task is stopped', { id: this.id } as LogContext); return; } if (this.resourceId && this.pollingManager.queues.has(this.resourceId)) { const queue = this.pollingManager.queues.get(this.resourceId); queue?.markTaskReady(this); } else if (!this.resourceId) { if (this.stopped) { this.start(); } else if (!this.loopRunning) { this.loopRunning = true; this._runLoop(); } } } /** * Выполняет задачу один раз. * @returns {Promise<void>} */ async executeOnce(): Promise<void> { if (this.stopped || this.paused) { this.logger.debug('Cannot execute - task is stopped or paused', { id: this.id, } as LogContext); return; } if (this.shouldRun && this.shouldRun() === false) { this.logger.debug('Task should not run according to shouldRun function', { id: this.id, } as LogContext); this._scheduleNextRun(); return; } this.onBeforeEach?.(); this.executionInProgress = true; this.stats.totalRuns++; this.logger.debug('Executing task once', { id: this.id } as LogContext); const release = await this.transportMutex.acquire(); try { // Проверяем существование transport перед обращением к его методам const firstFunction = this.fn[0]; if (firstFunction && typeof firstFunction === 'function') { const result = firstFunction(); // Используем type guard для проверки наличия свойства transport if (result && hasTransportProperty(result) && result.transport) { // Используем type guard для проверки наличия метода flush у transport if (hasFlushMethod(result.transport)) { try { await result.transport.flush(); this.logger.debug('Transport flushed successfully', { id: this.id } as LogContext); } catch (flushErr: unknown) { const error = flushErr instanceof Error ? flushErr : new PollingManagerError(String(flushErr)); this.logger.warn('Flush failed', { id: this.id, error: error.message } as LogContext); } } } } let success = false; const results: unknown[] = []; for (let fnIndex = 0; fnIndex < this.fn.length; fnIndex++) { let retryCount = 0; let result: unknown = null; let fnSuccess = false; while (!this.stopped && retryCount <= this.maxRetries) { if (this.paused) { this.logger.debug('Task paused during execution', { id: this.id } as LogContext); this.executionInProgress = false; return; } try { const fnResult = this.fn[fnIndex]; // Извлекаем элемент массива if (typeof fnResult !== 'function') { // Проверяем, что это функция throw new PollingManagerError( `Task ${this.id} fn at index ${fnIndex} is not a function` ); } const promiseResult = fnResult(); // Вызываем функцию if (!(promiseResult instanceof Promise)) { throw new PollingManagerError( `Task ${this.id} fn ${fnIndex} did not return a Promise` ); } result = await this._withTimeout(promiseResult, this.taskTimeout); // Ждем результата fnSuccess = true; this.stats.successes++; this.stats.lastError = null; break; } catch (err: unknown) { const error = err instanceof Error ? err : new PollingManagerError(String(err)); // Обработка конкретных ошибок Modbus if (error instanceof ModbusTimeoutError) { this.logger.error('Modbus timeout error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusCRCError) { this.logger.error('Modbus CRC error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusParityError) { this.logger.error('Modbus parity error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusNoiseError) { this.logger.error('Modbus noise error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusFramingError) { this.logger.error('Modbus framing error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusOverrunError) { this.logger.error('Modbus overrun error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusCollisionError) { this.logger.error('Modbus collision error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusConfigError) { this.logger.error('Modbus config error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusBaudRateError) { this.logger.error('Modbus baud rate error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusSyncError) { this.logger.error('Modbus sync error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusFrameBoundaryError) { this.logger.error('Modbus frame boundary error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusLRCError) { this.logger.error('Modbus LRC error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusChecksumError) { this.logger.error('Modbus checksum error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusDataConversionError) { this.logger.error('Modbus data conversion error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusBufferOverflowError) { this.logger.error('Modbus buffer overflow error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusBufferUnderrunError) { this.logger.error('Modbus buffer underrun error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusMemoryError) { this.logger.error('Modbus memory error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusStackOverflowError) { this.logger.error('Modbus stack overflow error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusResponseError) { this.logger.error('Modbus response error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusInvalidAddressError) { this.logger.error('Modbus invalid address error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusInvalidFunctionCodeError) { this.logger.error('Modbus invalid function code error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusInvalidQuantityError) { this.logger.error('Modbus invalid quantity error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusIllegalDataAddressError) { this.logger.error('Modbus illegal data address error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusIllegalDataValueError) { this.logger.error('Modbus illegal data value error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusSlaveBusyError) { this.logger.error('Modbus slave busy error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusAcknowledgeError) { this.logger.error('Modbus acknowledge error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusSlaveDeviceFailureError) { this.logger.error('Modbus slave device failure error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusMalformedFrameError) { this.logger.error('Modbus malformed frame error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusInvalidFrameLengthError) { this.logger.error('Modbus invalid frame length error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusInvalidTransactionIdError) { this.logger.error('Modbus invalid transaction ID error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusUnexpectedFunctionCodeError) { this.logger.error('Modbus unexpected function code error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusConnectionRefusedError) { this.logger.error('Modbus connection refused error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusConnectionTimeoutError) { this.logger.error('Modbus connection timeout error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusNotConnectedError) { this.logger.error('Modbus not connected error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusAlreadyConnectedError) { this.logger.error('Modbus already connected error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusInsufficientDataError) { this.logger.error('Modbus insufficient data error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusGatewayPathUnavailableError) { this.logger.error('Modbus gateway path unavailable error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusGatewayTargetDeviceError) { this.logger.error('Modbus gateway target device error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusInvalidStartingAddressError) { this.logger.error('Modbus invalid starting address error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusMemoryParityError) { this.logger.error('Modbus memory parity error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusBroadcastError) { this.logger.error('Modbus broadcast error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusGatewayBusyError) { this.logger.error('Modbus gateway busy error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusDataOverrunError) { this.logger.error('Modbus data overrun error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusTooManyEmptyReadsError) { this.logger.error('Modbus too many empty reads error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusInterFrameTimeoutError) { this.logger.error('Modbus inter-frame timeout error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusSilentIntervalError) { this.logger.error('Modbus silent interval error', { id: this.id, error: error.message, } as LogContext); } retryCount++; this.stats.totalErrors++; this.stats.retries++; this.stats.lastError = error; this.onRetry?.(error, fnIndex, retryCount); const isFlushedError = error instanceof ModbusFlushError; let backoffDelay = this.backoffDelay; if (isFlushedError) { this.logger.debug('Flush error detected, resetting backoff', { id: this.id, } as LogContext); backoffDelay = this.backoffDelay; } const delay = isFlushedError ? Math.min(50, backoffDelay) : backoffDelay * Math.pow(2, retryCount - 1); if (retryCount > this.maxRetries) { this.stats.failures++; this.onFailure?.(error); this.onError?.(error, fnIndex, retryCount); this.logger.warn('Max retries exhausted for fn[' + fnIndex + ']', { id: this.id, fnIndex, retryCount, error: error.message, } as LogContext); } else { this.logger.debug('Retrying fn[' + fnIndex + '] with delay', { id: this.id, delay, retryCount, } as LogContext); await this._sleep(delay); if (this.stopped) { this.executionInProgress = false; return; } } } } results.push(result); success = success || fnSuccess; } this.stats.lastResult = results; this.stats.lastRunTime = Date.now(); if (results.length > 0 && results.some(r => r !== null && r !== undefined)) { this.onData?.(results); // Используем только разрешенные поля в LogContext this.logger.debug('Data callback executed', { id: this.id, resultsCount: results.length, } as LogContext); } else { this.logger.warn('Skipping onData - all results invalid', { id: this.id, results: 'invalid', } as LogContext); } this.onFinish?.(success, results); // Используем только разрешенные поля в LogContext this.logger.info('Task execution completed', { id: this.id, success, resultsCount: results.length, } as LogContext); this.pollingManager.loggerInstance.flush(); } finally { release(); this.executionInProgress = false; } this._scheduleNextRun(); } /** * Планирует следующий запуск задачи. * @private */ _scheduleNextRun(): void { if (!this.stopped && this.resourceId) { setTimeout(() => { if (!this.stopped) { this.logger.debug('Scheduling next run (queued)', { id: this.id } as LogContext); this.scheduleRun(); } }, this.interval); } else if (!this.stopped && !this.resourceId) { setTimeout(() => { if (!this.stopped && this.loopRunning) { this.logger.debug('Scheduling next run (loop)', { id: this.id } as LogContext); this._runLoop(); } }, this.interval); } } /** * Проверяет, запущена ли задача. */ isRunning(): boolean { return !this.stopped; } /** * Проверяет, приостановлена ли задача. */ isPaused(): boolean { return this.paused; } /** * Устанавливает интервал задачи. */ setInterval(ms: number): void { this.interval = ms; this.logger.info('Interval updated', { id: this.id, interval: ms } as LogContext); } /** * Возвращает состояние задачи. */ getState(): PollingTaskState { return { stopped: this.stopped, paused: this.paused, running: this.loopRunning, inProgress: this.executionInProgress, }; } /** * Возвращает статистику задачи. */ getStats(): PollingTaskStats { return { ...this.stats }; } /** * Оригинальный цикл выполнения задачи (для задач без resourceId). * @private */ async _runLoop(): Promise<void> { let backoffDelay = this.backoffDelay; this.logger.info('Starting run loop', { id: this.id } as LogContext); while (this.loopRunning && !this.stopped) { if (this.paused) { this.logger.debug('Task paused in loop', { id: this.id } as LogContext); await this._sleep(this.interval); continue; } if (this.shouldRun && this.shouldRun() === false) { this.logger.debug('Task should not run according to shouldRun function', { id: this.id, } as LogContext); await this._sleep(this.interval); continue; } this.onBeforeEach?.(); this.executionInProgress = true; this.stats.totalRuns++; const release = await this.transportMutex.acquire(); try { // Проверяем существование transport перед обращением к его методам const firstFunction = this.fn[0]; if (firstFunction && typeof firstFunction === 'function') { const result = firstFunction(); // Используем type guard для проверки наличия свойства transport if (result && hasTransportProperty(result) && result.transport) { // Используем type guard для проверки наличия метода flush у transport if (hasFlushMethod(result.transport)) { try { await result.transport.flush(); this.logger.debug('Transport flushed successfully', { id: this.id } as LogContext); } catch (flushErr: unknown) { const error = flushErr instanceof Error ? flushErr : new PollingManagerError(String(flushErr)); this.logger.warn('Flush failed', { id: this.id, error: error.message, } as LogContext); } } } } let success = false; const results: unknown[] = []; for (let fnIndex = 0; fnIndex < this.fn.length; fnIndex++) { let retryCount = 0; let result: unknown = null; let fnSuccess = false; while (this.loopRunning && !this.stopped && retryCount <= this.maxRetries) { try { const fnResult = this.fn[fnIndex]; // Извлекаем элемент массива if (typeof fnResult !== 'function') { // Проверяем, что это функция throw new PollingManagerError( `Task ${this.id} fn at index ${fnIndex} is not a function` ); } const promiseResult = fnResult(); // Вызываем функцию if (!(promiseResult instanceof Promise)) { throw new PollingManagerError( `Task ${this.id} fn ${fnIndex} did not return a Promise` ); } result = await this._withTimeout(promiseResult, this.taskTimeout); // Ждем результата fnSuccess = true; this.stats.successes++; this.stats.lastError = null; backoffDelay = this.backoffDelay; break; } catch (err: unknown) { const error = err instanceof Error ? err : new PollingManagerError(String(err)); // Обработка конкретных ошибок Modbus if (error instanceof ModbusTimeoutError) { this.logger.error('Modbus timeout error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusCRCError) { this.logger.error('Modbus CRC error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusParityError) { this.logger.error('Modbus parity error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusNoiseError) { this.logger.error('Modbus noise error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusFramingError) { this.logger.error('Modbus framing error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusOverrunError) { this.logger.error('Modbus overrun error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusCollisionError) { this.logger.error('Modbus collision error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusConfigError) { this.logger.error('Modbus config error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusBaudRateError) { this.logger.error('Modbus baud rate error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusSyncError) { this.logger.error('Modbus sync error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusFrameBoundaryError) { this.logger.error('Modbus frame boundary error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusLRCError) { this.logger.error('Modbus LRC error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusChecksumError) { this.logger.error('Modbus checksum error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusDataConversionError) { this.logger.error('Modbus data conversion error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusBufferOverflowError) { this.logger.error('Modbus buffer overflow error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusBufferUnderrunError) { this.logger.error('Modbus buffer underrun error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusMemoryError) { this.logger.error('Modbus memory error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusStackOverflowError) { this.logger.error('Modbus stack overflow error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusResponseError) { this.logger.error('Modbus response error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusInvalidAddressError) { this.logger.error('Modbus invalid address error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusInvalidFunctionCodeError) { this.logger.error('Modbus invalid function code error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusInvalidQuantityError) { this.logger.error('Modbus invalid quantity error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusIllegalDataAddressError) { this.logger.error('Modbus illegal data address error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusIllegalDataValueError) { this.logger.error('Modbus illegal data value error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusSlaveBusyError) { this.logger.error('Modbus slave busy error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusAcknowledgeError) { this.logger.error('Modbus acknowledge error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusSlaveDeviceFailureError) { this.logger.error('Modbus slave device failure error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusMalformedFrameError) { this.logger.error('Modbus malformed frame error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusInvalidFrameLengthError) { this.logger.error('Modbus invalid frame length error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusInvalidTransactionIdError) { this.logger.error('Modbus invalid transaction ID error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusUnexpectedFunctionCodeError) { this.logger.error('Modbus unexpected function code error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusConnectionRefusedError) { this.logger.error('Modbus connection refused error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusConnectionTimeoutError) { this.logger.error('Modbus connection timeout error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusNotConnectedError) { this.logger.error('Modbus not connected error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusAlreadyConnectedError) { this.logger.error('Modbus already connected error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusInsufficientDataError) { this.logger.error('Modbus insufficient data error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusGatewayPathUnavailableError) { this.logger.error('Modbus gateway path unavailable error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusGatewayTargetDeviceError) { this.logger.error('Modbus gateway target device error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusInvalidStartingAddressError) { this.logger.error('Modbus invalid starting address error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusMemoryParityError) { this.logger.error('Modbus memory parity error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusBroadcastError) { this.logger.error('Modbus broadcast error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusGatewayBusyError) { this.logger.error('Modbus gateway busy error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusDataOverrunError) { this.logger.error('Modbus data overrun error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusTooManyEmptyReadsError) { this.logger.error('Modbus too many empty reads error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusInterFrameTimeoutError) { this.logger.error('Modbus inter-frame timeout error', { id: this.id, error: error.message, } as LogContext); } else if (error instanceof ModbusSilentIntervalError) { this.logger.error('Modbus silent interval error', { id: this.id, error: error.message, } as LogContext); } retryCount++; this.stats.totalErrors++; this.stats.retries++; this.stats.lastError = error; this.onRetry?.(error, fnIndex, retryCount); const isFlushedError = error instanceof ModbusFlushError; if (isFlushedError) { this.logger.debug('Flush error detected, resetting backoff', { id: this.id, } as LogContext); backoffDelay = this.backoffDelay; } const delay = isFlushedError ? Math.min(50, backoffDelay) : backoffDelay * Math.pow(2, retryCount - 1); if (retryCount > this.maxRetries) { this.stats.failures++; this.onFailure?.(error); this.onError?.(error, fnIndex, retryCount); this.logger.warn('Max retries exhausted for fn[' + fnIndex + ']', { id: this.id, fnIndex, retryCount, error: error.message, } as LogContext); } else { this.logger.debug('Retrying fn[' + fnIndex + '] with delay', { id: this.id, delay, retryCount, } as LogContext); await this._sleep(delay); } } } results.push(result); success = success || fnSuccess; } this.stats.lastResult = results; this.stats.lastRunTime = Date.now(); if (results.length > 0 && results.some(r => r !== null && r !== undefined)) { this.onData?.(results); // Используем только разрешенные поля в LogContext this.logger.debug('Data callback executed', { id: this.id, resultsCount: results.length, } as LogContext); } else { this.logger.warn('Skipping onData - all results invalid', { id: this.id, results: 'invalid', } as LogContext); } this.onFinish?.(success, results); // Используем только разрешенные поля в LogContext this.logger.info('Task execution completed', { id: this.id, success, resultsCount: results.length, } as LogContext); this.pollingManager.loggerInstance.flush(); } finally { release(); this.executionInProgress = false; } await this._sleep(this.interval); } this.loopRunning = false; this.logger.debug('Run loop finished', { id: this.id } as LogContext); } /** * Sleeps for given amount of milliseconds. * @param {number} ms * @returns {Promise} * @private */ _sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Wraps a promise with a timeout. * @param {Promise} promise * @param {number} timeout * @returns {Promise} * @private */ _withTimeout<T>(promise: Promise<T>, timeout: number): Promise<T> { return new Promise<T>((resolve, reject) => { const timer = setTimeout(() => reject(new ModbusTimeoutError('Task timed out')), timeout); promise .then(result => { clearTimeout(timer); resolve(result); }) .catch(err => { clearTimeout(timer); reject(err); }); }); } } /** * TaskQueue управляет очередью задач для конкретного ресурса. * Гарантирует отсутствие дубликатов и последовательное выполнение. */ class TaskQueue { public resourceId: string; private pollingManager: PollingManager; private queuedOrProcessingTasks: Set<string>; public taskQueue: string[]; private mutex: Mutex; private processing: boolean; public logger: LoggerInstance; constructor( resourceId: string, pollingManager: PollingManager, taskSet: Set<string>, loggerInstance: Logger ) { this.resourceId = resourceId; this.pollingManager = pollingManager; this.queuedOrProcessingTasks = taskSet; this.taskQueue = []; this.mutex = new Mutex(); this.processing = false; this.logger = loggerInstance.createLogger('TaskQueue'); this.logger.setLevel('error'); } /** * Добавляет задачу в очередь на обработку. * @param {TaskController} taskController */ enqueue(taskController: TaskController): void { if (!taskController.stopped && taskController.resourceId === this.resourceId) { const taskKey = `${this.resourceId}:${taskController.id}`; if (!this.queuedOrProcessingTasks.has(taskKey)) { this.queuedOrProcessingTasks.add(taskKey); this.taskQueue.push(taskController.id); this.logger.debug('Task enqueued', { taskId: taskController.id } as LogContext); this._processNext(); } else { this.logger.debug('Task already queued', { taskId: taskController.id } as LogContext); } } } /** * Удаляет задачу из очереди. * @param {string} taskId */ removeTask(taskId: string): void { const taskKey = `${this.resourceId}:${taskId}`; this.queuedOrProcessingTasks.delete(taskKey); this.taskQueue = this.taskQueue.filter(id => id !== taskId); this.logger.debug('Task removed from queue', { taskId } as LogContext); } /** * Проверяет, пуста ли очередь. * @returns {boolean} */ isEmpty(): boolean { return this.taskQueue.length === 0; } /** * Очищает очередь. */ clear(): void { for (const taskId of this.taskQueue) { const taskKey = `${this.resourceId}:${taskId}`; this.queuedOrProcessingTasks.delete(taskKey); } this.taskQueue = []; this.logger.debug('Queue cleared', { resourceId: this.resourceId } as LogContext); } /** * Сообщает очереди, что задача готова к следующему запуску. * @param {TaskController} taskController */ markTaskReady(taskController: TaskController): void { if (!taskController.stopped && taskController.resourceId === this.resourceId) { const taskKey = `${this.resourceId}:${taskController.id}`; if (!this.queuedOrProcessingTasks.has(taskKey)) { this.queuedOrProcessingTasks.add(taskKey); this.taskQueue.push(taskController.id); this.logger.debug('Task marked as ready', { taskId: taskController.id } as LogContext); this._processNext(); } } } /** * Обрабатывает очередь задач. * @private */ async _processNext(): Promise<void> { if (this.processing || this.taskQueue.length === 0) { if (this.taskQueue.length === 0) { this.logger.debug('Queue is empty', { resourceId: this.resourceId } as LogContext); } return; } this.processing = true; this.logger.debug('Acquiring mutex for task processing', { resourceId: this.resourceId, } as LogContext); const release = await this.mutex.acquire(); let taskKey: string | null = null; try { const taskId = this.taskQueue.shift(); if (!taskId) return; taskKey = `${this.resourceId}:${taskId}`; this.logger.debug('Processing task', { taskId } as LogContext); const taskController = this.pollingManager.tasks.get(taskId); if (!taskController || taskController.stopped) { this.logger.debug('Task is stopped or does not exist', { taskId } as LogContext); this.queuedOrProcessingTasks.delete(taskKey); if (this.taskQueue.length > 0) { setTimeout(() => { this.processing = false; this._processNext(); }, 0); } return; } await this.pollingManager._executeQueuedTask(taskController); this.logger.debug('Task executed successfully', { taskId } as LogContext); } catch (error: unknown) { const err = error instanceof Error ? error : new PollingManagerError(String(error)); // Обработка конкретных ошибок Modbus if (err instanceof ModbusTimeoutError) { this.logger.error('Modbus timeout error in queue processing', { resourceId: this.resourceId, error: err.message, } as LogContext); } else if (err instanceof ModbusCRCError) { this.logger.error('Modbus CRC error in queue processing', { resourceId: this.resourceId, error: err.message, } as LogContext); } else if (err instanceof ModbusParityError) { this.logger.error('Modbus parity error in queue processing', { resourceId: this.resourceId, error: err.message, } as LogContext); } else if (err instanceof ModbusNoiseError) { this.logger.error('Modbus noise error in queue processing', { resourceId: this.resourceId, error: err.message, } as LogContext); } else if (err instanceof ModbusFramingError) { this.logger.error('Modbus framing error in queue processing', { resourceId: this.resourceId, error: err.message, } as LogContext); } else if (err instanceof ModbusOverrunError) { this.logger.error('Modbus overrun error in queue processing', { resourceId: this.resourceId, error: err.message, } as LogContext); } else if (err instanceof ModbusCollisionError) { this.logger.error('Modbus collision error in queue processing', { resourceId: this.resourceId, error: err.message, } as LogContext); } else if (err instanceof ModbusConfigError) { this.logger.error('Modbus config error in queue processing', { resourceId: this.resourceId, error: err.message, } as LogContext); } else if (err instanceof ModbusBaudRateError) { this.logger.error('Modbus baud rate error in queue processing', { resourceId: this.resourceId, error: err.message, } as LogContext); } else if (err instanceof ModbusSyncError) { this.logger.error('Modbus sync error in queue processing', { resourceId: this.resourceId, error: err.message, } as LogContext); } else if (err instanceof ModbusFrameBoundaryError) { this.logger.error('Modbus frame boundary error in queue processing', { resourceId: this.resourceId, error: err.message, } as LogContext); } else if (err instanceof ModbusLRCError) { this.logger.error('Modbus LRC error in queue processing', { resourceId: this.resourceId, error: err.message, } as LogContext); } else if (err instanceof ModbusChecksumError) { this.logger.error('Modbus checksum error in queue proces