Studio
Soil to Signal: Teaching Three Pots to Say When They're Thirsty
A soil-moisture project for the kitchen garden — capacitive probes on an ESP32, a Raspberry Pi dashboard in a 3D-printed case, and the week of watching data before anything is allowed to touch water.
It began, as these things do, with a dead basil. Not dramatically dead — the slow, sulking kind where you realise you have been watering by mood rather than by need, and the mood was wrong for a fortnight. I wanted the garden to tell me. Not an app pinging from a server somewhere; something in the house, on a screen I already glance at, saying Pot B is drying out and has been for two days.
That is Soil to Signal: a probe in the soil, a small board that reads it, a dashboard that never leaves the house. Here is how it actually went.

The idea, and the rule
The ambition is the kitchen garden — beds, a greenhouse, a pump that waters a bed on its own. But ambition is how projects flood kitchens, so the scope right now is one board, three probes, three pots on a windowsill. Two rules decide everything downstream:
The control loop never leaves the garden. Each bed eventually gets its own little computer that senses, decides and waters locally. The link back to the house is for reporting — the dashboard asks, the node answers, and nothing arriving over that link can ever start a pump. WiFi down? Garden carries on. Dashboard off? Garden carries on.
Nothing that can move water exists until the probes are proven. Firmware, calibration, a week of watching real soil dry — then a relay and a pump. There is currently nothing in the build capable of flooding anything, and that is on purpose.

And no cloud. A node lives outdoors where anyone can pick it up, so any key on it is a key you must assume is lost — and the honest answer to "what does the cloud add?" was "a place for things to go wrong that is not in my house". The dashboard is a Python script on a Raspberry Pi indoors: polls the node, writes a CSV, makes no external requests. Even the fonts are vendored.
Sensors: the physics did the shopping
Capacitive, never resistive. The two-prong sensors every tutorial starts with electrolyse themselves and corrode away inside a season. Capacitive probes sense the soil around a coated PCB; nothing touches water. I bought five. I had five.
ESP32, not Raspberry Pi, at the plant. A Pi has no analogue input at all. An ESP32 has one on the chip, plus WiFi, for the price of a coffee. The Pi's job is to be the screen in the house.
ADC1 pins only. The WiFi radio hijacks the ESP32's second analogue block the moment it joins a network. A probe on the wrong pin reads perfectly on the bench and returns garbage the instant it connects. This one would have cost me a week.
The ADC is mediocre. Nine useful bits rather than the advertised twelve, non-linear, and different on every chip — so calibration is per probe per board. Swap either and start again.
3.3 V, not 5 V. Most tutorials are written for Arduinos. Copying them kills ESP32s.
The breadboard incident
I lost two of the five in one evening. Probe 2 never gave a reading. Probe 1 was fine at 17:27 and dead by 18:40, after a plug went into the breadboard one row off and put 3.3 V into its output. The breadboard is gone; the three survivors are wired straight to the board, and the rules from that evening are at the top of the repo: USB out before any plug moves. Jumper by the silkscreen name, never by wire colour. One change, one run. They read as obvious. They were not obvious with a hot board in my hand.
Calibration: two jars and a notebook
A capacitive probe gives a voltage that falls as soil gets wetter. You want a percentage, so you take two points — probe in open air (0 %), tip in a glass of water to the line (100 %) — and draw a line between them.
| Probe | Pin | Supply | Air (V) | Water (V) |
|---|---|---|---|---|
| 3 | GPIO34 | 3V3 | 2.715 | 0.977 |
| 4 | GPIO35 | VIN (USB 5 V) | 2.770 | 1.124 |
| 5 | GPIO32 | GPIO25 driven high | 2.610 | 1.070 |
The third reads lower in air because a GPIO used as a power supply sags a little. Calibration absorbs that — as long as that probe stays on that supply forever. Dull bookkeeping, and the only thing that makes the percentages mean anything.
My very first reading was 0.142 V, which is nonsense — two jumpers swapped. The instinct to believe a number because a screen printed it is strong.
The cyberdeck
The screen in the house is a Raspberry Pi 5 with a 5-inch 1024×600 touch panel — the cyberdeck, which started as a separate project to put garden, wardrobe and weather on one ambient display and has since grown a home page. The Soil-to-signal tile opens the moisture deck in place.

