Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

WebSocket Server

The cgd1-rs-ws crate provides a WebSocket and REST server built on Axum. It exposes the full CGD1 device API over JSON for network integration (e.g., Home Assistant, web dashboards).

Installation

cargo install --path cgd1-rs-ws

Starting the Server

cgd1-ws --address 0.0.0.0 --port 3000
FlagDefaultDescription
--address0.0.0.0Bind address
--port3000Listen port
-v, --verbose0Verbosity level (0–3)

Architecture

flowchart TB
    subgraph Server["cgd1-rs-ws"]
        State["ServerState<br/>(transport, manager, token_store)"]
        Listener["WebSocket Listener<br/>(axum)"]
        Session["WsSession<br/>(per-connection)"]
        Dispatch["dispatch_command<br/>+ subscribe_events"]
        Commands["command/<br/>(free functions)"]
        RestRoutes["REST Routes<br/>(GET /devices, etc.)"]
    end

    Client["WebSocket Client"] --> Listener
    Listener --> Session
    Session --> Dispatch
    Dispatch --> Commands
    Commands --> State
    RestRoutes --> State

ServerState

ServerState owns the BLE transport, device manager, and token store. It is Clone (via Arc) and shared across all connections:

#![allow(unused)]
fn main() {
pub struct ServerState {
    transport: Arc<BtleplugTransport>,
    manager: Arc<ClockManager>,
    token_store: Arc<FileTokenStore>,
}
}

WsSession

Each WebSocket connection creates a WsSession that processes incoming JSON requests and sends JSON responses. Per-message tasks are spawned with tokio::spawn for concurrent command handling.

WebSocket Protocol

Request Format

{
  "id": 1,
  "command": {
    "type": "scan",
    "duration_secs": 10
  }
}

The id field is used to match responses to requests. The command field uses a tagged enum with snake_case serialization.

Response Format

{
  "id": 1,
  "result": { ... },
  "error": null
}

Error responses:

{
  "id": 1,
  "result": null,
  "error": "device AA:BB:CC:DD:EE:FF is not connected"
}

Commands

CommandParametersDescription
scanduration_secsScan for devices
connectaddressConnect and authenticate
disconnectaddressDisconnect from device
sync_timeaddressSynchronize device clock
read_alarmsaddressRead all alarm slots
set_alarmaddress, slot, time, repeat_mask, enabled, snoozeSet an alarm
delete_alarmaddress, slotDelete an alarm
read_settingsaddressRead device settings
write_settingsaddress, settingsWrite device settings
set_brightnessaddress, valueSet immediate brightness
preview_ringtoneaddress, volume (optional)Preview ringtone
read_firmwareaddressRead firmware version
read_batteryaddressRead battery level
subscribe_eventsaddressSubscribe to push events

All address fields use the MacAddress newtype (colon-separated hex). Duration uses ScanDuration (1–600 seconds). Slot indices use AlarmSlotIndex (0–15). Brightness uses Brightness (0–150, multiple of 10).

Event Subscription

After sending subscribe_events, the server pushes WsEvent messages to the client:

{
  "event": "sensor_update",
  "data": {
    "temperature": 23.4,
    "humidity": 45.6
  }
}

Event types:

EventPayloadDescription
sensor_updatetemperature, humidityReal-time sensor data
battery_levellevelBattery level change
disconnected(empty)Device disconnected
reconnected(empty)Device reconnected
ackcommand, statusCommand ACK from device
advertisement(full advertisement data)Passive advertisement received

REST API

Read-only endpoints are available via REST:

MethodPathDescription
GET/healthServer health check
GET/api/devicesList connected devices
GET/api/devices/{address}/sensorsLatest sensor data
GET/api/devices/{address}/batteryBattery level
GET/api/devices/{address}/firmwareFirmware version
GET/api/devices/{address}/alarmsAll alarms
GET/api/devices/{address}/settingsDevice settings

Path parameters use {address} syntax (Axum 0.8+). The address is a MAC address in colon-separated hex format (e.g., AA:BB:CC:DD:EE:FF).

Example: REST Request

curl http://localhost:3000/api/devices/AA:BB:CC:DD:EE:FF/sensors

Response:

{
  "temperature": 23.4,
  "humidity": 45.6
}

Example: WebSocket Session

const ws = new WebSocket("ws://localhost:3000/ws");

ws.onopen = () => {
  // Connect to a device
  ws.send(JSON.stringify({
    id: 1,
    command: { type: "connect", address: "AA:BB:CC:DD:EE:FF" }
  }));

  // Subscribe to events
  ws.send(JSON.stringify({
    id: 2,
    command: { type: "subscribe_events", address: "AA:BB:CC:DD:EE:FF" }
  }));
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.result !== null) {
    console.log("Response:", msg);
  } else if (msg.event) {
    console.log("Event:", msg);
  }
};

Error Handling

ServerError converts to HTTP status codes for REST endpoints:

ErrorStatus Code
NotConnected404
Json parse error400
Core (BLE/library errors)500

WebSocket errors are returned in the error field of the JSON response.