Differences

This shows you the differences between two versions of the page.

Link to this comparison view

iothings:laboratoare:2025:lab9 [2025/11/23 12:20]
dan.tudose [6. Next Steps & Improvements]
iothings:laboratoare:2025:lab9 [2025/11/23 13:43] (current)
dan.tudose [3. ESP32-C6 Sparrow Firmware (HTTPS + BME680 + NeoPixel)]
Line 174: Line 174:
 ---- ----
  
-===== 3. ESP32-C6 Firmware (HTTPS + BME680 + NeoPixel) ===== +===== 3. ESP32-C6 ​Sparrow ​Firmware (HTTPS + BME680 + NeoPixel) =====
- +
-==== 3.1 Overview ​====+
  
 On boot, the firmware will: On boot, the firmware will:
Line 191: Line 189:
  
   * ''​WiFiClientSecure''​ for HTTPS.   * ''​WiFiClientSecure''​ for HTTPS.
-  * For simplicity we will call ''​setInsecure()''​ (it still uses TLS but skips certificate ​validation).  ​ +  * We have embedded a proper CA certificate. 
-    ​*For productionembed proper CA certificates.*+  ​* ​The certificate is the full Google TLS root (GTS Root R1cross-signed by GlobalSign) captured from a live identitytoolkit.googleapis.com chain and reverted to setCACert using that PEM
  
-==== 3.2 Configure Pins and Credentials ==== 
  
 +Edit ''​src/​main.cpp'',​ get the code [[iothings:​laboratoare:​2025_code:​lab9_1|here]].
 Fill in your credentials:​ Fill in your credentials:​
- 
   * Wi-Fi SSID and password.   * Wi-Fi SSID and password.
   * Firebase ''​apiKey''​ and ''​databaseURL''​.   * Firebase ''​apiKey''​ and ''​databaseURL''​.
Line 203: Line 200:
   * Device ID: ''​sparrow-01''​.   * Device ID: ''​sparrow-01''​.
  
-==== 3.3 Full main.cpp Example ==== 
  
-Create or edit ''​src/​main.cpp'':​ +  1. Build and upload ​the firmware
- +  2. In the serial monitorverify:
-<code 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 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 +
-+
-</​code>​ +
- +
-==== 3.Build, Upload, ​and Test ==== +
- +
-  1. Connect ​the Sparrow using USB+
-  2. In pioarduinobuild and upload the firmware. +
-  3. Open the **Serial Monitor** at 115200 baud. +
-  4. Verify:+
      * Wi-Fi connects.      * Wi-Fi connects.
      * Firebase sign-in succeeds.      * Firebase sign-in succeeds.
      * POST/GET requests show HTTP 200.      * POST/GET requests show HTTP 200.
-  ​5. In Firebase console → Realtime Database, you should see data appearing under:+  ​3. In Firebase console → Realtime Database, you should see data appearing under:
      * ''/​devices/​sparrow-01/​telemetry''​      * ''/​devices/​sparrow-01/​telemetry''​
  
 If you see sign-in or HTTPS errors, double-check:​ If you see sign-in or HTTPS errors, double-check:​
- 
   * ''​FIREBASE_API_KEY''​   * ''​FIREBASE_API_KEY''​
   * ''​FIREBASE_DB_URL''​   * ''​FIREBASE_DB_URL''​
Line 585: Line 215:
   * Wi-Fi credentials.   * Wi-Fi credentials.
  
-----+
  
 ===== 4. Web Dashboard (Password-Protected) ===== ===== 4. Web Dashboard (Password-Protected) =====
Line 610: Line 240:
     index.html     index.html
     app.js     app.js
 +    chart.umd.min.js
 </​code>​ </​code>​
  
Line 616: Line 247:
 ==== 4.2 index.html ==== ==== 4.2 index.html ====
  
-Create ''​dashboard/​public/​index.html''​ with:+Create ''​dashboard/​public/​index.html''​ with the code from [[iothings:laboratoare:​2025_code:​lab9_2|here]].
  
-<code html 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>​ 
- 
-</​code>​ 
  
 ==== 4.3 app.js ==== ==== 4.3 app.js ====
  
-Create ''​dashboard/​public/​app.js''​ and paste this.  ​ +Create ''​dashboard/​public/​app.js''​ and paste [[iothings:​laboratoare:​2025_code:​lab9_3|this]].  ​
-**Replace** the ''​firebaseConfig''​ values with those from your Firebase console:+
  
-<code javascript>​ +**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}`;​ +
-      }); +
-  }); +
-}); +
- +
-</​code>​+
  
 +==== 4.4 chart.umd.min.js ====
  
 +Create ''​dashboard/​public/​chart.umd.min.js''​. You need this for rendering the charts locally. ​
  
 +The code is [[iothings:​laboratoare:​2025_code:​lab9_4|here]].
 ===== 5. Deploy Dashboard with Firebase Hosting ===== ===== 5. Deploy Dashboard with Firebase Hosting =====
  
iothings/laboratoare/2025/lab9.1763893215.txt.gz · Last modified: 2025/11/23 12:20 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