# Pico + ILI9341 soil monitor for Wokwi (MicroPython)
# GP26 = potentiometer SIG
# GP15 = soil-dry LED
# GP14 = passive buzzer
# GP16 = night lamp
# GP21 = LDR digital output
# GP22 = PIR motion sensor
# TFT: SCK=GP18, MOSI=GP19, CS=GP17, D/C=GP20
from machine import Pin, ADC, PWM, SPI
import framebuf
import time
import math
import gc
LIGHT_HOLD_MS = 10000
DRY_LIMIT = 40
WET_LIMIT = 80
DRY_RAW = 0
WET_RAW = 65535
def moisture_percent(raw):
return max(
0,
min(
100,
round((raw - DRY_RAW) * 100 / (WET_RAW - DRY_RAW))
)
)
def soil_state(moisture):
if moisture < DRY_LIMIT:
return "dry"
if moisture >= WET_LIMIT:
return "wet"
return "moist"
def night_light_step(dark, motion, now, last_motion):
if not dark:
return False, None
if motion:
last_motion = now
on = (
last_motion is not None
and time.ticks_diff(now, last_motion) < LIGHT_HOLD_MS
)
return on, last_motion if on else None
def color(rgb):
r, g, b = rgb
value = ((r & 248) << 8) | ((g & 252) << 3) | (b >> 3)
return ((value & 255) << 8) | (value >> 8)
BLACK = color((0, 0, 0))
WHITE = color((255, 255, 255))
RED = color((255, 65, 65))
GREEN = color((50, 230, 120))
GRAY = color((40, 50, 60))
YELLOW = color((255, 210, 40))
class Display:
def __init__(self):
self.cs = Pin(17, Pin.OUT, value=1)
self.dc = Pin(20, Pin.OUT, value=0)
self.spi = SPI(
0,
baudrate=20000000,
polarity=0,
phase=0,
sck=Pin(18),
mosi=Pin(19)
)
self.command(0x01)
time.sleep_ms(150)
for cmd, data in (
(0xC0, b'\x23'),
(0xC1, b'\x10'),
(0xC5, b'\x3e\x28'),
(0xC7, b'\x86'),
(0x36, b'\x48'),
(0x3A, b'\x55'),
(0xB1, b'\x00\x18'),
(0xB6, b'\x08\x82\x27'),
):
self.command(cmd, data)
self.command(0x11)
time.sleep_ms(120)
self.command(0x29)
gc.collect()
self.buffer = bytearray(240 * 320 * 2)
self.fb = framebuf.FrameBuffer(
self.buffer,
240,
320,
framebuf.RGB565
)
self.glyph = framebuf.FrameBuffer(
bytearray(8),
8,
8,
framebuf.MONO_HLSB
)
def command(self, cmd, data=None):
self.cs(0)
self.dc(0)
self.spi.write(bytes([cmd]))
if data is not None:
self.dc(1)
self.spi.write(data)
self.cs(1)
def show(self):
self.command(0x2A, b'\x00\x00\x00\xef')
self.command(0x2B, b'\x00\x00\x01\x3f')
self.command(0x2C)
self.cs(0)
self.dc(1)
self.spi.write(self.buffer)
self.cs(1)
def text(self, text, y, ink=WHITE, scale=2):
x = (240 - len(text) * 8 * scale) // 2
for char in text:
self.glyph.fill(0)
self.glyph.text(char, 0, 0, 1)
for row in range(8):
for col in range(8):
if self.glyph.pixel(col, row):
self.fb.fill_rect(
x + col * scale,
y + row * scale,
scale,
scale,
ink
)
x += 8 * scale
def disk(self, x, y, radius, ink):
for dy in range(-radius, radius + 1):
dx = int(math.sqrt(radius * radius - dy * dy))
self.fb.hline(
x - dx,
y + dy,
2 * dx + 1,
ink
)
def draw(self, moisture, state, dark, lamp_on):
dry = state == 'dry'
wet = state == 'wet'
ink = RED if dry else (YELLOW if wet else GREEN)
label = (
'SOIL DRY'
if dry
else ('TOO WET' if wet else 'SOIL MOIST')
)
message = (
'WATER ME!'
if dry
else ('NO MORE WATER!' if wet else 'I AM HAPPY!')
)
self.fb.fill(BLACK)
self.text('money plant', 12)
self.text('soil monitar', 38, WHITE, 1)
self.disk(120, 122, 58, ink)
self.disk(101, 108, 6, BLACK)
self.disk(139, 108, 6, BLACK)
if wet:
for offset in range(3):
self.fb.line(
91,
95 + offset,
108,
87 + offset,
BLACK
)
self.fb.line(
132,
87 + offset,
149,
95 + offset,
BLACK
)
for dx in range(-27, 28):
y = 143 + int(
4 * math.sin(dx * math.pi / 13)
)
self.fb.fill_rect(
120 + dx,
y,
2,
4,
BLACK
)
else:
for dx in range(-27, 28):
curve = int(
16 * (1 - (dx / 27) ** 2)
)
y = 150 - curve if dry else 133 + curve
self.fb.fill_rect(
120 + dx,
y,
2,
5,
BLACK
)
self.text(label, 196, ink)
self.text(str(moisture) + '%', 224, WHITE, 3)
self.fb.fill_rect(
20,
260,
200,
12,
GRAY
)
self.fb.fill_rect(
20,
260,
2 * moisture,
12,
ink
)
self.text(message, 285, ink)
footer = (
'NIGHT: LIGHT ON'
if lamp_on
else (
'NIGHT: WAITING'
if dark
else 'DAY: LIGHT OFF'
)
)
self.text(footer, 308, WHITE, 1)
self.show()
# --------------------------------
# ROW ROW ROW YOUR BOAT
# --------------------------------
C4 = 262
D4 = 294
E4 = 330
F4 = 349
G4 = 392
ROW_ROW_BOAT = [
(C4, 300),
(C4, 300),
(C4, 300),
(D4, 300),
(E4, 600),
(E4, 300),
(D4, 300),
(E4, 300),
(F4, 300),
(G4, 600),
(C4, 450),
(C4, 450),
(G4, 450),
(G4, 450),
(E4, 300),
(E4, 300),
(C4, 300),
(C4, 300),
(G4, 300),
(F4, 300),
(E4, 300),
(D4, 300),
(C4, 600)
]
def play_row_row_boat():
for frequency, duration in ROW_ROW_BOAT:
buzzer.freq(frequency)
buzzer.duty_u16(16000)
time.sleep_ms(duration)
buzzer.duty_u16(0)
time.sleep_ms(70)
# --------------------------------
# COMPONENTS
# --------------------------------
light_sensor = Pin(21, Pin.IN)
motion_sensor = Pin(
22,
Pin.IN,
Pin.PULL_DOWN
)
night_lamp = Pin(
16,
Pin.OUT,
value=0
)
last_motion = None
last_display = None
soil = ADC(26)
led = Pin(
15,
Pin.OUT,
value=0
)
# Passive buzzer on GP14
buzzer = PWM(Pin(14))
buzzer.duty_u16(0)
tft = Display()
last_moisture = None
# Song timer
last_song = time.ticks_ms() - 5000
print('Soil monitor ready.')
print('Turn the knob to change simulated moisture.')
try:
while True:
now = time.ticks_ms()
# LDR
dark = bool(light_sensor.value())
# PIR + night light
lamp_on, last_motion = night_light_step(
dark,
bool(motion_sensor.value()),
now,
last_motion
)
night_lamp.value(
1 if lamp_on else 0
)
# Soil moisture
moisture = moisture_percent(
soil.read_u16()
)
state = soil_state(moisture)
# Dry soil = red LED
dry = state == "dry"
led.value(
1 if dry else 0
)
# Update TFT
display_values = (
moisture,
dark,
lamp_on
)
if display_values != last_display:
tft.draw(
moisture,
state,
dark,
lamp_on
)
last_display = display_values
print(
'Moisture: {}% | {}'.format(
moisture,
state.upper()
)
)
last_moisture = moisture
# --------------------------------
# PLAY ROW ROW ROW YOUR BOAT
# WHEN SOIL IS DRY
# --------------------------------
if dry:
if time.ticks_diff(
now,
last_song
) >= 5000:
print('Soil is dry - playing Row Row Row Your Boat!')
play_row_row_boat()
last_song = time.ticks_ms()
else:
buzzer.duty_u16(0)
time.sleep_ms(150)
finally:
buzzer.duty_u16(0)
buzzer.deinit()
led.off()
night_lamp.off()