Updated for temp and humidity
This commit is contained in:
314
AirQuality.ino
314
AirQuality.ino
@@ -1,35 +1,86 @@
|
||||
#include <WiFi.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include "DFRobot_ENS160.h"
|
||||
#include "Adafruit_AHTX0.h"
|
||||
|
||||
// Wi-Fi credentials
|
||||
const char* ssid = "MegMan-WiFi";
|
||||
const char* password = "Zp5egGamPcENXr";
|
||||
const char* ssid = "HickmanWiFi";
|
||||
const char* password = "BlackBriar8787@@";
|
||||
|
||||
// I2C Communication setup
|
||||
DFRobot_ENS160_I2C ENS160(&Wire, 0x53); // Default I2C address is 0x53
|
||||
DFRobot_ENS160_I2C ENS160(&Wire, 0x53); // ENS160 I2C address
|
||||
Adafruit_AHTX0 aht; // AHTX0 instance
|
||||
|
||||
AsyncWebServer server(80);
|
||||
|
||||
String jsonData = "[]"; // JSON buffer to hold the air quality data
|
||||
uint8_t lastAQI = 0; // Store the last AQI value for display
|
||||
// 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);
|
||||
|
||||
// Initialise the sensor
|
||||
while (NO_ERR != ENS160.begin()) {
|
||||
Serial.println("Communication with ENS160 failed. Please check connections.");
|
||||
delay(3000);
|
||||
}
|
||||
Serial.println("ENS160 initialised successfully.");
|
||||
|
||||
// Set power mode
|
||||
ENS160.setPWRMode(ENS160_STANDARD_MODE);
|
||||
|
||||
// Set temperature and humidity compensation
|
||||
ENS160.setTempAndHum(25.0, 50.0); // Example values: 25°C, 50% humidity
|
||||
|
||||
// Connect to Wi-Fi
|
||||
WiFi.begin(ssid, password);
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
@@ -40,6 +91,30 @@ void setup() {
|
||||
Serial.print("ESP32 IP Address: ");
|
||||
Serial.println(WiFi.localIP());
|
||||
|
||||
// Sync time using NTP
|
||||
syncTime();
|
||||
|
||||
// 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(
|
||||
@@ -60,39 +135,33 @@ void setup() {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.card {
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #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: #ff4722;
|
||||
background-color: #ff5722;
|
||||
color: #ffffff;
|
||||
}
|
||||
.card.unhealthy {
|
||||
background-color: #de36f4;
|
||||
}
|
||||
.apexcharts-canvas {
|
||||
background: #1e1e1e;
|
||||
background-color: #f44336;
|
||||
color: #ffffff;
|
||||
}
|
||||
.apexcharts-tooltip {
|
||||
background: #333333 !important;
|
||||
color: #ffffff !important;
|
||||
border: 1px solid #333333 !important;
|
||||
}
|
||||
.dark-mode .card {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
.dark-mode .card-title, .dark-mode p {
|
||||
color: #bebcbc;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -103,7 +172,7 @@ void setup() {
|
||||
<div id="aqiCard" class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Air Quality Index (AQI)</h5>
|
||||
<h4 id="aqiValue">Value: Loading...</h4>
|
||||
<h4 id="aqiValue">Loading...</h4>
|
||||
<p id="aqiRecommendation">Loading recommendation...</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,8 +180,8 @@ void setup() {
|
||||
<div class="col-md-4">
|
||||
<div id="eco2Card" class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">eCO2 Recommendation</h5>
|
||||
<h4 id="eco2Value">Value: Loading...</h4>
|
||||
<h5 class="card-title">eCO2 (ppm)</h5>
|
||||
<h4 id="eco2Value">Loading...</h4>
|
||||
<p id="eco2Recommendation">Loading recommendation...</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,13 +189,31 @@ void setup() {
|
||||
<div class="col-md-4">
|
||||
<div id="tvocCard" class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">TVOC Recommendation</h5>
|
||||
<h4 id="tvocValue">Value: Loading...</h4>
|
||||
<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">
|
||||
@@ -143,37 +230,18 @@ void setup() {
|
||||
chart: {
|
||||
type: 'line',
|
||||
height: 350,
|
||||
animations: {
|
||||
enabled: true,
|
||||
easing: 'linear',
|
||||
dynamicAnimation: { speed: 1000 }
|
||||
},
|
||||
toolbar: { show: false },
|
||||
zoom: { enabled: false },
|
||||
animations: { enabled: true, easing: 'linear', dynamicAnimation: { speed: 1000 } },
|
||||
background: '#1e1e1e'
|
||||
},
|
||||
series: [
|
||||
{ name: 'TVOC (ppb)', data: [] },
|
||||
{ name: 'eCO2 (ppm)', data: [] }
|
||||
{ name: 'eCO2 (ppm)', data: [] },
|
||||
{ name: 'Temperature (°C)', data: [] },
|
||||
{ name: 'Humidity (%)', data: [] }
|
||||
],
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: { style: { colors: '#ffffff' } },
|
||||
range: 60000 // 1-minute range
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: { colors: '#ffffff' },
|
||||
formatter: function (val) { return Math.round(val); }
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
theme: 'dark'
|
||||
},
|
||||
grid: { borderColor: '#333333' },
|
||||
stroke: { curve: 'smooth' },
|
||||
dataLabels: { enabled: false },
|
||||
legend: { labels: { colors: '#ffffff' } }
|
||||
xaxis: { type: 'datetime', labels: { style: { colors: '#ffffff' } } },
|
||||
yaxis: { labels: { style: { colors: '#ffffff' } } },
|
||||
tooltip: { theme: 'dark' },
|
||||
};
|
||||
|
||||
let chart = new ApexCharts(document.querySelector("#chart"), chartOptions);
|
||||
@@ -183,33 +251,35 @@ void setup() {
|
||||
const response = await fetch('/data');
|
||||
const data = await response.json();
|
||||
|
||||
const tvocData = data.map(item => [item.timestamp, item.tvoc]);
|
||||
const eco2Data = data.map(item => [item.timestamp, item.eco2]);
|
||||
console.log(data.latest.humidity);
|
||||
|
||||
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));
|
||||
|
||||
chart.updateSeries([
|
||||
{ name: 'TVOC (ppb)', data: tvocData },
|
||||
{ name: 'eCO2 (ppm)', data: eco2Data }
|
||||
{ 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 }
|
||||
]);
|
||||
|
||||
const latest = data[data.length - 1];
|
||||
updateCard("aqiCard", latest.aqi, getAQIRecommendation(latest.aqi));
|
||||
updateCard("eco2Card", latest.eco2, getECO2Recommendation(latest.eco2));
|
||||
updateCard("tvocCard", latest.tvoc, getTVOCRecommendation(latest.tvoc));
|
||||
}
|
||||
|
||||
function updateCard(cardId, value, recommendation) {
|
||||
const card = document.getElementById(cardId);
|
||||
const valueText = card.querySelector(".card-body h4");
|
||||
const recText = card.querySelector(".card-body p");
|
||||
const valueText = card.querySelector(".card-body h4:nth-of-type(1)");
|
||||
card.className = `card text-center ${recommendation.class}`;
|
||||
valueText.textContent = `${value}`;
|
||||
valueText.textContent = value.toFixed(1);
|
||||
recText.textContent = recommendation.text;
|
||||
}
|
||||
|
||||
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, close to water sources." };
|
||||
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." };
|
||||
}
|
||||
@@ -229,46 +299,90 @@ void setup() {
|
||||
return { class: "excellent", text: "No effects on health." };
|
||||
}
|
||||
|
||||
setInterval(fetchData, 1000); // Update data every second
|
||||
function getTemperatureClass(temp) {
|
||||
if (temp < 18) return { class: "moderate", text: "Too cold, consider heating." };
|
||||
if (temp > 26) return { class: "poor", text: "Too hot, ventilate or cool down." };
|
||||
return { class: "good", text: "Comfortable temperature." };
|
||||
}
|
||||
|
||||
function getHumidityClass(hum) {
|
||||
if (hum < 30) return { class: "moderate", text: "Too dry, consider humidifying." };
|
||||
if (hum > 60) return { class: "poor", text: "Too humid, consider dehumidifying." };
|
||||
return { class: "good", text: "Comfortable humidity." };
|
||||
}
|
||||
|
||||
setInterval(fetchData, 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
)rawliteral";
|
||||
request->send(200, "text/html", html);
|
||||
});
|
||||
|
||||
// Route for sensor data (JSON)
|
||||
server.on("/data", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "application/json", jsonData);
|
||||
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);
|
||||
});
|
||||
|
||||
// Start the server
|
||||
server.begin();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
uint16_t TVOC = ENS160.getTVOC();
|
||||
uint16_t ECO2 = ENS160.getECO2();
|
||||
uint8_t AQI = ENS160.getAQI();
|
||||
// Periodically calibrate the ENS160 sensor
|
||||
calibrateENS160();
|
||||
|
||||
unsigned long timestamp = millis();
|
||||
String newEntry = "{\"timestamp\":" + String(timestamp) + ",\"tvoc\":" + String(TVOC) + ",\"eco2\":" + String(ECO2) + ",\"aqi\":" + String(AQI) + "}";
|
||||
|
||||
if (jsonData == "[]") {
|
||||
jsonData = "[" + newEntry + "]";
|
||||
} else {
|
||||
jsonData = jsonData.substring(0, jsonData.length() - 1) + "," + newEntry + "]";
|
||||
}
|
||||
|
||||
// Limit data buffer to 60 entries (last minute)
|
||||
int maxEntries = 60;
|
||||
int count = std::count(jsonData.begin(), jsonData.end(), '{');
|
||||
if (count > maxEntries) {
|
||||
int firstComma = jsonData.indexOf(',');
|
||||
jsonData = "[" + jsonData.substring(firstComma + 1);
|
||||
}
|
||||
|
||||
delay(1000); // Update every second
|
||||
// Delay for the main loop
|
||||
delay(15000); // Calibrate every 10 seconds
|
||||
}
|
||||
|
||||
388
AirQuality/AirQuality.ino
Normal file
388
AirQuality/AirQuality.ino
Normal file
@@ -0,0 +1,388 @@
|
||||
#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();
|
||||
|
||||
// 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;
|
||||
}
|
||||
.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: #ff5722;
|
||||
color: #ffffff;
|
||||
}
|
||||
.card.unhealthy {
|
||||
background-color: #f44336;
|
||||
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() {
|
||||
const response = await fetch('/data');
|
||||
const data = await response.json();
|
||||
|
||||
console.log(data.latest.humidity);
|
||||
|
||||
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));
|
||||
|
||||
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 }
|
||||
]);
|
||||
}
|
||||
|
||||
function updateCard(cardId, value, recommendation) {
|
||||
const card = document.getElementById(cardId);
|
||||
const valueText = card.querySelector(".card-body h4");
|
||||
const recText = card.querySelector(".card-body p");
|
||||
card.className = `card text-center ${recommendation.class}`;
|
||||
valueText.textContent = value.toFixed(1);
|
||||
recText.textContent = recommendation.text;
|
||||
}
|
||||
|
||||
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, consider heating." };
|
||||
if (temp > 26) return { class: "poor", text: "Too hot, ventilate or cool down." };
|
||||
return { class: "good", text: "Comfortable temperature." };
|
||||
}
|
||||
|
||||
function getHumidityClass(hum) {
|
||||
if (hum < 30) return { class: "moderate", text: "Too dry, consider humidifying." };
|
||||
if (hum > 60) return { class: "poor", text: "Too humid, consider dehumidifying." };
|
||||
return { class: "good", text: "Comfortable humidity." };
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user