How to Build a Plant Disco Guardian with ESP32

A plant monitor that celebrates watering with lights and sound

ESP32GardenBeginner35 minutes3 components

Updated

How to Build a Plant Disco Guardian with ESP32
For illustrative purposes only
On this page

What you'll build

This guide shows you how to build a Plant Disco Guardian -- an ESP32-based soil moisture monitor that alerts you when your plant is thirsty and runs a brief light and sound celebration when you water it. A capacitive soil moisture sensor continuously reads the hydration level. When it drops below a configurable threshold, a WS2812B LED ring glows red and the buzzer emits a periodic chirp. When you water the plant and moisture rises back above the threshold, the guardian runs a rainbow chasing animation and a short victory melody before returning to its calm idle glow.

The project teaches you how to read an analogue capacitive sensor on the ESP32's ADC, apply hysteresis-based threshold logic to avoid false triggers when moisture hovers near the boundary, and drive a 12-LED WS2812B ring through the FastLED library. The disco celebration sequence chains multiple animation patterns using non-blocking timers so the main loop stays responsive throughout.

When the build is complete you have a self-contained monitor that runs from any USB supply. The threshold constants are easy to tune for different soil types and pot sizes, and the code is structured so you can add new animation routines or extend it with Wi-Fi logging. If you want to explore more FastLED animation techniques, the gesture-controlled mood lamp builds on the same library with hand-wave interaction.

What you are building

This build has a specific scope. Knowing what is and is not included helps you decide whether to extend it later.

  1. Read raw analogue moisture values from the Capacitive Soil Moisture Sensor on GPIO 34 and compare them against two configurable thresholds.
  2. Drive the WS2812B LED Ring with a calm idle colour when moisture is normal, switch to a solid red alert when the soil is dry.
  3. Sound a short periodic chirp on the Piezo Buzzer when the dry alert is active.
  4. Trigger a rainbow chase animation and a short victory melody when moisture recovers above the wet threshold, running the celebration for DISCO_DURATION milliseconds before returning to idle.
  5. Print moisture readings and state changes to Serial Monitor at 115200 baud for calibration and debugging.

Out of scope: Wi-Fi, push notifications, moisture data logging, multiple sensor support, and battery power management. These are reasonable extensions but are not part of this guide.

Upload and calibrate

Install libraries in the Arduino IDE Library Manager before opening the sketch:

  • FastLED (by Daniel Garcia)

Pin and threshold constants are near the top of the sketch:

#define SOIL_PIN        34
#define LED_PIN          4
#define BUZZER_PIN      26
#define NUM_LEDS        12
#define DRY_THRESHOLD 1700   // below this value = dry alert
#define WET_THRESHOLD 2200   // above this value = watered, trigger disco
#define DISCO_DURATION 5000  // celebration length in milliseconds

The sensor produces lower ADC values in wet soil and higher values in dry soil. DRY_THRESHOLD and WET_THRESHOLD are starting points; calibrate them for your specific sensor and soil mix by watching the Serial Monitor readings with the probe in dry soil, then in freshly watered soil, and adjusting the constants to sit comfortably between those two readings.

Upload: select your ESP32 board and port, then click Upload. Open the Serial Monitor at 115200 baud.

Expected Serial Monitor output:

Plant Disco Guardian ready
Moisture: 1580  State: DRY
Moisture: 1574  State: DRY
Moisture: 2310  State: DISCO
Moisture: 2295  State: IDLE

After the "Plant Disco Guardian ready" message, readings appear once per second. Dry soil typically gives readings below 1700; submerged or very wet soil reads above 2200. If readings look inverted (dry soil gives low numbers rather than high), your sensor module may have a different output polarity — swap the threshold comparison operators in the sketch.

Troubleshooting

  • LED ring stays off or flickers randomly. Confirm the ring's 5V wire goes to the USB 5V (VIN) pin, not 3V3. Also check that the 330 Ω resistor is in the DIN line and that the GND wire is connected.
  • Moisture readings are stuck at 4095 or 0. GPIO 34 is not connected to the sensor SIG pin, or the sensor VCC is not powered. Verify all three sensor wires (VCC, GND, SIG) are seated firmly.
  • Disco never triggers after watering. The WET_THRESHOLD may be set too high for your sensor. Water the soil thoroughly, note the Serial Monitor reading, and set WET_THRESHOLD about 50 counts below that value.
  • Dry alert triggers even with wet soil. The DRY_THRESHOLD is set too high. Run the probe in freshly watered soil, note the reading, and set DRY_THRESHOLD about 50 counts above that value to leave a comfortable gap.
  • Buzzer makes no sound. GPIO 26 is not a PWM-capable pin on all ESP32 board variants. If you hear nothing, try moving the buzzer wire to GPIO 25 and updating BUZZER_PIN in the sketch.
  • Serial Monitor shows garbled characters. The baud rate is not set to 115200. Change it in the Serial Monitor dropdown and press the reset button on the ESP32.

Going further

The most practical extension is replacing the single threshold chirp with a graduated alert. Because SOIL_PIN gives a continuous analogue value, you can map moisture level to LED colour on a gradient -- green for well-watered, yellow for borderline, red for dry -- without adding any hardware. The FastLED CHSV colour model makes this straightforward: map the raw ADC reading to a hue value and call fill_solid.

A second direction is adding Wi-Fi so the ESP32 posts readings to a home automation system whenever the state changes. The ESP32's built-in Wi-Fi can send an HTTP POST to Home Assistant, a simple MQTT broker, or a webhook in a few dozen lines alongside the existing sensor and LED code. Because the state machine is already cleanly separated in the sketch, the networking layer drops in without restructuring the core logic.

Wiring diagram

Loading diagram…
Interactive wiring diagram

Components needed

Supplier links, prices, and availability are shown as a guide and may change. Schematik may earn a commission from purchases made through affiliate links.

Assembly

1

Connect the soil moisture sensor

Wire capacitive soil moisture sensor VCC to 3.3V, GND to ground, and analog output to GPIO34.

2

Wire the LED ring

Connect WS2812B 5V to USB 5V, GND to ESP32 GND, and DIN to GPIO4. Add a 330 Ω resistor on the DIN line.

3

Add the buzzer

Wire piezo buzzer signal to GPIO26 with the other lead to GND.

4

Calibrate and deploy

Flash the sketch and open Serial Monitor at 115200 baud. Read the moisture values when the soil is dry and wet, then adjust DRY_THRESHOLD and WET_THRESHOLD in the code. Stick the sensor in your plant pot and enjoy the disco.

Pin assignments

PinConnectionType
3V3soil-sensor-1 VCCPOWER
GNDsoil-sensor-1 GNDGROUND
GPIO 34soil-sensor-1 SIGANALOG
5Vplant-led-ring-1 5VPOWER
GNDplant-led-ring-1 GNDGROUND
GPIO 4plant-led-ring-1 DINDATA
GNDplant-buzzer-1 GNDGROUND
GPIO 26plant-buzzer-1 SIGPWM

Code

Arduino C++
#include <FastLED.h>

#define SOIL_PIN 34
#define LED_PIN 4
#define NUM_LEDS 12
#define BUZZER_PIN 26

CRGB leds[NUM_LEDS];

const int DRY_THRESHOLD = 1700;
const int WET_THRESHOLD = 2200;

bool wasDry = false;
unsigned long discoEndMs = 0;
const unsigned long DISCO_DURATION = 5000;

int smoothRead(int pin, int samples = 8) {
  long sum = 0;
  for (int i = 0; i < samples; i++) {
    sum += analogRead(pin);
    delay(2);
  }
  return sum / samples;
}

void discoPattern() {
  for (int i = 0; i < NUM_LEDS; i++) {
    leds[i] = CHSV((millis() / 6 + i * 21) % 255, 255, 200);
  }
}

void warningPattern() {
  uint8_t pulse = beatsin8(30, 60, 200);
  fill_solid(leds, NUM_LEDS, CRGB(pulse, 0, 0));
}

void idlePattern() {
  uint8_t glow = beatsin8(15, 40, 120);
  fill_solid(leds, NUM_LEDS, CRGB(0, glow, glow / 3));
}

void setup() {
  Serial.begin(115200);
  delay(100);
  FastLED.addLeds<NEOPIXEL, LED_PIN>(leds, NUM_LEDS);
  FastLED.setBrightness(140);
  pinMode(BUZZER_PIN, OUTPUT);
  Serial.println("Plant Disco Guardian ready");
}

void loop() {
  int moisture = smoothRead(SOIL_PIN);
  bool isDry = moisture < DRY_THRESHOLD;
  bool isWet = moisture > WET_THRESHOLD;

  if (wasDry && isWet) {
    discoEndMs = millis() + DISCO_DURATION;
    tone(BUZZER_PIN, 1800, 100);
    delay(110);
    tone(BUZZER_PIN, 2200, 100);
    delay(110);
    tone(BUZZER_PIN, 2600, 150);
    Serial.println("DISCO TIME!");
  }

  bool discoMode = millis() < discoEndMs;

  if (discoMode) {
    discoPattern();
  } else if (isDry) {
    warningPattern();
    if ((millis() / 3000) % 2 == 0) {
      tone(BUZZER_PIN, 900, 80);
    }
  } else {
    idlePattern();
  }

  FastLED.show();
  wasDry = isDry;

  Serial.printf("Moisture: %d | %s\n", moisture,
    discoMode ? "DISCO" : isDry ? "DRY" : "OK");
  delay(discoMode ? 30 : 200);
}

// Run this and build other cool things at schematik.io
Libraries: FastLED