传感器接上以后,最容易做错的一步是直接拿一个数字下结论。比如气体阻值突然变了,就想把它标成异常;读数回落了,又马上标成正常。这样做在页面上看起来很灵敏,但实际很容易误判。
BME690 的气体阻值会受很多因素影响:刚上电的预热、温湿度变化、空气流动、传感器附近的人体气流、酒精或清洁剂、厨房油烟,都会让读数变化。早期算法如果太激进,页面会频繁跳状态;如果太迟钝,又看不出变化。所以我先做了一个简单可解释的版本:原始值、平滑值、环境基线、相对偏离、持续时间,最后再合成异常评分。
第一件事是别把原始值当结论
传感器原始值必须保留,但不能直接当最终状态。原始值最大的价值是追溯问题:当页面显示异常时,能回头看真实数据到底发生了什么。我会把几个字段分开:json{ "gas_raw": 126.4, "gas_smooth": 122.8, "gas_baseline": 98.5, "deviation": 0.247, "duration_seconds": 12, "score": 0.68}这些字段分别承担不同任务。gas_raw用来追溯;gas_smooth用来减少尖峰影响;gas_baseline表示当前环境的日常参考;deviation表示偏离程度;duration_seconds表示持续了多久;score才是页面更容易理解的状态指标。如果只保留一个最终分数,调试会很难。分数高了,不知道是原始值真的变了,还是平滑太敏感,或者基线更新策略不合适。预热阶段不要参与判断
BME690 刚启动时读数不会马上稳定。如果上电后立刻建立基线,后面的判断很容易被带偏。我的做法是引入warmup状态,预热阶段只采集和显示,不参与异常评分,也不更新长期基线。伪代码可以这样写:cif (state.warmup) { state.score = 0.0f; state.duration_seconds = 0; return;}这个逻辑简单,但很重要。它能避免一种常见误判:设备刚上电,读数快速变化,页面马上显示异常。实际这不是环境异常,只是传感器还没稳定。平滑是为了少被尖峰带跑
环境数据会有尖峰。一次气流、一次手靠近传感器、一次短暂扰动,都可能让原始值跳一下。如果直接用原始值判断,页面会频繁闪。第一版可以用简单指数平滑:cfloat smooth_value(float old_value, float new_value){ const float alpha = 0.15f; if (old_value <= 0.0f) { return new_value; } return old_value * (1.0f - alpha) + new_value * alpha;}````alpha` 越大,反应越快,也越容易跟着噪声跳;`alpha` 越小,曲线越稳,但响应更慢。这个参数没有万能值,需要结合采样频率和实际测试调整。这里不要追求一开始就完美。先把原始值和平滑值同时显示出来,看它们在真实环境下怎么变化,再决定参数。<p><span style="color:#1f6feb;"><strong><em><u>基线不是固定常数</u></em></strong></span></p>很多人会想设一个阈值,比如大于某个数就是异常。但环境传感器不太适合这么做。不同房间、不同温湿度、不同摆放位置,基础读数都可能不同。一个固定阈值很容易在某些环境里太敏感,在另一些环境里又完全没反应。所以我更倾向于维护一个环境基线。基线表示“当前环境最近一段时间的大致正常水平”。它应该缓慢跟随日常变化,但不能被突发扰动马上带走。伪代码可以这样写:cfloat update_baseline(float baseline, float smooth, bool abnormal){ if (baseline <= 0.0f) { return smooth; } if (abnormal) { return baseline; } const float beta = 0.02f; return baseline * (1.0f - beta) + smooth * beta;}这里的关键是:异常期间不要快速更新基线。否则持续异常会被系统慢慢当成新的正常,最后分数反而降下来。<p><span style="color:#1f6feb;"><strong><em><u>只看偏离还不够,还要看持续时间</u></em></strong></span></p>一次短暂波动和一次持续偏离,意义不同。比如有人从传感器旁边经过,读数可能会瞬间变化;但如果变化持续几十秒,就更值得关注。第一版可以用相对偏离和持续时间一起判断:cfloat calc_deviation(float smooth, float baseline){ if (baseline <= 0.0f) { return 0.0f; } return fabsf(smooth - baseline) / baseline;}void update_duration(env_state_t *s){ if (s->deviation > 0.25f) { s->duration_seconds += 1; } else { s->duration_seconds = 0; }}这里的 `0.25` 只是示例阈值,不应该直接当成通用答案。实际项目里要根据采样频率、传感器位置和样本数据调整。<p><span style="color:#1f6feb;"><strong><em><u>异常评分要能解释</u></em></strong></span></p>我不喜欢一上来做一个黑盒分数。早期版本的评分最好能解释清楚:为什么分数变高,为什么还没报警,为什么短暂扰动被忽略。一个简单评分可以这样写:cfloat calc_score(float deviation, int duration_seconds){ float deviation_score = deviation / 0.5f; if (deviation_score > 1.0f) deviation_score = 1.0f; float duration_score = duration_seconds / 30.0f; if (duration_score > 1.0f) duration_score = 1.0f; return deviation_score * 0.6f + duration_score * 0.4f;}```这个公式不复杂,但适合早期调试。偏离程度和持续时间分别占一部分,页面上也能把两个因素展示出来。读者看到分数时,不会觉得它是凭空冒出来的。页面一定要显示中间值
如果页面只显示“正常/异常”,调试会很难。我会把中间值也放出来,包括原始值、平滑值、基线、偏离比例、持续时间和数据来源。参数别在一次测试里定死
平滑系数、偏离阈值、持续时间阈值都不应该靠一次测试拍脑袋定下来。比较稳的办法是保存几批数据,用同一套算法回放。这样你能看到参数变化对结果的影响。比如同一批数据里,把偏离阈值从 0.2 调到 0.3,看看短时扰动有没有被过滤掉;把持续时间从 10 秒调到 30 秒,看看系统是不是变得太迟钝。参数不是越敏感越好,读者实际使用时更怕频繁误触发。不要把一次安全样本说成结论
测试时可以用安全方式制造一些变化,比如改变通风、让传感器靠近普通日用品气味源、短时间观察读数变化。但这些测试只能说明传感器对环境变化有反应,不能说明它已经能识别某种具体气味。写到这里,第一版算法重点是可解释
第一版环境基线算法不追求复杂。它的价值在于把原始值、平滑、基线、偏离和持续时间拆开,让每一步都能被看懂、被调试。如果你也在做类似项目,建议先别急着上复杂模型。先把中间值显示出来,拿几批真实数据看看它们怎么变化。等你能解释为什么某次变化被判成异常、为什么某次扰动被忽略,再考虑更复杂的识别方法。