/* USER CHANGES NEEDED:
 *  WIFI_SSID_1  // Your primary WiFi network.
 *  WIFI_PASS_1
 *  WIFI_SSID_2  // Usually your cell phone hot-spot.
 *  WIFI_PASS_2
 *  const char* lat =  "45.4801";  //  Orleans, ON
 *  const char* lon = "-75.4851"; 
 */

/* ESP32-2432S028R https://www.banggood.com/ESP32-MCU-2_8-Inch-Smart-Display-for-Arduino-LVGL-WIFI-bluetooth-Touch-WROOM-240+320-Screen-LCD-TFT-Module-with-Free-Tutorials-p-2000291.html?rmmds=myorder&cur_warehouse=CN&ID=6331822
   Overview: ESP32 gets outside weather (OS) update from open-meteo.com every 2 minutes. 
             This OS update is displayed for 15s then the "carousel" switches to inside weather (IS) 
             which is read from onboard AHT10 (Tc, Hu) and SGP40 (AQ) sensors.
             The IS update is displayed for 10s and then the carousel switches back to OS and continues to cycle.
             If there is no physical activity for 25s the display is blanked.
             When the PIR detects motion, the carousel switches to OS.
             Every 10 minutes the OS stats are written to SD disk.
             Serial Monitor details all actions and results.
*/

/* YTM_6y.ino
   2026-06-17
   prefontDon@gmail.com
   Platform: ESP32-2432S028R
*/

#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <TFT_eSPI.h>
#include <time.h>
#include <SPI.h>
#include <SD.h>
#include <Wire.h>
#include <Adafruit_AHTX0.h>
#include <Adafruit_SGP40.h> 
#include <esp_chip_info.h>

// --- Metadata ---
const char* SKETCH_NAME = "YTM_6y"; 
const char* VERSION = "6y";

// --- Configuration ---
unsigned long gWeatherUpdateInterval = 120000; // Defaults to 2 minutes
static const char* WIFI_SSID_1 = "----------------";
static const char* WIFI_PASS_1 = "----------------";
static const char* WIFI_SSID_2 = "----------------";
static const char* WIFI_PASS_2 = "----------------";

const char* lat = "45.4801";
const char* lon = "-75.4851";

#define PIR_PIN 35
#define LCD_BACKLIGHT 21
#define SD_CS 5
#define SPEAKER_PIN 26   

// --- SPI Pins (Shared Motherboard Architecture) ---
#define TFT_CS 15

// --- I2C Sensor Bus Configuration ---
#define I2C_SDA 27
#define I2C_SCL 22
//--------------------------------------

// OS (15s) + IS (10s) = 25s Total Base Routine Lifetime
const unsigned long CAROUSEL_SWITCH_TIME = 15000;     
const unsigned long MOTION_TIMEOUT = 25000;           

bool gDisplayActive = true;
unsigned long gActiveUntilMs = 0;
unsigned long gLockoutUntilMs = 0;
bool gMotionDetectedState = false;                    

// Automated Carousel Management
bool gShowingInternalView = false;                     
unsigned long gAwakeSequenceStartMs = 0;              
bool gCarouselFlipExecuted = false;                    

// --- GLOBAL TIMING TRACKERS ---
unsigned long gLastClockUpdateMs = 0;
unsigned long gLastWeatherUpdateMs = 0;
unsigned long gLastSntpSyncMs = 0;
unsigned long gLastWiFiCheckMs = 0;
unsigned long gLastSensorFetchMs = 0;

// --- NTP Configuration Constants ---
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = -18000;
const int daylightOffset_sec = 3600;

TFT_eSPI tft = TFT_eSPI();
Adafruit_AHTX0 aht;
Adafruit_SGP40 sgp; 
//--------------------------------------

// --- LOCAL DATA CACHE ARRAY REGISTERS ---
float curTemp = 0, curHum = 0, curPress = 0, curWind = 0;
int todayMaxTemp = 0, todayMinTemp = 0, todayPoP = 0;
int tomMaxTemp = 0, todayRainSumRaw = 0, tomMinTemp = 0, tomPoP = 0;
int yestMaxTemp = 0, yestMinTemp = 0, yestPoP = 0;
float dailyRainSum = 0.0f;
int windDirDeg = 0;
int weatherCodeRaw = 0;

float roomTemp = NAN, roomHum = NAN; 
int32_t roomVocIndex = -1; 
bool gTimeInitialized = false; 
bool gSensorsOnline = false;
bool gSgpOnline = false;
//--------------------------------------

