Skip to content

File espos_health_policy.h

File List > espos_health > include > espos_health_policy.h

Go to the documentation of this file

/*
 * SPDX-FileCopyrightText: 2026 Dirk Wahrheit
 * SPDX-License-Identifier: Apache-2.0
 *
 * espos_health policy — the device watchdog as a pure C state machine.
 *
 * One tick (every CONFIG_ESPOS_HEALTH_POLICY_TICK_S on a device) reads the
 * heap, raises or clears the built-in conditions — lowMemory from the heap,
 * taskStalled from the registry of watched tasks — then asks whether any
 * condition flagged ESPOS_HEALTH_F_REBOOT_ON_ALARM is in ALARM and counts
 * strikes: N consecutive such ticks write a reset record and restart. WARN
 * never restarts, an ALARM without the flag never restarts, and one clean tick
 * resets the count, so a condition that comes and goes never adds up.
 *
 * Nothing here touches the platform. Clock, heap, the condition table, the
 * record store and the restart are injected through espos_health_policy_port_t,
 * so the machine runs unchanged on the host under test — the same shape as
 * espos_wifi_sm and espos_sk_token_sm. espos_health_policy_start()
 * (espos_health.h) owns the one instance a device runs.
 *
 * Threading: the caller serialises init/tick/watch/unwatch (the singleton uses
 * its own lock); port callbacks run on the caller's task with whatever it
 * holds. espos_health_policy_kick() is deliberately lock-free — it only stamps
 * a slot — so it may run on any task at any rate.
 */
#pragma once

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"

#include "espos_health.h"

#ifdef __cplusplus
extern "C" {
#endif

#define ESPOS_HEALTH_WATCHED_MAX   8
#define ESPOS_HEALTH_TASK_NAME_MAX 16

/* What the policy reads on every tick. Bytes throughout. */
typedef struct {
    uint32_t total_free;    /* free in the default heap */
    uint32_t total_min;     /* low-water mark of total_free since boot */
    uint32_t internal_free; /* free internal RAM — the scarce pool on PSRAM boards */
    uint32_t internal_min;  /* low-water mark of internal_free since boot */
    uint32_t largest_block; /* largest allocatable internal 8-bit block */
} espos_health_heap_t;

typedef struct {
    uint32_t strikes;                /* consecutive fatal ticks before a restart (0 counts as 1) */
    uint32_t heap_warn_kb;           /* lowMemory WARN below this much total heap; 0 = off */
    uint32_t internal_warn_kb;       /* lowMemory WARN below this much internal RAM; 0 = off */
    uint32_t internal_alarm_kb;      /* lowMemory fatal ALARM below this much internal RAM; 0 = off */
    uint32_t largest_block_alarm_kb; /* lowMemory fatal ALARM below this largest internal block; 0 = off */
    /* memoryTrough WARN when the internal-RAM low-water mark since boot is below
     * this much; 0 = off. Reporting only -- never fatal, by construction.
     *
     * The mark is heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL), which SUMS
     * each matching region's own minimum. Those minima need not be simultaneous, so
     * the sum is a LOWER bound on the worst simultaneous total -- no higher than it,
     * and usually below it. It says the device never had LESS free than this, not
     * that it ever had this little. Pessimistic, which is safe for a warning.
     *
     * Appended, not inserted: a mid-struct member shifts every later field for a
     * caller built against the old header. Appending changes only sizeof, which is
     * still an ABI break -- hence the ESPOS_ABI_VERSION bump -- but the narrow kind. */
    uint32_t internal_trough_warn_kb;
} espos_health_policy_cfg_t;

/* Everything the machine needs from the outside world. */
typedef struct {
    uint32_t (*now_ms)(void *ctx);   /* monotonic, wraps; differences only */
    uint32_t (*uptime_s)(void *ctx); /* for the record */
    int64_t (*unix_ms)(void *ctx);   /* wall clock; 0 when unknown */
    void (*heap)(void *ctx, espos_health_heap_t *out);
    /* Raise/clear a built-in condition: espos_health_report_ex() on a device. */
    esp_err_t (*report)(void *ctx, const char *key, espos_health_state_t state, const char *message, uint32_t flags);
    /* Is a condition flagged ESPOS_HEALTH_F_REBOOT_ON_ALARM held in ALARM?
     * espos_health_fatal_alarm() on a device. */
    bool (*fatal_alarm)(void *ctx, espos_health_condition_t *out);
    void (*store_record)(void *ctx, const espos_health_reset_record_t *rec);
    void (*restart)(void *ctx); /* does not return on a device */
} espos_health_policy_port_t;

typedef struct {
    void *task; /* opaque task identity (TaskHandle_t on a device); NULL = free slot */
    char name[ESPOS_HEALTH_TASK_NAME_MAX];
    uint32_t timeout_ms;
    uint32_t last_kick_ms;
} espos_health_watched_t;

typedef struct espos_health_policy {
    const espos_health_policy_port_t *port;
    void *ctx;
    espos_health_policy_cfg_t cfg;
    uint32_t strikes;               /* consecutive ticks a fatal ALARM was held */
    uint32_t ticks;
    bool restarting;                /* restart requested; further ticks do nothing */
    espos_health_condition_t fatal; /* the condition the last strike was for */
    espos_health_watched_t watched[ESPOS_HEALTH_WATCHED_MAX];
    bool ever_watched;              /* taskStalled exists only once a task registered */
} espos_health_policy_t;

void espos_health_policy_init(espos_health_policy_t *p, const espos_health_policy_port_t *port, void *ctx,
                              const espos_health_policy_cfg_t *cfg);

uint32_t espos_health_policy_tick(espos_health_policy_t *p);

esp_err_t espos_health_policy_watch(espos_health_policy_t *p, void *task, const char *name, uint32_t timeout_ms);
esp_err_t espos_health_policy_unwatch(espos_health_policy_t *p, void *task);
void espos_health_policy_kick(espos_health_policy_t *p, void *task);

/* Did this device come close to exhaustion at some point since boot?
 *
 * A different question from espos_health_policy_memory(), which asks about NOW.
 * Answered from the low-water marks the port already samples, so it costs a
 * comparison rather than a measurement. Returns WARN or NORMAL; never ALARM --
 * see the note on the implementation for why this must not be fatal. */
espos_health_state_t espos_health_policy_trough(const espos_health_policy_cfg_t *cfg,
                                                const espos_health_heap_t *h, char *message,
                                                size_t message_size);

espos_health_state_t espos_health_policy_memory(const espos_health_policy_cfg_t *cfg, const espos_health_heap_t *heap,
                                                char *message, size_t message_size, uint32_t *flags);

#ifdef __cplusplus
}
#endif