community firmware

Programmierung in Python: ein Modell steuern

Die vorigen Tutorials haben gezeigt, wie man eine erste App entwickelt (Python: Die erste Anwendung)) und wie man sich die Entwicklung erleichtern kann (Python: Entwicklung). In diesem Teil wird jetzt gezeigt, wie man ein Modell ansteuert. Den Quellcode für die Beispiele aus diesem Tutorial findest du in Github.

Die benötigten Bibliotheken einbinden

Die Community-Firmware enthält das Modul ftrobopy, das die Anbindung von Python an die Fischertechnik-spezifischen Ein- und Ausgänge des TXT ermöglicht. Das Modul kann einfach in das Programm aus Tutorial #1 eingefügt werden:

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
#
import sys
import ftrobopy                                              # Import the ftrobopy module
from TouchStyle import *

class FtcGuiApplication(TouchApplication):
    def __init__(self, args):
        TouchApplication.__init__(self, args)
        # create the empty main window
        w = TouchWindow("Tut_3_1")

        try:
            txt = ftrobopy.ftrobopy("localhost", 65000)      # connect to TXT's IO controller
        except:
            txt = None                                       # set TXT to "None" of connection failed

        if not txt:
            # display error if TXT could no be connected
            # error messages is centered and may span
            # over several lines
            err_msg = QLabel("Error connecting IO server")   # create the error message label
            err_msg.setWordWrap(True)                        # allow it to wrap over several lines
            err_msg.setAlignment(Qt.AlignCenter)             # center it horizontally
            w.setCentralWidget(err_msg)                      # attach it to the main output area
            
        w.show()
        self.exec_()        

if __name__ == "__main__":
    FtcGuiApplication(sys.argv)


Wenn du diese App auf deinem TXT wie in Tutorial #1 beschrieben startest, wird das Ergebnis so aussahen wie dort, nur der Name der App ist natürlich anders. Wenn es das macht und keine Fehlermeldung erscheint, dann hat sich die App erfolgreich mit dem Server-Prozess auf dem TXT verbunden und hat Zugriff auf die Ein- und Ausgänge des TXT.

Für die Ausgabe der Fehlermeldung verwenden wir QLabel. Mehr Informationen zu diesem Thema findest du (auf Englisch) in den PyQt-Tutorials. Auch die Benutzung von QLabel wird dort erklärt. Die dort angegebenen Beispiele unterscheiden sich etwas von unseren Beispielen, weil wir hier ein spezielles Fensterformat für den TXT verwenden. Die größten Unterschiede beziehen sich nur auf das Hauptfenster. Die Verwendung der Widgets unterscheidet sich kaum zwischen PC und TXT. QLabel z.B. funktioniert auf dem TXT genauso wie auf dem PC.

Einen Ausgang steuern

Natürlich wollen wir, dass unsere kleine App etwas Schönes macht. Bitte schließe eine Lampe an den Ausgang O1 deines TXT an. Du kannst zum Beispiel das Modell “Fußgänger-Ampel aus dem ROBOTICS TXT Discovery Set verwenden.

Wir programmieren einen Button auf dem TXT-Bildschirm, mit dem man die Lampe an- und ausschaltet. Dazu initialisieren wir die Schnittstellen für die Ausgänge und setzen sie auf vernünftige Werte. Außerdem bauen wir einen Button in die GUI ein:

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
#
import sys
import ftrobopy                                              # Import the ftrobopy module
from TouchStyle import *

class FtcGuiApplication(TouchApplication):
    def __init__(self, args):
        TouchApplication.__init__(self, args)

    # create the empty main window
        w = TouchWindow("Tut_3_2")

        try:
            txt = ftrobopy.ftrobopy("localhost", 65000)      # connect to TXT's IO controller
        except:
            txt = None                                       # set TXT to "None" of connection failed

        if not txt:
	        # display error of TXT could no be connected
            # error messages is centered and may span
            # over several lines
            err_msg = QLabel("Error connecting IO server")   # create the error message label
            err_msg.setWordWrap(True)                        # allow it to wrap over several lines
            err_msg.setAlignment(Qt.AlignCenter)             # center it horizontally
            w.setCentralWidget(err_msg)                      # attach it to the main output area
        else:
            # initialization went fine. So the main gui
            # is being drawn
            button = QPushButton("Toggle O1")                # create a button labeled "Toggle O1"
            w.setCentralWidget(button)                       # attach it to the main output area

	    # configure all TXT outputs to normal mode
            M = [ txt.C_OUTPUT, txt.C_OUTPUT, txt.C_OUTPUT, txt.C_OUTPUT ]
            I = [ (txt.C_SWITCH, txt.C_DIGITAL ),
                  (txt.C_SWITCH, txt.C_DIGITAL ),
                  (txt.C_SWITCH, txt.C_DIGITAL ),
                  (txt.C_SWITCH, txt.C_DIGITAL ),
                  (txt.C_SWITCH, txt.C_DIGITAL ),
                  (txt.C_SWITCH, txt.C_DIGITAL ),
                  (txt.C_SWITCH, txt.C_DIGITAL ),
                  (txt.C_SWITCH, txt.C_DIGITAL ) ]
            txt.setConfig(M, I)
            txt.updateConfig()

        w.show()
        self.exec_()

