Irrigation · Relay control · Olimex ESP32-C6-EVB

Build a Wi-Fi Lawn Sprinkler Controller

Take an off-the-shelf relay board, wire it to your existing 24 volt sprinkler valves, and generate a browser-based controller — per-zone manual control plus a weekly watering schedule — without hand-writing a single line of HTML.

Board: Olimex ESP32-C6-EVB Level: beginner-friendly Time: ~1–2 hours Zones: up to 4 Published 19 July 2026

The problem we're solving

A typical residential irrigation system is just a handful of solenoid valves buried in a box near the garden, each feeding one "zone" — the front lawn, the flower beds, the drip line along the fence. A traditional sprinkler timer is a closed appliance bolted to the garage wall: fiddly buttons, a dim LCD, and no way to trigger a zone from your phone when you notice a dry patch.

We can do better. Each valve is opened by sending 24 volts AC to its solenoid; cut the voltage and the valve springs shut. That's a perfect job for a relay. If we put four relays behind an ESP32 and give it a web page, we get a sprinkler controller we can reach from any browser on the home network — with a clean per-zone UI and a weekly schedule we design visually instead of coding by hand.

Why this is a friendly first project. Sprinkler valves run on low-voltage 24 VAC, not mains — much safer to wire than a 120/240 V appliance — and the logic is simple on/off switching. It's an ideal way to learn relay control and the ESP-GenUI workflow before tackling anything mains-powered.

The board: Olimex ESP32-C6-EVB

Photo of the Olimex ESP32-C6-EVB: a red PCB with four blue relays, screw-terminal blocks, a USB-C port, and the ESP32-C6 module.
Olimex ESP32-C6-EVB — four relays on one board.

The Olimex ESP32-C6-EVB is an open-source-hardware evaluation board built around Espressif's ESP32-C6. It pairs the MCU with four onboard relays and four opto-isolated inputs, which is exactly the shape of a four-zone sprinkler controller — no relay HAT or breadboard wiring required.

ESP32-C6-EVB ESP32-C6 WROOM-1 USB-C RELAY 1GPIO10 RELAY 2GPIO11 RELAY 3GPIO22 RELAY 4GPIO23 NO COM NC 4× OPTO-ISOLATED INPUTS  (5–30 VDC) IN1 GPIO1   IN2 GPIO2   IN3 GPIO3   IN4 GPIO15 PWR JACK 8–50 VDC
Fig. 1 — Board map. The four relays are on GPIO10, 11, 22, 23; those are the pins we'll switch to open each valve.

Specifications that matter for this build

Olimex ESP32-C6-EVB — relevant specs
FeatureDetailWhy it matters here
MCU / moduleESP32-C6-WROOM-1 (RISC-V, up to 160 MHz, 4 MB flash)Runs the generated web server
WirelessWi-Fi 6 (2.4 GHz), Bluetooth 5 LE, 802.15.4 (Thread/Zigbee/Matter)Serves the UI over your home Wi-Fi
Relays, rated 10 A / 240 VAC, dry contacts (NO/COM/NC)One relay switches one sprinkler zone
Relay GPIOsGPIO10, GPIO11, GPIO22, GPIO23The pins your UI toggles drive
Opto inputs4× isolated, 5–30 VDC (GPIO1/2/3/15)Optional: rain sensor / flow switch
Power inBarrel jack 8–50 VDC, or USB-CPower from a small DC adapter
ProgrammingUSB-C (also JTAG); ESP-PROG headerFlash straight from the browser
Size100 × 70 mmFits a small weatherproof enclosure

The relay contacts are rated for up to 240 VAC, but our sprinkler valves are only 24 VAC — we're using a fraction of the relay's capability, which is exactly what you want for reliability.

How the pieces map to zones

The mental model is one-to-one: one relay = one zone = one valve, and each zone gets its own on/off toggle and its own weekly schedule. Each toggle drives the matching relay GPIO, which closes the relay contact, which sends 24 VAC to that valve's solenoid and opens the water.

