13.4.1.5. 이벤트#

지금까지의 페이지들은 카메라를 호출하는 방식이었습니다: 스크립트 업로드, 프레임 읽기, 채널에 쓰기. 이 모든 작업은 호스트가 시작합니다 – 호스트가 요청하면 카메라가 응답합니다. 프로토콜은 반대 방향으로도 작동합니다. 카메라는 요청받지 않고도 이벤트를 호스트에 푸시할 수 있으며, 호스트 SDK는 각 이벤트를 애플리케이션이 재정의할 수 있는 콜백으로 전달합니다.

이것은 애플리케이션이 카메라가 알아챈 무언가에 대해 요청하기 전에 반응하고자 할 때 적합한 도구입니다. 이벤트가 없다면 알아내는 유일한 방법은 루프에서 read_status()를 계속 호출하는 것뿐입니다.

13.4.1.5.1. 기본 콜백#

Camera는 이미 내부적으로 이벤트를 구독합니다. _handle_event()는 이벤트 패킷이 도착할 때마다 트랜스포트가 실행하는 콜백입니다. 기본 구현은 세 가지 시스템 이벤트를 처리합니다:

  • CHANNEL_REGISTERED – 호스트가 연결된 후 카메라에 새 채널이 나타났습니다. 프레임워크는 채널 캐시를 새로 고쳐 다음 has_channel() 조회에서 이를 찾을 수 있도록 합니다.

  • CHANNEL_UNREGISTERED – 채널이 사라졌습니다.

  • SOFT_REBOOT – 카메라가 스스로 재부팅했습니다(워치독, 하드 폴트, 의도적인 machine.reset()).

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

13.4.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.4.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.4.1.5.4. 유휴 상태에서 수신 경로 구동하기#

이벤트는 다른 모든 것과 동일한 연결로 도착하므로, 바이트를 보내거나 받는 모든 호스트 호출은 트랜스포트가 대기 중인 이벤트를 인라인으로 처리할 기회를 줍니다. 주기마다 이미 read_status()read_frame()를 한 번 호출하는 폴링 루프는 추가로 필요한 것이 없습니다.

다른 I/O 없이 몇 분씩 지나가는 프로그램의 경우, poll_events()는 명령을 보내지 않고 수신 경로를 한 번 실행합니다. 인바운드 버퍼가 비는 즉시 반환되므로, 이를 둘러싼 빠른 루프 – 또는 GUI 이벤트 루프의 짧은 타이머 – 가 핸들러를 반응적으로 유지하는 방법입니다.

13.4.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로 프레임을 가져오지 않습니다.