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

ESP32-C6 做 Zigbee 传感器:加入自己的协调器

用两块同款C6分别做自有协调器与SHT40终端,完成标准温湿度端点、物理入网窗口、属性报告、离网与重新入网的完整工程及检查步骤。

读完能做什么
用两块同款C6分别做自有协调器与SHT40终端,完成标准温湿度端点、物理入网窗口、属性报告、离网与重新入网的完整工程及检查步骤。
开始之前
先完成所列前置;能够在精确IDF环境构建、识别UART桥口、保存原始串口日志并断电追线。
所需器材
两块官方ESP32-C6-DevKitC-1-N8/WROOM-1;指南v1.2、电路V1.4/PW≥2024-03-0595或0921、Adafruit PID4885 SHT40(已可靠焊接排针)、短I²C线;两套10k电阻+常开按钮、两条USB-UART数据线;不使用商业协调棒、HA或ZHA运行环境
版本组合
板卡:C6 DevKitC-1-N8;指南v1.2/电路V1.4;PW≥2024-03-0595或0921SDK:ESP-IDF5.5.5 / b774170ff46c393eeb5e495ea37936038d3f4f4fZigbee:esp-zigbee-lib2.0.5 / e2fdf4ad8558e5beb306b0d7040072d76244c413;上游推荐5.5.4,此组合实际5.5.5构建传感器/总线:PID4885 SHT40;SHT4x V6.4;GPIO6/7;0x44/100kHz网络:channel15;HA0x0104;0x0402+0x0405;本例无应用ACK
适用范围

仅C6-N8官方指南v1.2/电路V1.4;IDF5.5.5+原生Zigbee2.0.5,协调器EP1与SHT40 EP10。无本站射频/入网/HA或标准认证结果。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/wireless-c5-c6-sources.json;固定gh原文/已安装字节核对及板卡/SHT PDF实际阅读验证日期:
构建 / 配置已通过
docs/esp32-evidence/wireless-c5-c6-builds.json;end-device/coordinator最终范围检查镜像均实际Exit0;编译源与角色配置留SHA对象:ESP32-C6版本:ESP-IDF5.5.5验证日期:
仿真已通过
docs/esp32-evidence/wireless-c5-c6-hosts.json;生产CRC/单位转换与按键7组ASan/UBSan通过;不是射频/芯片仿真验证日期:
本站实板待验证
无本站实板、入网、覆盖/时延/功耗或长期运行结果;观察CSV仅表头

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

01 / 无线实验

先做自己的协调器,不从“兼容所有网关”开始

本任务需要一个真正的协调器,所以给两块同款C6分别烧入同工程的coordinator与end-device角色。协调器建网并注册EP1温湿度client,终端EP10用HA profile0x0104/Temperature Sensor0x0302加湿度server。HA在这里是Zigbee Home Automation,不是Home Assistant集成。

官网SDK有标准cluster并不等于本站完成认证、手机或商业网关互通。本文不需要HA、USB协调棒或Zigbee2MQTT;本站只实际构建了两个角色,所有入网/报告/离网检查仍是待实板执行的步骤。

参考:[3] Zigbee2.0.5固定提交/README版本映射[5] 2.0.5 ZCL报告请求/接收变量

02 / 无线实验

SHT40只接终端,GPIO10按钮两板各一套

两块固定C6-DevKitC-1-N8/WROOM-1 PCB天线,8MB Flash无PSRAM;官方指南标题v1.2,但后期PW-2024-03-0595或0921及以后对应V1.4电路图。不能把指南版本误当PCB电路版本。分别贴COORD和SHT标签,避免将错误角色刷入另一板。

PID4885必须已经可靠焊接排针,未焊请由有条件者处理,松插排针不能当电气连接。用3V3供VIN,保持SDA/SCL的上拉在3.3V域。每块GPIO10各接10k到本板3V3、常开按钮到本板GND;两板之间不拉I²C线。

各自只接USB-UART桥供电,保留J5,桥是UART16/17;原生USB12/13、BOOT9、RGB8不使用。USB类与5V/3V3排针供电互斥。必须断电接线;运行故障注入若需要开断SDA,先断电预装信号开关。

