Categories
ESP32

Automatische Gartenhaus Belüftung

Nachdem es im Sommer im Gartenhaus etwas warm wird, habe ich mich dazu entschlossen eine automatische Belüftung zu entwickeln. Hier beschreibe ich dieses Projekt.

Problem

Im Gartenhaus wird es im Sommer immer etwas warm. Wenn man dort Pflanzensamen oder ähnliches lagert, ist Hitze diesen nicht besonders zuträglich. Ein Fenster dauerhaft offen lassen, wollte ich nicht. Es musste eine flexible Lösung sein. Zudem sollte die Lösung nicht teuer sein.

Auch wollte ich keine manuelle Lösung, sondern eine, die sich zwar Manuell schalten lässt, aber im Normalbetrieb automatisch abhängig von Innen und Außentemperatur schaltet. Es ist nicht zielführend, wenn es innen bereits kalt ist und durch die Lüftung noch kälter wird oder im umgekehrten Fall draußen wärmer als im Gartenhaus ist und der Lüfter die Wärme ins Gebäude zieht.

Lösung

Da ich noch einige ältere PC-Lüfter in einer Kiste hatte, war die Idee mit diesen eine Belüftung zu bauen. Zudem sollte eine Elektronik Innen- sowie Außentemperatur erfassen und anhand dessen entscheiden, ob der Lüfter aktiv sein soll.

Hardware

Zur Erfassung der Temperatur habe ich mich für wasserdichte DS18B20 Sensoren entschieden, die je einmal innen und einmal außen montiert werden.

Kontrolliert wird das ganze von einem ESP32 mit 30 PINs, der mit dem WLAN verbunden ist und damit aus dem Netzwerk geschaltet, sowie die Temperatur und Zustandswerte ausgelesen werden können.

Zur Lüftung werden zwei unterschiedlich große PC Lüfter hintereinander verwendet, um einen stärkeren Luftstrom zu gewährleisten. Die Luft wird mit der Entlüftung an der Decke nach draußen befördert, nachströmen kann die Luft über die Spalten z.B. in der Tür oder zwischen den Planken.

Lüfterkonstruktion

Die Lüfter müssen in einem Kanal hintereinander gebracht werden. Zudem soll ein “Rückschlag”, wenn der Winddruck bei aktivierter Lüftung zu groß wird, sowie das unbeabsichtigte Lüften bei deaktivierten Lüftern verhindert werden. Auch ein “Stehenbleiben” der Lüfter durch zu viel Gegendruck sollte vermieten werden. Da PC-Lüfter in der Regel keinen Starken Luftdruck erzeugen, müssen die Rückschlagklappen sehr leicht sein.

Grundkörper ineinander gesteckt und mit Draht fixiert: Links wird ein 120mm Lüfter montiert, rechts ein 80mm Lüfter, beim Belüften wird die Luft von rechts nach Links gesaugt.

Da es durch das Hintereinanderschalten von Lüftern zu Verwirbelungen in einem Luftkanal kommt habe ich nachträglich eine Art Blatt oder Flügen eingebaut, um dies zu verhindern.

Grundkörper mit montiertem Flügel, sowie einer “Abdichtung” mit Acryl.
Blick ins Innere des Grundkörpers, in Schwarz der Flügel. Die Lüfter sind hier bereits montiert.

Um auf das DN50 Rohr, welches nach Außen führt, zu kommen, sowie Platz für die Rückschlagklappen zu schaffen, habe ich einen weiteren Körper inklusive einer Art Überwurf gedruckt, damit man den Grundkörper mit dem weiteren Körper verbinden kann.

Zulaufender Körper mit Platz für die Rückschlagklappen.

Die Rückschlagklappen habe ich sehr dünn gedruckt und jeweils an einem Faden mit Sekundenkleber angeklebt.

Innere des zusätzlichen Körpers, in dem die Rückschlagklappen montiert sind. Die Rückschlagklappen sind schwarz, im Bild geschlossen.

Nach einigen Test habe ich festgestellt, dass die Klappen nicht mehr zu fallen, wenn sie senkrecht stehen.

Hier gut zu sehen die linke Klappe steht senkrecht.

Also musste verhindert werden, dass die Klappen ganz auf gehen.

Ein umgedrehtes U verhindert ein 90° aufstellen der Rückschlagklappen.

Hier der ganze nach Montage des Rohrstücks und der Durchführung durch die Gartenhauswand. Das graue Rohrstück ist “im Wasser” der Rest des Aufbaus wird “fallend” montiert, damit die Rückschlagklappen von selbst zu fallen.

Ganzer Aufbau mit Zwischen-Rohrstück, sowie Wand Anschlussstück.

Mit einer gedruckten Halterung an der Wand befestigt, habe ich den Aufbau dann im Gartenhaus montiert.

Innen im Gartenhaus: Vorne zu sehen das Schutzgitter in rot, um den Lüfter etwas zu schützen. Hier noch mit Fixierung zu sehen, da das Schutzgitter eingeklebt ist.
Außenansicht, hinter der Abdeckung befindet sich ein ~90° DN50 Rohrstück.

Die Abdeckung ist genau wie der Rest aus ABS gedruckt und lackiert, um der Witterung besser Stand zu halten.

Außen, sowie Innensensor sind mit etwas Abstand zur Wand montiert.

Innensensor rechts oben
Außensensor auf der Nordseite unter der Dachkante

Elektronik

Die PC-Lüfter benötigen 12V, ein Microcontroller des Type ESP32 kann jedoch nur 3.3V, sowie nur kleine Ströme liefern. Aus diesem Grund habe ich mich dazu entschieden die Lüfter mit dem ESP32 über einen P-MOSFET zu schalten. Da der MOSFET im Idealfall für Sperren/Schalten entsprechend die Versorgungsspannung von 12V benötigt, wird er MOSFET über einen am ESP32 angeschlossenen Transistor geschaltet.

Hier die Platine dazu (Afillate): https://aisler.net/p/TAHNLBDW

Bauteile

  • C1, C2, C3 : 10nF
  • IC1 : RECOM R-78E50-05
  • L1 : 10 µH
  • LED1, LED2 : Standard LEDs
  • R1, R2, R3, R8, R9 : 5.6kO
  • R5 : 1kO
  • R6, R7 : 10kO
  • T1 : BC547C
  • MOSFET : IRF5210

