Apps für Android programmieren leicht gemacht!
Raspberr Pi mit dem Smartphone steruern - teil 1

1. Raspberry Pi mit dem Smartphone steuern

Ich bin bereits seit geraumer Zeit ein riesiger Fan des kleinen Mini-Computers Raspberry Pi und der enormen Möglichkeiten, die dieses kleine Wunderwerk eröffnet.
Selber habe ich meinen eigenen Raspberry Pi 2 erst seit etwa einem halben Jahr, doch bastel ich seit dem ersten Tag an munter an ihm herum.

Da ich nun im Eifer des Gefechts auf die Idee kam meinen Raspberry Pi Steckdosen ein- und ausschalten zu lassen, bin ich im Internet auf dieses Tutorial von tutorials-raspberrypi.de gestoßen.
Natürlich habe ich dieses Projekt direkt umgesetzt.

 

Zurück zum Thema:

Nun denn, genug um den heißen Brei herum geredet.
Eines verregneten Tages hatte ich dann die Idee meinen Raspberry Pi per Handy fernsteuern zu wollen.
Steckdosen ein, Steckdosen aus.
Es gibt im Google PlayStore bereits Apps, die es uns erlauben eine SSH Verbindung zu einem Linux Gerät aufzubauen. Leider sind diese Apps nicht darauf ausgelegt per einfachem Knopfdruck einen längeren Befehl auf dem Gerät auszuführen.

Wir wollen dies nun ändern!

 

Vorwort:

Wir werden in diesem Artikel ebenfalls SSH für unsere Verbindung zum Raspberry Pi nutzen, allerdings auf Tastendruck nur eine Verbindung aufbauen, um einen Befehl zu senden und die Verbindung anschließend wieder schließen.
Das Protokoll von SSH ist nicht sonderlich einfach zu programmieren, weshalb wir auf eine bereits bestehende Bibliothek (JSch) zurückgreifen werden.
Bitte versteht, dass diese Anleitung zwar so einfach wie möglich gehalten ist, aber tiefgreifendere Programmierung darstellt.

 

Vorbereitung:

Bevor wir anfangen können zu programmieren ist es zwingend von Nöten die bereits angesprochene Bibliothek in Android zu importieren.
Diese Bibliothek wird uns viel Arbeit abnehmen.

Als erstes müssen wir JSch herunterladen.
http://sourceforge.net/projects/jsch/files/jsch.jar/0.1.53/jsch-0.1.53.jar/download

Ist dies geschehen verschieben wir die Datei in den Ordner app -> libs.

Abschließend muss noch „Sync Project with Gradle Files“ in der oberen Leiste von Android Studio gewählt werden.

 

Programmierung:

Die Vorarbeit ist geleistet. Fangen wir an zu programmieren!

Als Erstes benötigen wir ein paar wenige Imports.
Diese fügen wir direkt unterhalb unseres Packagepfades, in die zu benutzende Java Datei, ein:

import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;

import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.Properties;

 

Wir erstellen uns nun eine eigene Funktion. Wir nennen diese „sshBefehl“:

public static ArrayList<String> sshBefehl(String username, String passwort, String hostname, int port, String befehl) throws Exception {
   //Hier den Code einfügen.
}

Dieser Funktion können wir dann später die entsprechenden Nutzer- und Verbindungsdaten und den Befehl übergeben.
Sie soll unseren Befehl per SSH ausführen, die Ausgabe der Konsole zurückliefern und bei einem Fehler eine Meldung ausgeben („throws Exception“).

 

Verbindung aufbauen und Befehl senden:

Unsere Aufgabe gestaltet sich nun etwas komplexer.
Als Erstes müssen wir versuchen eine Verbindung zu unserem Raspberry Pi aufzubauen, anschließend müssen wir noch einen Channel öffnen, indem wir den Befehl senden. Erst innerhalb unseres Channels können wir Ausgaben der Konsole protokollieren.

Wir benötigen nun ein Objekt aus der Library „JSch“, um eine Verbindung aufbauen zu können:

