Windows 10 IoT auf dem Raspbery Pi 2 – App-Entwicklung

Raspberry + Windows + Visual Studio

Die letzten beiden Beiträge haben gezeigt, wie man Windows 10 IoT auf dem Raspberry Pi 2 (Affiliate-Link) installiert und einrichtet. Der nächste Schritt ist nun die Entwicklung von eigenen Apps für den Raspberry – und zwar mit Visual Studio und C#.

Voraussetzungen

Um Apps für Windows 10 IoT entwickeln zu können, ist zunächst einmal ein fertig eingerichteter Raspberry Pi mit Windows 10 IoT notwendig. Wer diese Schritte noch nicht erledigt hat, kann sich die dazu passenden Artikel ansehen:

Darüber hinaus ist zum Entwickeln Visual Studio 2015 erforderlich.
Hinweis: Als der Artikel geschrieben wurde, war Visual Studio 2015 RC (Release Candidate) aktuell.

Update (01.09.2015): Mittlerweile wurde Visual Studio 2015 in der finalen Version veröffentlicht. Bereits die Community Edition beitet alles, was für die Entwicklung mit Windows 10 IoT benötigt wird. Eine Testversion der (kostenpflichtigen) Enterprise Edition steht ebenfalls zur Verfügung (90 Tage lauffähig):

Update (28.02.2016): Der Artikel wurde grundlegend überarbeitet, und für die Verwendung mit der finalen Version von Visual Studio 2015 angepasst. Außerdem ist das Projekt nun nicht mehr zum Download als ZIP-Datei verfügbar, sondern steht zur freien Verwendung unter GitHub zur Verfügung.

Die Universal Windows Platform (UWP)

Der .NET/C#-Entwickler wird sich sicherlich freuen, dass hier auf einem kleinen Gerät wie dem Raspberry Pi mit der gewohnten Entwicklungsumgebung und Programmiersprache entwickelt werden kann. Man muss sich allerdings bewusst sein, dass hier nicht der volle Umfang des .NET-Frameworks zur Verfügung steht.

Wer schon für Windows Phone oder generell Windows Store Apps entwickelt hat, wird sich mit der Umstellung sicher nicht so schwer tun, da hier auch nur ein kleiner Teil des .NET-Frameworks verwendet werden konnte. Man programmierte hier nicht gegen das komplette .NET-Framework, sondern gegen die Windows Runtime (WinRT).

Für die Entwicklung von Apps für Windows oder Windows Phone wurde seitens Microsoft immer empfohlen, sog. Universal Apps zu programmieren. Dieses Konzept sieht vor, dass man gewisse Programmteile (z.B. die reine Business-Logik) in einem universellen Projekt implementiert und dann angepasste Projekte für die jeweilige Ziel-Plattform (Windows/Windows Phone) hinzufügt, die meistens nur Plattform-spezifischen Code und Oberflächen beinhalten.

Mit der Universal Windows Platform (UWP) geht Microsoft nun noch einen Schritt weiter: Man entwickelt eine App, die dann auf allen Plattformen läuft: Desktop, Smartphone, Xbox, Geräten wie dem Raspberry Pi (IoT), etc.

Apps, die für den Raspberry Pi entwickelt werden, sind demnach auch „Universal Windows Apps“.

Anforderungen für eine einfache App: Wake On LAN Proxy

Im Folgenden soll nun eine einfache App für den Raspberry Pi entwickelt werden: Ein Wake On LAN Proxy.

Hintergrund: Ich verwende schon länger einen Wake On LAN Proxy (auf Raspberry Pi/Linux), um meinen Home-Server auch über das Internet starten zu können (Wake On WAN). Dieser Proxy ist leider notwendig, da sich auf meiner FritzBox keine Portfreigaben einrichten lassen, die an die Broadcast-Adresse des Netzwerks (255.255.255.255) weitergeleitet werden. Hier springt ein Wake On LAN Proxy ein: Dieser erhält in der FritzBox eine Portfreigabe (UDP, Port 9), empfängt ein Wake On WAN Signal und leitet das dann an die Broadcast-Adresse weiter.

