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

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

François Beaufort
François Beaufort

Web Serial API とは何ですか?

シリアルポートは、バイト単位でデータを送受信できる双方向通信インターフェースです。

Web Serial API を使用すると、ウェブサイトは JavaScript を使用してシリアル デバイスから読み取り、シリアル デバイスに書き込むことができます。シリアル デバイスは、ユーザーのシステムのシリアルポート、またはシリアルポートをエミュレートする取り外し可能な 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 オブジェクトにアクセスします。これを行うには、タッチやマウスクリックなどのユーザー ジェスチャーに応じて navigator.serial.requestPort() を呼び出して、1 つのシリアルポートを選択するようユーザーに促すか、navigator.serial.getPorts() から選択します。この関数は、ウェブサイトにアクセス権が付与されているシリアルポートのリストを返します。

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 プロダクト ID(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();
ウェブサイトのシリアルポート プロンプトのスクリーンショット
BBC micro:bit を選択するようユーザーに促す

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: 作成する読み取りバッファと書き込みバッファのサイズ(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.readableTextDecoderStream にパイプできます。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() にデータを渡します。シリアルポートを後で閉じるには、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");

シリアルポートを閉じる

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 プロミスと writableStreamClosed プロミスを使用して、port.readableport.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 などの一部のデバイスでは、Data Terminal Ready(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 を介してのみパイプされているため、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();

シリアル デバイスの通信に関する問題のデバッグには、port.readabletee() メソッドを使用して、シリアル デバイスとの間で送受信されるストリームを分割します。作成された 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 か所で確認できます。

Web Serial API のデバッグ用の内部ページのスクリーンショット。
Chrome の内部ページで Web Serial API をデバッグする。

Codelab

Google デベロッパーの Codelab では、Web Serial API を使用して BBC micro:bit ボードとやり取りし、5x5 LED マトリックスに画像を表示します。

ブラウザ サポート

Web Serial API は、Chrome 89 のすべてのデスクトップ プラットフォーム(ChromeOS、Linux、macOS、Windows)で利用できます。

ポリフィル

Android では、WebUSB API と Serial API ポリフィルを使用して、USB ベースのシリアルポートをサポートできます。このポリフィルは、組み込みのデバイス ドライバによって要求されていないため、デバイスに WebUSB API 経由でアクセスできるハードウェアとプラットフォームに限定されます。

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

仕様の作成者は、ユーザー制御、透明性、人間工学など、強力なウェブ プラットフォーム機能へのアクセスを制御するで定義されている基本 原則を使用して、Web Serial API を設計、実装しました。この API を使用できるかどうかは、主に、一度に 1 つのシリアル デバイスにのみアクセス権を付与する権限モデルによって制御されます。ユーザー プロンプトに応じて、ユーザーは特定のシリアル デバイスを選択する手順を積極的に行う必要があります。

セキュリティ上のトレードオフについては、Web Serial API の解説のセキュリティプライバシー のセクションをご覧ください。

フィードバック

Chrome チームは、Web Serial API に関する皆様のご意見やご感想をお待ちしております。

API 設計について

API で想定どおりに動作しない点がありますか?アイデアを実装するために必要なメソッドやプロパティが不足していますか?

Web Serial API GitHub レポジトリで仕様に関する問題を報告するか、既存の問題に 意見を追加してください。

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

Chrome の実装にバグが見つかりましたか?実装が仕様と異なっていますか?

https://new.crbug.com でバグを報告してください。できるだけ詳細な情報を含め、バグを再現するための簡単な手順を記載し、[Components] を Blink>Serial に設定してください。

サポートを示す

Web Serial API を使用する予定はありますか?公開サポートは、Chrome チームが機能の優先順位を決定するのに役立ち、他のブラウザ ベンダーにサポートの重要性を示すことができます。

ハッシュタグ #SerialAPI を使用して@ChromiumDevにツイートし、使用している場所と方法をお知らせください。

関連情報

デモ

謝辞

このドキュメントのレビューにご協力いただいた Reilly Grant 様と Joe Medley 様に感謝いたします。