终端C6端点PID4885/按钮端点
J1-1 3V3 / J1-15 GNDVIN / GND
J1-5 GPIO6SDA;0x44/100kHz
J1-6 GPIO7SCL;PID4885板上上拉,内部上拉关闭
两板各自J1-10 GPIO10各自10k上拉到3V3,按钮到GND;不碰GPIO8/9启动配置脚

参考:[1] C6官方指南v1.2:N8、J1/J3、V1.4/PW修订和供电互斥[2] C6电路V1.4:p2 GPIO6/7/10、UART/USB、J5[6] Adafruit PID4885具体模块/连接说明[7] Adafruit模块pinouts:VIN随逻辑电压、10k上拉与0x44[8] SHT4x V6.4 p3/10–12:0xFD、CRC和公式

03 / 无线实验

明确2.0.5的EZB接口和两个角色配置

锁原生esp-zigbee-lib2.0.5/component_hash84562c41…,不是Arduino库,也不把旧ZBOSS1.x接口拼进去。其固定提交README推荐IDF5.5.4,组件manifest允许IDF>=5.0;本包复用5.5.5,两个角色真实编译通过,准确记录这个补丁版映射。

两角色使用不同SDKCONFIG目录:终端CONFIG_ZB_ZED,协调器CONFIG_ZB_ZCZR并在运行时选COORDINATOR/max_children4。自定义分区明确给nvs、phy、factory1.5MB与zb_fct1KB,其余8MB留空,不把未定义空间当可用文件系统。NVS错误不会自动erase,因为那会丢网络保存。

锁channel15、集中式安全、install_code_policy=false。这个可信台架的物理短窗口用来理解入网,不承诺生产认证、安装码策略或跨厂商兼容。组件包顶层Apache2.0/LICENSE、registry Custom标签及专有栈说明各自保留,本站MIT不替代依赖许可。

代码 / 配置
idf.py -B build-coord -DSDKCONFIG="$PWD/build-coord/sdkconfig" '-DSDKCONFIG_DEFAULTS=sdkconfig.defaults;sdkconfig.coordinator' -DW09_COORDINATOR=1 build
idf.py -B build-sensor -DSDKCONFIG="$PWD/build-sensor/sdkconfig" -DSDKCONFIG_DEFAULTS=sdkconfig.defaults -DW09_COORDINATOR=0 build
# 给对应命令加 -p 对应UART端口 flash monitor;两块板分别操作。

参考:[3] Zigbee2.0.5固定提交/README版本映射[4] 2.0.5 bdb.h:open/close/local reset明确边界

04 / 无线实验

完整应用:采样不能阻塞Zigbee主循环

下面main.c和w09_model.h与下载原文件逐字一致。Zigbee任务运行stack mainloop,普通应用循环采按键和I²C;在普通任务调用SDK前获取Zigbee锁,在SDK回调中不再次取锁。I²C等待发生在应用侧,不把6B读传感器塞进网络回调。

按键使用30ms稳定判定、3s单次长按、64位单调时间;上电按住必须先稳定释放才解锁,不会一上电就抹网络。报告接收回调里的value属于栈,只在声明的16位类型下复制后输出。配置/内存/注册等必要操作失败明确停止,不靠可编译成空的assert隐藏注册调用。

