# Offline verdict for the 2026-09-22 primary-cause localization battery (v4). # usage: # python MDK-ARM/probe_classify.py A # boundary (one-shot or sustained) # python MDK-ARM/probe_classify.py B # status (hold) # python MDK-ARM/probe_classify.py C # partial drain # python MDK-ARM/probe_classify.py F # fast-recon chain (10 pairs) # runlog = the run's stdout capture (TS/STEP lines + HEX dump, ASCII) # seriallog = tools/serial_tool/state/serial.log (the session's log) # run_index = 1-based position of this run in the session (A/B/C) # or 0-based first connection index (F). # Sustained probe (MODE 2): the per-run eaten bytes = sum of POSITIVE probe-line # n's in the run's device-time window (TS lines in the runlog, 8h PC->device # offset + 4s margins; 15s run gaps keep windows disjoint). import re, sys from datetime import datetime, timezone, timedelta CST = timezone(timedelta(hours=8)) # PC local; device logs in UTC def pattern(base, n): return bytes(((base + i) & 0xFF) for i in range(n)) def delete_block(data, off, n): if n <= 0: return data return data[:off] + data[off + n:] def run_timestamps(path): ts = [] for raw in open(path, encoding="utf-8-sig", errors="replace"): m = re.match(r"TS (\d+)", raw.strip()) if m: ts.append(int(m.group(1))) return ts def read_run(path): steps, streams = {}, {} cur = None for raw in open(path, encoding="utf-8-sig", errors="replace"): line = raw.strip() m = re.match(r"PAIR (\d+)", line) or re.match(r"CONN (\d+) canary=\d", line) if m: cur = int(m.group(1)) continue if re.match(r"TS \d+", line): continue m = re.match(r"STEP (\S+) rx=(\d+)/(\d+) (OK|QUIET)", line) if m: steps[(cur, m.group(1))] = (int(m.group(2)), int(m.group(3)), m.group(4)) continue m = re.match(r"(HEX\w+) (\d{5}) ([0-9a-f]*)", line) if m: streams.setdefault(m.group(1), []).append( bytes.fromhex(m.group(3)) if m.group(3) else b"") return steps, {k: b"".join(v) for k, v in streams.items()} DEV_RE = re.compile(r"\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]\[") def probe_events(seriallog): """(device_utc_epoch, n) for every probe line, in log order.""" hits = [] for raw in open(seriallog, encoding="utf-8", errors="replace"): m = re.search(r"probe: recv64 n=(-?\d+)", raw) if not m: continue t = DEV_RE.search(raw) if not t: continue dt = datetime.strptime(t.group(1), "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc) hits.append((dt.timestamp(), int(m.group(1)))) return hits def run_eaten(events, ts, ts_next): """Sum of positive probe n's in this run's device-time window.""" if not ts: return None lo = ts[0] - 4.0 hi = (ts_next - 4.0) if ts_next is not None else (lo + 14.0) tot = 0 for t, n in events: if lo <= t < hi and n > 0: tot += n return tot def single_eaten(events, ts, ts_next): """One-shot probe: the (single) n in the run window; None if absent.""" if not ts: return None lo = ts[0] - 4.0 hi = (ts_next - 4.0) if ts_next is not None else (lo + 14.0) for t, n in events: if lo <= t < hi: return n return None def main(): mode, runlog, seriallog = sys.argv[1], sys.argv[2], sys.argv[3] base = int(sys.argv[4]) steps, streams = read_run(runlog) ts_all = run_timestamps(runlog) events = probe_events(seriallog) if mode == "F": for pair in range(10): ci, cj = 2 * pair, 2 * pair + 1 nA = single_eaten(events, [ts_all[ci]] if ci < len(ts_all) else [], [ts_all[cj + 1]] if cj + 1 < len(ts_all) else None) or 0 nB = single_eaten(events, [ts_all[cj]] if cj < len(ts_all) else [], [ts_all[cj + 1]] if cj + 1 < len(ts_all) else None) or 0 gotA = streams.get("HEXc%02dF1" % ci, b"") gotB1 = streams.get("HEXc%02dF1" % cj, b"") gotG = streams.get("HEXc%02dG" % cj, b"") wantA = 2048 - nA wantB1 = 2048 - nB a_ok = len(gotA) == wantA and gotA == delete_block(pattern(640, 2048), 0, nA) b1_ok = len(gotB1) == wantB1 and gotB1 == delete_block(pattern(640, 2048), 0, nB) expF2, expTail = pattern(2688, 2048), pattern(4736, 640) if len(gotG) < 2048 or gotG[:2048] != expF2: v = "WEDGE@F2" elif len(gotG) < 2688 or gotG[2048:] != expTail: v = "WEDGE@TAIL" else: v = "PASS" if not a_ok: v = "WEDGE@A" if len(gotA) < wantA else "MISMATCH@A" elif not b1_ok: v = "WEDGE@B1" if len(gotB1) < wantB1 else "MISMATCH@B1" print("PAIR %d: %-12s A %d/%d B1 %d/%d F2 %d/2688 (nA=%s nB=%s)" % (pair, v, len(gotA), wantA, len(gotB1), wantB1, len(gotG), nA, nB)) return ts = [ts_all[base - 1]] if base - 1 < len(ts_all) else [] ts_next = ts_all[base] if (base < len(ts_all) and len(ts_all) > 1) else None eaten = single_eaten(events, ts, ts_next) eaten_sum = run_eaten(events, ts, ts_next) or 0 eaten_pos = eaten if (eaten is not None and eaten > 0) else 0 f = streams.get("HEX", b"") if mode == "A": # dual expectation: one-shot (first probe line) or sustained (window sum) cands = {eaten_pos} if eaten_sum != eaten_pos: cands.add(eaten_sum) verdict = None for e in sorted(cands): want_f = 4096 - e exp_f = delete_block(pattern(2100, 4096), 2048, e) got_f, got_tail = f[:want_f], f[want_f:] if len(got_f) == want_f and got_f == exp_f and len(got_tail) == 640 \ and got_tail == pattern(6196, 640): verdict = ("PASS", e) break if verdict: print("VERDICT PASS | eaten=%d (probe n=%s, window sum=%d) | f %d/4096 + tail 640" % (verdict[1], eaten, eaten_sum, 4096 - verdict[1])) else: want = 4096 if len(f) < 2048: print("VERDICT STALL@F1 | probe n=%s sum=%d | stream %d bytes (fill1 echo lost)" % (eaten, eaten_sum, len(f))) elif len(f) < 4096 - eaten_sum: print("VERDICT STALL@F2 | probe n=%s sum=%d | stream %d bytes (fill2 echo cut)" % (eaten, eaten_sum, len(f))) else: print("VERDICT MISMATCH | probe n=%s sum=%d | stream %d bytes, bytes differ" % (eaten, eaten_sum, len(f))) elif mode == "B": want_f2 = 2048 - eaten_pos exp_f2 = delete_block(pattern(2688, 2048), 0, eaten_pos) exp_tail = pattern(4736, 640) got_f2, got_tail = f[:want_f2], f[want_f2:] if len(got_f2) == want_f2 and got_f2 == exp_f2 and len(got_tail) == 640 \ and got_tail == exp_tail: print("VERDICT PASS | probe n=%s (eaten=%d) | canary 2048 + tail 640" % (eaten, eaten_pos)) elif len(got_f2) < want_f2 or got_f2 != exp_f2[:len(got_f2)]: print("VERDICT STALL@F2 | probe n=%s (eaten=%d) | canary %d/%d" % (eaten, eaten_pos, len(got_f2), want_f2)) else: print("VERDICT STALL@TAIL | probe n=%s | tail %d/640" % (eaten, len(got_tail))) elif mode == "C": b1 = streams.get("HEX", b"") k = streams.get("HEX2", b"") want_b = 1460 - eaten_pos exp_b = delete_block(pattern(640, 1460), 1024, eaten_pos) if len(b1) < want_b or b1[:want_b] != exp_b: print("VERDICT STALL@B1460 | probe n=%s (eaten=%d) | fill part %d/%d" % (eaten, eaten_pos, len(b1), want_b)) return exp_k, exp_tail = pattern(2100, 1460), pattern(3560, 640) got_k, got_tail = k[:1460], k[1460:] if len(got_k) < 1460 or got_k != exp_k: print("VERDICT STALL@K1460 | probe n=%s | canary %d/1460" % (eaten, len(got_k))) elif len(got_tail) < 640 or got_tail != exp_tail: print("VERDICT STALL@TAIL | probe n=%s | tail %d/640" % (eaten, len(got_tail))) else: print("VERDICT PASS | probe n=%s (eaten=%d) | fill 1460 + canary 1460 + tail 640" % (eaten, eaten_pos)) else: print("unknown mode: %s" % mode) sys.exit(3) main()