13.3.1.5. Події

Попередні сторінки викликають дії на камері: завантажити скрипт, зчитати кадр, записати в канал. Кожна з цих операцій ініціюється хостом — хост запитує, камера відповідає. Протокол також працює у зворотному напрямку. Камера може надсилати події хосту без запиту, і SDK хоста доставляє кожну з них у зворотний виклик, який застосунок може перевизначити.

Це правильний інструмент, коли застосунок хоче реагувати на щось, що камера помітила до того, як він запитав. Без подій єдиний спосіб дізнатися — постійно викликати read_status() у циклі.

13.3.1.5.1. Стандартний зворотний виклик

Camera вже підписується на події внутрішньо. _handle_event() — це зворотний виклик, який транспорт запускає щоразу, коли надходить пакет події. Стандартний обробляє три системні події:

  • CHANNEL_REGISTERED — на камері після підключення хоста з’явився новий канал. Фреймворк оновлює кеш каналів, щоб наступний пошук has_channel() його знайшов.

  • CHANNEL_UNREGISTERED — канал зник.

  • SOFT_REBOOT — камера самостійно перезавантажилась (сторожовий таймер, апаратна помилка, навмисний machine.reset()).

Також відстежується подія готовності кадру каналу stream для шляху трансляції та подія запуску/зупинки скрипту каналу stdin для буферизації stdout. Стандартне значення конструктора events=True зберігає все це увімкненим; застосунок, якому нічого з цього не потрібно, може передати events=False до Camera, і підсистема подій залишиться тихою.

13.3.1.5.2. Підклас для реагування

Щоб обробляти специфічні для застосунку події, які піднімає камера, створіть підклас Camera і перевизначте _handle_event(). Спочатку викличте батьківський метод, щоб зберегти стандартну поведінку, а потім обробіть події, які цікавлять застосунок:

from openmv import Camera

class MyCamera(Camera):
    def _handle_event(self, channel_id, event):
        super()._handle_event(channel_id, event)

        name = self.channels_by_id.get(
            channel_id, {}).get('name')
        if name == 'motion' and event == 1:
            self.on_motion()

    def on_motion(self):
        print("motion detected")

Сигнатура — (channel_id, event). channel_id дорівнює 0 для системних подій і є числовим ID каналу, який її підняв, для інших; event — це ціле число, обране скриптом на стороні камери. Перелік EventType дає імена трьом системним подіям; події каналу використовують будь-які значення, визначені бекендом на стороні камери.

Події каналу повертаються з ключем за числовим ID, а не за іменем. Словник channels_by_id у кеші — це те, що наведене вище перевизначення використовує для пошуку імені; channels_by_name є його дзеркалом із ключем у зворотному напрямку.

13.3.1.5.3. Половина на стороні камери

Скрипт на стороні камери піднімає подію, викликаючи send_event() на дескрипторі, поверненому з protocol.register()

import protocol

class MotionChannel:
    def size(self):
        return 0

    def read(self, offset, size):
        return b''

    def poll(self):
        return False

ch = protocol.register(
    name='motion', backend=MotionChannel())

while True:
    if detect_motion():
        ch.send_event(1)

Номер події — це ціле число, обране застосунком. Будь-яке значення, яке готове обробити перевизначення хоста, є прийнятним; рівень протоколу сприймає його як непрозрате корисне навантаження. За замовчуванням виклик запускається без очікування; передайте wait_ack=True, щоб заблокувати виконання до підтвердження хостом, якщо впевненість у доставці події важливіша за затримку обміну.

Канал, який лише піднімає події і не несе зчитуваних даних, є допустимим шаблоном — size повертає 0, а read повертає порожні байти. Бібліотека протоколу все одно вимагає наявності обох методів, щоб позначити канал як зчитуваний; скрипт на стороні камери просто ніколи не поміщає в нього дані.

13.3.1.5.4. Управління шляхом отримання в режимі очікування

Події надходять по тому самому з’єднанню, що й все інше, тому будь-який виклик хоста, що надсилає або отримує байти, дає транспорту можливість обробляти очікуючі події вбудовано. Цикл опитування, який вже викликає read_status() або read_frame() раз за цикл, не потребує нічого додаткового.

Для програм, що тривалий час не виконують інший I/O, poll_events() запускає шлях отримання один раз без відправлення команди. Він повертається, щойно вхідний буфер порожній, тому щільний цикл навколо нього — або короткий таймер у циклі подій GUI — підтримує обробники реактивними.

13.3.1.5.5. Повний цикл

В загальному вигляді шаблон такий: скрипт на стороні камери реєструє канал і викликає send_event(), коли щось відбувається; підклас на стороні хоста перевизначає _handle_event() і обробляє події. Цикл хоста, який нічого не робить, крім обслуговування подій, виглядає так:

with MyCamera('/dev/ttyACM0') as cam:
    cam.stop()
    cam.exec(open('motion_cam.py').read())

    while True:
        cam.poll_events()

Камера знімає, вирішує і піднімає події. Хост перебуває всередині poll_events(), поки не надійде одна, тоді запускається on_motion. read_status() не викликається, коли нічого не відбувається, і жодний кадр не передається через USB, коли камері нема про що звітувати.