Licht durch die Wolken – Homeautomation mit Windows 10, IoT Core, Cortana und Azure (Teil2 Remote App)

Nachdem sich der erste Teil dieser Blogreihe dem eigentlichen Szenario, sowie der Einrichtung der Kommunikationswege über Microsoft Azure gewidmet hat, dreht sich der zweite Teil um das Erstellen einer Windows 10 App zum Kommunizieren mit dem RaspberryPI2 und somit der Lampe. Dank der Integration von Cortana ist dies auch über Sprachbefehle möglich.

 

Kurzversion:
Wer direkt den Code im finalen Zustand verwenden und nachvollziehen möchte, kann sich diesen von GitHub herunterladen und ausprobieren. Der Code ist HIER zu finden.

Langversion:
Für das Erstellen der benötigten Solutions mit ihren Projekten für die folgenden Windows 10 Apps ist Visual Studio 2015 erforderlich. Eine kostenlose Möglichkeit bietet Visual Studio 2015 Community, welche HIER zum Download bereitsteht.

Im ersten Step gilt es nun eine Solution zu erstellen, sowie einem Projekt für die Windows Universal Windows Platform App. Die leere Solution kann über die Project-Templates in Visual Studio ausgewählt werden:

 BlankSolution

Nachdem die Solution erstellt ist, kann ein einzelnes App-Projekt hinzugefügt werden.

AddNewProject

In den Project-Templates sollte hier eine Blank App (Universal Windows) gewählt werden.

NewProjectVoiceRemoteApp

Die Integration von Cortana

Der erste Schritt nachdem alles angelegt ist sollte die Integration von Cortana sein, um die Lampe (bzw. alles was später vielleicht sonst am RaspberryPi2 verfügbar sein soll) mit der Sprache steuern zu können. Dank der Integration von Cortana in die Core-Features aller Windows 10 Ausprägungen, sind keine weiteren API´s oder Extensions notwendig um Cortana in die eigene App zu integrieren.
Damit Cortana weiß, auf welche unserer Befehle sie reagieren und somit in unserer App agieren soll, müssen wir ihr einen „Beipackzettel“ geben. In diesem sind für Cortana alle Schlüsselbegriffe (und deren möglicher Kombinationen) aufgelistet, und einer Action zugewiesen. Die genannten Parameter können in diesem Voice Command Definition File auch für andere verfügbare Sprachen definiert werden. Cortana zieht abhängig von der Geräteeinstellung die richtige Sprache heran.
Das Definition File wird im XML Format erwartet, und kann einfach im Root des Projektes angelegt werden (aber auch in jedem beliebigen Unterordner). Der Inhalt für dieses
Szenario sollte so aussehen und VoiceCommands.xml heißen:

xmlSnippet

Somit können direkt die benötigten Code-Anpassungen vorgenommen werden. Dafür sind in der App.xaml.cs einige Anpassungen zu machen.
Dieses Definition File wird bei jedem Start der App auf dem Gerät erneut installiert. Warum? Da Cortana recht intelligent an Hand der Häufigkeit von Befehlen und Aufrufen verwaltet, welche Definitionen sie sich merken sollte, „vergisst“ sie auch eben die Definitionen wieder, die nie verwendet werden. Wird also eine App manuell aufgerufen, erneuert sich automatisch der „Merker“ von Cortana.
Das Installieren des Defintion Files erfolgt in der App.xaml.cs. Dafür ist der Code der „OnLaunched“-Methode anzupassen. Die modifizierte Variante sollte am Ende so aussehen:

  1. protected override async void OnLaunched(LaunchActivatedEventArgs e)
  2.         {
  3.             #if DEBUG
  4.             if (System.Diagnostics.Debugger.IsAttached)
  5.             {
  6.                 this.DebugSettings.EnableFrameRateCounter = true;
  7.             }
  8.             #endif
  9.             rootFrame = Window.Current.Content as Frame;
  10.  
  11.             // Do not repeat app initialization when the Window already has content,
  12.             // just ensure that the window is active
  13.             if (rootFrame == null)
  14.             {
  15.                 // Create a Frame to act as the navigation context and navigate to the first page
  16.                 rootFrame = new Frame();
  17.  
  18.                 rootFrame.NavigationFailed += OnNavigationFailed;
  19.  
  20.                 if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
  21.                 {
  22.                     //TODO: Load state from previously suspended application
  23.                 }
  24.  
  25.                 // Place the frame in the current Window
  26.                 Window.Current.Content = rootFrame;
  27.             }
  28.  
  29.             if (rootFrame.Content == null)
  30.             {
  31.                 // When the navigation stack isn't restored navigate to the first page,
  32.                 // configuring the new page by passing required information as a navigation
  33.                 // parameter
  34.                 rootFrame.Navigate(typeof(MainPage), e.Arguments);
  35.             }
  36.             // Ensure the current window is active
  37.             //Window.Current.Activate();
  38.  
  39.             //Installing the Voice Command File (every start of the app)
  40.             var vcFile = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///VoiceCommands.xml"));
  41.             await Windows.ApplicationModel.VoiceCommands.VoiceCommandDefinitionManager.InstallCommandDefinitionsFromStorageFileAsync(vcFile);
  42.         }

