Monitoreo Inteligente de Biorreactor con ESP32
Sistema IoT para la recolección y almacenamiento de datos de pH y temperatura.
Fecha: 3 de julio de 2024
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 y la temperatura de una solución acuosa. El sistema integra un motor para mezclar la solución a intervalos programados y muestra todos los datos relevantes en una pantalla OLED para una fácil visualización. 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 una calibración personalizada mediante una regresión polinómica para maximizar la exactitud de las mediciones.
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)
- Fuente de Alimentación: Adaptador de 9V 2A con conector Jack DC
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 "GND Común en Estrella", donde el terminal de tierra (GND) del módulo L298N sirve como punto de unión central para todas las tierras del circuito, garantizando una referencia de voltaje estable.
Desde | Pin de Origen | Conectado a... | Pin de Destino | Notas y Propósito |
---|---|---|---|---|
Adaptador 9V | + (Positivo) | L298N | Terminal 12V | Alimentación principal del sistema. |
Adaptador 9V | - (Negativo) | L298N | Terminal GND | Punto de GND Común Central. |
L298N | Terminal 5V | ESP32 | VIN | Alimenta el ESP32 con 5V regulados por el L298N. |
L298N | Terminal GND | ESP32 | GND | Conecta la tierra del ESP32 al GND común. |
L298N | OUT1 / OUT2 | Motor DC | Terminales 1 y 2 | Salidas de potencia para el motor. |
L298N | IN1 | ESP32 | GPIO 26 | Control de dirección del motor. |
L298N | IN2 | ESP32 | GPIO 27 | Control de dirección del motor. |
L298N | ENA | ESP32 | GPIO 14 | Control de velocidad y encendido del motor (PWM). |
Sensor pH | V+ | L298N | Terminal 5V | Alimentación de 5V para el sensor de pH. |
Sensor pH | GND | L298N | Terminal GND | Conexión al GND común. |
Sensor pH | Po | ADS1115 | A0 | Envía la señal de voltaje analógica al ADC. |
ADS1115 | VDD | ESP32 | 3V3 | Alimentación para la lógica del ADC. |
ADS1115 | GND | ESP32 | GND | Conexión al GND común. |
ADS1115 | SCL | ESP32 | GPIO 22 | Línea de reloj del bus I2C. |
ADS1115 | SDA | ESP32 | GPIO 21 | Línea de datos del bus I2C. |
OLED SSD1306 | VCC | ESP32 | 3V3 | Alimentación para la pantalla. |
OLED SSD1306 | GND | ESP32 | GND | Conexión al GND común. |
OLED SSD1306 | SCL | ESP32 | GPIO 22 | Comparte el bus I2C con el ADS1115. |
OLED SSD1306 | SDA | ESP32 | GPIO 21 | Comparte el bus I2C con el ADS1115. |
DS18B20 Module | VCC | ESP32 | 3V3 | Alimentación para el sensor de temperatura. |
DS18B20 Module | GND | ESP32 | GND | Conexión al GND común. |
DS18B20 Module | DATA | ESP32 | GPIO 23 | Línea de datos del bus One-Wire. |
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.py
en 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.py
una vez. - Iniciar el Servidor: En la PC, ejecutar
python servidor.py
y 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.py
para 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 = "INFINITUM4CA5_5"
WIFI_PASS = "B0l1graf0_N3gr0&2023"
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.")