Skip to main content

Decode a bus and export it

Capture a bus, run a protocol decoder over it, look at what came out, and write it to a CSV file for a spreadsheet, pandas or a report.

The program​

import sys
from collections import Counter

from ikalogic_scanastudio import ScanaStudio, Trigger

SCRIPT = "i2c.js"


def main(device: str = "se254") -> int:
with ScanaStudio.connect() as server:
print(server.info)
for available in server.devices():
print(" ", available.key, available.name)

workspace = server.create(device)
print(f"\nopened {workspace.device_name} "
f"with {len(workspace.channels)} channels")

last = workspace.capture.run(
samples=1_000_000,
sample_rate=25_000_000,
trigger=Trigger.rising(channel=0, position=0.1),
)
seconds = workspace.capture.seconds(last)
print(f"captured {last} samples ({seconds:.3f} s) "
f"at {workspace.capture.sample_rate} S/s")

if server.scripts.find(SCRIPT) is None:
print(f"\n{SCRIPT} is not installed; skipping the decode")
return 0

# What does this script take? Ask it.
print(f"\n{SCRIPT} takes:")
for option in workspace.decoders.options(SCRIPT):
print(" ", option)

# Channels by name, so the same script works on any device that has them.
i2c = workspace.decoders.add(
SCRIPT,
{
"ch_scl": workspace.channel("I2C SCL"),
"ch_sda": workspace.channel("I2C SDA"),
},
wait=True,
)

packets = list(i2c.packets())
print(f"\n{i2c.name} produced {len(packets)} packets:")
for packet in packets[:10]:
at = workspace.capture.seconds(packet.start)
content = f": {packet.content}" if packet.content else ""
print(f" {at:.6f}s {packet.title}{content}")

counts = Counter(p.title for p in packets)
print(" " + ", ".join(f"{t} x{n}" for t, n in counts.most_common()))

# The byte stream behind those packets.
payload = bytes(entry.byte for entry in i2c.hex_bytes())
print(f"\n{len(payload)} bytes decoded; first 16: {payload[:16].hex(' ')}")

workspace.export_csv(
"packets.csv", source="packets", instances=[i2c.instance_id])
print("\nwrote packets.csv (on the server's machine)")

workspace.close()
return 0


if __name__ == "__main__":
raise SystemExit(main(*sys.argv[1:]))

What it prints​

Against the se254 demo device:

ScanaStudio 6.0.23 (cd798ee), protocol 8
se254 SE254 (demo)
sq25 SQ25 (demo)
sq50 SQ50 (demo)
sq100 SQ100 (demo)
sq200 SQ200 (demo)
sp209 SP209 (demo)
...

opened SE254 (demo) with 4 channels
captured 1000000 samples (0.040 s) at 25000000 S/s

i2c.js takes:
ch_sda: SDA Channel (ch_selector) = None
ch_scl: SCL Channel (ch_selector) = None
AUTO0: Advanced options (tab) = False
address_opt [Advanced options]: Address convention (combo) one of ['7 bit address', '8 bit address (inlcuding R/W flag)'] = '7 bit address'
...

I2C produced 111 packets:
0.000009s I2C: CH4
0.000007s START
0.000014s STOP
0.000029s I2C: CH4
...
I2C x33, START x33, STOP x33, RE-START x12

0 bytes decoded; first 16:

wrote packets.csv (on the server's machine)

0 bytes decoded is the demo device showing through: the decoder found framing in the noise but no data bytes, so the hex view is empty. On real bus traffic that line reports the payload.

And packets.csv on the server looks like:

Time,I2C,DATA
0.000009160,I2C,CH4
0.000007360,START,
0.000013960,STOP,

Notes for your own bench​

Ask the script what it takes. decoders.options(script) prints every setting with its default. Do not guess option names from another decoder: a UART script wants baud, an SPI script wants a chip-select channel, and the names are the script's own.

Pick the right export source. Four are available, and they answer different questions:

sourceUse it for
packetsThe protocol view: addresses, commands, ACKs. Best for a report.
hexThe payload as bytes. Best for firmware and CRC work.
rawEvery element a decoder produced, with its exact span. Best for timing.
samplesThe logic levels themselves. Big, and the only one that needs no decoder.

Any other source raises ValueError in Python and RangeError in NodeJS before anything is sent to the server.

Write several files one after the other. Call export_csv once per file. Each call returns when its own export has finished, so the calls can follow each other directly, as the C example does. If another client is exporting from the same workspace at that moment, the call raises Busy.

Remember where the file lands. The server writes it. Pointing a local script at a remote server puts the CSV on the remote machine, not yours.

Filter on the server, not in your loop. packets(filter=...) narrows the read before the data is sent. See Reading captured data.

The workspace freezes while it exports

A CSV export freezes the workspace: every other command, from every other client, is refused with Busy until it finishes. Export at the end of a run, not in the middle of one.

Capture only the frames you want. The decoder that reads the bus can also start the capture: i2c.trigger({"trig_addr": "0x41"}) waits for a frame to 0x41 before recording, instead of capturing whatever the bus does after a rising edge. See Trigger on a protocol frame.

Chain decoders

A high-level decoder can sit on top of a low-level one (a sensor protocol over I²C, say). Add both; each one is an instance with its own packets, and packets() without a filter merges them in time order.