ESP-IDF 工程能力 / 事件状态应用发布 内容更新

事件驱动怎么组织 ESP32?让配网、采样和显示状态一致

用明确的生命周期状态表、单一owner和有界事件队列,组织Security1配网、SHT40采样与OLED;重放超时、掉线和读错,实板结果独立待测。

读完能做什么
得到能构建与重放的完整状态应用,按状态表检查每次入口、超时、退出和恢复。
开始之前
先完成所列前置并会保存同次固件、ELF和配置;硬件操作只对自己的固定实验板执行。
所需器材
固定S3-DevKitC-1 v1.1 / WROOM-1-N8R8、USB数据线、IDF5.5.5及本例README指定工具、SHT40 PID4885、PID326 2020自动复位SSD1306、SPST按钮和短线;两个模块排针已可靠焊接
版本组合
基线:ESP-IDF5.5.5 / b774170ff46c393eeb5e495ea37936038d3f4f4f器材复验:ESP32-S3-DevKitC-1 v1.1 / WROOM-1-N8R8
适用范围

S3-DevKitC-1 v1.1 / WROOM-1-N8R8;IDF5.5.5、8MiB Flash、桥接UART115200、PSRAM不初始化;SHT40 PID4885和PID326 2020自动复位SSD1306,SDA8/SCL9/100kHz,GPIO5恢复键。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/engineering-maintenance-sources.json;43份固定GitHub原始文件,安装器tools.json策略差异单列,不冒称全字节相同。验证日期:
构建 / 配置已通过
docs/esp32-evidence/engineering-maintenance-builds.json;E05最终实际Exit0,原源/配置/日志/bin/ELF/map SHA保留。对象:ESP32-S3 / N8R8计划版本:ESP-IDF5.5.5验证日期:
仿真不适用
仅执行适用主机逻辑/离线工具,不是ESP32芯片、I2C、USB或无线仿真。
本站实板待验证
尚无本例实际烧录、运行或接线结果;observations.csv实测字段为空。

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

01 / 事件状态应用

从数据管线进入状态应用

固定ESP32-S3-DevKitC-1 v1.1 / WROOM-1-N8R8,IDF5.5.5(b774170ff46c393eeb5e495ea37936038d3f4f4f),8MiB Flash/DIO80MHz,不启用PSRAM,桥接UART115200。SHT40 PID4885 0x44和Adafruit PID326的2020自动复位128×64 SSD1306版0x3D,共用SDA8/SCL9/100kHz;不是任意同外观小屏。

E04讨论生产者/消费者与队列满;本例把“已配网、正在重连、采样错误、屏幕旧值”集中为一个应用状态。设备可以离线继续读温湿度;ONLINE只表示当前无线IP链路,不等于传感器读数合格。屏幕和串口从同一Device快照取值,不各自维护另一个在线标志。

无本站烧录、Wi-Fi/PoP/Flash持久、按钮或I2C/图案结果;observations.csv实测栏全空。已做真实构建、同源模型重放和本机Security1合成服务器,不把它们叫实板或RTOS仿真。

做到这里,你应该看到
先核固定板型、版本与本例结果范围;将尚未做的实体步骤保留空白。

参考:[1] IDF5.5.5配网管理器:持久配置、结束、失败会话与恢复[2] IDF5.5.5配网API的资源生命周期

02 / 事件状态应用

一张状态转移表

进入状态在owner内更新entered/deadline,退出由同源device_step决定。BOOT不是独立看门狗;API的同步调用不被此表抢占。日志/队列/绘屏也使请求周期延后,不能把这些数字写成端到端实时保证。

代码/状态入口及动作时间规则退出
0 BOOT同步SDK/外设初始化后立刻投递存储是否存在的事件模型防漏启动事件1s;不覆盖前面的SDK初始化调用总耗时有SSID→CONNECTING;无SSID且私有参数有效→PROVISIONING;否则NEED_CONFIG
1 NEED_CONFIG入口关闭,不自动开AP;本地采样照常无自动寿命,owner20ms轮询/60s串口提示;不是阻塞等待刷入自己的参数并重启;GPIO5长按清Wi-Fi后重启(占位仍无效则继续关闭)
2 PROVISIONINGSDK SoftAP Security1,原生官方客户端入口300s完整入口窗口;单次失败不续窗口,SDK每次连接最多3尝试GOT_IP只记link,等PROV_END再ONLINE;到期请求停止→STOPPING_PROV
3 STOPPING_PROV只等待服务结束,不开第二入口5s等待END;这不是中断SDK内部阻塞API的总deadlineEND后link真→ONLINE,否则关闭Wi-Fi→NEED_CONFIG;未END→STOPPED
4 CONNECTING已有配置,启动STA/请求连接10s应用窗口,期限到请求disconnect实际IP→ONLINE;断开/超时→BACKOFF
5 ONLINE仅当前AP+非零IP已核对无强制寿命;5s核对链路,2s名义采样断开→BACKOFF;失败或队列溢出→STOPPED
6 BACKOFF只重连STA,不重开配网2/4/8/16/30s封顶;ONLINE掉线从2s重建时基到期→CONNECTING;已核对IP→ONLINE
7 STOPPED清有效读数,停止网络服务;保留串口/按钮路径不自动恢复;仍轮询物理长按修复根因后GPIO5恢复Wi-Fi并重启,或正常刷回已保存固件

参考:[1] IDF5.5.5配网管理器:持久配置、结束、失败会话与恢复[2] IDF5.5.5配网API的资源生命周期[7] 完整64位单调计时器

03 / 事件状态应用

唯一所有者与事件队列

默认事件循环回调只复制Notice(kind,reason)到容量16的队列,零等待;不传SDK临时指针、不打印SSID/密码、不调用Wi-Fi重连或I2C。满则原子标记overflow;owner下一轮进入STOPPED,而不是静默丢事件后仍宣称在线。

app_main就是唯一owner,每轮最多消费4项:先dequeue再取完整64位esp_timer单调毫秒;GOT_IP再核当前AP与非零IP,旧通知不当当前证明。owner拥有模型、Wi-Fi请求、provision manager句柄与两I2C设备。SDK内部仍有自己的网络任务;“唯一所有者”指应用调用与状态,不声称接管SDK全部线程。管理器END后才deinit,已保存配置路线先deinit再启动STA;应用回调不做这些动作。

本例把SHT读和OLED写放同一owner,减少状态所有权问题;因此它们会串行占用主循环,并非E04的吞吐/并发优化版。I2C读写各有原生事务超时、测量等待由Sensirion协议确定;不是硬实时响应模型。拥塞输入、错误高频事件、慢外设都要实板观察。

