📶

初めてのBLE(RN4870)。本格的な組込み開発に入る前にプロトタイピング。

2023/07/13に公開

1. はじめに

システムゼウスでIoT開発をしている南坂です。

BLEモジュール(チップ、基板)を用いた組込み開発に初めて取り組む方向けの記事になります。BLEモジュールはMicrochip社製のRN4870を取り上げます。

通常は以下のような構成でスマホアプリからデバイスに対してデータを書き込んだり(送ったり)、逆にデータを読み取ったりすると思います。

スマホアプリ <-> BLEモジュール(RN4870) <-> MCU

上記構成での本格的な組込み開発に入る前に、後述するようにPC+Pythonを用いた簡易な環境にてBLE通信にかかる基礎的な事項を確認することをおすすめします。

2. 開発環境

2.1. 物理構成

モック環境
モック環境

No. デバイス名 メーカー 型番・品名など 説明
1 BLE MIKROE RN4870 アプリとの通信に用いる。モジュールはMicrochip製RN4870。
2 FT232RL FTDI FT232RL USB<->UART変換

2.2. ソフトウェア構成

動作確認済みのPC環境はWindows 11 22H2です。

No. ソフトウェア名 バージョン 説明
1 Python3 >3.10.5 モックの動作
2 pyserial 3.5b0 pythonライブラリ。BLEモジュールとのシリアル通信。
3 bleak 0.19.5 pythonライブラリ。BLEモジュールとのBLE通信。

※バージョンは動作確認がとれているものです

3. RN4870の設定

RN4870はすべてASCII文字を用いて設定を行います。
コマンドの詳細は公式ドキュメントを見てください。

3.1. FWバージョンの確認

RN4870のFWバージョンを確認してください。初期状態だとFWバージョンが古い可能性があります。(購入先でいつ在庫になったかで違うみたいです)

確認方法はPC(Teraterm) <-USB-> USB-UART変換(FT232RLなど) <-UART-> RN4870のように接続し、V\rコマンドを送信するだけです。

RN4870 V1.41 11/06/2020 (c)Microchip Technology Inc

FWバージョンが1.41未満だった場合は最新のFWに更新してください。
更新手順はここを参照してください。

3.2. GATTテーブルの作成

以下のようなGATT(ガットと呼びます)テーブルを作ります。

Service UUID: 4fafc201-1fb5-459e-8fcc-c5c9c331914b

No. Characteristic UUID Property
1 7b842730-a65c-457b-8b85-5dce1fa2ead1 write | read

3.3. ペアリングの設定

BLEでWi-Fiのパスワードやワンタイムトークンといった秘密情報がやり取りされるのでペアリングをし、暗号通信をします。
RN4870ではペアリング方式を以下の4種類から選択できます。

方式 説明
DisplayYesNo Numeric Comparison: RN4870とスマホ両方に数値が表示され、一致していることを確認しペアリングを行う
NoInputNoOutput Just Works: 認証は特にせずにペアリングを行う
KeyboardOnly Passkey Entry: スマホ側で数値が表示されるのでRN4870側にその数値の入力することでペアリングを行う
DisplayOnly Passkey Entry: RN4870側に数値が表示されるのでスマホ側にその数値の入力することでペアリングを行う

またそれぞれ設定した場合のペアリング時のスマホ画面は以下のようになります。

DisplayYesNo NoInputNoOutput KeyboardOnly DisplayOnly
DisplayYesNo デバイスを選択するだけでペアリングが完了(確認画面が出ることはある) KeyboardOnly DisplayOnly

※画面はAndroid9のもの

今回はDisplayOnlyを設定します。
モジュールに設定するPINは固定値にします。PINを事前に知っている人だけがペアリングを行うことができます。
※プロトタイピング目的なのでここでは固定値にしていますが、商用システムではセキュリティ強度の高い仕組みを考える必要があります。ときにはBLEの仕組みに依らない上位レイヤでセキュリティを確保する必要もあるでしょう。

4. モックの作成

必要なライブラリをインストールします。

$ pip install pyserial==3.5b0 bleak==0.19.5

4.1. アプリモック

処理概要

  1. TESTという名前のBLEを検索、接続
  2. characteristic UUIDが7b842730-a65c-457b-8b85-5dce1fa2ead1のものに対して、値を読込->書込->読込の順に行う

スクリプト

mock_app.py
import sys
import time
import pprint
import asyncio

from bleak import discover
from bleak import BleakClient

BLE_NAME = 'TEST'

TEST_CHARA_UUID = ("7b842730-a65c-457b-8b85-5dce1fa2ead1")


async def run(address, loop, name=None):
    devices = await discover()
    pprint.pprint(devices)
    address = None
    for d in devices:
        if d.name == name:
            address = d.address
    if address is None:
        print(f'Not Found. {name}')
        sys.exit(1)
    print(f'Find. {name} (address: {address})')

    async with BleakClient(address, loop=loop) as client:
        # 接続
        x = await client.is_connected()
        print(f'Connected: {x}')

        test = await client.read_gatt_char(TEST_CHARA_UUID)
        print(test)

        print('Write.')
        await client.write_gatt_char(TEST_CHARA_UUID, b'message from App', response=True)

        test = await client.read_gatt_char(TEST_CHARA_UUID)
        print(test)


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(run(None, loop, name=BLE_NAME))

4.2. MCUモック

処理概要

  1. $$$\rコマンド送信(これでRN4870がASCIIコマンドを受け付けるようになります)
  2. PZ\rコマンド送信(登録していたサービスを一度すべて削除)
  3. PS,4fafc201-1fb5-459e-8fcc-c5c9c331914b\rコマンド送信(サービス登録)
  4. PCコマンドを用いてGATTテーブルで定義したものを登録
  5. SP,123456\rコマンド送信(ペアリングのPINを設定)
  6. SA,4\rコマンド送信(ペアリング方式をDisplayOnlyに設定)
  7. R,1\nコマンド送信(設定を反映されるためにモジュールの再起動)
  8. SHWでcharacteristicに初期値(message from MCU)を代入
  9. ループでcharacteristicの値を表示

