Skip to content

File onewire.hpp

File List > espos_sensors > include > espos_sensors > onewire.hpp

Go to the documentation of this file

// SPDX-FileCopyrightText: 2026 Dirk Wahrheit
// SPDX-License-Identifier: Apache-2.0
//
// espos::sensors — DS18B20 temperatures as graph nodes.
//
// Only compiled when CONFIG_ESPOS_SENSORS_ONEWIRE is on, which is also what
// makes espressif/ds18b20 a dependency of the firmware. See docs/sensors.md.
//
// The shape here is trigger-then-read, and it is the interesting part: a
// 12-bit conversion takes 750 ms, so a node that converted and read in one
// call would block the flow task -- and with it every other sensor in the
// firmware -- for three quarters of a second. Instead one timer broadcasts a
// conversion to every sensor on the bus at once, and a second timer, one
// conversion-time later, reads them. Eight sensors then cost ONE conversion
// window rather than eight.
#pragma once

#include "sdkconfig.h"

#if CONFIG_ESPOS_SENSORS_ONEWIRE

#include <cstdint>

#include "espos_flow/flow.hpp"
#include "espos_onewire.h"

namespace espos::sensors {

// One DS18B20, emitting KELVIN -- the Signal K unit, so nothing downstream
// has to convert and no path ever carries celsius by accident.
//
// Owned by a OneWireBus, which does the conversion timing for the whole bus.
class Ds18b20 : public espos::flow::NodeBase,
                public espos::flow::Producer<float> {
 public:
  // `addr` is the sensor's 64-bit ROM id, which is what a sensor IS. Pass 0
  // only when the bus has exactly one sensor: an index would renumber every
  // sensor after one that failed to answer, and the engine-room reading
  // would quietly become the fridge.
  Ds18b20(const char* id, espos_onewire_bus_handle_t bus, uint64_t addr)
      : NodeBase(id) {
    open_err_ = espos_ds18b20_open(bus, addr, &dev_);
  }

  esp_err_t set_resolution(int bits) {
    return dev_ ? espos_ds18b20_set_resolution(dev_, bits) : open_err_;
  }

  // Read what the last conversion produced and emit it. A disconnected
  // sensor reads exactly 85 C (the chip's power-on value) and emits NOTHING
  // rather than a plausible number -- a steady 85 C on an engine path is the
  // kind of fault that gets believed until something melts.
  void read_and_emit() {
    if (!dev_) return;
    float k = 0.0f;
    if (espos_ds18b20_read_kelvin(dev_, &k) != ESP_OK) {
      errors_++;
      return;
    }
    this->emit(k);
  }

  uint64_t address() const { return espos_ds18b20_address(dev_); }
  uint32_t errors() const { return errors_; }
  esp_err_t open_error() const { return open_err_; }

 protected:
  const char* node_id_for_error() const override { return id(); }

 private:
  espos_ds18b20_handle_t dev_ = nullptr;
  esp_err_t open_err_ = ESP_OK;
  uint32_t errors_ = 0;
};

// The bus, and the conversion clock for every sensor on it.
//
// `Max` is the number of sensors this bus can carry; the array is inline, so
// a bus sized for two costs two pointers.
template <std::size_t Max = 8>
class OneWireBus {
 public:
  // GPIO needs an external 4.7k pull-up to 3V3. The internal pull-up cannot
  // supply a DS18B20 and the bus simply reads as empty -- the single most
  // common "my sensors do not appear".
  explicit OneWireBus(int gpio) { open_err_ = espos_onewire_open(gpio, &bus_); }

  espos_onewire_bus_handle_t handle() const { return bus_; }
  esp_err_t open_error() const { return open_err_; }

  // Every sensor found, most useful at boot: log the addresses once and put
  // them in the configuration.
  esp_err_t search(uint64_t* addrs, std::size_t max, std::size_t* out_n) {
    return espos_onewire_search(bus_, addrs, max, out_n);
  }

  // Register a node so the bus converts and reads it.
  esp_err_t add(Ds18b20& sensor) {
    if (n_ >= Max) return ESP_ERR_NO_MEM;
    sensors_[n_++] = &sensor;
    return ESP_OK;
  }

  // Start the cycle: convert every `period_ms`, read `convert_ms` later.
  // period_ms must be longer than the conversion time or the reads never
  // catch up; 2000 ms at 12 bits is comfortable.
  esp_err_t start(uint32_t period_ms, int bits = 12) {
    if (open_err_ != ESP_OK) return open_err_;
    convert_ms_ = espos_ds18b20_convert_ms(bits);
    if (period_ms <= convert_ms_) return ESP_ERR_INVALID_ARG;
    for (std::size_t i = 0; i < n_; i++) sensors_[i]->set_resolution(bits);
    return espos_flow_every(period_ms, &OneWireBus::convert_tick, this,
                            &timer_);
  }

 private:
  // Broadcast a conversion to the whole bus, then arm a one-shot to read.
  // The read cannot happen here: it is 750 ms away, and the flow task has
  // other sensors to serve in the meantime.
  static void convert_tick(void* self) {
    OneWireBus* b = static_cast<OneWireBus*>(self);
    if (espos_onewire_convert_all(b->bus_) != ESP_OK) return;
    espos_flow_timer_t t;
    espos_flow_after(b->convert_ms_, &OneWireBus::read_tick, b, &t);
  }

  static void read_tick(void* self) {
    OneWireBus* b = static_cast<OneWireBus*>(self);
    for (std::size_t i = 0; i < b->n_; i++) b->sensors_[i]->read_and_emit();
  }

  espos_onewire_bus_handle_t bus_ = nullptr;
  esp_err_t open_err_ = ESP_OK;
  Ds18b20* sensors_[Max] = {};
  std::size_t n_ = 0;
  uint32_t convert_ms_ = 760;
  espos_flow_timer_t timer_ = ESPOS_FLOW_TIMER_NONE;
};

}  // namespace espos::sensors

#endif  // CONFIG_ESPOS_SENSORS_ONEWIRE