15.3.1.5. Events

The pages so far call into the cam: upload a script, read a frame, write to a channel. Every one of those operations is host-initiated – the host asks, the cam responds. The protocol also runs in the other direction. The cam can push events to the host without being asked, and the host SDK delivers each one to a callback the application can override.

This is the right tool whenever the application wants to react to something the cam noticed before it asks. Without events, the only way to find out is to keep calling read_status() in a loop.

15.3.1.5.1. The default callback

Camera already subscribes to events internally. _handle_event() is the callback the transport runs whenever an event packet arrives. The default handles three system events:

  • CHANNEL_REGISTERED – a new channel appeared on the cam after the host connected. The framework refreshes its channel cache so the next has_channel() lookup finds it.

  • CHANNEL_UNREGISTERED – a channel disappeared.

  • SOFT_REBOOT – the cam rebooted on its own (watchdog, hard fault, intentional machine.reset()).

It also tracks the stream channel’s frame-ready event for the streaming path and the stdin channel’s script start / stop for stdout buffering. The constructor’s events=True default keeps all of this on; an application that wants none of it can pass events=False to Camera and the event subsystem stays quiet.

15.3.1.5.2. Subclassing to react

To handle application-specific events the cam raises, subclass Camera and override _handle_event(). Call the parent first to keep the default behaviour, then dispatch the events the application cares about:

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")

The signature is (channel_id, event). channel_id is 0 for system events and otherwise the numeric ID of the channel that raised it; event is an integer the cam-side script chose. The EventType enum gives names to the three system events; channel events use whatever values the cam-side backend defines.

Channel events come back keyed by numeric ID, not name. The cached channels_by_id dict is what the override above uses to look up the name; channels_by_name is its mirror, keyed the other way.

15.3.1.5.3. The cam-side half

The cam-side script raises an event by calling send_event() on the handle returned from 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)

The event number is an integer chosen by the application. Any value the host’s override is prepared to handle is fair game; the protocol layer treats it as opaque payload. By default the call fires and forgets; pass wait_ack=True to block until the host acknowledges, when knowing the event landed matters more than the latency of the round trip.

A channel that only fires events and carries no readable data is a valid pattern – size returns 0 and read returns empty bytes. The protocol library still needs both methods present to mark the channel as readable; the cam-side script just never puts data in it.

15.3.1.5.4. Driving the receive path while idle

Events arrive on the same connection as everything else, so any host call that sends or receives bytes gives the transport a chance to process pending events inline. A polling loop that already calls read_status() or read_frame() once per cycle does not need anything extra.

For programs that go minutes without other I/O, poll_events() runs the receive path once without sending a command. It returns as soon as the inbound buffer is empty, so a tight loop around it – or a short timer in a GUI event loop – is what keeps the handlers reactive.

15.3.1.5.5. A complete loop

End to end, the pattern is: the cam-side script registers a channel and calls send_event() when something happens; the host-side subclass overrides _handle_event() and dispatches. A host loop that does nothing but service events looks like:

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

    while True:
        cam.poll_events()

The cam captures, decides, and raises events. The host sits inside poll_events() until one arrives, then on_motion runs. No read_status() call runs when nothing has happened, and no frame is pulled across USB when the cam has nothing to report.