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:

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
 
#include <Adafruit_BME680.h>
#include <Adafruit_NeoPixel.h>
#include <ArduinoJson.h>
 
// ====== USER CONFIG ======
const char* WIFI_SSID     = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
 
const char* FIREBASE_API_KEY = "YOUR_FIREBASE_API_KEY"; // from firebaseConfig.apiKey
const char* FIREBASE_DB_URL  = "https://YOUR_PROJECT_ID-default-rtdb.YOUR_REGION.firebasedatabase.app";
 
const char* DEVICE_EMAIL    = "device@sparrow.local";
const char* DEVICE_PASSWORD = "DEVICE_PASSWORD";
const char* DEVICE_ID       = "sparrow-01";
 
#define I2C_SDA       21   
#define I2C_SCL       22   
#define NEOPIXEL_PIN  3    
#define MIN_VALID_EPOCH 1577836800UL // Jan 1 2020 used to detect if NTP time is set
 
// How often to send data / poll LED
const unsigned long TELEMETRY_INTERVAL_MS = 10UL * 1000UL; // 10 seconds
const unsigned long LED_POLL_INTERVAL_MS  = 2UL * 1000UL;  // 2 seconds
 
// Re-auth roughly every 50 minutes
const unsigned long TOKEN_REFRESH_MS = 50UL * 60UL * 1000UL;
 
// ====== Globals ======
WiFiClientSecure secureClient;
Adafruit_BME680 bme;
Adafruit_NeoPixel pixel(1, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
 
String idToken;
unsigned long lastSignIn      = 0;
unsigned long lastTelemetry   = 0;
unsigned long lastLedPoll     = 0;
bool timeSynced               = false;
 
// ====== Utility: Time/NTP ======
bool hasValidTime() {
  return time(nullptr) > MIN_VALID_EPOCH;
}
 
void syncTimeIfNeeded() {
  if (timeSynced || WiFi.status() != WL_CONNECTED) {
    return;
  }
 
  // Adjust offsets for your timezone / daylight saving as needed
  const long gmtOffset_sec = 0;
  const int daylightOffset_sec = 0;
  configTime(gmtOffset_sec, daylightOffset_sec, "pool.ntp.org", "time.nist.gov");
 
  struct tm timeinfo;
  if (getLocalTime(&timeinfo, 2000)) { // wait up to 2 seconds
    timeSynced = true;
    Serial.println("NTP time synced");
  }
}
 
// ====== Utility: WiFi ======
void connectWiFi() {
  Serial.print("Connecting to WiFi ");
  Serial.println(WIFI_SSID);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
 
  int retries = 0;
  while (WiFi.status() != WL_CONNECTED && retries < 40) {
    delay(500);
    Serial.print(".");
    retries++;
  }
  Serial.println();
 
  if (WiFi.status() == WL_CONNECTED) {
    Serial.print("WiFi connected, IP = ");
    Serial.println(WiFi.localIP());
  } else {
    Serial.println("WiFi connection failed");
  }
}
 
// ====== Utility: Firebase Sign-in (email/password) ======
bool firebaseSignIn() {
  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
    if (WiFi.status() != WL_CONNECTED) {
      return false;
    }
  }
 
  String url = String("https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=") + FIREBASE_API_KEY;
 
  HTTPClient https;
  https.begin(secureClient, url);
  https.addHeader("Content-Type", "application/json");
 
  StaticJsonDocument<256> payloadDoc;
  payloadDoc["email"] = DEVICE_EMAIL;
  payloadDoc["password"] = DEVICE_PASSWORD;
  payloadDoc["returnSecureToken"] = true;
 
  String payload;
  serializeJson(payloadDoc, payload);
 
  Serial.println("Signing in to Firebase...");
  int httpCode = https.POST(payload);
  if (httpCode != 200) {
    Serial.print("Sign-in failed, HTTP code ");
    Serial.println(httpCode);
    Serial.println(https.getString());
    https.end();
    return false;
  }
 
  DynamicJsonDocument respDoc(1024);
  DeserializationError err = deserializeJson(respDoc, https.getString());
  https.end();
 
  if (err) {
    Serial.print("Failed to parse sign-in response: ");
    Serial.println(err.c_str());
    return false;
  }
 
  idToken = respDoc["idToken"].as<String>();
  String expiresInStr = respDoc["expiresIn"].as<String>();
  Serial.print("Sign-in OK, idToken length = ");
  Serial.println(idToken.length());
  Serial.print("Token expires in (sec): ");
  Serial.println(expiresInStr);
 
  lastSignIn = millis();
  return true;
}
 
// Ensure we have a valid token
bool ensureSignedIn() {
  if (idToken.length() == 0 || millis() - lastSignIn > TOKEN_REFRESH_MS) {
    return firebaseSignIn();
  }
  return true;
}
 
// ====== Utility: HTTP POST to Realtime DB ======
bool firebasePost(const String& path, const String& jsonBody) {
  if (!ensureSignedIn()) {
    Serial.println("Cannot POST: not signed in");
    return false;
  }
  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
    if (WiFi.status() != WL_CONNECTED) return false;
  }
 
  String url = String(FIREBASE_DB_URL) + path + ".json?auth=" + idToken;
 
  HTTPClient https;
  https.begin(secureClient, url);
  https.addHeader("Content-Type", "application/json");
 
  int httpCode = https.POST(jsonBody);
  Serial.print("POST ");
  Serial.print(path);
  Serial.print(" -> HTTP ");
  Serial.println(httpCode);
 
  if (httpCode < 200 || httpCode >= 300) {
    Serial.println(https.getString());
    https.end();
    return false;
  }
  https.end();
  return true;
}
 
