📖 About This Project
Turn your ESP8266 D1 Mini into a powerful 5-relay controller. Uses negative logic (LOW activates relay). Great for smart home lighting, garden watering systems, and remote appliance control. The compact D1 Mini fits easily inside electrical boxes. Features secure token authentication, session management, and offline backup. All 5 relays can be controlled independently from the OceanRemote dashboard. Requires external 5V power for the relay module.
💻 Firmware Code
C++ (Arduino)
// OceanicRemote Secure Firmware - ESP8266 D1 Mini
// Version 5.1 - CORRECTED INDEPENDENT GPIO CONTROL
// Pin mapping: D1=GPIO5, D2=GPIO4, D3=GPIO0, D6=GPIO12, D5=GPIO14
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include <EEPROM.h>
// ========== 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 51
// ========== 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_AV11KCbqZcuezpmw2PFQPlOIAGnzuLp36lrn3bJ9QbQ";
// ========== 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;
// ***** CORRECT ESP8266 D1 MINI PIN DEFINITIONS *****
// D1 = GPIO5, D2 = GPIO4, D3 = GPIO0, D6 = GPIO12, D5 = GPIO14
const int relayPins[] = {5, 4, 0, 12, 14};
const int relayCount = 5;
#define LED_PIN 2 // Built-in LED on GPIO2 (active LOW) - NOT used for relays anymore
// ========== 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, LOW); // LED ON (active LOW)
delay(500);
digitalWrite(LED_PIN, HIGH); // LED OFF
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 "ESP_" + 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
// Each case controls ONE and ONLY ONE pin
switch(index) {
case 0:
// Relay 1: Negative logic (LOW = ON) - D1/GPIO5
digitalWrite(5, state ? LOW : HIGH);
break;
case 1:
// Relay 2: Positive logic (HIGH = ON) - D2/GPIO4
digitalWrite(4, state ? HIGH : LOW);
break;
case 2:
// Relay 3: Positive logic (HIGH = ON) - D3/GPIO0
digitalWrite(0, state ? HIGH : LOW);
break;
case 3:
// Relay 4: Positive logic (HIGH = ON) - D6/GPIO12
digitalWrite(12, state ? HIGH : LOW);
break;
case 4:
// Relay 5: Positive logic (HIGH = ON) - D5/GPIO14
digitalWrite(14, state ? HIGH : LOW);
break;
default:
break;
}
relayStates[index] = state;
}
void initPins() {
// Built-in LED on GPIO2 - active LOW, NOT used for relays
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, HIGH); // LED OFF (active LOW)
// Initialize relay pins with proper initial states
pinMode(5, OUTPUT);
digitalWrite(5, HIGH); // Relay 1 OFF (D1/GPIO5, negative logic)
pinMode(4, OUTPUT);
digitalWrite(4, LOW); // Relay 2 OFF (D2/GPIO4, positive logic)
pinMode(0, OUTPUT);
digitalWrite(0, LOW); // Relay 3 OFF (D3/GPIO0, positive logic)
pinMode(12, OUTPUT);
digitalWrite(12, LOW); // Relay 4 OFF (D6/GPIO12, positive logic)
pinMode(14, OUTPUT);
digitalWrite(14, LOW); // Relay 5 OFF (D5/GPIO14, positive logic)
Serial.println("");
Serial.println("[INIT] Pins initialized");
Serial.println("[RELAY] ESP8266 D1 Mini GPIO mappings:");
Serial.println(" Relay1: GPIO5 (D1) - Physical Pin D1 ✅ Safe");
Serial.println(" Relay2: GPIO4 (D2) - Physical Pin D2 ✅ Safe");
Serial.println(" Relay3: GPIO0 (D3) - Physical Pin D3 ⚠️ Boot strapping pin");
Serial.println(" Relay4: GPIO12 (D6) - Physical Pin D6 ✅ Safe - NO LED conflict!");
Serial.println(" Relay5: GPIO14 (D5) - Physical Pin D5 ✅ Safe");
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] Built-in LED on GPIO2 is INDEPENDENT (not used for relays)");
Serial.println("[INFO] All relays control their own pins independently!");
}
void readSensor() {
temperature = -999;
humidity = -999;
Serial.println("[SENSOR] No sensor configured - sending error code -999");
}
bool connectToWiFi() {
if (WiFi.status() == WL_CONNECTED) return true;
Serial.print("[WiFi] Connecting to ");
Serial.print(WIFI_SSID);
Serial.print("... ");
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: %s\n", WiFi.localIP().toString().c_str());
wifiRetryCount = 0;
return true;
}
Serial.println(" Failed!");
wifiRetryCount++;
return false;
}
// ========== SSL/TLS Helper for ESP8266 ==========
WiFiClientSecure* getSecureClient() {
WiFiClientSecure* client = new WiFiClientSecure();
client->setInsecure();
return client;
}
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 = getSecureClient();
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();
}
delete client;
}
void getSession() {
if (WiFi.status() != WL_CONNECTED) return;
if (permanentToken.length() == 0) return;
Serial.println("[SESSION] Getting session...");
WiFiClientSecure* client = getSecureClient();
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();
}
delete client;
}
void updateDeviceState() {
if (WiFi.status() != WL_CONNECTED) return;
if (!deviceRegistered) return;
if (currentState != STATE_REGISTERED) return;
readSensor();
WiFiClientSecure* client = getSecureClient();
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 {
delete client;
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);
}
}
// Brief blink to show activity
digitalWrite(LED_PIN, LOW);
delay(20);
digitalWrite(LED_PIN, HIGH);
} else if (code == 401) {
Serial.println("[STATE] Token invalid");
deviceRegistered = false;
sessionValid = false;
permanentToken = "";
sessionId = "";
clearAllStorage();
currentState = STATE_PROVISIONING;
}
http.end();
}
delete client;
}
void setup() {
Serial.begin(115200);
delay(2000);
Serial.println("");
Serial.println("########################################");
Serial.println("# OceanicRemote v5.1 - ESP8266 D1 Mini #");
Serial.println("# D6 REPLACES D4 - NO LED CONFLICT #");
Serial.println("########################################");
initEEPROM();
randomSeed(analogRead(A0) + 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);
ESP.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);
ESP.restart();
}
}
}
delay(10);
}