String getWiFiStatusString(wl_status_t status) {
  switch (status) {
    case WL_IDLE_STATUS:     return "IDLE_STATUS (0)";
    case WL_NO_SSID_AVAIL:   return "NO_SSID_AVAILABLE (1)";
    case WL_SCAN_COMPLETED:  return "SCAN_COMPLETED (2)";
    case WL_CONNECTED:       return "CONNECTED (3)";
    case WL_CONNECT_FAILED:  return "CONNECT_FAILED (4)";
    case WL_CONNECTION_LOST: return "CONNECTION_LOST (5)";
    case WL_DISCONNECTED:    return "DISCONNECTED (6)";
    default:                 return "UNKNOWN_ERROR (" + String(status) + ")";
  }
}

String getLogTime(bool includeDate = false) {
  if (!gTimeInitialized) return "[NO SYNC]";
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) return "[NO SYNC]";
  char timeStringBuff[32];
  if (includeDate) strftime(timeStringBuff, sizeof(timeStringBuff), "[%a %b %d %H:%M:%S]", &timeinfo);
  else strftime(timeStringBuff, sizeof(timeStringBuff), "[%H:%M:%S]", &timeinfo);
  return String(timeStringBuff);
}
//--------------------------------------

void releaseSPIBus() {
  digitalWrite(TFT_CS, HIGH);
  digitalWrite(SD_CS, HIGH);
}
//--------------------------------------

void drawViewButton() {
  if (gShowingInternalView) {
    tft.fillRect(250, 213, 67, 24, TFT_DARKGREEN);
    tft.drawRect(250, 213, 67, 24, TFT_WHITE);
    tft.setTextColor(TFT_WHITE, TFT_DARKGREEN);
    tft.drawCentreString("AUTO", 283, 217, 2);
  } else {
    tft.fillRect(250, 213, 67, 24, TFT_NAVY);
    tft.drawRect(250, 213, 67, 24, TFT_WHITE);
    tft.setTextColor(TFT_WHITE, TFT_NAVY);
    tft.drawCentreString("AUTO", 283, 217, 2);
  }
}

void statusUpdate(String DTmsg, uint16_t color = TFT_WHITE, int fontSize = 4) {
  tft.fillRect(0, 210, 320, 30, TFT_BLUE);
  tft.setTextColor(color, TFT_BLUE);
  tft.drawCentreString(DTmsg, 154, 214, fontSize); 
  tft.setTextColor(TFT_LIGHTGREY, TFT_BLUE);
  tft.drawString(SKETCH_NAME, 2, 219, 2);
  drawViewButton();
}

void renderCurrentSystemTimeFooter() {
  struct tm timeinfo;
  if (getLocalTime(&timeinfo)) {
    char statusBuf[64];
    strftime(statusBuf, sizeof(statusBuf), "%b %d  %l:%M%P", &timeinfo);
    statusUpdate(statusBuf, TFT_WHITE);
  } else {
    statusUpdate("[NO SNTP SYNC]", TFT_ORANGE, 2);
  }
}
//--------------------------------------

void triggerChime(int frequencyHz = 2300, int durationMs = 15, int beeps = 1) {
  long toggleDelayUs = 1000000L / frequencyHz / 2;
  long totalCycles = (durationMs * 1000L) / (toggleDelayUs * 2);

  for (int b = 0; b < beeps; b++) {
    for (long i = 0; i < totalCycles; i++) {
      digitalWrite(SPEAKER_PIN, HIGH);
      delayMicroseconds(toggleDelayUs);
      digitalWrite(SPEAKER_PIN, LOW);
      delayMicroseconds(toggleDelayUs);
    }
    digitalWrite(SPEAKER_PIN, LOW); 
    if (beeps > 1 && b < (beeps - 1)) {
      delay(100); 
    }
  }
}
//--------------------------------------

