Teil 5: Mit ESPAsyncWebServer das Handy als Steuerpult

In Teil 4: Wir steuern einen Zug mit dem ESP32 haben wir die Steuerung eines Zuges mit einem einfachen Potentiometer gebaut. Als Grundlage für weitere Interaktionen, wie der Anzeige von Signalen oder der Steuerung von Weichen benötigen wir ein richtiges Steuerpult. Jetzt können wir 3D-Drucker, Holzsäge und Lötkolben rausholen – oder wir gehen einen anderen Weg und nutzen die WiFi-Funktionen des ESP32 mit dem ESPAsyncWebServer und bauen ein virtuelles Steuerpult, das sich auch leicht verändern und mit zur Couch tragen lässt!

Das brauchst Du für diesen Teil

  • WLAN Netz (z.B. das des Routers zuhause)
  • Endgerät mit Webbrowser und Zugriff auf das gleiche Netz (z.B. Handy, Tablet, PC)

Grundlagen

Der ESP32 enthält ein integriertes WiFi-Modul für WLAN im 2,4 GHz Band nach den Standards IEEE 802.11 b/g/n. Das Modul nutzt intern den ADC2 (Analog-Digital-Converter), so dass dieser gleichzeitig zum WiFi-Betrieb nicht genutzt werden kann. In den hier vorgestellten Sketches reicht jeweils der ADC1. Für andere Projekte gibt es zahlreiche Möglichkeiten weitere analoge Werte einzulesen, z.B. über SPI oder I2C – das werde ich eventuell in einem anderen Beitrag vertiefen.

Für unsere Zwecke muss zunächst der ESP32 mit dem WLAN verbunden werden und eine IP-Adresse erhalten. Dann starten wir auf dem ESP32 einen Webserver über den dann die Interaktion erfolgt.

WLAN Verbindung

Zuerst stellen wir die WLAN Verbindung her. Dazu benötigen wir die Library wifi.h, die wir am Anfang des Sketches mit include einbinden. Dann müssen noch die WLAN-Zugangsdaten als char-Array hinterlegt werden und alles Weitere packen wir in eine Funktion WifiSetup(), die wir über setup() aufrufen. Wie alle Anweisungen in setup(), erfolgt das einmalig beim Start des ESP32.

Wichtig ist, dass im WLAN-Router der sogenannte DHCP-Server aktiviert ist. DHCP (dynamic host configuration protocol) ermöglicht neuen Geräten im WLAN eine IP-Adresse vom Router zugeteilt zu bekommen. Es ist auch möglich, eine IP-Adresse über die Funktion WiFi.config() manuell zu konfigurieren, da aber in den allermeisten Fällen DHCP ohnehin aktiviert sein sollte, lasse ich diese Schritte hier zur besseren Übersicht aus. Mehr dazu unter https://www.arduino.cc/reference/en/libraries/wifi/wifi.config/

Zum Test starten wir mit einem neuen, leeren Sketch:

#include <WiFi.h>

// WLAN Zugangsdaten
const char* ssid = "SSID_NETWORKNAME";
const char* password = "WLAN_PASSWORD";