// Erstelle das benötigte Objekt, um eine SSH-Verbindung aufbauen zu können.
JSch jsch = new JSch();

 

Nun können wir dieses Objekt mit den Verbindungs- und Nutzerdaten füttern und eine Verbindung aufbauen. Der Einfachheit halber verzichten wir auf die Verifizierung des Keys. Damit prüfen wir nicht mehr auf Man-In-Middle Angriffe, aber haben es deutlich einfacher eine Verbindung zu öffnen. Im heimischen Netzwerk ist die Gefahr eines solchen Angriffes ohnehin sehr gering.

// Füttere das Objekt mit allen nötigen Informationen, um eine Verbindung aufbauen zu können.
Session session = jsch.getSession(username, hostname, port);
session.setPassword(passwort);

// Umgehe das Abgleichen nach dem richtigen Key. (Es wird nun eine Man-In-Middle Attacke nicht mehr abgefangen)
Properties prop = new Properties();
prop.put("StrictHostKeyChecking", "no");
session.setConfig(prop);

// Stelle eine Verbindung her.
session.connect();

 

Da wir idealerweise bereits erfolgreich eine Verbindung zu unserem Raspberry geöffnet haben können wir mit den erhaltenen Daten einen Channel öffnen:

// Erstelle ein neues Objekt, für einen neuen SSH Channel.
ChannelExec channelssh = (ChannelExec) session.openChannel("exec");

// Füttere den Channel mit dem Befehl und schicke den Befehl ab.
channelssh.setCommand( befehl );
channelssh.connect();

 

Eigentlich sind wir bereits fertig.
Doch der Sicherheit und der Performance zu Liebe sollten wir den bestehenden Channel und die bestehende Verbindung noch schließen.

// Beende alle Verbindungen.
channelssh.disconnect();
session.disconnect();

 

Aber Achtung!
Wir überprüfen bei der Verbindung über SSH nicht, ob die Keys stimmen.
Wir können so also keinen Man-in-the-Middle Angriff abfangen.
Des Weiteren sollte unser Raspberry Pi am besten eine statische IP haben.

 

Konsolenausgabe protokollieren:

Zwar ist unsere Funktion vom Prinzip her fertig, doch haben wir oben definiert, dass wir ein Array zurückgeben wollen.
Dieses Array soll alle Zeilen der Konsolenausgabe der Reihe nach enthalten.

An diesem Punkt wird es wieder etwas Tricky.
Wir müssen nämlich während der Protokollierung auf mehrere Fehler gleichzeitig prüfen.
Ist die Zeile leer? Wurde die Zeile bereits protokolliert? Wurden wir ausgeloggt? Ist die Verbindung abgebrochen? Ist der Befehl beendet?
Ich werde hier an dieser Stelle nicht großartig auf den Code eingehen, sondern nur zur Verfügung stellen. Er ist mit Kommentaren versehen.

Unterhalb von „JSch jsch = new JSch();“ einfügen:

// Bereite ein paar Variablen vor, um Ausgaben der Konsole auslesen zu können.
byte[] buffer = new byte[1024];
ArrayList<String> lines = new ArrayList<>();

 

Zwischen „channelssh.connect();“ und „channelssh.disconnect();“ einfügen:

// Fange an die Ausgaben der Konsole auszulesen.
try {
    InputStream in = channelssh.getInputStream();
    String line = "";

    // Lese alle Ausgaben aus, bis der Befehl beendet wurde oder die Verbindung abbricht.
    while (true) {

        // Schreibe jede Zeile der Konsolenausgabe in unser Array.
        while (in.available() > 0) {
            int i = in.read(buffer, 0, 1024);

            // Brich die Protokollierung der Ausgabe, für diese Zeile, ab, wenn die Ausgabe leer sein sollte.
            if (i < 0) {
                break;
            }

            line = new String(buffer, 0, i);
            lines.add(line);
        }

        // Wir wurden ausgeloggt.
        if (line.contains("logout")) {
            break;
        }

        // Befehl beendet oder Verbindung abgebrochen.
        if (channelssh.isClosed()) {
            break;
        }

        // Warte einen kleinen Augenblick mit der nächsten Zeile.
        try {
            Thread.sleep(1000);
        } catch (Exception ee) {}
    }
}catch (Exception e){}

 

