Raspberry Pi を SORACOM Beam から AWS IoT に接続する

 前回の記事で Raspberry Pi を AWS SDK を使って AWS IoT に接続してみましたが、今回は SORACOM Beam 経由で AWS IoT に接続してみたいと思います。

soracom.jp

 SORACOM Beam はデバイスからの接続先の設定やプロトコル変換処理をオフロードできるサービスで、例えばデバイスの中に認証情報を置いておかなくても、Beam から接続先にアクセスする際に認証用の情報を追加することができるので、セキュリティの面でも有効なサービスです。

 今回は下記のガイドを参考に Raspberry Pi から MQTT で AWS IoT に接続し、Amazon SNS からメールを送信する処理を実行してみたいと思います。

dev.soracom.io

AWS側の設定

 AWS IoT のモノの登録については、前回登録したものをそのまま使います。

 また、AWS IoT のルール作成時にIAMのロールの作成・参照権限が必要になりますので、今回はとりあえずIAMFullAccessポリシーをアタッチしてフル権限を付与しておきます。

f:id:akanuma-hiroaki:20170706091200p:plain

 そしてメール通知を行うためのSNSトピックを作成しておきます。サブスクリプション作成時の Protocol は Email を指定し、メールアドレスを登録しておきます。

f:id:akanuma-hiroaki:20170708104430p:plain

 ここではSNSのトピックとサブスクリプションの作成方法の詳細は割愛しますが、下記AWSドキュメントに記載されています。 

AWS IoT ルールの作成

 デバイスから送信された内容をハンドリングするためのルールを作成します。AWS IoTコンソールの左側のメニューから、 ルール をクリックします。

f:id:akanuma-hiroaki:20170706091933p:plain

 まだルールが作成されていない場合は下記のような画面が表示されますので、 ルールを作成する をクリックします。

f:id:akanuma-hiroaki:20170706092045p:plain

 ルールの作成フォームが表示されますので、「名前」と「説明」に自分がわかりやすい内容を設定します。

f:id:akanuma-hiroaki:20170706092330p:plain

 画面下部の「メッセージのリソース」の各項目で今回対象とするデータの条件を設定します。

 SQLバージョンはデフォルトのままです。また、今回はひとまず全データを対象とするので、属性には「*」を、トピックフィルターには「#」を設定します。条件はブランクのままにします。

 そしてデータが送信された時の処理を設定するために、 アクションの追加 をクリックします。

