12 ago 2016

Control centralizado del sistema mixto Solar-Biomasa mediante placa Arduino y TFT Shield


En esta entrada voy a compartir con vosotros las mejoras que he realizado al sistema mixto solar-biomasa, que consiste básicamente en cambiar el obsoleto sistema de control por termostatos con un sistema centralizado basado en Arduino totalmente personalizable, flexible y ampliable; veremos su montaje, componentes y código fuente.
Actualización 20/10/16: He tenido problemas con la detección de la temperatura de biomasa, y he agregado otro sensor en la parte baja, ya que curiosamente a veces alcanza antes en esa zona la temperatura de ebullición y se soltaba la conexión de polibutileno. Veréis en el código las modificaciones.

Materiales utilizados:

   1- Arduino MEGA 2560 (en concreto he usado un clon Funduino), ya que el TFT shield no me deja suficientes entradas/salidas libres en el UNO R3.
   2- Pantalla táctil TFT de 2,4" McuFriend, que nos dará gran libertad a la hora de mostrar la información en pantalla. Las librerías que mejor me han funcionado son las de BUHOSOFT; aquí podéis descargar la versión que yo he usado.
   3 - Relés para activar electroválvulas; con unos alimentados a 5V que pueden manejar 230V y 10A nos vale de sobra. Para no estresar el regulador de la placa arduino, los alimento independientemente con otro regulador L7805 (maneja entre 7-30V y 1,5A máx). Al ser tan bajo el consumo, ni necesitamos un disipador:


   - Un altavoz-zumbador para la alarma de aviso en caso de problemas, que funciona entre 7 y 12V con 100dB, suficiente.
   - Un relé SSR DC-DC para activar el zumbador; tan sólo consume entre 3 y 25mA por lo que podemos activarlo directamente con el pin Arduino. Empecé usando uno de este tipo por agilizarlo pero lo ideal es usar un simple transistor de 500 mA para activarlo.

- Otro relé estado sólido AC-DC para activar el motor de al menos 10A, será el elemento que más sufra las contínuas activaciones, por lo que la durabilidad y fiabilidad del relé de estado sólido asegurará mucho tiempo sin mantenimiento. Le he añadido un disipador por si se calienta, aunque me he excedido en su tamaño como luego he comprobado, pero mejor pecar por exceso ;).

   - 4 sensores NTC 6K8 (a 25º su resistencia son 6K8 ohm), además de 4 resistencias 6K8 necesarias, obtenidos de unas baterías viejas, que mide temperaturas entre -25 y 125ºC, ideal para las temperaturas que se manejarán:

Este tipo de sensores son fáciles de obtener y configurar cogiendo temperaturas y resistencia en dos puntos (como expliqué en la entrada anterior); éstos se pueden obtener de baterías de litio y NI-MH grandes; los llevan para regular su temperatura dentro de unos márgenes seguros, pegados a las mismas. También los tenemos en lotes muy económicos. Aseguraos que es del tipo que tenéis contemplado en la programación; en mi caso tipo NTC, (Negative Temp. Coef.); que indica subida de temp. al bajar su resistencia.
He preferido utilizar este tipo de sensores por precio y comodidad más que nada, en vez de los habituales transistores DS18B20, muy comunes también.
Tened en cuenta que no sirven para ambientes muy húmedos; el agua es conductora y haría subir la temperatura indicada erróneamente. Para evitar posibles problemas con la lluvia, escapes de agua, etc se pueden aislar con "silicona líquida" de una termoencoladora o con silicona normal, como he hecho yo.

Montando los elementos y depurando el programa

Con una tarjeta de prototipos conecto las resistencias y los sensores, y voy probando los datos en pantalla y con el feedback por serie vamos depurando los errores.
(Para aquellos que lo necesiten, en cuanto pueda prepararé el esquema con el Fritzing para aclarar las conexiones).


Tras un par de días funcionando correctamente, conecto los elementos de forma definitiva:


Para simplificar he soldado directamente las resistencias 6K8 de cada sensor (pines analógicos 11 a 14) detrás de la placa a masa/negativo:


En el vídeo siguiente pruebo superficialmente el funcionamiento; cuando esté en marcha haremos los últimos ajustes:


Sustituyendo el viejo sistema

Lo primero es identificar cada cable; de la configuración anterior no lo estaban, y me hubiera ahorrado tiempo; entre sensores, alimentación, elecroválvulas y motor, son unos cuantos cables y no puede haber errores:


Aprovechamos la misma caja, dejando la pantalla en el centro agrandando un poco la ventana, la alarma en la superior:


Repartí todo por la caja, que tenía profundidad de sobra para todos los elementos:


En la siguiente foto no se ve, pero dejé conectado a la placa un cable USB colgando por fuera para las posibles actualizaciones y depuraciones finales del programa:



Sustituir los sensores de temperatura

Los que tenía colocados podrían haber servido, pero dado que los termostatos son compatibles con un rango limitado de sensores, preferí reservarlos al retirarlos.
Para reemplazar el del depósito, insertamos el nuevo en su lugar; el depósito tiene un alojamiento de 6 mm. x 50 cms de largo donde se introduce el sensor hasta la zona media del depósito; el nuevo sensor se introdujo sin dificultad, más fino que el antiguo que se ve en la imagen:


Cambiamos también el del captador. He podido comprobar que tras 7 años de uso, se mantiene en buen estado, sin apenas haber blanqueado el policarbonato, y gracias a las altas temperaturas mantiene a raya cualquier musgo o planta que quiera desarrollarse:


He aprovechado para cambiar la junta del purgador, que ya estaba muy corroída.



En la imagen siguiente ya hemos colocado el sensor en la parte alta de salida del agua caliente del captador solar (más detalles en las entradas de su construcción), con cinta aislante blanca y varias bridas. Después colocaré silicona blanca por encima para aislarlo mejor del exterior y que no falsee las temperaturas en invierno, manteniendo su temperatura lo más cercana a la real del agua del tubo.


El programa es el siguiente (ahora en Github para que tengáis las diferentes versiones) (lo he ido mejorando desde que publiqué la entrada, como se indica al principio del código) (última actu en 22/02/19):

/*
 * ARCHIVO: Control sistema térmico mixto biomasa-solar
 * AUTOR: David Losada (Modificado por Gemini AI 3, corregido puntos 4-7 por Claude Sonnet 4.6)
 * FECHA: 18/02/2026
 * URL: http://miqueridopinwino.blogspot.com.es/
 * Versión 2.1 (Optimized Edition - Fix 4-7)
 *
 * MEJORAS v2.1:
 * - Cambio de variable de tiempo a 'unsigned long' para evitar desbordamiento y pérdida de precisión.
 * - Optimización de EEPROM: Solo escribe si el valor ha cambiado (Wear Leveling).
 * - Eliminación de parpadeo en pantalla (refresco inteligente).
 * - Corrección matemática de kWh (Factor 0.29 vs Factor 23).
 * - Gestión correcta de Watchdog en pausas largas.
 *
 * CORRECCIONES v2.1-fix:
 * - [Fix4] Condición reset de timers cambiada de >= a > para evitar reset espurio al arranque del motor.
 * - [Fix5] Condición de refresco inicial cambiada de millis()<8000 a segundos<10 (fiable tras sleep).
 * - [Fix6] Orden de operaciones en cálculo de 'duracion' corregido para evitar desbordamiento.
 * - [Fix7] Eliminado #define pellets 5 (índice fuera de rango, nunca usado).
 */

#define Copyright "Copyright 2016-26 Ringmaster v2.1"

//Librerías
#include <avr/sleep.h>
#include <avr/power.h>
#include <avr/wdt.h>
#include <pin_magic.h>
#include <pin_magic_MEGA.h>
#include <pin_magic_UNO.h>
#include <registers.h>
#include <TFTlcd.h>
#include <TouchScreen.h>