参考:[5] 官方客户端HTTP网络操作超时[6] 事件回调及事件数据的生命周期[7] 完整64位单调计时器

04 / 事件状态应用

接线与屏幕图例

本例使用排针已可靠焊接的指定模块;未焊者先由具备条件者焊好并断电检查,不以松插排针作连接。断电接线,短线,共地;GPIO5常开SPST到GND,使用内部上拉。两个指定模块的VIN/VCC接板3V3、GND到公共GND、SDA到8、SCL到9;SHT40 3Vo是输出且不接。PID326按2020版默认I2C跳线0x3D,RST=-1因该版自动复位,旧版RST及其它地址不在本例范围。模块已带上拉,不随意再并一组。桥接USB单路供电,不为配网插入另一供电源。

OLED四行:S<状态码> L<当前link>、T温度℃、H相对湿度%、E<样本尝试序号>。仅quality=1(FRESH)展示数值;0等待、2读错、3陈旧显示ERR和数字。CRC/事务失败立即读错,保留内存旧值但不再展示为有效;最后好样本超过6000ms或消费者时间早于样本时刻判STALE。读错优先,直到新好样本清除;离线但新本地采样仍可FRESH。湿度显示裁到0..100%,不把裁边当校准。

参考:[8] Sensirion C1.1.0固定提交:原始SHT4x驱动与许可[9] IDF5.5.5 SSD1306页打包、地址与高度处理[10] Adafruit SHT40具体供电、3V输出、0x44和模块上拉[11] Adafruit PID326新STEMMA版:I2C跳线、自动复位、128×64地址[12] 固定官方DevKitC-1 v1.1:N8R8、双USB接口、GPIO与供电[13] Adafruit OLED供电:指定模块可用3.3V电源与逻辑

05 / 事件状态应用

准备自己的配网参数

公开provision_secrets.example.h是无效占位,默认不开放AP。首次生成自己独有的PoP和AP密码(本机生成,不发到网站),复制文件到被.gitignore忽略的main/provision_secrets.h,再填值:

PoP本例16..64字符、AP12..63字符,均只用URL-safe字母数字/_/-,拒绝REPLACE前缀。参数进固件后不因.gitignore而加密,不能公开自己的构建/凭据。按E01进入固定SDK,在工程根执行idf.py -B build build,指定桥接串口flash monitor。默认源码构建也确实链接Security1/manager分支,公开占位仍在运行时关闭入口;不是只编了空配置壳。

电脑手动加入HIX-E05-后缀SoftAP,输入刚生成的AP密码;不是浏览器表单或captive portal。新建仅客户端依赖的小venv并固定安装,不再装SDK:

IDF_PATH仍指向5.5.5;Windows用py -3.12及.client-venv\Scripts\python。包装器调用固定SDK的tools/esp_prov官方Security1函数,隐藏交互输入PoP/2.4GHz WPA2 SSID/口令,不把凭据放argv或开verbose;配置失败不显示成功。单网络操作可有60s超时,20次状态轮询不是整流程20s期限。Security1是X25519+PoP+AES-CTR,不是HTTPS或开放热点认证替代。

代码 / 配置
python3 -c 'import secrets; print("PoP:",secrets.token_urlsafe(16)); print("AP:",secrets.token_urlsafe(16))'
cp main/provision_secrets.example.h main/provision_secrets.h

python3.12 -m venv .client-venv
.client-venv/bin/python -m pip install -r client-requirements.txt
.client-venv/bin/python provision_client.py

参考:[1] IDF5.5.5配网管理器:持久配置、结束、失败会话与恢复[2] IDF5.5.5配网API的资源生命周期[3] IDF5.5.5 SoftAP scheme[4] 官方Python Security1:X25519、PoP与AES-CTR[5] 官方客户端HTTP网络操作超时

06 / 事件状态应用

失败矩阵与恢复

GPIO5去抖50ms稳定后开始5s长按,只触发一次;松开稳定后重新武装。wifi_prov_mgr_reset_provisioning固定实现调用esp_wifi_restore,仅恢复Wi-Fi栈持久配置,不清所有应用NVS、不安全销毁Flash,返回OK才重启;原PoP/AP参数保留。NVS初始化错误不自动erase,先按E09保存数据,再按具体原因恢复。

  • 占位参数/无SSID:NEED_CONFIG,不能把看不到AP当射频故障。
  • 错Wi-Fi口令:配网窗口内看到reason及失败回执,重置失败session后可重填,窗口不延;重启后仅SSID存在仍不证明密码可用。
  • 已上线AP关闭/恢复:BACKOFF→CONNECTING→ONLINE,屏幕link变0/1,本地温湿度按自己的质量继续;不会因普通掉线开放AP。
  • 拔SHT40:下一次尝试立即ERR2,绝不平滑旧数据;重新接线前断电,恢复后好样本才有效。
  • OLED写失败:串口明确OLED_WRITE,停用失效屏,不声称它已自愈;修正接线后重启复验。
  • 队列溢出/API失败:STOPPED关闭网络、清有效读数;修复后重启,不用扩队列掩盖无限事件风暴。

参考:[1] IDF5.5.5配网管理器:持久配置、结束、失败会话与恢复[2] IDF5.5.5配网API的资源生命周期[8] Sensirion C1.1.0固定提交:原始SHT4x驱动与许可

07 / 事件状态应用

重放与本机客户端夹具

真实固件使用的同一model .c/.h重放启动/失败不续期限/300s+5s/有配置/上线/掉线/重复down/10s/30s上限/样本错误-陈旧-恢复/完整32位周期以后/overflow停止/长按与占位。它不模拟FreeRTOS/射频/I2C/OLED。另有真实127.0.0.1 HTTP+官方客户端Security1加密set/apply/status三例,但服务端是合成Python、connected/failed是合成状态,不能证明本板Wi-Fi或Flash。实际重启保存与实体显示仍须独立记录。

代码 / 配置
cc -std=c11 -Wall -Wextra -Werror -Imain main/device_model.c host/replay.c -o replay
./replay
.client-venv/bin/python security1_loopback.py

参考:[4] 官方Python Security1:X25519、PoP与AES-CTR[7] 完整64位单调计时器[8] Sensirion C1.1.0固定提交:原始SHT4x驱动与许可

08 / 事件状态应用

依赖与许可

