openvibers/openvibe.link Commits

Words: Stop, not E-STOP (#164)

OP OpenVibers committed 20 h ago · eafca23c06 · 41 files, +170 −135

The E-STOP language leaves Link's pages, docs, comments and messages; the protocol field names devices already send stay exactly as they are.

- Docs and CLI messages say Stop / local stop; the LOOI page says in one sentence that the tray's own Stop stays local.
- No behaviour change: the local kill switch and the legacy remote latch still work, and `halt` is the plain stop command.
- A Go test now checks that Link's docs, README and AGENTS.md never say E-STOP, estop or emergency stop outside inline code names.

Checked: `go test ./internal/...`.

Changes

ddiff --git a/AGENTS.md b/AGENTS.md
iindex b110fc3..b5c8cf0 100644
---- a/AGENTS.md
++++ b/AGENTS.md
@@@ -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
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).
- **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 after
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,
-- 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
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
@@@ -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. Every
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.
- **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 through
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.
## 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 shared
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
ddiff --git a/README.md b/README.md
iindex 250cfb8..0580fc4 100644
---- a/README.md
++++ b/README.md
@@@ -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 video
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
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.
([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 admin
and signed by Bot (Ed25519 over its canonical JSON). `openvibe-link profile verify community.<name>` checks that
ddiff --git a/STATUS.json b/STATUS.json
iindex 1602d16..aa0f524 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -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"
],
ddiff --git a/android/scripts/screenshots.sh b/android/scripts/screenshots.sh
iindex fb78391..e1a2fbb 100644
---- a/android/scripts/screenshots.sh
++++ b/android/scripts/screenshots.sh
@@@ -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 || true
adb shell am start -W -n network.openvibe.link/.PreviewActivity --es screen "$screen" >/dev/null
sleep 2
adb exec-out screencap -p > "$out/$screen.png"
@@@ -26,7 +26,7 @@ for screen in pair approvals camera location notify activity phone; do
done
# 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
sleep 2
adb exec-out screencap -p > "$out/app.png"
ddiff --git a/cmd/openvibe-link/main.go b/cmd/openvibe-link/main.go
iindex 52d7e41..ca5268f 100644
---- a/cmd/openvibe-link/main.go
++++ b/cmd/openvibe-link/main.go
@@@ -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"
}
ddiff --git a/docs/ADR-043-bot-devices-and-control.md b/docs/ADR-043-bot-devices-and-control.md
iindex d0b273d..2c14c09 100644
---- a/docs/ADR-043-bot-devices-and-control.md
++++ b/docs/ADR-043-bot-devices-and-control.md
@@@ -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 lost
heartbeat (1 s interval, stop after 2 missed) and on every disconnect or crash path, without asking the network. The
-- e-stop is latched on the device and in Bot and only the owner clears it. Speed, turn and range limits and time windows
++ stop is latched on the device and in Bot and only the owner clears it. Speed, turn and range limits and time windows
are set by the owner; an operator never exceeds them. Cozmo additionally stops on a cliff or pick-up event. The agent
never runs or exposes a kit's stock control server (the Adeept `admin:123456` port stays off).
7. **Robot profiles, not rows.** `bot.robot-profile@1` (Contracts): capabilities (drive differential/mecanum, pan-tilt,
@@@ -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
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
-- 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.
- 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.
ddiff --git a/docs/HANDOFF.md b/docs/HANDOFF.md
iindex 9467a11..c60af5b 100644
---- a/docs/HANDOFF.md
++++ b/docs/HANDOFF.md
@@@ -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
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`,
-- `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.
- 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 backoff
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`):
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 the
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.
## Plugin 2: `cozmo` (a bridge plugin)
ddiff --git a/docs/ai.md b/docs/ai.md
iindex 59dd184..c9e5d1e 100644
---- a/docs/ai.md
++++ b/docs/ai.md
@@@ -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
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
-- 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.
### Setting one up
ddiff --git a/docs/computer.md b/docs/computer.md
iindex 32b2d82..5db0da6 100644
---- a/docs/computer.md
++++ b/docs/computer.md
@@@ -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
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
--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
latch, 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 of
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.
- **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.
ddiff --git a/docs/hardware-adeept.md b/docs/hardware-adeept.md
iindex 1852076..5a1f8cc 100644
---- a/docs/hardware-adeept.md
++++ b/docs/hardware-adeept.md
@@@ -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"`).
ddiff --git a/docs/hardware-looi.md b/docs/hardware-looi.md
iindex 4685ab6..8938d44 100644
---- a/docs/hardware-looi.md
++++ b/docs/hardware-looi.md
@@@ -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
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
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
@@@ -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, the
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
ddiff --git a/docs/install.md b/docs/install.md
iindex bbd8f06..fa48629 100644
---- a/docs/install.md
++++ b/docs/install.md
@@@ -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 |
ddiff --git a/docs/plugins.md b/docs/plugins.md
iindex de3d1a4..e9976e0 100644
---- a/docs/plugins.md
++++ b/docs/plugins.md
@@@ -39,7 +39,7 @@ computer plugin's shared desks) reads the same config as the Link that started i
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
--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 cha
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
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:
@@@ -333,9 +332,9 @@ Reviewed again by the same authors against v0.17.0 (2026-10-27, `632207f`), and
owner's command.
- `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.
-- - **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 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
they were queued. So the replay arrives before any command, and the runtime refuses motion (`estopped`) from
then on.
@@@ -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`,
`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 emergency
plugin 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 STOP
def on_resume(self, info): # the owner cleared the platform's latch: information, never a re-arm
ddiff --git a/docs/protocol.md b/docs/protocol.md
iindex e633fe1..9b5c3d7 100644
---- a/docs/protocol.md
++++ b/docs/protocol.md
@@@ -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
(`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 offline
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.
`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
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.
`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).
### 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 the
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:
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 by
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).
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 and
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,
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.
@@@ -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
ddiff --git a/docs/worker.md b/docs/worker.md
iindex c60a9b4..1e9318b 100644
---- a/docs/worker.md
++++ b/docs/worker.md
@@@ -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
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
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.
@@@ -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 |
ddiff --git a/esp32/README.md b/esp32/README.md
iindex a096514..8e28ea5 100644
---- a/esp32/README.md
++++ b/esp32/README.md
@@@ -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 stretch
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
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
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.
## What it does not
@@@ -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
`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.
## 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 (including
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,
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
--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.
## 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` carry
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
-- 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
ddiff --git a/esp32/examples/Basic/Basic.ino b/esp32/examples/Basic/Basic.ino
iindex ea4cf3b..bee0a53 100644
---- a/esp32/examples/Basic/Basic.ino
++++ b/esp32/examples/Basic/Basic.ino
@@@ -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
ddiff --git a/esp32/library.properties b/esp32/library.properties
iindex 7bb165d..4ecc712 100644
---- a/esp32/library.properties
++++ b/esp32/library.properties
@@@ -2,7 +2,7 @@ name=OpenVibeLink
version=0.1.0
author=OpenVibers
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.
category=Communication
url=https://github.com/OpenVibers/OpenVibe.Link
ddiff --git a/esp32/src/ov_core.cpp b/esp32/src/ov_core.cpp
iindex 3fe2e87..7876ef7 100644
---- a/esp32/src/ov_core.cpp
++++ b/esp32/src/ov_core.cpp
@@@ -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 either
notify_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();
}
}
ddiff --git a/esp32/src/ov_core.h b/esp32/src/ov_core.h
iindex ff8b00d..ea721de 100644
---- a/esp32/src/ov_core.h
++++ b/esp32/src/ov_core.h
@@@ -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 explicitly
inline constexpr size_t kMaxIdBytes = 64; // command ids (and kinds) longer than this are rejected unread
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
// total size small while leaving room for the "by " prefix.
inline constexpr size_t kMaxEstopReasonBytes = 64;
ddiff --git a/esp32/test/conformance.cpp b/esp32/test/conformance.cpp
iindex 42224aa..eecb572 100644
---- a/esp32/test/conformance.cpp
++++ b/esp32/test/conformance.cpp
@@@ -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::string
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) +
",\"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) {
ddiff --git a/internal/agent/agent.go b/internal/agent/agent.go
iindex 7fb6563..1e6b0fe 100644
---- a/internal/agent/agent.go
++++ b/internal/agent/agent.go
@@@ -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 write
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) {
-- 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.LocalReason
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"
}
}
ddiff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go
iindex 491be8b..69b25fb 100644
---- a/internal/agent/agent_test.go
++++ b/internal/agent/agent_test.go
@@@ -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()
ddiff --git a/internal/agent/panel.go b/internal/agent/panel.go
iindex cad28cb..edbc34b 100644
---- a/internal/agent/panel.go
++++ b/internal/agent/panel.go
@@@ -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
ddiff --git a/internal/agent/screen.go b/internal/agent/screen.go
iindex cea89ab..bf18896 100644
---- a/internal/agent/screen.go
++++ b/internal/agent/screen.go
@@@ -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 = 600
ScreenMinSeconds = 30
ddiff --git a/internal/fakebot/fakebot.go b/internal/fakebot/fakebot.go
iindex f823967..0d7b5ad 100644
---- a/internal/fakebot/fakebot.go
++++ b/internal/fakebot/fakebot.go
@@@ -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 string
relayAllowed func(string, string, string, string) bool
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
pairRefusal *refusal
bindRefusals []refusal
ddiff --git a/internal/plugins/manager.go b/internal/plugins/manager.go
iindex aa1d39f..3dd574a 100644
---- a/internal/plugins/manager.go
++++ b/internal/plugins/manager.go
@@@ -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)
ddiff --git a/internal/plugins/plugin.go b/internal/plugins/plugin.go
iindex 0752107..d476879 100644
---- a/internal/plugins/plugin.go
++++ b/internal/plugins/plugin.go
@@@ -110,7 +110,7 @@ type Plugin struct {
desc *Describe
fault *protocol.Fault
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
// 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 lost
StopShutdown = "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 device
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
@@@ -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()
ddiff --git a/internal/plugins/plugins_test.go b/internal/plugins/plugins_test.go
iindex 9b0fc55..b53b2b2 100644
---- a/internal/plugins/plugins_test.go
++++ b/internal/plugins/plugins_test.go
@@@ -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 []string
for 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()
ddiff --git a/internal/protocol/protocol.go b/internal/protocol/protocol.go
iindex ccb93f8..31b3851 100644
---- a/internal/protocol/protocol.go
++++ b/internal/protocol/protocol.go
@@@ -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 it
TypeHeartbeatAck = "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 shape
FaultUnsupported = "unsupported" // no driver on this device handles the kind or the actuator
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
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
@@@ -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"`
ddiff --git a/internal/protocol/protocol_test.go b/internal/protocol/protocol_test.go
iindex 9ddd43a..20cfa1f 100644
---- a/internal/protocol/protocol_test.go
++++ b/internal/protocol/protocol_test.go
@@@ -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)
++ }
++ }
++}
ddiff --git a/internal/protocol/testdata/bot/error.json b/internal/protocol/testdata/bot/error.json
iindex 96cef4b..b22e007 100644
---- a/internal/protocol/testdata/bot/error.json
++++ b/internal/protocol/testdata/bot/error.json
@@@ -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" }
ddiff --git a/internal/safety/latch.go b/internal/safety/latch.go
iindex 51ec2ee..971cc8e 100644
---- a/internal/safety/latch.go
++++ b/internal/safety/latch.go
@@@ -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.Mutex
path 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{} })
}
ddiff --git a/internal/worker/worker.go b/internal/worker/worker.go
iindex 582f4de..f6a85c0 100644
---- a/internal/worker/worker.go
++++ b/internal/worker/worker.go
@@@ -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()
ddiff --git a/plugins/adeept_adr036/README.md b/plugins/adeept_adr036/README.md
iindex 208f3f0..fedbf31 100644
---- a/plugins/adeept_adr036/README.md
++++ b/plugins/adeept_adr036/README.md
@@@ -1,7 +1,7 @@
# openvibe-adeept-adr036
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
kit's code is imported.
ddiff --git a/plugins/ai/openvibe_ai/__init__.py b/plugins/ai/openvibe_ai/__init__.py
iindex 81b3370..13d8f0e 100644
---- a/plugins/ai/openvibe_ai/__init__.py
++++ b/plugins/ai/openvibe_ai/__init__.py
@@@ -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 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
about the pool. Without a pool, every stop is as before (nothing moves here)."""
if not self.cfg.get("pool_directory"):
ddiff --git a/plugins/computer/openvibe_computer/__init__.py b/plugins/computer/openvibe_computer/__init__.py
iindex e8c33c2..6016dc7 100644
---- a/plugins/computer/openvibe_computer/__init__.py
++++ b/plugins/computer/openvibe_computer/__init__.py
@@@ -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 running
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
its 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 publishes
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),
-- 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 press
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):
# ---- 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.level
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:
# 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)
ddiff --git a/plugins/ros2/README.md b/plugins/ros2/README.md
iindex 93e1e14..08dd530 100644
---- a/plugins/ros2/README.md
++++ b/plugins/ros2/README.md
@@@ -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 same
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
ddiff --git a/plugins/sdk/openvibe_plugin/__init__.py b/plugins/sdk/openvibe_plugin/__init__.py
iindex aa586ca..a192e9e 100644
---- a/plugins/sdk/openvibe_plugin/__init__.py
++++ b/plugins/sdk/openvibe_plugin/__init__.py
@@@ -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 no
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.
"""
@@@ -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's
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".
-- 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
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.
ddiff --git a/plugins/sdk/pyproject.toml b/plugins/sdk/pyproject.toml
iindex d4acca6..e0f9ab8 100644
---- a/plugins/sdk/pyproject.toml
++++ b/plugins/sdk/pyproject.toml
@@@ -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" }
ddiff --git a/plugins/sdk/tests/test_runtime.py b/plugins/sdk/tests/test_runtime.py
iindex 35961b7..86908a4 100644
---- a/plugins/sdk/tests/test_runtime.py
++++ b/plugins/sdk/tests/test_runtime.py
@@@ -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()