无线与 USB 拓展 / 无线实验发布 内容更新

ESP32-C5 双频 Wi-Fi 怎么比较:配网、覆盖和重连实验方法

用同一台C5和双频AP做成对探测,记录失败包、真实频段/国家设置和断线恢复;提供完整两种固件、LAN回显服务与拒绝坏记录的分析器,不虚构覆盖结果。

读完能做什么
用同一台C5和双频AP做成对探测,记录失败包、真实频段/国家设置和断线恢复;提供完整两种固件、LAN回显服务与拒绝坏记录的分析器,不虚构覆盖结果。
开始之前
先完成所列前置;能够在精确IDF环境构建、识别UART桥口、保存原始串口日志并断电追线。
所需器材
官方ESP32-C5-DevKitC-1-N8R8 / WROOM-1 PCB天线,指南v1.2/PW≥2025-04-0446,电路V1.2、USB-UART数据线、双频AP(独立SSID/同20MHz与鉴权策略)、有线LAN电脑/Python3.12、无需外接GPIO、LED或电流表;本页不做功耗测量
版本组合
板卡:C5 DevKitC-1-N8R8;指南v1.2/PW≥2025-04-0446;电路V1.2/2025-02-11SDK:ESP-IDF5.5.5 / b774170ff46c393eeb5e495ea37936038d3f4f4f存储/工具:8MB DIO80;PSRAM关闭;RV GCC14.2.0 esp-14.2.0_20260121链路:STA;WPA2/WPA3 Personal;20MHz/PS_NONE;UDP45680/24B
适用范围

仅C5 DevKitC-1-N8R8指南v1.2/电路V1.2;IDF5.5.5,两种限制频段STA镜像,可信LAN UDP。PSRAM关闭仍不复用GPIO15;无本站无线实验。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/wireless-c5-c6-sources.json;固定gh原文/已安装字节核对及板卡/SHT PDF实际阅读验证日期:
构建 / 配置已通过
docs/esp32-evidence/wireless-c5-c6-builds.json;C5 band2/band5最终stale-IP-guard镜像均实际Exit0,默认空配置不启动无线对象:ESP32-C5版本:ESP-IDF5.5.5验证日期:
仿真已通过
docs/esp32-evidence/wireless-c5-c6-hosts.json;同源11组帧/链路快照、16组探测表+11组关联表合成校验及真实localhost UDP5组;不是射频/芯片仿真验证日期:
本站实板待验证
无本站实板、入网、覆盖/时延/功耗或长期运行结果;观察CSV仅表头

构建通过只说明指定环境中的代码或配置通过检查;实物功能、功耗和稳定性以对应的实板记录为准。

01 / 无线实验

这次回答“在我的环境下怎样选”,先不给胜负

住处2.4GHz拥挤时,换到5GHz可能改变干扰、可用信道和穿墙表现;单凭芯片支持双频无法推断你的房间里一定更远、更快。本任务把同一台C5限制在单一频段,向同一台LAN电脑发送同样24字节消息,再按点位成对比较成功率、应用RTT和受控重连。

本站没有C5实板和AP测量。下面交付的是可执行实验方法与已经构建的源码;CSV只含表头,图中也不填预造的距离、RSSI或“提升百分比”。本页不跑最大吞吐、不测功耗,不把2.4GHz上的ESP-NOW/LR功能扩展成5GHz结论。

参考:[3] IDF5.5.5 C5 Wi-Fi:功能/事件、IP后建socket、断线重建

02 / 无线实验

先核对C5板型,收起原来S3的引脚设置

固定官方C5-DevKitC-1-N8R8/WROOM-1 PCB天线,指南v1.2的PW-2025-04-0446及以后,电路V1.2。该模组有8MB Flash和8MB PSRAM;工程用8MB DIO80、关闭PSRAM以保持变量简单,但GPIO15依然连接模组PSRAM CS,绝不是可以拿来接LED的空脚。

本例不接GPIO。只用标UART的USB-C桥口、保持J5跳帽;桥TX/RX是GPIO11/12,BOOT是GPIO28,RGB在GPIO27。USB这一类供电、5V排针供电、3V3排针供电互斥,本实验只选USB,不能接着USB再并一个排针电源。

