Für diese Projekt setzen wir voraus, dass eine C# Entwicklungsumgebung 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.
Eine Demo-Anwendung basierend auf diesem Projekt ist verfügbar: (Download: Windows, Linux, macOS):
mono RemoteSwitchGUI.exe
aus einem Terminal-Fenster heraus
gestartet werden.In diesem Projekt wird ein robustes GUI Programm erstellt, das die Funktionalität der realen Fernbedienung nachbildet.
Das Programm wird ein einfaches Windows Forms GUI erstellt mit vier Knöpfen und einer Liste für Statusmeldungen:
Als erstes wird eine Form
mit Titel und Größe erstellt:
class RemoteSwitchGUI : Form
{
public RemoteSwitchGUI()
{
Text = "Remote Switch";
Size = new Size(300, 500);
}
static public void Main()
{
Application.Run(new RemoteSwitchGUI());
}
}
Dann wird ein 40 Pixel hohes Panel für die Knöpfe an der oberen Kante eingefügt. Die Liste für Statusmeldungen füllt den Rest der Form:
public RemoteSwitchGUI()
{
Text = "Remote Switch";
Size = new Size(300, 500);
panel = new Panel();
panel.Parent = this;
panel.Height = 40;
panel.Dock = DockStyle.Top;
listBox = new ListBox();
listBox.Parent = this;
listBox.Dock = DockStyle.Fill;
listBox.BringToFront();
Die CreateButton()
Methode erzeugt einen neuen Knopf mit vergebenem Namen
und vorgegebener Position:
private Button CreateButton(string name, int x)
{
Button button = new Button();
button.Text = name;
button.Parent = panel;
button.Width = 50;
button.Location = new Point(x, 10);
return button;
}
Zuletzt werden die vier Knöpfe erzeugt, einen für jeden zu steuernden Knopf auf der Fernbedienung:
public RemoteSwitchGUI()
{
// [...]
buttonAOn = CreateButton("A On", 10);
buttonAOff = CreateButton("A Off", 70);
buttonBOn = CreateButton("B On", 130);
buttonBOff = CreateButton("B Off", 190);
Das GUI ist jetzt fertig.
Dieser Schritt ist der gleiche wie Schritt 1 aus dem Rauchmelder mit C# auslesen Projekts, allerdings mit ein paar Änderungen, damit es in einem GUI Programm ordentlich funktioniert.
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
extra Thread aufgerufen werden, so dass es im Hintergrund ausgeführt wird und
die GUI nicht blockiert wird:
public RemoteSwitchGUI()
{
// [...]
Thread thread = new Thread(delegate() { Connect(); });
thread.IsBackground = true;
thread.Start();
}
private void Connect()
{
ipcon = new IPConnection();
ipcon.Connect(HOST, PORT);
ipcon.EnumerateCallback += EnumerateCB;
ipcon.Connected += ConnectedCB;
ipcon.Enumerate();
}
Die ConnectedCB
Callback-Funktion ist die die gleiche wie im Rauchmelder
Projekt. Aber die EnumerateCB
Callback-Funktion ist einfacher, da das
Industrial Quad Relay Bricklet nicht extra konfiguriert werden muss für diese
Projekt:
private void EnumerateCB(IPConnection sender, string UID, string connectedUID, char position,
short[] hardwareVersion, short[] firmwareVersion,
int deviceIdentifier, short enumerationType)
{
if(enumerationType == IPConnection.ENUMERATION_TYPE_CONNECTED ||
enumerationType == IPConnection.ENUMERATION_TYPE_AVAILABLE)
{
if(deviceIdentifier == BrickletIndustrialQuadRelay.DEVICE_IDENTIFIER)
{
brickletIndustrialQuadRelay = new BrickletIndustrialQuadRelay(UID, ipcon);
}
}
}
Die Verbindung wurde hergestellt und ein Industrial Quad Relay Bricklet wurde gefunden, aber es fehlt noch die Logik um einen Klick auf einen Knopf des Programms in das Auslösen einen Tasters auf der Fernbedienung zu übersetzen.
Dafür wird ein Delegate zum Click
-Event eines jeden Knopfes hinzugefügt.
Der Delegate ruft dann eine TriggerSwitch()
Methode mit einer gegebenen
Bitmaske auf. Diese Bitmaske legt fest welche Relais des Industrial Quad Relay
Bricklets geschlossen werden sollen, wenn ein bestimmter Knopf geklickt wird:
private Button CreateButton(string name, int x, int selectionMask)
{
// [...]
button.Click += delegate(object sender, System.EventArgs e)
{
TriggerSwitch(selectionMask);
};
return button;
}
Gemäße der Hardware-Aufbau Beschreibung ist die Fernbedienung wie folgt mit den Relais verbunden:
Signal | Relais |
---|---|
A | 0 |
B | 1 |
ON | 2 |
OFF | 3 |
Um "A ON" auf der Fernbedienung auszulösen müssen also die Relais 0 und 2
geschlossen werden. Dies wird durch die Bitmaske (1 << 0) | (1 << 2)
repräsentiert.
Der Konstruktor wird so abgewandelt, dass er CreateButton()
mit den
passenden Bitmasken für jeden Knopf aufruft:
public RemoteSwitchGUI()
{
// [...]
buttonAOn = CreateButton("A On", 10, (1 << 0) | (1 << 2));
buttonAOff = CreateButton("A Off", 70, (1 << 0) | (1 << 3));
buttonBOn = CreateButton("B On", 130, (1 << 1) | (1 << 2));
buttonBOff = CreateButton("B Off", 190, (1 << 1) | (1 << 3));
Die TriggerSwitch()
Methode benutzt SetMonoflop()
um eine Tasterdruck
auf der Fernbedienung auszulösen. Ein Monoflop setzt einen neuen Zustand
(Relais offen oder geschlossen) und hält diesen für eine bestimmte Zeit
(0,5s in diesem Fall). Nach dieser Zeit wird der vorheriger Zustand
wiederhergestellt. Dieses Ansatz simuliert einen Tasterdruck der für 0,5s
anhält.
private void TriggerSwitch(int selectionMask)
{
brickletIndustrialQuadRelay.SetMonoflop(selectionMask, 15, 500);
}
Das ist es. Wenn wir diese drei Schritte zusammen in eine Datei kopieren und ausführen, dann hätten wir jetzt eine funktionierendes Programm, das Funksteckdosen über deren gehackte Fernbedienung fernsteuern kann!
Das Programm ist noch nicht robust genug. Was passiert wenn die Verbindung beim Start des Programms nicht hergestellt werden kann, oder wenn das Enumerate nach einem Auto-Reconnect nicht funktioniert?
Wir brauchen noch Fehlerbehandlung!
Es werden die gleichen Konzepte wie in Schritt 4 des Rauchmelder mit C# auslesen Projekts verwendet, aber mit einigen Abwandelungen, damit sie in einem GUI Programm richtig funktionieren.
Wir können nicht einfach System.Console.WriteLine()
für Logausgaben
verwenden, da dies ein GUI Programm ist und kein Konsolenfenster hat.
Stattdessen werden die Logausgaben in einer Liste im GUI ausgegeben.
Dieser Ansatz hat aber noch ein Problem. Die Verbindung wird in einem extra
Thread hergestellt, allerdings darf nur der Haupt-Thread mit der GUI
interagieren. Glücklicherweise bietet C# die Invoke()
Methode, die es
erlaubt aus anderen Threads Code im Haupt-Thread auszuführen und so korrekt
mit der GUI zu interagieren. Mit dieser Methode erstellen wir eine Log()
Methode, die es allen Threads erlaubt Logausgaben in die Liste für
Statusmeldungen zu schreiben:
private void Log(string message)
{
Invoke((MethodInvoker) delegate() { listBox.Items.Add(message); });
}
Alle Änderungen die in Schritt 4 des Rauchmelder Projekts beschrieben werden
sind hier auch notwendig. Zusätzlich müssen noch mögliche Fehler in der
TriggerSwitch()
Methode behandelt werden:
private void TriggerSwitch(string name, int selectionMask)
{
if(brickletIndustrialQuadRelay == null) {
Log("No Industrial Quad Relay Bricklet found");
return;
}
try
{
brickletIndustrialQuadRelay.SetMonoflop(selectionMask, 15, 500);
Log("Triggered '" + name + "'");
}
catch(TinkerforgeException e)
{
Log("Trigger '" + name + "' Error: " + e.Message);
}
}
Für bessere Logausgaben wird der Name des Knopfes an TriggerSwitch()
übergeben, so dass dieser in Logausgaben angezeigt werden kann.
Schlussendlich, da der Thread der die Verbindung aufbaut jetzt mit der GUI
interagiert kann er nicht mehr direkt im Konstruktor gestartet werden.
Stattdessen wird der Thread erst gestartet wenn die GUI das erst Mal angezeigt
wird. Der Load
-Event kann dafür verwendet werden:
public RemoteSwitchGUI()
{
// [...]
Load += delegate(object sender, System.EventArgs e)
{
Thread thread = new Thread(delegate() { Connect(); });
thread.IsBackground = true;
thread.Start();
};
}
Das ist es! Das C# Programm zum Fernsteuern der gehackten Funksteckdosen ist fertig und sollte nun alle gesteckten Ziele erfüllen.
Das gesamte Programm in einem Stück (download):
using System.Windows.Forms;
using System.Drawing;
using System.Threading;
using Tinkerforge;
class RemoteSwitchGUI : Form
{
private static string HOST = "localhost";
private static int PORT = 4223;
private Panel panel = null;
private Button buttonAOn = null;
private Button buttonAOff = null;
private Button buttonBOn = null;
private Button buttonBOff = null;
private ListBox listBox = null;
private IPConnection ipcon = null;
private BrickletIndustrialQuadRelay brickletIndustrialQuadRelay = null;
public RemoteSwitchGUI()
{
Text = "Remote Switch GUI 1.0.1";
Size = new Size(300, 500);
MinimumSize = new Size(260, 200);
panel = new Panel();
panel.Parent = this;
panel.Height = 40;
panel.Dock = DockStyle.Top;
listBox = new ListBox();
listBox.Parent = this;
listBox.Dock = DockStyle.Fill;
listBox.BringToFront();
buttonAOn = CreateButton("A On", 10, (1 << 0) | (1 << 2));
buttonAOff = CreateButton("A Off", 70, (1 << 0) | (1 << 3));
buttonBOn = CreateButton("B On", 130, (1 << 1) | (1 << 2));
buttonBOff = CreateButton("B Off", 190, (1 << 1) | (1 << 3));
Load += delegate(object sender, System.EventArgs e)
{
Thread thread = new Thread(delegate() { Connect(); });
thread.IsBackground = true;
thread.Start();
};
}
private Button CreateButton(string name, int x, int selectionMask)
{
Button button = new Button();
button.Text = name;
button.Parent = panel;
button.Width = 50;
button.Location = new Point(x, 10);
button.Click += delegate(object sender, System.EventArgs e)
{
TriggerSwitch(name, selectionMask);
};
return button;
}
private void Log(string message)
{
Invoke((MethodInvoker) delegate() { listBox.Items.Add(message); });
}
private void Connect()
{
Log("Connecting to " + HOST + ":" + PORT);
ipcon = new IPConnection();
while(true)
{
try
{
ipcon.Connect(HOST, PORT);
break;
}
catch(System.Net.Sockets.SocketException e)
{
Log("Connection Error: " + e.Message);
Thread.Sleep(1000);
}
}
ipcon.EnumerateCallback += EnumerateCB;
ipcon.Connected += ConnectedCB;
while(true)
{
try
{
ipcon.Enumerate();
break;
}
catch(NotConnectedException e)
{
Log("Enumeration Error: " + e.Message);
Thread.Sleep(1000);
}
}
Log("Connected");
}
private void TriggerSwitch(string name, int selectionMask)
{
if(brickletIndustrialQuadRelay == null) {
Log("No Industrial Quad Relay Bricklet found");
return;
}
try
{
brickletIndustrialQuadRelay.SetMonoflop(selectionMask, 15, 500);
Log("Triggered '" + name + "'");
}
catch(TinkerforgeException e)
{
Log("Trigger '" + name + "' Error: " + e.Message);
}
}
private void EnumerateCB(IPConnection sender, string UID, string connectedUID, char position,
short[] hardwareVersion, short[] firmwareVersion,
int deviceIdentifier, short enumerationType)
{
if(enumerationType == IPConnection.ENUMERATION_TYPE_CONNECTED ||
enumerationType == IPConnection.ENUMERATION_TYPE_AVAILABLE)
{
if(deviceIdentifier == BrickletIndustrialQuadRelay.DEVICE_IDENTIFIER)
{
try
{
brickletIndustrialQuadRelay = new BrickletIndustrialQuadRelay(UID, ipcon);
Log("Industrial Quad Relay initialized");
}
catch(TinkerforgeException e)
{
Log("Industrial Quad Relay init failed: " + e.Message);
brickletIndustrialQuadRelay = null;
}
}
}
}
private void ConnectedCB(IPConnection sender, short connectedReason)
{
if(connectedReason == IPConnection.CONNECT_REASON_AUTO_RECONNECT)
{
Log("Auto Reconnect");
while(true)
{
try
{
ipcon.Enumerate();
break;
}
catch(NotConnectedException e)
{
Log("Enumeration Error: " + e.Message);
Thread.Sleep(1000);
}
}
}
}
static public void Main()
{
Application.Run(new RemoteSwitchGUI());
}
}