12.6. 具名通道¶
每個封包標頭中的通道 ID 讓多達 32 條獨立的串流可以共用同一條實體傳輸。通道層將這些數值 ID 轉換成具名、且應用程式可見的端點,讓主機程式碼能以字串來指稱它們。
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())
後端物件的方法決定了通道能做什麼。一個只有 size 和 read 的後端是 唯讀的資料通道;加入 write 它就變成雙向的;加入 poll 則主機在付出讀取代價之前就能詢問是否有新資料就緒。當酬載小到足以放進一個片段時,在 size 內部取樣資料是最簡單的模式——緩衝區依需求產生,從不快取,也從不發生競爭。較大的酬載——影像影格、感測器軌跡——則需要一種閂鎖(latching)模式,將緩衝區保留到主機完成其多片段讀取為止,這部分會在影格通道一節中說明。
少量的記錄工作會自動進行:
函式庫會指派下一個可用的通道 ID(介於 0 到 31 之間)。
功能旗標由存在的方法推導而來:若定義了
read則設CHANNEL_FLAG_READ,若定義了write則設CHANNEL_FLAG_WRITE,若定義了lock/unlock則設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. 通道之間的獨立性¶
關於通道如何互動,有三件事值得了解:
獨立的流量控制。 每條通道有自己待處理的讀取狀態、自己的資料,以及自己的
size/read/write回呼函式。在stream通道上長時間進行的讀取,不會阻塞應用程式config通道上的讀取。每條通道內依序。 在單一通道內,封包會依序傳遞。即使涉及重傳,可靠性層也會保證這一點。
共用傳輸、共用重傳預算。 所有通道共用這唯一一條實體連結,因此某條通道上洪水般的流量會因霸占線路而拖慢其他通道。
CHANNEL_LOCK機制讓一條通道能保留線路以進行不可分割的多封包讀取;後端透過實作lock/unlock回呼函式來選擇加入。
通道是主機程式與相機程式同意協作的最小介面範圍。名稱、方向性(讀、寫或兩者皆可)、相機端的回呼方法,以及主機端對應的方法呼叫,便構成了完整的契約。