Nachrichten Client → Server
Nachrichtenformat
Jede Nachricht ist ein JSON-Objekt mit genau einem Schlüssel der obersten Ebene, der den Befehl identifiziert. Der Wert ist ein Objekt, dessen Struktur vom Befehl abhängt.
Alle Client-Befehlsschlüssel stehen in GROSSBUCHSTABEN (z. B. DU, SV, SA, GC).
Befehlsübersicht
| Schlüssel | Beschreibung |
|---|---|
DU | Vollständigen Dump anfordern |
XX | Heartbeat / Keepalive |
LO | Anmeldung |
IL | Sprache ändern |
SV | Wert eines einzelnen Elements senden |
SA | Alle Elementwerte senden |
DP | Property-Dump anfordern |
I1 | Klick auf preicon1 der Property |
I2 | Klick auf preicon2 der Property |
I3 | Klick auf posticon1 der Property |
I4 | Klick auf posticon2 der Property |
J1 | Klick auf preicon des Elements |
J2 | Klick auf posticon des Elements |
PL | Profil laden |
PS | Profil speichern |
GC | Grid-Zeile erstellen |
GU | Grid-Zeile aktualisieren |
GD | Grid-Zeile löschen |
GF | Grid-Zeile auswählen |
GH | Grid-Zeile nach oben verschieben |
GB | Grid-Zeile nach unten verschieben |
GR | Grid zurücksetzen (leeren) |
CL | Modulkonfiguration laden |
CS | Aktuelle Konfiguration speichern |
FS | Überwachten Dateiordner wählen |
ML | Modul laden |
MK | Modul entladen |
QY | Modul abfragen (Inter-Modul-Mechanismus, nicht für Clients gedacht) |
YA | Eingebetteten INDI-Server starten |
YZ | Eingebetteten INDI-Server stoppen |
YL | INDI-Treiber laden |
YR | INDI-Treiber neu laden |
YS | INDI-Treiber stoppen |
YC | Gerät eines INDI-Treibers verbinden |
YD | Gerät eines INDI-Treibers trennen |
YK | Gespeicherte INDI-Treiberkonfiguration laden |
Alle Nachrichten, die ein Modul oder eine Property ansprechen, verwenden folgende Struktur:
{
"COMMANDE": {
"m": {
"NomDuModule": {
"p": {
"nomDeLaPropriété": { ... }
}
}
}
}
}wobei m für modules und p für properties steht.
Sitzungsverwaltung
DU — Vollständigen Dump anfordern
Muss unmittelbar nach der Verbindung gesendet werden. Der Server antwortet mit einer d-Nachricht, die alle Module enthält.
{ "DU": { "language": "fr" } }| Feld | Beschreibung |
|---|---|
language | Bevorzugte Sprache für übersetzte Bezeichnungen ("en", "fr", usw.) |
XX — Heartbeat
Alle 30 Sekunden zu senden, um die Verbindung aktiv zu halten. Der Server antwortet mit xx.
{ "XX": {} }LO — Anmeldung
Erforderlich, wenn der Server die Zugriffskontrolle aktiviert hat (grant-client ist "-1" in der Dump-Antwort).
{
"LO": {
"user": "username",
"pw": "password",
"language": "fr"
}
}IL — Sprache ändern
Ändert die Sprache der übersetzten Bezeichnungen ohne erneute Verbindung.
{ "IL": { "language": "fr" } }Aktualisierung von Property-Werten
SV — Wert eines einzelnen Elements senden
Zu verwenden, wenn directedit beim Element true ist — sendet den Wert eines einzelnen Elements sofort. Bei einem Element mit directedit: false lehnt das Backend es ab (Fehler im Modul-Log, nichts wird angewendet): stattdessen SA verwenden.
{
"SV": {
"m": {
"Focus": {
"p": {
"parameters": {
"e": {
"iterations": 7
}
}
}
}
}
}
}e enthält genau ein Paar Elementname: Wert.
SA — Alle Elementwerte senden
Wird verwendet, um alle editierbaren Elemente einer Property auf einmal zu übermitteln (z. B. aus einem Formular). Wird auch verwendet, wenn directedit false ist.
{
"SA": {
"m": {
"Focus": {
"p": {
"parameters": {
"e": {
"iterations": 7,
"exposure": 3.5,
"active": true
}
}
}
}
}
}
}Ein SA muss alle editierbaren Elemente der Property enthalten. Fehlt auch nur eines, wendet das Backend gar nichts an — nicht einmal die vorhandenen Elemente — und protokolliert serverseitig eine Warnung (für den Client sichtbar über eine l-Nachricht). Das ist kein Laufzeitfall, der elegant abgefangen werden soll: Es zeigt einen Bug im Client an, der immer den vollständigen Satz an Elementen senden muss.
DP — Property-Dump anfordern
Bittet den Server, den vollständigen Zustand einer Property erneut zu senden, als wäre sie gerade (neu) geladen worden — nützlich, um eine Property zu aktualisieren, bei der ein Client eine Desynchronisation vermutet, ohne das ganze Modul neu zu laden. Der Server antwortet mit einer ap-Nachricht für genau diese Property.
{
"DP": {
"m": {
"Focus": {
"p": {
"parameters": {}
}
}
}
}
}Kein e-Feld: DP bezieht sich auf die gesamte Property, nicht auf ein einzelnes Element.
Icon-Aktionen
preicon1/preicon2/posticon1/posticon2 (Properties) und preicon/posticon (Elemente, siehe Datenmodell) sind keine Dekoration: jedes ist eine klickbare Aktion. Ein nicht-leerer Wert ist der Name des zu rendernden Google-Fonts-Icons (Material Icons); ein Klick darauf sendet den passenden Befehl unten, ganz ohne Wert oder Parameter — ein leeres Payload, das einfach die Property (oder das Element) adressiert. Was die Aktion serverseitig tatsächlich tut (Profil speichern, Profil laden, Prozess starten…), entscheidet allein das Modul; die einzige Aufgabe des Clients ist, das Icon zu rendern und den Klick unverändert weiterzugeben.
Ein leerer String bedeutet, dass an dieser Stelle kein Icon vorhanden ist — nichts zu rendern, folglich nichts zu klicken oder zu senden. Für ein leeres Icon-Feld sollte weder ein Platzhalter gerendert noch ein Klick-Handler registriert werden.
I1 — Klick auf preicon1 der Property
{
"I1": {
"m": {
"Focus": {
"p": {
"saveprofile": {}
}
}
}
}
}I2 — Klick auf preicon2 der Property
{
"I2": {
"m": {
"Focus": {
"p": {
"loadprofile": {}
}
}
}
}
}I3 — Klick auf posticon1 der Property
Gleiche Struktur wie I1 / I2.
I4 — Klick auf posticon2 der Property
Gleiche Struktur wie I1 / I2.
J1 — Klick auf preicon des Elements
{
"J1": {
"m": {
"Focus": {
"p": {
"devices": {
"e": {
"myDevice": {}
}
}
}
}
}
}
}J2 — Klick auf posticon des Elements
Gleiche Struktur wie J1.
Profilverwaltung
PL — Profil laden
{
"PL": {
"m": {
"Focus": {
"profile": "myProfile"
}
}
}
}Der Server antwortet mit einer fl-Nachricht (Profil geladen) und sendet anschließend die aktualisierten Property-Werte.
fc geht fl vorausDer Server sendet unmittelbar vor fl ein fc (nicht gespeicherte Änderungen), als Zwischenschritt beim Laden des Profils, bevor sich die Werte der Property tatsächlich ändern. Ein Client, der auf fc reagiert, indem er einen Hinweis auf „nicht gespeicherte Änderungen" anzeigt, sieht diesen für eine Nachricht lang aufblitzen und sich selbst korrigieren, sobald direkt danach fl eintrifft — erwartet, kein Bug.
PS — Profil speichern
{
"PS": {
"m": {
"Focus": {
"profile": "myProfile"
}
}
}
}Der Server antwortet mit einer fs-Nachricht (Profil gespeichert).
Grid-Operationen
Grid-Befehle betreffen eine bestimmte Property innerhalb eines Moduls. Alle verwenden die m / p-Struktur.
GC — Grid-Zeile erstellen
Fügt der Grid eine neue Zeile mit den übergebenen Elementwerten hinzu.
{
"GC": {
"m": {
"Sequencer": {
"p": {
"sequence": {
"e": {
"target": "M31",
"exposure": 120,
"count": 10
}
}
}
}
}
}
}Der Server antwortet mit einer gc-Nachricht, die den Index der neuen Zeile bestätigt.
ea einfach anwenden, wie es kommt — nichts zu hinterfragenDas Erstellen einer Grid-Zeile wendet die übermittelten Werte zuerst auf die Basis-Elemente der Property an, fügt dann die Zeile hinzu und sendet gc. Das kann mehr als eine ea-Nachricht brauchen (eine pro geändertem Basis-Element, nacheinander gesendet — das Senden von target/exposure/count wie oben erzeugt drei separate ea-Nachrichten vor dem abschließenden gc), aber ein Client muss diese Anzahl nie kennen oder vorhersagen: der Server sendet immer nur genau das, was nötig ist, um den Datastore des Clients korrekt zu halten — nicht mehr und nicht weniger (siehe Grundsätze der Client-Architektur). Jede Nachricht genau so anwenden, wie sie kommt, wie immer — das Ergebnis ist korrekt, egal wie viele Nachrichten es waren.
Der einzige sichtbare Nebeneffekt: eine Property, die gleichzeitig ihre Basis-Elemente und ihr Grid anzeigt (showElts und showGrid beide aktiv — eine unübliche Kombination), flackert durch diese Zwischenzustände, bevor die neue Zeile erscheint. Das ist der echte Datastore-Zustand, kein Bug, und er löst sich von selbst auf — kein Grund, ihn herauszufiltern oder mit Polling zu umgehen.
GU — Grid-Zeile aktualisieren
Aktualisiert eine bestimmte Zeile (nullbasierter Index i) mit neuen Elementwerten.
{
"GU": {
"m": {
"Sequencer": {
"p": {
"sequence": {
"i": 1,
"e": {
"target": "M42",
"exposure": 60
}
}
}
}
}
}
}GD — Grid-Zeile löschen
Löscht die Zeile mit Index i (nullbasiert).
{
"GD": {
"m": {
"Sequencer": {
"p": {
"sequence": {
"i": 0
}
}
}
}
}
}GF — Grid-Zeile auswählen
Fordert den Server auf, eine Grid-Zeile in die editierbaren Elemente der Property zu laden (wird verwendet, wenn eine Zeile zur Bearbeitung angeklickt wird).
{
"GF": {
"m": {
"Sequencer": {
"p": {
"sequence": {
"i": 2
}
}
}
}
}
}GH — Grid-Zeile nach oben verschieben
Verschiebt die Zeile mit Index i um eine Position nach oben.
{
"GH": {
"m": {
"Sequencer": {
"p": {
"sequence": {
"i": 3
}
}
}
}
}
}GB — Grid-Zeile nach unten verschieben
Verschiebt die Zeile mit Index i um eine Position nach unten.
{
"GB": {
"m": {
"Sequencer": {
"p": {
"sequence": {
"i": 2
}
}
}
}
}
}GR — Grid zurücksetzen
Leert das gesamte Grid einer Property (alle Zeilen entfernt) — nicht nur eine einzelne Zeile wie GD. Der Server sendet daraufhin gr.
{
"GR": {
"m": {
"Sequencer": {
"p": {
"sequence": {}
}
}
}
}
}Kein i-Feld: GR bezieht sich auf das gesamte Grid, nicht auf eine einzelne Zeile.
Modul-Lebenszyklus
CL — Konfiguration laden
Lädt alle Module einer gespeicherten Konfiguration (Liste in controllerdata.availableconfs, siehe Controller).
{ "CL": { "name": "modules" } }CS — Aktuelle Konfiguration speichern
Speichert die Menge der aktuell geladenen Module unter dem angegebenen Namen (GlobalDatastore ist nie enthalten).
{ "CS": { "name": "modules" } }ML — Modul laden
Fordert den Server auf, ein Modul dynamisch zu laden. Diese Befehle richten sich an den Controller: der Payload verwendet nicht den m/p-Umschlag.
{ "ML": { "lib": "focus", "label": "MyFocuser", "profile": "default" } }| Feld | Beschreibung |
|---|---|
lib | Bibliothek des Moduls, ohne das Präfix libost (der Server fügt es hinzu): focus für libostfocus. Die verfügbaren Bibliotheken sind die Schlüssel von controllerdata.libraries |
label | Label dieser Instanz; der Modulname ist dieses Label ohne Leerzeichen. Ist ein Modul dieses Namens bereits geladen, wird die Anfrage abgelehnt |
profile | Name des zu ladenden Profils (optional) |
Die Schlüssel von controllerdata.libraries sind bereits präfigiert (libostfocus, libostdummy…). Der Server stellt libost bedingungslos erneut voran, sodass ein direkt weitergereichter Schlüssel doppelt präfigiert wird und fehlschlägt:
{ "ML": { "lib": "libostdummy", "label": "MyDummy" } }Error loading library MyDummy - Cannot load library libostlibostdummy: cannot open shared object file: No such file or directoryDas Präfix libost zuerst vom Schlüssel entfernen (dieselbe Transformation, auf die sich die Schlüsselkonvention von profiles stützt, siehe Controller-Daten):
{ "ML": { "lib": "dummy", "label": "MyDummy" } }MK — Modul entladen
Fordert den Server auf, ein laufendes Modul zu entladen, bezeichnet durch seinen Namen.
{ "MK": { "name": "MyFocuser" } }GlobalDatastore kann nicht entladen werden: die Anfrage wird abgelehnt (Fehlermeldung in den Logs).
Modul abfragen
QY — Modul abfragen
QY/qa existiert in erster Linie, damit ein Modul einem anderen Modul intern eine Frage stellen kann (ein Modul, das Basemodule::otherModuleQuery() aufruft, läuft über denselben Mechanismus). Ein Client kann technisch auch QY senden — die Hülle ist identisch — aber dafür gibt es heute keinen etablierten Anwendungsfall auf Client-Seite. Hier der Vollständigkeit halber dokumentiert, und weil es relevant sein wird, wenn dokumentiert wird, wie man ein Modul baut — nicht als etwas, worauf ein Client eine Funktion aufbauen sollte.
Generischer, pro Modul definierter Anfrage-/Antwort-Mechanismus: einem Modul eine benannte Frage mit Parametern stellen, eine benannte Antwort über qa zurückerhalten. Welche Anfragenamen ein Modul tatsächlich versteht (falls überhaupt), entscheidet allein das Modul — das Protokoll definiert nur die Hülle. Zu beachten: keine p-Hülle hier, QY adressiert das Modul selbst, nicht eine seiner Properties.
{
"QY": {
"m": {
"Sequencer": {
"query": "profileduration",
"params": { "profile": "default" }
}
}
}
}Heute implementiert nur das Modul Sequencer eine echte Antwort (profileduration), intern genutzt vom Planner-Modul. Kein anderes ausgeliefertes Modul antwortet bislang sinnvoll auf irgendeinen Anfragenamen.
Dateisystem
FS — Überwachten Ordner wählen
Bestimmt den Ordner, dessen Dateiliste der Server veröffentlicht (siehe Dateisystem). Der Pfad ist relativ zum Webroot; die leere Zeichenkette bezeichnet das Wurzelverzeichnis.
{ "FS": { "folder": "/Sequencer/gam Cyg/LIGHT/Luminance" } }Die Änderung gilt global für den Server (alle Clients sehen die neue Liste). Der Pfad wird nicht validiert: ein nicht existierender Ordner ergibt eine leere Liste.
Steuerung des eingebetteten INDI-Servers
Diese Befehle sind nur verfügbar, wenn der Server über eine eingebettete INDI-Instanz verfügt.
| Befehl | Beschreibung |
|---|---|
YA | Eingebetteten INDI-Server starten |
YZ | Eingebetteten INDI-Server stoppen |
YL | INDI-Treiber laden ({"YL": {"driver": "<binary>"}}) |
YR | INDI-Treiber neu laden ({"YR": {"driver": "<binary>"}}) |
YS | INDI-Treiber stoppen ({"YS": {"driver": "<binary>"}}) |
YC | Gerät eines INDI-Treibers verbinden ({"YC": {"driver": "<binary>"}}) |
YD | Gerät eines INDI-Treibers trennen ({"YD": {"driver": "<binary>"}}) |
YK | Gespeicherte INDI-Treiberkonfiguration laden ({"YK": {"name": "<Konfigurationsname>"}}) |
YA und YZ haben keinen Payload ({"YA": {}}). Bei YL, YR, YS, YC und YD ist driver das Feld binary eines Eintrags aus controllerdata.indidrivers. Bei YK ist name ein Schlüssel von controllerdata.indiconfigurations (siehe Controller) — es startet alle unter diesem Namen gespeicherten Treiber, unabhängig von CL/CS, die die aktiven Treiber stattdessen in eine Modulkonfiguration bündeln.
YL startet nur den Prozess des Treibers - das Gerät muss noch verbunden werden, bevor es nutzbar ist, mit YC (der INDI-Eigenschaft CONNECTION.CONNECT). Das ist bewusst ein separater Schritt: INDI selbst modelliert “Prozess läuft” und “Gerät verbunden” unabhängig voneinander. Wie YA/YZ haben YC/YD keine eigene Bestätigung (siehe Warnung oben): derzeit gibt es kein controllerdata-Feld, das meldet, ob das Gerät eines Treibers tatsächlich verbunden ist, nur ob dessen Prozess aktiv ist (indiactivedrivers).
Zusammenfassung der Zugriffskontrolle
| Grant-Wert des Servers | Erlaubte Befehle |
|---|---|
grant-client: "1" | Alle Befehle |
grant-client: "0" | Nur DU, LO, IL |
grant-client: "-1" | Nur LO (Authentifizierung erforderlich) |
Befehle, die ohne ausreichende Rechte gesendet werden, werden vom Server stillschweigend ignoriert.