Sensirion embedded-i2c-sht4x C1.1.0固定8e16a39098725bd53284b9a68268567f1315a1b4,9原文件未改,BSD-3-Clause LICENSE保留;HAL与页缓冲OLED/应用/模型/客户端包装器MIT。SDK5.5.5本机源、工具、客户端固定依赖、原始构建/host日志与SHA单列。初次构建漏HAL公共声明而失败的原源/日志留史,最终加正确include并成功构建,不关掉编译诊断来绕过。

参考:[8] Sensirion C1.1.0固定提交:原始SHT4x驱动与许可[9] IDF5.5.5 SSD1306页打包、地址与高度处理

09 / 事件状态应用

状态模型与重放输入使用同一份源

以下为下载工程中对应文件的全文;带文件名的注释只是网页分隔,构建时使用独立原文件。其它构建配置、锁、许可与夹具在完整示例中。

代码 / 配置
// ---- main/device_model.h ----
/* SPDX-License-Identifier: MIT */
#pragma once
#include <stdbool.h>
#include <stdint.h>
typedef enum {BOOT,NEED_CONFIG,PROVISIONING,STOPPING_PROV,CONNECTING,ONLINE,BACKOFF,STOPPED} DevicePhase;
typedef enum {BOOT_STORED,BOOT_EMPTY,STA_READY,IP_UP,LINK_DOWN,PROV_FAILED,PROV_ENDED,TICK,API_ERROR,QUEUE_OVERFLOW} DeviceEvent;
enum {START_STA=1,CONNECT_STA=2,START_PROV=4,RESET_PROV_SESSION=8,STOP_PROV=16,DEINIT_PROV=32,DISCONNECT_STA=64,STOP_WIFI=128};
typedef struct {
 DevicePhase phase;
 bool link,have_sample,read_error;
 uint8_t retry_power;
 uint64_t entered_ms,deadline_ms,last_sample_ms;
 uint32_t sample_sequence;
 int32_t temperature_milli,humidity_milli;
} Device;
void device_init(Device *d,uint64_t now);
uint32_t device_step(Device *d,DeviceEvent e,uint64_t now,bool secrets_ok);
void device_sample(Device *d,uint64_t now,bool success,int32_t temperature,int32_t humidity);
unsigned device_quality(const Device *d,uint64_t now); /*0WAIT,1FRESH,2READ_ERROR,3STALE*/
bool device_secrets_valid(const char *pop,const char *ap_password);
const char *device_phase_name(DevicePhase p);
typedef struct {bool candidate,stable,fired;uint64_t changed_ms,pressed_ms;} ResetButton;
bool reset_button_step(ResetButton *b,bool raw,uint64_t now);


// ---- main/device_model.c ----
/* SPDX-License-Identifier: MIT */
#include <string.h>
#include "device_model.h"
static void enter(Device *d,DevicePhase p,uint64_t now,uint64_t timeout){
 d->phase=p;d->entered_ms=now;d->deadline_ms=timeout?now+timeout:0;
}
static uint32_t backoff(Device *d,uint64_t now){
 uint64_t wait=2000ULL<<d->retry_power;
 if(wait>30000)wait=30000;
 if(d->retry_power<4)++d->retry_power;
 d->link=false;enter(d,BACKOFF,now,wait);return 0;
}
void device_init(Device *d,uint64_t now){memset(d,0,sizeof(*d));enter(d,BOOT,now,1000);}
uint32_t device_step(Device *d,DeviceEvent e,uint64_t now,bool secrets_ok){
 if(e==API_ERROR||e==QUEUE_OVERFLOW){
  bool provisioning=d->phase==PROVISIONING||d->phase==STOPPING_PROV;
  d->link=false;d->have_sample=false;d->read_error=true;
  enter(d,STOPPED,now,0);return provisioning?STOP_PROV:STOP_WIFI;
 }
 if(d->phase==STOPPED)return e==PROV_ENDED?DEINIT_PROV|STOP_WIFI:0;
 switch(d->phase){
 case BOOT:
  if(e==BOOT_STORED){enter(d,CONNECTING,now,10000);return DEINIT_PROV|START_STA;}
  if(e==BOOT_EMPTY){
   if(secrets_ok){enter(d,PROVISIONING,now,300000);return START_PROV;}
   enter(d,NEED_CONFIG,now,0);return DEINIT_PROV;
  }
  if(e==TICK&&now>=d->deadline_ms)return device_step(d,API_ERROR,now,secrets_ok);
  break;
 case NEED_CONFIG:break; /* Closed wait: only physical reset/new private build exits. */
 case PROVISIONING:
  if(e==IP_UP)d->link=true;
  if(e==LINK_DOWN)d->link=false;
  if(e==PROV_FAILED)return RESET_PROV_SESSION;
  if(e==PROV_ENDED){
   if(d->link){d->retry_power=0;enter(d,ONLINE,now,0);return DEINIT_PROV;}
   enter(d,NEED_CONFIG,now,0);return DEINIT_PROV|STOP_WIFI;
  }
  if(e==TICK&&now>=d->deadline_ms){enter(d,STOPPING_PROV,now,5000);return STOP_PROV;}
  break;
 case STOPPING_PROV:
  if(e==IP_UP)d->link=true;
  if(e==LINK_DOWN)d->link=false;
  if(e==PROV_ENDED){
   if(d->link){d->retry_power=0;enter(d,ONLINE,now,0);return DEINIT_PROV;}
   enter(d,NEED_CONFIG,now,0);return DEINIT_PROV|STOP_WIFI;
  }
  if(e==TICK&&now>=d->deadline_ms)return device_step(d,API_ERROR,now,secrets_ok);
  break;
 case CONNECTING:
  if(e==STA_READY)return CONNECT_STA;
  if(e==IP_UP){d->link=true;d->retry_power=0;enter(d,ONLINE,now,0);return 0;}
  if(e==LINK_DOWN)return backoff(d,now);
  if(e==TICK&&now>=d->deadline_ms){backoff(d,now);return DISCONNECT_STA;}
  break;
 case ONLINE:
  if(e==LINK_DOWN){d->retry_power=0;return backoff(d,now);}
  break;
 case BACKOFF:
  if(e==IP_UP){d->link=true;d->retry_power=0;enter(d,ONLINE,now,0);return 0;}
  if(e==TICK&&now>=d->deadline_ms){enter(d,CONNECTING,now,10000);return CONNECT_STA;}
  break;
 case STOPPED:break;
 }
 return 0;
}
void device_sample(Device *d,uint64_t now,bool success,int32_t temperature,int32_t humidity){
 if(d->phase==STOPPED)return;
 ++d->sample_sequence;d->read_error=!success;
 if(success){d->have_sample=true;d->last_sample_ms=now;d->temperature_milli=temperature;d->humidity_milli=humidity;}
}
unsigned device_quality(const Device *d,uint64_t now){
 if(d->read_error)return 2;
 if(!d->have_sample)return 0;
 if(now<d->last_sample_ms||now-d->last_sample_ms>6000)return 3;
 return 1;
}
/* Separate translation unit keeps the invalid public defaults from erasing the
 * entire provisioning branch in the actual non-LTO build. */
