from machine import Pin, ADC, I2C, SPI
from time import sleep, ticks_ms, ticks_diff
import os
# --- Import custom libraries ---
# Ensure these files are uploaded to your Wokwi project:
# sdcard.py
# ds1307.py
# lcd_api.py
# i2c_lcd.py
import sdcard
from ds1307 import DS1307
from lcd_api import LcdApi
from i2c_lcd import I2cLcd
# === PIN SETUP ===
# NTC Temperature Sensor (simulating a soil/ambient temp sensor)
# Connected to ADC Pin 12. This requires a voltage divider on the breadboard.
ntc_adc = ADC(Pin(12))
# ADC configuration for NTC
ntc_adc.atten(ADC.ATTN_11DB) # Set input range to 0-3.3V (or full ADC range)
ntc_adc.width(ADC.WIDTH_12BIT) # Set 12-bit resolution (0-4095)
# LDR (Light Dependent Resistor)
# Connected to ADC Pin 13. This also requires a voltage divider on the breadboard.
ldr_adc = ADC(Pin(13))
# ADC configuration for LDR
ldr_adc.atten(ADC.ATTN_11DB) # Set input range to 0-3.3V (or full ADC range)
ldr_adc.width(ADC.WIDTH_12BIT) # Set 12-bit resolution (0-4095)
relay = Pin(15, Pin.OUT) # Relay output for pump/actuator. Connect to IN pin of relay module.
relay.off() # Ensure relay starts in OFF state
button = Pin(25, Pin.IN, Pin.PULL_UP) # Manual trigger button. Connect one side to GND.
# RGB LED - Common Anode (LOW = ON, HIGH = OFF)
# Connected via resistors to ensure proper current limiting. COM pin to 3.3V/5V.
red_led = Pin(26, Pin.OUT)
green_led = Pin(27, Pin.OUT)
blue_led = Pin(14, Pin.OUT)
red_led.on(); green_led.on(); blue_led.on() # Initialize: Turn off all RGB LEDs (HIGH for Common Anode)
# White LED for darkness indication
# Connected directly to Pin 4 with a series resistor (e.g., 330 Ohm) to GND.
white_led_pin = Pin(4, Pin.OUT)
white_led_pin.off() # Initialize: Turn off white LED
# I2C setup for LCD and RTC
# SCL (Clock) on Pin 22, SDA (Data) on Pin 21
i2c = I2C(0, scl=Pin(22), sda=Pin(21))
# DS1307 Real-Time Clock module
rtc = DS1307(i2c)
# LCD init (I2C address 0x27, 2 rows, 16 columns)
# If your LCD is not working, try changing 0x27 to 0x3F or use an I2C scanner.
lcd = I2cLcd(i2c, 0x27, 2, 16)
# SD Card init
sd_card_ok = False
try:
# SPI Bus 1, Baudrate 1MHz
# SCK (Clock) on Pin 18, MOSI (Master Out Slave In) on Pin 23, MISO (Master In Slave Out) on Pin 19
spi = SPI(1, baudrate=1000000, sck=Pin(18), mosi=Pin(23), miso=Pin(19))
cs = Pin(5, Pin.OUT) # Chip Select pin for SD card
sd = sdcard.SDCard(spi, cs)
vfs = os.VfsFat(sd)
os.mount(vfs, "/sd") # Mount SD card as "/sd"
sd_card_ok = True
lcd.clear()
lcd.putstr("SD Card OK")
sleep(1) # Display for a short period
except Exception as e:
lcd.clear()
lcd.putstr("SD Card Error")
print(f"SD Card Error: {e}") # Print detailed error to console for debugging
sleep(2) # Display longer for user to see
# === VARIABLES ===
# Operation mode: True = Manual, False = Auto
is_manual_mode = False
manual_watering_active = False # Flag to indicate if manual watering is running
last_button_press_time = 0 # For button debouncing
current_light_condition = None # To track light changes for logging (e.g., "Day" or "Night")
last_rgb_color = None # To track the last RGB LED color
# === FUNCTIONS ===
def calculate_temperature(raw_adc_value):
"""
Calculates temperature in Celsius from raw NTC ADC value.
This is an approximate formula. For higher accuracy, use the Steinhart-Hart equation
with specific NTC values (Beta, R25, etc.).
Assumption: NTC in a voltage divider configuration with a 10k Ohm resistor to GND.
ADC value decreases as temperature increases (for most NTCs).
"""
# Fixed resistor value in voltage divider (e.g., 10k Ohm)
R_FIXED = 10000.0
# NTC resistance at reference temperature (e.g., 10k Ohm at 25C)
R_NTC_AT_REF = 10000.0
# Reference temperature in Kelvin (25C = 298.15K)
TEMP_REF_K = 298.15
# Beta constant of the thermistor (typically between 3000-4500)
BETA = 3950.0 # Typical value for 10k NTC
# Maximum ADC reading (12-bit = 4095)
MAX_ADC_VALUE = 4095.0
# Calculate NTC resistance
# This is the formula for a voltage divider where NTC is connected to 3.3V and AO, and R_FIXED to AO and GND.
# If NTC to GND and R_FIXED to 3.3V, the formula is R_NTC = R_FIXED * (MAX_ADC_VALUE / raw_adc_value - 1)
if raw_adc_value == 0: # Avoid division by zero
return 999.0 # Return high value as error indicator or very low temperature
voltage_at_adc = raw_adc_value * (3.3 / MAX_ADC_VALUE) # Voltage at ADC pin (if Vref is 3.3V)
# Formula for NTC connected to 3.3V, and R_FIXED to GND, with reading in the middle
# (This is the arrangement I assumed for ntc1 in diagram.json, so ADC value decreases when temperature rises)
if voltage_at_adc >= 3.3: # Avoid division by zero or negative
resistance_ntc = 0.1 # Very small value for very high temperature
else:
resistance_ntc = R_FIXED * (voltage_at_adc / (3.3 - voltage_at_adc))
# Steinhart-Hart Equation (simplified form for thermistor)
# 1/T = 1/T0 + (1/Beta) * ln(R/R0)
# T = 1 / (1/T0 + (1/Beta) * ln(R/R0))
try:
ln_ratio = math.log(resistance_ntc / R_NTC_AT_REF)
temp_kelvin = 1.0 / ((1.0 / TEMP_REF_K) + (1.0 / BETA) * ln_ratio)
temp_celsius = temp_kelvin - 273.15
return temp_celsius
except ValueError: # If log(negative) or other error
return -999.0 # Return error value
import math # Import math module for logarithm
def update_rgb_indicator(temperature):
"""
Updates RGB LED color based on simulated temperature.
Common Anode: LOW = ON, HIGH = OFF
LED will remain lit with the appropriate color as long as the temperature is within its range.
"""
global last_rgb_color, relay # Need global access for relay
# Temperature ranges
LOW_TEMP_THRESHOLD = 20
HIGH_TEMP_THRESHOLD = 30
new_color = None
if temperature < LOW_TEMP_THRESHOLD:
new_color = "BLUE" # Low Temperature
elif temperature >= LOW_TEMP_THRESHOLD and temperature <= HIGH_TEMP_THRESHOLD:
new_color = "GREEN" # Moderate Temperature
else: # temperature > HIGH_TEMP_THRESHOLD
new_color = "RED" # High Temperature
# Only change color if it's different from the previous color
if new_color != last_rgb_color:
red_led.on(); green_led.on(); blue_led.on() # Turn all off first
if new_color == "BLUE":
blue_led.off()
elif new_color == "GREEN":
green_led.off()
elif new_color == "RED":
red_led.off()
# When temperature is high (RED), activate relay if not in manual mode
if not is_manual_mode: # Ensure auto watering only in auto mode
lcd.clear()
lcd.putstr("High Temp!")
sleep(1)
lcd.clear()
lcd.putstr("Auto Watering...")
relay.on() # Turn on water pump/valve
log_data_event("Auto Watering Started (High Temp:{:.1f}C)".format(temperature))
sleep(5) # Water for 5 seconds
relay.off() # Turn off pump/valve
log_data_event("Auto Watering Done")
lcd.clear()
lcd.putstr("Watering Done!")
sleep(2) # Display "Watering Done!" message for 2 seconds
last_rgb_color = new_color # Update last color
def display_current_status(temperature, light_reading, soil_reading_raw):
"""
Displays current sensor readings on the LCD.
Includes operation mode status.
"""
lcd.clear()
mode_str = "Manual" if is_manual_mode else "Auto"
lcd.putstr("Mode: {}\n".format(mode_str))
lcd.putstr("T:{:.1f}C L:{}".format(temperature, light_reading))
def log_data_event(message):
"""
Logs events with a timestamp to the SD card.
Manages potential SD card write errors.
"""
if not sd_card_ok:
return # Do not attempt to log if SD card is not OK
try:
now = rtc.datetime()
# RTC datetime format: (year, month, day, weekday, hour, minute, second, subsecond)
# Format timestamp as DD-MM-YYYY HH:MM:SS
timestamp = "{:02d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format(
now[2], now[1], now[0], now[4], now[5], now[6])
with open("/sd/log.txt", "a") as f:
f.write("[{}] {}\n".format(timestamp, message))
except Exception as e:
print(f"Log write error: {e}") # Print error to console if logging fails
pass # Fail silently in production, or implement more robust error handling
def control_grow_light(state_on):
"""
Controls the RGB LED acting as a 'grow light'.
Turns on all colors (LOW) for illumination.
"""
if state_on:
red_led.off(); green_led.off(); blue_led.off() # All ON
else:
red_led.on(); green_led.on(); blue_led.on() # All OFF
# perform_auto_watering_soil function is now largely integrated into update_rgb_indicator
# for simplicity based on the new temperature-driven relay logic.
# If you still need soil-based auto-watering separately, this function can be re-added
# and called conditionally based on is_manual_mode.
def button_interrupt_handler(pin):
"""
Interrupt Service Routine (ISR) for the manual watering/mode toggle button.
Performs debouncing and switches operation mode.
"""
global is_manual_mode, manual_watering_active, last_button_press_time
current_time_ms = ticks_ms()
# Check if enough time (300ms) has passed since the last button press
if ticks_diff(current_time_ms, last_button_press_time) > 300:
last_button_press_time = current_time_ms
# Toggle mode
is_manual_mode = not is_manual_mode
if is_manual_mode:
print("Switched to Manual Mode. Starting Manual Watering.")
log_data_event("Mode Switched to Manual. Manual watering initiated.")
manual_watering_active = True # Set flag for manual watering
# Ensure relay is OFF if it was on from cancelled auto-watering
relay.off()
else:
print("Switched to Auto Mode.")
log_data_event("Mode Switched to Auto.")
manual_watering_active = False # Ensure manual watering flag is off
relay.off() # Ensure relay is OFF when returning to auto mode
def handle_manual_watering_loop():
"""
Manages manual watering triggered by the button.
It will only water once when the button first switches to manual mode.
"""
global manual_watering_active
if manual_watering_active:
lcd.clear()
lcd.putstr("Manual Watering")
relay.on() # Turn on water pump/valve
control_grow_light(True) # Turn on grow light
log_data_event("Manual Watering Started by User")
sleep(5) # Water for 5 seconds (adjust duration)
relay.off() # Turn off pump/valve
control_grow_light(False) # Turn off grow light
log_data_event("Manual Watering Done")
lcd.clear()
lcd.putstr("Watering Done!")
sleep(2) # Display "Watering Done!" message for 2 seconds
manual_watering_active = False # Reset flag so it doesn't water repeatedly in manual mode
# until the button is pressed again.
# Attach button interrupt for falling edge (when button is pressed)
button.irq(trigger=Pin.IRQ_FALLING, handler=button_interrupt_handler)
# === MAIN LOOP ===
while True:
# Read sensor values (raw ADC readings, typically 0-4095 for ESP32)
soil_raw_val = ntc_adc.read()
light_raw_val = ldr_adc.read()
# --- Temperature Calculation ---
simulated_temp_celsius = calculate_temperature(soil_raw_val)
# Update RGB LED indicator based on simulated temperature
# This will also activate the relay if temperature is high and in Auto mode.
update_rgb_indicator(simulated_temp_celsius)
# Display current status on LCD
# Display calculated temperature, not raw NTC value
display_current_status(simulated_temp_celsius, light_raw_val, soil_raw_val)
# Check and perform manual watering if triggered
# This will execute manual watering only once when entering manual mode
handle_manual_watering_loop()
# Control white LED based on LDR (darkness detection)
# Only log when light condition changes to avoid flooding the log file.
# Adjust threshold 500 based on your LDR readings in different light conditions.
new_light_condition = "Night" if light_raw_val < 500 else "Day"
if new_light_condition == "Night":
white_led_pin.on() # Turn on white LED
else:
white_led_pin.off() # Turn off white LED
if new_light_condition != current_light_condition:
if new_light_condition == "Night":
log_data_event("Light: Night Detected - White LED ON")
else:
log_data_event("Light: Daylight Detected - White LED OFF")
current_light_condition = new_light_condition # Update stored light condition
# Log current sensor data periodically
log_data_event("Data: T:{:.1f}, L:{}, S_Raw:{}".format(
simulated_temp_celsius, light_raw_val, soil_raw_val))
sleep(10) # Main loop delay (data update interval)