/* =========================================================================================
 * Project: ENS_v2v.ino
 * Build:   2026-03-03 
 * SERVER platform: ESP32 CYD (Cheap Yellow Display) - ESP32-2432S028R
 * CLIENT platform: ESP32-C3 Super Mini.
 * Alternative client OLED: SSD1306: U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, -1, OLED_SCL, OLED_SDA); 
 * =========================================================================================
 * CLIENT IMPLEMENTED FEATURES
 * 1. Startup Splash Screen: 3s branding animation with versioning.
 * 2. Dedicated SPI for Touch: Prevents bus clashing on pins 25, 39, 32, 33.
 * 3. 3-Page Navigation: Dashboard, RSSI Diagnostics, and System MAC Info.
 * 4. SNTP Watchdog: 10-minute auto-restart for clock synchronization.
 * 5. Signal Bar UI: Color-coded dBm diagnostics with 4-bar graphics.
 * 6. Optimized Layout: Hard-left alignment to prevent ID10 text wrapping.
 * 7. Audio Feedback: High-freq "chirp" for discovery and touch.
 * =========================================================================================
 * A.I. MUST REVIEW BEFORE CODING
 * 1. Comment Delimiters: REMOVE all citations or tags.
 * 2. Function Separators: PRESERVE all //------------------------------
 * 3. Verbosity: Serial Monitor must be HIGHLY VERBOSE.
 * 4. Variable Consistency: Use NODE_ID and FILENAME constants.
 * =====================================================================
 * Critical Requirements for Client-Server Sync
 * To ensure the Client and Server communicate successfully, four specific factors must align:
 * MAC Address Match: The Client must send data specifically to the Server's 6-byte Hardware MAC address.
 * WiFi Channel Lock: Both devices must be forced onto the same WiFi channel (Channel 1) to prevent the radios from "missing" each other while scanning.
 * Peer Registration: The Server must register the Client as a "Peer" before it can send the handshake back.
 * Software Handshake: The Server must manually send a return packet to the Client so the Client can trigger its OnDataRecv callback and capture the RSSI (signal bars).
 * =====================================================================
 * Name:  ENS_v2v.ino
 * SYNC REQUIREMENT: Target Server MAC must be C8:2E:18:F8:D0:6C.
 * SYNC REQUIREMENT: Radio must be locked to Channel 1.
 */

#include <TFT_eSPI.h>
#include <WiFi.h>
#include <esp_now.h>
#include <Adafruit_AHTX0.h>
#include <XPT2046_Touchscreen.h>
#include <SPI.h>
#include <esp_wifi.h>

// --- Hardware Pins (CYD Standard) ---
#define I2C_SDA 27
#define I2C_SCL 22
#define SPKR_PIN 26
#define TOUCH_CS 33
#define TOUCH_IRQ 36
#define TOUCH_SCLK 25
#define TOUCH_MISO 39
#define TOUCH_MOSI 32

// --- Global Objects ---
const char *FILENAME = "ENS_v2v.ino";
TFT_eSPI tft = TFT_eSPI();
SPIClass touchscreenSPI(VSPI);
XPT2046_Touchscreen ts(TOUCH_CS, TOUCH_IRQ);
Adafruit_AHTX0 ahtMaster;

// --- Data Structures ---
typedef struct struct_message {
  int id;
  int Tc;
  int Hu;
  int Lx;
} struct_message;

struct_message boardsStruct[11];
unsigned long lastSeenMs[11] = { 0 };
int8_t lastRssi[11] = { 0 };
uint8_t nodeMACs[11][6];

// --- State Variables ---
int gPage = 0;
unsigned long gLastManualTouchMs = 0;
unsigned long lastSyncMillis = 0;
const int TEMP_THRESHOLD = 18;
const int HUMID_THRESHOLD = 25;

//----------------------------------------------------------------------

