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
+10 -1
View File
@@ -5,7 +5,16 @@ WIFI_SSID=your-ssid
WIFI_PASS=your-password
# Optional
# Capture resolution: qqvga | qvga | cif | hvga | vga | svga | xga | sxga | uxga
#CAM_RES=vga
# JPEG quality 0-63, lower is better quality
#CAM_QUALITY=12
#CAM_QUALITY=12
# Set to 0 if your board inverts VSYNC in hardware.
#CAM_VSYNC_INVERT=1
# Pin the authentication mode instead of trusting the scan: none | wep | wpa |
# wpa2 | wpawpa2 | wpa3 | wpa2wpa3 | wapi. Only needed when the scan cannot see
# the AP (a hidden SSID, for instance) - otherwise the scan reports the real
# mode and this only makes it fail faster.
#WIFI_AUTH=wpa2
+11 -8
View File
@@ -1,14 +1,17 @@
# Name, Type, SubType, Offset, Size, Flags
#
# The app partition starts at 0x40000 rather than the usual 0x10000 on purpose:
# the module this firmware was first brought up on has a defective byte in its
# SPI flash at 0xFADE7 (bits 5..7 cannot be programmed). The app image is
# unchanged by where it is placed, so moving the partition moves which image byte
# lands on that address - and at 0x40000 the byte there is 0x1A, which the flash
# accepts.
# The app partition offset here is chosen by scripts/flash.sh, not by hand. It
# sits well above the usual 0x10000 because the module this firmware was first
# brought up on has a defective byte in its SPI flash at 0xFADE7 (bits 5..7
# cannot be programmed). The app image is unchanged by where it is placed, so
# moving the partition moves which image byte lands on that address - and
# because a rebuild can shift the image again, re-deriving the offset by hand
# would break on the next build. flash.sh picks the lowest 64 KB-aligned offset
# whose byte at that address the flash can actually store, writes it back here,
# and rebuilds.
#
# If you move to healthy hardware, put `factory` back at 0x10000 and delete the
# note in sdkconfig.defaults; nothing else depends on this.
# If you move to healthy hardware, put `factory` back at 0x10000 and delete
# BAD_FLASH_ADDR from scripts/flash.sh; nothing else depends on this.
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x060000, 0x120000,
1 # Name, Type, SubType, Offset, Size, Flags
2 #
3 # The app partition starts at 0x40000 rather than the usual 0x10000 on purpose: # The app partition offset here is chosen by scripts/flash.sh, not by hand. It
4 # the module this firmware was first brought up on has a defective byte in its # sits well above the usual 0x10000 because the module this firmware was first
5 # SPI flash at 0xFADE7 (bits 5..7 cannot be programmed). The app image is # brought up on has a defective byte in its SPI flash at 0xFADE7 (bits 5..7
6 # unchanged by where it is placed, so moving the partition moves which image byte # cannot be programmed). The app image is unchanged by where it is placed, so
7 # lands on that address - and at 0x40000 the byte there is 0x1A, which the flash # moving the partition moves which image byte lands on that address - and
8 # accepts. # because a rebuild can shift the image again, re-deriving the offset by hand
9 # would break on the next build. flash.sh picks the lowest 64 KB-aligned offset
10 # whose byte at that address the flash can actually store, writes it back here,
11 # and rebuilds.
12 #
13 # If you move to healthy hardware, put `factory` back at 0x10000 and delete the # If you move to healthy hardware, put `factory` back at 0x10000 and delete
14 # note in sdkconfig.defaults; nothing else depends on this. # BAD_FLASH_ADDR from scripts/flash.sh; nothing else depends on this.
15 nvs, data, nvs, 0x9000, 0x6000,
16 phy_init, data, phy, 0xf000, 0x1000,
17 factory, app, factory, 0x060000, 0x120000,
+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
+3 -2
View File
@@ -35,8 +35,9 @@ CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB=y
# only 1 MB - smaller than the ~1.15 MB this firmware produces, so the image
# overflows the partition.
#
# The custom table also moves the app to 0x40000, which is what keeps the image
# off the module's defective flash byte; see partitions.csv for why.
# The custom table also carries an app offset that keeps the image off the
# module's defective flash byte. That offset is written by scripts/flash.sh, not
# fixed here; see partitions.csv for why it cannot be a constant.
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="../../../../../../partitions.csv"
CONFIG_PARTITION_TABLE_OFFSET=0x8000