/* =========================================================================================
 * Project: ENC_v3v.ino (ENC_v3vX for Node ID X: 10)
 * Build:   2026-03-04 
 * SERVER platform: ESP32 CYD (Cheap Yellow Display) - ESP32-2432S028R
 * CLIENT platform: ESP32-C3 Super Mini with SH1106 OLED display.
 * Alternative client OLED: SSD1306: U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, -1, OLED_SCL, OLED_SDA); 
 * =========================================================================================
 * Name:  ENC_v3v.ino
 * SYNC REQUIREMENT: Target Server MAC must be C8:2E:18:F8:D0:6C.
 * SYNC REQUIREMENT: Radio must be locked to Channel 1.
 */

#include <Wire.h>
#include <Adafruit_AHT10.h>
#include <U8g2lib.h>
#include <esp_now.h>
#include <WiFi.h>
#include <esp_wifi.h>

// --- Global Settings ---
const int NODE_ID = 10; // SET TO 10 TO MAP TO "IDX" ON MASTER
const char *FILENAME = "ENC_v3vX.ino";

const int ldrPin = 3;  
const int pirPin = 2;  
const int ledPin = 1;  

#define OLED_SDA 8
#define OLED_SCL 9

U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, -1, OLED_SCL, OLED_SDA);
Adafruit_AHT10 aht;

uint8_t masterAddress[] = { 0xC8, 0x2E, 0x18, 0xF8, 0xD0, 0x6C };
uint8_t broadcastAddress[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
esp_now_peer_info_t peerInfo;
static volatile int8_t gLastRssi = 127; 

volatile bool motionDetected = false;
bool pirLatched = false;
unsigned long elapsedMillis = 0;
unsigned long timerValue = 60000; 

const int TEMP_THRESHOLD = 18;
const int HUMID_THRESHOLD = 25;

typedef struct struct_message {
  int id;
  int Tc;
  int Hu;
  int Lx; 
} struct_message;

struct_message readings; 

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

void IRAM_ATTR catchesMotion() {
  motionDetected = true; 
}

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

static void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  bool isDiscovery = (mac_addr[0] == 0xFF);
  Serial.printf("Action: %s Send Status for ID %d: %s\n", 
                isDiscovery ? "Discovery" : "Unicast", 
                NODE_ID, 
                status == ESP_NOW_SEND_SUCCESS ? "SUCCESS" : "FAIL");
}

static void OnDataRecv(const esp_now_recv_info_t *info, const uint8_t *data, int len) {
  if (info->rx_ctrl) {
    gLastRssi = info->rx_ctrl->rssi;
    Serial.printf(">>> HANDSHAKE CAPTURED: %d dBm from master\n", gLastRssi);
  }
}

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

void setup() {
  Serial.begin(115200);
  delay(1500);
  
  WiFi.mode(WIFI_STA);
  esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
  delay(500);

  Serial.println("\n//--------------------------------------");
  Serial.printf("FILE: %s | PRODUCTION ID: %d\n", FILENAME, NODE_ID);
  Serial.println("//--------------------------------------");

  pinMode(ledPin, OUTPUT);
  pinMode(pirPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(pirPin), catchesMotion, RISING);

  aht.begin();
  u8g2.begin();
  u8g2.setContrast(150);
  
  u8g2.clearBuffer();
  u8g2.sendBuffer();

  if (esp_now_init() == ESP_OK) {
    esp_now_register_send_cb(OnDataSent);
    esp_now_register_recv_cb(OnDataRecv);
    
    memset(&peerInfo, 0, sizeof(peerInfo));
    memcpy(peerInfo.peer_addr, broadcastAddress, 6);
    peerInfo.channel = 1; 
    peerInfo.encrypt = false;
    esp_now_add_peer(&peerInfo);

    memcpy(peerInfo.peer_addr, masterAddress, 6);
    esp_now_add_peer(&peerInfo);
  }
}

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

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

  if (now - elapsedMillis >= timerValue) {
    elapsedMillis = now;
    RunSyncSequence(false); 
  }

  if (motionDetected && !pirLatched) {
    pirLatched = true; 
    motionDetected = false; 
    flashLedOnce();
    RunSyncSequence(true); 
  } 
  
  if (digitalRead(pirPin) == LOW && pirLatched) {
    pirLatched = false;
  }
}

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

