Files
2026-03-24 00:26:25 +01:00

21 KiB

CDAP — Custom Device API Protocol

Protocol Version: 1.0
Server Version: BetterDesk v3.0.0
Status: Production-ready (Phases 0-7 implemented)


Table of Contents

  1. Overview
  2. Transport Layer
  3. Authentication
  4. Message Envelope
  5. Lifecycle
  6. Widgets
  7. Commands
  8. Events
  9. Media Channel
  10. Device Linking
  11. RBAC
  12. Bridge SDK
  13. Error Codes

Overview

CDAP (Custom Device API Protocol) is a WebSocket-based protocol that connects non-RustDesk devices to the BetterDesk ecosystem. It provides:

  • Real-time state synchronization — Devices push state updates; panel renders widgets
  • Bidirectional commands — Operators send commands to devices; devices respond with results
  • Media relay — Binary frame channel for remote desktop sessions (E2E encrypted)
  • Device management — Enrollment, revocation, linking, grouping
  • Bridge ecosystem — SDKs for Modbus TCP, SNMP, REST webhooks, and custom protocols

When to Use CDAP

Scenario Use CDAP? Alternative
IoT sensor dashboard Yes
SCADA/PLC monitoring Yes
Network device management Yes
Remote desktop (existing) No RustDesk client
Remote desktop (CDAP agent) Yes BetterDesk native agent
Custom automation agent Yes

Transport Layer

WebSocket Connection

ws://host:21122    (plain)
wss://host:21122   (TLS)

The CDAP gateway uses dual-mode listening — auto-detects TLS (first byte 0x16) and plain connections on the same port. No separate TLS port needed.

TLS Configuration

# Enable TLS on CDAP gateway
betterdesk-server --tls-cert /path/to/cert.pem --tls-key /path/to/key.pem

# CDAP auto-detects TLS when cert/key are provided
# Both plain and TLS connections accepted on port 21122

Connection Parameters

Parameter Default Description
CDAP_PORT 21122 Gateway listen port
CDAP_MAX_CONNS 10000 Maximum concurrent connections
CDAP_READ_LIMIT 1048576 Maximum message size (1 MiB)
CDAP_PING_INTERVAL 30s WebSocket ping interval
CDAP_PONG_TIMEOUT 10s Pong response deadline
CDAP_WRITE_TIMEOUT 10s Write deadline per message

Reconnection

Clients should implement exponential backoff reconnection:

Attempt 1: wait 1s
Attempt 2: wait 2s
Attempt 3: wait 4s
Attempt N: wait min(2^(N-1), 60)s

On reconnect, clients must re-authenticate and re-register. The server preserves device state for 5 minutes after disconnection.


Authentication

Authentication must be the first message after WebSocket upgrade. The server closes the connection after 10 seconds without auth.

Method 1: API Key

{
    "type": "auth",
    "id": "msg-001",
    "payload": {
        "method": "api_key",
        "key": "a1b2c3d4e5f6...",
        "device_id": "CDAP-SENSOR01"
    }
}

Method 2: User/Password

{
    "type": "auth",
    "id": "msg-002",
    "payload": {
        "method": "user_password",
        "username": "operator1",
        "password": "secure-password",
        "device_id": "CDAP-AGENT01"
    }
}

If the user has TOTP 2FA enabled, the server responds with auth_2fa_required:

{
    "type": "auth_2fa_required",
    "id": "msg-002",
    "payload": {
        "message": "TOTP code required"
    }
}

Client must then send:

{
    "type": "auth_2fa",
    "id": "msg-003",
    "payload": {
        "code": "123456"
    }
}

Method 3: Device Token

For automated enrollment (one-time tokens generated by admin):

{
    "type": "auth",
    "id": "msg-004",
    "payload": {
        "method": "device_token",
        "token": "enroll-abc123def456",
        "device_id": "CDAP-NEW01"
    }
}

Auth Response

Success:

{
    "type": "auth_ok",
    "id": "msg-001",
    "payload": {
        "session_id": "sess-xyz789",
        "expires_in": 86400,
        "role": "operator",
        "permissions": ["read", "control", "media"]
    }
}

Failure:

{
    "type": "auth_error",
    "id": "msg-001",
    "payload": {
        "code": "AUTH_INVALID_KEY",
        "message": "Invalid API key"
    }
}

Message Envelope

All CDAP messages use a JSON envelope:

{
    "type": "message_type",
    "id": "unique-message-id",
    "payload": { ... },
    "ts": 1711834567890
}
Field Type Required Description
type string Message type identifier
id string Unique message ID (for request/response correlation)
payload object Message-specific data
ts number Unix timestamp in milliseconds

Message Types (Client → Server)

