IoT Smart Irrigation System
IoT Smart Irrigation System is a runnable microcontroller simulation example on SensBench — open the wired circuit, inspect the code, and compile real firmware in your browser, no install needed. Last updated Aug 14, 2026.
FREE SIMULATOR EXAMPLE · ADVANCED
IoT Smart Irrigation System
This example runs a smart irrigation system on the ESP32 — a soil moisture sensor drives a pump through a threshold loop with hysteresis. The sense-decide-actuate pattern behind every automated garden.
| Board | ESP32 DevKit (Xtensa LX6, 240 MHz) |
|---|---|
| Difficulty | advanced |
| Category | communication |
| Libraries | None — just the Arduino core |
| Runs in | Sensbench browser simulator — free, no account |

The exact circuit you get when you open this example — already wired, ready to run.
The sketch
# BlynkLib.py
# Copyright (c) 2015-2019 Volodymyr Shymanskyy. See the file LICENSE for copying permission.
__version__ = "1.0.0"
import struct
import time
import sys
import os
try:
import machine
gettime = lambda: time.ticks_ms()
SOCK_TIMEOUT = 0
except ImportError:
const = lambda x: x
gettime = lambda: int(time.time() * 1000)
SOCK_TIMEOUT = 0.05
def dummy(*args):
pass
MSG_RSP = const(0)
MSG_LOGIN = const(2)
MSG_PING = const(6)
MSG_TWEET = const(12)
MSG_NOTIFY = const(14)
MSG_BRIDGE = const(15)
MSG_HW_SYNC = const(16)
MSG_INTERNAL = const(17)
MSG_PROPERTY = const(19)
MSG_HW = const(20)
MSG_HW_LOGIN = const(29)
MSG_EVENT_LOG = const(64)
MSG_REDIRECT = const(41) # TODO: not implemented
MSG_DBG_PRINT = const(55) # TODO: not implemented
STA_SUCCESS = const(200)
STA_INVALID_TOKEN = const(9)
DISCONNECTED = const(0)
CONNECTING = const(1)
CONNECTED = const(2)
print("""
___ __ __
/ _ )/ /_ _____ / /__
/ _ / / // / _ \\/ '_/
/____/_/\\_, /_//_/_/\\_\\
/___/ for Python v""" + __version__ + " (" + sys.platform + ")\n")
class EventEmitter:
def __init__(self):
self._cbks = {}
def on(self, evt, f=None):
if f:
self._cbks[evt] = f
else:
def D(f):
self._cbks[evt] = f
return f
return D
def emit(self, evt, *a, **kv):
if evt in self._cbks:
self._cbks[evt](*a, **kv)
class BlynkProtocol(EventEmitter):
def __init__(self, auth, tmpl_id=None, fw_ver=None, heartbeat=50, buffin=1024, log=None):
EventEmitter.__init__(self)
self.heartbeat = heartbeat*1000
self.buffin = buffin
self.log = log or dummy
self.auth = auth
self.tmpl_id = tmpl_id
self.fw_ver = fw_ver
self.state = DISCONNECTED
self.connect()
def virtual_write(self, pin, *val):
self._send(MSG_HW, 'vw', pin, *val)
def send_internal(self, pin, *val):
self._send(MSG_INTERNAL, pin, *val)
def set_property(self, pin, prop, *val):
self._send(MSG_PROPERTY, pin, prop, *val)
def sync_virtual(self, *pins):
self._send(MSG_HW_SYNC, 'vr', *pins)
def log_event(self, *val):
self._send(MSG_EVENT_LOG, *val)
def _send(self, cmd, *args, **kwargs):
if 'id' in kwargs:
id = kwargs.get('id')
else:
id = self.msg_id
self.msg_id += 1
if self.msg_id > 0xFFFF:
self.msg_id = 1
if cmd == MSG_RSP:
data = b''
dlen = args[0]
else:
data = ('\0'.join(map(str, args))).encode('utf8')
dlen = len(data)
self.log('<', cmd, id, '|', *args)
msg = struct.pack("!BHH", cmd, id, dlen) + data
self.lastSend = gettime()
self._write(msg)
def connect(self):
if self.state != DISCONNECTED: return
self.msg_id = 1
(self.lastRecv, self.lastSend, self.lastPing) = (gettime(), 0, 0)
self.bin = b""
self.state = CONNECTING
self._send(MSG_HW_LOGIN, self.auth)
def disconnect(self):
if self.state == DISCONNECTED: return
self.bin = b""
self.state = DISCONNECTED
self.emit('disconnected')
def process(self, data=None):
if not (self.state == CONNECTING or self.state == CONNECTED): return
now = gettime()
if now - self.lastRecv > self.heartbeat+(self.heartbeat//2):
return self.disconnect()
if (now - self.lastPing > self.heartbeat//10 and
(now - self.lastSend > self.heartbeat or
now - self.lastRecv > self.heartbeat)):
self._send(MSG_PING)
self.lastPing = now
if data != None and len(data):
self.bin += data
while True:
if len(self.bin) < 5:
break
cmd, i, dlen = struct.unpack("!BHH", self.bin[:5])
if i == 0: return self.disconnect()
self.lastRecv = now
if cmd == MSG_RSP:
self.bin = self.bin[5:]
self.log('>', cmd, i, '|', dlen)
if self.state == CONNECTING and i == 1:
if dlen == STA_SUCCESS:
self.state = CONNECTED
dt = now - self.lastSend
info = ['ver', __version__, 'h-beat', self.heartbeat//1000, 'buff-in', self.buffin, 'dev', sys.platform+'-py']
if self.tmpl_id:
info.extend(['tmpl', self.tmpl_id])
info.extend(['fw-type', self.tmpl_id])
if self.fw_ver:
info.extend(['fw', self.fw_ver])
self._send(MSG_INTERNAL, *info)
try:
self.emit('connected', ping=dt)
except TypeError:
self.emit('connected')
else:
if dlen == STA_INVALID_TOKEN:
self.emit("invalid_auth")
print("Invalid auth token")
return self.disconnect()
else:
if dlen >= self.buffin:
print("Cmd too big: ", dlen)
return self.disconnect()
if len(self.bin) < 5+dlen:
break
data = self.bin[5:5+dlen]
self.bin = self.bin[5+dlen:]
args = list(map(lambda x: x.decode('utf8'), data.split(b'\0')))
self.log('>', cmd, i, '|', ','.join(args))
if cmd == MSG_PING:
self._send(MSG_RSP, STA_SUCCESS, id=i)
elif cmd == MSG_HW or cmd == MSG_BRIDGE:
if args[0] == 'vw':
self.emit("V"+args[1], args[2:])
self.emit("V*", args[1], args[2:])
elif cmd == MSG_INTERNAL:
self.emit("internal:"+args[0], args[1:])
elif cmd == MSG_REDIRECT:
self.emit("redirect", args[0], int(args[1]))
else:
print("Unexpected command: ", cmd)
return self.disconnect()
import socket
class Blynk(BlynkProtocol):
def __init__(self, auth, **kwargs):
self.insecure = kwargs.pop('insecure', False)
self.server = kwargs.pop('server', 'blynk.cloud')
self.port = kwargs.pop('port', 80 if self.insecure else 443)
BlynkProtocol.__init__(self, auth, **kwargs)
self.on('redirect', self.redirect)
def redirect(self, server, port):
self.server = server
self.port = port
self.disconnect()
self.connect()
def connect(self):
print('Connecting to %s:%d...' % (self.server, self.port))
s = socket.socket()
s.connect(socket.getaddrinfo(self.server, self.port)[0][-1])
try:
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
except:
pass
if self.insecure:
self.conn = s
else:
try:
import ussl
ssl_context = ussl
except ImportError:
import ssl
ssl_context = ssl.create_default_context()
self.conn = ssl_context.wrap_socket(s, server_hostname=self.server)
try:
self.conn.settimeout(SOCK_TIMEOUT)
except:
s.settimeout(SOCK_TIMEOUT)
BlynkProtocol.connect(self)
def _write(self, data):
#print('<', data)
self.conn.write(data)
# TODO: handle disconnect
def run(self):
data = b''
try:
data = self.conn.read(self.buffin)
#print('>', data)
except KeyboardInterrupt:
raise
except socket.timeout:
# No data received, call process to send ping messages when needed
pass
except: # TODO: handle disconnect
return
self.process(data)
# main.py
'''Copyright (c) 2026 Kritish Mohapatra'''
import network
import time
from machine import Pin, ADC
import BlynkLib
# 🔑 Blynk
BLYNK_AUTH = "auth"
# 🌐 WiFi
WIFI_SSID = "kritish"
WIFI_PASS = "@pass"
# 📍 Pins
SOIL_PIN = 3 # ADC
RELAY_PIN = 1 # Relay (active LOW)
# 🌱 Soil ADC setup
soil = ADC(Pin(SOIL_PIN))
soil.atten(ADC.ATTN_11DB) # 0–3.3V
soil.width(ADC.WIDTH_12BIT) # 0–4095
# 💧 Relay setup
relay = Pin(RELAY_PIN, Pin.OUT)
relay.on() # Pump OFF initially
# 🌐 WiFi connect (safe)
wifi = network.WLAN(network.STA_IF)
wifi.active(False)
time.sleep(1)
wifi.active(True)
time.sleep(1)
wifi.connect(WIFI_SSID, WIFI_PASS)
print("Connecting WiFi", end="")
t0 = time.time()
while not wifi.isconnected():
print(".", end="")
time.sleep(1)
if time.time() - t0 > 20:
print("\nWiFi failed")
break
if wifi.isconnected():
print("\nWiFi OK:", wifi.ifconfig())
# 📲 Blynk init
blynk = BlynkLib.Blynk(BLYNK_AUTH, insecure=True)
# 🔘 Manual Pump Control (V1)
@blynk.on("V1")
def pump_control(value):
if int(value[0]) == 1:
relay.off() # Pump ON
print("Pump ON")
else:
relay.on() # Pump OFF
print("Pump OFF")
# 📊 Read soil & send to Blynk
def read_soil():
raw = soil.read() # 0–4095
moisture = int((4095 - raw) * 100 / 4095)
moisture = max(0, min(100, moisture))
print("Soil:", moisture, "%")
blynk.virtual_write(5, moisture)
# 🔁 Main loop
last = time.ticks_ms()
while True:
if wifi.isconnected():
blynk.run()
if time.ticks_diff(time.ticks_ms(), last) > 2000:
read_soil()
last = time.ticks_ms()
How it works
The firmware reads the soil moisture sensor on a schedule and compares the value to a threshold. Dry conditions switch the pump on; the loop keeps checking until the soil crosses the wet threshold, then switches off. Hysteresis between the two thresholds prevents rapid pump cycling.
The pump control goes through a driver output — the ESP32’s pin only signals, the driver carries the current. The serial output logs every state change, so the watering schedule is readable from the console.
Before you build it for real
The capacitive soil sensor’s reading depends on soil type and temperature — a threshold calibrated in potting mix is wrong for garden soil, and the sensor’s absolute value drifts with moisture distribution around the probe. The pump needs its own power path: switching a pump through the ESP32’s rail browns out the board at motor startup. And hysteresis is not optional — without the two-threshold band, the pump cycles on and off at the boundary, which destroys both the pump and the reading’s usefulness.
Get the parts
Related examples
FAQ
How does the system decide when to water?
A soil-moisture sensor reports the ground’s wetness, and the firmware compares it against a threshold: dry below it, the pump switches on until the soil reads wet again. The threshold is the one calibration that matters — soil type changes everything.
Why is irrigation a classic IoT project?
Because the decision is simple and the benefit is real: a pump that waters exactly when needed saves water and keeps plants alive unattended. The sensor-pump loop is also the cleanest demonstration of sensing driving actuation.