Unser Windows 10 IoT Wake On LAN Proxy soll nun folgende Features haben:

  • Empfangen von sog. Magic Packets (6x FF + 16x MAC-Adresse des zu startenden Rechners) von der FritzBox und weiterleiten dieser Pakete per UDP an die Broadcast-Adresse (255.255.255.255, Port 9).
  • Die App soll im Hintergrund laufen, also keine Oberfläche besitzen.
  • Die App soll automatisch starten, wenn der Raspberry Pi gestartet wird.
  • Die App sollte möglichst einfach aufgebaut sein, d.h. es wird hier kein besonderer Wert auf die eigentliche Programmlogik gelegt. Das Hauptaugenmerk liegt auf der Entwicklung der ersten App für Windows 10 IoT und die Zusammenhänge und Hintergründe, auf die man diesbzgl. achten sollte.

Entwicklung mit Visual Studio 2015

Nach dem Definieren der Anforderungen kann es mit der Entwicklung losgehen.

Visual Studio Project Template installieren

Zuvor muss für das Visual Studio 2015 noch das entsprechende Project Template installiert werden. Am einfachsten geht dies, indem man unter Tools > Extensions and Updates… nach Windows IoT Core Project Templates sucht und diese installiert.

Visual Studio Project Templates für IoT
Visual Studio Project Templates für IoT

WOL Proxy als Background Application (IoT) anlegen

Nach einem Neustart von Visual Studio und legen wird nun ein neues Projekt vom Typ Background Application (IoT) an. Die Background Application ist hier der passende Projekt-Typ, da die App im Hintergrund ohne Oberfläche laufen soll. Wir nennen das Projekt einfach WolProxy.

Projekt vom Typ 'Background Application (IoT)' anlegen
Projekt vom Typ ‚Background Application (IoT)‘ anlegen

Es wird dabei eine Klasse StartupTask angelegt, die das Interface IBackgroundTask implementiert. Dieses Interface beinhaltet lediglich die Methode Run, die immer dann ausgeführt wird, wenn der BackgroundTask ausgeführt werden soll. Diese Klasse wird zunächst umbenannt in WolProxyStartupTask.

Implementieren der Programm-Logik

Die Programm-Logik wird nicht direkt in die Klasse WolProxyStartupTask implementiert, auch wenn dies problemlos möglich wäre. Besser ist in jedem Fall, die Logik in eine eigene Klasse zu packen. Dazu wird dem Projekt eine neue Klasse WolProxy hinzugefügt. Die Implementierung sieht in unserem Fall dann folgendermaßen aus:

public sealed class WolProxy
{
	// Socket to receive UDP packets.
	private DatagramSocket datagramSocketReceive;
	// Socket to send UDP packets.
	private DatagramSocket datagramSocketSend;

	// Ports for sending/receiving.
	private int listenPort;
	private int sendPort;

	public WolProxy() : this(9, 9)
	{

	}

	public WolProxy(int listenPort) : this(listenPort, 9)
	{
	}

	public WolProxy(int listenPort, int sendPort)
	{
		this.listenPort = listenPort;
		this.sendPort = sendPort;
	}

	public async void Start()
	{
		this.datagramSocketReceive = new DatagramSocket();
		this.datagramSocketReceive.MessageReceived += Socket_MessageReceived;

		this.datagramSocketSend = new DatagramSocket();

		var portStr = this.listenPort.ToString(CultureInfo.InvariantCulture);
		await this.datagramSocketReceive.BindServiceNameAsync(portStr);
	}

	public void Stop()
	{
		if (this.datagramSocketSend != null)
		{
			this.datagramSocketSend.Dispose();
			this.datagramSocketSend = null;
		}

		if (this.datagramSocketReceive != null)
		{
			this.datagramSocketReceive.MessageReceived -= Socket_MessageReceived;
			this.datagramSocketReceive.Dispose();
			this.datagramSocketReceive = null;
		}
	}

	private async void Socket_MessageReceived(DatagramSocket sender, DatagramSocketMessageReceivedEventArgs args)
	{
		try
		{
			// Get the content of the UDP packet.
			var length = args.GetDataReader().UnconsumedBufferLength;
			var bArr = new byte[length];
			args.GetDataReader().ReadBytes(bArr);

			// Re-send Magic Packet only if it was not received from the local IP and it is a real MagicPacket.
			var ip = GetLocalIPAddress();
			var remoteAddress = args.RemoteAddress;

			if (ip != args.RemoteAddress.DisplayName && IsByteArrayMagicPacket(bArr))
				await SendMagicPacket(bArr, remoteAddress);
		}
		catch (Exception ex)
		{
		}
	}