Zone → relay → GPIO mapping (edit to match your yard)
ZoneExample useRelayESP32-C6 GPIO
Zone 1Front lawnRelay 1GPIO10
Zone 2Back lawnRelay 2GPIO11
Zone 3Flower bedsRelay 3GPIO22
Zone 4Drip lineRelay 4GPIO23
Water only one zone at a time. Home water pressure usually can't feed several zones at once. We'll enforce this in the UI logic in Step 3 so turning on one zone turns the others off.

Step 1 Wire the valves to the relays

Sprinkler valves use a common 24 VAC transformer. One transformer wire is shared by every valve (the "common"); the other wire of each valve is switched on and off — that's the job of the relay. Each relay gives you three screw terminals: COM (common), NO (normally open), and NC (normally closed). We use COM and NO so the valve is off until the ESP32 energizes the relay.

24 VAC transformer COMMON (shared by all valves) 24VAC hot REL1 REL2 REL3 REL4 Zone 1Zone 2 Zone 3Zone 4 GPIO10 / 11 / 22 / 23 drive the coils
Fig. 2 — Wiring diagram. Transformer hot feeds every relay COM; each relay NO goes to one valve; all valve returns join the transformer common.
  1. Power off the transformer. Land the transformer's hot lead on a small jumper bus feeding all four relay COM terminals.
  2. Run each relay's NO terminal out to one valve wire (Zone 1 → Relay 1, and so on to match the table above).
  3. Join every valve's other wire together with the transformer's common lead.
  4. Power the board itself from a separate 5 V USB-C supply (or an 8–50 VDC adapter on the barrel jack) — not from the 24 VAC sprinkler transformer.
Keep the two circuits separate. The relay contacts switch the 24 VAC valve circuit; the board's logic runs on its own DC supply. Never bridge 24 VAC onto the ESP32's GPIO or 3.3 V rail. Double-check polarity and terminal labels against your board revision before powering on.
An Olimex ESP32-C6-EVB relay board on a workbench, powered over USB-C with one relay's status LED lit, its screw terminal wired to the coil of a green brass sprinkler solenoid valve sitting beside it.
Fig. 3 — The relay board driving a single sprinkler solenoid valve on the bench. Test one zone this way first — confirm the relay clicks and the valve opens — before wiring every valve in the yard.

Step 2 Describe it — let AI build the diagram

Open the editor. Instead of dragging modules onto the canvas one at a time, click ✨ Describe and tell it what you're building in plain English. The AI assembles the whole diagram for you — the page, the zone controls, the schedule, and the wiring between them — so you go from a sentence to a working starting point in one step. For this project, paste something like:

✨ Describe prompt

"A four-zone lawn sprinkler controller. Give me one page with a labelled on/off switch for each zone — Front lawn(gpio 10), Back lawn(gpio 11), Flower beds(gpio 22), and Drip line(gpio 23) — that I can turn on and off from my phone, and a separate weekly watering schedule for each zone so every zone can run on its own days and times."

Describe reads that and builds a matching diagram — typically a Page with a Header, four browser-controllable on/off GPIO toggles (one per zone, because a load you switch by hand is a GPIO output), a Schedule for each zone's weekly plan, and an NTP time source it wires in automatically so the schedules know the time. It often adds a WiFi Connect step too, so the board can join your network on first boot. That's the whole structure, generated for you. (If Describe starts you with a single shared schedule, add one per zone — Step 3 explains why that matters.)

ESP-GenUI editor showing the AI-generated sprinkler diagram: a Header, a WiFi Connect step, a Sprinkler Control page with a Zone Control card of four GPIO toggles (Front Lawn, Back Lawn, Flower Beds, Drip Line) and a Schedule card with an NTP node and one schedule per zone, beside a live phone preview of the page.
Fig. 4 — The real diagram Describe generated from the prompt above, with the live phone preview. It produced four GPIO zone toggles and a Schedule per zone — on placeholder pins (12/13/14/27), which you reassign to the board's relay pins next.

