From 7d6a502c9673465dcba7763f0be19d68bdee31ca Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Sat, 5 Sep 2020 11:57:08 +0200 Subject: [PATCH 01/21] Added support for AHT10/15/20 devices --- src/devices/Ahtxx/Aht20.cs | 22 +++ src/devices/Ahtxx/AhtBase.cs | 179 ++++++++++++++++++ src/devices/Ahtxx/Ahtxx.csproj | 15 ++ src/devices/Ahtxx/Ahtxx.sln | 53 ++++++ src/devices/Ahtxx/README.md | 18 ++ src/devices/Ahtxx/samples/Ahtxx.Sample.cs | 26 +++ .../Ahtxx/samples/Ahtxx.Samples.csproj | 13 ++ src/devices/Ahtxx/samples/README.md | 4 + 8 files changed, 330 insertions(+) create mode 100644 src/devices/Ahtxx/Aht20.cs create mode 100644 src/devices/Ahtxx/AhtBase.cs create mode 100644 src/devices/Ahtxx/Ahtxx.csproj create mode 100644 src/devices/Ahtxx/Ahtxx.sln create mode 100644 src/devices/Ahtxx/README.md create mode 100644 src/devices/Ahtxx/samples/Ahtxx.Sample.cs create mode 100644 src/devices/Ahtxx/samples/Ahtxx.Samples.csproj create mode 100644 src/devices/Ahtxx/samples/README.md diff --git a/src/devices/Ahtxx/Aht20.cs b/src/devices/Ahtxx/Aht20.cs new file mode 100644 index 0000000000..4c8820071b --- /dev/null +++ b/src/devices/Ahtxx/Aht20.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. + +using System.Device.I2c; + +namespace Iot.Device.Ahtxx +{ + /// + /// Add documentation here + /// + public class Aht20 : AhtBase + { + /// + /// Initializes a new instance of the class. + /// + public Aht20(I2cDevice i2cDevice) + : base(i2cDevice) + { + } + } +} diff --git a/src/devices/Ahtxx/AhtBase.cs b/src/devices/Ahtxx/AhtBase.cs new file mode 100644 index 0000000000..b84fea31bb --- /dev/null +++ b/src/devices/Ahtxx/AhtBase.cs @@ -0,0 +1,179 @@ +// 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 +{ + /// + /// AHT temperature and humidity sensor family. + /// It has been tested with AHT20, but should work with AHT10 and AHT15 as well. + /// Up to now all functions are contained in the base class as I'm not aware of differences + /// between the sensors. + /// + public class AhtBase : IDisposable + { + /// + /// Address of AHTxx 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 DeviceAddress = 0x38; + + private enum StatusBit : byte // datasheet version 1.1, table 10 + { + Calibrated = 0x08, + Busy = 0x80 + } + + private enum Command : byte + { + Calibrate = 0xbe, + SoftRest = 0xba, + Measure = 0xac + } + + private I2cDevice _i2cDevice = null; + private double _temperature; + private double _humidity; + + /// + /// Initializes a new instance of the device connected through I2C interface. + /// + /// Reference to the initialized I2C interface device + public AhtBase(I2cDevice i2cDevice) + { + _i2cDevice = i2cDevice ?? throw new ArgumentNullException(nameof(i2cDevice)); + + SoftReset(); // even if not clearly stated in datasheet, start with a software reset to assure clear start conditions + + // 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()) + { + Calibrate(); + } + } + + /// + /// Gets the current temperature reading from the sensor. + /// + /// Temperature reading + public Temperature Temperature + { + get + { + Measure(); + return new Temperature(_temperature, UnitsNet.Units.TemperatureUnit.DegreeCelsius); + } + } + + /// + /// Gets the current humidity reading from the sensor. + /// + /// Temperature reading + public Ratio Humidity + { + get + { + Measure(); + return new Ratio(_humidity, UnitsNet.Units.RatioUnit.Percent); + } + } + + /// + /// Perform sequence to retrieve current readings from device + /// + private void Measure() + { + Span buffer = stackalloc byte[3] + { + (byte)Command.Measure, + 0x33, // command parameters c.f. datasheet, version 1.1, ch. 5.4 + 0x00 + }; + _i2cDevice.Write(buffer); + + while (IsBusy()) + { + Thread.Sleep(10); + } + + buffer = stackalloc byte[6]; + _i2cDevice.Read(buffer); + + // Int32 rawHumidity; + // rawHumidity = buffer[1] << 8; + // rawHumidity |= buffer[2]; + // rawHumidity <<= 4; + // rawHumidity |= buffer[3] >> 4; + // _humidity = ((double)rawHumidity * 100) / 0x100000; + + // Int32 rawTemperature = buffer[3] & 0x0F; + // rawTemperature <<= 8; + // rawTemperature |= buffer[4]; + // rawTemperature <<= 8; + // rawTemperature |= buffer[5]; + // _temperature = ((double)rawTemperature * 200 / 0x100000) - 50; + Int32 rawHumidity = (buffer[1] << 12) | (buffer[2] << 4) | (buffer[3] >> 4); + _humidity = (rawHumidity * 100.0) / 0x100000; + Int32 rawTemperature = ((buffer[3] & 0xF) << 16) | (buffer[4] << 8) | buffer[5]; + _temperature = ((rawTemperature * 200.0) / 0x100000) - 50; + } + + /// + /// Perform soft reset command sequence + /// + private void SoftReset() + { + _i2cDevice.WriteByte((byte)Command.SoftRest); + Thread.Sleep(20); // reset requires 20ms at most, c.f. datasheet version 1.1, ch. 5.5 + } + + /// + /// Perform calibration command sequence + /// + private void Calibrate() + { + Span buffer = stackalloc byte[3] + { + (byte)Command.Calibrate, + 0x08, // command parameters c.f. datasheet, version 1.1, ch. 5.4 + 0x00 + }; + _i2cDevice.Write(buffer); + Thread.Sleep(10); // wait 10ms c.f. datasheet, version 1.1, ch. 5.4 + } + + private byte GetStatusByte() + { + _i2cDevice.WriteByte(0x71); + Thread.Sleep(10); // whithout this delay the reading the status fails often. + byte status = _i2cDevice.ReadByte(); + return status; + } + + private bool IsBusy() + { + return (GetStatusByte() & (byte)StatusBit.Busy) == (byte)StatusBit.Busy; + } + + private bool IsCalibrated() + { + return (GetStatusByte() & (byte)StatusBit.Calibrated) == (byte)StatusBit.Calibrated; + } + + /// + public void Dispose() + { + if (_i2cDevice != null) + { + _i2cDevice?.Dispose(); + _i2cDevice = null; + } + } + } +} 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..3727927d87 --- /dev/null +++ b/src/devices/Ahtxx/README.md @@ -0,0 +1,18 @@ +# Ahtxx + +## Summary +Provide a brief description on what the component is and its functionality. + +## Device Family +Provide a list of component names and link to datasheets (if available) the binding will work with. + +**[Family Name Here]**: [Datasheet link here] + +## Binding Notes + +Provide any specifics related to binding API. This could include how to configure component for particular functions and example code. + +**NOTE**: Don't repeat the basics related to System.Device.API* (e.g. connection settings, etc.). This helps keep text/steps down to a minimum for maintainability. + +## References +Provide any references to other tutorials, blogs and hardware related to the component that could help others get started. diff --git a/src/devices/Ahtxx/samples/Ahtxx.Sample.cs b/src/devices/Ahtxx/samples/Ahtxx.Sample.cs new file mode 100644 index 0000000000..73a8bb525b --- /dev/null +++ b/src/devices/Ahtxx/samples/Ahtxx.Sample.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. +// See the LICENSE file in the project root for more information. + +using System; +using System.Device.Gpio; +using System.Device.I2c; +using System.Device.Spi; +using System.Threading; + +namespace Iot.Device.Ahtxx.Samples +{ + /// + /// Samples for Ahtxx + /// + public class Program + { + /// + /// Main entry point + /// + public static void Main(string[] args) + { + Console.WriteLine("Hello Ahtxx Sample!"); + } + } +} diff --git a/src/devices/Ahtxx/samples/Ahtxx.Samples.csproj b/src/devices/Ahtxx/samples/Ahtxx.Samples.csproj new file mode 100644 index 0000000000..ff09566dc2 --- /dev/null +++ b/src/devices/Ahtxx/samples/Ahtxx.Samples.csproj @@ -0,0 +1,13 @@ + + + + Exe + netcoreapp2.1 + + + + + + + + diff --git a/src/devices/Ahtxx/samples/README.md b/src/devices/Ahtxx/samples/README.md new file mode 100644 index 0000000000..a93d5c33e4 --- /dev/null +++ b/src/devices/Ahtxx/samples/README.md @@ -0,0 +1,4 @@ +# TODO: This needs to be determined + +Help Wanted Please + From 27251b02b7f05b8f158f7188e6324ccb68ddf96a Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Sun, 6 Sep 2020 21:53:06 +0200 Subject: [PATCH 02/21] Cleanup and docuementation --- src/devices/Ahtxx/AhtBase.cs | 18 ++---------------- src/devices/Ahtxx/README.md | 14 ++++++-------- src/devices/Ahtxx/samples/Ahtxx.Sample.cs | 13 +++++++++++-- src/devices/Ahtxx/samples/Ahtxx.Samples.csproj | 2 +- src/devices/Ahtxx/samples/README.md | 4 ---- 5 files changed, 20 insertions(+), 31 deletions(-) delete mode 100644 src/devices/Ahtxx/samples/README.md diff --git a/src/devices/Ahtxx/AhtBase.cs b/src/devices/Ahtxx/AhtBase.cs index b84fea31bb..1b1d16f0c3 100644 --- a/src/devices/Ahtxx/AhtBase.cs +++ b/src/devices/Ahtxx/AhtBase.cs @@ -11,9 +11,8 @@ namespace Iot.Device.Ahtxx { /// /// AHT temperature and humidity sensor family. - /// It has been tested with AHT20, but should work with AHT10 and AHT15 as well. - /// Up to now all functions are contained in the base class as I'm not aware of differences - /// between the sensors. + /// Note: has been tested with AHT20 only, but should work with AHT10 and AHT15 as well. + /// Up to now all functions are contained in the base class, though there might be differences between the sensors types. /// public class AhtBase : IDisposable { @@ -105,19 +104,6 @@ private void Measure() buffer = stackalloc byte[6]; _i2cDevice.Read(buffer); - // Int32 rawHumidity; - // rawHumidity = buffer[1] << 8; - // rawHumidity |= buffer[2]; - // rawHumidity <<= 4; - // rawHumidity |= buffer[3] >> 4; - // _humidity = ((double)rawHumidity * 100) / 0x100000; - - // Int32 rawTemperature = buffer[3] & 0x0F; - // rawTemperature <<= 8; - // rawTemperature |= buffer[4]; - // rawTemperature <<= 8; - // rawTemperature |= buffer[5]; - // _temperature = ((double)rawTemperature * 200 / 0x100000) - 50; Int32 rawHumidity = (buffer[1] << 12) | (buffer[2] << 4) | (buffer[3] >> 4); _humidity = (rawHumidity * 100.0) / 0x100000; Int32 rawTemperature = ((buffer[3] & 0xF) << 16) | (buffer[4] << 8) | buffer[5]; diff --git a/src/devices/Ahtxx/README.md b/src/devices/Ahtxx/README.md index 3727927d87..c5dac980f8 100644 --- a/src/devices/Ahtxx/README.md +++ b/src/devices/Ahtxx/README.md @@ -1,18 +1,16 @@ # Ahtxx ## Summary -Provide a brief description on what the component is and its functionality. +AHTxx temperature and humidity sensor family. ## Device Family -Provide a list of component names and link to datasheets (if available) the binding will work with. - -**[Family Name Here]**: [Datasheet link here] +The binding supports the following types: +* AHT10 - http://www.aosong.com/en/products-40.html (not tested, yet) +* AHT20 - http://www.aosong.com/en/products-32.html ## Binding Notes -Provide any specifics related to binding API. This could include how to configure component for particular functions and example code. +The AHT-devices can be accessed as an I2C bus device. However, you can use only one device per bus as the address is fix. + -**NOTE**: Don't repeat the basics related to System.Device.API* (e.g. connection settings, etc.). This helps keep text/steps down to a minimum for maintainability. -## References -Provide any references to other tutorials, blogs and hardware related to the component that could help others get started. diff --git a/src/devices/Ahtxx/samples/Ahtxx.Sample.cs b/src/devices/Ahtxx/samples/Ahtxx.Sample.cs index 73a8bb525b..9442efcef4 100644 --- a/src/devices/Ahtxx/samples/Ahtxx.Sample.cs +++ b/src/devices/Ahtxx/samples/Ahtxx.Sample.cs @@ -13,14 +13,23 @@ namespace Iot.Device.Ahtxx.Samples /// /// Samples for Ahtxx /// - public class Program + internal class Program { /// /// Main entry point /// public static void Main(string[] args) { - Console.WriteLine("Hello Ahtxx Sample!"); + const int I2cBus = 1; + I2cConnectionSettings i2cSettings = new I2cConnectionSettings(I2cBus, Aht20.DeviceAddress); + I2cDevice i2cDevice = I2cDevice.Create(i2cSettings); + Aht20 aht20Sensor = new Aht20(i2cDevice); + + while (true) + { + Console.WriteLine($"{DateTime.Now.ToLongTimeString()}: {aht20Sensor.Temperature}, {aht20Sensor.Humidity}"); + Thread.Sleep(1000); + } } } } diff --git a/src/devices/Ahtxx/samples/Ahtxx.Samples.csproj b/src/devices/Ahtxx/samples/Ahtxx.Samples.csproj index ff09566dc2..a052a33d81 100644 --- a/src/devices/Ahtxx/samples/Ahtxx.Samples.csproj +++ b/src/devices/Ahtxx/samples/Ahtxx.Samples.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.1 + netcoreapp3.1 diff --git a/src/devices/Ahtxx/samples/README.md b/src/devices/Ahtxx/samples/README.md deleted file mode 100644 index a93d5c33e4..0000000000 --- a/src/devices/Ahtxx/samples/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# TODO: This needs to be determined - -Help Wanted Please - From 584614a6029584c1e16f876737cea81d087525b0 Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Sun, 6 Sep 2020 22:04:48 +0200 Subject: [PATCH 03/21] API changed according the IOT project conventions. --- src/devices/Ahtxx/AhtBase.cs | 18 ++++++------------ src/devices/Ahtxx/samples/Ahtxx.Sample.cs | 2 +- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/devices/Ahtxx/AhtBase.cs b/src/devices/Ahtxx/AhtBase.cs index 1b1d16f0c3..b67328997c 100644 --- a/src/devices/Ahtxx/AhtBase.cs +++ b/src/devices/Ahtxx/AhtBase.cs @@ -61,26 +61,20 @@ public AhtBase(I2cDevice i2cDevice) /// Gets the current temperature reading from the sensor. /// /// Temperature reading - public Temperature Temperature + public Temperature GetTemperature() { - get - { - Measure(); - return new Temperature(_temperature, UnitsNet.Units.TemperatureUnit.DegreeCelsius); - } + Measure(); + return new Temperature(_temperature, UnitsNet.Units.TemperatureUnit.DegreeCelsius); } /// /// Gets the current humidity reading from the sensor. /// /// Temperature reading - public Ratio Humidity + public Ratio GetHumidity() { - get - { - Measure(); - return new Ratio(_humidity, UnitsNet.Units.RatioUnit.Percent); - } + Measure(); + return new Ratio(_humidity, UnitsNet.Units.RatioUnit.Percent); } /// diff --git a/src/devices/Ahtxx/samples/Ahtxx.Sample.cs b/src/devices/Ahtxx/samples/Ahtxx.Sample.cs index 9442efcef4..b007b3fcdc 100644 --- a/src/devices/Ahtxx/samples/Ahtxx.Sample.cs +++ b/src/devices/Ahtxx/samples/Ahtxx.Sample.cs @@ -27,7 +27,7 @@ public static void Main(string[] args) while (true) { - Console.WriteLine($"{DateTime.Now.ToLongTimeString()}: {aht20Sensor.Temperature}, {aht20Sensor.Humidity}"); + Console.WriteLine($"{DateTime.Now.ToLongTimeString()}: {aht20Sensor.GetTemperature()}, {aht20Sensor.GetHumidity()}"); Thread.Sleep(1000); } } From 14428f6b2de71a862bd817583c5dd407d1a30eac Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Sat, 12 Sep 2020 21:11:02 +0200 Subject: [PATCH 04/21] Review remarks implemented --- src/devices/Ahtxx/Aht20.cs | 2 +- src/devices/Ahtxx/AhtBase.cs | 38 +++++++++++++++++++++++++++++------- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/devices/Ahtxx/Aht20.cs b/src/devices/Ahtxx/Aht20.cs index 4c8820071b..40f33bea9d 100644 --- a/src/devices/Ahtxx/Aht20.cs +++ b/src/devices/Ahtxx/Aht20.cs @@ -7,7 +7,7 @@ namespace Iot.Device.Ahtxx { /// - /// Add documentation here + /// AHT20 temperature and humidity sensor binding. /// public class Aht20 : AhtBase { diff --git a/src/devices/Ahtxx/AhtBase.cs b/src/devices/Ahtxx/AhtBase.cs index b67328997c..3fd7aa0072 100644 --- a/src/devices/Ahtxx/AhtBase.cs +++ b/src/devices/Ahtxx/AhtBase.cs @@ -35,6 +35,7 @@ private enum Command : byte Measure = 0xac } + private bool _disposed = false; private I2cDevice _i2cDevice = null; private double _temperature; private double _humidity; @@ -59,16 +60,18 @@ public AhtBase(I2cDevice i2cDevice) /// /// 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 new Temperature(_temperature, UnitsNet.Units.TemperatureUnit.DegreeCelsius); + return Temperature.FromDegreesCelsius(_temperature); } /// /// Gets the current humidity reading from the sensor. + /// Reading the humidity takes between 10 ms and 80 ms. /// /// Temperature reading public Ratio GetHumidity() @@ -90,6 +93,8 @@ private void Measure() }; _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); @@ -98,9 +103,15 @@ private void Measure() 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); - _humidity = (rawHumidity * 100.0) / 0x100000; 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; } @@ -146,14 +157,27 @@ private bool IsCalibrated() return (GetStatusByte() & (byte)StatusBit.Calibrated) == (byte)StatusBit.Calibrated; } - /// - public void Dispose() + /// + public void Dispose() => Dispose(true); + + /// + protected virtual void Dispose(bool disposing) { - if (_i2cDevice != null) + if (_disposed) { - _i2cDevice?.Dispose(); - _i2cDevice = null; + return; } + + if (disposing) + { + if (_i2cDevice != null) + { + _i2cDevice?.Dispose(); + _i2cDevice = null; + } + } + + _disposed = true; } } } From 79361c444aa97330f9939ba1cee933516de95d6b Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Mon, 21 Sep 2020 17:15:55 +0200 Subject: [PATCH 05/21] Initial implementation of the MH-Z19B binding --- src/devices/Mhz19b/AbmState.cs | 22 ++ src/devices/Mhz19b/DetectionRange.cs | 22 ++ src/devices/Mhz19b/Mhz19b.cs | 218 ++++++++++++++++++ src/devices/Mhz19b/Mhz19b.csproj | 16 ++ src/devices/Mhz19b/Mhz19b.sln | 56 +++++ src/devices/Mhz19b/README.md | 23 ++ src/devices/Mhz19b/samples/Mhz19b.Sample.cs | 34 +++ .../Mhz19b/samples/Mhz19b.Samples.csproj | 13 ++ src/devices/Mhz19b/samples/README.md | 4 + 9 files changed, 408 insertions(+) create mode 100644 src/devices/Mhz19b/AbmState.cs create mode 100644 src/devices/Mhz19b/DetectionRange.cs create mode 100644 src/devices/Mhz19b/Mhz19b.cs create mode 100644 src/devices/Mhz19b/Mhz19b.csproj create mode 100644 src/devices/Mhz19b/Mhz19b.sln create mode 100644 src/devices/Mhz19b/README.md create mode 100644 src/devices/Mhz19b/samples/Mhz19b.Sample.cs create mode 100644 src/devices/Mhz19b/samples/Mhz19b.Samples.csproj create mode 100644 src/devices/Mhz19b/samples/README.md diff --git a/src/devices/Mhz19b/AbmState.cs b/src/devices/Mhz19b/AbmState.cs new file mode 100644 index 0000000000..5e0144768c --- /dev/null +++ b/src/devices/Mhz19b/AbmState.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 if automatic baseline correction is on or off + /// + public enum AbmState + { + /// + /// ABM off + /// + Off = 0x00, + + /// + /// ABM on + /// + On = 0x0a + } +} 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..195da70bc2 --- /dev/null +++ b/src/devices/Mhz19b/Mhz19b.cs @@ -0,0 +1,218 @@ +// 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.Ports; +using System.Text; +using System.Threading; +using UnitsNet; + +namespace Iot.Device.Mhz19b +{ + /// + /// Add documentation here + /// + public sealed partial class Mhz19b : IDisposable + { + private enum CommonMessageBytes + { + Start = 0xff, + SensorNumber = 0x01, + Empty = 0x00 + } + + private enum Command : byte + { + ReadCo2Concentration = 0x86, + CalibrateZeroPoint = 0x87, + CalibrateSpanPoint = 0x88, + AutoCalibrationSwitch = 0x79, + DetectionRangeSetting = 0x99 + } + + private enum MessageFormat + { + Start = 0x00, + SensorNum = 0x01, + Command = 0x02, + DataHigh = 0x03, + DataLow = 0x04, + Checksum = 0x08 + } + + private const int MessageSize = 9; + + private bool _disposed = false; + private SerialPort _serialPort = null; + + /// + /// Initializes a new instance of the class. + /// + /// Path to the UART device / serial port, e.g. /dev/serial0 + public Mhz19b(string uartDevice) + { + if (uartDevice == null) + { + throw new ArgumentNullException(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); + _serialPort.Encoding = Encoding.ASCII; + _serialPort.ReadTimeout = 1000; + _serialPort.WriteTimeout = 1000; + } + + /// + /// Gets the current CO2 concentration from the sensor. + /// The validity is true if the current concentration was successfully read. + /// If the serial communication timed out or the checksum was invalid the validity is false. + /// If the validity is false the ratio is set to 0. + /// + /// CO2 concentration in ppm and validity + public (Ratio, bool) GetCo2Reading() + { + Ratio concentration = Ratio.FromPartsPerMillion(0); + bool validity = false; + + try + { + var request = CreateRequest(Command.ReadCo2Concentration); + request[(int)MessageFormat.Checksum] = Checksum(request); + + _serialPort.Open(); + _serialPort.Write(request, 0, request.Length); + + byte[] response = new byte[MessageSize]; + if ((_serialPort.Read(response, 0, response.Length) == response.Length) && (response[(int)MessageFormat.Checksum] == Checksum(response))) + { + concentration = Ratio.FromPartsPerMillion((int)response[(int)MessageFormat.DataHigh] * 256 + (int)response[(int)MessageFormat.DataLow]); + validity = true; + } + } + finally + { + _serialPort.Close(); + } + + return (concentration, validity); + } + + /// + /// Initiates a zero point calibration. + /// The sensor doesn't respond anything, so this is fire and forget. + /// + /// true, if the command could be send + public bool ZeroPointCalibration() => SendRequest(CreateRequest(Command.CalibrateZeroPoint)); + + /// + /// Initiate a span point calibration. + /// The sensor doesn't respond anything, so this is fire and forget. + /// + /// span value, e.g. 2000[ppm] + /// true, if the command could be send/// + public bool SpanPointCalibration(int span) + { + var request = CreateRequest(Command.CalibrateSpanPoint); + // set span in request, c. f. datasheet rev. 1.0, pg. 8 for details + request[(int)MessageFormat.DataHigh] = (byte)(span / 256); + request[(int)MessageFormat.DataLow] = (byte)(span % 256); + + return SendRequest(request); + } + + /// + /// Switches the autmatic baseline correction on and off. + /// The sensor doesn't respond anything, so this is fire and forget. + /// + /// State of automatic correction + /// true, if the command could be send + public bool AutomaticBaselineCorrection(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.DataHigh] = (byte)state; + + return SendRequest(request); + } + + /// + /// Set the sensor detection range. + /// The sensor doesn't respond anything, so this is fire and forget + /// + /// Detection range of the sensor + /// true, if the command could be send + public bool SensorDetectionRange(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.DataHigh] = (byte)((int)detectionRange / 256); + request[(int)MessageFormat.DataLow] = (byte)((int)detectionRange % 256); + + return SendRequest(request); + } + + private bool SendRequest(byte[] request) + { + bool validity = false; + + request[(int)MessageFormat.Checksum] = Checksum(request); + + try + { + _serialPort.Open(); + _serialPort.Write(request, 0, request.Length); + validity = true; + } + finally + { + _serialPort.Close(); + } + + return validity; + } + + private byte[] CreateRequest(Command command) => new byte[] + { + (byte)CommonMessageBytes.Start, (byte)CommonMessageBytes.SensorNumber, (byte)command, + (byte)CommonMessageBytes.Empty, (byte)CommonMessageBytes.Empty, (byte)CommonMessageBytes.Empty, + (byte)CommonMessageBytes.Empty, (byte)CommonMessageBytes.Empty, (byte)CommonMessageBytes.Empty + }; + + /// + /// 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 (_disposed) + { + return; + } + + if (_serialPort != null) + { + _serialPort.Dispose(); + _serialPort = null; + _disposed = true; + } + } + } +} \ 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..360551ddc7 --- /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..15bb1d3a94 --- /dev/null +++ b/src/devices/Mhz19b/README.md @@ -0,0 +1,23 @@ +# 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 binding supports the connection through its UART interface. The binding gets configured with the UART to be used (e.g. ```/dev/serial0```). The UART is held open only while reading the current concentration from the sensor to enable UART multiplexing. + +The sensor 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 + +**Make sure that you read the datasheet carefully before altering the default calibration behaviour. +Automatic baseline correction is enabled by default.** + +## 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..5e11fb2f16 --- /dev/null +++ b/src/devices/Mhz19b/samples/Mhz19b.Sample.cs @@ -0,0 +1,34 @@ +// 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; + +namespace Iot.Device.Mhz19b.Samples +{ + /// + /// Samples for Mhz19b + /// + public class Program + { + /// + /// Main entry point + /// + public static void Main(string[] args) + { + Mhz19b sensor = new Mhz19b("/dev/serial0"); + while (true) + { + var reading = sensor.GetCo2Reading(); + if (reading.Item2) + { + Console.WriteLine($"{reading.Item1}"); + } + else + { + Console.WriteLine("Concentration counldn't be read"); + } + } + } + } +} diff --git a/src/devices/Mhz19b/samples/Mhz19b.Samples.csproj b/src/devices/Mhz19b/samples/Mhz19b.Samples.csproj new file mode 100644 index 0000000000..fabc37b0ea --- /dev/null +++ b/src/devices/Mhz19b/samples/Mhz19b.Samples.csproj @@ -0,0 +1,13 @@ + + + + Exe + netcoreapp2.1 + + + + + + + + diff --git a/src/devices/Mhz19b/samples/README.md b/src/devices/Mhz19b/samples/README.md new file mode 100644 index 0000000000..a93d5c33e4 --- /dev/null +++ b/src/devices/Mhz19b/samples/README.md @@ -0,0 +1,4 @@ +# TODO: This needs to be determined + +Help Wanted Please + From e736c5ea4fadc65aad8fe01d895bee01bbf49ca4 Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Mon, 21 Sep 2020 21:02:17 +0200 Subject: [PATCH 06/21] -Implementation completed -Documentation extended -Functions manually tested --- src/devices/Mhz19b/AbmState.cs | 9 +-- src/devices/Mhz19b/Mhz19b.cs | 76 ++++++++++++--------- src/devices/Mhz19b/samples/Mhz19b.Sample.cs | 12 ++-- src/devices/Mhz19b/samples/README.md | 7 +- 4 files changed, 62 insertions(+), 42 deletions(-) diff --git a/src/devices/Mhz19b/AbmState.cs b/src/devices/Mhz19b/AbmState.cs index 5e0144768c..18ca819f9c 100644 --- a/src/devices/Mhz19b/AbmState.cs +++ b/src/devices/Mhz19b/AbmState.cs @@ -5,18 +5,19 @@ namespace Iot.Device.Mhz19b { /// - /// Defines if automatic baseline correction is on or off + /// 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 + /// ABM off (value acc. to datasheet) /// Off = 0x00, /// - /// ABM on + /// ABM on (value acc. to datasheet) /// - On = 0x0a + On = 0xA0 } } diff --git a/src/devices/Mhz19b/Mhz19b.cs b/src/devices/Mhz19b/Mhz19b.cs index 195da70bc2..be7dca6739 100644 --- a/src/devices/Mhz19b/Mhz19b.cs +++ b/src/devices/Mhz19b/Mhz19b.cs @@ -11,17 +11,10 @@ namespace Iot.Device.Mhz19b { /// - /// Add documentation here + /// MH-Z19B CO2 concentration sensor binding /// - public sealed partial class Mhz19b : IDisposable + public sealed class Mhz19b : IDisposable { - private enum CommonMessageBytes - { - Start = 0xff, - SensorNumber = 0x01, - Empty = 0x00 - } - private enum Command : byte { ReadCo2Concentration = 0x86, @@ -36,8 +29,10 @@ private enum MessageFormat Start = 0x00, SensorNum = 0x01, Command = 0x02, - DataHigh = 0x03, - DataLow = 0x04, + DataHighRequest = 0x03, + DataLowRequest = 0x04, + DataHighResponse = 0x02, + DataLowResponse = 0x03, Checksum = 0x08 } @@ -71,11 +66,8 @@ public Mhz19b(string uartDevice) /// If the validity is false the ratio is set to 0. /// /// CO2 concentration in ppm and validity - public (Ratio, bool) GetCo2Reading() + public (Ratio concentration, bool validity) GetCo2Reading() { - Ratio concentration = Ratio.FromPartsPerMillion(0); - bool validity = false; - try { var request = CreateRequest(Command.ReadCo2Concentration); @@ -84,19 +76,38 @@ public Mhz19b(string uartDevice) _serialPort.Open(); _serialPort.Write(request, 0, request.Length); + // wait until the response has been completely received + int timeout = 100; + while (timeout > 0 && _serialPort.BytesToRead < MessageSize) + { + Thread.Sleep(1); + timeout--; + } + + if (timeout == 0) + { + return (Ratio.FromPartsPerMillion(0), false); + } + + // read and process response byte[] response = new byte[MessageSize]; if ((_serialPort.Read(response, 0, response.Length) == response.Length) && (response[(int)MessageFormat.Checksum] == Checksum(response))) { - concentration = Ratio.FromPartsPerMillion((int)response[(int)MessageFormat.DataHigh] * 256 + (int)response[(int)MessageFormat.DataLow]); - validity = true; + return (Ratio.FromPartsPerMillion((int)response[(int)MessageFormat.DataHighResponse] * 256 + (int)response[(int)MessageFormat.DataLowResponse]), true); + } + else + { + return (Ratio.FromPartsPerMillion(0), false); } } + catch + { // no need for details here + return (Ratio.FromPartsPerMillion(0), false); + } finally { _serialPort.Close(); } - - return (concentration, validity); } /// @@ -116,8 +127,8 @@ public bool SpanPointCalibration(int span) { var request = CreateRequest(Command.CalibrateSpanPoint); // set span in request, c. f. datasheet rev. 1.0, pg. 8 for details - request[(int)MessageFormat.DataHigh] = (byte)(span / 256); - request[(int)MessageFormat.DataLow] = (byte)(span % 256); + request[(int)MessageFormat.DataHighRequest] = (byte)(span / 256); + request[(int)MessageFormat.DataLowRequest] = (byte)(span % 256); return SendRequest(request); } @@ -132,7 +143,7 @@ public bool AutomaticBaselineCorrection(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.DataHigh] = (byte)state; + request[(int)MessageFormat.DataHighRequest] = (byte)state; return SendRequest(request); } @@ -147,37 +158,38 @@ public bool SensorDetectionRange(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.DataHigh] = (byte)((int)detectionRange / 256); - request[(int)MessageFormat.DataLow] = (byte)((int)detectionRange % 256); + request[(int)MessageFormat.DataHighRequest] = (byte)((int)detectionRange / 256); + request[(int)MessageFormat.DataLowRequest] = (byte)((int)detectionRange % 256); return SendRequest(request); } private bool SendRequest(byte[] request) { - bool validity = false; - request[(int)MessageFormat.Checksum] = Checksum(request); try { _serialPort.Open(); _serialPort.Write(request, 0, request.Length); - validity = true; + return true; + } + catch + { // no need fo details here + return false; } finally { _serialPort.Close(); } - - return validity; } private byte[] CreateRequest(Command command) => new byte[] { - (byte)CommonMessageBytes.Start, (byte)CommonMessageBytes.SensorNumber, (byte)command, - (byte)CommonMessageBytes.Empty, (byte)CommonMessageBytes.Empty, (byte)CommonMessageBytes.Empty, - (byte)CommonMessageBytes.Empty, (byte)CommonMessageBytes.Empty, (byte)CommonMessageBytes.Empty + 0xff, // start byte, + 0x01, // sensor number, always 0x1 + (byte)command, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // empty bytes }; /// diff --git a/src/devices/Mhz19b/samples/Mhz19b.Sample.cs b/src/devices/Mhz19b/samples/Mhz19b.Sample.cs index 5e11fb2f16..bba262d2c7 100644 --- a/src/devices/Mhz19b/samples/Mhz19b.Sample.cs +++ b/src/devices/Mhz19b/samples/Mhz19b.Sample.cs @@ -3,11 +3,13 @@ // See the LICENSE file in the project root for more information. using System; +using System.Threading; +using UnitsNet; namespace Iot.Device.Mhz19b.Samples { /// - /// Samples for Mhz19b + /// Sample for MH-Z19B sensor /// public class Program { @@ -19,15 +21,17 @@ public static void Main(string[] args) Mhz19b sensor = new Mhz19b("/dev/serial0"); while (true) { - var reading = sensor.GetCo2Reading(); - if (reading.Item2) + (Ratio concentration, bool validity) reading = sensor.GetCo2Reading(); + if (reading.validity) { - Console.WriteLine($"{reading.Item1}"); + Console.WriteLine($"{reading.concentration}"); } else { Console.WriteLine("Concentration counldn't be read"); } + + Thread.Sleep(1000); } } } diff --git a/src/devices/Mhz19b/samples/README.md b/src/devices/Mhz19b/samples/README.md index a93d5c33e4..2894f5a068 100644 --- a/src/devices/Mhz19b/samples/README.md +++ b/src/devices/Mhz19b/samples/README.md @@ -1,4 +1,7 @@ -# TODO: This needs to be determined +# Sample for MH-Z19B CO2 NDIR infrared gas module -Help Wanted Please +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 From b0a55d226759f71c8affff3f48d1cda24fc4c369 Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Wed, 23 Sep 2020 12:50:30 +0200 Subject: [PATCH 07/21] Review remarks implemented --- src/devices/Ahtxx/AhtBase.cs | 33 ++++++------ src/devices/Ahtxx/category.txt | 2 + .../Ahtxx/samples/Ahtxx.Samples.csproj | 2 +- src/devices/Mhz19b/Mhz19b.cs | 53 ++++++++++--------- src/devices/Mhz19b/Mhz19b.csproj | 2 +- src/devices/Mhz19b/samples/Mhz19b.Sample.cs | 23 +++++++- src/devices/Mhz19b/samples/category.txt | 1 + 7 files changed, 69 insertions(+), 47 deletions(-) create mode 100644 src/devices/Ahtxx/category.txt create mode 100644 src/devices/Mhz19b/samples/category.txt diff --git a/src/devices/Ahtxx/AhtBase.cs b/src/devices/Ahtxx/AhtBase.cs index 3fd7aa0072..261291b888 100644 --- a/src/devices/Ahtxx/AhtBase.cs +++ b/src/devices/Ahtxx/AhtBase.cs @@ -22,7 +22,8 @@ public class AhtBase : IDisposable /// public const int DeviceAddress = 0x38; - private enum StatusBit : byte // datasheet version 1.1, table 10 + // datasheet version 1.1, table 10 + private enum StatusBit : byte { Calibrated = 0x08, Busy = 0x80 @@ -35,7 +36,6 @@ private enum Command : byte Measure = 0xac } - private bool _disposed = false; private I2cDevice _i2cDevice = null; private double _temperature; private double _humidity; @@ -48,7 +48,8 @@ public AhtBase(I2cDevice i2cDevice) { _i2cDevice = i2cDevice ?? throw new ArgumentNullException(nameof(i2cDevice)); - SoftReset(); // even if not clearly stated in datasheet, start with a software reset to assure clear start conditions + // 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 @@ -87,8 +88,9 @@ private void Measure() { Span buffer = stackalloc byte[3] { + // command parameters c.f. datasheet, version 1.1, ch. 5.4 (byte)Command.Measure, - 0x33, // command parameters c.f. datasheet, version 1.1, ch. 5.4 + 0x33, 0x00 }; _i2cDevice.Write(buffer); @@ -121,7 +123,8 @@ private void Measure() private void SoftReset() { _i2cDevice.WriteByte((byte)Command.SoftRest); - Thread.Sleep(20); // reset requires 20ms at most, c.f. datasheet version 1.1, ch. 5.5 + // reset requires 20ms at most, c.f. datasheet version 1.1, ch. 5.5 + Thread.Sleep(20); } /// @@ -136,13 +139,15 @@ private void Calibrate() 0x00 }; _i2cDevice.Write(buffer); - Thread.Sleep(10); // wait 10ms c.f. datasheet, version 1.1, ch. 5.4 + // wait 10ms c.f. datasheet, version 1.1, ch. 5.4 + Thread.Sleep(10); } private byte GetStatusByte() { _i2cDevice.WriteByte(0x71); - Thread.Sleep(10); // whithout this delay the reading the status fails often. + // whithout this delay the reading the status fails often. + Thread.Sleep(10); byte status = _i2cDevice.ReadByte(); return status; } @@ -163,21 +168,13 @@ private bool IsCalibrated() /// protected virtual void Dispose(bool disposing) { - if (_disposed) + if (_i2cDevice == null) { return; } - if (disposing) - { - if (_i2cDevice != null) - { - _i2cDevice?.Dispose(); - _i2cDevice = null; - } - } - - _disposed = true; + _i2cDevice?.Dispose(); + _i2cDevice = null; } } } 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.Samples.csproj b/src/devices/Ahtxx/samples/Ahtxx.Samples.csproj index a052a33d81..ff09566dc2 100644 --- a/src/devices/Ahtxx/samples/Ahtxx.Samples.csproj +++ b/src/devices/Ahtxx/samples/Ahtxx.Samples.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + netcoreapp2.1 diff --git a/src/devices/Mhz19b/Mhz19b.cs b/src/devices/Mhz19b/Mhz19b.cs index be7dca6739..e8ab06fd49 100644 --- a/src/devices/Mhz19b/Mhz19b.cs +++ b/src/devices/Mhz19b/Mhz19b.cs @@ -38,7 +38,6 @@ private enum MessageFormat private const int MessageSize = 9; - private bool _disposed = false; private SerialPort _serialPort = null; /// @@ -47,9 +46,9 @@ private enum MessageFormat /// Path to the UART device / serial port, e.g. /dev/serial0 public Mhz19b(string uartDevice) { - if (uartDevice == null) + if (string.IsNullOrEmpty(uartDevice)) { - throw new ArgumentNullException(nameof(uartDevice)); + throw new ArgumentException(nameof(uartDevice)); } // create serial port using the setting acc. to datasheet, pg. 7, sec. general settings @@ -66,7 +65,7 @@ public Mhz19b(string uartDevice) /// If the validity is false the ratio is set to 0. /// /// CO2 concentration in ppm and validity - public (Ratio concentration, bool validity) GetCo2Reading() + public (VolumeConcentration concentration, bool validity) GetCo2Reading() { try { @@ -86,23 +85,23 @@ public Mhz19b(string uartDevice) if (timeout == 0) { - return (Ratio.FromPartsPerMillion(0), false); + return (VolumeConcentration.Zero, false); } // read and process response byte[] response = new byte[MessageSize]; if ((_serialPort.Read(response, 0, response.Length) == response.Length) && (response[(int)MessageFormat.Checksum] == Checksum(response))) { - return (Ratio.FromPartsPerMillion((int)response[(int)MessageFormat.DataHighResponse] * 256 + (int)response[(int)MessageFormat.DataLowResponse]), true); + return (VolumeConcentration.FromPartsPerMillion((int)response[(int)MessageFormat.DataHighResponse] * 256 + (int)response[(int)MessageFormat.DataLowResponse]), true); } else { - return (Ratio.FromPartsPerMillion(0), false); + return (VolumeConcentration.Zero, false); } } catch { // no need for details here - return (Ratio.FromPartsPerMillion(0), false); + return (VolumeConcentration.Zero, false); } finally { @@ -115,20 +114,26 @@ public Mhz19b(string uartDevice) /// The sensor doesn't respond anything, so this is fire and forget. /// /// true, if the command could be send - public bool ZeroPointCalibration() => SendRequest(CreateRequest(Command.CalibrateZeroPoint)); + public bool PerformZeroPointCalibration() => SendRequest(CreateRequest(Command.CalibrateZeroPoint)); /// /// Initiate a span point calibration. /// The sensor doesn't respond anything, so this is fire and forget. /// - /// span value, e.g. 2000[ppm] + /// span value, between 1000[ppm] and 5000[ppm]. The typical value is 2000[ppm]. /// true, if the command could be send/// - public bool SpanPointCalibration(int span) + /// Thrown when span value is out of range + public bool 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 / 256); - request[(int)MessageFormat.DataLowRequest] = (byte)(span % 256); + request[(int)MessageFormat.DataHighRequest] = (byte)(span.PartsPerMillion / 256); + request[(int)MessageFormat.DataLowRequest] = (byte)(span.PartsPerMillion % 256); return SendRequest(request); } @@ -139,7 +144,7 @@ public bool SpanPointCalibration(int span) /// /// State of automatic correction /// true, if the command could be send - public bool AutomaticBaselineCorrection(AbmState state) + public bool SetAutomaticBaselineCorrection(AbmState state) { var request = CreateRequest(Command.AutoCalibrationSwitch); // set on/off state in request, c. f. datasheet rev. 1.0, pg. 8 for details @@ -154,7 +159,7 @@ public bool AutomaticBaselineCorrection(AbmState state) /// /// Detection range of the sensor /// true, if the command could be send - public bool SensorDetectionRange(DetectionRange detectionRange) + public bool SetSensorDetectionRange(DetectionRange detectionRange) { var request = CreateRequest(Command.DetectionRangeSetting); // set detection range in request, c. f. datasheet rev. 1.0, pg. 8 for details @@ -171,11 +176,13 @@ private bool SendRequest(byte[] request) try { _serialPort.Open(); - _serialPort.Write(request, 0, request.Length); - return true; + // _serialPort.Write(request, 0, request.Length); + return false; + // return true; } catch - { // no need fo details here + { + // no need fo details here return false; } finally @@ -214,17 +221,13 @@ private byte Checksum(byte[] packet) /// public void Dispose() { - if (_disposed) + if (_serialPort == null) { return; } - if (_serialPort != null) - { - _serialPort.Dispose(); - _serialPort = null; - _disposed = true; - } + _serialPort.Dispose(); + _serialPort = null; } } } \ No newline at end of file diff --git a/src/devices/Mhz19b/Mhz19b.csproj b/src/devices/Mhz19b/Mhz19b.csproj index 360551ddc7..d6953ed678 100644 --- a/src/devices/Mhz19b/Mhz19b.csproj +++ b/src/devices/Mhz19b/Mhz19b.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/devices/Mhz19b/samples/Mhz19b.Sample.cs b/src/devices/Mhz19b/samples/Mhz19b.Sample.cs index bba262d2c7..f2e94c5577 100644 --- a/src/devices/Mhz19b/samples/Mhz19b.Sample.cs +++ b/src/devices/Mhz19b/samples/Mhz19b.Sample.cs @@ -19,16 +19,35 @@ public class Program public static void Main(string[] args) { 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(200)); + + // Continously read current concentration while (true) { - (Ratio concentration, bool validity) reading = sensor.GetCo2Reading(); + (VolumeConcentration concentration, bool validity) reading = sensor.GetCo2Reading(); if (reading.validity) { Console.WriteLine($"{reading.concentration}"); } else { - Console.WriteLine("Concentration counldn't be read"); + Console.WriteLine("Concentration couldn't be read"); } Thread.Sleep(1000); 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 From 82ace237c528fe38074dd4a371057125c6aadb1c Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Wed, 23 Sep 2020 14:51:17 +0200 Subject: [PATCH 08/21] Changed from return code to exception. Removed debug change from SendRequest method --- src/devices/Mhz19b/Mhz19b.cs | 52 ++++++++++----------- src/devices/Mhz19b/samples/Mhz19b.Sample.cs | 9 ++-- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/src/devices/Mhz19b/Mhz19b.cs b/src/devices/Mhz19b/Mhz19b.cs index e8ab06fd49..d69d89dd28 100644 --- a/src/devices/Mhz19b/Mhz19b.cs +++ b/src/devices/Mhz19b/Mhz19b.cs @@ -3,6 +3,7 @@ // 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; @@ -44,6 +45,7 @@ private enum MessageFormat /// Initializes a new instance of the class. /// /// Path to the UART device / serial port, e.g. /dev/serial0 + /// uartDevice is null or empty public Mhz19b(string uartDevice) { if (string.IsNullOrEmpty(uartDevice)) @@ -65,7 +67,8 @@ public Mhz19b(string uartDevice) /// If the validity is false the ratio is set to 0. /// /// CO2 concentration in ppm and validity - public (VolumeConcentration concentration, bool validity) GetCo2Reading() + /// Communication with sensor failed + public VolumeConcentration GetCo2Reading() { try { @@ -85,23 +88,23 @@ public Mhz19b(string uartDevice) if (timeout == 0) { - return (VolumeConcentration.Zero, false); + throw new IOException("Timeout"); } // read and process response byte[] response = new byte[MessageSize]; if ((_serialPort.Read(response, 0, response.Length) == response.Length) && (response[(int)MessageFormat.Checksum] == Checksum(response))) { - return (VolumeConcentration.FromPartsPerMillion((int)response[(int)MessageFormat.DataHighResponse] * 256 + (int)response[(int)MessageFormat.DataLowResponse]), true); + return VolumeConcentration.FromPartsPerMillion((int)response[(int)MessageFormat.DataHighResponse] * 256 + (int)response[(int)MessageFormat.DataLowResponse]); } else { - return (VolumeConcentration.Zero, false); + throw new IOException("Invalid response message received from sensor"); } } - catch - { // no need for details here - return (VolumeConcentration.Zero, false); + catch (Exception e) + { + throw new IOException("Sensor communication failed", e); } finally { @@ -113,17 +116,17 @@ public Mhz19b(string uartDevice) /// Initiates a zero point calibration. /// The sensor doesn't respond anything, so this is fire and forget. /// - /// true, if the command could be send - public bool PerformZeroPointCalibration() => SendRequest(CreateRequest(Command.CalibrateZeroPoint)); + /// Communication with sensor failed + public void PerformZeroPointCalibration() => SendRequest(CreateRequest(Command.CalibrateZeroPoint)); /// /// Initiate a span point calibration. /// The sensor doesn't respond anything, so this is fire and forget. /// /// span value, between 1000[ppm] and 5000[ppm]. The typical value is 2000[ppm]. - /// true, if the command could be send/// - /// Thrown when span value is out of range - public bool PerformSpanPointCalibration(VolumeConcentration span) + /// Thrown when span value is out of range + /// Communication with sensor failed + public void PerformSpanPointCalibration(VolumeConcentration span) { if ((span.PartsPerMillion < 1000) || (span.PartsPerMillion > 5000)) { @@ -135,7 +138,7 @@ public bool PerformSpanPointCalibration(VolumeConcentration span) request[(int)MessageFormat.DataHighRequest] = (byte)(span.PartsPerMillion / 256); request[(int)MessageFormat.DataLowRequest] = (byte)(span.PartsPerMillion % 256); - return SendRequest(request); + SendRequest(request); } /// @@ -143,14 +146,14 @@ public bool PerformSpanPointCalibration(VolumeConcentration span) /// The sensor doesn't respond anything, so this is fire and forget. /// /// State of automatic correction - /// true, if the command could be send - public bool SetAutomaticBaselineCorrection(AbmState state) + /// 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; - return SendRequest(request); + SendRequest(request); } /// @@ -158,32 +161,29 @@ public bool SetAutomaticBaselineCorrection(AbmState state) /// The sensor doesn't respond anything, so this is fire and forget /// /// Detection range of the sensor - /// true, if the command could be send - public bool SetSensorDetectionRange(DetectionRange detectionRange) + /// 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); - return SendRequest(request); + SendRequest(request); } - private bool SendRequest(byte[] request) + private void SendRequest(byte[] request) { request[(int)MessageFormat.Checksum] = Checksum(request); try { _serialPort.Open(); - // _serialPort.Write(request, 0, request.Length); - return false; - // return true; + _serialPort.Write(request, 0, request.Length); } - catch + catch (Exception e) { - // no need fo details here - return false; + throw new IOException("Sensor communication failed", e); } finally { diff --git a/src/devices/Mhz19b/samples/Mhz19b.Sample.cs b/src/devices/Mhz19b/samples/Mhz19b.Sample.cs index f2e94c5577..261c7515ab 100644 --- a/src/devices/Mhz19b/samples/Mhz19b.Sample.cs +++ b/src/devices/Mhz19b/samples/Mhz19b.Sample.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.IO; using System.Threading; using UnitsNet; @@ -40,12 +41,12 @@ public static void Main(string[] args) // Continously read current concentration while (true) { - (VolumeConcentration concentration, bool validity) reading = sensor.GetCo2Reading(); - if (reading.validity) + try { - Console.WriteLine($"{reading.concentration}"); + VolumeConcentration reading = sensor.GetCo2Reading(); + Console.WriteLine($"{reading}"); } - else + catch (IOException) { Console.WriteLine("Concentration couldn't be read"); } From 75605bb509c74e6b7b98c208e0d999326f4e8fcb Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Thu, 24 Sep 2020 20:00:22 +0200 Subject: [PATCH 09/21] Mhz19b binding with stream support --- src/devices/Mhz19b/Mhz19b.cs | 62 ++++++++++++------- src/devices/Mhz19b/README.md | 13 ++-- src/devices/Mhz19b/samples/Mhz19b.Sample.cs | 19 +++++- .../Mhz19b/samples/Mhz19b.Samples.csproj | 2 +- 4 files changed, 66 insertions(+), 30 deletions(-) diff --git a/src/devices/Mhz19b/Mhz19b.cs b/src/devices/Mhz19b/Mhz19b.cs index d69d89dd28..a339c6f470 100644 --- a/src/devices/Mhz19b/Mhz19b.cs +++ b/src/devices/Mhz19b/Mhz19b.cs @@ -40,9 +40,19 @@ private enum MessageFormat private const int MessageSize = 9; private SerialPort _serialPort = null; + private Stream _serialPortStream = null; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class using an existing (serial port) stream. + /// + /// Existing stream + public Mhz19b(Stream stream) + { + _serialPortStream = stream ?? throw new ArgumentNullException(nameof(stream)); + } + + /// + /// 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 @@ -58,6 +68,8 @@ public Mhz19b(string uartDevice) _serialPort.Encoding = Encoding.ASCII; _serialPort.ReadTimeout = 1000; _serialPort.WriteTimeout = 1000; + _serialPort.Open(); + _serialPortStream = _serialPort.BaseStream; } /// @@ -72,16 +84,18 @@ public VolumeConcentration GetCo2Reading() { try { + // send read command request var request = CreateRequest(Command.ReadCo2Concentration); request[(int)MessageFormat.Checksum] = Checksum(request); + _serialPortStream.Write(request, 0, request.Length); - _serialPort.Open(); - _serialPort.Write(request, 0, request.Length); - - // wait until the response has been completely received + // read complete response (9 bytes expected) + byte[] response = new byte[MessageSize]; int timeout = 100; - while (timeout > 0 && _serialPort.BytesToRead < MessageSize) + int bytesRead = 0; + while (timeout > 0 && bytesRead < MessageSize) { + bytesRead += _serialPortStream.Read(response, bytesRead, response.Length - bytesRead); Thread.Sleep(1); timeout--; } @@ -91,9 +105,8 @@ public VolumeConcentration GetCo2Reading() throw new IOException("Timeout"); } - // read and process response - byte[] response = new byte[MessageSize]; - if ((_serialPort.Read(response, 0, response.Length) == response.Length) && (response[(int)MessageFormat.Checksum] == Checksum(response))) + // 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]); } @@ -106,10 +119,6 @@ public VolumeConcentration GetCo2Reading() { throw new IOException("Sensor communication failed", e); } - finally - { - _serialPort.Close(); - } } /// @@ -178,17 +187,12 @@ private void SendRequest(byte[] request) try { - _serialPort.Open(); - _serialPort.Write(request, 0, request.Length); + _serialPortStream.Write(request, 0, request.Length); } catch (Exception e) { throw new IOException("Sensor communication failed", e); } - finally - { - _serialPort.Close(); - } } private byte[] CreateRequest(Command command) => new byte[] @@ -221,13 +225,27 @@ private byte Checksum(byte[] packet) /// public void Dispose() { - if (_serialPort == null) + if (_serialPort == null && _serialPortStream == null) { return; } - _serialPort.Dispose(); - _serialPort = null; + if (_serialPortStream != null) + { + _serialPortStream.Dispose(); + _serialPortStream = null; + } + + if (_serialPort != null) + { + if (_serialPort.IsOpen) + { + _serialPort.Close(); + } + + _serialPort.Dispose(); + _serialPort = null; + } } } } \ No newline at end of file diff --git a/src/devices/Mhz19b/README.md b/src/devices/Mhz19b/README.md index 15bb1d3a94..f64ffa44ad 100644 --- a/src/devices/Mhz19b/README.md +++ b/src/devices/Mhz19b/README.md @@ -4,9 +4,7 @@ Binding for the MH-Z19B NDIR infrared gas module. The gas module measures the CO2 gas concentration in the ambient air. ## Binding Notes -The binding supports the connection through its UART interface. The binding gets configured with the UART to be used (e.g. ```/dev/serial0```). The UART is held open only while reading the current concentration from the sensor to enable UART multiplexing. - -The sensor is supplied with 5V. The UART level is at 3.3V and no level shifter is required. +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| |--------|-----------|------------| @@ -15,7 +13,14 @@ The sensor is supplied with 5V. The UART level is at 3.3V and no level shifter i |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.** diff --git a/src/devices/Mhz19b/samples/Mhz19b.Sample.cs b/src/devices/Mhz19b/samples/Mhz19b.Sample.cs index 261c7515ab..7e1eebecef 100644 --- a/src/devices/Mhz19b/samples/Mhz19b.Sample.cs +++ b/src/devices/Mhz19b/samples/Mhz19b.Sample.cs @@ -4,6 +4,8 @@ using System; using System.IO; +using System.IO.Ports; +using System.Text; using System.Threading; using UnitsNet; @@ -19,7 +21,16 @@ public class Program ///
public static void Main(string[] args) { - Mhz19b sensor = new Mhz19b("/dev/serial0"); + // 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); + serialPort.Encoding = Encoding.ASCII; + serialPort.ReadTimeout = 1000; + serialPort.WriteTimeout = 1000; + serialPort.Open(); + Mhz19b sensor = new Mhz19b(serialPort.BaseStream); + + // 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); @@ -36,7 +47,7 @@ public static void Main(string[] args) // saturated at the target level. // sensor.PerformZeroPointCalibration(); // ---- Now change to target concentration for span point. - // sensor.PerformSpanPointCalibration(VolumeConcentration.FromPartsPerMillion(200)); + // sensor.PerformSpanPointCalibration(VolumeConcentration.FromPartsPerMillion(2000)); // Continously read current concentration while (true) @@ -46,9 +57,11 @@ public static void Main(string[] args) VolumeConcentration reading = sensor.GetCo2Reading(); Console.WriteLine($"{reading}"); } - catch (IOException) + 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 index fabc37b0ea..cb1c6dc474 100644 --- a/src/devices/Mhz19b/samples/Mhz19b.Samples.csproj +++ b/src/devices/Mhz19b/samples/Mhz19b.Samples.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.1 + netcoreapp3.1 From 8cd5d68cd30f69b5916fd88f96ef6906c22488aa Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Fri, 25 Sep 2020 11:09:31 +0200 Subject: [PATCH 10/21] Made stream disposal optional --- src/devices/Mhz19b/Mhz19b.cs | 9 ++++++--- src/devices/Mhz19b/samples/Mhz19b.Sample.cs | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/devices/Mhz19b/Mhz19b.cs b/src/devices/Mhz19b/Mhz19b.cs index a339c6f470..e599338935 100644 --- a/src/devices/Mhz19b/Mhz19b.cs +++ b/src/devices/Mhz19b/Mhz19b.cs @@ -38,7 +38,7 @@ private enum MessageFormat } private const int MessageSize = 9; - + private bool _shouldDispose = false; private SerialPort _serialPort = null; private Stream _serialPortStream = null; @@ -46,9 +46,11 @@ private enum MessageFormat /// Initializes a new instance of the class using an existing (serial port) stream. ///
/// Existing stream - public Mhz19b(Stream 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; } /// @@ -70,6 +72,7 @@ public Mhz19b(string uartDevice) _serialPort.WriteTimeout = 1000; _serialPort.Open(); _serialPortStream = _serialPort.BaseStream; + _shouldDispose = true; } /// @@ -230,7 +233,7 @@ public void Dispose() return; } - if (_serialPortStream != null) + if (_shouldDispose && _serialPortStream != null) { _serialPortStream.Dispose(); _serialPortStream = null; diff --git a/src/devices/Mhz19b/samples/Mhz19b.Sample.cs b/src/devices/Mhz19b/samples/Mhz19b.Sample.cs index 7e1eebecef..b6d3998153 100644 --- a/src/devices/Mhz19b/samples/Mhz19b.Sample.cs +++ b/src/devices/Mhz19b/samples/Mhz19b.Sample.cs @@ -27,7 +27,7 @@ public static void Main(string[] args) serialPort.ReadTimeout = 1000; serialPort.WriteTimeout = 1000; serialPort.Open(); - Mhz19b sensor = new Mhz19b(serialPort.BaseStream); + Mhz19b sensor = new Mhz19b(serialPort.BaseStream, true); // Alternatively you can let the binding create the serial port stream: // Mhz19b sensor = new Mhz19b("/dev/serial0"); From 7f6a5deb0c5e117a3c8cf2712ab8177105e62b54 Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Thu, 1 Oct 2020 20:18:23 +0200 Subject: [PATCH 11/21] Review remarks --- src/devices/Mhz19b/samples/Mhz19b.Sample.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/devices/Mhz19b/samples/Mhz19b.Sample.cs b/src/devices/Mhz19b/samples/Mhz19b.Sample.cs index b6d3998153..e794a8de0e 100644 --- a/src/devices/Mhz19b/samples/Mhz19b.Sample.cs +++ b/src/devices/Mhz19b/samples/Mhz19b.Sample.cs @@ -22,10 +22,12 @@ public class Program 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); - serialPort.Encoding = Encoding.ASCII; - serialPort.ReadTimeout = 1000; - serialPort.WriteTimeout = 1000; + 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); @@ -55,7 +57,7 @@ public static void Main(string[] args) try { VolumeConcentration reading = sensor.GetCo2Reading(); - Console.WriteLine($"{reading}"); + Console.WriteLine($"{reading.PartsPerMillion:F0} ppm"); } catch (IOException e) { From 834c886405d2f7460561673eceaa872751ac8f57 Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Thu, 1 Oct 2020 20:19:41 +0200 Subject: [PATCH 12/21] Review remarks --- src/devices/Ahtxx/AhtBase.cs | 2 +- src/devices/Ahtxx/samples/Ahtxx.Sample.cs | 4 +--- src/devices/Ahtxx/samples/Ahtxx.Samples.csproj | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/devices/Ahtxx/AhtBase.cs b/src/devices/Ahtxx/AhtBase.cs index 261291b888..353ebf1523 100644 --- a/src/devices/Ahtxx/AhtBase.cs +++ b/src/devices/Ahtxx/AhtBase.cs @@ -78,7 +78,7 @@ public Temperature GetTemperature() public Ratio GetHumidity() { Measure(); - return new Ratio(_humidity, UnitsNet.Units.RatioUnit.Percent); + return Ratio.FromPercent(_humidity); } /// diff --git a/src/devices/Ahtxx/samples/Ahtxx.Sample.cs b/src/devices/Ahtxx/samples/Ahtxx.Sample.cs index b007b3fcdc..7944155257 100644 --- a/src/devices/Ahtxx/samples/Ahtxx.Sample.cs +++ b/src/devices/Ahtxx/samples/Ahtxx.Sample.cs @@ -3,9 +3,7 @@ // See the LICENSE file in the project root for more information. using System; -using System.Device.Gpio; using System.Device.I2c; -using System.Device.Spi; using System.Threading; namespace Iot.Device.Ahtxx.Samples @@ -27,7 +25,7 @@ public static void Main(string[] args) while (true) { - Console.WriteLine($"{DateTime.Now.ToLongTimeString()}: {aht20Sensor.GetTemperature()}, {aht20Sensor.GetHumidity()}"); + Console.WriteLine($"{DateTime.Now.ToLongTimeString()}: {aht20Sensor.GetTemperature().DegreesCelsius:F1}°C, {aht20Sensor.GetHumidity().Percent:F0}%"); Thread.Sleep(1000); } } diff --git a/src/devices/Ahtxx/samples/Ahtxx.Samples.csproj b/src/devices/Ahtxx/samples/Ahtxx.Samples.csproj index ff09566dc2..a052a33d81 100644 --- a/src/devices/Ahtxx/samples/Ahtxx.Samples.csproj +++ b/src/devices/Ahtxx/samples/Ahtxx.Samples.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.1 + netcoreapp3.1 From a1a24c16b8dc1c673798178b4a3ddbc99e99f3ce Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Fri, 2 Oct 2020 14:46:27 +0200 Subject: [PATCH 13/21] Cleanup acc. to review remarks --- src/devices/Ahtxx/AhtBase.cs | 5 ----- src/devices/Mhz19b/Mhz19b.cs | 10 ++++++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/devices/Ahtxx/AhtBase.cs b/src/devices/Ahtxx/AhtBase.cs index 353ebf1523..95430b8ff3 100644 --- a/src/devices/Ahtxx/AhtBase.cs +++ b/src/devices/Ahtxx/AhtBase.cs @@ -168,11 +168,6 @@ private bool IsCalibrated() /// protected virtual void Dispose(bool disposing) { - if (_i2cDevice == null) - { - return; - } - _i2cDevice?.Dispose(); _i2cDevice = null; } diff --git a/src/devices/Mhz19b/Mhz19b.cs b/src/devices/Mhz19b/Mhz19b.cs index e599338935..3eb902e246 100644 --- a/src/devices/Mhz19b/Mhz19b.cs +++ b/src/devices/Mhz19b/Mhz19b.cs @@ -66,10 +66,12 @@ public Mhz19b(string 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); - _serialPort.Encoding = Encoding.ASCII; - _serialPort.ReadTimeout = 1000; - _serialPort.WriteTimeout = 1000; + _serialPort = new SerialPort(uartDevice, 9600, Parity.None, 8, StopBits.One) + { + Encoding = Encoding.ASCII, + ReadTimeout = 1000, + WriteTimeout = 1000 + }; _serialPort.Open(); _serialPortStream = _serialPort.BaseStream; _shouldDispose = true; From 99929beb29b647894ac7755a20072b59a09ef0a9 Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Thu, 8 Oct 2020 10:38:57 +0200 Subject: [PATCH 14/21] Update src/devices/Ahtxx/AhtBase.cs Co-authored-by: Krzysztof Wicher --- src/devices/Ahtxx/AhtBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/devices/Ahtxx/AhtBase.cs b/src/devices/Ahtxx/AhtBase.cs index 95430b8ff3..37891f499e 100644 --- a/src/devices/Ahtxx/AhtBase.cs +++ b/src/devices/Ahtxx/AhtBase.cs @@ -71,7 +71,7 @@ public Temperature GetTemperature() } /// - /// Gets the current humidity reading from the sensor. + /// Gets the current relative humidity reading from the sensor. /// Reading the humidity takes between 10 ms and 80 ms. /// /// Temperature reading From f16e86b4ae637ef5817f7689011366cde2024011 Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Thu, 8 Oct 2020 10:39:34 +0200 Subject: [PATCH 15/21] Update src/devices/Ahtxx/AhtBase.cs Co-authored-by: Krzysztof Wicher --- src/devices/Ahtxx/AhtBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/devices/Ahtxx/AhtBase.cs b/src/devices/Ahtxx/AhtBase.cs index 37891f499e..a34c57aa37 100644 --- a/src/devices/Ahtxx/AhtBase.cs +++ b/src/devices/Ahtxx/AhtBase.cs @@ -74,7 +74,7 @@ public Temperature GetTemperature() /// Gets the current relative humidity reading from the sensor. /// Reading the humidity takes between 10 ms and 80 ms. /// - /// Temperature reading + /// Relative humidity reading public Ratio GetHumidity() { Measure(); From 0ba0b87d343a65607a464973e66978ad3369f2b0 Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Thu, 8 Oct 2020 18:41:38 +0200 Subject: [PATCH 16/21] Review remarks. Timeout handling for Mhz19b changed. --- src/devices/Ahtxx/AhtBase.cs | 38 ++++---- src/devices/Ahtxx/samples/Ahtxx.Sample.cs | 2 +- src/devices/Mhz19b/Mhz19b.cs | 112 ++++++++++------------ 3 files changed, 71 insertions(+), 81 deletions(-) diff --git a/src/devices/Ahtxx/AhtBase.cs b/src/devices/Ahtxx/AhtBase.cs index 95430b8ff3..0f57d8d525 100644 --- a/src/devices/Ahtxx/AhtBase.cs +++ b/src/devices/Ahtxx/AhtBase.cs @@ -14,29 +14,15 @@ namespace Iot.Device.Ahtxx /// Note: has been tested with AHT20 only, but should work with AHT10 and AHT15 as well. /// Up to now all functions are contained in the base class, though there might be differences between the sensors types. /// - public class AhtBase : IDisposable + public abstract class AhtBase : IDisposable { /// /// Address of AHTxx 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 DeviceAddress = 0x38; + public const int DefaultI2cAddress = 0x38; - // datasheet version 1.1, table 10 - private enum StatusBit : byte - { - Calibrated = 0x08, - Busy = 0x80 - } - - private enum Command : byte - { - Calibrate = 0xbe, - SoftRest = 0xba, - Measure = 0xac - } - - private I2cDevice _i2cDevice = null; + private I2cDevice _i2cDevice; private double _temperature; private double _humidity; @@ -93,6 +79,7 @@ private void Measure() 0x33, 0x00 }; + _i2cDevice.Write(buffer); // According to the datasheet the measurement takes 80 ms and completion is indicated by the status bit. @@ -122,7 +109,7 @@ private void Measure() /// private void SoftReset() { - _i2cDevice.WriteByte((byte)Command.SoftRest); + _i2cDevice.WriteByte((byte)Command.SoftReset); // reset requires 20ms at most, c.f. datasheet version 1.1, ch. 5.5 Thread.Sleep(20); } @@ -138,6 +125,7 @@ private void Calibrate() 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); @@ -171,5 +159,19 @@ protected virtual void Dispose(bool disposing) _i2cDevice?.Dispose(); _i2cDevice = null; } + + // datasheet version 1.1, table 10 + private enum StatusBit : byte + { + Calibrated = 0x08, + Busy = 0x80 + } + + private enum Command : byte + { + Calibrate = 0xbe, + SoftReset = 0xba, + Measure = 0xac + } } } diff --git a/src/devices/Ahtxx/samples/Ahtxx.Sample.cs b/src/devices/Ahtxx/samples/Ahtxx.Sample.cs index 7944155257..cc6d6bb666 100644 --- a/src/devices/Ahtxx/samples/Ahtxx.Sample.cs +++ b/src/devices/Ahtxx/samples/Ahtxx.Sample.cs @@ -19,7 +19,7 @@ internal class Program public static void Main(string[] args) { const int I2cBus = 1; - I2cConnectionSettings i2cSettings = new I2cConnectionSettings(I2cBus, Aht20.DeviceAddress); + I2cConnectionSettings i2cSettings = new I2cConnectionSettings(I2cBus, Aht20.DefaultI2cAddress); I2cDevice i2cDevice = I2cDevice.Create(i2cSettings); Aht20 aht20Sensor = new Aht20(i2cDevice); diff --git a/src/devices/Mhz19b/Mhz19b.cs b/src/devices/Mhz19b/Mhz19b.cs index 3eb902e246..a6e5009c50 100644 --- a/src/devices/Mhz19b/Mhz19b.cs +++ b/src/devices/Mhz19b/Mhz19b.cs @@ -16,28 +16,7 @@ namespace Iot.Device.Mhz19b /// public sealed class Mhz19b : IDisposable { - 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 - } - - private const int MessageSize = 9; + private const int MessageBytes = 9; private bool _shouldDispose = false; private SerialPort _serialPort = null; private Stream _serialPortStream = null; @@ -72,6 +51,7 @@ public Mhz19b(string uartDevice) ReadTimeout = 1000, WriteTimeout = 1000 }; + _serialPort.Open(); _serialPortStream = _serialPort.BaseStream; _shouldDispose = true; @@ -79,63 +59,52 @@ public Mhz19b(string uartDevice) /// /// Gets the current CO2 concentration from the sensor. - /// The validity is true if the current concentration was successfully read. - /// If the serial communication timed out or the checksum was invalid the validity is false. - /// If the validity is false the ratio is set to 0. /// - /// CO2 concentration in ppm and validity - /// Communication with sensor failed + /// CO2 volume concentration + /// Communication with sensor failed + /// A timeout occurred while communicating with the sensor public VolumeConcentration GetCo2Reading() { - try - { - // send read command request - var request = CreateRequest(Command.ReadCo2Concentration); - request[(int)MessageFormat.Checksum] = Checksum(request); - _serialPortStream.Write(request, 0, request.Length); + // 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[MessageSize]; - int timeout = 100; - int bytesRead = 0; - while (timeout > 0 && bytesRead < MessageSize) - { - bytesRead += _serialPortStream.Read(response, bytesRead, response.Length - bytesRead); - Thread.Sleep(1); - timeout--; - } + // read complete response (9 bytes expected) + byte[] response = new byte[MessageBytes]; - if (timeout == 0) - { - throw new IOException("Timeout"); - } + 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); + } - // 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"); - } + if (bytesRead < MessageBytes) + { + throw new TimeoutException($"Communication with sensor failed."); } - catch (Exception e) + + // check response and return calculated concentration if valid + if (response[(int)MessageFormat.Checksum] == Checksum(response)) { - throw new IOException("Sensor communication failed", e); + 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. - /// The sensor doesn't respond anything, so this is fire and forget. /// /// Communication with sensor failed public void PerformZeroPointCalibration() => SendRequest(CreateRequest(Command.CalibrateZeroPoint)); /// /// Initiate a span point calibration. - /// The sensor doesn't respond anything, so this is fire and forget. /// /// span value, between 1000[ppm] and 5000[ppm]. The typical value is 2000[ppm]. /// Thrown when span value is out of range @@ -157,7 +126,6 @@ public void PerformSpanPointCalibration(VolumeConcentration span) /// /// Switches the autmatic baseline correction on and off. - /// The sensor doesn't respond anything, so this is fire and forget. /// /// State of automatic correction /// Communication with sensor failed @@ -172,7 +140,6 @@ public void SetAutomaticBaselineCorrection(AbmState state) /// /// Set the sensor detection range. - /// The sensor doesn't respond anything, so this is fire and forget /// /// Detection range of the sensor /// Communication with sensor failed @@ -252,5 +219,26 @@ public void 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 From 34702f8c97933e8fd05f17427e255c68d4f4726b Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Thu, 15 Oct 2020 19:26:44 +0200 Subject: [PATCH 17/21] Added support for AHT10/15 type. Extended documentation and sample application. --- src/devices/Ahtxx/Aht10.cs | 27 ++++++++++++++ src/devices/Ahtxx/Aht20.cs | 7 +++- src/devices/Ahtxx/AhtBase.cs | 41 ++++++++++----------- src/devices/Ahtxx/README.md | 29 ++++++++++++--- src/devices/Ahtxx/samples/Ahtxx.Sample.cs | 10 +++-- src/devices/Ahtxx/samples/Ahtxx_sample.png | Bin 0 -> 36093 bytes src/devices/Ahtxx/samples/README.md | 10 +++++ 7 files changed, 94 insertions(+), 30 deletions(-) create mode 100644 src/devices/Ahtxx/Aht10.cs create mode 100644 src/devices/Ahtxx/samples/Ahtxx_sample.png create mode 100644 src/devices/Ahtxx/samples/README.md diff --git a/src/devices/Ahtxx/Aht10.cs b/src/devices/Ahtxx/Aht10.cs new file mode 100644 index 0000000000..885add1ad6 --- /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) + { + } + } +} \ No newline at end of file diff --git a/src/devices/Ahtxx/Aht20.cs b/src/devices/Ahtxx/Aht20.cs index 40f33bea9d..e26272a9a6 100644 --- a/src/devices/Ahtxx/Aht20.cs +++ b/src/devices/Ahtxx/Aht20.cs @@ -11,11 +11,16 @@ namespace Iot.Device.Ahtxx /// 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) + : base(i2cDevice, Aht20InitCommand) { } } diff --git a/src/devices/Ahtxx/AhtBase.cs b/src/devices/Ahtxx/AhtBase.cs index 5a77d65c71..857a1494ce 100644 --- a/src/devices/Ahtxx/AhtBase.cs +++ b/src/devices/Ahtxx/AhtBase.cs @@ -10,29 +10,29 @@ namespace Iot.Device.Ahtxx { /// - /// AHT temperature and humidity sensor family. - /// Note: has been tested with AHT20 only, but should work with AHT10 and AHT15 as well. - /// Up to now all functions are contained in the base class, though there might be differences between the sensors types. + /// Base class for common functions of the AHT10/15 and AHT20 sensors. /// public abstract class AhtBase : IDisposable { /// - /// Address of AHTxx device (0x38). This address is fix and cannot be changed. + /// 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 device connected through I2C interface. + /// Initializes a new instance of the binding for a sensor connected through I2C interface. /// /// Reference to the initialized I2C interface device - public AhtBase(I2cDevice i2cDevice) + 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(); @@ -41,7 +41,7 @@ public AhtBase(I2cDevice i2cDevice) // and perform calibration if indicated ==> c.f. datasheet, version 1.1, ch. 5.4 if (!IsCalibrated()) { - Calibrate(); + Initialize(); } } @@ -75,7 +75,7 @@ private void Measure() Span buffer = stackalloc byte[3] { // command parameters c.f. datasheet, version 1.1, ch. 5.4 - (byte)Command.Measure, + (byte)CommonCommand.Measure, 0x33, 0x00 }; @@ -109,19 +109,19 @@ private void Measure() /// private void SoftReset() { - _i2cDevice.WriteByte((byte)Command.SoftReset); + _i2cDevice.WriteByte((byte)CommonCommand.SoftReset); // reset requires 20ms at most, c.f. datasheet version 1.1, ch. 5.5 Thread.Sleep(20); } /// - /// Perform calibration command sequence + /// Perform initialization (calibration) command sequence /// - private void Calibrate() + private void Initialize() { Span buffer = stackalloc byte[3] { - (byte)Command.Calibrate, + _initCommand, 0x08, // command parameters c.f. datasheet, version 1.1, ch. 5.4 0x00 }; @@ -131,7 +131,7 @@ private void Calibrate() Thread.Sleep(10); } - private byte GetStatusByte() + private byte GetStatus() { _i2cDevice.WriteByte(0x71); // whithout this delay the reading the status fails often. @@ -142,12 +142,12 @@ private byte GetStatusByte() private bool IsBusy() { - return (GetStatusByte() & (byte)StatusBit.Busy) == (byte)StatusBit.Busy; + return (GetStatus() & (byte)StatusBit.Busy) == (byte)StatusBit.Busy; } private bool IsCalibrated() { - return (GetStatusByte() & (byte)StatusBit.Calibrated) == (byte)StatusBit.Calibrated; + return (GetStatus() & (byte)StatusBit.Calibrated) == (byte)StatusBit.Calibrated; } /// @@ -163,15 +163,14 @@ protected virtual void Dispose(bool disposing) // datasheet version 1.1, table 10 private enum StatusBit : byte { - Calibrated = 0x08, - Busy = 0x80 + Calibrated = 0b_0000_1000, + Busy = 0b1000_0000 } - private enum Command : byte + private enum CommonCommand : byte { - Calibrate = 0xbe, - SoftReset = 0xba, - Measure = 0xac + SoftReset = 0b1011_1010, + Measure = 0b1010_1100 } } } diff --git a/src/devices/Ahtxx/README.md b/src/devices/Ahtxx/README.md index c5dac980f8..7039f8e132 100644 --- a/src/devices/Ahtxx/README.md +++ b/src/devices/Ahtxx/README.md @@ -1,16 +1,35 @@ -# Ahtxx +# AHT10/15/20 Temperature and Humidity Sensor Modules ## Summary -AHTxx temperature and humidity sensor family. +The AHT10/15 and AHT20 sensors are high-precision, calibrated temperature and relative humidity sensor modules with a I2C digital interface. +

-## Device Family +## Supported Devices The binding supports the following types: -* AHT10 - http://www.aosong.com/en/products-40.html (not tested, yet) +* 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 +

## Binding Notes +This binding supports +* acquiring the temperature and relative humidty readings +* reading status +* issueing calibration and reset commands +

+ +|Sensor|Supporting class| +|-----|-------| +|AHT10|Aht10| +|Aht15|Aht10| +|Aht20|Aht20| + + + + + + -The AHT-devices can be accessed as an I2C bus device. However, you can use only one device per bus as the address is fix. diff --git a/src/devices/Ahtxx/samples/Ahtxx.Sample.cs b/src/devices/Ahtxx/samples/Ahtxx.Sample.cs index cc6d6bb666..afa3c41d6c 100644 --- a/src/devices/Ahtxx/samples/Ahtxx.Sample.cs +++ b/src/devices/Ahtxx/samples/Ahtxx.Sample.cs @@ -9,7 +9,7 @@ namespace Iot.Device.Ahtxx.Samples { /// - /// Samples for Ahtxx + /// Samples for Aht10 and Aht20 bindings /// internal class Program { @@ -21,11 +21,15 @@ public static void Main(string[] args) const int I2cBus = 1; I2cConnectionSettings i2cSettings = new I2cConnectionSettings(I2cBus, Aht20.DefaultI2cAddress); I2cDevice i2cDevice = I2cDevice.Create(i2cSettings); - Aht20 aht20Sensor = new Aht20(i2cDevice); + + // 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()}: {aht20Sensor.GetTemperature().DegreesCelsius:F1}°C, {aht20Sensor.GetHumidity().Percent:F0}%"); + 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_sample.png b/src/devices/Ahtxx/samples/Ahtxx_sample.png new file mode 100644 index 0000000000000000000000000000000000000000..a321729c8a7b0331f8dca58a3ded1595d59db3ae GIT binary patch literal 36093 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

VZ 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

CKu{=bK5>SpKa;Eo3=rT9P=$jf$Ar3HY%l)+jAp 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 Z{{!Wy@%!+dX1)Lb002ovPDHLkV1meLdu0Fs literal 0 HcmV?d00001 diff --git a/src/devices/Ahtxx/samples/README.md b/src/devices/Ahtxx/samples/README.md new file mode 100644 index 0000000000..a81746e548 --- /dev/null +++ b/src/devices/Ahtxx/samples/README.md @@ -0,0 +1,10 @@ +# Sample for use the of Ahtxx binding + +## Sample application +The sample application demonstrates how to setup the binding and retrieve the current readings. +It can be used with all supported types by commenting/uncommenting the instantiation of 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 From 182960d0258e7747be3643df773bbe63aec7d11d Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Fri, 16 Oct 2020 18:47:29 +0200 Subject: [PATCH 18/21] Documentation and sample for all Aht types completed. --- src/devices/Ahtxx/AhtBase.cs | 2 ++ src/devices/Ahtxx/README.md | 23 ++++++++++++++--------- src/devices/Ahtxx/samples/Ahtxx.Sample.cs | 1 - src/devices/Ahtxx/samples/README.md | 6 +++--- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/devices/Ahtxx/AhtBase.cs b/src/devices/Ahtxx/AhtBase.cs index 857a1494ce..5f097e416c 100644 --- a/src/devices/Ahtxx/AhtBase.cs +++ b/src/devices/Ahtxx/AhtBase.cs @@ -29,6 +29,7 @@ public abstract class AhtBase : IDisposable /// 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)); @@ -161,6 +162,7 @@ protected virtual void Dispose(bool disposing) } // datasheet version 1.1, table 10 + [Flags] private enum StatusBit : byte { Calibrated = 0b_0000_1000, diff --git a/src/devices/Ahtxx/README.md b/src/devices/Ahtxx/README.md index 7039f8e132..1b2d299033 100644 --- a/src/devices/Ahtxx/README.md +++ b/src/devices/Ahtxx/README.md @@ -1,28 +1,33 @@ # 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 a I2C digital interface. +The AHT10/15 and AHT20 sensors are high-precision, calibrated temperature and relative humidity sensor modules with an I2C digital interface.

-## Supported Devices + +## 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

-## Binding Notes -This binding supports +### Functions +The binding supports the following sensor functions: * acquiring the temperature and relative humidty readings * reading status * issueing calibration and reset commands

-|Sensor|Supporting class| -|-----|-------| -|AHT10|Aht10| -|Aht15|Aht10| -|Aht20|Aht20| +### Sensor classes +You need to choose the class depending on the sensor type. + +|Sensor|Required class| +|-----|---------------| +|AHT10|Aht10 | +|Aht15|Aht10 | +|Aht20|Aht20 | diff --git a/src/devices/Ahtxx/samples/Ahtxx.Sample.cs b/src/devices/Ahtxx/samples/Ahtxx.Sample.cs index afa3c41d6c..931823fb51 100644 --- a/src/devices/Ahtxx/samples/Ahtxx.Sample.cs +++ b/src/devices/Ahtxx/samples/Ahtxx.Sample.cs @@ -26,7 +26,6 @@ public static void Main(string[] args) // 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}%"); diff --git a/src/devices/Ahtxx/samples/README.md b/src/devices/Ahtxx/samples/README.md index a81746e548..0f0003e95a 100644 --- a/src/devices/Ahtxx/samples/README.md +++ b/src/devices/Ahtxx/samples/README.md @@ -1,8 +1,8 @@ -# Sample for use the of Ahtxx binding +# Sample application for use the of Ahtxx binding -## Sample application +## Summary The sample application demonstrates how to setup the binding and retrieve the current readings. -It can be used with all supported types by commenting/uncommenting the instantiation of the specific binding class (Aht10 / Aht20). +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. From d465f1e5db3c31b4785360de8230cceb5ab160cd Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Fri, 16 Oct 2020 18:58:37 +0200 Subject: [PATCH 19/21] New line appended --- src/devices/Ahtxx/Aht10.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/devices/Ahtxx/Aht10.cs b/src/devices/Ahtxx/Aht10.cs index 885add1ad6..831765a9ad 100644 --- a/src/devices/Ahtxx/Aht10.cs +++ b/src/devices/Ahtxx/Aht10.cs @@ -24,4 +24,4 @@ public Aht10(I2cDevice i2cDevice) { } } -} \ No newline at end of file +} From 80f4bb85c269c3de8106dfaba349ff1b49475caf Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Sun, 18 Oct 2020 22:19:46 +0200 Subject: [PATCH 20/21] More documentation added --- src/devices/Mhz19b/README.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/devices/Mhz19b/README.md b/src/devices/Mhz19b/README.md index f64ffa44ad..8cc0f91097 100644 --- a/src/devices/Mhz19b/README.md +++ b/src/devices/Mhz19b/README.md @@ -21,8 +21,28 @@ The use of an existing stream adds flexibility to the actual interface that used 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. +**Make sure that you read the datasheet carefully before altering the default calibration behaviour. Automatic baseline correction is enabled by default.** -## References +## 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) From e12ff57b280dc32ac3c33ae740c991d7952fa6f8 Mon Sep 17 00:00:00 2001 From: Marcus Fehde Date: Mon, 19 Oct 2020 19:20:37 +0200 Subject: [PATCH 21/21] Added missing section of Ahtxx README. --- src/devices/Ahtxx/README.md | 23 ++++++++++++++++------- src/devices/Mhz19b/README.md | 2 -- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/devices/Ahtxx/README.md b/src/devices/Ahtxx/README.md index 1b2d299033..680cfdba2d 100644 --- a/src/devices/Ahtxx/README.md +++ b/src/devices/Ahtxx/README.md @@ -2,8 +2,6 @@ ## 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 @@ -11,14 +9,13 @@ 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. @@ -30,11 +27,23 @@ You need to choose the class depending on the sensor type. |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/Mhz19b/README.md b/src/devices/Mhz19b/README.md index 8cc0f91097..d60800d4a7 100644 --- a/src/devices/Mhz19b/README.md +++ b/src/devices/Mhz19b/README.md @@ -14,12 +14,10 @@ The MH-Z19B gas module provides a serial communication interface (UART) which ca |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.**