Closed-loop control on a $5 microcontroller
An ESP32 has no business running a chemistry lab. Here's what it took to make one hold a saltwater tank's water parameters steady — and where cheap silicon stops being the bottleneck.
- Published
- Reading time
- 3 min read
- Topics
- Embedded Systems · Control Systems · ESP32
The aquarium controller started as a chore-avoidance project and turned into a small lesson in how far you can push a closed loop on genuinely cheap silicon.
The problem is analog all the way down. A reef tank wants its alkalinity, calcium, and magnesium held inside narrow bands. Left alone, those numbers drift — coral consumes minerals, evaporation concentrates salts, and a missed day of dosing shows up as a measurable swing. The commercial controllers that automate this are expensive, closed, and stingy with the raw data you'd actually want to reason about.
The loop is the easy part
People expect the control law to be the hard bit. It usually isn't. A dosing loop is slow, heavily damped, and forgiving — you're nudging a large volume of water with small injections and waiting hours to see the effect. A conservative controller with generous deadbands and rate limits is more than enough. The dynamics don't justify anything exotic.
What's actually hard is everything the loop assumes:
- That your measurement means what you think it means.
- That the actuator did what you told it to.
- That the system fails into a safe state when one of those two things isn't true.
On a reef tank the failure mode isn't an annoyance, it's a dead tank. So most of the firmware isn't the controller. It's the scaffolding that makes the controller's inputs and outputs trustworthy.
Trusting the sensor
The interesting hardware here is a small RGB spectrometer prototype — narrow-band LEDs and a photodiode array doing colorimetric analysis on standard test reagents, so the tank can read its own chemistry instead of waiting on a human with a test kit.
Colorimetry on cheap optics is noisy. The photodiode sees ambient light, LED output drifts with temperature, and reagent color develops over time. None of that is a control problem — it's a measurement-conditioning problem that happens before the loop ever sees a number. Dark-frame subtraction, a fixed develop-time delay, and averaging across samples turn a jittery raw reading into something you can actually close a loop around.
The lesson generalizes: a control system is only as good as its worst-conditioned input. Spend your effort there first.
Trusting the actuator
Peristaltic pumps are open-loop by nature — you command a run time and hope the right volume moved. Tubing wears, air gets in, a line kinks. So the firmware treats every dose as a hypothesis to be checked on the next measurement cycle, with hard daily caps so a stuck sensor can never command an unbounded amount of dosing. The cap isn't a control feature. It's a safety interlock that sits outside the loop and overrides it.
Where the cheap silicon stops mattering
The ESP32 never broke a sweat. FreeRTOS handles the sampling schedule, the dosing logic, and a small web UI with cycles to spare. The constraint was never CPU or memory — it was the physical reality the loop is wired into. The board is the cheapest, most reliable part of the whole system.
That's the part I keep relearning across very different projects, from a fish tank to safety-critical avionics: the controller is rarely the risk. The risk lives at the seams — sensor to firmware, firmware to actuator, actuator back to the physical world. Get those seams honest and even a $5 chip can hold a line.