Comucacion entre ESP32 Y Arduino Mega

Me explico intentar aclarar el problema:

Tengo una pantalla ESP32 conectada a una placa Arduino Mega.

Cuando alimento la ESP32 y la Arduino mega únicamente mediante su USB de la ESP32, la pantalla recibe y muestra correctamente todos los datos procedentes de la Arduino Mega: temperatura, estado de los ventiladores, qué fuente está activa, etc.

El problema aparece cuando conecto simultáneamente:

  • La alimentación de la Arduino Mega.
  • El USB de la ESP32.

En ese momento, la ESP32 deja de recibir los datos de temperatura y el resto de información de la Arduino Mega.

Sin embargo, hay algunas funciones que siguen funcionando. Por ejemplo, puedo hacer un RESET de la Arduino Mega y también puedo activar mediante la pantalla la resistencia anti condensación.

Si, en cambio, utilizo solamente el USB de la Arduino Mega, o alimento la Mega y la ESP32 mediante el USB de Arduino mega o una fuente externa de 5 V, aparentemente todo funciona correctamente en la Arduino mega pero la ESP32 solo muestra algunos datos.

Para la alimentación externa utilizo un regulador que convierte los 12 V de la instalación a 5 V, por lo que la Arduino Mega recibe una tensión regulada.

Mi duda es:

¿Puede existir algún problema de alimentación, masas (GND), niveles de tensión o comunicación serie entre la ESP32 y la Arduino Mega cuando ambas están alimentadas simultáneamente?

Lo que me resulta extraño es que la comunicación no se pierde completamente, ya que algunas funciones siguen respondiendo, pero dejan de llegar los datos de temperatura y otros estados.

Para que el foro pueda ayudarte, hace falta dar más información sobre cómo están interconectadas exactamente las dos placas.

La ESP32 trabaja con lógica de 3.3 V mientras que la Arduino Mega trabaja con lógica de 5 V, y eso es relevante para el problema descrito. Habría que precisar qué pines de la Mega están conectados a qué pines de la ESP32 para la comunicación serie, si se usa UART por hardware o software serial, si hay algún divisor de tensión o convertidor de nivel lógico entre el TX de la Mega y el RX de la ESP32, y cómo están unidas las masas, GND, de ambas placas y de la fuente externa de 5 V cuando se utiliza.

También sería útil precisar si la conexión de datos es directa por cable entre placas o si pasa por algún módulo intermedio, y si el regulador de 12 V a 5 V usado para la Mega comparte masa con el USB del ordenador cuando la ESP32 se alimenta por USB al mismo tiempo.

etc...

si tengo un convertidor intermedio entre la esp32 y Arduino y por un lado 5 V y por otro 3 v pero la esp32 estas alimentada con 5 V tengo conectado el pin 18 de la mega esta conectado al RX de la esp32 y el pin 19 de la mega al TX pero intermedio hay un regulador

Aunque el ESP32 esté alimentado con 5V, sus pines solo soportan 3.3V, así que está bien que todo lo que venga del MEGA pase por una adaptación de tensión.

Parece que no has conectado las GND entre el MEGA y el ESP32. Eso es necesario para que se pongan de acuerdo entre ellos sobre lo que significa 0V.

buenas tarde J-M-L Jackson buernas tarde y gracias por contestar

te cuento entre la mega y la esp32 tengo conectado un convertido de 4 polos y por la parte H esta alimentado con 5 V + GND y por la parte L esta alimentado con 3,3 V + GND y la ESP32 la tengo alimentada con un GND que es común a todos los

GND y a 5 V

si te parece te adjunto dos fotos si la conecto al regulador de 5 V que tengo para alimentar a la arduino mega y a los relés opto acoplados y alas sondeas de temperatura no me lee las sondas de temperatura pero si hay dos opciones que funcionan el RESET para la Aduino mega y un botón que tengo para abrir el relé opto acoplado para una resistencia pequeña que hay en el frigorífico de la auto caravana (es una nevera trivialmente).

PERO cuando solo la conecto a trabes del puerto USB de la ESP32 que esta conectado al ordenador funciona perfectamente  todo (solo si esta conectada al PC) esto me tiene loco 


muchas gracias saludos

Sube un esquema (no Fritzing :folded_hands:) de las conexiones y que incluya las lineas de alimentación y masas.
Sube los códigos así también los revisamos.

Sospecho que el problema está en la alimentación. ¿Utiliza sondas DS18B20? El reset y el botón del relé funcionan porque son simples señales de todo o nada que toleran fácilmente el ruido o una tensión inestable. Las sondas de temperatura, si utilizan el protocolo OneWire como las DS18B20, son mucho más sensibles al timing y al ruido en la línea de datos. Una alimentación ruidosa o inestable puede por lo tanto interrumpir la comunicación sin impedir que el resto del circuito funcione.

Esto explicaría por qué el regulador de 5V que alimenta la Mega, los relés y las sondas quizás no es suficientemente estable bajo carga, lo cual basta para perturbar el OneWire sin afectar al reset ni al relé. Esto también coincide con el hecho de que todo funciona perfectamente cuando la ESP32 está alimentada por el puerto USB del ordenador, que proporciona una alimentación mucho más limpia y estable.

OS ADJUNTO PDF CON LAS CONEXIONES REALES Y LOS DOS codigos el de la mega y el de esp32


lo siento pero no se como subir el código lo pegare a aqui
este es el código de la Arduino Mega
// ==========================================
// TRIVALENTE CONTROL SYSTEM - VERSION SIN PANTALLA LOCAL
// (Pantalla OLED SSD1309 eliminada; toda la UI delegada a ESP32)
// ==========================================

#include <OneWire.h>
#include <DallasTemperature.h>
#include <Wire.h>
#include <SPI.h>
#include <EEPROM.h>
#if defined(ARDUINO_ARCH_AVR)
#include <avr/wdt.h>
#endif

// ==========================================
// DEFINICION DE PINES
// ==========================================
#define PIN_IN_220V 14 // Modulo opto comercial 220V. LOGICA INVERTIDA: LOW = hay 220V
#define PIN_IN_12V 6 // Modulo opto comercial EL817_12V. LOGICA INVERTIDA: LOW = hay 12V

// SENSOR LLAMA (P810 + opto 4N35): LECTURA ANALOGICA en A0
#define PIN_IN_FLAME A0
const int FLAME_ADC_HI = 378;
const int FLAME_ADC_LOOSE = 256;
const int FLAME_ADC_LO = 225;
const int FLAME_ADC_SHORT = 50;
const int FLAME_ADC_SAMPLES = 64;

int lastFlameDecision = -1;

#define PIN_TEMP_SENSOR 7 // Sensores DS18B20

// Salidas Reles
#define PIN_RELAY_220V 2 // IN1
#define PIN_RELAY_12V 3 // IN2
#define PIN_RELAY_GAS 12 // IN3 (VALVULA GAS)
#define PIN_RELAY_FAN 4 // IN4 (VENTILADORES)
#define PIN_RELAY_COND 5 // IN5 (CONDENSACION)

#define RELAY_ON LOW
#define RELAY_OFF HIGH

#define PIN_LED_STATUS 8
#define PIN_BUZZER 13

// ==========================================
// CONSTANTES Y CONFIGURACION
// ==========================================
const float HYSTERESIS = 1.0;
const float TEMP_MIN = -14.0;
const float TEMP_MAX = 20.0;
float targetTemp = 4.0;
const unsigned long SWITCH_DELAY_MS = 3000;
const unsigned long GAS_BLOCK_DURATION_MS = 20UL * 60UL * 1000UL;
const unsigned long GAS_IGNITION_TIMEOUT_MS = 30000UL;
const unsigned long GAS_PURGE_DURATION_MS = 10000UL;
const unsigned long FLAME_LOSS_CONFIRM_MS = 5000UL;
const int GAS_MAX_RETRIES = 3;

const float V_REF = 5.0;
const unsigned long INPUT_DEBOUNCE_MS = 500;
const unsigned long DIAG_SERIAL_INTERVAL_MS = 350;
const bool GAS_PRUEBA_IGNORA_LLAMA = false;
const unsigned long GAS_FLAME_STABLE_MS = 5000;

const float FAN_REAR_ON_TEMP = 60.0;
const float FAN_REAR_OFF_TEMP = 50.0;

const unsigned long COND_AUTO_OFF_MS = 2UL * 60UL * 60UL * 1000UL;

const int ADDR_TARGET_TEMP = 0;
const int ADDR_TOTAL_RUNTIME = ADDR_TARGET_TEMP + sizeof(float);
const int ADDR_PARTIAL_RUNTIME = ADDR_TOTAL_RUNTIME + sizeof(unsigned long);

// Direcciones DS18B20 fijadas segun las pruebas realizadas.
// 0 = frigorifico interior, 1 = congelador, 2 = ventiladores exteriores.
DeviceAddress fridgeSensorAddress = { 0x28, 0x8C, 0x73, 0x22, 0x00, 0x00, 0x00, 0xDD };
DeviceAddress freezerSensorAddress = { 0x28, 0x51, 0x2C, 0x22, 0x00, 0x00, 0x00, 0xA9 };
DeviceAddress rearSensorAddress = { 0x28, 0xD9, 0xC6, 0x23, 0x00, 0x00, 0x00, 0x0A };

bool rawIn220VStable = false;
bool rawIn12VStable = false;
unsigned long debounce220VT = 0;
unsigned long debounce12VT = 0;

// ==========================================
// VARIABLES DE ESTADO
// ==========================================
enum EnergyMode {
MODE_NONE,
MODE_220V,
MODE_12V,
MODE_GAS,
MODE_TRANSITION
};

EnergyMode currentMode = MODE_NONE;
EnergyMode targetMode = MODE_NONE;
unsigned long switchTimer = 0;
unsigned long sensorTimer = 0;
unsigned long statusTimer = 0;
bool tempRequested = false;

bool prevHas12V = false;
unsigned long gasBlockTimer = 0;
bool gasBlocked = false;

unsigned long gasIgnitionTimer = 0;
bool gasFault = false;
int gasRetryCount = 0;
bool gasWaitingRetry = false;
unsigned long gasRetryTimer = 0;
unsigned long flameLossTimer = 0;

bool coolerActive = false;
float fridgeTemp = 0.0;
float freezerTemp = 0.0;
float rearTemp = 0.0;
bool fanActive = false;

bool condActive = false;
unsigned long condOnTimer = 0;

bool fanManualMode = false;
bool fanManualState = false;

int lastFlameAdcRaw = 0;
float lastFlamePinV = 0.0;

unsigned long tempSettingTimer = 0;
unsigned long bootTime = 0;
unsigned long runtimeTick = 0;
unsigned long runtimeSaveTimer = 0;
unsigned long fridgeRunTotalSeconds = 0;
unsigned long fridgeRunPartialSeconds = 0;
bool runtimeDirty = false;

// ==========================================
// COMUNICACION ESP32 (Serial1 - D18/D19)
// ==========================================
unsigned long esp32TxTimer = 0;
String esp32RxBuffer = "";
unsigned long esp32LastCommandMs = 0;
unsigned long esp32LastStatusTxMs = 0;
unsigned long esp32StatusPacketsSent = 0;
unsigned long esp32CommandsReceived = 0;
bool gasFlameStable = false;
bool flameReachedStableOnce = false;
bool oledDisplayPresent = false;
unsigned long oledRefreshTimer = 0;
int oledLastFridgeRounded = 32767;
int oledLastFreezerRounded = 32767;
bool fridgeSensorMissing = false;
bool freezerSensorMissing = false;
bool rearSensorMissing = false;
bool oledShowingAlert = false;
char oledLastAlertTop[10] = "";
char oledLastAlertBottom[10] = "";

const uint8_t OLED_PIN_CS = 49;
const uint8_t OLED_PIN_DC = 48;
const uint8_t OLED_PIN_RES = 47;
const uint8_t OLED_TEMP_AREA_START = 0;
const uint8_t OLED_TEMP_AREA_WIDTH = 74;
const uint8_t OLED_MODE_AREA_START = 78;
const uint8_t OLED_MODE_AREA_WIDTH = 50;

OneWire oneWire(PIN_TEMP_SENSOR);
DallasTemperature sensors(&oneWire);

const uint8_t OLED_CMD_MODE = 0x00;
const uint8_t OLED_DATA_MODE = 0x40;
const uint8_t OLED_PAGE_COUNT = 8;
const uint8_t OLED_WIDTH = 128;

