AdSense

Sonntag, 30. August 2020

Home Automation mit Raspberry PI und ESP8266, Teil 2: Ein einfacher Sensor (ESP8266)

(English version) Als ersten Teil möchte ich auf meine Temperatur- und Luftfeuchte-Sensoren eingehen. Ich nutze hierfür einen ESP8266, welcher einen Si7021-Sensor ausliest. Den Si7021 habe ich mit einem 4-poligen Kabel mit dem ESP8266 an den Pins D1-D4 verbunden, wie man es hier im Bild sehen kann:

Zusätzlich muss noch der Pin D0 mit RST verbunden werden, damit das Deep Sleep funktioniert, welches wir noch brauchen werden. Da ich außerdem relativ instabile Handyladegeräte für die Sensoren benutzt habe, habe ich noch einen Kondensator parallel zu G und 3V geschalten. 

Das Ganze kommt dann in ein 3D-gedrucktes Gehäuse, welches man hier auf Thingiverse finden kann: https://www.thingiverse.com/thing:4583759. Mit dem kleinen Bügel wird der Sensor im Gehäuse fixiert, danach kommt der ESP oben drauf und der Deckel fixiert dann alles. An der offenen Stelle ist nun genau genug Platz für den Anschluss des Handyladegeräts.



Der Code ist vergleichbar mit meinem anderen Post zu dem Thema, https://physudo.blogspot.com/2019/07/wlan-thermometer-mit-dem-dht22-und.html. Zuerst wird im Setup-Teil das WLan verbunden, sollte das nicht klappen resettet sich der ESP automatisch nach 100 Sekunden. Außerdem wird der Si7021-Sensor initialisiert.

In Loop wird dann, falls das WLan verbunden ist (ansonsten wird der ESP auch resettet), die Temperatur und Luftfeuchte gemessen. Sollte hier etwas schief gehen wird auch der ESP neu gestartet. Anschließend wird alles per http Request an den Raspberry gesendet, wie im vorigen Post beschrieben (https://physudo.blogspot.com/2020/08/home-automation-mit-raspberry-pi-und.html).

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
const char* ssid     = "WIFI SSID";
const char* password = "WIFI PASSWORD";

#include "Adafruit_Si7021.h"

Adafruit_Si7021 sensor = Adafruit_Si7021();

void setup()
{
  Serial.begin(9600);

  Serial.print("Your are connecting to;");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  int resetCtr = 0;
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    resetCtr ++;
    Serial.print(".");
    if (resetCtr > 200)
    {
      ESP.restart();
    }
  }

  pinMode(D0, WAKEUP_PULLUP);
  pinMode(D3, OUTPUT);
  pinMode(D4, OUTPUT);
  digitalWrite(D3, HIGH);
  digitalWrite(D4, LOW);
  delay(500);

  while (!sensor.begin())
  {
    Serial.println("Did not find Si7021 sensor!");
  }

  digitalWrite(D3, LOW);
}



void loop() {
  if (WiFi.status() != WL_CONNECTED)
  {
    ESP.restart();
  }

  if (WiFi.status() == WL_CONNECTED)
  {
    Serial.println("");
    Serial.println("Your ESP is connected!");
    Serial.println("Your IP address is: ");
    Serial.println(WiFi.localIP());

    digitalWrite(D3, HIGH);
    delay(500);
    float t = sensor.readTemperature();
    Serial.print("Temperature: ");
    Serial.println(t);
    float h = sensor.readHumidity();
    Serial.print("Humidity: ");
    Serial.println(h);
    if (isnan(t))
    {
      t = sensor.readTemperature();
    }
    if (isnan(h))
    {
      h = sensor.readHumidity();
    }
    digitalWrite(D3, LOW);

    if (isnan(t))
    {
      ESP.restart();
    }
    if (isnan(h))
    {
      ESP.restart();
    }
    

        
    HTTPClient http;
    char requestString[255] = "";
    sprintf(requestString, "http://raspberrypi/SendValues.php?name=Temperatursensor_1&split=1&data=%f,%f", t, h);    
    http.begin(requestString);
    int httpCode = http.GET();
    Serial.println(httpCode);

    if (httpCode > 0)
    {
      String payload = http.getString();
      Serial.println(payload);
    }

    http.end();

    ESP.deepSleep(300e6); 
    delay(300000);
    ESP.reset();
  }
  else
  {
    Serial.println("");
    Serial.println("WiFi not connected");
    delay(1000);
  }
}

