12.8. Widgeti i telemetrija s CBORChannel#

Backendi na prethodne dvije stranice prenose sirove bajtove, a kanal sirovih bajtova treba host program koji ih zna dekodirati. Većinu vremena ono što kamera želi objaviti jednostavnije je od toga: nekoliko imenovanih očitanja, nekoliko imenovanih kontrola, valni oblik, karta dubine. Za taj slučaj paket protocol isporučuje protocol.CBORChannel, gotov backend koji drži imenovana polja, serijalizira ih kao CBOR zapise u SenML rasporedu i dekodira zapisivanja s hosta natrag u vrijednosti polja. Njegova je prednost što ga OpenMV IDE već razumije: registrirajte CBORChannel i prikaz Kanali u IDE-u iscrtava svako polje kao widget uživo – oznake, prekidače, klizače, grafove, karte – bez ikakvog host koda. Isti zapisi su običan CBOR, pa ih prilagođeni host dekodira bilo kojom CBOR bibliotekom kad prerastete 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

Kanal primjera controls_channel.py kako ga IDE iscrtava: očitanja, prekidači i klizači za FLIR Lepton. Svaka kontrola piše natrag u skriptu.#

12.8.1. Očitanja i kontrole#

CBORChannel je rječnik tipiziranih polja. add deklarira svako od njih s nazivom, type widgeta i argumentima koje ta vrsta treba; dodjela u ch["name"] ažurira polje; on_write povratni poziv prima promjene s hosta:

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)

Pokrenite je u IDE-u, prebacite okno ispod međuspremnika slike na Kanali i widgeti se pojavljuju: oznaka je vrijednost samo za čitanje (sa svojim unit pored), text je blok statičnog obogaćenog teksta, toggle je prekidač, slider i spinbox postavljaju broj unutar min / max / step (brojčani okvir za precizan unos), radio i select biraju jednu od options, lineedit je polje za slobodan unos teksta, a pushbutton je trenutna radnja koja poziva on_write s True. Skripta čita svoje kontrole natrag s ch["Threshold"] kad god želi – kanal drži posljednju vrijednost – a on_write je kuka za one kojima treba trenutna reakcija. N-torka (min, max, value) dodijeljena klizaču ili brojčanom okviru pomiče njegov raspon zajedno s vrijednošću, što je način na koji kontrola prati senzor čije granice ovise o drugoj postavci.

Neobavezni on_read(channel) povratni poziv izvodi se neposredno prije serijalizacije kanala za host – mjesto za uzorkovanje senzora u oznaku tako da se očitanje uzima samo kad netko gleda:

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

Primjer controls_channel.py pod File → Examples → 12-Protocol koristi upravo ovaj oblik da izloži način mjerenja, raspon temperature i postavke zrcaljenja / okretanja FLIR Leptona dok kamera struji.

12.8.2. Valni oblici#

Polje waveform nosi blok uzoraka s brzinom uzorkovanja. Uzorci su sirovi bajtovi array polja u typecode tog polja (16-bitni bez predznaka prema zadanom); za više signala odjednom – tri osi akcelerometra – ispreplećite ih i navedite koliko series ima i kako ih nazvati:

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 iscrtava svaki valni oblik kao pomični graf s jednim tragom po nizu, vremenski označava svaki dio podataka tako da se praznine između ažuriranja prikazuju kao praznine, i nudi spektralni prikaz, okidače, markere i snimanje u CSV, WAV, NumPy ili Edge Impulse datoteke. Zamijenite sintetički sinus uzorcima audio ili očitanjima IMU upravljačkog programa i skripta je alat za prikupljanje podataka.

12.8.3. Karte dubine#

Polje depth je mreža width x height 32-bitnih floatova, jedna udaljenost po ćeliji – ono što proizvodi time-of-flight senzor. min / max deklariraju raspon koji host treba mapirati u boju; IDE također može automatski odrediti raspon boja iz podataka:

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)

Primjer sensors_channel.py uparuje ovakav kanal dubine s kanalom očitanja i detektorom lica na istoj kameri. Poziv send_event() je neobavezan – IDE sam ispituje kanale – ali govori hostu vođenom događajima da je nova sličica spremna, a da on ne mora pitati.

12.8.4. Dekodiranje na prilagođenom hostu#

Zapisi su standardni CBOR: svako čitanje kanala vraća polje mapa s cjelobrojnim ključevima SenML-a (0 naziv, 1 jedinica, 2 brojčana vrijednost, 3 vrijednost niza, 8 podatkovna vrijednost te negativni ključevi za vrstu widgeta, opcije, raspon i dimenzije). Host koji je prerastao IDE čita kanal s channel_read() i dekodira ga bilo kojom CBOR bibliotekom (pip install cbor2); zapisivanje kontrole natrag je CBOR polje mapa {0: name, 2: value} poslano s channel_write(). Referenca protocol.CBORChannel navodi svaki ključ. Za većinu projekata, ipak, prikaz Kanali u IDE-u je host, a jedini kod je skripta na strani kamere iznad.