Monitoreo Inteligente de Biorreactor con ESP32
Sistema IoT para la recolección y almacenamiento de datos de pH, temperatura y volumen de gas.
Fecha: 19 de septiembre de 2025
Dashboard en Vivo
Cargando datos iniciales...
Predicción del Modelo AM2
Simulación a 14 días desde el estado actual del reactor con pH tomado del sensor.
pH Predicho
Biogás Acumulado
Concentración de Sustratos
Evolución de Biomasas
Señal de Control
Diagnóstico basado en la predicción del modelo AM2 para los próximos 14 días
pH mínimo predicho
—
en día —
Biogás total predicho (14 días)
—
mL acumulados en el reactor
1. Resumen del Proyecto
La gestión de residuos orgánicos en entornos urbanos representa uno de los desafíos ambientales más críticos de la actualidad. La digestión anaerobia (DA) se presenta como una solución tecnológica robusta al permitir la valorización de estos residuos mediante la producción de biogás. La estabilidad del proceso es extremadamente sensible a las variaciones en la carga orgánica y el uso de sustratos con alta densidad energética suele desencadenar la acumulación excesiva de Ácidos Grasos Volátiles (AGV). Este fenómeno provoca una caída drástica del pH, inhibiendo la actividad de las arqueas metanogénicas y comprometiendo la viabilidad del reactor.
El objetivo central de este proyecto consistió en el desarrollo e implementación de un gemelo digital diseñado específicamente para un reactor anaerobio de flujo intermitente (SBR de 1 L). El aporte principal radica en la integración del modelo biológico dinámico AM2, que simplifica la complejidad del ADM1 a seis ecuaciones diferenciales, dentro de una arquitectura de Internet de las Cosas (IoT) basada en el microcontrolador ESP32. El sistema incorpora sensores de pH (PH-4502C), de temperatura (DS18B20) y de medición de biogás, lo que permite detectar de forma temprana estados de inhibición antes de que ocurra un colapso irreversible del sistema biológico.
Los resultados experimentales demostraron que el gemelo digital es capaz de reproducir con precisión la ventana operativa estable (pH 6.7–7.3) y predecir el punto de inflexión hacia la inhibición ante sobrecargas orgánicas. Se identificó un modo de fallo crítico en el que la cinética de crecimiento acidogénico supera a la metanogénica, lo que resulta en el agotamiento de la alcalinidad. Aunque se detectaron limitaciones en la calibración inicial de los parámetros y una sobreestimación en la fase inicial del perfil de biogás, el modelo mostró una concordancia cualitativa sólida con los datos experimentales.
En conclusión, la implementación del gemelo digital basado en el modelo AM2 valida una estructura capaz de anticipar el comportamiento del biorreactor en condiciones críticas. Este trabajo establece las bases para el desarrollo de sistemas de control predictivo más sofisticados, demostrando que la integración de modelado matemático y herramientas IoT accesibles es una vía viable para optimizar la estabilidad y eficiencia en la producción de bioenergía.
2. Componentes del Sistema
El sistema experimental es un Biorreactor de Lote Secuencial (SBR) con un volumen total de 1 L (0.6 L de inóculo de lodo anaerobio proveniente de una planta de tratamiento de aguas residuales). El mezclado se realiza mediante una turbina de aspas inclinadas (PBT) basada en los principios de McCabe et al. (Operaciones Unitarias).
La instrumentación y control utiliza los siguientes componentes:
- Microcontrolador: ESP32-WROOM-32D — controlador principal que lee sensores, envía telemetría a Google Sheets y consulta el comando de control cada 15 minutos.
- Sensor de pH: módulo PH-4502C con sonda BNC; señal analógica procesada por un ADC de precisión ADS1115 (16 bits) para minimizar ruido.
- Sensor de Temperatura: DS18B20 sumergible (protocolo One-Wire).
- Actuadores: tira calefactora de 24 V para control térmico, motorreductor DC con controlador L298N para agitación, y tres bombas peristálticas (alimentación, purga y dosificación alcalina).
- Pantalla local: OLED SSD1306 (128x64, I2C) para visualización de estado.
- Medición de biogás: sensor de nivel por conductividad eléctrica, con cables conductores sumergidos en solución salina para detectar el desplazamiento de volumen.
- Control de actuadores: módulo de relé de 2 canales.
- Fuentes de Alimentación: 24 V 5 A (potencia) y 5 V 5 A (lógica), en topología aislada para minimizar ruido eléctrico.
Régimen de alimentación: 12 pulsos por día de 10 mL cada uno (cada 2 horas) con una solución de azúcar al 100 g/L (DQO del influente ≈ 100 g/L), manteniendo estable el volumen de operación mediante activación simultánea de las bombas de alimentación y purga.
3. Diagrama de Conexiones del Circuito
La correcta conexión de los componentes es crucial para el funcionamiento del sistema. Se utilizó una topología de alimentación aislada para separar los componentes de potencia (motor, calentador) de los componentes de lógica/sensores, minimizando el ruido eléctrico.
| Desde | Pin/Terminal | Hacia | Pin/Terminal | Notas y Propósito |
|---|---|---|---|---|
| Alimentación de Potencia (24V) | ||||
| Fuente 24V | + (Positivo) | Relé (Calentador) | COM1 | Entrada de 24V para el calentador. |
| Fuente 24V | + (Positivo) | Relé (Bomba Gas) | COM2 | Entrada de 24V para la bomba de gas. |
| Fuente 24V | + (Positivo) | L298N | Terminal 12V | Alimentación de POTENCIA para el motor. |
| Fuente 24V | - (Negativo) | ESP32 | GND | Punto de Tierra Común Central. |
| Alimentación de Lógica (5V) | ||||
| Fuente 5V | + (Positivo) | ESP32 | VIN | Alimentación principal para ESP32 y sensores. |
| Fuente 5V | + (Positivo) | L298N | Terminal 5V | Alimentación de LÓGICA para el L298N. |
| Fuente 5V | - (Negativo) | ESP32 | GND | Conecta la tierra de 5V al punto común. |
| Actuadores y Señales de Control | ||||
| Relé (Calentador) | NO1 | Calentador (+) | Positivo | Salida de 24V controlada al calentador. |
| Relé (Bomba Gas) | NO2 | Bomba Gas (+) | Positivo | Salida de 24V controlada a la bomba. |
| L298N | OUT1 / OUT2 | Motor DC | Terminales | Salidas de potencia para el motor. |
| ESP32 | GPIO 25 | Relé (Calentador) | IN1 | Señal de control para el calentador. |
| ESP32 | GPIO 13 | Relé (Bomba Gas) | IN2 | Señal de control para la bomba de gas. |
| ESP32 | GPIO 26 | L298N | IN1 | Control de dirección del motor. |
| ESP32 | GPIO 27 | L298N | IN2 | Control de dirección del motor. |
| ESP32 | GPIO 14 | L298N | ENA | Control de velocidad del motor (PWM). |
| Sensores y Periféricos | ||||
| ESP32 | GPIO 32 | Sensor Nivel Alto | Pin de Señal | Detecta cuando el gas ha llenado el volumen. |
| ESP32 | GPIO 33 | Sensor Nivel Bajo | Pin de Señal | Detecta cuando la bomba ha vaciado el gas. |
| ESP32 | 3V3 | Sensores Nivel | Pin Común | Alimentación para cerrar el circuito de los sensores de nivel. |
| ESP32 | GPIO 23 | Sensor Temp | DATA | Línea de datos One-Wire. |
| Sensor pH (Módulo) | Po | ADS1115 | A0 | Señal de voltaje del pH al ADC. |
| ESP32 | GPIO 22 | ADS1115 / OLED | SCL | Línea de reloj del bus I2C (compartida). |
| ESP32 | GPIO 21 | ADS1115 / OLED | SDA | Línea de datos del bus I2C (compartida). |
4. El modelo AM2
El modelo se basa en el trabajo de Bernard et al. (2001) y describe la interacción entre las poblaciones bacterianas acidogénicas y metanogénicas mediante un sistema de 6 ecuaciones diferenciales ordinarias:
- X1 — Biomasa acidogénica (g/L)
- X2 — Biomasa metanogénica (g/L)
- S1 — Sustrato orgánico / DQO (g/L)
- S2 — Ácidos grasos volátiles (mmol/L)
- Z — Alcalinidad total (mmol/L)
- C — Carbono inorgánico disuelto (mmol/L)
La cinética de las bacterias acidogénicas sigue la ecuación de Monod, mientras que la de las metanogénicas se rige por la ecuación de Haldane, que representa la inhibición por sustrato a concentraciones elevadas de AGV. Las condiciones iniciales se establecieron con base en la caracterización del lodo inoculado: S₁ = 0.5 g/L, S₂ = 6.4 mmol/L, Z = 26.4 mmol/L y C = 23.1 mmol/L.
El gemelo digital actualiza automáticamente el pH inicial usando el último valor medido por el sensor y recalcula el carbono inorgánico disuelto (C) mediante el equilibrio carbónico: C = B · (1 + 10−pH / Kb), donde B = Z − S₂.
El reactor opera en modo SBR discreto: entre pulsos solo ocurren las reacciones biológicas; en cada pulso se aplica una mezcla instantánea con factor de dilución f = V_pulso / V_total. Se incluye inhibición por pH sobre las metanogénicas (activa bajo pH 7, nula bajo pH 5.5). Integración numérica RK4 con paso h = 0.001 días, horizonte de 14 días.
5. Análisis de laboratorio
La validación de los estados estimados por el gemelo digital se realizó mediante métodos químicos convencionales:
- Alcalinidad Total (Z): titulación potenciométrica con ácido clorhídrico (HCl) 0.1 M hasta pH final 4.3.
- Ácidos Grasos Volátiles (S₂): titulación con hidróxido de sodio (NaOH) 0.1 M para cuantificar ácidos orgánicos acumulados.
- Sólidos: pruebas de Sólidos Volátiles (SV) y Sólidos Suspendidos Volátiles (SSV) para estimar la biomasa activa inicial.
6. Integración software
La arquitectura conecta el ESP32 con este dashboard mediante Google Sheets. El ESP32 envía telemetría (pH, temperatura, volumen de gas) por HTTP POST; un Apps Script la registra y guarda el comando de control en una columna adicional. Este dashboard, escrito en JavaScript puro, ejecuta la simulación AM2 en tiempo real con cuatro gráficas (pH, biogás, sustratos, biomasas) y recalibra desde la hoja "Condiciones" cada vez que se carga.
Existe además una versión en Python del mismo modelo (AM2py.py) con SciPy solve_ivp y método LSODA, útil para validación numérica y ajuste de parámetros. El estado "CRÍTICO" generado por el modelo se envía de vuelta a Google Sheets; el ESP32 lo lee cada 15 minutos y, si está activo, dispara la bomba alcalina por 10 segundos.
7. Código del ESP32
El siguiente código MicroPython controla el sistema completo: ciclo SBR, lectura de sensores (pH y temperatura), control de actuadores (motor, calentador, bomba de gas) y envío de datos a Google Sheets.
import machine
import time
import onewire, ds18x20
from ads1115 import ADS1115
from ssd1306 import SSD1306_I2C
import network
import urequests
import ujson
import gc
# --- 1. CONFIGURACIÓN DE PINES Y COMPONENTES ---
gc.enable()
i2c = machine.I2C(0, scl=machine.Pin(22), sda=machine.Pin(21))
ads = ADS1115(i2c, address=0x48, gain=1)
try:
oled = SSD1306_I2C(128, 64, i2c, addr=0x3C)
pantalla_ok = True
except Exception as e:
print("Error OLED:", e)
pantalla_ok = False
ow_bus = onewire.OneWire(machine.Pin(23))
ds_sensor = ds18x20.DS18X20(ow_bus)
sensor_temp_roms = ds_sensor.scan()
# Actuadores existentes
motor_in1 = machine.Pin(26, machine.Pin.OUT)
motor_in2 = machine.Pin(27, machine.Pin.OUT)
motor_ena = machine.PWM(machine.Pin(14), freq=1000, duty=0)
pin_calentador = machine.Pin(25, machine.Pin.OUT)
pin_bomba_gas = machine.Pin(13, machine.Pin.OUT)
pin_nivel_alto = machine.Pin(32, machine.Pin.IN, machine.Pin.PULL_DOWN)
pin_nivel_bajo = machine.Pin(33, machine.Pin.IN, machine.Pin.PULL_DOWN)
# Bombas Peristálticas (Lógica Inversa: 1 = APAGADO, 0 = ENCENDIDO)
B_ON = 0
B_OFF = 1
bomba_alimentacion = machine.Pin(16, machine.Pin.OUT, value=B_OFF)
bomba_purga = machine.Pin(17, machine.Pin.OUT, value=B_OFF)
bomba_alcalino = machine.Pin(18, machine.Pin.OUT, value=B_OFF) # Relé 3: bomba NaOH/HCO3
# --- 2. PARÁMETROS Y VARIABLES ---
WIFI_SSID = "nombreSSID"
WIFI_PASS = "contraseñaSSID"
URL_SERVIDOR = "url del macro en Apps Script"
VELOCIDAD_MOTOR_SEGURO = 650
MOTOR_TIEMPO_ENCENDIDO = 300000
MOTOR_TIEMPO_APAGADO = 600000
SETPOINT_TEMP = 35
HISTERESIS = 0.5
VOLUMEN_POR_CICLO_ML = 50.0
INTERVALO_SBR_MS = 2 * 3600 * 1000 # 2 horas (12 pulsos/día)
DURACION_BOMBAS_SBR_MS = 15000 # Calibración manual: 15 s entregan 10 mL de pulso
ultimo_ciclo_sbr = time.ticks_ms()
INTERVALO_PH_MS = 90000
INTERVALO_DATOS_MS = 900000
INTERVALO_COMANDO_MS = 900000 # Leer comando del gemelo digital cada 15 min
DURACION_ALCALINO_MS = 10000 # Pulso de bomba NaOH: 10 s (calibrar según flujo)
ultimo_cambio_motor = 0
motor_esta_encendido_por_horario = False
calentador_esta_encendido = False
volumen_total_ml = 0.0
estado_gas = "LLENANDO"
ultimo_tiempo_ph = -INTERVALO_PH_MS
ultimo_tiempo_datos = 0
ultimo_tiempo_comando = 0
# --- 3. FUNCIONES AUXILIARES ---
def detener_bombas_sbr():
bomba_alimentacion.value(B_OFF)
bomba_purga.value(B_OFF)
def ejecutar_ciclo_sbr():
print("Iniciando Ciclo SBR: Alimentación y Purga simultánea.")
bomba_alimentacion.value(B_ON)
bomba_purga.value(B_ON)
time.sleep_ms(DURACION_BOMBAS_SBR_MS)
detener_bombas_sbr()
print("Ciclo SBR finalizado.")
def leer_ph_promedio(num_muestras=10):
v_lects = []
try:
for _ in range(num_muestras):
v_lects.append(ads.read_voltage(0))
time.sleep_ms(10)
if not v_lects: return None, None
v_prom = sum(v_lects) / len(v_lects)
ph_calc = (-0.0866 * (v_prom**2)) - (5.2239 * v_prom) + 25.29
return ph_calc, v_prom
except Exception as e:
print("Error pH:", e)
return None, None
def conectar_wifi():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
wlan.connect(WIFI_SSID, WIFI_PASS)
timeout = 15
while not wlan.isconnected() and timeout > 0:
time.sleep(1)
timeout -= 1
def enviar_datos_google(temperatura, ph_val, vol_gas):
gc.collect()
if temperatura is None or ph_val is None: return
datos = {"temperatura": float(temperatura), "ph": float(ph_val), "volumen_gas": float(vol_gas)}
try:
json_payload = ujson.dumps(datos)
headers = {'Content-Type': 'application/json'}
res = urequests.post(URL_SERVIDOR, data=json_payload, headers=headers)
res.close()
except: pass
def detener_motor(): motor_ena.duty(0); motor_in1.value(0); motor_in2.value(0)
def encender_motor(): motor_in1.value(1); motor_in2.value(0); motor_ena.duty(VELOCIDAD_MOTOR_SEGURO)
def detener_calentador(): pin_calentador.value(1)
def encender_calentador(): pin_calentador.value(0)
def detener_bomba_gas(): pin_bomba_gas.value(1)
def encender_bomba_gas(): pin_bomba_gas.value(0)
def dosificar_alcalino():
"""Activa la bomba de NaOH/HCO3 por DURACION_ALCALINO_MS milisegundos."""
print("Dosificando solución alcalina...")
bomba_alcalino.value(B_ON)
time.sleep_ms(DURACION_ALCALINO_MS)
bomba_alcalino.value(B_OFF)
print("Dosificación completada.")
def leer_comando_google():
"""Consulta el Apps Script para obtener la señal del gemelo digital."""
gc.collect()
try:
res = urequests.get(URL_SERVIDOR + "?action=getComando")
data = ujson.loads(res.text)
res.close()
return data.get("comando_alcalino", "ESTABLE")
except:
return "ESTABLE"
# --- 4. PROGRAMA PRINCIPAL ---
print("Iniciando sistema...")
conectar_wifi()
detener_motor(); detener_calentador(); detener_bomba_gas(); detener_bombas_sbr()
ph_valor, voltaje_actual, temp_valor = None, None, None
try:
while True:
tiempo_actual = time.ticks_ms()
# Ciclo SBR (Bombas) — cada 2 horas
if time.ticks_diff(tiempo_actual, ultimo_ciclo_sbr) > INTERVALO_SBR_MS:
ejecutar_ciclo_sbr()
ultimo_ciclo_sbr = tiempo_actual
# Temperatura
if sensor_temp_roms:
try:
ds_sensor.convert_temp()
time.sleep_ms(750)
temp_valor = ds_sensor.read_temp(sensor_temp_roms[0])
except: temp_valor = None
# Control actuadores (temperatura y agitación)
if temp_valor is not None:
if temp_valor < (SETPOINT_TEMP - HISTERESIS):
encender_calentador(); calentador_esta_encendido = True
elif temp_valor > (SETPOINT_TEMP + HISTERESIS):
detener_calentador(); calentador_esta_encendido = False
else:
detener_calentador(); calentador_esta_encendido = False
if motor_esta_encendido_por_horario:
if time.ticks_diff(tiempo_actual, ultimo_cambio_motor) > MOTOR_TIEMPO_ENCENDIDO:
motor_esta_encendido_por_horario = False
ultimo_cambio_motor = tiempo_actual
else:
if time.ticks_diff(tiempo_actual, ultimo_cambio_motor) > MOTOR_TIEMPO_APAGADO:
motor_esta_encendido_por_horario = True
ultimo_cambio_motor = tiempo_actual
motor_realmente_encendido = motor_esta_encendido_por_horario or calentador_esta_encendido
if motor_realmente_encendido: encender_motor()
else: detener_motor()
# Gestión de gas
bomba_gas_encendida = (estado_gas == "VACIANDO")
if estado_gas == "LLENANDO":
if pin_nivel_alto.value() == 1:
volumen_total_ml += VOLUMEN_POR_CICLO_ML
encender_bomba_gas(); estado_gas = "VACIANDO"; time.sleep_ms(50)
elif estado_gas == "VACIANDO":
if pin_nivel_bajo.value() == 1:
detener_bomba_gas(); estado_gas = "LLENANDO"; time.sleep_ms(50)
# Muestreo pH (solo en condiciones seguras)
condiciones_seguras_ph = not motor_realmente_encendido and not calentador_esta_encendido and not bomba_gas_encendida
if time.ticks_diff(tiempo_actual, ultimo_tiempo_ph) > INTERVALO_PH_MS:
if condiciones_seguras_ph:
ph_valor, voltaje_actual = leer_ph_promedio()
ultimo_tiempo_ph = tiempo_actual
# Pantalla OLED
if pantalla_ok:
try:
oled.fill(0)
v_txt = "{:.3f}V".format(voltaje_actual) if voltaje_actual is not None else "V:---"
ph_txt = "pH: {:.2f}".format(ph_valor) if ph_valor is not None else "pH:---"
oled.text(ph_txt, 0, 0); oled.text(v_txt, 75, 0)
oled.text("Temp: {:.1f}C".format(temp_valor) if temp_valor is not None else "Temp: ERR", 0, 15)
oled.text("Gas: {:.1f}mL".format(volumen_total_ml), 0, 30)
m_stat = "ON" if motor_realmente_encendido else "OFF"
h_stat = "ON" if calentador_esta_encendido else "OFF"
b_stat = "ON" if bomba_gas_encendida else "OFF"
oled.text("M:{} H:{} B:{}".format(m_stat, h_stat, b_stat), 0, 45)
wifi_stat = "WIFI: OK" if network.WLAN(network.STA_IF).isconnected() else "WIFI: DISCON"
oled.text(wifi_stat, 0, 56)
oled.show()
except: pass
# Envío a Google Sheets
if time.ticks_diff(tiempo_actual, ultimo_tiempo_datos) > INTERVALO_DATOS_MS:
if ph_valor is not None and temp_valor is not None:
enviar_datos_google(temp_valor, ph_valor, volumen_total_ml)
ultimo_tiempo_datos = tiempo_actual
# Lectura del comando del gemelo digital (cada 15 min)
if time.ticks_diff(tiempo_actual, ultimo_tiempo_comando) > INTERVALO_COMANDO_MS:
comando = leer_comando_google()
if comando == "CRÍTICO":
dosificar_alcalino()
ultimo_tiempo_comando = tiempo_actual
time.sleep_ms(500)
finally:
detener_calentador(); detener_motor(); detener_bomba_gas()
detener_bombas_sbr(); bomba_alcalino.value(B_OFF)