📖 About This Project

Build a temperature-controlled fan system using ESP8266 D1 Large, DHT22 sensor, and relay. The board reads temperature and humidity, then automatically switches a fan ON/OFF based on user-defined threshold. Manual override via OceanRemote dashboard. Negative logic relay (LOW = ON). Includes persistent storage, WiFi auto-reconnect, and secure token authentication. Great for workshop ventilation, server rack cooling, or reptile enclosure temperature control.

💻 Firmware Code

C++ (Arduino)
// OceanicRemote Secure Firmware - ESP8266 D1 Large
// Version 5.0 - Persistent Device ID & Recovery Logic
// With Session Authentication & Custom Relay Names & Per-Relay Logic

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include <EEPROM.h>


#include <DHT.h>
#define DHTPIN D2
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

// ========== EEPROM CONFIGURATION ==========
#define EEPROM_SIZE 512
#define TOKEN_ADDRESS 0
#define TOKEN_MAGIC 0xAA55
#define SESSION_ADDRESS 128
#define SESSION_MAGIC 0xBB66
#define FLAG_ADDRESS 256
#define FLAG_MAGIC 0xCC77
#define VERSION_ADDRESS 260
#define DEVICE_ID_ADDRESS 300
#define DEVICE_ID_MAGIC 0xDD88
#define FIRMWARE_VERSION 50

// ========== RELAY NAMES ==========
const char* relayNames[5] = {
  "FAN1",
  "Relay 2",
  "Relay 3",
  "Relay 4",
  "Relay 5"
};

// ========== DEVICE STATE ==========
enum DeviceState {
  STATE_PROVISIONING = 0,
  STATE_REGISTERED = 1,
  STATE_ERROR = 2,
  STATE_RECONNECTING = 3
};

DeviceState currentState = STATE_PROVISIONING;

// ========== WIFI CONFIGURATION ==========
const char* WIFI_SSID = "SSID_WIF";
const char* WIFI_PASSWORD = "PASSWROD_WIFI";

// ========== SERVER CONFIGURATION ==========
const char* serverHost = "www.oceanremote.net";

// ========== REGISTRATION TOKEN ==========
const char* REGISTRATION_TOKEN = "oc_reg_ibJdNRh-UZ4VsDIhxiI-yYZdMZrS7uUe7fukFeTvADA";

// ========== OPTIMIZED TIMING ==========
const unsigned long BASE_POLL_INTERVAL = 3000;
unsigned long nextCheckTime = 0;
const int JITTER_RANGE_MS = 1500;

unsigned long lastWiFiReconnect = 0;
const unsigned long wifiReconnectInterval = 30000;
int wifiRetryCount = 0;
const int MAX_WIFI_RETRIES = 5;
int registrationRetryCount = 0;
const int MAX_REGISTRATION_RETRIES = 3;
int consecutiveFailures = 0;

// ========== SESSION MANAGEMENT ==========
String sessionId = "";
String permanentToken = "";
bool sessionValid = false;
bool registrationTokenUsed = false;

// ========== DEVICE ID ==========
String deviceId = "";
String macAddress = "";

// ========== RELAY STATES ==========
bool relayStates[5] = {false, false, false, false, false};
bool deviceRegistered = false;

// ========== PIN DEFINITIONS ==========
const int relayPins[] = {D1, D2, D3, D4, D5};
const int relayCount = 5;
#define LED_PIN D4
#define ERROR_LED_PIN D4

// ========== SENSOR VARIABLES ==========
float temperature = -999;
float humidity = -999;

// ========== ERROR HANDLING ==========
void enterErrorState(String reason) {
  Serial.println("");
  Serial.println("========================================");
  Serial.print("[ERROR] Entering error state: ");
  Serial.println(reason);
  Serial.println("========================================");

  currentState = STATE_ERROR;

  for (int i = 0; i < 3; i++) {
    digitalWrite(ERROR_LED_PIN, HIGH);
    delay(500);
    digitalWrite(ERROR_LED_PIN, LOW);
    delay(500);
  }

  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);
}

// ========== EEPROM STORAGE FUNCTIONS ==========