Das generelle Setzen des RootFrames und Aktivieren als Windows.Current wurde in der OnLaunched-Methode auskommentiert. Damit wir diesen Vorgang an einer anderen Stelle abhängig von Cortana-Entscheidungen nachholen können, braucht es folgende ausgelagerte Methode in der App.xaml.cs:

  1. private async void EnsureRootFrame(ApplicationExecutionState previousExecutionState)
  2.         {
  3.             this.rootFrame = Window.Current.Content as Frame;
  4.  
  5.             // Do not repeat app initialization when the Window already has content,
  6.             // just ensure that the window is active
  7.             if (this.rootFrame == null)
  8.             {
  9.                 // Create a Frame to act as the navigation context and navigate to the first page
  10.                 this.rootFrame = new Frame();
  11.  
  12.                 this.rootFrame.CacheSize = 1;
  13.  
  14.                
  15.                 // Place the frame in the current Window
  16.                 Window.Current.Content = this.rootFrame;
  17.             }
  18.  
  19.             // Ensure the current window is active
  20.             Window.Current.Activate();
  21.         }

Damit Cortana die eingegebenen Commands (gesprochen oder auch geschrieben) interpretieren kann, ist in der App.xaml.cs folgende Methode notwendig, um den Command-Abgleich durchzuführen:

  1. protected override void OnActivated(IActivatedEventArgs e)
  2.         {
  3.             if (e.Kind != Windows.ApplicationModel.Activation.ActivationKind.VoiceCommand)
  4.             {
  5.                 return;
  6.             }
  7.  
  8.             var commandArgs = e as Windows.ApplicationModel.Activation.VoiceCommandActivatedEventArgs;
  9.             Windows.Media.SpeechRecognition.SpeechRecognitionResult speechRecognitionResult = commandArgs.Result;
  10.  
  11.             // TCommand Mode kann sein Texteingabe, oder Spracheingabe
  12.             string commandMode = this.SemanticInterpretation("commandMode", speechRecognitionResult);
  13.  
  14.             // If so, get the name of the voice command, the actual text spoken, and the value of Command/Navigate@Target.
  15.             string voiceCommandName = speechRecognitionResult.RulePath[0];
  16.             string textSpoken = speechRecognitionResult.Text;
  17.             string navigationTarget = this.SemanticInterpretation("NavigationTarget", speechRecognitionResult);
  18.  
  19.             Type navigateToPageType = typeof(MainPage);
  20.             string navigationParameterString = string.Empty;
  21.  
  22.             switch (voiceCommandName)
  23.             {
  24.                 case "CallToAction":
  25.                    
  26.                     navigateToPageType = typeof(MainPage);
  27.                     navigationParameterString = "";
  28.                     break;
  29.             }
  30.  
  31.             this.EnsureRootFrame(e.PreviousExecutionState);
  32.             if (!this.rootFrame.Navigate(navigateToPageType, navigationParameterString))
  33.             {
  34.                 throw new Exception("Sprachbefehle konnte nicht entgegen genommen werden");
  35.             }
  36.  
  37.  
  38.             //base.OnActivated(e);
  39.         }

Wird nun das Sprachkommando (die Zusammensetzung von möglichen Schlüsselwörtern im Command Definition File, welches oben angelegt wurde) identifiziert, wird die dazu hinterlegte Action ausgeführt. In diesem Fall wird in der App zur „MainPage.xaml“ navigiert. Zusätzlich zur Action „Navigate“ können dem Vorgang auch Parameter mitgegeben werden, die beim Laden der Seite interpretiert werden können (sinnvoll wenn mehr als nur ein Endgerät zu steuern sein soll).