需要固定为什么要记录
同一台双频AP与固件/天线朝向不同AP、功率和朝向会混入频段以外变量
两个独立SSID、同鉴权策略与20MHz避免band steering偷偷换频段/宽度
有线接LAN的电脑、固定IPv4减少电脑无线链路和DNS对往返测量的影响
真实国家/实际AP country、信道API允许的配置与设备实际关联信息分别保留
做到这里,你应该看到
板上型号/后缀、指南PW范围、USB桥口与两个AP SSID均可说清楚。

参考:[1] C5官方指南v1.2:N8R8/PW、GPIO15、供电与桥口[2] C5电路V1.2(2025-02-11)p2

03 / 无线实验

用两个目录构建,默认凭据为空时不启动无线

复用ESP-IDF5.5.5和匹配RISC-V14.2.0工具;main/idf_component.yml/dependencies.lock锁定目标,工程没有额外组件。main/config.c单独保存占位SSID/密码/两字符国家码/电脑IPv4;默认空值使CONFIG_DISABLED在初始化Wi-Fi之前返回。把凭据留在本地,不提交到公开站点。

先填2.4GHz SSID并构建build-2g,再换5GHz SSID构建build-5g。两个镜像都固定HT20与PS_NONE。set_band_mode要求Wi-Fi已经start,所以回调不在STA_START抢先connect,主任务设置频段后才开始连接。编译常量2/5限制频段,不表示同时利用两频段传包。

country_code必须对应真实使用地区;802.11d=true时关联AP的国家信息可以取代本地值,断开后恢复。本API文档明确国家配置会写Flash,不能因为连接配置用了WIFI_STORAGE_RAM就称所有配置都不会保存。记录AP日志中的实际country/policy/channel,遇到不能关联先检查AP地区/鉴权条件,不通过假国家码解锁信道。

代码 / 配置
idf.py -B build-2g -DSDKCONFIG="$PWD/build-2g/sdkconfig" -DW08_BAND=2 build
idf.py -B build-2g -DSDKCONFIG="$PWD/build-2g/sdkconfig" -DW08_BAND=2 -p YOUR_C5_UART_PORT flash monitor
# 另填5GHz SSID后:
idf.py -B build-5g -DSDKCONFIG="$PWD/build-5g/sdkconfig" -DW08_BAND=5 build
idf.py -B build-5g -DSDKCONFIG="$PWD/build-5g/sdkconfig" -DW08_BAND=5 -p YOUR_C5_UART_PORT flash monitor

参考:[3] IDF5.5.5 C5 Wi-Fi:功能/事件、IP后建socket、断线重建[4] 固定esp_wifi.h:set_band_mode启动前提、国家设置/802.11d

04 / 无线实验

完整主循环:IP、socket和ACK分别确认

下载包同时包含下面五个原文件、CMake/版本锁、完整服务/分析器和README。链路状态更新与取快照使用同一临界区;快照消费标记不会清掉一个并发更新。IP generation变化时重建socket,即使一次短暂断线和重连都发生在主循环两次观察之间,也不沿用旧socket。

得到IP之后才建UDP socket;断线/IP更新会关闭并在有效IP下重建。RSSI信息查询时连接可能再次变化,因此AP_RECORD_UNAVAILABLE只记日志,不把可恢复的瞬时状态变成abort。应用尝试15s窗口与最高16s退避都用64位单调时间;日志中的reason保留驱动原值。

DATA中64位boot与seq区分当前运行和请求,ACK还要来自指定电脑IP/端口并回显发送时间等字段。错误/旧包继续等到窗口结束;这不是安全认证,也不是单向PHY时延测量。500ms是应用检查窗口,系统调度不是硬实时上界。

代码 / 配置
// ---- main/config.h ----
// SPDX-License-Identifier: MIT
#pragma once
extern const char w08_ssid[],w08_password[],w08_country[],w08_echo_ip[];

// ---- main/config.c ----
// SPDX-License-Identifier: MIT
#include "config.h"
// Set locally. Do not commit credentials. Each band has its own AP SSID; use the same AP hardware/security/channel width.
const char w08_ssid[]="";
const char w08_password[]="";
// Your actual operating country ISO code, for example CN only when operating in China. Empty disables the experiment.
const char w08_country[]="";
// IPv4 of the wired computer running tools/echo_server.py on the same LAN; no DNS dependency.
const char w08_echo_ip[]="";