const uint8_t GLYPH_SPACE[5] = { 0x00, 0x00, 0x00, 0x00, 0x00 };
const uint8_t GLYPH_DASH[5] = { 0x08, 0x08, 0x08, 0x08, 0x08 };
const uint8_t GLYPH_DOT[5] = { 0x00, 0x60, 0x60, 0x00, 0x00 };
const uint8_t GLYPH_0[5] = { 0x3E, 0x51, 0x49, 0x45, 0x3E };
const uint8_t GLYPH_1[5] = { 0x00, 0x42, 0x7F, 0x40, 0x00 };
const uint8_t GLYPH_2[5] = { 0x42, 0x61, 0x51, 0x49, 0x46 };
const uint8_t GLYPH_3[5] = { 0x21, 0x41, 0x45, 0x4B, 0x31 };
const uint8_t GLYPH_4[5] = { 0x18, 0x14, 0x12, 0x7F, 0x10 };
const uint8_t GLYPH_5[5] = { 0x27, 0x45, 0x45, 0x45, 0x39 };
const uint8_t GLYPH_6[5] = { 0x3C, 0x4A, 0x49, 0x49, 0x30 };
const uint8_t GLYPH_7[5] = { 0x01, 0x71, 0x09, 0x05, 0x03 };
const uint8_t GLYPH_8[5] = { 0x36, 0x49, 0x49, 0x49, 0x36 };
const uint8_t GLYPH_9[5] = { 0x06, 0x49, 0x49, 0x29, 0x1E };
const uint8_t GLYPH_A[5] = { 0x7E, 0x11, 0x11, 0x11, 0x7E };
const uint8_t GLYPH_B[5] = { 0x7F, 0x49, 0x49, 0x49, 0x36 };
const uint8_t GLYPH_C[5] = { 0x3E, 0x41, 0x41, 0x41, 0x22 };
const uint8_t GLYPH_D[5] = { 0x7F, 0x41, 0x41, 0x22, 0x1C };
const uint8_t GLYPH_E[5] = { 0x7F, 0x49, 0x49, 0x49, 0x41 };
const uint8_t GLYPH_F[5] = { 0x7F, 0x09, 0x09, 0x09, 0x01 };
const uint8_t GLYPH_G[5] = { 0x3E, 0x41, 0x49, 0x49, 0x7A };
const uint8_t GLYPH_H[5] = { 0x7F, 0x08, 0x08, 0x08, 0x7F };
const uint8_t GLYPH_I[5] = { 0x00, 0x41, 0x7F, 0x41, 0x00 };
const uint8_t GLYPH_L[5] = { 0x7F, 0x40, 0x40, 0x40, 0x40 };
const uint8_t GLYPH_M[5] = { 0x7F, 0x02, 0x0C, 0x02, 0x7F };
const uint8_t GLYPH_N[5] = { 0x7F, 0x02, 0x0C, 0x10, 0x7F };
const uint8_t GLYPH_O[5] = { 0x3E, 0x41, 0x41, 0x41, 0x3E };
const uint8_t GLYPH_P[5] = { 0x7F, 0x09, 0x09, 0x09, 0x06 };
const uint8_t GLYPH_Q[5] = { 0x3E, 0x41, 0x51, 0x21, 0x5E };
const uint8_t GLYPH_R[5] = { 0x7F, 0x09, 0x19, 0x29, 0x46 };
const uint8_t GLYPH_S[5] = { 0x26, 0x49, 0x49, 0x49, 0x32 };
const uint8_t GLYPH_T[5] = { 0x01, 0x01, 0x7F, 0x01, 0x01 };
const uint8_t GLYPH_U[5] = { 0x3F, 0x40, 0x40, 0x40, 0x3F };
const uint8_t GLYPH_V[5] = { 0x1F, 0x20, 0x40, 0x20, 0x1F };
const uint8_t GLYPH_Y[5] = { 0x03, 0x04, 0x78, 0x04, 0x03 };
const uint8_t GLYPH_PLUS[5] = { 0x08, 0x08, 0x3E, 0x08, 0x08 };

// ==========================================
// UTILIDADES DE ENTRADA
// ==========================================
bool isSensorAddressConfigured(const DeviceAddress address) {
for (byte i = 0; i < 8; i++) {
if (address[i] != 0) return true;
}
return false;
}

void printSensorAddress(const DeviceAddress address) {
for (byte i = 0; i < 8; i++) {
if (address[i] < 16) Serial.print('0');
Serial.print(address[i], HEX);
if (i < 7) Serial.print(':');
}
}

void printDetectedSensorAddresses() {
byte sensorCount = sensors.getDeviceCount();
Serial.print(F("[DS18B20] Sondas detectadas: "));
Serial.println(sensorCount);

for (byte i = 0; i < sensorCount; i++) {
    DeviceAddress detectedAddress;
    if (sensors.getAddress(detectedAddress, i)) {
        Serial.print(F("[DS18B20] Indice "));
        Serial.print(i);
        Serial.print(F(" -> "));
        printSensorAddress(detectedAddress);
        Serial.println();
    }
}

}

void handleESP32Command(const String& command) {
if (command.length() == 0) return;

esp32LastCommandMs = millis();
esp32CommandsReceived++;

if (command.startsWith("TEMP,")) {
    float newTemp = command.substring(5).toFloat();
    if (newTemp >= TEMP_MIN && newTemp <= TEMP_MAX) {
        targetTemp   = newTemp;
        tempSettingTimer = millis();
        EEPROM.put(ADDR_TARGET_TEMP, targetTemp);
        Serial.print(F("[ESP32] Temp recibida: "));
        Serial.println(targetTemp, 1);
    }
}
else if (command.startsWith("COND,")) {
    int val = command.substring(5).toInt();
    if (val == 1 && !condActive) {
        condActive  = true;
        condOnTimer = millis();
        Serial.println(F("[ESP32] COND ON"));
    } else if (val == 0) {
        condActive  = false;
        condOnTimer = 0;
        Serial.println(F("[ESP32] COND OFF"));
    }
}
else if (command.startsWith("FAN,")) {
    int val = command.substring(4).toInt();
    if (val == 1) {
        fanManualMode  = true;
        fanManualState = true;
        Serial.println(F("[ESP32] FAN manual ON"));
    } else {
        fanManualMode  = true;
        fanManualState = false;
        Serial.println(F("[ESP32] FAN manual OFF -> auto"));
    }
}
else if (command == "PARTIAL_RESET") {
    resetRuntimeCounters(false, true);
    Serial.println(F("[ESP32] Tiempo parcial reiniciado."));
}
else if (command == "TOTAL_RESET") {
    resetRuntimeCounters(true, true);
    Serial.println(F("[ESP32] Tiempo total y parcial reiniciados."));
}
else if (command == "RESET_MEGA") {
    restartMegaFromCommand();
}

}

float readManagedTemperature(const DeviceAddress address, byte fallbackIndex) {
if (isSensorAddressConfigured(address)) {
return sensors.getTempC(address);
}
return sensors.getTempCByIndex(fallbackIndex);
}

bool readDigitalFiltered(byte pin, bool *stableState, unsigned long *debounceT, unsigned long debounceMs, bool usePullupLogic) {
bool rawNow = usePullupLogic ? (digitalRead(pin) == LOW) : (digitalRead(pin) == HIGH);
if (rawNow != *stableState) {
if (*debounceT == 0) *debounceT = millis();
else if (millis() - *debounceT >= debounceMs) {
*stableState = rawNow;
*debounceT = 0;
}
} else {
*debounceT = 0;
}
return *stableState;
}

bool readRaw220VPresent() {
return readDigitalFiltered(PIN_IN_220V, &rawIn220VStable, &debounce220VT, INPUT_DEBOUNCE_MS, true);
}
bool readRaw12VPresent() {
return readDigitalFiltered(PIN_IN_12V, &rawIn12VStable, &debounce12VT, INPUT_DEBOUNCE_MS, true);
}

bool readFlameOK() {
long sum = 0;
for (byte i = 0; i < FLAME_ADC_SAMPLES; i++) {
sum += analogRead(PIN_IN_FLAME);
delayMicroseconds(200);
}
int mean = (int)(sum / FLAME_ADC_SAMPLES);
lastFlameAdcRaw = mean;
lastFlamePinV = (mean * V_REF) / 1023.0;

int thresholdOn  = flameReachedStableOnce ? FLAME_ADC_LOOSE : FLAME_ADC_LO;
int thresholdOff = flameReachedStableOnce ? (FLAME_ADC_LOOSE + ((FLAME_ADC_HI - FLAME_ADC_LOOSE) / 2))
                                          : (FLAME_ADC_LO    + ((FLAME_ADC_HI - FLAME_ADC_LO)    / 2));

bool result;
if (mean < FLAME_ADC_SHORT) { result = false; lastFlameDecision = 0; }
else if (mean > FLAME_ADC_HI) { result = false; lastFlameDecision = 0; }
else if (mean < thresholdOn) { result = true; lastFlameDecision = 1; }
else if (mean > thresholdOff) { result = false; lastFlameDecision = 0; }
else {
    if (lastFlameDecision < 0) lastFlameDecision = 0;
    result = (lastFlameDecision == 1);
}
return result;

}

byte readFlameState() {
long sum = 0;
for (byte i = 0; i < 32; i++) {
sum += analogRead(PIN_IN_FLAME);
delayMicroseconds(150);
}
int mean = (int)(sum / 32);
lastFlameAdcRaw = mean;
lastFlamePinV = (mean * V_REF) / 1023.0;
int thresholdOn = flameReachedStableOnce ? FLAME_ADC_LOOSE : FLAME_ADC_LO;
int thresholdOff = FLAME_ADC_HI;

if (mean < FLAME_ADC_SHORT) return 0;
if (mean > thresholdOff) return 0;
if (mean < thresholdOn) return 1;
return 2;

}

int lastFlameADC = 0;
int readLastFlameADC() {
long sum = 0;
for (byte i = 0; i < FLAME_ADC_SAMPLES; i++) {
sum += analogRead(PIN_IN_FLAME);
delayMicroseconds(120);
}
lastFlameADC = (int)(sum / FLAME_ADC_SAMPLES);
return lastFlameADC;
}

bool isGasOpen() {
return currentMode == MODE_GAS && !gasFault && !gasWaitingRetry && digitalRead(PIN_RELAY_GAS) == RELAY_ON;
}

bool isGasOpenNoFlame() {
if (!isGasOpen()) return false;
if (GAS_PRUEBA_IGNORA_LLAMA) return false;
byte st = readFlameState();
if (st == 0) return true;
if (st == 2 && lastFlameDecision != 1) return true;
return false;
}

bool isGasFlameConfirmed() {
if (!isGasOpen()) return false;
if (GAS_PRUEBA_IGNORA_LLAMA) return gasFlameStable;
return gasFlameStable && readFlameOK();
}

bool isFlameLostAfterStable() {
if (gasFault) return true;
if (gasWaitingRetry) return true;
if (flameLossTimer > 0) return true;
if (isGasOpen() && (!readFlameOK() || isGasOpenNoFlame())) {
if (flameReachedStableOnce) return true;
if (gasIgnitionTimer > 0 && (millis() - gasIgnitionTimer) >= (GAS_IGNITION_TIMEOUT_MS / 2)) return true;
}
return false;
}

void printDiagSerial() {
static unsigned long lastT = 0;
if (millis() - lastT < DIAG_SERIAL_INTERVAL_MS) return;
lastT = millis();

int flameADC = readLastFlameADC();
bool f220 = readRaw220VPresent();
bool f12 = readRaw12VPresent();
bool flameOK = GAS_PRUEBA_IGNORA_LLAMA ? true : readFlameOK();

Serial.print(F("[DIAG] t=")); Serial.print(millis());
Serial.print(F(" M=")); Serial.print(currentMode);
Serial.print(F(" Fadc=")); Serial.print(flameADC);
Serial.print(F(" F=")); Serial.print(f220 ? '1' : '0'); Serial.print(f12 ? '1' : '0'); Serial.print(flameOK ? '1' : '0');
Serial.print(F(" Tf=")); Serial.print(fridgeTemp, 1);
Serial.print(F(" Tcong=")); Serial.print(freezerTemp, 1);
Serial.print(F(" Trear=")); Serial.print(rearTemp, 1);
Serial.print(F(" Fan=")); Serial.print(fanActive ? '1' : '0');
Serial.print(F(" Cond=")); Serial.print(condActive ? '1' : '0');
Serial.print(F(" Ts=")); Serial.print(targetTemp, 1);
Serial.print(F(" S1tx=")); Serial.print(esp32StatusPacketsSent);
Serial.print(F(" S1rx=")); Serial.print(esp32CommandsReceived);
Serial.print(F(" S1ageTx=")); Serial.print((esp32LastStatusTxMs == 0) ? 0 : (millis() - esp32LastStatusTxMs));
Serial.print(F(" S1ageRx=")); Serial.print((esp32LastCommandMs == 0) ? 0 : (millis() - esp32LastCommandMs));
Serial.println();

}