bool device_secrets_valid(const char *pop,const char *ap){
 if(!pop||!ap||strlen(pop)<16||strlen(pop)>64||strlen(ap)<12||strlen(ap)>63)return false;
 if(!strncmp(pop,"REPLACE",7)||!strncmp(ap,"REPLACE",7))return false;
 for(unsigned which=0;which<2;++which){const char *p=which?ap:pop;for(;*p;++p)if(!((*p>='A'&&*p<='Z')||(*p>='a'&&*p<='z')||(*p>='0'&&*p<='9')||*p=='_'||*p=='-'))return false;}
 return true;
}
const char *device_phase_name(DevicePhase p){
 static const char *names[]={"BOOT","NEED_CONFIG","PROVISIONING","STOPPING_PROV","CONNECTING","ONLINE","BACKOFF","STOPPED"};
 return p<=STOPPED?names[p]:"INVALID";
}
bool reset_button_step(ResetButton *b,bool raw,uint64_t now){
 if(raw!=b->candidate){b->candidate=raw;b->changed_ms=now;}
 if(now<b->changed_ms)return false;
 if(raw!=b->stable&&now-b->changed_ms>=50){b->stable=raw;b->pressed_ms=now;if(!raw)b->fired=false;}
 if(b->stable&&!b->fired&&now>=b->pressed_ms&&now-b->pressed_ms>=5000){b->fired=true;return true;}
 return false;
}


// ---- host/replay.c ----
/* SPDX-License-Identifier: MIT */
#include <assert.h>
#include <stdio.h>
#include "device_model.h"
static void expect(Device *d,DeviceEvent event,uint64_t at,bool secrets,DevicePhase phase,uint32_t action){
 uint32_t result=device_step(d,event,at,secrets);printf("SYNTHETIC event=%u at_ms=%llu phase=%s actions=%u\n",event,(unsigned long long)at,device_phase_name(d->phase),result);assert(d->phase==phase&&result==action);
}
int main(void){
 Device d;device_init(&d,0);expect(&d,BOOT_EMPTY,0,false,NEED_CONFIG,DEINIT_PROV);expect(&d,TICK,400000,false,NEED_CONFIG,0);
 device_init(&d,0);expect(&d,BOOT_EMPTY,0,true,PROVISIONING,START_PROV);expect(&d,PROV_FAILED,1,true,PROVISIONING,RESET_PROV_SESSION);assert(d.deadline_ms==300000);expect(&d,TICK,299999,true,PROVISIONING,0);expect(&d,TICK,300000,true,STOPPING_PROV,STOP_PROV);expect(&d,TICK,305000,true,STOPPED,STOP_PROV);
 device_init(&d,0);expect(&d,BOOT_EMPTY,0,true,PROVISIONING,START_PROV);expect(&d,IP_UP,100,true,PROVISIONING,0);assert(d.link);expect(&d,PROV_ENDED,200,true,ONLINE,DEINIT_PROV);
 expect(&d,LINK_DOWN,300,true,BACKOFF,0);assert(d.deadline_ms==2300);expect(&d,TICK,2299,true,BACKOFF,0);expect(&d,TICK,2300,true,CONNECTING,CONNECT_STA);expect(&d,TICK,12300,true,BACKOFF,DISCONNECT_STA);assert(d.deadline_ms==16300);expect(&d,LINK_DOWN,12301,true,BACKOFF,0);expect(&d,IP_UP,13000,true,ONLINE,0);
 device_init(&d,0);expect(&d,BOOT_STORED,1,false,CONNECTING,DEINIT_PROV|START_STA);expect(&d,STA_READY,2,false,CONNECTING,CONNECT_STA);expect(&d,IP_UP,100,false,ONLINE,0);
 device_sample(&d,200,true,25000,50000);assert(device_quality(&d,6200)==1);assert(device_quality(&d,6201)==3);device_sample(&d,6300,false,0,0);assert(device_quality(&d,6300)==2);device_sample(&d,6400,true,26000,60000);assert(device_quality(&d,6400)==1);
 uint64_t cycle=UINT64_C(1)<<32;assert(device_quality(&d,cycle+6400)==3);expect(&d,LINK_DOWN,cycle+6500,true,BACKOFF,0);assert(d.deadline_ms==cycle+8500);expect(&d,TICK,cycle+8500,true,CONNECTING,CONNECT_STA);
 for(unsigned i=0;i<8;++i){expect(&d,LINK_DOWN,d.entered_ms+1,true,BACKOFF,0);uint64_t delay=d.deadline_ms-d.entered_ms;assert(delay<=30000);expect(&d,TICK,d.deadline_ms,true,CONNECTING,CONNECT_STA);}assert(d.retry_power==4);
 expect(&d,QUEUE_OVERFLOW,d.entered_ms+1,true,STOPPED,STOP_WIFI);assert(!d.link&&device_quality(&d,d.entered_ms)==2);expect(&d,IP_UP,d.entered_ms+100,true,STOPPED,0);expect(&d,PROV_ENDED,d.entered_ms+200,true,STOPPED,DEINIT_PROV|STOP_WIFI);
 assert(!device_secrets_valid("REPLACE_WITH_PER_DEVICE_POP","REPLACE_WITH_PER_DEVICE_AP_PASSWORD"));assert(!device_secrets_valid("abcdefghijklmnop","short"));assert(device_secrets_valid("SYNTHETIC_HOST_ONLY_POP","SYNTHETIC_HOST_ONLY_PASSWORD"));assert(!device_secrets_valid("spaces are invalid","password_with12"));
 ResetButton b={0};assert(!reset_button_step(&b,true,cycle));assert(!reset_button_step(&b,true,cycle+50));assert(!reset_button_step(&b,true,cycle+5049));assert(reset_button_step(&b,true,cycle+5050));assert(!reset_button_step(&b,true,cycle+9999));
 puts("PASS E05 replay: closed defaults, provision deadline/failure/end,link/reconnect/backoff,read error/stale/recovery,64-bit time,queue overflow,secrets,physical-hold policy. Pure application model only; no Wi-Fi/I2C/OLED/RTOS simulation.");
}

