A Python CLI tool that processes, queries, and verifies piclone bus-capture output.
Piclone is a Raspberry Pi Pico firmware that emulates ROM and clocks a W65C02S CPU. Its hardware capture command streams NDJSON bus cycles over USB-serial. Maximus turns that raw output into structured, testable results.
Built with uv.
- Parse raw piclone NDJSON into a clean cycle summary.
- Query captures with filters (
--addr,--data,--rw,--min-seq,--max-seq). - JSON Query — run declarative jsonquerylang expressions directly against the capture.
- Check — run multiple jsonquery assertions from a spec file and get a single pass/fail result.
- Verify (legacy) — verify captures against a YAML/JSON expectation list.
- CI-native — automatically writes GitHub Actions step summaries, annotations, and workflow outputs when running inside GHA.
uv syncuv run maximus parse capture.jsonlPrints a human-readable summary of every bus cycle.
Filter cycles by any combination of criteria. Each matching cycle prints on its own line.
# Show only writes to address 0200
uv run maximus query capture.jsonl --addr 0200 --rw 1
# Show writes of value 32 (hex 20) to 0200
uv run maximus query capture.jsonl --addr 0200 --data 20 --rw 1
# Show all reads in sequence range 10-20
uv run maximus query capture.jsonl --rw 0 --min-seq 10 --max-seq 20Output format:
seq=26 addr=0200 data=20 rw=W
Note: query always exits 0 even when nothing matches (empty output = no cycles matched the filters). For pass/fail verification, use check or jsonquery instead.
Run a jsonquerylang expression against the capture data. The capture is exposed as a JSON object with .cycles (list) and .result (object).
# Get the stop reason
uv run maximus jsonquery '.result.reason' capture.jsonl
# -> "stp"
# Count cycles
uv run maximus jsonquery '.cycles | size()' capture.jsonl
# -> 39
# Find all writes to 0200
uv run maximus jsonquery '.cycles | filter(.addr == "0200" and .rw == 1)' capture.jsonl --format human
# -> seq=26 addr=0200 data=00 rw=W
# -> seq=34 addr=0200 data=00 rw=WDefault output is JSON. Use --format human for readable tables.
Write a plain-text spec file where each line is Name -> jsonquery expression:
# checks.txt
Ends with STP -> .result.reason == "stp"
Exactly 39 cycles -> .cycles | size() == 39
Write to 0200 exists -> .cycles | filter(.addr == "0200" and .rw == 1) | size() > 0
Program starts at 8000 -> .cycles | filter(.addr == "8000") | size() > 0
Run it:
uv run maximus check capture.jsonl --spec checks.txtDefault JSON output:
{"pass":true,"results":[{"name":"Ends with STP","pass":true,"value":true},...]}Human-readable output:
uv run maximus check capture.jsonl --spec checks.txt --format human✅ Ends with STP
✅ Exactly 39 cycles
✅ Write to 0200 exists
✅ Program starts at 8000
[PASS] 4/4 checks passed
Exit codes:
0→ all checks passed1→ one or more checks failed
Since Romulan outputs NDJSON to stdout, you can pipe directly into Maximus:
uv run romulan hardware capture --max-cycles 500 | uv run maximus jsonquery '.result.reason'
uv run romulan hardware capture --max-cycles 500 | uv run maximus check --spec checks.txt --format humanReconstruct the 6502/65C02 instruction stream from captured ROM cycles. Validates opcode bytes, operand counts, and detects undefined opcodes or truncated instructions.
# Human-readable disassembly
uv run maximus decode capture.jsonl --format humanOutput:
Reset vector: $8000
8000: D8 CLD
8001: 18 CLC
8002: A9 05 LDA #$05
8004: 18 CLC
8005: 69 0A ADC #$0A
...
8016: DB STP
JSON output (default, for workflows):
uv run maximus decode capture.jsonl
# -> {"instructions":[{"addr":"8000","bytes":["D8"],"mnemonic":"CLD","mode":"imp","length":1,"valid":true},...],"reset_vector":"8000","valid":true,"errors":[]}Override start address:
uv run maximus decode capture.jsonl --start 8002 --format humanAdd --trace to annotate branches, jumps, jump-targets, and unreachable code:
uv run maximus decode capture.jsonl --format human --traceExample output for a program with branches:
Reset vector: $8000
8000: A9 00 LDA #$00
8002: F0 02 BEQ $8006 → 8006 *
8004: A9 01 LDA #$01
8006: D0 FA BNE $8002 → 8002 *
8008: DB STP
Annotations:
→ 8006— computed branch/jump target address*— this instruction is the target of a branch or jump[unreachable]— no execution path reaches this instruction (e.g., code afterSTPwith no jumps to it)
JSON output with trace metadata:
uv run maximus decode capture.jsonl --trace
# -> includes "annotations":{"8002":{"flow_type":"branch","target_addr":"8006","is_target":false},...}Compare the captured bytes against a 6502 assembly source file to catch mismatches:
uv run maximus decode capture.jsonl --source demo.s --format humanOutput:
Reset vector: $8000
8000: D8 CLD
...
8016: DB STP
✅ Cross-check: 25 bytes match
On mismatch:
❌ Cross-check failed (24 matches)
Mismatches:
8003: expected 05, captured 03
Missing in capture: 8017
Extra in capture: FFFF
JSON output includes the cross-check block:
uv run maximus decode capture.jsonl --source demo.s
# -> {"instructions":[...],"cross_check":{"matches":25,"mismatches":[],"missing":[],"extra":[],"valid":true}}Create a spec file (YAML or JSON):
# test_spec.yaml
expect:
- addr: "8000"
data: "D8"
rw: 0
label: "reset_vector"
- addr: "8001"
data: "18"
rw: 0
label: "CLC"
- addr: "0200"
data: "02"
rw: 1
label: "STA result"Run verification:
uv run maximus verify capture.jsonl --spec test_spec.yamlExit codes:
0→ all expectations matched1→ at least one expectation failed
Get machine-readable JSON output:
uv run maximus verify capture.jsonl --spec test_spec.yaml --json
# {"pass": true, "matched": 3, "total": 3, "failed_at": null, "message": "All expectations matched"}When GITHUB_ACTIONS=true is set in the environment, maximus check, maximus verify, and maximus decode automatically:
- Append a markdown summary to the job step summary.
- Emit a
::error::annotation on failure. - Write the JSON result to the
maximus_resultoutput variable for downstream steps.
Example workflow step:
- name: Verify piclone capture
run: |
romulan hardware capture --until stp --port /dev/ttyACM0 | uv run maximus check --spec specs/addition.txt --json
id: verify
- name: Fail on verification error
if: ${{ fromJson(steps.verify.outputs.maximus_result).pass == false }}
run: echo "Verification failed!"maximus/
├── src/maximus/
│ ├── __init__.py # Package version
│ ├── cli.py # argparse CLI (parse, query, jsonquery, check, decode, verify)
│ ├── models.py # Cycle, Capture, CaptureResult dataclasses
│ ├── parser.py # NDJSON ingestion
│ ├── query.py # Filtering / exploration
│ ├── verify.py # Assertion engine + spec loader (legacy)
│ ├── jsonquery_engine.py # jsonquerylang wrapper + formatting
│ ├── decode6502.py # 6502/65C02 opcode metadata + linear decoder
│ ├── asm_parser.py # Loose 6502 assembly parser for source cross-check
│ └── ci.py # GitHub Actions output helpers
├── tests/
│ ├── test_cli.py
│ ├── test_cli_jsonquery.py
│ ├── test_parser.py
│ ├── test_query.py
│ ├── test_verify.py
│ ├── test_jsonquery_engine.py
│ ├── test_decode.py
│ ├── test_decode_trace.py
│ └── test_asm_parser.py
├── pyproject.toml
└── README.md
MIT