import machine
import utime
import ustruct
import math
from ssd1306 import SSD1306_I2C
# ════════════════════════════════════════════════════════════
# PIN & HARDWARE DEFINITIONS
# ════════════════════════════════════════════════════════════
I2C_SDA_PIN = 0
I2C_SCL_PIN = 1
DS18B20_PIN = 4
DHT22_PIN = 15
SOIL_ADC_PIN = 26 # ADC0 — GP26
PH_ADC_PIN = 27 # ADC1 — GP27
OLED_WIDTH = 128
OLED_HEIGHT = 64
OLED_ADDR = 0x3C
BMP180_ADDR = 0x77
READ_INTERVAL_SEC = 3
# ════════════════════════════════════════════════════════════
# BMP180 DRIVER
# ════════════════════════════════════════════════════════════
BMP180_REG_CTRL = 0xF4
BMP180_REG_DATA = 0xF6
BMP180_REG_CALIB = 0xAA
BMP180_CMD_TEMP = 0x2E
BMP180_CMD_PRESSURE = 0x34
BMP180_OSS = 0
class BMP180:
def __init__(self, i2c):
self.i2c = i2c
self.addr = BMP180_ADDR
self._load_calibration()
def _read_reg(self, reg, n):
return self.i2c.readfrom_mem(self.addr, reg, n)
def _write_reg(self, reg, val):
self.i2c.writeto_mem(self.addr, reg, bytes([val]))
def _load_calibration(self):
raw = self._read_reg(BMP180_REG_CALIB, 22)
(self.AC1, self.AC2, self.AC3,
self.AC4, self.AC5, self.AC6,
self.B1, self.B2,
self.MB, self.MC, self.MD) = ustruct.unpack('>hhhHHHhhhhh', raw)
def _raw_temp(self):
self._write_reg(BMP180_REG_CTRL, BMP180_CMD_TEMP)
utime.sleep_ms(5)
return ustruct.unpack('>H', self._read_reg(BMP180_REG_DATA, 2))[0]
def _raw_pressure(self):
self._write_reg(BMP180_REG_CTRL, BMP180_CMD_PRESSURE + (BMP180_OSS << 6))
utime.sleep_ms(8)
d = self._read_reg(BMP180_REG_DATA, 3)
return ((d[0] << 16) | (d[1] << 8) | d[2]) >> (8 - BMP180_OSS)
def read(self):
UT = self._raw_temp()
UP = self._raw_pressure()
X1 = ((UT - self.AC6) * self.AC5) >> 15
X2 = (self.MC << 11) // (X1 + self.MD)
B5 = X1 + X2
temp = ((B5 + 8) >> 4) / 10.0
B6 = B5 - 4000
X1 = (self.B2 * ((B6 * B6) >> 12)) >> 11
X2 = (self.AC2 * B6) >> 11
B3 = (((self.AC1 * 4 + X1 + X2) << BMP180_OSS) + 2) // 4
X1 = (self.AC3 * B6) >> 13
X2 = (self.B1 * ((B6 * B6) >> 12)) >> 16
X3 = ((X1 + X2) + 2) >> 2
B4 = (self.AC4 * (X3 + 32768)) >> 15
B7 = (UP - B3) * (50000 >> BMP180_OSS)
p = (B7 * 2) // B4 if B7 < 0x80000000 else (B7 // B4) * 2
X1 = (p >> 8) ** 2
X1 = (X1 * 3038) >> 16
X2 = (-7357 * p) >> 16
pressure = (p + ((X1 + X2 + 3791) >> 4)) / 100.0
altitude = 44330.0 * (1.0 - math.pow(pressure / 1013.25, 0.1903))
return temp, pressure, altitude
# ════════════════════════════════════════════════════════════
# DS18B20 DRIVER
# ════════════════════════════════════════════════════════════
_CRC_TABLE = [
0,94,188,226,97,63,221,131,194,156,126,32,163,253,31,65,
157,195,33,127,252,162,64,30,95,1,227,189,62,96,130,220,
35,125,159,193,66,28,254,160,225,191,93,3,128,222,60,98,
190,224,2,92,223,129,99,61,124,34,192,158,29,67,161,255,
70,24,250,164,39,121,155,197,132,218,56,102,229,187,89,7,
219,133,103,57,186,228,6,88,25,71,165,251,120,38,196,154,
101,59,217,135,4,90,184,230,167,249,27,69,198,152,122,36,
248,166,68,26,153,199,37,123,58,100,134,216,91,5,231,185,
140,210,48,110,237,179,81,15,78,16,242,172,47,113,147,205,
17,79,173,243,112,46,204,146,211,141,111,49,178,236,14,80,
175,241,19,77,206,144,114,44,109,51,209,143,12,82,176,238,
50,108,142,208,83,13,239,177,240,174,76,18,145,207,45,115,
202,148,118,40,171,245,23,73,8,86,180,234,105,55,213,139,
87,9,235,181,54,104,138,212,149,203,41,119,244,170,72,22,
233,183,85,11,136,214,52,106,43,117,151,201,74,20,246,168,
116,42,200,150,21,75,169,247,182,232,10,84,215,137,107,53
]
def _crc8(data):
crc = 0
for b in data:
crc = _CRC_TABLE[crc ^ b]
return crc
class DS18B20:
def __init__(self, pin_num):
self.pin = machine.Pin(pin_num, machine.Pin.OPEN_DRAIN,
pull=machine.Pin.PULL_UP)
def _reset(self):
p = self.pin
p.value(0); utime.sleep_us(480)
p.value(1); utime.sleep_us(70)
present = (p.value() == 0)
utime.sleep_us(410)
return present
def _write_bit(self, bit):
self.pin.value(0)
utime.sleep_us(2 if bit else 60)
self.pin.value(1)
utime.sleep_us(60 if bit else 2)
def _read_bit(self):
self.pin.value(0); utime.sleep_us(2)
self.pin.value(1); utime.sleep_us(10)
b = self.pin.value()
utime.sleep_us(50)
return b
def _write_byte(self, byte):
for _ in range(8):
self._write_bit(byte & 1)
byte >>= 1
def _read_byte(self):
b = 0
for i in range(8):
b |= self._read_bit() << i
return b
def read_temp(self):
if not self._reset():
return None
self._write_byte(0xCC)
self._write_byte(0x44)
utime.sleep_ms(800)
if not self._reset():
return None
self._write_byte(0xCC)
self._write_byte(0xBE)
data = bytearray(9)
for i in range(9):
data[i] = self._read_byte()
if _crc8(data[:8]) != data[8]:
return None
return ustruct.unpack('<h', data[:2])[0] / 16.0
# ════════════════════════════════════════════════════════════
# DHT22 DRIVER
# ════════════════════════════════════════════════════════════
class DHT22:
def __init__(self, pin_num):
self.pin = machine.Pin(pin_num, machine.Pin.OPEN_DRAIN,
pull=machine.Pin.PULL_UP)
def read(self):
data = bytearray(5)
pin = self.pin
pin.init(machine.Pin.OUT)
pin.value(0); utime.sleep_ms(2)
pin.value(1); utime.sleep_us(30)
pin.init(machine.Pin.IN)
t = 0
while pin.value() == 1:
t += 1
if t > 200: return None, None
utime.sleep_us(1)
t = 0
while pin.value() == 0:
t += 1
if t > 200: return None, None
utime.sleep_us(1)
t = 0
while pin.value() == 1:
t += 1
if t > 200: return None, None
utime.sleep_us(1)
for i in range(40):
t = 0
while pin.value() == 0:
t += 1
if t > 100: return None, None
utime.sleep_us(1)
utime.sleep_us(35)
data[i // 8] <<= 1
if pin.value() == 1:
data[i // 8] |= 1
t = 0
while pin.value() == 1:
t += 1
if t > 100: return None, None
utime.sleep_us(1)
if ((data[0] + data[1] + data[2] + data[3]) & 0xFF) != data[4]:
return None, None
humidity = ((data[0] << 8) | data[1]) / 10.0
raw_t = ((data[2] & 0x7F) << 8) | data[3]
temp = raw_t / 10.0 * (-1 if data[2] & 0x80 else 1)
return temp, humidity
# ════════════════════════════════════════════════════════════
# OLED DISPLAY HELPERS
# ════════════════════════════════════════════════════════════
# Pages cycle on the OLED every DISPLAY_INTERVAL loops
DISPLAY_INTERVAL = 1 # show each page for 1 read cycle
def oled_page_1(oled, soil_pct, soil_label, ph, ph_label):
"""Page 1 — Soil & pH"""
oled.fill(0)
oled.text("=== SOIL & PH ===", 0, 0)
oled.text(f"Soil:{soil_pct:>3}% {soil_label}", 0, 16)
oled.text(f"pH :{ph:>5.2f} {ph_label}", 0, 28)
oled.text("----------------", 0, 38)
oled.text("Page 1/3", 40, 54)
oled.show()
def oled_page_2(oled, soil_temp, bmp_temp, bmp_pres, bmp_alt):
"""Page 2 — Temperature & Pressure"""
oled.fill(0)
oled.text("== TEMP & PRES ==", 0, 0)
oled.text(f"Soil:{soil_temp:>5.1f}C", 0, 14)
oled.text(f"Air :{bmp_temp:>5.1f}C", 0, 26)
oled.text(f"Pres:{bmp_pres:>6.1f}hP", 0, 38)
oled.text(f"Alt :{bmp_alt:>5.1f}m", 0, 50)
oled.show()
def oled_page_3(oled, dht_temp, dht_hum, hi):
"""Page 3 — DHT22 Air"""
oled.fill(0)
oled.text("=== AIR (DHT) ===", 0, 0)
oled.text(f"Temp:{dht_temp:>5.1f} C", 0, 16)
oled.text(f"Hum :{dht_hum:>5.1f} %", 0, 28)
oled.text(f"HIdx:{hi:>5.1f} C", 0, 40)
oled.text("----------------", 0, 50)
oled.show()
def oled_error(oled, msg):
oled.fill(0)
oled.text("!!! ERROR !!!", 16, 10)
oled.text(msg[:16], 0, 30)
oled.show()
def oled_startup(oled):
oled.fill(0)
oled.text("Smart Agri Pico", 4, 8)
oled.text("----------------", 0, 20)
oled.text("DS18B20 OK", 8, 30)
oled.text("BMP180 OK", 8, 40)
oled.text("DHT22 OK", 8, 50)
oled.show()
utime.sleep(2)
# ════════════════════════════════════════════════════════════
# SOIL & pH HELPERS
# ════════════════════════════════════════════════════════════
def soil_label(pct):
if pct >= 70: return "WET "
if pct >= 40: return "GOOD"
if pct >= 20: return "LOW "
return "DRY "
def ph_label(ph):
if ph < 4.0: return "ACID"
if ph < 6.5: return "ACDL"
if ph <= 7.5: return "NEUT"
if ph <= 9.0: return "ALKL"
return "ALKH"
def heat_index(temp, hum):
return (-8.78469
+ 1.61139411 * temp
+ 2.33854884 * hum
- 0.14611605 * temp * hum
- 0.01230809 * temp * temp
- 0.01642482 * hum * hum
+ 0.00221173 * temp * temp * hum
+ 0.00072546 * temp * hum * hum
- 0.00000358 * temp * temp * hum * hum)
# ════════════════════════════════════════════════════════════
# MAIN
# ════════════════════════════════════════════════════════════
def main():
# ── I2C bus (shared: SSD1306 + BMP180) ───────────────
i2c = machine.I2C(0,
sda=machine.Pin(I2C_SDA_PIN),
scl=machine.Pin(I2C_SCL_PIN),
freq=400_000)
# ── Scan I2C ──────────────────────────────────────────
found = i2c.scan()
print(f"I2C devices found: {[hex(d) for d in found]}")
# ── OLED ──────────────────────────────────────────────
oled = SSD1306_I2C(OLED_WIDTH, OLED_HEIGHT, i2c, addr=OLED_ADDR)
# ── BMP180 ────────────────────────────────────────────
bmp = BMP180(i2c)
# ── DS18B20 ───────────────────────────────────────────
ds = DS18B20(DS18B20_PIN)
# ── DHT22 ─────────────────────────────────────────────
dht = DHT22(DHT22_PIN)
# ── ADC — Soil moisture (GP26) ────────────────────────
adc0 = machine.ADC(machine.Pin(SOIL_ADC_PIN))
# ── ADC — pH sensor (GP27) ────────────────────────────
adc1 = machine.ADC(machine.Pin(PH_ADC_PIN))
# ── Startup screen ────────────────────────────────────
oled_startup(oled)
print("=" * 44)
print(" Smart Agriculture Monitor — Pico")
print("=" * 44)
page = 0 # cycles 0 → 1 → 2 → 0 …
loop = 0
# ── Cached values (used if a sensor temporarily fails)
soil_temp_c = 0.0
bmp_temp_c = 0.0
bmp_pres_hpa = 0.0
bmp_alt_m = 0.0
dht_temp_c = 0.0
dht_hum_pct = 0.0
while True:
loop += 1
print(f"\n── Read #{loop} ──────────────────────────────")
# ── 1. Soil Moisture (ADC0 / GP26) ───────────────
soil_val = adc0.read_u16() # 0 – 65535
# High voltage = dry, Low voltage = wet
soil_pct = int((1.0 - soil_val / 65535) * 100)
soil_pct = max(0, min(100, soil_pct))
s_label = soil_label(soil_pct)
print(f"[SOIL] Raw={soil_val:>5} Moisture={soil_pct:>3}% {s_label}")
# ── 2. pH Sensor (ADC1 / GP27) ───────────────────
ph_val = adc1.read_u16() # 0 – 65535
ph_level = (ph_val / 65535) * 14 # map to 0–14
p_label = ph_label(ph_level)
print(f"[pH] Raw={ph_val:>5} pH={ph_level:>5.2f} {p_label}")
# ── 3. DS18B20 ────────────────────────────────────
t = ds.read_temp()
if t is not None:
soil_temp_c = t
print(f"[DS18B20] Soil Temp = {soil_temp_c:.2f} °C")
else:
print("[DS18B20] Read failed — using last value")
# ── 4. BMP180 ─────────────────────────────────────
try:
bmp_temp_c, bmp_pres_hpa, bmp_alt_m = bmp.read()
print(f"[BMP180] Air Temp = {bmp_temp_c:.2f} °C")
print(f"[BMP180] Pressure = {bmp_pres_hpa:.2f} hPa")
print(f"[BMP180] Altitude = {bmp_alt_m:.1f} m")
except Exception as e:
print(f"[BMP180] Read failed: {e}")
# ── 5. DHT22 ──────────────────────────────────────
dt, dh = dht.read()
if dt is not None:
dht_temp_c = dt
dht_hum_pct = dh
hi = heat_index(dht_temp_c, dht_hum_pct)
print(f"[DHT22] Air Temp = {dht_temp_c:.2f} °C")
print(f"[DHT22] Humidity = {dht_hum_pct:.1f} %")
print(f"[DHT22] Heat Index = {hi:.2f} °C")
else:
hi = 0.0
print("[DHT22] Read failed — using last value")
# ── Irrigation decision ───────────────────────────
print()
if soil_pct < 30:
print(">> ALERT: Soil too DRY — Irrigation recommended!")
elif soil_pct > 80:
print(">> INFO: Soil WET — No irrigation needed.")
else:
print(">> OK: Soil moisture normal.")
if ph_level < 5.5:
print(">> ALERT: Soil too ACIDIC — Consider liming.")
elif ph_level > 7.5:
print(">> ALERT: Soil too ALKALINE — Consider acidifying.")
else:
print(">> OK: pH level normal for most crops.")
# ── OLED — Cycle through 3 pages ─────────────────
if page == 0:
oled_page_1(oled, soil_pct, s_label, ph_level, p_label)
elif page == 1:
oled_page_2(oled, soil_temp_c, bmp_temp_c, bmp_pres_hpa, bmp_alt_m)
else:
oled_page_3(oled, dht_temp_c, dht_hum_pct, hi)
page = (page + 1) % 3 # advance page each cycle
utime.sleep(READ_INTERVAL_SEC)
main()