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 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
when the value is dropped.
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.13 (a0db302), protocol 8
server.info.protocol # 8
server.info.version # "6.0.13"
server.info.commit # "a0db302"
server.info.date # build date
// Already read by the time connect() resolved.
server.info.protocol; // 8
server.info.version; // "6.0.13"
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);
/* JSON: {"protocol":8,"version":"6.0.13","commit":"...","date":"..."} */
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.13 (1a909ee), protocol 8
In NodeJS the object itself prints as:
{ protocol: 8, version: "6.0.13", commit: "1a909ee", date: "2026-09-02" }
and in C it arrives as JSON:
{"commit":"1a909ee","date":"2026-09-02","protocol":8,"version":"6.0.13"}
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():
print(device) # "se254 — SE254 (demo)"
device.key # what open() takes: "se254" or "hw:SP259-000123"
device.name # "SE254"
device.model
device.serial # None for a demo device
device.demo # bool
device.probes # probe models, per group
// key is the string open() takes.
for (const device of await server.devices()) {
device.key; // "se254" or "hw:SP259-000123"
device.name;
device.model;
device.serial; // null for a demo device
device.demo;
device.probes;
}
// device.key is the string open() takes.
for device in server.devices().await? {
println!("{} — {}{}", device.key, device.name, if device.demo { " (demo)" } else { "" });
}
/* JSON array: [{"key":"se254","name":"SE254","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 open() takes.
Example output. A server with no hardware plugged in still offers the demo
devices, printed here through Python's Device.__str__:
se254 — SE254 (demo) (demo)
sp209 — SP209 (demo) (demo)
sp209i — SP209I (demo) (demo)
sp259 — SP259 (demo) (demo)
sp259i — SP259I (demo) (demo)
sp1018 — SP1018 (demo) (demo)
sp1036 — SP1036 (demo) (demo)
sp1054 — SP1054 (demo) (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 # the channels can generate as well as capture
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.can_ext_trig_in, caps.can_ext_trig_out
caps.industrial_inputs # empty on a non-industrial version
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; // can this device generate as well as capture
caps.pre_trig_limit;
caps.trigger_engines;
// 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>.
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. Three devices:
--- se254 ---
sample_rates: [250000000, 125000000, 25000000, 12500000, 6250000, 2500000] (6)
logic_levels: [1.2, 1.5, 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
--- 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
Three things to note: only the SP1018G reports pattern_generator: True; 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:SP259-000123") # real hardware
workspace = server.create("se254", timeout=60.0)
const workspace = await server.create('se254'); // a demo device
const workspace = await server.create('hw:SP259-000123', 60_000); // real hardware
// "se254" is a demo device; "hw:<serial>" is real hardware.
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.
Opening a second workspace on the same client detaches it from the first. The first workspace keeps running on the server; you have only stopped watching it. Use a second connection 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
for (const session of await server.workspaces()) {
session.workspace_id; // what attach() takes
session.device_name;
session.state;
session.last_sample;
session.clients;
session.capturing;
}
// 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.
let sessions = server.workspaces().await?;
let workspace = server.attach(sessions[0].workspace_id).await?;
ss_workspace *workspace = ss_server_attach(server, workspace_id);
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.
open() waits until the file is loaded. When it returns, last_sample
describes the recording and the transitions are readable. See
Opening a .scana.
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 with the value
ss_server_disconnect(server);