1. Extensions to the Navigator interface
[Exposed =Window ,SecureContext ]partial interface Navigator { [SameObject ]readonly attribute Serial serial ; };
1.1. serial attribute
When getting, the serial attribute always returns the same
instance of the Serial object.
2. Extensions to the WorkerNavigator interface
[Exposed =DedicatedWorker ,SecureContext ]partial interface WorkerNavigator { [SameObject ]readonly attribute Serial serial ; };
2.1. serial attribute
When getting, the serial attribute always returns the same
instance of the Serial object.
3. Serial interface
[Exposed =(DedicatedWorker ,Window ),SecureContext ]interface Serial :EventTarget {attribute EventHandler onconnect ;attribute EventHandler ondisconnect ;Promise <sequence <SerialPort >>getPorts (); [Exposed =Window ]Promise <SerialPort >requestPort (optional SerialPortRequestOptions = {}); };options
3.1. requestPort() method
requestPort(). This call
gives the browser the opportunity to prompt the user for which device the site
can be allowed to control. If the site is designed to work with a
particular device which is always connected via USB the site can provide a
filter restricting the devices the user can select to only those that would be
compatible. For example, a site which programs Arduino-powered robots could
specify a like the following to limit the set of selectable ports to only USB
devices with Arduino’s USB vendor ID,
const filter= { usbVendorId: 0x2341 }; const port= await navigator. serial. requestPort({ filters: [ filter] });
If on the other hand the site expects to be used with a wide variety of devices or devices connected through a USB to serial converter it can specify no filter at all and rely on the user to select the appropriate device,
const port= await navigator. serial. requestPort();
Asking the user to choose a port requires showing a prompt to the user and so the site has to have transient activation from something like the user clicking a button.
< button id = "connect" > Connect</ button >
const connectButton= document. getElementById( "connect" ); connectButton. addEventListener( 'click' , () => { try { const port= await navigator. serial. requestPort(); // Continue connecting to the device attached to |port|. } catch ( e) { // The prompt has been dismissed without selecting a device. } });
The user can choose not to select a device, in which case the Promise will
be rejected with a "NotFoundError" DOMException that the site has to
handle.
requestPort() method steps are:
-
Let promise be a new promise.
-
If this’s relevant global object’s associated Document is not allowed to use the policy-controlled feature named "serial", reject promise with a "
SecurityError"DOMExceptionand return promise. -
If the relevant global object of this does not have transient activation, reject promise with a "
SecurityError"DOMExceptionand return promise. -
If options["
filters"] is present, then for each filter in options["filters"] run the following steps:-
If filter["
bluetoothServiceClassId"] is present:-
If filter["
usbVendorId"] is present, reject promise with aTypeErrorand return promise. -
If filter["
usbProductId"] is present, reject promise with aTypeErrorand return promise.
-
-
If filter["
usbVendorId"] is not present, reject promise with aTypeErrorand return promise.Note: This check implements the combined rule that a
SerialPortFiltercannot be empty and ifusbProductIdis specified thenusbVendorIdmust also be specified.
-
-
Run the following steps in parallel:
-
Let allPorts be an empty list.
-
For each Bluetooth device registered with the system:
-
For each
BluetoothServiceUUIDuuid supported by the device:-
If uuid is not a blocked Bluetooth service class UUID:
-
If uuid is equal to the Serial Port Profile service class ID, or
-
options["
allowedBluetoothServiceClassIds"] is present and contains uuid:-
Let port be a
SerialPortrepresenting the service on the Bluetooth device. -
Append port to allPorts.
-
-
-
-
-
For each available non-Bluetooth serial port:
-
Let port be a
SerialPortrepresenting the port. -
Append port to allPorts.
-
-
Prompt the user to grant the site access to a serial port by presenting them with a list of ports in allPorts that match any filter in options["
filters"] if present and allPorts otherwise. -
If the user does not choose a port, queue a global task on the relevant global object of this using the serial port task source to reject promise with a "
NotFoundError"DOMExceptionand abort these steps. -
Let port be a
SerialPortrepresenting the port chosen by the user. -
Queue a global task on the relevant global object of this using the serial port task source to resolve promise with port.
-
-
Return promise.
A serial port is available if it is a wired serial port and the port is physically connected to the system, or if it is a wireless serial port and the wireless device hosting the port is registered with the system.
3.1.1. SerialPortRequestOptions dictionary
dictionary SerialPortRequestOptions {sequence <SerialPortFilter >filters ;sequence <BluetoothServiceUUID >allowedBluetoothServiceClassIds ; };
filtersmember, of type sequence<SerialPortFilter>- Filters for serial ports
allowedBluetoothServiceClassIdsmember, of type sequence<BluetoothServiceUUID>-
A list of
BluetoothServiceUUIDvalues representing Bluetooth service class IDs. Bluetooth ports with custom service class IDs are excluded from the list of ports presented to the user unless the service class ID is included in this list.
3.1.2. SerialPortFilter dictionary
dictionary SerialPortFilter {unsigned short usbVendorId ;unsigned short usbProductId ;BluetoothServiceUUID bluetoothServiceClassId ; };
usbVendorIdmember, of type unsigned short- USB Vendor ID
usbProductIdmember, of type unsigned short- USB Product ID
bluetoothServiceClassIdmember, of type BluetoothServiceUUID- Bluetooth service class ID
true:
-
Let info be the result of calling port.
getInfo(). -
If filter["
bluetoothServiceClassId"] is present:-
If the serial port is not part of a Bluetooth device, return
false. -
If filter["
bluetoothServiceClassId"] is equal to info["bluetoothServiceClassId"], returntrue. -
Otherwise, return
false.
-
-
If filter["
usbVendorId"] is not present, returntrue. -
If the serial port is not part of a USB device, return
false. -
If info["
usbVendorId"] is not equal to filter["usbVendorId"], returnfalse. -
If filter["
usbProductId"] is not present, returntrue. -
If info["
usbProductId"] is not equal to filter["usbProductId"], returnfalse. -
Otherwise, return
true.
SerialPortFilter if these steps return true:
-
For each filter in the sequence, run these sub-steps:
-
If port matches the filter filter, return
true.
-
-
Return
false.
3.2. getPorts() method
// Check to see what ports are available when the page loads. document. addEventListener( 'DOMContentLoaded' , async () => { let ports= await navigator. serial. getPorts(); // Populate the UI with options for the user to select or // automatically connect to devices. }); navigator. serial. addEventListener( 'connect' , e=> { // Add |e.target| to the UI or automatically connect. }); navigator. serial. addEventListener( 'disconnect' , e=> { // Remove |e.target| from the UI. If the device was open the // disconnection can also be observed as a stream error. });
getPorts() method steps are:
-
Let promise be a new promise.
-
If this’s relevant global object’s associated Document is not allowed to use the policy-controlled feature named
"serial", reject promise with a "SecurityError"DOMExceptionand return promise. -
Run the following steps in parallel:
-
Let availablePorts be the sequence of available serial ports which the user has allowed the site to access as the result of a previous call to
requestPort(). -
Let ports be the sequence of the
SerialPorts representing the ports in availablePorts. -
Queue a global task on the relevant global object of this using the serial port task source to resolve promise with ports.
-
-
Return promise.
3.3. onconnect attribute
onconnect is an event handler IDL attribute for the
connect event type.
3.4. ondisconnect attribute
ondisconnect is an event handler IDL attribute for the
disconnect event type.
4. SerialPort interface
[Exposed =(DedicatedWorker ,Window ),SecureContext ]interface SerialPort :EventTarget {attribute EventHandler onconnect ;attribute EventHandler ondisconnect ;readonly attribute boolean connected ;readonly attribute ReadableStream ?readable ;readonly attribute WritableStream ?writable ;SerialPortInfo getInfo ();Promise <undefined >open (SerialOptions );options Promise <undefined >setSignals (optional SerialOutputSignals = {});signals Promise <SerialInputSignals >getSignals ();Promise <undefined >close ();Promise <undefined >forget (); };
Methods on this interface typically complete asynchronously, queuing work on the serial port task source.
The get the parent algorithm for SerialPort returns the same Serial
instance that is returned by the SerialPort’s relevant global object’s
Navigator object’s serial getter.
Instances of SerialPort are created with the internal slots described in the
following table:
| Internal slot | Initial value | Description (non-normative) |
|---|---|---|
[[state]]
| "closed"
| Tracks the active state of the SerialPort
|
[[bufferSize]]
| undefined | The amount of data to buffer for transmit and receive |
[[connected]]
| false
| A flag indicating the logical connection state of serial port |
[[readable]]
| null
| A ReadableStream that receives data from the port
|
[[readFatal]]
| false
| A flag indicating that the port has encountered a fatal read error |
[[writable]]
| null
| A WritableStream that transmits data to the port
|
[[writeFatal]]
| false
| A flag indicating that the port has encountered a fatal write error |
[[pendingClosePromise]]
| null
|
A Promise used to wait for readable and
writable to close
|
4.1. onconnect attribute
onconnect is an event handler IDL attribute for the
connect event type.
requestPort() becomes logically connected, run
the following steps:
-
Let port be a
SerialPortrepresenting the port. -
Set port.
[[connected]]totrue. -
Fire an event named
connectat port with itsbubblesattribute initialized totrue.
A serial port is logically connected if it is a wired serial port and the port is physically connected to the system, or if it is a wireless serial port and the system has active connections to the wireless device (e.g. an open Bluetooth L2CAP channel).
4.2. ondisconnect attribute
ondisconnect is an event handler IDL attribute for the
disconnect event type.
requestPort() is no longer logically connected, run the following steps:
-
Let port be a
SerialPortrepresenting the port. -
Set port.
[[connected]]tofalse. -
Fire an event named
disconnectat port with itsbubblesattribute initialized totrue.
4.3. getInfo() method
getInfo() method steps are:
-
Let info be an empty ordered map.
-
If the port is part of a USB device, perform the following steps:
-
Set info["
usbVendorId"] to the vendor ID of the device. -
Set info["
usbProductId"] to the product ID of the device.
-
-
If the port is a service on a Bluetooth device, perform the following steps:
-
Set info["
bluetoothServiceClassId"] to the service class UUID of the Bluetooth service.
-
-
Return info.
4.3.1. SerialPortInfo dictionary
dictionary SerialPortInfo {unsigned short usbVendorId ;unsigned short usbProductId ;BluetoothServiceUUID bluetoothServiceClassId ; };
usbVendorIdmember, of type unsigned short-
If the port is part of a USB device this member will be the 16-bit vendor ID
of that device. Otherwise it will be
undefined. usbProductIdmember, of type unsigned short-
If the port is part of a USB device this member will be the 16-bit product
ID of that device. Otherwise it will be
undefined. bluetoothServiceClassIdmember, of type BluetoothServiceUUID-
If the port is a service on a Bluetooth device this member will be a
BluetoothServiceUUIDcontaining the service class UUID. Otherwise it will beundefined.
4.4. open() method
await port. open({ baudRate: /* pick your baud rate */ });
Once open() has resolved the readable and
writable attributes can be accessed to get the
ReadableStream and WritableStream instances for receiving data from
and sending data to the connected device.
open() method steps are:
-
Let promise be a new promise.
-
If this.
[[state]]is not"closed", reject promise with an "InvalidStateError"DOMExceptionand return promise. -
If options["
baudRate"] is 0, reject promise with aTypeErrorand return promise. -
If options["
dataBits"] is not 7 or 8, reject promise with aTypeErrorand return promise. -
If options["
stopBits"] is not 1 or 2, reject promise with aTypeErrorand return promise. -
If options["
bufferSize"] is 0, reject promise with aTypeErrorand return promise. -
Optionally, if options["
bufferSize"] is larger than the implementation is able to support, reject promise with aTypeErrorand return promise. -
Perform the following steps in parallel.
-
Invoke the operating system to open the serial port using the connection parameters (or their defaults) specified in options.
-
If this fails for any reason, queue a global task on the relevant global object of this using the serial port task source to reject promise with a "
NetworkError"DOMExceptionand abort these steps. -
Set this.
[[bufferSize]]to options["bufferSize"]. -
Queue a global task on the relevant global object of this using the serial port task source to resolve promise with
undefined.
-
-
Return promise.
4.4.1. SerialOptions dictionary
dictionary SerialOptions {required [EnforceRange ]unsigned long baudRate ; [EnforceRange ]octet dataBits = 8; [EnforceRange ]octet stopBits = 1;ParityType parity = "none"; [EnforceRange ]unsigned long bufferSize = 255;FlowControlType flowControl = "none"; };
baudRatemember, of type unsigned long-
A positive, non-zero value indicating the baud rate at which serial
communication should be established.
Note:
baudRateis the only required member of this dictionary. While there are common default for other connection parameters it is important for developers to consider and consult with the documentation for devices they intend to connect to determine the correct values. While some values are common there is no standard baud rate. Requiring this parameter reduces the potential for confusion if an arbitrary default were chosen by this specification. dataBitsmember, of type octet, defaulting to8- The number of data bits per frame. Either 7 or 8.
stopBitsmember, of type octet, defaulting to1- The number of stop bits at the end of a frame. Either 1 or 2.
paritymember, of type ParityType, defaulting to"none"- The parity mode.
bufferSizemember, of type unsigned long, defaulting to255- A positive, non-zero value indicating the size of the read and write buffers that should be created.
flowControlmember, of type FlowControlType, defaulting to"none"- The flow control mode.
4.4.1.1. ParityType enum
enum ParityType {"none" ,"even" ,"odd" };
none- No parity bit is sent for each data word.
even- Data word plus parity bit has even parity.
odd- Data word plus parity bit has odd parity.
4.4.1.2. FlowControlType enum
enum FlowControlType {"none" ,"hardware" };
none- No flow control is enabled.
hardware- Hardware flow control using the RTS and CTS signals is enabled.
4.5. connected attribute
connected getter steps are:
-
Return this.
[[connected]].
4.6. readable attribute
while ( port. readable) { const reader= port. readable. getReader(); try { while ( true ) { const { value, done} = await reader. read(); if ( done) { // |reader| has been canceled. break ; } // Do something with |value|... } } catch ( error) { // Handle |error|... } finally { reader. releaseLock(); } }
The inner loop will read chunks of data from the port until an error is
encountered, at which point the code in the "catch" block will be executed.
The outer loop handles recoverable errors such as parity check failures by
opening a new reader. Fatal errors will cause readable to
become null and the loop to end.
As long as the serial port is open it can continue to produce data and the
amount of data in each of the chunks returned by
read() will be essentially arbitrary based on
the timing of when it is called. It is up to the device and the code
communicating with it to decide what constitutes a complete message. For
example, a device might communicate with the host using ASCII-formatted text
where each message ends with a newline (or the sequence "\r\n"). A pipeline
of TransformStreams can be used to automatically convert the
Uint8Array chunks provided by readable into DOMStrings
containing an entire line of text each.
class LineBreakTransformer{ constructor () { this . container= '' ; } transform( chunk, controller) { this . container+= chunk; const lines= this . container. split( '\r\n' ); this . container= lines. pop(); lines. forEach( line=> controller. enqueue( line)); } flush( controller) { controller. enqueue( this . container); } } const decoder= new TextDecoderStream(); const streamClosed= port. readable. pipeTo( decoder. writable); const lineReader= decoder. readable. pipeThrough( new TransformStream( new LineBreakTransformer())) . getReader();
As in Example 7 the pipe chain cannot be
constructed using only pipeThrough(). It is necessary to use pipeTo() when
attaching the first TransformStream to the SerialPort so that you can
wait for the pipe chain to be closed when you want to close the port.
lineReader. cancel(); await streamClosed; await port. close();
Some other ways of encoding message boundaries are to prefix each message with
its length or to wait a defined length of time before transmitting the next
message. Implementing a TransformStream for these types of message
boundaries is left as an exercise for the reader.
While the read() method is asynchronous and
does not block execution, in code using async/await syntax it can seem as if
it does. In this situation it might be helpful to implement a timeout which will
allow the code to continue execution if no data is received for a period of
time. The example below uses the releaseLock()
method to interrupt a call to read() after a
timer expires. This will not close the stream and so any data received after
the timeout can still be read later after calling
getReader() again.
async function readWithTimeout( port, timeout) { const reader= port. readable. getReader(); const timer= setTimeout(() => { reader. releaseLock(); }, timeout); const result= await reader. read(); clearTimeout( timer); reader. releaseLock(); return result; }
This feature of releaseLock() was added in
whatwg/streams#1168
and has only recently been implemented by browsers.
readable getter steps are:
-
If this.
[[readable]]is notnull, return this.[[readable]]. -
If this.
[[readFatal]]istrue, returnnull. -
Let stream be a new
ReadableStream. -
Let pullAlgorithm be the following steps:
-
Let desiredSize be the desired size to fill up to the high water mark for this.
[[readable]]. -
If this.
[[readable]]’s current BYOB request view is non-null, then set desiredSize to this.[[readable]]’s current BYOB request view’s byte length. -
Let promise be a new promise.
-
Run the following steps in parallel:
-
Invoke the operating system to read up to desiredSize bytes from the port, putting the result in the byte sequence bytes.
Note: this.
[[state]]becoming"forgotten"can be treated as if the port was disconnected. -
Queue a global task on the relevant global object of this using the serial port task source to run the following steps:
-
If no errors were encountered, then:
-
If this.
[[readable]]’s current BYOB request view is non-null, then write bytes into this.[[readable]]’s current BYOB request view, and set view to this.[[readable]]’s current BYOB request view. -
Otherwise, set view to the result of creating a
Uint8Arrayfrom bytes in this’s relevant Realm. -
Enqueue view into this.
[[readable]]. -
Resolve promise with
undefined.
-
-
If a buffer overrun condition was encountered, invoke error on this.
[[readable]]with a "BufferOverrunError"DOMExceptionand invoke the steps to handle closing the readable stream. -
If a break condition was encountered, invoke error on this.
[[readable]]with a "BreakError"DOMExceptionand invoke the steps to handle closing the readable stream. -
If a framing error was encountered, invoke error on this.
[[readable]]with a "FramingError"DOMExceptionand invoke the steps to handle closing the readable stream. -
If a parity error was encountered, invoke error on this.
[[readable]]with a "ParityError"DOMExceptionand invoke the steps to handle closing the readable stream. -
If an operating system error was encountered, invoke error on this.
[[readable]]with an "UnknownError"DOMExceptionand invoke the steps to handle closing the readable stream. -
If the port was disconnected, run the following steps:
-
Set this.
[[readFatal]]totrue, -
Invoke error on this.
[[readable]]with a "NetworkError"DOMException. -
Invoke the steps to handle closing the readable stream.
-
-
-
-
Return promise.
-
-
Let cancelAlgorithm be the following steps:
-
Let promise be a new promise.
-
Run the following steps in parallel.
-
Invoke the operating system to discard the contents of all software and hardware receive buffers for the port.
-
Queue a global task on the relevant global object of this using the serial port task source to run the following steps:
-
Invoke the steps to handle closing the readable stream.
-
Resolve promise with
undefined.
-
-
-
Return promise.
-
-
Set up with byte reading support stream with pullAlgorithm set to pullAlgorithm, cancelAlgorithm set to cancelAlgorithm, and highWaterMark set to this.
[[bufferSize]]. -
Set this.
[[readable]]to stream. -
Return stream.
-
Set this.
[[readable]]tonull. -
If this.
[[writable]]isnulland this.[[pendingClosePromise]]is notnull, resolve this.[[pendingClosePromise]]withundefined.
4.7. writable attribute
WritableStreamDefaultWriter can be created and released as necessary. This
example uses a TextEncoder to encode a DOMString as the necessary
Uint8Array for transmission.
const encoder= new TextEncoder(); const writer= port. writable. getWriter(); await writer. write( encoder. encode( "PING" )); writer. releaseLock();
When writing larger chunks it can be important to allow the port to apply back
pressure so that the serial transmitter does not get too far behind sending
data generated by the application. The
write() method returns a Promise which
resolves when data has been written. While having some data available in the
transmit buffer is important to maintain good throughput awaiting this
Promise before generating too many chunks of data is a good practice to
avoid excessive buffering.
writable getter steps are:
-
If this.
[[writable]]is notnull, return this.[[writable]]. -
If this.
[[writeFatal]]istrue, returnnull. -
Let stream be a new
WritableStream. -
Let signal be stream’s signal.
-
Let writeAlgorithm be the following steps, given chunk:
-
Let promise be a new promise.
-
Assert: signal is not aborted.
-
If chunk cannot be converted to an IDL value of type
BufferSource, reject promise with aTypeErrorand return promise. Otherwise, save the result of the conversion in source. -
Get a copy of the buffer source source and save the result in bytes.
-
In parallel, run the following steps:
-
Invoke the operating system to write bytes to the port. Alternately, store the chunk for future coalescing.
Note: The operating system can return from this operation once bytes has been queued for transmission rather than after it has been transmitted.
Note: this.
[[state]]becoming"forgotten"can be treated as if the port was disconnected. -
Queue a global task on the relevant global object of this using the serial port task source to run the following steps:
-
If the chunk was successfully written, or was stored for future coalescing, resolve promise with
undefined.Note: [STREAMS] specifies that writeAlgorithm will only be invoked after the
Promisereturned by a previous invocation of this algorithm has resolved. For efficiency an implementation is allowed to resolve thisPromiseearly in order to coalesce multiple chunks waiting in theWritableStream’s internal queue into a single request to the operating system. -
If an operating system error was encountered, reject promise with an "
UnknownError"DOMException. -
If the port was disconnected, run the following steps:
-
Set this.
[[writeFatal]]totrue. -
Reject promise with a "
NetworkError"DOMException. -
Invoke the steps to handle closing the writable stream.
-
-
If signal is aborted, reject promise with signal’s abort reason.
-
-
-
Return promise.
-
-
Let abortAlgorithm be the following steps:
-
Let promise be a new promise.
-
Run the following steps in parallel.
-
Invoke the operating system to discard the contents of all software and hardware transmit buffers for the port.
-
Queue a global task on the relevant global object of this using the serial port task source to run the following steps:
-
Invoke the steps to handle closing the writable stream.
-
Resolve promise with
undefined.
-
-
-
Return promise.
-
-
Let closeAlgorithm be the following steps:
-
Let promise be a new promise.
-
Run the following steps in parallel.
-
Invoke the operating system to flush the contents of all software and hardware transmit buffers for the port.
-
Queue a global task on the relevant global object of this using the serial port task source to run the following steps:
-
Invoke the steps to handle closing the writable stream.
-
If signal is aborted, reject promise with signal’s abort reason.
-
Otherwise, resolve promise with
undefined.
-
-
-
Return promise.
-
-
Set up stream with writeAlgorithm set to writeAlgorithm, abortAlgorithm set to abortAlgorithm, closeAlgorithm set to closeAlgorithm, highWaterMark set to this.
[[bufferSize]], and sizeAlgorithm set to a byte-counting size algorithm. -
Add the following abort steps to signal:
-
Cause any invocation of the operating system to write to the port to return as soon as possible no matter how much data has been written.
-
-
Set this.
[[writable]]to stream. -
Return stream.
-
Set this.
[[writable]]tonull. -
If this.
[[readable]]isnulland this.[[pendingClosePromise]]is notnull, resolve this.[[pendingClosePromise]]withundefined.
4.8. setSignals() method
await port. setSignals({ dataTerminalReady: false }); await new Promise( resolve=> setTimeout( resolve, 200 )); await port. setSignals({ dataTerminalReady: true });
setSignals() method steps are:
-
Let promise be a new promise.
-
If this.
[[state]]is not"opened", reject promise with an "InvalidStateError"DOMExceptionand return promise. -
If all of the specified members of signals are not present reject promise with a
TypeErrorand return promise. -
Perform the following steps in parallel:
Note: Ideally the changes specified in signals would be applied atomically however this is not supported by either the POSIX or Windows APIs user agents will use to implement these steps. Therefore the ordering given below is likely to be relied upon by applications.
-
If signals["
dataTerminalReady"] is present, invoke the operating system to either assert (iftrue) or deassert (iffalse) the "data terminal ready" or "DTR" signal on the serial port. -
If signals["
requestToSend"] is present, invoke the operating system to either assert (iftrue) or deassert (iffalse) the "request to send" or "RTS" signal on the serial port. -
If signals["
break"] is present, invoke the operating system to either assert (iftrue) or deassert (iffalse) the "break" signal on the serial port.Note: The "break" signal is typically implemented as an in-band signal by holding the transmit line at the "mark" voltage and thus prevents data transmission for as long as it remains asserted.
-
If the operating system fails to change the state of any of these signals for any reason, queue a global task on the relevant global object of this using the serial port task source to reject promise with a "
NetworkError"DOMException. -
Queue a global task on the relevant global object of this using the serial port task source to resolve promise with
undefined.
-
-
Return promise.
4.8.1. SerialOutputSignals dictionary
dictionary SerialOutputSignals {boolean dataTerminalReady ;boolean requestToSend ;boolean break ; };
dataTerminalReady, of type boolean- Data Terminal Ready (DTR)
requestToSend, of type boolean- Request To Send (RTS)
break, of type boolean- Break
4.9. getSignals() method
getSignals() method steps are:
-
Let promise be a new promise.
-
If this.
[[state]]is not"opened", reject promise with an "InvalidStateError"DOMExceptionand return promise. -
Perform the following steps in parallel:
-
Query the operating system for the status of the control signals that may be asserted by the device connected to the serial port.
-
If the operating system fails to determine the status of these signals for any reason, queue a global task on the relevant global object of this using the serial port task source to reject promise with a "
NetworkError"DOMExceptionand abort these steps. -
Let dataCarrierDetect be
trueif the "data carrier detect" or "DCD" signal has been asserted by the device, andfalseotherwise. -
Let clearToSend be
trueif the "clear to send" or "CTS" signal has been asserted by the device, andfalseotherwise. -
Let ringIndicator be
trueif the "ring indicator" or "RI" signal has been asserted by the device, andfalseotherwise. -
Let dataSetReady be
trueif the "data set ready" or "DSR" signal has been asserted by the device, andfalseotherwise. -
Let signals be the ordered map «[ "
dataCarrierDetect" → dataCarrierDetect, "clearToSend" → clearToSend, "ringIndicator" → ringIndicator, "dataSetReady" → dataSetReady ]». -
Queue a global task on the relevant global object of this using the serial port task source to resolve promise with signals.
-
-
Return promise.
4.9.1. SerialInputSignals dictionary
dictionary SerialInputSignals {required boolean dataCarrierDetect ;required boolean clearToSend ;required boolean ringIndicator ;required boolean dataSetReady ; };
dataCarrierDetectmember, of type boolean- Data Carrier Detect (DCD)
clearToSendmember, of type boolean- Clear To Send (CTS)
ringIndicatormember, of type boolean- Ring Indicator (RI)
dataSetReadymember, of type boolean- Data Set Ready (DSR)
4.10. close() method
Calling port.close() implicitly invokes
port.readable.cancel() and
port.writable.abort() in order to clear
any buffered data. If the application has called
port.readable.getReader() or
port.writable.getWriter() the stream is
locked and the port cannot be closed. This forces the developer to decide how
to handle any read or write operations that are in progress. For example, to
ensure that all buffered data has been transmitted before the port is closed
the application has to await the Promise returned by
writer.close().
const encoder= new TextEncoder(); const writer= port. writable. getWriter(); writer. write( encoder. encode( "A long message that will take..." )); await writer. close(); await port. close();
To discard any unsent data the application could instead call
writer.abort().
If a TransformStream is being piped to port.writable then
waiting for the Promise returned by
writer.close() to resolve is insufficient.
The application has to wait for the pipe chain to close by waiting for the
Promise returned by pipeTo() to resolve instead.
const encoder= new TextEncoderStream(); const writableStreamClosed= encoder. readable. pipeTo( port. writable); const writer= encoder. writable. getWriter(); writer. write( "A long message that will take..." ); writer. close(); await writableStreamClosed; await port. close();
If a loop is being used to read chunks from the port, as is done in
Example 4, then it has to be exited before
calling port.close().
let keepReading= true ; let reader; async function readUntilClosed() { while ( port. readable&& keepReading) { reader= port. readable. getReader(); try { while ( true ) { const { value, done} = await reader. read(); if ( done) { // |reader| has been canceled. break ; } // Do something with |value|... } } catch ( error) { // Handle |error|... } finally { reader. releaseLock(); } } await port. close(); } const closed= readUntilClosed(); // Sometime later... keepReading= false ; reader. cancel(); await closed;
Calling reader.cancel() causes the call to
reader.read() to return immediately, exiting
the inner loop and calling
reader.releaseLock(). The outer loop then
exits because keepReading has been set to false and with the stream
unlocked port.close() can complete successfully.
While it is also possible to call port.close() immediately
after awaiting the Promise returned by
reader.cancel() it is better to place the
call to port.close() as the last step of readUntilClosed()
so that the port is also closed when a fatal error is encountered and
port.readable becomes null.
close() method steps are:
-
Let promise be a new promise.
-
If this.
[[state]]is not"opened", reject promise with an "InvalidStateError"DOMExceptionand return promise. -
Let cancelPromise be the result of invoking cancel on this.
[[readable]]or a promise resolved withundefinedif this.[[readable]]isnull. -
Let abortPromise be the result of invoking abort on this.
[[writable]]or a promise resolved withundefinedif this.[[writable]]isnull. -
Let pendingClosePromise be a new promise.
-
If this.
[[readable]]and this.[[writable]]arenull, resolve pendingClosePromise withundefined. -
Set this.
[[pendingClosePromise]]to pendingClosePromise. -
Let combinedPromise be the result of getting a promise to wait for all with «cancelPromise, abortPromise, pendingClosePromise».
-
React to combinedPromise.
-
If combinedPromise was fulfilled, then:
-
Run the following steps in parallel:
-
Invoke the operating system to close the serial port and release any associated resources.
-
Set this.
[[readFatal]]and this.[[writeFatal]]tofalse. -
Set this.
[[pendingClosePromise]]tonull. -
Queue a global task on the relevant global object of this using the serial port task source to resolve promise with
undefined.
-
-
-
If combinedPromise was rejected with reason r, then:
-
Set this.
[[pendingClosePromise]]tonull. -
Queue a global task on the relevant global object of this using the serial port task source to reject promise with r.
-
-
-
Return promise.
4.11. forget() method
// Request a serial port. const port= await navigator. serial. requestPort(); // Then later... revoke permission to the serial port. await port. forget();
forget() method steps are:
-
If the user agent can’t perform this action (e.g. permission was granted by administrator policy), return a promise resolved with
undefined. -
Run the following steps in parallel:
-
Remove this from the sequence of serial ports on the system which the user has allowed the site to access as the result of a previous call to
requestPort(). -
Queue a global task on the relevant global object of this using the serial port task source to resolve promise with
undefined.
-
Return promise.
5. Blocklist
This specification relies on a blocklist file in the https://github.com/whatwg/serial repository to restrict the set of ports a website can access.
The result of parsing the Bluetooth service class ID blocklist at a
URL url is a list of UUID values representing custom
service IDs.
The Serial Port Profile service class ID is a
BluetoothServiceUUID with value "00001101-0000-1000-8000-00805f9b34fb".
BluetoothServiceUUID serviceUuid is a blocked Bluetooth service
class UUID if the following steps return true:
-
Let uuid be the result of calling
BluetoothUUID.getService()with serviceUuid. -
Let blocklist be the result of parsing the Bluetooth service class ID blocklist at https://github.com/whatwg/serial/blob/main/bluetooth-service-blocklist.txt.
-
If blocklist contains uuid, return
true. -
If uuid is the Serial Port Profile service class ID, return
false. -
If uuid ends with "
-0000-1000-8000-00805f9b34fb", returntrue. -
Otherwise, return
false.
6. Integrations
6.1. Permissions Policy
This specification defines a feature that controls whether the methods exposed
by the serial attribute on the Navigator object may be used.
The feature name for this feature is "serial".
The default allowlist for this feature is
'self'.
7. Security considerations
This section is non-normative.
This API poses similar a security risk to [WEBBLUETOOTH] and [WEBUSB] and so lessons from those are applicable here. The primary threats are:
-
A malicious site that has tricked the user into granting it access to a device using the device’s intended capabilities for malicious purposes. For example, a robot causing physical damage.
-
A malicious site that has tricked the user into granting it access to a device installing its own firmware into the device in order to modify the device’s intended capabilities for malicious purposes or to attack the host to which it is connected. For example, triggering a buffer overflow in other host software which communicates with the device.
-
Malicious code injected into a trusted site which has been granted access to the device doing any of the above. For example, an online firmware update utility being hacked to deliver malicious firmware.
-
An attacker convincing the user to connect a malicious device to their system which colludes with a malicious or exploited site to create a web-based channel for communicating back to the attacker.
The primary mitigation to all of these attacks is the requestPort()
pattern, which requires user interaction and only supports granting access to a
single device at a time. This prevents drive-by attacks because a site cannot
enumerate all connected devices to determine whether a vulnerable device exists
and must instead proactively inform the user that it desires access.
Implementations may also provide a visual indication that a site is currently
communicating with a device and controls for revoking that permission at any
time.
This specification requires the site to be served from a secure context in order to prevent malicious code from being injected by a network-based attacker. This ensures that the site identity shown to the user when making permission decisions is accurate. This specification also requires top-level documents to opt-in through [PERMISSIONS-POLICY] before allowing a cross-origin iframe to use the API. When combined with [CSP3] these mechanisms provide protection against malicious code injection attacks.
The remaining concern is the exploitation of a connected device through a phishing attack that convinces the user to grant a malicious site access to a device. These attacks can be used to either exploit the device’s capabilities as designed or to install malicious firmware on the device that will in turn attack the host computer. Host software may be vulnerable to attack because it improperly validates input from connected devices. Security research in this area has encouraged software vendors to treat connected devices as untrustworthy.
There is no mechanism that will completely prevent this type of attack because data sent from a page to the device is an opaque sequence of bytes. Efforts to block a particular type of data from being sent are likely be met by workarounds on the part of device manufacturers who nevertheless want to send this type of data to their devices.
User agents can implement additional mechanisms to control access to devices:
-
A setting which prevents sites from calling
requestPort()unless added to an explicit allow list.Systems administrators could apply such a setting across their managed fleet using enterprise policy controls. Such controls may allow the administrator to automatically grant selected sites access to particular devices and no others.
-
A list of device IDs for hardware which is known to be exploitable could be deployed with the user agent. Connections to listed devices would be blocked. An implementation could use its automatic update or experiment management system to deploy updates to this list on the fly to block an active attack.
Implementations of [WEBBLUETOOTH] and [WEBUSB] have experimented with these mitigations however there are limits to their effectiveness. First, it is difficult to define whether a device is exploitable. For example, this API will allow a site to upload firmware to a microcontroller development board. This is a key use case for this API as these devices are common in the educational and hobbyist markets. These boards do not implement firmware signature verification and so can easily be turned into a malicious device. These boards are clearly exploitable but should not be blocked.
In addition, maintaining a list of vulnerable devices works well for USB and Bluetooth because those protocols define out-of-band mechanisms to gather device metadata. The make and model of such devices can thus be easily identified even if they present themselves to the host as a virtual serial ports. However, there are generic USB- or Bluetooth-to-serial adapters as well as systems with "real" serial ports using a DB-25, DE-9 or RJ-45 connector. For these there is no metadata that can be read to determine the identity of the device connected to the port and so blocking access to these is not possible.
8. Privacy considerations
This section is non-normative.
Serial ports and serial devices contain two kinds of sensitive information. When a port is a USB or Bluetooth device there are identifiers such as the vendor and product IDs (which identify the make and model) as well as a serial number or MAC address. The serial device itself may also have its own identifier that is available through commands sent via the serial port. The device may also store other private information which may or may not be identifying.
In order to manage device permissions an implementation will likely store device identifiers such as the USB vendor ID, product ID and serial number in its user preferences file to be used as stable identifiers for devices the user has granted sites access to. These would not be shared directly with sites and would be cleared when permission is revoked or site data in general is cleared.
Commands a page can send to the device after it has been granted access a page may also be able to access any of the other sensitive information stored by the device. For the reasons mentioned in § 7 Security considerations it is impractical and undesirable to attempt to prevent a page from accessing this information.
Implementations should provide users with complete control over which devices a
site can access and not grant device access without user interaction. This is
the intention of the requestPort() method. This prevents a site from
silently enumerating and collecting data from all connected devices. This is
similar to the file picker UI. A site has no knowledge of the filesystem, only
the files or directories that have been chosen by the user. An implementation
could notify the user when a site is using these permissions with an indicator
icon appearing in the tab or address bar.
Implementations that provide a "private" or "incognito" browsing mode should ensure that permissions from the user’s normal profile do not carry over to such a session and permissions granted in this session are not persisted when the session ends. An implementation may warn the user when granting access to a device in such as session as, similar to entering identifying information by hand, device identifiers and other unique properties available from communicating with the device mentioned previously can be used to identify the user between sessions.
Users may be surprised by the capabilities granted by this API if they do not understand the ways in which granting access to a device breaks traditional isolation boundaries in the web security model. Security UI and documentation should explain that granting a site access to a device could give the site full control over the device and any data contained within.
Acknowledgments
Thanks to Anatol Ulrich, Chris Mumford, Clément Menard, Domenic Denicola, Dominique Hazael-Massieux, Florian Loitsch, Florian Scholz, Francis Gulotta, François Beaufort, Jack Hsieh, Keavon Chambers, Kenneth Rohde Christiansen, Marcos Cáceres, Matt Reynolds, Jesse Melhuish, Michael Kohler, Ms2ger, Rick Waldron, Sankha Narayan Guria, Simon Pieters, Suz Hinton, Travis Leithead, and Vincent Scheib for being awesome.
This standard is written by Reilly Grant (Google, reillyg@google.com).
Intellectual property rights
Copyright © WHATWG (Apple, Google, Mozilla, Microsoft). This work is licensed under a Creative Commons Attribution 4.0 International License. To the extent portions of it are incorporated into source code, such portions in the source code are licensed under the BSD 3-Clause License instead.