默认没有可用的公共配网密码
N08已有SoftAP配网入口;这里增加每设备私有生成、自定义应用令牌、可恢复清除和公开导出。不是浏览器配网页,也不是API业务客户端。指定S3 v1.1 N8R8,真实默认app903680B构建;占位会拒绝开放AP,实际无线/重启/实体键尚待板验。
固件和NVS未加密,没有Secure Boot/eFuse操作。private header、PoP、AP密码一旦编译进未受保护固件,不能视为抗提取秘密;删除文件也不会撤销旧令牌。
参考:[1] 配网管理器:自定义端点先于WiFi、create/register顺序[4] 固定Security1服务端实现[7] NVS set/commit与错误返回[9] 固定官方DevKitC-1 v1.1:N8R8、供电、USB口与J1针脚
从每设备生成值开始,保留空白公开工程
tools/make_private.py在工程根生成0600 private_config.h,树外新建0700 vault,device-access.json为0600。PoP为随机32hex,AP密码另生成;stdout不打印值、不放argv,已有目录/文件拒绝覆盖。
私有头生成/轮换后用新的IDF构建目录,避免默认构建未跟踪不存在的__has_include头而复用旧对象;下载README给出完整命令。在本机读vault并识别HIX-R07-MAC后缀AP;不要共享vault/header/QR/截图/编译产物。构建仍在IDF终端,主机工具另用Python3.12 venv及tools/requirements.txt,保留同版IDF_PATH。SDK Python未必装protobuf,不能只凭idf.py可运行就说客户端依赖齐全。
# POSIX shell · 主机Python3.12环境
python -m pip install -r tools/requirements.txt
python tools/make_private.py --project "$PWD" --vault "$HOME/esp32-private/r07-device-new"
# 在IDF构建终端按README重建;不要把生成的header或产物公开。参考:[4] 固定Security1服务端实现[5] 官方原生客户端入口[6] 官方Python Security1客户端
原生客户端先提交令牌,再提交Wi-Fi
客户端隐藏输入PoP、SSID、密码和8..128无空白可打印ASCII令牌;官方Security1会话建立后,把v1:令牌加密发app-token端点。设备mutex保护set_str+commit,只有COMMITTED才继续SetConfig/ApplyConfig。
端点必须start之前create、start之后register;客户端必须在WiFi完成停止服务前访问。入口300s,错误凭据允许reset_sm_state_on_failure但不重新开始窗口。ApplyConfig回执不等于GOT_IP,COMMITTED也不是重启持久化证明。
# POSIX shell · 主机venv,已连接该设备AP,IDF_PATH保留
python tools/provision_private.py --address 192.168.4.1:80参考:[1] 配网管理器:自定义端点先于WiFi、create/register顺序[2] 管理器停止、重试和存储路径[3] SoftAP方案实现[4] 固定Security1服务端实现[5] 官方原生客户端入口[6] 官方Python Security1客户端
Wi-Fi和应用令牌不是一个全局事务
token提交后WiFi失败,可以只留下token;已有WiFi而token缺失也不能自动开放AP补写。TOKEN_PRESENT只核NVS键长度存在,不验证下游API服务。设备已有WiFi时直接STA,需按逻辑清除后才能重新配网。
实体操作应核token存在、GOT_IP、重启后两者分别保留;本例没有实际板上提交或真实业务令牌使用。源码临时缓冲清零也不构成整个Flash/RAM的秘密清除保证。应用日志不回显输入,但SDK/驱动可记录SSID/MAC、DEBUG可含掩码密码线索,原日志私密保留并人工复核后分享。
参考:[1] 配网管理器:自定义端点先于WiFi、create/register顺序[2] 管理器停止、重试和存储路径[7] NVS set/commit与错误返回[8] esp_wifi_restore恢复SDK持久设置范围
按键清除先落标记,下一次启动继续做
断电接GPIO5/J1-5→SPST→GND/J1-22,10kΩ上拉到3V3/J1-1或2;板排针与按钮连接可靠,不接5V,不拿BOOT/GPIO0代替。上电已LOW不擦除,先释放再连续按5s,20ms轮询;实体去抖/资格尚待验证。
请求清除先提交r07/clear=1再重启。每次启动在开放AP/STA之前,先restore WiFi,再erase token/commit,再erase clear/commit;任一步失败停启动,缺键幂等,其他namespace保留。NVS错误可能已部分改变,不能假称Flash回滚或安全擦除。原固件PoP/AP密码没有轮换。
参考:[1] 配网管理器:自定义端点先于WiFi、create/register顺序[2] 管理器停止、重试和存储路径[7] NVS set/commit与错误返回[8] esp_wifi_restore恢复SDK持久设置范围[9] 固定官方DevKitC-1 v1.1:N8R8、供电、USB口与J1针脚
完整令牌端点、长按和启动续做固件
resetting和mutex排斥令牌写与清除请求;标记提交失败不宣称成功,不自动erase整个NVS。clear中断后可重复执行,但物理掉电结果仍要单独实测。
// ---- main/main.c ----
/* SPDX-License-Identifier: MIT */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdatomic.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_event.h"
#include "esp_wifi.h"
#include "esp_netif.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_mac.h"
#include "driver/gpio.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "wifi_provisioning/manager.h"
#include "wifi_provisioning/scheme_softap.h"
#include "secret_policy.h"
#include "credentials.h"
static const char*TAG="R07";
static SemaphoreHandle_t store_lock;
static atomic_bool resetting,got_ip,failed,ended,provisioning;
static esp_err_t open_store(nvs_handle_t*h){return nvs_open("r07",NVS_READWRITE,h);}
/* Every boot resumes any interrupted logical reset, before opening any endpoint. */
static esp_err_t resume_clear(void){
nvs_handle_t n;esp_err_t e=open_store(&n);if(e!=ESP_OK)return e;uint8_t pending=0;e=nvs_get_u8(n,"clear",&pending);if(e==ESP_ERR_NVS_NOT_FOUND)e=ESP_OK;if(e!=ESP_OK||!pending){nvs_close(n);return e;}
ESP_LOGW(TAG,"RESUME_CLEAR: logical Wi-Fi and app-token reset; no secure flash sanitization");e=esp_wifi_restore();
if(e==ESP_OK){e=nvs_erase_key(n,"token");if(e==ESP_ERR_NVS_NOT_FOUND)e=ESP_OK;}
if(e==ESP_OK)e=nvs_commit(n);
if(e==ESP_OK){e=nvs_erase_key(n,"clear");if(e==ESP_ERR_NVS_NOT_FOUND)e=ESP_OK;}
if(e==ESP_OK){e=nvs_commit(n);}
nvs_close(n);return e;
}
static esp_err_t token_endpoint(uint32_t sid,const uint8_t*in,ssize_t len,uint8_t**out,ssize_t*outlen,void*unused){
(void)sid;(void)unused;bool accepted=false;
if(!atomic_load(&resetting)&&len>=3&&!memcmp(in,"v1:",3)&&token_valid(in+3,(size_t)len-3)&&xSemaphoreTake(store_lock,pdMS_TO_TICKS(1000))==pdTRUE){
if(!atomic_load(&resetting)){nvs_handle_t n;esp_err_t e=open_store(&n);if(e==ESP_OK){char token[129];size_t count=(size_t)len-3;memcpy(token,in+3,count);token[count]=0;e=nvs_set_str(n,"token",token);memset(token,0,sizeof(token));if(e==ESP_OK)e=nvs_commit(n);nvs_close(n);accepted=e==ESP_OK;}ESP_LOGI(TAG,"TOKEN_COMMIT=%s (value omitted)",accepted?"OK":"REJECT");}
xSemaphoreGive(store_lock);
}
const char*reply=accepted?"COMMITTED":"REJECTED";*outlen=(ssize_t)strlen(reply);*out=malloc((size_t)*outlen);if(!*out)return ESP_ERR_NO_MEM;memcpy(*out,reply,(size_t)*outlen);return ESP_OK;
}
static void event(void*a,esp_event_base_t base,int32_t id,void*d){
(void)a;(void)d;if(base==WIFI_PROV_EVENT){if(id==WIFI_PROV_CRED_RECV)ESP_LOGI(TAG,"WIFI_CREDENTIALS_RECEIVED values omitted; not connection success");else if(id==WIFI_PROV_CRED_FAIL)atomic_store(&failed,true);else if(id==WIFI_PROV_END)atomic_store(&ended,true);}
else if(base==IP_EVENT&&id==IP_EVENT_STA_GOT_IP)atomic_store(&got_ip,true);
else if(base==WIFI_EVENT&&id==WIFI_EVENT_STA_DISCONNECTED)atomic_store(&got_ip,false);
}
static bool token_present(void){nvs_handle_t n;esp_err_t e=nvs_open("r07",NVS_READONLY,&n);if(e!=ESP_OK)return false;size_t len=0;e=nvs_get_str(n,"token",NULL,&len);nvs_close(n);return e==ESP_OK&&len>=9&&len<=129;}
static bool request_clear(void){
atomic_store(&resetting,true);if(xSemaphoreTake(store_lock,pdMS_TO_TICKS(1000))!=pdTRUE){atomic_store(&resetting,false);return false;}nvs_handle_t n;esp_err_t e=open_store(&n);if(e==ESP_OK){e=nvs_set_u8(n,"clear",1);if(e==ESP_OK)e=nvs_commit(n);nvs_close(n);}xSemaphoreGive(store_lock);if(e!=ESP_OK){ESP_LOGE(TAG,"CLEAR_MARKER_FAILED=%s; no reset success claimed",esp_err_to_name(e));atomic_store(&resetting,false);return false;}
ESP_LOGW(TAG,"CLEAR_MARKER_COMMITTED: restart resumes reset before any AP/STA; other namespaces preserved");fflush(stdout);vTaskDelay(pdMS_TO_TICKS(200));esp_restart();return true;
}
void app_main(void){
gpio_config_t gp={.pin_bit_mask=1ULL<<5,.mode=GPIO_MODE_INPUT,.pull_up_en=GPIO_PULLUP_ENABLE};ESP_ERROR_CHECK(gpio_config(&gp));store_lock=xSemaphoreCreateMutex();if(!store_lock){ESP_LOGE(TAG,"MUTEX_ALLOC_FAILED");return;}
esp_err_t e=nvs_flash_init();if(e!=ESP_OK){ESP_LOGE(TAG,"NVS=%s no auto erase; back up before deliberate partition recovery",esp_err_to_name(e));return;}
ESP_ERROR_CHECK(esp_netif_init());ESP_ERROR_CHECK(esp_event_loop_create_default());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));e=resume_clear();if(e!=ESP_OK){ESP_LOGE(TAG,"RESUME_CLEAR_FAILED=%s; no AP or station started",esp_err_to_name(e));return;}
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_PROV_EVENT,ESP_EVENT_ANY_ID,event,NULL));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));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));bool has_wifi=false;ESP_ERROR_CHECK(wifi_prov_mgr_is_provisioned(&has_wifi));bool manager=true,station=false;int64_t began=esp_timer_get_time()/1000,next_retry=began;ResetHold hold={0};
ESP_LOGI(TAG,"TOKEN_PRESENT=%d (logical key existence only); hardware/secret extraction resistance unverified",token_present());
if(has_wifi){ESP_LOGI(TAG,"STORED_WIFI_PRESENT: token and Wi-Fi not globally transactional; verify both after reboot");wifi_prov_mgr_deinit();manager=false;station=true;ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));ESP_ERROR_CHECK(esp_wifi_start());}
else if(!r07_private_config_valid()){ESP_LOGW(TAG,"PRIVATE_CONFIG_MISSING: no AP opened; generate per-device private header, rebuild; no secret printed");}
else{
uint8_t mac[6];ESP_ERROR_CHECK(esp_read_mac(mac,ESP_MAC_WIFI_STA));char ap[32];snprintf(ap,sizeof(ap),"HIX-R07-%02X%02X%02X",mac[3],mac[4],mac[5]);ESP_ERROR_CHECK(wifi_prov_mgr_endpoint_create("app-token"));atomic_store(&provisioning,true);ESP_ERROR_CHECK(wifi_prov_mgr_start_provisioning(WIFI_PROV_SECURITY_1,r07_pop(),ap,r07_ap_password()));ESP_ERROR_CHECK(wifi_prov_mgr_endpoint_register("app-token",token_endpoint,NULL));ESP_LOGI(TAG,"SOFTAP=%s Security1 native client; 300s window. Token first, then Wi-Fi apply; no browser form.",ap);
}
bool window_closed=false;
for(;;){int64_t now=esp_timer_get_time()/1000;if(reset_hold(&hold,gpio_get_level(GPIO_NUM_5)==0,now))request_clear();
if(manager&&atomic_exchange(&failed,false)){e=wifi_prov_mgr_reset_sm_state_on_failure();ESP_LOGW(TAG,"PROVISION_RETRY=%s original deadline retained",esp_err_to_name(e));}
if(manager&&atomic_load(&provisioning)&&!window_closed&&now-began>=300000){window_closed=true;wifi_prov_mgr_stop_provisioning();ESP_LOGW(TAG,"WINDOW_EXPIRED no automatic reopen; release then hold GPIO5 five seconds for logical clear");}
if(manager&&atomic_exchange(&ended,false)){wifi_prov_mgr_deinit();manager=false;atomic_store(&provisioning,false);station=atomic_load(&got_ip);if(!station)esp_wifi_stop();}
if(station&&!atomic_load(&got_ip)&&now>=next_retry){next_retry=now+5000;e=esp_wifi_connect();if(e!=ESP_OK&&e!=ESP_ERR_WIFI_CONN)ESP_LOGW(TAG,"CONNECT_REQUEST=%s",esp_err_to_name(e));}
vTaskDelay(pdMS_TO_TICKS(20));
}
}
// ---- main/secret_policy.h ----
/* SPDX-License-Identifier: MIT */
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
static inline bool token_valid(const uint8_t*p,size_t n){if(n<8||n>128)return false;for(size_t i=0;i<n;i++)if(p[i]<33||p[i]>126)return false;return true;}
static inline bool private_config_valid(const char*pop,const char*ap){return strlen(pop)>=16&&strlen(pop)<=64&&strlen(ap)>=12&&strlen(ap)<=63&&!strstr(pop,"REPLACE_")&&!strstr(ap,"REPLACE_");}
typedef struct{bool released,holding,fired;int64_t since;} ResetHold;
/* Boot-held button cannot erase; release then hold continuously for five seconds. */
static inline bool reset_hold(ResetHold*s,bool low,int64_t now){if(!low){s->released=true;s->holding=false;s->fired=false;return false;}if(!s->released||s->fired)return false;if(!s->holding){s->holding=true;s->since=now;return false;}if(now<s->since){s->since=now;return false;}if(now-s->since<5000)return false;s->fired=true;return true;}
参考:[1] 配网管理器:自定义端点先于WiFi、create/register顺序[2] 管理器停止、重试和存储路径[7] NVS set/commit与错误返回[8] esp_wifi_restore恢复SDK持久设置范围
把私有头隔在独立翻译单元
credentials.c加载私有头或公开占位;main只通过外部函数取得值与有效性,明确关闭LTO,使默认安全配置也保留真实配网链接路径。这个代码边界便于构建检查,不是秘密存储或安全隔离边界。
// ---- main/credentials.c ----
/* SPDX-License-Identifier: MIT */
#include "credentials.h"
#include "secret_policy.h"
#if __has_include("../private_config.h")
#include "../private_config.h"
#else
#include "private_config.example.h"
#endif
/* Separate translation unit, LTO disabled: the default safe-placeholder build
* still links the real provisioning path. This is no secret-storage boundary. */
const char *r07_pop(void){return R07_POP;}
const char *r07_ap_password(void){return R07_AP_PASSWORD;}
bool r07_private_config_valid(void){return private_config_valid(R07_POP,R07_AP_PASSWORD);}
// ---- main/credentials.h ----
/* SPDX-License-Identifier: MIT */
#pragma once
#include <stdbool.h>
const char *r07_pop(void);
const char *r07_ap_password(void);
bool r07_private_config_valid(void);
参考:[1] 配网管理器:自定义端点先于WiFi、create/register顺序[4] 固定Security1服务端实现
完整客户端和公开导出规则
公开导出按允许文件类别新建目录,排除private header、sdkconfig、build、ELF/bin/map、日志和vault;拒绝公开符号链接、规定的C字面凭据和原始/引号起始PEM头。附逐文件SHA,不回显拒绝值。
规则不覆盖任意编码、其他变量名、二进制、Git旧提交、截图和附件;人工复核与真实泄露后的撤销/轮换仍必要。以下完整代码可复跑本机检查,不能把扫描PASS写成“项目不存在秘密”。
# ---- tools/provision_private.py ----
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""SDK5.5.5 Security1 native HTTP client; hidden prompts, no credentials in argv."""
import argparse,asyncio,os,pathlib,sys,getpass
idf=pathlib.Path(os.environ['IDF_PATH']);sys.path[:0]=[str(idf/'tools/esp_prov'),str(idf/'components/protocomm/python')];import esp_prov
esp_prov.config_throw_except=True
async def provision(address,pop,ssid,password,token):
if not 1<=len(ssid.encode())<=32 or not 8<=len(password.encode())<=63 or not 8<=len(token)<=128 or any(ord(c)<33 or ord(c)>126 for c in token):raise ValueError('input format rejected; values omitted')
sec=esp_prov.get_security(1,0,'','',pop,False);tp=await esp_prov.get_transport('softap',address)
try:
if not await esp_prov.establish_session(tp,sec):raise RuntimeError('Security1 session failed')
payload=sec.encrypt_data(('v1:'+token).encode('ascii'));response=await tp.send_data('app-token',payload);reply=sec.decrypt_data(response.encode('latin1'))
if reply!=b'COMMITTED':raise RuntimeError('token commit rejected; Wi-Fi not applied')
print('App token commit acknowledged; no token printed. This is not a reboot persistence proof.')
if not await esp_prov.send_wifi_config(tp,sec,ssid,password):raise RuntimeError('Wi-Fi staging rejected')
if not await esp_prov.apply_wifi_config(tp,sec):raise RuntimeError('Wi-Fi apply rejected')
print('Wi-Fi apply acknowledged; verify device GOT_IP/reboot and app token separately. They are not one global transaction.')
finally:
if hasattr(tp,'conn'):tp.conn.close()
def main():
p=argparse.ArgumentParser();p.add_argument('--address',default='192.168.4.1:80');a=p.parse_args()
pop=getpass.getpass('Per-device PoP (hidden): ');ssid=getpass.getpass('Owned 2.4GHz SSID (hidden): ');password=getpass.getpass('Wi-Fi password (hidden): ');token=getpass.getpass('App token,8..128 printable ASCII chars (hidden): ')
try:asyncio.run(provision(a.address,pop,ssid,password,token))
except Exception as e:print(type(e).__name__+': provisioning failed; values deliberately omitted',file=sys.stderr);return 2
return 0
if __name__=='__main__':raise SystemExit(main())
# ---- tools/share_check.py ----
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Explicit public-file export plus literal credential checks; not a secret oracle."""
import argparse,pathlib,re,shutil,json,hashlib
p=argparse.ArgumentParser();p.add_argument('--project',required=True);p.add_argument('--out',required=True);a=p.parse_args();root=pathlib.Path(a.project).resolve();out=pathlib.Path(a.out).resolve()
if out==root or root in out.parents:p.error('export must be outside source project')
allowed={'CMakeLists.txt','sdkconfig.defaults','partitions.csv','dependencies.lock','versions.json','README.md','LICENSE','THIRD_PARTY.md','.gitignore'}
rows=[];issues=[]
for f in sorted(root.rglob('*')):
if not f.is_file():continue
rel=f.relative_to(root);parts=rel.parts
public=str(rel)in allowed or(parts[0]in('main','tests','tools')and f.suffix in('.c','.h','.py','.txt','.yml')and f.name not in('private_config.h',))or str(rel)=='main/Kconfig.projbuild' or (len(parts)==1 and f.name.endswith('-observations.csv'))
if not public:continue
if f.is_symlink():issues.append(str(rel)+': public symlink requires manual review');continue
data=f.read_bytes();text=data.decode('utf8',errors='replace')
if re.search(r'(?m)^\s*"?-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----',text):issues.append(str(rel)+': private PEM')
# Deliberately conservative literal rules, placeholders/examples/compiled configuration not globally considered secret.
patterns=[r'(?im)^\s*#define\s+(?:R07_POP|R07_AP_PASSWORD)\s+"([^"\n]+)"',r'(?im)^\s*(?:static\s+)?(?:const\s+)?char\s*\*\s*(?:api_token|wifi_password|wifi_ssid)\s*=\s*"([^"\n]+)"']
for pattern in patterns:
for m in re.finditer(pattern,text):
value=m[1]
if not any(x in value for x in('REPLACE_','COMPILE_ONLY','EXAMPLE_')):issues.append(str(rel)+': literal credential assignment (value omitted)')
rows.append((f,str(rel),data))
if issues:print(json.dumps({'status':'REJECT','issues':issues}));raise SystemExit(2)
out.mkdir(parents=True,exist_ok=False)
manifest=[]
for src,rel,data in rows:
dest=out/rel;dest.parent.mkdir(parents=True,exist_ok=True);dest.write_bytes(data);manifest.append({'path':rel,'bytes':len(data),'sha256':hashlib.sha256(data).hexdigest()})
(out/'share-manifest.json').write_text(json.dumps({'scope':'Explicit allowlist/literal scanner only. Does not prove absence of arbitrary, encoded, binary or previously committed secrets. No sdkconfig/private header/build/ELF/log/vault exported.','files':manifest},indent=2)+'\n');print('PUBLIC_EXPORT_OK',len(rows),'files; manually review git history/logs/attachments separately')
参考:[4] 固定Security1服务端实现[5] 官方原生客户端入口[6] 官方Python Security1客户端
从实际函数提取故障夹具,保留方法范围
同源token/长按输入通过;resume_body.py从实际main提取完整resume_clear,对外部存储调用失败前已改变模型的情况逐项重试,九场景通过,不假设错误后回滚。它不是实体Flash掉电模型。
实际官方客户端经真实本机HTTP与合成服务端验证令牌先成功再Set/Apply、token拒绝就不WiFi、错PoP不达端点三场景。share_fixture真实运行生成/导出、字面凭据/PEM/符号链接拒绝及已有vault拒绝六用例;原样credentials.c在头缺失/主机公开canary存在/移除三个profile全新编译执行,未宣称IDF增量自动发现新头;默认ELF另核配网与清除符号,仍未运行ESP32无线服务端。
# POSIX shell · 固定主机依赖;每次使用新证据目录
python tests/resume_body.py --out /tmp/r07-resume-proof
python tests/security1_loopback.py
python tests/share_fixture.py --out /tmp/r07-share-proof参考:[1] 配网管理器:自定义端点先于WiFi、create/register顺序[2] 管理器停止、重试和存储路径[4] 固定Security1服务端实现[5] 官方原生客户端入口[6] 官方Python Security1客户端[7] NVS set/commit与错误返回
实板记录留空,分享之前也复核历史
provisioning-observations.csv只有表头;等待真实AP/错误凭据/重启持久键/释放长按/中断清除与重新配网记录。错误先保存不含凭据的步骤、码与构建SHA,检查设备对应PoP、300s期限和NVS状态;不为重试自动清所有数据。
公开项目保留空白配置与恢复步骤,个人值和构建产物单独保管。需要签名、Flash加密与生命周期约束时,再进入后续R08实验;这不改变本篇硬编码可被提取的结论。
参考:[1] 配网管理器:自定义端点先于WiFi、create/register顺序[2] 管理器停止、重试和存储路径[7] NVS set/commit与错误返回[8] esp_wifi_restore恢复SDK持久设置范围
资料来源与版本说明
保留原计划ID、slug、M5阶段和用途。固定来源鲜取;编译、主机服务/逻辑、QEMU与实体结果分别记录。实际实体观察表仍空。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。