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.

Categories
ESP8266

Weather Station with Dust Sensor based on ESP8266

Here I want to show you how I built my weather station in my gardenhouse.

Youtube: DK1TEO – Wetterstation mit Feinstaubsensor – Vortrag (Live-Recording)

Problem

  • Weatherdata from the internet are often for a large region
  • Dust: Official sensors are often far away
  • Dist: Official sensor locations are sometimes not good placed

Target

  • Fun
  • Measured values at own location
  • Own measure value post processing

Hardware

Concept at gardenhouse

The red line is the roof, the yellow block is the wall. There will be installed some pipes and a fan to take the air in, which passes the sensors, then the air will be blowed out at the lower pipe.

Intake detail view

There will be installed two 3D printed parts in blue and also an intake fan to blow the air thru the pipes.

Parts – 3D Print

Youtube: Print Pipe Holders

Components

  • ESP8266 NodeMCU
  • BME280 for Temperature, Humidity and Pressure
  • CCS811 for eCO2 and TVOC
  • Nova SDS011 for Dust

PCB

Both PCBs without mounted display. ESP8266 is on the backsite.

PCB for BME280 and CCS811 can be ordered from Aisler here
R6 = 100kO
R7 = 5.6kO

PCB for SDS011 dust sensor can be ordered from Aisler here
R6 = 100kO
R7 = 5.6kO
SDS011 should be powered externally.

All OTHER resistors should be between 4.7kO and 5.6kO.
A reset button and a “Next” button to e.g. enable the display when pressed can be used.
Both boards are deepsleep ready.

Both PCBs with attached components
Youtube: Print PCB Holder

Setting

The Parts are Mounted via Screws to the backplate. I bought a housing from amazon here.
Inner view from the gardenhouse.
Outer view from the gardenhouse.

Software

Concept

ESP8266 acts as client.

The problem is, that the ESP8266 does not know any SSL trusted certs. It is just possible to say “trust all, nothing, this one”. When the SSL-Cert is changed one time per year normally the ESP has to be reflashed one time per year.

The solution is to have single host with a long term self signed certificate where the ESP requests the fingerprint for the real connection.

Development

I use the Arduino IDE with the following libraries:

  • ESP_Adafruit_SSD1306 (Display)
  • NTPClient (Uhrzeit/Datum)
  • Adafruit_Sensor
  • Adafruit_BME280 (Temperatur, Feuchtigkeit, Luftdruck)
  • Adafruit_CCS811 (eCO2, TVOC)
  • SdsDustSensor (SDS011)
  • ESP8266HTTPClient, WiFiClientSecureBearSSL
  • etc.

Code – Weather Sensors

This code is not usable, it is just a compilation of some snippets.

Inlcudes:

#include <Wire.h>
#include <ESP_Adafruit_SSD1306.h>

#include <ESP8266WiFi.h>

#include <ESP8266HTTPClient.h>
#include <WiFiClientSecureBearSSL.h>

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

#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

#include "Adafruit_CCS811.h"

I use multiple arrays to store the data:

String station[] {
  "ESP 1", // name 0
  "", // key 1
  "", // date 2
  "", // temp 3
  "", // hum 4
  "", // press 5
  "", // CO2 6
  "", // TVOC 7
  "", // Volt 1 8
  "", // Türkontakt 1 9
  "", // Türkontakt 2 10
  "", // Türkontakt 3 11
};

int valEnabled[] {
  1, // name 0
  1, // nothing 1
  1, // date 2
  1, // temp 3
  1, // hum 4
  1, // press 5
  1, // CO2 6
  1, // TVOC 7
  0, // Volt 1 8
  0, // Türkontakt 1 9
  0, // Türkontakt 2 10
  0, // Türkontakt 3 11
};

String valNames[] {
  "", // name 0
  "station", // key 1
  "", // date 2
  "TMP-1", // temp 3
  "HUM-1", // hum 4
  "PRES-1", // press 5
  "CO2-1", // CO2 6
  "TVOC-1", // TVOC 7
  "VOLT-1", // Volt 1 8
  "TUER-1", // Türkontakt 1 9
  "TUER-2", // Türkontakt 2 10
  "TUER-3", // Türkontakt 3 11
};

PIN definitions (important if you want to use the PCB):

const int nextButton = 15; // GPIO 15 - D8
const int tuer1Pin = 14;  // GPIO14 -  D5
const int tuer2Pin = 12;  // GPIO12 -  D6
const int tuer3Pin = 13;  // GPIO13 -  D7
// D1 SCL
// D2 SDA

Get sensor values:

void getVoltVal() // at the moment not needed for this usecase
{
  if (valEnabled[8] == 0)
  {
    return;
  }
  
  double analogVal = (double)analogRead(A0);
  double multiplikator = 58.20;  
  double analogVolt = analogVal * (1.0 / 1023.0) * multiplikator;
  station[8] = String(analogVolt);
}

void getCcs811Vals()
{
  if (valEnabled[6] == 0)
  {
    return;
  }
  
  if(ccs.available())
  {
    if(!ccs.readData())
    {
      station[6] = String(ccs.geteCO2());
      station[7] = String(ccs.getTVOC());
    }
    else
    {
      Serial.println("ERROR!");
    }
  }
}

void getBme280Vals()
{
  if (valEnabled[3] == 0)
  {
    return;
  }
  
  station[3] = String(bme.readTemperature());
  station[4] = String(bme.readHumidity());
  station[5] = String(bme.readPressure() / 100.0F);
}

Send values via HTTPS:

void sendSingleVal(int valNumber)
{
  if (valEnabled[valNumber] == 1)
    {
      String curUrl = SendApiIotUrl + "?" + valNames[1] + "=" + station[1] + "&sensor=" + valNames[valNumber] + "&value=" + station[valNumber];
      Serial.println("url: " + curUrl);
      String completeRetVal = sendOneSensorVal(curUrl);
      Serial.println("payload: " + completeRetVal);
    }
}

