NucuCar/NucuCar.Sensors/Environment/Bmxx80/Bmxx80Base.cs

346 lines
13 KiB
C#
Raw Normal View History

// 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
{
/// <summary>
/// Represents the core functionality of the Bmxx80 family.
/// </summary>
public abstract class Bmxx80Base : IDisposable
{
/// <summary>
/// Calibration data for the sensor.
/// </summary>
internal Bmxx80CalibrationData _calibrationData;
/// <summary>
/// I2C device used to communicate with the device.
/// </summary>
protected I2cDevice _i2cDevice;
/// <summary>
/// Chosen communication protocol.
/// </summary>
protected CommunicationProtocol _communicationProtocol;
/// <summary>
/// The control register of the sensor.
/// </summary>
protected byte _controlRegister;
/// <summary>
/// Bmxx80 communication protocol.
/// </summary>
public enum CommunicationProtocol
{
/// <summary>
/// I²C communication protocol.
/// </summary>
I2c
}
/// <summary>
/// The variable TemperatureFine carries a fine resolution temperature value over to the
/// pressure compensation formula and could be implemented as a global variable.
/// </summary>
protected int TemperatureFine
{
get;
set;
}
/// <summary>
/// The temperature calibration factor.
/// </summary>
protected virtual int TempCalibrationFactor => 1;
private Sampling _temperatureSampling;
private Sampling _pressureSampling;
/// <summary>
/// Initializes a new instance of the <see cref="Bmxx80Base"/> class.
/// </summary>
/// <param name="deviceId">The ID of the device.</param>
/// <param name="i2cDevice">The <see cref="I2cDevice"/> to create with.</param>
/// <exception cref="ArgumentNullException">Thrown when the given <see cref="I2cDevice"/> is null.</exception>
/// <exception cref="IOException">Thrown when the device cannot be found on the bus.</exception>
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();
}
/// <summary>
/// Gets or sets the pressure sampling.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the <see cref="Sampling"/> is set to an undefined mode.</exception>
public Sampling PressureSampling
{
get => _pressureSampling;
set
{
byte status = Read8BitsFromRegister(_controlRegister);
status = (byte)(status & 0b1110_0011);
status = (byte)(status | (byte)value << 2);
Span<byte> command = stackalloc[]
{
_controlRegister, status
};
_i2cDevice.Write(command);
_pressureSampling = value;
}
}
/// <summary>
/// Gets or sets the temperature sampling.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the <see cref="Sampling"/> is set to an undefined mode.</exception>
public Sampling TemperatureSampling
{
get => _temperatureSampling;
set
{
byte status = Read8BitsFromRegister(_controlRegister);
status = (byte)(status & 0b0001_1111);
status = (byte)(status | (byte)value << 5);
Span<byte> command = stackalloc[]
{
_controlRegister, status
};
_i2cDevice.Write(command);
_temperatureSampling = value;
}
}
/// <summary>
/// When called, the device is reset using the complete power-on-reset procedure.
/// The device will reset to the default configuration.
/// </summary>
public void Reset()
{
const byte resetCommand = 0xB6;
Span<byte> command = stackalloc[]
{
(byte)Bmxx80Register.RESET, resetCommand
};
_i2cDevice.Write(command);
SetDefaultConfiguration();
}
/// <summary>
/// Reads the temperature. A return value indicates whether the reading succeeded.
/// </summary>
/// <param name="temperature">
/// Contains the measured temperature if the <see cref="TemperatureSampling"/> was not set to <see cref="Sampling.Skipped"/>.
/// Contains <see cref="double.NaN"/> otherwise.
/// </param>
/// <returns><code>true</code> if measurement was not skipped, otherwise <code>false</code>.</returns>
public abstract bool TryReadTemperature(out Temperature temperature);
/// <summary>
/// Reads the pressure. A return value indicates whether the reading succeeded.
/// </summary>
/// <param name="pressure">
/// Contains the measured pressure if the <see cref="PressureSampling"/> was not set to <see cref="Sampling.Skipped"/>.
/// Contains <see cref="double.NaN"/> otherwise.
/// </param>
/// <returns><code>true</code> if measurement was not skipped, otherwise <code>false</code>.</returns>
public abstract bool TryReadPressure(out Pressure pressure);
/// <summary>
/// Compensates the temperature.
/// </summary>
/// <param name="adcTemperature">The temperature value read from the device.</param>
/// <returns>The <see cref="Temperature"/>.</returns>
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);
}
/// <summary>
/// Reads an 8 bit value from a register.
/// </summary>
/// <param name="register">Register to read from.</param>
/// <returns>Value from register.</returns>
protected internal byte Read8BitsFromRegister(byte register)
{
if (_communicationProtocol == CommunicationProtocol.I2c)
{
_i2cDevice.WriteByte(register);
byte value = _i2cDevice.ReadByte();
return value;
}
else
{
throw new NotImplementedException();
}
}
/// <summary>
/// Reads a 16 bit value over I2C.
/// </summary>
/// <param name="register">Register to read from.</param>
/// <param name="endianness">Interpretation of the bytes (big or little endian).</param>
/// <returns>Value from register.</returns>
protected internal ushort Read16BitsFromRegister(byte register, Endianness endianness = Endianness.LittleEndian)
{
Span<byte> 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)
};
}
/// <summary>
/// Reads a 24 bit value over I2C.
/// </summary>
/// <param name="register">Register to read from.</param>
/// <param name="endianness">Interpretation of the bytes (big or little endian).</param>
/// <returns>Value from register.</returns>
protected internal uint Read24BitsFromRegister(byte register, Endianness endianness = Endianness.LittleEndian)
{
Span<byte> 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)
};
}
/// <summary>
/// Converts byte to <see cref="Sampling"/>.
/// </summary>
/// <param name="value">Value to convert.</param>
/// <returns><see cref="Sampling"/></returns>
protected Sampling ByteToSampling(byte value)
{
// Values >=5 equals UltraHighResolution.
if (value >= 5)
{
return Sampling.UltraHighResolution;
}
return (Sampling)value;
}
/// <summary>
/// Sets the default configuration for the sensor.
/// </summary>
protected virtual void SetDefaultConfiguration()
{
PressureSampling = Sampling.UltraLowPower;
TemperatureSampling = Sampling.UltraLowPower;
}
/// <summary>
/// Specifies the Endianness of a device.
/// </summary>
protected internal enum Endianness
{
/// <summary>
/// Indicates little endian.
/// </summary>
LittleEndian,
/// <summary>
/// Indicates big endian.
/// </summary>
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);
}
/// <summary>
/// Cleanup.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases the unmanaged resources used by the Bmxx80 and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">True to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
_i2cDevice?.Dispose();
_i2cDevice = null;
}
}
}