File espos_flow.h¶
File List > espos_flow > include > espos_flow.h
Go to the documentation of this file
/*
* SPDX-FileCopyrightText: 2026 Dirk Wahrheit
* SPDX-License-Identifier: Apache-2.0
*
* espos_flow — one task, one clock, one mailbox: the runtime a data-flow graph
* runs on.
*
* This is the C half. It owns a single FreeRTOS task (the *flow loop*) that
* does exactly three things forever: fire whatever timers are due, drain
* whatever other tasks have posted, and sleep until the next of those. On top
* of it sits the typed graph in the espos_flow/ headers — producers, transforms and
* sinks wired with connect_to(). Everything the graph does happens on this
* task. Nothing else needs to exist for the C half to be useful: a firmware
* that only wants "call this every 500 ms, and let me hand work to that task
* from an ISR" can use espos_flow.h alone and never include a header.
*
* ── The threading contract ────────────────────────────────────────────────
*
* espOS today documents a different callback context per component: the
* writer's task for a config change, the stream task for a SignalK update,
* the esp_timer task for a health tick, the event loop task for ESPOS_EVENT
* (docs/concepts.md, "Which task calls you back"). Every one of them is a
* separate set of rules to remember and a separate chance to touch a variable
* from two tasks at once.
*
* espos_flow replaces all of that with ONE rule:
*
* Everything in a graph runs on the flow task. Every value that enters a
* graph from anywhere else enters through a Mailbox.
*
* So: a node's transform, a Poll's read function, a Sink's write, an emit()
* anywhere — all on the flow task, one at a time, never concurrently. No node
* needs a lock, because no node is ever re-entered. In exchange, nothing on
* that task may block: a callback that sleeps stops every timer in the
* firmware. Work that must block belongs on its own task, which posts its
* result back with espos_flow_post() (or Mailbox<T>::post()).
*
* An ISR, a driver callback, another component's callback on its own task:
* all of them use espos_flow_post_from_isr() / espos_flow_post(), which is
* the only supported way in. CONFIG_ESPOS_FLOW_CHECK_TASK (on by default in
* debug builds) turns a violation into an assert with the offending task's
* name rather than a corruption that shows up a week later.
*
* ── Cost when unused ──────────────────────────────────────────────────────
*
* The loop task is started by espos_flow_start(), which nothing calls for you.
* A firmware that does not REQUIRES espos_flow links nothing; one that does
* but never calls start pays the code size and no RAM beyond the static
* tables. The C++ graph starts the loop when a Graph is run, not when a node
* is constructed.
*
* Threading of the API itself: espos_flow_post(), _post_from_isr(),
* _now_ms() and _stats() are safe from any task. _every/_after/_cancel are
* safe from any task as well (they take the loop's lock), though the natural
* place to call them is the flow task itself. _start/_stop are for the
* application's start-up task, not for a callback.
*/
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/* A timer's identity. 0 is never a live timer, so a struct member zeroed at
* start-up reads as "nothing scheduled". */
typedef uint32_t espos_flow_timer_t;
#define ESPOS_FLOW_TIMER_NONE ((espos_flow_timer_t)0)
/* Runs on the flow task. Must not block. */
typedef void (*espos_flow_cb_t)(void *arg);
esp_err_t espos_flow_start(void);
esp_err_t espos_flow_stop(void);
bool espos_flow_is_running(void);
uint32_t espos_flow_now_ms(void);
esp_err_t espos_flow_every(uint32_t period_ms, espos_flow_cb_t cb, void *arg, espos_flow_timer_t *out);
esp_err_t espos_flow_after(uint32_t delay_ms, espos_flow_cb_t cb, void *arg, espos_flow_timer_t *out);
esp_err_t espos_flow_cancel(espos_flow_timer_t h);
esp_err_t espos_flow_post(espos_flow_cb_t cb, void *arg);
esp_err_t espos_flow_post_from_isr(espos_flow_cb_t cb, void *arg, bool *hp_task_woken);
bool espos_flow_run_until_idle(uint32_t timeout_ms);
/* ------------------------------------------------------------- inspection */
typedef struct {
uint32_t posts; /* accepted espos_flow_post/_from_isr calls */
uint32_t dropped; /* posts refused because the mailbox was full */
uint32_t timers_fired; /* timer callbacks run since start */
uint32_t timers_live; /* timers currently scheduled */
uint32_t queue_peak; /* deepest the mailbox has been */
uint32_t edges_used; /* graph edges taken from the static pool */
} espos_flow_stats_t;
void espos_flow_stats(espos_flow_stats_t *out);
bool espos_flow_on_loop_task(void);
esp_err_t espos_flow_adopt_loop(void);
void espos_flow_release_loop(void);
/* Reported by the graph when the edge pool (CONFIG_ESPOS_FLOW_MAX_EDGES) runs
* out. Not an espos_flow_* function because it is the C++ layer that counts
* edges; declared here so both halves agree on the number. */
void espos_flow_note_edges_used(uint32_t n);
#ifdef __cplusplus
}
#endif