Seiten Übersicht Installation Benutzerhandbuch Funktionen API-Referenz Module SDK Entwickler-Guide Quickstart für Entwickler
Inhalt Übersicht Modul-Struktur Manifest (module.json) Manifest-Felder Konfigurations-Feldtypen Event-Metadaten (eventMeta) Event-Gruppen (eventGroups) Effects-Liste (effects) Intent-System Zweck von Intents Praxis-Beispiel: WLED-Modul Aktions-Felder Intents aussenden (aus einem Prozess-Modul) Standard-Subscriptions Wann Intents vs Events verwenden Internationalisierung (i18n) Unterstützte Sprachen i18n-Struktur i18n-Übersetzungsfelder Übersetzungen im Frontend nutzen Sprach-Fallback Modul-Init-Payload (Prozess-Module) Events Native Events (von Autodarts weitergeleitet) Synthetische Events (generiert vom EventParser des Hubs) Cricket- und Tactics-Daten Wurf-Unterschlüssel auf Modul-Ebene (keine Hub-Events) Custom API Routes (Module Routes) Routes registrieren Route Definition Felder Vom Frontend aufrufen Frontend-Entwicklung Setup vite.config.ts WebSocket-Verbindung API-Aufrufe Installation Aus Git-Repository Lokale Installation Best Practices 1. Fehlerbehandlung 2. Konfiguration validieren 3. Ressourcen freigeben 4. Spielerfarben nutzen 5. Temporäre Effekte mit Spielerfarben-Wiederherstellung 6. Fade-Zeit / Übergänge 7. Einzelspieler-Unterstützung 8. Spieler-spezifische Konfigurationsüberschreibungen Beispiel-Module Minimal-Modul Modul mit UI Modul mit externer Hardware Modul mit Audio/TTS Prozess-Modul-Beispiel Debugging Logs Browser DevTools WebSocket Events für Modul-UIs caller Event module Event KI-Funktionen in Modulen Verfügbare KI-Provider Lizenz Support
English

IO-Darts-Agent: Module SDK

Diese Dokumentation beschreibt, wie du eigene Module für den IO-Darts-Agent entwickeln kannst.

Übersicht

Module sind Erweiterungen, die auf Autodarts-Events reagieren und Aktionen ausführen. Jedes Modul besteht aus:

  1. Manifest (module.json): Metadaten und Konfigurationsschema (erforderlich)
  2. Code: entweder direkt im Hub verdrahtet (entry: "internal", nur für Hub-Entwicklung) oder ein eigenständiger Node.js-Prozess auf Basis des @autodarts-hub/module-sdk-Pakets (jeder andere entry-Wert)
  3. Web UI: eigene Benutzeroberfläche, ausgeliefert unter /module-ui/<module-id>/ (optional)

Modul-Struktur

my-module/
├── module.json          # Manifest (erforderlich)
├── package.json         # Erforderlich für ein Prozess-Modul
├── README.md            # Dokumentation
├── src/                 # Prozess-Entry-Point(s), z.B. src/index.ts
├── dist/                # Build-Output (entry zeigt hierher auf dist/index.js)
└── web/                 # Frontend (optional)
    ├── package.json
    ├── vite.config.ts
    └── src/
        ├── main.ts
        ├── App.vue
        └── ...

Manifest (module.json)

Das Manifest definiert alle Metadaten und die Konfiguration deines Moduls:

{
  "id": "my-module",
  "name": "My Awesome Module",
  "version": "1.0.0",
  "description": "Beschreibung des Moduls",
  "author": "Dein Name",
  "entry": "dist/index.js",
  "events": [
    "throw.detected",
    "player.change",
    "match.start",
    "match.finished"
  ],
  "config": {
    "hostAddress": {
      "type": "string",
      "label": "Server-Adresse",
      "description": "IP-Adresse oder Hostname des Servers",
      "required": true,
      "default": "192.168.1.100"
    },
    "port": {
      "type": "number",
      "label": "Port",
      "default": 80,
      "min": 1,
      "max": 65535
    },
    "enabled": {
      "type": "boolean",
      "label": "Aktiviert",
      "default": true
    },
    "mode": {
      "type": "select",
      "label": "Modus",
      "default": "normal",
      "options": [
        { "value": "normal", "label": "Normal" },
        { "value": "advanced", "label": "Erweitert" }
      ]
    }
  }
}

Manifest-Felder

Feld Typ Erforderlich Beschreibung
id string Ja Eindeutige Modul-ID (kebab-case)
name string Ja Anzeigename (Englisch)
version string Ja Semantic Version (x.y.z)
description string Nein Kurzbeschreibung (Englisch)
author string Nein Autor/Entwickler
repository string Nein URL zum Git-Repository
entry string Ja "internal" für Code, der direkt im Hub verdrahtet ist, sonst ein JS-Dateipfad (z.B. "dist/index.js"), den der Hub als eigenen Prozess startet
events string[] Nein AutodartsEventType-Werte, die die Konfig-UI des Moduls als Trigger anbietet
config object Nein Konfigurationsschema
i18n object Nein Mehrsprachige Übersetzungen
intents object Nein Intent-System-Konfiguration
eventMeta object Nein Metadaten für Events (Labels, Beschreibungen)
eventGroups array Nein Gruppierung von Events für die UI
effects array Nein Liste von verfügbaren Effekten (z.B. für WLED)

