📖 About This Project

Create a smart fan controller with Raspberry Pi Pico W, DHT22 sensor, and relay. The Pico W monitors temperature continuously. When temperature exceeds threshold, the relay activates the fan. When temperature normalizes, fan stops. Manual control available via OceanRemote dashboard from anywhere. Uses negative logic relay (LOW = ON). Perfect for 3D printer enclosures, grow tents, AV cabinets, or any space needing automated cooling.

💻 Firmware Code

C++ (Arduino)
// OceanicRemote Secure Firmware - Raspberry Pi Pico W
// Version 5.4 - FULLY INDEPENDENT GPIO PINS (11,12,13,14,15)
// EACH RELAY CONTROLS ITS OWN PIN - NO CROSS TALK

#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include <EEPROM.h>


#include <DHT.h>
#define DHTPIN 26
#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 FIRMWARE_VERSION 54

// ========== 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
};

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_0cZBvLXFFwhTBQahwh9yeqNwvTnOYJ5syJ4c1xMEB5U";

// ========== 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;

// ========== 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;

// ***** CORRECTED PIN DEFINITIONS - FULLY INDEPENDENT *****
// GP11 (Pin15) -> Relay 1
// GP12 (Pin16) -> Relay 2
// GP13 (Pin17) -> Relay 3
// GP14 (Pin19) -> Relay 4
// GP15 (Pin20) -> Relay 5
const int relayPins[] = {11, 12, 13, 14, 15};
const int relayCount = 5;
#define LED_PIN 25  // Built-in LED (GP25 - independent)

// ========== 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(LED_PIN, HIGH);
    delay(500);
    digitalWrite(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\n");
  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();
}

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

String generateDeviceId() {
  uint8_t mac[6];
  WiFi.macAddress(mac);
  macAddress = String(mac[0], HEX) + String(mac[1], HEX) + String(mac[2], HEX) +
               String(mac[3], HEX) + String(mac[4], HEX) + String(mac[5], HEX);
  macAddress.toUpperCase();
  return "PICO_" + macAddress;
}

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 with independent pin control
  switch(index) {
    case 0:
      // Relay 1: Negative logic (LOW = ON) - GPIO11
      digitalWrite(11, state ? LOW : HIGH);
      break;
    case 1:
      // Relay 2: Positive logic (HIGH = ON) - GPIO12
      digitalWrite(12, state ? HIGH : LOW);
      break;
    case 2:
      // Relay 3: Positive logic (HIGH = ON) - GPIO13
      digitalWrite(13, state ? HIGH : LOW);
      break;
    case 3:
      // Relay 4: Positive logic (HIGH = ON) - GPIO14
      digitalWrite(14, state ? HIGH : LOW);
      break;
    case 4:
      // Relay 5: Positive logic (HIGH = ON) - GPIO15
      digitalWrite(15, state ? HIGH : LOW);
      break;
    default:
      break;
  }
  relayStates[index] = state;
}

void initPins() {
  // Built-in LED on GPIO25
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
  
  // Initialize relay pins with proper initial states
  pinMode(11, OUTPUT);
  digitalWrite(11, HIGH);  // Relay 1 OFF (negative logic)
  pinMode(12, OUTPUT);
  digitalWrite(12, LOW);   // Relay 2 OFF (positive logic)
  pinMode(13, OUTPUT);
  digitalWrite(13, LOW);   // Relay 3 OFF (positive logic)
  pinMode(14, OUTPUT);
  digitalWrite(14, LOW);   // Relay 4 OFF (positive logic)
  pinMode(15, OUTPUT);
  digitalWrite(15, LOW);   // Relay 5 OFF (positive logic)
  
    dht.begin();
  Serial.println("");
  Serial.println("[INIT] Pins initialized");
  Serial.println("[RELAY] GPIO mappings (FULLY INDEPENDENT):");
  Serial.println("  Relay1: GPIO11 (Physical Pin 15)");
  Serial.println("  Relay2: GPIO12 (Physical Pin 16)");
  Serial.println("  Relay3: GPIO13 (Physical Pin 17)");
  Serial.println("  Relay4: GPIO14 (Physical Pin 19)");
  Serial.println("  Relay5: GPIO15 (Physical Pin 20)");
  Serial.println("");
  Serial.println("[RELAY] Custom relay names:");
  for (int i = 0; i < 5; i++) {
    Serial.printf("  Relay %d: %s\n", i+1, relayNames[i]);
  }
  Serial.println("");
  Serial.println("[INFO] All relay pins are completely independent!");
  Serial.println("[INFO] No cross-talk or internal conflicts with these pins.");
}

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 {
      Serial.printf("[SENSOR] DHT22 - Temp: %.1f°C, Hum: %.1f%%\n", temperature, humidity);
    }
  }
}