Set the hardware specifics

Describe designs the interface, but it assigns placeholder pins that won't match your board — so finish the zones in the Inspector. Click each zone's GPIO node and set its pin to the matching relay (10, 11, 22, 23) and its active level to HIGH (the ESP32-C6-EVB relays energize on a HIGH). Each GPIO node already renders its own on/off toggle on the page, so manual per-zone control is done — no code required.

Describe uses one AI-generation credit per build, from its own daily pool (the Free plan includes a daily allowance). Prefer to build by hand? Every node it uses is in the palette — drag a Page, four GPIO outputs, a Schedule, and an NTP node and you'll have the same diagram.
Give each zone node a clear label. ESP-GenUI assigns every node a unique internal id and names its generated helper/stub after it — but it tags each one with a comment showing the node's label (e.g. // gpio/Front Lawn). So label your zones clearly; that's how you'll tell the generated functions apart when you fill them in in Step 3.

Step 3 Add the watering logic in Callbacks.h

Because each zone is a GPIO node with its pin and active level set in the Inspector, ESP-GenUI has already written the hard part. When you generate, Callbacks.h comes pre-populated: a helper for every GPIO zone and a pair of stubs for every Schedule. The on-page toggles already call the helpers, so manual control needs no code at all — the only thing you fill in is what each schedule should do.

Edit the stubs the generator wrote — don't rename them. Every generated function is named after its node's internal id (a long unique string, e.g. onScheduleStart__9e6ea0b9_0086_…), and the rest of the sketch calls it by that exact name. If you retype a stub with a friendlier name of your own, the build fails with 'onScheduleStart_…' was not declared in this scope. Fill in the bodies of the stubs already in the file and leave their names exactly as generated.

You don't need to memorize those ids — the generator tags each helper and stub with a comment showing the node's label. Here's the shape of what's in your Callbacks.h; find each zone by its // gpio/… and // schedule/… comment and copy the real name from there:

// ---- Generated near the top: one helper per GPIO zone ----
//   setGpio_<id>(true|false)   — gpio/Front Lawn
//   setGpio_<id>(true|false)   — gpio/Back Lawn
//   setGpio_<id>(true|false)   — gpio/Flower Beds
//   setGpio_<id>(true|false)   — gpio/Drip Line
// (each <id> is that node's unique id — use the real names from your file)
// ---- Fill in the BODY of each schedule stub (its name is already there) ----
// schedule/Front Lawn — open only the front lawn while its window runs.
inline void onScheduleStart_<id>(int index, int duration) {
  allZonesOff();
  setGpio_<id>(true);    // gpio/Front Lawn
}
inline void onScheduleEnd_<id>(int index) {
  setGpio_<id>(false);   // gpio/Front Lawn
}

// Repeat the same three lines in the schedule/Back Lawn, schedule/Flower Beds
// and schedule/Drip Line stubs — each opening its own zone.
Your schedule survives a reboot — for free. Alongside the start/end hooks, ESP-GenUI generates onScheduleLoad_<id>() and onScheduleChanged_<id>() for each Schedule (plus a matching pair for the pause switch), already filled in. Their default bodies persist the entry list to flash (NVS) with the Preferences library — which ESP-GenUI includes for you — so the watering times you set aren't lost on a power cut. Leave them as-is unless you'd rather store the schedule elsewhere (an SD card, LittleFS, a cloud sync).
Why one Schedule per zone? A schedule entry stores only a day, a time, and a duration — not which zone. So a single shared schedule can only tell its entries apart by their position in the list (the index), which breaks the moment a zone needs two watering windows or you reorder them. Giving each zone its own Schedule sidesteps that entirely: the callback identity is the zone.
Active level is a setting, not code. Whether a relay is active-HIGH or active-LOW is the GPIO node's active level property in the Inspector. Set it once there and the generated setGpio_* helpers stay logicaltrue always means the valve is open. If a relay clicks on at boot, flip that node's active level; you never hand-edit pin logic.

Because this file is yours, your edits survive a regenerate: tweak the diagram and generate again, and ESP-GenUI rewrites the site files but merges your callbacks instead of overwriting them.

Step 4 Generate, compile & flash

  1. Click Generate Code. You'll get a complete Arduino sketch — demo.ino, EmbeddedSite.h, and your edited Callbacks.h.
  2. Plug the board into your computer with USB-C. In a Chromium-based browser (Chrome or Edge), use Compile to build it server-side and flash it over Web Serial — no local Arduino IDE needed.
  3. Prefer your own toolchain? Use Download ZIP, or — on Chrome/Edge — Save to Folder, which writes the sketch straight into a folder on disk (no ZIP to unzip) and remembers it, so the Arduino IDE, PlatformIO, or VS Code picks the files up in place each time you regenerate. Either way, select an ESP32-C6 board target. Save to Folder is the fastest loop for this path — when a folder is attached, ESP-GenUI can even run the Diff/Merge automatically on the next save so your Callbacks.h edits are preserved.
Board target. Make sure the ESP32-C6 is selected before flashing — an image built for a different chip won't boot. When you compile from the browser, connecting the board over USB lets it detect the chip and pick the right target for you.
Compile error — 'onScheduleStart_…' was not declared in this scope? This means a schedule (or GPIO) stub in Callbacks.h was renamed or rewritten, so its name no longer matches what the rest of the sketch calls — usually from typing your own function name instead of editing the generated one. To recover:
  1. Regenerate Callbacks.h (Generate Code again, or accept ESP-GenUI's stubs in the Save-to-Folder Diff/Merge). This restores every stub with its correct generated name.
  2. Move your logic into the stub bodies — find each // schedule/… stub and fill in its body as in Step 3, calling the // gpio/… helper by the exact name from the file. Delete any leftover functions you'd named yourself.
  3. Compile again. The generated caller now finds every stub.
The sprinkler control web page served by the ESP32, shown on a phone (view 1 of 2). The sprinkler control web page served by the ESP32, shown on a phone (view 2 of 2).
Fig. 5 — The live page the ESP32 serves once flashed, on your phone or laptop.

Step 5 Test and schedule

After flashing, the board joins your Wi-Fi and serves the page at its IP address (or its .local name if you added an mDNS node). Open it from a browser on the same network and:

  1. Bench-test first, dry. Before trusting the valves, toggle each zone and listen for the relay click and its status LED — confirm Zone 1 drives Relay 1, and so on.
  2. Test with water. Turn on the transformer and toggle one zone; you should hear the valve open and see that zone water. Toggle it off and confirm it closes.
  3. Set each zone's schedule. Open a zone's Schedule and add its watering windows (e.g. 6:00 AM Mon/Wed/Fri for 10 minutes). Because each schedule drives just one valve, every zone keeps its own independent days and times — and a zone can have as many windows as you like. The NTP node keeps the clock accurate so windows fire on time.
Add a safety timeout. A valve stuck open wastes a lot of water. Consider a maximum run time per zone in your callback logic so a zone always closes even if a schedule or toggle is left on.

Where to take it next

  • Rain skip. Wire a rain sensor to one of the opto-isolated inputs (GPIO1/2/3/15) and skip a scheduled run when it's wet.
  • Flow feedback. Add a flow switch to another input to detect a zone that isn't actually flowing (a stuck valve or a burst line).
  • Notifications. Drop in a Webhook node to ping your phone when a watering cycle starts or finishes.
  • Remote access. Add the Auth Gate node so the page asks for a username and password before anyone can trigger a zone.

That's the pattern for every guide here: pick real hardware, wire it, sketch the interface, map a few callbacks, and flash.

← All Build Guides Open the editor →