entry: "internal" funktioniert nur für die drei Module, die der Hub anhand ihrer ID als Sonderfall behandelt (WLED, Game Display, Darts Caller); ein neu erstelltes Modul braucht einen Prozess-Entry-Point. Siehe Developer Guide: Modul-Typen.

Konfigurations-Feldtypen

string

{
  "type": "string",
  "label": "Name",
  "description": "Hilfetext",
  "required": true,
  "default": "Standardwert",
  "placeholder": "Platzhaltertext..."
}

Das placeholder-Attribut ist optional und zeigt einen Hinweistext im leeren Eingabefeld an.

number

{
  "type": "number",
  "label": "Port",
  "default": 80,
  "min": 1,
  "max": 65535,
  "step": 1
}

Das step-Attribut ist optional und definiert die Schrittweite für Slider-Eingaben.

boolean

{
  "type": "boolean",
  "label": "Aktiviert",
  "default": true
}

select

{
  "type": "select",
  "label": "Modus",
  "default": "normal",
  "options": [
    { "value": "normal", "label": "Normal" },
    { "value": "fast", "label": "Schnell" }
  ]
}

color

{
  "type": "color",
  "label": "Farbe",
  "default": "#3b82f6"
}

provider-select

Zeigt ein Dropdown der auf dem Hub konfigurierten KI-Provider (siehe GET /api/ai/provider-options in der API-Dokumentation), optional nach Fähigkeit gefiltert:

{
  "type": "provider-select",
  "label": "TTS-Provider",
  "providerCapability": "audio-tts",
  "default": ""
}

group (Verschachtelte Konfiguration)

Gruppen ermöglichen es, zusammengehörige Einstellungen zu organisieren, mit optionaler bedingter Sichtbarkeit:

{
  "type": "group",
  "label": "TTS Einstellungen",
  "description": "Einstellungen für Text-to-Speech",
  "showIf": { "field": "callerType", "value": "tts" },
  "fields": {
    "voice": {
      "type": "string",
      "label": "Stimme",
      "default": ""
    },
    "rate": {
      "type": "number",
      "label": "Geschwindigkeit",
      "default": 1.0,
      "min": 0.5,
      "max": 2.0,
      "step": 0.1
    }
  }
}

Die showIf-Eigenschaft zeigt die Gruppe bedingt basierend auf dem Wert eines anderen Konfigurationsfelds an.

Event-Metadaten (eventMeta)

Module können Metadaten für Events mit mehrsprachiger Unterstützung bereitstellen:

{
  "eventMeta": {
    "throw.detected": {
      "label": { "en": "Throw detected", "de": "Wurf erkannt", "es": "Tiro detectado" },
      "description": { "en": "A throw was detected", "de": "Ein Wurf wurde erkannt", "es": "Se detectó un tiro" }
    }
  }
}

Diese Metadaten werden in der UI verwendet, um lesbare Event-Namen und Beschreibungen anzuzeigen. Sie sind rein kosmetisch. Sie ändern nicht, welche Events das Modul tatsächlich empfängt.

Event-Gruppen (eventGroups)

Events können in logische Gruppen für bessere UI-Darstellung organisiert werden:

{
  "eventGroups": [
    {
      "title": { "en": "Match Events", "de": "Match Events", "es": "Eventos de Partido" },
      "events": ["match.start", "match.finished", "match.delete"]
    },
    {
      "title": { "en": "Player Events", "de": "Spieler Events", "es": "Eventos de Jugador" },
      "events": ["player.change", "turn.finished"]
    }
  ]
}

Effects-Liste (effects)

Module können eine Liste von verfügbaren Effekten definieren. Dies ist besonders nützlich für Hardware-Module wie WLED:

{
  "effects": [
    { "id": 0, "name": "Solid" },
    { "id": 1, "name": "Blink" },
    { "id": 2, "name": "Breathe" },
    { "id": 9, "name": "Rainbow" },
    { "id": 52, "name": "Fire 2012" },
    { "id": 82, "name": "Fireworks" }
  ]
}

Diese Liste wird in der UI angezeigt und kann für Effekt-Auswahl in Intent-Aktionen verwendet werden.

Intent-System

Das Intent-System ermöglicht Modul-zu-Modul-Kommunikation. Ein Modul kann Aktionen anbieten, die andere Module aufrufen können. Es geht dabei NICHT um das Replizieren von Events, dafür gibt es bereits Events. Intents dienen dazu, spezifische Funktionalitäten eines Moduls anzusprechen.

Zweck von Intents