void initEEPROM() {
  Serial.println("[EEPROM] Initializing...");
  EEPROM.begin(EEPROM_SIZE);

  int storedVersion;
  EEPROM.get(VERSION_ADDRESS, storedVersion);
  Serial.printf("[EEPROM] Stored version: %d, Current: %d\n", storedVersion, FIRMWARE_VERSION);

  if (storedVersion != FIRMWARE_VERSION) {
    Serial.println("[EEPROM] Version mismatch - CLEARING EEPROM!");
    for (int i = 0; i < EEPROM_SIZE; i++) {
      EEPROM.write(i, 0);
    }
    EEPROM.put(VERSION_ADDRESS, FIRMWARE_VERSION);
    EEPROM.commit();
    Serial.println("[EEPROM] EEPROM cleared!");
  }
}

void saveTokenToEEPROM(String token) {
  Serial.println("[EEPROM] Saving token...");

  uint16_t magic = TOKEN_MAGIC;
  EEPROM.put(TOKEN_ADDRESS, magic);

  int addr = TOKEN_ADDRESS + sizeof(magic);
  int len = token.length();
  EEPROM.put(addr, len);

  addr += sizeof(len);
  for (int i = 0; i < len; i++) {
    EEPROM.write(addr + i, token[i]);
  }

  uint16_t checksum = 0;
  for (int i = 0; i < len; i++) {
    checksum += token[i];
  }
  EEPROM.put(addr + len, checksum);

  EEPROM.commit();
  Serial.printf("[EEPROM] Token saved\n");
}

String loadTokenFromEEPROM() {
  Serial.println("[EEPROM] Loading token...");

  uint16_t magic;
  EEPROM.get(TOKEN_ADDRESS, magic);
  if (magic != TOKEN_MAGIC) {
    Serial.println("[EEPROM] No valid token found");
    return "";
  }

  int addr = TOKEN_ADDRESS + sizeof(magic);
  int len;
  EEPROM.get(addr, len);
  if (len <= 0 || len > 200) {
    Serial.println("[EEPROM] Invalid token length");
    return "";
  }

  addr += sizeof(len);
  char buffer[256];
  for (int i = 0; i < len; i++) {
    buffer[i] = EEPROM.read(addr + i);
  }
  buffer[len] = '\0';

  uint16_t storedChecksum;
  EEPROM.get(addr + len, storedChecksum);
  uint16_t calculatedChecksum = 0;
  for (int i = 0; i < len; i++) {
    calculatedChecksum += buffer[i];
  }

  if (storedChecksum != calculatedChecksum) {
    Serial.println("[EEPROM] Checksum mismatch!");
    return "";
  }

  String token = String(buffer);
  Serial.printf("[EEPROM] Loaded token (length: %d)\n", token.length());
  return token;
}

void clearTokenFromEEPROM() {
  Serial.println("[EEPROM] Clearing token...");
  for (int i = 0; i < 64; i++) {
    EEPROM.write(TOKEN_ADDRESS + i, 0);
  }
  EEPROM.commit();
}

void saveSessionToEEPROM(String session) {
  Serial.printf("[EEPROM] Saving session: %s\n", session.c_str());
  uint16_t magic = SESSION_MAGIC;
  EEPROM.put(SESSION_ADDRESS, magic);

  int addr = SESSION_ADDRESS + sizeof(magic);
  int len = session.length();
  EEPROM.put(addr, len);

  addr += sizeof(len);
  for (int i = 0; i < len; i++) {
    EEPROM.write(addr + i, session[i]);
  }
  EEPROM.commit();
}

String loadSessionFromEEPROM() {
  uint16_t magic;
  EEPROM.get(SESSION_ADDRESS, magic);
  if (magic != SESSION_MAGIC) return "";

  int addr = SESSION_ADDRESS + sizeof(magic);
  int len;
  EEPROM.get(addr, len);
  if (len <= 0 || len > 200) return "";

  addr += sizeof(len);
  char buffer[256];
  for (int i = 0; i < len; i++) {
    buffer[i] = EEPROM.read(addr + i);
  }
  buffer[len] = '\0';
  return String(buffer);
}

void clearSessionFromEEPROM() {
  for (int i = 0; i < 32; i++) {
    EEPROM.write(SESSION_ADDRESS + i, 0);
  }
  EEPROM.commit();
}