bool isFridgeRunningNow() {
if (digitalRead(PIN_RELAY_220V) == RELAY_ON) return true;
if (digitalRead(PIN_RELAY_12V) == RELAY_ON) return true;
if (currentMode == MODE_GAS) {
if (GAS_PRUEBA_IGNORA_LLAMA) return digitalRead(PIN_RELAY_GAS) == RELAY_ON;
return isGasFlameConfirmed();
}
return false;
}

void saveRuntimeCounters() {
EEPROM.put(ADDR_TOTAL_RUNTIME, fridgeRunTotalSeconds);
EEPROM.put(ADDR_PARTIAL_RUNTIME, fridgeRunPartialSeconds);
runtimeDirty = false;
runtimeSaveTimer = millis();
}

void updateRuntimeCounters() {
unsigned long now = millis();
if (runtimeTick == 0) runtimeTick = now;

while (now - runtimeTick >= 1000UL) {
    runtimeTick += 1000UL;
    if (isFridgeRunningNow()) {
        fridgeRunTotalSeconds++;
        fridgeRunPartialSeconds++;
        runtimeDirty = true;
    }
}

if (runtimeDirty && (now - runtimeSaveTimer >= 60000UL)) {
    saveRuntimeCounters();
}

}

void resetRuntimeCounters(bool resetTotal, bool resetPartial) {
if (resetTotal) fridgeRunTotalSeconds = 0;
if (resetPartial) fridgeRunPartialSeconds = 0;
saveRuntimeCounters();
}

void restartMegaFromCommand() {
Serial.println(F("[ESP32] Reinicio software solicitado."));
saveRuntimeCounters();
#if defined(ARDUINO_ARCH_AVR)
delay(50);
wdt_enable(WDTO_15MS);
while (true) {}
#endif
}

// ==========================================
// OLED I2C AUXILIAR (solo temperaturas)
// ==========================================
const uint8_t* getOledGlyph(char c) {
switch (c) {
case '0': return GLYPH_0;
case '1': return GLYPH_1;
case '2': return GLYPH_2;
case '3': return GLYPH_3;
case '4': return GLYPH_4;
case '5': return GLYPH_5;
case '6': return GLYPH_6;
case '7': return GLYPH_7;
case '8': return GLYPH_8;
case '9': return GLYPH_9;
case 'A': return GLYPH_A;
case 'B': return GLYPH_B;
case 'C': return GLYPH_C;
case 'D': return GLYPH_D;
case 'E': return GLYPH_E;
case 'F': return GLYPH_F;
case 'G': return GLYPH_G;
case 'H': return GLYPH_H;
case 'I': return GLYPH_I;
case 'L': return GLYPH_L;
case 'M': return GLYPH_M;
case 'N': return GLYPH_N;
case 'O': return GLYPH_O;
case 'P': return GLYPH_P;
case 'Q': return GLYPH_Q;
case 'R': return GLYPH_R;
case 'S': return GLYPH_S;
case 'T': return GLYPH_T;
case 'U': return GLYPH_U;
case 'V': return GLYPH_V;
case 'Y': return GLYPH_Y;
case '+': return GLYPH_PLUS;
case '-': return GLYPH_DASH;
case '.': return GLYPH_DOT;
case ' ': return GLYPH_SPACE;
default: return GLYPH_SPACE;
}
}

void oledGetModeText(char* outBuffer, size_t outSize) {
if (currentMode == MODE_220V) snprintf(outBuffer, outSize, "220V");
else if (currentMode == MODE_12V) snprintf(outBuffer, outSize, "12V");
else if (currentMode == MODE_GAS) snprintf(outBuffer, outSize, "GAS");
else snprintf(outBuffer, outSize, "OFF");
}

void oledGetFanText(char* outBuffer, size_t outSize) {
snprintf(outBuffer, outSize, fanActive ? "FANON" : "FANOFF");
}

void oledSendCommand(uint8_t command) {
if (!oledDisplayPresent) return;
SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
digitalWrite(OLED_PIN_CS, LOW);
digitalWrite(OLED_PIN_DC, LOW);
SPI.transfer(command);
digitalWrite(OLED_PIN_CS, HIGH);
SPI.endTransaction();
}

void oledWriteColumns(const uint8_t* columns, uint8_t count) {
if (!oledDisplayPresent) return;
SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
digitalWrite(OLED_PIN_CS, LOW);
digitalWrite(OLED_PIN_DC, HIGH);
for (uint8_t i = 0; i < count; i++) SPI.transfer(columns[i]);
digitalWrite(OLED_PIN_CS, HIGH);
SPI.endTransaction();
}

void oledSetCursor(uint8_t page, uint8_t column) {
oledSendCommand(0xB0 | (page & 0x07));
oledSendCommand(0x00 | (column & 0x0F));
oledSendCommand(0x10 | ((column >> 4) & 0x0F));
}

void oledClear() {
if (!oledDisplayPresent) return;
uint8_t empty[16];
memset(empty, 0x00, sizeof(empty));

for (uint8_t page = 0; page < OLED_PAGE_COUNT; page++) {
    oledSetCursor(page, 0);
    for (uint8_t block = 0; block < (OLED_WIDTH / sizeof(empty)); block++) {
        oledWriteColumns(empty, sizeof(empty));
    }
}

}

void oledPrintText(uint8_t page, uint8_t column, const char* text) {
if (!oledDisplayPresent) return;
oledSetCursor(page, column);

while (*text) {
    uint8_t buffer[6];
    const uint8_t* glyph = getOledGlyph(*text++);
    for (uint8_t i = 0; i < 5; i++) buffer[i] = glyph[i];
    buffer[5] = 0x00;
    oledWriteColumns(buffer, sizeof(buffer));
}

}

void oledPrintTextInverted(uint8_t page, uint8_t column, const char* text) {
if (!oledDisplayPresent) return;
oledSetCursor(page, column);

while (*text) {
    uint8_t buffer[6];
    const uint8_t* glyph = getOledGlyph(*text++);
    for (uint8_t i = 0; i < 5; i++) buffer[i] = ~glyph[i];
    buffer[5] = 0xFF;
    oledWriteColumns(buffer, sizeof(buffer));
}

}

void oledClearPageRange(uint8_t pageStart, uint8_t pageCount) {
if (!oledDisplayPresent) return;
uint8_t empty[16];
memset(empty, 0x00, sizeof(empty));

for (uint8_t page = pageStart; page < (pageStart + pageCount) && page < OLED_PAGE_COUNT; page++) {
    oledSetCursor(page, 0);
    for (uint8_t block = 0; block < (OLED_WIDTH / sizeof(empty)); block++) {
        oledWriteColumns(empty, sizeof(empty));
    }
}

}

void oledClearArea(uint8_t pageStart, uint8_t pageCount, uint8_t columnStart, uint8_t columnCount) {
if (!oledDisplayPresent) return;
uint8_t empty[16];
memset(empty, 0x00, sizeof(empty));

for (uint8_t page = pageStart; page < (pageStart + pageCount) && page < OLED_PAGE_COUNT; page++) {
    uint8_t remaining = columnCount;
    uint8_t column = columnStart;
    while (remaining > 0) {
        uint8_t chunk = remaining > sizeof(empty) ? sizeof(empty) : remaining;
        oledSetCursor(page, column);
        oledWriteColumns(empty, chunk);
        column += chunk;
        remaining -= chunk;
    }
}

}

void oledFillArea(uint8_t pageStart, uint8_t pageCount, uint8_t columnStart, uint8_t columnCount, uint8_t value) {
if (!oledDisplayPresent) return;
uint8_t block[16];
memset(block, value, sizeof(block));

for (uint8_t page = pageStart; page < (pageStart + pageCount) && page < OLED_PAGE_COUNT; page++) {
    uint8_t remaining = columnCount;
    uint8_t column = columnStart;
    while (remaining > 0) {
        uint8_t chunk = remaining > sizeof(block) ? sizeof(block) : remaining;
        oledSetCursor(page, column);
        oledWriteColumns(block, chunk);
        column += chunk;
        remaining -= chunk;
    }
}

}

void oledPrintText3x(uint8_t pageTop, uint8_t column, const char* text) {
if (!oledDisplayPresent) return;

while (*text) {
    const uint8_t* glyph = getOledGlyph(*text++);
    uint8_t pageBuffers[2][18];
    memset(pageBuffers, 0x00, sizeof(pageBuffers));

    for (uint8_t i = 0; i < 5; i++) {
        uint8_t src = glyph[i];
        uint16_t expanded = 0;

        for (uint8_t bit = 0; bit < 8; bit++) {
            if (src & (1 << bit)) {
                uint8_t expandedBit = bit * 2;
                expanded |= (uint16_t(0x03) << expandedBit);
            }
        }

        for (uint8_t page = 0; page < 2; page++) {
            uint8_t pageByte = (expanded >> (page * 8)) & 0xFF;
            uint8_t offset = i * 3;
            pageBuffers[page][offset]     = pageByte;
            pageBuffers[page][offset + 1] = pageByte;
            pageBuffers[page][offset + 2] = pageByte;
        }
    }

    for (uint8_t page = 0; page < 2 && (pageTop + page) < OLED_PAGE_COUNT; page++) {
        oledSetCursor(pageTop + page, column);
        oledWriteColumns(pageBuffers[page], sizeof(pageBuffers[page]));
    }

    column += sizeof(pageBuffers[0]);
}

}

void oledDrawDivider(uint8_t page, uint8_t pattern) {
if (!oledDisplayPresent || page >= OLED_PAGE_COUNT) return;
oledSetCursor(page, 0);
uint8_t line[16];
memset(line, pattern, sizeof(line));
for (uint8_t block = 0; block < (OLED_WIDTH / sizeof(line)); block++) {
oledWriteColumns(line, sizeof(line));
}
}

void oledDrawVerticalDivider(uint8_t column, uint8_t pattern) {
if (!oledDisplayPresent || column >= OLED_WIDTH) return;
for (uint8_t page = 0; page < OLED_PAGE_COUNT; page++) {
oledSetCursor(page, column);
oledWriteColumns(&pattern, 1);
}
}

void oledFormatTemperature(float value, char* outBuffer, size_t outSize) {
int scaledValue = (int)(value >= 0.0 ? (value * 10.0f + 0.5f) : (value * 10.0f - 0.5f));
int absScaled = scaledValue >= 0 ? scaledValue : -scaledValue;
int wholePart = absScaled / 10;
int decimalPart = absScaled % 10;

if (scaledValue < 0) {
    snprintf(outBuffer, outSize, "-%d.%dC", wholePart, decimalPart);
} else {
    snprintf(outBuffer, outSize, "%d.%dC", wholePart, decimalPart);
}

}

uint8_t oledCenteredColumn3x(const char* text) {
size_t len = strlen(text);
int width = (int)len * 18;
int column = (OLED_WIDTH - width) / 2;
if (column < 0) column = 0;
return (uint8_t)column;
}

uint8_t oledCenteredColumn2x(const char* text) {
size_t len = strlen(text);
int width = (int)len * 12;
int column = (OLED_WIDTH - width) / 2;
if (column < 0) column = 0;
return (uint8_t)column;
}

uint8_t oledCenteredColumn1xArea(const char* text, uint8_t startColumn, uint8_t areaWidth) {
int width = (int)strlen(text) * 6;
int column = startColumn + ((int)areaWidth - width) / 2;
if (column < startColumn) column = startColumn;
return (uint8_t)column;
}

