Skip to main content
STUB DOCUMENT: This page is intentionally minimal and will be expanded with deeper technical details in a future update.
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 (stub):
  • 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:
SubcommandDescription
installInstall cyberwave-edge-core and register systemd service. On macOS, also sets up the camera stream bridge and prompts for camera selection.
install --reconfigure-cameraRe-select camera and restart the stream and edge-core without a full reinstall
startStart the edge node
stopStop the edge node
restartRestart the edge node (systemd or process)
statusCheck if the edge node is running
logsShow edge node logs
driverManage edge driver containers (subgroup)
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

STUB: content below pending human curation.
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

STUB: content below pending human curation.
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:
{
  "streams": {
    "rgb-0":      { "stream_config": { "kind": "camera", "source": "/dev/video0", "resolution": "1280x720", "fps": 15, "camera_type": "cv2" } },
    "audio":      { "stream_config": { "kind": "audio",  "sample_rate_hz": 48000, "channels": 2, "codec": "opus" } },
    "lidar-front":{ "stream_config": { "kind": "lidar",  "source": "/point_cloud2", "scan_rate_hz": 10 } }
  }
}
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

STUB: content below pending human curation.
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

Quick Start

Connect real hardware step by step

Edge Drivers

Write and manage hardware drivers