Web Serial API スタートガイド

1. はじめに

最終更新日: 2022-09-19

作成するアプリの概要

この Codelab では、Web Serial API を使用して BBC micro:bit ボードとやり取りし、5x5 LED マトリックスに画像を表示するウェブページを作成します。Web Serial API について学習し、読み取り可能ストリーム、書き込み可能ストリーム、変換ストリームを使用して、ブラウザ経由でシリアル デバイスと通信する方法を学びます。

67543f4caaaca5de.png

学習内容

  • Web Serial シリアルポートを開閉する方法
  • 読み取りループを使用して入力ストリームからデータを処理する方法
  • 書き込みストリーム経由でデータを送信する方法

必要なもの

この Codelab では、micro:bit v1 を使用します。これは、手頃な価格で、いくつかの入力(ボタン)と出力(5x5 LED ディスプレイ)を備えており、追加の入出力も可能です。micro:bit の機能について詳しくは、Espruino サイトの BBC micro:bit ページをご覧ください。

2. Web Serial API について

Web Serial API を使用すると、ウェブサイトでスクリプトを使用してシリアル デバイスから読み取り、シリアル デバイスに書き込むことができます。この API は、ウェブサイトがマイクロコントローラや 3D プリンタなどのシリアル デバイスと通信できるようにすることで、ウェブと物理世界を橋渡しします。

ウェブ技術を使用して構築された制御ソフトウェアの例は数多くあります。次に例を示します。

場合によっては、これらのウェブサイトは、ユーザーが手動でインストールしたネイティブ エージェント アプリケーションを介してデバイスと通信します。また、Electron などのフレームワークを介して、パッケージ化されたネイティブ アプリケーションとして配信される場合もあります。また、コンパイル済みアプリケーションを USB フラッシュ ドライブでデバイスにコピーするなど、追加の手順が必要になる場合もあります。

サイトと制御対象のデバイス間で直接通信を行うことで、ユーザー エクスペリエンスを向上させることができます。

3. 設定方法

コードを取得する

この Codelab で必要なものはすべて Glitch プロジェクトに用意されています。

  1. 新しいブラウザタブを開き、 https://web-serial-codelab-start.glitch.me/ にアクセスします。
  2. [Remix Glitch] リンクをクリックして、スターター プロジェクトの独自のバージョンを作成します。
  3. [Show] ボタンをクリックし、[In a New Window] を選択して、コードの動作を確認します。

4. シリアル接続を開く

Web Serial API がサポートされているかどうかを確認する

まず、現在のブラウザで Web Serial API がサポートされているかどうかを確認します。これを行うには、navigatorserial があるかどうかを確認します。

DOMContentLoaded イベントで、次のコードをプロジェクトに追加します。

script.js - DOMContentLoaded

// CODELAB: Add feature detection here.
const notSupported = document.getElementById('notSupported');
notSupported.classList.toggle('hidden', 'serial' in navigator);

これにより、Web Serial がサポートされているかどうかが確認されます。サポートされている場合、このコードは Web Serial が対象外であることを示すバナーを非表示にします。

試してみる

  1. ページを読み込みます。
  2. Web Serial がサポートされていないことを示す赤いバナーがページに表示されないことを確認します。

シリアルポートを開く

次に、シリアルポートを開く必要があります。他の最新の API と同様に、Web Serial API は非同期です。これにより、入力待ちのときに UI がブロックされるのを防ぐことができます。また、シリアルデータはいつでもウェブページで受信できるため、リッスンする方法が必要です。

パソコンに複数のシリアル デバイスが接続されている場合、ブラウザがポートをリクエストしようとすると、接続するデバイスを選択するよう求めるプロンプトが表示されます。

次のコードをプロジェクトに追加します。

script.js - connect()

// CODELAB: Add code to request & open port here.
// - Request a port and open a connection.
port = await navigator.serial.requestPort();
// - Wait for the port to open.
await port.open({ baudRate: 9600 });

requestPort 呼び出しにより、接続するデバイスを選択するよう求めるプロンプトが表示されます。port.open を呼び出すと、ポートが開きます。また、シリアル デバイスとの通信速度を指定する必要があります。BBC micro:bit は、USB-to-serial チップとメイン プロセッサ間で 9600 ボーの接続を使用します。

