Skip to main content

Automated bench & end-of-line test

Arm a trigger, capture the board under test, decode the bus, check the result, and exit with a pass/fail code. This is the shape of an end-of-line tester, and of a CI job that checks firmware for regressions.

The run is unattended: no window, no buttons, no operator.

The program​

"""Pass/fail an I2C board test. Exit code 0 passes, 1 fails, 2 is a bench fault."""

import sys
from collections import Counter

from ikalogic_scanastudio import (
ScanaStudio, ScanaStudioError, Refused, ScriptError, Timeout, Trigger,
)

DEVICE = "se254"
SCRIPT = "i2c.js"
EXPECTED_ADDRESS = "0x4E"


def run() -> int:
with ScanaStudio.connect() as server:
workspace = server.create(DEVICE)

# Check the script library after creating the workspace: the list
# arrives shortly after connect() and may still be empty before that.
if server.scripts.find(SCRIPT) is None:
workspace.close()
print(f"{SCRIPT} is not installed on the server", file=sys.stderr)
return 2

try:
# Arm on the board's reset line going high, keeping 10% of history.
workspace.capture.run(
samples=2_000_000,
sample_rate=25_000_000,
trigger=Trigger.rising(channel=workspace.channel("RESET"), position=0.1),
timeout=30.0,
)

i2c = workspace.decoders.add(
SCRIPT,
{
"ch_scl": workspace.channel("I2C SCL"),
"ch_sda": workspace.channel("I2C SDA"),
},
wait=True,
)

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

# --- the assertions -------------------------------------------
failures = []

if not packets:
failures.append("the bus was silent")

if not any(p.title == "Address" and p.content == EXPECTED_ADDRESS
for p in packets):
failures.append(f"never saw address {EXPECTED_ADDRESS}")

nacks = counts.get("NACK", 0)
if nacks:
failures.append(f"{nacks} NACK(s) on the bus")

if failures:
for failure in failures:
print(f"FAIL: {failure}", file=sys.stderr)
workspace.save("/data/failures/last-failure.scana")
return 1

print("PASS")
return 0
finally:
workspace.close()


def main() -> int:
try:
return run()
except (Refused, Timeout, ScriptError) as exc:
print(f"bench fault: {exc}", file=sys.stderr)
return 2
except ScanaStudioError as exc:
print(f"bench fault: {exc}", file=sys.stderr)
return 2


if __name__ == "__main__":
raise SystemExit(main())

What it prints​

A failing run. This is the demo device, which produces noise rather than I²C traffic, so the address assertion never passes:

144 packets: I2C x41, START x41, STOP x41, RE-START x21
FAIL: never saw address 0x4E

The exit code is 1, and /data/failures/last-failure.scana is written so you can open it in ScanaStudio afterwards.

A passing run against real traffic prints the packet counts and then:

PASS

with exit code 0. A bench fault (no device, a missing script, a timeout) prints the server's reason to stderr and exits 2:

bench fault: <the reason, as the server phrased it>

Print those reasons as they are rather than paraphrasing them. They tell an operator whether to re-seat a probe or call someone.

Notes for your own bench​

Three exit codes, not two. 0 passed, 1 the board failed, 2 the bench failed (no device, missing script, a timeout). A CI job that cannot tell those apart may pass a bad board because the analyser was unplugged.

Trigger on the event. Trigger.rising(...) with a position keeps history from before the event, so a failure shows you the run-up and not just the aftermath. See Triggers for pulse and sequence triggers when a plain edge is too broad.

Save the failures. workspace.save() on the failing path gives you a .scana you can open in ScanaStudio later to see why a board failed.

Look up channels by name. workspace.channel("I2C SCL") keeps the same script running on an SE254 at the desk and an SP259 on the line.

Check the script library after creating the workspace. The list arrives shortly after connect() returns, so reading it too early can show every decoder as missing. See Decoders.

Assert on protocol content, not on counts alone. "12 packets" passes for the wrong 12. Check for the address you expect, and for the errors you do not.

This example cannot pass on a demo device

se254 generates pseudo-random noise, so the I²C decoder finds packets by chance and never the address you are looking for. Use the demo device to check that your script runs; use hardware to check that it judges correctly.

Run several boards without reconnecting

The connection and the workspace are the expensive parts. Open once, then loop over capture.run() and the assertions per board. A decoder can be relaunched with relaunch() instead of re-added.