BLE(Bluetooth Low Energy)を人力で解析する

# はじめに
この記事は『BLEってなんだろう？』というレベルの方が読み物として読むことと、『BLEの解析したいけどやり方わからんからググって見つけた』というようなレベルの方が方法を見つけるために読むことの二通りを想定しています。
前者の場合は初めから、後者の場合は [BLEの概念](#bleの概念)または[BLEの解析](#bleの解析)から読んでいただくと無駄がないかもしれません。

# BLEとは
BLEとは `Bluetooth Low Energy`のことで、その名の通りより低電力で扱えるようにしたBluetoothの拡張の一つです。
従来のBluetooth(BLEと比較してBluetooth Classicと呼ばれます)よりも通信距離が短く通信速度もかなり劣りますが、ボタン電池で何年も動作するので、昨今のIoTブームでは引っ張りだこの技術になっています。

実際にあなたがAmazonやらIKEAやらで何かしらのIoT機器・遠隔操作ができるようなアイテムを買った時、その一部はBLEで通信しているほど我々の生活に浸透しています。

# モチベーション
Appleストアを眺めていると、何やら興味深そうなアイテムがありました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236785/6746e823-c167-bed0-cc6a-f42cf8bdbd90.png)
Ember Mug( https://www.apple.com/jp/shop/product/HNNF2PA/A/ember-10-oztemperature-control-mug-2 )というらしいですが、これは良さそう！


ヒーターがコップの底に内蔵されていて、飲み物を温めてくれるスグレモノです。
早速ポチって使ってみました。

温まるのが気持ちゆっくりな気もしますが、ちょうどいい温度にしてくれるので寒くなってきた最近は重宝しています。

![イラスト.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236785/b543e586-3c54-5db1-e361-295c411bae01.png)

左がMugを操作するアプリの画面です。直感的に操作しやすく設定温度になれば通知も飛んでくるので、非常に使いやすいです。

ただ、問題が1つ。

**操作のために毎回スマホのアプリを開くのがめんどくさい**

どうやら通信はBLEで行っているようなので、通信を解析してPC上で動作するEmber Mug Controllerを作成して常駐させてみようと思います。

# 技術選定
開発・実行環境は自前のWindows11で、言語は 卍x_爆速開発_x卍 ができるPythonにします。また、BLEのためのライブラリとして[『Bleak』](https://github.com/hbldh/bleak)を選定しました。

最終的にはGUIアプリケーションにしたいので、Tkinterを用います。

# BLEの概念
まずは、BLEの主な概念を理解しておきましょう。(ここら辺は調べれば詳しい情報が出てくるので、表面を撫でる程度にとどめておきます)
文字が多くてごめんなさい

## セントラルとペリフェラル
BLEの世界には2種類の機器があります。**セントラル**と**ペリフェラル**です。
セントラルは主に機器を操作する側で、例えばスマホ・PCなどがあたります。文字通り中央に立つような機器ですね。スマホなど、場合によってペリフェラルになれる機器も多くあります。
対してペリフェラルは主に操作される側で、例えばスマートウォッチ・LEDライトなどがあたります。
ペリフェラルは同時に1つのセントラルとしか通信できませんが、セントラルは複数のペリフェラルと通信できます。

さて、セントラルはどのようにして通信できるペリフェラルを探せばよいのでしょうか。
ペリフェラルは定期的(100ms~1sが多いですが、どこかのサイトに300ms程度がちょうどいいと書かれていた気もします)に**アドバタイズ**と呼ばれる無線信号を発します。自分自身の存在を周りに宣伝するのが目的です。

このアドバタイズには、ペリフェラルのアドレスや機器名、製造企業の番号、有効なサービス(後述)一覧など重要な情報が載せられています。
これによって、機器名以外(特に製造企業の番号)の情報でその機器が望むものかどうかを識別できるようになっています。

## サービスとキャラクタリスティック
BLEでは主に**GATT**という通信方式でデータのやりとりを行い、様々な機能を**キャラクタリスティック**という概念に分割しています。
キャラクタリスティックはどういった値をどういったデータ構造で送受信するかという取り決めであり、UUIDで指定されます。
また、それぞれは

- read
- write
- notify

という3つのアトリビュートが割り当てられており、読み取り可能・書き込み可能・ペリフェラルからデータ送信可能といった属性になっています。readとwriteは同時に設定可能です。

キャラクタリスティックはすでに定義されているものと制作側が自由に定義できる領域があるので、そこらへんも考える必要があります。
例えば『圧力』や『一分あたりの歩数』はそれぞれ『00002A6D-0000-1000-8000-00805f9b34fb』『000027BA-0000-1000-8000-00805f9b34fb』と定義されていますが、『今日の晩御飯』なんてものは定義されていない(と信じたい)ので、自由な領域に定義するしかありません。

ペリフェラル内で機能を種類ごとに分けたい場合、例えば音楽再生機能付きのフィットネス器具のキャラクタリスティックを『フィットネスに関わる機能』と『音楽に関わる機能』に分けてまとめたい場合に、**サービス**という単位でくくります。

対応しているキャラクタリスティックとサービスを見れば、どのような機能が備わっているかが一目瞭然であるということですね。

ただ逆を言えば、自由領域にキャラクタリスティックが定義されまくっていると解析は非常に難しいということでもあります。

# BLEの解析
さて、今回解析にあたって使用するのは、LightBlueというアプリです。[iOS版](https://apps.apple.com/jp/app/lightblue/id557428110)と[Android版](https://play.google.com/store/apps/details?id=com.punchthrough.lightblueexplorer&hl=ja&gl=US)を見つけました。

デバイスに接続すると左のようなキャラクタリスティックの一覧が開き、それぞれが右のようになっています。

![イラス2ト.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236785/9af2cd24-6eb2-5f65-81e1-b62a441ea1b3.png)

read属性の値は読み込みができますので、適宜読み込んでみましょう。
さて、この画像の右側の値ですが、何やらマグカップ内の飲み物の温度が変わるたびに値が変わっているようです...
(これを発見するためにそこそこ時間がかかりました。何しろ20個以上のキャラクタリスティックが何を意味するか分からないので、状況を変えながらいろんな値を監視しなければならないからです...)

さて、アプリで温度を確かめ、このキャラクタリスティックの値をともに並べてみましょう
ちなみに、キャラクタリスティックは `FC540002-236C-4C94-8FA9-944A3E5353FA` になります。

```
20.0    0xD007
20.5    0x0208
21.0    0x3408
21.5    0x6608
22.0    0x9808
22.5    0xCA08
23.0    0xFC08
23.5    0x2E09
24.0    0x6009
24.5    0x9209
25.0    0xC409
25.5    0xF609
26.0    0x280A
26.5    0x5A0A
27.0    0x8C0A
27.5    0xBE0A
28.0    0xF00A
28.5    0x220B
29.0    0x540B
29.5    0x860B
```

細かい値が変わるごとに上1Byteが変わってその後下1Byteが変わってますので、リトルエンディアンっぽいなということは掴めました。
それを踏まえて、二つの値の右にキャラクタリスティックの値を一旦intに変換したものを合わせてつけてみましょう。

```
20.0    0xD007    2000
20.5    0x0208    2050
21.0    0x3408    2100
21.5    0x6608    2150
22.0    0x9808    2200
22.5    0xCA08    2250
23.0    0xFC08    2300
23.5    0x2E09    2350
24.0    0x6009    2400
24.5    0x9209    2450
25.0    0xC409    2500
25.5    0xF609    2550
26.0    0x280A    2600
26.5    0x5A0A    2650
27.0    0x8C0A    2700
27.5    0xBE0A    2750
28.0    0xF00A    2800
28.5    0x220B    2850
29.0    0x540B    2900
29.5    0x860B    2950
```

なるほど！つまり、摂氏の値を100倍した数字を送ってきているわけですね。

同様に目標の設定温度を表すキャラクタリスティックが`FC540003-236C-4C94-8FA9-944A3E5353FA`であることも判明し、これも同じように設定温度を100倍してリトルエンディアンでエンコードした値を用いているようです。
この段階で、温度の設定・取得という最低限の機能を理解することができました。


さて、`FC540012-236C-4C94-8FA9-944A3E5353FA`は`notify`でしたが、定期的に`0x01`と`0x05`が送られてきています。
特に`0x05`は飲み物を温めている最中に、`0x01`は充電用のコースターに置いていないときに多く送られてきています。

そこから、前者は温度変化、後者はバッテリーの充電量変化に関係していそうだと考えられます。

このような試行を重ねた結果、キャラクタリスティックは以下のように設定されていると推測しました。
なおUUIDは`FC5400{UUID}-236C-4C94-8FA9-944A3E5353FA`に代入することで実際のキャラクタリスティックになります。

|    UUID    |     内容     |  type |
|:----------:|:------------:|:------:|
| 0x02       | 現在温度      |  read |
| 0x03       | 設定温度      | read, write   |
| 0x04       | 摂氏or華氏    | read, write  |
| 0x07       | バッテリー    |  read |
| 0x08       | notify       |  notify |
| 0x12       | 状態         |  read, write |
| 0x14       | LEDの色       |  read |

# BLEの利用
お目当てのキャラクタリスティックとその使い方が分かったならば、あとは実装に落とし込むのみです。

まずは、マグカップと接続しなければなりませんね

```python
import asyncio
from bleak import discover

EMBER_MANUFACTURER_CODE = 0xFFFF  # 企業コード。これがキーにあれば、その企業の製品

async def main():
    devices = await discover()
    for d in devices:
        if EMBER_MANUFACTURER_CODE in d.metadata.get('manufacturer_data', {}):  # キーに存在するか確認
            user_input = input('Device {!r} found. Is this ember mug? Y/N [Y]: '.format(d.name)) or 'y'
            if user_input.lower() == 'y':
                ember = d  # あってたら使用
                break
    else:
        print('Ember mug is not found. Exiting...')  # 見つからなかったら終了
        return
    print(ember)
    print(ember.metadata)

if __name__ == '__main__':
    asyncio.run(main())

"""
output:
    Device 'Ember Ceramic Mug' found. Is this ember mug? Y/N [Y]: y
    FF:FF:FF:FF:FF:FF: Ember Ceramic Mug
    {'uuids': [], 'manufacturer_data': {65535: b'\x81'}}
"""
```

このようにするとアドバタイズを解析し、全てのペリフェラルを走査してember mugを見つけてくれます。


ember mugが見つかったら、それに接続してみましょう！

```python
from bleak import BleakClient

def decode_temperature(value: bytearray) -> float:
    return int.from_bytes(value, byteorder='little') / 100  # bytearrayを温度に変換

async def main():
    devices = await discover()
    for d in devices:
        if EMBER_MANUFACTURER_CODE in d.metadata.get('manufacturer_data', {}):
            user_input = input('Device {!r} found. Is this ember mug? Y/N [Y]: '.format(d.name)) or 'y'
            if user_input.lower() == 'y':
                ember = d
                break
    else:
        print('Ember mug is not found. Exiting...')
        return

    #-#-#-# ここから追加 #-#-#-#
    async with BleakClient(ember.address) as client:
        value = await client.read_gatt_char('FC540002-236C-4C94-8FA9-944A3E5353FA')  # 温度を取得するキャラクタリスティック
        value = decode_temperature(value)
        print(value)
        await client.disconnect()  # 忘れずに切断

"""
output:
    20.0
"""
```

これで、現在の温度を取得することができるようになりましたね！

このような感じで、目的のキャラクタリスティックから読み取ったり、逆に書き込んだりを繰り返すことでソフトウェアが動作します。

# 完成形

[GitHub](https://github.com/nagataaaas/ember-mug-controller)にコードがありますが、ember mugのすべての機能を考えうる限り操作できるようにしました。
GUIも(自前のWindowsでしかテストしてませんが)非常によく動作します。
![prod.png](https://github.com/nagataaaas/ember-mug-controller/blob/main/static/asset/screenshot1.png?raw=true)

このように、地道な作業を積み重ねることでほぼ完ぺきにアプリの機能を再現することができます。

# 終わりに
自分で自在に操作できるように作ると、自由度が広がります。
たとえばember mugは温度設定が最低でも50℃ですが、現在温度に合わせて設定温度を0℃と50℃に自動で切り替えることで疑似的にすべての温度に設定できるようになります。

ほかにも、部屋のライトがBLEで操作できるならゲーミング照明にできるかもしれませんし、ちらつく蛍光灯を再現してホラーゲームをより怖く楽しめるかもしれません。

できることが増えるって、素晴らしいですよね。
そのためにも、みんなでもっと技術力を高めていきましょう。

ここまでお付き合いいただきありがとうございました。