void oledPrintText2x(uint8_t pageTop, uint8_t column, const char* text) {
if (!oledDisplayPresent) return;

while (*text) {
    const uint8_t* glyph = getOledGlyph(*text++);
    uint8_t pageBuffers[2][12];
    memset(pageBuffers, 0x00, sizeof(pageBuffers));

    for (uint8_t i = 0; i < 5; i++) {
        uint8_t src = glyph[i];
        uint16_t expanded = 0;

        for (uint8_t bit = 0; bit < 8; bit++) {
            if (src & (1 << bit)) {
                uint8_t expandedBit = bit * 2;
                expanded |= (uint16_t(0x03) << expandedBit);
            }
        }

        for (uint8_t page = 0; page < 2; page++) {
            uint8_t pageByte = (expanded >> (page * 8)) & 0xFF;
            uint8_t offset = i * 2;
            pageBuffers[page][offset] = pageByte;
            pageBuffers[page][offset + 1] = pageByte;
        }
    }

    for (uint8_t page = 0; page < 2 && (pageTop + page) < OLED_PAGE_COUNT; page++) {
        oledSetCursor(pageTop + page, column);
        oledWriteColumns(pageBuffers[page], sizeof(pageBuffers[page]));
    }

    column += sizeof(pageBuffers[0]);
}

}

void oledDrawFanIcon(uint8_t page, uint8_t column, bool active) {
if (!oledDisplayPresent || page >= OLED_PAGE_COUNT) return;

static const uint8_t fanOn[8]  = { 0x24, 0x66, 0x3C, 0x18, 0x18, 0x3C, 0x66, 0x24 };
static const uint8_t fanOff[8] = { 0x00, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x00 };

oledSetCursor(page, column);
oledWriteColumns(active ? fanOn : fanOff, 8);

}

bool oledGetAlertText(char* topText, size_t topSize, char* bottomText, size_t bottomSize) {
bool has220V = readRaw220VPresent();
bool has12V = readRaw12VPresent();

if (has220V && has12V) {
    snprintf(topText, topSize, "220V");
    snprintf(bottomText, bottomSize, "+12V");
    return true;
}

if (isFlameLostAfterStable() || isGasOpenNoFlame()) {
    snprintf(topText, topSize, "SIN");
    snprintf(bottomText, bottomSize, "LLAMA");
    return true;
}

if (gasFault) {
    snprintf(topText, topSize, "FALLO");
    snprintf(bottomText, bottomSize, "GAS");
    return true;
}

if (fridgeSensorMissing) {
    snprintf(topText, topSize, "FRIGO");
    snprintf(bottomText, bottomSize, "ERR");
    return true;
}

if (freezerSensorMissing) {
    snprintf(topText, topSize, "CONG");
    snprintf(bottomText, bottomSize, "ERR");
    return true;
}

if (rearSensorMissing) {
    snprintf(topText, topSize, "TRAS");
    snprintf(bottomText, bottomSize, "ERR");
    return true;
}

return false;

}

unsigned long getGasBlockRemainingSeconds() {
if (!gasBlocked) return 0;
unsigned long elapsed = millis() - gasBlockTimer;
if (elapsed >= GAS_BLOCK_DURATION_MS) return 0;
return (GAS_BLOCK_DURATION_MS - elapsed) / 1000UL;
}

void formatCountdownMmSs(unsigned long totalSeconds, char* outBuffer, size_t outSize) {
unsigned long minutes = totalSeconds / 60UL;
unsigned long seconds = totalSeconds % 60UL;
snprintf(outBuffer, outSize, "%02lu:%02lu", minutes, seconds);
}

bool oledStateLineIsCritical(const char* line) {
return strncmp(line, "GAS BLOQ", 8) == 0 ||
strncmp(line, "FALLO GAS", 9) == 0 ||
strncmp(line, "SIN LLAMA", 9) == 0;
}

void oledDrawNormalLayout() {
oledClear();
oledLastFridgeRounded = 32767;
oledLastFreezerRounded = 32767;

oledPrintText(0, oledCenteredColumn1xArea("TEMPERATURAS", OLED_TEMP_AREA_START, OLED_TEMP_AREA_WIDTH), "TEMPERATURAS");
oledPrintText(0, oledCenteredColumn1xArea("ENERGIA", OLED_MODE_AREA_START, OLED_MODE_AREA_WIDTH), "ENERGIA");
for (uint8_t page = 0; page < 4; page++) {
    oledSetCursor(page, 74);
    uint8_t solid = 0xFF;
    oledWriteColumns(&solid, 1);
}
oledPrintText(4, oledCenteredColumn1xArea("ESTADOS", 0, OLED_WIDTH), "ESTADOS");

}

void initAuxOledDisplay() {
pinMode(OLED_PIN_CS, OUTPUT);
pinMode(OLED_PIN_DC, OUTPUT);
pinMode(OLED_PIN_RES, OUTPUT);

digitalWrite(OLED_PIN_CS, HIGH);
digitalWrite(OLED_PIN_DC, HIGH);
digitalWrite(OLED_PIN_RES, HIGH);

SPI.begin();

digitalWrite(OLED_PIN_RES, LOW);
delay(10);
digitalWrite(OLED_PIN_RES, HIGH);
delay(10);

oledDisplayPresent = true;

oledSendCommand(0xAE);
oledSendCommand(0xD5); oledSendCommand(0x80);
oledSendCommand(0xA8); oledSendCommand(0x3F);
oledSendCommand(0xD3); oledSendCommand(0x00);
oledSendCommand(0x40);
oledSendCommand(0x8D); oledSendCommand(0x14);
oledSendCommand(0x20); oledSendCommand(0x02);
oledSendCommand(0xA1);
oledSendCommand(0xC8);
oledSendCommand(0xDA); oledSendCommand(0x12);
oledSendCommand(0x81); oledSendCommand(0xCF);
oledSendCommand(0xD9); oledSendCommand(0xF1);
oledSendCommand(0xDB); oledSendCommand(0x40);
oledSendCommand(0xA4);
oledSendCommand(0xA6);
oledSendCommand(0xAF);

oledDrawNormalLayout();
oledShowingAlert = false;
oledLastAlertTop[0] = '\0';
oledLastAlertBottom[0] = '\0';
Serial.println(F("[OLED SPI] Inicializada."));

}

void updateAuxOledDisplay() {
if (!oledDisplayPresent) return;
if (millis() - oledRefreshTimer < 500) return;
oledRefreshTimer = millis();

char fridgeText[10];
char freezerText[10];
char rearText[10];
char targetText[10];
char fridgeLine[18];
char freezerLine[18];
char rearLine[18];
char stateLine1[22];
char stateLine2[22];
char stateLine3[22];
int roundedFridge = (int)(fridgeTemp >= 0.0 ? (fridgeTemp + 0.5) : (fridgeTemp - 0.5));
int roundedFreezer = (int)(freezerTemp >= 0.0 ? (freezerTemp + 0.5) : (freezerTemp - 0.5));
bool showAdjust = (millis() - tempSettingTimer) <= 3000UL;
uint8_t modeBoxStart = OLED_MODE_AREA_START + 4;
uint8_t modeBoxWidth = OLED_MODE_AREA_WIDTH - 8;
unsigned long gasBlockRemainingSeconds = getGasBlockRemainingSeconds();
char countdownText[8];

oledFormatTemperature(fridgeTemp, fridgeText, sizeof(fridgeText));
oledFormatTemperature(freezerTemp, freezerText, sizeof(freezerText));
oledFormatTemperature(rearTemp, rearText, sizeof(rearText));
oledFormatTemperature(targetTemp, targetText, sizeof(targetText));
formatCountdownMmSs(gasBlockRemainingSeconds, countdownText, sizeof(countdownText));
snprintf(fridgeLine, sizeof(fridgeLine), "FRIGO ->%s", fridgeText);
snprintf(freezerLine, sizeof(freezerLine), "CONG  ->%s", freezerText);
snprintf(rearLine, sizeof(rearLine), "T.COND->%s", rearText);
stateLine1[0] = '\0';
stateLine2[0] = '\0';
stateLine3[0] = '\0';

uint8_t stateIndex = 0;
if (gasBlocked && stateIndex < 3) {
    snprintf(stateIndex == 0 ? stateLine1 : (stateIndex == 1 ? stateLine2 : stateLine3), 22, "GAS BLOQ %s", countdownText);
    stateIndex++;
}
if (readRaw220VPresent() && readRaw12VPresent() && stateIndex < 3) {
    strncpy(stateIndex == 0 ? stateLine1 : (stateIndex == 1 ? stateLine2 : stateLine3), "220V + 12V", 21);
    if (stateIndex == 0) stateLine1[21] = '\0';
    else if (stateIndex == 1) stateLine2[21] = '\0';
    else stateLine3[21] = '\0';
    stateIndex++;
}
if ((isFlameLostAfterStable() || isGasOpenNoFlame()) && stateIndex < 3) {
    strncpy(stateIndex == 0 ? stateLine1 : (stateIndex == 1 ? stateLine2 : stateLine3), "SIN LLAMA", 21);
    if (stateIndex == 0) stateLine1[21] = '\0';
    else if (stateIndex == 1) stateLine2[21] = '\0';
    else stateLine3[21] = '\0';
    stateIndex++;
}
if (gasFault && stateIndex < 3) {
    strncpy(stateIndex == 0 ? stateLine1 : (stateIndex == 1 ? stateLine2 : stateLine3), "FALLO GAS", 21);
    if (stateIndex == 0) stateLine1[21] = '\0';
    else if (stateIndex == 1) stateLine2[21] = '\0';
    else stateLine3[21] = '\0';
    stateIndex++;
}
if (fridgeSensorMissing && stateIndex < 3) {
    strncpy(stateIndex == 0 ? stateLine1 : (stateIndex == 1 ? stateLine2 : stateLine3), "FRIGO ERR", 21);
    if (stateIndex == 0) stateLine1[21] = '\0';
    else if (stateIndex == 1) stateLine2[21] = '\0';
    else stateLine3[21] = '\0';
    stateIndex++;
}
if (freezerSensorMissing && stateIndex < 3) {
    strncpy(stateIndex == 0 ? stateLine1 : (stateIndex == 1 ? stateLine2 : stateLine3), "CONG ERR", 21);
    if (stateIndex == 0) stateLine1[21] = '\0';
    else if (stateIndex == 1) stateLine2[21] = '\0';
    else stateLine3[21] = '\0';
    stateIndex++;
}
if (rearSensorMissing && stateIndex < 3) {
    strncpy(stateIndex == 0 ? stateLine1 : (stateIndex == 1 ? stateLine2 : stateLine3), "T.COND ERR", 21);
    if (stateIndex == 0) stateLine1[21] = '\0';
    else if (stateIndex == 1) stateLine2[21] = '\0';
    else stateLine3[21] = '\0';
    stateIndex++;
}
if (condActive && stateIndex < 3) {
    strncpy(stateIndex == 0 ? stateLine1 : (stateIndex == 1 ? stateLine2 : stateLine3), "CONDENSACION ON", 21);
    if (stateIndex == 0) stateLine1[21] = '\0';
    else if (stateIndex == 1) stateLine2[21] = '\0';
    else stateLine3[21] = '\0';
    stateIndex++;
}
if (fanActive && stateIndex < 3) {
    strncpy(stateIndex == 0 ? stateLine1 : (stateIndex == 1 ? stateLine2 : stateLine3), "VENTILADOR ON", 21);
    if (stateIndex == 0) stateLine1[21] = '\0';
    else if (stateIndex == 1) stateLine2[21] = '\0';
    else stateLine3[21] = '\0';
    stateIndex++;
}
if (showAdjust && stateIndex < 3) {
    snprintf(stateIndex == 0 ? stateLine1 : (stateIndex == 1 ? stateLine2 : stateLine3), 22, "AJUSTE %s", targetText);
    stateIndex++;
}

oledClearArea(1, 3, OLED_TEMP_AREA_START, OLED_TEMP_AREA_WIDTH);
oledPrintText(1, oledCenteredColumn1xArea(fridgeLine, OLED_TEMP_AREA_START, OLED_TEMP_AREA_WIDTH), fridgeLine);
oledPrintText(2, oledCenteredColumn1xArea(freezerLine, OLED_TEMP_AREA_START, OLED_TEMP_AREA_WIDTH), freezerLine);
oledPrintText(3, oledCenteredColumn1xArea(rearLine, OLED_TEMP_AREA_START, OLED_TEMP_AREA_WIDTH), rearLine);

oledClearArea(1, 3, OLED_MODE_AREA_START, OLED_MODE_AREA_WIDTH);
if (currentMode == MODE_220V) oledFillArea(1, 1, modeBoxStart, modeBoxWidth, 0xFF);
if (currentMode == MODE_12V) oledFillArea(2, 1, modeBoxStart, modeBoxWidth, 0xFF);
if (currentMode == MODE_GAS) oledFillArea(3, 1, modeBoxStart, modeBoxWidth, 0xFF);

if (currentMode == MODE_220V) oledPrintTextInverted(1, oledCenteredColumn1xArea("220V", modeBoxStart, modeBoxWidth), "220V");
else oledPrintText(1, oledCenteredColumn1xArea("220V", modeBoxStart, modeBoxWidth), "220V");

if (currentMode == MODE_12V) oledPrintTextInverted(2, oledCenteredColumn1xArea("12V", modeBoxStart, modeBoxWidth), "12V");
else oledPrintText(2, oledCenteredColumn1xArea("12V", modeBoxStart, modeBoxWidth), "12V");

if (currentMode == MODE_GAS) oledPrintTextInverted(3, oledCenteredColumn1xArea("GAS", modeBoxStart, modeBoxWidth), "GAS");
else oledPrintText(3, oledCenteredColumn1xArea("GAS", modeBoxStart, modeBoxWidth), "GAS");

oledClearArea(5, 3, 0, OLED_WIDTH);
if (stateLine1[0] != '\0') {
    if (oledStateLineIsCritical(stateLine1)) {
        oledFillArea(5, 1, 0, OLED_WIDTH, 0xFF);
        oledPrintTextInverted(5, oledCenteredColumn1xArea(stateLine1, 0, OLED_WIDTH), stateLine1);
    } else {
        oledPrintText(5, oledCenteredColumn1xArea(stateLine1, 0, OLED_WIDTH), stateLine1);
    }
}
if (stateLine2[0] != '\0') {
    if (oledStateLineIsCritical(stateLine2)) {
        oledFillArea(6, 1, 0, OLED_WIDTH, 0xFF);
        oledPrintTextInverted(6, oledCenteredColumn1xArea(stateLine2, 0, OLED_WIDTH), stateLine2);
    } else {
        oledPrintText(6, oledCenteredColumn1xArea(stateLine2, 0, OLED_WIDTH), stateLine2);
    }
}
if (stateLine3[0] != '\0') {
    if (oledStateLineIsCritical(stateLine3)) {
        oledFillArea(7, 1, 0, OLED_WIDTH, 0xFF);
        oledPrintTextInverted(7, oledCenteredColumn1xArea(stateLine3, 0, OLED_WIDTH), stateLine3);
    } else {
        oledPrintText(7, oledCenteredColumn1xArea(stateLine3, 0, OLED_WIDTH), stateLine3);
    }
}

oledLastFridgeRounded = roundedFridge;
oledLastFreezerRounded = roundedFreezer;

}