参考:[1] IDF5.5.5配网管理器:持久配置、结束、失败会话与恢复[2] IDF5.5.5配网API的资源生命周期[3] IDF5.5.5 SoftAP scheme[4] 官方Python Security1:X25519、PoP与AES-CTR[5] 官方客户端HTTP网络操作超时[6] 事件回调及事件数据的生命周期[7] 完整64位单调计时器[8] Sensirion C1.1.0固定提交:原始SHT4x驱动与许可[9] IDF5.5.5 SSD1306页打包、地址与高度处理[10] Adafruit SHT40具体供电、3V输出、0x44和模块上拉[11] Adafruit PID326新STEMMA版:I2C跳线、自动复位、128×64地址[12] 固定官方DevKitC-1 v1.1:N8R8、双USB接口、GPIO与供电[13] Adafruit OLED供电:指定模块可用3.3V电源与逻辑

10 / 事件状态应用

完整应用入口:回调复制,owner执行

以下为下载工程中对应文件的全文;带文件名的注释只是网页分隔,构建时使用独立原文件。其它构建配置、锁、许可与夹具在完整示例中。

代码 / 配置
// ---- main/main.c ----
/* SPDX-License-Identifier: MIT */
#include <stdio.h>
#include <stdatomic.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "esp_wifi.h"
#include "esp_timer.h"
#include "esp_mac.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "wifi_provisioning/manager.h"
#include "wifi_provisioning/scheme_softap.h"
#include "sensirion/sht4x_i2c.h"
#include "sensirion/sensirion_i2c_hal.h"
#include "sensirion_hal_idf.h"
#include "device_model.h"
#include "oled.h"
#if __has_include("provision_secrets.h")
#include "provision_secrets.h"
#else
#include "provision_secrets.example.h"
#endif
/* Callback owns only a bounded copy into queue; owner task owns the model,
 * provisioning resource, Wi-Fi requests and both I2C devices. */
