diff --git a/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Datasheet/Sensirion_Gas_Sensors_Datasheet_SGP40-2001008.pdf b/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Datasheet/Sensirion_Gas_Sensors_Datasheet_SGP40-2001008.pdf new file mode 100644 index 0000000000..22195a6396 Binary files /dev/null and b/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Datasheet/Sensirion_Gas_Sensors_Datasheet_SGP40-2001008.pdf differ diff --git a/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Driver/Sensors.Atmospheric.Sgp40.csproj b/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Driver/Sensors.Atmospheric.Sgp40.csproj new file mode 100644 index 0000000000..becdad18fa --- /dev/null +++ b/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Driver/Sensors.Atmospheric.Sgp40.csproj @@ -0,0 +1,32 @@ + + + true + icon.png + Wilderness Labs, Inc + netstandard2.1 + Library + Sgp40 + Wilderness Labs, Inc + http://developer.wildernesslabs.co/Meadow/Meadow.Foundation/ + Meadow.Foundation.Sensors.Atmospheric.Sgp40 + https://github.com/WildernessLabs/Meadow.Foundation + Meadow.Foundation, Atmospheric, SGP40, Sensiron + 0.1.44 + true + SGP40 VOC sensor driver + enable + + + 8.0 + TRACE;JETSON + + + 8.0 + + + + + + + + diff --git a/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Driver/Sgp40.Enums.cs b/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Driver/Sgp40.Enums.cs new file mode 100644 index 0000000000..7cfd7dfbb9 --- /dev/null +++ b/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Driver/Sgp40.Enums.cs @@ -0,0 +1,29 @@ +using System; + +namespace Meadow.Foundation.Sensors.Atmospheric +{ + public partial class Sgp40 + { + /// + /// Valid addresses for the sensor + /// + public enum Address : byte + { + /// + /// Bus address 0x59 + /// + Address_0x59 = 0x59, + /// + /// Bus address 0x59 + /// + Default = Address_0x59 + } + + private static byte[] sgp40_measure_raw_signal = { 0x26, 0x0f }; + private static byte[] sgp40_measure_raw_signal_uncompensated = { 0x26, 0x0F, 0x80, 0x00, 0xA2, 0x66, 0x66, 0x93 }; + private static byte[] sgp40_execute_self_test = { 0x28, 0x0e }; + private static byte[] sgp4x_turn_heater_off = { 0x36, 0x15 }; + private static byte[] sgp4x_get_serial_number = { 0x36, 0x82 }; + + } +} diff --git a/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Driver/Sgp40.cs b/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Driver/Sgp40.cs new file mode 100644 index 0000000000..83ee35c7ae --- /dev/null +++ b/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Driver/Sgp40.cs @@ -0,0 +1,164 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Meadow.Hardware; +using Meadow.Peripherals.Sensors; +using Meadow.Units; +using HU = Meadow.Units.RelativeHumidity.UnitType; +using TU = Meadow.Units.Temperature.UnitType; + +namespace Meadow.Foundation.Sensors.Atmospheric +{ + /// + /// Provides access to the Sensiron SGP40 VOC sensor + /// + public partial class Sgp40 : + ByteCommsSensorBase + { + /// + /// + public event EventHandler> VocIndexUpdated = delegate { }; + + /// + /// The VOC Index, from the last reading. + /// + public int VocIndex => Conditions; + + /// + /// Serial number of the device. + /// + public ulong SerialNumber { get; private set; } + + private byte[]? _compensationData = null; + + /// + /// Creates a new SGP40 VOC sensor. + /// + /// Sensor address (default to 0x40). + /// I2CBus. + public Sgp40(II2cBus i2cBus, byte address = (byte)Address.Default) + : base(i2cBus, address, 9, 8) + { + Initialize(); + } + + protected void Initialize() + { + // write buffer for initialization commands only can be two bytes. + Span tx = WriteBuffer.Span[0..2]; + + Peripheral.Write(sgp4x_get_serial_number); + + Thread.Sleep(1); // per the data sheet + + Peripheral.Read(ReadBuffer.Span[0..9]); + + var bytes = ReadBuffer.ToArray(); + + SerialNumber = (ulong)(bytes[0] << 40 | bytes[1] << 32 | bytes[3] << 24 | bytes[4] << 16 | bytes[6] << 8 | bytes[7] << 0); + } + + /// + /// This command triggers the built-in self-test checking for integrity of both hotplate and MOX material + /// + /// true on sucessful test, otherwise false + public bool RunSelfTest() + { + Peripheral.Write(sgp40_execute_self_test); + + Thread.Sleep(325); // test requires 320ms to complete + + Peripheral.Read(ReadBuffer.Span[0..3]); + + return ReadBuffer.Span[0..1][0] == 0xd4; + } + + protected async override Task ReadSensor() + { + return await Task.Run(() => + { + if(_compensationData != null) + { + Peripheral.Write(_compensationData); + } + else + { + Peripheral.Write(sgp40_measure_raw_signal_uncompensated); + } + + Thread.Sleep(30); // per the data sheet + + Peripheral.Read(ReadBuffer.Span[0..3]); + + var data = ReadBuffer.Span[0..3].ToArray(); + + return data[0] << 8 | data[1]; + }); + } + + /// + /// Inheritance-safe way to raise events and notify observers. + /// + /// + protected override void RaiseEventsAndNotify(IChangeResult changeResult) + { + VocIndexUpdated?.Invoke(this, new ChangeResult(VocIndex, changeResult.Old)); + + base.RaiseEventsAndNotify(changeResult); + } + + /// + /// This command turns the hotplate off and stops the measurement. Subsequently, the sensor enters idle mode. + /// + public void TurnHeaterOff() + { + Peripheral.Write(sgp4x_turn_heater_off); + } + + public void SetCompensationData(RelativeHumidity humidity, Meadow.Units.Temperature temperature) + { + _compensationData = new byte[8]; + + Array.Copy(sgp40_measure_raw_signal, 0, _compensationData, 0, 2); + + var rh = BitConverter.GetBytes(System.Net.IPAddress.HostToNetworkOrder((ushort)(humidity.Percent * 65535 / 100))); + _compensationData[2] = rh[0]; + _compensationData[3] = rh[1]; + _compensationData[4] = Crc(rh); + + var t = BitConverter.GetBytes(System.Net.IPAddress.HostToNetworkOrder((ushort)(temperature.Celsius * 65535 / 175))); + _compensationData[5] = t[0]; + _compensationData[6] = t[1]; + _compensationData[7] = Crc(t); + + } + + public void ClearCompensationData() + { + _compensationData = null; + } + + private byte Crc(byte[] data) + { + if (data.Length != 2) throw new ArgumentException(); + + byte crc = 0xFF; + for (int i = 0; i < 2; i++) + { + crc ^= data[i]; + for (byte bit = 8; bit > 0; --bit) + { + if ((crc & 0x80) != 0) + { + crc = (byte)((crc << 1) ^ 0x31); + } + else + { + crc = (byte)(crc << 1); + } + } + } + return crc; + } + } +} diff --git a/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Samples/Sgp40_Sample/MeadowApp.cs b/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Samples/Sgp40_Sample/MeadowApp.cs new file mode 100644 index 0000000000..234eddeec0 --- /dev/null +++ b/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Samples/Sgp40_Sample/MeadowApp.cs @@ -0,0 +1,73 @@ +using Meadow; +using Meadow.Devices; +using Meadow.Foundation.Sensors.Atmospheric; +using System; +using System.Threading.Tasks; + +namespace BasicSensors.Atmospheric.SI7021_Sample +{ + public class MeadowApp : App + { + // + + Sgp40 sensor; + + public MeadowApp() + { + } + + public override Task Initialize() + { + Resolver.Log.Info("Initializing..."); + + sensor = new Sgp40(Device.CreateI2cBus()); + + Resolver.Log.Info($"Sensor SN: {sensor.SerialNumber:x6}"); + + if (sensor.RunSelfTest()) + { + Resolver.Log.Info("Self test successful"); + } + else + { + Resolver.Log.Warn("Self test failed"); + } + + var consumer = Sgp40.CreateObserver( + handler: result => + { + Resolver.Log.Info($"Observer: VOC changed by threshold; new index: {result.New}"); + }, + filter: result => + { + //c# 8 pattern match syntax. checks for !null and assigns var. + return Math.Abs(result.New - result.Old ?? 0) > 10; + } + ); + sensor.Subscribe(consumer); + + sensor.Updated += (sender, result) => + { + Resolver.Log.Info($" VOC: {result.New}"); + }; + + return base.Initialize(); + } + + public override async Task Run() + { + await ReadConditions(); + + sensor.StartUpdating(TimeSpan.FromSeconds(1)); + } + + async Task ReadConditions() + { + var result = await sensor.Read(); + Resolver.Log.Info("Initial Readings:"); + Resolver.Log.Info($" Temperature: {result}"); + } + + // + } +} \ No newline at end of file diff --git a/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Samples/Sgp40_Sample/Program.cs b/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Samples/Sgp40_Sample/Program.cs new file mode 100644 index 0000000000..1d765cd8d3 --- /dev/null +++ b/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Samples/Sgp40_Sample/Program.cs @@ -0,0 +1,17 @@ +using Meadow; +using System.Threading; + +namespace BasicSensors.Atmospheric.SI7021_Sample +{ + class Program + { + static IApp? app; + + public static void Main(string[] args) + { + app = new MeadowApp(); + + Thread.Sleep(Timeout.Infinite); + } + } +} \ No newline at end of file diff --git a/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Samples/Sgp40_Sample/Sgp40_Sample.csproj b/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Samples/Sgp40_Sample/Sgp40_Sample.csproj new file mode 100644 index 0000000000..beffd4fcaf --- /dev/null +++ b/Source/Meadow.Foundation.Peripherals/Sensors.Atmospheric.Sgp40/Samples/Sgp40_Sample/Sgp40_Sample.csproj @@ -0,0 +1,22 @@ + + + https://github.com/WildernessLabs/Meadow.Foundation + Wilderness Labs, Inc + Wilderness Labs, Inc + true + netstandard2.1 + Exe + App + enable + + + 8.0 + + + 8.0 + + + + + + diff --git a/Source/Meadow.Foundation.sln b/Source/Meadow.Foundation.sln index cb1536021e..329392da13 100644 --- a/Source/Meadow.Foundation.sln +++ b/Source/Meadow.Foundation.sln @@ -1046,6 +1046,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{494A EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bh1900Nux_Sample", "Meadow.Foundation.Peripherals\Sensors.Atmospheric.Bh1900Nux\Samples\Bh1900Nux_Sample\Bh1900Nux_Sample.csproj", "{43DA872D-F7E2-4CD6-A5F9-3E50FFBE73B3}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sensors.Atmospheric.Sgp40", "Sensors.Atmospheric.Sgp40", "{B98533B9-CA48-4F0D-961B-C95BD5158391}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{F2ECD777-3B3B-4EF1-B68C-E31AAADBF2B5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sensors.Atmospheric.Sgp40", "Meadow.Foundation.Peripherals\Sensors.Atmospheric.Sgp40\Driver\Sensors.Atmospheric.Sgp40.csproj", "{71EBC24D-5B32-4E76-ADF3-DE0017946108}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sgp40_Sample", "Meadow.Foundation.Peripherals\Sensors.Atmospheric.Sgp40\Samples\Sgp40_Sample\Sgp40_Sample.csproj", "{1ACC6BA0-8108-4BFA-9A75-9063B5E07493}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -2446,6 +2454,18 @@ Global {43DA872D-F7E2-4CD6-A5F9-3E50FFBE73B3}.Release|Any CPU.ActiveCfg = Release|Any CPU {43DA872D-F7E2-4CD6-A5F9-3E50FFBE73B3}.Release|Any CPU.Build.0 = Release|Any CPU {43DA872D-F7E2-4CD6-A5F9-3E50FFBE73B3}.Release|Any CPU.Deploy.0 = Release|Any CPU + {71EBC24D-5B32-4E76-ADF3-DE0017946108}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71EBC24D-5B32-4E76-ADF3-DE0017946108}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71EBC24D-5B32-4E76-ADF3-DE0017946108}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {71EBC24D-5B32-4E76-ADF3-DE0017946108}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71EBC24D-5B32-4E76-ADF3-DE0017946108}.Release|Any CPU.Build.0 = Release|Any CPU + {71EBC24D-5B32-4E76-ADF3-DE0017946108}.Release|Any CPU.Deploy.0 = Release|Any CPU + {1ACC6BA0-8108-4BFA-9A75-9063B5E07493}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1ACC6BA0-8108-4BFA-9A75-9063B5E07493}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1ACC6BA0-8108-4BFA-9A75-9063B5E07493}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {1ACC6BA0-8108-4BFA-9A75-9063B5E07493}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1ACC6BA0-8108-4BFA-9A75-9063B5E07493}.Release|Any CPU.Build.0 = Release|Any CPU + {1ACC6BA0-8108-4BFA-9A75-9063B5E07493}.Release|Any CPU.Deploy.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2966,6 +2986,10 @@ Global {71FE0FFA-F897-4067-8687-A033B5D0BB5D} = {6E207804-A991-42DD-BE14-AB8E18940162} {494AAADA-7CF7-44A4-AFB2-4AEC664151E3} = {6E207804-A991-42DD-BE14-AB8E18940162} {43DA872D-F7E2-4CD6-A5F9-3E50FFBE73B3} = {494AAADA-7CF7-44A4-AFB2-4AEC664151E3} + {B98533B9-CA48-4F0D-961B-C95BD5158391} = {64623FCA-6086-4F0A-A59D-2BF372EA38AA} + {F2ECD777-3B3B-4EF1-B68C-E31AAADBF2B5} = {B98533B9-CA48-4F0D-961B-C95BD5158391} + {71EBC24D-5B32-4E76-ADF3-DE0017946108} = {B98533B9-CA48-4F0D-961B-C95BD5158391} + {1ACC6BA0-8108-4BFA-9A75-9063B5E07493} = {F2ECD777-3B3B-4EF1-B68C-E31AAADBF2B5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AF7CA16F-8C38-4546-87A2-5DAAF58A1520}