Skip to content

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