openvibers/openvibe.link Commits

CI: fix Windows concurrent-command timeouts and PyAV numpy skip; align ADR-043 docs with Bot protocol

OP OpenVibers committed 29 Sept 2026 · 45d03ae881 · 6 files, +19 −17

- Fake test plugin no longer fsyncs its log per line (tens of ms each on Windows pushed
  replies to 50 concurrent commands past MinReplyTimeout).
- TestDecodesWithFFmpegLibraries skips unless both av and numpy import; CI installs numpy.
- ADR-043 mirror and docs: pairing field code, relative deadline_ms, plugin events in
  telemetry.events.

Changes

ddiff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
iindex 66d31db..da3f4be 100644
---- a/.github/workflows/ci.yml
++++ b/.github/workflows/ci.yml
@@@ -20,8 +20,8 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.11"
-- # The end-to-end tests run the real Python dry-run plugin; PyAV decodes the test pattern with libavcodec.
-- - run: pip install av
++ # The end-to-end tests run the real Python dry-run plugin; PyAV (+ numpy) decodes the test pattern with libavcodec.
++ - run: pip install av numpy
- run: test -z "$(gofmt -l .)" || (gofmt -l . && exit 1)
- run: go vet ./...
- run: go test -race -count=1 ./...
ddiff --git a/docs/ADR-043-bot-devices-and-control.md b/docs/ADR-043-bot-devices-and-control.md
iindex 2bea063..1c7dd61 100644
---- a/docs/ADR-043-bot-devices-and-control.md
++++ b/docs/ADR-043-bot-devices-and-control.md
@@@ -32,7 +32,7 @@ a Cozmo-based robot).
stream key. Stream keys stop being robot credentials.
2. **Pairing is a one-time code.** The owner adds a robot; Bot shows an 8-character code (Crockford base32, `XXXX-XXXX`,
10-minute lifetime, single use, 5 wrong tries end it) as text, as a QR code and inside the one-line installer command.
-- The agent sends `{pair: code, agent_version, device_kind, drivers, capabilities}` and receives its device id,
++ The agent sends `{code, agent_version, device_kind, drivers, capabilities}` and receives its device id,
credential, publish key and the robot's profile. The owner sees the device appear and confirms it.
3. **Three kinds of device connection, one agent.** **On-board**: the agent runs on the robot's own computer (Raspberry
Pi; ESP32 through a small library speaking the same protocol). **Bridge**: the agent runs on a computer next to the
@@@ -49,9 +49,10 @@ a Cozmo-based robot).
5. **One message set** (JSON, fields `v`, `seq`, `ts` on every frame): server→device `hello`, `config` (limits, heartbeat,
the operator's allowed commands), `command` (`id` as an idempotency key, `kind` drive/actuator/ptz/say/display/halt,
`value`, `deadline_ms`, operator and role), `estop`, `heartbeat_ack`; device→server `status`, `telemetry` (≤ 2 Hz),
-- `ack`/`nack` (with a fault code), `heartbeat`, `estop_state`. Commands are never queued for an offline device and
++ `ack`/`nack` (with a fault code), `heartbeat`, `estop_state`; robot events (cliff, picked up, low battery) travel in
++ `telemetry.events`, not as a separate message type. Commands are never queued for an offline device and
never replayed after a reconnect.
--6. **Safety is not optional.** Every motion command carries an absolute deadline (default 300 ms, at most the robot's
++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
ddiff --git a/docs/HANDOFF.md b/docs/HANDOFF.md
iindex e677651..cdd6a0a 100644
---- a/docs/HANDOFF.md
++++ b/docs/HANDOFF.md
@@@ -52,7 +52,7 @@ mode 0600 and its contents never appear in logs, errors or `status`.
## Safety (ADR-043 decision 6; not optional)
--- Every motion command carries an absolute deadline (default 300 ms, capped by the profile's `max_command_ms`); the
++- Every motion command carries a deadline relative to receipt (`deadline_ms`, default 300 ms, capped by the profile's `max_command_ms`); the
plugin stops at the 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-node stop`, an `estop`.
ddiff --git a/docs/protocol.md b/docs/protocol.md
iindex 225f2cf..105d3e5 100644
---- a/docs/protocol.md
++++ b/docs/protocol.md
@@@ -2,8 +2,9 @@
This is the wire format the Node speaks today, from ADR-043 decisions 1, 2, 4, 5 and 6. OpenVibe.Bot's own
`docs/protocol.md` is the source of truth; every field name below lives in one Go package, `internal/protocol/`, so
--aligning with it is a change to that package only. Points still to be confirmed with the service are marked **(to
--align)**.
++aligning with it is a change to that package only. The ADR-043 text mirrored in this repo is updated to match (pairing
++field `code`, relative `deadline_ms`, plugin events in `telemetry.events`). Points still to be confirmed with the
++service are marked **(to align)**.
## 1. Pairing
@@@ -23,8 +24,7 @@ Content-Type: application/json
`device_kind` is `onboard` (the Node runs on the robot) or `bridge` (it drives the robot over the robot's own link, as
with Cozmo). `capabilities` is keyed by plugin name and holds each plugin's `describe` capabilities
--([plugins.md](plugins.md)). ADR-043 writes the code field as `pair`; the Node sends `code` as the handoff brief says
--**(to align)**.
++([plugins.md](plugins.md)). The code field is `code`, as in Bot's `docs/protocol.md`.
Answer `2xx`:
@@@ -91,8 +91,8 @@ Unknown `type`s are ignored (a newer server may send more). Frames are at most 1
`status` and `estop_state` are sent on every connect and whenever a plugin changes state, faults or the latch changes.
Plugin events (`cliff`, `picked_up`, `low_battery`, `heartbeat_lost`, …) travel in `telemetry.events` and are sent
--within 100 ms; sensor telemetry is sent at most every `telemetry_ms` (default 500 ms, so ≤ 2 Hz) **(to align: a
--separate `event` type?)**.
++within 100 ms; sensor telemetry is sent at most every `telemetry_ms` (default 500 ms, so ≤ 2 Hz). There is no
++separate `event` message type.
### Commands
@@@ -111,8 +111,7 @@ servo in their config to make the hardware match).
`deadline_ms` is **relative to receipt** (milliseconds the command stays valid), not an absolute timestamp: a device's
clock may be minutes off, and a relative deadline cannot be stretched by clock skew. Default 300 ms; capped at
--`max_command_ms` (default 1000). A held control is re-sent by the panel every 150 ms, each with a new `id`
--**(to align: ADR-043 says "absolute deadline")**.
++`max_command_ms` (default 1000). A held control is re-sent by the panel every 150 ms, each with a new `id`.
What the Node does with a command, in order:
ddiff --git a/internal/plugins/plugins_test.go b/internal/plugins/plugins_test.go
iindex 9fa8f2a..9ee0dd6 100644
---- a/internal/plugins/plugins_test.go
++++ b/internal/plugins/plugins_test.go
@@@ -35,7 +35,9 @@ func TestMain(m *testing.M) {
func fakePlugin(logPath string, in io.Reader, out io.Writer) {
f, _ := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
defer f.Close()
-- rec := func(s string) { fmt.Fprintln(f, s); f.Sync() }
++ // No fsync: the tests read the log through the OS, and fsync per line takes tens of ms on Windows, enough to push
++ // replies to 50 concurrent commands past MinReplyTimeout.
++ rec := func(s string) { fmt.Fprintln(f, s) }
w := json.NewEncoder(out)
sc := bufio.NewScanner(in)
mode := os.Getenv("OPENVIBE_FAKE_MODE")
ddiff --git a/internal/video/h264enc_test.go b/internal/video/h264enc_test.go
iindex 4b04bb2..8d7c854 100644
---- a/internal/video/h264enc_test.go
++++ b/internal/video/h264enc_test.go
@@@ -92,8 +92,8 @@ func TestTestPatternRuns(t *testing.T) {
// the pixels: the strongest evidence the bitstream is valid H.264.
func TestDecodesWithFFmpegLibraries(t *testing.T) {
py, err := exec.LookPath("python3")
-- if err != nil || exec.Command(py, "-c", "import av").Run() != nil {
-- t.Skip("python3 with PyAV (pip install av) not available")
++ if err != nil || exec.Command(py, "-c", "import av, numpy").Run() != nil {
++ t.Skip("python3 with PyAV and numpy (pip install av numpy) not available")
}
enc, _ := NewPCMEncoder(320, 240)
var stream []byte