Funksteckdosen in C# mit GUI fernsteuern

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):

  • Auf Windows wird das .NET Framework benötigt, dies ist typischerweise aber bereits installiert.
  • Auf Linux wird die Mono Runtime for Linux benötigt, dies ist typischerweise aber bereits auch installiert.
  • Auf macOS wird die Mono Runtime for macOS benötigt. Seit macOS 10.8 muss außerdem noch XQuartz installiert werden damit die Mono Runtime richtig funktioniert. Jetzt kann die Demo-Anwendung mittels mono RemoteSwitchGUI.exe aus einem Terminal-Fenster heraus gestartet werden.

Ziele

In diesem Projekt wird ein robustes GUI Programm erstellt, das die Funktionalität der realen Fernbedienung nachbildet.

Schritt 1: Die GUI erstellen

Das Programm wird ein einfaches Windows Forms GUI erstellt mit vier Knöpfen und einer Liste für Statusmeldungen:

Windows Forms GUI

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.

Schritt 2: Bricks und Bricklets dynamisch erkennen

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);
        }
    }
}

Schritt 3: Taster auslösen

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!

Schritt 4: Fehlerbehandlung und Logging

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();
    };
}

Schritt 5: Alles zusammen

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());
    }
}