Skip to content

Project configuration

A libsig project is a set of JSON documents that describes which capsules exist, how they are configured, and how they are grouped and wired into services. sig_project_load(store, registry, out) reads it from a persistence store; nothing is started by loading. sighost uses the file backend, so the project is a directory:

my_project/
├── configurations/ one file per capsule configuration
│ ├── Sim.cfg
│ └── Count.cfg
└── services/ one file per service
└── test.cfg
  • Only files ending in .cfg are loaded; everything else is ignored. (In store terms: the configurations and services namespaces, one key per file, stored by the file backend at <base>/<namespace>/<key>.cfg.)
  • The file name stem is the name: configurations/Sim.cfg defines the capsule configuration Sim; services/test.cfg defines the service test.
  • A missing configurations/ or services/ directory counts as empty. An unreadable one is an error. sighost refuses to start if the project directory itself does not exist; if it is removed while the host runs, a runtime restart loads an empty project and the next save recreates the directories.
  • A name may be any 1 to 128 bytes without /, .. or control characters, so files such as My Svc.cfg load. Creating one through the API is stricter: names must match [A-Za-z0-9_-]{1,64}. Writes are atomic (temp file and rename).
  • Any invalid file fails the whole load; no partially built project is ever returned.

The runnable example is examples/basic_counter/; examples.md walks through it.

Projects can also be edited at runtime through the management API; the host rewrites these files in compact JSON, and a change takes effect when the affected service next starts.

Capsule configuration files (configurations/<Name>.cfg)

Section titled “Capsule configuration files (configurations/<Name>.cfg)”
{
"type": "simulator",
"attr_value": {
"start": 0,
"end": 9,
"step": 1,
"interval_ms": 250
}
}
Key Required Meaning
type yes Name of a capsule registered with the host (e.g. counter, simulator). An unregistered type is an error.
attr_value no Object of configuration properties handed to the capsule. Values must be scalars: integer, boolean, floating point or string. Arrays, objects and null anywhere inside it are rejected.

The set of properties a capsule understands is defined by the capsule itself. See Built-in capsules.

A configuration file is shared by the whole project: any service may list it, and each service that does gets one instance of it.

{
"auto_start": true,
"execution": [
{
"name": "Sim",
"receivers": {
"__default_terminal_value": [
{ "name": "Count", "input": "__default_terminal_value" }
]
}
},
{ "name": "Count", "receivers": {} }
]
}
Key Required Meaning
auto_start no (default false) Start this service automatically when the runtime starts. Otherwise start it with POST /services/{name}/start or sig_runtime_service_start().
execution yes Array of entries; each creates one capsule instance owned by this service.

Each execution entry:

Key Required Meaning
name yes Name of a capsule configuration (a configurations/<name>.cfg file). The instance takes the same name, so a configuration can appear at most once per service.
receivers no Object mapping output terminal name to an array of destinations.

Each destination is { "name": "<instance>", "input": "<input terminal>" }: signals emitted on that output terminal are delivered to that input terminal of the named instance in the same file. List several destinations to fan out. Receivers may refer to entries that appear later in the file. A destination naming an unknown instance is an error.

Every instance in a service runs on that service’s one worker thread; services are independent of each other.

Capsules communicate through named terminals. Most capsules use the single default terminal, whose name is __default_terminal_value (SIG_TERMINAL_DEFAULT in sig_capsule.h). A capsule’s available input and output terminals are defined by its capsule.

Compiled into sighost according to the SIG_CAPSULES CMake option. Each capsule declares a category (source, sink, flow or state: simulator and weather are sources, logger a sink, filter and switch flow, counter and state_change state) and its properties in a table, and the tables below are those declarations; GET /capsules returns them too. A value of the wrong type, out of range, or (for logger’s level) not one of the choices is an error that names the property.

Emits an integer sequence on its default output terminal on a fixed interval, wrapping back to start once the next value would pass end. With count above 1, each interval emits that many consecutive values, one after the other: start 0, end 9, count 2 sends 0 and 1, waits, then 2 and 3, and so on.

Property Required Default Meaning
start no 0 First value, and the value wrapped back to
end yes — Inclusive upper bound
step no 1 Increment; must be > 0
interval_ms yes — Milliseconds between emissions (≥ 1)
count no 1 Values emitted each interval (≥ 1)

Terminals: no inputs; one output (__default_terminal_value). A configuration missing end or interval_ms, with interval_ms, step or count below 1, or with a non-integer value is rejected on write and keeps the service from starting.

Counts signals arriving on its default input terminal and logs the count at INFO every interval, then resets it: counter Count: processed N signals in the past Mms. It also emits N (an int) on its default output terminal at each report, so the count can be wired to another capsule such as logger (see examples/basic_counter/).

Property Required Default Meaning
report_interval_ms no 1000 Milliseconds between reports (≥ 1)

Terminals: one input and one output (both __default_terminal_value).

