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 monochrome front panel for control and Home Assistant for brew history and graphs. Built in nine strictly-gated phases: no phase starts until the previous one survives its endurance test.

The Phase 2 press rig — plunger and coupler over the AeroPress chamber, fed by the Tiger dispenser

Right now

THE MACHINE MADE COFFEE (08-20). First full hands-free brew: grind → pour → vacuum steep → extract → mug, one unattended chain through the fixed-plumbed brim. Slightly weak but good — recipe tuning begins. The entry.
Real frame + trimmed plunger (08-31). Butcher-block base, 15 in shelf around the actuator column, grinder and boiler mounted on it; the Ø84 plunger lip is trimmed flush, closing the issue that aborted the first eject. The entry.
Open items. Full-depth clearance descent, contact-eject re-tune, grounds-path v3 (shallower ~45° entry, ~Ø23–26 stainless — cardstock A/B first), 20-dose repeatability gate, then Phase 2's 50-cycle endurance. Phase 3 · Phase 2.
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

The ESP32-S3 is the whole machine — state machine, all I/O, safety interlocks, recipe presets, and the front panel. Home Assistant (optional, any always-on box) connects over the native API for brew history, graphs, and dashboards with zero custom code; the machine is fully functional without it. The originally planned Raspberry Pi + custom web app was dropped 2026-08-16 — deterministic, fail-safe control was always on the microcontroller, and HA covers everything the Pi was for.

