Client → Server Messages

Message format

Every message is a JSON object with one top-level key identifying the command. The value is an object whose structure depends on the command.

All client command keys are UPPERCASE (e.g. DU, SV, SA, GC).


Command index

KeyDescription
DURequest full dump
XXHeartbeat / keepalive
LOLogin
ILSet language
SVSet single element value
SASet all element values
DPRequest property dump
I1Click property preicon1
I2Click property preicon2
I3Click property posticon1
I4Click property posticon2
J1Click element preicon
J2Click element posticon
PLLoad profile
PSSave profile
GCCreate grid line
GUUpdate grid line
GDDelete grid line
GFFetch/select grid line
GHMove grid line up
GBMove grid line down
GRReset (clear) grid
CLLoad a module configuration
CSSave the current configuration
FSSelect the watched file folder
MLLoad module
MKKill (unload) module
QYQuery a module (inter-module mechanism, not client-facing)
YAStart embedded INDI server
YZStop embedded INDI server
YLLoad INDI driver
YRReload INDI driver
YSStop INDI driver

All messages that target a module or property use the wrapper:

{
  "COMMAND": {
    "m": {
      "ModuleName": {
        "p": {
          "propertyName": { ... }
        }
      }
    }
  }
}

where m stands for modules and p stands for properties.


Session management

DU — Request full dump

Must be sent immediately after connecting. The server replies with a d message containing all modules.

{ "DU": { "language": "en" } }
FieldDescription
languagePreferred language for translated labels ("en", "fr", etc.)

XX — Heartbeat

Send every 30 seconds to keep the connection alive. The server replies with xx.

{ "XX": {} }

LO — Login

Required when the server has access control enabled (grant-client is "-1" in the dump response).

{
  "LO": {
    "user": "username",
    "pw": "password",
    "language": "en"
  }
}

IL — Set language

Changes the language for translated labels without re-connecting.

{ "IL": { "language": "fr" } }

Property value updates

SV — Set single element value

Use when directedit is true on the element — sends only one element’s value immediately. On a directedit: false element the backend rejects it (error in the module log, nothing applied): use SA instead.

{
  "SV": {
    "m": {
      "Focus": {
        "p": {
          "parameters": {
            "e": {
              "iterations": 7
            }
          }
        }
      }
    }
  }
}

e contains a single elementName: value pair.


SA — Set all element values

Use to submit all editable elements of a property at once (e.g. from a form). Also used when directedit is false.

{
  "SA": {
    "m": {
      "Focus": {
        "p": {
          "parameters": {
            "e": {
              "iterations": 7,
              "exposure": 3.5,
              "active": true
            }
          }
        }
      }
    }
  }
}
Always send every element

An SA must include every editable element of the property. If any is missing, the backend applies nothing at all — not even the elements that were present — and logs a warning server-side (visible to the client via an l message). This is not a runtime condition to handle gracefully: it signals a bug in the client, which must always submit the complete set of elements.


DP — Request property dump

Asks the server to resend the full state of one property, as if it had just been (re)loaded — useful to force-refresh a property a client suspects is out of sync, without reloading the whole module. The server replies with an ap message for that property alone.

{
  "DP": {
    "m": {
      "Focus": {
        "p": {
          "parameters": {}
        }
      }
    }
  }
}

No e field: DP targets the whole property, not a specific element.


Icon actions

preicon1/preicon2/posticon1/posticon2 (properties) and preicon/posticon (elements, see Data model) are not decorations: each is a clickable action. A non-empty value is the Google Fonts (Material Icons) icon name to render for it; clicking the rendered icon sends the matching command below with no value or parameter at all — an empty payload that simply targets the property (or element). What the action actually does server-side (saving a profile, loading a profile, starting a process…) is entirely up to the module; the client’s only job is to render the icon and forward the click as-is.

An empty string means there is no icon in that slot — nothing to render, and consequently nothing to click or send. Don’t render a placeholder or wire up a click handler for an empty icon field.

