0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

学習メモ

Last updated at Posted at 2025-04-27

PiPicoW + スマホのBluetooth(BLE)接続プログラムを作った際のメモ


:beginner: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 値の変化をリアルタイムで通知してくれる機能 スマホ側で自動的に更新される値

:beginner:2)アドバタイズ Advertising とは?

・Bluetoothデバイスが「私はここにいるよ!」と周囲に向かって定期的に送る、自己紹介のような信号
・周囲に向けて定期的に電波を「発信」しているのが アドバタイズ
・Pingのように1対1で応答を確認するのではなく、「CM」「看板」「ビーコン」…のように、接続前の“自己紹介放送”をしているイメージ

実際のコードでは、ここ

self._ble.gap_advertise(100_000,adv_data=self._payload)
# これは「100msごとに広告を出す」という意味

:beginner:3)pythonでのfloat型について

:loud_sound: 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)
:speaker:エディアンとは?

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)

:beginner: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)

:beginner: 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ハンドラなど…


:beginner: 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
みたいに、自然な回数分ループできる


:beginner:7)importfrom 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 ssd1306import time → 標準的な汎用ライブラリや追加インストールしたライブラリ


:beginner: 8)"{:.0f}".format(temp_max) の記号の意味

記号 意味
{} フォーマットしたい場所
: フォーマット指定の開始
.0f 小数点以下0桁で表示する(=整数表示にする)

小数点以下の表示を変えたい場合は、

"{:.1f}".format(25.345)  # → "25.3"
"{:.2f}".format(25.345)  # → "25.35"
"{:.0f}".format(25.345)  # → "25"

:beginner: 9)python記述時の注意ポイント

項目 内容
if / for / while / def / class の後は 必ず「:(コロン)」を付ける
その次の行は 必ずインデント(空白4つ推奨)する
ブロックの終わり インデントを戻すだけでOK(endや}は不要)

例:

for i in range(5):
    print(i)  # ← インデント!
print("done")  # ← forが終わったので左に戻る

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?