また、[Connect] ボタンをフックして、ユーザーがクリックしたときに connect() を呼び出すようにします。

次のコードをプロジェクトに追加します。

script.js - clickConnect()

// CODELAB: Add connect code here.
await connect();

試してみる

これで、プロジェクトを開始するための最小限の準備ができました。[Connect] ボタンをクリックすると、接続するシリアル デバイスを選択するよう求めるプロンプトが表示され、micro:bit に接続されます。

  1. ページを再読み込みします。
  2. [Connect] ボタンをクリックします。
  3. [Serial Port chooser] ダイアログで、BBC micro:bit デバイスを選択し、[Connect] をクリックします。
  4. タブに、シリアル デバイスに接続したことを示すアイコンが表示されます。

e695daf2277cd3a2.png

シリアルポートからのデータをリッスンする入力ストリームを設定する

接続が確立されたら、入力ストリームとリーダーを設定してデバイスからデータを読み取る必要があります。まず、port.readable を呼び出して、ポートから読み取り可能なストリームを取得します。デバイスからテキストが返されることがわかっているため、テキスト デコーダを介してパイプ処理します。次に、リーダーを取得して読み取りループを開始します。

次のコードをプロジェクトに追加します。

script.js - connect()

// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable;

reader = inputStream.getReader();
readLoop();

読み取りループは、ループ内で実行され、メインスレッドをブロックせずにコンテンツを待機する非同期関数です。新しいデータが到着すると、リーダーは valuedone ブール値の 2 つのプロパティを返します。done が true の場合、ポートが閉じられているか、データが受信されていません。

次のコードをプロジェクトに追加します。

script.js - readLoop()

// CODELAB: Add read loop here.
while (true) {
  const { value, done } = await reader.read();
  if (value) {
    log.textContent += value + '\n';
  }
  if (done) {
    console.log('[readLoop] DONE', done);
    reader.releaseLock();
    break;
  }
}

試してみる

これで、プロジェクトはデバイスに接続し、デバイスから受信したデータをログ要素に追加できるようになりました。

  1. ページを再読み込みします。
  2. [Connect] ボタンをクリックします。
  3. [Serial Port chooser] ダイアログで、BBC micro:bit デバイスを選択し、[Connect] をクリックします。
  4. Espruino のロゴが表示されます。

dd52b5c37fc4b393.png

シリアルポートにデータを送信する出力ストリームを設定する

通常、シリアル通信は双方向です。シリアルポートからデータを受信するだけでなく、ポートにデータを送信することもできます。入力ストリームと同様に、出力ストリームを介して micro:bit にテキストのみを送信します。

まず、テキスト エンコーダ ストリームを作成し、ストリームを port.writeable にパイプ処理します。

script.js - connect()

// CODELAB: Add code setup the output stream here.
const encoder = new TextEncoderStream();
outputDone = encoder.readable.pipeTo(port.writable);
outputStream = encoder.writable;

Espruino ファームウェアを使用してシリアル接続すると、BBC micro:bit ボードは JavaScript の 読み取り、評価、出力のループ(REPL)として機能します。これは Node.js シェルで取得するものと似ています。次に、ストリームにデータを送信する方法を指定する必要があります。次のコードでは、出力ストリームからライターを取得し、write を使用して各行を送信します。送信される各行には、micro:bit に送信されたコマンドを評価するよう指示する改行文字(\n)が含まれています。

script.js - writeToStream()

// CODELAB: Write to output stream
const writer = outputStream.getWriter();
lines.forEach((line) => {
  console.log('[SEND]', line);
  writer.write(line + '\n');
});
writer.releaseLock();

システムを既知の状態にして、送信した文字がエコーバックされないようにするには、CTRL-C を送信してエコーをオフにする必要があります。

script.js - connect()

// CODELAB: Send CTRL-C and turn off echo on REPL
writeToStream('\x03', 'echo(false);');

試してみる

