12.8. Віджети та телеметрія з CBORChannel#

Бекенди на попередніх двох сторінках переміщують сирі байти, а каналу сирих байтів потрібна хост-програма, яка вміє їх декодувати. Здебільшого те, що камера хоче публікувати, простіше: кілька іменованих показників, кілька іменованих елементів керування, осцилограма, карта глибини. Для цього випадку пакет protocol постачає protocol.CBORChannel – готовий бекенд, що зберігає іменовані поля, серіалізує їх як записи CBOR у форматі SenML і декодує записи хоста назад у значення полів. Його перевага в тому, що OpenMV IDE вже його розуміє: зареєструйте CBORChannel, і подання «Канали» IDE відобразить кожне поле як живий віджет – мітки, перемикачі, повзунки, графіки, карти – без жодного хост-коду. Ті самі записи – це звичайний CBOR, тож власний хост декодує їх будь-якою бібліотекою CBOR, коли ви переростете IDE.

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

Канал прикладу controls_channel.py, як його відображає IDE: показники, перемикачі та повзунки для 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, lineedit – поле вільного тексту, а pushbutton – миттєва дія, що викликає on_write з True. Скрипт може будь-коли зчитати свої елементи керування через 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)

Приклад controls_channel.py у File → Examples → 12-Protocol використовує саме таку форму, щоб відкрити режим вимірювання, діапазон температур і налаштування дзеркалення / перевертання FLIR Lepton під час потокової передачі камери.

12.8.2. Осцилограми#

Поле waveform несе блок вибірок із частотою дискретизації. Вибірки – це сирі байти array у typecode поля (за замовчуванням беззнакові 16-бітні); для кількох сигналів одночасно – трьох осей акселерометра – чергуйте їх і вкажіть, скільки є 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 малює кожну осцилограму як графік, що прокручується, з одним трасуванням на серію, позначає кожен фрагмент часовою міткою, тож прогалини між оновленнями показуються як прогалини, і пропонує подання спектра, тригери, маркери та запис у файли CSV, WAV, NumPy або Edge Impulse. Замініть синтетичну синусоїду на вибірки audio або показники драйвера IMU – і скрипт стає інструментом збору даних.

12.8.3. Карти глибини#

Поле depth – це сітка width x height 32-бітних чисел з рухомою комою, по одній відстані на комірку – те, що створює сенсор часу прольоту. 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 поєднує такий канал глибини з каналом показників і детектором облич на тій самій камері. Виклик send_event() необов’язковий – IDE опитує канали самостійно – але він повідомляє подієво-орієнтованому хосту, що новий кадр готовий, без потреби запитувати.

12.8.4. Декодування на власному хості#

Записи – це стандартний CBOR: кожне читання каналу повертає масив словників із цілочисельними ключами SenML (0 ім’я, 1 одиниця, 2 числове значення, 3 рядкове значення, 8 значення даних та від’ємні ключі для типу віджета, варіантів, діапазону та розмірів). Хост, що переріс IDE, читає канал через channel_read() і декодує його будь-якою бібліотекою CBOR (pip install cbor2); запис елемента керування назад – це масив CBOR зі словників {0: name, 2: value}, надісланий через channel_write(). Довідка protocol.CBORChannel перелічує кожен ключ. Однак для більшості проєктів хостом є подання «Канали» IDE, а єдиний код – це наведений вище скрипт на боці камери.