Web Serial API スタートガイド

1. はじめに

最終更新日: 2022 年 9 月 19 日

作成するアプリの概要

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

67543f4caaaca5de.png

学習内容

  • ウェブシリアルポートの開閉方法
  • 読み取りループを使用して入力ストリームのデータを処理する方法
  • 書き込みストリームでデータを送信する方法

必要なもの

この Codelab では micro:bit v1 を使用することにしました。手頃な価格で、入力(ボタン)と出力(5 x 5 の 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 がサポートされているかどうかを確認します。そのためには、serialnavigator に含まれているかどうかを確認します。

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

script.js - DOMContentLoaded

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

ウェブのシリアルがサポートされているかどうかを確認します。使用されている場合、このコードにより、ウェブのシリアルはサポートされていないことを示すバナーが非表示になります。

試してみる

  1. ページを読み込みます。
  2. ウェブのシリアルがサポートされていないことを示す赤いバナーがページに表示されていないことを確認します。

シリアルポートを開く

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

1 台のパソコンに複数のシリアル デバイスが搭載されていることがあるため、ブラウザがポートをリクエストしようとすると、接続するデバイスを選択するようユーザーに促されます。

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

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 - シリアル チップとメイン プロセッサ間の 9600 ボー接続を使用します。

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

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

script.js - clickConnect()

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

試してみる

これで、プロジェクトを開始するのに必要な最低限のものになりました。[接続] ボタンをクリックすると、接続するシリアル デバイスを選択して micro:bit に接続するように求められます。

  1. ページを再読み込みする。
  2. [接続] ボタンをクリックします。
  3. シリアルポート選択ダイアログで、BBC micro:bit デバイスを選択し、[接続] をクリックします。
  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. [接続] ボタンをクリックします。
  3. シリアルポート選択ダイアログで、BBC micro:bit デバイスを選択し、[接続] をクリックします。
  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 ボードは、Node.js シェルと同様に JavaScript の read-eval-print ループ(REPL)として動作します。次に、ストリームにデータを送信するメソッドを提供する必要があります。以下のコードは、出力ストリームからライターを取得し、write を使用して各行を送信します。送信される各行には改行文字(\n)が含まれており、送信されたコマンドを評価するように micro:bit に指示します。

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. [接続] ボタンをクリックします。
  3. シリアルポート選択ダイアログで、BBC micro:bit デバイスを選択し、[接続] をクリックします。
  4. Chrome DevTools の [Console] タブを開き、「writeToStream('console.log("yes")');」と入力します。

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

15e2df0064b5de28.png

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

行列グリッドの文字列を作成する

micro:bit の LED マトリックスを制御するには、show() を呼び出す必要があります。この方法では、内蔵の 5x5 LED スクリーンにグラフィックスを表示します。2 進数または文字列を取ります。

チェックボックスを反復処理して、オンとオフを示す 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. [接続] ボタンをクリックします。
  3. シリアルポート選択ダイアログで、BBC micro:bit デバイスを選択し、[接続] をクリックします。
  4. micro:bit の LED マトリックスに笑顔が表示されます。
  5. チェックボックスを切り替えて、LED マトリックスに異なるパターンを描画します。

6. micro:bit ボタンを接続する

micro:bit ボタンにウォッチイベントを追加する

micro:bit には LED マトリックスの両側に 1 つずつ、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 文字は 1 行に 1 つずつ記述します。

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

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

基本的なストリーム処理

micro:bit ボタンの 1 つを押すと、micro:bit はストリームを介してシリアルポートにデータを送信します。ストリームは非常に便利ですが、必ずしもすべてのデータが一度に取得されるわけではなく、データが任意のチャンクに分割される可能性があるため、難しいこともあります。

アプリは現在、受信ストリームの到着時に(readLoop で)出力します。ほとんどの場合、1 行につき 1 文字ずつ表示されますが、これはあまり役に立ちません。ストリームを個々の行に解析し、各メッセージを個別の行として表示するのが理想的です。

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 のみをパイプ処理していたため、pipeThrough を追加して新しい LineBreakTransformer でパイプ処理する必要があります。

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 行で返されるはずです。

  1. ページを再読み込みする。
  2. [接続] ボタンをクリックします。
  3. シリアルポート選択ダイアログで、BBC micro:bit デバイスを選択し、[接続] をクリックします。
  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. [接続] ボタンをクリックします。
  3. シリアルポート選択ダイアログで、BBC micro:bit デバイスを選択し、[接続] をクリックします。
  4. micro:bit の LED マトリックスに笑顔が表示されます。
  5. micro:bit のボタンを押して、次のように表示されることを確認します。

ボタンの押下への応答

micro:bit ボタンの押下に応答するには、readLoop を更新して、受信したデータが button プロパティを持つ object かどうかを確認します。次に、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 ボタンの 1 つを押すと、LED マトリックスが幸せな顔または悲しい顔に変わるはずです。

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

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

最後のステップとして、接続解除機能を接続して、ユーザーが作業を完了したときにポートを閉じます。

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

ユーザーが [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. [接続] ボタンをクリックします。
  3. シリアルポート選択ダイアログで BBC micro:bit デバイスを選択し、[接続] をクリックします。
  4. micro:bit の LED マトリックスに笑顔が表示されます。
  5. [切断] ボタンを押し、LED マトリックスが消えて、コンソールにエラーがないことを確認します。

9. 完了

これで、Web Serial API を使用する最初のウェブアプリを正常にビルドできました。

Web Serial API に関する最新情報や、Chrome チームが現在取り組んでいるその他の魅力的な新しいウェブ機能については、https://goo.gle/fugu-api-tracker をご覧ください。