Teil 6: Android App mit Bluetooth als Steuerpult

Im letzten Teil (Teil 5: Mit ESPAsyncWebServer das Handy als Steuerpult) habe ich gezeigt, wie man ohne viel Löten ein virtuelles Steuerpult als Webseite auf direkt auf dem ESP32 einrichten kann. In diesem Teil möchte ich noch eine weitere Möglichkeit vorstellen: Eine Android-App, die sich mit dem ESP32 über Bluetooth austauscht. Letztlich sind beides gute Methoden, die App bietet aber einen direkteren Kommunikationsweg und weniger WLAN-bedingter Verzögerungen und einfachere Auswertung von zurückkommender Information, die wir später bei den Blockstellen, Abstellgleisen und Weichen benötigen.

Die Modellbahn-App

Wer sich für die Erstellung der App nicht interessiert, kann diesen Abschnitt auch überspringen und weiter unten einfach die fertige .APK Datei herunterladen und auf einem Smartphone installieren.

Der MIT App Inventor und erste Schritte

Die App basiert auf dem MIT App Inventor, ein hervorragendes Web-Tool, um ohne spezielles Programmierwissen im Bereich Android durchaus komplexe Apps erstellen zu können. Die ersten Schritte habe ich in zwei gesonderten Beiträgen zusammengefasst. Bevor wir hier weitergehen, empfiehlt es sich, beide Beiträge durchzuarbeiten und erste Erfahrungen mit der grafischen Programmierumgebung und Bluetooth auf dem ESP32 zu sammeln:

Die Elemente in der Design Ansicht

Als Ausgangspunkt nehmen wir die im zweiten Beitrag vorgestellte „Bluetooth Basics“ App. Sie enthält bereits einen Screen mit zwei Buttons zur Herstellung einer Bluetooth-Verbindung zu einem bereits über die Bluetooth-Einstellungen von Android gekoppelten ESP32. Sofern möglich, stellt diese App die einmal erfolgreich aufgebaute Verbindung wieder her.

Jetzt fügen wir unter die Buttons zunächst aus dem Bereich „Layout“ ein HorizontalArrangement hinzu und setzen rechts in der Spalte „Properties“ eine Height von 40 Pixeln für den Einsatz als Abstandshalter. Optional per Rename in „Spacer“ umbenennen.

Darunter kommt dann wieder aus „Layout“ ein VerticalArrangement, das dann die weiteren Steuerelemente (alle zu finden unter „User Interface“) beinhalten soll:

  • Einen Button, Rename in „ButtonStop“
  • Ein weiteres HorizontalArrangement als Abstandshalter mit Height 40 Pixel, Rename in „Spacer1“
  • Einen Slider, Width 90%, minValue 0, maxValue 4095, Rename in „SliderA“
  • Ein weiteres HorizontalArrangement als Abstandshalter mit Height 40 Pixel, Rename in „Spacer2“
  • Ein Label für die Ausgabe von Diagnosedaten, Rename in „LabelDebug“

Zusätzlich benötigen wir folgende „nicht sichtbare“ Elemente, die ebenfalls per Drag&Drop auf den Screen gezogen werden, dann aber unterhalb sichtbar werden:

  • Eine Clock aus dem Bereich „Sensors“, TimerInterval 100, Namen als „Clock1“ belassen
  • Ein Web aus dem Bereich „Connectivity“, Namen als „Web1“ belassen
  • BluetoothClient1 und File1 sollten bereits aus der Bluetooth Basics Anleitung (hier) enthalten sein.

Der Design-Bereich des App-Inventors sollte nun so aussehen:

Blöcke für den funktionellen Unterbau

