Skip to content

littlemanbuilds/SnapshotBus

Repository files navigation

SnapshotBus

Single-writer, multi-reader latest-state channel for RTOS/Arduino.
Publish a small POD struct from one task (or ESP32 ISR), and let any number of tasks read a consistent snapshot with minimal overhead.

Queues are for streams. SnapshotBus is for the latest state.


🆕 New in 1.2

SnapshotRTOS is now typed (breaking)

SnapshotRTOS in v1.1.x published std::array<float,N> channels and used a Policy{epsilon,min_interval_us}.

In v1.2.0, SnapshotRTOS publishes typed frames:

  • The Reader/callback fills a Frame (read(Frame&) or read(void*, Frame*)).
  • A Changed predicate decides publish vs. skip (payload-only; ignore stamp fields).
  • A simple heartbeat is available via PublishPolicy::min_interval_us.
  • Timestamping is injected via a now_us() callable (improves portability/testability).
  • Internal robustness improvements: SnapshotRTOS task state is now constructed in-place using the provided reader, time source, and change detector. This allows non-default-constructible callables (such as lambdas) to be used safely and avoids unnecessary copies.
  • SnapshotRTOS.h — tiny FreeRTOS publishers so you don’t write loop code:
    • snapshot::rtos::start_frame_publisher(...)
    • snapshot::rtos::start_frame_publisher_cb(...)
    • snapshot::rtos::start_frame_publisher_on_change(...) (semantic on-change; ignores stamps)
    • snapshot::rtos::start_frame_publisher_cb_on_change(...) (semantic on-change; ignores stamps)
    • PublishPolicy{min_interval_us} (optional heartbeat)
    • Changed(prev,next) predicate (payload-level change gating)

Removed (v1.1.x)

  • snapshot::rtos::Policy
  • start_publisher<N, Frame>(...)
  • start_publisher_cb<Frame>(...)
  • Built-in float epsilon gating (replace with a Changed predicate)

Added (v1.2.0)

  • snapshot::rtos::PublishPolicy
  • snapshot::rtos::AlwaysPublish (default change detector)
  • start_frame_publisher<Frame>(...)
  • start_frame_publisher_cb<Frame>(...)

Multiple concurrent publishers (fix)

SnapshotRTOS task state is heap-allocated so you can run multiple publishers at once without a hidden “one instance only” limitation.


🚀 Installation

Arduino IDE

  1. Search SnapshotBus in Library Manager and install.
  2. Or download a release ZIP → Sketch → Include Library → Add .ZIP Library…

PlatformIO
Add to your environment:

lib_deps = littlemanbuilds/SnapshotBus@^1.2.0

Manual
Copy src/ (headers only) into your project and include the headers you need.


✅ Platform/Feature Support

SnapshotBus has two layers of support:

  • Core layer: SnapshotBus.h, InputModel.h, SnapshotTools.h
  • RTOS layer: SnapshotRTOS.h (publisher task helpers)
Target family Core layer SnapshotRTOS layer
ESP32 (Arduino) Tested Tested
ESP8266 Tested Not supported (no FreeRTOS publisher tasks)
SAMD (MKR/Zero) Tested Not supported (no FreeRTOS publisher tasks)
RP2040 / Teensy / STM32 Not currently tested in this project (may compile; treat as experimental) Not supported unless FreeRTOS headers are present

Notes:

  • SnapshotRTOS.h requires FreeRTOS headers (freertos/FreeRTOS.h, freertos/task.h).
  • On non-FreeRTOS targets, calling SnapshotRTOS start functions intentionally fails at compile time with a clear message.
  • Some platforms are listed in package metadata for discoverability, but the table above reflects tested status.
  • ISR publishing is treated as ESP32-only in this project.
  • For non-ESP32 multicore targets, define SNAPSHOTBUS_USE_ATOMICS=1 if your toolchain/platform supports atomics safely.

🧭 When to use SnapshotBus

  • You have one writer (task, or ISR on ESP32) publishing a small struct.
  • Many readers need the latest consistent value, not every intermediate value.
  • You want zero allocation, header-only, and portable code.

Think: sensor fusion inputs, RC channel states, debounced switches, powertrain state, telemetry snapshots.


⚡ Quick Start (RTOS Example with Core Bus)

For non-RTOS usage, see examples/01_Basic_Polling/01_Basic_Polling.ino and examples/04_Model_Polling/04_Model_Polling.ino.

