Reading captured data
Everything the capture produced: raw transitions, decoded packets, the Raw
View, the hex view and a decoder's Memory 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 (EDGE_PAGE,
200 000 edges per request).
Reading a generation row​
On a device that generates signals, each channel also has a generation row: the
signal the next generation replays. In Python and NodeJS, generator=True
reads that row instead of the capture, on transitions and level_at:
for edge in workspace.data.transitions(channel=0, generator=True):
print(edge.sample, edge.level)
workspace.data.level_at(channel=0, sample=1_000, generator=True)
for await (const edge of workspace.data.transitions(0, { generator: true })) { /* ... */ }
await workspace.data.level_at(0, 1_000, { generator: true });
Rust and C read generation rows through the generation API:
workspace.generation().transitions(...) / level_at(...) and
ss_generation_transitions. See
generation.
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. next_edge and previous_edge
read the capture only.
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"...
packet.title_bg # "#FF0000": the colours the script chose
packet.content_bg
# Packets are fetched 5000 at a time (PAGE); `page` changes that.
for packet in workspace.data.packets(page=1000):
...
// 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.channel;
packet.start; // sample index
packet.end;
packet.title; // "Address", "Data", "Start"...
packet.content; // "0x4E", "ACK"...
packet.title_bg; // "#FF0000": the colours the script chose
packet.content_bg;
}
// Packets are fetched 5000 at a time (PAGE); `page` changes that.
workspace.data.packets({ page: 1000 });
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 | overlapping this sample range |
dur_min, dur_max | of at least / at most this duration, in samples |
Text matching is case-insensitive. In the Packet View a group is kept when its root matches, or when one of its children does; the root then shows above the matching children.
- 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])))
late = list(workspace.data.packets(filter=RowFilter(from_=100_000, to=200_000)))
from is a Python keyword, so the field is written from_. RowFilter
accepts unknown keywords without an error and the server ignores them: a
misspelt field such as titel="Error" filters nothing. Check the spelling when
a filter keeps everything.
// 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);
packet_groups​
One entry per packet group, in Packet View order: the root's instance_id and
start, which together identify the group, and children, the number of
packets that follow the root. Use it to count frames or to collapse groups
without reading every packet. It takes the same filter as packets.
- Python
- NodeJS
- Rust
- C
for group in workspace.data.packet_groups():
print(group.instance_id, group.start, group.children) # 1 1200 4
for (const group of await workspace.data.packet_groups()) {
console.log(group.instance_id, group.start, group.children);
}
let groups = workspace.data().packet_groups(&RowFilter::default()).await?;
/* Not available in the C library: count the packets with root set instead. */
items​
The Raw View: every item the decoders emitted, merged across decoders and
sorted by start sample. Each row has instance_id, channel, start, end,
content (the longest form the script gave) and emphasis (the style the
script asked for: success, warning or error). It takes a filter, and hidden,
a list of decoder instance ids to leave out.
- Python
- NodeJS
- Rust
- C
for item in workspace.data.items(hidden=[uart.instance_id]):
print(item.start, item.content) # 517 STOP
for await (const item of workspace.data.items({ hidden: [uart.instance_id] })) {
console.log(item.start, item.content);
}
let items = workspace.data().items(&RowFilter::default(), &[uart.instance_id]).await?;
/* filter_json, then hidden_json; NULL for either keeps everything. */
ss_items *batch = ss_items_read(workspace, NULL, NULL);
size_t count = ss_items_count(batch);
ss_item *items = malloc(sizeof *items * count);
ss_items_copy(batch, 0, items, count);
/* items[i].content points into the batch */
free(items);
ss_items_free(batch);
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(hidden=[uart.instance_id]): # leave UART out
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({ hidden: [uart.instance_id] })) {
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.
hidden on hex and find_hex is a list of decoder instance ids to leave
out of the merged view. Offsets returned by find_hex are positions in that
same view, with the hidden decoders left out. The search starts at start and
wraps around the end of the view (or its beginning, with backwards).
One decoder's store​
The merged views above combine every decoder. These read one decoder instance, straight from its own store. They are also available on the decoder object (see Decoders).
| Call | Returns |
|---|---|
instance_items(id, start=0, end=None, min_length=0) | The decoder's items over a sample window, each with channel, start, end, emphasis and contents (every form the script gave, longest first). min_length drops items shorter than that many samples. |
instance_hex(id, offset=0, count=5000) | A HexPage: generation, total, and bytes (each channel, value, start, end). |
instance_memory(id, offset=0, count=5000) | A MemPage: the decoder's Memory View. |
A decoder's hex and memory stores only grow until they are cleared, so a
program that polls them reads only the tail: keep the offset you reached and
ask from there. A generation that differs from the one you saw last means the
store was cleared, for example by a relaunch: drop what you cached and start
again from offset 0. total is the size of the whole store.
- Python
- NodeJS
- Rust
- C
items = workspace.data.instance_items(i2c.instance_id, start=0, end=100_000)
items[0].contents # ['STOP', 'SP', 'P']
page = workspace.data.instance_hex(uart.instance_id, offset=0, count=5000)
page.generation, page.total
for b in page.bytes:
print(b.value, b.start)
const items = await workspace.data.instance_items(i2c.instance_id, { start: 0, end: 100_000 });
const page = await workspace.data.instance_hex(uart.instance_id, { offset: 0, count: 5000 });
let items = workspace.data().instance_items(i2c.instance_id, 0, Some(100_000), 0).await?;
let page = workspace.data().instance_hex(uart.instance_id, 0, 5000).await?;
ss_items *items = ss_items_read_instance(workspace, i2c, 0, 100000, 0);
ss_hex *page = ss_hex_read_instance(workspace, uart, 0, 5000);
printf("generation %u, total %u\n", ss_hex_generation(page), ss_hex_total(page));
ss_items_free(items);
ss_hex_free(page);
Memory View​
A decoder that reads a target's memory (an EEPROM, a flash, a register file)
can publish what it read in a Memory View. Each byte sits at the target's
address, not at a sample index. A MemPage holds:
| Field | Meaning |
|---|---|
generation, total | As for HexPage. |
base | The address the script suggests opening the view on, or nothing if it named none. |
endian | The byte order the script names for its target: "little", "big", or nothing. |
bytes | Each with address and value. |
An address that appears on no page was never read by the script, which is not the same as a zero.
- Python
- NodeJS
- Rust
- C
mem = workspace.data.instance_memory(decoder.instance_id)
if mem.base is not None:
print(f"opens at 0x{mem.base:08X}, {mem.endian or 'no'} byte order")
dump = {b.address: b.value for b in mem.bytes}
const mem = await workspace.data.instance_memory(decoder.instance_id);
const dump = new Map(mem.bytes.map((b) => [b.address, b.value]));
// base: Option<u32>, endian: Option<Endian>
let mem = workspace.data().instance_memory(decoder.instance_id, 0, 5000).await?;
ss_mem *mem = ss_mem_read_instance(workspace, decoder, 0, 5000);
uint32_t base;
if (ss_mem_base(mem, &base)) printf("opens at 0x%08X\n", base);
uint32_t endian = ss_mem_endian(mem); /* 0 none, 1 little, 2 big */
size_t count = ss_mem_count(mem);
ss_mem_byte *bytes = malloc(sizeof *bytes * count);
ss_mem_copy(mem, 0, bytes, count);
free(bytes);
ss_mem_free(mem);
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.