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.

Link

WEBSDR

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

Categories
Power Supply

12V Car Power Adapter