The Pi sits on a shelf as a bare module for now. The case is where the evenings have gone, because I wanted it to open like a book: two leaves on a spine, the 5-inch screen with the Pi behind it in one, a smaller 3.5-inch panel and a battery in the other. Every dimension in the model is a named constant — which sounds like an affectation until the first test print says the window is 6.95 mm too wide and the fix is one number rather than a weekend.
Two things worth passing on. Print the coupons, not the box — a numbered set of five-gram test pieces, each answering something the next depends on, before the 42-gram leaf. And the hinge is where the wires go: fifteen jumpers cross from one leaf to the other, so the spine is a passage and the bundle twists along the fold rather than bending. The hinge strap and the full box are next on the printer.
The screen
The deck is a single Python file serving plain HTML — no framework, nothing to install. It started with four views — Home, Pots, Nodes, Log — and is down to two, because the node's health belongs on the pot's card rather than on a page of its own, and a log of pump runs is a page for a deck that has a pump. Home is one card per pot. The hero number is moisture as a percentage; it was volts for the first day and I switched it, because volts are what the bench cares about but a person walking past wants 81 %. Under it, how much the pot dried in the last 24 hours, and beside it a dry line with a verdict in words — Leave, Water tomorrow, Water today — because a number needs a sentence before it becomes a decision. A seven-day sparkline on the right, and bench-node · 8 s ago in small type, which is all of the old Nodes page that mattered.

Pots is the chart — 24 hours, 7 days or 30 — with buttons to log a feed or a watering so a step in the line has an explanation next to it, and the two-jar calibration stepper. Everything that used to be a page is now an alert strip across the top: a silent node, a stuck probe, a pot under its dry line. The deck reads and logs and never sends a node anything. The intelligence is in the garden, not in the house.

Data validation: the part I underestimated
I thought the hard part would be the electronics. It was believing the numbers. A soil probe has half a dozen ways to lie, and every one of them produces a plausible value.
The stuck-sensor guard. Sensors do not fail to an error; they fail to a constant. Dead probe 1 read a perfectly steady 3.145 V, and a steady number looks like a healthy sensor. So the deck keeps the last five real updates per probe and, if they are identical, flags it as stuck rather than believing it. The reading that never changes is the one to trust least.
The reboot counter. A weak charger sags under the radio's current peaks and the board silently resets. The deck counts every time uptime goes backwards and keeps the count in the node's state. It stood at 14 after the first day, mostly me reflashing — but a rising count on a node nobody has touched means fix the supply, not the code.
Markers for things that are not soil. Fertiliser raises the soil's conductivity and a capacitive probe reads 2–5 % wetter for every dS/m of it — so it reads wet exactly when the plant most needs water to move nutrients. No fixing that in software; you can only know when you fed. Hence the button.
And Pot C sat at 100 % for the first day — real, but also the calibration ceiling, and a number sitting on a ceiling tells you nothing. It has since come off it, which is one of the things the week was for.
The pot week
The pots went live on 19 September, logging a row per probe per minute. The question the week has to answer: does a week of readings look like soil drying? A slow decline, a step when I water, a knee when the surface dries before the depth. If yes, the dry-pot threshold comes out of the curve. If no, I would rather find out from a windowsill than from a bed of leeks.
The next phases
Everything above is phase one. The diagrams below are the plan for what follows, in order, because the order is the point.
Phase 2 — the node on its own. A day on a wall charger with nobody near it, watching the reboot count. A dry-pot alert on the deck. Watering the pots — and the answer there is not a pump at all but a 3D-printed gravity kit, a funnel feeding perforated pipes on spikes; the deck's job becomes saying when the funnel needs a refill.
Phase 3 — the deck talks to the garden app. Readings still never leave the house; what crosses the line is small and mostly one way. The deck reads my Peas & Quiet app for tasks, crop names and the rain forecast, so a pot card can say tomatoes instead of Pot A and show rain beside moisture. If a pot is dry it writes one task. An hourly digest up to the copilot is the only thing that would reopen the no-cloud decision, and it stays dashed on the drawing until it earns its place.

Phase 4 — the greenhouse, and beds too far from a socket. The first thing that moves water, behind interlocks, on mains. And for a bed with no power, a solar version of the node: the same board, asleep 99 % of the time, waking every half hour to power its probe for 300 ms and answer the deck. Awake all day it needs ~12 Wh; on that cycle about 1 Wh, which a 5 W panel makes on a grey September day and roughly on a December one. Nothing for it has been bought, and nothing will be until the USB node has run a month on a shelf without anyone touching it.

The end goal
One bed, summer 2027. No socket, no cable to the bed: a 25 W panel, a 12 V LiFePO4 in an IP65 box, and one ESP32 that sleeps, wakes every half hour, reads its three probes and — behind every interlock in the YAML — closes the one relay that can run the pump. Fused cable in conduit to the butt, check valve so the bed does not siphon after the pump stops, drip line kept well away from the probes so a probe never ends up reading the dripper instead of the bed. The deck in the house reads it over WiFi and can do nothing else. Cloud, deck, phone: none of them can start the pump.

Everything between here and there is the order above, and the order is the point.
That is the state of it on a Sunday in September: three pots, a screen, a CSV growing by three lines a minute, and a basil that is — for the moment — being watered by evidence rather than by mood.



