【WebBluetooth】WebブラウザからBLEプリンタを操作してみた

![156462872-9c9490f4-89df-4248-b267-e6262a1f742c.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2528939/1836643d-39a2-7007-9a56-0897e701c4ad.jpeg)
# WebBluetooth使ってみた
WebBluetoothについて勉強する機会があったので、BLE（Bluetooth Low Energy）プリンタを使って試してみました。
今回はWebブラウザに入力したテキストをBLEプリンタに印刷させるプログラムを作成しました。
ソースコードはGithubにあります。
[https://github.com/Kenty250/WebBluetoothTest](https://github.com/Kenty250/WebBluetoothTest)

# WebBluetooth概要
- Webブラウザ（JavaScript）からBLE通信ができる
- HTTPS上でのみ動作する
- すべてのWebブラウザが対応しているわけではない
対応状況は[Can I use](https://caniuse.com/?search=web%20bluetooth)で確認可能。
今回試したのはAndroidのChromeブラウザです。

# ATT/GATTについて
- ATT(Attribute Protocol)はBLE通信で双方が持つ属性(Attribute)データを確認するプロトコル
- GATT(Generic attribute profile)はATTを用いてデータを構造化する方法とアプリケーション間でのデータのやり取りの方法を定義する
- **BLEのアプリケーションではデータ転送にGATTを使用する**

この辺は以下のWebページが参考になりました。
[GATTとは](https://techweb.rohm.co.jp/iot/knowledge/iot02/s-iot02/04-s-iot02/3469)

WebBluetooth（というかBLE）について調べていると、「Service」とか「Characteristic」といった言葉が出てきますが、これらはGATTを構成する要素を指しています。
![gatt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2528939/ec7a6301-68f8-dfd3-0f1d-97d36fb9b6d5.png)

# BLE通信の流れ（ペアリングまで）
BLE通信のペアリングまでの大まかな流れは以下のような感じです。
![ペアリング.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2528939/e72c7683-706a-92cf-6196-7773616ef8a3.png)
今回はプリンタを操作したいので、セントラル（デバイスをスキャンする）側のプログラムを作成します。
プログラムとしては、上図の②と③を実装する必要があります。
JavaScriptのコードは以下になります。
```javascript
printButton.addEventListener('click', function () {
  // ペアリングしていなければプリンタをスキャン・ペアリングしてから印刷
  if (printCharacteristic == null) {
    // Bluetoothデバイスを取得（取得したデバイスが画面に表示される）
    navigator.bluetooth.requestDevice({
      // ペアリングするBluetoothデバイスを取得する際のフィルタを設定(JSON形式)
      filters: [{
        // プリンタのService UUIDでフィルタをかけ、スキャンの時に関係ないBluetooth端末が表示されないようにする
        services: ['000018f0-0000-1000-8000-00805f9b34fb']
      }]
    })
    .then(device => {
      // ユーザが選択したBluetoothデバイスと接続
      return device.gatt.connect();
    })
    // プリンタのServiceを取得
    .then(server => server.getPrimaryService("000018f0-0000-1000-8000-00805f9b34fb"))
    // プリンタServiceのWriteProperty(Characteristic)を取得
    .then(service => service.getCharacteristic("00002af1-0000-1000-8000-00805f9b34fb"))
    .then(characteristic => {
      // characteristicを保持
      printCharacteristic = characteristic;
      sendTextData();
    })
    .catch(handleError);
  }
  // ペアリング済みならば印刷処理のみ実施
  else
  {
    sendTextData();
  }
});
```
GATTはサーバ－クライアント方式のアーキテクチャで、ServiceやCharacteristicを持つペリフェラルがGATTサーバになります。
```javascript
// プリンタのServiceを取得
    .then(server => server.getPrimaryService("000018f0-0000-1000-8000-00805f9b34fb"))
```
セントラルが親機でペリフェラルが子機みたいなイメージがあるのですが、GATTではペリフェラルがサーバでセントラルがクライアントになるので、この辺で私はちょっと混乱しました。。。

UUIDはServiceやCharacteristicにアクセスするための固有のIDです。
標準のUUIDはBluetooth SIGで定義されていて、以下のWebサイトで確認できます。
[https://www.bluetooth.com/specifications/assigned-numbers/](https://www.bluetooth.com/specifications/assigned-numbers/)

UUIDは無料スマホアプリで確認することもできます。
LightBlueを使ってBLEプリンタのUUIDを確認したところ、標準UUIDの共通部分が省略されていました。
![lightblue.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2528939/bc4d718f-5eee-178f-55af-8dbce455a515.png)

# BLE通信の流れ（データ送信）
続いて、データ送信です。
大まかな流れは以下のような感じです。
![write.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2528939/8662a838-049f-6b47-8b26-0ca4e87a96ad.png)
データを送信する場合は、データ送信先のデバイスの特定の領域（Characteristic）にデータを書き込むイメージです。
データを受信する場合は逆で、取得したいService-Characteristicのプロパティから値を読み取ることでデータを取得できます。

JavaScriptのコードは以下になります。
```javascript
function sendTextData() {
  let encoder = new TextEncoder("utf-8");
  // '\u000A\u000D'はラインフィード＋キャリッジリターン(5つ連続しているのは紙送りのため)
  let text = encoder.encode(message.value + '\u000A\u000D\u000A\u000D\u000A\u000D\u000A\u000D\u000A\u000D');
  // writePropertyにテキストを書き込み
  return printCharacteristic.writeValue(text);
}
```

# アプリの動作
![ezgif-1-2df33fc01b.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2528939/2ae45bb9-bf24-bfac-d9c6-f7210d6f3f9d.gif)

WebBluetoothはHTTPS上でのみ動作するため、クラウドの勉強も兼ねてAWSに動作環境を構築しました。
アプリ自体はHTML+CSS+JavaScriptなので、Amazon S3にホスティングして、AWS Certificate ManagerでリクエストしたSSL/TLS証明書をAmazon CloudFrontに統合することでHTTPS通信に対応しました。
インフラ関係の手順は本題ではないので省略しますが、構成図は以下になります。
![awsenv.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2528939/7018aa75-ac49-ae02-fe11-251bc08293ac.png)


# 今回できなかったこと
本当は画像も印刷したかったのですが、AndroidだとBLEのMTUが20byteしかない（拡張できる？）らしく、うまくいかなかったのでテキストだけのアプリになりました。
送信データを20byteずつBLEプリンタに送ってみましたが、プリンタ側でうまく印刷できず画像が崩れてしまいました。

BLEのMTUはセントラル（GATTクライアント）によって異なるようで、512byteずつ画像データを送信するとMacのChromeからは印刷できるけどAndroidのChromeからだと画像が崩れる、ということが起きました。（[このとき使用したアプリ](https://webbluetoothcg.github.io/demos/bluetooth-printer/)）

GATTクライアントのMTUの最大サイズを取得することもできるみたいですが、BLEプリンタ側で20byteずつ受信した画像データをうまく印刷できないので、どのみちAndroidのChromeでBLEプリンタに画像を印刷させるのは難しそうです。
おそらくBLE自体、通知を送ったりセンサデータを取得したりといった低容量のデータ送信をメインに想定していると思うので、画像を印刷しようとしたのがそもそも良くなかったのかもしれません。