Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extracts air-quality calculations for general use. #207

Merged
merged 2 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading