Skip to content

API guide

How to do the common jobs with the driver: identify the module, measure, read raw data, measure without blocking and handle errors.

Every snippet on this page is compiled by the host CI from tests/docs/api_tasks.cpp, with Sensor standing for fdo2::Driver<MyUart>. The API reference lists every member.

The shape of the API

One fdo2::Driver<UartT> object per sensor. Each command is one call that sends the command, waits for the reply and returns a DriverResult<T>.

Call Command Returns
ReadVersion() #VERS VersionInfo: device id, channels, firmware, sensors
ReadUniqueId() #IDNR uint64_t
MeasureMoxy(timeout_ms) #MOXY MoxyReading: pO2, temperature, status
MeasureMraw(timeout_ms) #MRAW MrawReading: the above plus dphi, signal, ambient light, pressure, humidity
FlashLogo() #LOGO DriverResult<void>
SendMeasure(), TryReadReplyLine(), ParseMoxyLine(), ParseMrawLine(), ParseVersionLine() any of the above The same exchange in two halves

A DriverResult<T> has value, error and ok(), and converts to bool. The driver never throws and never allocates.

Identify the module

const auto version = sensor.ReadVersion();           // #VERS D N R S
if (!version) {
  std::printf("#VERS failed: %s\n", fdo2::ToString(version.error).data());
  return false;
}
std::printf("device %" PRId32 ", firmware %.2f, sensors 0x%" PRIX32 "\n", version.value.device_id,
            fdo2::FirmwareRevisionToDouble(version.value.firmware_revision),
            static_cast<uint32_t>(version.value.sensor_types));  // 15: O2, T, P, RH

const auto id = sensor.ReadUniqueId();               // #IDNR N, unsigned 64-bit
if (id) {
  std::printf("unique id %" PRIu64 "\n", id.value);
}

Call it once after power-up (wait kFdo2G2PowerUpSettleMs first). Device id 8 confirms an FDO2-G2; the firmware revision tells you whether the noise erratum applies.

Measure oxygen

const auto m = sensor.MeasureMoxy();                 // #MOXY O T S
if (!m) {
  return false;                                      // Timeout, ProtocolError or DeviceError
}
if (!fdo2::MoxyStatusOkForOxygen(m.value.status_u32)) {
  return false;                                      // S > 1: O and T are or may be wrong
}
const double po2_hpa = m.value.p_o2_hpa;             // O x 10^-3
const double temp_c = m.value.temp_c;                // T x 10^-3

#MOXY is the shortest exchange: one measurement, three fields. A successful call means the module answered; the status check tells you whether the answer is a valid oxygen reading. A status of 1 (amplification reduced) is still valid.

Read raw data and volume percent

const auto r = sensor.MeasureMraw();                 // #MRAW O T S D I A P H
if (!r || !fdo2::MoxyStatusOkForOxygen(r.value.status_u32)) {
  return false;
}
// Valid when the gas at the membrane is at the pressure measured at the back of the housing.
const double percent_o2 = fdo2::VolumePercentO2(r.value.p_o2_hpa, r.value.pressure_mbar);

// Optics check: keep signal + ambient light under about 2000 mV (shade the sensor otherwise).
const bool too_bright = r.value.signal_intensity_mv + r.value.ambient_light_mv > 2000.0;

#MRAW takes the same measurement and adds the raw optical values and the pressure and humidity inside the housing. Use it for diagnostics, for volume percent, or at a slower rate next to fast #MOXY reads, as the fdo2_sensor_demo example does. UART protocol lists every field and its unit.

Measure without blocking

MeasureMoxy() blocks for the whole measurement, up to about 150 ms. A loop that serves other devices can split the exchange: send the command, keep working, and pick up the reply when the UART has a complete line.

// Start a measurement, do other work, collect the reply when the UART has it.
class OxygenPoller {
public:
  explicit OxygenPoller(Sensor& sensor) : sensor_(sensor) {}

  void start() { pending_ = sensor_.SendMeasure(Sensor::Measure::Moxy) == fdo2::DriverError::None; }