I1 — Click property preicon1

{
  "I1": {
    "m": {
      "Focus": {
        "p": {
          "saveprofile": {}
        }
      }
    }
  }
}

I2 — Click property preicon2

{
  "I2": {
    "m": {
      "Focus": {
        "p": {
          "loadprofile": {}
        }
      }
    }
  }
}

I3 — Click property posticon1

Same structure as I1 / I2.

I4 — Click property posticon2

Same structure as I1 / I2.


J1 — Click element preicon

{
  "J1": {
    "m": {
      "Focus": {
        "p": {
          "devices": {
            "e": {
              "myDevice": {}
            }
          }
        }
      }
    }
  }
}

J2 — Click element posticon

Same structure as J1.


Profile management

PL — Load profile

{
  "PL": {
    "m": {
      "Focus": {
        "profile": "myProfile"
      }
    }
  }
}

The server replies with an fl (profile loaded) message and then sends updated property values.

A transient fc precedes fl

The server sends an fc (unsaved changes) message immediately before fl, as an intermediate step of loading the profile, before the property’s values actually change. A client that reacts to fc by showing an “unsaved changes” indicator will see it flash on for one message and self-correct once fl arrives right after — expected, not a bug.


PS — Save profile

{
  "PS": {
    "m": {
      "Focus": {
        "profile": "myProfile"
      }
    }
  }
}

The server replies with an fs (profile saved) message.


Grid operations

Grid commands target a specific property within a module. All use the m / p wrapper.

GC — Create grid line

Adds a new row to the grid with the provided element values.

{
  "GC": {
    "m": {
      "Sequencer": {
        "p": {
          "sequence": {
            "e": {
              "target": "M31",
              "exposure": 120,
              "count": 10
            }
          }
        }
      }
    }
  }
}

The server replies with a gc message confirming the new row index.

Just apply each ea as it arrives — nothing to reason about

Creating a grid line applies the submitted values to the property’s base elements first, then appends the row and emits gc. This can take more than one ea message (one per changed base element, sent one after another — submitting target/exposure/count as above produces three separate ea messages before the final gc), but a client never needs to know or predict that: the server only ever sends exactly what’s needed to keep the client’s datastore correct, no more and no less (see Client Architecture Principles). Apply each message exactly as it comes, the same way you always do, and the result is correct regardless of how many messages there were.

The one visible side effect: a property showing both its base elements and its grid at the same time (showElts and showGrid both true — an uncommon combination) will flicker through these intermediate states before the new row appears. This is the real datastore state, not a bug, and it resolves itself — no need to filter it out or poll to work around it.


GU — Update grid line

Updates a specific row (zero-based index i) with new element values.

{
  "GU": {
    "m": {
      "Sequencer": {
        "p": {
          "sequence": {
            "i": 1,
            "e": {
              "target": "M42",
              "exposure": 60
            }
          }
        }
      }
    }
  }
}

GD — Delete grid line

Removes the row at index i (zero-based).

{
  "GD": {
    "m": {
      "Sequencer": {
        "p": {
          "sequence": {
            "i": 0
          }
        }
      }
    }
  }
}

GF — Fetch/select grid line

Requests the server to load a grid row into the property’s editable elements (used when clicking a row to edit it).

{
  "GF": {
    "m": {
      "Sequencer": {
        "p": {
          "sequence": {
            "i": 2
          }
        }
      }
    }
  }
}

GH — Move grid line up

Moves the row at index i up by one position.

{
  "GH": {
    "m": {
      "Sequencer": {
        "p": {
          "sequence": {
            "i": 3
          }
        }
      }
    }
  }
}

GB — Move grid line down

Moves the row at index i down by one position.

{
  "GB": {
    "m": {
      "Sequencer": {
        "p": {
          "sequence": {
            "i": 2
          }
        }
      }
    }
  }
}

GR — Reset grid

