传感器与可信数据 / 数据质量发布 内容更新

传感器偶尔跳值怎么办?用范围、迟滞和时间窗口处理

在固定SHT40例程里同时保留原始值、质量、趋势、年龄与告警;注入NaN、通信错、停采样和突变,避免用平滑曲线掩盖断线。

读完能做什么
得到质量和年龄可见的数据流:无效时趋势立即不可用,真实可能的突变保留并标记。
开始之前
先完成前置文章的实际基础步骤;下载包README含独立安装、配置、构建与恢复命令。
所需器材
指定S3板与USB数据线、Adafruit SHT40 PID4885、短杜邦线、固定Arduino与Sensirion库
版本组合
框架:Arduino-ESP32 3.3.12依赖:Sensirion I2C SHT4x1.1.2 / Core0.7.3
适用范围

官方DevKitC-1 v1.1 N8R8 + Adafruit SHT40 PID4885;Arduino3.3.12、SHT4x1.1.2/Core0.7.3;GPIO8/9、100kHz、0x44、3.3V。演示数据质量,不提供环境校准或安全控制。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/network-data.md#s08;固定上游章节与来源摘要。验证日期:
构建 / 配置已通过
docs/esp32-evidence/logs/s08-build.log;真实Exit0,构建输入/日志/固件SHA见network-data-builds.json。对象:ESP32-S3-DevKitC-1 v1.1 / WROOM-1-N8R8版本:CLI1.5.1 / Arduino3.3.12 / SHT4x1.1.2 / Core0.7.3验证日期:
仿真不适用
未执行ESP32或器件仿真;同源主机输入检查单独留证,不代表网络、I2C、调度或电气测试。
本站实板待验证
本站无这套固件烧录/串口/实体网络或传感器运行记录,observations.csv实测字段为空。

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

01 / 数据质量

先别滤波,先定义哪些数据能用

扫描应答、CRC正确、数值在可用范围、当前仍新鲜是四个不同判断。一个低噪声均值可能由断线前旧点构成,曲线好看并不表示传感器还在工作。本次同时输出raw_T/raw_RH、quality、usable、trend、count、alarm和两种年龄。

已有固定版本真实构建和共享模型的主机输入检查;本站尚无该固件烧录、SHT40读数、板上故障fixture或断线恢复记录。物理测量精度也未验证,下面过程是可执行的验收步骤。

参考:[1] SHT4x Datasheet V6.4:温度工作范围、CRC、命令、换算与RH裁边[2] Sensirion SHT4x1.1.2:measureHighPrecision与返回码[3] Sensirion Core0.7.3 receiveFrame:数据长度和每字CRC

02 / 数据质量

沿用S01的具体模块与四线接法

指定Adafruit PID4885带稳压、电平转换和上拉,VIN接主控3V3,GND共地,GPIO8/SDA、GPIO9/SCL,3Vo输出端留空。模块0x44不可在软件随意改成0x45;不能把裸芯片供电范围套到这块模块上。

完全断电后逐端追线,GPIO是信号号而非排针序号。用短线、单模块和100kHz;模块已有10k上拉,初次不再并联电阻。只从板USB-to-UART供电,日志115200。

主控端模块端核对
3V3 / J1-1VIN本例3.3V,不接5V
GND / J1-22GND共地
GPIO8 / J1-12SDA显式指定
GPIO9 / J1-15SCL100kHz
不接3Vo输出端留空

参考:[4] Adafruit PID4885模块引脚、固定0x44与上拉[6] 官方S3 DevKitC-1 v1.1:排针与模组[7] 官方DevKitC-1 v1.1固定原理图:J1排针与GPIO端点

03 / 数据质量

先编译固定驱动,再连接传感器

使用Arduino-ESP32 3.3.12、Sensirion Core0.7.3和SHT4x1.1.2,sketch.yaml固定同一组合。先安装Core再用--no-deps安装驱动,避免公共依赖静默变版;完整core安装、上传和串口命令在README。

驱动measureHighPrecision发0xFD、等待10ms后读六字节;Core receiveFrame检查每个16位数据字的CRC,长度或CRC失败返回错误。Wire.setTimeOut(20)约束I2C操作等待,不保证整个loop总共≤20ms。应用先检查error,不能拿输出变量当成功。