void logToSD() {
  Serial.printf("%s SD: Isolating bus layout parameters...\n", getLogTime().c_str());
  releaseSPIBus();
  SPI.end();
  SPI.begin(18, 19, 23, SD_CS);

  if (!SD.begin(SD_CS, SPI, 25000000)) {
    Serial.printf("%s SD ERROR: Mount hardware failed on Pin %d.\n", getLogTime().c_str(), SD_CS);
    SPI.end();
    return;
  }

  File dataFile = SD.open("/weather_log.csv", FILE_APPEND);
  if (dataFile) {
    String dataLine = getLogTime(true) + ",";
    dataLine += String(curTemp) + "C," + String(curHum) + "%," + String(curPress) + "hPa," + String(curWind) + "kmh";
    dataFile.println(dataLine);
    dataFile.close();
    Serial.printf("%s SD SUCCESS: Raw payload line logged to weather_log.csv\n", getLogTime().c_str());
    if (gDisplayActive) {
      tft.fillRect(234, 219, 12, 12, TFT_GREEN); 
      delay(1000);
      renderCurrentSystemTimeFooter();
    }
  }
  SD.end();
  SPI.end();
}
//--------------------------------------

bool connectToWiFi() {
  auto attempt = [](const char* ssid, const char* pass) -> bool {
    Serial.printf("[%lu] WIFI: Starting network connection routines for SSID: %s\n", millis(), ssid);
    WiFi.disconnect(true);
    delay(1000);
    WiFi.begin(ssid, pass);

    int retry = 0;
    while (WiFi.status() != WL_CONNECTED && retry < 10) {
      statusUpdate("WiFi: " + String(ssid) + " (" + String(retry + 1) + ")", TFT_WHITE, 2);
      delay(1000); 
      retry++;
      wl_status_t currentStatus = WiFi.status();
      Serial.printf("[%lu] WIFI: Poll count: %d | Internal code state: %s\n", millis(), retry, getWiFiStatusString(currentStatus).c_str());
    }

    if (WiFi.status() == WL_CONNECTED) {
      Serial.printf("%s WIFI: Connection stable!\n", getLogTime().c_str());
      Serial.printf("%s WIFI INFO -> Active Network SSID : [%s]\n", getLogTime().c_str(), WiFi.SSID().c_str());
      Serial.printf("%s WIFI INFO -> Assigned Local IP    : %s\n", getLogTime().c_str(), WiFi.localIP().toString().c_str());
      Serial.printf("%s WIFI INFO -> Board Hardware MAC   : %s\n", getLogTime().c_str(), WiFi.macAddress().c_str());
      return true;
    }
    return false;
  };

  if (attempt(WIFI_SSID_1, WIFI_PASS_1)) return true;
  Serial.printf("[%lu] WIFI WARNING: Primary access point authentication failed. Invoking fallback routing...\n", millis());
  return attempt(WIFI_SSID_2, WIFI_PASS_2);
}
//--------------------------------------

void syncTime() {
  Serial.printf("%s NTP: Initializing clock request configuration framework...\n", getLogTime().c_str());
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  statusUpdate("Syncing Time...", TFT_CYAN, 2);
  struct tm timeinfo;
  int retry = 0;
  while (!getLocalTime(&timeinfo) && retry < 15) {
    delay(1000);
    retry++;
    Serial.printf("%s NTP: Awaiting network server packet response (Attempt %d/15)...\n", getLogTime().c_str(), retry);
  }
  if (getLocalTime(&timeinfo)) {
    gTimeInitialized = true; 
    Serial.printf("%s NTP SUCCESS: Time packets synchronized! Core wall clock set to: %s\n", getLogTime().c_str(), getLogTime(true).c_str());
  }
}
//--------------------------------------

void drawLayout() {
  tft.drawFastVLine(95, 0, 90, TFT_SILVER);
  tft.drawFastVLine(225, 0, 90, TFT_SILVER);
  tft.drawFastHLine(0, 30, 95, TFT_SILVER);
  tft.drawFastHLine(225, 30, 95, TFT_SILVER);
  tft.drawFastHLine(0, 90, 95, TFT_SILVER);
  tft.drawFastHLine(225, 90, 95, TFT_SILVER);
}
//--------------------------------------

void refreshDateHeaders() {
  tft.fillRect(0, 0, 320, 91, TFT_BLACK);
  drawLayout();
  struct tm info;
  if (getLocalTime(&info)) {
    char buf[10];
    tft.setTextColor(TFT_WHITE, TFT_BLACK);
    time_t now = time(NULL);
    strftime(buf, sizeof(buf), "%a", localtime(&now));
    tft.drawCentreString(buf, 160, 2, 4);
    time_t y = now - 86400;
    strftime(buf, sizeof(buf), "%a", localtime(&y));
    tft.drawCentreString(buf, 47, 2, 4);
    time_t t = now + 86400;
    strftime(buf, sizeof(buf), "%a", localtime(&t));
    tft.drawCentreString(buf, 273, 2, 4);
  }
}
//--------------------------------------

