Skip to main content

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.

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")

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 edge is not necessarily inside your window

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.

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

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.

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

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.

FieldKeeps packets…
titlewhose title contains this text
contentwhose content contains this text
sourcesproduced by these decoder instance ids
channelson these channels
from, tooverlapping this sample range
dur_min, dur_maxof 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.

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.

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.

for group in workspace.data.packet_groups():
print(group.instance_id, group.start, group.children) # 1 1200 4

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.

for item in workspace.data.items(hidden=[uart.instance_id]):
print(item.start, item.content) # 517 STOP
Read one decoder's packets directly

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.

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)

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

CallReturns
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.

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)

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:

FieldMeaning
generation, totalAs for HexPage.
baseThe address the script suggests opening the view on, or nothing if it named none.
endianThe byte order the script names for its target: "little", "big", or nothing.
bytesEach 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.

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}

detect_baud​

Estimates a channel's baud rate from the signal itself, using the shortest interval between transitions. Useful for an unlabelled UART.

measured, standard = workspace.data.detect_baud(channel=4)
print(f"{measured:.0f} Bd, nearest standard rate {standard:.0f}")

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.

A channel with too little traffic measures 0

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.