f:id:akanuma-hiroaki:20170707005055p:plain

 アクションの選択画面が表示されますので、今回は SNSプッシュ通知としてメッセージを送信する`` を選択し、画面下部のアクションの設定``` ボタンをクイックします。

f:id:akanuma-hiroaki:20170707005647p:plain

 アクションの設定画面では、SNSターゲットとしてあらかじめ作成しておいたSNSトピックを選択し、メッセージ形式はRAW形式を選択します。

 そして、SNSリソースにAWS IoTへのアクセス権限を付与するためのロールを作成するため、 新しいロールの作成 をクリックします。

f:id:akanuma-hiroaki:20170707055500p:plain

 ロール名の入力フォームが表示されますので、任意のロール名を入力して 新しいロールの作成 をクリックします。

f:id:akanuma-hiroaki:20170707060041p:plain

 作成したロールが選択できるようになりますので、選択して アクションの追加 をクリックします。

f:id:akanuma-hiroaki:20170707060413p:plain

 下記のようにルールにアクションが追加されたことが確認できます。

f:id:akanuma-hiroaki:20170707060645p:plain

SORACOM Beam の設定

 まずは SORACOM Beam からのアクセス先となる、 AWS IoT 側のカスタムエンドポイントを確認しておきます。カスタムエンドポイントは AWS IoT を使用する各AWSアカウントごとに割り当てられるエンドポイントです。

 AWS IoT コンソールの左下の 設定 をクリックします。

f:id:akanuma-hiroaki:20170707075144p:plain

 するとカスタムエンドポイントが確認できます。

f:id:akanuma-hiroaki:20170707075152p:plain

 次に SORACOM Beam の設定を行います。

 SORACOM Consoleのメニューから Cellular -> Groups を選択し、対象のAir Sim が属するグループを選択したら、Basic settingsタブの SORACOM Beam をクリックして展開し、 から MQTT entry point を選択します。

f:id:akanuma-hiroaki:20170707073555p:plain

 入力フォームが表示されますので、 Configuration name には後で自分がわかりやすい任意の名前を設定します。

 Protocol は MQTTS、Host name には先ほど確認した AWS IoT のカスタムエンドポイント、Port number には 8883 を設定します。また、AWS IoT の認証に証明書を使うので、 Client cert を ON にします。そして、証明書を登録するために + をクリックします。

f:id:akanuma-hiroaki:20170707074014p:plain

 登録には秘密鍵、証明書、ルート証明書が必要になります。このうち秘密鍵、証明書は前回ダウンロードした connect_device_package.zip に含まれる下記ファイルを使います。

  • 秘密鍵:raspberry_pi.private.key

  • 証明書:raspberry_pi.cert.pem

 また、ルート証明書はSymantec社のサイトからダウンロードできます

https://www.symantec.com/content/en/us/enterprise/verisign/roots/VeriSign-Class%203-Public-Primary-Certification-Authority-G5.pem

 これらの内容ををそれぞれ Key、Cert、CA のフォームに入力します。Typeはデフォルトで X.509 certificate が選択されているのでそのままにしておきます。Credentials set ID と Description はわかりやすい内容を任意で登録します。入力が終わったら Register をクリックします。

f:id:akanuma-hiroaki:20170707074356p:plain

 するとプルダウンで先ほど登録した Credentials Set が選択できるようになっているので選択し、 Save をクリックします。

f:id:akanuma-hiroaki:20170707074530p:plain

 SORACOM Beam の設定が追加されたことが確認できます。

f:id:akanuma-hiroaki:20170707074758p:plain

Raspberry Pi から接続してみる

 ここまでで設定は完了なので、 Raspberry Pi から接続してみます。今回は MQTT Broker のオープンソース実装である Mosquitto を使用しますので、下記コマンドでインストールします。

pi@raspberrypi:~ $ sudo apt-get install mosquitto mosquitto-clients

 そして SORACOM Air で接続した上で、下記コマンドを実行してデータを送信します。

pi@raspberrypi:~ $ mosquitto_pub -d -h beam.soracom.io -t beamdemo -m "Hello, World from AWS IoT!"
Client mosqpub/1224-raspberryp sending CONNECT
Client mosqpub/1224-raspberryp received CONNACK
Client mosqpub/1224-raspberryp sending PUBLISH (d0, q0, r0, m1, 'beamdemo', ... (26 bytes))
Client mosqpub/1224-raspberryp sending DISCONNECT

 すると下記のようにメールが送信されます。

f:id:akanuma-hiroaki:20170707082336p:plain

 ひとまずこれで Raspberry Pi から SORACOM Beam を経由して AWS IoT に送信されたデータがルールによって処理され Amazon SNS へ送信され、メールが送られたことが確認できました。Raspberry Pi からの送信時は特に証明書等は指定せず、 SORACOM Beam に登録した証明書が使われていますので、 Raspberry Pi に証明書を置く必要なく処理が行えるようになっています。

 デバイスからの送信だけであれば SORACOM Funnel でも良いかもしれませんが、サーバからデバイスへの送信も行いたい場合は SORACOM Beam を使う必要があります。今回はデバイスからの送信だけでしたが、デバイスへの送信も試してみたいと思います。

 また、先日の SORACOM Conference 2017 で SORACOM Inventory が発表されました。

新サービス: SORACOM Inventory を発表 - SORACOM Blog

 まだ Limited Preview ということで実際には使えていませんが、用途としては AWS IoT に近いものだと思いますので、 SORACOM Air を使うデバイスであれば、 SORACOM Inventory だけでも良いのかもしれません。とりあえず Limited Preview の利用申請はしてみたので、使えるようになったら試してみたいと思います。

Raspberry Pi を AWS IoT に接続する

 今回はRaspberry PiをAWS IoTに接続してみたいと思います。AWS IoTとは簡単に言うと、IoTデバイスをAWS上のプラットフォームに登録しておき、デバイスの状態を記録するとともに、複数のデバイス間やデバイスとAWSサービス間の通信のハンドリングを行うことができるサービスです。

docs.aws.amazon.com

 デバイスとAWS IoTプラットフォーム間のプロトコルとしては MQTT, HTTP, WebSocket に対応していて、AWSサービスとの連携としては Lambda や DynamoDB、Kinesis、SNSなどに対応しています。

 今のところまだどういった連携をしていくか決めていませんが、ひとまずは Raspberry Pi を AWS IoT に登録して、接続ができるところまでを行ってみたいと思います。

AWS側の環境構築

 AWS IoT コンソールからデバイスの登録等を行うにはAWS IoT関連の権限が付与されている必要があります。今回はテストということで、 AWSIoTFullAccess ポリシーをアタッチして、AWS IoTに関する全ての権限を付与しました。実際のサービス提供時は最低限の権限に絞って付与した方が良いかと思います。

docs.aws.amazon.com

 権限が付与されたら該当のユーザでAWSマネジメントコンソールにログインし、サービスのリストから AWS IoT を選択して AWS IoT のコンソールへ移動します。

f:id:akanuma-hiroaki:20170701120135p:plain

 今すぐ始める をクリックします。

f:id:akanuma-hiroaki:20170701120553p:plain

 AWS IoT コンソールが表示されますので、 「AWS IoT に接続する」の 接続オプションの表示 をクリックします。

f:id:akanuma-hiroaki:20170701121115p:plain

 今回は Raspberry Pi を接続するので、「デバイスの設定」の 今すぐ始める をクリックします。

f:id:akanuma-hiroaki:20170701121210p:plain

 ステップの案内ページが表示されますので、 今すぐ始める をクリックします。

f:id:akanuma-hiroaki:20170701121336p:plain

 プラットフォームとSDKの選択画面が表示されますので、今回は Linux と Python を選択して、画面右下の 次へ をクリックします。

f:id:akanuma-hiroaki:20170701133125p:plain

 モノ(IoTデバイス)の登録画面になりますので、名前を決めて入力し、 次のステップ をクリックします。

f:id:akanuma-hiroaki:20170701133847p:plain

 作成されるモノの情報と接続キットの情報が表示されますので、 Linux/OSX をクリックして接続キットをダウンロードします。ダウンロードすると画面右下の 次のステップ ボタンが活性化しますので、クリックして次へ進みます。

f:id:akanuma-hiroaki:20170701134046p:plain

 デバイス上でのテスト手順が表示されます。

f:id:akanuma-hiroaki:20170701134441p:plain

Raspberry Pi 側からの接続操作

 ダウンロードした接続キットをscp等で Raspberry Pi 上に転送し、上記手順を実行します。

pi@raspberrypi:~/aws_iot $ ls
connect_device_package.zip
pi@raspberrypi:~/aws_iot $ 
pi@raspberrypi:~/aws_iot $ unzip connect_device_package.zip 
Archive:  connect_device_package.zip
  inflating: raspberry_pi.private.key  
  inflating: raspberry_pi.public.key  
  inflating: raspberry_pi.cert.pem   
  inflating: start.sh                
pi@raspberrypi:~/aws_iot $ 
pi@raspberrypi:~/aws_iot $ ls
connect_device_package.zip  raspberry_pi.cert.pem  raspberry_pi.private.key  raspberry_pi.public.key  start.sh
pi@raspberrypi:~/aws_iot $ 
pi@raspberrypi:~/aws_iot $ chmod +x start.sh 
pi@raspberrypi:~/aws_iot $ 
pi@raspberrypi:~/aws_iot $ ls -l
total 20
-rw-r--r-- 1 pi pi 3620 Jul  1 02:05 connect_device_package.zip
-rw-r--r-- 1 pi pi 1224 Jul  1 02:01 raspberry_pi.cert.pem
-rw-r--r-- 1 pi pi 1679 Jul  1 02:01 raspberry_pi.private.key
-rw-r--r-- 1 pi pi  451 Jul  1 02:01 raspberry_pi.public.key
-rwxr-xr-x 1 pi pi  928 Jul  1 02:01 start.sh
pi@raspberrypi:~/aws_iot $ 
pi@raspberrypi:~/aws_iot $ ./start.sh 

Downloading AWS IoT Root CA certificate from Symantec...
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1758  100  1758    0     0  12381      0 --:--:-- --:--:-- --:--:-- 12468

Installing AWS SDK...
Cloning into 'aws-iot-device-sdk-python'...
remote: Counting objects: 116, done.
remote: Compressing objects: 100% (19/19), done.
remote: Total 116 (delta 3), reused 15 (delta 3), pack-reused 92
Receiving objects: 100% (116/116), 117.74 KiB | 0 bytes/s, done.
Resolving deltas: 100% (35/35), done.
Checking connectivity... done.
~/aws_iot/aws-iot-device-sdk-python ~/aws_iot
running install
running build
running build_py
creating build
creating build/lib.linux-armv7l-2.7
creating build/lib.linux-armv7l-2.7/AWSIoTPythonSDK
copying AWSIoTPythonSDK/MQTTLib.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK
copying AWSIoTPythonSDK/__init__.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK
creating build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core
copying AWSIoTPythonSDK/core/__init__.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core
creating build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/exception
copying AWSIoTPythonSDK/exception/operationTimeoutException.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/exception
copying AWSIoTPythonSDK/exception/AWSIoTExceptions.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/exception
copying AWSIoTPythonSDK/exception/operationError.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/exception
copying AWSIoTPythonSDK/exception/__init__.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/exception
creating build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/shadow
copying AWSIoTPythonSDK/core/shadow/shadowManager.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/shadow
copying AWSIoTPythonSDK/core/shadow/deviceShadow.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/shadow
copying AWSIoTPythonSDK/core/shadow/__init__.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/shadow
creating build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/util
copying AWSIoTPythonSDK/core/util/sigV4Core.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/util
copying AWSIoTPythonSDK/core/util/offlinePublishQueue.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/util
copying AWSIoTPythonSDK/core/util/__init__.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/util
copying AWSIoTPythonSDK/core/util/progressiveBackoffCore.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/util
creating build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/protocol
copying AWSIoTPythonSDK/core/protocol/__init__.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/protocol
copying AWSIoTPythonSDK/core/protocol/mqttCore.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/protocol
creating build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/protocol/paho
copying AWSIoTPythonSDK/core/protocol/paho/client.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/protocol/paho
copying AWSIoTPythonSDK/core/protocol/paho/__init__.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/protocol/paho
creating build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/protocol/paho/securedWebsocket
copying AWSIoTPythonSDK/core/protocol/paho/securedWebsocket/securedWebsocketCore.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/protocol/paho/securedWebsocket
copying AWSIoTPythonSDK/core/protocol/paho/securedWebsocket/__init__.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/protocol/paho/securedWebsocket
running install_lib
creating /usr/local/lib/python2.7/dist-packages/AWSIoTPythonSDK
error: could not create '/usr/local/lib/python2.7/dist-packages/AWSIoTPythonSDK': Permission denied
pi@raspberrypi:~/aws_iot $ 
pi@raspberrypi:~/aws_iot $ sudo ./start.sh                                                                                                                                                                                                    

Running pub/sub sample application...
Traceback (most recent call last):
  File "aws-iot-device-sdk-python/samples/basicPubSub/basicPubSub.py", line 18, in <module>
    from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient
ImportError: No module named AWSIoTPythonSDK.MQTTLib
pi@raspberrypi:~/aws_iot $ 

 AWSIoTPythonSDK がみつからないということなので、インストールします。

pi@raspberrypi:~/aws_iot $ sudo pip install AWSIoTPythonSDK                                                                                                                                                                                   
Downloading/unpacking AWSIoTPythonSDK
  Downloading AWSIoTPythonSDK-1.1.2.tar.gz (55kB): 55kB downloaded
  Running setup.py (path:/tmp/pip-build-06EpCW/AWSIoTPythonSDK/setup.py) egg_info for package AWSIoTPythonSDK
    
Installing collected packages: AWSIoTPythonSDK
  Running setup.py install for AWSIoTPythonSDK
    
Successfully installed AWSIoTPythonSDK
Cleaning up...
pi@raspberrypi:~/aws_iot $ 

 再度テスト用スクリプトを実行します。

pi@raspberrypi:~/aws_iot $ sudo ./start.sh                                                                                                                                                                                                    

Running pub/sub sample application...
2017-07-01 02:14:37,153 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Paho MQTT Client init.
2017-07-01 02:14:37,153 - AWSIoTPythonSDK.core.protocol.mqttCore - INFO - ClientID: basicPubSub
2017-07-01 02:14:37,153 - AWSIoTPythonSDK.core.protocol.mqttCore - INFO - Protocol: MQTTv3.1.1
2017-07-01 02:14:37,154 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Register Paho MQTT Client callbacks.
2017-07-01 02:14:37,154 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - mqttCore init.
2017-07-01 02:14:37,154 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Load CAFile from: root-CA.crt
2017-07-01 02:14:37,154 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Load Key from: raspberry_pi.private.key
2017-07-01 02:14:37,155 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Load Cert from: raspberry_pi.cert.pem
2017-07-01 02:14:37,155 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Custom setting for backoff timing: baseReconnectTime = 1 sec
2017-07-01 02:14:37,155 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Custom setting for backoff timing: maximumReconnectTime = 32 sec
2017-07-01 02:14:37,155 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Custom setting for backoff timing: minimumConnectTime = 20 sec
2017-07-01 02:14:37,155 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Custom setting for publish queueing: queueSize = -1
2017-07-01 02:14:37,156 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Custom setting for publish queueing: dropBehavior = Drop Newest
2017-07-01 02:14:37,156 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Custom setting for draining interval: 0.5 sec
2017-07-01 02:14:37,156 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Set maximum connect/disconnect timeout to be 10 second.
2017-07-01 02:14:37,156 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Set maximum MQTT operation timeout to be 5 second
2017-07-01 02:14:37,157 - AWSIoTPythonSDK.core.protocol.mqttCore - INFO - Connection type: TLSv1.2 Mutual Authentication
2017-07-01 02:14:37,507 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Connect result code 0
2017-07-01 02:14:37,510 - AWSIoTPythonSDK.core.protocol.mqttCore - INFO - Connected to AWS IoT.
2017-07-01 02:14:37,510 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Connect time consumption: 70.0ms.
2017-07-01 02:14:37,511 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Started a subscribe request 1
2017-07-01 02:14:37,559 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - _resubscribeCount: -1
2017-07-01 02:14:37,560 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Subscribe request 1 sent.
2017-07-01 02:14:37,561 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Subscribe request 1 succeeded. Time consumption: 50.0ms.
2017-07-01 02:14:37,562 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Recover subscribe context for the next request: subscribeSent: False
2017-07-01 02:14:39,565 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Try to put a publish request 2 in the TCP stack.
2017-07-01 02:14:39,566 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Publish request 2 succeeded.
Received a new message: 
New Message 0
from topic: 
sdk/test/Python
--------------


2017-07-01 02:14:40,568 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Try to put a publish request 3 in the TCP stack.
2017-07-01 02:14:40,569 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Publish request 3 succeeded.
Received a new message: 
New Message 1
from topic: 
sdk/test/Python
--------------


2017-07-01 02:14:41,571 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Try to put a publish request 4 in the TCP stack.
2017-07-01 02:14:41,572 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Publish request 4 succeeded.
Received a new message: 
New Message 2
from topic: 
sdk/test/Python
--------------


2017-07-01 02:14:42,575 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Try to put a publish request 5 in the TCP stack.
2017-07-01 02:14:42,576 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Publish request 5 succeeded.
Received a new message: 
New Message 3
from topic: 
sdk/test/Python
--------------


2017-07-01 02:14:43,578 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Try to put a publish request 6 in the TCP stack.
2017-07-01 02:14:43,579 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Publish request 6 succeeded.
Received a new message: 
New Message 4
from topic: 
sdk/test/Python
--------------

 エラーなく実行され、メッセージが送信され続けているようです。コンソール側を確認すると、 デバイスからのメッセージを待機中 となっていたところが下記のように変わり、メッセージが受信されていることが確認できます。

f:id:akanuma-hiroaki:20170701134928p:plain

 上記画面の「ステップ 4: デバイスにメッセージを送信する」フォームに「Hello, IoT!!」のように入力して メッセージの送信 をクリックすると、Raspberry Pi 側でメッセージが受信され、下記のように出力されます。

Received a new message: 
Hello, IoT!!
from topic: 
sdk/test/Python

 ここまででひとまずデバイスの登録から接続の確認までは完了です。AWS IoT コンソールのダッシュボードを表示すると、下記のように処理の状況が確認できます。

f:id:akanuma-hiroaki:20170701135615p:plain

f:id:akanuma-hiroaki:20170701135628p:plain

f:id:akanuma-hiroaki:20170701135643p:plain

 テスト用のサンプルはMQTTによる接続なので、プロトコルとしてMQTTがカウントされています。

 ちなみにダッシュボードの情報を表示するには CloudWatch の表示権限が必要になります。今回はテストということで、 CloudWatchFullAccess ポリシーをアタッチしました。実際のサービス提供時は最低限の権限に絞って許可した方が良いかと思います。

 ひとまず今回は接続まででしたが、ダッシュボード上でデバイスの通信状況が確認できるというのはとても便利そうに思えました。他のAWSとの連携や、SORACOM Beamを通しての接続も簡単にできるようなので、色々と組み合わせて動かしていってみたいと思います。

D-BusからBLE Advertisementを送信する

 今まではRaspberry PiをBLEのCentralとして他のデバイスへの接続などを試していましたが、今回はPeripheralとしてAdvertisementを送信してみました。

 PythonでAdvertisementを送信している例があったので下記サイトを参考にさせてもらいました。

qiita.com

 また、BLEについての下記サイトも参考にさせてもらいました。

http://events.linuxfoundation.org/sites/events/files/slides/Doing%20Bluetooth%20Low%20Energy%20on%20Linux.pdf

インタフェースの確認と設定

 Advertisementを送信するには、 org.bluez.LEAdvertisingManager1RegisterAdvertisement() メソッドを使うようなので、まずはインタフェースを確認してみます。

irb(main):001:0> require 'dbus'
irb(main):002:0> bus = DBus::SystemBus.instance
irb(main):003:0> bluez = bus.service('org.bluez')
irb(main):004:0> adapter = bluez.object('/org/bluez/hci0')
irb(main):005:0> adapter.introspect
〜〜〜ここまでのアウトプットは省略〜〜〜
irb(main):006:0> adapter.interfaces
=> ["org.freedesktop.DBus.Introspectable", "org.bluez.Adapter1", "org.freedesktop.DBus.Properties", "org.bluez.GattManager1", "org.bluez.Media1", "org.bluez.NetworkServer1"]

 該当のインタフェースが存在していない様子。試しに bluetoothctl でも見てみます。

[bluetooth]# advertise on
LEAdvertisingManager not found

 やはりインタフェースがみつからないようです。

 BlueZのドキュメントを確認してみます。

advertising-api.txt\doc - bluez.git - Bluetooth protocol stack for Linux

Service org.bluez
Interface org.bluez.LEAdvertisingManager1 [Experimental]
Object path /org/bluez/{hci0,hci1,…}

 org.bluez.LEAdvertisingManager1 は Experimental となっているようです。

 BlueZのインストール時の内容を確認してみましたが、 --enable-experimental オプションは付けてビルドしてました。

blog.akanumahiroaki.com

 bluetoothd のプロセスを確認してみます。

pi@raspberrypi:~ $ ps aux | grep bluetoothd
root       658  0.0  0.3   4780  3280 ?        Ss   22:23   0:00 /usr/local/libexec/bluetooth/bluetoothd
pi        1122  0.0  0.2   4276  2008 pts/3    S+   22:54   0:00 grep --color=auto bluetoothd

 下記記事を参照したところ、 --experimental オプションを付けて bluetoothd を起動する必要があるようです。

blog.mrgibbs.io

 /etc/systemd/system/bluetooth.target.wants/bluetooth.service を下記のように編集します。

ExecStart=/usr/local/libexec/bluetooth/bluetoothd  
 ↓  
ExecStart=/usr/local/libexec/bluetooth/bluetoothd --experimental

 編集後にRaspberry Piを再起動して、 bluetoothd を再確認します。

pi@raspberrypi:~ $ ps aux | grep bluetoothd
root       717  0.0  0.3   4780  3248 ?        Ss   23:05   0:00 /usr/local/libexec/bluetooth/bluetoothd --experimental
pi         910  0.0  0.1   4276  1812 pts/1    S+   23:09   0:00 grep --color=auto bluetoothd

 --experimental オプション付きで bluetoothd が起動しました。 bluetoothctl で確認してみます。

[bluetooth]# advertise on
Advertising object registered
[bluetooth]# advertise off
Advertising object unregistered
Agent unregistered

 bluetoothctl でも advertise コマンドが使えるようになりました。Rubyからも確認してみます

irb(main):006:0> adapter.interfaces
=> ["org.freedesktop.DBus.Introspectable", "org.bluez.Adapter1", "org.freedesktop.DBus.Properties", "org.bluez.GattManager1", "org.bluez.Media1", "org.bluez.NetworkServer1", "org.bluez.LEAdvertisingManager1"]

 org.bluez.LEAdvertisingManager1 インタフェースが追加されました。

Advertisementの送信(bluetoothctl)

 bluetoothctl でPeripheralとしてAdvertisementを送信してみます。

pi@raspberrypi:~ $ sudo bluetoothctl
[bluetooth]# advertise peripheral
Advertising object registered

 上記実行時のHCIの動作を hcidump で確認すると下記のようになりました。

pi@raspberrypi:~ $ sudo hcidump -i hci0
HCI sniffer - Bluetooth packet analyzer ver 5.23
device: hci0 snap_len: 1500 filter: 0xffffffff
< HCI Command: LE Set Advertising Parameters (0x08|0x0006) plen 15
    min 1280.000ms, max 1280.000ms
    type 0x00 (ADV_IND - Connectable undirected advertising) ownbdaddr 0x00 (Public)
    directbdaddr 0x00 (Public) 00:00:00:00:00:00
    channelmap 0x07 filterpolicy 0x00 (Allow scan from any, connection from any)
> HCI Event: Command Complete (0x0e) plen 4
    LE Set Advertising Parameters (0x08|0x0006) ncmd 1
    status 0x00
< HCI Command: LE Set Advertise Enable (0x08|0x000a) plen 1
> HCI Event: Command Complete (0x0e) plen 4
    LE Set Advertise Enable (0x08|0x000a) ncmd 1
    status 0x00

 PeripheralとしてAdvertisementを送信しているので、 ADV_IND - Connectable undirected advertising となっており、他のデバイスからの接続が可能なタイプのAdvertisementになっていることがわかります。

 ではLightBlueから接続してみます。

f:id:akanuma-hiroaki:20170621074917p:plain:w300 f:id:akanuma-hiroaki:20170621074923p:plain:w300

 デバイスの一覧に raspberrypi が表示され、タップして接続することができました。

 次にBroadcastで送信してみます。

[bluetooth]# advertise broadcast
Advertising object registered

 hcidump での出力は下記の通り。

< HCI Command: LE Set Advertising Data (0x08|0x0008) plen 32
> HCI Event: Command Complete (0x0e) plen 4
    LE Set Advertising Data (0x08|0x0008) ncmd 1
    status 0x00
< HCI Command: LE Set Random Address (0x08|0x0005) plen 6
    bdaddr 32:DC:0C:A9:69:2B
> HCI Event: Command Complete (0x0e) plen 4
    LE Set Random Address (0x08|0x0005) ncmd 1
    status 0x00
< HCI Command: LE Set Advertising Parameters (0x08|0x0006) plen 15
    min 1280.000ms, max 1280.000ms
    type 0x03 (ADV_NONCONN_IND - Non connectable undirected advertising) ownbdaddr 0x01 (Random)
    directbdaddr 0x00 (Public) 00:00:00:00:00:00
    channelmap 0x07 filterpolicy 0x00 (Allow scan from any, connection from any)
> HCI Event: Command Complete (0x0e) plen 4
    LE Set Advertising Parameters (0x08|0x0006) ncmd 1
    status 0x00
< HCI Command: LE Set Advertise Enable (0x08|0x000a) plen 1
> HCI Event: Command Complete (0x0e) plen 4
    LE Set Advertise Enable (0x08|0x000a) ncmd 1
    status 0x00

 今度は ADV_NONCONN_IND - Non connectable undirected advertising となっており、他のデバイスからの接続はできない(Advertisementパケットの参照だけできる)タイプのAdvertisementになっていることがわかります。

 とりあえず bluetoothctl からAdvertisementを送信して他のデバイスから検知することができましたが、Rubyから送信することにはかなり調べたもののまだ成功していないので、今後実現していきたいと思います。

D-BusからBLEデバイスのNotificationを受け取る

 前回の記事で、Raspberry Pi上でRuby(irb)からD-Busを使ってBLEデバイスに接続し、値を読み取るというところまでやりましたが、今回はPeripheralからのNotificationを受け取って値の変化を検知するところまでを実装してみました。前回はirbから試してみましたが、今回はスクリプトにまとめてあります。また、対象のBLEデバイスは前回同様にLightBlueで擬似Peripheralデバイスを作成して使用しています。

スクリプト全体

 まずはスクリプト全体を掲載しておきます。

require 'bundler/setup'
require 'dbus'

SERVICE_PATH = '/org/bluez'
ADAPTER      = 'hci0'

DEVICE_IF         = 'org.bluez.Device1'
SERVICE_IF        = 'org.bluez.GattService1'
CHARACTERISTIC_IF = 'org.bluez.GattCharacteristic1'
PROPERTIES_IF     = 'org.freedesktop.DBus.Properties'

HEARTRATE_SERVICE_UUID = '0000180d-0000-1000-8000-00805f9b34fb'
HEARTRATE_UUID         = '00002a37-0000-1000-8000-00805f9b34fb'

log = Logger.new('ble_notification.log')

bus = DBus::SystemBus.instance
bluez = bus.service('org.bluez')

adapter = bluez.object("#{SERVICE_PATH}/#{ADAPTER}")
adapter.introspect
puts 'Discoverying Nodes...'
adapter.StartDiscovery
sleep(10)

nodes = adapter.subnodes
connected_device = nil

begin
  nodes.each do |node|
    device = bluez.object("#{SERVICE_PATH}/#{ADAPTER}/#{node}")
    device.introspect

    properties = device.GetAll(DEVICE_IF)[0]
    name    = properties['Name']
    address = properties['Address']
    rssi    = properties['RSSI']

    next if name.nil? || rssi.nil?
    next unless name.downcase.include?('heart rate')

    puts "Connecting to the device: #{address} #{name} RSSI:#{properties['RSSI']}"
    begin
      device.Connect
      puts 'Connected. Resolving Services...'
      device.introspect

      while !properties['ServicesResolved'] do
        sleep(0.5)
        device.introspect
        properties = device.GetAll(DEVICE_IF)[0]
      end
      puts 'Resolved.'

      connected_device = device
      break
    rescue => e
      puts e
    end
  end

  raise 'No device connected.' if connected_device.nil?

  heartrate_service = nil
  nodes = connected_device.subnodes
  nodes.each do |node|
    service = bluez.object("#{connected_device.path}/#{node}")
    service.introspect

    properties = service.GetAll(SERVICE_IF)[0]
    uuid = properties['UUID']

    next unless uuid == HEARTRATE_SERVICE_UUID

    heartrate_service = service
  end

  nodes = heartrate_service.subnodes
  nodes.each do |node|
    characteristic = bluez.object("#{heartrate_service.path}/#{node}")
    characteristic.introspect

    properties = characteristic.GetAll(CHARACTERISTIC_IF)[0]
    uuid = properties['UUID']

    next unless uuid == HEARTRATE_UUID

    characteristic.StartNotify
    characteristic.default_iface = PROPERTIES_IF
    characteristic.on_signal('PropertiesChanged') do |_, v|
      log.info("#{heartrate_service.path.split('/')[4]} #{v}")
    end
  end

  main = DBus::Main.new
  main << bus

  puts 'Monitoring Heart Rate...'
  main.run
rescue Interrupt => e
  puts e
  puts "Interrupted."
rescue => e
  puts e
ensure
  connected_device.Disconnect unless connected_device.nil?
  adapter.StopDiscovery
end

 それでは各パートごとに説明していきます。

Peripheralへの接続

 まずはBluetoothアダプタのDiscoveryModeをONにし、BLEデバイスを検索します。今回はとりあえず10秒間は検索のために待機しています。

bus = DBus::SystemBus.instance
bluez = bus.service('org.bluez')

adapter = bluez.object("#{SERVICE_PATH}/#{ADAPTER}")
adapter.introspect
puts 'Discoverying Nodes...'
adapter.StartDiscovery
sleep(10)

nodes = adapter.subnodes

 そして見つかった各デバイスのデバイス名を確認して、対象のデバイスであれば接続します。そして配下のGATTサービスが検知されて ServicesResolved のプロパティがtrueになるのを待ってから次の処理に進みます。

nodes.each do |node|
  device = bluez.object("#{SERVICE_PATH}/#{ADAPTER}/#{node}")
  device.introspect

  properties = device.GetAll(DEVICE_IF)[0]
  name    = properties['Name']
  address = properties['Address']
  rssi    = properties['RSSI']

  next if name.nil? || rssi.nil?
  next unless name.downcase.include?('heart rate')

  puts "Connecting to the device: #{address} #{name} RSSI:#{properties['RSSI']}"
  begin
    device.Connect
    puts 'Connected. Resolving Services...'
    device.introspect

    while !properties['ServicesResolved'] do
      sleep(0.5)
      device.introspect
      properties = device.GetAll(DEVICE_IF)[0]
    end
    puts 'Resolved.'

    connected_device = device
    break
  rescue => e
    puts e
  end
end

GATTサービスの取得

 次にデバイス配下の各サービスのUUIDを確認し、今回対象とするUUIDを持つサービスを取得します。

nodes = connected_device.subnodes
nodes.each do |node|
  service = bluez.object("#{connected_device.path}/#{node}")
  service.introspect

  properties = service.GetAll(SERVICE_IF)[0]
  uuid = properties['UUID']

  next unless uuid == HEARTRATE_SERVICE_UUID

  heartrate_service = service
end

GATTキャラクタリスティックの取得とシグナル検知時のコールバック設定

 サービス配下の各キャラクタリスティックのUUIDを確認し、今回対象とするUUIDを持つキャラクタリスティックを特定したら、Peripheralからのシグナルを検知して処理を行うためのコールバックの設定を行います。

nodes = heartrate_service.subnodes
nodes.each do |node|
  characteristic = bluez.object("#{heartrate_service.path}/#{node}")
  characteristic.introspect

  properties = characteristic.GetAll(CHARACTERISTIC_IF)[0]
  uuid = properties['UUID']

  next unless uuid == HEARTRATE_UUID

  characteristic.StartNotify
  characteristic.default_iface = PROPERTIES_IF
  characteristic.on_signal('PropertiesChanged') do |_, v|
    log.info("#{heartrate_service.path.split('/')[4]} #{v}")
  end
end

 上記のコールバック設定部分については、まず StartNotify メソッドでPeripheralからのシグナルの送信を開始しています。

characteristic.StartNotify

 そして on_signal メソッドでシグナル検知時のコールバックを指定するのですが、検知対象のインタフェースはデフォルトインタフェースになるので、先に default_iface メソッドでデフォルトインタフェースを指定しておきます。値の変更は D-Bus の org.freedesktop.DBus.Properties インタフェースで検知します。

characteristic.default_iface = PROPERTIES_IF

 シグナルとしては PropertiesChanged シグナルになりますので、 on_signal メソッドでシグナルを指定してコールバックを設定します。今回は検知時の処理としてはデバイスのアドレスと値をログに出力しています。

characteristic.on_signal('PropertiesChanged') do |_, v|
  log.info("#{heartrate_service.path.split('/')[4]} #{v}")
end

非同期処理の開始

 そして最後にイベントループでの非同期処理を開始します。

  main = DBus::Main.new
  main << bus
  main.run

 これでPeripheral側からシグナルが送信されると、下記のようにログに出力されるようになります。

I, [2017-06-14T18:40:31.151687 #3589]  INFO -- : dev_7C_12_A1_23_8C_21 {"Value"=>[0, 60]}
I, [2017-06-14T18:40:32.163818 #3589]  INFO -- : dev_7C_12_A1_23_8C_21 {"Value"=>[0, 60]}
I, [2017-06-14T18:40:33.110175 #3589]  INFO -- : dev_7C_12_A1_23_8C_21 {"Value"=>[0, 60]}
I, [2017-06-14T18:40:34.121697 #3589]  INFO -- : dev_7C_12_A1_23_8C_21 {"Value"=>[0, 80]}
I, [2017-06-14T18:40:35.134417 #3589]  INFO -- : dev_7C_12_A1_23_8C_21 {"Value"=>[0, 80]}
I, [2017-06-14T18:40:36.146261 #3589]  INFO -- : dev_7C_12_A1_23_8C_21 {"Value"=>[0, 80]}
I, [2017-06-14T18:40:37.159118 #3589]  INFO -- : dev_7C_12_A1_23_8C_21 {"Value"=>[0, 100]}
I, [2017-06-14T18:40:38.103964 #3589]  INFO -- : dev_7C_12_A1_23_8C_21 {"Value"=>[0, 100]}
I, [2017-06-14T18:40:39.184016 #3589]  INFO -- : dev_7C_12_A1_23_8C_21 {"Value"=>[0, 100]}

Raspberry Pi 3でD-BusからBLEデバイスにアクセスする

 Raspberry PiからBLEデバイスへの接続についていろいろ調べている中でD-Busについても調べたので、D-BusからBlueZを使ってBLEデバイスにアクセスしてみました。D-Busについては下記サイトでわかりやすく解説されていて、とても参考になりました。

www.silex.jp

 D-Busについての詳細は上記サイトを参照いただくとしてここでは割愛しますが、すごくざっくり言うと、コンピュータ上の複数のプログラム間で情報のやり取りをするためのIPC(Inter Process Communication)で、オブジェクト(プログラム)間でメッセージ(データ)を届けるためのものです。BlueZもD-Busのサービスの一つとして登録されるので、D-BusからBlueZを使用してBLEデバイスに接続することができます。

 また、下記資料も参考にさせていただきました。

Bluetooth on modern Linux
http://events.linuxfoundation.org/sites/events/files/slides/Bluetooth%20on%20Modern%20Linux_0.pdf

 接続対象のBLEデバイスとしては、LightBlueというアプリでiPhoneを擬似BLEデバイスとして使用してみました。

LightBlue Explorer - Bluetooth Low Energy

LightBlue Explorer - Bluetooth Low Energy

  • Punch Through
  • ユーティリティ
  • 無料

 いろいろな種類の擬似Peripheralを作成できるので、今回はHeart Rateを作成して使用してみます。Body Sensor Locationキャラクタリスティックを読み書きすることを目標とします。

f:id:akanuma-hiroaki:20170610095856p:plain:w300 f:id:akanuma-hiroaki:20170610095902p:plain:w300

コマンドラインからD-Busを操作する

 Linux上でD-Busを操作するには dbus-send というコマンドを使います。

dbus-send
https://dbus.freedesktop.org/doc/dbus-send.1.html

 まずはD-Busにどんなサービスが登録されているかを確認してみます。

pi@raspberrypi:~ $ sudo dbus-send --print-reply --system --dest=org.freedesktop.DBus / --type=method_call org.freedesktop.DBus.ListNames                                                                                                      
method return sender=org.freedesktop.DBus -> dest=:1.6 reply_serial=2
   array [
      string "org.freedesktop.DBus"
      string ":1.3"
      string "org.freedesktop.login1"
      string "org.freedesktop.systemd1"
      string "org.freedesktop.Avahi"
      string ":1.0"
      string ":1.5"
      string "org.bluez"
      string ":1.1"
      string ":1.6"
      string ":1.2"
   ]

 サービス名は慣習としてサービスの開発元のドメイン名が使われます。サービス登録時にはサービス名を明示しなくても登録できますが、その場合には上記の string ":1.3" のようにD-Busサーバが適当に生成した数字が使われます。

 上記の結果の中の string "org.bluez" がBlueZのサービス名になります。BlueZのサービスが持つオブジェクトのリストを表示してみます。 --dest オプションで対象のサービスを指定し、引数として対象の階層を指定します。今回はサービス配下のトップレベルのオブジェクトのリストを表示するために引数として / を指定します。また、オブジェクトのリストを表示するためのメソッドは org.freedesktop.DBus.Introspectable.Introspect ですので、 --type オプションで method_call を指定し、引数にメソッド名を渡します。

pi@raspberrypi:~ $ sudo dbus-send --print-reply --system --dest=org.bluez / --type=method_call org.freedesktop.DBus.Introspectable.Introspect
method return sender=:1.3 -> dest=:1.7 reply_serial=2
   string "<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node><interface name="org.freedesktop.DBus.Introspectable"><method name="Introspect"><arg name="xml" type="s" direction="out"/>
</method></interface><interface name="org.freedesktop.DBus.ObjectManager"><method name="GetManagedObjects"><arg name="objects" type="a{oa{sa{sv}}}" direction="out"/>
</method><signal name="InterfacesAdded"><arg name="object" type="o"/>
<arg name="interfaces" type="a{sa{sv}}"/>
</signal>
<signal name="InterfacesRemoved"><arg name="object" type="o"/>
<arg name="interfaces" type="as"/>
</signal>
</interface><node name="org"/></node>"

 上記のように結果はXMLで出力されます。 <node name="org"/> とあることから、トップレベルのパスの配下に org というパスがあることがわかりますので、そこを掘り下げてみます。 --dest オプションの引数に /org を指定します。

pi@raspberrypi:~ $ sudo dbus-send --print-reply --system --dest=org.bluez /org --type=method_call org.freedesktop.DBus.Introspectable.Introspect                                                                                              
method return sender=:1.3 -> dest=:1.8 reply_serial=2
   string "<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
  <node name="bluez"/>
</node>
"

 するとさらに <node name="bluez"/> となって /org/bluez というパスがあることがわかるのでさらに掘り下げます。

pi@raspberrypi:~ $ sudo dbus-send --print-reply --system --dest=org.bluez /org/bluez --type=method_call org.freedesktop.DBus.Introspectable.Introspect                                                                                        
method return sender=:1.3 -> dest=:1.9 reply_serial=2
   string "<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node><interface name="org.freedesktop.DBus.Introspectable"><method name="Introspect"><arg name="xml" type="s" direction="out"/>
</method></interface><interface name="org.bluez.AgentManager1"><method name="RegisterAgent"><arg name="agent" type="o" direction="in"/>
<arg name="capability" type="s" direction="in"/>
</method><method name="UnregisterAgent"><arg name="agent" type="o" direction="in"/>
</method><method name="RequestDefaultAgent"><arg name="agent" type="o" direction="in"/>
</method></interface><interface name="org.bluez.ProfileManager1"><method name="RegisterProfile"><arg name="profile" type="o" direction="in"/>
<arg name="UUID" type="s" direction="in"/>
<arg name="options" type="a{sv}" direction="in"/>
</method><method name="UnregisterProfile"><arg name="profile" type="o" direction="in"/>
</method></interface><node name="hci0"/></node>"

 さらに <node name="hci0"/> となって、 /org/bluez/hci0 というパスがあることがわかります。 hci0 はBluetoothアダプタになりますので、複数のBluetoothアダプタがある場合は末尾の数字が連番になっていきます。さらに掘り下げてこのBluetoothアダプタが持っているオブジェクトのリストを表示します。

pi@raspberrypi:~ $ sudo dbus-send --print-reply --system --dest=org.bluez /org/bluez/hci0 --type=method_call org.freedesktop.DBus.Introspectable.Introspect                                                                                   
method return sender=:1.3 -> dest=:1.10 reply_serial=2
   string "<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node><interface name="org.freedesktop.DBus.Introspectable"><method name="Introspect"><arg name="xml" type="s" direction="out"/>
</method></interface><interface name="org.bluez.Adapter1"><method name="StartDiscovery"></method><method name="SetDiscoveryFilter"><arg name="properties" type="a{sv}" direction="in"/>
</method><method name="StopDiscovery"></method><method name="RemoveDevice"><arg name="device" type="o" direction="in"/>
</method><property name="Address" type="s" access="read"></property><property name="Name" type="s" access="read"></property><property name="Alias" type="s" access="readwrite"></property><property name="Class" type="u" access="read"></prop
erty><property name="Powered" type="b" access="readwrite"></property><property name="Discoverable" type="b" access="readwrite"></property><property name="DiscoverableTimeout" type="u" access="readwrite"></property><property name="Pairable
" type="b" access="readwrite"></property><property name="PairableTimeout" type="u" access="readwrite"></property><property name="Discovering" type="b" access="read"></property><property name="UUIDs" type="as" access="read"></property><pro
perty name="Modalias" type="s" access="read"></property></interface><interface name="org.freedesktop.DBus.Properties"><method name="Get"><arg name="interface" type="s" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="value" type="v" direction="out"/>
</method><method name="Set"><arg name="interface" type="s" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="value" type="v" direction="in"/>
</method><method name="GetAll"><arg name="interface" type="s" direction="in"/>
<arg name="properties" type="a{sv}" direction="out"/>
</method><signal name="PropertiesChanged"><arg name="interface" type="s"/>
<arg name="changed_properties" type="a{sv}"/>
<arg name="invalidated_properties" type="as"/>
</signal>
</interface><interface name="org.bluez.GattManager1"><method name="RegisterApplication"><arg name="application" type="o" direction="in"/>
<arg name="options" type="a{sv}" direction="in"/>
</method><method name="UnregisterApplication"><arg name="application" type="o" direction="in"/>
</method></interface><interface name="org.bluez.Media1"><method name="RegisterEndpoint"><arg name="endpoint" type="o" direction="in"/>
<arg name="properties" type="a{sv}" direction="in"/>
</method><method name="UnregisterEndpoint"><arg name="endpoint" type="o" direction="in"/>
</method><method name="RegisterPlayer"><arg name="player" type="o" direction="in"/>
<arg name="properties" type="a{sv}" direction="in"/>
</method><method name="UnregisterPlayer"><arg name="player" type="o" direction="in"/>
</method></interface><interface name="org.bluez.NetworkServer1"><method name="Register"><arg name="uuid" type="s" direction="in"/>
<arg name="bridge" type="s" direction="in"/>
</method><method name="Unregister"><arg name="uuid" type="s" direction="in"/>
</method></interface><node name="dev_49_25_2A_9A_90_44"/><node name="dev_43_19_24_21_00_5F"/><node name="dev_88_4A_EA_8A_3F_2B"/><node name="dev_FC_E9_98_21_23_B7"/></node>"

  <node name="dev_49_25_2A_9A_90_44"/> のようにこのBluetoothアダプタに検知されているBLEデバイスが表示されています。デバイスの情報を見る前にこのBluetoothアダプタのプロパティの一覧を表示してみます。プロパティ表示用のメソッドは org.freedesktop.DBus.Properties.GetAll で、引数にBlueZのインタフェース名を指定します。先ほどのIntrospectの結果に <interface name="org.bluez.Adapter1"> という内容が含まれていて、 org.bluez.Adapter1 というインタフェースがあることがわかりますので、これを指定します。

pi@raspberrypi:~ $ sudo dbus-send --print-reply --system --dest=org.bluez /org/bluez/hci0 --type=method_call org.freedesktop.DBus.Properties.GetAll string:org.bluez.Adapter1
method return sender=:1.3 -> dest=:1.11 reply_serial=2
   array [
      dict entry(
         string "Address"
         variant             string "B8:27:EB:19:76:07"
      )
      dict entry(
         string "Name"
         variant             string "raspberrypi"
      )
      dict entry(
         string "Alias"
         variant             string "raspberrypi"
      )
      dict entry(
         string "Class"
         variant             uint32 0
      )
      dict entry(
         string "Powered"
         variant             boolean true
      )
      dict entry(
         string "Discoverable"
         variant             boolean false
      )
      dict entry(
         string "DiscoverableTimeout"
         variant             uint32 180
      )
      dict entry(
         string "Pairable"
         variant             boolean true
      )
      dict entry(
         string "PairableTimeout"
         variant             uint32 0
      )
      dict entry(
         string "Discovering"
         variant             boolean false
      )
      dict entry(
         string "UUIDs"
         variant             array [
               string "00001801-0000-1000-8000-00805f9b34fb"
               string "0000110e-0000-1000-8000-00805f9b34fb"
               string "00001200-0000-1000-8000-00805f9b34fb"
               string "00001800-0000-1000-8000-00805f9b34fb"
               string "0000110c-0000-1000-8000-00805f9b34fb"
            ]
      )
      dict entry(
         string "Modalias"
         variant             string "usb:v1D6Bp0246d052D"
      )
   ]

 MACアドレスや、デバイスのスキャンを行なっているか(Discovering)といった情報が確認できます。

 それではhci0配下のデバイスの情報を見てみます。 --dest オプションの引数のパスにデバイスを追加します。

pi@raspberrypi:~ $ sudo dbus-send --print-reply --system --dest=org.bluez /org/bluez/hci0/dev_49_25_2A_9A_90_44 --type=method_call org.freedesktop.DBus.Introspectable.Introspect
method return sender=:1.3 -> dest=:1.9 reply_serial=2
   string "<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node><interface name="org.freedesktop.DBus.Introspectable"><method name="Introspect"><arg name="xml" type="s" direction="out"/>
</method></interface><interface name="org.bluez.Device1"><method name="Disconnect"></method><method name="Connect"></method><method name="ConnectProfile"><arg name="UUID" type="s" direction="in"/>
</method><method name="DisconnectProfile"><arg name="UUID" type="s" direction="in"/>
</method><method name="Pair"></method><method name="CancelPairing"></method><property name="Address" type="s" access="read"></property><property name="Name" type="s" access="read"></property><property name="Alias" type="s" access="readwrite"></property><property name="Class" type="u" access="read"></property><property name="Appearance" type="q" access="read"></property><property name="Icon" type="s" access="read"></property><property name="Paired" type="b" access="read"></property><property name="Trusted" type="b" access="readwrite"></property><property name="Blocked" type="b" access="readwrite"></property><property name="LegacyPairing" type="b" access="read"></property><property name="RSSI" type="n" access="read"></property><property name="Connected" type="b" access="read"></property><property name="UUIDs" type="as" access="read"></property><property name="Modalias" type="s" access="read"></property><property name="Adapter" type="o" access="read"></property><property name="ManufacturerData" type="a{qv}" access="read"></property><property name="ServiceData" type="a{sv}" access="read"></property><property name="TxPower" type="n" access="read"></property><property name="ServicesResolved" type="b" access="read"></property></interface><interface name="org.freedesktop.DBus.Properties"><method name="Get"><arg name="interface" type="s" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="value" type="v" direction="out"/>
</method><method name="Set"><arg name="interface" type="s" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="value" type="v" direction="in"/>
</method><method name="GetAll"><arg name="interface" type="s" direction="in"/>
<arg name="properties" type="a{sv}" direction="out"/>
</method><signal name="PropertiesChanged"><arg name="interface" type="s"/>
<arg name="changed_properties" type="a{sv}"/>
<arg name="invalidated_properties" type="as"/>
</signal>
</interface></node>"

 そしてこのデバイスに接続します。

pi@raspberrypi:~ $ sudo dbus-send --print-reply --system --dest=org.bluez /org/bluez/hci0/dev_49_25_2A_9A_90_44 --type=method_call org.bluez.Device1.Connect
method return sender=:1.3 -> dest=:1.14 reply_serial=2

 そして再度オブジェクトのリストを表示してみます。

pi@raspberrypi:~ $ sudo dbus-send --print-reply --system --dest=org.bluez /org/bluez/hci0/dev_49_25_2A_9A_90_44 --type=method_call org.freedesktop.DBus.Introspectable.Introspect
method return sender=:1.3 -> dest=:1.16 reply_serial=2
   string "<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node><interface name="org.freedesktop.DBus.Introspectable"><method name="Introspect"><arg name="xml" type="s" direction="out"/>
</method></interface><interface name="org.bluez.Device1"><method name="Disconnect"></method><method name="Connect"></method><method name="ConnectProfile"><arg name="UUID" type="s" direction="in"/>
</method><method name="DisconnectProfile"><arg name="UUID" type="s" direction="in"/>
</method><method name="Pair"></method><method name="CancelPairing"></method><property name="Address" type="s" access="read"></property><property name="Name" type="s" access="read"></property><property name="Alias" type="s" access="readwrite"></property><property name="Class" type="u" access="read"></property><property name="Appearance" type="q" access="read"></property><property name="Icon" type="s" access="read"></property><property name="Paired" type="b" access="read"></property><property name="Trusted" type="b" access="readwrite"></property><property name="Blocked" type="b" access="readwrite"></property><property name="LegacyPairing" type="b" access="read"></property><property name="RSSI" type="n" access="read"></property><property name="Connected" type="b" access="read"></property><property name="UUIDs" type="as" access="read"></property><property name="Modalias" type="s" access="read"></property><property name="Adapter" type="o" access="read"></property><property name="ManufacturerData" type="a{qv}" access="read"></property><property name="ServiceData" type="a{sv}" access="read"></property><property name="TxPower" type="n" access="read"></property><property name="ServicesResolved" type="b" access="read"></property></interface><interface name="org.freedesktop.DBus.Properties"><method name="Get"><arg name="interface" type="s" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="value" type="v" direction="out"/>
</method><method name="Set"><arg name="interface" type="s" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="value" type="v" direction="in"/>
</method><method name="GetAll"><arg name="interface" type="s" direction="in"/>
<arg name="properties" type="a{sv}" direction="out"/>
</method><signal name="PropertiesChanged"><arg name="interface" type="s"/>
<arg name="changed_properties" type="a{sv}"/>
<arg name="invalidated_properties" type="as"/>
</signal>
</interface><node name="service0006"/><node name="service000a"/><node name="service000e"/><node name="service0014"/><node name="service0019"/><node name="service0023"/><node name="service002f"/><node name="service0034"/><node name="service0039"/><node name="service004c"/></node>"

 接続前は表示されていなかった、 <node name="service0006"/> などのBLEサービスの情報が表示されているのがわかります。また、 org.bluez.Device1 というインタフェースがあることがわかります。

 デバイスのプロパティも確認してみます。

pi@raspberrypi:~ $ sudo dbus-send --print-reply --system --dest=org.bluez /org/bluez/hci0/dev_49_25_2A_9A_90_44 --type=method_call org.freedesktop.DBus.Properties.GetAll string:org.bluez.Device1
method return sender=:1.3 -> dest=:1.27 reply_serial=2
   array [
      dict entry(
         string "Address"
         variant             string "49:25:2A:9A:90:44"
      )
      dict entry(
         string "Name"
         variant             string "iPhone"
      )
      dict entry(
         string "Alias"
         variant             string "iPhone"
      )
      dict entry(
         string "Appearance"
         variant             uint16 64
      )
      dict entry(
         string "Icon"
         variant             string "phone"
      )
      dict entry(
         string "Paired"
         variant             boolean false
      )
      dict entry(
         string "Trusted"
         variant             boolean false
      )
      dict entry(
         string "Blocked"
         variant             boolean false
      )
      dict entry(
         string "LegacyPairing"
         variant             boolean false
      )
      dict entry(
         string "RSSI"
         variant             int16 -44
      )
      dict entry(
         string "Connected"
         variant             boolean true
      )
      dict entry(
         string "UUIDs"
         variant             array [
               string "00001800-0000-1000-8000-00805f9b34fb"
               string "00001801-0000-1000-8000-00805f9b34fb"
               string "00001805-0000-1000-8000-00805f9b34fb"
               string "0000180a-0000-1000-8000-00805f9b34fb"
               string "0000180d-0000-1000-8000-00805f9b34fb"
               string "0000180f-0000-1000-8000-00805f9b34fb"
               string "7905f431-b5ce-4e99-a40f-4b1e122d00d0"
               string "89d3502b-0f36-433a-8ef4-c502ad55f8dc"
               string "9fa480e0-4967-4542-9390-d343dc5d04ae"
               string "d0611e78-bbb4-4591-a5f8-487910ae4366"
            ]
      )
      dict entry(
         string "Adapter"
         variant             object path "/org/bluez/hci0"
      )
      dict entry(
         string "ServicesResolved"
         variant             boolean true
      )
   ]

 MACアドレスやデバイス名などが確認できます。

 続けてデバイス配下のBLEサービスの情報を参照してみます。

pi@raspberrypi:~ $ sudo dbus-send --print-reply --system --dest=org.bluez /org/bluez/hci0/dev_49_25_2A_9A_90_44/service004c --type=method_call org.freedesktop.DBus.Introspectable.Introspect
method return sender=:1.3 -> dest=:1.43 reply_serial=2
   string "<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node><interface name="org.freedesktop.DBus.Introspectable"><method name="Introspect"><arg name="xml" type="s" direction="out"/>
</method></interface><interface name="org.bluez.GattService1"><property name="UUID" type="s" access="read"></property><property name="Device" type="o" access="read"></property><property name="Primary" type="b" access="read"></property><property name="Includes" type="ao" access="read"></property></interface><interface name="org.freedesktop.DBus.Properties"><method name="Get"><arg name="interface" type="s" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="value" type="v" direction="out"/>
</method><method name="Set"><arg name="interface" type="s" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="value" type="v" direction="in"/>
</method><method name="GetAll"><arg name="interface" type="s" direction="in"/>
<arg name="properties" type="a{sv}" direction="out"/>
</method><signal name="PropertiesChanged"><arg name="interface" type="s"/>
<arg name="changed_properties" type="a{sv}"/>
<arg name="invalidated_properties" type="as"/>
</signal>
</interface><node name="char004d"/><node name="char0050"/><node name="char0052"/></node>"

 string:org.bluez.GattService1 というインタフェースがあることがわかりますのでプロパティを参照してみます。

pi@raspberrypi:~ $ sudo dbus-send --print-reply --system --dest=org.bluez /org/bluez/hci0/dev_49_25_2A_9A_90_44/service004c --type=method_call org.freedesktop.DBus.Properties.GetAll string:org.bluez.GattService1
method return sender=:1.3 -> dest=:1.44 reply_serial=2
   array [
      dict entry(
         string "UUID"
         variant             string "0000180d-0000-1000-8000-00805f9b34fb"
      )
      dict entry(
         string "Device"
         variant             object path "/org/bluez/hci0/dev_49_25_2A_9A_90_44"
      )
      dict entry(
         string "Primary"
         variant             boolean true
      )
      dict entry(
         string "Includes"
         variant             array [
            ]
      )
   ]

 さらにそのサービス配下のキャラクタリスティックを見てみます。

pi@raspberrypi:~ $ sudo dbus-send --print-reply --system --dest=org.bluez /org/bluez/hci0/dev_49_25_2A_9A_90_44/service004c/char0050 --type=method_call org.freedesktop.DBus.Introspectable.Introspect                                        
method return sender=:1.3 -> dest=:1.59 reply_serial=2
   string "<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node><interface name="org.freedesktop.DBus.Introspectable"><method name="Introspect"><arg name="xml" type="s" direction="out"/>
</method></interface><interface name="org.bluez.GattCharacteristic1"><method name="ReadValue"><arg name="options" type="a{sv}" direction="in"/>
<arg name="value" type="ay" direction="out"/>
</method><method name="WriteValue"><arg name="value" type="ay" direction="in"/>
<arg name="options" type="a{sv}" direction="in"/>
</method><method name="StartNotify"></method><method name="StopNotify"></method><property name="UUID" type="s" access="read"></property><property name="Service" type="o" access="read"></property><property name="Value" type="ay" access="re
ad"></property><property name="Notifying" type="b" access="read"></property><property name="Flags" type="as" access="read"></property></interface><interface name="org.freedesktop.DBus.Properties"><method name="Get"><arg name="interface" t
ype="s" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="value" type="v" direction="out"/>
</method><method name="Set"><arg name="interface" type="s" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="value" type="v" direction="in"/>
</method><method name="GetAll"><arg name="interface" type="s" direction="in"/>
<arg name="properties" type="a{sv}" direction="out"/>
</method><signal name="PropertiesChanged"><arg name="interface" type="s"/>
<arg name="changed_properties" type="a{sv}"/>
<arg name="invalidated_properties" type="as"/>
</signal>
</interface></node>"
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ sudo dbus-send --print-reply --system --dest=org.bluez /org/bluez/hci0/dev_49_25_2A_9A_90_44/service004c/char0050 --type=method_call org.freedesktop.DBus.Properties.GetAll string:org.bluez.GattCharacteristic1
method return sender=:1.3 -> dest=:1.58 reply_serial=2
   array [
      dict entry(
         string "UUID"
         variant             string "00002a38-0000-1000-8000-00805f9b34fb"
      )
      dict entry(
         string "Service"
         variant             object path "/org/bluez/hci0/dev_49_25_2A_9A_90_44/service004c"
      )
      dict entry(
         string "Value"
         variant             array [
            ]
      )
      dict entry(
         string "Flags"
         variant             array [
               string "read"
            ]
      )
   ]

 org.bluez.GattCharacteristic1 インタフェースには ReadValue メソッドがあり、キャラクタリスティックの値を参照するために使えます。ただ、このメソッドの引数には空の配列を渡したいのですが、 dbus-sendコマンドのドキュメントには下記のような記述があり、dbus-sendコマンドは空の配列やネストされた配列には対応してないようです。

D-Bus supports more types than these, but dbus-send currently does not. Also, dbus-send does not permit empty containers or nested containers (e.g. arrays of variants).

https://dbus.freedesktop.org/doc/dbus-send.1.html

 なので dbus-send での操作は一旦ここで諦め、RubyからD-Busを操作してみたいと思います。

RubyからD-Busを操作する

 RubyからD-Busを操作するためのgemとして、 ruby-dbus というgemが公開されているのでこちらを使用します。

github.com

 gemをインストールした上でirbを起動してrequireします。

pi@raspberrypi:~ $ sudo bundle exec irb
irb(main):001:0> require 'dbus'
=> true

 まずはD-Busのシステムバスのインスタンスを取得します。

irb(main):002:0> bus = DBus::SystemBus.instance
/home/pi/vendor/bundle/ruby/2.4.0/gems/ruby-dbus-0.13.0/lib/dbus/marshall.rb:299: warning: constant ::Fixnum is deprecated
=> #<DBus::SystemBus:0x56b32d08 @message_queue=#<DBus::MessageQueue:0x56b32cc0 @address="unix:path=/var/run/dbus/system_bus_socket", @buffer="l\x04\x01\x01\n\x00\x00\x00\x02\x00\x00\x00\x8D\x00\x00\x00\x01\x01o\x00\x15\x00\x00\x00/org/fre
edesktop/DBus\x00\x00\x00\x02\x01s\x00\x14\x00\x00\x00org.freedesktop.DBus\x00\x00\x00\x00\x03\x01s\x00\f\x00\x00\x00NameAcquired\x00\x00\x00\x00\x06\x01s\x00\x05\x00\x00\x00:1.46\x00\x00\x00\b\x01g\x00\x01s\x00\x00\a\x01s\x00\x14\x00\x00
\x00org.freedesktop.DBus\x00\x00\x00\x00\x05\x00\x00\x00:1.46\x00", @is_tcp=false, @socket=#<Socket:fd 9>, @client=#<DBus::Client:0x56b32570 @socket=#<Socket:fd 9>, @state=:Authenticated, @auth_list=[DBus::DBusCookieSHA1, DBus::Anonymous]
, @authenticator=#<DBus::External:0x56b31c70>>>, @unique_name=":1.46", @method_call_replies={}, @method_call_msgs={}, @signal_matchrules={}, @proxy=nil, @object_root=<DBus::Node {}>, @service=#<DBus::Service:0x56b17870 @name=":1.46", @bus
=#<DBus::SystemBus:0x56b32d08 ...>, @root=<DBus::Node {}>>>

 そして dbus-send の時と同様に、D-Busに登録されているサービスを確認してみます。

irb(main):003:0> bus.proxy.ListNames[0]
=> ["org.freedesktop.DBus", ":1.3", "org.freedesktop.login1", "org.freedesktop.systemd1", ":1.46", ":1.31", "org.freedesktop.Avahi", ":1.0", "org.bluez", ":1.1", ":1.2"]

 ruby-dbus でD-Busのサービスを参照するには、 service メソッドでサービス名を指定します。

irb(main):004:0> bluez = bus.service('org.bluez')
=> #<DBus::Service:0x56b0e8e8 @name="org.bluez", @bus=#<DBus::SystemBus:0x56b32d08 @message_queue=#<DBus::MessageQueue:0x56b32cc0 @address="unix:path=/var/run/dbus/system_bus_socket", @buffer="l\x04\x01\x01\n\x00\x00\x00\x02\x00\x00\x00\x
8D\x00\x00\x00\x01\x01o\x00\x15\x00\x00\x00/org/freedesktop/DBus\x00\x00\x00\x02\x01s\x00\x14\x00\x00\x00org.freedesktop.DBus\x00\x00\x00\x00\x03\x01s\x00\f\x00\x00\x00NameAcquired\x00\x00\x00\x00\x06\x01s\x00\x05\x00\x00\x00:1.46\x00\x00
\x00\b\x01g\x00\x01s\x00\x00\a\x01s\x00\x14\x00\x00\x00org.freedesktop.DBus\x00\x00\x00\x00\x05\x00\x00\x00:1.46\x00", @is_tcp=false, @socket=#<Socket:fd 9>, @client=#<DBus::Client:0x56b32570 @socket=#<Socket:fd 9>, @state=:Authenticated,
 @auth_list=[DBus::DBusCookieSHA1, DBus::Anonymous], @authenticator=#<DBus::External:0x56b31c70>>>, @unique_name=":1.46", @method_call_replies={}, @method_call_msgs={}, @signal_matchrules={}, @proxy=nil, @object_root=<DBus::Node {}>, @ser
vice=#<DBus::Service:0x56b17870 @name=":1.46", @bus=#<DBus::SystemBus:0x56b32d08 ...>, @root=<DBus::Node {}>>>, @root=<DBus::Node {}>>

 introspect メソッドでオブジェクトのリストを参照できます。

irb(main):005:0> bluez.introspect
=> #<DBus::Service:0x560d6990 @name="org.bluez", @bus=#<DBus::SystemBus:0x560f9448 @message_queue=#<DBus::MessageQueue:0x560f9388 @address="unix:path=/var/run/dbus/system_bus_socket", @buffer="", @is_tcp=false, @socket=#<Socket:fd 9>, @client=#<DBus::Client:0x560f8a58 @socket=#<Socket:fd 9>, @state=:Authenticated, @auth_list=[DBus::DBusCookieSHA1, DBus::Anonymous], @authenticator=#<DBus::External:0x560f89e0>>>, @unique_name=":1.6", @method_call_replies={36=>#<Proc:0x564acfb0@/home/pi/vendor/bundle/ruby/2.4.0/gems/ruby-dbus-0.13.0/lib/dbus/bus.rb:339>, 38=>#<Proc:0x5649f408@/home/pi/vendor/bundle/ruby/2.4.0/gems/ruby-dbus-0.13.0/lib/dbus/bus.rb:339>, 142=>#<Proc:0x55bc79b0@/home/pi/vendor/bundle/ruby/2.4.0/gems/ruby-dbus-0.13.0/lib/dbus/bus.rb:339>, 196=>#<Proc:0x56500038@/home/pi/vendor/bundle/ruby/2.4.0/gems/ruby-dbus-0.13.0/lib/dbus/bus.rb:339>, 216=>#<Proc:0x55dcebf0@/home/pi/vendor/bundle/ruby/2.4.0/gems/ruby-dbus-0.13.0/lib/dbus/bus.rb:339>, 224=>#<Proc:0x56135508@/home/pi/vendor/bundle/ruby/2.4.0/gems/ruby-dbus-0.13.0/lib/dbus/bus.rb:339>}, @method_call_msgs={36=>#<DBus::Message:0x564ad010 @message_type=1, @flags=0, @protocol=1, @body_length=0, @signature="", @serial=36, @params=[], @destination="org.bluez", @interface="org.bluez.Device1", @error_name=nil, @member="Connect", @path="/org/bluez/hci0/dev_53_B5_AD_3D_0E_8D", @reply_serial=nil, @sender=":1.6">, 38=>#<DBus::Message:0x5649f468 @message_type=1, @flags=0, @protocol=1, @body_length=0, @signature="", @serial=38, @params=[], @destination="org.bluez", @interface="org.bluez.Device1", @error_name=nil, @member="Disconnect", @path="/org/bluez/hci0/dev_53_B5_AD_3D_0E_8D", @reply_serial=nil, @sender=":1.6">, 142=>#<DBus::Message:0x55bc7a28 @message_type=1, @flags=0, @protocol=1, @body_length=0, @signature="", @serial=142, @params=[], @destination="org.bluez", @interface="org.bluez.Device1", @error_name=nil, @member="Connect", @path="/org/bluez/hci0/dev_55_6D_EC_78_2A_61", @reply_serial=nil, @sender=":1.6">, 196=>#<DBus::Message:0x565001d0 @message_type=1, @flags=0, @protocol=1, @body_length=34, @signature="s", @serial=196, @params=[["s", "org.bluez.GattCharacteristic1"]], @destination="org.bluez", @interface="org.freedesktop.DBus.Properties", @error_name=nil, @member="GetAll", @path="/org/bluez/hci0/dev_68_32_1E_76_3F_5D/service004c/char0050", @reply_serial=nil, @sender=":1.6">, 216=>#<DBus::Message:0x55dcf028 @message_type=1, @flags=0, @protocol=1, @body_length=16, @signature="aya{sv}", @serial=216, @params=[["ay", [3]], ["a{sv}", {}]], @destination="org.bluez", @interface="org.bluez.GattCharacteristic1", @error_name=nil, @member="WriteValue", @path="/org/bluez/hci0/dev_68_32_1E_76_3F_5D/service004c/char0050", @reply_serial=nil, @sender=":1.6">, 224=>#<DBus::Message:0x56135688 @message_type=1, @flags=0, @protocol=1, @body_length=16, @signature="aya{sv}", @serial=224, @params=[["ay", [3]], ["a{sv}", {}]], @destination="org.bluez", @interface="org.bluez.GattCharacteristic1", @error_name=nil, @member="WriteValue", @path="/org/bluez/hci0/dev_68_32_1E_76_3F_5D/service004c/char0050", @reply_serial=nil, @sender=":1.6">}, @signal_matchrules={}, @proxy=nil, @object_root=<DBus::Node {}>, @service=#<DBus::Service:0x560e0158 @name=":1.6", @bus=#<DBus::SystemBus:0x560f9448 ...>, @root=<DBus::Node {}>>>, @root=<DBus::Node 2b28631c {org => {bluez => 2b091c4c {hci0 => 2b0c82a0 {dev_04_05_F2_46_99_7D => 2b078efc {},dev_20_16_06_23_98_85 => 2adec438 {},dev_34_36_3B_C7_FB_E9 => 2b268f94 {},dev_43_19_24_21_00_5F => 2b0b3b64 {},dev_47_03_50_13_27_49 => 2b271ebc {},dev_55_6D_EC_78_2A_61 => 2b24713c {},dev_5A_81_CF_0B_28_3B => 2b094730 {},dev_5E_2A_AF_7F_B9_29 => 2ade5ab4 {},dev_68_32_1E_76_3F_5D => 2b264a34 {},dev_68_81_60_74_D0_31 => 2b23aa34 {},dev_69_96_A2_B0_24_B2 => 2af6fb88 {},dev_78_C3_DC_6E_59_BE => 2b275ad4 {},dev_88_4A_EA_8A_39_D2 => 2b242de0 {},dev_88_4A_EA_8A_3F_2B => 2af7da00 {},dev_FC_E9_98_21_23_B7 => 2afc0108 {}}}}}>>

 また、 root メソッドで配下のオブジェクトのツリー構造を確認することができます。

irb(main):006:0> bluez.root
=> <DBus::Node 2b28631c {org => {bluez => 2b091c4c {hci0 => 2b0c82a0 {dev_04_05_F2_46_99_7D => 2b078efc {},dev_20_16_06_23_98_85 => 2adec438 {},dev_34_36_3B_C7_FB_E9 => 2b268f94 {},dev_43_19_24_21_00_5F => 2b0b3b64 {},dev_47_03_50_13_27_49 => 2b271ebc {},dev_55_6D_EC_78_2A_61 => 2b24713c {},dev_5A_81_CF_0B_28_3B => 2b094730 {},dev_5E_2A_AF_7F_B9_29 => 2ade5ab4 {},dev_68_32_1E_76_3F_5D => 2b264a34 {},dev_68_81_60_74_D0_31 => 2b23aa34 {},dev_69_96_A2_B0_24_B2 => 2af6fb88 {},dev_78_C3_DC_6E_59_BE => 2b275ad4 {},dev_88_4A_EA_8A_39_D2 => 2b242de0 {},dev_88_4A_EA_8A_3F_2B => 2af7da00 {},dev_FC_E9_98_21_23_B7 => 2afc0108 {}}}}}>

 オブジェクトのインスタンスを取得するには object メソッドを使用します。hci0インタフェース配下のデバイスのインスタンスを取得してみます。

irb(main):007:0> device = bluez.object('/org/bluez/hci0/dev_55_6D_EC_78_2A_61')                                                                                                                                                               
=> #<DBus::ProxyObject:0x561831a8 @bus=#<DBus::SystemBus:0x560f9448 @message_queue=#<DBus::MessageQueue:0x560f9388 @address="unix:path=/var/run/dbus/system_bus_socket", @buffer="", @is_tcp=false, @socket=#<Socket:fd 9>, @client=#<DBus::Cl
ient:0x560f8a58 @socket=#<Socket:fd 9>, @state=:Authenticated, @auth_list=[DBus::DBusCookieSHA1, DBus::Anonymous], @authenticator=#<DBus::External:0x560f89e0>>>, @unique_name=":1.6", @method_call_replies={36=>#<Proc:0x564acfb0@/home/pi/ve
ndor/bundle/ruby/2.4.0/gems/ruby-dbus-0.13.0/lib/dbus/bus.rb:339>, 38=>#<Proc:0x5649f408@/home/pi/vendor/bundle/ruby/2.4.0/gems/ruby-dbus-0.13.0/lib/dbus/bus.rb:339>, 142=>#<Proc:0x55bc79b0@/home/pi/vendor/bundle/ruby/2.4.0/gems/ruby-dbus
-0.13.0/lib/dbus/bus.rb:339>}, @method_call_msgs={36=>#<DBus::Message:0x564ad010 @message_type=1, @flags=0, @protocol=1, @body_length=0, @signature="", @serial=36, @params=[], @destination="org.bluez", @interface="org.bluez.Device1", @err
or_name=nil, @member="Connect", @path="/org/bluez/hci0/dev_53_B5_AD_3D_0E_8D", @reply_serial=nil, @sender=":1.6">, 38=>#<DBus::Message:0x5649f468 @message_type=1, @flags=0, @protocol=1, @body_length=0, @signature="", @serial=38, @params=[
], @destination="org.bluez", @interface="org.bluez.Device1", @error_name=nil, @member="Disconnect", @path="/org/bluez/hci0/dev_53_B5_AD_3D_0E_8D", @reply_serial=nil, @sender=":1.6">, 142=>#<DBus::Message:0x55bc7a28 @message_type=1, @flags
=0, @protocol=1, @body_length=0, @signature="", @serial=142, @params=[], @destination="org.bluez", @interface="org.bluez.Device1", @error_name=nil, @member="Connect", @path="/org/bluez/hci0/dev_55_6D_EC_78_2A_61", @reply_serial=nil, @send
er=":1.6">}, @signal_matchrules={}, @proxy=nil, @object_root=<DBus::Node {}>, @service=#<DBus::Service:0x560e0158 @name=":1.6", @bus=#<DBus::SystemBus:0x560f9448 ...>, @root=<DBus::Node {}>>>, @destination="org.bluez", @path="/org/bluez/h
ci0/dev_68_32_1E_76_3F_5D", @introspected=false, @interfaces={}, @subnodes=[], @api=#<DBus::ApiOptions:0x55d91ee8 @proxy_method_returns_array=true>>
irb(main):008:0> 
irb(main):009:0* device.introspect
=> "<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\"\n\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n<node><interface name=\"org.freedesktop.DBus.Introspectable\"><method name=\"Introspe
ct\"><arg name=\"xml\" type=\"s\" direction=\"out\"/>\n</method></interface><interface name=\"org.bluez.Device1\"><method name=\"Disconnect\"></method><method name=\"Connect\"></method><method name=\"ConnectProfile\"><arg name=\"UUID\" ty
pe=\"s\" direction=\"in\"/>\n</method><method name=\"DisconnectProfile\"><arg name=\"UUID\" type=\"s\" direction=\"in\"/>\n</method><method name=\"Pair\"></method><method name=\"CancelPairing\"></method><property name=\"Address\" type=\"s
\" access=\"read\"></property><property name=\"Name\" type=\"s\" access=\"read\"></property><property name=\"Alias\" type=\"s\" access=\"readwrite\"></property><property name=\"Class\" type=\"u\" access=\"read\"></property><property name=
\"Appearance\" type=\"q\" access=\"read\"></property><property name=\"Icon\" type=\"s\" access=\"read\"></property><property name=\"Paired\" type=\"b\" access=\"read\"></property><property name=\"Trusted\" type=\"b\" access=\"readwrite\">
</property><property name=\"Blocked\" type=\"b\" access=\"readwrite\"></property><property name=\"LegacyPairing\" type=\"b\" access=\"read\"></property><property name=\"RSSI\" type=\"n\" access=\"read\"></property><property name=\"Connect
ed\" type=\"b\" access=\"read\"></property><property name=\"UUIDs\" type=\"as\" access=\"read\"></property><property name=\"Modalias\" type=\"s\" access=\"read\"></property><property name=\"Adapter\" type=\"o\" access=\"read\"></property>
<property name=\"ManufacturerData\" type=\"a{qv}\" access=\"read\"></property><property name=\"ServiceData\" type=\"a{sv}\" access=\"read\"></property><property name=\"TxPower\" type=\"n\" access=\"read\"></property><property name=\"Servi
cesResolved\" type=\"b\" access=\"read\"></property></interface><interface name=\"org.freedesktop.DBus.Properties\"><method name=\"Get\"><arg name=\"interface\" type=\"s\" direction=\"in\"/>\n<arg name=\"name\" type=\"s\" direction=\"in\"
/>\n<arg name=\"value\" type=\"v\" direction=\"out\"/>\n</method><method name=\"Set\"><arg name=\"interface\" type=\"s\" direction=\"in\"/>\n<arg name=\"name\" type=\"s\" direction=\"in\"/>\n<arg name=\"value\" type=\"v\" direction=\"in\"
/>\n</method><method name=\"GetAll\"><arg name=\"interface\" type=\"s\" direction=\"in\"/>\n<arg name=\"properties\" type=\"a{sv}\" direction=\"out\"/>\n</method><signal name=\"PropertiesChanged\"><arg name=\"interface\" type=\"s\"/>\n<ar
g name=\"changed_properties\" type=\"a{sv}\"/>\n<arg name=\"invalidated_properties\" type=\"as\"/>\n</signal>\n</interface></node>"

 Connectメソッドでデバイスに接続して、オブジェクトのリストを表示してみます。

irb(main):010:0* device.Connect
=> []
irb(main):011:0>
irb(main):012:0* device.introspect
=> "<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\"\n\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n<node><interface name=\"org.freedesktop.DBus.Introspectable\"><method name=\"Introspe
ct\"><arg name=\"xml\" type=\"s\" direction=\"out\"/>\n</method></interface><interface name=\"org.bluez.Device1\"><method name=\"Disconnect\"></method><method name=\"Connect\"></method><method name=\"ConnectProfile\"><arg name=\"UUID\" ty
pe=\"s\" direction=\"in\"/>\n</method><method name=\"DisconnectProfile\"><arg name=\"UUID\" type=\"s\" direction=\"in\"/>\n</method><method name=\"Pair\"></method><method name=\"CancelPairing\"></method><property name=\"Address\" type=\"s
\" access=\"read\"></property><property name=\"Name\" type=\"s\" access=\"read\"></property><property name=\"Alias\" type=\"s\" access=\"readwrite\"></property><property name=\"Class\" type=\"u\" access=\"read\"></property><property name=
\"Appearance\" type=\"q\" access=\"read\"></property><property name=\"Icon\" type=\"s\" access=\"read\"></property><property name=\"Paired\" type=\"b\" access=\"read\"></property><property name=\"Trusted\" type=\"b\" access=\"readwrite\">
</property><property name=\"Blocked\" type=\"b\" access=\"readwrite\"></property><property name=\"LegacyPairing\" type=\"b\" access=\"read\"></property><property name=\"RSSI\" type=\"n\" access=\"read\"></property><property name=\"Connect
ed\" type=\"b\" access=\"read\"></property><property name=\"UUIDs\" type=\"as\" access=\"read\"></property><property name=\"Modalias\" type=\"s\" access=\"read\"></property><property name=\"Adapter\" type=\"o\" access=\"read\"></property>
<property name=\"ManufacturerData\" type=\"a{qv}\" access=\"read\"></property><property name=\"ServiceData\" type=\"a{sv}\" access=\"read\"></property><property name=\"TxPower\" type=\"n\" access=\"read\"></property><property name=\"Servi
cesResolved\" type=\"b\" access=\"read\"></property></interface><interface name=\"org.freedesktop.DBus.Properties\"><method name=\"Get\"><arg name=\"interface\" type=\"s\" direction=\"in\"/>\n<arg name=\"name\" type=\"s\" direction=\"in\"
/>\n<arg name=\"value\" type=\"v\" direction=\"out\"/>\n</method><method name=\"Set\"><arg name=\"interface\" type=\"s\" direction=\"in\"/>\n<arg name=\"name\" type=\"s\" direction=\"in\"/>\n<arg name=\"value\" type=\"v\" direction=\"in\"
/>\n</method><method name=\"GetAll\"><arg name=\"interface\" type=\"s\" direction=\"in\"/>\n<arg name=\"properties\" type=\"a{sv}\" direction=\"out\"/>\n</method><signal name=\"PropertiesChanged\"><arg name=\"interface\" type=\"s\"/>\n<ar
g name=\"changed_properties\" type=\"a{sv}\"/>\n<arg name=\"invalidated_properties\" type=\"as\"/>\n</signal>\n</interface><node name=\"service0006\"/><node name=\"service000a\"/><node name=\"service000e\"/><node name=\"service0014\"/><no
de name=\"service0019\"/><node name=\"service0023\"/><node name=\"service002f\"/><node name=\"service0034\"/><node name=\"service0039\"/><node name=\"service004c\"/></node>"

 subnodes メソッドを使うと配下のオブジェクトのリストが参照できます。

irb(main):013:0> device.subnodes
=> ["service0006", "service000a", "service000e", "service0014", "service0019", "service0023", "service002f", "service0034", "service0039", "service004c"]

 配下のサービスとキャラクタリスティックを確認してみます。今回対象としているHeart Rateのサービスは service004c になります。

irb(main):014:0> service = bluez.object('/org/bluez/hci0/dev_55_6D_EC_78_2A_61/service004c')                                                                                                                                                  
=> #<DBus::ProxyObject:0x55ef9cd8 @bus=#<DBus::SystemBus:0x560f9448 @message_queue=#<DBus::MessageQueue:0x560f9388 @address="unix:path=/var/run/dbus/system_bus_socket", @buffer="", @is_tcp=false, @socket=#<Socket:fd 9>, @client=#<DBus::Cl
ient:0x560f8a58 @socket=#<Socket:fd 9>, @state=:Authenticated, @auth_list=[DBus::DBusCookieSHA1, DBus::Anonymous], @authenticator=#<DBus::External:0x560f89e0>>>, @unique_name=":1.6", @method_call_replies={36=>#<Proc:0x564acfb0@/home/pi/ve
ndor/bundle/ruby/2.4.0/gems/ruby-dbus-0.13.0/lib/dbus/bus.rb:339>, 38=>#<Proc:0x5649f408@/home/pi/vendor/bundle/ruby/2.4.0/gems/ruby-dbus-0.13.0/lib/dbus/bus.rb:339>}, @method_call_msgs={36=>#<DBus::Message:0x564ad010 @message_type=1, @fl
ags=0, @protocol=1, @body_length=0, @signature="", @serial=36, @params=[], @destination="org.bluez", @interface="org.bluez.Device1", @error_name=nil, @member="Connect", @path="/org/bluez/hci0/dev_53_B5_AD_3D_0E_8D", @reply_serial=nil, @se
nder=":1.6">, 38=>#<DBus::Message:0x5649f468 @message_type=1, @flags=0, @protocol=1, @body_length=0, @signature="", @serial=38, @params=[], @destination="org.bluez", @interface="org.bluez.Device1", @error_name=nil, @member="Disconnect", @
path="/org/bluez/hci0/dev_53_B5_AD_3D_0E_8D", @reply_serial=nil, @sender=":1.6">}, @signal_matchrules={}, @proxy=nil, @object_root=<DBus::Node {}>, @service=#<DBus::Service:0x560e0158 @name=":1.6", @bus=#<DBus::SystemBus:0x560f9448 ...>, 
@root=<DBus::Node {}>>>, @destination="org.bluez", @path="/org/bluez/hci0/dev_55_6D_EC_78_2A_61/service004c", @introspected=false, @interfaces={}, @subnodes=[], @api=#<DBus::ApiOptions:0x55d91ee8 @proxy_method_returns_array=true>>
irb(main):015:0> 
irb(main):016:0* service.introspect
=> "<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\"\n\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n<node><interface name=\"org.freedesktop.DBus.Introspectable\"><method name=\"Introspe
ct\"><arg name=\"xml\" type=\"s\" direction=\"out\"/>\n</method></interface><interface name=\"org.bluez.GattService1\"><property name=\"UUID\" type=\"s\" access=\"read\"></property><property name=\"Device\" type=\"o\" access=\"read\"></pr
operty><property name=\"Primary\" type=\"b\" access=\"read\"></property><property name=\"Includes\" type=\"ao\" access=\"read\"></property></interface><interface name=\"org.freedesktop.DBus.Properties\"><method name=\"Get\"><arg name=\"in
terface\" type=\"s\" direction=\"in\"/>\n<arg name=\"name\" type=\"s\" direction=\"in\"/>\n<arg name=\"value\" type=\"v\" direction=\"out\"/>\n</method><method name=\"Set\"><arg name=\"interface\" type=\"s\" direction=\"in\"/>\n<arg name=
\"name\" type=\"s\" direction=\"in\"/>\n<arg name=\"value\" type=\"v\" direction=\"in\"/>\n</method><method name=\"GetAll\"><arg name=\"interface\" type=\"s\" direction=\"in\"/>\n<arg name=\"properties\" type=\"a{sv}\" direction=\"out\"/>
\n</method><signal name=\"PropertiesChanged\"><arg name=\"interface\" type=\"s\"/>\n<arg name=\"changed_properties\" type=\"a{sv}\"/>\n<arg name=\"invalidated_properties\" type=\"as\"/>\n</signal>\n</interface><node name=\"char004d\"/><no
de name=\"char0050\"/><node name=\"char0052\"/></node>"
irb(main):017:0>
irb(main):018:0* service.subnodes
=> ["char004d", "char0050", "char0052"]

 そして Body Sensor Locationキャラクタリスティックは char0050 です。

irb(main):019:0* characteristic = bluez.object('/org/bluez/hci0/dev_55_6D_EC_78_2A_61/service004c/char0050')
=> #<DBus::ProxyObject:0x564f5418 @bus=#<DBus::SystemBus:0x560f9448 @message_queue=#<DBus::MessageQueue:0x560f9388 @address="unix:path=/var/run/dbus/system_bus_socket", @buffer="", @is_tcp=false, @socket=#<Socket:fd 9>, @client=#<DBus::Cl
ient:0x560f8a58 @socket=#<Socket:fd 9>, @state=:Authenticated, @auth_list=[DBus::DBusCookieSHA1, DBus::Anonymous], @authenticator=#<DBus::External:0x560f89e0>>>, @unique_name=":1.6", @method_call_replies={36=>#<Proc:0x564acfb0@/home/pi/ve
ndor/bundle/ruby/2.4.0/gems/ruby-dbus-0.13.0/lib/dbus/bus.rb:339>, 38=>#<Proc:0x5649f408@/home/pi/vendor/bundle/ruby/2.4.0/gems/ruby-dbus-0.13.0/lib/dbus/bus.rb:339>}, @method_call_msgs={36=>#<DBus::Message:0x564ad010 @message_type=1, @fl
ags=0, @protocol=1, @body_length=0, @signature="", @serial=36, @params=[], @destination="org.bluez", @interface="org.bluez.Device1", @error_name=nil, @member="Connect", @path="/org/bluez/hci0/dev_53_B5_AD_3D_0E_8D", @reply_serial=nil, @se
nder=":1.6">, 38=>#<DBus::Message:0x5649f468 @message_type=1, @flags=0, @protocol=1, @body_length=0, @signature="", @serial=38, @params=[], @destination="org.bluez", @interface="org.bluez.Device1", @error_name=nil, @member="Disconnect", @
path="/org/bluez/hci0/dev_53_B5_AD_3D_0E_8D", @reply_serial=nil, @sender=":1.6">}, @signal_matchrules={}, @proxy=nil, @object_root=<DBus::Node {}>, @service=#<DBus::Service:0x560e0158 @name=":1.6", @bus=#<DBus::SystemBus:0x560f9448 ...>, 
@root=<DBus::Node {}>>>, @destination="org.bluez", @path="/org/bluez/hci0/dev_55_6D_EC_78_2A_61/service004c/char0050", @introspected=false, @interfaces={}, @subnodes=[], @api=#<DBus::ApiOptions:0x55d91ee8 @proxy_method_returns_array=true>
>
irb(main):020:0> 
irb(main):021:0* characteristic.introspect                                                                                                                                                                                                    
=> "<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\"\n\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n<node><interface name=\"org.freedesktop.DBus.Introspectable\"><method name=\"Introspe
ct\"><arg name=\"xml\" type=\"s\" direction=\"out\"/>\n</method></interface><interface name=\"org.bluez.GattCharacteristic1\"><method name=\"ReadValue\"><arg name=\"options\" type=\"a{sv}\" direction=\"in\"/>\n<arg name=\"value\" type=\"a
y\" direction=\"out\"/>\n</method><method name=\"WriteValue\"><arg name=\"value\" type=\"ay\" direction=\"in\"/>\n<arg name=\"options\" type=\"a{sv}\" direction=\"in\"/>\n</method><method name=\"StartNotify\"></method><method name=\"StopN
otify\"></method><property name=\"UUID\" type=\"s\" access=\"read\"></property><property name=\"Service\" type=\"o\" access=\"read\"></property><property name=\"Value\" type=\"ay\" access=\"read\"></property><property name=\"Notifying\" t
ype=\"b\" access=\"read\"></property><property name=\"Flags\" type=\"as\" access=\"read\"></property></interface><interface name=\"org.freedesktop.DBus.Properties\"><method name=\"Get\"><arg name=\"interface\" type=\"s\" direction=\"in\"/
>\n<arg name=\"name\" type=\"s\" direction=\"in\"/>\n<arg name=\"value\" type=\"v\" direction=\"out\"/>\n</method><method name=\"Set\"><arg name=\"interface\" type=\"s\" direction=\"in\"/>\n<arg name=\"name\" type=\"s\" direction=\"in\"/>
\n<arg name=\"value\" type=\"v\" direction=\"in\"/>\n</method><method name=\"GetAll\"><arg name=\"interface\" type=\"s\" direction=\"in\"/>\n<arg name=\"properties\" type=\"a{sv}\" direction=\"out\"/>\n</method><signal name=\"PropertiesCh
anged\"><arg name=\"interface\" type=\"s\"/>\n<arg name=\"changed_properties\" type=\"a{sv}\"/>\n<arg name=\"invalidated_properties\" type=\"as\"/>\n</signal>\n</interface></node>"
irb(main):022:0> 
irb(main):023:0* characteristic.subnodes
=> []

 interfaces でオブジェクトが持っているインタフェースのリストを参照できます。

irb(main):024:0> characteristic.interfaces
=> ["org.freedesktop.DBus.Introspectable", "org.bluez.GattCharacteristic1", "org.freedesktop.DBus.Properties"]

 また、プロパティを参照するには GetAll メソッドを使用し、引数にインタフェース名を指定します。

irb(main):025:0> characteristic.GetAll('org.bluez.GattCharacteristic1')
=> [{"UUID"=>"00002a38-0000-1000-8000-00805f9b34fb", "Service"=>"/org/bluez/hci0/dev_55_6D_EC_78_2A_61/service004c", "Value"=>[], "Flags"=>["read"]}]

 そして、dbus-sendではできなかった、ReadValueメソッドを使った値の参照を実行してみます。LightBlueでBody Sensor Locationの値を 0x01 に設定しておきます。

f:id:akanuma-hiroaki:20170610100405p:plain:w300

 ReadValueメソッドで参照してみます。

irb(main):026:0> characteristic.ReadValue([])
=> [[1]]

 配列として値が参照できました。LightBlueで値を 0x02 に変更してもう一度参照してみます。

f:id:akanuma-hiroaki:20170610100721p:plain:w300

irb(main):027:0> characteristic.ReadValue([])
=> [[2]]

 今度はWriteValueメソッドで値を書き込んでみます。値は配列で指定します。また、第二引数にオプションをHashで指定しますが、今回は特にオプションはないので空のHashを渡します。

irb(main):028:0> characteristic.WriteValue([0x03], {})
=> []
irb(main):029:0> 
irb(main):030:0* characteristic.ReadValue([])                                                                                                                                                                                                 
=> [[3]]

 書き込んだ値が参照できました。LightBlue側で見ても値が変わっているのが確認できました。

f:id:akanuma-hiroaki:20170610100922p:plain:w300

 今回は単純な値の読み書きだけで、Notificationなどはまだ使えていないですが、今後Notification周りも使えるようにして、デバイスの値の変化を検知して処理を行うようなものを作ってみたいと思います。

Raspberry Pi 3でBluetoothデバイス接続

 Raspberry Pi 3 からは標準でBluetoothモジュールが搭載されているということで、他のデバイスとの接続を試してみました。

BlueZインストール

 BlueZはオープンソースのBluetoothプロトコルスタックで、Linux上でBluetooth, BLEを扱う場合には標準的に使われているということなので、インストールします。

 まずはソースをダウンロードして解凍します。

pi@raspberrypi:~/tmp $ wget http://www.kernel.org/pub/linux/bluetooth/bluez-5.45.tar.xz
--2017-05-27 10:23:48--  http://www.kernel.org/pub/linux/bluetooth/bluez-5.45.tar.xz
Resolving www.kernel.org (www.kernel.org)... 147.75.110.187
Connecting to www.kernel.org (www.kernel.org)|147.75.110.187|:80... connected.
HTTP request sent, awaiting response... 301 Moved Permanently
Location: https://www.kernel.org/pub/linux/bluetooth/bluez-5.45.tar.xz [following]
--2017-05-27 10:23:48--  https://www.kernel.org/pub/linux/bluetooth/bluez-5.45.tar.xz
Connecting to www.kernel.org (www.kernel.org)|147.75.110.187|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1672404 (1.6M) [application/x-xz]
Saving to: ‘bluez-5.45.tar.xz’

bluez-5.45.tar.xz                                           100%[===========================================================================================================================================>]   1.59M  2.56MB/s   in 0.6s   

2017-05-27 10:23:49 (2.56 MB/s) - ‘bluez-5.45.tar.xz’ saved [1672404/1672404]

pi@raspberrypi:~/tmp $ xz -dv bluez-5.45.tar.xz 
bluez-5.45.tar.xz (1/1)
  100 %      1,633.2 KiB / 14.4 MiB = 0.111                                    
pi@raspberrypi:~/tmp $ tar -xf bluez-5.45.tar 
pi@raspberrypi:~/tmp $ cd bluez-5.45/

 ビルドに必要なライブラリをインストールします。configureしながら確認した結果、下記ライブラリをインストールしました。

pi@raspberrypi:~/tmp/bluez-5.45 $ sudo apt-get install -y libglib2.0-dev
pi@raspberrypi:~/tmp/bluez-5.45 $ sudo apt-get install -y libdbus-1-dev
pi@raspberrypi:~/tmp/bluez-5.45 $ sudo apt-get install -y libudev-dev
pi@raspberrypi:~/tmp/bluez-5.45 $ sudo apt-get install -y libical-dev

 そして下記コマンドでビルド、インストールします。

pi@raspberrypi:~/tmp/bluez-5.45 $ ./configure --enable-experimental
pi@raspberrypi:~/tmp/bluez-5.45 $ make
pi@raspberrypi:~/tmp/bluez-5.45 $ sudo make install

 無事にインストールできたらひとまずBluetoothデバイスをスキャンしてみます。hcitoolというコマンドを使って、Bluetoothデバイスを検索する場合は scan、BLEデバイスを検索する場合は lescan をオプションとして渡します。

pi@raspberrypi:~ $ hcitool
hcitool - HCI Tool ver 5.23
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ sudo hcitool lescan
LE Scan ...
74:DE:1A:E6:4E:4F (unknown)
34:36:3B:C7:FB:E9 (unknown)
34:36:3B:C7:FB:E9 (unknown)
74:DE:1A:E6:4E:4F (unknown)
F9:D8:AA:9A:CF:96 (unknown)
F9:D8:AA:9A:CF:96 Charge HR
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ sudo hcitool scan                                                                                                                                                                                                          
Scanning ...
        FC:E9:98:21:23:B7       Akanuma_Hiroaki_iPhone
pi@raspberrypi:~ $ 

 とりあえずデバイスの検知は行えているようです。

Bluetooth関連の各種ツール

 下記サイトを参考に、各種ツールのインストールやバージョンの確認をしてみました。

Raspberry Pi 3に Bluetooth BlueZ Version 5.42 BLE (ラズパイで Bluetooth 4.0の BLE gatt通信を行なう TIの SensorTagや iBeacon実験など)

 BlueZをインストールすると対話型の設定ツールとしてbluetoothctlが使えるようになっていますので、バージョンを確認してみます。

pi@raspberrypi:~/tmp/bluez-5.45 $ bluetoothctl 
[NEW] Controller B8:27:EB:19:76:07 raspberrypi [default]
[bluetooth]# version
Version 5.23
[bluetooth]# quit
[DEL] Controller B8:27:EB:19:76:07 raspberrypi [default]

 Bluetoothデバイス間のトラフィックのキャプチャツールとして、bluez-hcidumpをインストールしておきます。

pi@raspberrypi:~/tmp/bluez-5.45 $ sudo apt-get install bluez-hcidump
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following packages were automatically installed and are no longer required:
  libbison-dev libsigsegv2 m4
Use 'apt-get autoremove' to remove them.
The following NEW packages will be installed:
  bluez-hcidump
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
Need to get 157 kB of archives.
After this operation, 490 kB of additional disk space will be used.
Get:1 http://archive.raspberrypi.org/debian/ jessie/main bluez-hcidump armhf 5.23-2+rpi2 [157 kB]
Fetched 157 kB in 1s (91.8 kB/s)                          
Selecting previously unselected package bluez-hcidump.
(Reading database ... 36730 files and directories currently installed.)
Preparing to unpack .../bluez-hcidump_5.23-2+rpi2_armhf.deb ...
Unpacking bluez-hcidump (5.23-2+rpi2) ...
Processing triggers for man-db (2.7.0.2-5) ...
Setting up bluez-hcidump (5.23-2+rpi2) ...
pi@raspberrypi:~/tmp/bluez-5.45 $ 
pi@raspberrypi:~/tmp/bluez-5.45 $ hcidump
HCI sniffer - Bluetooth packet analyzer ver 5.23
device: hci0 snap_len: 1500 filter: 0xffffffff

 hciconfigではBluetoothインタフェースの状態が確認できます。

pi@raspberrypi:~/tmp/bluez-5.45 $ hciconfig
hci0:   Type: BR/EDR  Bus: UART
        BD Address: B8:27:EB:19:76:07  ACL MTU: 1021:8  SCO MTU: 64:1
        UP RUNNING 
        RX bytes:710 acl:0 sco:0 events:41 errors:0
        TX bytes:1496 acl:0 sco:0 commands:41 errors:0

iBeacon化してiPhoneから検知

 BlueZを使ってiBeacon化するためのツールとして下記が公開されているので、Raspberry Pi上で動かしてiPhoneから検知してみます。

github.com

 まずはソースをダウンロードして解凍します。

pi@raspberrypi:~/tmp $ git clone https://github.com/carsonmcdonald/bluez-ibeacon.git
Cloning into 'bluez-ibeacon'...
remote: Counting objects: 77, done.
remote: Total 77 (delta 0), reused 0 (delta 0), pack-reused 77
Unpacking objects: 100% (77/77), done.
Checking connectivity... done.
pi@raspberrypi:~/tmp $ 
pi@raspberrypi:~/tmp $ cd bluez-ibeacon/bluez-beacon/

 ビルドに必要なライブラリをインストール。

pi@raspberrypi:~/tmp/bluez-ibeacon/bluez-beacon $ sudo apt-get -y install libbluetooth-dev

 そしてビルドします。

pi@raspberrypi:~/tmp/bluez-ibeacon/bluez-beacon $ make
cc -g -o ibeacon ibeacon.c -lbluetooth

 無事ビルドできたら起動してみます。オプションはとりあえずサンプルと同じにしています。

pi@raspberrypi:~/tmp/bluez-ibeacon/bluez-beacon $ sudo ./ibeacon 200 e2c56db5dffb48d2b060d0f5a71096e0 1 1 -29                                                                                                                                 
Hit ctrl-c to stop advertising

 これでRaspberry Pi側はiBeaconとして動作しているはずなので、iPhoneに下記アプリをインストールしてiBeaconを検知してみます。

Locate Beacon

Locate Beacon

  • Radius Networks
  • Utilities
  • Free

f:id:akanuma-hiroaki:20170528203456p:plain:w300f:id:akanuma-hiroaki:20170528203505p:plain:w300

 距離の表示はだいぶずれてますが、ひとまず検知はされて、RSSIの情報も表示されました。

Bluetoothデバイスとペアリング&接続

 自宅にBluetooth接続のキーボードがあったのでペアリング&接続してみます。まずは bluetoothctl を起動して、デフォルトagentを設定します。

pi@raspberrypi:~ $ sudo bluetoothctl --agent=DisplayYesNo
[NEW] Controller B8:27:EB:19:76:07 raspberrypi [default]
[NEW] Device FC:E9:98:21:23:B7 Akanuma_Hiroaki_iPhone
Agent registered
[Akanuma_Hiroaki_iPhone]# default-agent
Default agent request successful

 続いてBluetoothデバイスのスキャンを開始します。Bluetooth 3.0 Keyboard が検知されたらスキャンを停止します。

[Akanuma_Hiroaki_iPhone]# scan on
Discovery started
[CHG] Controller B8:27:EB:19:76:07 Discovering: yes
[NEW] Device 34:36:3B:C7:FB:E9 34-36-3B-C7-FB-E9
[NEW] Device 20:16:06:23:98:85 20-16-06-23-98-85
[CHG] Device 20:16:06:23:98:85 LegacyPairing: no
[CHG] Device 20:16:06:23:98:85 Name: Bluetooth 3.0 Keyboard
[CHG] Device 20:16:06:23:98:85 Alias: Bluetooth 3.0 Keyboard
[CHG] Device 20:16:06:23:98:85 LegacyPairing: yes
[CHG] Device 34:36:3B:C7:FB:E9 RSSI: -65
[Akanuma_Hiroaki_iPhone]# scan off
[CHG] Device 20:16:06:23:98:85 RSSI is nil
[CHG] Device 34:36:3B:C7:FB:E9 RSSI is nil
Discovery stopped
[CHG] Controller B8:27:EB:19:76:07 Discovering: no

 対象のデバイスのMACアドレスを指定してペアリングします。

[Akanuma_Hiroaki_iPhone]# pair 20:16:06:23:98:85
Attempting to pair with 20:16:06:23:98:85
[CHG] Device 20:16:06:23:98:85 Connected: yes
[CHG] Device 20:16:06:23:98:85 Modalias: usb:v05ACp023Cd0102
[CHG] Device 20:16:06:23:98:85 UUIDs: 00001124-0000-1000-8000-00805f9b34fb
[CHG] Device 20:16:06:23:98:85 UUIDs: 00001200-0000-1000-8000-00805f9b34fb
[CHG] Device 20:16:06:23:98:85 ServicesResolved: yes
[CHG] Device 20:16:06:23:98:85 Paired: yes
Pairing successful
[CHG] Device 20:16:06:23:98:85 ServicesResolved: no
[CHG] Device 20:16:06:23:98:85 Connected: no
[Akanuma_Hiroaki_iPhone]# connect 20:16:06:23:98:85
Attempting to connect to 20:16:06:23:98:85
[CHG] Device 20:16:06:23:98:85 Connected: yes
Connection successful
[CHG] Device 20:16:06:23:98:85 ServicesResolved: yes
[DEL] Device 34:36:3B:C7:FB:E9 34-36-3B-C7-FB-E9
[DEL] Device 74:DE:1A:E6:4E:4F 74-DE-1A-E6-4E-4F
[CHG] Device 20:16:06:23:98:85 ServicesResolved: no
[CHG] Device 20:16:06:23:98:85 Connected: no

 そして接続してみます。

[bluetooth]# connect 20:16:06:23:98:85
Attempting to connect to 20:16:06:23:98:85
Failed to connect: org.bluez.Error.Failed
[bluetooth]# connect 20:16:06:23:98:85
Attempting to connect to 20:16:06:23:98:85
[CHG] Device 20:16:06:23:98:85 Connected: yes
Connection successful
[CHG] Device 20:16:06:23:98:85 ServicesResolved: yes

 無事接続できたようです。正しく通信できているか確認するために、evtest というインプットイベントのモニタツールを使ってみます。

evtest
~whot/evtest - Simple tool for input event debugging.

 インストールした後で下記のように実行します。evtestを起動した状態でキーボード入力してみたところ、下記のように検知され、正しく通信できていることが確認できました。

pi@raspberrypi:~/tmp/evtest $ evtest
No device specified, trying to scan all of /dev/input/event*
Not running as root, no devices may be available.
Available devices:
/dev/input/event0:      FC:E9:98:21:23:B7
/dev/input/event1:      Bluetooth 3.0 Keyboard
Select the device event number [0-1]: 1
Input driver version is 1.0.1
Input device ID: bus 0x5 vendor 0x5ac product 0x23c version 0x102
Input device name: "Bluetooth 3.0 Keyboard"
Supported events:
  Event type 0 (EV_SYN)
  Event type 1 (EV_KEY)
    Event code 1 (KEY_ESC)
    Event code 2 (KEY_1)
    Event code 3 (KEY_2)
    Event code 4 (KEY_3)
    Event code 5 (KEY_4)
    Event code 6 (KEY_5)
    Event code 7 (KEY_6)
    Event code 8 (KEY_7)
    Event code 9 (KEY_8)
    Event code 10 (KEY_9)
    Event code 11 (KEY_0)
    Event code 12 (KEY_MINUS)
    Event code 13 (KEY_EQUAL)
    Event code 14 (KEY_BACKSPACE)
    Event code 15 (KEY_TAB)
〜〜〜中略〜〜〜
    Event code 217 (KEY_SEARCH)
    Event code 240 (KEY_UNKNOWN)
    Event code 374 (KEY_KEYBOARD)
    Event code 581 (?)
  Event type 3 (EV_ABS)
    Event code 40 (ABS_MISC)
      Value      0
      Min        0
      Max      255
    Event code 41 (?)
      Value      0
      Min        0
      Max        1
    Event code 42 (?)
      Value      0
      Min        0
      Max        1
    Event code 43 (?)
      Value      0
      Min        0
      Max        1
  Event type 4 (EV_MSC)
    Event code 4 (MSC_SCAN)
  Event type 17 (EV_LED)
    Event code 0 (LED_NUML)
    Event code 1 (LED_CAPSL)
    Event code 2 (LED_SCROLLL)
    Event code 3 (LED_COMPOSE)
    Event code 4 (LED_KANA)
  Event type 20 (EV_REP)
Properties:
Testing ... (interrupt to exit)
Event: time 1495901204.274595, type 4 (EV_MSC), code 4 (MSC_SCAN), value 70004
Event: time 1495901204.274595, type 1 (EV_KEY), code 30 (KEY_A), value 1
Event: time 1495901204.274595, -------------- SYN_REPORT ------------
Event: time 1495901204.323288, type 4 (EV_MSC), code 4 (MSC_SCAN), value 70004
Event: time 1495901204.323288, type 1 (EV_KEY), code 30 (KEY_A), value 0
Event: time 1495901204.323288, -------------- SYN_REPORT ------------
Event: time 1495901206.455897, type 4 (EV_MSC), code 4 (MSC_SCAN), value 70005
Event: time 1495901206.455897, type 1 (EV_KEY), code 48 (KEY_B), value 1
Event: time 1495901206.455897, -------------- SYN_REPORT ------------
Event: time 1495901206.568359, type 4 (EV_MSC), code 4 (MSC_SCAN), value 70005
Event: time 1495901206.568359, type 1 (EV_KEY), code 48 (KEY_B), value 0
Event: time 1495901206.568359, -------------- SYN_REPORT ------------
Event: time 1495901208.728378, type 4 (EV_MSC), code 4 (MSC_SCAN), value 70006
Event: time 1495901208.728378, type 1 (EV_KEY), code 46 (KEY_C), value 1
Event: time 1495901208.728378, -------------- SYN_REPORT ------------
Event: time 1495901208.818369, type 4 (EV_MSC), code 4 (MSC_SCAN), value 70006
Event: time 1495901208.818369, type 1 (EV_KEY), code 46 (KEY_C), value 0
Event: time 1495901208.818369, -------------- SYN_REPORT ------------

RubyでBluetoothデバイスとペアリング&接続

 Bluetoothデバイスの検知、ペアリング、接続までをRubyでやってみます。RubyからBLEデバイスを操作するための gem として、ruby-bleというのがあるようなのでそちらを使ってみます。

github.com

Documentation for ble (0.1.0)

 Gemfileに下記エントリを追加して、bundle installしておきます。

gem 'ble'

   今回はとりあえずirbで試してみます。irbを起動して'ble'をrequireします。warningメッセージが気になるところですが、今回はスルー。

pi@raspberrypi:~ $ sudo bundle exec irb
irb(main):001:0> require 'ble'
/home/pi/vendor/bundle/ruby/2.4.0/gems/ruby-dbus-0.13.0/lib/dbus/marshall.rb:299: warning: constant ::Fixnum is deprecated
=> true

 BLEアダプタのリストを表示してみます。

irb(main):002:0> BLE::Adapter.list
/home/pi/vendor/bundle/ruby/2.4.0/gems/ruby-dbus-0.13.0/lib/dbus/marshall.rb:299: warning: constant ::Fixnum is deprecated
=> ["hci0"]

 hci0というアダプタが見つかるので、インスタンスを作成します。

irb(main):003:0> adapter = BLE::Adapter.new('hci0')

 Bluetoothデバイスの検知を開始します。目的のBLEデバイスのMACアドレスが検知されたら停止します。

irb(main):008:0* adapter.start_discovery
=> true
irb(main):011:0> adapter.devices
=> ["20:16:06:23:98:85", "34:36:3B:C7:FB:E9", "43:19:24:21:00:5F", "74:DE:1A:E6:4E:4F", "FC:E9:98:21:23:B7"]
irb(main):012:0> adapter.stop_discovery
=> true

 該当のBluetoothデバイスのオブジェクトを取得します。

irb(main):013:0> kb = adapter['20:16:06:23:98:85']

 デバイス名等を確認してみます。

irb(main):015:0* kb.name
=> "Bluetooth 3.0 Keyboard"
irb(main):016:0> kb.alias
=> "Bluetooth 3.0 Keyboard"
irb(main):017:0> kb.address
=> "20:16:06:23:98:85"

 続けてペアリングします。

irb(main):020:0> kb.pair
=> true
irb(main):021:0> kb.is_paired?
=> true

 そして接続してみます。

irb(main):026:0> kb.connect
=> true
irb(main):027:0> kb.is_connected?
=> true

 無事接続できたようです。この状態で evtest を実行すると、 bluetoothctl から接続した時と同じように、キーボードの入力内容が確認できます。

 ここまでだとまだBluetoothクラシックのデバイスと接続したところまでなので、次回以降でBLEデバイスとの通信を試してみたいと思います。

超音波センサー + SORACOM Beam でSlack通知

 IoTエンジニア養成読本のハンズオンのラスト、距離を測定するための超音波センサーの測定結果を元にした情報をSORACOM Beamを経由してSlackに通知する処理を実装してみました。

gihyo.jp

超音波センサーの接続

 まずは超音波センサー(HC-SR04)を下記の図のようにRaspberry Piに接続します。動作状況の確認用にLEDも一緒に接続しています。センサーの向きを間違えるとショートしてセンサーやRaspberry Piが壊れる可能性があるということなので注意して配線します。

f:id:akanuma-hiroaki:20170504232728p:plain:w300:left

超音波センサー
赤:2番ピン(+5V) - VCC
黒:20番ピン(GND) - GND
黄:13番ピン(GPIO27) - ECHO
青:11番ピン(GPIO17) - TRIG

LED
黒:6番ピン(GND)
黄:12番ピン(GPIO18)


f:id:akanuma-hiroaki:20170526062708j:plain:w500

距離の測定処理

 まずは距離の測定処理を実装します。超音波センサーでは送信した超音波が物体に当たって跳ね返り、戻ってくるまでにかかる時間から距離を測定しています。超音波センサーの端子としては、送信用がTRIG、受信用がECHOになります。今回はそれぞれGPIO17と27を使用しています。

require 'bundler/setup'
require 'pi_piper'

TRIG_GPIO = 17
ECHO_GPIO = 27

def read_distance(trig_pin_no, echo_pin_no)
  # 送信用(TRIG)、受信用(ECHO)のピンの設定
  trig_pin = PiPiper::Pin.new(pin: trig_pin_no, direction: :out)
  echo_pin = PiPiper::Pin.new(pin: echo_pin_no, direction: :in, trigger: :both)
  trig_pin.off
  sleep(0.3)

  # TRIGに短いパルスを送る
  trig_pin.on
  sleep(0.00001)
  trig_pin.off

  # ECHOがONになる(待ち受け状態になる)まで待ち、時間を記録
  echo_pin.wait_for_change
  signal_off = Time.now

  # ECHOがOFFになる(音波を受信して停止する)まで待ち、時間を記録
  echo_pin.wait_for_change
  signal_on = Time.now

  # 送出時刻と受信時刻の差分を求め、距離を計算する
  time_passed = signal_on - signal_off
  distance = time_passed * 17_000

  # ピンを解放
  PiPiper::Platform.driver.unexport_pin(trig_pin_no)
  PiPiper::Platform.driver.unexport_pin(echo_pin_no)

  return distance if distance <= 500
end

if $0 == __FILE__
  loop do
    start_time = Time.now
    distance = read_distance(TRIG_GPIO, ECHO_GPIO)
    unless distance.nil?
      puts "Distance: %.1f cm" % distance
    end

    wait = start_time + 3 - Time.now
    sleep(wait) if wait > 0
  end
end

 今回使用している距離センサーの使い方は、TRIGに10μs電圧をかけるとパルスが8回送出され、同時にECHOがHIGHになり、音波を受信するとLOWになるとのことでした。

www.switch-science.com

 元のPythonコードではループでECHOのHIGH/LOWの状態を検知して処理していますが、同僚からエッジ検出という手があると聞いて調べたところ、ピンの状態の変化を検知する wait_for_change というメソッドがあったのでこれを使っています。

PiPiper::Pin#wait_for_change
Method: PiPiper::Pin#wait_for_change — Documentation for jwhitehorn/pi_piper (master)

 このメソッドは指定した方向のピンの状態変化を検知するまでそこで待ち、検知するとそれ以降の処理に進みます。wait_for_change のソースは下記のようになっていて、 pin_wait_for というメソッドを呼び出しています。

def wait_for_change
  Platform.driver.pin_wait_for(@pin, @trigger)
end

 pin_wait_for メソッドの実装は下記のようになっていて、ループでピンの状態を検知して、ピンのオブジェクト作成時に trigger オプションに指定した内容次第で、ONになった時、OFFになった時、もしくは両方の場合で変更を検知します。今回は trigger に :both を指定していますので、両方の場合で変更を検知してループを抜けます。

def self.pin_wait_for(pin, trigger)
  pin_set_edge(pin, trigger)

  fd = File.open("/sys/class/gpio/gpio#{pin}/value", "r")
  value = nil
  loop do
    fd.read
    IO.select(nil, nil, [fd], nil)
    last_value = value
    value = self.pin_read(pin)
    if last_value != value
      next if trigger == :rising and value == 0
      next if trigger == :falling and value == 1
      break
    end
  end
end

 自前でループ処理を書かなくても良いのでコードがスッキリするのですが、タイムアウトは設定できません。今回使用している距離センサーはケースによっては状態の変化が起こらなくなってしまうケースもあるようなので、それを考慮すると一定時間でタイムアウトさせて再度測定を開始する必要があります。その場合はやはり自前でループ処理を書いてタイムアウトを設定した方が良いかもしれません。今回はひとまずこのまま pin_for_wait を使った実装で進めてみます。

 これを実行すると下記のように測定結果が出力されていきます。

pi@raspberrypi:~ $ sudo bundle exec ruby distance.rb                                                                                                                                                                                          
Distance: 9.3 cm
Distance: 9.2 cm
Distance: 9.1 cm
Distance: 9.0 cm
Distance: 9.0 cm
Distance: 9.3 cm
Distance: 9.4 cm
Distance: 8.9 cm
Distance: 9.0 cm
Distance: 9.3 cm
Distance: 9.5 cm
Distance: 9.1 cm
Distance: 8.6 cm
Distance: 9.4 cm
Distance: 10.1 cm
Distance: 10.1 cm
Distance: 9.4 cm
Distance: 8.8 cm
Distance: 8.3 cm

 ちなみに書籍で紹介されていたPythonのコードで測定すると、上記と同じ条件で実行しても下記のように測定距離やばらつき度合いが異なります。

pi@raspberrypi:~ $ python distance.py 
Distance: 67.7 cm
Distance: 10.1 cm
Distance: 10.0 cm
Distance: 10.1 cm
Distance: 10.0 cm
Distance: 10.0 cm
Distance: 10.1 cm
Distance: 10.0 cm
Distance: 10.0 cm
Distance: 10.0 cm
Distance: 10.0 cm
Distance: 10.0 cm
Distance: 10.7 cm
Distance: 10.0 cm
Distance: 10.0 cm
Distance: 10.1 cm
Distance: 10.0 cm
Distance: 10.1 cm

 実際の距離は10cm程なので、Pythonの方がばらつきが少なく、実際の距離との誤差も少ないようです。この辺りはECHOでの待ち受け処理やピンの状態検知処理の部分に差があるように思いますが、まだ詳細は調べられてないので、今後時間があるときに調べてみたいと思います。今回はそんなに厳密な測定精度は必要ないので、このままRubyの実装を使用します。

SORACOM Beam + Slackの使用設定

 今回は状態判定結果をSlackへ通知するために、SORACOM Beamを使用します。もちろんRaspberry Piから直接SlackのAPIにリクエストを送ることもできますが、その場合、セキュリティートークンを含むURLをコードの中に書く必要がありますし、IoTデバイスを設置した後で送信先を変更したくなった場合などは設置場所まで行くか、デバイスを回収する等の対応が必要になってしまいます。SORACOM Beamを利用すると、デバイスからはBeamのAPIにリクエストを投げておけば、Beamからアクセスする先のURLはWebコンソールから後からでも変更できますし、その際にセキュリティートークンなどの情報を追加することができます。また、デバイスからBeamへはAir SIMから3G/LTE回線での接続になるので安全に接続できますし、そこから外部のAPIへリクエストを投げる時にTLSによる暗号化を行うことで、デバイス側で暗号化の処理を行う必要もなくなります。

 SORACOM Beamを使うには、ユーザーコンソールからあらかじめ設定しておく必要があります。SIMグループのメニューから該当するグループの「SORACOM Beam設定」をクリックして開き、「+」メニューから「HTTPエントリポイント」を選び、開いたフォームにSlackのIncoming webhook APIのURL等を入力します。ここではSlack側でのIncoming webhook APIの設定方法は割愛します。

f:id:akanuma-hiroaki:20170504232825p:plain

測定結果からの状態判定 + SORACOM Beamへの送信処理実装

 ここまでで実装した距離の測定処理を使って、測定結果から状態を判定し、今までと状態が変わった場合はSORACOM Beamへリクエストを送信する処理を実装します。また、検知状態がわかるようにLEDのON/OFFも行います。

 弊社の会議室はドアを閉めてしまうと外から中の様子は見えないので、使用中かどうかを示すためにドアに「使用中 / 空室」のプレートを貼ってあるのですが、入退室時に変更を忘れてそのままになってしまって結局使っているのかどうか開けてみないとわからないという状態がよく発生します。そこで今回は距離センサーを使って、会議室のテーブルに誰かいる = 会議室使用中ということで、その状態変化をSlackに通知して、会議室が使われているのかどうかわかるようにしてみたいと思います。

require 'bundler/setup'
require 'httpclient'
require 'logger'
require 'pi_piper'
require './distance.rb'

BEAM_URL = 'http://beam.soracom.io:8888/room'

TRIG_GPIO = 17
ECHO_GPIO = 27
LED_GPIO  = 18
INTERVAL  = 5.0 # sec

logger = Logger.new('room_notify.log')

led_pin  = PiPiper::Pin.new(pin: LED_GPIO,  direction: :out)

occupied = false
state_changed_at = Time.now

threshold = 50 # cm
unless ARGV.empty?
  threshold = ARGV.first.to_f
end

http_client = HTTPClient.new
loop do
  start_time = Time.now
  distance = read_distance(TRIG_GPIO, ECHO_GPIO)

  current_status = distance < threshold
  if occupied != current_status
    duration = Time.now - state_changed_at
    state_changed_at = Time.now
    message = "Distance: %.1f cm - Status changed to %s. (Duration: %d sec)" % [distance, current_status ? 'OCCUPIED' : 'EMPTY', duration]
    payload = '{"text":"%s"}' % message
    res = http_client.post(BEAM_URL, payload, 'Content-Type' => 'application/json')
    logger.info("PAYLOAD: #{payload} / BEAM Response: #{res.status}")
    occupied = current_status
  end

  occupied ? led_pin.on : led_pin.off

  if Time.now < start_time + INTERVAL
    sleep(start_time + INTERVAL - Time.now)
    next
  end
end

 5秒毎にセンサーから距離を読み取り、閾値(デフォルト50cm)を下回った場合は会議室使用中と判定しています。そして前回のステータスと異なっていた場合はSORACOM Beamへリクエストを送信します。また、使用中の場合はLEDを点灯させています。

 これを実行するとセンサーの測定距離に応じてステータスが判定され、ステータスが変更になった場合はSORACOM Beamへリクエストが送信されます。

pi@raspberrypi:~ $ sudo bundle exec ruby room_notify.rb &                                                                                                                                                                                     
[2] 9426

 ログファイルにはPAYLOADとBEAMからのレスポンスを出力しています。

pi@raspberrypi:~ $ tail room_notify.log                                                                                                                                                                                                       
I, [2017-05-24T23:09:50.528556 #5596]  INFO -- : PAYLOAD: {"text":"Distance: 52.7 cm - Status changed to EMPTY. (Duration: 25 sec)"} / BEAM Response: 200
I, [2017-05-24T23:10:05.498609 #5596]  INFO -- : PAYLOAD: {"text":"Distance: 10.1 cm - Status changed to OCCUPIED. (Duration: 14 sec)"} / BEAM Response: 200
I, [2017-05-24T23:18:39.312326 #7964]  INFO -- : PAYLOAD: {"text":"Distance: 9.9 cm - Status changed to OCCUPIED. (Duration: 0 sec)"} / BEAM Response: 200
I, [2017-05-24T23:18:49.242184 #7964]  INFO -- : PAYLOAD: {"text":"Distance: 98.0 cm - Status changed to EMPTY. (Duration: 10 sec)"} / BEAM Response: 200
I, [2017-05-24T23:18:54.314298 #7964]  INFO -- : PAYLOAD: {"text":"Distance: 14.3 cm - Status changed to OCCUPIED. (Duration: 4 sec)"} / BEAM Response: 200
I, [2017-05-24T23:23:33.491639 #9430]  INFO -- : PAYLOAD: {"text":"Distance: 11.3 cm - Status changed to OCCUPIED. (Duration: 0 sec)"} / BEAM Response: 200
I, [2017-05-24T23:23:48.761726 #9430]  INFO -- : PAYLOAD: {"text":"Distance: 54.2 cm - Status changed to EMPTY. (Duration: 15 sec)"} / BEAM Response: 200
I, [2017-05-24T23:23:53.451585 #9430]  INFO -- : PAYLOAD: {"text":"Distance: 46.9 cm - Status changed to OCCUPIED. (Duration: 5 sec)"} / BEAM Response: 200
I, [2017-05-24T23:23:58.791505 #9430]  INFO -- : PAYLOAD: {"text":"Distance: 51.0 cm - Status changed to EMPTY. (Duration: 5 sec)"} / BEAM Response: 200
I, [2017-05-24T23:24:18.531670 #9430]  INFO -- : PAYLOAD: {"text":"Distance: 10.2 cm - Status changed to OCCUPIED. (Duration: 19 sec)"} / BEAM Response: 200

 Slackにも下記のように通知されます。

f:id:akanuma-hiroaki:20170504232956p:plain:w500

 IoTデバイスはWebサービスと違って、設置してしまうとコードを変更したりするのは大変なので、SORACOM Beamのようなサービスで設置後も柔軟に設定を変更できるようになり、同時にセキュリティ面の信頼性も向上できるというのはとても便利だと感じました。

 今回実装したコードは下記リポジトリにも公開してあります。

github.com