Header Ads Widget

Responsive Advertisement

Esp32 Relay Automation Using Firebase + Bluetooth & Wifi

 WebApp View:


Circuit Diagram:





IoT 12V Home Automation: ESP32 & 8-Channel Relay Realtime Control

Welcome back to the workbench! Today, we are building a complete, high-performance IoT Smart Home Automation system using an ESP32 DevKit V1 and a 12V 8-Channel Relay Module. This entire setup interfaces seamlessly with a custom Firebase Realtime Database web dashboard, allowing you to trigger heavy components, studio monitors, lighting setups, or hardware peripherals from anywhere in the world instantly.

ESP32 Source Code Configuration (Version 2 - Safety Update)

This upgraded firmware incorporates critical Power-Restore Safety Logic. When the ESP32 reboots after power comes back, it instantly forces all states to 0 (OFF) in Firebase before entering the main telemetry loop. This prevents hazardous situations where appliances flip back on unprompted. Flash this straight to your microcontroller:

📁 Firmware_ESP32_Relay_v2.ino
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

#define WIFI_SSID "Adeeb Technology Lab YouTuber"
#define WIFI_PASSWORD "03092333121786"

#define FIREBASE_DB_URL "https://fir-esprelay-default-rtdb.firebaseio.com"

#define BLE_DEVICE_NAME "Adeeb Office Remote"
#define BLE_ACCESS_CODE "3121"
#define BLE_SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define BLE_RELAY_CHAR_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

const bool RELAY_ACTIVE_LOW = true;
const int relayPins[8] = {23, 22, 21, 19, 18, 13, 25, 26};
int relayStates[8] = {0, 0, 0, 0, 0, 0, 0, 0};

unsigned long lastReadMs = 0;
const unsigned long readIntervalMs = 250;
unsigned long skipFirebaseReadUntilMs = 0;

WiFiClientSecure secureClient;
BLECharacteristic *relayCharacteristic = nullptr;
bool bluetoothConnected = false;
bool bluetoothAuthorized = false;

void writeRelay(int index, int state) {
  int pinLevel;
  if (RELAY_ACTIVE_LOW) {
    pinLevel = state == 1 ? LOW : HIGH;
  } else {
    pinLevel = state == 1 ? HIGH : LOW;
  }
  digitalWrite(relayPins[index], pinLevel);
}

void setRelayState(int index, int state, bool notifyApp) {
  if (index < 0 || index >= 8) return;

  relayStates[index] = state == 1 ? 1 : 0;
  writeRelay(index, relayStates[index]);

  Serial.print("CH");
  Serial.print(index + 1);
  Serial.print(" = ");
  Serial.println(relayStates[index]);

  if (notifyApp) {
    notifyRelayStates();
  }
}

void allRelaysOff(bool notifyApp) {
  for (int i = 0; i < 8; i++) {
    relayStates[i] = 0;
    writeRelay(i, 0);
  }
  if (notifyApp) {
    notifyRelayStates();
  }
}

String relayStateMessage() {
  String message = "";
  for (int i = 0; i < 8; i++) {
    if (i > 0) message += ",";
    message += "r";
    message += String(i + 1);
    message += "=";
    message += String(relayStates[i]);
  }
  return message;
}

void notifyRelayStates() {
  if (!bluetoothConnected || relayCharacteristic == nullptr) return;

  String message = relayStateMessage();
  relayCharacteristic->setValue(message.c_str());
  relayCharacteristic->notify();
}

bool writeFirebasePath(String path, String value) {
  if (WiFi.status() != WL_CONNECTED) connectWiFi();
  if (WiFi.status() != WL_CONNECTED) return false;

  HTTPClient http;
  String url = String(FIREBASE_DB_URL) + path + ".json";

  http.begin(secureClient, url);
  http.addHeader("Content-Type", "application/json");
  int httpCode = http.PUT(value);
  http.end();

  if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_NO_CONTENT) return true;
  return false;
}

void writeRelayToFirebase(int index) {
  String path = "/relays/r" + String(index + 1);
  writeFirebasePath(path, String(relayStates[index]));
}

void writeAllRelaysToFirebase() {
  String json = "{";
  for (int i = 0; i < 8; i++) {
    if (i > 0) json += ",";
    json += "\"r" + String(i + 1) + "\":" + String(relayStates[i]);
  }
  json += "}";
  writeFirebasePath("/relays", json);
}

