Web Serial API スタートガイド

1. はじめに

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

作成するアプリの概要

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

67543f4caaaca5de.png

学習内容

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

必要なもの

この Codelab では、手頃な価格で、いくつかの入力(ボタン)と出力(5x5 LED ディスプレイ)を備え、追加の入力と出力を備えている micro:bit v1 を使用しています。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 の REPL(読み取り、評価、出力のループ)として機能します。次に、ストリームにデータを送信するメソッドを提供する必要があります。以下のコードは、出力ストリームからライターを取得し、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. [接続] ボタンをクリックします。
  3. シリアルポート選択ツールのダイアログで、BBC micro:bit デバイスを選択し、[接続] をクリックします。
  4. Chrome DevTools で [コンソール] タブを開き、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. [接続] ボタンをクリックします。
  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)に基づいてチャンク化します。このクラスには、transform メソッドと flush メソッドの 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:bit の 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. [Disconnect] ボタンを押して、LED マトリックスがオフになり、コンソールにエラーが表示されないことを確認します。

9. 完了

これで、これで、Web Serial API を使用する最初のウェブアプリを作成できました。

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