シリアルポートとの読み書き
WebシリアルAPIを使用すると、Webサイトはシリアルデバイスと通信できます。
機能プロジェクトの一部であるWebSerial APIは、Chrome89でリリースされました。
WebシリアルAPIとは何ですか?
シリアルポートは、データをバイト単位で送受信できる双方向通信インターフェースです。
WebシリアルAPIは、WebサイトがJavaScriptを使用してシリアルデバイスとの間で読み取りおよび書き込みを行うための方法を提供します。シリアルデバイスは、ユーザーのシステムのシリアルポートを介して、またはシリアルポートをエミュレートする取り外し可能なUSBおよびBluetoothデバイスを介して接続されます。
言い換えると、WebシリアルAPIは、Webサイトがマイクロコントローラーや3Dプリンターなどのシリアルデバイスと通信できるようにすることで、Webと物理的な世界を橋渡しします。
オペレーティングシステムは、一部のシリアルポートとの通信において、アプリケーションの低レベルのUSB APIではなく高レベルのシリアルAPIを使用する必要があるため、このAPIはWebUSBの優れたコンパニオンでもあります。
推奨されるユースケース
教育、ホビイスト、および産業部門では、ユーザーは周辺機器をコンピューターに接続します。これらのデバイスは、多くの場合、カスタムソフトウェアで使用されるシリアル接続を介してマイクロコントローラーによって制御されます。これらのデバイスを制御するためのいくつかのカスタムソフトウェアは、Webテクノロジーで構築されています。
一部のケースでは、Webサイトは、ユーザーが手動でインストールしたエージェントアプリケーションを介してデバイスと通信します。また他のケースでは、アプリケーションは、Electronなどのフレームワークを介してパッケージ化されたアプリケーションで提供される場合もあります。さらに他のケースでは、コンパイルされたアプリケーションをUSBフラッシュドライブを介してデバイスにコピーするなど、追加の手順を実行する必要ももあります。
これらすべての場合において、ユーザーエクスペリエンスは、Webサイトとそれが制御するデバイスとの間にダイレクトな通信を提供することによって改善されます。
現在のステータス
WebシリアルAPIの使用
機能検出
WebシリアルAPIがサポートされているかどうかを確認するには、次を使用します。
if ("serial" in navigator) {
// The Web Serial API is supported.
}
シリアルポートを開く
WebシリアルAPIは、設計上、非同期型です。このため、入力を待機するときにWebサイトのUIがブロックされるのが防止されます。これは、シリアルデータの受信がいつでも行われる可能性があり、それをリスンする方法が必要になるため重要です。
シリアルポートを開くには、最初にSerialPort
オブジェクトにアクセスします。このためには、タッチやマウスクリックなどのユーザージェスチャに応答して navigator.serial.requestPort()
を呼び出して単一のシリアルポートを選択するか、Webサイトがアクセスを付与するシリアルポートのリストを返すnavigator.serial.getPorts()
から1つ選択するようにユーザーにプロンプトできます。
document.querySelector('button').addEventListener('click', async () => {
// Prompt user to select any serial port.
const port = await navigator.serial.requestPort();
});
// Get all serial ports the user has previously granted the website access to.
const ports = await navigator.serial.getPorts();
navigator.serial.requestPort()
関数は、フィルターを定義するオブジェクトリテラルを任意に取ります。これらは、USB経由で接続されている任意のシリアルデバイスを必須のUSBベンダー(usbVendorId
)とオプションのUSB製品識別子( usbProductId
)に照合するために使用されます。
// Filter on devices with the Arduino Uno USB Vendor/Product IDs.
const filters = [
{ usbVendorId: 0x2341, usbProductId: 0x0043 },
{ usbVendorId: 0x2341, usbProductId: 0x0001 }
];
// Prompt user to select an Arduino Uno device.
const port = await navigator.serial.requestPort({ filters });
const { usbProductId, usbVendorId } = port.getInfo();
requestPort()
呼び出すと、ユーザーはデバイスを選択するように求められ、SerialPort
オブジェクトが返されます。SerialPort
オブジェクトを取得したら、目的のボーレートでport.open()
を呼び出すことで、シリアルポートが開きます。baudRate
ディクショナリメンバーは、データがシリアル回線を介して送信される速度を指定します。これは、ビット/秒(bps)の単位で表されます。これが正しく指定されていない場合、送受信するすべてのデータが意味のないものになるため、デバイスのドキュメントが正しい値で作成されていることを確認してください。シリアルポートをエミュレートする一部のUSBおよびBluetoothデバイスでは、この値はエミュレーションによって無視されるため、任意の値に安全に設定できます。
// Prompt user to select any serial port.
const port = await navigator.serial.requestPort();
// Wait for the serial port to open.
await port.open({ baudRate: 9600 });
シリアルポートを開くときに、以下のオプションのいずれかを指定することもできます。これらのオプションは任意であり、便利なデフォルト値を使用できます。
dataBits
: フレームあたりのデータビット数(7または8)。stopBits
: フレームの終わりのストップビットの数(1または2)。parity
: パリティモード("none"
、"even"
、"odd"
のいずれか)。bufferSize
: 作成する必要のある読み取りおよび書き込みバッファーのサイズ(16MB未満である必要があります)。flowControl
: フロー制御モード("none"
または"hardware"
のいずれか)。
シリアルポートからの読み取り
WebシリアルAPIの入力ストリームと出力ストリームは、Streams APIによって処理されます。
ストリームに詳しくない場合は、Streams APIの概念を確認してください。この記事では、ストリームとストリーム処理についてあまり詳しく説明していません。
シリアルポート接続が確立された後、SerialPort
オブジェクトのreadable
およびwritable
プロパティはReadableStreamとWritableStreamを返します。これらは、シリアルデバイスとの間でデータを送受信するために使用されます。どちらもデータ転送にUint8Array
インスタンスを使用します。
シリアルデバイスから新しいデータが届くと、 port.readable.getReader().read()
は、value
とdone
ブール型の2つのプロパティを非同期的に返します。done
がtrueである場合、シリアルポートが閉じられているか、データが入力されていません。port.readable.getReader()
を呼び出すと、リーダーが作成され、readable
にロックされます。readable
がロックされている間は、シリアルポートを閉じることはできません。
const reader = port.readable.getReader();
// Listen to data coming from the serial device.
while (true) {
const { value, done } = await reader.read();
if (done) {
// Allow the serial port to be closed later.
reader.releaseLock();
break;
}
// value is a Uint8Array.
console.log(value);
}
一部の重大ではないシリアルポート読み取りのエラーは、バッファオーバーフロー、フレーミングエラー、パリティエラーなどの特定の条件で発生する可能性があります。これらは例該当してスローされ、port.readable
をチェックする前のループの上に別のループを追加することでキャッチできます。これは、エラーが重大でない限り、新しいReadableStreamが自動的に作成されるため機能します。シリアルデバイスが取り外されるなどの重大なエラーが発生した場合、port.readable
はnullになります。
while (port.readable) {
const reader = port.readable.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
// Allow the serial port to be closed later.
reader.releaseLock();
break;
}
if (value) {
console.log(value);
}
}
} catch (error) {
// TODO: Handle non-fatal read error.
}
}
シリアルデバイスがテキストを送り返す場合は、以下に示すようにTextDecoderStream
を介してport.readable
を通信(パイプ)します。 TextDecoderStream
は、すべてのUint8Array
チャンクを取得して文字列に変換する変換ストリームです。
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable.getReader();
// Listen to data coming from the serial device.
while (true) {
const { value, done } = await reader.read();
if (done) {
// Allow the serial port to be closed later.
reader.releaseLock();
break;
}
// value is a string.
console.log(value);
}
シリアルポートへの書き込み
シリアルデバイスにデータを送信するには、データを port.writable.getWriter().write()
に渡します。後でシリアルポートを閉じるには、port.writable.getWriter()
でreleaseLock()
を呼び出す必要があります。
const writer = port.writable.getWriter();
const data = new Uint8Array([104, 101, 108, 108, 111]); // hello
await writer.write(data);
// Allow the serial port to be closed later.
writer.releaseLock();
以下に示すように、port.writable
にパイプされたTextEncoderStream
を介してデバイスにテキストを送信します。
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
const writer = textEncoder.writable.getWriter();
await writer.write("hello");
シリアルポートを閉じる
readable
およびwritable
メンバーのロックが解除されている場合、つまり、 releaseLock()
がそれぞれのリーダーとライター用に呼び出されている場合、port.close()
はシリアルポートを閉じます。
await port.close();
ただし、ループを使用してシリアルデバイスからデータを継続的に読み取る場合、 port.readable
は、エラーが発生するまで常にロックされます。この場合、reader.cancel()
を呼び出すと、reader.read()
が直ちに { value: undefined, done: true }
で解決することが強制されるため、ループがreader.releaseLock()
を呼び出せるようになります。
// Without transform streams.
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.cancel() has been called.
break;
}
// value is a Uint8Array.
console.log(value);
}
} catch (error) {
// Handle error...
} finally {
// Allow the serial port to be closed later.
reader.releaseLock();
}
}
await port.close();
}
const closedPromise = readUntilClosed();
document.querySelector('button').addEventListener('click', async () => {
// User clicked a button to close the serial port.
keepReading = false;
// Force reader.read() to resolve immediately and subsequently
// call reader.releaseLock() in the loop example above.
reader.cancel();
await closedPromise;
});
変換ストリーム(TextDecoderStream
やTextEncoderStream
など)を使用すると、シリアルポートを閉じるのがより複雑になります。以前と同じようにreader.cancel()
を呼び出してから、 writer.close()
とport.close()
を呼び出すと、変換ストリームを介して基礎のシリアルポートにエラーが伝播されます。エラーの伝播はすぐには発生しないため、port.readable
とport.writable
がロック解除されていることを検出するために前に作成したreadableStreamClosed
とwritableStreamClosed
プロミスを使用する必要があります。reader
をキャンセルすると、ストリームが中止されてしまうため、これが、生成されるエラーをキャッチして無視する必要がある理由です。
// With transform streams.
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable.getReader();
// Listen to data coming from the serial device.
while (true) {
const { value, done } = await reader.read();
if (done) {
reader.releaseLock();
break;
}
// value is a string.
console.log(value);
}
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
reader.cancel();
await readableStreamClosed.catch(() => { /* Ignore the error */ });
writer.close();
await writableStreamClosed;
await port.close();
接続と切断をリスンする
シリアルポートがUSBデバイスによって提供されている場合、そのデバイスはシステムに接続されている可能性もあれば、切断されている可能性もあります。Webサイトに、シリアルポートにアクセスするための権限を付与されている場合、connect
とdisconnect
のイベントはWebサイトによって監視されています。
navigator.serial.addEventListener("connect", (event) => {
// TODO: Automatically open event.target or warn user a port is available.
});
navigator.serial.addEventListener("disconnect", (event) => {
// TODO: Remove |event.target| from the UI.
// If the serial port was opened, a stream error would be observed as well.
});
Chrome 89の前は、connect
イベントとdisconnect
イベントによって、port
属性として使用できる影響のあるSerialPort
インターフェースとともに、カスタムSerialConnectionEvent
オブジェクトが起動されていました。この移行に対応するには、event.port || event.target
を使用することをお勧めします。
信号を処理する
シリアルポート接続を確立したら、デバイスの検出とフロー制御のためにシリアルポートが公開する信号を明示的にクエリして設定することができます。これらの信号はブール値として定義されます。たとえば、Arduinoなどのデバイスは、データ端末レディ(DTR)信号が切り替えられるとプログラミングモードになります。
出力信号の設定と入力信号の取得はそれぞれport.setSignals()
とport.getSignals()
の呼び出しによって行われます。以下の使用例を参照してください。
// Turn off Serial Break signal.
await port.setSignals({ break: false });
// Turn on Data Terminal Ready (DTR) signal.
await port.setSignals({ dataTerminalReady: true });
// Turn off Request To Send (RTS) signal.
await port.setSignals({ requestToSend: false });
const signals = await port.getSignals();
console.log(`Clear To Send: ${signals.clearToSend}`);
console.log(`Data Carrier Detect: ${signals.dataCarrierDetect}`);
console.log(`Data Set Ready: ${signals.dataSetReady}`);
console.log(`Ring Indicator: ${signals.ringIndicator}`);
ストリームの変換
シリアルデバイスからデータを受信する場合、必ずしもすべてのデータを一度に取得するとは限らず、任意にチャンク化される可能性があります。詳細については、 Streams APIの概念を参照してください。
これに対処するには、TextDecoderStream
などの組み込みの変換ストリームを使用するか、着信ストリームを解析して解析されたデータを返すことができる独自の変換ストリームを作成することができます。変換ストリームは、シリアルデバイスとそのストリームを消費している読み取りループの間にあります。データが消費される前に、任意の変換を適用できます。組み立てラインのようにイメージするとよいでしょう。ウィジェットがラインに入ると、ラインの各工程がウィジェットを変更するため、最終的な目的地に到達するまでに、ウィジェットは完全に機能するウィジェットになります。
たとえば、ストリームを消費し、改行に基づいてストリームをチャンク化する変換ストリームクラスを作成する方法を考察してみましょう。そのtransform()
メソッドは、ストリームが新しいデータを受信するたびに呼び出されます。データをキューに入れるか、後で使用できるように保存することができます。flush()
メソッドは、ストリームが閉じられたときに呼び出され、まだ処理されていないデータを処理します。
変換ストリームクラスを使用するには、着信ストリームをパイプで渡す必要があります。「シリアルポートからの読み取り」の3番目のコード例では、元の入力ストリームは TextDecoderStream
のみをパイプしていただけであるため、新しいLineBreakTransformer
を介してパイプするようにpipeThrough()
を呼び出す必要があります。
class LineBreakTransformer {
constructor() {
// A container for holding stream data until a new line.
this.chunks = "";
}
transform(chunk, controller) {
// Append new chunks to existing chunks.
this.chunks += chunk;
// For each line breaks in chunks, send the parsed lines out.
const lines = this.chunks.split("\r\n");
this.chunks = lines.pop();
lines.forEach((line) => controller.enqueue(line));
}
flush(controller) {
// When the stream is closed, flush any remaining chunks out.
controller.enqueue(this.chunks);
}
}
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable
.pipeThrough(new TransformStream(new LineBreakTransformer()))
.getReader();
シリアルデバイスの通信の問題をデバッグするには、port.readable
のtee()
メソッドを使用して、シリアルデバイスに送信されるストリームとシリアルデバイスから送信されるストリームを分割します。作成された2つのストリームは個別に使用できるため、1つをコンソールに出力して検査できます。
const [appReadable, devReadable] = port.readable.tee();
// You may want to update UI with incoming data from appReadable
// and log incoming data in JS console for inspection from devReadable.
開発のヒント
ChromeでWebシリアルAPIをデバッグするには、Chrome内部のabout://device-log
ページで簡単に行えます。このページでは、すべてのシリアルデバイス関連イベントをまとめて表示することができます。
Codelab
Google Developerコードラボでは、WebシリアルAPIを使用してBBC micro:bitボードとやり取りし、5x5 LED行列に画像を表示します。
ブラウザのサポート
WebシリアルAPIは、Chrome 89のすべてのデスクトッププラットフォーム(ChromeOS、Linux、macOS、およびWindows)で使用できます。
ポリフィル
Androidでは、WebUSB APIとシリアルAPIポリフィルを使用して、USBベースのシリアルポートをサポートできます。このポリフィルは、デバイスが組み込みデバイスドライバーによって引き受けられていないため、WebUSB APIを通じてデバイスにアクセス可能なハードウェアとプラットフォームに制限されています。
セキュリティとプライバシー
仕様の作成者は、ユーザー制御、透過性、人間工学など、強力なWebプラットフォーム機能へのアクセスの制御で定義されたコア原則を使用してWebシリアルAPIを設計し実装しています。このAPIを使用する機能は、主に、一度に1つのシリアルデバイスのみへのアクセスを付与する権限モデルによって制御されています。ユーザープロンプトに応答し、ユーザーは積極的な手順を踏んで特定のシリアルデバイスを選択する必要があります。
セキュリティのトレードオフを理解するには、WebシリアルAPIの説明文書のセキュリティとプライバシーのセクションをご覧ください。
フィードバック
Chromeチームは、WebシリアルAPIに関する意見や体験についてのフィードバックをお待ちしています。
APIの設計についてお聞かせください
期待どおりに動作しないAPIについてのご意見をお持ちですか?または、アイデアを実装するために必要なメソッドやプロパティが不足していませんか?
WebシリアルAPIのGitHubリポジトリに仕様の問題を提出するか、既存の問題についてのご意見を追加してください。
実装に関する問題を報告する
Chromeの実装にバグを見つけましたか?または、実装が仕様と異なりますか?
https://new.crbug.comにバグをご報告ください。できる限り詳しい情報を含め、バグを再現するための単純な手順をご説明の上、ComponentsをBlink>Serial
に設定してください。すばやく簡単に再現を共有するには、Glitchが最適です。
サポートの表明
WebシリアルAPIを使用することをお考えですか?あなたのパブリックサポートは、Chromeチームが機能に優先順位を付け、他のブラウザベンダーにそれらをサポートすることがいかに重要であるかを示す上でとても役立ちます。
@ChromiumDevにツイートして、このAPIをどこで、どのように使用されているのかお知らせください。ハッシュタグは#SerialAPI
をお使いください。
便利なリンク
- 仕様
- バグ追跡
- ChromeStatus.comエントリ
- Blinkコンポーネント:
Blink>Serial
デモ
謝辞
この記事をレビューしていただいたReilly GrantとJoe Medleyに感謝いたします。飛行機工場の写真提供: Birmingham Museums Trust(Unsplash)