void markRegistrationUsed() {
  Serial.println("[EEPROM] Marking registration as used...");
  uint16_t magic = FLAG_MAGIC;
  EEPROM.put(FLAG_ADDRESS, magic);
  EEPROM.commit();
}

bool isRegistrationUsed() {
  uint16_t magic;
  EEPROM.get(FLAG_ADDRESS, magic);
  return (magic == FLAG_MAGIC);
}

void clearAllStorage() {
  Serial.println("[EEPROM] Clearing all storage...");
  clearTokenFromEEPROM();
  clearSessionFromEEPROM();
  for (int i = 0; i < 32; i++) {
    EEPROM.write(FLAG_ADDRESS + i, 0);
  }
  EEPROM.commit();
}

// ========== PERSISTENT DEVICE ID FUNCTIONS ==========

void saveDeviceIdToEEPROM(String id) {
  Serial.println("[EEPROM] Saving Device ID...");
  
  uint16_t magic = DEVICE_ID_MAGIC;
  EEPROM.put(DEVICE_ID_ADDRESS, magic);
  
  int addr = DEVICE_ID_ADDRESS + sizeof(magic);
  int len = id.length();
  EEPROM.put(addr, len);
  
  addr += sizeof(len);
  for (int i = 0; i < len; i++) {
    EEPROM.write(addr + i, id[i]);
  }
  
  uint16_t checksum = 0;
  for (int i = 0; i < len; i++) {
    checksum += id[i];
  }
  EEPROM.put(addr + len, checksum);
  
  EEPROM.commit();
  Serial.printf("[EEPROM] Device ID saved: %s\n", id.c_str());
}

String loadDeviceIdFromEEPROM() {
  uint16_t magic;
  EEPROM.get(DEVICE_ID_ADDRESS, magic);
  if (magic != DEVICE_ID_MAGIC) {
    Serial.println("[EEPROM] No saved Device ID found");
    return "";
  }
  
  int addr = DEVICE_ID_ADDRESS + sizeof(magic);
  int len;
  EEPROM.get(addr, len);
  if (len <= 0 || len > 50) return "";
  
  addr += sizeof(len);
  char buffer[64];
  for (int i = 0; i < len; i++) {
    buffer[i] = EEPROM.read(addr + i);
  }
  buffer[len] = '\0';
  
  uint16_t storedChecksum;
  EEPROM.get(addr + len, storedChecksum);
  uint16_t calculatedChecksum = 0;
  for (int i = 0; i < len; i++) {
    calculatedChecksum += buffer[i];
  }
  
  if (storedChecksum != calculatedChecksum) {
    Serial.println("[EEPROM] Device ID checksum mismatch!");
    return "";
  }
  
  String id = String(buffer);
  Serial.printf("[EEPROM] Loaded Device ID: %s\n", id.c_str());
  return id;
}

String generateRandomDeviceId() {
  uint8_t mac[6];
  WiFi.macAddress(mac);
  
  // Generate random numbers
  uint32_t random1 = ESP.getCycleCount();
  uint32_t random2 = ESP.getCycleCount();
  
  // Create a unique ID combining MAC and random numbers
  char buffer[32];
  snprintf(buffer, sizeof(buffer), "D1L_%02X%02X%02X%02X%02X%02X_%08X",
           mac[0], mac[1], mac[2], mac[3], mac[4], mac[5], random1);
  
  String deviceId = String(buffer);
  Serial.printf("[DEVICE_ID] Generated random ID: %s\n", deviceId.c_str());
  return deviceId;
}

String getOrCreateDeviceId() {
  // First, try to load from EEPROM
  String savedId = loadDeviceIdFromEEPROM();
  if (savedId.length() > 0) {
    return savedId;
  }
  
  // If not found, generate a new random ID and save it
  String newId = generateRandomDeviceId();
  saveDeviceIdToEEPROM(newId);
  return newId;
}

// ========== HELPER FUNCTIONS ==========

uint8_t packRelays() {
  uint8_t packed = 0;
  if (relayStates[0]) packed |= 1 << 0;
  if (relayStates[1]) packed |= 1 << 1;
  if (relayStates[2]) packed |= 1 << 2;
  if (relayStates[3]) packed |= 1 << 3;
  if (relayStates[4]) packed |= 1 << 4;
  return packed;
}

