Categories
ESP32 Mast

SkyCam mit ESP32

Link zum aktuellen Kamerabild: SkyCam bei Heiligenberg in JN47qu

Mittlerweile sind in meinem Gartenhaus allerlei Sensoren installiert. Was allerdings noch fehlt ist eine Art Webcam. Zunächst ist die Frage was man auf einer Webcam sehen möchte. Mich interessiert, wenn ich nicht daheim bin, wie das Wetter zuhause ist. Ob es bewölkt ist, regnet oder der Himmel frei von Wolken ist. Was für mich nicht in Frage kam, ist eine Webcam, mit der man meinen Garten sieht. Warum also nicht eine Webcam, die den Himmel ablichtet. So entstand die Idee eine SkyCam zu bauen. Dafür sollte die Kamera zudem Weitwinkel können, sonst ist das Sichtfeld in den Himmel doch etwas eingeschränkt.

Für mich wichtig war, dass die Kamera günstig und zuverlässig ist. Nicht wichtig waren mir nächtliche Aufnahmen des Sternenhimmels. Ich wollte kein Betriebssystem (wie bei einem RaspberryPi) mit SD-Karte, die kaputt gehen kann, pflegen und ggf. tauschen.

Somit entschloss ich mich ein ESP32 Kameramodul mit Weitwinkel zu nehmen.

Kamera

Als Kamera habe ich einen ES32 Cam verwendet. Das Modul ist relativ günstig und auch in Weitwinkel zu bekommen. Ohne Weitwinkel sieht ein Modul in etwa so aus:

ESP32 CAM mit Standardobjektiv

Platine

Da ich die Kamera auf einem Mast installieren wollte und nicht 5V bis zum Mast hochführen wollte, habe ich mir eine Platine gestaltet, auf dem ein Step-Down Wandler sitzt, der alles zwischen 6 V und ~25V akzeptiert.

Auf der Platine sind die Anschlüsse zum Flashen des ESP32 zudem extrahiert.

https://aisler.net/p/NCICYTNB (Werbelink)

Bestückung:

  • C1, C2, C3 : 10nF
  • L1 : 10 uH
  • DC-Wandler : RECOM R-78E50-05

Mit dem Brücken der Kontakte Flash-EN kann der ESP32 geflasht werden. Mit dem Brücken der Kontakte Step-Down-EN wird der Ausgang des Step-Down Wandlers mit dem Versorgungsspannunsgeingang des ESP32 verbunden. Es ist in jedem Fall zu vermeiden Step-Down-EN gebrückt zu haben und den ESP anderweitig z.B. via Flash-Kabel mit einer weiteren Spannungsquelle, wie beispielsweise einem Computer verbunden zu haben. Dies kann zur Beschädigung führen.

Gehäuse

Danach musste ich mir überlegen in was für ein Gehäuse ich für die Kamera nehmen möchte. Um den Himmel zu beobachten bietet sich ein “Dome” einer Überwachungskamera an. Diese können aus diversen Quellen bezogen werden. Meiner ist aus Acrylglas. Der Innendurchmesser beträgt ~76 mm, der Außendurchmesser ~85mm. Den Unterbau, in dem sich die Kamera mit dem ESP befindet habe ich selbst gestaltet, 3D gedruckt und ganz wichtig; angeschliffen und mit Farbe besprüht, um eine gewisse UV Festigkeit zu erreichen. Als Material habe ich ABS verwendet. Das gedruckte Gehäuse besteht aus zwei Teilen. Einem Oberteil, in das der “Dome” eingepasst wird, in den die Kamera schaut, sowie einem unteren Teil, der auf den Mast gesteckt und fest mit dem oberen Teil verbunden wird.

SkyCam gedrucktes Oberteil Foto 1 – die beiden Öffnungen links und rechts der Kameraöffnung sind für Silikagel damit es trocken bleibt.
SkyCam gedrucktes Oberteil Foto 2 – die Kamera wird mitsamt der Platine von unten in das Oberteil eingepasst.
SkyCam gedrucktes Unterteil Foto 1 – diese Seite wird auf dem Mast gesteckt.
SkyCam gedrucktes Unteritel Foto 2 – Diese Seite wird am Oberteil befestigt.

Im Unterteil habe ich eine Sechskantöffnung verwendet, da ich dort eine Wasserdichte Kabeldurchführung für das Stromversorgungskabel verwende.

Der “Dome” wird mit Fugmasse am Oberteil eingelassen, die beiden Fächer im Oberteil sind für Silikagel Kugeln, damit der “Dome” nicht beschlägt.

Endmontage

Fertig am Mast installierte SykCam
Oberteil mit Silikagel und Tape mit “Löchern” damit die Kugeln nicht bei der Montage am Mast durch die Kuppel fliegen. – Das gesamte Oberteil ist lackiert, der Teil in der Kuppel jedoch NICHT mit Klarlack versehen, um reflexionen zu vermeiden.
Lackiertes Unterteil mit Kabeldurchführung. – Gut zu erkennen ist, dass der Druck ursprünglich blau war und schwarz lackiert wurde.
Montiert auf dem Mast, noch mit Schutzfolie.
Auch die Unterseite (an der das Unterteil mit Schrauben am Oberteil befestigt ist) wurde abgedichtet.

Software

Die Kamera stellt einen HTTP-Server zur Verfügung. Das Kamerabild muss von einem Server im Heimnetzwerk aktiv abgeholt werden. Ich empfehle die Kamera niemals von außerhalb des Heimnetzwerks erreichbar zu machen.

#include <WebServer.h>
#include <TaskScheduler.h>

#define CAMERA_MODEL_AI_THINKER
#include "camera_pins.h"
#include "WiFi.h"
#include "esp_camera.h"
#include "esp_timer.h"
#include "img_converters.h"
#include "Arduino.h"
#include "soc/soc.h"           // Disable brownour problems
#include "soc/rtc_cntl_reg.h"  // Disable brownour problems
#include "driver/rtc_io.h"

#include <FS.h>

// --------- WIFI -----------

#define STASSID    "SSID"
#define STAPSK     "PWD"

#define DEVICENAME "ESP32-SkyCamName"

unsigned long previousMillis = 0;
unsigned long interval = 30000;

// --------- END WIFI -------

// --------- INITS -------

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

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

// --------- END INITS -------

// --------- SCHEDULER BEGIN -------

void checkFreeRam();
Task scheduleCheckFreeRam(21*1000, TASK_FOREVER, &checkFreeRam);