if __name__ == "__main__":
    FtcGuiApplication(sys.argv)

Wenn du die App auf dem TXT startest, siehst du einen Button auf dem Bildschirm. Damit er auch etwas macht, müssen wir den Button mit einer Funktion verbinden:

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
#
import sys
import ftrobopy                                              # Import the ftrobopy module
from TouchStyle import *

class FtcGuiApplication(TouchApplication):
    def __init__(self, args):
        TouchApplication.__init__(self, args)

        # create the empty main window
        w = TouchWindow("Tut_3_3")

        try:
            self.txt = ftrobopy.ftrobopy("localhost", 65000) # connect to TXT's IO controller
        except:
            self.txt = None                                  # set TXT to "None" of connection failed

        if not self.txt:
	        # display error of TXT could no be connected
            # error messages is centered and may span
            # over several lines
            err_msg = QLabel("Error connecting IO server")   # create the error message label
            err_msg.setWordWrap(True)                        # allow it to wrap over several lines
            err_msg.setAlignment(Qt.AlignCenter)             # center it horizontally
            w.setCentralWidget(err_msg)                      # attach it to the main output area
        else:
            # initialization went fine. So the main gui
            # is being drawn
            button = QPushButton("Toggle O1")                # create a button labeled "Toggle O1"
            button.clicked.connect(self.on_button_clicked)   # connect button to event handler
            w.setCentralWidget(button)                       # attach it to the main output area

	    # configure all TXT outputs to normal mode
            M = [ self.txt.C_OUTPUT, self.txt.C_OUTPUT, self.txt.C_OUTPUT, self.txt.C_OUTPUT ]
            I = [ (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ) ]
            self.txt.setConfig(M, I)
            self.txt.updateConfig()

	        # initially switch light on
            self.light_on = True                             # remember that the light is on
            self.txt.setPwm(0,512)                           # set PWM to 512 (full on)

        w.show()
        self.exec_()

    # an event handler for our button (called a "slot" in qt)
    # it will be called whenever the user clicks the button
    def on_button_clicked(self):
        self.light_on = not self.light_on                   # change state
        if self.light_on:                                   # set output accordingly
            self.txt.setPwm(0,512)                          # PWM=512 means full on
        else:
            self.txt.setPwm(0,0)                            # PWM=0 means off

if __name__ == "__main__":
    FtcGuiApplication(sys.argv)

Die fertige App ermöglicht dir, die Lampe ein- und auszuschalten, indem sie den PWN-Wert auf 512 (EIN) oder 0 (AUS) setzt. Dazu haben wir einen Handler eingebaut, der jedes Mal aufgerufen wird, wenn der Benutzer den Button drückt. Damit die Variable txt bei allen Funktionen in der Klasse verfügbar ist, setzen wir self davor. Mehr über die Verwendung des Keywords self und Objektorientierung in Python im Allgemeinen erfährst du zum Beispiel in diesem Tutorial (auf Englisch).

Einen Eingang lesen

Nachdem wir jetzt einen der Ausgänge steuern können, wollen wir auch einen Eingang einlesen. Bitte schließe einen Taster an den Eingang I1 des TXT an.

Es ist nicht möglich, einen Eingang selbst eine Nachricht senden zu lassen. Daher müssen wir regelmäßig den Status des Eingangs abfragen. Die einfachsten Programme würden in einer Dauerschleife fortwährend den Input einlesen und auf Änderungen prüfen. Da die komplette Qt-GUI parallel laüft und auch versorgt werden muss, können wir nicht einfach den Prozess durch eine Dauerschleife übernehmen. Stattdessen müssen wir die Abfrage parallel zu der Verarbeitung der GUI durchführen.

