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