National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 80 24°C PM2.5 21
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 17

藍牙低功耗(BLE)

BLE 的角色、服務與特徵值
Interaction導覽 25 minute(s)

Learning Objectives

  • 說明 BLE 適合的用途,以及它和 Wi-Fi 的差別
  • 分辨中央與周邊、GATT 伺服器與用戶端,說明廣播與連線的過程
  • 看懂服務、特徵值、UUID 與讀取、寫入、通知的差別
  • 把感測值編碼成位元組(例如溫度的 sint16 小端序),並讀懂 MicroPython 的 BLE 周邊程式

另一種無線:藍牙低功耗

第 12~14 課用 Wi-Fi 把 ESP32 接上網路。Wi-Fi 很方便,但要有無線網路分享器,而且耗電(第 15 課:發射時約 200 mA 等級)。 有些情況其實只需要:手機靠近時,直接和裝置交換一點點資料。例如心率帶把心跳傳給手機、手機設定一顆智慧燈泡、體重計把數字送到 App。

這就是藍牙低功耗(Bluetooth Low Energy,BLE)的用途。BLE 在 2010 年的藍牙核心規格 4.0 加入, 和傳統藍牙(耳機、喇叭用的那一種)名字相同,但是不同的通訊方式。ESP32 的資料手冊寫明它支援藍牙 4.2 的傳統藍牙與 BLE。

Wi-Fi+MQTT BLE
需要分享器、網際網路 要 不用,手機和裝置直接連
距離 整棟建築(透過網路可以到全世界) 通常是同一個房間
一次的資料量 大 小(MicroPython 的特徵值預設最多 20 位元組)
耗電 高 低,適合電池或鈕扣電池
常見用途 感測資料上雲端、遠端控制 穿戴裝置、設定新裝置、靠近時互動

這一課的觀念用下面的模擬器操作(模擬器沒有模擬 MicroPython 的 bluetooth 模組),實機程式放在課文後半。

廣播、連線與四種角色

先廣播,再連線

一個 BLE 裝置要被找到,先要廣播(advertising):每隔一段時間送出一個小封包,內容是「我是誰、我有什麼服務」。

  • 廣播使用三個專門的頻道,編號 37、38、39;每次廣播在三個頻道各送一份,提高被收到的機會。
  • 廣播間隔可以從 20 毫秒到 10.24 秒:越短越快被找到,也越耗電。
  • 傳統的廣播封包,資料部分最多 31 位元組;放不下的名稱或資訊要縮短,或放進「掃描回應」。

手機掃描時收聽這三個頻道,列出附近的裝置;選了一個按下連線,兩邊就建立連線,之後在其他頻道上交換資料。

四種角色(GAP)

角色 做什麼 例子
廣播者(broadcaster) 只廣播,不接受連線 博物館展品旁的信標
觀察者(observer) 只收聽廣播,不連線 收集信標訊號的閘道器
周邊(peripheral) 廣播,等別人來連線 ESP32 溫度計、心率帶
中央(central) 掃描並發起連線 手機、電腦

連線後:GATT 伺服器與用戶端

連線後交換資料的規則叫 GATT(Generic Attribute Profile)。保存資料的一方是 GATT 伺服器,來讀寫的一方是 GATT 用戶端。 常見的組合是「周邊=伺服器、中央=用戶端」:ESP32 有溫度,手機來讀。

伺服器的資料整理成一張屬性表,分成兩層:

服務(service)0x181A 環境感測
 ├─ 特徵值(characteristic)0x2A6E 溫度      性質:讀取、通知
 │    └─ 描述元(descriptor)0x2902 CCCD     用戶端在這裡開關通知
服務 0x180F 電池
 └─ 特徵值 0x2A19 電量                        性質:讀取
  • UUID:每個服務與特徵值都有一個編號。藍牙技術聯盟(Bluetooth SIG)制定的標準項目用 16 位元的短編號(例如 0x181A), 自己發明的服務用 128 位元的長編號,例如 9f3e0001-5b1c-4c8a-9e8b-2a6d7c0e1f00。用標準編號,任何認得它的 App 都知道 0x2A6E 是溫度。
  • 讀取(read):用戶端問,伺服器答。
  • 寫入(write):用戶端送一個值過去,例如開燈。
  • 通知(notify):用戶端先在 CCCD 寫入 01 00 訂閱,之後伺服器的值一改變就主動送過來,用戶端不必一直問; 指示(indicate)和通知類似,但用戶端要回覆確認。

