Files
vt1200/harness/mame/vidstream_status_check.py

318 lines
14 KiB
Python

#!/usr/bin/env python3
"""vidstream_status_check.py -- decode the terminal's status frames.
Task 6's host-side reading of the status path. In MAME the payload takes the
LANCE over for transmit and, every 64 accepted frames, builds a 60-byte
Ethernet frame in its own buffer and hands it to the chip
(harness/udpstream/vidstream.c, send_status()). The Lua side
(harness/mame/vidstream.lua, VT1200_MODE=stream) plays the sender: it announces
200 strip frames, then sets RX_EOF so the terminal reports the final count, and
dumps every status frame it saw in the payload's transmit buffer:
stream-status.bin each status frame exactly as the payload built it
(60 bytes each, back to back)
stream-status.txt key=value counters from the payload's marker page
This script is the independent decode: it uses harness/udpstream/frame.py, the
same module the host sender parses with, so the C encoder and the Python parser
cannot drift apart without a failure here. It checks:
* the Ethernet envelope (broadcast destination, the terminal's own address,
the stream's ethertype 0x88B7);
* the VS header (magic, version, KIND_STATUS, rows/stretch zero, payload_len
12, header CRC valid);
* the 16-byte body (frames_rx, crc_err, drops, csr0, heap_free_kb,
dict_state, decode_worst);
* the cadence and the verdict the brief asks for: the early report on the
first accepted frame (frames_rx == 1), then the periodic ones 64 accepted
frames later (65, 129, 193 -- the cadence is measured from the last
report), and the last reporting frames_rx == 200 with drops == 0 and
crc_err == 0;
* that the transmit descriptor actually completed (TMD1's OWN bit clear).
TMD1's ERR bit is expected to be set here and is not a failure: the LANCE
has no network medium in MAME (ruling R11), so the chip reports loss of
carrier after completing the DMA. The bytes and the descriptor are the
evidence this run can produce; the wire is Task 7's.
usage:
vidstream_status_check.py [--bin PATH] [--status PATH] [--frames 200]
Exit status 0 only if every check passes.
"""
import argparse
import os
import struct
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__))))
if ROOT not in sys.path:
sys.path.insert(0, ROOT)
from harness.udpstream.frame import ( # noqa: E402
BROADCAST, ETHERTYPE, HEADER_SIZE, KIND_STATUS, STATUS_SIZE, TERMINAL_MAC,
unpack_header, unpack_status)
STATUS_BYTES = 60 # STATUS_LEN in vidstream.c
STATUS_BODY = 14 + HEADER_SIZE # Ethernet header + VS header = 30
STATUS_EVERY = 64 # STATUS_EVERY in vidstream.c
CSR0_IDON = 0x0100
CSR0_TXON = 0x0010
TMD1_OWN = 0x8000
TMD1_ERR = 0x4000
failures = 0
def check(cond, what):
global failures
if not cond:
failures += 1
print("FAIL: " + what)
return cond
def read_file(path):
try:
with open(path, "rb") as f:
return f.read()
except OSError as e:
print("vidstream_status_check: cannot read %s: %s" % (path, e))
return None
def read_status(path):
st = {}
try:
with open(path, "r", encoding="ascii", errors="replace") as f:
for line in f:
if "=" in line:
k, v = line.strip().split("=", 1)
try:
st[k] = int(v, 0)
except ValueError:
st[k] = v
except OSError as e:
print("vidstream_status_check: cannot read %s: %s" % (path, e))
return None
return st
def decode_one(raw, n):
"""Decode one 60-byte captured frame; returns the body dict or None."""
tag = "status frame %d" % n
if not check(len(raw) == STATUS_BYTES,
"%s is %d bytes, expected %d"
% (tag, len(raw), STATUS_BYTES)):
return None
if not check(raw[0:6] == BROADCAST,
"%s destination is %s, expected broadcast"
% (tag, raw[0:6].hex(":"))):
return None
if not check(raw[6:12] == TERMINAL_MAC,
"%s source is %s, expected the terminal's %s"
% (tag, raw[6:12].hex(":"), TERMINAL_MAC.hex(":"))):
return None
if not check(raw[12:14] == struct.pack(">H", ETHERTYPE),
"%s ethertype is %s, expected 0x%04X"
% (tag, raw[12:14].hex(), ETHERTYPE)):
return None
try:
h = unpack_header(raw[14:14 + HEADER_SIZE])
except ValueError as e:
check(False, "%s header does not parse: %s" % (tag, e))
return None
if not check(h["magic"] == b"VS", "%s magic is %r" % (tag, h["magic"])):
return None
if not check(h["kind"] == KIND_STATUS,
"%s kind is %d, expected KIND_STATUS=%d"
% (tag, h["kind"], KIND_STATUS)):
return None
if not check(h["payload_len"] == STATUS_SIZE,
"%s payload_len is %d, expected %d"
% (tag, h["payload_len"], STATUS_SIZE)):
return None
if not check(h["rows"] == 0 and h["stretch"] == 0,
"%s reports geometry rows=%d stretch=%d; a status has none"
% (tag, h["rows"], h["stretch"])):
return None
if not check(h["crc_ok"], "%s header CRC is wrong" % tag):
return None
body = unpack_status(raw[STATUS_BODY:STATUS_BODY + STATUS_SIZE])
body["frame_no"] = h["frame_no"]
return body
def main():
ap = argparse.ArgumentParser()
d = os.environ.get("VID_DUMP_DIR", "/tmp/opencode")
ap.add_argument("--bin", default=os.path.join(d, "stream-status.bin"))
ap.add_argument("--status", default=os.path.join(d, "stream-status.txt"))
ap.add_argument("--frames", type=int, default=200,
help="frames the sender announced (VID_STREAM_FRAMES)")
args = ap.parse_args()
blob = read_file(args.bin)
st = read_status(args.status)
if blob is None or st is None:
return 2
check(len(blob) % STATUS_BYTES == 0,
"%s is %d bytes, not a whole number of %d-byte frames"
% (args.bin, len(blob), STATUS_BYTES))
records = [blob[i:i + STATUS_BYTES]
for i in range(0, len(blob) - STATUS_BYTES + 1, STATUS_BYTES)]
print("vidstream_status_check: %d status frame(s) in %s; counters from %s"
% (len(records), args.bin, args.status))
# --- the run's own record --------------------------------------------
if st.get("TIMEOUT", 0) != 0:
print("FAIL: the run timed out waiting for the final status "
"(injected=%r, status_count=%r, last frames_rx=%r)"
% (st.get("INJECTED"), st.get("STATUS_COUNT"),
st.get("LAST_FRAMES_RX")))
print("vidstream_status_check: FAIL (timeout)")
return 1
check(st.get("INJECTED") == args.frames,
"INJECTED=%r, expected %d" % (st.get("INJECTED"), args.frames))
check(st.get("RX_OK") == args.frames,
"the payload accepted RX_OK=%r frames, expected %d"
% (st.get("RX_OK"), args.frames))
check(st.get("RX_BAD") == 0,
"RX_BAD=%r: the payload rejected a frame it should have accepted"
% (st.get("RX_BAD"),))
check(st.get("BAD_STATUS_FRAMES", 0) == 0,
"BAD_STATUS_FRAMES=%r: the payload published something that is not "
"a VS status frame" % (st.get("BAD_STATUS_FRAMES"),))
check(st.get("STATUS_COUNT") == len(records),
"STATUS_COUNT=%r but the dump holds %d frames"
% (st.get("STATUS_COUNT"), len(records)))
check(st.get("EOF_SET") == 1,
"RX_EOF was never set, so the final report cannot exist")
# The LANCE is ours: IDON set. ST_INIT is CSR0 right after the INIT wait
# and BEFORE STRT, so TXON is not expected there; the transmitter is
# checked on the status body's own csr0 below.
st_init = st.get("ST_INIT", 0)
check(isinstance(st_init, int) and (st_init & CSR0_IDON) != 0,
"CSR0 after the INIT wait is %r; IDON (0x%04X) is not set, so the "
"init block was never read by the chip" % (st_init, CSR0_IDON))
# The last descriptor must have been given back: the chip consumed it.
# TMD1_DRAINED is read from the descriptor after the run waited for it;
# ST_TMD1 is the value the payload itself saw at send time (its wait is
# bounded, and the model completes a 60-byte frame in about 46 us: 10
# Mbit/s is m_bandwidth = 10 << 17 bytes/s in dinetwork.cpp).
tmd1 = st.get("TMD1_DRAINED", 0)
check(isinstance(tmd1, int) and (tmd1 & TMD1_OWN) == 0,
"the last TX descriptor still has OWN set after the drain wait "
"(TMD1=%r): the chip never took the status frame" % (tmd1,))
if isinstance(tmd1, int) and (tmd1 & TMD1_ERR):
print("note: the drained TMD1=0x%04X has ERR set -- expected: the "
"LANCE has no network medium in MAME (R11), so it reports loss "
"of carrier after completing the DMA" % tmd1)
at_send = st.get("ST_TMD1", 0)
if isinstance(at_send, int) and (at_send & TMD1_OWN):
print("note: the descriptor was still owned when the payload finished "
"its bounded wait (ST_TMD1=0x%04X); the drain check above is "
"what decides whether the chip ever took it" % at_send)
# --- the bytes -------------------------------------------------------
if not check(len(records) >= 4,
"%d status frames, expected at least 4 (the first accepted "
"frame plus three periodic reports every %d accepted frames)"
% (len(records), STATUS_EVERY)):
print("vidstream_status_check: FAIL")
return 1
bodies = []
for i, raw in enumerate(records, 1):
body = decode_one(raw, i)
if body is None:
continue
bodies.append(body)
print(" frame %d: frame_no=%d frames_rx=%d crc_err=%d drops=%d "
"csr0=0x%04X heap_free_kb=%d dict_state=0x%02X "
"decode_worst=%d"
% (i, body["frame_no"], body["frames_rx"], body["crc_err"],
body["drops"], body["csr0"], body["heap_free_kb"],
body["dict_state"], body["decode_worst"]))
if len(bodies) != len(records):
print("vidstream_status_check: FAIL (%d of %d frames decoded)"
% (len(bodies), len(records)))
return 1
# --- the cadence -----------------------------------------------------
# The first accepted frame forces an early report, so the periodic ones
# start at index 1. That early frame is also the only place the stream
# path can show dict_state: no dictionary is uploaded here, so it is 0.
check(bodies[0]["frames_rx"] == 1,
"the first status reports frames_rx=%d, expected the early report "
"on the first accepted frame (1)" % (bodies[0]["frames_rx"],))
check(bodies[0]["dict_state"] == 0,
"the early status reports dict_state=0x%02X, expected 0 (no "
"dictionary in a strip stream)" % (bodies[0]["dict_state"],))
for i, want in enumerate((STATUS_EVERY + 1, 2 * STATUS_EVERY + 1,
3 * STATUS_EVERY + 1), start=1):
if i < len(bodies):
check(bodies[i]["frames_rx"] == want,
"status %d reports frames_rx=%d, expected %d (a report "
"every %d accepted frames after the one at frame 1)"
% (i + 1, bodies[i]["frames_rx"], want, STATUS_EVERY))
for i in range(1, len(bodies)):
check(bodies[i]["frames_rx"] > bodies[i - 1]["frames_rx"],
"status %d reports frames_rx=%d, which does not advance past "
"the previous %d" % (i + 1, bodies[i]["frames_rx"],
bodies[i - 1]["frames_rx"]))
# After RX_EOF the terminal reports each further accepted frame, so the
# reports after the third periodic one are consecutive. That is how the
# final count (200) is reached without waiting for 256.
after = bodies[4:]
for i in range(1, len(after)):
check(after[i]["frames_rx"] == after[i - 1]["frames_rx"] + 1,
"post-EOF report %d jumps from %d to %d; the terminal reports "
"each further accepted frame"
% (len(bodies) - len(after) + i,
after[i - 1]["frames_rx"], after[i]["frames_rx"]))
# --- the brief's verdict --------------------------------------------
last = bodies[-1]
check(last["frames_rx"] == args.frames,
"the last status reports frames_rx=%d, expected %d"
% (last["frames_rx"], args.frames))
check(last["drops"] == 0,
"the last status reports drops=%d, expected 0" % last["drops"])
check(last["crc_err"] == 0,
"the last status reports crc_err=%d, expected 0" % last["crc_err"])
# The transmitter must have been on when the reports were built: the
# payload ran STRT once, and this is the chip's own answer at report time.
check((last["csr0"] & CSR0_TXON) != 0,
"the last status reports csr0=0x%04X with TXON (0x%04X) clear, so "
"the transmitter was never started"
% (last["csr0"], CSR0_TXON))
# The free-heap field must be a reading, not the zero padding a wrong
# offset would pick up.
check(last["heap_free_kb"] > 0,
"heap_free_kb=%d: the heap field looks like padding"
% last["heap_free_kb"])
# The Lua's own decode of the same frame must agree with this one.
if "LAST_FRAMES_RX" in st:
check(st["LAST_FRAMES_RX"] == last["frames_rx"],
"stream-status.txt says LAST_FRAMES_RX=%r but the decoded last "
"frame says %d" % (st["LAST_FRAMES_RX"], last["frames_rx"]))
print("vidstream_status_check: %d/%d status frames decoded, last reports "
"frames_rx=%d drops=%d crc_err=%d heap_free_kb=%d"
% (len(bodies), len(records), last["frames_rx"], last["drops"],
last["crc_err"], last["heap_free_kb"]))
if failures:
print("vidstream_status_check: FAIL (%d checks)" % failures)
return 1
print("vidstream_status_check: PASS")
return 0
if __name__ == "__main__":
sys.exit(main())