目前 IDS 伺服器在藍牙的使用上分為三個部分:
目前這些全都寫在 main.py 裡,當程式規模還小時,不會有太大問題。但當往後需求愈來愈多,若全累積在一個檔案裡實在不好查閱,所以本喵將它們拆分開來管理:
因 bluetooth.BLE 必須先啟用後才能呼叫它的其它方法,所以將此前置作業放在此函數內:
def init():
ble = bluetooth.BLE()
ble.irq(_ble_isr)
ble.active(True)
這裡讓 _ble_isr() 負責處理 BLE 中斷事件,它會先行處理完中斷事件後,才將事件分發給上層各自的 ISR。
因 bluetooth.BLE.irq() 只能指定一個函數來處理 BLE 中斷,而在 init() 裡已經指定 _ble_isr() 為處理 BLE 中斷的 ISR,這樣若有人再以 bluetooth.BLE.irq() 指定新的 ISR,會導致 stack.py 模組無法接收到 BLE 中斷。為了解決這問題,本喵提供 register_irq_handler(),讓其它想監聽 BLE 中斷的模組可以註冊自己的 BLE ISR。當 _ble_isr() 處理完相關事件後,就會讓註冊的 ISR 依序執行:
_irq_handlers = []
def register_irq_handler(handler):
_irq_handlers.append(handler)
def _ble_isr(event, data):
ret = None
# 處理訊息後,分發 BLE IRQ 給所有訂閱的函式
for h in _irq_handlers:
r = h(event, data)
if r is not None:
ret = r
return ret
將設定配對相關的安全層級放在一個函數內,並且強制使用命名參數:
def set_pairing_mode(*, bond: bool, mitm: bool, le_secure: bool):
ble = bluetooth.BLE()
ble.config(bond=bond)
ble.config(mitm=mitm)
ble.config(le_secure=le_secure)
這樣呼叫時就不可以如此使用:
set_pairing_mode(True, False, True)
而必須以參數名指定:
set_pairing_mode(bond=True, mitm=False, le_secure=True)
雖然略顯蠻橫,但能讓查閱程式的人更明確了解呼叫者的意圖。
此類別是為方便註冊服務時,將其格式轉為 bluetooth.BLE.gatts_register_services() 所要求的格式。因為 IDS 伺服器沒有使用除 CCCD(Client Characteristic Configuration Descriptor)以外的 Descriptor,所以沒有建立 Descriptor 類別。
class Characteristic:
def __init__(
self,
uuid: int | str,
*,
read=False,
read_enc=False,
read_authen=False,
read_author=False,
write=False,
write_enc=False,
write_authen=False,
write_author=False,
write_no_rsp=False,
notify=False,
indicate=False,
):
self.uuid = bluetooth.UUID(uuid)
self.char_flags = _build_props(
read=read,
read_enc=read_enc,
read_authen=read_authen,
read_author=read_author,
write=write,
write_enc=write_enc,
write_authen=write_authen,
write_author=write_author,
write_no_rsp=write_no_rsp,
notify=notify,
indicate=indicate,
)
self.value_handle = 0
def _to_tuple(self) -> tuple[bluetooth.UUID, int]:
return (self.uuid, self.char_flags)
出於與 Characteristic 類別相同的理由而設。
class Service:
def __init__(self, uuid: int | str):
self.uuid = bluetooth.UUID(uuid)
self.chars: list[Characteristic] = []
def add_char(self, char: Characteristic):
self.chars.append(char)
def _to_tuple(
self,
) -> tuple[bluetooth.UUID, tuple[tuple[bluetooth.UUID, int], ...]]:
return (self.uuid, tuple(c._to_tuple() for c in self.chars))
由 Server 類別負責註冊 BLE 服務,但實際建立 services 和相關 characteristics 的則是由子類別負責,因為只有子類別才知道自己需要什麼服務。當註冊完成,會將得到的 characteristic value handles 設定到相應的 Characteristic 物件。
class Server:
def _build_services(self) -> tuple[Service, ...]:
"""由子類別負責建立所需的 Service 物件"""
return tuple()
def register_services(self):
# 儲存所有的 Service 物件
self.srvs = self._build_services()
handles = _register_services(tuple(s._to_tuple() for s in self.srvs))
for i, s in enumerate(self.srvs):
char_handles = handles[i]
for j, char in enumerate(s.chars):
h = char_handles[j]
char.value_handle = h
最初的想法是希望儘量將 MicroPython 的 bluetooth 模組都封裝到 stack.py,讓其它模組能不直接相依於 MicroPython 的 bluetooth 模組,方便管理、觀看程式碼為由才拆分,因為本喵的腦容量很小,只能看少少小小的檔案。
但這樣的設計產生了不小的問題──浪費記憶體,因為有些物件只在註冊服務時用到,之後完全不會使用,這樣無形中就浪費了記憶體。如 Characteristic.uuid、Characteristic.char_flags 和 Service.uuid,甚至 Service.chars 此 list 在註冊後也未必是必要的存在。當然,也可以在註冊完服務後將其刪除,雖然這樣的做法讓本喵疑惑 Characteristic 等類別有必要存在嗎?沒有更好的做法嗎?所以若之後遇到記憶體不足的情況,需要重新考慮方法解決,甚至可能要放棄一些封裝原則來重新設計。
stack.py 裡還有一些前面未說明的部分,但因大部分都只是簡單的封裝,請直接參閱原始碼。
未完待續
Σ( ° △ °)