Initial Commit

This commit is contained in:
Denis-Cosmin Nutiu 2021-04-10 17:48:03 +03:00
commit b70e14c028
19 changed files with 613 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
notes.md

13
.idea/.idea.PMS5003/.idea/.gitignore vendored Normal file
View file

@ -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/

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ContentModelUserStore">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

22
PMS5003.sln Normal file
View file

@ -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

View file

@ -0,0 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=3358b775_002D8738_002D4b8b_002D978a_002D13b28217715f/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="Pms5003DataUnitTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;TestAncestor&gt;&#xD;
&lt;TestId&gt;xUnit::AF7541BD-C0B1-42FE-AEFB-1A1D9CEAC6B9::.NETCoreApp,Version=v3.1::PMS5003Tests.Pms5003DataUnitTest&lt;/TestId&gt;&#xD;
&lt;/TestAncestor&gt;&#xD;
&lt;/SessionState&gt;</s:String></wpf:ResourceDictionary>

View file

@ -0,0 +1,16 @@
using System;
namespace PMS5003.Exceptions
{
/// <summary>
/// BufferUnderflowExceptions is thrown when the PMS5003 data buffer is incomplete.
/// </summary>
[Serializable]
public class BufferUnderflowException : Exception
{
public BufferUnderflowException() : base("The PMS5003 data buffer is underrun, not enough bytes read!")
{
}
}
}

View file

@ -0,0 +1,14 @@
using System;
namespace PMS5003.Exceptions
{
/// <summary>
/// ChecksumMismatchException when the frame contents of PMS5003 doesn't match the checksum.
/// </summary>
public class ChecksumMismatchException : Exception
{
public ChecksumMismatchException() : base("Checksum mismatch.")
{
}
}
}

View file

@ -0,0 +1,16 @@
using System;
namespace PMS5003.Exceptions
{
/// <summary>
/// Exception that is thrown by the PMS5003 sensor when the read contains an invalid start sequence.
/// </summary>
[Serializable]
public class InvalidStartByteException : Exception
{
public InvalidStartByteException(short firstByte, short secondByte) : base(
$"Invalid start characters, expected 0x42 0x4d got [{firstByte:X}, {secondByte:X}]")
{
}
}
}

View file

@ -0,0 +1,14 @@
using System;
namespace PMS5003.Exceptions
{
/// <summary>
/// Thrown the the read has failed.
/// </summary>
public class ReadFailedException : Exception
{
public ReadFailedException() : base("PMS5003 read failed, max retries exceeded")
{
}
}
}

151
PMS5003/PMS5003.cs Normal file
View file

@ -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
{
/// <summary>
/// PMS5003 digital universal particle concentration sensor.
/// Datasheet: https://www.aqmd.gov/docs/default-source/aq-spec/resources-page/plantower-pms5003-manual_v2-3.pdf
/// </summary>
public class Pms5003
{
public static Logger<Pms5003> Logger;
private readonly GpioController _gpioController;
private readonly SerialPort _serialPort;
private readonly short _pinSet;
private readonly short _pinReset;
private bool _isSleeping;
/// <summary>
/// Initializes the <see cref="Pms5003"/>.
/// </summary>
/// <param name="portSerialName">The name of the serial port.</param>
/// <param name="pinSet">The number of the SET pin.</param>
/// <param name="pinReset">The number if the RESET pin.</param>
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.");
}
}
/// <summary>
/// Initializes the <see cref="Pms5003"/> using /dev/ttyAMA0 serial port name.
/// </summary>
/// <param name="pinSet"></param>
/// <param name="pinReset"></param>
public Pms5003(short pinSet, short pinReset) : this("/dev/ttyAMA0", pinSet, pinReset)
{
}
/// <summary>
/// Initializes the <see cref="Pms5003"/> using /dev/ttyAMA0 serial port name
/// and no GPIO pins for SET and RESET.
/// </summary>
public Pms5003() : this("/dev/ttyAMA0", -1, -1)
{
}
/// <summary>
/// Reads the data from the sensor.
/// </summary>
/// <returns cref="Pms5003Data">The data.</returns>
/// <exception cref="ReadFailedException">Thrown when the read fails.</exception>
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();
}
/// <summary>
/// Resets the PMS5003 Sensor.
/// </summary>
public void Reset()
{
if (_gpioController == null || _pinReset < 0) return;
_gpioController.Write(_pinReset, PinValue.Low);
Thread.Sleep(200);
_gpioController.Write(_pinReset, PinValue.High);
}
/// <summary>
/// Enables Sleep Mode for the PMS5003 Sensor.
/// </summary>
public void Sleep()
{
if (_gpioController == null || _pinSet < 0) return;
_isSleeping = true;
_gpioController.Write(_pinSet, PinValue.Low);
}
/// <summary>
/// Disables Sleep Mode for the PMS5003 Sensor.
/// </summary>
public void WakeUp()
{
if (_gpioController == null || _pinSet < 0) return;
_isSleeping = false;
_gpioController.Write(_pinSet, PinValue.High);
}
/// <summary>
/// Checks if Sleep Mode is enabled.
/// </summary>
/// <returns>True if sensor is in Sleep Mode, false otherwise.</returns>
public bool IsSleeping()
{
return _isSleeping;
}
~Pms5003()
{
_serialPort.Close();
_gpioController?.Dispose();
}
}
}

13
PMS5003/PMS5003.csproj Normal file
View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Iot.Device.Bindings" Version="1.4.0" />
<PackageReference Include="System.Device.Gpio" Version="1.4.0" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,15 @@
namespace PMS5003
{
/// <summary>
/// The Pms5003Constants defines constants required by the PMS5003 communication protocol.
/// </summary>
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;
}
}

102
PMS5003/Pms5003Data.cs Normal file
View file

@ -0,0 +1,102 @@
using System.Text;
using PMS5003.Exceptions;
namespace PMS5003
{
/// <summary>
/// Pms5003Measurement is an abstraction over the PMS5003 data.
/// </summary>
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];
}
/// <summary>
/// Instantiates a new instance from the given byte buffer.
/// </summary>
/// <param name="buffer">The data buffer</param>
/// <returns>A new instance.</returns>
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;
}
/// <summary>
/// Returns a string representation of the Pms5003Data.
/// </summary>
/// <returns>String with all the fields and values.</returns>
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();
}
}
}

16
PMS5003/Utils.cs Normal file
View file

@ -0,0 +1,16 @@
namespace PMS5003
{
public class Utils
{
/// <summary>
/// Combines two bytes and returns the results ((High RSHIFT 8) | Low)
/// </summary>
/// <param name="high">The high byte.</param>
/// <param name="low">The low byte.</param>
/// <returns></returns>
public static uint CombineBytes(byte high, byte low)
{
return (uint)(low | (high << 8));;
}
}
}

36
PMS5003/readme.md Normal file
View file

@ -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);
}
}
}
}
}
```

View file

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
<PackageReference Include="coverlet.collector" Version="1.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PMS5003\PMS5003.csproj" />
</ItemGroup>
</Project>

View file

@ -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<ChecksumMismatchException>(() => 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<InvalidStartByteException>(() => 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());
}
}
}