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
| Key | Description |
|---|---|
DU | Request full dump |
XX | Heartbeat / keepalive |
LO | Login |
IL | Set language |
SV | Set single element value |
SA | Set all element values |
DP | Request property dump |
I1 | Click property preicon1 |
I2 | Click property preicon2 |
I3 | Click property posticon1 |
I4 | Click property posticon2 |
J1 | Click element preicon |
J2 | Click element posticon |
PL | Load profile |
PS | Save profile |
GC | Create grid line |
GU | Update grid line |
GD | Delete grid line |
GF | Fetch/select grid line |
GH | Move grid line up |
GB | Move grid line down |
GR | Reset (clear) grid |
CL | Load a module configuration |
CS | Save the current configuration |
FS | Select the watched file folder |
ML | Load module |
MK | Kill (unload) module |
QY | Query a module (inter-module mechanism, not client-facing) |
YA | Start embedded INDI server |
YZ | Stop embedded INDI server |
YL | Load INDI driver |
YR | Reload INDI driver |
YS | Stop 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" } }| Field | Description |
|---|---|
language | Preferred 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
}
}
}
}
}
}
}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.
fc precedes flThe 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.
ea as it arrives — nothing to reason aboutCreating 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" } }| Field | Description |
|---|---|
lib | Module library without the libost prefix (the server adds it): focus for libostfocus. Available libraries are the keys of controllerdata.libraries |
label | Label of this instance; the module name is this label without spaces. If a module with that name is already loaded, the request is refused |
profile | Profile name to load (optional) |
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 directoryStrip 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
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" }
}
}
}
}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.
| Command | Description |
|---|---|
YA | Start the embedded INDI server |
YZ | Stop the embedded INDI server |
YL | Load an INDI driver ({"YL": {"driver": "<binary>"}}) |
YR | Reload an INDI driver ({"YR": {"driver": "<binary>"}}) |
YS | Stop 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 grant | Allowed 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.