先锁定能看见实体的参考链路
本篇是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控制器/手机配对要求
断电按指定板和外设接线
选官方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封装/极性与电气表
固定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:-}"默认不启动配对,明确启用另一构建
默认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页
完整实现:同一属性同时服务本地与远端
远端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锁与事件线程接口
有界按钮与明确恢复的共享策略
唯一输入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锁与事件线程接口
从手机配对到真实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控制器/手机配对要求
状态保存、移除和重新配对分开做
固定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控制器/手机配对要求
可复跑主机检查与错误定位
下载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控制器/手机配对要求
下一篇先补网络角色,再换成按钮
R10保留独立C6工程与Generic Switch事件模型,加入专用Border Router、首选Thread网络和手机凭据同步。它不是把本篇Wi-Fi小灯改个名字,也不把事件按钮直接当可调用button.press服务。
资料来源与版本说明
保留原计划ID/slug/M6与用途。R08以私有QEMU文件学习安全状态,原首次就地加密失败与文件替代路径分别留证;R09/R10完整配对路线未冒充无线或控制器实测。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。