Web Serial API を使ってみる

最終更新日: 2019 年 11 月 8 日

作成するアプリの概要

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

ラボの内容

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

必要なもの

この Codelab では、価格が安く、入力(ボタン)と出力(5 x 5)LED の数が多く、入力と出力が追加のため、micro:bit を選択しました。micro:bit の機能の詳細については、Espruino のサイトの BBC micro:bit ページをご覧ください。

Web Serial API は、ウェブサイトがスクリプトを使用してシリアル デバイスに対する読み取りと書き込みを行う手段を提供します。この API は、ウェブサイトがマイクロコントローラや 3D プリンタなどのシリアル デバイスと通信できるようにすることで、ウェブと現実世界をつなぎます。

ウェブ テクノロジーを使用して構築される制御ソフトウェアの例は数多くあります。次に例を示します。

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

サイトと制御しているサイトとの間で直接通信することで、ユーザー エクスペリエンスを改善できます。

Web Serial API を有効にする

Web Serial API は現在開発中で、フラグ後でのみ利用できます。chrome://flags#enable-experimental-web-platform-features フラグを有効にする必要があります。

コードを取得する

この Codelab に必要なすべてが Glitch プロジェクトにまとめられています。

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

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

まず、現在のブラウザ内で Web Serial API がサポートされているかどうかを確認します。そのためには、serialnavigator に含まれているかどうかを確認します。

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

script.js - DOMContentLoaded

// CODELAB: Add feature detection here.
if ('serial' in navigator) {
  const notSupported = document.getElementById('notSupported');
  notSupported.classList.add('hidden');
}

これは、ウェブ シリアルがサポートされているかどうかをチェックします。その場合、このコードでは Web Serial がサポートされていないことを示すバナーが非表示になります。

試してみる

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

シリアルポートを開く

次に、シリアルポートを開く必要があります。他の最新の 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 - シリアル チップとメイン プロセッサの間に 9600 ボー接続を使用します。

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

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

script.js - clickConnect()

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

試してみる

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

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

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

接続が確立されたら、デバイスからデータを読み取るための入力ストリームとリーダーを設定する必要があります。まず、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 のロゴが表示されます。

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

通常、シリアル通信は双方向です。シリアルポートからデータを受信するだけでなく、ポートにデータを送信する必要もあります。入力ストリームと同様、出力ストリーム経由での 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")');」と入力します。

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

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

micro:bit の LED マトリックスを操作するには、show() を呼び出す必要があります。この方法では、内蔵 5×5 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 マトリックスに別のパターンを作成します。

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

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 行に 1 つずつ記述されます。

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

ストリームの基本的な処理

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

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

TransformStream を使用したストリームの変換

そのためには、変換ストリーム(TransformStream)を使用します。これにより、受信ストリームを解析し、解析されたデータを返すことができます。変換ストリームは、ストリーム ソース(この場合は micro:bit)とストリームを使用するもの(この場合は readLoop)の間に配置され、最終的に使用される前に任意の変換を適用できます。組み立てラインとして考えてください。ウィジェットがラインの上方へ進むと、行の各ステップでウィジェットが変更され、最終デスティネーションに到達するまでに完全に機能するウィジェットになります。

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

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 のいずれかのボタンを押したときに、LED マトリックスが明るい顔または悲しい顔に変わるはずです。

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

最後のステップとして、ユーザーとの接続が終了したときに、切断機能を接続します。

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

ユーザーが [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;
  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 マトリックスがオフになり、コンソールにエラーがないことを確認します。

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

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