void unpackRelays(uint8_t packed) {
  for (int i = 0; i < 5; i++) {
    bool state = (packed >> i) & 1;
    if (state != relayStates[i]) {
      setRelay(i, state);
      Serial.printf("[RELAY] %s set to %s\n", relayNames[i], state ? "ON" : "OFF");
    }
  }
}

void setRelay(int index, bool state) {
  // Apply per-relay logic
    // Relay 1: Negative logic (LOW = ON)
  digitalWrite(relayPins[0], state ? LOW : HIGH);
  // Relay 2: Positive logic (HIGH = ON)
  digitalWrite(relayPins[1], state ? HIGH : LOW);
  // Relay 3: Positive logic (HIGH = ON)
  digitalWrite(relayPins[2], state ? HIGH : LOW);
  // Relay 4: Positive logic (HIGH = ON)
  digitalWrite(relayPins[3], state ? HIGH : LOW);
  // Relay 5: Positive logic (HIGH = ON)
  digitalWrite(relayPins[4], state ? HIGH : LOW);
  relayStates[index] = state;
}

void initPins() {
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, HIGH);
  for (int i = 0; i < relayCount; i++) {
    pinMode(relayPins[i], OUTPUT);
    setRelay(i, false);
  }
    dht.begin();
  Serial.println("[INIT] Pins initialized");
  Serial.println("[RELAY] Custom relay names configured:");
  for (int i = 0; i < 5; i++) {
    Serial.printf("  Relay %d: %s\n", i+1, relayNames[i]);
  }
}

void readSensor() {
  
  static unsigned long lastDHTRead = 0;
  unsigned long now = millis();

  if (now - lastDHTRead >= 2000) {
    lastDHTRead = now;
    temperature = dht.readTemperature();
    humidity = dht.readHumidity();

    if (isnan(temperature) || isnan(humidity)) {
      temperature = -999;
      humidity = -999;
      Serial.println("[SENSOR] DHT22 read failed - sending error code -999");
    } else if (temperature < -50 || temperature > 80) {
      temperature = -999;
      Serial.println("[SENSOR] DHT22 temperature out of range - sending error code -999");
    } else {
      Serial.printf("[SENSOR] DHT22 - Temp: %.1f°C, Hum: %.1f%%\n", temperature, humidity);
    }
  }
}

bool connectToWiFi() {
  if (WiFi.status() == WL_CONNECTED) return true;
  
  Serial.printf("[WiFi] Attempting connection to SSID: %s\n", WIFI_SSID);
  Serial.print("[WiFi] Connecting");
  
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  
  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 40) {
    delay(500);
    Serial.print(".");
    attempts++;
  }
  
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println(" Connected!");
    Serial.printf("[WiFi] IP Address: %s\n", WiFi.localIP().toString().c_str());
    Serial.printf("[WiFi] Signal Strength (RSSI): %d dBm\n", WiFi.RSSI());
    wifiRetryCount = 0;
    consecutiveFailures = 0;
    return true;
  }
  
  Serial.printf(" Failed! (Reason: %d)\n", WiFi.status());
  wifiRetryCount++;
  return false;
}

