chakokuのブログ(rev4)

テック・コミック・ごくまれにチャリ

Raspberry Pi Zero WH + BLE + bluepyで BLEを学ぶ

BLEスキャナが大量のパケットを捕まえるのでどれが目指すパケットなのかよく分からず。。だったら、もっと確実に動作するBLE付きラズパイでスキャンしてみようと思った。安いラズパイは、Pi Zero-Wなんだが、在庫がなく、ピンヘッダ付きのPi Zero-WHを買った。
f:id:chakoku:20200805235020p:plain:w300
Linux+Pythonで動くBluetoothライブラリはいろいろあるようなのだが、手始めに、bluepyでBLEデバイスをスキャンしてみた。
(コードはアイ・プライド社様の解説記事を参考にした。分かりやすい!)

解説記事を参考に以下のコードを実行(Pi Zeroが搭載しているBLEを使ってスキャン)

from bluepy import btle

sc = btle.Scanner(0)
dev = sc.scan(3.0)
for d in dev:
   print("---------------------")
   print("mac:",end='')
   print(d.addr)
   print(d.getScanData())

実行結果は以下(関係ないBLEデバイスは除いています)
KorgのmicroKEY2-25 Airが見つけられている。

---------------------
mac:b8:d7:af:33:74:XX
[(1, 'Flags', '06'), (7, 'Complete 128b Services', '03b80e5a-ede8-4b33-a751-6ce34ec4c700'), (9, 'Complete Local Name', 'microKEY2-25 Air')]

インスタンスが保持している変数の値は以下

[('ADVERTISING_INTERVAL', 26),
 ('APPEARANCE', 25),
 ('COMPLETE_128B_SERVICES', 7),
 ('COMPLETE_16B_SERVICES', 3),
 ('COMPLETE_32B_SERVICES', 5),
 ('COMPLETE_LOCAL_NAME', 9),
 ('FLAGS', 1),
 ('INCOMPLETE_128B_SERVICES', 6),
 ('INCOMPLETE_16B_SERVICES', 2),
 ('INCOMPLETE_32B_SERVICES', 4),
 ('MANUFACTURER', 255),
 ('PUBLIC_TARGET_ADDRESS', 23),
 ('RANDOM_TARGET_ADDRESS', 24),
 ('SERVICE_DATA_128B', 33),
 ('SERVICE_DATA_16B', 22),
 ('SERVICE_DATA_32B', 32),
 ('SERVICE_SOLICITATION_128B', 21),
 ('SERVICE_SOLICITATION_16B', 20),
 ('SERVICE_SOLICITATION_32B', 31),
 ('SHORT_LOCAL_NAME', 8),
 ('TX_POWER', 10),
 ('__class__', <class 'bluepy.btle.ScanEntry'>),
 ('__dict__', {'addr': 'b8:d7:af:33:74:XX', 'iface': 0, 'addrType': 'public', 'rssi': -76, 'connectable': True, 'rawData': b'\x02\x01\x06\x11\x07\x00\xc7\xc4N\xe3lQ\xa73K\xe8\xedZ\x0e\xb8\x03\x11\tmicroKEY2-25 Air', 'scanData': {1: b'\x06', 7: b'\x00\xc7\xc4N\xe3lQ\xa73K\xe8\xedZ\x0e\xb8\x03', 9: b'microKEY2-25 Air'}, 'updateCount': 1}),
 ('__doc__', None),
 ('__module__', 'bluepy.btle'),
 ('__weakref__', None),
 ('addr', 'b8:d7:af:33:74:80'),
 ('addrType', 'public'),
 ('addrTypes', {1: 'public', 2: 'random'}),
 ('connectable', True),
 ('dataTags', {1: 'Flags', 2: 'Incomplete 16b Services', 3: 'Complete 16b Services', 4: 'Incomplete 32b Services', 5: 'Complete 32b Services', 6: 'Incomplete 128b Services', 7: 'Complete 128b Services', 8: 'Short Local Name', 9: 'Complete Local Name', 10: 'Tx Power', 20: '16b Service Solicitation', 31: '32b Service Solicitation', 21: '128b Service Solicitation', 22: '16b Service Data', 32: '32b Service Data', 33: '128b Service Data', 23: 'Public Target Address', 24: 'Random Target Address', 25: 'Appearance', 26: 'Advertising Interval', 255: 'Manufacturer'}),
 ('iface', 0),
 ('rawData', b'\x02\x01\x06\x11\x07\x00\xc7\xc4N\xe3lQ\xa73K\xe8\xedZ\x0e\xb8\x03\x11\tmicroKEY2-25 Air'),
 ('rssi', -76),
 ('scanData', {1: b'\x06', 7: b'\x00\xc7\xc4N\xe3lQ\xa73K\xe8\xedZ\x0e\xb8\x03', 9: b'microKEY2-25 Air'}),
 ('updateCount', 1)]