typedef struct {DeviceEvent kind;int reason;} Notice;
static QueueHandle_t queue;
static atomic_bool overflow;
static Device state;
static bool manager_alive,secrets_ok,oled_ok;
static esp_netif_t *station;
static uint64_t now_ms(void){return (uint64_t)(esp_timer_get_time()/1000);}
static void event(void *arg,esp_event_base_t base,int32_t id,void *data){
 (void)arg;Notice n={0};bool relevant=true;
 if(base==WIFI_EVENT&&id==WIFI_EVENT_STA_START)n.kind=STA_READY;
 else if(base==WIFI_EVENT&&id==WIFI_EVENT_STA_DISCONNECTED){n.kind=LINK_DOWN;n.reason=((wifi_event_sta_disconnected_t*)data)->reason;}
 else if(base==IP_EVENT&&id==IP_EVENT_STA_GOT_IP)n.kind=IP_UP;
 else if(base==WIFI_PROV_EVENT&&id==WIFI_PROV_CRED_FAIL){n.kind=PROV_FAILED;n.reason=*(wifi_prov_sta_fail_reason_t*)data;}
 else if(base==WIFI_PROV_EVENT&&id==WIFI_PROV_END)n.kind=PROV_ENDED;
 else relevant=false;
 if(relevant&&xQueueSend(queue,&n,0)!=pdTRUE)atomic_store(&overflow,true);
}
static bool link_is_current(void){
 wifi_ap_record_t ap;esp_netif_ip_info_t ip;
 return esp_wifi_sta_get_ap_info(&ap)==ESP_OK&&esp_netif_get_ip_info(station,&ip)==ESP_OK&&ip.ip.addr!=0;
}
static void actions(uint32_t a){
 esp_err_t error=ESP_OK;
 if((a&DEINIT_PROV)&&manager_alive){wifi_prov_mgr_deinit();manager_alive=false;}
 if(a&START_STA){error=esp_wifi_set_mode(WIFI_MODE_STA);if(error==ESP_OK)error=esp_wifi_start();}
 if((a&CONNECT_STA)&&error==ESP_OK)error=esp_wifi_connect();
 if((a&START_PROV)&&error==ESP_OK){
  uint8_t mac[6];error=esp_read_mac(mac,ESP_MAC_WIFI_STA);char name[32];
  if(error==ESP_OK){snprintf(name,sizeof(name),"HIX-E05-%02X%02X%02X",mac[3],mac[4],mac[5]);error=wifi_prov_mgr_start_provisioning(WIFI_PROV_SECURITY_1,DEMO_POP,name,DEMO_AP_PASSWORD);if(error==ESP_OK)printf("E05 SoftAP=%s Security1; use official native client, no web form; do not log credentials\n",name);}
 }
 if((a&RESET_PROV_SESSION)&&manager_alive&&error==ESP_OK)error=wifi_prov_mgr_reset_sm_state_on_failure();
 if((a&STOP_PROV)&&manager_alive)wifi_prov_mgr_stop_provisioning();
 if(a&DISCONNECT_STA){esp_err_t e=esp_wifi_disconnect();if(e!=ESP_OK&&e!=ESP_ERR_WIFI_NOT_CONNECT&&error==ESP_OK)error=e;}
 if(a&STOP_WIFI){esp_err_t e=esp_wifi_stop();if(e!=ESP_OK&&e!=ESP_ERR_WIFI_NOT_STARTED&&error==ESP_OK)error=e;}
 if(error!=ESP_OK){
  printf("E05 API_ERROR=%s; owner enters STOPPED, no callback retry\n",esp_err_to_name(error));
  uint32_t stop=device_step(&state,API_ERROR,now_ms(),secrets_ok);
  if((stop&STOP_PROV)&&manager_alive)wifi_prov_mgr_stop_provisioning();else (void)esp_wifi_stop();
 }
}
static void dispatch(DeviceEvent e,uint64_t now){
 DevicePhase before=state.phase;uint32_t a=device_step(&state,e,now,secrets_ok);actions(a);
 if(before!=state.phase)printf("E05 STATE %s -> %s event=%u link=%u quality=%u\n",device_phase_name(before),device_phase_name(state.phase),(unsigned)e,state.link?1U:0U,device_quality(&state,now));
}
void app_main(void){
 gpio_config_t button={.pin_bit_mask=1ULL<<5,.mode=GPIO_MODE_INPUT,.pull_up_en=GPIO_PULLUP_ENABLE};ESP_ERROR_CHECK(gpio_config(&button));
 esp_err_t e=nvs_flash_init();if(e!=ESP_OK){printf("E05 NVS_INIT=%s; no automatic erase\n",esp_err_to_name(e));return;}
 queue=xQueueCreate(16,sizeof(Notice));if(!queue){puts("E05 STOP queue allocation");return;}
 device_init(&state,now_ms());secrets_ok=device_secrets_valid(DEMO_POP,DEMO_AP_PASSWORD);
 sensirion_i2c_hal_init();if(!demo_i2c_ready()){puts("E05 STOP I2C init; no partial running application");return;}
 sht4x_init(0x44);e=oled_init(demo_i2c_bus());oled_ok=e==ESP_OK;
 if(!oled_ok)printf("E05 OLED_INIT=%s; serial still shows state, physical screen acceptance failed\n",esp_err_to_name(e));
 ESP_ERROR_CHECK(esp_netif_init());ESP_ERROR_CHECK(esp_event_loop_create_default());station=esp_netif_create_default_wifi_sta();esp_netif_create_default_wifi_ap();
 wifi_init_config_t wi=WIFI_INIT_CONFIG_DEFAULT();ESP_ERROR_CHECK(esp_wifi_init(&wi));ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_FLASH));
 ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT,ESP_EVENT_ANY_ID,event,NULL));
 ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT,IP_EVENT_STA_GOT_IP,event,NULL));
 ESP_ERROR_CHECK(esp_event_handler_register(WIFI_PROV_EVENT,ESP_EVENT_ANY_ID,event,NULL));
 wifi_prov_mgr_config_t cfg={.scheme=wifi_prov_scheme_softap,.scheme_event_handler=WIFI_PROV_EVENT_HANDLER_NONE,.wifi_prov_conn_cfg={.wifi_conn_attempts=3}};
 ESP_ERROR_CHECK(wifi_prov_mgr_init(cfg));manager_alive=true;bool stored=false;ESP_ERROR_CHECK(wifi_prov_mgr_is_provisioned(&stored));
 dispatch(stored?BOOT_STORED:BOOT_EMPTY,now_ms());
 ResetButton reset={0};uint64_t next_sample=now_ms(),next_view=next_sample,next_link=next_sample+5000,next_note=next_sample+60000;
 for(;;){
  if(atomic_exchange(&overflow,false)){puts("E05 EVENT_QUEUE_OVERFLOW: fail closed instead of pretending states still agree");dispatch(QUEUE_OVERFLOW,now_ms());}
  for(unsigned count=0;count<4;++count){
   Notice n;if(xQueueReceive(queue,&n,0)!=pdTRUE)break;
   uint64_t now=now_ms(); /* dequeue first; only then take the consumer time. */
   if(n.kind==IP_UP&&!link_is_current())continue; /* late notice is not current link proof */
   printf("E05 NOTICE=%u reason=%d\n",(unsigned)n.kind,n.reason);dispatch(n.kind,now);
  }
  uint64_t now=now_ms();dispatch(TICK,now);
  if(reset_button_step(&reset,gpio_get_level(GPIO_NUM_5)==0,now)){
   e=wifi_prov_mgr_reset_provisioning();
   if(e==ESP_OK){puts("E05 physical5s: Wi-Fi stack config restored; other app NVS unchanged, not secure erase. Restart.");vTaskDelay(pdMS_TO_TICKS(200));esp_restart();}
   else printf("E05 RESET_FAILED=%s; no restart/success claimed\n",esp_err_to_name(e));
  }
  if(state.phase!=STOPPED&&now>=next_sample){
   int32_t t=0,rh=0;int16_t result=sht4x_measure_high_precision(&t,&rh);uint64_t completed=now_ms();device_sample(&state,completed,result==0,t,rh);next_sample=completed+2000;
   printf("E05 SAMPLE seq=%lu result=%d T_milli=%ld RH_milli=%ld quality=%u\n",(unsigned long)state.sample_sequence,result,(long)t,(long)rh,device_quality(&state,completed));
  }
  now=now_ms();
  if(now>=next_link){next_link=now+5000;if(!manager_alive){if(link_is_current())dispatch(IP_UP,now);else if(state.phase==ONLINE)dispatch(LINK_DOWN,now);}}
  if(now>=next_view){next_view=now+500;if(oled_ok){e=oled_present(&state,now);if(e!=ESP_OK){oled_ok=false;printf("E05 OLED_WRITE=%s; no silent screen recovery claimed\n",esp_err_to_name(e));}}}
  if(now>=next_note){next_note=now+60000;printf("E05 STATUS=%s link=%u quality=%u; holdGPIO5 to restoreWiFi/restart; CONFIG_MISSING never opensAP\n",device_phase_name(state.phase),state.link?1U:0U,device_quality(&state,now));}
  vTaskDelay(pdMS_TO_TICKS(20));
 }
}


// ---- main/provision_secrets.example.h ----
/* SPDX-License-Identifier: MIT
 * Copy to ignored provision_secrets.h; generate unique per-device values. */
#pragma once
#define DEMO_POP "REPLACE_WITH_PER_DEVICE_POP"
#define DEMO_AP_PASSWORD "REPLACE_WITH_PER_DEVICE_AP_PASSWORD"

参考:[1] IDF5.5.5配网管理器:持久配置、结束、失败会话与恢复[2] IDF5.5.5配网API的资源生命周期[3] IDF5.5.5 SoftAP scheme[4] 官方Python Security1:X25519、PoP与AES-CTR[5] 官方客户端HTTP网络操作超时[6] 事件回调及事件数据的生命周期[7] 完整64位单调计时器[8] Sensirion C1.1.0固定提交:原始SHT4x驱动与许可[9] IDF5.5.5 SSD1306页打包、地址与高度处理[10] Adafruit SHT40具体供电、3V输出、0x44和模块上拉[11] Adafruit PID326新STEMMA版:I2C跳线、自动复位、128×64地址[12] 固定官方DevKitC-1 v1.1:N8R8、双USB接口、GPIO与供电[13] Adafruit OLED供电:指定模块可用3.3V电源与逻辑

11 / 事件状态应用

完整I2C与页缓冲屏实现

以下为下载工程中对应文件的全文;带文件名的注释只是网页分隔,构建时使用独立原文件。其它构建配置、锁、许可与夹具在完整示例中。

代码 / 配置
// ---- main/sensirion_hal_idf.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <stdbool.h>
#include "esp_err.h"
bool demo_i2c_ready(void);
esp_err_t demo_i2c_last_error(void);

