12.8. CBORChannel을 이용한 위젯과 원격 측정#

앞의 두 페이지의 백엔드는 원시 바이트를 옮기며, 원시 바이트 채널에는 이를 디코딩할 줄 아는 호스트 프로그램이 필요합니다. 대부분의 경우 캠이 게시하려는 것은 그보다 단순합니다. 이름 붙은 판독값 몇 개, 이름 붙은 컨트롤 몇 개, 파형, 깊이 맵입니다. 그런 경우를 위해 protocol 패키지는 이름 붙은 필드를 보관하고, 이를 SenML 레이아웃의 CBOR 레코드로 직렬화하며, 호스트 쓰기를 다시 필드 값으로 디코딩하는 기성 백엔드인 protocol.CBORChannel을 제공합니다. 그 이점은 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 중 하나를 고르고, 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의 원시 바이트입니다. 여러 신호를 한 번에 – 가속도계의 세 축처럼 – 보내려면 인터리브하고 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비트 float입니다 – 비행 시간 센서가 생성하는 것입니다. 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) 디코딩합니다. 컨트롤을 되돌려 쓰는 것은 channel_write()로 보내는 {0: name, 2: value} 맵의 CBOR 배열입니다. protocol.CBORChannel 레퍼런스에 모든 키가 나열되어 있습니다. 하지만 대부분의 프로젝트에서는 IDE의 채널 뷰가 호스트이고, 유일한 코드는 위의 캠 측 스크립트입니다.