Skip to content

File system.hpp

File List > espos_sensors > include > espos_sensors > system.hpp

Go to the documentation of this file

// SPDX-FileCopyrightText: 2026 Dirk Wahrheit
// SPDX-License-Identifier: Apache-2.0
//
// espos::sensors::system — the device reporting on itself.
//
// espos_sk ALREADY publishes the standard set (uptime, free heap, RSSI, …)
// under `espos.<label>.*` on its own schedule. These nodes are for the case
// that set does not cover: putting one of those numbers on a path of YOUR
// choosing, feeding it through a transform, or alarming on it in the graph
// rather than in espos_health.
//
// Do not wire these to `espos.<label>.freeHeap` -- that path is already
// published, and two writers on one path is a fight nobody wins.
//
//   FreeHeap heap("heap", 10000);
//   heap >> below_20k >> notify;    // a graph-side alarm, not a duplicate
//   publish
#pragma once

#include <cstdint>
#include <string>

#include "esp_heap_caps.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "espos_flow/flow.hpp"

namespace espos::sensors::system {

// Seconds since boot. int32_t rather than float: at 1 Hz a float loses
// integer precision after ~97 days of uptime, which is exactly the uptime
// worth reporting.
inline int32_t uptime_s() {
  return static_cast<int32_t>(esp_timer_get_time() / 1000000);
}

// Free heap in bytes, all capabilities.
inline int32_t free_heap() {
  return static_cast<int32_t>(esp_get_free_heap_size());
}

// Free INTERNAL heap. On a chip with PSRAM this is the number that matters:
// ~30 MB of free SPIRAM hides an internal pool that is nearly gone, and it is
// internal RAM that a WiFi buffer or a task stack has to come from. A device
// that has been up for days and stops accepting connections is almost always
// this, not total heap.
inline int32_t free_internal() {
  return static_cast<int32_t>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
}

// The largest single block of internal RAM. Fragmentation shows up here
// first: plenty free, none of it contiguous, and the next allocation fails.
inline int32_t largest_internal_block() {
  return static_cast<int32_t>(
      heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL));
}

// Why the device last restarted, as the short string a human reads in a
// dashboard. A panic or a task watchdog here is the first thing to look at
// after a device "just rebooted".
inline const char* reset_reason() {
  switch (esp_reset_reason()) {
    case ESP_RST_POWERON:
      return "poweron";
    case ESP_RST_EXT:
      return "external";
    case ESP_RST_SW:
      return "software";
    case ESP_RST_PANIC:
      return "panic";
    case ESP_RST_INT_WDT:
      return "interruptWatchdog";
    case ESP_RST_TASK_WDT:
      return "taskWatchdog";
    case ESP_RST_WDT:
      return "otherWatchdog";
    case ESP_RST_DEEPSLEEP:
      return "deepSleep";
    case ESP_RST_BROWNOUT:
      return "brownout";
    case ESP_RST_SDIO:
      return "sdio";
    default:
      return "unknown";
  }
}

// The nodes. Each is a Poll over the function above, so they cost a timer
// and nothing else. The function pointer is the template argument, which is
// why these are `using` rather than classes: no std::function, no capture.
using Uptime = espos::flow::Poll<int32_t, int32_t (*)()>;
using FreeHeap = espos::flow::Poll<int32_t, int32_t (*)()>;
using InternalFreeHeap = espos::flow::Poll<int32_t, int32_t (*)()>;
using LargestInternalBlock = espos::flow::Poll<int32_t, int32_t (*)()>;

// Convenience makers, so the wiring reads as what it is:
//
//   auto& up = make_uptime(g, "uptime", 10000);
template <typename Graph>
Uptime& make_uptime(Graph& g, const char* id, uint32_t period_ms) {
  return g.template make<Uptime>(id, period_ms, &uptime_s);
}
template <typename Graph>
FreeHeap& make_free_heap(Graph& g, const char* id, uint32_t period_ms) {
  return g.template make<FreeHeap>(id, period_ms, &free_heap);
}
template <typename Graph>
InternalFreeHeap& make_internal_free_heap(Graph& g, const char* id,
                                          uint32_t period_ms) {
  return g.template make<InternalFreeHeap>(id, period_ms, &free_internal);
}

// ResetReason is emitted ONCE, at start: it cannot change while the device
// is running, and a device that republishes it every minute is describing a
// reboot that happened days ago as if it were news.
class ResetReason : public espos::flow::NodeBase,
                    public espos::flow::Producer<std::string> {
 public:
  explicit ResetReason(const char* id) : NodeBase(id) {}

  // Call from the wiring function, after the graph is connected.
  void emit_now() { this->emit(std::string(reset_reason())); }

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

}  // namespace espos::sensors::system