	private async Task SendMagicPacket(byte[] magicPacket, HostName remoteAddress)
	{
		// This forwards the received Magic Packet (as byte array) to the network's broadcast address.
		try
		{
			// Send Magic Packet to broadcast address (port 9).
			var portStr = this.sendPort.ToString(CultureInfo.InvariantCulture);
			Log.WriteLog(string.Format("Forwarding Magic Paket from {0} to MAC address {1} (Port {2})", remoteAddress.CanonicalName, GetMacStringFromMagicPacket(magicPacket), sendPort));

			using (var stream = await this.datagramSocketSend.GetOutputStreamAsync(new HostName("255.255.255.255"), portStr))
			{
				using (var writer = new DataWriter(stream))
				{
					writer.WriteBytes(magicPacket);
					await writer.StoreAsync();
				}
			}
		}
		catch (Exception ex)
		{
		}
	}

	private string GetLocalIPAddress()
	{
		var hostNames = NetworkInformation.GetHostNames();

		foreach (var item in hostNames)
		{
			if (item.Type == HostNameType.Ipv4 && item.IPInformation != null)
				return item.DisplayName;
		}

		return string.Empty;
	}

	private bool IsByteArrayMagicPacket(byte[] bArr)
	{
		try
		{
			// Header 6x FF.
			for (int i = 0; i < 6; i++)
			{
				if (bArr[i] != 0xFF)
					return false;
			}

			// Get the MAC address.
			var macArr = new byte[6];

			for (int i = 0; i < macArr.Length; i++)
			{
				macArr[i] = bArr[i + 6];
			}

			// This MAC adress has to be the same 16 times in a row.
			for (int i = 1; i < 17; i++)
			{
				var checkArr = new byte[6];
				Array.Copy(bArr, 6 * i, checkArr, 0, 6);

				for (int j = 0; j < checkArr.Length; j++)
				{
					if (checkArr[j] != macArr[j])
						return false;
				}
			}

			return true;
		}
		catch (Exception ex)
		{
			return false;
		}
	}

	private string GetMacStringFromMagicPacket(byte[] magicPacket)
	{
		// Get the MAC address.
		var macArr = new byte[6];

		for (int i = 0; i < macArr.Length; i++)
		{
			macArr[i] = magicPacket[i + 6];
		}

		return ConvertMacByteArrayToString(macArr);
	}

	private static string ConvertMacByteArrayToString([ReadOnlyArray]byte[] macByte)
	{
		StringBuilder sb = new StringBuilder();

		for (int i = 0; i < macByte.Length; i++)
		{
			sb.Append(macByte[i].ToString("x2"));

			if (i % 1 == 0 && i != macByte.Length - 1)
				sb.Append(":");
		}

		return sb.ToString().ToUpper();
	}
}

Mit der Methode Start der Klasse wird ein DatagramSocket zum Empfangen von UDP-Paketen erstellt. Durch die Methode BindServiceNameAsync wird auf dem entsprechenden Port „gelauscht“, der im Konstruktor der Klasse mit angegeben wurde.

Wenn ein UDP-Paket empfangen wurde, wird dieses Paket zunächst einmal gelesen (Methode Socket_MessageReceived). Hier könnte man sich weitere Programmlogik vorstellen, die beispielsweise überprüft, ob es sich tatsächlich um ein Magic Paket handelt und die Weiterleitung nur in diesem Fall vornimmt. In unserem Beispiel wird lediglich überprüft, von wo das Paket versendet wurde: Falls es von der lokalen IP-Adresse kommt (Methode GetLocalIPAddress), wird es nicht weitergeleitet, da man ansonsten in eine Endlosschleife mit dem Empfangen und Weiterleiten von UPD-Paketen käme.

Das Weiterleiten an sich übernimmt die Methode SendMagicPacket: Es wird wiederum ein DatagramSocket verwendet – nur diesmal zum Senden von UDP-Paketen an die Broadcast-Adresse des Netzwerks (255.255.255.255). Das zuvor empfangene Paket wird einfach unverändert weitergeleitet.

Einstiegspunkt und Aufruf der Klasse WolProxy

