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¶
純寫入通道仍然需要定義 size 與 read,即使它們只是回傳 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 內容,讓主機總能看到最新的值;而增加另一個旋鈕(threshold、exposure、orientation)只需在雙方的 JSON 字典中各加一行。
12.9.4. 一個完整的迴圈¶
有了用於相機 → 主機資料的影格通道、用於主機 → 相機控制的設定通道,再加上少量的黏合程式碼,這個應用程式就成了一個互動式工具:
主機開啟相機、開始拉取影格,並將它們顯示在一個視窗中。
當操作人員拖曳滑桿時,主機會在
config上寫入新值。相機的擷取迴圈會在下一個影格時取得該值。
新的影格則透過同一個
frame通道流動。
這就是整個模型。兩個通道、各兩個回呼函式、相機上的一個擷取迴圈,以及主機上的一個讀寫迴圈。看不到封裝邏輯,也看不到錯誤處理——協定函式庫讓可靠的位元組搬移消失於無形。
從這裡往後的一切都是應用程式碼。增加第三個通道用於直方圖、第四個用於遙測,或第五個用於感測器觸發,都是同一套後端類別加 protocol.register 的配方,反覆套用而已。一旦相機專案到達這個程度,協定就不再是有趣的問題了;取而代之的是應用程式本身的邏輯。