307 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			307 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| '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);
 | |
|   }
 | |
| };
 | 
