#include #include #include #include #include #include #include #include #include // ==================================================== // WIFI // ==================================================== const char* WIFI_SSID = "wifiPrueba"; const char* WIFI_PASS = "12345678"; // Peru UTC-5 const long GMT_OFFSET_SEC = -5 * 3600; const int DAYLIGHT_OFFSET_SEC = 0; const char* NTP_SERVER = "pool.ntp.org"; bool horaSincronizada = false; unsigned long ultimoIntentoHora = 0; // ==================================================== // TFT ILI9341 - XIAO ESP32C3 // ==================================================== #define TFT_CS 5 #define TFT_DC 6 #define TFT_RST -1 #define TFT_SCLK 8 #define TFT_MOSI 10 Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST); // ==================================================== // I2C PRINCIPAL // ==================================================== #define I2C_SDA 21 #define I2C_SCL 20 // ==================================================== // TCA9548A // ==================================================== #define TCA_ADDR 0x70 // Si el sensor que detecta la mano no activa, // prueba intercambiar estos canales: // CANAL_STOCK = 1 y CANAL_DISPENSAR = 0 #define CANAL_STOCK 0 #define CANAL_DISPENSAR 1 Adafruit_VL53L0X sensorStock = Adafruit_VL53L0X(); Adafruit_VL53L0X sensorDispensar = Adafruit_VL53L0X(); bool stockDetectado = false; bool dispensarDetectado = false; // ==================================================== // INA219 // ==================================================== Adafruit_INA219 ina219; bool inaDetectado = false; float voltajeBateria = 0.0; float corrienteMA = 0.0; float potenciaMW = 0.0; float shuntMV = 0.0; float busVoltage = 0.0; // Bateria Li-ion 3S const float BAT_LLENA = 12.6; const float BAT_BAJA = 10.8; const float BAT_CRITICA = 9.9; // ==================================================== // MOTOR 28BYJ-48 + ULN2003 // ==================================================== #define IN1 2 #define IN2 3 #define IN3 4 #define IN4 7 const int stepsPerRevolution = 2048; const int VELOCIDAD_RPM = 5; Stepper myStepper(stepsPerRevolution, IN1, IN3, IN2, IN4); bool motorGirando = false; bool objetoAntes = false; // ==================================================== // CALIBRACION // ==================================================== // Sensor de stock: // menor distancia = mas stock // mayor distancia = menos stock const int STOCK_LLENO_MM = 35; const int STOCK_VACIO_MM = 160; // Sensor de dispensado. // Lo subi a 150 mm para que sea mas facil probar con la mano. const int DISTANCIA_DISPENSAR_MM = 150; // Para pruebas: true permite dispensar aunque el stock marque 0%. // En producto final puedes ponerlo en false. const bool PERMITIR_DISPENSAR_SIN_STOCK = true; // ==================================================== // VARIABLES DE LECTURA // ==================================================== unsigned long tiempoLectura = 0; unsigned long tiempoPantalla = 0; const unsigned long INTERVALO_LECTURA = 300; const unsigned long INTERVALO_PANTALLA = 5000; int distanciaStockMM = 9999; int distanciaDispensarMM = 9999; int stockPorcentaje = 0; int bateriaPorcentaje = 0; // ==================================================== // CICLO MENSTRUAL CONFIGURABLE // ==================================================== // Cambia esta fecha por el inicio del ultimo ciclo registrado. int cicloInicioYear = 2026; int cicloInicioMonth = 6; int cicloInicioDay = 1; const int CICLO_DIAS = 28; int diaCiclo = 1; // Dias marcados visualmente en la pantalla calendario. // Usa -1 para desactivar uno. const int DIA_MARCADO_1 = 6; const int DIA_MARCADO_2 = 7; // ==================================================== // CONTROL DE PANTALLAS // ==================================================== int pantallaActual = 0; const int TOTAL_PANTALLAS = 5; // ==================================================== // COLORES // ==================================================== #define COLOR_BG ILI9341_BLACK #define COLOR_PINK ILI9341_PINK #define COLOR_MAGENTA ILI9341_MAGENTA #define COLOR_CYAN ILI9341_CYAN #define COLOR_WHITE ILI9341_WHITE #define COLOR_YELLOW ILI9341_YELLOW #define COLOR_GREEN ILI9341_GREEN #define COLOR_RED ILI9341_RED #define COLOR_ORANGE ILI9341_ORANGE #define COLOR_LILAC 0xA81F #define COLOR_SOFTPINK 0xFC9F #define COLOR_CREAM 0xFFDB // ==================================================== // UTILIDADES GRAFICAS // ==================================================== void textoCentrado(String texto, int y, int size, uint16_t color) { int16_t x1, y1; uint16_t w, h; tft.setTextSize(size); tft.setTextColor(color, COLOR_BG); tft.getTextBounds(texto, 0, y, &x1, &y1, &w, &h); int x = (320 - w) / 2; if (x < 0) x = 0; tft.setCursor(x, y); tft.print(texto); } void dibujarBarra(int x, int y, int w, int h, int porcentaje, uint16_t color) { porcentaje = constrain(porcentaje, 0, 100); tft.drawRoundRect(x, y, w, h, 5, ILI9341_DARKGREY); int ancho = map(porcentaje, 0, 100, 0, w - 4); if (ancho > 0) { tft.fillRoundRect(x + 2, y + 2, ancho, h - 4, 4, color); } } void dibujarCorazon(int x, int y, uint16_t color) { tft.fillCircle(x, y, 6, color); tft.fillCircle(x + 10, y, 6, color); tft.fillTriangle(x - 6, y + 3, x + 16, y + 3, x + 5, y + 18, color); } // ==================================================== // TCA9548A // ==================================================== void seleccionarCanalTCA(uint8_t canal) { if (canal > 7) return; Wire.beginTransmission(TCA_ADDR); Wire.write(1 << canal); Wire.endTransmission(); } void apagarCanalesTCA() { Wire.beginTransmission(TCA_ADDR); Wire.write(0); Wire.endTransmission(); } // ==================================================== // WIFI Y HORA // ==================================================== void conectarWiFi() { Serial.print("Conectando a WiFi: "); Serial.println(WIFI_SSID); WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS); int intentos = 0; while (WiFi.status() != WL_CONNECTED && intentos < 30) { delay(500); Serial.print("."); intentos++; } Serial.println(); if (WiFi.status() == WL_CONNECTED) { Serial.println("WiFi conectado"); Serial.print("IP: "); Serial.println(WiFi.localIP()); } else { Serial.println("No se pudo conectar a WiFi"); } } void sincronizarHora() { if (WiFi.status() != WL_CONNECTED) { horaSincronizada = false; Serial.println("Sin WiFi, no se puede sincronizar hora"); return; } configTime(GMT_OFFSET_SEC, DAYLIGHT_OFFSET_SEC, NTP_SERVER); Serial.println("Sincronizando hora NTP..."); struct tm timeinfo; int intentos = 0; while (!getLocalTime(&timeinfo) && intentos < 20) { Serial.print("."); delay(500); intentos++; } Serial.println(); if (getLocalTime(&timeinfo)) { horaSincronizada = true; Serial.println("Hora sincronizada"); Serial.println(&timeinfo, "%d/%m/%Y %H:%M:%S"); } else { horaSincronizada = false; Serial.println("No se pudo obtener hora"); } } bool obtenerHora(struct tm &timeinfo) { if (!horaSincronizada) return false; return getLocalTime(&timeinfo); } // ==================================================== // FECHA / CICLO // ==================================================== time_t crearFecha(int year, int month, int day) { struct tm fecha; memset(&fecha, 0, sizeof(fecha)); fecha.tm_year = year - 1900; fecha.tm_mon = month - 1; fecha.tm_mday = day; fecha.tm_hour = 12; return mktime(&fecha); } int calcularDiaCiclo() { struct tm ahora; if (!obtenerHora(ahora)) { return 1; } time_t fechaActual = mktime(&ahora); time_t fechaInicio = crearFecha(cicloInicioYear, cicloInicioMonth, cicloInicioDay); long diferenciaSegundos = fechaActual - fechaInicio; int diasPasados = diferenciaSegundos / 86400; if (diasPasados < 0) diasPasados = 0; return (diasPasados % CICLO_DIAS) + 1; } String textoFlujo(int dia) { if (dia >= 1 && dia <= 2) return "Flujo alto esperado"; if (dia >= 3 && dia <= 5) return "Flujo moderado"; if (dia >= 6 && dia <= 7) return "Flujo bajo"; if (dia >= 12 && dia <= 16) return "Ventana fertil aprox."; return "Seguimiento normal"; } // ==================================================== // LECTURA DE SENSORES // ==================================================== int leerVL53(Adafruit_VL53L0X &sensor, uint8_t canal, bool detectado) { if (!detectado) return 9999; seleccionarCanalTCA(canal); VL53L0X_RangingMeasurementData_t measure; sensor.rangingTest(&measure, false); if (measure.RangeStatus != 4) { return measure.RangeMilliMeter; } return 9999; } void leerINA219() { if (!inaDetectado) return; shuntMV = ina219.getShuntVoltage_mV(); busVoltage = ina219.getBusVoltage_V(); corrienteMA = ina219.getCurrent_mA(); potenciaMW = ina219.getPower_mW(); voltajeBateria = busVoltage + (shuntMV / 1000.0); bateriaPorcentaje = map( (int)(voltajeBateria * 100), (int)(BAT_CRITICA * 100), (int)(BAT_LLENA * 100), 0, 100 ); bateriaPorcentaje = constrain(bateriaPorcentaje, 0, 100); } void calcularStock() { if (!stockDetectado || distanciaStockMM == 9999) { stockPorcentaje = 0; return; } stockPorcentaje = map(distanciaStockMM, STOCK_VACIO_MM, STOCK_LLENO_MM, 0, 100); stockPorcentaje = constrain(stockPorcentaje, 0, 100); } String estadoStock() { if (stockPorcentaje <= 5) return "Out of stock"; if (stockPorcentaje <= 30) return "Almost finished"; return "Full"; } String estadoBateria() { if (!inaDetectado) return "INA219 error"; if (voltajeBateria <= BAT_CRITICA) return "Bateria critica"; if (voltajeBateria <= BAT_BAJA) return "Bateria baja"; return "Bateria OK"; } // ==================================================== // MOTOR // ==================================================== void apagarMotor() { digitalWrite(IN1, LOW); digitalWrite(IN2, LOW); digitalWrite(IN3, LOW); digitalWrite(IN4, LOW); } void girarUnaVuelta() { motorGirando = true; Serial.println("Dispensando: motor una vuelta"); myStepper.step(stepsPerRevolution); apagarMotor(); motorGirando = false; Serial.println("Dispensado completo"); } // ==================================================== // PANTALLA 1: CALENDARIO // ==================================================== void pantallaCalendario() { tft.fillScreen(COLOR_BG); tft.drawRoundRect(10, 8, 300, 225, 18, COLOR_SOFTPINK); textoCentrado("Menstrual Cycle", 22, 2, COLOR_CREAM); struct tm ahora; bool okHora = obtenerHora(ahora); const char* dias[] = {"L", "M", "M", "J", "V", "S", "D"}; int x0 = 38; int yDias = 72; int yNums = 115; int sep = 40; tft.setTextSize(2); for (int i = 0; i < 7; i++) { tft.setTextColor(COLOR_CYAN, COLOR_BG); tft.setCursor(x0 + i * sep, yDias); tft.print(dias[i]); } for (int i = 0; i < 7; i++) { int numero = i + 1; int x = x0 + i * sep; int y = yNums; bool marcado = (numero == DIA_MARCADO_1 || numero == DIA_MARCADO_2); if (marcado) { tft.fillCircle(x + 8, y + 8, 16, COLOR_PINK); tft.setTextColor(COLOR_WHITE, COLOR_PINK); } else { tft.setTextColor(COLOR_WHITE, COLOR_BG); } tft.setCursor(x, y); tft.print(numero); } tft.setTextSize(1); tft.setTextColor(COLOR_CYAN, COLOR_BG); if (okHora) { char fecha[20]; char hora[20]; strftime(fecha, sizeof(fecha), "%d/%m/%Y", &ahora); strftime(hora, sizeof(hora), "%H:%M:%S", &ahora); textoCentrado(String(fecha), 170, 1, COLOR_CYAN); textoCentrado(String(hora), 188, 2, COLOR_WHITE); } else { textoCentrado("Sin hora WiFi/NTP", 178, 1, COLOR_RED); } tft.setTextSize(1); tft.setTextColor(COLOR_SOFTPINK, COLOR_BG); tft.setCursor(105, 218); tft.print("Dia ciclo: "); tft.print(diaCiclo); } // ==================================================== // PANTALLA 2: BATERIA // ==================================================== void pantallaBateria() { tft.fillScreen(COLOR_BG); textoCentrado("BATTERY STATUS", 25, 2, COLOR_CREAM); if (inaDetectado) { tft.setTextSize(3); tft.setTextColor(COLOR_YELLOW, COLOR_BG); String volt = String(voltajeBateria, 1) + "V"; textoCentrado(volt, 70, 3, COLOR_YELLOW); } else { textoCentrado("INA219 ERROR", 75, 2, COLOR_RED); } uint16_t colorBarra = COLOR_GREEN; if (bateriaPorcentaje <= 20) colorBarra = COLOR_RED; else if (bateriaPorcentaje <= 45) colorBarra = COLOR_YELLOW; dibujarBarra(35, 125, 250, 25, bateriaPorcentaje, colorBarra); textoCentrado(String(bateriaPorcentaje) + "% restante", 165, 2, colorBarra); textoCentrado(estadoBateria(), 200, 1, COLOR_WHITE); tft.setTextSize(1); tft.setTextColor(COLOR_CYAN, COLOR_BG); tft.setCursor(105, 220); tft.print("I: "); tft.print(corrienteMA, 0); tft.print(" mA"); } // ==================================================== // PANTALLA 3: STOCK // ==================================================== void pantallaStock() { tft.fillScreen(COLOR_BG); textoCentrado("STOCK ALERT", 25, 2, COLOR_CREAM); String estado = estadoStock(); uint16_t colorEstado = COLOR_GREEN; if (estado == "Out of stock") colorEstado = COLOR_RED; else if (estado == "Almost finished") colorEstado = COLOR_YELLOW; textoCentrado(estado, 70, 2, colorEstado); dibujarBarra(35, 115, 250, 26, stockPorcentaje, colorEstado); textoCentrado(String(stockPorcentaje) + "% restante", 155, 2, colorEstado); tft.setTextSize(1); tft.setTextColor(COLOR_WHITE, COLOR_BG); tft.setCursor(70, 205); tft.print("Stock sensor: "); tft.print(distanciaStockMM); tft.print(" mm"); } // ==================================================== // PANTALLA 4: DIA DEL CICLO // ==================================================== void pantallaDiaCiclo() { tft.fillScreen(COLOR_BG); textoCentrado("DIA DEL CICLO", 25, 3, COLOR_CREAM); textoCentrado(String(diaCiclo), 90, 6, COLOR_PINK); textoCentrado(textoFlujo(diaCiclo), 180, 2, COLOR_WHITE); } // ==================================================== // PANTALLA 5: FRASE MOTIVACIONAL // ==================================================== void pantallaMotivacional() { tft.fillScreen(COLOR_BG); textoCentrado("RECORDATORIO", 25, 2, COLOR_CYAN); dibujarCorazon(35, 165, COLOR_CYAN); tft.fillRoundRect(85, 85, 22, 55, 5, COLOR_PINK); tft.fillRoundRect(120, 65, 22, 75, 5, COLOR_PINK); tft.fillRoundRect(155, 95, 22, 45, 5, COLOR_PINK); tft.fillRoundRect(190, 75, 22, 65, 5, COLOR_PINK); tft.fillRoundRect(225, 105, 22, 35, 5, COLOR_PINK); textoCentrado("You've got this", 165, 3, COLOR_CYAN); textoCentrado("Toma agua. Descansa.", 210, 2, COLOR_WHITE); } // ==================================================== // MOSTRAR PANTALLA ACTUAL // ==================================================== void mostrarPantallaActual() { switch (pantallaActual) { case 0: pantallaCalendario(); break; case 1: pantallaBateria(); break; case 2: pantallaStock(); break; case 3: pantallaDiaCiclo(); break; case 4: pantallaMotivacional(); break; } } // ==================================================== // SETUP // ==================================================== void setup() { Serial.begin(115200); delay(1000); Serial.println("Iniciando dispensador inteligente"); pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT); pinMode(IN3, OUTPUT); pinMode(IN4, OUTPUT); apagarMotor(); myStepper.setSpeed(VELOCIDAD_RPM); SPI.begin(TFT_SCLK, -1, TFT_MOSI, TFT_CS); tft.begin(); tft.setRotation(1); tft.fillScreen(COLOR_BG); textoCentrado("Iniciando...", 100, 2, COLOR_WHITE); Wire.begin(I2C_SDA, I2C_SCL); conectarWiFi(); sincronizarHora(); seleccionarCanalTCA(CANAL_STOCK); if (!sensorStock.begin()) { Serial.println("No se detecta sensor de stock"); stockDetectado = false; } else { Serial.println("Sensor de stock OK"); stockDetectado = true; } seleccionarCanalTCA(CANAL_DISPENSAR); if (!sensorDispensar.begin()) { Serial.println("No se detecta sensor de dispensado"); dispensarDetectado = false; } else { Serial.println("Sensor de dispensado OK"); dispensarDetectado = true; } apagarCanalesTCA(); if (!ina219.begin()) { Serial.println("No se detecta INA219"); inaDetectado = false; } else { Serial.println("INA219 OK"); inaDetectado = true; } diaCiclo = calcularDiaCiclo(); leerINA219(); distanciaStockMM = leerVL53(sensorStock, CANAL_STOCK, stockDetectado); distanciaDispensarMM = leerVL53(sensorDispensar, CANAL_DISPENSAR, dispensarDetectado); apagarCanalesTCA(); calcularStock(); mostrarPantallaActual(); Serial.println("Sistema listo"); } // ==================================================== // LOOP // ==================================================== void loop() { if (!horaSincronizada && millis() - ultimoIntentoHora >= 60000) { ultimoIntentoHora = millis(); if (WiFi.status() == WL_CONNECTED) { sincronizarHora(); } } if (millis() - tiempoLectura >= INTERVALO_LECTURA) { tiempoLectura = millis(); distanciaStockMM = leerVL53(sensorStock, CANAL_STOCK, stockDetectado); distanciaDispensarMM = leerVL53(sensorDispensar, CANAL_DISPENSAR, dispensarDetectado); apagarCanalesTCA(); calcularStock(); leerINA219(); diaCiclo = calcularDiaCiclo(); bool objetoCerca = false; if (dispensarDetectado && distanciaDispensarMM > 0 && distanciaDispensarMM < DISTANCIA_DISPENSAR_MM) { objetoCerca = true; } bool stockPermite = PERMITIR_DISPENSAR_SIN_STOCK || stockPorcentaje > 0; if (objetoCerca && stockPermite && !objetoAntes && !motorGirando) { girarUnaVuelta(); // Refresca pantalla despues de dispensar mostrarPantallaActual(); } objetoAntes = objetoCerca; Serial.print("Stock: "); Serial.print(distanciaStockMM); Serial.print(" mm | Dispensar: "); Serial.print(distanciaDispensarMM); Serial.print(" mm | Stock%: "); Serial.print(stockPorcentaje); Serial.print("% | Bat: "); Serial.print(voltajeBateria); Serial.print("V | Hora: "); Serial.println(horaSincronizada ? "OK" : "NO"); } if (millis() - tiempoPantalla >= INTERVALO_PANTALLA) { tiempoPantalla = millis(); pantallaActual++; if (pantallaActual >= TOTAL_PANTALLAS) { pantallaActual = 0; } mostrarPantallaActual(); } }