Skip to content

File espos_time.h

File List > espos_time > include > espos_time.h

Go to the documentation of this file

/*
 * SPDX-FileCopyrightText: 2026 Dirk Wahrheit
 * SPDX-License-Identifier: Apache-2.0
 *
 * espos_time — the device's wall clock, and the reason a delta can carry a
 * timestamp at all.
 *
 * An ESP32 boots with no idea what time it is. Until something tells it, the
 * only clock it has is a monotonic millisecond counter that starts at zero,
 * which is enough to schedule work and useless for saying when a measurement
 * was taken. That gap is not academic: values a device buffers while the
 * server is unreachable are replayed minutes or hours later, and a delta
 * without a timestamp is stamped by the server with the time it arrived — so
 * an hour of wind data recorded during an outage lands in the log as one
 * burst at reconnect, in the wrong place and the wrong order.
 *
 * This component closes it. It keeps one wall clock, learned from whichever
 * source got there first and is ranked highest (SNTP over a manual set over
 * the SignalK stream over an RTC value carried through a deep sleep), and
 * hands it out as unix milliseconds or as an ISO 8601 string. Everything
 * else — the delta engine's timestamps, the log's wall-clock prefix, the
 * `at` field of a reset record — reads it from here.
 *
 * The clock may be wrong or absent, and callers must cope: espos_time_now_ms()
 * returns 0 while unsynced rather than a plausible-looking 1970, so a consumer
 * that forgets to check produces an obviously missing value instead of a
 * quietly wrong one.
 *
 * Timezone stance: SignalK is UTC and so is everything this component
 * publishes. A local time is a display concern — a panel showing the crew what
 * o'clock it is — so espos_time_set_tz() and espos_time_parts() exist for a UI
 * to use, and nothing in the data path ever looks at them.
 *
 * Threading: every getter is a lock-protected snapshot copy and never waits on
 * a network. The SNTP sync callback runs on IDF's SNTP task, espos_time_set()
 * on whatever task the source lives on, and subscriber callbacks run on that
 * same task with no espos_time lock held: copy what you need and return, never
 * block there.
 */
#pragma once

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

#ifdef __cplusplus
extern "C" {
#endif

/* Longest string espos_time_iso8601() writes, NUL included:
 * "2026-09-07T10:12:13.456Z" is 24 characters. Part of the ABI. */
#define ESPOS_TIME_ISO_MAX 25

/* Longest POSIX TZ string espos_time_set_tz() accepts, NUL included.
 * "CET-1CEST,M3.5.0,M10.5.0/3" and friends fit comfortably. */
#define ESPOS_TIME_TZ_MAX 40

typedef enum {
    ESPOS_TIME_SRC_NONE = 0,   /* no clock: espos_time_now_ms() returns 0 */
    ESPOS_TIME_SRC_RTC = 1,    /* carried through a deep sleep in RTC memory */
    ESPOS_TIME_SRC_SK = 2,     /* navigation.datetime from the SignalK stream */
    ESPOS_TIME_SRC_MANUAL = 3, /* PUT /api/v1/time, or an application call */
    ESPOS_TIME_SRC_SNTP = 4,   /* an NTP server */
    ESPOS_TIME_SRC_MAX = 5,
} espos_time_src_t;

esp_err_t espos_time_start(void);

bool espos_time_is_synced(void);

int64_t espos_time_now_ms(void);

espos_time_src_t espos_time_source(void);

const char *espos_time_src_str(espos_time_src_t src);

size_t espos_time_iso8601(char *buf, size_t n);

size_t espos_time_iso8601_of(int64_t unix_ms, char *buf, size_t n);

esp_err_t espos_time_set(int64_t unix_ms, espos_time_src_t src);

esp_err_t espos_time_set_tz(const char *posix_tz);

const char *espos_time_tz(void);

typedef struct {
    int32_t year;         /* full year, e.g. 2026 */
    uint8_t month;        /* 1-12 */
    uint8_t day;          /* 1-31 */
    uint8_t hour;         /* 0-23 */
    uint8_t minute;       /* 0-59 */
    uint8_t second;       /* 0-60, a leap second included */
    uint16_t millisecond; /* 0-999 */
    uint8_t wday;         /* 0 = Sunday */
    uint16_t yday;        /* 0-365 */
    int32_t utc_offset_s; /* seconds east of UTC at this instant */
} espos_time_parts_t;

esp_err_t espos_time_parts(espos_time_parts_t *out);

typedef void (*espos_time_cb_t)(espos_time_src_t src, void *arg);
esp_err_t espos_time_subscribe(espos_time_cb_t cb, void *arg);
esp_err_t espos_time_unsubscribe(espos_time_cb_t cb, void *arg);

esp_err_t espos_time_status_json(char **out_json);

int64_t espos_time_now_ms_or_zero(void *arg);

#ifdef __cplusplus
}
#endif