mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 17:45:42 +00:00
922 lines
21 KiB
Markdown
922 lines
21 KiB
Markdown
# 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](#overview)
|
|
2. [Transport Layer](#transport-layer)
|
|
3. [Authentication](#authentication)
|
|
4. [Message Envelope](#message-envelope)
|
|
5. [Lifecycle](#lifecycle)
|
|
6. [Widgets](#widgets)
|
|
7. [Commands](#commands)
|
|
8. [Events](#events)
|
|
9. [Media Channel](#media-channel)
|
|
10. [Device Linking](#device-linking)
|
|
11. [RBAC](#rbac)
|
|
12. [Bridge SDK](#bridge-sdk)
|
|
13. [Error Codes](#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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```json
|
|
{
|
|
"type": "auth",
|
|
"id": "msg-001",
|
|
"payload": {
|
|
"method": "api_key",
|
|
"key": "a1b2c3d4e5f6...",
|
|
"device_id": "CDAP-SENSOR01"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Method 2: User/Password
|
|
|
|
```json
|
|
{
|
|
"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`:
|
|
|
|
```json
|
|
{
|
|
"type": "auth_2fa_required",
|
|
"id": "msg-002",
|
|
"payload": {
|
|
"message": "TOTP code required"
|
|
}
|
|
}
|
|
```
|
|
|
|
Client must then send:
|
|
|
|
```json
|
|
{
|
|
"type": "auth_2fa",
|
|
"id": "msg-003",
|
|
"payload": {
|
|
"code": "123456"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Method 3: Device Token
|
|
|
|
For automated enrollment (one-time tokens generated by admin):
|
|
|
|
```json
|
|
{
|
|
"type": "auth",
|
|
"id": "msg-004",
|
|
"payload": {
|
|
"method": "device_token",
|
|
"token": "enroll-abc123def456",
|
|
"device_id": "CDAP-NEW01"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Auth Response
|
|
|
|
Success:
|
|
|
|
```json
|
|
{
|
|
"type": "auth_ok",
|
|
"id": "msg-001",
|
|
"payload": {
|
|
"session_id": "sess-xyz789",
|
|
"expires_in": 86400,
|
|
"role": "operator",
|
|
"permissions": ["read", "control", "media"]
|
|
}
|
|
}
|
|
```
|
|
|
|
Failure:
|
|
|
|
```json
|
|
{
|
|
"type": "auth_error",
|
|
"id": "msg-001",
|
|
"payload": {
|
|
"code": "AUTH_INVALID_KEY",
|
|
"message": "Invalid API key"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Message Envelope
|
|
|
|
All CDAP messages use a JSON envelope:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
{
|
|
"id": "firmware_version",
|
|
"type": "text",
|
|
"label": "Firmware"
|
|
}
|
|
```
|
|
|
|
State: `{ "value": "v3.5.2-stable" }`
|
|
|
|
### Slider
|
|
|
|
Numeric range input with step.
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
{
|
|
"id": "temp_history",
|
|
"type": "chart",
|
|
"label": "Temperature History",
|
|
"chartType": "line",
|
|
"maxPoints": 60,
|
|
"unit": "°C"
|
|
}
|
|
```
|
|
|
|
State:
|
|
```json
|
|
{
|
|
"points": [
|
|
{ "t": 1711834500, "v": 42.1 },
|
|
{ "t": 1711834560, "v": 42.3 },
|
|
{ "t": 1711834620, "v": 42.0 }
|
|
]
|
|
}
|
|
```
|
|
|
|
### Table (v3.0)
|
|
|
|
Dynamic sortable data table.
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```json
|
|
{
|
|
"type": "command",
|
|
"id": "cmd-001",
|
|
"payload": {
|
|
"action": "set",
|
|
"widget_id": "motor_speed",
|
|
"value": 2000,
|
|
"operator": "admin@example.com",
|
|
"timestamp": 1711834567890
|
|
}
|
|
}
|
|
```
|
|
|
|
### Client → Server Response
|
|
|
|
```json
|
|
{
|
|
"type": "command_response",
|
|
"id": "cmd-001",
|
|
"payload": {
|
|
"status": "ok",
|
|
"message": "Motor speed set to 2000 RPM",
|
|
"applied_value": 2000
|
|
}
|
|
}
|
|
```
|
|
|
|
### Command Error
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"type": "media_offer",
|
|
"id": "media-001",
|
|
"payload": {
|
|
"target_device": "CDAP-AGENT01",
|
|
"codecs": ["h264", "vp9"],
|
|
"resolution": { "width": 1920, "height": 1080 },
|
|
"fps": 30
|
|
}
|
|
}
|
|
```
|
|
|
|
2. Target device responds with `media_answer`:
|
|
|
|
```json
|
|
{
|
|
"type": "media_answer",
|
|
"id": "media-001",
|
|
"payload": {
|
|
"accepted": true,
|
|
"codec": "h264",
|
|
"resolution": { "width": 1920, "height": 1080 },
|
|
"fps": 30,
|
|
"encryption": "nacl"
|
|
}
|
|
}
|
|
```
|
|
|
|
3. 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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"id": "factory_reset",
|
|
"type": "button",
|
|
"label": "Factory Reset",
|
|
"dangerous": true,
|
|
"confirm": true,
|
|
"permissions": {
|
|
"execute": "admin"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Bridge SDK
|
|
|
|
### Python SDK
|
|
|
|
```bash
|
|
pip install betterdesk-cdap
|
|
```
|
|
|
|
```python
|
|
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*
|