FDO2 Driver 1.0.0
API reference
Loading...
Searching...
No Matches
fdo2_driver.hpp
Go to the documentation of this file.
1
17#pragma once
18
19#include "fdo2_types.hpp"
21
22#include <cctype>
23#include <cerrno>
24#include <cstdio>
25#include <cstdlib>
26#include <cstring>
27#include <string_view>
28
29namespace fdo2 {
30namespace detail {
31
32inline bool AppendCmd(char* buf, std::size_t cap, std::string_view cmd) noexcept {
33 if (cmd.size() + 2U > cap) {
34 return false;
35 }
36 std::memcpy(buf, cmd.data(), cmd.size());
37 buf[cmd.size()] = '\r';
38 buf[cmd.size() + 1U] = '\0';
39 return true;
40}
41
47inline uint16_t Crc16Modbus(const char* data, std::size_t len) noexcept {
48 uint16_t crc = 0xFFFFU;
49 for (std::size_t i = 0; i < len; ++i) {
50 crc = static_cast<uint16_t>(crc ^ static_cast<unsigned char>(data[i]));
51 for (int bit = 0; bit < 8; ++bit) {
52 if ((crc & 1U) != 0U) {
53 crc = static_cast<uint16_t>((crc >> 1U) ^ 0xA001U);
54 } else {
55 crc = static_cast<uint16_t>(crc >> 1U);
56 }
57 }
58 }
59 return crc;
60}
61
72inline DriverError VerifyAndStripCrc(char* line, bool require) noexcept {
73 char* colon = std::strrchr(line, ':');
74 if (colon == nullptr) {
76 }
77 const char* p = colon + 1;
78 while (*p == ' ') {
79 ++p;
80 }
81 uint32_t value = 0;
82 std::size_t nd = 0;
83 while (std::isdigit(static_cast<unsigned char>(*p)) != 0) {
84 value = value * 10U + static_cast<uint32_t>(*p - '0');
85 ++p;
86 if (++nd > 5U) {
88 }
89 }
90 if (nd == 0U || *p != '\0' || value > 0xFFFFU) {
92 }
93 const std::size_t covered = static_cast<std::size_t>(colon - line);
94 if (Crc16Modbus(line, covered) != static_cast<uint16_t>(value)) {
96 }
97 *colon = '\0';
98 return DriverError::None;
99}
100
102[[deprecated("use VerifyAndStripCrc(), which checks the checksum")]]
103inline void StripOptionalModbusCrcSuffix(char* line) noexcept {
104 if (line != nullptr) {
105 char* colon = std::strrchr(line, ':');
106 if (colon != nullptr) {
107 *colon = '\0';
108 }
109 }
110}
111
112template <typename UartT>
113inline DriverError ReadAsciiLine(UartT& uart, char* buf, std::size_t cap, uint32_t timeout_ms,
114 bool require_crc = false) noexcept {
115 if (cap < 2) {
117 }
118 uint8_t tmp[512];
119 const std::size_t max_rx = sizeof(tmp) - 1U;
120 const std::size_t n = uart.read(tmp, max_rx, timeout_ms);
121 if (n == 0U) {
123 }
124 std::size_t pos = 0;
125 bool terminated = false;
126 for (std::size_t i = 0; i < n; ++i) {
127 const char c = static_cast<char>(tmp[i]);
128 if (c == '\r') {
129 terminated = true;
130 break;
131 }
132 if (c == '\n') {
133 continue;
134 }
135 if (pos + 1 >= cap) {
137 }
138 buf[pos++] = c;
139 }
140 buf[pos] = '\0';
141 if (!terminated) {
142 // Data sheet §4.1: every reply ends with a single CR. Without it the line is incomplete.
144 }
145 return VerifyAndStripCrc(buf, require_crc);
146}
147
148inline const char* SkipWs(const char* p) noexcept {
149 while (*p != '\0' && std::isspace(static_cast<unsigned char>(*p)) != 0) {
150 ++p;
151 }
152 return p;
153}
154
155inline DriverError Tokenize(const char* line, char* store, std::size_t store_cap,
156 const char* tokens[], std::size_t max_tokens,
157 std::size_t* out_count) noexcept {
158 *out_count = 0;
159 const char* p = SkipWs(line);
160 if (*p == '\0') {
162 }
163 std::size_t bi = 0;
164 while (*p != '\0' && *out_count < max_tokens) {
165 if (bi + 1U >= store_cap) {
167 }
168 const std::size_t start = bi;
169 tokens[*out_count] = store + start;
170 while (*p != '\0' && std::isspace(static_cast<unsigned char>(*p)) == 0) {
171 if (bi + 1U >= store_cap) {
173 }
174 store[bi++] = *p++;
175 }
176 if (bi + 1U >= store_cap) {
178 }
179 store[bi++] = '\0';
180 ++(*out_count);
181 p = SkipWs(p);
182 }
183 if (*p != '\0') {
185 }
186 return DriverError::None;
187}
188
190inline bool IsDecimalToken(const char* s, bool allow_minus) noexcept {
191 if (s == nullptr) {
192 return false;
193 }
194 if (allow_minus && *s == '-') {
195 ++s;
196 }
197 if (*s == '\0') {
198 return false;
199 }
200 for (; *s != '\0'; ++s) {
201 if (std::isdigit(static_cast<unsigned char>(*s)) == 0) {
202 return false;
203 }
204 }
205 return true;
206}
207
209inline int32_t ParseI32(const char* s, bool* ok) noexcept {
210 bool valid = IsDecimalToken(s, true);
211 long long v = 0;
212 if (valid) {
213 errno = 0;
214 v = std::strtoll(s, nullptr, 10);
215 valid = (errno == 0) && v >= -2147483648LL && v <= 2147483647LL;
216 }
217 if (ok != nullptr) {
218 *ok = valid;
219 }
220 return valid ? static_cast<int32_t>(v) : 0;
221}
222
224inline uint32_t ParseU32(const char* s, bool* ok) noexcept {
225 bool valid = IsDecimalToken(s, false);
226 unsigned long long v = 0;
227 if (valid) {
228 errno = 0;
229 v = std::strtoull(s, nullptr, 10);
230 valid = (errno == 0) && v <= 0xFFFFFFFFULL;
231 }
232 if (ok != nullptr) {
233 *ok = valid;
234 }
235 return valid ? static_cast<uint32_t>(v) : 0U;
236}
237
239inline uint64_t ParseU64(const char* s, bool* ok) noexcept {
240 bool valid = IsDecimalToken(s, false);
241 unsigned long long v = 0;
242 if (valid) {
243 errno = 0;
244 v = std::strtoull(s, nullptr, 10);
245 valid = (errno == 0);
246 }
247 if (ok != nullptr) {
248 *ok = valid;
249 }
250 return valid ? static_cast<uint64_t>(v) : 0U;
251}
252
254inline int32_t FieldI32(const char* s, bool& all_ok) noexcept {
255 bool ok = false;
256 const int32_t v = ParseI32(s, &ok);
257 all_ok = all_ok && ok;
258 return v;
259}
260
262inline uint32_t FieldU32(const char* s, bool& all_ok) noexcept {
263 bool ok = false;
264 const uint32_t v = ParseU32(s, &ok);
265 all_ok = all_ok && ok;
266 return v;
267}
268
269inline bool IsErroHeader(const char* tok0) noexcept {
270 return std::strcmp(tok0, "#ERRO") == 0;
271}
272
273} // namespace detail
274
279template <typename UartT>
280class Driver {
281public:
282 explicit Driver(UartT& uart) noexcept : uart_(uart) {}
283
284 void SetLineTimeoutMs(uint32_t ms) noexcept { line_timeout_ms_ = ms; }
285 void SetMeasureTimeoutMs(uint32_t ms) noexcept { measure_timeout_ms_ = ms; }
293 void SetSlowCommandTimeoutMs(uint32_t ms) noexcept { slow_timeout_ms_ = ms; }
294 uint32_t GetSlowCommandTimeoutMs() const noexcept { return slow_timeout_ms_; }
295
302 void SetCrcRequired(bool required) noexcept { crc_required_ = required; }
303 bool IsCrcRequired() const noexcept { return crc_required_; }
304
305 uint32_t GetLineTimeoutMs() const noexcept { return line_timeout_ms_; }
306 uint32_t GetMeasureTimeoutMs() const noexcept { return measure_timeout_ms_; }
307 int32_t LastDeviceErrorCode() const noexcept { return last_device_error_; }
308
310 last_device_error_ = 0;
311 char tx[16];
312 if (!detail::AppendCmd(tx, sizeof(tx), "#VERS")) {
314 }
315 char line[160];
316 const auto err = TransactLine(tx, line, sizeof(line));
317 if (err != DriverError::None) {
319 }
320 return ParseVersionLine(line, &last_device_error_);
321 }
322
324 last_device_error_ = 0;
325 char tx[16];
326 if (!detail::AppendCmd(tx, sizeof(tx), "#IDNR")) {
328 }
329 char line[96];
330 const auto err = TransactLine(tx, line, sizeof(line));
331 if (err != DriverError::None) {
333 }
334 char tstore[128];
335 const char* tok[8];
336 std::size_t nt = 0;
337 if (detail::Tokenize(line, tstore, sizeof(tstore), tok, 8, &nt) != DriverError::None) {
339 }
340 if (nt >= 2 && detail::IsErroHeader(tok[0])) {
341 return FailErro<uint64_t>(tok, nt);
342 }
343 if (nt != 2U || std::strcmp(tok[0], "#IDNR") != 0) {
345 }
346 bool ok = false;
347 const uint64_t id = detail::ParseU64(tok[1], &ok);
348 if (!ok) {
350 }
352 }
353
355 DriverResult<MoxyReading> MeasureMoxy(uint32_t timeout_ms = 0U) noexcept {
356 last_device_error_ = 0;
357 char tx[12];
358 if (!detail::AppendCmd(tx, sizeof(tx), "#MOXY")) {
360 }
361 char line[96];
362 const uint32_t tmo = (timeout_ms != 0U) ? timeout_ms : measure_timeout_ms_;
363 const auto err = TransactLine(tx, line, sizeof(line), tmo);
364 if (err != DriverError::None) {
366 }
367 return ParseMoxyLine(line, &last_device_error_);
368 }
369
371 DriverResult<MrawReading> MeasureMraw(uint32_t timeout_ms = 0U) noexcept {
372 last_device_error_ = 0;
373 char tx[12];
374 if (!detail::AppendCmd(tx, sizeof(tx), "#MRAW")) {
376 }
377 char line[192];
378 const uint32_t tmo = (timeout_ms != 0U) ? timeout_ms : measure_timeout_ms_;
379 const auto err = TransactLine(tx, line, sizeof(line), tmo);
380 if (err != DriverError::None) {
382 }
383 return ParseMrawLine(line, &last_device_error_);
384 }
385
397 enum class Measure : uint8_t { Moxy, Mraw, Version };
398
401 last_device_error_ = 0;
402 char tx[12];
403 const char* cmd = (m == Measure::Mraw) ? "#MRAW" : (m == Measure::Version) ? "#VERS" : "#MOXY";
404 if (!detail::AppendCmd(tx, sizeof(tx), cmd)) {
406 }
407 uart_.flush_rx();
408 uart_.write(reinterpret_cast<const uint8_t*>(tx), std::strlen(tx));
409 return DriverError::None;
410 }
411
416 DriverError TryReadReplyLine(char* line, std::size_t line_cap) noexcept {
417 return detail::ReadAsciiLine(uart_, line, line_cap, 0U, crc_required_);
418 }
419
422 int32_t* device_error) noexcept {
423 char tstore[128];
424 const char* tok[8];
425 std::size_t nt = 0;
426 if (detail::Tokenize(line, tstore, sizeof(tstore), tok, 8, &nt) != DriverError::None) {
428 }
429 if (nt >= 2 && detail::IsErroHeader(tok[0])) {
430 return FailErroStatic<VersionInfo>(tok, nt, device_error);
431 }
432 if (nt != 5U || std::strcmp(tok[0], "#VERS") != 0) {
434 }
435 bool ok = true;
436 VersionInfo v{};
437 v.device_id = detail::FieldI32(tok[1], ok);
438 v.num_channels = detail::FieldI32(tok[2], ok);
439 v.firmware_revision = detail::FieldI32(tok[3], ok);
440 v.sensor_types = detail::FieldI32(tok[4], ok);
441 if (!ok) {
443 }
445 }
446
449 int32_t* device_error) noexcept {
450 char tstore[128];
451 const char* tok[8];
452 std::size_t nt = 0;
453 if (detail::Tokenize(line, tstore, sizeof(tstore), tok, 8, &nt) != DriverError::None) {
455 }
456 if (nt >= 2 && detail::IsErroHeader(tok[0])) {
457 return FailErroStatic<MoxyReading>(tok, nt, device_error);
458 }
459 if (nt != 4U || std::strcmp(tok[0], "#MOXY") != 0) {
461 }
462 bool ok = true;
463 const int32_t o = detail::FieldI32(tok[1], ok);
464 const int32_t t = detail::FieldI32(tok[2], ok);
465 const uint32_t s = detail::FieldU32(tok[3], ok);
466 if (!ok) {
468 }
470 }
471
474 int32_t* device_error) noexcept {
475 char tstore[256];
476 const char* tok[16];
477 std::size_t nt = 0;
478 if (detail::Tokenize(line, tstore, sizeof(tstore), tok, 16, &nt) != DriverError::None) {
480 }
481 if (nt >= 2 && detail::IsErroHeader(tok[0])) {
482 return FailErroStatic<MrawReading>(tok, nt, device_error);
483 }
484 if (nt != 9U || std::strcmp(tok[0], "#MRAW") != 0) {
486 }
487 bool ok = true;
488 const int32_t o = detail::FieldI32(tok[1], ok);
489 const int32_t t = detail::FieldI32(tok[2], ok);
490 const uint32_t s = detail::FieldU32(tok[3], ok);
491 const int32_t d = detail::FieldI32(tok[4], ok);
492 const int32_t i = detail::FieldI32(tok[5], ok);
493 const int32_t a = detail::FieldI32(tok[6], ok);
494 const int32_t p = detail::FieldI32(tok[7], ok);
495 const int32_t h = detail::FieldI32(tok[8], ok);
496 if (!ok) {
498 }
499 return DriverResult<MrawReading>::success(DecodeMraw(o, t, s, d, i, a, p, h));
500 }
506 last_device_error_ = 0;
507 return SimpleEchoCommand("#LOGO", 5, slow_timeout_ms_);
508 }
509
510private:
511 template <typename T>
512 static DriverResult<T> FailErroStatic(const char* tok[], std::size_t nt,
513 int32_t* device_error) noexcept {
514 int32_t code = -1;
515 if (nt >= 2) {
516 bool ok = false;
517 code = detail::ParseI32(tok[1], &ok);
518 if (!ok) {
519 code = -1;
520 }
521 }
522 if (device_error != nullptr) {
523 *device_error = code;
524 }
526 }
527
528 template <typename T>
529 DriverResult<T> FailErro(const char* tok[], std::size_t nt) noexcept {
530 if (nt >= 2) {
531 bool ok = false;
532 last_device_error_ = detail::ParseI32(tok[1], &ok);
533 if (!ok) {
534 last_device_error_ = -1;
535 }
536 } else {
537 last_device_error_ = -1;
538 }
540 }
541
542 DriverResult<void> SimpleEchoCommand(const char* cmd, std::size_t cmd_len,
543 uint32_t timeout_ms) noexcept {
544 char tx[16];
545 if (cmd_len + 2U > sizeof(tx)) {
547 }
548 char line[32];
549 std::memcpy(tx, cmd, cmd_len);
550 tx[cmd_len] = '\r';
551 tx[cmd_len + 1] = '\0';
552 const auto err = TransactLine(tx, line, sizeof(line), timeout_ms);
553 if (err != DriverError::None) {
554 return DriverResult<void>::failure(err);
555 }
556 char tstore[48];
557 const char* tok[4];
558 std::size_t nt = 0;
559 if (detail::Tokenize(line, tstore, sizeof(tstore), tok, 4, &nt) != DriverError::None) {
561 }
562 if (nt >= 2 && detail::IsErroHeader(tok[0])) {
563 return FailErro<void>(tok, nt);
564 }
565 if (nt != 1U || std::strlen(tok[0]) != cmd_len || std::strncmp(tok[0], cmd, cmd_len) != 0) {
567 }
569 }
570
571 DriverError TransactLine(const char* tx, char* line, std::size_t line_cap,
572 uint32_t timeout_ms = 0U) noexcept {
573 uart_.flush_rx();
574 uart_.write(reinterpret_cast<const uint8_t*>(tx), std::strlen(tx));
575 const uint32_t tmo = (timeout_ms != 0U) ? timeout_ms : line_timeout_ms_;
576 return detail::ReadAsciiLine(uart_, line, line_cap, tmo, crc_required_);
577 }
578
579 UartT& uart_;
580 uint32_t line_timeout_ms_{400};
581 uint32_t measure_timeout_ms_{250};
582 uint32_t slow_timeout_ms_{12000};
583 int32_t last_device_error_{0};
584 bool crc_required_{false};
585};
586
587} // namespace fdo2
FDO2-G2 UART client.
Definition fdo2_driver.hpp:280
DriverError TryReadReplyLine(char *line, std::size_t line_cap) noexcept
Take one complete reply line if the transport already has it.
Definition fdo2_driver.hpp:416
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:355
DriverResult< uint64_t > ReadUniqueId() noexcept
Definition fdo2_driver.hpp:323
Driver(UartT &uart) noexcept
Definition fdo2_driver.hpp:282
uint32_t GetSlowCommandTimeoutMs() const noexcept
Definition fdo2_driver.hpp:294
uint32_t GetMeasureTimeoutMs() const noexcept
Definition fdo2_driver.hpp:306
void SetLineTimeoutMs(uint32_t ms) noexcept
Definition fdo2_driver.hpp:284
static DriverResult< VersionInfo > ParseVersionLine(const char *line, int32_t *device_error) noexcept
Decode a #VERS reply line. device_error receives the #ERRO code (may be null).
Definition fdo2_driver.hpp:421
Measure
Which command SendMeasure starts (Version = the #VERS identity handshake).
Definition fdo2_driver.hpp:397
DriverResult< VersionInfo > ReadVersion() noexcept
Definition fdo2_driver.hpp:309
DriverError SendMeasure(Measure m) noexcept
Drop stale input, then send #MOXY / #MRAW. Does not wait for the reply.
Definition fdo2_driver.hpp:400
DriverResult< void > FlashLogo() noexcept
Definition fdo2_driver.hpp:505
void SetCrcRequired(bool required) noexcept
Require the #CRCE 1 checksum on every reply (data sheet §4.2).
Definition fdo2_driver.hpp:302
int32_t LastDeviceErrorCode() const noexcept
Definition fdo2_driver.hpp:307
bool IsCrcRequired() const noexcept
Definition fdo2_driver.hpp:303
static DriverResult< MoxyReading > ParseMoxyLine(const char *line, int32_t *device_error) noexcept
Decode a #MOXY reply line. device_error receives the #ERRO code (may be null).
Definition fdo2_driver.hpp:448
void SetSlowCommandTimeoutMs(uint32_t ms) noexcept
Timeout for commands the module answers only after a long task (default 12000 ms).
Definition fdo2_driver.hpp:293
void SetMeasureTimeoutMs(uint32_t ms) noexcept
Definition fdo2_driver.hpp:285
uint32_t GetLineTimeoutMs() const noexcept
Definition fdo2_driver.hpp:305
static DriverResult< MrawReading > ParseMrawLine(const char *line, int32_t *device_error) noexcept
Decode a #MRAW reply line. device_error receives the #ERRO code (may be null).
Definition fdo2_driver.hpp:473
DriverResult< MrawReading > MeasureMraw(uint32_t timeout_ms=0U) noexcept
Same measurement plus raw optics / vent-path pressure / internal RH.
Definition fdo2_driver.hpp:371
FDO2-G2 UART types, scaling, and status decoding (PyroScience data sheet v5).
CRTP byte transport for the PyroScience Unified Protocol (PSUP).
uint16_t Crc16Modbus(const char *data, std::size_t len) noexcept
CRC-16/MODBUS (CRC-16-IBM, reflected polynomial 0xA001, initial value 0xFFFF, no final XOR).
Definition fdo2_driver.hpp:47
DriverError VerifyAndStripCrc(char *line, bool require) noexcept
Verify and remove the optional CRC suffix of a reply line (CR already removed).
Definition fdo2_driver.hpp:72
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:155
const char * SkipWs(const char *p) noexcept
Definition fdo2_driver.hpp:148
bool IsErroHeader(const char *tok0) noexcept
Definition fdo2_driver.hpp:269
int32_t FieldI32(const char *s, bool &all_ok) noexcept
Parse one field of a reply; all_ok is cleared if it is invalid and never set back to true.
Definition fdo2_driver.hpp:254
int32_t ParseI32(const char *s, bool *ok) noexcept
Parse a signed 32-bit decimal (data sheet §4.1 value range). ok is set (not accumulated).
Definition fdo2_driver.hpp:209
uint32_t FieldU32(const char *s, bool &all_ok) noexcept
Parse one field of a reply; all_ok is cleared if it is invalid and never set back to true.
Definition fdo2_driver.hpp:262
DriverError ReadAsciiLine(UartT &uart, char *buf, std::size_t cap, uint32_t timeout_ms, bool require_crc=false) noexcept
Definition fdo2_driver.hpp:113
void StripOptionalModbusCrcSuffix(char *line) noexcept
Definition fdo2_driver.hpp:103
bool AppendCmd(char *buf, std::size_t cap, std::string_view cmd) noexcept
Definition fdo2_driver.hpp:32
uint64_t ParseU64(const char *s, bool *ok) noexcept
Parse an unsigned 64-bit decimal (no sign; #IDNR). ok is set (not accumulated).
Definition fdo2_driver.hpp:239
uint32_t ParseU32(const char *s, bool *ok) noexcept
Parse an unsigned 32-bit decimal (no sign). ok is set (not accumulated).
Definition fdo2_driver.hpp:224
bool IsDecimalToken(const char *s, bool allow_minus) noexcept
True when s is an optional '-' followed by one or more decimal digits and nothing else.
Definition fdo2_driver.hpp:190
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:152
DriverError
Driver-level error codes (stable for logging / telemetry).
Definition fdo2_types.hpp:28
@ CrcMismatch
#CRCE 1 checksum suffix did not match the reply, or was required and missing.
MoxyReading DecodeMoxy(int32_t o_raw, int32_t t_raw, uint32_t s) noexcept
Definition fdo2_types.hpp:142
Definition fdo2_types.hpp:52
static constexpr DriverResult success(T v) noexcept
Definition fdo2_types.hpp:59
static constexpr DriverResult failure(DriverError e) noexcept
Definition fdo2_types.hpp:60
#VERS D N R S fields (FDO2-G2 data sheet §4.3).
Definition fdo2_types.hpp:78
int32_t device_id
D (8 for FDO2-G2).
Definition fdo2_types.hpp:79