初めてのBLE(RN4870)。本格的な組込み開発に入る前にプロトタイピング。
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 |
---|---|---|---|
デバイスを選択するだけでペアリングが完了(確認画面が出ることはある) |
※画面はAndroid9のもの
今回はDisplayOnlyを設定します。
モジュールに設定するPINは固定値にします。PINを事前に知っている人だけがペアリングを行うことができます。
※プロトタイピング目的なのでここでは固定値にしていますが、商用システムではセキュリティ強度の高い仕組みを考える必要があります。ときにはBLEの仕組みに依らない上位レイヤでセキュリティを確保する必要もあるでしょう。
4. モックの作成
必要なライブラリをインストールします。
$ pip install pyserial==3.5b0 bleak==0.19.5
4.1. アプリモック
処理概要
-
TEST
という名前のBLEを検索、接続 - characteristic UUIDが
7b842730-a65c-457b-8b85-5dce1fa2ead1
のものに対して、値を読込->書込->読込の順に行う
スクリプト
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モック
処理概要
-
$$$\r
コマンド送信(これでRN4870がASCIIコマンドを受け付けるようになります) -
PZ\r
コマンド送信(登録していたサービスを一度すべて削除) -
PS,4fafc201-1fb5-459e-8fcc-c5c9c331914b\r
コマンド送信(サービス登録) -
PC
コマンドを用いてGATTテーブルで定義したものを登録 -
SP,123456\r
コマンド送信(ペアリングのPINを設定) -
SA,4\r
コマンド送信(ペアリング方式をDisplayOnlyに設定) -
R,1\n
コマンド送信(設定を反映されるためにモジュールの再起動) -
SHW
でcharacteristicに初期値(message from MCU
)を代入 - ループでcharacteristicの値を表示
スクリプト
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モックを起動します。
$ 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モジュールに対して読み書きができていることが確認できる。
$ 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') # <- アプリモックが書き込んだ値
$ python mock_mcu.py
...
-----
6D6573736167652066726F6D204D4355
message from MCU
-----
6D6573736167652066726F6D204D4355 # <- アプリモックから書き込まれる前
message from MCU
-----
6D6573736167652066726F6D20417070 # <- アプリモックから書き込まれた後
message from App
-----
6. おわりに
BLEモジュールを用いたプロトタイピング方法について説明しました。本記事を基にしたプロトタイピングを通じ、BLE開発の基礎がつかめると幸いです。
免責事項
作者または著作権者は、本記事に起因または関連して生じる一切の請求、損害、その他の義務について何ら責任を負いません。
Discussion