昨天已經知道如何由 BTree
取出資料了,那麼就來完成最後一哩路吧!
基本上,IDD Record Access Control Point
和先前的 IDD Status Reader Control Point
與 IDD Command Control Point
幾乎一樣,所以若不支援 Abort
指令的話,可以直接照抄其他的 Control Point。不過因咱們會以支援 Abort
指令為前提,所以雖然還是繼承了 BaseCP
,但其邏輯實現會有些不一樣。
首先實現 IDD Record Access Control Point 的建構函數:
class IddRACP(BaseCP):
def __init__(self, history_mgr: HistoryManager):
super().__init__(0x2B27)
self._history_mgr = history_mgr
self._fn_on_opcode = self._on_opcode
# 指示是否在處理歷史資料
self._is_transmitting = False
# 指示是否收到 Abort 指令
self._is_aborting = False
# 指示是否先前的動作已停止
self._is_aborted = asyncio.Event()
然後咱們也必須覆寫 BaseCP
的 _check_att_error()
:
def _check_att_error(self, data: bytes) -> int:
att_err = super()._check_att_error(data)
# IDS 並沒有說明若前一個指令是以下情況時怎麼處理:
# Abort
# Delete
# Report Number of Stored Records
# 目前實作:
# 若指令不會造成 _is_transmitting 為 True,
# 或 abort 已經在執行,
# 則 ATT Error 都為 Procedure Already In Progress
# 返回 ATT Eroor,如果:
# ATT Error 不是 Procedure Already In Progress
# 或 不是 Abort 指令
# 或 目前沒有在處理 History Data
# 或 abort 已經在執行
if att_err and (
att_err != ATT_ERR_PROCEDURE_ALREADY_IN_PROGRESS
or data[0] != _ABORT
or not self._is_transmitting
or self._is_aborting
):
return att_err
return 0
接著實現 _parse_write_data()
:
def _parse_write_data(self, data: bytes):
opcode = data[0]
operator = data[1]
data_mv = memoryview(data)
operand = self._get_operand(data_mv)
# 與 IDD Status Reader Control Point 和 IDD Command Control Point 的流程不同,
# 先檢查 operator 和 operand 是否正確。
# 因為 IDD RACP 有 Abort,
# 若留到協程運作時才檢查,
# 可能導致接踵而來的 Abort 無法正確判定,
# 雖然在正常流程下,
# 理應不該發生
if opcode == _REPORT_RECORDS or opcode == _REPORT_NUMBER:
if self._check_rec_range(operator, operand) == _SUCCESS:
self._is_transmitting = True
self._is_aborting = False
self._is_aborted.clear()
elif opcode == _ABORT:
if self._check_abort_params(operator, operand) == _SUCCESS:
self._is_aborting = True
ble.global_var.is_cp_in_progress = True
micropython.schedule(self._fn_on_opcode, data_mv)
這裡使用到的 _check_rec_range()
只是檢查 operator
和 operand
是否符合規定的內容,便由看官自由發揮吧~
(Report Stored Records 的指令格式請參考昨天的內容)
然後咱們用 _on_opcode()
根據 Op Code
來分發給協程處理:
def _on_opcode(self, data: bytes):
opcode = data[0]
operator = data[1]
operand = self._get_operand(data)
if opcode == _REPORT_RECORDS:
asyncio.create_task(self._on_report_records(operator, operand))
elif opcode == _REPORT_NUMBER:
asyncio.create_task(self._on_report_number(operator, operand))
elif opcode == _ABORT:
asyncio.create_task(self._on_abort(operator, operand))
else:
asyncio.create_task(self._on_not_supported_opcode(opcode))
要注意, _on_opcode()
是在 _parse_write_data()
裡,以 _fn_on_opcode
為參數,間接傳進 micropython.schedule()
。
接下來就是重頭戲了!在 _on_report_records()
裡,會定義 2 個內部函數:
send_record()
_respond()
Response Code
給 GATT Client,告知傳送結束。async def _on_report_records(self, operator: int, operand: bytes):
async def send_record(rec: bytes):
common.eventbus.publish(core.events.EVENT_HISTORY_SENDING, rec)
# 暫停一會兒,以讓資料可以傳送。
# 因協程是單執行緒,若無此,將等到所有資料都放進 queue 後才有機會傳送。
await asyncio.sleep_ms(10)
def _respond(count: int):
self._respond_error(
_REPORT_RECORDS, _SUCCESS if count else _NO_RECORDS_FOUND
)
await self._handle_record_work(
_REPORT_RECORDS, operator, operand, send_record, _respond
)
而在 _handle_record_work()
裡,會從 operator
和 operand
取得對應的 filter 和 History Data:
async def _handle_record_work(
self, opcode: int, operator: int, operand: bytes, fn_item_async, fn_final
):
...
if operator == _FIRST:
rec = self._history_mgr.get_first_history()
elif operator == _LAST:
rec = self._history_mgr.get_last_history()
if self._check_abort_and_set():
return
if rec is not None:
if fn_item_async is not None:
await fn_item_async(rec)
record_count += 1
else:
...
for rec in self._history_mgr.get_histories(filter_min, filter_max):
if self._check_abort_and_set():
return
if fn_item_async is not None:
await fn_item_async(rec)
record_count += 1
fn_final(record_count)
那麼 Report Stored Records
就完成了,最後就剩實際送出 History Data。
還記得在 IddRACP._on_report_records()
裡,咱們讓它每接收一筆 History Data,便發送一次 EVENT_HISTORY_SENDING
嗎?
咱們讓 IDD History Data 來處理這件事:
class IddHistoryData(ble.mixin.NotifyMixin, ble.stack.Characteristic):
def __init__(self):
ble.stack.Characteristic.__init__(self, 0x2B28, notify=True)
ble.mixin.NotifyMixin.__init__(self)
common.eventbus.subscribe(
core.events.EVENT_HISTORY_SENDING, self._on_history_sending
)
def _build_notify_payload(self, buf: bytearray | memoryview, arg: bytes) -> int:
data_len = len(arg)
buf[:data_len] = arg
return data_len
def _on_history_sending(self, history: bytes):
ble.stack.BleTxScheduler().add(
ble.stack.ACT_NOTIFY, self.send_data, self.value_handle, history
)
IddHistoryData
訂閱事件 EVENT_HISTORY_SENDING
後,只是將收到的資料以 Notify
發送出去。_build_notify_payload()
則只是單純地將資料複製到緩衝區。如此一來,當咱們將這些 Characteristic 加入到 IDS Server 裡後,便可用相關指令來傳送 History Data。
至此,咱們的 IDS Server 便宣告完成了~
。:.゚ヽ(*´∀`)ノ゚.:。