Software

Teile des Codes, den ich verwende werde ich hier veröffentlichen.

Für das regelmäßige Auslesen der Sensoren, des freien RAMs und das prüfen der benötigten Belüftung verwende ich den Task Scheduler.

Als Interface verwende ich einerseits eine Web-Darstellung, sowie eine JSON Schnittstelle.

Als optimale Innentemperatur habe ich 18°C angegeben, nur wenn die Außentemperatur mindestens 1.5°C näher an der Zieltemperatur liegt, wird die Belüftung aktiviert.

Man kann die Belüftung sowohl über Taster, wie auch über das Webinterface in den manuellen Modus umstellen.

Außerdem verbindet sich der ESP32 nach getrennter WLAN Verbindung erneut automatisch.

Der ESP32 gibt über die onboard LED entsprechend Rückmeldung. Beispielsweise, wenn Sensoren nicht gefunden werden bzw. diese ungültige Werte liefern.

#include <OneWire.h>
#include <DallasTemperature.h>

#include <TaskScheduler.h>
#include <ArduinoJson.h>

#include "WiFi.h"
#include <WebServer.h>

#include <NTPClient.h>
#include <WiFiUdp.h>

[...]


WebServer server(80);
StaticJsonDocument<1024> jsonDocument;

// --------- SCHEDULER BEGIN -------
void readTemperatures();
Task scheduleRead(5000, TASK_FOREVER, &readTemperatures);

void controlAir();
Task scheduleControl(30000, TASK_FOREVER, &controlAir);

void checkFreeRam();
Task scheduleCheckFreeRam(60000, TASK_FOREVER, &checkFreeRam);

Scheduler runner;

// --------- SCHEDULER END ---------

// --------- PIN BEGIN -------------
const int morsePin = 2;
const int innerSensorPin = 32;  
const int outerSensorPin = 33;    
const int airControlPin = 25;
const int manualControlPin = 26;
const int manualControlSwitchPin = 27;
const int manualOnOffSwitchPin = 13;
// --------- PIN END ---------------

// --------- PIN BEGIN -------------
const int morsePin = 2;
const int innerSensorPin = 32;  
const int outerSensorPin = 33;    
const int airControlPin = 25;
const int manualControlPin = 26;
const int manualControlSwitchPin = 27;
const int manualOnOffSwitchPin = 13;
// --------- PIN END ---------------

// ------- DEFINITIONS BEGIN -------
static String linkColorNormal = "#2321B0";
static String linkColorVisited = "#2321B0";
static String activeMarkerBegin = "<b>&raquo;";
static String activeMarkerEnd = "&laquo;</b>";
static float tempInnerOpti = 18;
static float tempDiffToChange = 1.5;
// ------- DEFINITIONS END ----------

// --------- VARS BEGIN ------------
bool airControlState = false;
bool airControlManual = false;
// --------- VARS END --------------

// Setup a oneWire instance to communicate with any OneWire devices
OneWire oneWireInner(innerSensorPin);
OneWire oneWireOuter(outerSensorPin);

// Pass our oneWire reference to Dallas Temperature sensor 
DallasTemperature innerSensor(&oneWireInner);
DallasTemperature outerSensor(&oneWireOuter);

float innerTemperature = 0;
float outerTemperature = 0;

void setup() 
{

  [...]

  // ---------- OUTPUT PIN BEGIN ----------
  pinMode(airControlPin, OUTPUT);
  digitalWrite(airControlPin, LOW);
  
  pinMode(manualControlPin, OUTPUT);
  digitalWrite(manualControlPin, LOW);

  pinMode(morsePin, OUTPUT);
  digitalWrite(morsePin, HIGH);
  // ---------- OUTPUT PIN END ------------

  // Start the DS18B20 sensor
  innerSensor.begin();
  outerSensor.begin();

  delay(100);
  // ---------- WEBSERVER BEGIN -----
  server.on("/", handleConnect);
  server.on("/auto", handleAuto);
  server.on("/manual", handleManual);
  server.on("/on", handleOn);
  server.on("/off", handleOff);
  server.on("/jsonstatus", sendJsonStatus);
  server.on("/jsondoaction", jsonDoAct);
  server.on("/jsondoaction", HTTP_POST, jsonDoAct);
  server.begin();
  Serial.println("HTTP server started");
  // ---------- WEBSERVER END -------

  delay(100);
  runner.init();
  
  runner.addTask(scheduleRead);
  scheduleRead.enable();

  runner.addTask(scheduleControl);
  scheduleControl.enable();

  runner.addTask(scheduleCheckFreeRam);
  scheduleCheckFreeRam.enable();



  digitalWrite(morsePin, LOW);
  
}


void loop() 
{
  unsigned long currentMillis = millis();
  // if WiFi is down, try reconnecting every CHECK_WIFI_TIME seconds
  if ((WiFi.status() != WL_CONNECTED) && (currentMillis - previousMillis >=interval))
    {
    Serial.print(millis());
    Serial.println("Reconnecting to WiFi...");
    WiFi.disconnect();
    WiFi.reconnect();
    previousMillis = currentMillis;
  }
  
  server.handleClient();
  runner.execute();
  handleButtons();
  //delay(5000);

}

void checkFreeRam()
{
  if (ESP.getFreeHeap() < 60000)
  {
    ESP.restart();
  }

  Serial.println("Free RAM: " + String(ESP.getFreeHeap()));
}

void handleConnect()
{
  Serial.println("WebServer: /");
  server.send(200, "text/html", SendHTML("")); 
}

