Skip to content

File espos_health.h

File List > espos_health > include > espos_health.h

Go to the documentation of this file

/*
 * SPDX-FileCopyrightText: 2026 Dirk Wahrheit
 * SPDX-License-Identifier: Apache-2.0
 *
 * espos_health — the device's own view of what is wrong with it.
 *
 * A component that notices a condition it cannot fix — a wake service that
 * went away, internal RAM running out, a bus that stopped answering — raises
 * it here with a short stable key. Something else decides what to do with it:
 * espos_sk turns conditions into SignalK notifications, and any application
 * may add a sink of its own (a red LED, a line on a display, a relay).
 *
 * This exists so that "report a problem" is not a reason to depend on the
 * SignalK stack. espos_voice used to call espos_sk_notify() directly for its
 * one notification, which made a voice satellite unbuildable without SignalK
 * and pointed the dependency graph the wrong way — an optional component
 * depending on another optional component for a core concern. Reporting is
 * the core concern; SignalK is one sink.
 *
 * Conditions are level-triggered and idempotent: report the same state and
 * message twice and sinks are called once, so a caller may re-report on every
 * poll of whatever it is watching. ESPOS_HEALTH_NORMAL clears a condition and
 * is delivered like any other change — it is what retires an alert a previous
 * boot raised.
 *
 * The policy half (docs/health.md) is the device watchdog: a 10 s tick that
 * raises the built-in conditions (lowMemory, taskStalled), counts strikes for
 * conditions flagged ESPOS_HEALTH_F_REBOOT_ON_ALARM and restarts after N of
 * them, leaving a record the next boot can read. Loss of WiFi is never a
 * reason to restart; see espos_core's netDown.
 *
 * Thread-safe. Sinks run on the reporting task with no lock held, so a sink
 * may report conditions of its own; it must not block for long.
 */
#pragma once

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

#ifdef __cplusplus
extern "C" {
#endif

typedef enum {
    ESPOS_HEALTH_NORMAL = 0,  /* condition cleared */
    ESPOS_HEALTH_WARN = 1,
    ESPOS_HEALTH_ALARM = 2,
} espos_health_state_t;

/* A key is an identifier, not a sentence ("lowMemory", "wakeService"): it is
 * what a rule or a dashboard keys on, and it ends up in a SignalK path. The
 * message is the human-readable half and may change without re-notifying. */
#define ESPOS_HEALTH_KEY_MAX 24
#define ESPOS_HEALTH_MSG_MAX 96

/* Condition flags (espos_health_report_ex). */

/* A fatal condition: held in ALARM for CONFIG_ESPOS_HEALTH_POLICY_STRIKES
 * consecutive policy ticks, it restarts the device. WARN with this flag is
 * still only a warning. Reserve it for what a restart actually fixes — a
 * wedged co-processor link, memory that will not come back — never for the
 * network being away. */
#define ESPOS_HEALTH_F_REBOOT_ON_ALARM (1u << 0)

esp_err_t espos_health_report(const char *key, espos_health_state_t state, const char *message);

esp_err_t espos_health_report_ex(const char *key, espos_health_state_t state, const char *message, uint32_t flags);

const char *espos_health_state_str(espos_health_state_t state);

/* ------------------------------------------------------------------ sinks */

typedef void (*espos_health_sink_t)(const char *key, espos_health_state_t state,
                                    const char *message, void *arg);

esp_err_t espos_health_add_sink(espos_health_sink_t sink, void *arg);

esp_err_t espos_health_remove_sink(espos_health_sink_t sink, void *arg);

/* -------------------------------------------------------------- inspection */

typedef struct {
    char key[ESPOS_HEALTH_KEY_MAX];
    espos_health_state_t state;
    char message[ESPOS_HEALTH_MSG_MAX];
    uint32_t flags; /* ESPOS_HEALTH_F_* of the latest report */
} espos_health_condition_t;

size_t espos_health_snapshot(espos_health_condition_t *out, size_t max);

espos_health_state_t espos_health_worst(void);

bool espos_health_fatal_alarm(espos_health_condition_t *out);

void espos_health_reset(void);

/* ------------------------------------------------------- synthetic conditions */

#define ESPOS_HEALTH_TEST_PREFIX "test."

#define ESPOS_HEALTH_TEST_TTL_MAX_MS (300u * 1000u)

esp_err_t espos_health_report_test(const char *key, espos_health_state_t state, const char *message,
                                   uint32_t ttl_ms);

bool espos_health_test_expire(void);

/* ------------------------------------------------------------------ policy */

esp_err_t espos_health_policy_start(void);

esp_err_t espos_health_watch_task(const char *name, uint32_t timeout_ms);

esp_err_t espos_health_unwatch_task(void);

void espos_health_kick(void);

/* ------------------------------------------------------------ reset record */

/* Written right before the policy restarts the device; survives the restart in
 * RTC (or plain no-init) memory. `magic` is set by the writer and cleared by
 * the first reader, so a record is seen by exactly one boot. */
typedef struct {
    uint32_t magic;
    char key[ESPOS_HEALTH_KEY_MAX]; /* the condition that struck out */
    char message[64];               /* its message, clipped */
    uint32_t min_free_heap;         /* low-water marks at the time, bytes */
    uint32_t min_internal;
    uint32_t largest_block;         /* largest free internal block at the time */
    uint32_t uptime_s;              /* how long that boot had run */
    int64_t unix_ms;                /* wall clock, 0 when it was never set */
} espos_health_reset_record_t;

bool espos_health_last_reset(espos_health_reset_record_t *out);

#ifdef __cplusplus
}
#endif