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 채널의 프레임 준비 이벤트를, stdout 버퍼링을 위해 stdin 채널의 스크립트 시작/정지를 추적합니다. 생성자의 events=True 기본값은 이 모든 것을 켜둡니다. 이 중 아무것도 원하지 않는 애플리케이션은 Cameraevents=False를 전달할 수 있으며, 그러면 이벤트 서브시스템은 조용히 있습니다.

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. 카메라 측 절반

카메라 측 스크립트는 protocol.register()가 반환한 핸들에서 send_event()를 호출하여 이벤트를 발생시킵니다:

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)

이벤트 번호는 애플리케이션이 선택한 정수입니다. 호스트의 재정의가 처리할 준비가 된 값이라면 무엇이든 사용할 수 있으며, 프로토콜 계층은 이를 불투명한 페이로드로 취급합니다. 기본적으로 호출은 발사 후 잊어버립니다(fire and forget). 왕복 지연 시간보다 이벤트가 도착했는지 아는 것이 더 중요할 때는 wait_ack=True를 전달하여 호스트가 확인할 때까지 차단하세요.

이벤트만 발생시키고 읽을 수 있는 데이터를 전달하지 않는 채널도 유효한 패턴입니다 – size0을 반환하고 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로 프레임을 가져오지 않습니다.