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:
#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:
- 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 |
|---|---|---|---|
| CH1 | IN1 | GPIO 23 | Light 1 |
| CH2 | IN2 | GPIO 22 | Fan Control |
| CH3 | IN3 | GPIO 21 | Hardware Array |
| CH4 | IN4 | GPIO 19 | Studio System |
| CH5 | IN5 | GPIO 18 | Classroom Main |
| CH6 | IN6 | GPIO 13 | Light 2 |
| CH7 | IN7 | GPIO 25 | 3D Printer Array |
| CH8 | IN8 | GPIO 26 | LED Indication Loop |
Power Supply Delivery Paths
| Device Hardware | Specific Port Pin | Source Destination | Recommended Wire Standard |
|---|---|---|---|
| Relay Module | VCC | 12V Power Supply (+) | 18 AWG Red (Heavy Load Line) |
| Relay Module | GND | 12V Power Supply (-) | 18 AWG Black (Heavy Ground Line) |
| ESP32 DevKit | GND | 12V PSU Ground Rail | 22 AWG Black (Common Ground Reference) |
| ESP32 DevKit | 5V / VIN | USB Connection or 5V Regulator | 22 AWG Red Power Feed |
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)
<!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>
<form class="login-panel" id="loginForm">
<div>
<div class="login-title">Adeeb Office Remote</div>
<div class="login-subtitle">Enter app password</div>
</div>
<input class="login-input" id="passwordInput" type="password" inputmode="numeric" autocomplete="current-password" placeholder="Password">
<label class="remember-row">
<input id="rememberCheck" type="checkbox">
<span>Remember me</span>
</label>
<button class="login-btn" type="submit">Unlock</button>
<button class="thumb-btn" id="thumbBtn" type="button">Use Phone Lock</button>
<div class="login-message" id="loginMessage"></div>
</form>
<div class="hdr">
<div class="top-row">
<span class="app-title">ESP32 · 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">⊗ 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)
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)
* { 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:
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.
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"
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
Follow these structural phases inside your cloud workspace environment to link your local dashboard instance safely:
- 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.
- 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.
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
}
}
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
}
}
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
https://fir-esprelay.web.app
- 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.
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:
| Relay Interface Channel | Input Bus Node Pin | ESP32 GPIO Reference Target | System Operational Label |
|---|---|---|---|
| Channel 1 | IN1 | GPIO 23 | Light Controller |
| Channel 2 | IN2 | GPIO 22 | Fan Controller |
| Channel 3 | IN3 | GPIO 21 | Hardware Line |
| Channel 4 | IN4 | GPIO 19 | Studio Mains |
| Channel 5 | IN5 | GPIO 18 | Class Indicator |
| Channel 6 | IN6 | GPIO 13 | Secondary Light 2 |
| Channel 7 | IN7 | GPIO 12 | Printer Station |
| Channel 8 | IN8 | GPIO 2 | Status onboard LED Channel |
| Hardware Target Device | Pin Interface Point | External Rail Connection Route | Standard Color Code |
|---|---|---|---|
| Relay Power Input Block | VCC Pin | +12V DC External Output rail line | Red Line VCC |
| Relay Power Input Block | GND Pin | Common Ground DC Return Rail | Black Line GND |
| ESP32 Core Microprocessor | GND Pin | Common Ground reference system plane | Black Line GND |
| ESP32 Core Microprocessor | VIN Pin | +5V USB standard supply rail line | Red Line VCC |
Verify your setup step-by-step to confirm complete cloud-to-hardware system synchronization:

0 Comments