Clears the entire grid of a property (all rows removed) — not a single row like GD. The server broadcasts gr in response.

{
  "GR": {
    "m": {
      "Sequencer": {
        "p": {
          "sequence": {}
        }
      }
    }
  }
}

No i field: GR targets the whole grid, not one row.


Module lifecycle

CL — Load a configuration

Loads all the modules of a saved configuration (list in controllerdata.availableconfs, see Controller).

{ "CL": { "name": "modules" } }

CS — Save the current configuration

Saves the set of currently loaded modules under the given name (GlobalDatastore is never included).

{ "CS": { "name": "modules" } }

ML — Load module

Requests the server to dynamically load a module. These commands target the controller: the payload does not use the m/p envelope.

{ "ML": { "lib": "focus", "label": "MyFocuser", "profile": "default" } }
FieldDescription
libModule library without the libost prefix (the server adds it): focus for libostfocus. Available libraries are the keys of controllerdata.libraries
labelLabel of this instance; the module name is this label without spaces. If a module with that name is already loaded, the request is refused
profileProfile name to load (optional)
Don’t pass a controllerdata.libraries key as-is

The keys of controllerdata.libraries are already prefixed (libostfocus, libostdummy…). The server prepends libost again unconditionally, so forwarding a key straight through double-prefixes it and fails:

{ "ML": { "lib": "libostdummy", "label": "MyDummy" } }
Error loading library MyDummy - Cannot load library libostlibostdummy: cannot open shared object file: No such file or directory

Strip the libost prefix from the key first (same transform profiles’ keying convention relies on, see Controller data):

{ "ML": { "lib": "dummy", "label": "MyDummy" } }

MK — Kill (unload) module

Requests the server to unload a running module, identified by its name.

{ "MK": { "name": "MyFocuser" } }

GlobalDatastore cannot be unloaded: the request is refused (error message in the logs).


Module query

QY — Query a module

Inter-module communication, not a client feature

QY/qa exists primarily so one module can ask another module a question internally (a module calling Basemodule::otherModuleQuery(), which goes through this same mechanism). A client can technically send QY too — the envelope is identical — but there is no established client-facing use case for it today. It’s documented here for completeness, and because it will matter when documenting how to build a module, not as something a client should build a feature around.

Generic, per-module request/answer mechanism: ask a module a named question with parameters, get a named answer back via qa. Which query names a module actually understands (if any) is entirely up to that module — the protocol only defines the envelope. Note the payload has no p wrapper: QY targets the module itself, not one of its properties.

{
  "QY": {
    "m": {
      "Sequencer": {
        "query": "profileduration",
        "params": { "profile": "default" }
      }
    }
  }
}
Not much to see yet

Today only the Sequencer module implements a real answer (profileduration), used internally by the Planner module. No other shipped module responds meaningfully to any query name yet.


File system

FS — Select the watched folder

Selects the folder whose file list the server publishes (see File system). The path is relative to the webroot; the empty string designates the root.

{ "FS": { "folder": "/Sequencer/gam Cyg/LIGHT/Luminance" } }

The change is server-wide (all clients see the new list). The path is not validated: a non-existent folder yields an empty list.


Embedded INDI server control

These commands are only available when the server has an embedded INDI instance.

CommandDescription
YAStart the embedded INDI server
YZStop the embedded INDI server
YLLoad an INDI driver ({"YL": {"driver": "<binary>"}})
YRReload an INDI driver ({"YR": {"driver": "<binary>"}})
YSStop an INDI driver ({"YS": {"driver": "<binary>"}})

YA and YZ have no payload ({"YA": {}}). For YL, YR and YS, driver is the binary field of an entry of controllerdata.indidrivers.


Access control summary

Server grantAllowed commands
grant-client: "1"All commands
grant-client: "0"DU, LO, IL only
grant-client: "-1"LO only (login required)

Commands sent without sufficient grant are silently rejected by the server.