← SHEET 02 · ASSEMBLIES EL-006
Deep-Sleep Plant Waterer
| PART NO | EL-006 |
|---|---|
| MATL / SYSTEM | ESP32 · CAPACITIVE PROBE · 5V PUMP |
| TOOLS | Arduino IDE · Wokwi · C++ |
A battery-operated plant waterer built around an ESP32’s deep-sleep timer wake rather than a continuously-running loop: every 30 minutes the chip wakes, averages 16 ADC samples from a capacitive soil probe, decides whether to water using a hysteresis band, and — if it does — enforces an 8-second hard-cap on pump runtime and checks on the next wake whether the moisture reading actually dropped, locking the pump out after three straight failures to water (empty reservoir, kinked tube, dead pump). The one number that makes or breaks a project like this is the battery budget, so it’s worked from the firmware’s own duty cycle rather than quoted from a spec sheet: the arithmetic below, with every current figure labeled either datasheet-typical or an explicit design assumption, projects roughly 5.6 years on a single 18650 before Li-ion self-discharge — not the circuit — becomes the limiting factor.
OVERVIEW & MOTIVATION
Most “smart” plant waterers either poll continuously (killing battery life) or water on a blind timer with no feedback (killing plants when the reservoir runs dry). This design treats watering as a closed decision with two independent safety nets — a hard runtime cap that doesn’t care what the sensor says, and a moisture-feedback check that catches the case the runtime cap can’t: the pump ran for its full allotted time and the soil still didn’t get wetter. Both live in RTC memory so they survive every deep-sleep cycle without touching flash. The design brief was explicit about not padding numbers from a datasheet, so the battery-life section works forward from the firmware’s actual measured-by-design timing (wake interval, awake-time budget, sampling count) rather than backward from a marketing “years of battery life” claim.
COMPONENTS & BOM
| REF | COMPONENT | SPEC | ROLE |
|---|---|---|---|
| U1 | ESP32 DevKitC-V4 | ESP32-WROOM-32, deep-sleep ≈10 µA typ | controller — RTC timer wake, ADC read, pump logic |
| PR1 | Capacitive soil-moisture probe | v1.2-class board, analog out ~0–3 V | moisture sensing; no exposed metal in soil, so no electrolytic corrosion |
| K1 | Relay module or logic-level MOSFET | opto relay board, or e.g. IRLZ44N + gate resistor | switches the 5V pump from GPIO27 |
| P1 | 5V DC mini pump | diaphragm/submersible, ~400 mA typ | delivers water to the plant |
| BT1 | 18650 Li-ion cell | 3.7 V nominal, ~2600 mAh typ | battery power source |
| U2 | Low-Iq LDO/buck regulator | e.g. MCP1700 / ME6211-class, ~1–3 µA Iq | 3V3 logic rail — must be low-quiescent, see power budget |
| U3 | 5V boost converter | e.g. MT3608 module | pump supply rail stepped up from the single Li-ion cell |
WIRING
| ESP32 PIN | NET | PERIPHERAL PIN |
|---|---|---|
| GPIO34 (ADC1_CH6, input-only) | MOISTURE_SIG | Probe SIG — Wokwi: pot1:SIG |
| GPIO25 | PROBE_PWR | Probe VCC, switched — Wokwi: pot1:VCC |
| GPIO27 | PUMP_CTRL | Driver IN — Wokwi: relay1:IN |
| 3V3 | 3V3 | Regulator 3V3 OUT |
| GND | GND | Common ground bus |
FIRMWARE & POWER BUDGET
State machine. Deep sleep restarts execution at setup() every wake — there is no
persistent loop() — so the whole decision lives there, backed by RTC_DATA_ATTR variables
that survive sleep (but not a power-on/EN reset): bootCount, pumpLockedOut,
dryRunStrikes, wateringCheckPending, preWaterMoistureRaw. Each wake: read the probe
(powered only for the ~100 ms sampling window via PROBE_PWR_PIN), resolve any pending
watering check from the previous wake, decide whether to water this wake, then re-arm the
timer and sleep.
uint16_t readMoistureAveraged() {
digitalWrite(PROBE_PWR_PIN, HIGH);
delay(PROBE_SETTLE_MS);
uint32_t sum = 0;
for (int i = 0; i < ADC_SAMPLES; i++) { sum += analogRead(MOISTURE_ADC_PIN); delay(5); }
digitalWrite(PROBE_PWR_PIN, LOW); // cut probe power right after sampling
return (uint16_t)(sum / ADC_SAMPLES);
}
Hysteresis. A capacitive probe reads a higher raw ADC count as soil gets drier.
MOISTURE_DRY_RAW = 2800 triggers watering; MIN_MOISTURE_DROP = 150 is the fall in raw
counts the next wake’s reading must show for the watering event to count as successful —
that comparison, not a fixed wet threshold, is what drives the lockout:
int32_t drop = (int32_t)preWaterMoistureRaw - (int32_t)raw;
if (drop >= MIN_MOISTURE_DROP) {
dryRunStrikes = 0;
} else if (++dryRunStrikes >= MAX_DRY_RUN_STRIKES) {
pumpLockedOut = true; // reservoir/tubing/pump suspected empty or failed
}
Safety. runPumpWithSafetyCap() enforces MAX_PUMP_RUNTIME_MS = 8000 unconditionally —
it is a millis() timeout with no feedback early-exit, deliberately, since a single-shot
wake-sleep node can’t watch the ADC continuously while the pump runs without abandoning the
low-power design. Three consecutive failed waterings (MAX_DRY_RUN_STRIKES = 3) latch
pumpLockedOut, which only clears on a power-on/EN reset.
Power budget. Every current figure below is either labeled datasheet-typical or an explicit design assumption — none of it is a field measurement, since the unit hasn’t been deployed (see STATUS). The arithmetic is driven entirely by the firmware’s own numbers:
| TERM | VALUE | SOURCE |
|---|---|---|
Wake interval T | 1800 s (30 min) | design — WAKE_INTERVAL_SEC |
Awake current I_awake | 80 mA | datasheet-typical, ESP32 active CPU, radio off |
Awake time t_awake | 0.3 s | design estimate: wake boot + 16-sample ADC average + logic |
Sleep current I_sleep | 10 µA | datasheet-typical, ESP32 deep sleep, timer-only RTC wake |
| Avg standby current | (I_awake·t_awake + I_sleep·(T−t_awake)) / T = 23.3 µA | computed |
| Pump current @5V | 400 mA | datasheet-typical, small 5V diaphragm pump |
| Watering assumption | 1 event/day × 4 s (within the 8 s cap) | design assumption, not measured |
| Boost efficiency | 85% | design assumption, MT3608-class converter |
| Avg pump-duty current | 29.4 µA | computed, battery-side via 3.7V→5V boost |
| Total avg current | 52.8 µA | standby + pump duty |
| Battery capacity | 2600 mAh | datasheet-typical, single 18650 cell |
| Modeled runtime | ≈2053 days ≈ 5.6 yr | 2600 mAh / 52.8 µA |
| Realistic runtime | ≈2–3 yr | Li-ion self-discharge (~2%/month typical) and any regulator leakage aren’t modeled above and will cap the real figure well below the modeled one |
The standby term alone (23.3 µA) already implies the design decision that matters most: an LDO with a few mA of quiescent current (a generic AMS1117, say) would outweigh the entire modeled budget by roughly three orders of magnitude and make the whole calculation moot — which is why the BOM calls out a low-Iq regulator specifically.
SIMULATION
Wokwi simulates deep sleep faithfully (the sim clock advances through
esp_deep_sleep_start() and lets you fast-forward through the 30-minute intervals), so the
whole state machine — including the multi-wake dry-run check — is exercisable without hardware:
- Load
sketch.ino+diagram.jsoninto a Wokwi ESP32 project, start the simulation, open the Serial Monitor at 115200 baud. - Moisture probe substitution: a potentiometer (
pot1) stands in for the capacitive probe — Wokwi has no capacitive soil-probe part. Its wiper feedsGPIO34and its VCC is switched fromGPIO25, exactly like the real probe. Twist it toward the “dry” end to push the raw ADC reading aboveMOISTURE_DRY_RAWand trigger a watering cycle on the next wake; twist it back to simulate the soil having absorbed water. - Pump substitution: a
wokwi-relay-module(relay1) stands in for the real MOSFET/relay driver, energized fromGPIO27exactly like the real build. A green LED + 220 Ω resistor across the relay’s switched contacts just gives a visible “pump on” indicator — the real load there is a 5V pump, not an LED. - To rehearse the lockout: leave the potentiometer parked “dry” across three watering cycles
in a row. The Serial Monitor prints
PUMP LOCKED OUTon the third failure, and the relay stops firing until the simulation is reset.
STATUS
Design and Wokwi-simulated only — this has not been built or run on a real plant. The
state machine, hysteresis, dry-run lockout, and max-runtime safety all execute correctly in
simulation, and the power-budget arithmetic is internally consistent, but the moisture
thresholds (MOISTURE_DRY_RAW, MOISTURE_WET_RAW, MIN_MOISTURE_DROP) are design values,
not calibrated against a real probe in real soil, and the 80 mA/10 µA current figures are
datasheet-typical, not bench-measured on this specific board/regulator combination. Next:
bench-calibrate the probe in air/water, select and verify the low-Iq regulator, measure actual
awake current on a scope/USB power meter, and re-run the battery-life arithmetic against
measured numbers instead of datasheet ones.
USE CASES & APPLICATIONS
The pattern here — read a sensor, make a bounded actuation decision with hardware/firmware safety limits, sleep for the rest of the interval — generalizes past plant watering to any duty-cycled battery IoT node: greenhouse micro-irrigation zones (many nodes, each independent, where a single stuck-pump failure must stay local), remote environmental/soil sensing nodes that only need to report on a slow cadence, and livestock-trough or rain-barrel level control where a pump/valve needs the same max-runtime-plus-feedback safety structure. The specific lesson worth reusing elsewhere: computing the battery budget from the firmware’s actual duty cycle (not a spec-sheet “years of battery life” claim) is what turns a plausible-sounding low-power design into one whose numbers can be checked.
FILES
sketch.ino— full firmware: RTC-persisted state machine, averaged ADC read, hysteresis watering decision, hard max-runtime pump safety, dry-run lockout, timer-based deep sleep.diagram.json— Wokwi circuit (ESP32 DevKitC-V4 + potentiometer probe stand-in + relay module pump-driver stand-in + LED pump indicator).README.md— Wokwi simulation walkthrough and real-build notes (capacitive vs. resistive probes, switched probe power, low-Iq regulator choice, pump driver wiring).
Paths: sources/electronics/plant-waterer/sketch.ino, sources/electronics/plant-waterer/diagram.json,
sources/electronics/plant-waterer/README.md.