故障定位与升级 / Matter完整开发实验发布 内容更新

ESP32-C6 Matter over Thread 按钮:补齐 Border Router 和控制器链路

独立C6 Generic Switch工程,明确Thread终端、Border Router、手机与控制器职责;四边沿缓存、位置重同步和配对步骤均给出范围。

读完能做什么
构建Momentary+Release按钮,按BLE、Dataset、Thread/IPv6、fabric和实际event实体逐层验收。
开始之前
先完成所列前置;能激活固定IDF环境、保存同次源码/配置/ELF/log。下载README含完整器材、命令、失败与恢复步骤。
所需器材
ESP32-C6-DevKitC-1-N8 / WROOM-1 PCB天线,按v1.4范围核板、SPST常开按钮与10kΩ电阻各2,面包板短线,单根USB-to-UART数据线、64位HA OS+Matter Server、完整Android Companion、专用Thread BR(如独立ZBT-2/OTBR)
版本组合
框架:ESP-IDF5.5.5 / b774170ff46c393eeb5e495ea37936038d3f4f4fMatter固定源:ESP-Matter aeefd294b55ad9209da9bcc1757be73c8bad3cf3 / CHIP 9c2a63634e880b92e29b25a49543b70a4baf1572
适用范围

ESP32-C6-DevKitC-1-N8/WROOM-1,v1.4电路范围,GPIO10事件/11恢复;8MiB DIO80无PSRAM,FTD receiver-on。HA OS+专用Thread BR参考路线,物理全pending。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/maintenance-frontier-sources.json;固定官方源码鲜取/安装cmp及原厂板图,包含一条404与正确v1.1路径重试。验证日期:
构建 / 配置已通过
docs/esp32-evidence/maintenance-frontier-builds.json;实际最终构建Exit0,输入快照与产物/原日志SHA单列。对象:C6N8版本:ESP-IDF5.5.5验证日期:
仿真不适用
只运行固件共享输入策略的主机检查;未仿真CHIP/BLE/Wi-Fi/Thread/控制器。
本站实板待验证
没有本例实体烧录、安全配置、电气、无线配对、控制器事件或重启恢复记录;下载observations CSV仅表头。

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

01 / Matter完整开发实验

先选控制器支持的设备类型

本例Generic Switch设备类型0x000F,只启用Momentary+MomentaryRelease,当前位置0/1,提交InitialPress/ShortRelease。不提供长按/多击业务,也不做OnOff Light Switch Binding控制另一灯。

固定检查HA Core2026.10.2的event.py支持对应FeatureMap/CurrentPosition组合,映射为event实体的initial_press/short_release。它不是button.press服务或light实体;先确认实际控制器范围,再谈按钮自动化。没有运行HA或观察实体/事件。

参考:[1] 固定ESP-Matter Generic Switch端点[2] 固定Momentary/Release feature与事件wrapper[8] HA Core2026.10.2 Matter事件映射

02 / Matter完整开发实验

把网络角色和供电接线补齐

使用C6-DevKitC-1-N8/WROOM-1 PCB天线,按官方v1.4(PW-2024-03-0595/0921及后续)电路范围核到手板。GPIO10/11各独立10kΩ上拉3V3,各SPST到GND;事件与恢复分开,不用BOOT9/RGB8或USB12/13/UART16/17。

仅USB-to-UART单线供电/日志。C6是原生FTD receiver-on终端,可能参与路由,但不是Border Router、不是电池省电例程。HA OS控制器加专用Thread BR;独立ZBT-2配置Thread/OTBR,不能同一棒同时做Zigbee/Thread,也不重刷家庭现有协调器。

参考:[5] 固定C6 OpenThread平台入口[6] 固定C6官方指南/修订范围[7] C6 v1.4原厂原理图第2页[9] HA Thread:首选网络与手机凭据[10] HA ZBT-2:专用Thread/OTBR角色

03 / Matter完整开发实验

复用固定依赖,不下载第二份SDK

