// 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;
}
}
}