13
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ラズパイ3BをBLEセントラルにして、micro:bit をBLEペリフェラルとして使う(主に無線の入力装置として)

Last updated at Posted at 2019-02-11

概要

以前の記事 で、ラズパイと AE-TYBLE16をやったので、今度はラズパイとmicro:bit でBLEしてみようという話。今回も bluepy でやります。

  • device name, serial number 取得(read)
  • LEDの点灯操作(write)
  • 文字列の表示(write)
  • 温度の取得(read)
  • ボタン状態の notification 取得
  • 加速度センサー状態の notification 取得

あたりをやってます。

micro:bit 側準備

参考記事1を参考に、高度なブロックの中の拡張機能の追加でBluetoothを追加、設定-プロジェクトの設定で「ペアリングなしでの接続設定」にする、最初だけで Bluetoothサービスオンにする設定、をする。

Bluetooth 拡張機能を追加するときに「【無線(radio)】が使えなくなるけどいいか?」と脅されるけど、プロジェクト毎に【無線(radio)】 or 【Bluetooth】どちらかだけ使えるということなので気にせず進みましょう。(今まで無線で作ったプログラムがあっても大丈夫)

ペアリングはしたい方は別途調べてください。

ラズパイ側準備

以前の記事の通りです。相変わらず bluepy を使うための最小インストールが解らないので

$ sudo apt-get update
$ (たぶん不要) sudo apt-get install bluez
$ sudo apt-get install python-dev libbluetooth3-dev libglib2.0 libboost-python-dev libboost-thread-dev
$ (たぶん不要) sudo pip install pybluez
$ sudo pip install gattlib
$ sudo pip install bluepy
$ sudo systemctl daemon-reload
$ sudo service bluetooth restart

あとは sudo hcitool lescan でデバイスがみつかればOK。
出てこなかったら micro:bit をリセット。それでもでないならプロジェクトの設定(ペアリング要否)あたりミスってないか確認。

micro:bit の BLEアドレスはメモっておきましょう。ここでは仮に"C4:A7:19:XX:XX:XX"とします。

機器名とシリアルNoの取得

いきなりpythonのコードから行きますが

microtest1.py
# -*- coding: utf-8 -*-
from bluepy.btle import Peripheral
import bluepy.btle as btle

str_dev =  "C4:A7:19:XX:XX:XX"

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

class MyPeripheral(Peripheral):
    def __init__(self, addr):
        Peripheral.__init__(self, addr, addrType="random")

def main():
    # 初期設定
    perip = MyPeripheral(str_dev)
    perip.setDelegate(MyDelegate(btle.DefaultDelegate))

    # device name, serial number
    devname = perip.readCharacteristic(0x0003)
    print( "Device Name: %s" % devname )
    serialnum = perip.readCharacteristic(0x0017)
    print( "Serial Number: %s" % serialnum )

if __name__ == "__main__":
    main()

main のところで readCharacteristic ってやってるのが情報取得系です。引数は handle なのですが、これは後述 「handle の調べ方」 のように gatttool で調べるのですが、自分が調べた範囲はここに載せるので同じで試して違ったら gatttool 使うので良いのかな、と。

デバイス名は handle 0x0003、 serial number は handle 0x0017 でした。

MyDelegate は callback 関数を記述するクラスで、このサンプルでは何もしませんが、notification を受け取るときに書き足します。

LED(5x5)を点灯させる

handle は 0x003e, LEDの付ける消すを5バイトのデータに入れて writeCharacteristic で送ります。詳細はコードのコメント参照。

microtest_led.py
# -*- coding: utf-8 -*-
from bluepy.btle import Peripheral
import bluepy.btle as btle
import time

str_dev = "C4:A7:19:XX:XX:XX"

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

class MyPeripheral(Peripheral):
    def __init__(self, addr):
        Peripheral.__init__(self, addr, addrType="random")

def main():
    # 初期設定
    perip = MyPeripheral(str_dev)
    perip.setDelegate(MyDelegate(btle.DefaultDelegate))

    # LED
    print( "LED操作" )
    perip.writeCharacteristic(0x003e,"\x01\x02\x04\x08\x1f" ) # 5x5 LED の光る場所を 5bit x 5つ16進数で指定
    """
    00001 = \x01
    00010 = \x02
    00100 = \x04
    01000 = \x08
    11111 = \x1f   という意味
    """
    time.sleep(1)