#if defined(__SAM3X8E__)
    #undef __FlashStringHelper::F(string_literal)
    #define F(string_literal) string_literal
#endif

#include <Adafruit_GFX.h>
#include <EEPROMex.h>
#include <Time.h> 

// DEFINICIÓN PINES PANTALLA
#define LCD_CS A3
#define LCD_CD A2
#define LCD_WR A1
#define LCD_RD A0
#define LCD_RESET A4

#define YP A1 
#define XM A2 
#define YM 7 
#define XP 6 

#define TS_MINX 150
#define TS_MINY 120
#define TS_MAXX 920
#define TS_MAXY 940

TouchScreen ts = TouchScreen(XP, YP, XM, YM, 326);

// COLORES
#define BLACK   0x0000
#define BLUE    0x001F
#define RED     0xF800
#define GREEN   0x07E0
#define CYAN    0x07FF
#define MAGENTA 0xF81F
#define YELLOW  0xFFE0
#define WHITE   0xFFFF
#define ROZ     0xFBE0
#define GRI     0xBDF7

Adafruit_TFTLCD tft(LCD_CS, LCD_CD, LCD_WR, LCD_RD, LCD_RESET);

// ***************** PARÁMETROS CONFIGURABLES ***********************
const long B = 3850; 
const int resistor = 6800; 
const float voltage = 5.01; // Nota: Idealmente medir Vcc real periódicamente

const byte DtFsolar = 10; 
const byte Dtosolar = 5; 
const byte DtFbiomasa = 12; 
const byte Dtobiomasa = 4; 
const byte DtFEnfriar = 47; 
const byte DtoEnfriar = 40; 
const byte TempDesvio = 60; 

const int frecseg = 1; 
const byte difTemp = 40; 
const double ltsdeposito = 250; // Litros reales del depósito
const int minTimeMotor = 180; 

#define ledPIN 13 
#define motorPIN 25 
#define valvbioPIN 27 
#define valvenfriaPIN 29 
#define zumbadorPIN 31 
#define primerSensor 11 
#define numeroSensores 5 

float calibTemps[numeroSensores] = {0,0,0,0,0}; 
int horasValvEnfria=120; // 120 ciclos de 3 horas = 15 días aprox

// ***************** VARIABLES ***********************
double Msensores[numeroSensores] = {0,0,0,0,0}; 
double Mtempsens[numeroSensores] = {0,0,0,0,0}; 
double MtempAnt[numeroSensores] = {0,0,0,0,0}; 
const char* Mnombres[]={"Captador:","Biomasa1:","Biomasa2:","Retorno: ","Deposito:"}; 
String TextoAnt;

#define captador 0 
#define biomasa1 1
#define biomasa2 2
#define retorno  3
#define deposito 4
// [Fix7] Eliminado: #define pellets 5
// 'pellets' era índice 5, fuera del rango del array Mtempsens[5] (índices 0-4) y nunca se usaba.

boolean motorON=false; 
boolean valvula=false; 
boolean tresvias=false; 

long Mdatos[6] = {0,0,0,0,0,0}; 
const char* MnomDatos[]={"Hrs motor ON","Hrs Arduino","CTR RELE Motor","CTR VALVULA Fuego","CTR VALVULA Enfriar","KWh RENOVABLES"}; 
#define hrsMotorOn 0 
#define hrsArduino 1
#define ctrReleMotor 2
#define ctrValvulaFuego 3
#define ctrValvulaEnfriar 4
#define ctrKWh 5

double tempPrevia=0; 
double kwhAhorro=0; 
float kwhParcialAcumulado = 0.0; // Acumulador de decimales de energía

// CÁLCULO FÍSICO REAL: 
// 1 Kcal calienta 1kg de agua 1ºC. 1kWh = 860 Kcal.
// Factor: 250 litros * (1/860) = ~0.29 kWh por cada grado que sube el depósito completo.
const double kwhGrado =  ltsdeposito * (1.0/860.0); 