void drawCloud(int x, int y, uint16_t color) {
  tft.fillCircle(x, y - 4, 12, color);
  tft.fillCircle(x - 10, y + 4, 9, color);
  tft.fillCircle(x + 10, y + 4, 9, color);
  tft.fillRect(x - 10, y + 5, 20, 8, color);
}

void drawSun(int x, int y) {
  tft.fillCircle(x, y, 12, TFT_YELLOW);
  for (int i = 0; i < 360; i += 45) {
    float rad = i * 0.01745;
    tft.drawLine(x + cos(rad) * 15, y + sin(rad) * 15, x + cos(rad) * 22, y + sin(rad) * 22, TFT_YELLOW);
  }
}

void drawWeatherIcon(int x, int y, int code) {
  tft.fillRect(x - 55, y - 35, 110, 75, TFT_BLACK);
  if (code == 0) drawSun(x, y);
  else if (code >= 1 && code <= 3) {
    if (code == 1) {
      drawSun(x - 8, y - 8);
      drawCloud(x + 6, y + 6, TFT_SILVER);
    } else drawCloud(x, y, TFT_WHITE);
  } else {
    drawCloud(x, y - 5, TFT_DARKGREY);
    for (int i = 0; i < 3; i++) tft.drawLine(x - 12 + (i * 12), y + 12, x - 16 + (i * 12), y + 22, TFT_BLUE);
  }
}
//--------------------------------------

void displayInternalWeather() {
  tft.setTextColor(TFT_BLUE, TFT_BLACK);
  tft.drawCentreString("ENVIRO DASHBOARD", 160, 36, 4); 
  
  if (gSensorsOnline && gSgpOnline) {
    tft.setTextColor(TFT_GREEN, TFT_BLACK);
    tft.drawCentreString("AHT10 & SGP40 Bus Online", 160, 64, 2);
  } else if (gSensorsOnline && !gSgpOnline) {
    tft.setTextColor(TFT_ORANGE, TFT_BLACK);
    tft.drawCentreString("SGP40 Offline - Check I2C Wire", 160, 64, 2);
  } else if (!gSensorsOnline && gSgpOnline) {
    tft.setTextColor(TFT_ORANGE, TFT_BLACK);
    tft.drawCentreString("AHT10 Offline - Check I2C Wire", 160, 64, 2);
  } else {
    tft.setTextColor(TFT_RED, TFT_BLACK);
    tft.drawCentreString("Climate Sensors Offline", 160, 64, 2);
  }
  
  tft.setTextDatum(TL_DATUM); 
  
  // --- ROW 1: Temperature (Tc) ---
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.drawString("Tc:", 15, 94, 4); 
  if (!isnan(roomTemp)) {
    uint16_t tcColor;
    String tcAlertText = "";

    // FIXED: Continuous Cascading Ceilings to perfectly bridge decimals across boundary integer jumps
    if (roomTemp < 15.0f) {
      tcColor = TFT_BLUE;
      tcAlertText = "Cold";
    } else if (roomTemp < 21.0f) { // Covers 15.0 to 20.999
      tcColor = TFT_CYAN;
      tcAlertText = "Cool";
    } else if (roomTemp < 24.0f) { // Covers 21.0 to 23.999
      tcColor = TFT_GREEN;
      tcAlertText = "Comfort";
    } else if (roomTemp < 28.0f) { // Covers 24.0 to 27.999
      tcColor = TFT_YELLOW;
      tcAlertText = "Warm";
    } else {                       // Covers 28.0+
      tcColor = TFT_RED;
      tcAlertText = "Hot";
    }

    tft.setTextColor(tcColor, TFT_BLACK);
    tft.drawString(String((int)roomTemp) + "c", 85, 94, 6); 
    
    if (tcAlertText.length() > 0) {
      tft.setTextColor(tcColor, TFT_BLACK);
      tft.drawString(tcAlertText, 175, 100, 4); 
    }
  } else {
    tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
    tft.drawString("--c", 85, 94, 6);
  }
  
  // --- ROW 2: Humidity (Hu) ---
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.drawString("Hu:", 15, 138, 4); 
  if (!isnan(roomHum)) {
    uint16_t huColor;
    String huAlertText = "";

    // FIXED: Continuous Cascading Ceilings to perfectly bridge humidity decimals
    if (roomHum < 35.0f) { 
      huColor = TFT_ORANGE; 
      huAlertText = "Dry"; 
    } else if (roomHum <= 55.0f) { 
      huColor = TFT_GREEN; 
      huAlertText = ""; 
    } else if (roomHum <= 59.0f) { 
      huColor = TFT_YELLOW; 
      huAlertText = "Humid"; 
    } else { 
      huColor = TFT_RED; 
      huAlertText = "Too Humid"; 
    }

    tft.setTextColor(huColor, TFT_BLACK);
    tft.drawString(String((int)roomHum) + "%", 85, 138, 6); 
    
    if (huAlertText.length() > 0) {
      tft.setTextColor(huColor, TFT_BLACK);
      tft.drawString(huAlertText, 175, 144, 4); 
    }
  } else {
    tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
    tft.drawString("--%", 85, 138, 6);
  }
  
  // --- ROW 3: Air Quality (AQ) ---
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.drawString("AQ:", 15, 184, 4); 
  
  if (gSgpOnline && roomVocIndex >= 0) {
    if (roomVocIndex < 50) { tft.setTextColor(TFT_GREEN, TFT_BLACK); tft.drawString("Pristine", 85, 184, 4); } 
    else if (roomVocIndex < 150) { tft.setTextColor(TFT_DARKGREEN, TFT_BLACK); tft.drawString("Good", 85, 184, 4); } 
    else if (roomVocIndex < 300) { tft.setTextColor(TFT_YELLOW, TFT_BLACK); tft.drawString("Fair", 85, 184, 4); } 
    else { tft.setTextColor(TFT_RED, TFT_BLACK); tft.drawString("Poor", 85, 184, 4); tft.setTextColor(TFT_RED, TFT_BLACK); tft.drawString("Poor", 175, 190, 4); }
  } else {
    tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
    tft.drawString(String("Calibrating..."), 85, 184, 4); 
  }
  
  Serial.printf("%s DISPLAY: Refreshed local I2C sensor rendering metrics matrix.\n", getLogTime().c_str());
}
//--------------------------------------

