12.8. CBORChannel によるウィジェットとテレメトリ#

前の2ページのバックエンドは生のバイトを運び、生のバイトチャンネルにはそれをデコードする方法を知っているホストプログラムが必要です。しかしカメラが公開したいものは、たいていそれより単純です。いくつかの名前付きの読み取り値、いくつかの名前付きのコントロール、波形、深度マップ。そのために protocol パッケージには protocol.CBORChannel が付属しています。名前付きの フィールド を保持し、それらを SenML レイアウトの CBOR レコードとしてシリアライズし、ホストからの書き込みをフィールド値にデコードし直す既製のバックエンドです。その利点は、OpenMV IDE が既にそれを理解している ことです。CBORChannel を登録すると、IDE のチャンネルビューが、ホストコードをまったく書かずに、すべてのフィールドをライブウィジェット(ラベル、スイッチ、スライダー、グラフ、マップ)として描画します。同じレコードは単なる CBOR なので、IDE では足りなくなったときには、カスタムホストが任意の CBOR ライブラリでデコードできます。

The Channels view showing a thermal camera script's controls channel: FPA and AUX temperature readouts with their unit, Measurement and High Temp toggles, Temp Min and Temp Max sliders, and H-Mirror and V-Flip toggles

IDE が描画した controls_channel.py サンプルのチャンネル。FLIR Lepton 用の読み出し、トグル、スライダー。すべてのコントロールがスクリプトに書き戻します。#

12.8.1. 読み出しとコントロール#

CBORChannel は型付きフィールドの辞書です。add は各フィールドを名前、ウィジェットの type、その型が必要とする引数で宣言します。ch["name"] への代入はフィールドを更新し、on_write コールバックはホストからの変更を受け取ります:

import time
import protocol
from protocol import CBORChannel

def on_write(ch, name, value):
    # Called for every control the host changes.
    print(name, "=", value)
    if name == "Reset":
        ch["Count"] = 0

ch = CBORChannel(on_write=on_write)
ch.add("Status", type="label", value="starting")
ch.add("Count", type="label", value=0)
ch.add("Enable", type="toggle", value=True)
ch.add("Threshold", type="slider", min=0, max=100, step=1,
       value=50, unit="%")
ch.add("Gap", type="spinbox", min=0.0, max=15.0, step=0.1,
       value=1.5, unit="mm")
ch.add("Mode", type="radio", options=["Idle", "Track", "Record"],
       value="Idle")
ch.add("Quality", type="select", options=["Low", "Medium", "High"],
       value="Medium")
ch.add("Name", type="lineedit", value="openmv-cam")
ch.add("Reset", type="pushbutton")
protocol.register(name="controls", backend=ch)

count = 0
while True:
    if ch["Enable"]:
        count += 1
        ch["Count"] = count
    ch["Status"] = "threshold %d%%" % ch["Threshold"]
    time.sleep_ms(100)

IDE で実行し、フレームバッファの下のペインを チャンネル に切り替えると、ウィジェットが現れます。label は読み取り専用の値(横に unit が付きます)、text は静的なリッチテキストのブロック、toggle はスイッチ、slider と spinbox は min / max / step の中で数値を設定し(精密な入力にはスピンボックス)、radio と select は options から1つを選び、lineedit は自由入力のテキストフィールド、pushbutton は True を伴って on_write を呼ぶ瞬間的なアクションです。スクリプトはいつでも ch["Threshold"] でコントロールを読み戻せます(チャンネルは最新の値を保持します)。on_write は即座の反応が必要なものに使うフックです。スライダーやスピンボックスに (min, max, value) タプルを代入すると、値とともに範囲も移動します。別の設定に上限が依存するセンサーにコントロールを追従させる方法です。

オプションの on_read(channel) コールバックは、チャンネルがホスト向けにシリアライズされる直前に実行されます。センサーをサンプリングしてラベルに入れる場所で、誰かが見ているときだけ読み取りが行われるようにできます:

def on_read(ch):
    ch["FPA Temp"] = round(csi0.ioctl(csi.IOCTL_LEPTON_GET_FPA_TEMP), 1)

