7efa58d9a1
分两段(用户裁定 2026-11-02):先做地址迁移,再把同类字面量收敛成
单一真源,以后再改地址只动一处。
一、IP 迁移 192.168.1.100 → 192.168.31.100(gw 同步 → .31.1)
- 工厂默认:Drivers/BSP/NETCFG/net_cfg.c 的 NET_CFG_DEF_IP/_GW
- 注释/文档示例、主机与板级测试、台架 py 脚本、两份 sim_config.ini
- 字节序教学点同步换算 hex:0x6401A8C0 → 0x641FA8C0(内存
C0 A8 1F 64),旧版示例引文与历史 Trap 记录按冻结纪律不动
- UI 黄金基准:网口设置 4 态 + shot_info 共 5 张 .bin 由
check_content_pages.py --update 重刷(渲染值 IP 变了,属预期变更)
- 保留判定:客户点表引用(IEC104/继电保护规划)、芯片手册摘录、
日期冻结的变更记录行、已退役台架档案 MDK-ARM/retracted_bench_*
二、单一真源收口(A+B+C+D,用户批准全做)
- 新建 test/host/bench_defaults.py:台架 py 默认值唯一定义点
(DEVICE_IP/DEVICE_GW/PC_IP/NET_BCAST),22 个脚本改为引用;
MDK-ARM 两探针与 scripts/board_ui_nav.py 经 sys.path shim 引入
- test/board/net_test_task.h 新增 TEST_PC_PEER_IP,板测三处
PHASE*_REMOTE_IP 改引该宏(保留 -D 覆盖);Phase1 断言改
memcmp 数组比较,数组各只出现一次
- simulator/tests/check_content_pages.py 从 net_cfg.c 正则解析
NET_CFG_DEF_* 推导期望值(解析失败即报错、不静默兜底),
脚本内台架 IP 字面量清零
- 装饰性注释去具体值(net_cfg.h/net_socket.h/net_socket.c/
net_socket_sim.c);字节序教学 hex 对刻意保留为唯一写值点
- 刻意不动:主机单测夹具 IP(逐文件钉住的任意值是正确测试
实践)、文档正文 IP(那是文档内容本身)
顺带:scripts/gen_task_table.py 规范串清掉「,B5」——编号清退提交
72b4b094 改了受管区没重跑生成器,task-table 门禁因此一直红
验证:产品 @build 0/0;CH395F 套件开关构建 0/0 后还原产品态;
6 套主机单测 rc=0;content_pages(含宏推导路径)/菜单 8 态/
推流位比对全 PASS;bom-drift 99 无漂移;doc-links/coding-standard/
cppcheck 绿;py_compile 23 文件 + --help 冒烟(含 shim 绝对路径)
222 lines
8.7 KiB
Python
222 lines
8.7 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""RemoDispBus(7003) 拉帧压力客户端:「双通道并发回归」的网络侧激励。
|
||
|
||
做什么:
|
||
连设备 IP(默认见 bench_defaults.DEVICE_IP):7003,先发 INIT 查询屏幕参数(应得 8 B:宽u16/高u16/帧字节数u32,
|
||
大端),随后按固定节拍持续发 LCDMEM(偏移 0) 拉整帧 3200 B 显存,逐帧校验
|
||
XOR 校验和与长度,统计请求数 / 应答数 / CRC 错 / 超时 / 往返延迟 min-avg-max。
|
||
可 --dump last_frame.pbm 存最后一帧(1bpp 布局与屏板帧 B 点阵段逐字节同构)。
|
||
|
||
为什么它能当 4-1 的激励:
|
||
7003 的每次 LCDMEM 往返在固件里是「uiTask 入队 + 等 netTask 回执」
|
||
(osMessageQueuePut(osWaitForever) + xTaskNotifyWait(portMAX_DELAY),最坏 ≈4 s,
|
||
见 ui_net_sink.h 的 ⚠),即**每一拍拉帧都在真实占用 uiTask**——这正是
|
||
「7003 拉帧期间屏板不超时」要压的条件。屏板侧应答起在 USART1 空闲中断里,
|
||
理论上不被 uiTask 拖住;4-1 的判据就是拿两边的数交叉证明这件事:
|
||
网络侧(本脚本):超时 ≈0、延迟分布正常;
|
||
固件侧(日志口 grep SBLINK):tx 跟得上 rx(drop 无新增)、bad=0、kerr=0。
|
||
|
||
协议(Drivers/BSP/UI/ui_net_sink.c 实测口径):
|
||
帧 = <TAG 1B> <CMD 1B> <LEN u16 大端> <payload LEN B> <XOR(payload) 1B>
|
||
客户端 TAG=0xAA,设备 TAG=0xBB;CMD:0=KEEP 1=INIT 2=KEY 3=LCDMEM。
|
||
LCDMEM 请求 payload = 偏移 u32 大端(仅支持 0);应答 payload = 偏移 u32 + 3200 B 位图。
|
||
(设备对 CRC 只做异或累加——与屏板链路的 CRC-16 是两套,别混。)
|
||
|
||
用法(仓库根目录,先清代理再跑——本机有 HTTP 代理会劫持内网连接):
|
||
& '.venv\\Scripts\\python.exe' test/host/remolcd_pull_test.py --seconds 30
|
||
--hz 10 拉帧节拍(默认 10 = 每秒 10 次,重压;1 次/秒 ≈ DTU-RemoteLCD 观感)
|
||
--dump x.pbm 退出前把最后一帧写成 PBM(160x160 1bpp,可直接图片查看)
|
||
|
||
退出码:0 = 全程无超时且 CRC 全对;1 = 出现超时/CRC 错/断连;2 = 用法错误。
|
||
|
||
设计依据:docs/子系统设计/屏板链路设计说明.md §9.1(双通道并发)。
|
||
创建日期:2026-10-30
|
||
"""
|
||
import argparse
|
||
import socket
|
||
import struct
|
||
import sys
|
||
import time
|
||
|
||
import bench_defaults # 台架连接默认值单一真源
|
||
|
||
DEFAULT_IP = bench_defaults.DEVICE_IP
|
||
DEFAULT_PORT = 7003
|
||
TAG_CLIENT = 0xAA
|
||
TAG_DEVICE = 0xBB
|
||
CMD_KEEP = 0
|
||
CMD_INIT = 1
|
||
CMD_LCDMEM = 3
|
||
FB_SIZE = 3200
|
||
SCREEN_W = 160
|
||
SCREEN_H = 160
|
||
|
||
|
||
def build_frame(cmd, payload=b""):
|
||
"""组一帧客户端请求:AA <cmd> <len16be> <payload> <xor>。"""
|
||
body = bytes([TAG_CLIENT, cmd, (len(payload) >> 8) & 0xFF, len(payload) & 0xFF])
|
||
xor = 0
|
||
for b in payload:
|
||
xor ^= b
|
||
return body + payload + bytes([xor])
|
||
|
||
|
||
class FrameReader:
|
||
"""设备应答流重组(逐字节状态机,TAG 定界——与固件侧同思路,噪声字节自动重同步)。"""
|
||
|
||
def __init__(self):
|
||
self.buf = bytearray()
|
||
|
||
def pop(self):
|
||
"""从缓冲里取一个完整 (cmd, payload);没有则 None。坏帧丢弃继续找下一个 TAG。"""
|
||
while True:
|
||
i = self.buf.find(bytes([TAG_DEVICE]))
|
||
if i < 0:
|
||
del self.buf[:] # 整段都不是帧头:全弃
|
||
return None
|
||
if i > 0:
|
||
del self.buf[:i] # 丢帧头前的噪声
|
||
if len(self.buf) < 4:
|
||
return None
|
||
cmd = self.buf[1]
|
||
ln = (self.buf[2] << 8) | self.buf[3]
|
||
total = 4 + ln + 1
|
||
if len(self.buf) < total:
|
||
return None
|
||
payload = bytes(self.buf[4:4 + ln])
|
||
crc = self.buf[4 + ln]
|
||
del self.buf[:total]
|
||
xor = 0
|
||
for b in payload:
|
||
xor ^= b
|
||
if crc != xor:
|
||
return ("CRCERR", payload)
|
||
return (cmd, payload)
|
||
|
||
def feed(self, data):
|
||
self.buf += data
|
||
|
||
|
||
def run(args):
|
||
try:
|
||
s = socket.create_connection((args.ip, args.port), timeout=5.0)
|
||
except OSError as exc:
|
||
print("[ERR] 连 %s:%d 失败:%s" % (args.ip, args.port, exc))
|
||
print(" (若在内网却报超时/奇怪拒绝:先清 HTTP 代理再跑,见文件头用法行)")
|
||
return 1
|
||
s.settimeout(5.0)
|
||
rd = FrameReader()
|
||
|
||
def wait_frame(want_cmd):
|
||
"""收一个 cmd==want_cmd 的应答;CRC 错返回 ('CRCERR', None);超时返回 None。"""
|
||
t0 = time.monotonic()
|
||
while time.monotonic() - t0 < 5.0:
|
||
f = rd.pop()
|
||
if f is not None:
|
||
cmd, payload = f
|
||
if cmd == "CRCERR":
|
||
return ("CRCERR", None)
|
||
if cmd == want_cmd:
|
||
return (cmd, payload)
|
||
# 其它命令的应答(如残留 KEEP 回显):跳过继续找
|
||
continue
|
||
try:
|
||
data = s.recv(4096)
|
||
except socket.timeout:
|
||
return None
|
||
if not data:
|
||
return "CLOSED"
|
||
rd.feed(data)
|
||
return None
|
||
|
||
# ---- INIT:查询屏幕参数,顺带把几何钉死 ----
|
||
s.sendall(build_frame(CMD_INIT))
|
||
r = wait_frame(CMD_INIT)
|
||
if r is None or r == "CLOSED" or r[0] == "CRCERR":
|
||
print("[ERR] INIT 无有效应答(%s)" % (r,))
|
||
s.close()
|
||
return 1
|
||
_, payload = r
|
||
if len(payload) >= 8:
|
||
w, h, nbytes = struct.unpack(">HHI", payload[:8])
|
||
print("[INIT] %dx%d frame_bytes=%u" % (w, h, nbytes))
|
||
if w != SCREEN_W or h != SCREEN_H or nbytes != FB_SIZE:
|
||
print("[WARN] INIT 几何与预期 160x160/3200 不符——继续,以设备为准")
|
||
else:
|
||
print("[WARN] INIT 应答长度 %d < 8,跳过几何核对" % len(payload))
|
||
|
||
# ---- LCDMEM 主循环 ----
|
||
n_req = n_resp = n_crc = n_to = 0
|
||
lat_sum = lat_min = lat_max = 0.0
|
||
last_bmp = None
|
||
period = 1.0 / args.hz
|
||
t_end = time.monotonic() + args.seconds
|
||
next_at = time.monotonic()
|
||
while time.monotonic() < t_end:
|
||
now = time.monotonic()
|
||
if now < next_at:
|
||
time.sleep(min(next_at - now, 0.005))
|
||
continue
|
||
t0 = time.monotonic()
|
||
s.sendall(build_frame(CMD_LCDMEM, struct.pack(">I", 0)))
|
||
n_req += 1
|
||
r = wait_frame(CMD_LCDMEM)
|
||
if r == "CLOSED":
|
||
print("[ERR] 第 %d 次拉帧时对端断开" % n_req)
|
||
s.close()
|
||
break
|
||
if r is None:
|
||
n_to += 1
|
||
print("[WARN] #%d LCDMEM 超时(5 s)" % n_req)
|
||
else:
|
||
cmd_got, payload = r
|
||
if cmd_got == "CRCERR":
|
||
n_crc += 1
|
||
print("[WARN] #%d CRC 错" % n_req)
|
||
elif len(payload) != 4 + FB_SIZE:
|
||
n_crc += 1
|
||
print("[WARN] #%d 应答长度 %d != %d" % (n_req, len(payload), 4 + FB_SIZE))
|
||
else:
|
||
dt = time.monotonic() - t0
|
||
n_resp += 1
|
||
lat_sum += dt
|
||
lat_min = dt if lat_min == 0 else min(lat_min, dt)
|
||
lat_max = max(lat_max, dt)
|
||
last_bmp = payload[4:]
|
||
next_at += period
|
||
if next_at < time.monotonic() - 1.0: # 落后太多不追(避免突发风暴)
|
||
next_at = time.monotonic()
|
||
|
||
s.close()
|
||
avg = (lat_sum / n_resp * 1000.0) if n_resp else 0.0
|
||
print("[STAT] 请求 %d / 应答 %d / CRC错 %d / 超时 %d" % (n_req, n_resp, n_crc, n_to))
|
||
print("[STAT] 往返延迟 min %.1f / avg %.1f / max %.1f ms"
|
||
% (lat_min * 1000.0, avg, lat_max * 1000.0))
|
||
if args.dump and last_bmp:
|
||
with open(args.dump, "wb") as f:
|
||
f.write(b"P4\n%d %d\n" % (SCREEN_W, SCREEN_H))
|
||
f.write(last_bmp)
|
||
print("[DUMP] 最后一帧已写 %s(P4 PBM,与屏板帧 B 点阵段同构)" % args.dump)
|
||
ok = (n_to == 0) and (n_crc == 0) and (n_resp > 0)
|
||
print("[VERDICT] %s" % ("PASS" if ok else "FAIL"))
|
||
return 0 if ok else 1
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||
ap.add_argument("--ip", default=DEFAULT_IP)
|
||
ap.add_argument("--port", type=int, default=DEFAULT_PORT)
|
||
ap.add_argument("--seconds", type=float, default=30.0)
|
||
ap.add_argument("--hz", type=float, default=10.0, help="拉帧节拍(次/秒,默认 10)")
|
||
ap.add_argument("--dump", default="", help="退出前把最后一帧写成的 PBM 路径")
|
||
args = ap.parse_args()
|
||
if args.seconds <= 0 or args.hz <= 0:
|
||
print("[ERR] --seconds / --hz 必须为正")
|
||
return 2
|
||
return run(args)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|