void wifiReconnectCheck();
Task scheduleWifiReconnectCheck(8*60*1000, TASK_FOREVER, &wifiReconnectCheck);

Scheduler runner;

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

// ------- PINS ----------
static int morsePin = 2;
// ------- END PINS ----------

// --------- Variables ---------
int freeHeap = 0;
// --------- END Variables ---------

void setup()
{
  initSerial();
  initWifi();
  initCamera();
  initPinModes();
  initTimeClient();
  initServer();
  initSchedules();
  checkFreeRam();
}

void loop()
{
  server.handleClient();
  runner.execute();
}

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

void initWifi()
{
  Serial.println("WiFi init");
  Serial.print("Wifi: ");
  Serial.println(ssid);
  //Serial.print("WifiPW: ");
  //Serial.println(password);
  
  Serial.println("turn wifi off...");
  WiFi.mode(WIFI_OFF);
  delay(10);
  //WiFi.forceSleepBegin();
  delay(200);
  //WiFi.forceSleepWake();
  WiFi.mode(WIFI_STA);
  delay(250);
  
  WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE);    
  delay(200); 
  //WiFi.mode(WIFI_STA);
  Serial.println("setting hostname");
  WiFi.setHostname(deviceName);
  delay(200); 
  Serial.println("Connecting to WiFi..");
  WiFi.begin(ssid, password);
  delay(200); 

  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(WiFi.localIP()); 
}

void initPinModes()
{
  Serial.println("PIN MODES init");

  pinMode(morsePin, OUTPUT);
  digitalWrite(morsePin, LOW);
}

void initSchedules()
{
  Serial.println("SCHEDULES init");
  
  runner.init();

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

  runner.addTask(scheduleWifiReconnectCheck);
  scheduleWifiReconnectCheck.enable();
}

void initServer()
{
  server.on("/", sendPhoto);
  server.begin();
  Serial.println("HTTP server started");
}

void wifiReconnectCheck()
{
  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;
  }
}