void handleBluetoothCommand(String command) {
  command.trim();
  command.toLowerCase();
  if (command == "code=" BLE_ACCESS_CODE || command == "pin=" BLE_ACCESS_CODE) {
    bluetoothAuthorized = true;
    Serial.println("Bluetooth code accepted");
    notifyRelayStates();
    return;
  }

  if (!bluetoothAuthorized) {
    if (bluetoothConnected && relayCharacteristic != nullptr) {
      relayCharacteristic->setValue("auth=0");
      relayCharacteristic->notify();
    }
    return;
  }

  if (command == "get") {
    notifyRelayStates();
    return;
  }

  if (command == "all=0") {
    allRelaysOff(true);
    skipFirebaseReadUntilMs = millis() + 2500;
    writeAllRelaysToFirebase();
    return;
  }

  if (command.length() == 4 && command[0] == 'r' && command[2] == '=') {
    int relayNumber = command[1] - '0';
    int state = command[3] - '0';

    if (relayNumber >= 1 && relayNumber <= 8 && (state == 0 || state == 1)) {
      setRelayState(relayNumber - 1, state, true);
      skipFirebaseReadUntilMs = millis() + 2500;
      writeRelayToFirebase(relayNumber - 1);
    }
  }
}

class RelayBluetoothCallbacks : public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic *characteristic) {
    String command = characteristic->getValue().c_str();
    handleBluetoothCommand(command);
  }
};

class RelayServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer *server) {
    bluetoothConnected = true;
    bluetoothAuthorized = false;
    Serial.println("Bluetooth Connected!");
  }
  void onDisconnect(BLEServer *server) {
    bluetoothConnected = false;
    bluetoothAuthorized = false;
    Serial.println("Bluetooth Disconnected!");
    server->startAdvertising();
  }
};

void setupBluetooth() {
  BLEDevice::init(BLE_DEVICE_NAME);
  BLEDevice::setPower(ESP_PWR_LVL_P9);
  BLEServer *server = BLEDevice::createServer();
  server->setCallbacks(new RelayServerCallbacks());

  BLEService *service = server->createService(BLE_SERVICE_UUID);
  relayCharacteristic = service->createCharacteristic(
    BLE_RELAY_CHAR_UUID,
    BLECharacteristic::PROPERTY_READ |
    BLECharacteristic::PROPERTY_WRITE |
    BLECharacteristic::PROPERTY_NOTIFY
  );
  relayCharacteristic->addDescriptor(new BLE2902());
  relayCharacteristic->setCallbacks(new RelayBluetoothCallbacks());
  relayCharacteristic->setValue(relayStateMessage().c_str());

  service->start();
  BLEAdvertising *advertising = BLEDevice::getAdvertising();
  advertising->addServiceUUID(BLE_SERVICE_UUID);
  advertising->setScanResponse(true);
  advertising->start();
  Serial.println("Bluetooth Ready!");
}

void connectWiFi() {
  Serial.print("Connecting to WiFi");
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  unsigned long startedAt = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - startedAt < 15000) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();
}

int parseRelayValue(String payload, int relayNumber) {
  String key = "\"r" + String(relayNumber) + "\"";
  int keyIndex = payload.indexOf(key);
  if (keyIndex < 0) return -1;

  int colonIndex = payload.indexOf(':', keyIndex + key.length());
  if (colonIndex < 0) return -1;

  int valueIndex = colonIndex + 1;
  while (valueIndex < payload.length() && payload[valueIndex] == ' ') valueIndex++;
  if (valueIndex >= payload.length()) return -1;

  if (payload[valueIndex] == '1') return 1;
  if (payload[valueIndex] == '0') return 0;
  if (payload.substring(valueIndex, valueIndex + 4) == "true") return 1;
  if (payload.substring(valueIndex, valueIndex + 5) == "false") return 0;
  return -1;
}

bool readAllRelaysFromFirebase(int values[8]) {
  if (WiFi.status() != WL_CONNECTED) return false;

  HTTPClient http;
  String url = String(FIREBASE_DB_URL) + "/relays.json";
  http.begin(secureClient, url);
  http.setTimeout(1200);
  int httpCode = http.GET();
  if (httpCode != HTTP_CODE_OK) {
    http.end();
    return false;
  }
  String payload = http.getString();
  payload.trim();
  http.end();

  if (payload == "null" || payload.length() == 0) return false;

  for (int i = 0; i < 8; i++) {
    values[i] = parseRelayValue(payload, i + 1);
  }
  return true;
}

void updateRelaysFromFirebase() {
  if (millis() < skipFirebaseReadUntilMs) return;
  if (WiFi.status() != WL_CONNECTED) connectWiFi();
  if (WiFi.status() != WL_CONNECTED) return;

  int newStates[8] = {-1, -1, -1, -1, -1, -1, -1, -1};
  if (!readAllRelaysFromFirebase(newStates)) return;

  for (int i = 0; i < 8; i++) {
    int newState = newStates[i];
    if (newState == -1 || relayStates[i] == newState) continue;
    setRelayState(i, newState, true);
  }
}

