ブロックを積みながら(20) micro:bit, BLE UartへPython書き出し

Joseph Halfmoon

前回は、BBC micro:bit のBLE特性(Characteristics)のうち、読めばわかりそうなものをラズパイPython3で読み出してみました。今回は、BLE Uartサービスに「書き込んで」micro:bitを鳴かせてみたいとおもいます。また、前回のコードだとmicro:bit v2ではうまく行かないことも発覚したので修正しました。

※「ブロックを積みながら」投稿順 index はこちら

今回は BLE Uartサービス経由で「コマンド」をmicro:bit へ送り、音を鳴らそうということなので、ターゲットの micro:bit を前回の v1.5から、スピーカ搭載のv2へ変更いたしました。micro:bitに書き込んだコードは、第14回で使用したものの微妙な修正版です。一応アイキャッチ画像にMakeCodeのブロック表現を掲げました(末尾にJavaScript表現も置きました。)第14回ではスマホのBluetooth UARTアプリからコマンドを送信して音を鳴らしていました。同等のことがようやくRaspberry Pi 3 model B+上のPython3からできるようになった、というわけです。

ラズパイ上のPython3コード(前回の追加修正版)も後ろに全文掲げました。修正点については後で述べますが、BLE UARTへの送信対応に加えて、micro:bit v1.5では動作するけれど、v2では動作しない点について対応もしています。

micro:bit 搭載の BLE Uartサービス

さて、MakeCodeエディタでブロックを配置すると使えるようになる Bluetooth UARTサービスは、nRF51/nRF52チップの開発元のNordic社の以下のソフトウエアモジュールであるようです。NUSというお名前。

Nordic UART Service (NUS)

上記ページは、Nordic社のSDK上での利用を前提としています。しかし当方、Python上の bluepy モジュールによるアクセスなので、Python上でのUartサービスの使い方を端的に知りたいです。

なお、Uartサービスの説明の中でRXとかTXとか表現が出てきます。UARTなので当然ですかね。その「向き」なのですが、今回はmicro:bit 側から見たときの RX, TXです。ラズパイPythonからみると「クロス」で接続する必要があるので逆になります。現物UARTと同じ。

先人の偉大な業績

当然といえば当然ですが、私のやりたいことなどとっくの昔に調べておられる先覚者の方々がおられます。以下に参考にさせていただいたサイトへのリンクを張らせていただきました。

bluepyで始めるBluetooth Low Energy(BLE)プログラミング

Raspberry Pi で bluepy を使ってmicro:bitをコントロール

Raspberry PiからBluetooth経由のUARTでmicrobitと通信する

これらを参考にさせていただいたので、あまり苦労もなくBLE Uartへの送信ができるようになりました。ありがとうございました。

今回の改訂ポイント

第14回のmicro:bitコードは、文字列(といっても先頭2文字)がコマンドとなっていました。特定の文字列に反応し、内蔵のメロディを奏でる。解釈できないコマンドのときはピロリ~ンと鳴る、というものでした。今回の送り出し側のPython3コードでは、文字列を送り出すのは

--SEND 文字列

という形式です。connectされたmicro:bitのUARTサービスのRXへ文字列を送り込む引数を設けました。2文字でなく最大20文字までです。

また、Uart関係の特性やデスクリプタを列挙するために

-uartinfo

なるオプションも設けました。これを使うと関係のcharacteristicsなど列挙します。

そして、直近の作業性のため、手元に3台ある micro:bit のアドレスをイチイチ指定しないで済むように、0, 1, 2という番号で指定できるオプションも増設しました。今回の micro:bit V2機の場合、

--N 1

で指定がすみます。

なお、前回から存在した -info というデバイスの情報をダンプする機能ですが、micro:bit v2ではうまく動作しませんでした。理由は、

v1.5のときのハンドルとv2でのハンドルが違う

ためでありました。第18回で調べたハンドルはv1.5のものでした。そこで、ちとメンドイですが、UUIDに戻って特性をリストアップすることで v1.5でもv2でも動作するように改版いたしました。ただし、ファームウエアバージョンについては、v2上でもそのUUIDは見つかるものの、読み出すと Unknown 表示となります。

