Events and logs
The server pushes events as things happen: capture state, progress, the decoder list, annotations. The SDKs handle them for you, so most scripts never touch this API. Use it when you want to react to an event rather than wait for it.
Reached through workspace.events.
Events are replayed when you attach, so a client that connects late still sees the current state.
subscribe​
Calls your listener for every event, or for the kinds you name. Returns a function that unsubscribes.
- Python
- NodeJS
- Rust
- C
def on_event(message):
print(message["type"], message)
stop = workspace.events.subscribe(on_event, only=["state", "progress"])
# ...
stop()
// The second argument filters by event kind; omit it for everything.
const stop = workspace.events.subscribe(
(message) => console.log(message.type, message),
['state', 'progress'],
);
// ...
stop(); // calling the returned function unsubscribes
// Rust hands you the channel rather than taking a callback.
let mut events = workspace.events().subscribe(); // broadcast::Receiver
while let Ok(event) = events.recv().await {
println!("{}", event["type"]); // filter on the kind yourself
}
static void on_event(const char *json, void *user) {
/* Keep it quick: this runs on a thread the library owns. */
printf("%s\n", json);
}
/* NULL kinds means every event. */
ss_subscription *sub =
ss_events_subscribe(workspace, "[\"state\", \"decode_progress\"]",
on_event, NULL);
/* ... */
ss_events_unsubscribe(sub); /* before the workspace handle is freed */
The callback runs on a thread the library owns. Guard whatever it touches, and
never call ss_events_unsubscribe from inside it, since that would join the
thread it is running on. ss_last_error() is thread-local, so a failure inside
the callback is not visible from your own thread (see
The C library).
ss_event_latest and ss_event_wait need no callback at all.
In Python the listener runs on the connection's reader thread: blocking it stalls every other message on that connection. In NodeJS everything runs on one thread, so long synchronous work in a listener delays every other callback and promise. In both cases, queue the work and return.
A listener that raises is logged (to the ikalogic_scanastudio logger in
Python, to console.error in NodeJS) and delivery to the other listeners goes
on.
A Rust receiver that falls behind is told it lagged and skips what it missed.
latest() always holds the last event of each kind, so a slow reader loses
intermediate events but not the current state.
The answer to a request this connection made, such as measure_added or
pattern_duration_result, goes to the call that is waiting for it and is not
passed to listeners.
latest​
The most recent event of one kind, without waiting. The SDK keeps the last of each, so this is how you read state the server pushed before you asked.
- Python
- NodeJS
- Rust
- C
config = workspace.events.latest("capture_config") # dict, or None
devices = workspace.events.latest("hw_devices")
// The SDK keeps the last event of each kind, so this needs no waiting.
const config = workspace.events.latest('capture_config');
// The SDK keeps the last event of each kind, so this needs no waiting.
let config = workspace.events().latest("capture_config"); // typed: Option<ServerMsg>
let raw = workspace.events().latest_raw("capture_config"); // raw JSON: Option<Value>
/* The library keeps the last event of each kind, so this needs no waiting. */
char *config = ss_event_latest(workspace, "capture_config"); /* NULL if none */
if (config) { printf("%s\n", config); ss_string_free(config); }
wait_for​
Blocks until the last event of a kind satisfies a predicate, or until one
arrives when you give no predicate. This is the building block behind
capture.wait() and decoders.wait(), and what you want for a condition they
do not cover.
wait_for checks the cached last event first. If an event received earlier
already matches, it returns at once without waiting for a new one. To wait for
something that has not happened yet, write the predicate so that the current
value does not satisfy it, for example a count above the one you read with
latest().
- Python
- NodeJS
- Rust
- C
# Wait until the trigger has fired at least 10 times.
event = workspace.events.wait_for(
"trigger_count",
predicate=lambda message: message.get("count", 0) >= 10,
timeout=60.0,
)
// Wait until the trigger has fired at least 10 times.
const event = await workspace.events.wait_for('trigger_count', {
predicate: (message) => (message.count ?? 0) >= 10,
timeout_ms: 60_000,
});
// Wait until the trigger has fired at least 10 times.
let event = workspace.events()
.wait_for("trigger_count",
|m| m["count"].as_u64().unwrap_or(0) >= 10,
Duration::from_secs(60))
.await?;
/* Returns at once if one already arrived, so waiting for "state" on an
idle workspace does not hang. There is no predicate in C: read the
JSON and wait again if it is not the one you wanted. */
char *event = ss_event_wait(workspace, "trigger_count", 60.0);
if (event) { printf("%s\n", event); ss_string_free(event); }
latest() or wait_for()capture_refused, log and log_history are not kept in the cache. The SDK
turns a refusal into a Refused error from capture.wait() and run(), and
folds the log lines into events.log. latest("capture_refused") is therefore
always empty and wait_for("capture_refused") always times out. To react to
them as events, use subscribe.
Errors: Timeout if nothing matching arrives in time. ConnectionClosed
(Error::Closed in Rust, NULL with ss_last_error() in C) if the link drops
while waiting: every pending wait fails at once.
Useful events​
The events a script usually cares about:
| Event | Fires when |
|---|---|
state | The capture changes state. See Capture. |
progress | Samples come in. |
pretrig_progress | The pre-trigger buffer is filling. |
trigger_count | The trigger count changed: it fired again in normal mode, or went back to 0 at the start of a capture. Sent only on change; the value at attach time is capture_config.trigger_count. |
capture_config | A capture configuration was applied: the settled rate, trigger, thresholds and the trigger count. |
capture_refused | The server refused to start a capture. reason is a sentence to show as is; subject says what it is about: device, trigger, probes, clock or pattern_generator. Reaches listeners only, see above. |
decode_progress | A decoder advanced. Throttled, but always sent at the end. Carries progress, items, done, and the totals hex_bytes, packets and mem_bytes produced so far. |
decoders | The decoder list changed. |
annotations | Markers or measurements changed, including recomputed results. |
hw_devices | A device was plugged or unplugged. The complete list, every time. |
device_attached | A device was attached to a workspace opened from a file. Carries device, device_name and the new channels. |
io_progress, io_done | A save, load or export is running or has finished. io_done carries op, path and error when it failed. Every client receives it; the connection that asked also gets a copy with its request id. |
busy | A command was refused because an export holds the workspace. reason is a sentence; id is the refused request's. The SDK turns it into a Busy error for that call. |
file_state | The workspace's path or dirty flag changed. |
ui_state | The stored view state, sent on attach and when a file is opened. |
pattern_generator | The pattern generator changed: the script attached, generate_on_trigger, and generating, which stays true while the device may still be generating. |
pattern_preview | A preview started, finished or failed. logs holds the script's messages. |
generation | The generation rows or their builders changed: the layout (samples, sample_rate), one entry per channel, and the builders. |
add_builder_result | The answer to generation.add_builder() or update_builder(): the builder attached, or error when its configuration was refused. The call returns it. |
pattern_duration_result | The answer to pattern.duration(): seconds, endless, over_limit. The call returns it. |
script_file_access_request | A script without file system access tried to open a path of its own. Carries file_name and path. |
error | A command was refused. code is stable and meant to be matched on. |
log | The server logged a line. Reaches listeners only. |
log_history | The whole log, sent on attach. It replaces events.log. Reaches listeners only. |
log​
The server's log for this workspace. The full history is replayed on attach, so you get the lines from before you connected too.
- Python
- NodeJS
- Rust
- C
for line in workspace.events.log:
print(line)
// The full history is replayed on attach, so this includes lines from
// before this client connected.
for (const line of workspace.events.log) {
console.log(line);
}
// The full history is replayed on attach, so this includes lines from
// before this client connected.
for line in workspace.events().log() {
println!("{line}");
}
/* A JSON array of lines, already timestamped by the server. The full
history is replayed on attach, so this includes lines from before
this client connected. */
char *log = ss_events_log(workspace);
if (log) { printf("%s\n", log); ss_string_free(log); }
When a decoder or generator script misbehaves, the reason is usually in the
server log rather than in the exception. ScriptError carries the relevant
lines with it; the log has the rest.