これで、プロジェクトは micro:bit との間でデータを送受信できるようになりました。コマンドを正しく送信できることを確認しましょう。

  1. ページを再読み込みします。
  2. [Connect] ボタンをクリックします。
  3. [Serial Port chooser] ダイアログで、BBC micro:bit デバイスを選択し、[Connect] をクリックします。
  4. Chrome DevTools で [Console] タブを開き、「writeToStream('console.log("yes")');」と入力します。

ページに次のような内容が表示されます。

15e2df0064b5de28.png

5. LED マトリックスを制御する

マトリックス グリッド文字列を作成する

micro:bit の LED マトリックスを制御するには、 show() を呼び出す必要があります。このメソッドは、組み込みの 5x5 LED 画面にグラフィックを表示します。バイナリ数または文字列を指定できます。

チェックボックスを反復処理し、オンになっているものとオフになっているものを示す 1 と 0 の配列を生成します。チェックボックスの順序はマトリックス内の LED の順序と逆になるため、配列を反転する必要があります。次に、配列を文字列に変換し、micro:bit に送信するコマンドを作成します。

script.js - sendGrid()

// CODELAB: Generate the grid
const arr = [];
ledCBs.forEach((cb) => {
  arr.push(cb.checked === true ? 1 : 0);
});
writeToStream(`show(0b${arr.reverse().join('')})`);

チェックボックスをフックしてマトリックスを更新する

