project
Ghost Hand
Public Unreviewedby John Lauer
A small battery gadget that keeps an automatic sink faucet running while you use the sink: it learns the faucet's timeout, re-arms with the shortest blip, and stops after 10 minutes or when picked up.
main
John Lauer
Studio: project overview, how it works, lights, charging, battery life, firmware source, dashboard screenshots; licenseā¦
c2eaf61
3d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
/*
* 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