// ==========================================
// SETUP
// ==========================================
void setup() {
Serial.begin(9600);
Serial1.begin(9600);
Wire.begin();
Wire.setClock(100000);

#if defined(ARDUINO_ARCH_AVR)
Wire.setWireTimeout(3000, true);
#endif

Serial.println(F("--- INICIO SISTEMA TRIVALENTE (SIN PANTALLA LOCAL) ---"));

pinMode(PIN_IN_220V, INPUT_PULLUP);
pinMode(PIN_IN_12V, INPUT_PULLUP);
prevHas12V = (digitalRead(PIN_IN_12V) == LOW);

pinMode(PIN_RELAY_220V, OUTPUT);
pinMode(PIN_RELAY_12V, OUTPUT);
pinMode(PIN_RELAY_GAS, OUTPUT);
pinMode(PIN_RELAY_FAN, OUTPUT);
pinMode(PIN_RELAY_COND, OUTPUT);
digitalWrite(PIN_RELAY_220V, RELAY_OFF);
digitalWrite(PIN_RELAY_12V, RELAY_OFF);
digitalWrite(PIN_RELAY_GAS, RELAY_OFF);
digitalWrite(PIN_RELAY_FAN, RELAY_OFF);
digitalWrite(PIN_RELAY_COND, RELAY_OFF);

pinMode(PIN_LED_STATUS, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);

digitalWrite(PIN_LED_STATUS, HIGH);

digitalWrite(PIN_BUZZER, HIGH); delay(100); digitalWrite(PIN_BUZZER, LOW);
delay(50);
digitalWrite(PIN_BUZZER, HIGH); delay(100); digitalWrite(PIN_BUZZER, LOW);

float storedTemp = 0.0;
EEPROM.get(ADDR_TARGET_TEMP, storedTemp);
if (storedTemp >= TEMP_MIN && storedTemp <= TEMP_MAX) targetTemp = storedTemp;
EEPROM.get(ADDR_TOTAL_RUNTIME, fridgeRunTotalSeconds);
EEPROM.get(ADDR_PARTIAL_RUNTIME, fridgeRunPartialSeconds);
if (fridgeRunTotalSeconds > 4294967000UL) fridgeRunTotalSeconds = 0;
if (fridgeRunPartialSeconds > 4294967000UL) fridgeRunPartialSeconds = 0;

sensors.begin();
sensors.setWaitForConversion(false);
printDetectedSensorAddresses();
initAuxOledDisplay();
Serial.println(F("[ESP32-LINK] Serial1 activo a 9600 bps por D18(TX1) y D19(RX1)."));

Serial.println(F("Iniciado con exito."));
statusTimer = millis();
bootTime = millis();
runtimeTick = millis();
runtimeSaveTimer = millis();

}

void loop() {
receiveFromESP32();
readSensors();

determinePriority();
handleModeSwitching();
controlThermostat();
controlExteriorFans();      
controlCondResistance();    
handleAudioAlarms();
updateRuntimeCounters();

updateIndicators();
updateAuxOledDisplay();

if (millis() - esp32TxTimer >= 500) {
    sendStatusToESP32();
    esp32TxTimer = millis();
}

printDiagSerial();

delay(50);

}

// ==========================================
// FUNCIONES DE LECTURA Y CONTROL
// ==========================================
void readSensors() {
if (!tempRequested) {
sensors.requestTemperatures();
tempRequested = true;
sensorTimer = millis();
}
if (tempRequested && (millis() - sensorTimer >= 800)) {
fridgeTemp = readManagedTemperature(fridgeSensorAddress, 0);
freezerTemp = readManagedTemperature(freezerSensorAddress, 1);
rearTemp = readManagedTemperature(rearSensorAddress, 2);

    fridgeSensorMissing = (fridgeTemp == DEVICE_DISCONNECTED_C);
    freezerSensorMissing = (freezerTemp == DEVICE_DISCONNECTED_C);
    rearSensorMissing = (rearTemp == DEVICE_DISCONNECTED_C);
    
    if (fridgeTemp  == DEVICE_DISCONNECTED_C) fridgeTemp  = targetTemp + HYSTERESIS + 0.5f;
    if (freezerTemp == DEVICE_DISCONNECTED_C) freezerTemp = 0.0;
    if (rearTemp    == DEVICE_DISCONNECTED_C) rearTemp    = 0.0;
    tempRequested = false;
}

}

void determinePriority() {
bool has220V = readRaw220VPresent();
bool has12V = readRaw12VPresent();

if (has220V || has12V) {
    if (gasBlocked || gasBlockTimer != 0) {
        gasBlocked = false;
        gasBlockTimer = 0;
        Serial.println(F("Bloqueo GAS cancelado por retorno de 12V/220V."));
    }
}

if (prevHas12V && !has12V && !has220V) {
    gasBlocked = true;
    gasBlockTimer = millis();
    Serial.println(F("Motor OFF. Bloqueo 20m."));
}
prevHas12V = has12V;

if (gasBlocked && (millis() - gasBlockTimer >= GAS_BLOCK_DURATION_MS)) {
    gasBlocked = false;
    gasBlockTimer = 0;
}

EnergyMode requestedMode = MODE_NONE;
if (has220V) requestedMode = MODE_220V;
else if (has12V) requestedMode = MODE_12V;
else requestedMode = gasBlocked ? MODE_NONE : MODE_GAS;

if (requestedMode != targetMode) {
    targetMode = requestedMode;
    if (currentMode != targetMode) {
        currentMode = MODE_TRANSITION;
        gasFault = false;
        gasRetryCount = 0;
        flameLossTimer = 0;
        flameReachedStableOnce = false;
        turnOffAllRelays();
        switchTimer = millis();
    }
}

}

void handleModeSwitching() {
if (currentMode == MODE_TRANSITION && (millis() - switchTimer >= SWITCH_DELAY_MS)) {
currentMode = targetMode;
}
}

void controlThermostat() {
if (fridgeTemp >= (targetTemp + HYSTERESIS)) coolerActive = true;
else if (fridgeTemp <= (targetTemp - HYSTERESIS)) coolerActive = false;

if (currentMode == MODE_TRANSITION) {
    turnOffAllRelays();
    gasIgnitionTimer = 0;
    flameLossTimer = 0;
} else if (coolerActive) {
    switch (currentMode) {
        case MODE_220V:
            digitalWrite(PIN_RELAY_220V, RELAY_ON); digitalWrite(PIN_RELAY_12V, RELAY_OFF); digitalWrite(PIN_RELAY_GAS, RELAY_OFF);
            break;
        case MODE_12V:
            digitalWrite(PIN_RELAY_220V, RELAY_OFF); digitalWrite(PIN_RELAY_12V, RELAY_ON); digitalWrite(PIN_RELAY_GAS, RELAY_OFF);
            break;
        case MODE_GAS:
            digitalWrite(PIN_RELAY_220V, RELAY_OFF); digitalWrite(PIN_RELAY_12V, RELAY_OFF);
            if (gasFault) {
                digitalWrite(PIN_RELAY_GAS, RELAY_OFF);
                gasFlameStable = false;
                flameReachedStableOnce = false;
            } else if (gasWaitingRetry) {
                digitalWrite(PIN_RELAY_GAS, RELAY_OFF);
                flameLossTimer = 0;
                gasFlameStable = false;
                if (millis() - gasRetryTimer >= GAS_PURGE_DURATION_MS) {
                    gasWaitingRetry = false;
                    flameReachedStableOnce = false;
                }
            } else {
                digitalWrite(PIN_RELAY_GAS, RELAY_ON);
                if (gasIgnitionTimer == 0) gasIgnitionTimer = millis();
                unsigned long elapsedIg = millis() - gasIgnitionTimer;
                if (GAS_PRUEBA_IGNORA_LLAMA) {
                    if (elapsedIg >= GAS_FLAME_STABLE_MS) {
                        gasFlameStable = true;
                        flameReachedStableOnce = true;
                    }
                    flameLossTimer = 0;
                } else {
                    bool flameNow = readFlameOK();
                    if (!flameNow) {
                        gasFlameStable = false;
                        if (elapsedIg >= GAS_IGNITION_TIMEOUT_MS || flameReachedStableOnce) {
                            if (flameLossTimer == 0) flameLossTimer = millis();
                            if (millis() - flameLossTimer >= FLAME_LOSS_CONFIRM_MS) {
                                digitalWrite(PIN_RELAY_GAS, RELAY_OFF);
                                gasRetryCount++;
                                flameLossTimer = 0;
                                if (gasRetryCount >= GAS_MAX_RETRIES) {
                                    gasFault = true;
                                    flameReachedStableOnce = false;
                                    Serial.println(F("Fallo GAS"));
                                }
                                else { gasWaitingRetry = true; gasRetryTimer = millis(); gasIgnitionTimer = 0; }
                            }
                        } else {
                            flameLossTimer = 0;
                        }
                    } else {
                        flameLossTimer = 0;
                        if (elapsedIg >= GAS_FLAME_STABLE_MS) {
                            gasRetryCount = 0;
                            gasFlameStable = true;
                            flameReachedStableOnce = true;
                        }
                    }
                }
            }
            break;
        default:
            turnOffAllRelays();
            break;
    }
} else {
    turnOffAllRelays();
    gasIgnitionTimer = 0;
    flameLossTimer = 0;
}

}

void controlExteriorFans() {
if (fanManualMode) {
fanActive = fanManualState;
if (!fanManualState && rearTemp <= FAN_REAR_OFF_TEMP) {
fanManualMode = false;
}
} else {
if (rearTemp >= FAN_REAR_ON_TEMP) fanActive = true;
if (rearTemp <= FAN_REAR_OFF_TEMP) fanActive = false;
}
digitalWrite(PIN_RELAY_FAN, fanActive ? RELAY_ON : RELAY_OFF);
}