int activarValvEnfria=0; 
const float K= 273.15; 
const float e = 2.718281828459045; 
const float unodivr = 1/(resistor * pow(e,(-B/298.15))); 

double T = 0; 
int grados, decimas; 

// *** MEJORA: Cambio a unsigned long para evitar errores de tiempo a largo plazo ***
unsigned long segundos = 0; 
unsigned long millisAnterior = 0; 
unsigned long segundosArduino = 0; 
unsigned long segundosRefresca = 0; 
unsigned long segundosInicioMotor = 0; 
unsigned long restoSegundosMotor = 0; 
unsigned long timeLED = 0; 
unsigned long segundosValvula = 0; 

byte error=0; 
int direccionInicial; 
// Número mágico para detectar versión de EEPROM y migrar datos
// V1 = 4011983 (Antigua con error)
// V2 = 4011985 (Nueva corregida)
const long MAGIC_NUMBER = 4011985; 

void setup() {
  Serial.begin(9600);
  Serial.println("Arduino Working OK - Version 2.1-fix");

  // WATCHDOG SETUP (2 Segundos)
  MCUSR &= ~(1<<WDRF);
  WDTCSR |= (1<<WDCE) | (1<<WDE);
  WDTCSR = 1<<WDP2 | 1<<WDP1 | 1<<WDP0; 
  WDTCSR |= _BV(WDIE);
  Serial.println("Watchdog Configurado.");

  Serial.print("TFT size is "); Serial.print(tft.width()); Serial.print("x"); Serial.println(tft.height());
  tft.reset();
  uint16_t identifier = tft.readID();
  // Forzamos identificador común si falla detección
  if(identifier == 0x0154 || identifier == 0x9325 || identifier == 0x9328 || identifier == 0x7575 || identifier == 0x9341 || identifier == 0x8357) {
      // ID válido detectado
  } else {
      identifier = 0x9341; 
  }

  tft.begin(identifier);
  tft.fillRect(0, 0, 320, 240, BLACK);
  tft.setRotation(1);
  tft.setTextSize(2);
  tft.print("    Arduino MEGA 2560");
  tft.setCursor(90, 220);
  tft.println(Copyright);
  
  // Pequeña pausa segura
  for(int i=0; i<20; i++) { delay(100); wdt_reset(); }

  EEPROM.setMemPool(20, EEPROMSizeMega);
  EEPROM.setMaxAllowedWrites(10000);
  direccionInicial = EEPROM.getAddress(4); 

  // --- LÓGICA DE MIGRACIÓN Y SANACIÓN DE DATOS ---
  long magicNumberLeido = EEPROM.readLong(direccionInicial+45);

  if (magicNumberLeido != MAGIC_NUMBER) { 
      Serial.println("Detectada version antigua o primer uso. Ejecutando migracion...");
      
      // Si venimos de la versión corrupta (el número mágico antiguo)
      if (magicNumberLeido == 4011983) {
          long valorCorrupto = EEPROM.readLong(direccionInicial+(ctrKWh*4));
          // Dividimos por 23 para obtener el valor real acumulado histórico
          long valorCorregido = valorCorrupto / 23;
          EEPROM.writeLong(direccionInicial+(ctrKWh*4), valorCorregido);
          Serial.print("KWh Corregidos de "); Serial.print(valorCorrupto); 
          Serial.print(" a "); Serial.println(valorCorregido);
      } else if (magicNumberLeido != 4011984) { 
          // Si no es la v1 ni la v2.0 (es virgen o basura), reseteamos todo a 0
          for(int i=0; i<6; i++) { EEPROM.writeLong(direccionInicial +(i*4),0); }
      }
      
      // Marcamos la EEPROM con la nueva versión v2.1
      EEPROM.writeLong(direccionInicial+45, MAGIC_NUMBER);
      Serial.println("Migracion completada.");
  }

  // Cargar datos
  for(int i=0; i<6; i++) {
      Mdatos[i]=EEPROM.readLong(direccionInicial+(i*4)); 
  }
  kwhParcialAcumulado = 0.0;

  pinMode(motorPIN, OUTPUT); digitalWrite(motorPIN,LOW);
  pinMode(valvbioPIN, OUTPUT); digitalWrite(valvbioPIN,LOW);
  pinMode(valvenfriaPIN, OUTPUT); digitalWrite(valvenfriaPIN,LOW);
  pinMode(ledPIN, OUTPUT);
  pinMode(zumbadorPIN, OUTPUT); digitalWrite(zumbadorPIN,LOW);
  for(int i=primerSensor; i<numeroSensores; i++) {
    pinMode(i, INPUT);  
  }

  // --- OPTIMIZACIÓN PANTALLA: DIBUJAR ESTRUCTURA FIJA UNA VEZ ---
  tft.fillScreen(BLACK);
  tft.setTextSize(3);
  tft.setCursor(0,0);
  for(int i=0; i<numeroSensores; i++) { 
       tft.setTextColor(YELLOW);
       tft.setCursor(0, i*24); // Posicionamos manualmente
       tft.print(Mnombres[i]);  
       tft.setCursor(280, i*24);
       tft.print("C");
  }
  // Etiquetas estadísticas fijas
  tft.setTextSize(2);
  tft.setCursor(0,144);
  tft.setTextColor(GREEN); 
  tft.print(MnomDatos[ctrKWh]); tft.print(": ");
  
  tft.setTextColor(BLUE);
  // Dibujamos las etiquetas de datos una vez (asumiendo posición fija aproximada)
  // Nota: Para simplificar, en el loop se redibujará la parte inferior, 
  // pero la parte superior de temperaturas ahora es mucho más estable.
}

