Published in openvibe-contracts v0.84.0 (docs/adr/ADR-043-bot-devices-and-control.md), rendered as is.

ADR-043: Bot — devices, pairing, control and safety

Status: Accepted 2026-09-29 (plan track T15, "Bot complete"; owner direction the same evening: openvibe.bot is an open control panel for robots, streaming optional; the first robots are the owner's Adeept 4WD Smart Car for Raspberry Pi and a Cozmo-based robot).

Context and current evidence

Decision

  1. Robots, devices and credentials are separate things. A robot (rob_…) is what the owner sees and shares; a device (dev_…) is a running agent attached to one or more robots (a Pi on a rover; a bridge computer driving a Cozmo; later a phone brain). A device holds one device credential: 32 random bytes, stored hashed, shown once, never in a URL, a log line or a browser, rotatable (the old one keeps working 60 s) and revocable (instant; the device must pair again). A device's camera publishes with its own publish key (per device, rotatable), never the owner's stream key. Stream keys stop being robot credentials.
  2. Pairing is a one-time code. The owner adds a robot; Bot shows an 8-character code (Crockford base32, XXXX-XXXX, 10-minute lifetime, single use, 5 wrong tries end it) as text, as a QR code and inside the one-line installer command. The agent sends {pair: code, agent_version, device_kind, drivers, capabilities} and receives its device id, credential, publish key and the robot's profile. The owner sees the device appear and confirms it.
  3. Three kinds of device connection, one agent. On-board: the agent runs on the robot's own computer (Raspberry Pi; ESP32 through a small library speaking the same protocol). Bridge: the agent runs on a computer next to the robot and reaches it over the robot's own link (Cozmo first through PyCozmo; later Vector, Tello, Sphero, LEGO); a bridge joins the robot's network on one interface and reaches OpenVibe on another. Server-side: Bot itself connects to ONVIF/RTSP cameras (credentials by secret reference, never in the profile). The agent is a Python 3 package (the Pi, PyCozmo and the kit libraries are Python), installed by curl -fsSL https://openvibe.bot/install | sh into a virtual environment with a systemd unit and the hardware watchdog.
  4. Control runs over one outbound WebSocket per device; video over WHIP. The device dials Bot (wss://openvibe.bot/device), authenticates with its credential, and keeps one connection for commands, telemetry and heartbeats; this works behind any home router and on an ESP32. The camera publishes to OpenRe over WHIP; viewers watch through OpenRe. Operators reach Bot over a WebSocket from the panel. A WebRTC data channel may be added later as a second control transport when measured latency asks for it; the message set does not change.
  5. One message set (JSON, fields v, seq, ts on every frame): server→device hello, config (limits, heartbeat, the operator's allowed commands), command (id as an idempotency key, kind drive/actuator/ptz/say/display/halt, value, deadline_ms, operator and role), estop, heartbeat_ack; device→server status, telemetry (≤ 2 Hz), ack/nack (with a fault code), heartbeat, estop_state. Commands are never queued for an offline device and never replayed after a reconnect.
  6. Safety is not optional. Every motion command carries an absolute deadline (default 300 ms, at most the robot's max_command_ms); a held control re-sends every 150 ms; the device stops the motors at the deadline, on a lost heartbeat (1 s interval, stop after 2 missed) and on every disconnect or crash path, without asking the network. The e-stop is latched on the device and in Bot and only the owner clears it. Speed, turn and range limits and time windows are set by the owner; an operator never exceeds them. Cozmo additionally stops on a cliff or pick-up event. The agent never runs or exposes a kit's stock control server (the Adeept admin:123456 port stays off).
  7. Robot profiles, not rows. bot.robot-profile@1 (Contracts): capabilities (drive differential/mecanum, pan-tilt, lift, head, servos, lights, speaker/say, display, sensors, cameras, battery), panel widgets bound to capabilities, limits and the driver mapping. The panel is a function of the profile; a new robot needs a profile, not code. First profiles: adeept.adr036 (ordinary and mecanum wheels), cozmo, camera.onvif, sim.rover (the browser simulator on the openvibe.bot home). Capability and widget names are open strings checked against a registry in code, so a new widget never needs a schema change.
  8. Access. Owner, operators (invited), viewers. A robot is private by default; public control exists only as a timed queue (a visible turn timer, a per-turn command budget, cooldowns, per-role command allowlists) with an owner kill switch. Chat control (Live, openvibe.chat) is a caller of the same gate: Chat or Live sends a command on behalf of the viewer with a service token holding bot.robot.control, and Bot applies the role, the allowlist and the limits.
  9. Everything is recorded. Each command (robot, device, operator principal, kind, result, latency) goes to an audit table kept 30 days; bot.robot.online|offline, bot.command.refused, bot.estop.set|cleared and telemetry summaries are Events; Actor agents drive robots only through a delegated bot.robot.control grant and the owner's approval.
  10. The service. OpenVibe.Bot is its own repository and service on PostgreSQL (the platform chassis: openvibe-sdk db, auth, limits, outbox; openvibe-shared Frame and showcase), public origin openvibe.bot. Capabilities bot.robot.read, bot.robot.manage, bot.robot.control, bot.device.connect. Live's server/controls/, its control tables and the stream-key device path are deleted once the panel embed works on Live channel pages and the existing robot owners have paired again (converting presets to profiles and whitelists to operators).

Alternatives considered

Migration consequences

Rollback

Acceptance tests