Web Serial API+ESP32(Arduino)研究 その1 シリアルコンソール

概要

最新版のChromeブラウザはWeb Serial APIが搭載されています。これを使うことでブラウザ経由でシリアルポートにアクセスすることができるようになっているので、ESP32(Arduino)でどんなことができるかを検証してみたいと思います。

Web Serial APIとは?

上記に仕様書があります。英語ですがそれほど長くないのでがんばって読み込んだほうがいいと思います。私はまだぜんぜん読めていません。。。

ざっくりいうとブラウザ経由でシリアルポートにアクセスが可能なAPIです。機能的には結構昔からあったのですが、セキュリティー的な問題があってなかなか公開されてこなかったものになります。

上記では似たようなWeb MIDI APIを使ってMIDIをブラウザ経由で制御しています。中身的にはほぼ同じような制御なのですがMIDIと比べると結構公開まで時間かかりました。

理由としてシリアルポート経由でファームウエアを書き換えられる端末が多いので、スパイウエア的なものを埋め込まれる危険性があるからみたいです。たとえばモバイルバッテリなども公開していませんがシリアル経由でファームウエアを転送できるものがあるみたいで、暴走するようなファームウエアを転送することで爆発する危険性があります。

てな感じで汎用的なUSBをブラウザからアクセスする技術ってのはセキュリティ的な問題があって公開が遅くなったみたいですね。現状も結構制限されていますが、まあ使う上では十分だと思います。

ESP32側のコード

まずは単純なシリアルコンソールを作ってみたいと思います。シリアルでのテキストデータ送受信をしてみます。とりあえずESP32でシリアルを定期的に送信しつつ、受信した文字列をそのままエコーするスケッチを作ってみます。

unsigned long lastMillis = 0;

void setup() {
  Serial.begin(115200);
}

void loop() {
  while (Serial.available()) {
    String command = Serial.readStringUntil('\n');
    Serial.println(command);
  }

  if(lastMillis + 1000 <= millis()){
    Serial.println(millis());
    lastMillis += 1000;
  }

  delay(1);
}

1秒間隔で起動経過時間を送信しつつ、受信した文字列をそのまま送信しています。中身は非常に単純ですね。

こんな感じで1秒ごとに経過時間が表示され、上のテキストボックスに入力して送信ボタンを押すと送信した文字列が戻ってきます。非常に単純な例ですね。

作成物

上記に作ったものを置きました。

上記のようなサイトになります。

Connectボタンを押すと上記のようなダイアログが表示され、どのシリアルポートを接続するかが聞かれます。ここで選択したポートのみがブラウザからアクセスできるようになります。

接続したところ、上記のように動きました。日本語を投げてもそのまま戻ってくるので表示できるんですね。

仕組み

youさんの記事をかなり参考にさせていただきました。感謝。

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Web Serial API Console</title>
    <script>
        let port;

        async function onConnectButtonClick() {
            try {
                port = await navigator.serial.requestPort();
                await port.open({ baudRate: 115200 });

                while (port.readable) {
                    const reader = port.readable.getReader();

                    try {
                        while (true) {
                            const { value, done } = await reader.read();
                            if (done) {
                                addSerial("Canceled\n");
                                break;
                            }
                            const inputValue = new TextDecoder().decode(value);
                            addSerial(inputValue);
                        }
                    } catch (error) {
                        addSerial("Error: Read" + error + "\n");
                    } finally {
                        reader.releaseLock();
                    }
                }
            } catch (error) {
                addSerial("Error: Open" + error + "\n");
            }
        }

        function addSerial(msg) {
            var textarea = document.getElementById('outputArea');
            textarea.value += msg;
            textarea.scrollTop = textarea.scrollHeight;
        }

        async function sendSerial() {
            var text = document.getElementById('sendInput').value;
            document.getElementById('sendInput').value = "";

            const encoder = new TextEncoder();
            const writer = port.writable.getWriter();
            await writer.write(encoder.encode(text + "\n"));
            writer.releaseLock();
        }
    </script>
</head>

<body>
    <h1>Web Serial API Console</h1>
    <button onclick="onConnectButtonClick()">Connect</button> 115200 Only
    <br />

    <input type="text" id="sendInput" />
    <input type="button" value="Send" onclick="sendSerial();" />
    <br />
    <textarea cols="80" rows="6" id="outputArea" readonly></textarea>

</body>

</html>

まずは全文です。細かい処理は入れていません。

接続処理

        let port;

        async function onConnectButtonClick() {
            try {
                port = await navigator.serial.requestPort();
                await port.open({ baudRate: 115200 });

                while (port.readable) {
                    const reader = port.readable.getReader();

                    try {
                        while (true) {
                            const { value, done } = await reader.read();
                            if (done) {
                                addSerial("Canceled\n");
                                break;
                            }
                            const inputValue = new TextDecoder().decode(value);
                            addSerial(inputValue);
                        }
                    } catch (error) {
                        addSerial("Error: Read" + error + "\n");
                    } finally {
                        reader.releaseLock();
                    }
                }
            } catch (error) {
                addSerial("Error: Open" + error + "\n");
            }
        }

ここらへんの処理ですね。navigator.serial.requestPort()でシリアルポートを取得して、速度やビット数などを指定してからオープンします。開いたあとはwhile(true)で無限ループして、受信したらaddSerial()関数でテキストエリアに追記しています。

追記処理

        function addSerial(msg) {
            var textarea = document.getElementById('outputArea');
            textarea.value += msg;
            textarea.scrollTop = textarea.scrollHeight;
        }

テキストエリアを取得して、メッセージを追記してから、自動スクロールするように設定しています。

送信処理

        async function sendSerial() {
            var text = document.getElementById('sendInput').value;
            document.getElementById('sendInput').value = "";

            const encoder = new TextEncoder();
            const writer = port.writable.getWriter();
            await writer.write(encoder.encode(text + "\n"));
            writer.releaseLock();
        }

テキストボックスから文字列を取得してクリア。その後シリアルポートの送信を取得してから送信後に開放しています。

html部分

<body>
    <h1>Web Serial API Console</h1>
    <button onclick="onConnectButtonClick()">Connect</button> 115200 Only
    <br />

    <input type="text" id="sendInput" />
    <input type="button" value="Send" onclick="sendSerial();" />
    <br />
    <textarea cols="80" rows="6" id="outputArea" readonly></textarea>
</body>

ここはベタ書きしています。

まとめ

結構あっさりと使えました。かんたんですね。jQueryなどを使わないでベタにJavaScriptを書くのは久しぶりです。

上記はBluetoothSerialを使って、シリアルのデータを無線で飛ばしています。受信したデータをProcessingで受信しつつグラフ化していたのですが、この手の処理がブラウザのみで完結します。

案外シリアルポートから受信したデータにパソコン側のタイムスタンプをつけて保存しつつ、グラフ化できるアプリって少ないんですよね。

次回は個人的には大好きなシリアルプロッタを作りたいところですが、もうちょっとマニアックで実用的なものを作ってみたいと思います。

続編

コメント

タイトルとURLをコピーしました