void registerDevice() {
  if (currentState != STATE_PROVISIONING) return;
  if (registrationTokenUsed) return;
  if (registrationRetryCount >= MAX_REGISTRATION_RETRIES) {
    enterErrorState("Registration failed");
    return;
  }
  if (WiFi.status() != WL_CONNECTED) return;

  Serial.println("[REG] Registering device...");
  Serial.printf("[REG] Registration token: %s\n", REGISTRATION_TOKEN);
  Serial.printf("[REG] Device ID: %s\n", deviceId.c_str());

  WiFiClientSecure client;
  client.setInsecure();
  HTTPClient http;

  String url = "https://";
  url += serverHost;
  url += "/api/device/register/";
  Serial.printf("[REG] URL: %s\n", url.c_str());

  JsonDocument doc;
  doc["token"] = REGISTRATION_TOKEN;
  doc["device_id"] = deviceId;
  doc["mac_address"] = "";
  doc["ip"] = WiFi.localIP().toString();
  doc["cpu_freq"] = ESP.getCpuFreqMHz();
  doc["free_heap"] = ESP.getFreeHeap();
  doc["flash_size"] = ESP.getFlashChipSize();

  String jsonString;
  serializeJson(doc, jsonString);
  Serial.printf("[REG] Payload: %s\n", jsonString.c_str());

  if (http.begin(client, url)) {
    http.addHeader("Content-Type", "application/json");
    int code = http.POST(jsonString);
    Serial.printf("[REG] HTTP Response: %d\n", code);

    if (code == 200) {
      String payload = http.getString();
      JsonDocument response;
      deserializeJson(response, payload);

      if (response["success"]) {
        permanentToken = response["token"].as<String>();
        deviceRegistered = true;
        registrationTokenUsed = true;
        currentState = STATE_REGISTERED;

        saveTokenToEEPROM(permanentToken);
        markRegistrationUsed();

        String verifyToken = loadTokenFromEEPROM();
        if (verifyToken == permanentToken) {
          Serial.println("[EEPROM] Token verified!");
        } else {
          Serial.println("[EEPROM] Token verification FAILED!");
        }

        Serial.println("[REG] ✓ Registration successful!");
        registrationRetryCount = 0;
        getSession();
      }
    } else if (code == 429) {
      registrationRetryCount++;
    } else if (code == 401) {
      Serial.println("[REG] Token already used or expired");
      registrationTokenUsed = true;
    }
    http.end();
  }
}

void getSession() {
  if (WiFi.status() != WL_CONNECTED) return;
  if (permanentToken.length() == 0) return;

  Serial.println("[SESSION] Getting session...");

  WiFiClientSecure client;
  client.setInsecure();
  HTTPClient http;

  String url = "https://";
  url += serverHost;
  url += "/api/device/state/?token=";
  url += permanentToken;
  url += "&device_id=";
  url += deviceId;
  url += "&ip=";
  url += WiFi.localIP().toString();
  url += "&cpu_freq=";
  url += String(ESP.getCpuFreqMHz());
  url += "&free_heap=";
  url += String(ESP.getFreeHeap());
  url += "&flash_size=";
  url += String(ESP.getFlashChipSize());

  if (http.begin(client, url)) {
    int code = http.GET();
    Serial.printf("[SESSION] HTTP Response: %d\n", code);

    if (code == 200) {
      String payload = http.getString();
      JsonDocument doc;
      deserializeJson(doc, payload);

      if (doc["session"].is<String>()) {
        sessionId = doc["session"].as<String>();
        sessionValid = true;
        saveSessionToEEPROM(sessionId);
        Serial.printf("[SESSION] Got session ID: %s\n", sessionId.c_str());
      }
    } else if (code == 401) {
      Serial.println("[SESSION] Token invalid - will re-register");
      deviceRegistered = false;
      sessionValid = false;
      permanentToken = "";
      sessionId = "";
      clearAllStorage();
      currentState = STATE_PROVISIONING;
    }
    http.end();
  }
}

