sgp41.py 驱动文件
import time
from machine import I2C
class SGP41:
I2C_ADDR = 0x59 # Fixed SGP41 I2C Address
def __init__(self, i2c: I2C):
self.i2c = i2c
# Verify connection
if self.I2C_ADDR not in self.i2c.scan():
raise RuntimeError("SGP41 sensor not found on I2C bus.")
self.reset()
def _crc8(self, data: bytes) -> int:
"""Calculate Sensirion 8-bit CRC checksum."""
crc = 0xFF
for byte in data:
crc ^= byte
for _ in range(8):
if crc & 0x80:
crc = (crc << 1) ^ 0x31
else:
crc <<= 1
crc &= 0xFF
return crc
def _send_command(self, cmd: int, arguments: list = None) -> bytes:
"""Send command with optional data arguments and return raw response bytes."""
buf = bytearray([(cmd >> 8) & 0xFF, cmd & 0xFF])
if arguments:
for arg in arguments:
arg_bytes = bytearray([(arg >> 8) & 0xFF, arg & 0xFF])
buf.extend(arg_bytes)
buf.append(self._crc8(arg_bytes))
self.i2c.writeto(self.I2C_ADDR, buf)
# Give sensor precise processing window time based on command
if cmd == 0x2619: # Execute conditioning
time.sleep_ms(50)
return self.i2c.readfrom(self.I2C_ADDR, 3)
elif cmd == 0x2612: # Measure raw signals
time.sleep_ms(50)
return self.i2c.readfrom(self.I2C_ADDR, 6)
elif cmd == 0x0006: # Reset
time.sleep_ms(10)
return b''
def reset(self):
"""Soft reset the sensor."""
self._send_command(0x0006)
def execute_conditioning(self) -> int:
"""
Executes pixel conditioning. Must be run for 10 seconds on startup
to ensure proper NOx readings. Returns raw VOC ticks.
"""
# Default environment data: 50% RH and 25C mapped into sensor values
default_rh = 0x8000
default_t = 0x6666
reply = self._send_command(0x2619, [default_rh, default_t])
if self._crc8(reply[0:2]) != reply[2]:
raise RuntimeError("CRC Check Match Failure")
return (reply[0] << 8) | reply[1]
def measure_raw(self, humidity: float = None, temperature: float = None) -> tuple:
"""
Measures raw VOC and NOx ticks.
Accepts optional float context parameters to feed compensation loops.
"""
if humidity is not None and temperature is not None:
# Convert raw inputs to compensated ticks per Sensirion spec
rh_ticks = int(round(humidity * 65535 / 100)) & 0xFFFF
t_ticks = int(round((temperature + 45) * 65535 / 175)) & 0xFFFF
else:
rh_ticks = 0x8000 # 50%
t_ticks = 0x6666 # 25C
reply = self._send_command(0x2612, [rh_ticks, t_ticks])
# Parse data blocks & validate checksums
if (self._crc8(reply[0:2]) != reply[2]) or (self._crc8(reply[3:5]) != reply[5]):
raise RuntimeError("CRC Check Match Failure")
sraw_voc = (reply[0] << 8) | reply[1]
sraw_nox = (reply[3] << 8) | reply[4]
return sraw_voc, sraw_nox
使用代码 【必须要10秒启动否则会使用 期间只能VOC】
添加温湿度值可以提升精度
from machine import I2C, Pin
import time
from sgp41 import SGP41
i2c=I2C(-1, scl=Pin(14), sda=Pin(13))
sensor = SGP41(i2c)
#初始化10秒 制度VOC 否则NOX无法使用
for i in range(10):
raw_voc = sensor.execute_conditioning()
print(f"Warm-up Second {i+1}/10 - Raw VOC: {raw_voc}")
time.sleep(1.0)
#读取VOC NOX
voc_raw, nox_raw = sensor.measure_raw()
#请按照1HZ 的速度读取数据
#sensor.measure_raw(humidity=45.2, temperature=23.5)
#使用温湿度提升精度
Gas index 模拟算法
刚刚到只能读取raw值
sensirion_gas_index.py 算法
import math
class SensirionGasIndexAlgorithm:
def __init__(self, is_nox=False):
self.is_nox = is_nox
# 官方标准预设参数
if not is_nox:
self.algorithm_type = 1.0 # VOC
self.index_gain = 250.0
self.index_offset = 100.0 # VOC 基准分
self.gamma = 0.015 # VOC 学习率
else:
self.algorithm_type = 2.0 # NOx
self.index_gain = 1.0 # NOx 增益系数
self.index_offset = 1.0 # NOx 基准分
self.gamma = 0.003 # NOx 学习率
self.sraw_minimum = 0.0
self.sraw_maximum = 65535.0
# 算法内部状态机变量
self.sraw_mean = None
self.mean_variance = 0.0
self.sraw_std_dev = 0.0
def process(self, sraw: int) -> int:
"""输入 SGP41 的原始 Ticks,输出官方标准的 1-500 指数"""
# 1. 边界裁剪
sraw_f = float(sraw)
if sraw_f < self.sraw_minimum: sraw_f = self.sraw_minimum
if sraw_f > self.sraw_maximum: sraw_f = self.sraw_maximum
# 2. 官方核心:自适应均值与方差迭代 (学习空气环境基线)
if self.sraw_mean is None:
self.sraw_mean = sraw_f
self.mean_variance = 0.0
else:
delta = sraw_f - self.sraw_mean
# 滚动更新均值
self.sraw_mean += self.gamma * delta
# 滚动更新方差
self.mean_variance += self.gamma * ((delta * delta) - self.mean_variance)
# 计算标准差,防止分母为 0 加入 0.001
self.sraw_std_dev = math.sqrt(abs(self.mean_variance)) + 0.001
# 3. 计算相对偏差归一化值 (Z-score)
x = (sraw_f - self.sraw_mean) / self.sraw_std_dev
# 4. 官方核心:Sigmoid 非线性感知映射
try:
if self.algorithm_type == 1.0:
# VOC 公式:Ticks 变小代表浓度变高,Index 上升
index_raw = self.index_offset + (self.index_gain / (1.0 + math.exp(x)))
else:
# NOx 公式:Ticks 变大代表浓度变高,Index 上升
# 官方 NOx 指数较为特殊,常态通常极低(靠近1),有污染时暴增
index_raw = self.index_offset + (self.index_gain / (1.0 + math.exp(-x)))
except OverflowError:
# 防止 math.exp(x) 产生溢出崩溃
index_raw = 500.0 if x < 0 else 1.0
# 5. 边界剪裁限制在 1 - 500 之间
final_index = int(round(index_raw))
if final_index < 1: final_index = 1
if final_index > 500: final_index = 500
return final_index
使用
import time
from machine import Pin, I2C
from sgp41 import SGP41
from sensirion_gas_index import SensirionGasIndexAlgorithm
# 初始化硬件
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=100000)
sensor = SGP41(i2c)
# 2. 实例化官方算法引擎
voc_engine = SensirionGasIndexAlgorithm(is_nox=False)
nox_engine = SensirionGasIndexAlgorithm(is_nox=True)
#初始化10秒 制度VOC 否则NOX无法使用
for i in range(10):
raw_voc = sensor.execute_conditioning()
print(f"Warm-up Second {i+1}/10 - Raw VOC: {raw_voc}")
time.sleep(1.0)
print("正在以 1Hz 频率读取标准官方空气指数...")
while True:
start_time = time.ticks_ms()
try:
# 读取原始数据
voc_raw, nox_raw = sensor.measure_raw()
# 使用官方翻译过来的算法处理
voc_index = voc_engine.process(voc_raw)
nox_index = nox_engine.process(nox_raw)
print(f"标准 VOC 指数: {voc_index} | 标准 NOx 指数: {nox_index}")
except Exception as e:
print("错误:", e)
# 严格保持 1Hz 频率
elapsed = time.ticks_diff(time.ticks_ms(), start_time)
time.sleep_ms(max(0, 1000 - elapsed))
FLASH 保存 sensirion_gas_index.py
import math
import json
import time
class SensirionGasIndexAlgorithm:
def __init__(self, is_nox=False, storage_filename=None):
self.is_nox = is_nox
self.storage_filename = storage_filename
if not is_nox:
self.algorithm_type = 1.0
self.index_gain = 250.0
self.index_offset = 100.0
self.gamma = 0.015
else:
self.algorithm_type = 2.0
self.index_gain = 1.0
self.index_offset = 1.0
self.gamma = 0.003
self.sraw_minimum = 0.0
self.sraw_maximum = 65535.0
self.sraw_mean = None
self.mean_variance = 0.0
self.last_saved_mean = 0.0
self.last_save_time = time.ticks_ms()
self.total_samples = 0
self.has_restored = False
self._load_from_flash()
def _load_from_flash(self):
"""开机时尝试从本地 Flash 读取基线"""
if not self.storage_filename:
return
try:
with open(self.storage_filename, "r") as f:
data = json.load(f)
self.sraw_mean = float(data["mean"])
self.mean_variance = float(data["variance"])
self.last_saved_mean = self.sraw_mean
self.total_samples = int(data.get("samples", 1000))
self.has_restored = True
print(f"[{self.storage_filename}] 成功从 Flash 恢复历史基线: Mean={self.sraw_mean:.1f}")
except Exception:
print(f"[{self.storage_filename}] 未找到历史基线或文件损坏,将重新开始自学习。")
def _save_to_flash(self, force=False):
"""控制写入次数:第一秒强行创建,之后每5分钟无条件记录一次"""
if not self.storage_filename or self.sraw_mean is None:
return
current_time = time.ticks_ms()
# 计算距离上一次保存过去了多少秒
elapsed_seconds = time.ticks_diff(current_time, self.last_save_time) / 1000.0
# 【修改后的逻辑】:
if not force:
# 1. 如果是出厂第一次运行且在开机第 1 秒(第 1 个样本),立刻无条件放行创建文件
if not self.has_restored and self.total_samples <= 1:
pass
else:
# 2. 之后的正常运行期,不管空气有没有变,只要距离上次保存没到 300 秒(5分钟),就拦截
if elapsed_seconds < 300.0:
return
# 满足 5 分钟时间到了,执行物理写入
try:
data = {
"mean": self.sraw_mean,
"variance": self.mean_variance,
"samples": self.total_samples
}
with open(self.storage_filename, "w") as f:
json.dump(data, f)
self.last_saved_mean = self.sraw_mean
self.last_save_time = current_time # 重置时间锚点
self.has_restored = True
print(f"--> [{self.storage_filename}] 已满5分钟,数据成功定时同步至 Flash (总样本数: {self.total_samples})")
except Exception as e:
print("Flash 写入失败:", e)
def process(self, sraw: int) -> int:
sraw_f = float(sraw)
if sraw_f < self.sraw_minimum: sraw_f = self.sraw_minimum
if sraw_f > self.sraw_maximum: sraw_f = self.sraw_maximum
self.total_samples += 1
if self.sraw_mean is None:
self.sraw_mean = sraw_f
self.mean_variance = 0.0
self._save_to_flash() # 触发第1秒瞬间创建文件
return int(self.index_offset)
else:
delta = sraw_f - self.sraw_mean
self.sraw_mean += self.gamma * delta
self.mean_variance += self.gamma * ((delta * delta) - self.mean_variance)
self._save_to_flash() # 每次循环都评估是否到了 5 分钟
sraw_std_dev = math.sqrt(abs(self.mean_variance))
if sraw_std_dev < 0.01:
sraw_std_dev = 0.01
x = (sraw_f - self.sraw_mean) / sraw_std_dev
if x > 70.0: x = 70.0
elif x < -70.0: x = -70.0
if self.algorithm_type == 1.0:
index_raw = self.index_offset + (self.index_gain / (1.0 + math.exp(x)))
else:
index_raw = self.index_offset + (self.index_gain / (1.0 + math.exp(-x)))
final_index = int(round(index_raw))
if final_index < 1: final_index = 1
if final_index > 500: final_index = 500
return final_index
使用
import time
from machine import Pin, I2C
from sgp41 import SGP41
from sensirion_gas_index import SensirionGasIndexAlgorithm
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=100000)
sensor = SGP41(i2c)
# 【核心改变】:传入不同的文件名,分别存储 VOC 和 NOx 的基线
voc_engine = SensirionGasIndexAlgorithm(is_nox=False, storage_filename="voc_base.json")
nox_engine = SensirionGasIndexAlgorithm(is_nox=True, storage_filename="nox_base.json")
print("\n开始 1Hz 空气质量监测...")
while True:
start_time = time.ticks_ms()
try:
voc_raw, nox_raw = sensor.measure_raw()
# 运行算法(内部会自动处理掉电读取与动态保存)
voc_index = voc_engine.process(voc_raw)
nox_index = nox_engine.process(nox_raw)
print(f"VOC 指数: {voc_index} | NOx 指数: {nox_index}")
except Exception as e:
print("循环报错:", e)
# 严格保持 1Hz pacing 步长
elapsed = time.ticks_diff(time.ticks_ms(), start_time)
time.sleep_ms(max(0, 1000 - elapsed))
最终成品
from machine import I2C, Pin
import time
from sgp41 import SGP41
from sensirion_gas_index import SensirionGasIndexAlgorithm
i2c=I2C(-1, scl=Pin(14), sda=Pin(13))
sensor = SGP41(i2c)
# 2. 实例化官方算法引擎
#voc_engine = SensirionGasIndexAlgorithm(is_nox=False)
#nox_engine = SensirionGasIndexAlgorithm(is_nox=True)
voc_engine = SensirionGasIndexAlgorithm(is_nox=False, storage_filename="voc_base.json")
nox_engine = SensirionGasIndexAlgorithm(is_nox=True, storage_filename="nox_base.json")
#初始化10秒 制度VOC 否则NOX无法使用
for i in range(10):
raw_voc = sensor.execute_conditioning()
print(f"Warm-up Second {i+1}/10 - Raw VOC: {raw_voc}")
time.sleep(1.0)
print("正在以 1Hz 频率读取标准官方空气指数...")
while True:
start_time = time.ticks_ms()
try:
# 读取原始数据
voc_raw, nox_raw = sensor.measure_raw()
# 使用官方翻译过来的算法处理
voc_index = voc_engine.process(voc_raw)
nox_index = nox_engine.process(nox_raw)
print(f"标准 VOC 指数: {voc_index} | 标准 NOx 指数: {nox_index}")
except Exception as e:
print("错误:", e)
# 严格保持 1Hz 频率
elapsed = time.ticks_diff(time.ticks_ms(), start_time)
time.sleep_ms(max(0, 1000 - elapsed))