代码 / 配置
// ---- main/w09_model.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <stdbool.h>
#include <stdint.h>
// Microseconds from esp_timer_get_time(), not a wrapping millis() counter.
typedef struct { bool raw,stable,armed,down,long_sent; uint64_t edge,pressed; } W09Button;
enum { W09_NONE, W09_SHORT, W09_LONG };
static inline int w09_button(W09Button *b,bool low,uint64_t now) {
    if (low!=b->raw) { b->raw=low;b->edge=now; }
    if (now-b->edge<30000) return W09_NONE;
    if (low!=b->stable) {
        b->stable=low;
        if (!low) {
            const bool click=b->armed && b->down && !b->long_sent;
            b->armed=true;b->down=false;b->long_sent=false;
            return click?W09_SHORT:W09_NONE;
        }
        if (b->armed) { b->down=true;b->pressed=now;b->long_sent=false; }
    }
    // A stable released level arms input even if the initial raw/stable values were both false.
    if (!low) b->armed=true;
    if (b->down && !b->long_sent && now-b->pressed>=3000000) {
        b->long_sent=true;return W09_LONG;
    }
    return W09_NONE;
}
static inline uint8_t w09_crc(const uint8_t *p,unsigned n) {
    uint8_t c=0xff;
    while(n--) { c^=*p++;for(unsigned i=0;i<8;i++) c=(c&0x80)?(uint8_t)((c<<1)^0x31):(uint8_t)(c<<1); }
    return c;
}
static inline bool w09_sample(const uint8_t raw[6],int16_t *temp,uint16_t *rh) {
    *temp=(int16_t)0x8000;*rh=0xffff;
    if(w09_crc(raw,2)!=raw[2] || w09_crc(raw+3,2)!=raw[5]) return false;
    const uint32_t t=(uint32_t)raw[0]*256+raw[1],h=(uint32_t)raw[3]*256+raw[4];
    const int32_t tc=-4500+(int32_t)((17500*t+32767)/65535);
    // Data-sheet operating range, not the wider raw formula endpoints.
    if(tc < -4000 || tc > 12500)return false;
    *temp=(int16_t)tc;
    int32_t v=-600+(int32_t)((12500*h+32767)/65535);
    if(v<0)v=0;
    if(v>10000)v=10000;
    *rh=(uint16_t)v;return true;
}

