Skip to main content
Cyberwave Edge Core acts as the orchestrator of Cyberwave edge drivers. Public repository: cyberwave-os/cyberwave-edge-core

Use Cyberwave Edge with the CLI

SSH to the edge device where you want to install Edge Core, then install the Cyberwave CLI and run the installer:
cyberwave pair is the recommended top-level alias for cyberwave edge install. Both run the same first-time setup flow.
Prerequisites by platform:
  • Linux: Docker is auto-installed via apt-get when missing (Ubuntu / Debian / Raspbian).
  • macOS: Docker Desktop must be installed and running before cyberwave edge install (the installer aborts early with a hint if not). The Rust toolchain is auto-installed via rustup (with confirmation) when missing — it is needed to compile the USB/IP server used for USB device passthrough.
Manage the edge node service lifecycle, configuration, and monitoring:
Want to check how fast an edge device actually is? Run cyberwave edge bench to micro-benchmark the Zenoh SDK hot paths and compare against a per-device baseline.

Host telemetry

Every edge_health heartbeat carries the host’s dynamic resource pressure alongside the per-stream health it has always carried:
  • host_memory_percent, host_memory_available_mb
  • cpu_temp_c
  • consecutive_critical — reconcile cycles where memory or CPU sat above the critical threshold; resets when pressure clears
  • watchdog_layers — e.g. ["systemd"] on a developer laptop or ["systemd", "hardware"] on a Raspberry Pi with /dev/watchdog
Driver containers see their container’s /proc, not the host’s, and omit these fields. Only the bootstrap publisher running on the edge host populates them. Static hardware identity — total RAM, CPU model, kernel, thermal source, hardware-watchdog availability — is uploaded via POST /api/v1/edges/discover and stored on Edge.metadata.host_facts. The first POST runs synchronously at startup; a background daemon then re-POSTs every ~30 s so the backend can tell “edge daemon alive” apart from “edge daemon dead” even when no twin MQTT traffic is flowing. The Edge Devices tab renders both signals: a colour-coded pressure pill from the live heartbeat plus a “what hardware is this” row from the REST identity. The REST keepalive powers a three-state liveness pill — Online (twin MQTT activity), Idle (edge alive, no twin work; liveness_state="standby" on the wire), Offline (neither signal within window). Idle is what shows up on a freshly-paired edge that hasn’t had any twins bound to it yet: previously such an edge would render as “Offline” even though edge-core was running fine. The Edge Devices tab sorts rows by liveness — online → idle → offline → unresolved binding — so working edges land at the top and broken bindings sink to the bottom. Static facts work on both Linux and macOS: Linux reads /proc/{meminfo,cpuinfo} and /sys/class/thermal; Darwin shells out to sysctl for RAM, CPU model and (logical) core count. Dynamic host pressure (memory %, CPU temp) remains Linux-only for now — the dashboard hides the pressure pill on macOS edges and shows only the static identity row, and thermal_source is omitted on macOS so it never claims a sensor that isn’t being sampled.

Per-stream configuration

Each entry under streams[id] in an edge_health heartbeat carries an optional typed stream_config block describing the runtime-negotiated config for that stream. The block is a discriminated union keyed by kind:
Drivers attach a block one of two ways:
  • EdgeHealthCheck.register_stream_config(stream_id, config) for static configs known at startup; the publisher keeps the snapshot and merges it into every heartbeat. The Go2 lidar_bridge.py and pointcloud_bridge.py use this path because the scan rate is wired from a ROS parameter at node bootstrap.
  • EdgeHealthCheck(..., stream_config_provider=callable) for dynamic configs where some fields are only known after streamer startup (post-V4L2 actual_fps, post-handshake codec, …). The callable is invoked on every heartbeat and its values override registered statics on key collision. MicrophoneAudioStreamer and MultimediaStreamer use this path so re-opening a mic at a different sample rate is reflected on the very next heartbeat without re-registration. MultimediaStreamer keys its audio block under "audio" rather than the legacy "stream" placeholder so a future video get_stream_config hook can slot in next to it as "video" without a wire-breaking rename.
register_stream_config enforces a per-kind required-field contract: kind: "camera" must carry source, resolution, fps; kind: "audio" must carry sample_rate_hz and channels (and may optionally carry codec and source); kind: "lidar" requires source and scan_rate_hz (points_per_second is optional and intentionally omitted by both Go2 bridges because the only available value is the post-downsample cap, not measured throughput); kind: "imu" requires source and rate_hz. Unknown kinds pass through additively. URL-shaped source values must have user:pass@ stripped before they reach the wire — the SDK’s CV2CameraStreamer does this automatically for RTSP and HTTP sources. kind: "audio" deliberately treats source as optional: across the rest of the SDK source is a device path / URL / ROS topic, and a WebRTC microphone has no equivalent (publishing the host ALSA / CoreAudio device path would leak operator-local filesystem state). Liveness — frame_count vs mark_alive. Camera and lidar publishers call EdgeHealthCheck.update_frame_count() per emitted frame so the wire-side fps / frames_sent numbers carry meaning to operators (frames per second is the standard quality signal for both). Audio publishers — and future IMU / encoder / GPS publishers — call EdgeHealthCheck.mark_alive() instead, which advances the staleness clock without bumping the counter. Reporting fps: 50.0 for a microphone emitting 20 ms Opus packets would be terminology-correct but operationally noise; the dashboard already hides those fields for audio rows, but raw MQTT subscribers and the analytics table see the cleaner shape too. mark_alive still flips ice_connection_state to connected on first call so audio rows render connected from the first heartbeat onward. A top-level camera_config slot remains populated for one deprecation release for out-of-tree consumers that still read the legacy single-camera shape. New consumers must read streams[id].stream_config instead — the legacy slot only ever covers one camera and is silent for audio / lidar / IMU streams. When more than one camera-kind stream_config is registered, the legacy slot deterministically reflects the lexicographically-smallest stream_id so multi-camera devices don’t flap identities heartbeat-to-heartbeat. An idle publisher that has registered no stream_config and has never seen a frame emits an empty streams: {} map, stream_count: 0, and camera_config: null. This is the wire shape the edge-core bootstrap publisher uses in the gap before a driver container starts — the dashboard’s Edge details renders nothing for streams in that case, rather than a phantom "stream" row that flickers away the moment a real driver takes over. Once a driver starts publishing (either by registering a stream_config or by ticking update_frame_count()), the entry appears on the next heartbeat. The dashboard’s per-row renderer falls back to the asset’s declared parameters.{width, height, update_rate} for camera streams when neither the wire stream_config nor the legacy camera_config carries that data, so paired cameras still show their target resolution and fps before the live stream catches up.

Upgrading

Edge Core ships the orchestrator binary and a systemd unit file. From the high-availability watchdog release onward the unit uses Type=notify and the binary issues a READY=1 notification at startup — the two must match. When upgrading on an existing device, re-run sudo cyberwave edge install after the package upgrade to regenerate the systemd unit. Skipping this step results in one of two states:
  • Old binary, new unit (Type=notify): systemd waits the full TimeoutStartSec (900 s) for a READY=1 that never arrives and marks the service failed.
  • New binary, old unit (Type=simple): the service runs, but WatchdogSec/OOMScoreAdjust/TimeoutStartSec are not in effect — no watchdog protection.
The Debian package post-install script regenerates the unit automatically, but manually installed binaries (e.g. pip install cyberwave-edge-core then cyberwave edge install) require the manual step.

Live Teleoperation

Control robots in real time through Edge Core

How hardware works

From a twin to your real hardware

Edge Drivers

Write and manage hardware drivers