diff --git a/diozero-core/src/main/java/com/diozero/devices/AirQualitySensorInterface.java b/diozero-core/src/main/java/com/diozero/devices/AirQualitySensorInterface.java new file mode 100644 index 00000000..27db0d31 --- /dev/null +++ b/diozero-core/src/main/java/com/diozero/devices/AirQualitySensorInterface.java @@ -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 + * indoor-air-quality.py + *

+ * 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. + *

+ * + * @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; + } + +} diff --git a/diozero-core/src/main/java/com/diozero/devices/BME68x.java b/diozero-core/src/main/java/com/diozero/devices/BME68x.java index 2780f47c..7fd176c3 100644 --- a/diozero-core/src/main/java/com/diozero/devices/BME68x.java +++ b/diozero-core/src/main/java/com/diozero/devices/BME68x.java @@ -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/. * %% @@ -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 @@ -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 @@ -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 @@ -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 @@ -910,6 +910,7 @@ public float getRelativeHumidity() { return getSensorData()[0].getHumidity(); } + @Override public float getGasResistance() { return getSensorData()[0].getGasResistance(); } @@ -2012,4 +2013,5 @@ public String toString() { + ", heaterResistance=" + heaterResistance + ", gasWaitMs=" + gasWaitMs + "]"; } } + } diff --git a/diozero-sampleapps/src/main/java/com/diozero/sampleapps/BME68xTest.java b/diozero-sampleapps/src/main/java/com/diozero/sampleapps/BME68xTest.java index 9e6bc3b3..e3d07b53 100644 --- a/diozero-sampleapps/src/main/java/com/diozero/sampleapps/BME68xTest.java +++ b/diozero-sampleapps/src/main/java/com/diozero/sampleapps/BME68xTest.java @@ -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/. * %% @@ -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 @@ -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; @@ -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()); @@ -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++; } @@ -180,13 +181,13 @@ 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(); @@ -194,10 +195,10 @@ private static void parallelModeTest(BME68x bme68x) { } } 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++; @@ -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 gas_res_burn_in_data = new ArrayList<>(); List 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); }