openvibers/openvibe.link Commits
Words: Stop, not E-STOP (#164)
Changes
@@@ -46,7 +46,7 @@ Install: `curl -fsSL https://openvibe.link/install | sh` (and `--robot/--driver`- **Core Go binary** ([cmd/openvibe-link/](cmd/openvibe-link/main.go), [internal/](internal/)): pairing, the outboundWebSocket, heartbeats/deadman, the safety rules (deadlines, owner limits clamped before any driver sees a value, a-- persisted e-stop, the local kill switch), the plugin supervisor, WHIP video (pion), the system service++ persisted stop, the local kill switch), the plugin supervisor, WHIP video (pion), the system service(systemd/launchd/Windows).- **Plugins** ([plugins/](plugins/), separate processes, JSON lines over stdin/stdout): `dryrun`,`adeept_adr036` (Adeept 4WD), `cozmo` (PyCozmo), `relay` (existing Live hardware clients), `ros2` (rclpy or@@@ -54,7 +54,7 @@ Install: `curl -fsSL https://openvibe.link/install | sh` (and `--robot/--driver`community LOOI driver contract. A plugin stops its own actuators when stdin closes, at a command deadline and after1 s without a heartbeat — the core dying never leaves motors running.- **ESP32 library** ([esp32/](esp32/README.md), `OpenVibeLink`): pairing, control link, drive/actuator commands,-- telemetry, heartbeat, deadman and e-stop; no jobs or video.++ telemetry, heartbeat, deadman and stop; no jobs or video.- **Computer plugin** ([docs/computer.md](docs/computer.md), [internal/agent/](internal/agent/)): `--computerobserve|ask|trusted|full`; runs as the person's account; approvals in the tray / notification / `openvibe-linkapprove`; browser over DevTools `--remote-debugging-pipe` (no port); exact-origin task checks; nonce-fenced page@@@ -77,7 +77,7 @@ Install: `curl -fsSL https://openvibe.link/install | sh` (and `--robot/--driver`## Safety, Egress and Security--- **The kill switch is the promise:** `openvibe-link stop` latches (persisted e-stop); `resume` clears it. Every++- **The kill switch is the promise:** `openvibe-link stop` latches (persisted stop); `resume` clears it. Everyactuator path checks deadlines and clamped owner limits before a driver sees a value; the deadman/heartbeat stopsmotion when the control link is lost.- **No open ports:** the core connects out; the panel is loopback-only with a per-run token; the browser is driven over@@@ -101,7 +101,7 @@ Install: `curl -fsSL https://openvibe.link/install | sh` (and `--robot/--driver`None of its own: people install it on their machines/robots (install script or release binary), it pairs throughOpenVibe.Bot/Network and runs as a system service. There is no database; local state lives in the Link's state--directory (config, e-stop latch, panel.json, usage/reservations, memory/skills settings). Production is a distribution++directory (config, stop latch, panel.json, usage/reservations, memory/skills settings). Production is a distributionmatter, not a service deploy.## Key Pitfalls@@@ -111,7 +111,7 @@ matter, not a service deploy.- **Never let a network role or a remote command turn on a local feature**; the local config is the upper bound.Approvals at `ask` must be answerable (tray/notification/CLI) and deny by default.- **A plugin that keeps moving when its stdin closes is a safety bug.** Every driver must stop on stdin close, deadline-- and 1 s without a heartbeat; keep the persisted e-stop checked on every path.++ and 1 s without a heartbeat; keep the persisted stop checked on every path.- **Computer/browser:** exact-origin checks for task origins, no direct DNS/QUIC/non-proxied WebRTC UDP in the sharedbrowser, nonce-fenced page text, and no port. Do not "simplify" the DevTools pipe into a TCP debug port.- **Egress failure is closed:** a Network/Bot outage must never fall back to direct browsing or a relay that bypasses@@@ -72,7 +72,7 @@ openvibe-link exchange serve --model qwen3:32b --public-url https://gpu.example.- **Core** (Go, one static binary per platform): pairing, one outbound WebSocket to `wss://openvibe.bot/device`(credential in the `Authorization` header only), heartbeats and a deadman, the safety rules (deadlines, owner limits-- clamped before any driver sees a value, a persisted e-stop, the local kill switch), the plugin supervisor, WHIP video++ clamped before any driver sees a value, a persisted stop, the local kill switch), the plugin supervisor, WHIP videopublishing with pion, and the system service (systemd, launchd, Windows).- **Plugins** (separate processes, JSON lines over stdin/stdout): [`dryrun`](plugins/dryrun), the Adeept 4WD Smart CarKit [`adeept_adr036`](plugins/adeept_adr036/README.md), [`cozmo`](plugins/cozmo/README.md) through PyCozmo, and@@@ -81,7 +81,7 @@ openvibe-link exchange serve --model qwen3:32b --public-url https://gpu.example.([docs/hardware-looi.md](docs/hardware-looi.md) is its contract). Each plugin stops its own actuators when stdin closes,at a command's deadline and after 1 s without a heartbeat, so the core dying never leaves motors running.- **ESP32 library** ([`esp32/`](esp32/README.md), `OpenVibeLink`): the device side of the protocol for ESP32 boards —-- pairing, the control link, drive/actuator commands, telemetry, heartbeat, deadman and e-stop — but no jobs or video.++ pairing, the control link, drive/actuator commands, telemetry, heartbeat, deadman and stop — but no jobs or video.**Community robots:** a profile people submit on openvibe.bot (`community.<name>`) is reviewed by an OpenVibe adminand signed by Bot (Ed25519 over its canonical JSON). `openvibe-link profile verify community.<name>` checks that@@@ -16,7 +16,7 @@"LOOI headlights: Bot's explicit on/off custom action reaches the community driver as kind \"headlight\" (never actuator), with provenance, 300 ms lifetime/250 ms deadline, 200 ms original reserve on receipt, expires_at_mono_ns on the shared clock, a 2 requests/second budget shared with local controls, and stage=headlight_pending/headlight_refused ack data","machine access: `openvibe-link access request|list|approve|deny|revoke` asks another paired machine for scoped agent, send, screen, desk, computer or worker use; the target tray answers Allow/Deny, the owner may only narrow the scopes and time, and every relayed use is verified against Network's signed grant, live revocation and the target's own level; the opt-in \"Let my agents talk\" directory and `openvibe-link mcp` (eight agent and machine tools) let local agent CLIs message each other across machines","pairing with a code approved on openvibe.bot (or through Network), one outbound WebSocket, no open ports",-- "safety: deadlines, owner limits clamped before any driver, heartbeats and a deadman, a persisted e-stop and the local kill switch (openvibe-link stop)",++ "safety: deadlines, owner limits clamped before any driver, heartbeats and a deadman, a persisted stop and the local kill switch (openvibe-link stop)","plugins as separate processes (JSON lines): dryrun, adeept_adr036, cozmo, relay and ros2 (rclpy or rosbridge); the community LOOI driver's contract; signed community profiles (openvibe-link profile verify)","WHIP video publishing (pion)","the computer plugin for OpenVibe.Actor: terminal, files, screen, input and its own browser, at the level the owner sets on the machine (observe, ask, trusted, full); approvals in the tray (Linux, Windows), a notification or openvibe-link approve",@@@ -26,7 +26,7 @@"OpenVibe Connect shares (CON-4): `openvibe-link share <port>` serves the share this computer was picked for, with no key; every saved share is in `openvibe-link status` (address, connected or why not, requests) and stops when the Link stops","one-command install on a Raspberry Pi or other Linux robot (I2C, SPI, camera tools, kit detection)","system service on systemd, launchd and Windows (or a Windows sign-in start)",-- "ESP32 library (OpenVibeLink): pairing, control link, drive and actuator commands, telemetry, heartbeat, deadman and e-stop",++ "ESP32 library (OpenVibeLink): pairing, control link, drive and actuator commands, telemetry, heartbeat, deadman and stop","`openvibe-link exchange serve --model <id>`: serve a local Ollama/llama.cpp/vLLM model through an OpenAI-compatible listener and register it on OpenVibe.Exchange's endpoint-seller API with the person's OPENVIBE_DEVELOPER_KEY, then remove the offer on Ctrl-C","Link for Android (v0, android/): the same Go pairing and connection bound with gomobile, a Kotlin + Jetpack Compose app (Pair, Approvals with a fingerprint for money, passwords, deletions and new recipients, Activity, This phone), Stay connected as an opt-in foreground service, and Send to OpenVibe on the share sheet; no phone automation"],@@@ -18,7 +18,7 @@ apk=$(find android/app/build/outputs/apk/debug -name "*.apk" | head -n 1)adb install -r "$apk"for screen in pair approvals camera location notify activity phone; do-- adb shell am force-stop network.openvibe.link || true++ adb shell am forcstop network.openvibe.link || trueadb shell am start -W -n network.openvibe.link/.PreviewActivity --es screen "$screen" >/dev/nullsleep 2adb exec-out screencap -p > "$out/$screen.png"@@@ -26,7 +26,7 @@ for screen in pair approvals camera location notify activity phone; dodone# The launcher entry, as a person sees it first.--adb shell am force-stop network.openvibe.link || true++adb shell am forcstop network.openvibe.link || trueadb shell am start -W -n network.openvibe.link/.MainActivity >/dev/nullsleep 2adb exec-out screencap -p > "$out/app.png"@@@ -9,7 +9,7 @@// openvibe-link uninstall stop and remove the system service// openvibe-link status show the link, the plugins, the stop latch and video// openvibe-link stop local kill switch: stop every actuator and hold it stopped--// openvibe-link resume release the local kill switch (and a server e-stop)++// openvibe-link resume release the local kill switch (and a server stop)// openvibe-link quit end the running node cleanly: every actuator stops, nothing is latched// openvibe-link plugins list plugins and their capabilities// openvibe-link memory scan [--tools a,b] [--project DIR …] [--json]@@@ -809,7 +809,7 @@ func cmdResume(g *globals, stdout io.Writer) error {}chownLikeDir(g.paths.LatchFile(), g.paths.StateDir)}-- fmt.Fprintln(stdout, "Resumed: the local stop and any server e-stop are released. Operators can drive again.")++ fmt.Fprintln(stdout, "Resumed: the local stop and any server stop latch are released. Operators can drive again.")return nil}@@@ -976,7 +976,7 @@ func deviceLine(device, principal, server string) string {func latchText(l safety.LatchState) string {if l.Computer {-- return "STOPPED for computer control (an e-stop on this computer); `openvibe-link resume` on this machine is required"++ return "STOPPED for computer control (a stop on this computer); `openvibe-link resume` on this machine is required"}switch {case l.Local && l.Remote:@@@ -984,7 +984,7 @@ func latchText(l safety.LatchState) string {case l.Local:return "STOPPED locally; `openvibe-link resume` releases it"case l.Remote:-- return "E-STOPPED by the server (" + l.RemoteReason + "); the owner clears it on openvibe.bot"++ return "STOPPED by the server (" + l.RemoteReason + "); the owner clears it on openvibe.bot"}return "clear"}@@@ -55,7 +55,7 @@ a Cozmo-based robot).6. **Safety is not optional.** Every motion command carries a deadline relative to receipt (`deadline_ms`, 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 lostheartbeat (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++ stop is latched on the device and in Bot and only the owner clears it. Speed, turn and range limits and time windowsare set by the owner; an operator never exceeds them. Cozmo additionally stops on a cliff or pick-up event. The agentnever 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,@@@ -108,7 +108,7 @@ a Cozmo-based robot).- Pairing: a code works once, expires after 10 minutes, dies after 5 wrong tries; the credential is never logged orreturned twice; rotation keeps the old credential for 60 s; revocation disconnects the device at once.- Safety: a drive command without a newer one stops the motors at its deadline; killing the network, the WebSocket or-- the agent process stops the motors; the e-stop latches on the device and only the owner clears it; limits cap every++ the agent process stops the motors; the stop latches on the device and only the owner clears it; limits cap everyoperator; Cozmo stops on a cliff event.- Access: a viewer cannot drive a private robot; a queued turn ends on its timer; chat commands pass the same gate.- Profiles: the Adeept and Cozmo profiles validate and render a panel with no robot-specific frontend code.@@@ -42,9 +42,9 @@ mode 0600 and its contents never appear in logs, errors or `status`.- Control link: `wss://openvibe.bot/device`, credential in the `Authorization: Bearer` header of the upgrade (never in theURL). Every frame is JSON with `v` (1), `seq` (per-direction counter), `ts` (ms). Server→device: `hello`, `config`(limits, heartbeat_ms, deadman_ms), `command` (`id`, `kind` drive|actuator|ptz|say|display|halt, `value`, `deadline_ms`,-- `operator`, `role`), `estop` (latched), `heartbeat_ack`. Device→server: `status` (agent version, drivers, faults,-- estop_latched), `telemetry` (≤ 2 Hz: battery, rssi, sensors), `ack` / `nack` (`id`, `fault_code`), `heartbeat` (1 s),-- `estop_state`. A repeated command `id` is answered with the first result and never executed twice. Commands are never++ `operator`, `role`), the old stop-latch frame, `heartbeat_ack`. Device→server: `status` (agent version, drivers, faults,++ and the legacy latch field), `telemetry` (≤ 2 Hz: battery, rssi, sensors), `ack` / `nack` (`id`, `fault_code`), `heartbeat` (1 s),++ the old Stop report. A repeated command `id` is answered with the first result and never executed twice. Commands are neverqueued while offline and never replayed after a reconnect. Reconnect with exponential backoff and jitter.- The service side (OpenVibe.Bot) is being built at the same time; its `docs/protocol.md` will be the exact wire format.Keep every wire detail inside `internal/protocol/` so aligning later is one package. Build a **fake Bot server** in Go@@@ -57,7 +57,7 @@ mode 0600 and its contents never appear in logs, errors or `status`.only the time left, and the plugin stops at that deadline unless a newer command arrived. Held controls are re-sent by the operator every 150 ms.- Stop all actuators on: 2 missed heartbeats, link loss, core shutdown, plugin crash (the core restarts it with backoffand it starts stopped), `openvibe-link stop`, an `estop`.--- Two latches, both persisted across restarts. The **remote** e-stop: a server `estop` sets it, and the owner's `estop`++- Two latches, both persisted across restarts. The **remote** stop: a server `estop` sets it, and the owner's `estop``latched:false` or the local `resume` clears it. The **local** stop (`openvibe-link stop`, or a driver's `local_stop`):only the local `resume` clears it, never the owner. Owner limits (`max_speed`, `max_turn`) clamp every value in the core before it reaches a plugin.- Never run or expose a kit's stock control server (the Adeept kit ships one on 0.0.0.0:8888 with the fixed login@@@ -87,7 +87,7 @@ Raspberry Pi OS Bookworm, Python 3, `adafruit-circuitpython-pca9685`, `adafruit-- **Buzzer**: `gpiozero.TonalBuzzer(18)`; **WS2812 LEDs** over SPI (`spidev`, bus 0 device 0).- **Camera**: Picamera2/libcamera. The core publishes it: prefer `rpicam-vid --codec h264 --inline -o -` piped into theWHIP publisher; fall back to MJPEG frames from the plugin.--- Tests: a fake I²C/GPIO layer so the plugin runs on any machine; assert the deadline stop, stdin-EOF stop, estop, and the++- Tests: a fake I²C/GPIO layer so the plugin runs on any machine; assert the deadline stop, stdin-EOF stop, a local stop, and theexact channel numbers.## Plugin 2: `cozmo` (a bridge plugin)@@@ -169,7 +169,7 @@ llama.cpp's RPC has no authentication of its own, so the plugin never exposes it- **Idle-only and the kill switch hold for the whole pool.** The head asks every member every half second; any memberthat is in use (a game, OBS, the person at the keyboard, another program on the GPU; the pool's own GPU use does notcount), outside its hours, offline or stopped by its kill switch pauses the whole pool, and a member the head stops-- calling pauses itself after three seconds. The kill switch (an emergency stop of any machine) holds the pool until++ calling pauses itself after three seconds. The kill switch (an stop of any machine) holds the pool untilits latch is cleared. Network marks the pool's offer not ready when any machine misses a renewal for ten seconds.### Setting one up@@@ -92,9 +92,9 @@ moment", and the tray and the plugin ask again. Deny always goes through.At most 8 requests wait at once. The panel on openvibe.bot sees `approval` and `approval_done` events, so it can saythat a request is waiting on the machine.--**Kill switch:** `openvibe-link stop` (and an owner's e-stop) drops the plugin to `observe`: what runs is killed, what++**Kill switch:** `openvibe-link stop` (and an owner's stop) drops the plugin to `observe`: what runs is killed, whatwaits is denied, the agent's browser is closed, and the core also refuses `exec`, file writes, `input` and the browser's--acting actions until `openvibe-link resume`. `halt` kills what runs. An e-stop on this computer always needs that local++acting actions until `openvibe-link resume`. `halt` kills what runs. An stop on this computer always needs that localresume: the owner's remote clear (`estop` `latched:false`, or a config without `estop_latched`) releases the robot's remotelatch, never this computer.@@@ -127,7 +127,7 @@ You can watch your machine's screen live on openvibe.bot (**Your machines** →- **You are told.** The desktop says when it starts ("Your screen is live on OpenVibe"), and `openvibe-link status`shows `Screen: live … until …` while it runs.- **It ends** when its time is up, when you press Stop, when capturing fails five times in a row, and at once on-- `openvibe-link stop` or an e-stop. While a stop is latched, a new start is refused.++ `openvibe-link stop` or an stop. While a stop is latched, a new start is refused.- **Levels.** It runs at every level, like a screenshot. To never offer it, set `allow_screen_stream: false`.- **Without Pillow** the capture tool must write JPEG itself: `screencapture`, `grim`, `import` and `scrot` do;`gnome-screenshot` does not.@@@ -211,7 +211,7 @@ An agent can work on a desk of its own instead of your screen (plan track K: Vib`status` and `list` run at every level. What runs on the desk keeps its own rules: input and the browser still act.- **Watching and taking over.** Start the live screen with `"desk": "private"` (your desktop is not told, since nothing ofyours is shown) and drive it the same way.--- **It ends** on `stop`, and at once on `openvibe-link stop` or an e-stop: every process the desk started is ended by++- **It ends** on `stop`, and at once on `openvibe-link stop` or an stop: every process the desk started is ended byhandle, never by name.- **Needs** Linux with `Xvfb` (Debian/Ubuntu: `apt install xvfb`), `xdotool` for input and ImageMagick `import` or `scrot`for screenshots. Not offered elsewhere; `allow_desk: false` turns it off.@@@ -59,7 +59,7 @@ Drive from openvibe.bot; watch `openvibe-link status --json` for telemetry.| Line tracking (GPIO 22/27/17) | `sensors.line` flips over tape | || Buzzer (GPIO 18) / WS2812 (SPI 0.0) | horn + lights widgets work | || Camera (`rpicam-vid --codec h264 --inline -o -` → `internal/video/whip.go`) | panel shows live video (needs the Ops-WHIP row) | |--| Safety | kill Wi-Fi / Ctrl-C / panel e-stop → wheels stop; e-stop stays latched through a restart | |++| Safety | kill Wi-Fi / Ctrl-C / panel stop → wheels stop; stop stays latched through a restart | |Mecanum chassis: re-run with `--driver adeept-mecanum` (same profile `adeept.adr036`, plugin config `wheels: "mecanum"`).@@@ -17,7 +17,7 @@ them per robot.Who may drive is Bot's job, not the driver's: the owner, invited operators, an optional public queue with timedturns, cooldowns and per-role allowed commands, all enforced by Bot's command gate before a command reaches the Link.--The e-stop is latched in Bot and only the owner clears it. Each command tells the driver who sent it (`cmd.operator`,++Stop is a plain command: Bot sends `halt`, and the tray's own Stop stays local. Each command tells the driver who sent it (`cmd.operator`,`{"subject", "role"}`), and when a queue turn ends or its holder leaves Bot halts the robot before the next persondrives (the driver gets `stop`), so commands from the last driver never come after it([plugins.md](plugins.md#core--plugin)). The owner or an operator driving during someone's turn pauses that turn and@@@ -115,8 +115,8 @@ The driver should send telemetry with `battery` (`{"percent": 0..100}` or `{"vol- **A stop on the robot itself.** If the driver has its own stop (a button, a hotkey, a sensor it trusts), call`self.local_stop("reason")` (or send `{"op": "local_stop", "reason": "…"}`). Every actuator stops at once, theLink latches its **local** stop (persisted across restarts), and the panel shows the robot as stopped on the robot.-- The owner's remote clear does not lift it: only `openvibe-link resume` on the bridge computer does, and then the-- owner clears the panel's e-stop. Running `openvibe-link stop` does the same from a terminal.++ Only `openvibe-link resume` on the bridge computer lifts it. Running `openvibe-link stop` does the same from a++ terminal.## Installing the driver@@@ -276,7 +276,7 @@ console window that shows its log; closing the window stops the Link and every a| `openvibe-link uninstall --user-session --home DIR` | Windows: remove that directory's sign-in start (one per directory) || `openvibe-link status [--json]` | link, stop latch, limits, plugins, video, battery, running jobs || `openvibe-link stop` | **kill switch**: stop every actuator now and hold it stopped |--| `openvibe-link resume` | release the local stop (and a server e-stop) |++| `openvibe-link resume` | release the local stop (and a server stop) || `openvibe-link quit` | end the running Link cleanly: every actuator stops, **nothing is latched**, and it waits until the Link has exited || `openvibe-link plugins [--json]` | plugins and their capabilities |@@@ -301,7 +301,7 @@ that owns the Link's life should run `openvibe-link run --home DIR` itself, with|---------------------------------------|-------------------------------------------------------|-----------------------------------|| `/etc/openvibe-link/config.json` | `…/OpenVibe Link/config.json` | configuration || `/etc/openvibe-link/credential.json` | `…/OpenVibe Link/credential.json` | device or node credential (mode 600) |--| `/var/lib/openvibe-link/latch.json` | `…/OpenVibe Link/latch.json` | e-stop / kill switch latch |++| `/var/lib/openvibe-link/latch.json` | `…/OpenVibe Link/latch.json` | stop / kill switch latch || `/var/lib/openvibe-link/node.sock` | `…/OpenVibe Link/node.sock` | local control socket (mode 600) || `/var/lib/openvibe-link/venv` | `…/OpenVibe Link/venv` | plugin Python environment |@@@ -39,7 +39,7 @@ computer plugin's shared desks) reads the same config as the Link that started iLinux and macOS.A plugin that exits is restarted with exponential backoff (1 s doubling to 30 s, reset after a minute of uptime). It--starts stopped; if the e-stop or the local kill switch is latched, the core sends `estop` right after `hello`.++starts stopped; if a local stop or the kill switch is latched, the core sends `estop` right after `hello` (the old name of a Stop).A plugin that does not describe itself within 15 s, or stops reading stdin, is killed.Probe mode (`hello` with `"probe": true`) is used by `openvibe-link pair` and `openvibe-link plugins`: the plugin@@@ -168,7 +168,7 @@ Asked by the Generic LOOI driver's authors (2026-10-27). Each answer is what Bot- The core passes `provenance` through unchanged.- An operator who repeats their own id gets the first result, and no second command reaches the robot.- A retry under a new id, or a held control re-sent every 150 ms, is a **new** command. It passes the gate again at-- that moment: whose turn it is, the epoch, the e-stop and the limits. Its id, `seq`, `issued_at_ms`, deadline and++ that moment: whose turn it is, the epoch, the stop latch and the limits. Its id, `seq`, `issued_at_ms`, deadline and`turn_epoch` are all new.- So a stamp is always a fresh decision, never an old command made current.3. **Order across a change of driver.** A queue turn can end, its holder can leave, or the owner can take it over. Then:@@@ -193,10 +193,10 @@ Asked by the Generic LOOI driver's authors (2026-10-27). Each answer is what Bot- a plugin missing its reply window.- `estop` stops every actuator **and latches**. The core refuses `drive`, `ptz` and `actuator` itself (`estopped`,`local_stop`), and so does the runtime, until `resume`. The latch:-- - is set by Bot's e-stop (an owner or operator presses it), by `openvibe-link stop`, or by a driver's `local_stop`;++ - is set by an old server stop latch, by `openvibe-link stop`, or by a driver's `local_stop`;- is written to disk, survives a Link restart, and is sent again at once to a plugin that restarts;- is reapplied from Bot's config on every reconnect.-- - Only the owner clears Bot's e-stop. A local stop is cleared only by `openvibe-link resume` on the machine.++ - Bot no longer sets this latch; it sends a plain `halt` instead. A local stop is cleared only by `openvibe-link resume` on the machine, and an old server latch is released by the same `resume`.- A `halt` still passes while latched.- The driver's own stops (a motion deadline passing, a second without a heartbeat, stdin closing) are `stop`s.Heartbeat loss also refuses motion until heartbeats return.@@@ -251,7 +251,7 @@ Asked next by the same authors (2026-10-27, P7 LOOI shadow integration), and cha| `stop` | `link_lost` | the link to Bot dropped (two missed heartbeats, a close) | no || `stop` | `shutdown` | the Link is stopping (service stop, `openvibe-link quit`), or the core is stopping this process | no || `stop` | `timeout` | a command got no answer from the driver within its window | no |-- | `estop` | `remote` | the platform's e-stop (a robot owner or staff), with its `reason` | yes, until the owner clears it |++ | `estop` | `remote` | the platform's stop (a robot owner or staff), with its `reason` | yes, until the owner clears it || `estop` | `local` | `openvibe-link stop` on the device | yes, until `openvibe-link resume` || `estop` | `driver` | a driver's own `local_stop` (this plugin's or another's) | yes, until `openvibe-link resume` || `estop` | `latched` | the latch was already set when the Link or this process started (`replayed: true`, `first_cause` when known) | yes |@@@ -267,8 +267,7 @@ Asked next by the same authors (2026-10-27, P7 LOOI shadow integration), and chatrue; an ordinary halt, a disconnect, a timeout or a handoff only revokes remote motion. Treat `on_resume` asinformation. Never ARM P7, clear its physical latch or restore a remote grant there; the next command still passesevery check. Across a plugin restart the latch arrives again as `estop` `latched` with `replayed: true`, and the halt-- epoch comes in `hello`. Across a Link reconnect the link's loss is a `stop` (`link_lost`); a platform e-stop set-- while offline is applied from Bot's config before any command.++ epoch comes in `hello`. Across a Link reconnect the link's loss is a `stop` (`link_lost`); an old server stop latch set while offline is applied from config before any command.Reviewed again by the same authors against v0.17.0 (2026-10-27, `632207f`), and changed in Link 0.18.0:@@@ -333,9 +332,9 @@ Reviewed again by the same authors against v0.17.0 (2026-10-27, `632207f`), andowner's command.- `on_resume` is information. Its default does nothing, and the runtime re-arms nothing, so P7 is not re-armed andno local grant comes back.-- - **A replayed e-stop comes before any motion.** The core keeps the latch on disk and sets it before it starts any++ - **A replayed stop comes before any motion.** The core keeps the latch on disk and sets it before it starts anyplugin.-- - **Plugin restart.** The process gets `hello` with the latest halt epoch, then the e-stop again (`latched`,++ - **Plugin restart.** The process gets `hello` with the latest halt epoch, then the stop again (`latched`,`replayed: true`, `first_cause`). No command is routed to it before its `ready`, and lines reach it in the orderthey were queued. So the replay arrives before any command, and the runtime refuses motion (`estopped`) fromthen on.@@@ -599,7 +598,7 @@ not its label. Live's `key_held` / `key_released` names are viewer status broadchardware messages; the hardware socket, and so this relay, sends `key_down` / `key_up`. The old `control_id`,`from_user` and `timestamp` fields are unavailable in Bot's Link → plugin command; Live's example bridge reads`from_user` only for logging and defaults it, so hardware clients treat all three as optional. The SDK releases all--held buttons on deadline, missed heartbeat, stop, e-stop, halt and stdin close.++held buttons on deadline, missed heartbeat, stop, halt and stdin close.## Writing a plugin in Python@@@ -633,9 +632,9 @@ A driver with a safety latch of its own tells an ordinary stop from an emergencyplugin is stopped" above):```python-- def on_stop(self, info): # every stop: a halt, a lost link, a deadline, an e-stop…++ def on_stop(self, info): # every stop: a halt, a lost link, a deadline, a stop…self.stop() # always stop every actuator first-- if info.emergency: # the platform's e-stop (remote, local, driver, or replayed as latched)++ if info.emergency: # the platform's stop (remote, local, driver, or replayed as latched)self.hardware_latch.set() # only then latch the hardware's own STOPdef on_resume(self, info): # the owner cleared the platform's latch: information, never a re-arm@@@ -14,7 +14,7 @@ doc are pinned verbatim in [`internal/protocol/testdata/bot`](../internal/protocBot commit). `go test ./internal/protocol ./internal/link` decodes every one of them and drives the fake server(`internal/fakebot`) through pair → connect → command → ack → reconnect with those exact bytes.--An ESP32 library ([`esp32/`](../esp32/README.md), `OpenVibeLink`) implements this device side minus jobs and video: pairing, the control link, drive/actuator/halt, telemetry, heartbeat, deadman and the e-stop.++An ESP32 library ([`esp32/`](../esp32/README.md), `OpenVibeLink`) implements this device side minus jobs and video: pairing, the control link, drive/actuator/halt, telemetry, heartbeat, deadman and the stop.## 1. Pairing@@@ -166,11 +166,11 @@ server's own counter, never the heartbeat's.| `hello` | `session_id`, `device_id`, `robot_ids` [], `server_time` (RFC 3339) — on every authenticated connection || `config` | `heartbeat_ms`, `limits` {`max_speed` 0..1, `max_turn` 0..1, `max_command_ms`, `heartbeat_ms`}, `allowed_commands` [kinds], `estop_latched`, `roles` [ids] — right after `hello` || `command` | `id` (server-minted: the idempotency and ack key), `ref` (the operator's own id, logs only), `kind`, `value`, `deadline_ms` (absolute, motion kinds and `headlight`), `operator` {`subject`, `role`}, `robot_id`; Link only: `target` (plugin name); `headlight` only: `provenance` {`session`, `seq`, `issued_at_ms`, `expires_at_ms`, `clock_uncertainty_ms`} |--| `estop` | `latched` (true: the e-stop latched; false: the owner cleared it), `by`, `at` |++| `estop` | `latched` (true: the stop latched; false: the owner cleared it), `by`, `at` || `heartbeat_ack` | `echo` (the heartbeat's `t`, or null), `t` (the same send time when the server has it), `server_time` (RFC 3339); the Link measures RTT from the echoed send time || `error` | `code`, `detail` — a frame the server refused; logged, never fatal |--`config.estop_latched` is the robot's e-stop as the server holds it: an owner e-stop set while the device was offline++`config.estop_latched` is the robot's stop as the server holds it: an owner stop set while the device was offlinelatches the Link on reconnect, and a clear made while offline releases it. Until a connection's `config` has beenapplied the Link runs nothing but `halt` (`nack not_ready`), so that latch is in force before any motion.`config.allowed_commands` absent allows every kind; present (Bot always sends it, `halt` included) it is the list.@@@ -179,7 +179,7 @@ applied the Link runs nothing but `halt` (`nack not_ready`), so that latch is inthis machine was installed for; the local configuration always wins (a role never installs anything and never turns ona feature the owner turned off here), and a role the Link does not know is ignored.`estop` `latched:false` is the owner's clear: it clears the remote latch only, never the local kill switch. A computer--plugin is the one exception to the remote latch's reach: an e-stop on a computer waits for `openvibe-link resume` on the++plugin is the one exception to the remote latch's reach: an stop on a computer waits for `openvibe-link resume` on themachine, whatever the server clears (docs/computer.md).### Device → server@@@ -192,7 +192,7 @@ machine, whatever the server clears (docs/computer.md).| `nack` | `id`, `fault_code`; Link extra: `message` || `heartbeat` | `t` (send time, Unix ms), `rtt_ms` (the last measured round trip, once there is one; 0 is a real measurement); Link extra: `fleet` [{`agent`, `installed`, `signed_in`, `windows` [{`name`, `used`, `limit`, `resets_at`}]}] — what the machine's coding agents have left (Codes C9c) || `reauth` | `token`: a fresh node token for the same principal (a Network-paired machine only; refused → `error` `bot.reauth_refused`) |--| `estop_state` | `latched` (the device is stopped: remote e-stop or local kill switch), `by` (`device`), `at`; Link extras: `robot_id` (when `hello` named exactly one robot), `local_stop` (local kill switch), `reason` |++| `estop_state` | `latched` (the device is stopped: remote stop or local kill switch), `by` (`device`), `at`; Link extras: `robot_id` (when `hello` named exactly one robot), `local_stop` (local kill switch), `reason` |The first `status` and `estop_state` go out once the connection's `config` has been applied, never right after theupgrade or `hello`; then `status` whenever a plugin changes state or faults and `estop_state` after every `config`,@@@ -282,7 +282,7 @@ What the Link does with a command, in order:3. `halt` → every plugin stops, `ack`.4. No `config` yet on this connection → `nack not_ready`. Kind not in `config.allowed_commands` → `nack not_allowed`.5. `drive`, `ptz`, `actuator`, `button` and `point` while the local kill switch is latched → `nack local_stop`; while-- the remote e-stop is latched → `nack estopped`; past its `deadline_ms` → `nack expired`. `headlight` is checked by++ the remote stop is latched → `nack estopped`; past its `deadline_ms` → `nack expired`. `headlight` is checked byits own provenance/lifetime rules above instead (it is not motion and is not refused by the latch here; the driverapplies STOP and its own preconditions).6. The value is clamped in the core, before any plugin sees it: `throttle`, `x`, `y` to ±`max_speed`; `steer`,@@@ -299,7 +299,7 @@ What the Link does with a command, in order:reply window (an owner's approval on the machine plus the program's own run): absent → 60 s, capped at 10 minutes, no`max_command_ms`. Their value must be a JSON object of at most 384 KB and is not clamped. Running a program andwriting a file (`exec`, `fs` other than `list`/`stat`/`read`) are refused while the local kill switch is latched--(`nack local_stop`) or the remote e-stop is (`nack estopped`); capturing the screen and reading files are not. The++(`nack local_stop`) or the remote stop is (`nack estopped`); capturing the screen and reading files are not. The`computer` plugin applies its owner's control level, and its `ack` carries the answer in `data` (at most 512 KB of JSON,else `nack bad_value`). An `ask`-level command waits for `openvibe-link approve|deny <id>` on the machine; the pluginraises an `approval` event while it waits and `approval_done` when it is answered or expires.@@@ -338,7 +338,7 @@ checked in this order:| `job inputs not supported` | `unsupported` | the job carries `inputs`: this Link cannot stage and check them yet, so it never runs it with them dropped || `missing or invalid args, ttl_ms or limits` | `bad_value` | `args` is not an object, or `ttl_ms` or a limit is missing or below 1 || `class not available` | `unsupported` | the class is valid but this Link does not run it (every class but `function`, `code` and `linux`; every class while the worker is off) |--| `the e-stop or local stop is latched` | `estopped` or `local_stop` | the stop latch is set: no job starts until it clears |++| `the stop or local stop is latched` | `estopped` or `local_stop` | the stop latch is set: no job starts until it clears || `class not available` | `shutting_down` | the Link is stopping || `unknown artifact` | `unsupported` | `artifact` is not a function the owner declared, at that exact version || `worker busy` | `not_ready` | `worker.caps.max_jobs` jobs are already running |@@@ -371,7 +371,7 @@ A Link shutting down sends `job_exit` (`stopped`) also for a job acked but not s| `cancelled` | null | `job_cancel`: the process group is killed; a job cancelled before it started never starts || `ttl` | null | `ttl_ms` (from receipt, clamped to `worker.caps.max_ttl_ms`) ran out, before or while it ran || `limit` | null | `wall_ms`, `cpu_ms` or `mem_bytes` (each clamped to the local cap), more stdout than `max_output_bytes`, or more than 256 KiB of result |--| `stopped` | null | the e-stop or the local stop latched, or the Link shut down |++| `stopped` | null | the stop or the local stop latched, or the Link shut down || `failed` | null | the worker could not run it: the namespaces could not be created, the spawn failed, or its CPU limit could not be set |`job_cancel` for an id that is not running (unknown, refused or ended) and `job_exit_ack` for an unknown id are@@@ -309,7 +309,7 @@ AppArmor; a Link running as root with `run_as` is not affected.Every kill is SIGKILL to the whole process group and to the leader, which is PID 1 of the job's PID namespace, so itsdeath also kills every process the job left behind. The watchdog kills at `ttl_ms` (from receipt), `wall_ms` (from--start), `cpu_ms`, `mem_bytes`, at `max_output_bytes` of stdout, on `job_cancel`, and the moment the e-stop or the local++start), `cpu_ms`, `mem_bytes`, at `max_output_bytes` of stdout, on `job_cancel`, and the moment the stop or the localstop latches (`openvibe-link stop`); while the latch is set every job is refused, and `openvibe-link resume` admitsthem again. A Link shutting down kills its jobs (`stopped`); a job acked but not started yet never starts and gets its`job_exit` (`stopped`) all the same.@@@ -463,7 +463,7 @@ Refused with `nack` (keyed by the job id; the first matching row wins, after the| `class not available` | `unsupported` | the worker is off (disabled, not Linux, probe failed), or no declared entry has the job's class (`function`, `code` or `linux`) || `job inputs not supported` | `unsupported` | the job carries `inputs` and `media` is off: this Link never runs a job with them silently dropped || `net policy not supported` | `unsupported` | the job names a `net` this host cannot enforce (`public` or `openvibe-only` while `egress` is `none`) |--| `the e-stop or local stop is latched` | `estopped` or `local_stop` | the stop latch is set |++| `the stop or local stop is latched` | `estopped` or `local_stop` | the stop latch is set || `class not available` | `shutting_down` | the Link is stopping || `unknown artifact` | `unsupported` | no declared entry of the job's class has that name and exact version || `worker busy` | `not_ready` | `max_jobs` jobs are running |@@@ -476,5 +476,5 @@ Accepted jobs end with `job_exit.reason`:| `cancelled` | `job_cancel`; a job cancelled before it started never starts || `ttl` | `ttl_ms` (clamped) ran out, before or while it ran || `limit` | `wall_ms`, `cpu_ms` (sampled, or `RLIMIT_CPU`'s SIGXCPU) or `mem_bytes` (sampled, or the cgroup's OOM kill) exceeded, stdout past `max_output_bytes`, or a result over 256 KiB (over `media.max_bytes` with `media` on) |--| `stopped` | the e-stop or local stop latched, or the Link shut down |++| `stopped` | the stop or local stop latched, or the Link shut down || `failed` | the job could not run isolated: sandbox, cgroup or namespaces not set up, spawn error, working directory not made, CPU limit not set; never run with less isolation. Also: an input not fetched or not matching its digest (nothing ran), or a large result not uploaded |@@@ -27,7 +27,7 @@ ArduinoJson (7.4.2) and WebSockets (2.6.1). Nothing here has been on real hardwa`ptz`, `say` and `display` are valid protocol kinds this device has no driver for, so they are `nack unsupported`.A command id or kind over 64 bytes is rejected, and the dedup cache answers a repeated id for the spec's 10 minutes(across reconnects) while every cached message stays within a fixed bound: the id and kind are rejected or not-- echoed, and a hostile e-stop reason is truncated before it is copied into an entry.++ echoed, and a hostile stop reason is truncated before it is copied into an entry.- **Telemetry** — `battery`, `voltage`, `rssi` and `sensors`, at most every 500 ms (Bot's cap).- **Heartbeat and deadman** — a `heartbeat` every `config.heartbeat_ms` (capped at 5000 ms so a server cannot stretchthe deadman for hours) once `hello` has arrived, both `heartbeat_ack` shapes (the newer `echo`/`t` and the older@@@ -50,7 +50,7 @@ ArduinoJson (7.4.2) and WebSockets (2.6.1). Nothing here has been on real hardwaends before `hello` as the refused case and logs that the cause is uncertain; see *Limitations* below.- **Frame and memory bounds** — the protocol allows frames up to 1 MiB; this device bounds them at 16 KiB and a JSONdepth of 10. A frame over either bound, or one the JSON parser cannot allocate, stops the motors and drops the link-- (it could have been an e-stop; the reconnect re-reads `config.estop_latched`). A frame that loses fields to a failed++ (it could have been an stop; the reconnect re-reads `config.estop_latched`). A frame that loses fields to a failedallocation is never sent half-formed.## What it does not@@@ -103,7 +103,7 @@ An L298N-class dual H-bridge; change the pins in `Basic.ino` if they clash withThe two enable pins are the `ledc` PWM channels (20 kHz, 8-bit) and the four IN pins the direction. `onDrive` mixes`throttle ± steer` for the two wheels; the core has already clamped them. `onStop` is the only place that zeroes the--motors, and it runs on `halt`, on a motion deadline, on two missed heartbeats, on an e-stop and whenever the link++motors, and it runs on `halt`, on a motion deadline, on two missed heartbeats, on an stop and whenever the linkcloses.## Building the conformance harness@@@ -113,12 +113,12 @@ sh esp32/test/run.sh```It needs `g++` with C++17 and a way to fetch one file (curl, wget or python3). The harness prints one line per--scenario and exits non-zero on a failure. Its scenarios cover the fixtures, the deadman, the e-stop latch (including++scenario and exits non-zero on a failure. Its scenarios cover the fixtures, the deadman, the stop latch (includingwrong-typed frames), clamping, nack codes, job refusals, close codes, heartbeat cadence, a wall clock that stepsbackwards, 64-bit monotonic time across the 32-bit `millis()` wrap, oversize/too-deep/allocation-failing frames,over-long ids and kinds, the dedup cache's 10-minute limit, the heartbeat cap, guarded commands while the link islost, the backoff reset after a stable link, the 10 s floor after a refused reconnect, the pre-hello timer staying--quiet while the transport is down, a hostile e-stop `by` being bounded in the latch and in every cached message, and a++quiet while the transport is down, a hostile stop `by` being bounded in the latch and in every cached message, and acommand or actuator value document that overflows being refused rather than reaching a driver truncated.## Limitations@@@ -127,7 +127,7 @@ command or actuator value document that overflows being refused rather than reac- The library never starts SNTP (step 4). Before the sketch syncs time, `ts`, `estop_state.at` and `heartbeat.t` carrythe monotonic clock rather than Unix milliseconds. The core's timers never read that clock, so safety is unaffected.- NVS is not encrypted by default: the credential and the local kill switch live in flash readable to anyone with-- physical access unless the sketch enables NVS encryption. The remote e-stop is not stored at all; each connection's++ physical access unless the sketch enables NVS encryption. The remote stop is not stored at all; each connection's`config.estop_latched` re-applies it (including after a reboot).- arduinoWebSockets 2.x delivers `WStype_DISCONNECTED` without the WebSocket close code or the upgrade's HTTP status.The transport therefore treats "disconnected before `hello`" as the 4002/401/403 case (10 s floor, logged as an@@@ -9,7 +9,7 @@//// The two enable pins are the ledc PWM channels and the four IN pins the direction. The core clamps every value to the// owner's limits before onDrive sees it, stops the motors when a motion deadline lapses or two heartbeats are missed,--// and stops them on an e-stop or when the link closes. onStop() is the only place that zeroes the motors.++// and stops them on an stop or when the link closes. onStop() is the only place that zeroes the motors.//// Fill in the four defines below. OV_CODE is the 8-character, single-use code openvibe.bot shows for the robot// (XXXX-XXXX, 10 minutes). After the first pairing the device id and credential are kept in NVS, so OV_CODE is only@@@ -2,7 +2,7 @@ name=OpenVibeLinkversion=0.1.0author=OpenVibersmaintainer=OpenVibers <[email protected]>--sentence=ESP32 client for the OpenVibe Bot device protocol: pairing, control link, drive and actuator commands, telemetry, heartbeat, deadman and e-stop.++sentence=ESP32 client for the OpenVibe Bot device protocol: pairing, control link, drive and actuator commands, telemetry, heartbeat, deadman and stop.paragraph=Implements the bot.device-message@1 device side (protocol sections 1-3) minus jobs and video. The protocol core builds without any Arduino headers and is exercised by a desktop conformance harness; the transport adds WiFi, TLS with the ESP32 CA bundle, arduinoWebSockets and NVS.category=Communicationurl=https://github.com/OpenVibers/OpenVibe.Link@@@ -399,7 +399,7 @@ void Core::handle_hello(JsonDocument& doc, int64_t now) {void Core::handle_config(JsonDocument& doc) {// A wrong-typed estop_latched is a malformed frame (Go's json.Unmarshal would reject it whole): apply nothing and-- // keep the latch exactly as it is, rather than reading `"false"` or `0` as false and releasing the e-stop.++ // keep the latch exactly as it is, rather than reading `"false"` or `0` as false and releasing the stop.JsonVariantConst estopv = doc["estop_latched"];if (!estopv.isNull() && !estopv.is<bool>()) {log("warn", "config with a non-boolean estop_latched ignored; the latch is unchanged");@@@ -530,7 +530,7 @@ void Core::handle_command(JsonDocument& doc, int64_t ts, int64_t now) {return;}if (remote_stop_) {-- const std::string msg = remote_reason_.empty() ? std::string("the remote e-stop is latched") : remote_reason_;++ const std::string msg = remote_reason_.empty() ? std::string("the remote stop is latched") : remote_reason_;send_nack(id, kFaultEstopped, msg);remember(id, DedupResult{false, kFaultEstopped, msg});return;@@@ -605,7 +605,7 @@ void Core::handle_estop(JsonDocument& doc) {JsonVariantConst latchedv = doc["latched"];if (!latchedv.isNull() && !latchedv.is<bool>()) {// Wrong type: ignore the frame and keep the latch (Go's json.Unmarshal rejects it too). Reading `"false"` or 0 as-- // false would release the e-stop on a malformed frame.++ // false would release the stop on a malformed frame.log("warn", "estop with a non-boolean latched ignored; the latch is unchanged");return;}@@@ -615,7 +615,7 @@ void Core::handle_estop(JsonDocument& doc) {// The frame can carry up to kMaxFrameBytes, and this string is copied into every dedup entry that answers an// estopped command: bound it so the cache cannot be filled with megabytes.if (by.size() > kMaxEstopReasonBytes) by.resize(kMaxEstopReasonBytes);-- set_remote_stop(true, by.empty() ? std::string("the remote e-stop is latched") : ("by " + by), true);++ set_remote_stop(true, by.empty() ? std::string("the remote stop is latched") : ("by " + by), true);} else {set_remote_stop(false, "", true);}@@@ -910,14 +910,14 @@ void Core::set_remote_stop(bool on, const std::string& reason, bool notify) {remote_reason_ = reason.size() > kMaxEstopReasonBytes ? reason.substr(0, kMaxEstopReasonBytes) : reason;stop_motion();if (notify) {-- log("warn", "e-stop latched: " + remote_reason_); // the bounded copy, so a huge `by` cannot flood the log either++ log("warn", "stop latched: " + remote_reason_); // the bounded copy, so a huge `by` cannot flood the log eithernotify_latch();}} else {remote_stop_ = false;remote_reason_.clear();if (notify) {-- log("warn", "e-stop cleared; motion allowed again");++ log("warn", "stop cleared; motion allowed again");notify_latch();}}@@@ -2,7 +2,7 @@//// This is the whole stateful device side of docs/protocol.md sections 1–3 (minus jobs and video): the frame envelope,// hello/config/command/estop/heartbeat_ack handling, clamping before any driver sees a value, ack/nack, the heartbeat--// cadence and deadman, the motion deadline, the e-stop latch and the close-code policy. It has no Arduino headers:++// cadence and deadman, the motion deadline, the stop latch and the close-code policy. It has no Arduino headers:// it includes only ArduinoJson (v7) and the standard library, so the same file runs on an ESP32 and in the desktop// conformance harness (esp32/test).//@@@ -45,12 +45,12 @@ inline constexpr int64_t kDedupTTLMS = 600000; // a repeated id is answered fo// Resource limits for one inbound frame. The protocol allows frames up to 1 MiB; a device with a few hundred KiB of// heap bounds them far lower. Anything over these limits (or a frame the JSON parser cannot allocate) is treated as a--// frame that might have been an e-stop: motion stops and the link is dropped to re-read config.estop_latched.++// frame that might have been an stop: motion stops and the link is dropped to re-read config.estop_latched.inline constexpr size_t kMaxFrameBytes = 16384;inline constexpr size_t kMaxJsonDepth = 10; // ArduinoJson's own default nesting limit, passed explicitlyinline constexpr size_t kMaxIdBytes = 64; // command ids (and kinds) longer than this are rejected unreadinline constexpr size_t kMaxKindBytes = 64;--// The e-stop reason (`by`, or a cached nack message) is bounded too: an estop frame can carry a `by` up to the frame++// The stop reason (`by`, or a cached nack message) is bounded too: an estop frame can carry a `by` up to the frame// limit, and that string is copied into every dedup entry that answers an estopped command. 64 bytes keeps the cache's// total size small while leaving room for the "by " prefix.inline constexpr size_t kMaxEstopReasonBytes = 64;@@@ -4,11 +4,11 @@// header in esp32/test/.deps (run.sh fetches and checks it). It replays every fixture in// internal/protocol/testdata/bot — each server-to-device frame is decoded and handled, and each device-to-server frame// the core emits is checked for the fixture's keys and value types — and runs the protocol's scenarios (deadman,--// e-stop, clamping, nack codes, a job refusal, close codes, heartbeat cadence, hello/config ordering) plus the safety++// stop, clamping, nack codes, a job refusal, close codes, heartbeat cadence, hello/config ordering) plus the safety// hardening scenarios (a wall clock that steps backwards, 64-bit monotonic time across the millis() wrap, wrong-typed// latch frames, oversize/too-deep/allocation-failing frames, over-long ids and kinds, the dedup cache's ten-minute// limit, the heartbeat cap, guarded commands while the link is lost, the backoff reset, the refused-reconnect floor,--// the pre-hello timer with the transport down, a bounded e-stop reason and an overflowing command value).++// the pre-hello timer with the transport down, a bounded stop reason and an overflowing command value).//// Exit status is non-zero if any scenario fails; every scenario prints one PASS/FAIL line.#include <ArduinoJson.h>@@@ -317,8 +317,8 @@ std::string scenarioEstop(const std::string& hello, const std::string& config) {h.sent.clear();h.core.feed("{\"v\":1,\"seq\":5,\"ts\":" + std::to_string(h.now) +",\"type\":\"estop\",\"latched\":true,\"by\":\"usr_1\",\"at\":\"2026-09-29T19:20:01.200Z\"}");-- REQUIRE(h.core.estopped(), "the e-stop did not latch");-- REQUIRE(!h.stops.empty(), "the e-stop did not stop motion");++ REQUIRE(h.core.estopped(), "the stop did not latch");++ REQUIRE(!h.stops.empty(), "the stop did not stop motion");JsonDocument st;REQUIRE(lastOfType(h.sent, "estop_state", st), "no estop_state on latch");REQUIRE(st["latched"].as<bool>(), "estop_state did not report the latch");@@@ -326,11 +326,11 @@ std::string scenarioEstop(const std::string& hello, const std::string& config) {h.core.feed(commandFrame(id, "drive", "{\"throttle\":0.5,\"steer\":0}", h.now, h.now + 300));std::string fault;std::string message;-- REQUIRE(h.lastNack(id, fault, message), "a drive while e-stopped was not nacked");-- REQUIRE(fault == "estopped", "drive while e-stopped nacked " + fault);-- REQUIRE(h.drives.empty(), "a drive reached the motors while e-stopped");++ REQUIRE(h.lastNack(id, fault, message), "a drive while stopped was not nacked");++ REQUIRE(fault == "estopped", "drive while stopped nacked " + fault);++ REQUIRE(h.drives.empty(), "a drive reached the motors while stopped");h.core.feed("{\"v\":1,\"seq\":6,\"ts\":" + std::to_string(h.now) + ",\"type\":\"estop\",\"latched\":false}");-- REQUIRE(!h.core.estopped(), "the e-stop did not clear");++ REQUIRE(!h.core.estopped(), "the stop did not clear");h.core.feed(commandFrame("cmd_after_clear", "drive", "{\"throttle\":0.5,\"steer\":0}", h.now, h.now + 300));REQUIRE(h.drives.size() == 1, "a drive after the clear did not reach the motors");return "";@@@ -526,7 +526,7 @@ std::string scenarioWrongTypedLatch(const std::string& hello, const std::string&const std::string estop_true = "{\"v\":1,\"seq\":5,\"ts\":" + std::to_string(h.now) +",\"type\":\"estop\",\"latched\":true,\"by\":\"usr_1\"}";h.core.feed(estop_true);-- REQUIRE(h.core.estopped(), "the e-stop did not latch");++ REQUIRE(h.core.estopped(), "the stop did not latch");for (const std::string& raw : {std::string("\"false\""), std::string("0"), std::string("1"), std::string("{}")}) {h.core.feed("{\"v\":1,\"seq\":6,\"ts\":" + std::to_string(h.now) + ",\"type\":\"estop\",\"latched\":" + raw + "}");REQUIRE(h.core.estopped(), "estop latched:" + raw + " released the latch");@@@ -807,7 +807,7 @@ std::string scenarioEstopReasonBound(const std::string& hello, const std::stringconst std::string big_by(16000, 'x'); // under the frame limit, far over the reason caph.core.feed("{\"v\":1,\"seq\":5,\"ts\":" + std::to_string(h.now) +",\"type\":\"estop\",\"latched\":true,\"by\":\"" + big_by + "\"}");-- REQUIRE(h.core.estopped(), "the e-stop with a huge by did not latch");++ REQUIRE(h.core.estopped(), "the stop with a huge by did not latch");JsonDocument st;REQUIRE(lastOfType(h.sent, "estop_state", st), "no estop_state for the huge-by latch");const std::string reason = st["reason"] | "";@@@ -1033,7 +1033,7 @@ int main(int argc, char** argv) {report("the reconnect backoff resets after a stable link", scenarioBackoffReset(hello));report("the 10 s floor after a refused reconnect", scenarioRefusedFloor(hello));report("the pre-hello timer does not fire with the transport down", scenarioPreHelloTimer());-- report("a huge e-stop by is bounded in every cached message", scenarioEstopReasonBound(hello, config));++ report("a huge stop by is bounded in every cached message", scenarioEstopReasonBound(hello, config));report("an overflowing command or actuator value is refused", scenarioValueOverflow(hello, config));if (failures > 0) {@@@ -355,7 +355,7 @@ func (n *Agent) Run(parent context.Context) error {pctx, cancelPlugins := context.WithCancel(context.Background())// A stop latched on disk is set on every plugin before any starts: each process's first lines are then hello and-- // the e-stop replayed (cause latched, first_cause what set it), before it can be routed any command.++ // the stop replayed (cause latched, first_cause what set it), before it can be routed any command.if st := n.latch.State(); st.Stopped() || st.Computer {n.log.Warn("starting with the stop latched", "remote", st.Remote, "local", st.Local, "computer", st.Computer)}@@@ -506,17 +506,17 @@ func (n *Agent) Frame(f protocol.Frame) {}func (n *Agent) remoteEstop(reason, by string) {-- n.log.Warn("e-stop from the server", "by", by)++ n.log.Warn("stop from the server", "by", by)n.mgr.EstopAll(plugins.EstopRemote, safety.CleanReason(reason)) // at once, before the disk writeif _, err := n.latch.SetRemoteWithComputer(reason, n.hasComputer()); err != nil {-- n.log.Error("could not persist the e-stop (it is held in memory)", "err", err)++ n.log.Error("could not persist the stop (it is held in memory)", "err", err)}}func (n *Agent) remoteClear(by string) {-- n.log.Warn("e-stop cleared by the server", "by", by)++ n.log.Warn("stop cleared by the server", "by", by)if _, err := n.latch.ClearRemote(); err != nil {-- n.log.Error("could not persist the e-stop clear", "err", err)++ n.log.Error("could not persist the stop clear", "err", err)}}@@@ -982,7 +982,7 @@ func (n *Agent) admitHeadlight(at mono.Instant) bool {}// computer runs one exec, fs or screen command: no clamping, a long reply window (the server's deadline, at most--// MaxComputerMS, else DefaultComputerMS), refused while the local stop or an e-stop is latched when it would change the++// MaxComputerMS, else DefaultComputerMS), refused while the local stop or an stop is latched when it would change the// machine, and answered with the plugin's data. The plugin applies its owner's control level.func (n *Agent) computer(c protocol.Command, entry *safety.Entry, sentMS int64, received mono.Instant, nack func(code, msg string)) {if len(c.Value) > protocol.MaxComputerValue {@@@ -1009,7 +1009,7 @@ func (n *Agent) computer(c protocol.Command, entry *safety.Entry, sentMS int64,return}if st.Computer {-- nack(protocol.FaultLocalStop, "the emergency stop on this computer needs a local `openvibe-link resume`: observe only")++ nack(protocol.FaultLocalStop, "the stop on this computer needs a local `openvibe-link resume`: observe only")return}if st.Remote {@@@ -1128,7 +1128,7 @@ func latchReason(st safety.LatchState) string {case st.Local:return st.LocalReasondefault:-- return "the computer's emergency stop: `openvibe-link resume` on this machine releases it"++ return "the computer's stop: `openvibe-link resume` on this machine releases it"}}@@@ -391,7 +391,7 @@ func TestMissedHeartbeatsStop(t *testing.T) {}// TestRobotEstopLatchedUntilOwnerClears: the remote latch is the server's to clear, and the owner clears it--// with `estop latched:false`. The computer plugin is the one exception: an e-stop on a computer waits for a++// with `estop latched:false`. The computer plugin is the one exception: an stop on a computer waits for a// local `openvibe-link resume` (TestComputerRemoteStopRequiresLocalResume).func TestRobotEstopLatchedUntilOwnerClears(t *testing.T) {e := start(t)@@@ -411,7 +411,7 @@ func TestRobotEstopLatchedUntilOwnerClears(t *testing.T) {// Persisted: a restarted Agent comes back latched.l, _ := safety.OpenLatch(e.paths.LatchFile())if !l.State().Remote {-- t.Fatal("e-stop not persisted")++ t.Fatal("stop not persisted")}// Survives a reconnect.e.conn.Close()@@@ -685,7 +685,7 @@ func (n *Agent) stopEverything(reason string) error {return err}--// resumeEverything releases the local kill switch (and a server e-stop).++// resumeEverything releases the local kill switch (and a server stop).func (n *Agent) resumeEverything() error {_, err := n.latch.Resume()return err@@@ -23,7 +23,7 @@ import (// with stream "screen"; the core encodes and publishes them on a second WHIP session, apart from a robot's camera.//// A stream ends when its time is up, on stop, when the plugin says it ended, and at once when the local kill switch--// or an e-stop latches; a start is refused while either is latched. One stream at a time: a new start replaces it.++// or an stop latches; a start is refused while either is latched. One stream at a time: a new start replaces it.const (ScreenDefaultSeconds = 600ScreenMinSeconds = 30@@@ -4,9 +4,9 @@// and then authenticates the Authorization header (a bad credential closes with 4002, a revoked device with 4003, a// second connection replaces the first with 4000), sends hello and then config (always with allowed_commands and// the robot's estop_latched), sends commands with a server-minted id, an absolute deadline_ms, the operator and the--// robot, and answers heartbeats. Like Bot, it keeps the robot's e-stop latch: an estop frame it sends sets or clears++// robot, and answers heartbeats. Like Bot, it keeps the robot's stop latch: an estop frame it sends sets or clears// it, a device's estop_state latched:true sets it, and latched:false is only a report. Tests drive it to send--// commands, e-stops, errors and silence, and read back what the device sent.++// commands, stops, errors and silence, and read back what the device sent.//// heartbeat_ack carries Bot's own envelope seq and echoes the heartbeat's t as both t and echo (echo null when the// heartbeat had no t), with server_time as Bot's clock; the device measures RTT from the echoed send time.@@@ -59,7 +59,7 @@ type Server struct {relayOrigin stringrelayAllowed func(string, string, string, string) boolagentResult func(string, string, json.RawMessage)-- estop map[string]bool // robot id → the robot's e-stop latch, as Bot keeps it++ estop map[string]bool // robot id → the robot's stop latch, as Bot keeps itpairRequests []protocol.PairRequestpairRefusal *refusalbindRefusals []refusal@@@ -94,7 +94,7 @@ func (m *Manager) HaltAll(turnEpoch int64) {}}--// EstopAll latches every plugin's e-stop, saying why (EstopRemote, EstopLocal, EstopDriver, EstopLatched).++// EstopAll latches every plugin's stop, saying why (EstopRemote, EstopLocal, EstopDriver, EstopLatched).func (m *Manager) EstopAll(cause, reason string) {for _, p := range m.plugins {p.Estop(cause, reason)@@@ -110,7 +110,7 @@ type Plugin struct {desc *Describefault *protocol.Faultestopped bool-- // The e-stop's cause and reason, sent again (replayed) to a restarted process while the stop is latched.++ // The stop's cause and reason, sent again (replayed) to a restarted process while the stop is latched.estopCause, estopReason string// The newest turn_epoch a halt carried: hello tells a restarted process, so a stale turn stays refused there too.haltEpoch int64@@@ -302,7 +302,7 @@ const (StopLinkLost = "link_lost" // the link to Bot was lostStopShutdown = "shutdown" // the Link is stopping (service stop, `openvibe-link quit`)StopTimeout = "timeout" // a command got no answer from the driver within its window-- EstopRemote = "remote" // an e-stop from the platform (a robot owner or staff)++ EstopRemote = "remote" // an stop from the platform (a robot owner or staff)EstopLocal = "local" // `openvibe-link stop` on the deviceEstopDriver = "driver" // a driver's own local stop (a plugin reported one)EstopLatched = "latched" // the stop was already latched when the Link or this process started@@@ -324,7 +324,7 @@ func (p *Plugin) Halt(turnEpoch int64) {_ = p.send(msg)}--// Estop latches the plugin's e-stop (the core also refuses motion itself), saying why.++// Estop latches the plugin's stop (the core also refuses motion itself), saying why.func (p *Plugin) Estop(cause, reason string) {p.mu.Lock()p.estopped, p.estopCause, p.estopReason = true, cause, reason@@@ -332,7 +332,7 @@ func (p *Plugin) Estop(cause, reason string) {_ = p.send(map[string]any{"op": "estop", "cause": cause, "reason": reason})}--// Resume tells the plugin the platform's e-stop latch was cleared by its owner. It is information, never a re-arm: the++// Resume tells the plugin the platform's stop latch was cleared by its owner. It is information, never a re-arm: the// driver keeps its own hardware latch and grants as they are.func (p *Plugin) Resume() {p.mu.Lock()@@@ -353,7 +353,7 @@ func TestCrashRestartsWithEstop(t *testing.T) {if p.Info().Restarts < 1 {t.Fatal("no restart counted")}-- // After the restart the latched e-stop is sent again right after hello, as a replay of the same emergency.++ // After the restart the latched stop is sent again right after hello, as a replay of the same emergency.l := fx.lines()var afterCrash []stringfor i, s := range l {@@@ -368,7 +368,7 @@ func TestCrashRestartsWithEstop(t *testing.T) {}// A latch set before the manager starts (the Link booting with its stop latched on disk) is the first thing each--// process hears after hello: the e-stop replayed, naming what set it, before the plugin can be routed any command.++// process hears after hello: the stop replayed, naming what set it, before the plugin can be routed any command.func TestLatchSetBeforeStartIsReplayedFirst(t *testing.T) {logPath := filepath.Join(t.TempDir(), "plugin.log")exe, _ := os.Executable()@@@ -36,7 +36,7 @@ const (TypeHello = "hello"TypeConfig = "config"TypeCommand = "command"-- TypeEstop = "estop" // latched true sets the e-stop, false is the owner clearing it++ TypeEstop = "estop" // latched true sets the stop, false is the owner clearing itTypeHeartbeatAck = "heartbeat_ack"TypeError = "error"// TypeVoiceAnswer is Vibe's answer to a voice_ask (contracts 0.214.0): working, answered or refused, to speak.@@@ -134,9 +134,9 @@ const (JobNotAvailable = "class not available"// Refused by a running worker (platform.job@1: a missing artifact, the local kill switch, limits it cannot honor).-- JobUnknownArtifact = "unknown artifact" // not a function declared in the Link's local config-- JobStopped = "the e-stop or local stop is latched" // fault estopped or local_stop-- JobBusy = "worker busy" // worker.caps.max_jobs are running; fault not_ready++ JobUnknownArtifact = "unknown artifact" // not a function declared in the Link's local config++ JobStopped = "the stop or local stop is latched" // fault estopped or local_stop++ JobBusy = "worker busy" // worker.caps.max_jobs are running; fault not_ready)// job_exit reasons.@@@ -222,7 +222,7 @@ const (// ComputerActs reports whether a computer command changes the machine (runs a program, writes a file, uses the mouse// or keyboard, drives the browser, starts or stops the private desk): those are refused while the local stop or an--// e-stop is latched. Capturing the screen, reading files, reading or closing the browser and the desk's status are not.++// stop is latched. Capturing the screen, reading files, reading or closing the browser and the desk's status are not.func ComputerActs(kind string, value json.RawMessage) bool {switch kind {case KindExec, KindInput:@@@ -255,7 +255,7 @@ func ComputerActs(kind string, value json.RawMessage) bool {return false}--// GuardedKinds move something and are refused while an e-stop or the local kill switch is latched.++// GuardedKinds move something and are refused while an stop or the local kill switch is latched.var GuardedKinds = map[string]bool{KindDrive: true, KindActuator: true, KindPTZ: true, KindButton: true, KindPoint: true}// Fault codes carried in nack.fault_code and status.faults[].code.@@@ -264,7 +264,7 @@ const (FaultBadValue = "bad_value" // a value is missing, not a number, or out of shapeFaultUnsupported = "unsupported" // no driver on this device handles the kind or the actuatorFaultNotAllowed = "not_allowed" // the kind is not in config.allowed-- FaultEstopped = "estopped" // the remote e-stop is latched++ FaultEstopped = "estopped" // the remote stop is latchedFaultLocalStop = "local_stop" // the local kill switch (`openvibe-link stop`) is latchedFaultNoHeartbeat = "no_heartbeat" // a plugin lost the core's heartbeatFaultNotReady = "not_ready" // the driver is starting or restarting, or the server's config has not arrived@@@ -402,7 +402,7 @@ type CommandProvenance struct {ClockUncertaintyMS *int64 `json:"clock_uncertainty_ms,omitempty"`}--// Estop is sent when the e-stop latches (Latched true) or the owner clears it (Latched false).++// Estop is sent when the stop latches (Latched true) or the owner clears it (Latched false).type Estop struct {Latched bool `json:"latched"`By string `json:"by,omitempty"`@@@ -7,6 +7,7 @@ import ("os""path/filepath""reflect"++ "regexp""strings""testing")@@@ -485,3 +486,38 @@ func TestHeadlightFrameCarriesBotProvenance(t *testing.T) {t.Fatalf("headlight kind: known=%v guarded=%v computer=%v", known, GuardedKinds[KindHeadlight], ComputerKinds[KindHeadlight])}}++++func TestDocsSayStopNotEStop(t *testing.T) {++ root := filepath.Join("..", "..")++ var files []string++ for _, name := range []string{"AGENTS.md", "README.md", "STATUS.json"} {++ files = append(files, filepath.Join(root, name))++ }++ err := filepath.WalkDir(filepath.Join(root, "docs"), func(path string, d os.DirEntry, err error) error {++ if err != nil {++ return err++ }++ if !d.IsDir() && strings.HasSuffix(path, ".md") {++ files = append(files, path)++ }++ return nil++ })++ if err != nil {++ t.Fatal(err)++ }++ fence := regexp.MustCompile("(?s)```.*?```")++ code := regexp.MustCompile("`[^`]*`")++ word := regexp.MustCompile(`(?i)(^|[^A-Za-z])e-?stop([^A-Za-z]|$)|emergency\s+stop`)++ for _, file := range files {++ raw, err := os.ReadFile(file)++ if err != nil {++ t.Fatal(err)++ }++ text := fence.ReplaceAllString(string(raw), "")++ text = code.ReplaceAllString(text, "")++ if loc := word.FindStringIndex(text); loc != nil {++ line := strings.Count(text[:loc[0]], "\n") + 1++ t.Errorf("%s:%d still says e-stop; these are small hobby robots, write Stop", file, line)++ }++ }++}@@@ -1 +1 @@--{ "v": 1, "seq": 4, "ts": 1738065601001, "type": "error", "code": "bot.forbidden", "detail": "only the owner clears the e-stop" }++{ "v": 1, "seq": 4, "ts": 1738065601001, "type": "error", "code": "bot.forbidden", "detail": "only the owner clears the stop" }@@@ -1,4 +1,4 @@--// Package safety holds the rules that keep motors from running when they should not: the persisted e-stop and local++// Package safety holds the rules that keep motors from running when they should not: the persisted stop and local// kill-switch latch, the owner's limits applied to every value before it reaches a plugin, deadline capping, and the// command-id cache that makes a repeated command answer with its first result instead of running twice.package safety@@@ -14,7 +14,7 @@ import ("time")--// LatchState is the persisted e-stop state. Two independent latches:++// LatchState is the persisted stop state. Two independent latches://// - Remote: set by a server `estop`; cleared by a server `estop` with latched:false (the owner) or a local `resume`.// - Local: set on the machine itself — `openvibe-link stop`, or a driver's own stop (`local_stop`); cleared only by@@@ -36,7 +36,7 @@ type LatchState struct {// so a remote clear can still release robot plugins.func (s LatchState) Stopped() bool { return s.Remote || s.Local }--// Latch is the persisted latch. It survives restarts: a Link that was e-stopped comes back e-stopped.++// Latch is the persisted latch. It survives restarts: a Link that was stopped comes back stopped.type Latch struct {mu sync.Mutexpath string@@@ -134,7 +134,7 @@ func (l *Latch) persistLocked() error {return nil}--// SetRemote latches the server e-stop.++// SetRemote latches the server stop.func (l *Latch) SetRemote(reason string) (LatchState, error) {return l.SetRemoteWithComputer(reason, false)}@@@ -150,7 +150,7 @@ func (l *Latch) SetRemoteWithComputer(reason string, computer bool) (LatchState,})}--// ClearRemote clears the server e-stop (the owner's estop latched:false). The local latch is untouched.++// ClearRemote clears the server stop (the owner's estop latched:false). The local latch is untouched.func (l *Latch) ClearRemote() (LatchState, error) {return l.update(func(s *LatchState) { s.Remote, s.RemoteReason, s.RemoteAt = false, "", time.Time{} })}@@@ -261,7 +261,7 @@ func (w *Worker) ExitAck(id string) {}}--// SetStopped follows the stop latch (the e-stop or the local stop). A fault code kills every job at once (job_exit++// SetStopped follows the stop latch (the stop or the local stop). A fault code kills every job at once (job_exit// says stopped) and refuses new ones with that fault until SetStopped("").func (w *Worker) SetStopped(fault string) {w.mu.Lock()@@@ -1,7 +1,7 @@# openvibe-adeept-adr036OpenVibe Link plugin for the **Adeept 4WD Smart Car Kit for Raspberry Pi (ADR036)**, ordinary or mecanum wheels,--on Raspberry Pi OS Bookworm. Built on `openvibe_plugin` (deadlines, heartbeat loss, stdin EOF, e-stop and halt all stop++on Raspberry Pi OS Bookworm. Built on `openvibe_plugin` (deadlines, heartbeat loss, stdin EOF, stop and halt all stopthe wheels). The mapping was checked against Adeept's `Server/Server_*Wheels/Move.py` and `Voltage.py`; none of thekit's code is imported.@@@ -618,7 +618,7 @@ class Ai(Plugin):self.pool_stopped.wait(2)def on_stop(self, info):-- """The kill switch on this machine (an emergency stop) stops the whole pool now and holds it until the latch is++ """The kill switch on this machine (an stop) stops the whole pool now and holds it until the latch iscleared; the Link stopping closes it and keeps the key. A turn's halt, a lost link or a command's timeout is notabout the pool. Without a pool, every stop is as before (nothing moves here)."""if not self.cfg.get("pool_directory"):@@@ -11,7 +11,7 @@ command can never raise it:* ``trusted``: run programs, write files inside the roots and use the mouse and keyboard without asking.* ``full``: as trusted, anywhere the account running the Node can reach.--The local kill switch (``openvibe-link stop``) and a remote e-stop drop the plugin to observe, kill what is running++The local kill switch (``openvibe-link stop``) and a remote stop drop the plugin to observe, kill what is runningand deny what waits; the core refuses acting commands while either is latched too. The plugin runs as the Node's ownaccount and never as root, except on a shared desk host for other people, where it runs as root and every desk sits inits own sandbox.@@@ -32,7 +32,7 @@ Command kinds and their answers (the ack's ``data``):most 5) with the same tools as ``screen`` and hands each to the core as video (``stream: "screen"``), which publishesit to the owner's private stream; the core alone holds the WHIP URL and key. It runs at every level, like ascreenshot, says so on the desktop when it starts (config ``notify``), and ends when its time is up (30 s to 30 min),-- on stop, and at once on the local kill switch or an e-stop. Config ``allow_screen_stream: false`` turns it off.++ on stop, and at once on the local kill switch or an stop. Config ``allow_screen_stream: false`` turns it off.* ``input`` ``{"action", "x"?, "y"?, "width"?, "height"?, "dy"?, "text"?, "keys"?}`` → ``{done, action, x?, y?}``: move,click, double_click or right_click at a point, scroll by ``dy`` clicks, type ``text`` into whatever has focus, or pressone key or chord (``keys``, for example ``ctrl+s``). ``x``/``y`` are in the space of a screen picture ``width`` ×@@@ -564,13 +564,13 @@ class Computer(Plugin):# ---- safety ----------------------------------------------------------------------------------------------------def effective_level(self):-- """The level now: observe while a local stop or an e-stop is latched."""++ """The level now: observe while a local stop or an stop is latched."""if self.runtime is not None and self.runtime.estopped:return "observe"return self.leveldef stop(self):-- """halt, stop, the local kill switch and an e-stop: kill what runs, deny what waits."""++ """halt, stop, the local kill switch and an stop: kill what runs, deny what waits."""with self.mu:# The generation rises before anything is killed: a closure authorized before this stop fails its check.self.generation += 1@@@ -1012,7 +1012,7 @@ class Computer(Plugin):if RANK[level] < RANK["ask"]:raise Fault("not_allowed", "this machine allows observing only")# The Node has opened and checked the single-use encrypted Companion grant. At level ask this is the-- # owner's approval; observe-only and the Node's emergency stop still refuse it.++ # owner's approval; observe-only and the Node's stop still refuse it.return self._act(cmd, "trusted", run, summary, threaded=True)if acting:return self._act(cmd, level, run, summary, threaded=True)@@@ -3,7 +3,7 @@This plugin drives a ROS 2 robot from OpenVibe.Bot: TurtleBot 3 or 4, a Jackal, a home-built base, anything that takes`geometry_msgs/msg/Twist` on a `cmd_vel` topic. The Link runs on the robot's computer, or on a computer on the samenetwork. The plugin turns Bot's commands into ROS 2 messages, and the robot's topics into telemetry and camera frames.--Bot's gate (roles, the owner's limits, cooldowns and the e-stop) has already judged every command before it gets here.++Bot's gate (roles, the owner's limits, cooldowns and the stop) has already judged every command before it gets here.## What it bridges@@@ -7,7 +7,7 @@ says how to move its hardware:* describe first, touch hardware only after ``hello`` (and never in probe mode);* stop every actuator when stdin closes, when a motion command's deadline passes without a newer command, when noheartbeat has arrived from the core for ``HEARTBEAT_TIMEOUT_S``, and on ``stop`` / ``estop``;--* keep an e-stop latched until the core sends ``resume``.++* keep an stop latched until the core sends ``resume``.See docs/plugins.md for the wire format."""@@@ -34,7 +34,7 @@ MAX_LONG_DEADLINE_MS = 600000# Kinds that move something continuously and therefore stop at their deadline. A plugin may widen this set# (Plugin.motion_kinds) for actuators it drives by speed rather than by position.MOTION_KINDS = frozenset({"drive"})--# Kinds refused while an e-stop is latched or the heartbeat is lost. halt is always accepted.++# Kinds refused while an stop is latched or the heartbeat is lost. halt is always accepted.GUARDED_KINDS = frozenset({"drive", "ptz", "actuator"})@@@ -191,7 +191,7 @@ class StopInfo:ends with, so all actuators start stopped), "deadline" (a motion command'sown deadline passed; motion_only is True: stop what moves on its own), "heartbeat" (the core went quiet),"handler_error", "eof". An older core sends no cause: "stop".-- emergency is True for the platform's e-stop, with cause "remote" (a robot owner or staff), "local"++ emergency is True for the platform's stop, with cause "remote" (a robot owner or staff), "local"(`openvibe-link stop` on the device), "driver" (a driver's own local stop) or "latched" (the stop was alreadylatched when this process or the Node started: replayed is True and first_cause is the original cause when the coreknows it); an older core sends "estop". reason is the latch's reason text, when there is one.@@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"[project]name = "openvibe-plugin"version = "0.36.0"--description = "Runtime for OpenVibe Link plugins (JSON lines over stdin/stdout, deadlines, heartbeat, e-stop)"++description = "Runtime for OpenVibe Link plugins (JSON lines over stdin/stdout, deadlines, heartbeat, stop)"requires-python = ">=3.9"license = { text = "AGPL-3.0-or-later" }@@@ -422,7 +422,7 @@ class RuntimeTest(unittest.TestCase):self.assertGreater(p.handled[0].deadline_ms, 0)def test_a_replayed_estop_is_applied_before_any_motion(self):-- # A restarted process hears, in this order: hello (with the newest halt epoch), the latched e-stop replayed,++ # A restarted process hears, in this order: hello (with the newest halt epoch), the latched stop replayed,# and only then commands (the core routes none to it before its ready, and lines reach it in the order they# were queued). No motion handler runs while the replayed latch holds; resume is information only.rt, p, out, clock = make()
@@ -46,7 +46,7 @@ Install: `curl -fsSL https://openvibe.link/install | sh` (and `--robot/--driver`
@@ -46,7 +46,7 @@ Install: `curl -fsSL https://openvibe.link/install | sh` (and `--robot/--driver`
- **Core Go binary** ([cmd/openvibe-link/](cmd/openvibe-link/main.go), [internal/](internal/)): pairing, the outbound
- **Core Go binary** ([cmd/openvibe-link/](cmd/openvibe-link/main.go), [internal/](internal/)): pairing, the outbound
WebSocket, heartbeats/deadman, the safety rules (deadlines, owner limits clamped before any driver sees a value, a
WebSocket, heartbeats/deadman, the safety rules (deadlines, owner limits clamped before any driver sees a value, a
- persisted e-stop, the local kill switch), the plugin supervisor, WHIP video (pion), the system service
+ persisted stop, the local kill switch), the plugin supervisor, WHIP video (pion), the system service
(systemd/launchd/Windows).
(systemd/launchd/Windows).
- **Plugins** ([plugins/](plugins/), separate processes, JSON lines over stdin/stdout): `dryrun`,
- **Plugins** ([plugins/](plugins/), separate processes, JSON lines over stdin/stdout): `dryrun`,
`adeept_adr036` (Adeept 4WD), `cozmo` (PyCozmo), `relay` (existing Live hardware clients), `ros2` (rclpy or
`adeept_adr036` (Adeept 4WD), `cozmo` (PyCozmo), `relay` (existing Live hardware clients), `ros2` (rclpy or
@@ -54,7 +54,7 @@ Install: `curl -fsSL https://openvibe.link/install | sh` (and `--robot/--driver`
@@ -54,7 +54,7 @@ Install: `curl -fsSL https://openvibe.link/install | sh` (and `--robot/--driver`
community LOOI driver contract. A plugin stops its own actuators when stdin closes, at a command deadline and after
community LOOI driver contract. A plugin stops its own actuators when stdin closes, at a command deadline and after
1 s without a heartbeat — the core dying never leaves motors running.
1 s without a heartbeat — the core dying never leaves motors running.
- **ESP32 library** ([esp32/](esp32/README.md), `OpenVibeLink`): pairing, control link, drive/actuator commands,
- **ESP32 library** ([esp32/](esp32/README.md), `OpenVibeLink`): pairing, control link, drive/actuator commands,
- telemetry, heartbeat, deadman and e-stop; no jobs or video.
+ telemetry, heartbeat, deadman and stop; no jobs or video.
- **Computer plugin** ([docs/computer.md](docs/computer.md), [internal/agent/](internal/agent/)): `--computer
- **Computer plugin** ([docs/computer.md](docs/computer.md), [internal/agent/](internal/agent/)): `--computer
observe|ask|trusted|full`; runs as the person's account; approvals in the tray / notification / `openvibe-link
observe|ask|trusted|full`; runs as the person's account; approvals in the tray / notification / `openvibe-link
approve`; browser over DevTools `--remote-debugging-pipe` (no port); exact-origin task checks; nonce-fenced page
approve`; browser over DevTools `--remote-debugging-pipe` (no port); exact-origin task checks; nonce-fenced page
@@ -77,7 +77,7 @@ Install: `curl -fsSL https://openvibe.link/install | sh` (and `--robot/--driver`
@@ -77,7 +77,7 @@ Install: `curl -fsSL https://openvibe.link/install | sh` (and `--robot/--driver`
## Safety, Egress and Security
## Safety, Egress and Security
-- **The kill switch is the promise:** `openvibe-link stop` latches (persisted e-stop); `resume` clears it. Every
+- **The kill switch is the promise:** `openvibe-link stop` latches (persisted stop); `resume` clears it. Every
actuator path checks deadlines and clamped owner limits before a driver sees a value; the deadman/heartbeat stops
actuator path checks deadlines and clamped owner limits before a driver sees a value; the deadman/heartbeat stops
motion when the control link is lost.
motion when the control link is lost.
- **No open ports:** the core connects out; the panel is loopback-only with a per-run token; the browser is driven over
- **No open ports:** the core connects out; the panel is loopback-only with a per-run token; the browser is driven over
@@ -101,7 +101,7 @@ Install: `curl -fsSL https://openvibe.link/install | sh` (and `--robot/--driver`
@@ -101,7 +101,7 @@ Install: `curl -fsSL https://openvibe.link/install | sh` (and `--robot/--driver`
None of its own: people install it on their machines/robots (install script or release binary), it pairs through
None of its own: people install it on their machines/robots (install script or release binary), it pairs through
OpenVibe.Bot/Network and runs as a system service. There is no database; local state lives in the Link's state
OpenVibe.Bot/Network and runs as a system service. There is no database; local state lives in the Link's state
-directory (config, e-stop latch, panel.json, usage/reservations, memory/skills settings). Production is a distribution
+directory (config, stop latch, panel.json, usage/reservations, memory/skills settings). Production is a distribution
matter, not a service deploy.
matter, not a service deploy.
## Key Pitfalls
## Key Pitfalls
@@ -111,7 +111,7 @@ matter, not a service deploy.
@@ -111,7 +111,7 @@ matter, not a service deploy.
- **Never let a network role or a remote command turn on a local feature**; the local config is the upper bound.
- **Never let a network role or a remote command turn on a local feature**; the local config is the upper bound.
Approvals at `ask` must be answerable (tray/notification/CLI) and deny by default.
Approvals at `ask` must be answerable (tray/notification/CLI) and deny by default.
- **A plugin that keeps moving when its stdin closes is a safety bug.** Every driver must stop on stdin close, deadline
- **A plugin that keeps moving when its stdin closes is a safety bug.** Every driver must stop on stdin close, deadline
- and 1 s without a heartbeat; keep the persisted e-stop checked on every path.
+ and 1 s without a heartbeat; keep the persisted stop checked on every path.
- **Computer/browser:** exact-origin checks for task origins, no direct DNS/QUIC/non-proxied WebRTC UDP in the shared
- **Computer/browser:** exact-origin checks for task origins, no direct DNS/QUIC/non-proxied WebRTC UDP in the shared
browser, nonce-fenced page text, and no port. Do not "simplify" the DevTools pipe into a TCP debug port.
browser, nonce-fenced page text, and no port. Do not "simplify" the DevTools pipe into a TCP debug port.
- **Egress failure is closed:** a Network/Bot outage must never fall back to direct browsing or a relay that bypasses
- **Egress failure is closed:** a Network/Bot outage must never fall back to direct browsing or a relay that bypasses
@@ -72,7 +72,7 @@ openvibe-link exchange serve --model qwen3:32b --public-url https://gpu.example.
@@ -72,7 +72,7 @@ openvibe-link exchange serve --model qwen3:32b --public-url https://gpu.example.
- **Core** (Go, one static binary per platform): pairing, one outbound WebSocket to `wss://openvibe.bot/device`
- **Core** (Go, one static binary per platform): pairing, one outbound WebSocket to `wss://openvibe.bot/device`
(credential in the `Authorization` header only), heartbeats and a deadman, the safety rules (deadlines, owner limits
(credential in the `Authorization` header only), heartbeats and a deadman, the safety rules (deadlines, owner limits
- clamped before any driver sees a value, a persisted e-stop, the local kill switch), the plugin supervisor, WHIP video
+ clamped before any driver sees a value, a persisted stop, the local kill switch), the plugin supervisor, WHIP video
publishing with pion, and the system service (systemd, launchd, Windows).
publishing with pion, and the system service (systemd, launchd, Windows).
- **Plugins** (separate processes, JSON lines over stdin/stdout): [`dryrun`](plugins/dryrun), the Adeept 4WD Smart Car
- **Plugins** (separate processes, JSON lines over stdin/stdout): [`dryrun`](plugins/dryrun), the Adeept 4WD Smart Car
Kit [`adeept_adr036`](plugins/adeept_adr036/README.md), [`cozmo`](plugins/cozmo/README.md) through PyCozmo, and
Kit [`adeept_adr036`](plugins/adeept_adr036/README.md), [`cozmo`](plugins/cozmo/README.md) through PyCozmo, and
@@ -81,7 +81,7 @@ openvibe-link exchange serve --model qwen3:32b --public-url https://gpu.example.
@@ -81,7 +81,7 @@ openvibe-link exchange serve --model qwen3:32b --public-url https://gpu.example.
([docs/hardware-looi.md](docs/hardware-looi.md) is its contract). Each plugin stops its own actuators when stdin closes,
([docs/hardware-looi.md](docs/hardware-looi.md) is its contract). Each plugin stops its own actuators when stdin closes,
at a command's deadline and after 1 s without a heartbeat, so the core dying never leaves motors running.
at a command's deadline and after 1 s without a heartbeat, so the core dying never leaves motors running.
- **ESP32 library** ([`esp32/`](esp32/README.md), `OpenVibeLink`): the device side of the protocol for ESP32 boards —
- **ESP32 library** ([`esp32/`](esp32/README.md), `OpenVibeLink`): the device side of the protocol for ESP32 boards —
- pairing, the control link, drive/actuator commands, telemetry, heartbeat, deadman and e-stop — but no jobs or video.
+ pairing, the control link, drive/actuator commands, telemetry, heartbeat, deadman and stop — but no jobs or video.
**Community robots:** a profile people submit on openvibe.bot (`community.<name>`) is reviewed by an OpenVibe admin
**Community robots:** a profile people submit on openvibe.bot (`community.<name>`) is reviewed by an OpenVibe admin
and signed by Bot (Ed25519 over its canonical JSON). `openvibe-link profile verify community.<name>` checks that
and signed by Bot (Ed25519 over its canonical JSON). `openvibe-link profile verify community.<name>` checks that
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
"LOOI headlights: Bot's explicit on/off custom action reaches the community driver as kind \"headlight\" (never actuator), with provenance, 300 ms lifetime/250 ms deadline, 200 ms original reserve on receipt, expires_at_mono_ns on the shared clock, a 2 requests/second budget shared with local controls, and stage=headlight_pending/headlight_refused ack data",
"LOOI headlights: Bot's explicit on/off custom action reaches the community driver as kind \"headlight\" (never actuator), with provenance, 300 ms lifetime/250 ms deadline, 200 ms original reserve on receipt, expires_at_mono_ns on the shared clock, a 2 requests/second budget shared with local controls, and stage=headlight_pending/headlight_refused ack data",
"machine access: `openvibe-link access request|list|approve|deny|revoke` asks another paired machine for scoped agent, send, screen, desk, computer or worker use; the target tray answers Allow/Deny, the owner may only narrow the scopes and time, and every relayed use is verified against Network's signed grant, live revocation and the target's own level; the opt-in \"Let my agents talk\" directory and `openvibe-link mcp` (eight agent and machine tools) let local agent CLIs message each other across machines",
"machine access: `openvibe-link access request|list|approve|deny|revoke` asks another paired machine for scoped agent, send, screen, desk, computer or worker use; the target tray answers Allow/Deny, the owner may only narrow the scopes and time, and every relayed use is verified against Network's signed grant, live revocation and the target's own level; the opt-in \"Let my agents talk\" directory and `openvibe-link mcp` (eight agent and machine tools) let local agent CLIs message each other across machines",
"pairing with a code approved on openvibe.bot (or through Network), one outbound WebSocket, no open ports",
"pairing with a code approved on openvibe.bot (or through Network), one outbound WebSocket, no open ports",
- "safety: deadlines, owner limits clamped before any driver, heartbeats and a deadman, a persisted e-stop and the local kill switch (openvibe-link stop)",
+ "safety: deadlines, owner limits clamped before any driver, heartbeats and a deadman, a persisted stop and the local kill switch (openvibe-link stop)",
"plugins as separate processes (JSON lines): dryrun, adeept_adr036, cozmo, relay and ros2 (rclpy or rosbridge); the community LOOI driver's contract; signed community profiles (openvibe-link profile verify)",
"plugins as separate processes (JSON lines): dryrun, adeept_adr036, cozmo, relay and ros2 (rclpy or rosbridge); the community LOOI driver's contract; signed community profiles (openvibe-link profile verify)",
"WHIP video publishing (pion)",
"WHIP video publishing (pion)",
"the computer plugin for OpenVibe.Actor: terminal, files, screen, input and its own browser, at the level the owner sets on the machine (observe, ask, trusted, full); approvals in the tray (Linux, Windows), a notification or openvibe-link approve",
"the computer plugin for OpenVibe.Actor: terminal, files, screen, input and its own browser, at the level the owner sets on the machine (observe, ask, trusted, full); approvals in the tray (Linux, Windows), a notification or openvibe-link approve",
@@ -26,7 +26,7 @@
@@ -26,7 +26,7 @@
"OpenVibe Connect shares (CON-4): `openvibe-link share <port>` serves the share this computer was picked for, with no key; every saved share is in `openvibe-link status` (address, connected or why not, requests) and stops when the Link stops",
"OpenVibe Connect shares (CON-4): `openvibe-link share <port>` serves the share this computer was picked for, with no key; every saved share is in `openvibe-link status` (address, connected or why not, requests) and stops when the Link stops",
"one-command install on a Raspberry Pi or other Linux robot (I2C, SPI, camera tools, kit detection)",
"one-command install on a Raspberry Pi or other Linux robot (I2C, SPI, camera tools, kit detection)",
"system service on systemd, launchd and Windows (or a Windows sign-in start)",
"system service on systemd, launchd and Windows (or a Windows sign-in start)",
- "ESP32 library (OpenVibeLink): pairing, control link, drive and actuator commands, telemetry, heartbeat, deadman and e-stop",
+ "ESP32 library (OpenVibeLink): pairing, control link, drive and actuator commands, telemetry, heartbeat, deadman and stop",
"`openvibe-link exchange serve --model <id>`: serve a local Ollama/llama.cpp/vLLM model through an OpenAI-compatible listener and register it on OpenVibe.Exchange's endpoint-seller API with the person's OPENVIBE_DEVELOPER_KEY, then remove the offer on Ctrl-C",
"`openvibe-link exchange serve --model <id>`: serve a local Ollama/llama.cpp/vLLM model through an OpenAI-compatible listener and register it on OpenVibe.Exchange's endpoint-seller API with the person's OPENVIBE_DEVELOPER_KEY, then remove the offer on Ctrl-C",
"Link for Android (v0, android/): the same Go pairing and connection bound with gomobile, a Kotlin + Jetpack Compose app (Pair, Approvals with a fingerprint for money, passwords, deletions and new recipients, Activity, This phone), Stay connected as an opt-in foreground service, and Send to OpenVibe on the share sheet; no phone automation"
"Link for Android (v0, android/): the same Go pairing and connection bound with gomobile, a Kotlin + Jetpack Compose app (Pair, Approvals with a fingerprint for money, passwords, deletions and new recipients, Activity, This phone), Stay connected as an opt-in foreground service, and Send to OpenVibe on the share sheet; no phone automation"
],
],
@@ -18,7 +18,7 @@ apk=$(find android/app/build/outputs/apk/debug -name "*.apk" | head -n 1)
@@ -18,7 +18,7 @@ apk=$(find android/app/build/outputs/apk/debug -name "*.apk" | head -n 1)
adb install -r "$apk"
adb install -r "$apk"
for screen in pair approvals camera location notify activity phone; do
for screen in pair approvals camera location notify activity phone; do
- adb shell am force-stop network.openvibe.link || true
+ adb shell am forcstop network.openvibe.link || true
adb shell am start -W -n network.openvibe.link/.PreviewActivity --es screen "$screen" >/dev/null
adb shell am start -W -n network.openvibe.link/.PreviewActivity --es screen "$screen" >/dev/null
sleep 2
sleep 2
adb exec-out screencap -p > "$out/$screen.png"
adb exec-out screencap -p > "$out/$screen.png"
@@ -26,7 +26,7 @@ for screen in pair approvals camera location notify activity phone; do
@@ -26,7 +26,7 @@ for screen in pair approvals camera location notify activity phone; do
done
done
# The launcher entry, as a person sees it first.
# The launcher entry, as a person sees it first.
-adb shell am force-stop network.openvibe.link || true
+adb shell am forcstop network.openvibe.link || true
adb shell am start -W -n network.openvibe.link/.MainActivity >/dev/null
adb shell am start -W -n network.openvibe.link/.MainActivity >/dev/null
sleep 2
sleep 2
adb exec-out screencap -p > "$out/app.png"
adb exec-out screencap -p > "$out/app.png"
@@ -9,7 +9,7 @@
@@ -9,7 +9,7 @@
// openvibe-link uninstall stop and remove the system service
// openvibe-link uninstall stop and remove the system service
// openvibe-link status show the link, the plugins, the stop latch and video
// openvibe-link status show the link, the plugins, the stop latch and video
// openvibe-link stop local kill switch: stop every actuator and hold it stopped
// openvibe-link stop local kill switch: stop every actuator and hold it stopped
-// openvibe-link resume release the local kill switch (and a server e-stop)
+// openvibe-link resume release the local kill switch (and a server stop)
// openvibe-link quit end the running node cleanly: every actuator stops, nothing is latched
// openvibe-link quit end the running node cleanly: every actuator stops, nothing is latched
// openvibe-link plugins list plugins and their capabilities
// openvibe-link plugins list plugins and their capabilities
// openvibe-link memory scan [--tools a,b] [--project DIR …] [--json]
// openvibe-link memory scan [--tools a,b] [--project DIR …] [--json]
@@ -809,7 +809,7 @@ func cmdResume(g *globals, stdout io.Writer) error {
@@ -809,7 +809,7 @@ func cmdResume(g *globals, stdout io.Writer) error {
}
}
chownLikeDir(g.paths.LatchFile(), g.paths.StateDir)
chownLikeDir(g.paths.LatchFile(), g.paths.StateDir)
}
}
- fmt.Fprintln(stdout, "Resumed: the local stop and any server e-stop are released. Operators can drive again.")
+ fmt.Fprintln(stdout, "Resumed: the local stop and any server stop latch are released. Operators can drive again.")
return nil
return nil
}
}
@@ -976,7 +976,7 @@ func deviceLine(device, principal, server string) string {
@@ -976,7 +976,7 @@ func deviceLine(device, principal, server string) string {
func latchText(l safety.LatchState) string {
func latchText(l safety.LatchState) string {
if l.Computer {
if l.Computer {
- return "STOPPED for computer control (an e-stop on this computer); `openvibe-link resume` on this machine is required"
+ return "STOPPED for computer control (a stop on this computer); `openvibe-link resume` on this machine is required"
}
}
switch {
switch {
case l.Local && l.Remote:
case l.Local && l.Remote:
@@ -984,7 +984,7 @@ func latchText(l safety.LatchState) string {
@@ -984,7 +984,7 @@ func latchText(l safety.LatchState) string {
case l.Local:
case l.Local:
return "STOPPED locally; `openvibe-link resume` releases it"
return "STOPPED locally; `openvibe-link resume` releases it"
case l.Remote:
case l.Remote:
- return "E-STOPPED by the server (" + l.RemoteReason + "); the owner clears it on openvibe.bot"
+ return "STOPPED by the server (" + l.RemoteReason + "); the owner clears it on openvibe.bot"
}
}
return "clear"
return "clear"
}
}
@@ -55,7 +55,7 @@ a Cozmo-based robot).
@@ -55,7 +55,7 @@ a Cozmo-based robot).
6. **Safety is not optional.** Every motion command carries a deadline relative to receipt (`deadline_ms`, default 300 ms, at most the robot's
6. **Safety is not optional.** Every motion command carries a deadline relative to receipt (`deadline_ms`, 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
`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
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
+ 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
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).
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,
7. **Robot profiles, not rows.** `bot.robot-profile@1` (Contracts): capabilities (drive differential/mecanum, pan-tilt,
@@ -108,7 +108,7 @@ a Cozmo-based robot).
@@ -108,7 +108,7 @@ a Cozmo-based robot).
- Pairing: a code works once, expires after 10 minutes, dies after 5 wrong tries; the credential is never logged or
- Pairing: a code works once, expires after 10 minutes, dies after 5 wrong tries; the credential is never logged or
returned twice; rotation keeps the old credential for 60 s; revocation disconnects the device at once.
returned twice; rotation keeps the old credential for 60 s; revocation disconnects the device at once.
- Safety: a drive command without a newer one stops the motors at its deadline; killing the network, the WebSocket or
- Safety: a drive command without a newer one stops the motors at its deadline; killing the network, the WebSocket or
- the agent process stops the motors; the e-stop latches on the device and only the owner clears it; limits cap every
+ the agent process stops the motors; the stop latches on the device and only the owner clears it; limits cap every
operator; Cozmo stops on a cliff event.
operator; Cozmo stops on a cliff event.
- Access: a viewer cannot drive a private robot; a queued turn ends on its timer; chat commands pass the same gate.
- Access: a viewer cannot drive a private robot; a queued turn ends on its timer; chat commands pass the same gate.
- Profiles: the Adeept and Cozmo profiles validate and render a panel with no robot-specific frontend code.
- Profiles: the Adeept and Cozmo profiles validate and render a panel with no robot-specific frontend code.
@@ -42,9 +42,9 @@ mode 0600 and its contents never appear in logs, errors or `status`.
@@ -42,9 +42,9 @@ mode 0600 and its contents never appear in logs, errors or `status`.
- Control link: `wss://openvibe.bot/device`, credential in the `Authorization: Bearer` header of the upgrade (never in the
- Control link: `wss://openvibe.bot/device`, credential in the `Authorization: Bearer` header of the upgrade (never in the
URL). Every frame is JSON with `v` (1), `seq` (per-direction counter), `ts` (ms). Server→device: `hello`, `config`
URL). Every frame is JSON with `v` (1), `seq` (per-direction counter), `ts` (ms). Server→device: `hello`, `config`
(limits, heartbeat_ms, deadman_ms), `command` (`id`, `kind` drive|actuator|ptz|say|display|halt, `value`, `deadline_ms`,
(limits, heartbeat_ms, deadman_ms), `command` (`id`, `kind` drive|actuator|ptz|say|display|halt, `value`, `deadline_ms`,
- `operator`, `role`), `estop` (latched), `heartbeat_ack`. Device→server: `status` (agent version, drivers, faults,
- estop_latched), `telemetry` (≤ 2 Hz: battery, rssi, sensors), `ack` / `nack` (`id`, `fault_code`), `heartbeat` (1 s),
- `estop_state`. A repeated command `id` is answered with the first result and never executed twice. Commands are never
+ `operator`, `role`), the old stop-latch frame, `heartbeat_ack`. Device→server: `status` (agent version, drivers, faults,
+ and the legacy latch field), `telemetry` (≤ 2 Hz: battery, rssi, sensors), `ack` / `nack` (`id`, `fault_code`), `heartbeat` (1 s),
+ the old Stop report. A repeated command `id` is answered with the first result and never executed twice. Commands are never
queued while offline and never replayed after a reconnect. Reconnect with exponential backoff and jitter.
queued while offline and never replayed after a reconnect. Reconnect with exponential backoff and jitter.
- The service side (OpenVibe.Bot) is being built at the same time; its `docs/protocol.md` will be the exact wire format.
- The service side (OpenVibe.Bot) is being built at the same time; its `docs/protocol.md` will be the exact wire format.
Keep every wire detail inside `internal/protocol/` so aligning later is one package. Build a **fake Bot server** in Go
Keep every wire detail inside `internal/protocol/` so aligning later is one package. Build a **fake Bot server** in Go
@@ -57,7 +57,7 @@ mode 0600 and its contents never appear in logs, errors or `status`.
@@ -57,7 +57,7 @@ mode 0600 and its contents never appear in logs, errors or `status`.
only the time left, and the plugin stops at that deadline unless a newer command arrived. Held controls are re-sent by the operator every 150 ms.
only the time left, and the plugin stops at that deadline unless a newer command arrived. Held controls are re-sent by the operator every 150 ms.
- Stop all actuators on: 2 missed heartbeats, link loss, core shutdown, plugin crash (the core restarts it with backoff
- Stop all actuators on: 2 missed heartbeats, link loss, core shutdown, plugin crash (the core restarts it with backoff
and it starts stopped), `openvibe-link stop`, an `estop`.
and it starts stopped), `openvibe-link stop`, an `estop`.
-- Two latches, both persisted across restarts. The **remote** e-stop: a server `estop` sets it, and the owner's `estop`
+- Two latches, both persisted across restarts. The **remote** stop: a server `estop` sets it, and the owner's `estop`
`latched:false` or the local `resume` clears it. The **local** stop (`openvibe-link stop`, or a driver's `local_stop`):
`latched:false` or the local `resume` clears it. The **local** stop (`openvibe-link stop`, or a driver's `local_stop`):
only the local `resume` clears it, never the owner. Owner limits (`max_speed`, `max_turn`) clamp every value in the core before it reaches a plugin.
only the local `resume` clears it, never the owner. Owner limits (`max_speed`, `max_turn`) clamp every value in the core before it reaches a plugin.
- Never run or expose a kit's stock control server (the Adeept kit ships one on 0.0.0.0:8888 with the fixed login
- Never run or expose a kit's stock control server (the Adeept kit ships one on 0.0.0.0:8888 with the fixed login
@@ -87,7 +87,7 @@ Raspberry Pi OS Bookworm, Python 3, `adafruit-circuitpython-pca9685`, `adafruit-
@@ -87,7 +87,7 @@ Raspberry Pi OS Bookworm, Python 3, `adafruit-circuitpython-pca9685`, `adafruit-
- **Buzzer**: `gpiozero.TonalBuzzer(18)`; **WS2812 LEDs** over SPI (`spidev`, bus 0 device 0).
- **Buzzer**: `gpiozero.TonalBuzzer(18)`; **WS2812 LEDs** over SPI (`spidev`, bus 0 device 0).
- **Camera**: Picamera2/libcamera. The core publishes it: prefer `rpicam-vid --codec h264 --inline -o -` piped into the
- **Camera**: Picamera2/libcamera. The core publishes it: prefer `rpicam-vid --codec h264 --inline -o -` piped into the
WHIP publisher; fall back to MJPEG frames from the plugin.
WHIP publisher; fall back to MJPEG frames from the plugin.
-- Tests: a fake I²C/GPIO layer so the plugin runs on any machine; assert the deadline stop, stdin-EOF stop, estop, and the
+- Tests: a fake I²C/GPIO layer so the plugin runs on any machine; assert the deadline stop, stdin-EOF stop, a local stop, and the
exact channel numbers.
exact channel numbers.
## Plugin 2: `cozmo` (a bridge plugin)
## Plugin 2: `cozmo` (a bridge plugin)
@@ -169,7 +169,7 @@ llama.cpp's RPC has no authentication of its own, so the plugin never exposes it
@@ -169,7 +169,7 @@ llama.cpp's RPC has no authentication of its own, so the plugin never exposes it
- **Idle-only and the kill switch hold for the whole pool.** The head asks every member every half second; any member
- **Idle-only and the kill switch hold for the whole pool.** The head asks every member every half second; any member
that is in use (a game, OBS, the person at the keyboard, another program on the GPU; the pool's own GPU use does not
that is in use (a game, OBS, the person at the keyboard, another program on the GPU; the pool's own GPU use does not
count), outside its hours, offline or stopped by its kill switch pauses the whole pool, and a member the head stops
count), outside its hours, offline or stopped by its kill switch pauses the whole pool, and a member the head stops
- calling pauses itself after three seconds. The kill switch (an emergency stop of any machine) holds the pool until
+ calling pauses itself after three seconds. The kill switch (an stop of any machine) holds the pool until
its latch is cleared. Network marks the pool's offer not ready when any machine misses a renewal for ten seconds.
its latch is cleared. Network marks the pool's offer not ready when any machine misses a renewal for ten seconds.
### Setting one up
### Setting one up
@@ -92,9 +92,9 @@ moment", and the tray and the plugin ask again. Deny always goes through.
@@ -92,9 +92,9 @@ moment", and the tray and the plugin ask again. Deny always goes through.
At most 8 requests wait at once. The panel on openvibe.bot sees `approval` and `approval_done` events, so it can say
At most 8 requests wait at once. The panel on openvibe.bot sees `approval` and `approval_done` events, so it can say
that a request is waiting on the machine.
that a request is waiting on the machine.
-**Kill switch:** `openvibe-link stop` (and an owner's e-stop) drops the plugin to `observe`: what runs is killed, what
+**Kill switch:** `openvibe-link stop` (and an owner's stop) drops the plugin to `observe`: what runs is killed, what
waits is denied, the agent's browser is closed, and the core also refuses `exec`, file writes, `input` and the browser's
waits is denied, the agent's browser is closed, and the core also refuses `exec`, file writes, `input` and the browser's
-acting actions until `openvibe-link resume`. `halt` kills what runs. An e-stop on this computer always needs that local
+acting actions until `openvibe-link resume`. `halt` kills what runs. An stop on this computer always needs that local
resume: the owner's remote clear (`estop` `latched:false`, or a config without `estop_latched`) releases the robot's remote
resume: the owner's remote clear (`estop` `latched:false`, or a config without `estop_latched`) releases the robot's remote
latch, never this computer.
latch, never this computer.
@@ -127,7 +127,7 @@ You can watch your machine's screen live on openvibe.bot (**Your machines** →
@@ -127,7 +127,7 @@ You can watch your machine's screen live on openvibe.bot (**Your machines** →
- **You are told.** The desktop says when it starts ("Your screen is live on OpenVibe"), and `openvibe-link status`
- **You are told.** The desktop says when it starts ("Your screen is live on OpenVibe"), and `openvibe-link status`
shows `Screen: live … until …` while it runs.
shows `Screen: live … until …` while it runs.
- **It ends** when its time is up, when you press Stop, when capturing fails five times in a row, and at once on
- **It ends** when its time is up, when you press Stop, when capturing fails five times in a row, and at once on
- `openvibe-link stop` or an e-stop. While a stop is latched, a new start is refused.
+ `openvibe-link stop` or an stop. While a stop is latched, a new start is refused.
- **Levels.** It runs at every level, like a screenshot. To never offer it, set `allow_screen_stream: false`.
- **Levels.** It runs at every level, like a screenshot. To never offer it, set `allow_screen_stream: false`.
- **Without Pillow** the capture tool must write JPEG itself: `screencapture`, `grim`, `import` and `scrot` do;
- **Without Pillow** the capture tool must write JPEG itself: `screencapture`, `grim`, `import` and `scrot` do;
`gnome-screenshot` does not.
`gnome-screenshot` does not.
@@ -211,7 +211,7 @@ An agent can work on a desk of its own instead of your screen (plan track K: Vib
@@ -211,7 +211,7 @@ An agent can work on a desk of its own instead of your screen (plan track K: Vib
`status` and `list` run at every level. What runs on the desk keeps its own rules: input and the browser still act.
`status` and `list` run at every level. What runs on the desk keeps its own rules: input and the browser still act.
- **Watching and taking over.** Start the live screen with `"desk": "private"` (your desktop is not told, since nothing of
- **Watching and taking over.** Start the live screen with `"desk": "private"` (your desktop is not told, since nothing of
yours is shown) and drive it the same way.
yours is shown) and drive it the same way.
-- **It ends** on `stop`, and at once on `openvibe-link stop` or an e-stop: every process the desk started is ended by
+- **It ends** on `stop`, and at once on `openvibe-link stop` or an stop: every process the desk started is ended by
handle, never by name.
handle, never by name.
- **Needs** Linux with `Xvfb` (Debian/Ubuntu: `apt install xvfb`), `xdotool` for input and ImageMagick `import` or `scrot`
- **Needs** Linux with `Xvfb` (Debian/Ubuntu: `apt install xvfb`), `xdotool` for input and ImageMagick `import` or `scrot`
for screenshots. Not offered elsewhere; `allow_desk: false` turns it off.
for screenshots. Not offered elsewhere; `allow_desk: false` turns it off.
@@ -59,7 +59,7 @@ Drive from openvibe.bot; watch `openvibe-link status --json` for telemetry.
@@ -59,7 +59,7 @@ Drive from openvibe.bot; watch `openvibe-link status --json` for telemetry.
| Line tracking (GPIO 22/27/17) | `sensors.line` flips over tape | |
| Line tracking (GPIO 22/27/17) | `sensors.line` flips over tape | |
| Buzzer (GPIO 18) / WS2812 (SPI 0.0) | horn + lights widgets work | |
| Buzzer (GPIO 18) / WS2812 (SPI 0.0) | horn + lights widgets work | |
| Camera (`rpicam-vid --codec h264 --inline -o -` → `internal/video/whip.go`) | panel shows live video (needs the Ops-WHIP row) | |
| Camera (`rpicam-vid --codec h264 --inline -o -` → `internal/video/whip.go`) | panel shows live video (needs the Ops-WHIP row) | |
-| Safety | kill Wi-Fi / Ctrl-C / panel e-stop → wheels stop; e-stop stays latched through a restart | |
+| Safety | kill Wi-Fi / Ctrl-C / panel stop → wheels stop; stop stays latched through a restart | |
Mecanum chassis: re-run with `--driver adeept-mecanum` (same profile `adeept.adr036`, plugin config `wheels: "mecanum"`).
Mecanum chassis: re-run with `--driver adeept-mecanum` (same profile `adeept.adr036`, plugin config `wheels: "mecanum"`).
@@ -17,7 +17,7 @@ them per robot.
@@ -17,7 +17,7 @@ them per robot.
Who may drive is Bot's job, not the driver's: the owner, invited operators, an optional public queue with timed
Who may drive is Bot's job, not the driver's: the owner, invited operators, an optional public queue with timed
turns, cooldowns and per-role allowed commands, all enforced by Bot's command gate before a command reaches the Link.
turns, cooldowns and per-role allowed commands, all enforced by Bot's command gate before a command reaches the Link.
-The e-stop is latched in Bot and only the owner clears it. Each command tells the driver who sent it (`cmd.operator`,
+Stop is a plain command: Bot sends `halt`, and the tray's own Stop stays local. Each command tells the driver who sent it (`cmd.operator`,
`{"subject", "role"}`), and when a queue turn ends or its holder leaves Bot halts the robot before the next person
`{"subject", "role"}`), and when a queue turn ends or its holder leaves Bot halts the robot before the next person
drives (the driver gets `stop`), so commands from the last driver never come after it
drives (the driver gets `stop`), so commands from the last driver never come after it
([plugins.md](plugins.md#core--plugin)). The owner or an operator driving during someone's turn pauses that turn and
([plugins.md](plugins.md#core--plugin)). The owner or an operator driving during someone's turn pauses that turn and
@@ -115,8 +115,8 @@ The driver should send telemetry with `battery` (`{"percent": 0..100}` or `{"vol
@@ -115,8 +115,8 @@ The driver should send telemetry with `battery` (`{"percent": 0..100}` or `{"vol
- **A stop on the robot itself.** If the driver has its own stop (a button, a hotkey, a sensor it trusts), call
- **A stop on the robot itself.** If the driver has its own stop (a button, a hotkey, a sensor it trusts), call
`self.local_stop("reason")` (or send `{"op": "local_stop", "reason": "…"}`). Every actuator stops at once, the
`self.local_stop("reason")` (or send `{"op": "local_stop", "reason": "…"}`). Every actuator stops at once, the
Link latches its **local** stop (persisted across restarts), and the panel shows the robot as stopped on the robot.
Link latches its **local** stop (persisted across restarts), and the panel shows the robot as stopped on the robot.
- The owner's remote clear does not lift it: only `openvibe-link resume` on the bridge computer does, and then the
- owner clears the panel's e-stop. Running `openvibe-link stop` does the same from a terminal.
+ Only `openvibe-link resume` on the bridge computer lifts it. Running `openvibe-link stop` does the same from a
+ terminal.
## Installing the driver
## Installing the driver
@@ -276,7 +276,7 @@ console window that shows its log; closing the window stops the Link and every a
@@ -276,7 +276,7 @@ console window that shows its log; closing the window stops the Link and every a
| `openvibe-link uninstall --user-session --home DIR` | Windows: remove that directory's sign-in start (one per directory) |
| `openvibe-link uninstall --user-session --home DIR` | Windows: remove that directory's sign-in start (one per directory) |
| `openvibe-link status [--json]` | link, stop latch, limits, plugins, video, battery, running jobs |
| `openvibe-link status [--json]` | link, stop latch, limits, plugins, video, battery, running jobs |
| `openvibe-link stop` | **kill switch**: stop every actuator now and hold it stopped |
| `openvibe-link stop` | **kill switch**: stop every actuator now and hold it stopped |
-| `openvibe-link resume` | release the local stop (and a server e-stop) |
+| `openvibe-link resume` | release the local stop (and a server stop) |
| `openvibe-link quit` | end the running Link cleanly: every actuator stops, **nothing is latched**, and it waits until the Link has exited |
| `openvibe-link quit` | end the running Link cleanly: every actuator stops, **nothing is latched**, and it waits until the Link has exited |
| `openvibe-link plugins [--json]` | plugins and their capabilities |
| `openvibe-link plugins [--json]` | plugins and their capabilities |
@@ -301,7 +301,7 @@ that owns the Link's life should run `openvibe-link run --home DIR` itself, with
@@ -301,7 +301,7 @@ that owns the Link's life should run `openvibe-link run --home DIR` itself, with
|---------------------------------------|-------------------------------------------------------|-----------------------------------|
|---------------------------------------|-------------------------------------------------------|-----------------------------------|
| `/etc/openvibe-link/config.json` | `…/OpenVibe Link/config.json` | configuration |
| `/etc/openvibe-link/config.json` | `…/OpenVibe Link/config.json` | configuration |
| `/etc/openvibe-link/credential.json` | `…/OpenVibe Link/credential.json` | device or node credential (mode 600) |
| `/etc/openvibe-link/credential.json` | `…/OpenVibe Link/credential.json` | device or node credential (mode 600) |
-| `/var/lib/openvibe-link/latch.json` | `…/OpenVibe Link/latch.json` | e-stop / kill switch latch |
+| `/var/lib/openvibe-link/latch.json` | `…/OpenVibe Link/latch.json` | stop / kill switch latch |
| `/var/lib/openvibe-link/node.sock` | `…/OpenVibe Link/node.sock` | local control socket (mode 600) |
| `/var/lib/openvibe-link/node.sock` | `…/OpenVibe Link/node.sock` | local control socket (mode 600) |
| `/var/lib/openvibe-link/venv` | `…/OpenVibe Link/venv` | plugin Python environment |
| `/var/lib/openvibe-link/venv` | `…/OpenVibe Link/venv` | plugin Python environment |
@@ -39,7 +39,7 @@ computer plugin's shared desks) reads the same config as the Link that started i
@@ -39,7 +39,7 @@ computer plugin's shared desks) reads the same config as the Link that started i
Linux and macOS.
Linux and macOS.
A plugin that exits is restarted with exponential backoff (1 s doubling to 30 s, reset after a minute of uptime). It
A plugin that exits is restarted with exponential backoff (1 s doubling to 30 s, reset after a minute of uptime). It
-starts stopped; if the e-stop or the local kill switch is latched, the core sends `estop` right after `hello`.
+starts stopped; if a local stop or the kill switch is latched, the core sends `estop` right after `hello` (the old name of a Stop).
A plugin that does not describe itself within 15 s, or stops reading stdin, is killed.
A plugin that does not describe itself within 15 s, or stops reading stdin, is killed.
Probe mode (`hello` with `"probe": true`) is used by `openvibe-link pair` and `openvibe-link plugins`: the plugin
Probe mode (`hello` with `"probe": true`) is used by `openvibe-link pair` and `openvibe-link plugins`: the plugin
@@ -168,7 +168,7 @@ Asked by the Generic LOOI driver's authors (2026-10-27). Each answer is what Bot
@@ -168,7 +168,7 @@ Asked by the Generic LOOI driver's authors (2026-10-27). Each answer is what Bot
- The core passes `provenance` through unchanged.
- The core passes `provenance` through unchanged.
- An operator who repeats their own id gets the first result, and no second command reaches the robot.
- An operator who repeats their own id gets the first result, and no second command reaches the robot.
- A retry under a new id, or a held control re-sent every 150 ms, is a **new** command. It passes the gate again at
- A retry under a new id, or a held control re-sent every 150 ms, is a **new** command. It passes the gate again at
- that moment: whose turn it is, the epoch, the e-stop and the limits. Its id, `seq`, `issued_at_ms`, deadline and
+ that moment: whose turn it is, the epoch, the stop latch and the limits. Its id, `seq`, `issued_at_ms`, deadline and
`turn_epoch` are all new.
`turn_epoch` are all new.
- So a stamp is always a fresh decision, never an old command made current.
- So a stamp is always a fresh decision, never an old command made current.
3. **Order across a change of driver.** A queue turn can end, its holder can leave, or the owner can take it over. Then:
3. **Order across a change of driver.** A queue turn can end, its holder can leave, or the owner can take it over. Then:
@@ -193,10 +193,10 @@ Asked by the Generic LOOI driver's authors (2026-10-27). Each answer is what Bot
@@ -193,10 +193,10 @@ Asked by the Generic LOOI driver's authors (2026-10-27). Each answer is what Bot
- a plugin missing its reply window.
- a plugin missing its reply window.
- `estop` stops every actuator **and latches**. The core refuses `drive`, `ptz` and `actuator` itself (`estopped`,
- `estop` stops every actuator **and latches**. The core refuses `drive`, `ptz` and `actuator` itself (`estopped`,
`local_stop`), and so does the runtime, until `resume`. The latch:
`local_stop`), and so does the runtime, until `resume`. The latch:
- - is set by Bot's e-stop (an owner or operator presses it), by `openvibe-link stop`, or by a driver's `local_stop`;
+ - is set by an old server stop latch, by `openvibe-link stop`, or by a driver's `local_stop`;
- is written to disk, survives a Link restart, and is sent again at once to a plugin that restarts;
- is written to disk, survives a Link restart, and is sent again at once to a plugin that restarts;
- is reapplied from Bot's config on every reconnect.
- is reapplied from Bot's config on every reconnect.
- - Only the owner clears Bot's e-stop. A local stop is cleared only by `openvibe-link resume` on the machine.
+ - Bot no longer sets this latch; it sends a plain `halt` instead. A local stop is cleared only by `openvibe-link resume` on the machine, and an old server latch is released by the same `resume`.
- A `halt` still passes while latched.
- A `halt` still passes while latched.
- The driver's own stops (a motion deadline passing, a second without a heartbeat, stdin closing) are `stop`s.
- The driver's own stops (a motion deadline passing, a second without a heartbeat, stdin closing) are `stop`s.
Heartbeat loss also refuses motion until heartbeats return.
Heartbeat loss also refuses motion until heartbeats return.
@@ -251,7 +251,7 @@ Asked next by the same authors (2026-10-27, P7 LOOI shadow integration), and cha
@@ -251,7 +251,7 @@ Asked next by the same authors (2026-10-27, P7 LOOI shadow integration), and cha
| `stop` | `link_lost` | the link to Bot dropped (two missed heartbeats, a close) | no |
| `stop` | `link_lost` | the link to Bot dropped (two missed heartbeats, a close) | no |
| `stop` | `shutdown` | the Link is stopping (service stop, `openvibe-link quit`), or the core is stopping this process | no |
| `stop` | `shutdown` | the Link is stopping (service stop, `openvibe-link quit`), or the core is stopping this process | no |
| `stop` | `timeout` | a command got no answer from the driver within its window | no |
| `stop` | `timeout` | a command got no answer from the driver within its window | no |
- | `estop` | `remote` | the platform's e-stop (a robot owner or staff), with its `reason` | yes, until the owner clears it |
+ | `estop` | `remote` | the platform's stop (a robot owner or staff), with its `reason` | yes, until the owner clears it |
| `estop` | `local` | `openvibe-link stop` on the device | yes, until `openvibe-link resume` |
| `estop` | `local` | `openvibe-link stop` on the device | yes, until `openvibe-link resume` |
| `estop` | `driver` | a driver's own `local_stop` (this plugin's or another's) | yes, until `openvibe-link resume` |
| `estop` | `driver` | a driver's own `local_stop` (this plugin's or another's) | yes, until `openvibe-link resume` |
| `estop` | `latched` | the latch was already set when the Link or this process started (`replayed: true`, `first_cause` when known) | yes |
| `estop` | `latched` | the latch was already set when the Link or this process started (`replayed: true`, `first_cause` when known) | yes |
@@ -267,8 +267,7 @@ Asked next by the same authors (2026-10-27, P7 LOOI shadow integration), and cha
@@ -267,8 +267,7 @@ Asked next by the same authors (2026-10-27, P7 LOOI shadow integration), and cha
true; an ordinary halt, a disconnect, a timeout or a handoff only revokes remote motion. Treat `on_resume` as
true; an ordinary halt, a disconnect, a timeout or a handoff only revokes remote motion. Treat `on_resume` as
information. Never ARM P7, clear its physical latch or restore a remote grant there; the next command still passes
information. Never ARM P7, clear its physical latch or restore a remote grant there; the next command still passes
every check. Across a plugin restart the latch arrives again as `estop` `latched` with `replayed: true`, and the halt
every check. Across a plugin restart the latch arrives again as `estop` `latched` with `replayed: true`, and the halt
- epoch comes in `hello`. Across a Link reconnect the link's loss is a `stop` (`link_lost`); a platform e-stop set
- while offline is applied from Bot's config before any command.
+ epoch comes in `hello`. Across a Link reconnect the link's loss is a `stop` (`link_lost`); an old server stop latch set while offline is applied from config before any command.
Reviewed again by the same authors against v0.17.0 (2026-10-27, `632207f`), and changed in Link 0.18.0:
Reviewed again by the same authors against v0.17.0 (2026-10-27, `632207f`), and changed in Link 0.18.0:
@@ -333,9 +332,9 @@ Reviewed again by the same authors against v0.17.0 (2026-10-27, `632207f`), and
@@ -333,9 +332,9 @@ Reviewed again by the same authors against v0.17.0 (2026-10-27, `632207f`), and
owner's command.
owner's command.
- `on_resume` is information. Its default does nothing, and the runtime re-arms nothing, so P7 is not re-armed and
- `on_resume` is information. Its default does nothing, and the runtime re-arms nothing, so P7 is not re-armed and
no local grant comes back.
no local grant comes back.
- - **A replayed e-stop comes before any motion.** The core keeps the latch on disk and sets it before it starts any
+ - **A replayed stop comes before any motion.** The core keeps the latch on disk and sets it before it starts any
plugin.
plugin.
- - **Plugin restart.** The process gets `hello` with the latest halt epoch, then the e-stop again (`latched`,
+ - **Plugin restart.** The process gets `hello` with the latest halt epoch, then the stop again (`latched`,
`replayed: true`, `first_cause`). No command is routed to it before its `ready`, and lines reach it in the order
`replayed: true`, `first_cause`). No command is routed to it before its `ready`, and lines reach it in the order
they were queued. So the replay arrives before any command, and the runtime refuses motion (`estopped`) from
they were queued. So the replay arrives before any command, and the runtime refuses motion (`estopped`) from
then on.
then on.
@@ -599,7 +598,7 @@ not its label. Live's `key_held` / `key_released` names are viewer status broadc
@@ -599,7 +598,7 @@ not its label. Live's `key_held` / `key_released` names are viewer status broadc
hardware messages; the hardware socket, and so this relay, sends `key_down` / `key_up`. The old `control_id`,
hardware messages; the hardware socket, and so this relay, sends `key_down` / `key_up`. The old `control_id`,
`from_user` and `timestamp` fields are unavailable in Bot's Link → plugin command; Live's example bridge reads
`from_user` and `timestamp` fields are unavailable in Bot's Link → plugin command; Live's example bridge reads
`from_user` only for logging and defaults it, so hardware clients treat all three as optional. The SDK releases all
`from_user` only for logging and defaults it, so hardware clients treat all three as optional. The SDK releases all
-held buttons on deadline, missed heartbeat, stop, e-stop, halt and stdin close.
+held buttons on deadline, missed heartbeat, stop, halt and stdin close.
## Writing a plugin in Python
## Writing a plugin in Python
@@ -633,9 +632,9 @@ A driver with a safety latch of its own tells an ordinary stop from an emergency
@@ -633,9 +632,9 @@ A driver with a safety latch of its own tells an ordinary stop from an emergency
plugin is stopped" above):
plugin is stopped" above):
```python
```python
- def on_stop(self, info): # every stop: a halt, a lost link, a deadline, an e-stop…
+ def on_stop(self, info): # every stop: a halt, a lost link, a deadline, a stop…
self.stop() # always stop every actuator first
self.stop() # always stop every actuator first
- if info.emergency: # the platform's e-stop (remote, local, driver, or replayed as latched)
+ if info.emergency: # the platform's stop (remote, local, driver, or replayed as latched)
self.hardware_latch.set() # only then latch the hardware's own STOP
self.hardware_latch.set() # only then latch the hardware's own STOP
def on_resume(self, info): # the owner cleared the platform's latch: information, never a re-arm
def on_resume(self, info): # the owner cleared the platform's latch: information, never a re-arm
@@ -14,7 +14,7 @@ doc are pinned verbatim in [`internal/protocol/testdata/bot`](../internal/protoc
@@ -14,7 +14,7 @@ doc are pinned verbatim in [`internal/protocol/testdata/bot`](../internal/protoc
Bot commit). `go test ./internal/protocol ./internal/link` decodes every one of them and drives the fake server
Bot commit). `go test ./internal/protocol ./internal/link` decodes every one of them and drives the fake server
(`internal/fakebot`) through pair → connect → command → ack → reconnect with those exact bytes.
(`internal/fakebot`) through pair → connect → command → ack → reconnect with those exact bytes.
-An ESP32 library ([`esp32/`](../esp32/README.md), `OpenVibeLink`) implements this device side minus jobs and video: pairing, the control link, drive/actuator/halt, telemetry, heartbeat, deadman and the e-stop.
+An ESP32 library ([`esp32/`](../esp32/README.md), `OpenVibeLink`) implements this device side minus jobs and video: pairing, the control link, drive/actuator/halt, telemetry, heartbeat, deadman and the stop.
## 1. Pairing
## 1. Pairing
@@ -166,11 +166,11 @@ server's own counter, never the heartbeat's.
@@ -166,11 +166,11 @@ server's own counter, never the heartbeat's.
| `hello` | `session_id`, `device_id`, `robot_ids` [], `server_time` (RFC 3339) — on every authenticated connection |
| `hello` | `session_id`, `device_id`, `robot_ids` [], `server_time` (RFC 3339) — on every authenticated connection |
| `config` | `heartbeat_ms`, `limits` {`max_speed` 0..1, `max_turn` 0..1, `max_command_ms`, `heartbeat_ms`}, `allowed_commands` [kinds], `estop_latched`, `roles` [ids] — right after `hello` |
| `config` | `heartbeat_ms`, `limits` {`max_speed` 0..1, `max_turn` 0..1, `max_command_ms`, `heartbeat_ms`}, `allowed_commands` [kinds], `estop_latched`, `roles` [ids] — right after `hello` |
| `command` | `id` (server-minted: the idempotency and ack key), `ref` (the operator's own id, logs only), `kind`, `value`, `deadline_ms` (absolute, motion kinds and `headlight`), `operator` {`subject`, `role`}, `robot_id`; Link only: `target` (plugin name); `headlight` only: `provenance` {`session`, `seq`, `issued_at_ms`, `expires_at_ms`, `clock_uncertainty_ms`} |
| `command` | `id` (server-minted: the idempotency and ack key), `ref` (the operator's own id, logs only), `kind`, `value`, `deadline_ms` (absolute, motion kinds and `headlight`), `operator` {`subject`, `role`}, `robot_id`; Link only: `target` (plugin name); `headlight` only: `provenance` {`session`, `seq`, `issued_at_ms`, `expires_at_ms`, `clock_uncertainty_ms`} |
-| `estop` | `latched` (true: the e-stop latched; false: the owner cleared it), `by`, `at` |
+| `estop` | `latched` (true: the stop latched; false: the owner cleared it), `by`, `at` |
| `heartbeat_ack` | `echo` (the heartbeat's `t`, or null), `t` (the same send time when the server has it), `server_time` (RFC 3339); the Link measures RTT from the echoed send time |
| `heartbeat_ack` | `echo` (the heartbeat's `t`, or null), `t` (the same send time when the server has it), `server_time` (RFC 3339); the Link measures RTT from the echoed send time |
| `error` | `code`, `detail` — a frame the server refused; logged, never fatal |
| `error` | `code`, `detail` — a frame the server refused; logged, never fatal |
-`config.estop_latched` is the robot's e-stop as the server holds it: an owner e-stop set while the device was offline
+`config.estop_latched` is the robot's stop as the server holds it: an owner stop set while the device was offline
latches the Link on reconnect, and a clear made while offline releases it. Until a connection's `config` has been
latches the Link on reconnect, and a clear made while offline releases it. Until a connection's `config` has been
applied the Link runs nothing but `halt` (`nack not_ready`), so that latch is in force before any motion.
applied the Link runs nothing but `halt` (`nack not_ready`), so that latch is in force before any motion.
`config.allowed_commands` absent allows every kind; present (Bot always sends it, `halt` included) it is the list.
`config.allowed_commands` absent allows every kind; present (Bot always sends it, `halt` included) it is the list.
@@ -179,7 +179,7 @@ applied the Link runs nothing but `halt` (`nack not_ready`), so that latch is in
@@ -179,7 +179,7 @@ applied the Link runs nothing but `halt` (`nack not_ready`), so that latch is in
this machine was installed for; the local configuration always wins (a role never installs anything and never turns on
this machine was installed for; the local configuration always wins (a role never installs anything and never turns on
a feature the owner turned off here), and a role the Link does not know is ignored.
a feature the owner turned off here), and a role the Link does not know is ignored.
`estop` `latched:false` is the owner's clear: it clears the remote latch only, never the local kill switch. A computer
`estop` `latched:false` is the owner's clear: it clears the remote latch only, never the local kill switch. A computer
-plugin is the one exception to the remote latch's reach: an e-stop on a computer waits for `openvibe-link resume` on the
+plugin is the one exception to the remote latch's reach: an stop on a computer waits for `openvibe-link resume` on the
machine, whatever the server clears (docs/computer.md).
machine, whatever the server clears (docs/computer.md).
### Device → server
### Device → server
@@ -192,7 +192,7 @@ machine, whatever the server clears (docs/computer.md).
@@ -192,7 +192,7 @@ machine, whatever the server clears (docs/computer.md).
| `nack` | `id`, `fault_code`; Link extra: `message` |
| `nack` | `id`, `fault_code`; Link extra: `message` |
| `heartbeat` | `t` (send time, Unix ms), `rtt_ms` (the last measured round trip, once there is one; 0 is a real measurement); Link extra: `fleet` [{`agent`, `installed`, `signed_in`, `windows` [{`name`, `used`, `limit`, `resets_at`}]}] — what the machine's coding agents have left (Codes C9c) |
| `heartbeat` | `t` (send time, Unix ms), `rtt_ms` (the last measured round trip, once there is one; 0 is a real measurement); Link extra: `fleet` [{`agent`, `installed`, `signed_in`, `windows` [{`name`, `used`, `limit`, `resets_at`}]}] — what the machine's coding agents have left (Codes C9c) |
| `reauth` | `token`: a fresh node token for the same principal (a Network-paired machine only; refused → `error` `bot.reauth_refused`) |
| `reauth` | `token`: a fresh node token for the same principal (a Network-paired machine only; refused → `error` `bot.reauth_refused`) |
-| `estop_state` | `latched` (the device is stopped: remote e-stop or local kill switch), `by` (`device`), `at`; Link extras: `robot_id` (when `hello` named exactly one robot), `local_stop` (local kill switch), `reason` |
+| `estop_state` | `latched` (the device is stopped: remote stop or local kill switch), `by` (`device`), `at`; Link extras: `robot_id` (when `hello` named exactly one robot), `local_stop` (local kill switch), `reason` |
The first `status` and `estop_state` go out once the connection's `config` has been applied, never right after the
The first `status` and `estop_state` go out once the connection's `config` has been applied, never right after the
upgrade or `hello`; then `status` whenever a plugin changes state or faults and `estop_state` after every `config`,
upgrade or `hello`; then `status` whenever a plugin changes state or faults and `estop_state` after every `config`,
@@ -282,7 +282,7 @@ What the Link does with a command, in order:
@@ -282,7 +282,7 @@ What the Link does with a command, in order:
3. `halt` → every plugin stops, `ack`.
3. `halt` → every plugin stops, `ack`.
4. No `config` yet on this connection → `nack not_ready`. Kind not in `config.allowed_commands` → `nack not_allowed`.
4. No `config` yet on this connection → `nack not_ready`. Kind not in `config.allowed_commands` → `nack not_allowed`.
5. `drive`, `ptz`, `actuator`, `button` and `point` while the local kill switch is latched → `nack local_stop`; while
5. `drive`, `ptz`, `actuator`, `button` and `point` while the local kill switch is latched → `nack local_stop`; while
- the remote e-stop is latched → `nack estopped`; past its `deadline_ms` → `nack expired`. `headlight` is checked by
+ the remote stop is latched → `nack estopped`; past its `deadline_ms` → `nack expired`. `headlight` is checked by
its own provenance/lifetime rules above instead (it is not motion and is not refused by the latch here; the driver
its own provenance/lifetime rules above instead (it is not motion and is not refused by the latch here; the driver
applies STOP and its own preconditions).
applies STOP and its own preconditions).
6. The value is clamped in the core, before any plugin sees it: `throttle`, `x`, `y` to ±`max_speed`; `steer`,
6. The value is clamped in the core, before any plugin sees it: `throttle`, `x`, `y` to ±`max_speed`; `steer`,
@@ -299,7 +299,7 @@ What the Link does with a command, in order:
@@ -299,7 +299,7 @@ What the Link does with a command, in order:
reply window (an owner's approval on the machine plus the program's own run): absent → 60 s, capped at 10 minutes, no
reply window (an owner's approval on the machine plus the program's own run): absent → 60 s, capped at 10 minutes, no
`max_command_ms`. Their value must be a JSON object of at most 384 KB and is not clamped. Running a program and
`max_command_ms`. Their value must be a JSON object of at most 384 KB and is not clamped. Running a program and
writing a file (`exec`, `fs` other than `list`/`stat`/`read`) are refused while the local kill switch is latched
writing a file (`exec`, `fs` other than `list`/`stat`/`read`) are refused while the local kill switch is latched
-(`nack local_stop`) or the remote e-stop is (`nack estopped`); capturing the screen and reading files are not. The
+(`nack local_stop`) or the remote stop is (`nack estopped`); capturing the screen and reading files are not. The
`computer` plugin applies its owner's control level, and its `ack` carries the answer in `data` (at most 512 KB of JSON,
`computer` plugin applies its owner's control level, and its `ack` carries the answer in `data` (at most 512 KB of JSON,
else `nack bad_value`). An `ask`-level command waits for `openvibe-link approve|deny <id>` on the machine; the plugin
else `nack bad_value`). An `ask`-level command waits for `openvibe-link approve|deny <id>` on the machine; the plugin
raises an `approval` event while it waits and `approval_done` when it is answered or expires.
raises an `approval` event while it waits and `approval_done` when it is answered or expires.
@@ -338,7 +338,7 @@ checked in this order:
@@ -338,7 +338,7 @@ checked in this order:
| `job inputs not supported` | `unsupported` | the job carries `inputs`: this Link cannot stage and check them yet, so it never runs it with them dropped |
| `job inputs not supported` | `unsupported` | the job carries `inputs`: this Link cannot stage and check them yet, so it never runs it with them dropped |
| `missing or invalid args, ttl_ms or limits` | `bad_value` | `args` is not an object, or `ttl_ms` or a limit is missing or below 1 |
| `missing or invalid args, ttl_ms or limits` | `bad_value` | `args` is not an object, or `ttl_ms` or a limit is missing or below 1 |
| `class not available` | `unsupported` | the class is valid but this Link does not run it (every class but `function`, `code` and `linux`; every class while the worker is off) |
| `class not available` | `unsupported` | the class is valid but this Link does not run it (every class but `function`, `code` and `linux`; every class while the worker is off) |
-| `the e-stop or local stop is latched` | `estopped` or `local_stop` | the stop latch is set: no job starts until it clears |
+| `the stop or local stop is latched` | `estopped` or `local_stop` | the stop latch is set: no job starts until it clears |
| `class not available` | `shutting_down` | the Link is stopping |
| `class not available` | `shutting_down` | the Link is stopping |
| `unknown artifact` | `unsupported` | `artifact` is not a function the owner declared, at that exact version |
| `unknown artifact` | `unsupported` | `artifact` is not a function the owner declared, at that exact version |
| `worker busy` | `not_ready` | `worker.caps.max_jobs` jobs are already running |
| `worker busy` | `not_ready` | `worker.caps.max_jobs` jobs are already running |
@@ -371,7 +371,7 @@ A Link shutting down sends `job_exit` (`stopped`) also for a job acked but not s
@@ -371,7 +371,7 @@ A Link shutting down sends `job_exit` (`stopped`) also for a job acked but not s
| `cancelled` | null | `job_cancel`: the process group is killed; a job cancelled before it started never starts |
| `cancelled` | null | `job_cancel`: the process group is killed; a job cancelled before it started never starts |
| `ttl` | null | `ttl_ms` (from receipt, clamped to `worker.caps.max_ttl_ms`) ran out, before or while it ran |
| `ttl` | null | `ttl_ms` (from receipt, clamped to `worker.caps.max_ttl_ms`) ran out, before or while it ran |
| `limit` | null | `wall_ms`, `cpu_ms` or `mem_bytes` (each clamped to the local cap), more stdout than `max_output_bytes`, or more than 256 KiB of result |
| `limit` | null | `wall_ms`, `cpu_ms` or `mem_bytes` (each clamped to the local cap), more stdout than `max_output_bytes`, or more than 256 KiB of result |
-| `stopped` | null | the e-stop or the local stop latched, or the Link shut down |
+| `stopped` | null | the stop or the local stop latched, or the Link shut down |
| `failed` | null | the worker could not run it: the namespaces could not be created, the spawn failed, or its CPU limit could not be set |
| `failed` | null | the worker could not run it: the namespaces could not be created, the spawn failed, or its CPU limit could not be set |
`job_cancel` for an id that is not running (unknown, refused or ended) and `job_exit_ack` for an unknown id are
`job_cancel` for an id that is not running (unknown, refused or ended) and `job_exit_ack` for an unknown id are
@@ -309,7 +309,7 @@ AppArmor; a Link running as root with `run_as` is not affected.
@@ -309,7 +309,7 @@ AppArmor; a Link running as root with `run_as` is not affected.
Every kill is SIGKILL to the whole process group and to the leader, which is PID 1 of the job's PID namespace, so its
Every kill is SIGKILL to the whole process group and to the leader, which is PID 1 of the job's PID namespace, so its
death also kills every process the job left behind. The watchdog kills at `ttl_ms` (from receipt), `wall_ms` (from
death also kills every process the job left behind. The watchdog kills at `ttl_ms` (from receipt), `wall_ms` (from
-start), `cpu_ms`, `mem_bytes`, at `max_output_bytes` of stdout, on `job_cancel`, and the moment the e-stop or the local
+start), `cpu_ms`, `mem_bytes`, at `max_output_bytes` of stdout, on `job_cancel`, and the moment the stop or the local
stop latches (`openvibe-link stop`); while the latch is set every job is refused, and `openvibe-link resume` admits
stop latches (`openvibe-link stop`); while the latch is set every job is refused, and `openvibe-link resume` admits
them again. A Link shutting down kills its jobs (`stopped`); a job acked but not started yet never starts and gets its
them again. A Link shutting down kills its jobs (`stopped`); a job acked but not started yet never starts and gets its
`job_exit` (`stopped`) all the same.
`job_exit` (`stopped`) all the same.
@@ -463,7 +463,7 @@ Refused with `nack` (keyed by the job id; the first matching row wins, after the
@@ -463,7 +463,7 @@ Refused with `nack` (keyed by the job id; the first matching row wins, after the
| `class not available` | `unsupported` | the worker is off (disabled, not Linux, probe failed), or no declared entry has the job's class (`function`, `code` or `linux`) |
| `class not available` | `unsupported` | the worker is off (disabled, not Linux, probe failed), or no declared entry has the job's class (`function`, `code` or `linux`) |
| `job inputs not supported` | `unsupported` | the job carries `inputs` and `media` is off: this Link never runs a job with them silently dropped |
| `job inputs not supported` | `unsupported` | the job carries `inputs` and `media` is off: this Link never runs a job with them silently dropped |
| `net policy not supported` | `unsupported` | the job names a `net` this host cannot enforce (`public` or `openvibe-only` while `egress` is `none`) |
| `net policy not supported` | `unsupported` | the job names a `net` this host cannot enforce (`public` or `openvibe-only` while `egress` is `none`) |
-| `the e-stop or local stop is latched` | `estopped` or `local_stop` | the stop latch is set |
+| `the stop or local stop is latched` | `estopped` or `local_stop` | the stop latch is set |
| `class not available` | `shutting_down` | the Link is stopping |
| `class not available` | `shutting_down` | the Link is stopping |
| `unknown artifact` | `unsupported` | no declared entry of the job's class has that name and exact version |
| `unknown artifact` | `unsupported` | no declared entry of the job's class has that name and exact version |
| `worker busy` | `not_ready` | `max_jobs` jobs are running |
| `worker busy` | `not_ready` | `max_jobs` jobs are running |
@@ -476,5 +476,5 @@ Accepted jobs end with `job_exit.reason`:
@@ -476,5 +476,5 @@ Accepted jobs end with `job_exit.reason`:
| `cancelled` | `job_cancel`; a job cancelled before it started never starts |
| `cancelled` | `job_cancel`; a job cancelled before it started never starts |
| `ttl` | `ttl_ms` (clamped) ran out, before or while it ran |
| `ttl` | `ttl_ms` (clamped) ran out, before or while it ran |
| `limit` | `wall_ms`, `cpu_ms` (sampled, or `RLIMIT_CPU`'s SIGXCPU) or `mem_bytes` (sampled, or the cgroup's OOM kill) exceeded, stdout past `max_output_bytes`, or a result over 256 KiB (over `media.max_bytes` with `media` on) |
| `limit` | `wall_ms`, `cpu_ms` (sampled, or `RLIMIT_CPU`'s SIGXCPU) or `mem_bytes` (sampled, or the cgroup's OOM kill) exceeded, stdout past `max_output_bytes`, or a result over 256 KiB (over `media.max_bytes` with `media` on) |
-| `stopped` | the e-stop or local stop latched, or the Link shut down |
+| `stopped` | the stop or local stop latched, or the Link shut down |
| `failed` | the job could not run isolated: sandbox, cgroup or namespaces not set up, spawn error, working directory not made, CPU limit not set; never run with less isolation. Also: an input not fetched or not matching its digest (nothing ran), or a large result not uploaded |
| `failed` | the job could not run isolated: sandbox, cgroup or namespaces not set up, spawn error, working directory not made, CPU limit not set; never run with less isolation. Also: an input not fetched or not matching its digest (nothing ran), or a large result not uploaded |
@@ -27,7 +27,7 @@ ArduinoJson (7.4.2) and WebSockets (2.6.1). Nothing here has been on real hardwa
@@ -27,7 +27,7 @@ ArduinoJson (7.4.2) and WebSockets (2.6.1). Nothing here has been on real hardwa
`ptz`, `say` and `display` are valid protocol kinds this device has no driver for, so they are `nack unsupported`.
`ptz`, `say` and `display` are valid protocol kinds this device has no driver for, so they are `nack unsupported`.
A command id or kind over 64 bytes is rejected, and the dedup cache answers a repeated id for the spec's 10 minutes
A command id or kind over 64 bytes is rejected, and the dedup cache answers a repeated id for the spec's 10 minutes
(across reconnects) while every cached message stays within a fixed bound: the id and kind are rejected or not
(across reconnects) while every cached message stays within a fixed bound: the id and kind are rejected or not
- echoed, and a hostile e-stop reason is truncated before it is copied into an entry.
+ echoed, and a hostile stop reason is truncated before it is copied into an entry.
- **Telemetry** — `battery`, `voltage`, `rssi` and `sensors`, at most every 500 ms (Bot's cap).
- **Telemetry** — `battery`, `voltage`, `rssi` and `sensors`, at most every 500 ms (Bot's cap).
- **Heartbeat and deadman** — a `heartbeat` every `config.heartbeat_ms` (capped at 5000 ms so a server cannot stretch
- **Heartbeat and deadman** — a `heartbeat` every `config.heartbeat_ms` (capped at 5000 ms so a server cannot stretch
the deadman for hours) once `hello` has arrived, both `heartbeat_ack` shapes (the newer `echo`/`t` and the older
the deadman for hours) once `hello` has arrived, both `heartbeat_ack` shapes (the newer `echo`/`t` and the older
@@ -50,7 +50,7 @@ ArduinoJson (7.4.2) and WebSockets (2.6.1). Nothing here has been on real hardwa
@@ -50,7 +50,7 @@ ArduinoJson (7.4.2) and WebSockets (2.6.1). Nothing here has been on real hardwa
ends before `hello` as the refused case and logs that the cause is uncertain; see *Limitations* below.
ends before `hello` as the refused case and logs that the cause is uncertain; see *Limitations* below.
- **Frame and memory bounds** — the protocol allows frames up to 1 MiB; this device bounds them at 16 KiB and a JSON
- **Frame and memory bounds** — the protocol allows frames up to 1 MiB; this device bounds them at 16 KiB and a JSON
depth of 10. A frame over either bound, or one the JSON parser cannot allocate, stops the motors and drops the link
depth of 10. A frame over either bound, or one the JSON parser cannot allocate, stops the motors and drops the link
- (it could have been an e-stop; the reconnect re-reads `config.estop_latched`). A frame that loses fields to a failed
+ (it could have been an stop; the reconnect re-reads `config.estop_latched`). A frame that loses fields to a failed
allocation is never sent half-formed.
allocation is never sent half-formed.
## What it does not
## What it does not
@@ -103,7 +103,7 @@ An L298N-class dual H-bridge; change the pins in `Basic.ino` if they clash with
@@ -103,7 +103,7 @@ An L298N-class dual H-bridge; change the pins in `Basic.ino` if they clash with
The two enable pins are the `ledc` PWM channels (20 kHz, 8-bit) and the four IN pins the direction. `onDrive` mixes
The two enable pins are the `ledc` PWM channels (20 kHz, 8-bit) and the four IN pins the direction. `onDrive` mixes
`throttle ± steer` for the two wheels; the core has already clamped them. `onStop` is the only place that zeroes the
`throttle ± steer` for the two wheels; the core has already clamped them. `onStop` is the only place that zeroes the
-motors, and it runs on `halt`, on a motion deadline, on two missed heartbeats, on an e-stop and whenever the link
+motors, and it runs on `halt`, on a motion deadline, on two missed heartbeats, on an stop and whenever the link
closes.
closes.
## Building the conformance harness
## Building the conformance harness
@@ -113,12 +113,12 @@ sh esp32/test/run.sh
@@ -113,12 +113,12 @@ sh esp32/test/run.sh
```
```
It needs `g++` with C++17 and a way to fetch one file (curl, wget or python3). The harness prints one line per
It needs `g++` with C++17 and a way to fetch one file (curl, wget or python3). The harness prints one line per
-scenario and exits non-zero on a failure. Its scenarios cover the fixtures, the deadman, the e-stop latch (including
+scenario and exits non-zero on a failure. Its scenarios cover the fixtures, the deadman, the stop latch (including
wrong-typed frames), clamping, nack codes, job refusals, close codes, heartbeat cadence, a wall clock that steps
wrong-typed frames), clamping, nack codes, job refusals, close codes, heartbeat cadence, a wall clock that steps
backwards, 64-bit monotonic time across the 32-bit `millis()` wrap, oversize/too-deep/allocation-failing frames,
backwards, 64-bit monotonic time across the 32-bit `millis()` wrap, oversize/too-deep/allocation-failing frames,
over-long ids and kinds, the dedup cache's 10-minute limit, the heartbeat cap, guarded commands while the link is
over-long ids and kinds, the dedup cache's 10-minute limit, the heartbeat cap, guarded commands while the link is
lost, the backoff reset after a stable link, the 10 s floor after a refused reconnect, the pre-hello timer staying
lost, the backoff reset after a stable link, the 10 s floor after a refused reconnect, the pre-hello timer staying
-quiet while the transport is down, a hostile e-stop `by` being bounded in the latch and in every cached message, and a
+quiet while the transport is down, a hostile stop `by` being bounded in the latch and in every cached message, and a
command or actuator value document that overflows being refused rather than reaching a driver truncated.
command or actuator value document that overflows being refused rather than reaching a driver truncated.
## Limitations
## Limitations
@@ -127,7 +127,7 @@ command or actuator value document that overflows being refused rather than reac
@@ -127,7 +127,7 @@ command or actuator value document that overflows being refused rather than reac
- The library never starts SNTP (step 4). Before the sketch syncs time, `ts`, `estop_state.at` and `heartbeat.t` carry
- The library never starts SNTP (step 4). Before the sketch syncs time, `ts`, `estop_state.at` and `heartbeat.t` carry
the monotonic clock rather than Unix milliseconds. The core's timers never read that clock, so safety is unaffected.
the monotonic clock rather than Unix milliseconds. The core's timers never read that clock, so safety is unaffected.
- NVS is not encrypted by default: the credential and the local kill switch live in flash readable to anyone with
- NVS is not encrypted by default: the credential and the local kill switch live in flash readable to anyone with
- physical access unless the sketch enables NVS encryption. The remote e-stop is not stored at all; each connection's
+ physical access unless the sketch enables NVS encryption. The remote stop is not stored at all; each connection's
`config.estop_latched` re-applies it (including after a reboot).
`config.estop_latched` re-applies it (including after a reboot).
- arduinoWebSockets 2.x delivers `WStype_DISCONNECTED` without the WebSocket close code or the upgrade's HTTP status.
- arduinoWebSockets 2.x delivers `WStype_DISCONNECTED` without the WebSocket close code or the upgrade's HTTP status.
The transport therefore treats "disconnected before `hello`" as the 4002/401/403 case (10 s floor, logged as an
The transport therefore treats "disconnected before `hello`" as the 4002/401/403 case (10 s floor, logged as an
@@ -9,7 +9,7 @@
@@ -9,7 +9,7 @@
//
//
// The two enable pins are the ledc PWM channels and the four IN pins the direction. The core clamps every value to the
// The two enable pins are the ledc PWM channels and the four IN pins the direction. The core clamps every value to the
// owner's limits before onDrive sees it, stops the motors when a motion deadline lapses or two heartbeats are missed,
// owner's limits before onDrive sees it, stops the motors when a motion deadline lapses or two heartbeats are missed,
-// and stops them on an e-stop or when the link closes. onStop() is the only place that zeroes the motors.
+// and stops them on an stop or when the link closes. onStop() is the only place that zeroes the motors.
//
//
// Fill in the four defines below. OV_CODE is the 8-character, single-use code openvibe.bot shows for the robot
// Fill in the four defines below. OV_CODE is the 8-character, single-use code openvibe.bot shows for the robot
// (XXXX-XXXX, 10 minutes). After the first pairing the device id and credential are kept in NVS, so OV_CODE is only
// (XXXX-XXXX, 10 minutes). After the first pairing the device id and credential are kept in NVS, so OV_CODE is only
@@ -2,7 +2,7 @@ name=OpenVibeLink
@@ -2,7 +2,7 @@ name=OpenVibeLink
version=0.1.0
version=0.1.0
author=OpenVibers
author=OpenVibers
maintainer=OpenVibers <[email protected]>
maintainer=OpenVibers <[email protected]>
-sentence=ESP32 client for the OpenVibe Bot device protocol: pairing, control link, drive and actuator commands, telemetry, heartbeat, deadman and e-stop.
+sentence=ESP32 client for the OpenVibe Bot device protocol: pairing, control link, drive and actuator commands, telemetry, heartbeat, deadman and stop.
paragraph=Implements the bot.device-message@1 device side (protocol sections 1-3) minus jobs and video. The protocol core builds without any Arduino headers and is exercised by a desktop conformance harness; the transport adds WiFi, TLS with the ESP32 CA bundle, arduinoWebSockets and NVS.
paragraph=Implements the bot.device-message@1 device side (protocol sections 1-3) minus jobs and video. The protocol core builds without any Arduino headers and is exercised by a desktop conformance harness; the transport adds WiFi, TLS with the ESP32 CA bundle, arduinoWebSockets and NVS.
category=Communication
category=Communication
url=https://github.com/OpenVibers/OpenVibe.Link
url=https://github.com/OpenVibers/OpenVibe.Link
@@ -399,7 +399,7 @@ void Core::handle_hello(JsonDocument& doc, int64_t now) {
@@ -399,7 +399,7 @@ void Core::handle_hello(JsonDocument& doc, int64_t now) {
void Core::handle_config(JsonDocument& doc) {
void Core::handle_config(JsonDocument& doc) {
// A wrong-typed estop_latched is a malformed frame (Go's json.Unmarshal would reject it whole): apply nothing and
// A wrong-typed estop_latched is a malformed frame (Go's json.Unmarshal would reject it whole): apply nothing and
- // keep the latch exactly as it is, rather than reading `"false"` or `0` as false and releasing the e-stop.
+ // keep the latch exactly as it is, rather than reading `"false"` or `0` as false and releasing the stop.
JsonVariantConst estopv = doc["estop_latched"];
JsonVariantConst estopv = doc["estop_latched"];
if (!estopv.isNull() && !estopv.is<bool>()) {
if (!estopv.isNull() && !estopv.is<bool>()) {
log("warn", "config with a non-boolean estop_latched ignored; the latch is unchanged");
log("warn", "config with a non-boolean estop_latched ignored; the latch is unchanged");
@@ -530,7 +530,7 @@ void Core::handle_command(JsonDocument& doc, int64_t ts, int64_t now) {
@@ -530,7 +530,7 @@ void Core::handle_command(JsonDocument& doc, int64_t ts, int64_t now) {
return;
return;
}
}
if (remote_stop_) {
if (remote_stop_) {
- const std::string msg = remote_reason_.empty() ? std::string("the remote e-stop is latched") : remote_reason_;
+ const std::string msg = remote_reason_.empty() ? std::string("the remote stop is latched") : remote_reason_;
send_nack(id, kFaultEstopped, msg);
send_nack(id, kFaultEstopped, msg);
remember(id, DedupResult{false, kFaultEstopped, msg});
remember(id, DedupResult{false, kFaultEstopped, msg});
return;
return;
@@ -605,7 +605,7 @@ void Core::handle_estop(JsonDocument& doc) {
@@ -605,7 +605,7 @@ void Core::handle_estop(JsonDocument& doc) {
JsonVariantConst latchedv = doc["latched"];
JsonVariantConst latchedv = doc["latched"];
if (!latchedv.isNull() && !latchedv.is<bool>()) {
if (!latchedv.isNull() && !latchedv.is<bool>()) {
// Wrong type: ignore the frame and keep the latch (Go's json.Unmarshal rejects it too). Reading `"false"` or 0 as
// Wrong type: ignore the frame and keep the latch (Go's json.Unmarshal rejects it too). Reading `"false"` or 0 as
- // false would release the e-stop on a malformed frame.
+ // false would release the stop on a malformed frame.
log("warn", "estop with a non-boolean latched ignored; the latch is unchanged");
log("warn", "estop with a non-boolean latched ignored; the latch is unchanged");
return;
return;
}
}
@@ -615,7 +615,7 @@ void Core::handle_estop(JsonDocument& doc) {
@@ -615,7 +615,7 @@ void Core::handle_estop(JsonDocument& doc) {
// The frame can carry up to kMaxFrameBytes, and this string is copied into every dedup entry that answers an
// The frame can carry up to kMaxFrameBytes, and this string is copied into every dedup entry that answers an
// estopped command: bound it so the cache cannot be filled with megabytes.
// estopped command: bound it so the cache cannot be filled with megabytes.
if (by.size() > kMaxEstopReasonBytes) by.resize(kMaxEstopReasonBytes);
if (by.size() > kMaxEstopReasonBytes) by.resize(kMaxEstopReasonBytes);
- set_remote_stop(true, by.empty() ? std::string("the remote e-stop is latched") : ("by " + by), true);
+ set_remote_stop(true, by.empty() ? std::string("the remote stop is latched") : ("by " + by), true);
} else {
} else {
set_remote_stop(false, "", true);
set_remote_stop(false, "", true);
}
}
@@ -910,14 +910,14 @@ void Core::set_remote_stop(bool on, const std::string& reason, bool notify) {
@@ -910,14 +910,14 @@ void Core::set_remote_stop(bool on, const std::string& reason, bool notify) {
remote_reason_ = reason.size() > kMaxEstopReasonBytes ? reason.substr(0, kMaxEstopReasonBytes) : reason;
remote_reason_ = reason.size() > kMaxEstopReasonBytes ? reason.substr(0, kMaxEstopReasonBytes) : reason;
stop_motion();
stop_motion();
if (notify) {
if (notify) {
- log("warn", "e-stop latched: " + remote_reason_); // the bounded copy, so a huge `by` cannot flood the log either
+ log("warn", "stop latched: " + remote_reason_); // the bounded copy, so a huge `by` cannot flood the log either
notify_latch();
notify_latch();
}
}
} else {
} else {
remote_stop_ = false;
remote_stop_ = false;
remote_reason_.clear();
remote_reason_.clear();
if (notify) {
if (notify) {
- log("warn", "e-stop cleared; motion allowed again");
+ log("warn", "stop cleared; motion allowed again");
notify_latch();
notify_latch();
}
}
}
}
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
//
//
// This is the whole stateful device side of docs/protocol.md sections 1–3 (minus jobs and video): the frame envelope,
// This is the whole stateful device side of docs/protocol.md sections 1–3 (minus jobs and video): the frame envelope,
// hello/config/command/estop/heartbeat_ack handling, clamping before any driver sees a value, ack/nack, the heartbeat
// hello/config/command/estop/heartbeat_ack handling, clamping before any driver sees a value, ack/nack, the heartbeat
-// cadence and deadman, the motion deadline, the e-stop latch and the close-code policy. It has no Arduino headers:
+// cadence and deadman, the motion deadline, the stop latch and the close-code policy. It has no Arduino headers:
// it includes only ArduinoJson (v7) and the standard library, so the same file runs on an ESP32 and in the desktop
// it includes only ArduinoJson (v7) and the standard library, so the same file runs on an ESP32 and in the desktop
// conformance harness (esp32/test).
// conformance harness (esp32/test).
//
//
@@ -45,12 +45,12 @@ inline constexpr int64_t kDedupTTLMS = 600000; // a repeated id is answered fo
@@ -45,12 +45,12 @@ inline constexpr int64_t kDedupTTLMS = 600000; // a repeated id is answered fo
// Resource limits for one inbound frame. The protocol allows frames up to 1 MiB; a device with a few hundred KiB of
// Resource limits for one inbound frame. The protocol allows frames up to 1 MiB; a device with a few hundred KiB of
// heap bounds them far lower. Anything over these limits (or a frame the JSON parser cannot allocate) is treated as a
// heap bounds them far lower. Anything over these limits (or a frame the JSON parser cannot allocate) is treated as a
-// frame that might have been an e-stop: motion stops and the link is dropped to re-read config.estop_latched.
+// frame that might have been an stop: motion stops and the link is dropped to re-read config.estop_latched.
inline constexpr size_t kMaxFrameBytes = 16384;
inline constexpr size_t kMaxFrameBytes = 16384;
inline constexpr size_t kMaxJsonDepth = 10; // ArduinoJson's own default nesting limit, passed explicitly
inline constexpr size_t kMaxJsonDepth = 10; // ArduinoJson's own default nesting limit, passed explicitly
inline constexpr size_t kMaxIdBytes = 64; // command ids (and kinds) longer than this are rejected unread
inline constexpr size_t kMaxIdBytes = 64; // command ids (and kinds) longer than this are rejected unread
inline constexpr size_t kMaxKindBytes = 64;
inline constexpr size_t kMaxKindBytes = 64;
-// The e-stop reason (`by`, or a cached nack message) is bounded too: an estop frame can carry a `by` up to the frame
+// The stop reason (`by`, or a cached nack message) is bounded too: an estop frame can carry a `by` up to the frame
// limit, and that string is copied into every dedup entry that answers an estopped command. 64 bytes keeps the cache's
// limit, and that string is copied into every dedup entry that answers an estopped command. 64 bytes keeps the cache's
// total size small while leaving room for the "by " prefix.
// total size small while leaving room for the "by " prefix.
inline constexpr size_t kMaxEstopReasonBytes = 64;
inline constexpr size_t kMaxEstopReasonBytes = 64;
@@ -4,11 +4,11 @@
@@ -4,11 +4,11 @@
// header in esp32/test/.deps (run.sh fetches and checks it). It replays every fixture in
// header in esp32/test/.deps (run.sh fetches and checks it). It replays every fixture in
// internal/protocol/testdata/bot — each server-to-device frame is decoded and handled, and each device-to-server frame
// internal/protocol/testdata/bot — each server-to-device frame is decoded and handled, and each device-to-server frame
// the core emits is checked for the fixture's keys and value types — and runs the protocol's scenarios (deadman,
// the core emits is checked for the fixture's keys and value types — and runs the protocol's scenarios (deadman,
-// e-stop, clamping, nack codes, a job refusal, close codes, heartbeat cadence, hello/config ordering) plus the safety
+// stop, clamping, nack codes, a job refusal, close codes, heartbeat cadence, hello/config ordering) plus the safety
// hardening scenarios (a wall clock that steps backwards, 64-bit monotonic time across the millis() wrap, wrong-typed
// hardening scenarios (a wall clock that steps backwards, 64-bit monotonic time across the millis() wrap, wrong-typed
// latch frames, oversize/too-deep/allocation-failing frames, over-long ids and kinds, the dedup cache's ten-minute
// latch frames, oversize/too-deep/allocation-failing frames, over-long ids and kinds, the dedup cache's ten-minute
// limit, the heartbeat cap, guarded commands while the link is lost, the backoff reset, the refused-reconnect floor,
// limit, the heartbeat cap, guarded commands while the link is lost, the backoff reset, the refused-reconnect floor,
-// the pre-hello timer with the transport down, a bounded e-stop reason and an overflowing command value).
+// the pre-hello timer with the transport down, a bounded stop reason and an overflowing command value).
//
//
// Exit status is non-zero if any scenario fails; every scenario prints one PASS/FAIL line.
// Exit status is non-zero if any scenario fails; every scenario prints one PASS/FAIL line.
#include <ArduinoJson.h>
#include <ArduinoJson.h>
@@ -317,8 +317,8 @@ std::string scenarioEstop(const std::string& hello, const std::string& config) {
@@ -317,8 +317,8 @@ std::string scenarioEstop(const std::string& hello, const std::string& config) {
h.sent.clear();
h.sent.clear();
h.core.feed("{\"v\":1,\"seq\":5,\"ts\":" + std::to_string(h.now) +
h.core.feed("{\"v\":1,\"seq\":5,\"ts\":" + std::to_string(h.now) +
",\"type\":\"estop\",\"latched\":true,\"by\":\"usr_1\",\"at\":\"2026-09-29T19:20:01.200Z\"}");
",\"type\":\"estop\",\"latched\":true,\"by\":\"usr_1\",\"at\":\"2026-09-29T19:20:01.200Z\"}");
- REQUIRE(h.core.estopped(), "the e-stop did not latch");
- REQUIRE(!h.stops.empty(), "the e-stop did not stop motion");
+ REQUIRE(h.core.estopped(), "the stop did not latch");
+ REQUIRE(!h.stops.empty(), "the stop did not stop motion");
JsonDocument st;
JsonDocument st;
REQUIRE(lastOfType(h.sent, "estop_state", st), "no estop_state on latch");
REQUIRE(lastOfType(h.sent, "estop_state", st), "no estop_state on latch");
REQUIRE(st["latched"].as<bool>(), "estop_state did not report the latch");
REQUIRE(st["latched"].as<bool>(), "estop_state did not report the latch");
@@ -326,11 +326,11 @@ std::string scenarioEstop(const std::string& hello, const std::string& config) {
@@ -326,11 +326,11 @@ std::string scenarioEstop(const std::string& hello, const std::string& config) {
h.core.feed(commandFrame(id, "drive", "{\"throttle\":0.5,\"steer\":0}", h.now, h.now + 300));
h.core.feed(commandFrame(id, "drive", "{\"throttle\":0.5,\"steer\":0}", h.now, h.now + 300));
std::string fault;
std::string fault;
std::string message;
std::string message;
- REQUIRE(h.lastNack(id, fault, message), "a drive while e-stopped was not nacked");
- REQUIRE(fault == "estopped", "drive while e-stopped nacked " + fault);
- REQUIRE(h.drives.empty(), "a drive reached the motors while e-stopped");
+ REQUIRE(h.lastNack(id, fault, message), "a drive while stopped was not nacked");
+ REQUIRE(fault == "estopped", "drive while stopped nacked " + fault);
+ REQUIRE(h.drives.empty(), "a drive reached the motors while stopped");
h.core.feed("{\"v\":1,\"seq\":6,\"ts\":" + std::to_string(h.now) + ",\"type\":\"estop\",\"latched\":false}");
h.core.feed("{\"v\":1,\"seq\":6,\"ts\":" + std::to_string(h.now) + ",\"type\":\"estop\",\"latched\":false}");
- REQUIRE(!h.core.estopped(), "the e-stop did not clear");
+ REQUIRE(!h.core.estopped(), "the stop did not clear");
h.core.feed(commandFrame("cmd_after_clear", "drive", "{\"throttle\":0.5,\"steer\":0}", h.now, h.now + 300));
h.core.feed(commandFrame("cmd_after_clear", "drive", "{\"throttle\":0.5,\"steer\":0}", h.now, h.now + 300));
REQUIRE(h.drives.size() == 1, "a drive after the clear did not reach the motors");
REQUIRE(h.drives.size() == 1, "a drive after the clear did not reach the motors");
return "";
return "";
@@ -526,7 +526,7 @@ std::string scenarioWrongTypedLatch(const std::string& hello, const std::string&
@@ -526,7 +526,7 @@ std::string scenarioWrongTypedLatch(const std::string& hello, const std::string&
const std::string estop_true = "{\"v\":1,\"seq\":5,\"ts\":" + std::to_string(h.now) +
const std::string estop_true = "{\"v\":1,\"seq\":5,\"ts\":" + std::to_string(h.now) +
",\"type\":\"estop\",\"latched\":true,\"by\":\"usr_1\"}";
",\"type\":\"estop\",\"latched\":true,\"by\":\"usr_1\"}";
h.core.feed(estop_true);
h.core.feed(estop_true);
- REQUIRE(h.core.estopped(), "the e-stop did not latch");
+ REQUIRE(h.core.estopped(), "the stop did not latch");
for (const std::string& raw : {std::string("\"false\""), std::string("0"), std::string("1"), std::string("{}")}) {
for (const std::string& raw : {std::string("\"false\""), std::string("0"), std::string("1"), std::string("{}")}) {
h.core.feed("{\"v\":1,\"seq\":6,\"ts\":" + std::to_string(h.now) + ",\"type\":\"estop\",\"latched\":" + raw + "}");
h.core.feed("{\"v\":1,\"seq\":6,\"ts\":" + std::to_string(h.now) + ",\"type\":\"estop\",\"latched\":" + raw + "}");
REQUIRE(h.core.estopped(), "estop latched:" + raw + " released the latch");
REQUIRE(h.core.estopped(), "estop latched:" + raw + " released the latch");
@@ -807,7 +807,7 @@ std::string scenarioEstopReasonBound(const std::string& hello, const std::string
@@ -807,7 +807,7 @@ std::string scenarioEstopReasonBound(const std::string& hello, const std::string
const std::string big_by(16000, 'x'); // under the frame limit, far over the reason cap
const std::string big_by(16000, 'x'); // under the frame limit, far over the reason cap
h.core.feed("{\"v\":1,\"seq\":5,\"ts\":" + std::to_string(h.now) +
h.core.feed("{\"v\":1,\"seq\":5,\"ts\":" + std::to_string(h.now) +
",\"type\":\"estop\",\"latched\":true,\"by\":\"" + big_by + "\"}");
",\"type\":\"estop\",\"latched\":true,\"by\":\"" + big_by + "\"}");
- REQUIRE(h.core.estopped(), "the e-stop with a huge by did not latch");
+ REQUIRE(h.core.estopped(), "the stop with a huge by did not latch");
JsonDocument st;
JsonDocument st;
REQUIRE(lastOfType(h.sent, "estop_state", st), "no estop_state for the huge-by latch");
REQUIRE(lastOfType(h.sent, "estop_state", st), "no estop_state for the huge-by latch");
const std::string reason = st["reason"] | "";
const std::string reason = st["reason"] | "";
@@ -1033,7 +1033,7 @@ int main(int argc, char** argv) {
@@ -1033,7 +1033,7 @@ int main(int argc, char** argv) {
report("the reconnect backoff resets after a stable link", scenarioBackoffReset(hello));
report("the reconnect backoff resets after a stable link", scenarioBackoffReset(hello));
report("the 10 s floor after a refused reconnect", scenarioRefusedFloor(hello));
report("the 10 s floor after a refused reconnect", scenarioRefusedFloor(hello));
report("the pre-hello timer does not fire with the transport down", scenarioPreHelloTimer());
report("the pre-hello timer does not fire with the transport down", scenarioPreHelloTimer());
- report("a huge e-stop by is bounded in every cached message", scenarioEstopReasonBound(hello, config));
+ report("a huge stop by is bounded in every cached message", scenarioEstopReasonBound(hello, config));
report("an overflowing command or actuator value is refused", scenarioValueOverflow(hello, config));
report("an overflowing command or actuator value is refused", scenarioValueOverflow(hello, config));
if (failures > 0) {
if (failures > 0) {
@@ -355,7 +355,7 @@ func (n *Agent) Run(parent context.Context) error {
@@ -355,7 +355,7 @@ func (n *Agent) Run(parent context.Context) error {
pctx, cancelPlugins := context.WithCancel(context.Background())
pctx, cancelPlugins := context.WithCancel(context.Background())
// A stop latched on disk is set on every plugin before any starts: each process's first lines are then hello and
// A stop latched on disk is set on every plugin before any starts: each process's first lines are then hello and
- // the e-stop replayed (cause latched, first_cause what set it), before it can be routed any command.
+ // the stop replayed (cause latched, first_cause what set it), before it can be routed any command.
if st := n.latch.State(); st.Stopped() || st.Computer {
if st := n.latch.State(); st.Stopped() || st.Computer {
n.log.Warn("starting with the stop latched", "remote", st.Remote, "local", st.Local, "computer", st.Computer)
n.log.Warn("starting with the stop latched", "remote", st.Remote, "local", st.Local, "computer", st.Computer)
}
}
@@ -506,17 +506,17 @@ func (n *Agent) Frame(f protocol.Frame) {
@@ -506,17 +506,17 @@ func (n *Agent) Frame(f protocol.Frame) {
}
}
func (n *Agent) remoteEstop(reason, by string) {
func (n *Agent) remoteEstop(reason, by string) {
- n.log.Warn("e-stop from the server", "by", by)
+ n.log.Warn("stop from the server", "by", by)
n.mgr.EstopAll(plugins.EstopRemote, safety.CleanReason(reason)) // at once, before the disk write
n.mgr.EstopAll(plugins.EstopRemote, safety.CleanReason(reason)) // at once, before the disk write
if _, err := n.latch.SetRemoteWithComputer(reason, n.hasComputer()); err != nil {
if _, err := n.latch.SetRemoteWithComputer(reason, n.hasComputer()); err != nil {
- n.log.Error("could not persist the e-stop (it is held in memory)", "err", err)
+ n.log.Error("could not persist the stop (it is held in memory)", "err", err)
}
}
}
}
func (n *Agent) remoteClear(by string) {
func (n *Agent) remoteClear(by string) {
- n.log.Warn("e-stop cleared by the server", "by", by)
+ n.log.Warn("stop cleared by the server", "by", by)
if _, err := n.latch.ClearRemote(); err != nil {
if _, err := n.latch.ClearRemote(); err != nil {
- n.log.Error("could not persist the e-stop clear", "err", err)
+ n.log.Error("could not persist the stop clear", "err", err)
}
}
}
}
@@ -982,7 +982,7 @@ func (n *Agent) admitHeadlight(at mono.Instant) bool {
@@ -982,7 +982,7 @@ func (n *Agent) admitHeadlight(at mono.Instant) bool {
}
}
// computer runs one exec, fs or screen command: no clamping, a long reply window (the server's deadline, at most
// computer runs one exec, fs or screen command: no clamping, a long reply window (the server's deadline, at most
-// MaxComputerMS, else DefaultComputerMS), refused while the local stop or an e-stop is latched when it would change the
+// MaxComputerMS, else DefaultComputerMS), refused while the local stop or an stop is latched when it would change the
// machine, and answered with the plugin's data. The plugin applies its owner's control level.
// machine, and answered with the plugin's data. The plugin applies its owner's control level.
func (n *Agent) computer(c protocol.Command, entry *safety.Entry, sentMS int64, received mono.Instant, nack func(code, msg string)) {
func (n *Agent) computer(c protocol.Command, entry *safety.Entry, sentMS int64, received mono.Instant, nack func(code, msg string)) {
if len(c.Value) > protocol.MaxComputerValue {
if len(c.Value) > protocol.MaxComputerValue {
@@ -1009,7 +1009,7 @@ func (n *Agent) computer(c protocol.Command, entry *safety.Entry, sentMS int64,
@@ -1009,7 +1009,7 @@ func (n *Agent) computer(c protocol.Command, entry *safety.Entry, sentMS int64,
return
return
}
}
if st.Computer {
if st.Computer {
- nack(protocol.FaultLocalStop, "the emergency stop on this computer needs a local `openvibe-link resume`: observe only")
+ nack(protocol.FaultLocalStop, "the stop on this computer needs a local `openvibe-link resume`: observe only")
return
return
}
}
if st.Remote {
if st.Remote {
@@ -1128,7 +1128,7 @@ func latchReason(st safety.LatchState) string {
@@ -1128,7 +1128,7 @@ func latchReason(st safety.LatchState) string {
case st.Local:
case st.Local:
return st.LocalReason
return st.LocalReason
default:
default:
- return "the computer's emergency stop: `openvibe-link resume` on this machine releases it"
+ return "the computer's stop: `openvibe-link resume` on this machine releases it"
}
}
}
}
@@ -391,7 +391,7 @@ func TestMissedHeartbeatsStop(t *testing.T) {
@@ -391,7 +391,7 @@ func TestMissedHeartbeatsStop(t *testing.T) {
}
}
// TestRobotEstopLatchedUntilOwnerClears: the remote latch is the server's to clear, and the owner clears it
// TestRobotEstopLatchedUntilOwnerClears: the remote latch is the server's to clear, and the owner clears it
-// with `estop latched:false`. The computer plugin is the one exception: an e-stop on a computer waits for a
+// with `estop latched:false`. The computer plugin is the one exception: an stop on a computer waits for a
// local `openvibe-link resume` (TestComputerRemoteStopRequiresLocalResume).
// local `openvibe-link resume` (TestComputerRemoteStopRequiresLocalResume).
func TestRobotEstopLatchedUntilOwnerClears(t *testing.T) {
func TestRobotEstopLatchedUntilOwnerClears(t *testing.T) {
e := start(t)
e := start(t)
@@ -411,7 +411,7 @@ func TestRobotEstopLatchedUntilOwnerClears(t *testing.T) {
@@ -411,7 +411,7 @@ func TestRobotEstopLatchedUntilOwnerClears(t *testing.T) {
// Persisted: a restarted Agent comes back latched.
// Persisted: a restarted Agent comes back latched.
l, _ := safety.OpenLatch(e.paths.LatchFile())
l, _ := safety.OpenLatch(e.paths.LatchFile())
if !l.State().Remote {
if !l.State().Remote {
- t.Fatal("e-stop not persisted")
+ t.Fatal("stop not persisted")
}
}
// Survives a reconnect.
// Survives a reconnect.
e.conn.Close()
e.conn.Close()
@@ -685,7 +685,7 @@ func (n *Agent) stopEverything(reason string) error {
@@ -685,7 +685,7 @@ func (n *Agent) stopEverything(reason string) error {
return err
return err
}
}
-// resumeEverything releases the local kill switch (and a server e-stop).
+// resumeEverything releases the local kill switch (and a server stop).
func (n *Agent) resumeEverything() error {
func (n *Agent) resumeEverything() error {
_, err := n.latch.Resume()
_, err := n.latch.Resume()
return err
return err
@@ -23,7 +23,7 @@ import (
@@ -23,7 +23,7 @@ import (
// with stream "screen"; the core encodes and publishes them on a second WHIP session, apart from a robot's camera.
// with stream "screen"; the core encodes and publishes them on a second WHIP session, apart from a robot's camera.
//
//
// A stream ends when its time is up, on stop, when the plugin says it ended, and at once when the local kill switch
// A stream ends when its time is up, on stop, when the plugin says it ended, and at once when the local kill switch
-// or an e-stop latches; a start is refused while either is latched. One stream at a time: a new start replaces it.
+// or an stop latches; a start is refused while either is latched. One stream at a time: a new start replaces it.
const (
const (
ScreenDefaultSeconds = 600
ScreenDefaultSeconds = 600
ScreenMinSeconds = 30
ScreenMinSeconds = 30
@@ -4,9 +4,9 @@
@@ -4,9 +4,9 @@
// and then authenticates the Authorization header (a bad credential closes with 4002, a revoked device with 4003, a
// and then authenticates the Authorization header (a bad credential closes with 4002, a revoked device with 4003, a
// second connection replaces the first with 4000), sends hello and then config (always with allowed_commands and
// second connection replaces the first with 4000), sends hello and then config (always with allowed_commands and
// the robot's estop_latched), sends commands with a server-minted id, an absolute deadline_ms, the operator and the
// the robot's estop_latched), sends commands with a server-minted id, an absolute deadline_ms, the operator and the
-// robot, and answers heartbeats. Like Bot, it keeps the robot's e-stop latch: an estop frame it sends sets or clears
+// robot, and answers heartbeats. Like Bot, it keeps the robot's stop latch: an estop frame it sends sets or clears
// it, a device's estop_state latched:true sets it, and latched:false is only a report. Tests drive it to send
// it, a device's estop_state latched:true sets it, and latched:false is only a report. Tests drive it to send
-// commands, e-stops, errors and silence, and read back what the device sent.
+// commands, stops, errors and silence, and read back what the device sent.
//
//
// heartbeat_ack carries Bot's own envelope seq and echoes the heartbeat's t as both t and echo (echo null when the
// heartbeat_ack carries Bot's own envelope seq and echoes the heartbeat's t as both t and echo (echo null when the
// heartbeat had no t), with server_time as Bot's clock; the device measures RTT from the echoed send time.
// heartbeat had no t), with server_time as Bot's clock; the device measures RTT from the echoed send time.
@@ -59,7 +59,7 @@ type Server struct {
@@ -59,7 +59,7 @@ type Server struct {
relayOrigin string
relayOrigin string
relayAllowed func(string, string, string, string) bool
relayAllowed func(string, string, string, string) bool
agentResult func(string, string, json.RawMessage)
agentResult func(string, string, json.RawMessage)
- estop map[string]bool // robot id → the robot's e-stop latch, as Bot keeps it
+ estop map[string]bool // robot id → the robot's stop latch, as Bot keeps it
pairRequests []protocol.PairRequest
pairRequests []protocol.PairRequest
pairRefusal *refusal
pairRefusal *refusal
bindRefusals []refusal
bindRefusals []refusal
@@ -94,7 +94,7 @@ func (m *Manager) HaltAll(turnEpoch int64) {
@@ -94,7 +94,7 @@ func (m *Manager) HaltAll(turnEpoch int64) {
}
}
}
}
-// EstopAll latches every plugin's e-stop, saying why (EstopRemote, EstopLocal, EstopDriver, EstopLatched).
+// EstopAll latches every plugin's stop, saying why (EstopRemote, EstopLocal, EstopDriver, EstopLatched).
func (m *Manager) EstopAll(cause, reason string) {
func (m *Manager) EstopAll(cause, reason string) {
for _, p := range m.plugins {
for _, p := range m.plugins {
p.Estop(cause, reason)
p.Estop(cause, reason)
@@ -110,7 +110,7 @@ type Plugin struct {
@@ -110,7 +110,7 @@ type Plugin struct {
desc *Describe
desc *Describe
fault *protocol.Fault
fault *protocol.Fault
estopped bool
estopped bool
- // The e-stop's cause and reason, sent again (replayed) to a restarted process while the stop is latched.
+ // The stop's cause and reason, sent again (replayed) to a restarted process while the stop is latched.
estopCause, estopReason string
estopCause, estopReason string
// The newest turn_epoch a halt carried: hello tells a restarted process, so a stale turn stays refused there too.
// The newest turn_epoch a halt carried: hello tells a restarted process, so a stale turn stays refused there too.
haltEpoch int64
haltEpoch int64
@@ -302,7 +302,7 @@ const (
@@ -302,7 +302,7 @@ const (
StopLinkLost = "link_lost" // the link to Bot was lost
StopLinkLost = "link_lost" // the link to Bot was lost
StopShutdown = "shutdown" // the Link is stopping (service stop, `openvibe-link quit`)
StopShutdown = "shutdown" // the Link is stopping (service stop, `openvibe-link quit`)
StopTimeout = "timeout" // a command got no answer from the driver within its window
StopTimeout = "timeout" // a command got no answer from the driver within its window
- EstopRemote = "remote" // an e-stop from the platform (a robot owner or staff)
+ EstopRemote = "remote" // an stop from the platform (a robot owner or staff)
EstopLocal = "local" // `openvibe-link stop` on the device
EstopLocal = "local" // `openvibe-link stop` on the device
EstopDriver = "driver" // a driver's own local stop (a plugin reported one)
EstopDriver = "driver" // a driver's own local stop (a plugin reported one)
EstopLatched = "latched" // the stop was already latched when the Link or this process started
EstopLatched = "latched" // the stop was already latched when the Link or this process started
@@ -324,7 +324,7 @@ func (p *Plugin) Halt(turnEpoch int64) {
@@ -324,7 +324,7 @@ func (p *Plugin) Halt(turnEpoch int64) {
_ = p.send(msg)
_ = p.send(msg)
}
}
-// Estop latches the plugin's e-stop (the core also refuses motion itself), saying why.
+// Estop latches the plugin's stop (the core also refuses motion itself), saying why.
func (p *Plugin) Estop(cause, reason string) {
func (p *Plugin) Estop(cause, reason string) {
p.mu.Lock()
p.mu.Lock()
p.estopped, p.estopCause, p.estopReason = true, cause, reason
p.estopped, p.estopCause, p.estopReason = true, cause, reason
@@ -332,7 +332,7 @@ func (p *Plugin) Estop(cause, reason string) {
@@ -332,7 +332,7 @@ func (p *Plugin) Estop(cause, reason string) {
_ = p.send(map[string]any{"op": "estop", "cause": cause, "reason": reason})
_ = p.send(map[string]any{"op": "estop", "cause": cause, "reason": reason})
}
}
-// Resume tells the plugin the platform's e-stop latch was cleared by its owner. It is information, never a re-arm: the
+// Resume tells the plugin the platform's stop latch was cleared by its owner. It is information, never a re-arm: the
// driver keeps its own hardware latch and grants as they are.
// driver keeps its own hardware latch and grants as they are.
func (p *Plugin) Resume() {
func (p *Plugin) Resume() {
p.mu.Lock()
p.mu.Lock()
@@ -353,7 +353,7 @@ func TestCrashRestartsWithEstop(t *testing.T) {
@@ -353,7 +353,7 @@ func TestCrashRestartsWithEstop(t *testing.T) {
if p.Info().Restarts < 1 {
if p.Info().Restarts < 1 {
t.Fatal("no restart counted")
t.Fatal("no restart counted")
}
}
- // After the restart the latched e-stop is sent again right after hello, as a replay of the same emergency.
+ // After the restart the latched stop is sent again right after hello, as a replay of the same emergency.
l := fx.lines()
l := fx.lines()
var afterCrash []string
var afterCrash []string
for i, s := range l {
for i, s := range l {
@@ -368,7 +368,7 @@ func TestCrashRestartsWithEstop(t *testing.T) {
@@ -368,7 +368,7 @@ func TestCrashRestartsWithEstop(t *testing.T) {
}
}
// A latch set before the manager starts (the Link booting with its stop latched on disk) is the first thing each
// A latch set before the manager starts (the Link booting with its stop latched on disk) is the first thing each
-// process hears after hello: the e-stop replayed, naming what set it, before the plugin can be routed any command.
+// process hears after hello: the stop replayed, naming what set it, before the plugin can be routed any command.
func TestLatchSetBeforeStartIsReplayedFirst(t *testing.T) {
func TestLatchSetBeforeStartIsReplayedFirst(t *testing.T) {
logPath := filepath.Join(t.TempDir(), "plugin.log")
logPath := filepath.Join(t.TempDir(), "plugin.log")
exe, _ := os.Executable()
exe, _ := os.Executable()
@@ -36,7 +36,7 @@ const (
@@ -36,7 +36,7 @@ const (
TypeHello = "hello"
TypeHello = "hello"
TypeConfig = "config"
TypeConfig = "config"
TypeCommand = "command"
TypeCommand = "command"
- TypeEstop = "estop" // latched true sets the e-stop, false is the owner clearing it
+ TypeEstop = "estop" // latched true sets the stop, false is the owner clearing it
TypeHeartbeatAck = "heartbeat_ack"
TypeHeartbeatAck = "heartbeat_ack"
TypeError = "error"
TypeError = "error"
// TypeVoiceAnswer is Vibe's answer to a voice_ask (contracts 0.214.0): working, answered or refused, to speak.
// TypeVoiceAnswer is Vibe's answer to a voice_ask (contracts 0.214.0): working, answered or refused, to speak.
@@ -134,9 +134,9 @@ const (
@@ -134,9 +134,9 @@ const (
JobNotAvailable = "class not available"
JobNotAvailable = "class not available"
// Refused by a running worker (platform.job@1: a missing artifact, the local kill switch, limits it cannot honor).
// Refused by a running worker (platform.job@1: a missing artifact, the local kill switch, limits it cannot honor).
- JobUnknownArtifact = "unknown artifact" // not a function declared in the Link's local config
- JobStopped = "the e-stop or local stop is latched" // fault estopped or local_stop
- JobBusy = "worker busy" // worker.caps.max_jobs are running; fault not_ready
+ JobUnknownArtifact = "unknown artifact" // not a function declared in the Link's local config
+ JobStopped = "the stop or local stop is latched" // fault estopped or local_stop
+ JobBusy = "worker busy" // worker.caps.max_jobs are running; fault not_ready
)
)
// job_exit reasons.
// job_exit reasons.
@@ -222,7 +222,7 @@ const (
@@ -222,7 +222,7 @@ const (
// ComputerActs reports whether a computer command changes the machine (runs a program, writes a file, uses the mouse
// ComputerActs reports whether a computer command changes the machine (runs a program, writes a file, uses the mouse
// or keyboard, drives the browser, starts or stops the private desk): those are refused while the local stop or an
// or keyboard, drives the browser, starts or stops the private desk): those are refused while the local stop or an
-// e-stop is latched. Capturing the screen, reading files, reading or closing the browser and the desk's status are not.
+// stop is latched. Capturing the screen, reading files, reading or closing the browser and the desk's status are not.
func ComputerActs(kind string, value json.RawMessage) bool {
func ComputerActs(kind string, value json.RawMessage) bool {
switch kind {
switch kind {
case KindExec, KindInput:
case KindExec, KindInput:
@@ -255,7 +255,7 @@ func ComputerActs(kind string, value json.RawMessage) bool {
@@ -255,7 +255,7 @@ func ComputerActs(kind string, value json.RawMessage) bool {
return false
return false
}
}
-// GuardedKinds move something and are refused while an e-stop or the local kill switch is latched.
+// GuardedKinds move something and are refused while an stop or the local kill switch is latched.
var GuardedKinds = map[string]bool{KindDrive: true, KindActuator: true, KindPTZ: true, KindButton: true, KindPoint: true}
var GuardedKinds = map[string]bool{KindDrive: true, KindActuator: true, KindPTZ: true, KindButton: true, KindPoint: true}
// Fault codes carried in nack.fault_code and status.faults[].code.
// Fault codes carried in nack.fault_code and status.faults[].code.
@@ -264,7 +264,7 @@ const (
@@ -264,7 +264,7 @@ const (
FaultBadValue = "bad_value" // a value is missing, not a number, or out of shape
FaultBadValue = "bad_value" // a value is missing, not a number, or out of shape
FaultUnsupported = "unsupported" // no driver on this device handles the kind or the actuator
FaultUnsupported = "unsupported" // no driver on this device handles the kind or the actuator
FaultNotAllowed = "not_allowed" // the kind is not in config.allowed
FaultNotAllowed = "not_allowed" // the kind is not in config.allowed
- FaultEstopped = "estopped" // the remote e-stop is latched
+ FaultEstopped = "estopped" // the remote stop is latched
FaultLocalStop = "local_stop" // the local kill switch (`openvibe-link stop`) is latched
FaultLocalStop = "local_stop" // the local kill switch (`openvibe-link stop`) is latched
FaultNoHeartbeat = "no_heartbeat" // a plugin lost the core's heartbeat
FaultNoHeartbeat = "no_heartbeat" // a plugin lost the core's heartbeat
FaultNotReady = "not_ready" // the driver is starting or restarting, or the server's config has not arrived
FaultNotReady = "not_ready" // the driver is starting or restarting, or the server's config has not arrived
@@ -402,7 +402,7 @@ type CommandProvenance struct {
@@ -402,7 +402,7 @@ type CommandProvenance struct {
ClockUncertaintyMS *int64 `json:"clock_uncertainty_ms,omitempty"`
ClockUncertaintyMS *int64 `json:"clock_uncertainty_ms,omitempty"`
}
}
-// Estop is sent when the e-stop latches (Latched true) or the owner clears it (Latched false).
+// Estop is sent when the stop latches (Latched true) or the owner clears it (Latched false).
type Estop struct {
type Estop struct {
Latched bool `json:"latched"`
Latched bool `json:"latched"`
By string `json:"by,omitempty"`
By string `json:"by,omitempty"`
@@ -7,6 +7,7 @@ import (
@@ -7,6 +7,7 @@ import (
"os"
"os"
"path/filepath"
"path/filepath"
"reflect"
"reflect"
+ "regexp"
"strings"
"strings"
"testing"
"testing"
)
)
@@ -485,3 +486,38 @@ func TestHeadlightFrameCarriesBotProvenance(t *testing.T) {
@@ -485,3 +486,38 @@ func TestHeadlightFrameCarriesBotProvenance(t *testing.T) {
t.Fatalf("headlight kind: known=%v guarded=%v computer=%v", known, GuardedKinds[KindHeadlight], ComputerKinds[KindHeadlight])
t.Fatalf("headlight kind: known=%v guarded=%v computer=%v", known, GuardedKinds[KindHeadlight], ComputerKinds[KindHeadlight])
}
}
}
}
+
+func TestDocsSayStopNotEStop(t *testing.T) {
+ root := filepath.Join("..", "..")
+ var files []string
+ for _, name := range []string{"AGENTS.md", "README.md", "STATUS.json"} {
+ files = append(files, filepath.Join(root, name))
+ }
+ err := filepath.WalkDir(filepath.Join(root, "docs"), func(path string, d os.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if !d.IsDir() && strings.HasSuffix(path, ".md") {
+ files = append(files, path)
+ }
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ fence := regexp.MustCompile("(?s)```.*?```")
+ code := regexp.MustCompile("`[^`]*`")
+ word := regexp.MustCompile(`(?i)(^|[^A-Za-z])e-?stop([^A-Za-z]|$)|emergency\s+stop`)
+ for _, file := range files {
+ raw, err := os.ReadFile(file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ text := fence.ReplaceAllString(string(raw), "")
+ text = code.ReplaceAllString(text, "")
+ if loc := word.FindStringIndex(text); loc != nil {
+ line := strings.Count(text[:loc[0]], "\n") + 1
+ t.Errorf("%s:%d still says e-stop; these are small hobby robots, write Stop", file, line)
+ }
+ }
+}
@@ -1 +1 @@
@@ -1 +1 @@
-{ "v": 1, "seq": 4, "ts": 1738065601001, "type": "error", "code": "bot.forbidden", "detail": "only the owner clears the e-stop" }
+{ "v": 1, "seq": 4, "ts": 1738065601001, "type": "error", "code": "bot.forbidden", "detail": "only the owner clears the stop" }
@@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
-// Package safety holds the rules that keep motors from running when they should not: the persisted e-stop and local
+// Package safety holds the rules that keep motors from running when they should not: the persisted stop and local
// kill-switch latch, the owner's limits applied to every value before it reaches a plugin, deadline capping, and the
// kill-switch latch, the owner's limits applied to every value before it reaches a plugin, deadline capping, and the
// command-id cache that makes a repeated command answer with its first result instead of running twice.
// command-id cache that makes a repeated command answer with its first result instead of running twice.
package safety
package safety
@@ -14,7 +14,7 @@ import (
@@ -14,7 +14,7 @@ import (
"time"
"time"
)
)
-// LatchState is the persisted e-stop state. Two independent latches:
+// LatchState is the persisted stop state. Two independent latches:
//
//
// - Remote: set by a server `estop`; cleared by a server `estop` with latched:false (the owner) or a local `resume`.
// - Remote: set by a server `estop`; cleared by a server `estop` with latched:false (the owner) or a local `resume`.
// - Local: set on the machine itself — `openvibe-link stop`, or a driver's own stop (`local_stop`); cleared only by
// - Local: set on the machine itself — `openvibe-link stop`, or a driver's own stop (`local_stop`); cleared only by
@@ -36,7 +36,7 @@ type LatchState struct {
@@ -36,7 +36,7 @@ type LatchState struct {
// so a remote clear can still release robot plugins.
// so a remote clear can still release robot plugins.
func (s LatchState) Stopped() bool { return s.Remote || s.Local }
func (s LatchState) Stopped() bool { return s.Remote || s.Local }
-// Latch is the persisted latch. It survives restarts: a Link that was e-stopped comes back e-stopped.
+// Latch is the persisted latch. It survives restarts: a Link that was stopped comes back stopped.
type Latch struct {
type Latch struct {
mu sync.Mutex
mu sync.Mutex
path string
path string
@@ -134,7 +134,7 @@ func (l *Latch) persistLocked() error {
@@ -134,7 +134,7 @@ func (l *Latch) persistLocked() error {
return nil
return nil
}
}
-// SetRemote latches the server e-stop.
+// SetRemote latches the server stop.
func (l *Latch) SetRemote(reason string) (LatchState, error) {
func (l *Latch) SetRemote(reason string) (LatchState, error) {
return l.SetRemoteWithComputer(reason, false)
return l.SetRemoteWithComputer(reason, false)
}
}
@@ -150,7 +150,7 @@ func (l *Latch) SetRemoteWithComputer(reason string, computer bool) (LatchState,
@@ -150,7 +150,7 @@ func (l *Latch) SetRemoteWithComputer(reason string, computer bool) (LatchState,
})
})
}
}
-// ClearRemote clears the server e-stop (the owner's estop latched:false). The local latch is untouched.
+// ClearRemote clears the server stop (the owner's estop latched:false). The local latch is untouched.
func (l *Latch) ClearRemote() (LatchState, error) {
func (l *Latch) ClearRemote() (LatchState, error) {
return l.update(func(s *LatchState) { s.Remote, s.RemoteReason, s.RemoteAt = false, "", time.Time{} })
return l.update(func(s *LatchState) { s.Remote, s.RemoteReason, s.RemoteAt = false, "", time.Time{} })
}
}
@@ -261,7 +261,7 @@ func (w *Worker) ExitAck(id string) {
@@ -261,7 +261,7 @@ func (w *Worker) ExitAck(id string) {
}
}
}
}
-// SetStopped follows the stop latch (the e-stop or the local stop). A fault code kills every job at once (job_exit
+// SetStopped follows the stop latch (the stop or the local stop). A fault code kills every job at once (job_exit
// says stopped) and refuses new ones with that fault until SetStopped("").
// says stopped) and refuses new ones with that fault until SetStopped("").
func (w *Worker) SetStopped(fault string) {
func (w *Worker) SetStopped(fault string) {
w.mu.Lock()
w.mu.Lock()
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
# openvibe-adeept-adr036
# openvibe-adeept-adr036
OpenVibe Link plugin for the **Adeept 4WD Smart Car Kit for Raspberry Pi (ADR036)**, ordinary or mecanum wheels,
OpenVibe Link plugin for the **Adeept 4WD Smart Car Kit for Raspberry Pi (ADR036)**, ordinary or mecanum wheels,
-on Raspberry Pi OS Bookworm. Built on `openvibe_plugin` (deadlines, heartbeat loss, stdin EOF, e-stop and halt all stop
+on Raspberry Pi OS Bookworm. Built on `openvibe_plugin` (deadlines, heartbeat loss, stdin EOF, stop and halt all stop
the wheels). The mapping was checked against Adeept's `Server/Server_*Wheels/Move.py` and `Voltage.py`; none of the
the wheels). The mapping was checked against Adeept's `Server/Server_*Wheels/Move.py` and `Voltage.py`; none of the
kit's code is imported.
kit's code is imported.
@@ -618,7 +618,7 @@ class Ai(Plugin):
@@ -618,7 +618,7 @@ class Ai(Plugin):
self.pool_stopped.wait(2)
self.pool_stopped.wait(2)
def on_stop(self, info):
def on_stop(self, info):
- """The kill switch on this machine (an emergency stop) stops the whole pool now and holds it until the latch is
+ """The kill switch on this machine (an stop) stops the whole pool now and holds it until the latch is
cleared; the Link stopping closes it and keeps the key. A turn's halt, a lost link or a command's timeout is not
cleared; the Link stopping closes it and keeps the key. A turn's halt, a lost link or a command's timeout is not
about the pool. Without a pool, every stop is as before (nothing moves here)."""
about the pool. Without a pool, every stop is as before (nothing moves here)."""
if not self.cfg.get("pool_directory"):
if not self.cfg.get("pool_directory"):
@@ -11,7 +11,7 @@ command can never raise it:
@@ -11,7 +11,7 @@ command can never raise it:
* ``trusted``: run programs, write files inside the roots and use the mouse and keyboard without asking.
* ``trusted``: run programs, write files inside the roots and use the mouse and keyboard without asking.
* ``full``: as trusted, anywhere the account running the Node can reach.
* ``full``: as trusted, anywhere the account running the Node can reach.
-The local kill switch (``openvibe-link stop``) and a remote e-stop drop the plugin to observe, kill what is running
+The local kill switch (``openvibe-link stop``) and a remote stop drop the plugin to observe, kill what is running
and deny what waits; the core refuses acting commands while either is latched too. The plugin runs as the Node's own
and deny what waits; the core refuses acting commands while either is latched too. The plugin runs as the Node's own
account and never as root, except on a shared desk host for other people, where it runs as root and every desk sits in
account and never as root, except on a shared desk host for other people, where it runs as root and every desk sits in
its own sandbox.
its own sandbox.
@@ -32,7 +32,7 @@ Command kinds and their answers (the ack's ``data``):
@@ -32,7 +32,7 @@ Command kinds and their answers (the ack's ``data``):
most 5) with the same tools as ``screen`` and hands each to the core as video (``stream: "screen"``), which publishes
most 5) with the same tools as ``screen`` and hands each to the core as video (``stream: "screen"``), which publishes
it to the owner's private stream; the core alone holds the WHIP URL and key. It runs at every level, like a
it to the owner's private stream; the core alone holds the WHIP URL and key. It runs at every level, like a
screenshot, says so on the desktop when it starts (config ``notify``), and ends when its time is up (30 s to 30 min),
screenshot, says so on the desktop when it starts (config ``notify``), and ends when its time is up (30 s to 30 min),
- on stop, and at once on the local kill switch or an e-stop. Config ``allow_screen_stream: false`` turns it off.
+ on stop, and at once on the local kill switch or an stop. Config ``allow_screen_stream: false`` turns it off.
* ``input`` ``{"action", "x"?, "y"?, "width"?, "height"?, "dy"?, "text"?, "keys"?}`` → ``{done, action, x?, y?}``: move,
* ``input`` ``{"action", "x"?, "y"?, "width"?, "height"?, "dy"?, "text"?, "keys"?}`` → ``{done, action, x?, y?}``: move,
click, double_click or right_click at a point, scroll by ``dy`` clicks, type ``text`` into whatever has focus, or press
click, double_click or right_click at a point, scroll by ``dy`` clicks, type ``text`` into whatever has focus, or press
one key or chord (``keys``, for example ``ctrl+s``). ``x``/``y`` are in the space of a screen picture ``width`` ×
one key or chord (``keys``, for example ``ctrl+s``). ``x``/``y`` are in the space of a screen picture ``width`` ×
@@ -564,13 +564,13 @@ class Computer(Plugin):
@@ -564,13 +564,13 @@ class Computer(Plugin):
# ---- safety ----------------------------------------------------------------------------------------------------
# ---- safety ----------------------------------------------------------------------------------------------------
def effective_level(self):
def effective_level(self):
- """The level now: observe while a local stop or an e-stop is latched."""
+ """The level now: observe while a local stop or an stop is latched."""
if self.runtime is not None and self.runtime.estopped:
if self.runtime is not None and self.runtime.estopped:
return "observe"
return "observe"
return self.level
return self.level
def stop(self):
def stop(self):
- """halt, stop, the local kill switch and an e-stop: kill what runs, deny what waits."""
+ """halt, stop, the local kill switch and an stop: kill what runs, deny what waits."""
with self.mu:
with self.mu:
# The generation rises before anything is killed: a closure authorized before this stop fails its check.
# The generation rises before anything is killed: a closure authorized before this stop fails its check.
self.generation += 1
self.generation += 1
@@ -1012,7 +1012,7 @@ class Computer(Plugin):
@@ -1012,7 +1012,7 @@ class Computer(Plugin):
if RANK[level] < RANK["ask"]:
if RANK[level] < RANK["ask"]:
raise Fault("not_allowed", "this machine allows observing only")
raise Fault("not_allowed", "this machine allows observing only")
# The Node has opened and checked the single-use encrypted Companion grant. At level ask this is the
# The Node has opened and checked the single-use encrypted Companion grant. At level ask this is the
- # owner's approval; observe-only and the Node's emergency stop still refuse it.
+ # owner's approval; observe-only and the Node's stop still refuse it.
return self._act(cmd, "trusted", run, summary, threaded=True)
return self._act(cmd, "trusted", run, summary, threaded=True)
if acting:
if acting:
return self._act(cmd, level, run, summary, threaded=True)
return self._act(cmd, level, run, summary, threaded=True)
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
This plugin drives a ROS 2 robot from OpenVibe.Bot: TurtleBot 3 or 4, a Jackal, a home-built base, anything that takes
This plugin drives a ROS 2 robot from OpenVibe.Bot: TurtleBot 3 or 4, a Jackal, a home-built base, anything that takes
`geometry_msgs/msg/Twist` on a `cmd_vel` topic. The Link runs on the robot's computer, or on a computer on the same
`geometry_msgs/msg/Twist` on a `cmd_vel` topic. The Link runs on the robot's computer, or on a computer on the same
network. The plugin turns Bot's commands into ROS 2 messages, and the robot's topics into telemetry and camera frames.
network. The plugin turns Bot's commands into ROS 2 messages, and the robot's topics into telemetry and camera frames.
-Bot's gate (roles, the owner's limits, cooldowns and the e-stop) has already judged every command before it gets here.
+Bot's gate (roles, the owner's limits, cooldowns and the stop) has already judged every command before it gets here.
## What it bridges
## What it bridges
@@ -7,7 +7,7 @@ says how to move its hardware:
@@ -7,7 +7,7 @@ says how to move its hardware:
* describe first, touch hardware only after ``hello`` (and never in probe mode);
* describe first, touch hardware only after ``hello`` (and never in probe mode);
* stop every actuator when stdin closes, when a motion command's deadline passes without a newer command, when no
* stop every actuator when stdin closes, when a motion command's deadline passes without a newer command, when no
heartbeat has arrived from the core for ``HEARTBEAT_TIMEOUT_S``, and on ``stop`` / ``estop``;
heartbeat has arrived from the core for ``HEARTBEAT_TIMEOUT_S``, and on ``stop`` / ``estop``;
-* keep an e-stop latched until the core sends ``resume``.
+* keep an stop latched until the core sends ``resume``.
See docs/plugins.md for the wire format.
See docs/plugins.md for the wire format.
"""
"""
@@ -34,7 +34,7 @@ MAX_LONG_DEADLINE_MS = 600000
@@ -34,7 +34,7 @@ MAX_LONG_DEADLINE_MS = 600000
# Kinds that move something continuously and therefore stop at their deadline. A plugin may widen this set
# Kinds that move something continuously and therefore stop at their deadline. A plugin may widen this set
# (Plugin.motion_kinds) for actuators it drives by speed rather than by position.
# (Plugin.motion_kinds) for actuators it drives by speed rather than by position.
MOTION_KINDS = frozenset({"drive"})
MOTION_KINDS = frozenset({"drive"})
-# Kinds refused while an e-stop is latched or the heartbeat is lost. halt is always accepted.
+# Kinds refused while an stop is latched or the heartbeat is lost. halt is always accepted.
GUARDED_KINDS = frozenset({"drive", "ptz", "actuator"})
GUARDED_KINDS = frozenset({"drive", "ptz", "actuator"})
@@ -191,7 +191,7 @@ class StopInfo:
@@ -191,7 +191,7 @@ class StopInfo:
ends with, so all actuators start stopped), "deadline" (a motion command's
ends with, so all actuators start stopped), "deadline" (a motion command's
own deadline passed; motion_only is True: stop what moves on its own), "heartbeat" (the core went quiet),
own deadline passed; motion_only is True: stop what moves on its own), "heartbeat" (the core went quiet),
"handler_error", "eof". An older core sends no cause: "stop".
"handler_error", "eof". An older core sends no cause: "stop".
- emergency is True for the platform's e-stop, with cause "remote" (a robot owner or staff), "local"
+ emergency is True for the platform's stop, with cause "remote" (a robot owner or staff), "local"
(`openvibe-link stop` on the device), "driver" (a driver's own local stop) or "latched" (the stop was already
(`openvibe-link stop` on the device), "driver" (a driver's own local stop) or "latched" (the stop was already
latched when this process or the Node started: replayed is True and first_cause is the original cause when the core
latched when this process or the Node started: replayed is True and first_cause is the original cause when the core
knows it); an older core sends "estop". reason is the latch's reason text, when there is one.
knows it); an older core sends "estop". reason is the latch's reason text, when there is one.
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project]
[project]
name = "openvibe-plugin"
name = "openvibe-plugin"
version = "0.36.0"
version = "0.36.0"
-description = "Runtime for OpenVibe Link plugins (JSON lines over stdin/stdout, deadlines, heartbeat, e-stop)"
+description = "Runtime for OpenVibe Link plugins (JSON lines over stdin/stdout, deadlines, heartbeat, stop)"
requires-python = ">=3.9"
requires-python = ">=3.9"
license = { text = "AGPL-3.0-or-later" }
license = { text = "AGPL-3.0-or-later" }
@@ -422,7 +422,7 @@ class RuntimeTest(unittest.TestCase):
@@ -422,7 +422,7 @@ class RuntimeTest(unittest.TestCase):
self.assertGreater(p.handled[0].deadline_ms, 0)
self.assertGreater(p.handled[0].deadline_ms, 0)
def test_a_replayed_estop_is_applied_before_any_motion(self):
def test_a_replayed_estop_is_applied_before_any_motion(self):
- # A restarted process hears, in this order: hello (with the newest halt epoch), the latched e-stop replayed,
+ # A restarted process hears, in this order: hello (with the newest halt epoch), the latched stop replayed,
# and only then commands (the core routes none to it before its ready, and lines reach it in the order they
# and only then commands (the core routes none to it before its ready, and lines reach it in the order they
# were queued). No motion handler runs while the replayed latch holds; resume is information only.
# were queued). No motion handler runs while the replayed latch holds; resume is information only.
rt, p, out, clock = make()
rt, p, out, clock = make()