代码 / 配置
arduino-cli lib install 'Sensirion Core@0.7.3'
arduino-cli lib install --no-deps 'Sensirion I2C SHT4x@1.1.2'
arduino-cli compile --fqbn 'esp32:esp32:esp32s3:FlashSize=8M,PSRAM=opi,FlashMode=qio,PartitionScheme=default_8MB,CDCOnBoot=default,USBMode=hwcdc' .

参考:[2] Sensirion SHT4x1.1.2:measureHighPrecision与返回码[3] Sensirion Core0.7.3 receiveFrame:数据长度和每字CRC[5] Arduino-ESP32 3.3.12 Wire:setTimeOut与requestFrom

04 / 数据质量

把规则写成固件与主机共用的模型

先检查传输错误、NaN/Infinity,再检查应用可用范围。温度采用工作范围[-40,125]℃,原始RH保留完整换算范围[-6,119];温度换算本身可达[-45,130],所以范围不合是本应用策略,不是证明器件坏了。RH展示/趋势裁到0–100,同时保留raw并标CLIPPED_RH。

每次接收新样本时,以该输入时刻裁掉超过6500ms的旧点,最多保留4点求均值,只用于趋势;STATUS时刻不重新裁窗或重算均值,tick另行管理STALE。相邻可用点≤3000ms且变化>5℃或>20%RH,标STEP_CHANGE并接受新点、重置窗口。演示阈值不能证明变化是假;恰好5℃/20%RH不触发。STEP_CHANGE优先于裁边标签,原始RH仍可识别裁边。

任何无效输入立即usable=0、trend=nan、count=0、alarm=UNKNOWN;超过4500ms没有输入则STALE,恢复第一个好点另起窗口。迟滞使用验证后的原始温度:>=30置ON,<=28置OFF,中间保持;坏数据后在29恢复仍UNKNOWN,直到抵达明确边界。

代码 / 配置
// SPDX-License-Identifier: MIT
#pragma once
#include <stdint.h>
#include <math.h>
namespace quality {
enum class Flag { NO_DATA, VALID, CLIPPED_RH, STEP_CHANGE, TRANSPORT_ERROR, NON_FINITE, OUT_OF_RANGE, STALE };
enum class Alarm { UNKNOWN, OFF, ON };
inline const char* name(Flag q){switch(q){case Flag::NO_DATA:return "NO_DATA";case Flag::VALID:return "VALID";case Flag::CLIPPED_RH:return "CLIPPED_RH";case Flag::STEP_CHANGE:return "STEP_CHANGE";case Flag::TRANSPORT_ERROR:return "TRANSPORT_ERROR";case Flag::NON_FINITE:return "NON_FINITE";case Flag::OUT_OF_RANGE:return "OUT_OF_RANGE";case Flag::STALE:return "STALE";}return "?";}
inline const char* name(Alarm a){return a==Alarm::ON?"ON":a==Alarm::OFF?"OFF":"UNKNOWN";}
struct Point {uint32_t at;float t,rh;};
struct Model {
 static constexpr uint32_t STALE_MS=4500, WINDOW_MS=6500;
 Point window[4]{};uint8_t count=0;Flag flag=Flag::NO_DATA;Alarm alarm=Alarm::UNKNOWN;
 bool good=false,seen=false,usable=false;uint32_t last_good=0,last_seen=0;float raw_t=NAN,raw_rh=NAN,filtered_t=NAN,filtered_rh=NAN,previous_t=NAN,previous_rh=NAN;
 void invalidate(Flag f){flag=f;usable=false;count=0;filtered_t=filtered_rh=NAN;alarm=Alarm::UNKNOWN;}
 void tick(uint32_t now){if(seen && uint32_t(now-last_seen)>STALE_MS && flag!=Flag::STALE)invalidate(Flag::STALE);}
 int64_t age(uint32_t now)const{return good?int64_t(uint32_t(now-last_good)):-1;}
 void accept(uint32_t now,float t,float rh,int transport){
  // If a caller skipped tick(), enforce the same stale gap before considering recovery.
  tick(now);bool previous_usable=usable;uint32_t previous_at=last_good;
  seen=true;last_seen=now;raw_t=t;raw_rh=rh;
  if(transport){invalidate(Flag::TRANSPORT_ERROR);return;}
  if(!isfinite(t)||!isfinite(rh)){invalidate(Flag::NON_FINITE);return;}
  // SHT4x conversion supports raw RH -6..119; clamp only the displayed RH.
  if(t < -40 || t > 125 || rh < -6 || rh > 119){invalidate(Flag::OUT_OF_RANGE);return;}
  bool step=previous_usable && uint32_t(now-previous_at)<=3000 && (fabsf(t-previous_t)>5 || fabsf(rh-previous_rh)>20);
  if(step)count=0; // preserve the real new value; do not silently discard a possible true change
  uint8_t keep=0;for(uint8_t i=0;i<count;i++)if(uint32_t(now-window[i].at)<=WINDOW_MS)window[keep++]=window[i];count=keep;
  if(count==4){for(uint8_t i=1;i<4;i++)window[i-1]=window[i];count=3;}
  float shown=fminf(100,fmaxf(0,rh));window[count++]={now,t,shown};float st=0,sr=0;for(uint8_t i=0;i<count;i++){st+=window[i].t;sr+=window[i].rh;}filtered_t=st/count;filtered_rh=sr/count;
  flag=step?Flag::STEP_CHANGE:(shown!=rh?Flag::CLIPPED_RH:Flag::VALID);usable=true;good=true;last_good=now;previous_t=t;previous_rh=rh;
  // Hysteresis uses validated RAW temperature, not the smoothed trend.
  if(t>=30)alarm=Alarm::ON;else if(t<=28)alarm=Alarm::OFF;
 }
};
}