void handleButtons()
{
  int manualControlSwitchSelect = digitalRead(manualControlSwitchPin);
  int manualOnOffSwitchSelect = digitalRead(manualOnOffSwitchPin);
  
  if (manualControlSwitchSelect == 1 || manualOnOffSwitchSelect == 1)
  {
    delay(20);
    manualControlSwitchSelect = digitalRead(manualControlSwitchPin);
    manualOnOffSwitchSelect = digitalRead(manualOnOffSwitchPin);
    delay(20);

    if (manualControlSwitchSelect == 1)
    {
      digitalWrite(morsePin, HIGH);
      if (airControlManual == true)
      {
        disableManual();
      }
      else
      {
        enableManual();
      }
      delay(200);
      digitalWrite(morsePin, LOW);
    }
    else if (manualOnOffSwitchSelect == 1)
    {
      if (airControlManual == true)
      {
        digitalWrite(morsePin, HIGH);
        if (airControlState == true)
        {
          disableAir();
        }
        else
        {
          enableAir();
        }
        delay(200);
        digitalWrite(morsePin, LOW);
      }
      else
      {
        digitalWrite(morsePin, HIGH);
        delay(20);
        digitalWrite(morsePin, LOW);
        delay(20);
        digitalWrite(morsePin, HIGH);
        delay(20);
        digitalWrite(morsePin, LOW);
        delay(20);
        digitalWrite(morsePin, HIGH);
        delay(20);
        digitalWrite(morsePin, LOW);
        delay(20);
        digitalWrite(morsePin, HIGH);
        delay(20);
        digitalWrite(morsePin, LOW);
        delay(20);
        digitalWrite(morsePin, HIGH);
        delay(20);
        digitalWrite(morsePin, LOW);
        delay(20);
      }
    }

    delay(400);
  }

}

void handleAuto()
{
  Serial.println("WebServer: /auto");

  disableManual();

  server.sendHeader("Location", String("/"), true);
  server.send (302, "text/plain", "");
}

void handleManual()
{
  Serial.println("WebServer: /manual");

  enableManual();

  server.sendHeader("Location", String("/"), true);
  server.send (302, "text/plain", "");
}

void handleOn()
{
  Serial.println("WebServer: /on");

  enableAir();

  server.sendHeader("Location", String("/"), true);
  server.send (302, "text/plain", "");
}

void handleOff()
{
  Serial.println("WebServer: /off");

  disableAir();

  server.sendHeader("Location", String("/"), true);
  server.send (302, "text/plain", "");
}

void sendJsonStatus()
{
  Serial.println("JSON Status");

  String jsonBuffer = createStatusJson("");

  server.send(200, "application/json", jsonBuffer); 
}

void jsonDoAct()
{
  Serial.println("JSON Act");

  if (server.hasArg("plain") == false) 
  {
    //handle error here
  }
  
  String body = server.arg("plain");
  Serial.println(body);
  deserializeJson(jsonDocument, body);

  if (jsonDocument.containsKey("manual") == true)
  {
    int manual = jsonDocument["manual"];
    Serial.println("JSON Act - Manual Set to " + (String)manual);
    if (manual == 1)
    {
      enableManual();
    }
    else if (manual == 0)
    {
      disableManual();
    }
  }

  if (airControlManual == true)
  {
    if (jsonDocument.containsKey("fanenabled") == true)
    {
      int fanenabled = jsonDocument["fanenabled"];
      Serial.println("JSON Act - fanenabled Set to " + (String)fanenabled);
      if (fanenabled == 1)
      {
        enableAir();
      }
      else if (fanenabled == 0)
      {
        disableAir();
      }
    }
  }

  controlAir();

  String jsonBuffer = createStatusJson("");

  server.send(200, "application/json", jsonBuffer); 
}

String createStatusJson(String statusIn) 
{
  char jsonBuffer[1024];
  
  if (statusIn == "")
  {
    statusIn = "OK";
  }
  
  jsonDocument.clear();  
  jsonDocument["state"] = statusIn;
  jsonDocument["innertemp"] = innerTemperature;
  jsonDocument["outertemp"] = outerTemperature;

  if (airControlState == true)
  {
    jsonDocument["fanenabled"] = true;
  }
  else
  {
    jsonDocument["fanenabled"] = false;
  }

  if (airControlManual == true)
  {
    jsonDocument["manual"] = true;
  }
  else
  {
    jsonDocument["manual"] = false;
  }

  int freeram = ESP.getFreeHeap();
  jsonDocument["freeram"] = freeram;

  serializeJson(jsonDocument, jsonBuffer);

  return String(jsonBuffer);
}


String SendHTML(String context){
  String ptr = "<!DOCTYPE html> <html>\n";
  ptr +="<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr +="<title>ESP32 Air Control</title>\n";
  ptr +="<style>html { font-family: Arial; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr +="body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;} h3 {color: #444444;margin-bottom: 50px;}\n";
  ptr +=".button {display: block;width: 80px;background-color: #3498db;border: none;color: white;padding: 13px 30px;text-decoration: none;font-size: 25px;margin: 0px auto 35px;cursor: pointer;border-radius: 4px;}\n";
  ptr +="a, a:active, { color: " + linkColorNormal + "; text-decoration: underline; }\n";
  ptr +="a:visited { color: " + linkColorVisited + "; text-decoration: underline; }\n";
  ptr +="p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr +="</style>\n";
  ptr +="</head>\n";
  ptr +="<body>\n";
  ptr +="<h1>ESP32 Air Control</h1>\n";

  if (airControlManual == true)
  {
    ptr +="<h2>Manual Control - ";
  }
  else
  {
    ptr +="<h2>Auto Control - ";
  }

  if (airControlState == true)
  {
    ptr +="Air: On";
  }
  else
  {
    ptr +="Air: Off";
  }
  
  ptr += "</h2>";
  

  ptr +="<p>Inner Temp: " + String(innerTemperature) + " &deg;C</p>";
  ptr +="<p>Outer Temp: " + String(outerTemperature) + " &deg;C</p>";
  
  ptr +="<br><br>";

  if (airControlManual == true)
  {
    ptr +="<a href=\"/auto\">Enable Auto Control</a><br><br>";

    String textToAdd = "<a href=\"/on\">Enable Air</a>";
    String lineBreak = "<br><br>";
    
    if (airControlState == true)
    {
      ptr += activeMarkerBegin + textToAdd + activeMarkerEnd + lineBreak;
    }
    else
    {
      ptr += textToAdd + lineBreak;
    }

    textToAdd = "<a href=\"/off\">Disable Air</a>";
    if (airControlState == false)
    {
      ptr += activeMarkerBegin + textToAdd + activeMarkerEnd + lineBreak;
    }
    else
    {
      ptr += textToAdd + lineBreak;
    }
  }
  else
  {
    ptr +="<a href=\"/manual\">Disable Auto Control</a><br><br>";
  }

  ptr +="</body>\n";
  ptr +="</html>\n";
  return ptr;
}

