"""
INDUSYNC CONNECT - Módulo IoT para mantenimiento predictivo
Fusión: Monitoreo Bomba Putzmeister M-360 + Conexión MQTT
Autor: Luis Anderson Tique Pinto - SOLUCIONES INDUSYNC S.A.S
NOTA DE ARQUITECTURA:
Para la demo en vivo usamos el broker público broker.mqttdashboard.com
(anónimo, puerto plano 1883) porque el simulador Wokwi prioriza
disponibilidad del canal. El despliegue productivo usa HiveMQ Cloud
con TLS 8883 y credenciales (ya aprovisionado y verificado).
La arquitectura es idéntica: solo cambia la URL del broker.
"""
from machine import Pin, ADC, I2C
import time
import onewire
import ds18x20
import ujson
import network
from umqtt.simple import MQTTClient
# ==================== CONFIGURACIÓN MQTT (broker público para demo) ====================
MQTT_CLIENT_ID = "indusync-esp32-wokwi"
MQTT_BROKER = "broker.mqttdashboard.com" # público, sin auth: funciona siempre
MQTT_PORT = 1883 # puerto PLANO (sin TLS)
MQTT_TOPIC = "indusync/ESP32-001/telemetria"
MQTT_STATUS_TOPIC = "indusync/ESP32-001/estado"
MQTT_MTBF_TOPIC = "indusync/ESP32-001/metricas"
# ==================== CONFIGURACIÓN DE PINES ====================
pin_presion = ADC(Pin(34)) # 0-250 Bar (potenciómetro)
pin_corriente = ADC(Pin(35)) # 0-100 A (potenciómetro)
pin_led = Pin(2, Pin.OUT) # LED indicador de alerta
pin_onewire = Pin(4) # DS18B20 (temperatura)
pin_presion.atten(ADC.ATTN_11DB)
pin_corriente.atten(ADC.ATTN_11DB)
# ==================== CONFIGURAR SENSORES ====================
# 1) Temperatura DS18B20 (OneWire) - la clase de MicroPython se llama DS18X20
ow = onewire.OneWire(pin_onewire)
sensor_temp = ds18x20.DS18X20(ow)
roms = sensor_temp.scan()
if len(roms) == 0:
print("AVISO: No se encontró sensor DS18B20. Usando simulación.")
# 2) Vibración MPU6050 (acelerómetro I2C)
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000)
MPU6050_ADDR = 0x68
try:
i2c.writeto_mem(MPU6050_ADDR, 0x6B, b'\x00') # despertar sensor
print("MPU6050 detectado correctamente")
except:
print("AVISO: MPU6050 no detectado.")
# ==================== VARIABLES GLOBALES MTBF / MTTR ====================
total_fallas = 0
total_tiempo_operacion_seg = 0
tiempo_inicio_operacion = 0
tiempo_inicio_falla = 0
total_tiempo_reparacion_seg = 0
def calcular_mtbf():
"""MTBF = tiempo total de operación / número de fallas (horas)"""
if total_fallas == 0:
return 0.0
return (total_tiempo_operacion_seg / total_fallas) / 3600.0
def calcular_mttr():
"""MTTR = tiempo total de reparación / número de fallas (horas)"""
if total_fallas == 0:
return 0.0
return (total_tiempo_reparacion_seg / total_fallas) / 3600.0
def calcular_disponibilidad():
"""Disponibilidad = MTBF / (MTBF + MTTR) * 100"""
mtbf = calcular_mtbf()
mttr = calcular_mttr()
if (mtbf + mttr) == 0:
return 100.0
return (mtbf / (mtbf + mttr)) * 100.0
# ==================== IA PREDICTIVA (regresión lineal ponderada) ====================
def predecir_falla(temperatura, vibracion_x, presion):
"""
riesgo = 0.35*temp_norm + 0.40*vib_norm + 0.25*pres_norm
Retorna: (nivel_riesgo, mensaje, porcentaje)
"""
temp_norm = min(1.0, temperatura / 100.0)
vib_norm = min(1.0, abs(vibracion_x) / 2.5)
pres_norm = min(1.0, presion / 250.0)
riesgo = (temp_norm * 0.35) + (vib_norm * 0.40) + (pres_norm * 0.25)
porcentaje = riesgo * 100
if riesgo >= 0.7:
return "CRITICO", "Falla inminente - Detener máquina", porcentaje
elif riesgo >= 0.4:
return "ALTO", "Programar mantenimiento pronto", porcentaje
elif riesgo >= 0.2:
return "MEDIO", "Monitorear tendencias", porcentaje
else:
return "BAJO", "Operación normal", porcentaje
# ==================== FUNCIONES DE LECTURA ====================
def leer_presion():
"""ADC -> 0 a 250 Bar"""
return (pin_presion.read() / 4095.0) * 250.0
def leer_corriente():
"""ADC -> 0 a 100 A"""
return (pin_corriente.read() / 4095.0) * 100.0
def leer_vibracion():
"""MPU6050 eje X -> g"""
try:
data = i2c.readfrom_mem(MPU6050_ADDR, 0x3B, 2)
acc_x = (data[0] << 8) | data[1]
if acc_x > 32767:
acc_x -= 65536
return acc_x / 16384.0
except:
return 0.0
def leer_temperatura():
"""DS18B20 o simulación si no responde"""
if len(roms) == 0:
return 25.0 + (time.ticks_ms() % 1000) / 100.0
try:
sensor_temp.convert_temp()
return sensor_temp.read_temp(roms[0])
except:
return 25.0
# ==================== CONEXIÓN RED + MQTT (broker público, SIN TLS) ====================
def conectar_red_y_mqtt():
print("\nConectando Wi-Fi Wokwi...", end="")
sta_if = network.WLAN(network.STA_IF)
sta_if.active(True)
sta_if.connect('Wokwi-GUEST', '')
intentos = 0
while not sta_if.isconnected() and intentos < 20:
print(".", end="")
time.sleep(0.5)
intentos += 1
if sta_if.isconnected():
print(" ¡Conectado!")
else:
print(" Falló Wi-Fi")
return False
print("Conectando al broker público...", end="")
try:
global client
# SIN ssl, SIN user, SIN password: broker anónimo y plano
client = MQTTClient(MQTT_CLIENT_ID, MQTT_BROKER, port=MQTT_PORT)
client.connect()
print(" ¡Conectado al broker público!")
return True
except Exception as e:
print(" Error:", e)
return False
# Conexión inicial
conectar_red_y_mqtt()
# ==================== SINCRONIZACIÓN DE RELOJ (NTP) ====================
try:
import ntptime
ntptime.settime()
print("🕐 Reloj sincronizado por NTP")
except Exception:
print("⚠️ NTP no disponible; el backend usará su hora")
# ==================== UMBRALES DE ALERTA ====================
UMBRAL_PRESION_ALERTA = 220.0 # Bar (límite hidráulico TK 70)
UMBRAL_TEMP_ALERTA = 90.0 # °C (límite aceite térmico)
UMBRAL_VIB_ALERTA = 1.5 # g (límite vibración estructural)
estado_previo_alerta = None
tiempo_ultimo_envio_metricas = 0
print("\n" + "="*60)
print("INDUSYNC CONNECT - SISTEMA DE TELEMETRÍA ACTIVO")
print("Broker:", MQTT_BROKER, "| ritmo: 1 Hz")
print("MTBF y MTTR en cálculo continuo")
print("="*60)
# ==================== BUCLE PRINCIPAL (1 Hz) ====================
while True:
inicio_ciclo = time.ticks_ms()
# 1) LECTURA DE SENSORES
presion = leer_presion()
corriente = leer_corriente()
temperatura = leer_temperatura()
vibracion_x = leer_vibracion()
# 2) DETECCIÓN DE ALERTAS POR UMBRAL
alerta = False
motivo_alerta = ""
if presion > UMBRAL_PRESION_ALERTA:
alerta = True
motivo_alerta += "ALTA PRESION MOTOBOMBA DE CONCRETO "
if temperatura > UMBRAL_TEMP_ALERTA:
alerta = True
motivo_alerta += "SOBRECALENTAMIENTO DE MOTOBOMBA DE CONCRETO "
if abs(vibracion_x) > UMBRAL_VIB_ALERTA:
alerta = True
motivo_alerta += "ALTA VIBRACION DE MOTOBOMBA DE CONCRETO "
# 3) MTBF / MTTR AL CAMBIAR DE ESTADO
tiempo_actual = time.ticks_ms() // 1000
if alerta != estado_previo_alerta:
if alerta:
# OPERATIVO -> FALLA
tiempo_inicio_falla = tiempo_actual
if tiempo_inicio_operacion > 0:
total_tiempo_operacion_seg += tiempo_actual - tiempo_inicio_operacion
print(f"\n⚠️ FALLA DETECTADA en t={tiempo_actual}s | Motivo: {motivo_alerta}")
else:
# FALLA -> OPERATIVO
if tiempo_inicio_falla > 0:
tiempo_reparado = tiempo_actual - tiempo_inicio_falla
total_tiempo_reparacion_seg += tiempo_reparado
total_fallas += 1
print(f"\n✅ MÁQUINA OPERATIVA en t={tiempo_actual}s "
f"| Reparación: {tiempo_reparado}s | Fallas: {total_fallas}")
tiempo_inicio_operacion = tiempo_actual
estado_previo_alerta = alerta
# 4) INDICADORES DE CONFIABILIDAD
mtbf = calcular_mtbf()
mttr = calcular_mttr()
disponibilidad = calcular_disponibilidad()
# 5) PREDICCIÓN DE FALLA (IA)
nivel_riesgo, mensaje_ia, porcentaje_riesgo = predecir_falla(
temperatura, vibracion_x, presion)
# 6) LED LOCAL
pin_led.value(1 if alerta else 0)
# 7) PAYLOAD DE TELEMETRÍA (contrato que el backend espera)
payload = {
"ts": int(time.time()),
"maq": "ESP32-001",
"presion": round(presion, 1),
"corriente": round(corriente, 1),
"temperatura": round(temperatura, 1),
"vibracion": round(vibracion_x, 2),
"alerta": alerta,
"motivo": motivo_alerta if alerta else "Ninguno",
"mtbf_hrs": round(mtbf, 2),
"mttr_hrs": round(mttr, 2),
"disponibilidad": round(disponibilidad, 1),
"nivel_riesgo": nivel_riesgo,
"porcentaje_riesgo": round(porcentaje_riesgo, 1),
"fallas_totales": total_fallas,
}
# 8) PUBLICACIÓN MQTT tolerante a fallos
try:
client.publish(MQTT_TOPIC, ujson.dumps(payload))
print(f"📡 [{tiempo_actual}s] Pres:{presion:.1f}Bar | "
f"Temp:{temperatura:.1f}°C | Riesgo:{nivel_riesgo}"
f"({porcentaje_riesgo:.0f}%) | MTBF:{mtbf:.1f}h | "
f"Disp:{disponibilidad:.0f}%")
except Exception:
print("⚠️ Error MQTT, reconectando...")
try:
client.connect()
print("Reconectado!")
except:
pass
# 9) MÉTRICAS AGREGADAS CADA 60 s
if time.ticks_ms() - tiempo_ultimo_envio_metricas > 60000:
payload_metricas = {
"ts": int(time.time()),
"maq": "ESP32-001",
"total_fallas": total_fallas,
"mtbf_hrs": round(mtbf, 2),
"mttr_hrs": round(mttr, 2),
"disponibilidad": round(disponibilidad, 1),
"tiempo_operacion_hrs": round(total_tiempo_operacion_seg / 3600.0, 2),
"tiempo_reparacion_hrs": round(total_tiempo_reparacion_seg / 3600.0, 2),
}
try:
client.publish(MQTT_MTBF_TOPIC, ujson.dumps(payload_metricas))
tiempo_ultimo_envio_metricas = time.ticks_ms()
except:
pass
# 10) CAMBIO DE ESTADO (solo al cambiar)
if alerta != estado_previo_alerta:
payload_estado = {
"ts": int(time.time()),
"maq": "ESP32-001",
"estado": "FALLA" if alerta else "OPERATIVO",
"motivo": motivo_alerta if alerta else "Ninguno",
"timestamp": tiempo_actual,
}
try:
client.publish(MQTT_STATUS_TOPIC, ujson.dumps(payload_estado))
print(f"📢 Cambio de estado: {payload_estado['estado']}")
except:
pass
# 11) TEMPORIZACIÓN: ciclo cada 1000 ms (1 Hz)
# Un mensaje por segundo: ideal para demo en vivo y respetuoso
# con el rate-limiting del broker público gratuito.
duracion = time.ticks_diff(time.ticks_ms(), inicio_ciclo)
tiempo_espera = 1000 - duracion
if tiempo_espera > 0:
time.sleep_ms(tiempo_espera)