void controlCondResistance() {
if (condActive) {
if (condOnTimer > 0 && (millis() - condOnTimer >= COND_AUTO_OFF_MS)) {
condActive = false;
condOnTimer = 0;
Serial.println(F("[COND] Auto-OFF: 2 horas cumplidas de seguridad."));
}
}
digitalWrite(PIN_RELAY_COND, condActive ? RELAY_ON : RELAY_OFF);
}

void handleAudioAlarms() {
static unsigned long buzzerTimer = 0;
static bool buzzerState = false;

bool has220V = readRaw220VPresent();
bool has12V = readRaw12VPresent();
if (has220V && has12V) {
    if (millis() - buzzerTimer >= 100) { buzzerState = !buzzerState; digitalWrite(PIN_BUZZER, buzzerState); buzzerTimer = millis(); }
    return;
}

bool noFlameImmediate = isGasOpenNoFlame();
bool flameLost = isFlameLostAfterStable();
if (flameLost) {
    if (millis() - buzzerTimer >= 300) { buzzerState = !buzzerState; digitalWrite(PIN_BUZZER, buzzerState); buzzerTimer = millis(); }
} else if (gasFault) {
    if (millis() - buzzerTimer >= 500) { buzzerState = !buzzerState; digitalWrite(PIN_BUZZER, buzzerState); buzzerTimer = millis(); }
} else if (noFlameImmediate) {
    if (millis() - buzzerTimer >= 400) { buzzerState = !buzzerState; digitalWrite(PIN_BUZZER, buzzerState); buzzerTimer = millis(); }
} else {
    digitalWrite(PIN_BUZZER, LOW);
}

}

void updateIndicators() {
digitalWrite(PIN_LED_STATUS, HIGH);
}

void turnOffAllRelays() {
digitalWrite(PIN_RELAY_220V, RELAY_OFF);
digitalWrite(PIN_RELAY_12V, RELAY_OFF);
digitalWrite(PIN_RELAY_GAS, RELAY_OFF);
digitalWrite(PIN_RELAY_FAN, RELAY_OFF);
}

// ==========================================
// COMUNICACION CON ESP32 (Serial1)
// ==========================================
void sendStatusToESP32() {
bool has220V = readRaw220VPresent();
bool has12V = readRaw12VPresent();
bool flameOK = GAS_PRUEBA_IGNORA_LLAMA ? true : readFlameOK();
bool fridgeRunning = isFridgeRunningNow();
unsigned long gasBlockTimeLeftSeconds = getGasBlockRemainingSeconds();

unsigned long condTimeLeftSeconds = 0;
if (condActive && condOnTimer > 0) {
    unsigned long elapsed = millis() - condOnTimer;
    unsigned long condTimeLeftMs = (elapsed < COND_AUTO_OFF_MS) ? (COND_AUTO_OFF_MS - elapsed) : 0;
    condTimeLeftSeconds = condTimeLeftMs / 1000UL;
}

const char* modeStr = "NONE";
if (currentMode == MODE_220V) modeStr = "220V";
else if (currentMode == MODE_12V) modeStr = "12V";
else if (currentMode == MODE_GAS) modeStr = "GAS";
else if (currentMode == MODE_TRANSITION) modeStr = "TRANS";

Serial1.print(F("STAT,"));
Serial1.print(modeStr);                    Serial1.print(',');
Serial1.print(fridgeTemp, 1);             Serial1.print(',');
Serial1.print(targetTemp, 1);             Serial1.print(',');
Serial1.print(freezerTemp, 1);            Serial1.print(',');
Serial1.print(rearTemp, 1);               Serial1.print(',');
Serial1.print(gasFault ? 1 : 0);          Serial1.print(',');
Serial1.print(gasBlocked ? 1 : 0);        Serial1.print(',');
Serial1.print(gasBlockTimeLeftSeconds);   Serial1.print(',');
Serial1.print(gasRetryCount);             Serial1.print(',');
Serial1.print(fanActive ? 1 : 0);         Serial1.print(',');
Serial1.print(fanManualMode ? 1 : 0);     Serial1.print(',');
Serial1.print(condActive ? 1 : 0);        Serial1.print(',');
Serial1.print(condTimeLeftSeconds);       Serial1.print(',');
Serial1.print(has220V ? 1 : 0);           Serial1.print(',');
Serial1.print(has12V ? 1 : 0);            Serial1.print(',');
Serial1.print(flameOK ? 1 : 0);           Serial1.print(',');
Serial1.print(fridgeRunning ? 1 : 0);     Serial1.print(',');
Serial1.print(fridgeRunTotalSeconds);     Serial1.print(',');
Serial1.println(fridgeRunPartialSeconds);
esp32LastStatusTxMs = millis();
esp32StatusPacketsSent++;

}

void receiveFromESP32() {
while (Serial1.available()) {
char c = (char)Serial1.read();
if (c == '\n') {
esp32RxBuffer.trim();
handleESP32Command(esp32RxBuffer);
esp32RxBuffer = "";
} else if (c != '\r') {
if (esp32RxBuffer.length() < 32) esp32RxBuffer += c;
}
}
}
Y Este es el de la ESP32
#include <SPI.h>
#include <TFT_eSPI.h>
#include <XPT2046_Touchscreen.h>

#define XPT2046_IRQ 36
#define XPT2046_MOSI 32
#define XPT2046_MISO 39
#define XPT2046_CLK 25
#define XPT2046_CS 33

#define Tft_Backlight 21
#define MODO_DEMO 0
const char* FW_ESP32_VERSION = "SERIE-U0";
#define MEGA_SERIAL Serial

SPIClass mySPI(VSPI);
XPT2046_Touchscreen ts(XPT2046_CS, XPT2046_IRQ);
TFT_eSPI tft = TFT_eSPI();

#define COLOR_FONDO TFT_BLACK
#define COLOR_PANEL 0x18C3
#define COLOR_PANEL_INNER 0x0861
#define COLOR_PANEL_HEADER 0x2126
#define COLOR_BOTON_OPCIONES 0x4210
#define COLOR_BOTON_OPCIONES_TEXTO 0x07FF
#define COLOR_BORDE_LUZ TFT_WHITE
#define COLOR_BORDE_SOMBRA 0x39C7
#define COLOR_SOMBRA 0x0000
#define COLOR_TEXTO_SECUNDARIO TFT_LIGHTGREY

int modoActivo = 0;
float tempInterior = 0.0;
float tempObjetivo = 4.0;
float tempCongelador = 0.0;
float tempTrasera = 0.0;
bool gasFault = false;
bool gasBlocked = false;
unsigned long gasBlockTimeLeft = 0;
int gasRetryCount = 0;
bool fanActive = false;
bool fanManualMode = false;
bool condActive = false;
unsigned long condTimeLeft = 0;
bool has220V = false;
bool has12V = false;
bool flameOK = false;
bool fridgeRunning = false;
unsigned long fridgeRunTotalSeconds = 0;
unsigned long fridgeRunPartialSeconds = 0;

int estadoPantalla = 0; // 0 = Principal, 1 = Menú Opciones

unsigned long ultimaInteraccion = 0;
const unsigned long TIEMPO_APAGADO = 60000;
bool pantallaApagada = false;
unsigned long ultimaDemoActualizacion = 0;
unsigned long ultimoTickDemo = 0;
int demoEscena = 0;
bool avisoCable220VActivo = false;
unsigned long ultimoPaqueteMegaMs = 0;
bool comunicacionMegaActiva = false;
const unsigned long TIMEOUT_COM_MEGA_MS = 2500;
unsigned long megaStatPacketsRx = 0;
unsigned long megaRawBytesRx = 0;
char megaLastByteRx = '-';
String bufferSerial = "";
unsigned long megaParseErrors = 0;
unsigned long megaLinesRx = 0;
int megaLastLineLen = 0;

void disenarInterfazEstatica();
void disenarMenuOpciones();
void actualizarDatosPantalla();
void dibujarBarraYTriangulo(float temp);
void dibujarIconosFuentes();
void dibujarAspasVentilador(int x, int y, uint16_t color);
void encenderPantalla();
void apagarPantalla();
void dibujarPanelRelieve(int x, int y, int w, int h, const char* titulo, uint16_t colorTitulo);
void dibujarBotonRelieve(int x, int y, int w, int h, uint16_t fondo, uint16_t texto, const char* etiqueta, int tx, int ty, int textSize);
void aplicarEstadoDemo();
void actualizarModoDemo();
void dibujarAvisoCable220V();
void actualizarEstadoComunicacionMega();
void procesarSerialMega();
void enviarComandoMega(const String& comando);
bool procesarLineaMega(const String& linea);

void setup() {
pinMode(Tft_Backlight, OUTPUT);
digitalWrite(Tft_Backlight, HIGH);

tft.init();
tft.setRotation(1);
tft.invertDisplay(false);
tft.fillScreen(COLOR_FONDO);

mySPI.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS);
ts.begin(mySPI);
ts.setRotation(1);

#if MODO_DEMO
aplicarEstadoDemo();
#endif

MEGA_SERIAL.begin(9600);

disenarInterfazEstatica();
actualizarDatosPantalla();

ultimaInteraccion = millis();
}

void loop() {
// Procesar datos entrantes desde la Mega
#if MODO_DEMO
actualizarModoDemo();
#else
procesarSerialMega();
actualizarEstadoComunicacionMega();
#endif

// Lectura de panel táctil
if (ts.touched()) {
TS_Point p = ts.getPoint();

if (pantallaApagada) {
  encenderPantalla();
  ultimaInteraccion = millis(); 
  delay(300); 
  return; 
}

ultimaInteraccion = millis();

int x = map(p.x, 200, 3800, 0, 320);
int y = map(p.y, 200, 3800, 0, 240);

if (estadoPantalla == 0) {
  if (x > 168 && x < 213 && y > 126 && y < 171) {
    if (tempObjetivo > -14.0) { 
      tempObjetivo -= 0.5; 

#if !MODO_DEMO
enviarComandoMega(String("TEMP,") + String(tempObjetivo, 1));
#endif
actualizarDatosPantalla();
}
tft.drawRoundRect(168, 126, 45, 45, 4, TFT_WHITE);
delay(150);
tft.drawRoundRect(168, 126, 45, 45, 4, TFT_DARKGREY);
}

  if (x > 262 && x < 307 && y > 126 && y < 171) {
    if (tempObjetivo < 20.0) { 
      tempObjetivo += 0.5; 

#if !MODO_DEMO
enviarComandoMega(String("TEMP,") + String(tempObjetivo, 1));
#endif
actualizarDatosPantalla();
}
tft.drawRoundRect(262, 126, 45, 45, 4, TFT_WHITE);
delay(150);
tft.drawRoundRect(262, 126, 45, 45, 4, TFT_DARKGREY);
}

  if (x > 162 && x < 310 && y > 186 && y < 234) {
    estadoPantalla = 1;
    tft.fillScreen(COLOR_FONDO);
    disenarMenuOpciones();
    delay(300); 
  }

} else if (estadoPantalla == 1) {
  // Volver
  if (x > 20 && x < 300 && y > 190 && y < 230) {
    estadoPantalla = 0;
    tft.fillScreen(COLOR_FONDO);
    disenarInterfazEstatica();
    actualizarDatosPantalla();
    delay(300); 
  }
  
  // Reset Arduino Mega
  if (x > 220 && x < 305 && y > 43 && y < 75) {
    tft.fillScreen(COLOR_FONDO);
    tft.setTextSize(2);
    tft.setTextColor(TFT_RED, COLOR_FONDO);

#if MODO_DEMO
tft.drawString("MODO DEMO", 80, 85);
tft.setTextSize(1);
tft.setTextColor(TFT_WHITE, COLOR_FONDO);
tft.drawString("Reset Mega desactivado", 72, 120);
#else
tft.drawString("REINICIANDO MEGA...", 30, 100);
enviarComandoMega("RESET_MEGA");
#endif
delay(600);
disenarMenuOpciones();
}

  // Reset Total
  if (x > 255 && x < 295 && y > 80 && y < 106) {
    fridgeRunTotalSeconds = 0;
    fridgeRunPartialSeconds = 0;

#if !MODO_DEMO
enviarComandoMega("TOTAL_RESET");
#endif
disenarMenuOpciones();
delay(200);
}

  // Reset Parcial
  if (x > 255 && x < 295 && y > 115 && y < 141) {
    fridgeRunPartialSeconds = 0;

#if !MODO_DEMO
enviarComandoMega("PARTIAL_RESET");
#endif
disenarMenuOpciones();
delay(200);
}

  // Toggle Resistencia IN5
  if (x > 200 && x < 295 && y > 146 && y < 178) {
    bool newCondState = !condActive;
    condActive = newCondState;

#if !MODO_DEMO
enviarComandoMega(String("COND,") + String(newCondState ? 1 : 0));
#endif
disenarMenuOpciones();
delay(200);
}
}
}

if (!pantallaApagada && (millis() - ultimaInteraccion >= TIEMPO_APAGADO)) {
apagarPantalla();
}
}

