シリアルポートに対して読み取り / 書き込みを行う

Web Serial API を使用すると、ウェブサイトがシリアル デバイスと通信できます。

François Beaufort
François Beaufort

Web Serial API とは

シリアルポートは双方向通信インターフェースであり、 1 バイトずつ受信することを意味します。

Web Serial API は、ウェブサイトでの読み書きを可能にする シリアル デバイスを作成します。シリアル デバイスは、 システムのシリアルポート、またはリムーバブル USB デバイスや Bluetooth デバイス ポートをエミュレートします。

つまり、Web Serial API は、 ウェブサイトとマイクロコントローラなどのシリアル デバイスとの通信を可能にする 3D プリンタです。

この API は、WebUSB の優れたコンパニオンとしても役立ちます。オペレーティング システムでは シリアルポートと通信するために、その上位レベル 低レベル USB API ではなくシリアル API を使用します。

推奨されるユースケース

教育、趣味、産業の各分野では、ユーザーが周辺機器を パソコンにインストールすることもできます。これらのデバイスは多くの場合、 マイクロコントローラに接続できます。一部のカスタム これらのデバイスを制御するためのソフトウェアがウェブ テクノロジーで構築されています。

ウェブサイトがエージェントを介してデバイスと通信している場合があります。 ユーザーが手動でインストールしたアプリケーション。それ以外の環境では、 Electron などのフレームワークを通じてパッケージ化されたアプリケーションで配布できます。 ユーザーが次のような追加の手順を行う必要がある場合もあります。 コンパイル済みのアプリを USB フラッシュ ドライブ経由でデバイスにコピーする。

これらすべてのケースで、ダイレクト レスポンスを ウェブサイトと、そのウェブサイトが制御するデバイスとの間の通信。

現在のステータス

ステップ ステータス
1. 説明を作成 完了
2. 仕様の初期ドラフトを作成する 完了
3. フィードバックの収集と設計の反復処理 完了
4. オリジン トライアル 完了
5. 導入 完了

Web Serial API の使用

機能検出

Web Serial API がサポートされているかどうかを確認するには、次のコマンドを使用します。

if ("serial" in navigator) {
  // The Web Serial API is supported.
}

シリアルポートを開く

Web Serial API は、設計上非同期です。これによりウェブサイトの UI が ブロックします。これは重要なことです。なぜなら、シリアル データは 呼び出すには、それを聴く方法が必要です。

シリアルポートを開くには、まず SerialPort オブジェクトにアクセスします。そのためには 呼び出して、1 つのシリアルポートを選択するようユーザーに促すか、 タップなどのユーザー操作に応答する navigator.serial.requestPort() クリックするか、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 ベンダー(usbVendorId)とオプションの USB 製品を伴う 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();
<ph type="x-smartling-placeholder">
</ph> ウェブサイト上のシリアルポート プロンプトのスクリーンショット
BBC micro:bit の選択を求めるユーザー プロンプト

requestPort() を呼び出すと、ユーザーにデバイスの選択を促し、 SerialPort オブジェクト。SerialPort オブジェクトを取得したら、port.open() を呼び出す すると、シリアルポートが開きます。baudRate 辞書 メンバーは、シリアル ライン経由でデータを送信する速度を指定します。式で表されます。 1 秒あたりのビット数(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: 作成する必要がある読み取り / 書き込みバッファのサイズ (16 MB 未満にする必要があります)。
  • flowControl: フロー制御モード("none" または "hardware")。

シリアルポートから読み取る

Web Serial API の入力ストリームと出力ストリームは、Streams API によって処理されます。

シリアルポート接続が確立された後、readablewritable SerialPort オブジェクトのプロパティは、ReadableStreamWritableStream。これらのアドレスは、 接続します。どちらもデータ転送に Uint8Array インスタンスを使用します。

シリアル デバイスから新しいデータが届くと、port.readable.getReader().read() は、valuedone ブール値の 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.
  }
}

シリアル デバイスからテキストが返された場合は、port.readable を 次のように TextDecoderStream を設定します。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);
}

