This is an old revision of the document!
This laboratory tutorial shows you how to:
esp32-c6-devkitm-1).
Edit the generated platformio.ini to look similar to this (adjust board if needed):
[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.
sparrow-lab.europe-west1 or us-central1).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.
Then go to the Users tab and create these accounts:
device@sparrow.localadmin@sparrow.localBoth will use normal passwords of your choice. The device account will be hard-coded in the firmware (fine for lab use).
sparrow-dashboard.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:
apiKeydatabaseURLprojectIdWe 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:
/devices/sparrow-01/telemetry/devices/sparrow-01/led/devices/sparrow-01/led and updates the NeoPixel.In the Firebase console, go to Realtime Database → Rules and set:
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
This means:
Thus all data access is password protected.
On boot, the firmware will:
device@sparrow.local and its password.We will use:
WiFiClientSecure for HTTPS.setInsecure() (it still uses TLS but skips certificate validation). Fill in your credentials:
apiKey and databaseURL.sparrow-01.
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 }
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_KEYFIREBASE_DB_URLWe will build a simple dashboard with:
The dashboard is protected because:
auth != null.Create a folder for the dashboard, for example:
dashboard/
public/
index.html
app.js
Firebase Hosting will serve the public/ folder.
Create dashboard/public/index.html with:
<!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>
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}`; }); }); });
Install Firebase CLI globally:
npm install -g firebase-tools
Log in and initialize hosting:
firebase login cd dashboard firebase init hosting
When prompted:
sparrow-lab project.publicy
This will create firebase.json and other files.
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.
secureClient.setInsecure(), embed the CA certificates for Google's HTTPS endpoints and call setCACert().admin vs guest)./devices/sparrow-01.You now have:
</code>