昨天咱們已經把 IDD Status Reader Control Point 的架構完成了,但它對所有輸入的資料都是回傳 Op Code not supported
,就像光禿禿的樹一樣。所以現在就來實作一些指令,為 IDD Status Reader Control Point 增添實質功能吧。
雖然昨天咱們已經看過了,但在開始前,還是複習一下 IDD Status Reader Control Point 的指令格式:
因為咱們已經將 BaseCP
和 E2ECPMixin
完成了,IddStatusReaderCP
的基礎也已奠定好,所以要實作特定 Op Code,就只是在 _on_opcode()
方法裡,指派協程去執行而已,比如:
class IddStatusReaderCP(BaseCP):
def _on_opcode(self, data: bytes):
mv_data = memoryview(data)
opcode = mv_data[0] | mv_data[1] << 8
operand = self._get_operand(mv_data)
if opcode == _RESET_STATUS:
asyncio.create_task(self._on_reset_status(operand))
elif opcode == _GET_ACTIVE_BOLUS_IDS:
asyncio.create_task(self._on_get_active_bolus_ids(operand))
elif opcode == _GET_ACTIVE_BOLUS_DELIVERY:
asyncio.create_task(self._on_get_active_bolus_delivery(operand))
elif opcode == _GET_TOTAL_DAILY_INSULIN_STATUS:
asyncio.create_task(self._on_get_total_daily_insulin_status(operand))
else:
asyncio.create_task(self._on_not_supported_opcode(opcode))
其他 IDS 的 Control Point 也基本上都是使用此形式來設計。
Reset Status 指令的 Operand 欄位如下:
這裡的 Flags 就是 IDD Status Changed 所使用的欄位:
Flags fit Bit | Definition |
---|---|
0 | Therapy Control State Changed |
1 | Operational State Changed |
2 | Reservoir Status Changed |
3 | Annunciation Status Changed |
4 | Total Daily Insulin Status Changed |
5 | Active Basal Rate Status Changed |
6 | Active Bolus Status Changed |
7 | History Event Recorded |
All other bits | RFU |
因為在第 18 天的 Event Bus 裡,IddStatusChanged
類別已經完成了事件 EVENT_IDD_STATUS_RESETTING
的實作,所以要實現 Reset Status 這指令非常簡單:
async def _on_reset_status(self, operand: bytes):
try:
if not self._check_condition(
len(operand) == 2, _RESET_STATUS, _INVALID_OPERAND
):
return
flags = operand[1] << 8 | operand[0]
if not self._check_condition(
flags & 0xFF00 == 0, _RESET_STATUS, _INVALID_OPERAND
):
return
common.eventbus.publish(core.events.EVENT_IDD_STATUS_RESETTING, flags)
self._respond_error(_RESET_STATUS, _SUCCESS)
finally:
state = machine.disable_irq()
ble.global_var.is_cp_in_progress = False
machine.enable_irq(state)
operand
的資料長度是否為 2 bytes
,若是,則繼續執行;否則回應錯誤碼 Invalid Operand
。flags
值,並且檢查 flags
是否在規定範圍內,若否(即 RFU 位元被設定),則回應 Invalid Operand
。EVENT_IDD_STATUS_RESETTING
,表示要重設 flags
所指定的狀態。Success
。is_cp_in_progress
設為 False
,表示 Control Point 的行為已結束。此指令沒有 operand,而它的回應結構中,operand 會包含每天的 Bolus、Basal、和二者總和的注射量。要注意的是,因 SFLOAT 精度的原因,總和欄位未必一定等於 Bolus 和 Basal 二欄位的總合:
為了實現這個指令,咱們先在 Config
類別增加兩個變數:
class Config:
def __init__(self):
# 已累積的每天 Bolus 施打量
# Unit: IU
self.total_daily_bolus_delivered = common.fixedfloat.FixedFloat(0)
# 已累積的每天 Basal 施打量
# Unit: IU
self.total_daily_basal_delivered = common.fixedfloat.FixedFloat(0)
def to_dict(self):
return {
...
"total_daily_bolus_delivered": self.total_daily_bolus_delivered.to_json(),
"total_daily_basal_delivered": self.total_daily_basal_delivered.to_json(),
}
@classmethod
def from_dict(cls, d: dict):
...
obj.total_daily_bolus_delivered = common.fixedfloat.FixedFloat.from_json(
d["total_daily_bolus_delivered"]
)
obj.total_daily_basal_delivered = common.fixedfloat.FixedFloat.from_json(
d["total_daily_basal_delivered"]
)
return obj
雖然咱們是可以在此指令裡直接使用 Config.total_daily_bolus_delivered
和 Config.total_daily_bolus_delivered
,但為了統一管理,會建立 InsulinManager
類別來管理胰島素相關操作:
class InsulinManager:
def __init__(self, config: config.Config):
self._config = config
def get_total_daily_insulin_status(self):
return (
float(self._config.total_daily_bolus_delivered),
float(self._config.total_daily_basal_delivered),
float(
self._config.total_daily_bolus_delivered
+ self._config.total_daily_basal_delivered
),
)
若已遺忘 FixedFloat
,或不明白為什麼它可以執行相加
動作的看官,請參考第 19 天的 避免浮點數誤差 。
材料都已準備好了,那就來實現 Get Total Daily Insulin Status
這指令吧:
async def _on_get_total_daily_insulin_status(self, operand: bytes):
try:
if not self._check_condition(
len(operand) == 0, _GET_TOTAL_DAILY_INSULIN_STATUS, _INVALID_OPERAND
):
return
bolus, basal, total = self._insulin_mgr.get_total_daily_insulin_status()
ble.stack.BleTxScheduler().add(
ble.stack.ACT_INDICATE,
self.send_data,
self.value_handle,
(_respond, bolus, basal, total),
)
finally:
state = machine.disable_irq()
ble.global_var.is_cp_in_progress = False
machine.enable_irq(state)
operand
欄位是否符合規定,若不是,則回應錯誤碼 Invalid Operand
。InsulinManager.get_total_daily_insulin_status()
取得每天胰島素的相關注射劑量。(_respond, bolus, basal, total)
加入到 BleTxScheduler
,讓系統送出 Indicate。BleTxScheduler
的看官,請參考第 14 天的 Notify & Indicate 排程器。ble.global_var.is_cp_in_progress
。如果討厭每次都要寫那麼多一樣的程式的話,就定義一個方法來處理這件事吧。最後來看如何組成回應的內容 _respond()
:
def _respond(
buf: bytearray | memoryview,
bolus: float,
basal: float,
total: float,
) -> int:
common.utils.write_uint16(buf, 0, _GET_TOTAL_DAILY_INSULIN_STATUS_RESPONSE)
t = common.sfloat.float_to_sfloat(bolus)
common.utils.write_uint16(buf, 2, t)
t = common.sfloat.float_to_sfloat(basal)
common.utils.write_uint16(buf, 4, t)
t = common.sfloat.float_to_sfloat(total)
common.utils.write_uint16(buf, 6, t)
return 8
_respond()
只是簡單照著規格書上的欄位來存放資料而已,可能有疑惑的是:
Get Total Daily Insulin Status 小節一開始的欄位組成圖不是只有 3 個欄位嗎?怎麼
_respond()
一開始要先存放_GET_TOTAL_DAILY_INSULIN_STATUS_RESPONSE
這個東西?
那是因為小節一開始給的是 Operand
的欄位圖,而 _GET_TOTAL_DAILY_INSULIN_STATUS_RESPONSE
是 Op Code
。此章一開始的整體指令組成有說明,回應的資料開頭必須包含 Op Code
。
雖然 Get Total Daily Insulin Status 目前讀回的欄位都是 0,也沒有將相關資訊在子夜時重設,但本喵會在之後的說明中完成 ... 可能 ...也許 ... 希望 ... 能講到 ... 吧 ...
(|||゚д゚)