13.3.1.4. Własne kanały

Kanał to nazwany, dwukierunkowy strumień bajtów między skryptem po stronie kamery a hostem. Kamera rejestruje kanał i udostępnia wywołania zwrotne, które produkują lub konsumują dane; host odczytuje z tego kanału i zapisuje do niego po nazwie. Ten sam mechanizm, którego pakiet używa wewnętrznie dla kanału stream przenoszącego ramki, kanału stdout przenoszącego wyjście skryptu oraz kanału stdin przenoszącego przesyłany skrypt, jest udostępniony skryptom użytkownika, więc dowolne dane specyficzne dla aplikacji, których potrzebuje host, mogą korzystać z tego samego połączenia USB bez wymyślania drugiego protokołu.

To najbardziej przydatna funkcja pakietu i zarazem ta, którą standardowa dokumentacja opisuje najsłabiej, dlatego ta strona omawia ją od początku do końca.

13.3.1.4.1. Dwie połówki

Własny kanał wymaga współpracującego kodu po obu stronach. Skrypt po stronie kamery importuje protocol, definiuje klasę z trzema metodami (size(), read(), poll()) oraz opcjonalną metodą write(), a następnie wywołuje protocol.register(name=..., backend=...), aby opublikować kanał pod wybraną nazwą:

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())

Metoda size() zwraca liczbę bajtów aktualnie dostępnych w kanale. read() jest producentem: dla podanego przez hosta przesunięcia offset i żądanego rozmiaru size zwraca bajty (lub łańcuch znaków, który koduje warstwa protokołu). poll() zwraca True, gdy jest coś do odczytania – warstwa protokołu wykorzystuje to do oznaczenia kanału jako gotowego w read_status().

Program po stronie hosta korzysta z czterech metod klasy openmv.Camera: has_channel(), aby sprawdzić, czy kanał istnieje, channel_size(), aby zapytać, ile danych czeka, channel_read(), aby pobrać bajty, oraz channel_write(), aby przesłać bajty. read_status() odpytuje wszystkie kanały naraz:

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()}")

Pętla hosta odpytuje read_status(); gdy kanał ticks jest gotowy, wywołuje channel_read() bez parametru size, aby pobrać wszystko, co jest dostępne. Metoda TicksChannel.poll() kamery zwraca True przy każdym sprawdzeniu, więc kanał jest zawsze „gotowy”, a host otrzymuje świeżą wartość licznika przy każdym odpytaniu.

13.3.1.4.2. Kanał dwukierunkowy

Dla hosta, który musi przesyłać dane z powrotem, klasa po stronie kamery dodaje metodę write() przyjmującą przychodzące bajty:

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())

Host zapisuje do kanału za pomocą channel_write() i odczytuje odpowiedź zwykłym wzorcem 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. Co to daje aplikacji

Własne kanały to właściwe narzędzie, gdy aplikacja chce wykorzystać istniejące połączenie USB do danych niebędących ramkami ani wydrukami: liczniki telemetrii, pokrętła konfiguracyjne przesyłane na żywo z interfejsu na hoście, polecenia sterujące wysyłane w drugą stronę, wyniki pomiaru obliczonego przez kamerę, które nie pasują do ramowania „obrazu” zakładanego przez kanał stream. Warstwa protokołu obsługuje ramowanie, fragmentację, potwierdzanie i ponawianie; skrypt musi jedynie zaimplementować czterometodowy backend, a host musi znać tylko nazwę kanału i kształt danych.

Flaga --channel NAME interfejsu wiersza poleceń to szybki sposób na weryfikację własnego kanału z terminala bez pisania programu po stronie hosta: interfejs odpytuje wskazany kanał i wypisuje pierwszych dziesięć bajtów każdej aktualizacji.

Limit rozmiaru pojedynczego wywołania channel_read() lub channel_write() to wynegocjowany przez protokół max_payload – domyślnie 4096 bajtów. Metody po stronie hosta automatycznie dzielą większe zapisy na odpowiednią liczbę pakietów, więc aplikacja może przekazywać dowolnie duże bufory; fragmentacja jest niewidoczna.