// ---- main/w08_link.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <stdbool.h>
#include <stdint.h>
typedef struct {bool online,disconnected,lost_ip;uint64_t generation;} W08Link;
static inline void w08_got_ip(W08Link *s){s->online=true;s->generation++;}
static inline void w08_disconnected(W08Link *s){s->online=false;s->disconnected=true;}
static inline void w08_lost_ip(W08Link *s){s->online=false;s->lost_ip=true;}
// Caller holds the same critical section used for updates; consuming flags cannot erase a concurrent update.
static inline W08Link w08_snapshot(W08Link *s){W08Link r=*s;s->disconnected=false;s->lost_ip=false;return r;}

// ---- main/w08_wire.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#define W08_SIZE 24
static inline void w08_data(uint8_t p[W08_SIZE],uint64_t boot,uint32_t seq,uint64_t sent_us){
    memcpy(p,"W08D",4);
    for(unsigned i=0;i<8;i++){p[4+i]=(uint8_t)(boot>>(56-i*8));p[16+i]=(uint8_t)(sent_us>>(56-i*8));}
    for(unsigned i=0;i<4;i++)p[12+i]=(uint8_t)(seq>>(24-i*8));
}
static inline int w08_ack(uint8_t *out,const uint8_t *in,size_t n){
    if(n!=W08_SIZE || memcmp(in,"W08D",4))return 0;
    memcpy(out,in,W08_SIZE);out[3]='A';return 1;
}
static inline int w08_match(const uint8_t *ack,size_t n,const uint8_t expected[W08_SIZE]){
    return n==W08_SIZE && !memcmp(ack,"W08A",4) && !memcmp(ack+4,expected+4,W08_SIZE-4);
}

