Theory of Operation¶
The internal design of the firmware. Skippable; read it before hacking on the components or debugging a deployment.
Structure¶
Kegboard-specific logic lives in a framework-agnostic core (kbcore
namespace, plain C++, no ESPHome/ESP-IDF headers, time passed in as
arguments) that runs under host unit tests. The ESPHome components are thin
adapters around it. Rationale and rules in
components/kegboard/CORE.md.
Flow sensing and pour detection¶
Each meter pin counts falling edges in an interrupt handler, with a software
debounce (default 1200 µs, matching the legacy firmware). Open-collector
hall-effect meters emit a fixed volume per pulse, so volume is tick count ×
ml_per_tick — an odometer.
On top of the counter runs a pour state machine:
The first tick after idle starts a pour (
on_pour_start,pouringgoes on).Silence for
idle_timeoutends it. Pours shorter thanmin_pour_ticksare discarded as drips; pours exceedingmax_pour_durationare cut off as a stuck meter.The finished pour carries ticks,
volume_ml, duration, attribution from the meter’s grant (if any), and a bounded tick time series (series_resolutionbuckets) for diagnostics.
Calibration is applied on the device; the reported volume_ml is
authoritative, raw ticks are advisory.
Reporting¶
The reporter batches events — pours, live pour updates, temperatures, token
presentments, heartbeats, command results — and POSTs them as JSON to
reporting_url per the event protocol.
Queueing. Events that can’t be delivered wait in a bounded queue and retry with exponential backoff (base
retry_interval, capped at 5 min). Each event carries its age, so a batch delivered late lands with correct timestamps even on a board whose clock never synced. The queue is bounded RAM: when full, the oldest events are evicted first — during a long outage the most recent pours are the ones worth keeping — and thedroppedcounter advances. Events do not survive reboot.Idempotency. Delivery is at-least-once;
(device, boot_id, id)makes processing idempotent, so a retry can never create a duplicate drink.Heartbeats every
heartbeat_intervalgive the server a liveness signal and bound worst-case command latency, since server commands ride only in HTTP responses.Pairing. A 401 sends the device into pairing: it appears on the server dashboard, and on approval receives a bearer token, persisted in flash. A later 401 revokes it and restarts pairing.
Relays¶
A relay left on is usually a valve held open. Each relay in
packages/relays.yaml starts a watchdog timer when switched on and switches
itself off after relay_watchdog_timeout. The timer runs from the on-edge
and cannot be refreshed while the relay is on; it protects relays driven
manually or from Home Assistant. Grant-driven relays are bounded by the
grant clamp (max_grant_duration) instead — set the watchdog longer than
the clamp (or 0s) on relays the server grants, or the watchdog will
close the valve mid-grant.
Buzzer¶
When a passive piezo is connected, the board plays melodies transcribed from the AVR firmware:
Event |
Sound |
|---|---|
Boot complete |
Short musical tune ( |
Token authorized |
Rising three-note chirp ( |
Ping |
Two notes ( |
The scripts are plain rtttl; wire them to any trigger you like.