| 'use strict'; |
| |
| /// Web Serial API Implementation |
| /// https://developer.mozilla.org/en-US/docs/Web/API/SerialPort |
| class SerialPort { |
| constructor(port) { |
| this._port = port; |
| this._readLoopPromise = null; |
| this._reader = null; |
| this._writer = null; |
| this._initialized = false; |
| this._keepReading = true; |
| this.isConnected = false; |
| } |
| |
| /// Connect and start reading loop |
| async connect(options = { baudRate: 9600 }) { |
| if (this._initialized) { |
| try { |
| await this.disconnect(); |
| } catch (error) { |
| console.error('Error disconnecting previous port:', error); |
| } |
| |
| if (this._readLoopPromise) { |
| try { |
| await this._readLoopPromise; |
| } catch (error) { |
| console.error('Error in read loop:', error); |
| } |
| } |
| this._readLoopPromise = null; |
| } |
| this._initialized = true; |
| |
| this.isConnected = true; |
| this._keepReading = true; |
| |
| try { |
| await this._port.open(options); |
| } catch (error) { |
| this.isConnected = false; |
| throw error; |
| } |
| |
| this._readLoopPromise = this._readLoop(); |
| } |
| |
| /// Internal continuous read loop |
| async _readLoop() { |
| try { |
| while (this._port.readable && this._keepReading) { |
| this._reader = this._port.readable.getReader(); |
| try { |
| while (true) { |
| const { value, done } = await this._reader.read(); |
| if (done) { |
| // |reader| has been canceled. |
| break; |
| } |
| if (this.onReceive) { |
| this.onReceive(value); |
| } |
| } |
| } catch (error) { |
| if (this.onReceiveError) this.onReceiveError(error); |
| } finally { |
| this._reader.releaseLock(); |
| } |
| } |
| } finally { |
| this.isConnected = false; |
| await this._port.close(); |
| } |
| } |
| |
| /// Stop reading and release port |
| async disconnect() { |
| this._keepReading = false; |
| |
| if (this._reader) { |
| try { |
| await this._reader.cancel(); |
| } catch (error) { |
| console.error('Error cancelling reader:', error); |
| } |
| this._reader.releaseLock(); |
| } |
| |
| if (this._writer) { |
| try { |
| await this._writer.abort(); |
| } catch (error) { |
| console.error('Error closing writer:', error); |
| } |
| this._writer.releaseLock(); |
| } |
| |
| try { |
| await this._port.close(); |
| } catch (error) { |
| console.error('Error closing port:', error); |
| } |
| |
| if (this._readLoopPromise) { |
| try { |
| await this._readLoopPromise; |
| } catch (error) { |
| console.error('Error in read loop:', error); |
| } |
| } |
| } |
| |
| /// Send data to port |
| send(data) { |
| if (!this._port.writable) { |
| throw new Error('Port is not writable'); |
| } |
| this._writer = this._port.writable.getWriter(); |
| if (!this._writer) { |
| throw new Error('Failed to get writer from port'); |
| } |
| try { |
| return this._writer.write(data); |
| } finally { |
| this._writer.releaseLock(); |
| } |
| } |
| |
| async forgetDevice() {} |
| } |
| |
| /// WebUSB Implementation |
| class WebUsbSerialPort { |
| constructor(device) { |
| this._device = device; |
| this._interfaceNumber = 0; |
| this._endpointIn = 0; |
| this._endpointOut = 0; |
| this.isConnected = false; |
| this._readLoopPromise = null; |
| this._initialized = false; |
| this._keepReading = true; |
| |
| this._vendorId = device.vendorId; |
| this._productId = device.productId; |
| } |
| |
| _isSameWebUsbSerialPort(webUsbSerialPort) { |
| return this._vendorId === webUsbSerialPort._vendorId && this._productId === webUsbSerialPort._productId; |
| } |
| |
| /// Connect and start reading loop |
| async connect() { |
| if (this._initialized) { |
| try { |
| await this.disconnect(); |
| } catch (error) { |
| console.error('Error disconnecting previous device:', error); |
| } |
| |
| const webUsbSerialPorts = await serial.getWebUsbSerialPorts(); |
| const webUsbSerialPort = webUsbSerialPorts.find(serialPort => this._isSameWebUsbSerialPort(serialPort)); |
| this._device = webUsbSerialPort ? webUsbSerialPort._device : this._device; |
| } |
| this._initialized = true; |
| |
| this.isConnected = true; |
| this._keepReading = true; |
| try { |
| await this._device.open(); |
| |
| if (!this._device.configuration) { |
| await this._device.selectConfiguration(1); |
| } |
| |
| // Find interface with vendor-specific class (0xFF) and endpoints |
| for (const iface of this._device.configuration.interfaces) { |
| for (const alternate of iface.alternates) { |
| if (alternate.interfaceClass === 0xff) { |
| this._interfaceNumber = iface.interfaceNumber; |
| for (const endpoint of alternate.endpoints) { |
| if (endpoint.direction === 'out') this._endpointOut = endpoint.endpointNumber; |
| else if (endpoint.direction === 'in') this._endpointIn = endpoint.endpointNumber; |
| } |
| } |
| } |
| } |
| |
| if (this._interfaceNumber === undefined) { |
| throw new Error('No suitable interface found.'); |
| } |
| |
| await this._device.claimInterface(this._interfaceNumber); |
| await this._device.selectAlternateInterface(this._interfaceNumber, 0); |
| |
| // Set device to ENABLE (0x22 = SET_CONTROL_LINE_STATE, value 0x01 = activate) |
| await this._device.controlTransferOut({ |
| requestType: 'class', |
| recipient: 'interface', |
| request: 0x22, |
| value: 0x01, |
| index: this._interfaceNumber, |
| }); |
| } catch (error) { |
| this.isConnected = false; |
| throw error; |
| } |
| |
| this._readLoopPromise = this._readLoop(); |
| } |
| |
| /// Internal continuous read loop |
| async _readLoop() { |
| try { |
| while (this._keepReading && this.isConnected) { |
| try { |
| const result = await this._device.transferIn(this._endpointIn, 16384); |
| if (result.data && this.onReceive) { |
| this.onReceive(result.data); |
| } |
| } catch (error) { |
| this.isConnected = false; |
| if (this.onReceiveError) { |
| this.onReceiveError(error); |
| } |
| } |
| } |
| } finally { |
| this.isConnected = false; |
| await this._device.close(); |
| } |
| } |
| |
| /// Stop reading and release device |
| async disconnect() { |
| this._keepReading = false; |
| |
| try { |
| await this._device.controlTransferOut({ |
| requestType: 'class', |
| recipient: 'interface', |
| request: 0x22, |
| value: 0x00, |
| index: this._interfaceNumber, |
| }); |
| } catch (error) { |
| console.error('Error sending control transfer:', error); |
| } |
| |
| await this._device.releaseInterface(this._interfaceNumber); |
| |
| if (this._readLoopPromise) { |
| try { |
| await this._readLoopPromise; |
| } catch (error) { |
| console.error('Error in read loop:', error); |
| } |
| } |
| } |
| |
| /// Send data to device |
| send(data) { |
| return this._device.transferOut(this._endpointOut, data); |
| } |
| |
| async forgetDevice() { |
| await this.disconnect(); |
| await this._device.forget(); |
| } |
| } |
| |
| // Utility Functions |
| const serial = { |
| isWebSerialSupported: () => 'serial' in navigator, |
| isWebUsbSupported: () => 'usb' in navigator, |
| |
| async getSerialPorts() { |
| if (!this.isWebSerialSupported()) return []; |
| const ports = await navigator.serial.getPorts(); |
| return ports.map(port => new SerialPort(port)); |
| }, |
| |
| async getWebUsbSerialPorts() { |
| if (!this.isWebUsbSupported()) return []; |
| const devices = await navigator.usb.getDevices(); |
| return devices.map(device => new WebUsbSerialPort(device)); |
| }, |
| |
| async requestSerialPort() { |
| const port = await navigator.serial.requestPort(); |
| return new SerialPort(port); |
| }, |
| |
| async requestWebUsbSerialPort() { |
| const filters = [ |
| { vendorId: 0xcafe }, // TinyUSB |
| { vendorId: 0x239a }, // Adafruit |
| { vendorId: 0x2e8a }, // Raspberry Pi |
| { vendorId: 0x303a }, // Espressif |
| { vendorId: 0x2341 }, // Arduino |
| ]; |
| const device = await navigator.usb.requestDevice({ filters }); |
| return new WebUsbSerialPort(device); |
| } |
| }; |