Remote control

SpecMan4EPR can be driven from any external program over a plain TCP/IP text connection. This is the recommended way to integrate SpecMan4EPR into an automation framework, a measurement robot, or a higher-level orchestration script. The interface is deterministic: every command produces a correlated, machine-parsable response, and long-running operations report both when they start and when they finish — without blocking the connection.

This interface received a major update in version 3.8.0.

1. Enabling remote control

In Options:

  • Select Network → Communication port (e.g. 8023)
  • Add the remote host IP to Network → List of allowed hosts (e.g. localhost)
  • Specify the buffer directory in Directories and files → Data buffer directory

    Use any TCP/IP terminal software. For Windows users the TCPCheck app is provided on request. The connection is line-oriented: terminate every command with LF (0x0A). CR (0x0D) and CRLF are also accepted.

    2. Command syntax

    Commands address a tree of objects, written as a dotted path that starts with the top-level object and drills down to a property or action:

    .<object>.<sub-object>.<property> [= value]

    Reading omits the assignment; writing supplies = value. String values are single-quoted. For each command the response carries the current value of the addressed property, named by its full path, e.g.

    .d.state %% read → .daemon.state=4 .s.echo = 0 %% write → .server.echo=0

    The top-level objects are:

  • .s — .server : connection, files, data transfer, JSON engine
  • .d — .daemon : execution control (run / tune / stop), progress
  • .e — .experiment : the loaded experiment, its axes and processing
  • .spec — .spectrometer : device configuration

    Append .help (or .h) to any object to list its commands, e.g. .s.help, .d.help, .e.help. Append it to a command instead to describe that one command, e.g. .spec.ADC.state.helpSTate (ro, int): device state: 0 disabled, 1 comError, .... Commands accept the usual abbreviation (the capitalised letters of the listed name): STate takes ST or STATE, so .spec.ADC.st and .spec.ADC.st.help work too. For a device, each configuration key carries its own text: .spec.ADC.cfg.<key>.help.

    Help and text replies (help, info, description, history) are returned on a single line: multi-line content is collapsed with the literal separator \n, which the client splits to display. When the content will not fit the single-quoted ='...' form (it contains a '), the reply is delivered instead as JSON on a .json leaf — a command listing becomes .<path>.help.json='[{"name":..,"access":..,"type":..,"help":..}, ...]', and free text becomes a JSON string on .<path>.info.json (any single quote inside is unicode-escaped, so the value never breaks the wire framing). Clients should parse the value as JSON whenever the leaf ends in .json.

    3. Request correlation with .id

    Any command may be prefixed with .id<number>. The response then echoes the same id, so an automation client can match replies to requests even when responses arrive out of order or asynchronously (see the deterministic protocol below). The id is reflected in upper case:

    .id55.d.state.ID55.daemon.state=4 .id7.d.error.ID7.daemon.error='...'

    Using a unique id per command is strongly recommended for unattended automation: it is the only reliable way to associate a delayed completion event (below) with the command that started it.

    4. The deterministic command interface

    Structural operations are issued through the exec entry point, which accepts a JSON payload. Because JSON uses double quotes, wrap the payload in single quotes:

    .s.exec = '{"cmd":"<name>", ...}'

    Every exec command issued with an .id prefix produces a correlated reply whose leaf token encodes the outcome:

  • ack — command received and started (long-running operations only; immediate)
  • nack — rejected: malformed, busy, or a missing/invalid file (immediate)
  • ok — completed successfully
  • error — failed during or after execution (delayed)

    Short commands resolve in one cycle and reply with ok or nack. Long-running commands (run, tune, batch) reply ack immediately, leaving the connection free, then later emit ok or error when the daemon settles. A rejected detail rides in the value:

    .id1.s.exec = '{"cmd":"loadTemplate","file":"\\Demo\\Demo 2\\test_WURST.tpl"}'.ID1.server.ok .id2.s.exec = '{"cmd":"loadTemplate","file":"missing.tpl"}'.ID2.server.nack='experiment template does not exist' .id3.s.exec = '{"cmd":"run"}'.ID3.server.ack %% accepted, executing ... %% connection stays free.ID3.server.ok %% daemon returned to idle

    Important — escape backslashes inside JSON. Backslash is the JSON escape character, so a Windows path must double each one ("\\Demo\\x.tpl") or use forward slashes ("/Demo/x.tpl"). A bare "\Demo" is invalid JSON (\D is not a valid escape) and the command is rejected with nack='JSON parse failed'. This applies only to the JSON interface; the plain .s.open='\Demo\x.tpl' form takes the path verbatim and needs no escaping.

    exec commands:

  • loadTemplate {"file":"<path>"[,"fld":<n>]} — load a template/experiment. Missing or unparseable files reply nack and put SpecMan into the Repair state. "fld" is an optional directory selector (default: template folder).
  • revertTemplate {} — reload the daemon's stored experiment copy together with its data.
  • saveTemplate {"file":"<name>"} — save the current experiment as a template.
  • setParArray {"par":"<name>","str":"1 MHz to 10 MHz"} — set a parameter from an ASCII formula; or {"par":"...","b64":"<base64 float array>"} to set it directly from a binary array.
  • getParArray {"par":"<name>"} — return a parameter's array as base64 (reply cmd "parArrayData").
  • run | tune | batch {} — start the corresponding long-running mode (ack → ok/error).
  • getError {} — return the cause of the last Repair transition in the reply value.
  • status {} — return a consolidated status snapshot as a single JSON object in the reply value (see below). One poll replaces separate state/error queries.

    The status reply is a JSON object covering every top-level subsystem, so a single poll answers "is it busy, is it valid, did anything fail":

    .id7.s.exec = '{"cmd":"status"}'
       →   .ID7.server.ok='{"cmd":"status","ok":true,
                  "spectrometer":{"state":"ready"},
                  "daemon":{"id":"...","class":"...","state":"idle","action":"idle","busy":false,"error":""},
                  "experiment":{"name":"test_WURST","valid":true,"error":""},
                  "saveQueue":{"busy":false},
                  "lastConsoleError":"",
                  "plugins":[{"id":"...","state":"idle","error":""}],
                  "errors":[]}'
    

    Top-level ok is false whenever any subsystem reports an error; errors is the flattened list of every non-empty error message across the spectrometer, daemon, experiment, console, and plugins — one place to look for a health check.

    Unlike most exec commands, status always answers even without an .id prefix: a bare .s.exec = '{"cmd":"status"}' returns the same object on an uncorrelated .server.status='{...}' leaf, so an interactive poll is never silently dropped. Network clients receive it as a tcpJSONCommand reply (cmd "status").

    5. The Repair state and error retrieval

    Errors are reported on two independent fields:

  • .d.error (reply leaf .daemon.error) — the daemon error: the cause of the last error ("Repair") transition. Set whenever an operation invalidates the experiment — a missing or unparseable template, a hardware fault during execution, a critical stop — which also drops the daemon into the error state (daemon.state=0).
  • .s.error (reply leaf .server.error) — the server error: the most recent console/parser error (e.g. a malformed command line). It is independent of the daemon state and is empty when the last command parsed cleanly.

    The daemon (Repair) cause can be retrieved at any time, by console or JSON:

    .id9.d.error %% console getter (daemon error).ID9.daemon.error='experiment template does not exist: \tpl\x.tpl' .id9.s.exec = '{"cmd":"getError"}' %% JSON equivalent.ID9.server.ok='experiment template does not exist: \tpl\x.tpl'

    A run that fails delivers the same reason in its delayed reply:

    .id3.s.exec = '{"cmd":"run"}'.ID3.server.ack.ID3.server.error='Loop service code: device timeout'

    The stored reason persists as the "last cause" until the next failure, so it can be read after the fact. To leave the Repair state, load a valid experiment or issue .d.idle.

    6. Command reference

    .server (.s)

  • .s.echo = 0|1 — mirror each received command line to the server log (debug; IN:/OUT: lines stay local and are never sent to the CLI).
  • .s.show = 0|1 — suppress / enable unrequested responses (alias of showlog).
  • .s.message = 'text' — post a message to the SpecMan log and connected clients.
  • .s.open = '<path>' — load a template (shorthand for the loadTemplate exec command).
  • .s.exec = '{...}' — the structural command interface (section 4).
  • .s.error / .s.err — read the last server-level error (the most recent console/parser error; empty when the last command parsed cleanly).
  • .s.opmode [= 'mode'] — read or set the spectrometer operational mode. .s.opmode.list returns the comma-separated list of available mode IDs.
  • .s.tpllib — return the template library as JSON.
  • .s.fileready — 0 = no file for current .d.fguid, 1 = ready, 2 = save in progress.
  • .s.fg = '<name>' / .s.fileget — fetch a buffer file (binary; see section 8).
  • .s.traceget — fetch the current trace (binary; see section 8).
  • .s.clrbuf — clear the data buffer directory.

    .daemon (.d)

  • .d.state — execution state (see codes below).
  • .d.error — read the cause of the last error ("Repair") transition (daemon error reason; same value as the JSON getError command).
  • .d.run / .d.tune / .d.pause / .d.stop (= .d.idle) — control execution.
  • .d.v / .d.version — daemon version.
  • .d.exptime — elapsed run time (seconds; 0 when not running).
  • .d.expdone — fraction of the experiment completed (0..1).
  • .d.fguid = '<id>' — tag the next acquired file so .s.fg can fetch it (one-shot: cleared after save).
  • .d.qs.<name> [= value] — quick-set variable.
  • .d.hwreset — clear the command counter.

    .experiment (.e)

  • .e.state — experiment validity (0 = error, 1 = OK).
  • .e.name — experiment name.
  • .e.filename / .e.fname — source file.
  • .e.datastate / .e.dstate — data status.
  • .e.eax.<axis>.<field> / .e.expaxes... — experiment axes (e.g. .e.eax.P.reps = 1000).
  • .e.ppars.<stage>.<parameter> / .e.procpars... — post-acquisition processing, the console equivalent of the Post Acquisition Processing dialog. One node per stage, listed in the order they are applied; .e.ppars.list lists the stages and .e.ppars.<stage>.list the parameters of one stage. Every stage node carries a .use switch (0/1) that enables the stage.
    • .e.ppars.baseline (bl) — .use, .alg (none, zero, first; .alg.list returns the names), .fp1 / .w1 / .fp2 / .w2 (reference regions, first point and width in points).
    • .e.ppars.fft.use, .zero (zero-filling factor), .fp (first point), .win (apodization window, 0..1).
    • .e.ppars.filter (filt) — .use, .lp (low-pass cut-off [Hz]), .hp (high-pass cut-off [Hz]).
    • .e.ppars.notch.use, .freq (centre frequency [Hz]), .q (quality factor).
    • .e.ppars.mixer (mix) — .use, .freq (demodulation frequency [Hz]), .bw (bandwidth [Hz]), .ref (0 = internal reference at .freq, 1 = the recorded channel 2 of the pair; .freq is then not used).
    • .e.ppars.phase.use and .value; the stage has a single parameter, so plain .e.ppars.phase is that value [deg].
    • .e.ppars.abs.use only (magnitude calculation has no parameters).
    • .e.ppars.int (integration) — .use, .alg (none, integral, pk2pk, rms; .alg.list returns the names; the three are one choice, and the signal axis becomes the integrated kind while any is on), .fp / .w (window, first point and width in points). .use = 1 keeps the reduction already chosen and falls back to integral; .use = 0 clears all three.
    • .e.ppars.mask — the raw processing flag word, for saving and restoring all switches at once.
    • .e.ppars.json — the whole processing chain as a JSON object, in one reply.

    Every numeric parameter above is a property subtree, so .e.ppars.mixer.freq and .e.ppars.mixer.freq.value are the same thing and .minvalue / .maxvalue / .increment are available as well. Note that switching the baseline stage on with .e.ppars.baseline.use = 1 selects the zero-order algorithm when none was chosen yet, because the stage is skipped unless .alg is zero or first.

    .spectrometer (.spec)

  • .spec.dev.list / .spec.list — list configured device names.
  • .spec.suspend = '<DEV1,DEV2,...>' — user-suspend a comma-separated list of devices. Suspended devices are excluded from the "spectrometer not ready" check. The reply names the devices acted on and, after a semicolon, any names that were not found, e.g. .spec.suspend='DEV1,DEV2 suspended; not found: DEVX'.
  • .spec.resume = '<DEV1,DEV2,...>' — resume (re-initialise) a comma-separated list of devices, the equivalent of the configuration dialog's Activate button.
  • .spec.suspend — with no value, read back the devices currently user-suspended.
  • .spec.exec = '{...}' — structural device operations (add/remove devices, etc.).
  • .spec.json — the whole spectrometer image as a JSON object, in one reply.
  • .spec.<device>.json — one device: properties, config keys, pulse outputs.

    Daemon state codes (.d.state):

    0  error (Repair)      3  idle (finished)     6  run        9  test
    1  error (terminating) 4  idle (default)      7  batch
    2  idle (terminating)  5  tune                8  pause
    

    States 0..3 are transient/error; SpecMan is "busy" in 5,6,7,8; ready in 4.

    7. Worked examples

    Comments below use the console's own syntax: %% to the end of the line, or %…% inline. They may be sent to the console as they are — the parser strips them (since 3.8.7). Requests are in bold.

    Run a template (classic console control)

    .s.echo = 0 %% disable echo .s.show = 0 %% disable unrequested responces .d.v %% get daemon version .s.message='SpecMan4EPR is under control' %% here we come .s.open='\Demo\Demo 2\test_WURST.tpl' %% load tpl script .e.eax.P.reps=1000 %% let experiment to run very long .e.state %% request experiment state 0 - error / 1 - OK .d.state %% request daemon state 4 - idle / 6 - run .d.run %% run .d.exptime %% request how long experiment is running .d.expdone %% request fraction of experiment done .d.state %% request daemon state 4 - idle / 6 - run .d.stop %% stop SpecMan4EPR .d.state %% request daemon state 4 - idle / 6 - run

    Run a template deterministically (recommended for automation)

    .s.echo = 0 .s.show = 0 .id1.s.exec='{"cmd":"loadTemplate","file":"\\Demo\\Demo 2\\test_WURST.tpl"}' %% wait for .ID1.server.ok ; .ID1.server.nack='...' means it failed .id2.s.exec='{"cmd":"run"}' %% .ID2.server.ack = started %% .ID2.server.ok = finished (then fetch data) %% .ID2.server.error = failed; read .d.error for the cause

    Run a template and get data

    .s.open='\Demo\Demo 2\test_WURST.tpl' %% load tpl script .d.fguid='123' %% name the experiment we want to acquire (one-shot: cleared after save) .d.run %% run .d.state %% wait until .daemon.state=4 .s.traceget %% get current trace .s.fileready %% 0 = no file for current .d.fguid, 1 = ready, 2 = save in progress %% poll until =1 before requesting the file .s.fg='123.exp' %% get file .s.fg='123.d01' %% get file

    Handle a failure / Repair state

    .id5.s.exec='{"cmd":"loadTemplate","file":"\\tpl\\does_not_exist.tpl"}'.ID5.server.nack='experiment template does not exist' .id6.d.error %% read why SpecMan is in Repair.ID6.daemon.error='experiment template does not exist: \tpl\does_not_exist.tpl' .id7.d.idle %% leave Repair (or load a valid template)

    8. Binary response formats

    Trace data (response to .s.traceget):

    .server.traceget=[i],[j],[k],[l]#[start, 4 char hex][length, 4 char hex][body binary data, length]

    File transfer (response to .s.fg / .s.fileget):

    .server.fileget=[file name]#[length, 8 char hex][body binary data, length]

    When no matching file exists the server replies .server.fileget=nullptr#00000000.