/*
Controlador de Aires acondicionados para Shelters
Placa Esp32-s3-devkitc-1
Módulo Ethernet: Basado en W5500
*/
//#include "ethernet.h"
#include "config.h" // <--- Change settings for YOUR network here.
#include "funciones.h"
#include "pantalla.h"
#include "almacenamiento.h"
#include "funciones_estados.h"
#include <Ticker.h>
//#include <unordered_map>
uint8_t estado;
bool marca_seleccionado;
bool modo_auto;
bool equipo_principal; // Si true => AA1, si false => AA2
//bool estado_equipo_principal; // Si true => en_marcha
float temp_de_control; // Temperatura de control. Tomará el valor de AA1_temp_in ó de AA2_temp_in según corresponda.
bool marcha_AA1;
bool marcha_AA2;
bool fan_AA1;
bool fan_AA2;
bool habilitado_AA1;
bool habilitado_AA2;
String rot_equipos;
float setpoint_1;
float histeresis;
float setpoint_2;
uint8_t direccionIP[4];
uint8_t mascara_subnet[4];
uint8_t servidor_dns[4];
uint8_t puerta_enlace[4];
float _contador; // Valor temporal para edición de los parámetros en el menu
uint8_t _contadorIP[4]; // Valor temporal para edición de los parámetros IP en el menu
uint8_t _nbyte_a_ajustar; // Valor temporal del byte a ajustar para edición de los parámetros IP en el menu
bool modo_manual;
// Creo el objeto oled para manejar el display
Pantalla pantalla(SCL_PIN,SDA_PIN);
//OLED oled;
// Declaro el objeto que controla los parámetros del sistema
Parametros params;
// Temporizador para apagar la pantalla
Ticker inactivityTimer;
const unsigned long inactivityTimeout = 30000; // 1/2 minuto
bool botones_en_uso = true; // Va a indicar si se están presionando los botones del display
bool display_encendido = true;
bool delay_active = false;
//#include "display.h"
// Defino la versión del programa
#define VerNum "1.4.5"
/*
uint8_t btn_estado[4];
uint8_t btnPres(uint8_t btn){
uint8_t new_value = digitalRead(button[btn]);
uint8_t resutl = btn_estado[btn] != new_value && new_value == 1;
btn_estado[btn] = new_value;
return resutl;
}
*/
/*
uint8_t btn_estado[4];
uint8_t btnPres(uint8_t btn) {
uint8_t new_value = digitalRead(button[btn]);
uint8_t result = btn_estado[btn] == 0 && new_value == 1;
btn_estado[btn] = new_value;
return result;
}
uint8_t btn_estado[4];
uint8_t btnPres(uint8_t btn) {
uint8_t new_value = digitalRead(button[btn]);
uint8_t result = btn_estado[btn] == HIGH && new_value == LOW;
btn_estado[btn] = new_value;
return result;
}
*/
//uint8_t btn_estado[4];
volatile bool btnBackPressed = false;
volatile bool btnSelectPressed = false;
volatile bool btnUpPressed = false;
volatile bool btnDownPressed = false;
//volatile bool btnUpCheckLongPress = false;
bool btnUpHold = false;
bool btnDownHold = false;
bool btnUpHoldStep = false;
bool btnDownHoldStep = false;
uint8_t btn_up_state = 0;
uint8_t btn_down_state = 0;
uint8_t btn_up_prev_state = 0;
uint8_t btn_down_prev_state = 0;
//volatile bool btnDownCheckLongPress = false;
const uint16_t BTN_MIN_HOLD_DURATION = 1000; // Mínimo intervalo de tiempo para considerar mantenido al botón
const uint16_t DEBOUNCE_DELAY = 350; // Tiempo de debounce en ms
volatile unsigned long current_millis;
volatile unsigned long last_millis = 0;
unsigned long time_init_btn_press = 0; // Memoriza el número de millis en el inicio del conteo
float counter = 0.0;
const uint16_t COUNTER_TIME_STEP = 200;
unsigned long lastCounterTime = 0;
unsigned long lastTempCheckTime = 0;
const uint16_t TEMP_CHECK_INTERVAL = 1000; // Tiempo de chequeo entre mediciones
unsigned long rotation_interval = 0; // Intervalo de rotación en milisegundos
unsigned long last_rotation_time = 0; // Último tiempo de rotación registrado
// volatile uint8_t btn_estado[4] = {HIGH, HIGH, HIGH, HIGH};
void IRAM_ATTR buttonBackInterrupt();
void IRAM_ATTR buttonSelectInterrupt();
void IRAM_ATTR buttonUpInterrupt();
void IRAM_ATTR buttonDownInterrupt();
void checkHoldButtons();
//void IRAM_ATTR buttonUpCheckLongPressInterrupt();
//void IRAM_ATTR buttonDownCheckLongPressInterrupt();
//uint8_t btnPres(uint8_t btn);
void reiniciarInactividad();
void apagarPantalla();
void encenderPantalla();
//void delayStable();
void setup() {
//delay(2000);
Serial.begin(115200);
current_millis = millis();
// Inicializo el espacio de nombres del objeto de la clase Parametros, se inicializa dentro de la función
// setup() porque sino me da problemas con la inicialización del espacio de nombre de la partición NVS
params.begin();
Serial.println("Hello, ESP32-S3!");
// Borro todos los parámetros del sistema (todo el espacio de nombres)
//params.systemParamsReset();
//params.netParamsReset();
// Chequeo si es la primera vez que se corre el programa entonces seteo los parámetros por defecto
if(!params.existeClave("seteo_inicial")) {
Serial.println("--- Seteo los parámetros por defecto ---");
params.defaultToSystemParams();
params.defaultToNetParams();
}
// Mostrar los valores de los parámetros en el puerto serial
params.showSystemParams();
params.showNetParams();
// Inicializo las salidas
for (uint8_t i = 0; i < NUM_SALIDAS; i++) {
pinMode(salidas[i], OUTPUT); // Configura los pines de salidas
}
// Inicializo las entradas analógicas
for (int i = 0; i < NUM_ENTRADAS_ANAL; i++) {
pinMode(entradas_anal[i], INPUT);
}
// Inicializo los botones y el arreglo btn_estado[] en HIGH para poner el estado inicial de los botones como no presionado
// pinMode(button[BTN_BACK], INPUT_PULLUP);
// pinMode(button[BTN_SELECT], INPUT_PULLUP);
// pinMode(button[BTN_UP], INPUT_PULLUP);
// pinMode(button[BTN_DOWN], INPUT_PULLUP);
pinMode(button[BTN_BACK], INPUT_PULLUP);
pinMode(button[BTN_SELECT], INPUT_PULLUP);
pinMode(button[BTN_UP], INPUT_PULLUP);
pinMode(button[BTN_DOWN], INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(button[BTN_BACK]), buttonBackInterrupt, RISING);
attachInterrupt(digitalPinToInterrupt(button[BTN_SELECT]), buttonSelectInterrupt, RISING);
attachInterrupt(digitalPinToInterrupt(button[BTN_UP]), buttonUpInterrupt, RISING);
attachInterrupt(digitalPinToInterrupt(button[BTN_DOWN]), buttonDownInterrupt, RISING);
//attachInterrupt(digitalPinToInterrupt(button[BTN_BACK]), buttonBackInterrupt, RISING);
//attachInterrupt(digitalPinToInterrupt(button[BTN_SELECT]), buttonSelectInterrupt, RISING);
//attachInterrupt(digitalPinToInterrupt(button[BTN_UP]), buttonUpInterrupt, RISING);
//attachInterrupt(digitalPinToInterrupt(button[BTN_DOWN]), buttonDownInterrupt, RISING);
// btn_estado[0] = btn_estado[1] = btn_estado[2] = btn_estado[3] = HIGH;
// Inicializo las variables que almacenarán los valores de los parámetros y si al momento tiene marca de selección
modo_auto = params.obtenerModoAuto();
p_estado[AUTO_ON] = modo_auto;
p_estado[AUTO_OFF] = !modo_auto;
marcha_AA1 = false; // *** Ver si hay que dejarlo solo como variable (como está acá), pero sacarlo de almacenamiento como parámetro
p_estado[AA1_ON] = marcha_AA1;
p_estado[AA1_OFF] = !marcha_AA1;
marcha_AA2 = false; // *** Ver si hay que dejarlo solo como variable (como está acá), pero sacarlo de almacenamiento como parámetro
p_estado[AA2_ON] = marcha_AA2;
p_estado[AA2_OFF] = !marcha_AA2;
Serial.printf("setup::p_estado AA1_ON: %d, - AA1_OFF: %d, - AA2_ON: %d, - AA2_OFF: %d\n", p_estado[AA1_ON], p_estado[AA1_OFF], p_estado[AA2_ON], p_estado[AA2_OFF]);
habilitado_AA1 = params.obtenerHabilitadoAA1();
habilitado_AA2 = params.obtenerHabilitadoAA2();
p_estado[HAB_TODOS] = (habilitado_AA1 && habilitado_AA2) ? true : false;
p_estado[SOLO_AA1] = (habilitado_AA1 && !habilitado_AA2) ? true : false;
p_estado[SOLO_AA2] = (!habilitado_AA1 && habilitado_AA2) ? true : false;
rot_equipos = params.obtenerRotacionEquipos();
p_estado[ROT_DIARIA] = (rot_equipos == "DIARIA") ? true : false;
p_estado[ROT_SEMANAL] = (rot_equipos == "SEMANAL") ? true : false;
p_estado[ROT_POR_CICLO] = (rot_equipos == "POR_CICLO") ? true : false;
setIntervaloRotacion(); // Seteo el intervalo de rotación
Serial.printf("setup::rot_equipos: %s - p_estado[ROT_DIARIA]: %u - p_estado[ROT_SEMANAL]: %u - p_estado[ROT_POR_CICLO]: %u\n", rot_equipos, p_estado[ROT_DIARIA], p_estado[ROT_SEMANAL], p_estado[ROT_POR_CICLO]);
setpoint_1 = params.obtenerSetPoint1();
histeresis = params.obtenerHisteresis();
setpoint_2 = params.obtenerSetPoint2();
// Defino el equipo principal
equipo_principal = true; // Si true => AA1, si false => AA2
// temp_de_control
params.obtenerIP("direccion_ip", direccionIP);
params.obtenerIP("mascara_subnet", mascara_subnet);
params.obtenerIP("servidor_dns", servidor_dns);
params.obtenerIP("puerta_enlace", puerta_enlace);
modo_manual = false; // Se utiliza para hacer test de funcionamiento de los equipos de aire
// Defino la variable que va a indicar cuando hay que marcar en el display el parámetro actualmente vigente
marca_seleccionado = false;
// inicializo el display
pantalla.begin();
Serial.print("AireControl SRL - Version ");
Serial.println(VerNum);
char version[20];
snprintf(version, sizeof(version), "Version: %s", VerNum);
pantalla.clearDisplay();
pantalla.mostrarMsg("AireControl SRL", version);
//pantalla.mostrarMenu("AireControl SRL", version, marca_seleccionado);
//delay(2000);
// *** INTERCALAR LO DEL NTP PARA EL SETUP CUANDO LO DEMÁS ESTÉ LISTO ***
// Estado inicial
estado = VISUALIZACION;
// Dibujo el menu principal
pantalla.clearDisplay();
pantalla.mostrarHeader(modo_auto,true, true);
pantalla.mostrarMenu(tablaTransiciones[estado].menu_raiz, tablaTransiciones[estado].menu_elemento, marca_seleccionado);
Serial.printf("Menu por defecto: %d\n", estado);
// Configuración inicial del temporizador de inactividad de la pantalla
//inactivityTimer.attach_ms(inactivityTimeout, apagarPantalla); // Iniciar el temporizador de inactividad
}
void loop() {
// Ejecuto una vez la lectura de los botones y lo muestro por el serial
static bool primer_lectura_BTN = false;
if(!primer_lectura_BTN){
// Serial.printf("BTN_DOWN: %d - BTN_UP: %d - BTN_SELECT: %d - BTN_BACK: %d\n", btnPres(BTN_DOWN), btnPres(BTN_UP), btnPres(BTN_SELECT), btnPres(BTN_BACK));
Serial.printf("BTN_DOWN: %d - BTN_UP: %d - BTN_SELECT: %d - BTN_BACK: %d\n", btnDownPressed, btnUpPressed, btnSelectPressed, btnBackPressed);
Serial.printf("p_estado AA1_ON: %d, - AA1_OFF: %d, - AA2_ON: %d, - AA2_OFF: %d\n", p_estado[AA1_ON], p_estado[AA1_OFF], p_estado[AA2_ON], p_estado[AA2_OFF]);
primer_lectura_BTN = true;
}
current_millis = millis();
// **** INTERCALAR CON LO DEL NTP PARA EL LOOP CUANDO LO DEMÁS ESTÉ LISTO ****
// delay(10); // this speeds up the simulation
// **** MEDICION DE LAS ENTRADAS ANALOGICAS Cant: 5 ****
// Leo las temperaturas: AA1_temp_in, AA1_temp_out, AA2_temp_in, AA2_temp_out y temp_ambiente
// Las temperaturas: AA1_temp_in y AA2_temp_in son las que controlan los equipos
if (current_millis - lastTempCheckTime >= TEMP_CHECK_INTERVAL) {
leerTemperaturas();
lastTempCheckTime = current_millis;
}
// **** MEDICION DE LAS ENTRADAS DIGITALES Cant: 6 ****
// Alarmas equipos
// Falta de fase
// **** ESTADO DE SALIDAS DIGITALES Cant: 5 ****
digitalWrite(salidas[AA1_COMP], marcha_AA1); // Comando compresor AA1
digitalWrite(salidas[AA1_FAN], fan_AA1); // Comando turbina AA1
//if (marcha_AA1) {digitalWrite(salidas[AA1_FAN], HIGH);} // Comando turbina AA1
digitalWrite(salidas[AA2_COMP], marcha_AA2); // Comando compresor AA2
digitalWrite(salidas[AA2_FAN], fan_AA2); // Comando turbina AA2
//if (marcha_AA2) {digitalWrite(salidas[AA2_FAN], HIGH);} // Comando turbina AA2
// Falla general (activa cuando: alarma de equipos/falta de fase, falla sensores temp., y alarma de temp. en sitio).
// **** MODO AUTO ****
if (modo_auto) {
// La temperatura de control que comanda el arranque o la parada es la que marca el equipo principal
if (equipo_principal) {
//Serial.println("-- modo_auto::Controla AA1_temp_in");
temp_de_control = AA1_temp_in;
} else {
//Serial.println("-- modo_auto::Controla AA2_temp_in");
temp_de_control = AA2_temp_in;
}
// Comprobar si no rota por ciclo y si ha pasado suficiente tiempo para rotar => Rotar
if (!p_estado[ROT_POR_CICLO] && (current_millis - last_rotation_time >= rotation_interval)) {
Serial.println("modo_auto:: Rotación por intervalo de tiempo - Debo rotar");
// Rotar equipos
equipo_principal = !equipo_principal; // Alternar entre equipos principales
last_rotation_time = current_millis; // Actualizar el último tiempo de rotación
Serial.printf("-- modo_auto::equipo_principal: %u - rotation_interval: %u\n", equipo_principal, rotation_interval);
}
// Si ambos equipos apagados Y temp >= (SP1 + H/2) => Enciendo equipo principal
if ((!marcha_AA1 && !marcha_AA2) && (temp_de_control >= setpoint_1 + histeresis/2)) {
Serial.println("modo_auto:: - Enciendo equipo principal");
Serial.printf("modo_auto::temp_de_control: %.2f - AA1_temp_in: %.2f - AA1_temp_out: %.2f - AA2_temp_in: %.2f - AA2_temp_out: %.2f - temp_ambiente: %.2f\n", temp_de_control, AA1_temp_in, AA1_temp_out, AA2_temp_in, AA2_temp_out, temp_de_control);
arranqueEquipo(true);
}
// Si (marcha_AA1 XOR marcha_AA2 está encendido) Y temp >= (SP2) => Enciendo equipo stand-by
if ((marcha_AA1 ^ marcha_AA2) && (temp_de_control >= setpoint_2)) {
Serial.println("modo_auto - Enciendo equipo secundario");
Serial.printf("modo_auto::temp_de_control: %.2f - AA1_temp_in: %.2f - AA1_temp_out: %.2f - AA2_temp_in: %.2f - AA2_temp_out: %.2f - temp_ambiente: %.2f\n", temp_de_control, AA1_temp_in, AA1_temp_out, AA2_temp_in, AA2_temp_out, temp_de_control);
arranqueApoyo();
}
// Si (marcha_AA1 ó marcha_AA1 están encendidos) Y temp <= (SP1 - H/2) => Apago ambos equipos
if ((marcha_AA1 || marcha_AA2) && (temp_de_control <= setpoint_1 - histeresis/2)) {
Serial.println("modo_auto - Apago ambos equipos");
Serial.printf("modo_auto::temp_de_control: %.2f - AA1_temp_in: %.2f - AA1_temp_out: %.2f - AA2_temp_in: %.2f - AA2_temp_out: %.2f - temp_ambiente: %.2f\n", temp_de_control, AA1_temp_in, AA1_temp_out, AA2_temp_in, AA2_temp_out, temp_de_control);
arranqueEquipo(false);
// equipoSecundario(false);
}
}
// Contemplar situación particular en que el equipo enciende por alta temperatura y nunca apaga porque
// no logra bajarla pero la misma no llega a ser tan alta como para disparar el segundo setpoint.
// Ver donde y como apago los FANs
// Apago los ventiladores
//fan_AA1 = false;
//fan_AA2 = false;
// **** REGISTRO DE FALLAS HISTÓRICAS ****
// Mientras no esté activo el retardo de bloqueo de los botones evalúo si hay alguno presionado
if (!delay_active && display_encendido){
// Obtener la transición correspondiente al estado actual
//Transicion transicion = tablaTransiciones[estado];
if(btnDownPressed && !btnDownHold){
// Cambio el estado y luego ejecuto la acción de la transición si la tuviera.
Serial.printf("*** BTN_DOWN::Estado actual: %d ***\n", estado);
estado = tablaTransiciones[estado].sig_estado;
ejecutarTransicion(BTN_DOWN);
Serial.printf("*** BTN_DOWN::Nuevo estado: %d ***\n", estado);
btnDownPressed = false;
} else if (btnUpPressed && !btnUpHold){
Serial.printf("*** BTN_UP::Estado actual: %d ***\n", estado);
estado = tablaTransiciones[estado].ant_estado;
ejecutarTransicion(BTN_UP);
Serial.printf("*** BTN_UP::Nuevo estado: %d ***\n", estado);
btnUpPressed = false;
} else if(btnSelectPressed){
Serial.printf("*** BTN_SELECT::Estado actual: %d ***\n", estado);
// Llamo a la función ejecutarTransición. En el caso del BTN_SELECT, el cambio de estado se evaluará allí.
ejecutarTransicion(BTN_SELECT);
Serial.printf("*** BTN_SELECT::Nuevo estado: %d ***\n", estado);
btnSelectPressed = false;
} else if(btnBackPressed){
Serial.printf("*** BTN_BACK::Estado actual: %d ***\n", estado);
estado = tablaTransiciones[estado].atras_estado;
ejecutarTransicion(BTN_BACK);
Serial.printf("*** BTN_BACK::Nuevo estado: %d ***\n", estado);
btnBackPressed = false;
} else if(!btnDownPressed && btnDownHold && btnDownHoldStep){
Serial.printf("*** BTN_DOWN HOLD::Estado actual: %d ***\n", estado);
ejecutarTransicion(BTN_DOWN);
btnDownHold = false;
btnDownHoldStep = false;
Serial.printf("*** BTN_DOWN HOLD::Nuevo estado: %d ***\n", estado);
} else if(!btnUpPressed && btnUpHold && btnUpHoldStep){
Serial.printf("*** BTN_UP HOLD::Estado actual: %d ***\n", estado);
ejecutarTransicion(BTN_UP);
btnUpHold = false;
btnUpHoldStep = false;
Serial.printf("*** BTN_UP HOLD::Nuevo estado: %d ***\n", estado);
}
checkHoldButtons();
}
// Si está en modo visualización muestro las temperaturas
if(estado == VISUALIZACION){
}
// Si el display está apagado y se presionó algún botón, llamo a la función reiniciarInactividad
if (!display_encendido){
if(btnDownPressed || btnUpPressed || btnSelectPressed || btnBackPressed) {
btnBackPressed = false;
btnSelectPressed = false;
btnUpPressed = false;
btnDownPressed = false;
reiniciarInactividad(); // Se presionó un botón, reiniciar el temporizador de inactividad
}
// Si el display está encendido y la variable botones_en_uso es true, llamo a la función reiniciarInactividad
} else if (display_encendido && botones_en_uso){
reiniciarInactividad(); // Se presionó un botón, reiniciar el temporizador de inactividad
}
// Ya evalué si los botones están en uso, lo paso a false
botones_en_uso = false;
/* btnBackPressed = false;
btnSelectPressed = false;
btnUpPressed = false;
btnDownPressed = false;
*/
}
void IRAM_ATTR buttonBackInterrupt() {
current_millis = millis();
if (current_millis - last_millis >= DEBOUNCE_DELAY) {
btnBackPressed = true;
last_millis = current_millis; // Establecer el inicio de la pulsación
}
}
void IRAM_ATTR buttonSelectInterrupt() {
current_millis = millis();
if (current_millis - last_millis >= DEBOUNCE_DELAY) {
btnSelectPressed = true;
last_millis = current_millis; // Establecer el inicio de la pulsación
}
}
void IRAM_ATTR buttonUpInterrupt() {
current_millis = millis();
if (current_millis - last_millis >= DEBOUNCE_DELAY) {
if(btnUpHold) {
btnUpPressed = false;
btnUpHold = false;
} else {
btnUpPressed = true;
}
last_millis = current_millis; // Establecer el inicio de la pulsación
}
}
void IRAM_ATTR buttonDownInterrupt() {
current_millis = millis();
if (current_millis - last_millis >= DEBOUNCE_DELAY) {
if(btnDownHold) {
btnDownPressed = false;
btnDownHold = false;
} else {
btnDownPressed = true;
}
last_millis = current_millis; // Establecer el inicio de la pulsación
}
}
void checkHoldButtons() {
current_millis = millis();
// Read the digital value of the button (LOW/HIGH)
uint8_t btn_down_state = digitalRead(button[BTN_DOWN]) == LOW;
uint8_t btn_up_state = digitalRead(button[BTN_UP]) == LOW;
//empiezo a contar el tiempo
if ((!btn_down_prev_state && btn_down_state) || (!btn_up_prev_state && btn_up_state)) {
Serial.println("checkHoldButtons::Permutación estado botones - Empiezo a contar el tiempo");
time_init_btn_press = millis();
}
if (btn_down_prev_state && btn_down_state && current_millis - time_init_btn_press >= BTN_MIN_HOLD_DURATION) {
//Serial.println("checkHoldButtons::Botón Mantenido !!");
btnDownHold = true;
if(current_millis - lastCounterTime >= COUNTER_TIME_STEP){
btnDownHoldStep = true;
Serial.print("_contador: ");
Serial.print(_contador);
Serial.flush(); // Asegura que los datos se muestran inmediatamente
Serial.print("\r"); // Carrera de carro para sobrescribir la línea
lastCounterTime = current_millis;
}
} else if (btn_up_prev_state && btn_up_state && current_millis - time_init_btn_press >= BTN_MIN_HOLD_DURATION) {
//Serial.println("checkHoldButtons::Botón Mantenido !!");
btnUpHold = true;
if(current_millis - lastCounterTime >= COUNTER_TIME_STEP){
btnUpHoldStep = true;
Serial.print("_contador: ");
Serial.print(_contador);
Serial.flush(); // Asegura que los datos se muestran inmediatamente
Serial.print("\r"); // Carrera de carro para sobrescribir la línea
lastCounterTime = current_millis;
}
}
btn_down_prev_state = btn_down_state;
btn_up_prev_state = btn_up_state;
}
void reiniciarInactividad() {
Serial.println("reiniciarInactividad::Paso por la función");
inactivityTimer.detach(); // Detener el temporizador para evitar llamadas múltiples
if (!display_encendido){encenderPantalla();} // Si la pantalla está apagada encenderla
inactivityTimer.attach_ms(inactivityTimeout, apagarPantalla); // Volver a iniciar el temporizador
}
void apagarPantalla(){
Serial.println("apagarPantalla");
pantalla.pantallaOnOFF(1);
display_encendido = false;
// Con la pantalla apagada, apago el timer
inactivityTimer.detach();
}
void encenderPantalla(){
Serial.println("encenderPantalla");
pantalla.pantallaOnOFF(0);
display_encendido = true;
}