if __name__ == "__main__":
    main()

温度センサの数値を取ったり、文字列を表示させたり

温度は handle 0x0045 を readCharacteristic すれば良し(数値への変換は面倒だけど)。文字列表示は 0x0040 に文字列を writeCharacteristic すればよし。

そろそろ全部書かなくても良いかな?ということで main の後半だけ抜粋。

温度は気温より高く出るので(PCのCPU温度とかもそうですよね)何に使えばいいのか解らない。

    val0045 = perip.readCharacteristic(0x0045) # 1バイトのバイト列がStr化される
    print( "温度: %d " % int(binascii.b2a_hex(val0045[0]) ,16)) # python の型変換よくわからん

    print( "micro:bit に文字列'ABC'を表示" )
    perip.writeCharacteristic(0x0040, "ABC")

ボタンや加速度センサーの状態を notification で取得する

ここでようやく MyDelegate にハンドラ追加です。まずは class MyDelegate だけ解説。(python 書き慣れてなくて if だらけで申し訳ない)

import binascii
import struct

exflag = False #無限ループから抜ける判定用:ボタン長押しで抜けるようにしている。

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

    def handleNotification(self, cHandle, data):
    # notify callback: cHandle で何の情報かを見分けて処理分岐
        global exflag
        c_data = binascii.b2a_hex(data)

        if cHandle == 0x27: # 加速度センサー
            accX = struct.unpack('h', data[0:2] )[0]
            accY = struct.unpack('h', data[2:4] )[0]
            accZ = struct.unpack('h', data[4:6] )[0]
            strX = "Flat"
            strY = "Flat"
            if accX > 700: strX = "Plus"
            if accX < -700: strX = "Minus"
            if accY > 700: strY = "Plus"
            if accY < -700: strY = "Minus"

            print( "%s: %d, %d, %d : %s %s " % (c_data, accX, accY, accZ, strX, strY) )
            return

        if cHandle == 0x2d: # left button
            b = "button1"
            if data[0] == "\x02": exflag = True # long push to exit
        if cHandle == 0x30: # right button
            b = "button2"
            if data[0] == "\x02": exflag = True # long push to exit

        print( "%s: %s" % (b,c_data) )

cHandle というところの値で button1 (0x002d) , button2(0x0030), 加速度センサー(0x0027)等、どれの通知かが解ります。(これ以外は調べてません)
data のほうは、ボタンではdata[0] が 0 or 1 or 2 がそれぞれ「押してない」「押してる」「長押し」です。
加速度センサーは、6バイトのデータが来て、2バイト(16bit)ずつX,Y,Zのセンサーの値を示す「符号付き16bit整数(リトルエンディアン)」で届きますので、unpack して accX,accY,accZ などに格納しています。Zは使いにくいので無視してますが、左右と前後はどれくらい傾けたら反応するかの閾値としてここでは±700を設定しています。

と、これだけやっても通知はこないので、「通知を送るようにmicro:bitに指示する」ことがmain() あたりで必要になります。具体的なコードはこちら。

    perip = MyPeripheral(str_dev)
    perip.setDelegate(MyDelegate(btle.DefaultDelegate))

    # ボタン notify を要求
    perip.writeCharacteristic(0x002e, "\x01\x00", True)
    perip.writeCharacteristic(0x0031, "\x01\x00", True)

    # 加速度 notify 間隔を指定して、通知要求
    perip.writeCharacteristic(0x002a, "\x50\x00", True) # 80ms ごとに通知 #デフォルトは 20ms
    perip.writeCharacteristic(0x0028, "\x01\x00", True) # 通知有効化

    # データを受信し続ける
    print( "Notification を待機。A or B ボタン長押しでプログラム終了")
    while exflag == False:
        if perip.waitForNotifications(1.0):
            continue

それぞれ決まってる handle (button1=0x002e, button2=0x0031) に "\x01\x00" という値を write して notification を有効にします。(Arduino-ESP32 あたりでは、Characteristic ではなく、Descriptor への writeValue である、と区別されてるのですが、bluepy は writeCharacteristic に descriptor の handle を指定して"\x01\x00")