void paintOutsideWeatherLayout() {
  tft.fillRect(0, 31, 320, 178, TFT_BLACK); 
  refreshDateHeaders();

  tft.setTextColor(TFT_ORANGE, TFT_BLACK);  tft.drawRightString("H: " + String(todayMaxTemp) + "c", 190, 32, 4);
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);  tft.drawRightString(String((int)curTemp) + "c", 190, 58, 6);
  tft.setTextColor(TFT_CYAN, TFT_BLACK);    tft.drawRightString("L: " + String(todayMinTemp) + "c", 190, 102, 4);

  tft.setTextColor(TFT_ORANGE, TFT_BLACK);  tft.drawCentreString("H: " + String(yestMaxTemp) + "c", 47, 32, 4);
  tft.setTextColor(TFT_CYAN, TFT_BLACK);    tft.drawCentreString("L: " + String(yestMinTemp) + "c", 47, 62, 4);
  tft.setTextColor(TFT_GREEN, TFT_BLACK);   tft.drawCentreString(String((int)curHum) + "%", 60, 105, 4);
  tft.drawCentreString("Today RH", 60, 130, 2); 
  tft.setTextColor(TFT_MAGENTA, TFT_BLACK); tft.drawCentreString(String((int)curPress), 60, 155, 4);
  tft.drawCentreString("Today hPa", 60, 180, 2);

  tft.setTextColor(TFT_ORANGE, TFT_BLACK);  tft.drawCentreString("H: " + String(tomMaxTemp) + "c", 273, 32, 4);
  tft.setTextColor(TFT_CYAN, TFT_BLACK);    tft.drawCentreString("L: " + String(tomMinTemp) + "c", 273, 62, 4);
  
  tft.setTextColor(TFT_BLUE, TFT_BLACK);    tft.drawCentreString(String(todayPoP) + "%", 260, 105, 4);
  tft.drawCentreString("Today PoP", 260, 130, 2);

  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  if (dailyRainSum == 0.0f) {
    tft.drawCentreString("0", 260, 155, 4);
  } else {
    tft.drawCentreString(String(dailyRainSum, 1), 260, 155, 4);
  }
  tft.drawCentreString("Today mm", 260, 180, 2);

  const char* directions[] = { "N", "NE", "E", "SE", "S", "SW", "W", "NW" };
  int dirIdx = (int)((windDirDeg + 22.5) / 45.0) % 8;
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.drawCentreString(String(directions[dirIdx]) + ": " + String((int)curWind) + " km/h", 160, 135, 2);
  drawWeatherIcon(160, 188, weatherCodeRaw);
  renderCurrentSystemTimeFooter();
}
//--------------------------------------

