はじめに
コロナ禍ということで、毎日体温測っています。
体温をいちいちメモるのは面倒だっていうのと、GWひたすら暇である、ということで、体温計をハックした試みをしてみました。
以下のことが書いてあります
- ラズパイでのgatttoolの使い方
- ラズパイでpythonでBLEすること
- GATTのHealth Thermometerサービスのこと
以下のことは書いていません
- そもそも、BLEとは何か、GATTとは何か、などの基本的なこと
どんなもの?
体温を測ったらLINEに通知します。
体温計→BLE→ラズパイ →WebAPI→LINE→スマホと測定値を送信します。
体温計とスマホが物理的に離れていても通知が飛ぶので、別の場所にいる人や実家のおばあちゃんの体温をリモートで把握することなんかもできるかと思います。
環境
- ラズパイ:Raspberry Pi 3 Model b+
- ラズパイのOS:Raspbian Buster with desktop 4.19
- Python 3.7.3
- blueman stable,now 2.0.8-1+b2 armhf
- bluepy 1.3.0
- A&D Bluetooth内蔵 体温計 UT-201BLE
ここまでセットアップした状態から始めます
- Wifi接続済み
- Google日本語入力「Mozc」インストール済み
体温計について
体温計はA&D Bluetooth内蔵 体温計 UT-201BLEを使います。
この体温計はBLEの**GATT(Generic attribute profile)**という世界標準規格のインタフェース仕様を実装しています。
以下のサイトでインタフェースが公開されています。
操作手順は以下のとおりです。
- ホストと体温計をペアリングする。
- 最初の1回だけやればOK。
- 体温を測り終えると自動的にホストに測定値を送信する。
- 一定時間送信をトライして送信できなければ次回の送信時にまとめて送ります。
- 時計を内蔵しているので測定時間も送ります。
今回の場合、ホストはラズパイです。
体温計とラズパイをペアリングする
体温計とラズパイをペアリングします。
①まずはbluemanを入れます。
$ sudo apt-get install blueman
$ reboot
②再起動すると、アイコンが増えているのでここからペアリングします。
③「新しいデバイスを設定」からウィザードでペアリングします。
④ペアリング成功すると、デバイスの左上に鍵のようなマークが表示されます。これでOK。
ここが結局謎だったのですが、ペアリングが失敗します。何度もやってやっとペアリングできました。私の環境だけかもしれません。ペアリングして④のように鍵マークがつく状態になっていないと、この後の作業のどこかで変なエラーになるので注意です。
BLEの確認
さっそくプログラム、ではなく、勉強も兼ねて手軽にできることから確認していきます。
hcitool
まずは体温計が発信しているアドバイスパケットをスキャンしてみましょう。
hcitoolというのがありまして、これを使うとBLEデバイスのアドバタイズパケットをスキャンすることができます。
以下のコマンドを実行します。
$ sudo hcitool lescan
コマンドを実行すると近くでアドバタイジングしているBLEデバイスがスキャンされます。
この状態で体温計をペアリングモードにしてみるとアドバイスパケットを確認できるかと思います。
私の体温計のBLEアドレスは18:93:D7:76:C9:B8
でした。
$ sudo hcitool lescan
LE Scan ...
5A:60:E6:D4:EF:94 (unknown)
5F:C1:20:2B:BE:60 (unknown)
5F:C1:20:2B:BE:60 AQtGSk1xNFF0YQ
18:93:D7:76:C9:B8 A&D_UT201BLE_76C9B8 ←これ!
gatttool
次にgatttoolで体温計に接続してみます。
gatttoolはGATTでBLEデバイスとお話しするツールです。
-bでBLEアドレスを指定して実行します。
$ gatttool -b xx:xx:xx:xx:xx:xx -I
-Iで実行すると対話モードでコマンド入力待ちになります。
connect
コマンドで接続します。
すかさず体温を測り体温計を送信モードにします。
体温計と接続するとConnection successfulとなります。
ちなみに体温計の送信モードは1分ほどで強制終了してしまうので切れたらまた体温を計って送信モードにしましょう。これが地味にめんどくさいのですがデバイスの仕様なので仕方がない...
$ gatttool -b 18:93:D7:76:C9:B8 -I
[18:93:D7:76:C9:B8][LE]> connect
Attempting to connect to 18:93:D7:76:C9:B8
Connection successful
接続したらprimary
コマンドで体温計が持っているサービスのUUIDを見てみます。
[18:93:D7:76:C9:B8][LE]> primary
attr handle: 0x0001, end grp handle: 0x000b uuid: 00001800-0000-1000-8000-00805f9b34fb
attr handle: 0x000c, end grp handle: 0x000f uuid: 00001801-0000-1000-8000-00805f9b34fb
attr handle: 0x0010, end grp handle: 0x0017 uuid: 00001809-0000-1000-8000-00805f9b34fb
attr handle: 0x0018, end grp handle: 0x0028 uuid: 0000180a-0000-1000-8000-00805f9b34fb
attr handle: 0x0029, end grp handle: 0x002b uuid: 0000180f-0000-1000-8000-00805f9b34fb
attr handle: 0x002c, end grp handle: 0xffff uuid: 233bf000-5a34-1b6d-975c-000d5690abe4
これらのUUIDは何なのか、GATTのサイトをみると、なんとなくわかります。
handle | UUID | Name |
---|---|---|
0x0001 - 0x000b | 00001800-0000-1000-8000-00805f9b34fb | Generic Access |
0x000c - 0x000f | 00001801-0000-1000-8000-00805f9b34fb | Generic Attribute |
0x0010 - 0x0017 | 00001809-0000-1000-8000-00805f9b34fb | Health Thermometer |
0x0018 - 0x0028 | 0000180a-0000-1000-8000-00805f9b34fb | Device Information |
0x0029 - 0x002b | 0000180f-0000-1000-8000-00805f9b34fb | Battery Service |
0x002c - 0xffff | 233bf000-5a34-1b6d-975c-000d5690abe4 | 不明 |
重要なのは1809のHealth Thermometerサービスです。
Health Thermometerサービスのキャラクタリスティックを確認するにはchar-desc
コマンドです。
char-desc
にはHealth Thermometerサービスのハンドル(attire handleとgrp handle値)を指定します。
またUUIDがずらずらとでてきます。
[18:93:D7:76:C9:B8][LE]> char-desc 0x0010 0x0017
handle: 0x0010, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x0011, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0012, uuid: 00002a1c-0000-1000-8000-00805f9b34fb
handle: 0x0013, uuid: 00002902-0000-1000-8000-00805f9b34fb
handle: 0x0014, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0015, uuid: 00002a1d-0000-1000-8000-00805f9b34fb
handle: 0x0016, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0017, uuid: 00002a08-0000-1000-8000-00805f9b34fb
GATTのサイトで確認します。
handle | UUID | Name |
---|---|---|
0x0010 | 00002800-0000-1000-8000-00805f9b34fb | Primary Service |
0x0011 | 00002803-0000-1000-8000-00805f9b34fb | Characteristic Declaration |
0x0012 | 00002a1c-0000-1000-8000-00805f9b34fb | Temperature Measurement |
0x0013 | 00002902-0000-1000-8000-00805f9b34fb | Descriptor |
0x0014 | 00002803-0000-1000-8000-00805f9b34fb | Characteristic Declaration |
0x0015 | 00002a1d-0000-1000-8000-00805f9b34fb | Temperature Type |
0x0016 | 00002803-0000-1000-8000-00805f9b34fb | Characteristic Declaration |
0x0017 | 00002a08-0000-1000-8000-00805f9b34fb | Date Time |
ここで大事なのが以下の2つです。
-
2a1c - Temperature Measurement
- 測定結果のキャラクタリスティックです。Indicateタイプで、つまり、データを送信してくるタイプです。
-
2902 - Descriptor
- ディスクリプタというもので、2a1cの追加属性です。具体的にはIndicateをONにしてデータ送信開始するかどうかのフラグを持っています。
- 設定をReadしたり、Writeして設定変更したりできます。
要するに、Descriptorに「送信開始」と設定するとTemperature Measurementからデータが送信されてくる、ということです。
やってみましょう。
まずはDescriptorの設定値をReadしてみます。キャラクタリスティックの値をReadするのはchar-read-hnd
コマンドです。引数にDescriptorのHandle値を指定します。
[18:93:D7:76:C9:B8][LE]> char-read-hnd 0x0013
Characteristic value/descriptor: 00 00
00 00 という値が取れています。これはIndicateがOFFの状態、という意味です。
Descriptorの値は
- 0100 → Notifyを有効にする
- 0200 → Indicateを有効にする
という意味です。Temperature MeasurementがIndicateというのはGATT仕様で決まっているので、0200にすればOKです。
設定値のWriteはchar-write-req
と叩きます。引数にDescriptorのHandle値と設定値0200を指定します。
[18:93:D7:76:C9:B8][LE]> char-write-req 0x0013 0200
Characteristic value was written successfully
Indication handle = 0x0012 value: 06 73 01 00 ff e4 07 05 02 0a 28 13 02
うまくいくとIndicateが始まり、handle=0x0012からデータが流れてきます。
handle=0x0012とは2a1c - Temperature Measurementのことです。
gatttoolの話が長くなってしまったのでまとめます。
# gatttoolの起動
$ gatttool -b xx:xx:xx:xx:xx:xx -I
# デバイスと接続
> connect
# サービスUUID一覧取得
> primary
# キャラクタリスティック一覧取得
> char-desc 開始handle 終了handle
# キャラクタリスティックの値をReadする
> char-read-hnd handle
# キャラクタリスティックの値をWriteする
> char-write-req handle data
体温を測り終わったら以下のコマンドで測定値がGETできます。
$ gatttool -b 18:93:D7:76:C9:B8 -I
> connect
> char-write-req 0x0013 0200
測定値データのフォーマット
体温計からはこんなデータが贈られてきましたが、これは一体何なのでしょうか?
06 73 01 00 ff e4 07 05 02 0a 28 13 02
データのフォーマットはここに書いてあります。
めちゃくちゃわかりにくいので書き直します。
Temperature Measurement
byte | name | |
---|---|---|
1 | Flags | データの構造を示すフラグ bit 0 - Temperature Units Flag: 0 = C1有 C2無 , 1 = C1無 C2有 bit 1 - Time Stamp Flag : 0 = C3無 , 1 = C3有 bit 2 - Temperature Type Flag : 0 = C4無 , 1 = C4有 bit 3-7 : 未使用 |
4 | C1 | Temperature Measurement Value (Celsius) - 測定値 摂氏 IEEE 11073 32bit float形式 |
4 | C2 | Temperature Measurement Value (Fahrenheit) - 測定値 華氏 IEEE 11073 32bit float形式 |
7 | C3 | Time Stamp 測定日時 - yyyy 2byte ushort - mm 1byte - dd 1byte - hh 1byte - mm 1byte - ss 1byte |
1 | C4 | Temperature Type 温度タイプ 参照 |
受信したデータをパースしてみます。
byte | data | パース結果 |
---|---|---|
Flags | 06 | 0000-0110 bit 0 = 0 : C1有 C2無 bit 1 = 1 : C3有 bit 2 = 1 : C4有 |
C1 | 73 01 00 ff | 36.1 |
C3 | e4 07 05 02 0a 28 13 | 2020/5/2 10:40:19 |
C4 | 02 | Body (general) |
C1は4byteのデータでIEEE 11073 32bit float形式です。この形式の詳しい説明は以下のサイトが参考になります。
- JavaScriptでバイナリデータを扱ってみる~IEEE-754とIEEE-11073の浮動小数点~(2/3)
- WindowsデスクトップアプリでBLEのGATTで体温計と血圧計と通信する
- to_float_from_11073_32bit_float.py
結果
- 体温=36.1℃
- 測定日時=2020/5/2 10:40:19
というデータであることがわかりました。体温と測定した日時がちゃんと取れています。いい感じです。
おつかれさまでした
キリがいいので今回はここまで。