This shows you the differences between two versions of the page.
|
iothings:laboratoare:2025:lab9 [2025/11/23 12:49] dan.tudose |
iothings:laboratoare:2025:lab9 [2025/11/23 13:43] (current) dan.tudose [3. ESP32-C6 Sparrow Firmware (HTTPS + BME680 + NeoPixel)] |
||
|---|---|---|---|
| Line 189: | Line 189: | ||
| * ''WiFiClientSecure'' for HTTPS. | * ''WiFiClientSecure'' for HTTPS. | ||
| - | * For simplicity we will call ''setInsecure()'' (it still uses TLS but skips certificate validation). | + | * We have embedded a proper CA certificate. |
| - | *For production, embed proper CA certificates.* | + | * The certificate is the full Google TLS root (GTS Root R1, cross-signed by GlobalSign) captured from a live identitytoolkit.googleapis.com chain and reverted to setCACert using that PEM |
| Edit ''src/main.cpp'', get the code [[iothings:laboratoare:2025_code:lab9_1|here]]. | Edit ''src/main.cpp'', get the code [[iothings:laboratoare:2025_code:lab9_1|here]]. | ||
| Fill in your credentials: | Fill in your credentials: | ||
| - | |||
| * Wi-Fi SSID and password. | * Wi-Fi SSID and password. | ||
| * Firebase ''apiKey'' and ''databaseURL''. | * Firebase ''apiKey'' and ''databaseURL''. | ||
| Line 211: | Line 210: | ||
| If you see sign-in or HTTPS errors, double-check: | If you see sign-in or HTTPS errors, double-check: | ||
| - | |||
| * ''FIREBASE_API_KEY'' | * ''FIREBASE_API_KEY'' | ||
| * ''FIREBASE_DB_URL'' | * ''FIREBASE_DB_URL'' | ||
| Line 217: | Line 215: | ||
| * Wi-Fi credentials. | * Wi-Fi credentials. | ||
| - | ---- | + | |
| ===== 4. Web Dashboard (Password-Protected) ===== | ===== 4. Web Dashboard (Password-Protected) ===== | ||
| Line 242: | Line 240: | ||
| index.html | index.html | ||
| app.js | app.js | ||
| + | chart.umd.min.js | ||
| </code> | </code> | ||
| Line 248: | Line 247: | ||
| ==== 4.2 index.html ==== | ==== 4.2 index.html ==== | ||
| - | Create ''dashboard/public/index.html'' with: | + | Create ''dashboard/public/index.html'' with the code from [[iothings:laboratoare:2025_code:lab9_2|here]]. |
| - | <code html index.html> | ||
| - | <!doctype html> | ||
| - | <html lang="en"> | ||
| - | <head> | ||
| - | <meta charset="utf-8" /> | ||
| - | <title>Sparrow Dashboard</title> | ||
| - | <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| - | |||
| - | <style> | ||
| - | body { font-family: system-ui, sans-serif; margin: 0; padding: 0; background: #111; color: #eee; } | ||
| - | .container { max-width: 900px; margin: 0 auto; padding: 1.5rem; } | ||
| - | h1, h2 { font-weight: 600; } | ||
| - | .card { | ||
| - | background: #1c1c1c; | ||
| - | border-radius: 1rem; | ||
| - | padding: 1rem 1.25rem; | ||
| - | margin-bottom: 1rem; | ||
| - | box-shadow: 0 10px 30px rgba(0,0,0,0.3); | ||
| - | } | ||
| - | input, button { | ||
| - | font-size: 1rem; | ||
| - | padding: 0.5rem 0.75rem; | ||
| - | border-radius: 0.5rem; | ||
| - | border: 1px solid #444; | ||
| - | background: #222; | ||
| - | color: #eee; | ||
| - | margin: 0.25rem 0; | ||
| - | } | ||
| - | button { cursor: pointer; } | ||
| - | button:hover { background: #333; } | ||
| - | table { | ||
| - | width: 100%; | ||
| - | border-collapse: collapse; | ||
| - | margin-top: 0.5rem; | ||
| - | } | ||
| - | th, td { | ||
| - | border-bottom: 1px solid #333; | ||
| - | text-align: left; | ||
| - | padding: 0.35rem 0.5rem; | ||
| - | font-size: 0.9rem; | ||
| - | } | ||
| - | .flex { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; } | ||
| - | .right { float: right; } | ||
| - | #dashboard { display: none; } | ||
| - | #led-color { padding: 0; border: none; width: 3rem; height: 2rem; } | ||
| - | .badge { | ||
| - | display: inline-block; | ||
| - | padding: 0.15rem 0.4rem; | ||
| - | border-radius: 999px; | ||
| - | font-size: 0.75rem; | ||
| - | background: #333; | ||
| - | } | ||
| - | .chart-stack { display: flex; flex-direction: column; gap: 1rem; } | ||
| - | .chart-card { background: #0d0d0d; border-radius: 0.75rem; padding: 0.75rem; } | ||
| - | canvas { width: 100%; height: 220px; } | ||
| - | </style> | ||
| - | |||
| - | <!-- Firebase compat CDN (v9.x) --> | ||
| - | <script defer src="https://www.gstatic.com/firebasejs/9.10.0/firebase-app-compat.js"></script> | ||
| - | <script defer src="https://www.gstatic.com/firebasejs/9.10.0/firebase-auth-compat.js"></script> | ||
| - | <script defer src="https://www.gstatic.com/firebasejs/9.10.0/firebase-database-compat.js"></script> | ||
| - | <!-- Chart.js self-hosted for realtime plots (avoid CDN/CSP issues) --> | ||
| - | <script defer src="chart.umd.min.js?v=1"></script> | ||
| - | |||
| - | <script defer src="app.js"></script> | ||
| - | </head> | ||
| - | <body> | ||
| - | <div class="container"> | ||
| - | <!-- Login screen --> | ||
| - | <div id="login-screen" class="card"> | ||
| - | <h1>🔐 Sparrow Login</h1> | ||
| - | <p>Enter your Firebase email & password.</p> | ||
| - | <input id="login-email" type="email" placeholder="Email" style="width:100%" /> | ||
| - | <input id="login-password" type="password" placeholder="Password" style="width:100%" /> | ||
| - | <button id="login-btn">Login</button> | ||
| - | <p id="login-error" style="color:#ff8080;"></p> | ||
| - | </div> | ||
| - | |||
| - | <!-- Dashboard --> | ||
| - | <div id="dashboard"> | ||
| - | <div class="card"> | ||
| - | <div class="flex"> | ||
| - | <h1 style="margin:0;">🌡️ Sparrow Dashboard</h1> | ||
| - | <span class="badge" id="user-email"></span> | ||
| - | <button id="logout-btn" class="right">Logout</button> | ||
| - | </div> | ||
| - | </div> | ||
| - | |||
| - | <div class="card"> | ||
| - | <h2>Latest reading</h2> | ||
| - | <div id="latest-reading"> | ||
| - | <p>No data yet…</p> | ||
| - | </div> | ||
| - | </div> | ||
| - | |||
| - | <div class="card"> | ||
| - | <h2>Realtime plots</h2> | ||
| - | <p id="chart-status" style="margin:0 0 0.5rem 0; color:#aaa; font-size:0.9rem;">Loading charts…</p> | ||
| - | <div class="chart-stack"> | ||
| - | <div class="chart-card"> | ||
| - | <div style="margin-bottom:0.25rem; color:#ccc; font-size:0.95rem;">Temperature (°C)</div> | ||
| - | <canvas id="chart-temp"></canvas> | ||
| - | </div> | ||
| - | <div class="chart-card"> | ||
| - | <div style="margin-bottom:0.25rem; color:#ccc; font-size:0.95rem;">Humidity (%)</div> | ||
| - | <canvas id="chart-hum"></canvas> | ||
| - | </div> | ||
| - | <div class="chart-card"> | ||
| - | <div style="margin-bottom:0.25rem; color:#ccc; font-size:0.95rem;">Pressure (hPa)</div> | ||
| - | <canvas id="chart-press"></canvas> | ||
| - | </div> | ||
| - | </div> | ||
| - | </div> | ||
| - | |||
| - | <div class="card"> | ||
| - | <h2>History (last 20)</h2> | ||
| - | <table> | ||
| - | <thead> | ||
| - | <tr> | ||
| - | <th>Time</th> | ||
| - | <th>Temp (°C)</th> | ||
| - | <th>Hum (%)</th> | ||
| - | <th>Press (hPa)</th> | ||
| - | </tr> | ||
| - | </thead> | ||
| - | <tbody id="history-body"> | ||
| - | </tbody> | ||
| - | </table> | ||
| - | </div> | ||
| - | |||
| - | <div class="card"> | ||
| - | <h2>NeoPixel control</h2> | ||
| - | <div class="flex"> | ||
| - | <label><input type="checkbox" id="led-on" /> LED On</label> | ||
| - | <label>Color <input type="color" id="led-color" value="#ffffff" /></label> | ||
| - | <button id="led-apply-btn">Apply</button> | ||
| - | </div> | ||
| - | <p id="led-status"></p> | ||
| - | </div> | ||
| - | </div> | ||
| - | </div> | ||
| - | </body> | ||
| - | </html> | ||
| - | |||
| - | </code> | ||
| ==== 4.3 app.js ==== | ==== 4.3 app.js ==== | ||
| - | Create ''dashboard/public/app.js'' and paste this. | + | Create ''dashboard/public/app.js'' and paste [[iothings:laboratoare:2025_code:lab9_3|this]]. |
| - | **Replace** the ''firebaseConfig'' values with those from your Firebase console: | + | |
| - | <code javascript> | + | **Replace** the ''firebaseConfig'' values with those from your Firebase console. |
| - | // ===== Firebase config (from Firebase console web app) ===== | + | |
| - | const firebaseConfig = { | + | |
| - | apiKey: "YOUR_API_KEY", | + | |
| - | authDomain: "your-project-id.firebaseapp.com", | + | |
| - | databaseURL: "https://your-project-id-default-rtdb.YOUR_REGION.firebasedatabase.app", | + | |
| - | projectId: "your-project-id", | + | |
| - | }; | + | |
| - | + | ||
| - | const DEVICE_ID = "sparrow-01"; | + | |
| - | const MAX_CHART_POINTS = 50; | + | |
| - | const CHART_LOCAL_SRC = "chart.umd.min.js?v=1"; | + | |
| - | const CHART_CDN_SRC = "https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"; | + | |
| - | + | ||
| - | // Wait for Firebase scripts (loaded via defer) | + | |
| - | window.addEventListener("DOMContentLoaded", () => { | + | |
| - | firebase.initializeApp(firebaseConfig); | + | |
| - | + | ||
| - | const auth = firebase.auth(); | + | |
| - | const db = firebase.database(); | + | |
| - | + | ||
| - | const loginScreen = document.getElementById("login-screen"); | + | |
| - | const dashboard = document.getElementById("dashboard"); | + | |
| - | + | ||
| - | const loginEmail = document.getElementById("login-email"); | + | |
| - | const loginPassword = document.getElementById("login-password"); | + | |
| - | const loginBtn = document.getElementById("login-btn"); | + | |
| - | const loginError = document.getElementById("login-error"); | + | |
| - | + | ||
| - | const logoutBtn = document.getElementById("logout-btn"); | + | |
| - | const userEmail = document.getElementById("user-email"); | + | |
| - | const latestDiv = document.getElementById("latest-reading"); | + | |
| - | const historyBody = document.getElementById("history-body"); | + | |
| - | const chartTempEl = document.getElementById("chart-temp"); | + | |
| - | const chartHumEl = document.getElementById("chart-hum"); | + | |
| - | const chartPressEl= document.getElementById("chart-press"); | + | |
| - | const chartStatus = document.getElementById("chart-status"); | + | |
| - | + | ||
| - | const ledOnCheckbox = document.getElementById("led-on"); | + | |
| - | const ledColorInput = document.getElementById("led-color"); | + | |
| - | const ledApplyBtn = document.getElementById("led-apply-btn"); | + | |
| - | const ledStatus = document.getElementById("led-status"); | + | |
| - | + | ||
| - | let charts = null; | + | |
| - | const chartState = { | + | |
| - | labels: [], | + | |
| - | temp: [], | + | |
| - | hum: [], | + | |
| - | press: [], | + | |
| - | }; | + | |
| - | + | ||
| - | function loadScript(src) { | + | |
| - | return new Promise((resolve, reject) => { | + | |
| - | const s = document.createElement("script"); | + | |
| - | s.src = src; | + | |
| - | s.onload = resolve; | + | |
| - | s.onerror = reject; | + | |
| - | document.head.appendChild(s); | + | |
| - | }); | + | |
| - | } | + | |
| - | + | ||
| - | async function ensureChartReady() { | + | |
| - | if (window.Chart) { | + | |
| - | if (chartStatus) chartStatus.textContent = `Charts ready (Chart.js ${window.Chart.version || ""})`; | + | |
| - | return true; | + | |
| - | } | + | |
| - | if (chartStatus) chartStatus.textContent = "Loading charts…"; | + | |
| - | try { | + | |
| - | await loadScript(CHART_LOCAL_SRC); | + | |
| - | } catch (e) { | + | |
| - | console.warn("Local Chart.js failed, trying CDN", e); | + | |
| - | try { | + | |
| - | await loadScript(CHART_CDN_SRC); | + | |
| - | } catch (e2) { | + | |
| - | console.error("Chart.js failed to load from all sources", e2); | + | |
| - | return false; | + | |
| - | } | + | |
| - | } | + | |
| - | const ok = !!window.Chart; | + | |
| - | if (chartStatus) chartStatus.textContent = ok ? `Charts ready (Chart.js ${window.Chart.version || ""})` : "Charts unavailable"; | + | |
| - | return ok; | + | |
| - | } | + | |
| - | + | ||
| - | function resetCharts() { | + | |
| - | chartState.labels = []; | + | |
| - | chartState.temp = []; | + | |
| - | chartState.hum = []; | + | |
| - | chartState.press = []; | + | |
| - | if (charts) { | + | |
| - | charts.temp.data.labels = []; | + | |
| - | charts.temp.data.datasets[0].data = []; | + | |
| - | charts.hum.data.labels = []; | + | |
| - | charts.hum.data.datasets[0].data = []; | + | |
| - | charts.press.data.labels = []; | + | |
| - | charts.press.data.datasets[0].data = []; | + | |
| - | charts.temp.update("none"); | + | |
| - | charts.hum.update("none"); | + | |
| - | charts.press.update("none"); | + | |
| - | } | + | |
| - | } | + | |
| - | + | ||
| - | function initCharts() { | + | |
| - | if (!window.Chart) { | + | |
| - | console.warn("Chart.js not available"); | + | |
| - | return; | + | |
| - | } | + | |
| - | + | ||
| - | const baseOptions = { | + | |
| - | responsive: true, | + | |
| - | animation: false, | + | |
| - | plugins: { legend: { display: false } }, | + | |
| - | scales: { | + | |
| - | x: { ticks: { color: "#aaa" }, grid: { color: "rgba(255,255,255,0.05)" } }, | + | |
| - | y: { ticks: { color: "#aaa" }, grid: { color: "rgba(255,255,255,0.05)" } }, | + | |
| - | }, | + | |
| - | }; | + | |
| - | + | ||
| - | charts = { | + | |
| - | temp: new Chart(chartTempEl.getContext("2d"), { | + | |
| - | type: "line", | + | |
| - | data: { labels: [], datasets: [{ label: "Temp (°C)", data: [], borderColor: "#ff6b6b", backgroundColor: "rgba(255,107,107,0.15)", tension: 0.2, fill: true, pointRadius: 0 }] }, | + | |
| - | options: baseOptions, | + | |
| - | }), | + | |
| - | hum: new Chart(chartHumEl.getContext("2d"), { | + | |
| - | type: "line", | + | |
| - | data: { labels: [], datasets: [{ label: "Hum (%)", data: [], borderColor: "#4fd1c5", backgroundColor: "rgba(79,209,197,0.15)", tension: 0.2, fill: true, pointRadius: 0 }] }, | + | |
| - | options: baseOptions, | + | |
| - | }), | + | |
| - | press: new Chart(chartPressEl.getContext("2d"), { | + | |
| - | type: "line", | + | |
| - | data: { labels: [], datasets: [{ label: "Press (hPa)", data: [], borderColor: "#9f7aea", backgroundColor: "rgba(159,122,234,0.15)", tension: 0.2, fill: true, pointRadius: 0 }] }, | + | |
| - | options: baseOptions, | + | |
| - | }), | + | |
| - | }; | + | |
| - | } | + | |
| - | + | ||
| - | function updateCharts(ts, t, h, p) { | + | |
| - | if (!charts) return; | + | |
| - | const label = new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); | + | |
| - | + | ||
| - | chartState.labels.push(label); | + | |
| - | chartState.temp.push(t); | + | |
| - | chartState.hum.push(h); | + | |
| - | chartState.press.push(p); | + | |
| - | + | ||
| - | if (chartState.labels.length > MAX_CHART_POINTS) { | + | |
| - | chartState.labels.shift(); | + | |
| - | chartState.temp.shift(); | + | |
| - | chartState.hum.shift(); | + | |
| - | chartState.press.shift(); | + | |
| - | } | + | |
| - | + | ||
| - | charts.temp.data.labels = chartState.labels.slice(); | + | |
| - | charts.hum.data.labels = chartState.labels.slice(); | + | |
| - | charts.press.data.labels = chartState.labels.slice(); | + | |
| - | + | ||
| - | charts.temp.data.datasets[0].data = chartState.temp.slice(); | + | |
| - | charts.hum.data.datasets[0].data = chartState.hum.slice(); | + | |
| - | charts.press.data.datasets[0].data= chartState.press.slice(); | + | |
| - | + | ||
| - | charts.temp.update("none"); | + | |
| - | charts.hum.update("none"); | + | |
| - | charts.press.update("none"); | + | |
| - | if (chartStatus) chartStatus.textContent = `Charts live — last update ${label}`; | + | |
| - | } | + | |
| - | + | ||
| - | // ===== Auth UI handling ===== | + | |
| - | auth.onAuthStateChanged((user) => { | + | |
| - | if (user) { | + | |
| - | loginScreen.style.display = "none"; | + | |
| - | dashboard.style.display = "block"; | + | |
| - | userEmail.textContent = user.email || "(no email)"; | + | |
| - | resetCharts(); | + | |
| - | ensureChartReady().then((ok) => { | + | |
| - | if (ok) { | + | |
| - | initCharts(); | + | |
| - | } else { | + | |
| - | console.warn("Charts disabled: Chart.js not available"); | + | |
| - | if (chartStatus) chartStatus.textContent = "Charts disabled: Chart.js not available (check console / cache)"; | + | |
| - | } | + | |
| - | startDataListeners(); | + | |
| - | }); | + | |
| - | } else { | + | |
| - | dashboard.style.display = "none"; | + | |
| - | loginScreen.style.display = "block"; | + | |
| - | userEmail.textContent = ""; | + | |
| - | historyBody.innerHTML = ""; | + | |
| - | latestDiv.innerHTML = "<p>No data yet…</p>"; | + | |
| - | resetCharts(); | + | |
| - | if (chartStatus) chartStatus.textContent = "Charts require login to load."; | + | |
| - | } | + | |
| - | }); | + | |
| - | + | ||
| - | loginBtn.addEventListener("click", () => { | + | |
| - | loginError.textContent = ""; | + | |
| - | const email = loginEmail.value.trim(); | + | |
| - | const pass = loginPassword.value.trim(); | + | |
| - | auth.signInWithEmailAndPassword(email, pass) | + | |
| - | .catch(err => { | + | |
| - | console.error(err); | + | |
| - | loginError.textContent = err.message; | + | |
| - | }); | + | |
| - | }); | + | |
| - | + | ||
| - | logoutBtn.addEventListener("click", () => { | + | |
| - | auth.signOut(); | + | |
| - | }); | + | |
| - | + | ||
| - | // ===== Telemetry listeners ===== | + | |
| - | function addHistoryRow(ts, t, h, p) { | + | |
| - | const tr = document.createElement("tr"); | + | |
| - | const date = new Date(ts); | + | |
| - | + | ||
| - | tr.innerHTML = ` | + | |
| - | <td>${date.toLocaleString()}</td> | + | |
| - | <td>${t.toFixed(1)}</td> | + | |
| - | <td>${h.toFixed(1)}</td> | + | |
| - | <td>${p.toFixed(1)}</td> | + | |
| - | `; | + | |
| - | historyBody.prepend(tr); // newest on top | + | |
| - | + | ||
| - | // Keep at most 20 rows | + | |
| - | while (historyBody.rows.length > 20) { | + | |
| - | historyBody.deleteRow(historyBody.rows.length - 1); | + | |
| - | } | + | |
| - | } | + | |
| - | + | ||
| - | function updateLatest(ts, t, h, p) { | + | |
| - | const date = new Date(ts); | + | |
| - | latestDiv.innerHTML = ` | + | |
| - | <p><strong>${date.toLocaleString()}</strong></p> | + | |
| - | <p>Temperature: <strong>${t.toFixed(1)} °C</strong></p> | + | |
| - | <p>Humidity: <strong>${h.toFixed(1)} %</strong></p> | + | |
| - | <p>Pressure: <strong>${p.toFixed(1)} hPa</strong></p> | + | |
| - | `; | + | |
| - | } | + | |
| - | + | ||
| - | function startDataListeners() { | + | |
| - | // Listen for last 20 telemetry entries | + | |
| - | const telemRef = db.ref(`devices/${DEVICE_ID}/telemetry`).limitToLast(20); | + | |
| - | telemRef.off(); // avoid duplicates on re-login | + | |
| - | + | ||
| - | telemRef.on("child_added", (snap) => { | + | |
| - | const val = snap.val(); | + | |
| - | if (!val) return; | + | |
| - | const ts = val.timestamp || Date.now(); | + | |
| - | const t = val.temperature || 0; | + | |
| - | const h = val.humidity || 0; | + | |
| - | const p = val.pressure || 0; | + | |
| - | + | ||
| - | updateLatest(ts, t, h, p); | + | |
| - | addHistoryRow(ts, t, h, p); | + | |
| - | updateCharts(ts, t, h, p); | + | |
| - | }); | + | |
| - | + | ||
| - | // Keep dashboard LED controls in sync with DB | + | |
| - | const ledRef = db.ref(`devices/${DEVICE_ID}/led`); | + | |
| - | ledRef.off(); | + | |
| - | + | ||
| - | ledRef.on("value", (snap) => { | + | |
| - | const val = snap.val(); | + | |
| - | if (!val) return; | + | |
| - | const state = val.state || "off"; | + | |
| - | const color = val.color || "#ffffff"; | + | |
| - | + | ||
| - | ledOnCheckbox.checked = state === "on"; | + | |
| - | ledColorInput.value = color; | + | |
| - | ledStatus.textContent = `Current: ${state.toUpperCase()} ${color}`; | + | |
| - | }); | + | |
| - | } | + | |
| - | + | ||
| - | // ===== LED control write ===== | + | |
| - | ledApplyBtn.addEventListener("click", () => { | + | |
| - | const state = ledOnCheckbox.checked ? "on" : "off"; | + | |
| - | const color = ledColorInput.value || "#ffffff"; | + | |
| - | + | ||
| - | const ledRef = db.ref(`devices/${DEVICE_ID}/led`); | + | |
| - | ledRef.set({ state, color }) | + | |
| - | .then(() => { | + | |
| - | ledStatus.textContent = `Set: ${state.toUpperCase()} ${color}`; | + | |
| - | }) | + | |
| - | .catch(err => { | + | |
| - | console.error(err); | + | |
| - | ledStatus.textContent = `Error: ${err.message}`; | + | |
| - | }); | + | |
| - | }); | + | |
| - | }); | + | |
| - | + | ||
| - | </code> | + | |
| + | ==== 4.4 chart.umd.min.js ==== | ||
| + | Create ''dashboard/public/chart.umd.min.js''. You need this for rendering the charts locally. | ||
| + | The code is [[iothings:laboratoare:2025_code:lab9_4|here]]. | ||
| ===== 5. Deploy Dashboard with Firebase Hosting ===== | ===== 5. Deploy Dashboard with Firebase Hosting ===== | ||