IDF5.5.5/ESP-Matter aeefd294…/CHIP9c2a6363…与R09相同。已有最小安装用tools/prepare_dependencies.py --verify;固定GN2255、8个Python隔离包、9公共/ESP32子模块保留许可。未执行全平台bootstrap/hostchip-tool/其他MCUSDK。

本机只审macOS arm64/Python3.12.13。下载README有完整exports;其他系统按固定官方路径另验。私有生成的空pigweed_environment.gni对应固定scripts/configure分支,源码库tracked文件未改。

代码 / 配置
export LAB_DEPS="/absolute/path/matter-1.5-tools"
python tools/prepare_dependencies.py --prefix "$LAB_DEPS" --verify
export ESP_MATTER_PATH="$LAB_DEPS/esp-matter"
export MATTER_SDK_PATH="$ESP_MATTER_PATH/connectedhomeip/connectedhomeip"
export PATH="$LAB_DEPS/tools/gn:$PATH"
export PYTHONPATH="$LAB_DEPS/python-overlay:$MATTER_SDK_PATH/scripts/py_matter_idl:${PYTHONPATH:-}"

参考:[1] 固定ESP-Matter Generic Switch端点[4] 固定Generic Switch应用参考[5] 固定C6 OpenThread平台入口

04 / Matter完整开发实验

独立C6配置,默认不广播

默认LAB_ENABLE_MATTER=n立即返回,不初始化NVS/BLE/Thread/Matter;另一个新build明确加入sdkconfig.lab才启用。C6原生无线电、FTD/RX-on、Wi-Fi STA/SoftAP关闭、BR/CLI/PM关闭,不自动生成Dataset;网络由Matter commissioning传入。

8MiB DIO80、无PSRAM,factory0x20000/0x600000、无OTA槽。按E09备份原Flash后再考虑实体初刷;不能复用S3 sdkconfig或把此app当旧OTA包。作者明确启用且修正初始化门禁的最终新目录构建实际Exit0,app1566096B;旧SDK成功镜像被独立故障反例判定存在初始化事件顺序缺陷,已保留并替代。首次PATH失败与第二次示例私有宏误用失败分开保留,最终使用公开结构字段。记录以最终build JSON与同次源/锁/ELF/map/原log为准,无线与恢复还未执行。

代码 / 配置
export LAB_PROJECT="/absolute/path/esp32-matter-thread-button"
export LAB_BUILD="/absolute/path/r10-enabled"
idf.py -C "$LAB_PROJECT" -B "$LAB_BUILD" -D IDF_TARGET=esp32c6 \
  -D SDKCONFIG="$LAB_BUILD/sdkconfig" \
  -D "SDKCONFIG_DEFAULTS=$LAB_PROJECT/sdkconfig.defaults;$LAB_PROJECT/sdkconfig.lab" build

参考:[4] 固定Generic Switch应用参考[5] 固定C6 OpenThread平台入口[6] 固定C6官方指南/修订范围

05 / Matter完整开发实验

完整实现:先位置,再事件

input_task是两个去抖器、四边沿队列和事件配对的唯一owner;每10ms读取64位单调时刻。TryLock忙继续采样;获得CHIP锁后,初始化位置0若失败就解锁/延迟并继续采样,保留四边沿队列但不消费;初始化成功后再更新CurrentPosition,成功才提交对应事件,不从ISR调用SDK。

固定SDK的事件wrapper返回ESP_OK但不是控制器送达ACK;应用不提供掉线历史按键重放。NVS初始化失败停止不自动擦除,任务创建失败可能留下无线栈活跃而无按键采样,均按原log排查。

代码 / 配置
// ---- main/main.cpp ----
// SPDX-License-Identifier: MIT
#include <inttypes.h>
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_matter.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nvs_flash.h"
#include <platform/PlatformManager.h>
#include <setup_payload/OnboardingCodesUtil.h>
#include <platform/ESP32/OpenthreadLauncher.h>
#include "input_policy.h"

