13.3.1.4. 커스텀 채널

채널은 카메라 측 스크립트와 호스트 사이의 이름이 지정된 양방향 바이트 스트림입니다. 카메라는 채널을 등록하고 데이터를 생성하거나 소비하는 콜백을 제공하며, 호스트는 이름으로 해당 채널을 읽고 씁니다. 패키지가 프레임을 전달하는 stream 채널, 스크립트 출력을 전달하는 stdout 채널, 스크립트 업로드를 전달하는 stdin 채널에 내부적으로 사용하는 것과 동일한 메커니즘이 사용자 스크립트에도 노출됩니다. 따라서 호스트가 필요로 하는 모든 애플리케이션 전용 데이터는 두 번째 프로토콜을 고안할 필요 없이 동일한 USB 연결을 타고 전달될 수 있습니다.

이것은 패키지의 가장 유용한 기능이면서도 표준 문서에서 가장 부실하게 다루는 기능이므로, 이 페이지에서 처음부터 끝까지 차근차근 설명합니다.

13.3.1.4.1. 두 부분

커스텀 채널에는 양쪽에서 협력하는 코드가 필요합니다. 카메라 측 스크립트protocol을 임포트하고, 세 개의 메서드(size(), read(), poll())와 선택적인 write()를 가진 클래스를 정의한 뒤, protocol.register(name=..., backend=...)를 호출하여 선택한 이름으로 채널을 게시합니다:

import protocol
import time

class TicksChannel:
    def size(self):
        return 10

    def read(self, offset, size):
        return f'{time.ticks_ms():010d}'

    def poll(self):
        return True

protocol.register(name='ticks', backend=TicksChannel())

size() 메서드는 채널에 현재 사용 가능한 바이트 수를 반환합니다. read()는 생산자입니다. 호스트가 요청한 offsetsize를 받아 바이트(또는 프로토콜 계층이 인코딩하는 문자열)를 반환합니다. poll()은 읽을 것이 있을 때 True를 반환합니다. 프로토콜 계층은 이를 사용하여 read_status()에서 채널을 준비됨으로 표시합니다.

호스트 측 프로그램은 네 개의 openmv.Camera 메서드를 사용합니다: 채널이 존재하는지 확인하는 has_channel(), 대기 중인 데이터 양을 묻는 channel_size(), 바이트를 꺼내는 channel_read(), 바이트를 밀어 넣는 channel_write(). read_status()는 모든 채널을 한 번에 폴링합니다:

from openmv import Camera

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

    while True:
        status = cam.read_status()

        if status.get('ticks'):
            data = cam.channel_read('ticks')
            print(f"ticks: {data.decode()}")

호스트 루프는 read_status()를 폴링하고, ticks 채널이 준비되면 size 없이 channel_read()를 호출하여 사용 가능한 모든 것을 가져옵니다. 카메라의 TicksChannel.poll()은 매 확인마다 True를 반환하므로 채널은 항상 “준비됨” 상태이며, 호스트는 폴링할 때마다 새로운 tick 값을 받습니다.

13.3.1.4.2. 양방향 채널

데이터를 다시 밀어 넣어야 하는 호스트를 위해, 카메라 측 클래스는 들어오는 바이트를 받아들이는 write() 메서드를 추가합니다:

import protocol

class CommandChannel:
    def __init__(self):
        self.last_command = b''
        self.replied = False

    def size(self):
        return len(self.last_command)

    def read(self, offset, size):
        self.replied = True
        return self.last_command

    def write(self, offset, data):
        self.last_command = b'echo: ' + bytes(data)
        self.replied = False

    def poll(self):
        return not self.replied and len(self.last_command) > 0

protocol.register(name='echo', backend=CommandChannel())

호스트는 channel_write()로 채널에 쓰고, 일반적인 read_status() / channel_read() 패턴을 통해 응답을 읽어옵니다:

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

    cam.channel_write('echo', b'hello')

    while True:
        if cam.read_status().get('echo'):
            print(cam.channel_read('echo').decode())
            break

13.3.1.4.3. 이것이 애플리케이션에 주는 이점

커스텀 채널은 애플리케이션이 프레임이나 출력이 아닌 데이터를 기존 USB 연결로 전달하고자 할 때 적합한 도구입니다: 텔레메트리 카운터, 호스트 UI에서 실시간으로 스트리밍되는 설정 값, 반대 방향으로 전송되는 제어 명령, 카메라가 계산한 측정 결과 중 stream 채널이 가정하는 “이미지” 프레이밍에 맞지 않는 것 등입니다. 프로토콜 계층이 프레이밍, 분할, 확인 응답, 재시도를 처리하므로, 스크립트는 네 메서드로 된 백엔드만 구현하면 되고 호스트는 채널 이름과 데이터 형태만 알면 됩니다.

CLI의 --channel NAME 플래그는 호스트 측 프로그램을 작성하지 않고도 터미널에서 커스텀 채널을 빠르게 검증하는 방법입니다: CLI는 지정된 채널을 폴링하고 각 업데이트의 처음 10바이트를 출력합니다.

단일 channel_read() 또는 channel_write() 호출의 크기 제한은 프로토콜이 협상한 max_payload입니다 – 기본값 4096바이트. 호스트 측 메서드는 더 큰 쓰기를 자동으로 적절한 수의 패킷으로 분할하므로, 애플리케이션은 임의로 큰 버퍼를 전달할 수 있으며 분할은 보이지 않습니다.