JINS Advent calendar 7日目も引き続きウェアラブルチームの菰田が担当します。
Web Serial API
さて、CircuitPythonで良い感じにコーディングしてプロトタイプができたら、他の人に使ってもらうフェーズがきます。しかしJINSは文系の会社なので「設定変えたい時はterminalからシリアル通信で変更してね」と言っても会話が終了してしまうため、簡単にシリアル通信を行える仕組みの需要が高いです。そんな時に強力な味方になってくれるのが Web Serial API です。Chrome(Edge)でしか動作しないという制約はあるものの、Windows/MacOS/ChromeOSでブラウザさえあれば安定して動作するので、最も敷居の低いシリアル通信手段であると言えます。
Web Serial API、大変便利なのですが、ネットによくあるサンプルコードだと手動切断や接続ロストのハンドリングを行っていないコード(再接続するにはリロードが必要)が多かったので、切断まで考慮に入れたコードを書いてみました。
コード
もうほぼ出オチなのですが、(1)ボタンを押して切断した、(2)機器と接続中にケーブルを抜いた、の2種類に反応できるフローにします。
<!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.readable
がfalse
になり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 がお送りします。