Phomemo M02SにWeb Serial APIで画像を印刷する

## この記事について
この記事は [SUPER STUDIO Advent Calendar 2023](https://qiita.com/advent-calendar/2023/superstudio) の3日目の記事になります。

## Phomemo M02Sとは
[Phomemo M02S](https://phomemo.com/ja/products/m02s-portable-printer) は中国メーカー製の「スマホ対応モバイルラベルプリンター」になります。
エアメールのデザインをしたおもちゃみたいな見た目ですが、300DPIのサーマルプリンターを小さなユニットにまとめあげていて、[Amazon等で7000円くらいで売っています](https://amzn.to/40mX3NV)(2023年12月現在)。
スマホアプリ(iOS/Android)とBluetoothで接続することでアプリ側で指定した画像などを印刷することができます。

## PCから印刷しよう
純正アプリはスマホのみで、PC用のアプリやドライバーなどは公式から提供されていません。
けれども、先人たちはいろいろと試行錯誤して印刷を実現しています。

@cure_honey さんの記事
コマンドの解析結果など、とても参考になりました。

https://qiita.com/cure_honey/items/72124ff8effddc075e9b

Stargirl Flowersさんのリポジトリ
こちらはPythonでの出力プログラム。ラスターデータの送り方など参考にさせてもらいました。

https://github.com/theacodes/phomemo_m02s

自分はブラウザ(HTML+JavaScript)からの出力にチャレンジしてみました。

## Web Serial API
最初、Bluetooth接続ということでWeb Bluetooth APIを試してみましたが、結局はBT経由のシリアル通信ということでWeb Serial APIから出力するのが正解でした。
なお、Web Serial APIは、PCのChrome、Edge、Operaしか[サポートされていません](https://caniuse.com/?search=Web%20Serial%20API)。
```javascript
    const ESC = 0x1B;
    
    port = await navigator.serial.requestPort();
    await port.open({ baudRate: 115200 });

    writer = port.writable.getWriter();

    await writer.write(new Uint8Array([ESC, 0x40, 0x02])); // reset
```
`serial.requestPort()` をコールするとブラウザがシリアル通信ポートを聞いてきます。
自分の環境の場合は以下のようになりました。

### Windowsの場合
![スクリーンショット 2023-11-04 235243.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19193/40626e95-eb80-78f6-9a08-6e7c189865e8.jpeg)
設定→Bluetoothとデバイス→プリンターとスキャナーからM02Sを追加します。
「ドライバーは使用できません」と表示されますが無視して構いません。
この状態で上記プログラムを実行すると、シリアルポートへの接続先一覧が表示されるので「M02S」を選択すれば接続は完了です。
![スクリーンショット 2023-11-05 195934.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19193/21028d70-ae75-83a0-5320-4d9a09d6949e.png)


### Macの場合
![スクリーンショット 2023-11-05 21.22.13.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19193/9c85e886-a04b-31e4-4345-7a477f1593c1.jpeg)
設定アプリのBluetoothからM02Sを追加します。
「接続済み」と表示されたあとすぐ「未接続」になりますが問題ありません。逆に接続をしてしまうとプログラムから制御できなくなります。
この状態で上記プログラムを実行すると、シリアルポートへの接続先一覧が表示されるので「cu.M02S」を選択すれば接続は完了です。
![スクリーンショット 2023-11-05 21.23.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19193/352e8aa6-f156-8b4a-6de4-b3ffc7ca5962.png)

## 画像のラスターデータ化
Phomemo M02Sは熱転写式の黒印刷のみなので、画像を印刷しようとするなら白黒2値の画像データに変換しないとモノクロ印刷できません。
そこでカラー画像 → グレイスケール画像 → 誤差拡散法によるハーフトーン処理を施す必要があります。
Pythonとかだとこの辺の画像処理のライブラリはすぐ見つかりますが、それらしいJavaScriptの画像処理ライブラリが見つかりませんでした。
ここでも先人の知恵を借りました。
@ltzz さんが書いたドンピシャの記事とサンプルソースがありましたので、そのまま移植させてもらいました。

https://qiita.com/ltzz/items/2160b5a73c206e14bde3

これでCanvasに表示できるRGBAの4バイトのピクセルデータが出来上がるので、R/G/Bチャンネルのどれも黒は0x00、白は0xFFになります。
これを白黒2値のラスターデータにするために、Rのチャンネルが0xFFならビットデータは「0」(白は印字しないので0になる)、0x00ならビットデータは「1」としてビット列をバイトデータに変換します。

```javascript
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x += 8) { // 8bitを1Byteにするため8単位
      let bit8 = 0;
      for (let i = 0; i < 8; i++) {
        let r = inputData[((x + i) + y * width) * 4]; // Rチャンネルのデータを取り出す
        bit8 |= (r & 0x01) << (7-i); // 1Byteに詰めていく
      }
      outputArray[bytes] = ~bit8; // ビット反転(白は0になるため)
      bytes++;
    }
  }
```

## 成果物

https://github.com/tanapi/m02s_js_print

[デモ](https://tanapi.github.io/m02s_js_print/)

[ファイルを選択]ボタンを押してローカルの画像ファイルを選択すると、Phomemo M02Sの解像度に合わせて横576ピクセルにリサイズして画像表示します。
画像選択後、[印刷]ボタンを押すと、シリアルポートの選択になるのでM02Sを選択すれば印刷されます。
![スクリーンショット 2023-11-05 202158.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19193/a03908ff-7cbf-7cc8-0435-8156e9fbbbf0.png)

## おまけ
なかなか思うように画像が出力されなくて、試行錯誤してデバッグしてる様子。
業務ではバックエンドでクラウドを触っているので忘れていましたが、この機器と対話しながら仕様を理解していく感覚は制御系プログラミングならではですね。
![スクリーンショット 2023-11-05 203159.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19193/3a41c34a-2ab7-e7b8-8323-5604558ea561.png)