File espos_sched.h¶
File List > espos_flow > include > espos_sched.h
Go to the documentation of this file
/*
* SPDX-FileCopyrightText: 2026 Dirk Wahrheit
* SPDX-License-Identifier: Apache-2.0
*
* espos_sched — the timer wheel behind espos_flow, as pure C.
*
* Deciding what is due next is the one part of a scheduler worth testing
* exhaustively and the one part that needs no platform at all: given a set of
* deadlines and a clock reading, which timer fires, in what order, and how
* long may the loop sleep before the next one. None of that wants FreeRTOS or
* esp_timer, so none of it is here — the caller reads the clock and calls
* espos_sched_fire(). The same shape as espos_wifi_sm, espos_sk_token_sm,
* espos_health_policy and espos_time_policy: a host test drives a whole day of
* timers in microseconds and can single-step the millisecond the counter wraps.
*
* Time is uint32_t milliseconds, because that is what a device's monotonic
* counter is cheapest in and what the flow API takes. It wraps every 49.7
* days, and a device that runs a season must survive that: every comparison
* in here is a subtraction interpreted as a signed 32-bit difference
* ((int32_t)(a - b) < 0), never a < b. A deadline is "due" when that
* difference says it is at or behind the clock, so a wrap is not a special
* case, it is arithmetic that was always modular. The one thing this costs:
* no timer may be scheduled more than 2^31-1 ms (~24.8 days) ahead, which
* espos_sched_add() rejects rather than silently firing it immediately half a
* lifetime later.
*
* Threading: none. Everything here runs on whatever task calls it; the caller
* (espos_flow.c) owns the mutex. Nothing allocates, blocks or logs.
*/
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/* The furthest ahead a timer may be scheduled. Past this the signed
* difference that orders deadlines changes sign and "later" would read as
* "overdue" — the classic wrap bug, rejected at the door instead. */
#define ESPOS_SCHED_MAX_DELAY_MS 0x7FFFFFFFu
/* A timer's identity, returned by espos_sched_add(). Never 0 for a live
* timer, so 0 is a usable "no timer" value in application structs. Handles
* carry a generation counter, so cancelling a slot that has since been reused
* by a different timer cancels nothing rather than the innocent newcomer. */
typedef uint32_t espos_sched_handle_t;
#define ESPOS_SCHED_HANDLE_NONE ((espos_sched_handle_t)0)
/* Fired on the caller's task by espos_sched_fire(). A callback may add and
* cancel timers, including its own handle. */
typedef void (*espos_sched_cb_t)(void *arg);
typedef struct {
espos_sched_cb_t cb;
void *arg;
uint32_t due_ms; /* absolute, modular */
uint32_t period_ms; /* 0 = one-shot */
uint16_t gen; /* bumped on every reuse of the slot */
bool used;
bool cancelled; /* cancelled from inside its own callback; reaped by fire() */
bool swept; /* already run in the espos_sched_fire() now in progress */
bool fresh; /* added by a callback during that sweep: not eligible in it */
} espos_sched_timer_t;
/* Fixed table, sized by the caller: no allocation, and the number of timers a
* firmware can hold is decided at build time like every other espOS table. */
typedef struct {
espos_sched_timer_t *slots;
size_t cap;
uint32_t seq; /* generation source */
bool firing; /* inside espos_sched_fire(): defers slot reuse */
} espos_sched_t;
esp_err_t espos_sched_init(espos_sched_t *s, espos_sched_timer_t *slots, size_t cap);
esp_err_t espos_sched_add(espos_sched_t *s, uint32_t now_ms, uint32_t delay_ms, uint32_t period_ms,
espos_sched_cb_t cb, void *arg, espos_sched_handle_t *out);
esp_err_t espos_sched_cancel(espos_sched_t *s, espos_sched_handle_t h);
uint32_t espos_sched_next_due(const espos_sched_t *s, uint32_t now_ms);
size_t espos_sched_fire(espos_sched_t *s, uint32_t now_ms);
size_t espos_sched_count(const espos_sched_t *s);
static inline bool espos_sched_reached(uint32_t a, uint32_t b)
{
return (int32_t)(a - b) >= 0;
}
#ifdef __cplusplus
}
#endif