// ====== Utility: HTTP GET from Realtime DB ======
bool firebaseGet(const String& path, String& responseOut) {
  if (!ensureSignedIn()) {
    Serial.println("Cannot GET: not signed in");
    return false;
  }
  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
    if (WiFi.status() != WL_CONNECTED) return false;
  }
 
  String url = String(FIREBASE_DB_URL) + path + ".json?auth=" + idToken;
 
  HTTPClient https;
  https.begin(secureClient, url);
 
  int httpCode = https.GET();
  Serial.print("GET ");
  Serial.print(path);
  Serial.print(" -> HTTP ");
  Serial.println(httpCode);
 
  if (httpCode != 200) {
    Serial.println(https.getString());
    https.end();
    return false;
  }
 
  responseOut = https.getString();
  https.end();
  return true;
}
 
// ====== BME680 ======
bool initBME() {
  Wire.begin(I2C_SDA, I2C_SCL);
 
  if (!bme.begin(0x76)) {  // change to 0x77 if needed
    Serial.println("Could not find BME680 sensor!");
    return false;
  }
 
  bme.setTemperatureOversampling(BME680_OS_8X);
  bme.setHumidityOversampling(BME680_OS_2X);
  bme.setPressureOversampling(BME680_OS_4X);
  bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
  bme.setGasHeater(0, 0); // turn off gas heater to save power
 
  Serial.println("BME680 initialized.");
  return true;
}
 
bool readBME(float& temperature, float& humidity, float& pressure) {
  if (!bme.performReading()) {
    Serial.println("Failed to read BME680");
    return false;
  }
  temperature = bme.temperature;              // °C
  humidity    = bme.humidity;                 // %
  pressure    = bme.pressure / 100.0f;        // hPa
  return true;
}
 
// ====== NeoPixel helpers ======
uint32_t parseColor(const String& hexColor) {
  String hex = hexColor;
  if (hex.startsWith("#")) {
    hex.remove(0, 1);
  }
  if (hex.length() != 6) {
    return pixel.Color(255, 255, 255); // default to white
  }
  long rgb = strtol(hex.c_str(), nullptr, 16);
  uint8_t r = (rgb >> 16) & 0xFF;
  uint8_t g = (rgb >> 8) & 0xFF;
  uint8_t b = rgb & 0xFF;
  return pixel.Color(r, g, b);
}
 
void applyLedState(const String& state, const String& colorHex) {
  bool on = state == "on";
  uint32_t color = parseColor(colorHex);
  if (!on) {
    pixel.setPixelColor(0, 0, 0, 0);
  } else {
    pixel.setPixelColor(0, color);
  }
  pixel.show();
 
  Serial.print("LED -> ");
  Serial.print(on ? "ON " : "OFF ");
  Serial.println(colorHex);
}
 
// ====== Periodic tasks ======
void sendTelemetryIfDue() {
  if (millis() - lastTelemetry < TELEMETRY_INTERVAL_MS) return;
  lastTelemetry = millis();
 
  float t, h, p;
  if (!readBME(t, h, p)) return;
 
  StaticJsonDocument<256> doc;
  long long nowMs = hasValidTime() ? (long long) (time(nullptr) * 1000LL)
                                   : (long long) millis();
  doc["timestamp"]   = nowMs;
  doc["temperature"] = t;
  doc["humidity"]    = h;
  doc["pressure"]    = p;
 
  String json;
  serializeJson(doc, json);
 
  String path = "/devices/" + String(DEVICE_ID) + "/telemetry";
  firebasePost(path, json);
}
 
void pollLedIfDue() {
  if (millis() - lastLedPoll < LED_POLL_INTERVAL_MS) return;
  lastLedPoll = millis();
 
  String response;
  String path = "/devices/" + String(DEVICE_ID) + "/led";
  if (!firebaseGet(path, response)) return;
 
  if (response == "null") {
    // no LED data yet
    return;
  }
 
  DynamicJsonDocument doc(256);
  DeserializationError err = deserializeJson(doc, response);
  if (err) {
    Serial.print("LED JSON parse error: ");
    Serial.println(err.c_str());
    return;
  }
 
  String state = doc["state"] | "off";
  String color = doc["color"] | "#ffffff";
  applyLedState(state, color);
}
 
// ====== SETUP / LOOP ======
void setup() {
  Serial.begin(115200);
  delay(1000);
 
  connectWiFi();
  syncTimeIfNeeded();
 
  secureClient.setTimeout(15000);
  secureClient.setInsecure(); // NOTE: uses HTTPS but skips cert validation (lab use)
 
  initBME();
 
  pixel.begin();
  pixel.clear();
  pixel.show();
 
  // Initial sign in
  firebaseSignIn();
}
 
void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
  }
 
  syncTimeIfNeeded();
  sendTelemetryIfDue();
  pollLedIfDue();
 
  delay(10); // tiny delay to keep loop friendly
}

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.

6. Next Steps & Improvements

  • TLS verification
    • Instead of secureClient.setInsecure(), embed the CA certificates for Google's HTTPS endpoints and call setCACert().
  • More secure device auth
    • For production, consider using Firebase custom tokens or another backend that generates a short-lived token for each device instead of hard-coding email/password in firmware.
  • Per-user access control
    • You can refine Realtime Database rules to allow different users to access different devices (e.g. admin vs guest).
  • More sensors and controls
    • Extend the JSON payload for additional readings, or add more command keys under /devices/sparrow-01.

You now have:

  • A Sparrow ESP32-C6 sending BME680 telemetry over HTTPS to Firebase.
  • A password-protected web dashboard to view data and control the on-board NeoPixel.

</code>

iothings/laboratoare/2025/lab9.1763893191.txt.gz · Last modified: 2025/11/23 12:19 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