Repository navigation
Conversation
The BME680 gas sensor reports only an ohmic resistance, not a CO2 concentration, so derive an indoor air quality index from the gas resistance instead. The scale follows the Bosch BSEC convention of 0 (clean air) to 500 (heavily polluted air) documented in the BME680 datasheet, Sec. 4.2 and Table 4 (https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme680-ds001.pdf). The arithmetic is an adaptation of the basic open-source IAQ calculator in thstielow/raspi-bme680-iaq (https://github.com/thstielow/raspi-bme680-iaq), which circumvents the closed-source BSEC library by squaring the ratio between the gas resistance and a baseline ceiling value for a steeper slope at high air quality and rescaling the result to a 0-500 index. The percentage-based gas quality and the baseline/drift handling are inspired by the example in the Pimoroni bme680-python library (https://github.com/pimoroni/bme680-python). In place of the linear rescale IAQ = (1 - AQ/100) * 500 used there, this driver maps the squared excess ratio through a saturating Hill function u = (k * x)^2 with IAQ = 500 * u / (1 + u), where x = R_baseline / R_gas - 1 and resistance below the baseline is clamped to zero. A tVOC equivalent in ppb is derived from the same excess ratio with a saturating (Michaelis-Menten style) curve. The metal-oxide resistance also depends on ambient humidity, so the reading is normalized to a dry-air equivalent before it enters the baseline and the ratio. As in the thstielow reference, absolute humidity is derived from temperature and relative humidity with the Magnus equation and the ideal gas law, and the resistance is scaled by exp(ph_slope * hum_abs), with ph_slope defaulting to 0.03 per g/m3. This keeps the index stable across humidity changes instead of drifting with them. The baseline is the clean-air ceiling of that compensated resistance. It is learned as a running mean during an initial burn-in of 2880 measurements, about 96 minutes at the 2 second reading interval, and then adapted in two directions: towards cleaner readings with an exponential moving average of 0.005, a time constant of about 7 minutes, and back down with a time constant of about 2.3 days to follow the slow aging of the metal-oxide layer. Only the clean-air direction moves the ceiling quickly, so a pollution event does not drag the reference along with it and flatten the index. Readings outside 1..5000 kOhm are dropped from the index, which rejects individual sensor glitches while leaving clean, dry air well inside the range, and a stored baseline is validated against the wider range that the humidity compensation can produce. MQTT and the JSON API report the raw 0-500 index and the tVOC value in ppb. The WebUI shows the same information as a 0-100 quality percentage where 100 is clean air, plus the tVOC in ppb, both with localized labels from the language files. While the burn-in runs, the WebUI reports the calibration progress instead, and no index is published until it is based on a real baseline. The console command BmpIaq reports, sets and resets the gas baseline: BmpIaq prints the current baseline as the uncompensated resistance in kOhm, directly comparable with the Gas reading BmpIaq <1..5000> stores the value as the calibrated clean-air baseline in kOhm, normalized to dry air for the ratio; values outside 1..5000 kOhm are rejected BmpIaq Reset discards the learned baseline and restarts the burn-in The baseline is written to flash as soon as the burn-in completes and again on a graceful restart or OTA update through the save-before-restart hook. It is kept in a free area of the reserved TSettings block on every CPU target, so the calibration survives reboots and updates without frequent flash writes.
s-hadinger
left a comment
There was a problem hiding this comment.
Please review the Settings part
| #if CONFIG_IDF_TARGET_ESP32S31 | ||
| uint8_t ws_color[4][3]; // EB0 | ||
| uint8_t free_esp32s31_ebc[8]; // EBC | ||
| #ifdef USE_BME68X |
There was a problem hiding this comment.
The definition inside Settings should not depend on compile option. The value is there, the compile options only dictate whether the values are used or not.
Also it looks like it's only for ESP32S31, is that normal?
There was a problem hiding this comment.
Have I understood you correctly that values in TSettings are always retained, even when the code that uses them is not active?
Please suggest a free value that the code could use.
There was a problem hiding this comment.
Correct. That's the only way to support different drivers/sensors on the same device.
Let me see where to find four bytes for a single float...
There was a problem hiding this comment.
This would be the preferred solution:
uint16_t zcdimmerset[5]; // EA6
#if CONFIG_IDF_TARGET_ESP32S31
uint8_t ws_color[4][3]; // EB0
#else
uint8_t free_eb0[12]; // EB0 All but ESP32-S31 !!!
#endif
uint8_t free_ebc[4]; // EBC
float bme680_iaq_baseline; // EC0
uint16_t light_pixels_height_1 : 15;// EC4 Pixels height minus 1, default 0 (0 means 1 line)
There was a problem hiding this comment.
Why is the GUI Air quality different from the JSON Air quality:
"MQTT and the JSON API report the raw 0-500 index and the tVOC value in ppb. The WebUI shows the same information as a 0-100 quality percentage where 100 is clean air."
The idea behind this is to display a value for the user in the GUI, but to pass on the raw value for any further calculations that may be required, as we do not know how it will be used at the destination.
There was a problem hiding this comment.
Let me add that by using an integer we'll loose the decimal part. Can you verify that losing the decimals isn't a big issue? I suppose you can test your current setup with integer calibration value and see if the outcome differs a lot.
Give me a bit of time to answer that question. I need to test it. I’ll get back to you.
There was a problem hiding this comment.
In case of GUI percentage pls add the percent sign %
There was a problem hiding this comment.
No. The stored float is not the raw BmpIaq <1..5000 kOhm> calibration value. It is the humidity-compensated baseline:
comp_gas = gas_kohm * exp(0.03f * absolute_humidity);
baseline = comp_gas; // or the mean of it
So the stored value is typically a factor of ~1.0–2.3 above the raw value, i.e. roughly 1..12000 kOhm in practice; validation allows up to BME680_IAQ_BASE_MAX = 20000.0f. The 0..5000 kOhm range is only the raw gas resistance / the argument to BmpIaq.
That said, a uint16_t still handles this range easily (0..65535), even with 1 kOhm resolution.
There was a problem hiding this comment.
In case of GUI percentage pls add the percent sign
%
The value is normalized and is therefore not automatically expressed as a percentage. 100% IAQ sounds a bit odd.
holding one baseline per sensor, replacing the single float. Removed the hardcoded first-sensor index; Loop over all detected sensors. Bme680IaqCommand: reworked to control per-sensor * BmpIaq -> report every BME680 baseline * BmpIaq <n> -> report baseline of BME680 n (0-3) * BmpIaq <n>,<value> -> set baseline of sensor n * BmpIaq Reset -> reset all sensors * BmpIaq <n>,Reset -> reset sensor n
Description:
The BME680 gas sensor reports only an ohmic resistance, not a CO2 concentration, so derive an indoor air quality index from the gas resistance instead. The scale follows the Bosch BSEC convention of 0 (clean air) to 500 (heavily polluted air) documented in the BME680 datasheet, Sec. 4.2 and Table 4
(https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme680-ds001.pdf).
The arithmetic is an adaptation of the basic open-source IAQ calculator in thstielow/raspi-bme680-iaq (https://github.com/thstielow/raspi-bme680-iaq), which circumvents the closed-source BSEC library by squaring the ratio between the gas resistance and a baseline ceiling value for a steeper slope at high air quality and rescaling the result to a 0-500 index. The percentage-based gas quality and the baseline/drift handling are inspired by the example in the Pimoroni bme680-python library (https://github.com/pimoroni/bme680-python). In place of the linear rescale IAQ = (1 - AQ/100) * 500 used there, this driver maps the squared excess ratio through a saturating Hill function u = (k * x)^2 with IAQ = 500 * u / (1 + u), where x = R_baseline / R_gas - 1 and resistance below the baseline is clamped to zero. A tVOC equivalent in ppb is derived from the same excess ratio with a saturating (Michaelis-Menten style) curve.
The metal-oxide resistance also depends on ambient humidity, so the reading is normalized to a dry-air equivalent before it enters the baseline and the ratio. As in the thstielow reference, absolute humidity is derived from temperature and relative humidity with the Magnus equation and the ideal gas law, and the resistance is scaled by exp(ph_slope * hum_abs), with ph_slope defaulting to 0.03 per g/m3. This keeps the index stable across humidity changes instead of drifting with them.
The baseline is the clean-air ceiling of that compensated resistance. It is learned as a running mean during an initial burn-in of 2880 measurements, about 96 minutes at the 2 second reading interval, and then adapted in two directions: towards cleaner readings with an exponential moving average of 0.005, a time constant of about 7 minutes, and back down with a time constant of about 2.3 days to follow the slow aging of the metal-oxide layer. Only the clean-air direction moves the ceiling quickly, so a pollution event does not drag the reference along with it and flatten the index.
Readings outside 1..5000 kOhm are dropped from the index, which rejects individual sensor glitches while leaving clean, dry air well inside the range, and a stored baseline is validated against the wider range that the humidity compensation can produce.
MQTT and the JSON API report the raw 0-500 index and the tVOC value in ppb. The WebUI shows the same information as a 0-100 quality percentage where 100 is clean air, plus the tVOC in ppb, both with localized labels from the language files. While the burn-in runs, the WebUI reports the calibration progress instead, and no index is published until it is based on a real baseline.
The console command BmpIaq reports, sets and resets the gas baseline:
BmpIaqprints the current baseline as the uncompensated resistance in kOhm, directly comparable with the Gas readingBmpIaq <1..5000>stores the value as the calibrated clean-air baseline in kOhm, normalized to dry air for the ratio; values outside 1..5000 kOhm are rejectedBmpIaq Resetdiscards the learned baseline and restarts the burn-inThe baseline is written to flash as soon as the burn-in completes and again on a graceful restart or OTA update through the save-before-restart hook. It is kept in a free area of the reserved TSettings block on every CPU target, so the calibration survives reboots and updates without frequent flash writes.
Related issue (if applicable): fixes #
Checklist:
NOTE: The code change must pass CI tests. Your PR cannot be merged unless tests pass