Intents ermöglichen Modulen:

  1. Fähigkeiten anbieten: ein Modul deklariert, welche Aktionen es ausführen kann (intents.actions)
  2. Von anderen Modulen aufgerufen werden: andere Module können diese Aktionen nutzen
  3. Eigene semantische Intents aussenden: ein Modul kann Intents deklarieren, die es aussendet (intents.emits), auf die andere Module Reaktionen abonnieren können

Praxis-Beispiel: WLED-Modul

Das WLED-Modul bietet Aktionen zur LED-Steuerung an:

{
  "intents": {
    "actions": [
      {
        "type": "solidColor",
        "label": "Einfarbig",
        "description": "Alle LEDs auf eine Farbe setzen",
        "configSchema": {
          "color": { "type": "color", "label": "Farbe", "default": "#ff0000" },
          "brightness": { "type": "number", "label": "Helligkeit", "default": 255, "min": 0, "max": 255 },
          "duration": { "type": "number", "label": "Dauer (ms)", "description": "0 = permanent", "default": 0, "min": 0 }
        }
      },
      {
        "type": "effect",
        "label": "Effekt abspielen",
        "description": "Einen Effekt abspielen",
        "configSchema": {
          "effectId": { "type": "number", "label": "Effekt-ID", "default": 0 },
          "duration": { "type": "number", "label": "Dauer (ms)", "default": 2000, "min": 0 }
        }
      },
      {
        "type": "off",
        "label": "Ausschalten",
        "description": "Alle Ausgaben ausschalten"
      }
    ],
    "defaultSubscriptions": []
  }
}

Anwendungsfall: Ein anderes Modul könnte wled.effect auslösen, wenn ein besonderer Score angesagt wird.

Aktions-Felder

Feld Typ Erforderlich Beschreibung
type string Ja Eindeutiger Aktions-Identifier (wird beim Aufrufen verwendet)
label string Ja Lesbarer Name für die UI-Anzeige
description string Nein Beschreibung der Aktion
configSchema object Nein Konfigurationsoptionen, die der Aufrufer bereitstellen muss/kann

Intents aussenden (aus einem Prozess-Modul)

Wenn du AutodartsModule aus @autodarts-hub/module-sdk erweiterst, rufe die geschützte Methode this.emitIntent(intentId, payload) auf. Deklariere jede ausgesendete Intent-ID unter intents.emits in module.json, damit die UI sie als etwas anzeigen kann, das andere Module abonnieren können:

{
  "intents": {
    "emits": [
      {
        "id": "scoreboard.milestone",
        "description": "Ein bemerkenswerter Score wurde geworfen",
        "category": "my-module",
        "payload": {
          "score": { "type": "number", "description": "Der Score" },
          "playerName": { "type": "string" }
        }
      }
    ]
  }
}
this.emitIntent('scoreboard.milestone', { score: 180, playerName: 'Max' });

Mit dem kleinen Templating-Helfer des SDKs lässt sich Ansagetext an einer Stelle pflegen:

import { registerIntentTemplate } from '@autodarts-hub/module-sdk';

registerIntentTemplate({
  id: 'milestone-180',
  category: 'scoreboard',
  priority: 10,
  duration: 3000,
  templates: { en: '{{playerName}} just threw {{score}}!', de: '{{playerName}} hat gerade {{score}} geworfen!' },
  requiredParams: ['playerName', 'score'],
});

// später
this.emitTemplatedIntent('scoreboard.milestone', 'milestone-180', { playerName: 'Max', score: 180 }, 'de');

Standard-Subscriptions

Module können Standard Event-zu-Aktion-Zuordnungen definieren. Das sind vorkonfigurierte Reaktionen, die ein Modul von Haus aus anbietet (der Nutzer kann sie in der UI trotzdem ändern, gespeichert als intentSubscriptions in der Modul-Konfiguration):

{
  "defaultSubscriptions": [
    {
      "event": "player.change",
      "action": "solidColor",
      "config": { "usePlayerColor": true }
    }
  ]
}

Wann Intents vs Events verwenden

Anwendungsfall Lösung
Auf Spielgeschehnisse reagieren (Würfe, Spielerwechsel, etc.) Events abonnieren
Funktionalität eines anderen Moduls auslösen Intent-Aktion aufrufen
Anderen Modulen eigene Funktionalität anbieten Intent-Aktionen bereitstellen
Mitteilen, dass etwas passiert ist Intent aussenden

Internationalisierung (i18n)

Module können Übersetzungen für mehrere Sprachen bereitstellen. Die Standardsprache ist Englisch, welche für alle Labels im config-Abschnitt verwendet werden sollte. Übersetzungen werden im i18n-Abschnitt definiert.

Unterstützte Sprachen

Die Hub-UI selbst liefert diese zehn mit; ein Modul kann Übersetzungen für eine beliebige Teilmenge bereitstellen.

Code Sprache
en Englisch (Standard)
de Deutsch
es Spanisch
fr Französisch
it Italienisch
nl Niederländisch
ja Japanisch
zh Chinesisch
ru Russisch
uk Ukrainisch

