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. + +![Sample wiring](./Ahtxx_sample.png) \ 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