1 // Copyright 2020 The Pigweed Authors
3 // Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 // use this file except in compliance with the License. You may obtain a copy of
7 // https://www.apache.org/licenses/LICENSE-2.0
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 // License for the specific language governing permissions and limitations under
15 /* eslint-env browser */
16 import {BehaviorSubject, Subject, Subscription, Observable} from 'rxjs';
17 import DeviceTransport from './device_transport';
19 const DEFAULT_SERIAL_OPTIONS: SerialOptions & {baudRate: number} = {
20 // Some versions of chrome use `baudrate` (linux)
22 // Some versions use `baudRate` (chromebook)
29 interface PortReadConnection {
30 chunks: Observable<Uint8Array>;
31 errors: Observable<Error>;
34 interface PortConnection extends PortReadConnection {
35 sendChunk: (chunk: Uint8Array) => Promise<void>;
38 export class DeviceLostError extends Error {
39 message = 'The device has been lost';
42 export class DeviceLockedError extends Error {
44 "The device's port is locked. Try unplugging it" +
45 ' and plugging it back in.';
49 * WebSerialTransport sends and receives UInt8Arrays to and
50 * from a serial device connected over USB.
52 export class WebSerialTransport implements DeviceTransport {
53 chunks = new Subject<Uint8Array>();
54 errors = new Subject<Error>();
55 connected = new BehaviorSubject<boolean>(false);
56 private portConnections: Map<SerialPort, PortConnection> = new Map();
57 private activePortConnectionConnection: PortConnection | undefined;
58 private rxSubscriptions: Subscription[] = [];
61 private serial: Serial = navigator.serial,
62 private filters: SerialPortFilter[] = [],
63 private serialOptions = DEFAULT_SERIAL_OPTIONS
67 * Send a UInt8Array chunk of data to the connected device.
68 * @param {Uint8Array} chunk The chunk to send
70 async sendChunk(chunk: Uint8Array): Promise<void> {
71 if (this.activePortConnectionConnection) {
72 return this.activePortConnectionConnection.sendChunk(chunk);
74 throw new Error('Device not connected');
78 * Attempt to open a connection to a device. This includes
79 * asking the user to select a serial port and should only
80 * be called in response to user interaction.
82 async connect(): Promise<void> {
83 const port = await this.serial.requestPort({filters: this.filters});
84 await this.connectPort(port);
87 private disconnect() {
88 for (const subscription of this.rxSubscriptions) {
89 subscription.unsubscribe();
91 this.rxSubscriptions = [];
93 this.activePortConnectionConnection = undefined;
94 this.connected.next(false);
98 * Connect to a given SerialPort. This involves no user interaction.
99 * and can be called whenever a port is available.
101 async connectPort(port: SerialPort): Promise<void> {
104 this.activePortConnectionConnection =
105 this.portConnections.get(port) ?? (await this.conectNewPort(port));
107 this.connected.next(true);
109 this.rxSubscriptions.push(
110 this.activePortConnectionConnection.chunks.subscribe(
112 this.chunks.next(chunk);
115 throw new Error(`Chunks observable had an unexpeted error ${err}`);
118 this.connected.next(false);
119 this.portConnections.delete(port);
120 // Don't complete the chunks observable because then it would not
121 // be able to forward any future chunks.
126 this.rxSubscriptions.push(
127 this.activePortConnectionConnection.errors.subscribe(error => {
128 this.errors.next(error);
129 if (error instanceof DeviceLostError) {
130 // The device has been lost
131 this.connected.next(false);
137 private async conectNewPort(port: SerialPort): Promise<PortConnection> {
138 await port.open(this.serialOptions);
139 const writer = port.writable.getWriter();
141 async function sendChunk(chunk: Uint8Array) {
143 await writer.write(chunk);
146 const {chunks, errors} = this.getChunks(port);
148 const connection: PortConnection = {sendChunk, chunks, errors};
149 this.portConnections.set(port, connection);
153 private getChunks(port: SerialPort): PortReadConnection {
154 const chunks = new Subject<Uint8Array>();
155 const errors = new Subject<Error>();
157 async function read() {
158 if (!port.readable) {
159 throw new DeviceLostError();
161 if (port.readable.locked) {
162 throw new DeviceLockedError();
164 await port.readable.pipeTo(
174 // Reconnect to the port.
182 read().catch(err => {
183 // Don't error the chunks observable since that stops it from
184 // reading any more packets, and we often want to continue
185 // despite an error. Instead, push errors to the 'errors'
193 return {chunks, errors};