// ---- main/main.c ----
// SPDX-License-Identifier: MIT
#include <errno.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/select.h>
#include "lwip/inet.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#include "esp_event.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_timer.h"
#include "esp_random.h"
#include "esp_wifi.h"
#include "nvs_flash.h"
#include "config.h"
#include "w08_wire.h"
#include "w08_link.h"
#ifndef W08_BAND
#define W08_BAND 2
#endif
static W08Link link_state;
static portMUX_TYPE link_mux=portMUX_INITIALIZER_UNLOCKED;
static const char *TAG="W08";
static uint64_t mono(void){return (uint64_t)esp_timer_get_time();}
static void event(void *arg,esp_event_base_t base,int32_t id,void *data){
    (void)arg;
    if(base==WIFI_EVENT && id==WIFI_EVENT_STA_DISCONNECTED){
        const wifi_event_sta_disconnected_t *d=data;
        portENTER_CRITICAL(&link_mux);w08_disconnected(&link_state);portEXIT_CRITICAL(&link_mux);
        ESP_LOGW(TAG,"DISCONNECT mono_us=%" PRIu64 " reason=%u",mono(),d->reason);
    }else if(base==IP_EVENT && id==IP_EVENT_STA_GOT_IP){
        portENTER_CRITICAL(&link_mux);w08_got_ip(&link_state);portEXIT_CRITICAL(&link_mux);
        ESP_LOGI(TAG,"GOT_IP mono_us=%" PRIu64,mono());
    }else if(base==IP_EVENT && id==IP_EVENT_STA_LOST_IP){
        portENTER_CRITICAL(&link_mux);w08_lost_ip(&link_state);portEXIT_CRITICAL(&link_mux);
    }
}
static void ap_record(void){
    wifi_ap_record_t a={0};wifi_country_t c={0};wifi_band_mode_t mode;
    const esp_err_t ret=esp_wifi_sta_get_ap_info(&a);
    if(ret!=ESP_OK){ESP_LOGW(TAG,"AP_RECORD_UNAVAILABLE ret=%s (connection may have changed)",esp_err_to_name(ret));return;}
    ESP_ERROR_CHECK(esp_wifi_get_country(&c));ESP_ERROR_CHECK(esp_wifi_get_band_mode(&mode));
    ESP_LOGI(TAG,"AP mono_us=%" PRIu64 " selected=%dG mode=%d channel=%u rssi_dbm=%d phy_11ax=%u country=%.3s policy=%d bssid=%02x:%02x:%02x:%02x:%02x:%02x",mono(),W08_BAND,mode,a.primary,a.rssi,a.phy_11ax,c.cc,c.policy,a.bssid[0],a.bssid[1],a.bssid[2],a.bssid[3],a.bssid[4],a.bssid[5]);
}
static void probe(int sock,const struct sockaddr_in *peer,uint64_t boot,uint32_t seq){
    uint8_t tx[W08_SIZE],rx[128];const uint64_t started=mono();
    w08_data(tx,boot,seq,started);
    const int sent=sendto(sock,tx,sizeof tx,0,(const struct sockaddr*)peer,sizeof *peer);
    bool ack=false;
    while(sent==(int)sizeof tx && mono()-started<500000){
        fd_set fds;FD_ZERO(&fds);FD_SET(sock,&fds);struct timeval tv={.tv_sec=0,.tv_usec=50000};
        if(select(sock+1,&fds,NULL,NULL,&tv)<=0)continue;
        struct sockaddr_in from={0};socklen_t len=sizeof from;
        const int n=recvfrom(sock,rx,sizeof rx,0,(struct sockaddr*)&from,&len);
        if(n<0)break;
        if(from.sin_addr.s_addr!=peer->sin_addr.s_addr || from.sin_port!=peer->sin_port)continue;
        if(w08_match(rx,(size_t)n,tx)){ack=true;break;}
    }
    ESP_LOGI(TAG,"PROBE mono_us=%" PRIu64 " boot=%016" PRIx64 " seq=%" PRIu32 " ack=%d elapsed_us=%" PRIu64 " send_errno=%d",mono(),boot,seq,ack,mono()-started,sent<0?errno:0);
}
void app_main(void){
    struct sockaddr_in peer={.sin_family=AF_INET,.sin_port=htons(45680)};
    if(!w08_ssid[0] || !w08_password[0] || strlen(w08_country)!=2 || inet_pton(AF_INET,w08_echo_ip,&peer.sin_addr)!=1){
        ESP_LOGW(TAG,"CONFIG_DISABLED: set actual SSID/password/country/echo IPv4 in main/config.c; no Wi-Fi initialized");return;
    }
    if(strlen(w08_ssid)>32 || strlen(w08_password)<8 || strlen(w08_password)>63){ESP_LOGE(TAG,"CONFIG_INVALID: WPA2/WPA3 personal passphrase, SSID <=32 bytes");return;}
    ESP_ERROR_CHECK(nvs_flash_init());ESP_ERROR_CHECK(esp_netif_init());ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();
    ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT,ESP_EVENT_ANY_ID,event,NULL));
    ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT,ESP_EVENT_ANY_ID,event,NULL));
    wifi_init_config_t init=WIFI_INIT_CONFIG_DEFAULT();ESP_ERROR_CHECK(esp_wifi_init(&init));
    ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_country_code(w08_country,true));
    wifi_config_t cfg={0};memcpy(cfg.sta.ssid,w08_ssid,strlen(w08_ssid));memcpy(cfg.sta.password,w08_password,strlen(w08_password));
    cfg.sta.threshold.authmode=WIFI_AUTH_WPA2_PSK;cfg.sta.pmf_cfg.capable=true;
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA,&cfg));ESP_ERROR_CHECK(esp_wifi_start());
    // set_band_mode requires started Wi-Fi. STA_START handler intentionally never calls connect.
    ESP_ERROR_CHECK(esp_wifi_set_band_mode(W08_BAND==5?WIFI_BAND_MODE_5G_ONLY:WIFI_BAND_MODE_2G_ONLY));
    ESP_ERROR_CHECK(esp_wifi_set_bandwidth(WIFI_IF_STA,WIFI_BW_HT20));ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
    const uint64_t boot=((uint64_t)esp_random()<<32)|esp_random();
    int sock=-1;uint64_t socket_generation=0;
    bool attempting=false,was_online=false;uint64_t deadline=0,next_try=0,next_probe=0;uint32_t backoff_ms=1000,seq=0;
    for(;;){
        portENTER_CRITICAL(&link_mux);const W08Link bits=w08_snapshot(&link_state);portEXIT_CRITICAL(&link_mux);
        if(bits.disconnected || bits.lost_ip){
            if(sock>=0){close(sock);sock=-1;}
            attempting=false;next_try=mono()+(uint64_t)backoff_ms*1000;
            // A later GOT_IP in this snapshot supersedes the earlier LOST_IP.
            if(bits.lost_ip && !bits.online)(void)esp_wifi_disconnect();
            if(backoff_ms<16000)backoff_ms*=2;
        }
        const uint64_t now=mono();const bool online=bits.online;
        if(online){
            attempting=false;backoff_ms=1000;
            if(!was_online || bits.generation!=socket_generation){
                if(sock>=0){close(sock);sock=-1;}
                socket_generation=bits.generation;ap_record();next_probe=now;
            }
            if(now>=next_probe){
                if(sock<0)sock=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
                if(sock>=0)probe(sock,&peer,boot,++seq);
                else ESP_LOGE(TAG,"SOCKET_FAILED errno=%d; resource failure is not an RF coverage result",errno);
                next_probe=mono()+2000000;
            }
        }else{
            if(was_online){attempting=false;next_try=now;backoff_ms=1000;}
            if(attempting && now>=deadline){attempting=false;(void)esp_wifi_disconnect();next_try=now+(uint64_t)backoff_ms*1000;ESP_LOGW(TAG,"CONNECT_TIMEOUT");}
            if(!attempting && now>=next_try){
                const esp_err_t ret=esp_wifi_connect();attempting=ret==ESP_OK;deadline=now+15000000;
                if(!attempting){next_try=now+(uint64_t)backoff_ms*1000;if(backoff_ms<16000)backoff_ms*=2;}
                ESP_LOGI(TAG,"CONNECT_TRY mono_us=%" PRIu64 " ret=%s",now,esp_err_to_name(ret));
            }
        }
        was_online=online;vTaskDelay(pdMS_TO_TICKS(20));
    }
}