void checkFreeRam()
{
  freeHeap = ESP.getFreeHeap();

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

void initCamera()
{
  Serial.println("Init Camera");
  
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector

  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  //config.xclk_freq_hz = 20000000;
  config.xclk_freq_hz = 10000000;
  config.pixel_format = PIXFORMAT_JPEG; 

  config.frame_size = FRAMESIZE_UXGA;
  config.jpeg_quality = 10;
  config.fb_count = 1;

  // camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) 
  {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }
}

void sendPhoto()
{
  capturePhotoAndSend();
}

// Photo BEGIN
void capturePhotoAndSend() 
{
  camera_fb_t * fb = NULL; // pointer
  bool ok = 0; // Boolean indicating if the picture has been taken correctly

  size_t _jpg_buf_len;
  uint8_t * _jpg_buf;

  Serial.println("Taking a photo...");

  do
  {
    // Take a photo with the camera
    //Serial.println("Taking a photo...");

    fb = esp_camera_fb_get();
    if (!fb)
    {
      Serial.println("Camera capture failed");
      server.send(500, "text/html", "Error 500"); 
      return;
    }

    /*if (fb->len > 0)
    {
      server.send_P(200, "image/jpg", (const char *)fb->buf, fb->len);
      Serial.println("Length: " + (String)fb->len);
      ok = true;
    }*/

    if(fb->format == PIXFORMAT_JPEG)
    {
      //frame2jpg(fb, 100, &_jpg_buf, &_jpg_buf_len);
      server.send_P(200, "image/jpg", (const char *)fb->buf, fb->len);
      ok = true;
    }
    else
    {
      //frame2jpg_cb(fb, 80, jpg_encode_stream, &jchunk)?ESP_OK:ESP_FAIL;
      frame2jpg(fb, 100, &_jpg_buf, &_jpg_buf_len);
      server.send_P(200, "image/jpg", (const char *)_jpg_buf, _jpg_buf_len);
      //server.send_P(req, NULL, 0);
      //fb_len = jchunk.len;
      ok = true;
    }

    esp_camera_fb_return(fb);
  }

  while(!ok);
}

// Photo END

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 Home Assistant Mast

Windgeschwindigkeit und Windrichtung mit ESP32 erfassen

Nachdem ich in meinem Gartenhaus bereits Temperatur, Luftfeuchtigkeit, Luftdruck, Feinstaub und einige andere Werte erfasse ( Weather Station with Dust Sensor based on ESP8266 ) , fehlen mir eigentlich nur noch Niederschlag und Windrichtung, sowie Windgeschwindigkeit.

Der Erfassung der Daten wird über einen ESP32 Mikrocontroller umgesetzt, der diese als JSON bereitstellt, damit ich sie mit HomeAssistant abgreifen und zur Verfügung stellen kann.

Im Gartenhaus sind bereits 12V DC, sowie 5V DC Leitungen verlegt, die Stromversorgung ist damit sichergestellt.

Windrichtung

Für die Erfassung der Windrichtung gibt es verschiedene Sensoren, ich habe mich für einen entschieden, der je nach Richtung verschiedene Spannungen ausgibt. Versorgt wird er mit 12V. Meiner gibt unterteilt in sieben Schritten die Spannungen 0V, 0.8V, 1.4V, 2.1V, 3V, 3,6V, 3,9V aus.

Wichtig zu erwähnen ist, dass man die Gradzahlen, also von-bis welche Spannung ausgegeben wird ausmessen sollte, da die Winkel für die Spannungen nicht gleichgroß sind, bei mir ist beispielsweise der Teil mit 3,9V zirka 90° groß, jedoch der Winkel für 0V nur 40° groß. Für die spätere Richtungsberechnung nehme ich die Mitte als Gradzahl zur Hand.

Da der ESP Spannungen über 3.3V nicht bevorzugt, habe ich mit einem 50/50 Spannungsteiler (2x 5.6kO) die Spannung halbiert, die am ESP32 ankommt.

Windgeschwindigkeit

Bei der Windgeschwindigkeitsmessung habe ich mich für ein Schalenanemometer entschieden. Es gibt sie in verschiedenen Ausführungen und verschiedenen Maxima bei der Messung der Windgeschwindigkeit. Das ist unter anderem auf verschiedene Messtechnologien zurückzuführen. Mein Anemometer kann maximal 70 m/s -> 252 km/h, die hoffentlich nie eintreten werden.

Mein Schalenanemometer wird mit 12V versorgt und generiert über ein NPN-Transistor Low-Hi Signale, die ich mit dem ESP32 auswerte. Bei meinem Anemometer war angegeben, dass eine Umdrehung 20 “Ticks” sind. Es müssen die “Ticks” pro Sekunde gemessen werden und mit dem Auflösungswert multipliziert werden. Meins hat eine Auflösung von 0.0875m/s pro “Tick” das bedeutet; erhalte ich zwei “Ticks” pro Sekunde, liegt eine Windgeschwindigkeit von 0.175 m/s vor.

Hardware

Zunächst stellt sich die Frage, wie die Montage aussehen soll. Windsensoren sollten relativ hoch und frei aufgestellt werden. Da ich am Gartenhaus bereits einen Mast habe, war dieser dafür prädestiniert. Also habe ich mir ein Rohr besorgt, dass sich horizontal zum vertikalen Mast an diesem montierten lässt. Im Rohr habe ich Kerben mit der Flex gemacht und dann 2mm stake Bleche angeschweißt. In der Mitte habe ich ein Loch für die Kabel gebohrt. Das Rohr mit den Stahlblechen habe ich mit Alu-Zink Spray als Rostschutz eingesprüht.

Rohr mit angeschweißten Platten für die Sensormontage. In der Mitte je ein Loch für die Kabel.

Danach habe ich die beiden Sensoren montiert. Als Unterlegscheiben habe ich Teflon Unterlegscheiben verwendet. Wichtig ist, dass der Richtungssensor so montiert ist, dass es bei der Montage am Mast leicht fällt diesen so aus zu richten, dass der Sensor die richtige Richtung detektiert. Da man direkt im Rohr keine Muttern befestigen kann, weil man dort sehr schlecht bis gar nicht hin kommen würde, habe ich Gewinde in das Stahlblech geschnitten, die die Schrauben halten.

Montierter Sensor im Detail, Links und rechts durch geschnittene Gewinde im Blech gehalten, vorne und hinten durch Muttern auf der Unterseite befestigt
Beide Sensoren montiert

Nachdem die Sensoren montiert sind, muss die Technik verstaut werden. Als Mikrocontroller wird ein ESP32 mit eigens gestalteter Platine verwendet. (Den Link zur Platine, sowie dessen Bestückung werden später in diesem Artikel erläutert.) Dazu habe ich ein wasserdichtes Gehäuse mit Kabeldurchführungen erworben, sowie eine Masthalterung, an der das Gehäuse befestigt wird. Zunächst wird das Gehäuse mit Bohrungen, sowie Platinen-Halterung vorbereitet. Die Halterung ist 3D gedruckt und steht zum Download bereit.

3D Modell Screenshot
Montierte Platinen-Halterung mit Kabeldurchführung, unten für das CAT.7 Kabel zum Gartenhaus für die Stromversorgung, links für die beiden Sensoren
Montierte Masthalterung für die Box mit dem Mikrocontroller. Die Löcher für die kleinen Schrauben musste ich selbst in die Masthalterung bohren, danach habe ich wieder Zink-Alu Spray als Rostschutz für die Bohrungen verwendet.

Nachdem die Befestigungen und Boxen für die Technik soweit vorbereitet sind, können wir uns der Platine widmen.

Schaltplan

Die 12V DC vom Gartenhaus werden über einen DC-Wandler zu 5V umgewandelt und dann wird der ESP32 über VIN versorgt. Diese Schaltung klammere ich hier explizit aus.

Der Windrichtungssensor liefert je nach Richtung zwischen 0-3.9V als Spannungen zurück. Über einen Spannungsteiler halbiere ich die Spannung und erfasse sie dann mit dem ESP32 als Analogsignal. Wichtig ist, dass man die ADC2 Kanäle nicht nutzen kann, wenn WLAN aktiviert ist, was der Fall ist. Deshalb müssen wir ADC1 PINs nutzen. Die M-Kontakte im Bild sind Messpunkte auf der Platine, die zur Diagnose mit dem Oszilloskop verwendet werden können.

Spannungsteiler für die Windrichtung mit Kondensator für die Störungsreduktion

Der Windgeschwindigkeitssensor liefert via NPN entsprechende Low-High Rechteck Signale. Da der ESP32 auf das Abfallen oder Ansteigen eines Digital-PINs via Interrupt reagieren kann, wir jedoch keine 12V am ESP32 verarbeiten können oder eben nur einmal mit einer Rauchfahne, wandle ich das 12V Low-High Signal mit einer einfachen Transistorschaltung in ein 3.3V Low-High Signal um.

Einfache Transistorschaltung zur Wandlung des 12V Low-High Signal des Windgeschwindigkeit Messers in ein 3.3V Signal für den ESP32

Wie sich die Geschwindigkeit anhand der Low-High Signale berechnet habe ich bereits am Anfang des Artikels erläutert.

Platine

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

  • ESP32 mit 30 Pins
  • EN-DC -> Brücke um den ESP über den DC-Wandler zu versorgen (im Normalbetrieb) oder eben über USB, wenn nicht gebrückt wurde, interessant für Debug-Zwecke
  • DC-Wandler : RECOM R-78E50-05
  • M1 : Eingangsspannung
  • M2 : Ausgangsspannung DC-Wandler
  • M3 : Windrichtungssensor Ausgang
  • M4 : Windgeschwindigkeitssensor Ausgang
  • M5 : Masse
  • C1, C2, C3, C4 : 10nF
  • L1 : 10µH
  • R1, R2, R3, R4 : 5.6kO
  • T1 : BC547C
Bestückte und montierte Platine in der Halterung im Gehäuse

Endmontage

Sensoren am Mast montiert
Sensoren am Mast montiert – Detailansicht
Links die Box mit dem ESP32 und der Platine, sowie rechts oben das Rohr zu sehen auf dem die Sensoren montiert sind

Home Assistant

Der ESP32 ist so programmiert, dass er einmal pro Minute die Wind-Werte der vergangenen Minute zu einer Aussage zusammenfasst, dazu später. Diese Werte fragen wir alle 30 Sekunden im Home Assistant via REST ab, sowohl in m/s, wie auch in km/h. Der ESP32 liefert bei der Geschwindigkeit das Minimum, Maximum, sowie den Durchschnitt. Bei der Windrichtung ist die Berechnung etwas kompexer, hier wird ein berechneter Wert, sowie der Text, also beispielsweise O (für Ost), SW (für Südwest) etc. geliefert.

rest:
  - scan_interval: 30
    resource: http://<ESP32-IP>/jsondoaction
    sensor:
     - name: "ESP32 Wind - Speed AVG - m/s"
       unit_of_measurement: "m/s"
       value_template: "{{ value_json.windSpeedMinuteAvgMS | float | round(2) }}"
       
     - name: "ESP32 Wind - Speed Min - m/s"
       unit_of_measurement: "m/s"
       value_template: "{{ value_json.windSpeedMinuteMinMS | float | round(2) }}"
       
     - name: "ESP32 Wind - Speed Max - m/s"
       unit_of_measurement: "m/s"
       value_template: "{{ value_json.windSpeedMinuteMaxMS | float | round(2) }}"
       
     - name: "ESP32 Wind - Speed AVG - km/h"
       unit_of_measurement: "km/h"
       value_template: "{{ value_json.windSpeedMinuteAvgKMH | float | round(2) }}"
       
     - name: "ESP32 Wind - Speed Min - km/h"
       unit_of_measurement: "km/h"
       value_template: "{{ value_json.windSpeedMinuteMinKMH | float | round(2) }}"
       
     - name: "ESP32 Wind - Speed Max - km/h"
       unit_of_measurement: "km/h"
       value_template: "{{ value_json.windSpeedMinuteMaxKMH | float | round(2) }}"
       
     - name: "ESP32 Wind - Direction - Degree"
       unit_of_measurement: "°"
       value_template: "{{ value_json.windDirectionCalculatedDegree | int }}"
       
     - name: "ESP32 Wind - Direction - Text"
       value_template: "{{ value_json.windDirectionCalculatedText }}"
       
     - name: "ESP32 Radiation - Free RAM"
       unit_of_measurement: "bytes"
       value_template: "{{ value_json.freeram | int }}"
Home Assistant Screenshot der Live Werte
Home Assistant Screenshot der Histogramme

Software

Windgeschwindigkeit

Beginnen wir mit der Bestimmung der Windgeschwindigkeit; Hier müssen wie die Low-Hi Signale pro Sekunde zählen. Zunächst definieren wir eine statische Variable mit der Auflösung des Geschwindigkeitsmessers, sowie die entsprechenden Arrays:

static double speedResolution = 0.0875; // m/s per tick per second

int oneSecondWindTicks[60];
int oneSecondWindTicksLastMinute[60];

int minuteTicksSum;
double minuteWindSpeedAvgMS; // m/s
double minuteWindSpeedMinMS; // m/s
double minuteWindSpeedMaxMS; // m/s

Dafür registrieren wir ein Interrupt, dass in ein Array für die jeweilige Sekunde die “Ticks” hochzählt:

void IRAM_ATTR eventWindSpeedTick()
{
  int actualSecond = timeClient.getSeconds();
  if (actualSecond > -1 && actualSecond < 60)
  {
    oneSecondWindTicks[actualSecond]++;
  }
}

In der initPinModes() Funktion registrieren wir das Event auf das Fallen des PINs:

pinMode(windSpeedPin, INPUT);
attachInterrupt(digitalPinToInterrupt(windSpeedPin), eventWindSpeedTick, FALLING);

In der changeMinute() Funktion, die über den Scheduler jede Sekunde aufgerufen wird und prüft, ob sich die Minute geändert hat, wird das oneSecondWindTicks in ein genau gleichgroßes Array für die vergangene Minute kopiert, um darauf Berechnungen durch zu führen. Danach wird das aktuelle Array “genullt”.

memcpy(oneSecondWindTicksLastMinute, oneSecondWindTicks, sizeof(oneSecondWindTicks));
for (int i=0; i<60; i++)
{
    oneSecondWindTicks[i] = 0;
}

Um dann in der buildLastMinuteVals() Funktion Minimum, Maximum und Durchschnitt zu ermitteln. Dort wird auch die “Auflösung” des Windgeschwindigkeitssensor relevant, denn diese sagt aus welche Windgeschwindigkeit pro Tick gilt. Die Umrechnung der m/s in km/h wird beim Abruf des JSON getätigt.

// wind speed
int minTickVal = oneSecondWindTicksLastMinute[0];
int maxTickVal = 0;
minuteTicksSum = 0;

for (int i=0; i<60; i++)
{
int workVal = oneSecondWindTicksLastMinute[i];

if (workVal < 1000)
{   
  if (workVal < minTickVal) // set min
  {
	minTickVal = workVal;
  }
  if (workVal > maxTickVal) // set max
  {
	maxTickVal = workVal;
  }

  minuteTicksSum += workVal; // add to sum
}    
}

double avgTickVal = (double)minuteTicksSum / (double)60;

// convert to m/s
minuteWindSpeedAvgMS = avgTickVal * speedResolution;
minuteWindSpeedMinMS = (double)minTickVal * speedResolution;
minuteWindSpeedMaxMS = (double)maxTickVal * speedResolution;

Die Berechnung der Windgeschwindigkeit ist relativ einfach, komplizierter wird es bei der Berechnung der Windrichtung.

Windrichtung

Um auf die Aussage vom Anfang des Artikels zurück zu kommen, hat nicht jeder Teil des Richtungssensors die gleiche Größe (in Grad). Zunächst habe ich den Richtungssensor auf ein Blatt Papier gestellt, markiert wo die Schrauben und das Label sind, dann den Sensor angeschlossen und gemessen von wo bis wo welche Spannung anliegt. Danach habe ich diese Kompassrose (https://de.m.wikipedia.org/wiki/Datei:Kompassrose.svg) aufgeklebt und die Mitte der Bereiche in Grad dazu geschrieben, die als Basis für eine Berechnung dienen. Es sind Augenmaß Werte, kann also durchaus etwas ungenauer aussehen. Hier der Scan meines Papiers (nicht schön aber selten):

Blau eingerahmt die anliegende Spannung, orange markiert die per Augenmaß ermittelte Gradzahl in der Mitte des Bereichs

Danach habe ich am ESP32 die Analogwerte ermittelt, die je Bereich ausgegeben werden, zwischen zwei Analogwerten die Mitte ermittelt und diese als Basis für die Ermittlung der Richtung genommen (größer-gleich). Die Richtung frage ich einmal pro Sekunde hab und schreibe sie an die entsprechende Position im Array die Richtung.

static int diectionAnalogVal[] = {  -1, 128, 463, 880,  1302, 1724, 2143}; // min analog value
// Middle Vals                       0  256  670  1091  1514  1934  2352
static int diectionVals[] =      {  60, 100,  140, 190,  240, 280, 350}; // directions in degree
int directionCountLastMinute[7];
int directionFirst = 0;
int directionSecond = 0;
int directionMostCalculated = 0;

Einmal pro Sekunde rufe ich die Funktion checkDirection() via Task Scheduler auf, die dann das Array entsprechend beschreibt:

void checkDirection()
{
  int actualSecond = timeClient.getSeconds();
  if (actualSecond > -1 && actualSecond < 60)
  {
    int analogVal = analogRead(windDirectionPin);
    int direction = 0;
    for (int i=0; i<7 && (analogVal > diectionAnalogVal[i]); i++)
    {
      direction = diectionVals[i];
    }

    oneSecondWindDirection[actualSecond] = direction;

    Serial.println("WindDirectionAnalogVal: " + (String)analogVal);
    Serial.println("WindDirectionDegreeVal: " + (String)direction);
  }
}

Auch hier wird in der Funktion changeMinute() das aktuelle Array mit den Werten in das Array der vorigen Minuten kopiert, um dann Berechnungen durch zu führen.

memcpy(oneSecondWindDirectionLastMinute, oneSecondWindDirection, sizeof(oneSecondWindDirection));
for (int i=0; i<60; i++)
{
  oneSecondWindDirection[i] = 0;
}

Komplexer wird die Berechnung der gemittelten Windrichtung. Zunächst ermittle ich die beiden am meisten auftretenden Richtungen. Danach ermittle ich welcher Weg (im oder gegen den Uhrzeigersinn) der kürzere ist. Die “Distanz” zwischen den beiden Gradzahlen wird dann ermittelt und durch die Summe der beiden am häufigsten auftretenden Windrichtungen geteilt und dann entsprechend mit einer der beiden multipliziert und dazu addiert bzw. subtrahiert. Ein einfaches Beispiel: 20x 0° und 30x 100° -> 100-0 -> 100 / (20+30) -> 2° je Messung -> 20 x 2° -> 40° -> 0° + 40° -> 40° -> Nord-Ost. Das passiert in der Funktion buildLastMinuteVals() in den Kommentaren hinter den Berechnungen finden sich die Beispiele mit denen man die entsprechenden Bereiche durchläuft:

// wind direction

// prepararion
for(int i=0; i<7; i++)
{
  directionCountLastMinute[i] = 0;
}

directionFirst = -1;
directionSecond = -1;
directionMostCalculated = 0;    

for (int i=0; i<60; i++)
{
  int workVal = oneSecondWindDirectionLastMinute[i];
  
  for (int j=0; j<7; j++)
  {
    if (workVal == diectionVals[j])
    {
      directionCountLastMinute[j]++;
    }
  }
}

// check which is most used
for (int i=0; i<7; i++)
{
  if (directionCountLastMinute[i] > directionCountLastMinute[directionFirst] || directionFirst == -1)
  {
    directionFirst = i;
  }
}

// check which is second most used  
for (int i=0; i<7; i++)
{
  if (i != directionFirst)
  {
    if (directionCountLastMinute[i] > directionCountLastMinute[directionSecond] || directionSecond == -1 )
    {
      directionSecond = i;
    }
  }
}

if (directionFirst == -1)
{
  directionFirst = 0;
}

if (directionSecond == -1)
{
  directionSecond = directionFirst;
}

Serial.println("First direction: " + (String)directionFirst + " / Count: " + (String)directionCountLastMinute[directionFirst]);
Serial.println("Senco direction: " + (String)directionSecond + " / Count: " + (String)directionCountLastMinute[directionSecond]);

if (directionCountLastMinute[directionFirst] > 0 || directionCountLastMinute[directionSecond] > 0) // otherwise we have zero division
{

  int workVal = 0;
  double degreePerCount;

  if (diectionVals[directionFirst] < diectionVals[directionSecond]) // 10 / 100 | 10 / 200 | 10 / 350
  {
    if (diectionVals[directionFirst] + 180 >= diectionVals[directionSecond]) // 10 / 100
    {
      workVal = diectionVals[directionSecond] - diectionVals[directionFirst];
      degreePerCount = (double)workVal / (directionCountLastMinute[directionFirst] + directionCountLastMinute[directionSecond]);
      directionMostCalculated = diectionVals[directionFirst] + (degreePerCount * directionCountLastMinute[directionSecond]);
    }
    else if (diectionVals[directionFirst] + 180 < diectionVals[directionSecond]) // 10 / 200
    {
      workVal = 360 - diectionVals[directionSecond] + diectionVals[directionFirst];
      degreePerCount = (double)workVal / (directionCountLastMinute[directionFirst] + directionCountLastMinute[directionSecond]);
      directionMostCalculated = diectionVals[directionSecond] + (degreePerCount * directionCountLastMinute[directionFirst]);
    }
    else // 10 / 350
    {
      workVal = 180 - diectionVals[directionSecond] + diectionVals[directionFirst];
      degreePerCount = (double)workVal / (directionCountLastMinute[directionFirst] + directionCountLastMinute[directionSecond]);
      directionMostCalculated = diectionVals[directionSecond] + (degreePerCount * directionCountLastMinute[directionFirst]);
    }

  }
  else if (diectionVals[directionFirst] > diectionVals[directionSecond]) // 100 / 10 | 200 / 10 | 350 / 10
  {
    if (diectionVals[directionSecond] + 180 >= diectionVals[directionSecond]) // 100 / 10
    {
      workVal = diectionVals[directionFirst] - diectionVals[directionSecond];
      degreePerCount = (double)workVal / (directionCountLastMinute[directionFirst] + directionCountLastMinute[directionSecond]);
      directionMostCalculated = diectionVals[directionSecond] + (degreePerCount * directionCountLastMinute[directionFirst]);
    }
    else if (diectionVals[directionSecond] + 180 < diectionVals[directionSecond]) // 200 / 10
    {
      workVal = 360 - diectionVals[directionFirst] + diectionVals[directionSecond];
      degreePerCount = (double)workVal / (directionCountLastMinute[directionFirst] + directionCountLastMinute[directionSecond]);
      directionMostCalculated = diectionVals[directionFirst] + (degreePerCount * directionCountLastMinute[directionSecond]);
    }
    else // 350 / 10
    {
      workVal = 180 - diectionVals[directionFirst] + diectionVals[directionSecond];
      degreePerCount = (double)workVal / (directionCountLastMinute[directionFirst] + directionCountLastMinute[directionSecond]);
      directionMostCalculated = diectionVals[directionFirst] + (degreePerCount * directionCountLastMinute[directionSecond]);
    }
  }

  while (directionMostCalculated > 360)
  {
    directionMostCalculated -= 360;
  }
}
else
{
  directionMostCalculated = 0;
}

Die Ermittlung der Texte zu den Gradzahlen ist relativ einfach:

String getWindDirectionDescriptionFromDegree(int degreeIn)
{
  String retStr = "";
  if (degreeIn > -1) // 22.5 / 11.25 / round all
  {
    if (degreeIn >= 349 || degreeIn < 12) // 348.75 / 11.5
    {
      retStr = "N";
    }
    else if (degreeIn >= 12 && degreeIn < 34) // 11.5 / 34
    {
      retStr = "NNO";
    }
    else if (degreeIn >= 34 && degreeIn < 57) // 34 / 56.5
    {
      retStr = "NO";
    }
    else if (degreeIn >= 57 && degreeIn < 79) // 56.5 / 79
    {
      retStr = "ONO";
    }
    else if (degreeIn >= 79 && degreeIn < 102) // 79 / 101.5
    {
      retStr = "O";
    }
    else if (degreeIn >= 102 && degreeIn < 124) // 101.5 / 124
    {
      retStr = "OSO";
    }
    else if (degreeIn >= 124 && degreeIn < 147) // 124 / 146.5
    {
      retStr = "SO";
    }
    else if (degreeIn >= 147 && degreeIn < 169) // 146.5 / 169
    {
      retStr = "SSO";
    }
    else if (degreeIn >= 169 && degreeIn < 192) // 169 / 191.5
    {
      retStr = "S";
    }
    else if (degreeIn >= 192 && degreeIn < 214) // 191.5 / 214
    {
      retStr = "SSW";
    }
    else if (degreeIn >= 214 && degreeIn < 237) // 214 / 236.5
    {
      retStr = "SW";
    }
    else if (degreeIn >= 237 && degreeIn < 259) // 236.5 / 259
    {
      retStr = "WSW";
    }
    else if (degreeIn >= 259 && degreeIn < 282) // 259 / 281.5
    {
      retStr = "W";
    }
    else if (degreeIn >= 282 && degreeIn < 304) // 281.5 / 304
    {
      retStr = "WNW";
    }
    else if (degreeIn >= 304 && degreeIn < 327) // 304 / 326.5
    {
      retStr = "NW";
    }
    else if (degreeIn >= 327 && degreeIn < 349) // 326.5 / 349
    {
      retStr = "NNW";
    }
  }

  return retStr;
}

Die Initialisierung des Servers und Rückgabe der Werte als JSON erwähne ich an dieser Stelle nicht explizit.

Code

#include <ArduinoJson.h>

#include <WebServer.h>

#include <TaskScheduler.h>

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

// --------- WIFI -----------
#define STASSID    "" // wifi name
#define STAPSK     "" // wifi pw

#define DEVICENAME "ESP32-Wind";

unsigned long previousMillis = 0;
unsigned long interval = 2000;

// --------- END WIFI -------

// --------- INITS -------

const char* ssid = STASSID;
const char* password = STAPSK;
const char* deviceName = DEVICENAME;
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "de.pool.ntp.org", 0, 0);
WebServer server(80);
StaticJsonDocument<1024> jsonDocument;
char jsonBuffer[1024];

// --------- END INITS -------

// --------- SCHEDULER BEGIN -------

void checkFreeRam();
Task scheduleCheckFreeRam(21*1000, TASK_FOREVER, &checkFreeRam);

void wifiReconnectCheck();
Task scheduleWifiReconnectCheck(5*1000, TASK_FOREVER, &wifiReconnectCheck);

void refreshTime();
Task scheduleRefreshTime(10*60*1000, TASK_FOREVER, &refreshTime);

void changeMinute();
Task scheduleChangeMinute(1000, TASK_FOREVER, &changeMinute);

void checkDirection();
Task scheduleCheckDirection(1000, TASK_FOREVER, &checkDirection);

Scheduler runner;

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

// ------- DEFINITIONS ----------

static String linkColorNormal = "#2321B0";
static String linkColorVisited = "#2321B0";
static String activeMarkerBegin = "<b>&raquo;";
static String activeMarkerEnd = "&laquo;</b>";

// ------- END DEFINITIONS ----------

// ------- PINS ----------
static int morsePin = 2;
static int i2cSdaPin = 21;
static int i2cSclPin = 22;
static int windSpeedPin = 26;
static int windDirectionPin = 34;
// ------- END PINS ----------

// --------- Variables ---------

int freeHeap = 0;

int actualMin = 0;
int actualTenSecond = 0;

static double speedResolution = 0.0875; // m/s per tick per second

int oneSecondWindTicks[60];
int oneSecondWindTicksLastMinute[60];

int minuteTicksSum;
double minuteWindSpeedAvgMS; // m/s
double minuteWindSpeedMinMS; // m/s
double minuteWindSpeedMaxMS; // m/s

static int diectionAnalogVal[] = {  -1, 128, 463, 880,  1302, 1724, 2143}; // min analog value
// Middle Vals                       0  256  670  1091  1514  1934  2352
static int diectionVals[] =      {  60, 100,  140, 190,  240, 280, 350}; // directions in degree
int directionCountLastMinute[7];
int directionFirst = 0;
int directionSecond = 0;
int directionMostCalculated = 0;

int oneSecondWindDirection[60];
int oneSecondWindDirectionLastMinute[60];

// --------- END Variables ---------

// --------- Interrupt Functions -----------

void IRAM_ATTR eventWindSpeedTick()
{
  int actualSecond = timeClient.getSeconds();
  if (actualSecond > -1 && actualSecond < 60)
  {
    oneSecondWindTicks[actualSecond]++;
  }
}

// --------- END Interrupt Functions -----------

void setup() 
{
  initSerial();
  initWifi();
  initSchedules();
  initPinModes();
  initTimeClient();
  initServer();
  checkFreeRam();
}

void loop() 
{
  // put your main code here, to run repeatedly:
  server.handleClient();
  runner.execute();
}

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

void initWifi()
{
  Serial.println("WiFi init");
  Serial.print("Wifi: ");
  Serial.println(ssid);
  //Serial.print("WifiPW: ");
  //Serial.println(password);
  
  Serial.println("turn wifi off...");
  WiFi.mode(WIFI_OFF);
  delay(10);
  //WiFi.forceSleepBegin();
  delay(200);
  //WiFi.forceSleepWake();
  WiFi.mode(WIFI_STA);
  delay(250);
  
  WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE);    
  delay(200); 
  //WiFi.mode(WIFI_STA);
  Serial.println("setting hostname");
  WiFi.setHostname(deviceName);
  delay(200); 
  Serial.println("Connecting to WiFi..");
  WiFi.begin(ssid, password);
  delay(200); 

  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(WiFi.localIP()); 
}

