Skip to content

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