iT邦幫忙

2025 iThome 鐵人賽

DAY 9
0
Software Development

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

Day 09 - 實作最簡單的 Characteristic - IDD Features

  • 分享至 

  • xImage
  •  

咱們已經將 BLE 相關函數都封裝好了,終於可以正式開始實作 IDS 了。
第一個目標當然是最簡單的 IDD Features,本喵會以 Insulin Delivery Service 1.0.2 為實作標準。

1. IDD Features 資料格式

先來看看 IDD Features 需要那些欄位:
https://ithelp.ithome.com.tw/upload/images/20250809/20177799lUJr3ZTLjL.png

先前在 config.py 的 Config 類別裡,咱們用 idd_features 做為臨時的 IDD Features 的回應,現在咱們用正式的 Insulin Concentration 和 Flags 來取代它:

class Config:
    def __init__(self):
        self.idd_features_insulin_conc = 100
        self.idd_features_flags = 0b_0000_0000_0000_0001_1110_0001
        self._refresh(self.idd_features_flags)

    def _refresh(self, flags):
        self.is_e2e_protection_supported = bool(flags & 0x0001)

    def to_dict(self) -> dict[str, str | int | tuple]:
        return {
            "idd_features_insulin_conc": self.idd_features_insulin_conc,
            "idd_features_flags": self.idd_features_flags,
        }

    @classmethod
    def from_dict(cls, d: dict[str, str | int | tuple]):
        obj.idd_features_insulin_conc = d["idd_features_insulin_conc"]
        obj.idd_features_flags = d["idd_features_flags"]
        obj._refresh(obj.idd_features_flags)

咱們使用 idd_features_insulin_conc 和 idd_features_flags 分別控制 Insulin Concentration 和 Flags,而 E2E-CRC 和 E2E-Counter 皆由系統來設定。

這裡須要特別提的是 _refresh()。Flags 是一個由 24 bit 組成的結構,在程式裡,咱們是用整數來表示。以下是一部分位元所表示的意義:
https://ithelp.ithome.com.tw/upload/images/20250809/20177799VGlR67zrjg.png

因咱們以整數來儲存此結構,所以若要得到特定欄位是否被設定,須以「邏輯與」來達成。如對應以上三個欄位可以這樣判別:

self.is_e2e_protection_supported = bool(flags & 0x0001)
self.is_basal_rate_supported = bool(flags & 0x0002)
self.is_tbr_absolute_supported = bool(flags & 0x0004)

當然不一定須要將其以成員變數來儲存,完全可以將每一項都以函數形式呈現,視看官喜好、需求而定。

但請注意,idd_features_insulin_conc 的型態是 float,而 Insulin Concentration 則是 SFLOAT,所以之後在回應 GATT Client 的讀取要求時,須要將其由 float 轉為 SFLOAT。

2. 讀取需求的共通性

在開始寫程式之前,咱們先來想想讀取的流程包含什麼:

次序 動作 特定 必要
1 取得模型的最新資料 Y Y
2 資料取得後,模型的後置處理 Y N
3 更新 characteristic 的值 N Y
4 以藍牙回應 N Y

特定:characteristic 是否有其特定的行為
必要:是否 characteristic 都需要

為什麼要特別考慮「資料取得後,模型的後置處理」呢?這其實是為了 E2E-Protection 而設,目前可先無視它。
然後咱們根據上表來設計一個支援讀取的類別:

_rbuf = bytearray(20)
_rbuf_mv = memoryview(_rbuf)

class ReadMixin:
    def on_read(self, value_handle: int):
        n = self._build_read_rsp(_rbuf_mv)
        self._after_build_tx_data()

        # 將回覆資料寫入 characteristic 裡
        ble.stack.gatts_write(value_handle, _rbuf_mv[:n])

    def _build_read_rsp(self, buf: memoryview) -> int:
        """返回資料長度"""
        raise NotImplementedError

    def _after_build_tx_data(self):
        pass

它的對應關係如下:

次序 動作 函數
1 取得模型的最新資料 _build_read_rsp()
2 資料取得後,模型的後置處理 _after_build_tx_data()
3 更新 characteristic 的值 gatts_write()
4 以藍牙回應 N/A

也許看官會疑惑,為什麼不在 ble.stack.Characteristic 裡新增這些函數,而要另外創建一個類別呢?這是因為並非每個 characteristic 都必須支援讀取屬性,若將讀取相關函數放在 Characteristic 類別裡,那是否其它屬性的相關函數也應放在同一類別呢?這樣的類別是否過於龐大?是否適合管理?

3. 建立 IddFeatures 類別

接著咱們將先前已建好的類別組合成 IddFeatures:

class IddFeatures(ble.mixin.ReadMixin, ble.stack.Characteristic):
    def __init__(self, config: config.Config):
        ble.stack.Characteristic.__init__(self, 0x2B23, read=True)

        self._config = config

    def _build_read_rsp(self, buf: memoryview) -> int:
        buf[0] = 0xFF  # Low byte of E2E-CRC
        buf[1] = 0xFF  # High byte of E2E-CRC
        buf[2] = 0x00  # E2E-Counter

        t = common.sfloat.float_to_sfloat(self._config.idd_features_insulin_conc)
        common.utils.write_uint16(buf, 3, t)

        common.utils.write_uint24(buf, 5, self._config.idd_features_flags)

        return 8

__init__(),咱們明確呼叫 ble.stack.Characteristic.__init__(),而不是 super().__init__(),這是因為 MicroPython 的多重繼承與 super() 機制並不完整,在標準 Python 可正常運行的情境,在 MicroPython 卻有問題,這些留待以後討論。

在 _build_read_rsp(),咱們根據 IDS 規格將資訊填入 buf 這個 memoryview 參數,然後回傳使用的長度,讓 ReadMixin.on_read() 可以將正確的資料片段傳給 ble.stack.gatts_write()。

因為 IddFeatures 還沒支援 E2E-Protection,所以不須覆寫 _after_build_tx_data()。

4. 重構 IdsServer

接下來讓 IdsServer 使用 IddFeatures 類別:

def _build_ids(self) -> ble.stack.Service:

    self._ids = ble.stack.Service(_IDS_UUID)

    features = ble.ids.features.IddFeatures(_config)
    self._ids.add_char(features)

    return self._ids

def _ble_isr(self, event, data):
    if event == _IRQ_GATTS_READ_REQUEST:
        conn_handle, value_handle = data

        for c in self._ids.chars:
            if value_handle == c.value_handle:
                c.on_read(value_handle)
                return

在 _build_ids(),features 直接使用 IddFeatures 類別建立實例,而不是使用基礎類別 Characteristic 來建構。

在 _ble_isr(),咱們由註冊的 characteristics 中找到第一個符合條件的 Characteristic 物件,然後呼叫其 on_read()。若這個物件沒有此方法,直接讓程式死掉,因為若真發生這情況,完全是實作此 Characteristic 的人的鍋,咱們寶貴的記憶體空間不應該浪費在這種防護上。

5. 執行

當完成以上修改後,就可以來驗證。一樣用 nRF Connect app 與咱們的 IDS 伺服器相連,然後讀取 IDD Features,它應該類似如下:
https://ithelp.ithome.com.tw/upload/images/20250809/20177799rTU2UuHXK9.jpg


在這一篇裡,本喵並沒有解釋 float 怎麼轉成 SFLAOT,欲知後事如何,且聽下回分解~


上一篇
Day 08 - 封裝 BLE 相關功能 (3)
下一篇
Day 10 - 藍牙低功耗環境下的浮點數 SFLOAT
系列文
以MicroPython在ESP32上實作Insulin Delivery Service31
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言