Web Serial APIからRaspberry Pi PicoをREPLを介して操作

## 今回の内容

[前回](https://qiita.com/asd_m/items/69b0e28b4f5148fc2dae)はWeb Serial APIの基礎の基礎について解説しました。今回はWeb Serial APIを使用して、実際にRaspberry Pi PicoにインストールしたMicroPythonのREPL（Read-Eval-Print Loop）と通信を行ってみます。

## 準備

当然ですが、MicroPythonをインストールしたRaspberry Pi Picoが必要です。インストール方法に関しては検索すれば非常に多くの情報が見つかると思いますので、改めて解説するまでもないでしょう。

## REPL（Read-Eval-Print Loop）とは？

ごく簡単にいえば、入力した文字列を直接実行し、結果を表示できる、インタプリタ高級言語の対話的実行環境です。かなり昔の話になりますが、MS-DOS以前にROMベーシックがPCに組み込まれていた頃、スイッチを入れると出てくるBASICのプロンプト画面が近い環境だったと思います。

このREPL機能はRaspberry Pi Picoのようなマイコンの開発には非常に便利ですが、今回注目するのは以下の2点です。

- Raspberry Pi Picoは（BOOTSELボタンを押さずに）PCとUSB接続すると、自身をシリアルポートとして認識させ、そのシリアルポート経由でREPLを操作可能にしている
- import文もREPLで実行可能であり、importした内容はその後のREPL上で利用できる

特に重要なのは後者です。つまり、（boot.py等で自動起動しているプログラムがなければ）Picoに保存済みの`.py`ファイルをREPLからimportし、定義された関数等をそのままREPL上で利用できるということになります。

## 実際にWeb Serial APIからやってみる

ということで、[今回のソース](https://github.com/DBA-Z21A/raspico_wifi_set_repl/blob/main/web/frontend/pico_repl.html)をご覧ください。  
※このソースでは、わかりやすさ重視で変数やオブジェクトの多くをグローバルに直書きしています。

このソースで実現しているのは、以下の機能です。

1. ブラウザのWeb Serial APIを呼び出し、portオブジェクトを作成する（前回の内容）
2. portオブジェクトから送信用・受信用のオブジェクトを生成する
3. 送信および受信処理を実装する  
   3-1. 送信処理は送信用オブジェクトに文字列を渡す形で実装  
   3-2. 受信処理は受信用オブジェクトに対して「何か文字列が送られてきたら受信バッファに格納する」処理を無限ループ内で行う  
4. Web Serial APIとREPLが期待通り動作するか確認するため、「import sys」「print(sys.version)」をシリアルポート経由で送信し、その結果を受信・表示する

### 送信用オブジェクトと受信用オブジェクトの作成

まず通信速度等（今回は必要ありませんが、パリティやビット長、ストップビット等の設定が必要ならここで行います）を設定してポートを開き、portオブジェクトから送受信用のオブジェクトを生成します。

```javascript
await port.open({ baudRate: 115200 });
reader = port.readable.getReader();
writer = port.writable.getWriter();
```

### 送信処理

送信用オブジェクトにUTF-8エンコードした文字列を渡して送信します。

```javascript
if (writer) {
  const dataArray = new TextEncoder().encode(data);
  await writer.write(dataArray);
}
```

### 受信処理

シリアル通信は非同期・双方向なので、受信処理は無限ループで待ち受けます。

```javascript
while (port && port.readable) {
  const { value, done } = await reader.read();
  if (done) {
    break;
  }
  if (value) {
    const textDecoder = new TextDecoder();
    const receivedText = textDecoder.decode(value);
  }
}
```

受信用オブジェクトの `read` メソッドは `{ value, done }` を返します。`done` が true ならポートが閉じられたことを意味し、ループを脱出します。  
`value` には受信したバイト列が含まれており、やはりUTF-8でデコードしてから受信用バッファに格納しています。

なお、実務で使用するコードではUSBケーブルの切断や他の操作によるポートのクローズに備えて、`try...catch` で例外処理を行うべきです。また以下のようなループに入る前のチェックも必須です。

```javascript
while (port && port.readable) {
```

### 処理終了（ポートを閉じる）

通常、ブラウザを閉じればポートは自動的に閉じられますが、JavaScriptから明示的に閉じるには以下のようにします。

```javascript
if (reader) {
  await reader.cancel();
  await reader.releaseLock();
  reader = null;
}
if (writer) {
  await writer.close();
  await writer.releaseLock();
  writer = null;
}
if (port && port.readable) {
  await port.readable.cancel();
}
if (port) {
  await port.close();
  port = null;
}
```

シリアル通信は双方向なので、送信／受信用の各オブジェクトにも個別にクローズ処理を施しています。

---

### REPLとの通信

Raspberry Pi PicoのREPLでは、プロンプトとして `>>> ` が表示されます。この文字列を検知してREPLの準備完了を判定しています。

```javascript
async function waitForPrompt() {
  return new Promise((resolve) => {
    const checkPrompt = () => {
      if (
        receivedLogDiv.textContent.endsWith(">>> \n") ||
        receivedLogDiv.textContent.endsWith(">>> ")
      ) {
        resolve();
      } else {
        setTimeout(checkPrompt, 50); // 50ms ごとにプロンプトをチェック
      }
    };
    checkPrompt();
  });
}
```

---

## 次回の予定

次回は、今回の実装内容を汎用性の高い JavaScript クラスに書き直し、GUI 部分を Vue.js で実装してみる予定です。