参考:[3] IDF5.5.5 C5 Wi-Fi:功能/事件、IP后建socket、断线重建[4] 固定esp_wifi.h:set_band_mode启动前提、国家设置/802.11d

05 / 无线实验

先让近点两频段都跑通同一台电脑服务

在同LAN电脑启动下面的stdlib服务,绑定真实LAN IPv4。允许本地防火墙UDP45680,关闭该测试SSID客户端隔离;没有公网端口或云服务。服务LISTEN只证明电脑在监听,必须实际收到合法DATA才会写ACK行。

同一轮快照若已依次收到LOST_IP→GOT_IP,仍关闭旧socket,但不会因旧LOST_IP标志额外断开新关联;只有快照仍offline时才请求disconnect。处理以该次快照为界,不能排除之后的无线变化。

实板成功检查同时看板端GOT_IP、所选频段的AP记录、PROBE ack=1,以及电脑相同boot/seq的ACK。仅GOT_IP不能证明服务收到;仅服务进程启动也不能证明无线。本站实际跑过的是localhost回环服务,不是这两频段实板检查。

代码 / 配置
# ---- tools/echo_server.py ----
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""LAN UDP echo endpoint, not evidence that a board connected. Bind an explicit local IPv4."""
import argparse,socket,struct,time

def ack(data):
    return b"W08A"+data[4:] if len(data)==24 and data[:4]==b"W08D" else None

def main():
    p=argparse.ArgumentParser();p.add_argument("--bind",required=True);p.add_argument("--port",type=int,default=45680);a=p.parse_args()
    with socket.socket(socket.AF_INET,socket.SOCK_DGRAM) as s:
        s.bind((a.bind,a.port));print("LISTEN",s.getsockname(),flush=True)
        while True:
            data,peer=s.recvfrom(2048);reply=ack(data)
            if reply is None:print("REJECT",peer,len(data),flush=True);continue
            s.sendto(reply,peer)
            boot,seq,mono=struct.unpack("!QIQ",data[4:])
            print("ACK",time.monotonic_ns(),peer,f"{boot:016x}",seq,mono,flush=True)
if __name__=="__main__":main()
做到这里,你应该看到
近点两频段各自有真实双方对应记录后,才开始移动位置采集。

参考:[3] IDF5.5.5 C5 Wi-Fi:功能/事件、IP后建socket、断线重建

06 / 无线实验

固定点位、完整保留30条,再单独测断线恢复

固定AP/供电/方向、20MHz与鉴权条件,给每个位置编号并记录距离/隔墙。每到一个点或切换频段先RESET重新关联,取得此点的AP/RSSI和新boot;随后每频段连续记录30条PROBE,至少3次trial,轮换2G先/5G先的顺序。中途重启就开始新trial,不混同一组boot。

每个trial/点位/频段另从本次首次CONNECT_TRY的mono_us起固定观察120秒,在association.csv保留no_ip、ip_no_ack或ip_ack;仅真实发生的GOT_IP/首ACK才填时间;no_ip没有PROBE,boot也留空,以trial/point/频段及复位日志标识本次。没有IP也必须留下失败行,不能删去这些点后宣布覆盖更好。CONFIG_DISABLED/错误目标/socket资源失败先修环境,不算射频失败。

每一失败探测也占一个分母位置。ACK0的RTT留空,RSSI抄本次关联记录,不冒充每包即时RSSI。CSV写实际镜像SHA、boot、信道、AP实物id、距离/障碍;同AP的两个频段BSSID可不同,原始AP日志保留。

近点分别关闭AP无线10秒再开启,保留DISCONNECT→GOT_IP→新的ACK。本文重连时间从驱动报告断开算到重新获得IP,不包含驱动尚未发现的失联时间;操作者开关时刻单独注记。恢复IP后仍需新ACK,不能把连上路由器当作业务恢复。

做到这里,你应该看到
每个点两频段的成功和失败行都在,且记录的是同环境、同条数的独立trial。
07 / 无线实验

分析器先拒绝坏记录,不用空表推出性能结论

先运行tools/association.py分析固定120秒可用性表;它拒绝空表、非固定窗口、未成对/不同AP、非有限时间和ACK早于IP,no_ip不给虚构时间。两个频段不能都取得探测时,不计算RTT胜负。

运行python3 tools/compare.py records/coverage.csv。工具拒绝空表、缺一频段、不同环境、混固件/boot、重复或缺失序号、不同条数、NaN/Inf、失败行却填RTT等;只有输入一致才给已关联条件下的探测成功率、成功包RTT中位数、关联RSSI中位数和重连列表。全失败时RTT是null,不能拿零代替。

“5GHz是否值得”应写成你的点位/业务门槛判断:先规定最低成功率和可接受应用延迟,再看各位置是否达标;RSSI不是距离计,也不把一轮差异泛化到其他AP/住处。phy_11ax只是AP记录能力字段,不证明每个包都使用ax调制。分析器能拒坏格式,不能证明人工填入的数据来自实物。

代码 / 配置
# ---- tools/compare.py ----
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Analyze operator records. No generated observations; headers-only input is an error."""
import argparse,csv,json,math,re,statistics

