// 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.Buffers.Binary; using System.Device.I2c; using System.IO; using NucuCar.Sensors.Environment.Bmxx80.CalibrationData; using NucuCar.Sensors.Environment.Bmxx80.Register; using NucuCar.Sensors.Environment.Bmxx80.Units; namespace NucuCar.Sensors.Environment.Bmxx80 { /// /// Represents the core functionality of the Bmxx80 family. /// public abstract class Bmxx80Base : IDisposable { /// /// Calibration data for the sensor. /// internal Bmxx80CalibrationData _calibrationData; /// /// I2C device used to communicate with the device. /// protected I2cDevice _i2cDevice; /// /// Chosen communication protocol. /// protected CommunicationProtocol _communicationProtocol; /// /// The control register of the sensor. /// protected byte _controlRegister; /// /// Bmxx80 communication protocol. /// public enum CommunicationProtocol { /// /// I²C communication protocol. /// I2c } /// /// The variable TemperatureFine carries a fine resolution temperature value over to the /// pressure compensation formula and could be implemented as a global variable. /// protected int TemperatureFine { get; set; } /// /// The temperature calibration factor. /// protected virtual int TempCalibrationFactor => 1; private Sampling _temperatureSampling; private Sampling _pressureSampling; /// /// Initializes a new instance of the class. /// /// The ID of the device. /// The to create with. /// Thrown when the given is null. /// Thrown when the device cannot be found on the bus. protected Bmxx80Base(byte deviceId, I2cDevice i2cDevice) { _i2cDevice = i2cDevice ?? throw new ArgumentNullException(nameof(i2cDevice)); _i2cDevice.WriteByte((byte)Bmxx80Register.CHIPID); byte readSignature = _i2cDevice.ReadByte(); if (readSignature != deviceId) { throw new IOException($"Unable to find a chip with id {deviceId}"); } ReadCalibrationData(); SetDefaultConfiguration(); } /// /// Gets or sets the pressure sampling. /// /// Thrown when the is set to an undefined mode. public Sampling PressureSampling { get => _pressureSampling; set { byte status = Read8BitsFromRegister(_controlRegister); status = (byte)(status & 0b1110_0011); status = (byte)(status | (byte)value << 2); Span command = stackalloc[] { _controlRegister, status }; _i2cDevice.Write(command); _pressureSampling = value; } } /// /// Gets or sets the temperature sampling. /// /// Thrown when the is set to an undefined mode. public Sampling TemperatureSampling { get => _temperatureSampling; set { byte status = Read8BitsFromRegister(_controlRegister); status = (byte)(status & 0b0001_1111); status = (byte)(status | (byte)value << 5); Span command = stackalloc[] { _controlRegister, status }; _i2cDevice.Write(command); _temperatureSampling = value; } } /// /// When called, the device is reset using the complete power-on-reset procedure. /// The device will reset to the default configuration. /// public void Reset() { const byte resetCommand = 0xB6; Span command = stackalloc[] { (byte)Bmxx80Register.RESET, resetCommand }; _i2cDevice.Write(command); SetDefaultConfiguration(); } /// /// 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 abstract bool TryReadTemperature(out Temperature temperature); /// /// 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 abstract bool TryReadPressure(out Pressure pressure); /// /// Compensates the temperature. /// /// The temperature value read from the device. /// The . protected Temperature CompensateTemperature(int adcTemperature) { // The temperature is calculated using the compensation formula in the BMP280 datasheet. // See: https://cdn-shop.adafruit.com/datasheets/BST-BMP280-DS001-11.pdf double var1 = ((adcTemperature / 16384.0) - (_calibrationData.DigT1 / 1024.0)) * _calibrationData.DigT2; double var2 = (adcTemperature / 131072.0) - (_calibrationData.DigT1 / 8192.0); var2 *= var2 * _calibrationData.DigT3 * TempCalibrationFactor; TemperatureFine = (int)(var1 + var2); double temp = (var1 + var2) / 5120.0; return Temperature.FromCelsius(temp); } /// /// Reads an 8 bit value from a register. /// /// Register to read from. /// Value from register. protected internal byte Read8BitsFromRegister(byte register) { if (_communicationProtocol == CommunicationProtocol.I2c) { _i2cDevice.WriteByte(register); byte value = _i2cDevice.ReadByte(); return value; } else { throw new NotImplementedException(); } } /// /// Reads a 16 bit value over I2C. /// /// Register to read from. /// Interpretation of the bytes (big or little endian). /// Value from register. protected internal ushort Read16BitsFromRegister(byte register, Endianness endianness = Endianness.LittleEndian) { Span bytes = stackalloc byte[2]; switch (_communicationProtocol) { case CommunicationProtocol.I2c: _i2cDevice.WriteByte(register); _i2cDevice.Read(bytes); break; default: throw new NotImplementedException(); } return endianness switch { Endianness.LittleEndian => BinaryPrimitives.ReadUInt16LittleEndian(bytes), Endianness.BigEndian => BinaryPrimitives.ReadUInt16BigEndian(bytes), _ => throw new ArgumentOutOfRangeException(nameof(endianness), endianness, null) }; } /// /// Reads a 24 bit value over I2C. /// /// Register to read from. /// Interpretation of the bytes (big or little endian). /// Value from register. protected internal uint Read24BitsFromRegister(byte register, Endianness endianness = Endianness.LittleEndian) { Span bytes = stackalloc byte[4]; switch (_communicationProtocol) { case CommunicationProtocol.I2c: _i2cDevice.WriteByte(register); _i2cDevice.Read(bytes.Slice(1)); break; default: throw new NotImplementedException(); } return endianness switch { Endianness.LittleEndian => BinaryPrimitives.ReadUInt32LittleEndian(bytes), Endianness.BigEndian => BinaryPrimitives.ReadUInt32BigEndian(bytes), _ => throw new ArgumentOutOfRangeException(nameof(endianness), endianness, null) }; } /// /// Converts byte to . /// /// Value to convert. /// protected Sampling ByteToSampling(byte value) { // Values >=5 equals UltraHighResolution. if (value >= 5) { return Sampling.UltraHighResolution; } return (Sampling)value; } /// /// Sets the default configuration for the sensor. /// protected virtual void SetDefaultConfiguration() { PressureSampling = Sampling.UltraLowPower; TemperatureSampling = Sampling.UltraLowPower; } /// /// Specifies the Endianness of a device. /// protected internal enum Endianness { /// /// Indicates little endian. /// LittleEndian, /// /// Indicates big endian. /// BigEndian } private void ReadCalibrationData() { switch (this) { case NucuCar.Sensors.Environment.Bmxx80.Bme280 _: _calibrationData = new Bme280CalibrationData(); _controlRegister = (byte)Bmx280Register.CTRL_MEAS; break; case NucuCar.Sensors.Environment.Bmxx80.Bmp280 _: _calibrationData = new Bmp280CalibrationData(); _controlRegister = (byte)Bmx280Register.CTRL_MEAS; break; case NucuCar.Sensors.Environment.Bmxx80.Bme680 _: _calibrationData = new Bme680CalibrationData(); _controlRegister = (byte)Bme680Register.CTRL_MEAS; break; } _calibrationData.ReadFromDevice(this); } /// /// Cleanup. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases the unmanaged resources used by the Bmxx80 and optionally releases the managed resources. /// /// True to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { _i2cDevice?.Dispose(); _i2cDevice = null; } } }