This is an old revision of the document!


Lab 9. Firebase & Dashboard Hosting

This laboratory tutorial shows you how to:

  • Send sensor data securely to Firebase Realtime Database over HTTPS.
  • Deploy a password-protected web dashboard (Firebase Auth) to:
    • View the sensor readings.
    • Control the on-board NeoPixel LED (on/off + color).

1. Create the PlatformIO Project

  1. In pioarduino, create a New Project.
  2. Choose an ESP32-C6 board that matches your Sparrow (e.g. esp32-c6-devkitm-1).
  3. Framework: Arduino.

Edit the generated platformio.ini to look similar to this (adjust board if needed):

platformio.ini
[env:sparrow]
; Community fork of espressif32 platform w/ Arduino core 3.x for ESP32-C6
platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20/platform-espressif32.zip
 
board = esp32-c6-devkitm-1
framework = arduino
 
monitor_speed = 115200
 
build_flags =
  -D ARDUINO_USB_MODE=1
  -D ARDUINO_USB_CDC_ON_BOOT=1
  -D ESP32_C6_env
 
; We'll need Wire (built-in), math, etc. For the RGB LED we'll pull Adafruit NeoPixel.
lib_deps =
  adafruit/Adafruit NeoPixel @ ^1.12.0
  bblanchon/ArduinoJson
  adafruit/Adafruit BME680 Library @ ^2.0.5
  adafruit/Adafruit Unified Sensor
  adafruit/Adafruit BusIO

We will put the firmware in src/main.cpp.

2. Create and Configure the Firebase Project

2.1 Create a Firebase Project

  1. Click Go to console.
  2. Click Add project.
  3. Enter a project name, for example: sparrow-lab.
  4. Enable or disable Google Analytics as you prefer.
  5. Finish project creation.

2.2 Create a Realtime Database

  1. In the Firebase console, open your project.
  2. In the left menu, go to Build → Realtime Database.
  3. Click Create database.
  4. Choose a region (e.g. europe-west1 or us-central1).
  5. Start in locked mode (recommended).

Note the generated database URL, it will look like:

https://YOUR_PROJECT_ID-default-rtdb.REGION.firebasedatabase.app

We will call this FIREBASE_DB_URL in the firmware.

2.3 Enable Email/Password Authentication

  1. In the Firebase console, go to Build → Authentication → Sign-in method.
  2. Enable Email/Password.
  3. Save.

Then go to the Users tab and create these accounts:

  • Device account: device@sparrow.local
  • Admin account: admin@sparrow.local

Both will use normal passwords of your choice. The device account will be hard-coded in the firmware (fine for lab use).

2.4 Register a Web App (Dashboard)

  1. In the project overview, click the Web (</>) icon (Add app).
  2. Give it a name, e.g. sparrow-dashboard.
  3. Register the app.

Firebase will show you a JavaScript config object like:

const firebaseConfig = {
  apiKey: "AIzaSy...",
  authDomain: "your-project-id.firebaseapp.com",
  databaseURL: "https://your-project-id-default-rtdb.europe-west1.firebasedatabase.app",
  projectId: "your-project-id",
  storageBucket: "your-project-id.appspot.com",
  messagingSenderId: "1234567890",
  appId: "1:1234567890:web:abcdef123456",
};

Copy this; we will use:

  • apiKey
  • databaseURL
  • projectId

2.5 Database Structure

We will store data in Realtime Database like this:

{
  "devices": {
    "sparrow-01": {
      "telemetry": {
        "-Nx123...": {
          "timestamp": 1730000000000,
          "temperature": 23.5,
          "humidity": 45.2,
          "pressure": 1008.3
        }
      },
      "led": {
        "state": "on",
        "color": "#ff00aa"
      }
    }
  }
}

Paths used:

  • ESP32 sends BME680 data to:
    • /devices/sparrow-01/telemetry
  • The web app reads that telemetry.
  • The web app writes LED commands to:
    • /devices/sparrow-01/led
  • The ESP32 reads /devices/sparrow-01/led and updates the NeoPixel.

2.6 Security Rules (Password Protection)

In the Firebase console, go to Realtime Database → Rules and set:

{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null"
  }
}

This means:

  • Only authenticated users can read and write.
  • The ESP32 authenticates using Firebase Auth REST (email/password).
  • The dashboard authenticates via Firebase JS SDK (email/password).

Thus all data access is password protected.


3. ESP32-C6 Firmware (HTTPS + BME680 + NeoPixel)

3.1 Overview

On boot, the firmware will:

  1. Connect to Wi-Fi.
  2. Authenticate via Firebase Auth REST API using device@sparrow.local and its password.
  3. Obtain an ID token.
  4. Periodically:
    • Read BME680 (temperature, humidity, pressure).
    • POST telemetry to Realtime Database via HTTPS REST.
    • Poll the LED node and update the NeoPixel.

We will use:

  • WiFiClientSecure for HTTPS.
  • For simplicity we will call setInsecure() (it still uses TLS but skips certificate validation).
    • For production, embed proper CA certificates.*

3.2 Configure Pins and Credentials

Fill in your credentials:

  • Wi-Fi SSID and password.
  • Firebase apiKey and databaseURL.
  • Device email and password.
  • Device ID: sparrow-01.

3.3 Full main.cpp Example

Create or edit src/main.cpp:

3.4 Build, Upload, and Test

1. Connect the Sparrow using USB.
2. In pioarduino, build and upload the firmware.
3. Open the **Serial Monitor** at 115200 baud.
4. Verify:
   * Wi-Fi connects.
   * Firebase sign-in succeeds.
   * POST/GET requests show HTTP 200.
5. In Firebase console → Realtime Database, you should see data appearing under:
   * ''/devices/sparrow-01/telemetry''

If you see sign-in or HTTPS errors, double-check:

  • FIREBASE_API_KEY
  • FIREBASE_DB_URL
  • Device email/password.
  • Wi-Fi credentials.

4. Web Dashboard (Password-Protected)

We will build a simple dashboard with:

  • Login form (email/password via Firebase Auth).
  • Live display of latest BME680 reading.
  • History table (last 20 entries).
  • LED controls (on/off + color picker).

The dashboard is protected because:

  • Database rules require auth != null.
  • The page uses Firebase Auth to sign in users.

4.1 Project Structure

Create a folder for the dashboard, for example:

dashboard/
  public/
    index.html
    app.js

Firebase Hosting will serve the public/ folder.

4.2 index.html

Create dashboard/public/index.html with:

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>

4.3 app.js

Create dashboard/public/app.js and paste this. 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}`;
      });
  });
});

5. Deploy Dashboard with Firebase Hosting

5.1 Install Firebase CLI

Install Firebase CLI globally:

npm install -g firebase-tools

Log in and initialize hosting:

firebase login
cd dashboard
firebase init hosting

When prompted:

  • Which project? → choose your sparrow-lab project.
  • Public directory?public
  • Configure as single-page app (rewrite all urls to /index.html)?y

This will create firebase.json and other files.

5.2 Deploy the Dashboard

From inside dashboard:

firebase deploy --only hosting

After deployment you will get a Hosting URL like:

https://sparrow-lab.web.app

Open that URL in a browser and:

1. Log in with ''admin@sparrow.local'' and its password.
2. You should see:
   * Latest reading section.
   * History table.
   * NeoPixel control panel.
3. With the Sparrow powered and connected, BME680 data should appear.
4. Changing the LED state/color should update the on-board NeoPixel within a few seconds.
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