def number(s,low,high):
    v=float(s)
    if not math.isfinite(v) or not low<=v<=high:raise ValueError("nonfinite/out-of-range number")
    return v

def analyze(path):
    groups={};seen=set()
    with open(path,newline="",encoding="utf-8") as f:
        for row in csv.DictReader(f):
            band=row['band']
            if band not in ('2','5'):raise ValueError('band must be 2 or 5')
            trial,point,ap=row['trial'],row['point'],row['ap_id']
            if not all((trial,point,ap,row['obstacle'])):raise ValueError('record environment identifiers')
            if not re.fullmatch('[0-9a-f]{64}',row['firmware_sha256']):raise ValueError('firmware SHA256')
            if not re.fullmatch('[0-9a-f]{16}',row['boot']):raise ValueError('boot 16 lowercase hex')
            if row['width_mhz']!='20':raise ValueError('this comparison fixes 20 MHz')
            ch=int(row['channel'])
            if not (1<=ch<=14 if band=='2' else 32<=ch<=177):raise ValueError('channel/band mismatch; this does not certify regional legality')
            distance=number(row['distance_m'],0,100000)
            seq=int(row['seq'])
            if not 1<=seq<=0xffffffff:raise ValueError('seq range')
            key=(trial,point,band,seq)
            if key in seen:raise ValueError('duplicate record')
            seen.add(key)
            if row['ack'] not in ('0','1'):raise ValueError('ack binary')
            ack=row['ack']=='1'
            rtt=number(row['rtt_ms'],0,float('inf')) if ack else None
            if not ack and row['rtt_ms']:raise ValueError('failed probe must not invent RTT')
            rssi=number(row['rssi_dbm'],-127,0)
            reconnect=number(row['reconnect_ms'],0,86400000) if row['reconnect_ms'] else None
            k=(trial,point,band)
            g=groups.setdefault(k,{'environment':(ap,distance,row['obstacle']),'firmware':row['firmware_sha256'],'boot':row['boot'],'rows':[]})
            if g['environment']!=(ap,distance,row['obstacle']) or g['firmware']!=row['firmware_sha256'] or g['boot']!=row['boot']:raise ValueError('mixed environment/firmware inside group')
            g['rows'].append((seq,ack,rtt,rssi,reconnect))
    if not groups:raise ValueError('no observations; blank template is not a result')
    out=[]
    for trial,point in sorted({(t,p) for t,p,b in groups}):
        a,b=groups.get((trial,point,'2')),groups.get((trial,point,'5'))
        if a is None or b is None:raise ValueError('pair both bands at every trial/point')
        if a['environment']!=b['environment'] or len(a['rows'])!=len(b['rows']):raise ValueError('unequal environment/probe count')
        for band,g in [('2',a),('5',b)]:
            seqs=sorted(x[0] for x in g['rows'])
            if seqs!=list(range(seqs[0],seqs[0]+len(seqs))):raise ValueError('missing probe rows')
            good=[x[2] for x in g['rows'] if x[1]]
            out.append({'trial':trial,'point':point,'band':band,'attempts':len(seqs),'acked':len(good),'success_fraction':len(good)/len(seqs),'median_rtt_ms':statistics.median(good) if good else None,'median_rssi_dbm':statistics.median(x[3] for x in g['rows']),'reconnect_ms':[x[4] for x in g['rows'] if x[4] is not None]})
    return out