void controlAir()
{
  if (innerTemperature == -127 || outerTemperature == -127)
  {
    errorBlink();
  }
  else
  {
    if (airControlManual == false)
    {
      if (innerTemperature <= tempInnerOpti && outerTemperature >= (innerTemperature+tempDiffToChange)) // innen zu kalt
      {
        enableAir();
      }
      else if (innerTemperature >= tempInnerOpti && (outerTemperature+tempDiffToChange) <= innerTemperature) // innen zu heiß
      {
        enableAir();
      }
      else
      {
        disableAir();
      }
    }
  }
}



void enableManual()
{
  digitalWrite(manualControlPin, HIGH);
  airControlManual = true;
}

void disableManual()
{
  digitalWrite(manualControlPin, LOW);
  airControlManual = false;
}

void enableAir()
{
  Serial.println("Enable Air!");
  digitalWrite(airControlPin, HIGH);
  airControlState = true;
}

void disableAir()
{
  Serial.println("Disable Air!");
  digitalWrite(airControlPin, LOW);
  airControlState = false;
}

void errorBlink()
{
  digitalWrite(morsePin, HIGH);
  delay(200);
  digitalWrite(morsePin, LOW);
  delay(200);
  digitalWrite(morsePin, HIGH);
  delay(200);
  digitalWrite(morsePin, LOW);
  delay(200);
  digitalWrite(morsePin, HIGH);
  delay(200);
  digitalWrite(morsePin, LOW);
}

void readTemperatures()
{
  innerSensor.requestTemperatures(); 
  innerTemperature = innerSensor.getTempCByIndex(0);
  Serial.print("InnerTemp: ");
  Serial.print(innerTemperature);
  Serial.println("ºC");
  
  outerSensor.requestTemperatures(); 
  outerTemperature = outerSensor.getTempCByIndex(0);
  Serial.print("OuterTemp: ");
  Serial.print(outerTemperature);
  Serial.println("ºC");
}

Hinweis

Dieser Artikel dokumentiert lediglich meinen Aufbau. Für den Nachbau, die Nutzung einzelner Komponenten, die Platinen und den gesamten Inhalt wird die Haftung in jeglicher Form ausgeschlossen.

Categories
ESP32 Mast Raspberry Pi

APRS iGate und WEB-SDR mit Antennen Trennrelais + Gewitterwarner

Seit längerem betreibe ich ein iGate, um APRS-Nachrichten ins Internet ein zu speisen. Ein Bekannter brachte mich auf die Idee auch ein WEBSDR ins Leben zu rufen. Ich habe dann mein iGate verbessert, sowie ein WEBSDR mit automatischen Antennen Trennrelais mit Gewitterwarner gebaut. Dieses Projekt möchte ich hier beschreiben.

WEBSDR

Empfangene APRS Nachrichten

Empfänger

Für mein APRS iGate nutze ich ein NESDR SMART Stick von NooElec. Da die Geräte relativ heiß werden, habe ich mir Gedanken gemacht, wie man diese kühlen könnte. Nach der Demontage des Sticks stellte sich heraus, dass dort noch etwas Platz ist, um eine Schraube zu platzieren, somit habe ich mich entschieden einen Kühlkörper mit Wärmeleitpaste zu befestigen.

SDR Stick mit Kühlkörper

Der Stick ist dann zwar etwas kühler, aber immer noch etwas zu warm für meinen Geschmack. Zudem wollte ich den dazugehörigen Raspberry Pi 3 noch kühlen.

Also entschied ich mich dazu ein Gehäuse mit Lüfter für das WEBSDR, sowie das iGate zu drucken. Als Material habe ich ABS gewählt.

3D Druck Video – https://youtu.be/mmZAZ56P3KI
3D Druck Video – https://youtu.be/5ix60GqjWIw

Im Gehäuse ist Platz für den Raspberry Pi, den Lüfter, sowie den SDR Stick. Der Lüfter, sowie der Raspberry Pi werden mit Schrauben befestigt, der Stick ist nur eingesteckt.

Offenes Gehäuse von oben
Offenes Gehäuse von vorne

Der Raspberry Pi wird über das Mainboard mit Strom versorgt, da ich dort, wo ich das iGate, sowie den WEBSDR aufbauen möchte, bereits eine 5V Spannungsversorgung vorhanden ist. Der Lüfter wird vom Mainboard des Raspberry Pi mit Strom versorgt. Mit Deckel bleiben Raspberry Pi, sowie der SDR Stick relativ kühl. Hier ist allerdings Vorsicht geboten, dass man einen Lüfter wählt, der nicht zu viel Strom verbraucht. Die GPIO PINS des Raspberry Pi dürfen nicht überlastet werden. Der Lüfter ist an +5V sowie GND angeschlossen.

Gewitterwarner

Als Gewitterwarner habe ich den GW1 von ELV gewählt, da dieser über Ausgänge verfügt, die je nach Warn-Lage auf GND gezogen werden. Wenn also Entwarnung aktiv ist, ist der Entwarnungs-PIN des Warners auf 0V und der Rest nicht. Deshalb benötigen wir den Pull UP der Eingänge auf der ESP32 Platine.

Für den Gewitterwarner habe ich wieder ein 3D Druck Gehäuse gezeichnet, aus dem man den Gewitterwarner für dass Ändern der Einstellungen relativ einfach entnehmen kann.

3D Druck Video – https://youtu.be/571t1ngLhKA
Gewitterwarner GW1 mit Halterung

Mast

Für die Antennen habe ich einen Mast montiert und diesen separat geerdet, um Störungen aus dem Haus etwas zu minimieren.

Staberder
Erdung Wandmontage
Mast Wandhalterung
Montierter Mast mit X-50 für das APRS iGate, sowie Big Wheel für das WEBSDR

Trennrelais

