iT邦幫忙

2025 iThome 鐵人賽

DAY 30
0
Software Development

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

Day 30 - 取回 History Data (2) - IDD RACP & IDD History Data

  • 分享至 

  • xImage
  •  

昨天已經知道如何由 BTree 取出資料了,那麼就來完成最後一哩路吧!

1. 實作 Report Stored Records

基本上,IDD Record Access Control Point 和先前的 IDD Status Reader Control PointIDD 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() 只是檢查 operatoroperand 是否符合規定的內容,便由看官自由發揮吧~
(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()
    因 RACP 會一次取得許多 History Data,所以此函數負責每次發送一筆事件,告知訂閱者要傳送 History Data。
  • _respond()
    當所有 History Data 傳送完畢,必須傳送 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() 裡,會從 operatoroperand 取得對應的 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。

2. IDD 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 便宣告完成了~
。:.゚ヽ(*´∀`)ノ゚.:。


上一篇
Day 29 - 取回 History Data (1)
下一篇
Day 31 - 不要一直重複要求配對阿!
系列文
以MicroPython在ESP32上實作Insulin Delivery Service31
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言