Type Description
auth Authentication request
auth_2fa TOTP 2FA code
register Device registration with manifest
state_update Widget state push
command_response Response to server command
heartbeat Keep-alive with optional metrics
media_offer SDP offer for media channel
media_answer SDP answer for media channel
media_frame Binary frame (uses binary WebSocket frames)
event Client-originated event

Message Types (Server → Client)

Type Description
auth_ok Authentication success
auth_error Authentication failure
auth_2fa_required TOTP required
registered Registration acknowledged
command Command from operator
state_request Server requests full state refresh
media_offer SDP offer from peer
media_answer SDP answer from peer
config_update Server configuration change
error Generic error
ping Application-level ping

Lifecycle

Connection Flow

Client                              Server
  │                                    │
  │──── WebSocket Upgrade ────────────>│
  │<─── 101 Switching Protocols ───────│
  │                                    │
  │──── auth {method, credentials} ───>│
  │<─── auth_ok {session, role} ───────│
  │                                    │
  │──── register {manifest} ──────────>│
  │<─── registered {ack} ─────────────│
  │                                    │
  │──── state_update {widgets} ───────>│  (periodic)
  │<─── command {action, params} ──────│  (on-demand)
  │──── command_response {result} ────>│
  │                                    │
  │──── heartbeat {metrics} ──────────>│  (every 30s)
  │<─── pong ─────────────────────────│
  │                                    │
  │──── close ────────────────────────>│
  │<─── close ─────────────────────────│

Registration (Manifest)

The manifest describes the device's capabilities:

{
    "type": "register",
    "id": "msg-010",
    "payload": {
        "manifest": {
            "name": "Factory Floor Controller",
            "device_type": "scada",
            "version": "2.1.0",
            "firmware": "PLC-FW-3.5.2",
            "capabilities": ["widgets", "commands", "media"],
            "categories": [
                {
                    "id": "temperature",
                    "label": "Temperature Sensors",
                    "icon": "thermostat"
                },
                {
                    "id": "actuators",
                    "label": "Actuators",
                    "icon": "settings"
                }
            ],
            "widgets": [
                {
                    "id": "temp_zone1",
                    "type": "gauge",
                    "label": "Zone 1 Temperature",
                    "category": "temperature",
                    "unit": "°C",
                    "min": -10,
                    "max": 100,
                    "thresholds": {
                        "warning": 60,
                        "danger": 85
                    },
                    "permissions": {
                        "read": "viewer"
                    }
                },
                {
                    "id": "motor_speed",
                    "type": "slider",
                    "label": "Motor Speed",
                    "category": "actuators",
                    "unit": "RPM",
                    "min": 0,
                    "max": 3000,
                    "step": 50,
                    "permissions": {
                        "read": "viewer",
                        "control": "operator"
                    }
                },
                {
                    "id": "emergency_stop",
                    "type": "button",
                    "label": "Emergency Stop",
                    "category": "actuators",
                    "confirm": true,
                    "confirmMessage": "Are you sure you want to trigger emergency stop?",
                    "dangerous": true,
                    "permissions": {
                        "read": "viewer",
                        "execute": "admin"
                    }
                }
            ],
            "commands": [
                {
                    "id": "reboot",
                    "label": "Reboot Device",
                    "confirm": true,
                    "permissions": {
                        "execute": "admin"
                    }
                },
                {
                    "id": "calibrate",
                    "label": "Calibrate Sensors",
                    "params": [
                        {
                            "id": "zone",
                            "type": "select",
                            "label": "Zone",
                            "options": ["zone1", "zone2", "zone3"]
                        }
                    ],
                    "permissions": {
                        "execute": "operator"
                    }
                }
            ]
        }
    }
}

State Updates

Devices push widget state periodically or on change:

{
    "type": "state_update",
    "id": "msg-020",
    "payload": {
        "widgets": {
            "temp_zone1": {
                "value": 42.5,
                "status": "normal"
            },
            "motor_speed": {
                "value": 1500
            },
            "emergency_stop": {
                "active": false
            }
        }
    }
}

Heartbeat

Sent every 30s (configurable) with optional system metrics:

{
    "type": "heartbeat",
    "id": "msg-030",
    "payload": {
        "uptime": 86400,
        "cpu": 23.5,
        "memory": 67.2,
        "disk": 45.0,
        "custom_metrics": {
            "queue_depth": 42,
            "error_rate": 0.01
        }
    }
}

Widgets

Toggle

Boolean on/off switch.

{
    "id": "relay1",
    "type": "toggle",
    "label": "Main Relay",
    "category": "actuators"
}

State: { "value": true }

Command: { "action": "set", "widget_id": "relay1", "value": false }

Gauge

Numeric value with thresholds and optional unit.

{
    "id": "pressure",
    "type": "gauge",
    "label": "Pressure",
    "unit": "bar",
    "min": 0,
    "max": 10,
    "decimals": 2,
    "thresholds": {
        "warning": 7.5,
        "danger": 9.0
    }
}

