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

ESP32 做第一个 Matter Wi-Fi 小灯:从编译到控制器配对

固定ESP-Matter与IDF,提供外接LED、单槽按钮、状态恢复和完整Home Assistant配对路线;开发身份与实板配对结果分开记录。

读完能做什么
完成可构建On/Off Light工程,知道如何按BLE、Wi-Fi/IPv6、fabric、实体和真实LED逐层验收。
开始之前
先完成所列前置;能激活固定IDF环境、保存同次源码/配置/ELF/log。下载README含完整器材、命令、失败与恢复步骤。
所需器材
ESP32-S3-DevKitC-1 v1.1 / WROOM-1-N8R8,USB-to-UART数据线、Kingbright WP7113ID红LED、330Ω电阻;SPST按钮、10kΩ电阻;面包板短线、64位HA OS及官方Matter Server app、完整Android Companion/Google Play Matter能力、2.4GHz实验LAN
版本组合
框架:ESP-IDF5.5.5 / b774170ff46c393eeb5e495ea37936038d3f4f4fMatter固定源:ESP-Matter aeefd294b55ad9209da9bcc1757be73c8bad3cf3 / CHIP 9c2a63634e880b92e29b25a49543b70a4baf1572
适用范围

S3-DevKitC-1 v1.1/WROOM-1-N8R8,GPIO4外接LED、GPIO5按钮;8MiB DIO/8MiB OPI80,桥接UART。HA OS/Android参考路线,未运行实板或控制器。

这篇验证到了哪一步

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

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

01 / Matter完整开发实验

先锁定能看见实体的参考链路

本篇是On/Off Light设备类型0x0100、OnOff cluster0x0006;不复述N08的Wi-Fi配网入口。完整应用含本地按钮、控制器属性、持久状态与明确恢复操作。固定参考为HA OS(64位)+官方Matter Server app+完整Android Companion,不泛化所有生态支持。

当前只执行真实编译和固件共享输入策略,未运行HA/手机/板端无线。按后文步骤取得真实light实体、GPIO/LED一致及重启恢复,才能填写空表。使用SDK开发test DAC/setup参数,不是Matter认证或生产身份。

参考:[1] ESP-Matter固定README:推荐IDF5.5.5[2] 固定On/Off Light端点与属性创建[8] HA官方Matter控制器/手机配对要求

02 / Matter完整开发实验

断电按指定板和外设接线

选官方S3-DevKitC-1 v1.1/WROOM-1-N8R8;GPIO4→330Ω→WP7113ID阳极,阴极到GND。GPIO5经SPST到GND,另用10kΩ上拉3V3;代码不开内部上拉。LED阴极按原厂封装图Cathode识别,剪过脚后不只凭长短判断。先测按钮两侧触点,不把同侧常通脚误当开关。

只接USB-to-UART一根数据线供电/日志,不同时接原生USB或外电源。引脚看本板GPIO丝印,不套其他排针序号;GPIO只带此独立LED,不带灯带/继电器。原理图第2页已核GPIO4/5、桥接UART与原生USB路径;实际到手板修订仍需确认。

参考:[6] S3 v1.1官方指南[7] S3 v1.1原理图第2页[10] WP7113ID原厂V14A封装/极性与电气表

03 / Matter完整开发实验

固定ESP-Matter、CHIP与最小安装边界

本例ESP-Matter release/v1.5固定aeefd294…,其CHIP gitlink9c2a6363…;README推荐IDF5.5.5。Registry更早1.5.1快照不是同一源,不能互换。固定GN2255和8个隔离Python包,9个所需公共/ESP32子模块,保留原许可。

下载tools/prepare_dependencies.py只覆盖macOS arm64最小准备;复用R09安装可用--verify只读校验。没有全平台bootstrap、camera/WebRTC递归、其他MCU SDK、host chip-tool/chip-cert或mfg-tool。归档SDK没有.git时核版本与列出的固定文件SHA,不冒称整份SDK字节审核。

首轮实际GN因缺少pigweed_environment.gni失败;固定scripts/configure本就提供空override分支。作者按该分支创建唯一ignored生成文件,保留首失败,再完成真实构建;未改tracked SDK。完整安装/exports和SHA在README与library-lock。

