|
|
|
@ -5,6 +5,8 @@ export class TransportWebSerial implements Types.Transport { |
|
|
|
private _toDevice: WritableStream<Uint8Array>; |
|
|
|
private _fromDevice: ReadableStream<Types.DeviceOutput>; |
|
|
|
private connection: SerialPort; |
|
|
|
private pipePromise: Promise<void> | null = null; |
|
|
|
private abortController: AbortController; |
|
|
|
|
|
|
|
public static async create(baudRate?: number): Promise<TransportWebSerial> { |
|
|
|
const port = await navigator.serial.requestPort(); |
|
|
|
@ -26,8 +28,13 @@ export class TransportWebSerial implements Types.Transport { |
|
|
|
} |
|
|
|
|
|
|
|
this.connection = connection; |
|
|
|
this.abortController = new AbortController(); |
|
|
|
|
|
|
|
Utils.toDeviceStream.readable.pipeTo(connection.writable); |
|
|
|
// Set up the pipe with abort signal for clean cancellation
|
|
|
|
this.pipePromise = Utils.toDeviceStream.readable.pipeTo( |
|
|
|
connection.writable, |
|
|
|
{ signal: this.abortController.signal } |
|
|
|
); |
|
|
|
|
|
|
|
this._toDevice = Utils.toDeviceStream.writable; |
|
|
|
this._fromDevice = connection.readable.pipeThrough( |
|
|
|
@ -43,7 +50,54 @@ export class TransportWebSerial implements Types.Transport { |
|
|
|
return this._fromDevice; |
|
|
|
} |
|
|
|
|
|
|
|
disconnect() { |
|
|
|
return this.connection.close(); |
|
|
|
/** |
|
|
|
* Safely disconnects the serial port, following best practices from |
|
|
|
* https://github.com/WICG/serial/. Cancels any active pipe
|
|
|
|
* operations and only closes the port after streams are unlocked. |
|
|
|
*/ |
|
|
|
async disconnect() { |
|
|
|
try { |
|
|
|
this.abortController.abort(); |
|
|
|
|
|
|
|
if (this.pipePromise) { |
|
|
|
try { |
|
|
|
await this.pipePromise; |
|
|
|
} catch (error) { |
|
|
|
if (error instanceof Error && error.name !== 'AbortError') { |
|
|
|
throw error; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Cancel any remaining streams
|
|
|
|
if (this._fromDevice && this._fromDevice.locked) { |
|
|
|
try { |
|
|
|
await this._fromDevice.cancel(); |
|
|
|
} catch (error) { |
|
|
|
// Stream cancellation might fail if already cancelled
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
await this.connection.close(); |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
// If we can't close cleanly, let the browser handle cleanup
|
|
|
|
console.warn('Could not cleanly disconnect serial port:', error); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Reconnects the transport by creating a new AbortController and re-establishing |
|
|
|
* the pipe connection. Only call this after disconnect() or if the connection failed. |
|
|
|
*/ |
|
|
|
async reconnect() { |
|
|
|
// Create a new AbortController for the new connection
|
|
|
|
this.abortController = new AbortController(); |
|
|
|
|
|
|
|
// Re-establish the pipe connection
|
|
|
|
this.pipePromise = Utils.toDeviceStream.readable.pipeTo( |
|
|
|
this.connection.writable, |
|
|
|
{ signal: this.abortController.signal } |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|