diff --git a/src/devices/README.md b/src/devices/README.md index 248831a22c19247027cd5f246ca59722dc396424..b3d540e749f2350995f71c7f5888e9560053ce62 100755 --- a/src/devices/README.md +++ b/src/devices/README.md @@ -37,7 +37,8 @@ Our vision: the majority of .NET bindings are written completely in .NET languag * [AGS01DB - MEMS VOC Gas Sensor](Ags01db/README.md) * [BMxx80 Device Family](Bmxx80/README.md) * [CCS811 Gas sensor](Ccs811/README.md) -* [MH-Z19B CO2-Sensor](Mhz19b/README.md) +* [MH-Z19B CO2 sensor](Mhz19b/README.md) +* [SCD4x - CO2, humidity, temperature sensor](Scd4x/README.md) ### Liquid sensors @@ -57,6 +58,7 @@ Our vision: the majority of .NET bindings are written completely in .NET languag * [BMP180 - barometer, altitude and temperature sensor](Bmp180/README.md) * [BMxx80 Device Family](Bmxx80/README.md) +* [SCD4x - CO2, humidity, temperature sensor](Scd4x/README.md) * [LPS25H - Piezoresistive pressure and thermometer sensor](Lps25h/README.md) * [Sense HAT](SenseHat/README.md) * [SensorHub - Environmental sensor](SensorHub/README.md) @@ -84,7 +86,9 @@ Our vision: the majority of .NET bindings are written completely in .NET languag * [OpenHardwareMonitor client library](HardwareMonitor/README.md) * [Sense HAT](SenseHat/README.md) * [SensorHub - Environmental sensor](SensorHub/README.md) +* [SCD4x - CO2, humidity, temperature sensor](Scd4x/README.md) * [SHT3x - Temperature & Humidity Sensor](Sht3x/README.md) +* [SHT4x - Temperature & Humidity Sensor](Sht4x/README.md) * [SHTC3 - Temperature & Humidity Sensor](Shtc3/README.md) * [Si7021 - Temperature & Humidity Sensor](Si7021/README.md) * [μFire ISE Probe - pH, ORP and temperature sensor](UFireIse/README.md) diff --git a/src/devices/Scd4x/README.md b/src/devices/Scd4x/README.md new file mode 100644 index 0000000000000000000000000000000000000000..745bce85a5500a3503631e163d3a4dcb3d995c25 --- /dev/null +++ b/src/devices/Scd4x/README.md @@ -0,0 +1,110 @@ +# SCD4x - CO2, Temperature & Humidity Sensor + +SCD4x is a CO2, temperature & humidity sensor from Sensirion. This project supports the SCD40 and SCD41 sensors. + +## Documentation + +- SCD4x [datasheet](https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/9.5_CO2/Sensirion_CO2_Sensors_SCD4x_Datasheet.pdf) + +## Usage + +### Hardware Required + +- SCD40. + +### Code (telemetry / properties) + +Less efficient, but simple to use and compatible with telemetry system. + +```csharp +I2cConnectionSettings settings = + new I2cConnectionSettings(1, Scd4x.DefaultI2cAddress); + +using I2cDevice device = I2cDevice.Create(settings); +using Scd4x sensor = new Scd4x(device); + +while (true) +{ + // Reading more than once per measurement + // period will result in duplicate values. + Thread.Sleep(Scd4x.MeasurementPeriod); + + // read co2 (PPM) + double co2 = sensor.Co2.PartsPerMillion; + // read temperature (℃) + double temperature = sensor.Temperature.Celsius; + // read humidity (%) + double humidity = sensor.RelativeHumidity.Percent; +} +``` + +### Code (synchronous) + +```csharp +I2cConnectionSettings settings = + new I2cConnectionSettings(1, Scd4x.DefaultI2cAddress); + +using I2cDevice device = I2cDevice.Create(settings); +using Scd4x sensor = new Scd4x(device); + +while (true) +{ + // Read the measurement. + // This call will block until the next measurement period. + (VolumeConcentration? co2, RelativeHumidity? hum, Temperature? temp) = + sensor.ReadPeriodicMeasurement(); + + if (co2 is null || hum is null || temp is null) + { + throw new Exception("CRC failure"); + } + + // read co2 (PPM) + double co2 = co2.Value.PartsPerMillion; + // read temperature (℃) + double temperature = temp.Value.Celsius; + // read humidity (%) + double humidity = hum.Value.Percent; +} +``` + +### Calibrating pressure + +Giving the device the current barometric pressure will increase accuracy until reset. + +```c# +Scd4x sensor = ...; +Pressure currentPressure = Pressure.FromKilopascals(100); + +sensor.SetPressureCalibration(currentPressure); +``` + +### Code (asynchronous) + +```csharp +I2cConnectionSettings settings = + new I2cConnectionSettings(1, Scd4x.DefaultI2cAddress); + +I2cDevice device = I2cDevice.Create(settings); +Scd4x sensor = new Scd4x(device); + +while (true) +{ + // Read the measurement. + // This async operation will not finish until the next measurement period. + (VolumeConcentration? co2, RelativeHumidity? hum, Temperature? temp) = + await sensor.ReadPeriodicMeasurementAsync(); + + if (co2 is null || hum is null || temp is null) + { + throw new Exception("CRC failure"); + } + + // read co2 (PPM) + double co2 = co2.Value.PartsPerMillion; + // read temperature (℃) + double temperature = temp.Value.Celsius; + // read humidity (%) + double humidity = hum.Value.Percent; +} +``` \ No newline at end of file diff --git a/src/devices/Scd4x/Scd4x.cs b/src/devices/Scd4x/Scd4x.cs new file mode 100644 index 0000000000000000000000000000000000000000..f9ecc0d2620d27b8eb2efe364b5d9cca18cd8f4c --- /dev/null +++ b/src/devices/Scd4x/Scd4x.cs @@ -0,0 +1,326 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers.Binary; +using System.Device.I2c; +using System.Device.Model; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using UnitsNet; + +namespace Iot.Device.Scd4x +{ + /// + /// CO₂, Humidity, and Temperature sensor SCD4x + /// + [Interface("CO₂, Humidity, and Temperature sensor SCD4x")] + public sealed class Scd4x : IDisposable + { + private const int MeasurementPeriodMs = 5000; + + /// + /// The default I²C address of this device. + /// + public const int DefaultI2cAddress = 0x62; + + /// + /// The period to wait for each measurement: five seconds. + /// + public static TimeSpan MeasurementPeriod => TimeSpan.FromTicks(TimeSpan.TicksPerMillisecond * MeasurementPeriodMs); + + private static ReadOnlySpan StartPeriodicMeasurementBytes => new byte[] { 0x21, 0xB1 }; + private static ReadOnlySpan CheckDataReadyStatusBytes => new byte[] { 0xE4, 0xB8 }; + private static ReadOnlySpan ReadPeriodicMeasurementBytes => new byte[] { 0xEC, 0x05 }; + private static ReadOnlySpan StopPeriodicMeasurementBytes => new byte[] { 0x3F, 0x86 }; + private static ReadOnlySpan ReInitBytes => new byte[] { 0x36, 0x46 }; + + private readonly I2cDevice _device; + private VolumeConcentration _lastCo2; + private RelativeHumidity _lastHum; + private Temperature _lastTemp; + private int _nextReadPeriod; + private bool _started; + + /// + /// The most recent CO₂ measurement. + /// + [Telemetry] + public VolumeConcentration Co2 + { + get + { + RefreshIfInNextPeriod(); + return _lastCo2; + } + } + + /// + /// The most recent relative humidity measurement. + /// + [Telemetry] + public RelativeHumidity RelativeHumidity + { + get + { + RefreshIfInNextPeriod(); + return _lastHum; + } + } + + /// + /// The most recent temperature measurement. + /// + [Telemetry] + public Temperature Temperature + { + get + { + RefreshIfInNextPeriod(); + return _lastTemp; + } + } + + /// + /// Instantiates a new . + /// + /// The I²C device to operate on. + public Scd4x(I2cDevice device) + { + _device = device; + Reset(); + } + + /// + public void Dispose() + { + if (_started) + { + StopPeriodicMeasurements(); + } + + _device.Dispose(); + } + + private void RefreshIfInNextPeriod() + { + if (!_started || _nextReadPeriod - Environment.TickCount <= 0) + { + _ = ReadPeriodicMeasurement(); + } + } + + /// + /// Resets the device. + /// + public void Reset() + { + StopPeriodicMeasurements(); + + _device.Write(ReInitBytes); + Thread.Sleep(20); + } + + /// + /// Calibrates the sensor to operate at a specific barometric pressure. + /// + /// The pressure to use when calibrating the sensor. + public void SetPressureCalibration(Pressure pressure) + { + int delay = SetPressureCalibrationImpl(pressure); + Thread.Sleep(delay); + } + + /// + public Task SetPressureCalibrationAsync(Pressure pressure) + { + try + { + int delay = SetPressureCalibrationImpl(pressure); + return Task.Delay(delay); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + private int SetPressureCalibrationImpl(Pressure pressure) + { + Span buffer = stackalloc byte[5]; + + BinaryPrimitives.WriteUInt16BigEndian(buffer, 0xE000); + Sensirion.WriteUInt16BigEndianAndCRC8(buffer.Slice(2), (ushort)(Math.Max(0.0, Math.Min(pressure.Pascals, 1.0)) * (1.0 / 100.0))); + + _device.Write(buffer); + return 1; + } + + /// + /// + /// Instructs the sensor to start performing periodic measurements. + /// + /// + /// + /// Every period, the length of which is available in , the measurement can then be read via . + /// + /// + /// + /// Periodic measurement can be stopped with . + /// + /// + public void StartPeriodicMeasurements() + { + _device.Write(StartPeriodicMeasurementBytes); + _nextReadPeriod = Environment.TickCount + MeasurementPeriodMs; + _started = true; + } + + /// + /// + /// Reads the next periodic CO₂, humidity, and temperature measurement from the sensor. + /// + /// + /// + /// A tuple of CO₂, humidity, and temperature. + /// If a CRC check failed for a measurement, it will be . + /// + public (VolumeConcentration? CarbonDioxide, RelativeHumidity? RelativeHumidity, Temperature? Temperature) ReadPeriodicMeasurement() + { + var ret = ReadPeriodicMeasurementImpl(CancellationToken.None, async: false); + + Debug.Assert(ret.IsCompleted, "An async=false call should complete synchronously."); + return ret.Result; + } + + /// + public ValueTask<(VolumeConcentration? CarbonDioxide, RelativeHumidity? RelativeHumidity, Temperature? Temperature)> ReadPeriodicMeasurementAsync(CancellationToken cancellationToken = default) => + ReadPeriodicMeasurementImpl(cancellationToken, async: true); + + private async ValueTask<(VolumeConcentration? CarbonDioxide, RelativeHumidity? RelativeHumidity, Temperature? Temperature)> ReadPeriodicMeasurementImpl(CancellationToken cancellationToken, bool async) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!_started) + { + StartPeriodicMeasurements(); + } + + // Wait for the next period. + int delay = _nextReadPeriod - Environment.TickCount; + + if (delay > 0) + { + await DelayAsync(Math.Min(delay, MeasurementPeriodMs), cancellationToken, async); + } + + // Wait for the device to have a measurement. + // When this loops, it is due to a small desync in device clocks. + int waitStart = Environment.TickCount; + + while (true) + { + BeginCheckDataReady(); + await DelayAsync(1, CancellationToken.None, async); + + if (EndCheckDataReady()) + { + break; + } + + if (Environment.TickCount - waitStart > (MeasurementPeriodMs + 1000)) + { + throw new Exception("SCD4x not responding."); + } + + await DelayAsync(100, cancellationToken, async); + } + + // Record the next period to expect data ready. + _nextReadPeriod = Environment.TickCount + 5000; + + // Retrieve the measurements. + BeginReadPeriodicMeasurement(); + await DelayAsync(2, CancellationToken.None, async); + + return EndReadPeriodicMeasurement(); + + static Task DelayAsync(int delay, CancellationToken cancellationToken, bool async) + { + if (async) + { + return Task.Delay(delay, cancellationToken); + } + + Thread.Sleep(delay); + return Task.CompletedTask; + } + } + + private void BeginCheckDataReady() => + _device.Write(CheckDataReadyStatusBytes); + + private bool EndCheckDataReady() + { + Span buffer = stackalloc byte[3]; + _device.Read(buffer); + + return Sensirion.ReadUInt16BigEndianAndCRC8(buffer) is ushort response && (response & 0x7FF) != 0; + } + + private void BeginReadPeriodicMeasurement() => + _device.Write(ReadPeriodicMeasurementBytes); + + private (VolumeConcentration? CarbonDioxide, RelativeHumidity? RelativeHumidity, Temperature? Temperature) EndReadPeriodicMeasurement() + { + Span buffer = stackalloc byte[9]; + _device.Read(buffer); + + VolumeConcentration? co2 = Sensirion.ReadUInt16BigEndianAndCRC8(buffer) switch + { + ushort sco2 => VolumeConcentration.FromPartsPerMillion(sco2), + null => (VolumeConcentration?)null + }; + + Temperature? temp = Sensirion.ReadUInt16BigEndianAndCRC8(buffer.Slice(3, 3)) switch + { + ushort st => Temperature.FromDegreesCelsius(st * (35.0 / 13107.0) - 45.0), + null => (Temperature?)null + }; + + RelativeHumidity? humidity = Sensirion.ReadUInt16BigEndianAndCRC8(buffer.Slice(6, 3)) switch + { + ushort srh => RelativeHumidity.FromPercent(srh * (100.0 / 65535.0)), + null => (RelativeHumidity?)null + }; + + if (co2 is not null) + { + _lastCo2 = co2.GetValueOrDefault(); + } + + if (temp is not null) + { + _lastTemp = temp.GetValueOrDefault(); + } + + if (humidity is not null) + { + _lastHum = humidity.GetValueOrDefault(); + } + + return (co2, humidity, temp); + } + + /// + /// Instructs the sensor to stop performing periodic measurements. + /// + public void StopPeriodicMeasurements() + { + _device.Write(StopPeriodicMeasurementBytes); + _started = false; + Thread.Sleep(500); + } + } +} diff --git a/src/devices/Scd4x/Scd4x.csproj b/src/devices/Scd4x/Scd4x.csproj new file mode 100644 index 0000000000000000000000000000000000000000..057b9ead75e5972f903794696e2ee03dcc6bf6cf --- /dev/null +++ b/src/devices/Scd4x/Scd4x.csproj @@ -0,0 +1,13 @@ + + + $(DefaultBindingTfms) + false + + + + + + + + + \ No newline at end of file diff --git a/src/devices/Scd4x/Scd4x.sln b/src/devices/Scd4x/Scd4x.sln new file mode 100644 index 0000000000000000000000000000000000000000..9867aaf181d791a92cd9c874e3ca4082ed7b1df2 --- /dev/null +++ b/src/devices/Scd4x/Scd4x.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31710.8 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scd4x", "Scd4x.csproj", "{91955027-9C88-478E-BC6A-CE0C9F5D29A7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scd4x.Samples", "samples\Scd4x.Samples.csproj", "{7F05F2F1-F8D2-4ED8-8895-7F5A2A146647}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{B14195D9-602C-4057-A32B-1ECE385BDC34}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {91955027-9C88-478E-BC6A-CE0C9F5D29A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {91955027-9C88-478E-BC6A-CE0C9F5D29A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {91955027-9C88-478E-BC6A-CE0C9F5D29A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {91955027-9C88-478E-BC6A-CE0C9F5D29A7}.Release|Any CPU.Build.0 = Release|Any CPU + {7F05F2F1-F8D2-4ED8-8895-7F5A2A146647}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F05F2F1-F8D2-4ED8-8895-7F5A2A146647}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F05F2F1-F8D2-4ED8-8895-7F5A2A146647}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F05F2F1-F8D2-4ED8-8895-7F5A2A146647}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {7F05F2F1-F8D2-4ED8-8895-7F5A2A146647} = {B14195D9-602C-4057-A32B-1ECE385BDC34} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A6935864-7401-42B8-AE98-AD542C55CADE} + EndGlobalSection +EndGlobal diff --git a/src/devices/Scd4x/samples/Program.cs b/src/devices/Scd4x/samples/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..74c7d9dd6afa3acbe7f5d4acb674eabd87dae05d --- /dev/null +++ b/src/devices/Scd4x/samples/Program.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Device.I2c; +using System.Threading; +using Iot.Device.Common; +using Iot.Device.Scd4x; +using UnitsNet; + +using var cts = new CancellationTokenSource(); +Console.CancelKeyPress += (s, e) => +{ + e.Cancel = true; + Console.WriteLine("Stopping..."); + cts.Cancel(); +}; + +I2cConnectionSettings settings = new(1, Scd4x.DefaultI2cAddress); +using I2cDevice device = I2cDevice.Create(settings); +using Scd4x sensor = new(device); + +// Async loop. +for (int i = 0; i < 3; ++i) +{ + Console.WriteLine("Waiting for measurement..."); + Console.WriteLine(); + + (VolumeConcentration? co2, RelativeHumidity? hum, Temperature? temp) = await sensor.ReadPeriodicMeasurementAsync(cts.Token); + + Console.WriteLine(co2 is not null + ? $"CO₂: {co2.Value}" + : $"CO₂: CRC check failed."); + + Console.WriteLine(temp is not null + ? $"Temperature: {temp.Value}" + : "Temperature: CRC check failed."); + + Console.WriteLine(hum is not null + ? $"Relative humidity: {hum.Value}" + : "Relative humidity: CRC check failed."); + + if (temp is not null && hum is not null) + { + // WeatherHelper supports more calculations, such as saturated vapor pressure, actual vapor pressure and absolute humidity. + Console.WriteLine($"Heat index: {WeatherHelper.CalculateHeatIndex(temp.Value, hum.Value).DegreesCelsius:0.#}\u00B0C"); + Console.WriteLine($"Dew point: {WeatherHelper.CalculateDewPoint(temp.Value, hum.Value).DegreesCelsius:0.#}\u00B0C"); + } + + Console.WriteLine(); +} + +// Sync loop +for (int i = 0; i < 3; ++i) +{ + Console.WriteLine("Waiting for measurement..."); + Console.WriteLine(); + + Thread.Sleep(Scd4x.MeasurementPeriod); + + Console.WriteLine($"CO₂: {sensor.Co2}"); + Console.WriteLine($"Temperature: {sensor.Temperature}"); + Console.WriteLine($"Relative humidity: {sensor.RelativeHumidity}"); + + Console.WriteLine(); +} diff --git a/src/devices/Scd4x/samples/Scd4x.Samples.csproj b/src/devices/Scd4x/samples/Scd4x.Samples.csproj new file mode 100644 index 0000000000000000000000000000000000000000..d60b16d7e3e521ad0f99751ad6379319aa31a50c --- /dev/null +++ b/src/devices/Scd4x/samples/Scd4x.Samples.csproj @@ -0,0 +1,10 @@ + + + Exe + net5.0 + + + + + + \ No newline at end of file diff --git a/src/devices/Shared/Sensirion.cs b/src/devices/Shared/Sensirion.cs new file mode 100644 index 0000000000000000000000000000000000000000..49061c03a5f94a10b759fc39d66c1eb9d0d4d98d --- /dev/null +++ b/src/devices/Shared/Sensirion.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Iot.Device +{ + /// + /// Common code for Sensirion devices. + /// + internal static class Sensirion + { + /// If the CRC8 matched the data, a of data. Otherwise, . + public static ushort? ReadUInt16BigEndianAndCRC8(ReadOnlySpan bytes) + { + Debug.Assert(bytes.Length >= 3, $"{nameof(bytes)} must contain at least 3 bytes."); + + _ = bytes[2]; + + byte hi = bytes[0]; + byte lo = bytes[1]; + byte crc = bytes[2]; + + return CRC8(hi, lo) == crc + ? (ushort)((hi << 8) | lo) + : null; + } + + public static void WriteUInt16BigEndianAndCRC8(Span bytes, ushort value) + { + Debug.Assert(bytes.Length >= 3, $"{nameof(bytes)} must contain at least 3 bytes."); + + byte hi = (byte)((uint)value >> 8); + byte lo = (byte)value; + + _ = bytes[2]; + + bytes[0] = hi; + bytes[1] = lo; + bytes[2] = CRC8(hi, lo); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte CRC8(byte byteA, byte byteB) => + CrcLookup[CrcLookup[0xFF ^ byteA] ^ byteB]; + + private static ReadOnlySpan CrcLookup => new byte[] + { + 0x00, 0x31, 0x62, 0x53, 0xC4, 0xF5, 0xA6, 0x97, 0xB9, 0x88, 0xDB, 0xEA, 0x7D, 0x4C, 0x1F, 0x2E, + 0x43, 0x72, 0x21, 0x10, 0x87, 0xB6, 0xE5, 0xD4, 0xFA, 0xCB, 0x98, 0xA9, 0x3E, 0x0F, 0x5C, 0x6D, + 0x86, 0xB7, 0xE4, 0xD5, 0x42, 0x73, 0x20, 0x11, 0x3F, 0x0E, 0x5D, 0x6C, 0xFB, 0xCA, 0x99, 0xA8, + 0xC5, 0xF4, 0xA7, 0x96, 0x01, 0x30, 0x63, 0x52, 0x7C, 0x4D, 0x1E, 0x2F, 0xB8, 0x89, 0xDA, 0xEB, + 0x3D, 0x0C, 0x5F, 0x6E, 0xF9, 0xC8, 0x9B, 0xAA, 0x84, 0xB5, 0xE6, 0xD7, 0x40, 0x71, 0x22, 0x13, + 0x7E, 0x4F, 0x1C, 0x2D, 0xBA, 0x8B, 0xD8, 0xE9, 0xC7, 0xF6, 0xA5, 0x94, 0x03, 0x32, 0x61, 0x50, + 0xBB, 0x8A, 0xD9, 0xE8, 0x7F, 0x4E, 0x1D, 0x2C, 0x02, 0x33, 0x60, 0x51, 0xC6, 0xF7, 0xA4, 0x95, + 0xF8, 0xC9, 0x9A, 0xAB, 0x3C, 0x0D, 0x5E, 0x6F, 0x41, 0x70, 0x23, 0x12, 0x85, 0xB4, 0xE7, 0xD6, + 0x7A, 0x4B, 0x18, 0x29, 0xBE, 0x8F, 0xDC, 0xED, 0xC3, 0xF2, 0xA1, 0x90, 0x07, 0x36, 0x65, 0x54, + 0x39, 0x08, 0x5B, 0x6A, 0xFD, 0xCC, 0x9F, 0xAE, 0x80, 0xB1, 0xE2, 0xD3, 0x44, 0x75, 0x26, 0x17, + 0xFC, 0xCD, 0x9E, 0xAF, 0x38, 0x09, 0x5A, 0x6B, 0x45, 0x74, 0x27, 0x16, 0x81, 0xB0, 0xE3, 0xD2, + 0xBF, 0x8E, 0xDD, 0xEC, 0x7B, 0x4A, 0x19, 0x28, 0x06, 0x37, 0x64, 0x55, 0xC2, 0xF3, 0xA0, 0x91, + 0x47, 0x76, 0x25, 0x14, 0x83, 0xB2, 0xE1, 0xD0, 0xFE, 0xCF, 0x9C, 0xAD, 0x3A, 0x0B, 0x58, 0x69, + 0x04, 0x35, 0x66, 0x57, 0xC0, 0xF1, 0xA2, 0x93, 0xBD, 0x8C, 0xDF, 0xEE, 0x79, 0x48, 0x1B, 0x2A, + 0xC1, 0xF0, 0xA3, 0x92, 0x05, 0x34, 0x67, 0x56, 0x78, 0x49, 0x1A, 0x2B, 0xBC, 0x8D, 0xDE, 0xEF, + 0x82, 0xB3, 0xE0, 0xD1, 0x46, 0x77, 0x24, 0x15, 0x3B, 0x0A, 0x59, 0x68, 0xFF, 0xCE, 0x9D, 0xAC + }; + } + +} diff --git a/src/devices/Sht4x/README.md b/src/devices/Sht4x/README.md new file mode 100644 index 0000000000000000000000000000000000000000..8f77a392ff6ce0cec533df0de94e9e8b2b1f7fb3 --- /dev/null +++ b/src/devices/Sht4x/README.md @@ -0,0 +1,52 @@ +# SHT4x - Temperature & Humidity Sensor + +SHT4x is a temperature and humidity sensor from Sensirion. This project supports the SHT40, SHT41, and SHT45 sensors. + +## Documentation + +- SHT40 [datasheet](https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/2_Humidity_Sensors/Datasheets/Sensirion_Humidity_Sensors_SHT4x_Datasheet.pdf) + +## Usage + +### Hardware Required + +- SHT40. + +### Code (synchronous) + +```csharp +I2cConnectionSettings settings = + new I2cConnectionSettings(1, Sht4x.DefaultI2cAddress); + +using I2cDevice device = I2cDevice.Create(settings); +using Sht4x sensor = new Sht4x(device); + +// read humidity (%) +double humidity = sensor.RelativeHumidity.Percent; +// read temperature (℃) +double temperature = sensor.Temperature.Celsius; +``` + +### Code (asynchronous) + +```csharp +I2cConnectionSettings settings = + new I2cConnectionSettings(1, Sht4x.DefaultI2cAddress); + +using I2cDevice device = I2cDevice.Create(settings); +using Sht4x sensor = new Sht4x(device); + +// Read both humidity and temperature. +(RelativeHumidity? rh, Temperature? t) = + await sensor.ReadHumidityAndTemperatureAsync(); + +if(rh is null || t is null) +{ + throw new Exception("CRC failure"); +} + +// read humidity (%) +double humidity = rh.Value.Percent; +// read temperature (℃) +double temperature = t.Value.Celsius; +``` \ No newline at end of file diff --git a/src/devices/Sht4x/Sht4x.cs b/src/devices/Sht4x/Sht4x.cs new file mode 100644 index 0000000000000000000000000000000000000000..24e81e253953d6240930602d5908adc7fa8f464d --- /dev/null +++ b/src/devices/Sht4x/Sht4x.cs @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Device.I2c; +using System.Device.Model; +using System.Threading; +using System.Threading.Tasks; +using UnitsNet; + +namespace Iot.Device.Sht4x +{ + /// + /// Humidity and Temperature Sensor SHT4x + /// + [Interface("Humidity and Temperature Sensor SHT4x")] + public sealed class Sht4x : IDisposable + { + /// + /// The default I²C address of this device. + /// + public const int DefaultI2cAddress = 0x44; + + private readonly I2cDevice _device; + private RelativeHumidity _lastHum; + private Temperature _lastTemp; + + /// + /// The level of repeatability to use when measuring relative humidity. + /// + [Property] + public Sht4xRepeatability Repeatability { get; set; } = Sht4xRepeatability.High; + + /// + /// The most recent relative humidity measurement. + /// + [Telemetry] + public RelativeHumidity RelativeHumidity + { + get + { + ReadHumidityAndTemperature(Repeatability); + return _lastHum; + } + } + + /// + /// The most recent temperature measurement. + /// + [Telemetry] + public Temperature Temperature + { + get + { + ReadHumidityAndTemperature(Repeatability); + return _lastTemp; + } + } + + /// + /// Instantiates a new . + /// + /// The I²C device to operate on. + public Sht4x(I2cDevice device) + { + _device = device; + Reset(); + } + + /// + public void Dispose() => + _device.Dispose(); + + /// + /// Resets the device. + /// + public void Reset() + { + _device.WriteByte(0x94); + Thread.Sleep(1); + } + + /// + /// Reads relative humidity and temperature. + /// + /// + /// A tuple of relative humidity and temperature. + /// If a CRC check failed for a measurement, it will be . + /// + public (RelativeHumidity? RelativeHumidity, Temperature? Temperature) ReadHumidityAndTemperature(Sht4xRepeatability repeatability = Sht4xRepeatability.High) + { + int delay = BeginReadHumidityAndTemperature(repeatability); + Thread.Sleep(delay); + return EndReadHumidityAndTemperature(); + } + + /// + public async ValueTask<(RelativeHumidity? RelativeHumidity, Temperature? Temperature)> ReadHumidityAndTemperatureAsync(Sht4xRepeatability repeatability = Sht4xRepeatability.High) + { + int delay = BeginReadHumidityAndTemperature(repeatability); + await Task.Delay(delay); + return EndReadHumidityAndTemperature(); + } + + private int BeginReadHumidityAndTemperature(Sht4xRepeatability repeatability) + { + (byte cmd, int delayInMs) = repeatability switch + { + Sht4xRepeatability.Low => ((byte)0xE0, 2), + Sht4xRepeatability.Medium => ((byte)0xF6, 5), + Sht4xRepeatability.High => ((byte)0xFD, 9), + _ => throw new ArgumentOutOfRangeException(nameof(repeatability)) + }; + + _device.WriteByte(cmd); + return delayInMs; + } + + private (RelativeHumidity? RelativeHumidity, Temperature? Temperature) EndReadHumidityAndTemperature() + { + Span buffer = stackalloc byte[6]; + _device.Read(buffer); + + Temperature? t = Sensirion.ReadUInt16BigEndianAndCRC8(buffer) switch + { + ushort deviceTemperature => Temperature.FromDegreesCelsius(deviceTemperature * (35.0 / 13107.0) - 45.0), + null => (Temperature?)null + }; + + RelativeHumidity? h = Sensirion.ReadUInt16BigEndianAndCRC8(buffer.Slice(3, 3)) switch + { + ushort deviceHumidity => RelativeHumidity.FromPercent(Math.Max(0.0, Math.Min(deviceHumidity * (100.0 / 52428.0) - (300.0 / 50.0), 100.0))), + null => (RelativeHumidity?)null + }; + + if (h is not null) + { + _lastHum = h.GetValueOrDefault(); + } + + if (t is not null) + { + _lastTemp = t.GetValueOrDefault(); + } + + return (h, t); + } + } +} diff --git a/src/devices/Sht4x/Sht4x.csproj b/src/devices/Sht4x/Sht4x.csproj new file mode 100644 index 0000000000000000000000000000000000000000..90beda188d54e49f37e4882da2f62bbfc26d97f2 --- /dev/null +++ b/src/devices/Sht4x/Sht4x.csproj @@ -0,0 +1,14 @@ + + + $(DefaultBindingTfms) + false + + + + + + + + + + \ No newline at end of file diff --git a/src/devices/Sht4x/Sht4x.sln b/src/devices/Sht4x/Sht4x.sln new file mode 100644 index 0000000000000000000000000000000000000000..f8ce73a1cefae58f0d2415aef1dbce2c3012898c --- /dev/null +++ b/src/devices/Sht4x/Sht4x.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31710.8 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sht4x", "Sht4x.csproj", "{6F1F02B2-1C0F-4BC1-8354-6C4F5255B6D7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sht4x.Samples", "samples\Sht4x.Samples.csproj", "{DF1C2C6B-71D1-4B40-BD85-79C4AA4A68FA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{D23EB428-8E64-4906-ACAB-48EB63459C45}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6F1F02B2-1C0F-4BC1-8354-6C4F5255B6D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F1F02B2-1C0F-4BC1-8354-6C4F5255B6D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F1F02B2-1C0F-4BC1-8354-6C4F5255B6D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F1F02B2-1C0F-4BC1-8354-6C4F5255B6D7}.Release|Any CPU.Build.0 = Release|Any CPU + {DF1C2C6B-71D1-4B40-BD85-79C4AA4A68FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF1C2C6B-71D1-4B40-BD85-79C4AA4A68FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF1C2C6B-71D1-4B40-BD85-79C4AA4A68FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF1C2C6B-71D1-4B40-BD85-79C4AA4A68FA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {DF1C2C6B-71D1-4B40-BD85-79C4AA4A68FA} = {D23EB428-8E64-4906-ACAB-48EB63459C45} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {70971141-EF20-4FED-8937-CC5ACC0CDC26} + EndGlobalSection +EndGlobal diff --git a/src/devices/Sht4x/Sht4xRepeatability.cs b/src/devices/Sht4x/Sht4xRepeatability.cs new file mode 100644 index 0000000000000000000000000000000000000000..e4c18c33dbe3f30d7c054e5c4a730a6e3b9902ad --- /dev/null +++ b/src/devices/Sht4x/Sht4xRepeatability.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Iot.Device.Sht4x +{ + /// + /// Desired repeatability of relative humidity measurement. + /// + public enum Sht4xRepeatability + { + /// + /// 0.25% RH error + /// + Low, + + /// + /// 0.15% RH error + /// + Medium, + + /// + /// 0.08% RH error + /// + High + } +} diff --git a/src/devices/Sht4x/samples/Program.cs b/src/devices/Sht4x/samples/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..351e98d93db1be2eb19f5549de4d7723b851313c --- /dev/null +++ b/src/devices/Sht4x/samples/Program.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Device.I2c; +using System.Threading; +using System.Threading.Tasks; +using Iot.Device.Common; +using Iot.Device.Sht4x; +using UnitsNet; + +I2cConnectionSettings settings = new(1, Sht4x.DefaultI2cAddress); +using I2cDevice device = I2cDevice.Create(settings); +using Sht4x sensor = new(device); + +// Async loop. +for (int i = 0; i < 3; ++i) +{ + (RelativeHumidity? hum, Temperature? temp) = await sensor.ReadHumidityAndTemperatureAsync(); + + Console.WriteLine(temp is not null + ? $"Temperature: {temp.Value}" + : "Temperature: CRC check failed."); + + Console.WriteLine(hum is not null + ? $"Relative humidity: {hum.Value}" + : "Relative humidity: CRC check failed."); + + if (temp is not null && hum is not null) + { + // WeatherHelper supports more calculations, such as saturated vapor pressure, actual vapor pressure and absolute humidity. + Console.WriteLine($"Heat index: {WeatherHelper.CalculateHeatIndex(temp.Value, hum.Value)}"); + Console.WriteLine($"Dew point: {WeatherHelper.CalculateDewPoint(temp.Value, hum.Value)}"); + } + + Console.WriteLine(); + + await Task.Delay(1000); +} + +// Property-based access. +for (int i = 0; i < 3; ++i) +{ + Console.WriteLine($"Temperature: {sensor.Temperature}"); + Console.WriteLine($"Relative humidity: {sensor.RelativeHumidity}"); + + Console.WriteLine(); + + Thread.Sleep(1000); +} diff --git a/src/devices/Sht4x/samples/Sht4x.Samples.csproj b/src/devices/Sht4x/samples/Sht4x.Samples.csproj new file mode 100644 index 0000000000000000000000000000000000000000..0806ca49a214c9af077d57b156a5934f9cd6b368 --- /dev/null +++ b/src/devices/Sht4x/samples/Sht4x.Samples.csproj @@ -0,0 +1,10 @@ + + + Exe + net5.0 + + + + + + \ No newline at end of file