  // Call from your loop; true when a new reading was stored in `out`.
  bool poll(fdo2::MoxyReading& out) {
    char line[96];
    if (!pending_ || sensor_.TryReadReplyLine(line, sizeof(line)) != fdo2::DriverError::None) {
      return false;                                    // no complete line yet; you own the timeout
    }
    pending_ = false;
    int32_t device_error = 0;
    const auto m = Sensor::ParseMoxyLine(line, &device_error);
    if (!m || !fdo2::MoxyStatusOkForOxygen(m.value.status_u32)) {
      return false;
    }
    out = m.value;
    return true;
  }

private:
  Sensor& sensor_;
  bool pending_ = false;
};

TryReadReplyLine() calls your port's read() with a timeout of 0, so the port must return a whole line or nothing (Porting). You own the timeout: if no reply arrives within about 300 ms, send the command again.

Set timeouts

sensor.SetMeasureTimeoutMs(400);   // #MOXY / #MRAW: covers M = 3 or 4 (up to about 300 ms)
sensor.SetLineTimeoutMs(400);      // #VERS, #IDNR
sensor.SetSlowCommandTimeoutMs(2000);  // #LOGO: echoed after the ~1 s flash sequence
const auto m = sensor.MeasureMoxy(1000);             // or per call, in ms
Timeout Default Used by
Measurement 250 ms MeasureMoxy(), MeasureMraw() when called with 0 (the default argument)
Line 400 ms ReadVersion(), ReadUniqueId()
Slow command 12000 ms FlashLogo(): the echo comes after the LED has flashed, about 1 s

The defaults fit the factory setting (M = 2, up to about 150 ms per measurement). A module set to M = 3 or 4 takes up to about 300 ms: raise the measurement timeout above that.

Handle errors and status

const auto m = sensor.MeasureMoxy();
switch (m.error) {
  case fdo2::DriverError::None:
    break;
  case fdo2::DriverError::Timeout:                   // no reply: wiring, baud, power, still booting
  case fdo2::DriverError::ProtocolError:             // reply did not parse: noise, wrong baud, broadcast on
  case fdo2::DriverError::CrcMismatch:               // #CRCE 1 checksum wrong: corrupted in transit
    sensor.MeasureMoxy();                            // the data sheet's advice: send the command again
    break;
  case fdo2::DriverError::DeviceError:               // #ERRO E
    std::printf("#ERRO %" PRId32 "\n", sensor.LastDeviceErrorCode());  // -21, -22, -23: repeat; -26: unknown
    break;
  default:
    std::printf("%s\n", fdo2::ToString(m.error).data());
    break;
}

if (m && (m.value.status_u32 & fdo2::moxy_status::kWarnHumidityHigh) != 0U) {
  std::printf("humidity in the housing above 90 %%RH\n");
}
Signal Meaning What to do
DriverError::Timeout No reply within the timeout Check wiring, baud rate, supply; wait 1 s after power-up
DriverError::ProtocolError A reply that did not parse: wrong header or field count, a value that is not a number in range, no CR at the end Repeat the command; check the baud rate and broadcast mode
DriverError::CrcMismatch With #CRCE 1: the reply's checksum is wrong, or missing while SetCrcRequired(true) Repeat the command; see optional CRC
DriverError::DeviceError #ERRO E LastDeviceErrorCode(); error codes
DriverError::BufferOverflow A reply longer than the line buffer Noise on RX, or broadcast replies running together
MoxyStatusOkForOxygen(S) is false Status bits other than bit 0 set Do not use O and T; see the status bits

LastDeviceErrorCode() is reset to 0 at the start of every command, so read it right after the call that failed.

Identify a sensor on a port

return sensor.FlashLogo().ok();    // status LED flashes 4 times, about 1 s: which sensor is on this port

With several modules on several UARTs, FlashLogo() shows which connector belongs to which port. The module answers only after the flash sequence, so the call blocks for about 1 s, up to the slow-command timeout.

Next

Configuration lists every setting and default; Troubleshooting maps symptoms to causes.