12.6. 名前付きチャネル

各パケットのヘッダーにあるチャネル ID により、最大 32 個の独立したストリームが同じ物理トランスポートを共有できます。チャネル層は、これらの数値 ID を、ホストコードが文字列で参照できる名前付きでアプリケーションから見えるエンドポイントに変換します。

左側にある 1 本のトランスポート配線が、cam 側で 4 つの ラベル付きチャネル(stdin、stdout、stream、および ユーザー登録のステータスチャネル)に分かれ、 それぞれが独立したボックスとして表示される様子。

12.6.1. 4 つの組み込みチャネル

cam はアプリケーションコードが実行される前、起動時に 4 つのチャネルを登録します。

  • stdin -- ホストが実行のために cam にプッシュするスクリプトのバイト。IDE はこのチャネルを使って編集中のスクリプトを送信します。ホスト SDK の exec() は Python プログラムからの同等の呼び出しです。

  • stdout -- cam の print() 呼び出しや捕捉されなかった例外のトレースバックからのバイト。IDE のシリアルコンソールはこのチャネルを読み取ります。

  • stream -- ライブプレビューのチャネル。IDE はここから JPEG フレームを取得します。任意のホストスクリプトも read_frame() で同じことができます。

  • profile -- プロファイラーイベント。cam がプロファイリングを有効にしてビルドされた場合にのみ存在します。ほとんどのリリースビルドではこれは省略されます。

アプリケーションコードが組み込みチャネルのいずれかに触れる必要があることはまれです。興味深い作業は、アプリケーション自身が登録するチャネル上で行われます。

12.6.2. チャネルの登録

cam 側のスクリプトは、名前と Python の バックエンド オブジェクトを指定して protocol.register() を呼び出すことで、新しいチャネルを登録します:

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 の中でデータをサンプリングすることは、ペイロードが 1 つのフラグメントに収まるほど小さい場合の最もシンプルなパターンです。バッファは要求に応じて生成され、キャッシュされることもレースされることもありません。より大きなペイロード(画像フレーム、センサーのトレース)には、ホストが複数フラグメントの読み取りを完了するまでバッファを保持するラッチングパターンが必要で、これはフレームチャネルで扱います。

少量の管理処理が自動的に行われます。

  • ライブラリは次の空いているチャネル ID(0 から 31 の間)を割り当てます。

  • 機能フラグは存在するメソッドから導出されます。read が定義されていれば CHANNEL_FLAG_READwrite が定義されていれば CHANNEL_FLAG_WRITElock / unlock が定義されていれば CHANNEL_FLAG_LOCK です。

  • CHANNEL_REGISTERED イベントパケットが、接続されているすべてのホストに送信され、ホストのチャネルリストが更新されます。

戻り値はアプリケーションが保持できる protocol.ProtocolChannel ハンドルです。ハンドルの send_event() メソッドは、「読み取り可能なデータを変更することなくこのチャネルで何かが起きた」とホストに伝えるための cam 側のフックです。トリガーが発火した、ボタンが押された、サンプル数の節目を通過した、といった具合です。

12.6.3. ホストからのチャネルの読み取り

ホスト SDK は PyPI 上の openmv パッケージ(pip install openmv)として提供され、トランスポートには pyserial を使用しています。その openmv.camera.Camera クラスは、cam の名前付きチャネルを高水準のメソッドを通じて公開します:

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* になります。実際の番号は cam がどのポートとして列挙されたかによります。

  • ボーレートはプロトコルのマジック値 921600 で、cam の USB-CDC スタックはこれを「このクライアントは REPL ではなくプロトコルを話す」と認識します。それ以外のレートはプレーンなシリアル回線にフォールバックします。

  • with Camera(...) as cam: コンテキストマネージャはトランスポートを開き、PROTO_SYNC を実行し、機能を交換し、終了時にはポートをクリーンに閉じます。入った後に明示的に update_channels() を呼び出すと、起動後にアプリケーションが登録したチャネルでローカルのチャネルリストを更新します。

channel_size()channel_read() は主力となるメソッドです。channel_write() は、バックエンドに write メソッドがあればバッファを cam に往復させます。has_channel() は、名前を使用する前にそれが登録されているかを安全に確認する方法です。チャネル名は、register 時に cam が割り当てたチャネル ID に一度だけ解決され、それ以降はすべてのパケットで使用されます。

channel_size() / channel_read() のペアは 2 回の往復のコストがかかります。サイズを問い合わせるパケットが 1 つ、バイトを問い合わせるパケットが 1 つです。USB-CDC では両方を合わせて約 1 ミリ秒で完了します。UART では同じやり取りが、シリアル回線のボーレートに比例してより長くかかります。タイトなループで読み取るアプリケーションコードは、サイズが実際に変化しうる場合にのみ channel_size() を呼び出すべきです。固定サイズのデータの場合、最初の呼び出しで得たサイズをキャッシュできます。

12.6.4. チャネル間の独立性

チャネルがどのように相互作用するかについて、知っておく価値のあることが 3 つあります。

  • 独立したフロー制御。 各チャネルは、それ自身の保留中の読み取り状態、それ自身のデータ、それ自身の size / read / write コールバックを持ちます。stream チャネルでの長時間の読み取りは、アプリケーションの config チャネルでの読み取りをブロックしません。

  • チャネルごとに順次。 単一のチャネル内では、パケットは順序どおりに配信されます。信頼性層は、再送が関わる場合でもこれを保証します。

  • トランスポートの共有、再送予算の共有。 すべてのチャネルは 1 本の物理リンクを共有するため、あるチャネルでの大量のトラフィックは配線を占有することで他のチャネルを遅くします。CHANNEL_LOCK の仕組みにより、1 つのチャネルがアトミックな複数パケットの読み取りのために配線を予約できます。バックエンドは lock / unlock コールバックを実装することでこれをオプトインします。

チャネルは、ホストプログラムと cam プログラムが協調することに合意する最小の面積です。名前、方向性(読み取りか書き込みか両方か)、cam 側のコールバックメソッド、そしてホスト側の対応するメソッド呼び出しが、契約のすべてです。