Files
espdht/esp32c3-dht-mq-wifi.ino
T
2026-08-13 20:58:07 +00:00

399 lines
12 KiB
Arduino
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <WiFi.h>
#include <WebServer.h>
#include <DHT.h>
// ----------- WiFi -----------
static const char* WIFI_SSID = "SSID";
static const char* WIFI_PASS = "PASS";
// ----------- DHT -----------
static const int DHT_PIN = 10; // GPIO for DHT DATA
static const int DHT_TYPE = DHT22; // DHT11 / DHT22 / DHT12?
DHT dht(DHT_PIN, DHT_TYPE);
// ----------- MQ-135 -----------
static const int MQ_PIN = A2; // Analog pin for MQ-135 (GPIO2 on ESP32-C3)
static const int MQ_ADC_CALIBRATION = 1900; // ADC baseline in clean air - ADJUST THIS
static const float MQ_AQ_MIN = 0; // Min AQ value
static const float MQ_AQ_MAX = 500; // Max AQ value (standard AQI scale)
static const float MQ_ADC_MAX = 4095.0; // ESP32-C3 12-bit ADC max
// ----------- Server -----------
WebServer server(80);
// ----------- Reading interval -----------
static const unsigned long READ_MS = 2000;
unsigned long lastRead = 0;
// ----------- Latest values -----------
float latestH = NAN;
float latestT_F = NAN;
float latestAQ = NAN;
// ----------- MQ-135 helper functions -----------
// Returns raw ADC average (multiple reads for stability)
int readMQRaw() {
long sum = 0;
const int samples = 16;
for (int i = 0; i < samples; i++) {
sum += analogRead(MQ_PIN);
delay(2);
}
return sum / samples;
}
// Maps raw ADC to a continuous AQ value (0500 scale)
// Higher ADC = more gas = worse air quality
float readAirQuality() {
int adc = readMQRaw();
// Map ADC range to AQ scale linearly.
// MQ_ADC_CALIBRATION = your baseline in clean air.
// Anything above that degrades air quality proportionally.
//
// Tune MQ_ADC_CALIBRATION by noting the ADC value in fresh air,
// then setting it slightly below that number.
int adcMin = MQ_ADC_CALIBRATION; // clean air baseline
int adcMax = 4095; // sensor saturated
// Linear interpolation between baseline (AQ=0) and saturation (AQ=500)
float aq;
if (adc <= adcMin) {
aq = 0;
} else {
aq = ((float)(adc - adcMin) / (float)(adcMax - adcMin)) * 500.0;
}
aq = constrain(aq, 0.0, 500.0);
return aq;
}
String htmlPage() {
return R"HTML(
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>ESP32 DHT+MQ</title>
<style>
body { font-family: system-ui, Arial; margin: 2rem; }
.card { padding: 1rem 1.2rem; border: 1px solid #ddd; border-radius: 12px; max-width: 800px; }
.val { font-size: 2rem; margin: 0.2rem 0; }
.small { color: #555; font-size: 0.9rem; }
.row { display: flex; gap: 2rem; flex-wrap: wrap; }
.box { flex: 1; min-width: 220px; }
canvas { width: 100%; height: 80%; background: #d3d3d3; border: 1px solid #eee; border-radius: 10px; }
.legend { display: flex; gap: 1rem; align-items: center; margin-top: 0.5rem; flex-wrap: wrap; }
.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 0.35rem; }
.btnbar { margin-top: 0.8rem; display: flex; gap: 0.6rem; flex-wrap: wrap; }
button { padding: 0.5rem 0.8rem; border-radius: 10px; border: 1px solid #ddd; background: #fff; cursor: pointer; }
.aq-badge { padding: 0.4rem 0.8rem; border-radius: 8px; font-weight: bold; font-size: 0.9rem; }
</style>
</head>
<body>
<div class="card">
<h2>ESP32 DHT+MQ</h2>
<div class="small">Updates every 2 seconds • Last ~43,200 Samples Max • ~24 Hours</div>
<div class="row">
<div class="box">
<p><b>Temperature:</b></p>
<p class="val" id="temp">--</p>
</div>
<div class="box">
<p><b>Humidity:</b></p>
<p class="val" id="hum">--</p>
</div>
<div class="box">
<p><b>AQI:</b></p>
<p class="val" id="aq">--</p>
<p id="aqLabel" class="small">--</p>
</div>
</div>
<canvas id="chart" width="800" height="600"></canvas>
<div class="legend">
<div><span class="dot" style="background:#e53935"></span>Temp (°F)</div>
<div><span class="dot" style="background:#1e88e5"></span>Humidity (%)</div>
<div><span class="dot" style="background:#43a047"></span>Air Quality</div>
</div>
<div class="btnbar">
<button onclick="clearData()">Clear chart</button>
</div>
</div>
<script>
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
// Keep separate series; chart shares the x-axis (time)
const MAX_POINTS = 43200;
let tempSeries = []; // {x, v}
let humSeries = []; // {x, v}
let aqSeries = []; // {x, v}
function resizeForDPR() {
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // draw in CSS pixels
}
window.addEventListener('resize', resizeForDPR);
resizeForDPR();
function getAQLabel(aq) {
if (aq === null || isNaN(aq)) return "--";
if (aq <= 50) return "Excellent";
if (aq <= 100) return "Good";
if (aq <= 150) return "Moderate";
if (aq <= 250) return "Unhealthy for Sensitive";
if (aq <= 350) return "Unhealthy";
return "Hazardous";
}
function getAQColor(aq) {
if (aq === null || isNaN(aq)) return '#43a047';
if (aq <= 50) return '#43a047'; // Green
if (aq <= 100) return '#7cb342'; // Light green
if (aq <= 150) return '#fdd835'; // Yellow
if (aq <= 250) return '#ffb300'; // Orange
if (aq <= 350) return '#fb8c00'; // Dark orange
return '#e53935'; // Red
}
function draw() {
const w = canvas.getBoundingClientRect().width;
const h = canvas.getBoundingClientRect().height;
ctx.clearRect(0, 0, w, h);
// Background grid
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, w, h);
ctx.strokeStyle = '#eee';
ctx.lineWidth = 1;
const gridLines = 5;
for (let i = 1; i < gridLines; i++) {
const y = (h * i) / gridLines;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(w, y);
ctx.stroke();
}
// If no data, stop
if (tempSeries.length === 0 && humSeries.length === 0 && aqSeries.length === 0) return;
// Compute ranges (ignore missing nulls)
const allTemp = tempSeries.map(p => p.v).filter(v => Number.isFinite(v));
const allHum = humSeries.map(p => p.v).filter(v => Number.isFinite(v));
const allAQ = aqSeries.map(p => p.v).filter(v => Number.isFinite(v));
const tempMin = allTemp.length ? Math.min(...allTemp) : 60;
const tempMax = allTemp.length ? Math.max(...allTemp) : 100;
const humMin = allHum.length ? Math.min(...allHum) : 0;
const humMax = allHum.length ? Math.max(...allHum) : 100;
const aqMin = 0;
const aqMax = 500;
// Avoid flat-line divisions
const tempSpan = (tempMax - tempMin) || 1;
const humSpan = (humMax - humMin) || 1;
const aqSpan = (aqMax - aqMin) || 1;
// x scale: index across MAX_POINTS window
const count = Math.max(tempSeries.length, humSeries.length, aqSeries.length);
const xForIndex = (i) => {
if (count <= 1) return 0;
return (w * i) / (count - 1);
};
function plot(series, color, min, span) {
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i < series.length; i++) {
const v = series[i].v;
if (!Number.isFinite(v)) continue;
const x = xForIndex(i);
const y = h - ((v - min) / span) * (h - 20) - 10; // padding
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
}
// Draw both series
plot(tempSeries, '#e53935', tempMin, tempSpan);
plot(humSeries, '#1e88e5', humMin, humSpan);
// AQ uses dynamic color based on current/latest value
const latestAQVal = aqSeries.length > 0 ? aqSeries[aqSeries.length - 1].v : 50;
plot(aqSeries, getAQColor(latestAQVal), aqMin, aqSpan);
// Labels (min/max)
ctx.fillStyle = '#333';
ctx.font = '12px system-ui, Arial';
ctx.fillText(`Temp range: ${tempMin.toFixed(1)}${tempMax.toFixed(1)} °F`, 10, 16);
ctx.fillText(`Hum range: ${humMin.toFixed(1)}${humMax.toFixed(1)} %`, 10, 32);
if (allAQ.length > 0) {
ctx.fillText(`AQ range: ${Math.min(...allAQ.filter(v => isFinite(v))).toFixed(0)}${Math.max(...allAQ.filter(v => isFinite(v))).toFixed(0)}`, 10, 48);
}
}
function pushPoint(series, value) {
if (Number.isFinite(value)) {
series.push({x: Date.now(), v: value});
while (series.length > MAX_POINTS) series.shift();
}
}
function clearData() {
tempSeries = [];
humSeries = [];
aqSeries = [];
draw();
}
async function refresh() {
try {
const res = await fetch('/data?nocache=' + Date.now());
const j = await res.json();
// Update numbers
if (j.t_f === null) document.getElementById('temp').textContent = '--';
else document.getElementById('temp').textContent = j.t_f.toFixed(1) + ' °F';
if (j.h === null) document.getElementById('hum').textContent = '--';
else document.getElementById('hum').textContent = j.h.toFixed(1) + ' %';
if (j.aq === null) {
document.getElementById('aq').textContent = '--';
document.getElementById('aqLabel').textContent = '--';
} else {
document.getElementById('aq').textContent = Math.round(j.aq);
document.getElementById('aqLabel').textContent = getAQLabel(j.aq);
document.getElementById('aqLabel').className = 'small aq-badge';
document.getElementById('aqLabel').style.backgroundColor = getAQColor(j.aq) + '40';
document.getElementById('aqLabel').style.color = j.aq > 250 ? '#fff' : '#333';
}
// Update series + chart
if (j.t_f !== null) tempSeries.push({x: Date.now(), v: j.t_f});
if (j.h !== null) humSeries.push({x: Date.now(), v: j.h});
if (j.aq !== null) aqSeries.push({x: Date.now(), v: j.aq});
while (tempSeries.length > MAX_POINTS) tempSeries.shift();
while (humSeries.length > MAX_POINTS) humSeries.shift();
while (aqSeries.length > MAX_POINTS) aqSeries.shift();
draw();
} catch (e) {
// ignore fetch errors
}
}
refresh();
setInterval(refresh, 2000);
</script>
</body>
</html>
)HTML";
}
void handleRoot() {
server.send(200, "text/html", htmlPage());
}
void handleData() {
// Convert NaN to null for easy JSON handling
float h = latestH;
float t = latestT_F;
float aq = latestAQ;
String json = "{";
json += "\"h\": " + (isnan(h) ? String("null") : String(h, 1)) + ",";
json += "\"t_f\": " + (isnan(t) ? String("null") : String(t, 1)) + ",";
json += "\"aq\": " + (isnan(aq) ? String("null") : String(aq, 1));
json += "}";
server.send(200, "application/json", json);
}
void setup() {
Serial.begin(115200);
delay(200);
Serial.println("ESP32-C3 DHT11 + MQ-135 starting...");
dht.begin();
// Configure MQ-135 pin as analog input
analogReadResolution(12); // 12-bit ADC (0-4095)
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(300);
Serial.print(".");
}
Serial.println();
Serial.print("Connected. IP address: ");
Serial.println(WiFi.localIP());
server.on("/", handleRoot);
server.on("/data", handleData);
server.begin();
Serial.println("HTTP server started.");
}
void loop() {
// keep server responsive
server.handleClient();
unsigned long now = millis();
if (now - lastRead < READ_MS) return;
lastRead = now;
// Read DHT sensor
float h = dht.readHumidity();
float tC = dht.readTemperature(); // Celsius
// Read MQ-135
float aq = readAirQuality();
if (isnan(h) || isnan(tC)) {
Serial.println("DHT read failed (check wiring/pull-up).");
latestH = NAN;
latestT_F = NAN;
} else {
latestH = h;
latestT_F = dht.convertCtoF(tC); // Already in F
}
latestAQ = aq;
Serial.print("Humidity: ");
Serial.print(latestH, 1);
Serial.print(" %\tTemp: ");
Serial.print(latestT_F, 1);
Serial.print(" F\tAQI: ");
Serial.print(latestAQ, 1);
Serial.println();
Serial.print("Connected. IP address: ");
Serial.println(WiFi.localIP());
}