Edited at
aptpodDay 13

BlueZのAPI/サンプルコードのメモ

この記事は Aptpod Advent Calendar 2018の13日目の記事です.

Aptpodで、エンベデッドエンジニアをしている @ffmatsuです.

弊社ではエッジデバイス(データ収集用の車載機/ラズパイ)の開発をしており、これらのエッジデバイスの動作状態を簡単に把握する仕組みのひとつとして、Bluetooth(BLE)を用いたiOS/Androidアプリとの通信機能も開発しています.

今回のAdvent Calendarでは、BlueZのサンプルコードを使ってBLE通信する方法を紹介しようと思ったのですが、そもそも BlueZのサンプルコードを紹介した記事が少なかったので、本記事では BlueZのAPIの概略やサンプルコードの解説をお送りしたいと思います.


What is BlueZ ?

http://www.bluez.org/

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機能の記事を見ると、 hciconfiggattoolsなどのコマンドツールを使った実例が多いのですが、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についての解説は、以下の記事がとても詳しいです.

http://www.silex.jp/blog/wireless/2017/01/d-bus.html

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を構築できればと思います.