// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; using System.Device.I2c; using System.Linq; using NucuCar.Sensors.Modules.Environment.Bmxx80.CalibrationData; using NucuCar.Sensors.Modules.Environment.Bmxx80.FilteringMode; using NucuCar.Sensors.Modules.Environment.Bmxx80.PowerMode; using NucuCar.Sensors.Modules.Environment.Bmxx80.Register; using NucuCar.Sensors.Modules.Environment.Bmxx80.Units; namespace NucuCar.Sensors.Modules.Environment.Bmxx80 { /// /// Represents a BME680 temperature, pressure, relative humidity and VOC gas sensor. /// public class Bme680 : Bmxx80Base { /// /// Default I2C bus address. /// public const byte DefaultI2cAddress = 0x76; /// /// Secondary I2C bus address. /// public const byte SecondaryI2cAddress = 0x77; /// /// The expected chip ID of the BME680. /// private const byte DeviceId = 0x61; /// /// Calibration data for the . /// private Bme680CalibrationData _bme680Calibration; /// protected override int TempCalibrationFactor => 16; private readonly List _heaterConfigs = new List(); private bool _gasConversionIsEnabled; private bool _heaterIsEnabled; private Bme680HeaterProfile _heaterProfile; private Bme680FilteringMode _filterMode; private Sampling _humiditySampling; private static readonly byte[] s_osToMeasCycles = { 0, 1, 2, 4, 8, 16 }; private static readonly byte[] s_osToSwitchCount = { 0, 1, 1, 1, 1, 1 }; private static readonly double[] s_k1Lookup = { 0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, -0.8, 0.0, 0.0, -0.2, -0.5, 0.0, -1.0, 0.0, 0.0 }; private static readonly double[] s_k2Lookup = { 0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 0.0, -0.8, -0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }; /// /// Initialize a new instance of the class. /// /// The to create with. public Bme680(I2cDevice i2cDevice) : base(DeviceId, i2cDevice) { _communicationProtocol = CommunicationProtocol.I2c; } /// /// Gets or sets the humidity sampling. /// /// Thrown when the is set to an undefined mode. public Sampling HumiditySampling { get => _humiditySampling; set { if (!Enum.IsDefined(typeof(Sampling), value)) { throw new ArgumentOutOfRangeException(); } var status = Read8BitsFromRegister((byte)Bme680Register.CTRL_HUM); status = (byte)((status & (byte)~Bme680Mask.HUMIDITY_SAMPLING) | (byte)value); Span command = stackalloc[] { (byte)Bme680Register.CTRL_HUM, status }; _i2cDevice.Write(command); _humiditySampling = value; } } /// /// Gets or sets the heater profile to be used for measurements. /// Current heater profile is only set if the chosen profile is configured. /// /// Thrown when the is set to an undefined profile. public Bme680HeaterProfile HeaterProfile { get => _heaterProfile; set { if (_heaterConfigs.Exists(config => config.HeaterProfile == value)) { if (!Enum.IsDefined(typeof(Bme680HeaterProfile), value)) { throw new ArgumentOutOfRangeException(); } var heaterProfile = Read8BitsFromRegister((byte)Bme680Register.CTRL_GAS_1); heaterProfile = (byte)((heaterProfile & (byte)~Bme680Mask.NB_CONV) | (byte)value); Span command = stackalloc[] { (byte)Bme680Register.CTRL_GAS_1, heaterProfile }; _i2cDevice.Write(command); _heaterProfile = value; } } } /// /// Gets or sets the filtering mode to be used for measurements. /// /// Thrown when the is set to an undefined mode. public Bme680FilteringMode FilterMode { get => _filterMode; set { if (!Enum.IsDefined(typeof(Bme680FilteringMode), value)) { throw new ArgumentOutOfRangeException(); } var filter = Read8BitsFromRegister((byte)Bme680Register.CONFIG); filter = (byte)((filter & (byte)~Bme680Mask.FILTER_COEFFICIENT) | (byte)value << 2); Span command = stackalloc[] { (byte)Bme680Register.CONFIG, filter }; _i2cDevice.Write(command); _filterMode = value; } } /// /// Gets or sets whether the heater is enabled. /// public bool HeaterIsEnabled { get => _heaterIsEnabled; set { var heaterStatus = Read8BitsFromRegister((byte)Bme680Register.CTRL_GAS_0); heaterStatus = (byte)((heaterStatus & (byte)~Bme680Mask.HEAT_OFF) | Convert.ToByte(!value) << 3); Span command = stackalloc[] { (byte)Bme680Register.CTRL_GAS_0, heaterStatus }; _i2cDevice.Write(command); _heaterIsEnabled = value; } } /// /// Gets or sets whether gas conversions are enabled. /// public bool GasConversionIsEnabled { get => _gasConversionIsEnabled; set { var gasConversion = Read8BitsFromRegister((byte)Bme680Register.CTRL_GAS_1); gasConversion = (byte)((gasConversion & (byte)~Bme680Mask.RUN_GAS) | Convert.ToByte(value) << 4); Span command = stackalloc[] { (byte)Bme680Register.CTRL_GAS_1, gasConversion }; _i2cDevice.Write(command); _gasConversionIsEnabled = value; } } /// /// Reads whether new data is available. /// public bool ReadNewDataIsAvailable() { var newData = Read8BitsFromRegister((byte)Bme680Register.STATUS); newData = (byte)(newData >> 7); return Convert.ToBoolean(newData); } /// /// Reads whether a gas measurement is in process. /// public bool ReadGasMeasurementInProcess() { var gasMeasInProcess = Read8BitsFromRegister((byte)Bme680Register.STATUS); gasMeasInProcess = (byte)((gasMeasInProcess & (byte)Bme680Mask.GAS_MEASURING) >> 6); return Convert.ToBoolean(gasMeasInProcess); } /// /// Reads whether a measurement of any kind is in process. /// public bool ReadMeasurementInProcess() { var measInProcess = Read8BitsFromRegister((byte)Bme680Register.STATUS); measInProcess = (byte)((measInProcess & (byte)Bme680Mask.MEASURING) >> 5); return Convert.ToBoolean(measInProcess); } /// /// Reads whether the target heater temperature is reached. /// public bool ReadHeaterIsStable() { var heaterStable = Read8BitsFromRegister((byte)Bme680Register.GAS_RANGE); heaterStable = (byte)((heaterStable & (byte)Bme680Mask.HEAT_STAB) >> 4); return Convert.ToBoolean(heaterStable); } /// /// Sets the power mode to the given mode /// /// The to set. /// Thrown when the power mode does not match a defined mode in . public void SetPowerMode(Bme680PowerMode powerMode) { if (!Enum.IsDefined(typeof(Bme680PowerMode), powerMode)) { throw new ArgumentOutOfRangeException(); } var status = Read8BitsFromRegister((byte)Bme680Register.CTRL_MEAS); status = (byte)((status & (byte)~Bme680Mask.PWR_MODE) | (byte)powerMode); Span command = stackalloc[] { (byte)Bme680Register.CTRL_MEAS, status }; _i2cDevice.Write(command); } /// /// Configures a heater profile, making it ready for use. /// /// The to configure. /// The target temperature in °C. Ranging from 0-400. /// The duration in ms. Ranging from 0-4032. /// The ambient temperature in °C. /// Thrown when the heating profile does not match a defined profile in . public void ConfigureHeatingProfile(Bme680HeaterProfile profile, ushort targetTemperature, ushort duration, double ambientTemperature) { if (!Enum.IsDefined(typeof(Bme680HeaterProfile), profile)) { throw new ArgumentOutOfRangeException(); } // read ambient temperature for resistance calculation var heaterResistance = CalculateHeaterResistance(targetTemperature, (short)ambientTemperature); var heaterDuration = CalculateHeaterDuration(duration); Span resistanceCommand = stackalloc[] { (byte)((byte)Bme680Register.RES_HEAT_0 + profile), heaterResistance }; Span durationCommand = stackalloc[] { (byte)((byte)Bme680Register.GAS_WAIT_0 + profile), heaterDuration }; _i2cDevice.Write(resistanceCommand); _i2cDevice.Write(durationCommand); // cache heater configuration if (_heaterConfigs.Exists(config => config.HeaterProfile == profile)) { _heaterConfigs.Remove(_heaterConfigs.Single(config => config.HeaterProfile == profile)); } _heaterConfigs.Add(new Bme680HeaterProfileConfig(profile, heaterResistance, duration)); } /// /// Read the state. /// /// The current . public Bme680PowerMode ReadPowerMode() { var status = Read8BitsFromRegister((byte)Bme680Register.CTRL_MEAS); return (Bme680PowerMode)(status & (byte)Bme680Mask.PWR_MODE); } /// /// Gets the required time in ms to perform a measurement. The duration of the gas /// measurement is not considered if is set to false /// or the chosen is not configured. /// The precision of this duration is within 1ms of the actual measurement time. /// /// The used . /// public int GetMeasurementDuration(Bme680HeaterProfile profile) { var measCycles = s_osToMeasCycles[(int)TemperatureSampling]; measCycles += s_osToMeasCycles[(int)PressureSampling]; measCycles += s_osToMeasCycles[(int)HumiditySampling]; var switchCount = s_osToSwitchCount[(int)TemperatureSampling]; switchCount += s_osToSwitchCount[(int)PressureSampling]; switchCount += s_osToSwitchCount[(int)HumiditySampling]; double measDuration = measCycles * 1963; measDuration += 477 * switchCount; // TPH switching duration if (GasConversionIsEnabled) { measDuration += 477 * 5; // Gas measurement duration } measDuration += 500; // get it to the closest whole number measDuration /= 1000.0; // convert to ms measDuration += 1; // wake up duration of 1ms if (GasConversionIsEnabled && _heaterConfigs.Exists(config => config.HeaterProfile == profile)) { measDuration += _heaterConfigs.Single(config => config.HeaterProfile == profile).HeaterDuration; } return (int)Math.Ceiling(measDuration); } /// /// Reads the humidity. A return value indicates whether the reading succeeded. /// /// /// Contains the measured humidity as %rH if the was not set to . /// Contains otherwise. /// /// true if measurement was not skipped, otherwise false. public bool TryReadHumidity(out double humidity) { if (HumiditySampling == Sampling.Skipped) { humidity = double.NaN; return false; } // Read humidity data. var hum = Read16BitsFromRegister((byte)Bme680Register.HUMIDITYDATA, Endianness.BigEndian); TryReadTemperature(out _); humidity = CompensateHumidity(hum); return true; } /// /// Reads the pressure. A return value indicates whether the reading succeeded. /// /// /// Contains the measured pressure if the was not set to . /// Contains otherwise. /// /// true if measurement was not skipped, otherwise false. public override bool TryReadPressure(out Pressure pressure) { if (PressureSampling == Sampling.Skipped) { pressure = Pressure.FromPascal(double.NaN); return false; } // Read pressure data. var press = (int)Read24BitsFromRegister((byte)Bme680Register.PRESSUREDATA, Endianness.BigEndian); // Read the temperature first to load the t_fine value for compensation. TryReadTemperature(out _); pressure = CompensatePressure(press >> 4); return true; } /// /// Reads the temperature. A return value indicates whether the reading succeeded. /// /// /// Contains the measured temperature if the was not set to . /// Contains otherwise. /// /// true if measurement was not skipped, otherwise false. public override bool TryReadTemperature(out Temperature temperature) { if (TemperatureSampling == Sampling.Skipped) { temperature = Temperature.FromCelsius(double.NaN); return false; } var temp = (int)Read24BitsFromRegister((byte)Bme680Register.TEMPDATA, Endianness.BigEndian); temperature = CompensateTemperature(temp >> 4); return true; } /// /// Reads the gas resistance. A return value indicates whether the reading succeeded. /// /// /// Contains the measured gas resistance in Ohm if the heater module reached the target temperature and /// the measurement was valid. Contains otherwise. /// /// true if measurement was not skipped, otherwise false. public bool TryReadGasResistance(out double gasResistance) { if (!ReadGasMeasurementIsValid() || !ReadHeaterIsStable()) { gasResistance = double.NaN; return false; } // Read 10 bit gas resistance value from registers var gasResRaw = Read8BitsFromRegister((byte)Bme680Register.GAS_RES); var gasRange = Read8BitsFromRegister((byte)Bme680Register.GAS_RANGE); var gasRes = (ushort)((ushort)(gasResRaw << 2) + (byte)(gasRange >> 6)); gasRange &= (byte)Bme680Mask.GAS_RANGE; gasResistance = CalculateGasResistance(gasRes, gasRange); return true; } /// /// Sets the default configuration for the sensor. /// protected override void SetDefaultConfiguration() { base.SetDefaultConfiguration(); HumiditySampling = Sampling.UltraLowPower; FilterMode = Bme680FilteringMode.C0; _bme680Calibration = (Bme680CalibrationData)_calibrationData; TryReadTemperature(out var temp); ConfigureHeatingProfile(Bme680HeaterProfile.Profile1, 320, 150, temp.Celsius); HeaterProfile = Bme680HeaterProfile.Profile1; HeaterIsEnabled = true; GasConversionIsEnabled = true; } /// /// Compensates the humidity. /// /// The humidity value read from the device. /// The percentage relative humidity. private double CompensateHumidity(int adcHumidity) { // Calculate the humidity. var temperature = TemperatureFine / 5120.0; var var1 = adcHumidity - ((_bme680Calibration.DigH1 * 16.0) + ((_bme680Calibration.DigH3 / 2.0) * temperature)); var var2 = var1 * ((_bme680Calibration.DigH2 / 262144.0) * (1.0 + ((_bme680Calibration.DigH4 / 16384.0) * temperature) + ((_bme680Calibration.DigH5 / 1048576.0) * temperature * temperature))); var var3 = _bme680Calibration.DigH6 / 16384.0; var var4 = _bme680Calibration.DigH7 / 2097152.0; var calculatedHumidity = var2 + ((var3 + (var4 * temperature)) * var2 * var2); if (calculatedHumidity > 100.0) { calculatedHumidity = 100.0; } else if (calculatedHumidity < 0.0) { calculatedHumidity = 0.0; } return calculatedHumidity; } /// /// Compensates the pressure. /// /// The pressure value read from the device. /// The pressure in Pa. private Pressure CompensatePressure(long adcPressure) { // Calculate the pressure. var var1 = (TemperatureFine / 2.0) - 64000.0; var var2 = var1 * var1 * (_bme680Calibration.DigP6 / 131072.0); var2 += (var1 * _bme680Calibration.DigP5 * 2.0); var2 = (var2 / 4.0) + (_bme680Calibration.DigP4 * 65536.0); var1 = ((_bme680Calibration.DigP3 * var1 * var1 / 16384.0) + (_bme680Calibration.DigP2 * var1)) / 524288.0; var1 = (1.0 + (var1 / 32768.0)) * _bme680Calibration.DigP1; var calculatedPressure = 1048576.0 - adcPressure; // Avoid exception caused by division by zero. if (var1 != 0) { calculatedPressure = (calculatedPressure - (var2 / 4096.0)) * 6250.0 / var1; var1 = _bme680Calibration.DigP9 * calculatedPressure * calculatedPressure / 2147483648.0; var2 = calculatedPressure * (_bme680Calibration.DigP8 / 32768.0); var var3 = (calculatedPressure / 256.0) * (calculatedPressure / 256.0) * (calculatedPressure / 256.0) * (_bme680Calibration.DigP10 / 131072.0); calculatedPressure += (var1 + var2 + var3 + (_bme680Calibration.DigP7 * 128.0)) / 16.0; } else { calculatedPressure = 0; } return Pressure.FromPascal(calculatedPressure); } private bool ReadGasMeasurementIsValid() { var gasMeasValid = Read8BitsFromRegister((byte)Bme680Register.GAS_RANGE); gasMeasValid = (byte)((gasMeasValid & (byte)Bme680Mask.GAS_VALID) >> 5); return Convert.ToBoolean(gasMeasValid); } private double CalculateGasResistance(ushort adcGasRes, byte gasRange) { var var1 = 1340.0 + 5.0 * _bme680Calibration.RangeSwErr; var var2 = var1 * (1.0 + s_k1Lookup[gasRange] / 100.0); var var3 = 1.0 + s_k2Lookup[gasRange] / 100.0; var gasResistance = 1.0 / (var3 * 0.000000125 * (1 << gasRange) * ((adcGasRes - 512.0) / var2 + 1.0)); return gasResistance; } private byte CalculateHeaterResistance(ushort setTemp, short ambientTemp) { // limit maximum temperature to 400°C if (setTemp > 400) { setTemp = 400; } var var1 = _bme680Calibration.DigGh1 / 16.0 + 49.0; var var2 = _bme680Calibration.DigGh2 / 32768.0 * 0.0005 + 0.00235; var var3 = _bme680Calibration.DigGh3 / 1024.0; var var4 = var1 * (1.0 + var2 * setTemp); var var5 = var4 + var3 * ambientTemp; var heaterResistance = (byte)(3.4 * (var5 * (4.0 / (4.0 + _bme680Calibration.ResHeatRange)) * (1.0 / (1.0 + _bme680Calibration.ResHeatVal * 0.002)) - 25)); return heaterResistance; } // The duration is interpreted as follows: // Byte [7:6]: multiplication factor of 1, 4, 16 or 64 // Byte [5:0]: 64 timer values, 1ms step size // Values are rounded down private byte CalculateHeaterDuration(ushort duration) { byte factor = 0; byte durationValue; // check if value exceeds maximum duration if (duration > 0xFC0) { durationValue = 0xFF; } else { while (duration > 0x3F) { duration = (ushort)(duration >> 2); factor += 1; } durationValue = (byte)(duration + factor * 64); } return durationValue; } } }