"""
Estacao Meteorologica Virtual - ESP32
Modulos: Sensores (DHT22, LDR, MQ2), Display OLED, MQTT, HTTP (Sheets) e SMTP.
Projeto IoT - 7 Semestre Engenharia de Software
"""
from machine import Pin, ADC, SoftI2C
from time import sleep, time
import dht
import network
import ujson
import gc # Garbage Collector
try:
import urequests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
try:
from umqtt.simple import MQTTClient
HAS_MQTT = True
except ImportError:
HAS_MQTT = False
# Tenta importar a biblioteca umail
try:
import umail
HAS_UMAIL = True
except ImportError:
HAS_UMAIL = False
print("AVISO: O arquivo umail.py nao foi encontrado no projeto!")
# Configuracoes de Rede
WIFI_SSID = "Wokwi-GUEST"
WIFI_PASSWORD = ""
# Configuracoes MQTT
MQTT_BROKER = "broker.hivemq.com"
MQTT_PORT = 1883
MQTT_CLIENT_ID = "esp32_weather_joao"
TOPIC_BASE = "joao/weather"
TOPIC_TEMP = TOPIC_BASE + "/temperature"
TOPIC_HUMIDITY = TOPIC_BASE + "/humidity"
TOPIC_LIGHT = TOPIC_BASE + "/light"
TOPIC_GAS = TOPIC_BASE + "/gas"
TOPIC_COMFORT = TOPIC_BASE + "/comfort"
TOPIC_FORECAST = TOPIC_BASE + "/forecast"
TOPIC_ALL = TOPIC_BASE + "/all"
# Integracoes
SHEETS_URL = "https://script.google.com/macros/s/AKfycbzTXyBz_O3nat3-nkEClt98iZjQf7s_NPb83nboDb5tDVCIw9IZB7sThkkEi2czRyqk/exec"
THINGSPEAK_KEY = "GU9SRXOADTRIB1GN"
THINGSPEAK_URL = "http://api.thingspeak.com/update"
USE_THINGSPEAK = False
# Credenciais SMTP (Gmail)
EMAIL_FROM = "[email protected]"
EMAIL_PASS = "vpcf vjil zjsk frpd"
EMAIL_TO = "[email protected]"
EMAIL_HOST = "smtp.gmail.com"
EMAIL_PORT = 465
# Configuracoes OWM
OWM_KEY = ""
OWM_CITY = "Sao Paulo"
USE_OWM = False
# Temporizadores (segundos)
MQTT_INTERVAL = 5
SHEETS_INTERVAL = 30
EMAIL_INTERVAL = 120
# Pinos e Hardware
DHT_PIN = 15
LDR_PIN = 34
MQ2_PIN = 35
BTN_PIN = 4
I2C_SDA = 21
I2C_SCL = 22
sensor_dht = dht.DHT22(Pin(DHT_PIN))
ldr = ADC(Pin(LDR_PIN))
ldr.atten(ADC.ATTN_11DB)
mq2 = ADC(Pin(MQ2_PIN))
mq2.atten(ADC.ATTN_11DB)
btn = Pin(BTN_PIN, Pin.IN, Pin.PULL_UP)
i2c = SoftI2C(scl=Pin(I2C_SCL), sda=Pin(I2C_SDA), freq=100000)
FONT8 = {
' ':[0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], '!':[0x18,0x3C,0x3C,0x18,0x18,0x00,0x18,0x00],
'.':[0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00], ':':[0x00,0x18,0x18,0x00,0x18,0x18,0x00,0x00],
'/':[0x06,0x0C,0x18,0x30,0x60,0x00,0x00,0x00], '-':[0x00,0x00,0x3C,0x00,0x00,0x00,0x00,0x00],
'%':[0x62,0x64,0x08,0x10,0x26,0x46,0x00,0x00], '0':[0x3C,0x66,0x6E,0x76,0x66,0x66,0x3C,0x00],
'1':[0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00], '2':[0x3C,0x66,0x06,0x0C,0x18,0x30,0x7E,0x00],
'3':[0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00], '4':[0x0C,0x1C,0x3C,0x6C,0x7E,0x0C,0x0C,0x00],
'5':[0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00], '6':[0x3C,0x60,0x7C,0x66,0x66,0x66,0x3C,0x00],
'7':[0x7E,0x06,0x0C,0x18,0x30,0x30,0x30,0x00], '8':[0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00],
'9':[0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00], 'A':[0x18,0x3C,0x66,0x7E,0x66,0x66,0x66,0x00],
'B':[0x7C,0x66,0x66,0x7C,0x66,0x66,0x7C,0x00], 'C':[0x3C,0x66,0x60,0x60,0x60,0x66,0x3C,0x00],
'D':[0x78,0x6C,0x66,0x66,0x66,0x6C,0x78,0x00], 'E':[0x7E,0x60,0x60,0x78,0x60,0x60,0x7E,0x00],
'F':[0x7E,0x60,0x60,0x78,0x60,0x60,0x60,0x00], 'G':[0x3C,0x66,0x60,0x6E,0x66,0x66,0x3C,0x00],
'H':[0x66,0x66,0x66,0x7E,0x66,0x66,0x66,0x00], 'I':[0x3C,0x18,0x18,0x18,0x18,0x18,0x3C,0x00],
'J':[0x06,0x06,0x06,0x06,0x06,0x66,0x3C,0x00], 'K':[0x66,0x6C,0x78,0x70,0x78,0x6C,0x66,0x00],
'L':[0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x00], 'M':[0x63,0x77,0x7F,0x6B,0x63,0x63,0x63,0x00],
'N':[0x66,0x76,0x7E,0x7E,0x6E,0x66,0x66,0x00], 'O':[0x3C,0x66,0x66,0x66,0x66,0x66,0x3C,0x00],
'P':[0x7C,0x66,0x66,0x7C,0x60,0x60,0x60,0x00], 'Q':[0x3C,0x66,0x66,0x66,0x6E,0x3C,0x06,0x00],
'R':[0x7C,0x66,0x66,0x7C,0x6C,0x66,0x66,0x00], 'S':[0x3C,0x66,0x60,0x3C,0x06,0x66,0x3C,0x00],
'T':[0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00], 'U':[0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x00],
'V':[0x66,0x66,0x66,0x66,0x66,0x3C,0x18,0x00], 'W':[0x63,0x63,0x63,0x6B,0x7F,0x77,0x63,0x00],
'X':[0x66,0x66,0x3C,0x18,0x3C,0x66,0x66,0x00], 'Y':[0x66,0x66,0x3C,0x18,0x18,0x18,0x18,0x00],
'Z':[0x7E,0x06,0x0C,0x18,0x30,0x60,0x7E,0x00], 'a':[0x00,0x00,0x3C,0x06,0x3E,0x66,0x3E,0x00],
'b':[0x60,0x60,0x7C,0x66,0x66,0x66,0x7C,0x00], 'c':[0x00,0x00,0x3C,0x60,0x60,0x60,0x3C,0x00],
'd':[0x06,0x06,0x3E,0x66,0x66,0x66,0x3E,0x00], 'e':[0x00,0x00,0x3C,0x66,0x7E,0x60,0x3C,0x00],
'f':[0x1C,0x30,0x30,0x7C,0x30,0x30,0x30,0x00], 'g':[0x00,0x00,0x3E,0x66,0x66,0x3E,0x06,0x3C],
'h':[0x60,0x60,0x7C,0x66,0x66,0x66,0x66,0x00], 'i':[0x18,0x00,0x38,0x18,0x18,0x18,0x3C,0x00],
'j':[0x06,0x00,0x06,0x06,0x06,0x66,0x3C,0x00], 'k':[0x60,0x60,0x66,0x6C,0x78,0x6C,0x66,0x00],
'l':[0x38,0x18,0x18,0x18,0x18,0x18,0x3C,0x00], 'm':[0x00,0x00,0x66,0x7F,0x7F,0x6B,0x63,0x00],
'n':[0x00,0x00,0x7C,0x66,0x66,0x66,0x66,0x00], 'o':[0x00,0x00,0x3C,0x66,0x66,0x66,0x3C,0x00],
'p':[0x00,0x00,0x7C,0x66,0x66,0x7C,0x60,0x60], 'q':[0x00,0x00,0x3E,0x66,0x66,0x3E,0x06,0x06],
'r':[0x00,0x00,0x6C,0x76,0x60,0x60,0x60,0x00], 's':[0x00,0x00,0x3C,0x60,0x3C,0x06,0x3C,0x00],
't':[0x30,0x30,0x7C,0x30,0x30,0x30,0x1C,0x00], 'u':[0x00,0x00,0x66,0x66,0x66,0x66,0x3E,0x00],
'v':[0x00,0x00,0x66,0x66,0x66,0x3C,0x18,0x00], 'w':[0x00,0x00,0x63,0x6B,0x7F,0x77,0x63,0x00],
'x':[0x00,0x00,0x66,0x3C,0x18,0x3C,0x66,0x00], 'y':[0x00,0x00,0x66,0x66,0x3E,0x06,0x3C,0x00],
'z':[0x00,0x00,0x7E,0x0C,0x18,0x30,0x60,0x7E,0x00],
}
class SSD1306:
"""Driver minimalista para display OLED SSD1306."""
def __init__(self, i2c, addr=0x3C, w=128, h=64):
self.i2c = i2c
self.addr = addr
self.w = w
self.h = h
self.buf = bytearray(w * h // 8)
self._init()
def _cmd(self, c):
self.i2c.writeto(self.addr, bytes([0x00, c]))
def _init(self):
cmds = [0xAE,0xD5,0x80,0xA8,0x3F,0xD3,0x00,0x40,
0x8D,0x14,0x20,0x00,0xA0,0xC8,0xDA,0x12,
0x81,0xCF,0xD9,0xF1,0xDB,0x40,0xA4,0xA6,0xAF]
for c in cmds:
self._cmd(c)
def clear(self):
for i in range(len(self.buf)):
self.buf[i] = 0
def pixel(self, x, y, c=1):
if 0 <= x < self.w and 0 <= y < self.h:
idx = x + (y >> 3) * self.w
if c:
self.buf[idx] |= 1 << (y & 7)
else:
self.buf[idx] &= ~(1 << (y & 7))
def hline(self, x, y, w):
for i in range(w):
self.pixel(x + i, y)
def text(self, s, x, y):
for i in range(len(s)):
ch = s[i]
glyph = FONT8.get(ch, FONT8[' '])
for row in range(8):
for col in range(8):
if glyph[row] & (1 << col):
self.pixel(x + i * 8 + col, y + row)
def show(self):
for p in range(8):
self._cmd(0xB0 + p)
self._cmd(0x00)
self._cmd(0x10)
start = p * self.w
self.i2c.writeto(
self.addr,
bytes([0x40]) + self.buf[start:start + self.w]
)
# Variaveis Globais de Estado
display = None
wifi_ok = False
mqtt_client = None
screen = 0
last_btn = 1
ts_mqtt = 0
ts_sheets = 0
ts_email = 0
boot_time = 0
temperature = 0.0
humidity = 0.0
light_level = 0
gas_level = 0
comfort = ""
forecast = ""
daily_t = []
daily_h = []
daily_l = []
daily_g = []
report_sent = False
def fmt_time():
elapsed = int(time() - boot_time)
s = elapsed % 60
elapsed //= 60
m = elapsed % 60
elapsed //= 60
h = elapsed % 24
return "%02d:%02d:%02d" % (h, m, s)
def fmt_datetime():
return "2026-05-01 " + fmt_time()
def connect_wifi():
global wifi_ok
wlan = network.WLAN(network.STA_IF)
wlan.active(False)
sleep(1)
wlan.active(True)
sleep(1)
for tentativa in range(3):
if wlan.isconnected():
break
print("[WiFi] Tentativa " + str(tentativa + 1) + "/3...")
wlan.connect(WIFI_SSID, WIFI_PASSWORD)
for _ in range(20):
if wlan.isconnected():
break
sleep(1)
print(".", end="")
print("")
if wlan.isconnected():
break
wlan.active(False)
sleep(2)
wlan.active(True)
sleep(1)
wifi_ok = wlan.isconnected()
if wifi_ok:
print("[WiFi] OK - IP: " + wlan.ifconfig()[0])
else:
print("[WiFi] FALHOU - continuando sem rede")
return wifi_ok
def connect_mqtt():
global mqtt_client
if not HAS_MQTT:
return False
try:
mqtt_client = MQTTClient(MQTT_CLIENT_ID, MQTT_BROKER, port=MQTT_PORT)
mqtt_client.connect()
print("[MQTT] Conectado: " + MQTT_BROKER)
return True
except Exception as e:
print("[MQTT] Erro na conexao: " + str(e))
return False
def publish_mqtt():
global ts_mqtt, mqtt_client
if not wifi_ok: return False
if mqtt_client is None:
if not connect_mqtt(): return False
try:
mqtt_client.publish(TOPIC_TEMP, str(temperature))
mqtt_client.publish(TOPIC_HUMIDITY, str(humidity))
mqtt_client.publish(TOPIC_LIGHT, str(light_level))
mqtt_client.publish(TOPIC_GAS, str(gas_level))
mqtt_client.publish(TOPIC_COMFORT, str(comfort))
mqtt_client.publish(TOPIC_FORECAST, str(forecast))
data = {
"datetime": fmt_datetime(),
"temperature": temperature,
"humidity": humidity,
"light": light_level,
"gas": gas_level,
"comfort": comfort,
"forecast": forecast
}
mqtt_client.publish(TOPIC_ALL, ujson.dumps(data))
print("[MQTT] Update - T=" + str(temperature) + " H=" + str(humidity))
ts_mqtt = time()
return True
except Exception as e:
print("[MQTT] Erro. Desconectando para retry.")
try: mqtt_client.disconnect()
except: pass
mqtt_client = None
return False
def read_sensors():
global temperature, humidity, light_level, gas_level
try:
sensor_dht.measure()
temperature = round(sensor_dht.temperature(), 1)
humidity = round(sensor_dht.humidity(), 1)
except Exception:
pass
light_level = int(ldr.read() / 4095 * 100)
gas_level = int(mq2.read() / 4095 * 100)
def calc_comfort():
global comfort
t, h = temperature, humidity
if t < 18: comfort = "Frio"
elif t < 24 and h <= 80: comfort = "Confortavel"
elif t < 24: comfort = "Umido"
elif t < 28 and h <= 70: comfort = "Agradavel"
elif t < 28: comfort = "Qte/Umido"
elif h <= 60: comfort = "Quente"
else: comfort = "Muito Quente"
def calc_forecast():
global forecast
if humidity > 80 and temperature < 20: forecast = "Chuva"
elif humidity > 70: forecast = "Nublado"
elif temperature > 30 or light_level > 70: forecast = "Ensolarado"
else: forecast = "Parcial Nublado"
def accumulate():
daily_t.append(temperature)
daily_h.append(humidity)
daily_l.append(light_level)
daily_g.append(gas_level)
def get_averages():
n = len(daily_t)
if n == 0: return None
return {
"n": n,
"t": round(sum(daily_t) / n, 1),
"h": round(sum(daily_h) / n, 1),
"l": round(sum(daily_l) / n, 1),
"g": round(sum(daily_g) / n, 1)
}
def save_to_sheets():
global ts_sheets
if not HAS_REQUESTS or not wifi_ok: return False
gc.collect()
payload = {
"datetime": fmt_datetime(),
"temperature": temperature,
"humidity": humidity,
"light": light_level,
"gas": gas_level,
"comfort": comfort,
"forecast": forecast
}
r = None
try:
r = urequests.post(SHEETS_URL, json=payload, timeout=10)
_ = r.text
print("[Sheets] Dados sincronizados.")
except Exception as e:
if "113" not in str(e) and "ECONNABORTED" not in str(e):
print("[Sheets] Erro HTTP: " + str(e))
finally:
if r is not None:
try: r.close()
except: pass
ts_sheets = time()
return True
# ============================================================
# SOLUÇÃO COM A BIBLIOTECA UMAIL (Otimizada para MicroPython)
# ============================================================
def send_email():
global ts_email, report_sent
if not wifi_ok or "seuemail" in EMAIL_FROM: return False
if not HAS_UMAIL:
print("[Email] ERRO: Biblioteca 'umail' nao encontrada. Crie o arquivo umail.py no Wokwi!")
return False
avgs = get_averages()
if not avgs: return False
gc.collect()
print("[Email] Iniciando envio com a biblioteca umail...")
try:
# A biblioteca umail constroi o SMTP usando SSL=True na porta 465 internamente
smtp = umail.SMTP(EMAIL_HOST, EMAIL_PORT, ssl=True)
smtp.login(EMAIL_FROM, EMAIL_PASS.replace(" ", ""))
smtp.to(EMAIL_TO)
# O ".write()" envia em pequenos pedaços, poupando a RAM do simulador
smtp.write(f"From: ESP32 <{EMAIL_FROM}>\n")
smtp.write(f"To: {EMAIL_TO}\n")
smtp.write(f"Subject: Relatorio Meteorologico - {fmt_datetime()[:10]}\n\n")
smtp.write("Relatorio Diario - Estacao Meteorologica\n")
smtp.write("=========================================\n\n")
smtp.write(f"Data: {fmt_datetime()[:10]}\n")
smtp.write(f"Leituras: {avgs['n']}\n\n")
smtp.write("MEDIAS DO DIA:\n")
smtp.write(f" Temperatura: {avgs['t']} C\n")
smtp.write(f" Umidade: {avgs['h']} %\n")
smtp.write(f" Luminosidade: {avgs['l']} %\n")
smtp.write(f" Gas/Ar: {avgs['g']} %\n\n")
smtp.write("ULTIMA LEITURA:\n")
smtp.write(f" Temperatura: {temperature} C\n")
smtp.write(f" Umidade: {humidity} %\n")
smtp.write(f" Conforto: {comfort}\n")
smtp.write(f" Previsao: {forecast}\n\n")
smtp.write("--\nESP32 - Projeto IoT\n")
smtp.send()
smtp.quit()
print("[Email] Relatorio entregue no Gmail COM SUCESSO!")
report_sent = True
return True
except Exception as e:
print("[Email] Falha na comunicacao umail: " + str(e))
return False
finally:
ts_email = time()
# Telas
def _hdr(title):
display.text(title, 0, 0)
display.hline(0, 9, 128)
def screen0():
display.clear()
_hdr("TEMP / UMIDADE")
display.text("T: " + str(temperature) + "C", 0, 14)
display.text("H: " + str(humidity) + "%", 0, 24)
display.text(comfort[:16], 0, 38)
display.text(fmt_time(), 0, 54)
display.show()
def screen1():
display.clear()
_hdr("LUZ / AR")
display.text("Luz: " + str(light_level) + "%", 0, 14)
display.text("Gas: " + str(gas_level) + "%", 0, 24)
if gas_level < 30: ar = "BOM"
elif gas_level < 60: ar = "MODERADO"
else: ar = "RUIM!"
display.text("Ar: " + ar, 0, 38)
display.show()
def screen2():
display.clear()
_hdr("PREVISAO")
display.text(forecast[:16], 0, 14)
display.text("T: " + str(temperature) + "C", 0, 28)
display.text("H: " + str(humidity) + "%", 0, 40)
display.show()
def screen3():
display.clear()
_hdr("MEDIAS DO DIA")
avgs = get_averages()
if avgs:
display.text("T: " + str(avgs['t']) + "C", 0, 14)
display.text("H: " + str(avgs['h']) + "%", 0, 24)
display.text("N: " + str(avgs['n']) + " leit", 0, 34)
else:
display.text("Aguardando...", 0, 24)
if temperature > 35: display.text("! TEMP ALTA", 0, 50)
elif gas_level > 60: display.text("! GAS ALTO", 0, 50)
display.show()
SCREENS = [screen0, screen1, screen2, screen3]
def handle_btn():
global screen, last_btn
state = btn.value()
if state == 0 and last_btn == 1:
screen = (screen + 1) % 4
print("[HMI] Tela " + str(screen))
sleep(0.25)
last_btn = state
def main():
global display, boot_time
boot_time = time()
print("==============================================")
print(" ESTACAO METEOROLOGICA - IoT")
print("==============================================")
display = SSD1306(i2c)
display.clear()
display.text("Conectando...", 0, 24)
display.show()
connect_wifi()
if wifi_ok: connect_mqtt()
cycle = 0
while True:
try:
read_sensors()
calc_comfort()
if cycle % 30 == 0: calc_forecast()
accumulate()
handle_btn()
SCREENS[screen]()
network_busy = False
if time() - ts_sheets >= SHEETS_INTERVAL:
network_busy = True
save_to_sheets()
if not network_busy and not report_sent:
if time() - boot_time >= EMAIL_INTERVAL and time() - ts_email >= EMAIL_INTERVAL:
network_busy = True
send_email()
if not network_busy and time() - ts_mqtt >= MQTT_INTERVAL:
publish_mqtt()
cycle += 1
sleep(1)
except KeyboardInterrupt:
print("Encerrando via teclado...")
if mqtt_client:
try: mqtt_client.disconnect()
except: pass
break
except Exception as e:
print("[Loop Principal] Erro generico: " + str(e))
sleep(2)
main()