void aplicarEstadoDemo() {
switch (demoEscena) {
case 0:
modoActivo = 0;
has220V = false;
has12V = false;
gasFault = false;
gasBlocked = false;
flameOK = false;
fanActive = false;
fridgeRunning = false;
tempInterior = 7.5;
tempCongelador = -10.0;
tempTrasera = 19.0;
break;
case 1:
modoActivo = 1;
has220V = true;
has12V = false;
gasFault = false;
gasBlocked = false;
flameOK = false;
fanActive = false;
fridgeRunning = true;
tempInterior = 5.0;
tempCongelador = -14.5;
tempTrasera = 28.0;
break;
case 2:
modoActivo = 2;
has220V = false;
has12V = true;
gasFault = false;
gasBlocked = false;
flameOK = false;
fanActive = true;
fridgeRunning = true;
tempInterior = 4.2;
tempCongelador = -16.0;
tempTrasera = 32.0;
break;
case 3:
modoActivo = 3;
has220V = false;
has12V = false;
gasFault = false;
gasBlocked = false;
flameOK = true;
fanActive = true;
fridgeRunning = true;
tempInterior = 3.0;
tempCongelador = -18.0;
tempTrasera = 34.0;
break;
default:
modoActivo = 3;
has220V = false;
has12V = false;
gasFault = true;
gasBlocked = true;
flameOK = false;
fanActive = false;
fridgeRunning = false;
tempInterior = 8.8;
tempCongelador = -8.0;
tempTrasera = 24.0;
break;
}
}

void actualizarModoDemo() {
unsigned long ahora = millis();

if (ahora - ultimoTickDemo >= 1000) {
ultimoTickDemo = ahora;
fridgeRunTotalSeconds++;
if (fridgeRunning) fridgeRunPartialSeconds++;
}

if (ahora - ultimaDemoActualizacion >= 4000) {
ultimaDemoActualizacion = ahora;
demoEscena = (demoEscena + 1) % 5;
aplicarEstadoDemo();
if (!pantallaApagada) {
if (estadoPantalla == 0) actualizarDatosPantalla();
else disenarMenuOpciones();
}
}
}

bool procesarLineaMega(const String& linea) {
if (!linea.startsWith("STAT,")) return false;

String campos[19];
int inicio = 5;
int indiceCampo = 0;

while (indiceCampo < 19) {
int coma = linea.indexOf(',', inicio);
if (coma == -1) {
campos[indiceCampo++] = linea.substring(inicio);
break;
}
campos[indiceCampo++] = linea.substring(inicio, coma);
inicio = coma + 1;
}

if (indiceCampo != 18 && indiceCampo != 19) return false;

int idxGasRetry = 7;
int idxFanActive = 8;
int idxFanManual = 9;
int idxCondActive = 10;
int idxCondTimeLeft = 11;
int idxHas220 = 12;
int idxHas12 = 13;
int idxFlameOK = 14;
int idxFridgeRunning = 15;
int idxTotalRuntime = 16;
int idxPartialRuntime = 17;

String modo = campos[0];
if (modo == "220V") modoActivo = 1;
else if (modo == "12V") modoActivo = 2;
else if (modo == "GAS") modoActivo = 3;
else modoActivo = 0;

tempInterior = campos[1].toFloat();
tempObjetivo = campos[2].toFloat();
tempCongelador = campos[3].toFloat();
tempTrasera = campos[4].toFloat();
gasFault = campos[5].toInt() != 0;
gasBlocked = campos[6].toInt() != 0;
if (indiceCampo == 19) {
gasBlockTimeLeft = campos[7].toInt() * 1000UL;
idxGasRetry = 8;
idxFanActive = 9;
idxFanManual = 10;
idxCondActive = 11;
idxCondTimeLeft = 12;
idxHas220 = 13;
idxHas12 = 14;
idxFlameOK = 15;
idxFridgeRunning = 16;
idxTotalRuntime = 17;
idxPartialRuntime = 18;
} else {
gasBlockTimeLeft = 0;
}
gasRetryCount = campos[idxGasRetry].toInt();
fanActive = campos[idxFanActive].toInt() != 0;
fanManualMode = campos[idxFanManual].toInt() != 0;
condActive = campos[idxCondActive].toInt() != 0;
condTimeLeft = campos[idxCondTimeLeft].toInt() * 1000UL;
has220V = campos[idxHas220].toInt() != 0;
has12V = campos[idxHas12].toInt() != 0;
flameOK = campos[idxFlameOK].toInt() != 0;
fridgeRunning = campos[idxFridgeRunning].toInt() != 0;
fridgeRunTotalSeconds = campos[idxTotalRuntime].toInt();
fridgeRunPartialSeconds = campos[idxPartialRuntime].toInt();

ultimoPaqueteMegaMs = millis();
megaStatPacketsRx++;
megaLastByteRx = 'S';
return true;
}

void procesarSerialMega() {
while (MEGA_SERIAL.available() > 0) {
char c = (char)MEGA_SERIAL.read();
megaRawBytesRx++;
megaLastByteRx = c;

if (c == '\n') {
  megaLastLineLen = bufferSerial.length();
  megaLinesRx++;
  bufferSerial.trim();
  bool estadoValido = procesarLineaMega(bufferSerial);
  if (!estadoValido && bufferSerial.length() > 0) {
    megaParseErrors++;
  }
  bufferSerial = "";

  if (estadoValido && !pantallaApagada) {
    if (estadoPantalla == 0) actualizarDatosPantalla();
    else disenarMenuOpciones();
  }
} else if (c != '\r') {
  if (bufferSerial.length() < 180) bufferSerial += c;
}

}
}

void enviarComandoMega(const String& comando) {
String comandoNormalizado = comando;
comandoNormalizado.trim();
if (comandoNormalizado.length() == 0) return;
MEGA_SERIAL.println(comandoNormalizado);
}

void apagarPantalla() {
digitalWrite(Tft_Backlight, LOW);
pantallaApagada = true;
}

void encenderPantalla() {
digitalWrite(Tft_Backlight, HIGH);
pantallaApagada = false;
if (estadoPantalla == 0) {
tft.fillScreen(COLOR_FONDO);
disenarInterfazEstatica();
actualizarDatosPantalla();
} else {
disenarMenuOpciones();
}
}

void dibujarPanelRelieve(int x, int y, int w, int h, const char* titulo, uint16_t colorTitulo) {
tft.fillRoundRect(x + 2, y + 2, w, h, 6, COLOR_SOMBRA);
tft.fillRoundRect(x, y, w, h, 6, COLOR_PANEL);
tft.drawRoundRect(x, y, w, h, 6, COLOR_BORDE_LUZ);
tft.drawRoundRect(x + 1, y + 1, w - 2, h - 2, 6, COLOR_BORDE_SOMBRA);
tft.fillRoundRect(x + 4, y + 4, w - 8, h - 8, 5, COLOR_PANEL_INNER);

if (titulo != nullptr && titulo[0] != '\0') {
tft.fillRoundRect(x + 4, y + 4, w - 8, 17, 5, colorTitulo);
tft.fillRect(x + 4, y + 13, w - 8, 8, colorTitulo);
tft.setTextSize(1);
tft.setTextColor(TFT_WHITE, colorTitulo);
tft.drawString(titulo, x + 8, y + 8);
}
}

void dibujarBotonRelieve(int x, int y, int w, int h, uint16_t fondo, uint16_t texto, const char* etiqueta, int tx, int ty, int textSize) {
tft.fillRoundRect(x + 2, y + 2, w, h, 5, COLOR_SOMBRA);
tft.fillRoundRect(x, y, w, h, 5, fondo);
tft.drawRoundRect(x, y, w, h, 5, COLOR_BORDE_LUZ);
tft.drawRoundRect(x + 1, y + 1, w - 2, h - 2, 5, COLOR_BORDE_SOMBRA);
tft.drawFastHLine(x + 5, y + 4, w - 10, TFT_WHITE);
tft.drawFastVLine(x + 4, y + 5, h - 10, TFT_WHITE);
tft.setTextSize(textSize);
tft.setTextColor(texto, fondo);
tft.drawString(etiqueta, tx, ty);
}

void dibujarAvisoCable220V() {
const int x = 34;
const int y = 86;
const int w = 252;
const int h = 70;
const uint16_t fondoAviso = 0x1000;
const uint16_t sombraTexto = 0x3000;

tft.fillRoundRect(x, y, w, h, 8, fondoAviso);
tft.drawRoundRect(x, y, w, h, 8, TFT_RED);
tft.drawRoundRect(x + 1, y + 1, w - 2, h - 2, 8, TFT_WHITE);

tft.setTextDatum(MC_DATUM);
tft.setTextSize(2);

tft.setTextColor(sombraTexto, fondoAviso);
tft.drawString("CABLE DE 220V", 161, 109);
tft.drawString("CONECTADO", 161, 133);

tft.setTextColor(TFT_RED, fondoAviso);
tft.drawString("CABLE DE 220V", 160, 108);
tft.drawString("CONECTADO", 160, 132);

tft.setTextDatum(TL_DATUM);
}

void actualizarEstadoComunicacionMega() {
bool enlaceActual = (ultimoPaqueteMegaMs != 0) && ((millis() - ultimoPaqueteMegaMs) <= TIMEOUT_COM_MEGA_MS);
if (enlaceActual == comunicacionMegaActiva) return;

comunicacionMegaActiva = enlaceActual;

if (!pantallaApagada) {
tft.fillScreen(COLOR_FONDO);
if (estadoPantalla == 0) {
disenarInterfazEstatica();
actualizarDatosPantalla();
} else {
disenarMenuOpciones();
}
}
}

