この記事は Aptpod Advent Calendar 2018の13日目の記事です.
Aptpodで、エンベデッドエンジニアをしている @ffmatsuです.
弊社ではエッジデバイス(データ収集用の車載機/ラズパイ)の開発をしており、これらのエッジデバイスの動作状態を簡単に把握する仕組みのひとつとして、Bluetooth(BLE)を用いたiOS/Androidアプリとの通信機能も開発しています.
今回のAdvent Calendarでは、BlueZのサンプルコードを使ってBLE通信する方法を紹介しようと思ったのですが、そもそも BlueZのサンプルコードを紹介した記事が少なかったので、本記事では BlueZのAPIの概略やサンプルコードの解説をお送りしたいと思います.
What is BlueZ ?
Linuxの標準のBluetoothプロトコルスタックです.
ラズパイOSのRaspbianでもBlueZが使われているので、このBlueZを制御する事でラズパイ上でBluetoothの各機能(BLE通信など)を使う事ができます.
BlueZのソースコード
BlueZのソースコードは以下のリポジトリで公開されています.
https://git.kernel.org/pub/scm/bluetooth/bluez.git
今回は、最新のRaspbianでも採用されている、 v5.47
をターゲットにお話します.
以下のページから実際にソースコードをダウンロードした上で記事を読んで頂けると理解しやすいかと思います.
http://www.bluez.org/release-of-bluez-5-47/
BlueZのフォルダ構成
BlueZのソースコードを見ると、以下のようなフォルダ構成になっています.
├── client
├── doc
├── emulator
├── gdbus
├── gobex
├── monitor
├── obexd
├── peripheral
├── plugins
├── profiles
├── src
├── test
├── test-driver
├── tools
└── unit
and more ...
この中で、注目すべきフォルダを以下に示します.
- doc
- D-Bus InterfaceのAPI仕様を記載したテキスト
- test
- 動作確認用のPythonのサンプルコード
- tools
-
hciconfig
などの、コマンドツールのソースコード
-
- client
-
bluetoothctl
のソースコード
-
BlueZのAPI仕様について
他のBlueZを使用したBLE機能の記事を見ると、 hciconfig
や gattools
などのコマンドツールを使った実例が多いのですが、BlueZの機能を十分に活用するのであれば、BlueZのAPIを使った方がよいかと思います.
BlueZのAPI仕様は、 doc/
フォルダに展開されています.
https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc?h=5.47
これらのAPI仕様ですが、D-Busのインターフェースで記載されています.
D-Bus
D-Busについての解説は、以下の記事がとても詳しいです.
https://www.silex.jp/library/blog/20170126-1
D-Bus制御のプログラミングは上述の記事通り、とても面倒くさいです.
ただし、Pythonに関しては割と直感的に記述でき、BlueZのサンプルコードには各機能毎のAPIを実際に動かす事ができるPythonのコードがあるので、これらのサンプルコードを使ってAPIの実動作を確認しながらプログラミングしていくと幸せになれます.
docフォルダの内容
doc/
フォルダには、BlueZのD-Bus APIの仕様が記載されたドキュメントが格納されています.
以下に各Profileに関連するファイルの要約を示します.
各Profileの詳細は、実際にファイルを参照してください.
共通
BLE/ Classic 両方で使用するAPIです
file | 概略 |
---|---|
adapter-api.txt | 自デバイス(Adapter)を制御するAPI |
agent-api.txt | ペアリング処理のAPI |
device-api.txt | 接続相手のデバイスを制御するAPI |
BLE関連
BLEのGATT/Advertisingの制御や、BLE専用のProfile/Serviceを実装したAPIです.
file | 概略 |
---|---|
advertising-api.txt | Advertising関連のAPI |
gatt-api.txt | GATT Prfileの設定API |
alert-api.txt | Phone Alert Status (PASP)のAPI |
cyclingspeed-api.txt | Cycling Speed and Cadence のAPI |
health-api.txt | HealthのAPI |
heartrate-api.txt | Heart RateのAPI |
proximity-api.txt | Proximity のAPI |
thermometer-api.txt | Health Thermometer のAPI |
Classic関連
Classic (Bluetooth BR/EDR)関連のProfileを制御するAPIです.
file | 概略 |
---|---|
input-api.txt | HID ProrileのAPI |
media-api.txt | A2DP/AVRCPを抽象化したAPI |
network-api.txt | PANなどのNetwork 関連のAPI |
obex-agent-api.txt | PBAPなどでも使われるOBEX protocolのAPI |
obex-api.txt | OBEX全体を制御するAPI |
profile-api.txt | SPPなど、RFCOMMをProfileとして登録するAPI |
sap-api.txt | SIM Access ProfileのAPI |
ちなみに、Hands Free Profile (HFP)関連のAPIは
ofonoを使う事になります.
testフォルダの内容
test/
フォルダ内には、上述のD-Bus APIをpythonから呼び出すサンプルコードが展開されています.
これらのコードを実際に実行すると、各APIの簡単な動作を確認する事が可能になります.
以下に、各サンプルコードの概要を記載します
共通
file | 概略 |
---|---|
bluezutils.py | 他のpythonコードからimportされるutil群 |
dbusdef.py |
bluezutils.py 内のfind_adapter() を試しに呼ぶスクリプト.BlueZが認識しているBluetoothアダプタ(hci*)を一覧表示する |
test-adapter | Adapter インターフェースの各Property (デバイス名やアドレス)を取得/表示するスクリプト |
monitor-bluetooth | Adapter, Deviceのinterfaceから発行される - PropertyChanged - Added - Removed の各シグナルをシグナルハンドラに登録し、メッセージ内容を表示するスクリプト |
test-manager |
org.freedesktop.DBus.ObjectManager (D-Busの共通インターフェース)の- Added - Removed を表示する |
test-discovery | Adapterに対して StartDiscovery() を要求し、周辺に存在する相手デバイスを探索する |
list-devices | Adapterのinterfaceに対して GetManagedObject() を要求し、現在、BlueZが認識している相手デバイス(ペアリング済み、発見済み)の一覧を表示する |
simple-agent | ペアリング処理を代行するAgent をD-Busに登録し、ペアリング処理(Numeric Comparisonなど)を実行するスクリプト |
test-device | 相手デバイス(ペアリング済み、発見済み)に対して、接続要求や削除、リスト取得などのAPIを要求する |
BLE (Low Energy向け)
BLE向けのAPIのテストコードです.
後半のtest-*
で始まる、Profile毎のテストコードの詳細は省略します.
(GATT Profileの一覧はこちら)
file | 概略 |
---|---|
example-advertisement | BLEのAdvertisingのサンプル. v.5.43ではExperimental 扱いなので、そのままでは動きません. |
example-gatt-client | 接続中のデバイス(GATT Server)からHeart Rate Serviceを受信するClientを起動 |
example-gatt-server | 擬似的にGATT Server を立ち上げる |
test-gatt-profile | GATT Profile APIのテスト. Gatt Clientを登録する事で、自動接続などの機能が提供される |
test-cyclingspeed | Cycling Speed and Cadenceのテスト |
test-health | Healthのテスト |
test-heartrate | Heart Rateのテスト |
test-proximity | Proximity Profileのテスト |
test-thermometer | Health Thermometerのテスト |
test-alert | Phone Alert Statusのテスト |
Classic向け
こちらは従来型と呼ばれるProfileのサンプル/テストコードです.
需要も少なそうなので、さらっと紹介します。
(Classic Profileの一覧はこちら)
file | 概略 |
---|---|
test-profile | シリアル通信プロファイル(SSP)やHands Free Profile(HFP)などで使われる共通のProtocolであるRFCOMM. これをProfileという概念で制御しています. このテストでは引数でパラメータを指定して、Profileを登録します. (そのあと、Profileに対して NewConnection() を要求すれば接続されるはず) |
pbap-client | PhoneBook Access Profile (PBAP)のOBEX Clientを接続するサンプルです. その後、対話方式で電話帳のindexやサイズを選択すると、接続中の携帯電話の電話帳情報(vCard)を表示できます. |
map-client | Message Access Profile (MAP)のClientサンプル. こちらも中身は OBEXのClientを接続します. |
ftp-client | File Transfer Profile(FTP)のClientサンプル |
opp-client | Object Push Profile (OPP)の Clientサンプルです. 古い仕様なので省略 |
sap_client.py | SIM Access Profileです. かなり古いプロファイルですね |
simple-endpoint | 電話音声(HFP)やストリーミング転送(A2DP)の音声出力先も制御する Media のAPIのサンプル |
simple-player | 上述のMedia を制御するMediaPlayer のサンプル. このサンプルを使って、ラズパイでBluetoothスピーカにする記事がありますね |
test-hfp | Profileを使ってHFPのRFCOMMを接続し、ATコマンドで制御するサンプルです. |
test-network | 接続中のデバイスに対して Personal Area Network(PAN)などの接続を要求するテスト. Network APIのConnect() を実行しています. |
test-nap | こちらはPANなどのServer側のテスト. NetworkServer APIの Register() を実行しています. |
サンプルコードを実際に動かしてみる
実際に、test/
フォルダ以下の各pythonのコードを動かしてみましょう
list-devices
test
フォルダのlist-devices
を覗いてみると、以下の実装を確認できます
manager = dbus.Interface(bus.get_object("org.bluez", "/"),
"org.freedesktop.DBus.ObjectManager")
...
objects = manager.GetManagedObjects()
D-Busのサービス org.bluez
で管理しているObjectsから、デバイス情報を出力するコードだとわかります。
実際に動かしてみます.
$ ./list-devices
[ /org/bluez/hci0 ]
Name = ubuntu
Powered = 1
Modalias = usb:v1D6Bp0246d052B
DiscoverableTimeout = 0
Alias = ubuntu
PairableTimeout = 0
Discoverable = 0
Address = 00:XX:XX:XX:XX:XX
Discovering = 0
Pairable = 1
Class = 786700
UUIDs = 0x1112 0x1801 0x110e 0x1800 0x1200 0x110c 0x110a 0x110b
[ /org/bluez/hci0/dev_XX_XX_XX_XX_XX_EC ]
Name = M720 Triathlon
Paired = 1
Modalias = usb:v046DpB015d0007
Adapter = /org/bluez/hci0
ServicesResolved = 1
Appearance = 962
LegacyPairing = 0
Alias = M720 Triathlon
Connected = 1
UUIDs = 0x1800 0x1801 0x180a 0x180f 0x1812 00010000-0000-1000-8000-011f2000046d
Address = XX:XX:XX:XX:XX:EC
Blocked = 0
Trusted = 1
Icon = input-mouse
[ /org/bluez/hci0/dev_XX_XX_XX_XX_XX_26 ]
...
bluezの getManagedObjects()
の結果を表示できました.筆者の環境では、前半が自デバイス(ubuntu)、後半がペアリング済みデバイス(M720)の情報になりました.
どのようにPythonからD-Busを呼び出しているのか?を詳細に説明するのは難しいのですが、ここではPythonのサンプルコードでBlueZを制御できている. という事を実感して頂ければと思います.
(ちなみに、dbus-pythonのチュートリアルはこちらを参照してください.)
monitor-bluetooth
test/
フォルダ以下のmonitor-bluetooth
は、BlueZのD-Busのやり取りを出力するテストコードです.
試しに、周辺のBluetooth機器を探索して、D-Busの受信を確認してみます.
ターミナルを2個開いて、一つのターミナルで monitor-bluetooth
を実行します.
次に、別のターミナルで、bluetoothctl
を使ってscan on
を実行します.
$ bluetoothctl
[HHKB-BT]# scan on
Discovery started
[CHG] Controller 00:E1:8C:23:60:C1 Discovering: yes
[NEW] Device XX:8A:9F:9D:AB:EE XX-8A-9F-9D-AB-EE
[NEW] Device 4F:XX:9D:85:59:2D 4F-XX-9D-85-59-2D
[NEW] Device 66:15:XX:01:FB:FD 66-15-XX-01-FB-FD
[NEW] Device 46:E8:80:XX:20:5A 46-E8-80-XX-20-5A
[NEW] Device 56:DF:07:F3:XX:F0 56-DF-07-F3-XX-F0
[NEW] Device 8C:85:90:77:1A:XX 8C-85-90-77-1A-XX
[NEW] Device XX:43:62:9D:FE:AA XX-43-62-9D-FE-AA
[NEW] Device 4D:XX:14:F0:4B:78 4D-XX-14-F0-4B-78
すると、monitor-bluetooth
を実行しているターミナル側に、
BlueZに関するD-Busのメッセージの内容が出力されてきます.
$ ./monitor-bluetooth
{Adapter1.PropertyChanged} [/org/bluez/hci0] Discovering = 1
{Added org.bluez.Device1} [/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX]
Paired = 0
ServicesResolved = 0
Adapter = /org/bluez/hci0
LegacyPairing = 0
Alias = XX_XX_XX_XX_XX_XX
Connected = 0
UUIDs = dbus.Array([], signature=dbus.Signature('s'), variant_level=1)
Address = XX:XX:XX:XX:XX:XX
RSSI = -65
Trusted = 0
Blocked = 0
{Added org.bluez.Device1} [/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX]
Paired = 0
ServicesResolved = 0
Adapter = /org/bluez/hci0
LegacyPairing = 0
Alias = XX-XX-XX-XX-XX-XX
...
...
実際に通信してみると、BlueZがどのようなメッセージ(Signal)をD-Bus上で通知してくるのかがわかるので、使えそうなメッセージがあれば、signalハンドラをdbus-pythonに登録して検出する事ができます.
また、デバッグ用途でも使えるかも.
example-gatt-server
今回、一番の目的だった、BLEのGATT通信が動作するサンプルコードです.
このサンプルコードは、GATT Serverが動作します.
コードを覗いて見ると、以下のGATT Serviceを立ち上げています.
- Battery Service (UUID=
0x1801
) - Heart Rate (UUID=
0x180D
) - Test Service (UUID=
12345678-1234-5678-1234-56789abcdef0
)
実際にBattery ServiceやHeart Rate Serviceは実装しておらず、あくまでもエミュレートしたものになっています.
例えばHeart Rateはランダムな値を通知しますし、Battery Serviceは5秒毎に値が減っていき、その変化を通知する実装になっています.
GObject.timeout_add(5000, self.drain_battery)
def drain_battery(self):
if self.battery_lvl > 0:
self.battery_lvl -= 2
if self.battery_lvl < 0:
self.battery_lvl = 0
print('Battery Level drained: ' + repr(self.battery_lvl))
self.notify_battery_level()
return True
では、本当にBLEの通信ができるか試してみましょう.
サンプルプログラムを、以下の通りターミナルで実行します.
$ ./example-gatt-server
Registering GATT application...
GetManagedObjects
GATT application registered
Battery Level drained: 98
Battery Level drained: 96
Battery Level drained: 94
Battery Level drained: 92
Battery Level drained: 90
Battery Levelが徐々に減ってきていますね.
早速BLEを接続したいのですが、GATT Serverを起動しただけでは、Client側から発見できません.なので、Advertisingする事で周囲のデバイスに対して自分の存在を通知します.
別のターミナルを立ち上げて、以下のコマンドを実行してAdvertisingを開始します.
sudo hciconfig hci0 leadv
この状態で、スマートフォンのBLEテストアプリから、GATT Serverに接続します.
確認には、以下のアプリを使用しました。
- iOS: BLE Scnanner 4.0
- Android: nFR Connect for Mobile
実際に接続した時のスクショです。
Battery LevelやHeart Rateが取れていますね!
最後に
誤記/追記情報がありましたらご指摘頂けるとうれしいです.
次回は、実際にこのサンプルコードを使って、ラズパイに接続した温度センサーの値を通知するGATT Serverを構築できればと思います.