#include <Arduino.h>
#include <LiquidCrystal_I2C.h>
#include <SD.h>
#include <RTClib.h>
#include <ArduinoJson.h>
// Configuraciones y Constantes
#define DEBUG true
// Paths
#define PATH_SD "/sd"
#define PATH_HISTORIAL "/sd/HISTORIAL.csv"
#define PATH_PRODUCTOS "/sd/PRODUCTOS.txt"
#define PATH_ULTIMOS_REGISTROS "/sd/ultimos_registros.json"
// Pines
/*
+---------------------------+
--| 3V3 GND |--
--| EN GPIO23 |-- (SDCARD_MOSI)
--| GPIO36 GPIO22 |-- (LCD_PIN_SCL/RTC_PIN_SCL)
(ENCODER_PIN_DT) --| GPIO39 GPIO1 |--
(ENCODER_PIN_CLK) --| GPIO34 GPIO3 |--
(ENCODER_PIN_SW) --| GPIO35 GPIO21 |-- (LCD_PIN_SDA/RTC_PIN_SDA)
(PIN_LED_VERDE) --| GPIO32 GND |--
(PIN_LED_ROJO) --| GPIO33 GPIO19 |-- (SDCARD_MISO)
(PIN_BUZZER) --| GPIO25 GPIO18 |-- (SDCARD_SCK)
(PIN_RELE_CONSUMO) --| GPIO26 GPIO5 |-- (SDCARD_CS)
(PIN_RELE_CN) --| GPIO27 GPIO17 |--
(PIN_RELE_PAT) --| GPIO14 GPIO16 |--
(PIN_RELE_1K) --| GPIO12 GPIO4 |--
--| GND GPIO0 |--
(PIN_RELE_2K5) --| GPIO13 GPIO2 |--
--| GPIO9 GPIO15 |--
--| GPIO10 GPIO8 |--
--| GPIO11 GPIO7 |--
--| 5V0 GPIO6 |--
+---------------------------+
*/
#define PIN_BUZZER 25 // GPIO25
#define PIN_RELE_CONSUMO 26 // GPIO26
#define PIN_LED_VERDE 32 // GPIO32
#define PIN_LED_ROJO 33 // GPIO33
#define PIN_RELE_CN 27 // GPIO27
#define PIN_RELE_PAT 14 // GPIO14
#define PIN_RELE_1K 12 // GPIO12
#define PIN_RELE_2K5 13 // GPIO13
// LCD (I2C)
#define LCD_I2C_ADDR 0x27
#define LCD_NUM_ROWS 4
#define LCD_NUM_COLS 20
#define LCD_PIN_SDA 21 // GPIO21 (SDA)
#define LCD_PIN_SCL 22 // GPIO22 (SCL)
#define LCD_FREQ 400000
// SD Card (SPI)
#define SDCARD_CS 5 // GPIO5 (CS)
#define SDCARD_SCK 18 // GPIO18 (SCK)
#define SDCARD_MOSI 23 // GPIO23 (MOSI)
#define SDCARD_MISO 19 // GPIO19 (MISO)
#define SDCARD_FREQ 10000000
// RTC (I2C) - Puede compartir bus con LCD
#define RTC_PIN_SDA 21 // GPIO21 (SDA)
#define RTC_PIN_SCL 22 // GPIO22 (SCL)
// Encoder
#define ENCODER_PIN_DT 39 // GPIO39 (Input Only)
#define ENCODER_PIN_CLK 34 // GPIO34 (Input Only)
#define ENCODER_PIN_SW 35 // GPIO35
// Otras constantes
#define PZEM_CONS_ADDR 0x01
#define PZEM_MD1_ADDR 0x02
#define PZEM_MD2_ADDR 0x03
#define SETEAR_HORA false
#define NUEVA_HORA {2024,8,6,2,16,56,0}
#define NOMBRES_ENSAYOS_COUNT 5
const char* NOMBRES_ENSAYOS[NOMBRES_ENSAYOS_COUNT] = {"consumo", "fuga", "corte_neutro", "pat", "tension_resistida"};
// Variables Globales
String producto_seleccionado;
JsonArray historialJson;
int idProxEnsayo = 1;
// Funciones para Manejo de LEDs y Buzzer
class LEDManager {
public:
void begin() {
pinMode(PIN_LED_ROJO, OUTPUT);
pinMode(PIN_LED_VERDE, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
setRed(false);
setGreen(false);
buzz(false);
}
void setRed(bool state) { digitalWrite(PIN_LED_ROJO, state ? HIGH : LOW); }
void setGreen(bool state) { digitalWrite(PIN_LED_VERDE, state ? HIGH : LOW); }
void buzz(bool state) { digitalWrite(PIN_BUZZER, state ? HIGH : LOW); }
};
// Funciones para Manejo de LCD
class LCDManager {
public:
LCDManager() : lcd(LCD_I2C_ADDR, LCD_NUM_COLS, LCD_NUM_ROWS) {}
void begin() {
lcd.init();
lcd.backlight();
}
void print(const String &message) {
lcd.clear();
lcd.print(message.c_str());
}
void print(const char *message) {
lcd.clear();
lcd.print(message);
}
// Sobrecarga para manejar ArduinoJson::MemberProxy
template<typename T>
void print(const T& message) {
lcd.clear();
lcd.print(String(message).c_str());
}
void clear() { lcd.clear(); }
void setCursor(int col, int row) { lcd.setCursor(col, row); }
private:
LiquidCrystal_I2C lcd;
};
// Funciones para Manejo de SD Card
class SDManager {
public:
bool begin() { return SD.begin(SDCARD_CS); }
bool testSD() {
File file = SD.open("/test.txt", FILE_WRITE);
if (!file) { return false; }
file.println("Texto de prueba");
file.close();
file = SD.open("/test.txt", FILE_READ);
if (!file) { return false; }
String content = file.readString();
file.close();
SD.remove("/test.txt");
return content == "Texto de prueba\n";
}
};
// Funciones para Manejo de RTC
class RTCManager {
public:
void begin() {
if (!rtc.begin()) {
Serial.println("Couldn't find RTC");
}
if (!rtc.isrunning()) {
Serial.println("RTC is NOT running, let's set the time!");
rtc.adjust(DateTime(__DATE__, __TIME__));
}
}
DateTime now() { return rtc.now(); }
void setTime(DateTime dt) { rtc.adjust(dt); }
private:
RTC_DS1307 rtc;
};
// Funciones para Manejo de Relés
class RelayManager {
public:
RelayManager() {
relayPins[0] = PIN_RELE_CONSUMO;
relayPins[1] = PIN_RELE_CN;
relayPins[2] = PIN_RELE_PAT;
relayPins[3] = PIN_RELE_1K;
relayPins[4] = PIN_RELE_2K5;
}
void begin() {
for (int i = 0; i < 5; i++) {
pinMode(relayPins[i], OUTPUT);
setRelay(i, false);
}
}
void setRelay(int index, bool state) {
if (index >= 0 && index < 5) {
digitalWrite(relayPins[index], state ? HIGH : LOW);
}
}
private:
int relayPins[5];
};
// Funciones para Manejo de PZEM (Placeholder)
class PZEMManager {
public:
PZEMManager(HardwareSerial &serial) {}
void begin() {}
void readAll(float *voltages, float *currents, float *powers) {
// Placeholder para lecturas PZEM
for (int i = 0; i < 3; i++) {
voltages[i] = 230.0 + i;
currents[i] = 5.0 + i;
powers[i] = 1000.0 + i;
}
}
};
// Manejo de los Ensayos
class TestManager {
public:
TestManager(PZEMManager &pzem, LEDManager &led, LCDManager &lcd, RelayManager &relay)
: pzem(pzem), led(led), lcd(lcd), relay(relay) {}
void runConsumoTest() {
lcd.clear();
lcd.print("Ensayando CONSUMO");
// Controlar los relés para este ensayo
controlRelays(0, true);
delay(2500);
// Realizar medición con PZEM
float voltages[3], currents[3], powers[3];
pzem.readAll(voltages, currents, powers);
// Evaluar resultados
bool passed = true;
float i_teorica = 1000 / 220.0;
if (currents[0] > i_teorica * 1.2 || currents[0] < i_teorica * 0.8) {
passed = false;
}
// Mostrar resultado y desactivar relés
showTestResult(0, passed);
controlRelays(0, false);
}
void runFugaTest() {
lcd.clear();
lcd.print("Ensayando FUGA");
controlRelays(1, true);
delay(2500);
float voltages[3], currents[3], powers[3];
pzem.readAll(voltages, currents, powers);
bool passed = true;
float corriente = currents[1] * 10;
if (corriente > 30.0) { passed = false; }
showTestResult(1, passed);
controlRelays(1, false);
}
void runCNTest() {
lcd.clear();
lcd.print("Ensayando CN");
controlRelays(2, true);
delay(2000);
float voltages[3], currents[3], powers[3];
pzem.readAll(voltages, currents, powers);
bool passed = true;
float corriente = currents[1] * 16;
if (corriente > 160.0) { passed = false; }
showTestResult(2, passed);
controlRelays(2, false);
}
void runPATTest() {
lcd.clear();
lcd.print("Ensayando PAT");
controlRelays(3, true);
delay(1500);
float voltages[3], currents[3], powers[3];
pzem.readAll(voltages, currents, powers);
float voltaje = voltages[2];
float corriente = currents[2] * 1000;
float resistencia = voltaje / (corriente + 0.001);
bool passed = resistencia >= 10000;
if (resistencia > 10000) {
delay(1500);
controlRelays(3, true);
pzem.readAll(voltages, currents, powers);
voltaje = voltages[2];
corriente = currents[2] * 1000;
resistencia = voltaje / (corriente + 0.001);
passed = resistencia >= 10000;
}
showTestResult(3, passed);
controlRelays(3, false);
}
void runTRTest() {
lcd.clear();
lcd.print("Ensayando TR");
float v_teorica = producto_seleccionado == "Clase 1" ? 1000 : 2500;
controlRelays(4, true);
delay(2000);
float voltages[3], currents[3], powers[3];
pzem.readAll(voltages, currents, powers);
bool passed = voltages[1] * 10 >= v_teorica * 0.9 && voltages[1] * 10 <= v_teorica * 1.1;
showTestResult(4, passed);
controlRelays(4, false);
}
private:
PZEMManager &pzem;
LEDManager &led;
LCDManager &lcd;
RelayManager &relay;
void showTestResult(int testIndex, bool passed) {
lcd.clear();
lcd.print(passed ? "Test Passed" : "Test Failed");
if (passed) {
led.setGreen(true);
led.setRed(false);
} else {
led.setGreen(false);
led.setRed(true);
}
delay(2000);
led.setGreen(false);
led.setRed(false);
}
void controlRelays(int testIndex, bool close) {
switch (testIndex) {
case 0: // Ensayo de Consumo
relay.setRelay(0, close);
relay.setRelay(1, !close);
relay.setRelay(2, !close);
relay.setRelay(3, !close);
relay.setRelay(4, !close);
break;
case 1: // Ensayo de Fuga
relay.setRelay(0, !close);
relay.setRelay(1, !close);
relay.setRelay(2, !close);
relay.setRelay(3, !close);
relay.setRelay(4, !close);
break;
case 2: // Ensayo de Corte Neutro (CN)
relay.setRelay(0, !close);
relay.setRelay(1, close);
relay.setRelay(2, !close);
relay.setRelay(3, !close);
relay.setRelay(4, !close);
break;
case 3: // Ensayo de PAT
relay.setRelay(0, !close);
relay.setRelay(1, !close);
relay.setRelay(2, close);
relay.setRelay(3, !close);
relay.setRelay(4, !close);
break;
case 4: // Ensayo de Tensión Resistida (TR)
relay.setRelay(0, !close);
relay.setRelay(1, !close);
relay.setRelay(2, !close);
if (producto_seleccionado == "Clase 1") {
relay.setRelay(3, close);
relay.setRelay(4, !close);
} else {
relay.setRelay(3, !close);
relay.setRelay(4, close);
}
break;
}
}
};
// Manejo del Historial
class HistorialManager {
public:
HistorialManager(SDManager &sd, RTCManager &rtc) : sd(sd), rtc(rtc), idProxEnsayo(1) {}
void logTestResult(const char *productName, const char *testName, bool passed) {
JsonObject newEntry = historialJson.createNestedObject();
newEntry["id_ensayo"] = idProxEnsayo++;
newEntry["equipo"] = productName;
newEntry["fecha"] = rtc.now().timestamp();
newEntry[testName] = passed ? "A" : "N";
saveHistorialToFile();
}
void saveHistorialToFile() {
File file = SD.open(PATH_ULTIMOS_REGISTROS, FILE_WRITE);
if (file) {
serializeJson(historialJson, file);
file.close();
}
}
void loadHistorialFromFile() {
File file = SD.open(PATH_ULTIMOS_REGISTROS, FILE_READ);
if (file) {
deserializeJson(historialJson, file);
file.close();
idProxEnsayo = historialJson[0]["id_ensayo"] | 1;
}
}
private:
SDManager &sd;
RTCManager &rtc;
JsonArray historialJson;
int idProxEnsayo;
};
// Manejo de los Menús
class MenuManager {
public:
MenuManager(LCDManager &lcd, TestManager &testManager, std::vector<String>& productos)
: lcd(lcd), testManager(testManager), listaProductos(productos), posMenu(-1), posInd(-1), posHistorial(-1), posProductos(-1), tipoMenu(0) {}
void showMainMenu() {
if (posMenu == -1) {
lcd.clear();
lcd.print("~ENSAYAR TODO");
lcd.setCursor(0, 1);
lcd.print(" ENSAYO INDIVIDUAL");
lcd.setCursor(0, 2);
lcd.print(" HISTORIAL");
lcd.setCursor(0, 3);
lcd.print(" CAMBIAR DE PRODUCTO");
posMenu = 0;
}
refreshMainMenu();
}
void showIndividualMenu() {
if (posInd == -1) {
lcd.clear();
lcd.print("Ensayos Individuales");
lcd.setCursor(0, 1);
lcd.print("~CONSUMO");
lcd.setCursor(0, 2);
lcd.print(" FUGA");
lcd.setCursor(0, 3);
lcd.print(" CORTE NEUTRO");
posInd = 0;
}
refreshIndividualMenu();
}
void showHistorialMenu() {
if (posHistorial == -1) {
lcd.clear();
lcd.print("Historial");
lcd.setCursor(0, 1);
lcd.print("~Atras");
posHistorial = 0;
}
refreshHistorialMenu();
}
void showProductSelectionMenu() {
if (posProductos == -1) {
lcd.clear();
lcd.print("Seleccion de Producto");
lcd.setCursor(0, 1);
lcd.print("~Atras");
posProductos = 0;
}
refreshProductSelectionMenu();
}
void handleButtonPress() {
if (tipoMenu == 0) { // Menú Principal
switch (posMenu) {
case 0: // Ensayar Todo
testManager.runConsumoTest();
testManager.runFugaTest();
testManager.runCNTest();
testManager.runPATTest();
testManager.runTRTest();
break;
case 1: // Ensayo Individual
tipoMenu = 1;
showIndividualMenu();
break;
case 2: // Historial
tipoMenu = 2;
showHistorialMenu();
break;
case 3: // Selección de Producto
tipoMenu = 3;
showProductSelectionMenu();
break;
default:
break;
}
} else if (tipoMenu == 1) { // Ensayos Individuales
switch (posInd) {
case 0:
testManager.runConsumoTest();
break;
case 1:
testManager.runFugaTest();
break;
case 2:
testManager.runCNTest();
break;
case 3:
testManager.runPATTest();
break;
case 4:
testManager.runTRTest();
break;
default:
break;
}
tipoMenu = 0;
showMainMenu();
} else if (tipoMenu == 2) { // Historial
if (posHistorial == 0) {
tipoMenu = 0;
showMainMenu();
} else {
// Mostrar detalles del registro seleccionado
int index = posHistorial - 1;
if (index >= 0 && index < historialJson.size()) {
showHistorialDetail(historialJson[index]);
}
}
} else if (tipoMenu == 3) { // Selección de productos
if (posProductos == 0) {
tipoMenu = 0;
showMainMenu();
} else {
// Seleccionar el producto
int index = posProductos - 1;
if (index >= 0 && index < listaProductos.size()) {
selectProduct(listaProductos[index]);
}
}
}
}
void handleRotaryEncoderInput(int direction) {
if (tipoMenu == 0) { // Menú Principal
posMenu += direction;
if (posMenu < 0) posMenu = 0;
if (posMenu > 3) posMenu = 3;
refreshMainMenu();
} else if (tipoMenu == 1) { // Ensayos Individuales
posInd += direction;
if (posInd < 0) posInd = 0;
if (posInd > 4) posInd = 4;
refreshIndividualMenu();
} else if (tipoMenu == 2) { // Historial
posHistorial += direction;
if (posHistorial < 0) posHistorial = 0;
if (posHistorial > historialJson.size()) posHistorial = historialJson.size();
refreshHistorialMenu();
} else if (tipoMenu == 3) { // Selección de productos
posProductos += direction;
if (posProductos < 0) posProductos = 0;
if (posProductos > listaProductos.size()) posProductos = listaProductos.size();
refreshProductSelectionMenu();
}
}
private:
LCDManager &lcd;
TestManager &testManager;
int posMenu;
int posInd;
int posHistorial;
int posProductos;
int tipoMenu;
std::vector<String>& listaProductos;
void refreshMainMenu() {
lcd.setCursor(0, 0);
lcd.print(posMenu == 0 ? "~" : " ");
lcd.setCursor(0, 1);
lcd.print(posMenu == 1 ? "~" : " ");
lcd.setCursor(0, 2);
lcd.print(posMenu == 2 ? "~" : " ");
lcd.setCursor(0, 3);
lcd.print(posMenu == 3 ? "~" : " ");
}
void refreshIndividualMenu() {
lcd.clear();
lcd.setCursor(0, 1);
lcd.print(posInd == 0 ? "~CONSUMO" : " CONSUMO");
lcd.setCursor(0, 2);
lcd.print(posInd == 1 ? "~FUGA" : " FUGA");
lcd.setCursor(0, 3);
lcd.print(posInd == 2 ? "~CORTE NEUTRO" : " CORTE NEUTRO");
if (posInd >= 3) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(posInd == 3 ? "~PAT" : " PAT");
lcd.setCursor(0, 1);
lcd.print(posInd == 4 ? "~TENSION RESISTIDA" : " TENSION RESISTIDA");
}
}
void refreshHistorialMenu() {
lcd.clear();
if (posHistorial == 0) {
lcd.setCursor(0, 1);
lcd.print("~Atras");
} else {
for (int i = 0; i < 3; i++) {
int index = posHistorial - 1 + i;
if (index < historialJson.size()) {
lcd.setCursor(0, i);
lcd.print(index == posHistorial - 1 ? "~" : " ");
lcd.print(formatHistorialEntry(historialJson[index]));
}
}
}
}
void refreshProductSelectionMenu() {
lcd.clear();
if (posProductos == 0) {
lcd.setCursor(0, 1);
lcd.print("~Atras");
} else {
for (int i = 0; i < 3; i++) {
int index = posProductos - 1 + i;
if (index < listaProductos.size()) {
lcd.setCursor(0, i);
lcd.print(index == posProductos - 1 ? "~" : " ");
lcd.print(formatProductEntry(listaProductos[index]));
}
}
}
}
String formatHistorialEntry(const JsonObject& entry) {
String formatted = String(entry["id_ensayo"].as<const char*>()) + " " + String(entry["equipo"].as<const char*>());
String resultados;
for (int i = 0; i < NOMBRES_ENSAYOS_COUNT; i++) {
resultados += String(entry[NOMBRES_ENSAYOS[i]]["aprueba"]);
}
return formatted + " " + resultados;
}
void showHistorialDetail(const JsonObject& entry) {
lcd.clear();
lcd.print("Ensayo: ");
lcd.print(entry["id_ensayo"]);
lcd.setCursor(0, 1);
lcd.print("Equipo: ");
lcd.print(entry["equipo"]);
lcd.setCursor(0, 2);
lcd.print("Fecha: ");
lcd.print(entry["fecha"]);
lcd.setCursor(0, 3);
for (int i = 0; i < NOMBRES_ENSAYOS_COUNT; i++) {
lcd.print(String(NOMBRES_ENSAYOS[i][0]) + ":");
lcd.print(entry[NOMBRES_ENSAYOS[i]]["aprueba"]);
lcd.print(" ");
}
delay(3000);
showHistorialMenu();
}
String formatProductEntry(const String& product) {
return product;
}
void selectProduct(const String& product) {
producto_seleccionado = product;
lcd.clear();
lcd.print("Producto Seleccionado:");
lcd.setCursor(0, 1);
lcd.print(product);
delay(2000);
tipoMenu = 0;
showMainMenu();
}
};
// Variables Globales
std::vector<String> listaProductos = {"Producto A", "Producto B", "Producto C"};
LEDManager ledManager;
LCDManager lcdManager;
SDManager sdManager;
RTCManager rtcManager;
RelayManager relayManager;
PZEMManager pzemManager(Serial1);
TestManager testManager(pzemManager, ledManager, lcdManager, relayManager);
MenuManager menuManager(lcdManager, testManager, listaProductos);
HistorialManager historialManager(sdManager, rtcManager);
void setup() {
Serial.begin(115200);
ledManager.begin();
lcdManager.begin();
// sdManager.begin();
rtcManager.begin();
// relayManager.begin();
// pzemManager.begin();
// historialManager.loadHistorialFromFile();
// menuManager.showMainMenu();
}
void loop() {
// int direction = 0; // Leer dirección del encoder
// menuManager.handleRotaryEncoderInput(direction);
// bool buttonPressed = false; // Leer estado del botón
// if (buttonPressed) {
// menuManager.handleButtonPress();
// }
// PRUEBA LEDS =====================================================================
// Alternar los LEDs rojo y verde cada 500 ms
ledManager.setRed(true);
ledManager.setGreen(false);
delay(500);
// Apagar el LED rojo y encender el verde
ledManager.setRed(false);
ledManager.setGreen(true);
delay(500);
// PRUEBA LCD ======================================================================
lcdManager.print("Hola Mundo");
delay(2000);
// PRUEBA RTC ======================================================================
// Obtener la hora actual
DateTime now = rtcManager.now();
// Formatear la hora como HH:MM:SS
char buffer[20];
snprintf(buffer, sizeof(buffer), "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
// Mostrar la hora en el LCD
lcdManager.clear();
lcdManager.setCursor(0, 0);
lcdManager.print("Hora actual:");
lcdManager.setCursor(0, 1);
lcdManager.print(buffer);
// Esperar 1 segundo antes de actualizar
delay(1000);
// PRUEBA SDCARD ==================================================================
// Intentar abrir la tarjeta SD
if (!SD.begin(SDCARD_CS)) {
lcdManager.clear();
lcdManager.setCursor(0, 0);
lcdManager.print("Error SD");
Serial.println("Error: No se puede inicializar la tarjeta SD");
delay(2000);
return;
}
// Escribir en un archivo en la tarjeta SD
File testFile = SD.open("/test.txt", FILE_WRITE);
if (testFile) {
testFile.println("Prueba de tarjeta SD exitosa!");
testFile.close();
} else {
lcdManager.clear();
lcdManager.setCursor(0, 0);
lcdManager.print("Error: Escritura");
Serial.println("Error: No se puede abrir test.txt para escribir");
delay(2000);
return;
}
// Leer el archivo desde la tarjeta SD
testFile = SD.open("/test.txt", FILE_READ);
if (testFile) {
String content = testFile.readString();
testFile.close();
// Mostrar el contenido en el monitor serie y en el LCD
Serial.println("Contenido del archivo:");
Serial.println(content);
lcdManager.clear();
lcdManager.setCursor(0, 0);
lcdManager.print("Lectura SD:");
lcdManager.setCursor(0, 1);
lcdManager.print(content.substring(0, 20)); // Muestra solo los primeros 20 caracteres
} else {
lcdManager.clear();
lcdManager.setCursor(0, 0);
lcdManager.print("Error: Lectura");
Serial.println("Error: No se puede abrir test.txt para leer");
}
// Esperar 5 segundos antes de repetir el proceso
delay(5000);
}