UNPKG

modbus-connect

Version:

Modbus RTU over Web Serial and Node.js SerialPort

793 lines (792 loc) 31.4 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_utils = require("../../utils/utils.js"); var import_async_mutex = require("async-mutex"); var import_errors = require("../../errors.js"); var import_modbus_types = require("../../types/modbus-types.js"); const WEB_SERIAL_CONSTANTS = { MIN_BAUD_RATE: 300, MAX_BAUD_RATE: 115200, MAX_READ_BUFFER_SIZE: 65536, POLL_INTERVAL_MS: 10 }; const loggerInstance = new import_logger.default(); loggerInstance.setLogFormat(["timestamp", "level", "logger"]); loggerInstance.setCustomFormatter("logger", (value) => value ? `[${value}]` : ""); const logger = loggerInstance.createLogger("WebSerialTransport"); logger.setLevel("info"); const ERROR_HANDLERS = { [import_errors.ModbusTimeoutError.name]: () => logger.error("Timeout error detected"), [import_errors.ModbusCRCError.name]: () => logger.error("CRC error detected"), [import_errors.ModbusParityError.name]: () => logger.error("Parity error detected"), [import_errors.ModbusNoiseError.name]: () => logger.error("Noise error detected"), [import_errors.ModbusFramingError.name]: () => logger.error("Framing error detected"), [import_errors.ModbusOverrunError.name]: () => logger.error("Overrun error detected"), [import_errors.ModbusCollisionError.name]: () => logger.error("Collision error detected"), [import_errors.ModbusConfigError.name]: () => logger.error("Configuration error detected"), [import_errors.ModbusBaudRateError.name]: () => logger.error("Baud rate error detected"), [import_errors.ModbusSyncError.name]: () => logger.error("Sync error detected"), [import_errors.ModbusFrameBoundaryError.name]: () => logger.error("Frame boundary error detected"), [import_errors.ModbusLRCError.name]: () => logger.error("LRC error detected"), [import_errors.ModbusChecksumError.name]: () => logger.error("Checksum error detected"), [import_errors.ModbusDataConversionError.name]: () => logger.error("Data conversion error detected"), [import_errors.ModbusBufferOverflowError.name]: () => logger.error("Buffer overflow error detected"), [import_errors.ModbusBufferUnderrunError.name]: () => logger.error("Buffer underrun error detected"), [import_errors.ModbusMemoryError.name]: () => logger.error("Memory error detected"), [import_errors.ModbusStackOverflowError.name]: () => logger.error("Stack overflow error detected"), [import_errors.ModbusResponseError.name]: () => logger.error("Response error detected"), [import_errors.ModbusInvalidAddressError.name]: () => logger.error("Invalid address error detected"), [import_errors.ModbusInvalidFunctionCodeError.name]: () => logger.error("Invalid function code error detected"), [import_errors.ModbusInvalidQuantityError.name]: () => logger.error("Invalid quantity error detected"), [import_errors.ModbusIllegalDataAddressError.name]: () => logger.error("Illegal data address error detected"), [import_errors.ModbusIllegalDataValueError.name]: () => logger.error("Illegal data value error detected"), [import_errors.ModbusSlaveBusyError.name]: () => logger.error("Slave busy error detected"), [import_errors.ModbusAcknowledgeError.name]: () => logger.error("Acknowledge error detected"), [import_errors.ModbusSlaveDeviceFailureError.name]: () => logger.error("Slave device failure error detected"), [import_errors.ModbusMalformedFrameError.name]: () => logger.error("Malformed frame error detected"), [import_errors.ModbusInvalidFrameLengthError.name]: () => logger.error("Invalid frame length error detected"), [import_errors.ModbusInvalidTransactionIdError.name]: () => logger.error("Invalid transaction ID error detected"), [import_errors.ModbusUnexpectedFunctionCodeError.name]: () => logger.error("Unexpected function code error detected"), [import_errors.ModbusConnectionRefusedError.name]: () => logger.error("Connection refused error detected"), [import_errors.ModbusConnectionTimeoutError.name]: () => logger.error("Connection timeout error detected"), [import_errors.ModbusNotConnectedError.name]: () => logger.error("Not connected error detected"), [import_errors.ModbusAlreadyConnectedError.name]: () => logger.error("Already connected error detected"), [import_errors.ModbusInsufficientDataError.name]: () => logger.error("Insufficient data error detected"), [import_errors.ModbusGatewayPathUnavailableError.name]: () => logger.error("Gateway path unavailable error detected"), [import_errors.ModbusGatewayTargetDeviceError.name]: () => logger.error("Gateway target device error detected"), [import_errors.ModbusInvalidStartingAddressError.name]: () => logger.error("Invalid starting address error detected"), [import_errors.ModbusMemoryParityError.name]: () => logger.error("Memory parity error detected"), [import_errors.ModbusBroadcastError.name]: () => logger.error("Broadcast error detected"), [import_errors.ModbusGatewayBusyError.name]: () => logger.error("Gateway busy error detected"), [import_errors.ModbusDataOverrunError.name]: () => logger.error("Data overrun error detected"), [import_errors.ModbusTooManyEmptyReadsError.name]: () => logger.error("Too many empty reads error detected"), [import_errors.ModbusFlushError.name]: () => logger.error("Flush error detected"), [import_errors.WebSerialTransportError.name]: () => logger.error("WebSerial transport error detected"), [import_errors.WebSerialConnectionError.name]: () => logger.error("WebSerial connection error detected"), [import_errors.WebSerialReadError.name]: () => logger.error("WebSerial read error detected"), [import_errors.WebSerialWriteError.name]: () => logger.error("WebSerial write error detected") }; const handleModbusError = (err) => { const handler = ERROR_HANDLERS[err.constructor.name]; if (handler) { handler(); } else { logger.error(`Unknown error: ${err.message}`); } }; class WebSerialTransport { isOpen = false; portFactory; port = null; options; reader = null; writer = null; readBuffer; _connectedSlaveIds = /* @__PURE__ */ new Set(); _isPortReady = false; _reconnectAttempts = 0; _shouldReconnect = true; _isConnecting = false; _isDisconnecting = false; _isFlushing = false; _pendingFlushPromises = []; _reconnectTimer = null; _emptyReadCount = 0; _readLoopActive = false; _readLoopAbortController = null; _operationMutex; _connectionPromise = null; _resolveConnection = null; _rejectConnection = null; _portClosePromise = null; _portCloseResolve = null; _deviceStateHandler = null; _portStateHandler = null; _wasEverConnected = false; constructor(portFactory, options = {}) { if (typeof portFactory !== "function") { throw new import_errors.WebSerialTransportError( "A port factory function must be provided to WebSerialTransport" ); } this.portFactory = portFactory; this.options = { baudRate: 9600, dataBits: 8, stopBits: 1, parity: "none", readTimeout: 1e3, writeTimeout: 1e3, reconnectInterval: 3e3, maxReconnectAttempts: Infinity, maxEmptyReadsBeforeReconnect: 10, RSMode: options.RSMode || "RS485", ...options }; this.readBuffer = new Uint8Array(0); this._operationMutex = new import_async_mutex.Mutex(); } getRSMode() { return this.options.RSMode; } setDeviceStateHandler(handler) { this._deviceStateHandler = handler; } setPortStateHandler(handler) { this._portStateHandler = handler; } async disableDeviceTracking() { this._deviceStateHandler = null; logger.debug("Device connection tracking disabled"); } async enableDeviceTracking(handler) { if (handler) { this._deviceStateHandler = handler; } logger.debug("Device connection tracking enabled"); } notifyDeviceConnected(slaveId) { if (this._connectedSlaveIds.has(slaveId)) { return; } this._connectedSlaveIds.add(slaveId); if (this._deviceStateHandler) { this._deviceStateHandler(slaveId, true); } } notifyDeviceDisconnected(slaveId, errorType, errorMessage) { if (!this._connectedSlaveIds.has(slaveId)) { return; } this._connectedSlaveIds.delete(slaveId); if (this._deviceStateHandler) { this._deviceStateHandler(slaveId, false, { type: errorType, message: errorMessage }); } } removeConnectedDevice(slaveId) { if (this._connectedSlaveIds.has(slaveId)) { this._connectedSlaveIds.delete(slaveId); logger.debug(`Manually removed device ${slaveId} from connected set`); } } isPortReady() { const ready = this.isOpen && this._isPortReady && this.writer !== null; logger.debug( `isPortReady check: isOpen=${this.isOpen}, _isPortReady=${this._isPortReady}, writer=${this.writer !== null}, result=${ready}` ); return ready; } async _notifyPortConnected() { this._wasEverConnected = true; if (this._portStateHandler) { this._portStateHandler(true, [], void 0); } } async _notifyPortDisconnected(errorType = import_modbus_types.ConnectionErrorType.UnknownError, errorMessage = "Port disconnected") { if (!this._wasEverConnected) { logger.debug("Skipping DISCONNECTED \u2014 port was never connected"); return; } if (this._portStateHandler) { this._portStateHandler(false, [], { type: errorType, message: errorMessage }); } } _handlePortClose() { logger.info("WebSerial port physically closed (via close())"); if (this._portCloseResolve) { this._portCloseResolve(); this._portCloseResolve = null; } this._handleConnectionLoss("Port closed via close()"); } _watchForPortClose() { if (!this._portClosePromise) return; this._portClosePromise.then(() => { this._handlePortClose(); }).catch(() => { }); } async _releaseAllResources(hardClose = false) { logger.debug("Releasing WebSerial resources"); this._readLoopActive = false; if (this._readLoopAbortController) { this._readLoopAbortController.abort(); this._readLoopAbortController = null; } if (this.reader) { try { await this.reader.cancel(); this.reader.releaseLock(); } catch (err) { logger.debug("Error cancelling reader:", err.message); } this.reader = null; } if (this.writer) { try { this.writer.releaseLock(); if (hardClose && this.port && this.port.writable) { await this.writer.close().catch(() => { }); } } catch (err) { logger.debug("Error releasing writer:", err.message); } this.writer = null; } if (hardClose && this.port) { try { await this.port.close(); logger.debug("Port closed successfully"); if (this._portCloseResolve) { this._portCloseResolve(); this._portCloseResolve = null; } } catch (err) { logger.warn(`Error closing port: ${err.message}`); } this.port = null; } this.isOpen = false; this._isPortReady = false; this.readBuffer = (0, import_utils.allocUint8Array)(0); this._emptyReadCount = 0; this._connectedSlaveIds.clear(); } async connect() { if (this._isConnecting) { logger.warn("Connection had already started, waiting..."); return this._connectionPromise ?? Promise.resolve(); } if (!this.isOpen && !this._isConnecting && !this._isDisconnecting) { logger.debug("Transport in disconnected state, resetting resources"); await this._releaseAllResources(false); } this._isConnecting = true; this._connectionPromise = new Promise((resolve, reject) => { this._resolveConnection = resolve; this._rejectConnection = reject; }); try { if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null; } this._shouldReconnect = true; this._reconnectAttempts = 0; this._emptyReadCount = 0; logger.debug("Requesting new SerialPort instance from factory..."); if (this.port && this.isOpen) { logger.debug("Closing existing port before reconnecting"); await this._releaseAllResources(true); } this.port = await this.portFactory(); if (!this.port || typeof this.port.open !== "function") { throw new import_errors.WebSerialConnectionError( "Port factory did not return a valid SerialPort object." ); } logger.debug("New SerialPort instance acquired."); if (this.options.baudRate < WEB_SERIAL_CONSTANTS.MIN_BAUD_RATE || this.options.baudRate > WEB_SERIAL_CONSTANTS.MAX_BAUD_RATE) { throw new import_errors.ModbusConfigError(`Invalid baud rate: ${this.options.baudRate}`); } await this.port.open({ baudRate: this.options.baudRate, dataBits: this.options.dataBits, stopBits: this.options.stopBits, parity: this.options.parity, flowControl: "none" }); const readable = this.port.readable; const writable = this.port.writable; if (!readable || !writable) { const errorMsg = "Serial port not readable/writable after open"; logger.error(errorMsg); await this._releaseAllResources(true); throw new import_errors.WebSerialConnectionError(errorMsg); } if (this.reader) { try { await this.reader.cancel(); this.reader.releaseLock(); } catch (err) { logger.debug(`Could not cancel previous reader, continuing: ${err.message}`); } this.reader = null; } if (this.writer) { try { this.writer.releaseLock(); } catch (err) { logger.debug(`Could not release previous writer, continuing: ${err.message}`); } this.writer = null; } this.reader = readable.getReader(); this.writer = writable.getWriter(); logger.debug(`New writer created: ${this.writer !== null}, reader: ${this.reader !== null}`); this.isOpen = true; this._isPortReady = true; this._reconnectAttempts = 0; this._portClosePromise = new Promise((resolve) => { this._portCloseResolve = resolve; }); this._watchForPortClose(); this._startReading(); logger.info("WebSerial port opened successfully with new instance"); await this._notifyPortConnected(); if (this._resolveConnection) { this._resolveConnection(); this._resolveConnection = null; this._rejectConnection = null; } } catch (err) { logger.error(`Failed to open WebSerial port: ${err.message}`); this.isOpen = false; this._isPortReady = false; this._isConnecting = false; if (this._wasEverConnected) { await this._notifyPortDisconnected( import_modbus_types.ConnectionErrorType.ConnectionLost, err.message ); } if (this._shouldReconnect && this._reconnectAttempts < this.options.maxReconnectAttempts) { logger.info("Auto-reconnect enabled, starting reconnect process..."); this._scheduleReconnect(err); } else { if (this._rejectConnection) { this._rejectConnection(err); this._resolveConnection = null; this._rejectConnection = null; } throw err; } } finally { this._isConnecting = false; } } _startReading() { if (!this.isOpen || !this.reader || this._readLoopActive) { logger.warn("Cannot start reading: port not open, no reader, or loop already active"); return; } this._readLoopActive = true; this._readLoopAbortController = new AbortController(); logger.debug("Starting read loop"); const loop = async () => { try { while (this.isOpen && this.reader && this._readLoopAbortController && !this._readLoopAbortController.signal.aborted) { try { const { value, done } = await this.reader.read(); if (done || this._readLoopAbortController.signal.aborted) { logger.debug("Read stream ended. It might restart if the port is still open."); break; } if (value && value.length > 0) { this._emptyReadCount = 0; if (this.readBuffer.length + value.length > WEB_SERIAL_CONSTANTS.MAX_READ_BUFFER_SIZE) { logger.error("Buffer overflow detected"); throw new import_errors.ModbusBufferOverflowError( this.readBuffer.length + value.length, WEB_SERIAL_CONSTANTS.MAX_READ_BUFFER_SIZE ); } const newBuffer = new Uint8Array(this.readBuffer.length + value.length); newBuffer.set(this.readBuffer, 0); newBuffer.set(value, this.readBuffer.length); this.readBuffer = newBuffer; } else { this._emptyReadCount++; logger.debug(`Empty read received, count: ${this._emptyReadCount}`); if (this._emptyReadCount >= this.options.maxEmptyReadsBeforeReconnect) { logger.warn( `Max empty reads (${this.options.maxEmptyReadsBeforeReconnect}) reached. Triggering reconnect.` ); this._handleConnectionLoss("Too many empty reads"); break; } } } catch (readErr) { if (this._readLoopAbortController.signal.aborted) { logger.debug("Read loop aborted"); break; } logger.warn(`Read error: ${readErr.message}`); this._handleConnectionLoss(`Read error: ${readErr.message}`); break; } } } catch (loopErr) { logger.error(`Unexpected error in read loop: ${loopErr.message}`); this._handleConnectionLoss(`Read loop failed: ${loopErr.message}`); } finally { this._readLoopActive = false; logger.debug("Read loop finished"); } }; loop().catch((err) => { logger.error("Read loop promise rejected:", err); this._handleConnectionLoss(`Read loop promise rejected: ${err.message}`); }); } async write(buffer) { if (this._isFlushing) { logger.debug("Write operation aborted due to ongoing flush"); throw new import_errors.ModbusFlushError(); } if (!this.isPortReady()) { logger.warn(`Write attempted on disconnected port`); throw new import_errors.WebSerialWriteError("Port is not ready for writing"); } if (buffer.length === 0) { throw new import_errors.ModbusBufferUnderrunError(0, 1); } const release = await this._operationMutex.acquire(); try { const timeout = this.options.writeTimeout; const abort = new AbortController(); const timer = setTimeout(() => abort.abort(), timeout); try { await this.writer.write(buffer); logger.debug(`Wrote ${buffer.length} bytes to WebSerial port`); } catch (err) { if (abort.signal.aborted) { logger.warn(`Write timeout on WebSerial port`); const timeoutError = new import_errors.ModbusTimeoutError("Write timeout"); this._onError(timeoutError); throw timeoutError; } else { logger.error(`Write error on WebSerial port: ${err.message}`); const isPhysical = err.message.includes("failed to write") || err.message.includes("device disconnected") || err.message.includes("The device has been lost"); if (isPhysical) { if (this.writer) { try { this.writer.releaseLock(); } catch (err2) { logger.debug( `Could not release writer is physical, continuing: ${err2.message}` ); } this.writer = null; } if (this.port && this.isOpen) { this.port.close().catch(() => { }); } this._handleConnectionLoss("Write failed \u2014 port unplugged"); } else { this._onError(err); } throw err; } } finally { clearTimeout(timer); abort.abort(); } } finally { release(); } } async read(length, timeout = this.options.readTimeout) { if (!this.isPortReady()) { throw new import_errors.WebSerialReadError("Port is not ready"); } if (length <= 0) { throw new import_errors.ModbusDataConversionError(length, "positive integer"); } const release = await this._operationMutex.acquire(); const start = Date.now(); try { return new Promise((resolve, reject) => { const check = () => { if (!this.isOpen) { return reject(new import_errors.WebSerialReadError("Port is closed")); } if (this._isFlushing) { return reject(new import_errors.ModbusFlushError()); } if (this.readBuffer.length >= length) { const data = this.readBuffer.slice(0, length); this.readBuffer = this.readBuffer.slice(length); if (data.length !== length) { return reject(new import_errors.ModbusInsufficientDataError(data.length, length)); } return resolve(data); } if (Date.now() - start > timeout) { return reject( new import_errors.ModbusTimeoutError(`Read timeout: No data received within ${timeout}ms`) ); } setTimeout(check, WEB_SERIAL_CONSTANTS.POLL_INTERVAL_MS); }; check(); }); } finally { release(); } } async disconnect() { logger.info("Disconnecting WebSerial transport..."); this._shouldReconnect = false; this._isDisconnecting = true; try { if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null; } await this._releaseAllResources(true); await this._notifyPortDisconnected( import_modbus_types.ConnectionErrorType.ManualDisconnect, "Port closed by user" ); if (this._rejectConnection) { this._rejectConnection(new import_errors.WebSerialConnectionError("Connection manually disconnected")); this._resolveConnection = null; this._rejectConnection = null; } logger.info("WebSerial transport disconnected successfully"); } catch (err) { logger.error(`Error during WebSerial transport shutdown: ${err.message}`); } finally { this._isDisconnecting = false; this.isOpen = false; this._isPortReady = false; this.port = null; this.reader = null; this.writer = null; } } async flush() { logger.debug("Flushing WebSerial transport buffer"); if (this._isFlushing) { logger.warn("Flush already in progress"); await Promise.all(this._pendingFlushPromises).catch(() => { }); return; } this._isFlushing = true; const flushPromise = new Promise((resolve) => { this._pendingFlushPromises.push(resolve); }); try { this.readBuffer = (0, import_utils.allocUint8Array)(0); this._emptyReadCount = 0; logger.debug("WebSerial read buffer flushed"); } finally { this._isFlushing = false; this._pendingFlushPromises.forEach((resolve) => resolve()); this._pendingFlushPromises = []; logger.debug("WebSerial transport flush completed"); } return flushPromise; } _onError(err) { handleModbusError(err); logger.warn(`Modbus error: ${err.message}`); } _handleConnectionLoss(reason) { if (!this.isOpen && !this._isConnecting) return; logger.warn(`Connection loss detected: ${reason}`); if (this._isConnecting && this._rejectConnection) { const err = new import_errors.WebSerialConnectionError(`Connection lost during connect: ${reason}`); this._rejectConnection(err); this._resolveConnection = null; this._rejectConnection = null; } this.isOpen = false; this._isPortReady = false; this._readLoopActive = false; if (this.writer) { try { this.writer.releaseLock(); } catch (err) { logger.debug( `Could not release writer is connection loss, continuing: ${err.message}` ); } this.writer = null; } if (this.reader) { try { this.reader.cancel(); this.reader.releaseLock(); } catch (err) { logger.debug( `Could not release reader is connection loss, continuing: ${err.message}` ); } this.reader = null; } this._notifyPortDisconnected(import_modbus_types.ConnectionErrorType.ConnectionLost, reason); if (this._shouldReconnect && !this._isDisconnecting) { this._scheduleReconnect(new Error(reason)); } } _scheduleReconnect(err) { if (!this._shouldReconnect || this._isDisconnecting) { logger.info("Reconnect disabled or disconnecting, not scheduling"); return; } if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); } if (this._reconnectAttempts >= this.options.maxReconnectAttempts) { logger.error( `Max reconnect attempts (${this.options.maxReconnectAttempts}) reached for WebSerial port` ); const maxAttemptsError = new import_errors.WebSerialConnectionError( `Max reconnect attempts (${this.options.maxReconnectAttempts}) reached` ); this._notifyPortDisconnected(import_modbus_types.ConnectionErrorType.MaxReconnect, maxAttemptsError.message); if (this._rejectConnection) { this._rejectConnection(maxAttemptsError); this._resolveConnection = null; this._rejectConnection = null; } this._shouldReconnect = false; return; } this._reconnectAttempts++; logger.info( `Scheduling reconnect in ${this.options.reconnectInterval} ms (attempt ${this._reconnectAttempts}) due to: ${err.message}` ); this._reconnectTimer = setTimeout(() => { this._reconnectTimer = null; this._attemptReconnect(); }, this.options.reconnectInterval); } async _attemptReconnect() { try { if (this.port && this.isOpen) { await this._releaseAllResources(true); } this.port = await this.portFactory(); if (!this.port || typeof this.port.open !== "function") { throw new import_errors.WebSerialConnectionError( "Port factory did not return a valid SerialPort object." ); } await this.port.open({ baudRate: this.options.baudRate, dataBits: this.options.dataBits, stopBits: this.options.stopBits, parity: this.options.parity, flowControl: "none" }); const readable = this.port.readable; const writable = this.port.writable; if (!readable || !writable) { throw new import_errors.WebSerialConnectionError("Serial port not readable/writable after open"); } if (this.reader) { try { await this.reader.cancel(); this.reader.releaseLock(); } catch (err) { logger.debug( `Could not release reader is attempt reconnect, continuing: ${err.message}` ); } this.reader = null; } if (this.writer) { try { this.writer.releaseLock(); } catch (err) { logger.debug( `Could not release writer is attempt reconnect, continuing: ${err.message}` ); } this.writer = null; } this.reader = readable.getReader(); this.writer = writable.getWriter(); logger.debug(`New writer created: ${this.writer !== null}, reader: ${this.reader !== null}`); this.isOpen = true; this._isPortReady = true; this._reconnectAttempts = 0; this._portClosePromise = new Promise((resolve) => { this._portCloseResolve = resolve; }); this._watchForPortClose(); this._startReading(); logger.info(`Reconnect successful`); await this._notifyPortConnected(); if (this._resolveConnection) { this._resolveConnection(); this._resolveConnection = null; this._rejectConnection = null; } } catch (err) { logger.warn(`Reconnect attempt failed: ${err.message}`); this._reconnectAttempts++; if (this._shouldReconnect && !this._isDisconnecting && this._reconnectAttempts <= this.options.maxReconnectAttempts) { this._scheduleReconnect(err); } else { const maxAttemptsError = new import_errors.WebSerialConnectionError( `Max reconnect attempts (${this.options.maxReconnectAttempts}) reached` ); await this._notifyPortDisconnected( import_modbus_types.ConnectionErrorType.MaxReconnect, maxAttemptsError.message ); if (this._rejectConnection) { this._rejectConnection(maxAttemptsError); this._resolveConnection = null; this._rejectConnection = null; } this._shouldReconnect = false; } } } destroy() { logger.info("Destroying WebSerial transport..."); this._shouldReconnect = false; if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null; } if (this._rejectConnection) { this._rejectConnection(new import_errors.WebSerialTransportError("Transport destroyed")); this._resolveConnection = null; this._rejectConnection = null; } if (this._portCloseResolve) { this._portCloseResolve(); this._portCloseResolve = null; } this._readLoopActive = false; this._releaseAllResources(true).catch(() => { }); this._notifyPortDisconnected(import_modbus_types.ConnectionErrorType.Destroyed, "Transport destroyed"); this.isOpen = false; this._isPortReady = false; this._isConnecting = false; this._isDisconnecting = false; } } module.exports = WebSerialTransport;