Damit ich nicht jedes Mal manuell den Antenneneingang des iGate bzw. WEBSDR entfernen muss, habe ich mich dazu entschlossen mithilfe zweier Relais, dem Gewitterwarner GW1 von ELV, sowie einem ESP32 (mit 30 Pins) eine Abschaltelektronik zu entwickeln.

Die Relais sind so angeschlossen, dass sie bei fehlender Spannung die Antenne auf Masse schließen. So ist auch bei Stromausfall sichergestellt, dass die Antenneneingänge nicht geschaltet sind.

Die Platine mit den Relais sind in einem Aluminiumgehäuse eingebaut. Die Antennenkabel werden direkt auf der Platine aufgelötet, um unnötige Übergangswiderstände zu vermeiden. Die Platine mit dem ESP32 ist außen am Alugehäuse angebracht. Im Aluminiumgehäuse habe ich eine kleine 3D gedruckte Plattform eingeschraubt, auf der die Platine aufgeschraubt wird, um Kurzschlüsse mit dem Aluminiumgehäuse zu vermeiden. Das Aluminiumgehäuse hat die Maße: 100 x 160 x 81 mm, ein kleines hätte mit Sicherheit auch gut funktioniert, in größeren Gehäusen lässt es sich besser arbeiten.

3D Druck Video – https://youtu.be/Imv8_j-KqeY
3D Druck Video – https://youtu.be/MYv9ufbvdCk
Aluminiumgehäuse mit Platinen Halter, sowie ESP32 Gehäuse

Die Schaltung wird mit 12V versorgt. Die Platinen habe ich zur Verfügung gestellt, sie können direkt bei Aisler bestellt werden:

Relais Platine (Afillate): https://aisler.net/p/OUAABZDY

ESP32 Platine (Affilate): https://aisler.net/p/FCIBCYRQ

Bauteile – Relais Platine

  • Relais Sockel: OMRON P2R-087P
  • Relais1, Relais 2: OMRON G2R-2-S-DC12(S)
  • D1, D2: 1N 4007 Gleichrichterdiode

Bauteile – ESP32 Platine

Platine mit aufgelöteten Bauteilen
  • C1, C2, C3: 10nF
  • D1, D2: LED Rot
  • F1, F2: IRF5210 MOSFET
  • L1: 10µH
  • R1, R2: 1kO
  • R3, R4, R5, R6, R7, R9, R10, R11: 5.6kO
  • R8: 2.2kO
  • R12, R13: 10kO
  • T1, T2, T3 Transistoren: BC547C
  • DC-Wandler: RECOM R-78E50-05
  • Bei Verwendung des GW1 Gewitterwarners muss bei PULL die 3V3 Verbindung gebrückt werden. NIEMALS; GND-Brücke und 3V3-Brücke gleichzeitig brücken!

Montage

Als Coax-Kabel habe ich Aircell 7 verwendet. Montiert sieht das offene Gehäuse mit den Relais wie folgt aus.

Montierte Abschaltelektronik ohne Gewitterwarner
Komplette Elektronik mit APRS iGate und WEBSDR
Detailbild der Installation

Software

Der ESP32 ist so programmiert, dass er bei Warnung die Relais abschaltet, damit wird die Antenne mit Masse verbunden und die Verbindung zum SDR/iGate unterbrochen. Sobald der Gewitterwarner Entwarnung gibt, werden die Relais wieder geschaltet, sodass der Kontakt zur Antenne gegeben ist.

Außerdem ist es möglich vor Ort über die Taster in den manuellen Modus um zu schalten und dann über die beiden anderen Taster die Relais an- und auszuschalten. Damit wird die Automatik, die bei Gewitterwarnung die Relais abschaltet temporär deaktiviert. Diese Funktion steht auch auf einem Webinterface zur Verfügung, da sich der ESP32 ins lokale WLAN einklinkt.

Durch das JSON Interface der Software, lässt sich dieser auch in Smart Home Lösungen integrieren.

Der Code befindet sich unten auf der Seite.

Probleme

Zu Anfang hat Das APRS-iGate kaum Signale empfangen.

Um dem Problem auf die Spur zu kommen, habe ich verschiedene Einstellungen für GAIN bei pymultimonaprs probiert, die jedoch nicht zielführend waren. Also habe ich mir das APRS Signal als Ton auf meinen Rechner gestreamt.

Der Stream kann nach der Installation einiger Komponenten wie folgt geöffnet werden. Möchte man den Stream als root ausführen muss der Erste Befehl einmalig ausgeführt werden:

sed -i 's/geteuid/getppid/' /usr/bin/vlc

rtl_fm -g80 -f 144.8M -M fm -s 22050 - | sox -traw -r22050 -es -b16 -c1 -V1 - -t flac - | cvlc - --sout "#standard{access=http,mux=ogg,dst=RASPBERRYPI-IP-ADRESSE:8080/audio.ogg}"

Der Stream lässt sich beispielsweise über den VLC Player anhören.

Dort habe ich festgestellt, dass ein sehr hoher Rauschanteil vorliegt.

Mit Klappferriten an den LAN- und Strom-Leitungen ließ sich das Problem relativ schnell und gut beheben.

Code

#include "WiFi.h"
#include <WebServer.h>
#include <ArduinoJson.h>


// --------- WIFI -----------
#define STASSID    "WLAN-NAME"
#define STAPSK     "WLAN-PWD"
#define DEVICENAME "ESP32-NETZWERK-NAME";
unsigned long previousMillis = 0;
unsigned long interval = 30000;
// --------- END WIFI -------

const char* ssid = STASSID;
const char* password = STAPSK;
const char* deviceName = DEVICENAME;

WebServer server(80);
StaticJsonDocument<250> jsonDocument;
char jsonBuffer[250];

// ------- PINS ----------
static int relais01 = 32; // D32
static int relais02 = 33; // D33
static int manualLed = 26; // D26

static int warnungPin = 34; // D34
static int blitzPin = 35; // D35
static int entwarnungPin = 23; // D23

static int manualSwitch = 19; // D19
static int onOffSwitch1 = 18; // D18
static int onOffSwitch2 = 4; // D4

static int i2cSdaPin = 21;
static int i2cSclPin = 22;
// ------- END PINS ----------

