12.9. Двунаправленный поток

Каналы не односторонние. Бэкенд, реализующий write, позволяет хосту отправлять байты в камеру, и камера реагирует. Это шаблон, лежащий в основе любого настоящего интерактивного инструмента: оператор поворачивает ручку в графическом интерфейсе хоста, хост записывает новое значение в канал конфигурации, а камера считывает его при следующем захвате.

12.9.1. Канал конфигурации

Добавив к потоковому скрипту на стороне камеры, представьте второй канал для качества JPEG:

class ConfigChannel:
    def __init__(self):
        self.quality = 85

    def size(self):
        return 0

    def read(self, offset, size):
        # Not used for "host writes to cam" -- but the library
        # still needs the method present.
        return b''

    def write(self, offset, data):
        # data is a bytearray view into the protocol buffer.
        # Copy out the contents before doing anything with it.
        new_q = int(bytes(data))
        if 1 <= new_q <= 100:
            self.quality = new_q
        return len(data)

config = ConfigChannel()
protocol.register(name='config', backend=config)

Цикл захвата считывает из config.quality всякий раз, когда сжимает кадр:

while True:
    img = csi0.snapshot()
    latest_jpeg = bytes(
        img.compress(quality=config.quality).bytearray()
    )
    ch.send_event(0x01)

Теперь у хоста есть ручка. Установите её в 50 – и следующий кадр будет меньше (и хуже по качеству); установите её в 95 – и следующий кадр будет больше (и чётче). Камера продолжает захват без перезапуска; хосту не нужно отправлять новый скрипт.

12.9.2. Вызов записи со стороны хоста

На стороне хоста channel_write() отправляет байты в именованный канал:

cam.channel_write('config', b'50')

Хост-SDK кодирует байты в один (или фрагментированный) пакет CHANNEL_WRITE, уровень протокола доставляет его камере, выполняется write(offset=0, data=...) камеры, и сторона камеры подтверждает приём. К моменту возврата вызова камера уже получила и приняла новое значение.

С точки зрения камеры запись атомарна – библиотека протокола гарантирует, что метод write бэкенда выполнится до конца прежде, чем продолжится любая другая операция над этим каналом. Код приложения может читать config.quality изнутри цикла захвата, не беспокоясь о том, что хост вмешается прямо посреди снимка.

12.9.3. Заглушки size и read на канале только для записи

Чистому каналу записи всё равно нужны определённые size и read, даже если это заглушки, возвращающие 0 и b''. Библиотека использует наличие методов для вывода флагов возможностей канала; бэкенд, у которого отсутствует read, не получит установленного флага CHANNEL_FLAG_READ, и хост откажется от попытки чтения.

Однако байты, возвращаемые read на канале только для записи, полезны для другой цели: возврата текущего значения, чтобы только что подключившийся хост мог спросить у камеры «какова текущая настройка?», а не начинать со значения по умолчанию. Чтобы это работало, обе стороны должны договориться о сериализации. Разбор сырых байтов int(bytes(data)) в предыдущем примере работает для одного целочисленного поля, но не масштабируется, как только появляется вторая ручка для настройки. Переключение write на разбор JSON в паре с read, возвращающим соответствующий дамп JSON, превращает канал в настоящее двустороннее хранилище конфигурации:

import json

class ConfigChannel:
    def __init__(self):
        self.quality = 85
        self._buf = b''
    def size(self):
        self._buf = json.dumps({'quality': self.quality}).encode()
        return len(self._buf)
    def read(self, offset, size):
        return self._buf[offset:offset + size]
    def write(self, offset, data):
        new = json.loads(bytes(data))
        if 'quality' in new:
            self.quality = int(new['quality'])
        return len(data)

Теперь хост записывает cam.channel_write('config', b'{"quality": 50}'), чтобы установить значение, и cam.channel_read('config'), чтобы прочитать текущее состояние обратно. Камера сериализует свежий дамп JSON при каждом чтении, так что хост всегда видит самые актуальные значения, а добавление ещё одной ручки (threshold, exposure, orientation) – это одна строка в словаре JSON на каждой стороне.

12.9.4. Полный цикл

С каналом кадров для данных камера → хост, каналом конфигурации для управления хост → камера и небольшим количеством связующего кода приложение становится интерактивным инструментом:

  • Хост открывает камеру, начинает забирать кадры и отображает их в окне.

  • Когда оператор перетаскивает ползунок, хост записывает новое значение в config.

  • Цикл захвата камеры подхватывает значение на следующем кадре.

  • Новые кадры проходят через тот же канал frame.

Это вся модель. Два канала, по два обратных вызова на каждый, цикл захвата на камере, цикл чтения и записи на хосте. Никакой видимой логики кадрирования, никакой видимой обработки ошибок – библиотека протокола делает надёжное перемещение байтов незаметным.

Всё, что находится дальше этой точки, – это код приложения. Добавление третьего канала для гистограммы, четвёртого для телеметрии или пятого для срабатываний датчика – это тот же рецепт «класс бэкенда и protocol.register», повторённый снова. Как только проект камеры достигает этой точки, протокол перестаёт быть интересной проблемой; ею становится собственная логика приложения.