if __name__=='__main__':
    p=argparse.ArgumentParser();p.add_argument('csv');a=p.parse_args()
    print(json.dumps(analyze(a.csv),ensure_ascii=False,indent=2,allow_nan=False))
08 / 无线实验

分清配置错误、LAN错误和真正的无线覆盖失败

CONFIG_DISABLED/INVALID先修本地配置;没有GOT_IP先读reason,再核频段SSID/国家/AP鉴权/DHCP,最后移动近点。已经GOT_IP但没有ACK,先检查电脑IPv4、服务、防火墙和客户端隔离。SOCKET_FAILED是资源故障,不计作射频覆盖失败;重连中的AP查询失败也不填假RSSI。

USB桥找不到先换数据线/驱动;按住BOOT28再按RESET进下载模式,重刷esp32c5目标。最后手段erase-flash会清NVS等数据,只有明确选择此板恢复时使用,之后重新填实际配置。

本轮两种最终镜像实际构建Exit0,同源帧/链路快照11组、探测表16组/关联表11组合成输入和真实localhost UDP5组通过;未执行C5关联、双频覆盖、断线恢复、手机或电流实验。先修复前置N01/L02的方法基础,再把本页自己的记录留存;若要转向无路由器通信,可回读已发布ESP-NOW教程。

BACK TO THE SOURCE

资料来源与版本说明

官方网页与具体固定源码逐项核对,详见docs/esp32-evidence/wireless-c5-c6.md及sources/builds/hosts JSON。硬件和无线链路没有本站观测。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. C5官方指南v1.2:N8R8/PW、GPIO15、供电与桥口
  2. C5电路V1.2(2025-02-11)p2
  3. IDF5.5.5 C5 Wi-Fi:功能/事件、IP后建socket、断线重建
  4. 固定esp_wifi.h:set_band_mode启动前提、国家设置/802.11d