188 lines
6.9 KiB
Python
188 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
# Reusable bench probe (non-FTP :7003 close->reopen boundary; lives in MDK-ARM/bench_diag/).
|
|
# Non-FTP boundary probe: the same close->reopen recycle, but on the raw RemoDispBus
|
|
# service (:7003, Drivers/BSP/UI/ui_net_sink.c), which shares the net/chip layer with FTP
|
|
# and none of lftpd's code. If a rapid recycle damages data here too, the defect lives in
|
|
# the net/chip layer and FTP only makes it visible.
|
|
#
|
|
# Session A (the trigger): connect -> CMD_INIT -> CMD_LCDMEM (3209B, CRC) -> [--burst N
|
|
# keepalive frames queued unread = data still in flight at abort] -> abort (RST / --fin).
|
|
# Gap G ms.
|
|
# Session B (the victim): connect -> [--burst N keepalive frames -> count the echoed acks,
|
|
# this is the RX direction analogue of "upload truncated to 2048B"] -> CMD_INIT ->
|
|
# CMD_LCDMEM -> frame length + CRC (device->client analogue).
|
|
#
|
|
# Classification per round:
|
|
# RX_SHORT : session B echoed fewer acks than frames sent (client->device data cut short)
|
|
# TX_SHORT : session B LCDMEM frame short of 3209 (device->client data cut short)
|
|
# CORRUPT : full length but CRC mismatch / unexpected frame
|
|
# OK : everything intact
|
|
import argparse, socket, struct, sys, time
|
|
|
|
TAG_CLIENT = 0xAA
|
|
TAG_DEVICE = 0xBB
|
|
CMD_KEEP = 0
|
|
CMD_INIT = 1
|
|
CMD_LCDMEM = 3
|
|
MEMSIZE = 3200
|
|
LCDMEM_TOTAL = 4 + 4 + MEMSIZE + 1 # 3209
|
|
ACK_LEN = 5 # empty frame reply to a keepalive
|
|
|
|
|
|
def log(m):
|
|
sys.stdout.write(m + "\n")
|
|
sys.stdout.flush()
|
|
|
|
|
|
def xor_bytes(b):
|
|
c = 0
|
|
for x in b:
|
|
c ^= x
|
|
return c
|
|
|
|
|
|
def req(cmd, data=b""):
|
|
body = bytes(data)
|
|
return (bytes([TAG_CLIENT, cmd, (len(body) >> 8) & 0xFF, len(body) & 0xFF]) + body
|
|
+ bytes([xor_bytes(body)]))
|
|
|
|
|
|
def read_frame(s, deadline):
|
|
"""Read exactly one device frame; returns total/got/crc_ok/short."""
|
|
buf = bytearray()
|
|
total = None
|
|
while True:
|
|
want = (4 - len(buf)) if total is None else (total - len(buf))
|
|
if want <= 0:
|
|
break
|
|
left = deadline - time.monotonic()
|
|
if left <= 0:
|
|
return {"total": (total or 0), "got": len(buf), "crc_ok": False, "short": True}
|
|
s.settimeout(left)
|
|
try:
|
|
d = s.recv(min(want, 4096))
|
|
except socket.timeout:
|
|
return {"total": (total or 0), "got": len(buf), "crc_ok": False, "short": True}
|
|
except OSError:
|
|
d = b""
|
|
if not d:
|
|
return {"total": (total or 0), "got": len(buf), "crc_ok": False, "short": True}
|
|
buf.extend(d)
|
|
if total is None and len(buf) >= 4:
|
|
total = 4 + (((buf[2] << 8) | buf[3])) + 1
|
|
body = bytes(buf[4:total - 1])
|
|
return {"total": total, "got": len(buf), "crc_ok": (buf[total - 1] == xor_bytes(body)),
|
|
"short": (len(buf) != total)}
|
|
|
|
|
|
def read_n(s, want, deadline):
|
|
"""Read up to `want` bytes; stops at deadline / EOF. Returns bytes."""
|
|
buf = bytearray()
|
|
while len(buf) < want:
|
|
left = deadline - time.monotonic()
|
|
if left <= 0:
|
|
break
|
|
s.settimeout(left)
|
|
try:
|
|
d = s.recv(min(want - len(buf), 4096))
|
|
except socket.timeout:
|
|
break
|
|
except OSError:
|
|
break
|
|
if not d:
|
|
break
|
|
buf.extend(d)
|
|
return bytes(buf)
|
|
|
|
|
|
def abort_sock(s, fin):
|
|
try:
|
|
if fin:
|
|
s.shutdown(socket.SHUT_RDWR)
|
|
else:
|
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0))
|
|
except OSError:
|
|
pass
|
|
try:
|
|
s.close()
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--ip", default="192.168.1.100")
|
|
ap.add_argument("--port", type=int, default=7003)
|
|
ap.add_argument("--n", type=int, default=8)
|
|
ap.add_argument("--gap", type=int, default=0, help="abort -> reconnect gap in ms")
|
|
ap.add_argument("--fin", action="store_true")
|
|
ap.add_argument("--burst", type=int, default=400,
|
|
help="keepalive frames used to load the RX direction (0 = TCP-only test)")
|
|
ap.add_argument("--no-abort", action="store_true", help="no session A (baseline)")
|
|
a = ap.parse_args()
|
|
|
|
res = {"OK": 0, "RX_SHORT": 0, "TX_SHORT": 0, "CORRUPT": 0, "ERR": 0}
|
|
log("== sink recycle: port=%d gap=%dms abort=%s burst=%d n=%d"
|
|
% (a.port, a.gap, "NONE" if a.no_abort else ("FIN" if a.fin else "RST"),
|
|
a.burst, a.n))
|
|
for i in range(1, a.n + 1):
|
|
try:
|
|
# ---- session A: trigger ----
|
|
if not a.no_abort:
|
|
s1 = socket.create_connection((a.ip, a.port), timeout=10.0)
|
|
s1.sendall(req(CMD_INIT))
|
|
read_frame(s1, time.monotonic() + 10.0)
|
|
if a.burst:
|
|
s1.sendall(req(CMD_KEEP) * a.burst)
|
|
s1.sendall(req(CMD_LCDMEM, bytes(4)))
|
|
f1 = read_frame(s1, time.monotonic() + 10.0)
|
|
abort_sock(s1, a.fin)
|
|
t_ab = time.monotonic()
|
|
else:
|
|
f1 = {"got": -1, "crc_ok": True}
|
|
t_ab = time.monotonic()
|
|
# ---- gap ----
|
|
if a.gap > 0:
|
|
time.sleep(a.gap / 1000.0)
|
|
# ---- session B: victim ----
|
|
s2 = socket.create_connection((a.ip, a.port), timeout=10.0)
|
|
rx_bytes = 0
|
|
if a.burst:
|
|
s2.sendall(req(CMD_KEEP) * a.burst)
|
|
got = read_n(s2, a.burst * ACK_LEN, time.monotonic() + 10.0)
|
|
rx_bytes = len(got)
|
|
s2.sendall(req(CMD_INIT))
|
|
ini = read_frame(s2, time.monotonic() + 10.0)
|
|
s2.sendall(req(CMD_LCDMEM, bytes(4)))
|
|
f2 = read_frame(s2, time.monotonic() + 10.0)
|
|
dt = (time.monotonic() - t_ab) * 1e3
|
|
abort_sock(s2, a.fin)
|
|
rx_want = a.burst * ACK_LEN
|
|
rx_short = a.burst and rx_bytes < rx_want
|
|
tx_short = (f2["got"] != LCDMEM_TOTAL) or (ini["got"] != 13)
|
|
if rx_short:
|
|
cls = "RX_SHORT"
|
|
elif tx_short:
|
|
cls = "TX_SHORT"
|
|
elif not (f2["crc_ok"] and ini["crc_ok"]):
|
|
cls = "CORRUPT"
|
|
else:
|
|
cls = "OK"
|
|
res[cls] += 1
|
|
log(" %2d A lcd=%4d crc=%-4s | B ack=%5d/%5d init=%2dB lcd=%4d/%d crc=%-4s "
|
|
"dt=%6.0fms -> %s"
|
|
% (i, f1["got"], "OK" if f1["crc_ok"] else "BAD", rx_bytes, rx_want,
|
|
ini["got"], f2["got"], LCDMEM_TOTAL,
|
|
"OK" if f2["crc_ok"] else "BAD", dt, cls))
|
|
except Exception as e:
|
|
res["ERR"] += 1
|
|
log(" %2d EXC %s: %s" % (i, type(e).__name__, str(e)[:70]))
|
|
time.sleep(0.3)
|
|
log("SUMMARY port=%d gap=%dms abort=%s burst=%d n=%d ok=%d rx_short=%d tx_short=%d "
|
|
"corrupt=%d err=%d"
|
|
% (a.port, a.gap, "NONE" if a.no_abort else ("FIN" if a.fin else "RST"), a.burst,
|
|
a.n, res["OK"], res["RX_SHORT"], res["TX_SHORT"], res["CORRUPT"], res["ERR"]))
|
|
|
|
|
|
main()
|