#include "driver/i2c_master.h"
i2c_master_bus_handle_t demo_i2c_bus(void);


// ---- main/sensirion_hal_idf.c ----
// SPDX-License-Identifier: MIT
// New ESP-IDF 5.5.5 master API adapter; upstream protocol/CRC files unchanged.
#include "driver/i2c_master.h"
#include "esp_rom_sys.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sensirion_i2c_hal.h"
#include "sensirion_hal_idf.h"
static i2c_master_bus_handle_t bus;
static i2c_master_dev_handle_t device;
static esp_err_t last_error = ESP_ERR_INVALID_STATE;
bool demo_i2c_ready(void) { return device != NULL; }
esp_err_t demo_i2c_last_error(void) { return last_error; }
int16_t sensirion_i2c_hal_select_bus(uint8_t index) { return index == 0 ? 0 : -1; }
void sensirion_i2c_hal_init(void) {
    if (device) return;
    i2c_master_bus_config_t config = {.i2c_port = I2C_NUM_0, .sda_io_num = 8, .scl_io_num = 9, .clk_source = I2C_CLK_SRC_DEFAULT, .glitch_ignore_cnt = 7, .flags = {.enable_internal_pullup = false}};
    last_error = i2c_new_master_bus(&config, &bus);
    if (last_error != ESP_OK) return;
    i2c_device_config_t sensor = {.dev_addr_length = I2C_ADDR_BIT_LEN_7, .device_address = 0x44, .scl_speed_hz = 100000};
    last_error = i2c_master_bus_add_device(bus, &sensor, &device);
    if (last_error != ESP_OK) { i2c_del_master_bus(bus); bus = NULL; }
}
void sensirion_i2c_hal_free(void) {
    if (device) { i2c_master_bus_rm_device(device); device = NULL; }
    if (bus) { i2c_del_master_bus(bus); bus = NULL; }
}
int8_t sensirion_i2c_hal_read(uint8_t address, uint8_t *data, uint8_t count) {
    if (!device || address != 0x44) { last_error = ESP_ERR_INVALID_ARG; return -1; }
    last_error = i2c_master_receive(device, data, count, 100);
    return last_error == ESP_OK ? 0 : -1;
}
int8_t sensirion_i2c_hal_write(uint8_t address, const uint8_t *data, uint8_t count) {
    if (!device || address != 0x44) { last_error = ESP_ERR_INVALID_ARG; return -1; }
    last_error = i2c_master_transmit(device, data, count, 100);
    return last_error == ESP_OK ? 0 : -1;
}
void sensirion_i2c_hal_sleep_usec(uint32_t useconds) {
    const uint32_t tick_us = portTICK_PERIOD_MS * 1000U;
    if (useconds >= tick_us) vTaskDelay(((uint64_t)useconds + tick_us - 1) / tick_us + 1);
    else esp_rom_delay_us(useconds);
}

i2c_master_bus_handle_t demo_i2c_bus(void) { return bus; }


// ---- main/oled.h ----
#pragma once
#include <stdio.h>
#include "esp_err.h"
#include "driver/i2c_master.h"
#include "device_model.h"
esp_err_t oled_init(i2c_master_bus_handle_t bus);
esp_err_t oled_present(const Device *d,uint64_t now);


// ---- main/oled.c ----
/* SPDX-License-Identifier: MIT
 * Original small 5x7 glyphs. SSD1306 page-packed framebuffer (8 vertical bits).
 */
#include <string.h>
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_vendor.h"
#include "oled.h"
static esp_lcd_panel_handle_t panel;
static uint8_t frame[128*64/8];
static const uint8_t digits[10][5]={
 {0x3e,0x51,0x49,0x45,0x3e},{0,0x42,0x7f,0x40,0},{0x42,0x61,0x51,0x49,0x46},
 {0x21,0x41,0x45,0x4b,0x31},{0x18,0x14,0x12,0x7f,0x10},{0x27,0x45,0x45,0x45,0x39},
 {0x3c,0x4a,0x49,0x49,0x30},{1,0x71,9,5,3},{0x36,0x49,0x49,0x49,0x36},{6,0x49,0x49,0x29,0x1e}};
static void text(int page,const char *s) {
 int x=0;
 while (*s && x<122) {
  uint8_t g[5]={0}; char c=*s++;
  if(c>='0' && c<='9') memcpy(g,digits[c-'0'],5);
  else if(c=='S') { uint8_t t[]={0x26,0x49,0x49,0x49,0x32};memcpy(g,t,5); }
  else if(c=='L') { uint8_t t[]={0x7f,0x40,0x40,0x40,0x40};memcpy(g,t,5); }
  else if(c=='T') { uint8_t t[]={1,1,0x7f,1,1};memcpy(g,t,5); }
  else if(c=='H') { uint8_t t[]={0x7f,8,8,8,0x7f};memcpy(g,t,5); }
  else if(c=='G') { uint8_t t[]={0x3e,0x41,0x49,0x49,0x3a};memcpy(g,t,5); }
  else if(c=='E') { uint8_t t[]={0x7f,0x49,0x49,0x49,0x41};memcpy(g,t,5); }
  else if(c=='R') { uint8_t t[]={0x7f,9,0x19,0x29,0x46};memcpy(g,t,5); }
  else if(c=='.') g[2]=0x60;
  else if(c=='-') memset(g,8,5);
  memcpy(frame+page*128+x,g,5); x+=6;
 }
}
esp_err_t oled_init(i2c_master_bus_handle_t bus) {
 if (!bus) return ESP_ERR_INVALID_STATE;
 esp_lcd_panel_io_handle_t io=NULL;
 esp_lcd_panel_io_i2c_config_t config={.dev_addr=0x3D,.scl_speed_hz=100000,.control_phase_bytes=1,.lcd_cmd_bits=8,.lcd_param_bits=8,.dc_bit_offset=6,.transaction_timeout_ms=300};
 esp_err_t e=esp_lcd_new_panel_io_i2c(bus,&config,&io); if(e!=ESP_OK) return e;
 esp_lcd_panel_ssd1306_config_t vendor={.height=64};
 esp_lcd_panel_dev_config_t device={.reset_gpio_num=-1,.bits_per_pixel=1,.vendor_config=&vendor};
 e=esp_lcd_new_panel_ssd1306(io,&device,&panel); if(e!=ESP_OK) return e;
 if((e=esp_lcd_panel_reset(panel))!=ESP_OK || (e=esp_lcd_panel_init(panel))!=ESP_OK || (e=esp_lcd_panel_disp_on_off(panel,true))!=ESP_OK) { panel=NULL; return e; }
 return ESP_OK;
}
esp_err_t oled_present(const Device *d,uint64_t now) {
 if(!panel)return ESP_ERR_INVALID_STATE;
 memset(frame,0,sizeof(frame));char line[32];
 snprintf(line,sizeof(line),"S %u L %u",(unsigned)d->phase,d->link?1U:0U);text(0,line);
 if(device_quality(d,now)==1){
  snprintf(line,sizeof(line),"T %.1f",d->temperature_milli/1000.0);text(2,line);
  int32_t rh=d->humidity_milli<0?0:d->humidity_milli>100000?100000:d->humidity_milli;
  snprintf(line,sizeof(line),"H %.1f",rh/1000.0);text(4,line);
 }else{snprintf(line,sizeof(line),"ERR %u",device_quality(d,now));text(2,line);}
 snprintf(line,sizeof(line),"E %lu",(unsigned long)d->sample_sequence);text(6,line);
 return esp_lcd_panel_draw_bitmap(panel,0,0,128,64,frame);
}