void WifiSetup() {
  // Connect to Wi-Fi network with SSID and password
  Serial.print("Connecting Wifi to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println(""); 

  // Print local IP address
  Serial.println("WiFi connected.");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

void setup() {
  Serial.begin(115200);
  
  WifiSetup();
}

void loop() {
  //empty
}

In den Zeilen 4 und 5 müssen die WLAN-Zugangsdaten anstelle der Platzhalter eingegeben werden.

WiFi.begin(ssid, password);

Hierdurch wird die WLAN Verbindung hergestellt. In der folgenden while-Schleife wird dann auf den erfolgreichen Verbindungsaufbau gewartet und im Anschluss die über DHCP erhaltene IP-Adresse ausgegeben:

Connecting Wifi to xxxxx
.........
WiFi connected.
IP address: 192.168.0.176

ESPAsyncWebServer

Sobald die Anmeldung im WLAN abgeschlossen und die IP-Adresse verfügbar ist, kann der Webserver eingerichtet werden. Die Funktion des Webservers ist auf der IP-Adresse auf Anfragen eines Webbrowsers zu warten und entsprechende Antworten zurückzugeben.

Installation

Zunächst müssen die benötigten Libraries der Arduino IDE hinzugefügt werden:

Das geht in der IDE über das Menü Sketch > Bibliothek einbinden > .ZIP Bibliothek hinzufügen.

Webseite einrichten

Der Webserver benötigt nun auch eine Webseite, die ausgeliefert werden kann. Für das in unserem Fall gewünschte Steuerpult reicht eine einfache Seite, auf der alle Steuerelemente platziert werden können. Die Struktur ist wie folgt und entspricht einer Standard-HTML-Seite:

const char index_html[] PROGMEM = R"stringliteral(
<!DOCTYPE HTML>
<html>
<head>
  <!-- Anweisungen im HTML header -->
</head>
<body>
  <h2>ESP32 Modellbahn Steuerung</h2>
  <!-- Elemente für das Steuerpult -->
<script>
  <!-- notwendige Java-Scripts -->
</script>
</body>
</html>
)stringliteral";

Den Code der Webseite schreiben wir direkt in den Flash-Speicher des ESP32 anstelle des deutlich kleineren RAM. Das geht über die Anweisung PROGMEM in der Definition des char-Arrays außerhalb aller Funktionen als globale Variable. Mehr dazu unter https://reference.arduino.cc/reference/en/language/variables/utilities/progmem/. Die Anweisung R“ beginnt einen String Literal, der vom Beginn der Escape-Sequenz (hier stringliteral( – kann aber auch jede andere Zeichenfolge sein, die nicht im eigentlichen Inhalt vorkommt) bis zum erneuten Auftreten der Sequenz alle Zeichen beinhaltet. Nur dadurch lässt sich der HTML-Code in einem Stück bearbeiten und einfügen, da die enthaltenen “ sonst als Ende eines Strings erkannt werden würden. Mehr dazu unter https://en.cppreference.com/w/cpp/language/string_literal.

Insgesamt sieht das Template der Webseite dann wie folgt aus, das unterhalb der anderen globalen Variablen in den Sketch eingefügt wird:

const char index_html[] PROGMEM = R"stringliteral(
<!DOCTYPE HTML>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ESP32 Modellbahn Steuerung</title>
  <style>
    html {font-family: Arial; display: inline-block; text-align: center; color: lightgrey;}
    body {max-width: 400px; margin:0px auto; padding-bottom: 25px; background-color: #222222;}
    section {display: block; width: 95%%; margin-top: 30px; padding: 15px; background: #2A2A2A}
    h2 {font-size: 2.3rem; color: lightgrey;}
    p {font-size: 1.9rem; color: lightgrey;}
    
    .slider { -webkit-appearance: none; margin: 14px; width: 90%%; height: 25px; background: #555555; outline: none; -webkit-transition: .2s; transition: opacity .2s;}
    .slider::-webkit-slider-thumb {-webkit-appearance: none; appearance: none; width: 35px; height: 35px; background: #003249; cursor: pointer;}
    .slider::-moz-range-thumb { width: 35px; height: 35px; background: #003249; cursor: pointer; }
    .button  {background-color: #555555; border: none; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}
  </style>
</head>
<body>
  <h2>Modellbahn</h2>
  <section>
    <p><span id="textSliderValue">0</span></p>
    <p><input type="range" oninput="updateSliderPWM(this)" id="pwmSlider" min="-255" max="255" value="0" step="5" value="0" class="slider"></p>
    <p><button onclick="zeroSlider(this)" class="button buttonRed">STOP</button></p>
  </section>

<script>
function updateSliderPWM(element) {
  var sliderValue = document.getElementById("pwmSlider").value;
  document.getElementById("textSliderValue").innerHTML = sliderValue;
  console.log(sliderValue);
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/slider?value="+sliderValue, true);
  xhr.send();
}

function zeroSlider(element) {
  document.getElementById("pwmSlider").value = 0;
  updateSliderPWM(element);
}
</script>
</body>
</html>
)stringliteral";

Ein paar Dinge im Detail:

.slider { -webkit-appearance: none; margin: 14px; width: 90%%; height: 25px; background: #555555; outline: none; -webkit-transition: .2s; transition: opacity .2s;}
.slider::-webkit-slider-thumb {-webkit-appearance: none; appearance: none; width: 35px; height: 35px; background: #003249; cursor: pointer;}
.slider::-moz-range-thumb { width: 35px; height: 35px; background: #003249; cursor: pointer; }

Diese Stylesheets sind für den Schieberegler notwendig, der als Regler für den Zug genutzt wird.

function updateSliderPWM(element) {
  var sliderValue = document.getElementById("pwmSlider").value;
  document.getElementById("textSliderValue").innerHTML = sliderValue;
  console.log(sliderValue);
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/slider?value="+sliderValue, true);
  xhr.send();
}

Diese JavaScript Funktion wird aufgerufen, sobald der Schieberegler bewegt wird. Über einen neuen HTTP-Request an den ESPAsyncWebserver wird die neue Slider Position and den ESP32 übertragen. Wie diese Übertragungen auf dem ESP32 verarbeitet werden und wie die Webseite insgesamt ausgeliefert wird, schauen wir uns jetzt an.

Webserver Antworten einrichten

Wir haben jetzt den Code der Webseite fertig und die Abspeicherung mittels PROGMEM besprochen. Jetzt muss dem Webserver noch gesagt werden, auf welche Anfragen wie geantwortet werden soll.

Zunächst ergänzen wir dazu folgenden Zeilen am Anfang des Sketches, um den Webserver auf Port 80 (Standard für http-Anfragen) zu starten und globale Variablen als Speicherbereich vorzuhalten:

#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
AsyncWebServer server(80);
String sliderValue = "0";
const char* PARAM_INPUT = "value";

Dann fügen wir dem Sketch eine neue Funktion hinzu:

void WebserverSetup() {
  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", index_html, processor);
  });

  // Reply to GET request to <ESP_IP>/slider?value=<inputMessage>
  server.on("/slider", HTTP_GET, [] (AsyncWebServerRequest *request) {
    String inputMessageSlider;
    // GET input1 value on <ESP_IP>/slider?value=<inputMessage>
    if (request->hasParam(PARAM_INPUT)) {
      inputMessageSlider = request->getParam(PARAM_INPUT)->value();
      sliderValue = inputMessageSlider;
    }
    else {
      inputMessageSlider = "No valid message sent";
    }
    Serial.println(inputMessageSlider);
    request->send(200, "text/plain", "OK");
  });

  // Start server
  server.begin();
  Serial.println((String)"Webserver started.");
}

Und schließlich muss diese Funktion nach WifiSetup() am Ende der oben im Abschnitt „WLAN einrichten“ erstellten Funktion Setup() aufgerufen werden. Wir fügen also dort hinzu:

WebserverSetup();

Was macht das im Detail?

Betrachten wir zunächst die Ausgabe der ganzen Webseite.

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", index_html, processor);
  });

Der Webserver soll in seinem Root-Verzeichnis (also bei Anfragen ohne weitere Verzeichnisangabe) auf GET-Anfragen eines Browsers den Statuscode 200 („OK“) und einen text/html-Inhalt aus der Variable index_html zurückgeben. (Mehr zu Statuscodes unter https://developer.mozilla.org/en-US/docs/Web/HTTP/Status. Unter der Variable index_html hatten wir ja über PROGMEM den Webseiteninhalt abgespeichert:

const char index_html[] PROGMEM = R"rawliteral( INHALT )rawliteral";

Beim Aufruf von http://<IP-Adresse>/ wird also die abgespeicherte Webseite durch den ESP32 an den Browser übertragen. Für einen kleinen Mikrocontroller ist das eine sehr gute Funktion, die ganz neue Anwendungsgebiete erschließt!

Dann schauen wir noch auf den Slider.

Weiter oben hatte ich beschrieben, wie die JavaScript Funktion einen neuen HTTP-Request beim Bewegen des Sliders ausführt:

xhr.open("GET", "/slider?value="+sliderValue, true);

Im Gegensatz zum eben beschriebenen Root-Verzeichnis, wird hier das Verzeichnis /slider aufgerufen und ein Wert übertragen. Beantwortet werden muss das folglich durch eine andere Anweisung:

  server.on("/slider", HTTP_GET, [] (AsyncWebServerRequest *request) {
    String inputMessageSlider;
    // GET input1 value on <ESP_IP>/slider?value=<inputMessage>
    if (request->hasParam(PARAM_INPUT)) {
      inputMessageSlider = request->getParam(PARAM_INPUT)->value();
      sliderValue = inputMessageSlider;
    }
    else {
      inputMessageSlider = "Slider value not received";
    }
    Serial.println(inputMessageSlider);
    request->send(200, "text/plain", "OK");
  });

Der Webserver soll im Verzeichnis /slider prüfen, ob ein Wert als Parameter übergeben wurde. Ist dies der Fall wird der Wert in der Variable sliderValue abgespeichert, die wir ja schon als globale Variable angelegt haben. Anderenfalls wird eine Fehlermeldung generiert. Zur Diagnose wird der Slider-Wert zusätzlich auf der Seriellen Konsole ausgegeben.

Zur Kontrolle kann an diesem Punkt per Browser die IP-Adresse des ESP32 aufgerufen werden. Bewegt man dort den Schieberegler, wird der jeweils eingestellte Wert auf der Seriellen Konsole ausgegeben.

Anpassung des PWM duty cycle durch den Slider

Jetzt holen wir uns noch die PWM Funktionen aus Teil 4: Wir steuern einen Zug mit dem ESP32. Dazu kopieren wir von dort

  • die globalen Variablen Definitionen
  • die Funktion setCommonPwm()
  • die Anweisungen aus setup()
  • die Anweisungen aus loop()

Dann muss die Funktion setCommonPwm() am Anfang noch etwas angepasst werden, um den Wert des Schiebereglers zu nutzen, statt des Poti.

Update: Aufgrund einiger Hinwesie und Fragen, die sich ausschließlich auf die Weboberfläche bezogen haben, habe ich in dieser neuen Version die Funktion weiter vereinfacht und nur noch die Webfunktionalität erhalten. Den physischen Poti als Steuerungsmöglichkeit habe ich entfernt.

Der fertige Sketch sieht also wie folgt aus:

#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
AsyncWebServer server(80);
String sliderValue = "0";
const char* PARAM_INPUT = "value";

// WLAN Zugangsdaten
const char* ssid = "SSID_NETWORKNAME";
const char* password = "WLAN_PASSWORD";

const int enPin = 27;
const int freq = 19531;
const int ledChannel = 0;
const int resolution = 8;
const int minPwm = 0;
const int fwdPin = 32;
const int bckPin = 33;
int lastSlider = 0;
int direction = 0;  // +1 for FWD, -1 for REV

const char index_html[] PROGMEM = R"stringliteral(
<!DOCTYPE HTML>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ESP32 Modellbahn Steuerung</title>
  <style>
    html {font-family: Arial; display: inline-block; text-align: center; color: lightgrey;}
    body {max-width: 400px; margin:0px auto; padding-bottom: 25px; background-color: #222222;}
    section {display: block; width: 95%%; margin-top: 30px; padding: 15px; background: #2A2A2A}
    h2 {font-size: 2.3rem; color: lightgrey;}
    p {font-size: 1.9rem; color: lightgrey;}
     
    .slider { -webkit-appearance: none; margin: 14px; width: 90%%; height: 25px; background: #555555; outline: none; -webkit-transition: .2s; transition: opacity .2s;}
    .slider::-webkit-slider-thumb {-webkit-appearance: none; appearance: none; width: 35px; height: 35px; background: #003249; cursor: pointer;}
    .slider::-moz-range-thumb { width: 35px; height: 35px; background: #003249; cursor: pointer; }
    .button  {background-color: #555555; border: none; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}
  </style>
</head>
<body>
  <h2>Modellbahn</h2>
  <section>
    <p><span id="textSliderValue">0</span></p>
    <p><input type="range" oninput="updateSliderPWM(this)" id="pwmSlider" min="-255" max="255" value="0" step="5" value="0" class="slider"></p>
    <p><button onclick="zeroSlider(this)" class="button buttonRed">STOP</button></p>
  </section>
 
<script>
function updateSliderPWM(element) {
  var sliderValue = document.getElementById("pwmSlider").value;
  document.getElementById("textSliderValue").innerHTML = sliderValue;
  console.log(sliderValue);
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/slider?value="+sliderValue, true);
  xhr.send();
}
 
function zeroSlider(element) {
  document.getElementById("pwmSlider").value = 0;
  updateSliderPWM(element);
}
</script>
</body>
</html>
)stringliteral";

void WifiSetup() {
  // Connect to Wi-Fi network with SSID and password
  Serial.print("Connecting Wifi to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println(""); 
 
  // Print local IP address
  Serial.println("WiFi connected.");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

void WebserverSetup() {
  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", index_html);
  });
 
  // Reply to GET request to <ESP_IP>/slider?value=<inputMessage>
  server.on("/slider", HTTP_GET, [] (AsyncWebServerRequest *request) {
    String inputMessageSlider;
    // GET input1 value on <ESP_IP>/slider?value=<inputMessage>
    if (request->hasParam(PARAM_INPUT)) {
      inputMessageSlider = request->getParam(PARAM_INPUT)->value();
      sliderValue = inputMessageSlider;
    }
    else {
      inputMessageSlider = "No valid message sent";
    }
    //Serial.println(inputMessageSlider);
    request->send(200, "text/plain", "OK");
  });
 
  // Start server
  server.begin();
  Serial.println((String)"Webserver started.");
}

void setCommonPwm() {
  int pwmOutput = 0;
  int slider = sliderValue.toInt();
  if (lastSlider != slider) {
    // update of PWM-Output required
    lastSlider = slider;

    // SET COMMON MOTOR SPEED AND DIRECTION
    if (slider > -25 && slider < 25) {  
      // Neutral range
      pwmOutput = 0;
      if (direction != 0) {
        direction = 0;
        Serial.println("Speed STOP");
        digitalWrite(fwdPin, LOW);
        digitalWrite(bckPin, LOW);
      }
    }
    else {
      if (slider > 0) {
        // Forward
        pwmOutput = map(slider, 0, 255, minPwm , 255);
        if (direction != 1) {
          direction = 1;
          Serial.println("Speed FWD");
          digitalWrite(fwdPin, HIGH);
          digitalWrite(bckPin, LOW);
        }
      }
      else {
        // Back      
        pwmOutput = map(slider*-1, 0, 255, minPwm , 255);
        if (direction != -1) {
        direction = -1;
          Serial.println("Speed REV");
          digitalWrite(fwdPin, LOW);
          digitalWrite(bckPin, HIGH);
        }
      }
    }
    Serial.println(pwmOutput);
    ledcWrite(0, pwmOutput);
  }
}

void setup() {
  // Start Serial
  Serial.begin(115200);
  
  ledcSetup(0, freq, resolution);
  ledcAttachPin(enPin, 0);

  // Set pin modes
  pinMode(fwdPin, OUTPUT);
  pinMode(bckPin, OUTPUT);
   
  WifiSetup();
  WebserverSetup();
}

void loop() {
  setCommonPwm();
  delay(100);
}

Fertig ist die Steuerung Deiner kleinen Eisenbahn per Weboberfläche!

In den nächsten Teilen gehen wir dann Blockstellen, Abstellgleise, Weichen und vieles mehr an! Melde Dich am besten zum Nesletter an, um nicht zu verpassen, wenn es weitergeht!

Anhang: Slider Tuning

Für die Nutzung auf breiten Displays (wie Tablet oder PC) kann der Slider auch breiter gezogen werden, um eine feinere Bedienung zu ermöglichen:

  • in Zeile 30: max-width des body auf z.B. 1000px setzen
  • in Zeile 35: width des sliders auf z.B. 800px setzen
  • in Zeile 45: gegebenenfalls step des sliders auf 1 setzen

Kommentar verfassen

WordPress Cookie Plugin von Real Cookie Banner