加速度センサーのほうは、0x002a で通知の更新間隔を指定できます。
前述のサイトにあるとおり 1, 2, 5, 10, 20, 80, 160 and 640 ms から指定します。10進数80 = 0x0050 なので、リトルエンディアンで "\x50\x00" です。

handle の調べ方

もっとちゃんとしたやり方もあるかと思いますが、下記の雑なやり方で足りているので深く追求してません。
(2019/3/19 もうちょい追求して記事を書きました https://qiita.com/ajtajta_j/items/ab0b09edf45976af0094 、が、 手順だけなら下記で十分かと)

加速度センサーを例にしますが、何度も挙げてる前述のサイト で情報収集すると、

Accelerometer Service は UUID: E95D0753251D470AA062FA1922DFA9A8 です。

センサの値の characteristics UUID: E95DCA4B251D470AA062FA1922DFA9A8 で、Read/Notify 可能。
また Client Characteristic Configuration : 2902 という情報もあり、こちらが通知可能に設定するための情報のようです。(2019/3/19追記:これ、CCCD といわれるディスクリプタの 16bit uuid で、どこの機器でも 2902 だったり。)

また、通知間隔の characteristics もあり、UUID: E95DFB24251D470AA062FA1922DFA9A8 で Read/Write 可能です。

あとは gatttool で

$ gatttool -b C4:A7:19:XX:XX:XX -t random -I 
[C4:A7:19:XX:XX:XX][LE]> connect
Attempting to connect to C4:A7:19:XX:XX:XX
Connection successful

みたいに繋いで調べます。

primary (サービスの列挙)してみると

[C4:A7:19:XX:XX:XX][LE]> primary 
attr handle: 0x0025, end grp handle: 0x002a uuid: e95d0753-251d-470a-a062-fa1922dfa9a8 

が見つかります。(他にもずらずらでるので サービス uuid で区別します)なんか 0x0025から 0x002a が1つの group っぽい感じ。範囲指定で characteristics(キャラクタリスティクの列挙) してみると

[C4:A7:19:XX:XX:XX][LE]> characteristics 0x0025 0x002a
handle: 0x0026, char properties: 0x12, char value handle: 0x0027, uuid: e95dca4b-251d-470a-a062-fa1922dfa9a8 
handle: 0x0029, char properties: 0x0a, char value handle: 0x002a, uuid: e95dfb24-251d-470a-a062-fa1922dfa9a8 

が見つかり、前述サイトの characteristics uuid と 一致しています。これにより handle 0x0027 はnotification が来た時に cHandle に入ってくる値となり、0x002a は通知間隔を設定するときに使う handle と特定できます。

通知を有効に有効にするのは descriptor への write なので、範囲指定 char-desc してみると

[C4:A7:19:XX:XX:XX][LE]> char-desc 0x0025 0x002a
handle: 0x0025, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x0026, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0027, uuid: e95dca4b-251d-470a-a062-fa1922dfa9a8
handle: 0x0028, uuid: 00002902-0000-1000-8000-00805f9b34fb
handle: 0x0029, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x002a, uuid: e95dfb24-251d-470a-a062-fa1922dfa9a8

ここで「2902」を思い出すと…UUIDの最初が「00002902」って奴が1つだけあります。0x0028 です。これが notify を有効にするための奴。(19/3/19 16bit uuid 0x2902 の 128bit 表記が 00002902-0000-1000-8000-00805f9b34fb です。どこの機器でも notify を有効にするためのディスクリプタである CCCD はこのuuid になっています。)(ちなみに範囲指定なしで char-desc すると同じ uuid でいくつも出てきます。他のサービスでも 2902 が config 用なので)

長いソース

うちで実際に使ってるプログラムはこちら。(BLEアドレスだけ伏せてる)
ですが、長いし他のスクリプトを subprocess.call とかしてて意味不明だし、見なくても良いと思います。

github にも置きました。そしてファイル名が2種類書いてあったので統一。

長いソース: RPi-microbit2.py
RPi-microbit2.py
# -*- coding: utf-8 -*-
from bluepy.btle import Peripheral
import bluepy.btle as btle
import binascii
import time
import sys
import datetime
import subprocess
import struct

exflag = False #無限ループから抜ける判定用:ボタン長押しで抜けるようにしている。
# BLE ペリフェラル= micro:bit のBLEアドレス, 複数あるのでコマンドパラメーターで切り替えられるようにしている。

list_dev = [ "C4:A7:19:XX:XX:XX", "C4:A7:19:YY:YY:YY" ]

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

    def handleNotification(self, cHandle, data): # notify callback: cHandle で何の情報かを見分けて処理分岐
        global exflag
        c_data = binascii.b2a_hex(data)

        if cHandle == 0x27: # 加速度センサー
            accX = struct.unpack('h', data[0:2] )[0]
            accY = struct.unpack('h', data[2:4] )[0]
            accZ = struct.unpack('h', data[4:6] )[0]
            strX = "Flat"
            strY = "Flat"
            if accX > 700: strX = "Plus"
            if accX < -700: strX = "Minus"
            if accY > 700: strY = "Plus"
            if accY < -700: strY = "Minus"

            print( "%s: %d, %d, %d : %s %s " % (c_data, accX, accY, accZ, strX, strY) )
            return

        if cHandle == 0x2d: # left button
            b = "button1"
            if data[0] == "\x02": exflag = True # long push to exit
        if cHandle == 0x30: # right button
            b = "button2"
            if data[0] == "\x02": exflag = True # long push to exit
            if data[0] == "\x01":
                subprocess.call(['/usr/bin/python', '/home/pi/TYBLE/ntptime1.py', '2', 'SingleDivert'] )
                print( "Call SingleDivert" )

        print( "%s: %s" % (b,c_data) )

class MyPeripheral(Peripheral):
    def __init__(self, addr):
        Peripheral.__init__(self, addr, addrType="random")

def main():
    args = sys.argv
    if ( len(args) <= 1 or (args[1] != "1" and args[1] != "2") ) :
        print( "Bad Param: ", args[1] )
        return

    # 初期設定
    perip = MyPeripheral(list_dev[int(args[1])-1]) # コマンドラインパラメーターで 1 or 2 を指定するので、1引いてlist_dev の添え字に
    perip.setDelegate(MyDelegate(btle.DefaultDelegate))

    # device name, serial number
    devname = perip.readCharacteristic(0x0003)
    print( "Device Name: %s" % devname )
    serialnum = perip.readCharacteristic(0x0017)
    print( "Serial Number: %s" % serialnum )

    # 温度を表示してみる(気温より高く出るのでそのまま表示してもあまり意味が無い)
    val0045 = perip.readCharacteristic(0x0045) # 1バイトのバイト列なのだが文字列にされてしまう
    print( "温度: %d " % int(binascii.b2a_hex(val0045[0]) ,16)) # python の型変換よくわからん

    # LED
    print( "LED操作" )
    perip.writeCharacteristic(0x003e,"\x01\x02\x04\x08\x1f" ) # 5x5 LED の光る場所を 5bit x 5つ16六進数で指定
    """
    00001 = \x01
    00010 = \x02
    00100 = \x04
    01000 = \x08
    11111 = \x1f   という意味
    """
    time.sleep(1)

    # 文字列表示
    print( "micro:bit に文字列'ABC'を表示" )
    perip.writeCharacteristic(0x0040, "ABC")

   # ABCを表示完了しなくても次に進むので、表示途中でもボタンなどの通知は来る

    # ボタン notify を要求
    perip.writeCharacteristic(0x002e, "\x01\x00", True) # button A notifyを有効にする
    perip.writeCharacteristic(0x0031, "\x01\x00", True) # button B notifyを有効にする

    # 加速度 notify 間隔を指定して、通知要求
    perip.writeCharacteristic(0x002a, "\x50\x00", True) # 80ms ごとに通知 #デフォルトは 20ms
    perip.writeCharacteristic(0x0028, "\x01\x00", True) # 通知有効化


    # データを受信し続ける
    print( "Notification を待機。A or B ボタン長押しでプログラム終了")
    while exflag == False:
        if perip.waitForNotifications(1.0):
            continue
        #sys.stdout.write( "." )

if __name__ == "__main__":
    main()

読みづらくてすまんて

備忘録としてはこれでいいのだろうけども…

その他の参考サイト

ESP32@arduino ideとmicrobitのBLE通信 は大いに参考にさせて頂きました(ESP32なのでこの記事では引用してませんが)

13
11
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?