/*
============================================================================
NeoLum-Dosis — Avance de simulación en Wokwi
ITM — Desarrollo de Equipos Biomédicos — Entregable #2
============================================================================
Este firmware simula el lazo de control descrito en el Anexo 1
(tabla "Funcionalidad, Parte") sobre los componentes que Wokwi sí permite
simular. Cubre:
- Items 1-6: dosimetría de doble fotodiodo, control PWM del LED y
bitácora de dosis/calibración.
- Items 7-9: seguridad térmica (doble DS18B20 + NTC de respaldo,
interbloqueo por software + relé).
- Items 10-12: setpoint remoto simulado por Monitor Serie (ACK y
tolerancia a pérdida de "conexión"), con bitácora de
auditoría.
- Protección ocular (interruptor "gafas colocadas") y parada de
emergencia física (kill switch), según las Advertencias del Anexo 1.
LIMITACIONES DE LA SIMULACIÓN (léase antes de interpretar los números):
- Wokwi no tiene modelos de TSL2591, VL53L0X, INA219, NRF24L01 ni
SIM800L. Se sustituyen por análogos funcionales (fotorresistencias,
interruptor, botón) para validar la LÓGICA de control, no el
hardware final.
- Las "irradiancias" calculadas en leerIrradiancia() son un valor
PROXY proporcional a la luz que detecta el LDR simulado (0-50,
en las mismas unidades que el setpoint para que el lazo de control
tenga sentido) — NO son una medición radiométrica calibrada. En el
prototipo físico ese valor lo entrega el fotodiodo TSL2591 calibrado
contra un radiómetro patrón (Anexo 2).
- El driver de corriente constante (MOSFET/BuckPuck, item BOM #6) se
simplifica aquí a un LED + resistencia controlados por PWM directo
del GPIO, para poder validar el algoritmo sin modelar la etapa de
potencia.
============================================================================
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <RTClib.h>
#include <SPI.h>
#include <SD.h>
// ---------------------------------------------------------------------------
// Mapa de pines (ver diagram.json para el cableado completo)
// ---------------------------------------------------------------------------
#define PIN_SDA 21
#define PIN_SCL 22
#define PIN_ONEWIRE 4
#define PIN_FOTO_REF 34 // Fotodiodo #1 — referencia (proxy LDR)
#define PIN_FOTO_PIEL 35 // Fotodiodo #2 — superficie tratamiento (proxy LDR)
#define PIN_NTC_BACKUP 32 // Termistor NTC de respaldo (BOM #19)
#define PIN_LED_TERAPIA 27 // PWM al LED terapéutico 450-460nm (proxy)
#define PIN_RELE_TERMICO 26 // Relé de corte de emergencia térmica
#define PIN_LED_VERDE 13 // Indicador OK
#define PIN_LED_AMARILLO 12 // Indicador ALERTA
#define PIN_LED_ROJO 14 // Indicador PARADA
#define PIN_BUZZER 19 // Alarma sonora
#define PIN_BOTON_EMERGENCIA 18 // Kill switch físico
#define PIN_SW_GAFAS 23 // Confirmación "gafas colocadas"
#define PIN_SD_CS 5
#define PIN_SD_SCK 15
#define PIN_SD_MOSI 2
#define PIN_SD_MISO 25
// ---------------------------------------------------------------------------
// Parámetros clínicos y de control (ver Anexo 1 y Anexo 3 §4.3)
// ---------------------------------------------------------------------------
const float DOSIS_OBJETIVO_UW_DEFAULT = 30.0; // µW/cm²/nm — guía AAP 2022
const float TEMP_CRITICA_C = 38.5; // límite IEC 60601-2-50
const float TEMP_ALERTA_C = 37.0; // margen preventivo antes del corte
const float DIFERENCIAL_MAX_C = 8.0; // diferencial conector-ambiente máximo
const int PWM_MAX = 255;
const int PWM_PASO = 6; // paso del control proporcional simple
const unsigned long INTERVALO_CONTROL_MS = 2000;
const unsigned long INTERVALO_BITACORA_MS = 10000; // registro/autocalibración periódica
const int CICLOS_SATURACION_ALERTA = 6; // ciclos con PWM al máximo antes de alertar degradación
// ---------------------------------------------------------------------------
// Objetos de periféricos
// ---------------------------------------------------------------------------
Adafruit_SSD1306 display(128, 64, &Wire, -1);
OneWire oneWire(PIN_ONEWIRE);
DallasTemperature sensoresTemp(&oneWire);
RTC_DS1307 rtc;
SPIClass spiSD(VSPI);
// ---------------------------------------------------------------------------
// Estado del sistema
// ---------------------------------------------------------------------------
// NOTA: no se llama "OK" porque el core de ESP32 ya usa ese nombre
// internamente (enum STATUS en ets_sys.h) y choca al compilar.
enum EstadoEquipo { ESTADO_OK, ALERTA, PARADA };
EstadoEquipo estado = PARADA; // arranca en espera hasta confirmar gafas
bool emergenciaActivada = false; // parada de emergencia: sólo se limpia con reinicio
bool sdDisponible = false;
bool rtcDisponible = false;
float setpointActual = DOSIS_OBJETIVO_UW_DEFAULT;
int pwmActual = 0;
int ciclosEnSaturacion = 0;
unsigned long ultimoControl = 0;
unsigned long ultimaBitacora = 0;
unsigned long ultimoParpadeo = 0;
bool buzzerEncendido = false;
// ---------------------------------------------------------------------------
// Prototipos
// ---------------------------------------------------------------------------
float leerIrradiancia(int pin);
void evaluarEstado(float tConector, float tAmbiente, float irrPiel);
void actualizarControlDosis(float irrPiel);
void actualizarIndicadores();
void actualizarPantalla(float irrRef, float irrPiel, float tConector, float tAmbiente);
void imprimirSerial(float irrRef, float irrPiel, float tConector, float tAmbiente);
void registrarBitacora(String evento);
String marcaDeTiempo();
void revisarComandosSerie();
// ===========================================================================
void setup() {
Serial.begin(115200); //
delay(200);
Serial.println(F("=== NeoLum-Dosis | Avance de simulacion Wokwi ==="));
pinMode(PIN_LED_TERAPIA, OUTPUT);
pinMode(PIN_RELE_TERMICO, OUTPUT);
pinMode(PIN_LED_VERDE, OUTPUT);
pinMode(PIN_LED_AMARILLO, OUTPUT);
pinMode(PIN_LED_ROJO, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
pinMode(PIN_BOTON_EMERGENCIA, INPUT_PULLUP);
pinMode(PIN_SW_GAFAS, INPUT_PULLUP);
digitalWrite(PIN_RELE_TERMICO, LOW);
digitalWrite(PIN_BUZZER, LOW);
analogWrite(PIN_LED_TERAPIA, 0);
Wire.begin(PIN_SDA, PIN_SCL);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("[ERROR] No se detecto la pantalla OLED (direccion 0x3C)"));
} else {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println(F("NeoLum-Dosis"));
display.println(F("Avance Wokwi"));
display.println(F("Iniciando..."));
display.display();
}
sensoresTemp.begin();
rtcDisponible = rtc.begin();
if (!rtcDisponible) {
Serial.println(F("[ERROR] No se detecto el RTC DS1307"));
}
// --- REEMPLAZA EL BLOQUE DE LA TARJETA SD POR ESTE NUEVO ---
// Inicializa el bus SPI estándar usando los pines nativos asignados en tu mapa
SPI.begin(PIN_SD_SCK, PIN_SD_MISO, PIN_SD_MOSI, PIN_SD_CS);
// Llama a SD.begin pasándole únicamente el pin de CS y deja que use el bus SPI activo
sdDisponible = SD.begin(PIN_SD_CS);
if (!sdDisponible) {
Serial.println(F("[AVISO] microSD no disponible; la bitacora solo se enviara por Serial"));
}
// ------------------------------------------------------------
registrarBitacora("Equipo encendido. Setpoint inicial=" + String(setpointActual, 1) + " uW/cm2/nm");
Serial.println(F("Escriba SET:<valor> en el Monitor Serie para simular el cambio remoto"));
Serial.println(F("de dosis desde la app clinica (ej: SET:32). Escriba STATUS para ver el estado."));
}
// ===========================================================================
void loop() {
revisarComandosSerie();
// --- Parada de emergencia: prioridad maxima, se revisa en cada vuelta ---
if (digitalRead(PIN_BOTON_EMERGENCIA) == LOW && !emergenciaActivada) {
emergenciaActivada = true;
digitalWrite(PIN_RELE_TERMICO, HIGH);
analogWrite(PIN_LED_TERAPIA, 0);
pwmActual = 0;
registrarBitacora("PARADA DE EMERGENCIA activada manualmente por el usuario");
}
if (millis() - ultimoControl >= INTERVALO_CONTROL_MS) {
ultimoControl = millis();
sensoresTemp.requestTemperatures();
float tConector = sensoresTemp.getTempCByIndex(0);
float tAmbiente = sensoresTemp.getTempCByIndex(1);
float irrRef = leerIrradiancia(PIN_FOTO_REF);
float irrPiel = leerIrradiancia(PIN_FOTO_PIEL);
// Redundancia de temperatura (BOM #19): solo se audita, no gobierna el corte
int crudoNtc = analogRead(PIN_NTC_BACKUP);
float tRespaldo = 15.0 + (crudoNtc / 4095.0) * 30.0; // proxy simplificado 15-45C
if (!isnan(tConector) && fabs(tConector - tRespaldo) > 6.0) {
Serial.println(F("[AVISO] Discrepancia DS18B20 vs NTC de respaldo > 6 C"));
}
evaluarEstado(tConector, tAmbiente, irrPiel);
if (estado != PARADA) {
actualizarControlDosis(irrPiel);
} else {
pwmActual = 0;
analogWrite(PIN_LED_TERAPIA, 0);
}
actualizarIndicadores();
actualizarPantalla(irrRef, irrPiel, tConector, tAmbiente);
imprimirSerial(irrRef, irrPiel, tConector, tAmbiente);
}
if (millis() - ultimaBitacora >= INTERVALO_BITACORA_MS) {
ultimaBitacora = millis();
registrarBitacora("Autocalibracion/chequeo periodico. PWM=" + String(pwmActual) +
" Estado=" + String((int)estado) +
" Setpoint=" + String(setpointActual, 1));
}
}
// ===========================================================================
// Items 1-2: lectura de irradiancia (valor proxy, ver nota al inicio)
// ===========================================================================
float leerIrradiancia(int pinAnalogico) {
int crudo = analogRead(pinAnalogico); // 0-4095
return (crudo / 4095.0) * 50.0; // escala 0-50 (mismas unidades que el setpoint)
}
// ===========================================================================
// Items 7-9 + protección ocular + emergencia: máquina de estados de seguridad
// Prioridad: emergencia > térmica > protección ocular > degradación LED
// ===========================================================================
void evaluarEstado(float tConector, float tAmbiente, float irrPiel) {
if (emergenciaActivada) {
estado = PARADA;
return;
}
float diferencial = tConector - tAmbiente;
if (tConector >= TEMP_CRITICA_C || diferencial >= DIFERENCIAL_MAX_C) {
if (estado != PARADA) {
registrarBitacora("ALERTA TERMICA: corte activado. Tconector=" + String(tConector, 1) +
" Tambiente=" + String(tAmbiente, 1) +
" Diferencial=" + String(diferencial, 1));
}
estado = PARADA;
digitalWrite(PIN_RELE_TERMICO, HIGH);
return;
}
digitalWrite(PIN_RELE_TERMICO, LOW);
bool gafasColocadas = (digitalRead(PIN_SW_GAFAS) == LOW);
if (!gafasColocadas) {
if (estado != PARADA) {
registrarBitacora("Tratamiento en espera: proteccion ocular no confirmada");
}
estado = PARADA; // Anexo 1: "el equipo no debe iniciar la sesion sin esta confirmacion"
return;
}
if (tConector >= TEMP_ALERTA_C) {
if (estado == ESTADO_OK) registrarBitacora("Alerta termica preventiva. Tconector=" + String(tConector, 1));
estado = ALERTA;
return;
}
if (ciclosEnSaturacion >= CICLOS_SATURACION_ALERTA) {
if (estado == ESTADO_OK) registrarBitacora("Compensacion de degradacion del LED insuficiente (PWM saturado)");
estado = ALERTA;
return;
}
estado = ESTADO_OK;
}
// ===========================================================================
// Items 3-5: comparación contra setpoint y ajuste proporcional del PWM
// ===========================================================================
void actualizarControlDosis(float irrPiel) {
float error = setpointActual - irrPiel;
if (error > 0.3) {
pwmActual = min(PWM_MAX, pwmActual + PWM_PASO);
} else if (error < -0.3) {
pwmActual = max(0, pwmActual - PWM_PASO);
}
analogWrite(PIN_LED_TERAPIA, pwmActual);
if (pwmActual >= PWM_MAX && irrPiel < setpointActual * 0.9) {
ciclosEnSaturacion++;
} else {
ciclosEnSaturacion = 0;
}
}
// ===========================================================================
// Indicadores visuales/sonoros (codigo de colores del Anexo: verde/amarillo/rojo)
// ===========================================================================
void actualizarIndicadores() {
digitalWrite(PIN_LED_VERDE, estado == ESTADO_OK);
digitalWrite(PIN_LED_AMARILLO, estado == ALERTA);
digitalWrite(PIN_LED_ROJO, estado == PARADA);
if (estado == ESTADO_OK) {
noTone(PIN_BUZZER);
return;
}
// Parpadeo/beep no bloqueante: mas rapido en PARADA que en ALERTA
unsigned long periodo = (estado == PARADA) ? 300 : 800;
if (millis() - ultimoParpadeo >= periodo) {
ultimoParpadeo = millis();
buzzerEncendido = !buzzerEncendido;
if (buzzerEncendido) {
tone(PIN_BUZZER, 2000);
} else {
noTone(PIN_BUZZER);
}
}
}
// ===========================================================================
// Pantalla OLED
// ===========================================================================
void actualizarPantalla(float irrRef, float irrPiel, float tConector, float tAmbiente) {
const char* nombreEstado = (estado == ESTADO_OK) ? "OK" : (estado == ALERTA) ? "ALERTA" : "PARADA";
display.clearDisplay();
display.setCursor(0, 0);
display.print(F("NeoLum-Dosis "));
display.println(nombreEstado);
display.print(F("Irr ref: "));
display.println(irrRef, 1);
display.print(F("Irr piel:"));
display.print(irrPiel, 1);
display.print(F(" / "));
display.println(setpointActual, 1);
display.print(F("Tconn:"));
display.print(tConector, 1);
display.print(F(" Tamb:"));
display.println(tAmbiente, 1);
display.print(F("PWM: "));
display.print(pwmActual);
display.print(F("/255 "));
display.println(emergenciaActivada ? F("EMERG") : F(""));
display.display();
}
// ===========================================================================
// Espejo por Monitor Serie de lo que muestra el OLED (una linea cada ciclo)
// ===========================================================================
void imprimirSerial(float irrRef, float irrPiel, float tConector, float tAmbiente) {
const char* nombreEstado = (estado == ESTADO_OK) ? "OK" : (estado == ALERTA) ? "ALERTA" : "PARADA";
Serial.print(F("Estado="));
Serial.print(nombreEstado);
Serial.print(F(" | IrrRef="));
Serial.print(irrRef, 1);
Serial.print(F(" IrrPiel="));
Serial.print(irrPiel, 1);
Serial.print(F("/"));
Serial.print(setpointActual, 1);
Serial.print(F(" | Tconn="));
Serial.print(tConector, 1);
Serial.print(F("C Tamb="));
Serial.print(tAmbiente, 1);
Serial.print(F("C | PWM="));
Serial.print(pwmActual);
Serial.print(F("/255"));
if (emergenciaActivada) Serial.print(F(" | EMERGENCIA"));
Serial.println();
}
// ===========================================================================
// Items 6, 9 y 12: bitácora de dosis, eventos térmicos y auditoría de cambios
// ===========================================================================
String marcaDeTiempo() {
if (!rtcDisponible) return String(millis() / 1000) + "s";
DateTime ahora = rtc.now();
char buffer[20];
snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d",
ahora.year(), ahora.month(), ahora.day(),
ahora.hour(), ahora.minute(), ahora.second());
return String(buffer);
}
void registrarBitacora(String evento) {
String linea = "[" + marcaDeTiempo() + "] " + evento;
Serial.println(linea);
if (sdDisponible) {
File archivo = SD.open("/bitacora.txt", FILE_APPEND);
if (archivo) {
archivo.println(linea);
archivo.close();
}
}
}
// ===========================================================================
// Items 10-11: setpoint remoto simulado por Monitor Serie (sustituye la app
// clinica/GSM-WiFi real, que Wokwi no puede simular en este avance) + ACK
// ===========================================================================
void revisarComandosSerie() {
if (!Serial.available()) return;
String linea = Serial.readStringUntil('\n');
linea.trim();
if (linea.length() == 0) return;
if (linea.startsWith("SET:")) {
float nuevo = linea.substring(4).toFloat();
if (nuevo > 0 && nuevo <= 50.0) {
setpointActual = nuevo;
Serial.println("[ACK] Nuevo setpoint confirmado: " + String(setpointActual, 1) + " uW/cm2/nm");
registrarBitacora("Cambio remoto de setpoint a " + String(setpointActual, 1) + " uW/cm2/nm (vía Serial, simula app clinica)");
} else {
Serial.println(F("[ERROR] Valor fuera de rango (0-50). Setpoint anterior se mantiene (tolerancia a fallos)."));
}
} else if (linea == "STATUS") {
Serial.println("Estado=" + String((int)estado) + " Setpoint=" + String(setpointActual, 1) +
" PWM=" + String(pwmActual) + " Emergencia=" + String(emergenciaActivada));
} else {
Serial.println(F("Comando no reconocido. Use SET:<valor> o STATUS"));
}
}
Loading
ssd1306
ssd1306