//----------------------------------PROGRAMA PRINCIPAL-------------------------
void loop() {
  millisAnterior = millis(); 

  // --- RECOGIDA SONDAS ---
  for(int i=0; i<numeroSensores; i++) { Msensores[i]=0; }  
  for(int x=0; x<5; x++) {
      for(int i=0; i<numeroSensores; i++) {               
         Msensores[i]=Msensores[i]+analogRead(i+primerSensor);
      }  
      delay(25);
      wdt_reset(); 
  }
  
  for(int i=0; i<numeroSensores; i++) { Msensores[i]=(Msensores[i]/5)+calibTemps[i]; }  

  error=0; 
  for(int i=0; i<numeroSensores; i++) { if (Msensores[i]<50) { error++; } }  

  // Conversión a grados
  for(int i=0; i<numeroSensores; i++) {
    float v2 = (voltage*float(Msensores[i]))/1024.0f;  
    float r1 = ((voltage*float(resistor))/v2) - resistor;
    float T = B/log(r1*unodivr);
    Msensores[i]=T-273.15 + calibTemps[i]; 
  }  

  // Comprobar excesos
  for(int i=0; i<numeroSensores; i++) { if (Msensores[i]>95) { error++; } }  

  for(int i=0; i<numeroSensores; i++) { MtempAnt[i]=Mtempsens[i]; }

  Mtempsens[0]=Msensores[0];
  Mtempsens[1]=Msensores[2];
  Mtempsens[2]=Msensores[4];
  Mtempsens[3]=Msensores[3];
  Mtempsens[4]=Msensores[1];

  // --- LÓGICA DE CONTROL ---
  if ((Mtempsens[biomasa1]-Mtempsens[retorno])>= (DtFbiomasa*20/Mtempsens[biomasa1]) or (Mtempsens[biomasa2]-Mtempsens[retorno])>= (DtFbiomasa*20/Mtempsens[biomasa2])) { 
    valvula=true;
    motorON=true;
  }
  else { 
    if ((Mtempsens[biomasa1]-Mtempsens[retorno])<= (Dtobiomasa*20/Mtempsens[biomasa1]) and (Mtempsens[biomasa2]-Mtempsens[retorno])<= (Dtobiomasa*20/Mtempsens[biomasa2])) {
      motorON=false;
      valvula=false;
    }
    if ((Mtempsens[captador]-Mtempsens[deposito])>= DtFsolar) { 
      motorON=true;
      }
    if ((Mtempsens[captador]-Mtempsens[retorno])< Dtosolar and valvula==false) {
      motorON=false;
    } 
   }

  // --- GESTIÓN MOTOR Y CÁLCULO ENERGÍA ---
  if (motorON==true) {
      if (digitalRead(motorPIN)==LOW) { 
        Mdatos[ctrReleMotor]++;
        tempPrevia=Mtempsens[deposito]; 
        segundosInicioMotor=segundos; 
      }
      digitalWrite(motorPIN,HIGH);
  }
  else  {
    if (digitalRead(motorPIN)==HIGH) {
      if ((segundos-segundosInicioMotor)<minTimeMotor) {
        motorON=true;
      }
      else {
        // Cálculo de energía acumulada con decimales
        if (Mtempsens[deposito]>tempPrevia) {
          double energiaCiclo = (Mtempsens[deposito]-tempPrevia)*kwhGrado; 
          
          kwhParcialAcumulado += energiaCiclo;
          
          if (kwhParcialAcumulado >= 1.0) {
              int enteros = int(kwhParcialAcumulado);
              Mdatos[ctrKWh] = Mdatos[ctrKWh] + enteros;
              kwhParcialAcumulado = kwhParcialAcumulado - enteros; 
          }
          segundosValvula=segundos; 
        }
        digitalWrite(motorPIN,LOW);
        }
      }
  }

  if (valvula==false and (segundos-segundosValvula)>=600) { tresvias=false; }
  if (valvula==true) { tresvias=true; }

  if (tresvias==true) {
      if (digitalRead(valvbioPIN)==LOW) { Mdatos[ctrValvulaFuego]++; }
      digitalWrite(valvbioPIN, HIGH);
  } else  {
      digitalWrite(valvbioPIN,LOW);
  }

  // --- VÁLVULA ENFRIADO ---
  if (((Mtempsens[deposito]> DtFEnfriar) and motorON==true) or (motorON==true and Mtempsens[biomasa1]>TempDesvio)) {
      if (digitalRead(valvenfriaPIN)==LOW) { 
        Mdatos[ctrValvulaEnfriar]++; 
      }
    digitalWrite(valvenfriaPIN,HIGH);
  }
  else { 
      if ((Mtempsens[biomasa1]<(TempDesvio-5) and Mtempsens[deposito]< DtoEnfriar) or motorON==false ) {
      digitalWrite(valvenfriaPIN,LOW);
    }
  }
  
  // --- ACTUALIZACIÓN PANTALLA SIN PARPADEO ---
  // [Fix5] Condición de refresco inicial: 'segundos < 10' en lugar de 'millis() < 8000'.
  // millis() se congela durante el sleep y no es fiable como indicador de primer arranque.
  if ((segundos-segundosRefresca)>=3600 || segundos < 10) {
      segundosRefresca=segundos;

      // Imprimimos valores estadísticos usando color de fondo para borrar lo previo
      tft.setTextSize(2);
      
      // KWh
      tft.setCursor(180, 144); // Ajustar coordenada X según longitud de texto
      tft.setTextColor(GREEN, BLACK); 
      tft.print(Mdatos[ctrKWh]); tft.print("   "); // Espacios extra para limpiar dígitos viejos

      // Resto de estadísticas
      tft.setTextColor(BLUE, BLACK);
      int yPos = 164; // Posición Y inicial para lista
      for(int i=0; i<numeroSensores; i++) { // Usamos numeroSensores como limite del bucle de visualizacion
            tft.setCursor(0, yPos);
            tft.print(MnomDatos[i]);  
            tft.print(": ");
            tft.print(Mdatos[i]); tft.println("   ");
            yPos += 20; // Salto de línea
      }  
  }

  // Temperaturas (Actualización frecuente)
  tft.setTextSize(3);
  for(int i=0; i<numeroSensores; i++) {
      
      // Borrado inteligente: Imprimir el valor anterior en NEGRO, o usar fondo (mejor fondo)
      tft.setCursor(162,i*24);
      
      if (Mtempsens[i]<=-270) { 
          tft.setTextColor(RED, BLACK);
          tft.print("ERROR"); 
      }
      else {
        // Lógica de color de temperatura
        uint16_t colorTemp = YELLOW;
        if ((i==captador) || (i==biomasa1) || (i==biomasa2)) { 
          if ((Mtempsens[i]-Mtempsens[i+1])>difTemp) {
            colorTemp = RED; 
            error++; 
          }
        }
        
        tft.setTextColor(colorTemp, BLACK);
        
        grados=int(Mtempsens[i]);
        if (grados<10) { tft.print(" "); } // Alineación
        tft.print(grados); tft.print(",");
        decimas=(Mtempsens[i]-grados)*10;
        tft.print(abs(decimas)); tft.print(" "); // Espacio final por si bajamos de 3 a 2 dígitos
      }
  }

  // AVISOS TEXTO
  // Sobrescribimos en el mismo sitio
  tft.setCursor(0,120); tft.setTextSize(3);
  if (motorON==true) {
    tft.setTextColor(RED, BLACK); tft.print("MOTOR ON       "); // Espacios para borrar "OFF"
    TextoAnt="MOTOR ON";
  }
  else {
      tft.setTextColor(GREEN, BLACK); tft.print("MOTOR OFF      "); 
      TextoAnt="MOTOR OFF";
    }
  
  // Estado Válvulas (Añadimos al lado)
  if (digitalRead(valvenfriaPIN)==HIGH) { 
    tft.setTextColor(WHITE, BLACK); tft.print(" ENFRIA "); 
  }
  else if (digitalRead(valvbioPIN)==HIGH) { 
    tft.setTextColor(WHITE, BLACK); tft.print(" BIOMASA"); 
  } else {
    tft.setTextColor(BLACK, BLACK); tft.print("        "); // Borrar texto válvula si no hay
  }
  
  // _______________________________ GUARDADO TIEMPOS _______________________________________
  if (motorON==false and segundosInicioMotor>0) { 
    // [Fix6] Orden de operaciones corregido: primero la resta (que es segura con unsigned long
    // incluso con desbordamiento), luego la suma del resto acumulado.
    // El orden anterior (segundos + restoSegundosMotor - segundosInicioMotor) podía desbordar
    // en la suma intermedia si segundos estaba cerca del límite de unsigned long.
    unsigned long duracion = (segundos - segundosInicioMotor) + restoSegundosMotor;
    Mdatos[hrsMotorOn] += duracion / 3600;
    restoSegundosMotor = duracion % 3600;
    segundosInicioMotor = 0;
  }

  // --- GUARDADO EEPROM Y MANTENIMIENTO VÁLVULA ---
  if ((segundos-segundosArduino)>=(3600*3)) { 
       Serial.println("Chequeo periodico 3h...");
       Mdatos[hrsArduino] += 3; 
       segundosArduino = segundos; 
       
       activarValvEnfria++;
       // Mantenimiento válvula
       if (activarValvEnfria>=horasValvEnfria) {
         activarValvEnfria=0;
         digitalWrite(valvenfriaPIN,HIGH);
         
         // Bucle seguro con Watchdog Reset
         for(int w=0; w<100; w++) { // 100 * 100ms = 10 segundos
             delay(100); 
             wdt_reset(); 
         }
         
         digitalWrite(valvenfriaPIN,LOW);
       }
      
      // Guardado con Wear Leveling (Solo si cambia)
      for(int i=0; i<6; i++) { 
         long valActual = EEPROM.readLong(direccionInicial+(i*4));
         if (valActual != Mdatos[i]) {
            EEPROM.writeLong(direccionInicial+(i*4),Mdatos[i]);
            wdt_reset();
         }
        }
  }

  // LED
  if ((segundos-timeLED)>2) {
      digitalWrite(ledPIN,HIGH); 
      timeLED=segundos;
  }
  else { digitalWrite(ledPIN,LOW); }

  wdt_reset(); 

  // Sleep y Alarma
  if (error>0) { 
    for(int i=0; i<frecseg; i++) { 
      digitalWrite(zumbadorPIN,HIGH);
      delay(300); wdt_reset();
      digitalWrite(zumbadorPIN,LOW);
      delay(700); wdt_reset();
      }
    }
  else {
      digitalWrite(zumbadorPIN,LOW);
      
      // Actualizamos segundos ANTES de dormir sumando el tiempo que vamos a dormir
      // frecseg (veces) * 2 segundos aprox por ciclo de sleep (WDTCSR = 2.0s)
      segundos += (unsigned long)frecseg * 2; 

      for(int i=0; i<frecseg; i++) {
          enterSleep(); 
          // Al despertar del sleep, el watchdog resetea y volvemos aquí
      }
      
      // Ajuste fino: sumamos el tiempo de ejecución del código principal
      segundos += (millis() - millisAnterior) / 1000;
  }

  // [Fix4] Condición cambiada de >= a > para evitar reset espurio de timers.
  // Con >=, la condición se cumplía también cuando segundosInicioMotor == segundos,
  // es decir, justo al arrancar el motor, reseteando incorrectamente segundosArduino
  // y retrasando el guardado en EEPROM.
  if (segundosInicioMotor > segundos) { 
     // Reset de seguridad por si acaso hubo un salto extraño
     segundosInicioMotor=segundos; 
     segundosArduino=segundos;
     segundosValvula=segundos;
     segundosRefresca=0;
  } 
}