Kommunikation mit der Azure Queue im Service Bus

Ist Cortana erfolgreich integriert, geht es an die Verbindung zur Queue im Azure Service Bus, in der die Befehle für den Raspberry PI2 abgelegt werden sollen. Erfreulicherweise ist dieses Vorhaben noch einfacher und schneller umgesetzt, als die recht einfache Integration von Cortana.

Für die Kommunikation von Windows Universal Apps und den Messaging Produkten im Azure Service Bus gibt es ein wertvolles NuGet-PackageWindowsAzure.Messaging.Managed“, welches die Hauptarbeit der Implementierung beinhaltet und somit erledigt. Den Referenz-Verweisen des Projektes kann dieses NuGet-Package über „Manage NuGet Packages…“ hinzugefügt werden.

 

AddNuGetPackageMessaging

Um das eingebundene Package zu verwenden, werden die MainPage.xaml und die MainPage.xaml.cs angepasst. In diesem Szenario wird die Lampe per Touch wieder ausgeschalten. Damit dies erfolgen kann, wird der Seite ein Textblock hinzugefügt. Wird dieser gedrückt, sendet die App einen Befehl zum Ausschalten der Lampe:

MainPage.xaml

MainpageXAML

In der dazugehörigen CodeBehind-Datei wird nun folgendes erweitert:

 

MainPage.xaml.cs

 

  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Runtime.InteropServices.WindowsRuntime;
  6. using Windows.Foundation;
  7. using Windows.Foundation.Collections;
  8. using Windows.UI.Xaml;
  9. using Windows.UI.Xaml.Controls;
  10. using Windows.UI.Xaml.Controls.Primitives;
  11. using Windows.UI.Xaml.Data;
  12. using Windows.UI.Xaml.Input;
  13. using Windows.UI.Xaml.Media;
  14. using Windows.UI.Xaml.Navigation;
  15. using Microsoft.WindowsAzure.Messaging;
  16.  
  17. // The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409
  18.  
  19. namespace VoiceRemoteApp
  20. {
  21.     ///
  22. <summary>
  23.     /// An empty page that can be used on its own or navigated to within a Frame.
  24.     /// </summary>
  25.  
  26.    
  27.     public sealed partial class MainPage : Page
  28.     {
  29.         //the Queuemanager
  30.         private Queue myQueueClient;
  31.  
  32.         //your Azure-Informations
  33.         private const string azureConnectionString = "<Insert your Service Bus Connection String from the portal here>";
  34.         private const string queueName = "<Insert your queuename here";
  35.  
  36.         public MainPage()
  37.         {
  38.             this.InitializeComponent();
  39.            
  40.         }
  41.  
  42.         protected async override void OnNavigatedTo(NavigationEventArgs e)
  43.         {
  44.             myQueueClient = new Queue(queueName,azureConnectionString);
  45.             await myQueueClient.SendAsync(new Message("turn on"));
  46.             base.OnNavigatedTo(e);
  47.  
  48.         }
  49.  
  50.         private async void Output_Tapped(object sender, TappedRoutedEventArgs e)
  51.         {
  52.             if(myQueueClient != null)
  53.             {
  54.                 await myQueueClient.SendAsync(new Message("turn off"));
  55.             }
  56.         }
  57.     }
  58. }

Sobald Cortana zur Page navigiert, wird in der Methohde „OnNavigatedTo“ eine Verbindung zur Queue hergestellt und eine neue Message mit dem Befehl „turn on“ gesendet. Für den Fall, dass der Textblock auf der Page gedrückt wird, sorgt die Handler-Methode „Output_Tapped“ dafür, dass eine Message mit dem Befehl „turn off“ in die Queue im Service Bus publiziert wird.

Somit sind alle Arbeiten abgeschlossen, und die Remote App ist einsatzbereit. Der dritte und vierte Teil der Blogserie wird sich nun dem RaspberryPI2, die Verdrahtung und der App widmen, welche auf dem kleinen Controller die Nachrichten aus der Queue entgegen nimmt und für die Schaltung der Lampe sorgt.

Summary

Leave a reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>