Reading captured data
Everything the capture produced: raw transitions, decoded packets, and the hex
view. Reached through workspace.data.
Bulk data is pulled, never pushed. Nothing crosses the wire until you ask, and reading slowly never slows the acquisition down.
transitions​
The edges on one channel, oldest first. The SDK pages the request for you, so you can walk a very long capture without holding it in memory.
- Python
- NodeJS
- Rust
- C
for edge in workspace.data.transitions(channel=0):
edge.sample # where it happened
edge.level # the level it changed TO (0 or 1)
# A window, and a count:
edges = list(workspace.data.transitions(channel=0, start=0, end=1_000_000))
print(f"{len(edges)} transitions")
for await (const edge of workspace.data.transitions(0)) {
edge.sample; // where it happened
edge.level; // the level it changed TO (0 or 1)
}
// A window, and a count:
const edges = [];
for await (const edge of workspace.data.transitions(0, { start: 0, end: 1_000_000 })) {
edges.push(edge);
}
// Rust returns the window as a Vec rather than a stream.
// The third argument is the end; None reads to the end of the capture.
let edges = workspace.data().transitions(0, 0, None).await?;
for edge in &edges {
println!("{} -> {}", edge.sample, edge.level);
}
enum { ROOM = 4096 };
ss_edge *edges = malloc(sizeof(ss_edge) * ROOM);
/* channel, from, to (-1 = end), out, capacity; -1 on failure */
ptrdiff_t written = ss_data_transitions(workspace, 0, 0, -1, edges, ROOM);
for (ptrdiff_t i = 0; i < written; i += 1) {
printf("%lld -> %u\n", (long long)edges[i].sample, edges[i].level);
}
free(edges);
If the window holds more edges than capacity, ss_data_transitions fills the
buffer and stops. Ask again starting from the last sample you received.
Example output. The first few edges of channel 0, printed as
sample -> level:
0 -> 0
0 -> 1
172 -> 0
1043 -> 1
1371 -> 0
The first record is the level in force at sample 0, which is why two records
share sample 0 here.
The first record you get back is the level in force where you started
reading, so its sample index may be earlier than start. This way a window of
transitions tells you what the channel was doing when the window opened, without
a second query.
Parameters: channel, start (default 0), end (default: the end of the
capture), and the page size, which you rarely need to touch.
level_at, next_edge, previous_edge​
Point queries, for when you want one answer rather than a stream.
- Python
- NodeJS
- Rust
- C
workspace.data.level_at(channel=0, sample=1_000) # 0, 1 or None
workspace.data.next_edge(channel=0, after=1_000) # sample index or None
workspace.data.previous_edge(channel=0, before=1_000) # sample index or None
await workspace.data.level_at(0, 1_000); // 0, 1 or null
await workspace.data.next_edge(0, 1_000); // sample index, or null
await workspace.data.previous_edge(0, 1_000); // sample index, or null
workspace.data().level_at(0, 1_000).await?; // the level: Option<u8>
workspace.data().next_edge(0, 1_000).await?; // sample index: Option<i64>
workspace.data().previous_edge(0, 1_000).await?; // sample index: Option<i64>
ss_data_level_at(workspace, 0, 1000); /* 0, 1, or -1 if not known */
ss_data_next_edge(workspace, 0, 1000); /* sample index, or -1 */
ss_data_previous_edge(workspace, 0, 1000); /* sample index, or -1 */
Each seek costs one round trip, where ss_data_transitions reads a whole window.
Seeking lets a program walk a long signal without holding it in memory.
Returns: the level, or the edge's sample index. Nothing (None / null /
Option::None) when there is no such edge.
packets​
The decoded packets, merged across every decoder attached to the workspace, in time order. This is the Packet View you see in ScanaStudio.
- Python
- NodeJS
- Rust
- C
for packet in workspace.data.packets():
packet.instance_id # which decoder produced it
packet.root # True for a top-level packet, False for a child
packet.channel
packet.start # sample index
packet.end
packet.title # "Address", "Data", "Start"...
packet.content # "0x4E", "ACK"...
// Merged across every decoder attached to the workspace, in time order.
for await (const packet of workspace.data.packets()) {
packet.instance_id; // which decoder produced it
packet.root; // true for a top-level packet, false for a child
packet.start; // sample index
packet.title; // "Address", "Data", "Start"...
packet.content; // "0x4E", "ACK"...
}
use scanastudio_client::proto::RowFilter;
// A default filter keeps everything, merged across every decoder.
let packets = workspace.data().packets(&RowFilter::default()).await?;
for packet in &packets {
println!("{} {}", packet.title, packet.content);
}
/* NULL keeps everything, merged across every decoder attached. */
ss_packets *batch = ss_packets_read(workspace, NULL);
if (!batch) fail("packets");
size_t count = ss_packets_count(batch);
ss_packet *packets = malloc(sizeof *packets * count);
ss_packets_copy(batch, 0, packets, count);
for (size_t i = 0; i < count; i += 1) {
printf("%s: %s\n", packets[i].title, packets[i].content);
}
free(packets);
ss_packets_free(batch); /* every title and content above dies here */
The strings in a copied record point into the batch. Read or strdup them
before ss_packets_free.
Example output. An I²C decoder over a demo capture, printed as
time title: content:
0.000006s I2C: CH4
0.000006s START:
0.000020s RE-START:
0.000032s STOP:
0.000038s I2C: CH4
The I2C rows are the root packets, and START / STOP / RE-START their
children. packet.root tells them apart. On real bus traffic you would also see
Address and Data rows carrying values like 0x4E.
Filtering​
A filter narrows the read on the server, so a big capture does not have to cross the wire to be searched.
| Field | Keeps packets… |
|---|---|
title | whose title contains this text |
content | whose content contains this text |
sources | produced by these decoder instance ids |
channels | on these channels |
from, to | inside this sample range |
dur_min, dur_max | of at least / at most this duration |
- Python
- NodeJS
- Rust
- C
from ikalogic_scanastudio import RowFilter
errors = list(workspace.data.packets(filter=RowFilter(title="Error")))
mine = list(workspace.data.packets(filter=RowFilter(sources=[i2c.instance_id])))
// The filter is applied on the SERVER, before anything crosses the wire.
const errors = [];
for await (const p of workspace.data.packets({ filter: { title: 'Error' } })) {
errors.push(p);
}
// The filter is applied on the SERVER, before anything crosses the wire.
let filter = RowFilter { sources: vec![decoder.instance_id], ..Default::default() };
let packets = workspace.data().packets(&filter).await?;
/* The filter is a RowFilter, applied on the SERVER before anything is sent. */
char filter[64];
snprintf(filter, sizeof filter, "{\"sources\": [%u]}", instance_id);
ss_packets *batch = ss_packets_read(workspace, filter);
A decoder instance can hand you its own packets without a filter, with
decoder.packets(). See Decoders.
hex and find_hex​
The byte-level view of everything the decoders produced, and a search in it. This is what you want for payload and firmware work: the bytes on the bus, without the packet structure around them.
- Python
- NodeJS
- Rust
- C
for entry in workspace.data.hex():
entry.byte # 0-255
entry.channel
entry.start # sample index
entry.end
# Find a byte pattern; returns the offset in the hex view, or None.
offset = workspace.data.find_hex(b"\xDE\xAD\xBE\xEF")
backwards = workspace.data.find_hex([0x55, 0xAA], start=10_000, backwards=True)
for await (const entry of workspace.data.hex()) {
entry.byte; // 0-255
entry.channel;
entry.start; // sample index
}
// Find a byte pattern; resolves to the offset in the hex view, or null.
const offset = await workspace.data.find_hex(new Uint8Array([0xde, 0xad, 0xbe, 0xef]));
const back = await workspace.data.find_hex([0x55, 0xaa], { start: 10_000, backwards: true });
// The empty slice hides no decoder; pass instance ids to exclude them.
let bytes = workspace.data().hex(&[]).await?;
// Find a byte pattern; Some(offset) in the hex view, or None.
let offset = workspace.data().find_hex(&[0xDE, 0xAD, 0xBE, 0xEF], 0, false, &[]).await?;
/* NULL hides no decoder; pass a JSON array of instance ids to exclude them. */
ss_hex *batch = ss_hex_read(workspace, NULL);
if (!batch) fail("hex");
size_t count = ss_hex_count(batch);
ss_hex_byte *bytes = malloc(sizeof *bytes * count);
ss_hex_copy(batch, 0, bytes, count);
printf("%02X at sample %lld\n", bytes[0].byte, (long long)bytes[0].start);
free(bytes);
ss_hex_free(batch);
/* Find a byte pattern; the offset in the hex view, or -1. */
const uint8_t pattern[] = { 0xDE, 0xAD, 0xBE, 0xEF };
int64_t offset = ss_data_find_hex(workspace, pattern, 4, 0, false, NULL);
One decoder's store is paged rather than read whole: ss_hex_read_instance
takes an offset and a count, ss_hex_total says how far to page, and a changed
ss_hex_generation means the store was cleared and anything cached is stale.
find_hex returns the offset of the match in the hex view, or nothing when
the pattern is not there.
detect_baud​
Estimates a channel's baud rate from the signal itself, using the shortest interval between transitions. Useful for an unlabelled UART.
- Python
- NodeJS
- Rust
- C
measured, standard = workspace.data.detect_baud(channel=4)
print(f"{measured:.0f} Bd, nearest standard rate {standard:.0f}")
// Two numbers: the rate measured, and the nearest standard rate.
const [measured, standard] = await workspace.data.detect_baud(4);
// Two numbers: the rate measured, and the nearest standard rate.
let (measured, standard) = workspace.data().detect_baud(4).await?;
double measured = 0.0, nearest = 0.0;
if (ss_data_detect_baud(workspace, 4, &measured, &nearest) == ss_status_Ok) {
printf("%.0f Bd, nearest standard rate %.0f\n", measured, nearest);
}
Either pointer may be NULL if you only want the other one.
Example output.
235849 Bd, nearest standard rate 230400
Returns: two numbers: the rate measured, and the nearest standard rate. Feed
the standard one into a UART decoder's baud option.
Detection needs transitions to work from. A quiet channel comes back as 0 for both numbers rather than as an error, so check the result before using it.