Home Automation mit Raspberry PI und ESP8266, Teil 1: Die Grundlagen (Raspberry PI)

(English version) Heute möchte ich anfangen, mein Smart Home System zu präsentieren. Ich habe damit Anfang des Jahres angefangen und inzwischen ist es ziemlich groß gewachsen. Aktuell besteht das System aus:

  • 6 Temperatur- und Luftfeuchte-Sensoren innen
  • 3 Temperatur- und Luftfeuchte-Sensoren außen
  • 2 Temperatur- und Luftfeuchte-Sensoren in unseren zwei Terrarien
  • 1 CO2-Sensor
  • 5 schaltbaren Steckdosen
  • 3 schaltbaren Lampen
  • 1 Umschalter für die Eingänge vom Soundsystem
  • Sensoren an Waschmaschine und Trockner
  • Mikrofon und zwei Kameras am 3D-Drucker
  • Dashboard als Website an einem alten Tablet bzw. auf dem Smartphone
  • Telegram-Bot mit automatischen Benachrichtigungen und Kommandos

Die Grundlage für alles ist ein Raspberry PI 4, welcher als Server im WLAN fungiert. Hier werden alle Daten gespeichert, die Website liegt hier und alle Sensoren kommunizieren mit ihm. Auf dem Raspberry PI muss eigentlich nicht viel eingerichtet werden, ich habe den Apache2 Webserver installiert, hier findet man zahllose Tutorials, im Endeffekt ist es aber nur der folgende Befehl:

sudo apt install apache2

Außerdem habe ich den Apache2 für Php installiert, auch dazu gibt es viele Tutorials, der Befehl hierfür wäre:

sudo apt-get install php libapache2-mod-php

Das sind nun eigentlich schon alle Sachen die auf dem Raspberry PI eingerichtet werden müssen. Es empfiehlt sich aber, den Ordner /var/www/html/ als Share freizugeben, dann kann man auch von einem anderen Computer im Netzwerk auf alle Daten und Skripte zugreifen. Außerdem habe ich noch den Ordner /var/www/html/data/ angelegt, hier werden alle gespeicherten Daten abgelegt.

Damit ist schon alles eingerichtet und es kann mit den ersten Skripten losgehen. Hier möchte ich nur das grundlegende Skript zum Senden von Daten posten, alle weitere Modifikationen und Skripte werde ich dann bei den jeweiligen Posts präsentieren. Zum Senden von Daten nutze ich die Datei "SendValues.php", welche über GET-Parameter die folgenden Daten vom Sensor erwartet:

  • "name", also der Name des Sensors, z.B. "Temperatursensor_1"
  • "split", wie die Daten in verschiedenen Dateien aufgesplittet werden. Die Idee ist hier, wenn Sensoren über Jahre hinweg laufen, dass die hier nicht riesige Dateien entstehen, die man dann gar nicht mehr öffnen kann, sondern man z.B. für jeden Monat oder jedes Jahr eine eigene Datei hat. Split 0 bedeutet kein Aufsplitten, 1 bedeutet eine Datei pro Jahr, 2 eine Datei pro Monat und 3 eine Datei pro Tag
  • "data", die eigentlichen Daten, per Komma getrennt. z.B. für einen Temperatur- und Luftfeuchte-Sensor: 24.1,54.6

Das Skript besteht nun aus folgenden Teilen. Zuerst der Header:

<html>
 <head>
  <title>Send Sensor Values</title>
 </head>
 <body>
  <?php 

Danach beginnt der eigentliche Code. Hier werden die Daten gelesen und das aktuelle Datum bestimmt:

$name = htmlspecialchars($_GET["name"]);
$split = htmlspecialchars($_GET["split"]);
$data = htmlspecialchars($_GET["data"]);

$date = gmdate("Y-m-d") . "T" . gmdate("H:i:s.u") . "Z";

Als nächstes wird eine [name]_last.csv-Datei erstellt, welche den aktuellsten Wert hat:

$file = "data/".$name."_last.csv";
$Saved_File = fopen($file, 'w');
fwrite($Saved_File, $date . "," . $data . "\r\n");
fclose($Saved_File);

