UNPKG

modbus-connect

Version:

Modbus RTU over Web Serial and Node.js SerialPort

982 lines (855 loc) 32.8 kB
// src/transport/web-transports/web-serialport.ts import Logger from '../../logger.js'; import { allocUint8Array } from '../../utils/utils.js'; import { Mutex } from 'async-mutex'; import { ModbusTimeoutError, ModbusTooManyEmptyReadsError, ModbusFlushError, WebSerialTransportError, WebSerialConnectionError, WebSerialReadError, WebSerialWriteError, 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, } from '../../errors.js'; import { Transport, WebSerialPort, WebSerialPortOptions, WebSerialTransportOptions, DeviceStateHandler, ConnectionErrorType, PortStateHandler, RSMode, } from '../../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, } as const; const loggerInstance = new Logger(); loggerInstance.setLogFormat(['timestamp', 'level', 'logger']); loggerInstance.setCustomFormatter('logger', value => (value ? `[${value}]` : '')); const logger = loggerInstance.createLogger('WebSerialTransport'); logger.setLevel('info'); const ERROR_HANDLERS: Record<string, () => void> = { [ModbusTimeoutError.name]: () => logger.error('Timeout error detected'), [ModbusCRCError.name]: () => logger.error('CRC error detected'), [ModbusParityError.name]: () => logger.error('Parity error detected'), [ModbusNoiseError.name]: () => logger.error('Noise error detected'), [ModbusFramingError.name]: () => logger.error('Framing error detected'), [ModbusOverrunError.name]: () => logger.error('Overrun error detected'), [ModbusCollisionError.name]: () => logger.error('Collision error detected'), [ModbusConfigError.name]: () => logger.error('Configuration error detected'), [ModbusBaudRateError.name]: () => logger.error('Baud rate error detected'), [ModbusSyncError.name]: () => logger.error('Sync error detected'), [ModbusFrameBoundaryError.name]: () => logger.error('Frame boundary error detected'), [ModbusLRCError.name]: () => logger.error('LRC error detected'), [ModbusChecksumError.name]: () => logger.error('Checksum error detected'), [ModbusDataConversionError.name]: () => logger.error('Data conversion error detected'), [ModbusBufferOverflowError.name]: () => logger.error('Buffer overflow error detected'), [ModbusBufferUnderrunError.name]: () => logger.error('Buffer underrun error detected'), [ModbusMemoryError.name]: () => logger.error('Memory error detected'), [ModbusStackOverflowError.name]: () => logger.error('Stack overflow error detected'), [ModbusResponseError.name]: () => logger.error('Response error detected'), [ModbusInvalidAddressError.name]: () => logger.error('Invalid address error detected'), [ModbusInvalidFunctionCodeError.name]: () => logger.error('Invalid function code error detected'), [ModbusInvalidQuantityError.name]: () => logger.error('Invalid quantity error detected'), [ModbusIllegalDataAddressError.name]: () => logger.error('Illegal data address error detected'), [ModbusIllegalDataValueError.name]: () => logger.error('Illegal data value error detected'), [ModbusSlaveBusyError.name]: () => logger.error('Slave busy error detected'), [ModbusAcknowledgeError.name]: () => logger.error('Acknowledge error detected'), [ModbusSlaveDeviceFailureError.name]: () => logger.error('Slave device failure error detected'), [ModbusMalformedFrameError.name]: () => logger.error('Malformed frame error detected'), [ModbusInvalidFrameLengthError.name]: () => logger.error('Invalid frame length error detected'), [ModbusInvalidTransactionIdError.name]: () => logger.error('Invalid transaction ID error detected'), [ModbusUnexpectedFunctionCodeError.name]: () => logger.error('Unexpected function code error detected'), [ModbusConnectionRefusedError.name]: () => logger.error('Connection refused error detected'), [ModbusConnectionTimeoutError.name]: () => logger.error('Connection timeout error detected'), [ModbusNotConnectedError.name]: () => logger.error('Not connected error detected'), [ModbusAlreadyConnectedError.name]: () => logger.error('Already connected error detected'), [ModbusInsufficientDataError.name]: () => logger.error('Insufficient data error detected'), [ModbusGatewayPathUnavailableError.name]: () => logger.error('Gateway path unavailable error detected'), [ModbusGatewayTargetDeviceError.name]: () => logger.error('Gateway target device error detected'), [ModbusInvalidStartingAddressError.name]: () => logger.error('Invalid starting address error detected'), [ModbusMemoryParityError.name]: () => logger.error('Memory parity error detected'), [ModbusBroadcastError.name]: () => logger.error('Broadcast error detected'), [ModbusGatewayBusyError.name]: () => logger.error('Gateway busy error detected'), [ModbusDataOverrunError.name]: () => logger.error('Data overrun error detected'), [ModbusTooManyEmptyReadsError.name]: () => logger.error('Too many empty reads error detected'), [ModbusFlushError.name]: () => logger.error('Flush error detected'), [WebSerialTransportError.name]: () => logger.error('WebSerial transport error detected'), [WebSerialConnectionError.name]: () => logger.error('WebSerial connection error detected'), [WebSerialReadError.name]: () => logger.error('WebSerial read error detected'), [WebSerialWriteError.name]: () => logger.error('WebSerial write error detected'), }; const handleModbusError = (err: Error): void => { const handler = ERROR_HANDLERS[err.constructor.name]; if (handler) { handler(); } else { logger.error(`Unknown error: ${err.message}`); } }; class WebSerialTransport implements Transport { public isOpen: boolean = false; private portFactory: () => Promise<WebSerialPort>; private port: WebSerialPort | null = null; private options: Required<WebSerialTransportOptions>; private reader: ReadableStreamDefaultReader<Uint8Array> | null = null; private writer: WritableStreamDefaultWriter<Uint8Array> | null = null; private readBuffer: Uint8Array; private _connectedSlaveIds: Set<number> = new Set(); private _isPortReady: boolean = false; private _reconnectAttempts: number = 0; private _shouldReconnect: boolean = true; private _isConnecting: boolean = false; private _isDisconnecting: boolean = false; private _isFlushing: boolean = false; private _pendingFlushPromises: Array<() => void> = []; private _reconnectTimer: NodeJS.Timeout | null = null; private _emptyReadCount: number = 0; private _readLoopActive: boolean = false; private _readLoopAbortController: AbortController | null = null; private _operationMutex: Mutex; private _connectionPromise: Promise<void> | null = null; private _resolveConnection: (() => void) | null = null; private _rejectConnection: ((reason?: Error | string | null) => void) | null = null; private _portClosePromise: Promise<void> | null = null; private _portCloseResolve: (() => void) | null = null; private _deviceStateHandler: DeviceStateHandler | null = null; private _portStateHandler: PortStateHandler | null = null; private _wasEverConnected: boolean = false; constructor(portFactory: () => Promise<WebSerialPort>, options: WebSerialTransportOptions = {}) { if (typeof portFactory !== 'function') { throw new WebSerialTransportError( 'A port factory function must be provided to WebSerialTransport' ); } this.portFactory = portFactory; this.options = { baudRate: 9600, dataBits: 8, stopBits: 1, parity: 'none', readTimeout: 1000, writeTimeout: 1000, reconnectInterval: 3000, maxReconnectAttempts: Infinity, maxEmptyReadsBeforeReconnect: 10, RSMode: options.RSMode || 'RS485', ...options, }; this.readBuffer = new Uint8Array(0); this._operationMutex = new Mutex(); } public getRSMode(): RSMode { return this.options.RSMode; } public setDeviceStateHandler(handler: DeviceStateHandler): void { this._deviceStateHandler = handler; } public setPortStateHandler(handler: PortStateHandler): void { this._portStateHandler = handler; } public async disableDeviceTracking(): Promise<void> { this._deviceStateHandler = null; logger.debug('Device connection tracking disabled'); } public async enableDeviceTracking(handler?: DeviceStateHandler): Promise<void> { if (handler) { this._deviceStateHandler = handler; } logger.debug('Device connection tracking enabled'); } public notifyDeviceConnected(slaveId: number): void { if (this._connectedSlaveIds.has(slaveId)) { return; } this._connectedSlaveIds.add(slaveId); if (this._deviceStateHandler) { this._deviceStateHandler(slaveId, true); } } public notifyDeviceDisconnected( slaveId: number, errorType: ConnectionErrorType, errorMessage: string ): void { if (!this._connectedSlaveIds.has(slaveId)) { return; } this._connectedSlaveIds.delete(slaveId); if (this._deviceStateHandler) { this._deviceStateHandler(slaveId, false, { type: errorType, message: errorMessage }); } } public removeConnectedDevice(slaveId: number): void { if (this._connectedSlaveIds.has(slaveId)) { this._connectedSlaveIds.delete(slaveId); logger.debug(`Manually removed device ${slaveId} from connected set`); } } public isPortReady(): boolean { 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; } private async _notifyPortConnected(): Promise<void> { this._wasEverConnected = true; if (this._portStateHandler) { this._portStateHandler(true, [], undefined); } } private async _notifyPortDisconnected( errorType: ConnectionErrorType = ConnectionErrorType.UnknownError, errorMessage: string = 'Port disconnected' ): Promise<void> { if (!this._wasEverConnected) { logger.debug('Skipping DISCONNECTED — port was never connected'); return; } if (this._portStateHandler) { this._portStateHandler(false, [], { type: errorType, message: errorMessage }); } } private _handlePortClose(): void { logger.info('WebSerial port physically closed (via close())'); if (this._portCloseResolve) { this._portCloseResolve(); this._portCloseResolve = null; } this._handleConnectionLoss('Port closed via close()'); } private _watchForPortClose(): void { if (!this._portClosePromise) return; this._portClosePromise .then(() => { this._handlePortClose(); }) .catch(() => {}); } private async _releaseAllResources(hardClose = false): Promise<void> { 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: unknown) { logger.debug('Error cancelling reader:', (err as Error).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: unknown) { logger.debug('Error releasing writer:', (err as Error).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: unknown) { logger.warn(`Error closing port: ${(err as Error).message}`); } this.port = null; } this.isOpen = false; this._isPortReady = false; this.readBuffer = allocUint8Array(0); this._emptyReadCount = 0; this._connectedSlaveIds.clear(); } async connect(): Promise<void> { 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 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 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', } as WebSerialPortOptions); 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 WebSerialConnectionError(errorMsg); } if (this.reader) { try { await this.reader.cancel(); this.reader.releaseLock(); } catch (err: unknown) { logger.debug(`Could not cancel previous reader, continuing: ${(err as Error).message}`); } this.reader = null; } if (this.writer) { try { this.writer.releaseLock(); } catch (err: unknown) { logger.debug(`Could not release previous writer, continuing: ${(err as Error).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<void>(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: unknown) { logger.error(`Failed to open WebSerial port: ${(err as Error).message}`); this.isOpen = false; this._isPortReady = false; this._isConnecting = false; if (this._wasEverConnected) { await this._notifyPortDisconnected( ConnectionErrorType.ConnectionLost, (err as Error).message ); } if (this._shouldReconnect && this._reconnectAttempts < this.options.maxReconnectAttempts) { logger.info('Auto-reconnect enabled, starting reconnect process...'); this._scheduleReconnect(err as Error); } else { if (this._rejectConnection) { this._rejectConnection(err as Error); this._resolveConnection = null; this._rejectConnection = null; } throw err; } } finally { this._isConnecting = false; } } private _startReading(): void { 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 (): Promise<void> => { 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 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: unknown) { if (this._readLoopAbortController.signal.aborted) { logger.debug('Read loop aborted'); break; } logger.warn(`Read error: ${(readErr as Error).message}`); this._handleConnectionLoss(`Read error: ${(readErr as Error).message}`); break; } } } catch (loopErr: unknown) { logger.error(`Unexpected error in read loop: ${(loopErr as Error).message}`); this._handleConnectionLoss(`Read loop failed: ${(loopErr as Error).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: Uint8Array): Promise<void> { if (this._isFlushing) { logger.debug('Write operation aborted due to ongoing flush'); throw new ModbusFlushError(); } if (!this.isPortReady()) { logger.warn(`Write attempted on disconnected port`); throw new WebSerialWriteError('Port is not ready for writing'); } if (buffer.length === 0) { throw new 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: unknown) { if (abort.signal.aborted) { logger.warn(`Write timeout on WebSerial port`); const timeoutError = new ModbusTimeoutError('Write timeout'); this._onError(timeoutError); throw timeoutError; } else { logger.error(`Write error on WebSerial port: ${(err as Error).message}`); const isPhysical = (err as Error).message.includes('failed to write') || (err as Error).message.includes('device disconnected') || (err as Error).message.includes('The device has been lost'); if (isPhysical) { if (this.writer) { try { this.writer.releaseLock(); } catch (err: unknown) { logger.debug( `Could not release writer is physical, continuing: ${(err as Error).message}` ); } this.writer = null; } if (this.port && this.isOpen) { this.port.close().catch(() => {}); } this._handleConnectionLoss('Write failed — port unplugged'); } else { this._onError(err as Error); } throw err; } } finally { clearTimeout(timer); abort.abort(); } } finally { release(); } } async read(length: number, timeout: number = this.options.readTimeout): Promise<Uint8Array> { if (!this.isPortReady()) { throw new WebSerialReadError('Port is not ready'); } if (length <= 0) { throw new ModbusDataConversionError(length, 'positive integer'); } const release = await this._operationMutex.acquire(); const start = Date.now(); try { return new Promise<Uint8Array>((resolve, reject) => { const check = () => { if (!this.isOpen) { return reject(new WebSerialReadError('Port is closed')); } if (this._isFlushing) { return reject(new 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 ModbusInsufficientDataError(data.length, length)); } return resolve(data); } if (Date.now() - start > timeout) { return reject( new ModbusTimeoutError(`Read timeout: No data received within ${timeout}ms`) ); } setTimeout(check, WEB_SERIAL_CONSTANTS.POLL_INTERVAL_MS); }; check(); }); } finally { release(); } } async disconnect(): Promise<void> { 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( ConnectionErrorType.ManualDisconnect, 'Port closed by user' ); if (this._rejectConnection) { this._rejectConnection(new WebSerialConnectionError('Connection manually disconnected')); this._resolveConnection = null; this._rejectConnection = null; } logger.info('WebSerial transport disconnected successfully'); } catch (err: unknown) { logger.error(`Error during WebSerial transport shutdown: ${(err as Error).message}`); } finally { this._isDisconnecting = false; this.isOpen = false; this._isPortReady = false; this.port = null; this.reader = null; this.writer = null; } } async flush(): Promise<void> { 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<void>(resolve => { this._pendingFlushPromises.push(resolve); }); try { this.readBuffer = 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; } private _onError(err: Error): void { handleModbusError(err); logger.warn(`Modbus error: ${err.message}`); } private _handleConnectionLoss(reason: string): void { if (!this.isOpen && !this._isConnecting) return; logger.warn(`Connection loss detected: ${reason}`); if (this._isConnecting && this._rejectConnection) { const err = new 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: unknown) { logger.debug( `Could not release writer is connection loss, continuing: ${(err as Error).message}` ); } this.writer = null; } if (this.reader) { try { this.reader.cancel(); this.reader.releaseLock(); } catch (err: unknown) { logger.debug( `Could not release reader is connection loss, continuing: ${(err as Error).message}` ); } this.reader = null; } this._notifyPortDisconnected(ConnectionErrorType.ConnectionLost, reason); if (this._shouldReconnect && !this._isDisconnecting) { this._scheduleReconnect(new Error(reason)); } } private _scheduleReconnect(err: Error): void { 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 WebSerialConnectionError( `Max reconnect attempts (${this.options.maxReconnectAttempts}) reached` ); this._notifyPortDisconnected(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); } private async _attemptReconnect(): Promise<void> { 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 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', } as WebSerialPortOptions); const readable = this.port.readable; const writable = this.port.writable; if (!readable || !writable) { throw new WebSerialConnectionError('Serial port not readable/writable after open'); } if (this.reader) { try { await this.reader.cancel(); this.reader.releaseLock(); } catch (err: unknown) { logger.debug( `Could not release reader is attempt reconnect, continuing: ${(err as Error).message}` ); } this.reader = null; } if (this.writer) { try { this.writer.releaseLock(); } catch (err: unknown) { logger.debug( `Could not release writer is attempt reconnect, continuing: ${(err as Error).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<void>(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: unknown) { logger.warn(`Reconnect attempt failed: ${(err as Error).message}`); this._reconnectAttempts++; if ( this._shouldReconnect && !this._isDisconnecting && this._reconnectAttempts <= this.options.maxReconnectAttempts ) { this._scheduleReconnect(err as Error); } else { const maxAttemptsError = new WebSerialConnectionError( `Max reconnect attempts (${this.options.maxReconnectAttempts}) reached` ); await this._notifyPortDisconnected( ConnectionErrorType.MaxReconnect, maxAttemptsError.message ); if (this._rejectConnection) { this._rejectConnection(maxAttemptsError); this._resolveConnection = null; this._rejectConnection = null; } this._shouldReconnect = false; } } } destroy(): void { logger.info('Destroying WebSerial transport...'); this._shouldReconnect = false; if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null; } if (this._rejectConnection) { this._rejectConnection(new 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(ConnectionErrorType.Destroyed, 'Transport destroyed'); this.isOpen = false; this._isPortReady = false; this._isConnecting = false; this._isDisconnecting = false; } } export = WebSerialTransport;