i18n-Struktur

{
  "id": "my-module",
  "name": "My Module",
  "description": "Module description in English",
  "config": {
    "hostAddress": {
      "type": "string",
      "label": "Host Address",
      "description": "IP address of the server"
    }
  },
  "i18n": {
    "en": {
      "name": "My Module",
      "description": "Module description in English",
      "config": {
        "hostAddress": {
          "label": "Host Address",
          "description": "IP address of the server"
        }
      },
      "strings": {
        "connectionSuccess": "Connected successfully!",
        "connectionFailed": "Connection failed"
      }
    },
    "de": {
      "name": "Mein Modul",
      "description": "Modulbeschreibung auf Deutsch",
      "config": {
        "hostAddress": {
          "label": "Host-Adresse",
          "description": "IP-Adresse des Servers"
        }
      },
      "strings": {
        "connectionSuccess": "Erfolgreich verbunden!",
        "connectionFailed": "Verbindung fehlgeschlagen"
      }
    }
  }
}

i18n-Übersetzungsfelder

Feld Beschreibung
name Übersetzter Modulname
description Übersetzte Modulbeschreibung
config Übersetzungen für Config-Felder
config.[key].label Übersetztes Feld-Label
config.[key].description Übersetzte Feld-Beschreibung
config.[key].options Übersetzungen für Select-Optionen (Key: Value Paare)
strings Eigene Strings, die vom Modul-Frontend verwendet werden

Übersetzungen im Frontend nutzen

// Übersetztes Modul-Manifest laden
const response = await fetch(`/api/modules/${moduleId}/translations/${language}`)
const { manifest, strings, availableLanguages } = await response.json()

// Übersetzte Strings verwenden
const successMessage = strings.connectionSuccess || 'Connected!'

Sprach-Fallback

Das Übersetzungssystem verwendet folgende Fallback-Reihenfolge:

  1. Angeforderte Sprache (z.B. de)
  2. Englisch (en) als Fallback
  3. Originalwerte aus dem Manifest

Dies stellt sicher, dass Module auch funktionieren, wenn Übersetzungen unvollständig sind.

Modul-Init-Payload (Prozess-Module)

Wenn onInit(config) eines Prozess-Moduls aufgerufen wird, hat der Hub bereits eine init-IPC-Nachricht gesendet mit:

interface InitPayload {
  moduleId: string;
  config: Record<string, unknown>;
  hubVersion: string;
  subscribedEvents: string[];
}

Events

Module können auf folgende Autodarts-Events reagieren. type ist einer aus einer festen AutodartsEventType-Union. Alle Payload-Felder stehen in der API-Dokumentation.

Native Events (von Autodarts weitergeleitet)

Event Beschreibung Wichtige Daten
board.state Board verbindet/trennt sich Board-Verbindungsinfo
board.event Jedes andere rohe Board-Event eventspezifisch
match.start Match gestartet matchId, boardId
match.state Match-Status-Update players, gameScores, player, round, settings
match.delete Match abgebrochen matchId, boardId
match.finish Benutzer schließt die Ergebnisanzeige matchId, boardId
throw.detected Wurf erkannt segment, coords?, throwNumber, boardId, points, turnId, turnPoints, busted (die letzten drei nur bei Würfen aus match.state, dem Normalfall); in Cricket/Tactics zusätzlich marks, turnMarks
takeout.started Dart-Entnahme gestartet boardId
takeout.finished Dart-Entnahme beendet boardId

Synthetische Events (generiert vom EventParser des Hubs)

Event Beschreibung Wichtige Daten
player.change Aktiver Spieler gewechselt previousPlayer, currentPlayer, playerIndex, playerName, remaining
turn.finished Aufnahme beendet playerIndex, playerName, turnScore, busted, remaining, round; in Cricket/Tactics zusätzlich turnMarks
game.finished Leg gewonnen winner, winnerName, winnerPlayerIndex, leg, set
match.finished Match gewonnen winner, winnerName, winnerPlayerIndex, scores
match.result Vollständige Ergebnisdaten, zusammen mit match.finished gesendet chalkboards, stats, scores, settings
cricket.state Cricket/Tactics-Zustand: Punkte und Treffer pro Feld je Spieler matchId, set, leg, mode, cutThroat, fields, deadFields, players[].{index,name,points,marks}
cricket.closed Cricket/Tactics: ein Spieler schließt ein Feld mit dem dritten Treffer matchId, set, leg, field, playerIndex, playerName

Hinweis: Synthetische Events bieten praktische Hooks für Module. turn.finished wird ausgelöst, wenn 3 Darts geworfen wurden, ein Bust auftritt oder sich die Rundennummer ändert.

Hinweis zu game: match.state, throw.detected, turn.finished, player.change, game.finished, match.finished, match.result, cricket.state und cricket.closed tragen zusätzlich data.game: { variant: string; mode: 'x01' | 'cricket' | 'tactics' | 'other'; cutThroat: boolean }. match.start, match.delete und match.finish tragen kein game.

