This shows you the differences between two versions of the page.
|
iothings:laboratoare:2025_code:lab9_1 [2025/11/23 12:45] dan.tudose created |
iothings:laboratoare:2025_code:lab9_1 [2025/11/24 16:58] (current) dan.tudose |
||
|---|---|---|---|
| Line 1: | Line 1: | ||
| <code C main.cpp> | <code C 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"; | ||
| + | |||
| + | // 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"); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | bool waitForTime(uint32_t timeoutMs = 8000) { | ||
| + | unsigned long start = millis(); | ||
| + | while (!hasValidTime() && millis() - start < timeoutMs) { | ||
| + | syncTimeIfNeeded(); | ||
| + | delay(200); | ||
| + | } | ||
| + | return hasValidTime(); | ||
| + | } | ||
| + | |||
| + | // ====== 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(); | ||
| + | waitForTime(); | ||
| + | |||
| + | secureClient.setTimeout(15000); | ||
| + | secureClient.setInsecure(); // NOTE: uses HTTPS but skips cert validation | ||
| + | |||
| + | 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> | </code> | ||