12.9. 雙向流動

通道並非單向的。實作了 write 的後端能讓主機把位元組推送相機,而相機會做出反應。這正是每個真正的互動式工具背後的模式:操作人員在主機 GUI 上轉動旋鈕,主機將新值寫入設定通道,相機則在下次擷取時讀取它。

12.9.1. 設定通道

在串流相機端的指令碼上加以擴充,公開第二個通道來控制 JPEG 品質:

class ConfigChannel:
    def __init__(self):
        self.quality = 85

    def size(self):
        return 0

    def read(self, offset, size):
        # Not used for "host writes to cam" -- but the library
        # still needs the method present.
        return b''

    def write(self, offset, data):
        # data is a bytearray view into the protocol buffer.
        # Copy out the contents before doing anything with it.
        new_q = int(bytes(data))
        if 1 <= new_q <= 100:
            self.quality = new_q
        return len(data)

config = ConfigChannel()
protocol.register(name='config', backend=config)

擷取迴圈每次壓縮影格時都會從 config.quality 讀取:

while True:
    img = csi0.snapshot()
    latest_jpeg = bytes(
        img.compress(quality=config.quality).bytearray()
    )
    ch.send_event(0x01)

現在主機有了一個旋鈕。把它設為 50,下一個影格就會變小(也更醜);設為 95,下一個影格就會變大(也更清晰)。相機持續擷取而無需重新啟動;主機也不必推送新的指令碼。

12.9.2. 來自主機的寫入呼叫

在主機端,channel_write() 會將位元組傳送到具名的通道:

cam.channel_write('config', b'50')

主機 SDK 將位元組編碼為單一(或分片的)CHANNEL_WRITE 封包,協定層將其遞交給相機,相機的 write(offset=0, data=...) 隨即執行,相機端再回覆確認。當該呼叫返回時,相機已經收到並接受了新值。

從相機的角度來看,這次寫入是不可分割的(atomic)——協定函式庫保證後端的 write 會執行完成,之後該通道上才會進行任何其他操作。應用程式碼可以從擷取迴圈內部讀取 config.quality,而不必擔心主機在快照進行到一半時介入干擾。

12.9.3. 佔位的 size 與唯寫通道上的 read

純寫入通道仍然需要定義 sizeread,即使它們只是回傳 0 與 b'' 的佔位實作。函式庫是依據方法的存在與否來推導通道的能力旗標;缺少 read 的後端不會被設定 CHANNEL_FLAG_READ,主機因此會拒絕讀取嘗試。

不過,唯寫通道上 read 回傳的位元組可用於另一個目的:回傳當前值,讓剛連上的主機能夠詢問相機「目前的設定是什麼?」,而不必從預設值開始。要讓這運作起來,雙方都必須對序列化方式達成一致。先前範例中以原始位元組進行的 int(bytes(data)) 解析,對單一整數欄位是可行的,但一旦有了第二個旋鈕要設定就無法擴展。將 write 改為解析 JSON,並搭配一個回傳對應 JSON 內容的 read,就能把通道變成真正可雙向往返的組態儲存區:

import json

class ConfigChannel:
    def __init__(self):
        self.quality = 85
        self._buf = b''
    def size(self):
        self._buf = json.dumps({'quality': self.quality}).encode()
        return len(self._buf)
    def read(self, offset, size):
        return self._buf[offset:offset + size]
    def write(self, offset, data):
        new = json.loads(bytes(data))
        if 'quality' in new:
            self.quality = int(new['quality'])
        return len(data)

現在主機寫入 cam.channel_write('config', b'{"quality": 50}') 來設定值,並以 cam.channel_read('config') 讀回當前狀態。相機在每次讀取時都會序列化一份全新的 JSON 內容,讓主機總能看到最新的值;而增加另一個旋鈕(thresholdexposureorientation)只需在雙方的 JSON 字典中各加一行。

12.9.4. 一個完整的迴圈

有了用於相機 → 主機資料的影格通道、用於主機 → 相機控制的設定通道,再加上少量的黏合程式碼,這個應用程式就成了一個互動式工具:

  • 主機開啟相機、開始拉取影格,並將它們顯示在一個視窗中。

  • 當操作人員拖曳滑桿時,主機會在 config 上寫入新值。

  • 相機的擷取迴圈會在下一個影格時取得該值。

  • 新的影格則透過同一個 frame 通道流動。

這就是整個模型。兩個通道、各兩個回呼函式、相機上的一個擷取迴圈,以及主機上的一個讀寫迴圈。看不到封裝邏輯,也看不到錯誤處理——協定函式庫讓可靠的位元組搬移消失於無形。

從這裡往後的一切都是應用程式碼。增加第三個通道用於直方圖、第四個用於遙測,或第五個用於感測器觸發,都是同一套後端類別加 protocol.register 的配方,反覆套用而已。一旦相機專案到達這個程度,協定就不再是有趣的問題了;取而代之的是應用程式本身的邏輯。