Auf dem Markt gibt es diverse ESP Controller, der Vorteil eines solchen Microcontrollers ist es, das dieser über analoge & digitale Ein/Ausgänge verfügt und als Schnittstelle WiFi besitzt.
In diesem Tutorial möchte ich erläutern wie man für diese Microcontroller eine Android App entwickelt und verschiedene Sensoren / Aktoren ansprechen oder auch auswerten kann.
Solltest du im nachfolgenden Tutorial Fehler finden, oder Fragen haben, so kannst du dich gerne per E-Mail oder über das Kontaktformular an mich wenden.
Inhaltsverzeichnis
- Voraussetzung
- leeres Projekt zum Mitmachen
- Kommunikation zwischen den Geräten
- Layout
- Berechtigungen der App setzen
- Abfrage des Netzwerkstatus
- Absenden eines Request
- Sketch für den ESP Controller
- Download
- Review & Ausblick
Voraussetzung
Ich setze in diesem Tutorial voraus dass, das Tool Android Studio installiert, konfiguriert und lauffähig ist.
Wie man Android Studio installiert habe ich im Tutorial https://draeger-it.blog/android-app-mit-einer-mysql-datenbank-verbinden-16-01-2016/#Entwicklungsumgebung ausführlich erläutert.
leeres Projekt zum Mitmachen
Am Anfang möchte ich gerne ein leeres Projekt zum Download anbieten.
Dieses Projekt kann in Android Studio importiert werden und für die nächsten Schritte weiter ausgebaut werden.
Kommunikation zwischen den Geräten
Zuerst möchte ich meine Lösung für die Kommunikation zwischen den Geräten erläutern.
Sicherlich gibt es mehrere Lösung für dieses Problem, jedoch habe ich mit dieser Lösung sehr gute Erfahrungen gesammelt.
Das Android Gerät sendet einen Request an den ESP in dem dieser eine HTTP Adresse aufruft. An diese Adresse können HTTP GET Parameter angehängt und vom ESP ausgewertet werden. Als Ergebnis sendet der ESP ein Respond mit einem JSON String.
Der Vorteil an einem JSON ist, das es ein kompaktes Datenformat in einer leicht lesbaren Sprache (in diesem Fall deutsch) ist.
Wenn du mehr über das JSON Format erfahren möchtest so empfehle ich dir den Wikipedia Artikel
Seite „JavaScript Object Notation“. In: Wikipedia, Die freie Enzyklopädie. Bearbeitungsstand: 11. Januar 2019, 11:21 UTC. URL: https://de.wikipedia.org/w/index.php?title=JavaScript_Object_Notation&oldid=184617074 (Abgerufen: 16. Januar 2019, 14:28 UTC)
Layout
Für dieses Tutorial nutze ich ein einfaches TableLayout mit 4 TableRows.
Die erste Zeile enthält ein EditText für die IP Adresse, die zweite Zeile ein EditText für den Text welcher abgesendet werden soll und die dritte Zeile enthält eine Schaltfläche für das ausführen der Aktion zum absenden des Textes. Die letzte Zeile enthält nur eine einfache TextView für das Anzeigen des empfangenen Ergebnisses.
Das Layout für das Hauptfenster wird in der Datei “activity_main.xml” definiert.
<?xml version="1.0" encoding="utf-8"?> <TableLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="25dp" tools:context=".MainActivity"> <TableRow android:layout_width="match_parent" android:layout_height="match_parent" > <TextView android:id="@+id/textView6" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/txtIpAdresse" android:textSize="18sp" /> <EditText android:id="@+id/editText5" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ems="10" /> </TableRow> <TableRow> <TextView android:id="@+id/textView5" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/txtText" android:textSize="18sp" /> <EditText android:id="@+id/editText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ems="10" /> </TableRow> <TableRow android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="15dp"> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_span="2" android:text="@string/btnAbsenden" /> </TableRow> <TableRow android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="25dp"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_span="2" android:text="Respond" android:textSize="18sp" /> </TableRow> </TableLayout>
Damit wir die Komponenten später verwenden können legen wir uns jeweils ein Feld in der Klasse “MainActivity.java” an. Zusätzlich müssen diesen Feldern dann noch eine “Verbindung” zur Komponente mit “findViewById” in der Funktion “onCreate” hergestellt werden.
private EditText ipAdressEditText; private EditText editText; private Button button; private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ipAdressEditText = findViewById(R.id.ipAdressEditText); editText = findViewById(R.id.editText); button = findViewById(R.id.button); textView = findViewById(R.id.textView); }
Da wir “nur” eine Handvoll komponenten für dieses Beispiel verwenden ersetze ich nicht die IDs durch sprechende Bezeichnungen. Jedoch empfehle ich wie bei dem Eingabefeld “ipAdressEditText” eine sprechende Bezeichnung zu wählen.
Berechtigungen der App setzen
Damit man mit einem Netzwerk kommunizieren kann, muss man der App die Berechtigung zum Zugriff auf das Internet geben. Der nachfolgende Text wird in der Datei “AndroidManifest.xml” eingetragen.
<uses-permission android:name="android.permission.INTERNET" />
Damit wir zusätzlich den Status abfragen können ob das Gerät mit einem Netzwerk verbunden ist, müssen wir die Berechtigung
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
hinzufügen.
Wenn die AndroidApp später über den GooglePlay Store ausgeliefert wird, muss der Benutzer diese Berechtigung manuell bestätigen.
Abfrage des Netzwerkstatus
Bevor wir ein Request an einen ESP Controller senden können benötigen wir eine Netzwerkverbindung. Diese baut das Handy/ Tablet in der Regel selber auf so das wir diese aus der App nutzen können. Jedoch kann es passieren das keine Netzwerkverbindung besteht. Hier sollten wir den Benutzer mit einem einfachen Dialog darauf hinweisen.
Wir holen uns also zuerst den “ConnectivityManager” aus den SystemServices. Der Manager kann null sein also prüfen wir zusätzlich ob dieser null ist bevor wir uns die Informationen über das aktive Netzwerk auslesen.
Wenn das Gerät mit keinem Netzwerk verbunden ist so liefert die Methode “connectivityManager.getActiveNetworkInfo()” null zurück d.h. auch hier müssen wir auf null prüfen bevor wir mit der Methode “isConnected” prüfen können. Wenn keine Netzwerkverbindung besteht so wird ein Dialog angezeigt welcher den Titel “Fehlermeldung” trägt und als Text “Es besteht keine Netzwerkverbindung!” hat. Es wird für diesen Dialog nur eine Schaltfläche benötigt und beim betätigen wird der Dialog geschlossen.
private boolean isNetworkAvailable() { ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetworkInfo = null; if (connectivityManager != null) { activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); } boolean available = activeNetworkInfo != null && activeNetworkInfo.isConnected(); if(!available) { AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(MainActivity.this); alertDialogBuilder.setTitle(getResources().getString(R.string.dialog_error)); alertDialogBuilder .setMessage(getResources().getString(R.string.msg_no_networkconnection)) .setCancelable(false) .setPositiveButton(getResources().getString(R.string.ok), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }); AlertDialog alertDialog = alertDialogBuilder.create(); alertDialog.show(); } return available; }
Wenn kein WLAN aktiviert ist, wird also nun ein Dialog angezeigt. Nun muss der Benutzer die App verlassen und manuell das WLAN aktivieren.
Aktivieren der WLAN Verbindung aus der App
Hier können wir den Benutzer im Dialog unterstützen und die “negative Schaltfläche” für das aktivieren des WLANs benutzen.
Wir holen uns zuerst wieder den “WifiManager” aus den SystemServices und aktivieren dort das Wifi über die Methode “setWifiEnabled”.
.setNegativeButton(getResources().getString(R.string.activate_wifi), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { WifiManager wifi = (WifiManager) getSystemService(Context.WIFI_SERVICE); wifi.setWifiEnabled(true); dialog.cancel(); } })
Damit wir diese Funktion nutzen können müssen wir der App noch 2 zusätzliche Berechtigungen in der “AndroidManifest.xml” geben.
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
Je nach Android Version und gewählten Theme kann die Farbe und das Layout abweichen.
Absenden eines Request
Nachdem wir nun die Vorbereitungen zum erfolgreichen absenden eines Request erstellt haben möchten wir eine URLConnection zu einem Gerät aufbauen.
Vorbereitungen
Damit wir den Text aus dem Textfeld mit dem Button absenden können müssen wir zunächst diesem eine Aktion zuweisen. Dazu setzen wir der Funktion “setOnClickListener” das Interface “View.OnClickListener” ein.
Zusätzlich prüfen wir in der Methode, ob eine aktive Netzwerkverbindung besteht.
button = findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (isNetworkAvailable()) { sendRequest(ipAdressEditText.getText().toString(), editText.getText().toString()); } } });
Alternativ könnten wir auch der Klasse “MainActivity” das Interface “View.OnClickListener” implementieren und somit die Funktion “onClick”. Das hätte den Vorteil das bei mehr als einen Button nur eine Funktion alles Regelt.
Wir rufen in der Funktion dann die weitere neue Funktion sendRequest auf, dieser Funktion übergeben wir den Text aus dem EditText.
private void sendRequest(String ipAdresse, String text) {}
Aufbauen einer asynchronen Verbindung
Die Verbindung zum ESP wird asynchron aufgebaut. Dieses macht man immer dann wenn man Daten an einen Empfänger sendet und nicht auf das Ergebnis warten möchte. Das Ergebnis wird dann abgearbeitet wenn dieses bereit steht. Wenn nach einer bestimmten Zeit keine Antwort kommt wir ein Timeout ausgelöst.
Wir benötigen zunächst eine interne Klasse welche die Abstrakte Klasse “AsyncTask” erweitert.
class RequestAsyncTask extends AsyncTask<Void, Void, String> {...}
Und einen Konstruktor welchem wir die IPAdresse und den Text als Parameter übergeben können.
private final String text; private String ipAdresse; public RequestAsyncTask(String ipAdresse, String text) { this.ipAdresse = ipAdresse; this.text = text; }
Mit dem einbinden der Abstrakten Klasse müssen wir mindestens die Methode “doBackground” implementieren.
protected String doInBackground(Void... voids) {...}
In dieser Methode senden wir unseren Text an den ESP.
Zuerst möchten wir eine einfache Begrüßung absenden und empfangen. Dazu senden wir einen Text an den ESP Controller.
Dazu bauen wir uns zuerst die Adresse zusammen, diese besteht aus
- dem Protokoll “http” (gefolgt von einem Doppelpunkt und zwei Slashes)
- der IP Adresse des ESP Controllers,
- dem Servlet welches angesprochen werden soll, in unserem Fall heißt dieses “greeting”
- und dem Parameter
Möchte man mehrere Parameter anhängen so müssen diese mit einem “&” getrennt werden. Dazu aber später mehr.
@Override protected String doInBackground(Void... voids) { try { StringBuffer urlBuffer = new StringBuffer(); urlBuffer.append("http://"); urlBuffer.append(ipAdresse); urlBuffer.append("/greeting"); urlBuffer.append("?text="); urlBuffer.append(text); URL url = new URL(urlBuffer.toString()); URLConnection conn = url.openConnection(); conn.setDoOutput(true); return readResult(conn); } catch (Exception e) { e.printStackTrace(); } return ""; }
Da bei der Verbindung eine Exception auftreten kann muss diese abgefangen werden. Hier könnte man diese Fehlermeldung noch an die Oberfläche mit einem Dialog weitergeben.
Als erstes erzeugen wir uns ein neues Feld vom Typ “String” um die ggf. auftretende Fehlermeldung zu speichern.
private String errorMsg;
Dieses Feld befüllen wir dann in dem Catch Block mit der Fehlermeldung:
... } catch (Exception e) { errorMsg = e.getMessage(); e.printStackTrace(); } ...
Ich gebe zusätzlich den Stacktrace auf der Console aus. Die Fehlermeldung wird dann ausgewertet wenn das Ergebnis der Asyncronen Verbindung ausgewertet wird.
Wenn keine Fehlermeldung aufgetreten ist so wird von der URLConnection das Ergebnis gelesen.
private String readResult(URLConnection conn) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line); } return sb.toString(); }
Dieses Ergebnis geben wir dann in der Methode “doInBackground” zurück, die Verarbeitung dieses Ergebnisses wird dann in der Methode “onPostExecute” verarbeitet.
Wir haben uns im Catch Block beim absenden uns eine ggf. auftretende Fehlermeldung gespeichert, nun prüfen wir also ob dieses Feld befüllt ist. Da das Feld initial mit “null” belegt ist machen wir einen einfachen Null Check und prüfen zusätzlich ob das “result” nicht leer ist. (Wir sollten ja ein JSON vom ESP Controller erhalten.) Wenn das Feld “errorMsg” jedoch befüllt ist so zeigen wir die Fehlermeldung an.
@Override protected void onPostExecute(String result) { super.onPostExecute(result); if (errorMsg == null && result.trim().length() > 0) { textView.setText(result); } else if (errorMsg != null) { showErrorMessage(errorMsg); } }
Anzeigen der Fehlermeldung in einem Dialog
Die neue Methode baut dann die Fehlermeldung auf und zeigt diese an.
private void showErrorMessage(String message) { AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(MainActivity.this); alertDialogBuilder.setTitle(getResources().getString(R.string.dialog_error)); alertDialogBuilder .setMessage(message) .setCancelable(false) .setPositiveButton(getResources().getString(R.string.ok), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }); AlertDialog alertDialog = alertDialogBuilder.create(); alertDialog.show(); }
Sollte also nun zbsp. eine Verbindung nicht aufgebaut werden können, so wird eine Fehlermeldung angezeigt.
Sketch für den ESP Controller
Nachdem wir unsere AndroidApp geschrieben haben wollen wir uns nun dem ESP Controller widmen. Dieser muss den Request annehmen, verarbeiten und eine Antwort (Respond) an das Android Gerät senden.
Wir benötigen zunächst einen Microcontroller mit einem WiFi Chip, zumeist ist dieses ein ESP8266 oder ähnliches. Diese Microcontroller gibt es günstig bei ebay.de zu kaufen zbsp. den NodeMCU, WemosD1Mini*.
Hinweis von mir: Die mit einem Sternchen (*) markierten Links sind Affiliate-Links. Wenn du über diese Links einkaufst, erhalte ich eine kleine Provision, die dazu beiträgt, diesen Blog zu unterstützen. Der Preis für dich bleibt dabei unverändert. Vielen Dank für deine Unterstützung!
Ich verwende in diesem Tutorial den WittyCloud , dieser Microcontroller verfügt neben den üblichen Ein/Ausgängen noch zusätzlich über einen Fotowiderstand und einem NeoPixel.
//Bibliotheken für die Kommunikation mit WiFi Geräten #include <ESP8266WiFi.h> #include <ESP8266WebServer.h> const char* ssid = ""; //SSID aus dem Router const char* password = ""; //Passwort für den Zugang zum WLAN ESP8266WebServer server(80); //Port auf welchem der Server laufen soll. void setup() { Serial.begin(115200); //Baudrate für die Serielle Geschwindigkeit. delay(10); //10ms. Warten damit die Seriele Kommunikation aufgebaut wurde. Serial.print("Aufbau der Verbindung zu: "); //Ausgabe der SSID auf der Seriellen Schnittstelle. Serial.println(ssid); WiFi.begin(ssid, password); //Initialisieren der Wifi Verbindung. while (WiFi.status() != WL_CONNECTED) { //Warten bis die Verbindung aufgebaut wurde. delay(500); //kleine Pause von 500ms. Serial.print("."); } //Wenn eine Verbindung erfolgreich aufgebaut wurde, //werden die Daten auf dem seriellen Ausgang ausgegeben. Serial.println(""); Serial.print("Mit "); Serial.print(ssid); Serial.println(" erfolgreich verbunden!"); //Wenn der Server angewiesen wird das Servlet mit der Bezeichnung "greeting" bereitzustellen //so wird die Funktion "callGreeting" aufgerufen. server.on("/greeting", callGreeting); server.begin(); // Starten des Servers. Serial.println("Server gestartet"); //Ausgabe auf der Seriellen Schnittstelle das der Server gestartet wurde. // Ausgabe der IP Adresse Serial.print("Adresse : http://"); Serial.print(WiFi.localIP()); Serial.println("/"); } void loop() { //alle Anfragen der Clients verarbeiten. server.handleClient(); //Hier wird keine Pause eingelegt da dieses sonst die Verarbeitung stören würde. } void callGreeting(){ //Eine Variable zum speichern des gelesenen Wertes. String text = "-undefined-"; //Über alle möglichen Parameter iterieren. for (int i = 0; i < server.args(); i++) { //Zuweisen der Schlüssel/Wertepaare String parameterName = server.argName(i); String parameterValue = server.arg(i); //Wenn der Parametername gleich "text" ist dann... if(parameterName == "text"){ //zuweisen des Wertes zu unserer Variable text = parameterValue; } } //Absenden eines JSONS mit einer Begrüßung und unseres gelesenen Textes. sendResult("{\"msg\": \"Hello from ESP8266!- "+text+"\"}"); } //Diese Funktion sendet eine Antwort an den Client. void sendResult(String content){ //200 ist die Antwort das alles OK ist, //text/html ist der MimeType //content ist unser Text server.send(200, "text/html", content); }
Download
Review & Ausblick
Die erste Ausbaustufe ist geschafft und als nächstes folgt nun die Umwandlung des JSONs mit Hilfe von Google Gson. Und da der WittyCloud mit einem Sensor & einen Aktor daherkommt wollen wir diese verwenden und die Daten senden bzw. empfangen. D.h. den Fotowiderstand auslesen und dem NeoPixel einen Farbwert zuzuweisen.
Hallo Stefan,
ich bin gestern zufällig auf dinen Blog und dieses Projekt gestoßen. Es hat mich direkt neugierig gemacht und ich wollte es mal für meine Zwecke ausprobieren.
Ich muß zugeben ich bin noch absoluter Neuling auf dem Gebiet und komme deshalb wohl hier jetzt nicht weiter.
Ích habe bei mir einen NodeMCU der einen LED-Streifen ansteuert. Auf dem NodeMCU ist Tasmota und ich kann jede einzelne LED mit z.B. folgendem Befehl “http://192.168.178.103/cm?cmnd=Led19%200,40,0” ansteuern.
Nun habe ich mir gedacht, ich könnte dein Projekt ein bisschen anpassen und für meine Zwecke nutzen. Und da scheitere ich schon dran. Ich habe die Zeilen 115 bis 118 wie folgt erstmal angepasst:
urlBuffer.append(“http://”);
urlBuffer.append(ipAdresse);
urlBuffer.append(“/cm?cmnd=Led1%20”);
urlBuffer.append(text);
Bei IP Adresse gebe ich die IP des NodeMCU ein und bei Text gebe ich z.B. 40,40,40 ein.
Wenn ich dann auf Absenden drücke bekomme ich folgende Fehlermeldung:
“Fehlermeldung Cleartext HTTP traffic to 192.168.178.103 not permitted”
Wo muß ich da was ändern, bzw was mache ich falsch?
Vielen Dank schonmal
Gruß Torsten
Hi,
das Problem ist bereits bekannt. Es liegt an deinem Handy / Tablet. Der letzte User hatte ein Samsung S9 und musste in den Einstellungen die Kommunikation über HTTP erlauben.
Gruß,
Stefan Draeger
Muss ich der App die Berechtigung geben oder allgemein für das Handy. Kann nämlich momentan keine Einstellung finden. Habe das Huwei Psmart mit Android 9.0
Hi Torsten,
ich habe mal geschaut und diesen Beitrag gefunden. Das sieht für mich erstmal schlüssig aus
https://stackoverflow.com/questions/51902629/how-to-allow-all-network-connection-types-http-and-https-in-android-9-pie
Da ich selbst kein Android Gerät mit Version 9 habe kann ich es nicht testen. Wäre supi wenn du mir eine Rückmeldung geben könntest ob es klappt.
Gruß,
Stefan Draeger
Ich habe es auf einen anderen Weg hinbekommen. Nach der Vorlage von folgender Seite:
https://medium.com/@son.rommer/fix-cleartext-traffic-error-in-android-9-pie-2f4e9e2235e6
Wie unter Punkt 1 & 2 die Datei angelegt und den Text eingetragen
Bei “your_domain.com” habe ich die IP-Adresse des NodeMCU eingetragen.
Wie bei Punkt 3 die AndroidManifest angepasst
Mehrfach ausprobiert, es funktioniert (bei mir!)
Bei mit klappt es nicht: ich habe Android 8.1 auf meinem Smartphone. Die App scheint fehlerfrei zu senden, aber es kommt beim ESP nicht an. Der gleiche Text mit dem Browser vom Handy gesendet kommt auf dem ESP an. Von jedem beliebigen Notebook im WLAN kommt die Message zum ESP, nur nicht aus der APP direkt. Die Einstellungen im Manifest habe ich alle gemacht inkl. der zusätzlichen XML, in der ich die IP-Adresse eingetragen habe.
Hat noch jemand eine Idee?
Hallo Rüdiger,
gibt es eine Fehlermeldung?
Oder kannst du mir bitte dein Projekt per E-Mail senden, ich schaue mir dieses dann gerne einmal an.
Danke und Gruß,
Stefan Draeger