末尾のプログラムを以下の引数を与えて起動すると、ラズパイ上では以下のような表示がでます。そしてmicro:bit V2は内蔵楽曲 blues を奏でました。

$ python3 microbitBLEutil02.py --N 1 --SEND blues
microbitBLEutil v0.2
CONNECTED: F7:FB:7A:03:96:AA
SEND: b'blues\n'
DISCONNECTED.

まあ、とりあえず、Uartに文字列送信はできてたか。

ブロックを積みながら(19) micro:bit BLE、Pythonで「特性」読み出し に戻る

ブロックを積みながら(21) MakeCode, Compilation failed へ進む

ラズパイ上のPython3コード(micro:bit v2、UART送信対応)
#! /usr/bin/python3
import argparse
import bluepy
import sys
import time

versionSTR = "microbitBLEutil v0.2"

class microbitBLE:

    def __init__(self, adr):
        self.devadr = adr
        self.pobj = None
        self.uartOBJ = None

    def conPobj(self):
        try:
            self.pobj = bluepy.btle.Peripheral()
            self.pobj.connect(self.devadr, bluepy.btle.ADDR_TYPE_RANDOM)
        except:
            print("ERROR: connect.")
            return False
        return True

    def listCharacteristics(self):
        chrLis = self.pobj.getCharacteristics()
        for item in chrLis:
            print(" UUID: {0} Handle: 0x{1:04x} Prop: {2}".format(item.uuid, item.getHandle(), item.propertiesToString()))

    def getDeviceInfo(self):
        work = dict()
        work['DeviceName'] = str(self.pobj.readCharacteristic(0x0003), 'utf-8')
        modelNumCH = self.pobj.getCharacteristics(uuid="00002a24-0000-1000-8000-00805f9b34fb")[0]
        serialNumCH = self.pobj.getCharacteristics(uuid="00002a25-0000-1000-8000-00805f9b34fb")[0]
        firmRevCH = self.pobj.getCharacteristics(uuid="00002a26-0000-1000-8000-00805f9b34fb")[0]
        work['ModelNumber'] = str(self.pobj.readCharacteristic(modelNumCH.getHandle()), 'utf-8')
        work['SerialNumber'] = str(self.pobj.readCharacteristic(serialNumCH.getHandle()), 'utf-8')
        work['FirmwareRevision'] = str(self.pobj.readCharacteristic(firmRevCH.getHandle()), 'utf-8')
        return work

    def getServiceInfo(self):
        return self.pobj.getServices()

    def getDescInfo(self):
        return self.pobj.getDescriptors()

    def setupUart(self):
        chTX = self.pobj.getCharacteristics(uuid="6E400002-B5A3-F393-E0A9-E50E24DCCA9E")[0]
        ch_cccd=chTX.getDescriptors(forUUID=0x2902)[0]
        ch_cccd.write(b"\x01\x00", False)
        chRX = self.pobj.getCharacteristics(uuid="6E400003-B5A3-F393-E0A9-E50E24DCCA9E")[0]
        self.uartOBJ = bleDelegate(chTX, chRX)
        self.pobj.setDelegate( self.uartOBJ )

    def sendStrUart(self, arg):
        if self.uartOBJ is None:
            self.setupUart()
        barg = bytes(arg + '\n', 'utf-8')
        if len(barg) > 20:
            return None
        self.pobj.writeCharacteristic(0x002a, barg, False)
        return barg

    def uartInfoTX(self):
        uartTX = self.pobj.getCharacteristics(uuid="6E400002-B5A3-F393-E0A9-E50E24DCCA9E")[0]
        print("TX Handle: 0x{0:04x} Prop: {1}".format(uartTX.getHandle(), uartTX.propertiesToString()))
        descTX = uartTX.getDescriptors()
        for item in descTX:
            print(str(item))

    def uartInfoRX(self):
        uartRX = self.pobj.getCharacteristics(uuid="6E400003-B5A3-F393-E0A9-E50E24DCCA9E")[0]
        print("RX Handle: 0x{0:04x} Prop: {1}".format(uartRX.getHandle(), uartRX.propertiesToString()))
        descRX = uartRX.getDescriptors()
        for item in descRX:
            print(str(item))

    def close(self):
        self.pobj.disconnect()