void initPinModes()
{
  Serial.println("PIN MODES init");

  pinMode(morsePin, OUTPUT);
  digitalWrite(morsePin, LOW);

  pinMode(windSpeedPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(windSpeedPin), eventWindSpeedTick, FALLING);

  analogRead(windDirectionPin);

}

void initSchedules()
{
  Serial.println("SCHEDULES init");
  
  runner.init();

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

  runner.addTask(scheduleWifiReconnectCheck);
  scheduleWifiReconnectCheck.enable();

  runner.addTask(scheduleRefreshTime);
  scheduleRefreshTime.enable();

  runner.addTask(scheduleChangeMinute);
  scheduleChangeMinute.enable();

  runner.addTask(scheduleCheckDirection);
  scheduleCheckDirection.enable();
}

void initTimeClient()
{
  timeClient.begin();
  delay(1000);
  timeClient.update();
}

void initServer()
{
  server.on("/", handleConnect);
  server.on("/jsondoaction", jsonDoAct);
  server.on("/jsondoaction", HTTP_POST, jsonDoAct);  
  server.onNotFound(handleConnect);
  server.begin();
  Serial.println("HTTP server started");
}

void wifiReconnectCheck()
{
  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;
  }
}

void refreshTime()
{
  timeClient.update();
}

void changeMinute()
{
  if (timeClient.getMinutes() != actualMin)
  {
    Serial.println("Changing Minute...");    
    actualMin = timeClient.getMinutes();
    
    memcpy(oneSecondWindTicksLastMinute, oneSecondWindTicks, sizeof(oneSecondWindTicks));

    memcpy(oneSecondWindDirectionLastMinute, oneSecondWindDirection, sizeof(oneSecondWindDirection));

    //Serial.print("Arrays old/new: ");

    for (int i=0; i<60; i++)
    {
      oneSecondWindTicks[i] = 0;
      //Serial.print(" " + (String)oneSecondWindTicksLastMinute[i] + "/" + (String)oneSecondWindTicks[i]);
    }

    for (int i=0; i<60; i++)
    {
      oneSecondWindDirection[i] = 0;
      //Serial.print(" " + (String)oneSecondWindDirectionLastMinute[i] + "/" + (String)oneSecondWindDirection[i]);
    }

    //Serial.println();

    buildLastMinuteVals();
    
  }
}

