PythonでSerial Bluetooth Terminalの動作を再現してみる

ラズベリーパイPico WでBluetoothを使ってみるでラズベリーパイPico Wでブルートゥースの子機(ペリフェラル)を構築し、



AndroidアプリのSerial Bluetooth Terminalでブルートゥースの動作を確認してみました。

今回はラズベリーパイ等のパソコンでPythonで似たような動作をするコードを作成してみます。

※2024年1月31日時点でクロームブックのクロスティーニではブルートゥースの機能は利用できません


コードの話題に入る前にブルートゥースでのデータ転送の主軸となるGATT(Generic attribute profile)について触れておきます。

親機と子機がブルートゥースで接続された時、お互いの機器はGATTプロファイルに基づくプロファイルで情報のやり取りを行います。


GATTの構造は

のようにProfie、Service(サービス)とCharacteristic(キャラクタリスティック)で構成されている。

ラズベリーパイPico Wを子機とした時を例とすると、サービスがラズベリーパイPico Wに該当し、キャラクタリスティックがラズベリーパイPico Wに組み込んだ機能と捉えると以後のコードの理解が進みます。


キャラクタリスティックにはATTと呼ばれる属性値が含まれていて、

・ハンドル(handle)

・型(type)

・値(value)

・権限(permission)

があります。


ここらへんの話は一旦用語の把握までにしておいて、実際のコードに触れてから改めて調べる事をおすすめします。




Pythonでコードを書く際にbluepyというライブラリを用いる事にします。

GitHub - IanHarvey/bluepy: Python interface to Bluetooth LE on Linux


最初に端末(ターミナル)を開き、下記のコマンドでbluepyを使用できるようにします。

$ sudo apt install python-pip libglib2.0-dev
$ sudo pip install bluepy



今回はプロジェクト名をbleにして話を進めます。

下記のコマンドでブルートゥースの親機(セントラル)用のコードを作成する為のディレクトリを作成します。

$ cd ~
$ mkdir workspace
$ cd workspace
$ mkdir ble
$ cd ble

最初に子機に割り当てられたMACアドレスを調べるコードを作成します。

~/workspace/ble/scan.py

from bluepy import btle

scanner = btle.Scanner(0)
devices = scanner.scan(3.0)

for dev in devices:
    print("[MAC-Adress]:", dev.addr)
    print("[Address-Type]:", dev.addrType)

上記のコードを実行してみると、

$ python3 ~/workspace/ble/scan.py
[MAC-Adress]: XX:XX:XX:XX:XX:XX
[Address-Type]: public

のように出力されます。

どちらの値も重要ですので、記録しておきます。




続いて、子機のUUIDやハンドル(handle)について調べます。

~/workspace/ble/handle.py

from bluepy import btle
import time

PERIPHERAL_MAC_ADDRESS = "XX:XX:XX:XX:XX:XX"

p = btle.Peripheral()
# scan.pyで調べたMACアドレスとアドレスタイプを指定します
p.connect(PERIPHERAL_MAC_ADDRESS, btle.ADDR_TYPE_PUBLIC)
print("connected")

chars = p.getCharacteristics();
for char in chars:
	print("UUID : %s" % char.uuid )
	print("Handle %04x: %s" % (char.getHandle(), char.propertiesToString()))

p.disconnect()
print("disconnected")

上記のコードを実行してみると、

$ python3 ~/workspace/ble/handle.py
UUID : 00002a00-0000-1000-8000-00805f9b34fb
Handle 0003: READ 
UUID : 00002a05-0000-1000-8000-00805f9b34fb
Handle 0006: READ 
UUID : 6e400003-b5a3-f393-e0a9-e50e24dcca9e
Handle 0009: READ NOTIFY 
UUID : 6e400002-b5a3-f393-e0a9-e50e24dcca9e
Handle 000c: WRITE NO RESPONSE WRITE 

のような値が返ってきます。

今回は子機から送信された値を取得する予定ですので、ATTの権限でREADが含まれている方のUUIDやハンドラ(0003)を記録しておきます。

※以後で作成するコードではUUID等の値は使用しません




得られた値を基にして、Serial Bluetooth Terminalのように動作する為のコードを作成してみます。

~/workspace/ble/main.py

from bluepy import btle

PERIPHERAL_MAC_ADDRESS = "XX:XX:XX:XX:XX:XX"

class MyDelegate(btle.DefaultDelegate):
    def __init__(self):
        btle.DefaultDelegate.__init__(self)

    def handleNotification(self, cHandle, data):
        # 子機から得られたデータを端末に出力する
        print(data)
        
p = btle.Peripheral()
p.connect(PERIPHERAL_MAC_ADDRESS, btle.ADDR_TYPE_PUBLIC)
    
print("connected")
p.withDelegate(MyDelegate())

# 通信が切れたりSIGINTの割り込みが発生しない限り接続を維持する
try:
	TIMEOUT = 3.0
	while True:
		if p.waitForNotifications(TIMEOUT):
			continue
except:
	# SIGINTを許可
	p.disconnect()
	print("disconnected")

上記のコードを実行してみると、

$ python3 ~/workspace/ble/main.py

端末にSerial Bluetooth Terminalで出力されたような値が出力され続けます。

出力を止めたい場合はctrl + cで接続を切ることが出来ます。