12.9. 双方向のフロー

チャネルは一方向ではありません。write を実装したバックエンドは、ホストがバイト列をカメラ送り込めるようにし、カメラはそれに反応します。これがすべての実用的なインタラクティブツールの背後にあるパターンです。オペレーターがホストGUI上のつまみを回すと、ホストは新しい値をコンフィグチャネルに書き込み、カメラは次にキャプチャするときにそれを読み取ります。

12.9.1. コンフィグチャネル

ストリーミング用のカメラ側スクリプトに追加して、JPEG品質用の2つ目のチャネルを公開します:

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=...) が実行され、カメラ側が確認応答を返します。呼び出しが戻る頃には、カメラは新しい値を受け取り、受け入れ済みです。

書き込みはカメラの観点からはアトミックです -- プロトコルライブラリは、そのチャネル上で他の操作が進む前に、バックエンドの write が完了まで実行されることを保証します。アプリケーションコードは、ホストがスナップショットの途中で割り込むことを心配せずに、キャプチャループの中から config.quality を読み取ることができます。

12.9.3. スタブのsizeと書き込み専用チャネルでのread

純粋な書き込み専用チャネルであっても、sizeread を定義する必要があります。たとえそれらが0と b'' を返すだけのスタブであってもです。ライブラリはメソッドの存在を使ってチャネルの機能フラグを導出します。read を欠いたバックエンドは CHANNEL_FLAG_READ が設定されず、ホストは読み取りの試みを拒否します。

もっとも、書き込み専用チャネルでの read から返されるバイト列は、別の目的には役立ちます。すなわち、現在の値をエコーバックすることで、接続したばかりのホストがデフォルトから始めるのではなく、カメラに「現在の設定は何か?」と尋ねられるようにするのです。これを機能させるには、両方向がシリアライズについて合意していなければなりません。先ほどの例の生バイト列 int(bytes(data)) による解析は、単一の整数フィールドには機能しますが、設定すべきつまみが2つになるとスケールしなくなります。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辞書に1行加えるだけで済みます。

12.9.4. 完全なループ

カメラ → ホストのデータ用のフレームチャネル、ホスト → カメラの制御用のコンフィグチャネル、そして少量の接着コードがあれば、アプリケーションはインタラクティブなツールになります。

  • ホストはカメラを開き、フレームの取得を開始し、それらをウィンドウに表示します。

  • オペレーターがスライダーをドラッグすると、ホストは新しい値を config に書き込みます。

  • カメラのキャプチャループは、次のフレームでその値を取り込みます。

  • 新しいフレームは同じ frame チャネルを通って流れます。

これがモデルのすべてです。2つのチャネル、それぞれに2つのコールバック、カメラ上のキャプチャループ、ホスト上の読み書きループ。フレーミングのロジックは見えず、エラー処理も見えません -- プロトコルライブラリが信頼性の高いバイトの移動を見えなくしてくれます。

この先はすべてアプリケーションコードです。ヒストグラム用の3つ目のチャネル、テレメトリ用の4つ目、センサートリガー用の5つ目を追加するのも、同じバックエンドクラスと protocol.register のレシピを繰り返すだけです。カメラプロジェクトがこの段階に達すると、プロトコルはもはや興味深い問題ではなくなり、アプリケーション自身のロジックがその役割を担います。