Als nächstes wird nun das Splitten gemacht. Dazu wird der Dateiname erstellt, [name]_[Jahr]-[Monat]-[Tag].csv. Da ich einen Überlapp zwischen den verschiedenen Dateien haben will erstellt er auch gleich die nächste Datei, also für nächstes Jahr, nächsten Monat, nächsten Tag, und schreibt hier auch den Wert rein, das sorgt dafür, dass wenn ich mir das letzte Jahr plotten möchte, ich nicht nur Werte bis zum 1.1. habe, sondern auch von dem Jahr davor, sodass der Zeitraum von einem Jahr auf jeden Fall in der Datei enthalten ist. $file1 ist die aktelle Datei, $file2 die für den nächsten Zeitraum.

$file = "data/".$name.".csv";

if ($split == "1")
{
  $file = "data/".$name."_" . gmdate("Y") . ".csv";
  $file2 = "data/".$name."_" . gmdate("Y", strtotime("+1 year")) . ".csv";
}
if ($split == "2")
{
  $file = "data/".$name."_" . gmdate("Y-m") . ".csv";
  $file2 = "data/".$name."_" . gmdate("Y-m", strtotime("+1 month")) . ".csv";
}
if ($split == "3")
{
  $file = "data/".$name."_" . gmdate("Y-m-d") . ".csv";
  $file2 = "data/".$name."_" . gmdate("Y-m-d", strtotime("+1 day")) . ".csv";
}

Anschließend wird das nun in die Dateien gespeichert. Falls "split" 0 ist, wird auf das Speichern in die zweite Datei verzichtet, da es ja für den gesamten Zeitraum nur eine Datei gibt.

$Saved_File = fopen($file, 'a');
fwrite($Saved_File, $date . "," . $data . "\r\n");
fclose($Saved_File);
if ($split == "0")
{
}
else
{
  $Saved_File = fopen($file2, 'a');
  fwrite($Saved_File, $date . "," . $data . "\r\n");
  fclose($Saved_File);
}

Und das war auch schon der Code, man muss jetzt noch die ganzen Tags wieder zu machen, und schon ist SendValues.php fertig:

  ?>
 </body>
</html>

Das ist nun eigentlich schon alles, was man auf der Server-Seite benötigt, als nächstes können die Sensoren ihre Werte senden. Zum Ausprobieren kann man das auch über den Browser im Heimnetz testen, dabei sollten die Dateien Temperatursensor_1_last.csv, und noch zwei weitere Dateien mit dem aktuellen und dem kommenden Jahr als Endung erstellt werden:

http://raspberrypi/SendValues.php?name=Temperatursensor_1&split=1&data=25.3,64.59

Für alle, die sich das nicht einzeln kopieren möchten hier nochmal der gesamte Quellcode:

<html>
 <head>
  <title>Send Sensor Values</title>
 </head>
 <body>
  <?php
 
$name = htmlspecialchars($_GET["name"]);
$split = htmlspecialchars($_GET["split"]);
$data = htmlspecialchars($_GET["data"]);

$date = gmdate("Y-m-d") . "T" . gmdate("H:i:s.u") . "Z";

$file = "data/".$name."_last.csv";
$Saved_File = fopen($file, 'w');
fwrite($Saved_File, $date . "," . $data . "\r\n");
fclose($Saved_File);

$file = "data/".$name.".csv";

if ($split == "1")
{
  $file = "data/".$name."_" . gmdate("Y") . ".csv";
  $file2 = "data/".$name."_" . gmdate("Y", strtotime("+1 year")) . ".csv";
}
if ($split == "2")
{
  $file = "data/".$name."_" . gmdate("Y-m") . ".csv";
  $file2 = "data/".$name."_" . gmdate("Y-m", strtotime("+1 month")) . ".csv";
}
if ($split == "3")
{
  $file = "data/".$name."_" . gmdate("Y-m-d") . ".csv";
  $file2 = "data/".$name."_" . gmdate("Y-m-d", strtotime("+1 day")) . ".csv";
}
$Saved_File = fopen($file, 'a');
fwrite($Saved_File, $date . "," . $data . "\r\n");
fclose($Saved_File);
if ($split == "0")
{
}
else
{
  $Saved_File = fopen($file2, 'a');
  fwrite($Saved_File, $date . "," . $data . "\r\n");
  fclose($Saved_File);
}

  ?>
 </body>