void fetchWeatherPayloadNetwork() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.printf("%s HTTP ERROR: Query blocked. No active WiFi transport connection layer.\n", getLogTime().c_str());
    return;
  }

  HTTPClient http;
  http.setTimeout(8000); 
  String url = "https://api.open-meteo.com/v1/forecast?latitude=" + String(lat) + "&longitude=" + String(lon) + "&current=temperature_2m,relative_humidity_2m,surface_pressure,weather_code,wind_speed_10m,wind_direction_10m&daily=temperature_2m_max,temperature_2m_min,precipitation_probability_max,precipitation_sum&past_days=1&timezone=America%2FNew_York";
  
  Serial.printf("%s HTTP: Opening payload outbound buffer channel to Open-Meteo...\n", getLogTime().c_str());
  http.begin(url);
  
  int httpCode = http.GET();
  if (httpCode == 200) {
    JsonDocument doc;
    String payload = http.getString();
    Serial.printf("%s HTTP SUCCESS: Payload received. Size: %d bytes.\n", getLogTime().c_str(), payload.length());
    
    DeserializationError error = deserializeJson(doc, payload);
    if (error) {
      Serial.printf("%s ARDUINOJSON ERROR: Memory engine parsing failed: %s\n", getLogTime().c_str(), error.c_str());
      http.end();
      return;
    }
    
    curTemp = doc["current"]["temperature_2m"];
    curHum = doc["current"]["relative_humidity_2m"];
    curPress = doc["current"]["surface_pressure"];
    curWind = doc["current"]["wind_speed_10m"];
    windDirDeg = doc["current"]["wind_direction_10m"];
    weatherCodeRaw = doc["current"]["weather_code"];
    
    yestMaxTemp = doc["daily"]["temperature_2m_max"][0];
    yestMinTemp = doc["daily"]["temperature_2m_min"][0];
    todayMaxTemp = doc["daily"]["temperature_2m_max"][1];
    todayMinTemp = doc["daily"]["temperature_2m_min"][1];
    tomMaxTemp = doc["daily"]["temperature_2m_max"][2];
    tomMinTemp = doc["daily"]["temperature_2m_min"][2];
    
    todayPoP = doc["daily"]["precipitation_probability_max"][1];
    dailyRainSum = doc["daily"]["precipitation_sum"][1];
    
    gWeatherUpdateInterval = 120000;
    Serial.printf("%s CACHE ENGINE: Local array registers refreshed successfully. Cadence set to 2m.\n", getLogTime().c_str());
  } else {
    if (httpCode == -1) {
      gWeatherUpdateInterval = 30000;
      Serial.printf("%s HTTP ERROR FAILURE: Response code: -1. Watchdog active: Requesting 30s recovery retry path.\n", getLogTime().c_str());
    } else {
      gWeatherUpdateInterval = 120000; 
      Serial.printf("%s HTTP ERROR FAILURE: Response code: %d\n", getLogTime().c_str(), httpCode);
    }
  }
  http.end();
}
//--------------------------------------

void updateWeather() {
  if (gShowingInternalView) {
    tft.fillRect(0, 31, 320, 178, TFT_BLACK);
    refreshDateHeaders();
    displayInternalWeather();
    renderCurrentSystemTimeFooter();
  } else {
    paintOutsideWeatherLayout();
  }
}
//--------------------------------------//--------------------------------------//--------------------------------------

