Skip to content

Commit

Permalink
Merge pull request #207 from EAGrahamJr/air-quality
Browse files Browse the repository at this point in the history
Extracts air-quality calculations for general use.
  • Loading branch information
mattjlewis authored Jul 29, 2024
2 parents 129bcba + 741f0d1 commit 70e95c7
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 67 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.diozero.devices;
/*-
* #%L
* Organisation: diozero
* Project: diozero - Core
* Filename: AirQualitySensorInterface.java
*
* This file is part of the diozero project. More information about this project
* can be found at https://www.diozero.com/.
* %%
* Copyright (C) 2016 - 2024 diozero
* %%
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
* #L%
*/

/**
* An air-quality sensor uses relative humidity and a "resistance" value to measure air quaility.
*/
public interface AirQualitySensorInterface extends HygrometerInterface {
/**
* "Standard" humidity baseline typically used for indoor air-quality.
*/
float STANDARD_INDOOR_HUMIDITY = 40f;

float getGasResistance();

/**
* Calculates the indoor air quality as a percentage based on a baseline reading. Based off of Pimoroni's
* <a href="https://github.com/pimoroni/bme680-python/blob/main/examples/indoor-air-quality.py">indoor-air-quality.py</a>
* <p>
* Under non-calibrated, general usage, it is recommended that the sensor "warm up" in the current mode for at least
* 30 minutes before taking readings. After that time, any kind of statistical baseline (e.g. average) for the gas
* and humidity readings can be used to get an indication of general air-quality.
* </p>
*
* @param gasReading current reading
* @param gasBaseline the "baseline" to score off of
* @param humidityReading current reading
* @param humidityBaseline the "baseline" to score off of
* @param humidityWeighting weighting applied to scoring (a good default is 0.25f)
*/
static float airQuality(float gasReading, float gasBaseline, float humidityReading, float humidityBaseline,
float humidityWeighting) {
float gasOffset = gasBaseline - gasReading;
float humidityOffset = humidityReading - humidityBaseline;

float humidityScore;
if (humidityOffset > 0) {
humidityScore = (100 - humidityBaseline - humidityOffset) / (100 - humidityBaseline) * (humidityWeighting * 100);
}
else {
humidityScore = (humidityBaseline + humidityOffset) / humidityBaseline * (humidityWeighting * 100);
}

float gasScore;
if (gasOffset > 0) {
gasScore = (gasReading / gasBaseline) * (100 - (humidityWeighting * 100));
}
else {
gasScore = 100 - (humidityWeighting * 100);
}

// Calculate air_quality_score.
return humidityScore + gasScore;
}

}
14 changes: 8 additions & 6 deletions diozero-core/src/main/java/com/diozero/devices/BME68x.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Organisation: diozero
* Project: diozero - Core
* Filename: BME68x.java
*
*
* This file is part of the diozero project. More information about this project
* can be found at https://www.diozero.com/.
* %%
Expand All @@ -17,10 +17,10 @@
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
Expand All @@ -47,7 +47,7 @@
* https://github.com/pimoroni/bme680-python
* https://github.com/knobtviker/bme680
*/
public class BME68x implements BarometerInterface, ThermometerInterface, HygrometerInterface {
public class BME68x implements BarometerInterface, ThermometerInterface, AirQualitySensorInterface {
// Chip vendor for the BME680
private static final String CHIP_VENDOR = "Bosch";
// Chip name for the BME680
Expand Down Expand Up @@ -552,7 +552,7 @@ public BME68x(final I2CDeviceInterface device) {
* @param controller I2C bus the sensor is connected to.
* @param address I2C address of the sensor.
* @param humidityOversampling Humidity oversampling.
* @param termperatureOversampling Temperature oversampling.
* @param temperatureOversampling Temperature oversampling.
* @param pressureOversampling Pressure oversampling.
* @param filter Infinite Impulse Response (IIR) filter.
* @param standbyDuration Standby time between sequential mode
Expand All @@ -570,7 +570,7 @@ public BME68x(final int controller, final int address, OversamplingMultiplier hu
*
* @param device I2C device.
* @param humidityOversampling Humidity oversampling.
* @param termperatureOversampling Temperature oversampling.
* @param temperatureOversampling Temperature oversampling.
* @param pressureOversampling Pressure oversampling.
* @param filter Infinite Impulse Response (IIR) filter.
* @param standbyDuration Standby time between sequential mode
Expand Down Expand Up @@ -910,6 +910,7 @@ public float getRelativeHumidity() {
return getSensorData()[0].getHumidity();
}

@Override
public float getGasResistance() {
return getSensorData()[0].getGasResistance();
}
Expand Down Expand Up @@ -2012,4 +2013,5 @@ public String toString() {
+ ", heaterResistance=" + heaterResistance + ", gasWaitMs=" + gasWaitMs + "]";
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Organisation: diozero
* Project: diozero - Sample applications
* Filename: BME68xTest.java
*
*
* This file is part of the diozero project. More information about this project
* can be found at https://www.diozero.com/.
* %%
Expand All @@ -17,10 +17,10 @@
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
Expand All @@ -34,6 +34,7 @@
import java.util.ArrayList;
import java.util.List;

import com.diozero.devices.AirQualitySensorInterface;
import com.diozero.devices.BME68x;
import com.diozero.devices.BME68x.Data;
import com.diozero.devices.BME68x.HeaterConfig;
Expand All @@ -60,10 +61,10 @@ public static void main(String[] args) {
}

try (BME68x bme68x = new BME68x(controller, address)) {
System.out.format("chipId: 0x%x, variantId: 0x%x, uniqueId: 0x%x%n", Integer.valueOf(bme68x.getChipId()),
Integer.valueOf(bme68x.getVariantId()), Integer.valueOf(bme68x.getUniqueId()));
System.out.format("chipId: 0x%x, variantId: 0x%x, uniqueId: 0x%x%n", (int)bme68x.getChipId(),
(int)bme68x.getVariantId(), (int)bme68x.getUniqueId());
System.out.format(
"Humidity Oversampling: %s, Temperature Oversampling: %s, Pressure Oversampling: %s, Filter: %s, Standy Duration: %s%n",
"Humidity Oversampling: %s, Temperature Oversampling: %s, Pressure Oversampling: %s, Filter: %s, Stand Duration: %s%n",
bme68x.getHumidityOversample(), bme68x.getTemperatureOversample(), bme68x.getPressureOversample(),
bme68x.getIirFilterConfig(), bme68x.getStandbyDuration());

Expand Down Expand Up @@ -122,13 +123,13 @@ private static void forcedModeTest(BME68x bme68x) {
int reading = 0;
for (Data data : bme68x.getSensorData(target_operating_mode)) {
System.out.format(
"Reading [%d]: Idx: %,d. Temperature: %,.2f C. Pressure: %,.2f hPa. Relative Humidity: %,.2f %%rH. Gas Idx: %,d. Gas Resistance: %,.2f Ohms. IDAC: %,.2f mA. Gas Wait: %,d (ms or multiplier). (heater stable: %b, gas valid: %b).%n",
Integer.valueOf(reading), Integer.valueOf(data.getMeasureIndex()),
Float.valueOf(data.getTemperature()), Float.valueOf(data.getPressure()),
Float.valueOf(data.getHumidity()), Integer.valueOf(data.getGasMeasurementIndex()),
Float.valueOf(data.getGasResistance()), Float.valueOf(data.getIdacHeatMA()),
Short.valueOf(data.getGasWait()), Boolean.valueOf(data.isHeaterTempStable()),
Boolean.valueOf(data.isGasMeasurementValid()));
"Reading [%d]: Idx: %,d. Temperature: %,.2f C. Pressure: %,.2f hPa. Relative Humidity: %,.2f %%rH. Gas Idx: %,d. Gas Resistance: %,.2f Ohms. IDAC: %,.2f mA. Gas Wait: %,d (ms or multiplier). (heater stable: %b, gas valid: %b).%n",
reading, data.getMeasureIndex(),
data.getTemperature(), data.getPressure(),
data.getHumidity(), data.getGasMeasurementIndex(),
data.getGasResistance(), data.getIdacHeatMA(),
data.getGasWait(), data.isHeaterTempStable(),
data.isGasMeasurementValid());
reading++;
}

Expand Down Expand Up @@ -180,24 +181,24 @@ private static void parallelModeTest(BME68x bme68x) {
if (data.isNewData()) {
if (data.isGasMeasurementValid()) {
System.out.format(
"Reading [%d]: Idx: %,d. Temperature: %,.2f C. Pressure: %,.2f hPa. Relative Humidity: %,.2f %%rH. Gas Idx: %,d. Gas Resistance: %,.2f Ohms. IDAC: %,.2f mA. Gas Wait: %,d (ms or multiplier). (heater stable: %b, gas valid: %b).%n",
Integer.valueOf(reading), Integer.valueOf(data.getMeasureIndex() & 0xff),
Float.valueOf(data.getTemperature()), Float.valueOf(data.getPressure()),
Float.valueOf(data.getHumidity()), Integer.valueOf(data.getGasMeasurementIndex()),
Float.valueOf(data.getGasResistance()), Float.valueOf(data.getIdacHeatMA()),
Short.valueOf(data.getGasWait()), Boolean.valueOf(data.isHeaterTempStable()),
Boolean.valueOf(data.isGasMeasurementValid()));
"Reading [%d]: Idx: %,d. Temperature: %,.2f C. Pressure: %,.2f hPa. Relative Humidity: %,.2f %%rH. Gas Idx: %,d. Gas Resistance: %,.2f Ohms. IDAC: %,.2f mA. Gas Wait: %,d (ms or multiplier). (heater stable: %b, gas valid: %b).%n",
reading, data.getMeasureIndex() & 0xff,
data.getTemperature(), data.getPressure(),
data.getHumidity(), data.getGasMeasurementIndex(),
data.getGasResistance(), data.getIdacHeatMA(),
data.getGasWait(), data.isHeaterTempStable(),
data.isGasMeasurementValid());
if (data.getGasMeasurementIndex() != last_gas_meas_idx) {
System.out.println("delta: " + (System.currentTimeMillis() - last_gas_meas_ms));
last_gas_meas_ms = System.currentTimeMillis();
last_gas_meas_idx = data.getGasMeasurementIndex();
}
} else {
System.out.format(
"Reading [%d]: Idx: %,d. Temperature: %,.2f C. Pressure: %,.2f hPa. Relative Humidity: %,.2f %%rH.%n",
Integer.valueOf(reading), Integer.valueOf(data.getMeasureIndex() & 0xff),
Float.valueOf(data.getTemperature()), Float.valueOf(data.getPressure()),
Float.valueOf(data.getHumidity()));
"Reading [%d]: Idx: %,d. Temperature: %,.2f C. Pressure: %,.2f hPa. Relative Humidity: %,.2f %%rH.%n",
reading, data.getMeasureIndex() & 0xff,
data.getTemperature(), data.getPressure(),
data.getHumidity());
}
}
reading++;
Expand Down Expand Up @@ -238,74 +239,47 @@ private static void iaqTest(BME68x bme68x) {
// Collect gas resistance burn-in values, then use the average of the last 50
// values to set the upper limit for calculating gas_baseline
System.out.format("Collecting gas resistance burn-in data for %,d seconds...%n",
Integer.valueOf(burn_in_time_sec));
burn_in_time_sec);
List<Float> gas_res_burn_in_data = new ArrayList<>();
List<Float> hum_burn_in_data = new ArrayList<>();
while ((System.currentTimeMillis() - start_time_ms) / 1000 < burn_in_time_sec) {
Data[] data = bme68x.getSensorData(target_operating_mode);
if (data != null && data.length > 0 && data[0].isHeaterTempStable()) {
gas_res_burn_in_data.add(Float.valueOf(data[0].getGasMeasurementIndex()));
hum_burn_in_data.add(Float.valueOf(data[0].getHumidity()));
gas_res_burn_in_data.add((float)data[0].getGasMeasurementIndex());
hum_burn_in_data.add(data[0].getHumidity());
}
SleepUtil.sleepSeconds(1);
System.out.format("Gas: %,.2f Ohms. Remaining burn-in time: %,d secs%n",
Float.valueOf(data[0].getGasResistance()),
Long.valueOf(burn_in_time_sec - (System.currentTimeMillis() - start_time_ms) / 1000));
data[0].getGasResistance(),
burn_in_time_sec - (System.currentTimeMillis() - start_time_ms) / 1000);
}

// Get the average of the last 50% of values
int num_gas_samples = gas_res_burn_in_data.size();
float gas_baseline = gas_res_burn_in_data.subList(num_gas_samples / 2, num_gas_samples).stream()
.reduce(Float.valueOf(0f), Float::sum).floatValue() / num_gas_samples / 2;
.reduce(0f, Float::sum) / num_gas_samples / 2;

// Set the humidity baseline to 40%, an optimal indoor humidity.
// float hum_baseline = 40f;
int num_hum_samples = hum_burn_in_data.size();
float hum_baseline = hum_burn_in_data.subList(num_hum_samples / 2, num_hum_samples).stream()
.reduce(Float.valueOf(0f), Float::sum).floatValue() / num_hum_samples / 2;
.reduce(0f, Float::sum) / num_hum_samples / 2;

// This sets the balance between humidity and gas reading in the calculation of
// air_quality_score (20:80, humidity:gas)
float hum_weighting = 0.2f;

System.out.format("Gas baseline: %,.2f Ohms, humidity baseline: %,.2f %%RH%n", Float.valueOf(gas_baseline),
Float.valueOf(hum_baseline));
System.out.format("Gas baseline: %,.2f Ohms, humidity baseline: %,.2f %%RH%n", gas_baseline, hum_baseline);

while (true) {
Data[] data = bme68x.getSensorData(target_operating_mode);
if (data != null && data.length > 0 && data[0].isHeaterTempStable()) {
float gas = data[0].getGasResistance();
float gas_offset = gas_baseline - gas;

float hum = data[0].getHumidity();
float hum_offset = hum - hum_baseline;

// Calculate hum_score as the distance from the hum_baseline.
float hum_score;
if (hum_offset > 0) {
hum_score = (100 - hum_baseline - hum_offset);
hum_score /= (100 - hum_baseline);
hum_score *= (hum_weighting * 100);
} else {
hum_score = (hum_baseline + hum_offset);
hum_score /= hum_baseline;
hum_score *= (hum_weighting * 100);
}

// Calculate gas_score as the distance from the gas_baseline.
float gas_score;
if (gas_offset > 0) {
gas_score = (gas / gas_baseline);
gas_score *= (100 - (hum_weighting * 100));
} else {
gas_score = 100 - (hum_weighting * 100);
}

// Calculate air_quality_score.
float air_quality_score = hum_score + gas_score;

System.out.format("Gas: %,.2f Ohms, humidity: %,.2f %%RH, air quality: %,.2f%n", Float.valueOf(gas),
Float.valueOf(hum), Float.valueOf(air_quality_score));
float air_quality_score = AirQualitySensorInterface.airQuality(gas, gas_baseline, hum, hum_baseline, hum_weighting);
System.out.format("Gas: %,.2f Ohms, humidity: %,.2f %%RH, air quality: %,.2f%n", gas, hum, air_quality_score);

SleepUtil.sleepSeconds(1);
}
Expand Down

0 comments on commit 70e95c7

Please sign in to comment.