Example target scope:

  • examples/01_Basic_Polling/01_Basic_Polling.ino and examples/04_Model_Polling/04_Model_Polling.ino are core, non-RTOS examples.
  • examples/02_Polling_RTOS/02_Polling_RTOS.ino, examples/03_ISR_Publisher/03_ISR_Publisher.ino, examples/05_RTOS_Reader/05_RTOS_Reader.ino, and examples/06_RTOS_Callback/06_RTOS_Callback.ino are ESP32-focused RTOS examples.
  • examples/03_ISR_Publisher/03_ISR_Publisher.ino is explicitly ESP32-only in code (#error "This example requires ESP32.").
#include <Arduino.h>
#include <SnapshotBus.h>

// Payload must be trivially copyable (POD)
struct InputState {
  uint32_t buttons_mask;
  uint32_t stamp_ms;
};

snapshot::SnapshotBus<InputState> g_bus;

void publisherTask(void*) {
  pinMode(7, INPUT_PULLUP);
  for (;;) {
    InputState s{0, millis()};
    if (digitalRead(7) == LOW) s.buttons_mask |= (1u << 0);
    g_bus.publish(s);
    vTaskDelay(pdMS_TO_TICKS(10));
  }
}

void consumerTask(void*) {
  for (;;) {
    InputState s = g_bus.peek();   // stable snapshot
    if (s.buttons_mask & (1u << 0)) {
      // react to button press
    }
    vTaskDelay(pdMS_TO_TICKS(10));
  }
}

void setup() {
  xTaskCreate(publisherTask, "pub", 2048, nullptr, 1, nullptr);
  xTaskCreate(consumerTask,  "con", 2048, nullptr, 1, nullptr);
}
void loop() { vTaskDelete(nullptr); }

Why this works: SnapshotBus uses an even/odd sequence counter (seqlock‑style). Reads retry if a write is in flight, returning a consistent frame without locking.


🧩 InputModel: clean edges & masks (bitset helpers)

InputModel.h provides a simple State<N> to represent button levels and detect edges without writing bit‑twiddly code.

#include <InputModel.h>

using InputState = snapshot::input::State<8>; // 8 logical inputs

// Fill a snapshot (true == pressed)
InputState s{};
s.stamp_ms = millis();
s.set_button(0, true);

// Edges between two snapshots
InputState prev{}, cur{};
// ... fill cur ...
auto rising  = snapshot::input::rising_edges(prev, cur);
auto falling = snapshot::input::falling_edges(prev, cur);

// Iterate only changes
snapshot::input::for_each_edge(prev, cur,
  [](size_t i, bool pressed, uint32_t t_ms){
    // handle edge i at time t_ms
  });

SwitchBank interop: convert to/from masks.

uint32_t mask = cur.mask32<8>();          // N<=32
uint64_t mask64 = cur.mask64<8>();        // N<=64
cur.from_mask32<8>(mask);
cur.from_mask64<8>(mask64);

🔧 C++14 users: the header includes a no‑if constexpr fallback; no flags needed. C++17 still recommended.


🧵 SnapshotRTOS: stop writing publisher loops

SnapshotRTOS.h contains two helpers that build a FreeRTOS task for you and publish to a bus with change and/or heartbeat logic.

Reader API (tiny class with update/read/ok)

#include <SnapshotBus.h>
#include <SnapshotRTOS.h>
#include <InputModel.h>

#define BTN_PIN 4
#define NCH 1

struct InputFrame {
  float    out0{0.0f};
  uint64_t stamp_us{0};
  bool     failsafe{false};
};

snapshot::SnapshotBus<InputFrame> g_bus;

struct ButtonReader {
  void update() {}

  void read(InputFrame& f) {
    const bool pressed = (digitalRead(BTN_PIN) == LOW); // active-LOW
    f.out0 = pressed ? 1.0f : 0.0f;
  }

  bool ok() const { return true; }
};

static uint64_t now_us() noexcept {
  // Arduino-ESP32: micros() is fine for demo purposes.
  // SnapshotRTOS takes a time source so it stays portable/testable.
  return static_cast<uint64_t>(micros());
}

struct ButtonChanged {
  bool operator()(const InputFrame& a, const InputFrame& b) const noexcept {
    // Publish only on logical edges (equivalent to old epsilon=0.5 for 0/1 values).
    return (a.out0 > 0.5f) != (b.out0 > 0.5f);
  }
};

void setup() {
  pinMode(BTN_PIN, INPUT_PULLUP);

  snapshot::rtos::PublishPolicy pol{};
  pol.min_interval_us = 0; // optional heartbeat

  ButtonReader reader{};
  snapshot::rtos::start_frame_publisher<InputFrame>(
    g_bus,             ///< SnapshotBus to publish into.
    reader,            ///< Reader: samples hardware and fills InputFrame.
    now_us,            ///< Time source (µs).
    ButtonChanged{},   ///< Change detector: publish only on logical edges.
    pol,               ///< Publish policy (heartbeat / rate control).
    "BtnPub",         ///< FreeRTOS task name.
    1536,              ///< Stack size (words).
    1,                 ///< Task priority.
    5                  ///< Poll period in milliseconds.
  );
}

Time source (now_us)

The time source may be a function pointer, lambda, or functor returning a monotonically increasing timestamp in microseconds.

The callable is stored in task-local state and invoked without copying (via const&), so it does not need to be default-constructible. This allows lightweight lambdas or custom time providers to be used safely with FreeRTOS tasks.

Callback API (no class, just functions)

struct ButtonCtx { int pin; };

static void cb_update(void* /*ctx*/) { /* optional */ }

static void cb_read(void* ctx, InputFrame* out) {
  if (!out) return;
  const auto* c = static_cast<const ButtonCtx*>(ctx);
  out->out0 = (digitalRead(c->pin) == LOW) ? 1.0f : 0.0f;
}

static bool cb_ok(void* /*ctx*/) { return true; }

void setup() {
  static ButtonCtx ctx{4};
  pinMode(ctx.pin, INPUT_PULLUP);

  snapshot::rtos::PublishPolicy pol{};
  pol.min_interval_us = 0;

  snapshot::rtos::start_frame_publisher_cb<InputFrame>(
    g_bus,             ///< SnapshotBus to publish into.
    &ctx,              ///< Callback context.
    cb_update,         ///< update(): optional periodic work.
    cb_read,           ///< read(): fill payload fields.
    cb_ok,             ///< ok(): health check (nullptr => assumed OK).
    now_us,            ///< Time source (µs).
    ButtonChanged{},   ///< Change detector: publish only on logical edges.
    pol,               ///< Publish policy (heartbeat / rate control).
    "BtnPubCB",       ///< FreeRTOS task name.
    1536,              ///< Stack size (words).
    1,                 ///< Task priority.
    5                  ///< Poll period in milliseconds.
  );
}

Consuming frames (with InputModel edges)

using Bits = snapshot::input::State<NCH>;

void consumerTask(void*) {
  Bits prev{};
  for (;;) {
    const InputFrame f = g_bus.peek();
    Bits cur{};
    cur.stamp_ms = static_cast<uint32_t>(f.stamp_us / 1000ULL); ///< µs → ms.
    cur.set_button(0, f.out0 > 0.5f);

    snapshot::input::for_each_edge(prev, cur, [](size_t i, bool pressed, uint32_t t_ms){
      Serial.printf("[t=%lu ms] ch%u -> %s\n",
        (unsigned long)t_ms, (unsigned)i, pressed ? "Pressed" : "Released");
    });

    prev = cur;
    vTaskDelay(pdMS_TO_TICKS(20));
  }
}

🔎 SnapshotRTOS Publishing Semantics (important)

Stamp injection happens before change detection

SnapshotRTOS injects timestamp fields (e.g. stamp_us / stamp_ms / stamp_s) and failsafe before it calls your Changed(prev,next) predicate.

Therefore:

⚠️ Your change detector must ignore stamp fields or it will publish every cycle.

If you want a safe default, use the built-in semantic comparator wrappers:

  • snapshot::rtos::start_frame_publisher_on_change<Frame>(...)
  • snapshot::rtos::start_frame_publisher_cb_on_change<Frame>(...)

These use PublishOnChange<Frame> internally, which performs semantic equality (not byte-wise) and is designed to ignore injected stamps.

Always publish (control-critical)

For control loops or other “must publish every tick” cases:

snapshot::rtos::PublishPolicy pol{};
pol.min_interval_us = 0;

snapshot::rtos::start_frame_publisher<MyFrame>(
  bus, reader, now_us,
  snapshot::rtos::AlwaysPublish{},
  pol,
  "Task", 4096, 1, 10
);

If you want on-change behavior with a custom predicate, make sure it ignores stamps:

struct MyChanged {
  bool operator()(const MyFrame& a, const MyFrame& b) const noexcept {
    return a.value != b.value; // ignore stamps / failsafe
  }
};

🧵 StackWatch / Stack Watermark (recommended for RTOS tasks)

SnapshotRTOS publishers are long-lived FreeRTOS tasks. If a stack is undersized, failures can look like “random corruption”.

Recommended practice:

  • Measure stack usage using FreeRTOS high-water marks.
  • Track it consistently across your whole codebase (publishers, control tasks, orchestrators).

FreeRTOS built-in watermark

// Words remaining at the *minimum ever* watermark.
const UBaseType_t hw = uxTaskGetStackHighWaterMark(nullptr);

SnapshotRTOS task metadata registry (optional)

SnapshotRTOS provides a lightweight, built-in task metadata registry to retain configured task parameters that FreeRTOS does not expose at runtime (e.g. original stack size, priority, and core affinity).

Tasks may be registered via register_task() using their TaskHandle_t. This enables project-level diagnostics tools to compute consistent "used / allocated" stack metrics and display task configuration information without modifying application task creation code.

StackWatch integration

If your project already uses a StackWatch-style monitor, SnapshotRTOS publisher tasks can be registered and tracked in the same way as application tasks.

Alternatively, SnapshotRTOS provides its own optional TaskMeta registry (register_task(), find_task(), etc.) that can be used to implement lightweight stack and task diagnostics without external tooling.

// Example pattern (pseudo-API):
StackWatch::registerTask("BtnPub", /* TaskHandle_t */ handle);

Recommended wrapper pattern (consistency across the codebase)

If you already use a standard wrapper (e.g. PW_Publisher.h / pw::publisher::start), it’s a good idea to centralize:

  • task creation
  • core pinning defaults
  • stack sizing policy
  • StackWatch registration

This makes examples copy/paste-safe for your YouTube audience and keeps production builds consistent.


🔍 API Reference (high‑level)

snapshot::SnapshotBus<T>

  • void publish(const T& frame) noexcept — write new frame (single writer).
  • template<class F> void publish_inplace(F&& fill) noexcept — fill T& in place.
  • T peek() const noexcept — stable copy of latest frame.
  • void peek_into(T& out) const noexcept — stable snapshot into an existing object.
  • bool try_peek(T& out) const noexcept — bounded‑spin stable copy (false if not stable in limit).
  • bool try_peek_with_seq(T& out, uint32_t& seq_out) const noexcept — bounded‑spin stable copy plus the stable sequence used for that copy.
  • T peek_latest() const noexcept — zero‑spin best‑effort (may be mid‑write).
  • uint32_t sequence() const noexcept — current seq (even=stable, odd=writing).
  • uint32_t last_seq() const noexcept — last stable sequence captured by this instance.
  • bool was_updated_since(uint32_t seq) const noexcept — check if a newer stable frame exists.
  • template<class T> bool try_peek_new(const SnapshotBus<T>& bus, uint32_t& seen_seq, T& out) noexcept — latest-state helper; may advance seen_seq even if a stable capture is not possible in that call.
  • template<class T> bool try_peek_new_strict(const SnapshotBus<T>& bus, uint32_t& seen_seq, T& out) noexcept — strict helper; advances seen_seq only when a stable new frame is actually captured.

Constraints
T must be trivially copyable. One writer per bus. Multi-core use requires atomic mode (SNAPSHOTBUS_USE_ATOMICS=1).

snapshot::tools / snapshot::time

SnapshotTools is intentionally small: it provides a couple of “glue” helpers that show up a lot in real apps.

  • enum class UnstablePolicy { UseZero, UseLast }
  • peek_with_fallback(bus, out, last, has_last, unstable_reads, policy) — reads from a bus and, if a stable snapshot can’t be proven, applies a fallback policy:
    • UseZero: output a default-constructed frame
    • UseLast: output the last-known-good frame (if available), otherwise default
  • snapshot::time::ms_to_us_sat(ms) — converts ms→µs with saturation to 32-bit (helps keep microsecond fields stable on long uptimes).

snapshot::input

  • template<size_t N> struct State with:
    • buttons (bitset), stamp_ms (ms since boot).
    • set_button(id,bool), is_pressed(id), is_released(id), count(), mask32/64(), from_mask32/64().
  • Free functions: rising_edges, falling_edges, changed_edges, for_each_edge, to_mask32/64, to_bitset, assign_from_bits.

snapshot::rtos

  • struct PublishPolicy { uint32_t min_interval_us; }
  • struct AlwaysPublish (default Changed)
  • start_frame_publisher<Frame>(bus, reader, now_us, changed, pol, name, stack_words, prio, period_ms, core_id, out_handle)
  • start_frame_publisher_cb<Frame>(bus, ctx, update, read, ok, now_us, changed, pol, name, stack_words, prio, period_ms, core_id, out_handle)
  • start_frame_publisher_on_change<Frame>(bus, reader, now_us, pol, name, stack_words, prio, period_ms, core_id, out_handle)
  • start_frame_publisher_cb_on_change<Frame>(bus, ctx, update, read, ok, now_us, pol, name, stack_words, prio, period_ms, core_id, out_handle)

Reader contract
A Reader type provides:

  • void update()
  • void read(Frame& out)
  • bool ok() const (optional; if missing, the reader is assumed healthy)

Frame contract
Any trivially copyable Frame works. If these fields exist, SnapshotRTOS will populate them:

  • stamp_us and/or stamp_ms and/or stamp_s
  • failsafe (set to !ok())

⚙️ Configuration (macros)

Define these before including SnapshotBus.h to customize behavior:

  • SNAPSHOTBUS_SPIN_LIMIT — bound reader spin count (default 0 = unlimited, typical stable quickly).
  • SNAPSHOTBUS_ASSERT(x) — assert hook (defaults to configASSERT if present, else assert).
  • SNAPSHOTBUS_ASSERT_TASK_CONTEXT() — optional hook for non-atomic builds to assert publish paths are called from task/thread context.
  • SNAPSHOTBUS_PAUSE() — hint in spin loops (defaults to Xtensa nop on ESP).
  • SNAPSHOTBUS_YIELD() — reader spin yield hook (defaults to no‑op; uses taskYIELD() on FreeRTOS when available).
  • SNAPSHOTBUS_USE_ATOMICS — force atomic seqlock path (1) or interrupt-masked path (0). For non-ESP32 multicore targets, use 1 when supported by your toolchain.
  • SNAPSHOTBUS_MULTICORE — set to 1 when your app may run writer/reader across cores (requires SNAPSHOTBUS_USE_ATOMICS=1).

🧱 Design & Concurrency Model

  • Latest‑value channel (depth 1). Writers overwrite the previous frame.
  • Seqlock‑style: writer flips seq to odd, writes payload, release‑fences, flips seq to even.
    Readers load seq, copy payload, confirm seq unchanged & even.
  • Readers are fast; if a write collides, they retry.
    peek_latest() trades strict consistency for zero‑spin reads (best‑effort).

🧯 Pitfalls & Best Practices

  • Don’t put dynamic containers in T. Prefer plain arrays/fixed structs.
  • Keep frames small (a handful of scalars/arrays). Publish less often than you consume.
  • In this project, ISR publishing is ESP32-only. When used, prefer publish_inplace to minimize copies (and do no blocking work).
  • Gate noisy inputs in the publisher (Change predicate, heartbeat policy, or domain logic).

🔄 Versioning & Migration

1.2.0

SnapshotBus + InputModel behavior remains compatible with v1.1.x.

Breaking (SnapshotRTOS):

  • Replace Policy{epsilon,min_interval_us} with:
    • a Changed(prev,next) predicate for payload gating, and
    • PublishPolicy{min_interval_us} for heartbeat.
  • Replace:
    • start_publisher<N, Frame>(...)start_frame_publisher<Frame>(...)
    • start_publisher_cb<Frame>(...)start_frame_publisher_cb<Frame>(...)
  • Update Reader/callback read signature:
    • read(float* dst, size_t n)read(Frame& out)
    • read(void*, float*, size_t)read(void*, Frame*)
  • Provide a now_us() callable (microseconds).

1.1.0

Introduced snapshot::input (renamed from snapshot::model), SnapshotRTOS helpers, and the C++14 fallback in InputModel.h. Examples updated.

Breaking: If you previously used snapshot::model, rename to snapshot::input, and replace setSnapshotButton(...) with set_button(...) (method on State).


🧰 Toolchain Notes

  • C++17 recommended (best diagnostics, if constexpr where available), especially for larger projects.
  • SnapshotBus core is designed to be lightweight; InputModel.h supports C++14 without flags.
  • SnapshotRTOS in v1.2.0 avoids C++20-only features (no requires).

For Arduino/ESP32 + PlatformIO, if you want to force C++17:

build_unflags = -std=gnu++11 -std=gnu++14
build_flags   = -std=gnu++17

📜 License

MIT © Little Man Builds

About

Lock-free snapshot channel for Arduino/ESP32 — share the latest state between tasks or ISRs.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages