coffee-botautomated AeroPress machine

Project

An AeroPress that presses itself

coffee-bot grinds beans, heats and doses water, blooms, brews, presses the plunger, ejects the puck, and cleans itself — driven by an ESP32-S3 running ESPHome, with a web app for recipes, live monitoring, and brew history. Built in nine strictly-gated phases: no phase starts until the previous one survives its endurance test.

Concept render of the finished coffee-bot machine

Right now

Phase 1 — water heating + dispense. Dispense path works end to end: servo-pressed unlock + dispense on the Tiger, dead-time-modeled timed pours centered on 250 mL.
Next up: threadlock the unlock servo horn, verify native buttons with bracket on, tank-level flow sweep, then the 50-cycle endurance run.
Device live: coffee-bot.local — ESPHome on ESP32-S3, OTA from here on. See bench reference.

Phase progress

Fill = steps resolved (done or deliberately dropped). Click a phase to open it.

System architecture

Two-board control split. The Raspberry Pi owns the UX (web app, recipes, history, graphs) and never touches hardware. The ESP32-S3 owns real-time control (state machine, all I/O, safety interlocks) and keeps operating safely if the Pi disappears. Linux can't guarantee real-time timing; deterministic, fail-safe control lives on the microcontroller.

Raspberry Pi FastAPI · SQLite · HTMX UI recipes · history · live graphs Phase 6 — planned (Pi vs ESP32-only UI is an open call, deferred to Phase 6) ESP32-S3 · ESPHome brew/clean state machine all hardware I/O · interlocks fails safe alone — outputs default OFF, finishes any cycle if the Pi drops WiFi · native API (+ REST debug) Browser / phone trigger brews · watch live WebSocket updates Tiger hot-water dispenser 2 servos · Ph 1 Linear actuator H-bridge · Ph 2 Grinder relay · Ph 3 Spray pump MOSFET · Ph 5 Limit switches interlocks · Ph 2 Load cell HX711 · upgrade Sensors floats · temp Brew cycle state machine (lives on the ESP32): idle → preheat → grind → dose-check → bloom-pour → bloom → main-pour → brew → press → eject → clean → idle every transition guarded: water present · at temp · chamber seated · limits respected

Major decisions the load-bearing ones

UL-listed appliance, not a gutted kettle

Phase 1 heat comes from a stock Tiger PDU-A50U dispenser. Servos press its buttons from outside — zero electrical contact, zero teardown, the appliance keeps its own thermal safety stack. Why →

Stationary chamber, one axis

A 12″ actuator presses straight down into a fixed AeroPress. The rotating two-station plate was rejected: a motor, indexing, and a thrust bearing bought nothing a cleaning cycle can't. Why →

±15 mL accepted for v1

Open-loop timed pours can't beat pump spin-up variance. ~6% water error is imperceptible in an AeroPress; ±5 mL waits for the load-cell closed loop. Why →

No stirring, by design

Bloom phase + pulsed dispense + an offset pour swirl the grounds for free. Software and geometry replace an entire mechanism. Why →

Temp probe dropped for v1

The Tiger holds its own setpoint 24/7; nothing in the dispense path needs an independent reading. The DS18B20 was a nicety, not a control or safety input. Why →

Never 3D-printed in the wet path

FDM layer lines trap bacteria and coffee oils. Stainless, glass, silicone, or the AeroPress's own polypropylene touch water — PETG is structure only. Why →

Build log key dates

  • 2026-05-27

    Phase 1 pivot: Tiger PDU-A50U hot-water dispenser replaces the gutted-kettle PID design — UL-listed appliance takes over all thermal regulation and mains risk.

  • 2026-05-31

    Servo interface chosen over optocoupler teardown — fully reversible, warranty intact. Slip-over 10 mm silicone plumbing replaces the push-in plan.

  • 2026-06-03

    Servos bench-validated: both MG90S units press the Tiger's buttons with torque to spare. Bracket modeling begins in Fusion 360; stock horns with arc-tuned geometry.

  • 2026-06-05

    Firmware flashed, live on WiFi. Servos calibrated in-place (unlock 0.55 / dispense 0.45). Dead-time dispense model fitted: 32.0 mL/s + 1750 ms. Auto-relock measured ~10 s.

  • 2026-06-08

    Two v1 calls: DS18B20 temperature monitoring dropped (appliance self-regulates); ±15 mL open-loop tolerance accepted, ±5 mL deferred to a load-cell closed loop.

  • 2026-06-09

    Endurance harness written (scripts/endurance_run.sh) and smoke-tested — cycle 1 fired clean over REST.

The discipline

One rule above allEach phase has a hard exit criterion — typically 50 consecutive successful cycles, zero manual intervention, zero safety events. Phase N+1 does not start until phase N passes. This is what prevents a tangle of half-working subsystems.
Convenience snapshot, generated 2026-06-10 — not the source of truth. Canonical specs live in the repo: CLAUDE.md, coffee-bot-project-scope.md, docs/pin-assignments.md, hardware/phase1-bom.md.
Display-name candidates under review include J.A.V.A. 9000, The Plungenator, Brewbacca, and HAL 9000ml. “I'm afraid I can't grind that, Dave.”

Phase 0 · complete

Planning & sourcing

doneexit criterion met

Lock the design enough to start buying parts, and get the development environment ready. Everything downstream depends on the architecture choices made here — the control split, the stationary layout, and the staged build discipline.

Exit criterion — metPhase 1 parts ordered; dev environment working (ESPHome flashes the ESP32). Both confirmed — firmware now runs on the bench rig.

Tasks

  1. done

    Finalize the scope document and system architecture

    Full plan in coffee-bot-project-scope.md — nine phases, each with an exit criterion, plus the risk register and decisions log. Control split (Pi UX / ESP32 real-time) and stationary mechanical layout locked.

  2. done

    Choose specific Phase 1 components

    The big call: a UL-listed Tiger PDU-A50U hot-water dispenser instead of a gutted kettle + SSR + PID, interfaced by servos pressing its buttons (2026-05-27 / 05-31 decisions). ESP32-S3-DevKitC-1 N16R8 as the controller. Full list in Hardware & BOM.

  3. done

    Set up the dev environment and repo structure

    ESPHome 2026.5.2 installed; repo laid out as firmware/, webapp/, hardware/, docs/; secrets in secrets.yaml (never committed).

  4. done

    Order Phase 1 parts

    No long-lead items — everything ships in 1–3 days (McMaster silicone next-day). ~$435 from scratch, ~$325–365 net of already-owned tools.

  5. done

    Acquire safety equipment

    Inline GFCI adapter (15 A), surge protection, multimeter, Class C fire extinguisher within reach for all powered testing.

Wiring

No wiring in this phase — it's all paper and purchase orders. The wiring story starts in Phase 1.

Phase 1 · current focus

Water heating + dispense

in progressdispense path working end-to-enddevice: coffee-bot.local

A benchtop rig that holds water at an AeroPress-appropriate setpoint and dispenses ~250 mL on command, failing safe under any single-point failure. The most dangerous and most foundational subsystem — which is why it goes first.

Exit criterion50+ consecutive successful cycles, no manual intervention, no safety events. Volume holds at the v1 tolerance (~±15 mL) across all 50. The endurance harness is written and smoke-tested; the full run is the last gate.

Architecture