void updateDeviceState() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[UPDATE] WiFi not connected - skipping update");
    return;
  }
  if (!deviceRegistered) {
    Serial.println("[UPDATE] Device not registered - skipping update");
    return;
  }
  if (currentState != STATE_REGISTERED) {
    Serial.printf("[UPDATE] Invalid state: %d - skipping update\n", currentState);
    return;
  }

  readSensor();

  WiFiClientSecure client;
  client.setInsecure();
  HTTPClient http;

  String url = "https://";
  url += serverHost;
  url += "/api/device/state/";

  if (sessionValid && sessionId.length() > 0) {
    url += "?session=";
    url += sessionId;
    Serial.printf("[UPDATE] Using session: %s\n", sessionId.c_str());
  } else if (permanentToken.length() > 0) {
    url += "?token=";
    url += permanentToken;
    Serial.printf("[UPDATE] Using token\n");
  } else {
    Serial.println("[UPDATE] No session or token available!");
    return;
  }

  url += "&device_id=";
  url += deviceId;

  uint8_t packed = packRelays();
  url += "&s=";
  url += String(packed, HEX);

  // ALWAYS send temperature data - even if it's -999 (error code)
  int tempInt = (int)(temperature * 10);
  url += "&t=";
  url += String(tempInt);
  
  Serial.printf("[UPDATE] Sending temperature: %.1f°C (raw: %d)\n", temperature, tempInt);

  if (humidity >= 0 && humidity <= 100) {
    url += "&h=";
    url += String((int)humidity);
  }

  url += "&ip=";
  url += WiFi.localIP().toString();
  url += "&cpu_freq=";
  url += String(ESP.getCpuFreqMHz());
  url += "&free_heap=";
  url += String(ESP.getFreeHeap());
  url += "&flash_size=";
  url += String(ESP.getFlashChipSize());

  Serial.println("[UPDATE] ========== FULL URL ==========");
  Serial.println(url);
  Serial.println("[UPDATE] ===============================");

  if (http.begin(client, url)) {
    http.setTimeout(8000);
    int code = http.GET();
    Serial.printf("[UPDATE] HTTP Response Code: %d\n", code);

    if (code == 200) {
      String payload = http.getString();
      JsonDocument doc;
      deserializeJson(doc, payload);
      
      Serial.printf("[UPDATE] Response received, size: %d\n", payload.length());

      if (doc["session"].is<String>()) {
        sessionId = doc["session"].as<String>();
        sessionValid = true;
        saveSessionToEEPROM(sessionId);
        Serial.printf("[UPDATE] New session received: %s\n", sessionId.c_str());
      }

      // Process relay states
      bool relayChanged = false;
      for (int i = 0; i < 5; i++) {
        String key = "relay" + String(i+1);
        if (doc[key].is<bool>()) {
          bool newState = doc[key];
          if (newState != relayStates[i]) {
            setRelay(i, newState);
            relayChanged = true;
            Serial.printf("[RELAY] %s set to %s (from server)\n", relayNames[i], newState ? "ON" : "OFF");
          }
        }
      }

      if (!relayChanged && doc["s"].is<String>()) {
        uint8_t serverState = (uint8_t)strtol(doc["s"], NULL, 16);
        uint8_t currentRelayState = packRelays();
        if (serverState != currentRelayState) {
          unpackRelays(serverState);
          Serial.printf("[RELAY] Updated from packed state: %02X\n", serverState);
        }
      }

      consecutiveFailures = 0;
      Serial.print(".");
    } else if (code == 401) {
      Serial.println("[UPDATE] Session invalid - attempting to recover with token");
      
      // Try to get a new session using the permanent token
      if (permanentToken.length() > 0) {
        getSession();
        if (sessionValid) {
          Serial.println("[UPDATE] Successfully renewed session!");
          consecutiveFailures = 0;
        } else {
          Serial.println("[UPDATE] Failed to renew session. Will re-register.");
          deviceRegistered = false;
          sessionValid = false;
          currentState = STATE_PROVISIONING;
          clearAllStorage();
        }
      } else {
        Serial.println("[UPDATE] No permanent token available. Forcing re-registration.");
        deviceRegistered = false;
        sessionValid = false;
        currentState = STATE_PROVISIONING;
        clearAllStorage();
      }
      consecutiveFailures++;
    } else if (code == 0) {
      Serial.println("[UPDATE] Connection failed - no response");
      consecutiveFailures++;
    } else {
      Serial.printf("[UPDATE] Unexpected response: %d\n", code);
      consecutiveFailures++;
    }
    
    http.end();
  } else {
    Serial.println("[UPDATE] HTTP connection initialization failed!");
    consecutiveFailures++;
  }
  
  // If too many failures, force re-registration
  if (consecutiveFailures > 10) {
    Serial.println("[UPDATE] Too many failures - forcing re-registration");
    deviceRegistered = false;
    sessionValid = false;
    permanentToken = "";
    sessionId = "";
    clearAllStorage();
    currentState = STATE_PROVISIONING;
    consecutiveFailures = 0;
  }
}