Cricket- und Tactics-Daten

Cricket und Tactics (eine Cricket-Variante mit den Feldern 10 bis 20 statt 15 bis 20) laufen über dieselben Events wie X01, mit den oben genannten Zusatzfeldern. Felder sind Strings '10' bis '20' sowie '25' für Bull. Spieler stehen über den stabilen index (nicht die Array-Position), bleiben also über die Spieler-Array-Rotation nach einem Leg hinweg zuordenbar.

throw.detected in Cricket/Tactics, zusätzlich zu den üblichen Feldern:

{
  "segment": { "name": "20", "number": 20, "multiplier": 1, "bed": "Single" },
  "points": 20,
  "turnId": "abc123",
  "marks": 1,
  "turnMarks": 2,
  "game": { "variant": "Cricket", "mode": "cricket", "cutThroat": false }
}

marks sind die Treffer dieses einen Darts, turnMarks die Summe der Aufnahme bis einschließlich diesem Dart (für einen 180 auf dem dritten Dart sichtbar). turn.finished trägt nur turnMarks (Summe der ganzen Aufnahme).

cricket.state, gesendet beim ersten Cricket-Zustand und immer, wenn sich Treffer oder Punkte ändern:

{
  "matchId": "match-1",
  "set": 1,
  "leg": 1,
  "mode": "cricket",
  "cutThroat": false,
  "fields": ["20", "19", "18", "17", "16", "15", "25"],
  "deadFields": ["20"],
  "players": [
    { "index": 0, "name": "Anna", "points": 24, "marks": { "20": 3, "19": 1, "18": 0, "17": 0, "16": 0, "15": 0, "25": 0 } },
    { "index": 1, "name": "Max", "points": 12, "marks": { "20": 3, "19": 0, "18": 0, "17": 0, "16": 0, "15": 0, "25": 0 } }
  ]
}

cricket.closed, einmal pro Feld und Spieler im Leg, nicht für Felder, die bereits aus einem vorherigen Zustand (z.B. nach einem Hub-Neustart) geschlossen waren:

{ "matchId": "match-1", "set": 1, "leg": 1, "field": "20", "playerIndex": 0, "playerName": "Anna" }

Starke Aufnahme ist kein Hub-Event. Es gibt kein turn.strong oder Ähnliches; Module vergleichen turnMarks (aus throw.detected oder turn.finished) selbst mit einer eigenen Schwelle (3 bis 9, Standard 6). WLED und Darts Caller sind die Referenzimplementierungen.

GET /api/match/current liefert zusätzlich cricket (gleiche Form wie cricket.state, null außerhalb von Cricket/Tactics) und turns (ein Array mit der letzten Aufnahme).

Wurf-Unterschlüssel auf Modul-Ebene (keine Hub-Events)

Es gibt kein throw.triple, throw.double, throw.180, throw.bullseye, throw.bull, throw.miss oder throw.highfinish auf dem Event-Bus oder im WebSocket des Hubs. Ebenso ist cricket.marks (Starke Aufnahme) kein Hub-Event. Manche Module (das WLED-Modul ist die Referenzimplementierung, siehe modules/wled/src/WledHandler.ts) leiten diese als interne Lookup-Schlüssel aus einem throw.detected-Payload ab, rein um eine Aktion pro Ereignis in der eigenen Konfiguration auszuwählen:

function getThrowEventKey(data: { turnPoints?: number; busted?: boolean; segment?: { number?: number; multiplier?: number }; points?: number }): string {
  const { turnPoints = 0, segment, points = 0 } = data;
  const { number = 0, multiplier = 0 } = segment ?? {};
  if (turnPoints === 180) return 'throw.180';
  if (data.busted) return 'throw.bust';
  if (number === 25 && multiplier === 2) return 'throw.bullseye';
  if (number === 25 && multiplier === 1) return 'throw.bull';
  if (multiplier === 3) return 'throw.triple';
  if (multiplier === 2) return 'throw.double';
  if (points === 0) return 'throw.miss';
  return 'throw.detected';
}

Im Payload von throw.detected ist points der Wert des Darts, turnPoints die laufende Summe der Aufnahme einschließlich dieses Darts (180 beim dritten T20) und busted ist nur bei dem Dart wahr, der den Bust ausgelöst hat.

Soll die UI deines Moduls Aktionen pro Wurftyp anbieten, implementiere dieselbe Art Ableitung selbst; dafür gibt es heute keinen vom Hub bereitgestellten Helfer.

Custom API Routes (Module Routes)

Module können eigene API-Endpunkte registrieren, die unter /api/modules/<module-id>/routes/ verfügbar sind. Wenn du AutodartsModule aus @autodarts-hub/module-sdk erweiterst, nutze dessen eingebaute Route-API statt eigene IPC-Nachrichten zu bauen:

Routes registrieren

import { AutodartsModule, type ModuleRouteDefinition } from '@autodarts-hub/module-sdk';

