13.3.1.5. 事件

到目前為止的各頁面都是呼叫進入相機:上傳指令碼、讀取影格、寫入頻道。其中每一項操作都是由主機發起的——主機詢問,相機回應。通訊協定也能朝另一個方向運作。相機可以在未被詢問的情況下將 事件(events)推送給主機,而主機 SDK 會將每個事件交付給應用程式可覆寫的回呼函式。

每當應用程式想對相機在被詢問之前所注意到的某件事做出反應時,這就是合適的工具。若沒有事件機制,唯一的查知方式就是在迴圈中不斷呼叫 read_status()

13.3.1.5.1. 預設回呼函式

Camera 內部已訂閱事件。_handle_event() 是傳輸層在每次事件封包抵達時執行的回呼函式。預設會處理三個系統事件:

  • CHANNEL_REGISTERED ——主機連線後,相機上出現了一個新頻道。框架會更新其頻道快取,讓下一次 has_channel() 查詢能找到它。

  • CHANNEL_UNREGISTERED ——某個頻道消失了。

  • SOFT_REBOOT ——相機自行重新開機了(看門狗、硬體故障,或刻意呼叫 machine.reset())。

它也會追蹤 stream 頻道的影格就緒事件以供串流路徑使用,以及 stdin 頻道的指令碼啟動/停止以供 stdout 緩衝使用。建構子的 events=True 預設值會讓這一切保持啟用;不需要這些功能的應用程式可向 Camera 傳入 events=False,事件子系統便會保持靜默。

13.3.1.5.2. 子類別化以做出反應

若要處理相機引發的應用程式專屬事件,請子類別化 Camera 並覆寫 _handle_event()。先呼叫父類別以保留預設行為,然後派發應用程式關心的事件:

from openmv import Camera

class MyCamera(Camera):
    def _handle_event(self, channel_id, event):
        super()._handle_event(channel_id, event)

        name = self.channels_by_id.get(
            channel_id, {}).get('name')
        if name == 'motion' and event == 1:
            self.on_motion()

    def on_motion(self):
        print("motion detected")

其簽章為 (channel_id, event)。對系統事件而言 channel_id0,否則為引發事件之頻道的數值 ID;event 是相機端指令碼所選擇的整數。EventType 列舉為這三個系統事件命名;頻道事件則使用相機端後端所定義的任何值。

頻道事件回傳時是以數值 ID 而非名稱作為鍵值。上述覆寫所使用的快取 channels_by_id 字典就是用來查詢名稱的;channels_by_name 則是它的鏡像,以另一個方向作為鍵值。

13.3.1.5.3. 相機端部分

相機端指令碼透過對 protocol.register() 回傳的控制代碼呼叫 send_event() 來引發事件:

import protocol

class MotionChannel:
    def size(self):
        return 0

    def read(self, offset, size):
        return b''

    def poll(self):
        return False

ch = protocol.register(
    name='motion', backend=MotionChannel())

while True:
    if detect_motion():
        ch.send_event(1)

事件編號是應用程式所選擇的整數。任何主機端覆寫準備好處理的值都可使用;通訊協定層將其視為不透明的酬載。預設情況下,該呼叫會發出後即不再理會;傳入 wait_ack=True 可阻塞直到主機確認,適用於知道事件確實送達比往返延遲更重要的情況。

一個只引發事件而不承載任何可讀資料的頻道是有效的模式——size 回傳 0read 回傳空位元組。通訊協定函式庫仍需這兩個方法都存在才能將頻道標記為可讀;相機端指令碼只是從不在其中放入資料。

13.3.1.5.4. 在閒置時驅動接收路徑

事件與其他所有資料都在同一條連線上抵達,因此任何傳送或接收位元組的主機呼叫,都讓傳輸層有機會就地處理待處理的事件。一個本來就每個週期呼叫一次 read_status()read_frame() 的輪詢迴圈,不需要任何額外處理。

對於數分鐘內都沒有其他 I/O 的程式,poll_events() 會在不傳送命令的情況下執行一次接收路徑。它在輸入緩衝區清空後立即返回,因此環繞它的緊密迴圈——或 GUI 事件迴圈中的一個短計時器——就是讓處理常式保持反應靈敏的方式。

13.3.1.5.5. 一個完整的迴圈

從端到端來看,其模式為:相機端指令碼註冊一個頻道,並在某事發生時呼叫 send_event();主機端子類別覆寫 _handle_event() 並進行派發。一個只服務事件而不做其他事的主機迴圈看起來像這樣:

with MyCamera('/dev/ttyACM0') as cam:
    cam.stop()
    cam.exec(open('motion_cam.py').read())

    while True:
        cam.poll_events()

相機進行擷取、判斷並引發事件。主機停留在 poll_events() 中,直到有事件抵達,接著 on_motion 便執行。在沒有任何事發生時,不會執行 read_status() 呼叫;在相機沒有任何回報內容時,也不會透過 USB 取出影格。