</html>

Dienstag, 11. August 2020

Machine Learning für einen Kamera-überwachten 3D-Drucker

(English version) Nach über einem Jahr seit meinem letzten Post möchte ich heute endlich wieder eines meiner Projekte vorstellen. Im letzten Jahr habe ich an einigen Sachen gearbeitet, vor allem dem Upgrade meines Ultimakers 2+ auf 5 verschiedene Farben. Der Post heute tangiert das Thema, ist aber gleichzeitig auch teil meines IoT-Systems: Eine kamerabasierte Überwachung für einen 3D-Drucker mit automatischer Zustandserkennung.

Zuerst zu den Kameras. Ich habe zwei EPS32-Cam module, wie diese programmiert werden ist bereits im Internet ausführlich erklärt. Diese habe ich an meinem 3D-Drucker angebracht, die erste Kamera von oben, mit einem 3D-gedruckten gestell welches man hier herunterladen kann: https://www.thingiverse.com/thing:3899159

Die zweite Kamera is im 3D-Drucker mit Klebeband fixiert und hat unter dem Kameramodul noch einen kleinen 45°-Block um im richtigen Winkel zu stehen.

Die Kameras erzeugen nun etwa alle 3 Minuten ein neues Bild, insofern hat sich inzwischen schon eine gute Menge an Trainingsdaten angesammelt. Die beiden Blickwinkel sehen so aus:



Die Bilder können in verschiedene Kategorien klassifiziert werden:

  1. Leer: Der Druckkopf ist an seiner normalen Position und das Druckbett ist leer
  2. Vorbereiten: Der Druckkopf ist vorne links und der Drucker heizt sich auf
  3. Drucken: Das versteht sich von selbst
  4. Fertig: Der Druckkopf ist an seiner normalen Position und das Druckbett ist nicht leer
  5. Problem: Ein Abstand zwischen Druckbett bzw. Druckobjekt und Düse ist erkennbar
  6. Wartung: Meine Hände sind sichtbar, der Druckkopf ist zerlegt oder nicht an seiner normalen Position
  7. Aus: Alle Lichter sind aus und die Bilder sind schwarz

Kamera 1 kann den Unterschied zwischen Drucken und Problem nicht erkennen, also erkennt diese Kamera die Kategorie "Problem" nicht, Kamera 2 kann nicht zwischen Leer und Fertig unterscheiden, also kann diese Kamera die Kategorie "Fertig" nicht erkennen.

Ich habe die ersten Bilder die ich hatte dann in die 7 Kategorien sortiert und dann ein neuronales Netz darauf trainiert. Meine Architektur des Netzes (in Tensorflow, Python) sieht folgendermaßen aus:

model = models.Sequential()
model.add(layers.Conv2D(32, (5, 5), activation='relu', input_shape=(128, 128, 3)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(32, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(32, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(32, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(32, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(7))

Das Netz ist für einen Input mit 128x128 Pixeln mit jeweils 3 Farbkanälen ausgelegt und besitzt 5 convolutional Layers, welche jeweils ein max Pooling nachgeschaltet haben. Danach gibt es noch ein fully connected Layer mit 64 Neuronen welches dann zum output Layer führt, welches 7 neuronen hat.

Um die Erkennung weiter zu verbessern habe ich ein zweites Netz erstellt, welches Bilder der Größe 64x64 nutzt und daher nur 4 convolutional Layers hat. Die Ergebnisse der Netze in Prozent werden dann addiert (und durch 2 gteilt), dies verbessert die Genauigkeit weiter. Am Ende wird nun das Ergebnis beider Kameras addiert, zusätzlich dazu habe ich noch ein Mikrofon im Drucker, welches über eine Messung des Geräuschlevels erkennt, ob der Lüfter im Druckkopf an ist, sodass ich hier noch einen weiteren Input habe. Das Gesamtergebnis aus beiden Kameras und dem Mikrofon liefert mir dann eine Vorhersage für den Status des Druckers. Wie im vorherigen Post erklärt wird dieses Ergebnis dann an mein IoT-System gesendet, welches mir eine Nachricht schickt, falls

  • Der Drucker seinen Status zu "Problem" ändert
  • Der Drucker seinen Status zu "Fertig" ändert