void setup() {
  Serial.begin(115200);
  delay(500);
  for (int i = 0; i < 8; i++) {
    pinMode(relayPins[i], OUTPUT);
  }
  allRelaysOff(false);

  secureClient.setInsecure();
  setupBluetooth();
  connectWiFi();

  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("Power-on safety: resetting all relays to OFF in Firebase...");
    skipFirebaseReadUntilMs = millis() + 3000;
    writeAllRelaysToFirebase();
    Serial.println("Power-on reset complete.");
  } else {
    Serial.println("No WiFi on boot — Firebase reset skipped.");
  }
  Serial.println("=== Ready ===");
}

void loop() {
  if (millis() - lastReadMs >= readIntervalMs) {
    lastReadMs = millis();
    updateRelaysFromFirebase();
  }
}

System Architecture & How It Works

The system leverages a direct web app link to Firebase cloud nodes, cutting down latency so transitions feel entirely local. The architecture flows effortlessly across your wireless environment:

Your Web App Interface (Mobile / Desktop) │ ▼ Firebase Cloud Nodes │ ▼ ESP32 (Home 2.4GHz WiFi) │ ▼ 8-Channel Relay Module (12V) │ ▼ Connected Household Devices / Lab Loads
  • Web App Dashboard: A lightweight, highly responsive frontend bundle (HTML, CSS, JS) that push/pulls instantaneous digital state variables (0 or 1) directly out of your private Firebase server instance.
  • Firebase Database Backend: Operates as the centralized control buffer, streaming data downstream to the microcontroller node using real-time synchronization hooks.
  • ESP32 Hub: Monitors database strings continuously via the network connection, immediately translating modifications into high/low physical logic potentials across target GPIO nodes.

Complete Hardware Connection Pinout

For safe and reliable execution, your microcontroller and relay modules require explicit path mapping. Use this exact connectivity index to populate your breadboard or control panel enclosures:

ESP32 to 12V 8-Channel Relay Mapping

Relay Channel Input Node ESP32 Target GPIO Default Label Assignment
CH1IN1GPIO 23Light 1
CH2IN2GPIO 22Fan Control
CH3IN3GPIO 21Hardware Array
CH4IN4GPIO 19Studio System
CH5IN5GPIO 18Classroom Main
CH6IN6GPIO 13Light 2
CH7IN7GPIO 253D Printer Array
CH8IN8GPIO 26LED Indication Loop

Power Supply Delivery Paths

Device Hardware Specific Port Pin Source Destination Recommended Wire Standard
Relay ModuleVCC12V Power Supply (+)18 AWG Red (Heavy Load Line)
Relay ModuleGND12V Power Supply (-)18 AWG Black (Heavy Ground Line)
ESP32 DevKitGND12V PSU Ground Rail22 AWG Black (Common Ground Reference)
ESP32 DevKit5V / VINUSB Connection or 5V Regulator22 AWG Red Power Feed
⚠️ CRITICAL WIRING RULES: Always link your ESP32 GND and your 12V Power Supply Ground rail together into a Common Ground configuration. Without this shared ground plane reference point, the low-voltage control signals cannot loop reliably, causing unstable switching or complete relay failures.

Web Application Workspace Configurations

Below are the full deployment files for building the web dashboard interface. These files establish the dual network streaming connection (Firebase + Web Bluetooth API BLE) and incorporate native smartphone biometric lock verification mechanisms.

1. Document Object Architecture (HTML Structure)

📁 index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>ESP32 - Cloud Relay Control</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>


<div class="hdr"> <div class="top-row"> <span class="app-title">ESP32 &middot; Cloud Hub</span> <span class="clock" id="clock">00:00:00</span> </div> <div class="status-row"> <div class="pill" id="cloud-pill"><div class="dot"></div>Cloud Signal</div> <button class="pill bt-pill" id="btBtn" type="button"><div class="dot"></div>Bluetooth</button> </div> <div class="sys-bar" id="main-status">System: <span>Loading...</span></div> </div> <main class="grid" id="relayGrid"></main> <footer class="footer"> <button class="kill-btn" id="killAllBtn">&#8855; ALL OFF</button> </footer>
<script src="https://www.gstatic.com/firebasejs/10.13.0/firebase-app-compat.js"></script> <script src="https://www.gstatic.com/firebasejs/10.13.0/firebase-database-compat.js"></script> <script src="app.js"></script> </body> </html>

2. Dynamic Execution & Telemetry Hooks (JavaScript Controller)

📁 app.js
const firebaseConfig = {
    apiKey: "AIzaSyBExYcRBI24hi3NtoqKNwr72f8PVBet8cQ",
    authDomain: "fir-esprelay.firebaseapp.com",
    databaseURL: "https://fir-esprelay-default-rtdb.firebaseio.com",
    projectId: "fir-esprelay",
    storageBucket: "fir-esprelay.firebasestorage.app",
    messagingSenderId: "305057307536",
    appId: "1:305057307536:web:8c4e8c0555ab0d55583115",
    measurementId: "G-L0QYH7LXW0"
};

