diff --git a/src/devices/Ahtxx/Aht10.cs b/src/devices/Ahtxx/Aht10.cs
new file mode 100644
index 0000000000..831765a9ad
--- /dev/null
+++ b/src/devices/Ahtxx/Aht10.cs
@@ -0,0 +1,27 @@
+// 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.Device.I2c;
+
+namespace Iot.Device.Ahtxx
+{
+ ///
+ /// AHT10/15 temperature and humidity sensor binding.
+ ///
+ public class Aht10 : AhtBase
+ {
+ ///
+ /// Initialization command acc. to datasheet
+ ///
+ private const byte Aht10InitCommand = 0b1110_0001;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Aht10(I2cDevice i2cDevice)
+ : base(i2cDevice, Aht10InitCommand)
+ {
+ }
+ }
+}
diff --git a/src/devices/Ahtxx/Aht20.cs b/src/devices/Ahtxx/Aht20.cs
new file mode 100644
index 0000000000..e26272a9a6
--- /dev/null
+++ b/src/devices/Ahtxx/Aht20.cs
@@ -0,0 +1,27 @@
+// 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.Device.I2c;
+
+namespace Iot.Device.Ahtxx
+{
+ ///
+ /// AHT20 temperature and humidity sensor binding.
+ ///
+ public class Aht20 : AhtBase
+ {
+ ///
+ /// Initialization command acc. to datasheet
+ ///
+ private const byte Aht20InitCommand = 0b1011_1110;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Aht20(I2cDevice i2cDevice)
+ : base(i2cDevice, Aht20InitCommand)
+ {
+ }
+ }
+}
diff --git a/src/devices/Ahtxx/AhtBase.cs b/src/devices/Ahtxx/AhtBase.cs
new file mode 100644
index 0000000000..5f097e416c
--- /dev/null
+++ b/src/devices/Ahtxx/AhtBase.cs
@@ -0,0 +1,178 @@
+// 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.Device.I2c;
+using System.Threading;
+using UnitsNet;
+
+namespace Iot.Device.Ahtxx
+{
+ ///
+ /// Base class for common functions of the AHT10/15 and AHT20 sensors.
+ ///
+ public abstract class AhtBase : IDisposable
+ {
+ ///
+ /// Address of AHT10/15/20 device (0x38). This address is fix and cannot be changed.
+ /// This implies that only one device can be attached to a single I2C bus at a time.
+ ///
+ public const int DefaultI2cAddress = 0x38;
+
+ private readonly byte _initCommand;
+ private I2cDevice _i2cDevice;
+ private double _temperature;
+ private double _humidity;
+
+ ///
+ /// Initializes a new instance of the binding for a sensor connected through I2C interface.
+ ///
+ /// Reference to the initialized I2C interface device
+ /// Type specific command for device initialization
+ public AhtBase(I2cDevice i2cDevice, byte initCommand)
+ {
+ _i2cDevice = i2cDevice ?? throw new ArgumentNullException(nameof(i2cDevice));
+ _initCommand = initCommand;
+
+ // even if not clearly stated in datasheet, start with a software reset to assure clear start conditions
+ SoftReset();
+
+ // check whether the device indicates the need for a calibration cycle
+ // and perform calibration if indicated ==> c.f. datasheet, version 1.1, ch. 5.4
+ if (!IsCalibrated())
+ {
+ Initialize();
+ }
+ }
+
+ ///
+ /// Gets the current temperature reading from the sensor.
+ /// Reading the temperature takes between 10 ms and 80 ms.
+ ///
+ /// Temperature reading
+ public Temperature GetTemperature()
+ {
+ Measure();
+ return Temperature.FromDegreesCelsius(_temperature);
+ }
+
+ ///
+ /// Gets the current relative humidity reading from the sensor.
+ /// Reading the humidity takes between 10 ms and 80 ms.
+ ///
+ /// Relative humidity reading
+ public Ratio GetHumidity()
+ {
+ Measure();
+ return Ratio.FromPercent(_humidity);
+ }
+
+ ///
+ /// Perform sequence to retrieve current readings from device
+ ///
+ private void Measure()
+ {
+ Span buffer = stackalloc byte[3]
+ {
+ // command parameters c.f. datasheet, version 1.1, ch. 5.4
+ (byte)CommonCommand.Measure,
+ 0x33,
+ 0x00
+ };
+
+ _i2cDevice.Write(buffer);
+
+ // According to the datasheet the measurement takes 80 ms and completion is indicated by the status bit.
+ // However, it seems to be faster at around 10 ms and sometimes up to 50 ms.
+ while (IsBusy())
+ {
+ Thread.Sleep(10);
+ }
+
+ buffer = stackalloc byte[6];
+ _i2cDevice.Read(buffer);
+
+ // data format: 20 bit humidity, 20 bit temperature
+ // 7 0 7 0 7 4 0 7 0 7 0
+ // [humidity 19..12] [humidity 11..4] [humidity 3..0|temp 19..16] [temp 15..8] [temp 7..0]
+ // c.f. datasheet ch. 5.4.5
+ Int32 rawHumidity = (buffer[1] << 12) | (buffer[2] << 4) | (buffer[3] >> 4);
+ Int32 rawTemperature = ((buffer[3] & 0xF) << 16) | (buffer[4] << 8) | buffer[5];
+ // RH[%] = Hraw / 2^20 * 100%, c.f. datasheet ch. 6.1
+ _humidity = (rawHumidity * 100.0) / 0x100000;
+ // T[°C] = Traw / 2^20 * 200 - 50, c.f. datasheet ch. 6.1
+ _temperature = ((rawTemperature * 200.0) / 0x100000) - 50;
+ }
+
+ ///
+ /// Perform soft reset command sequence
+ ///
+ private void SoftReset()
+ {
+ _i2cDevice.WriteByte((byte)CommonCommand.SoftReset);
+ // reset requires 20ms at most, c.f. datasheet version 1.1, ch. 5.5
+ Thread.Sleep(20);
+ }
+
+ ///
+ /// Perform initialization (calibration) command sequence
+ ///
+ private void Initialize()
+ {
+ Span buffer = stackalloc byte[3]
+ {
+ _initCommand,
+ 0x08, // command parameters c.f. datasheet, version 1.1, ch. 5.4
+ 0x00
+ };
+
+ _i2cDevice.Write(buffer);
+ // wait 10ms c.f. datasheet, version 1.1, ch. 5.4
+ Thread.Sleep(10);
+ }
+
+ private byte GetStatus()
+ {
+ _i2cDevice.WriteByte(0x71);
+ // whithout this delay the reading the status fails often.
+ Thread.Sleep(10);
+ byte status = _i2cDevice.ReadByte();
+ return status;
+ }
+
+ private bool IsBusy()
+ {
+ return (GetStatus() & (byte)StatusBit.Busy) == (byte)StatusBit.Busy;
+ }
+
+ private bool IsCalibrated()
+ {
+ return (GetStatus() & (byte)StatusBit.Calibrated) == (byte)StatusBit.Calibrated;
+ }
+
+ ///
+ public void Dispose() => Dispose(true);
+
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ _i2cDevice?.Dispose();
+ _i2cDevice = null;
+ }
+
+ // datasheet version 1.1, table 10
+ [Flags]
+ private enum StatusBit : byte
+ {
+ Calibrated = 0b_0000_1000,
+ Busy = 0b1000_0000
+ }
+
+ private enum CommonCommand : byte
+ {
+ SoftReset = 0b1011_1010,
+ Measure = 0b1010_1100
+ }
+ }
+}
diff --git a/src/devices/Ahtxx/Ahtxx.csproj b/src/devices/Ahtxx/Ahtxx.csproj
new file mode 100644
index 0000000000..8cecbb7c89
--- /dev/null
+++ b/src/devices/Ahtxx/Ahtxx.csproj
@@ -0,0 +1,15 @@
+
+
+
+ netcoreapp2.1
+
+ false
+
+
+
+
+
+
+
+
+
diff --git a/src/devices/Ahtxx/Ahtxx.sln b/src/devices/Ahtxx/Ahtxx.sln
new file mode 100644
index 0000000000..734bcae88a
--- /dev/null
+++ b/src/devices/Ahtxx/Ahtxx.sln
@@ -0,0 +1,53 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26124.0
+MinimumVisualStudioVersion = 15.0.26124.0
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ahtxx", "Ahtxx.csproj", "{B82C190A-642B-465B-BD3F-DB56FFF22253}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{6A4DE7B1-03F3-4EE0-BF73-A0BAEF88BA2B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ahtxx.Samples", "samples\Ahtxx.Samples.csproj", "{3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Debug|x64.Build.0 = Debug|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Debug|x86.Build.0 = Debug|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Release|x64.ActiveCfg = Release|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Release|x64.Build.0 = Release|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Release|x86.ActiveCfg = Release|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Release|x86.Build.0 = Release|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Debug|x64.Build.0 = Debug|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Debug|x86.Build.0 = Debug|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Release|x64.ActiveCfg = Release|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Release|x64.Build.0 = Release|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Release|x86.ActiveCfg = Release|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF} = {6A4DE7B1-03F3-4EE0-BF73-A0BAEF88BA2B}
+ EndGlobalSection
+EndGlobal
diff --git a/src/devices/Ahtxx/README.md b/src/devices/Ahtxx/README.md
new file mode 100644
index 0000000000..680cfdba2d
--- /dev/null
+++ b/src/devices/Ahtxx/README.md
@@ -0,0 +1,49 @@
+# AHT10/15/20 Temperature and Humidity Sensor Modules
+
+## Summary
+The AHT10/15 and AHT20 sensors are high-precision, calibrated temperature and relative humidity sensor modules with an I2C digital interface.
+
+## Binding Notes
+### Supported Devices
+The binding supports the following types:
+* AHT10 - http://www.aosong.com/en/products-40.html
+* AHT15 - http://www.aosong.com/en/products-45.html
+* AHT20 - http://www.aosong.com/en/products-32.html
+
+### Functions
+The binding supports the following sensor functions:
+* acquiring the temperature and relative humidty readings
+* reading status
+* issueing calibration and reset commands
+
+
+### Sensor classes
+You need to choose the class depending on the sensor type.
+
+|Sensor|Required class|
+|-----|---------------|
+|AHT10|Aht10 |
+|Aht15|Aht10 |
+|Aht20|Aht20 |
+
+
+### Basic Usage
+
+The binding gets instantiated using an existing ```I2cDevice`` instance. The AHT-sensor modules support only the default I2C address.
+
+Setup for an AHT20 sensor module:
+```
+const int I2cBus = 1;
+I2cConnectionSettings i2cSettings = new I2cConnectionSettings(I2cBus, Aht20.DefaultI2cAddress);
+I2cDevice i2cDevice = I2cDevice.Create(i2cSettings);
+Aht20 sensor = new Aht20(i2cDevice);
+```
+
+The temperature and humidity readings are acquired by using the following methods:
+
+```
+public Temperature GetTemperature()
+public Ratio GetHumidity()
+```
+
+Refer to the sample application for a complete example.
diff --git a/src/devices/Ahtxx/category.txt b/src/devices/Ahtxx/category.txt
new file mode 100644
index 0000000000..5f95b0d2d0
--- /dev/null
+++ b/src/devices/Ahtxx/category.txt
@@ -0,0 +1,2 @@
+hygrometers
+thermometers
\ No newline at end of file
diff --git a/src/devices/Ahtxx/samples/Ahtxx.Sample.cs b/src/devices/Ahtxx/samples/Ahtxx.Sample.cs
new file mode 100644
index 0000000000..931823fb51
--- /dev/null
+++ b/src/devices/Ahtxx/samples/Ahtxx.Sample.cs
@@ -0,0 +1,36 @@
+// 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.Device.I2c;
+using System.Threading;
+
+namespace Iot.Device.Ahtxx.Samples
+{
+ ///
+ /// Samples for Aht10 and Aht20 bindings
+ ///
+ internal class Program
+ {
+ ///
+ /// Main entry point
+ ///
+ public static void Main(string[] args)
+ {
+ const int I2cBus = 1;
+ I2cConnectionSettings i2cSettings = new I2cConnectionSettings(I2cBus, Aht20.DefaultI2cAddress);
+ I2cDevice i2cDevice = I2cDevice.Create(i2cSettings);
+
+ // For AHT10 or AHT15 use:
+ // Aht10 sensor = new Aht10(i2cDevice);
+ // For AHT20 use:
+ Aht20 sensor = new Aht20(i2cDevice);
+ while (true)
+ {
+ Console.WriteLine($"{DateTime.Now.ToLongTimeString()}: {sensor.GetTemperature().DegreesCelsius:F1}°C, {sensor.GetHumidity().Percent:F0}%");
+ Thread.Sleep(1000);
+ }
+ }
+ }
+}
diff --git a/src/devices/Ahtxx/samples/Ahtxx.Samples.csproj b/src/devices/Ahtxx/samples/Ahtxx.Samples.csproj
new file mode 100644
index 0000000000..a052a33d81
--- /dev/null
+++ b/src/devices/Ahtxx/samples/Ahtxx.Samples.csproj
@@ -0,0 +1,13 @@
+
+
+
+ Exe
+ netcoreapp3.1
+
+
+
+
+
+
+
+
diff --git a/src/devices/Ahtxx/samples/Ahtxx_sample.png b/src/devices/Ahtxx/samples/Ahtxx_sample.png
new file mode 100644
index 0000000000..a321729c8a
Binary files /dev/null and b/src/devices/Ahtxx/samples/Ahtxx_sample.png differ
diff --git a/src/devices/Ahtxx/samples/README.md b/src/devices/Ahtxx/samples/README.md
new file mode 100644
index 0000000000..0f0003e95a
--- /dev/null
+++ b/src/devices/Ahtxx/samples/README.md
@@ -0,0 +1,10 @@
+# Sample application for use the of Ahtxx binding
+
+## Summary
+The sample application demonstrates how to setup the binding and retrieve the current readings.
+It can be used with all three supported types by using the specific binding class (Aht10 / Aht20).
+
+## Wiring
+The AHTxx sensor is wired to the I2C interface (SDC/SDA) of the Raspberry Pi. The sensor is supplied with 3.3V to comply with the 3.3V interface level of the RPi.
+
+
\ No newline at end of file
diff --git a/src/devices/Mhz19b/AbmState.cs b/src/devices/Mhz19b/AbmState.cs
new file mode 100644
index 0000000000..18ca819f9c
--- /dev/null
+++ b/src/devices/Mhz19b/AbmState.cs
@@ -0,0 +1,23 @@
+// 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.
+
+namespace Iot.Device.Mhz19b
+{
+ ///
+ /// Defines if automatic baseline correction (ABM) is on or off
+ /// For details refer to datasheet, rev. 1.0, pg. 8
+ ///
+ public enum AbmState
+ {
+ ///
+ /// ABM off (value acc. to datasheet)
+ ///
+ Off = 0x00,
+
+ ///
+ /// ABM on (value acc. to datasheet)
+ ///
+ On = 0xA0
+ }
+}
diff --git a/src/devices/Mhz19b/DetectionRange.cs b/src/devices/Mhz19b/DetectionRange.cs
new file mode 100644
index 0000000000..da74dbff67
--- /dev/null
+++ b/src/devices/Mhz19b/DetectionRange.cs
@@ -0,0 +1,22 @@
+// 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.
+
+namespace Iot.Device.Mhz19b
+{
+ ///
+ /// Defines the sensor detection range, which is either 2000 or 5000ppm.
+ ///
+ public enum DetectionRange
+ {
+ ///
+ /// Detection range 2000ppm
+ ///
+ Range2000 = 2000,
+
+ ///
+ /// Detection range 5000ppm
+ ///
+ Range5000 = 5000
+ }
+}
diff --git a/src/devices/Mhz19b/Mhz19b.cs b/src/devices/Mhz19b/Mhz19b.cs
new file mode 100644
index 0000000000..a6e5009c50
--- /dev/null
+++ b/src/devices/Mhz19b/Mhz19b.cs
@@ -0,0 +1,244 @@
+// 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.IO;
+using System.IO.Ports;
+using System.Text;
+using System.Threading;
+using UnitsNet;
+
+namespace Iot.Device.Mhz19b
+{
+ ///
+ /// MH-Z19B CO2 concentration sensor binding
+ ///
+ public sealed class Mhz19b : IDisposable
+ {
+ private const int MessageBytes = 9;
+ private bool _shouldDispose = false;
+ private SerialPort _serialPort = null;
+ private Stream _serialPortStream = null;
+
+ ///
+ /// Initializes a new instance of the class using an existing (serial port) stream.
+ ///
+ /// Existing stream
+ /// If true, the stream gets disposed when disposing the binding
+ public Mhz19b(Stream stream, bool shouldDispose)
+ {
+ _serialPortStream = stream ?? throw new ArgumentNullException(nameof(stream));
+ _shouldDispose = shouldDispose;
+ }
+
+ ///
+ /// Initializes a new instance of the class and creates a new serial port stream.
+ ///
+ /// Path to the UART device / serial port, e.g. /dev/serial0
+ /// uartDevice is null or empty
+ public Mhz19b(string uartDevice)
+ {
+ if (string.IsNullOrEmpty(uartDevice))
+ {
+ throw new ArgumentException(nameof(uartDevice));
+ }
+
+ // create serial port using the setting acc. to datasheet, pg. 7, sec. general settings
+ _serialPort = new SerialPort(uartDevice, 9600, Parity.None, 8, StopBits.One)
+ {
+ Encoding = Encoding.ASCII,
+ ReadTimeout = 1000,
+ WriteTimeout = 1000
+ };
+
+ _serialPort.Open();
+ _serialPortStream = _serialPort.BaseStream;
+ _shouldDispose = true;
+ }
+
+ ///
+ /// Gets the current CO2 concentration from the sensor.
+ ///
+ /// CO2 volume concentration
+ /// Communication with sensor failed
+ /// A timeout occurred while communicating with the sensor
+ public VolumeConcentration GetCo2Reading()
+ {
+ // send read command request
+ var request = CreateRequest(Command.ReadCo2Concentration);
+ request[(int)MessageFormat.Checksum] = Checksum(request);
+ _serialPortStream.Write(request, 0, request.Length);
+
+ // read complete response (9 bytes expected)
+ byte[] response = new byte[MessageBytes];
+
+ long endTicks = DateTime.Now.AddMilliseconds(250).Ticks;
+ int bytesRead = 0;
+ while (DateTime.Now.Ticks < endTicks && bytesRead < MessageBytes)
+ {
+ bytesRead += _serialPortStream.Read(response, bytesRead, response.Length - bytesRead);
+ Thread.Sleep(1);
+ }
+
+ if (bytesRead < MessageBytes)
+ {
+ throw new TimeoutException($"Communication with sensor failed.");
+ }
+
+ // check response and return calculated concentration if valid
+ if (response[(int)MessageFormat.Checksum] == Checksum(response))
+ {
+ return VolumeConcentration.FromPartsPerMillion((int)response[(int)MessageFormat.DataHighResponse] * 256 + (int)response[(int)MessageFormat.DataLowResponse]);
+ }
+ else
+ {
+ throw new IOException("Invalid response message received from sensor");
+ }
+ }
+
+ ///
+ /// Initiates a zero point calibration.
+ ///
+ /// Communication with sensor failed
+ public void PerformZeroPointCalibration() => SendRequest(CreateRequest(Command.CalibrateZeroPoint));
+
+ ///
+ /// Initiate a span point calibration.
+ ///
+ /// span value, between 1000[ppm] and 5000[ppm]. The typical value is 2000[ppm].
+ /// Thrown when span value is out of range
+ /// Communication with sensor failed
+ public void PerformSpanPointCalibration(VolumeConcentration span)
+ {
+ if ((span.PartsPerMillion < 1000) || (span.PartsPerMillion > 5000))
+ {
+ throw new ArgumentException("Span value out of range (1000-5000[ppm])", nameof(span));
+ }
+
+ var request = CreateRequest(Command.CalibrateSpanPoint);
+ // set span in request, c. f. datasheet rev. 1.0, pg. 8 for details
+ request[(int)MessageFormat.DataHighRequest] = (byte)(span.PartsPerMillion / 256);
+ request[(int)MessageFormat.DataLowRequest] = (byte)(span.PartsPerMillion % 256);
+
+ SendRequest(request);
+ }
+
+ ///
+ /// Switches the autmatic baseline correction on and off.
+ ///
+ /// State of automatic correction
+ /// Communication with sensor failed
+ public void SetAutomaticBaselineCorrection(AbmState state)
+ {
+ var request = CreateRequest(Command.AutoCalibrationSwitch);
+ // set on/off state in request, c. f. datasheet rev. 1.0, pg. 8 for details
+ request[(int)MessageFormat.DataHighRequest] = (byte)state;
+
+ SendRequest(request);
+ }
+
+ ///
+ /// Set the sensor detection range.
+ ///
+ /// Detection range of the sensor
+ /// Communication with sensor failed
+ public void SetSensorDetectionRange(DetectionRange detectionRange)
+ {
+ var request = CreateRequest(Command.DetectionRangeSetting);
+ // set detection range in request, c. f. datasheet rev. 1.0, pg. 8 for details
+ request[(int)MessageFormat.DataHighRequest] = (byte)((int)detectionRange / 256);
+ request[(int)MessageFormat.DataLowRequest] = (byte)((int)detectionRange % 256);
+
+ SendRequest(request);
+ }
+
+ private void SendRequest(byte[] request)
+ {
+ request[(int)MessageFormat.Checksum] = Checksum(request);
+
+ try
+ {
+ _serialPortStream.Write(request, 0, request.Length);
+ }
+ catch (Exception e)
+ {
+ throw new IOException("Sensor communication failed", e);
+ }
+ }
+
+ private byte[] CreateRequest(Command command) => new byte[]
+ {
+ 0xff, // start byte,
+ 0x01, // sensor number, always 0x1
+ (byte)command,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // empty bytes
+ };
+
+ ///
+ /// Calculate checksum for requests and responses.
+ /// For details refer to datasheet rev. 1.0, pg. 8.
+ ///
+ /// Packet the checksum is calculated for
+ /// Cheksum
+ private byte Checksum(byte[] packet)
+ {
+ byte checksum = 0;
+ for (int i = 1; i < 8; i++)
+ {
+ checksum += packet[i];
+ }
+
+ checksum = (byte)(0xff - checksum);
+ checksum += 1;
+ return checksum;
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (_serialPort == null && _serialPortStream == null)
+ {
+ return;
+ }
+
+ if (_shouldDispose && _serialPortStream != null)
+ {
+ _serialPortStream.Dispose();
+ _serialPortStream = null;
+ }
+
+ if (_serialPort != null)
+ {
+ if (_serialPort.IsOpen)
+ {
+ _serialPort.Close();
+ }
+
+ _serialPort.Dispose();
+ _serialPort = null;
+ }
+ }
+
+ private enum Command : byte
+ {
+ ReadCo2Concentration = 0x86,
+ CalibrateZeroPoint = 0x87,
+ CalibrateSpanPoint = 0x88,
+ AutoCalibrationSwitch = 0x79,
+ DetectionRangeSetting = 0x99
+ }
+
+ private enum MessageFormat
+ {
+ Start = 0x00,
+ SensorNum = 0x01,
+ Command = 0x02,
+ DataHighRequest = 0x03,
+ DataLowRequest = 0x04,
+ DataHighResponse = 0x02,
+ DataLowResponse = 0x03,
+ Checksum = 0x08
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/devices/Mhz19b/Mhz19b.csproj b/src/devices/Mhz19b/Mhz19b.csproj
new file mode 100644
index 0000000000..d6953ed678
--- /dev/null
+++ b/src/devices/Mhz19b/Mhz19b.csproj
@@ -0,0 +1,16 @@
+
+
+
+ netcoreapp2.1
+
+ false
+
+
+
+
+
+
+
+
+
+
diff --git a/src/devices/Mhz19b/Mhz19b.sln b/src/devices/Mhz19b/Mhz19b.sln
new file mode 100644
index 0000000000..927815d4f5
--- /dev/null
+++ b/src/devices/Mhz19b/Mhz19b.sln
@@ -0,0 +1,56 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30413.136
+MinimumVisualStudioVersion = 15.0.26124.0
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mhz19b", "Mhz19b.csproj", "{B82C190A-642B-465B-BD3F-DB56FFF22253}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{6A4DE7B1-03F3-4EE0-BF73-A0BAEF88BA2B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mhz19b.Samples", "samples\Mhz19b.Samples.csproj", "{3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Debug|x64.Build.0 = Debug|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Debug|x86.Build.0 = Debug|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Release|x64.ActiveCfg = Release|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Release|x64.Build.0 = Release|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Release|x86.ActiveCfg = Release|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Release|x86.Build.0 = Release|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Debug|x64.Build.0 = Debug|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Debug|x86.Build.0 = Debug|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Release|x64.ActiveCfg = Release|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Release|x64.Build.0 = Release|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Release|x86.ActiveCfg = Release|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF} = {6A4DE7B1-03F3-4EE0-BF73-A0BAEF88BA2B}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {2C4E84CC-5D02-4715-88D0-C7260026E6DE}
+ EndGlobalSection
+EndGlobal
diff --git a/src/devices/Mhz19b/README.md b/src/devices/Mhz19b/README.md
new file mode 100644
index 0000000000..d60800d4a7
--- /dev/null
+++ b/src/devices/Mhz19b/README.md
@@ -0,0 +1,46 @@
+# MH-Z19B CO2-Sensor
+
+## Summary
+Binding for the MH-Z19B NDIR infrared gas module. The gas module measures the CO2 gas concentration in the ambient air.
+
+## Binding Notes
+The MH-Z19B gas module provides a serial communication interface (UART) which can be directly wired to a Raspberry PI board. The module is supplied with 5V. The UART level is at 3.3V and no level shifter is required.
+
+|Function| Raspi pin| MH-Z19 pin|
+|--------|-----------|------------|
+|Vcc +5V |2 (+5V) |6 (Vin) |
+|GND |6 (GND) |7 (GND) |
+|UART |8 (TXD0) |2 (RXD) |
+|UART |10 (RXD0) |3 (TXD) |
+Table: MH-Z19B to RPi 3 connection
+
+The binding supports the connection through an UART interface (e.g. ```/dev/serial0```) or (serial port) stream.
+When using the UART interface the binding instantiates the port with the required UART settings and opens it.
+The use of an existing stream adds flexibility to the actual interface that used with the binding.
+In either case the binding supports all commands of the module.
+
+**Make sure that you read the datasheet carefully before altering the default calibration behaviour.
+Automatic baseline correction is enabled by default.**
+
+## Basic Usage
+The binding can be instantiated using an existing serial UART stream or with the name (e.g. ```/dev/serial0``` ) of the serial interface to be used.
+If using an existing stream ```shouldDispose``` indicates whether the stream shall be disposed when the binding gets disposed.
+If providing the name of the serial interface the connection gets closed and disposed when the binding is disposed.
+
+```
+public Mhz19b(Stream stream, bool shouldDispose)
+public Mhz19b(string uartDevice)
+```
+
+The CO2 concentration reading can be retrieved with
+
+```
+public VolumeConcentration GetCo2Reading()
+```
+
+The sample application demonstrates the use of the binding API for sensor calibration.
+
+**Note:** Refer to the datasheet for more details on sensor calibration **before** using the calibration API of the binding. You may decalibrate the sensor otherwise!
+
+## References
+[MH-Z19b Datasheet](https://www.winsen-sensor.com/d/files/infrared-gas-sensor/mh-z19b-co2-ver1_0.pdf)
diff --git a/src/devices/Mhz19b/samples/Mhz19b.Sample.cs b/src/devices/Mhz19b/samples/Mhz19b.Sample.cs
new file mode 100644
index 0000000000..e794a8de0e
--- /dev/null
+++ b/src/devices/Mhz19b/samples/Mhz19b.Sample.cs
@@ -0,0 +1,73 @@
+// 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.IO;
+using System.IO.Ports;
+using System.Text;
+using System.Threading;
+using UnitsNet;
+
+namespace Iot.Device.Mhz19b.Samples
+{
+ ///
+ /// Sample for MH-Z19B sensor
+ ///
+ public class Program
+ {
+ ///
+ /// Main entry point
+ ///
+ public static void Main(string[] args)
+ {
+ // create serial port using the setting acc. to datasheet, pg. 7, sec. general settings
+ var serialPort = new SerialPort("/dev/serial0", 9600, Parity.None, 8, StopBits.One)
+ {
+ Encoding = Encoding.ASCII,
+ ReadTimeout = 1000,
+ WriteTimeout = 1000
+ };
+ serialPort.Open();
+ Mhz19b sensor = new Mhz19b(serialPort.BaseStream, true);
+
+ // Alternatively you can let the binding create the serial port stream:
+ // Mhz19b sensor = new Mhz19b("/dev/serial0");
+
+ // Switch ABM on (default).
+ // sensor.SetAutomaticBaselineCorrection(AbmState.On);
+
+ // Set sensor detection range to 2000ppm (default).
+ // sensor.SetSensorDetectionRange(DetectionRange.Range2000);
+
+ // Perform calibration
+ // Step #1: perform zero point calibration
+ // Step #2: perform span point calibration at 2000ppm
+ // CAUTION: enable the following lines only if you know exactly what you do.
+ // Consider also that zero point and span point calibration are performed
+ // at different concentrations. The sensor requires up to 20 min to be
+ // saturated at the target level.
+ // sensor.PerformZeroPointCalibration();
+ // ---- Now change to target concentration for span point.
+ // sensor.PerformSpanPointCalibration(VolumeConcentration.FromPartsPerMillion(2000));
+
+ // Continously read current concentration
+ while (true)
+ {
+ try
+ {
+ VolumeConcentration reading = sensor.GetCo2Reading();
+ Console.WriteLine($"{reading.PartsPerMillion:F0} ppm");
+ }
+ catch (IOException e)
+ {
+ Console.WriteLine("Concentration couldn't be read");
+ Console.WriteLine(e.Message);
+ Console.WriteLine(e.InnerException.Message);
+ }
+
+ Thread.Sleep(1000);
+ }
+ }
+ }
+}
diff --git a/src/devices/Mhz19b/samples/Mhz19b.Samples.csproj b/src/devices/Mhz19b/samples/Mhz19b.Samples.csproj
new file mode 100644
index 0000000000..cb1c6dc474
--- /dev/null
+++ b/src/devices/Mhz19b/samples/Mhz19b.Samples.csproj
@@ -0,0 +1,13 @@
+
+
+
+ Exe
+ netcoreapp3.1
+
+
+
+
+
+
+
+
diff --git a/src/devices/Mhz19b/samples/README.md b/src/devices/Mhz19b/samples/README.md
new file mode 100644
index 0000000000..2894f5a068
--- /dev/null
+++ b/src/devices/Mhz19b/samples/README.md
@@ -0,0 +1,7 @@
+# Sample for MH-Z19B CO2 NDIR infrared gas module
+
+The sample demonstrates the reading of the current CO2 concentration.
+You need to wire the sensor to the UART of the RPi. The sensor requires a pre-heating time of 3 minutes.
+After pre-heating readings can be considered as stable and precise. The t90-time is given with 120s. However the response of the sensor is much faster. Therefore you can easily test the measurement by blowing your breath to the sensor.
+
+Please refer to [main documentation](../README.md) for more details.
\ No newline at end of file
diff --git a/src/devices/Mhz19b/samples/category.txt b/src/devices/Mhz19b/samples/category.txt
new file mode 100644
index 0000000000..222ee20533
--- /dev/null
+++ b/src/devices/Mhz19b/samples/category.txt
@@ -0,0 +1 @@
+gas sensors
\ No newline at end of file