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...
1. Resumen del Proyecto
El objetivo de este proyecto fue desarrollar un sistema automatizado capaz de monitorear en tiempo real el pH, la temperatura y el volumen de gas producido en un biorreactor. El sistema integra un motor para mezclar la solución y un calentador para el control de temperatura, mostrando todos los datos relevantes en una pantalla OLED y en un dashboard web. El cerebro del sistema es un microcontrolador ESP32-WROOM-32D programado en MicroPython.
El proyecto se destaca por el uso de un conversor analógico-digital (ADC) externo de alta precisión (ADS1115) para obtener lecturas de pH confiables y un novedoso medidor de gas automatizado que proporciona datos de producción en tiempo real.
2. Componentes Utilizados
- Microcontrolador: ESP32-WROOM-32D
- Sensor de pH: Módulo PH-4502C con sonda BNC
- ADC de Precisión: Módulo ADS1115
- Sensor de Temperatura: Módulo con sensor DS18B20 (One-Wire)
- Controlador de Motor: Módulo L298N
- Motor: Motorreductor DC
- Pantalla: Módulo OLED SSD1306 (128x64, I2C)
- Actuadores: Tira calefactora de 24V, Bomba peristáltica de 24V
- Control de Actuadores: Módulo de Relé de 2 Canales
- Sensores de Nivel de Gas: Cables conductores en agua salina
- Fuentes de Alimentación: 24V 5A (Potencia), 5V 5A (Lógica)
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. Calibración del Sensor de pH (Software)
La calibración se realiza por software para obtener la máxima precisión y flexibilidad, calculando la relación lineal entre el voltaje medido por el ADS1115 y el valor de pH real.
4.1. Procedimiento de Medición (Paso a Paso)
- Ejecutar el script: Cargar y ejecutar
calibracion_ph.pyen el ESP32 para ver las lecturas de voltaje en la consola de Thonny. - Medir Punto 1 (pH 7.0): Sumergir la sonda en el buffer de pH 7.0, esperar a que el voltaje se estabilice y anotar el valor. Ejemplo:
Voltaje_pH7 = 2.5120 V. - Enjuagar: Limpiar la sonda con agua destilada para no contaminar la siguiente muestra.
- Medir Punto 2 (pH 4.0): Sumergir la sonda en el buffer de pH 4.0, esperar a que el voltaje se estabilice y anotar el valor. Ejemplo:
Voltaje_pH4 = 3.2560 V.
4.2. Cálculo de la Fórmula de Calibración (Ejemplo)
Utilizamos la ecuación de la recta: pH = m * Voltaje + b
Cálculo de la Pendiente (m):
m = (pH2 - pH1) / (Voltaje2 - Voltaje1)
m = (4.0 - 7.0) / (3.2560 - 2.5120)
m = -3.0 / 0.744
m = -4.032
Cálculo de la Intersección (b):
b = pH1 - (m * Voltaje1)
b = 7.0 - (-4.032 * 2.5120)
b = 7.0 + 10.128
b = 17.128
Las constantes resultantes (PENDIENTE_PH y INTERSECCION_PH) se introducen en el código main.py para su uso en las mediciones reales.
5. Estructura y Ejecución del Software
5.1. Archivos en el ESP32 (MicroPython)
main.py: Código principal que controla todo el sistema.calibracion_ph.py: Script auxiliar usado solo para realizar la calibración del sensor.- Librerías (archivos .py):
ssd1306.py,onewire.py,ds18x20.py,ads1x15.py. - Librerías (a instalar):
micropython-urequests.
5.2. Archivos en la Computadora (Python)
crear_db.py: Prepara la base de datos (ejecutar una vez).servidor.py: Inicia el servidor que recibe los datos (dejar corriendo).visualizar.py: Analiza los datos guardados y genera una gráfica.sensores.db: Archivo de la base de datos (se crea automáticamente).
5.3. Pasos para la Puesta en Marcha
- Preparar el Hardware: Ensamblar todo el circuito según la tabla de conexiones.
- Preparar el ESP32: Cargar todos los archivos y librerías necesarios en el ESP32 usando Thonny. Realizar la calibración del pH si es necesario.
- Preparar la Base de Datos: En la PC, ejecutar
python crear_db.pyuna vez. - Iniciar el Servidor: En la PC, ejecutar
python servidor.pyy dejar la terminal abierta. Asegurarse de que el firewall permite conexiones en el puerto 5000. - Iniciar el Cliente: Encender o reiniciar el ESP32 con el código
main.py. El dispositivo se conectará al WiFi y comenzará a enviar datos. - Visualizar Resultados: Después de un tiempo, detener el servidor (Ctrl+C) y ejecutar
python visualizar.pypara ver las gráficas del experimento.
6. Códigos Completos del Proyecto
6.1. Código Principal del ESP32 (main.py)
# main.py (Versión Final Integrada)
# --- LIBRERÍAS ---
from machine import Pin, PWM, I2C
import time
import onewire, ds18x20
from ssd1306 import SSD1306_I2C
from ads1x15 import ADS1115
import network
import urequests
import ujson
# --- CONFIGURACIÓN DE PINES Y HARDWARE ---
# Motor
pin_motor_adelante = Pin(19, Pin.OUT)
pin_motor_atras = Pin(18, Pin.OUT)
pin_velocidad_ena = Pin(5)
pwm_motor = PWM(pin_velocidad_ena, freq=1000)
# I2C (Compartido por OLED y ADS1115)
i2c = I2C(1, scl=Pin(22), sda=Pin(21))
# Pantalla OLED
pantalla = SSD1306_I2C(128, 64, i2c)
# Sensor de Temperatura
pin_onewire = Pin(4)
ds_sensor = ds18x20.DS18X20(onewire.OneWire(pin_onewire))
try:
roms = ds_sensor.scan()
sensor_temp_encontrado = True
print('Sensor de temperatura encontrado:', roms)
except:
sensor_temp_encontrado = False
print('Error: No se encontró el sensor de temperatura.')
# Sensor de pH (ADS1115)
try:
ads = ADS1115(i2c)
ads.set_gain(1) # Ganancia para ±4.096V
sensor_ph_encontrado = True
print('Sensor ADS1115 (pH) encontrado.')
except:
sensor_ph_encontrado = False
print('Error: No se encontró el sensor ADS1115.')
# --- CONSTANTES DE CALIBRACIÓN DE PH ---
# !!! IMPORTANTE: REEMPLAZA ESTOS VALORES CON LOS TUYOS !!!
PENDIENTE_PH = -4.032
INTERSECCION_PH = 17.128
# --- CONFIGURACIÓN DE RED Y SERVIDOR ---
WIFI_SSID = "AAAAAA"
WIFI_PASS = "AAAA"
URL_SERVIDOR = "http://192.168.1.118:5000/datos"
# --- PARÁMETROS DE FUNCIONAMIENTO ---
VELOCIDAD_MEZCLA = 1000
DURACION_MEZCLA_SEG = 30
TIEMPO_ESPERA_SEG = 60 # Tiempo entre ciclos de mezcla
# --- FUNCIONES ---
def conectar_wifi():
print("Conectando a la red WiFi...")
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
wlan.connect(WIFI_SSID, WIFI_PASS)
while not wlan.isconnected():
print(".", end="")
time.sleep(1)
print("\n¡Conexión exitosa!")
print("Configuración de red:", wlan.ifconfig())
def leer_ph_calibrado():
if not sensor_ph_encontrado:
return None
try:
raw_value = ads.read(channel=0)
voltaje = raw_value * (4.096 / 32767.0)
valor_ph = (PENDIENTE_PH * voltaje) + INTERSECCION_PH
return valor_ph
except Exception as e:
print(f"Error leyendo el pH: {e}")
return None
def enviar_datos_servidor(temperatura, ph_val):
if not network.WLAN(network.STA_IF).isconnected():
print("WiFi desconectado. No se pueden enviar datos.")
return
datos_a_enviar = {"temperatura": temperatura, "ph": ph_val}
json_data = ujson.dumps(datos_a_enviar)
try:
print(f"Enviando a servidor: {json_data}")
respuesta = urequests.post(URL_SERVIDOR, headers={'Content-Type': 'application/json'}, data=json_data)
print(f"Respuesta: [{respuesta.status_code}] {respuesta.text}")
respuesta.close()
except Exception as e:
print(f"Error al enviar datos: {e}")
def actualizar_pantalla(status, temperatura, ph_val):
pantalla.fill(0)
pantalla.text("Estado:", 0, 0)
pantalla.text(status, 0, 10)
temp_str = f"{temperatura:.1f} C" if temperatura is not None else "Error"
ph_str = f"{ph_val:.2f}" if ph_val is not None else "Error"
pantalla.text(f"Temp:{temp_str}", 0, 30)
pantalla.text(f"pH: {ph_str}", 0, 45)
pantalla.show()
def mezclar(duracion, velocidad, temp, ph):
actualizar_pantalla("Mezclando...", temp, ph)
pin_motor_adelante.value(1)
pin_motor_atras.value(0)
pwm_motor.duty(velocidad)
time.sleep(duracion)
pwm_motor.duty(0)
pin_motor_adelante.value(0)
print("Mezcla terminada.")
# --- PROGRAMA PRINCIPAL ---
conectar_wifi()
actualizar_pantalla("Iniciando...", None, None)
time.sleep(2)
while True:
temp_actual = None
if sensor_temp_encontrado:
try:
ds_sensor.convert_temp()
time.sleep_ms(750)
temp_actual = ds_sensor.read_temp(roms[0])
except:
print("Error al leer la temperatura")
temp_actual = None
ph_actual = leer_ph_calibrado()
actualizar_pantalla("En Espera", temp_actual, ph_actual)
enviar_datos_servidor(temp_actual, ph_actual)
print(f"Estado: En Espera. Temp: {temp_actual:.1f}C. pH: {ph_actual:.2f}. Próxima mezcla en {TIEMPO_ESPERA_SEG}s.")
time.sleep(TIEMPO_ESPERA_SEG)
mezclar(DURACION_MEZCLA_SEG, VELOCIDAD_MEZCLA, temp_actual, ph_actual)
6.2. Script de Calibración (calibracion_ph.py)
# calibracion_ph.py
from machine import Pin, I2C
from ads1x15 import ADS1115
import time
i2c = I2C(1, scl=Pin(22), sda=Pin(21))
try:
ads = ADS1115(i2c)
sensor_ph_encontrado = True
print("Sensor ADS1115 encontrado. Listo para calibrar.")
ads.set_gain(1)
except Exception as e:
sensor_ph_encontrado = False
print(f"Error: No se encontró el sensor ADS1115. Verifica las conexiones. {e}")
if sensor_ph_encontrado:
while True:
try:
raw_value = ads.read(channel=0)
voltaje = raw_value * (4.096 / 32767.0)
print(f"Valor Crudo ADC: {raw_value}, Voltaje Calculado: {voltaje:.4f} V")
time.sleep(2)
except KeyboardInterrupt:
print("Calibración detenida.")
break
except Exception as e:
print(f"Ocurrió un error: {e}")
break
6.3. Scripts de la Computadora
crear_db.py
# crear_db.py
import sqlite3
DB_FILE = "sensores.db"
try:
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS lecturas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
temperatura_c REAL,
ph_valor REAL
);
''')
print(f"Base de datos '{DB_FILE}' y tabla 'lecturas' verificadas/creadas exitosamente.")
except Exception as e:
print(f"Ocurrió un error al crear la base de datos: {e}")
finally:
if 'conn' in locals() and conn:
conn.commit()
conn.close()
servidor.py
# servidor.py
from flask import Flask, request, jsonify
import sqlite3
DB_FILE = "sensores.db"
app = Flask(__name__)
@app.route('/datos', methods=['POST'])
def recibir_datos():
try:
datos = request.get_json()
temp = datos.get('temperatura')
ph = datos.get('ph')
print(f"Dato recibido -> Temp: {temp} °C, pH: {ph}")
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute("INSERT INTO lecturas (temperatura_c, ph_valor) VALUES (?, ?)", (temp, ph))
conn.commit()
conn.close()
return jsonify({"status": "ok"}), 200
except Exception as e:
print(f"Error procesando la petición: {e}")
return jsonify({"status": "error en el servidor"}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
visualizar.py
# visualizar.py
import sqlite3
import pandas as pd
import matplotlib.pyplot as plt
DB_FILE = "sensores.db"
try:
conn = sqlite3.connect(DB_FILE)
query = "SELECT timestamp, temperatura_c, ph_valor FROM lecturas ORDER BY timestamp"
df = pd.read_sql_query(query, conn)
print("Datos cargados exitosamente desde la base de datos.")
print("Últimas 5 lecturas:")
print(df.tail())
except Exception as e:
print(f"Error al leer la base de datos: {e}")
exit()
finally:
if 'conn' in locals() and conn:
conn.close()
if not df.empty:
print("Generando la gráfica...")
plt.style.use('ggplot')
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
fig.suptitle('Monitoreo del Biorreactor', fontsize=16)
ax1.plot(df['timestamp'], df['temperatura_c'], marker='.', linestyle='-', color='r', label='Temperatura')
ax1.set_ylabel('Temperatura (°C)')
ax1.legend()
ax1.grid(True)
ax2.plot(df['timestamp'], df['ph_valor'], marker='.', linestyle='-', color='b', label='pH')
ax2.set_ylabel('Valor de pH')
ax2.set_xlabel('Fecha y Hora')
ax2.legend()
ax2.grid(True)
fig.autofmt_xdate()
plt.tight_layout(rect=[0, 0, 1, 0.96])
plt.show()
else:
print("No hay datos en la base de datos para graficar.")