UNPKG

modbus-connect

Version:

Modbus RTU over Web Serial and Node.js SerialPort

208 lines (188 loc) 6.51 kB
// src/transport/trackers/PortConnectionTracker.ts import { Mutex } from 'async-mutex'; import { ConnectionErrorType, PortStateHandler } from '../../types/modbus-types.js'; /** * Состояние подключения физического порта */ export interface PortConnectionState { /** Порт открыт и готов к работе */ isConnected: boolean; /** Тип последней ошибки (если отключён) */ errorType?: ConnectionErrorType; /** Подробное сообщение об ошибке */ errorMessage?: string; /** Список slaveId, подключённых к порту */ slaveIds: number[]; /** Время последнего изменения состояния (ms) */ timestamp: number; } /** * Опции для PortConnectionTracker */ export interface PortConnectionTrackerOptions { /** Интервал дебонса уведомлений об отключении (мс), по умолчанию: 300 */ debounceMs?: number; } /** * Отслеживает состояние подключения физического порта (Serial, TCP и т.д.). * Поддерживает: * - Дебонс уведомлений об отключении * - Потокобезопасность * - Иммутабельные возвращаемые данные * - Автоматическая отмена таймеров * - Передачу списка slaveIds */ export class PortConnectionTracker { private _handler?: PortStateHandler; private _state: PortConnectionState; private readonly _debounceMs: number; private readonly _mutex = new Mutex(); private _debounceTimeout: NodeJS.Timeout | null = null; constructor(options: PortConnectionTrackerOptions = {}) { this._debounceMs = options.debounceMs ?? 300; this._state = { isConnected: false, slaveIds: [], timestamp: Date.now(), }; } /** * Устанавливает обработчик изменения состояния порта. * При установке — вызывает обработчик с текущим состоянием. */ public async setHandler(handler: PortStateHandler): Promise<void> { const release = await this._mutex.acquire(); try { this._handler = handler; handler( this._state.isConnected, this._state.slaveIds, this._state.isConnected ? undefined : { type: this._state.errorType!, message: this._state.errorMessage! } ); } finally { release(); } } /** * Уведомляет о подключении порта. * Игнорируется, если порт уже подключён. * @param slaveIds — список slaveId, подключённых к порту */ public async notifyConnected(slaveIds: number[] = []): Promise<void> { const release = await this._mutex.acquire(); try { if (this._debounceTimeout) { clearTimeout(this._debounceTimeout); this._debounceTimeout = null; } if (this._state.isConnected && arraysEqual(this._state.slaveIds, slaveIds)) { return; } this._state = { isConnected: true, slaveIds: [...slaveIds], timestamp: Date.now(), }; this._handler?.(true, this._state.slaveIds); } finally { release(); } } /** * Уведомляет об отключении порта с trailing debounce. * Последний вызов в серии будет выполнен через `debounceMs`. * @param errorType Тип ошибки, по умолчанию: `ConnectionErrorType.UnknownError` * @param errorMessage Подробное сообщение, по умолчанию: `'Port disconnected'` * @param slaveIds Список slaveId, которые были активны */ public notifyDisconnected( errorType: ConnectionErrorType = ConnectionErrorType.UnknownError, errorMessage: string = 'Port disconnected', slaveIds: number[] = [] ): void { if (this._debounceTimeout) { clearTimeout(this._debounceTimeout); } (async () => { const release = await this._mutex.acquire(); try { if (!this._state.isConnected) { return; } this._state = { isConnected: false, errorType, errorMessage, slaveIds: [...slaveIds], timestamp: Date.now(), }; this._debounceTimeout = setTimeout(() => { this._debounceTimeout = null; this._handler?.(false, this._state.slaveIds, { type: errorType, message: errorMessage }); }, this._debounceMs); } finally { release(); } })(); } /** * Возвращает копию текущего состояния порта. */ public async getState(): Promise<PortConnectionState> { const release = await this._mutex.acquire(); try { return { ...this._state, slaveIds: [...this._state.slaveIds] }; } finally { release(); } } /** * Очищает таймер дебонса и сбрасывает состояние. * Вызывается при `destroy()` или полном сбросе. */ public async clear(): Promise<void> { const release = await this._mutex.acquire(); try { if (this._debounceTimeout) { clearTimeout(this._debounceTimeout); this._debounceTimeout = null; } this._state = { isConnected: false, slaveIds: [], timestamp: Date.now(), }; } finally { release(); } } /** * Проверяет, подключён ли порт. */ public async isConnected(): Promise<boolean> { const release = await this._mutex.acquire(); try { return this._state.isConnected; } finally { release(); } } /** * Сбрасывает таймер дебонса (только для тестов). * @internal */ public __resetDebounce(): void { if (this._debounceTimeout) { clearTimeout(this._debounceTimeout); this._debounceTimeout = null; } } } function arraysEqual(a: number[], b: number[]): boolean { if (a.length !== b.length) return false; const sortedA = [...a].sort((x, y) => x - y); const sortedB = [...b].sort((x, y) => x - y); return sortedA.every((val, idx) => val === sortedB[idx]); }