MACアドレスが分かったので、BLEスニファでも確認。フラグとUUIDがパケットに入っているは分かったけど、microKEY2-25 Airをどうやって取得しているのか不明
f:id:chakoku:20200806200614p:plain

次は、上記デバイスをコネクトして、アトリビュートを取得してみる

peripheral = btle.Peripheral()
peripheral.connect('b8:d7:af:33:74:XX')
for service in peripheral.getServices():
    print('UUID:{:s}'.format(str(service.uuid)))

得られたServices

UUID:00001800-0000-1000-8000-00805f9b34fb
UUID:00001801-0000-1000-8000-00805f9b34fb
UUID:0000180a-0000-1000-8000-00805f9b34fb
UUID:d0611e78-bbb4-4591-a5f8-487910ae4366
UUID:03b80e5a-ede8-4b33-a751-6ce34ec4c700   BLE MIDI

目的のcharacteristecにはたどり着いたが。。

>>> str(s[4].getCharacteristics()[0].uuid)
'7772e5db-3868-4112-a1a9-f2669d106bf3'

単純にReadを実行するとエラーになる

>>> s[4].getCharacteristics()[0].read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.7/dist-packages/bluepy/btle.py", line 197, in read
    return self.peripheral.readCharacteristic(self.valHandle)
  File "/usr/local/lib/python3.7/dist-packages/bluepy/btle.py", line 529, in readCharacteristic
    self._writeCmd("rd %X\n" % handle)
  File "/usr/local/lib/python3.7/dist-packages/bluepy/btle.py", line 302, in _writeCmd
    raise BTLEInternalError("Helper not started (did you call connect()?)")
bluepy.btle.BTLEInternalError: Helper not started (did you call connect()?)

操作している途中で、スマフォとペアリングしたようで、通信エラーとなった。スマフォのBLEをOFFしたけど、一度接続すると、KORG Keyboardはadvertiseしないのか、電源Off/OnしてもKeyboard側のLEDが点滅しない。アプリを無視して、Raspberry PIのBLEとKorg Keyboardがconnectionを確立ししているのか?? キャプチャしてみると、KeyboardをOnした直後はadvertiseしてるが、通信先のadvertiseを見つけると?KeyBoardのadvertiseは止めてしまう。なぜこうなるのか分からず。強制的にdisconnectできないかを調べるが、、そんな手段はなさそうだ。

# hcitool -i hci0 lescan

■ご参考URL
bluepyで始めるBluetooth Low Energy(BLE)プログラミング | 株式会社アイ・プライド


■追記 前回、MIDIデータを読み込めなかった問題を解消

bluepyでKORG MicroKEY Airと接続してMIDIデータを取り込もうとしてうまくいかなかった。この原因として、以下2点が原因であった。
(1)pairingを行っていなかった
(2)CCCDのフラグを設定していなかった。
当初、CCCDだけ設定しようとして書き込めなかったのだが、これはpairingをしていなかったため。ESP32のBLEライブラリにはpairngメソッドがないのだが、、GATCのwriteでどうにかparingやってくれということなんだろうか。pairingにどのようなパケットが流れているのか、ちと分からず

from bluepy import btle

sc = btle.Scanner(0)
dev = sc.scan(3.0)
	
class MyDelegate(btle.DefaultDelegate):
    def __init__(self, params):
        btle.DefaultDelegate.__init__(self)

    def handleNotification(self, cHandle, data):
        print(data)

peripheral = btle.Peripheral()
peripheral.connect('b8:d7:af:33:74:80')
peripheral.pair()
peripheral.withDelegate(MyDelegate(btle.DefaultDelegate))

# dsはCCCDのハンドル

ds.read()
ds.write(bytes((0x01,0x00)))
ds.read()

while True:
  if peripheral.waitForNotifications(10):
      continue
  print("zzz")

参考になった記事。感謝
Raspberry pi でセンサメダルのデータを受信する - Qiita

■追記
以下の記事より、やっぱりpairingって鍵交換しているのではないか。だったら、MicroPythonで実装されているBLEライブラリではKORG microKEY AirからMIDIデータを取ってくるのは無理と思う(BLEライブラリでは現在のところ、鍵交換機構は実装されていないとのことなので)。ESP32上でBLEを走らせて、pairing/bondingしたいなら、MicroPython版BLEではなく、ESP-IDF版のBLEを使うしかなさそうだ。。
BLE Series Part 2: A Closer Look at BLE Pairing | by Rida IDIL | Rtone IoT Security | Medium