Project Details

This project is a precision liquid dosing controller built around an ESP32 and multiple stepper-driven peristaltic pumps. The main idea was to build something more capable than a simple timer-based doser: a controller that can deliver measured doses, track what actually happened, expose live status over the network, and stay practical to use in real installations.

It is meant as a general-purpose dosing platform, but aquarium automation is one of the clearest real-world use cases. In that setup, separate channels can be used for alkalinity, calcium, magnesium, trace elements, or fertilizer, with each channel calibrated and scheduled independently. The same architecture also fits hydroponics, small lab setups, and other DIY fluid-handling systems where repeatability matters.

Hardware

At the core is an ESP32 running custom ESP-IDF firmware. The controller is designed for multiple pump channels, with each pump driven by a stepper motor rather than a basic DC motor. That makes it possible to control motion more predictably and build dosing around calibration and measured flow instead of rough timing assumptions.

The current hardware target assumes 16 MB flash, with enough room for two OTA firmware slots, the embedded web UI, and a dedicated history partition. The system can also support reduced configurations for smaller flash targets, but the main development target is the roomier layout because it leaves space for the interface, telemetry, and update workflow without squeezing the firmware too hard.

The board-level configuration includes motor-channel mapping, UART pin assignments, I2C devices, GPIO, and ADC configuration. Optional external RTC and EEPROM or FRAM support are included for better timekeeping and more robust persistence across resets or power loss. When external persistent storage is present, it is used for runtime-related backups more comfortably than relying on flash alone.

Pump Control And Safety Model

Each channel is treated as a configurable dosing pump with manual run support, scheduled run support, calibration data, tank-volume tracking, runtime history, and safety limits. The firmware supports both direct manual dosing and automatic dosing modes that depend on valid calibration data.

A key design choice is that the controller does not blindly resume an interrupted pump run after a reset or power loss. Persisted counters and state are restored, but an in-progress dose is not automatically continued. That behavior is intentional: for dosing applications, a missed partial dose is usually safer than an accidental overdose caused by uncertain restart conditions.

Safety limits can be applied per pump and globally. These include per-run, per-hour, and per-day limits, as well as an overall daily total. If a request would exceed those limits, or if an automatic mode lacks valid calibration data, the firmware rejects the run instead of guessing.

Driver Configuration

The stepper driver layer is based on a shared TMC2209 profile applied to all active pump channels. That profile is stored in board configuration and can be read and updated over the API. Instead of hiding the driver behind fixed hardcoded values, the project exposes the useful configuration points so the system can be tuned for real hardware.

The TMC2209 profile includes:

A fresh board configuration uses a conservative baseline profile:

{
  "rsense_milliohm": 220,
  "run_current_ma": 400,
  "hold_current_percent": 10,
  "stealthchop_enabled": true,
  "stealthchop_threshold_rpm": 0,
  "coolstep_enabled": false,
  "coolstep_threshold_rpm": 0,
  "coolstep_sg_threshold": 45,
  "coolstep_semin": 4,
  "coolstep_semax": 2,
  "coolstep_seup": 1,
  "coolstep_sedn": 1,
  "coolstep_seimin": 0
}

Current calculation uses the configured sense resistor, run current, and hold-current percentage, then clamps the internal TMC2209 register values to safe register ranges. The firmware also selects VSENSE automatically.

Threshold registers such as TPWMTHRS and TCOOLTHRS are derived from RPM and channel microstep settings rather than entered directly. That keeps the user-facing configuration more understandable while still programming the driver correctly for each active channel.

The validation rules are intentionally strict. For example:

If validation fails, the API returns an HTTP 400 error with a specific message instead of accepting an unsafe or contradictory configuration.

Runtime Diagnostics

The project does not treat the motor driver as a black box. Runtime data includes TMC2209 diagnostic information for each active channel, including UART availability, thermal and fault flags, actual current scale, StallGuard data, StealthChop state, standstill state, and driver version.

That makes the controller useful not only as a doser, but also as a practical embedded platform for tuning and observing stepper-driven pump hardware. The web UI surfaces this data in a more compact form on the main pages and in more detail on the board or configuration side.

Connectivity And Provisioning

The controller exposes a built-in web interface served directly by the ESP32. There is no separate server requirement and no cloud dependency for normal use. The production web bundle is embedded into the firmware image and served alongside the API.

Wi-Fi onboarding supports two paths:

BLE provisioning uses ESP-IDF protocomm_ble with Security1 and a proof-of-possession derived from the device MAC. This makes first-time setup easier when the device does not yet have network access.

The web interface is also packaged with a lightweight PWA setup. The service worker is intentionally conservative and does not cache API, WebSocket, or upload traffic, so live control and telemetry always come from the real device rather than stale cached responses.

API

The HTTP and WebSocket API is shared by the web UI and the native iOS client. The goal was to keep the interface simple enough for direct clients while still exposing useful real-time state.

Core REST endpoints include:

A manual run request looks like this:

{
  "id": 0,
  "speed": 1,
  "direction": true,
  "time_seconds": 10
}

The board configuration API also exposes the shared TMC2209 block, which means hardware-specific tuning can be changed without rebuilding firmware.

Realtime API

Live updates are provided over WebSocket at /ws. Instead of continuously pushing the entire device status blob, the firmware sends partial updates only when tracked fields change. That keeps traffic smaller and avoids wasting CPU and memory on the ESP32.

Realtime message types include:

The most important runtime message for pump activity is pump_runtime, which includes channel ID, active state, run state, speed, direction, remaining time, delivered volume, alert flags, and driver diagnostics.

That split between REST for snapshots and WebSocket for deltas makes the system responsive without turning the device into a constant high-volume broadcaster.

Why This Architecture

A lot of DIY dosing systems stop at “turn pump on for N seconds.” This project goes further by combining calibrated delivery, network control, persistent history, live diagnostics, OTA updates, and explicit safety behavior in one controller. The result is a dosing platform that is still approachable as a hobby project, but closer in spirit to a real embedded control system than a simple timed pump switch.