参考:[1] SHT4x Datasheet V6.4:温度工作范围、CRC、命令、换算与RH裁边[2] Sensirion SHT4x1.1.2:measureHighPrecision与返回码[3] Sensirion Core0.7.3 receiveFrame:数据长度和每字CRC

05 / 数据质量

完整程序:真实与合成来源从日志就能分清

默认SENSOR每2000ms读取一次SHT40。f进入FIXTURE并清模型,此时不调用测量;r返回真实来源并再次清模型。每轮最多读8个串口字符,不让命令处理无限占用loop。

kind=SAMPLE才表示当轮采样输入及error;kind=STATUS不是新I2C数据,error=NA,raw仍是旧输入并由raw_age_ms说明。last_good_age_ms是最后好样本年龄;无好值=-1。nan表示不可用,不是0℃。uint32_t短差值支持millis回绕,假设主循环持续运行,不承诺跨一次完整49.7天不调用仍正确计龄。

代码 / 配置
// SPDX-License-Identifier: MIT
// S08: fixed SHT40 sensor path and explicitly labelled fault fixture.
#include <Arduino.h>
#include <Wire.h>
#include <SensirionI2cSht4x.h>
#include "quality_model.h"
SensirionI2cSht4x sensor;
quality::Model model;
constexpr uint8_t SDA_PIN=8,SCL_PIN=9;
constexpr uint32_t PERIOD_MS=2000;
bool busReady=false,fixture=false;uint32_t sampled=0,sequence=0,fixtureStep=0;
quality::Flag reported=quality::Flag::NO_DATA;
bool startBus(){bool ok=Wire.begin(SDA_PIN,SCL_PIN,100000);Wire.setTimeOut(20);sensor.begin(Wire,SHT40_I2C_ADDR_44);return ok;}
void report(uint32_t now,int error,bool acquired=true){
 char errorText[16]="NA";if(acquired)snprintf(errorText,sizeof errorText,"%d",error);
 Serial.printf("seq=%lu uptime_ms=%lu source=%s kind=%s error=%s raw_age_ms=%lld raw_T=%.2f raw_RH=%.2f quality=%s usable=%u trend_T=%.2f trend_RH=%.2f count=%u alarm=%s last_good_age_ms=%lld\n",
 (unsigned long)++sequence,(unsigned long)now,fixture?"FIXTURE":"SENSOR",acquired?"SAMPLE":"STATUS",errorText,model.seen?(long long)uint32_t(now-model.last_seen):-1LL,model.raw_t,model.raw_rh,quality::name(model.flag),model.usable,model.filtered_t,model.filtered_rh,model.count,quality::name(model.alarm),(long long)model.age(now));
 reported=model.flag;
}
void setup(){Serial.begin(115200);busReady=startBus();sampled=millis();Serial.println("S08 core=3.3.12 SHT4x=1.1.2 Core=0.7.3 SDA8 SCL9 0x44; f=fixture r=real; nan trend means unavailable");}
void loop(){
 for(unsigned n=0;n<8 && Serial.available();++n){int c=Serial.read();if(c=='f'||c=='r'){fixture=c=='f';model=quality::Model{};fixtureStep=0;sampled=millis();reported=quality::Flag::NO_DATA;Serial.printf("MODE_RESET source=%s; no retained filter or alarm\n",fixture?"FIXTURE":"SENSOR");}}
 uint32_t now=millis();model.tick(now);if(model.flag!=reported)report(now,0,false);
 if(uint32_t(now-sampled)>=PERIOD_MS){
  sampled=now;float t=NAN,rh=NAN;int16_t error=0;bool acquire=true;
  if(fixture){
   // 2 s slots. Four skipped slots exercise >4.5 s stale without inventing a sensor value.
   const float temps[]={22,22.2f,NAN,22,22,22,22,22,22,22,40,40,29,28,31};
   uint32_t k=fixtureStep++%15;rh=50;t=temps[k];if(k==3)error=-1;if(k>=5&&k<=8)acquire=false;
  }else{if(!busReady)busReady=startBus();if(busReady)error=sensor.measureHighPrecision(t,rh);else error=-1;}
  if(acquire){model.accept(now,t,rh,error);report(now,error);}
 }
 delay(1);
}