void OnDataRecv(const esp_now_recv_info_t* info, const uint8_t* incomingData, int len) {
  struct_message packet;
  memcpy(&packet, incomingData, sizeof(packet));

  Serial.println("//--------------------------------------");
  Serial.printf(">>> RECEIVED FROM ID: %d\n", packet.id);

  if (!esp_now_is_peer_exist(info->src_addr)) {
    esp_now_peer_info_t newPeer;
    memset(&newPeer, 0, sizeof(newPeer));
    memcpy(newPeer.peer_addr, info->src_addr, 6);
    newPeer.channel = 1;
    newPeer.encrypt = false;
    esp_now_add_peer(&newPeer);
    Serial.println("Action: Registered new client MAC.");
  }

  if (packet.id > 0 && packet.id <= 10) {
    boardsStruct[packet.id] = packet;
    lastSeenMs[packet.id] = millis();
    if (info->rx_ctrl) lastRssi[packet.id] = info->rx_ctrl->rssi;
    memcpy(nodeMACs[packet.id], info->src_addr, 6);

    uint8_t ackData = 1;
    esp_now_send(info->src_addr, &ackData, 1);
    
    // Violation Alarm: Only chirp if thresholds are hit
    if (packet.id != 0 && (packet.Tc < TEMP_THRESHOLD || packet.Hu < HUMID_THRESHOLD)) {
       tone(SPKR_PIN, 3800, 30);
       Serial.println("ALARM: Violation Detected. Chirp triggered.");
    }
    
    Serial.printf("Action: Data Handled. RSSI: %d dBm\n", lastRssi[packet.id]);
  }
  Serial.println("//--------------------------------------");
}

//----------------------------------------------------------------------

void setup() {
  Serial.begin(115200);
  delay(1500);
  WiFi.mode(WIFI_STA);
  delay(200);

  tft.begin();
  tft.setRotation(3);
  touchscreenSPI.begin(TOUCH_SCLK, TOUCH_MISO, TOUCH_MOSI, TOUCH_CS);
  ts.begin(touchscreenSPI);
  ts.setRotation(3);
  pinMode(SPKR_PIN, OUTPUT);

  // Splash Screen Branding
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_CYAN);
  tft.drawCentreString("ENVIRO NET", 160, 60, 4);
  tft.setTextColor(TFT_WHITE);
  tft.drawCentreString("SYSTEM MASTER 2v", 160, 100, 2);
  
  // Loading Animation Loop
  int barWidth = 200;
  int barX = 60;
  int barY = 140;
  tft.drawRect(barX, barY, barWidth, 10, TFT_WHITE);
  for (int i = 0; i <= barWidth - 4; i++) {
    tft.fillRect(barX + 2, barY + 2, i, 6, TFT_CYAN);
    if (i % 40 == 0) tone(SPKR_PIN, 2000 + (i * 2), 10);
    delay(15);
  }
  tone(SPKR_PIN, 2500, 50);
  delay(500);

  Wire.begin(I2C_SDA, I2C_SCL);
  ahtMaster.begin();

  esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
  if (esp_now_init() == ESP_OK) {
    esp_now_register_recv_cb(esp_now_recv_cb_t(OnDataRecv));
  }

  lastSyncMillis = millis();
  tft.fillScreen(TFT_BLACK);
}

//----------------------------------------------------------------------

void loop() {
  sensors_event_t hum, temp;
  ahtMaster.getEvent(&hum, &temp);
  boardsStruct[0].Tc = (int)temp.temperature;
  boardsStruct[0].Hu = (int)hum.relative_humidity;

  if (millis() - lastSyncMillis >= 3600000) { ESP.restart(); }
  
  handleTouch();
  updateUI();
  delay(100);
}

//----------------------------------------------------------------------

