Events
Whenever something happens to the robot, the voice server posts a JSON event to
every subscriber you have configured. A subscriber is any HTTP endpoint that
accepts a POST. This is how a brain — personality, memory, vision — plugs in
without anybody editing anybody else’s source.
Configuration
Section titled “Configuration”In apiConfig.json:
"events": { "enable": true, "urls": ["http://127.0.0.1:8090/v1/events"]}The first URL is the primary subscriber. For a handful of events the server waits for its reply and acts on it. Every other URL is notified asynchronously and its response is discarded.
Delivery is deliberately weak: one retry after 500 ms, then the event is dropped and logged. Events never block the audio or gRPC path, and the outbound queue holds 256. Losing an event must never cost the robot a syllable.
The envelope
Section titled “The envelope”{ "event": "sensor.pet", "ts": "2026-09-02T19:40:12.345Z", "esn": "005037a5", "data": { }}esn is the robot’s serial number, so one server can carry several robots.
data is per-event and may be absent.
Voice and touch
Section titled “Voice and touch”| Event | Fires when | data |
|---|---|---|
wake_word | the robot opens a voice stream, from the wake word or the back button | — |
transcript | speech-to-text finished | text |
intent.matched | an intent was chosen for the utterance | intent, text, params |
response.start | the server starts a language-model answer | text |
response.end | that answer finished, spoken or interrupted | — |
tts.start / tts.end | the robot speaks a line through the say helper | text on start |
button.pressed | the back button interrupted a response | — |
sensor.pickup / sensor.putdown | he was lifted, or set down | — |
sensor.pet | the touch sensor became active | — |
sensor.charger | his docked state changed | on (bool) |
face.seen | the camera recognised, or failed to recognise, a face; at most one per face per five seconds | face_id, name (empty means a stranger) |
Idle chatter is driven off these: face.seen means someone is about,
sensor.charger means he is docked, and the voice events are how the brain
knows not to talk over a conversation in progress.
The cube
Section titled “The cube”| Event | Fires when | data |
|---|---|---|
cube.tapped | his cube was tapped | object_id, factory_id, robot_ts |
cube.moved | the cube started moving after five seconds of stillness — not every jolt | object_id, factory_id, robot_ts |
cube.up_axis | the cube was turned onto a different face | up_axis, object_id, robot_ts |
cube.connected / cube.disconnected | the Bluetooth link to the cube came up or went down | object_id, factory_id |
These come from a cube watch that is only open while something is using the cube. Any cube call from the dashboard starts it, and it closes ten minutes after the last one. Over MQTT they become a connected sensor, a moving sensor, an up-axis sensor and a Home Assistant device trigger, so a tap can start an automation.
What he can see
Section titled “What he can see”While a vision session is running — the live view in the dashboard, or the always-on detectors in newer EmberOS builds — the robot’s own detections are forwarded as events too. With no session running, nothing is emitted.
Rate limits are per robot: faces one per two seconds per face, motion one per five seconds, everything else one per two seconds per kind. Rectangles are given in the 640-pixel-wide camera frame.
| Event | data |
|---|---|
vision.face | id, name, expression, rect {x,y,w,h} |
vision.motion | area as a fraction of the image, centroid {x,y}, ground {x,y} in mm from him when the engine had a ground point |
vision.pet | type (cat or dog), score, rect |
vision.laser | x, y, ground {x,y} |
vision.light | state (illuminated or darkened) |
vision.person / vision.hand | score, area, rect |
Pets, laser, light and salient points need a recent EmberOS build; older robots
produce only vision.face and vision.motion. Cube and charger sightings stay
in the live view and are not forwarded. Over MQTT the vision events go out raw
on the events topic; there are no dedicated Home Assistant entities for them.
What the brain does with them is one setting, off by default: when it is off, every decision is logged and nothing is said. When it is on, and it is not quiet hours and he is not mid-conversation, a known face goes through the ordinary greeting machinery, a cat gets one hello every ten minutes, and an unrecognised person gets a “who’s there?” only if a second setting is also on. Light level is recorded but never spoken. Motion, hands and laser dots are counted and nothing else. The brain answers immediately and reacts in the background, so a greeting’s language-model call never holds up the server’s delivery worker.
Stopping at an edge
Section titled “Stopping at an edge”Recent EmberOS builds report the stop-on-cliff reflex firing while the server holds behaviour control, and the same vision session forwards it. By the time the event arrives the engine has already cancelled whatever was running; the event says why, and whether he reversed off the edge.
| Event | data |
|---|---|
robot.cliff_stopped | cliff_detected_flags (bit 1 front-left, 2 front-right, 4 back-left, 8 back-right), backed_off, backed_off_mm, robot_ts |
backed_off is true only when whoever took control asked for a slow back-off.
The default is to stop dead, which leaves him sitting on the lip of the edge.
Like the vision events this needs a live session, and the filter is only
requested from robots new enough to know the name — older gateways can fault on
a filter they do not recognise.
Social reactions
Section titled “Social reactions”The social engine emits every decision it makes, so with dry run on — which is the default — a whole reaction plays out in the event log and nothing moves. See Social reactions for what the fields mean.
| Event | data |
|---|---|
social.episode.start | id, friend, kind, style, ism, trigger, guessed, dry_run, steps |
social.episode.step | id, i, friend, dry_run, op, and that step’s own fields |
social.episode.end | id, friend, ism, result, ms, steps |
social.skipped | friend, kind, reason, lingered_ms — an approach was judged and nothing ran, and why |
Replies
Section titled “Replies”For sensor.pickup, sensor.putdown, sensor.pet and cube.tapped the server
does not just emit: it waits up to twenty seconds for the primary subscriber’s
JSON reply and acts on it. There is currently one field:
{ "say": "Whoa. A little warning next time." }That line is spoken through behaviour control. An empty object, an empty say,
or any error at all means the robot stays quiet. Cooldowns, not talking over the
user, and persona are the subscriber’s business, not the server’s.
A talk step in a social reaction asks in the same way, sending the friend, the
kind, the style, the reaction and a hint; no answer means a stock line is used.
Anything a subscriber wants to do to the robot beyond speaking goes through the SDK routes instead.
Adding an event
Section titled “Adding an event”Events not listed here do not exist. A new one is a call to events.Emit with a
name, the robot’s serial and a data map, from wherever the thing actually
happens — and a row in this table.