12.6. Именованные каналы

Идентификатор канала в заголовке каждого пакета позволяет до 32 независимым потокам совместно использовать один физический транспорт. Уровень каналов превращает эти числовые идентификаторы в именованные, видимые приложению конечные точки, к которым код хоста может обращаться по строке.

Один транспортный провод слева, расходящийся на четыре именованных канала со стороны камеры -- stdin, stdout, stream и зарегистрированный пользователем канал состояния -- каждый показан как независимый блок.

12.6.1. Четыре встроенных канала

Камера регистрирует четыре канала при загрузке, до запуска какого-либо кода приложения:

  • stdin – байты скрипта, которые хост отправляет камере на выполнение. IDE использует этот канал для отправки редактируемого скрипта; exec() в SDK хоста – это эквивалентный вызов из программы на Python.

  • stdout – байты от вызовов print() на камере и трассировки неперехваченных исключений. Последовательная консоль IDE читает этот канал.

  • stream – канал предпросмотра в реальном времени. IDE извлекает из него кадры JPEG; любой скрипт хоста может делать то же самое с помощью read_frame().

  • profile – события профилировщика, присутствуют только в том случае, если камера была собрана с включённым профилированием. Большинство релизных сборок его не содержат.

Коду приложения редко нужно обращаться к встроенным каналам; интересная работа происходит на каналах, которые приложение регистрирует само.

12.6.2. Регистрация канала

Скрипт на стороне камеры регистрирует новый канал, вызывая protocol.register() с именем и объектом бэкенда на Python:

import json
import protocol
import time

trigger_count = 0

class StatusChannel:
    def size(self):
        # Refresh the snapshot on every host query.
        self._buf = json.dumps({
            'uptime_s': time.ticks_ms() // 1000,
            'triggers': trigger_count,
        }).encode()
        return len(self._buf)

    def read(self, offset, size):
        return self._buf[offset:offset + size]

protocol.register(name='status', backend=StatusChannel())

Методы объекта бэкенда определяют, что может делать канал. Бэкенд только с size и read – это канал данных, доступный только для чтения; добавьте write, и он становится двунаправленным; добавьте poll, и хост сможет спросить, готовы ли новые данные, прежде чем платить за чтение. Выборка данных внутри size – простейший шаблон, когда полезная нагрузка достаточно мала, чтобы поместиться в один фрагмент: буфер генерируется по запросу, никогда не кешируется и не подвержен состоянию гонки. Более крупные полезные нагрузки – кадры изображений, трассировки датчиков – требуют шаблона с фиксацией, который удерживает буфер, пока хост не завершит своё многофрагментное чтение; это рассматривается на примере канала кадров.

Небольшой объём учёта выполняется автоматически:

  • Библиотека назначает следующий свободный идентификатор канала (от 0 до 31).

  • Флаги возможностей выводятся из присутствующих методов: CHANNEL_FLAG_READ, если определён read, CHANNEL_FLAG_WRITE, если определён write, CHANNEL_FLAG_LOCK, если определены lock / unlock.

  • Любому подключённому хосту отправляется пакет события CHANNEL_REGISTERED, чтобы его список каналов обновился.

Возвращаемое значение – дескриптор protocol.ProtocolChannel, который приложение может сохранить. Метод дескриптора send_event() – это хук на стороне камеры для того, чтобы сообщить хосту «на этом канале что-то произошло, но читаемые данные не изменились»: сработал триггер, была нажата кнопка, пройдена контрольная отметка по числу выборок.

12.6.3. Чтение каналов с хоста

SDK хоста поставляется как пакет openmv на PyPI (pip install openmv) и построен на pyserial для транспорта. Его класс openmv.camera.Camera предоставляет именованные каналы камеры через высокоуровневые методы:

from openmv.camera import Camera

with Camera('/dev/ttyACM0', baudrate=921600) as cam:
    cam.update_channels()
    if cam.has_channel('status'):
        size = cam.channel_size('status')
        data = cam.channel_read('status', size)

Предупреждение

Пакет openmv требует CPython 3.12 или новее. В более ранних интерпретаторах отсутствуют возможности, от которых зависит SDK; установите сборку 3.12+ перед pip install openmv.

Несколько моментов, на которые стоит обратить внимание при настройке:

  • Строка последовательного порта – здесь /dev/ttyACM0 – имеет вид COM3 в Windows, /dev/cu.usbmodemXXXX в macOS и /dev/ttyACM* в Linux. Фактический номер зависит от того, как порт камеры был перечислен.

  • Скорость передачи – это магическое значение протокола 921600, которое USB-CDC-стек камеры распознаёт как «этот клиент говорит на протоколе, а не с REPL». Любая другая скорость приводит к откату на обычную последовательную линию.

  • Контекстный менеджер with Camera(...) as cam: открывает транспорт, выполняет PROTO_SYNC, обменивается возможностями и при выходе корректно закрывает порт. Явный вызов update_channels() после входа обновляет локальный список каналов любыми каналами, которые приложение зарегистрировало после загрузки.

channel_size() и channel_read() – основные рабочие методы; channel_write() передаёт буфер на камеру и обратно, если бэкенд имеет метод write; has_channel() – безопасный способ проверить, что имя зарегистрировано, прежде чем использовать его. Имя канала один раз преобразуется в идентификатор канала, назначенный камерой во время register, и используется в каждом пакете с этого момента.

Каждая пара channel_size() / channel_read() стоит двух обращений: один пакет для запроса размера, один для запроса байтов. По USB-CDC оба завершаются примерно за миллисекунду в сумме; по UART тот же обмен занимает дольше пропорционально скорости передачи последовательной линии. Код приложения, который читает в плотном цикле, должен вызывать channel_size() только тогда, когда размер действительно может измениться – для данных фиксированного размера размер из первого вызова можно кешировать.

12.6.4. Независимость между каналами

Три вещи стоит знать о том, как взаимодействуют каналы:

  • Независимое управление потоком. У каждого канала есть собственное состояние ожидающего чтения, собственные данные и собственные функции обратного вызова size / read / write. Длительное чтение на канале stream не блокирует чтение на канале приложения config.

  • Последовательность в пределах канала. В пределах одного канала пакеты доставляются по порядку. Уровень надёжности гарантирует это даже при наличии повторных передач.

  • Общий транспорт, общий бюджет повторных передач. Все каналы используют одну физическую линию, поэтому поток трафика на одном канале замедляет остальные, занимая провод. Механизм CHANNEL_LOCK позволяет одному каналу зарезервировать провод для атомарного многопакетного чтения; бэкенд подключает это, реализуя функции обратного вызова lock / unlock.

Канал – это минимальная область, на которой программа хоста и программа камеры договариваются о взаимодействии. Имя, направленность (чтение, запись или оба), методы обратного вызова на стороне камеры и соответствующие вызовы методов на стороне хоста составляют весь контракт.