2
3

Web Serial API の接続ライフサイクルのハンドリング

Posted at

JINS Advent calendar 7日目も引き続きウェアラブルチームの菰田が担当します。

Web Serial API

さて、CircuitPythonで良い感じにコーディングしてプロトタイプができたら、他の人に使ってもらうフェーズがきます。しかしJINSは文系の会社なので「設定変えたい時はterminalからシリアル通信で変更してね」と言っても会話が終了してしまうため、簡単にシリアル通信を行える仕組みの需要が高いです。そんな時に強力な味方になってくれるのが Web Serial API です。Chrome(Edge)でしか動作しないという制約はあるものの、Windows/MacOS/ChromeOSでブラウザさえあれば安定して動作するので、最も敷居の低いシリアル通信手段であると言えます。

Web Serial API、大変便利なのですが、ネットによくあるサンプルコードだと手動切断や接続ロストのハンドリングを行っていないコード(再接続するにはリロードが必要)が多かったので、切断まで考慮に入れたコードを書いてみました。

コード

もうほぼ出オチなのですが、(1)ボタンを押して切断した、(2)機器と接続中にケーブルを抜いた、の2種類に反応できるフローにします。

sample.html
<!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>
</head>

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

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

<script>
    let port;
    let reader;
    let writer;
    let connectionStatus = false;

    async function onConnectButtonClick() {
        //切断時にクリックされた場合
        if(connectionStatus == false){
            try {
                port = await navigator.serial.requestPort();
                await port.open({ baudRate: 9600 });

                //errorになったらcatch (error)に飛ぶ
                connectionStatus = true;
                console.log("connected")
                document.getElementById('connectButton').innerHTML = "Disconnect"

                reader = port.readable.getReader();
                
                //繋がっている最中の処理
                //物理的に突然抜いた時port.readableがfalseになる
                //ボタンを押してCloseした時はconnectionStatusがFalseにされる(切断処理は2回目のクリック側で行われる)
                while (connectionStatus && port.readable) {
                    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");
                    }
                }
            } catch (error) {
                addSerial("Error Open: " + error + "\n");
            }
        }

        //接続時にクリックされた場合、もしくはconnectionStatus == trueの時に突然引き抜かれて上のループを抜けて流れてきた場合
        if(connectionStatus == true){
            connectionStatus = false;

            //readerのリリース(writerは都度リリースしている)
            reader.cancel();//突然引き抜かれたケースではエラーになる
            reader.releaseLock();
            await port.close()
            console.log("disconnected")
            document.getElementById('connectButton').innerHTML = "Connect"
        }
    }

    //結果をmulti-inputに反映
    function addSerial(msg) {
        const textarea = document.getElementById('outputArea');
        textarea.value += msg;
        textarea.scrollTop = textarea.scrollHeight;
    }

    //データの送信
    async function sendSerial() {
        const text = document.getElementById('sendInput').value;
        document.getElementById('sendInput').value = "";

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

        writer.releaseLock();
    }
</script>
</html>

説明

接続・切断ボタン

接続に関してはtryのところから淡々と実行されるだけです。
問題は切断で、パターンとして(1)ボタンを押した、(2)ケーブルを抜いたの2種類があります。機器からクローズされた場合は(2)と同じことが起こります。
(1)の時はif(connectionStatus == false)を通過、(2)の時はport.readablefalseになりwhileを抜けることで切断時のif(connectionStatus == true)内が実行されます。

イベントリスナ

実は自分でもあまり良いコードに見えないなと思いつつも、MDNに乗っている以下のイベントリスナが 動作する時と動作しない時がある ようでしたので、UIの変更を条件分岐内で処理しています。何か認識間違っていたらご指摘ください、、。

navigator.serial.addEventListener("connect", (e) => {
  // `e.target` に接続する、すなわち利用可能なポートのリストに加えます。
});

navigator.serial.addEventListener("disconnect", (e) => {
  // `e.target` を利用可能なポートのリストから外します。
});

最後に

わたくし、Web Bluetoothのほうもかなりいじっていた時期もあったのですが、Web Serial のほうが圧倒的に安定しています、、、。やっぱり有線は偉大ですね!

JINS Advent calendar 8日目はフワッとしたこと大好き @Takuma_Sato がお送りします。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3