Konzept
Sensoren
- BME280: Temperatur[°C], Druck[hPa], relative Feuchte [%]
- LDR: Helligkeit, dimensionslos (0..100)
Kommunikation
Das ESP-Board ist mit dem lokalen WLAN verbunden und überträgt die Sensordaten in die Datenbank beim Web-Provider. Dazu wird über den Aufruf einer HTML-Seite ein php-Skript ausgeführt, das die Datenbank befüllt. Der Timestamp des Datensatzes stammt dabei vom DB-Server.
- Aktuelle Daten: ein Datensatz je Minute, gemittelt über 10 Messwerte, max. 360 Datensätze
- Wochendaten: ein Datensatz alle 10 Minuten, über aktuelle Daten je Minute gemittelt, max. 1008 Datensätze (10080min=168h=7d)
- Monatsdaten: ein Datensatz je Stunde, über aktuelle Daten gemittelt, max 744 Datensätze (365,25d)
- Jahresdaten: wie Monatsdaten, aber max. 8766 Datensätze
Visualisierung
Die Daten werden mit php aus der Datenbank ausgelesen und mit jpgraph visualisiert. Über Woody snippets werden die php-Skripte in WordPress eingebunden.
Verbindungen
Schaltung v1
Schaltung v2
Da die Schaltung so einfach ist, habe ich diesmal keine Platine geätzt, sondern direkt auf einer Lochrasterplatine aufgebaut.
Programmierung
- Arduino-IDE für ESP2866
- Brackets für php
- WordPress
Bauteile
- ESP2866
- BME280
- LDR + Widerstand 1-10k, je nach LDR. Unkritisch, da keine physikalische Grösse gemessen werden soll.
- OLED-Display 128 x 64
- Touch-Board oder Taster (Display an/aus, mit 3min Display-TimeOut)
- Platine oder „fliegende Verkabelung“
- Gehäuse
- USB-Kabel
- USB-Netzteil
Display
Skripte & Code:
Vielleicht sind die folgenden Skripte für den einen oder anderen hilfreich – Lesen und Verwendung auf eigene Gefahr! :o) Hinweise und Anmerkungen gerne an mich. Bei den mysql und php-Skripten bitte keinen Herzanfall bekommen.
Arduino
Wetterstation2.07.ino
/******************************************************
Wetterstation mit ESP2866, BME280, LDR, OLED-Display 128x64
Sensoren für Temperatur, Druck, relative Feuchtigkeit und Helligkeit (dimensionslos).
BM_E_280 hat zusätzlich einen Feuchtesensor, BM_P_280 nicht.
Die gesammelten Daten werden via http-GET an php-Skript auf dem Webserver geschickt.
Das Skript legt die Daten in einer mysql-DB in verschiedenen Tabellen ab.
Timestamp stammt vom Server.
Daten Tabelle max. Datensätze
Aktuelle Daten h6_1min 360 6h x 60min
Monatsdaten monat_1h 744 31d x 24h
Wochendaten woche_10min 1008 7d x 24h x 6
Jahresdaten jahr_1h 8766 365,25d x 24h
5-Jahresdaten jahre5_1h 43830 5 x 365,25d x 24h
Wemos-D1-Board D1:SCL(GPIO5), D2:SDA(GPIO4) oder Wire.begin(int sda, int scl)
D5: Touch-Taste für Display on/off, Timeout für Abschaltung
GND---LDR---R---3V3 U=3V3*LDR/(LDR+R)
| Board hat Spannungsteiler von A0 auf ADC von 3V3 auf 1V1
A0 R an LDR-Werte anpassen, unkritisch. R~Wurzel(LDRmin*LDRmax)
(ca. 950..1060hPa min/max in DL)
******************************************************/
#define Version "Wetterstation_2.07"
//===== Eintragen! =======================================================
char *ssid = ""; // SSID des lokalen WLAN
char *password = ""; // Passwort -"-
String DBKEY = ""; // Schlüssel zum Übertragen der Daten zum Server.
// Muss mit $dbkey in tab_insert.php übereinstimmen
//========================================================================
#include <Wire.h>
#include "SparkFunBME280.h"
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
//#include <ArduinoOTA.h> // OTA-Handling, darf nicht auskommentiert werden, sonst wirft es Fehler
#include "SSD1306Ascii.h" // Display
#include "SSD1306AsciiWire.h"
#define headerString "Uptime \tT[°C] \tP[hPa] \tRH[%] \tLDR" //Für serielle Ausgabe
WiFiClient client;
BME280 BME;
char strBuf[20];
String s, hs;
double BME_p, BME_T, BME_RH, LDR;
double st, sp, sf, sl, sth, sph, sfh, slh;
double mint, minp, minf, minl, maxt, maxp, maxf, maxl;
int hi, mi;
char TAB = 0x09;
//----- Display -----
#define DISPLAY_ADDRESS 0x3C //0x3C or 0x3D
SSD1306AsciiWire oled;
const int TouchPin = 14; //für Touch-Taster an D5
const int OLED_TimeOut = 3 * 60; //TimeOut für OLED-Display in Sekunden.
int OLED_Timer = OLED_TimeOut;
volatile boolean Display_on = true;
//-------------------
//===== structs =============================================
struct SensorData {
double t, p, f, l;
String ts, ps, fs, ls;
};
SensorData sensors; //Globale Variable, um mitteln zu können
//===========================================================
void php_tab_insert (String tabname, int maxnum) {
//aktuelle Werte an Tabelle tabname anhängen, falls mehr als maxnum Datensätze vorhanden,
//werden die ersten Datensätze entfernt bis Länge stimmt.
String server = "cuprum.de"; //Als Konstant ganz nach oben ziehen!
String s;
if (client.connect(server, 80)) {
Serial.println("connecting...");
// send the HTTP PUT request:
// GET /wetter/tab_insert.php?tabelle=TABNAME&max=MAXNUM&key=xxxx&temperatur=23.29&druck=998.96&feuchte=33.22&licht=90.00
String s = "GET /wetter/tab_insert.php?tabelle=" + tabname + "&max=";
s += maxnum;
s += "key="; //key
s += DBKEY;
//Daten aus globaler Variable sensors holen
s += "&temperatur=";
s += sensors.t;
s += "&druck=";
s += sensors.p;
s += "&feuchte=";
s += sensors.f;
s += "&licht=";
s += sensors.l;
Serial.println("tab_insert");
Serial.println(s);
client.print(s);
client.println(" HTTP/1.1");
client.print("Host: ");
client.println(server);
client.println("Connection: close");
client.println();
while (client.available()) { //unnötig?
char c = client.read();
Serial.write(c);
//s = client.read();
//Serial.println (c);
}
client.stop();
}
else {
// if you couldn't make a connection:
Serial.println("Verbindung gescheitert.");
Serial.println("Trennung.");
client.stop();
}
} //php_tab_insert
ICACHE_RAM_ATTR void handleTouchInterrupt(){
//Hatte vorher ohne ICACHE_RAM_ATTR funktioniert, jetzt Fehlermeldung
Display_on = !Display_on;
if (Display_on == true){
Serial.println("Display EIN");
OLED_Timer = millis();
oled.clear();
oled.println ("Display EIN");
}
else{oled.clear();
}
}
void setminmax (double v, double &minv, double &maxv){
if (v < minv){minv = v;}
if (v > maxv){maxv = v;}
} //setminmax
//=== Setup =======================================================================
void setup() {
Serial.begin(115200);
Serial.print ("Version: "); Serial.println (Version);
//--- OLED-Display ---
Wire.begin ();
Wire.setClock(100000);
oled.begin(&Adafruit128x64, DISPLAY_ADDRESS);
//oled.set400kHz();
oled.setFont(Callibri15); //Aus Adafruit-Library
oled.clear();
oled.setContrast (100);
oled.println("Version: "); oled.println (Version);
oled.println("Mit WLAN verbinden.");
//--- Touch-Pin ---
pinMode(TouchPin, INPUT);
attachInterrupt(digitalPinToInterrupt(TouchPin), handleTouchInterrupt, RISING);
//--- WLAN ---
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
//Verbindung zum WLAN aufbauen
int counter = 0; //Counter für Timeout
while (WiFi.status() != WL_CONNECTED) {
delay (250);
Serial.print(".");
oled.print(".");
counter++;
if (counter > 100) {
Serial.println("Kein WLAN. Restart!");
ESP.restart();
}
}
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
oled.println(WiFi.localIP());
//----- BME280-Settings -----
BME.settings.commInterface = I2C_MODE;
BME.settings.I2CAddress = 0x76; //oder 0x77
BME.settings.runMode = 3; //Normal mode
BME.settings.tStandby = 0;
BME.settings.filter = 2;
BME.settings.tempOverSample = 1;
BME.settings.pressOverSample = 1;
delay(10); //Für Startup BMP280
Serial.print ("BME280...");
if (!BME.begin()) {
Serial.println("nicht gefunden!");
}
else {
Serial.println("OK.");
}
for (int i = 0; i < 5; i++) { //die ersten fünf Werte verwerfen
BME_p = BME.readFloatPressure() / 100; // [hPa]
BME_T = BME.readTempC();
BME_RH = BME.readFloatHumidity(); //hat BMP nicht
}
//----- Header -----
Serial.println("");
Serial.println(headerString);
oled.clear();
st = 0; sp = 0; sf = 0; sl = 0;
sth = 0; sph = 0; sfh = 0; slh = 0;
hi = 0; mi = 0;
mint = 100; minp = 2000; minf = 101; minl = 101;
maxt = -100; maxp = 0; maxf = 0; maxl = 0;
} //setup
//=== Loop =======================================================================
void loop() {
int sec;
int osec;
sec = millis() / 1000;
osec = sec;
while (osec == sec) {
sec = millis() / 1000; //auf neue Sekunde warten
yield();
}
sec = millis() / 1000;
if ((sec % 60) == 0) { //1 x pro Minute
int ms0 = millis();
//--- Mitteln ---
float v;
v = BME.readTempC(); st += v; sth += v; //setminmax (v, mint, maxt);
v = BME.readFloatPressure() / 100; sp += v; sph += v; //setminmax (v, minp, maxp);
v = BME.readFloatHumidity(); sf += v; sfh += v; //setminmax (v, minf, maxf);
v = getLight(); sl += v; slh += v; //setminmax (v, minl, maxl);
mi += 1;
hi += 1;
//--------------
getSensorData();
php_tab_insert ("h6_1min", 360); //Daten an Server senden
ms0 = millis() - ms0;
}
if ((sec % 600) == 0) { //Alle 10 Minuten
int ms0 = millis();
sensors.t = st/mi; //Gemittelte Daten, statt getSensorData()
sensors.p = sp/mi;
sensors.f = sf/mi;
sensors.l = sl/mi;
php_tab_insert ("woche_10min", 1008); //Daten an Server senden
st = 0; sp = 0; sf = 0; sl = 0;
mi = 0;
ms0 = millis() - ms0;
Serial.print (ms0);
Serial.println (" ms");
}
if ((sec % 3600) == 0) { //Alle 60 Minuten
int ms0 = millis();
sensors.t = sth/hi; //Gemittelte Daten, statt getSensorData()
sensors.p = sph/hi;
sensors.f = sfh/hi;
sensors.l = slh/hi;
//Daten an Server senden
php_tab_insert ("monat_1h", 744); //Diese drei Aufrufe könnten ggf. in ein PHP-Skript
php_tab_insert ("jahr_1h", 8766);
php_tab_insert ("jahre5_1h", 43830);
sth = 0; sph = 0; sfh = 0; slh = 0;
hi = 0;
ms0 = millis() - ms0;
Serial.print (ms0);
Serial.println (" ms");
}
if ((sec % 2) == 0) { //Alle zwei Sekunden Werte seriell ausgeben
s = getSensorString(); // für Ausgabe auf Display
Serial.print (millis() / 1000);
Serial.print (" ");
Serial.println(s);
//----- Display ---
BME_p = BME.readFloatPressure() / 100; // [hPa]
BME_T = BME.readTempC();
BME_RH = BME.readFloatHumidity();
LDR = getLight();
if (millis() > OLED_Timer + 1000*OLED_TimeOut){Display_on = false; oled.clear();}
if (Display_on == true) {
int i;
char c[10];
oled.home();
dtostrf(BME_T, 5, 1, c);
oled.print(c); oled.print(" C ");
dtostrf(BME_RH, 2, 0, c);
oled.print(c); oled.print(" %RH ");
oled.println(); // oled.clearToEOL();
dtostrf(BME_p, 4, 0, c);
oled.print(c); oled.print(" hPa ");
dtostrf(LDR, 4, 1, c);
oled.print(c); oled.print(" L ");
oled.println();
oled.println(millis() / 1000); oled.print(" ");
oled.print(WiFi.localIP());
oled.print(" D:");
if ((OLED_Timer + 1000*OLED_TimeOut - millis())/1000 < OLED_TimeOut){
oled.print((OLED_Timer + 1000*OLED_TimeOut - millis())/1000);}
oled.println(" ");
}
//------------------
}
} //end loop
String getSensorString () {
String s;
//msh = millis(); Serial.print ("Sensoren ");
double t, p, f, l;
t = 0; p = 0; f = 0; l = 0;
int num_samples = 10; // ~ 43ms
for (int i = 0; i < num_samples; i++) {
p += BME.readFloatPressure() / 100; // [hPa]
t += BME.readTempC();
f += BME.readFloatHumidity();
l += getLight();
//yield();
}
t = t / num_samples;
p = p / num_samples;
f = f / num_samples;
l = l / num_samples;
s = dtostrf(t, 4, 1, strBuf); //s = formatSensorString(t, 6, 1);
s += TAB;
s += dtostrf(p, 6, 1, strBuf);
s += TAB;
s += dtostrf(f, 4, 1, strBuf);
s += TAB;
s += dtostrf(l, 4, 1, strBuf);
s.replace(".", ",");
return s;
} //getSensorString
float getLight (){
float l = (1024 - analogRead(A0))/10.23; //auf 0..100 normieren
return l;
}
void getSensorData () {
//Werte in globale Variable sensors speichern
sensors.t = BME.readTempC();
sensors.p = BME.readFloatPressure() / 100; // [hPa]
sensors.f = BME.readFloatHumidity();
sensors.l = getLight();
String s;
s = dtostrf(sensors.t, 5, 1, strBuf); s.replace(".", ",");
sensors.fs = s;
s = dtostrf(sensors.p, 5, 1, strBuf); s.replace(".", ",");
sensors.ps = s;
s = dtostrf(sensors.f, 2, 0, strBuf); s.replace(".", ",");
sensors.fs = s;
s = dtostrf(sensors.l, 5, 1, strBuf); s.replace(".", ",");
sensors.ls = s;
} //getSensorData
PHP (Serverscripte)
Als absoluter Neuling in PHP, mysql und WordPress, musste ich einiges „zusammenbasteln“, um es ans Laufen zu bringen.
db_header.php
<?php
/*
Alle Variablen testen?
if (isset($_GET['aid']) && is_numeric($_GET['aid'])) {
$aid = (int) $_GET['aid'];
} else {
$aid = 1;
}
==
$aid = $_GET['aid']) ?? 1;
*/
$LF = "<br \>";
//----- in include file verlagern -----
$servername = ""; //Anpassen!
$username = "";
$password = "";
$dbname = ""; //muss mit DBKEY aus dem Arduino-Skript übereinstimmen
$mysqli = new mysqli("$servername", "$username", "$password", "$dbname");
// Check connection
if ($mysqli->connect_errno) {
echo "Error: MySQL connection failed:" . $LF;
echo "Errno: [" . $mysqli->connect_errno . "]" . $LF;
echo "Error: [" . $mysqli->connect_error . "]" . $LF;
exit;
}
//echo "Connection OK." . $LF;
function sql_query ($mysqli, $query){
if (!$result = $mysqli->query($query)) {
echo "Error: Query failed to execute:";
echo $LF;
echo "Query: [" . $query . "]" . $LF;
echo "Errno: [" . $mysqli->errno . "]" . $LF;
echo "Error: [" . $mysqli->error . "]" . $LF;
echo mysqli_errno ();
echo $LF;
exit;
}
return $result;
} //sql_query
mysqli_select_db ($dbname);
?>
tab_insert.php
<?php
//----- in include file verlagern -----
//bzw. include 'db_header.php'; weiter oben
$dbkey = 'xxx'; //muss mit DBKEY aus dem Arduino-Skript übereinstimmen
$LF = "<br \>";
$datum = date ("Y-m-d H:i:s", time());
echo $datum . $LF;
$tabelle = $_GET['tabelle'];
$maxcount = $_GET['max'];
$key = $_GET['key'];
if ($key <> $dbkey){
echo "Key ungültig";
exit();} //Script beenden, falls key falsch ist
$temperatur = $_GET['temperatur'];
$druck = $_GET['druck'];
$feuchte = $_GET['feuchte'];
$licht = $_GET['licht'];
echo "$temperatur °C" . $LF;
echo "$druck hPa " . $LF;
echo "$feuchte %RH " . $LF;
echo "$licht L " . $LF;
?>
<?php
include 'db_header.php';
//===== Werte eintragen =====
$query = "INSERT INTO " . $tabelle . " (Date, T, p, RH, L) VALUES ('$datum', '$temperatur', '$druck', '$feuchte', '$licht')";
echo $sql . $LF;
$result = sql_query ($mysqli, $query);
//************
//schenller als "SELECT * FROM .."
$query = "SELECT COUNT(*) AS anzahl FROM " . $tabelle;
echo $sql . $LF;
$result = sql_query ($mysqli, $query);
$row = $result->fetch_assoc();
$count = $row['anzahl'];
echo $count . " Datensätze" . $LF;
//************
//----- falls mehr als $maxcount Einträge dann erste raus -----
//langsam und umständlich
echo "Erstes raus: " . $LF;
$query = "SELECT * FROM " . $tabelle . " ORDER BY DATE";
$result = sql_query ($mysqli, $query);
$count = $result->num_rows;
$geloescht = 0;
/*
//ungefähr so? PDO??? benutzen
$sql = "DELETE FROM " . $tabelle . " WHERE Date = '" . $row['Date'] . "'";
$result = $conn->query ($sql);
while count > maxcount do begin
$row = $result->fetch_assoc();
$sql = "DELETE FROM " . $tabelle . " WHERE Date = '" . $row['Date'] . "'";
$result_x = $conn->query ($sql); //result und result_x gleichzeitig möglich ?
$count = $count - 1;
end
*/
echo "max: " . $maxcount . $LF;
echo "count: " . $count . $LF;
while ($count > $maxcount){
$row = $result->fetch_assoc();
if ($count > $maxcount){
$geloescht = $geloescht + 1;
$query = "DELETE FROM " . $tabelle . " WHERE Date = '" . $row['Date'] . "'";
$result = sql_query ($mysqli, $query);
//print_r($result); echo "<br \>";
$query = "SELECT * FROM " . $tabelle . " ORDER BY DATE";
$result = sql_query ($mysqli, $query);
$count = $result->num_rows;
echo $count . " ";
}
}
echo "<br \>";
echo "Gelöschte Datesätze: " . $geloescht . "<br \>";
$result->free();
$mysqli->close();
//===================================
$conn->close();
echo "Ende";
?>
PHP-Script für die graphische Darstellung, über Woody snippets direkt in WordPress eingebunden.
Auf dem Webserver muss jpgraph installiert werden. Ich habe noch keine Möglichkeiten gefunden die Achsen mit Datumsangaben zu beschriften.
Aufruf über den Shortcode wbcr_php_snippet id=“##“ tabelle=“Tabellenname“
//tab_graph
//$LF = "<br \>";
include ("jpgraph/src/jpgraph.php");
include ("jpgraph/src/jpgraph_bar.php");
include ("jpgraph/src/jpgraph_line.php");
include 'meine/db_header.php';
$TempKorr = 0.0; // !!!!!
$query = "SELECT * FROM " . $tabelle;
$result = sql_query ($mysqli, $query);
if ($result->num_rows === 0) {
echo "ID $aid nicht gefunden";
exit;
}
$count = $result->num_rows;
//echo $count; echo " Datensätze" . $LF;
while ($row = $result->fetch_row()) {$rows[] = $row;}
//echo microtime($asfloat) - $ms . " s" . $LF;
//Datensätze in einzelne Array umsortieren
$num = $count;
$datax = array();
$datayl = array();
$datayp = array();
$datayf = array();
$datayt = array();
$numdatashow = 0;
if ($numdatashow > 0){echo "Ausgabe:" . $LF;}
for ($i=0;$i<$num;$i++){
if ($i < $numdatashow) {echo $i . " "; }
for ($j=0; $j < 5; $j++){
if ($i < $numdatashow) {echo $rows[$i][$j]; echo " ";}
if ($j == 0){$y = $rows[$i][$j]; array_push($datax, strtotime($y)/60); //Minuten
//echo $i . " " . $y/60 . $LF;
} //In Minuten
if ($j == 1){$y = $rows[$i][$j]; array_push($datayt, $y);}
if ($j == 2){$y = $rows[$i][$j]; array_push($datayp, $y);}
if ($j == 3){$y = $rows[$i][$j]; array_push($datayf, $y);}
if ($j == 4){$y = $rows[$i][$j]; array_push($datayl, $y);}
}
if ($i < $numdatashow) {echo $LF;}
}
//Minimum von allen x-Werten (Timestamp) suchen und abziehen
$minx = min($datax);
for ($i = 0; $i < $num; $i++) {$datax[$i] = $datax[$i] - $minx;}
//Temperatur korrigieren + $TempKorr
for ($i = 0; $i < $num; $i++) {$datayt[$i] = $datayt[$i] + $TempKorr;}
$minx = min($datax); $maxx = max($datax);
$mint = min($datayt); $maxt = max($datayt);
$minp = min($datayp); $maxp = max($datayp);
$minf = min($datayf); $maxf = max($datayf);
$minl = min($datayl); $maxl = max($datayl);
echo "Letzte Werte:" . $LF;
echo "<table>";
echo "<thead><tr><th>T[°C]</th><th>p[hPa]</th><th>F[%RH]</th><th>L[]</th></tr></thead>";
echo "<tbody>";
echo "<tr>";
$i = intval(10*$datayt[$num-1]); echo '<th>' . $i/10 . '</th>';
$i = intval(10*$datayp[$num-1]); echo '<th>' . $i/10 . '</th>';
$i = intval(10*$datayf[$num-1]); echo '<th>' . $i/10 . '</th>';
$i = intval(10*$datayl[$num-1]); echo '<th>' . $i/10 . '</th>';
echo "</tr>\n";
echo "</tbody>";
echo "</table>";
switch ($tabelle){
case "h6_1min": $xtitle = "t[min]"; break;
case "woche_10min": $xtitle = "t[10min]"; break;
case "monat_1h": $xtitle = "t[h]"; break;
case "jahr_1h": $xtitle = "t[h]"; break;
case "jahre5_1h": $xtitle = "t[h]"; break;
}
function graph_plot($w, $h, $gtitle, $xtitle, $ytitle, $datay){
$graph = new Graph($w, $h, 0); //($w, $h, "auto");
$graph->SetScale("intint");
$graph->title->Set($gtitle);
$graph->xaxis->SetTitle($xtitle);
$graph->xaxis->SetTitleSide("SIDE_TOP");
$graph->xaxis->SetTitleMargin(-8);
$graph->yaxis->SetTitle($ytitle, "high");
$graph->yaxis->SetTitleSide("SIDE_RIGHT");
$graph->yaxis->SetTitleMargin(12);
$bplott = new LinePlot($datay);
$graph->Add($bplott);
$pngname = $gtitle . ".png";
$graph->Stroke($pngname);
echo "<img src='" . $pngname . "'>" . $LF;
}
graph_plot (800, 200, "Temperatur", $xtitle, "T[°C]", $datayt);
graph_plot (800, 200, "Druck", $xtitle, "p[hPa]", $datayp);
graph_plot (800, 200, "Feuchte", $xtitle, "RH[%]", $datayf);
graph_plot (800, 200, "Helligkeit", $xtitle, "E[]", $datayl);
//echo microtime($asfloat) - $ms . " s" . $LF;
$result->free();
$mysqli->close();
?>