#include #include #include #include #include #include #include #define SEALEVEL_HPA (1013.25f) // adjust for better altitude accuracy // ===== WiFi ===== const char* WIFI_SSID = "YOUR_SSID"; const char* WIFI_PASS = "YOUR_PASSWORD"; const char* MDNS_NAME = "sparrow"; // -> http://sparrow.local/ // ===== Web ===== WebServer server(80); // ===== BME680 ===== Adafruit_BME680 bme; bool haveBME = false; // ===== History buffer (5 minutes @ 1 Hz) ===== static const size_t HISTORY_MAX = 300; struct Sample { uint32_t t_ms; float tC; float hPct; float pHpa; float altM; }; Sample hist[HISTORY_MAX]; size_t histHead = 0; size_t histCount = 0; void pushSample(const Sample& s) { hist[histHead] = s; histHead = (histHead + 1) % HISTORY_MAX; if (histCount < HISTORY_MAX) histCount++; } Sample getSampleByAge(size_t iOldToNew) { size_t idx = (histHead + HISTORY_MAX - histCount + iOldToNew) % HISTORY_MAX; return hist[idx]; } // ===== Sensor init and reading ===== bool beginBME680() { Wire.begin(21, 22); // set custom SDA/SCL here if needed: Wire.begin(SDA, SCL); if (bme.begin(0x76, &Wire)) { Serial.println("[BME680] Found at 0x76"); return true; } if (bme.begin(0x77, &Wire)) { Serial.println("[BME680] Found at 0x77"); return true; } Serial.println("[BME680] Not found at 0x76/0x77"); return false; } void setupBME680() { bme.setTemperatureOversampling(BME680_OS_8X); bme.setHumidityOversampling(BME680_OS_2X); bme.setPressureOversampling(BME680_OS_4X); bme.setIIRFilterSize(BME680_FILTER_SIZE_3); bme.setGasHeater(320, 150); // optional } bool takeAndStoreSample() { if (!haveBME) return false; if (!bme.performReading()) return false; Sample s; s.t_ms = millis(); s.tC = bme.temperature; s.hPct = bme.humidity; s.pHpa = bme.pressure / 100.0f; s.altM = bme.readAltitude(SEALEVEL_HPA); pushSample(s); return true; } // ===== HTTP handlers ===== void handleRoot() { File f = SPIFFS.open("/index.html", "r"); if (!f) { server.send(500, "text/plain", "index.html missing"); return; } server.streamFile(f, "text/html"); f.close(); } void handleApiSensor() { if (histCount == 0) takeAndStoreSample(); Sample s = (histCount == 0) ? Sample{millis(), NAN, NAN, NAN, NAN} : getSampleByAge(histCount - 1); String json = "{"; json += "\"ok\":" + String(haveBME ? "true" : "false"); json += ",\"temperature_c\":" + String(isnan(s.tC)?0:s.tC, 2); json += ",\"humidity_pct\":" + String(isnan(s.hPct)?0:s.hPct, 1); json += ",\"pressure_hpa\":" + String(isnan(s.pHpa)?0:s.pHpa, 2); json += ",\"altitude_m\":" + String(isnan(s.altM)?0:s.altM, 1); json += "}"; server.send(200, "application/json", json); } void handleApiHistory() { uint32_t nowms = millis(); size_t n = histCount; if (n == 0) { server.send(200, "application/json", "{\"ok\":true,\"n\":0}"); return; } String json; json.reserve(n * 64 + 128); json += "{\"ok\":true,\"n\":" + String(n); // seconds ago array json += ",\"s\":["; for (size_t i = 0; i < n; ++i) { Sample s = getSampleByAge(i); uint32_t secAgo = (nowms - s.t_ms) / 1000; json += String(secAgo); if (i + 1 < n) json += ","; } json += "]"; auto appendSeries = [&](const char* key, float Sample::*field, int digits) { json += ",\""; json += key; json += "\":["; for (size_t i = 0; i < n; ++i) { Sample s = getSampleByAge(i); json += String(s.*field, digits); if (i + 1 < n) json += ","; } json += "]"; }; appendSeries("tc", &Sample::tC, 2); appendSeries("rh", &Sample::hPct, 1); appendSeries("p", &Sample::pHpa, 2); appendSeries("alt", &Sample::altM, 1); json += "}"; server.send(200, "application/json", json); } void handleApiStatus() { bool up = WiFi.status() == WL_CONNECTED; long rssi = up ? WiFi.RSSI() : -127; // dBm uint32_t uptimeS = millis() / 1000; String json = "{"; json += "\"ok\":true"; json += ",\"rssi_dbm\":" + String(rssi); json += ",\"uptime_s\":" + String(uptimeS); json += ",\"hostname\":\"" + String(MDNS_NAME) + "\""; json += ",\"ip\":\"" + (up ? WiFi.localIP().toString() : String("")) + "\""; json += "}"; server.send(200, "application/json", json); } void setup() { Serial.begin(115200); while (!Serial) { delay(10); } Serial.println("\n[BOOT] ESP32-C6 BME680 Web Graphs + Status + mDNS"); if (!SPIFFS.begin(true)) Serial.println("[SPIFFS] Mount failed"); else Serial.println("[SPIFFS] Mounted"); // WiFi + mDNS WiFi.mode(WIFI_STA); WiFi.setSleep(false); WiFi.setHostname(MDNS_NAME); WiFi.begin(WIFI_SSID, WIFI_PASS); Serial.printf("[WiFi] Connecting to %s", WIFI_SSID); unsigned long t0 = millis(); while (WiFi.status() != WL_CONNECTED && millis() - t0 < 20000) { Serial.print("."); delay(500); } Serial.println(); if (WiFi.status() == WL_CONNECTED) { Serial.printf("[WiFi] Connected. IP: %s\n", WiFi.localIP().toString().c_str()); if (MDNS.begin(MDNS_NAME)) { MDNS.addService("http", "tcp", 80); Serial.printf("[mDNS] http://%s.local/\n", MDNS_NAME); } else { Serial.println("[mDNS] start failed"); } } else { Serial.println("[WiFi] Failed to connect (continuing)"); } // Sensor haveBME = beginBME680(); if (haveBME) setupBME680(); // Prime buffer takeAndStoreSample(); // Routes server.on("/", HTTP_GET, handleRoot); server.on("/api/sensor", HTTP_GET, handleApiSensor); server.on("/api/history",HTTP_GET, handleApiHistory); server.on("/api/status", HTTP_GET, handleApiStatus); server.onNotFound([]() { String path = server.uri(); if (path == "/") { handleRoot(); return; } if (!SPIFFS.exists(path)) { server.send(404, "text/plain", "Not found"); return; } String ct = "text/plain"; if (path.endsWith(".html")) ct = "text/html"; else if (path.endsWith(".css")) ct = "text/css"; else if (path.endsWith(".js")) ct = "application/javascript"; File f = SPIFFS.open(path, "r"); server.streamFile(f, ct); f.close(); }); server.begin(); Serial.println("[HTTP] Server started"); if (WiFi.status() == WL_CONNECTED) Serial.println("[HTTP] Open: http://" + WiFi.localIP().toString() + "/ or http://sparrow.local/"); } void loop() { server.handleClient(); // 1 Hz sampling static uint32_t lastSample = 0; uint32_t now = millis(); if (now - lastSample >= 1000) { lastSample = now; takeAndStoreSample(); } delay(2); }