void handleTouch() {
  unsigned long now = millis();
  if (ts.touched()) {
    TS_Point p = ts.getPoint();
    int calX = map(p.x, 300, 3800, 0, 320);
    
    if (now - gLastManualTouchMs > 500) {
      if (calX > 240) {
        gPage++;
        if (gPage > 2) gPage = 0;
        tft.fillScreen(TFT_BLACK);
        gLastManualTouchMs = now;
      } else if (calX < 80) {
        gPage--;
        if (gPage < 0) gPage = 2;
        tft.fillScreen(TFT_BLACK);
        gLastManualTouchMs = now;
      }
    }
  }
}

//----------------------------------------------------------------------

void updateUI() {
  if (gPage == 0) drawEnvPage();
  else if (gPage == 1) drawRssiPage();
  else drawInfoPage();

  drawNavButtons();
  drawFooterCountdown();
}

//----------------------------------------------------------------------

void drawEnvPage() {
  tft.setTextSize(2);
  tft.setTextColor(TFT_GREEN, TFT_BLACK);
  tft.drawCentreString("ENVIRO DASHBOARD", 160, 5, 2);
  
  tft.setCursor(45, 38);
  tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.print("MASTER ID0:");
  tft.setTextColor(TFT_CYAN, TFT_BLACK);  tft.printf("%dC ", boardsStruct[0].Tc);
  tft.setTextColor(0x5ADB, TFT_BLACK);    tft.printf("%d%%", boardsStruct[0].Hu);

  for (int i = 1; i <= 10; i++) {
    int col = (i <= 5) ? 2 : 164; 
    int row = 68 + ((i > 5 ? i - 6 : i - 1) * 28);
    bool dead = (millis() - lastSeenMs[i] > 120000 || lastSeenMs[i] == 0);
    
    tft.fillRect(col + 45, row, 110, 18, TFT_BLACK);
    tft.setCursor(col, row);
    
    const char* label = (i == 10) ? "IDX:" : "ID%d:";
    
    if (!dead) {
      tft.setTextColor(TFT_WHITE, TFT_BLACK);
      if (i == 10) tft.print(label); else tft.printf(label, i);
      
      // Violation Flashing Logic (Tc < 18 or Hu < 25)
      bool tempWarn = (boardsStruct[i].Tc < TEMP_THRESHOLD);
      bool humidWarn = (boardsStruct[i].Hu < HUMID_THRESHOLD);

      // Tc Flash Red if warning
      tft.setTextColor(tempWarn ? TFT_RED : TFT_CYAN, TFT_BLACK); 
      tft.printf("%dC", boardsStruct[i].Tc);
      
      char lxC = (boardsStruct[i].Lx < 500) ? 'B' : (boardsStruct[i].Lx < 1500) ? 'N' : 'D';
      tft.setTextColor(TFT_YELLOW, TFT_BLACK); tft.printf(" %c ", lxC);
      
      // Hu Flash Red if warning
      tft.setTextColor(humidWarn ? TFT_RED : 0x5ADB, TFT_BLACK); 
      tft.printf("%2d%%", boardsStruct[i].Hu);
      
      if (tempWarn || humidWarn) delay(500); // Enforce flash visibility
    } else {
      tft.setTextColor(TFT_WHITE, TFT_BLACK);
      if (i == 10) tft.print(label); else tft.printf(label, i);
      tft.setTextColor(TFT_RED, TFT_BLACK); 
      tft.print("--- - ---");
    }
  }
}

//----------------------------------------------------------------------

