iT邦幫忙

2025 iThome 鐵人賽

DAY 16
0
Software Development

以MicroPython在ESP32上實作Insulin Delivery Service系列 第 16

Day 16 - CCCD Monitor - 為 MicroPython 藍牙新增功能吧!

  • 分享至 

  • xImage
  •  

在先前 IDD Status Changed 的說明和測試中,一直沒有談到對於 Indicate 和 Notify 來說很重要的一件事,那就是 CCCD(Client Characteristic Configuration Descriptor)。所以在繼續開發 IDS 前,咱們先釐清 CCCD 是做什麼用?目前的 IDS server 又有什麼問題吧!

1. CCCD 的作用

藍牙規格書規定,GATT Client 在接收某個 characteristic 的 Indicate 或 Notify 前,必須先正確設定此 characteristic 的 CCCD:

  • Notify:0x0001
  • Indicate:0x0002
  • 禁止 Notify 和 Indicate:0x0000

2. bluetooth.BLE 的問題

在昨天咱們用 nRF Connect app 與 IDS server 進行測試時,遇到一些奇怪的現象,整個測試步驟如下:

  1. App 與 server 建立連線
  2. App 收到 IDD Status Changed 的 Indicate(app 先前並沒有存取過 IDD Status Changed 的 CCCD)
  3. App 讀取 IDD Status Changed 的 CCCD,app 顯示 CCCD 目前值為 0x0000
  4. App 再也收不到 IDD Status Changed 的 Indicate
  5. App 將 0x0002 寫入 IDD Status Changed 的 CCCD
  6. App 可以收到 IDD Status Changed 的 Indicate 了
  7. 中斷連線後,CCCD 被強制重設為 0x0000(無論斷線前的 CCCD 是什麼,無論是否有配對綁定)
  8. 再次連線,server 行為回到步驟 2 的狀況

問題:

  1. 步驟 2,在 GATT Client 正確設定一 characteristic 的 CCCD 前,GATT Client 不應該收到此 characteristic 的 Indicate/Notify
  2. 步驟 3,GATT Client 只是讀取 CCCD,卻導致 GATT Server 的行為改變
  3. 步驟 7,若沒有配對,CCCD 應在斷線後,回復為 0x0000;若已配對,應在再次連線後,回復其值。但無論有無配對,只要斷線,都會回復為 0x0000

這行為和藍牙規格書並不一致,但這似乎是 MicroPython 在 ESP32 上使用的藍牙堆疊 NimBLE 的設計。

另一個問題是,若希望能在 app 寫入 CCCD 時收到相關通知,那目前最新的 MicroPython 1.26.0 還不支援。這個問題比前述問題還嚴重,因為若希望 GATT Server 只有在 CCCD 被正確設定時才送出 Indicate/Notify,那 GATT Server 勢必須要知道 CCCD 何時被設定。對於 IDS 來說,這功能是必須的,否則 E2E-Counter 無法被正確遞增。因為即使 GATT Client 沒有正確設定 CCCD,呼叫 MicroPython 的 bluetooth.BLE.gatts_indicate()bluetooth.BLE.gatts_notify() 也不會有失敗通知。

所以為了至少能收到 CCCD 的寫入通知,咱們會修改 MicroPython,重新建置 ESP32 的 firmware。

3. 設定編譯環境

以下指令都是在 Ubuntu 所執行。

3-1. 更新已安裝軟體

sudo apt update

3-2. 安裝編譯所需工具

sudo apt-get install git wget flex bison gperf python3 python3-pip python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0

3-3. 下載 MicroPython

git clone https://github.com/micropython/micropython.git

3-4. 下載 ESP-IDF

micropython/ports/esp32/README.md 可以查到 MicroPython 支援的 ESP-IDF 版本。如本喵所下載的 MicroPython 支援到 ESP-IDF v5.4.2:

git clone -b v5.4.2 --recursive https://github.com/espressif/esp-idf.git

善意提醒,在使用 ESP-IDF v5.4.1 時發現,藍牙斷線中斷有時會收不到,所以若使用上有什麼問題,建議可以使用 ESP-IDF v5.4。

3-5. 進入到 ESP-IDF 資料夾

cd esp-idf

3-6. 為 ESP32 平台安裝編譯器

只須執行一次,除非要安裝其他平台

./install.sh esp32

3-7. 設定環境變數

每個 shell 啟用後執行一次

source export.sh

3-8. 編譯 MicroPython Cross-compiler

