/*
ESP32 Dashboard — Top-clock + polished UI (English)
- Temperature (left), Light (center), Humidity (right) in top half
- Real-time clock (hh:mm:ss.mmm) centered at top, updates every 10ms
- Chart.js for charts (CDN)
- Responsive + polished visuals
- BLE notifications & /data API preserved
*/
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <DHT.h>
#define DHTPIN 4
#define DHTTYPE DHT11
#define LDR_PIN 34 // Analog pin (ADC 0-4095)
const float MAX_LUX = 3000.0; // Adjust after calibration
const char *ssid = "Bin bin";
const char *password = "0935808037";
WebServer server(80);
DHT dht(DHTPIN, DHTTYPE);
// BLE UUIDs
#define SERVICE_UUID "12345678-1234-1234-1234-1234567890ab"
#define CHARACTERISTIC_UUID "abcdefab-1234-5678-90ab-abcdefabcdef"
BLECharacteristic *pCharacteristic;
void handleRoot() {
String html = R"rawliteral(
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ESP32 Dashboard — Real-time</title>
<!-- Google font + icons -->
<link href="https://fonts.googleapis.com/css2?
family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet">
<link rel='stylesheet' href='https://use.fontawesome.com/releases/v5.7.2/css/all.css'>
<style>
:root{
--bg1: #0f1724;
--bg2: #0b3a66;
--card-bg: rgba(255,255,255,0.95);
--muted: #6b7280;
html,body{height:100%;margin:0;font-family: 'Inter', system-ui, -apple-system, 'Segoe UI',
Roboto, sans-serif;background: linear-gradient(180deg,#071428 0%, #0b2f4a 60%, #0f3b5a
100%); color:#0f1724;}
/* Top header with clock */
header {
width:100%;
display:flex;
align-items:center;
justify-content:center;
padding:18px 12px;
box-sizing:border-box;
position:sticky;
top:0;
z-index:40;
background: linear-gradient(90deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));
backdrop-filter: blur(6px);
border-bottom: 1px solid rgba(255,255,255,0.04);
.brand {
position:absolute;
left:18px;
color: #e6f0ff;
font-weight:700;
letter-spacing:0.2px;
font-size:1rem;
#clock {
color: #e6f7ff;
font-weight:800;
font-size:1.2rem;
background: linear-gradient(90deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));
padding:8px 14px;
border-radius:999px;
box-shadow: 0 6px 18px rgba(2,6,23,0.45);
border: 1px solid rgba(255,255,255,0.06);
font-family: 'Inter', sans-serif;
/* Main layout: top half with 3 equal cards */
.top-area {
display:grid;
grid-template-columns: 1fr 1fr 1fr;
gap:14px;
padding: 14px;
height:50vh; /* Top half */
box-sizing:border-box;
align-items:start; /* cards anchored to top */
}
.card {
background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(255,255,255,0.92));
border-radius:14px;
padding:14px;
box-shadow: 0 10px 30px rgba(2,6,23,0.45);
display:flex;
flex-direction:column;
align-items:center;
justify-content:flex-start;
min-height:100%;
overflow:hidden;
transition: transform 220ms ease, box-shadow 220ms ease;
.card:hover{ transform: translateY(-6px); box-shadow: 0 20px 40px rgba(2,6,23,0.55); }
.title {
color:#0b3a66;
font-weight:700;
font-size:1.15rem;
margin-bottom:6px;
display:flex;
align-items:center;
gap:8px;
}
.title i { color:#1f7aef; font-size:1.15rem; }
.chart-wrap {
width:100%;
height: calc(50vh - 170px); /* chart area reduced so title/vals fit */
display:flex;
align-items:center;
justify-content:center;
margin-bottom:8px;
.value-num {
font-weight:900;
font-size:1.5rem;
color:#0b3a66;
margin-top:6px;
.status {
font-size:1rem;
color:var(--muted);
margin-top:6px;
/* Nice footer note below top area (empty space) */
.bottom-space {
padding: 16px;
color: rgba(255,255,255,0.85);
font-size:0.95rem;
text-align:center;
opacity:0.9;
/* Responsive: stack vertically on small screens; make cards smaller and center */
@media (max-width: 880px) {
.top-area {
grid-template-columns: 1fr;
height: auto;
padding: 12px;
.chart-wrap { height: 240px; }
#clock { font-size:1.05rem; padding:6px 10px; }
.title { font-size:1rem; }
.value-num { font-size:1.25rem; }
</style>
<!-- Chart.js CDN -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<header>
<div class="brand">ESP32 Dashboard</div>
<div id="clock">--:--:--.---</div>
</header>
<main>
<div class="top-area">
<div class="card" id="tempCol">
<div class="title"><i class="fas fa-thermometer-half"></i> Temperature</div>
<div class="chart-wrap"><canvas id="tempChart"></canvas></div>
<div class="value-num" id="tempVal">-- °C</div>
<div class="status" id="tempStatus">Status: --</div>
</div>
<div class="card" id="lightCol">
<div class="title"><i class="fas fa-sun"></i> Light (Lux)</div>
<div class="chart-wrap"><canvas id="lightChart"></canvas></div>
<div class="value-num" id="lightVal">-- lux</div>
<div class="status" id="lightStatus">Status: --</div>
</div>
<div class="card" id="humCol">
<div class="title"><i class="fas fa-tint"></i> Humidity</div>
<div class="chart-wrap"><canvas id="humChart"></canvas></div>
<div class="value-num" id="humVal">-- %</div>
<div class="status" id="humStatus">Status: --</div>
</div>
</div>
</main>
<script>
const MAX_LUX = )rawliteral";
html += String(MAX_LUX, 1);
html += R"rawliteral(;
// ---------- Clock (real-time with millisecond display) ----------
// Update every 10ms for millisecond resolution display.
function pad(n, width = 2) { return String(n).padStart(width, '0'); }
function updateClock() {
const d = new Date();
const hh = pad(d.getHours(),2);
const mm = pad(d.getMinutes(),2);
const ss = pad(d.getSeconds(),2);
const ms = pad(d.getMilliseconds(),3);
document.getElementById('clock').textContent = `${hh}:${mm}:${ss}.${ms}`;
// Start clock with 10ms interval (practical and shows ms)
setInterval(updateClock, 10);
updateClock();
// ---------- Classification functions (thresholds) ----------
function classifyTemperature(t) {
if (t === null) return {color:'#9ca3af', text:'--'};
if (t <= 24.0) return {color:'#16a34a', text:'Comfortable'};
if (t > 24.0 && t <= 28.0) return {color:'#d97706', text:'Warm'};
return {color:'#dc2626', text:'Hot'};
function classifyHumidity(h) {
if (h === null) return {color:'#9ca3af', text:'--'};
if (h >= 40.0 && h <= 60.0) return {color:'#16a34a', text:'Good'};
if ((h >= 30.0 && h < 40.0) || (h > 60.0 && h <= 70.0)) return {color:'#d97706',
text:'Acceptable'};
return {color:'#dc2626', text:'Uncomfortable'};
function classifyLight(lux) {
if (lux === null) return {color:'#9ca3af', text:'--'};
if (lux < 900.0) return {color:'#16a34a', text:'Very Bright'};
if (lux >= 900.0 && lux <= 2000.0) return {color:'#d97706', text:'Moderate'};
return {color:'#dc2626', text:'Dim / Low'};
// ---------- Chart setup ----------
let tempChart, humChart, lightChart;
function createCharts() {
// Temperature half-doughnut (0..50)
const tCtx = document.getElementById('tempChart').getContext('2d');
tempChart = new Chart(tCtx, {
type:'doughnut',
data:{
labels:['value','rest'],
datasets:[{ data:[0,50], backgroundColor:['#16a34a','#e6eef5'], borderWidth:0 }]
},
options:{
rotation:-90, circumference:180, cutout:'70%', maintainAspectRatio:false,
plugins:{legend:{display:false}}
});
// Humidity half-doughnut (0..100)
const hCtx = document.getElementById('humChart').getContext('2d');
humChart = new Chart(hCtx, {
type:'doughnut',
data:{ labels:['value','rest'], datasets:[{ data:[0,100], backgroundColor:
['#16a34a','#e6eef5'], borderWidth:0 }]},
options:{ rotation:-90, circumference:180, cutout:'70%', maintainAspectRatio:false,
plugins:{legend:{display:false}} }
});
// Light horizontal bar (x max = MAX_LUX)
const lCtx = document.getElementById('lightChart').getContext('2d');
lightChart = new Chart(lCtx, {
type:'bar',
data:{ labels:[''], datasets:[{ label:'Lux', data:[0], backgroundColor:['#16a34a'] }]},
options:{
indexAxis:'y', maintainAspectRatio:false, responsive:true,
plugins:{ legend:{display:false} },
scales:{
x:{ beginAtZero:true, max: MAX_LUX },
y:{ ticks:{ display:false } }
});
// ---------- UI update ----------
function updateUI(json) {
// Temp
if (json.temperature !== null) {
const t = parseFloat(json.temperature);
const cls = classifyTemperature(t);
document.getElementById('tempVal').textContent = t.toFixed(2) + ' °C';
document.getElementById('tempStatus').textContent = 'Status: ' + cls.text;
const topT = 50.0;
tempChart.data.datasets[0].data[0] = Math.min(Math.max(t,0),topT);
tempChart.data.datasets[0].data[1] = Math.max(topT - Math.min(Math.max(t,0),topT),
0.0001);
tempChart.data.datasets[0].backgroundColor[0] = cls.color;
} else {
document.getElementById('tempVal').textContent = '-- °C';
document.getElementById('tempStatus').textContent = 'Status: --';
tempChart.data.datasets[0].data[0] = 0; tempChart.data.datasets[0].data[1] = 50;
tempChart.data.datasets[0].backgroundColor[0] = '#9ca3af';
}
tempChart.update();
// Humidity
if (json.humidity !== null) {
const h = parseFloat(json.humidity);
const cls = classifyHumidity(h);
document.getElementById('humVal').textContent = h.toFixed(2) + ' %';
document.getElementById('humStatus').textContent = 'Status: ' + cls.text;
const topH = 100.0;
humChart.data.datasets[0].data[0] = Math.min(Math.max(h,0),topH);
humChart.data.datasets[0].data[1] = Math.max(topH - Math.min(Math.max(h,0),topH),
0.0001);
humChart.data.datasets[0].backgroundColor[0] = cls.color;
} else {
document.getElementById('humVal').textContent = '-- %';
document.getElementById('humStatus').textContent = 'Status: --';
humChart.data.datasets[0].data[0] = 0; humChart.data.datasets[0].data[1] = 100;
humChart.data.datasets[0].backgroundColor[0] = '#9ca3af';
humChart.update();
// Light
if (json.light_lux !== null) {
const lux = parseFloat(json.light_lux);
const cls = classifyLight(lux);
document.getElementById('lightVal').textContent = lux.toFixed(1) + ' lux';
document.getElementById('lightStatus').textContent = 'Status: ' + cls.text;
lightChart.data.datasets[0].data[0] = Math.min(Math.max(lux,0),MAX_LUX);
lightChart.data.datasets[0].backgroundColor[0] = cls.color;
} else {
document.getElementById('lightVal').textContent = '-- lux';
document.getElementById('lightStatus').textContent = 'Status: --';
lightChart.data.datasets[0].data[0] = 0;
lightChart.data.datasets[0].backgroundColor[0] = '#9ca3af';
lightChart.update();
// ---------- Polling ----------
async function pollData() {
try {
const res = await fetch('/data');
if (!res.ok) throw new Error('Network response not ok');
const json = await res.json();
updateUI(json);
} catch (err) {
console.error('Fetch error', err);
// Init
createCharts();
pollData();
setInterval(pollData, 2000);
</script>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html);
// JSON API
void handleSensorData() {
float temp = dht.readTemperature();
float hum = dht.readHumidity();
int raw = analogRead(LDR_PIN);
float lux = (raw / 4095.0) * MAX_LUX;
String tempStr = isnan(temp) ? String("null") : String(temp, 2);
String humStr = isnan(hum) ? String("null") : String(hum, 2);
String rawStr = String(raw);
String luxStr = String(lux, 1);
String json = "{";
json += "\"temperature\":" + tempStr + ",";
json += "\"humidity\":" + humStr + ",";
json += "\"light_raw\":" + rawStr + ",";
json += "\"light_lux\":" + luxStr;
json += "}";
server.send(200, "application/json", json);
void setup() {
Serial.begin(115200);
dht.begin();
// BLE setup
BLEDevice::init("ESP32_BLE_Sensor");
BLEServer *pServer = BLEDevice::createServer();
BLEService *pService = pServer->createService(SERVICE_UUID);
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY
);
pCharacteristic->setValue("Waiting for data...");
pService->start();
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->start();
// WiFi
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
Serial.println();
Serial.println("Connected to WiFi!");
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
server.on("/", handleRoot);
server.on("/data", handleSensorData);
server.begin();
void loop() {
float temp = dht.readTemperature();
float hum = dht.readHumidity();
int raw = analogRead(LDR_PIN);
float lux = (raw / 4095.0) * MAX_LUX;
String tStr = isnan(temp) ? String("--") : String(temp, 2);
String hStr = isnan(hum) ? String("--") : String(hum, 2);
String bleData = "T: " + tStr + "C, H: " + hStr + "%, L: " + String(lux, 1) + " lux";
Serial.println(bleData);
pCharacteristic->setValue(bleData.c_str());
pCharacteristic->notify();
server.handleClient();
delay(500);