String sendOneSensorVal(String urlIn)
{
  return webRequest(Fingerprint, urlIn);
}

String webRequest(const char *fingerprint, String URL)
{
  String retStr;

  WiFiClientSecure client;

  client.setFingerprint(fingerprint);
  
  HTTPClient https;

  //Serial.print("[HTTPS] begin...\n");

  if (https.begin(client, URL)) 
  {
    //Serial.print("[HTTPS] GET...\n");
    // start connection and send HTTP header
    int httpCode = https.GET();
  
    // httpCode will be negative on error
    if (httpCode > 0) 
    {
      // HTTP header has been send and Server response header has been handled
      //Serial.printf("[HTTPS] GET... code: %d\n", httpCode);
  
      // file found at server
      if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) 
      {
        String payload = https.getString();
        //Serial.println(payload);
        retStr = payload;
      }
    } 
    else 
    {
      Serial.printf("[HTTPS] GET... failed, error: %s\n", https.errorToString(httpCode).c_str());
      retStr = "error: ";
      retStr += https.errorToString(httpCode).c_str();
    }
  
    https.end();
  }
  else
  {
    Serial.printf("[HTTPS] Unable to connect\n");
    retStr = "ConErr";
  }

  //delete client;

  delay(50);

  return String(retStr);
}

The process how to get the fingerprint from the self signed page is not documented here but can be done by webRequest() function. The static fingerprint is saved in FingerprintSelfSignedCert, the dynamic downloaded fingerprint is saved in the same format but in the Fingerprint variable.

const char FingerprintSelfSignedCert[] = "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX";
char Fingerprint[60];

Code – Dust Sensor

This code is not usable, it is just a compilation of some snippets.

Includes:

#include <Wire.h>
#include <ESP_Adafruit_SSD1306.h>

#include <ESP8266WiFi.h>

#include <ESP8266HTTPClient.h>
#include <WiFiClientSecureBearSSL.h>

#include <Arduino.h>
#include <SdsDustSensor.h>

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

I use multiple arrays to store the data:

String station[] {
  "ESP IoT 1 Dust", // name 0
  "", // key 1
  "", // date 2
  "", // Volt 3
  "", // Kontakt 4
  "", // PM2.5 5
  "", // PM10 6
};

int valEnabled[] {
  1, // name 0
  1, // nothing 1
  1, // date 2
  0, // Volt 3
  0, // Kontakt 1
  1, // PM2.5 5
  1, // PM10 6
};

String valNames[] {
  "", // name 0
  "station", // key 1
  "", // date 2
  "VOLT-1", // Volt 1 2
  "SCHALT", // Kontakt 4
  "PM-2-5", // PM2.5 5
  "PM-10", // PM10 6
};

PIN definitions (important if you want to use the PCB):

const int nextButton = 15; // GPIO 15 - D8
const int kontPin = 14;  // GPIO14 -  D5
const int rxSDS = 12; // GPIO12 - D6
const int txSDS = 13; // GPIO13 - D7
// D1 SCL
// D2 SDA

Get sensor values:

void getSds011Values()
{
  PmResult pm = sds.queryPm();
  if (pm.isOk())
  {
    Serial.print("PM2.5 = ");
    Serial.print(pm.pm25); // float, μg/m3
    station[5] = String(pm.pm25);
    Serial.print(", PM10 = ");
    Serial.println(pm.pm10);
    station[6] = String(pm.pm10);
  }
  else 
  {
    Serial.print("Could not read values from sensor, reason: ");
    Serial.println(pm.statusToString());
    station[5] = "-1";
    station[6] = "-1";
  }
}

Send and fingerprint process works similar to the Weather Station one.

Data Quality

  • General:
    Value influences caused by environment circumstances (e.g. breathe out, bbq, walk by dust, heated surface by the sun)
  • SDS011 (dust):
    Laser based sensor
    Higher values by humidity or fog
  • CCS811 (eCO2, TVOC):
    Sensor has to “burn in” for 48h before use
    eCO2 is not CO2 !

Future

Ideas for the future are:

  • Sensor: Wind direction
  • Sensor: Wind speed
  • Sensor: Garden house inner temperature
  • Solarpanels with battery

Link

Sensor data is available online here.

An API is available by request to get the values from my weather station.

I don’t give a guarantee or assume any liability. If you build this, do it on your own risk and responsibility!

Categories
Raspberry Pi

3D Printed MMDVM Housing

3D Print Housing / Gehäuse

This model is made for the MMDVM which does not include a display on the board itself but it is possible to extend it with a display. Model made by DK1TEO. No liability assumed. Feel free to use this model for whatever you want.

Photos

These photos are from the one I made for my father (DK1REM).
The callsign on the housing is made by a laser engraving machine.

Model Screenshots

Categories
Arduino

Arduino – Cistern Level Measuring

Control unit (left) with measure unit (right)
When the button is pressed the control unit and the measure unit gets powered by the 9V battery
Measure unit in the cistern which sends the data to the control unit via RS-232
Mesure unit
Measure switches
measure switch with sand filled weight
Single measure switch

Categories
Raspberry Pi

Raspberry Pi 2 – Camera and Temperature / Humidity / Pressure Sensor

DHT22 + BME280
DHT22 + BME280
DHT22
Categories
Raspberry Pi

Raspberry Pi – Camera and Temperature / Humidity Sensor

DHT22
DHT22
DHT22
DHT22
DHT22
Categories
Adapter Power Supply

12V Step-Up Box

Categories
Adapter

Coax Connector-Box

Categories
Mast

GFK Antenna-Mast