#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"; // Google GTS Root R1 (cross-signed by GlobalSign Root CA), seen in TLS chain for // identitytoolkit.googleapis.com and *.firebaseio.com (captured via openssl s_client) static const char GOOGLE_ROOT_CA[] PROGMEM = R"EOF( -----BEGIN CERTIFICATE----- MIIFYjCCBEqgAwIBAgIQd70NbNs2+RrqIQ/E8FjTDTANBgkqhkiG9w0BAQsFADBX MQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEQMA4GA1UE CxMHUm9vdCBDQTEbMBkGA1UEAxMSR2xvYmFsU2lnbiBSb290IENBMB4XDTIwMDYx OTAwMDA0MloXDTI4MDEyODAwMDA0MlowRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoT GUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFIx MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAthECix7joXebO9y/lD63 ladAPKH9gvl9MgaCcfb2jH/76Nu8ai6Xl6OMS/kr9rH5zoQdsfnFl97vufKj6bwS iV6nqlKr+CMny6SxnGPb15l+8Ape62im9MZaRw1NEDPjTrETo8gYbEvs/AmQ351k KSUjB6G00j0uYODP0gmHu81I8E3CwnqIiru6z1kZ1q+PsAewnjHxgsHA3y6mbWwZ DrXYfiYaRQM9sHmklCitD38m5agI/pboPGiUU+6DOogrFZYJsuB6jC511pzrp1Zk j5ZPaK49l8KEj8C8QMALXL32h7M1bKwYUH+E4EzNktMg6TO8UpmvMrUpsyUqtEj5 cuHKZPfmghCN6J3Cioj6OGaK/GP5Afl4/Xtcd/p2h/rs37EOeZVXtL0m79YB0esW CruOC7XFxYpVq9Os6pFLKcwZpDIlTirxZUTQAs6qzkm06p98g7BAe+dDq6dso499 iYH6TKX/1Y7DzkvgtdizjkXPdsDtQCv9Uw+wp9U7DbGKogPeMa3Md+pvez7W35Ei Eua++tgy/BBjFFFy3l3WFpO9KWgz7zpm7AeKJt8T11dleCfeXkkUAKIAf5qoIbap sZWwpbkNFhHax2xIPEDgfg1azVY80ZcFuctL7TlLnMQ/0lUTbiSw1nH69MG6zO0b 9f6BQdgAmD06yK56mDcYBZUCAwEAAaOCATgwggE0MA4GA1UdDwEB/wQEAwIBhjAP BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTkrysmcRorSCeFL1JmLO/wiRNxPjAf BgNVHSMEGDAWgBRge2YaRQ2XyolQL30EzTSo//z9SzBgBggrBgEFBQcBAQRUMFIw JQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnBraS5nb29nL2dzcjEwKQYIKwYBBQUH MAKGHWh0dHA6Ly9wa2kuZ29vZy9nc3IxL2dzcjEuY3J0MDIGA1UdHwQrMCkwJ6Al oCOGIWh0dHA6Ly9jcmwucGtpLmdvb2cvZ3NyMS9nc3IxLmNybDA7BgNVHSAENDAy MAgGBmeBDAECATAIBgZngQwBAgIwDQYLKwYBBAHWeQIFAwIwDQYLKwYBBAHWeQIF AwMwDQYJKoZIhvcNAQELBQADggEBADSkHrEoo9C0dhemMXoh6dFSPsjbdBZBiLg9 NR3t5P+T4Vxfq7vqfM/b5A3Ri1fyJm9bvhdGaJQ3b2t6yMAYN/olUazsaL+yyEn9 WprKASOshIArAoyZl+tJaox118fessmXn1hIVw41oeQa1v1vg4Fv74zPl6/AhSrw 9U5pCZEt4Wi4wStz6dTZ/CLANx8LZh1J7QJVj2fhMtfTJr9w4z30Z209fOU0iOMy +qduBmpvvYuR7hZL6Dupszfnw0Skfths18dG9ZKb59UhvmaSGZRVbNQpsg3BZlvi d0lIKO2d1xozclOzgjXPYovJJIultzkMu34qQb9Sz/yilrbCgj8= -----END CERTIFICATE----- )EOF"; // Hardware (adapt to your Sparrow board) #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.setCACert(GOOGLE_ROOT_CA); // validate HTTPS using Google root 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 }