Unterhalb von „session.disconnect();“ einfügen:

// Gib die Ausgabe zurück
return lines;

 

Funktion starten:

Eine Kleinigkeit fehlt uns noch zu vollkommenem Glück.
Wie auch die Programmierung der eigentlichen Funktion etwas Tricky war, ist auch die Programmierung des Funktionsstarts etwas umständlicher.

Ich sollte etwas weiter ausholen.
Wir bewegen uns hier in einer Programmierung die Zeit benötigt.
Je nach Internetverbindung benötigen wir Zeit eine Verbindung aufzubauen.
Wir benötigen zusätzlich Zeit einen Channel zu öffnen.
Wie sollte es anders sein, benötigen wir wahrscheinlich noch Sekunden dafür die Verbindungen wieder zu schließen.
Ganz zu schweigen von der Performance des Raspberry Pi und der darauf ausgeführten Befehle.

Führen wir die Funktion „sshBefehl“ auf unserem Smartphone ganz normal aus, so müssen wir davon ausgehen, dass es Sekunden bis Minuten dauern wird bis es wieder reagiert.
Was können wir nun tun?

Ganz einfach!
Wir führen unsere Funktion in einem BackgroundThread aus.
Wir erstellen also eine Hintergrundaufgabe, um das Einfrieren unseres Smartphones zu verhindern.

// Hintergrundaufgabe erstellen
new AsyncTask<Integer, Void, Void>() {
    @Override
    protected Void doInBackground(Integer... params) {
        try {

            // Funktion ausführen und Konsolenausgabe in "lines" speichern.
            ArrayList<String> lines = sshBefehl("Nutzername", "Passwort", "IP-Adresse", Port, "Befehl");

            // Alle Zeilen der Konsolenausgabe in den Android Logs ausgeben.
            while(!lines.isEmpty()){
                Log.e("Rückgabe", lines.get(0));
                lines.remove(0);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}.execute(1);

 

 

Wie geht es weiter ?

Wir haben es ja bereits geschafft eine komplette Funktion zu schreiben, welche es uns erlaubt längere Befehle auf unserem SSH Server auszuführen.
Bisher ist die Funktion aber eher etwas zusammenhangslos gestaltet und ohne zugehörige App.

In den nächsten Teilen dieser Artikelreihe werden wir die zugehörige App dazu schreiben.
Den Source Code könnt ihr dann kostenfrei herunterladen.
Eine fertige App werden wir ebenfalls im Google PlayStore veröffentlichen.

Bis dahin wünsche ich viel Spaß beim programmieren.

Marvin

Ich bin 23 Jahre jung und studiere zurzeit Wirtschaftsinformatik an der Georg-August-Universität in Göttingen. Ich bin ein Mensch, der sich neben der Programmierung noch für tausend andere Dinge interessiert, die mal mehr und mal weniger verrückt sind. Vor allem aber bin ich Feuer und Flamme mit der Programmierung von eigenen kleinen Apps und Programmen, die mein Leben bereichern.

4 Kommentare

*Pflichtfeld

  • Vielen Dank für die ausführliche Erklärung! Ist es auch möglich, befehle die von der Raspberry Konsole zurück kommen in der App (über ein Textfeld oä.) ausgeben zu lassen? Bsp. wie bei einem „ls“ befehl, dass dann die Ordnerauflistung in der App erscheint?

    • Hallo Matthias,

      das ist selbstverständlich möglich.
      Schreibe einfach in die Klasse, die „AsyncTask“ benutzt das ein: static String konsolenausgabe = ""; und schreibe unter die Zeile Log.e("Rückgabe", lines.get(0)); einfach noch die Zeile konsolenausgabe += lines.get(0);.
      Nachdem der SSH-Befehl beendet ist ist auch der String vollkommen gefüllt, dies kann allerdings ein wenig dauern.
      Achte darauf, den String vor jedem neuen Ausführen wieder zu leeren.

      Gruß