Skip to content

Commit

Permalink
Merge pull request #618 from WildernessLabs/feature/next-pm
Browse files Browse the repository at this point in the history
tera netxPM particulate sensor
  • Loading branch information
ctacke authored Apr 20, 2023
2 parents d57b974 + 90c7a60 commit 7c006a7
Show file tree
Hide file tree
Showing 15 changed files with 666 additions and 6 deletions.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using System.Linq;

namespace Meadow.Foundation.Sensors.Environmental
{
internal static class CommandManager
{
public static (int commandLength, int expectedResponseLength) GenerateCommand(NextPm.CommandByte command, byte[] buffer, params byte[] payload)
{
buffer[0] = NextPm.SensorAddress;
buffer[1] = (byte)command;

var payloadLength = 2;

if (payload != null && payload.Length > 0)
{
Array.Copy(payload, 0, buffer, 2, payload.Length);
payloadLength += payload.Length;
}

buffer[payloadLength] = CalculateChecksum(buffer, 0, payloadLength);

switch (command)
{
case NextPm.CommandByte.ReadFirmware:
return (3, 6);
case NextPm.CommandByte.ReadTempAndHumidity:
return (3, 8);
case NextPm.CommandByte.ReadWriteFanSpeed:
return (payloadLength + 1, 6);
case NextPm.CommandByte.SetPowerMode:
return (payloadLength + 1, 4);
case NextPm.CommandByte.ReadSensorState:
return (3, 4);
case NextPm.CommandByte.Read10sConcentrations:
case NextPm.CommandByte.Read60sConcentrations:
case NextPm.CommandByte.Read900sConcentrations:
return (3, 16);
default: throw new NotImplementedException();
}
}

public static byte CalculateChecksum(byte[] data, int start, int length)
{
return (byte)(0x100 - (data.Skip(start).Take(length).Sum(d => d) % 256));
}

public static bool ValidateChecksum(byte[] data, int start, int length)
{
var expected = CalculateChecksum(data, start, length - 1);
return expected == data[start + length - 1];
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Meadow.Foundation.Sensors.Environmental
{
public partial class NextPm
{
internal const byte SensorAddress = 0x81; // fixed address, per the data sheet
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;

namespace Meadow.Foundation.Sensors.Environmental
{
public partial class NextPm
{
internal enum CommandByte : byte
{
Read10sConcentrations = 0x11,
Read60sConcentrations = 0x12,
Read900sConcentrations = 0x13,
ReadTempAndHumidity = 0x14,
SetPowerMode = 0x15,
ReadSensorState = 0x16,
ReadFirmware = 0x17,
ReadWriteFanSpeed = 0x21,
ReadWriteModbusAddress = 0x22
}

[Flags]
internal enum SensorStatus : byte
{
Sleep = 1 << 0,
Degraded = 1 << 1,
NotReady = 1 << 2,
HeaterError = 1 << 3,
TempHumidyError = 1 << 4,
FanError = 1 << 5,
MemoryError = 1 << 6,
LaserError = 1 << 7,
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using Meadow.Hardware;
using System;
using System.Threading.Tasks;

namespace Meadow.Foundation.Sensors.Environmental
{
/// <summary>
/// Represents a TERA Sensor NextPM particulate matter sensor
/// </summary>
public partial class NextPm
{
private ISerialPort _port;
// dev note: these common buffers are used to minimize heap allocations during serial communications with the sensor
private byte[] _readBuffer = new byte[16];
private byte[] _writeBuffer = new byte[16];

private ISerialPort InitializePort(ISerialPort port)
{
if (port.IsOpen)
{
port.Close();
}

port.BaudRate = 115200;
port.DataBits = 8;
port.Parity = Parity.Even;
port.StopBits = StopBits.One;

return port;
}

private async Task SendCommand(CommandByte command, params byte[] payload)
{
var parameters = CommandManager.GenerateCommand(command, _writeBuffer, payload);

if (!_port.IsOpen)
{
_port.Open();
}

_port.Write(_writeBuffer, 0, parameters.commandLength);

var read = 0;
var expected = parameters.expectedResponseLength;
var timeoutMs = 3000;
var readDelay = 500;

await Task.Delay(100); // initial device response delay

Array.Clear(_readBuffer, 0, _readBuffer.Length);

do
{
read += _port.Read(_readBuffer, read, _readBuffer.Length - read);
if (read >= expected) break;

if (read > 2 && _readBuffer[1] != (byte)command && _readBuffer[1] == 0x16)
{
var status = (SensorStatus)_readBuffer[2];
// typically a "fail" due to the device being in sleep state
throw new TeraException($"Device status: {status}");
}

await Task.Delay(readDelay);
timeoutMs -= readDelay;
} while (timeoutMs > 0);

if (read < expected)
{
throw new TeraException("Serial timeout");
}
else
{
var checksumIsValid = CommandManager.ValidateChecksum(_readBuffer, 0, parameters.expectedResponseLength);

if (!checksumIsValid)
{
throw new TeraException("Invalid response checksum");
}
}
}
}
}
Loading

0 comments on commit 7c006a7

Please sign in to comment.