Für immer wiederkehrende Aufgaben stellt Qt Timer zur Verfügung. Ähnlich wie der Button ruft auch der Timer einen Event-Handler auf, der die Aufgabe erledigt. Wir bauen also einen QTimer ein, der 10mal in der Sekunde, also alle 100ms feuert und dann den Status des Inputs liest. Jedes Mal, wenn der Input seinen Zustand zwischen 1 und 0 wechselt, ändern wir den Zustand der Lampe:

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
#
import sys
import ftrobopy                                              # Import the ftrobopy module
from TouchStyle import *

class FtcGuiApplication(TouchApplication):
    def __init__(self, args):
        TouchApplication.__init__(self, args)

        # create the empty main window
        w = TouchWindow("Tut_3_4")

        txt_ip = os.environ.get('TXT_IP')                    # try to read TXT_IP environment variable
        try:
            self.txt = ftrobopy.ftrobopy("localhost", 65000) # connect to TXT's IO controller
        except:
            self.txt = None

        vbox = QVBoxLayout()

        if not self.txt:
            # display error of TXT could no be connected
            # error messages is centered and may span
            # over several lines
            err_msg = QLabel("Error connecting IO server")   # create the error message label
            err_msg.setWordWrap(True)                        # allow it to wrap over several lines
            err_msg.setAlignment(Qt.AlignCenter)             # center it horizontally
            vbox.addWidget(err_msg)                          # attach it to the main output area
        else:
            # initialization went fine. So the main gui
            # is being drawn
            button = QPushButton("Toggle O1")                # create a button labeled "Toggle O1"
            button.clicked.connect(self.on_button_clicked)   # connect button to event handler
            vbox.addWidget(button)                           # attach it to the main output area

            # configure all TXT outputs to normal mode
            M = [ self.txt.C_OUTPUT, self.txt.C_OUTPUT, self.txt.C_OUTPUT, self.txt.C_OUTPUT ]
            I = [ (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ) ]
            self.txt.setConfig(M, I)
            self.txt.updateConfig()

            # initially switch light on
            self.light_on = True                             # remember that the light is on
            self.txt.setPwm(0,512)                           # set PWm to 512 (full on)

            self.timer = QTimer(self)                        # create a timer
            self.timer.timeout.connect(self.on_timer)        # connect timer to on_timer slot
            self.timer.start(100);                           # fire timer every 100ms (10 hz)

            self.button_state = False                        # assume initually the button is not pressed

        w.centralWidget.setLayout(vbox)
        w.show()
        self.exec_()

    def toggle_light(self):
        self.light_on = not self.light_on                   # change state
        if self.light_on:                                   # set output accordingly
            self.txt.setPwm(0,512)                          # PWM=512 means full on
        else:
            self.txt.setPwm(0,0)                            # PWM=0 means off

    # an event handler for our button (called a "slot" in qt)
    # it will be called whenever the user clicks the button
    def on_button_clicked(self):
        self.toggle_light()

    # an event handler for the timer (also a qt slot)
    def on_timer(self):
        # check if input state differs from saved state
        if self.button_state != self.txt.getCurrentInput(0):
            # change saved state to reflect input state
            self.button_state = not self.button_state
            # toggle lamp state if button has been pressed
            if self.button_state:
                self.toggle_light()

if __name__ == "__main__":
    FtcGuiApplication(sys.argv)

Mit dieser App ändert die Lampe ihren Zustand, wenn du den Button auf dem Bildschirm oder den echten Taster am Input I1 drückst.

tut3_img1

Die App auf dem PC ausführen

Wie im Tutorial Python: Entwicklung erklärt, können TXT Apps auch auf dem PC ausgeführt werden. Dabei können auch die Ein- und Ausgänge auf dem TXT genutzt werden. Eigentlich wurde das Modul ftrobopy genau für diesen Zweck geschrieben. Als erstes musst du die Datei ftrobopy.py in das gleiche Verzeichnis wie TouchStyle.py aus dem vorigen Tutorial kopieren. Nun müssen wir der App erkären, wie sie sich vom PC mit dem TXT verbindet, um dessen Ein- und Ausgänge zu verwenden. Dafür musst du in deinem Programm diese Zeile:

            self.txt = ftrobopy.ftrobopy("localhost", 65000) # connect to TXT's IO controller

durch diese ersetzen:

            self.txt = ftrobopy.ftrobopy("192.168.7.2", 65000) # connect to TXT's IO controller