class MyModule extends AutodartsModule {
  protected async onInit(): Promise<void> {
    const routes: ModuleRouteDefinition[] = [
      { method: 'GET', path: '/status', handlerId: 'getStatus', description: 'Get module status' },
      { method: 'POST', path: '/test', handlerId: 'runTest', description: 'Run a test action' },
      { method: 'GET', path: '/effects/:id', handlerId: 'getEffect', description: 'Get effect by ID' },
    ];

    this.registerRoutes(routes);

    this.setRouteHandler('getStatus', async () => ({
      statusCode: 200,
      body: { status: 'ok', connected: true },
    }));

    this.setRouteHandler('runTest', async (request) => {
      await runTestAction(request.body);
      return { statusCode: 200, body: { success: true } };
    });

    this.setRouteHandler('getEffect', async (request) => {
      const effect = getEffectById(request.params.id);
      return { statusCode: effect ? 200 : 404, body: effect ?? { error: 'Not found' } };
    });
  }

  protected async onEvent(): Promise<void> {}
}

registerRoutes und setRouteHandler sind geschützte Methoden auf AutodartsModule; rufe sie innerhalb deiner Modul-Klasse auf (z.B. in onInit).

Route Definition Felder

Feld Typ Erforderlich Beschreibung
method string Ja HTTP-Methode (GET, POST, PUT, PATCH, DELETE)
path string Ja Pfad relativ zu /api/modules/:moduleId/routes/
handlerId string Ja Eindeutige ID für den Handler
description string Nein Beschreibung des Endpunkts
requireAuth boolean Nein Authentifizierung erforderlich (Standard: true)
schema object Nein JSON-Schema für Validierung

Vom Frontend aufrufen

// Modul-Route aufrufen
const response = await fetch('/api/modules/my-module/routes/status');
const status = await response.json();

// Mit Body
await fetch('/api/modules/my-module/routes/test', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ action: 'flash' }),
});

Frontend-Entwicklung

Module können ein eigenes Frontend mitbringen, das unter /module-ui/<module-id>/ bereitgestellt wird.

Setup

cd modules/my-module/web
npm init -y
npm install vue vite @vitejs/plugin-vue

vite.config.ts

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  base: '/module-ui/my-module/',
  build: {
    outDir: 'dist',
    emptyOutDir: true,
  },
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        ws: true,
      },
    },
  },
})

WebSocket-Verbindung

Module können sich mit dem Hub-WebSocket verbinden:

const ws = new WebSocket(`ws://${window.location.host}/api/events`)

ws.onmessage = (event) => {
  const message = JSON.parse(event.data)

  switch (message.type) {
    case 'status':
      // Verbindungsstatus
      break
    case 'event':
      // Autodarts-Event: message.event.type, message.event.data
      handleEvent(message.event)
      break
    case 'log':
      // Log-Eintrag
      break
  }
}

function handleEvent(event: { type: string; data: unknown }) {
  switch (event.type) {
    case 'throw.detected':
      // Wurf verarbeiten
      break
    case 'player.change':
      // Spielerwechsel
      break
  }
}

API-Aufrufe

Module können die Hub-API nutzen:

// Modul-Konfiguration laden
const config = await fetch('/api/modules/my-module/config')
  .then(r => r.json())

// Konfiguration speichern
await fetch('/api/modules/my-module/config', {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ key: 'value' })
})

// Spieler laden (für Farben etc.)
const players = await fetch('/api/players').then(r => r.json())

Installation

Aus Git-Repository

Module können direkt aus Git installiert werden:

# Über die Web-UI
# Oder per API:
curl -X POST http://localhost:3000/api/modules/install \
  -H "Content-Type: application/json" \
  -d '{"source": "git", "url": "https://github.com/user/my-module.git"}'

Lokale Installation

Für Entwicklung können Module lokal installiert werden:

# Modul-Ordner verlinken
ln -s /path/to/my-module modules_installed/my-module

Best Practices

1. Fehlerbehandlung

try {
  await sendToDevice(payload)
} catch (error) {
  console.error(`Module error: ${error.message}`)
  // Modul sollte nicht crashen
}

2. Konfiguration validieren

function validateConfig(config) {
  if (!config.hostAddress) {
    throw new Error('Host address is required')
  }
  // Weitere Validierungen...
}

3. Ressourcen freigeben

protected async onShutdown(reason: string): Promise<void> {
  if (wsConnection) {
    wsConnection.close()
  }
}

4. Spielerfarben nutzen

// Farbe aus Event oder Spielerverwaltung
const playerColor = event.data.color || '#ffffff'

5. Temporäre Effekte mit Spielerfarben-Wiederherstellung

// Effekt mit automatischer Wiederherstellung der Spielerfarbe (empfohlen)
async function flashEffect(duration = 1000, playerColor: string) {
  await setEffect('flash')

  setTimeout(async () => {
    // Zur Spielerfarbe zurückkehren, nicht zum vorherigen Gerätezustand
    await setColor(playerColor)
  }, duration)
}