Logs every signal arriving on its default input terminal, as compact JSON, at a configurable level: logger Log: <json>. The host’s global level (sighost -l) still applies, so a debug logger prints nothing under -l info.

Property Required Default Meaning
level no info One of trace, debug, info, warn, error, fatal (case-insensitive)

The service fails to start if level is not one of those names.

Terminals: one input (__default_terminal_value); no outputs.

Evaluates a condition on every signal arriving on its default input terminal and forwards the signal, unchanged, to the true or the false output terminal.

Property Required Default Meaning
condition yes — Expression on input, the signal (see below)

Terminals: one input (__default_terminal_value); two outputs, true and false. The service fails to start, with the position of the problem in the log, if condition is missing or does not parse.

The condition language (include/sig/value/sig_expr.h) reads into the signal, which is usually a map or list rather than a bare number:

input % 2 == 0
input.reading.temp > 21.5 && input.status != "off"
input.subvalue ~ "failure"
input.items[0] >= 10
Value input; integers, decimals, "strings" or 'strings' (escapes \\ \" \' \n \t), true, false
Reach .key, ["key"] into a map; [n] into a list
Arithmetic + - * / % and unary -; integers and decimals mix; % is integers only
Compare == != < <= > >= on numbers or strings (by bytes); ==/!= on booleans
Contains a ~ b: string a contains string b
Logic ! && ||, with ( ) to group

A condition that cannot be worked out for a particular signal (a missing key, a list index out of range, a string where a number is needed, division by zero) has no value, and the signal goes to false. false && x and true || x still decide the result when x has no value, so input.level > 3 || input.urgent works on signals that lack one of the keys. There is no implicit conversion: input.n == "5" has no value when n is the integer 5.

Passes a stream through or blocks it, like a light switch. Readings on the state input are run through condition; while the latest result is true the stream on the default input is forwarded, unchanged, to the default output, and while it is false the stream is dropped.

Property Required Default Meaning
condition yes — Expression on input, a state reading (same language as filter)
default_state no false Whether the stream flows before the first state reading

Terminals: two inputs, __default_terminal_value (the stream) and state; one output, __default_terminal_value. A reading condition cannot evaluate leaves the switch as it was. The service fails to start if condition is missing or does not parse.

Evaluates condition on every reading arriving on its default input terminal and emits only when the result differs from the previous one. The emitted value is a map, {"state": <new result>, "prev_state": <old result>}, both booleans. The first reading always emits, since there is no earlier result to compare with; its map has state only, because a value has no null.

Property Required Default Meaning
condition yes — Expression on input, the reading (same language as filter), e.g. input > 5

Terminals: one input and one output, both __default_terminal_value. A reading condition cannot evaluate changes nothing and emits nothing. The service fails to start if condition is missing or does not parse.

Fetches the current temperature for a location and emits it on its default output terminal. Not built by default: add weather to SIG_CAPSULES and configure with -DSIG_HTTP_CLIENT=ON (it needs libcurl). Without SIG_HTTP_CLIENT the capsule is skipped with a configure warning. See examples/weather/.

Property Required Default Meaning
latitude yes — Degrees, -90 to 90 (double)
longitude yes — Degrees, -180 to 180 (double)
refresh_s no 600 Seconds between fetches (≥ 60)
provider no open-meteo Weather data source; one of the choices (currently only open-meteo)

Terminals: no inputs; one output (__default_terminal_value). Each fetch emits a map { "temperature": <double, degrees Celsius> }. The first fetch happens as soon as the service starts, then every refresh_s. Fetching is asynchronous (sig_http_get_async), so a slow network never blocks the service, threaded or single-threaded; each HTTP request times out after 5 s, and at most one is in flight per instance (a tick that finds the previous one still out is skipped). A failed fetch logs a warning and emits nothing; the next fetch is tried on schedule. latitude and longitude are range-checked when the service starts. Stopping the service does not wait for a request in flight: its result is dropped when it arrives. The 5 s bound assumes a libcurl built with the threaded or c-ares resolver: with the synchronous resolver, DNS lookups are not covered by the timeout (signals are disabled via CURLOPT_NOSIGNAL).

For writing your own, see writing-capsules.md.

Problem Result
Not valid JSON, or root not an object load fails (SIG_ERR_PARSE)
Capsule: missing/non-string type, unregistered type, non-object attr_value, non-scalar property load fails
Service: missing/non-array execution, non-boolean auto_start, entry without name, unknown configuration load fails
Receiver: not an object, missing name/input, unknown instance name, receivers not an object/array load fails
Property errors against the capsule’s table (missing required, wrong type, out of range, bad choice) the service fails to start, not to load; PUT/PATCH of the configuration through the API is rejected with the reason
A property the capsule does not declare a warning at start; rejected by PUT/PATCH

Errors are logged with the offending file path. Terminal names in receivers are not checked against the capsule’s declared terminals at load time.