From c7f42a1979c7823fa555fdd54721319d8405daae Mon Sep 17 00:00:00 2001 From: Horldsence Date: Mon, 5 Oct 2026 19:13:37 +0800 Subject: [PATCH] =?UTF-8?q?scripts:=20=E4=BF=AE=20udp=5Fprobe.py=20?= =?UTF-8?q?=E7=9A=84=E9=87=8D=E7=BB=84=E4=B8=8E=20fps=20=E4=B8=A4=E5=A4=84?= =?UTF-8?q?=E6=B5=8B=E9=87=8F=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 探测脚本是诊断工具,测错比不测更糟——它给出的数字会被当成结论。 **重组状态必须按 (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 秒——挨个试等于在猜一个 本该已知的值。 --- .env.example | 11 +++++++- partitions.csv | 19 +++++++------ scripts/udp_probe.py | 63 +++++++++++++++++++++++++++++++++----------- sdkconfig.defaults | 5 ++-- 4 files changed, 72 insertions(+), 26 deletions(-) diff --git a/.env.example b/.env.example index b86d6aa..454dbed 100644 --- a/.env.example +++ b/.env.example @@ -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 \ No newline at end of file +#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 diff --git a/partitions.csv b/partitions.csv index 2893cb8..e0cda0d 100644 --- a/partitions.csv +++ b/partitions.csv @@ -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, \ No newline at end of file diff --git a/scripts/udp_probe.py b/scripts/udp_probe.py index 3cba8bf..9ddd8ae 100644 --- a/scripts/udp_probe.py +++ b/scripts/udp_probe.py @@ -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 diff --git a/sdkconfig.defaults b/sdkconfig.defaults index f07734c..8f8bded 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -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