// ----------------------------------------------------------
// Kraus Stefan ©
// ----------------------------------------------------------
// Description:
// Levelindicators for Cistern
// The OLED-Panel displays the aktual Waterlevel
// in percent, liter and as a Bar chart.
//
// Hardware:
// - ESP32
// - 1,3" OLED I2C 128x64
// - AJ-SR04M Ultrasonic-Sensor
// ----------------------------------------------------------
// Historie:
// 2024-09-08 - Start Programming
// 2024-09-11 - Wifi Connection + Webserver
//
// ----------------------------------------------------------
/* ESP32 HTTP IoT Server Example for Wokwi.com
https://wokwi.com/projects/320964045035274834
To test, you need the Wokwi IoT Gateway, as explained here:
https://docs.wokwi.com/guides/esp32-wifi#the-private-gateway
Then start the simulation, and open http://localhost:9080
in another browser tab.
Note that the IoT Gateway requires a Wokwi Club subscription.
To purchase a Wokwi Club subscription, go to https://wokwi.com/club
*/
#include <Arduino.h> // Allgemeine Arduino-Bib.
#include <Wire.h> // fuer I2C Kommunikation
#include <NewPing.h> // fuer Ultraschall-Sensor
#include <U8g2lib.h> // fuer OLED-Display
#include <WiFi.h> // fuer WLAN
#include <WiFiClient.h> // fuer WLAN
#include <NTPClient.h> // fuer Timeserver
#include <WiFiUdp.h> // fuer Timeserver
#include <WebServer.h> // fuer Weboberflaeche
#include <uri/UriBraces.h> // fuer Weboberflaeche
#define LevelFull 50 // Hoehe Wassersaeule wenn voll (=100%) in cm
#define LevelEmpty 350 // Abstand Sensor zu Boden (=0%) in cm
#define MaxVolume 8000 // Volumen des zu messenden Mediums bei 100% in l
#define TrigPin 27 // USS-Sensor Trigger Pin
#define EchoPin 26 // USS-Sensor Echo Pin
#define Max_Distance 400 // Begrenzung fuer Ultraschallsensor
#define Iterations 5 // Anzahl der Messimpulse
#define IN_Button_Test 16 // Eingang: Test-Button
#define IN_Sensor_Pumpdef 35 // Eingang: Schwimmer Pumpenschutz
#define IN_Sensor_Refill 34 // Eingang: Schwimmer Nachfüllen
#define OUT_Waterpump 13 // Ausgang: Wasserpumpe Ein/Aus
#define OUT_Led_Error 12 // Ausgang: Stoerunglampe (Rot)
#define Wifi_SSID "FRITZ!Box 7590 QW" // Zugangsdaten WLAN
#define Wifi_Password "76372539927607722879" // Zugangsdaten WLAN
#define Wifi_Channel 6 // WLAN Kanal
float flDuration; // Dauer zwischen Senden und Empfangen eines Signals
int iDistance; // Gemessener Wert (Sensor <-> Oberfläche) in cm
int iLevelAct; // errechnete Fuellstandshöhe in cm
int iLevelPercent; // Prozentwert des Fuellstands im Tank (kann auch über 100%)
int iDisplLevelPercent; // Prozentwert des Fuellstands im Tank (0-100%)
int iVolumeAct; // Volumen des Tanks in Liter
int iDisplBarHeight; // Höhe des am Display anzuzeigenden Balkens in Pixel
int i; // Zählervariable für Schleifen
char strLevelPercent[4]; // Level in Prozent
char strVolumeAct[5]; // Volumen in Liter
char strLevelAct[4]; // Fuellstandshoehe in cm
String strErrorCode = "keine"; // Fehlerbeschreibung
String strTimeLastMeas; // Zeitangabe der letzen Messung
String strColor_B[10]; // Farbe Balken Weboberfläche
String strErrColor; // Textfarbe Fehlerbeschreibung Weboberfläche
String strPumpDefColor; // Farbe Anzeige Pumpenschutz Weboberfläche
String strRefillSensorColor; // Farbe Anzeige Nachfuellsensor Weboberfläche
int iStoerungAktiv = 0; // 0=keine, 1= FW, 2=Fehler
bool in_bButtonTest = false; // Eingang: Schwimmerschalter Pumpenschutz
bool in_bSensorPumpDef = false; // Eingang: Schwimmerschalter Pumpenschutz
bool in_bSensorRefill = false; // Eingang: Schwimmerschalter Nachfüllen
bool out_bLedError = false; // Ausgang: Stoerungslampe
bool out_bWaterpump = false; // Ausgang: Pumpe Ein/Aus
unsigned long myTime; // Aktuelle Zeit in Millisekunden seitdem die Runtime gestartet ist. (Überlauf nach 50 Tagen auf 0)
unsigned long timeLastMeasuring; // Zeitstempel der letzen Messung
unsigned long timeLastHtmlSend; // Zeitstempel der letzen Webserver aktualisierung
unsigned long timeLastToggleLED; // Zeitstempel Led-Toggel
unsigned long timePressButton; // Zeitstempel seit wann Test-Button gedrückt wurde
// Aufruf Treiber: Display
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/U8X8_PIN_NONE);
// Aufruf Treiber: Ultraschallsensor
NewPing sonar(TrigPin, EchoPin, Max_Distance);
// Aufruf Treiber: Webserver
WebServer server(80);
// Aufruf Treiber: Zeitserver
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "europe.pool.ntp.org", 3600, 60000);
// #############################################################################################################
// #############################################################################################################
// Erstelle Html-Webseite
void sendHtml() {
String response = R"(
<!DOCTYPE html><html>
<head>
<title>Fuelstandsanzeige Zisterne</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
html { font-family: sans-serif; text-align: center; }
body { display: inline-flex; flex-direction: column; }
h1 { margin-bottom: 1.2em; color: #5e9ca0; text-align: center; }
h2 { margin: 0; Color_Err; text-align: center;}
p { text-align: center; }
</style>
</head>
<body>
<h1>Fuellstandsanzeige Zisterne</h1>
<h2>Stoerung: Error_code</h2>
<p>Aktueller Fuellstand:<br />lvl_Percent %<br />lvl_cm cm <br />lvl_Liter L </p>
<table style="height: 35px; width: 60.0674%; border-collapse: collapse; margin-left: auto; margin-right: auto;">
<tbody>
<tr>
<td style="width: 86,902%; text-align: center;">Schwimmer Pumpenschutz</td>
<td style="width: 13.9098%; Color_PumpDef; text-align: center;"> </td>
</tr>
<tr>
<td style="width: 86,902%; text-align: center;">Schwimmer Nachfuellen</td>
<td style="width: 13.9098%; Color_Refill; text-align: center;"> </td>
</tr>
</tbody>
</table>
<h5> Letzte Messung: TimeLastMeas
</h5>
<table class="editorDemoTable" style="height: 118px; width: 130px; border-style: solid; background-color: lightgray; margin-left: auto; margin-right: auto;" width="224">
<tr style="height: 18px;">
<td style="width: 120px; height: 18px; Color_BA"> </td>
</tr>
<tr style="height: 18px;">
<td style="width: 120px; height: 18px; Color_B9;"> </td>
</tr>
<tr style="height: 18px;">
<td style="width: 120px; height: 18px; Color_B8;"> </td>
</tr>
<tr style="height: 18px;">
<td style="width: 120px; height: 18px; Color_B7;"> </td>
</tr>
<tr style="height: 18px;">
<td style="width: 120px; height: 18px; Color_B6;"> </td>
</tr>
<tr style="height: 18px;">
<td style="width: 120px; height: 18px; Color_B5;"> </td>
</tr>
<tr style="height: 18px;">
<td style="width: 120px; height: 18px; Color_B4;"> </td>
</tr>
<tr style="height: 18px;">
<td style="width: 120px; height: 18px; Color_B3;"> </td>
</tr>
<tr style="height: 18px;">
<td style="width: 120px; height: 18px; Color_B2;"> </td>
</tr>
<tr style="height: 18px;">
<td style="width: 120px; height: 18px; Color_B1;"> </td>
</tr>
</tbody>
</table>
<h6 style="text-align: center;">© Stefan Kraus 2024-09-15 </h6>
</body>
</html>
)";
// Setze Fuellstand der Balkenanzeige am Webserver zurück
for (i = 1; i <= 10; i++) {
strColor_B[i] = "";
}
// Setze nun nur die Balken des aktuellen Fuellstandes
if (iLevelPercent > 5) { strColor_B[1] = "background-color: #65D0EB"; } // -> Blau
if (iLevelPercent > 15) { strColor_B[2] = "background-color: #65D0EB"; }
if (iLevelPercent > 25) { strColor_B[3] = "background-color: #65D0EB"; }
if (iLevelPercent > 35) { strColor_B[4] = "background-color: #65D0EB"; }
if (iLevelPercent > 45) { strColor_B[5] = "background-color: #65D0EB"; }
if (iLevelPercent > 55) { strColor_B[6] = "background-color: #65D0EB"; }
if (iLevelPercent > 65) { strColor_B[7] = "background-color: #65D0EB"; }
if (iLevelPercent > 75) { strColor_B[8] = "background-color: #65D0EB"; }
if (iLevelPercent > 85) { strColor_B[9] = "background-color: #65D0EB"; }
if (iLevelPercent > 95) { strColor_B[10]= "background-color: #65D0EB"; }
// Ändere die Farbe der Störungsmeldung je nach Prio
switch (iStoerungAktiv) {
case 0: strErrColor = "color: #0CB000"; break; // -> Grün
case 1: strErrColor = "color: #E89300"; break; // -> Orange
case 2: strErrColor = "color: #DB0000"; break; // -> Rot
}
// Ändere die Farbe der Sensoreingaenge je nach Status
if (in_bSensorPumpDef) { strPumpDefColor = "background-color: #0CB000"; } // -> Grün
else { strPumpDefColor = "background-color: #DB0000"; } // -> Rot
if (in_bSensorRefill) { strRefillSensorColor = "background-color: #0CB000"; } // -> Grün
else { strRefillSensorColor = "background-color: #DB0000"; } // -> Rot
// Durchsuche Html-Datei und ersetze die jeweilig gefundenen Textausschnitte mit dem neuen Werten
response.replace("TimeLastMeas", timeClient.getFormattedTime());
response.replace("Error_code", strErrorCode);
response.replace("lvl_Percent", strLevelPercent);
response.replace("lvl_Liter", strVolumeAct);
response.replace("lvl_cm", strLevelAct);
response.replace("Color_Err", strErrColor);
response.replace("Color_PumpDef", strPumpDefColor);
response.replace("Color_Refill", strRefillSensorColor);
response.replace("Color_B1", strColor_B[1]);
response.replace("Color_B2", strColor_B[2]);
response.replace("Color_B3", strColor_B[3]);
response.replace("Color_B4", strColor_B[4]);
response.replace("Color_B5", strColor_B[5]);
response.replace("Color_B6", strColor_B[6]);
response.replace("Color_B7", strColor_B[7]);
response.replace("Color_B8", strColor_B[8]);
response.replace("Color_B9", strColor_B[9]);
response.replace("Color_BA", strColor_B[10]);
// Sende html-Datei an Server
server.send(200, "text/html", response);
}
// #############################################################################################################
// #############################################################################################################
void setup() {
// Starte Serielle Schnittstelle
Serial.begin(9600);
// Definiere Hardware Ein- & Ausgänge
pinMode(IN_Button_Test, INPUT_PULLUP); // Test-Button
pinMode(IN_Sensor_Pumpdef, INPUT_PULLDOWN); // Schwimmerschalter Pumpenschutz
pinMode(IN_Sensor_Refill, INPUT_PULLDOWN); // Schwimmerschalter Nachfuellen
pinMode(OUT_Led_Error, OUTPUT); // Störungslampe
pinMode(OUT_Waterpump, OUTPUT); // Wasserpumpe Ein/Aus
// Starte WLAN-Kommunikation
WiFi.begin(Wifi_SSID, Wifi_Password, Wifi_Channel);
Serial.print("Connecting to WiFi ");
Serial.print(Wifi_SSID);
// Warte auf Connection
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(" Connected!");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
// IBN SKR - Keine Ahnung was das hier machen?? :D
server.on("/", sendHtml);
// Sende aktuelle Websitedaten
sendHtml();
// Starte Display
u8g2.begin();
// Starte Webserver
server.begin();
Serial.println("HTTP server started");
timeClient.begin();
Serial.println("TimeClient started");
}
// #############################################################################################################
// #############################################################################################################
void loop() {
// IBN SKR - Keine Ahnung was das hier machen?? :D
server.handleClient();
// Aktualisiere Zeit
timeClient.update();
myTime = millis();
// Wartezeit zwischen Pings (ca. 5 pings/sec). nicht kleiner als 29ms!
delay(50);
// Lese Eingänge
in_bButtonTest = digitalRead(IN_Button_Test) == LOW;
in_bSensorRefill = digitalRead(IN_Sensor_Refill) == HIGH;
in_bSensorPumpDef = digitalRead(IN_Sensor_Pumpdef) == HIGH;
if ((myTime > (timeLastMeasuring + 1000)) || in_bButtonTest ) { // Alle 10 Sekunden eine Messung oder bei jedem Test
// Starte Ultraschallmessung
flDuration = sonar.ping_median(Iterations);
// ermittle Abstand in cm
iDistance = int((flDuration / 2) * 0.0343); // -> gemessener Weg : 2 * Schallgeschwindigkeit // Auf INT begrenzt somit kein Kommazahlen!
// Aktueller Fuellstand berechnen
iLevelAct = (LevelEmpty - iDistance);
// Fuellstand in Prozent berechnen
iLevelPercent = ((iLevelAct + LevelFull) * 100 / LevelEmpty); // LevelFull -> Offset
// Berechnung Volumen
iVolumeAct = ((MaxVolume / 100) * iLevelPercent);
// Setze Zeitwert der letzten Messung
timeLastMeasuring = myTime;
strTimeLastMeas = timeClient.getFormattedTime();
}
// Konvertiere INT to STRING damit diese als Text eingesetzt werden können
sprintf(strLevelPercent, "%d", iLevelPercent);
sprintf(strVolumeAct, "%d", iVolumeAct);
sprintf(strLevelAct, "%d", iLevelAct);
// Display anzeige
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.setFontRefHeightExtendedText();
u8g2.setDrawColor(1);
u8g2.setFontPosTop();
u8g2.setFontDirection(0);
// Ueberschrift
u8g2.drawStr(0, 0, "Fuelstandsanzeige");
// Fuellstand in Prozent
u8g2.drawStr(50, 20, strLevelPercent );
u8g2.drawStr(73, 20, "%" );
// Fuellstand in Prozent
u8g2.drawStr(50, 34, strLevelAct );
u8g2.drawStr(73, 34, "cm" );
// Fuellstand in Litern
u8g2.drawStr(50, 48, strVolumeAct);
u8g2.drawStr(77, 48, "L");
// Balkenanzeige
iDisplLevelPercent = constrain(iLevelPercent, 0, 100); //um nur Werte zwischen 0 und 100 zu erhalten
iDisplBarHeight = map(iDisplLevelPercent, 0, 100, 63, 16); //Hoehe des Balkens ermitteln
// Rahmen der Zisterne zeichnen
u8g2.drawFrame(2, 16, 38, 48);
// Wasserstand zeichnen
u8g2.drawBox(2, iDisplBarHeight, 38, 48);
// ----------------------------------------------------------------------------------
// Easteregg! :D
if (in_bButtonTest) {
u8g2.drawBox(0, 0, 128, 64); // -> Erstmal alles Weiß machen
u8g2.setDrawColor(0); // Farbe umschalten um micht schwarz zu schreiben
u8g2.drawStr(1, 5, "Lampntest basst! ");
u8g2.drawStr(1, 35, "& etz owe vom Knepfl ");
u8g2.drawStr(1, 50, "langt scho, du Depp ");
}
// ----------------------------------------------------------------------------------
// Sende Displaydaten
u8g2.sendBuffer();
// Stoermeldungen
if (iLevelPercent < 25) {
if (iLevelPercent >= 15) {
strErrorCode = "FW Wassermangel";
iStoerungAktiv = 1;
out_bLedError = true;
} else {
strErrorCode = "Wassermangel!";
iStoerungAktiv = 2;
// Bei Wassermangel -> LED Toggeln -> Mittels Relai hat man gleich ein akkustisches Signal!!
if ((myTime > (timeLastToggleLED + 2000)) && !out_bLedError) {
out_bLedError = true;
timeLastToggleLED = myTime;
}
if ((myTime > (timeLastToggleLED + 2000)) && out_bLedError) {
out_bLedError = false;
timeLastToggleLED = myTime;
}
};
}
else {
strErrorCode = "keine";
iStoerungAktiv = 0;
out_bLedError = false;
}
// Abfrage Schutzeinrichtungen
if ((!in_bSensorRefill) && (!iStoerungAktiv==2)) { // nur anzeigen, wenn nicht bereits ein Fehler ansteht!
strErrorCode = "Nachfuellsensor!";
iStoerungAktiv = 1;
out_bLedError = true;
}
if (!in_bSensorPumpDef) {
strErrorCode = "Pumpenschutz!";
iStoerungAktiv = 2;
// Bei Wassermangel -> LED Toggeln -> Mittels Relai hat man gleich ein akkustisches Signal!!
if ((myTime > (timeLastToggleLED + 2000)) && !out_bLedError) {
out_bLedError = true;
timeLastToggleLED = myTime;
}
if ((myTime > (timeLastToggleLED + 2000)) && out_bLedError) {
out_bLedError = false;
timeLastToggleLED = myTime;
}
}
switch (iStoerungAktiv) {
case 0: out_bWaterpump = true; break; // Pumpe ein -> keine Stoerung!
case 1: out_bWaterpump = true; break; // Pumpe ein -> nur Frühwarnung!
case 2: if (in_bSensorPumpDef) { out_bWaterpump = false;} break; // Pumpe aus -> Wassermangel oder Stoerung!
} // IBN SKR - Abschaltung aktuel nur über Schwimmerschalter !! Sensor muss noch geteached werden!
// Schreibe Ausgang
digitalWrite(OUT_Led_Error, out_bLedError);
digitalWrite(OUT_Waterpump, out_bWaterpump);
// Aktualisiere WebServer
if (myTime > (timeLastHtmlSend + 3000)) { /// Webseite nur alle 3 sekunden aktualisieren (Datenverkehr vermeiden)
sendHtml();
timeLastHtmlSend = myTime;
}
}