参考:[2] Sensirion SHT4x1.1.2:measureHighPrecision与返回码[3] Sensirion Core0.7.3 receiveFrame:数据长度和每字CRC[5] Arduino-ESP32 3.3.12 Wire:setTimeOut与requestFrom

06 / 数据质量

先注入NaN、通信错、停采样和突变

FIXTURE每两秒一个slot、15slot循环:22、22.2、NaN、通信错、22、跳过四轮采样、22恢复、40突变、40、29、28、31。跳过时loop继续tick,超过4.5秒应STALE;恢复22应count1,随后40应STEP_CHANGE且trend40,不能仍显示旧均值为有效。

所有合成行标source=FIXTURE,不能当真实传感器通信。回到SENSOR等有效数据后,若做断线实验先完全断电再断SDA、再上电观察;恢复接线也先断电。不要热拔供电线造成回灌。按实际uptime与raw/quality/age检查,不只盯曲线或slot号。

输入当前数据与趋势告警
NaN/通信错立即无效,清窗口,trend=nanUNKNOWN
>4500ms无输入STALE,保留旧raw但有raw_ageUNKNOWN
恢复29℃新窗口,不能猜旧告警UNKNOWN
突变40℃接受40,STEP_CHANGE,trend40ON
28℃/30℃边界按有效原始值判断OFF / ON

参考:[1] SHT4x Datasheet V6.4:温度工作范围、CRC、命令、换算与RH裁边[2] Sensirion SHT4x1.1.2:measureHighPrecision与返回码[3] Sensirion Core0.7.3 receiveFrame:数据长度和每字CRC

07 / 数据质量

用同源输入检查限定结论

host/host_check.cpp直接编译quality_model.h,实际通过12组检查:部分窗口、坏数/范围、迟滞、突变保留、stale边界与恢复、窗口容量/时间、无符号回绕等。它不是I2C/CRC实际注入、ESP调度或环境精度证据。实板CSV观测字段仍为空。

反复CRC或通信失败时断电检查VIN/GND/SDA/SCL与具体模块,不靠换滤波参数掩盖错误。本例不自动启加热、不用趋势控制安全设备。后续接MQTT时,raw、quality、age一起上报,不能只发送平滑后的数字。

代码 / 配置
c++ -std=c++17 -Wall -Wextra -Werror host/host_check.cpp -o /tmp/s08-host
/tmp/s08-host

参考:[1] SHT4x Datasheet V6.4:温度工作范围、CRC、命令、换算与RH裁边[3] Sensirion Core0.7.3 receiveFrame:数据长度和每字CRC[4] Adafruit PID4885模块引脚、固定0x44与上拉

BACK TO THE SOURCE

资料来源与版本说明

按固定版本官方源码与具体板级核对;实际构建、主机输入检查和实板是分开记录的状态。没有把预期步骤写成本站已运行结果。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. SHT4x Datasheet V6.4:温度工作范围、CRC、命令、换算与RH裁边
  2. Sensirion SHT4x1.1.2:measureHighPrecision与返回码
  3. Sensirion Core0.7.3 receiveFrame:数据长度和每字CRC
  4. Adafruit PID4885模块引脚、固定0x44与上拉
  5. Arduino-ESP32 3.3.12 Wire:setTimeOut与requestFrom
  6. 官方S3 DevKitC-1 v1.1:排针与模组
  7. 官方DevKitC-1 v1.1固定原理图:J1排针与GPIO端点