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() раз за цикл, не нужно ничего дополнительного.
Для программ, которые проводят минуты без другого ввода-вывода, 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, когда камере нечего сообщить.