Web Serial API スタートガイド

1. はじめに

最終更新日: 2020 年 7 月 21 日

作成するアプリの概要

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

81167ab7c01d353d.png

学習内容

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

必要なもの

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

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() を呼び出すようにしましょう。

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

script.js - clickConnect()

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

試してみる

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

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

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

読み取りループは、ループ内で実行され、メインスレッドをブロックせずにコンテンツを待機する非同期関数です。新しいデータが到着すると、リーダーは 2 つのプロパティ(valuedone ブール値)を返します。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. [Serial Port] 選択ダイアログで、BBC micro:bit デバイスを選択して [Connect] をクリックします。
  4. Espruino のロゴが表示されます。

93494fd58ea835eb.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 の read-eval-print loop(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. [接続] ボタンをクリックします。
  3. [Serial Port] 選択ダイアログで、BBC micro:bit デバイスを選択して [Connect] をクリックします。
  4. Chrome DevTools で [コンソール] タブを開き、writeToStream('console.log("yes")'); と入力します。

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

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

6c2193880c748412.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. [Serial Port] 選択ダイアログで、BBC micro:bit デバイスを選択して [Connect] をクリックします。
  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 のボタンのいずれかを押すと、LED マトリックスが笑顔または悲しい顔に変わります。

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

9. 完了

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

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