学習メモ

PiPicoW ＋ スマホのBluetooth（BLE）接続プログラムを作った際のメモ

-------------------------------------------------
### :beginner:１）キャラクタリスティック charactaristic　とは？

Bluetoothデバイスが持っている「データの最小単位」であり、1個のデータのこと。
例えば
・1つのセンサ値
・文字列データ
・ボタンのON/OFF状態
などがキャラクタリスティックにあたる。

##### BLEの構造（ざっくり図）
```md
温度センササービス（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つある。

１つのサービスに複数のキャラクタリスティックを持たすのであれば、
```python
((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:２）アドバタイズ Advertising　とは？

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

実際のコードでは、ここ
```python
self._ble.gap_advertise(100_000,adv_data=self._payload)
# これは「100msごとに広告を出す」という意味
```
-------------------------------------------------
### :beginner:３）pythonでのfloat型について

:loud_sound:　HEX表示で受信した温度センサー値を、float型に変換したい！

・PicoWからは　`0x85ebc141`　などのHEXデータが送られてくる。
・これは、`struct.pack("<f", 温度)` で送信された float（単精度浮動小数点） 型のデータ

・普通の16進数変換とちがい、IEEE 754 というルールでエンコードされている。
　　↓
・IEEE754 float は、32bit（4バイト）を使って：
```css
[符号 1bit][指数部 8bit][仮数部 23bit]
```
…という特殊な方法で値を表している。

Pythonで変換する場合は
```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進の並びはそのまま使えない。順番をひっくり返す必要がある。

例：
```css
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:４）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)
```
#### サービスとキャラクタリスティックの登録例

```python
((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`
```python
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`
```python
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: ５）pythonスクリプトの基本的な構成・順番

順番 | 内容 | 説明
|---|---|---|
① | インポート（import文） | 必要な標準ライブラリや外部モジュールの読み込み
② | クラスや関数の定義（def, class） | 機能のかたまりを先に作る
③ | 実際の実行コード（if name == "main":） | （あれば）メインルーチン開始

・Pythonは**「上から順に実行」**する言語
・関数やクラスは呼び出す前に必ず定義しておく必要がある
・C++みたいに「プロトタイプ宣言だけ先に書く」というのはない。定義そのものを先に書いておく

##### 構成の概要
・import machine
・class BLETemperature:</dd>
　　・def __init__(...) `#（初期化＋インスタンス変数宣言）`
　　・def send_combined(...)
　　・def _on_led() / def _off_led()
　　・def その他IRQハンドラなど…


-------------------------------------------------

### :beginner: ６）`for ~ in ~:`文について
Pythonの `for ~ in ~:` は
「リストや配列の要素をひとつずつ順番に取り出して使う」命令

```python
for fruit in ["apple", "banana", "cherry"]:
    print(fruit)
```
出力：
```nginx
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:７）`import` と `from import` の違い
書き方 | 意味
|---|---|
import module | モジュール全体を読み込んで、その名前で使う
from module import something | モジュールの中の特定のものだけを直接使えるようにする

具体例
```python
import math
print(math.sqrt(16))  # sqrtを使うには math. を付ける
```
```python
from math import sqrt
print(sqrt(16))  # 直接sqrt()を使える
```
つまり、
・`from machine import Pin` → Pico Wのハードウェア制御用のモジュール

・`import ssd1306`、`import time` → 標準的な汎用ライブラリや追加インストールしたライブラリ

-------------------------------------------------

### :beginner: ８）`"{:.0f}".format(temp_max)` の記号の意味

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

小数点以下の表示を変えたい場合は、
```python
"{:.1f}".format(25.345)  # → "25.3"
"{:.2f}".format(25.345)  # → "25.35"
"{:.0f}".format(25.345)  # → "25"
```

-------------------------------------------------

### :beginner: ９）python記述時の注意ポイント

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

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