void drawRssiPage() {
  tft.setTextSize(2);
  tft.setTextColor(TFT_CYAN, TFT_BLACK);
  tft.drawCentreString("SIGNAL STRENGTH (dBm)", 160, 5, 2);

  for (int i = 1; i <= 10; i++) {
    int col = (i <= 5) ? 2 : 164;
    int row = 52 + ((i > 5 ? i - 6 : i - 1) * 28);
    
    tft.fillRect(col + 45, row, 110, 18, TFT_BLACK);
    tft.setCursor(col, row);
    tft.setTextColor(TFT_WHITE, TFT_BLACK);
    
    if (i == 10) tft.print("IDX:"); else tft.printf("ID%d:", i);

    if (lastSeenMs[i] != 0) {
      int8_t rssi = lastRssi[i];
      uint16_t sigCol = (rssi > -65) ? TFT_GREEN : (rssi > -80) ? TFT_YELLOW : TFT_RED;
      tft.setTextColor(sigCol, TFT_BLACK);
      tft.printf("%d", rssi);
      
      int bars = (rssi > -60) ? 4 : (rssi > -70) ? 3 : (rssi > -80) ? 2 : 1;
      for (int b = 1; b <= 4; b++) {
        int h = b * 4;
        if (b <= bars) tft.fillRect(col + 95 + (b * 6), row + 15 - h, 4, h, sigCol);
        else tft.drawRect(col + 95 + (b * 6), row + 15 - h, 4, h, 0x7BEF);
      }
    } else {
      tft.setTextColor(0x7BEF, TFT_BLACK); tft.print("---");
    }
  }
}

//----------------------------------------------------------------------

void drawInfoPage() {
  tft.setTextSize(2);
  tft.setTextColor(TFT_GOLD, TFT_BLACK);
  tft.drawCentreString("SYSTEM DIAGNOSTICS", 160, 5, 2);
  
  tft.setTextSize(1);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  char headerBuf[64];
  sprintf(headerBuf, "WiFi Ch: %d   Server MAC: %s", WiFi.channel(), WiFi.macAddress().c_str());
  tft.drawCentreString(headerBuf, 160, 42, 2);

  for (int i = 1; i <= 10; i++) {
    int col = (i <= 5) ? 5 : 162;
    int row = 70 + ((i > 5 ? i - 6 : i - 1) * 25);
    
    tft.fillRect(col + 30, row, 120, 15, TFT_BLACK);
    tft.setCursor(col, row);
    
    const char* labelPrefix = (i == 10) ? "IDX:" : "ID%d:";
    if (lastSeenMs[i] != 0) {
      tft.setTextColor(TFT_CYAN, TFT_BLACK);
      if (i == 10) tft.print(labelPrefix); else tft.printf(labelPrefix, i);
      tft.printf("%02X:%02X:%02X:%02X:%02X:%02X", nodeMACs[i][0], nodeMACs[i][1], nodeMACs[i][2], nodeMACs[i][3], nodeMACs[i][4], nodeMACs[i][5]);
    } else {
      tft.setTextColor(0x7BEF, TFT_BLACK); 
      if (i == 10) tft.print(labelPrefix); else tft.printf(labelPrefix, i);
      tft.print(" No Data");
    }
  }
}

//----------------------------------------------------------------------

void drawFooterCountdown() {
  unsigned long elapsed = millis() - lastSyncMillis;
  long remaining = 3600000 - elapsed;
  if (remaining < 0) remaining = 0;
  tft.setTextSize(1);
  tft.setTextColor(TFT_ORANGE, TFT_BLACK);
  char countBuf[35];
  sprintf(countBuf, "Next SNTP Sync: %02d:%02d", (int)(remaining / 60000), (int)((remaining % 60000) / 1000));
  tft.drawCentreString(countBuf, 160, 220, 2);
}

//----------------------------------------------------------------------

void drawNavButtons() {
  const char* nextLabel = (gPage == 0) ? "RSSI >" : (gPage == 1) ? "INFO >" : "ENV >";
  tft.setTextSize(1);
  tft.fillRoundRect(5, 200, 75, 35, 6, TFT_MAROON);
  tft.drawRoundRect(5, 200, 75, 35, 6, TFT_WHITE);
  tft.setTextColor(TFT_WHITE);
  tft.drawCentreString("< BACK", 42, 210, 2);
  tft.fillRoundRect(240, 200, 75, 35, 6, 0x01E0); 
  tft.drawRoundRect(240, 200, 75, 35, 6, TFT_WHITE);
  tft.drawCentreString((char*)nextLabel, 277, 210, 2);
}
//----------------------------------------------------------------------