参考:[1] IDF5.5.5配网管理器:持久配置、结束、失败会话与恢复[2] IDF5.5.5配网API的资源生命周期[3] IDF5.5.5 SoftAP scheme[4] 官方Python Security1:X25519、PoP与AES-CTR[5] 官方客户端HTTP网络操作超时[6] 事件回调及事件数据的生命周期[7] 完整64位单调计时器[8] Sensirion C1.1.0固定提交:原始SHT4x驱动与许可[9] IDF5.5.5 SSD1306页打包、地址与高度处理[10] Adafruit SHT40具体供电、3V输出、0x44和模块上拉[11] Adafruit PID326新STEMMA版:I2C跳线、自动复位、128×64地址[12] 固定官方DevKitC-1 v1.1:N8R8、双USB接口、GPIO与供电[13] Adafruit OLED供电:指定模块可用3.3V电源与逻辑

12 / 事件状态应用

配网客户端包装器全文

以下为下载工程中对应文件的全文;带文件名的注释只是网页分隔,构建时使用独立原文件。其它构建配置、锁、许可与夹具在完整示例中。

代码 / 配置
# ---- provision_client.py ----
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Official fixed-IDF client functions; no credentials in argv or debug output."""
import argparse, asyncio, getpass, os, sys
from pathlib import Path

def sdk_client():
    root=Path(os.environ['IDF_PATH'])
    sys.path.insert(0,str(root/'tools/esp_prov'))
    import esp_prov
    return esp_prov

async def provision(address,pop,ssid,password):
    client=sdk_client()
    tp=await client.get_transport('softap',address)
    if not tp: raise RuntimeError('transport unavailable')
    try:
        sec=client.get_security(1,0,None,None,pop,False)
        if not await client.establish_session(tp,sec): raise RuntimeError('Security1 session rejected; no credentials sent')
        if not await client.send_wifi_config(tp,sec,ssid,password): raise RuntimeError('SetConfig rejected')
        if not await client.apply_wifi_config(tp,sec): raise RuntimeError('ApplyConfig rejected')
        for _ in range(20):
            state=await client.get_wifi_config(tp,sec)
            if state=='connected': return True
            if state in ('failed','disconnected'): return False
            await asyncio.sleep(1)
        raise RuntimeError('status window elapsed; inspect device without assuming success')
    finally:
        tp.conn.close()

if __name__=='__main__':
    parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--address',default='192.168.4.1:80');a=parser.parse_args()
    pop=getpass.getpass('Per-device PoP (hidden): ')
    ssid=getpass.getpass('2.4GHz WPA2 SSID (hidden): ')
    password=getpass.getpass('Wi-Fi password (hidden): ')
    if len(pop)<16 or not 1<=len(ssid.encode())<=32 or not 8<=len(password.encode())<=63:
        raise SystemExit('Invalid lengths; this demo uses WPA2-PSK only')
    try: ok=asyncio.run(provision(a.address,pop,ssid,password))
    except Exception as e: raise SystemExit(f'Provisioning not confirmed: {type(e).__name__}: {e}')
    print('Client observed connected; now test physical reboot and persistence separately' if ok else 'Connection failed; correct credentials or hold GPIO5 for reset')
    raise SystemExit(0 if ok else 1)
做到这里,你应该看到
按README保存自己的原始日志和SHA;下载包的observations.csv实测列仍为空,不能用合成输入补填。

参考:[1] IDF5.5.5配网管理器:持久配置、结束、失败会话与恢复[2] IDF5.5.5配网API的资源生命周期[3] IDF5.5.5 SoftAP scheme[4] 官方Python Security1:X25519、PoP与AES-CTR[5] 官方客户端HTTP网络操作超时[6] 事件回调及事件数据的生命周期[7] 完整64位单调计时器[8] Sensirion C1.1.0固定提交:原始SHT4x驱动与许可[9] IDF5.5.5 SSD1306页打包、地址与高度处理[10] Adafruit SHT40具体供电、3V输出、0x44和模块上拉[11] Adafruit PID326新STEMMA版:I2C跳线、自动复位、128×64地址[12] 固定官方DevKitC-1 v1.1:N8R8、双USB接口、GPIO与供电[13] Adafruit OLED供电:指定模块可用3.3V电源与逻辑

BACK TO THE SOURCE

资料来源与版本说明

保留原计划E05用途与slug;固定上游、实际本机构建和适用的逻辑检查独立留证。未补造无线、采样、屏幕、真实Flash或掉电结果。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. IDF5.5.5配网管理器:持久配置、结束、失败会话与恢复
  2. IDF5.5.5配网API的资源生命周期
  3. IDF5.5.5 SoftAP scheme
  4. 官方Python Security1:X25519、PoP与AES-CTR
  5. 官方客户端HTTP网络操作超时
  6. 事件回调及事件数据的生命周期
  7. 完整64位单调计时器
  8. Sensirion C1.1.0固定提交:原始SHT4x驱动与许可
  9. IDF5.5.5 SSD1306页打包、地址与高度处理
  10. Adafruit SHT40具体供电、3V输出、0x44和模块上拉
  11. Adafruit PID326新STEMMA版:I2C跳线、自动复位、128×64地址
  12. 固定官方DevKitC-1 v1.1:N8R8、双USB接口、GPIO与供电
  13. Adafruit OLED供电:指定模块可用3.3V电源与逻辑