scripts: 修 udp_probe.py 的重组与 fps 两处测量错误

探测脚本是诊断工具,测错比不测更糟——它给出的数字会被当成结论。

**重组状态必须按 (sender_id, frame_id) 存。** 原先只按 sender_id 存,于是丢片
之后下一帧的分片会接着凑上一帧的计数,凑够了交出去一张混着两帧的 JPEG,表现为
"JPEG 不完整"。修完:14 秒 132 帧,0 张坏帧。

换帧时被放弃的半成品单独计为"被新帧取代",不并入"JPEG 不完整"——它们本来就
不完整,混在一起会把两种情形都算成链路有问题。

**分窗 fps 的分子分母必须同量纲。** 原先用累计帧数除以窗口时长,于是报出 39、
61、79 这种随时间单调上升的假数字,看起来像板子越跑越快。改成窗口增量除以窗
口时长后稳定在 22,也就是传感器上限。

顺带修正两处与代码不符的注释:

  partitions.csv / sdkconfig.defaults
    两处都写死 app 分区在 0x40000,实际是 scripts/flash.sh 自动挑选并写回,
    当前为 0x060000。补上 flash.sh 会重编译并复验的原因(重编译会让镜像位移,
    所以偏移无法是常量)。

  .env.example
    补 CAM_VSYNC_INVERT 和 WIFI_AUTH。WIFI_AUTH 一直能用但没进文档:通常不用
    设,扫描会报告 AP 的真实认证方式;只有扫不到时(隐藏 SSID)才需要指定,
    指定后只试这一种,不成功直接报错而不是挨个试 10 秒——挨个试等于在猜一个
    本该已知的值。
This commit is contained in:
Horldsence
2026-10-05 19:13:37 +08:00
parent 7be8d7d2ba
commit c7f42a1979
4 changed files with 72 additions and 26 deletions
+48 -15
View File
@@ -103,6 +103,9 @@ def main():
sizes = []
start = time.time()
last_print = start
prev_frames = 0
prev_pkts = 0
prev_drops = 0
while time.time() - start < duration:
try:
@@ -135,15 +138,35 @@ def main():
counters["载荷超过总长"] += 1
continue
state = pending.setdefault(
sid,
{"buf": bytearray(h["total_len"]), "seen": 0, "expect": h["frag_count"],
"have": set(), "total": h["total_len"], "w": h["width"], "h": h["height"],
"started": time.time()},
)
# 重组状态必须按 (sender_id, frame_id) 存,缺一不可。
#
# 之前这里只按 sender_id 存,于是某一帧丢了分片之后,下一帧的分片会接着往
# 同一个计数器里凑,凑够了就交出去一张混着两帧的坏 JPEG——报成 34 张
# "JPEG 不完整",看起来像重复分片或丢包的证据,其实是这个键选错了。
# protocol crate 的 FrameAssembler 一直按 frame_id 区分(换个 frame_id
# 就重置),这里原先跟它不一致。
key = (sid, h["frame_id"])
# 重复分片直接跳过且不计数:否则一个丢失的分片会被副本凑数,
# seen 达标而缓冲区留空洞,交出去一张尾部是 0 的坏图。
state = pending.get(key)
if state is None:
# 新的一帧到了:丢掉之前那帧的半成品。实时画面宁可少一帧,也不等它。
# 这些帧算"被新帧取代",不是"JPEG 不完整"——它们本来就不完整,
# 混在一起会把两种情形都算成链路有问题。
counters["被新帧取代"] += len(pending)
pending = {
key: {
"buf": bytearray(h["total_len"]),
"have": set(),
"expect": h["frag_count"],
"total": h["total_len"],
"w": h["width"],
"h": h["height"],
"started": time.time(),
}
}
state = pending[key]
# 同一帧内的重复分片直接跳过且不计数:否则 seen 达标而缓冲区仍有空洞。
if h["frag_index"] in state["have"]:
counters["重复分片"] += 1
continue
@@ -151,26 +174,36 @@ def main():
off = h["frag_index"] * FRAG
state["buf"][off:off + len(payload)] = payload
state["seen"] += 1
tail = (h["total_len"] - 1) // FRAG
if state["seen"] >= state["expect"] and tail in state["have"]:
if len(state["have"]) >= state["expect"] and tail in state["have"]:
jpeg = bytes(state["buf"][:state["total"]])
if jpeg[:2] == b"\xff\xd8" and jpeg[-2:] == b"\xff\xd9":
counters["完整帧"] += 1
sizes.append((state["total"], state["w"], state["h"]))
else:
counters["JPEG 不完整"] += 1
del pending[sid]
del pending[key]
now = time.time()
if now - last_print >= 2.0:
# 分子分母必须都是本窗口的增量。早先用累计帧数除以窗口时长,于是报出
# 39、61、79 这种随时间单调上升的假 fps——看起来像板子越跑越快。
elapsed = now - last_print
good = counters["完整帧"]
window_frames = counters["完整帧"] - prev_frames
window_pkts = counters["type:video"] - prev_pkts
window_drops = counters["JPEG 不完整"] - prev_drops
loss = 100.0 * window_drops / max(window_frames + window_drops, 1)
print(
f" {good / elapsed:5.1f} fps "
f"视频包={counters['type:video']:5d} "
f"完整帧={good:4d} 丢弃={counters['JPEG 不完整']:3d}"
f" {window_frames / elapsed:5.1f} fps "
f"包/s={window_pkts / elapsed:5.0f} "
f"完整帧={counters['完整帧']:4d} "
f"丢弃={window_drops:3d} ({loss:.1f}%)"
)
prev_frames, prev_pkts, prev_drops = (
counters["完整帧"],
counters["type:video"],
counters["JPEG 不完整"],
)
last_print = now