Ohne weiteren Code bringen dieses Elemente keine Funktion. Daher müssen wir jetzt in der Blocks-Ansicht des App Inventors die notwendigen Funktionen hinzufügen. Blöcke werden im MIT App Inventor hinzugefügt, indem man das entsprechende Element aus der Liste ganz links ausgewählt und per Drag&Drop in das Hauptfenster gezogen wird. Ganz oben („Built-in“) sind die grundlegenden Elemente einer Programmiersprache: Bedingungen, Logik, Variablen und Prozeduren. Prozeduren, die zu einem Element gehören, sind darunter bei dem jeweiligen Element auffindbar. Hierbei wird zwischen Ereignissen und Methoden unterschieden. Ereignisse werden durch den Eintritt bestimmter Bedingungen, wie z.B. einer Nutzerinteraktion ausgelöst. Methoden steuern das Verhalten eines Elements. Nach Auswahl des Elements kann in der neu erscheinenden Spalte rechts daneben aus Methoden oder Ereignissen zu diesem Element das Passende ausgewählt werden.

Slider

Sobald der Slider auf eine neue Position bewegt wird, soll diese Information an den ESP32 übertragen werden. Bei dem Element „SliderA“ suchen wir also das Ereignis „PositionChanged“ heraus. Dieser Code wird nach Änderung der Slider-Position ausgeführt. In dieses Ereignis setzen wir weitere Blöcke, die den neuen Wert an den ESP32 übertragen: Senden eines Textes über die serielle Bluetooth-Verbindung mit dem Inhalt „sldA“ zur Identifikation des Elements, gefolgt von einer gerundeten Zahl der aktuellen Position (0-4095).

when SliderA.PositionChanged

Die fertige Funktion sieht dann wie hier dargestellt aus.

Stop-Button

In manchen Situationen muss der Zug schnell angehalten werden, ohne dass man erst den Slider genau zur mittigen Position bewegen will. Das erledigt der Stop-Button. Bei Click soll der Slider in die Mitte gestellt werden. Die Slider-Funktion übermittelt dann diese neue Position an den ESP32.

when ButtonStop.Click

Die fertige App

Zum Nachvollziehen, zum Prüfen der Vollsändigkeit oder einfach nur zum Überspringen der App-Enwicklung sind hier die App Inventor Projektdatei und fertige Android-APK.

Änderungen am ESP32

Nach dem Bau der App für eine Kommunikation per Bluetooth muss noch der ESP32 über diese Schnittstelle Daten empfangen. In Teil 5: Mit ESPAsyncWebServer das Handy als Steuerpult hatten wir den ESP32 mit einem Webserver ausgestattet. Den dort aufgeführten Sketch nehmen wir jetzt als Ausgangsbasis für folgende Änderungen:

  • Entfernen des Webservers, der Webseite und den Antwortfunktionen
  • Hinzufügen der Bluetooth-Funktionalität
  • Anpassung der Funktionen an den Dateneingang per seriellem Bluetooth

Entfernen des ESPAsyncWebServer

Da künftig die Kommunikation über Bluetooh läuft, können alle Teile im Zusammenhang mit dem ESPAsyncWebserver und TCP (Teil des Netzwerk Protokolls) entfernt werden. Ebenso wir die statisch im Flash (PROGMEM) abgelegte Webseite nicht mehr benötigt. Also entfernen wir:

#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>

AsyncWebServer server(80);
String sliderValue = "0";

// WLAN Zugangsdaten
const char* ssid = "xxxxxxxxxx";
const char* password = "xxxxxxxxxx";

const char index_html[] PROGMEM = R"stringliteral(
.......................
)stringliteral";

void WifiSetup() {
.......................
}

void WebserverSetup() {
.......................
}

// sowie in der Funktion setup() :
  WifiSetup();
  WebserverSetup();

Hinzufügen von Bluetooth

Am Anfang des Sketches fügen wir ein:

// Load libraries
#include <BluetoothSerial.h>

// init Bluetooth Class:
BluetoothSerial SerialBT;
const char* btname = "Modellbahn";  // Replace with bluetooth name
bool BTisConnected = false;

// sowie in der Funktion setup():
  SerialBT.register_callback (btStatus);
  SerialBT.begin(btname); //Name of your Bluetooth interface -> will show up on your phone
  Serial.print("Bluetooth started: "); Serial.println(btname);

