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-getwhen 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 viarustup(with confirmation) when missing — it is needed to compile the USB/IP server used for USB device passthrough.
Host telemetry
Everyedge_health heartbeat carries the host’s dynamic resource pressure alongside the per-stream health it has always carried:
host_memory_percent,host_memory_available_mbcpu_temp_cconsecutive_critical— reconcile cycles where memory or CPU sat above the critical threshold; resets when pressure clearswatchdog_layers— e.g.["systemd"]on a developer laptop or["systemd", "hardware"]on a Raspberry Pi with/dev/watchdog
/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 understreams[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:
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 Go2lidar_bridge.pyandpointcloud_bridge.pyuse 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-V4L2actual_fps, post-handshake codec, …). The callable is invoked on every heartbeat and its values override registered statics on key collision.MicrophoneAudioStreamerandMultimediaStreameruse this path so re-opening a mic at a different sample rate is reflected on the very next heartbeat without re-registration.MultimediaStreamerkeys its audio block under"audio"rather than the legacy"stream"placeholder so a future videoget_stream_confighook 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 usesType=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 fullTimeoutStartSec(900 s) for aREADY=1that never arrives and marks the service failed. - New binary, old unit (
Type=simple): the service runs, butWatchdogSec/OOMScoreAdjust/TimeoutStartSecare not in effect — no watchdog protection.
pip install cyberwave-edge-core then cyberwave edge install) require the manual step.
Related
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