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() – это производитель: получив запрошенные хостом offset и size, он возвращает байты (или строку, которую кодирует слой протокола). 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 готов, он вызывает channel_read() без size, чтобы извлечь всё, что доступно. Метод TicksChannel.poll() камеры возвращает True при каждой проверке, поэтому канал всегда «готов», и хост получает свежее значение тиков при каждом опросе.

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-соединение для данных, не являющихся ни кадрами, ни выводом: счётчики телеметрии, параметры конфигурации, передаваемые вживую из интерфейса на хосте, команды управления, отправляемые в обратном направлении, результаты измерения, вычисленного камерой, которые не вписываются в «изобразительное» обрамление канала stream. Слой протокола берёт на себя обрамление, фрагментацию, подтверждение и повторы; скрипту нужно лишь реализовать бэкенд из четырёх методов, а хосту – лишь знать имя канала и форму данных.

Флаг CLI --channel NAME – быстрый способ проверить пользовательский канал из терминала без написания программы на стороне хоста: CLI опрашивает указанный канал и выводит первые десять байт каждого обновления.

Ограничение на размер одного вызова channel_read() или channel_write() – это согласованное протоколом значение max_payload, по умолчанию 4096 байт. Методы на стороне хоста автоматически разбивают более крупные записи на нужное число пакетов, так что приложение может передавать буферы произвольного размера; фрагментация невидима.