スクリプト

mock_mcu.py
import time
import serial

COM = 'COM10'

DEVICE_NAME = 'TEST'

SERVICE_UUID = '4fafc201-1fb5-459e-8fcc-c5c9c331914b'
TEST_CHARA_UUID = '7b842730-a65c-457b-8b85-5dce1fa2ead1'
TEST_CHARA_DATA_SIZE = 151

PIN = '123456'

# Characteristic property
READ = 0b00000010
WRITE = 0b00001000

handle_info = {}


def send_cmd(cmd: str, end_str: str) -> str:
    if cmd == '$$$':
        port.write('$$'.encode('utf-8'))
        time.sleep(1)
        port.write('$'.encode('utf-8'))
    else:
        cmd += '\r'
        port.write(cmd.encode('utf-8'))
    read = ''
    while True:
        c = port.read()
        read += c.decode('utf-8')
        if read.endswith(end_str):
            return read
    return None

# --------------------------------------------------


port = serial.Serial(COM, 115200, timeout=3)

print('Enter CMD mode...')
send_cmd('$$$', end_str='CMD>')

print('Check version info...')
version_info = send_cmd('V', end_str='CMD>')
print(version_info.replace('CMD>', '').strip())

print('Set device name...')
cmd = f'SN,{DEVICE_NAME}'
send_cmd(cmd, end_str='CMD>')

print('Clear all service...')
send_cmd('PZ', end_str='CMD>')

print('Set Service...')
service_uuid = SERVICE_UUID.replace('-', '').upper()
cmd = f'PS,{service_uuid}'
send_cmd(cmd, end_str='CMD>')

print('Set characteristic...')
chara_uuid = TEST_CHARA_UUID.replace('-', '').upper()
property = READ | WRITE
cmd = f'PC,{chara_uuid},{property:02X},{TEST_CHARA_DATA_SIZE:02X}'
send_cmd(cmd, end_str='CMD>')

print('Get handle ID...')
handle_info_str = send_cmd('LS', end_str='CMD>')
handle_info_str = handle_info_str.replace('CMD>', '').replace('END', '')
handle_info_str = handle_info_str.strip()
handle_info_arr = handle_info_str.split()
now_service_uuid = ''
for i in handle_info_arr:
    if ',' not in i:
        now_service_uuid = i
        handle_info[i] = {}
    else:
        info = i.split(',')
        handle_info[now_service_uuid][info[0]] = info[1]

print('Set PIN...')
cmd = f'SP,{PIN}'
send_cmd(cmd, end_str='CMD>')

print('Set pairing mode...')
send_cmd('SA,4', end_str='CMD>')

print('Reboot...')
send_cmd('R,1', end_str='%REBOOT%')

# --------------------------------------------------

print('Enter CMD mode...')
send_cmd('$$$', end_str='CMD>')

print('Write value...')
handle = handle_info[SERVICE_UUID.replace(
    '-', '').upper()][TEST_CHARA_UUID.replace('-', '').upper()]
handle = int(handle, 16)
message = 'message from MCU'
message_hex = ''.join([f'{ord(i):02X}' for i in message])
cmd = f'SHW,{handle:04X},{message_hex}'
send_cmd(cmd, end_str='CMD>')

print('Check...')
while True:
    time.sleep(3)
    cmd = f'SHR,{handle:04X}'
    line = send_cmd(cmd, end_str='CMD>')
    line = line.replace('CMD>', '').strip()
    try:
        line_str = [chr(int(line[i:i+2], 16)) for i in range(0, len(line), 2)]
        line_str = ''.join(line_str)
    except:
        pass
    print('-----')
    print(line)
    print(line_str)


port.close()

5. 動作確認

5.1. MCUモックの実行

MCUモックを起動します。

mock_mcu
$ python mock_mcu.py
Enter CMD mode...
Check version info...
RN4870 V1.43 03/11/2022 (c)Microchip Technology Inc
Set device name...
Clear all service...
Set Service...
Set characteristic...
Get handle ID...
Set PIN...
Set pairing mode...
Reboot...
Enter CMD mode...
Write value...
Check...

5.2. BLEのペアリング

PCの設定 > Bluetoothとデバイス > デバイスの追加からBluetoothを選択。
TESTという名前のBLEがあるはずなので、それを選択しPIN123456で接続します。

5.3. アプリモックの実行

アプリモックを起動します。
実行ログからアプリモックから正常にBLEモジュールに対して読み書きができていることが確認できる。

mock_app
$ python mock_app.py
...
Find. TEST (address: FC:0F:E7:BF:DF:86)
...
Connected: True
bytearray(b'message from MCU') # <- 初期化でBLEモジュールに設定されていた値
Write.
bytearray(b'message from App') # <- アプリモックが書き込んだ値
mock_mcu
$ python mock_mcu.py
...
-----
6D6573736167652066726F6D204D4355
message from MCU
-----
6D6573736167652066726F6D204D4355 # <- アプリモックから書き込まれる前
message from MCU
-----
6D6573736167652066726F6D20417070 # <- アプリモックから書き込まれた後
message from App
-----

6. おわりに

BLEモジュールを用いたプロトタイピング方法について説明しました。本記事を基にしたプロトタイピングを通じ、BLE開発の基礎がつかめると幸いです。

免責事項

作者または著作権者は、本記事に起因または関連して生じる一切の請求、損害、その他の義務について何ら責任を負いません。

株式会社システムゼウス

Discussion