openvibers/openvibe.link Commits
CI: fix Windows concurrent-command timeouts and PyAV numpy skip; align ADR-043 docs with Bot protocol
Changes
@@@ -20,8 +20,8 @@ jobs:- uses: actions/setup-python@v5with: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 ./...@@@ -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 (RaspberryPi; 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 andnever 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 lostheartbeat (1 s interval, stop after 2 missed) and on every disconnect or crash path, without asking the network. Thee-stop is latched on the device and in Bot and only the owner clears it. Speed, turn and range limits and time windows@@@ -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`); theplugin 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 backoffand it starts stopped), `openvibe-node stop`, an `estop`.@@@ -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, aswith 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'sclock 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:@@@ -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")@@@ -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
@@ -20,8 +20,8 @@ jobs:
@@ -20,8 +20,8 @@ jobs:
- uses: actions/setup-python@v5
- uses: actions/setup-python@v5
with:
with:
python-version: "3.11"
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: test -z "$(gofmt -l .)" || (gofmt -l . && exit 1)
- run: go vet ./...
- run: go vet ./...
- run: go test -race -count=1 ./...
- run: go test -race -count=1 ./...
@@ -32,7 +32,7 @@ a Cozmo-based robot).
@@ -32,7 +32,7 @@ a Cozmo-based robot).
stream key. Stream keys stop being robot credentials.
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`,
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.
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.
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
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
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).
@@ -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,
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,
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),
`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.
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
`max_command_ms`); a held control re-sends every 150 ms; the **device** stops the motors at the deadline, on a lost
heartbeat (1 s interval, stop after 2 missed) and on every disconnect or crash path, without asking the network. The
heartbeat (1 s interval, stop after 2 missed) and on every disconnect or crash path, without asking the network. The
e-stop is latched on the device and in Bot and only the owner clears it. Speed, turn and range limits and time windows
e-stop is latched on the device and in Bot and only the owner clears it. Speed, turn and range limits and time windows
@@ -52,7 +52,7 @@ mode 0600 and its contents never appear in logs, errors or `status`.
@@ -52,7 +52,7 @@ mode 0600 and its contents never appear in logs, errors or `status`.
## Safety (ADR-043 decision 6; not optional)
## 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.
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
- 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`.
and it starts stopped), `openvibe-node stop`, an `estop`.
@@ -2,8 +2,9 @@
@@ -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
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
`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
## 1. Pairing
@@ -23,8 +24,7 @@ Content-Type: application/json
@@ -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
`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
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`:
Answer `2xx`:
@@ -91,8 +91,8 @@ Unknown `type`s are ignored (a newer server may send more). Frames are at most 1
@@ -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.
`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
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
### Commands
@@ -111,8 +111,7 @@ servo in their config to make the hardware match).
@@ -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
`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
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:
What the Node does with a command, in order:
@@ -35,7 +35,9 @@ func TestMain(m *testing.M) {
@@ -35,7 +35,9 @@ func TestMain(m *testing.M) {
func fakePlugin(logPath string, in io.Reader, out io.Writer) {
func fakePlugin(logPath string, in io.Reader, out io.Writer) {
f, _ := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
f, _ := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
defer f.Close()
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)
w := json.NewEncoder(out)
sc := bufio.NewScanner(in)
sc := bufio.NewScanner(in)
mode := os.Getenv("OPENVIBE_FAKE_MODE")
mode := os.Getenv("OPENVIBE_FAKE_MODE")
@@ -92,8 +92,8 @@ func TestTestPatternRuns(t *testing.T) {
@@ -92,8 +92,8 @@ func TestTestPatternRuns(t *testing.T) {
// the pixels: the strongest evidence the bitstream is valid H.264.
// the pixels: the strongest evidence the bitstream is valid H.264.
func TestDecodesWithFFmpegLibraries(t *testing.T) {
func TestDecodesWithFFmpegLibraries(t *testing.T) {
py, err := exec.LookPath("python3")
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)
enc, _ := NewPCMEncoder(320, 240)
var stream []byte
var stream []byte