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).
Edit src/main.cpp, get the code here.
Fill in your credentials:
apiKey and databaseURL.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_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:
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.