commit b70e14c0285109f8679b593b398251b15a9e8c20 Author: Denis Nutiu Date: Sat Apr 10 17:48:03 2021 +0300 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f2fb663 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ +notes.md \ No newline at end of file diff --git a/.idea/.idea.PMS5003/.idea/.gitignore b/.idea/.idea.PMS5003/.idea/.gitignore new file mode 100644 index 0000000..2559456 --- /dev/null +++ b/.idea/.idea.PMS5003/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/modules.xml +/projectSettingsUpdater.xml +/.idea.PMS5003.iml +# Datasource local storage ignored files +/../../../../../../../:\Projects\Rider\PMS5003\.idea\.idea.PMS5003\.idea/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/.idea.PMS5003/.idea/encodings.xml b/.idea/.idea.PMS5003/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.PMS5003/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.PMS5003/.idea/indexLayout.xml b/.idea/.idea.PMS5003/.idea/indexLayout.xml new file mode 100644 index 0000000..473077d --- /dev/null +++ b/.idea/.idea.PMS5003/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.PMS5003/.idea/vcs.xml b/.idea/.idea.PMS5003/.idea/vcs.xml new file mode 100644 index 0000000..9661ac7 --- /dev/null +++ b/.idea/.idea.PMS5003/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/PMS5003.sln b/PMS5003.sln new file mode 100644 index 0000000..a27fbb7 --- /dev/null +++ b/PMS5003.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PMS5003", "PMS5003\PMS5003.csproj", "{918D79E6-9AE8-4FA8-8400-BD3A26BB4A60}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PMS5003Tests", "PMS5003Tests\PMS5003Tests.csproj", "{AF7541BD-C0B1-42FE-AEFB-1A1D9CEAC6B9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {918D79E6-9AE8-4FA8-8400-BD3A26BB4A60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {918D79E6-9AE8-4FA8-8400-BD3A26BB4A60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {918D79E6-9AE8-4FA8-8400-BD3A26BB4A60}.Release|Any CPU.ActiveCfg = Release|Any CPU + {918D79E6-9AE8-4FA8-8400-BD3A26BB4A60}.Release|Any CPU.Build.0 = Release|Any CPU + {AF7541BD-C0B1-42FE-AEFB-1A1D9CEAC6B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF7541BD-C0B1-42FE-AEFB-1A1D9CEAC6B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF7541BD-C0B1-42FE-AEFB-1A1D9CEAC6B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF7541BD-C0B1-42FE-AEFB-1A1D9CEAC6B9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/PMS5003.sln.DotSettings.user b/PMS5003.sln.DotSettings.user new file mode 100644 index 0000000..e942980 --- /dev/null +++ b/PMS5003.sln.DotSettings.user @@ -0,0 +1,6 @@ + + <SessionState ContinuousTestingMode="0" IsActive="True" Name="Pms5003DataUnitTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>xUnit::AF7541BD-C0B1-42FE-AEFB-1A1D9CEAC6B9::.NETCoreApp,Version=v3.1::PMS5003Tests.Pms5003DataUnitTest</TestId> + </TestAncestor> +</SessionState> \ No newline at end of file diff --git a/PMS5003/Exceptions/BufferUnderflowExceptions.cs b/PMS5003/Exceptions/BufferUnderflowExceptions.cs new file mode 100644 index 0000000..fa94bf2 --- /dev/null +++ b/PMS5003/Exceptions/BufferUnderflowExceptions.cs @@ -0,0 +1,16 @@ +using System; + +namespace PMS5003.Exceptions +{ + /// + /// BufferUnderflowExceptions is thrown when the PMS5003 data buffer is incomplete. + /// + [Serializable] + public class BufferUnderflowException : Exception + { + public BufferUnderflowException() : base("The PMS5003 data buffer is underrun, not enough bytes read!") + { + + } + } +} \ No newline at end of file diff --git a/PMS5003/Exceptions/ChecksumMismatchException.cs b/PMS5003/Exceptions/ChecksumMismatchException.cs new file mode 100644 index 0000000..12c676a --- /dev/null +++ b/PMS5003/Exceptions/ChecksumMismatchException.cs @@ -0,0 +1,14 @@ +using System; + +namespace PMS5003.Exceptions +{ + /// + /// ChecksumMismatchException when the frame contents of PMS5003 doesn't match the checksum. + /// + public class ChecksumMismatchException : Exception + { + public ChecksumMismatchException() : base("Checksum mismatch.") + { + } + } +} \ No newline at end of file diff --git a/PMS5003/Exceptions/InvalidStartByteException.cs b/PMS5003/Exceptions/InvalidStartByteException.cs new file mode 100644 index 0000000..103f60b --- /dev/null +++ b/PMS5003/Exceptions/InvalidStartByteException.cs @@ -0,0 +1,16 @@ +using System; + +namespace PMS5003.Exceptions +{ + /// + /// Exception that is thrown by the PMS5003 sensor when the read contains an invalid start sequence. + /// + [Serializable] + public class InvalidStartByteException : Exception + { + public InvalidStartByteException(short firstByte, short secondByte) : base( + $"Invalid start characters, expected 0x42 0x4d got [{firstByte:X}, {secondByte:X}]") + { + } + } +} \ No newline at end of file diff --git a/PMS5003/Exceptions/ReadFailedException.cs b/PMS5003/Exceptions/ReadFailedException.cs new file mode 100644 index 0000000..9bd8bf6 --- /dev/null +++ b/PMS5003/Exceptions/ReadFailedException.cs @@ -0,0 +1,14 @@ +using System; + +namespace PMS5003.Exceptions +{ + /// + /// Thrown the the read has failed. + /// + public class ReadFailedException : Exception + { + public ReadFailedException() : base("PMS5003 read failed, max retries exceeded") + { + } + } +} \ No newline at end of file diff --git a/PMS5003/PMS5003.cs b/PMS5003/PMS5003.cs new file mode 100644 index 0000000..00df55b --- /dev/null +++ b/PMS5003/PMS5003.cs @@ -0,0 +1,151 @@ +using System; +using System.Device.Gpio; +using System.IO.Ports; +using System.Threading; +using Microsoft.Extensions.Logging; +using PMS5003.Exceptions; + +namespace PMS5003 +{ + /// + /// PMS5003 digital universal particle concentration sensor. + /// Datasheet: https://www.aqmd.gov/docs/default-source/aq-spec/resources-page/plantower-pms5003-manual_v2-3.pdf + /// + public class Pms5003 + { + public static Logger Logger; + private readonly GpioController _gpioController; + private readonly SerialPort _serialPort; + private readonly short _pinSet; + private readonly short _pinReset; + private bool _isSleeping; + + /// + /// Initializes the . + /// + /// The name of the serial port. + /// The number of the SET pin. + /// The number if the RESET pin. + public Pms5003(string portSerialName, short pinSet, short pinReset) + { + _serialPort = new SerialPort(portSerialName, Pms5003Constants.DefaultBaudRate); + _pinReset = pinReset; + _pinSet = pinSet; + + _serialPort.Open(); + if (Environment.OSVersion.Platform == PlatformID.Unix) + { + _gpioController = new GpioController(); + if (pinSet >= 0) + { + _gpioController.OpenPin(_pinSet, PinMode.Output); + _gpioController.Write(_pinSet, PinValue.High); + } + + if (pinReset >= 0) + { + _gpioController.OpenPin(_pinReset, PinMode.Output); + _gpioController.Write(_pinReset, PinValue.High); + } + } + else + { + Logger?.LogWarning("Not running under Unix platform, skipping GPIO configuration."); + } + } + + /// + /// Initializes the using /dev/ttyAMA0 serial port name. + /// + /// + /// + public Pms5003(short pinSet, short pinReset) : this("/dev/ttyAMA0", pinSet, pinReset) + { + } + + /// + /// Initializes the using /dev/ttyAMA0 serial port name + /// and no GPIO pins for SET and RESET. + /// + public Pms5003() : this("/dev/ttyAMA0", -1, -1) + { + } + + /// + /// Reads the data from the sensor. + /// + /// The data. + /// Thrown when the read fails. + public Pms5003Data ReadData() + { + var currentTry = 0; + const int maxRetries = 5; + + while (currentTry < maxRetries) + { + try + { + var buffer = new byte[32]; + _serialPort.Read(buffer, 0, 32); + return Pms5003Data.FromBytes(buffer); + } + catch (Exception e) + { + Logger?.LogWarning(e.ToString()); + } + finally + { + currentTry += 1; + } + } + + throw new ReadFailedException(); + } + + /// + /// Resets the PMS5003 Sensor. + /// + public void Reset() + { + if (_gpioController == null || _pinReset < 0) return; + _gpioController.Write(_pinReset, PinValue.Low); + Thread.Sleep(200); + _gpioController.Write(_pinReset, PinValue.High); + } + + /// + /// Enables Sleep Mode for the PMS5003 Sensor. + /// + public void Sleep() + { + if (_gpioController == null || _pinSet < 0) return; + _isSleeping = true; + _gpioController.Write(_pinSet, PinValue.Low); + } + + /// + /// Disables Sleep Mode for the PMS5003 Sensor. + /// + public void WakeUp() + { + if (_gpioController == null || _pinSet < 0) return; + _isSleeping = false; + _gpioController.Write(_pinSet, PinValue.High); + } + + /// + /// Checks if Sleep Mode is enabled. + /// + /// True if sensor is in Sleep Mode, false otherwise. + public bool IsSleeping() + { + return _isSleeping; + } + + ~Pms5003() + { + _serialPort.Close(); + _gpioController?.Dispose(); + } + } +} \ No newline at end of file diff --git a/PMS5003/PMS5003.csproj b/PMS5003/PMS5003.csproj new file mode 100644 index 0000000..c3fd72b --- /dev/null +++ b/PMS5003/PMS5003.csproj @@ -0,0 +1,13 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + diff --git a/PMS5003/Pms5003Constants.cs b/PMS5003/Pms5003Constants.cs new file mode 100644 index 0000000..0adc668 --- /dev/null +++ b/PMS5003/Pms5003Constants.cs @@ -0,0 +1,15 @@ +namespace PMS5003 +{ + /// + /// The Pms5003Constants defines constants required by the PMS5003 communication protocol. + /// + public class Pms5003Constants + { + public static readonly int DefaultBaudRate = 9600; + public static readonly int StartByte1 = 0x42; + public static readonly int StartByte2 = 0x4d; + public static readonly int CommandReadInPassive = 0xe2; + public static readonly int CommandChangeMode = 0xe1; + public static readonly int CommandSleep = 0xe4; + } +} \ No newline at end of file diff --git a/PMS5003/Pms5003Data.cs b/PMS5003/Pms5003Data.cs new file mode 100644 index 0000000..4333967 --- /dev/null +++ b/PMS5003/Pms5003Data.cs @@ -0,0 +1,102 @@ +using System.Text; +using PMS5003.Exceptions; + +namespace PMS5003 +{ + /// + /// Pms5003Measurement is an abstraction over the PMS5003 data. + /// + public class Pms5003Data + { + private readonly uint[] _data; + public uint Pm1Standard => _data[0]; + public uint Pm2Dot5Standard => _data[1]; + public uint Pm10Standard => _data[2]; + public uint Pm1Atmospheric => _data[3]; + public uint Pm2Dot5Atmospheric => _data[4]; + public uint Pm10Atmospheric => _data[5]; + public uint ParticlesDiameterBeyond0Dot3 => _data[6]; + public uint ParticlesDiameterBeyond0Dot5 => _data[7]; + public uint ParticlesDiameterBeyond1Dot0 => _data[8]; + public uint ParticlesDiameterBeyond2Dot5 => _data[9]; + public uint ParticlesDiameterBeyond5Dot0 => _data[10]; + public uint ParticlesDiameterBeyond10Dot0 => _data[11]; + private uint Reserved => _data[12]; + private uint Checksum => _data[13]; + + private Pms5003Data() + { + _data = new uint[14]; + } + + /// + /// Instantiates a new instance from the given byte buffer. + /// + /// The data buffer + /// A new instance. + public static Pms5003Data FromBytes(byte[] buffer) + { + var pms5003Measurement = new Pms5003Data(); + + if (buffer.Length < 4) + { + throw new BufferUnderflowException(); + } + + if (buffer[0] != Pms5003Constants.StartByte1 || buffer[1] != Pms5003Constants.StartByte2) + { + throw new InvalidStartByteException(buffer[0], buffer[1]); + } + + var frameLength = Utils.CombineBytes(buffer[2], buffer[3]); + if (frameLength > 0) + { + var currentDataPoint = 0; + for (var i = 4; i < frameLength + 4; i += 2) + { + pms5003Measurement._data[currentDataPoint] = Utils.CombineBytes(buffer[i], buffer[i + 1]); + currentDataPoint += 1; + } + } + + var checkSum = 0; + for (var i = 0; i < frameLength + 2; i++) + { + checkSum += buffer[i]; + } + + if (pms5003Measurement.Checksum != checkSum) + { + throw new ChecksumMismatchException(); + } + + return pms5003Measurement; + } + + /// + /// Returns a string representation of the Pms5003Data. + /// + /// String with all the fields and values. + public override string ToString() + { + var buffer = new StringBuilder(); + buffer.AppendLine("Pms5003Data["); + buffer.AppendLine($"Pm1Standard={Pm1Standard},"); + buffer.AppendLine($"Pm2Dot5Standard={Pm2Dot5Standard},"); + buffer.AppendLine($"Pm10Standard={Pm10Standard},"); + buffer.AppendLine($"Pm1Atmospheric={Pm1Atmospheric},"); + buffer.AppendLine($"Pm2Dot5Atmospheric={Pm2Dot5Atmospheric},"); + buffer.AppendLine($"Pm10Atmospheric={Pm10Atmospheric},"); + buffer.AppendLine($"ParticlesDiameterBeyond0Dot3={ParticlesDiameterBeyond0Dot3},"); + buffer.AppendLine($"ParticlesDiameterBeyond0Dot5={ParticlesDiameterBeyond0Dot5},"); + buffer.AppendLine($"ParticlesDiameterBeyond1Dot0={ParticlesDiameterBeyond1Dot0},"); + buffer.AppendLine($"ParticlesDiameterBeyond2Dot5={ParticlesDiameterBeyond2Dot5},"); + buffer.AppendLine($"ParticlesDiameterBeyond5Dot0={ParticlesDiameterBeyond5Dot0},"); + buffer.AppendLine($"ParticlesDiameterBeyond10Dot0={ParticlesDiameterBeyond10Dot0},"); + buffer.AppendLine($"Reserved={Reserved},"); + buffer.AppendLine($"Checksum={Checksum}"); + buffer.AppendLine("]"); + return buffer.ToString(); + } + } +} \ No newline at end of file diff --git a/PMS5003/Utils.cs b/PMS5003/Utils.cs new file mode 100644 index 0000000..9a3e3cc --- /dev/null +++ b/PMS5003/Utils.cs @@ -0,0 +1,16 @@ +namespace PMS5003 +{ + public class Utils + { + /// + /// Combines two bytes and returns the results ((High RSHIFT 8) | Low) + /// + /// The high byte. + /// The low byte. + /// + public static uint CombineBytes(byte high, byte low) + { + return (uint)(low | (high << 8));; + } + } +} \ No newline at end of file diff --git a/PMS5003/readme.md b/PMS5003/readme.md new file mode 100644 index 0000000..aaa44e5 --- /dev/null +++ b/PMS5003/readme.md @@ -0,0 +1,36 @@ +# Introduction + +C# Library for interfacing with the PMS5003 Particulate Matter Sensor. + +For wiring the Sensor consult the [datasheet](https://www.aqmd.gov/docs/default-source/aq-spec/resources-page/plantower-pms5003-manual_v2-3.pdf). + +## Example + +```csharp +using System; +using System.Threading; + +namespace Application +{ + class Example + { + static void Main(string[] args) + { + var pms = new Pms5003(); + while (true) + { + try + { + var data = pms.ReadData(); + Console.WriteLine(data); + Thread.Sleep(2000); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + } + } +} +``` \ No newline at end of file diff --git a/PMS5003Tests/PMS5003Tests.csproj b/PMS5003Tests/PMS5003Tests.csproj new file mode 100644 index 0000000..2876b25 --- /dev/null +++ b/PMS5003Tests/PMS5003Tests.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + diff --git a/PMS5003Tests/Pms5003DataUnitTest.cs b/PMS5003Tests/Pms5003DataUnitTest.cs new file mode 100644 index 0000000..cf53a1a --- /dev/null +++ b/PMS5003Tests/Pms5003DataUnitTest.cs @@ -0,0 +1,135 @@ +using System; +using PMS5003; +using PMS5003.Exceptions; +using Xunit; +using Xunit.Abstractions; + +namespace PMS5003Tests +{ + public class Pms5003DataUnitTest + { + private readonly ITestOutputHelper _testOutputHelper; + + public Pms5003DataUnitTest(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + [Fact] + public void Test_FromBytes_Ok() + { + var tests = new byte[][] + { + new byte[] + { + 66, 77, 0, 28, 0, 8, 0, 13, 0, 15, 0, 8, 0, 13, 0, 15, 7, 17, 1, 249, 0, 82, 0, 6, 0, 2, 0, 2, 151, + 0, 2, 248 + }, + new byte[] + { + 66, 77, 0, 28, 0, 9, 0, 15, 0, 16, 0, 9, 0, 15, 0, 16, 6, 246, 2, 11, 0, 104, 0, 4, 0, 2, 0, 2, 151, + 0, + 3, 11 + }, + new byte[] + { + 66, 77, 0, 28, 0, 7, 0, 12, 0, 13, 0, 7, 0, 12, 0, 13, 5, 226, 1, 152, 0, 66, 0, 12, 0, 2, 0, 2, + 151, 0, + 3, 84 + } + }; + + // Assert that no exception is thrown + foreach (var subTest in tests) + { + Pms5003Data.FromBytes(subTest); + } + } + + [Fact] + public void Test_FromBytes_ChecksumMismatchException() + { + var tests = new byte[][] + { + new byte[] + { + 66, 77, 0, 28, 0, 8, 0, 13, 0, 16, 0, 8, 0, 13, 0, 16, 6, 45, 1, 167, 0, 101, 0, 10, 0, 8, 0, 151, + 0, 144, 255, 0 + }, + new byte[] + { + 66, 77, 0, 28, 0, 7, 0, 11, 0, 13, 0, 7, 0, 11, 0, 13, 5, 193, 1, 155, 8, 1, 6, 0, 2, 0, 2, 151, 0, + 3, 44, 0 + }, + new byte[] + { + 66, 77, 0, 28, 0, 8, 0, 14, 0, 18, 0, 8, 0, 14, 0, 18, 6, 186, 1, 208, 0, 100, 0, 8, 0, 6, 0, 6, + 151, 3, 155, 0 + }, + new byte[] + { + 66, 77, 0, 28, 0, 7, 0, 10, 0, 10, 232, 0, 10, 0, 33, 208, 10, 165, 0, 70, 0, 4, 0, 0, 0, 0, 151, 0, + 2, 161, 0, 0, + } + }; + + foreach (var subTest in tests) + { + Assert.Throws(() => Pms5003Data.FromBytes(subTest)); + } + } + + [Fact] + public void Test_FromBytes_InvalidStartByteException() + { + var tests = new byte[][] + { + new byte[] + { + 66, 00, 0, 28, 0, 8, 0, 13, 0, 16, 0, 8, 0, 13, 0, 16, 6, 45, 1, 167, 0, 101, 0, 10, 0, 8, 0, 151, + 0, 144, 255, 0 + }, + new byte[] + { + 00, 77, 0, 28, 0, 7, 0, 11, 0, 13, 0, 7, 0, 11, 0, 13, 5, 193, 1, 155, 8, 1, 6, 0, 2, 0, 2, 151, 0, + 3, 44, 0 + }, + }; + + foreach (var subTest in tests) + { + Assert.Throws(() => Pms5003Data.FromBytes(subTest)); + } + } + + [Fact] + public void Test_ToString() + { + var pmsData = Pms5003Data.FromBytes(new byte[] + { + 66, 77, 0, 28, 0, 7, 0, 12, 0, 13, 0, 7, 0, 12, 0, 13, 5, 226, 1, 152, 0, 66, 0, 12, 0, 2, 0, 2, + 151, 0, + 3, 84 + }); + + _testOutputHelper.WriteLine(pmsData.ToString()); + Assert.Equal(@"Pms5003Data[ +Pm1Standard=7, +Pm2Dot5Standard=12, +Pm10Standard=13, +Pm1Atmospheric=7, +Pm2Dot5Atmospheric=12, +Pm10Atmospheric=13, +ParticlesDiameterBeyond0Dot3=1506, +ParticlesDiameterBeyond0Dot5=408, +ParticlesDiameterBeyond1Dot0=66, +ParticlesDiameterBeyond2Dot5=12, +ParticlesDiameterBeyond5Dot0=2, +ParticlesDiameterBeyond10Dot0=2, +Reserved=38656, +Checksum=852 +] +", pmsData.ToString()); + } + } +} \ No newline at end of file