「Bring Your Own Buffer」を使用して、ストリームから読み取る際のメモリの割り当て方法を制御できます。読み取りますport.readable.getReader({ mode: "byob" }) を呼び出して ReadableStreamBYOBReader インターフェースを取得し、read() を呼び出すときに独自の ArrayBuffer を指定します。Web Serial API は、Chrome 106 以降でこの機能をサポートしています。

try {
  const reader = port.readable.getReader({ mode: "byob" });
  // Call reader.read() to read data into a buffer...
} catch (error) {
  if (error instanceof TypeError) {
    // BYOB readers are not supported.
    // Fallback to port.readable.getReader()...
  }
}

value.buffer のバッファを再利用する方法の例を次に示します。

const bufferSize = 1024; // 1kB
let buffer = new ArrayBuffer(bufferSize);

// Set `bufferSize` on open() to at least the size of the buffer.
await port.open({ baudRate: 9600, bufferSize });

const reader = port.readable.getReader({ mode: "byob" });
while (true) {
  const { value, done } = await reader.read(new Uint8Array(buffer));
  if (done) {
    break;
  }
  buffer = value.buffer;
  // Handle `value`.
}

次の例は、シリアルポートから特定の量のデータを読み取る方法を示しています。

async function readInto(reader, buffer) {
  let offset = 0;
  while (offset < buffer.byteLength) {
    const { value, done } = await reader.read(
      new Uint8Array(buffer, offset)
    );
    if (done) {
      break;
    }
    buffer = value.buffer;
    offset += value.byteLength;
  }
  return buffer;
}

const reader = port.readable.getReader({ mode: "byob" });
let buffer = new ArrayBuffer(512);
// Read the first 512 bytes.
buffer = await readInto(reader, buffer);
// Then read the next 512 bytes.
buffer = await readInto(reader, buffer);

シリアルポートに書き込む

シリアル デバイスにデータを送信するには、データを port.writable.getWriter().write()releaseLock() に発信: シリアルポートを後で閉じるには、port.writable.getWriter() が必要です。

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");

シリアルポートを閉じる

port.close() は、readable メンバーと writable メンバーによりシリアルポートを閉じます。 ロック解除されている(それぞれのアカウントに対して releaseLock() が呼び出されている) 作成します

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;
});

変換ストリームを使用する場合、シリアルポートを閉じるのはより複雑です。前と同じように reader.cancel() を呼び出します。 次に、writer.close()port.close() を呼び出します。これによりエラーが 基になるシリアルポートにストリーミングされます。エラーの伝播によって すぐには行われない場合は、readableStreamClosed を使用し、 port.readable を検出するため、以前に作成された writableStreamClosed Promise とport.writableのロックを解除しました。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 デバイスでシリアルポートが提供される場合は、そのデバイスを接続できます。 接続を解除できますウェブサイトに許可を与えられた場合 シリアルポートにアクセスすると、connect イベントと disconnect イベントをモニタリングする必要があります。

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.
});

シグナルを処理する

シリアルポート接続を確立したら、サービス アカウント キーを明示的に デバイス検出とフロー制御のためにシリアルポートで公開されている信号。これらの ブール値として定義されます。たとえば、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 を使用するか、独自の変換ストリームを作成して 受信ストリームを解析し、解析したデータを返します。変換ストリームは ストリームを使用している読み取りループとの間の 通信速度が向上します機能 データが消費される前に任意の変換を適用する。Kubernetes は 組み立てライン: ウィジェットがラインの下に進むにつれ、ラインの 最終目的地にたどり着くまでに 作成します。

<ph type="x-smartling-placeholder">
</ph> 飛行機の工場の写真
第二次世界大戦の城ブロムウィッチ航空機工場

たとえば、カスタム レスポンスを使用する変換ストリーム クラスを 改行でチャンクに分割されます。transform() メソッドが呼び出される 受信するたびに発生します。データをキューに追加するか、 後で使用するために保存します。flush() メソッドは、ストリームが終了し、かつ まだ処理されていないデータを処理します

