modbus-connect
Version:
Modbus RTU over Web Serial and Node.js SerialPort
1,288 lines • 89.3 kB
text/typescript
// 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