const BLE_SERVICE_UUID = '4fafc201-1fb5-459e-8fcc-c5c9c331914b';
const BLE_RELAY_CHAR_UUID = 'beb5483e-36e1-4688-b7f5-ea07361b26a8';
const BLE_DEVICE_NAME = 'Adeeb Office Remote';
const BLE_ACCESS_CODE = '3121';
const APP_PASSWORD = '333121';
const AUTH_STORAGE_KEY = 'adeebOfficeRemoteAuth';
const AUTH_CREDENTIAL_KEY = 'adeebOfficeRemoteCredential';

let database = null;
let cloudReady = false;
let btDevice = null;
let btCharacteristic = null;
let btDisconnectHandler = null;
let btPressTimer = null;
const relayStates = Array(9).fill(0);

const devices = [
    { id: 1, name: "Light", path: 'M9 21h6v-1H9v1zm3-19C8.14 2 5 5.14 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.86-3.14-7-7-7z' },
    { id: 2, name: "Fan", path: 'M12 12c0-3 2.5-5.5 5.5-5.5S23 9 23 12H12zm0 0c0 3-2.5 5.5-5.5 5.5S1 15 1 12h11zm0 0c-3 0-5.5-2.5-5.5-5.5S9 1 12 1v11zm0 0c3 0 5.5 2.5 5.5 5.5S15 23 12 23V12z' },
    { id: 3, name: "Hardware", path: 'M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.5 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z' },
    { id: 4, name: "Studio", path: 'M20 18c1.1 0 1.99-.9 1.99-2L22 6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2H0v2h24v-2h-4zM4 6h16v10H4V6z' },
    { id: 5, name: "Class", path: 'M5 4v11h14V4H5zm16 13c0 .55-.45 1-1 1h-2l-2-2h-8l-2 2H4c-.55 0-1-.45-1-1v-1h18v1z' },
    { id: 6, name: "Light 2", path: 'M9 21h6v-1H9v1zm3-19C8.14 2 5 5.14 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.86-3.14-7-7-7z' },
    { id: 7, name: "Printer", path: 'M19 8H5c-1.66 0-3 1.34-3 3v6h4v4h12v-4h4v-6c0-1.66-1.34-3-3-3zm-3 11H8v-5h8v5zm3-7c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1zm-1-9H6v4h12V3z' },
    { id: 8, name: "LED", path: 'M7 19h10v2H7zM5 3h14c1.1 0 2 .9 2 2v11c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2z' }
];

const grid = document.getElementById('relayGrid');
const cloudPill = document.getElementById('cloud-pill');
const btBtn = document.getElementById('btBtn');
const mainStatus = document.getElementById('main-status');
const loginScreen = document.getElementById('loginScreen');
const loginForm = document.getElementById('loginForm');
const passwordInput = document.getElementById('passwordInput');
const rememberCheck = document.getElementById('rememberCheck');
const thumbBtn = document.getElementById('thumbBtn');
const loginMessage = document.getElementById('loginMessage');

devices.forEach((device) => {
    const el = document.createElement('div');
    el.className = 'relay';
    el.id = `r-${device.id}`;
    el.onclick = () => toggleRelay(device.id);
    el.innerHTML = `<span class="relay-num">CH\${device.id}</span><svg viewBox="0 0 24 24"><path d="\${device.path}"/></svg><span>\${device.name}</span>`;
    grid.appendChild(el);
});

function browserSupportsBluetooth() { return Boolean(window.isSecureContext && navigator.bluetooth); }

function bufferToBase64Url(buffer) {
    const bytes = new Uint8Array(buffer);
    let binary = '';
    bytes.forEach((byte) => { binary += String.fromCharCode(byte); });
    return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
}

function base64UrlToBuffer(value) {
    const padded = value.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(value.length / 4) * 4, '=');
    const binary = atob(padded);
    const bytes = new Uint8Array(binary.length);
    for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); }
    return bytes.buffer;
}

function canUsePhoneLock() { return Boolean(window.isSecureContext && window.PublicKeyCredential && navigator.credentials); }

function setLoginMessage(message, isError = true) {
    loginMessage.textContent = message;
    loginMessage.style.color = isError ? 'var(--red)' : 'var(--neon)';
}

function unlockApp() {
    loginScreen.classList.add('hidden');
    passwordInput.value = '';
    setLoginMessage('');
}

function saveRememberedLogin() { localStorage.setItem(AUTH_STORAGE_KEY, '1'); }
function isRemembered() { return localStorage.getItem(AUTH_STORAGE_KEY) === '1'; }
function getStoredCredentialId() { return localStorage.getItem(AUTH_CREDENTIAL_KEY); }

