protocol — Каналы протокола OpenMV#

Модуль protocol предоставляет протокол хоста OpenMV для Python. Он позволяет инициализировать и настраивать стек протокола на стороне прошивки, а также даёт пользовательскому коду регистрировать собственные логические каналы, реализованные объектом Python, который реализует интерфейс канала (read, write, size, poll и т. д.). Именно с этим взаимодействуют настольные сопутствующие инструменты, когда передают данные изображения в потоковом режиме или предоставляют интерактивные виджеты подключённой камере.

Примеры#

Передача изображения RGB565 в потоковом режиме на инструмент хоста с использованием собственного бэкенда, реализующего интерфейс необработанного канала (backend.size(), backend.shape(), backend.poll(), backend.read()):

import csi
import protocol

csi0 = csi.CSI()
csi0.reset()
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.HD)

img = csi0.snapshot()
img_mv = memoryview(img.bytearray())
frame_ready = True


class FrameChannel:
    def size(self):
        return len(img_mv)

    def shape(self):
        return (img.height(), img.width(), len(img_mv))

    def poll(self):
        return frame_ready

    def read(self, offset, size):
        global frame_ready
        end = offset + size
        chunk = img_mv[offset:end]
        if end >= len(img_mv):
            frame_ready = False
        return chunk


protocol.register(name="frame", backend=FrameChannel())

while True:
    if not frame_ready:
        img = csi0.snapshot()
        img_mv = memoryview(img.bytearray())
        frame_ready = True

Соответствующий скрипт на стороне хоста, использующий пакет Python openmv (pip install openmv) для подключения, отправки скрипта на камеру и получения каждого кадра:

import cv2
import numpy as np
from openmv.camera import Camera

# The on-cam script above, stored as a string (or read from a file).
SCRIPT = open("frame_streamer_on_cam.py").read()

with Camera("/dev/ttyACM0", baudrate=921600) as cam:
    cam.stop()
    cam.exec(SCRIPT)

    while True:
        status = cam.read_status()
        if not cam.has_channel("frame") or not status.get("frame"):
            continue

        h, w, size = cam._channel_shape(cam.get_channel(name="frame"))
        if cam.channel_size("frame") < size:
            continue

        data = cam.channel_read("frame", size)
        rgb565 = np.frombuffer(data, dtype="<u2").reshape(h, w)

        # Unpack RGB565 to an HxWx3 uint8 RGB image.
        r = ((rgb565 >> 11) & 0x1F) << 3
        g = ((rgb565 >>  5) & 0x3F) << 2
        b = ( rgb565        & 0x1F) << 3
        frame = np.dstack([r, g, b]).astype(np.uint8)

        # Display with OpenCV (cv2 expects BGR, not RGB).
        cv2.imshow("OpenMV", cv2.cvtColor(frame, cv2.COLOR_RGB2BGR))
        if cv2.waitKey(1) == ord("q"):
            break

cv2.destroyAllWindows()

Замените /dev/ttyACM0 последовательным портом камеры (например, COM3 в Windows). Конструктор openmv.camera.Camera принимает те же параметры протокола, что и init (crc / seq / ack / events / max_payload / max_retry / timeout), когда стек на стороне камеры был перенастроен соответствующим образом.

Функции#

protocol.init(crc: bool = True, seq: bool = True, ack: bool = True, events: bool = True, max_payload: int = ..., rtx_retries: int = 3, rtx_timeout_ms: int = 500, lock_interval_ms: int = 10, poll_ms: int = 0) → None#

Инициализирует (или перенастраивает) стек протокола и регистрирует логические каналы данных по умолчанию (stdin, stdout, stream и, если включён при сборке, profile). Вызывает RuntimeError, если инициализация не удалась. Прошивка загружается с уже запущенным USB-стеком протокола по умолчанию, поэтому вызывать эту функцию нужно только для смены транспорта или переопределения параметров фреймирования по умолчанию. Повторный вызов заново инициализирует стек: предыдущий таймер опроса удаляется, а каналы регистрируются заново.

crc включает проверку CRC для кадров протокола.

seq включает отслеживание порядковых номеров.

ack включает покадровые подтверждения.

events включает уведомления о событиях канала.

max_payload — максимальный размер полезной нагрузки в байтах. Если не указано, используется приведённое ниже значение по умолчанию для каждой камеры; оно вычисляется из размера буфера протокола каждой платы как buffer - 10 (header) - 4 (CRC).

Камера

Размер буфера

Макс. полезная нагрузка

OpenMV Cam M4 (OPENMV2)

512

498

OpenMV Cam M7 (OPENMV3)

512

498

OpenMV Cam H7 (OPENMV4)

512

498

OpenMV Cam H7 Plus (OPENMV4P)

4096

4082

OpenMV Pure Thermal (OPENMVPT)

4096

4082

OpenMV Cam RT1062 (OPENMV_RT1060)

4096

4082

OpenMV Cam N6 (OPENMV_N6)

8192

8178

OpenMV AE3 (OPENMV_AE3)

8192

8178

Arduino Portenta H7 (ARDUINO_PORTENTA_H7)

4096

4082

