"""
Smart Plant Watering System
Raspberry Pi Pico with Soil Moisture Sensor
Wokwi Simulator Compatible
Components:
- Raspberry Pi Pico
- Soil Moisture Sensor (Analog)
- Water Pump (DC Motor)
- Relay Module
- LCD Display (I2C 16x2)
- LED Indicators (Dry, Wet, Pump)
- Manual Override Button
"""
from machine import Pin, ADC, I2C
import time
# ============ PIN CONFIGURATION ============
MOISTURE_PIN = 26 # ADC0 - Soil moisture sensor (analog)
RELAY_PIN = 14 # Relay control for water pump
LED_DRY = 15 # Red LED - Soil is dry
LED_WET = 16 # Green LED - Soil is wet
LED_PUMP = 17 # Blue LED - Pump running
BUTTON_PIN = 18 # Manual watering button
LED_STATUS = 25 # Built-in LED - Status indicator
# Moisture thresholds (0-65535 for 16-bit ADC)
# Lower value = more moisture
DRY_THRESHOLD = 30000 # Below this = soil is dry
WET_THRESHOLD = 45000 # Above this = soil is wet
# Watering settings
PUMP_DURATION = 5 # Pump runs for 5 seconds
CHECK_INTERVAL = 2 # Check moisture every 2 seconds
COOLDOWN_TIME = 10 # Wait 10 seconds after watering
# ============ HARDWARE INITIALIZATION ============
# Initialize soil moisture sensor (ADC)
moisture_sensor = ADC(Pin(MOISTURE_PIN))
# Initialize relay for pump
relay = Pin(RELAY_PIN, Pin.OUT)
relay.off() # Pump off initially
# Initialize LED indicators
led_dry = Pin(LED_DRY, Pin.OUT)
led_wet = Pin(LED_WET, Pin.OUT)
led_pump = Pin(LED_PUMP, Pin.OUT)
led_status = Pin(LED_STATUS, Pin.OUT)
# Initialize manual button with pull-down
button = Pin(BUTTON_PIN, Pin.IN, Pin.PULL_DOWN)
# Initialize I2C for LCD (GP0=SDA, GP1=SCL)
i2c = I2C(0, scl=Pin(1), sda=Pin(0), freq=400000)
# ============ LCD 16x2 I2C CLASS ============
class LCD_I2C:
def __init__(self, i2c, addr=0x27, rows=2, cols=16):
self.i2c = i2c
self.addr = addr
self.rows = rows
self.cols = cols
# LCD Commands
self.LCD_CLEARDISPLAY = 0x01
self.LCD_RETURNHOME = 0x02
self.LCD_ENTRYMODESET = 0x04
self.LCD_DISPLAYCONTROL = 0x08
self.LCD_FUNCTIONSET = 0x20
self.LCD_SETDDRAMADDR = 0x80
# Flags
self.LCD_DISPLAYON = 0x04
self.LCD_CURSOROFF = 0x00
self.LCD_BLINKOFF = 0x00
self.LCD_ENTRYLEFT = 0x02
self.LCD_ENTRYSHIFTDECREMENT = 0x00
self.LCD_4BITMODE = 0x00
self.LCD_2LINE = 0x08
self.LCD_5x8DOTS = 0x00
self.LCD_BACKLIGHT = 0x08
self.LCD_NOBACKLIGHT = 0x00
self.backlight_state = self.LCD_BACKLIGHT
# Initialize display
self.init_display()
def write_byte(self, byte, mode):
"""Write a byte to the LCD"""
high_bits = mode | (byte & 0xF0) | self.backlight_state
low_bits = mode | ((byte << 4) & 0xF0) | self.backlight_state
# Write high nibble
self.i2c.writeto(self.addr, bytearray([high_bits]))
self.toggle_enable(high_bits)
# Write low nibble
self.i2c.writeto(self.addr, bytearray([low_bits]))
self.toggle_enable(low_bits)
def toggle_enable(self, byte):
"""Toggle enable bit"""
time.sleep_us(1)
self.i2c.writeto(self.addr, bytearray([byte | 0x04]))
time.sleep_us(1)
self.i2c.writeto(self.addr, bytearray([byte & ~0x04]))
time.sleep_us(50)
def init_display(self):
"""Initialize LCD display"""
time.sleep_ms(50)
# Put LCD into 4-bit mode
self.write_byte(0x03, 0)
time.sleep_ms(5)
self.write_byte(0x03, 0)
time.sleep_us(150)
self.write_byte(0x03, 0)
self.write_byte(0x02, 0)
# Function set
self.write_byte(self.LCD_FUNCTIONSET | self.LCD_4BITMODE |
self.LCD_2LINE | self.LCD_5x8DOTS, 0)
# Display control
self.write_byte(self.LCD_DISPLAYCONTROL | self.LCD_DISPLAYON |
self.LCD_CURSOROFF | self.LCD_BLINKOFF, 0)
# Clear display
self.clear()
# Entry mode
self.write_byte(self.LCD_ENTRYMODESET | self.LCD_ENTRYLEFT |
self.LCD_ENTRYSHIFTDECREMENT, 0)
def clear(self):
"""Clear the display"""
self.write_byte(self.LCD_CLEARDISPLAY, 0)
time.sleep_ms(2)
def set_cursor(self, row, col):
"""Set cursor position"""
row_offsets = [0x00, 0x40]
self.write_byte(self.LCD_SETDDRAMADDR | (col + row_offsets[row]), 0)
def print(self, text, row=0, col=0):
"""Print text at specified position"""
self.set_cursor(row, col)
for char in str(text):
self.write_byte(ord(char), 1)
def backlight(self, state):
"""Turn backlight on/off"""
if state:
self.backlight_state = self.LCD_BACKLIGHT
else:
self.backlight_state = self.LCD_NOBACKLIGHT
self.i2c.writeto(self.addr, bytearray([self.backlight_state]))
# Initialize LCD
try:
lcd = LCD_I2C(i2c)
lcd_available = True
print("LCD initialized successfully")
except:
lcd_available = False
print("LCD not found - using Serial Monitor only")
# ============ GLOBAL VARIABLES ============
watering_count = 0
last_watering_time = 0
system_enabled = True
# ============ HELPER FUNCTIONS ============
def startup_sequence():
"""Display startup message and test components"""
print("\n" + "="*50)
print(" SMART PLANT WATERING SYSTEM")
print(" Raspberry Pi Pico + Moisture Sensor")
print("="*50)
print("\nInitializing components...")
if lcd_available:
lcd.clear()
lcd.print("Plant Watering", 0, 1)
lcd.print("Starting...", 1, 2)
# Test LEDs
print("Testing indicators...")
led_dry.on()
led_wet.on()
led_pump.on()
time.sleep(0.5)
led_dry.off()
led_wet.off()
led_pump.off()
# Test pump relay (very brief)
print("Testing pump relay...")
relay.on()
time.sleep(0.2)
relay.off()
time.sleep(1)
print("\nConfiguration:")
print(f" Dry Threshold: {DRY_THRESHOLD}")
print(f" Wet Threshold: {WET_THRESHOLD}")
print(f" Pump Duration: {PUMP_DURATION}s")
print(f" Check Interval: {CHECK_INTERVAL}s")
if lcd_available:
lcd.clear()
lcd.print("System Ready!", 0, 2)
lcd.print("Monitoring...", 1, 1)
print("\nSystem ready!")
print("Monitoring soil moisture...")
print("="*50 + "\n")
def read_moisture():
"""Read soil moisture sensor (ADC)"""
# Read ADC value (0-65535 for 16-bit)
raw_value = moisture_sensor.read_u16()
# Convert to percentage (inverted - lower reading = more moisture)
# 0 = very wet (max moisture)
# 65535 = very dry (no moisture)
moisture_percent = 100 - int((raw_value / 65535) * 100)
return raw_value, moisture_percent
def get_soil_status(raw_value):
"""Determine soil moisture status"""
if raw_value < DRY_THRESHOLD:
return "WET"
elif raw_value > WET_THRESHOLD:
return "DRY"
else:
return "MODERATE"
def update_leds(status):
"""Update LED indicators based on soil status"""
if status == "DRY":
led_dry.on()
led_wet.off()
elif status == "WET":
led_dry.off()
led_wet.on()
else: # MODERATE
led_dry.off()
led_wet.off()
def water_plants(manual=False):
"""Activate water pump to water plants"""
global watering_count, last_watering_time
watering_count += 1
trigger = "MANUAL" if manual else "AUTO"
print("\n" + "š§"*25)
print(f"š± WATERING PLANTS! ({trigger})")
print(f"Watering cycle #{watering_count}")
print("š§"*25)
if lcd_available:
lcd.clear()
lcd.print("š§ WATERING! š§", 0, 0)
lcd.print(f"Cycle #{watering_count}", 1, 2)
# Activate pump
relay.on()
led_pump.on()
# Run pump for specified duration with countdown
for remaining in range(PUMP_DURATION, 0, -1):
print(f"Watering... {remaining}s remaining")
led_status.on()
time.sleep(0.5)
led_status.off()
time.sleep(0.5)
# Turn off pump
relay.off()
led_pump.off()
last_watering_time = time.time()
print("ā Watering complete!")
print(f"Total waterings: {watering_count}")
print("š§"*25 + "\n")
def display_status(raw_value, moisture_percent, status):
"""Display current status on LCD"""
if lcd_available:
lcd.clear()
lcd.print(f"Moisture: {moisture_percent}%", 0, 0)
lcd.print(f"Status: {status}", 1, 0)
def print_readings(raw_value, moisture_percent, status):
"""Print readings to serial monitor"""
print("\n" + "-"*50)
print("š SOIL MOISTURE READING")
print("-"*50)
print(f"Raw Value: {raw_value}")
print(f"Moisture: {moisture_percent}%")
print(f"Status: {status}")
if status == "DRY":
print("ā ļø WARNING: Soil is too dry!")
elif status == "WET":
print("ā Good: Soil has adequate moisture")
else:
print("ā¹ļø Moderate: Soil moisture is acceptable")
print(f"Waterings: {watering_count}")
print("-"*50)
def check_manual_button():
"""Check if manual watering button is pressed"""
return button.value() == 1
# ============ MAIN PROGRAM ============
def main():
global last_watering_time
# Startup
startup_sequence()
# Turn off all outputs initially
relay.off()
led_dry.off()
led_wet.off()
led_pump.off()
led_status.off()
last_check_time = 0
last_button_state = 0
last_button_time = 0
print("System running. Monitoring soil moisture and button...\n")
while True:
try:
current_time = time.time()
# Check manual button (with debouncing)
button_state = button.value()
if button_state == 1 and last_button_state == 0:
if current_time - last_button_time > 0.5: # 500ms debounce
print("\nš MANUAL WATERING TRIGGERED")
water_plants(manual=True)
last_button_time = current_time
time.sleep(COOLDOWN_TIME)
last_button_state = button_state
# Check moisture at regular intervals
if current_time - last_check_time >= CHECK_INTERVAL:
# Read sensor
raw_value, moisture_percent = read_moisture()
status = get_soil_status(raw_value)
# Update indicators
update_leds(status)
# Display on LCD
display_status(raw_value, moisture_percent, status)
# Print to serial
print_readings(raw_value, moisture_percent, status)
# Check if watering is needed
if status == "DRY":
# Check cooldown period
if current_time - last_watering_time > COOLDOWN_TIME:
water_plants(manual=False)
time.sleep(COOLDOWN_TIME) # Wait after watering
last_check_time = current_time
# Blink status LED slowly
if int(current_time * 2) % 2 == 0:
led_status.on()
else:
led_status.off()
time.sleep(0.1) # Small delay to reduce CPU usage
except KeyboardInterrupt:
print("\n\nSystem stopped by user")
# Turn off all outputs
relay.off()
led_dry.off()
led_wet.off()
led_pump.off()
led_status.off()
if lcd_available:
lcd.clear()
lcd.print("System Stopped", 0, 0)
break
except Exception as e:
print(f"\nError: {e}")
relay.off() # Ensure pump is off on error
time.sleep(1)
# Run the program
if __name__ == "__main__":
main()