次に、チェックボックスの変更をリッスンし、変更された場合はその情報を micro:bit に送信する必要があります。機能検出コード(// CODELAB: Add feature detection here.)に次の行を追加します。

script.js - DOMContentLoaded

initCheckboxes();

また、micro:bit が最初に接続されたときにグリッドをリセットして、笑顔が表示されるようにします。drawGrid() 関数はすでに用意されています。この関数は sendGrid() と同様に機能します。1 と 0 の配列を受け取り、必要に応じてチェックボックスをオンにします。

script.js - clickConnect()

// CODELAB: Reset the grid on connect here.
drawGrid(GRID_HAPPY);
sendGrid();

試してみる

ページが micro:bit への接続を開くと、笑顔が送信されます。チェックボックスをクリックすると、LED マトリックスの表示が更新されます。

  1. ページを再読み込みします。
  2. [Connect] ボタンをクリックします。
  3. [Serial Port chooser] ダイアログで、BBC micro:bit デバイスを選択し、[Connect] をクリックします。
  4. micro:bit LED マトリックスに笑顔が表示されます。
  5. チェックボックスを変更して、LED マトリックスに別のパターンを描画します。

6. micro:bit ボタンをフックする

micro:bit ボタンに watch イベントを追加する

micro:bit には、LED マトリックスの両側に 2 つのボタンがあります。Espruino には、ボタンが押されたときにイベント/コールバックを送信する setWatch 関数が用意されています。両方のボタンをリッスンするため、関数を汎用化してイベントの詳細を出力します。

script.js - watchButton()

// CODELAB: Hook up the micro:bit buttons to print a string.
const cmd = `
  setWatch(function(e) {
    print('{"button": "${btnId}", "pressed": ' + e.state + '}');
  }, ${btnId}, {repeat:true, debounce:20, edge:"both"});
`;
writeToStream(cmd);

次に、シリアルポートがデバイスに接続されるたびに、両方のボタン(micro:bit ボードでは BTN1 と BTN2)をフックする必要があります。

script.js - clickConnect()

// CODELAB: Initialize micro:bit buttons.
watchButton('BTN1');
watchButton('BTN2');

試してみる

接続時に笑顔を表示するだけでなく、micro:bit のいずれかのボタンを押すと、押されたボタンを示すテキストがページに追加されます。ほとんどの場合、各文字は独自の行に表示されます。

  1. ページを再読み込みします。
  2. [Connect] ボタンをクリックします。
  3. [Serial Port chooser] ダイアログで、BBC micro:bit デバイスを選択し、[Connect] をクリックします。
  4. micro:bit LED マトリックスに笑顔が表示されます。
  5. micro:bit のボタンを押して、押されたボタンの詳細を含む新しいテキストがページに追加されることを確認します。

7. 変換ストリームを使用して受信データを解析する

基本的なストリーム処理

micro:bit のボタンのいずれかが押されると、micro:bit はストリームを介してシリアルポートにデータを送信します。ストリームは非常に便利ですが、すべてのデータを一度に取得できるとは限らず、任意にチャンク化される可能性があるため、課題となることもあります。

現在、アプリは受信したストリームをそのまま出力します(readLoop)。ほとんどの場合、各文字は独自の行に表示されますが、あまり役に立ちません。理想的には、ストリームを個々の行に解析し、各メッセージを独自の行として表示する必要があります。

TransformStream を使用してストリームを変換する

これを行うには、変換ストリーム(TransformStream)を使用します。これにより、受信ストリームを解析して解析済みデータを返すことができます。変換ストリームは、ストリーム ソース(この場合は micro:bit)とストリームを使用する要素(この場合は readLoop)の間に配置でき、最終的に使用される前に任意の変換を適用できます。アセンブリラインのようなものです。ウィジェットがラインに沿って移動すると、ラインの各ステップでウィジェットが変更され、最終的な宛先に到達するまでに完全に機能するウィジェットになります。

詳細については、MDN の Streams API のコンセプトをご覧ください。

LineBreakTransformer を使用してストリームを変換する

LineBreakTransformer クラスを作成します。このクラスはストリームを受け取り、改行(\r\n)に基づいてチャンク化します。このクラスには、transformflush の 2 つのメソッドが必要です。transform メソッドは、ストリームが新しいデータを受信するたびに呼び出されます。データをキューに入れるか、後で使用するために保存できます。flush メソッドは、ストリームが閉じられたときに呼び出され、まだ処理されていないデータを処理します。

transform メソッドで、新しいデータを container に追加し、container に改行があるかどうかを確認します。改行がある場合は、配列に分割し、行を反復処理して controller.enqueue() を呼び出し、解析された行を送信します。

script.js - LineBreakTransformer.transform()

// CODELAB: Handle incoming chunk
this.container += chunk;
const lines = this.container.split('\r\n');
this.container = lines.pop();
lines.forEach(line => controller.enqueue(line));

ストリームが閉じられたら、enqueue を使用してコンテナ内の残りのデータをフラッシュします。

script.js - LineBreakTransformer.flush()

// CODELAB: Flush the stream.
controller.enqueue(this.container);

最後に、受信ストリームを新しい LineBreakTransformer を介してパイプ処理する必要があります。元の入力ストリームは TextDecoderStream を介してのみパイプ処理されていたため、新しい LineBreakTransformer を介してパイプ処理するには、追加の pipeThrough を追加する必要があります。

script.js - connect()

// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable
  .pipeThrough(new TransformStream(new LineBreakTransformer()));

試してみる

micro:bit のボタンのいずれかを押すと、出力されたデータが 1 行で返されます。

  1. ページを再読み込みします。
  2. [Connect] ボタンをクリックします。
  3. [Serial Port chooser] ダイアログで、BBC micro:bit デバイスを選択し、[Connect] をクリックします。
  4. micro:bit LED マトリックスに笑顔が表示されます。
  5. micro:bit のボタンを押して、次のような内容が表示されることを確認します。

eead3553d29ee581.png

JSONTransformer を使用してストリームを変換する

readLoop で文字列を JSON に解析することもできますが、代わりに、データを JSON オブジェクトに変換する非常にシンプルな JSON トランスフォーマーを作成しましょう。データが有効な JSON でない場合は、受信したデータをそのまま返します。

script.js - JSONTransformer.transform

// CODELAB: Attempt to parse JSON content
try {
  controller.enqueue(JSON.parse(chunk));
} catch (e) {
  controller.enqueue(chunk);
}

次に、LineBreakTransformer を通過した後に、ストリームを JSONTransformer を介してパイプ処理します。これにより、JSON は 1 行でしか送信されないことがわかっているため、JSONTransformer をシンプルに保つことができます。

script.js - connect

// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable
  .pipeThrough(new TransformStream(new LineBreakTransformer()))
  .pipeThrough(new TransformStream(new JSONTransformer()));

試してみる

micro:bit のボタンのいずれかを押すと、ページに [object Object] が出力されます。

  1. ページを再読み込みします。
  2. [Connect] ボタンをクリックします。
  3. [Serial Port chooser] ダイアログで、BBC micro:bit デバイスを選択し、[Connect] をクリックします。
  4. micro:bit LED マトリックスに笑顔が表示されます。
  5. micro:bit のボタンを押して、次のような内容が表示されることを確認します。

ボタンの押下に応答する

micro:bit のボタンの押下に応答するには、受信したデータが button プロパティを持つ object であるかどうかを確認するように readLoop を更新します。次に、buttonPushed を呼び出してボタンの押下を処理します。

script.js - readLoop()

const { value, done } = await reader.read();
if (value && value.button) {
  buttonPushed(value);
} else {
  log.textContent += value + '\n';
}

micro:bit のボタンが押されると、LED マトリックスの表示が変わります。次のコードを使用してマトリックスを設定します。

script.js - buttonPushed()

// CODELAB: micro:bit button press handler
if (butEvt.button === 'BTN1') {
  divLeftBut.classList.toggle('pressed', butEvt.pressed);
  if (butEvt.pressed) {
    drawGrid(GRID_HAPPY);
    sendGrid();
  }
  return;
}
if (butEvt.button === 'BTN2') {
  divRightBut.classList.toggle('pressed', butEvt.pressed);
  if (butEvt.pressed) {
    drawGrid(GRID_SAD);
    sendGrid();
  }
}

試してみる

micro:bit のボタンのいずれかを押すと、LED マトリックスが笑顔または悲しい顔に変わります。

  1. ページを再読み込みします。
  2. [Connect] ボタンをクリックします。
  3. [Serial Port chooser] ダイアログで、BBC micro:bit デバイスを選択し、[Connect] をクリックします。
  4. micro:bit LED マトリックスに笑顔が表示されます。
  5. micro:bit のボタンを押して、LED マトリックスが変化することを確認します。

8. シリアルポートを閉じる

最後のステップは、ユーザーが操作を完了したときにポートを閉じるように切断機能をフックすることです。

ユーザーが [Connect] / [Disconnect] ボタンをクリックしたときにポートを閉じる

ユーザーが [Connect]/[Disconnect] ボタンをクリックしたときに、接続を閉じる必要があります。ポートがすでに開いている場合は、disconnect() を呼び出して、ページがシリアル デバイスに接続されていないことを示すように UI を更新します。

script.js - clickConnect()

// CODELAB: Add disconnect code here.
if (port) {
  await disconnect();
  toggleUIConnected(false);
  return;
}

ストリームとポートを閉じる

disconnect 関数で、入力ストリームを閉じ、出力ストリームを閉じ、ポートを閉じる必要があります。入力ストリームを閉じるには、reader.cancel() を呼び出します。cancel の呼び出しは非同期であるため、完了するまで await を使用して待機する必要があります。

script.js - disconnect()

// CODELAB: Close the input stream (reader).
if (reader) {
  await reader.cancel();
  await inputDone.catch(() => {});
  reader = null;
  inputDone = null;
}

出力ストリームを閉じるには、writer を取得して close() を呼び出し、outputDone オブジェクトが閉じられるまで待機します。

script.js - disconnect()

// CODELAB: Close the output stream.
if (outputStream) {
  await outputStream.getWriter().close();
  await outputDone;
  outputStream = null;
  outputDone = null;
}

最後に、シリアルポートを閉じて、閉じるまで待機します。

script.js - disconnect()

// CODELAB: Close the port.
await port.close();
port = null;

試してみる

これで、シリアルポートを自由に開閉できます。

  1. ページを再読み込みします。
  2. [Connect] ボタンをクリックします。
  3. [Serial Port chooser] ダイアログで、BBC micro:bit デバイスを選択し、[Connect] をクリックします。
  4. micro:bit LED マトリックスに笑顔が表示されます。
  5. [Disconnect] ボタンを押して、LED マトリックスがオフになり、コンソールにエラーが表示されないことを確認します。

9. 完了

おめでとうございます!Web Serial API を使用する最初のウェブアプリを作成できました。

https://goo.gle/fugu-api-tracker で、Web Serial API と Chrome チームが取り組んでいるその他のエキサイティングな新しいウェブ機能の最新情報を確認してください。