13.3.1.5. イベント

これまでのページはすべてカメラを呼び出すものでした。スクリプトのアップロード、フレームの読み取り、チャンネルへの書き込みです。これらの操作はいずれもホスト主導であり、ホストが要求し、カメラが応答します。プロトコルは逆方向にも動作します。カメラは要求されなくてもホストに イベント をプッシュでき、ホスト SDK はそれぞれをアプリケーションがオーバーライドできるコールバックに配送します。

これは、アプリケーションがカメラの気づいた何かに、こちらから問い合わせる前に反応したい場合に適したツールです。イベントがなければ、それを知る唯一の方法は read_status() をループで呼び出し続けることだけです。

13.3.1.5.1. デフォルトのコールバック

Camera は内部で既にイベントを購読しています。_handle_event() は、イベントパケットが到着するたびにトランスポートが実行するコールバックです。デフォルトでは3つのシステムイベントを処理します。

  • CHANNEL_REGISTERED -- ホストの接続後にカメラ上で新しいチャンネルが現れました。フレームワークはチャンネルキャッシュを更新し、次の has_channel() のルックアップでそれが見つかるようにします。

  • CHANNEL_UNREGISTERED -- チャンネルが消えました。

  • SOFT_REBOOT -- カメラが自ら再起動しました(ウォッチドッグ、ハードフォルト、意図的な machine.reset())。

また、ストリーミングのパスのために stream チャンネルのフレーム準備完了イベントを、stdout バッファリングのために stdin チャンネルのスクリプト開始/停止を追跡します。コンストラクタの events=True デフォルトはこれらすべてを有効に保ちます。これらを一切必要としないアプリケーションは Cameraevents=False を渡すことができ、イベントサブシステムは静かなままになります。

13.3.1.5.2. サブクラス化して反応する

カメラが発生させるアプリケーション固有のイベントを処理するには、Camera をサブクラス化して _handle_event() をオーバーライドします。まず親を呼び出してデフォルトの動作を保ったうえで、アプリケーションが関心を持つイベントをディスパッチします:

from openmv import Camera

class MyCamera(Camera):
    def _handle_event(self, channel_id, event):
        super()._handle_event(channel_id, event)

        name = self.channels_by_id.get(
            channel_id, {}).get('name')
        if name == 'motion' and event == 1:
            self.on_motion()

    def on_motion(self):
        print("motion detected")

シグネチャは (channel_id, event) です。channel_id はシステムイベントの場合は 0 で、それ以外の場合はそれを発生させたチャンネルの数値 ID です。event はカメラ側スクリプトが選んだ整数です。EventType 列挙型は3つのシステムイベントに名前を与えます。チャンネルイベントは、カメラ側バックエンドが定義する任意の値を使用します。

チャンネルイベントは名前ではなく数値 ID をキーとして返ってきます。上記のオーバーライドが名前を引くために使っているのはキャッシュされた channels_by_id 辞書です。channels_by_name はその対になるもので、逆方向のキーを持ちます。

13.3.1.5.3. カメラ側の半分

カメラ側スクリプトは、protocol.register() から返されたハンドルに対して send_event() を呼び出すことでイベントを発生させます:

import protocol

class MotionChannel:
    def size(self):
        return 0

    def read(self, offset, size):
        return b''

    def poll(self):
        return False

ch = protocol.register(
    name='motion', backend=MotionChannel())

while True:
    if detect_motion():
        ch.send_event(1)

イベント番号はアプリケーションが選ぶ整数です。ホストのオーバーライドが処理できるよう準備されている値であれば何でも使えます。プロトコル層はそれを不透明なペイロードとして扱います。デフォルトでは呼び出しは送りっぱなしです。wait_ack=True を渡すと、ホストが確認応答するまでブロックします。これは、往復のレイテンシよりもイベントが届いたことを知る方が重要な場合に使います。

イベントを発生させるだけで読み取り可能なデータを持たないチャンネルも有効なパターンです。size0 を返し、read は空のバイト列を返します。プロトコルライブラリは、チャンネルを読み取り可能としてマークするために両方のメソッドが存在することを依然として必要とします。カメラ側スクリプトは単にそこにデータを入れないだけです。

13.3.1.5.4. アイドル時に受信パスを駆動する

イベントは他のすべてと同じ接続で到着するため、バイトを送受信するホストの呼び出しはいずれも、保留中のイベントをインラインで処理する機会をトランスポートに与えます。サイクルごとに既に read_status() または read_frame() を1回呼び出しているポーリングループには、追加で何かが必要になることはありません。

数分間にわたって他の I/O を行わないプログラムのために、poll_events() はコマンドを送ることなく受信パスを1回実行します。受信バッファが空になるとすぐに返るため、それを囲むタイトなループ(または GUI イベントループ内の短いタイマー)が、ハンドラを反応的に保ちます。

13.3.1.5.5. 完全なループ

エンドツーエンドのパターンは次のとおりです。カメラ側スクリプトはチャンネルを登録し、何かが起きたときに send_event() を呼び出します。ホスト側のサブクラスは _handle_event() をオーバーライドしてディスパッチします。イベントの処理だけを行うホストのループは次のようになります:

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

    while True:
        cam.poll_events()

カメラはキャプチャし、判断し、イベントを発生させます。ホストはイベントが到着するまで poll_events() の中で待機し、到着したら on_motion が実行されます。何も起きていないときには read_status() の呼び出しは行われず、カメラに報告すべきものがないときには USB 経由でフレームが取得されることもありません。