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 Sparrow Firmware (HTTPS + BME680 + NeoPixel)

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.*

Edit src/main.cpp, get the code here. Fill in your credentials:

  • Wi-Fi SSID and password.
  • Firebase apiKey and databaseURL.
  • Device email and password.
  • Device ID: sparrow-01.
1. Build and upload the firmware.
2. In the serial monitor, verify:
   * Wi-Fi connects.
   * Firebase sign-in succeeds.
   * POST/GET requests show HTTP 200.
3. 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:

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.1763895147.txt.gz · Last modified: 2025/11/23 12:52 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