State: { "value": 5.23 }

Button

Action trigger with optional confirmation dialog.

{
    "id": "reset_counters",
    "type": "button",
    "label": "Reset Counters",
    "icon": "restart_alt",
    "confirm": true,
    "confirmMessage": "Reset all production counters to zero?",
    "cooldown": 5000
}

State: { "active": false, "lastTriggered": "2026-03-20T10:30:00Z" }

Command: { "action": "execute", "widget_id": "reset_counters" }

LED

Status indicator with color states.

{
    "id": "connection_status",
    "type": "led",
    "label": "PLC Connection",
    "states": {
        "green": "Connected",
        "yellow": "Reconnecting",
        "red": "Disconnected"
    }
}

State: { "color": "green", "label": "Connected" }

Text

Read-only text display.

{
    "id": "firmware_version",
    "type": "text",
    "label": "Firmware"
}

State: { "value": "v3.5.2-stable" }

Slider

Numeric range input with step.

{
    "id": "brightness",
    "type": "slider",
    "label": "LED Brightness",
    "unit": "%",
    "min": 0,
    "max": 100,
    "step": 5
}

State: { "value": 75 }

Command: { "action": "set", "widget_id": "brightness", "value": 50 }

Select

Dropdown selection.

{
    "id": "operating_mode",
    "type": "select",
    "label": "Operating Mode",
    "options": [
        { "value": "auto", "label": "Automatic" },
        { "value": "manual", "label": "Manual" },
        { "value": "maintenance", "label": "Maintenance" }
    ]
}

State: { "value": "auto" }

Command: { "action": "set", "widget_id": "operating_mode", "value": "manual" }

Chart

Time-series or categorical chart data.

{
    "id": "temp_history",
    "type": "chart",
    "label": "Temperature History",
    "chartType": "line",
    "maxPoints": 60,
    "unit": "°C"
}

State:

{
    "points": [
        { "t": 1711834500, "v": 42.1 },
        { "t": 1711834560, "v": 42.3 },
        { "t": 1711834620, "v": 42.0 }
    ]
}

Table (v3.0)

Dynamic sortable data table.

{
    "id": "alarm_log",
    "type": "table",
    "label": "Active Alarms",
    "columns": [
        { "id": "time", "label": "Time", "type": "datetime" },
        { "id": "severity", "label": "Severity", "type": "badge" },
        { "id": "message", "label": "Message", "type": "text" },
        { "id": "ack", "label": "Acknowledge", "type": "action" }
    ],
    "sortable": true,
    "pagination": true,
    "pageSize": 20
}

State:

{
    "rows": [
        {
            "id": "alarm-001",
            "time": "2026-03-20T10:30:00Z",
            "severity": { "value": "critical", "color": "red" },
            "message": "Zone 3 temperature exceeded 85°C",
            "ack": { "label": "Acknowledge", "action": "ack_alarm", "params": { "id": "alarm-001" } }
        }
    ],
    "total": 42
}

Terminal (v3.0)

WebSocket shell relay for device management.

{
    "id": "shell",
    "type": "terminal",
    "label": "Device Shell",
    "permissions": {
        "read": "operator",
        "control": "admin"
    }
}

Terminal widget uses a separate binary WebSocket channel for stdin/stdout/stderr.


Commands

Server → Client Command

{
    "type": "command",
    "id": "cmd-001",
    "payload": {
        "action": "set",
        "widget_id": "motor_speed",
        "value": 2000,
        "operator": "admin@example.com",
        "timestamp": 1711834567890
    }
}

Client → Server Response

{
    "type": "command_response",
    "id": "cmd-001",
    "payload": {
        "status": "ok",
        "message": "Motor speed set to 2000 RPM",
        "applied_value": 2000
    }
}

Command Error

{
    "type": "command_response",
    "id": "cmd-001",
    "payload": {
        "status": "error",
        "code": "DEVICE_BUSY",
        "message": "Motor is in calibration mode, cannot change speed"
    }
}

Events

Devices can emit events for audit logging and alerting:

{
    "type": "event",
    "id": "evt-001",
    "payload": {
        "event_type": "alarm",
        "severity": "critical",
        "message": "Zone 3 temperature exceeded 85°C",
        "data": {
            "zone": 3,
            "temperature": 87.2,
            "threshold": 85
        }
    }
}

Event types:

  • alarm — Threshold violation or abnormal condition
  • status_change — Device state transition
  • maintenance — Scheduled or manual maintenance event
  • security — Security-related event (auth failure, tamper detection)
  • custom — Application-specific event

Media Channel

The media channel provides binary frame relay between CDAP devices and web clients for remote desktop sessions.