Best Practice: Bei temporären Effekten zur aktuellen Spielerfarbe zurückkehren statt zum vorherigen Gerätezustand. Dies gewährleistet konsistentes Verhalten auch wenn der vorherige Zustand falsch oder unbekannt war.

6. Fade-Zeit / Übergänge

// Unterstützung für sanfte Übergänge bei Farbwechseln
async function setColorWithFade(color: string, fadeTimeMs: number) {
  const payload = {
    seg: [{ col: [[r, g, b]] }],
    tt: Math.round(fadeTimeMs / 100) // WLED verwendet 100ms Einheiten; tt gilt nur für diesen Befehl
  }
  await sendToDevice(payload)
}

Hinweis: Nutze tt für einen Übergang pro Befehl. transition ändert die Standard-Übergangszeit des Controllers für alle folgenden Befehle.

7. Einzelspieler-Unterstützung

// Sowohl Einzelspieler- als auch Mehrspieler-Spiele behandeln
async function handlePlayerChange(event) {
  // Dieses Event wird für beide Modi ausgelöst
  const playerColor = event.data.color || '#ffffff'
  await setColor(playerColor)
}

async function handleTakeoutFinished(event) {
  // Spielerfarbe nach Dart-Entnahme wiederherstellen
  // Funktioniert für Einzel- und Mehrspieler
  const currentPlayer = getCurrentPlayerFromMatchState()
  await setColor(currentPlayer.color)
}

8. Spieler-spezifische Konfigurationsüberschreibungen

Module können spieler-spezifische Einstellungen unterstützen, die die Standard-Konfiguration überschreiben, wenn ein bestimmter Spieler aktiv ist. Dieses Pattern wird von WLED, Darts Caller und Game Display verwendet.

Konfigurationsstruktur

{
  "config": {
    "playerOverrides": {
      "type": "array",
      "label": "Player Overrides",
      "description": "Player-specific configuration overrides",
      "default": [],
      "hidden": true
    }
  }
}

Überschreibungs-Datenstruktur

interface PlayerOverride {
  playerId?: string      // Optionale Autodarts-Spieler-ID
  playerName: string     // Spielername (für Anzeige und Fallback-Matching)
  // Modul-spezifische Felder:
  color?: string         // z.B. für WLED
  animations?: {         // z.B. für Game Display
    '180'?: { enabled: boolean; url?: string; sound?: string }
    legWin?: { enabled: boolean; url?: string; sound?: string }
    // ...
  }
  voiceStyle?: string    // z.B. für Darts Caller
}

Spieler-Matching implementieren

Wichtig: Autodarts sendet Spielernamen in Kleinbuchstaben. Verwende immer case-insensitive Vergleiche:

function getPlayerOverride(
  playerOverrides: PlayerOverride[],
  playerName?: string,
  playerId?: string
): PlayerOverride | undefined {
  if (!playerOverrides || playerOverrides.length === 0) {
    return undefined
  }

  // 1. Zuerst nach playerId suchen (exakte Übereinstimmung)
  if (playerId) {
    const byId = playerOverrides.find(p => p.playerId === playerId)
    if (byId) return byId
  }

  // 2. Nach playerName suchen (case-insensitive!)
  if (playerName) {
    const byName = playerOverrides.find(
      p => p.playerName.toLowerCase() === playerName.toLowerCase()
    )
    if (byName) return byName
  }

  return undefined
}

UI-Integration

Der Hub stellt den GET /api/players-Endpunkt bereit, der registrierte Spieler auflistet. Module können dies nutzen, um Spieler für Überschreibungen auszuwählen:

// Verfügbare Spieler laden
const players = await fetch('/api/players').then(r => r.json())

// players: Array<{ id: string, name: string, color?: string }>

Event-Behandlung mit Überschreibungen

async function handleEvent(event: GameEvent) {
  const { playerName, playerId } = event.data

  // Spieler-spezifische Konfiguration laden
  const playerOverride = getPlayerOverride(
    config.playerOverrides,
    playerName,
    playerId
  )

  if (playerOverride?.animations?.['180']?.enabled) {
    // Spieler-spezifische Animation verwenden
    await playAnimation(playerOverride.animations['180'].url)
  } else if (config.animations?.['180']?.enabled) {
    // Auf Standard-Konfiguration zurückfallen
    await playAnimation(config.animations['180'].url)
  }
}

Best Practice: Prüfe immer zuerst die spieler-spezifische Überschreibung, dann falle auf die Standard-Modul-Konfiguration zurück. Dies ermöglicht personalisierte Erlebnisse pro Spieler, während Standardwerte für nicht konfigurierte Spieler erhalten bleiben.

Beispiel-Module

Minimal-Modul

{
  "id": "minimal",
  "name": "Minimal Module",
  "version": "1.0.0",
  "entry": "dist/index.js",
  "events": ["throw.detected"]
}

Modul mit UI

