Connecting and the server
The entry point of every SDK. You connect to a server, look at what it offers, and open or join a workspace.
Method and field names are snake_case in every SDK, NodeJS included, so the
four tabs read the same. C returns structured data as JSON strings with the same
field names.
Every NodeJS timeout is named timeout_ms and counted in milliseconds,
where Python and Rust take seconds.
connect​
Opens the connection and checks the protocol version. The SDK refuses a server it cannot speak to.
- Python
- NodeJS
- Rust
- C
from ikalogic_scanastudio import ScanaStudio
# Positional url, keyword-only options.
server = ScanaStudio.connect(
url="ws://127.0.0.1:4911",
timeout=30.0, # seconds to wait for any single answer
check_version=True,
)
# ScanaStudio is a context manager, and this is the idiomatic form.
with ScanaStudio.connect() as server:
...
import { ScanaStudio } from '@ikalogic/scanastudio';
const server = await ScanaStudio.connect('ws://127.0.0.1:4911', {
timeout_ms: 30_000, // milliseconds, unlike Python's seconds
check_version: true,
});
// There is no context manager: try/finally is the idiomatic form.
try {
// ...
} finally {
server.close();
}
use scanastudio_client::{ScanaStudio, DEFAULT_URL};
use std::time::Duration;
let server = ScanaStudio::connect(DEFAULT_URL).await?;
// Or with the two knobs spelled out: timeout, and the version check.
let server = ScanaStudio::connect_with(DEFAULT_URL, Duration::from_secs(30), true).await?;
#include "scanastudio.h"
/* NULL means ws://127.0.0.1:4911 */
ss_server *server = ss_server_connect(NULL);
if (!server) {
fprintf(stderr, "connect: %s\n", ss_last_error());
return 1;
}
/* ... */
ss_server_disconnect(server);
Parameters:
url— where the server listens. Defaults tows://127.0.0.1:4911.timeout/timeout_ms— how long to wait for any single answer. Default 30 s.check_version— refuse a server speaking a protocol this SDK does not. Leave it on.
Returns: a connected client.
Errors: VersionMismatch (Error::Version in Rust) when the server speaks
another protocol; a connection error when nothing is listening. In Rust, nothing
listening is Error::Socket, and a server that does not greet the client in
time is Error::Timeout. In C, NULL, with the reason in ss_last_error().
Closing a client leaves every workspace on the server running. Only
workspace.close() ends a session. Rust has no close(): the connection ends
once the ScanaStudio value and every Workspace obtained from it have been
dropped.
NodeJS also offers ScanaStudio.connect_with(Impl, url, options), which takes
the WebSocket class to use as its first argument. Use it in an environment where
the SDK cannot find one by itself, or to pass a WebSocket implementation of your
own. connect() uses the runtime's built-in WebSocket when there is one (Node.js
22 and later, Bun, browsers) and falls back to the optional ws package.
info​
Protocol version, product version, build commit and build date. Available as
soon as connect returns.
- Python
- NodeJS
- Rust
- C
print(server.info)
# ScanaStudio 6.0.23 (cd798ee), protocol 8
server.info.protocol # 8
server.info.version # "6.0.23"
server.info.commit # "cd798ee"
server.info.date # build date
// Already read by the time connect() resolved.
server.info.protocol; // 8
server.info.version; // "6.0.23"
server.info.commit; // the commit the server was built from
server.info.date;
// Already read by the time connect() returned.
let info = server.info();
println!("protocol {} version {}", info.protocol, info.version);
info.script_api; // 3: the ScanaStudio.* script API the server implements
info.sdk; // "0.2.10": the SDK version the server was built with
/* JSON: {"protocol":8,"version":"6.0.23","commit":"...","date":"...",
"script_api":3,"sdk":"0.2.10"} */
char *info = ss_server_info(server);
printf("server %s\n", info);
ss_string_free(info);
uint32_t supported = ss_protocol_version(); /* what this library speaks */
Example output. Python's ServerInfo has a readable __str__; the other
SDKs hand you the fields:
ScanaStudio 6.0.23 (cd798ee), protocol 8
In NodeJS the object itself prints as:
{ protocol: 8, version: "6.0.23", commit: "cd798ee", date: "2026-10-15" }
and in C it arrives as JSON:
{"commit":"cd798ee","date":"2026-10-15","protocol":8,"script_api":3,"sdk":"0.2.10","version":"6.0.23"}
Rust's ServerInfo and the C JSON also carry script_api, the version of the
ScanaStudio.* API the server's script engine implements, and sdk, the SDK
version the server was built alongside. That is the SDK version to install.
devices​
Every device a workspace can be opened on: connected hardware and the demo devices. The list is pushed again on every hotplug, so it is always current.
- Python
- NodeJS
- Rust
- C
for device in server.devices():
device.key # what create() takes: "se254" or "hw:1004250000604"
device.name # "SE254 (demo)"
device.model
device.serial # None for a demo device
device.demo # bool
device.probes # probe model per port, e.g. ["P992", None]
device.booting # True while the device has not finished starting
// key is the string create() takes.
for (const device of await server.devices()) {
device.key; // "se254" or "hw:1004250000604"
device.name;
device.model;
device.serial; // null for a demo device
device.demo;
device.probes;
}
// device.key is the string create() takes.
for device in server.devices().await? {
println!(" {} {}", device.key, device.name);
}
/* JSON array: [{"key":"se254","name":"SE254 (demo)","model":...,"serial":null,"demo":true}, ...] */
char *devices = ss_server_devices(server);
printf("%s\n", devices);
ss_string_free(devices);
Returns: the device list. key is the string create() takes. (open()
takes the path of a .scana file, not a device.)
Fields:
key:hw:<serial>for hardware, for examplehw:1004250000604, or the demo's own name, such asse254.name: the device's description, or"SE254 (demo)"for a demo device.probes: one entry per port, the probe model the port reports (P990,P991,P992,UNKNOWN) or none. The list is empty on families whose probes are fixed, which is not the same as no probe.booting(Python only): the device answers but has not finished starting. A capture on it is refused until it has.
Example output. A server with no hardware plugged in still offers the demo devices, printed as key and name:
se254 SE254 (demo)
sq25 SQ25 (demo)
sq50 SQ50 (demo)
sq100 SQ100 (demo)
sq200 SQ200 (demo)
sp209 SP209 (demo)
sp209i SP209I (demo)
sp259 SP259 (demo)
sp259i SP259I (demo)
sp1018 SP1018 (demo)
sp1036 SP1036 (demo)
sp1054 SP1054 (demo)
The C tab returns the same list as JSON:
[{"demo":true,"key":"se254","model":"se254","name":"SE254 (demo)","serial":null},
{"demo":true,"key":"sp209","model":"sp209","name":"SP209 (demo)","serial":null}, ...]
Real hardware appears with "demo": false, a serial number, and a key of the
form hw:<serial>.
capabilities​
What a device can do, available before you create a workspace on it. Use it to build a capture request the device will accept.
- Python
- NodeJS
- Rust
- C
caps = server.capabilities("sp259")
caps.sample_rates # [250000000, 125000000, 25000000, ...] descending
caps.sample_depths # acquisition depths, in samples
caps.logic_levels # discrete thresholds (V); empty if custom_threshold applies
caps.custom_threshold # a min/max/step range, or None
caps.logic_level_groups # ['CH1 to CH3', 'CH4 to CH6', ...]
caps.probes # physical probes in channel order; empty if not block-probed
caps.state_clock_channels # channels the external clock can be read on; empty = no state mode
caps.channel_pulls # switchable pull-up/pull-down
caps.pattern_generator # a pattern generator script can drive the channels
caps.signal_generator # the channels can replay generation rows (SQ family)
caps.channel_io # what a channel's direction can be set to, or None
caps.differential_pairs # channel pairs readable as one differential input
caps.per_channel_threshold # one threshold per channel through channel_thresholds (P992)
caps.pre_trig_limit # what it can hold as pre-trigger, in SAMPLES, or None
caps.trigger_engines # 2 gives A, B and a sequence; 1 gives a single event
caps.trig_timing # the trigger timer: reachable precision on widths
caps.can_ext_trig_in, caps.can_ext_trig_out
caps.can_ext_trig_test # the TRIG OUT -> TRIG IN loopback test is accepted
caps.default_state_edge # "rising", "falling" or "dual"
caps.industrial_inputs # empty on a non-industrial version
caps.error # None, or why the device is unknown
const caps = await server.capabilities('sp259');
caps.sample_rates; // descending
caps.sample_depths;
caps.logic_levels; // empty if custom_threshold applies
caps.custom_threshold;
caps.logic_level_groups;
caps.probes;
caps.state_clock_channels; // empty means no state mode
caps.channel_pulls;
caps.pattern_generator; // a pattern generator script can drive the channels
caps.signal_generator; // the channels can replay generation rows (SQ family)
caps.channel_io;
caps.differential_pairs;
caps.per_channel_threshold;
caps.pre_trig_limit;
caps.trigger_engines;
caps.trig_timing;
caps.can_ext_trig_in; caps.can_ext_trig_out; caps.can_ext_trig_test;
caps.default_state_edge;
caps.industrial_inputs;
caps.error; // set when the device is unknown
// Rust hands back the ServerMsg; match on it or read the fields you need.
let caps = server.capabilities("sp259").await?;
println!("{caps:?}");
/* JSON, and it needs no workspace. */
char *caps = ss_server_capabilities(server, "sp259");
if (caps) { printf("%s\n", caps); ss_string_free(caps); }
Parameters: device — the same vocabulary create() takes: a demo key such
as se254, or hw:<serial>.
Fields:
| Field | Meaning |
|---|---|
sample_rates | Timing-mode rates in Hz, descending. |
sample_depths | Acquisition depths offered, in samples. |
logic_levels | Discrete logic levels in volts; empty when custom_threshold applies. |
custom_threshold | A min/max/step range with presets, for an adjustable threshold. |
logic_level_groups | The channel groups that share one logic level. |
per_channel_threshold | A probe with one comparator per channel (P992) takes one threshold per channel through channel_thresholds. False while the device firmware cannot read them. |
probes | Physical probes in channel order; empty when the device is not block-probed. |
state_clock_channels | Channels the state-mode clock can be read on. Empty means no state mode. |
default_state_edge | rising, falling or dual. |
channel_pulls | Switchable pull-up or pull-down on every channel. |
channel_io | What a channel's direction can be set to (open_drain allowed, pulls, input impedance, series resistor), or none on a family whose channels only read. |
differential_pairs | Pairs of channels (first, second) that can be read as one differential input. |
industrial_inputs | Industrial bus receivers (channel, bus, termination); empty on a non-industrial version. |
pattern_generator | A pattern generator script can drive the channels (SP1000G). |
signal_generator | The channels can replay the generation rows, through the capture's operation (SQ family). |
trigger_engines | 2 gives events A and B plus a sequence; 1 gives a single event. |
trig_timing | The trigger timer (base_clock_hz, delta_time_bits, clk_div_bits, min_clock_hz, max_steps): what precision a pulse width or step time can reach. |
pre_trig_limit | How much pre-trigger the device can hold, in samples (kind is buffer or ring), or none when unknown. |
can_ext_trig_in, can_ext_trig_out | The external trigger input and output exist. |
can_ext_trig_test | The capture's ext_trig_test is accepted. |
error | Set when the server does not know the device. |
Errors: none for an unknown device. The answer comes back with error
set, for example unknown device: nope, and the other fields are then
meaningless. Check error before using the rest.
The capabilities come from the server's own tables, so a device that is not plugged in is still described, and the call never fails because somebody else is capturing on that device.
Example output. Four devices:
--- se254 ---
sample_rates: [250000000, 125000000, 25000000, 12500000, 6250000, 2500000] (6)
logic_levels: [1.8, 2.5, 3.3, 5.0]
logic_level_groups: ['CH1 to CH4']
state_clock_channels: [] channel_pulls: False pattern_generator: False
trigger_engines: 2 ext_in: False ext_out: False
--- sq25 ---
sample_rates: [25000000, 10000000, 5000000, 1000000, 500000, 250000, ...] (9)
logic_levels: [3.3]
logic_level_groups: ['CH1 to CH4']
state_clock_channels: [] channel_pulls: False signal_generator: True
channel_io: open_drain=False pulls=True input_impedance_ohm=100000 series_ohm=270
trigger_engines: 1 ext_in: False ext_out: False
--- sp259 ---
sample_rates: [250000000, 125000000, 25000000, 12500000, 6250000, 2500000] (6)
logic_level_groups: ['CH1 to CH3', 'CH4 to CH6', 'CH7 to CH9']
state_clock_channels: [8] channel_pulls: False pattern_generator: False
trigger_engines: 2 ext_in: True ext_out: True
--- sp1018 ---
sample_rates: [1000000000, 500000000, 250000000, 125000000, 50000000, ...] (8)
logic_levels: [] custom_threshold: min=0.0 max=3.0 step=0.025
probes: ['Probe A', 'Probe B']
state_clock_channels: [8, 17] channel_pulls: True pattern_generator: True
trigger_engines: 2 ext_in: True ext_out: True
Things to note: only the SP1018G reports pattern_generator: True, and only
the SQ25 reports signal_generator: True with a channel_io description; the
SE254 lists no state_clock_channels, so it has no state mode; and the SP1018G
has no discrete logic_levels because it takes a custom_threshold anywhere
from 0 to 3 V instead.
sample_rates lists what the device offers. A capture asking for anything else
is rounded down to a member of it rather than refused. See
Choosing a sample rate.
create​
Creates a workspace on a device and attaches this connection to it.
- Python
- NodeJS
- Rust
- C
workspace = server.create("se254") # a demo device
workspace = server.create("hw:1004250000604") # real hardware
workspace = server.create("se254", timeout=60.0)
const workspace = await server.create('se254'); // a demo device
const workspace = await server.create('hw:1004250000604', 60_000); // real hardware
// "se254" is a demo device; "hw:<serial>" is real hardware.
// Rust waits up to 60 s for the workspace; there is no timeout argument.
let workspace = server.create("se254").await?;
/* "se254" is a demo device; "hw:<serial>" is real hardware. */
ss_workspace *workspace = ss_server_create(server, "se254");
if (!workspace) { fprintf(stderr, "open: %s\n", ss_last_error()); }
printf("workspace %llu\n", (unsigned long long)ss_workspace_id(workspace));
Parameters:
device— a key fromdevices(): a demo key such asse254, orhw:<serial>.timeout/timeout_ms— how long to wait for the workspace. Default 60 s.
Returns: a Workspace.
Errors: a device key the server does not know is not reported as such: the
call waits and ends in Timeout (Error::Timeout in Rust; NULL in C) after
the timeout, 60 s by default. Take the key from devices().
create(), open() and attach() all move the connection to the new
workspace. The previous workspace keeps running on the server.
An older Workspace object keeps its own id, so its close() still closes the
right workspace. Its sub-APIs (capture, data, decoders and the others) do
not: they act on the workspace the connection is now attached to, and they read
a state cache shared with it, which can be stale. Use one connection per
workspace to drive two at once.
workspaces​
Lists every workspace the server holds, including ones this client never created and ones no client is watching. This is the discovery call that makes detaching and rejoining possible.
- Python
- NodeJS
- Rust
- C
for session in server.workspaces():
session.workspace_id # what attach() takes
session.device_name
session.state # "sampling", "done", ...
session.last_sample
session.sample_rate
session.decoders # how many decoders are attached
session.clients # how many clients are watching (informational)
session.capturing # an acquisition is running
session.device # the device key, reusable with create()
session.requested_samples
session.file_path # the .scana it was opened from or saved to, or None
for (const session of await server.workspaces()) {
session.workspace_id; // what attach() takes
session.device; // the device key, reusable with create()
session.device_name;
session.state;
session.last_sample;
session.clients;
session.capturing;
session.file_path; // the .scana it was opened from or saved to, if any
}
// workspace_id is what attach() takes.
for session in server.workspaces().await? {
println!("{} on {} — {}", session.workspace_id, session.device_name, session.state);
}
char *sessions = ss_server_workspaces(server); /* JSON array */
printf("%s\n", sessions);
ss_string_free(sessions);
clients is informationalIt says how many connections are watching a workspace, and nothing more. The server never closes a workspace because nobody is watching it: a session with zero clients keeps capturing.
Example output. Five sessions on one server, none of them being watched.
0 client(s) is normal:
192139071722857 SE254 (demo) done 1000000 samples 0 client(s)
192139071722858 SE254 (demo) done 1000000 samples 0 client(s)
192139071722860 SE254 (demo) done 1000000 samples 0 client(s)
192139071722862 SE254 (demo) done 1000000 samples 0 client(s)
192139071722863 SE254 (demo) sampling 1225000000 samples 0 client(s)
attach​
Joins a workspace already running on the server, by its id. The server replays its full state, so you see the capture configuration, the decoders, the markers and the log as if you had been there all along.
- Python
- NodeJS
- Rust
- C
sessions = server.workspaces()
workspace = server.attach(sessions[0].workspace_id)
// Pick the session you want; the server replays its full state on attach.
const sessions = await server.workspaces();
const workspace = await server.attach(sessions[0].workspace_id);
// Pick the session you want; the server replays its full state on attach.
// Rust waits up to 60 s; there is no timeout argument.
let sessions = server.workspaces().await?;
let workspace = server.attach(sessions[0].workspace_id).await?;
ss_workspace *workspace = ss_server_attach(server, workspace_id);
Parameters:
workspace_id: an id fromworkspaces().timeout/timeout_ms: default 60 s.
Errors: an id that is not on the server, such as a workspace already
closed, ends in Timeout after the timeout rather than in an immediate error.
In Python, capture.state and capture.running read idle right after
attach() to a workspace whose capture is running, until the server sends the
next state change. To know whether a session is capturing, read capturing in
workspaces().
See Headless datalogger for the full pattern.
open​
Opens a .scana file as a new workspace: the captured data, its decoders and
its annotations, with no device involved.
- Python
- NodeJS
- Rust
- C
workspace = server.open("/data/run-42.scana")
const workspace = await server.open('/data/run-42.scana');
let workspace = server.open("/data/run-42.scana").await?;
/* The path is resolved by the SERVER, on the server's machine. */
ss_workspace *workspace = ss_server_open(server, "/data/run-42.scana");
if (!workspace) fprintf(stderr, "open: %s\n", ss_last_error());
The path is opened by the server, on the server's machine and with the
server's permissions, not by your script. Against a remote server, a local path
means nothing. The same is true of save() and export_csv().
Parameters:
path— the.scanato open, as the server sees it.timeout/timeout_ms— default 600 s; loading a large capture is slow. Rust has no timeout argument and always waits up to 600 s.
Errors: a file the server cannot read (missing, not a .scana, no
permission) raises IoError (Error::Io in Rust) at once, with the server's
reason. A load that is still running when the timeout runs out raises
Timeout.
open() waits until the file is loaded. When it returns, last_sample
describes the recording and the transitions are readable. See
Opening a .scana.
file_info​
Reads the manifest of a .scana without opening it: the device it was
recorded on, the channel count, the sample rate and the length. It needs no
workspace, so a script can decide what to open before creating anything.
- Python
- NodeJS
- Rust
- C
info = server.file_info("/data/run-42.scana")
info["device"], info["device_name"], info["channels"]
info["sample_rate"], info["requested_samples"]
info["error"] # None, or why the file could not be read
const info = await server.file_info('/data/run-42.scana');
info.device_name;
info.error; // null, or why the file could not be read
let info: serde_json::Value = server.file_info("/data/run-42.scana").await?;
println!("{}", info["device_name"]);
char *info = ss_server_file_info(server, "/data/run-42.scana"); /* JSON */
if (info) { printf("%s\n", info); ss_string_free(info); }
As with open(), the server resolves the path. A file it cannot read does
not raise: the answer carries error and empty fields.
Example output. A one-million-sample SE254 capture:
{'device': 'se254', 'device_name': 'SE254 (demo)', 'channels': 4,
'sample_rate': 25000000, 'requested_samples': 1000000, 'error': None}
The current workspace​
- Python
- NodeJS
- Rust
- C
server.workspace # the Workspace this connection is attached to, or None
server.workspace; // Workspace | null
// The Workspace value returned by open()/attach() is the handle; keep it.
/* The ss_workspace * returned by ss_server_create()/ss_server_attach() is the handle. */
uint64_t id = ss_workspace_id(workspace);
Closing​
- Python
- NodeJS
- Rust
- C
server.close() # ends the connection; workspaces keep running
server.close(); // ends the connection; workspaces keep running
drop(server); // the connection ends once every Workspace from it is dropped too
ss_server_disconnect(server);
shutdown​
Stops the server itself, and with it every workspace it holds. Captures are aborted, data is freed and devices are released, for every client, including the ScanaStudio window. Any client may send it.
- Python
- NodeJS
- Rust
- C
server.shutdown()
server.shutdown();
server.shutdown()?;
ss_server_shutdown(server);
A test bench almost never wants this. To end a script, close the connection; to end one session, close its workspace.
scripts​
server.scripts (server.scripts() in Rust) manages the decoder and generator
scripts installed on the server. See
Script library.