Home Assistant (optional) brew history · graphs · dashboards native-API recorder, zero custom code Phase 6 — observer only; the machine is fully functional without it ESP32-S3 · ESPHome brew/clean state machine all hardware I/O · interlocks fails safe alone — outputs default OFF, runs standalone; HA only observes WiFi · native API (+ REST debug) Front panel + phone OLED · Brew/Clean buttons + ESPHome web page :80 Phase 6 — buttons/SPI on the ESP32; web page already live on the bench 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.

  • 2026-06-10

    Unlock press regressed — second geometry walk in a week; printed clamp-over horn extension under consideration. Site launched at coffeebot.alexmeub.com.

  • 2026-06-11

    Phase 1 exit gate passed: 50/50 endurance cycles, zero failures, zero interventions. Same day: unlock press re-landed at 0.53; recirc back-pressure leak at the spout gap found and sealed.

  • 2026-06-13

    Phase 1 fully closed; Phase 2 kicked off. Flow sweep found ~65 mL tank-level drift → fix with app-side tank-level compensation + fill bar (and/or the load cell). Drive-chain parts ordered.

  • 2026-06-18

    Phase 2 step 1 done: actuator extends/retracts full-stroke under ESP32 control via the BTS7960, Stop halts instantly. Firmware flashed (ESPHome 2026.6.1, OTA).

  • 2026-06-23

    Phase 2 step 3 done: plunger aligned, press pushes through the chamber with no binding. MVP goes open-loop (limit switches deferred).

  • 2026-06-24

    Press cycle calibrated dry; brew/clean sequence drafted. Open-loop timed press scripts + bench buttons.

  • 2026-06-25

    First real brew works. Fast seal plunge + ~5 mm backoff pulls a vacuum that holds the water through a 90 s steep; slow extract presses it through; puck ejects by contact (pneumatic eject didn't pan out).

  • 2026-08-06

    Phase 3 kicked off in parallel: Baratza Encore replaces the gutted-blade-grinder plan — appliance untouched, mains switched by an enclosed relay box. Restart-on-power-apply verified; timed-dose firmware flashed.

  • 2026-08-14

    Grinder power control proven: relay wired, full bring-up passed — boot-safe, 75 s watchdog, mid-dose abort. Grounds drawer printed; spout → air gap → catch-funnel delivery design drawn.

  • 2026-08-16

    Architecture simplified: Raspberry Pi + custom web app dropped. Home Assistant takes history/graphs over the native API; Phase 6 re-scoped to firmware (recipe presets, tank compensation) + a monochrome front panel (SSD1322 OLED + Brew/Clean buttons).

  • 2026-08-16

    Dose calibrated by weight: 1.50 g/s at grind setting 14 with RDTgrind_dur_ms 10667 ms, validation dose 16.0 g dead-on, 0.0 g spread on every calibration pair. Static beaten by adopting RDT (spritz the hopper beans) as part of the routine.

  • 2026-08-17

    Spout v2 validated (Ø26 exit, steady stream) — delivered rate 1.40 g/s, grind_dur_ms 11400. The lesson: over-spritzing cakes the path — RDT is 1–2 mists per fill, only.

  • 2026-08-20

    Rim brim built + clearance test passed. Both inputs plumbed into one printed brim (grounds socket + water port, opposite sides, 60°); the plunger passes both nozzles and the inner ring with zero contact. Press-safe — the "remove the hose before pressing" era is over.

  • 2026-08-20

    THE MACHINE MADE COFFEE. First full hands-free brew cycle — grind, pour, vacuum steep, extract, mug — one unattended chain. Slightly weak but good. New issue at eject: the plunger's Ø84 lip grazes the port towers at deep press → fix is trimming the lip into the seal's proven-clear column.

Full build log →

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-08-31 — 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, hardware/phase2-bom.md, hardware/phase3-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.”

Working log

Build log

Chronological record of the build — progress, issues, and the calls made along the way. Newest first. The overview timeline shows highlights; this is the full story.

  • 2026-08-31progress

    Real frame, trimmed plunger — and a rethink of how grounds get delivered

    The rig graduated from "parts arranged on a bench" to an actual machine: a butcher-block base with a 15 in shelf wrapping the main 2×4 actuator column, with the Encore and the Tiger now sitting securely on the shelf instead of loose alongside. The plunger lip is trimmed flush to the barrel, closing the issue that aborted the first brew's eject — the Ø84 second keep-out is gone and the plunger is a plain ≤Ø64 column for its whole length. Two things are still owed before any powered press: the full-depth clearance descent (the 08-20 test only went just past the brim ring) and the contact-eject re-tune.

    The extra height reopens the grounds path. The as-built brim enters at 60°, and the working theory for grounds escaping the chamber was that they exit with too much lateral speed, cross the bore, and ricochet off the far wall. Counterintuitively the fix is a shallower entry (~45°): drop height is fixed by the shelf, so a shallower chute is a longer one, friction eats more energy, and grounds arrive slower and land instead of bouncing. The floor is coffee's friction angle — below ~40° it stops sliding reliably, more so RDT-damp. A polished stainless tube is the likely material: lower wall friction than PETG, no static once bonded to the frame (the old "grounded stainless liner" idea, promoted to being the whole tube), and genuinely cleanable. Flexible food-grade hose was considered and rejected — any unsupported span sags into a valley that retains a variable 0.5–1 g, and silicone is both tacky and strongly static-generating, the worst surface for the problem RDT exists to fight.

    On diameter, the arithmetic says smaller isn't the lever. 16 g at 1.40 g/s is only ~4 mL/s of volumetric flow, so the tube runs under 5 % full at any bore in contention — the grounds slide as a thin ribbon on the bottom wall, and bore size barely changes the stream's shape. It changes clog risk, and bridging risk compounds with shallow angle, so shallow + narrow + damp is the one combination to avoid. Ø26 is the proven number; the target is ~Ø23–26 ID (1 in OD × 0.049 in wall stainless lands at ~Ø23), and the collimating gets done at the exit — last section close to the rim, aimed at the near half of the bore — rather than by squeezing the whole run. A low shroud outside the Ø66 keep-out, now affordable with the lip trimmed, catches whatever still bounces. The reference build that prompted the shallow-angle idea turns out to use a wide, flat ~20–25° ramp into a generous collar — containment at the receiving end, not a narrow stream. It's written up in full on the new prior art page, which is worth reading for a second reason: their actuator runs closed-loop on a potentiometer, with press and retract entered as absolute positions in inches rather than as durations. Next step is deliberately cheap: cardstock tubes at Ø20 and Ø26, taped at 45°, one 16 g dose through each, weigh and watch.

  • 2026-08-20milestone

    THE MACHINE MADE COFFEE — first full hands-free brew cycle

    The complete cycle ran end-to-end with no hands on the machine — the only human steps were the by-design ones (filter + cap, mug down, beans in). Grind Dose sent ~16 g through the brim; Brew Dispense poured 250 mL through the water port with the offset swirl; Brew Press chased the pour with the fast seal plunge, and the vacuum held through the full 90 s steep with both lines permanently plumbed — zero drips; then the slow extract pressed a real cup of coffee into the mug. Every subsystem the project has built — servo dispense, timed grind dosing, the press's 3-phase vacuum trick, the brim's dual plumbing — worked in one unattended chain. Verdict on the cup: slightly weak but good; next brew goes finer (setting 13) and presses closer to the hiss.

    One new issue surfaced at eject: the plunger's Ø84 hand-grip lip sweeps far outside the seal's Ø63–64 column, and at deep press it descends to brim level — where the port towers (75 / 50 mm tall) live. It grazed the grounds tower at the end of the extract; eject was aborted and the puck hand-ejected, no damage. The brim can't be reprinted around this (no boss height survives a lip that lands on the brim), but the fix is free: trim the lip flush to the barrel — the clearance test already proved the seal's column clears everything at full stroke, and a trimmed lip lives inside that proven column. The actuator mounts inside the plunger, so nothing depends on the lip. After the trim: full-depth clearance re-check, re-tune the contact-eject distance, then the repeatability gate.

  • 2026-08-20milestone

    Rim brim built, fully plumbed, and press-safe — clearance test passed

    The chamber-end design skipped a generation: instead of the single-purpose telescope receiver, a rim brim hosting both inputs got printed — grounds port and water port on opposite sides of the chamber mouth, both at 60°. The spout sockets directly into the Ø30 grounds port (hard joint; the Ø38–40 telescope with an air gap stays the v2 option if removal gets annoying), and the water port wraps the Ø13 silicone line from the Tiger — the tube's own compliance is the vibration decoupling on that side. Opposite-side entry delivers the offset-pour swirl the no-stirring design counts on. This is the Phase 4 "rim manifold" arriving a phase early, and it closes the oldest integration to-do: the dispense line no longer comes off by hand before pressing. Photos in the gallery.

    Clearance test passed (08-20): with everything plumbed, the plunger jog-stepped down from home — 8 s approach at 30 %, then 1.5 s jogs through the critical zone — and the seal passed both port nozzles and the brim's inner ring with zero contact, into the bore. The brim is a fixed, press-safe part: nothing gets removed for a press. Next up, the remaining shakedown: dose through the brim, water through the brim (swirl + splash check at 32 mL/s), then the first full hands-free brew cycle.

  • 2026-08-17progress

    Spout v2 delivers — and teaches the RDT dosage lesson

    The drawer spout is printed and in place: Ø26 ID / Ø30 OD round exit lofted from the rectangular drawer mouth (Fusion lesson re-learned: Shell fails on lofts — build hollow ducts as two lofts and Combine-Cut). Ø26 flows a steady stream with no bridging, even with RDT-damp grounds. But the session turned into a diagnosis: delivered weight declined dose-over-dose (1.41 → 1.23 g/s) until the culprit surfaced — over-spritzing. Repeated RDT on the same hopper load cakes damp fines at the spout entrance, and a loosely-seated spout rattling under grinder vibration made it worse. New rules: RDT is 1–2 light mists per hopper fill, at fill time only; seat the spout firmly; when the rate droops, brush the grinder exit + spout entrance and re-baseline with a 10 s dose.

    After cleanup the path settled at ~1.40 g/s delivered (vs 1.50 caught at the drawer) — grind_dur_ms is now 11400 ms for a 16 g dose. The honest envelope for time-based dosing is ~±10% across bean, moisture, and cleanliness — exactly the ±1 g the design budgeted, and exactly why the load cell stays the by-weight endgame. Chamber-end design also firmed up: the catch funnel evolves into an angled telescope receiver on the rim collar (Ø40 ID, Ø46 flared mouth, 12–15 mm overlap, ~5 mm air gap) — enclosed against static scatter but never touching the spout, keep-out column untouched. That's the next print.

  • 2026-08-16milestone

    Dose calibrated by weight — 1.50 g/s at setting 14, validated dead-on

    Live calibration session over REST: 10 s doses fired remotely, weighed on the 0.1 g scale between runs. First finding — static is a real problem, so RDT is now part of the loading routine (a light water spritz on the hopper beans); it killed the cling and the variance with it. The dry primer dose on empty burrs ran 3 g light and was dropped as an outlier. At the dial's starting position (21): 18.0 g and 18.0 g — 1.80 g/s. Then the dial went to 14, the AeroPress baseline, turned mid-spin per Baratza's rule, and the finer grind fed slower: 15.0 g and 15.0 g — 1.50 g/s, with 0.0 g spread on every pair, well inside the ±1 g budget.

    grind_dur_ms = target grams ÷ 1.5 × 1000 → set to 10667 ms for a 16 g dose (1:15.6 with the 250 mL pour) and the validation dose landed 16.0 g exactly. The rate is tied to setting 14 + RDT — any dial move or skipped spritz means a re-fit (two 10 s doses, minutes of work). Bonus: RDT may make the grounded stainless chute liner unnecessary — a candidate simplification for the Phase 4 rim collar. Remaining in Phase 3: the 20-dose repeatability gate and the spout + catch-funnel prototypes.

  • 2026-08-16decision

    Architecture simplified — Raspberry Pi and custom web app dropped, Home Assistant instead

    The planned Pi + FastAPI/SQLite/HTMX web app is out. The realization: the machine has run Pi-less through the entire bench program — the ESP32 already owns all control and every calibrated parameter — and the Pi's remaining value (brew history, graphs) is exactly what Home Assistant provides natively over the ESPHome API, with zero custom code. Phase 6 re-scoped to firmware + an HA hookup: recipe presets, tank-level tracking with feed-forward dispense compensation (the ~65 mL drift fix moves on-device), optional scheduling, and a monochrome front panel — SSD1322 256×64 OLED + physical Brew/Clean buttons, rotary encoder a maybe (soft call, same day). The Phase 7 touchscreen kiosk retires with the Pi. Nothing in firmware assumes HA exists, so the machine stays fully functional standalone — and the old plan stays documented if it's ever revived.

  • 2026-08-14milestone

    Grinder power control proven — relay bring-up passes every check

    The relay box arrived and went straight through bring-up: GPIO14 → opto-isolated control input, Encore's front switch latched ON into the "normally OFF" outlet. Tested lamp-first: manual toggle, a 5 s timed dose that shut itself off, and dark through a physical ESP32 reset (the boot-safety check that matters). The 75 s watchdog fired on cue — relay forced off with the ERROR line captured in the event stream. Then with the Encore in: clean 2 s spin, and Grinder Stop killed a 10 s dose at ~3 s with the relay staying off past the dose's would-be end — the stopped script's tail can't re-fire it. No mains ever touches the bench; the enclosed relay box is the only thing that sees 120 V.

    Also this week: the grounds drawer is printed and fits the Encore's bin recess — first physical piece of the delivery path — and the delivery design is split into drawer spout → 10–15 mm air gap → gravity-seated catch funnel on the chamber rim (docs/phase3-spout-catch-concept.svg; the funnel lifts off by hand for now, its Phase 4 successor is the zero-overhang rim collar). Next: dose calibration by weight (3× 10 s doses, fit g/s), then the spout + funnel prototypes.

  • 2026-08-06decision

    Phase 3 kicked off — Baratza Encore, appliance untouched

    Phase 3 starts in parallel with Phase 2's remaining wet re-tunes, and the plan's gutted blade grinder is out: the Baratza Encore is in — burr-quality grind from day one, with the Tiger philosophy intact: the appliance stays electrically untouched. Its front switch latches ON and the ESP32 switches the grinder's mains upstream through an enclosed opto-isolated relay box (Adafruit #2935, the rebranded DLI IoT Power Relay) for time-based dosing. The load-bearing assumption — the Encore restarts when power is applied — verified by hand: connect = grind, disconnect = stop, repeatable.

    Firmware shipped the same day: grinder_power (boot-forced OFF), grind_dur_ms, Grind Dose / Grinder Stop buttons, and a 75 s watchdog armed on every on-path. Grounds-delivery concept drawn up: printed carrier + grounded stainless liner against static (docs/phase3-encore-chute-concept.svg).

  • 2026-06-25milestone

    First real brew — seal-suction holds the water; 3-phase press validated

    Filled the AeroPress from the Tiger dispense (250 mL) and brewed end-to-end with the new 3-phase Brew Press. The breakthrough: a fast seal plunge (6000 ms @ 70 %) + a ~5 mm backoff pulls a vacuum that held the water through a full 90 s steep with no overflow — without it the paper filter just drains the pour. That suction-hold is what makes a stationary-chamber auto-AeroPress actually work. The slow extract pressed the coffee through; the hiss came at ~26000 ms (Press Duration set to 20000, just short of it).

    Eject finding: the planned pneumatic eject (trapped air pops the puck, plunger stays clean) doesn't work — by eject time the water's pressed through, so the spent damp-grounds puck won't seal the bore and the air just leaks past instead of pushing. Contact ejection is the answer: drive the seal down to push the puck out, accept a little coffee on the silicone seal (the Phase 5 spray clean handles it). This makes the "stop before the hiss for an air gap" idea moot, so the extract can press closer to the hiss for fuller extraction next time. press_eject_ms needs a wet re-tune as a contact-push distance.

  • 2026-06-24decision

    Press cycle calibrated (dry) + the real brew/clean sequence nailed down

    Wrote the open-loop timed press-cycle firmware (home / descend / press / eject scripts, tunable durations, bench buttons, boot-safe, Stop aborts all) and calibrated it dry against the bench: approach 12000 ms lands the plunger right at the chamber mouth, eject 36000 ms drives through and out the bottom (clean pass), home 50000 ms full return — and the full cycle chains cleanly. All times are tied to the 30 % travel speed (re-scale if that changes).

    The real sequence is two stages with a manual step between, not one auto cycle: (1) Brew Press — descend and press to ~80 % / the AeroPress hiss, then stop, leaving an air gap above the puck; (2) the user swaps mug → waste cup and removes the filter cap + bottom plate; (3) Eject & Clean — press a short bit further so the trapped air pneumatically pops the puck out the open bottom, and the seal never touches the grounds — the plunger stays clean. The "hiss" can't be sensed open-loop (approximated by a calibrated depth; current-sense is the honest fix later), and the real eject is a small partial press, not a drive through — so the auto "Full Cycle" is now just a bench-test button. Press Duration and the partial eject are wet-tuning tasks (need a real brew to press against). Buttons relabeled Brew Press / Eject & Clean / Home Plunger.

  • 2026-06-23milestone

    Plunger aligned, press pushes through clean — Phase 2 step 3 done

    Mount, cradle, and plunger coupler built from printed parts; the plunger is aligned to the chamber and dry test push-throughs run under actuator control with no binding. Alignment was solved by fitting the existing printed parts — after a good run through the options: an offset clevis-hole adapter (dropped — the actuator rod can spin, so a fixed offset would point in a random direction), a single-axis slotted alignment plate + shim, and a funnel + floating coupler for mechanical self-centering. The slotted-plate and funnel/float concepts are saved in docs/ as fallbacks if alignment ever drifts.

    MVP decision: no external limit switches (step 2 deferred). The press runs open-loop — actuator ends are known via its internal limits, press depth is a timed move homed from full retract. Current sensing on the BTS7960 is the cleaner future upgrade for end-of-press + over-force.

    Integration to-do (Phase 4): the dispense hose currently has to be pulled by hand before pressing or it blocks the plunger. The fix is the documented offset pour — mount the hose at a fixed offset, angled into the chamber beside the plunger's column, so nothing moves out of the way (and the off-center pour adds extraction swirl). Next on the bench: press-profile tuning (step 4) and puck ejection (step 5).

  • 2026-06-18milestone

    Actuator drive works — Phase 2 step 1 done

    The whole press drive chain is proven end to end: ESP32 → BTS7960 H-bridge → Firgelli 150 lb / 12 in actuator off the 12 V brick. Wired RPWM/GPIO9 (extend), LPWM/GPIO10 (retract), the tied enables on GPIO11 (boots LOW, fail-safe), and common ground to the ESP32; the brick is spliced straight in (no barrel jack on hand) through the 10 A fuse + kill switch. The 12 V supply was a splice job — verify polarity with a meter, never trust the wire markings.

    Firmware added to coffee-bot.yaml (boot-disabled enable, never-both-directions scripts, jog + run + stop bench buttons) and flashed OTA via a freshly-installed ESPHome 2026.6.1. Result: full-stroke extend and retract under web-UI control, internal end-stops hold both ends, and Actuator Stop halts it instantly mid-travel. No brownout — the 12.5 A brick swamps the 8 A surge. Phase 1 servos untouched. Next: measure the actuator, build the stand, fit the limit switches (step 2).

  • 2026-06-13decision

    Flow sweep: tank-level drift is the real volume error — fix it in software

    Ran the step-8 full→¼ tank sweep (15 weighed pours, logs/flow_sweep_20260613.csv) — the last Phase 1 loose end. Result: pour volume drifts ~65 mL from full (~312 mL) to ¼ tank (~247 mL), roughly linear at ~17 mL/L drained. That's ~4× the ±15 mL pour-to-pour spec, so the dominant volume error isn't pump spin-up noise — it's tank level. And since ¼ tank is the normal refill point, that whole span is the real operating envelope. The drift is at least systematic and predictable: over-pours when freshly filled, drifts to on-target by refill time.

    The fix is not re-tuning the open-loop model (it's already stale — upper-tank pours read ~37 mL above the 06-05 calibration, the 06-11 spout/hose rework shifted the plumbing). Two layered fixes instead: (1) app-side feed-forward compensation — the Pi already commands every pour, so it tracks water dispensed since refill, estimates tank level, and adjusts the per-pour dispense time off this measured drift curve; cancels the systematic ~65 mL, leaving ~±15 mL, and gives a tank fill bar + refill prediction in the UI for free. (2) load-cell closed loop — stop the pour at target weight; kills both error sources to ±5 mL and enables by-weight dosing. They stack (feed-forward + feedback), and the load cell stays a deferred upgrade since software comp covers v1. (2026-08-16: the Pi was later dropped — this compensation now lives in ESP32 firmware; same math, different host.)

    With this, native-button check, and threadlock all done today, every Phase 1 step is resolved and Phase 2 (press mechanism) is the active phase — drive-chain parts ordered.

  • 2026-06-13milestone

    Phase 2 kicked off — drive chain ordered

    BOM written (hardware/phase2-bom.md) and the drive chain ordered: Firgelli 150 lb / 12 V / 12 in actuator (0.30 in/s, IP66, 5 A draw / 8 A surge), BTS7960 (IBT-2) 43 A H-bridge with fail-safe enable wiring, a 12 V / 12.5 A UL-listed sealed brick sized for the surge, and 16 AWG power wire. Base is a butcher-block + braced 2×4/plywood bench fixture (aluminum baseplate deferred to Phase 4 / wet path). Safety adds the scope didn't list: inline 10 A fuse + a 12 V kill switch — this phase's hazard is mechanical crush, not electrical.

  • 2026-06-11milestone

    Endurance run passed 50/50 — Phase 1 exit gate cleared

    50 consecutive brew_dispense cycles, zero failures, zero manual interventions, zero safety events — the Phase 1 exit criterion, met. Single ~1 h session over the recirculation loop (20:30–21:28, 43 s/cycle cadence), all 50 REST triggers accepted, servos parked clean, bench dry and tank level constant at the end. Log: logs/endurance_20260611_203015.csv. The unlock press (re-landed at 0.53 the same day) held its geometry through all 50 presses — no drift.

    Harness gotcha for the record: cycles 16–20 stalled for up to 18 min when the bench Mac napped and suspended the script mid-run. Fix: pin the Mac awake with caffeinate -i -w <script pid> (now noted in the script header). The stalls were idle gaps between cycles — each dispense itself was unaffected.

    Phase 1 is done. Loose ends carried forward: confirm the Tiger's native buttons still work under the bracket (step 5), confirm threadlock on the unlock horn, and decide whether the full→¼ tank flow sweep (step 8) still matters now that ±15 mL is the accepted v1 tolerance. Next: Phase 2 — the press mechanism.

  • 2026-06-11issue

    Recirc back-pressure found a gap in the Tiger's spout

    First smoke cycle of the recirc loop leaked: the uphill tube run to the fill opening puts static head on the spout (the first back-pressure it has ever seen — open-air dispensing never pressurizes it), and water exited through a thin gap along the back of the nozzle that the slip-over hose seal couldn't cover. Silicone caulk was considered and rejected (not food-safe, and it would glue the spring-loaded spout solid).

    Fix: relieved the housing plastic around the spout with a Dremel for hose clearance, dealt with the flared features on the nozzle that broke the seal line, and zip-tied the silicone hose over the sealed zone. Smoke cycle after the fix: clean, no leaks, return flowing into the fill opening. Trade recorded: the Tiger is no longer cosmetically unmodified — the spout housing is permanently relieved. Electrical isolation and the appliance's internal safety stack are untouched.

    Same session: the 06-10 unlock regression was resolved by re-tuning — unlock press re-landed at 0.53 (third value: 0.65 → 0.55 → 0.53).

  • 2026-06-10issue

    Unlock press regressed — second geometry walk in a week

    The unlock servo stopped landing its press again (same mode as the 0.65 → 0.55 drift on 06-05; threadlock was still pending). Diagnosis fork: hand-press the button (rules out the Tiger side) → fire Test Unlock Press and watch where the tip lands — short/off-center means the horn screw backed out or the horn re-seated a spline tooth off (~18°/tooth ≈ 0.2 level units, much bigger than the earlier drift); skating past the button means the sweet spot moved again; square press but no unlock means dwell/depth, not geometry. Also check the bracket hasn't shifted on the panel.

    Likely fix beyond re-tuning: a 3D-printed clamp-over extension on the stock horn (don't print the spline — FDM won't hold the 20-tooth 4.8 mm spline). Paddle or cupped face larger than the button so small drift still presses center-ish; ideally a constant-radius cam face so overtravel adds preload instead of skating off. Add the silicone pad the pin-assignments doc already planned. Threadlock the screw the same session, before the endurance run.

    Same-day pre-flight while diagnosing: board up, all five calibration numbers persisted (250 / 32.0 / 1750 / 0.45 / 0.55). Gotcha found: curl on the bench Mac intermittently fails to resolve coffee-bot.local (ping resolves fine) — run the endurance script with HOST=192.168.1.19.

  • 2026-06-10milestone

    Project site launched — coffeebot.alexmeub.com

    This site: phases 0–8 with sub-step statuses, per-phase wiring diagrams, decisions log, pinout/BOM/safety/risk reference, bench cheat-sheet. Static single file, S3 + CloudFront, deployed via webapp/deploy.sh.

  • 2026-06-09progress

    Endurance harness written + smoke-tested

    scripts/endurance_run.sh: fires N brew_dispense cycles over REST with CSV logging, pre-flight reachability check, consecutive-failure abort, Ctrl-C parks the servos. Assumes a recirculation loop (spout tube routed back into the Tiger's fill opening) so 50 cycles don't need 12.5 L of catch vessel. Smoke test: cycle 1 ok.

  • 2026-06-08decision

    Two v1 calls: DS18B20 dropped · ±15 mL accepted

    Temperature monitoring removed from firmware — the UL-listed Tiger self-regulates and nothing in the dispense path consumes the reading (the bench probe had also died after a water dunk). And the measured ~±15 mL open-loop pour spread is accepted for v1: it's pump spin-up variance + tank drift, untunable by timing, ~6% of the water. ±5 mL deferred to a load-cell + HX711 closed loop. Details in the decisions log.

  • 2026-06-05progress

    Firmware live — dispense path works end to end

    Compiled (ESPHome 2026.5.2), flashed over UART, on WiFi at coffee-bot.local; OTA from here on. Servos calibrated in-place against the Tiger's buttons (unlock 0.55, dispense 0.45 — 0.65 overshot the unlock contact arc after a reboot). Dead-time dispense model fitted: 32.0 mL/s + 1750 ms, centered ~250 mL. Auto-relock measured ~10 s — the 300 ms unlock→dispense gap has ~33× margin. Gotcha of the day: servo PSU ground must tie to ESP32 ground — without it both servos spin continuously.

  • 2026-06-03progress

    Servo torque validated · bracket design begun

    Both MG90S units press the Tiger's buttons with force to spare (standalone servo-tester rig) — the button-press architecture is feasible before committing to bracket design. Parametric bracket modeling started in Fusion 360; decided on stock horns with arc-tuned press geometry.

  • 2026-05-31decision

    Servo button-press interface · slip-over plumbing

    Servos pressing buttons externally beat optocouplers soldered across the button taps: no teardown, no warranty risk, fully reversible. Plumbing switched to 10 mm ID silicone slipped over the spout exterior — the spout bore's internal notch made a push-in seal unreliable.

  • 2026-05-27decision

    Phase 1 pivot: Tiger PDU-A50U replaces the gutted-kettle PID

    A UL-listed appliance takes over all thermal regulation and mains safety — trades arbitrary setpoints for fixed ones (196 °F is well-centered for AeroPress) and cuts most of the Phase 1 electrical risk and timeline.

Photos

Build photos

The build in pictures, oldest first — from Phase 1 wiring to the fully-plumbed delivery train. Click any photo to zoom (arrow keys navigate). Originals plus a couple of clips live in the repo's images/build/ folder.

Phase 1 bench — ESP32-S3 + servo wiring on the mat, Tiger standing by.
The 06-11 leak fix — housing relieved so the silicone line seals past the nozzle gap.
Servo bracket clipped over the Tiger's panel; dispense line zip-tied over the sealed zone.
Recirc loop for the endurance run — the pour returns to the fill opening, so 50 cycles don't need 12.5 L.
Phase 2 bring-up (06-18) — actuator clamped down, BTS7960 + 12 V brick wired, first strokes.
Press stand going up — 2×4 post squared to the butcher-block base.
Stand detail — angle-bracket brace; 12 V brick and H-bridge tucked below deck.
Actuator strapped to the post with printed clamps, plunger over the chamber (06-23).
Printed cradle bolted to the base, chamber seated on the press axis.
Dry press stroke — plunger descending the bore with no binding.
Brew-ready column — plunger + coupler over the chamber, dispense line staged from the Tiger.
After the first real brew (06-25) — wet grounds at the chamber bottom, seated in the printed base.
Wet-session staging (06-25) — plunger homed above the mouth, line ready to fill.
The rim brim plumbed (08-18) — spout socketed in one 60° port, boiler line in the other, plunger on axis above.
Full delivery train standing (08-18) — Encore + spout, Tiger + line, brim, chamber, mug.
Grinder-to-chamber path (08-18) — spout at 60° into the grounds port; water port opposite for the swirl.

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 · complete

Water heating + dispense

passed 2026-06-1150/50 endurance cyclesdevice: 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. Met 2026-06-11: 50/50 cycles over the recirculation loop, zero failures, no leaks, no servo drift — logs/endurance_20260611_203015.csv. See the build log.

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 electrically untouched — no soldering, no teardown, its internal safety stack intact. One permanent cosmetic mod (06-11): the housing plastic around the spout was relieved with a Dremel so the recirc/dispense hose can seal past a gap in the nozzle.

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.53

Third value (0.65 → 0.55 → 0.53) after the 06-10 geometry walk. Held through all 50 endurance presses with no drift.

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.

Measured data from logs/ · re-render with scripts/render_charts.py

250 mL target (±15 band)24026028030032001000200030004000100%75%50%25%tank ≈water drained since full (mL)pour volume (mL)pours 1–2: line re-priming (excluded from fit)~63 mL drift full → ¼ tank (-18.5 mL per L drained)

Step-8 sweep (15 weighed pours, logs/flow_sweep_20260613.csv): pour volume tracks tank level almost linearly — the systematic ~65 mL drift the Phase 6 feed-forward compensation (firmware-side since the 2026-08-16 Pi drop) will cancel. Scatter at a fixed level is the accepted ±15 mL.

30s60s90s120ssteady cadence ~43 s/cycle5101520253035404550cycle # (gap since previous cycle started)18 minbench Mac fell asleep between cycles(idle gaps, not rig failures — fixed with caffeinate)5 min50/50 cycles OK · zero failures · zero interventions

Exit-gate endurance run (logs/endurance_20260611_203015.csv): 50/50 cycles at a steady ~43 s cadence. The spikes are the bench Mac napping between cycles — idle gaps, not rig failures; the harness now runs under caffeinate.

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. done

    5. Servo press calibration + validation

    Servos mounted and calibrated in-place via the web UI test buttons: dispense 0.45, unlock now at 0.53 — third value after two geometry walks (0.65 → 0.55 → 0.53; the 06-10 regression was resolved by re-tuning, 06-11). The fix held through all 50 endurance presses with no drift. Closed 2026-06-13: unlock horn threadlocked, and the Tiger's native buttons confirmed still working 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). Sealing fix (06-11): the recirc back-pressure leaked through a gap in the spout's nozzle — housing relieved for hose clearance, flares addressed, hose zip-tied over the sealed zone. No leaks through the 50-cycle run.

  8. done

    8. Flow-rate characterization

    Steady-state flow 32.0 mL/s and 1750 ms dead-time fitted at 196 °F. Full→¼ tank sweep done 2026-06-13 (15 pours, weighed; logs/flow_sweep_20260613.csv): pour volume drifts ~65 mL from full (~312 mL) to ¼ tank (~247 mL), roughly linear at ~17 mL/L drained — ~4× the ±15 mL spec, so tank level (not spin-up noise) is the dominant volume error across a refill cycle. The drift is systematic and predictable (over-pours when freshly filled, on-target near refill), and ¼ tank ≈ the normal refill point, so this span is the real operating envelope. Upper-tank pours read ~37 mL above the 06-05 calibration → the 06-11 spout/hose rework shifted the plumbing; the open-loop model is stale. Fix is not open-loop re-tuning — it's feed-forward tank-level compensation (near-term; firmware-side since the Pi was dropped 2026-08-16) and/or the load-cell closed loop (precision). See the decisions log.

  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. done

    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. Passed 2026-06-11: 50/50 cycles in a single ~1 h session over the recirc loop — zero failures, zero interventions, no leaks, no servo drift, bench dry at the end. Log: logs/endurance_20260611_203015.csv. (Run the script under caffeinate — a sleeping Mac suspends it mid-run.)

    Exit: 50 consecutive successful cycles, no manual intervention, no safety events. ✓ Phase 1 complete.

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.53 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 · current focus

Press mechanism

in progressdrive chain orderedBOM: hardware/phase2-bom.md

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 Firgelli 12 V linear actuator (12 in stroke, 150 lb force, 0.30 in/s, IP66) hangs above the cradle 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, with the enable line defaulting low at boot so the actuator can't move until firmware allows it; top and bottom limit switches feed the ESP32 as interlocks. The AeroPress sits in a rigid printed cradle on a butcher-block + braced 2×4/plywood bench fixture that has to shrug off 30–40+ lb of press force without flexing (the anodized-aluminum baseplate is deferred to Phase 4, where it joins the permanent wet path). Power is a 12 V / 12.5 A UL brick through an inline 10 A fuse and a manual kill switch.

Build sequence

  1. done

    1. Wire actuator + H-bridge + 12 V supply

    Verify extend/retract under ESP32 control. Done 2026-06-18: BTS7960 wired to GPIO9 (RPWM/extend), GPIO10 (LPWM/retract), GPIO11 (tied R_EN+L_EN, boots LOW), common ground to the ESP32; 12 V/12.5 A brick spliced through a 10 A fuse + kill switch to B+. Firmware flashed (ESPHome 2026.6.1, OTA). Actuator extends and retracts full-stroke under web-UI control, the internal end-stops hold both ends, and Actuator Stop halts it instantly mid-travel (drops the enable → motor coasts). No brownout — the 12.5 A brick has ample headroom over the 8 A surge.

  2. deferred · MVP

    2. Add limit switches as hard interlocks

    Deferred for the MVP (2026-06-23): the press runs open-loop — the actuator knows its two ends via internal limit switches, and press depth is a timed move (0.30 in/s) homed from full retract. External switches aren't wired. The cleaner future upgrade is current sensing on the BTS7960 (R_IS/L_IS) for end-of-press-by-load + over-force protection — see the build log. Drawing kept: docs/phase2-actuator-limit-switch-layout.svg.

  3. done

    3. Mount AeroPress in cradle, couple the plunger

    Verify alignment and zero binding through the full stroke. Done 2026-06-23: mount, cradle, and plunger coupler built from printed parts; plunger aligned to the chamber and dry test push-throughs run under actuator control with no binding. (Alignment was solved mechanically by fitting the existing parts — the offset-adapter idea was dropped once it was clear the actuator rod can spin, which would point a fixed offset in a random direction. The slotted-plate and funnel/float concepts in docs/ are kept as fallbacks.)

  4. in progress

    4. Tune press profile — 3-phase, wet-validated

    Validated on a real 250 mL brew (2026-06-25). The Brew Press is 3 phases: (1) Seal Plunge — fast (6000 ms @ 70 %) to ~7 mm past the mouth, to engage the seal before the filter drains; (1b) Backoff — a ~5 mm retract that pulls a vacuum holding the water through the full 90 s steep (the key enabler — without it the pour just drains out); (3) Extract — slow (30 %) press toward the hiss (found ~26000 ms; Press Duration set to 20000). "Hiss" can't be sensed open-loop (calibrated depth; current-sense is the honest detector later); the press is much slower under brew back-pressure (~1.3–2.2 mm/s vs ~3.3 free-air).

  5. in progress

    5. Puck ejection — contact push

    Revised after the wet test (2026-06-25). The planned pneumatic eject (leave an air gap, let trapped air pop the puck so the plunger stays clean) doesn't work — the spent, drained puck won't seal the bore, so the air leaks past instead of pushing. Contact ejection is the answer: drive the seal down to push the puck out the open bottom; a little coffee ends up on the food-grade silicone seal, which the Phase 5 spray clean handles. The puck did eject this way. press_eject_ms needs a wet re-tune as a contact-push distance.

  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.

Concept drawings docs/ — to scale · fallbacks + future upgrades

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 · in progress

Grinder integration

in progressgrinder: Baratza EncoreBOM: hardware/phase3-bom.md

Dose a repeatable quantity of ground coffee on command. A Baratza Encore, electrically untouched — front switch latched ON, its mains switched by the ESP32 through an enclosed relay box — with time-based dosing and grounds out a chute. The burr grinder landed early: the Encore replaced the planned gutted blade grinder, same appliance-untouched philosophy as the Tiger.

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

Build sequence

  1. done

    1. Switch the grinder's mains through an enclosed relay

    No gutting: the Encore stays electrically untouched. Its front switch latches ON and GPIO14 drives the opto-isolated control input of an enclosed relay box (Adafruit #2935 / DLI IoT Power Relay, "normally OFF" outlet) — no mains ever on the bench. Done 2026-08-14: restart-on-power-apply verified 08-06; wired and lamp-tested (toggle + 5 s timed dose), stays dark through a physical ESP32 reset.

  2. done

    2. ESP32 controls grinder run time

    A switch entity + scripted run duration, with a hard maximum runtime as a guard. Done 2026-08-14: grinder_power (boot-forced OFF), grind_dur_ms timed dose, Grind Dose / Grinder Stop buttons, 75 s watchdog armed on every on-path. Verified live: clean 2 s spin, mid-dose Stop abort with no script-tail re-fire, watchdog force-off at 75 s with the ERROR logged.

  3. done

    3. Build the time→grams curve

    Grind for N seconds, weigh the output, fit the rate. Done 2026-08-16: 1.50 g/s at grind setting 14 (AeroPress baseline) with RDTgrind_dur_ms = grams ÷ 1.5 × 1000, set to 10667 ms and validated at 16.0 g dead-on. Static was a real problem → RDT adopted (light water spritz on the hopper beans at load). The rate is tied to setting 14 + RDT: setting 21 measured 1.80 g/s, so re-fit after any dial move (turn the dial only while the motor runs). Dry primer dose on empty burrs ran 3 g light — expect that after any burr-chamber cleanout.

  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. in progress

    5. Design and test the chute

    Grounds must fall cleanly without clinging: steep angle (>60°), smooth surface, removable for cleaning. Nearly done (2026-08-20): drawer + spout printed and validated (Ø26 ID / Ø30 OD round exit, steady stream, no bridging), and the chamber end skipped ahead to a rim brim hosting both inputs — grounds port (Ø30 ID, spout sockets in) and water port (wraps the Ø13 boiler line) on opposite sides at 60°, giving the offset-pour swirl for free. Plunger clearance test passed with everything plumbed — the brim is a fixed, press-safe part, and the Phase 4 "remove the hose before pressing" to-do is closed. Both ran, and the first full hands-free brew followed. Delivery v3 in design (2026-08-31): the rebuilt frame's shelf adds height, so the entry angle can go shallower (~45°) — a longer chute bleeds off speed, so grounds land instead of ricocheting off the far chamber wall — in a ~Ø23–26 polished stainless tube (low friction, no static once bonded, actually cleanable). Bore barely shapes the stream (the tube runs under 5 % full), so the collimating happens at the exit and a low shroud outside the Ø66 keep-out catches the rest; flexible hose was rejected (sag retains grounds, silicone is tacky and static-prone). Cardstock Ø20-vs-Ø26 A/B at 45° settles it before anything gets printed. (v2 option if removal gets annoying: open the grounds port to a Ø38–40 telescope with an air gap. Earlier concepts: docs/phase3-spout-catch-concept.svg, docs/phase3-encore-chute-concept.svg.)

Wiring diagram as built 2026-08-14

120 VAC (appliance cords) control signal ground
ESP32-S3 GPIO14 GND grinder_power boots OFF 75 s watchdog on every on-path IoT Power Relay Adafruit #2935 · enclosed · UL-recognized opto-isolated 3–60 VDC control · 12 A breaker "+" control "−" control normally OFF outlet always-on + normally-on outlets unused the only mains on the bench — inside its own enclosure 22 AWG twisted pair · a few mA at 3.3 V Wall → GFCI + surge relay box's own inlet cord Baratza Encore electrically untouched front switch latched ON restart-on-power-apply verified 2026-08-06 timed dose = switched mains (hot/neutral/earth via its own cord) cord

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 was a planned non-goal for v1 (blade grinder) — then the Encore-untouched design made it free, so it landed early.

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, front panel. 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 · re-scoped 2026-08-16

On-device UI + Home Assistant

plannedwas: Pi web application — dropped

The custom Raspberry Pi web app is dropped. Phase 6 is now firmware work plus a Home Assistant hookup: recipe presets, tank-level tracking with feed-forward dispense compensation, a monochrome front panel (SSD1322 OLED + physical Brew / Clean buttons), and HA — connected over the ESPHome native API — for brew history, graphs, and dashboards with zero custom code. The machine stays fully functional with HA absent.

Exit criterionTrigger a brew from the front panel with a recipe preset, and review it afterward in Home Assistant. Tank fill bar tracks refills; history persists.
Why the drop (2026-08-16): the ESP32 already owned all control and every calibrated parameter (restore_value) — the machine ran Pi-less through the entire bench program. The Pi's remaining value was history + graphs, which Home Assistant provides natively over the same API the custom app would have been built against, at a fraction of the effort. Low-risk: nothing in firmware assumes HA exists, and the old plan stays documented if it's ever revived.

Architecture re-scoped 2026-08-16

Front panel + phone SSD1322 256×64 OLED (SPI) Brew / Clean buttons rotary encoder: maybe + ESPHome web page :80 for bench / phone control ESP32-S3 · ESPHome the whole machine brew/clean state machine + interlocks recipe presets: select → number entities tank counter + feed-forward comp all parameters on-device (restore_value) fully functional standalone — no observer required Home Assistant optional · any always-on box native API :6053, auto-discovery recorder → brew history + graphs dashboards · notifications zero firmware changes, zero custom code GPIO · SPI WiFi · push command flow: front panel / web page / HA → recipe preset loads the number entities + start button → ESP32 runs the cycle state flow: ESP32 → native API push → HA recorder → history, temp curves, dashboards brew logic NEVER lives off the ESP32 — Home Assistant observes and may set parameters

Build sequence

  1. todo

    1. Recipe presets

    ESPHome select entity ("Standard / Strong / Small / …") — choosing one loads its values into the existing calibrated number entities. A handful of presets, not unlimited CRUD.

  2. todo

    2. Tank-level tracking + feed-forward compensation

    Global counter of mL dispensed since refill, a "Tank Refilled" reset button, and a lambda adjusting the dispense hold off the measured drift curve (logs/flow_sweep_20260613.csv) — cancels the systematic ~65 mL tank-level error. Fill-bar sensor entity for the web page / OLED / HA.

  3. todo

    3. Home Assistant hookup

    Point HA at the ESP32's native API (already configured with encryption). Recorder gives brew history and temp/state graphs; build one dashboard. Verify the machine is unaffected with HA down.

  4. todo

    4. Front panel

    SSD1322 OLED + physical Brew / Clean buttons; monochrome UI kept shallow (state, temp, progress, recipe pick). Rotary encoder only if it earns its place.

  5. todo

    5. Scheduling (optional)

    SNTP time + on_time automation for a wake-up brew — or just an HA automation.

Wiring

New wiring is the front panel only: SSD1322 over SPI + two buttons on spare GPIO. The Home Assistant side is pure software — it joins over WiFi against I/O 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, front panel (OLED + buttons) 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 front panel, tidy cable runs

    SSD1322 OLED + Brew / Clean buttons from Phase 6 into the enclosure face (the planned touchscreen kiosk was retired with the Pi, 2026-08-16); 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.

The target

Concept render of the finished coffee-bot machine

The concept render this phase aims at — the machine the bench rig grows into.

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-08-31

Grounds path goes stainless — printed parts demoted to structure

The drawer adapter needs reprinting in something food-safe, but not like-for-like. The motivation is evidence rather than principle: the 08-17 session found damp fines caking at the spout entrance, and that retention — damp grounds sitting in layer lines between brews — is the realistic hazard in this path. Leached additives are not: contact is dry, seconds long, at ambient, and the result is brewed at 96 °C and paper-filtered. PLA's real disqualifier is that it softens around 60 °C, so the surface that most needs sanitizing can never take a hot wash. So the fix folds into the v3 chute instead of being a separate reprint: a stainless tube carries the grounds and the printed pieces become sockets and structure it passes through — the same "overlap, don't join" logic as the telescope, applied to materials. It collapses food safety, static, and wall friction into one fix, and it restores what the original Phase 3 concept specified (printed carrier + stainless liner) before the all-printed path got built. The Encore's rectangular exit → round transition is the one spot plastic is hard to avoid; first thing to try is a trimmed stainless kitchen funnel in a printed carrier (~$8, seamless) before any CNC quote. Anything that stays printed: PETG, thick solid walls, no interior supports, and a stainless nozzle rather than brass.

over: reprint the current geometry in PETG (thrown away when the angle and tube change) · food-contact epoxy interior seal (kept in reserve — coating a narrow duct evenly is fiddly, and a chipped coating is worse than none)

2026-08-31

Staying with the Baratza Encore — no espresso-style grinder upgrade

A single-dose espresso grinder (DF54 class) looked like it would simplify grounds delivery. It wouldn't, and it would cost the machine its best property. The delivery problem is at the chamber end, not the grinder end — grounds arriving fast enough to cross the bore and ricochet out — and a new grinder leaves that untouched; its "spout" is a short near-vertical drop into a dosing cup, not a routable chute, so the chute still gets built either way. Worse, single-dosing means loading beans per brew, which hands back the hands-free operation the first full cycle just earned, and the bellows purge those grinders rely on is another manual step. Critically, the whole Phase 3 control scheme rests on the Encore's latching mechanical power switch (mains applied = grind); any replacement must do the same, or the opto-relay approach dies and the grinder needs modifying — which both appliances have so far avoided. Real wins forgone: lower retention and a clean round exit. But if dose accuracy is the itch, the load cell fixes it better, cheaper, and regardless of grinder — weighing the output beats improving the thing that meters it. 54 mm flat burrs are a genuine espresso upgrade and marginal for AeroPress.

over: DF54 / single-dose espresso grinder · criteria if this ever reopens, in priority order: latching power switch → hopper-capable → low retention → defined round exit; burr geometry last

2026-08-16

Raspberry Pi + custom web app dropped — Home Assistant + on-device UI instead

The machine ran Pi-less through the entire bench program: the ESP32 owns all control and every calibrated parameter (restore_value). Every planned Pi job either moves into firmware — recipe presets (select → number entities), tank-level tracking + feed-forward dispense compensation, optional on_time scheduling, a monochrome front panel (SSD1322 OLED + Brew/Clean buttons, soft call) — or is covered by Home Assistant over the ESPHome native API: brew history, temp/state graphs, dashboards, with zero firmware changes and zero custom code. HA is an observer on any always-on box; the machine stays fully functional without it. Low-risk and reversible — nothing in firmware assumes HA exists, and the FastAPI/SQLite/HTMX plan stays documented in CLAUDE.md if it's ever revived.

over: custom FastAPI + SQLite + HTMX web app on a Pi (months of software for what HA's recorder provides natively) · Pi touchscreen kiosk (Phase 7) (retired with the Pi)

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. Superseded 2026-08-16: the Pi half was dropped entirely — see "ESP32 is the whole machine" above. The rationale (real-time control on deterministic hardware) carries over.

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
SSD1322 256×64 OLED + Brew/Clean buttons (Raspberry Pi + touchscreen — dropped 2026-08-16 for HA + on-device UI)6
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. The dry-grounds path is a separate, softer rule — brief ambient contact, then brewing at 96 °C and a paper filter — but the same failure mode still bites there, as damp fines caking in the printed spout proved on 08-17.
Part typeMaterial / process
Food-contact (funnel, fittings)CNC stainless (PCBWay, Xometry), off-the-shelf
Dry-grounds path (chute, spout)Stainless tube carries the grounds; printed PETG only as sockets and structure (2026-08-31). Where plastic is unavoidable — the grinder's rectangular exit → round transition — PETG, thick solid walls, no interior supports, stainless nozzle. Epoxy interior seal (21 CFR 175.300) in reserve, not default.
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

Prior art

Other people have automated the AeroPress. Studying a finished machine is cheaper than rediscovering its problems — this page records what they built, what's worth stealing, and where coffee-bot deliberately goes the other way.

Rushing25 — “Automated Aeropress Coffee Machine” YouTube · -o8cTh_tM68

A complete, working, wood-framed machine that grinds, doses water, steeps, presses, drains into the mug, and has its own clean cycle — demonstrated end to end in a two-minute video. It is the closest thing to a finished peer for this project, and it arrives at several of the same answers independently. Notes below are read off the video frames (a copy lives in the repo root); anything not visible on screen is marked as such rather than guessed.

How it's built

SubsystemTheir approach
FrameVarnished pine/fir, brass corner brackets, two shelves. The AeroPress stands upright in its normal orientation on the upper shelf, over a hole.
Coffee outGravity, straight down through the shelf hole into a mug on the deck below. No mug well, no drip tray — the frame is the plumbing.
WasteA clear plastic tub parked under the machine catches the ejected puck and clean-cycle water.
GrinderA separate consumer grinder with a clear hopper, on its own stand alongside the frame — not integrated into it.
Grounds deliveryA wide, flat, shallow printed ramp (~20–25° from horizontal) running from the grinder into a large collar wrapping the whole chamber mouth. Containment happens at the receiving end, not by narrowing the stream.
WaterReservoir tank → pump (brass fittings, silicone line) → inline thermoblock with thermistor feedback. Heats on demand; no held-temperature vessel.
PressMotorised actuator with a potentiometer for position feedback — the UI shows live stroke on a 0–6 inch gauge, and press/retract targets are entered in inches.
FiltersLoaded by hand, from a stack held in a printed holder bolted to the frame.
ControlAn Arduino (digital 7–13, analog A1/A3) tethered to a PC running a LabVIEW front panel over serial.

Their control panel, as configured on screen

ParameterValueNote
Grind duration10–15 stime-based dosing, same as coffee-bot
Pump duration45–55 stime-based water volume
Thermoblock target2.4 Va raw thermistor voltage, not a temperature
Steep time5–10 ssee below — the most interesting number on the panel
Press distance4.75–4.9 inabsolute position, closed-loop on the pot
Retract distance4.5–4.7 inabsolute position

The panel also carries Begin Brew, Clean, Reset, manual Run Pump / Run Thermoblock overrides, a STOP, and a “Waiting For Clean/Reset” state — so the machine deliberately parks after a brew until a human services it, rather than pretending to be continuously ready.

Worth stealing

AdoptedShallow, wide grounds delivery into a generous collar. This is the design that prompted coffee-bot's delivery-path v3. Their ~20–25° ramp is far shallower than coffee-bot's as-built 60° port and it works, which is the evidence that a longer, gentler chute — one that bleeds off the speed causing grounds to ricochet out of the bore — beats a steep, fast one. Their collar does the containment; coffee-bot's equivalent is a low shroud outside the Ø66 plunger keep-out.
Under considerationA potentiometer on the actuator for absolute position. coffee-bot's press is open-loop timed (seal_plunge_ms, press_duration_ms, press_eject_ms), so every calibration is tied to a duty-cycle setting and has to be re-tuned whenever speed changes. A pot turns all of that into positions in millimetres, which survive speed changes, make the stale press_eject_ms problem structural rather than recurring, and cost a few dollars. It is a genuine third option alongside the deferred limit switches and the planned BTS7960 current-sense upgrade.
Converged independentlyManual filter loading — reached by both projects as the one human step not worth automating. Also time-based grind dosing and time-based water volume, the same open-loop starting point coffee-bot uses (and the same reason a load cell is the endgame for both).

Where coffee-bot goes the other way

DimensionTheirscoffee-botWhy
Steep5–10 s90 s under vacuumThe most consequential difference. With an upright chamber and a paper filter, water starts draining the moment it lands — so a long steep isn't available to them, and the recipe compensates with a long pump and a short steep. coffee-bot's seal-plunge-plus-backoff pulls a closed-headspace vacuum that holds the water for a full 90 s, which is the actual AeroPress recipe rather than an approximation of it. Phase 2.
HeatingInline thermoblock, thermistor, custom mains heatingUL-listed Tiger boiler, untouched, servo-pressed buttonsDeliberate safety call: no custom mains heating element and no bespoke thermal protection. Costs standby power (~$5/month) and fixed setpoints; buys an appliance-grade thermostat, thermal fuse, and dry-fire interlock. Decisions.
Temperature targetA thermistor voltage (2.4 V)A real setpoint (196 °F), held 24/7Theirs is a working shortcut, but it isn't a temperature — it can't be reasoned about against a recipe or reproduced after a hardware change.
BrainArduino tethered to a PC running LabVIEWESP32-S3 running ESPHome, standalonecoffee-bot must brew with no computer present: all state, parameters, and interlocks live on the device, with Home Assistant as an optional observer. Their machine stops being a machine when the laptop closes.
GrinderConsumer grinder alongside, on its own standBaratza Encore, mains-switched, mounted on the frame shelfBurr grind quality, and the whole path fixed-plumbed so nothing is aligned by hand per brew.
Read honestlyTheir machine is finished and making coffee, which coffee-bot's phase gates have not yet earned across the whole cycle. Where the two designs differ, it is worth being clear which differences are real improvements (the vacuum steep, the standalone brain, the appliance-grade heating) and which are just coffee-bot spending more effort — a wooden frame with a hole in it drains into a mug perfectly well.

Reference

Bench reference

Everything needed to drive the bench rig — Phase 1 dispense and the Phase 2 press — 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-11 · persists across reboots

EntityValueMeaning
unlock_press_level0.53third value (0.65 → 0.55 → 0.53); survived the 50-cycle endurance run with no drift
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

Phase 2 press calibration 2026-06-25 · persists on-device

EntityValueMeaning
seal_plunge_ms @ 70 %6000fast descent, home → ~7 mm past the mouth — engages the seal before the filter drains the pour
seal_backoff_ms @ 30 %1500~5 mm retract — pulls the vacuum that holds the water through the steep
steep_ms90000no motion; the suction does the holding
press_duration_ms @ 30 %20000slow extract from the seal position; hiss found ~26000 — next tune can press closer (contact eject made the air-gap concern moot)
press_eject_ms @ 30 %stalere-tune wet as a contact-push distance — the stored ~4000 ms is the failed pneumatic value
press_home_ms @ 30 %50000bounds the retract; the actuator's internal top limit is the real stop
actuator_jog_ms≤ 5000jog burst length — sets above 5000 are silently rejected (number max_value clamp)

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"

# Phase 2 press — object_ids are NAME-derived, not script ids
curl -X POST -H "Content-Length: 0" http://coffee-bot.local/button/brew_press/press
curl -X POST -H "Content-Length: 0" http://coffee-bot.local/button/eject___clean/press   # "Eject & Clean": & and spaces each become _
curl -X POST -H "Content-Length: 0" http://coffee-bot.local/button/home_plunger/press
curl -X POST -H "Content-Length: 0" http://coffee-bot.local/button/actuator_stop/press   # software stop — the kill switch is the real one
curl -X POST -H "Content-Length: 0" "http://coffee-bot.local/number/press_duration_ms/set?value=20000"   # "Press Duration MS" → press_duration_ms (NOT press_dur_ms)

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.