-
-
Notifications
You must be signed in to change notification settings - Fork 111
/
Copy pathMhz19b.cs
243 lines (214 loc) · 8.99 KB
/
Mhz19b.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Device.Model;
using System.IO;
using System.IO.Ports;
using System.Threading;
using UnitsNet;
namespace Iot.Device.Mhz19b
{
/// <summary>
/// MH-Z19B CO2 concentration sensor binding
/// </summary>
[Interface("MH-Z19B CO2 concentration sensor binding")]
public sealed class Mhz19b : IDisposable
{
private const int MessageBytes = 9;
private bool _shouldDispose = false;
private SerialPort? _serialPort;
private Stream _serialPortStream;
/// <summary>
/// Initializes a new instance of the <see cref="Mhz19b"/> class using an existing (serial port) stream.
/// </summary>
/// <param name="stream">Existing stream</param>
/// <param name="shouldDispose">If true, the stream gets disposed when disposing the binding</param>
public Mhz19b(Stream stream, bool shouldDispose)
{
_serialPortStream = stream ?? throw new ArgumentNullException(nameof(stream));
_shouldDispose = shouldDispose;
}
/// <summary>
/// Initializes a new instance of the <see cref="Mhz19b"/> class and creates a new serial port stream.
/// </summary>
/// <param name="uartDevice">Path to the UART device / serial port, e.g. /dev/serial0</param>
/// <exception cref="System.ArgumentException">uartDevice is null or empty</exception>
public Mhz19b(string uartDevice)
{
if (uartDevice is not { Length: > 0 })
{
throw new ArgumentException(nameof(uartDevice));
}
// create serial port using the setting acc. to datasheet, pg. 7, sec. general settings
_serialPort = new SerialPort(uartDevice, 9600, Parity.None, 8, StopBits.One)
{
ReadTimeout = 1000,
WriteTimeout = 1000
};
_serialPort.Open();
_serialPortStream = _serialPort.BaseStream;
_shouldDispose = true;
}
/// <summary>
/// Gets the current CO2 concentration from the sensor.
/// </summary>
/// <returns>CO2 volume concentration</returns>
/// <exception cref="IOException">Communication with sensor failed</exception>
/// <exception cref="TimeoutException">A timeout occurred while communicating with the sensor</exception>
[Telemetry("Co2Concentration")]
public VolumeConcentration GetCo2Reading()
{
// send read command request
byte[] request = CreateRequest(Command.ReadCo2Concentration);
request[(int)MessageFormat.Checksum] = Checksum(request);
_serialPortStream.Write(request, 0, request.Length);
// read complete response (9 bytes expected)
byte[] response = new byte[MessageBytes];
long endTicks = DateTime.UtcNow.AddMilliseconds(250).Ticks;
int bytesRead = 0;
while (DateTime.UtcNow.Ticks < endTicks && bytesRead < MessageBytes)
{
bytesRead += _serialPortStream.Read(response, bytesRead, response.Length - bytesRead);
Thread.Sleep(1);
}
if (bytesRead < MessageBytes)
{
throw new TimeoutException($"Communication with sensor failed.");
}
// check response and return calculated concentration if valid
if (response[(int)MessageFormat.Checksum] == Checksum(response))
{
return VolumeConcentration.FromPartsPerMillion((int)response[(int)MessageFormat.DataHighResponse] * 256 + (int)response[(int)MessageFormat.DataLowResponse]);
}
else
{
throw new IOException("Invalid response message received from sensor");
}
}
/// <summary>
/// Initiates a zero point calibration.
/// </summary>
/// <exception cref="System.IO.IOException">Communication with sensor failed</exception>
[Command]
public void PerformZeroPointCalibration() => SendRequest(CreateRequest(Command.CalibrateZeroPoint));
/// <summary>
/// Initiate a span point calibration.
/// </summary>
/// <param name="span">span value, between 1000[ppm] and 5000[ppm]. The typical value is 2000[ppm].</param>
/// <exception cref="System.ArgumentException">Thrown when span value is out of range</exception>
/// <exception cref="System.IO.IOException">Communication with sensor failed</exception>
[Command]
public void PerformSpanPointCalibration(VolumeConcentration span)
{
if ((span.PartsPerMillion < 1000) || (span.PartsPerMillion > 5000))
{
throw new ArgumentException("Span value out of range (1000-5000[ppm])", nameof(span));
}
byte[] request = CreateRequest(Command.CalibrateSpanPoint);
// set span in request, c. f. datasheet rev. 1.0, pg. 8 for details
request[(int)MessageFormat.DataHighRequest] = (byte)(span.PartsPerMillion / 256);
request[(int)MessageFormat.DataLowRequest] = (byte)(span.PartsPerMillion % 256);
SendRequest(request);
}
/// <summary>
/// Switches the autmatic baseline correction on and off.
/// </summary>
/// <param name="state">State of automatic correction</param>
/// <exception cref="System.IO.IOException">Communication with sensor failed</exception>
[Command]
public void SetAutomaticBaselineCorrection(AbmState state)
{
byte[] request = CreateRequest(Command.AutoCalibrationSwitch);
// set on/off state in request, c. f. datasheet rev. 1.0, pg. 8 for details
request[(int)MessageFormat.DataHighRequest] = (byte)state;
SendRequest(request);
}
/// <summary>
/// Set the sensor detection range.
/// </summary>
/// <param name="detectionRange">Detection range of the sensor</param>
/// <exception cref="System.IO.IOException">Communication with sensor failed</exception>
[Property("DetectionRange")]
public void SetSensorDetectionRange(DetectionRange detectionRange)
{
byte[] request = CreateRequest(Command.DetectionRangeSetting);
// set detection range in request, c. f. datasheet rev. 1.0, pg. 8 for details
request[(int)MessageFormat.DataHighRequest] = (byte)((int)detectionRange / 256);
request[(int)MessageFormat.DataLowRequest] = (byte)((int)detectionRange % 256);
SendRequest(request);
}
private void SendRequest(byte[] request)
{
request[(int)MessageFormat.Checksum] = Checksum(request);
try
{
_serialPortStream.Write(request, 0, request.Length);
}
catch (Exception e)
{
throw new IOException("Sensor communication failed", e);
}
}
private byte[] CreateRequest(Command command) => new byte[]
{
0xff, // start byte,
0x01, // sensor number, always 0x1
(byte)command,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // empty bytes
};
/// <summary>
/// Calculate checksum for requests and responses.
/// For details refer to datasheet rev. 1.0, pg. 8.
/// </summary>
/// <param name="packet">Packet the checksum is calculated for</param>
/// <returns>Cheksum</returns>
private byte Checksum(byte[] packet)
{
byte checksum = 0;
for (int i = 1; i < 8; i++)
{
checksum += packet[i];
}
checksum = (byte)(0xff - checksum);
checksum += 1;
return checksum;
}
/// <inheritdoc cref="IDisposable" />
public void Dispose()
{
if (_shouldDispose)
{
_serialPortStream?.Dispose();
_serialPortStream = null!;
}
if (_serialPort is not null)
{
if (_serialPort.IsOpen)
{
_serialPort.Close();
}
}
_serialPort?.Dispose();
_serialPort = null;
}
private enum Command : byte
{
ReadCo2Concentration = 0x86,
CalibrateZeroPoint = 0x87,
CalibrateSpanPoint = 0x88,
AutoCalibrationSwitch = 0x79,
DetectionRangeSetting = 0x99
}
private enum MessageFormat
{
Start = 0x00,
SensorNum = 0x01,
Command = 0x02,
DataHighRequest = 0x03,
DataLowRequest = 0x04,
DataHighResponse = 0x02,
DataLowResponse = 0x03,
Checksum = 0x08
}
}
}