這和第 14 課的 MQTT 很像:訂閱之後由對方主動送來,不用一直詢問。

模擬器:手機和 ESP32 的 BLE 對話

左邊是 ESP32(周邊、GATT 伺服器),右邊是手機(中央、GATT 用戶端),下方是空中來回的封包。 依序完成任務。把滑鼠移到廣播封包的位元組上,可以看到每一個位元組代表什麼。

特徵值裡裝的是位元組

MQTT 的訊息常用 JSON 文字;BLE 的特徵值很小(MicroPython 預設 20 位元組),通常直接放二進位的數字。 標準的溫度特徵值(0x2A6E)規定:有號 16 位元整數(sint16)、單位 0.01 °C、小端序(低位元組在前)。

25.84 °C → × 100 → 2584 → 十六進位 0x0A18 → 小端序送出:18 0A
收到 18 0A → 組回 0x0A18 → 2584 → × 0.01 → 25.84 °C

Python 用 struct 模組做這件事(MicroPython 也有):

import struct
data = struct.pack("<h", 2584)       # "<" 小端序,"h" 有號 16 位元
print(data.hex())                     # 180a
value = struct.unpack("<h", data)[0]  # 2584
print(value / 100)                    # 25.84

用整數乘上 100,而不是傳浮點數,資料只要 2 個位元組,接收端也不會遇到浮點數格式不同的問題。

小測驗:角色與 GATT

1.

心率帶廣播自己的名稱,手機掃描後連上它,持續收到心跳數字。這裡手機是?

2.

手機想在 ESP32 溫度一變化就收到新值,又不想每秒讀一次,該怎麼做?

3.

自己設計一個「門鎖」服務,App 要能寫入開鎖指令。這個服務的 UUID 應該怎麼選?

第 1 次作答

ESP32 上的 BLE 周邊程式

MicroPython 的 bluetooth 模組是低階介面:事件(有人連線、離線、寫入)透過一個你登記的函式通知程式。 下面的程式改寫自 MicroPython 官方範例 examples/bluetooth/ble_temperature.py,它需要同一個資料夾的 ble_advertising.py (負責組出廣播封包,就是模擬器裡那串位元組)。

import bluetooth
import struct
import time
from micropython import const
from ble_advertising import advertising_payload

_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_FLAG_READ = const(0x0002)
_FLAG_NOTIFY = const(0x0010)

ENV_UUID = bluetooth.UUID(0x181A)                                  # 環境感測服務
TEMP_CHAR = (bluetooth.UUID(0x2A6E), _FLAG_READ | _FLAG_NOTIFY)    # 溫度:可讀取、可通知

ble = bluetooth.BLE()
ble.active(True)
((temp_handle,),) = ble.gatts_register_services(((ENV_UUID, (TEMP_CHAR,)),))
connections = set()
payload = advertising_payload(name="Uedu-Temp", services=[ENV_UUID])

def on_ble(event, data):
    if event == _IRQ_CENTRAL_CONNECT:
        conn, _, _ = data
        connections.add(conn)
        print("手機連上了")
    elif event == _IRQ_CENTRAL_DISCONNECT:
        conn, _, _ = data
        connections.discard(conn)
        print("手機離開了,重新廣播")
        ble.gap_advertise(500000, adv_data=payload)

ble.irq(on_ble)
ble.gap_advertise(500000, adv_data=payload)      # 每 0.5 秒(500000 微秒)廣播一次
print("廣播中:Uedu-Temp")

temp = 25.0
while True:
    data = struct.pack("<h", int(temp * 100))    # sint16、小端序、單位 0.01 °C
    ble.gatts_write(temp_handle, data)           # 更新屬性表裡的值:手機讀取時拿到它
    for conn in connections:
        ble.gatts_notify(conn, temp_handle)      # 主動送給已連線的手機
    temp += 0.1
    time.sleep(1)