Die Klasse WolProxyStartupTask nutzt nun den WolProxy, um die eigentliche Aufgabe zu erledigen. Da wir die Programm-Logik in eine eigene Klasse ausgelagert haben, kann der StartupTask sehr einfach gehalten werden:

public sealed class WolProxyStartupTask : IBackgroundTask
    {
        private BackgroundTaskDeferral backgroundTaskDeferral;
        private WolProxy wolProxy;

        public void Run(IBackgroundTaskInstance taskInstance)
        {
            // Get the deferral and save it to local variable so that the app stays alive.
            this.backgroundTaskDeferral = taskInstance.GetDeferral();
            taskInstance.Canceled += TaskInstance_Canceled;

            IAsyncAction asyncAction = ThreadPool.RunAsync((handler) =>
            {
                this.wolProxy = new WolProxy(9, 9);
                this.wolProxy.Start();
            });
        }

        private void TaskInstance_Canceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
        {
            if (this.wolProxy != null)
            {
                this.wolProxy.Stop();
            }

            // Release the deferral so that the app can be stopped.
            this.backgroundTaskDeferral.Complete();
        }
    }

In der Methode Run wird erst einmal ein sog. Deferral (zu Deutsch: Hintergrundtaskaufschub) durch die IBackgroundTaskInstance abgefragt und in einer Variablen gespeichert. Dies ist notwendig, um das vorzeitige Schließen des Tasks zu verhindern, so lange noch asynchroner Code ausgeführt wird. Ohne dieses Deferral würde die Run Methode einfach durchlaufen und die App würde danach beendet werden.

Anpassen des App-Manifests

Bevor die App nun auf einem Raspberry installiert und ausgeführt werden kann, muss nun noch das App-Manifest angepasst werden. Das reicht ein Doppelklick auf die Datei Package.appxmanifest im Solution Explorer. Im Manifest wird die App genauer beschrieben und die erforderlichen Rechte angegeben. Dies ist ein wichtiger Schritt, da die App ansonsten nicht lauffähig sein wird bzw. beim Senden eines Start-Befehle eine Exception werfen wird.

Unter Capabilities muss neben der bereits aktivierten Option Internet (Client) auch Internet (Client & Server) aktiviert werden. Dies ist notwendig, da die App ja nicht nur Dateien (Magic Packets) empfängt, sondern auch sendet und daher als Server fungiert.

Wake On LAN Proxy: Capabilities
Wake On LAN Proxy: Capabilities

Da wir die Klasse StartupTask umbenannt haben, muss nun noch unter Declarations der richtige Einstiegspunkt der App angegeben werden (WolProxy.WolProxyStartupTask):

Wake On LAN Proxy: Declarations
Wake On LAN Proxy: Declarations

Testen und Debuggen der App

Nun kann die App auf dem Raspberry ausgeführt werden. Dazu muss man zunächst in die Projekt-Eigenschaften wechseln und die Debug-Optionen wählen. Unter Start options ist hier Remotecomputer zu anzugeben. Unter Remote machine gibt man nun den Namen (minwinpc) oder die IP-Adresse des Raspberry an. Wichtig dabei ist nur, dass die Option Use authentication deaktiviert ist.

Einstellungen für Remote-Debugging
Einstellungen für Remote-Debugging

Nach dem Speichern der Einstellungen wählt man nun in der Menüleiste ARM als Zielplatform (da auf dem Raspberry nur ARM-Programme ausgeführt werden können) und daneben Remotecomputer. U.U. ist es noch einmal erforderlich, den Namen oder die IP-Adresse des Raspberry einzugeben (s.o.).

ARM-Plattform zum Debuggen auf dem Remotecomputer
ARM-Plattform zum Debuggen auf dem Remotecomputer

Nun kann die App ganz normal im Visual Studio gedebuggt werden. Um das Ganze in Aktion zu sehen, kann man den Router mit DynDNS im Internet betreiben und eine Portfreigabe auf dem Raspberry einrichten (UDP Port 9). Nun benötigt man nur noch ein Programm oder eine App, die ein sog. Wake On WAN Signal senden kann, wie z.B. MagicPacket. Dieses wird nun folgendermaßen konfiguriert:

  • MAC-Adresse des zu startenden PCs: Hier wird eine MAC-Adresse eines Computers im lokalen Netzwerk eingegeben (nur nicht die des Raspberry).
  • Host-Adresse/URL: Die DynDNS-Adresse des Routers, unter der dieser im Internet erreichbar ist.
  • Port: Hier ist Port 9 anzugeben, da die Windows 10 IoT App auf diesem Port „lauscht“.