void buildLastMinuteVals()
{
  // wind speed
  
  int minTickVal = oneSecondWindTicksLastMinute[0];
  int maxTickVal = 0;
  minuteTicksSum = 0;

  for (int i=0; i<60; i++)
  {
    int workVal = oneSecondWindTicksLastMinute[i];

    if (workVal < 1000)
    {   
      if (workVal < minTickVal) // set min
      {
        minTickVal = workVal;
      }
      if (workVal > maxTickVal) // set max
      {
        maxTickVal = workVal;
      }

      minuteTicksSum += workVal; // add to sum
    }    
  }

  double avgTickVal = (double)minuteTicksSum / (double)60;

  // convert to m/s
  minuteWindSpeedAvgMS = avgTickVal * speedResolution;
  minuteWindSpeedMinMS = (double)minTickVal * speedResolution;
  minuteWindSpeedMaxMS = (double)maxTickVal * speedResolution;


  // wind direction

  // prepararion
  for(int i=0; i<7; i++)
  {
    directionCountLastMinute[i] = 0;
  }
  
  directionFirst = -1;
  directionSecond = -1;
  directionMostCalculated = 0;    

  for (int i=0; i<60; i++)
  {
    int workVal = oneSecondWindDirectionLastMinute[i];
    
    for (int j=0; j<7; j++)
    {
      if (workVal == diectionVals[j])
      {
        directionCountLastMinute[j]++;
      }
    }
  }

  // check which is most used
  for (int i=0; i<7; i++)
  {
    if (directionCountLastMinute[i] > directionCountLastMinute[directionFirst] || directionFirst == -1)
    {
      directionFirst = i;
    }
  }

  // check which is second most used  
  for (int i=0; i<7; i++)
  {
    if (i != directionFirst)
    {
      if (directionCountLastMinute[i] > directionCountLastMinute[directionSecond] || directionSecond == -1 )
      {
        directionSecond = i;
      }
    }
  }

  if (directionFirst == -1)
  {
    directionFirst = 0;
  }

  if (directionSecond == -1)
  {
    directionSecond = directionFirst;
  }

  Serial.println("First direction: " + (String)directionFirst + " / Count: " + (String)directionCountLastMinute[directionFirst]);
  Serial.println("Senco direction: " + (String)directionSecond + " / Count: " + (String)directionCountLastMinute[directionSecond]);

  if (directionCountLastMinute[directionFirst] > 0 || directionCountLastMinute[directionSecond] > 0) // otherwise we have zero division
  {

    int workVal = 0;
    double degreePerCount;

    if (diectionVals[directionFirst] < diectionVals[directionSecond]) // 10 / 100 | 10 / 200 | 10 / 350
    {
      if (diectionVals[directionFirst] + 180 >= diectionVals[directionSecond]) // 10 / 100
      {
        workVal = diectionVals[directionSecond] - diectionVals[directionFirst];
        degreePerCount = (double)workVal / (directionCountLastMinute[directionFirst] + directionCountLastMinute[directionSecond]);
        directionMostCalculated = diectionVals[directionFirst] + (degreePerCount * directionCountLastMinute[directionSecond]);
      }
      else if (diectionVals[directionFirst] + 180 < diectionVals[directionSecond]) // 10 / 200
      {
        workVal = 360 - diectionVals[directionSecond] + diectionVals[directionFirst];
        degreePerCount = (double)workVal / (directionCountLastMinute[directionFirst] + directionCountLastMinute[directionSecond]);
        directionMostCalculated = diectionVals[directionSecond] + (degreePerCount * directionCountLastMinute[directionFirst]);
      }
      else // 10 / 350
      {
        workVal = 180 - diectionVals[directionSecond] + diectionVals[directionFirst];
        degreePerCount = (double)workVal / (directionCountLastMinute[directionFirst] + directionCountLastMinute[directionSecond]);
        directionMostCalculated = diectionVals[directionSecond] + (degreePerCount * directionCountLastMinute[directionFirst]);
      }

    }
    else if (diectionVals[directionFirst] > diectionVals[directionSecond]) // 100 / 10 | 200 / 10 | 350 / 10
    {
      if (diectionVals[directionSecond] + 180 >= diectionVals[directionSecond]) // 100 / 10
      {
        workVal = diectionVals[directionFirst] - diectionVals[directionSecond];
        degreePerCount = (double)workVal / (directionCountLastMinute[directionFirst] + directionCountLastMinute[directionSecond]);
        directionMostCalculated = diectionVals[directionSecond] + (degreePerCount * directionCountLastMinute[directionFirst]);
      }
      else if (diectionVals[directionSecond] + 180 < diectionVals[directionSecond]) // 200 / 10
      {
        workVal = 360 - diectionVals[directionFirst] + diectionVals[directionSecond];
        degreePerCount = (double)workVal / (directionCountLastMinute[directionFirst] + directionCountLastMinute[directionSecond]);
        directionMostCalculated = diectionVals[directionFirst] + (degreePerCount * directionCountLastMinute[directionSecond]);
      }
      else // 350 / 10
      {
        workVal = 180 - diectionVals[directionFirst] + diectionVals[directionSecond];
        degreePerCount = (double)workVal / (directionCountLastMinute[directionFirst] + directionCountLastMinute[directionSecond]);
        directionMostCalculated = diectionVals[directionFirst] + (degreePerCount * directionCountLastMinute[directionSecond]);
      }
    }

    while (directionMostCalculated > 360)
    {
      directionMostCalculated -= 360;
    }
  }
  else
  {
    directionMostCalculated = 0;
  }

}