代码 / 配置
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固定README:推荐IDF5.5.5[9] 固定CHIP最小环境生成分支

04 / Matter完整开发实验

默认不启动配对,明确启用另一构建

默认LAB_ENABLE_MATTER=n,将LED置低后返回,不初始化NVS/BLE/Matter;sdkconfig.lab才明确启用。完整工程固定8MiB Flash DIO80与8MiB OPI80 PSRAM,factory槽0x20000/0x600000,没有OTA槽。初刷前按E09备份原Flash/分区/SHA,不拿此app给原OTA工程升级。

作者实际启用版本r09-lab-gn-env-final构建Exit0,app1401376B;首次GN环境失败保留。源码/锁/配置/ELF/map/原log分别对应该次构建;实际build不证明PSRAM、电气、BLE或控制器可用。默认门控另有真实源的主机输入核查。

代码 / 配置
export LAB_PROJECT="/absolute/path/esp32-matter-wifi-light"
export LAB_BUILD="/absolute/path/r09-enabled"
idf.py -C "$LAB_PROJECT" -B "$LAB_BUILD" -D IDF_TARGET=esp32s3 \
  -D SDKCONFIG="$LAB_BUILD/sdkconfig" \
  -D "SDKCONFIG_DEFAULTS=$LAB_PROJECT/sdkconfig.defaults;$LAB_PROJECT/sdkconfig.lab" build
# 板修订/备份/UART口核对后,实体flash monitor命令见下载README。

参考:[1] ESP-Matter固定README:推荐IDF5.5.5[6] S3 v1.1官方指南[7] S3 v1.1原理图第2页

05 / Matter完整开发实验

完整实现:同一属性同时服务本地与远端

远端PRE_UPDATE回调只对本端点OnOff目标值应用GPIO;无关属性返回ESP_OK。Identify只记录,不拿额外闪烁覆盖持久OnOff。输入任务拿CHIP TryLock后读当前属性,再toggle或应用恢复值;没有一个旧main状态在重启或远端更新之后反向覆盖属性。

NVS初始化失败停止、不自动擦除;node/start/task创建失败分别记录。task创建失败时远端栈可能仍活跃,不把无按钮任务描述为全应用停止。以下main完整字节对应下载;测试DAC/setup与onboarding输出只用于私有开发实验。

代码 / 配置
// ---- 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 "input_policy.h"

extern "C" bool lab_enabled(void);
using namespace esp_matter;
using namespace chip::app::Clusters;
static constexpr gpio_num_t LED = GPIO_NUM_4;
static constexpr gpio_num_t BUTTON = GPIO_NUM_5;
static const char *TAG = "r09";
static uint16_t light_id;

static esp_err_t update_cb(attribute::callback_type_t type, uint16_t endpoint_id,
                           uint32_t cluster_id, uint32_t attribute_id,
                           esp_matter_attr_val_t *value, void *)
{
    if (type == attribute::PRE_UPDATE && endpoint_id == light_id &&
        cluster_id == OnOff::Id && attribute_id == OnOff::Attributes::OnOff::Id && value) {
        return gpio_set_level(LED, value->val.b ? 1 : 0);
    }
    return ESP_OK;
}

static esp_err_t identify_cb(identification::callback_type_t, uint16_t, uint8_t, uint8_t, void *)
{
    // Identification is logged, not mixed with the persistent OnOff output.
    ESP_LOGI(TAG, "Identify requested (no extra blink implemented)");
    return ESP_OK;
}

static void event_cb(const chip::DeviceLayer::ChipDeviceEvent *event, intptr_t)
{
    // Numeric SDK event only: commissioning completion is not an LED observation.
    ESP_LOGI(TAG, "Matter event=%u", static_cast<unsigned>(event->Type));
}