Dieser Code lädt die Library BluetoothSerial (Quellcode auf Github) und aktiviert ein serielles Interface namens „SerialBT“, das für andere Bluetooth-Devices unter dem Namen „Modellbahn“ angezeigt wird.

Zusätzlich fügen wir zu Diagnosezwecken eine Funktion hinzu, die die Bluetooth Verbindung prüft und Statusänderungen im Seriellen Monitor ausgibt:

// BT CONNECTION-CHECK--------------------------------------------
void btStatus (esp_spp_cb_event_t event, esp_spp_cb_param_t*param) {
  if(event == ESP_SPP_SRV_OPEN_EVT){
    Serial.println("Bluetooth Client Connected");
    BTisConnected = true;
  }

  else if (event == ESP_SPP_CLOSE_EVT ) {
    Serial.println ("Bluetooth Client Disconnected");
    BTisConnected = false;
  }
}

Damit ist der ESP32 bereits grundlegend in der Lage per Bluetooth zu kommunizieren und einen Verbindungsaufbau unserer App weiter oben anzunehmen. Bitte probiere vor den weiteren Schritten aus, ob „Bluetooth Client Connected“ im Seriellen Monitor der Arduino IDE ausgegeben wird!

Anpassung der Funktionen

Zuvor wurde ein String „sliderValue“ durch die server.on Methode des ESPAsyncWebservers gesetzt, sobald der Slider aus der Webseite bewegt wurde. Als Werte wurden -255 bis 255 übergeben. Jetzt erwarten wir eine Textnachricht auf dem seriellen Interface der Bluetooth-Verbindung mit dem Prefix „sldA“, gefolgt von einem Wert zwischen 0 und 4095.

Zunächst müssen wir daher die Serielle Schnittstelle regelmäßig auslesen:

int sliderValue = 2048;

void btCheckInput() {
  // called from loop()
   if (SerialBT.available() > 0) {
    String input = SerialBT.readStringUntil('\n');
    //Serial.print("Bluetooth Input: ");
    //Serial.println(input);
    
    if (input.startsWith("sld")) {
      // slider movement input
      String slider = input.substring(3, 4);
      String number = input.substring(4);
      if (slider == "A") {
        sliderValue = number.toInt();
        //Serial.println(sliderValue);
      }
    }
  }
}

Diese Funktion prüft, ob Zeichen im Eingangs-Pufferspeicher der Bluetooth-Verbindung vorhanden sind und – falls ja – liest diese bis zum nächsten Newline („\n“). Sofern eine Zeichenkette mit „sldA“ beginnt, wird der variablen sliderValue (jetzt ein Integer, statt String!) der numerische Wert der folgenden Zahl zugewiesen. Wie der Inline-Kommentar schon sagt, muss diese Funktion aus der Funktion loop() aufgerufen werden:

  btCheckInput();

Dadurch haben wir bei jedem Loop-Durchlauf eine Zahl zwischen 0 und 4095 zur Slider-Stellung.

Jetzt müssen wir die setCommonPwm() Funktion noch überarbeiten:

Als erstes ersetzen wir die lokale Variable slider und die globale Variable lastSlider besser verständlich durch „speed“ bzw „lastSpeed“. Dazu muss ganz oben im Sketch die globale Variable „lastSpeed“ definiert werden:

int lastSpeed = 0;

Die entsprechende Anweisung für lastSlider kann entfernt werden.

