Für diese Projekt setzen wir voraus, dass das Windows Phone SDK eingerichtet ist und ein grundsätzliches Verständnis der C# Programmiersprache vorhanden ist.
Falls dies nicht der Fall ist sollte hier begonnen werden. Informationen über die Tinkerforge API sind dann hier zu finden.
Wir setzen weiterhin voraus, dass die Fernbedienung mit einem Industrial Quad Relay Bricklet verbunden wurde wie hier beschrieben.
Das vollständige Visual Studio Projekt kann hier heruntergeladen werden. Eine Demo-App basierend auf diesem Projekt steht im
In diesem Projekt werden wir eine einfach Windows Phone App entwickeln, die die Funktionalität der eigentlichen Fernbedienung nachbildet.
Nach dem Erstellen eines neuen "Windows Phone App" namens "Garage Control" in Visual Studio beginnen wir mit der Erstellung der GUI:
An das bereits bestehende "LayoutRoot" Element wird ein "StackPanel" angehängt,
das dann alle weiteren GUI Elemente aufnehmen wird. Drei "TextBoxes" ermöglichen
die Eingabe von Host, Port und UID des Industrial Quad Relay Bricklets. Für die
Port Textbox wird das Attribut InputScope="Number"
gesetzt, dadurch wird
Windows Phone den Inhalt dieser Textbox auf Zahlen beschränken. Unterhalb der
Textboxen folgt eine "ProgressBar" (nicht sichtbar im Screenshot) die einen
laufenden Verbindungsversuch
anzeigen wird. Die letzten beiden Elemente sind die "Button" für den Aufbau und
das Trennen der Verbindung sowie das Auslösen eines Tastendrucks auf der
gehackten Fernbedienung. Hier ein Auszug aus der MainPage.xaml
Datei:
<phone:PhoneApplicationPage>
<Grid x:Name="LayoutRoot" Background="Transparent">
<!-- [...] -->
<StackPanel x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<TextBlock TextWrapping="Wrap" Text="Host" VerticalAlignment="Top"/>
<TextBox x:Name="host" Height="72" TextWrapping="Wrap" Text="192.168.178.46" VerticalAlignment="Top"/>
<TextBlock TextWrapping="Wrap" Text="Port" VerticalAlignment="Top"/>
<TextBox x:Name="port" Height="72" TextWrapping="Wrap" Text="4223" VerticalAlignment="Top"
InputScope="Number"/>
<TextBlock TextWrapping="Wrap" Text="UID (Industrial Quad Relay Bricklet)" VerticalAlignment="Top"/>
<TextBox x:Name="uid" Height="72" TextWrapping="Wrap" Text="ctG" VerticalAlignment="Top"/>
<ProgressBar x:Name="progress" Height="10" VerticalAlignment="Top"/>
<Button x:Name="connect" Content="Connect" VerticalAlignment="Top" Click="Connect_Click"/>
<Button x:Name="trigger" Content="Trigger" VerticalAlignment="Top" Height="144" Click="Trigger_Click"/>
</StackPanel>
</Grid>
</phone:PhoneApplicationPage>
Damit ist das Layout des GUIs fertig. Die initiale Konfiguration des GUIs wird
im Konstruktor der MainPage
Klasse vorgenommen. Die "ProgressBar" ist am
Anfang nicht sichtbar und Indeterminate Mode wird aktiviert, da die Dauer eines
Verbindungsversuchs unbekannt ist:
public partial class MainPage : PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
progress.Visibility = Visibility.Collapsed;
progress.IsIndeterminate = true;
}
}
Dieser Schritt ist ähnlich zu Schritt 1 des
Rauchmelder mit C# auslesen Projekts.
Einige Änderungen sind notwendig damit es in einem GUI Programm funktioniert.
Statt dem EnumerateCallback
zum Erkennen des Industrial Quad Relay
Bricklets verwenden muss dessen UID angegeben werden. Dieser Ansatz erlaubt es
gezielt ein Industrial Quad Relay Bricklet auszuwählen, selbst wenn mehrere am
gleichen Host angeschlossen sind.
Die Connect()
Methode soll nicht direkt aufgerufen werden, da dies einen
Moment dauern kann und in dieser Zeit die GUI nicht auf den Benutzer reagieren
könnte. Daher wird Connect()
aus einem BackgroundWorker
aufgerufen werden, so
dass es im Hintergrund ausgeführt und die GUI nicht blockiert wird
public partial class MainPage : PhoneApplicationPage
{
private IPConnection ipcon = null;
private BrickletIndustrialQuadRelay relay = null;
private BackgroundWorker connectWorker = null;
public MainPage()
{
// [...]
connectWorker = new BackgroundWorker();
connectWorker.DoWork += ConnectWorker_DoWork;
}
private void ConnectWorker_DoWork(object sender, DoWorkEventArgs e)
{
string[] argument = e.Argument as string[];
ipcon = new IPConnection();
relay = new BrickletIndustrialQuadRelay(argument[2], ipcon);
ipcon.Connect(argument[0], Convert.ToInt32(argument[1]));
}
private void Connect()
{
string[] argument = new string[3];
argument[0] = host.Text;
argument[1] = port.Text;
argument[2] = uid.Text;
connectWorker.RunWorkerAsync(argument);
}
}
Der BackgroundWorker
hat eine DoWork
-Event das von einem anderen Thread
ausgelöst wird sobald RunWorkerAsync()
aufgerufen wurde. Die Host, Port und
UID Konfiguration wird dann an das DoWork
-Event übergeben. Dies ist
notwendig, da die ConnectWorker_DoWork()
Methode diese Informationen
benötigt, selbst aber nicht auf die GUI Elemente zugreifen darf. Jetzt kann die
ConnectWorker_DoWork()
Methode die IPConnection
und
BrickletIndustrialQuadRelay
Objekte erzeugen und Connect()
aufrufen.
Schlussendlich soll der BackgroundWorker
gestartet werden, wenn der
Connect-Knopf geklickt wird. Dafür wird die Connect_Click()
Methode an das
Click
-Event des Connect-Knopfes gebunden:
private void Connect_Click(object sender, RoutedEventArgs e)
{
Connect();
}
Host, Port und UID können jetzt eingestellt werden und ein Klick auf den Connect Knopf stellt die Verbindung her.
Die Verbindung ist hergestellt und das Industrial Quad Relay Bricklet wurde gefunden, aber es fehlt noch die Logik um einen Taster auf der Fernbedienung auszulösen wenn der Knopf geklickt wurde.
Dafür wird die Trigger_Click()
Methode an das Click
-Event des
Trigger-Knopfes gebunden. Diese startet einen anderen BackgroundWorker
der
dann wiederum die SetMonoflop()
Methode des Industrial Quad Relay Bricklet
aufruft um einen Taster auf der gehackten Fernbedienung zu drücken:
public partial class MainPage : PhoneApplicationPage
{
// [...]
private BackgroundWorker triggerWorker = null;
public MainPage()
{
// [...]
triggerWorker = new BackgroundWorker();
triggerWorker.DoWork += TriggerWorker_DoWork;
}
private void TriggerWorker_DoWork(object sender, DoWorkEventArgs e)
{
relay.SetMonoflop(1 << 0, 1 << 0, 1500);
}
private void Trigger_Click(object sender, RoutedEventArgs e)
{
triggerWorker.RunWorkerAsync();
}
}
Der Aufruf von SetMonoflop(1 << 0, 1 << 0, 1500)
schließt das erste Relais für 1,5s und öffnet
es dann wieder.
Das ist es. Wenn wir diese drei Schritte zusammen in ein Projekt kopieren, dann hätten wir jetzt eine funktionierende App, die es ermöglicht vom Smartphone aus den Garagentoröffner mittels dessen gehackter Fernbedienung zu steuern.
Es fehlt noch ein Disconnect-Knopf und der Trigger-Knopf kann auch geklickt werden obwohl keine Verbindung besteht. Es fehlt also noch etwas mehr GUI-Logik!
Es gibt noch keinen Knopf um die Verbindung wieder zu trennen nachdem sie aufgebaut wurde. Der Connect-Knopf könnte dies tun. Wenn die Verbindung aufgebaut ist sollte er bei einem Klick die Verbindung wieder trennen:
public partial class MainPage : PhoneApplicationPage
{
// [...]
public MainPage()
{
// [...]
connectWorker.RunWorkerCompleted += ConnectWorker_RunWorkerCompleted;
}
private void ConnectWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
connect.Content = "Disconnect";
}
}
Die ConnectWorker_RunWorkerCompleted()
Methode wird nach
ConnectWorker_DoWork()
aufgerufen. sie ändert den Text des Knopfes zu
"Disconnect". Die Connect_Click()
Methode entscheidet nun dynamisch was zu
tun ist. Falls keine Verbindung besteht wird Connect()
aufgerufen,
andernfalls wird der BackgroundWorker
für das Trennen der Verbindung
gestartet:
private void Connect_Click(object sender, RoutedEventArgs e)
{
if (ipcon == null || ipcon.GetConnectionState() == IPConnection.CONNECTION_STATE_DISCONNECTED)
{
Connect();
}
else
{
disconnectWorker.RunWorkerAsync();
}
}
Der BackgroundWorker
für das Trennen der Verbindung ruft die
Disconnect()
Methode im Hintergrund
auf, da dies einen Moment dauern kann und während dieser Zeit die GUI blockiert
wäre:
public partial class MainPage : PhoneApplicationPage
{
// [...]
private BackgroundWorker disconnectWorker = null;
public MainPage()
{
// [...]
disconnectWorker = new BackgroundWorker();
disconnectWorker.DoWork += DisconnectWorker_DoWork;
disconnectWorker.RunWorkerCompleted += DisconnectWorker_RunWorkerCompleted;
}
private void DisconnectWorker_DoWork(object sender, DoWorkEventArgs e)
{
ipcon.Disconnect();
}
private void DisconnectWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
connect.Content = "Connect";
}
}
Außerdem sollte der Benutzer nicht den Inhalt der Eingabefelder ändern können solange die Verbindung aufgebaut wird oder besteht und der Trigger-Knopf sollte nicht klickbar sein wenn keine Verbindung besteht.
Der connectWorker
und der disconnectWorker
werden so erweitert,
dass sie die GUI Elemente abhängig von dem aktuellen Zustand der Verbindung
aktivieren oder deaktivieren:
private void ConnectWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
// [...]
connect.IsEnabled = true;
trigger.IsEnabled = true;
}
private void DisconnectWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
// [...]
host.IsEnabled = true;
port.IsEnabled = true;
uid.IsEnabled = true;
connect.IsEnabled = true;
}
private void Connect()
{
host.IsEnabled = false;
port.IsEnabled = false;
uid.IsEnabled = false;
connect.IsEnabled = false;
trigger.IsEnabled = false;
// [...]
}
Das Programm ist noch nicht robust genug. Was passiert wenn die Verbindung nicht hergestellt werden kann? Was passiert wenn kein Industrial Quad Relay Bricklet mit passender UID gefunden werden kann?
Wir brauchen noch Fehlerbehandlung!
Es werden die gleichen Konzepte wie in Schritt 4 des Rauchmelder mit C# auslesen Projekts angewandt, allerdings mit Abwandlungen damit sie in einem GUI Programm funktionieren.
Wir können nicht einfach System.Console.WriteLine()
für Fehlermeldungen
verwenden, da dies ein GUI Programm ist und kein Konsolenfenster hat.
Stattdessen werden Messageboxen verwendet.
Die Connect()
Methode muss die Benutzereingaben validieren bevor sie
verwendet werden. Mittels MessageBox
werden mögliche Probleme gemeldet.
Die "ProgressBar" wird eingeblendet um auf den laufenden Verbindungsversuch
hinzuweisen:
private void Connect()
{
if (host.Text.Length == 0 || port.Text.Length == 0 || uid.Text.Length == 0)
{
MessageBox.Show("Host/Port/UID cannot be empty", "Error", MessageBoxButton.OK);
return;
}
progress.Visibility = Visibility.Visible;
// [...]
}
private void ConnectWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
progress.Visibility = Visibility.Collapsed;
// [...]
}
Dann benötigt die ConnectWorker_DoWork()
Methode noch eine Möglichkeit das
Ergebnis des Verbindungsversuchs mitzuteilen. Sie darf zwar nicht direkt mit
dem GUI interagieren, kann aber einen Wert an den Result
Member des
DoWorkEventArgs
Parameters zuweisen, der dann an den Aufruf der
ConnectWorker_RunWorkerCompleted()
Methode übergeben wird. Wir verwenden
hier ein enum
, das die drei möglichen Ausgänge eines Verbindungsversuchs
repräsentiert:
enum ConnectResult
{
SUCCESS,
NO_CONNECTION,
NO_DEVICE
}
Mit der GetIdentity()
Methode wird überprüft, ob die angegebene UID
wirklich zu einem Industrial Quad Relay Bricklet gehört. Falls das nicht der
Fall ist wird die Verbindung getrennt:
private void ConnectWorker_DoWork(object sender, DoWorkEventArgs e)
{
string[] argument = e.Argument as string[];
ipcon = new IPConnection();
try
{
relay = new BrickletIndustrialQuadRelay(argument[2], ipcon);
}
catch (ArgumentOutOfRangeException)
{
e.Result = ConnectResult.NO_DEVICE;
return;
}
try
{
ipcon.Connect(argument[0], Convert.ToInt32(argument[1]));
}
catch (System.IO.IOException)
{
e.Result = ConnectResult.NO_CONNECTION;
return;
}
catch (ArgumentOutOfRangeException)
{
e.Result = ConnectResult.NO_CONNECTION;
return;
}
try
{
string uid;
string connectedUid;
char position;
byte[] hardwareVersion;
byte[] firmwareVersion;
int deviceIdentifier;
relay.GetIdentity(out uid, out connectedUid, out position,
out hardwareVersion, out firmwareVersion, out deviceIdentifier);
if (deviceIdentifier != BrickletIndustrialQuadRelay.DEVICE_IDENTIFIER)
{
ipcon.Disconnect();
e.Result = ConnectResult.NO_DEVICE;
return;
}
}
catch (TinkerforgeException)
{
try
{
ipcon.Disconnect();
}
catch (NotConnectedException)
{
}
e.Result = ConnectResult.NO_DEVICE;
return;
}
e.Result = ConnectResult.SUCCESS;
}
Die ConnectWorker_RunWorkerCompleted()
Methode muss jetzt entsprechend
reagieren. Als erstes wird immer die "ProgressBar`` ausgeblendet:
private void ConnectWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
ConnectResult result = (ConnectResult)e.Result;
progress.Visibility = Visibility.Collapsed;
Im Falle, dass die Verbindung erfolgreich war bleibt die ursprüngliche Logik bestehen:
if (result == ConnectResult.SUCCESS)
{
connect.Content = "Disconnect";
connect.IsEnabled = true;
trigger.IsEnabled = true;
}
Im Fehlerfall wird eine MessageBox
mit einer entsprechenden Meldung
angezeigt:
else
{
string message;
MessageBoxResult retry;
if (result == ConnectResult.NO_CONNECTION) {
message = "Could not connect to " + host.Text + ":" + port.Text + ". Retry?";
} else { // ConnectResult.NO_DEVICE
message = "Could not find Industrial Quad Relay Bricklet [" + uid.Text + "]. Retry?";
}
retry = MessageBox.Show(message, "Error", MessageBoxButton.OKCancel);
Abhängig vom Ergebnis der MessageBox
wird dann der Verbindungsversuch
abgebrochen oder wiederholt:
if (retry == MessageBoxResult.OK) {
Connect();
} else {
host.IsEnabled = true;
port.IsEnabled = true;
uid.IsEnabled = true;
connect.Content = "Connect";
connect.IsEnabled = true;
}
}
}
Die App kann sich zum eingestellten Host und Port verbinden und einen Taster auf der Fernbedienung des Garagentoröffners mittels eines Industrial Quad Relay Bricklets betätigen.
Die App speichert die Konfiguration noch nicht. Windows Phone bietet hierfür die
IsolatedStorageSettings
Klasse. In OnNavigatedTo()
wird die gespeicherte
Konfiguration wieder geladen und die Verbindung wiederhergestellt wenn sie zuvor
bestanden hat:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
bool connected = false;
try
{
host.Text = settings["host"] as string;
port.Text = settings["port"] as string;
uid.Text = settings["uid"] as string;
connected = settings["connected"].Equals(true);
}
catch (KeyNotFoundException)
{
settings["host"] = host.Text;
settings["port"] = port.Text;
settings["uid"] = uid.Text;
settings["connected"] = connected;
settings.Save();
}
if (connected &&
(ipcon == null ||
ipcon.GetConnectionState() == IPConnection.CONNECTION_STATE_DISCONNECTED))
{
Connect();
}
}
In OnNavigatedFrom()
wird die Konfiguration dann wieder gespeichert:
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
settings["host"] = host.Text;
settings["port"] = port.Text;
settings["uid"] = uid.Text;
if (ipcon != null && ipcon.GetConnectionState() == IPConnection.CONNECTION_STATE_CONNECTED)
{
settings["connected"] = true;
}
else
{
settings["connected"] = false;
}
settings.Save();
}
Jetzt wird die Konfiguration und der Zustand dauerhaft. auch über einen Neustart der App hinweg, gespeichert.
Das ist es! Die App für die gehackte Fernbedienung des Garagentoröffners ist fertig.
Das Hauptprogramm in einem Stück (Download):
using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.IO.IsolatedStorage;
using System.Windows;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using Tinkerforge;
namespace GarageControl
{
public partial class MainPage : PhoneApplicationPage
{
private IPConnection ipcon = null;
private BrickletIndustrialQuadRelay relay = null;
private BackgroundWorker connectWorker = null;
private BackgroundWorker disconnectWorker = null;
private BackgroundWorker triggerWorker = null;
private IsolatedStorageSettings settings = IsolatedStorageSettings.ApplicationSettings;
enum ConnectResult
{
SUCCESS,
NO_CONNECTION,
NO_DEVICE
}
public MainPage()
{
InitializeComponent();
progress.Visibility = Visibility.Collapsed;
progress.IsIndeterminate = true;
trigger.IsEnabled = false;
connectWorker = new BackgroundWorker();
connectWorker.DoWork += ConnectWorker_DoWork;
connectWorker.RunWorkerCompleted += ConnectWorker_RunWorkerCompleted;
disconnectWorker = new BackgroundWorker();
disconnectWorker.DoWork += DisconnectWorker_DoWork;
disconnectWorker.RunWorkerCompleted += DisconnectWorker_RunWorkerCompleted;
triggerWorker = new BackgroundWorker();
triggerWorker.DoWork += TriggerWorker_DoWork;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
bool connected = false;
try
{
host.Text = settings["host"] as string;
port.Text = settings["port"] as string;
uid.Text = settings["uid"] as string;
connected = settings["connected"].Equals(true);
}
catch (KeyNotFoundException)
{
settings["host"] = host.Text;
settings["port"] = port.Text;
settings["uid"] = uid.Text;
settings["connected"] = connected;
settings.Save();
}
if (connected &&
(ipcon == null ||
ipcon.GetConnectionState() == IPConnection.CONNECTION_STATE_DISCONNECTED))
{
Connect();
}
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
settings["host"] = host.Text;
settings["port"] = port.Text;
settings["uid"] = uid.Text;
if (ipcon != null && ipcon.GetConnectionState() == IPConnection.CONNECTION_STATE_CONNECTED)
{
settings["connected"] = true;
}
else
{
settings["connected"] = false;
}
settings.Save();
}
private void ConnectWorker_DoWork(object sender, DoWorkEventArgs e)
{
string[] argument = e.Argument as string[];
ipcon = new IPConnection();
try
{
relay = new BrickletIndustrialQuadRelay(argument[2], ipcon);
}
catch (ArgumentOutOfRangeException)
{
e.Result = ConnectResult.NO_DEVICE;
return;
}
try
{
ipcon.Connect(argument[0], Convert.ToInt32(argument[1]));
}
catch (System.IO.IOException)
{
e.Result = ConnectResult.NO_CONNECTION;
return;
}
catch (ArgumentOutOfRangeException)
{
e.Result = ConnectResult.NO_CONNECTION;
return;
}
try
{
string uid;
string connectedUid;
char position;
byte[] hardwareVersion;
byte[] firmwareVersion;
int deviceIdentifier;
relay.GetIdentity(out uid, out connectedUid, out position,
out hardwareVersion, out firmwareVersion, out deviceIdentifier);
if (deviceIdentifier != BrickletIndustrialQuadRelay.DEVICE_IDENTIFIER)
{
ipcon.Disconnect();
e.Result = ConnectResult.NO_DEVICE;
return;
}
}
catch (TinkerforgeException)
{
try
{
ipcon.Disconnect();
}
catch (NotConnectedException)
{
}
e.Result = ConnectResult.NO_DEVICE;
return;
}
e.Result = ConnectResult.SUCCESS;
}
private void ConnectWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
ConnectResult result = (ConnectResult)e.Result;
progress.Visibility = Visibility.Collapsed;
if (result == ConnectResult.SUCCESS)
{
connect.Content = "Disconnect";
connect.IsEnabled = true;
trigger.IsEnabled = true;
}
else
{
string message;
MessageBoxResult retry;
if (result == ConnectResult.NO_CONNECTION) {
message = "Could not connect to " + host.Text + ":" + port.Text + ". Retry?";
} else { // ConnectResult.NO_DEVICE
message = "Could not find Industrial Quad Relay Bricklet [" + uid.Text + "]. Retry?";
}
retry = MessageBox.Show(message, "Error", MessageBoxButton.OKCancel);
if (retry == MessageBoxResult.OK) {
Connect();
} else {
host.IsEnabled = true;
port.IsEnabled = true;
uid.IsEnabled = true;
connect.Content = "Connect";
connect.IsEnabled = true;
}
}
}
private void DisconnectWorker_DoWork(object sender, DoWorkEventArgs e)
{
try
{
ipcon.Disconnect();
e.Result = true;
}
catch (NotConnectedException)
{
e.Result = false;
}
}
private void DisconnectWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if ((bool)e.Result)
{
host.IsEnabled = true;
port.IsEnabled = true;
uid.IsEnabled = true;
connect.Content = "Connect";
}
connect.IsEnabled = true;
}
private void TriggerWorker_DoWork(object sender, DoWorkEventArgs e)
{
try
{
relay.SetMonoflop(1 << 0, 15, 1500);
}
catch (TinkerforgeException)
{
}
}
private void Connect()
{
if (host.Text.Length == 0 || port.Text.Length == 0 || uid.Text.Length == 0)
{
MessageBox.Show("Host/Port/UID cannot be empty", "Error", MessageBoxButton.OK);
return;
}
host.IsEnabled = false;
port.IsEnabled = false;
uid.IsEnabled = false;
connect.IsEnabled = false;
trigger.IsEnabled = false;
progress.Visibility = Visibility.Visible;
string[] argument = new string[3];
argument[0] = host.Text;
argument[1] = port.Text;
argument[2] = uid.Text;
connectWorker.RunWorkerAsync(argument);
}
private void Connect_Click(object sender, RoutedEventArgs e)
{
if (ipcon == null || ipcon.GetConnectionState() == IPConnection.CONNECTION_STATE_DISCONNECTED)
{
Connect();
}
else
{
connect.IsEnabled = false;
trigger.IsEnabled = false;
disconnectWorker.RunWorkerAsync();
}
}
private void Trigger_Click(object sender, RoutedEventArgs e)
{
triggerWorker.RunWorkerAsync();
}
}
}