import machine
import time
# ================= MAPEAMENTO DE PINOS (PICO) =================
BUZZER_PIN = 8
RELAY_PIN = 9
RELAY_NC_PIN = 7
LDR_PIN = 26 # ADC0 (Pino Físico 31 no Pico)
LED_PWM_PIN = 11
# Configuração dos Pinos de Saída e Entrada
relay = machine.Pin(RELAY_PIN, machine.Pin.OUT)
relay.value(0)
relay_nc = machine.Pin(RELAY_NC_PIN, machine.Pin.IN, machine.Pin.PULL_UP)
ldr = machine.ADC(machine.Pin(LDR_PIN))
# Configuração de Periféricos com PWM
buzzer = machine.PWM(machine.Pin(BUZZER_PIN))
buzzer.duty_u16(0)
led_pwm = machine.PWM(machine.Pin(LED_PWM_PIN))
led_pwm.freq(1000) # Frequência de 1kHz para o LED
led_pwm.duty_u16(0)
# ================= CONFIGURAÇÃO DO LCD I2C =================
# Nota: Este bloco assume o uso das bibliotecas padrão 'pico_i2c_lcd' e 'lcd_api'.
# Se não estiverem no Pico, o código usa um Fallback para não travar o script.
try:
from pico_i2c_lcd import I2cLcd
i2c = machine.I2C(0, sda=machine.Pin(0), scl=machine.Pin(1), freq=400000)
lcd = I2cLcd(i2c, 0x27, 2, 16)
except ImportError:
class FallbackLCD:
def move_to(self, x, y): pass
def putstr(self, s): print(f"[LCD emulado]: {s.strip()}")
def clear(self): pass
def backlight_on(self): pass
def backlight_off(self): pass
lcd = FallbackLCD()
print("Aviso: Instale 'pico_i2c_lcd.py' no Pico para ativar o display físico.")
# ================= CLASSE PID EM MICROPYTHON =================
class MiniPID:
def __init__(self, kp, ki, kd):
self.kp = kp
self.ki = ki
self.kd = kd
self.output_min = 0
self.output_max = 5000
self.integral = 0
self.last_input = 0
self.last_time = time.ticks_ms()
def set_output_limits(self, min_val, max_val):
self.output_min = min_val
self.output_max = max_val
def compute(self, current_input, setpoint):
now = time.ticks_ms()
dt = time.ticks_diff(now, self.last_time) / 1000.0 # Segundos
if dt <= 0:
dt = 0.5
error = setpoint - current_input
# Termo Integral
self.integral += self.ki * error * dt
self.integral = max(self.output_min, min(self.integral, self.output_max))
# Termo Derivativo (Evita o 'derivative kick' atuando sobre a variação do Input)
derivative = (current_input - self.last_input) / dt
output = self.kp * error + self.integral - self.kd * derivative
output = max(self.output_min, min(output, self.output_max))
self.last_input = current_input
self.last_time = now
return output
# ================= FUNÇÕES AUXILIARES DE MATEMÁTICA =================
def map_val(x, in_min, in_max, out_min, out_max):
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
def constrain(x, out_min, out_max):
return max(out_min, min(x, out_max))
# ================= VARIÁVEIS DE CONTROLE DO SISTEMA =================
Setpoint = 1500.0
Input = 0.0
Output = 0.0
myPID = MiniPID(3.5, 1.5, 0.5)
myPID.set_output_limits(0, 5000)
WindowSize = 5000
windowStartTime = time.ticks_ms()
lastSensorRead = time.ticks_ms()
lastDisplayTime = time.ticks_ms()
DISPLAY_TIMEOUT = 5000
ldrFiltrado = 0.0
ntu = 0.0
Tntu = 0.0
Dntu = 0.0
drenando = False
displayOn = True
inicioDrenoMaximo = 0
# ================= FUNÇÕES DO SISTEMA =================
def readLDR():
soma = 0
for _ in range(20):
# Transforma a leitura de 16-bits do Pico para 10-bits (0-1023)
soma += (ldr.read_u16() >> 6)
time.sleep_ms(2)
return soma / 20.0
def atualizarSensor():
global ldrFiltrado, ntu, Tntu, Dntu
leitura = readLDR()
# Filtro exponencial
ldrFiltrado = (ldrFiltrado * 0.85) + (leitura * 0.15)
# Conversões e limites
ntu = map_val(int(ldrFiltrado), 0, 1023, 3000, 0)
ntu = constrain(ntu, 0, 3000)
Tntu = map_val(int(ntu), 3000, 0, 0, 300)
Dntu = Tntu / 10.0
def beep(freq, tempo):
buzzer.freq(freq)
buzzer.duty_u16(32768) # 50% de ciclo de trabalho
time.sleep_ms(tempo)
buzzer.duty_u16(0)
def controlarLEDProporcional():
brilho_8bit = map_val(int(ntu), 2900, 1500, 10, 255)
brilho_8bit = constrain(brilho_8bit, 0, 255)
# Converte a escala de 8-bits para os 16-bits exigidos pelo PWM do Pico
brilho_16bit = int(brilho_8bit * 257)
led_pwm.duty_u16(brilho_16bit)
def atualizarLCD():
global displayOn, lastDisplayTime
if not displayOn and Output <= 50.00:
return
if not displayOn and Output > 50.00:
lcd.backlight_on()
displayOn = True
# Linha 1 formatada fixa em 16 caracteres para limpar resíduos
linha1 = f"NTU:{Dntu:.1f} Alvo:{int(Setpoint / 100.0)}"
lcd.move_to(0, 0)
lcd.putstr(f"{linha1:<16}")
lcd.move_to(0, 1)
if Output > 50.00:
lcd.putstr(f"Dreno PID:{int(Output)}ms ")
lastDisplayTime = time.ticks_ms()
else:
lcd.putstr("Status: AGUA OK ")
def controlarDisplay():
global displayOn
tempo_inativo = time.ticks_diff(time.ticks_ms(), lastDisplayTime)
if displayOn and (tempo_inativo > DISPLAY_TIMEOUT) and (Output <= 50.00):
displayOn = False
lcd.backlight_off()
def dispararAlarmeErro(linha1, linha2, freq):
relay.value(0) # Trava o dreno por segurança
lcd.clear()
lcd.backlight_on()
lcd.move_to(0, 0)
lcd.putstr(f"{linha1:<16}")
lcd.move_to(0, 1)
lcd.putstr(f"{linha2:<16}")
while True:
beep(freq, 300)
time.sleep_ms(600)
def verificarSegurancaDreno():
global inicioDrenoMaximo
if Output >= WindowSize:
if inicioDrenoMaximo == 0:
inicioDrenoMaximo = time.ticks_ms()
elif time.ticks_diff(time.ticks_ms(), inicioDrenoMaximo) > 120000: # 2 minutos
dispararAlarmeErro("LIMITE EXCEDIDO", " MANUTENCAO ", 1800)
else:
inicioDrenoMaximo = 0
def controlarRelePID():
global windowStartTime, drenando
now = time.ticks_ms()
if time.ticks_diff(now, windowStartTime) > WindowSize:
windowStartTime = time.ticks_add(windowStartTime, WindowSize)
tempo_decorrido = time.ticks_diff(now, windowStartTime)
if Output > tempo_decorrido:
if not drenando:
relay.value(1)
drenando = True
beep(1500, 50)
print("-> VALVULA ABERTA VIA PID")
else:
if drenando:
relay.value(0)
drenando = False
beep(1000, 50)
print("-> VALVULA FECHADA VIA PID")
time.sleep_ms(15) # Pausa mecânica de desmagnetização
if relay_nc.value() == 1:
dispararAlarmeErro(" ERRO: RELE NO ", " TRAVADO/ALERTA ", 2000)
def enviarDadosSerial():
print(f"Setpoint:{ntu},NTU_Filtrado:{ntu},Janela_Dreno_ms:{Output}")
# ================= BOOT / SETUP =================
lcd.clear()
lcd.backlight_on()
lcd.move_to(0, 0)
lcd.putstr("ElectronicDreno ")
lcd.move_to(0, 1)
lcd.putstr(" v4.0 - PID Mode")
time.sleep(3)
lcd.clear()
# ================= LOOP PRINCIPAL =================
while True:
# 1. ATUALIZAÇÃO SENSORIAL (A cada 500ms)
if time.ticks_diff(time.ticks_ms(), lastSensorRead) >= 500:
lastSensorRead = time.ticks_ms()
atualizarSensor()
Input = ntu
Output = myPID.compute(Input, Setpoint)
atualizarLCD()
controlarLEDProporcional()
enviarDadosSerial()
# 2. CONTROLE DO ATUADOR EM TEMPO REAL
controlarRelePID()
# 3. SUPERVISÃO E SEGURANÇA CONTÍNUA
controlarDisplay()
verificarSegurancaDreno()