UNPKG

modbus-connect

Version:

Modbus RTU over Web Serial and Node.js SerialPort

876 lines (875 loc) 34.1 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var import_logger = __toESM(require("../logger.js")); var import_polling_manager = __toESM(require("../polling-manager.js")); var import_modbus_types = require("../types/modbus-types.js"); var import_DeviceConnectionTracker = require("./trackers/DeviceConnectionTracker.js"); var import_PortConnectionTracker = require("./trackers/PortConnectionTracker.js"); var import_errors = require("../errors.js"); var import_utils = require("../utils/utils.js"); class TransportController { transports = /* @__PURE__ */ new Map(); slaveTransportMap = /* @__PURE__ */ new Map(); loadBalancerStrategy = "first-available"; loggerInstance = new import_logger.default(); logger = this.loggerInstance.createLogger("TransportController"); _roundRobinIndex = 0; _stickyMap = /* @__PURE__ */ new Map(); transportToDeviceTrackerMap = /* @__PURE__ */ new Map(); transportToPortTrackerMap = /* @__PURE__ */ new Map(); transportToDeviceHandlerMap = /* @__PURE__ */ new Map(); transportToPortHandlerMap = /* @__PURE__ */ new Map(); _externalDeviceStateHandler = null; _externalPortStateHandler = null; constructor() { this.logger.setLevel("info"); } /** * Устанавливает обработчик состояния устройства для внешнего мира. * @param handler - Обработчик состояния */ setDeviceStateHandler(handler) { this._externalDeviceStateHandler = handler; } /** * Устанавливает обработчик состояния порта для внешнего мира. * @param handler - Обработчик состояния */ setPortStateHandler(handler) { this._externalPortStateHandler = handler; } /** * Добавляет транспорт и инициализирует его PollingManager. * @param id - ID транспорта * @param type - Тип транспорта ('node' или 'web') * @param options - Опции транспорта * @param reconnectOptions - Параметры переподключения * @param pollingConfig - Конфигурация для PollingManager */ async addTransport(id, type, options, reconnectOptions, pollingConfig) { if (this.transports.has(id)) { throw new Error(`Transport with id "${id}" already exists`); } const rsMode = options.RSMode ?? "RS485"; const slaveIds = options.slaveIds || []; if (rsMode === "RS232" && slaveIds.length > 1) { throw new import_errors.RSModeConstraintError( `Transport "${id}" with RSMode 'RS232' cannot be assigned more than one device. Provided ${slaveIds.length} devices.` ); } let transport; try { switch (type) { case "node": { const path = options.port || options.path; if (!path) { throw new Error('Missing "port" (or "path") option for node transport'); } const NodeSerialTransport = (await import("./node-transports/node-serialport.js")).default; const nodeOptions = {}; const allowedNodeKeys = [ "baudRate", "dataBits", "stopBits", "parity", "readTimeout", "writeTimeout", "maxBufferSize", "reconnectInterval", "maxReconnectAttempts", "RSMode" ]; for (const key of allowedNodeKeys) { if (key in options) { nodeOptions[key] = options[key]; } } transport = new NodeSerialTransport(path, nodeOptions); break; } case "web": { const webOptionsIn = options; const port = webOptionsIn.port; if (!port) { throw new Error('Missing "port" option for web transport'); } const WebSerialTransport = (await import("./web-transports/web-serialport.js")).default; const portFactory = async () => { this.logger.debug("WebSerialTransport portFactory: Returning provided port instance"); try { if (port.readable || port.writable) { this.logger.debug( "WebSerialTransport portFactory: Port seems to be in use, trying to close..." ); try { await port.close(); this.logger.debug("WebSerialTransport portFactory: Existing port closed"); } catch (closeErr) { this.logger.warn( "WebSerialTransport portFactory: Error closing existing port (might be already closed or broken):", closeErr.message ); } } } catch (err) { this.logger.error( "WebSerialTransport portFactory: Failed to prepare existing port for reuse:", err ); } return port; }; const webOptions = {}; const allowedWebKeys = [ "baudRate", "dataBits", "stopBits", "parity", "readTimeout", "writeTimeout", "reconnectInterval", "maxReconnectAttempts", "maxEmptyReadsBeforeReconnect", "RSMode" ]; for (const key of allowedWebKeys) { if (key in webOptionsIn) { webOptions[key] = webOptionsIn[key]; } } this.logger.debug("Creating WebSerialTransport with provided port"); transport = new WebSerialTransport(portFactory, webOptions); break; } default: throw new Error(`Unknown transport type: ${type}`); } } catch (err) { this.logger.error(`Failed to create transport of type "${type}": ${err.message}`); throw err; } const seenSlaveIds = /* @__PURE__ */ new Set(); for (const slaveId of slaveIds) { if (seenSlaveIds.has(slaveId)) { throw new Error( `Duplicate slave ID ${slaveId} provided for transport "${id}". Each slave ID must be unique per transport.` ); } seenSlaveIds.add(slaveId); } const fallbacks = options.fallbacks || []; const pollingManager = new import_polling_manager.default(pollingConfig, this.loggerInstance); pollingManager.logger = this.loggerInstance.createLogger(`PM:${id}`); pollingManager.setLogLevelForAll("error"); const info = { id, type, transport, pollingManager, status: "disconnected", slaveIds, rsMode: transport.getRSMode(), fallbacks, createdAt: /* @__PURE__ */ new Date(), reconnectAttempts: 0, maxReconnectAttempts: reconnectOptions?.maxReconnectAttempts ?? 5, reconnectInterval: reconnectOptions?.reconnectInterval ?? 2e3 }; this.transports.set(id, info); const deviceTracker = new import_DeviceConnectionTracker.DeviceConnectionTracker(); const portTracker = new import_PortConnectionTracker.PortConnectionTracker(); this.transportToDeviceTrackerMap.set(id, deviceTracker); this.transportToPortTrackerMap.set(id, portTracker); transport.setDeviceStateHandler((slaveId, connected, error) => { this._onDeviceStateChange(id, slaveId, connected, error); }); transport.setPortStateHandler((connected, slaveIds2, error) => { this._onPortStateChange(id, connected, slaveIds2, error); }); this._updateSlaveTransportMap(id, slaveIds); this.logger.info(`Transport "${id}" added with PollingManager`, { type, slaveIds }); } /** * Удаляет транспорт по указанному ID. * @param id - ID транспорта */ async removeTransport(id) { const info = this.transports.get(id); if (!info) return; info.pollingManager.clearAll(); await this.disconnectTransport(id); this.transports.delete(id); this.transportToDeviceTrackerMap.delete(id); this.transportToPortTrackerMap.delete(id); this.transportToDeviceHandlerMap.delete(id); this.transportToPortHandlerMap.delete(id); for (const [slaveId, list] of this.slaveTransportMap.entries()) { const updated = list.filter((tid) => tid !== id); if (updated.length === 0) { this.slaveTransportMap.delete(slaveId); } else { this.slaveTransportMap.set(slaveId, updated); } } this.logger.info(`Transport "${id}" removed`); } // ========================================================= // Методы управления PollingManager (Прокси) // ========================================================= _getTransportInfo(transportId) { const info = this.transports.get(transportId); if (!info) throw new Error(`Transport "${transportId}" not found`); return info; } /** * Добавляет задачу опроса в указанный транспорт. * @param transportId - ID транспорта * @param options - Опции задачи */ addPollingTask(transportId, options) { const info = this._getTransportInfo(transportId); info.pollingManager.addTask(options); } /** * Удаляет задачу опроса из указанного транспорта. * @param transportId - ID транспорта * @param taskId - ID задачи */ removePollingTask(transportId, taskId) { const info = this._getTransportInfo(transportId); info.pollingManager.removeTask(taskId); } /** * Обновляет задачу опроса. * @param transportId - ID транспорта * @param taskId - ID задачи * @param newOptions - Новые опции */ updatePollingTask(transportId, taskId, newOptions) { const info = this._getTransportInfo(transportId); info.pollingManager.updateTask(taskId, newOptions); } /** * Управление состоянием конкретной задачи. * @param transportId - ID транспорта * @param taskId - ID задачи * @param action - Действие (start, stop, pause, resume) */ controlTask(transportId, taskId, action) { const info = this._getTransportInfo(transportId); switch (action) { case "start": info.pollingManager.startTask(taskId); break; case "stop": info.pollingManager.stopTask(taskId); break; case "pause": info.pollingManager.pauseTask(taskId); break; case "resume": info.pollingManager.resumeTask(taskId); break; } } /** * Управление всем опросом на транспорте. * @param transportId - ID транспорта * @param action - Действие (startAll, stopAll, pauseAll, resumeAll) */ controlPolling(transportId, action) { const info = this._getTransportInfo(transportId); switch (action) { case "startAll": info.pollingManager.startAllTasks(); break; case "stopAll": info.pollingManager.stopAllTasks(); break; case "pauseAll": info.pollingManager.pauseAllTasks(); break; case "resumeAll": info.pollingManager.resumeAllTasks(); break; } } /** * Получает статистику задач для транспорта. * @param transportId - ID транспорта * @returns Статистика задач */ getPollingStats(transportId) { const info = this._getTransportInfo(transportId); return info.pollingManager.getAllTaskStats(); } /** * Получает информацию об очереди. * @param transportId - ID транспорта * @returns Информация об очереди */ getPollingQueueInfo(transportId) { const info = this._getTransportInfo(transportId); return info.pollingManager.getQueueInfo(); } /** * Позволяет выполнить функцию (например, запись) с использованием мьютекса PollingManager'а транспорта. * Это предотвращает конфликты между опросом и ручными командами. * @param transportId - ID транспорта * @param fn - Функция для выполнения * @returns Результат выполнения функции */ async executeImmediate(transportId, fn) { const info = this._getTransportInfo(transportId); return info.pollingManager.executeImmediate(fn); } // ========================================================= /** * Получает транспорт по указанному ID. * @param id - ID транспорта * @returns Транспорт или null, если транспорт не найден */ getTransport(id) { const info = this.transports.get(id); return info ? info.transport : null; } /** * Получает список всех транспортов. * @returns Массив объектов TransportInfo */ listTransports() { return Array.from(this.transports.values()); } /** * Подключает все транспорты. */ async connectAll() { const promises = Array.from(this.transports.values()).map( (info) => this.connectTransport(info.id) ); await Promise.all(promises); } /** * Отключает все транспорты. */ async disconnectAll() { const promises = Array.from(this.transports.values()).map( (info) => this.disconnectTransport(info.id) ); await Promise.all(promises); } /** * Подключает транспорт по указанному ID. * @param id - ID транспорта */ async connectTransport(id) { const info = this.transports.get(id); if (!info) throw new Error(`Transport with id "${id}" not found`); if (info.status === "connecting" || info.status === "connected") return; info.status = "connecting"; try { await info.transport.connect(); info.status = "connected"; info.reconnectAttempts = 0; info.pollingManager.resumeAllTasks(); this.logger.info(`Transport "${id}" connected`); } catch (err) { info.status = "error"; info.lastError = err instanceof Error ? err : new Error(String(err)); this.logger.error(`Failed to connect transport "${id}":`, info.lastError.message); if (info.reconnectAttempts < info.maxReconnectAttempts) { info.reconnectAttempts++; setTimeout( () => this.connectTransport(id), info.reconnectInterval * info.reconnectAttempts ); } else { this.logger.error(`Max reconnection attempts reached for "${id}"`); } throw err; } } /** * Отключает транспорт по указанному ID. * @param id - ID транспорта */ async disconnectTransport(id) { const info = this.transports.get(id); if (!info) return; try { info.pollingManager.pauseAllTasks(); await info.transport.disconnect(); info.status = "disconnected"; this.logger.info(`Transport "${id}" disconnected`); } catch (err) { this.logger.error(`Error disconnecting transport "${id}":`, err.message); } } /** * Назначить slaveId транспорту. Если транспорт уже обслуживает этот slaveId — игнорирует. * @param transportId - ID транспорта * @param slaveId - ID устройства */ assignSlaveIdToTransport(transportId, slaveId) { const info = this.transports.get(transportId); if (!info) { throw new Error(`Transport with id "${transportId}" not found`); } if (info.rsMode === "RS232" && info.slaveIds.length >= 1) { const existingSlaveId = info.slaveIds[0]; throw new import_errors.RSModeConstraintError( `Cannot assign slaveId ${slaveId} to transport "${transportId}". It is in 'RS232' mode and already manages device ${existingSlaveId}.` ); } if (info.slaveIds.includes(slaveId)) { throw new Error( `Cannot assign slave ID ${slaveId}". The transport is already managing this ID.` ); } info.slaveIds.push(slaveId); this._updateSlaveTransportMap(transportId, [slaveId]); this.logger.info(`Assigned slaveId ${slaveId} to transport "${transportId}"`); } /** * Удаляет slaveId из транспорта. * Позволяет отвязать устройство, чтобы потом подключить его заново. * @param transportId - ID транспорта * @param slaveId - ID устройства */ removeSlaveIdFromTransport(transportId, slaveId) { const info = this.transports.get(transportId); if (!info) { this.logger.warn( `Attempted to remove slaveId ${slaveId} from non-existent transport "${transportId}"` ); return; } const index = info.slaveIds.indexOf(slaveId); if (index !== -1) { info.slaveIds.splice(index, 1); } else { this.logger.warn(`SlaveId ${slaveId} was not found in transport "${transportId}"`); return; } const transportList = this.slaveTransportMap.get(slaveId); if (transportList) { const updatedList = transportList.filter((tid) => tid !== transportId); if (updatedList.length === 0) { this.slaveTransportMap.delete(slaveId); } else { this.slaveTransportMap.set(slaveId, updatedList); } } const stickyTransport = this._stickyMap.get(slaveId); if (stickyTransport === transportId) { this._stickyMap.delete(slaveId); } const tracker = this.transportToDeviceTrackerMap.get(transportId); if (tracker) { try { tracker.removeState(slaveId); } catch (err) { this.logger.warn( `Failed to remove state for slaveId ${slaveId} from tracker: ${err.message}` ); } } const transportAny = info.transport; if (typeof transportAny.removeConnectedDevice === "function") { transportAny.removeConnectedDevice(slaveId); } this.logger.info(`Removed slaveId ${slaveId} from transport "${transportId}"`); } /** * Перезагружает транспорт с новыми опциями. * @param id - ID транспорта * @param options - Новые опции */ async reloadTransport(id, options) { const info = this.transports.get(id); if (!info) throw new Error(`Transport with id "${id}" not found`); const wasConnected = info.status === "connected"; info.pollingManager.clearAll(); await this.disconnectTransport(id); let newTransport; try { switch (info.type) { case "node": { const nodeOptionsIn = options; const path = nodeOptionsIn.port || nodeOptionsIn.path; if (!path) { throw new Error('Missing "port" (or "path") option for node transport'); } const NodeSerialTransport = (await import("./node-transports/node-serialport.js")).default; newTransport = new NodeSerialTransport(path, nodeOptionsIn); break; } case "web": { const webOptionsIn = options; const port = webOptionsIn.port; if (!port) { throw new Error('Missing "port" option for web transport'); } const WebSerialTransport = (await import("./web-transports/web-serialport.js")).default; const portFactory = async () => { return port; }; newTransport = new WebSerialTransport(portFactory, webOptionsIn); break; } default: throw new Error(`Unknown transport type: ${info.type}`); } } catch (err) { this.logger.error(`Failed to create new transport of type "${info.type}": ${err.message}`); throw err; } const deviceTracker = new import_DeviceConnectionTracker.DeviceConnectionTracker(); const portTracker = new import_PortConnectionTracker.PortConnectionTracker(); this.transportToDeviceTrackerMap.set(id, deviceTracker); this.transportToPortTrackerMap.set(id, portTracker); newTransport.setDeviceStateHandler((slaveId, connected, error) => { this._onDeviceStateChange(id, slaveId, connected, error); }); newTransport.setPortStateHandler((connected, slaveIds, error) => { this._onPortStateChange(id, connected, slaveIds, error); }); const deviceHandler = this.transportToDeviceHandlerMap.get(id); if (deviceHandler) { await deviceTracker.setHandler(deviceHandler); } const portHandler = this.transportToPortHandlerMap.get(id); if (portHandler) { await portTracker.setHandler(portHandler); } info.transport = newTransport; info.rsMode = newTransport.getRSMode(); if (wasConnected) { await this.connectTransport(id); } this.logger.info(`Transport "${id}" reloaded with new options`); } /** * Выполняет операцию записи (или любую другую команду, требующую эксклюзивного доступа) * на указанном транспорте, используя мьютекс PollingManager. * @param transportId - ID транспорта, на котором нужно выполнить запись. * @param data - Данные для записи (Uint8Array). * @param readLength - Ожидаемая длина ответа (если есть). * @param timeout - Таймаут на чтение ответа (в мс). * @returns Прочитанный ответ (Uint8Array) или пустой буфер, если readLength=0. */ async writeToPort(transportId, data, readLength = 0, timeout = 3e3) { const info = this._getTransportInfo(transportId); if (!info.transport.isOpen) { throw new Error( `Transport "${transportId}" is not open (connection status: ${info.status}).` ); } return info.pollingManager.executeImmediate(async () => { await info.transport.write(data); if (readLength > 0) { return info.transport.read(readLength, timeout); } await info.transport.flush(); return (0, import_utils.allocUint8Array)(0); }); } /** * Установить сопоставление slaveId -> [transportId, fallback1, ...] * Вызывается автоматически при addTransport. */ _updateSlaveTransportMap(id, slaveIds) { for (const slaveId of slaveIds) { const list = this.slaveTransportMap.get(slaveId) || []; if (!list.includes(id)) { list.push(id); this.slaveTransportMap.set(slaveId, list); } } } /** * Получить транспорт для конкретного slaveId. * Использует стратегию балансировки или fallback. */ getTransportForSlave(slaveId, requiredRSMode) { const transportIds = this.slaveTransportMap.get(slaveId); if (transportIds && transportIds.length > 0) { let transport = null; switch (this.loadBalancerStrategy) { case "round-robin": transport = this._getTransportRoundRobin(transportIds); break; case "sticky": transport = this._getTransportSticky(slaveId, transportIds); break; case "first-available": default: transport = this._getTransportFirstAvailable(transportIds); break; } if (transport) { const info = Array.from(this.transports.values()).find((i) => i.transport === transport); if (info && info.rsMode === requiredRSMode) { return transport; } } } for (const info of this.transports.values()) { if (info.status === "connected" && info.rsMode === requiredRSMode) { if (requiredRSMode === "RS485" || requiredRSMode === "RS232" && info.slaveIds.length === 0) { return info.transport; } } } this.logger.warn( `No connected transport found for slave ${slaveId} with required RSMode ${requiredRSMode}` ); return null; } /** * Получить транспорт по стратегии round-robin */ _getTransportRoundRobin(transportIds) { const connectedTransports = transportIds.map((id) => this.transports.get(id)).filter((info) => !!info && info.status === "connected"); if (connectedTransports.length === 0) { return this._getTransportFirstAvailable(transportIds); } this._roundRobinIndex = (this._roundRobinIndex + 1) % connectedTransports.length; const selectedInfo = connectedTransports[this._roundRobinIndex]; return selectedInfo?.transport ?? null; } /** * Получить транспорт по стратегии sticky */ _getTransportSticky(slaveId, transportIds) { const lastUsedId = this._stickyMap.get(slaveId); if (lastUsedId) { const info = this.transports.get(lastUsedId); if (info && info.status === "connected" && transportIds.includes(lastUsedId)) { return info.transport; } } const transport = this._getTransportFirstAvailable(transportIds); if (transport) { const transportEntry = Array.from(this.transports.entries()).find( ([_id, info]) => info.transport === transport ); if (transportEntry) { const newTransportId = transportEntry[0]; this._stickyMap.set(slaveId, newTransportId); } } return transport; } /** * Получить транспорт по стратегии first-available */ _getTransportFirstAvailable(transportIds) { for (const id of transportIds) { const info = this.transports.get(id); if (info && info.status === "connected") { return info.transport; } } for (const id of transportIds) { const info = this.transports.get(id); if (info && info.fallbacks) { for (const fallbackId of info.fallbacks) { const fallbackInfo = this.transports.get(fallbackId); if (fallbackInfo && fallbackInfo.status === "connected") { return fallbackInfo.transport; } } } } return null; } /** * Получает статус транспорта по указанному ID. * @param id - ID транспорта * @returns Статус транспорта или пустой объект, если транспорт не найден */ getStatus(id) { if (id) { const info = this.transports.get(id); if (!info) return {}; const queueInfo = info.pollingManager.getQueueInfo(); return { id: info.id, connected: info.status === "connected", lastError: info.lastError, connectedSlaveIds: info.slaveIds, uptime: Date.now() - info.createdAt.getTime(), reconnectAttempts: info.reconnectAttempts, pollingStats: { queueLength: queueInfo.queueLength, tasksRunning: info.pollingManager.getSystemStats().tasks ? Object.keys(info.pollingManager.getSystemStats().tasks).length : 0 } }; } const result = {}; for (const [tid, info] of this.transports) { const queueInfo = info.pollingManager.getQueueInfo(); result[tid] = { id: info.id, connected: info.status === "connected", lastError: info.lastError, connectedSlaveIds: info.slaveIds, uptime: Date.now() - info.createdAt.getTime(), reconnectAttempts: info.reconnectAttempts, pollingStats: { queueLength: queueInfo.queueLength, tasksRunning: info.pollingManager.getSystemStats().tasks ? Object.keys(info.pollingManager.getSystemStats().tasks).length : 0 } }; } return result; } /** * Получает количество активных транспортов. * @returns Количество активных транспортов */ getActiveTransportCount() { let count = 0; for (const info of this.transports.values()) { if (info.status === "connected") count++; } return count; } /** * Устанавливает стратегию балансировки. * @param strategy - Стратегия балансировки ('round-robin', 'sticky', 'first-available') */ setLoadBalancer(strategy) { this.loadBalancerStrategy = strategy; } /** * Устанавливает обработчик состояния устройства для транспорта. * @param transportId - ID транспорта * @param handler - Обработчик состояния */ async setDeviceStateHandlerForTransport(transportId, handler) { const tracker = this.transportToDeviceTrackerMap.get(transportId); if (!tracker) { throw new Error(`No device tracker found for transport "${transportId}"`); } await tracker.setHandler(handler); this.transportToDeviceHandlerMap.set(transportId, handler); } /** * Устанавливает обработчик состояния порта для транспорта. * @param transportId - ID транспорта * @param handler - Обработчик состояния */ async setPortStateHandlerForTransport(transportId, handler) { const tracker = this.transportToPortTrackerMap.get(transportId); if (!tracker) { throw new Error(`No port tracker found for transport "${transportId}"`); } await tracker.setHandler(handler); this.transportToPortHandlerMap.set(transportId, handler); } /** * Внутренний метод: вызывается транспортом при изменении состояния устройства. */ _onDeviceStateChange(transportId, slaveId, connected, error) { const tracker = this.transportToDeviceTrackerMap.get(transportId); if (!tracker) { this.logger.warn(`No device tracker found for transport "${transportId}"`); return; } if (connected) { tracker.notifyConnected(slaveId); } else { const errorType = error?.type || import_modbus_types.ConnectionErrorType.UnknownError; const errorMessage = error?.message || "Device disconnected"; tracker.notifyDisconnected(slaveId, errorType, errorMessage); } const handler = this.transportToDeviceHandlerMap.get(transportId); if (handler) { handler(slaveId, connected, error); } if (this._externalDeviceStateHandler) { this._externalDeviceStateHandler(slaveId, connected, error); } } /** * Внутренний метод: вызывается транспортом при изменении состояния порта. */ _onPortStateChange(transportId, connected, slaveIds, error) { const tracker = this.transportToPortTrackerMap.get(transportId); if (!tracker) { this.logger.warn(`No port tracker found for transport "${transportId}"`); return; } const info = this.transports.get(transportId); if (connected) { tracker.notifyConnected(); if (info) info.pollingManager.resumeAllTasks(); } else { const errorType = error?.type || import_modbus_types.ConnectionErrorType.UnknownError; const errorMessage = error?.message || "Port disconnected"; tracker.notifyDisconnected(errorType, errorMessage, slaveIds); if (info) info.pollingManager.pauseAllTasks(); } if (info) { info.status = connected ? "connected" : "disconnected"; if (!connected) { info.lastError = new Error(error?.message); } } const handler = this.transportToPortHandlerMap.get(transportId); if (handler) { handler(connected, slaveIds, error); } if (this._externalPortStateHandler) { this._externalPortStateHandler(connected, slaveIds, error); } } /** * Уничтожает контроллер транспорта. */ async destroy() { for (const info of this.transports.values()) { info.pollingManager.clearAll(); } await this.disconnectAll(); this.transports.clear(); this.slaveTransportMap.clear(); for (const tracker of this.transportToDeviceTrackerMap.values()) { await tracker.clear(); } for (const tracker of this.transportToPortTrackerMap.values()) { await tracker.clear(); } this.transportToDeviceTrackerMap.clear(); this.transportToPortTrackerMap.clear(); this.transportToDeviceHandlerMap.clear(); this.transportToPortHandlerMap.clear(); this._externalDeviceStateHandler = null; this._externalPortStateHandler = null; this.logger.info("TransportController destroyed"); } } module.exports = TransportController;