入库台架工具:真实路径冻结/中止/重连复现与判读脚本,含已撤回台架归档(带README)
This commit is contained in:
@@ -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))
|
||||
@@ -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)"))
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user