For this project we are assuming, that you have the Windows Phone SDK set up and that you have a rudimentary understanding of the C# language.
If you are totally new to C# itself you should start here. If you are new to the Tinkerforge API, you should start here.
We are also assuming that you have a remote control connected to an Industrial Quad Relay Bricklet as described here.
The complete Visual Studio project can be downloaded here.
In this project we will create a simple Windows Phone app that resembles the functionality of the actual remote control.
After creating a new "Windows Phone App" named "Garage Control" in Visual Studio we start with creating the GUI:
We extend the precreated layout by appending a "StackPanel" to the "LayoutRoot"
grid, that will contain the other GUI elements. Three "TextBoxes" allow
to enter the host, port and UID of the Industrial Quad Relay Bricklet. For the
port a text box with InputScope="Number"
is used, so Windows Phone will
restrict the content of this text box to numbers. Below the text boxes goes a
"ProgressBar" (not visible on screenshot) that will be used to indicate that a
connection attempt is in
progress. The final two elements are one "Button" to connect and
disconnect and another one to trigger the remote control. Here is a snippet of
the MainPage.xaml
file:
<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>
Now the GUI layout is finished. The initial GUI configuration is done in the
constructor of the MainPage
class. The progress bar is initially hidden and
indeterminate mode is enabled, because the duration of a connection attempt is
unknown:
public partial class MainPage : PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
progress.Visibility = Visibility.Collapsed;
progress.IsIndeterminate = true;
}
}
This step is similar to step 1 in the
Read out Smoke Detectors using C# project.
We apply some changes to make it work in a GUI program and instead of using the
EnumerateCallback
to discover the Industrial Quad Relay Bricklet its UID
has to be specified. This approach allows to pick the correct Industrial Quad
Relay Bricklet even if multiple are connected to the same host at once.
We don't want to call the Connect()
method directly, because it might take
a moment and block the GUI during that period of time. Instead Connect()
will
be called from a BackgroundWorker
, so it will run in the background and the GUI
stays responsive:
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);
}
}
The BackgroundWorker
has an DoWork
event that will be triggered from
another thread after RunWorkerAsync()
was called. The host, port and UID
configuration is passed to the DoWork
event. This is necessary, because the
ConnectWorker_DoWork()
method needs this information, but is not
allowed to access the GUI elements. Now the ConnectWorker_DoWork()
method
can create an IPConnection
and BrickletIndustrialQuadRelay
object and
call the Connect()
method.
Finally, the BackgroundWorker
should be started when the connect button is
clicked. To do this the Connect_Click()
method is bound to the Click
event of the connect button:
private void Connect_Click(object sender, RoutedEventArgs e)
{
Connect();
}
Host, port and UID can now be configured and a click on the connect button establishes the connection.
The connection is established and the Industrial Quad Relay Bricklet is found but there is no logic yet to trigger the switch on the remote control if the trigger button is clicked.
To do this the Trigger_Click()
method is bound to the Click
event of the
trigger button. It starts another BackgroundWorker
that in turn calls the
SetMonoflop()
method of the Industrial Quad Relay Bricklet to trigger the
switch on the remote control:
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, 15, 1500);
}
private void Trigger_Click(object sender, RoutedEventArgs e)
{
triggerWorker.RunWorkerAsync();
}
}
The call to SetMonoflop(1 << 0, 15, 1500)
closes the first relay for 1.5s then opens it again.
That's it. If we would copy these three steps together in one project, we would have a working app that allows a smart phone to control a garage door opener using its hacked remote control!
We don't have a disconnect button yet and the trigger button can be clicked before the connection is established. We need some more GUI logic!
There is no button to close the connection again after it got established. The connect button could do this. When the connection is established it should allow to disconnect it again:
public partial class MainPage : PhoneApplicationPage
{
// [...]
public MainPage()
{
// [...]
connectWorker.RunWorkerCompleted += ConnectWorker_RunWorkerCompleted;
}
private void ConnectWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
connect.Content = "Disconnect";
}
}
The ConnectWorker_RunWorkerCompleted()
method is called after
ConnectWorker_DoWork()
. It changes the text on the button to "Disconnect".
The Connect_Click()
method now decides dynamically what to do. If there is
no connection it calls Connect()
, if there is a connection is runs the
disconnect background worker:
private void Connect_Click(object sender, RoutedEventArgs e)
{
if (ipcon == null || ipcon.GetConnectionState() == IPConnection.CONNECTION_STATE_DISCONNECTED)
{
Connect();
}
else
{
disconnectWorker.RunWorkerAsync();
}
}
The disconnect background worker calls the Disconnect()
method in the background, because it might take a
moment and block the GUI during that period of time:
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";
}
}
Finally, the user should not be able to change the content of the text fields during the time the connection gets established and the trigger button should not be clickable if there is no connection.
The connectWorker
and the disconnectWorker
are extended to
disable and enable the GUI elements according to the current connection state:
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;
// [...]
}
But the program is not yet robust enough. What happens if can't connect? What happens if there is no Industrial Quad Relay Bricklet with the given UID?
What we need is error handling!
We will use similar principals as in step 4 of the Read out Smoke Detectors using C# project, but with some changes to make it work in a GUI program.
We can't just use System.Console.WriteLine()
for error reporting because
there is no console window in an app. Instead message boxes are used.
The Connect()
method has to validate the user input before using it. An
MessageBox
is used to report possible problems. Also the progress bar is
made visible to indicate that a connection attempt is in progress:
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;
// [...]
}
Then the ConnectWorker_DoWork()
method needs to be able to report its result.
But it is not allowed to interact with the GUI, but it can assign a value to the
Result
member of the DoWorkEventArgs
parameter that is then passed to
the ConnectWorker_RunWorkerCompleted()
method. We use this enum
that
represents the three possible outcomes of a connection attempt:
enum ConnectResult
{
SUCCESS,
NO_CONNECTION,
NO_DEVICE
}
The GetIdentity()
method is used to check that the device for the given
UID really is an Industrial Quad Relay Bricklet. If this is not the case then
the connection gets closed:
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;
}
Now the ConnectWorker_RunWorkerCompleted()
method has to handle this three
outcomes. First the progress bar is dismissed:
private void ConnectWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
ConnectResult result = (ConnectResult)e.Result;
progress.Visibility = Visibility.Collapsed;
In case the connection attempt was successful the original logic stays the same:
if (result == ConnectResult.SUCCESS)
{
connect.Content = "Disconnect";
connect.IsEnabled = true;
trigger.IsEnabled = true;
}
In the error case we use a MessageBox
and set the error message according
to the connection result:
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);
Retry to connect or cancel the connection attempt, according to the result of the message box:
if (retry == MessageBoxResult.OK) {
Connect();
} else {
host.IsEnabled = true;
port.IsEnabled = true;
uid.IsEnabled = true;
connect.Content = "Connect";
connect.IsEnabled = true;
}
}
}
Now the app can connect to an configurable host and port and trigger a button on the remote control of your garage door opener using an Industrial Quad Relay Bricklet.
The app doesn't store its configuration yet. Windows Phone provides the
IsolatedStorageSettings
class to take care of this. In OnNavigatedTo()
the configuration is restored and the connection is reestablished if it was
active before:
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()
the configuration is then stored again:
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();
}
Now the configuration and state is stored persistent across a restart of the app.
That's it! We are done with the app for our hacked garage door opener remote control.
Now all of the above put together (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();
}
}
}