12.6. 具名通道

每個封包標頭中的通道 ID 讓多達 32 條獨立的串流可以共用同一條實體傳輸。通道層將這些數值 ID 轉換成具名、且應用程式可見的端點,讓主機程式碼能以字串來指稱它們。

左側一條傳輸線扇出成相機側四條 標記過的通道——stdin、stdout、 stream,以及一條使用者註冊的狀態通道——每一條 都以一個獨立的方框呈現。

12.6.1. 四條內建通道

相機在開機時、任何應用程式碼執行之前,便註冊了四條通道:

  • stdin —— 主機推送給相機執行的指令碼位元組。IDE 使用此通道來傳送正在編輯的指令碼;主機端 SDK 上的 exec() 是 Python 程式中的對應呼叫。

  • stdout —— 來自相機 print() 呼叫及未捕捉之例外回溯(traceback)的位元組。IDE 的序列主控台會讀取此通道。

  • stream —— 即時預覽通道。IDE 從中拉取 JPEG 影格;任何主機指令碼都能以 read_frame() 做到相同的事。

  • profile —— 分析器(profiler)事件,僅在相機建置時啟用分析功能的情況下才存在。大多數釋出版建置會省略它。

應用程式碼很少需要動到任何內建通道;有趣的工作發生在應用程式自行註冊的通道上。

12.6.2. 註冊通道

相機端的指令碼透過呼叫 protocol.register() 並傳入一個名稱和一個 Python 後端 物件來註冊新通道::

import json
import protocol
import time

trigger_count = 0

class StatusChannel:
    def size(self):
        # Refresh the snapshot on every host query.
        self._buf = json.dumps({
            'uptime_s': time.ticks_ms() // 1000,
            'triggers': trigger_count,
        }).encode()
        return len(self._buf)

    def read(self, offset, size):
        return self._buf[offset:offset + size]

protocol.register(name='status', backend=StatusChannel())

後端物件的方法決定了通道能做什麼。一個只有 sizeread 的後端是 唯讀的資料通道;加入 write 它就變成雙向的;加入 poll 則主機在付出讀取代價之前就能詢問是否有新資料就緒。當酬載小到足以放進一個片段時,在 size 內部取樣資料是最簡單的模式——緩衝區依需求產生,從不快取,也從不發生競爭。較大的酬載——影像影格、感測器軌跡——則需要一種閂鎖(latching)模式,將緩衝區保留到主機完成其多片段讀取為止,這部分會在影格通道一節中說明。

少量的記錄工作會自動進行:

  • 函式庫會指派下一個可用的通道 ID(介於 0 到 31 之間)。

  • 功能旗標由存在的方法推導而來:若定義了 read 則設 CHANNEL_FLAG_READ,若定義了 write 則設 CHANNEL_FLAG_WRITE,若定義了 lockunlock 則設 CHANNEL_FLAG_LOCK

  • 一個 CHANNEL_REGISTERED 事件封包會被送往任何已連線的主機,讓其通道清單得以更新。

回傳值是一個 protocol.ProtocolChannel 控制代碼,應用程式可以保留它。該控制代碼的 send_event() 方法是相機端的掛鉤,用以告訴主機「此通道上發生了某件事,但可讀取的資料並未改變」——觸發器被觸動、按鈕被按下、樣本計數達到某個里程碑。

12.6.3. 從主機讀取通道

主機端 SDK 在 PyPI 上以 openmv 套件形式發行(pip install openmv),以 pyserial 作為傳輸的基礎。它的 openmv.camera.Camera 類別透過高階方法揭露相機的具名通道::

from openmv.camera import Camera

with Camera('/dev/ttyACM0', baudrate=921600) as cam:
    cam.update_channels()
    if cam.has_channel('status'):
        size = cam.channel_size('status')
        data = cam.channel_read('status', size)

警告

openmv 套件需要 CPython 3.12 或更新版本。較早的直譯器缺少 SDK 所依賴的功能;請在 pip install openmv 之前安裝 3.12 以上的版本。

關於這套設定,有幾點值得留意:

  • 序列埠字串——此處為 /dev/ttyACM0——在 Windows 上是 COM3 形式,在 macOS 上是 /dev/cu.usbmodemXXXX,在 Linux 上是 /dev/ttyACM*。實際的編號取決於相機列舉成哪一個埠。

  • 鮑率是協定的魔術值 921600,相機的 USB-CDC 堆疊會將其辨識為「這個用戶端說的是協定,而非 REPL」。任何其他鮑率都會退回成普通的序列連線。

  • with Camera(...) as cam: 情境管理員會開啟傳輸、執行 PROTO_SYNC、交換功能,並在離開時乾淨地關閉該埠。進入後明確呼叫的 update_channels() 會以應用程式在開機後註冊的任何通道來重新整理本地通道清單。

channel_size()channel_read() 是主力方法;若後端具有 write 方法,channel_write() 會將緩衝區往返傳送至相機;has_channel() 則是在使用名稱前安全地檢查它是否已註冊的方式。通道名稱只會被查詢一次以對應到相機在 register 期間指派的通道 ID,此後每個封包都會使用該 ID。

每一對 channel_size()channel_read() 都需付出兩次往返:一個封包詢問大小,一個封包索取位元組。在 USB-CDC 上兩者合計約一毫秒內完成;在 UART 上同樣的交換會依序列線鮑率成比例地花更久。在緊湊迴圈中讀取的應用程式碼,應該只在大小實際上會改變時才呼叫 channel_size()——對於固定大小的資料,第一次呼叫得到的大小可以快取起來。

12.6.4. 通道之間的獨立性

關於通道如何互動,有三件事值得了解:

  • 獨立的流量控制。 每條通道有自己待處理的讀取狀態、自己的資料,以及自己的 sizereadwrite 回呼函式。在 stream 通道上長時間進行的讀取,不會阻塞應用程式 config 通道上的讀取。

  • 每條通道內依序。 在單一通道內,封包會依序傳遞。即使涉及重傳,可靠性層也會保證這一點。

  • 共用傳輸、共用重傳預算。 所有通道共用這唯一一條實體連結,因此某條通道上洪水般的流量會因霸占線路而拖慢其他通道。CHANNEL_LOCK 機制讓一條通道能保留線路以進行不可分割的多封包讀取;後端透過實作 lockunlock 回呼函式來選擇加入。

通道是主機程式與相機程式同意協作的最小介面範圍。名稱、方向性(讀、寫或兩者皆可)、相機端的回呼方法,以及主機端對應的方法呼叫,便構成了完整的契約。