Arduino Giga (ARDUINO_GIGA)

4096

4082

Arduino Nicla Vision (ARDUINO_NICLA_VISION)

4096

4082

rtx_retries — количество попыток повторной передачи. По умолчанию 3.

rtx_timeout_ms — тайм-аут повторной передачи в миллисекундах (удваивается после каждого тайм-аута). По умолчанию 500.

lock_interval_ms — минимальный интервал блокировки в миллисекундах. По умолчанию 10.

poll_ms — интервал опроса в миллисекундах. 0 (по умолчанию) отключает опрос по таймеру.

protocol.is_active() → bool#

Возвращает True, если хост в данный момент подключён и стек протокола активен, иначе False.

protocol.poll() → int#

Выполняет одну итерацию задачи протокола: вычитывает приёмный буфер транспорта, разбирает все полные кадры, диспетчеризует команды и обслуживает события каналов. Обычно прошивка делает это за вас из USB-прерывания и по таймеру poll_ms, поэтому это нужно только для собственных транспортов, которые не обслуживаются ни тем, ни другим – например, когда poll_ms=0 и транспорт опрашивается из главного цикла скрипта.

Возвращает 0 в случае успеха и -1, если активный транспорт не зарегистрирован или вызов был сделан из контекста прерывания.

protocol.register(name: str, *, backend: object, flags: int = 0) → ProtocolChannel#

Регистрирует объект Python backend как новый логический канал и возвращает дескриптор ProtocolChannel. Доступные методы объекта backend (см. Интерфейс бэкенда ниже) определяют возможности канала; флаги protocol.CHANNEL_FLAG_READ, protocol.CHANNEL_FLAG_WRITE и protocol.CHANNEL_FLAG_LOCK добавляются к flags автоматически, когда соответствующие методы реализованы.

name — имя канала в виде строки. Усекается до размера буфера имени канала в прошивке. Обязательно.

backend — объект Python, реализующий интерфейс бэкенда. Обязательно. Обычно передаётся по ключевому слову (backend=...).

flags — дополнительные биты флагов канала (см. константы CHANNEL_FLAG_*). Необязательно; по умолчанию 0.

Вызывает RuntimeError, если канал не удаётся зарегистрировать (например, нет свободных слотов каналов).

Классы#

class protocol.ProtocolChannel#

Дескриптор, возвращаемый protocol.register. Экземпляры не создаются напрямую.

send_event(event: int, wait_ack: bool = False) → None#

Отправляет хосту уведомление о событии канала.

event — идентификатор события (целое число).

wait_ack — если True, блокирует выполнение до подтверждения события хостом.

Вызывает RuntimeError, если отправка события не удалась.

Интерфейс бэкенда#

Объект бэкенда, передаваемый в protocol.register, может реализовывать любое подмножество следующих методов. К C-уровню протокола подключаются только присутствующие на объекте методы; отсутствующие методы оставляют соответствующую возможность отключённой.

class protocol.backend#

Объект бэкенда канала, передаваемый в protocol.register. Методы ниже описывают необязательный интерфейс, который может реализовывать бэкенд на Python.

init() → object#

Вызывается один раз при инициализации канала. Возвращает любое значение, отличное от None, в случае успеха; исключение или отсутствие возвращаемого значения считается ошибкой.

poll() → bool#

Возвращает True, если канал имеет данные, готовые к чтению хостом.

lock() → bool#

Захватывает канал для передачи. Возвращает True в случае успеха.

unlock() → bool#

Освобождает канал после передачи. Возвращает True в случае успеха.

size() → int#

Возвращает количество байтов, доступных в данный момент для чтения из канала.

shape() → tuple#

Возвращает кортеж из не более чем четырёх целых чисел, описывающих форму данных (например, размеры изображения). Уровень протокола использует не более четырёх элементов.

flush() → object#

Сбрасывает все ожидающие данные. Возвращает любое значение, отличное от None, в случае успеха.

read(offset: int, size: int) → bytes#

Возвращает до size байтов начиная со смещения offset в виде bytes-подобного объекта, поддерживающего протокол буфера.

readp(offset: int, size: int) → bytes#

Вариант read без копирования. Возвращает буфер, базовая память которого читается напрямую уровнем протокола; буфер должен оставаться действительным в течение всей передачи.

write(offset: int, data: bytearray) → int#

Записывает data по смещению offset. data — это bytearray, ссылающийся непосредственно на C-буфер. Возвращает количество записанных байтов или 0 при успехе по умолчанию.

ioctl(cmd: int, length: int, arg: bytearray | None) → int#

Обрабатывает ioctl. arg равен None, если length равно нулю, иначе это bytearray, ссылающийся на C-буфер. Возвращает 0 или None в случае успеха либо отрицательное целое число при ошибке.

is_active() → bool#

Для транспортных каналов возвращает True, если базовый транспорт в данный момент подключён.

class protocol.CBORChannel(on_read: Callable | None = None, on_write: Callable | None = None)#

