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}