forked from arti/vt1200
302 lines
13 KiB
Python
302 lines
13 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 12-byte body (frames_rx, crc_err, drops, csr0, heap_free_kb);
|
|
* the cadence and the verdict the brief asks for: at least three status
|
|
frames, the periodic ones at 64, 128, 192, 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) >= 3,
|
|
"%d status frames, expected at least 3 (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"
|
|
% (i, body["frame_no"], body["frames_rx"], body["crc_err"],
|
|
body["drops"], body["csr0"], body["heap_free_kb"]))
|
|
if len(bodies) != len(records):
|
|
print("vidstream_status_check: FAIL (%d of %d frames decoded)"
|
|
% (len(bodies), len(records)))
|
|
return 1
|
|
|
|
# --- the cadence -----------------------------------------------------
|
|
for i, want in enumerate((STATUS_EVERY, 2 * STATUS_EVERY, 3 * STATUS_EVERY)):
|
|
if i < len(bodies):
|
|
check(bodies[i]["frames_rx"] == want,
|
|
"status %d reports frames_rx=%d, expected %d (a report "
|
|
"every %d accepted frames)"
|
|
% (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 are consecutive. That is how the final count
|
|
# (200) is reached without waiting for 256.
|
|
after = bodies[3:]
|
|
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())
|