Skip to main content

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​

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

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.

Removing a marker removes its measurements

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().

  • T can anchor a measurement like any other marker: pass its id to measures.add.
  • move_to and remove on it are ignored. markers.clear() keeps it.
  • A new capture with a trigger moves T to the new trigger position, and the measurements anchored to it are recomputed. A new capture without a trigger, or one whose trigger never fires, removes T and the measurements anchored to it.
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"])

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.

measure_id = workspace.measures.add(
first, second,
channel=0,
kinds=["time_total", "freq_avg", "duty_avg", "edges"],
time_precision=3,
freq_precision=3,
)

measures.between​

The same thing without placing the markers yourself: give two sample indices and the SDK creates the markers for you.

measure_id = workspace.measures.between(
0, workspace.capture.last_sample,
channel=0,
kinds=["freq_avg", "duty_avg"],
)

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().

KindResult
time_totalTime between the two markers, in seconds.
freq_avgAverage frequency, in hertz.
freq_minLowest frequency of a single period, in hertz.
freq_maxHighest frequency of a single period, in hertz.
time_highTotal time spent high, in seconds.
time_lowTotal time spent low, in seconds.
high_avgAverage high pulse width, in seconds.
high_shortestShortest high pulse, in seconds.
high_longestLongest high pulse, in seconds.
low_avgAverage low pulse width, in seconds.
low_shortestShortest low pulse, in seconds.
low_longestLongest low pulse, in seconds.
duty_avgAverage duty cycle, a ratio from 0 to 1.
duty_minLowest duty cycle of a single period, from 0 to 1.
duty_maxHighest duty cycle of a single period, from 0 to 1.
edgesRising plus falling edges.
rising_edgesRising edges alone.
falling_edgesFalling 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:

LanguageUnknown name
PythonValueError
NodeJSRangeError
RustDoes not compile: kinds is a &[MeasureKind]. MeasureKind::from_name("duty_avg") converts a string, and kind.as_str() gives the name back.
Css_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.

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

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.

Values are numbers, not display strings

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.

workspace.measures.set_kinds(measure_id, ["time_high", "duty_avg"], time_precision=6)

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.

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

Clearing​

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

After markers.clear(), new markers are numbered from M1 again.