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 nexthas_channel()lookup finds it.CHANNEL_UNREGISTERED– a channel disappeared.SOFT_REBOOT– the cam rebooted on its own (watchdog, hard fault, intentionalmachine.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.