Nun kann beispielsweise ein Breakpoint im Visual Studio in der Methode Socket_MessageReceived gesetzt werden. Sobald nun ein Wake On WAN Befehl von außen kommt, kann man ab diesem Breakpoint den Programmablauf gut nachvollziehen.

Wake On LAN Proxy beim Start des Raspberry automatisch starten

Der letzte Schritt ist nun die Konfiguration auf dem Raspberry selbst, damit die App automatisch geladen wird, sobald der Raspberry gestartet wurde.

Die App wurde durch das Remote-Debugging über das Visual Studio bereits auf dem Raspberry „installiert“. Dies kann man überprüfen, indem man sich in der Web-Oberfläche des Raspberry unter Apps die Liste der auf dem Gerät installierten Apps anzeigen lässt.

Die installierte App auf Windows 10 IoT
Die installierte App auf Windows 10 IoT

Die weitere Konfiguration erfolgt nun mittels PowerShell. Wie man sich mit der PowerShell auf den Raspberry verbindet, habe ich im Artikel Windows 10 IoT auf dem Raspberry Pi 2 – Einrichtung und Administration bereits beschrieben.

Damit die App nun automatisch mit gestartet wird, wenn der Raspberry gebootet wird, sind nun folgende Befehle notwendig:

iotstartup list wolproxy

Hier sollte die App WolProxy zwei Mal aufgeführt sein: einmal als headed und einmal als headless.

Da der WolProxy keine Oberfläche besitzt, soll dieser nur im headless-Modus gestartet werden:

iotstartup add headless wolproxy

Nun sollte eine Erfolgsmeldung kommen, dass die App sozusagen mit in den Autostart mit aufgenommen wurde.

WolProxy automatisch mit dem Raspberry starten
WolProxy automatisch mit dem Raspberry starten

Zu guter Letzt muss der Raspberry nun nur noch neu gestartet werden:

shutdown /r /t 0

Sobald der Kleinstcomputer erfolgreich neu gestartet wurde, läuft der WolProxy automatisch, auch wenn man ihn nicht explizit startet.

Der Raspberry nach dem Neustart: der WolProxy läuft
Der Raspberry nach dem Neustart: der WolProxy läuft

Um die App wieder aus dem Autostart zu entfernen, ist folgender Befehl auszuführen:

iotstartup remove headless wolproxy

Zusammenfassung

Der Artikel hat gezeigt, wie man eine eigene App für Windows 10 IoT auf dem Raspberry Pi 2 mit Visual Studio und C# entwickeln kann. Die in diesem Rahmen entstandene Wake On LAN Proxy App ist bewusst sehr einfach gehalten, aber die generellen Abläufe bei der App-Entwicklung sollten damit klar geworden sein. Diese App kann nun natürlich beliebig erweitert werden, beispielsweise mit einer Log-Funktion, etc. Die Visual Studio Solution gibt es dafür weiter unten zum Download.

Der Entwicklungs-Workflow ist dabei noch etwas holprig – jedoch darf man nicht vergessen, dass es sich bei Windows 10 IoT z.Zt. noch um eine sehr frühe Preview-Version handelt. An dieser Stelle wird sich vermutlich in Zukunft noch einiges tun. So ist es z.B. auch denkbar, dass Microsoft einen App-Store für Windows IoT Geräte anbietet. Dann wäre die Entwicklung und das Deployment von Apps ebenso einfach wie auf Windows oder Windows Phone.

Ich bin auf jeden Fall gespannt, was die Zukunft bringt – das Potential von Windows 10 IoT ist auf jeden Fall vorhanden…

Sourcecode der kompletten Solution

Die komplette Visual Studio Solution (mit ein paar Erweiterungen, z.B. einer Log-Funktion) ist auf Codeberg zu finden:

IoT.WakeOnLanProxy auf Codeberg

Das Projekt steht unter der MIT Lizenz, daher kann das Projekt beliebig verwendet oder weiter gegeben werden.

Weiterführende Artikel

Links

2 Kommentare zu „Windows 10 IoT auf dem Raspbery Pi 2 – App-Entwicklung“

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert