Skip to main content

Firmware (MicroPython)

The firmware runs on an Development board of choice

Development board using MicroPython
A compact version of Python for microcontrollers like the ESP32 and Raspberry Pi Pico. It lets you script directly on embedded hardware.
.
It reads the Capacitive Soil Moisture Sensor
Measures soil humidity via capacitance change instead of resistance. Corrosion-resistant and provides analog voltage proportional to moisture.
Capacitive Soil Moisture Sensor
, decides when to water based on calibrated thresholds, soft-starts the valve through a MOSFET
Metal-Oxide-Semiconductor Field-Effect Transistor - an electronic power switch. Logic-level MOSFETs can be driven directly from GPIO pins.
using PWM
Pulse Width Modulation - controls average power by switching rapidly. The duty cycle determines the effective output.
, and drives the on-board RGB LED
A light-emitting diode that can produce a wide range of colors by combining red, green, and blue light.
to indicate system state.

This page describes the structure and provides a reference implementation for main.py.


File layout

firmware/
└─ main.py # main program for the auto-watering pot

Copy main.py to the Development board of choice

Development board (e.g. using Thonny, rshell, or mpremote) so it runs on boot.


main.py (reference implementation)

⚠️ Before using this code, make sure you have calibrated the moisture sensor and updated the DRY_THRESHOLD and WET_THRESHOLD values. See the Calibration page for details.

⚠️ Before plugging in the Development board make sure the valve is disconnected, since accidental current surge may damage your computer!

from machine import Pin, ADC, PWM
from time import sleep, sleep_ms

# ============================================================
# Smart Auto-Watering Pot – Universal Version
# Works on ESP32-C3 (with WS2812 RGB) or Raspberry Pi Pico (simple LED)
# Just comment/uncomment the section for your board below.
# Once configured for your board you can delete or just uncomment the other section.
# ============================================================


# ============================================================
# === ESP32-C3 (with on-board WS2812 RGB LED) ===
# ============================================================

# from neopixel import NeoPixel
# MOISTURE_PIN = 0 # ADC input for capacitive sensor
# VALVE_PIN = 6 # MOSFET gate (PWM capable)
# LED_PIN = 10 # WS2812 RGB LED (on-board)
# np = NeoPixel(Pin(LED_PIN), 1)
# BRIGHTNESS = 0.05

# def set_led(r, g, b):
# """Set RGB LED color with brightness limit."""
# r = int(r * BRIGHTNESS)
# g = int(g * BRIGHTNESS)
# b = int(b * BRIGHTNESS)
# np[0] = (g, r, b) # GRB order for WS2812
# np.write()

# def led_closed():
# set_led(40, 0, 0) # red = idle/closed

# def led_open():
# set_led(0, 40, 0) # green = watering

# def led_ramping():
# set_led(40, 20, 0) # orange = transition


# ============================================================
# === Raspberry Pi Pico (with simple on-board LED) ===
# ============================================================

MOISTURE_PIN = 26 # ADC0 (GPIO26)
VALVE_PIN = 15 # PWM output to MOSFET gate
LED_PIN = 25 # LED GPIO 25 on-board

led = Pin(LED_PIN, Pin.OUT)

def set_led(state):
"""Helper to toggle the LED on/off."""
led.value(state)

def led_closed():
led.value(0) # LED off = idle/closed

def led_open():
led.value(1) # LED on = watering

def led_ramping():
"""Quick blink for transition."""
for _ in range(3):
led.toggle()
sleep(0.1)
led.toggle()
sleep(0.1)


# ============================================================
# Moisture Sensor and Valve Control
# ============================================================

adc = ADC(Pin(MOISTURE_PIN))

def read_moisture(samples=16):
"""Average several ADC readings to reduce noise."""
total = 0
for _ in range(samples):
total += adc.read_u16()
sleep_ms(10)
return total // samples

# IMPORTANT: higher ADC reading = drier soil on this sensor type
DRY_THRESHOLD = 30000 # above this -> soil considered too dry
WET_THRESHOLD = 25000 # below this -> soil considered wet enough

# Valve via PWM
pwm = PWM(Pin(VALVE_PIN), freq=1000)
pwm.duty_u16(0)

def valve_soft_open(open_time=1.5, min_duty=20000, max_duty=65535, steps=20):
"""Soft-start the valve by ramping PWM duty from min_duty to max_duty."""
led_ramping()
step_delay = open_time / steps
duty = min_duty
for _ in range(steps):
pwm.duty_u16(int(duty))
duty += (max_duty - min_duty) / steps
sleep(step_delay)
pwm.duty_u16(max_duty)
led_open()

def valve_soft_close(close_time=0.8, steps=15):
"""Soft-close the valve by ramping PWM duty down to zero."""
led_ramping()
step_delay = close_time / steps
for i in range(steps, -1, -1):
duty = int(65535 * i / steps)
pwm.duty_u16(duty)
sleep(step_delay)
pwm.duty_u16(0)
led_closed()


# ============================================================
# Main Loop
# ============================================================

watering = False
WATER_TIME_SEC = 5 # watering duration (seconds)
MEASURE_INTERVAL = 5 # delay between checks (seconds)

print("Starting auto-watering (NOTE: higher ADC = drier soil).")
led_closed()

while True:
moisture = read_moisture()
print("Moisture ADC:", moisture, " watering:", watering)

if not watering:
if moisture > DRY_THRESHOLD:
print("Soil dry -> start watering")
watering = True
valve_soft_open()
sleep(WATER_TIME_SEC)
else:
if moisture < WET_THRESHOLD:
print("Soil wet -> stop watering")
valve_soft_close()
watering = False

sleep(MEASURE_INTERVAL)


Next steps

  1. Run the calibration script from the Calibration page and record ADC
    Analog-to-Digital Converter - converts analog voltages into digital values that microcontrollers can read.
    values for dry and wet soil.
  2. Update DRY_THRESHOLD and WET_THRESHOLD in main.py.
  3. Monitor serial output during the first real watering sessions and fine-tune the thresholds and timings if needed.