13.3.1.4. カスタムチャンネル

チャンネル とは、カメラ側スクリプトとホストの間の、名前付きの双方向バイトストリームです。カメラがチャンネルを登録し、データを生成または消費するコールバックを提供します。ホストはそのチャンネルに対して名前で読み書きします。パッケージが内部的に使用しているのと同じ仕組み(フレームを運ぶ stream チャンネル、スクリプト出力を運ぶ stdout チャンネル、スクリプトのアップロードを運ぶ stdin チャンネル)がユーザースクリプトにも公開されているため、ホストが必要とするアプリケーション固有のあらゆるデータを、2つ目のプロトコルを考案することなく同じ USB 接続に載せられます。

これはパッケージの最も有用な機能でありながら、標準ドキュメントで最も説明が不十分な機能でもあるため、このページではこれをエンドツーエンドで解説します。

13.3.1.4.1. 2つの半分

カスタムチャンネルには、両側で連携するコードが必要です。カメラ側スクリプトprotocol をインポートし、3つのメソッド(size()read()poll())に加えてオプションの write() を持つクラスを定義し、protocol.register(name=..., backend=...) を呼び出して選択した名前でチャンネルを公開します:

import protocol
import time

class TicksChannel:
    def size(self):
        return 10

    def read(self, offset, size):
        return f'{time.ticks_ms():010d}'

    def poll(self):
        return True

protocol.register(name='ticks', backend=TicksChannel())

size() メソッドは、チャンネルが現在利用可能なバイト数を返します。read() はプロデューサです。ホストから要求された offsetsize を受け取り、そのバイト列(またはプロトコル層がエンコードする文字列)を返します。poll() は、読み取るものがある場合に True を返します。プロトコル層はこれを使って、read_status() でチャンネルが準備完了であることを示します。

ホスト側プログラムopenmv.Camera の4つのメソッドを使用します。チャンネルが存在するか確認する has_channel()、どれだけのデータが待機しているか問い合わせる channel_size()、バイトを取り出す channel_read()、バイトを送り込む channel_write() です。read_status() はすべてのチャンネルを一度にポーリングします:

from openmv import Camera

with Camera('/dev/ttyACM0') as cam:
    cam.stop()
    cam.exec(open('ticks_cam.py').read())

    while True:
        status = cam.read_status()

        if status.get('ticks'):
            data = cam.channel_read('ticks')
            print(f"ticks: {data.decode()}")

ホストのループは read_status() をポーリングします。ticks チャンネルが準備完了になると、size を指定せずに channel_read() を呼び出し、利用可能なものをすべて取り出します。カメラの TicksChannel.poll() はチェックのたびに True を返すため、チャンネルは常に「準備完了」となり、ホストはポーリングのたびに新しい tick の値を取得します。

13.3.1.4.2. 双方向チャンネル

データをホスト側から送り返す必要がある場合、カメラ側のクラスに、受信するバイトを受け取る write() メソッドを追加します:

import protocol

class CommandChannel:
    def __init__(self):
        self.last_command = b''
        self.replied = False

    def size(self):
        return len(self.last_command)

    def read(self, offset, size):
        self.replied = True
        return self.last_command

    def write(self, offset, data):
        self.last_command = b'echo: ' + bytes(data)
        self.replied = False

    def poll(self):
        return not self.replied and len(self.last_command) > 0

protocol.register(name='echo', backend=CommandChannel())

ホストは channel_write() でチャンネルに書き込み、通常の read_status() / channel_read() のパターンで返信を読み取ります:

with Camera('/dev/ttyACM0') as cam:
    cam.stop()
    cam.exec(open('echo_cam.py').read())

    cam.channel_write('echo', b'hello')

    while True:
        if cam.read_status().get('echo'):
            print(cam.channel_read('echo').decode())
            break

13.3.1.4.3. これがアプリケーションにもたらすもの

カスタムチャンネルは、アプリケーションがフレーム以外・出力以外のデータを既存の USB 接続に載せたい場合に適したツールです。たとえばテレメトリのカウンタ、ホスト上の UI からライブでストリーミングされる設定ノブ、逆方向に送られる制御コマンド、stream チャンネルが前提とする「画像」というフレーミングに収まらない、カメラが計算した測定結果などです。プロトコル層がフレーミング、分割、確認応答、リトライを処理します。スクリプトは4つのメソッドからなるバックエンドを実装するだけでよく、ホストはチャンネル名とデータの形を知っているだけで済みます。

CLI の --channel NAME フラグは、ホスト側のプログラムを書くことなくターミナルからカスタムチャンネルを検証する手軽な方法です。CLI は指定されたチャンネルをポーリングし、各更新の先頭10バイトを出力します。

1回の channel_read() または channel_write() 呼び出しのサイズ制限は、プロトコルがネゴシエーションした max_payload であり、デフォルトでは4096バイトです。ホスト側のメソッドは、より大きな書き込みを適切な数のパケットに自動的に分割するため、アプリケーションは任意の大きさのバッファを渡せます。分割は意識する必要がありません。