test(net/udp): 阶段 16 台架加固 + 真机首跑证据回填(#73)
第一次上真机把台架的两处软肋照出来了,都不是被测代码的问题: - peer 死在 Windows 的 ICMP 10054:板子还没 bind 时那一下端口不可达会被记在套接句上, 下一次 recvfrom 才抛出来。本机 CPython 3.14.7 既没有 socket.SIO_UDP_CONNRESET 也不接受 它的数值形式,所以主力防线改成把 10054 当「这一轮没收到」继续重试,并在回路自检里加 冷端口探针——探针走脚本真正在用的 _recv_maybe,另拿裸套接句证明这台机器确实会回 ICMP, 否则那条判定就没有牙。 - 单步预算错位:板端每步等 25 s,peer 每步只等 8 s,一步超时后整条梯子全灭。现在 peer 每步都重发、单步预算 --step-budget 默认 35 s(小于 26 s 打警告);板端 udp_bench_expect 按载荷第 0 字节的步号自愈,比本步小的旧包丢掉继续等(上限 UDP_BENCH_STALE_MAX = 8), 比本步大的直接报出来。DBG_WARN 在本工程不存在、字符串字面量必须纯 ASCII,两处已改。 真机已拿到的证据:TC-NET-1601a/b/c(建 socket、bind :6001、net_udp_get_mode 回 SERVER)与 TC-NET-1602(服务端形态收到 20 B、addrlen/sin_family/源端口全对、逐字节回显、形态翻成 CLIENT)全绿——剥 8 字节信息头与源地址回填在真芯片上成立;②③④⑤ 因板子当晚两次失电 (19:19、21:0x,SWD No target connected + ping 不通)未测,指南 §12.6 与覆盖率口径第 4 轮 的状态条据此改写。产品态(宏回默认)重新构建 0 Error 0 Warning,Code 249,124 / RO 29,836 / RW 2,052 / ZI 149,996 与本阶段之前逐字节一致,阶段 16 在产品里被预处理干净。
This commit is contained in:
@@ -66,10 +66,38 @@ def payload(tag: int, n: int) -> bytes:
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def new_peer_socket() -> socket.socket:
|
||||
"""建一条 UDP 源套接句;能关就关掉 Windows 的「ICMP 端口不可达回抛」。
|
||||
|
||||
真机第一次上台架(2026-11-03 20:54)就是栽在这里:板子还没把 UDP socket 打开时,
|
||||
我们发过去的第一颗包换来一个 ICMP 端口不可达,Windows 把它记在套接句上,随后的
|
||||
recvfrom 直接抛 `ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个
|
||||
现有的连接`——对端脚本当场死在第一条用例,板子白等到 25 秒超时,两边看到的都是
|
||||
「对端缺席」,分不清是谁的问题。
|
||||
|
||||
能关掉这个行为当然最好(SIO_UDP_CONNRESET=0),但实测本机 CPython 3.14.7 既没有
|
||||
`socket.SIO_UDP_CONNRESET` 这个常量,也不接受把它的数值 0x9800000C 直接喂给 ioctl
|
||||
(`ValueError: invalid ioctl command`)。所以**主力防线是 recv 侧把 WinError 10054
|
||||
当成「这一轮没收到」继续重试**(见 _recv_after / _recv_maybe 的 except),这里的
|
||||
ioctl 只是有常量时的锦上添花。判据由 udp_queue_bench_loopback.py 的冷端口探针守着。
|
||||
"""
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
flag = getattr(socket, "SIO_UDP_CONNRESET", None)
|
||||
if flag is not None:
|
||||
try:
|
||||
s.ioctl(flag, False)
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return s
|
||||
|
||||
|
||||
class Peer:
|
||||
def __init__(self, board: str, timeout: float) -> None:
|
||||
def __init__(self, board: str, timeout: float, step_budget: float = 35.0) -> None:
|
||||
self.board = board
|
||||
self.timeout = timeout
|
||||
# 单步总预算:必须盖过板端单步等待(UDP_BENCH_WAIT_MS = 25 s)再加余量,理由见
|
||||
# exchange() 里的说明。--first-wait 只覆盖第一步(板子要启动+起监听)。
|
||||
self.step_budget = step_budget
|
||||
self.total = 0
|
||||
self.failed = 0
|
||||
self.ports = {}
|
||||
@@ -112,26 +140,29 @@ class Peer:
|
||||
# 先清掉上一步可能迟到的回显,避免把它当成本步的应答
|
||||
self._drain(sock)
|
||||
|
||||
deadline = time.time() + (first_wait if first_wait > 0 else self.timeout)
|
||||
# 每一步都重发,且单步预算必须盖过板端单步等待(UDP_BENCH_WAIT_MS = 25 s)再加
|
||||
# 余量:板端只要超时一次就会晚一步,本端若只等 8 秒,后面每一步都会踩进板子的
|
||||
# 上一步预算里,整条梯子一起错位(2026-11-03 第一次上台架就是这样全灭)。
|
||||
# 重发是安全的:板端按载荷第 0 字节的步号丢掉旧包(步序自愈),本端每步开头也
|
||||
# 会先 drain 掉迟到的回显。
|
||||
step_budget = first_wait if first_wait > 0 else self.step_budget
|
||||
deadline = time.time() + step_budget
|
||||
got = None
|
||||
addr = None
|
||||
while got is None:
|
||||
sock.sendto(data_out, dst)
|
||||
# 首步:板子可能还在启动/还没 bind,每 4 秒重发一次直到首步总预算用尽。
|
||||
# 重发不会污染下一步——下一步开头会先 drain 掉多出来的那一份回显。
|
||||
attempt = 4.0 if first_wait > 0 else self.timeout
|
||||
left = deadline - time.time()
|
||||
if left <= 0:
|
||||
return self.check(False, "TC 0x{0:02X} 端口 {1}: 板端无回显(预算内)"
|
||||
.format(tag, port))
|
||||
return self.check(False, "TC 0x{0:02X} 端口 {1}: 板端 {2:.0f}s 内无回显"
|
||||
.format(tag, port, step_budget))
|
||||
attempt = 4.0 if first_wait > 0 else 6.0
|
||||
try:
|
||||
got, addr = self._recv_after(sock, min(attempt, left))
|
||||
self.note_port(sock, tag)
|
||||
except socket.timeout:
|
||||
if first_wait <= 0:
|
||||
return self.check(False, "TC 0x{0:02X} 端口 {1}: 板端 {2:.0f}s 内无回显"
|
||||
.format(tag, port, self.timeout))
|
||||
self.log("TC 0x{0:02X}: 无回显,重发(首步剩余 {1:.0f}s)".format(tag, left))
|
||||
except (socket.timeout, ConnectionResetError):
|
||||
# ConnectionResetError = 对端端口当时没开换来的 ICMP 不可达(见
|
||||
# new_peer_socket 的说明);这里和「这一轮没收到」同等对待,继续重试。
|
||||
self.log("TC 0x{0:02X}: 无回显,重发(本步剩余 {1:.0f}s)".format(tag, left))
|
||||
# 一个数据报进、一个数据报出——唯一例外是本步载荷超过芯片发送缓冲(1KB):
|
||||
# 那是已登记的芯片限制(NET_SEND_CHUNK_MAX 本是 TCP 流的分块口径,UDP 上每块
|
||||
# 各发一包,改造前后同一条路径)。>1KB 的步骤按「预期拆包」判,并把实测拆分打
|
||||
@@ -188,7 +219,7 @@ class Peer:
|
||||
try:
|
||||
sock.settimeout(budget)
|
||||
return sock.recvfrom(4096)
|
||||
except socket.timeout:
|
||||
except (socket.timeout, ConnectionResetError):
|
||||
return None
|
||||
finally:
|
||||
sock.settimeout(self.timeout)
|
||||
@@ -212,6 +243,8 @@ def main() -> int:
|
||||
help="对应 TC-NET-1606 的静默时长(板端探针预算 1 秒)")
|
||||
ap.add_argument("--late", type=float, default=12.0,
|
||||
help="对应 TC-NET-1607 的迟发时长:板端在这一步无限等包")
|
||||
ap.add_argument("--step-budget", type=float, default=35.0,
|
||||
help="单步总预算(含重发),必须盖过板端 25 秒单步等待,默认 35 秒")
|
||||
ap.add_argument("--port", type=int, default=0,
|
||||
help="覆盖阶段 16 的首个监听端口(默认用板端 UDP_BENCH_PORT 6001)")
|
||||
ap.add_argument("--port2", type=int, default=0,
|
||||
@@ -225,15 +258,20 @@ def main() -> int:
|
||||
if not (7.0 < args.late <= 22.0):
|
||||
print("--late 必须落在 (7, 22] 秒内,否则板端 7000~25000ms 判定窗会判失败", file=sys.stderr)
|
||||
return 2
|
||||
if args.step_budget <= 26.0:
|
||||
# 只提醒不拦死:自检脚本用假板子跑,预算给得很小;真机台架则会看到这条。
|
||||
print("[warn] --step-budget={0:.0f}s 小于板端单步等待 25s + 余量:"
|
||||
"板端一旦超时一次,本端后续每一步都会踩进板子的上一步预算里(错位全灭)"
|
||||
.format(args.step_budget), file=sys.stderr)
|
||||
|
||||
peer = Peer(args.board, args.timeout)
|
||||
peer = Peer(args.board, args.timeout, args.step_budget)
|
||||
# 三条独立源套接句 = 三个不同临时源端口,1604「换源端口」就是这么来的。
|
||||
# 端口号不能在未 connect 时用 getsockname() 取(Windows 会 WinError 10022,
|
||||
# 端口要到第一次 sendto 才分配),也不能 connect()——connect 会让内核丢掉
|
||||
# 非对端的包,那正是这一步要验的东西。改成首次收包后再查,照样打得出来。
|
||||
socks = {"A": socket.socket(socket.AF_INET, socket.SOCK_DGRAM),
|
||||
"B": socket.socket(socket.AF_INET, socket.SOCK_DGRAM),
|
||||
"C": socket.socket(socket.AF_INET, socket.SOCK_DGRAM)}
|
||||
socks = {"A": new_peer_socket(),
|
||||
"B": new_peer_socket(),
|
||||
"C": new_peer_socket()}
|
||||
peer.log("对端 = {0},三条源套接句 A/B/C 的临时源端口在首次收包后打印".format(args.board))
|
||||
|
||||
# 板端每一步用新源端口的条件:1604(换源端口验形态恢复)及其后
|
||||
|
||||
Reference in New Issue
Block a user