class bleDelegate(bluepy.btle.DefaultDelegate):
    def __init__(self, TXD, RXD):
        bluepy.btle.DefaultDelegate.__init__(self)
        self.TX = TXD
        self.RX = RXD

    def handleNotification(self, hdl, dat):
        print("hdl={0}, dat={1}".format(hdl, dat))

def tryIntParse(argstr):
    try:
        return int(argstr)
    except ValueError:
        return None

def main():
    parser = argparse.ArgumentParser(description='microbitBLEutil.')
    parser.add_argument('--ADR', nargs=1, help='BLE address.')
    parser.add_argument('--N', nargs=1, help='0) Gv1.5 1) Gv2 2) Rv1.5')
    parser.add_argument('--SEND', nargs=1, help='Send string via UART service.')
    parser.add_argument('-uartinfo', dest='uartinfo', help='uart Info.', action='store_true', default=False)
    parser.add_argument('-info', dest='info', help='device Info.', action='store_true', default=False)
    parser.add_argument('-service', dest='service', help='available service list.', action='store_true', default=False)
    parser.add_argument('-desc', dest='descriptor', help='available descriptor list.', action='store_true', default=False)
    parser.add_argument('-uuid', dest='uuid', help='list all UUIDs.', action='store_true', default=False)
    parser.add_argument('-V', dest='VERSION', help='Show Version, then exit', action='store_true', default=False)
    args = parser.parse_args()

    print(versionSTR)
    if args.VERSION:
        sys.exit(0)

    devLis = ["xx:xx:xx:xx:xx:xx", "xx:xx:xx:xx:xx:xx", "xx:xx:xx:xx:xx:xx"] # Your micro:bit BLE device address list
    devadr =  devLis[0]
    if args.ADR is not None:
        devadr = args.ADR[0]

    if args.N is not None:
        idx = tryIntParse(args.N[0])
        if (idx is not None) and (idx < len(devLis)):
            devadr = devLis[idx]

    mb = microbitBLE(devadr)

    if mb.conPobj():
        print("CONNECTED: ", devadr)
    else:
        sys.exit(1)

    if args.uuid:
        mb.listCharacteristics()

    if args.info:
        for key, value in mb.getDeviceInfo().items():
            print(key, "=", value)

    if args.service:
        for srv in mb.getServiceInfo():
            print(str(srv))

    if args.descriptor:
        for desc in mb.getDescInfo():
            print(str(desc))

    if args.uartinfo:
        mb.uartInfoTX()
        mb.uartInfoRX()

    if args.SEND is not None:
        print("SEND:", mb.sendStrUart(args.SEND[0]))

    mb.close()
    print("DISCONNECTED.")

if __name__ == "__main__":
    main()
micro:bit V2 用テストコード(JavaScript形態)
bluetooth.onUartDataReceived(serial.delimiters(Delimiters.NewLine), function () {
    command = bluetooth.uartReadUntil(serial.delimiters(Delimiters.NewLine)).substr(0, 2)
    if (command == "on") {
        music.setBuiltInSpeakerEnabled(true)
    } else if (command == "of") {
        music.setBuiltInSpeakerEnabled(false)
    } else if (command == "bl") {
        music.startMelody(music.builtInMelody(Melodies.Blues), MelodyOptions.Once)
    } else if (command == "fu") {
        music.startMelody(music.builtInMelody(Melodies.Funk), MelodyOptions.Once)
    } else {
        soundExpression.giggle.play()
    }
})
let command = ""
bluetooth.startUartService()
basic.forever(function () {
    led.toggle(0, 0)
    basic.pause(100)
})