String getWindDirectionDescriptionFromDegree(int degreeIn)
{
  String retStr = "";
  if (degreeIn > -1) // 22.5 / 11.25 / round all
  {
    if (degreeIn >= 349 || degreeIn < 12) // 348.75 / 11.5
    {
      retStr = "N";
    }
    else if (degreeIn >= 12 && degreeIn < 34) // 11.5 / 34
    {
      retStr = "NNO";
    }
    else if (degreeIn >= 34 && degreeIn < 57) // 34 / 56.5
    {
      retStr = "NO";
    }
    else if (degreeIn >= 57 && degreeIn < 79) // 56.5 / 79
    {
      retStr = "ONO";
    }
    else if (degreeIn >= 79 && degreeIn < 102) // 79 / 101.5
    {
      retStr = "O";
    }
    else if (degreeIn >= 102 && degreeIn < 124) // 101.5 / 124
    {
      retStr = "OSO";
    }
    else if (degreeIn >= 124 && degreeIn < 147) // 124 / 146.5
    {
      retStr = "SO";
    }
    else if (degreeIn >= 147 && degreeIn < 169) // 146.5 / 169
    {
      retStr = "SSO";
    }
    else if (degreeIn >= 169 && degreeIn < 192) // 169 / 191.5
    {
      retStr = "S";
    }
    else if (degreeIn >= 192 && degreeIn < 214) // 191.5 / 214
    {
      retStr = "SSW";
    }
    else if (degreeIn >= 214 && degreeIn < 237) // 214 / 236.5
    {
      retStr = "SW";
    }
    else if (degreeIn >= 237 && degreeIn < 259) // 236.5 / 259
    {
      retStr = "WSW";
    }
    else if (degreeIn >= 259 && degreeIn < 282) // 259 / 281.5
    {
      retStr = "W";
    }
    else if (degreeIn >= 282 && degreeIn < 304) // 281.5 / 304
    {
      retStr = "WNW";
    }
    else if (degreeIn >= 304 && degreeIn < 327) // 304 / 326.5
    {
      retStr = "NW";
    }
    else if (degreeIn >= 327 && degreeIn < 349) // 326.5 / 349
    {
      retStr = "NNW";
    }
  }

  return retStr;
}

