入库台架工具:真实路径冻结/中止/重连复现与判读脚本,含已撤回台架归档(带README)

This commit is contained in:
2026-09-23 07:27:42 +08:00
parent 77ee778605
commit 1ae6f0321a
21 changed files with 5277 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Re-audit every bench run log by its SCRIPT-LEVEL step lines (ground truth:
# "STEP <label> rx=<got>/<want> OK|QUIET"). probe_classify verdicts are advisory
# only; a real freeze shows a short/QUIET step here.
# ASCII output only.
import glob, os, re
def audit(path):
steps = []
for line in open(path, encoding="utf-8-sig", errors="replace"):
m = re.match(r"STEP (\S+) rx=(\d+)/(\d+) (OK|QUIET)", line.strip())
if m:
steps.append((m.group(1), int(m.group(2)), int(m.group(3)), m.group(4)))
bad = [s for s in steps if s[3] != "OK" or s[1] != s[2]]
return steps, bad
groups = {}
for path in sorted(glob.glob("MDK-ARM/ab_*.log")) + sorted(glob.glob("MDK-ARM/reg_*.log")):
name = os.path.basename(path)
key = re.sub(r"_run\d+\.log$", "", name)
steps, bad = audit(path)
groups.setdefault(key, []).append((name, len(steps), bad))
for key in sorted(groups):
runs = groups[key]
total = len(runs)
clean = sum(1 for _, n, bad in runs if n > 0 and not bad)
frozen = [name for name, n, bad in runs if n > 0 and bad]
nostep = [name for name, n, bad in runs if n == 0]
print("%-16s runs=%2d script-OK=%2d script-FAIL=%2d %s" %
(key, total, clean, len(frozen), ("FAIL:" + ",".join(x.replace(key + "_", "") for x in frozen)) if frozen else ""))
if nostep:
print(" (no STEP lines: %s)" % ",".join(nostep))
+33
View File
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Audit tcp_client-style run logs (the E / V1 series) by their own step markers.
# Real freeze = a step was sent but its match line never appeared (long gap).
# ASCII output only.
import glob, os, re, sys
def clean(s):
return re.sub(r"\x1b\[[0-9;]*m", "", s)
def audit(path):
sent = {}
matched = set()
order = []
for line in open(path, encoding="utf-8-sig", errors="replace"):
c = clean(line)
m = re.search(r"\[[\d:.]+\]\s+\[\.\.\]\s+.*?(\d+):\s+(\d+)B", c)
if m:
step = int(m.group(1))
if step not in sent:
sent[step] = int(m.group(2))
order.append(step)
m2 = re.search(r"\[[\d:.]+\]\s+\[OK\]\s+(\d+)B\s", c)
if m2:
matched.add(int(m2.group(1)))
return order, sent, matched
for path in sorted(glob.glob("MDK-ARM/ab_E*.log")) + sorted(glob.glob("MDK-ARM/ab_V1_run*.log")) + sorted(glob.glob("MDK-ARM/ab_V1f_run*.log")):
order, sent, matched = audit(path)
res = []
for step in order:
size = sent[step]
res.append("s%d(%dB)=%s" % (step, size, "OK" if size in matched else "NO-ECHO"))
print("%-22s %s" % (os.path.basename(path), " ".join(res) if res else "(no step lines)"))
+90
View File
@@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
# Authoritative bench verdict (2026-09-23), replacing probe_classify mode A/C
# verdicts which were proven WRONG (they reported STALL on runs whose script-level
# STEP lines show all bytes received, and ignored in-firmware readers that consume
# peer data without echoing it).
#
# Ground truth = the run log's own STEP lines printed by probe_bench_lib:
# STEP <label> rx=<got>/<want> OK|QUIET
# plus the firmware's eaten-byte log on the serial line:
# probe: recv64 n=<N> (test probe)
# NOTE: Phase 9/11 style in-firmware readers consume peer bytes WITHOUT logging.
# For such builds a shortfall is EXPECTED, so this tool reports the shortfall
# and whether it can be explained by logged eaten bytes:
# PASS - every step got its full byte count
# EXPECTED-SHORT - shortfall <= logged eaten bytes (test-reader artifact)
# SUSPECT-FREEZE - shortfall > logged eaten bytes, or a step got 0 bytes
# while no logged reader could explain it
# Only a build with NO non-echoing reader (e.g. Phase 6 only) can yield a
# meaningful freeze verdict.
# ASCII output only.
import sys, re
def clean(s):
return re.sub(r"\x1b\[[0-9;]*m", "", s)
def parse_run(path):
steps = []
ts = None
for line in open(path, encoding="utf-8-sig", errors="replace"):
c = clean(line)
m = re.match(r"TS (\d+)", c.strip())
if m and ts is None:
ts = int(m.group(1))
m = re.match(r"STEP (.+?) rx=(\d+)/(\d+) (OK|QUIET)", c.strip())
if m:
steps.append((m.group(1), int(m.group(2)), int(m.group(3)), m.group(4)))
return ts, steps
def parse_eaten(serial_path, ts, window_s=20):
"""sum of probe eaten bytes inside the run window [ts, ts+window)"""
eaten = 0
n = 0
for line in open(serial_path, encoding="utf-8-sig", errors="replace"):
m = re.search(r"\[(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})\].*probe: recv64 n=(-?\d+)", line)
if not m:
continue
# device UTC stamp; run TS is UTC epoch -> convert both to epoch
import calendar
ep = calendar.timegm((int(m.group(1)), int(m.group(2)), int(m.group(3)),
int(m.group(4)), int(m.group(5)), int(m.group(6)), 0, 0, 0))
if ts is not None and ts - 4 <= ep < ts + window_s:
v = int(m.group(7))
if v > 0:
eaten += v
n += 1
return eaten, n
def main():
run_log, serial_log = sys.argv[1], sys.argv[2]
silent_reader = (len(sys.argv) > 3 and sys.argv[3] == "1")
ts, steps = parse_run(run_log)
if not steps:
print("VERDICT UNKNOWN: no STEP lines in run log (not a probe-script run)")
return 2
eaten, nev = parse_eaten(serial_log, ts)
total_got = sum(s[1] for s in steps)
total_want = sum(s[2] for s in steps)
shortfall = total_want - total_got
zero_steps = [s[0] for s in steps if s[1] == 0]
print("run TS=%s steps=%d got=%d want=%d shortfall=%d logged_eaten=%d(%d reads)" %
(ts, len(steps), total_got, total_want, shortfall, eaten, nev))
for lab, got, want, st in steps:
if got != want:
print(" short: %-14s %d/%d %s" % (lab, got, want, st))
if shortfall == 0:
print("VERDICT PASS")
elif shortfall <= eaten:
print("VERDICT EXPECTED-SHORT (test probe ate %d bytes; not a freeze)" % eaten)
elif silent_reader:
print("VERDICT INCONCLUSIVE (shortfall %d; build contains a non-echoing in-firmware "
"reader (Phase 9/11) which eats peer bytes silently - need device-side evidence)" % shortfall)
elif zero_steps:
print("VERDICT SUSPECT-FREEZE (zero-byte step: %s; unexplained shortfall %d)" %
",".join(zero_steps), shortfall - eaten)
else:
print("VERDICT SUSPECT-FREEZE (shortfall %d exceeds logged eaten %d; note Phase 9/11 "
"readers eat bytes silently)" % (shortfall, eaten))
return 0
sys.exit(main())
+55
View File
@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
# Verify whether the C-recipe runs actually received correct CONTENT (fill part),
# i.e. whether "fill part 0/1460" from probe_classify mode C is real corruption
# or a classifier artifact. ASCII output only.
import sys, re
def pattern(base, n):
return bytes(((base + i) & 0xFF) for i in range(n))
def hex_blocks(path):
blocks = []
cur = []
for line in open(path, encoding="utf-8-sig", errors="replace"):
m = re.match(r"HEX2?\s+(\d+)\s+([0-9a-fA-F]+)", line.strip())
if m:
off = int(m.group(1))
if off == 0 and cur:
blocks.append(cur)
cur = []
cur.append((off, bytes.fromhex(m.group(2))))
if cur:
blocks.append(cur)
out = []
for b in blocks:
data = b"".join(x[1] for x in b)
out.append(data)
return out
def check(path):
blocks = hex_blocks(path)
print("=" * 60)
print("LOG:", path, "blocks:", [len(b) for b in blocks])
exp_fill = pattern(640, 1460)
exp_tail = pattern(2100, 1460) + b"".join(pattern(2100 + 1460 + i * 64, 64) for i in range(10))
if blocks:
got = blocks[0]
n = min(len(got), len(exp_fill))
m = 0
while m < n and got[m] == exp_fill[m]:
m += 1
print(" fill: got=%d exp=%d match_prefix=%d %s" %
(len(got), len(exp_fill), m, "MATCH" if m == len(exp_fill) else "MISMATCH"))
if m < n:
print(" first mismatch at %d: got=%02x exp=%02x" % (m, got[m], exp_fill[m]))
if len(blocks) > 1:
got2 = blocks[1]
n2 = min(len(got2), len(exp_tail))
m2 = 0
while m2 < n2 and got2[m2] == exp_tail[m2]:
m2 += 1
print(" tail: got=%d exp=%d match_prefix=%d %s" %
(len(got2), len(exp_tail), m2, "MATCH" if m2 == len(exp_tail) else "MISMATCH"))
for p in sys.argv[1:]:
check(p)
+121
View File
@@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
# Trustworthy abort/robustness probe for the production FTP path (no firmware probes).
#
# Scenario: client starts a STOR upload, sends part of the data, then ABORTS by
# resetting the data connection (SO_LINGER 0 -> RST), which is the common real-world
# pattern (user cancels an upload, client crashes, link drops). Then it checks:
# 1. the control channel still answers (NOOP on a fresh connection),
# 2. a FOLLOW-UP normal upload+download still passes the SHA-256 check,
# 3. the device's relisten actually happened (observable via a successful follow-up).
#
# Usage: ftp_abort_probe.py [--ip ..] [--port 21] [--rounds 10] [--send 8192] [--gap 2]
# Output: TS <epoch> / ABORT <n> sent=<b> ctrl=<reply> alive=<yes|no> followup=<OK|FAIL:x>
# SUMMARY rounds=<n> pass=<n> fail=<n>
# Exit 0 all follow-ups pass, 1 otherwise.
import argparse, hashlib, io, random, re, socket, struct, sys, time
from ftplib import FTP, all_errors
def log(m):
sys.stdout.write(m + "\n"); sys.stdout.flush()
def rd(sock):
try:
return sock.recv(4096).decode("latin1").strip()
except Exception:
return ""
def raw_login(ip, port, timeout=10.0):
s = socket.create_connection((ip, port), timeout=timeout)
rd(s) # 220 banner
s.sendall(b"USER anonymous\r\n"); rd(s)
s.sendall(b"TYPE I\r\n"); rd(s)
return s
def pasv_data_socket(ctl):
ctl.sendall(b"PASV\r\n")
r = rd(ctl)
m = re.search(r"\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)", r)
if not m:
raise RuntimeError("PASV failed: " + r)
host = ".".join(m.group(i) for i in (1, 2, 3, 4))
port = (int(m.group(5)) << 8) | int(m.group(6))
return socket.create_connection((host, port), timeout=10.0)
def followup_ok(ip, port, size=4096):
"""normal upload + download + sha256 -> True/False"""
data = bytes(random.getrandbits(8) for _ in range(size))
want = hashlib.sha256(data).hexdigest()
try:
f = FTP(); f.connect(ip, port, timeout=15.0); f.login("anonymous", "x")
f.storbinary("STOR /ab_ok.bin", io.BytesIO(data))
buf = bytearray(); f.retrbinary("RETR /ab_ok.bin", buf.extend)
try:
f.delete("/ab_ok.bin")
except all_errors:
pass
f.quit()
return hashlib.sha256(bytes(buf)).hexdigest() == want
except all_errors:
return False
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--ip", default="192.168.1.100")
ap.add_argument("--port", type=int, default=21)
ap.add_argument("--rounds", type=int, default=10)
ap.add_argument("--send", type=int, default=8192, help="bytes sent before the RST")
ap.add_argument("--gap", type=float, default=2.0)
ap.add_argument("--graceful", action="store_true",
help="close the data connection with FIN instead of RST")
a = ap.parse_args()
npass = nfail = 0
for n in range(1, a.rounds + 1):
log("TS %d" % int(time.time()))
ctl_reply = "NA"
alive = "no"
try:
ctl = raw_login(a.ip, a.port)
ds = pasv_data_socket(ctl)
ctl.sendall(("STOR /ab%04d.bin\r\n" % n).encode())
ctl_reply = rd(ctl) # 150
chunk = bytes(random.getrandbits(8) for _ in range(a.send))
ds.sendall(chunk)
if a.graceful:
ds.shutdown(socket.SHUT_RDWR) # graceful FIN (client cancel)
else:
# abort: RST the data connection (linger 0)
ds.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0))
ds.close()
time.sleep(0.5)
ctl_reply = rd(ctl) # 426/550/226
try:
ctl.sendall(b"QUIT\r\n"); rd(ctl)
except Exception:
pass
try:
ctl.close()
except Exception:
pass
except Exception as e:
ctl_reply = "EXC:%s" % type(e).__name__
# control-channel liveness on a fresh connection
try:
c2 = raw_login(a.ip, a.port)
c2.sendall(b"NOOP\r\n")
alive = "yes" if rd(c2).startswith("2") else "no"
c2.sendall(b"QUIT\r\n"); c2.close()
except Exception:
alive = "no"
fu = followup_ok(a.ip, a.port)
ok = (alive == "yes" and fu)
npass += 1 if ok else 0
nfail += 0 if ok else 1
log("ABORT %d sent=%d ctrl=%s alive=%s followup=%s" %
(n, a.send, ctl_reply.replace("\r\n", "|")[:40], alive, "OK" if fu else "FAIL"))
if a.gap > 0 and n < a.rounds:
time.sleep(a.gap)
log("SUMMARY rounds=%d pass=%d fail=%d" % (a.rounds, npass, nfail))
return 0 if nfail == 0 else 1
sys.exit(main())
+111
View File
@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
# Trustworthy TC-NET-604 freeze reproduction via the REAL production path.
#
# Why this one is trustworthy (unlike the 2026-09-22 probe batteries):
# * Production firmware, no in-firmware readers/probes -> the "a test reader ate the
# peer's bytes and the PC saw a short stream" artifact class cannot occur.
# * Judgement = the FTP client's own protocol result (STOR/RETR replies) plus an
# end-to-end SHA-256 taken from a RETR of the uploaded file (content ground truth).
# * Data-channel stall vs whole-net-layer stall is distinguished by a NOOP on the
# CONTROL channel (a different socket) while the data channel is stuck.
# * Only the device's own production logs are used for the device-side timeline
# (window them by the per-round "TS <epoch>" line printed here).
#
# Usage: ftp_freeze_probe.py [--ip 192.168.1.100] [--port 21] [--size 65536]
# [--rounds 10] [--gap 3] [--keep]
# Output lines (machine-auditable):
# TS <epoch>
# ROUND <n> size=<bytes> stor=<OK|FAIL:reason> t_stor=<s> retr=<OK|FAIL:reason>
# t_retr=<s> hash=<MATCH|MISMATCH|NA> [ctrl=<alive|dead|na>]
# SUMMARY rounds=<n> pass=<n> fail=<n>
# Exit: 0 all pass / 1 any failure / 2 usage or connect error.
import argparse, hashlib, io, os, random, socket, sys, time
from ftplib import FTP, error_reply, error_temp, error_perm, all_errors
def log(msg):
sys.stdout.write(msg + "\n")
sys.stdout.flush()
def liveness(ip, port, timeout=3.0):
"""Control-channel liveness on a FRESH connection (production lftpd control path)."""
try:
f = FTP()
f.connect(ip, port, timeout=timeout)
f.login("anonymous", "probe@local")
f.voidcmd("NOOP")
f.quit()
return "alive"
except Exception:
return "dead"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--ip", default="192.168.1.100")
ap.add_argument("--port", type=int, default=21)
ap.add_argument("--size", type=int, default=65536)
ap.add_argument("--rounds", type=int, default=10)
ap.add_argument("--gap", type=float, default=3.0)
ap.add_argument("--keep", action="store_true", help="do not DELE the uploaded file")
ap.add_argument("--seed", type=int, default=604)
a = ap.parse_args()
rng = random.Random(a.seed)
npass = nfail = 0
for n in range(1, a.rounds + 1):
data = bytes(rng.getrandbits(8) for _ in range(a.size))
want = hashlib.sha256(data).hexdigest()
name = "/fz%04d.bin" % n
log("TS %d" % int(time.time()))
f = FTP()
stor = retr = "NA"
t_stor = t_retr = 0.0
got_hash = "NA"
ctrl = "na"
try:
f.connect(a.ip, a.port, timeout=15.0)
f.login("anonymous", "probe@local")
t0 = time.time()
try:
f.storbinary("STOR " + name, io.BytesIO(data), blocksize=8192)
stor = "OK"
except all_errors as e:
stor = "FAIL:%s" % type(e).__name__
ctrl = liveness(a.ip, a.port)
t_stor = time.time() - t0
if stor == "OK":
buf = bytearray()
t0 = time.time()
try:
f.retrbinary("RETR " + name, buf.extend, blocksize=8192)
retr = "OK"
got_hash = "MATCH" if hashlib.sha256(bytes(buf)).hexdigest() == want else "MISMATCH"
except all_errors as e:
retr = "FAIL:%s" % type(e).__name__
ctrl = liveness(a.ip, a.port)
t_retr = time.time() - t0
if not a.keep:
try:
f.delete(name)
except all_errors:
pass
try:
f.quit()
except all_errors:
try:
f.close()
except Exception:
pass
except all_errors as e:
stor = "FAIL:connect-%s" % type(e).__name__
ctrl = liveness(a.ip, a.port)
ok = (stor == "OK" and retr == "OK" and got_hash == "MATCH")
npass += 1 if ok else 0
nfail += 0 if ok else 1
log("ROUND %d size=%d stor=%s t_stor=%.2f retr=%s t_retr=%.2f hash=%s ctrl=%s" %
(n, a.size, stor, t_stor, retr, t_retr, got_hash, ctrl))
if a.gap > 0 and n < a.rounds:
time.sleep(a.gap)
log("SUMMARY rounds=%d pass=%d fail=%d" % (a.rounds, npass, nfail))
return 0 if nfail == 0 else 1
sys.exit(main())
@@ -0,0 +1,5 @@
RETRACTED 2026-09-22 bench artifacts (TC-NET-604 'freeze' campaign).
Their 'STALL/WEDGE' verdicts were artifacts: (1) in-firmware non-echoing readers (Phase 9/11, probe) ate the peer's bytes; (2) probe_classify.py mode A/C misreported passing runs as STALL.
TC-NET-604 is CLOSED as NOT-A-DEFECT (see docs/问题记录/CH395F_Trap_Records.md Trap 21).
Do NOT use these recipes/verdicts for freeze detection.
Trustworthy tools (kept one level up): ftp_freeze_probe.py (real FTP path + RETR SHA-256), ftp_abort_probe.py (abort/reconnect), bench_verdict.py (script-level STEP truth).
@@ -0,0 +1,653 @@
{
"mode": "remo",
"host": "192.168.1.100",
"port": 7003,
"ts_start": "2026-09-22 16:37:37",
"ts_end": "2026-09-22 16:37:40",
"duration_s": 2.83,
"samples": 50,
"cadence_ms": 100.0,
"tcp_nodelay": false,
"immediate_ack": null,
"in_band_ack_probe": false,
"stress_from": null,
"connect_ms": 3.628,
"reconnect_ms": null,
"reconnect_list_ms": [],
"reconnect_stat": {
"n": 0
},
"init": {
"rtt_ms": 20.445,
"geom": {
"w": 160,
"h": 160,
"framebytes": 3200,
"payload_len": 8
}
},
"cycle_all": {
"n": 17,
"min_ms": 47.914,
"med_ms": 62.139,
"p95_ms": 71.32,
"max_ms": 71.32,
"spikes500": 0
},
"cycle_quiet": {
"n": 17,
"min_ms": 47.914,
"med_ms": 62.139,
"p95_ms": 71.32,
"max_ms": 71.32,
"spikes500": 0
},
"cycle_stress": {
"n": 0
},
"first_byte_all": {
"n": 17,
"min_ms": 3.359,
"med_ms": 12.193,
"p95_ms": 15.251,
"max_ms": 15.251,
"spikes500": 0
},
"tail_lag_all": {
"n": 17,
"min_ms": 44.042,
"med_ms": 52.676,
"p95_ms": 58.277,
"max_ms": 58.277,
"spikes500": 0
},
"crc": {
"ok": 17,
"fail": 0
},
"bitmap": {
"nontrivial_ok": 17,
"nontrivial_fail": 0
},
"frames": [
{
"cycle": 0,
"stress": false,
"first_ms": 13.517,
"total_ms": 59.748,
"tail_lag_ms": 46.078,
"batches": [
[
13.517,
8
],
[
13.559,
1016
],
[
15.33,
1024
],
[
16.725,
1024
],
[
59.595,
137
]
],
"bytes": 3209,
"tag": 187,
"cmd": 3,
"crc_ok": true,
"nontrivial": true
},
{
"cycle": 1,
"stress": false,
"first_ms": 13.514,
"total_ms": 63.153,
"tail_lag_ms": 49.579,
"batches": [
[
13.514,
8
],
[
13.579,
1016
],
[
15.264,
1024
],
[
17.108,
1024
],
[
63.092,
137
]
],
"bytes": 3209,
"tag": 187,
"cmd": 3,
"crc_ok": true,
"nontrivial": true
},
{
"cycle": 2,
"stress": false,
"first_ms": 10.477,
"total_ms": 65.17,
"tail_lag_ms": 54.592,
"batches": [
[
10.477,
8
],
[
10.51,
1016
],
[
11.859,
1024
],
[
13.165,
1024
],
[
65.069,
137
]
],
"bytes": 3209,
"tag": 187,
"cmd": 3,
"crc_ok": true,
"nontrivial": true
},
{
"cycle": 3,
"stress": false,
"first_ms": 15.251,
"total_ms": 67.989,
"tail_lag_ms": 52.676,
"batches": [
[
15.251,
8
],
[
15.29,
1016
],
[
17.545,
1024
],
[
18.689,
1024
],
[
67.928,
137
]
],
"bytes": 3209,
"tag": 187,
"cmd": 3,
"crc_ok": true,
"nontrivial": true
},
{
"cycle": 4,
"stress": false,
"first_ms": 6.239,
"total_ms": 56.937,
"tail_lag_ms": 50.635,
"batches": [
[
6.239,
8
],
[
6.266,
1016
],
[
8.081,
1024
],
[
9.893,
1024
],
[
56.873,
137
]
],
"bytes": 3209,
"tag": 187,
"cmd": 3,
"crc_ok": true,
"nontrivial": true
},
{
"cycle": 5,
"stress": false,
"first_ms": 8.853,
"total_ms": 62.139,
"tail_lag_ms": 53.201,
"batches": [
[
8.853,
8
],
[
8.903,
1016
],
[
10.628,
1024
],
[
12.473,
1024
],
[
62.053,
137
]
],
"bytes": 3209,
"tag": 187,
"cmd": 3,
"crc_ok": true,
"nontrivial": true
},
{
"cycle": 6,
"stress": false,
"first_ms": 14.097,
"total_ms": 70.635,
"tail_lag_ms": 56.473,
"batches": [
[
14.097,
8
],
[
14.14,
1016
],
[
15.894,
1024
],
[
17.742,
1024
],
[
70.57,
137
]
],
"bytes": 3209,
"tag": 187,
"cmd": 3,
"crc_ok": true,
"nontrivial": true
},
{
"cycle": 7,
"stress": false,
"first_ms": 14.092,
"total_ms": 70.009,
"tail_lag_ms": 55.854,
"batches": [
[
14.092,
8
],
[
14.129,
1016
],
[
15.891,
1024
],
[
17.73,
1024
],
[
69.946,
137
]
],
"bytes": 3209,
"tag": 187,
"cmd": 3,
"crc_ok": true,
"nontrivial": true
},
{
"cycle": 8,
"stress": false,
"first_ms": 3.805,
"total_ms": 47.914,
"tail_lag_ms": 44.042,
"batches": [
[
3.805,
8
],
[
3.837,
1016
],
[
5.599,
1024
],
[
7.473,
1024
],
[
47.847,
137
]
],
"bytes": 3209,
"tag": 187,
"cmd": 3,
"crc_ok": true,
"nontrivial": true
},
{
"cycle": 9,
"stress": false,
"first_ms": 4.308,
"total_ms": 56.061,
"tail_lag_ms": 51.644,
"batches": [
[
4.308,
8
],
[
4.335,
1016
],
[
6.114,
1024
],
[
7.966,
1024
],
[
55.953,
137
]
],
"bytes": 3209,
"tag": 187,
"cmd": 3,
"crc_ok": true,
"nontrivial": true
},
{
"cycle": 10,
"stress": false,
"first_ms": 7.908,
"total_ms": 55.037,
"tail_lag_ms": 47.068,
"batches": [
[
7.908,
8
],
[
7.946,
1016
],
[
9.693,
1024
],
[
11.515,
1024
],
[
54.977,
137
]
],
"bytes": 3209,
"tag": 187,
"cmd": 3,
"crc_ok": true,
"nontrivial": true
},
{
"cycle": 11,
"stress": false,
"first_ms": 12.254,
"total_ms": 68.577,
"tail_lag_ms": 56.259,
"batches": [
[
12.254,
8
],
[
12.29,
1016
],
[
14.079,
1024
],
[
15.907,
1024
],
[
68.513,
137
]
],
"bytes": 3209,
"tag": 187,
"cmd": 3,
"crc_ok": true,
"nontrivial": true
},
{
"cycle": 12,
"stress": false,
"first_ms": 3.359,
"total_ms": 57.811,
"tail_lag_ms": 54.392,
"batches": [
[
3.359,
8
],
[
3.393,
1016
],
[
5.18,
1024
],
[
7.013,
1024
],
[
57.751,
137
]
],
"bytes": 3209,
"tag": 187,
"cmd": 3,
"crc_ok": true,
"nontrivial": true
},
{
"cycle": 13,
"stress": false,
"first_ms": 13.204,
"total_ms": 59.676,
"tail_lag_ms": 46.401,
"batches": [
[
13.204,
8
],
[
13.258,
1016
],
[
14.962,
1024
],
[
16.799,
1024
],
[
59.604,
137
]
],
"bytes": 3209,
"tag": 187,
"cmd": 3,
"crc_ok": true,
"nontrivial": true
},
{
"cycle": 14,
"stress": false,
"first_ms": 12.941,
"total_ms": 71.32,
"tail_lag_ms": 58.277,
"batches": [
[
12.941,
8
],
[
12.973,
1016
],
[
14.747,
1024
],
[
16.595,
1024
],
[
71.218,
137
]
],
"bytes": 3209,
"tag": 187,
"cmd": 3,
"crc_ok": true,
"nontrivial": true
},
{
"cycle": 15,
"stress": false,
"first_ms": 12.193,
"total_ms": 61.254,
"tail_lag_ms": 48.998,
"batches": [
[
12.193,
8
],
[
12.249,
1016
],
[
14.025,
1024
],
[
15.82,
1024
],
[
61.191,
137
]
],
"bytes": 3209,
"tag": 187,
"cmd": 3,
"crc_ok": true,
"nontrivial": true
},
{
"cycle": 16,
"stress": false,
"first_ms": 10.55,
"total_ms": 65.811,
"tail_lag_ms": 55.201,
"batches": [
[
10.55,
8
],
[
10.581,
1016
],
[
12.348,
1024
],
[
14.233,
1024
],
[
65.751,
137
]
],
"bytes": 3209,
"tag": 187,
"cmd": 3,
"crc_ok": true,
"nontrivial": true
}
],
"error": "[WinError 10054] 远程主机强迫关闭了一个现有的连接。"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,60 @@
# Extract the per-run chip command sequence (netcmd lines) from the serial log.
# usage: python MDK-ARM/netcmd_extract.py <runlog> <seriallog> <run_index> [max_lines]
# run_index = 1-based run position in the session (TS lines in the runlog delimit
# each run's device-time window; PC local = device UTC + 8h, 4s margins).
import re, sys
from datetime import datetime, timezone
runlog, seriallog = sys.argv[1], sys.argv[2]
base = int(sys.argv[3])
maxl = int(sys.argv[4]) if len(sys.argv) > 4 else 300
nextrunlog = sys.argv[5] if (len(sys.argv) > 5 and sys.argv[5] not in ("focus", "plain")) else None
focus = any(a == "focus" for a in sys.argv[5:])
ts = []
for raw in open(runlog, encoding="utf-8-sig", errors="replace"):
m = re.match(r"TS (\d+)", raw.strip())
if m:
ts.append(int(m.group(1)))
ts_next = None
if nextrunlog:
for raw in open(nextrunlog, encoding="utf-8-sig", errors="replace"):
m = re.match(r"TS (\d+)", raw.strip())
if m:
ts_next = int(m.group(1))
break
if base - 1 >= len(ts):
print("run_index out of range (%d TS lines)" % len(ts))
sys.exit(2)
lo = ts[base - 1] - 4.0
hi = (ts_next - 4.0) if ts_next is not None else (lo + 600.0)
CMD_RE = re.compile(r"netcmd: (\S+) fd=(-?\d+) t=(\d+) d=(\d+) x=(-?\d+)")
DEV_RE = re.compile(r"\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]\[")
n = 0
for raw in open(seriallog, encoding="utf-8", errors="replace"):
m = CMD_RE.search(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).timestamp()
if lo <= dt < hi:
tag, fd, x = m.group(1), m.group(2), m.group(5)
if focus:
# 只留数据 socket 的 R/S 与 poll/事件行;A(各服务 accept 轮询洪水)与
# 稳态空读(phase6 每 tick 状态读)全部降噪
if tag == "R" and fd == "2" and x == "-1":
continue
if tag not in ("R", "S", "P", "p", "a", "d", "x"):
continue
if tag in ("R", "S") and fd != "2":
continue
n += 1
if n <= maxl:
print("%s fd=%s t=%s d=%s x=%s" % (m.group(1), m.group(2),
m.group(3), m.group(4), m.group(5)))
print("netcmd lines in run window: %d" % n)
@@ -0,0 +1,27 @@
# One-off bench probe: isolated 4096B echo test (4KB through 2KB chip buffer).
# Connects to MCU :8080, sends 4096B increment pattern, waits for full echo.
import socket, sys, time
ip, port = "192.168.1.100", 8080
data = bytes((i & 0xFF) for i in range(4096))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(15)
t0 = time.time()
s.connect((ip, port))
s.sendall(data)
rx = b""
first = None
while len(rx) < 4096:
chunk = s.recv(4096 - len(rx))
if not chunk:
break
if first is None:
first = time.time()
rx += chunk
elapsed = (time.time() - t0) * 1000
if len(rx) == 4096 and rx == data:
print("PASS 4096B echo full match in %.1f ms (first byte %.1f ms)" % (elapsed, (first - t0) * 1000))
else:
print("FAIL 4096B echo: got %d/4096 bytes after %.1f ms" % (len(rx), elapsed))
s.close()
sys.exit(0 if len(rx) == 4096 and rx == data else 1)
@@ -0,0 +1,57 @@
# Shared helpers for the 2026-09-22 primary-cause localization battery.
# Pure-ASCII output (run logs are parsed by probe_classify.py).
import socket, sys, time
IP, PORT = "192.168.1.100", 8080
def pattern(base, n):
return bytes(((base + i) & 0xFF) for i in range(n))
def connect():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3.0)
s.connect((IP, PORT))
print("TS %d" % int(time.time()))
return s
def wait_step(s, want, label, timeout_s=3.0):
"""recv until `want` bytes arrive or `timeout_s` of quiet; (data, stalled)"""
rx = b""
last = time.time()
while len(rx) < want:
try:
chunk = s.recv(want - len(rx))
except socket.timeout:
chunk = b""
except OSError:
chunk = b""
break
if chunk:
rx += chunk
last = time.time()
elif time.time() - last >= timeout_s:
break
print("STEP %s rx=%d/%d %s" % (label, len(rx), want,
"QUIET" if len(rx) < want else "OK"))
return rx, len(rx) < want
def send_rounds64(s, count, base, label, timeout_s=3.0):
"""send `count` 64B rounds (global-increment pattern), verify each echo inline."""
for i in range(count):
data = pattern(base + i * 64, 64)
s.sendall(data)
rx, stalled = wait_step(s, 64, "%s r%02d" % (label, i), timeout_s)
if stalled or rx != data:
return False
return True
def hexdump(data, prefix="HEX"):
for i in range(0, len(data), 16):
print("%s %05d %s" % (prefix, i, data[i:i + 16].hex()))
def finish(s, code):
try:
s.close()
except OSError:
pass
sys.exit(code)
@@ -0,0 +1,28 @@
# Battery A (boundary scan): 64x10 + 1460 + 4096 (two 2KB fills) + 0.25s + tail 64x10.
# The firmware probe (net_recv(64)) fires ~45+D ms after the fill1 full drain;
# if it lands on a refilled buffer it EATS 64B at pattern offset 2048 of the 4096 block
# (no echo for those bytes). probe_classify.py rebuilds the expected stream using the
# actual eaten count logged on the device serial line "probe: recv64 n=%d".
from probe_bench_lib import *
def main():
s = connect()
if not send_rounds64(s, 10, 0, "R"):
finish(s, 1)
b1460 = pattern(640, 1460)
s.sendall(b1460)
rx, stalled = wait_step(s, 1460, "B1460", 3.0)
if stalled or rx != b1460:
hexdump(rx)
finish(s, 1)
f4096 = pattern(2100, 4096)
s.sendall(f4096)
time.sleep(0.25)
tail_base = 2100 + 4096
for i in range(10):
s.sendall(pattern(tail_base + i * 64, 64))
rx, stalled = wait_step(s, 4736, "F4096+TAIL", 4.0)
hexdump(rx)
finish(s, 0)
main()
@@ -0,0 +1,199 @@
# Offline verdict for the 2026-09-22 primary-cause localization battery (v4).
# usage:
# python MDK-ARM/probe_classify.py A <runlog> <seriallog> <run_index> # boundary (one-shot or sustained)
# python MDK-ARM/probe_classify.py B <runlog> <seriallog> <run_index> # status (hold)
# python MDK-ARM/probe_classify.py C <runlog> <seriallog> <run_index> # partial drain
# python MDK-ARM/probe_classify.py F <runlog> <seriallog> <base_index> # 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()
@@ -0,0 +1,47 @@
# Battery F1 (pre-conditioning (b) / fast-reconnect residue) — chain of 20
# connections, each 0.6s after the previous close (the Trap-22 fast window).
# Even idx = plain connection (64x10 + 2048 fill, the residue-leaver);
# odd idx = canary connection (64x10 + 2048 F1 + 0.4s hold + 2048 F2 canary
# + 64x10 tail). Pair i = (conn 2i, conn 2i+1): the canary connection is
# pre-conditioned by the immediately preceding fast plain connection.
# Pair 0's plain connection follows a fresh boot (internal control).
# Verdict offline: probe_classify.py F
from probe_bench_lib import *
N_CONN = 20
GAP_S = 0.6
def run_conn(s, idx, with_canary):
if not send_rounds64(s, 10, 0, "R"):
pass # keep collecting; offline classification is authoritative
f = pattern(640, 2048)
s.sendall(f)
rx, _ = wait_step(s, 2048, "F1", 3.0)
hexdump(rx, "HEXc%02dF1" % idx)
if with_canary:
time.sleep(0.40) # hold: in-window status reads on the device side
f2 = pattern(2688, 2048)
s.sendall(f2)
time.sleep(0.30)
for i in range(10):
s.sendall(pattern(4736 + i * 64, 64))
rx, _ = wait_step(s, 2688, "F2+TAIL", 4.0)
hexdump(rx, "HEXc%02dG" % idx)
def main():
for idx in range(N_CONN):
print("CONN %d canary=%d" % (idx, 1 if idx % 2 == 1 else 0))
try:
s = connect()
except OSError as e:
print("CONN %d REFUSED: %s" % (idx, e))
time.sleep(1.0)
continue
run_conn(s, idx, with_canary=(idx % 2 == 1))
try:
s.close()
except OSError:
pass
time.sleep(GAP_S)
main()
@@ -0,0 +1,8 @@
import socket, time
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
t0 = time.time()
s.connect(('192.168.1.100', 8080))
print('connected, holding silently for 95s (Phase 11 keepalive)')
time.sleep(95)
print('hold done, closing at %.1fs' % (time.time() - t0))
s.close()
@@ -0,0 +1,40 @@
# TC-NET-604 诊断:芯片 TX 分包粒度探测(= SET_TCP_MSS 有效值)
# 向回显端发送 N 字节,紧循环小包 recv + 计时,输出到达分块分布。
# 芯片 TX 包 = MSS 大小 → 到达粒度 ≈ 芯片发包大小(同交换机微秒级间隔,按 1ms 窗口聚合)。
import socket, sys, time
ip, port = sys.argv[1] if len(sys.argv) > 1 else "192.168.1.100", 8080
size = int(sys.argv[2]) if len(sys.argv) > 2 else 4096
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(30)
s.connect((ip, int(port)))
payload = bytes(i & 0xFF for i in range(size))
t0 = time.perf_counter()
s.sendall(payload)
# 紧循环非阻塞式读取(recv 4096 上限,记录每次 recv 的字节数与相对时刻)
s.setblocking(False)
chunks = []
got = 0
deadline = time.time() + 10.0
while got < size and time.time() < deadline:
try:
b = s.recv(4096)
if b:
chunks.append((round((time.perf_counter() - t0) * 1000, 1), len(b)))
got += len(b)
except BlockingIOError:
time.sleep(0.001)
s.close()
print(f"sent={size} got={got} in {round((time.perf_counter()-t0)*1000,1)}ms")
if chunks:
from collections import Counter
c = Counter(n for _, n in chunks)
print("arrival chunks (ms, bytes):")
for t, n in chunks:
print(f" +{t:7.1f}ms {n:5d}B")
print("size histogram:", dict(sorted(c.items())))
else:
print("NO DATA (stall?)")
@@ -0,0 +1,30 @@
# Battery C (partial drain + partial read): 64x10 + 1460 fill + 1460 canary + tail 64x10.
# Firmware variant: PHASE6_RECV_LIMIT=1024 (drains only 1024 of the 1460 fill),
# PHASE_PROBE_ARM_MIN=1000 (probe arms on that partial drain). The probe then reads
# 64B from the 436B REMAINDER (partial buffer, partial read, after a NON-full drain:
# window re-announced 588 -> 1612, never zero). If the canary stalls, partial drains
# also create the fragile window; if it passes, only the zero-window (exact-full)
# re-arm is fragile.
from probe_bench_lib import *
def main():
s = connect()
if not send_rounds64(s, 10, 0, "R"):
finish(s, 1)
b1460 = pattern(640, 1460)
s.sendall(b1460)
# echo of the fill = first 1024 (drained) + 436 remainder minus probe-eaten 64
rx, _ = wait_step(s, 1460, "B1460", 2.0)
time.sleep(0.15)
k1460 = pattern(2100, 1460) # canary (separate pattern region)
s.sendall(k1460)
time.sleep(0.30)
tail_base = 2100 + 1460
for i in range(10):
s.sendall(pattern(tail_base + i * 64, 64))
rx2, _ = wait_step(s, 2100, "K1460+TAIL", 4.0)
hexdump(rx)
hexdump(rx2, "HEX2")
finish(s, 0)
main()
@@ -0,0 +1,28 @@
# Battery B (hold / pure status read): 64x10 + 2048 fill + 400ms HOLD + 2048 canary
# + 0.3s + tail 64x10. During the hold the buffer is empty when the probe lands,
# so the probe is a pure GET_RECV_LEN (no data movement). If a pure status read inside
# the post-drain re-arm window still wedges the chip, the canary stalls.
from probe_bench_lib import *
def main():
s = connect()
if not send_rounds64(s, 10, 0, "R"):
finish(s, 1)
f1 = pattern(640, 2048)
s.sendall(f1)
rx, stalled = wait_step(s, 2048, "F1", 3.0)
if stalled or rx != f1:
hexdump(rx)
finish(s, 1)
time.sleep(0.40) # hold: probe lands on the EMPTY buffer (pure status read)
f2 = pattern(2688, 2048) # canary fill (exact window size)
s.sendall(f2)
time.sleep(0.30)
tail_base = 2688 + 2048
for i in range(10):
s.sendall(pattern(tail_base + i * 64, 64))
rx, stalled = wait_step(s, 2688, "F2+TAIL", 4.0)
hexdump(rx)
finish(s, 0)
main()
+81
View File
@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
# Trap 22 / N-6 trustworthy reproduction probe: rapid reconnect to a device listener.
#
# Failure under test (original report): after a rapid reconnect (<~1s) on a
# relistened socket, the chip completes the handshake but the MCU never gets
# standalone accept -> the socket stays LISTENING -> later the chip auto-closes it
# -> THE LISTENER DIES PERMANENTLY (all later SYNs refused, no self-recovery).
#
# Judgement is purely client-side (no firmware probes):
# * every round must (re)connect successfully within --timeout
# * on failure the probe keeps retrying for --recover-s to distinguish
# "transient" from "permanently dead listener"
#
# Usage: tcp_reconnect_probe.py [--host 192.168.1.100] [--port 21] [--rounds 50]
# [--gap-ms 600] [--timeout 3] [--recover-s 10]
# [--banner] (read one line after connect, e.g. FTP 220)
# Output:
# TS <epoch>
# ROUND <n> gap=<ms> connect=<OK|FAIL:reason> recovered=<na|yes|no>
# SUMMARY rounds=<n> ok=<n> fail=<n> unrecovered=<n>
# Exit: 0 all rounds connected / 1 any failure / 2 usage
import argparse, socket, sys, time
def log(m):
sys.stdout.write(m + "\n"); sys.stdout.flush()
def try_connect(host, port, timeout, banner):
s = socket.create_connection((host, port), timeout=timeout)
if banner:
s.settimeout(timeout)
try:
s.recv(256) # e.g. FTP "220 ..." greeting
except socket.timeout:
pass
return s
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--host", default="192.168.1.100")
ap.add_argument("--port", type=int, default=21)
ap.add_argument("--rounds", type=int, default=50)
ap.add_argument("--gap-ms", type=int, default=600)
ap.add_argument("--timeout", type=float, default=3.0)
ap.add_argument("--recover-s", type=float, default=10.0)
ap.add_argument("--banner", action="store_true")
a = ap.parse_args()
nok = nfail = nunrec = 0
for n in range(1, a.rounds + 1):
log("TS %d" % int(time.time()))
res = "OK"
recovered = "na"
try:
s = try_connect(a.host, a.port, a.timeout, a.banner)
s.close()
except Exception as e:
res = "FAIL:%s" % type(e).__name__
# keep probing: transient (relisten window) or permanently dead?
t0 = time.time()
recovered = "no"
while time.time() - t0 < a.recover_s:
time.sleep(0.5)
try:
s = try_connect(a.host, a.port, a.timeout, a.banner)
s.close()
recovered = "yes"
break
except Exception:
pass
ok = (res == "OK")
nok += 1 if ok else 0
nfail += 0 if ok else 1
if not ok and recovered != "yes":
nunrec += 1
log("ROUND %d gap=%d connect=%s recovered=%s" % (n, a.gap_ms, res, recovered))
if a.gap_ms > 0 and n < a.rounds:
time.sleep(a.gap_ms / 1000.0)
log("SUMMARY rounds=%d ok=%d fail=%d unrecovered=%d" % (a.rounds, nok, nfail, nunrec))
return 0 if nfail == 0 else 1
sys.exit(main())