// INTERRUPCIONES
ISR(WDT_vect) { }

void enterSleep(void) {
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); 
  sleep_enable();
  sleep_mode();
  sleep_disable(); 
  power_all_enable();
}

Aislando mejor el depósito

Y para finalizar, he mejorado el aislamiento del depósito de 150 lts con una vieja manta y restos de una lámina aislante con capa de aluminio (aquí se recicla todo) para protegerla de los UV del sol.

Esto es esencial si el depósito se encuentra en el exterior, pues se traduce en menores pérdidas, que en invierno pueden ser muy elevadas.
Para "sujetar" las mantas he utilizado trozos de neumático viejo de bici metidos a presión contra la pared; al ser de goma elástica, se mantiene en su sitio.



En la parte inferior también he colocado, pero dejando el acceso fácil para los mantenimientos del depósito.
Y finalmente lo he asegurado con cinta americana:



Mantenimiento del vaso de expansión

Cada 3 años hay que revisar el vaso de expansión, ya que pierde la presión como cualquier neumático, y deja de realizar adecuadamente su función de mantener la presión a 1,5 bares.
Debajo tiene una válvula y en vacío debemos llenarlo de aire, con una presión de entre 0,5 y 1 bares (coge más presión al entrar agua, por lo que debemos buscar la forma de que a medio llenar de agua tenga 1,5 bares). Yo me pasé un poco y no llegaba a coger agua, así que lo regulé conectado y dejando escapar algo de presión hasta que cogió 10 kg de peso aprox (es de 24 lts) y se mantuvo entre 1,5 y 2 bares.
El vaso de mi instalación se oxidó toda la parte inferior al salpicarle la lluvia con los años, así que le lijé con cepillos de láminas y el taladro y le he aplicado pintura "forja" anti-óxido (lleva partículas metálicas que limitan mucho el paso de la humedad), con ella no hace falta una mano previa antioxido, es muy duradera.

Inflando con una bomba de bici y con un manómetro mido presión

Más info:
Guía práctica de la instalación de un sistema de energía solar térmica
Manual técnico instalación energía solar térmica

No hay comentarios:

Publicar un comentario

Puede dejar su comentario, que tratará de ser moderado en los días siguientes. En caso de ser algo importante/urgente, por favor utilicen el formulario de arriba a la derecha para contactar.