void actualizarDatosPantalla() {
if (pantallaApagada || estadoPantalla != 0) return;
bool cable220VConectado = has220V && has12V;

if (avisoCable220VActivo && !cable220VConectado) {
disenarInterfazEstatica();
}

uint16_t gasColor = COLOR_PANEL_HEADER;
String gasTexto = "GAS INACT.";
String gasTiempo = "";
tft.setTextSize(1);
if (modoActivo == 3) {
gasColor = gasFault ? TFT_RED : TFT_ORANGE;
gasTexto = gasFault ? "GAS BLOQ." : "GAS ACTIVO";
} else if (gasBlocked) {
gasColor = TFT_RED;
gasTexto = "GAS BLOQ.";
if (gasBlockTimeLeft > 0) {
unsigned long totalSeconds = gasBlockTimeLeft / 1000UL;
unsigned long minRest = totalSeconds / 60UL;
unsigned long secRest = totalSeconds % 60UL;
char tiempoBuffer[8];
snprintf(tiempoBuffer, sizeof(tiempoBuffer), "%02lu:%02lu", minRest, secRest);
gasTiempo = tiempoBuffer;
}
}
tft.fillRoundRect(14, 9, 87, 30, 5, gasColor);
uint16_t gasTextColor = ((modoActivo == 3) && !gasFault) ? TFT_RED : TFT_WHITE;
tft.setTextColor(gasTextColor, gasColor);
if (gasTiempo.length() > 0) {
tft.drawString(gasTexto, 28, 13);
tft.drawString(gasTiempo, 40, 25);
} else {
tft.drawString(gasTexto, 34, 20);
}

uint16_t c220 = has220V ? 0x07FF : COLOR_PANEL_HEADER;
tft.fillRoundRect(116, 9, 87, 30, 5, c220);
if (has220V) {
tft.setTextColor(TFT_BLACK, c220);
tft.drawString("220V ACT.", 136, 20);
} else {
tft.setTextColor(TFT_LIGHTGREY, c220);
tft.drawString("220V INAC.", 134, 20);
}

uint16_t c12 = has12V ? 0xF3BF : COLOR_PANEL_HEADER;
tft.fillRoundRect(219, 9, 87, 30, 5, c12);
if (has12V) {
tft.setTextColor(TFT_BLACK, c12);
tft.drawString("12V ACTIVO", 240, 20);
} else {
tft.setTextColor(TFT_LIGHTGREY, c12);
tft.drawString("12V INACT.", 238, 20);
}

// Los iconos van al final para que no queden tapados por los paneles superiores.
dibujarIconosFuentes();

tft.fillRect(18, 72, 130, 42, COLOR_PANEL_INNER);
uint16_t colorTempInt = (tempInterior > 8.0) ? TFT_RED : TFT_BLUE;

tft.setTextColor(colorTempInt, COLOR_FONDO);
tft.setTextSize(4);
tft.drawString(String(tempInterior, 1), 32, 74);
tft.setTextSize(2);
tft.setTextColor(TFT_SKYBLUE, COLOR_FONDO);
tft.drawString("C", 124, 74);

dibujarBarraYTriangulo(tempObjetivo);

tft.fillRoundRect(217, 126, 41, 45, 4, COLOR_PANEL_HEADER);
tft.drawRoundRect(217, 126, 41, 45, 4, COLOR_BORDE_LUZ);
tft.drawRoundRect(218, 127, 39, 43, 4, COLOR_BORDE_SOMBRA);
tft.setTextSize(1);
tft.setTextColor(TFT_WHITE, COLOR_PANEL_HEADER);
tft.drawString("SET", 228, 132);
tft.setTextColor(TFT_YELLOW, COLOR_PANEL_HEADER);
tft.drawString(String(tempObjetivo, 1), 222, 150);

tft.fillRect(34, 154, 94, 22, COLOR_PANEL_INNER);
tft.setTextSize(2);
tft.setTextColor(TFT_SKYBLUE, COLOR_FONDO);
tft.drawString(String(tempCongelador, 1) + " C", 36, 154);

uint16_t colorVent = fanActive ? 0x07FF : COLOR_PANEL_HEADER;
uint16_t textoVent = fanActive ? TFT_BLACK : TFT_LIGHTGREY;
tft.fillRoundRect(16, 204, 132, 24, 5, colorVent);
dibujarAspasVentilador(32, 216, fanActive ? TFT_BLACK : TFT_RED);
tft.setTextSize(2);
tft.setTextColor(textoVent, colorVent);
tft.drawString(fanActive ? "ON" : "OFF", 52, 208);

if (cable220VConectado) {
dibujarAvisoCable220V();
}

avisoCable220VActivo = cable220VConectado;
}

void dibujarIconosFuentes() {
tft.setTextColor(TFT_WHITE, COLOR_FONDO);
tft.fillRect(14, 11, 18, 16, COLOR_FONDO);
uint16_t colorLlamaExterior = (modoActivo == 3) ? TFT_RED : TFT_DARKGREY;
uint16_t colorLlamaInterior = (modoActivo == 3) ? TFT_YELLOW : TFT_LIGHTGREY;
tft.drawFastVLine(16, 21, 4, colorLlamaExterior);
tft.drawFastVLine(17, 18, 8, colorLlamaExterior);
tft.drawFastVLine(18, 16, 10, colorLlamaExterior);
tft.drawFastVLine(19, 15, 11, colorLlamaExterior);
tft.drawFastVLine(20, 17, 9, colorLlamaExterior);
tft.drawFastVLine(21, 14, 12, colorLlamaExterior);
tft.drawFastVLine(22, 13, 13, colorLlamaExterior);
tft.drawFastVLine(23, 15, 11, colorLlamaExterior);
tft.drawFastVLine(24, 16, 10, colorLlamaExterior);
tft.drawFastVLine(25, 17, 9, colorLlamaExterior);
tft.drawFastVLine(26, 19, 7, colorLlamaExterior);
tft.drawFastVLine(27, 22, 3, colorLlamaExterior);
tft.drawFastVLine(20, 22, 3, colorLlamaInterior);
tft.drawFastVLine(21, 20, 5, colorLlamaInterior);
tft.drawFastVLine(22, 19, 6, colorLlamaInterior);
tft.drawFastVLine(23, 21, 4, colorLlamaInterior);

tft.fillRect(118, 11, 16, 16, COLOR_FONDO);
if (has220V) {
uint16_t colorEnchufe = 0x07FF;
tft.drawRoundRect(119, 17, 10, 8, 2, colorEnchufe);
tft.fillRect(121, 19, 6, 4, colorEnchufe);
tft.drawFastVLine(121, 13, 4, colorEnchufe);
tft.drawFastVLine(126, 13, 4, colorEnchufe);
tft.drawFastVLine(124, 25, 3, colorEnchufe);
tft.drawFastHLine(123, 28, 3, colorEnchufe);
}

tft.fillRect(220, 12, 18, 16, COLOR_FONDO);
if (has12V) {
uint16_t colorBateria = TFT_WHITE;
tft.drawRoundRect(221, 15, 14, 10, 2, colorBateria);
tft.fillRect(235, 18, 2, 4, colorBateria);
tft.drawFastHLine(224, 20, 2, colorBateria);
tft.drawFastHLine(228, 20, 3, colorBateria);
tft.drawFastVLine(229, 19, 3, colorBateria);
}
}

void dibujarAspasVentilador(int x, int y, uint16_t color) {
tft.fillCircle(x, y, 8, COLOR_FONDO);
tft.drawLine(x - 7, y - 7, x + 7, y + 7, color);
tft.drawLine(x - 7, y + 7, x + 7, y - 7, color);
tft.drawLine(x - 7, y, x + 7, y, color);
tft.drawLine(x, y - 7, x, y + 7, color);
tft.fillCircle(x, y, 3, color);
}

void disenarInterfazEstatica() {
tft.fillScreen(COLOR_FONDO);

dibujarPanelRelieve(10, 5, 95, 38, "", COLOR_PANEL_HEADER);
dibujarPanelRelieve(112, 5, 95, 38, "", COLOR_PANEL_HEADER);
dibujarPanelRelieve(215, 5, 95, 38, "", COLOR_PANEL_HEADER);
dibujarPanelRelieve(10, 48, 145, 132, "FRIGO INTERIOR", 0x03EF);
dibujarPanelRelieve(162, 48, 148, 132, "", 0x2989);
dibujarPanelRelieve(10, 186, 145, 48, "VENTILADOR", 0x03EF);
dibujarPanelRelieve(162, 186, 148, 48, "", COLOR_PANEL_HEADER);

tft.setTextSize(1);
tft.setTextColor(COLOR_TEXTO_SECUNDARIO, COLOR_FONDO);
tft.drawString("ACTUAL:", 20, 118);
tft.drawString("CONGELADOR", 36, 138);
tft.setTextSize(2);
tft.setTextColor(TFT_SKYBLUE, COLOR_FONDO);
tft.drawString("-18.0 C", 36, 154);

tft.setTextSize(1);
tft.setTextColor(COLOR_TEXTO_SECUNDARIO, COLOR_FONDO);
tft.drawString("-14", 170, 94);
tft.drawString("+20", 278, 94);

dibujarBotonRelieve(168, 126, 45, 45, TFT_YELLOW, TFT_BLACK, "-", 186, 140, 2);
dibujarBotonRelieve(262, 126, 45, 45, TFT_CYAN, TFT_BLACK, "+", 278, 140, 2);
dibujarBotonRelieve(162, 186, 148, 48, COLOR_BOTON_OPCIONES, COLOR_BOTON_OPCIONES_TEXTO, "OPCIONES", 184, 200, 2);
}

void disenarMenuOpciones() {
tft.fillScreen(COLOR_FONDO);
tft.setTextSize(2);
tft.setTextColor(TFT_WHITE, COLOR_FONDO);
tft.drawString("MENU DE OPCIONES", 60, 10);

dibujarPanelRelieve(10, 38, 300, 148, "CONFIGURACION", 0x2989);

// 1. Reset Placa
tft.setTextSize(1);
tft.setTextColor(COLOR_TEXTO_SECUNDARIO, COLOR_FONDO);
tft.drawString("RESETO GENERAL DE PLACA", 18, 46);
dibujarBotonRelieve(220, 43, 84, 32, TFT_RED, TFT_WHITE, "RESET", 231, 49, 2);

// 2. Tiempo Total
unsigned long hTotal = fridgeRunTotalSeconds / 3600;
unsigned long mTotal = (fridgeRunTotalSeconds % 3600) / 60;

tft.setTextSize(1);
tft.setTextColor(COLOR_TEXTO_SECUNDARIO, COLOR_FONDO);
tft.drawString("TIEMPO TOTAL:", 18, 80);
tft.setTextSize(2);
tft.setTextColor(TFT_CYAN, COLOR_FONDO);
tft.drawString(String(hTotal) + "h " + String(mTotal) + "m", 18, 93);

dibujarBotonRelieve(255, 80, 40, 26, TFT_RED, TFT_WHITE, "R", 269, 83, 2);

// 3. Tiempo Parcial
unsigned long hParcial = fridgeRunPartialSeconds / 3600;
unsigned long mParcial = (fridgeRunPartialSeconds % 3600) / 60;

tft.setTextSize(1);
tft.setTextColor(COLOR_TEXTO_SECUNDARIO, COLOR_FONDO);
tft.drawString("TIEMPO PARCIAL:", 18, 115);
tft.setTextSize(2);
tft.setTextColor(TFT_YELLOW, COLOR_FONDO);
tft.drawString(String(hParcial) + "h " + String(mParcial) + "m", 18, 128);

dibujarBotonRelieve(255, 115, 40, 26, TFT_YELLOW, TFT_BLACK, "R", 269, 118, 2);

// 4. Resistencia Condensación (IN5)
tft.setTextSize(1);
tft.setTextColor(COLOR_TEXTO_SECUNDARIO, COLOR_FONDO);
tft.drawString("RES. CONDENSACION (IN5):", 18, 150);

unsigned long minRestantes = condTimeLeft / 60000;

uint16_t colorBotonIN5 = condActive ? TFT_GREEN : TFT_DARKGREY;
uint16_t colorTextoIN5 = condActive ? TFT_BLACK : TFT_WHITE;

tft.fillRoundRect(200, 146, 95, 32, 5, colorBotonIN5);
tft.setTextSize(1);
tft.setTextColor(colorTextoIN5, colorBotonIN5);
if (condActive) {
tft.drawString("ENCENDIDO", 212, 152);
tft.drawString("Rest: " + String(minRestantes) + "m", 215, 164);
} else {
tft.drawString("APAGADO", 222, 158);
}

// Volver
dibujarBotonRelieve(20, 192, 280, 32, COLOR_PANEL_HEADER, TFT_WHITE, "< VOLVER", 100, 198, 2);

}

void dibujarBarraYTriangulo(float temp) {
if (estadoPantalla != 0) return;

tft.fillRect(166, 54, 140, 38, COLOR_PANEL_INNER);

for(int i = 0; i < 120; i++){
uint16_t colorBarra = tft.color565(i * 2, 100, 255 - (i * 2));
tft.drawFastVLine(176 + i, 80, 8, colorBarra);
}

if (temp < -14.0) temp = -14.0;
if (temp > 20.0) temp = 20.0;

int xTriangulo = map(temp * 10, -140, 200, 176, 296);

tft.drawTriangle(xTriangulo - 4, 76, xTriangulo + 4, 76, xTriangulo, 79, TFT_WHITE);
tft.fillTriangle(xTriangulo - 4, 76, xTriangulo + 4, 76, xTriangulo, 79, TFT_WHITE);

tft.setTextSize(1);
tft.setTextColor(TFT_YELLOW, COLOR_FONDO);
String textoTemp = String(temp, 1);

int xTexto = xTriangulo - 10;
if (xTexto < 215) xTexto = 215;
if (xTexto > 240) xTexto = 240;

tft.drawString(textoTemp, xTexto, 62);
}

Edita tu post y adjunta el código entre etiquetas <CODE/> (ver Normas del foro en español, punto 7)

No necesitas una fuente de 3.3V independiente solo para alimentar el adaptador de niveles.
Toma los 3.3V de la Mega o de la pantalla, ya que el consumo es despreciable.

En tu esquema no muestras como alimentas la Mega.

buenos días muchas gracias no encuentro donde están las normas numero 7 soy nuevo en este foro si me puedes indicar gracias

Están como tema fijo al principio de cada sección, difícil no encontrarlas. :wink:

buenas tardes a todos y muchas gracias ya esta solucionado lo del monitor colocando dos convertidores RS485 uno al lado de la Arduino mega y otro al lado del monitor y todo funciona correctamente