Markers and measurements
A marker is a named point in time. A measurement spans two markers on a
channel and publishes numbers about what happens between them: duration,
frequency, pulse widths, duty cycle, edge counts. Markers are reached through
workspace.markers, measurements through workspace.measures.
Measurements are computed on the server and anchored to their markers: move a marker, and the numbers are recomputed.
add, move, remove​
- Python
- NodeJS
- Rust
- C
first = workspace.markers.add(sample=0) # returns the marker id
second = workspace.markers.add(sample=250_000)
workspace.markers.move_to(first, sample=1_000)
workspace.markers.remove(first)
for marker in workspace.markers.all():
marker.id
marker.label # "M1", "M2"... assigned by the server, "T" for the trigger
marker.sample
marker.trigger # True for the marker the trigger placed
const first = await workspace.markers.add(0); // returns the marker id
const second = await workspace.markers.add(250_000);
workspace.markers.move_to(first, 1_000);
workspace.markers.remove(first); // takes its measurements with it
for (const marker of workspace.markers.all()) {
marker.id;
marker.label; // "M1", "M2"... assigned by the server, "T" for the trigger
marker.sample;
marker.trigger; // true for the marker the trigger placed
}
let first = workspace.markers().add(0).await?; // returns the marker id
let second = workspace.markers().add(250_000).await?;
workspace.markers().move_to(first, 1_000)?;
workspace.markers().remove(first)?; // takes its measurements with it
for marker in workspace.markers().all() {
println!("{} at {} (trigger: {})", marker.label, marker.sample, marker.trigger);
}
uint32_t first = ss_marker_add(workspace, 0); /* 0 on failure */
uint32_t second = ss_marker_add(workspace, 250000);
ss_marker_move(workspace, first, 1000);
ss_marker_remove(workspace, first); /* takes its measurements with it */
/* Reading them is a batch: read, count, copy, free. */
ss_markers *batch = ss_markers_read(workspace);
size_t count = ss_markers_count(batch);
ss_marker *markers = malloc(sizeof *markers * count);
ss_markers_copy(batch, 0, markers, count);
for (size_t i = 0; i < count; i += 1) {
printf("%s at %lld%s\n", markers[i].label, (long long)markers[i].sample,
markers[i].trigger ? " (trigger)" : "");
}
free(markers);
ss_markers_free(batch); /* every `label` above dies here */
add returns once the server has pushed the new marker back, so all()
already holds it. A sample below 0 is placed at sample 0.
A measurement is anchored to two markers. Remove one of them and the
measurement goes with it. This holds in every language, ss_marker_remove and
ss_markers_clear included.
The trigger marker​
When a capture triggers, the server places a marker labelled T at the trigger
position. It is listed by markers.all() with trigger set, and its id is
always 4294967295 (the largest 32-bit unsigned value). When capture.run()
returns, T is already in markers.all().
Tcan anchor a measurement like any other marker: pass its id tomeasures.add.move_toandremoveon it are ignored.markers.clear()keeps it.- A new capture with a trigger moves
Tto the new trigger position, and the measurements anchored to it are recomputed. A new capture without a trigger, or one whose trigger never fires, removesTand the measurements anchored to it.
- Python
- NodeJS
- Rust
- C
t = next(m for m in workspace.markers.all() if m.trigger)
end = workspace.markers.add(workspace.capture.last_sample)
workspace.measures.add(t.id, end, channel=0, kinds=["time_total", "edges"])
const t = workspace.markers.all().find((m) => m.trigger);
const end = await workspace.markers.add(workspace.capture.last_sample);
await workspace.measures.add(t.id, end, { channel: 0, kinds: ['time_total', 'edges'] });
use scanastudio_client::{MeasureKind, Precision};
let t = workspace.markers().all().into_iter().find(|m| m.trigger).unwrap();
let end = workspace.markers().add(last).await?;
workspace.measures()
.add(t.id, end, 0, false, &[MeasureKind::TimeTotal, MeasureKind::Edges], Precision::default())
.await?;
/* The trigger marker always has this id. */
uint32_t t = 4294967295u;
uint32_t end = ss_marker_add(workspace, ss_capture_last_sample(workspace));
ss_measure_add(workspace, t, end, 0, "[\"time_total\", \"edges\"]", 3, 3);
measure​
Creates a measurement between two markers on a channel. Computation starts
immediately. add returns the measurement's id once the server has pushed it
back.
- Python
- NodeJS
- Rust
- C
measure_id = workspace.measures.add(
first, second,
channel=0,
kinds=["time_total", "freq_avg", "duty_avg", "edges"],
time_precision=3,
freq_precision=3,
)
// Computation starts immediately; wait() below blocks for the results.
const measureId = await workspace.measures.add(first, second, {
channel: 0,
kinds: ['time_total', 'freq_avg', 'duty_avg', 'edges'],
time_precision: 3,
freq_precision: 3,
});
use scanastudio_client::{MeasureKind, Precision};
// Computation starts immediately; wait() below blocks for the results.
// The fourth argument is `generator`: false measures the capture.
// Precision::default() is 3 and 3.
let measure_id = workspace.measures()
.add(
first, second, 0, false,
&[MeasureKind::TimeTotal, MeasureKind::FreqAvg, MeasureKind::DutyAvg, MeasureKind::Edges],
Precision::default(),
)
.await?;
/* NULL kinds means the standard set. The last two are the precisions
asked for on times and frequencies; 255 means exact. */
uint32_t measure = ss_measure_add(workspace, first, second, 0,
"[\"time_total\", \"freq_avg\", \"duty_avg\", \"edges\"]", 3, 3);
if (!measure) fprintf(stderr, "measure: %s\n", ss_last_error());
/* The 18 names, as a JSON array. */
char *kinds = ss_measure_kinds();
if (kinds) { printf("%s\n", kinds); ss_string_free(kinds); }
measures.between​
The same thing without placing the markers yourself: give two sample indices and the SDK creates the markers for you.
- Python
- NodeJS
- Rust
- C
measure_id = workspace.measures.between(
0, workspace.capture.last_sample,
channel=0,
kinds=["freq_avg", "duty_avg"],
)
// Places the two markers for you, then measures between them.
const measureId = await workspace.measures.between(
0, workspace.capture.last_sample,
{ channel: 0, kinds: ['freq_avg', 'duty_avg'] },
);
// Places the two markers for you, then measures between them.
// An empty list asks for the standard set.
let measure_id = workspace.measures()
.between(0, last, 0, false, &[], Precision::default())
.await?;
/* Places the two markers for you, then measures between them. */
uint32_t measure = ss_measure_between(workspace, 0, ss_capture_last_sample(workspace), 0,
"[\"freq_avg\", \"duty_avg\"]", 3, 3);
Measuring a generation row​
On a device that generates signals, each channel also has a generation row: the
signal the next generation replays (see
generation). generator=True
measures that row instead of the capture. It is a keyword in Python, an option
in NodeJS and the fourth argument in Rust. The C library does not offer it.
workspace.measures.add(first, second, channel=0, generator=True)
The measurement kinds​
kinds is a list drawn from a closed set of 18 names. Python and NodeJS export
it as MEASURE_KINDS; Rust as MEASURE_KINDS and MeasureKind::ALL, of type
MeasureKind; C returns it from ss_measure_kinds().
| Kind | Result |
|---|---|
time_total | Time between the two markers, in seconds. |
freq_avg | Average frequency, in hertz. |
freq_min | Lowest frequency of a single period, in hertz. |
freq_max | Highest frequency of a single period, in hertz. |
time_high | Total time spent high, in seconds. |
time_low | Total time spent low, in seconds. |
high_avg | Average high pulse width, in seconds. |
high_shortest | Shortest high pulse, in seconds. |
high_longest | Longest high pulse, in seconds. |
low_avg | Average low pulse width, in seconds. |
low_shortest | Shortest low pulse, in seconds. |
low_longest | Longest low pulse, in seconds. |
duty_avg | Average duty cycle, a ratio from 0 to 1. |
duty_min | Lowest duty cycle of a single period, from 0 to 1. |
duty_max | Highest duty cycle of a single period, from 0 to 1. |
edges | Rising plus falling edges. |
rising_edges | Rising edges alone. |
falling_edges | Falling edges alone. |
An empty or absent list asks for the standard set: time_total,
freq_avg, freq_min, freq_max. It is exported as
STANDARD_MEASURE_KINDS in Python and NodeJS, and MeasureKind::STANDARD in
Rust.
Every name is checked before anything is sent:
| Language | Unknown name |
|---|---|
| Python | ValueError |
| NodeJS | RangeError |
| Rust | Does not compile: kinds is a &[MeasureKind]. MeasureKind::from_name("duty_avg") converts a string, and kind.as_str() gives the name back. |
| C | ss_measure_add returns 0 and ss_last_error() says which name. |
The server applies the same check, and an unknown name refuses the whole measurement. The results come back under the names you asked for, in the order you asked for them.
wait and results​
Measurements are computed asynchronously. wait blocks until nothing is still
computing, and hands back the results.
- Python
- NodeJS
- Rust
- C
for measure in workspace.measures.wait(timeout=60.0):
measure.id
measure.channel
measure.generator # True when it reads a generation row
measure.marker_a, measure.marker_b
measure.computing # False once it is settled
for result in measure.results:
print(result.kind, result.value) # "time_total" 0.02
// Blocks until nothing is still computing.
for (const measure of await workspace.measures.wait(60_000)) {
measure.marker_a;
measure.marker_b;
measure.computing; // false once it is settled
for (const result of measure.results) {
console.log(result.kind, result.value); // "time_total" 0.02
}
}
In NodeJS, wait also waits for the measurements this object created to
appear. Its second argument adds more ids to wait for, such as measurements
created by another connection: wait(60_000, [id1, id2]).
// Blocks until nothing is still computing.
for measure in workspace.measures().wait(Duration::from_secs(60)).await? {
for result in &measure.results {
println!("{} = {}", result.kind.as_str(), result.value); // "time_total = 0.02"
}
}
/* Blocks until nothing is still computing, then reads. ss_measures_read
takes them as they stand, without waiting. */
ss_measures *batch = ss_measures_wait(workspace, 60.0);
if (!batch) fail("measures");
for (size_t i = 0; i < ss_measures_count(batch); i += 1) {
ss_measure row;
if (ss_measures_copy(batch, i, &row, 1) != 1) break;
ss_measure_value *values = malloc(sizeof *values * row.value_count);
ptrdiff_t got = ss_measure_values_copy(batch, i, 0, values, row.value_count);
for (ptrdiff_t v = 0; v < got; v += 1) {
printf("%s = %g\n", values[v].kind, values[v].value);
}
free(values);
}
ss_measures_free(batch); /* every `kind` above dies here */
Example output. One measurement over a 500 000-sample window of the SE254 demo's UART TX channel, asking for all 18 kinds:
time_total 0.02
freq_avg 49462.08970609917
freq_min 17111.567419575633
freq_max 114678.89908256881
time_high 0.00064016
time_low 0.01935984
high_avg 1.3620425531914893e-05
high_shortest 4.08e-06
high_longest 5.296e-05
low_avg 1.1711304347826087e-05
low_shortest 4.12e-06
low_longest 3.2200000000000003e-05
duty_avg 0.5430238870792616
duty_min 0.17921686746987953
duty_max 0.9062286105407256
edges 94.0
rising_edges 47.0
falling_edges 47.0
workspace.measures.all() (measures().all() in Rust, ss_measures_read in
C) returns the current list without waiting, if you would rather poll
computing yourself.
result.value is a plain number in SI units: seconds, hertz, a 0 to 1 ratio or
a count. The precision settings control how ScanaStudio displays the value;
they do not round what you read here.
set_kinds​
Changes what an existing measurement publishes, without recreating it.
- Python
- NodeJS
- Rust
- C
workspace.measures.set_kinds(measure_id, ["time_high", "duty_avg"], time_precision=6)
// Changes what an existing measurement publishes, without recreating it.
workspace.measures.set_kinds(measureId, ['time_high', 'duty_avg'], { time_precision: 6 });
// Rust has no keyword arguments, so the precisions are a builder.
workspace.measures().set_kinds(
measure_id,
&[MeasureKind::TimeHigh, MeasureKind::DutyAvg],
Precision::new().time(6),
)?;
/* The last two arguments are the precisions, on times and frequencies. */
ss_measure_set_kinds(workspace, measure, "[\"time_high\", \"duty_avg\"]", 6, 3);
move_to​
Moves a whole measurement: both of its markers, and optionally its channel.
Other measurements sharing one of those markers are recomputed too. This is the
only way to move a measurement to another channel: moving its markers one at a
time keeps the channel it was measured on. With a channel given, generator
puts the measurement on that channel's generation row.
- Python
- NodeJS
- Rust
- C
workspace.measures.move_to(measure_id, 0, 500_000) # same channel
workspace.measures.move_to(measure_id, 0, 500_000, channel=1) # to channel 1
workspace.measures.move_to(measureId, 0, 500_000); // same channel
workspace.measures.move_to(measureId, 0, 500_000, { channel: 1 }); // to channel 1
workspace.measures().move_to(measure_id, 0, 500_000, None, false)?; // same channel
workspace.measures().move_to(measure_id, 0, 500_000, Some(1), false)?; // to channel 1
/* -1 keeps the channel it was measured on. */
ss_measure_move(workspace, measure, 0, 500000, -1);
ss_measure_move(workspace, measure, 0, 500000, 1); /* to channel 1 */
Clearing​
- Python
- NodeJS
- Rust
- C
workspace.measures.remove(measure_id) # one measurement, markers stay
workspace.measures.clear() # every measurement, markers stay
workspace.markers.clear() # every marker but T, and every measurement
workspace.measures.remove(measureId); // one measurement, markers stay
workspace.measures.clear(); // every measurement, markers stay
workspace.markers.clear(); // every marker but T, and every measurement
workspace.measures().remove(measure_id)?; // one measurement, markers stay
workspace.measures().clear()?; // every measurement, markers stay
workspace.markers().clear()?; // every marker but T, and every measurement
ss_measure_remove(workspace, measure); /* one measurement, markers stay */
ss_measures_clear(workspace); /* every measurement, markers stay */
ss_markers_clear(workspace); /* every marker but T, and every measurement */
After markers.clear(), new markers are numbered from M1 again.