async function registerPhoneLock() {
    if (!canUsePhoneLock()) {
        setLoginMessage('Phone lock needs HTTPS and supported browser');
        return false;
    }
    const challenge = crypto.getRandomValues(new Uint8Array(32));
    const userId = crypto.getRandomValues(new Uint8Array(16));
    try {
        const credential = await navigator.credentials.create({
            publicKey: {
                challenge,
                rp: { name: 'Adeeb Office Remote' },
                user: { id: userId, name: 'adeeb-office', displayName: 'Adeeb Office' },
                pubKeyCredParams: [{ type: 'public-key', alg: -7 }, { type: 'public-key', alg: -257 }],
                authenticatorSelection: { authenticatorAttachment: 'platform', userVerification: 'required' },
                timeout: 60000,
                attestation: 'none'
            }
        });
        localStorage.setItem(AUTH_CREDENTIAL_KEY, bufferToBase64Url(credential.rawId));
        saveRememberedLogin();
        setLoginMessage('Phone lock saved', false);
        return true;
    } catch (error) {
        setLoginMessage('Phone lock setup failed');
        return false;
    }
}

async function unlockWithPhoneLock() {
    const credentialId = getStoredCredentialId();
    if (!credentialId) {
        setLoginMessage('Enter password, tick remember, then save phone lock');
        return false;
    }
    if (!canUsePhoneLock()) {
        setLoginMessage('Phone lock not available here');
        return false;
    }
    try {
        await navigator.credentials.get({
            publicKey: {
                challenge: crypto.getRandomValues(new Uint8Array(32)),
                allowCredentials: [{ id: base64UrlToBuffer(credentialId), type: 'public-key' }],
                userVerification: 'required',
                timeout: 60000
            }
        });
        unlockApp();
        return true;
    } catch (error) {
        setLoginMessage('Phone lock cancelled or failed');
        return false;
    }
}

async function handlePasswordLogin(event) {
    event.preventDefault();
    if (passwordInput.value !== APP_PASSWORD) {
        setLoginMessage('Wrong password');
        passwordInput.select();
        return;
    }
    if (rememberCheck.checked) saveRememberedLogin();
    unlockApp();
}

async function handlePhoneLockButton() {
    if (passwordInput.value === APP_PASSWORD && rememberCheck.checked) {
        const saved = await registerPhoneLock();
        if (saved) unlockApp();
        return;
    }
    await unlockWithPhoneLock();
}

function initAppLock() {
    thumbBtn.disabled = !canUsePhoneLock();
    thumbBtn.textContent = getStoredCredentialId() ? 'Unlock With Phone Lock' : 'Save Phone Lock';
    loginForm.addEventListener('submit', handlePasswordLogin);
    thumbBtn.addEventListener('click', handlePhoneLockButton);
}

function setStatus(text, className = 'warning') {
    mainStatus.innerHTML = `System: <span class="\${className}">\${text}</span>`;
}

function setRelayUI(id, state) {
    relayStates[id] = state;
    const el = document.getElementById(`r-\${id}`);
    if (el) el.classList.toggle('active', state === 1);
}

function applyRelayData(data = {}) {
    for (let i = 1; i <= 8; i++) {
        setRelayUI(i, Number(data[`r\${i}`]) === 1 ? 1 : 0);
    }
}

function updateBtUI(connected) {
    btBtn.classList.toggle('bt-on', connected);
    btBtn.innerHTML = connected ? '<div class="dot"></div>BT Connected' : '<div class="dot"></div>Bluetooth';
    btBtn.disabled = !browserSupportsBluetooth();
}

function setSavedBtUI(deviceName) {
    btBtn.classList.remove('bt-on');
    btBtn.innerHTML = '<div class="dot"></div>BT Saved';
    setStatus(`Tap Bluetooth to reconnect \${deviceName || 'ESP32'}`);
}

function initFirebase() {
    if (!window.firebase || !firebase.database) {
        cloudPill.classList.remove('cloud-on');
        setStatus('Cloud Offline - Use Bluetooth');
        return;
    }
    try {
        firebase.initializeApp(firebaseConfig);
        database = firebase.database();
        database.ref('relays').on('value', (snapshot) => {
            if (snapshot.exists()) {
                cloudReady = true;
                cloudPill.classList.add('cloud-on');
                setStatus(btCharacteristic ? 'Cloud + Bluetooth Online' : 'Cloud Online', 'online');
                applyRelayData(snapshot.val());
                return;
            }
            cloudReady = false;
            cloudPill.classList.remove('cloud-on');
            setStatus('Waiting for cloud data');
        }, (error) => {
            cloudReady = false;
            cloudPill.classList.remove('cloud-on');
            setStatus(btCharacteristic ? 'Bluetooth Online' : 'Connection Error', btCharacteristic ? 'online' : 'error');
        });
    } catch (error) {
        cloudPill.classList.remove('cloud-on');
        setStatus('Cloud Offline - Use Bluetooth');
    }
}

async function sendBluetooth(command) {
    if (!btCharacteristic) return false;
    const data = new TextEncoder().encode(command);
    await btCharacteristic.writeValue(data);
    return true;
}

