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