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 { [EnforceRange ]required 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
TransformStream
s
can
be
used
to
automatically
convert
the
Uint8Array
chunks
provided
by
readable
into
DOMString
s
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.