幾個要注意的地方:

  • gatts_register_services 依你給的服務定義建立屬性表,回傳每個特徵值的 handle(屬性表裡的編號),之後讀寫都用它。
  • 旗標常數(_FLAG_READ 等)沒有內建在模組裡,要像上面一樣自己定義。
  • gatts_notify 不管手機有沒有訂閱都會送出;正式的程式應該記錄手機寫入 CCCD 的值,有訂閱才送。
  • MicroPython 文件建議一般應用改用較高階的 aioble 函式庫,它幫你處理事件與連線管理,但需要 asyncio。
  • ESP32 的 Wi-Fi 與藍牙共用同一組 2.4 GHz 無線電,靠分時輪流使用;可以同時開著,但同時大量傳輸時兩邊都會變慢。

實機操作:讓手機找到你的 ESP32

這部分要用真的 ESP32 和手機操作(模擬器不能執行 bluetooth 模組)。檔案用第 16 課的 mpremote 上傳;輸出是範例。

  1. 1

    上傳廣播輔助模組

    從 MicroPython 原始碼的 examples/bluetooth/ 資料夾下載 ble_advertising.py,複製到板子上。

  2. 2

    執行周邊程式

    把上面的程式存成 ble_temp.py,用 run 直接執行(不會存進板子)。程式是無窮迴圈,要停止時按 Ctrl+C。

  3. 3

    用手機掃描與連線

    手機安裝一個通用的 BLE 除錯 App(例如 nRF Connect 或 LightBlue),開啟藍牙後掃描: 清單裡應該看得到 Uedu-Temp。按下連線,App 會列出 Environmental Sensing(0x181A)服務與 Temperature(0x2A6E)特徵值。 這時電腦上的終端機會印出「手機連上了」。

  4. 4

    讀取與訂閱

    按特徵值旁的「讀取」,App 顯示的原始值像 0A 0A,有些 App 會直接換算成 25.70 °C。 按下訂閱(通知)圖示,之後每秒都會收到新的溫度。斷開連線時終端機印出「手機離開了,重新廣播」,手機又能再掃描到它。

這段導覽完成了

小測驗:位元組與廣播封包

1.

下面的程式會印出什麼?

import struct
print(struct.pack("<h", 2584).hex())
2.

手機讀到溫度特徵值的兩個位元組是 F6 FF。下面的程式會印出什麼?

import struct
raw = struct.unpack("<h", bytes([0xF6, 0xFF]))[0]
print(raw / 100)
3.

廣播封包由 Flags(3 位元組)、名稱(2 + 名稱的位元組數)、服務 UUID 清單(4 位元組)組成。下面的程式算出名稱「教室溫度計」的封包長度,會印出什麼?

name = "教室溫度計"
total = 3 + (2 + len(name.encode("utf-8"))) + 4
print(total, total <= 31)
第 1 次作答

重點整理

  • BLE 讓手機和裝置不經過網路、直接交換少量資料,耗電低、距離短;ESP32 同時支援 Wi-Fi 與 BLE。
  • 周邊在 37、38、39 頻道廣播(資料最多 31 位元組),中央掃描後發起連線;只廣播不連線的是廣播者。
  • 連線後,保存資料的 GATT 伺服器提供屬性表:服務底下有特徵值,每個都有 UUID(標準 16 位元、自訂 128 位元)。
  • 用戶端可以讀取、寫入,或在 CCCD 寫入 01 00 訂閱通知,之後伺服器主動送新值。
  • 特徵值放位元組:標準溫度是 sint16、小端序、0.01 °C,用 struct.pack("<h", ...) 編碼。
  • MicroPython 的 bluetooth 模組是低階、事件驅動的介面;一般應用可以改用 aioble。

下一課是 ESP32 軌的專題:把 Wi-Fi、感測、MQTT 整合成一個完整的感測節點。

這一課完成了!
Next lesson:專題:Wi-Fi 感測節點
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。