// ------- DEFINITIONS ----------
static int morsePin = 2;
static int selfCheckPinDuration = 500;
static String relais1name = "Relais Antenna 1";
static String relaisConnected = "Connected";
static String relais2name = "Relais Antenna 2";
static String linkColorNormal = "#2321B0";
static String linkColorVisited = "#2321B0";
// ------- END DEFINITIONS ----------

// ------- VARS ----------
int warnungActive = 0;
int entwarnungActive = 1;
int manualEnabled = 0;
int relais01state = 0;
int relais02state = 0;
int blitzCount = 0;
// ------- END VARS ----------

// Blitz Counter
void IRAM_ATTR eventBlitz()
{
  detachInterrupt(blitzPin);
  blitzCount++;
  if (blitzCount > 100000)
  {
    blitzCount = 0;
  }
  delay(100);
  attachInterrupt(blitzPin, eventBlitz, FALLING);
}

void setup() 
{
  Serial.begin(115200); 
  while(!Serial){} // Waiting for serial connection
  Serial.println();

  delay(2000);

  // WIFI
  Serial.print("Wifi: ");
  Serial.println(ssid);
  
  Serial.println("turn wifi off...");
  WiFi.mode(WIFI_OFF);

  delay(200);
  WiFi.mode(WIFI_STA);
  delay(250);
  
  WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE);    
  delay(200); 
  Serial.println("setting hostname");
  WiFi.setHostname(deviceName);
  delay(200); 
  Serial.println("Connecting to WiFi..");
  WiFi.begin(ssid, password);
  delay(200); 

  Serial.println("Makeing Morse PIN inits");
  pinMode(morsePin, OUTPUT);
  digitalWrite(morsePin, HIGH);
  
  int iCounter = 0;
  int iMax = 30;
  while (WiFi.status() != WL_CONNECTED && iCounter < iMax)
  {
    digitalWrite(morsePin, LOW);
    delay(500);
    digitalWrite(morsePin, HIGH);
    delay(500);
    Serial.print(".");
    iCounter++;
  }
  
  Serial.println("");
  Serial.println(WiFi.localIP()); 
  delay(1000);

  Serial.println("Makeing Output PIN inits");
  pinMode(relais01, OUTPUT);
  digitalWrite(relais01, LOW);
  pinMode(relais02, OUTPUT);
  digitalWrite(relais02, LOW);

  pinMode(manualLed, OUTPUT);

  Serial.println("Makeing Input PIN inits");
  pinMode(manualSwitch, INPUT);
  pinMode(onOffSwitch1, INPUT);
  pinMode(onOffSwitch2, INPUT);
  pinMode(warnungPin, INPUT);
  pinMode(blitzPin, INPUT);
  attachInterrupt(blitzPin, eventBlitz, FALLING);
  pinMode(entwarnungPin, INPUT);

  Serial.println("Selfcheck...");
  
  Serial.println("relais01...");
  setSwitch(1, 1);
  delay(selfCheckPinDuration);
  setSwitch(1, 0);

  Serial.println("relais02...");
  setSwitch(2, 1);
  delay(selfCheckPinDuration);
  setSwitch(2, 0);

  digitalWrite(morsePin, LOW);

  delay(selfCheckPinDuration);
  digitalWrite(morsePin, HIGH);
  delay(selfCheckPinDuration);
  digitalWrite(morsePin, LOW);
 
  delay(selfCheckPinDuration);

  server.on("/", handleConnect);
  server.on("/alloff", handleAllOff);
  server.on("/allon", handleAllOn);
  server.on("/switch1on", handleSwitch1on);
  server.on("/switch1off", handleSwitch1off);
  server.on("/switch2on", handleSwitch2on);
  server.on("/switch2off", handleSwitch2off);
  server.on("/manual", handleSwitchManual);
  server.on("/auto", handleSwitchAuto);
  server.on("/jsonstatus", sendJsonStatus);
  server.on("/jsondoaction", jsonDoAct);
  server.on("/jsondoaction", HTTP_POST, jsonDoAct);  
  server.onNotFound(handleConnect);
  server.begin();
  Serial.println("HTTP server started");
}

void loop() 
{
  server.handleClient();
  handleButtons();
  handleWarner();

  unsigned long currentMillis = millis();
  // WLAN reconnect, falls Verbindung verloren wurde
  if ((WiFi.status() != WL_CONNECTED) && (currentMillis - previousMillis >=interval))
  {
    Serial.print(millis());
    Serial.println("Reconnecting to WiFi...");
    WiFi.disconnect();
    WiFi.reconnect();
    previousMillis = currentMillis;
  }
}

// Allgemeiner Button Handler
void handleButtons()
{
  handleButtonManual();
  if (manualEnabled == 1)
  {
    handleButtonSwitch1();
    handleButtonSwitch2();
  }
}

// Manual/Auto Button Handler
void handleButtonManual()
{
  int manualSelect = digitalRead(manualSwitch);

  if (manualSelect == 1)
  {
    digitalWrite(morsePin, HIGH);
    delay(20);
    manualSelect = digitalRead(manualSwitch);
    delay(100);

    if (manualSelect == 1)
    {
      if (manualEnabled == 1)
      {
        setManual(0);
      }
      else if (manualEnabled == 0)
      {
        setManual(1);
      }
      delay(20);
    }

    digitalWrite(morsePin, LOW);

    delay(200);
  }
}

// Relais 1 Button Handler (wird nur geprüft, wenn manueller Modus aktiviert ist)
void handleButtonSwitch1()
{
  int manualSelect = digitalRead(onOffSwitch1);

  if (manualSelect == 1)
  {
    digitalWrite(morsePin, HIGH);
    delay(20);
    manualSelect = digitalRead(onOffSwitch1);
    delay(100);

    if (manualSelect == 1)
    {
      if (manualEnabled == 1)
      {
        if (relais01state == 0)
        {
          setSwitch(1, 1);
        }
        else if (relais01state == 1)
        {
          setSwitch(1, 0);
        }
      }
      delay(20);
    }

    digitalWrite(morsePin, LOW);

    delay(200);
  }
}