Negotiation

  1. Web client sends media_offer through CDAP gateway:
{
    "type": "media_offer",
    "id": "media-001",
    "payload": {
        "target_device": "CDAP-AGENT01",
        "codecs": ["h264", "vp9"],
        "resolution": { "width": 1920, "height": 1080 },
        "fps": 30
    }
}
  1. Target device responds with media_answer:
{
    "type": "media_answer",
    "id": "media-001",
    "payload": {
        "accepted": true,
        "codec": "h264",
        "resolution": { "width": 1920, "height": 1080 },
        "fps": 30,
        "encryption": "nacl"
    }
}
  1. Binary frames flow through the gateway:
    • Device sends video frames as binary WebSocket messages
    • Client sends input events (mouse, keyboard) as JSON commands
    • Gateway relays without decryption (E2E between client and device)

Frame Encryption

Media frames use the same NaCl encryption as RustDesk:

  • Key exchange: X25519 Diffie-Hellman
  • Encryption: XSalsa20-Poly1305
  • Frame format: [24-byte nonce][encrypted payload]

Device Linking

CDAP devices can be linked to RustDesk peers for unified management:

{
    "type": "register",
    "id": "msg-050",
    "payload": {
        "manifest": {
            "name": "Server Room Agent",
            "device_type": "os_agent",
            "linked_peer_id": "1340238749",
            "version": "1.0.0",
            "widgets": [ ... ]
        }
    }
}

Linked devices appear in the same device detail page. The panel shows:

  • RustDesk tab: Remote desktop, file transfer
  • CDAP tab: Widgets, commands, metrics
  • Combined connection status

RBAC

Per-Widget Permissions

Each widget can specify minimum role requirements:

{
    "permissions": {
        "read": "viewer",
        "control": "operator",
        "execute": "admin"
    }
}

Permission types:

  • read — View widget state (default: viewer)
  • control — Change widget value (sliders, toggles, selects)
  • execute — Trigger actions (buttons, commands)

Dangerous Widgets

Widgets marked "dangerous": true are hidden from non-admin users and require explicit confirmation:

{
    "id": "factory_reset",
    "type": "button",
    "label": "Factory Reset",
    "dangerous": true,
    "confirm": true,
    "permissions": {
        "execute": "admin"
    }
}

Bridge SDK

Python SDK

pip install betterdesk-cdap
from betterdesk_cdap import CDAPBridge, Widget, WidgetType

bridge = CDAPBridge(
    server="ws://betterdesk.example.com:21122",
    api_key="your-api-key",
    device_id="BRIDGE-MODBUS01"
)

bridge.register(
    name="Modbus Gateway",
    device_type="scada",
    widgets=[
        Widget("coil_0", WidgetType.TOGGLE, "Output Coil 0"),
        Widget("register_0", WidgetType.GAUGE, "Holding Register 0",
               unit="mA", min=0, max=20, thresholds={"warning": 16, "danger": 19}),
    ]
)

@bridge.on_command("set", "coil_0")
async def handle_coil(value: bool):
    # Write to Modbus device
    await modbus_client.write_coil(0, value)
    return {"status": "ok"}

bridge.run()  # Starts event loop

Reference Bridges

Bridge Protocol Status
betterdesk-bridge-modbus Modbus TCP/RTU v3.0
betterdesk-bridge-snmp SNMP v2c/v3 v3.0
betterdesk-bridge-rest REST webhook v3.0
betterdesk-bridge-mqtt MQTT 3.1.1/5.0 Planned
betterdesk-bridge-opcua OPC UA Planned

Error Codes

Code HTTP Equiv Description
AUTH_INVALID_KEY 401 Invalid API key
AUTH_INVALID_CREDENTIALS 401 Wrong username/password
AUTH_2FA_REQUIRED 401 TOTP code needed
AUTH_2FA_INVALID 401 Wrong TOTP code
AUTH_TOKEN_EXPIRED 401 Device token expired
AUTH_TOKEN_REVOKED 401 Device token revoked
AUTH_RATE_LIMITED 429 Too many auth attempts
DEVICE_ALREADY_REGISTERED 409 Device ID already connected
DEVICE_BANNED 403 Device is banned
DEVICE_REVOKED 403 Device has been revoked
DEVICE_SOFT_DELETED 403 Device has been soft-deleted
PERMISSION_DENIED 403 Insufficient role for action
INVALID_MANIFEST 400 Manifest validation failed
INVALID_COMMAND 400 Unknown command or missing params
DEVICE_NOT_FOUND 404 Target device not connected
DEVICE_BUSY 503 Device cannot process command
MEDIA_REJECTED 403 Media channel rejected by device
MEDIA_CODEC_UNSUPPORTED 406 No common codec found
INTERNAL_ERROR 500 Server internal error
GATEWAY_DISABLED 503 CDAP gateway not enabled

Last updated: March 2026 — BetterDesk v3.0.0