bool connectToWiFi() {
  if (WiFi.status() == WL_CONNECTED) return true;
  Serial.print("[WiFi] Connecting to ");
  Serial.print(WIFI_SSID);
  Serial.print("... ");
  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: %s\n", WiFi.localIP().toString().c_str());
    wifiRetryCount = 0;
    return true;
  }
  Serial.println(" Failed!");
  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...");

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

  String url = "https://" + String(serverHost) + "/api/device/register/";

  JsonDocument doc;
  doc["token"] = REGISTRATION_TOKEN;
  doc["device_id"] = deviceId;
  doc["mac_address"] = macAddress;
  doc["ip"] = WiFi.localIP().toString();

  String jsonString;
  serializeJson(doc, jsonString);

  if (http.begin(client, url)) {
    http.addHeader("Content-Type", "application/json");
    int code = http.POST(jsonString);
    Serial.printf("[REG] 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();

        Serial.println("[REG] ✓ Registered!");
        registrationRetryCount = 0;
        getSession();
      }
    } else if (code == 429) {
      registrationRetryCount++;
    }
    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://" + String(serverHost) + "/api/device/state/";
  url += "?token=" + permanentToken;
  url += "&device_id=" + deviceId;
  url += "&ip=" + WiFi.localIP().toString();

  if (http.begin(client, url)) {
    int code = http.GET();
    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: %s\n", sessionId.c_str());
      }
    }
    http.end();
  }
}

void updateDeviceState() {
  if (WiFi.status() != WL_CONNECTED) return;
  if (!deviceRegistered) return;
  if (currentState != STATE_REGISTERED) return;

  readSensor();

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

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

  if (sessionValid && sessionId.length() > 0) {
    url += "?session=" + sessionId;
  } else if (permanentToken.length() > 0) {
    url += "?token=" + permanentToken;
  } else {
    return;
  }

  url += "&device_id=" + deviceId;
  url += "&s=" + String(packRelays(), HEX);
  
  int tempInt = (int)(temperature * 10);
  url += "&t=" + String(tempInt);

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

  url += "&ip=" + WiFi.localIP().toString();

  if (http.begin(client, url)) {
    http.setTimeout(5000);
    int code = http.GET();

    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);
      }

      // Process relay states independently
      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 -> %s\n", relayNames[i], newState ? "ON" : "OFF");
          }
        }
      }

      // Also handle packed format as backup
      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);
        }
      }

      // Blink LED briefly to show activity
      digitalWrite(LED_PIN, HIGH);
      delay(20);
      digitalWrite(LED_PIN, LOW);
      
    } else if (code == 401) {
      Serial.println("[STATE] Token invalid");
      deviceRegistered = false;
      sessionValid = false;
      permanentToken = "";
      sessionId = "";
      clearAllStorage();
      currentState = STATE_PROVISIONING;
    }
    http.end();
  }
}

void setup() {
  Serial.begin(115200);
  delay(2000);
  Serial.println("");
  Serial.println("########################################");
  Serial.println("# OceanicRemote v5.4 - Pico W         #");
  Serial.println("# INDEPENDENT PINS: 11,12,13,14,15    #");
  Serial.println("########################################");

  initEEPROM();

  randomSeed(analogRead(28) + micros());
  deviceId = generateDeviceId();
  initPins();

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

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

    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 token - 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 - restarting");
    delay(5000);
    rp2040.restart();
  }

  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...");
}

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

  unsigned long currentMillis = millis();

  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 (currentMillis - lastWiFiReconnect >= wifiReconnectInterval) {
      lastWiFiReconnect = currentMillis;
      if (wifiRetryCount < MAX_WIFI_RETRIES) {
        Serial.print("[WiFi] Reconnecting... ");
        connectToWiFi();
      } else {
        Serial.println("[WiFi] Max retries - restarting");
        delay(1000);
        rp2040.restart();
      }
    }
  }

  delay(10);
}