// Relais 2 Button Handler (wird nur geprüft, wenn manueller Modus aktiviert ist)
void handleButtonSwitch2()
{
  int manualSelect = digitalRead(onOffSwitch2);

  if (manualSelect == 1)
  {
    digitalWrite(morsePin, HIGH);
    delay(20);
    manualSelect = digitalRead(onOffSwitch2);
    delay(100);

    if (manualSelect == 1)
    {
      if (manualEnabled == 1)
      {
        if (relais02state == 0)
        {
          setSwitch(2, 1);
        }
        else if (relais02state == 1)
        {
          setSwitch(2, 0);
        }
      }
      delay(20);
    }

    digitalWrite(morsePin, LOW);

    delay(200);
  }
}

// HTTP Seiten Handler
void handleConnect()
{
  Serial.println("Connect");
  server.send(200, "text/html", SendHTML("")); 
}

// HTTP Seiten Handler - Not AUS
void handleAllOff()
{
  Serial.println("Connect");
  Serial.println("EMERGENCY ALL OFF");
  
  setManual(1);
  setSwitch(1, 0);
  setSwitch(2, 0);

  server.sendHeader("Location", String("/"), true);
  server.send (302, "text/plain", "");
  //server.send(200, "text/html", SendHTML("")); 
}

// HTTP Seiten Handler - Not AN
void handleAllOn()
{
  Serial.println("Connect");
  Serial.println("EMERGENCY ALL ON");

  setManual(1);
  setSwitch(1, 1);
  setSwitch(2, 1);

  server.sendHeader("Location", String("/"), true);
  server.send (302, "text/plain", "");
  //server.send(200, "text/html", SendHTML("")); 
}

// HTTP Seiten Handler - Relais 1 AN
void handleSwitch1on()
{
  Serial.println("Connect");
  Serial.println("Switch 1 ON");

  if (manualEnabled == 1)
  {
    setSwitch(1, 1);
  }
  
  server.sendHeader("Location", String("/"), true);
  server.send (302, "text/plain", "");
  //server.send(200, "text/html", SendHTML("")); 
}

// HTTP Seiten Handler - Relais 1 AUS
void handleSwitch1off()
{
  Serial.println("Connect");
  Serial.println("Switch 1 OFF");

  if (manualEnabled == 1)
  {
    setSwitch(1, 0);
  }
  
  server.sendHeader("Location", String("/"), true);
  server.send (302, "text/plain", "");
  //server.send(200, "text/html", SendHTML("")); 
}

// HTTP Seiten Handler - Relais 2 AN
void handleSwitch2on()
{
  Serial.println("Connect");
  Serial.println("Switch 1 ON");
  
  if (manualEnabled == 1)
  {
    setSwitch(2, 1);
  }

  server.sendHeader("Location", String("/"), true);
  server.send (302, "text/plain", "");
  //server.send(200, "text/html", SendHTML("")); 
}

// HTTP Seiten Handler - Relais 2 AUS
void handleSwitch2off()
{
  Serial.println("Connect");
  Serial.println("Switch 1 OFF");

  if (manualEnabled == 1)
  {
    setSwitch(2, 0);
  }
  
  server.sendHeader("Location", String("/"), true);
  server.send (302, "text/plain", "");
  //server.send(200, "text/html", SendHTML("")); 
}

// HTTP Seiten Handler - Manuell AN
void handleSwitchManual()
{
  Serial.println("Connect");
  Serial.println("Switch MANUAL");

  setManual(1);
  
  server.sendHeader("Location", String("/"), true);
  server.send (302, "text/plain", "");
  //server.send(200, "text/html", SendHTML("")); 
}

// HTTP Seiten Handler - Manuell AUS - Automatik Modus AN
void handleSwitchAuto()
{
  Serial.println("Connect");
  Serial.println("Switch AUTO");

  setManual(0);

  server.sendHeader("Location", String("/"), true);
  server.send (302, "text/plain", "");
  //server.send(200, "text/html", SendHTML("")); 
}

// Gewitterwarner Input PINS verarbeiten und entsprechende Maßnahmen im Automatik Modus ergreifen
void handleWarner()
{
  int warnPinVal = digitalRead(warnungPin);
  int entwarnungPinVal = digitalRead(entwarnungPin);
  
  if (warnPinVal == 0) // warnung aktiv
  {
    warnungActive = 1;
  }
  else if (warnPinVal == 1) // warnung INaktiv
  {
    warnungActive = 0;
  }

  if (entwarnungPinVal == 0) // entwarnung aktiv
  {
    entwarnungActive = 1;
  }
  else if (entwarnungPinVal == 1) // entwarnung INaktiv
  {
    entwarnungActive = 0;
  }

  if (manualEnabled == 0)
  {
    if (warnungActive == 1)
    {
        setSwitch(1, 0);
        setSwitch(2, 0);
    }
    else if (entwarnungActive == 1)
    {
        setSwitch(1, 1);
        setSwitch(2, 1);
    }
  }

  if (entwarnungActive == 1 && warnungActive == 0)
  {
    blitzCount = 0;
  }
}

// Setzt den Manuell bzw Automatik Modus
void setManual(int iMan)
{
  if (iMan == 0)
  {
    digitalWrite(manualLed, LOW);
    manualEnabled = iMan;
  }
  else if (iMan == 1)
  {
    digitalWrite(manualLed, HIGH);
    manualEnabled = iMan;
  }
}

// Setzt relais Zustand (es erfolgt keine Prüfung auf Manuell
void setSwitch(int iNum, int iState)
{
  if (iNum == 1)
  {
    if (iState == 0)
    {
      digitalWrite(relais01, LOW);
      relais01state = iState;
    }
    else if (iState == 1)
    {
      digitalWrite(relais01, HIGH);
      relais01state = iState;
    }
  }
  else if (iNum == 2)
  {
    if (iState == 0)
    {
      digitalWrite(relais02, LOW);
      relais02state = iState;
    }
    else if (iState == 1)
    {
      digitalWrite(relais02, HIGH);
      relais02state = iState;
    }
  }
}