static void input_task(void *)
{
    lab::Button button;
    bool initialized_output = false, pending_toggle = false, pending_reset = false;
    unsigned dropped = 0;
    bool codes_printed = false;
    for (;;) {
        const uint64_t now = static_cast<uint64_t>(esp_timer_get_time() / 1000);
        const auto edge = button.sample(gpio_get_level(BUTTON) == 0, now);
        if (edge == lab::Edge::Release) {
            if (button.last_hold_ms >= 8000) {
                pending_reset = true;
                pending_toggle = false;
            } else if (!pending_reset) {
                if (pending_toggle) ESP_LOGW(TAG, "Pending toggle full; drop new=%u", ++dropped);
                else pending_toggle = true;
            }
        }
        // One bounded owner; a busy stack does not block button sampling.
        // Remote updates and this read/update execute under the same CHIP lock.
        auto &manager = chip::DeviceLayer::PlatformMgr();
        if ((pending_reset || pending_toggle || !initialized_output) && manager.TryLockChipStack()) {
            if (!codes_printed) {
                PrintOnboardingCodes(chip::RendezvousInformationFlags(chip::RendezvousInformationFlag::kBLE));
                codes_printed = true;
            }
            auto *onoff = attribute::get(light_id, OnOff::Id, OnOff::Attributes::OnOff::Id);
            esp_matter_attr_val_t value = esp_matter_bool(false);
            const esp_err_t read = onoff ? attribute::get_val(onoff, &value) : ESP_ERR_NOT_FOUND;
            if (!initialized_output && read == ESP_OK) {
                initialized_output = gpio_set_level(LED, value.val.b ? 1 : 0) == ESP_OK;
                ESP_LOGI(TAG, "Restored OnOff=%u, GPIO apply=%u", value.val.b, initialized_output);
            }
            if (pending_reset) {
                const esp_err_t result = esp_matter::factory_reset();
                pending_reset = false;
                ESP_LOGW(TAG, "Explicit released 8s hold: factory-reset request=%s", esp_err_to_name(result));
                // The SDK schedules its reset even if its local namespace erase fails.
                manager.UnlockChipStack();
                vTaskDelete(nullptr);
                return;
            }
            if (pending_toggle) {
                esp_err_t result = read;
                if (read == ESP_OK) {
                    value.val.b = !value.val.b;
                    result = attribute::update(light_id, OnOff::Id, OnOff::Attributes::OnOff::Id, &value);
                }
                ESP_LOGI(TAG, "Local toggle request=%s (not controller acknowledgement)", esp_err_to_name(result));
                pending_toggle = false; // A failed request is not replayed later.
            }
            manager.UnlockChipStack();
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

extern "C" void app_main(void)
{
    gpio_config_t output = {};
    output.pin_bit_mask = 1ULL << LED;
    output.mode = GPIO_MODE_OUTPUT;
    ESP_ERROR_CHECK(gpio_config(&output));
    ESP_ERROR_CHECK(gpio_set_level(LED, 0));
    if (!lab_enabled()) {
        ESP_LOGW(TAG, "LAB_DISABLED: LED off; no NVS change, BLE advertisement or Matter start. 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 << BUTTON;
    input.mode = GPIO_MODE_INPUT; // External 10k pull-up to 3.3V.
    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::on_off_light::config_t light_config;
    light_config.on_off.on_off = false;
    light_config.on_off_lighting.start_up_on_off = nullptr;
    endpoint_t *light = endpoint::on_off_light::create(node, &light_config, ENDPOINT_FLAG_NONE, nullptr);
    if (!light) { ESP_LOGE(TAG, "endpoint create failed"); return; }
    light_id = endpoint::get_id(light);
    ESP_LOGW(TAG, "Development/test DAC and setup parameters only. endpoint=%u", light_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, "r09_input", 4096, nullptr, 4, nullptr) != pdPASS)
        ESP_LOGE(TAG, "Input task unavailable; remote Matter stack may remain active");
}

参考:[2] 固定On/Off Light端点与属性创建[3] 固定属性更新/持久化实现[4] 固定core:start/factory_reset[5] CHIP锁与事件线程接口

06 / Matter完整开发实验

有界按钮与明确恢复的共享策略

唯一输入owner每10ms采样,64位单调毫秒和30ms稳定门槛;启动按住被忽略直到稳定释放。短按在稳定释放请求一次toggle;待处理单槽满时丢新计数,锁忙继续采样,失败请求不重放。

稳定按住≥8秒再释放才请求factory_reset,长按优先清待toggle;不在一直按着或初始按住时擦除。固定SDK即使局部namespace擦除返回错误也可能已安排重启,所以核原log/实际恢复;它不是物理安全擦除。

代码 / 配置
// ---- 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_; }
};
}
// ---- 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
}

参考:[3] 固定属性更新/持久化实现[4] 固定core:start/factory_reset[5] CHIP锁与事件线程接口

07 / Matter完整开发实验

从手机配对到真实LED,逐层填写

在HA OS准备Matter integration与官方Matter Server app;手机使用完整Companion、支持Matter的Google Play服务,开启Bluetooth与官方要求的定位权限。S3仅2.4GHz Wi-Fi,无需Thread Border Router;LAN保持IPv6/mDNS可达,避开访客隔离。

启用固件在CHIP锁内打印本次开发手工码/QR,打印成功不等于commissioning窗口或配对完成。用Companion添加新Matter设备,按本次码走BLE传递网络参数。控制器可能提示开发attestation,按当前官方开发流程定位,不伪装认证成品。

依次记录BLE发现、Wi-Fi/IPv6可达、fabric完成、实际light实体、控制器开/关与LED一致;码/fabric/原log留私有目录,不公开。本文没有实际执行这些步骤。

参考:[4] 固定core:start/factory_reset[5] CHIP锁与事件线程接口[8] HA官方Matter控制器/手机配对要求

08 / Matter完整开发实验

状态保存、移除和重新配对分开做

固定SDK的OnOff/nullable StartUpOnOff走NONVOLATILE属性路径;本例StartUpOnOff=null恢复上次值,启动取得锁后读回再应用GPIO。需要真实分别测开/关后的复位与掉电恢复,源码不能保证Flash写失败或瞬时断电结果。

从HA移除当前fabric与恢复出厂不同。先保存entity/endpoint及版本,再按需要移除或用8秒释放恢复;确认下一次配对码/窗口与fabric状态。不用自动erase整片Flash掩盖NVS或网络问题。

参考:[2] 固定On/Off Light端点与属性创建[3] 固定属性更新/持久化实现[4] 固定core:start/factory_reset[8] HA官方Matter控制器/手机配对要求

09 / Matter完整开发实验

可复跑主机检查与错误定位

下载tests/host.cpp实际使用固件input_policy.h,8组ASan/UBSan检查稳定边界、初始按住、短毛刺、长按释放、7999ms及跨32位时间。它不运行CHIP/NVS/PSRAM/BLE/Wi-Fi或实际按钮。commissioning.csv/events.csv仅表头。

失败先看LAB_DISABLED、板/串口/PSRAM与NVS/node/start返回,再看手机权限/2.4GHz/IPv6/mDNS/旧fabric。状态不一致同时留实体、属性返回与真实GPIO,不以日志提交等同控制器ACK。

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

参考:[3] 固定属性更新/持久化实现[4] 固定core:start/factory_reset[5] CHIP锁与事件线程接口[8] HA官方Matter控制器/手机配对要求

10 / Matter完整开发实验

下一篇先补网络角色,再换成按钮

R10保留独立C6工程与Generic Switch事件模型,加入专用Border Router、首选Thread网络和手机凭据同步。它不是把本篇Wi-Fi小灯改个名字,也不把事件按钮直接当可调用button.press服务。

参考:[2] 固定On/Off Light端点与属性创建[8] HA官方Matter控制器/手机配对要求

BACK TO THE SOURCE

资料来源与版本说明

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

  1. ESP-Matter固定README:推荐IDF5.5.5
  2. 固定On/Off Light端点与属性创建
  3. 固定属性更新/持久化实现
  4. 固定core:start/factory_reset
  5. CHIP锁与事件线程接口
  6. S3 v1.1官方指南
  7. S3 v1.1原理图第2页
  8. HA官方Matter控制器/手机配对要求
  9. 固定CHIP最小环境生成分支
  10. WP7113ID原厂V14A封装/极性与电气表