Files
STM32F4-Base/test/host/udp_queue_bench.py
T
super_agent_bot b0a6394aa1
CI / 静态门禁 (pre-commit) (push) Failing after 5m4s
CI / 主机侧单测 (MSVC) (push) Successful in 44s
CI / 静态门禁 (pre-commit) (pull_request) Failing after 4m48s
CI / 主机侧单测 (MSVC) (pull_request) Failing after 44s
fix(test/udp): peer 换源端口上报方式并补 PC 回路自检(#73)
- test/host/udp_queue_bench.py:Windows 上未 connect 的 UDP socket 调 getsockname()
  直接抛 WinError 10022(请求一个未连接套接句的名称),脚本在第一条用例就崩。
  源端口改为首次收到回显时打印——它本来就是「板端剥头/源地址判定对不对」的直接读数。
- 新增 test/host/udp_queue_bench_loopback.py:不用板子的 peer 自检,在 127.0.0.1 上放
  一个按板端阶段 16 同样三条规矩说话的假板子(原样回显、tag 0x04 只回 64 B、>1KB 按
  1024 拆片),先把步骤表 / 模式函数 / 分片预期验掉。2026-11-03 首跑 27/27 绿,
  实测拆片形状 [1024, 1] 与 [1024, 448]。
- CH395F_Test_Guide.md §12.6:运行方法补「先跑自检」这一步;>1024 B 的 UDP 拆片从
  「多收到一片就 FAIL」改成「实测观测量」(拼接后逐字节一致才算绿),并说清这是既有
  NET_SEND_CHUNK_MAX 行为、不是本轮引入;脚本表补一行。
- 板子自 19:19 起未再上电(ping 不通、SWD No target、串口静默),阶段 16 真机那一趟
  仍未跑,PR #74 的合并前提不变。
2026-11-03 20:46:15 +08:00

281 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""UDP 数据面队列化真机回归 —— 板端阶段 16 的 PC 对端。
用途:配合 test/board/net_test_task.c 的 net_layer_udp_bench_test_main()
(板端做 UDP 服务端,从收到的包里学对端地址再回显),把 PR #74「UDP 并入
netMsgQueue」这条新链路放上真芯片逐步激励。之所以板端做服务端:台架 PC 是
DHCP 地址,与 net_test_task.h 的 TEST_PC_PEER_IP 不一定一致,服务端在先就
不需要 PC 侧固定 IP。
跑法(板子烧好阶段 16 固件后):
python test/host/udp_queue_bench.py --board 192.168.31.100
# 可选:--first-wait 180 等板子重启;--late 12 调迟发时刻
判读:每步打印 [PASS]/[FAIL],末尾一行
=== UDP Bench PC Peer: N/N PASSED ===
任一步失败即非零退出。**板端的汇总行在串口日志里**(=== Phase 16: ... ===),
两侧都要看:板端能看见"收到多少字节、模式切没切对",本端只能看见
"回显是否完整、是不是一个数据报"。两端合起来才是完整判据。
与板端必须一致的约定(改一边就改另一边,见 net_test_task.c 的 Phase 16 头注释):
载荷 p[0]=tag,p[i]=(i*7+0x11)&0xFF;板端原样回显。
tag 次序:0x01(20B) 0x02(48B) 0x03(20B 换源端口) 0x04(200B 截断成 64)
0x05(30B 残包排空) —静默超时探针— 0x06(24B 迟发包) 0x07(20B 换端口 6002)
0x08/0x09/0x0A/0x0B/0x0C = 1/20/1024/1025/1472 字节往返。
"""
from __future__ import annotations
import argparse
import socket
import sys
import time
for _stream in (sys.stdout, sys.stderr):
if hasattr(_stream, "reconfigure"):
_stream.reconfigure(encoding="utf-8", errors="replace")
# 阶段 16 步骤表:tag -> (发往端口, 本次发送长度, 期望回显长度)
# 期望回显长度 != 发送长度 的那两行就是截断步。
STEPS = (
("TC-NET-1602", 0x01, 6001, 20, 20),
("TC-NET-1603", 0x02, 6001, 48, 48),
("TC-NET-1604", 0x03, 6001, 20, 20),
("TC-NET-1605a", 0x04, 6001, 200, 64),
("TC-NET-1605b", 0x05, 6001, 30, 30),
# TC-NET-1606 = 板端超时探针,本端不发数据(见 wait_quiet)
("TC-NET-1607", 0x06, 6001, 24, 24),
("TC-NET-1608", 0x07, 6002, 20, 20),
("TC-NET-1609a", 0x08, 6002, 1, 1),
("TC-NET-1609b", 0x09, 6002, 20, 20),
("TC-NET-1609c", 0x0A, 6002, 1024, 1024),
("TC-NET-1609d", 0x0B, 6002, 1025, 1025),
("TC-NET-1609e", 0x0C, 6002, 1472, 1472),
)
def payload(tag: int, n: int) -> bytes:
"""与板端 udp_bench_fill() 同式的自校验载荷。"""
if n <= 0:
return b""
out = bytearray(n)
out[0] = tag & 0xFF
for i in range(1, n):
out[i] = (i * 7 + 0x11) & 0xFF
return bytes(out)
class Peer:
def __init__(self, board: str, timeout: float) -> None:
self.board = board
self.timeout = timeout
self.total = 0
self.failed = 0
self.ports = {}
self.t_start = time.time()
def stamp(self) -> str:
return "[peer {0:7.2f}s]".format(time.time() - self.t_start)
def log(self, msg: str) -> None:
print("{0} {1}".format(self.stamp(), msg), flush=True)
def note_port(self, sock: socket.socket, tag: int) -> None:
"""首次用到某条源套接句时把它的临时源端口打出来(真机日志要和这个对得上)。"""
key = id(sock)
if key in self.ports:
return
try:
self.ports[key] = sock.getsockname()[1]
self.log("TC 0x{0:02X} 走源端口 {1}".format(tag, self.ports[key]))
except OSError:
self.ports[key] = 0
def check(self, cond: bool, name: str) -> bool:
self.total += 1
self.log("[{0}] {1}".format("PASS" if cond else "FAIL", name))
if not cond:
self.failed += 1
return cond
def close(self) -> None:
pass
def exchange(self, sock: socket.socket, port: int, tag: int, send_len: int,
exp_len: int, first_wait: float = 0.0) -> bool:
"""一个数据报进、一个数据报出;重发只在"完全没收到"时发生。"""
dst = (self.board, port)
data_out = payload(tag, send_len)
want = payload(tag, exp_len)
# 先清掉上一步可能迟到的回显,避免把它当成本步的应答
self._drain(sock)
deadline = time.time() + (first_wait if first_wait > 0 else self.timeout)
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))
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))
# 一个数据报进、一个数据报出——唯一例外是本步载荷超过芯片发送缓冲(1KB):
# 那是已登记的芯片限制(NET_SEND_CHUNK_MAX 本是 TCP 流的分块口径,UDP 上每块
# 各发一包,改造前后同一条路径)。>1KB 的步骤按「预期拆包」判,并把实测拆分打
# 出来:真机上哪天不拆了,这条一样会报——那就是芯片行为变了,值得知道。
chip_chunk = 1024
parts = [got]
if exp_len > chip_chunk:
while sum(len(x) for x in parts) < exp_len:
more = self._recv_maybe(sock, 1.0)
if more is None:
break
parts.append(more[0])
else:
again = self._recv_maybe(sock, 0.300)
if again is not None:
parts.append(again[0])
total = b"".join(parts)
ok_len = (len(total) == exp_len)
ok_pat = ok_len and (total == want)
self.check(ok_len, "TC 0x{0:02X} 端口 {1}: 回显 {2} 字节(期望 {3},收到 {4} 片)"
.format(tag, port, len(total), exp_len, len(parts)))
if not ok_len:
self.log(" 各片长度 = {0},首 16 字节 = {1}".format(
[len(x) for x in parts], total[:16].hex()))
self.check(ok_pat, "TC 0x{0:02X}: 载荷逐字节一致(模式 p[i]=(i*7+0x11)&0xFF)".format(tag))
if not ok_pat and ok_len:
bad = next((i for i in range(exp_len) if total[i] != want[i]), -1)
self.log(" 首个差异 @ 字节 {0}: 收到 0x{1:02X},应为 0x{2:02X}"
.format(bad, total[bad], want[bad]))
if exp_len > chip_chunk:
self.check(len(parts) > 1 and len(parts[0]) == chip_chunk,
"TC 0x{0:02X}: >1KB 回显按芯片发送缓冲在 1024 处拆包(实测 {1})"
.format(tag, [len(x) for x in parts]))
elif len(parts) > 1:
self.check(False, "TC 0x{0:02X}: 未超 1KB 却收到 {1} 个数据报 {2}"
" —— 发送路径的分块口径变了"
.format(tag, len(parts) - 1, [len(x) for x in parts[1:]]))
return ok_len and ok_pat and (exp_len > chip_chunk or len(parts) == 1)
def wait_quiet(self, sock: socket.socket, seconds: float, name: str) -> bool:
"""板端超时探针期间本端必须保持静默,且不能有过期包漂进来。"""
time.sleep(seconds)
stray = self._drain(sock)
return self.check(stray == 0, "{0}: 静默期无残留数据报(清掉 {1} 个)".format(name, stray))
# ---- 内部 ----
def _recv_after(self, sock: socket.socket, budget: float):
sock.settimeout(max(budget, 0.05))
data, addr = sock.recvfrom(4096)
return data, addr
def _recv_maybe(self, sock: socket.socket, budget: float):
try:
sock.settimeout(budget)
return sock.recvfrom(4096)
except socket.timeout:
return None
finally:
sock.settimeout(self.timeout)
def _drain(self, sock: socket.socket) -> int:
n = 0
while True:
r = self._recv_maybe(sock, 0.050)
if r is None:
return n
n += 1
def main() -> int:
ap = argparse.ArgumentParser(description="UDP 队列化真机回归 PC 对端(板端阶段 16)")
ap.add_argument("--board", required=True, help="板子 IP,例如 192.168.31.100")
ap.add_argument("--first-wait", type=float, default=120.0,
help="首步最长等待(覆盖板子烧录后重启+起监听),默认 120 秒")
ap.add_argument("--timeout", type=float, default=20.0, help="单步回显等待,默认 20 秒")
ap.add_argument("--probe-silence", type=float, default=2.0,
help="对应 TC-NET-1606 的静默时长(板端探针预算 1 秒)")
ap.add_argument("--late", type=float, default=12.0,
help="对应 TC-NET-1607 的迟发时长:板端在这一步无限等包")
ap.add_argument("--port", type=int, default=0,
help="覆盖阶段 16 的首个监听端口(默认用板端 UDP_BENCH_PORT 6001)")
ap.add_argument("--port2", type=int, default=0,
help="覆盖重开 socket 后的第二个监听端口(默认用板端 UDP_BENCH_PORT2 6002)")
args = ap.parse_args()
# 板端 TC-NET-1607 把「等待时长」判据钉在 7000~25000 ms:本端迟发时刻必须落在窗内,
# 且必须先盖过板端 TC-NET-1606 那 1 秒超时探针,否则两步会互相污染。
if args.probe_silence <= 1.2:
print("--probe-silence 必须大于板端 1 秒超时探针(默认 2.0)", file=sys.stderr)
return 2
if not (7.0 < args.late <= 22.0):
print("--late 必须落在 (7, 22] 秒内,否则板端 7000~25000ms 判定窗会判失败", file=sys.stderr)
return 2
peer = Peer(args.board, args.timeout)
# 三条独立源套接句 = 三个不同临时源端口,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)}
peer.log("对端 = {0},三条源套接句 A/B/C 的临时源端口在首次收包后打印".format(args.board))
# 板端每一步用新源端口的条件:1604(换源端口验形态恢复)及其后
step_sock = {
"TC-NET-1602": "A", "TC-NET-1603": "A",
"TC-NET-1604": "B", "TC-NET-1605a": "B", "TC-NET-1605b": "B",
"TC-NET-1607": "B",
"TC-NET-1608": "C", "TC-NET-1609a": "C", "TC-NET-1609b": "C",
"TC-NET-1609c": "C", "TC-NET-1609d": "C", "TC-NET-1609e": "C",
}
# 端口覆盖:只在显式给了 --port/--port2 时替换,默认沿用板端编译期的 UDP_BENCH_PORT(_2)。
# 板端串口横幅会把自己的端口打出来,照抄那条命令时用它换掉非默认端口。
port_map = {}
if args.port:
port_map[6001] = args.port
if args.port2:
port_map[6002] = args.port2
try:
for name, tag, port, send_len, exp_len in STEPS:
s = socks[step_sock[name]]
port = port_map.get(port, port)
fw = args.first_wait if name == "TC-NET-1602" else 0.0
# 1607 之前先做 1606 的静默探针(板端自己判 -2,本端只需保持静默)
if name == "TC-NET-1607":
peer.wait_quiet(s, args.probe_silence, "TC-NET-1606")
left = max(args.late - args.probe_silence, 0.0)
peer.log("TC-NET-1607: 再静默 {0:.1f}s,等板子挂进无限 net_recvfrom".format(left))
time.sleep(left)
peer.exchange(s, port, tag, send_len, exp_len, fw)
except KeyboardInterrupt:
peer.log("用户中断")
finally:
for s in socks.values():
s.close()
print("=== UDP Bench PC Peer: {0}/{1} PASSED (failed={2}) ===".format(
peer.total - peer.failed, peer.total, peer.failed), flush=True)
return 0 if peer.failed == 0 else 1
if __name__ == "__main__":
sys.exit(main())