Capture
Arming, running and following an acquisition. Reached through
workspace.capture.
run​
Start a capture and wait for it to finish. The one call most scripts need.
- Python
- NodeJS
- Rust
- C
from ikalogic_scanastudio import Trigger
last = workspace.capture.run(
samples=1_000_000,
sample_rate=25_000_000,
trigger=Trigger.rising(channel=0, position=0.1),
timeout=300.0,
)
print(f"captured up to sample {last}")
import { Trigger } from '@ikalogic/scanastudio';
// Arm on a rising edge on channel 0, keeping 10% of history before it.
const last = await workspace.capture.run({
samples: 1_000_000,
sample_rate: 25_000_000,
trigger: Trigger.rising(0, { position: 0.1 }),
timeout_ms: 300_000,
});
use scanastudio_client::{CaptureRequest, Trigger, TriggerExt};
use std::time::Duration;
// Arm on a rising edge on channel 0, keeping 10% of history before it.
let last = workspace.capture().run(
&CaptureRequest::new(1_000_000, 25_000_000).trigger(Trigger::rising(0).at(0.1)),
Duration::from_secs(300),
).await?;
/* The request, then how long to wait for it. */
char *trigger = ss_trigger_edge(0, "rising", 0.1, NULL);
char request[192];
snprintf(request, sizeof request,
"{\"samples\": 1000000, \"sample_rate\": 25000000, \"trigger\": %s}",
trigger);
ss_string_free(trigger);
/* Returns the last sample index, or -1 on failure (not an ss_status). */
int64_t last = ss_capture_run(workspace, request, 300.0);
if (last < 0) fprintf(stderr, "capture: %s\n", ss_last_error());
Without a trigger field the capture begins as soon as the device is armed:
ss_capture_run(workspace, "{\"samples\": 1000000, \"sample_rate\": 25000000}", 300.0);
Every trigger Triggers describes
is built with an ss_trigger_* function and dropped into the same request.
Returns: the index of the last sample captured.
Errors:
Timeout: the capture did not start withinmin(timeout, 30 s)(Timeout("the capture to start")), or it was still running whentimeoutran out.Refused: the server declined to start the acquisition. The message is the server's reason.CaptureFailed: the acquisition started, then ended in theerrorstate. The message is the server's detail.ConnectionClosed: the connection dropped while waiting.
In Rust these are Error::Timeout, Error::Refused, Error::CaptureFailed and
Error::Closed. In C, ss_capture_run returns -1 and ss_last_error() holds
the reason.
How run() waits. It watches for a capture state that arrives after its
own start, so the state left by a previous capture is never taken for this one.
Once the capture is running it waits for a terminal state. After a refusal it
waits up to 5 s for the server to report that it is idle again, so that the next
run() starts cleanly.
Choosing a sample rate​
A device does not sample at any rate you like: it has a fixed list, and
server.capabilities(device).sample_rates
is that list, descending.
A rate that is not on the list is not refused. The server picks one for you:
The server rounds down to the nearest offered rate, never up. Sampling faster than requested would fill the acquisition sooner than planned. The one exception is a request below the slowest rate the device has, which rounds up to that rate.
Measured on an SP259, whose rates are
[250M, 125M, 25M, 12.5M, 6.25M, 2.5M]:
| You ask for | You get | Why |
|---|---|---|
| 250 000 000 | 250 000 000 | exactly on the list |
| 999 000 000 | 250 000 000 | above the fastest → the fastest |
| 100 000 000 | 25 000 000 | rounds down past 125 M, which is faster than asked |
| 26 000 000 | 25 000 000 | rounds down |
| 1 000 | 2 500 000 | below the slowest → the slowest |
That third row is the one that surprises people: 100 MS/s lands on 25 MS/s, not 125 MS/s, because 125 MS/s is faster than you asked for.
Always read the rate back. capture.sample_rate is what the device settled
on, and it is always a member of capabilities().sample_rates:
- Python
- NodeJS
- Rust
- C
workspace.capture.run(samples=1_000_000, sample_rate=100_000_000)
print(workspace.capture.sample_rate) # 25000000 on an SP259
# Or pick from the list up front, and get exactly what you chose.
caps = server.capabilities("sp259")
rate = next(r for r in caps.sample_rates if r <= 100_000_000)
workspace.capture.run(samples=1_000_000, sample_rate=rate)
await workspace.capture.run({ samples: 1_000_000, sample_rate: 100_000_000 });
console.log(workspace.capture.sample_rate); // 25000000 on an SP259
// Or pick from the list up front, and get exactly what you chose.
const caps = await server.capabilities('sp259');
const rate = caps.sample_rates.find((r) => r <= 100_000_000);
await workspace.capture.run({ samples: 1_000_000, sample_rate: rate });
workspace.capture()
.run(&CaptureRequest::new(1_000_000, 100_000_000), Duration::from_secs(300))
.await?;
println!("{}", workspace.capture().sample_rate()); // 25000000 on an SP259
ss_capture_run(workspace, "{\"samples\": 1000000, \"sample_rate\": 100000000}", 300.0);
printf("%llu\n", (unsigned long long)ss_capture_sample_rate(workspace)); /* 25000000 */
Dividing by the rate you asked for, rather than the one in
capture.sample_rate, gives results that are consistently off. On the 100 MS/s
example above, that is a factor of four.
start and stop​
start arms the capture and returns immediately. Use it when the script should
not wait, as in Headless datalogger.
start()In Rust and C, start(), stop() and close() (ss_capture_start,
ss_capture_stop, ss_workspace_close) put the command in a send queue and
return. A program that ends at once can exit before the queue is written, and
the server never receives the command. Make one request that waits for an
answer afterwards, such as server.workspaces() (ss_server_workspaces in C),
to make sure it went out.
- Python
- NodeJS
- Rust
- C
workspace.capture.start(
samples=2_000_000_000,
sample_rate=25_000_000,
trigger=Trigger.immediate(),
)
# ... the script can exit here; the capture keeps running on the server
workspace.capture.stop()
workspace.capture.start({
samples: 2_000_000_000,
sample_rate: 25_000_000,
trigger: Trigger.immediate(),
});
// ... the script can exit here; the capture keeps running on the server
workspace.capture.stop();
workspace.capture().start(&CaptureRequest::new(2_000_000_000, 25_000_000))?;
// start() only queues the command. Before the program exits, make one request
// that waits for an answer: commands go out in order, so its answer means the
// start has reached the server.
server.workspaces().await?;
// ... the program can exit here; the capture keeps running on the server
workspace.capture().stop()?;
/* The whole StartCapture payload, as JSON. ss_capture_start returns at once. */
ss_capture_start(workspace,
"{\"samples\": 2000000000, \"sample_rate\": 25000000,"
" \"trigger\": {\"trig_a\": {\"kind\": \"none\"}}}");
/* The start is only queued. One answered request makes sure it went out. */
ss_string_free(ss_server_workspaces(server));
/* ... the program can exit here; the capture keeps running on the server */
ss_capture_stop(workspace);
Both take the same request. ss_capture_run arms and waits in one call.
ss_capture_start returns as soon as the device is armed; call
ss_capture_wait later, or nothing at all if the program means to exit. The
ss_trigger_* builders write the trigger object for either one.
An unknown field in the request is refused, not ignored, so a misspelt name is an error.
Options. Anything left out keeps the device's default:
| Option | Meaning |
|---|---|
samples | How many samples to capture. |
sample_rate | Samples per second. The device rounds to a rate it offers; read sample_rate back to learn what it settled on. |
trigger | What arms the capture. See Triggers. Absent starts at once. |
mode | timing samples on the device's internal clock (the default); state samples on an external one. |
state_clock_channel, state_edge | Which channel clocks state mode, and on which edge. |
logic_levels | Input thresholds in volts, per channel group. |
pulls | Per-channel pull: up, down or none. |
probes | Per-port probe model, on families with removable probes. |
industrial | Per-channel industrial receiver settings. |
channel_thresholds | Input threshold in volts, per channel. Read only on the ports probes sets to a probe with one comparator per channel (P992), and only when the capabilities say per_channel_threshold. |
ext_trig_test | TRIG OUT to TRIG IN loopback test, with the two connected: TRIG OUT is held high once the device is armed, so an external rising-edge trigger fires through the link. Accepted only where the capabilities say can_ext_trig_test (the SP259 family), refused elsewhere. |
operation | capture (the default), generation or mixed, on a family that replays a signal (signal_generator in its capabilities: the SQ family). See Generation rows. |
generator_samples | How many samples the generator replays, from sample 0 of the generation rows. |
generator_loop | Replay until the capture is stopped rather than once. |
channel_io | Direction per channel: input, output or open_drain, on a family whose channels can drive (channel_io in its capabilities). |
differential | One boolean per differential pair of the device, in the order of differential_pairs in its capabilities: read that pair as one input. |
timeout_ms | NodeJS only, for run(): how long to wait, in milliseconds. Python's run() takes timeout in seconds; Rust and C take the timeout as a separate argument. |
CaptureRequest in Rust, and the request JSON in C, have no operation,
generator_samples, generator_loop, channel_io or differential field.
From those two SDKs a capture is always a plain capture: generation and mixed
operation on the SQ family need Python or NodeJS. An unknown field in the C
request is refused.
stop ends the acquisition; whatever was already captured stays and stays
readable.
wait​
Blocks until the capture reaches a terminal state.
- Python
- NodeJS
- Rust
- C
last = workspace.capture.wait(timeout=300.0)
// Blocks until the capture reaches a terminal state.
const last = await workspace.capture.wait(300_000);
// Blocks until the capture reaches a terminal state.
let last = workspace.capture().wait(Duration::from_secs(300)).await?;
/* What blocks after ss_capture_start. ss_capture_run has already waited.
Both return the last sample index. */
int64_t last = ss_capture_wait(workspace, 300.0);
if (last < 0) fprintf(stderr, "wait: %s\n", ss_last_error());
Returns: the index of the last sample captured. On a capture this
connection ran and that has already finished, wait() returns at once. After
attach() to a workspace whose capture has finished, the SDK does not yet know
the capture state, and wait() runs to its timeout: check
WorkspaceDesc.capturing from workspaces() instead.
Errors:
Timeout(Error::Timeoutin Rust): the capture was still running when the time ran out. The capture is not stopped by a timeout; decide whether tostop()or keep waiting.Refused: the server declined the start that preceded this wait.CaptureFailed(Error::CaptureFailed): the capture started, then ended in theerrorstate. The message is the server's detail.ConnectionClosed(Error::Closed): the connection dropped.
wait() returns when the capture is in a terminal state and has captured
something. On a workspace that has never captured, it waits until a capture
started by another client finishes, or until the timeout.
state and running​
- Python
- NodeJS
- Rust
- C
workspace.capture.state # "sampling"
workspace.capture.running # True while the capture is live
workspace.capture.state; // "sampling"
workspace.capture.running; // true while the capture is live
workspace.capture().state(); // String, e.g. "sampling"
workspace.capture().running(); // true while the capture is live
char *state = ss_capture_state(workspace); /* "sampling" */
if (state) { printf("%s\n", state); ss_string_free(state); }
ss_capture_running(workspace); /* true while it is live */
The states, in the order they occur:
| State | Meaning |
|---|---|
idle | Nothing has been started. |
preparing | A start was accepted and the server is getting ready. The first running state. |
configuring | The device is being set up. |
purging | Old data is being cleared out. |
pretrigger | Filling the pre-trigger buffer, so the trigger has history behind it. |
waiting_trigger | Armed, waiting for the trigger condition. |
sampling | Capturing. |
done | Finished normally. |
error | Finished badly; the log says why. |
idle, done and error are terminal; the rest mean the capture is running.
RUNNING_STATES holds the running ones in Python, NodeJS and Rust. ss_capture_state in C
returns the same strings.
last_sample, sample_rate, seconds​
- Python
- NodeJS
- Rust
- C
last = workspace.capture.last_sample # -1 before anything is captured
rate = workspace.capture.sample_rate # what the DEVICE settled on
print(f"{workspace.capture.seconds(last):.6f} s of signal")
const last = workspace.capture.last_sample; // -1 before anything is captured
const rate = workspace.capture.sample_rate; // what the DEVICE settled on
workspace.capture.seconds(last);
let last = workspace.capture().last_sample(); // -1 before anything is captured
let rate = workspace.capture().sample_rate(); // what the DEVICE settled on
workspace.capture().seconds(last); // sample index -> seconds
int64_t last = ss_capture_last_sample(workspace);
uint64_t rate = ss_capture_sample_rate(workspace); /* what the DEVICE settled on */
double seconds = ss_capture_seconds(workspace, last);
The device rounds the rate you asked for to one it can produce. Timing a signal
with the rate you requested rather than the one in sample_rate gives results
that are consistently off.
trigger_count​
How many times the trigger has fired. It only moves in normal mode, where the
capture re-arms after each trigger. It goes back to 0 at every start.
- Python
- NodeJS
- Rust
- C
workspace.capture.trigger_count
workspace.capture.trigger_count; // only moves in "normal" mode
workspace.capture().trigger_count(); // u32, only moves in "normal" mode
uint32_t seen = ss_capture_trigger_count(workspace); /* only moves in "normal" */
config​
The capture configuration the server actually applied: the settled sample rate, the trigger definition, the thresholds. Useful for logging what a test bench ran with.
- Python
- NodeJS
- Rust
- C
config = workspace.capture.config # a CaptureConfigEvent
config.sample_rate, config.trigger, config.operation
// The configuration the server actually applied.
const config = workspace.capture.config; // CaptureConfigEvent | undefined
// The configuration the server actually applied.
let config = workspace.capture().config(); // Option<ServerMsg>
/* JSON, or NULL while the server has sent none. */
char *config = ss_capture_config(workspace);
if (config) { printf("%s\n", config); ss_string_free(config); }
The server sends the configuration when the workspace opens, so before the
first capture it holds the workspace's defaults. Until it has arrived, Python
and Rust return None, NodeJS undefined and C NULL.
When a capture is refused​
If the server cannot honour the request, the capture is refused rather than
started, with a written reason you can show to an operator. The SDKs raise it as
Refused (Error::Refused in Rust; in C the call fails and ss_last_error()
holds the reason).
Each refusal also names a subject, the setting to go and change: device,
trigger, probes, clock or pattern_generator. The Refused exception
carries only the message. To read the subject, subscribe to the
capture_refused event (see Events);
in Python, events.subscribe is the only way to see that event, as it never
reaches latest() or wait_for().
Common reasons:
- the workspace was opened from a
.scanaand has no device attached; - the device is still starting up, or is being updated;
- a start sent while another start is still being prepared (
A start is already on its way); - a trigger the device cannot arm, including a step cell other than
x,0,1,riseorfall; ext_trig_teston a device without the loopback test;- a probe or clock setting the device cannot honour;
- a pattern generator script that refuses its configuration, or a generation with nothing on its output rows.