Files
STM32F4-Base/test/host/udp_queue_bench.py
T
super_agent_bot 8a81fa9e41
CI / 静态门禁 (pre-commit) (push) Failing after 5m27s
CI / 主机侧单测 (MSVC) (push) Successful in 33s
CI / 静态门禁 (pre-commit) (pull_request) Failing after 7m6s
CI / 主机侧单测 (MSVC) (pull_request) Successful in 48s
test(net/udp): 阶段 16 UDP 数据面真机回归台架(板端 + PC peer),台账补实测数字(#73)
- 板端 net_layer_udp_bench_test_main:TC-NET-1601~1609,逐步覆盖主机测替代不了的
  5 条台架必验——形态切换与 bind 后首包、48 B 应答在客户端形态的载荷完整(TRAP-NET-22
  的真机对照)、截断包排空、挂起等待期间的 IWDG 余量、DesIP 先于 OPEN_SOCKET 的顺序。
  每步都有 25 s 预算,PC 缺席时报 [FAIL] 而不是把 testTask 钉死。
- PC peer test/host/udp_queue_bench.py:板端先 bind 当服务端,对端地址从收到的包里学
  (PC 是 DHCP,TEST_PC_PEER_IP 那个固定地址不在本机上);对 >1024 B 的回显额外监听
  第二片,把 net_send_locked 分块在 UDP 上的后果量出来。
- ch395f_test.h:阶段 16 宏默认关(与其它阶段一致),RAM 台账补 2026-11-03 实测——
  台架镜像 RW_IRAM1 96,232 B / 73.4%,阶段 16 自身代价 1,600 B;产品镜像 RW+ZI 与改造前逐字节相同。
- 文档:CH395F_Test_Guide §12.6 跑法与判据 + Python 脚本表加一行;覆盖率口径第 4 轮
  「台架必验」指向本阶段的可执行形态,并写明尚未在真机上跑过。
2026-11-03 20:18:24 +08:00

234 lines
10 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.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 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))
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))
# 一个数据报进必须只出一个数据报出:再等一小会儿看有没有第二片
again = self._recv_maybe(sock, 0.300)
extra = []
if again is not None:
extra.append(len(again[0]))
ok_len = (len(got) == exp_len)
ok_pat = ok_len and (got == want)
self.check(ok_len, "TC 0x{0:02X} 端口 {1}: 回显 {2} 字节(期望 {3})".format(tag, port, len(got), exp_len))
if not ok_len:
self.log(" 首 16 字节 = {0}".format(got[: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 got[i] != want[i]), -1)
self.log(" 首个差异 @ 字节 {0}: 收到 0x{1:02X},应为 0x{2:02X}".format(bad, got[bad], want[bad]))
if extra:
self.check(False, "TC 0x{0:02X}: 一个请求收到 {1} 个额外数据报 {2}"
" —— 回显被分片了(芯片发送缓冲 = 1KB,>1KB 的分块会各发一包)"
.format(tag, len(extra), extra))
return ok_len and ok_pat and not extra
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 的迟发时长:板端在这一步无限等包")
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)
sock_a = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock_b = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock_c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
socks = {"A": sock_a, "B": sock_b, "C": sock_c}
peer.log("对端 = {0},本机临时源端口 A={1} B={2} C={3}".format(
args.board, sock_a.getsockname()[1], sock_b.getsockname()[1], sock_c.getsockname()[1]))
# 板端每一步用新源端口的条件: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",
}
try:
for name, tag, port, send_len, exp_len in STEPS:
s = socks[step_sock[name]]
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())