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 байт. Методы на стороне хоста автоматически разбивают более крупные записи на нужное число пакетов, так что приложение может передавать буферы произвольного размера; фрагментация невидима.