extern "C" bool lab_enabled(void);
using namespace esp_matter;
using namespace chip::app::Clusters;
static constexpr gpio_num_t EVENT_BUTTON = GPIO_NUM_10;
static constexpr gpio_num_t RESET_BUTTON = GPIO_NUM_11;
static const char *TAG = "r10";
static uint16_t switch_id;

static esp_err_t update_cb(attribute::callback_type_t, uint16_t, uint32_t, uint32_t,
                           esp_matter_attr_val_t *, void *) { return ESP_OK; }
static esp_err_t identify_cb(identification::callback_type_t, uint16_t, uint8_t, uint8_t, void *)
{
    ESP_LOGI(TAG, "Identify requested (no output indicator fitted)");
    return ESP_OK;
}
static void event_cb(const chip::DeviceLayer::ChipDeviceEvent *event, intptr_t)
{
    ESP_LOGI(TAG, "Matter event=%u", static_cast<unsigned>(event->Type));
}

static esp_err_t set_position(bool down)
{
    esp_matter_attr_val_t value = esp_matter_uint8(down ? 1 : 0);
    return attribute::update(switch_id, Switch::Id, Switch::Attributes::CurrentPosition::Id, &value);
}

static void input_task(void *)
{
    lab::Button event_button, reset_button;
    lab::Edges edges;
    lab::Dispatch dispatch;
    bool initialized_position = false, reset_pending = false;
    unsigned observed_overflows = 0;
    bool codes_printed = false;
    for (;;) {
        const uint64_t now = static_cast<uint64_t>(esp_timer_get_time() / 1000);
        const auto event_edge = event_button.sample(gpio_get_level(EVENT_BUTTON) == 0, now);
        if (event_edge != lab::Edge::None) edges.push(event_edge == lab::Edge::Press);
        if (reset_button.sample(gpio_get_level(RESET_BUTTON) == 0, now) == lab::Edge::Release &&
            reset_button.last_hold_ms >= 8000) reset_pending = true;
        if (edges.overflows != observed_overflows) {
            observed_overflows = edges.overflows;
            ESP_LOGW(TAG, "Four-edge backlog overflow=%u: discard backlog, resync position without event replay", observed_overflows);
        }
        auto &manager = chip::DeviceLayer::PlatformMgr();
        if (manager.TryLockChipStack()) {
            if (!codes_printed) {
                PrintOnboardingCodes(chip::RendezvousInformationFlags(chip::RendezvousInformationFlag::kBLE));
                codes_printed = true;
            }
            if (reset_pending) {
                const esp_err_t result = esp_matter::factory_reset();
                ESP_LOGW(TAG, "Maintenance button released after 8s: reset request=%s", esp_err_to_name(result));
                manager.UnlockChipStack();
                vTaskDelete(nullptr);
                return;
            }
            if (!initialized_position) {
                // Persisted position is not treated as a new physical gesture.
                initialized_position = set_position(false) == ESP_OK;
                dispatch.resync();
            }
            if (!initialized_position) {
                manager.UnlockChipStack();
                vTaskDelay(pdMS_TO_TICKS(10));
                continue;
            }
            bool down = false;
            if (edges.resync(down)) {
                const esp_err_t result = set_position(down);
                dispatch.resync();
                ESP_LOGW(TAG, "Position-only resync=%u result=%s; no recovered event", down, esp_err_to_name(result));
            }
            // Bounded four-item drain. No SDK call occurs in a GPIO ISR.
            while (edges.pop(down)) {
                const esp_err_t result = set_position(down);
                if (result != ESP_OK) {
                    dispatch.resync();
                    ESP_LOGE(TAG, "Position update failed=%s; suppress matching release event", esp_err_to_name(result));
                    continue;
                }
                const auto event = dispatch.next(down, true);
                if (event == lab::SwitchEvent::InitialPress) {
                    cluster::switch_cluster::event::send_initial_press(switch_id, 1);
                    ESP_LOGI(TAG, "InitialPress submitted to SDK; not a controller delivery acknowledgement");
                } else if (event == lab::SwitchEvent::ShortRelease) {
                    cluster::switch_cluster::event::send_short_release(switch_id, 1);
                    ESP_LOGI(TAG, "ShortRelease submitted to SDK; not replayed after disconnect");
                }
            }
            manager.UnlockChipStack();
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

extern "C" void app_main(void)
{
    if (!lab_enabled()) {
        ESP_LOGW(TAG, "LAB_DISABLED: no NVS change, Thread/Matter start or BLE advertisement. Explicit sdkconfig.lab required.");
        return;
    }
    const esp_err_t nvs = nvs_flash_init();
    if (nvs != ESP_OK) {
        ESP_LOGE(TAG, "NVS init=%s; preserve data, do not auto-erase", esp_err_to_name(nvs));
        return;
    }
    gpio_config_t input = {};
    input.pin_bit_mask = (1ULL << EVENT_BUTTON) | (1ULL << RESET_BUTTON);
    input.mode = GPIO_MODE_INPUT; // Each SPST has its own external 10k pull-up.
    ESP_ERROR_CHECK(gpio_config(&input));
    node::config_t node_config;
    node_t *node = node::create(&node_config, update_cb, identify_cb);
    if (!node) { ESP_LOGE(TAG, "node create failed"); return; }
    endpoint::generic_switch::config_t switch_config;
    switch_config.switch_cluster.number_of_positions = 2;
    switch_config.switch_cluster.current_position = 0;
    switch_config.switch_cluster.feature_flags =
        cluster::switch_cluster::feature::momentary_switch::get_id() |
        cluster::switch_cluster::feature::momentary_switch_release::get_id();
    endpoint_t *endpoint = endpoint::generic_switch::create(node, &switch_config, ENDPOINT_FLAG_NONE, nullptr);
    if (!endpoint) { ESP_LOGE(TAG, "endpoint create failed"); return; }
    switch_id = endpoint::get_id(endpoint);
    auto *position = attribute::get(switch_id, Switch::Id, Switch::Attributes::CurrentPosition::Id);
    if (!position || attribute::set_deferred_persistence(position) != ESP_OK) {
        ESP_LOGE(TAG, "CurrentPosition deferred persistence unavailable"); return;
    }
    // Native C6 radio, not a UART RCP and not a Border Router.
    esp_openthread_platform_config_t thread = {};
    thread.radio_config.radio_mode = RADIO_MODE_NATIVE;
    thread.host_config.host_connection_mode = HOST_CONNECTION_MODE_NONE;
    thread.port_config.storage_partition_name = "nvs";
    thread.port_config.netif_queue_size = 10;
    thread.port_config.task_queue_size = 10;
    const esp_err_t platform = set_openthread_platform_config(&thread);
    if (platform != ESP_OK) {
        ESP_LOGE(TAG, "Thread platform config=%s", esp_err_to_name(platform)); return;
    }
    ESP_LOGW(TAG, "Development/test credentials; GenericSwitch endpoint=%u, Thread receiver-on lab", switch_id);
    const esp_err_t result = esp_matter::start(event_cb);
    if (result != ESP_OK) { ESP_LOGE(TAG, "Matter start=%s", esp_err_to_name(result)); return; }
    if (xTaskCreate(input_task, "r10_input", 4096, nullptr, 4, nullptr) != pdPASS)
        ESP_LOGE(TAG, "Input task unavailable; Matter stack may remain active without button sampling");
}

参考:[1] 固定ESP-Matter Generic Switch端点[2] 固定Momentary/Release feature与事件wrapper[3] 固定属性更新与延后持久化[5] 固定C6 OpenThread平台入口

06 / Matter完整开发实验

四条边沿溢出后只同步最新位置

共享策略30ms稳定门槛、初始按住忽略直到稳定释放,不承诺捕获采样/去抖以下毛刺。队列至多4条边沿;满时清积压,只记最新位置,下一次拿锁做位置同步而不补发旧事件,并清active press标志。

正常顺序只允许已提交press之后对应release;位置更新失败清配对并抑制相关事件,不能声称位置已同步。重同步失败也不补一个孤立release。边沿数量/SDK提交不等于最终无线事件次数,物理输入和控制器需分别验收。

代码 / 配置
// ---- main/input_policy.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <stdint.h>
namespace lab {
enum class Edge { None, Press, Release };
// One owner calls sample() with monotonic milliseconds. Start held is ignored
// until a stable release. No ISR, no allocation, no SDK dependency.
class Button {
    bool candidate_ = false, stable_ = false, armed_ = false, started_ = false;
    uint64_t candidate_at_ = 0, press_at_ = 0;
public:
    uint64_t last_hold_ms = 0;
    Edge sample(bool down, uint64_t now) {
        if (!started_) { started_ = true; candidate_ = down; candidate_at_ = now; return Edge::None; }
        if (down != candidate_) { candidate_ = down; candidate_at_ = now; return Edge::None; }
        if (now < candidate_at_ || now - candidate_at_ < 30) return Edge::None;
        if (!armed_) {
            stable_ = candidate_;
            if (!stable_) armed_ = true;
            return Edge::None;
        }
        if (stable_ == candidate_) return Edge::None;
        stable_ = candidate_;
        if (stable_) { press_at_ = now; return Edge::Press; }
        last_hold_ms = now >= press_at_ ? now - press_at_ : 0;
        return Edge::Release;
    }
    bool down() const { return armed_ && stable_; }
};
// At most four ordered position changes. On overflow drop the backlog and
// suppress events until the consumer resynchronizes the latest position.
class Edges {
    bool values_[4] = {}; unsigned head_ = 0, count_ = 0;
    bool resync_ = false, latest_ = false;
public:
    unsigned overflows = 0;
    void push(bool down) {
        latest_ = down;
        if (resync_) return;
        if (count_ == 4) { head_ = count_ = 0; resync_ = true; ++overflows; return; }
        values_[(head_ + count_) % 4] = down; ++count_;
    }
    bool resync(bool &position) {
        if (!resync_) return false;
        position = latest_; resync_ = false; return true;
    }
    bool pop(bool &position) {
        if (!count_) return false;
        position = values_[head_]; head_ = (head_ + 1) % 4; --count_; return true;
    }
};
enum class SwitchEvent { None, InitialPress, ShortRelease };
class Dispatch {
    bool active_ = false;
public:
    void resync() { active_ = false; }
    SwitchEvent next(bool down, bool position_ok) {
        if (!position_ok) { active_ = false; return SwitchEvent::None; }
        if (down) { active_ = true; return SwitchEvent::InitialPress; }
        if (!active_) return SwitchEvent::None;
        active_ = false; return SwitchEvent::ShortRelease;
    }
};
}
// ---- main/lab_config.c ----
// SPDX-License-Identifier: MIT
#include <stdbool.h>
#include "sdkconfig.h"
bool lab_enabled(void) {
#ifdef CONFIG_LAB_ENABLE_MATTER
    return true;
#else
    return false;
#endif
}

参考:[2] 固定Momentary/Release feature与事件wrapper[3] 固定属性更新与延后持久化

07 / Matter完整开发实验

先准备首选Thread网络与手机

HA OS的Thread integration应看见专用BR和首选网络,LAN保留IPv6/mDNS路径。按HA当前官方流程在完整Android Companion的Thread设置发送凭据到手机;网络名相近不等于同一个Dataset。Google Play Matter能力/Bluetooth/定位权限需满足官方条件。

手机BLE负责初始commissioning,不是HA主机蓝牙代替。C6既不创建BR也不自动组网;由手机把正确首选网络参数送入。Dataset含密钥,原log/配对码/fabric细节留私有目录,空CSV不要求写出密钥。

参考:[9] HA Thread:首选网络与手机凭据[10] HA ZBT-2:专用Thread/OTBR角色[11] HA Matter控制器与手机条件

08 / Matter完整开发实验

配对按层验收,不把BLE成功当终点

明确启用后应用持栈锁打印本次开发onboarding码;打印不等于窗口打开、Thread关联或fabric完成。Companion添加新Matter设备,依本次码完成开发流程;控制器对test attestation的限制如实定位,不把开发身份伪装认证产品。

依次保存BLE发现→Dataset/Thread关联→BR IPv6可达→fabric完成→实际event实体。对GPIO10按下/释放,观察CurrentPosition与initial_press/short_release,再以实际实体按控制器当前UI建立自动化。本文没有捏造entity ID或实测事件次数。

参考:[5] 固定C6 OpenThread平台入口[8] HA Core2026.10.2 Matter事件映射[9] HA Thread:首选网络与手机凭据[11] HA Matter控制器与手机条件

09 / Matter完整开发实验

重启、移除与明确恢复各有边界

固定CurrentPosition为NONVOLATILE,本例启用延后持久化,启动位置归零,不把旧持久down当新手势。没有省电承诺,也不保证瞬时掉电存储结果。

GPIO11独立稳定保持≥8秒再释放请求factory_reset;启动按住或事件按钮长按不触发恢复。固定SDK可能在局部擦除错误时仍安排重启,核实际log;不是物理安全擦除。控制器移除fabric、Thread Dataset、HA自动化不是同一份数据,逐层保存并重配。

参考:[1] 固定ESP-Matter Generic Switch端点[3] 固定属性更新与延后持久化[4] 固定Generic Switch应用参考[9] HA Thread:首选网络与手机凭据

10 / Matter完整开发实验

适用主机检查与尚空的实板表

tests/host.cpp实际使用固件头,16组ASan/UBSan覆盖去抖/初始按住/64位时间、四队列顺序、满后最新位置、孤立release抑制、位置失败。没有执行CHIP/Thread/BLE、BR/HA或GPIO电气。独立复核发现初始位置失败与队列交错后,另新增tests/owner_body.py:完整实际input_task提取,加真实共享头,在8种SDK错误/锁忙/队列/64位时序夹具中核门禁;旧包同体反例为P1/R0,修后为P1/R1。这是新增作者回归,不是原包已有测试,也不是独立复核人的结论。

commissioning.csv/events.csv只有表头。板修订、BR/手机版本、Thread凭据匹配、实际事件、重启、移除/重配均待真实实验。先分BLE、关联、IPv6、fabric、实体定位,不靠反复整片擦除掩盖层次;原创代码MIT,外部许可保留。

代码 / 配置
clang++ -std=c++17 -Wall -Wextra -Werror -fsanitize=address,undefined \
  -fno-omit-frame-pointer tests/host.cpp -o /tmp/r10-policy-host
/tmp/r10-policy-host

参考:[1] 固定ESP-Matter Generic Switch端点[2] 固定Momentary/Release feature与事件wrapper[3] 固定属性更新与延后持久化[8] HA Core2026.10.2 Matter事件映射[9] HA Thread:首选网络与手机凭据[11] HA Matter控制器与手机条件

BACK TO THE SOURCE

资料来源与版本说明

保留原计划ID/slug/M6与用途。R08以私有QEMU文件学习安全状态,原首次就地加密失败与文件替代路径分别留证;R09/R10完整配对路线未冒充无线或控制器实测。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. 固定ESP-Matter Generic Switch端点
  2. 固定Momentary/Release feature与事件wrapper
  3. 固定属性更新与延后持久化
  4. 固定Generic Switch应用参考
  5. 固定C6 OpenThread平台入口
  6. 固定C6官方指南/修订范围
  7. C6 v1.4原厂原理图第2页
  8. HA Core2026.10.2 Matter事件映射
  9. HA Thread:首选网络与手机凭据
  10. HA ZBT-2:专用Thread/OTBR角色
  11. HA Matter控制器与手机条件