main
John Lauer Studio: project overview, how it works, lights, charging, battery life, firmware source, dashboard screenshots; license… c2eaf61 3d ago
/*
 * learn.h : faucet timeout / blip estimator (pure C, no hardware)
 *
 * The learner is fed two things only:
 *   - the time of every faucet interrogation pulse (ms since session start),
 *   - a periodic tick (ms since session start),
 * and it answers one question: should the echo be enabled right now?
 *
 * Everything in here compiles with plain gcc so test/ can drive it with a
 * simulated faucet. See README.md "How the learning works" for the full story.
 */
#ifndef LEARN_H
#define LEARN_H

#include <stdint.h>
#include <stdbool.h>

/* ---- tunables (calcs.json "timing") ---- */
#define LEARN_BLIP_START_MS     600u
#define LEARN_BLIP_MIN_MS       120u
#define LEARN_BLIP_MAX_MS       2000u
#define LEARN_RECOVER_MAX_MS    8000u   /* recovery from a timed-out faucet */
#define LEARN_MARGIN_MS         1500u   /* re-arm at timeout - 1.5 s */
#define LEARN_MARGIN_STEP_MS    500u    /* "re-arm earlier" after a miss */
#define LEARN_MARGIN_MAX_MS     4000u
#define LEARN_FALLBACK_REARM_MS 8000u   /* no observable rate change */
#define LEARN_TIMEOUT_MIN_MS    3000u   /* observations shorter than this are not timeouts */
#define LEARN_TIMEOUT_MAX_MS    65000u  /* uint16 ms storage limit */
#define LEARN_TIMEOUT_ASSUME_MS 60000u  /* assumed when no drop was ever seen */
#define LEARN_NO_TIMEOUT_MS     70000u  /* echo this long with no drop -> assume 60 s */
#define LEARN_RETURN_MS         1000u   /* "returned to active within 1 s" */
#define LEARN_DEFAULT_IDLE_MS   1000u   /* idle period when nothing was heard */

/* Rate classification: active period below 60 % of the idle period means the
 * faucet visibly speeds up when it sees a user. */
#define LEARN_RATE_RATIO_NUM    3u
#define LEARN_RATE_RATIO_DEN    5u

#define LEARN_STORE_MAGIC       0x4748u /* "GH" */
#define LEARN_STORE_VERSION     1u

/* Persistent part, kept in EEPROM between sessions. */
typedef struct {
    uint16_t magic;
    uint8_t  version;
    uint8_t  observable;        /* 0 unknown, 1 rate change seen, 2 no rate change */
    uint16_t timeout_ms;        /* learned faucet timeout (EWMA), 0 = unknown */
    uint16_t blip_ms;           /* current blip length */
    uint16_t blip_floor_ms;     /* never shorten below this (0 = no miss seen yet) */
    uint16_t blip_last_ok_ms;   /* shortest blip that has worked */
    uint16_t margin_ms;         /* re-arm margin before the timeout */
    uint16_t idle_period_ms;    /* fingerprint of the faucet this was learned on */
    uint16_t active_period_ms;
    uint8_t  crc;               /* CRC-8 over the bytes before it */
} learn_store_t;

typedef enum {
    LEARN_MODE_CLASSIFY = 0,    /* echo on, waiting to see whether the rate changes */
    LEARN_MODE_TRACK,           /* rate change observable: learn timeout + blip */
    LEARN_MODE_FALLBACK         /* no observable rate change: re-arm every 8 s */
} learn_mode_t;

typedef enum {
    LEARN_PH_ECHO = 0,          /* echo on, faucet should be running */
    LEARN_PH_BLIP,              /* echo off for blip_ms (re-arm) */
    LEARN_PH_VERIFY             /* echo back on, checking the faucet stayed on */
} learn_phase_t;

/* Event bits returned by learn_take_events() (for the lights and the log). */
#define LEARN_EV_BLIP_START     0x01
#define LEARN_EV_BLIP_END       0x02
#define LEARN_EV_DROP           0x04    /* faucet fell back to idle while we echoed */
#define LEARN_EV_TIMEOUT_LEARNT 0x08
#define LEARN_EV_BLIP_OK        0x10
#define LEARN_EV_BLIP_MISS      0x20
#define LEARN_EV_STORE_DIRTY    0x40    /* caller should save learn_store() */
#define LEARN_EV_MODE           0x80    /* classification finished */

typedef struct {
    learn_store_t s;

    learn_mode_t  mode;
    learn_phase_t phase;
    uint8_t       events;

    uint16_t p_idle;            /* idle interrogation period (ms) */
    uint16_t p_act;             /* active period (ms), EWMA */

    uint32_t now;
    uint32_t last_pulse;        /* time of the last pulse */
    uint32_t last_iv;           /* last interval (ms) */
    uint32_t last_fast;         /* time of the last pulse that came at the active rate */
    uint32_t npulses;
    bool     faucet_active;     /* derived from the pulse rate */

    uint32_t t_arm;             /* echo (re)enabled, also fallback re-arm base */
    uint32_t t_act;             /* last confirmed activation (faucet timer start) */
    uint32_t t_act_cand;        /* activation candidate after a blip */
    bool     act_cand_set;
    bool     timeout_measured;  /* this session has a real observation */
    bool     recovering;        /* blip was a recovery after a drop */

    uint32_t t_phase_end;       /* end of blip */
    uint32_t t_return_by;       /* VERIFY: must be active again by then */
    uint32_t t_verify_end;      /* VERIFY: no drop until then -> success */
    uint32_t t_classify_end;
    uint16_t blip_cur;          /* length of the blip in progress */
    uint16_t recover_ms;        /* next recovery blip length */
    bool     seen_active_after; /* VERIFY: saw active rate after blip end */

    /* statistics for tests / debug */
    uint16_t n_drops, n_blips, n_ok, n_miss;
} learn_t;

/* ---- listen phase helper (echo disabled) ---- */
typedef struct {
    uint32_t last;
    uint16_t iv[8];
    uint8_t  n_iv;
    uint8_t  n_pulses;
} learn_listen_t;

void     learn_listen_init(learn_listen_t *l);
void     learn_listen_pulse(learn_listen_t *l, uint32_t t_ms);
uint16_t learn_listen_period(const learn_listen_t *l);   /* 0 if unknown */

/* ---- store helpers ---- */
void     learn_store_defaults(learn_store_t *s);
uint8_t  learn_store_crc(const learn_store_t *s);
bool     learn_store_valid(const learn_store_t *s);

/* ---- session ---- */
/* stored may be NULL or invalid; idle_period_ms is from the listen phase (0 = unknown) */
void     learn_begin(learn_t *L, const learn_store_t *stored, uint16_t idle_period_ms, uint32_t now);
void     learn_pulse(learn_t *L, uint32_t t_ms);
void     learn_tick(learn_t *L, uint32_t now);
bool     learn_echo_on(const learn_t *L);
uint8_t  learn_take_events(learn_t *L);
const learn_store_t *learn_store(learn_t *L);   /* refreshes the CRC */

#endif