A stock Tiger PDU-A50U 5 L hot-water dispenser does all the heating — it's UL-listed, holds its setpoint 24/7 by design, and keeps its own thermal protection stack (thermostat, thermal fuse, dry-fire interlock). The ESP32 never touches mains or water. It drives two MG90S micro servos in a printed PETG bracket clipped over the Tiger's button panel; the servos physically press Unlock (the child-lock can't be disabled, and re-arms in ~10 s) and Dispense (held for the calibrated pour duration). The Tiger is unmodified and fully reversible — its own buttons still work.

Pour volume is open-loop timed using a dead-time model: hold = volume / flow + dead_time with flow = 32.0 mL/s and dead-time = 1750 ms (pump spin-up + tube priming). The dead-time term is what keeps small bloom and pulsed pours proportional, not just the 250 mL target. Water exits the spout through slip-over food-grade silicone tubing (10 mm ID over the 10.5 mm OD spout).

Unlock press level

0.55

0.65 overshot the contact arc after a reboot — 0.55 lands squarely. Horn screw needs threadlock.

Dispense press level

0.45

Held for the full pour; minimal overtravel to avoid a sustained stall.

Flow rate

32.0 mL/s

Two-point fit at 196 °F. Varies with tank level — full→¼ sweep still pending.

Dead time

1750 ms

Pump spin-up + priming lag, net of end-of-pour dribble. Fitted from 15.0 s→421 mL, 8.9 s→226 mL.

Build sequence

  1. dropped · v1

    1. ESP32 + DS18B20 temperature verification

    Dropped 2026-06-08. The Tiger self-regulates its setpoint, so an independent probe was never control or safety — just a monitoring nicety. Removed from firmware (was GPIO4); the bench probe had also failed its OneWire bus after a water dunk. Re-add later as a food-grade in-tank probe if wanted.

  2. done

    2. Servo bench test

    Both MG90S units validated standalone (servo tester, 06-03) with enough torque to depress the Tiger's buttons, then driven from the ESP32's LEDC PWM on a separate 5 V/2 A supply (06-05). The gotcha was the common ground — without tying the servo PSU ground to the ESP32, both servos spun continuously.

    Exit: reliable ESP32-driven sweep, no brownout, quiet at rest. ✓

  3. done

    3. Dispenser baseline verification

    Out-of-box function confirmed before any hardware went near it (non-negotiable — a DOA unit caught early is returnable). Auto-lock re-arm window measured at ~10 s, which gives the firmware's 300 ms unlock→dispense gap ~33× margin. Tiger set to 196 °F and left powered 24/7 as designed (~1 kWh/day standby).

  4. done

    4. Servo bracket design + print

    Parametric Fusion 360 model; PETG print clips over the button panel with no fasteners or adhesives. Stock single-arm horns, shaft axis parallel to the panel so the horn sweeps into the button, contact landing near end-of-travel where tip motion is straight in (minimal skate, max torque).

  5. in progress

    5. Servo press calibration + validation

    Servos mounted and calibrated in-place via the web UI test buttons: unlock 0.55, dispense 0.45 — both press reliably. Remaining: threadlock the unlock horn screw (it drifted after a reboot — geometry must not creep during endurance) and confirm the Tiger's native buttons still work with the bracket installed.

  6. dropped · v1

    6. DS18B20 body mount + offset calibration

    Dropped with step 1. If a probe returns later, it should be a food-grade in-tank unit reading true water temperature — no body-offset calibration needed.

  7. done

    7. Slip-over silicone tube on the spout

    Heat-softened 10 mm ID tube installed two-handed against the spring-loaded spout's flex, routed forward-and-down with strain relief near the spout exit. For endurance testing the tube is routed back into the Tiger's fill opening as a recirculation loop, so tank level stays constant across 50 cycles (~12.5 L of water otherwise).

  8. in progress

    8. Flow-rate characterization

    Steady-state flow 32.0 mL/s and 1750 ms dead-time fitted at 196 °F. Remaining: the full→¼ tank sweep, to quantify how much head-pressure drop moves the pour as the tank empties.

  9. done · v1

    9. Volume calibration

    Centered near 250 mL; repeat pours of 261/237/237 mL show an open-loop spread of ~±15 mL, dominated by pump spin-up variance plus slow tank-level drift — not tunable by timing. Accepted for v1 (~6% of the water, imperceptible in an AeroPress). ±5 mL is deferred to the load-cell closed loop (stop the pour at 250 g).

  10. in progress

    10. Endurance test — the exit gate

    scripts/endurance_run.sh fires N brew_dispense cycles over the REST API with CSV logging, a pre-flight reachability check, consecutive-failure abort, and a clean Ctrl-C that parks the servos. Smoke-tested 2026-06-09 (cycle 1 ok). The full 50-cycle week-long run — watching for servo drift, bracket slip, relock failures, tank-level effects — is what closes Phase 1.

Wiring diagram

+5 V (servo PSU) servo signal (3.3 V PWM) ground (common) 120 VAC mains mechanical press
USB-C 5 V / 3 A ESP32's own supply ESP32-S3 DevKitC · N16R8 GPIO5 GPIO6 GND Servo PSU — 5 V / 2 A separate supply, NOT the ESP32 rail +5V GND MG90S — “Dispense” held for full pour · press level 0.45 orange · sig red · +5V brown · GND MG90S — “Unlock” 400 ms press · press level 0.55 orange · sig red · +5V brown · GND single-point common ground — without it the PWM has no reference (servos spin) Tiger PDU-A50U · 5 L UL-listed · holds 196 °F 24/7 Dispense Unlock internal: thermostat · thermal fuse · dry-fire interlock · pump spout → 10 mm ID silicone (slip-over) → vessel mechanical press only — zero electrical connection Wall → inline GFCI 15 A NEMA 5-15 + surge 120 VAC — appliance cord only

The ESP32 side is entirely low-voltage; the only mains-connected component is the stock appliance behind a GFCI.

Power isolation is mandatory. Each MG90S pulls 250–500 mA peak on a press. Off the ESP32 USB rail that transient browns out the MCU mid-press. Both red leads come from the separate 5 V/2 A supply; its ground ties to an ESP32 GND pin at a single point.
3.3 V signal into a 5 V servo is fine. The MG90S registers pulse edges well below 5 V — no level shifter. Power at 5 V, signal at 3.3 V.
Boot safety — “Option A.” Rest = level 0.0 = the LEDC idle duty = the power-on default, and restore: false keeps boots deterministic. A reset, brownout, or crash can therefore never press a button: the failure mode is “no press,” never “stuck pressing.” on_boot re-asserts rest as belt-and-suspenders, and there's deliberately no auto_detach_time — a detached arm could drift onto a button, and a detach mid-pour would cut the dispense short.

Considerations

  • Setpoint is manual, held 24/7. 196 °F covers all AeroPress recipes; 176 °F-style recipes wait for a third servo or teardown. Standby ≈ 1 kWh/day (~$5/mo).
  • Unlock before every dispense. The child-lock can't be disabled and re-arms in ~10 s. The firmware's 300 ms gap has ~33× margin; the relock does not interrupt an in-progress pour (confirmed).
  • Gentle stall only. The horn bottoms out with slight overtravel and a brief stall — a sustained hard stall cooks MG90S gears. Threadlock the horn screws; they back out under press-and-stall cycling.
  • Spring-loaded spout. It flexes toward the tank when pushed. Two-handed tube install, route forward-and-down, strain-relieve within 2–3″ so tubing forces never load the spout's internal spring.
  • Tank level moves the pour. Pump output drops as the tank empties — part of the measured ±15 mL. Step 8's sweep quantifies it; the load-cell upgrade makes it irrelevant.
  • Tube geometry is part of the calibration. Any Phase 4 plumbing change means re-running flow characterization.

Phase 2 · planned

Press mechanism

plannedstarts after Phase 1 exit

Drive the AeroPress plunger through a full press stroke with controlled force and position — including pushing the spent puck out the bottom — with hard limit interlocks the firmware cannot ignore.

Exit criterion50 consecutive press + eject cycles. Plunger seats and presses without binding, ejects the puck cleanly, respects limit switches every time.

Architecture

A 12 V linear actuator (~12 in stroke, ~100 lb force) hangs from the top of the frame and couples to the plunger through a printed PETG coupler (fine here — an air gap separates the plunger from the coffee). A BTS7960 (IBT-2) H-bridge gives direction + speed control; top and bottom limit switches feed the ESP32 as interlocks. The AeroPress sits in a rigid printed cradle on an aluminum baseplate that has to shrug off 30–40+ lb of press force without flexing.

Build sequence

  1. todo

    1. Wire actuator + H-bridge + 12 V/10 A supply

    Verify extend/retract under ESP32 control. The supply needs headroom: 5–8 A under full load, and an undersized brick browns out and stalls the actuator — which looks exactly like a logic bug.

  2. todo

    2. Add limit switches as hard interlocks

    Top and bottom switches; firmware refuses to drive past either. The actuator's internal end-stops protect the motor, but external switches give the firmware position truth to reason about.

  3. todo

    3. Mount AeroPress in cradle, couple the plunger

    Verify alignment and zero binding through the full stroke. Small offsets compound into stuck plungers under load.

  4. todo

    4. Tune press speed and force profile

    Slow initial compression, then steady press. Optional: current-sense on the H-bridge to detect end-of-press by load — the press stiffens when the puck compresses.

  5. todo

    5. Puck ejection

    Drive the plunger fully through the chamber, pushing the spent puck out the bottom into a bin.

  6. todo

    6. Endurance — 50+ press + eject cycles

    Water-only AeroPress, full stroke every time, limit switches respected on every cycle.

Wiring diagram tentative — pins finalize at phase start

+12 V +5 V logic control signal ground
ESP32-S3 GPIO9 GPIO10 GPIO11 GPIO12 GPIO13 GND 5V GPIO12/13: INPUT_PULLUP BTS7960 (IBT-2) 43 A H-bridge module RPWM LPWM R_EN + L_EN VCC 5V GND M+ M− B+ B− extend retract enable/speed Linear actuator 12 V · 12 in stroke ~100 lb force 5–8 A under load 12 V / 10 A PSU headroom matters — undersized supply stalls look like logic bugs common ground with ESP32 Limit — TOP closes to GND at full retract Limit — BOTTOM closes to GND at full press firmware refuses to drive past either switch

Direction comes from which PWM pin is driven (RPWM extend / LPWM retract); both enables tie together to GPIO11. Limit switches are inputs with internal pull-ups, switched to ground.

Considerations

  • Cradle rigidity is the whole game. 30–40+ lb of press force; any flex shifts the AeroPress and binds the plunger.
  • PETG coupler is fine — air gap between plunger top and coffee means it never touches the food path.
  • Current sense (optional) enables end-of-press detection by load and nicer press profiling later; a stepper + leadscrew is the eventual upgrade path for true press profiles.
  • Park position: on any fault the actuator parks (retracted) — rule 8 of the safety constraints.

Phase 3 · planned

Grinder integration

planned

Dose a repeatable quantity of ground coffee on command. A gutted blade grinder switched by a relay, time-based dosing, beans in a hopper, grounds out a chute. Deliberately the crude v1 answer — see the decisions log.

Exit criterion20 consecutive doses within ±1 g of target. Grounds reliably exit the chute without significant clinging or clogging.

Build sequence

  1. todo

    1. Gut the grinder, wire the motor through a relay

    Bypass the lid safety interlock (the machine's enclosure becomes the new interlock), route the motor's hot line through a relay or SSR rated for the motor's inrush.

  2. todo

    2. ESP32 controls grinder run time

    A switch entity + scripted run duration, with a hard maximum runtime as a guard.

  3. todo

    3. Build the time→grams curve

    Grind for N seconds, weigh the output, fit the curve. Expect drift with bean type, hopper level, and blade wear.

  4. todo

    4. Test repeatability across 20 doses

    ±0.5–1 g is the accepted v1 spread. By-weight dosing (load cell under the chamber) is the upgrade that fixes this properly — same HX711 hardware as the dispense upgrade.

  5. todo

    5. Design and test the chute

    Grounds must fall cleanly without clinging: steep angle (>60°), smooth or metal surface against static cling, removable for cleaning since it gets coffee-oily over time.

Wiring diagram tentative — finalize at phase start

120 VAC hot (switched) neutral earth bond +5 V control signal ground
ESP32-S3 GPIO14 5V GND Relay / SSR module opto-isolated · rated for motor inrush IN VCC GND COM NO control side ←→ mains side fully separated Wall → GFCI + surge all mains in enclosed wiring hot → relay COM Blade grinder gutted · lid interlock bypassed by design RC snubber across motor (EMI + relay-contact arcing) NO → neutral direct to motor earth bond → grinder chassis / any exposed metal

Relay in the hot leg, normally-open: grinder is off on ESP32 reboot, crash, or brownout. Mains never touches a breadboard (safety rule 4).

Considerations

  • Grinder motors are electrically filthy. Snubbers/flyback protection, sensor wiring physically separated from motor wiring, separate ground if EMI shows up in Phase 4.
  • Retention is real: some grounds always stay behind. Accept it, or purge with a small pre-grind.
  • Dry grounds vs the wet-path rule: a printed chute is acceptable (dry contact), but it still oils up — make it removable.
  • Burr-quality grind (e.g. motorizing a Baratza Encore) is an explicit non-goal for v1.

Phase 4 · planned

Mechanical integration

plannedthe hardest phase

Combine heating/dispense, press, and grinder into one frame and run a full brew cycle end to end. Three individually-working subsystems can still fail together — timing interactions, mechanical interference, EMI, vibration. This phase is where the discipline pays off.

Exit criterion25 consecutive full brew cycles (grind → dose → dispense → bloom → brew → press → eject) producing drinkable coffee, no manual intervention except filter loading and mug placement.

Mechanical layout

Stationary AeroPress, one vertical axis. Actuator on top, ~6 in open dispense zone below the retracted plunger (funnel and spray nozzle offset from the plunger axis, grounds chute entering from the side), chamber in a cradle on the baseplate, mug well below. Side-mounted: elevated boiler, cold reservoir, grinder + hopper, electronics, touchscreen. Footprint ≈ 11 × 12 × 27 in.

Side view of the stationary layout, approximately to scale

Side view, approximately to scale. The offset pour doubles as the stirring mechanism — see decisions.

Build sequence

  1. todo

    1. Build the frame

    Aluminum extrusion or sheet metal. Rigidity is checked at the two load points: the actuator mount and the chamber cradle.

  2. todo

    2. Mount the actuator, verify travel

    Plunger must clear the dispense zone fully retracted and reach through the chamber for ejection fully extended.

  3. todo

    3. Mount boiler (elevated), reservoir, grinder, electronics

    Boiler elevation gives gravity assist; electronics go in a vented enclosure away from heat and splash.

  4. todo

    4. Position dispense funnel and grounds chute

    Both offset from the plunger axis, both aimed into the chamber. Alignment is everything — grounds, water, and plunger all have to land in the same 62 mm circle.

  5. todo

    5. Route all tubing and wiring

    P-clips and clean runs for silicone; strain relief and cable channels for wiring; sensor wiring kept away from motor wiring.

  6. todo

    6. Write the integrated brew state machine

    idle → preheating → grinding → dosing_check → dispensing_bloom → blooming → dispensing_main → brewing → pressing → ejecting → idle, every transition guarded by precondition checks (at temp, water present, chamber seated, limits respected).

  7. todo

    7. Dry run each step, then water-only, then a real brew

    Escalate only when the previous level is boring.

System wiring map power domains + signals

120 VAC domain — GFCI-protected strip, surge protection, earth-bonded, fully enclosed Tiger dispenser stock cord Grinder motor via relay · GPIO14 12 V / 10 A PSU actuator + pump rail 5 V PSUs (logic + servo) USB-C 3 A · servo 2 A 12 V domain Linear actuator BTS7960 H-bridge GPIO9 / 10 / 11 Spray pump MOSFET low-side GPIO15 · flyback diode 5 V domain 2× MG90S servos Tiger buttons GPIO5 / 6 Relay module · logic grinder relay coil, sensors, pull-ups ESP32-S3 — signal hub GPIO5/6 servos · 9/10/11 H-bridge · 12/13 limits 14 grinder relay · 15 spray MOSFET · 16+ sensors single common ground across all DC domains 3.3 V control signals 3.3 V control signals EMI rules: snubbers on motors · sensor runs separated from motor runs · threadlocker + strain relief everywhere (vibration)

Considerations

  • Bloom timing: dispense ~20–30% of the water, pause 30–45 s, then the rest in pulses. Software-only, tunable per recipe.
  • Thermal neighbors: the boiler warms nearby printed parts — PETG near heat, never PLA (softens ~140 °F).
  • Vibration loosens everything — grinder and actuator both. Threadlocker, lock washers, periodic inspection.
  • EMI from the grinder is the most likely "impossible" bug — plan wiring separation up front.

Phase 5 · planned

Cleaning system

planned

The #1 failure mode of DIY coffee machines is that they get gross and get abandoned. An automatic cold-spray + hot-rinse cycle keeps daily grime handled, so manual cleaning is a weekly task — and the parts that need it pop out by hand.

Exit criterionAfter 20 brew+clean cycles, the wet path shows no significant residue the cycle didn't handle. Manual cleaning is weekly, not daily.

Target cleaning cycle

eject puck → retract plunger → hot rinse (dispense path) → cold spray (pump + nozzle) → optional second rinse → drain pause → ready

Why cold spray? Coffee oils congeal in cold water and wash away; hot rinses can set tannin stains into plastic. The spray needs pressure (50–100 PSI diaphragm/washer pump), not just flow — peristaltics won't dislodge fines.

Build sequence draft — scope finalizes at phase start

  1. todo

    1. Bench-test pump + hollow-cone nozzle

    Verify the diaphragm pump drives the food-grade nozzle with enough force to dislodge clinging fines.

  2. todo

    2. Solve spray geometry around the plunger

    With a stationary chamber the plunger occupies the center — multiple jets around it, or side-mounted nozzles on the splash shroud hitting the walls.

  3. todo

    3. Waste drain at the chamber position

    Rinse water exits the chamber bottom when the mug isn't there — drain to a waste reservoir with a level sensor so it can't silently overflow.

  4. todo

    4. Integrate hot rinse + cold spray into the state machine

    The hot rinse reuses the Phase 1 dispense path; the spray pump gets its own MOSFET channel.

  5. todo

    5. Residue evaluation across 20 brew+clean cycles

    Inspect the wet path; the cycle has to keep up with daily use on its own.

Wiring diagram tentative — finalize at phase start

+12 V control signal ground sensor input water
ESP32-S3 GPIO15 GPIO16* GND *tentative — from the free pool Logic-level MOSFET low-side switch gate drain source 220 Ω gate resistor · 10 kΩ pulldown (off at boot) Spray pump 12 V diaphragm 50–100 PSI flyback diode across leads (cathode → +12 V) pump− → drain 12 V rail shared with actuator PSU +12 V → pump+ Waste reservoir float switch closes to GND when full INPUT_PULLUP · blocks clean cycle when full cold reservoir → pump → hollow-cone nozzle → chamber walls → waste reservoir

Low-side MOSFET with a pulldown: pump is off at boot and on any reset (safety rule 7). The float switch is a hard precondition on the cleaning state.

Considerations

  • No cycle replaces weekly manual cleaning — funnel, shroud, cradle, and chamber all pop out by hand. The reference DIY build failed because parts were uncleanable, not because it lacked a cycle.
  • Optional spray-line solenoid if the pump alone doesn't give crisp start/stop.
  • Mug logistics: cleaning happens with no mug present — that's why the waste drain exists at the chamber position.

Phase 6 · planned

Pi web application

plannedPi vs ESP32-only: open call

A standalone web app for control, monitoring, recipe management, and brew history with graphs. The Pi owns the experience; it never owns brew logic — the ESP32's state machine stays reflashable without touching the app.

Exit criterionTrigger a brew from the web app, watch it live, review the full data afterward. Recipes editable; history persists across reboots.
Open question, deliberately deferred: the ESP32-S3 alone could host basic control + a simple UI, skipping the Pi entirely. The Pi buys rich graphs, persistent history, and a touchscreen kiosk. Real usage during Phases 4–5 decides which features actually matter.

Architecture

Browser / phone HTMX fragments vanilla JS + Plotly live view over WebSocket Raspberry Pi Caddy (HTTPS) → FastAPI (async) SQLite · systemd recipes · brew_sessions · brew_metrics ~2000 rows/brew — trivial for SQLite background task: aioesphomeapi client, subscribe to all state, tag with brew_id, auto-reconnect on drop ESP32-S3 ESPHome native API :6053 REST :80 (debug) owns the brew state machine + all safety interlocks keeps running — and finishes any in-progress cycle safely — if the Pi disappears HTTPS WebSocket WiFi · push command flow: browser → FastAPI → number entities (temp, volume) + start button → ESP32 runs the cycle state flow: ESP32 → native API push → SQLite (tagged brew_id) + WebSocket broadcast → live chart brew logic NEVER lives on the Pi — it sets parameters, starts, and observes

Features priority order

  1. Live brew view — real-time temp curve, state, elapsed, progress
  2. Brew trigger — pick recipe, start, observe
  3. Recipe CRUD
  4. Brew detail — post-brew timeline, state transitions annotated
  5. History — searchable, thumbnail curves, ratings
  6. Recipe comparison — overlay brews for consistency
  7. Boiler diagnostics — rolling temp history
  8. Scheduling — optional wake-up brew

Build sequence

  1. todo

    1. aioesphomeapi connection

    Subscribe to all ESP32 state, log to SQLite, survive disconnects.

  2. todo

    2. FastAPI skeleton + SQLite schema

    recipes, brew_sessions, brew_metrics(brew_id, ts_offset_ms, metric, value) indexed on (brew_id, ts_offset_ms). Raw parameterized SQL, no ORM.

  3. todo

    3. Brew trigger + live view (WebSocket)

    The first end-to-end loop: set recipe numbers, press start, watch the curve draw itself.

  4. todo

    4. Recipe CRUD

    HTMX server-rendered fragments — no build step.

  5. todo

    5. History + detail views with Plotly

    Searchable history with thumbnail curves; detail view annotates state transitions on the timeline.

  6. todo

    6. Polish: comparison, diagnostics, scheduling

    The A/B-testing layer that makes the brew data fun.

  7. todo

    7. systemd + Caddy deployment

    Boots with the Pi, local HTTPS, survives power cuts.

Wiring

No new wiring — this phase is software. The Pi joins over WiFi; the ESP32's I/O is already in place from Phases 1–5.

Phase 7 · planned

Enclosure & polish

planned

Turn the working mechanism into something that looks finished and is safe on a counter. Deliberately last: resist the urge to start here — it comes after the mechanism works.

Exit criterionLooks finished, mains fully enclosed, touchscreen works, removable parts still accessible for cleaning.

Tasks

  1. todo

    Design + build the outer enclosure

    Printed panels, sheet metal, or both. Must not trap heat around boiler or electronics — ventilation matters. Removable panels, never glued: you will need to get back inside.

  2. todo

    Enclose all mains wiring

    Fully inaccessible during normal use; exposed metal bonded to earth.

  3. todo

    Mount the touchscreen, tidy cable runs

    Front panel kiosk for the web app; cables into channels with strain relief.

  4. todo

    Status lighting + cosmetic finishing

    Anodized parts, consistent fasteners, splash containment and drip management refined. This is where it starts looking like the render.

Wiring

No new circuits — this phase repackages Phase 4's wiring into enclosed, serviceable runs.

Phase 8 · the goal state

Daily use & refinement

futureno exit criterion — this is the destination

Use coffee-bot every day and refine from real experience. The brew history + ratings become a dataset: A/B test dispense profiles, temperatures, and doses. Data-driven coffee is the fun part.

Ongoing work

  1. future

    Use it daily, log issues

    The ultimate test was in the success criteria from day one: it lives on the counter and gets used.

  2. future

    Dial in recipes from history data

    Overlay brews, compare ratings, converge on house blends.

  3. future

    Watch for slow failures

    Seal degradation, boiler scale (descale on a schedule), loosening connections, grounds accumulating in awkward spots.

  4. future

    Upgrade deliberately — only once v1 is rock solid

    Queue, in rough order of payoff: load cell + HX711 (closed-loop ±5 mL pours and by-weight dosing), burr grinder (motorized Baratza Encore), stepper + leadscrew press profiling, Home Assistant integration, scheduled wake-up brews.

Wiring

Only what upgrades bring. The load-cell upgrade adds an HX711 board on two GPIOs — it sits under the mug, outside the wet path, which is exactly why it beat a flow meter.

Reference

Decisions log

Every load-bearing call, dated, with the rationale and what it beat. The rejected option matters as much as the chosen one — it's the answer to "why don't we just…"

Dated decisions

2026-06-08

±15 mL open-loop tolerance accepted for v1; precision path = load cell

Measured pour-to-pour spread at the 250 mL target is ~±12–17 mL (261/237/237), dominated by pump spin-up variance + tank-level drift — timing tweaks can't beat it. That's ~6% of the water, imperceptible in an AeroPress. ±5 mL comes later from a closed-loop by-weight pour: load cell + HX711 under the mug, stop at 250 g. Immune to flow variance and tank drift, and the same part later enables by-weight coffee dosing.

over: more timing calibration (can't fix variance) · inline flow meter (food-safe sensor in the wet path is harder than a load cell outside it)

2026-06-08

DS18B20 temperature monitoring dropped for v1

The UL-listed Tiger holds its own setpoint 24/7; nothing in the dispense path consumes an independent reading. The probe was never control or safety — just a nicety, and the bench unit failed its OneWire bus after a water dunk. Removed from firmware (GPIO4 freed). If re-added, it should be a food-grade in-tank probe reading true water temperature.

2026-06-03

Stock servo horns + arc-tuned press geometry

No custom horn. Shaft axis parallel to the panel so the horn sweeps into the button; contact lands near end-of-travel where tip motion is straight in (minimal skate, full torque). Coarse alignment by spline indexing (~18° steps), fine by ESPHome level:. Committed only after bench-proving both servos had the torque.

over: custom-printed horn/pusher — simpler is adequate

2026-05-31

Servos pressing buttons externally — not optocouplers inside

Two MG90S in a clip-on PETG bracket press Dispense and Unlock from outside. Eliminates the teardown entirely: no soldering into the appliance, no warranty/water-tightness/clip-breakage risk, fully reversible, native buttons still work. Cost delta +$13 net. Trade: no Temp Set actuation, so the setpoint is configured manually once (196 °F covers AeroPress; 176 °F recipes deferred).

over: optocouplers across the button taps (teardown, irreversible)

2026-05-31

Slip-over silicone plumbing (10 mm ID × 13 mm OD, 50A, platinum-cured)

The Tiger's spout bore has an internal notch in hard plastic — a push-in seal is unreliable. Slipping a soft 10 mm ID tube over the smooth 10.5 mm OD exterior sidesteps the notch and preserves full flow area. 50A durometer for conformance; platinum-cured for bio-inertness (FDA 21 CFR 177.2600).

over: 6 mm push-in fit; wet-path standardization on 6 mm deferred to Phase 4

2026-05-27

Hot-water dispenser appliance — not a gutted-kettle PID build

A Tiger PDU-A50U (UL-listed, 5 L, four setpoints) replaces the kettle + SSR + PID concept. The appliance brings its own thermostat, thermal fuse, and dry-fire interlock, removing nearly all custom mains-electrical risk and most of the Phase 1 timeline. Trades arbitrary setpoints for fixed ones — 196 °F is well-centered for AeroPress.

over: gutted kettle + SSR + PID (custom mains + thermal safety stack from scratch)

Founding decisions

initial

Control split: Pi for UX, ESP32-S3 for real-time

Linux can't guarantee timing — fine for a UI, dangerous for control loops. The ESP32 fails safe independently if the Pi crashes; each side develops and tests alone.

initial

Stationary chamber + 12 in actuator

One axis of motion instead of two. The rotating dual-position plate isolated press hardware from coffee nicely, but cost a rotation motor, precision indexing, a thrust bearing, and a press-force lockdown. Cleanliness comes from removable parts + a cleaning cycle instead. A short-stroke chamber-drop slide is the escape hatch if cleaning access proves inadequate.

over: rotating two-station plate

initial

No stir mechanism

Bloom phase (20–30% pour, 30–45 s pause), pulsed dispense bursts, and the off-axis pour's swirl replace stirring at zero hardware cost — all tunable per recipe and loggable for A/B tests.

initial

Gravity-fed boiler + NC solenoid valve for dispense

Simpler, silent, instant cutoff, fails closed. (Phase 1's Tiger uses its own internal pump — this decision governs the eventual integrated machine.)

over: pumped dispense

initial

Blade grinder + time-based dosing for v1

Vastly easier than motorizing a burr grinder. Accepts grind inconsistency and ±1 g dose variance; load cell upgrade fixes dosing later.

initial

Manual filter loading

Automating it is disproportionately hard. One quick human step is the accepted compromise.

initial

ESPHome over custom firmware

Built-in PID, interlocks, OTA, native API; YAML iterates faster than C++. Migrate only if genuinely outgrown.

initial

Food-safe materials in the wet path; FDM for structure only

Layer lines trap bacteria and coffee oils. Stainless / glass / silicone / AeroPress polypropylene where water or coffee flows; PETG (never PLA near heat) for brackets; water-contact aluminum must be Type II anodized.

Reference

Hardware & BOM

Phase 1 bill of materials — ~$435 from scratch, ~$325–365 net of already-owned tools. No long-lead items; everything ships in 1–3 days.

Phase 1 BOM

ItemNotesCost
Tiger PDU-A50U-K water boiler, 5 LUL-listed · setpoints 208/194/176/158 °F · ~16 brews per refill$160
Hosyond ESP32-S3-WROOM-1-N16R8, 3-pack16 MB flash / 8 MB PSRAM · one primary + two spares$30
USB-C data cable + 5 V/3 A supplydata-capable cable — not charge-only$20
MG90S metal-gear servo, 4-pack2 in use + 2 spares · metal gears for press-cycle durability$15
5 V/2 A servo supplyseparate from ESP32 rail · common ground$10
PETG servo bracketprinted in-house · clips on, no fasteners
Proto board, breadboard, wire, headers, heat-shrink22 AWG hookup assortment, perma-proto$39
DS18B20 probes ×2 + pull-up + tape + insulationnow spare parts — monitoring dropped for v1$26
Silicone tubing, 10 mm ID × 13 mm OD, 3–5 ftMcMaster high-temp “Soft” 50A, platinum-cured, FDA-compliant$15
Kitchen scale (0.1 g), cylinder, catch vessel1 g ≈ 1 mL — the scale beats a graduated cylinder$35
Inline GFCI (15 A), surge strip, Class C extinguisherrequired even with a UL-listed appliance$85
Total (from scratch)~$325–365 net of owned tools + extinguisher~$435

Deferred to later phases

ItemPhase
12 V linear actuator (12 in, ~100 lb), BTS7960 H-bridge, 12 V/10 A PSU, limit switches ×2, coupler, cradle, baseplate2
Blade grinder, motor relay/SSR, hopper, chute3
Frame, funnel (CNC stainless), plumbing standardization4
Diaphragm spray pump, hollow-cone nozzle, MOSFET driver, waste reservoir + level sensor5
Raspberry Pi + touchscreen6
Load cell + HX711 (closed-loop dispense + by-weight dosing)upgrade

Material strategy

Hard ruleNo FDM-printed part in the wet path, ever. Layer lines trap bacteria and coffee oils that cannot be cleaned out.
Part typeMaterial / process
Food-contact (funnel, fittings)CNC stainless (PCBWay, Xometry), off-the-shelf
Brew chamberAeroPress polypropylene — off-the-shelf
Structural platesLaser-cut/bent aluminum (SendCutSend) · Type II anodized if water-contact
Brackets, cradles, shields, trays3D-printed PETG (never PLA near heat — softens ~140 °F)
ReservoirsGlass or food-grade HDPE, off-the-shelf
TubingFood-grade high-temp silicone, platinum-cured
SealsFood-grade silicone sheet, cut to fit

Reference

ESP32-S3 pinout

ESP32-S3-WROOM-1-N16R8 on the Hosyond carrier — 16 MB flash, 8 MB octal PSRAM. The LEDC peripheral generates clean 50 Hz servo PWM on any general-purpose GPIO.

In use — Phase 1

GPIOFunctionNotes
GPIO5Servo PWM — Dispense50 Hz LEDC → MG90S signal · servo_dispense_pwm
GPIO6Servo PWM — Unlock50 Hz LEDC → MG90S signal · servo_unlock_pwm
GPIO4DS18B20 OneWire — freedtemp monitoring dropped for v1 (2026-06-08)

Reserved for later phases tentative

GPIOFunctionPhase
GPIO9 / 10 / 11H-bridge: direction ×2 + enable/speed2
GPIO12 / 13Limit switches top / bottom (INPUT_PULLUP)2
GPIO14Grinder relay3
GPIO15Spray pump MOSFET5
GPIO16, 17, 18, 21, 38–41, 47, 48Free / future (waste float, load cell, …)

Restricted — do not use

Pin(s)Why
GPIO0Strapping (boot mode) — must be HIGH at boot
GPIO3Strapping (JTAG vs USB) — set by USB hardware
GPIO19, GPIO20Native USB D− / D+
GPIO26–32Internal flash interface — not exposed
GPIO33–37Octal PSRAM bus (N16R8) — not exposed
GPIO45, GPIO46Strapping pins — avoid
Boot / fail-safe behavior. Servos initialize to rest (level 0.0 = LEDC idle duty = power-on default) with restore: false; a reboot, brownout, or crash can never press a button. There is no electrical path from the ESP32 to the dispenser at all — a servo arm pressing a plastic button transfers zero volts.

Reference

Safety constraints

Mains power plus water demands multiple independent layers of protection. Hardware interlocks back up software — never the reverse. These rules are never violated, in any phase, for any convenience.

#Rule
1Never disable a hardware interlock in software for convenience
2Never rely on the ESP32 as the only thermal protection — thermostat + thermal fuse mandatory
3Never energize a heater without a functional float-switch interlock
4Never route mains AC through breadboard or hobby-grade connectors
5Always plug into a GFCI outlet
6Always bond exposed metal to mains earth
7ESP32 watchdog defaults all dangerous outputs OFF on reboot or fault
8Fail safe by default: heater off, valve closed (NC), actuator parked
9Never leave the machine unattended until many months of reliable operation
10Class C fire extinguisher within reach during all mains testing

How Phase 1 satisfies this

The only mains-connected component is a stock UL-listed appliance behind an inline GFCI, carrying its own thermostat, thermal fuse, and dry-fire interlock (rules 2, 3, 5 by construction). The ESP32 side is entirely low-voltage and mechanically isolated — servo arms pressing plastic buttons, zero electrical contact. Boot-to-rest servo behavior covers rules 7–8: every reset path leaves both arms un-pressed.

Worth restatingPhase 1's architecture removed most mains risk by design — but the rules apply unchanged when Phases 2–5 add motors, pumps, and a relay-switched grinder.

Reference

Risk register

Known failure modes, ranked by likelihood × impact, each with a mitigation. Two have already fired — and were handled the way the register said they would be.

RiskLikelihoodImpactMitigation
Mains/water safety incidentLowSevereIndependent interlocks, GFCI, earth bond, never unattended early
SSR fails closed (heater stuck on)MediumHighHeatsink; thermostat + thermal fuse backstop (Phase 1: appliance's own stack)
Dispense volume drifts fired — handledHighMediumConfirmed ~±15 mL open-loop → accepted for v1; load-cell closed loop is the fix
Dose repeatability poor (blade grinder)HighLowAccept ±1 g for v1; load cell for by-weight later
Cleaning inadequate, machine gets grossMediumMediumCleanable materials, cold spray, removable parts, weekly manual clean
Grinder EMI disrupts sensorsMediumMediumSnubbers, wiring separation, separate ground
Vibration loosens connections firing nowMediumMediumThreadlocker (unlock horn screw drifted post-reboot — fix queued), strain relief, inspection
Scope creep stalls the projectHighHighStage discipline: finish v1 before upgrades
Boiler scale buildupHighLowPeriodic descaling, documented procedure
Plunger misalignment under loadMediumMediumRigid cradle, external limit switches, careful Phase 2 tuning

Reference

Bench reference

Everything needed to drive the Phase 1 rig from a laptop. The device must be on the same LAN — this card is a cheat-sheet, not a remote control.

Device

WhatWhere
Web UI (ESPHome web_server)http://coffee-bot.local (192.168.1.19)
Native API (Pi / aioesphomeapi)coffee-bot.local:6053 · encrypted, key in secrets.yaml
OTA flashesphome run firmware/coffee-bot.yaml --device coffee-bot.local

Current calibration 2026-06-08 · persists across reboots

EntityValueMeaning
unlock_press_level0.550.65 overshot the contact arc — threadlock the horn so this holds
dispense_press_level0.45held for the full pour; minimal overtravel
flow_rate_ml_per_sec32.0steady-state at 196 °F · two-point fit
dispense_dead_time_ms1750pump spin-up + priming · keeps small bloom pours proportional
dispense_volume_ml250hold = volume/flow + dead-time ≈ 9.6 s
Tiger auto-relock~10 sfirmware's 300 ms unlock→dispense gap has ~33× margin

REST quick commands

# trigger a full unlock + dispense cycle
curl -X POST -H "Content-Length: 0" http://coffee-bot.local/button/run_brew_dispense/press

# individual presses (calibration)
curl -X POST -H "Content-Length: 0" http://coffee-bot.local/button/test_unlock_press/press
curl -X POST -H "Content-Length: 0" http://coffee-bot.local/button/test_dispense_press/press

# SAFETY: abort + park both servos at rest
curl -X POST -H "Content-Length: 0" http://coffee-bot.local/button/all_servos_rest/press

# set a number entity (note: POST needs the Content-Length header)
curl -X POST -H "Content-Length: 0" "http://coffee-bot.local/number/dispense_volume_ml/set?value=250"

Endurance run Phase 1 exit gate

scripts/endurance_run.sh                    # 50 cycles, 30 s pause (defaults)
CYCLES=5 PAUSE=15 scripts/endurance_run.sh  # short shakedown
HOST=192.168.1.19 scripts/endurance_run.sh  # if mDNS is being mDNS
Before runningRecirculation is assumed: route the dispense tube back into the Tiger's fill opening so tank level stays constant — 50 × 250 mL ≈ 12.5 L otherwise. A 200 from the trigger means the command was accepted, not that water flowed (no sensor in the loop yet) — eyeball the first several cycles. Ctrl-C parks the servos cleanly; logs land in logs/endurance_*.csv.

Firmware workflow

cp firmware/secrets.yaml.example firmware/secrets.yaml   # once; fill in WiFi + keys
esphome run firmware/coffee-bot.yaml                      # first flash over USB
esphome run firmware/coffee-bot.yaml --device coffee-bot.local   # OTA thereafter

Safety conventions baked into firmware/coffee-bot.yaml: rest = level 0.0 = boot default (Option A), restore: false, no auto_detach_time (a floppy arm could drift onto a button; a mid-pour detach would cut the dispense), mode: single on brew_dispense so overlapping triggers are ignored, and an on_boot rest re-assert as belt-and-suspenders.