入库台架工具:真实路径冻结/中止/重连复现与判读脚本,含已撤回台架归档(带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
@@ -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()