Siehe das Game Display Modul als Referenz für ein mitgeliefertes Modul mit eigenem Vue.js-Frontend.

Modul mit externer Hardware

Siehe das WLED-Modul als Referenz für ein Modul, das externe Hardware steuert mit:

  • Dauer-basierte Effekte, die zur Spielerfarbe zurückkehren
  • Konfigurierbare Fade-Zeit (Übergänge)
  • Einzelspieler- und Mehrspieler-Unterstützung
  • Event-basierte Aktionskonfiguration, inklusive Wurf-Unterschlüsseln auf Modul-Ebene (siehe oben)
  • Intent-System-Unterstützung für Modul-zu-Modul-Kommunikation

Modul mit Audio/TTS

Siehe das Darts Caller Modul als Referenz für ein Modul, das:

  • Browser-TTS, ElevenLabs Live-TTS, Voice Packs oder eigene Audiodateien nutzt
  • Einen Voice Pack Generator mit ElevenLabs enthält
  • Mehrere Ausgabemodi unterstützt

Prozess-Modul-Beispiel

Das Autodarts-Trainer-Modul (separat entwickelt, nicht Teil dieses Repositorys) ist ein Beispiel für ein Modul, das als eigener Prozess läuft (entry: "dist/index.js") und eigene REST-Routen (/api/trainer/*) direkt registriert, statt über den oben beschriebenen Module-Routes-Mechanismus.

Debugging

Logs

Modul-Aktivitäten werden im Hub-Log angezeigt:

  1. Öffne das Web-Interface
  2. Gehe zu Logs (/logs)
  3. Filtere nach deinem Modul

Browser DevTools

Für Frontend-Module:

  1. Öffne /module-ui/my-module/
  2. Öffne die Browser-Entwicklertools (F12)
  3. Prüfe Console und Network-Tab

WebSocket Events für Modul-UIs

Modul-Frontends können auf spezielle WebSocket-Events lauschen (die vollständige Liste steht in der API-Dokumentation):

caller Event

Für das Darts Caller Modul, löst Audio-Wiedergabe im Browser aus:

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data)
  if (msg.type === 'caller') {
    const { text, audioFile } = msg.request
    // Audio abspielen oder TTS verwenden
    if (audioFile) {
      playAudio(audioFile)
    } else {
      speakText(text)
    }
  }
}

module Event

Benachrichtigt über Modul-Status-Änderungen:

{
  "type": "module",
  "moduleId": "wled",
  "status": "running" // oder "stopped", "error"
}

KI-Funktionen in Modulen

Es gibt derzeit keine IPC-Nachricht, mit der ein Prozess-Modul das KI-Provider-System des Hubs direkt aufrufen kann (kein ai.request/ai.response-Roundtrip existiert im @autodarts-hub/module-sdk). Zwei Wege funktionieren heute:

  1. Prozessinterne (mitgelieferte) Module können den Service direkt importieren, so wie es das Darts-Caller-Modul für ElevenLabs tut:

    import { getAIProviderService } from '../../../src/core/ai/index.js';
    
    const service = getAIProviderService();
    const result = await service.generateSpeech({ text: 'One hundred and eighty!' }, 'elevenlabs');
    
  2. Prozess-Module können die eigene REST-API des Hubs über HTTP aufrufen, da es sich einfach um einen lokalen Server handelt:

    const response = await fetch('http://localhost:3000/api/ai/generate/speech', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ providerId: 'elevenlabs', text: 'One hundred and eighty!' }),
    });
    

    Das funktioniert direkt, solange Web-Auth auf dem Hub deaktiviert ist; ist Web-Auth aktiviert, benötigt jede /api/ai/*-Route ein Session-Cookie (siehe API-Dokumentation), und ein Modul müsste sich selbst einloggen, um eines zu bekommen.

Verfügbare KI-Provider

Provider Fähigkeiten Modul-Anwendungsfälle
ElevenLabs Text-to-Speech Voice Packs, Spielernamen-Synthese, eigene Ansagen
Runway ML Video-Generierung Eigene Celebration-Animationen (eine von zwei Engines)
Higgsfield Bild-/Video-Generierung Eigene Celebration-Animationen (die andere Engine)
OpenAI Text, Bilder Dynamische Inhalte, Bildgenerierung
Anthropic Text Inhaltsgenerierung, Analyse
Stability AI Bilder Eigene Grafiken
Ollama Text (Lokal) Lokale KI-Inferenz
Replicate Verschiedene Eigene gehostete Modelle

Lizenz

IO-Darts-Agent selbst steht unter der PolyForm Noncommercial License 1.0.0. Das Modul-SDK in module-sdk/ (@autodarts-hub/module-sdk) steht unter der MIT-Lizenz (module-sdk/LICENSE), damit Module von Dritten es frei nutzen können.

Support

Bei Fragen oder Problemen:

  1. Prüfe die API-Dokumentation
  2. Schaue dir die Beispiel-Module unter /modules/ an
  3. Öffne ein Issue im GitHub-Repository