void checkWiFiAndReconnect() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.printf("[WiFi] Connection lost! Status: %d\n", WiFi.status());
    if (connectToWiFi()) {
      Serial.println("[WiFi] Reconnected successfully!");
      // After reconnection, try to recover session
      if (deviceRegistered && !sessionValid) {
        getSession();
      }
    } else {
      Serial.println("[WiFi] Reconnection failed!");
    }
  }
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("");
  Serial.println("########################################");
  Serial.println("# OceanicRemote v5.0 - ESP8266 D1 Large #");
  Serial.println("# Persistent Device ID & Recovery Logic #");
  Serial.println("########################################");

  initEEPROM();

  randomSeed(analogRead(A0));
  
  // Get or create persistent Device ID (stored in EEPROM)
  deviceId = getOrCreateDeviceId();
  
  initPins();

  Serial.printf("[SYSTEM] Device ID: %s\n", deviceId.c_str());
  Serial.printf("[SYSTEM] Server: %s\n", serverHost);
  Serial.printf("[SYSTEM] Polling interval: %d ms\n", BASE_POLL_INTERVAL);
  Serial.printf("[SYSTEM] WiFi SSID: %s\n", WIFI_SSID);
  Serial.printf("[SYSTEM] Free heap: %d bytes\n", ESP.getFreeHeap());

  String savedToken = loadTokenFromEEPROM();
  if (savedToken.length() > 0) {
    permanentToken = savedToken;
    deviceRegistered = true;
    currentState = STATE_REGISTERED;
    Serial.printf("[BOOT] Found saved token (length: %d)\n", permanentToken.length());

    String savedSession = loadSessionFromEEPROM();
    if (savedSession.length() > 0) {
      sessionId = savedSession;
      sessionValid = true;
      Serial.printf("[BOOT] Found saved session: %s\n", sessionId.c_str());
    } else {
      Serial.println("[BOOT] No saved session found");
    }
  } else {
    if (isRegistrationUsed()) {
      Serial.println("[BOOT] Device was registered but token missing! Will re-register.");
    } else {
      Serial.println("[BOOT] No token found - will register as NEW device");
      currentState = STATE_PROVISIONING;
      registrationTokenUsed = false;
      registrationRetryCount = 0;
    }
  }

  Serial.println("[PHASE 1] Connecting to WiFi...");
  if (!connectToWiFi()) {
    Serial.println("[ERROR] Cannot connect to WiFi - will retry in loop");
  }

  Serial.println("[PHASE 2] Establishing server connection...");
  if (!deviceRegistered && currentState == STATE_PROVISIONING) {
    registerDevice();
  }

  if (deviceRegistered && currentState == STATE_REGISTERED) {
    if (!sessionValid || sessionId.length() == 0) {
      getSession();
    }
    updateDeviceState();
  }

  nextCheckTime = millis() + random(0, 5000);
  Serial.println("[PHASE 3] Entering main loop...");
  Serial.println("[INFO] Device will now poll server every 3 seconds");
}

void loop() {
  if (currentState == STATE_ERROR) {
    delay(5000);
    return;
  }

  unsigned long currentMillis = millis();
  
  // Check WiFi connection periodically
  static unsigned long lastWiFiCheck = 0;
  if (currentMillis - lastWiFiCheck > 10000) {
    lastWiFiCheck = currentMillis;
    checkWiFiAndReconnect();
  }

  if (WiFi.status() == WL_CONNECTED) {
    wifiRetryCount = 0;

    if (currentMillis >= nextCheckTime) {
      int jitter = random(-JITTER_RANGE_MS, JITTER_RANGE_MS);
      nextCheckTime = currentMillis + BASE_POLL_INTERVAL + jitter;

      if (deviceRegistered && currentState == STATE_REGISTERED) {
        updateDeviceState();
      } else if (!deviceRegistered && currentState == STATE_PROVISIONING) {
        registerDevice();
      }
    }
  } else {
    if (currentMillis - lastWiFiReconnect >= wifiReconnectInterval) {
      lastWiFiReconnect = currentMillis;
      if (wifiRetryCount < MAX_WIFI_RETRIES) {
        Serial.printf("[WiFi] Attempting reconnect (%d/%d)...\n", wifiRetryCount + 1, MAX_WIFI_RETRIES);
        connectToWiFi();
        if (WiFi.status() == WL_CONNECTED && deviceRegistered && !sessionValid) {
          getSession();
        }
      } else {
        Serial.println("[WiFi] Max retries reached. Restarting...");
        delay(1000);
        ESP.restart();
      }
    }
  }

  delay(10);
}