File → Examples → 12-Protocol にある controls_channel.py サンプルは、まさにこの形を使って、カメラがストリーミングしている間に FLIR Lepton の測定モード、温度範囲、ミラー / フリップ設定を公開します。

12.8.2. 波形#

waveform フィールドはサンプルレート付きのサンプルのブロックを運びます。サンプルは、フィールドの typecode(デフォルトは符号なし 16 ビット)による array の生バイトです。複数の信号を同時に扱う場合(加速度計の3軸など)は、それらをインターリーブし、series がいくつあるか、何と呼ぶかを指定します:

import math
import time
import protocol
from array import array
from protocol import CBORChannel

RATE = 1000           # samples per second
CHUNK = 100           # samples per update (100 ms)

ch = CBORChannel()
ch.add("Mic", type="waveform", sample_rate=RATE,
       min=0, max=65535)                           # uint16
ch.add("IMU", type="waveform", sample_rate=RATE, series=3,
       typecode="f", min=-2.0, max=2.0,
       options=["X", "Y", "Z"], unit="g")          # float32 x 3
protocol.register(name="signals", backend=ch)

t = 0
while True:
    mic = array("H", (int(32768 + 20000 * math.sin(2 * math.pi * 50 * (t + i) / RATE))
                      for i in range(CHUNK)))
    imu = array("f")
    for i in range(CHUNK):
        phase = 2 * math.pi * 2 * (t + i) / RATE
        imu.extend((math.sin(phase), math.cos(phase), 1.0))
    ch["Mic"] = bytes(mic)    # the raw sample bytes
    ch["IMU"] = bytes(imu)
    t += CHUNK
    time.sleep_ms(100)

IDE は各波形を系列ごとに1本のトレースを持つスクロールグラフとしてプロットし、各チャンクにタイムスタンプを付けるため更新間の空白は空白として表示され、スペクトルビュー、トリガー、マーカー、そして CSV、WAV、NumPy、または Edge Impulse ファイルへの記録を提供します。合成した正弦波を audio のサンプルや IMU ドライバーの読み取り値に置き換えれば、このスクリプトはデータ収集ツールになります。

12.8.3. 深度マップ#

depth フィールドは width x height の 32 ビット浮動小数点のグリッドで、セルごとに1つの距離を持ちます。Time-of-Flight センサーが生成するものです。min / max はホストが色に対応付けるべき範囲を宣言します。IDE はデータから自動的に色の範囲を決めることもできます:

import struct
import time
import tof
import protocol
from protocol import CBORChannel

tof.init()
ch = CBORChannel()
ch.add("depth", type="depth", width=tof.width(), height=tof.height(),
       min=0, max=1000)
reg = protocol.register(name="ToF", backend=ch)

while True:
    try:
        d, dmin, dmax = tof.read_depth(vflip=True, hmirror=True)
    except RuntimeError:
        continue
    ch["depth"] = struct.pack("<%df" % len(d), *d)
    reg.send_event(0xFFFF)
    time.sleep_ms(50)

sensors_channel.py サンプルは、このような depth チャンネルを、同じカメラ上の readings チャンネルと顔検出器と組み合わせています。send_event() の呼び出しは任意です(IDE は自分でチャンネルをポーリングします)が、イベント駆動のホストに対して、問い合わせなしに新しいフレームの準備ができたことを伝えます。

12.8.4. カスタムホストでのデコード#

レコードは標準の CBOR です。各チャンネル読み取りは、SenML の整数キー(0 名前、1 単位、2 数値、3 文字列値、8 データ値、そしてウィジェット型、選択肢、範囲、次元のための負のキー)をキーとするマップの配列を返します。IDE では足りなくなったホストは、channel_read() でチャンネルを読み取り、任意の CBOR ライブラリ(pip install cbor2)でデコードします。コントロールの書き戻しは、channel_write() で送る {0: name, 2: value} マップの CBOR 配列です。protocol.CBORChannel のリファレンスにすべてのキーが一覧されています。とはいえほとんどのプロジェクトでは IDE のチャンネルビューがホストであり、唯一のコードは上記のカメラ側スクリプトです。