double convertMsToKmh(double msValIn)
{
  double tmpVal = msValIn * (double)3.6;
  return tmpVal;
}

void checkDirection()
{
  int actualSecond = timeClient.getSeconds();
  if (actualSecond > -1 && actualSecond < 60)
  {
    int analogVal = analogRead(windDirectionPin);
    int direction = 0;
    for (int i=0; i<7 && (analogVal > diectionAnalogVal[i]); i++)
    {
      direction = diectionVals[i];
    }

    oneSecondWindDirection[actualSecond] = direction;

    Serial.println("WindDirectionAnalogVal: " + (String)analogVal);
    Serial.println("WindDirectionDegreeVal: " + (String)direction);
  }
}

void checkFreeRam()
{
  freeHeap = ESP.getFreeHeap();

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

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

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

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 Garagentor TEST</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 Garagentor TEST</h1>\n";

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

  ptr += "";

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

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

  String status = "OK";

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

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

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

void createStatusJson(String statusIn) 
{
  if (statusIn == "")
  {
    statusIn = "OK";
  }
  
  jsonDocument.clear();  
  jsonDocument["state"] = statusIn;
  
  jsonDocument["windSpeedMinuteTicksSum"] = minuteTicksSum;
  jsonDocument["windSpeedMinuteAvgMS"] = minuteWindSpeedAvgMS;
  jsonDocument["windSpeedMinuteMinMS"] = minuteWindSpeedMinMS;
  jsonDocument["windSpeedMinuteMaxMS"] = minuteWindSpeedMaxMS;

  jsonDocument["windSpeedMinuteAvgKMH"] = convertMsToKmh(minuteWindSpeedAvgMS);
  jsonDocument["windSpeedMinuteMinKMH"] = convertMsToKmh(minuteWindSpeedMinMS);
  jsonDocument["windSpeedMinuteMaxKMH"] = convertMsToKmh(minuteWindSpeedMaxMS);

  jsonDocument["windDirectionDirectionFirstDegree"] = diectionVals[directionFirst];
  jsonDocument["windDirectionDirectionFirstCount"] = directionCountLastMinute[directionFirst];
  jsonDocument["windDirectionDirectionFirstText"] = getWindDirectionDescriptionFromDegree(diectionVals[directionFirst]);
  
  jsonDocument["windDirectionDirectionSecondDegree"] = diectionVals[directionSecond];
  jsonDocument["windDirectionDirectionSecondCount"] = directionCountLastMinute[directionSecond];
  jsonDocument["windDirectionDirectionSecondText"] = getWindDirectionDescriptionFromDegree(diectionVals[directionSecond]);

  jsonDocument["windDirectionCalculatedDegree"] = directionMostCalculated;
  jsonDocument["windDirectionCalculatedText"] = getWindDirectionDescriptionFromDegree(directionMostCalculated);

  jsonDocument["freeram"] = freeHeap;

  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
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
Mast

GFK Antenna-Mast