#include <DHT.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Adafruit_ILI9341.h>
#include <Adafruit_GFX.h>
#define DHTPIN 4
#define DHTTYPE DHT22
#define TGS2600_PIN 32
#define TGS2602_PIN 33
#define MICS_RED 34
#define MICS_OX 35
#define TFT_CS 15
#define TFT_RST 2
#define TFT_DC 0
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
const char* ssid = "BreathAnalyzer";
const char* password = "breath123";
DHT dht(DHTPIN, DHTTYPE);
WebServer server(80);
float temperatura = 0, umiditate = 0;
float tvoc = 0, eco2 = 0;
float rs2600 = 0, rs2602 = 0;
float vRED = 0, vOX = 0;
float h2_ppm = 0, nh3_ppm = 0;
float r0_2600 = 15000.0;
float r0_2602 = 30000.0;
float rsToPpm2600(float rs) {
if (rs <= 0) return 0;
return 10.0 * pow(rs / r0_2600, -1.5);
}
float rsToPpm2602(float rs) {
if (rs <= 0) return 0;
return 5.0 * pow(rs / r0_2602, -1.2);
}
void drawTFT() {
tft.fillScreen(ILI9341_BLACK);
// Titlu
tft.setTextColor(ILI9341_CYAN);
tft.setTextSize(2);
tft.setCursor(10, 5);
tft.print("BreathAnalyzer");
tft.drawFastHLine(0, 25, 240, ILI9341_CYAN);
// Temp + Hum
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(5, 30);
tft.print("Temp:");
tft.setTextColor(ILI9341_GREEN);
tft.print(temperatura, 1);
tft.print("C");
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(130, 30);
tft.print("Hum:");
tft.setTextColor(ILI9341_GREEN);
tft.print(umiditate, 1);
tft.print("%");
tft.drawFastHLine(0, 42, 240, ILI9341_DARKGREY);
// H2
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(5, 46);
tft.print("H2 (TGS2600):");
tft.setTextColor(ILI9341_DARKGREY);
tft.setCursor(155, 46);
tft.print("prag: 20ppm");
tft.setTextColor(h2_ppm > 20 ? ILI9341_RED : ILI9341_GREEN);
tft.setTextSize(2);
tft.setCursor(5, 57);
tft.print(h2_ppm, 1);
tft.print(" ppm");
tft.drawFastHLine(0, 80, 240, ILI9341_DARKGREY);
// NH3
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(5, 84);
tft.print("NH3 (TGS2602):");
tft.setTextColor(ILI9341_DARKGREY);
tft.setCursor(155, 84);
tft.print("prag: 2ppm");
tft.setTextColor(nh3_ppm > 2 ? ILI9341_RED : ILI9341_GREEN);
tft.setTextSize(2);
tft.setCursor(5, 95);
tft.print(nh3_ppm, 1);
tft.print(" ppm");
tft.drawFastHLine(0, 118, 240, ILI9341_DARKGREY);
// TVOC
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(5, 122);
tft.print("TVOC:");
tft.setTextColor(tvoc > 500 ? ILI9341_RED : ILI9341_GREEN);
tft.setTextSize(2);
tft.setCursor(5, 132);
tft.print(tvoc, 0);
tft.print(" ppb");
tft.drawFastHLine(0, 155, 240, ILI9341_DARKGREY);
// CO2
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(5, 159);
tft.print("CO2:");
tft.setTextColor(ILI9341_CYAN);
tft.setTextSize(2);
tft.setCursor(5, 169);
tft.print(eco2, 0);
tft.print(" ppm");
tft.drawFastHLine(0, 192, 240, ILI9341_DARKGREY);
// MICS
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(5, 196);
tft.print("MICS RED:");
tft.setTextColor(ILI9341_ORANGE);
tft.setTextSize(2);
tft.setCursor(5, 206);
tft.print(vRED, 2);
tft.print("V");
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(1);
tft.setCursor(130, 196);
tft.print("MICS OX:");
tft.setTextColor(ILI9341_YELLOW);
tft.setTextSize(2);
tft.setCursor(130, 206);
tft.print(vOX, 2);
tft.print("V");
tft.drawFastHLine(0, 230, 240, ILI9341_DARKGREY);
// Alerta
tft.fillRect(0, 233, 240, 35, h2_ppm > 20 || nh3_ppm > 2 || tvoc > 500 ? ILI9341_RED : 0x0720);
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(2);
tft.setCursor(20, 243);
tft.print(h2_ppm > 20 || nh3_ppm > 2 || tvoc > 500 ? "PRAG DEPASIT!" : " Valori normale");
// WiFi
tft.drawFastHLine(0, 270, 240, ILI9341_DARKGREY);
tft.setTextSize(1);
tft.setTextColor(ILI9341_DARKGREY);
tft.setCursor(5, 275);
tft.print("WiFi:BreathAnalyzer 192.168.4.1");
}
void handleRoot() {
String html = R"rawhtml(
<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BreathAnalyzer</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: Arial, sans-serif; background: #0d1117; color: #e6edf3; padding: 16px; }
h1 { text-align: center; color: #58a6ff; margin-bottom: 4px; font-size: 1.5em; }
.subtitle { text-align: center; color: #8b949e; font-size: 0.8em; margin-bottom: 16px; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px; }
.card { background: #161b22; border-radius: 10px; padding: 14px; border: 1px solid #30363d; }
.card-title { font-size: 0.75em; color: #8b949e; margin-bottom: 4px; text-transform: uppercase; }
.card-val { font-size: 1.8em; font-weight: bold; }
.card-unit { font-size: 0.8em; color: #8b949e; }
.green { color: #3fb950; }
.yellow { color: #d29922; }
.red { color: #f85149; }
.alert { background: #f85149; color: white; border-radius: 8px; padding: 10px;
text-align: center; font-weight: bold; margin-bottom: 10px; display: none; }
.chart-card { background: #161b22; border-radius: 10px; padding: 14px;
border: 1px solid #30363d; margin-bottom: 10px; }
canvas { width: 100% !important; }
.btn { background: #238636; color: white; border: none; border-radius: 6px;
padding: 10px 20px; font-size: 0.9em; cursor: pointer; width: 100%; margin-top: 8px; }
.btn:hover { background: #2ea043; }
.status { text-align: center; color: #8b949e; font-size: 0.75em; margin-top: 8px; }
.bar-bg { background: #30363d; border-radius: 4px; height: 8px; margin-top: 6px; }
.bar { height: 8px; border-radius: 4px; transition: width 0.5s; }
</style>
</head>
<body>
<h1>BreathAnalyzer</h1>
<p class="subtitle">UTM IBM-251M - Andrian Scripcari</p>
<div id="alert" class="alert">ATENTIE - Biomarker depasit!</div>
<div class="grid">
<div class="card">
<div class="card-title">Temperatura</div>
<div class="card-val green" id="temp">--</div>
<div class="card-unit">C</div>
</div>
<div class="card">
<div class="card-title">Umiditate</div>
<div class="card-val green" id="hum">--</div>
<div class="card-unit">%RH</div>
</div>
</div>
<div class="grid">
<div class="card">
<div class="card-title">H2 (TGS2600)</div>
<div class="card-val" id="h2val">--</div>
<div class="card-unit">ppm | Prag: 20 ppm</div>
<div class="bar-bg"><div class="bar green" id="h2bar" style="width:0%"></div></div>
</div>
<div class="card">
<div class="card-title">NH3 (TGS2602)</div>
<div class="card-val" id="nh3val">--</div>
<div class="card-unit">ppm | Prag: 2 ppm</div>
<div class="bar-bg"><div class="bar green" id="nh3bar" style="width:0%"></div></div>
</div>
</div>
<div class="grid">
<div class="card">
<div class="card-title">MICS RED (NH3/H2)</div>
<div class="card-val green" id="micsred">--</div>
<div class="card-unit">V</div>
<div class="bar-bg"><div class="bar green" id="micsredbar" style="width:0%"></div></div>
</div>
<div class="card">
<div class="card-title">MICS OX (NO2)</div>
<div class="card-val green" id="micsox">--</div>
<div class="card-unit">V</div>
<div class="bar-bg"><div class="bar green" id="micsoxbar" style="width:0%"></div></div>
</div>
</div>
<div class="grid">
<div class="card">
<div class="card-title">TVOC (ENS160)</div>
<div class="card-val" id="tvocval">--</div>
<div class="card-unit">ppb | Prag: 500 ppb</div>
</div>
<div class="card">
<div class="card-title">eCO2 (ENS160)</div>
<div class="card-val green" id="eco2val">--</div>
<div class="card-unit">ppm</div>
</div>
</div>
<div class="chart-card">
<div class="card-title">H2 in timp real</div>
<canvas id="chart" height="120"></canvas>
</div>
<button class="btn" onclick="downloadCSV()">Export CSV pentru Origin</button>
<div class="status" id="status">Se conecteaza...</div>
<script>
var h2Data = [];
var csvData = [["Timp(s)","Temp(C)","Hum(%)","H2(ppm)","NH3(ppm)","TVOC(ppb)","eCO2(ppm)","MICS_RED(V)","MICS_OX(V)"]];
var startTime = Date.now();
var canvas = document.getElementById('chart');
var ctx = canvas.getContext('2d');
function drawChart() {
canvas.width = canvas.offsetWidth;
canvas.height = 120;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (h2Data.length < 2) return;
var max = Math.max.apply(null, h2Data.concat([25]));
var w = canvas.width, h = canvas.height, pad = 10;
var pragY = h - pad - (20/max)*(h-2*pad);
ctx.strokeStyle = '#f85149';
ctx.setLineDash([4,4]);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(pad, pragY);
ctx.lineTo(w-pad, pragY);
ctx.stroke();
ctx.setLineDash([]);
ctx.strokeStyle = '#58a6ff';
ctx.lineWidth = 2;
ctx.beginPath();
for (var i = 0; i < h2Data.length; i++) {
var x = pad + (i/(h2Data.length-1))*(w-2*pad);
var y = h - pad - (h2Data[i]/max)*(h-2*pad);
if (i === 0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
}
ctx.stroke();
ctx.fillStyle = '#f85149';
ctx.font = '10px Arial';
ctx.fillText('20 ppm', w-45, pragY-3);
}
function getColor(val, prag) {
if (val < prag*0.5) return 'green';
if (val < prag) return 'yellow';
return 'red';
}
function updateData(d) {
document.getElementById('temp').textContent = d.temperatura.toFixed(1);
document.getElementById('hum').textContent = d.umiditate.toFixed(1);
var h2 = d.h2_ppm;
document.getElementById('h2val').textContent = h2.toFixed(2);
document.getElementById('h2val').className = 'card-val ' + getColor(h2, 20);
document.getElementById('h2bar').style.width = Math.min(h2/30*100,100)+'%';
document.getElementById('h2bar').className = 'bar ' + getColor(h2, 20);
var nh3 = d.nh3_ppm;
document.getElementById('nh3val').textContent = nh3.toFixed(2);
document.getElementById('nh3val').className = 'card-val ' + getColor(nh3, 2);
document.getElementById('nh3bar').style.width = Math.min(nh3/5*100,100)+'%';
document.getElementById('nh3bar').className = 'bar ' + getColor(nh3, 2);
var mred = d.mics_red;
document.getElementById('micsred').textContent = mred.toFixed(2);
document.getElementById('micsred').className = 'card-val ' + getColor(mred, 2.0);
document.getElementById('micsredbar').style.width = Math.min(mred/3.3*100,100)+'%';
document.getElementById('micsredbar').className = 'bar ' + getColor(mred, 2.0);
var mox = d.mics_ox;
document.getElementById('micsox').textContent = mox.toFixed(2);
document.getElementById('micsox').className = 'card-val ' + getColor(mox, 2.0);
document.getElementById('micsoxbar').style.width = Math.min(mox/3.3*100,100)+'%';
document.getElementById('micsoxbar').className = 'bar ' + getColor(mox, 2.0);
document.getElementById('tvocval').textContent = d.tvoc.toFixed(0);
document.getElementById('tvocval').className = 'card-val ' + getColor(d.tvoc, 500);
document.getElementById('eco2val').textContent = d.eco2.toFixed(0);
var alertDiv = document.getElementById('alert');
if (h2 > 20 || nh3 > 2 || d.tvoc > 500) {
alertDiv.style.display = 'block';
alertDiv.textContent = 'ATENTIE: ' +
(h2 > 20 ? 'H2=' + h2.toFixed(1) + 'ppm ' : '') +
(nh3 > 2 ? 'NH3=' + nh3.toFixed(1) + 'ppm ' : '') +
(d.tvoc > 500 ? 'TVOC=' + d.tvoc.toFixed(0) + 'ppb' : '');
} else {
alertDiv.style.display = 'none';
}
h2Data.push(h2);
if (h2Data.length > 50) h2Data.shift();
drawChart();
var t = ((Date.now()-startTime)/1000).toFixed(1);
csvData.push([t, d.temperatura.toFixed(1), d.umiditate.toFixed(1),
h2.toFixed(2), nh3.toFixed(2), d.tvoc.toFixed(0),
d.eco2.toFixed(0), d.mics_red.toFixed(2), d.mics_ox.toFixed(2)]);
document.getElementById('status').textContent =
'Ultima actualizare: ' + new Date().toLocaleTimeString();
}
function fetchData() {
fetch('/data')
.then(function(r) { return r.json(); })
.then(function(d) { updateData(d); })
.catch(function() {
document.getElementById('status').textContent = 'Eroare conectare...';
});
}
function downloadCSV() {
var content = csvData.map(function(r) { return r.join(','); }).join('\n');
var blob = new Blob([content], {type:'text/csv'});
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'BreathAnalyzer.csv';
a.click();
}
fetchData();
setInterval(fetchData, 2000);
</script>
</body>
</html>
)rawhtml";
server.send(200, "text/html", html);
}
void handleData() {
String json = "{";
json += "\"temperatura\":" + String(temperatura, 2) + ",";
json += "\"umiditate\":" + String(umiditate, 2) + ",";
json += "\"tvoc\":" + String(tvoc, 2) + ",";
json += "\"eco2\":" + String(eco2, 2) + ",";
json += "\"rs2600\":" + String(rs2600, 2) + ",";
json += "\"rs2602\":" + String(rs2602, 2) + ",";
json += "\"h2_ppm\":" + String(h2_ppm, 2) + ",";
json += "\"nh3_ppm\":" + String(nh3_ppm, 2) + ",";
json += "\"mics_red\":" + String(vRED, 2) + ",";
json += "\"mics_ox\":" + String(vOX, 2) + "}";
server.send(200, "application/json", json);
}
void setup() {
Serial.begin(115200);
dht.begin();
pinMode(MICS_RED, INPUT);
pinMode(MICS_OX, INPUT);
tft.begin();
tft.setRotation(0);
tft.fillScreen(ILI9341_BLACK);
tft.setTextColor(ILI9341_CYAN);
tft.setTextSize(2);
tft.setCursor(10, 5);
tft.print("BreathAnalyzer");
tft.drawFastHLine(0, 25, 240, ILI9341_CYAN);
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(5, 33);
tft.print("Temp:");
tft.setCursor(130, 33);
tft.print("Hum:");
tft.drawFastHLine(0, 45, 240, ILI9341_DARKGREY);
tft.setCursor(5, 52);
tft.print("H2 (TGS2600):");
tft.setCursor(185, 52);
tft.setTextColor(ILI9341_DARKGREY);
tft.print("prag:20");
tft.drawFastHLine(0, 112, 240, ILI9341_DARKGREY);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(5, 112);
tft.print("NH3 (TGS2602):");
tft.setCursor(185, 112);
tft.setTextColor(ILI9341_DARKGREY);
tft.print("prag:2");
tft.drawFastHLine(0, 163, 240, ILI9341_DARKGREY);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(5, 170);
tft.print("TVOC:");
tft.setCursor(5, 188);
tft.print("CO2: ");
tft.setCursor(5, 206);
tft.print("MICS RED:");
tft.setCursor(5, 222);
tft.print("MICS OX: ");
tft.drawFastHLine(0, 238, 240, ILI9341_DARKGREY);
tft.drawFastHLine(0, 283, 240, ILI9341_DARKGREY);
tft.setTextColor(ILI9341_DARKGREY);
tft.setCursor(5, 290);
tft.print("WiFi:BreathAnalyzer 192.168.4.1");
WiFi.softAP(ssid, password);
Serial.println("WiFi Hotspot pornit!");
Serial.print("IP: ");
Serial.println(WiFi.softAPIP());
server.on("/", handleRoot);
server.on("/data", handleData);
server.begin();
Serial.println("Server pornit - 192.168.4.1");
}
void loop() {
server.handleClient();
static unsigned long lastRead = 0;
if (millis() - lastRead > 2000) {
lastRead = millis();
float t = dht.readTemperature();
float h = dht.readHumidity();
if (!isnan(t)) temperatura = t;
if (!isnan(h)) umiditate = h;
tvoc = random(100, 500);
eco2 = random(400, 1000);
int raw2600 = analogRead(TGS2600_PIN);
float v2600 = raw2600 * (3.3 / 4095.0);
rs2600 = (v2600 > 0.05) ? (3.3 - v2600) / v2600 * 10000.0 : 0;
h2_ppm = rsToPpm2600(rs2600);
int raw2602 = analogRead(TGS2602_PIN);
float v2602 = raw2602 * (3.3 / 4095.0);
rs2602 = (v2602 > 0.05) ? (3.3 - v2602) / v2602 * 10000.0 : 0;
nh3_ppm = rsToPpm2602(rs2602);
int rawRED = analogRead(MICS_RED);
int rawOX = analogRead(MICS_OX);
vRED = rawRED * (3.3 / 4095.0);
vOX = rawOX * (3.3 / 4095.0);
Serial.print("T:"); Serial.print(temperatura, 1);
Serial.print("C | H2:"); Serial.print(h2_ppm, 2);
Serial.print("ppm | NH3:"); Serial.print(nh3_ppm, 2);
Serial.print("ppm | MICS_RED:"); Serial.print(vRED, 2);
Serial.print("V | MICS_OX:"); Serial.print(vOX, 2);
Serial.print("V | TVOC:"); Serial.print(tvoc, 0);
Serial.println("ppb");
drawTFT();
}
}TGS2600 H2
TGS2602 NH3
MICS OX
MICS RED