void setCommonPwm() {
  int pwmOutput = 0;
  int speed = 0;

  // Check for established BT Connection  
  if (BTisConnected) {
    // App slider will be used.
    speed = sliderValue - 2048;
  }
  else {
    // Set speed to 0 if BT connection is lost
    speed = 0;
  }

  if (lastSpeed != speed) {
    // update of PWM-Output required
    lastSpeed = speed;

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

Schauen wir uns die Veränderungen im Detail an!

Zunächst wird die lokale Variable speed und die neue globale Variable lastSpeed durchgehend als Ersatz für slider und lastSlider genutzt und eine Sicherungsfunktion für den Fall des Bluetooth-Verbindungsabbruchs ist hinzugekommen.

  int speed = 0;

  // Check for established BT Connection  
  if (BTisConnected) {
    // App slider will be used.
    speed = (sliderValue - 2048);
  }
  else {
    // Set speed to 0 if BT connection is lost
    speed = 0;
  }

Dann ist die Range der 0-Position aufgrund der größeren Zahlen für speed, die aus der App kommen (-2048 bis 2047 statt -255 bis 255), um den Faktor 8 vergrößert:

 // SET COMMON MOTOR SPEED AND DIRECTION
    if (speed > -200 && speed < 200) {

Auch für die Teile Forward und Reverse sind die neuen Ranges in den map-Befehlen angepasst.

Fertig! Viel Spaß beim Fahren per App! Das ist der fertige Sketch nochmal im Ganzen:

// Load libraries
#include <BluetoothSerial.h>

// init Bluetooth Class:
BluetoothSerial SerialBT;
const char* btname = "Modellbahn";  // Replace with bluetooth name
unsigned long lastBtTransmit = 0;   // Stores the Millis of last transmission
bool BTisConnected = false;

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 sliderValue = 2048;
int lastSpeed = 0;
int direction = 0;  // +1 for FWD, -1 for REV

// BT CONNECTION-CHECK--------------------------------------------
void btStatus (esp_spp_cb_event_t event, esp_spp_cb_param_t*param) {
  if(event == ESP_SPP_SRV_OPEN_EVT){
    Serial.println("Bluetooth Client Connected");
    BTisConnected = true;
  }

  else if (event == ESP_SPP_CLOSE_EVT ) {
    Serial.println ("Bluetooth Client Disconnected");
    BTisConnected = false;
  }
}

// BT RECEIVE INPUTS--------------------------------------------
void btCheckInput() {
  // called from loop()
   if (SerialBT.available() > 0) {
    String input = SerialBT.readStringUntil('\n');
    //Serial.print("Bluetooth Input: ");
    //Serial.println(input);
    
    if (input.startsWith("sld")) {
      // slider movement input
      String slider = input.substring(3, 4);
      String number = input.substring(4);
      if (slider == "A") {
        sliderValue = number.toInt();
        //Serial.println(sliderValue);
      }
    }
  }
}

// SET PWM AND DIRECTION PINS--------------------------------------------
void setCommonPwm() {
  int pwmOutput = 0;
  int speed = 0;

  // Check for established BT Connection  
  if (BTisConnected) {
    // App slider will be used.
    speed = sliderValue - 2048;
  }
  else {
    // Set speed to 0 if BT connection is lost
    speed = 0;
  }

  if (lastSpeed != speed) {
    // update of PWM-Output required
    lastSpeed = speed;

    // SET COMMON MOTOR SPEED AND DIRECTION
    if (speed > -200 && speed < 200) {  
      // Neutral range
      pwmOutput = 0;
      if (direction != 0) {
        direction = 0;
        Serial.println("Speed STOP");
        digitalWrite(fwdPin, LOW);
        digitalWrite(bckPin, LOW);
      }
    }
    else {
      if (speed > 0) {
        // Forward
        pwmOutput = map(speed, 0, 2047, minPwm , 255);
        if (direction != 1) {
          direction = 1;
          Serial.println("Speed FWD");
          digitalWrite(fwdPin, HIGH);
          digitalWrite(bckPin, LOW);
        }
      }
      else {
        // Reverse      
        pwmOutput = map(speed*-1, 0, 2048, 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);
  delay(500);
  Serial.println("++++Neustart++++");
  
  // Start BT Serial
  SerialBT.register_callback (btStatus);
  SerialBT.begin(btname); //Name of your Bluetooth interface -> will show up on your phone
  Serial.print("Bluetooth started: "); Serial.println(btname);

  // Setup PWM module (ledc)
  ledcSetup(0, freq, resolution);
  ledcAttachPin(enPin, 0);

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

}

void loop() {
  btCheckInput();
  setCommonPwm();
  delay(50);
}

WordPress Cookie Plugin von Real Cookie Banner