wenn dein TXT über USB verbunden ist.

Es ist ziemlich unbequem, diese Zeile jedes Mal zu ändern, wenn du für den App-Start zwischen PC und TXT wechseln willst. Die folgende Version funktioniert für beide Fälle:

        txt_ip = os.environ.get('TXT_IP')
        if txt_ip == None: txt_ip = "localhost"
        try:
            txt = ftrobopy.ftrobopy(txt_ip, 65000)
        except:
            txt = None

Der Code versucht, die Umgebungsvariable TXT_IP zu lesen. Wenn diese Variable existiert, versucht die App sich mit dem TXT zu verbinden. Wenn sie nicht existiert, dann wird die Standardeinstellung “localhost” verwendet, d.h. die App merkt, dass sie auf dem TXT lokal läuft und nutzt die Ein- und Ausgänge direkt.

Auf dem PC musst du diese Umgebungsvariable setzen wie hier unter Linux:

$ export TXT_IP=192.168.7.2

Unter Linux kannst du diesen Befehl in der Datei .bashrc in deinem Home-Verzeichnis setzen, so dass du dich in Zukunft nicht mehr darum kümmern musst. Die App läuft jetzt auf dem TXT und auf dem PC. Wenn du die App auf dem PC ausführen willst, musst du auf deinem TXT die App FT-GUI starten. Das vollständige Programm sieht jetzt so aus:

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
#
import sys
import ftrobopy                                              # Import the ftrobopy module
from TouchStyle import *

class FtcGuiApplication(TouchApplication):
    def __init__(self, args):
        TouchApplication.__init__(self, args)

        # create the empty main window
        w = TouchWindow("Tut_3_5")

        txt_ip = os.environ.get('TXT_IP')                    # try to read TXT_IP environment variable
        if txt_ip == None: txt_ip = "localhost"              # use localhost otherwise
        try:
            self.txt = ftrobopy.ftrobopy(txt_ip, 65000)      # try to connect to IO server
        except:
            self.txt = None

        vbox = QVBoxLayout()

        if not self.txt:
            # display error of TXT could no be connected
            # error messages is centered and may span
            # over several lines
            err_msg = QLabel("Error connecting IO server")   # create the error message label
            err_msg.setWordWrap(True)                        # allow it to wrap over several lines
            err_msg.setAlignment(Qt.AlignCenter)             # center it horizontally
            vbox.addWidget(err_msg)                          # attach it to the main output area
        else:
            # initialization went fine. So the main gui
            # is being drawn
            button = QPushButton("Toggle O1")                # create a button labeled "Toggle O1"
            button.clicked.connect(self.on_button_clicked)   # connect button to event handler
            vbox.addWidget(button)                           # attach it to the main output area

            # configure all TXT outputs to normal mode
            M = [ self.txt.C_OUTPUT, self.txt.C_OUTPUT, self.txt.C_OUTPUT, self.txt.C_OUTPUT ]
            I = [ (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ),
                  (self.txt.C_SWITCH, self.txt.C_DIGITAL ) ]
            self.txt.setConfig(M, I)
            self.txt.updateConfig()

            # initially switch light on
            self.light_on = True                             # remember that the light is on
            self.txt.setPwm(0,512)                           # set PWm to 512 (full on)

            self.timer = QTimer(self)                        # create a timer
            self.timer.timeout.connect(self.on_timer)        # connect timer to on_timer slot
            self.timer.start(100);                           # fire timer every 100ms (10 hz)

            self.button_state = False                        # assume initually the button is not pressed

        w.centralWidget.setLayout(vbox)
        w.show()
        self.exec_()

    def toggle_light(self):
        self.light_on = not self.light_on                   # change state
        if self.light_on:                                   # set output accordingly
            self.txt.setPwm(0,512)                          # PWN=512 means full on
        else:
            self.txt.setPwm(0,0)                            # PWM=0 means off

    # an event handler for our button (called a "slot" in qt)
    # it will be called whenever the user clicks the button
    def on_button_clicked(self):
        self.toggle_light()

    # an event handler for the timer (also a qt slot)
    def on_timer(self):
        # check if input state differs from saved state
        if self.button_state != self.txt.getCurrentInput(0):
            # change saved state to reflect input state
            self.button_state = not self.button_state
            # toggle lamp state if button has been pressed
            if self.button_state:
                self.toggle_light()

if __name__ == "__main__":
    FtcGuiApplication(sys.argv)