HF-FDO2 Driver 0.1.0-dev
UART driver for PyroScience FDO2-G2 (data sheet v5 §4: #MOXY, #MRAW, #VERS)
Loading...
Searching...
No Matches
fdo2_driver.hpp
Go to the documentation of this file.
1
16#pragma once
17
18#include "fdo2_types.hpp"
20
21#include <cctype>
22#include <cstdio>
23#include <cstdlib>
24#include <cstring>
25#include <string_view>
26
27namespace fdo2 {
28namespace detail {
29
30inline bool AppendCmd(char* buf, std::size_t cap, std::string_view cmd) noexcept {
31 if (cmd.size() + 2U > cap) {
32 return false;
33 }
34 std::memcpy(buf, cmd.data(), cmd.size());
35 buf[cmd.size()] = '\r';
36 buf[cmd.size() + 1U] = '\0';
37 return true;
38}
39
41inline void StripOptionalModbusCrcSuffix(char* line) noexcept {
42 if (line == nullptr || line[0] == '\0') {
43 return;
44 }
45 char* colon = std::strrchr(line, ':');
46 if (colon == nullptr || colon <= line + 1) {
47 return;
48 }
49 if (colon[-1] != ' ') {
50 return;
51 }
52 const char* p = colon + 1;
53 while (*p != '\0') {
54 if (std::isdigit(static_cast<unsigned char>(*p)) == 0) {
55 return;
56 }
57 ++p;
58 }
59 colon[-1] = '\0';
60}
61
62template <typename UartT>
63inline DriverError ReadAsciiLine(UartT& uart, char* buf, std::size_t cap,
64 uint32_t timeout_ms) noexcept {
65 if (cap < 2) {
67 }
68 uint8_t tmp[512];
69 const std::size_t max_rx = sizeof(tmp) - 1U;
70 const std::size_t n = uart.read(tmp, max_rx, timeout_ms);
71 if (n == 0U) {
73 }
74 std::size_t pos = 0;
75 for (std::size_t i = 0; i < n; ++i) {
76 const char c = static_cast<char>(tmp[i]);
77 if (c == '\r') {
78 break;
79 }
80 if (c == '\n') {
81 continue;
82 }
83 if (pos + 1 >= cap) {
85 }
86 buf[pos++] = c;
87 }
88 buf[pos] = '\0';
90 return DriverError::None;
91}
92
93inline const char* SkipWs(const char* p) noexcept {
94 while (*p != '\0' && std::isspace(static_cast<unsigned char>(*p)) != 0) {
95 ++p;
96 }
97 return p;
98}
99
100inline DriverError Tokenize(const char* line, char* store, std::size_t store_cap,
101 const char* tokens[], std::size_t max_tokens,
102 std::size_t* out_count) noexcept {
103 *out_count = 0;
104 const char* p = SkipWs(line);
105 if (*p == '\0') {
107 }
108 std::size_t bi = 0;
109 while (*p != '\0' && *out_count < max_tokens) {
110 if (bi + 1U >= store_cap) {
112 }
113 const std::size_t start = bi;
114 tokens[*out_count] = store + start;
115 while (*p != '\0' && std::isspace(static_cast<unsigned char>(*p)) == 0) {
116 if (bi + 1U >= store_cap) {
118 }
119 store[bi++] = *p++;
120 }
121 if (bi + 1U >= store_cap) {
123 }
124 store[bi++] = '\0';
125 ++(*out_count);
126 p = SkipWs(p);
127 }
128 if (*p != '\0') {
130 }
131 return DriverError::None;
132}
133
134inline int32_t ParseI32(const char* s, bool* ok) noexcept {
135 char* end = nullptr;
136 const long v = std::strtol(s, &end, 10);
137 if (ok != nullptr) {
138 *ok = (end != s) && (*end == '\0');
139 }
140 constexpr long kMin = -2147483648L;
141 constexpr long kMax = 2147483647L;
142 if (v < kMin || v > kMax) {
143 if (ok != nullptr) {
144 *ok = false;
145 }
146 return 0;
147 }
148 return static_cast<int32_t>(v);
149}
150
151inline uint32_t ParseU32(const char* s, bool* ok) noexcept {
152 char* end = nullptr;
153 const unsigned long v = std::strtoul(s, &end, 10);
154 if (ok != nullptr) {
155 *ok = (end != s) && (*end == '\0');
156 }
157 return static_cast<uint32_t>(v);
158}
159
160inline uint64_t ParseU64(const char* s, bool* ok) noexcept {
161 char* end = nullptr;
162 const unsigned long long v = std::strtoull(s, &end, 10);
163 if (ok != nullptr) {
164 *ok = (end != s) && (*end == '\0');
165 }
166 return static_cast<uint64_t>(v);
167}
168
169inline bool IsErroHeader(const char* tok0) noexcept {
170 return std::strncmp(tok0, "#ERRO", 5) == 0;
171}
172
173} // namespace detail
174
179template <typename UartT>
180class Driver {
181public:
182 explicit Driver(UartT& uart) noexcept : uart_(uart) {}
183
184 void SetLineTimeoutMs(uint32_t ms) noexcept { line_timeout_ms_ = ms; }
185 void SetMeasureTimeoutMs(uint32_t ms) noexcept { measure_timeout_ms_ = ms; }
186 void SetSlowCommandTimeoutMs(uint32_t ms) noexcept { slow_timeout_ms_ = ms; }
187
188 uint32_t GetLineTimeoutMs() const noexcept { return line_timeout_ms_; }
189 uint32_t GetMeasureTimeoutMs() const noexcept { return measure_timeout_ms_; }
190 int32_t LastDeviceErrorCode() const noexcept { return last_device_error_; }
191
193 last_device_error_ = 0;
194 char tx[16];
195 if (!detail::AppendCmd(tx, sizeof(tx), "#VERS")) {
197 }
198 char line[160];
199 const auto err = TransactLine(tx, line, sizeof(line));
200 if (err != DriverError::None) {
202 }
203 char tstore[128];
204 const char* tok[8];
205 std::size_t nt = 0;
206 if (detail::Tokenize(line, tstore, sizeof(tstore), tok, 8, &nt) != DriverError::None) {
208 }
209 if (nt >= 2 && detail::IsErroHeader(tok[0])) {
210 return FailErro<VersionInfo>(tok, nt);
211 }
212 if (nt < 5U || std::strncmp(tok[0], "#VERS", 5) != 0) {
214 }
215 bool ok = true;
216 VersionInfo v{};
217 v.device_id = detail::ParseI32(tok[1], &ok);
218 v.num_channels = detail::ParseI32(tok[2], &ok);
219 v.firmware_revision = detail::ParseI32(tok[3], &ok);
220 v.sensor_types = detail::ParseI32(tok[4], &ok);
221 if (!ok) {
223 }
225 }
226
228 last_device_error_ = 0;
229 char tx[16];
230 if (!detail::AppendCmd(tx, sizeof(tx), "#IDNR")) {
232 }
233 char line[96];
234 const auto err = TransactLine(tx, line, sizeof(line));
235 if (err != DriverError::None) {
237 }
238 char tstore[128];
239 const char* tok[8];
240 std::size_t nt = 0;
241 if (detail::Tokenize(line, tstore, sizeof(tstore), tok, 8, &nt) != DriverError::None) {
243 }
244 if (nt >= 2 && detail::IsErroHeader(tok[0])) {
245 return FailErro<uint64_t>(tok, nt);
246 }
247 if (nt != 2U || std::strncmp(tok[0], "#IDNR", 5) != 0) {
249 }
250 bool ok = false;
251 const uint64_t id = detail::ParseU64(tok[1], &ok);
252 if (!ok) {
254 }
256 }
257
259 DriverResult<MoxyReading> MeasureMoxy(uint32_t timeout_ms = 0U) noexcept {
260 last_device_error_ = 0;
261 char tx[12];
262 if (!detail::AppendCmd(tx, sizeof(tx), "#MOXY")) {
264 }
265 char line[96];
266 const uint32_t tmo = (timeout_ms != 0U) ? timeout_ms : measure_timeout_ms_;
267 const auto err = TransactLine(tx, line, sizeof(line), tmo);
268 if (err != DriverError::None) {
270 }
271 char tstore[128];
272 const char* tok[8];
273 std::size_t nt = 0;
274 if (detail::Tokenize(line, tstore, sizeof(tstore), tok, 8, &nt) != DriverError::None) {
276 }
277 if (nt >= 2 && detail::IsErroHeader(tok[0])) {
278 return FailErro<MoxyReading>(tok, nt);
279 }
280 if (nt != 4U || std::strncmp(tok[0], "#MOXY", 5) != 0) {
282 }
283 bool ok = true;
284 const int32_t o = detail::ParseI32(tok[1], &ok);
285 const int32_t t = detail::ParseI32(tok[2], &ok);
286 const uint32_t s = detail::ParseU32(tok[3], &ok);
287 if (!ok) {
289 }
291 }
292
294 DriverResult<MrawReading> MeasureMraw(uint32_t timeout_ms = 0U) noexcept {
295 last_device_error_ = 0;
296 char tx[12];
297 if (!detail::AppendCmd(tx, sizeof(tx), "#MRAW")) {
299 }
300 char line[192];
301 const uint32_t tmo = (timeout_ms != 0U) ? timeout_ms : measure_timeout_ms_;
302 const auto err = TransactLine(tx, line, sizeof(line), tmo);
303 if (err != DriverError::None) {
305 }
306 char tstore[256];
307 const char* tok[16];
308 std::size_t nt = 0;
309 if (detail::Tokenize(line, tstore, sizeof(tstore), tok, 16, &nt) != DriverError::None) {
311 }
312 if (nt >= 2 && detail::IsErroHeader(tok[0])) {
313 return FailErro<MrawReading>(tok, nt);
314 }
315 if (nt != 9U || std::strncmp(tok[0], "#MRAW", 5) != 0) {
317 }
318 bool ok = true;
319 const int32_t o = detail::ParseI32(tok[1], &ok);
320 const int32_t t = detail::ParseI32(tok[2], &ok);
321 const uint32_t s = detail::ParseU32(tok[3], &ok);
322 const int32_t d = detail::ParseI32(tok[4], &ok);
323 const int32_t i = detail::ParseI32(tok[5], &ok);
324 const int32_t a = detail::ParseI32(tok[6], &ok);
325 const int32_t p = detail::ParseI32(tok[7], &ok);
326 const int32_t h = detail::ParseI32(tok[8], &ok);
327 if (!ok) {
329 }
330 return DriverResult<MrawReading>::success(DecodeMraw(o, t, s, d, i, a, p, h));
331 }
332
335 last_device_error_ = 0;
336 return SimpleEchoCommand("#LOGO", 5);
337 }
338
339private:
340 template <typename T>
341 DriverResult<T> FailErro(const char* tok[], std::size_t nt) noexcept {
342 if (nt >= 2) {
343 bool ok = false;
344 last_device_error_ = detail::ParseI32(tok[1], &ok);
345 if (!ok) {
346 last_device_error_ = -1;
347 }
348 } else {
349 last_device_error_ = -1;
350 }
352 }
353
354 DriverResult<void> SimpleEchoCommand(const char* cmd, std::size_t cmd_len) noexcept {
355 char tx[16];
356 if (cmd_len + 2U > sizeof(tx)) {
358 }
359 char line[32];
360 std::memcpy(tx, cmd, cmd_len);
361 tx[cmd_len] = '\r';
362 tx[cmd_len + 1] = '\0';
363 const auto err = TransactLine(tx, line, sizeof(line), line_timeout_ms_);
364 if (err != DriverError::None) {
365 return DriverResult<void>::failure(err);
366 }
367 char tstore[48];
368 const char* tok[4];
369 std::size_t nt = 0;
370 if (detail::Tokenize(line, tstore, sizeof(tstore), tok, 4, &nt) != DriverError::None) {
372 }
373 if (nt >= 2 && detail::IsErroHeader(tok[0])) {
374 return FailErro<void>(tok, nt);
375 }
376 if (nt < 1U || std::strncmp(tok[0], cmd, cmd_len) != 0) {
378 }
380 }
381
382 DriverError TransactLine(const char* tx, char* line, std::size_t line_cap,
383 uint32_t timeout_ms = 0U) noexcept {
384 uart_.flush_rx();
385 uart_.write(reinterpret_cast<const uint8_t*>(tx), std::strlen(tx));
386 const uint32_t tmo = (timeout_ms != 0U) ? timeout_ms : line_timeout_ms_;
387 return detail::ReadAsciiLine(uart_, line, line_cap, tmo);
388 }
389
390 UartT& uart_;
391 uint32_t line_timeout_ms_{400};
392 uint32_t measure_timeout_ms_{250};
393 uint32_t slow_timeout_ms_{12000};
394 int32_t last_device_error_{0};
395};
396
397} // namespace fdo2
FDO2-G2 UART client.
Definition fdo2_driver.hpp:180
DriverResult< MoxyReading > MeasureMoxy(uint32_t timeout_ms=0U) noexcept
Single oxygen + temperature + status round-trip (typically < ~150 ms for M=2).
Definition fdo2_driver.hpp:259
DriverResult< uint64_t > ReadUniqueId() noexcept
Definition fdo2_driver.hpp:227
Driver(UartT &uart) noexcept
Definition fdo2_driver.hpp:182
uint32_t GetMeasureTimeoutMs() const noexcept
Definition fdo2_driver.hpp:189
void SetLineTimeoutMs(uint32_t ms) noexcept
Definition fdo2_driver.hpp:184
DriverResult< VersionInfo > ReadVersion() noexcept
Definition fdo2_driver.hpp:192
DriverResult< void > FlashLogo() noexcept
Flash the status LED (identification).
Definition fdo2_driver.hpp:334
int32_t LastDeviceErrorCode() const noexcept
Definition fdo2_driver.hpp:190
void SetSlowCommandTimeoutMs(uint32_t ms) noexcept
Definition fdo2_driver.hpp:186
void SetMeasureTimeoutMs(uint32_t ms) noexcept
Definition fdo2_driver.hpp:185
uint32_t GetLineTimeoutMs() const noexcept
Definition fdo2_driver.hpp:188
DriverResult< MrawReading > MeasureMraw(uint32_t timeout_ms=0U) noexcept
Same measurement plus raw optics / vent-path pressure / internal RH.
Definition fdo2_driver.hpp:294
FDO2-G2 UART types, scaling, and status decoding (PyroScience data sheet v5).
CRTP byte transport for the PyroScience Unified Protocol (PSUP).
DriverError Tokenize(const char *line, char *store, std::size_t store_cap, const char *tokens[], std::size_t max_tokens, std::size_t *out_count) noexcept
Definition fdo2_driver.hpp:100
const char * SkipWs(const char *p) noexcept
Definition fdo2_driver.hpp:93
bool IsErroHeader(const char *tok0) noexcept
Definition fdo2_driver.hpp:169
int32_t ParseI32(const char *s, bool *ok) noexcept
Definition fdo2_driver.hpp:134
void StripOptionalModbusCrcSuffix(char *line) noexcept
If CRC is enabled, response is ... : <decimal>\\r. Strip from last " :".
Definition fdo2_driver.hpp:41
bool AppendCmd(char *buf, std::size_t cap, std::string_view cmd) noexcept
Definition fdo2_driver.hpp:30
uint64_t ParseU64(const char *s, bool *ok) noexcept
Definition fdo2_driver.hpp:160
uint32_t ParseU32(const char *s, bool *ok) noexcept
Definition fdo2_driver.hpp:151
DriverError ReadAsciiLine(UartT &uart, char *buf, std::size_t cap, uint32_t timeout_ms) noexcept
Definition fdo2_driver.hpp:63
Definition fdo2.hpp:19
MrawReading DecodeMraw(int32_t o_raw, int32_t t_raw, uint32_t s, int32_t d_raw, int32_t i_raw, int32_t a_raw, int32_t p_raw, int32_t h_raw) noexcept
Definition fdo2_types.hpp:150
DriverError
Driver-level error codes (stable for logging / telemetry).
Definition fdo2_types.hpp:28
MoxyReading DecodeMoxy(int32_t o_raw, int32_t t_raw, uint32_t s) noexcept
Definition fdo2_types.hpp:140
Definition fdo2_types.hpp:50
static constexpr DriverResult success(T v) noexcept
Definition fdo2_types.hpp:57
static constexpr DriverResult failure(DriverError e) noexcept
Definition fdo2_types.hpp:58
#VERS D N R S fields (FDO2-G2 data sheet §4.3).
Definition fdo2_types.hpp:76
int32_t device_id
D (8 for FDO2-G2).
Definition fdo2_types.hpp:77