Skip to content

Commit c076893

Browse files
authored
Add support for the Sensirion SPS30 (#417)
1 parent d6bab8c commit c076893

19 files changed

+1109
-0
lines changed

devices/Sps30/Entities/Measurement.cs

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
//
2+
// Copyright (c) 2017 The nanoFramework project contributors
3+
// See LICENSE file in the project root for full license information.
4+
//
5+
6+
using System;
7+
using System.Buffers.Binary;
8+
using UnitsNet;
9+
10+
namespace Iot.Device.Sps30.Entities
11+
{
12+
/// <summary>
13+
/// Measurement class that can house both response types (Float vs UInt16) by using doubles. Depending on the
14+
/// amount of bytes passed, we can deduct the type.
15+
/// </summary>
16+
public class Measurement
17+
{
18+
/// <summary>
19+
/// Parse the passed data into usable measurements. Depending on the amount of bytes passed, the originally
20+
/// requested type is deducted and parsed accordingly.
21+
/// </summary>
22+
/// <param name="data">The response data on the requested measurement</param>
23+
public Measurement(byte[] data)
24+
{
25+
if (data.Length >= 40)
26+
{
27+
// When we have 40 bytes of data, we assume Float was requested and will be parsed as such
28+
Format = MeasurementOutputFormat.Float;
29+
MassConcentrationPm10 = MassConcentration.FromMicrogramsPerCubicMeter(BinaryPrimitives.ReadSingleBigEndian(new SpanByte(data, 0, 4)));
30+
MassConcentrationPm25 = MassConcentration.FromMicrogramsPerCubicMeter(BinaryPrimitives.ReadSingleBigEndian(new SpanByte(data, 4, 4)));
31+
MassConcentrationPm40 = MassConcentration.FromMicrogramsPerCubicMeter(BinaryPrimitives.ReadSingleBigEndian(new SpanByte(data, 8, 4)));
32+
MassConcentrationPm100 = MassConcentration.FromMicrogramsPerCubicMeter(BinaryPrimitives.ReadSingleBigEndian(new SpanByte(data, 12, 4)));
33+
NumberConcentrationPm05 = BinaryPrimitives.ReadSingleBigEndian(new SpanByte(data, 16, 4));
34+
NumberConcentrationPm10 = BinaryPrimitives.ReadSingleBigEndian(new SpanByte(data, 20, 4));
35+
NumberConcentrationPm25 = BinaryPrimitives.ReadSingleBigEndian(new SpanByte(data, 24, 4));
36+
NumberConcentrationPm40 = BinaryPrimitives.ReadSingleBigEndian(new SpanByte(data, 28, 4));
37+
NumberConcentrationPm100 = BinaryPrimitives.ReadSingleBigEndian(new SpanByte(data, 32, 4));
38+
TypicalParticleSize = Length.FromMicrometers(BinaryPrimitives.ReadSingleBigEndian(new SpanByte(data, 36, 4)));
39+
}
40+
else if (data.Length >= 20)
41+
{
42+
// When we have 20 bytes of data, we assume UInt16 was requested and will be parsed as such
43+
Format = MeasurementOutputFormat.UInt16;
44+
MassConcentrationPm10 = MassConcentration.FromMicrogramsPerCubicMeter(BinaryPrimitives.ReadUInt16BigEndian(new SpanByte(data, 0, 2)));
45+
MassConcentrationPm25 = MassConcentration.FromMicrogramsPerCubicMeter(BinaryPrimitives.ReadUInt16BigEndian(new SpanByte(data, 2, 2)));
46+
MassConcentrationPm40 = MassConcentration.FromMicrogramsPerCubicMeter(BinaryPrimitives.ReadUInt16BigEndian(new SpanByte(data, 4, 2)));
47+
MassConcentrationPm100 = MassConcentration.FromMicrogramsPerCubicMeter(BinaryPrimitives.ReadUInt16BigEndian(new SpanByte(data, 6, 2)));
48+
NumberConcentrationPm05 = BinaryPrimitives.ReadUInt16BigEndian(new SpanByte(data, 8, 2));
49+
NumberConcentrationPm10 = BinaryPrimitives.ReadUInt16BigEndian(new SpanByte(data, 10, 2));
50+
NumberConcentrationPm25 = BinaryPrimitives.ReadUInt16BigEndian(new SpanByte(data, 12, 2));
51+
NumberConcentrationPm40 = BinaryPrimitives.ReadUInt16BigEndian(new SpanByte(data, 14, 2));
52+
NumberConcentrationPm100 = BinaryPrimitives.ReadUInt16BigEndian(new SpanByte(data, 16, 2));
53+
TypicalParticleSize = Length.FromNanometers(BinaryPrimitives.ReadUInt16BigEndian(new SpanByte(data, 18, 2)));
54+
}
55+
else
56+
{
57+
throw new ApplicationException($"Not enough bytes received to parse a measurement");
58+
}
59+
}
60+
61+
/// <summary>
62+
/// The format assumed when parsing the data for this measurement instance.
63+
/// </summary>
64+
public MeasurementOutputFormat Format { get; protected set; }
65+
66+
/// <summary>
67+
/// Mass Concentration PM1.0 [µg/m³]
68+
/// </summary>
69+
public MassConcentration MassConcentrationPm10 { get; protected set; }
70+
71+
/// <summary>
72+
/// Mass Concentration PM2.5 [µg/m³]
73+
/// </summary>
74+
public MassConcentration MassConcentrationPm25 { get; protected set; }
75+
76+
/// <summary>
77+
/// Mass Concentration PM4.0 [µg/m³]
78+
/// </summary>
79+
public MassConcentration MassConcentrationPm40 { get; protected set; }
80+
81+
/// <summary>
82+
/// Mass Concentration PM10.0 [µg/m³]
83+
/// </summary>
84+
public MassConcentration MassConcentrationPm100 { get; protected set; }
85+
86+
/// <summary>
87+
/// Number Concentration PM0.5 [#/cm³]
88+
/// </summary>
89+
public double NumberConcentrationPm05 { get; protected set; }
90+
91+
/// <summary>
92+
/// Number Concentration PM1.0 [#/cm³]
93+
/// </summary>
94+
public double NumberConcentrationPm10 { get; protected set; }
95+
96+
/// <summary>
97+
/// Number Concentration PM2.5 [#/cm³]
98+
/// </summary>
99+
public double NumberConcentrationPm25 { get; protected set; }
100+
101+
/// <summary>
102+
/// Number Concentration PM4.0 [#/cm³]
103+
/// </summary>
104+
public double NumberConcentrationPm40 { get; protected set; }
105+
106+
/// <summary>
107+
/// Number Concentration PM10.0 [#/cm³]
108+
/// </summary>
109+
public double NumberConcentrationPm100 { get; protected set; }
110+
111+
/// <summary>
112+
/// Typical Particle Size depending on format (in µm for Float and nm for ushort, see <see cref="MeasurementOutputFormat"/>)
113+
/// </summary>
114+
public Length TypicalParticleSize { get; protected set; }
115+
116+
/// <summary>
117+
/// Conveniently show the measurement in a single string.
118+
/// </summary>
119+
/// <returns>The measurement as a convenient string</returns>
120+
public override string ToString()
121+
{
122+
return $"MassConcentration [µg/m³] PM1.0={MassConcentrationPm10.MicrogramsPerCubicMeter}, PM2.5={MassConcentrationPm25.MicrogramsPerCubicMeter}, PM4.0={MassConcentrationPm40.MicrogramsPerCubicMeter}, PM10.0={MassConcentrationPm100.MicrogramsPerCubicMeter}, NumberConcentration [#/cm³] PM0.5={NumberConcentrationPm05}, PM1.0={NumberConcentrationPm10}, PM2.5={NumberConcentrationPm25}, PM4.0={NumberConcentrationPm40}, PM10.0={NumberConcentrationPm100}, TypicalParticleSize[nm]={TypicalParticleSize.Nanometers}";
123+
}
124+
}
125+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// Copyright (c) 2017 The nanoFramework project contributors
3+
// See LICENSE file in the project root for full license information.
4+
//
5+
6+
namespace Iot.Device.Sps30.Entities
7+
{
8+
/// <summary>
9+
/// The SPS30 supports two output formats, depending on its firmware version. Float (1.0+) and ushorts (2.0+). Both formats are supported in this library.
10+
/// </summary>
11+
public enum MeasurementOutputFormat : byte
12+
{
13+
/// <summary>
14+
/// Measurement using big-endian float IEEE754. This works on earlier firmware versions.
15+
/// </summary>
16+
Float = 0x03,
17+
18+
/// <summary>
19+
/// Measurement using big-endian unsigned 16-bit integer. This requires firmware version 2.0.
20+
/// </summary>
21+
UInt16 = 0x05
22+
}
23+
}

devices/Sps30/Entities/Status.cs

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//
2+
// Copyright (c) 2017 The nanoFramework project contributors
3+
// See LICENSE file in the project root for full license information.
4+
//
5+
6+
using System;
7+
using System.Buffers.Binary;
8+
9+
namespace Iot.Device.Sps30.Entities
10+
{
11+
/// <summary>
12+
/// Parsed response after requesting the DeviceStatus.
13+
/// </summary>
14+
public class DeviceStatus
15+
{
16+
/// <summary>
17+
/// Parses the raw device status and initializes the public fields for readout.
18+
/// </summary>
19+
/// <param name="data">Raw data from the response</param>
20+
/// <exception cref="ArgumentOutOfRangeException">When less than 4 bytes are provided</exception>
21+
public DeviceStatus(byte[] data)
22+
{
23+
if (data.Length < 4)
24+
{
25+
throw new ArgumentOutOfRangeException(nameof(data), "Unexpected array size. Expecting at least 4 bytes."); // 5th byte is reserved
26+
}
27+
28+
RawRegister = BinaryPrimitives.ReadUInt32BigEndian(data);
29+
FanFailureBlockedOrBroken = (RawRegister & (1 << 4)) > 0;
30+
LaserFailure = (RawRegister & (1 << 5)) > 0;
31+
FanSpeedOutOfRange = (RawRegister & (1 << 21)) > 0;
32+
}
33+
34+
/// <summary>
35+
/// The raw value returned by the device.
36+
/// </summary>
37+
public uint RawRegister { get; private set; }
38+
39+
/// <summary>
40+
/// True when the "Fan speed out of range" bit is set. It's either too high or too low. Check the SPS30 data sheet for more information.
41+
/// </summary>
42+
public bool FanSpeedOutOfRange { get; private set; }
43+
44+
/// <summary>
45+
/// True when the "Laser failure" bit is set. This can occur at high temperatures outside of specifications or when the laser module is defective. Check the SPS30 data sheet for more information.
46+
/// </summary>
47+
public bool LaserFailure { get; private set; }
48+
49+
/// <summary>
50+
/// True when the "Fan failure" bit is set. The fan may be mechanically blocked or broken. Check the SPS30 data sheet for more information.
51+
/// </summary>
52+
public bool FanFailureBlockedOrBroken { get; private set; }
53+
54+
/// <summary>
55+
/// Conveniently show the status in a single string.
56+
/// </summary>
57+
/// <returns>The device status as a convenient string</returns>
58+
public override string ToString()
59+
{
60+
return $"{nameof(RawRegister)}: {RawRegister}, {nameof(FanSpeedOutOfRange)}: {FanSpeedOutOfRange}, {nameof(LaserFailure)}: {LaserFailure}, {nameof(FanFailureBlockedOrBroken)}: {FanFailureBlockedOrBroken}";
61+
}
62+
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//
2+
// Copyright (c) 2017 The nanoFramework project contributors
3+
// See LICENSE file in the project root for full license information.
4+
//
5+
6+
using System;
7+
8+
namespace Iot.Device.Sps30.Entities
9+
{
10+
/// <summary>
11+
/// Parsed response after requesting version information.
12+
/// </summary>
13+
public class VersionInformation
14+
{
15+
/// <summary>
16+
/// Parsed response after requesting version information.
17+
/// </summary>
18+
/// <param name="data">Raw data from the response</param>
19+
/// <exception cref="ArgumentOutOfRangeException">When less than 7 bytes are provided</exception>
20+
public VersionInformation(byte[] data)
21+
{
22+
if (data.Length < 7)
23+
{
24+
throw new ArgumentOutOfRangeException(nameof(data), "Unexpected array size. Expecting at least 7 bytes.");
25+
}
26+
27+
FirmwareVersion = new Version(data[0], data[1]);
28+
// data[2] is reserved
29+
HardwareRevision = data[3];
30+
// data[4] is reserved
31+
ShdlcProtocolVersion = new Version(data[5], data[6]);
32+
}
33+
34+
/// <summary>
35+
/// Firmware version
36+
/// </summary>
37+
public Version FirmwareVersion { get; private set; }
38+
39+
/// <summary>
40+
/// Hardware rivision
41+
/// </summary>
42+
public int HardwareRevision { get; private set; }
43+
44+
/// <summary>
45+
/// SHDLC protocol version
46+
/// </summary>
47+
public Version ShdlcProtocolVersion { get; private set; }
48+
49+
/// <summary>
50+
/// Conveniently show the version information in a single string.
51+
/// </summary>
52+
/// <returns>The version information as a convenient string</returns>
53+
public override string ToString()
54+
{
55+
return $"Firmware V{FirmwareVersion}, Hardware V{HardwareRevision}, SHDLC V{ShdlcProtocolVersion}";
56+
}
57+
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//
2+
// Copyright (c) 2017 The nanoFramework project contributors
3+
// See LICENSE file in the project root for full license information.
4+
//
5+
6+
namespace System.IO
7+
{
8+
/// <summary>
9+
/// Extensions on the MemoryStream to facilitate byte stuffing
10+
/// </summary>
11+
public static class MemoryStreamExtensions
12+
{
13+
/// <summary>
14+
/// Helper method that automatically stuffs bytes if required according to the SHDLC protocol.
15+
/// </summary>
16+
/// <param name="memoryStream">The stream to write to</param>
17+
/// <param name="b">The byte that is potentially stuffed/escaped</param>
18+
public static void WriteByteStuffed(this MemoryStream memoryStream, byte b)
19+
{
20+
switch (b)
21+
{
22+
case 0x7e:
23+
memoryStream.Write(new byte[] { 0x7d, 0x5e }, 0, 2);
24+
break;
25+
case 0x7d:
26+
memoryStream.Write(new byte[] { 0x7d, 0x5d }, 0, 2);
27+
break;
28+
case 0x11:
29+
memoryStream.Write(new byte[] { 0x7d, 0x31 }, 0, 2);
30+
break;
31+
case 0x13:
32+
memoryStream.Write(new byte[] { 0x7d, 0x33 }, 0, 2);
33+
break;
34+
default:
35+
memoryStream.WriteByte(b);
36+
break;
37+
}
38+
}
39+
40+
/// <summary>
41+
/// Helper method that automatically stuffs bytes if required according to the SHDLC protocol.
42+
/// </summary>
43+
/// <param name="memoryStream">The stream to write to</param>
44+
/// <param name="buffer">The bytes that are potentially stuffed/escaped when written</param>
45+
/// <param name="offset">Location within the buffer to start at</param>
46+
/// <param name="count">Number of bytes to take from the start position</param>
47+
public static void WriteStuffed(this MemoryStream memoryStream, byte[] buffer, int offset, int count)
48+
{
49+
for (int i = offset; i < offset + count; i++)
50+
{
51+
memoryStream.WriteByteStuffed(buffer[i]);
52+
}
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)