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