// ---- main/main.c ----
// SPDX-License-Identifier: MIT
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "driver/gpio.h"
#include "driver/i2c_master.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "nvs_flash.h"
#include "esp_zigbee.h"
#include "ezbee/zha.h"
#include "ezbee/zcl/cluster/rel_humidity_measurement_desc.h"
#include "w09_model.h"
#ifndef W09_COORDINATOR
#define W09_COORDINATOR 0
#endif
#define REQUIRE(x) do { if(!(x)) { ESP_LOGE("W09", "required operation failed: %s", #x); abort(); } } while(0)
#define SENSOR_EP 10
#define RECEIVER_EP 1
#define NETWORK_READY BIT0
#define STARTUP_READY BIT1
static const char *TAG="W09";
static EventGroupHandle_t state;
static uint64_t us(void){return (uint64_t)esp_timer_get_time();}
static bool zb_ok(ezb_err_t ret,const char *what){
    if(ret!=EZB_ERR_NONE) ESP_LOGE(TAG,"%s failed EZB=%d",what,(int)ret);
    return ret==EZB_ERR_NONE;
}
static void network_info(void){
    ESP_LOGI(TAG,"NETWORK local=0x%04x pan=0x%04x channel=%u role=%s",ezb_nwk_get_short_address(),ezb_nwk_get_panid(),ezb_nwk_get_current_channel(),W09_COORDINATOR?"coordinator":"end-device");
}
static bool signal_handler(const ezb_app_signal_t *s){
    const ezb_app_signal_type_t type=ezb_app_signal_get_type(s);
    const void *p=ezb_app_signal_get_params(s);
    ESP_LOGI(TAG,"signal=%s",ezb_app_signal_to_string(type));
    if(type==EZB_ZDO_SIGNAL_SKIP_STARTUP){
        zb_ok(ezb_bdb_start_top_level_commissioning(EZB_BDB_MODE_INITIALIZATION),"initialization");
    }else if(type==EZB_BDB_SIGNAL_DEVICE_FIRST_START || type==EZB_BDB_SIGNAL_DEVICE_REBOOT){
        const ezb_bdb_comm_status_t status=p?*(const ezb_bdb_comm_status_t*)p:EZB_BDB_STATUS_FORMATION_FAILURE;
        ESP_LOGI(TAG,"startup status=%u factory_new=%d",status,ezb_bdb_is_factory_new());
        if(status==EZB_BDB_STATUS_SUCCESS){
            xEventGroupSetBits(state,STARTUP_READY);
            if(ezb_bdb_is_factory_new()){
                if(W09_COORDINATOR) zb_ok(ezb_bdb_start_top_level_commissioning(EZB_BDB_MODE_NETWORK_FORMATION),"formation");
                else ESP_LOGI(TAG,"WAIT_JOIN: short press GPIO10 after coordinator opens 60 s window");
            }else{
                if(W09_COORDINATOR) zb_ok(ezb_bdb_close_network(),"close after reboot");
                xEventGroupSetBits(state,NETWORK_READY);network_info();
            }
        }else ESP_LOGE(TAG,"STARTUP_FAILED: inspect log; reset to retry, no automatic erase");
    }else if(type==EZB_BDB_SIGNAL_FORMATION || type==EZB_BDB_SIGNAL_STEERING){
        const ezb_bdb_comm_status_t status=p?*(const ezb_bdb_comm_status_t*)p:EZB_BDB_STATUS_FORMATION_FAILURE;
        ESP_LOGI(TAG,"commission status=%u",status);
        if(status==EZB_BDB_STATUS_SUCCESS){
            if(W09_COORDINATOR) zb_ok(ezb_bdb_close_network(),"closed by default");
            xEventGroupSetBits(state,NETWORK_READY);network_info();
        }else xEventGroupClearBits(state,NETWORK_READY);
    }else if(type==EZB_ZDO_SIGNAL_LEAVE || type==EZB_ZDO_SIGNAL_PARENT_LOST){
        xEventGroupClearBits(state,NETWORK_READY);
        ESP_LOGW(TAG,"NETWORK_UNAVAILABLE: no application reports until network resumes; short press to retry initialization/join");
    }else if(type==EZB_ZDO_SIGNAL_DEVICE_ANNCE && p){
        const ezb_zdo_signal_device_annce_params_t *a=p;
        ESP_LOGI(TAG,"ANNOUNCE short=0x%04x (not a sensor report)",a->short_addr);
    }else if(type==EZB_NWK_SIGNAL_PERMIT_JOIN_STATUS && p){
        ESP_LOGI(TAG,"permit_join_remaining_s=%u",*(const uint8_t*)p);
    }
    return false;
}
static void action(ezb_zcl_core_action_callback_id_t id,void *message){
    if(id!=EZB_ZCL_CORE_REPORT_ATTR_CB_ID || !W09_COORDINATOR || !message)return;
    ezb_zcl_cmd_report_attr_message_t *m=message;
    m->out.result=EZB_ZCL_STATUS_SUCCESS;
    if(m->info.status!=EZB_ZCL_STATUS_SUCCESS || m->info.dst_ep!=RECEIVER_EP || !m->in.header)return;
    if(m->in.header->src_addr.addr_mode!=EZB_ADDR_MODE_SHORT){ESP_LOGW(TAG,"REPORT_IGNORED: source address is not SHORT");return;}
    for(ezb_zcl_report_attr_variable_t *v=m->in.variables;v;v=v->next){
        if(v->attr_id!=0 || !v->attr_value)continue;
        // Callback storage belongs to stack; copy exactly the specified 16-bit value now.
        uint16_t raw;
        if(v->attr_type!=EZB_ZCL_ATTR_TYPE_INT16 && v->attr_type!=EZB_ZCL_ATTR_TYPE_UINT16)continue;
        memcpy(&raw,v->attr_value,sizeof raw);
        if(m->info.cluster_id==EZB_ZCL_CLUSTER_ID_TEMPERATURE_MEASUREMENT && v->attr_type==EZB_ZCL_ATTR_TYPE_INT16){
            ESP_LOGI(TAG,"REPORT mono_us=%" PRIu64 " source=0x%04x ep=%u temp_centi=%d valid=%d",us(),m->in.header->src_addr.u.short_addr,m->in.header->src_ep,(int16_t)raw,raw!=0x8000);
        }else if(m->info.cluster_id==EZB_ZCL_CLUSTER_ID_REL_HUMIDITY_MEASUREMENT && v->attr_type==EZB_ZCL_ATTR_TYPE_UINT16){
            ESP_LOGI(TAG,"REPORT mono_us=%" PRIu64 " source=0x%04x ep=%u rh_centi=%u valid=%d",us(),m->in.header->src_addr.u.short_addr,m->in.header->src_ep,raw,raw<=10000);
        }
    }
}
static void endpoints(void){
    ezb_af_device_desc_t dev=ezb_af_create_device_desc();
    REQUIRE(dev);
    ezb_af_ep_desc_t ep;
    if(W09_COORDINATOR){
        ezb_af_ep_config_t cfg={.ep_id=RECEIVER_EP,.app_profile_id=EZB_AF_HA_PROFILE_ID,.app_device_id=EZB_ZHA_CONFIGURATION_TOOL_DEVICE_ID,.app_device_version=1};
        ep=ezb_af_create_endpoint_desc(&cfg);REQUIRE(ep);
        REQUIRE(zb_ok(ezb_af_endpoint_add_cluster_desc(ep,ezb_zcl_temperature_measurement_create_cluster_desc(NULL,EZB_ZCL_CLUSTER_CLIENT)),"temp client"));
        REQUIRE(zb_ok(ezb_af_endpoint_add_cluster_desc(ep,ezb_zcl_rel_humidity_measurement_create_cluster_desc(NULL,EZB_ZCL_CLUSTER_CLIENT)),"humidity client"));
    }else{
        ezb_zha_temperature_sensor_config_t cfg=EZB_ZHA_TEMPERATURE_SENSOR_CONFIG();
        cfg.temp_meas_cfg.measured_value=(int16_t)0x8000;
        cfg.temp_meas_cfg.min_measured_value=-4000;cfg.temp_meas_cfg.max_measured_value=12500;
        ep=ezb_zha_create_temperature_sensor(SENSOR_EP,&cfg);REQUIRE(ep);
        ezb_zcl_rel_humidity_measurement_cluster_config_t rh={.measured_value=0xffff,.min_measured_value=0,.max_measured_value=10000};
        REQUIRE(zb_ok(ezb_af_endpoint_add_cluster_desc(ep,ezb_zcl_rel_humidity_measurement_create_cluster_desc(&rh,EZB_ZCL_CLUSTER_SERVER)),"humidity server"));
        ezb_zcl_cluster_desc_t basic=ezb_af_endpoint_get_cluster_desc(ep,EZB_ZCL_CLUSTER_ID_BASIC,EZB_ZCL_CLUSTER_SERVER);
        static const char manufacturer[]="\x07""HIXINWU",model[]="\x0c""C6-SHT40-W09";
        REQUIRE(zb_ok(ezb_zcl_basic_cluster_desc_add_attr(basic,EZB_ZCL_ATTR_BASIC_MANUFACTURER_NAME_ID,manufacturer),"manufacturer"));
        REQUIRE(zb_ok(ezb_zcl_basic_cluster_desc_add_attr(basic,EZB_ZCL_ATTR_BASIC_MODEL_IDENTIFIER_ID,model),"model"));
    }
    REQUIRE(zb_ok(ezb_af_device_add_endpoint_desc(dev,ep),"add endpoint"));
    REQUIRE(zb_ok(ezb_af_device_desc_register(dev),"register device"));
    ezb_zcl_core_action_handler_register(action);
}
static void zigbee_task(void *arg){
    (void)arg;
    esp_zigbee_config_t cfg={
        .device_config={.device_type=W09_COORDINATOR?EZB_NWK_DEVICE_TYPE_COORDINATOR:EZB_NWK_DEVICE_TYPE_END_DEVICE,.install_code_policy=false},
        .platform_config={.storage_partition_name="nvs",.radio_config={.radio_mode=ESP_ZIGBEE_RADIO_MODE_NATIVE}}
    };
    if(W09_COORDINATOR)cfg.device_config.zczr_config.max_children=4;
    else {cfg.device_config.zed_config.ed_timeout=EZB_NWK_ED_TIMEOUT_64MIN;cfg.device_config.zed_config.keep_alive=4000;}
    ESP_ERROR_CHECK(esp_zigbee_init(&cfg));
    REQUIRE(ezb_app_signal_add_handler(signal_handler));
    REQUIRE(zb_ok(ezb_bdb_set_primary_channel_set(1U<<15),"primary channel"));
    REQUIRE(zb_ok(ezb_bdb_set_secondary_channel_set(0),"secondary channels"));
    ezb_aps_secur_enable_distributed_security(false);
    endpoints();
    ESP_ERROR_CHECK(esp_zigbee_start(false));
    ESP_ERROR_CHECK(esp_zigbee_launch_mainloop());
    vTaskDelete(NULL);
}
static void report_value(uint16_t cluster,void *value){
    const ezb_zcl_status_t status=ezb_zcl_set_attr_value(SENSOR_EP,cluster,EZB_ZCL_CLUSTER_SERVER,0,EZB_ZCL_STD_MANUF_CODE,value,false);
    if(status!=EZB_ZCL_STATUS_SUCCESS){ESP_LOGE(TAG,"set attr cluster=%04x status=%u",cluster,status);return;}
    if(!(xEventGroupGetBits(state)&NETWORK_READY))return;
    ezb_zcl_report_attr_cmd_t cmd={.cmd_ctrl={.dst_addr={.addr_mode=EZB_ADDR_MODE_SHORT,.u.short_addr=0x0000},.dst_ep=RECEIVER_EP,.src_ep=SENSOR_EP,.cluster_id=cluster,.manuf_code=EZB_ZCL_STD_MANUF_CODE,.fc.direction=EZB_ZCL_CMD_DIRECTION_TO_CLI},.payload.attr_id=0};
    const ezb_err_t ret=ezb_zcl_report_attr_cmd_req(&cmd);
    ESP_LOGI(TAG,"REPORT_REQUEST cluster=%04x ret=%d (request acceptance is not receiver observation)",cluster,(int)ret);
}
void app_main(void){
    ESP_ERROR_CHECK(nvs_flash_init()); // Never silently erase saved Zigbee keys on an NVS error.
    state=xEventGroupCreate();REQUIRE(state);
    gpio_config_t pin={.pin_bit_mask=1ULL<<GPIO_NUM_10,.mode=GPIO_MODE_INPUT,.pull_up_en=GPIO_PULLUP_DISABLE,.pull_down_en=GPIO_PULLDOWN_DISABLE,.intr_type=GPIO_INTR_DISABLE};
    ESP_ERROR_CHECK(gpio_config(&pin));
    i2c_master_dev_handle_t sht=NULL;
    if(!W09_COORDINATOR){
        i2c_master_bus_config_t buscfg={.i2c_port=I2C_NUM_0,.sda_io_num=GPIO_NUM_6,.scl_io_num=GPIO_NUM_7,.clk_source=I2C_CLK_SRC_DEFAULT,.glitch_ignore_cnt=7,.flags.enable_internal_pullup=false};
        i2c_master_bus_handle_t bus;ESP_ERROR_CHECK(i2c_new_master_bus(&buscfg,&bus));
        i2c_device_config_t d={.dev_addr_length=I2C_ADDR_BIT_LEN_7,.device_address=0x44,.scl_speed_hz=100000};
        ESP_ERROR_CHECK(i2c_master_bus_add_device(bus,&d,&sht));
    }
    REQUIRE(xTaskCreate(zigbee_task,"zigbee",8192,NULL,5,NULL)==pdPASS);
    W09Button button={0};uint64_t next_sample=0;
    for(;;){
        const uint64_t now=us();
        const int key=w09_button(&button,gpio_get_level(GPIO_NUM_10)==0,now);
        if(key!=W09_NONE && (xEventGroupGetBits(state)&STARTUP_READY) && esp_zigbee_lock_acquire(pdMS_TO_TICKS(200))){
            if(W09_COORDINATOR){
                if(key==W09_SHORT){
                    if(xEventGroupGetBits(state)&NETWORK_READY)zb_ok(ezb_bdb_open_network(60),"permit join 60 s");
                    else zb_ok(ezb_bdb_start_top_level_commissioning(EZB_BDB_MODE_NETWORK_FORMATION),"retry formation");
                }else zb_ok(ezb_bdb_close_network(),"manual close");
            }else if(key==W09_LONG){
                if(xEventGroupGetBits(state)&NETWORK_READY){xEventGroupClearBits(state,NETWORK_READY);ezb_bdb_reset_via_local_action();ESP_LOGW(TAG,"LEAVE_REQUEST (wait for LEAVE signal; no automatic rejoin)");}
                else ESP_LOGW(TAG,"LEAVE_IGNORED: not on network; use documented erase only for recovery");
            }else{
                if(ezb_bdb_is_factory_new())zb_ok(ezb_bdb_start_top_level_commissioning(EZB_BDB_MODE_NETWORK_STEERING),"join");
                else zb_ok(ezb_bdb_start_top_level_commissioning(EZB_BDB_MODE_INITIALIZATION),"retry saved network");
            }
            esp_zigbee_lock_release();
        }
        if(!W09_COORDINATOR && now>=next_sample && (xEventGroupGetBits(state)&STARTUP_READY)){
            next_sample=now+10000000;
            uint8_t raw[6]={0};const uint8_t cmd=0xfd;
            esp_err_t err=i2c_master_transmit(sht,&cmd,1,100);
            if(err==ESP_OK){vTaskDelay(pdMS_TO_TICKS(20));err=i2c_master_receive(sht,raw,sizeof raw,100);}
            int16_t t=(int16_t)0x8000;uint16_t h=0xffff;
            const bool valid=err==ESP_OK && w09_sample(raw,&t,&h);
            ESP_LOGI(TAG,"SAMPLE mono_us=%" PRIu64 " valid=%d i2c=%s temp_centi=%d rh_centi=%u",us(),valid,esp_err_to_name(err),t,h);
            if(esp_zigbee_lock_acquire(pdMS_TO_TICKS(200))){
                report_value(EZB_ZCL_CLUSTER_ID_TEMPERATURE_MEASUREMENT,&t);
                report_value(EZB_ZCL_CLUSTER_ID_REL_HUMIDITY_MEASUREMENT,&h);
                esp_zigbee_lock_release();
            }else ESP_LOGW(TAG,"SAMPLE_NOT_PUBLISHED: Zigbee lock timeout");
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

参考:[4] 2.0.5 bdb.h:open/close/local reset明确边界[5] 2.0.5 ZCL报告请求/接收变量

05 / 无线实验

首次入网:先有网络,再开60秒窗口

先开COORD桥口。首次BDB初始化后自动formation,成功后显式close_network;保存过网络的重启也显式关闭加入。核日志中的PAN、channel15、local0x0000。没有组网时短按重试formation,已组网时短按才open_network(60),长按关闭。

SHT初次factory-new只显示WAIT_JOIN,不自行反复steering。两按钮先释放稳定30ms;开COORD窗口后短按SHT。成功检查依次是SHT的commission status/NETWORK、COORD的ANNOUNCE,然后下一次真正REPORT;仅ANNOUNCE不能记温湿度业务通过。

关闭permit-join后,已入网设备仍可报告;这个动作禁止新的加入,不是删除终端。可以先关窗口再让新出厂终端尝试,保留真实失败记录,随后重新开窗重试,而非把所有失败归为信号弱。

做到这里,你应该看到
同一个trial中,PAN/channel对应、入网信号和接收端真正的属性报告三类记录都保留。

参考:[4] 2.0.5 bdb.h:open/close/local reset明确边界

06 / 无线实验

标准属性的单位、未知值和“请求已接受”

SHT每约10s发0xFD,等待20ms,读6B并分别检查两个CRC(初值0xff、多项式0x31)。t=−45+175*raw/65535、RH=−6+125*raw/65535后换成百分之一,湿度裁到0..10000,ZCL温度Min/Max为−4000/12500;原始温度公式端点−45..130°C不是器件保证工作范围−40..125°C的扩张。

正常温度用signed16,湿度unsigned16;任一CRC或I²C失败,或换算温度超出器件−40..125°C范围,本轮温度写0x8000、湿度0xffff再报告未知,不继续重发上轮正常值。终端对0x0402/0x0405分别单播到0x0000/EP1,不要求读者另外建bind。

REPORT_REQUEST ret0只代表SDK接受请求,必须在COORD真实回调找相同属性。温度和湿度是两个独立ZCL报告,没有原子样本帧、业务应用ACK或丢包重放保证。短地址会在重新入网改变,读者用实物标签/设备IEEE信息记录本次映射,不能用short当永久身份。

COORD只逐条串口输出,不提供自动延用旧值的看板/HA实体。若一段时间没有新REPORT,外部记录应按最后实际接收时刻标为无更新,不能把上一行正常值当仍在线。

参考:[5] 2.0.5 ZCL报告请求/接收变量[6] Adafruit PID4885具体模块/连接说明[7] Adafruit模块pinouts:VIN随逻辑电压、10k上拉与0x44[8] SHT4x V6.4 p3/10–12:0xFD、CRC和公式

07 / 无线实验

离网、重新加入和保留网络的重启各测一次

已入网SHT长按3s立即撤销应用报告资格,调用local reset。等实际LEAVE信号,API会清Zigbee持久网络数据但保留outgoing NWK frame counter;它不会在本例离网后自动steering。重新开COORD窗口、SHT短按,再记录新short与真正REPORT。未入网时长按只警告,不擅自erase。

另做普通断电重上:不擦Flash,核SHT从保存网络恢复的BDB REBOOT/NETWORK和COORD的新报告。首次formation与旧网络恢复是两个用例,不能拿前者替后者。父节点丢失时本例停止应用报告;短按请求保存网络初始化/重试,先检查COORD供电和日志。

最后手段erase-flash只对明确选定板执行,会清所有数据再刷同角色;擦COORD会建新网络,旧终端可能还保存旧网,必须按local reset或明确清除后再入网。NVS初始化失败不会自动丢钥匙“修好”,先保存错误日志。

  1. 填写空记录表中的首入/关窗/报告/故障未知/离网/重入/断电恢复行。
  2. 同一用例保存两边原始日志、固件SHA、板卡标签和真实时间。
  3. 在各步骤真正观察到相应状态以后再填写通过;未做保持pending。

参考:[4] 2.0.5 bdb.h:open/close/local reset明确边界

08 / 无线实验

从角色与端点往下查,最后才动清除按钮

无网络先查COORD镜像/供电、channel15和formation;无法加入再查窗口和SHT是否保存旧网络。已入但无值,核EP10→EP1、HA profile、0x0402/0x0405及接收回调,再查I²C/CRC。若要观察断线未知,断电预装SDA开关后运行时打开,闭合后必要RESET;不要带电拔VIN造反供电风险。

桥口找不到先换数据线,BOOT9+RESET恢复下载。错误Zigbee/Thread/其他C6镜像不能互当协调器。本文不宣称已经连接Home Assistant、兼容任意协调棒或通过Zigbee认证。

本轮终端和协调器最终范围检查镜像实际构建Exit0,CRC/换算/按键7组ASan/UBSan通过,仍无SHT实测或无线结果。先回读S01完成有线采样、E07理解锁,再用本包独立完成两板记录。

BACK TO THE SOURCE

资料来源与版本说明

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

  1. C6官方指南v1.2:N8、J1/J3、V1.4/PW修订和供电互斥
  2. C6电路V1.4:p2 GPIO6/7/10、UART/USB、J5
  3. Zigbee2.0.5固定提交/README版本映射
  4. 2.0.5 bdb.h:open/close/local reset明确边界
  5. 2.0.5 ZCL报告请求/接收变量
  6. Adafruit PID4885具体模块/连接说明
  7. Adafruit模块pinouts:VIN随逻辑电压、10k上拉与0x44
  8. SHT4x V6.4 p3/10–12:0xFD、CRC和公式