function handleBluetoothMessage(event) {
    const message = new TextDecoder().decode(event.target.value);
    if (message.trim() === 'auth=0') {
        setStatus('Bluetooth code rejected', 'error');
        return;
    }
    message.split(',').forEach((pair) => {
        const match = pair.trim().match(/^r([1-8])=(0|1)$/);
        if (match) setRelayUI(Number(match[1]), Number(match[2]));
    });
}

function attachBluetoothDisconnectHandler(device) {
    if (btDisconnectHandler) device.removeEventListener('gattserverdisconnected', btDisconnectHandler);
    btDisconnectHandler = () => {
        btCharacteristic = null;
        updateBtUI(false);
        setStatus(cloudReady ? 'Cloud Online' : 'Bluetooth Disconnected', cloudReady ? 'online' : 'warning');
    };
    device.addEventListener('gattserverdisconnected', btDisconnectHandler);
}

async function finishBluetoothConnection(device) {
    const server = await device.gatt.connect();
    const service = await server.getPrimaryService(BLE_SERVICE_UUID);
    btCharacteristic = await service.getCharacteristic(BLE_RELAY_CHAR_UUID);
    await btCharacteristic.startNotifications();
    btCharacteristic.addEventListener('characteristicvaluechanged', handleBluetoothMessage);
    updateBtUI(true);
    setStatus(cloudReady ? 'Cloud + Bluetooth Online' : 'Bluetooth Online', 'online');
    await sendBluetooth(`code=\${BLE_ACCESS_CODE}`);
    await sendBluetooth('get');
}

async function connectBluetooth() {
    if (!browserSupportsBluetooth()) {
        setStatus('Bluetooth needs Chrome/Edge on HTTPS', 'error');
        return;
    }
    try {
        setStatus('Selecting Bluetooth device');
        btDevice = await navigator.bluetooth.requestDevice({
            filters: [{ name: BLE_DEVICE_NAME }, { namePrefix: 'Adeeb Office' }],
            optionalServices: [BLE_SERVICE_UUID]
        });
        attachBluetoothDisconnectHandler(btDevice);
        await finishBluetoothConnection(btDevice);
    } catch (error) {
        updateBtUI(false);
        setStatus('Bluetooth connection failed', 'error');
    }
}

async function connectAnyBluetoothDevice() {
    if (!browserSupportsBluetooth()) return;
    try {
        btDevice = await navigator.bluetooth.requestDevice({ acceptAllDevices: true, optionalServices: [BLE_SERVICE_UUID] });
        attachBluetoothDisconnectHandler(btDevice);
        await finishBluetoothConnection(btDevice);
    } catch (error) {
        updateBtUI(false);
    }
}

async function toggleRelay(id) {
    const newState = relayStates[id] === 1 ? 0 : 1;
    setRelayUI(id, newState);
    try {
        const sent = await sendBluetooth(`r\${id}=\${newState}`);
        if (!database && !sent) setStatus('No connection', 'error');
    } catch (error) {
        setStatus(cloudReady ? 'Cloud Online' : 'BT write failed', cloudReady ? 'online' : 'error');
    }
    if (database) {
        database.ref(`relays/r\${id}`).set(newState).catch(() => {
            setStatus(btCharacteristic ? 'Bluetooth Online' : 'Cloud write failed', btCharacteristic ? 'online' : 'error');
        });
    }
}

async function turnAllRelaysOff() {
    if (!confirm("Turn off all relays?")) return;
    for (let i = 1; i <= 8; i++) setRelayUI(i, 0);
    try { await sendBluetooth('all=0'); } catch (error) {}
    if (database) {
        const updates = {};
        for (let i = 1; i <= 8; i++) updates[`relays/r\${i}`] = 0;
        database.ref().update(updates);
    }
}

btBtn.onclick = connectBluetooth;
btBtn.addEventListener('pointerdown', () => { btPressTimer = setTimeout(connectAnyBluetoothDevice, 900); });
btBtn.addEventListener('pointerup', () => clearTimeout(btPressTimer));
document.getElementById('killAllBtn').onclick = turnAllRelaysOff;
setInterval(() => { document.getElementById('clock').innerText = new Date().toLocaleTimeString(); }, 1000);

setStatus('Connecting...');
initAppLock();
initFirebase();

3. Visual Interface & High-Contrast Layout (CSS Stylesheet)

