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
.cfgare loaded; everything else is ignored. (In store terms: theconfigurationsandservicesnamespaces, one key per file, stored by the file backend at<base>/<namespace>/<key>.cfg.) - The file name stem is the name:
configurations/Sim.cfgdefines the capsule configurationSim;services/test.cfgdefines the servicetest. - A missing
configurations/orservices/directory counts as empty. An unreadable one is an error.sighostrefuses 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 asMy Svc.cfgload. 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.
Service files (services/<name>.cfg)
Section titled “Service files (services/<name>.cfg)”{ "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.
Terminals
Section titled “Terminals”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.
Built-in capsules
Section titled “Built-in capsules”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.
simulator
Section titled “simulator”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.
counter
Section titled “counter”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).
logger
Section titled “logger”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.
filter
Section titled “filter”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 == 0input.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.
switch
Section titled “switch”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.
state_change
Section titled “state_change”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.
weather
Section titled “weather”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.
Validation summary
Section titled “Validation summary”| 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.