Porting¶
Run the driver on a new MCU by writing one class: the UART interface.
The driver never calls your SDK directly. It calls three methods (and an optional fourth) on a class you derive
from fdo2::UartInterface<Self> (in inc/fdo2_uart_interface.hpp). The base
class uses CRTP: the calls are resolved at compile time, so there are no virtual functions, no vtable and nothing
to allocate.
Figure description and sources
From top to bottom. Your application talks to one fdo2::Driver
Drawn from: inc/fdo2_driver.hpp Driver public calls; inc/fdo2_uart_interface.hpp UartInterface; figure module docs/diagrams/figures/driver-layers.py.
What you implement¶
| Method | Contract |
|---|---|
write(data, length) |
Send length bytes: one command, CR included. Return when they are queued or sent |
read(out, max, timeout_ms) |
Return one complete reply line, CR included, as a byte count; 0 if no whole line arrived within timeout_ms. With timeout_ms = 0, return only a line that is already complete |
flush_rx() |
Drop everything received so far, including a partial line |
delay_ms_impl(ms) |
Optional. Block for at least ms; UartInterface::delay_ms() calls it, and does nothing without it |
The driver calls flush_rx() and then write() at the start of every command, and read() once per reply.
UART port¶
class MyUart : public fdo2::UartInterface<MyUart> {
public:
// Send one command line (the driver adds the CR).
void write(const uint8_t* data, std::size_t length) noexcept { platform_uart_write(data, length); }
// Return one complete reply line, CR included, or 0 if none arrived within timeout_ms.
// timeout_ms == 0 means "only if a whole line is already here".
std::size_t read(uint8_t* out, std::size_t max, uint32_t timeout_ms) noexcept {
const uint32_t start = platform_millis();
for (;;) {
uint8_t byte = 0;
while (platform_uart_read_byte(byte)) {
if (len_ < sizeof(line_)) {
line_[len_++] = byte;
}
if (byte == '\r') { // the module ends every reply with one CR
const std::size_t n = (len_ < max) ? len_ : max;
std::memcpy(out, line_, n);
len_ = 0;
return n;
}
}
if (platform_millis() - start >= timeout_ms) {
return 0; // keep a partial line for the next call
}
platform_delay_ms(1);
}
}
void flush_rx() noexcept {
platform_uart_flush();
len_ = 0;
}
// Optional: lets the driver and your code wait through UartInterface::delay_ms().
void delay_ms_impl(uint32_t ms) noexcept { platform_delay_ms(ms); }
private:
uint8_t line_[192]{}; // longest reply: #MRAW, under 120 characters
std::size_t len_ = 0;
};
Rules the port must follow:
- 19200 baud, 8N1, no flow control. That is the module's setting after every power-up.
- Return at the CR. The driver parses what one
read()returns as the whole reply. A port that returns part of a line hands the driver a short reply (ProtocolError) and leaves the rest for the next command. - Return as soon as the line is complete. A port that waits for
maxbytes or for the full timeout (a plainuart_read_bytes(port, buf, max, timeout)in ESP-IDF does this) still works, but every call then takes the whole timeout. - Keep a partial line on timeout. The split exchange polls with
timeout_ms= 0 while a reply is arriving; the port above keeps the bytes it has and returns the line on a later call. - Use a receive buffer of at least 128 bytes. The longest reply,
#MRAWwith a CRC suffix, is under 120 characters.
Reference implementation¶
The ESP32 examples use hf_fdo2_esp_uart.hpp: a template
Fdo2EspIdfUart<port, tx_gpio, rx_gpio> over the ESP-IDF UART driver, with a static Install() that configures
19200 baud 8N1 and the pins. Its read() follows the rules above: it reads one byte at a time with the time left
of the timeout, returns at the CR, and keeps a partial line for the next call. The host tests build it against a
model of the ESP-IDF UART calls (tests/esp_idf_stub/) and check that a command returns when the reply arrives.
Check your port¶
- Build it: the port on this page is compiled by the host CI from
tests/docs/, with the driver instantiated over it. - Wait 1 s after power-up and call
ReadVersion(): device id 8 proves both directions and the baud rate. - Call
MeasureMoxy()in air: about 210 hPa at sea level, status 0 or 1. - Time a
MeasureMoxy()call: about 150 ms plus transfer time means the port returns at the CR; the full timeout means it waits for more bytes than the reply has.
Threads¶
The driver is not thread-safe. Give each sensor one owner task, or guard every call on a driver object (and its port) with one mutex. Several sensors on separate UARTs can run in parallel.