Files
ESP32_Air_Quality/AirQuality/AirQuality.ino
Hickmeister 0b2ec1b7a9 Updates
2025-01-20 23:45:58 +00:00

425 lines
13 KiB
C++

#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include "DFRobot_ENS160.h"
#include "Adafruit_AHTX0.h"
// Wi-Fi credentials
const char* ssid = "HickmanWiFi";
const char* password = "BlackBriar8787@@";
// I2C Communication setup
DFRobot_ENS160_I2C ENS160(&Wire, 0x53); // ENS160 I2C address
Adafruit_AHTX0 aht; // AHTX0 instance
AsyncWebServer server(80);
// Buffer to store historical data
struct SensorData {
time_t timestamp; // Use NTP time
uint16_t tvoc;
uint16_t eco2;
uint8_t aqi;
float temperature;
float humidity;
};
const int BUFFER_SIZE = 60; // Store 60 entries
SensorData sensorBuffer[BUFFER_SIZE];
int bufferIndex = 0;
// Function to sync time with NTP server
void syncTime() {
configTime(0, 0, "time.nist.gov", "pool.ntp.org");
Serial.println("Waiting for time synchronization...");
while (!time(nullptr)) {
delay(1000);
Serial.print(".");
}
Serial.println("\nTime synchronized!");
}
// Function to format the time into a human-readable string
String formatTime(time_t rawTime) {
char buffer[30];
struct tm* timeInfo = localtime(&rawTime);
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", timeInfo);
return String(buffer);
}
// Function to calibrate ENS160 using AHTX0 readings
void calibrateENS160() {
sensors_event_t tempEvent, humidityEvent;
aht.getEvent(&humidityEvent, &tempEvent);
float temperature = tempEvent.temperature;
float humidity = humidityEvent.relative_humidity;
ENS160.setTempAndHum(temperature, humidity);
Serial.printf("Calibrated ENS160 with Temp: %.1f°C, Hum: %.1f%%\n", temperature, humidity);
}
void prepopulateBuffer() {
sensors_event_t tempEvent, humidityEvent;
aht.getEvent(&humidityEvent, &tempEvent);
uint16_t TVOC = ENS160.getTVOC();
uint16_t ECO2 = ENS160.getECO2();
uint8_t AQI = ENS160.getAQI();
for (int i = 0; i < BUFFER_SIZE; i++) {
sensorBuffer[i] = {
time(nullptr),
TVOC,
ECO2,
AQI,
tempEvent.temperature,
humidityEvent.relative_humidity
};
}
bufferIndex = BUFFER_SIZE;
}
void setup() {
Serial.begin(115200);
// Connect to Wi-Fi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to Wi-Fi...");
}
Serial.println("Connected to Wi-Fi!");
Serial.print("ESP32 IP Address: ");
Serial.println(WiFi.localIP());
// Sync time using NTP
syncTime();
delay(1000);
// Initialize AHTX0 sensor
if (!aht.begin()) {
Serial.println("AHTX0 initialization failed. Check the connection!");
while (1) delay(1000);
}
Serial.println("AHTX0 initialized successfully.");
// Initialize ENS160 sensor
while (NO_ERR != ENS160.begin()) {
Serial.println("Communication with ENS160 failed. Please check connections.");
delay(3000);
}
Serial.println("ENS160 initialized successfully.");
ENS160.setPWRMode(ENS160_STANDARD_MODE);
// Calibrate ENS160 with AHTX0 readings
calibrateENS160();
// Prepopulate the buffer with initial readings
prepopulateBuffer();
// Route for the main webpage
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
String html = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Air Quality Monitoring</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
<style>
body {
background-color: #121212;
color: #f4f4f4;
}
.container {
margin-top: 20px;
}
.card {
border: 1px solid #333333;
background-color: #333333;
}
.card.excellent {
background-color: #4CAF50;
color: #ffffff;
}
.card.good {
background-color: #8BC34A;
color: #ffffff;
}
.card.moderate {
background-color: #ffa007;
color: #000000;
}
.card.poor {
background-color:rgb(255, 60, 34);
color: #ffffff;
}
.card.unhealthy {
background-color:rgb(244, 54, 244);
color: #ffffff;
}
.apexcharts-tooltip {
background: #333333 !important;
color: #ffffff !important;
border: 1px solid #333333 !important;
}
</style>
</head>
<body>
<div class="container">
<h1 class="text-center">Air Quality Monitoring</h1>
<div class="row mt-4">
<div class="col-md-4">
<div id="aqiCard" class="card text-center">
<div class="card-body">
<h5 class="card-title">Air Quality Index (AQI)</h5>
<h4 id="aqiValue">Loading...</h4>
<p id="aqiRecommendation">Loading recommendation...</p>
</div>
</div>
</div>
<div class="col-md-4">
<div id="eco2Card" class="card text-center">
<div class="card-body">
<h5 class="card-title">eCO2 (ppm)</h5>
<h4 id="eco2Value">Loading...</h4>
<p id="eco2Recommendation">Loading recommendation...</p>
</div>
</div>
</div>
<div class="col-md-4">
<div id="tvocCard" class="card text-center">
<div class="card-body">
<h5 class="card-title">TVOC (ppb)</h5>
<h4 id="tvocValue">Loading...</h4>
<p id="tvocRecommendation">Loading recommendation...</p>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-6">
<div id="temperatureCard" class="card text-center">
<div class="card-body">
<h5 class="card-title">Temperature (°C)</h5>
<h4 id="temperatureValue">Loading...</h4>
</div>
</div>
</div>
<div class="col-md-6">
<div id="humidityCard" class="card text-center">
<div class="card-body">
<h5 class="card-title">Humidity (%)</h5>
<h4 id="humidityValue">Loading...</h4>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<div class="card dark-mode">
<div class="card-body">
<h5 class="card-title">Real-Time Air Quality Graph</h5>
<div id="chart"></div>
</div>
</div>
</div>
</div>
</div>
<script>
let chartOptions = {
chart: {
type: 'line',
height: 350,
animations: { enabled: true, easing: 'linear', dynamicAnimation: { speed: 1000 } },
background: '#1e1e1e'
},
series: [
{ name: 'TVOC (ppb)', data: [] },
{ name: 'eCO2 (ppm)', data: [] },
{ name: 'Temperature (°C)', data: [] },
{ name: 'Humidity (%)', data: [] }
],
xaxis: { type: 'datetime', labels: { style: { colors: '#ffffff' } } },
yaxis: { labels: { style: { colors: '#ffffff' } } },
tooltip: { theme: 'dark' }
};
let chart = new ApexCharts(document.querySelector("#chart"), chartOptions);
chart.render();
async function fetchData() {
try {
const response = await fetch('/data');
if (!response.ok) {
console.error(`Failed to fetch data: ${response.statusText}`);
return;
}
const data = await response.json();
console.log("Fetched data:", data);
// Update cards
updateCard("aqiCard", data.latest.aqi, getAQIRecommendation(data.latest.aqi));
updateCard("eco2Card", data.latest.eco2, getECO2Recommendation(data.latest.eco2));
updateCard("tvocCard", data.latest.tvoc, getTVOCRecommendation(data.latest.tvoc));
updateCard("temperatureCard", data.latest.temperature, getTemperatureClass(data.latest.temperature));
updateCard("humidityCard", data.latest.humidity, getHumidityClass(data.latest.humidity));
// Update chart
chart.updateSeries([
{ name: 'TVOC (ppb)', data: data.history.tvoc },
{ name: 'eCO2 (ppm)', data: data.history.eco2 },
{ name: 'Temperature (°C)', data: data.history.temperature },
{ name: 'Humidity (%)', data: data.history.humidity }
]);
} catch (error) {
console.error("Error fetching data:", error);
}
}
function updateCard(cardId, value, recommendation) {
const card = document.getElementById(cardId);
if (!card) {
console.error(`Card with ID ${cardId} not found.`);
return;
}
const valueText = card.querySelector(".card-body h4");
const recText = card.querySelector(".card-body p");
if (valueText) {
valueText.textContent = value.toFixed(1);
} else {
console.error(`Value text element not found in ${cardId}`);
}
// Apply color class based on recommendation
if (recommendation && recommendation.class) {
card.className = `card text-center ${recommendation.class}`;
} else {
card.className = "card text-center"; // Default class if no recommendation
}
// Update recommendation text if available
if (recText && recommendation && recommendation.text) {
recText.textContent = recommendation.text;
} else if (recText) {
recText.textContent = ""; // Clear text if no recommendation
}
}
function getAQIRecommendation(aqi) {
if (aqi === 1) return { class: "excellent", text: "Suitable for long-term living." };
if (aqi === 2) return { class: "good", text: "Maintain adequate ventilation." };
if (aqi === 3) return { class: "moderate", text: "Strengthen ventilation." };
if (aqi === 4) return { class: "poor", text: "Find pollution sources, ventilate more." };
return { class: "unhealthy", text: "Avoid staying long; ventilate." };
}
function getECO2Recommendation(eco2) {
if (eco2 > 1500) return { class: "unhealthy", text: "Serious pollution, ventilate!" };
if (eco2 > 1000) return { class: "poor", text: "Polluted air, ventilation recommended." };
if (eco2 > 800) return { class: "moderate", text: "Consider ventilation." };
if (eco2 > 600) return { class: "good", text: "Air quality is fine." };
return { class: "excellent", text: "Air quality is excellent." };
}
function getTVOCRecommendation(tvoc) {
if (tvoc > 6000) return { class: "unhealthy", text: "Headaches and nerve issues possible." };
if (tvoc > 750) return { class: "poor", text: "May cause headaches, ventilate." };
if (tvoc > 50) return { class: "moderate", text: "Some discomfort possible." };
return { class: "excellent", text: "No effects on health." };
}
function getTemperatureClass(temp) {
if (temp < 18) return { class: "moderate", text: "" }; // Too cold
if (temp > 26) return { class: "poor", text: "" }; // Too hot
return { class: "good", text: "" }; // Comfortable
}
function getHumidityClass(hum) {
if (hum < 30) return { class: "moderate", text: "" }; // Too dry
if (hum > 60) return { class: "poor", text: "" }; // Too humid
return { class: "good", text: "" }; // Comfortable
}
setInterval(fetchData, 1000);
</script>
</body>
</html>
)rawliteral";
request->send(200, "text/html", html);
});
server.on("/data", HTTP_GET, [](AsyncWebServerRequest *request) {
uint16_t TVOC = ENS160.getTVOC();
uint16_t ECO2 = ENS160.getECO2();
uint8_t AQI = ENS160.getAQI();
sensors_event_t tempEvent, humidityEvent;
aht.getEvent(&humidityEvent, &tempEvent);
sensorBuffer[bufferIndex] = {
time(nullptr), // Use current NTP time
TVOC,
ECO2,
AQI,
tempEvent.temperature,
humidityEvent.relative_humidity
};
bufferIndex = (bufferIndex + 1) % BUFFER_SIZE;
String json = "{\"latest\":{";
json += "\"tvoc\":" + String(TVOC) + ",";
json += "\"eco2\":" + String(ECO2) + ",";
json += "\"aqi\":" + String(AQI) + ",";
json += "\"temperature\":" + String(tempEvent.temperature) + ",";
json += "\"humidity\":" + String(humidityEvent.relative_humidity);
json += "},\"history\":{";
json += "\"tvoc\":[";
for (int i = 0; i < BUFFER_SIZE; i++) {
int index = (bufferIndex + i) % BUFFER_SIZE;
if (sensorBuffer[index].timestamp == 0) continue;
if (i > 0) json += ",";
json += "[\"" + formatTime(sensorBuffer[index].timestamp) + "\"," + String(sensorBuffer[index].tvoc) + "]";
}
json += "],\"eco2\":[";
for (int i = 0; i < BUFFER_SIZE; i++) {
int index = (bufferIndex + i) % BUFFER_SIZE;
if (sensorBuffer[index].timestamp == 0) continue;
if (i > 0) json += ",";
json += "[\"" + formatTime(sensorBuffer[index].timestamp) + "\"," + String(sensorBuffer[index].eco2) + "]";
}
json += "],\"temperature\":[";
for (int i = 0; i < BUFFER_SIZE; i++) {
int index = (bufferIndex + i) % BUFFER_SIZE;
if (sensorBuffer[index].timestamp == 0) continue;
if (i > 0) json += ",";
json += "[\"" + formatTime(sensorBuffer[index].timestamp) + "\"," + String(sensorBuffer[index].temperature) + "]";
}
json += "],\"humidity\":[";
for (int i = 0; i < BUFFER_SIZE; i++) {
int index = (bufferIndex + i) % BUFFER_SIZE;
if (sensorBuffer[index].timestamp == 0) continue;
if (i > 0) json += ",";
json += "[\"" + formatTime(sensorBuffer[index].timestamp) + "\"," + String(sensorBuffer[index].humidity) + "]";
}
json += "]}}";
request->send(200, "application/json", json);
});
server.begin();
}
void loop() {
// Periodically calibrate the ENS160 sensor
calibrateENS160();
// Delay for the main loop
delay(15000); // Calibrate every 10 seconds
}