今天繼續封裝咱們的 BLE。
此模組的核心是類別 IdsServer,負責設定、創建、管理 characteristics 和所需資源,如設定配對安全性、characteristic 的種類與權限、廣播的資訊、以及 BLE 的中斷處理等。
如何設計 IdsServer 的 BLE ISR 是此次的重點,因為 ISR 裡不能配置記憶體,所以在 ISR 裡呼叫 micropython.schedule() 時,第一個參數不可以是綁定方法。咱們在此探討 2 種做法:
並用以下程式來觀察從「匯入模組」到「建立 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}")
有一點要特別注意,此記憶體資訊會因各種因素而異,即使同一裝置、同一程式,也不能保證每次都能測得相同的數值,所以只能做為一種相對參考。
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
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
在此例中,「模組內部函數」似乎使用更少記憶體,但這樣的比較是否公平?此例是否可能有高估「綁定方法的參考」的記憶體使用量之嫌呢?
在「綁定方法的參考」裡,是傳 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
是否本喵的寫法有誤?測試手段錯誤?或這樣的數值是否真的表示不好?本喵並不清楚,或許各位看官可以幫忙解惑。
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 的記憶體。在記憶體小、且極易產生記憶體碎片的環境下,若要完成一開始規劃的功能,本喵只好想方設法摳下去。
(之後大概會再多次精簡不少設計,不然以目前的設計,很大機會會遇到各種記憶體碎片、不足產生的問題)
接下來與 nRF Connect app 建立連線並讀取 IDD Features:
咦?!本喵恍神了嗎?!咱的小肉球沒點到?
重新執行 IDS 伺服器,建立連線:
為什麼啟動 IDS 伺服器後的第一次讀取會沒有資料回應?但是 Thonny 的 Shell 窗格上確實印出伺服器已經處理了讀取要求啊 Σ(;゚д゚)
本喵受到 -999 的暴擊!昏迷瀕死中 (´ཀ`」 ∠)