#!/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()