void setup() {
  Serial.begin(115200);
  Serial.flush();
  delay(500);
  
  pinMode(PIR_PIN, INPUT);
  pinMode(SPEAKER_PIN, OUTPUT);
  digitalWrite(SPEAKER_PIN, LOW);
  
  pinMode(TFT_CS, OUTPUT);  digitalWrite(TFT_CS, HIGH);
  pinMode(SD_CS, OUTPUT);   digitalWrite(SD_CS, HIGH);

  tft.init();
  tft.setRotation(3);
  tft.fillScreen(TFT_BLACK);

  Wire.begin(I2C_SDA, I2C_SCL);
  delay(100); 

  if (aht.begin(&Wire)) {
    gSensorsOnline = true;
    Serial.println("SYSTEM: Onboard AHT10 Climate sensor initialized successfully.");
  } else {
    Serial.println("SYSTEM WARNING: AHT10 sensor module handshake failed.");
  }

  delay(100);

  if (sgp.begin(&Wire)) {
    gSgpOnline = true;
    Serial.println("SYSTEM: Onboard SGP40 VOC Gas micro-sensor found and processing.");
  } else {
    Serial.println("SYSTEM WARNING: SGP40 gas indexing hardware dropped handshake.");
  }

  ledcAttach(LCD_BACKLIGHT, 5000, 8);
  ledcWrite(LCD_BACKLIGHT, 255);
  
  unsigned long startupMs = millis();
  gLastClockUpdateMs = startupMs;
  gLastWeatherUpdateMs = startupMs;
  gLastSntpSyncMs = startupMs;
  gLastWiFiCheckMs = startupMs;
  gLastSensorFetchMs = startupMs;

  Serial.printf("\n%s BOOT: Initializing configuration run for %s (V%s)\n", getLogTime().c_str(), SKETCH_NAME, VERSION);
  
  if (connectToWiFi()) syncTime();
  
  fetchWeatherPayloadNetwork(); 
  updateWeather(); 
  gAwakeSequenceStartMs = millis(); 
  gCarouselFlipExecuted = false;
  gActiveUntilMs = millis() + MOTION_TIMEOUT;
}
//--------------------------------------//--------------------------------------//--------------------------------------

