PiPicoW + スマホのBluetooth(BLE)接続プログラムを作った際のメモ
1)キャラクタリスティック charactaristic とは?
Bluetoothデバイスが持っている「データの最小単位」であり、1個のデータのこと。
例えば
・1つのセンサ値
・文字列データ
・ボタンのON/OFF状態
などがキャラクタリスティックにあたる。
BLEの構造(ざっくり図)
温度センササービス(Service UUID: xxxx `サービス`
├─ 温度(Characteristic UUID: abcdef01) `キャラクタリスティック1`
│ └─ Notify / Read
├─ タイムスタンプ(Characteristic UUID: abcdef02)`キャラクタリスティック2`
│ └─ Readのみ
└─ センサの状態(Characteristic UUID: abcdef03)`キャラクタリスティック3`
└─ Notifyのみ
ポイントまとめ
・サービスは「カテゴリの箱」
・キャラクタリスティックは「その箱の中のデータ」
・キャラクタリスティックは複数持てるし、それぞれに 通知(Notify)や読み取り(Read)などの性質を個別に持てる
実際のコード内での例
_TEMP_SERVICE_UUID = bluetooth.UUID("12345678-1234-5678-1234-56789abcdef0") # サービス
_TEMP_CHAR_UUID = bluetooth.UUID("abcdef01-1234-5678-1234-56789abcdef0") # キャラクタリスティック
上の構成では、
「温度センサのサービス」の中に、
「温度を表すキャラクタリスティック」が1つある。
1つのサービスに複数のキャラクタリスティックを持たすのであれば、
((self._temp_handle, self._time_handle),) = self._ble.gatts_register_services((
(_TEMP_SERVICE_UUID, (
(_TEMP_CHAR_UUID, _FLAG_READ | _FLAG_NOTIFY), # 温度
(_TIME_CHAR_UUID, _FLAG_READ), # タイムスタンプ
)),
))
用語 | 意味 | 例 |
---|---|---|
サービス | データのカテゴリ(温度、心拍数など) | 温度センサのサービス |
キャラクタリスティック | データの1項目(1つの値) | 温度の数値(例:25.3°C) |
Notify | 値の変化をリアルタイムで通知してくれる機能 | スマホ側で自動的に更新される値 |
2)アドバタイズ Advertising とは?
・Bluetoothデバイスが「私はここにいるよ!」と周囲に向かって定期的に送る、自己紹介のような信号
・周囲に向けて定期的に電波を「発信」しているのが アドバタイズ
・Pingのように1対1で応答を確認するのではなく、「CM」「看板」「ビーコン」…のように、接続前の“自己紹介放送”をしているイメージ
実際のコードでは、ここ
self._ble.gap_advertise(100_000,adv_data=self._payload)
# これは「100msごとに広告を出す」という意味
3)pythonでのfloat型について
HEX表示で受信した温度センサー値を、float型に変換したい!
・PicoWからは 0x85ebc141
などのHEXデータが送られてくる。
・これは、struct.pack("<f", 温度)
で送信された float(単精度浮動小数点) 型のデータ
・普通の16進数変換とちがい、IEEE 754 というルールでエンコードされている。
↓
・IEEE754 float は、32bit(4バイト)を使って:
[符号 1bit][指数部 8bit][仮数部 23bit]
…という特殊な方法で値を表している。
Pythonで変換する場合は
import struct
hex_val = 0x85ebc141
# 4バイトの値として、リトルエンディアンのfloatに変換
temp = struct.unpack('<f', hex_val.to_bytes(4, 'little'))[0]
print(temp)
エディアンとは?
Picoは struct.pack("<f", temp)
を使っていて、これは リトルエンディアン(下位バイトから先に並べる)。
なので、スマホに届いた16進の並びはそのまま使えない。順番をひっくり返す必要がある。
例:
0x85ebc141 → バイト列: [0x41, 0xc1, 0xeb, 0x85] → unpack("<f") → 24.24℃
Bit単位の MSB/LSB と Byte単位の MSB/LSB は、概念として非常に近い
1バイトの中でも、ビットの順序(bit0が先?bit7が先?)
複数バイトになると、バイトの並び順(最上位バイトが先か?)
両方とも、送信する側と受信する側が同じ順序で読み解く必要がある という点で共通している。
用語 | 意味 |
---|---|
LSB(Byte) | 最下位のバイト、値として一番小さい |
MSB(Byte) | 最上位のバイト、値として一番大きい |
リトルエンディアン | LSB → MSB(Picoがこれ) |
ビッグエンディアン | MSB → LSB(スマホ、表示系、一部CPU) |
4)UUIDを短くする
BLE公式が規格するUUIDを使用すると、短くできるしアドバタイズで確認した時もわかりやすい
用途 | UUID | 説明 |
---|---|---|
サービスUUID | 0x1809 | Health Thermometer Service(BLE公式規格) |
キャラクタリスティックUUID | 0x2A1C | Temperature Measurement(公式) |
UUIDの構造
Peripheral(Pico W)
└── Service: Health Thermometer (UUID: 0x1809)
├── Characteristic: 温度 (UUID: 0x2A1C)
└── Characteristic: タイムスタンプ (UUID: 0x2A08)
サービスとキャラクタリスティックの登録例
((self._temp_handle, self._time_handle),) = self._ble.gatts_register_services((
(_TEMP_SERVICE_UUID, (
(_TEMP_CHAR_UUID, _FLAG_READ | _FLAG_NOTIFY),
(_TIME_CHAR_UUID, _FLAG_READ | _FLAG_NOTIFY),
)),
))
例:温度と時刻を両方送信する
main.py
import time
from machine import ADC
from ble_temp_sensor import BLETemperature
import bluetooth
sensor_temp = ADC(4)
conversion_factor = 3.3 / 65535
def read_temp():
reading = sensor_temp.read_u16() * conversion_factor
temp_c = 27 - (reading - 0.706)/0.001721
return round(temp_c, 2)
ble = bluetooth.BLE()
temp_service = BLETemperature(ble)
def get_fake_timestamp():
now = time.localtime()
return "{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format(*now[:6])
while True:
temperature = read_temp()
timestamp = get_fake_timestamp()
print("Sending:", temperature, "@", timestamp)
temp_service.send_combined(temperature, timestamp)
time.sleep(2)
ble_temp_sensor.py
import struct
import bluetooth
from micropython import const
from ble_advertising import advertising_payload
# UUID(独自定義でもOKですが、今回は温度をベースにした統合データ)
_COMBINED_SERVICE_UUID = bluetooth.UUID(0x1809) # Health Thermometer
_COMBINED_CHAR_UUID = bluetooth.UUID(0x2A1C) # Temperature Measurement
_FLAG_READ = const(0x0002)
_FLAG_NOTIFY = const(0x0010)
class BLETemperature:
def __init__(self, ble, name="PicoTemp"):
self._ble = ble
self._ble.active(True)
self._ble.irq(self._irq)
self._connections = set()
# キャラ1個だけ登録
((self._combined_handle,),) = self._ble.gatts_register_services((
(_COMBINED_SERVICE_UUID, (
(_COMBINED_CHAR_UUID, _FLAG_READ | _FLAG_NOTIFY),
)),
))
self._payload = advertising_payload(name=name, services=[_COMBINED_SERVICE_UUID])
self._advertise()
def _irq(self, event, data):
if event == 1:
conn_handle, _, _ = data
print("Connected:", conn_handle)
self._connections.add(conn_handle)
elif event == 2:
conn_handle, _, _ = data
print("Disconnected:", conn_handle)
self._connections.discard(conn_handle)
self._advertise()
def _advertise(self):
self._ble.gap_advertise(100_000, adv_data=self._payload)
def send_combined(self, temperature_c, timestamp_str):
msg = "{:.1f}℃ @ {}".format(temperature_c, timestamp_str)
msg_bytes = msg.encode('utf-8')
for conn_handle in self._connections:
self._ble.gatts_notify(conn_handle, self._combined_handle, msg_bytes)
5)pythonスクリプトの基本的な構成・順番
順番 | 内容 | 説明 |
---|---|---|
① | インポート(import文) | 必要な標準ライブラリや外部モジュールの読み込み |
② | クラスや関数の定義(def, class) | 機能のかたまりを先に作る |
③ | 実際の実行コード(if name == "main":) | (あれば)メインルーチン開始 |
・Pythonは**「上から順に実行」**する言語
・関数やクラスは呼び出す前に必ず定義しておく必要がある
・C++みたいに「プロトタイプ宣言だけ先に書く」というのはない。定義そのものを先に書いておく
構成の概要
・import machine
・class BLETemperature:
・def init(...) #(初期化+インスタンス変数宣言)
・def send_combined(...)
・def _on_led() / def _off_led()
・def その他IRQハンドラなど…
6)for ~ in ~:
文について
Pythonの for ~ in ~:
は
「リストや配列の要素をひとつずつ順番に取り出して使う」命令
for fruit in ["apple", "banana", "cherry"]:
print(fruit)
出力:
apple
banana
cherry
ポイント:
・C言語みたいに for (i=0; i<length; i++)
みたいなインデックス管理は不要
・Pythonは「配列・リスト・文字列」に対して自然に「要素を順番に取り出す」という発想
・C言語では「0からスタートして、nまで1ずつ増加」って超定番だから、Pythonでは「それくらい暗黙的にやっといてあげるよ」というノリ
・range(5) なら 0,1,2,3,4
・range(1, 6) なら 1,2,3,4,5
みたいに、自然な回数分ループできる
7)import
と from import
の違い
書き方 | 意味 |
---|---|
import module | モジュール全体を読み込んで、その名前で使う |
from module import something | モジュールの中の特定のものだけを直接使えるようにする |
具体例
import math
print(math.sqrt(16)) # sqrtを使うには math. を付ける
from math import sqrt
print(sqrt(16)) # 直接sqrt()を使える
つまり、
・from machine import Pin
→ Pico Wのハードウェア制御用のモジュール
・import ssd1306
、import time
→ 標準的な汎用ライブラリや追加インストールしたライブラリ
8)"{:.0f}".format(temp_max)
の記号の意味
記号 | 意味 |
---|---|
{} | フォーマットしたい場所 |
: | フォーマット指定の開始 |
.0f | 小数点以下0桁で表示する(=整数表示にする) |
小数点以下の表示を変えたい場合は、
"{:.1f}".format(25.345) # → "25.3"
"{:.2f}".format(25.345) # → "25.35"
"{:.0f}".format(25.345) # → "25"
9)python記述時の注意ポイント
項目 | 内容 |
---|---|
if / for / while / def / class の後は | 必ず「:(コロン)」を付ける |
その次の行は | 必ずインデント(空白4つ推奨)する |
ブロックの終わり | インデントを戻すだけでOK(endや}は不要) |
例:
for i in range(5):
print(i) # ← インデント!
print("done") # ← forが終わったので左に戻る