32inline bool AppendCmd(
char* buf, std::size_t cap, std::string_view cmd)
noexcept {
33 if (cmd.size() + 2U > cap) {
36 std::memcpy(buf, cmd.data(), cmd.size());
37 buf[cmd.size()] =
'\r';
38 buf[cmd.size() + 1U] =
'\0';
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);
55 crc =
static_cast<uint16_t
>(crc >> 1U);
73 char* colon = std::strrchr(line,
':');
74 if (colon ==
nullptr) {
77 const char* p = colon + 1;
83 while (std::isdigit(
static_cast<unsigned char>(*p)) != 0) {
84 value = value * 10U +
static_cast<uint32_t
>(*p -
'0');
90 if (nd == 0U || *p !=
'\0' || value > 0xFFFFU) {
93 const std::size_t covered =
static_cast<std::size_t
>(colon - line);
94 if (
Crc16Modbus(line, covered) !=
static_cast<uint16_t
>(value)) {
102[[deprecated(
"use VerifyAndStripCrc(), which checks the checksum")]]
104 if (line !=
nullptr) {
105 char* colon = std::strrchr(line,
':');
106 if (colon !=
nullptr) {
112template <
typename UartT>
114 bool require_crc =
false) noexcept {
119 const std::size_t max_rx =
sizeof(tmp) - 1U;
120 const std::size_t n = uart.read(tmp, max_rx, timeout_ms);
125 bool terminated =
false;
126 for (std::size_t i = 0; i < n; ++i) {
127 const char c =
static_cast<char>(tmp[i]);
135 if (pos + 1 >= cap) {
148inline const char*
SkipWs(
const char* p)
noexcept {
149 while (*p !=
'\0' && std::isspace(
static_cast<unsigned char>(*p)) != 0) {
156 const char* tokens[], std::size_t max_tokens,
157 std::size_t* out_count)
noexcept {
159 const char* p =
SkipWs(line);
164 while (*p !=
'\0' && *out_count < max_tokens) {
165 if (bi + 1U >= store_cap) {
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) {
176 if (bi + 1U >= store_cap) {
194 if (allow_minus && *s ==
'-') {
200 for (; *s !=
'\0'; ++s) {
201 if (std::isdigit(
static_cast<unsigned char>(*s)) == 0) {
209inline int32_t
ParseI32(
const char* s,
bool* ok)
noexcept {
214 v = std::strtoll(s,
nullptr, 10);
215 valid = (errno == 0) && v >= -2147483648LL && v <= 2147483647LL;
220 return valid ?
static_cast<int32_t
>(v) : 0;
224inline uint32_t
ParseU32(
const char* s,
bool* ok)
noexcept {
226 unsigned long long v = 0;
229 v = std::strtoull(s,
nullptr, 10);
230 valid = (errno == 0) && v <= 0xFFFFFFFFULL;
235 return valid ?
static_cast<uint32_t
>(v) : 0U;
239inline uint64_t
ParseU64(
const char* s,
bool* ok)
noexcept {
241 unsigned long long v = 0;
244 v = std::strtoull(s,
nullptr, 10);
245 valid = (errno == 0);
250 return valid ?
static_cast<uint64_t
>(v) : 0U;
254inline int32_t
FieldI32(
const char* s,
bool& all_ok)
noexcept {
257 all_ok = all_ok && ok;
262inline uint32_t
FieldU32(
const char* s,
bool& all_ok)
noexcept {
264 const uint32_t v =
ParseU32(s, &ok);
265 all_ok = all_ok && ok;
270 return std::strcmp(tok0,
"#ERRO") == 0;
279template <
typename UartT>
282 explicit Driver(UartT& uart) noexcept : uart_(uart) {}
310 last_device_error_ = 0;
316 const auto err = TransactLine(tx, line,
sizeof(line));
324 last_device_error_ = 0;
330 const auto err = TransactLine(tx, line,
sizeof(line));
341 return FailErro<uint64_t>(tok, nt);
343 if (nt != 2U || std::strcmp(tok[0],
"#IDNR") != 0) {
356 last_device_error_ = 0;
362 const uint32_t tmo = (timeout_ms != 0U) ? timeout_ms : measure_timeout_ms_;
363 const auto err = TransactLine(tx, line,
sizeof(line), tmo);
372 last_device_error_ = 0;
378 const uint32_t tmo = (timeout_ms != 0U) ? timeout_ms : measure_timeout_ms_;
379 const auto err = TransactLine(tx, line,
sizeof(line), tmo);
401 last_device_error_ = 0;
408 uart_.write(
reinterpret_cast<const uint8_t*
>(tx), std::strlen(tx));
422 int32_t* device_error)
noexcept {
430 return FailErroStatic<VersionInfo>(tok, nt, device_error);
432 if (nt != 5U || std::strcmp(tok[0],
"#VERS") != 0) {
449 int32_t* device_error)
noexcept {
457 return FailErroStatic<MoxyReading>(tok, nt, device_error);
459 if (nt != 4U || std::strcmp(tok[0],
"#MOXY") != 0) {
474 int32_t* device_error)
noexcept {
482 return FailErroStatic<MrawReading>(tok, nt, device_error);
484 if (nt != 9U || std::strcmp(tok[0],
"#MRAW") != 0) {
506 last_device_error_ = 0;
507 return SimpleEchoCommand(
"#LOGO", 5, slow_timeout_ms_);
511 template <
typename T>
512 static DriverResult<T> FailErroStatic(
const char* tok[], std::size_t nt,
513 int32_t* device_error)
noexcept {
522 if (device_error !=
nullptr) {
523 *device_error = code;
528 template <
typename T>
529 DriverResult<T> FailErro(
const char* tok[], std::size_t nt)
noexcept {
534 last_device_error_ = -1;
537 last_device_error_ = -1;
542 DriverResult<void> SimpleEchoCommand(
const char* cmd, std::size_t cmd_len,
543 uint32_t timeout_ms)
noexcept {
545 if (cmd_len + 2U >
sizeof(tx)) {
549 std::memcpy(tx, cmd, cmd_len);
551 tx[cmd_len + 1] =
'\0';
552 const auto err = TransactLine(tx, line,
sizeof(line), timeout_ms);
563 return FailErro<void>(tok, nt);
565 if (nt != 1U || std::strlen(tok[0]) != cmd_len || std::strncmp(tok[0], cmd, cmd_len) != 0) {
571 DriverError TransactLine(
const char* tx,
char* line, std::size_t line_cap,
572 uint32_t timeout_ms = 0U)
noexcept {
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_;
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};
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
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