void loop() {
  unsigned long now = millis(); 

  static int lastTriggeredSecond = -1;
  static int lastLoggedMinute = -1; 
  static bool wasConnected = true;

  if (WiFi.status() != WL_CONNECTED) {
    wasConnected = false;
    if (now - gLastWiFiCheckMs > 30000) {
      Serial.printf("%s WATCHDOG ALERT: Connection layer down. Retrying...\n", gLastWiFiCheckMs);
      if (connectToWiFi()) {
        syncTime(); 
        fetchWeatherPayloadNetwork();
        gLastWeatherUpdateMs = millis(); 
        if (gDisplayActive) updateWeather();
        wasConnected = true;
      }
      gLastWiFiCheckMs = millis();
    }
  } else if (!wasConnected) {
    Serial.printf("%s WATCHDOG NOTIFY: WiFi link recovered.\n", gLastWiFiCheckMs);
    syncTime(); 
    fetchWeatherPayloadNetwork();
    gLastWeatherUpdateMs = millis(); 
    if (gDisplayActive) updateWeather();
    wasConnected = true;
  }

  // --- PIR Input Engine ---
  int currentPIRSample = digitalRead(PIR_PIN);
  if (currentPIRSample == HIGH) { 
    gActiveUntilMs = millis() + MOTION_TIMEOUT;
    
    if (!gMotionDetectedState) {
      gMotionDetectedState = true;
      Serial.printf("%s HARDWARE: Motion detected.\n", getLogTime().c_str());
    }

    if (!gDisplayActive && (millis() > gLockoutUntilMs)) {
      delay(150); 
      if (digitalRead(PIR_PIN) == HIGH) {
        Serial.printf("%s HARDWARE: PIR motion verified. Awake sequence triggered.\n", getLogTime().c_str());
        
        gAwakeSequenceStartMs = millis(); 
        gDisplayActive = true;
        gShowingInternalView = false;                 
        gCarouselFlipExecuted = false;                 
        
        tft.fillScreen(TFT_BLACK);
        ledcWrite(LCD_BACKLIGHT, 255);
        
        gLastWeatherUpdateMs = millis(); 
        updateWeather();
      }
    }
  }

  if (gDisplayActive && (millis() > gActiveUntilMs)) {
    if (gMotionDetectedState) {
      gMotionDetectedState = false;
      Serial.printf("%s HARDWARE: Motion no longer detected.\n", getLogTime().c_str());
    }
    
    Serial.printf("%s HARDWARE: Activity window timeout reached. Suspending backlight array.\n", getLogTime().c_str());
    ledcWrite(LCD_BACKLIGHT, 0);
    gDisplayActive = false;
    gLockoutUntilMs = millis() + 5000;
    
    gShowingInternalView = false;
    gCarouselFlipExecuted = false;
    gAwakeSequenceStartMs = 0; 
    
    tft.fillScreen(TFT_BLACK);
  }

  if (currentPIRSample == LOW && gMotionDetectedState) {
    gMotionDetectedState = false;
    Serial.printf("%s HARDWARE: Motion no longer detected.\n", getLogTime().c_str());
  }

  // --- AUTOMATED ENDLESS CAROUSEL TIMING CONTROLLER ---
  if (gDisplayActive && gAwakeSequenceStartMs > 0) {
    if (!gCarouselFlipExecuted && (now - gAwakeSequenceStartMs >= CAROUSEL_SWITCH_TIME)) {
      Serial.printf("%s CAROUSEL: 15s limit reached on OS. Switching automatically to Inside Dashboard (IS).\n", getLogTime().c_str());
      gShowingInternalView = true;
      gCarouselFlipExecuted = true; 
      
      gAwakeSequenceStartMs = millis(); 
      updateWeather(); 
    }
    
    if (gCarouselFlipExecuted && (now - gAwakeSequenceStartMs >= (MOTION_TIMEOUT - CAROUSEL_SWITCH_TIME))) {
      if (currentPIRSample == HIGH) {
        Serial.printf("%s CAROUSEL: Full 25s cycle run complete with active motion. Wrapping layout back to Outside (OS).\n", getLogTime().c_str());
        gShowingInternalView = false;
        gCarouselFlipExecuted = false;
        
        gAwakeSequenceStartMs = millis(); 
        updateWeather(); 
      }
    }
  }

  // --- Background I2C Micro-Sensor Internal Scan Gate ---
  if (gSensorsOnline && (millis() - gLastSensorFetchMs >= 10000)) {
    gLastSensorFetchMs = millis(); 
    sensors_event_t humidityEvent, tempEvent;
    aht.getEvent(&humidityEvent, &tempEvent);
    
    roomTemp = tempEvent.temperature;
    roomHum = humidityEvent.relative_humidity;
    
    if (gSgpOnline && !isnan(roomTemp) && !isnan(roomHum)) {
      roomVocIndex = sgp.measureVocIndex(roomTemp, roomHum);
    }
    
    if (gShowingInternalView && gDisplayActive) {
      tft.fillRect(0, 92, 320, 117, TFT_BLACK);
      displayInternalWeather();
    }
  }

  if (millis() - gLastSntpSyncMs > 43200000) {
    if (WiFi.status() == WL_CONNECTED) {
      Serial.printf("%s WATCHDOG: Scheduled background NTP sync run...\n", getLogTime().c_str());
      syncTime();
      gLastSntpSyncMs = millis();
    }
  }

  // --- 1-Second Precision Chrono-Engine Block ---
  if (millis() - gLastClockUpdateMs > 1000) {
    gLastClockUpdateMs = millis();
    struct tm info;
    if (getLocalTime(&info)) {
      if (gDisplayActive) {
        char statusBuf[64];
        strftime(statusBuf, sizeof(statusBuf), "%b %d  %l:%M%P", &info);
        statusUpdate(statusBuf, TFT_WHITE);
      }

      int currentMin = info.tm_min;
      int infoSec = info.tm_sec;
      
      if ((currentMin % 10 == 0) && (currentMin != lastLoggedMinute)) {
        Serial.printf("%s CLOCK ENGINE: 10-minute marker reached. Initiating SD write cycle.\n", getLogTime().c_str());
        logToSD();
        lastLoggedMinute = currentMin; 
      }
      
      if ((currentMin == 0 || currentMin == 30) && infoSec == 0 && (infoSec != lastTriggeredSecond)) {
        if (currentMin == 0) {
          Serial.printf("%s SYSTEM: Top-of-the-hour mark. Sounding DOUBLE chime.\n", getLogTime().c_str());
          triggerChime(2300, 15, 2);                 
        } else {
          Serial.printf("%s SYSTEM: Half-hour mark. Sounding single chime.\n", getLogTime().c_str());
          triggerChime(2300, 10, 1);                  
        }
        lastTriggeredSecond = infoSec;
      }
      
      if (infoSec != 0) {
        lastTriggeredSecond = -1;
      }
    }
  }

  if (millis() - gLastWeatherUpdateMs > gWeatherUpdateInterval) {
    if (WiFi.status() != WL_CONNECTED) return;
    gLastWeatherUpdateMs = millis(); 
    Serial.printf("%s WATCHDOG: Scheduled quiet background network cache refresh...\n", getLogTime().c_str());
    fetchWeatherPayloadNetwork();
    if (gDisplayActive && !gShowingInternalView) {
       updateWeather();
    }
  }
}
//--------------------------------------//--------------------------------------//--------------------------------------