Differences

This shows you the differences between two versions of the page.

Link to this comparison view

iothings:laboratoare:2025:lab9 [2025/11/23 12:46]
dan.tudose [3.3 Full main.cpp Example]
iothings:laboratoare:2025:lab9 [2025/11/23 13:43] (current)
dan.tudose [3. ESP32-C6 Sparrow Firmware (HTTPS + BME680 + NeoPixel)]
Line 174: Line 174:
 ---- ----
  
-===== 3. ESP32-C6 Firmware (HTTPS + BME680 + NeoPixel) ===== +===== 3. ESP32-C6 ​Sparrow ​Firmware (HTTPS + BME680 + NeoPixel) =====
- +
-==== 3.1 Overview ​====+
  
 On boot, the firmware will: On boot, the firmware will:
Line 191: 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 productionembed proper CA certificates.*+  ​* ​The certificate is the full Google TLS root (GTS Root R1cross-signed by GlobalSign) captured from a live identitytoolkit.googleapis.com chain and reverted to setCACert using that PEM
  
-==== 3.2 Configure Pins and Credentials ==== 
  
 +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 203: Line 200:
   * Device ID: ''​sparrow-01''​.   * Device ID: ''​sparrow-01''​.
  
-==== 3.3 Full main.cpp Example ==== 
  
-Create or edit ''​src/​main.cpp'':​ +  1. Build and upload ​the firmware
- +  2. In the serial monitorverify:
-==== 3.4 Build, Upload, ​and Test ==== +
- +
-  1. Connect ​the Sparrow using USB+
-  2. In pioarduinobuild and upload the firmware. +
-  3. Open the **Serial Monitor** at 115200 baud. +
-  4. Verify:+
      * Wi-Fi connects.      * Wi-Fi connects.
      * Firebase sign-in succeeds.      * Firebase sign-in succeeds.
      * POST/GET requests show HTTP 200.      * POST/GET requests show HTTP 200.
-  ​5. In Firebase console → Realtime Database, you should see data appearing under:+  ​3. In Firebase console → Realtime Database, you should see data appearing under:
      * ''/​devices/​sparrow-01/​telemetry''​      * ''/​devices/​sparrow-01/​telemetry''​
  
 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 226: Line 215:
   * Wi-Fi credentials.   * Wi-Fi credentials.
  
-----+
  
 ===== 4. Web Dashboard (Password-Protected) ===== ===== 4. Web Dashboard (Password-Protected) =====
Line 251: Line 240:
     index.html     index.html
     app.js     app.js
 +    chart.umd.min.js
 </​code>​ </​code>​
  
Line 257: 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 =====
  
iothings/laboratoare/2025/lab9.1763894796.txt.gz · Last modified: 2025/11/23 12:46 by dan.tudose
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0