Bleでセンサーデータのやり取りをするのに結構てこずったので備忘録としてまとめておきます。
ペリフェラル ⇒ micro:bit(オンボードのセンサで基板温度測定)、M5stack(ENV3にて温度、湿度、気圧測定)
セントラル ⇒ Raspberry Pi(ゲートウェイ)
グラフ化 ⇒ Raspberru Piからambientへ
2つのデバイスでセンサーを使って測定したデータをBleでRaspberry Piに送信。
Raspberry Piは受信データをWEBサービスのambientへ転送、グラフ化、の流れとなります。
なお、プログラム言語はpythonとmicropythonで作成しました。
Cならググれば山ほどBleに関する情報がヒットしますが、pythonのBleに関しては情報量が極端に少なく、苦労しました。
もし、私と同じようにpythonでBle通信をやろうとしている方がいれば、少しでも参考になれば幸いです。
なお、アドバタイジングでのデータ送信(ブロードキャスト)は出来ませんでしたので、コネクト後にUUIDを使ってデータのやり取りをする方法になります。
この方法でも、一応複数のデバイスからデータをゲートウェイに転送する事が可能です。
目次
はじめに
デバイスの構成
・micro:bit(ペリフェラル)
・M5Stack Core2(ペリフェラル)ENV3センサー付き
・raspberry Pi4(セントラル)
Ble通信について
クラシックのBlutoothの場合、デバイス同士でペアリング後、接続するのに仮想COMポートやチャンネルの設定が必要でした。
Bleは厳密に言えばペアリング無しで、スキャンした際にアドバタイズしているデバイスからMACアドレスを知る事が出来れば、任意のアドレスの指定だけで接続が完了します。
その後、基本は「UUID」の仕組みを利用した方法でデータのやり取りを行います。
スキャンについては、後述するペリフェラルのプログラム起動後、Raspberry piで下記コマンドを叩けば出来ます。
1 2 |
$bluetoothctl $scan on |
ブロードキャスト(アドバタイジング時にデータも乗っける)の場合は、接続する必要すらありません。UUIDは不要。ビーコンで使われていますね。
ただし、Bleは省電力や手軽な通信が可能な反面、クラシックに比べると通信が暗号化されないようですので、利用する場面は良く考えたほうがよさそうです。
UUID
UUIDは大きく分けて2つ、サービスとキャラクタリスティックに分かれます。
デバイスによって、あらかじめ持っているUUIDの多さや種類が異なります。
サービスは、フォルダに例えられます。キャラクタリスティックはファイルに該当します。
つまり、サービスは大きなくくりで「センサー」とすれば、キャラクタリスティックには「温度」や「加速度」などが該当します。したがって、温度情報を得たい場合は温度のキャラクタリスティックを指定するといった感じです。
となれば、サービスのUUIDっていらなくない?となりますが、正直、いらないと思います。。いまいち必要性がわかんない。。なにか必要な理由があるんでしょうけどね。
あと、UUIDは一意に決められたIDを各デバイスが持っているようですが、ここもちょっと分からない。。(なんか他のデバイスでも同じUUIDを見た)MACアドレスは重複する事は無いので、MAC+UUIDで必要なデータをやり取りしてる印象なんですけどね。。
ハンドル
後述しますが、定義されたUUIDには「ハンドル」という識別番号が割り振られています。
このハンドルの情報さえ入手すれば、MACアドレス+ハンドルの指定だけで紐づいたサービスのデータを入手できます。
前置きが長くなりましたが、本題に入って行こうかと思います。
Bleでセンサーの値を取得する流れ
ペリフェラルでアドバタイズ、セントラルでスキャンしアドレスとサービスを確認
今回、ペリフェラル(子機)にmicro:bit及びM5stackを使用していますが、まずこのデバイスで「自分はここにいますよ」と親機に伝えるためにアドバタイズと呼ばれるbluetoothのブロードキャスト通信を行わせます。
セントラル(親機)にはRaspberry Piを使用していますが、続いてこちらでBluetoothのスキャンをかけます。
ブロードキャストのデータにはMacアドレスやデバイス名がデフォルトで含まれているため、スキャン時に目当てのデバイスのMACアドレスを確認する事が出来ます。
また、UUIDを確認するプログラムもありますので、接続前に利用できるUUID(キャラクタリスティック、ハンドル)も控えておきます。
セントラルからペリフェラルに接続
UUIDとMACアドレスが分かれば、接続させます。
接続後は、ハンドルを使って指定したサービスの情報をペリフェラルから読み込む事が可能になります。
デバイスを複数台接続させたい場合は、面倒ですがAを接続、Aを切断、Bを接続、Bを切断、、、という流れになります。(もっといい方法はあるだろうけど知らん。)これがブロードキャスト可能なら楽になるんですけどね、、ほんとやりかた分からない。。
あとは読み込んだデータをambientに送るだけです。
ambientに関する事はググれば腐るほどあるので割愛します。
プログラム(ペリフェラル側)
micro:bit
温度計サービスを先頭にいれる事で、温度センサのUUIDが定義されます。
見た目訳わかりませんが、これで勝手にアドバタイズしてくれます。逆に使いづらいですよねブロッキー(-_-;)
M5Stack(micropython)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
from m5stack import * from m5stack_ui import * from uiflow import * import unit import time import ubluetooth screen = M5Screen() screen.clean_screen() screen.set_screen_bg_color(0xFFFFFF) env22 = unit.get(unit.ENV3, unit.PORTA) ble = ubluetooth.BLE() ble.active(True) NUS_UUID = '6E400001-B5A3-F393-E0A9-E50E24DCCA9E' RX_UUID = '6E400002-B5A3-F393-E0A9-E50E24DCCA9E' TX_UUID = '6E400003-B5A3-F393-E0A9-E50E24DCCA9E' rh_UUID = "3ef44852-cd43-4da4-8d45-4f9b87fb96c2" hpa_UUID = "eb901554-f7ed-4412-bc26-5937e6fb4418" BLE_NUS = ubluetooth.UUID(NUS_UUID) BLE_RX = (ubluetooth.UUID(RX_UUID), ubluetooth.FLAG_WRITE) BLE_TX = (ubluetooth.UUID(TX_UUID), ubluetooth.FLAG_READ | ubluetooth.FLAG_NOTIFY) BLE_rh = (ubluetooth.UUID(rh_UUID), ubluetooth.FLAG_READ | ubluetooth.FLAG_NOTIFY) BLE_hpa = (ubluetooth.UUID(hpa_UUID), ubluetooth.FLAG_READ | ubluetooth.FLAG_NOTIFY) BLE_UART = (BLE_NUS, (BLE_TX, BLE_RX, BLE_rh, BLE_hpa)) SERVICES = (BLE_UART, ) ble.gatts_register_services(SERVICES) label0 = M5Label('Text', x=48, y=35, color=0x000, font=FONT_MONT_30, parent=None) label1 = M5Label('Text', x=48, y=101, color=0x000, font=FONT_MONT_30, parent=None) label2 = M5Label('Text', x=48, y=167, color=0x000, font=FONT_MONT_30, parent=None) while(True): ondo = round(env22.temperature) ondo = str(ondo) label0.set_text("temp:" + ondo + "゜C") rh = round(env22.humidity) rh = str(rh) label1.set_text("rh:" + rh + "%") hpa = round(env22.pressure) hpa = str(hpa) label2.set_text("atm:" + hpa + "hpa") ble.gap_advertise(100, "asdf") ble.gatts_write(0x0015, ondo) ble.gatts_write(0x001a, rh) ble.gatts_write(0x001d, hpa) time.sleep(1) |
bleやるためにuiflowでble使えるcore2買ったのに、結局uiflowじゃうまく行かなかったのでubluetoothライブラリを使っています。使い方は公式ページで調べました。
さて、特筆するべき所は前半のUUIDの羅列の所です。
先ず、変数にUUIDを格納していますが、これでUUIDが定義されます。定義されるというのは、セントラルからスキャンしたときに「見える」ってことです。つまり使えるUUID。ただし、デフォルトで準備されているUUIDではないので、このままでは使えません。勝手に作成したUUIDですので。(前半3つのUUIDはble uartらしく、ネットの情報そのままのIDを流用しました。皆使ってるみたいでこの辺りが一意になっていないと思うところなんですけどね。。後半の2つは、WEBで自動で作成してくれるUUIDを使用しました。)
というか結論からいうと、UUIDは「既存のIDとかぶらなければなんでもいい」です。多分ね。。
WEBでランダムにUUIDを生成してくれる所があるので、そちらでいくつかUUIDを入手し、利用したいサービスの分UUIDを定義してあげればいいのです。あとはそれにサービスを紐づけるだけ。
続いてUUIDのフラグの記述ですが、これが「書き込み」「読み込み」等のパーミッション的な記述になります。これも重要です。
read、writeは、セントラルから見て出来る内容そのままです。notifyが、これがまた重要で、 ペリフェラルから 定義済みのUUIDにデータを書き込めるような働きをしてくれます。
つまり、個人で作成したUUIDには値が結びついていないので、ペリフェラルから値を入れてやる必要性があり、 notify が必須になるのです。多分ね。
ここまで出来れば楽勝です、ループ内の「ble.gatts_write」で引数に定義したUUIDのハンドルを値を指定してやれば、UUIDに値が結びつくのです!!!つまり、セントラルからreadすれば、値がちゃんと渡されます。
これ、ほんとやり方見つけるのしんどかったです(-_-;)
センサとかの説明は割愛します。
プログラム(セントラル側)
Raspberry Pi(python)UUIDスキャンプログラム
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import sys import bluepy def main(): try: peri = bluepy.btle.Peripheral() peri.connect("MACアドレス", bluepy.btle.ADDR_TYPE_RANDOM) # addrTypeがpublic なら、ADDR_TYPE_PUBLICを指定 except: print("device connect error") sys.exit() charas = peri.getCharacteristics() for chara in charas: print("======================================================") print(" UUID : %s" % chara.uuid ) print(" Handle %04x: %s" % (chara.getHandle(), chara.propertiesToString())) peri.disconnect() if __name__ == "__main__": if len(sys.argv) == 1: print('Usage: getHandle.py BLE_DEVICE_ADDRESS') sys.exit() devadr = sys.argv[1] main() |
ペリフェラルのMACアドレスが分かれば、このプログラムの実行でデバイスの持っているUUIDの情報を入手します。
ハンドルの情報も入手できます。
※プログラム名 MACアドレス で実行してください。
Raspberry Pi(python) センサーデータ受信&ambient送信
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
import ambient import time from bluepy import btle import bluepy am = ambient.Ambient(44444, "999999999999") am2 = ambient.Ambient(22222, "555555555555") HANDLE_LED = 0x0000 HANDLE_M5 = 0x0000 HANDLE_M52 = 0x0000 HANDLE_M53 = 0x0000 devadr = "Macアドレス1" devadr2 = "Macアドレス2" peri = bluepy.btle.Peripheral() while 1: # 引数(timeout=5.0)は、スキャン秒数 scanner = btle.Scanner(0) devices = scanner.scan(10.0) for device in devices: if device.addr == "Macアドレス1": # device.addrType == "random" try: peri.connect(devadr, bluepy.btle.ADDR_TYPE_RANDOM) print("connnect") print("micro:bit") ondo = peri.readCharacteristic(HANDLE_LED) #print(ondo) ondo = ondo.hex() #print(ondo) ondo = int(ondo, 16) print(ondo) peri.disconnect() print("disconnect") r = am.send({'d1': ondo}) #[, timeout = timeout]) except: print("connect error") pass time.sleep(1) if device.addr == "Macアドレス2": # device.addrType == "random": try: peri.connect(devadr2, bluepy.btle.ADDR_TYPE_PUBLIC) print("connnect") print("m5stack") ondo = peri.readCharacteristic(HANDLE_M5) rh = peri.readCharacteristic(HANDLE_M52) #time.sleep(5) hpa = peri.readCharacteristic(HANDLE_M53) ondo = ondo.decode() rh = rh.decode() hpa = hpa.decode() print("temp: " + ondo) print("rh: " + rh) print("hpa: " + hpa) peri.disconnect() print("disconnect") r = am2.send({'d1': ondo, "d2": rh, "d3": hpa}) except: print("connect error") pass time.sleep(1) print("....") |
ループ処理でスキャンをかけて周囲のデバイスを精査し、事前に調べておいた2つのデバイスのMACアドレスに該当した場合だけコネクト処理、UUIDから値取得、デコネクト処理、ambient送信、を行っています。
今回は2台のデバイスを登録していますが、いい感じで交互に接続処理を行ってくれます。
また、接続失敗が起こる場合があるので、エラー停止の防止の観点で例外処理を設けています。
完成
以上です。