12.9. Двостороннє передавання

Канали не є односторонніми. Бекенд, що реалізує write, дозволяє хосту надсилати байти до камери, і камера реагує. Це шаблон, що лежить в основі кожного справжнього інтерактивного інструменту: оператор повертає ручку в GUI хоста, хост записує нове значення в канал конфігурації, камера зчитує його наступного разу, коли робить знімок.

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, навіть якщо вони є заглушками, що повертають 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, повторений. Як тільки проєкт камери досягає цього моменту, протокол перестає бути цікавою проблемою; власна логіка застосунку стає нею.