从数据管线进入状态应用
固定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的资源生命周期
一张状态转移表
进入状态在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 PROVISIONING | SDK SoftAP Security1,原生官方客户端入口 | 300s完整入口窗口;单次失败不续窗口,SDK每次连接最多3尝试 | GOT_IP只记link,等PROV_END再ONLINE;到期请求停止→STOPPING_PROV |
| 3 STOPPING_PROV | 只等待服务结束,不开第二入口 | 5s等待END;这不是中断SDK内部阻塞API的总deadline | END后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位单调计时器
唯一所有者与事件队列
默认事件循环回调只复制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协议确定;不是硬实时响应模型。拥塞输入、错误高频事件、慢外设都要实板观察。
接线与屏幕图例
本例使用排针已可靠焊接的指定模块;未焊者先由具备条件者焊好并断电检查,不以松插排针作连接。断电接线,短线,共地;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电源与逻辑
准备自己的配网参数
公开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网络操作超时
失败矩阵与恢复
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驱动与许可
重放与本机客户端夹具
真实固件使用的同一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驱动与许可
依赖与许可
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页打包、地址与高度处理
状态模型与重放输入使用同一份源
以下为下载工程中对应文件的全文;带文件名的注释只是网页分隔,构建时使用独立原文件。其它构建配置、锁、许可与夹具在完整示例中。
// ---- 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电源与逻辑
完整应用入口:回调复制,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电源与逻辑
完整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电源与逻辑
配网客户端包装器全文
以下为下载工程中对应文件的全文;带文件名的注释只是网页分隔,构建时使用独立原文件。其它构建配置、锁、许可与夹具在完整示例中。
# ---- 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电源与逻辑
资料来源与版本说明
保留原计划E05用途与slug;固定上游、实际本机构建和适用的逻辑检查独立留证。未补造无线、采样、屏幕、真实Flash或掉电结果。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。
- IDF5.5.5配网管理器:持久配置、结束、失败会话与恢复
- IDF5.5.5配网API的资源生命周期
- IDF5.5.5 SoftAP scheme
- 官方Python Security1:X25519、PoP与AES-CTR
- 官方客户端HTTP网络操作超时
- 事件回调及事件数据的生命周期
- 完整64位单调计时器
- Sensirion C1.1.0固定提交:原始SHT4x驱动与许可
- IDF5.5.5 SSD1306页打包、地址与高度处理
- Adafruit SHT40具体供电、3V输出、0x44和模块上拉
- Adafruit PID326新STEMMA版:I2C跳线、自动复位、128×64地址
- 固定官方DevKitC-1 v1.1:N8R8、双USB接口、GPIO与供电
- Adafruit OLED供电:指定模块可用3.3V电源与逻辑