> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cyberwave.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Overview

> Cyberwave Edge Core acts as the orchestrator of Cyberwave edge drivers.

export const EdgeSetup = ({exclude = []}) => {
  const showMac = !exclude.includes("mac");
  const showLinux = !exclude.includes("linux");
  const macCode = `curl -fsSL https://cyberwave.com/install.sh | bash\ncyberwave pair`;
  const linuxCode = `curl -fsSL https://cyberwave.com/install.sh | bash\nsudo cyberwave pair`;
  const linuxTip = <Tip>
      First time on a Raspberry Pi? See{" "}
      <a href="/feature-reference/edge/raspberry-pi">Raspberry Pi Setup</a>.
      <br />
      First time on a Jetson Orin Nano? See{" "}
      <a href="/feature-reference/edge/jetson-orin-nano">
        Jetson Orin Nano Setup
      </a>
      .
    </Tip>;
  if (showMac && showLinux) {
    return <Tabs>
        <Tab title="Mac">
          <CodeBlock language="bash">{macCode}</CodeBlock>
        </Tab>
        <Tab title="Linux">
          {linuxTip}
          <CodeBlock language="bash">{linuxCode}</CodeBlock>
        </Tab>
      </Tabs>;
  }
  if (showMac) {
    return <CodeBlock language="bash">{macCode}</CodeBlock>;
  }
  if (showLinux) {
    return <>
        {linuxTip}
        <CodeBlock language="bash">{linuxCode}</CodeBlock>
      </>;
  }
  return null;
};

<Warning>
  **STUB DOCUMENT:** This page is intentionally minimal and will be expanded with deeper technical details in a future update.
</Warning>

Cyberwave Edge Core acts as the orchestrator of Cyberwave edge drivers.

Public repository: [cyberwave-os/cyberwave-edge-core](https://github.com/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:

<EdgeSetup />

<Note>
  `cyberwave pair` is the recommended top-level alias for `cyberwave edge install`. Both run the same first-time setup flow.
</Note>

<Note>
  **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.
</Note>

Manage the edge node service lifecycle, configuration, and monitoring:

| Subcommand                     | Description                                                                                                                                 |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `install`                      | Install cyberwave-edge-core and register systemd service. On macOS, also sets up the camera stream bridge and prompts for camera selection. |
| `install --reconfigure-camera` | Re-select camera and restart the stream and edge-core without a full reinstall                                                              |
| `start`                        | Start the edge node                                                                                                                         |
| `stop`                         | Stop the edge node                                                                                                                          |
| `restart`                      | Restart the edge node (systemd or process)                                                                                                  |
| `status`                       | Check if the edge node is running                                                                                                           |
| `logs`                         | Show edge node logs                                                                                                                         |
| `driver`                       | Manage edge driver containers (subgroup)                                                                                                    |

<Tip>
  Want to check how fast an edge device actually is? Run [`cyberwave edge bench`](/feature-reference/workflows/workers/bench) to micro-benchmark the Zenoh SDK hot paths and compare against a per-device baseline.
</Tip>

***

## Host telemetry

<Warning>
  **STUB:** content below pending human curation.
</Warning>

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

<Warning>
  **STUB:** content below pending human curation.
</Warning>

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`:

```json theme={null}
{
  "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

<Warning>
  **STUB:** content below pending human curation.
</Warning>

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.

***

## Related

<CardGroup cols={3}>
  <Card title="Live Teleoperation" icon="gamepad" href="/use-cyberwave/teleoperation">
    Control robots in real time through Edge Core
  </Card>

  <Card title="Quick Start" icon="bolt" href="/overview/develop-remote-hardware">
    Connect real hardware step by step
  </Card>

  <Card title="Edge Drivers" icon="truck-ramp-box" href="/edge/drivers/overview">
    Write and manage hardware drivers
  </Card>
</CardGroup>