変換ストリーム クラスを使用するには、受信ストリームを できます。シリアルポートから読み取るの 3 番目のコード例では、 元の入力ストリームは TextDecoderStream でのみパイプ処理されていたため、 pipeThrough() を呼び出して、新しい LineBreakTransformer でパイプ処理する必要があります。

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();

シリアル デバイスの通信の問題をデバッグするには、次の tee() メソッドを使用します。 port.readable: シリアル デバイスと送受信されるストリームを分割します。この 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.

シリアルポートへのアクセス権を取り消す

ウェブサイトで不要になったシリアルポートへのアクセス権限をクリーンアップできる SerialPort インスタンスの forget() を呼び出して保持を検討している。対象 たとえば、多くのユーザーと共有のパソコンで使用される教育用ウェブ アプリケーションの場合、 蓄積されたアクセス許可が蓄積されてしまうと、 向上させることができます

// Voluntarily revoke access to this serial port.
await port.forget();

forget() は Chrome 103 以降で利用できるため、この機能に対応しているかどうかをご確認ください 以下でサポートされます。

if ("serial" in navigator && "forget" in SerialPort.prototype) {
  // forget() is supported.
}

開発のヒント

Chrome の Web Serial API のデバッグは 内部ページを使用して簡単に行えます about://device-log。すべてのシリアル デバイス関連のイベントを 1 か所で確認できます。 1 か所で管理できます

<ph type="x-smartling-placeholder">
</ph> Web Serial API のデバッグに関する内部ページのスクリーンショット。
Web Serial API をデバッグするための Chrome の社内ページ

Codelab

Google Developer Codelab では、Web Serial API を使用して BBC micro:bit のボードを使用し、5x5 の LED マトリックスに画像を表示。

ブラウザ サポート

Web Serial API は、あらゆるデスクトップ プラットフォーム(ChromeOS、Linux、macOS、 (Chrome 89)でリリースされます。

ポリフィル

Android では、WebUSB API を使用して USB ベースのシリアルポートをサポートできます。 Serial API ポリフィルを使用します。このポリフィルはハードウェアと デバイスには WebUSB API 経由でアクセスできる必要がありますが、 要求されていると自動的に検出されます。

セキュリティとプライバシー

仕様の作成者は、Core を使用して Web Serial API を設計、実装しました。 強力なウェブ プラットフォーム機能へのアクセスの制御で定義されている原則 あらゆる側面に関与しています。この機能を API は主に、1 つのサービスにのみアクセスを許可する権限モデルによって 登録する必要がありますユーザーのプロンプトに応じて、ユーザーは 選択します。

セキュリティのトレードオフについては、セキュリティプライバシーをご確認ください。 (ウェブシリアル API の説明)をご覧ください。

フィードバック

Chrome チームでは、 Web Serial API

API 設計について教えてください

API に関して想定どおりに機能していないものはありますか?それとも アイデアを実装するために必要なメソッドやプロパティが足りませんか?

Web Serial API GitHub リポジトリで仕様に関する問題を報告するか、 考えをまとめます

実装に関する問題を報告する

Chrome の実装にバグは見つかりましたか?それとも どうなるでしょうか

https://new.crbug.com でバグを報告します。できるだけ多くの バグを再現するための簡単な手順を提供し、さらに、バグを再現するための [Components] を Blink>Serial に設定します。Glitch が適しているケース 簡単に再現できます。

サポートの気持ちを伝える

Web Serial API を使用する予定はありますか?皆様の公開サポートは、Chrome を チームは機能の優先度を判断し、他のブラウザ ベンダーが サポートします。

ハッシュタグを使用して @ChromiumDev にツイートしてください #SerialAPI どこで、どのように使用されているかをお知らせください。

関連情報

デモ

謝辞

この記事のレビューをしてくれた Reilly GrantJoe Medley に感謝します。 バーミンガム ミュージアムズ トラストが撮影した飛行機の工場の写真(Unsplash より)。