Files
UNITRONIX dd0889e3d6 Add SessionManager; update docs, i18n & CI
Introduce a SessionManager for relay-based remote sessions in the Tauri MGMT client: new SessionCommand API, start/stop/session input routing, clipboard/recording/quality controls, and notification read/dismiss state. Wire AppState with new mutexes and show main window on startup. CI: add SBOM generation (anchore) and Trivy vulnerability scan steps. Misc: change console Docker DB path, large README/CHANGELOG updates (chat E2E, unattended access/WOL, i18n expansion, CDAP/SDK docs), and many web-nodejs assets/locales/routes/views/services and server-side changes.
2026-04-05 13:42:07 +02:00

5.7 KiB

Building a CDAP Bridge

A CDAP bridge connects non-BetterDesk devices (industrial equipment, IoT sensors, network gear) to the BetterDesk management platform via the CDAP protocol.

What is a Bridge?

A bridge is a lightweight process that:

  1. Communicates with external hardware/software using its native protocol (Modbus, SNMP, REST, MQTT, etc.)
  2. Translates data into CDAP widget values
  3. Exposes CDAP commands that map to hardware actions
┌──────────────┐     Modbus/TCP     ┌──────────┐     CDAP/WS     ┌──────────────┐
│  PLC / RTU   │ ◄═══════════════► │  Bridge  │ ◄═════════════► │  BetterDesk  │
│  (hardware)  │                    │  (Python) │                 │  Server      │
└──────────────┘                    └──────────┘                  └──────────────┘

Reference Bridges

Bridge Protocol Directory Description
Modbus Modbus TCP/RTU bridges/modbus/ Register polling, data type encode/decode, write-back
SNMP SNMPv2c/v3 bridges/snmp/ OID polling, timetick formatting, counter rates
REST/Webhook HTTP bridges/rest-webhook/ REST polling + aiohttp webhook listener

Creating a Bridge (Python)

1. Install the SDK

pip install betterdesk-cdap   # or: pip install -e sdks/python/

2. Define Your Bridge

import asyncio
from betterdesk_cdap import CDAPBridge, Widget

class TemperatureBridge(CDAPBridge):
    """Bridge for a network temperature sensor."""

    def __init__(self, sensor_ip, sensor_port=502):
        super().__init__(
            server_url="ws://betterdesk-server:21122/cdap",
            device_id=f"TEMP-{sensor_ip.replace('.', '-')}",
            device_name=f"Temp Sensor {sensor_ip}",
            device_type="temperature_sensor",
            auth_method="api_key",
            api_key=os.environ["CDAP_API_KEY"]
        )
        self.sensor_ip = sensor_ip
        self.sensor_port = sensor_port

        # Define widgets
        self.add_widget(Widget.gauge("temp", "Temperature", group="Readings",
                                      unit="°C", min_val=-40, max_val=125,
                                      danger=80, warning=60))
        self.add_widget(Widget.text("firmware", "Firmware", group="Info"))
        self.add_widget(Widget.led("alarm", "High Temp Alarm", group="Status"))

    async def on_connected(self):
        # Read firmware version once
        fw = await self._read_firmware()
        await self.update_widget("firmware", fw)

    async def collect_metrics(self):
        """Called every heartbeat interval. Read sensor data."""
        temp = await self._read_temperature()
        alarm = temp > 80
        return {
            "temp": round(temp, 1),
            "alarm": alarm,
        }

    async def on_command(self, command_id, command, args):
        if command == "set_threshold":
            threshold = args.get("value", 80)
            await self._write_threshold(threshold)
            await self.send_command_response(command_id, True, f"Threshold set to {threshold}")
        else:
            await self.send_command_response(command_id, False, f"Unknown command: {command}")

    async def _read_temperature(self):
        # Your hardware-specific code here
        ...

asyncio.run(TemperatureBridge("192.168.1.100").run())

3. Configuration

Use a JSON or YAML config file:

{
    "server_url": "ws://betterdesk:21122/cdap",
    "api_key": "${CDAP_API_KEY}",
    "device_id": "MODBUS-PLC-001",
    "device_name": "Production Line PLC",
    "device_type": "modbus_plc",
    "poll_interval": 5,
    "registers": [
        { "address": 0, "type": "float32", "widget_id": "pressure", "label": "Pressure", "unit": "bar" },
        { "address": 2, "type": "uint16", "widget_id": "rpm", "label": "Motor RPM", "unit": "RPM" }
    ]
}

Bridge Best Practices

  1. Reconnection — The SDK handles WebSocket reconnection with exponential backoff. Don't implement your own retry logic.
  2. Error isolation — Catch hardware communication errors in collect_metrics(). Return last known values on failure.
  3. Poll intervals — Match the bridge's poll interval to the hardware's update rate. Don't poll a 1Hz sensor at 100ms.
  4. Bulk updates — Use bulk_update to send all widget values at once, reducing WebSocket messages.
  5. Device IDs — Use deterministic IDs based on hardware address (IP, serial number). Don't use random UUIDs.
  6. Graceful shutdown — Handle SIGINT/SIGTERM to disconnect cleanly from both hardware and server.

Node.js Bridge

const { CDAPBridge, Widget } = require('betterdesk-cdap');

const bridge = new CDAPBridge({
    serverUrl: 'ws://betterdesk:21122/cdap',
    deviceId: 'REST-SENSOR-001',
    deviceName: 'REST API Sensor',
    deviceType: 'rest_sensor',
    authMethod: 'api_key',
    apiKey: process.env.CDAP_API_KEY,
    heartbeatSec: 10
});

bridge.addWidget(Widget.gauge('humidity', 'Humidity', {
    group: 'Environment', unit: '%', min: 0, max: 100
}));

bridge.on('collectMetrics', async () => {
    const resp = await fetch('http://sensor-device/api/data');
    const data = await resp.json();
    return { humidity: data.humidity };
});

bridge.connect();

Deployment

Bridges are typically deployed as:

  • systemd services on Linux
  • Docker containers alongside the BetterDesk stack
  • NSSM services on Windows

See bridges/modbus/Dockerfile and bridges/modbus/bridge.service for examples.