void RunSyncSequence(bool visible) {
  sensors_event_t humidity, temp;
  if (aht.getEvent(&humidity, &temp)) { 
    readings.id = NODE_ID; 
    readings.Tc = (int)temp.temperature; 
    readings.Hu = (int)humidity.relative_humidity; 
    readings.Lx = analogRead(ldrPin); 

    if (visible) { displayLargeCycle(); }

    esp_now_send(broadcastAddress, (uint8_t *)&readings, sizeof(readings));
    delay(200); 
    for(int i=0; i<3; i++) {
      esp_now_send(masterAddress, (uint8_t *)&readings, sizeof(readings));
      delay(200); 
    }

    if (visible) { displaySmallSummary(); }
    
    u8g2.clearBuffer();
    u8g2.sendBuffer();
  }
}

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

void displayLargeCycle() {
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_fub20_tf); 
  char idStr[10];
  sprintf(idStr, "ID: %d", NODE_ID);
  u8g2.setCursor((128 - u8g2.getStrWidth(idStr)) / 2, 28);
  u8g2.print(idStr);
  u8g2.setFont(u8g2_font_doomalpha04_tr);
  u8g2.setCursor((128 - u8g2.getStrWidth(FILENAME)) / 2, 48);
  u8g2.print(FILENAME); 
  u8g2.drawStr(50, 62, "CH1");
  u8g2.sendBuffer();
  delay(2200);

  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_inb53_mr); 
  u8g2.setCursor(0, 60);
  u8g2.print(readings.Tc);
  u8g2.setFont(u8g2_font_inb46_mf);
  u8g2.print("c");
  u8g2.sendBuffer();
  delay(1200);

  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_inb53_mr);
  u8g2.setCursor(0, 60);
  u8g2.print(readings.Hu);
  u8g2.setFont(u8g2_font_inb46_mf); 
  u8g2.print("%");
  u8g2.sendBuffer();
  delay(1200);
}

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

void displaySmallSummary() {
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_doomalpha04_tr);
  bool violation = (readings.Tc < TEMP_THRESHOLD || readings.Hu < HUMID_THRESHOLD);
  u8g2.setCursor(2, 10); 
  u8g2.print(violation ? "!SAFETY ALERT!" : "-OUTGOING STATS-");
  
  char dataLine[40];
  sprintf(dataLine, "ID:%d  Tc:%d  Hu:%d", NODE_ID, readings.Tc, readings.Hu);
  int xData = (128 - u8g2.getStrWidth(dataLine)) / 2;
  u8g2.setCursor(xData, 25);
  u8g2.print(dataLine);
  
  char lightLine[20];
  const char* desc = (readings.Lx < 500) ? "Bright" : (readings.Lx < 1500) ? "Normal" : "Dark";
  sprintf(lightLine, "Light: %s", desc);
  int xLight = (128 - u8g2.getStrWidth(lightLine)) / 2;
  u8g2.setCursor(xLight, 40);
  u8g2.print(lightLine);

  u8g2.setCursor(0, 55); 
  u8g2.print("RSSI: "); 
  if (gLastRssi < 0) {
    u8g2.print(gLastRssi); u8g2.print(" dBm");
    int bars = (gLastRssi > -60) ? 4 : (gLastRssi > -70) ? 3 : (gLastRssi > -80) ? 2 : 1;
    for (int i = 1; i <= 4; i++) {
      int barH = i * 4;
      if (i <= bars) u8g2.drawBox(90 + (i * 7), 64 - barH, 5, barH);
      else u8g2.drawFrame(90 + (i * 7), 64 - barH, 5, barH);
    }
  } else {
    u8g2.print("---");
  }
  u8g2.sendBuffer();
  delay(5100); 
}

void flashLedOnce() { digitalWrite(ledPin, HIGH); delay(150); digitalWrite(ledPin, LOW); }
//----------------------------------------------------------------------