一条曲线背后,至少有四个不同的成功
室内环境站不能只显示一个温度数字。本例把“传感器读到了有效值”“写入本地记录”“网络接收机提交了记录”和“UTC可信到什么程度”分别表示。这样断网时还能保存原始行,卡失效时也不会把屏幕数字误认为已经记录。
系统持续供电,每60秒计划一次SHT40读取,OLED/串口显示资格状态,SD保存有身份和CRC的CSV;接收端SQLite提交后返回应用ACK。它与深睡攒批节点用途不同:这里优先连续记录、外部介质恢复和24小时观察。本轮实际完成构建/同源主机/本机服务夹具,指定板、真实卡和24小时现场表均为空。
参考:[3] Sensirion驱动1.1.0固定提交与BSD许可[6] IDF5.5.5 FAT VFS挂载和format_if_mount_failed[7] IDF5.5.5系统时间与SNTP[9] IDF5.5.5 NVS blob与commit语义
先固定两个I²C模块和一块带转换的SD板
板卡限定官方ESP32-S3-DevKitC-1 v1.1/WROOM-1-N8R8,桥USB为唯一电源。SHT40 PID4885 VIN/GND接3V3/GND、SDA8/SCL9、0x44,3V是输出留空。OLED限定PID326 128×64的2020+STEMMA QT/自动复位版,同总线100kHz、默认0x3D、VIN3V3,RST留空;旧复位版不在范围。
指定PID254是带稳压和电平转换的microSD模块,5V输入来自J1-21;CLK12、DI11、DO13、CS10,SPI10MHz,GND共地。只用FAT32的4–32GB SDHC卡,不拿裸SD卡、exFAT或另一块转换板套接。所有接线、插卡与更换介质都在断电时完成。
OLED用IDF内置SSD1306的1bpp页缓冲,原创3×5 ASCII字形,不依赖中文字体。初始化/事务失败只报显示故障,传感器/SD不跟着伪造失败;真实像素、镜像方向和供电下陷仍待实板。
| 主控端 | 模块端 | 注意 |
|---|---|---|
| 3V3 / GND | PID4885 VIN/GND;PID326 VIN/GND | PID4885 3V不接输入 |
| GPIO8/J1-12、GPIO9/J1-15 | 两模块SDA、SCL | 100kHz,0x44与0x3D |
| GPIO12/J1-18、GPIO11/J1-17 | SD CLK、DI | MOSI是主控→卡 |
| GPIO13/J1-19、GPIO10/J1-16 | SD DO、CS | DO是卡→主控 |
| 5V/J1-21、GND/J1-22 | PID254 5V、GND | 桥USB供电,不并外部5V |
参考:[1] 官方v1.1板卡:引脚、模块与单电源入口[2] SHT40 PID4885供电与引脚[4] Adafruit PID326 2020复位与默认I2C地址[5] Adafruit PID254 microSD供电、电平与SPI接线[8] IDF5.5.5 SSD1306驱动页缓冲/镜像/初始化
解压整个工程,改本机配置再构建
下载包包含main、原样Sensirion驱动、CMake/组件锁、分区、Python接收/修复工具和README。必须解压整个目录,不能只复制main.c。固定IDF5.5.5、esp32s3、8MB flash/OPI PSRAM,factory分区3MiB且没有OTA;本例用桥USB恢复固件。
menuconfig→Demo configuration填真实2.4GHz SSID/密码和隔离LAN的MQTT URI;默认192.0.2.10只是文档地址。另选可达NTP源。凭据只保存在本机sdkconfig。构建工具输出DIO启动头与QIO配置分别留证,不能据此手改成另一个未验证参数。NVS初始化失败只报错,不自动erase。
. /PATH/TO/esp-idf-v5.5.5/export.sh
idf.py --version
idf.py set-target esp32s3
idf.py menuconfig
idf.py build
idf.py -p /dev/cu.REPLACE_WITH_BRIDGE flash monitor- 做到这里,你应该看到
- 串口有S10 START与SCAN/STATUS;看到SD或MQTT异常先按对应状态排查,不能把build成功当作板已运行。
参考:[1] 官方v1.1板卡:引脚、模块与单电源入口[3] Sensirion驱动1.1.0固定提交与BSD许可[6] IDF5.5.5 FAT VFS挂载和format_if_mount_failed
CSV的时间和有效位,不靠看起来合理的数字
每行含version、journal、seq、uptime_ms、unix_s、time_valid、sample_valid、t_milli、rh_raw_milli和CRC32。journal为随机64位,seq连续从1开始,CRC为IEEE且覆盖最后逗号前的字节;身份不是密码学全球唯一保证。原始RH保留,OLED才夹到0–100%;无效采样写valid=0、两个数值为0,不偷用上一有效值。
只有本次启动已收到SNTP、UTC在2025–2099、且距最后同步不超过24小时才time_valid=1;否则unix_s=0。OLED的FRESH<=1小时/HOLDOVER<=24小时是应用策略,不承诺时钟精度。校时可能跳变,按journal+seq排序,uptime只在本次启动内单调;断网旧行不回填猜测的UTC。
采样下一期限是当前时间加60秒,I²C/SD和调度可能延迟;不是硬实时60秒。读取失败仍生成带无效位的行;缺卡/写失败则没有持久行,只增加本次启动的RAM LOST计数。
| 现象 | 记录/显示 | 不能推断 |
|---|---|---|
| SNTP未成功/超24h | time_valid=0 / unix_s=0 / UNKNOWN | 不能把系统默认日历当UTC |
| CRC/读取失败 | sample_valid=0 / SENSOR INVALID | 0不是实际温湿度 |
| 卡未挂载/写入停止 | NOT_RECORDED / LOST RAM | 屏幕有读数不代表已保存 |
| MQTT离线 | CSV继续、ACK不前进 | QoS1不替代应用提交证明 |
参考:[3] Sensirion驱动1.1.0固定提交与BSD许可[7] IDF5.5.5系统时间与SNTP[12] IDF5.5.5 64位单调计时
先提交数据库,再推进设备游标
设备把原CSV行发布到hixinwu/s10/<12hexMAC>/row,QoS1且retain=false,一个in-flight行,每10秒重试。接收机用Mosquitto2.0.20的-F %j接收转义JSON,检查topic、payloadlen、完整规范行、CRC和retain标记;SQLite事务COMMIT后才发/ack,内容journal,seq。设备只接完整非retained、当前journal和下一顺序的ACK。
主机主键(device,journal,seq)同内容重复只ACK,不重复插入;同身份不同内容拒绝并不ACK。断网、丢ACK或重启都可能重发;broker PUBACK或enqueue成功不等于接收机数据库已提交。SQLite FULL/WAL依赖真实磁盘与电源,软件顺序也不是介质抗掉电保证。
ACK每16条写有CRC的28字节NVS检查点。掉电可能重放最近行;检查点写失败报CP ERR,RAM仍前进,重启可能重放更多。若NVS点超过卡上有效末尾则停读写,要求人工对照服务器,不能偷偷倒退/重用序号。
参考:[9] IDF5.5.5 NVS blob与commit语义[10] Mosquitto2.0.20 %j转义JSON与payloadlen[11] Mosquitto2.0.20订阅输出格式和retain语义[12] IDF5.5.5 64位单调计时
先跑本机服务夹具,再接隔离LAN
主机依赖Python3.12标准库和Mosquitto2.0.20;完整命令在README。broker-loopback.conf只绑定127.0.0.1:18889,不能供ESP32连接。真板实验另复制LAN配置,改实际私有网卡IP、固件URI、receiver地址并限制DUT/接收机来源;默认无认证/TLS,只用于隔离可信台架,不能暴露公网。
receiver.py先提交再ACK,导出使用独占新文件。retained历史投递会被拒绝,但实时retained发布可能由broker转为retain=false,不能把此检查当授权安全策略。没有离线未落盘样本的无限缓存。接收端唯一行与SD行的身份/CRC相符,才算这一条的应用送达证据。
mosquitto -c broker-loopback.conf
python3.12 host/receiver.py receive --db ./receiver.sqlite --host 127.0.0.1 --port 18889 --client-dir /PATH/TO/CLIENTS
python3.12 host/receiver.py export --db ./receiver.sqlite --output /NEW/receiver-export.csv参考:[10] Mosquitto2.0.20 %j转义JSON与payloadlen[11] Mosquitto2.0.20订阅输出格式和retain语义
卡满、坏尾与重启,恢复时保留原件
应用上限512KiB与真实ENOSPC/卡满分开。扫描只接受完整CRC、连续序号、同journal的前缀;坏尾停写。write/fflush/fsync/fclose全部API成功才推进应用序号,失败可能留下半行,也可能留下整行。不会自动格式化、删尾或覆盖旧记录。
先停机断电,备份原卡/CSV与接收端一致的SQLite/WAL/SHM;repair_csv.py只检查备份,指定--output则独占创建一个验证前缀的新副本,原始文件不改。若卡有效末尾比已确认ACK短,先用服务器备份对照,不删NVS掩盖问题。容量到顶时确认全部行已送达并导出,归档原room.csv后人工换空卡/移走原文件开始新journal;旧数据库继续保存。
缺卡或写失败仍可看当前传感器/串口,但LOST只有本次RAM计数,复位后消失。不能热拔卡;重新插卡须停电、检查、重新启动。新行真正追加且ACK增长后才恢复记录资格;不把数据库导出含device列的CSV直接放回设备。
python3.12 host/repair_csv.py /BACKUP/room.csv
python3.12 host/repair_csv.py /BACKUP/room.csv --output /NEW/PREFIX-room.csv参考:[6] IDF5.5.5 FAT VFS挂载和format_if_mount_failed[9] IDF5.5.5 NVS blob与commit语义
1440合成行通过,24小时现场仍要按表做
本轮真实IDF编译Exit0;生产C模型/日志层用ASan+UBSan测试CRC、时间过期、顺序ACK、检查点、1440合成行、重扫/seek、应用容量、ENOSPC/短写/flush/sync/close注入。实际本机Mosquitto+SQLite测试丢首ACK重发、receiver重启、冲突/NUL/坏CRC/retained历史拒绝;坏尾副本工具也验证不改原件且拒绝覆盖。这些不是芯片、SD、RF或24小时运行。
24小时空表在下载包:记录供电/卡/版本→正常一小时→断实验AP30分钟但保持供电→恢复核同journal/seq补传和接收端唯一行→计划RESET看重扫/NVS→只在备份做坏尾/应用满故障→安全恢复→满24小时比较CSV、唯一行、UTC有效率和丢失。真实卡满/断电介质实验另记,不能被主机注入替代。失败与恢复原日志保留,所有物理列pending。
/* SPDX-License-Identifier: MIT */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <inttypes.h>
#include <errno.h>
#include <time.h>
#include <stdatomic.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/event_groups.h"
#include "driver/spi_common.h"
#include "driver/sdspi_host.h"
#include "sdmmc_cmd.h"
#include "esp_vfs_fat.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_random.h"
#include "esp_mac.h"
#include "esp_netif_sntp.h"
#include "mqtt_client.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "sdkconfig.h"
#include "net.h"
#include "sensirion_i2c_hal.h"
#include "sensirion_hal_idf.h"
#include "sht4x_i2c.h"
#include "station_model.h"
#include "station_journal.h"
#include "station_display.h"
static const char *TAG="S10",*PATH="/sd/room.csv";
static QueueHandle_t acks;static EventGroupHandle_t mqtt_flags;
static char row_topic[64],ack_topic[64],availability_topic[64],client_id[32];static int subscription_id;
static atomic_bool time_synced=false;
static atomic_uint_fast64_t last_sync_ms=0;
static station_scan_t scan;static station_pump_t pump;
static bool storage_ready,write_stopped,read_stopped,nvs_ready,checkpoint_fault;
static long next_offset=-1,pending_end=-1;static char pending_line[STATION_LINE_MAX];
static uint64_t lost_samples=0;static unsigned uncheckpointed=0;
typedef struct{uint64_t journal,sequence;} app_ack_t;
static void sync_notice(struct timeval *time){(void)time;atomic_store(&last_sync_ms,(uint64_t)esp_timer_get_time()/1000ULL);atomic_store(&time_synced,true);}
static esp_err_t mount_sd(void){
sdmmc_host_t host=SDSPI_HOST_DEFAULT();host.max_freq_khz=10000;
spi_bus_config_t bus={.mosi_io_num=11,.miso_io_num=13,.sclk_io_num=12,.quadwp_io_num=-1,.quadhd_io_num=-1,.max_transfer_sz=4096};
esp_err_t e=spi_bus_initialize(host.slot,&bus,SDSPI_DEFAULT_DMA);if(e!=ESP_OK)return e;
sdspi_device_config_t dev=SDSPI_DEVICE_CONFIG_DEFAULT();dev.gpio_cs=10;dev.host_id=host.slot;
esp_vfs_fat_sdmmc_mount_config_t cfg={.format_if_mount_failed=false,.max_files=4,.allocation_unit_size=16*1024};
sdmmc_card_t *card=NULL;e=esp_vfs_fat_sdspi_mount("/sd",&host,&dev,&cfg,&card);if(e!=ESP_OK)spi_bus_free(host.slot);return e;
}
static void load_checkpoint(void){
uint64_t journal=0,sequence=0;nvs_handle_t h;if(!nvs_ready||nvs_open("room_station",NVS_READONLY,&h)!=ESP_OK)return;
uint8_t bytes[28];size_t size=sizeof(bytes);esp_err_t e=nvs_get_blob(h,"ack",bytes,&size);nvs_close(h);
if(e!=ESP_OK||!station_checkpoint_decode(bytes,size,&journal,&sequence)||journal!=pump.journal)return;
if(sequence>scan.last_sequence){read_stopped=true;write_stopped=true;ESP_LOGE(TAG,"CHECKPOINT_AHEAD: possible lost media tail; preserve card and database; no silent reset");return;}
pump.acked=sequence;
}
static void checkpoint(void){
if(!nvs_ready){checkpoint_fault=true;return;}uint8_t bytes[28];station_checkpoint_encode(pump.journal,pump.acked,bytes);
nvs_handle_t h;esp_err_t e=nvs_open("room_station",NVS_READWRITE,&h);
if(e==ESP_OK){e=nvs_set_blob(h,"ack",bytes,sizeof(bytes));if(e==ESP_OK)e=nvs_commit(h);nvs_close(h);}
checkpoint_fault=e!=ESP_OK;if(!checkpoint_fault)uncheckpointed=0;
ESP_LOGI(TAG,"CHECKPOINT ack=%"PRIu64" result=%s; a failed checkpoint may replay duplicates",pump.acked,esp_err_to_name(e));
}
static void inspect_media(void){
FILE *file=fopen(PATH,"r");station_result_t result=STATION_OK;
if(!file){if(errno!=ENOENT)result=STATION_IO_ERROR;}
else{result=station_scan(file,&scan);if(fclose(file)!=0)result=STATION_IO_ERROR;}
write_stopped=result!=STATION_OK;read_stopped=result==STATION_IO_ERROR;
pump.journal=scan.journal;load_checkpoint();
if(scan.good_bytes)next_offset=station_offset_after(PATH,pump.acked);
ESP_LOGI(TAG,"SCAN result=%d validated_prefix_bytes=%u last_seq=%"PRIu64" ack=%"PRIu64"; no automatic truncate/format",result,(unsigned)scan.good_bytes,scan.last_sequence,pump.acked);
}
static void mqtt_event(void *arg,esp_event_base_t base,int32_t id,void *data){
(void)arg;(void)base;esp_mqtt_event_handle_t e=data;
if(id==MQTT_EVENT_CONNECTED){subscription_id=esp_mqtt_client_subscribe(e->client,ack_topic,1);ESP_LOGI(TAG,"MQTT connected; waiting for ACK topic SUBACK");}
else if(id==MQTT_EVENT_SUBSCRIBED&&e->msg_id==subscription_id){xEventGroupSetBits(mqtt_flags,BIT0);esp_mqtt_client_publish(e->client,availability_topic,"online",0,1,1);}
else if(id==MQTT_EVENT_DISCONNECTED){xEventGroupClearBits(mqtt_flags,BIT0);}
else if(id==MQTT_EVENT_DATA&&station_ack_frame(e->topic,e->topic_len,ack_topic,e->current_data_offset,e->total_data_len,e->data_len,e->retain)){
app_ack_t ack;if(station_ack_decode(e->data,e->data_len,&ack.journal,&ack.sequence)&&xQueueSend(acks,&ack,0)!=pdTRUE)ESP_LOGW(TAG,"ACK queue full: retry/dedup will recover; no cursor advanced");
}
}
static void pump_rows(esp_mqtt_client_handle_t client,uint64_t now){
app_ack_t ack;
while(xQueueReceive(acks,&ack,0)==pdTRUE)if(station_pump_ack(&pump,ack.journal,ack.sequence)){
next_offset=pending_end;pending_line[0]=0;if(++uncheckpointed>=16&&!checkpoint_fault)checkpoint();
}
if(!storage_ready||read_stopped||pump.acked>=scan.last_sequence||!(xEventGroupGetBits(mqtt_flags)&BIT0)||!station_pump_due(&pump,now))return;
if(!pump.waiting){
if(next_offset<0)next_offset=station_offset_after(PATH,pump.acked);
if(next_offset<0||!station_next(PATH,next_offset,pending_line,&pending_end)){read_stopped=true;ESP_LOGE(TAG,"BACKFILL_READ: preserve card; no cursor advanced");return;}
station_record_t row;if(!station_record_decode(pending_line,&row)||row.journal!=pump.journal||row.sequence!=pump.acked+1){read_stopped=true;return;}
}
station_pump_attempt(&pump,pump.acked+1,now);
int queued=esp_mqtt_client_enqueue(client,row_topic,pending_line,0,1,0,true);
ESP_LOGI(TAG,"BACKFILL seq=%"PRIu64" queued=%d; broker QoS1 is not receiver database ACK",pump.inflight,queued);
}
void app_main(void){
esp_err_t e=nvs_flash_init();nvs_ready=e==ESP_OK;if(!nvs_ready)ESP_LOGW(TAG,"NVS init=%s; no automatic erase",esp_err_to_name(e));
e=mount_sd();storage_ready=e==ESP_OK;if(storage_ready)inspect_media();else ESP_LOGE(TAG,"SD mount=%s; sample/display continue without durable rows",esp_err_to_name(e));
sensirion_i2c_hal_init();sht4x_init(0x44);bool screen=station_display_init();
if(!screen)ESP_LOGW(TAG,"OLED init path failed; serial/SD continue, no pixel claim");
uint8_t mac[6];ESP_ERROR_CHECK(esp_read_mac(mac,ESP_MAC_WIFI_STA));char device[13];snprintf(device,sizeof(device),"%02x%02x%02x%02x%02x%02x",mac[0],mac[1],mac[2],mac[3],mac[4],mac[5]);
snprintf(client_id,sizeof(client_id),"hixinwu-s10-%s",device);snprintf(row_topic,sizeof(row_topic),"hixinwu/s10/%s/row",device);snprintf(ack_topic,sizeof(ack_topic),"hixinwu/s10/%s/ack",device);snprintf(availability_topic,sizeof(availability_topic),"hixinwu/s10/%s/availability",device);
acks=xQueueCreate(8,sizeof(app_ack_t));mqtt_flags=xEventGroupCreate();if(!acks||!mqtt_flags){ESP_LOGE(TAG,"queue/event allocation failed");return;}
net_start();
esp_mqtt_client_config_t cfg={.broker.address.uri=CONFIG_DEMO_BROKER_URL,.credentials.client_id=client_id,.session.last_will={.topic=availability_topic,.msg="offline",.qos=1,.retain=1},.network={.timeout_ms=3000,.reconnect_timeout_ms=3000},.outbox.limit=2048};
esp_mqtt_client_handle_t client=esp_mqtt_client_init(&cfg);if(!client){ESP_LOGE(TAG,"MQTT init failed");return;}ESP_ERROR_CHECK(esp_mqtt_client_register_event(client,ESP_EVENT_ANY_ID,mqtt_event,NULL));
bool mqtt_started=false,sntp_started=false,last_valid=false;uint64_t sample_at=0,display_at=0,last_good=0;int32_t last_t=0,last_rh=0;unsigned failures=0;
ESP_LOGI(TAG,"S10 START device=%s; SD512KiB cap, sample60s, full64 time; no physical/24h run claimed",device);
for(;;){
uint64_t now=(uint64_t)esp_timer_get_time()/1000ULL;
if(net_online()&&!mqtt_started){e=esp_mqtt_client_start(client);mqtt_started=e==ESP_OK;}
if(net_online()&&!sntp_started){esp_sntp_config_t time_cfg=ESP_NETIF_SNTP_DEFAULT_CONFIG(CONFIG_STATION_NTP_SERVER);time_cfg.sync_cb=sync_notice;time_cfg.wait_for_sync=false;e=esp_netif_sntp_init(&time_cfg);sntp_started=e==ESP_OK;}
if(now>=sample_at){
sample_at=now+60000;int32_t t=0,rh=0;int16_t error=demo_i2c_ready()?sht4x_measure_high_precision(&t,&rh):-1;last_valid=error==0;
if(last_valid){last_t=t;last_rh=rh;last_good=now;failures=0;}else{if(++failures%3==0&&demo_i2c_ready())sht4x_soft_reset();ESP_LOGW(TAG,"SHT40 error=%d; invalid row fields, no old value as current",error);}
station_record_t row={.journal=pump.journal,.sequence=scan.last_sequence+1,.uptime=(uint64_t)esp_timer_get_time()/1000ULL,.unix_s=(int64_t)time(NULL),.sample_valid=last_valid,.t_milli=last_valid?t:0,.rh_milli=last_valid?rh:0};
row.time_valid=station_clock_recent(atomic_load(&time_synced),atomic_load(&last_sync_ms),row.uptime,row.unix_s);if(!row.time_valid)row.unix_s=0;
if(!row.journal){row.journal=((uint64_t)esp_random()<<32)|esp_random();if(!row.journal)row.journal=1;}
char line[STATION_LINE_MAX];station_result_t result=STATION_IO_ERROR;
if(storage_ready&&!write_stopped&&scan.last_sequence<UINT64_MAX&&station_record_encode(&row,line,sizeof(line)))result=station_append(PATH,&scan,line,&station_default_io);
if(result==STATION_OK){if(!scan.good_bytes)scan.good_bytes=strlen(STATION_CSV_HEADER);scan.good_bytes+=strlen(line);scan.last_sequence=row.sequence;scan.journal=pump.journal=row.journal;ESP_LOGI(TAG,"APPEND_API_SUCCESS %s",line);}
else{++lost_samples;if(storage_ready)write_stopped=true;ESP_LOGW(TAG,"NOT_RECORDED result=%d lost_this_boot=%"PRIu64"; cap and true I/O errors are separate",result,lost_samples);}
}
if(mqtt_started)pump_rows(client,(uint64_t)esp_timer_get_time()/1000ULL);
now=(uint64_t)esp_timer_get_time()/1000ULL;
if(now>=display_at){
display_at=now+1000;char text[8][33];const char *lines[8];for(unsigned i=0;i<8;i++)lines[i]=text[i];
snprintf(text[0],33,"S10 ROOM STATION");
if(last_valid&&now>=last_good&&now-last_good<=70000)snprintf(text[1],33,"T %.1f H %.1f",last_t/1000.0,(last_rh<0?0:last_rh>100000?100000:last_rh)/1000.0);else snprintf(text[1],33,"SENSOR INVALID");
time_t utc=time(NULL);uint64_t sync_at=atomic_load(&last_sync_ms);bool clock_ok=station_clock_recent(atomic_load(&time_synced),sync_at,now,utc);snprintf(text[2],33,"UTC %s",clock_ok?(now-sync_at<=3600000ULL?"FRESH":"HOLDOVER"):"UNKNOWN");
snprintf(text[3],33,"SD %s S %"PRIu64,!storage_ready?"MISSING":write_stopped?"STOP":"OK",scan.last_sequence);
snprintf(text[4],33,"MQTT %s",xEventGroupGetBits(mqtt_flags)&BIT0?"ACK READY":"OFFLINE");
snprintf(text[5],33,"ACK %"PRIu64,pump.acked);snprintf(text[6],33,"LOST %"PRIu64" RAM",lost_samples);snprintf(text[7],33,"CP %s READ %s",checkpoint_fault?"ERR":"OK",read_stopped?"STOP":"OK");
if(screen&&!station_display_lines(lines))ESP_LOGW(TAG,"OLED transaction failed; pixel state unverified");
ESP_LOGI(TAG,"STATUS seq=%"PRIu64" ack=%"PRIu64" clock=%s SD=%u write_stop=%u read_stop=%u lost=%"PRIu64,scan.last_sequence,pump.acked,text[2],storage_ready,write_stopped,read_stopped,lost_samples);
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
- 做到这里,你应该看到
- 分别给出源/构建/主机与实板证据,不把合成1440行改称24小时通过。
参考:[3] Sensirion驱动1.1.0固定提交与BSD许可[6] IDF5.5.5 FAT VFS挂载和format_if_mount_failed[7] IDF5.5.5系统时间与SNTP[9] IDF5.5.5 NVS blob与commit语义[10] Mosquitto2.0.20 %j转义JSON与payloadlen[12] IDF5.5.5 64位单调计时
资料来源与版本说明
已实际阅读文末固定版本的关键实现与指定厂商资料,来源/构建/主机夹具分别留证。具体板卡、固件和依赖范围见版本记录;指定实板、HA与现场结果尚未执行。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。
- 官方v1.1板卡:引脚、模块与单电源入口
- SHT40 PID4885供电与引脚
- Sensirion驱动1.1.0固定提交与BSD许可
- Adafruit PID326 2020复位与默认I2C地址
- Adafruit PID254 microSD供电、电平与SPI接线
- IDF5.5.5 FAT VFS挂载和format_if_mount_failed
- IDF5.5.5系统时间与SNTP
- IDF5.5.5 SSD1306驱动页缓冲/镜像/初始化
- IDF5.5.5 NVS blob与commit语义
- Mosquitto2.0.20 %j转义JSON与payloadlen
- Mosquitto2.0.20订阅输出格式和retain语义
- IDF5.5.5 64位单调计时