📁 style.css
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
    --bg: #0d0d10; --card: #18181c; --border: #2a2a30; --dim: #555;
    --neon: #39ff14; --glow: #fff01f; --blue: #3b8ef3; --cyan: #22d3ee; --red: #ff4455;
}
body { background: var(--bg); color: #fff; font-family: system-ui, sans-serif; height: 100dvh; overflow: hidden; }
.login-screen { position: fixed; inset: 0; z-index: 10; display: flex; align-items: center; justify-content: center; background: var(--bg); padding: 18px; }
.login-screen.hidden { display: none; }
.login-panel { width: min(100%, 360px); background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 18px; display: flex; flex-direction: column; gap: 12px; }
.login-title { font-size: 18px; font-weight: 800; }
.login-subtitle { color: #888; font-size: 12px; margin-top: 4px; }
.login-input { width: 100%; min-height: 46px; border-radius: 8px; border: 1px solid var(--border); background: #101014; color: #fff; font-size: 20px; font-weight: 700; padding: 8px 12px; outline: none; }
.login-input:focus { border-color: var(--cyan); }
.remember-row { display: flex; align-items: center; gap: 9px; color: #bbb; font-size: 13px; user-select: none; }
.remember-row input { width: 18px; height: 18px; }
.login-btn, .thumb-btn { min-height: 44px; border-radius: 8px; font-family: inherit; font-size: 14px; font-weight: 800; cursor: pointer; }
.login-btn { border: 1px solid var(--neon); background: #0d1f0d; color: var(--neon); }
.thumb-btn { border: 1px solid var(--cyan); background: transparent; color: var(--cyan); }
.thumb-btn:disabled { border-color: var(--dim); color: var(--dim); cursor: not-allowed; }
.wrap { display: flex; flex-direction: column; height: 100dvh; padding: 8px; gap: 6px; }
.hdr { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 10px; flex-shrink: 0; }
.top-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 7px; }
.app-title { font-size: 11px; font-weight: 700; color: #888; letter-spacing: 1.2px; text-transform: uppercase; }
.clock { font-size: 13px; font-weight: 700; color: #fff; background: #222; padding: 3px 8px; border-radius: 6px; font-variant-numeric: tabular-nums; }
.status-row { display: flex; gap: 6px; margin-bottom: 7px; }
.pill { display: flex; align-items: center; justify-content: center; gap: 5px; padding: 6px 12px; border-radius: 20px; border: 1px solid var(--border); font-size: 11px; font-weight: 700; color: var(--dim); flex: 1; }
.pill .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--dim); }
.pill.cloud-on { border-color: var(--blue); color: var(--blue); }
.pill.cloud-on .dot { background: var(--blue); box-shadow: 0 0 6px var(--blue); }
.bt-pill.bt-on { border-color: var(--cyan); color: var(--cyan); }
.bt-pill.bt-on .dot { background: var(--cyan); box-shadow: 0 0 6px var(--cyan); }
.sys-bar { font-size: 11px; color: #555; text-align: center; margin-top: 5px; }
.sys-bar .online { color: var(--neon); }
.sys-bar .error { color: var(--red); }
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; flex: 1; overflow-y: auto; padding-bottom: 10px; }
.relay { background: var(--card); border: 1.5px solid var(--border); border-radius: 10px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; cursor: pointer; padding: 8px 4px; }
.relay:active { transform: scale(0.93); }
.relay-num { font-size: 8px; font-weight: 700; color: var(--dim); }
.relay svg { width: 22px; height: 22px; fill: var(--dim); }
.relay span { font-size: 9px; font-weight: 600; color: var(--dim); text-align: center; }
.relay.active { border-color: var(--neon); background: #0d1f0d; }
.relay.active .relay-num { color: var(--neon); }
.relay.active svg { fill: var(--glow); }
.relay.active span { color: #ccc; }
.kill-btn { flex: 1; padding: 12px; background: #2a0a0a; border: 1.5px solid var(--red); color: var(--red); border-radius: 10px; font-size: 13px; font-weight: 700; cursor: pointer; }

Verification & Troubleshooting Bench Tests

  • Hardware Echo Checklist: Boot up your diagnostic Arduino Serial Monitor configured to 115200 baud. You should see successful connection notifications alongside real-time prints detailing active data states (e.g., CH1 = 0).
  • Control Loop Validation: Launch your generated Firebase web link inside a browser panel. Clicking on any relay channel switch should trigger an auditory mechanical relay click instantly within your workspace, updating both database values and active hardware arrays.

Now your high-contrast IoT automated framework is fully deployed and configured. Test with low-power diagnostic indicators first before applying live, high-amperage equipment lines!


ESP32 Firebase Relay Control - Complete Package

Download all source files, web configurations, and documentation assets instantly:

1️⃣ Download Checklist & Packages
Web App Files (4 Files):
  • index.html — Main web dashboard page.
  • app.js — JavaScript layer carrying pre-configured Firebase configurations.
  • style.css — Mobile-responsive UI stylesheet.
  • firebase.json — Firebase Hosting configuration routing file.
Firmware Source (1 File) — ACTION REQUIRED:

ArduinoFirebasereal.ino — ESP32 firmware control system loop. You must edit lines 7 and 8 to map your local environment parameters:

Line 7:  #define WIFI_SSID "Your_WiFi_SSID"
Line 8:  #define WIFI_PASSWORD "Your_WiFi_Password"
Target Project Workspace Directory Tree

Set up your workspace root directory configuration identically to the following tree before running deployment updates:

esp32-app/                    ← Root Project Directory
├── index.html               ← Core Document Markup
├── app.js                   ← Firebase Realtime Connection Scripts
├── style.css                ← Web Dashboard Layout UI Styles
└── firebase.json            ← Firebase Cloud Hosting Manifest Map
2️⃣ Firebase Cloud Project Setup

Follow these structural phases inside your cloud workspace environment to link your local dashboard instance safely:

Phase A: Project Registration
  • Navigate to the official client console portal at https://console.firebase.google.com.
  • Initialize a blank project footprint by targeting the "Create a project" utility pipeline.
  • Assign the designation string token identity: FirebaseEspRelay.
  • Deactivate Google Analytics modules to ensure direct stream configurations without runtime analytics dependencies.
Phase B: Realtime Database Configurations
  • Expand the left navigation panel menu section and choose Build → Realtime Database.
  • Execute the database creation setup tool and specify your closest localization availability node (e.g., us-central1).
  • Toggle configuration access levels to "Start in test mode" to allow immediate device integration testing cycles.
Phase C: Realtime Payload JSON Tree Structure

Under the Data view node, create the primary target array nodes matching this exact JSON namespace representation:

{
  "relays": {
    "r1": 0,
    "r2": 0,
    "r3": 0,
    "r4": 0,
    "r5": 0,
    "r6": 0,
    "r7": 0,
    "r8": 0
  }
}
Phase D: Security Policy Engine Mapping

Navigate into the database workspace Rules submenu view tier, replace the default structures, and publish these precise validation metrics:

{
  "rules": {
    "relays": {
      ".read": true,
      ".write": true
    },
    ".read": false,
    ".write": false
  }
}
3️⃣ Application & Infrastructure Deployment
Step 1: Hosting Platform Deployment

Open your local administrative command prompt utility pointing inside the esp32-app target directory workspace folder structure, then execute the following instructions:

# Install global access configuration hosting utility CLI tools
npm install -g firebase-tools

# Authentication linkage step
firebase login

# Initialize local project directory structure bindings
firebase init hosting

Configure the deployment wizard prompts with the following arguments:

  • Select Project: fir-esprelay
  • Public directory location context: . (Set to local root folder)
  • Configure as single-page application entry rule: N
  • File overwrite prevention overrides: N

Trigger live ecosystem synchronization updates by running:

firebase deploy
Live Link: Upon process verification, the production application instance will be globally reachable at:
https://fir-esprelay.web.app
Step 2: Firmware Integration
  • Launch the local desktop Arduino IDE environment setup workbench.
  • Open the file designated as ArduinoFirebasereal.ino.
  • Set target board compilation parameters under Tools → Board → DOIT ESP32 DEVKIT V1.
  • Bind target peripheral data link configurations via Tools → Port to match your device channel.
  • Compile and upload the firmware code onto the micro-architecture module.
4️⃣ Hardware Interfacing & Wire Pinout Specifications

The configuration mappings detailed below outline the structural pin routes between the ESP32 DOIT DevKit V1 and the 8-Channel optocoupler isolated relay interface module:

Control Bus Pinout Reference Routing Map
Relay Interface Channel Input Bus Node Pin ESP32 GPIO Reference Target System Operational Label
Channel 1IN1GPIO 23Light Controller
Channel 2IN2GPIO 22Fan Controller
Channel 3IN3GPIO 21Hardware Line
Channel 4IN4GPIO 19Studio Mains
Channel 5IN5GPIO 18Class Indicator
Channel 6IN6GPIO 13Secondary Light 2
Channel 7IN7GPIO 12Printer Station
Channel 8IN8GPIO 2Status onboard LED Channel
Power Supply Bus Infrastructure Interconnections
Hardware Target Device Pin Interface Point External Rail Connection Route Standard Color Code
Relay Power Input BlockVCC Pin+12V DC External Output rail lineRed Line VCC
Relay Power Input BlockGND PinCommon Ground DC Return RailBlack Line GND
ESP32 Core MicroprocessorGND PinCommon Ground reference system planeBlack Line GND
ESP32 Core MicroprocessorVIN Pin+5V USB standard supply rail lineRed Line VCC
CRITICAL SAFETY DIRECTIVE: Always establish a common ground plane reference line across all system operational voltages by bonding the ESP32 board GND nodes and the 12V external power supply ground rail directly together. Interpose a 2A inline safety fuse along the positive 12V operational voltage rail feed to protect your logic circuits from thermal overcurrent damage.
5️⃣ Final Project Verification Checklist

Verify your setup step-by-step to confirm complete cloud-to-hardware system synchronization:

Post a Comment

0 Comments