/**********************************************************************
* ESP32 - Mobiles Messgerät (Workshop-ready, 2025)
*
* Sensoren:
* - BMP180 (Temperatur & Luftdruck)
* - MAX4466 (Lautstärke)
* - LDR (Helligkeit)
*
* Anzeige:
* - OLED (128x64)
* - Webinterface mit Live-Werten, Ampeln & Lärmanzeige
*
* Funktionen:
* - Verkehrsampel (3s grün, 1s gelb, 3s rot, 1s rot+gelb)
* - Lärmampe (9-Stufen Skala aus Pegel, reagiert auf Raumlärm)
* - „AUS“-Button schaltet alles ab
* - Visuelle Warnung (rote LED blinkt bei MIC ≥ 8)
*
* CCC - Martin Biebl - Sankt-Ansgar-Schule Hamburg - CCC
*********************************************************************/
// === WLAN / Geräte-Setup ===========================================
#define DEVICE_NAME "Code-W"
#define WIFI_PASSWORD "12345678"
#define MEASUREMENT_INTERVAL_SEC 1
// === Hardware-Pins =================================================
#define OLED_SDA 23
#define OLED_SCL 22
#define ADC_PIN_LDR 34
#define ADC_PIN_MIC 35
#define LED_GREEN 15
#define LED_YELLOW 2
#define LED_RED 4
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_BMP085.h>
#include <WiFi.h>
#include <WebServer.h>
#include <time.h>
#include <math.h>
// === Objekte =======================================================
Adafruit_SSD1306 display(128, 64, &Wire, -1);
Adafruit_BMP085 bmp;
WebServer server(80);
// === Globale Messdaten =============================================
volatile int lastLdr = 0;
volatile int lastMic = 0;
volatile float lastTemp = 0.0;
volatile float lastPres = 0.0;
// === Ampelstatus ===================================================
bool autoAmpelMode = false; // Verkehrsampel aktiv?
bool lärmAmpelMode = false; // Lärmampe aktiv?
int ampelState = 0; // interner Zustand der Verkehrsampel
unsigned long ampelTimer = 0; // Timer für Ampelphasen
// === Zeit / Uhr ====================================================
bool timeSynced = false;
uint64_t startEpoch = 0;
uint64_t startMillis = 0;
// === Zeit-Funktionen ===============================================
String formatTime(uint64_t epochSec) {
time_t t = (time_t)epochSec;
struct tm *tm_info = localtime(&t);
char buffer[25];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", tm_info);
return String(buffer);
}
String getCurrentTimeString() {
if (!timeSynced) return "--:--:--";
uint64_t elapsedMs = millis() - startMillis;
uint64_t epochNow = startEpoch + (elapsedMs / 1000ULL);
return formatTime(epochNow);
}
// === LDR lesen =====================================================
int readLDR(int pin, int samples = 10) {
long sum = 0;
for (int i = 0; i < samples; i++) { sum += analogRead(pin); delay(2); }
return sum / samples;
}
// === Lautstärke mit automatischer Kalibrierung =====================
int readMicLevel(int pin) {
const int samples = 250; // Messzeit ~0,25s
long sum = 0;
for (int i = 0; i < samples; i++) { sum += analogRead(pin); delayMicroseconds(400); }
float mean = (float)sum / samples;
long sumSq = 0;
for (int i = 0; i < samples; i++) {
int val = analogRead(pin);
float diff = val - mean;
sumSq += diff * diff;
delayMicroseconds(400);
}
float rms = sqrt((float)sumSq / samples);
static float noiseFloor = 10; // Grundrauschen
static float noiseCeil = 120; // Max-Pegel
// Sanfte Anpassung an Raumgeräusche
noiseFloor = 0.995f * noiseFloor + 0.005f * min(noiseFloor, rms);
noiseCeil = max(noiseCeil * 0.98f, rms);
if (noiseCeil - noiseFloor < 30) noiseCeil = noiseFloor + 30;
float norm = (rms - noiseFloor) / (noiseCeil - noiseFloor);
if (norm < 0) norm = 0; if (norm > 1) norm = 1;
int level = 1 + roundf(norm * 8.0f); // Pegel 1..9
// Debug-Ausgabe zur Kontrolle
Serial.printf("MIC RMS=%.1f | floor=%.1f | ceil=%.1f | level=%d\n",
rms, noiseFloor, noiseCeil, level);
return level;
}
// === Sensoren einlesen =============================================
void readSensors() {
lastLdr = readLDR(ADC_PIN_LDR);
lastMic = readMicLevel(ADC_PIN_MIC);
if (bmp.begin()) {
lastTemp = bmp.readTemperature();
lastPres = bmp.readPressure() / 100.0;
} else { lastTemp = NAN; lastPres = NAN; }
}
// === OLED-Ausgabe ==================================================
void updateOLED() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
if (!timeSynced) {
display.print(DEVICE_NAME);
display.print(" "); display.println(WiFi.softAPIP().toString());
} else display.println(getCurrentTimeString());
display.setTextSize(2);
display.setCursor(0, 12);
display.printf("T %.1f C\n", isnan(lastTemp)?0:lastTemp);
display.setCursor(0, 34);
display.printf("P %.0f hPa", isnan(lastPres)?0:lastPres);
display.setCursor(0, 54);
display.setTextSize(1);
display.printf("LDR:%4d MIC:%d", lastLdr, lastMic);
display.display();
}
// === LED-Steuerung =================================================
void setLEDs(bool g, bool y, bool r) {
digitalWrite(LED_GREEN, g);
digitalWrite(LED_YELLOW, y);
digitalWrite(LED_RED, r);
}
// === Lärmampe-Logik ================================================
void updateLärmAmpel(int level) {
if (!lärmAmpelMode) return;
if (level <= 2) setLEDs(true, false, false);
else if (level <= 4) setLEDs(true, true, false);
else if (level <= 6) setLEDs(false, true, false);
else if (level <= 8) setLEDs(false, true, true);
else setLEDs(false, false, true);
}
// === Lärmwarnung (Blinksignal) =====================================
void checkLärmWarnung() {
static unsigned long lastWarn = 0;
static bool blinking = false;
static unsigned long blinkStart = 0;
// Blinkphase aktiv
if (blinking) {
if (millis() - blinkStart < 100) setLEDs(false, false, true); // Rot an
else if (millis() - blinkStart < 250) setLEDs(false, false, false); // aus
else blinking = false;
return;
}
// Bei Pegel >= 8 und nach 2s Pause
if (lärmAmpelMode && lastMic >= 8 && millis() - lastWarn > 2000) {
blinking = true;
blinkStart = millis();
lastWarn = millis();
Serial.println("🚨 Lärmwarnung ausgelöst (MIC >= 8)");
}
}
// === Verkehrsampel-Logik ===========================================
void updateVerkehrsAmpel() {
if (!autoAmpelMode) return;
unsigned long now = millis(), diff = now - ampelTimer;
switch (ampelState) {
case 0: setLEDs(true,false,false); if(diff>3000){ampelState=1;ampelTimer=now;} break;
case 1: setLEDs(false,true,false); if(diff>1000){ampelState=2;ampelTimer=now;} break;
case 2: setLEDs(false,false,true); if(diff>3000){ampelState=3;ampelTimer=now;} break;
case 3: setLEDs(false,true,true); if(diff>1000){ampelState=0;ampelTimer=now;} break;
}
}
// === Webinterface (HTML + JS) ======================================
String generateHTML() {
// Weboberfläche mit allen Buttons und Balkenanzeige
String html = R"rawliteral(
<!DOCTYPE html><html><head><meta charset="utf-8">
<title>)rawliteral"+String(DEVICE_NAME)+R"rawliteral(</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body{font-family:Arial;background:#111;color:#e0e0e0;text-align:center}
h1{font-size:1.4em}
.vals{margin:10px}
button{padding:12px 18px;margin:6px;font-size:1em;border-radius:6px;border:0;cursor:pointer}
.g{background:#0a0;color:#fff}.y{background:#aa0;color:#000}
.r{background:#a00;color:#fff}.a{background:#06c;color:#fff}
.b{background:#444;color:#fff}
.status{margin:10px;padding:6px 10px;border-radius:8px;display:inline-block;font-weight:bold;}
.green{background:#060;color:#fff}.blue{background:#06c;color:#fff}.gray{background:#555;color:#fff}
.bar-container{width:80%;height:20px;background:#333;margin:10px auto;border-radius:10px;overflow:hidden;}
.bar{height:100%;width:0%;background:#0f0;border-radius:10px;transition:width 0.3s,background 0.3s;}
@keyframes glow{0%{box-shadow:0 0 5px #f00;}50%{box-shadow:0 0 20px #ff4040;}100%{box-shadow:0 0 5px #f00;}}
.glow{animation:glow 0.8s infinite alternate;}
</style></head>
<body>
<h1>)rawliteral"+String(DEVICE_NAME)+R"rawliteral(</h1>
<p class="status gray" id="modeStatus">Modus: unbekannt</p>
<p>Zeit: <span id="zeit">--:--:--</span></p>
<div class="vals">
<div>T: <span id="temp">-</span> °C</div>
<div>P: <span id="pres">-</span> hPa</div>
<div>LDR: <span id="ldr">-</span></div>
<div>MIC: <span id="mic">-</span></div>
<div class="bar-container"><div id="micBar" class="bar"></div></div>
</div>
<div>
<button onclick="led('green')" class="g">GRÜN</button>
<button onclick="led('yellow')" class="y">GELB</button>
<button onclick="led('red')" class="r">ROT</button>
<button onclick="led('redyellow')" class="r">ROT+GELB</button>
<button onclick="led('ampel')" class="a">VERKEHRSAMPEL</button>
<button onclick="led('lärm')" class="a">LÄRMAMPEL</button>
<button onclick="led('off')" class="b">AUS</button>
</div>
<script>
function sendTime(){fetch("/settime?epoch="+Date.now());}
function updateValues(){
fetch('/value').then(r=>r.json()).then(obj=>{
document.getElementById('zeit').innerText=obj.time;
document.getElementById('temp').innerText=obj.temp?.toFixed(1)||'-';
document.getElementById('pres').innerText=obj.pres?.toFixed(1)||'-';
document.getElementById('ldr').innerText=obj.ldr;
document.getElementById('mic').innerText=obj.mic+" ("+obj.micText+")";
let s=document.getElementById('modeStatus');
s.className="status "+obj.modeColor; s.innerText="Modus: "+obj.modeText;
let bar=document.getElementById('micBar');
let width=(obj.mic/9*100);
let color=obj.mic<=3?'#0f0':(obj.mic<=6?'#ff0':'#f00');
bar.style.width=width+'%'; bar.style.background=color;
if(obj.mic>=7) bar.classList.add("glow"); else bar.classList.remove("glow");
});
}
function led(cmd){fetch('/led?cmd='+cmd);}
window.onload=()=>{sendTime();updateValues();setInterval(updateValues,1000);}
</script></body></html>)rawliteral";
return html;
}
// === Server-Endpunkte ==============================================
void handleRoot(){ server.send(200,"text/html",generateHTML()); }
void handleValue(){
String timeStr=getCurrentTimeString();
String micText;
if(lastMic<=2) micText="sehr leise";
else if(lastMic<=3) micText="leise";
else if(lastMic<=5) micText="normal";
else if(lastMic<=7) micText="laut";
else if(lastMic<=8) micText="sehr laut";
else micText="extrem laut";
String modeText,modeColor;
if(autoAmpelMode){modeText="Verkehrsampel";modeColor="green";}
else if(lärmAmpelMode){modeText="Lärmampe";modeColor="blue";}
else{modeText="Manuell / Aus";modeColor="gray";}
char buf[400];
snprintf(buf,sizeof(buf),
"{\"time\":\"%s\",\"temp\":%.2f,\"pres\":%.2f,\"ldr\":%d,\"mic\":%d,"
"\"micText\":\"%s\",\"modeText\":\"%s\",\"modeColor\":\"%s\"}",
timeStr.c_str(),lastTemp,lastPres,lastLdr,lastMic,
micText.c_str(),modeText.c_str(),modeColor.c_str());
server.send(200,"application/json",buf);
}
void handleLed(){
String cmd=server.arg("cmd"); cmd.toLowerCase();
if(cmd=="green"){setLEDs(1,0,0);autoAmpelMode=false;lärmAmpelMode=false;}
else if(cmd=="yellow"){setLEDs(0,1,0);autoAmpelMode=false;lärmAmpelMode=false;}
else if(cmd=="red"){setLEDs(0,0,1);autoAmpelMode=false;lärmAmpelMode=false;}
else if(cmd=="redyellow"){setLEDs(0,1,1);autoAmpelMode=false;lärmAmpelMode=false;}
else if(cmd=="ampel"){autoAmpelMode=true;lärmAmpelMode=false;ampelState=0;ampelTimer=millis();}
else if(cmd=="lärm"){lärmAmpelMode=true;autoAmpelMode=false;}
else if(cmd=="off"){setLEDs(0,0,0);autoAmpelMode=false;lärmAmpelMode=false;}
server.send(200,"text/plain","OK");
}
void handleSetTime(){
if(server.hasArg("epoch")){
double epochMs=server.arg("epoch").toDouble();
startEpoch=(uint64_t)(epochMs/1000ULL);
startMillis=millis(); timeSynced=true;
server.send(200,"text/plain","Time synced");
Serial.printf("Zeit gesetzt: %s\n",formatTime(startEpoch).c_str());
updateOLED();
} else server.send(400,"text/plain","Missing epoch");
}
// === Setup =========================================================
void setup(){
Serial.begin(115200);
Serial.println("=== ESP32 Messgerät Start ===");
pinMode(LED_GREEN,OUTPUT); pinMode(LED_YELLOW,OUTPUT); pinMode(LED_RED,OUTPUT);
analogReadResolution(12); analogSetAttenuation(ADC_11db);
Wire.begin(OLED_SDA,OLED_SCL);
display.begin(SSD1306_SWITCHCAPVCC,0x3C);
display.clearDisplay(); display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0); display.println("Start..."); display.display();
bmp.begin();
setenv("TZ","CET-1CEST,M3.5.0,M10.5.0/3",1); tzset();
WiFi.softAP(DEVICE_NAME,WIFI_PASSWORD);
Serial.printf("AP: %s IP: %s\n",DEVICE_NAME,WiFi.softAPIP().toString().c_str());
server.on("/",handleRoot);
server.on("/value",handleValue);
server.on("/led",handleLed);
server.on("/settime",handleSetTime);
server.begin();
readSensors(); updateOLED();
}
// === Loop ==========================================================
void loop(){
server.handleClient();
static unsigned long lastMillis=0;
if(millis()-lastMillis>=MEASUREMENT_INTERVAL_SEC*1000UL){
lastMillis=millis(); readSensors(); updateOLED();
}
if(autoAmpelMode) updateVerkehrsAmpel();
if(lärmAmpelMode){ updateLärmAmpel(lastMic); checkLärmWarnung(); }
}