只須執行一次,除非有修改過此工具

cd ~/micropython/
make -C mpy-cross

4. 修改原始碼

4-1. 新增 MP_BLUETOOTH_IRQ_SUBSCRIBE

檔案:micropython/extmod/modbluetooth.h
找到字串 MP_BLUETOOTH_IRQ_PASSKEY_ACTION,在其下方新增:

#define MP_BLUETOOTH_IRQ_SUBSCRIBE (32)

這是 CCCD 被寫入的事件代碼,當然,完全可以使用不重複的任意數。

4-2. 新增 mp_bluetooth_gap_on_subscribe

檔案:micropython/extmod/modbluetooth.h
找到字串 mp_bluetooth_gatts_on_write,在其上方新增:

void mp_bluetooth_gap_on_subscribe(uint16_t conn_handle, uint16_t value_handle, uint8_t reason, uint8_t cur_notify, uint8_t cur_indicate);

這是要傳給 MicroPython 程式的 CCCD 寫入事件的內容。

檔案:micropython/extmod/modbluetooth.c
找到字串 mp_bluetooth_gap_on_passkey_action,在其下方,與 mp_bluetooth_gatts_on_write 字串上方間新增:

void mp_bluetooth_gap_on_subscribe(uint16_t conn_handle, uint16_t value_handle, uint8_t reason, uint8_t cur_notify, uint8_t cur_indicate) {
    mp_int_t args[] = { conn_handle, value_handle, reason, cur_notify, cur_indicate };
    invoke_irq_handler(MP_BLUETOOTH_IRQ_SUBSCRIBE, args, 5, 0, NULL_ADDR, NULL_UUID, NULL_DATA, NULL_DATA_LEN, 0);
}

如以下位置:
https://ithelp.ithome.com.tw/upload/images/20250816/20177799WlwvPWtLgx.png

看官應該會疑惑,為什麼要用那麼繞口的說法,直接說在 mp_bluetooth_gatts_on_write 上方新增不就好了嗎?其實這不是故意要讓人迷惑,它的原因和「為什麼 ESP32 平台上,在中斷函數裡配置記憶體不會有任何錯誤訊息」可能有關。

modbluetooth.c 裡,其實有 2 個 mp_bluetooth_gatts_on_write() 的實作,除了先前展示的部分外,還有一個如下:
https://ithelp.ithome.com.tw/upload/images/20250816/20177799QGwbunLY2T.png

注意到函數上方的註解了嗎?

Callbacks are called in interrupt context (i.e. can't allocate)

因為 ESP32 有啟用 MICROPY_PY_BLUETOOTH_USE_SYNC_EVENTS,所以必須把 mp_bluetooth_gap_on_subscribe() 放在對的位置。

而這也可能是為什麼在 ESP32 上,即使於中斷函數裡配置記憶體,也不會有異常的原因。但目前咱們還沒有 100% 把握,所以還是暫時儘量照著文件來做。雖然有句俗語:

程式的註解(文件),騙人的鬼 Σ(*゚д゚ノ)ノ(大誤)~

4-3. 修改 central_gap_event_cb

檔案:micropython/extmod/nimble/modbluetooth_nimble.c
找到字串 case BLE_GAP_EVENT_SUBSCRIBE,在其下方新增:

mp_bluetooth_gap_on_subscribe(event->subscribe.conn_handle, event->subscribe.attr_handle, event->subscribe.reason, event->subscribe.cur_notify, event->subscribe.cur_indicate);

5. 編譯

5-1. 進入到 micropython/ports/esp32

cd ports/esp32

5-2. 編譯 submodules

只須執行一次

make submodules

5-3. 編譯

make

編譯完成後,以 Thonny 將 micropython/ports/esp32/build-ESP32_GENERIC/firmware.bin 燒錄到板子上,重啟裝置後即可使用新 firmware:
https://ithelp.ithome.com.tw/upload/images/20250816/20177799iNZpkdZPqr.png

如本喵目前編譯的版本為 MicroPython v1.27.0-preview,看官當然可以在下載 MicroPython 時,指定 1.26.0 或其他想要的版本。


6. 測試

雖然是想一氣呵成,但 ... 內容好像有點太多了 ...
還是明天再繼續好了 ∑( ̄□ ̄;)


上一篇
Day 15 - IDD Status Changed (3) 之 上 Indicate Buff
下一篇
Day 17 - 處理 CCCD 事件
系列文
以MicroPython在ESP32上實作Insulin Delivery Service31
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言