Более высокоуровневый бэкенд на Python (предоставляемый замороженным пакетом protocol), который сериализует именованные поля в CBOR с использованием SenML-совместимых целочисленных ключей. Поддерживает виджеты отображения (label, text, depth, waveform) и интерактивные элементы управления (toggle, pushbutton, slider, spinbox, select, radio, lineedit) с обратными вызовами on_read/on_write.

on_read — необязательный вызываемый объект on_read(channel), вызываемый перед сериализацией канала для хоста. Используйте его для обновления значений полей.

on_write — необязательный вызываемый объект on_write(channel, name, value), вызываемый, когда хост записывает новое значение для именованного поля.

add(name: str, type: str, value: Any = None, unit: str | None = None, min: int | float | None = None, max: int | float | None = None, step: int | float | None = None, options: list | None = None, width: int | None = None, height: int | None = None, sample_rate: float | None = None, series: int = 1, typecode: str | None = None) → None#

Добавляет именованное поле в канал.

name — отображаемое имя; должно быть уникальным в пределах этого канала.

type – тип виджета:

  • "label" – числовой индикатор только для чтения с необязательной единицей измерения unit.

  • "text" – строковый индикатор только для чтения.

  • "toggle" – логический переключатель.

  • "pushbutton" – одномоментное действие (запуск, калибровка, …). Хост записывает True при нажатии и не отображает состояние.

  • "slider" – числовой элемент управления с min/max/step.

  • "spinbox" – точный пошаговый числовой ввод; использует те же поля, что и slider, и ту же форму обновления кортежем (min, max, value).

  • "select" – выпадающий список строк options.

  • "radio" – options, отображаемые как группа радиокнопок.

  • "lineedit" – записываемое строковое поле ввода.

  • "depth" – отображение двумерной карты глубины размером width x height.

  • "waveform" – одномерный график временного ряда из чередующихся отсчётов.

value – начальное значение. Значение по умолчанию зависит от type: 0 для label, "" для text/lineedit/select/ radio, False для toggle/pushbutton, min для slider/spinbox.

unit – строка единицы измерения для label/slider/spinbox/ waveform (например, "Cel", "%RH").

min – минимальное значение (диапазон slider/spinbox или диапазон отображения depth/waveform).

max – максимальное значение (диапазон slider/spinbox или диапазон отображения depth/waveform).

step – размер шага (slider/spinbox).

options – список строк вариантов (select/radio) либо список имён серий (waveform).

width — ширина в пикселях (depth).

height — высота в пикселях (depth).

sample_rate – частота дискретизации в отсчётах в секунду (waveform). Передаётся хосту как период дискретизации.

series – количество чередующихся серий в каждом блоке отсчётов (waveform), например 3 для трассы акселерометра XYZ.

typecode – код типа array для отсчётов waveform (например, "H", "h", "f"). По умолчанию "H" (беззнаковое 16-битное).

__getitem__(name: str) → object#

Возвращает текущее значение именованного поля. Для полей depth и waveform возвращается буфер двоичных данных, иначе – скалярное значение.

__setitem__(name: str, value: Any) → None#

Устанавливает значение именованного поля. Для полей slider и spinbox кортеж (min, max, value) одновременно обновляет диапазон и текущее значение. Для полей depth value – это буфер двоичных данных. Для полей waveform value – это байтоподобный блок чередующихся отсчётов в typecode поля; количество отсчётов на серию вычисляется из его длины, и к нему прикрепляется монотонная метка времени (в микросекундах, накапливаемая из time.ticks_us()), чтобы хост мог разместить фрагмент на своей оси времени.

poll() → bool#

Метод интерфейса бэкенда. Возвращает True, когда для хоста доступны сериализованные данные.

size() → int#

Метод интерфейса бэкенда. Вызывает on_read (если задан) и возвращает размер сериализованного буфера.

read(offset: int, size: int) → bytes#

Метод интерфейса бэкенда. Возвращает срез сериализованного буфера.

write(offset: int, data: bytearray) → int#

Метод интерфейса бэкенда. Декодирует список обновлений CBOR и применяет значения к соответствующим именованным полям, вызывая on_write для каждого.

Константы#

Биты флагов канала (объединяются побитово; передаются в protocol.register через flags или устанавливаются автоматически на основе методов бэкенда).

protocol.CHANNEL_FLAG_READ: int#

Канал поддерживает чтение.

protocol.CHANNEL_FLAG_WRITE: int#

Канал поддерживает запись.

protocol.CHANNEL_FLAG_LOCK: int#

Канал реализует lock/unlock.

protocol.CHANNEL_FLAG_PHYSICAL: int#

Канал представляет физический транспорт (в отличие от логического канала данных).

Встроенные идентификаторы каналов.

protocol.CHANNEL_ID_TRANSPORT: int#

Зарезервированный идентификатор канала для активного транспорта.

protocol.CHANNEL_ID_STDIN: int#

Идентификатор встроенного канала stdin.

protocol.CHANNEL_ID_STDOUT: int#

Идентификатор встроенного канала stdout.

protocol.CHANNEL_ID_STREAM: int#

Идентификатор встроенного канала stream.

protocol.CHANNEL_ID_PROFILE: int#

Идентификатор встроенного канала профилировщика (присутствует только когда прошивка собрана с включённым профилировщиком).