iT邦幫忙

2025 iThome 鐵人賽

DAY 7
0
Software Development

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

Day 07 - 封裝 BLE 相關功能 (2) 之翻車現場

  • 分享至 

  • xImage
  •  

今天繼續封裝咱們的 BLE。

2. ble/server.py

此模組的核心是類別 IdsServer,負責設定、創建、管理 characteristics 和所需資源,如設定配對安全性、characteristic 的種類與權限、廣播的資訊、以及 BLE 的中斷處理等。

如何設計 IdsServer 的 BLE ISR 是此次的重點,因為 ISR 裡不能配置記憶體,所以在 ISR 裡呼叫 micropython.schedule() 時,第一個參數不可以是綁定方法。咱們在此探討 2 種做法:

  • MicroPython 的建議:綁定方法的參考
  • 模組內部函數

並用以下程式來觀察從「匯入模組」到「建立 IdsServer 物件」的記憶體資訊:

async def main():
    gc.collect()
    s = gc.mem_free()

    import ble.server
    server = ble.server.IdsServer()

    gc.collect()
    e = gc.mem_free()

    import micropython
    micropython.mem_info()

    print(f"\nMemory used: {s - e}")

有一點要特別注意,此記憶體資訊會因各種因素而異,即使同一裝置、同一程式,也不能保證每次都能測得相同的數值,所以只能做為一種相對參考。

2-1. 綁定方法的參考

class IdsServer(ble.stack.Server):
    def __init__(self):
        self._connect_ref = self._connect
        self._disconnect_ref = self._disconnect
        self._read_ref = self._read
        ble.stack.register_irq_handler(self._ble_isr)

        self.addr = None
        self.addr_type = None
        self.value_handle = 0
        self.rsp = bytes((0x01, 0x02, 0x03, 0x04))

    def _ble_isr(self, event, data):
        if event == _IRQ_CENTRAL_CONNECT:
            conn_handle, self.addr_type, addr = data
            self.addr = bytes(addr)
            micropython.schedule(self._connect_ref, self)

        elif event == _IRQ_CENTRAL_DISCONNECT:
            conn_handle, self.addr_type, addr = data
            self.addr = bytes(addr)
            micropython.schedule(self._disconnect_ref, self)

        elif event == _IRQ_GATTS_READ_REQUEST:
            conn_handle, self.value_handle = data
            micropython.schedule(self._read_ref, self)

    def _connect(self, server):
        print(f"Connected to {server.addr} {server.addr_type}")

    def _disconnect(self, server):
        print(f"Disconnected from {server.addr} {server.addr_type}")

    def _read(self, server):
        ble.stack.gatts_write(server.value_handle, server.rsp)

記憶體資訊:

stack: 1616 out of 15360
GC: total: 56000, used: 10928, free: 45072, max new split: 110592
No. of 1-blocks: 144, 2-blocks: 46, max blk sz: 37, max free sz: 2043
Memory used: 7104

2-2. 模組內部函數

class IdsServer(ble.stack.Server):
    def __init__(self):
        ble.stack.register_irq_handler(self._ble_isr)

        self.addr = None
        self.addr_type = None
        self.value_handle = 0
        self.rsp = bytes((0x01, 0x02, 0x03, 0x04))

    def _ble_isr(self, event, data):
        if event == _IRQ_CENTRAL_CONNECT:
            conn_handle, self.addr_type, addr = data
            self.addr = bytes(addr)
            micropython.schedule(_connect, self)

        elif event == _IRQ_CENTRAL_DISCONNECT:
            conn_handle, self.addr_type, addr = data
            self.addr = bytes(addr)
            micropython.schedule(_disconnect, self)

        elif event == _IRQ_GATTS_READ_REQUEST:
            conn_handle, self.value_handle = data
            micropython.schedule(_read, self)

def _connect(server):
    print(f"Connected to {server.addr} {server.addr_type}")

def _disconnect(server):
    print(f"Disconnected from {server.addr} {server.addr_type}")

def _read(server):
    ble.stack.gatts_write(server.value_handle, server.rsp)

記憶體資訊:

stack: 1616 out of 15360
GC: total: 56000, used: 10736, free: 45264, max new split: 110592
No. of 1-blocks: 142, 2-blocks: 49, max blk sz: 37, max free sz: 2046
Memory used: 6912

2-3. 討論

在此例中,「模組內部函數」似乎使用更少記憶體,但這樣的比較是否公平?此例是否可能有高估「綁定方法的參考」的記憶體使用量之嫌呢?

在「綁定方法的參考」裡,是傳 self 給 micropython.schedule(fn, arg) 的第二個參數。但既然最後實際執行的是綁定方法,它本來就可以得到 self,那 arg 不就浪費了?若將 fn 要處理的參數藉由 arg 傳入,是否可以更好地例用記憶體?

本喵並不知道答案是什麼,因為本喵有嘗試這樣做,得到的記憶體使用量反而更高:

stack: 1616 out of 15360
GC: total: 56000, used: 10992, free: 45008, max new split: 110592
No. of 1-blocks: 144, 2-blocks: 46, max blk sz: 37, max free sz: 2042
Memory used: 7168

是否本喵的寫法有誤?測試手段錯誤?或這樣的數值是否真的表示不好?本喵並不清楚,或許各位看官可以幫忙解惑。

2-4. IdsServer 實例

IdsServer 在設計上不是單例模式,亦即沒有任何標準單例模式的保護,但用法上是單例。它只是簡單地在 ble/server.py 裡定義一個實例:

class IdsServer(ble.stack.Server):
    def __init__(self):
        pass

instance = IdsServer()

然後由 main.py 呼叫其 register_services() 和 run(),最後在 server.py 裡被模組內部函數存取:

async def main():
    server = ble.server.instance
    server.register_services()

    # 主執行緒會一直等下去
    await server.run()

if __name__ == "__main__":
    asyncio.run(main())

這是不好的設計,全賴程式設計師的自我修養來維護,但卻可以省下約 176 bytes 的記憶體。在記憶體小、且極易產生記憶體碎片的環境下,若要完成一開始規劃的功能,本喵只好想方設法摳下去。
(之後大概會再多次精簡不少設計,不然以目前的設計,很大機會會遇到各種記憶體碎片、不足產生的問題)

3. 執行

接下來與 nRF Connect app 建立連線並讀取 IDD Features:

  1. 讀取 IDD Features:沒有收到資料?!
  2. 讀取 IDD Features:收到資料了
  3. 讀取 IDD Features:收到資料了

咦?!本喵恍神了嗎?!咱的小肉球沒點到?
重新執行 IDS 伺服器,建立連線:

  1. 讀取 IDD Features:沒有收到資料?!
  2. 讀取 IDD Features:收到資料了
  3. 讀取 IDD Features:收到資料了

為什麼啟動 IDS 伺服器後的第一次讀取會沒有資料回應?但是 Thonny 的 Shell 窗格上確實印出伺服器已經處理了讀取要求啊 Σ(;゚д゚)

本喵受到 -999 的暴擊!昏迷瀕死中 (´ཀ`」 ∠)


原始碼


上一篇
Day 06 - 封裝 BLE 相關功能 (1)
下一篇
Day 08 - 封裝 BLE 相關功能 (3)
系列文
以MicroPython在ESP32上實作Insulin Delivery Service31
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言