// HTML Seite senden
String SendHTML(String context)
{
  String ptr = "<!DOCTYPE html> <html>\n";
  ptr +="<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr +="<title>ESP32 RPI Protect</title>\n";
  ptr +="<style>html { font-family: Arial; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr +="body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;} h3 {color: #444444;margin-bottom: 50px;}\n";
  ptr +=".button {display: block;width: 80px;background-color: #3498db;border: none;color: white;padding: 13px 30px;text-decoration: none;font-size: 25px;margin: 0px auto 35px;cursor: pointer;border-radius: 4px;}\n";
  ptr +="a, a:active, { color: " + linkColorNormal + "; text-decoration: underline; }\n";
  ptr +="a:visited { color: " + linkColorVisited + "; text-decoration: underline; }\n";
  ptr +="p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr +="</style>\n";
  ptr +="</head>\n";
  ptr +="<body>\n";
  ptr +="<h1>ESP32 RPI Protect</h1>\n";

  String lineBreak = "<br><br>";

  ptr += "Warning: ";
  if (warnungActive == 1)
  {
    ptr += "<b>ON</b><br>";
  }
  else if (warnungActive == 0)
  {
    ptr += "<b>OFF</b><br>";
  }

  ptr += "All-Clear: ";
  if (entwarnungActive == 1)
  {
    ptr += "<b>ON</b><br>";
  }
  else if (entwarnungActive == 0)
  {
    ptr += "<b>OFF</b><br>";
  }

  ptr += "Lightning Count: <b>" + String(blitzCount) + "</b><br>";
  
  ptr += lineBreak;

  ptr += "Mode: ";
  if (manualEnabled == 0)
  {
    ptr += "<b><a href=\"/manual\">AUTO</a></b>" + lineBreak;
  }
  else
  {
    ptr += "<b><a href=\"/auto\">MANUAL</a></b>" + lineBreak;
  }

  ptr += "Relais 1: ";
  if (manualEnabled == 1)
  {
    if (relais01state == 0)
    {
      ptr += "<b><a href=\"/switch1on\">OFF</a></b>" + lineBreak;
    }
    else if (relais01state == 1)
    {
      ptr += "<b><a href=\"/switch1off\">ON</a></b>" + lineBreak;
    }
  }
  else
  {
    if (relais01state == 0)
    {
      ptr += "OFF" + lineBreak;
    }
    else if (relais01state == 1)
    {
      ptr += "ON" + lineBreak;
    }
  }

  ptr += "Relais 2: ";
  if (manualEnabled == 1)
  {
    if (relais02state == 0)
    {
      ptr += "<b><a href=\"/switch2on\">OFF</a></b>" + lineBreak;
    }
    else if (relais02state == 1)
    {
      ptr += "<b><a href=\"/switch2off\">ON</a></b>" + lineBreak;
    }
  }
  else
  {
    if (relais02state == 0)
    {
      ptr += "OFF" + lineBreak;
    }
    else if (relais02state == 1)
    {
      ptr += "ON" + lineBreak;
    }
  }

  ptr += "EMERGENCY <a href=\"/allon\"><b>ALL ON</b></a>" + lineBreak;
  ptr += "EMERGENCY <a href=\"/alloff\"><b>ALL OFF</b></a>" + lineBreak;

  ptr +="</body>\n";
  ptr +="</html>\n";
  return ptr;
}

// -------------------- JSON API --------------------------
// Beispielsweise für Home Assitant oder andere Komponenten

void sendJsonStatus()
{
  Serial.println("JSON Status");

  createStatusJson("");

  server.send(200, "application/json", jsonBuffer); 
}

void jsonDoAct()
{
  Serial.println("JSON Act");

  if (server.hasArg("plain") == false) 
  {
    //handle error here
  }
  
  String body = server.arg("plain");
  Serial.println(body);
  deserializeJson(jsonDocument, body);

  if (jsonDocument.containsKey("manual") == true)
  {
    int manual = jsonDocument["manual"];
    Serial.println("JSON Act - Manual Set to " + (String)manual);
    if (manual == 1)
    {
      setManual(1);
    }
    else if (manual == 0)
    {
      setManual(0);
    }
  }

  if (manualEnabled == 1)
  {
    if (jsonDocument.containsKey("relais1") == true)
    {
      int relais1 = jsonDocument["relais1"];
      Serial.println("JSON Act - relais1 Set to " + (String)relais1);
      if (relais1 == 1)
      {
        setSwitch(1, 1);
      }
      else if (relais1 == 0)
      {
        setSwitch(1, 0);
      }
    }
    
    if (jsonDocument.containsKey("relais2") == true)
    {
      int relais2 = jsonDocument["relais2"];
      Serial.println("JSON Act - relais2 Set to " + (String)relais2);
      if (relais2 == 1)
      {
        setSwitch(2, 1);
      }
      else if (relais2 == 0)
      {
        setSwitch(2, 0);
      }
    }
  }

  createStatusJson("");

  server.send(200, "application/json", jsonBuffer); 
}

// -------------------- JSON HELPER --------------------------

void createStatusJson(String statusIn) 
{
  if (statusIn == "")
  {
    statusIn = "OK";
  }
  jsonDocument.clear();  
  jsonDocument["state"] = statusIn;

  if (relais01state == 1)
  {
    jsonDocument["relais1"] = true;
  }
  else
  {
    jsonDocument["relais1"] = false;
  }

  if (relais02state == 1)
  {
    jsonDocument["relais2"] = true;
  }
  else
  {
    jsonDocument["relais2"] = false;
  }

  if (manualEnabled == true)
  {
    jsonDocument["manual"] = true;
  }
  else
  {
    jsonDocument["manual"] = false;
  }

  if (warnungActive == 1)
  {
    jsonDocument["warnung"] = true;
  }
  else
  {
    jsonDocument["warnung"] = false;
  }

  if (entwarnungActive == 1)
  {
    jsonDocument["entwarnung"] = true;
  }
  else
  {
    jsonDocument["entwarnung"] = false;
  }

  jsonDocument["blitzcount"] = blitzCount;
  serializeJson(jsonDocument, jsonBuffer);
}

Hinweis

Dieser Artikel dokumentiert lediglich meinen Aufbau. Für den Nachbau, die Nutzung einzelner Komponenten, die Platinen und den gesamten Inhalt wird die Haftung in jeglicher Form ausgeschlossen.