Programmieren für das N900 – Teil 3 Signals and Slots

Im dritten Teil wollen wir nun unsere Oberfläche, die wir im zweiten Teil mit dem Qt-Designer erstellt haben, mit Leben füllen. Dazu muss man sich zunächst mal mit dem Konzept von Signals and Slots vertraut machen:

Der generelle Ablauf eines Programms ist bei einer grafischen Benutzeroberfläche nicht linear, d.h. z.B. vorhandenen Knöpfe werden nicht immer in der gleichen Reihenfolge gedrückt oder der Benutzer gibt wie in unserem Beispiel nicht immer die gleiche IP-Adresse ein.Aus diesem Grund kann man sich einen typischen Programmablauf so vorstellen, dass sich das Programm die ganze Zeit in einer Endlosschleife befindet und auf bestimmte Ereignisse reagiert. Wird z.B. ein Knopf gedrückt, sendet er ein Signal an das Hauptprogramm. Ist dieses Signal im Hauptprogramm mit einem bestimmten Slot verbunden, wird die Wirkung dieses Slots ausgeführt. Jedes Oberflächenelement bringt schon eine Reihe von Standardsignalen mit, so dass man sich bei vielen Programmen dieser Signale bedienen kann. In der Dokumentation der Klassen von Qt findet man z.B. für den Knopf QAbstractButton, von dem unser verwendeter Knopf der Klasse QPushButton abgeleitet ist, folgende Signale:

  1. void clicked (bool = 0)
  2. void pressed ()
  3. void released ()
  4. void toggled (bool)

Weiter unten findet man eine kurze Beschreibung, die hier aber durch die Wortwahl der Signale fast überflüssig ist:

  1. clicked : wird ausgelöst, wenn der Knopf gedrückt und wieder losgelassen wurde
  2. pressed : wird ausgelöst, wenn der Knopf gedrückt wurde
  3. released : wird ausgelöst, wenn der Knopf losgelassen wird
  4. toggled:  wird ausgelöst, wenn der Status eines Buttons sich ändert

Für unser einfaches Programm reicht das Signal clicked, da wir ja auf einen Knopfdruck unser N900 in eine Webcam verwandeln wollen. Die Idee ist also, dass wir unserem Programm mitteilen, dass es eine bestimmte Aktion ausführt, wenn der Startknopf gedrückt wurde. Folgende Zeile fügen wir deshalb in unsere init-Methode ein:

self.ui.button_start.clicked.connect(self.starte_stream)

Die Zeile ist folgendermaßen zu lesen: In unserer Oberfläche self.ui gibt es einen Knopf button_start, d.h. mit self.ui.button_start greifen wir auf den Knopf zu. Dieser Knopf hat ja wie oben beschrieben die Methode clicked, d.h. self.ui.button_start.clicked greift jetzt auf diese Methode des Knopfes button_start zu und verbindet (connect) diese mit der Methode self.starte_stream. Diese Methode haben wir aber noch nicht geschrieben, was wir jetzt aber nachholen.

Eine Methode (auch oft Funktion oder Prozedur genannt) ist eine Möglichkeiten in der Programmierung Aufgaben, die ein Programm leisten soll, zu separieren und somit eine Strukturierung zu erreichen. Soll ein Programm z.B. eine Aufgabe immer wieder ausführen, müsste man den Quelltext immer wieder neu hinschreiben. Besser ist es aber den Quelltext einmal in eine eigene Methode zu schreiben und dann nur noch die Methode immer wieder aufzurufen.

Um eine Methode in unserem Programm zu definieren benutzt man in Python die folgende Syntax:

def methoden_name(self):
    ...

Die Punkte stehen dabei dann für den Quelltext, der bei dem Aufrufen der Methode ausgeführt werden soll. Dieser Quelltext muss eingerückt sein.

Die Methode starte_stream() sieht folgendermaßen aus:

def starte_stream(self):
        #liest die aktuellen Werte aus den beiden Eingabefeldern aus
        ip = self.ui.lineEdit_IP.text()
        port = self.ui.lineEdit_Port.text()
        # Terminalbefehl
        launch = ['gst-launch', 'v4l2camsrc', 'device=/dev/video0', '!', 'dsph264enc',
                '!', 'rtph264pay', '!', 'udpsink', 'host=%s' % ip, 'port=%s' % port]
        # starte Befehl
        self.process = subprocess.Popen(launch)

Bevor ich die Methode erkläre noch kurz was zur Konzeption:

Es gibt für python ein eigenes Modul pygst um auf gstreamer, dem verwendeten Multimedia Framework zuzugreifen. Ich greife aber aus verschiedenen Gründen auf die hier vorgestellte quick-and-dirty-Methode zurück:

  1. Meine Kenntnis von pygst ist so gut wie nicht vorhanden.
  2. Mir fehlt die Zeit mich in pygst einzuarbeiten.
  3. Das Programm würde wesentlich umfangreicher werden, was dem Ziel dieses Tutorials entgegenwirkt.

Man sollte nur im Hinterkopf behalten, dass es auch noch eine andere Möglichkeit gibt und diese sich bei genug Zeit und Muße mal angucken. Ich habe vor einiger Zeit in einem Thread im Forum von maemo.org ein Skript für’s Terminal gefunden, der einen Stream der Kamera zu einem anderen PC startet. Das ganze läuft über gst-launch und sieht folgendermaßen aus:

#!/bin/sh
gst-launch v4l2camsrc device=/dev/video0 ! \
           dsph264enc ! \
           rtph264pay ! \
           udpsink host=192.100.11.2 port=5434

nachträglicher Hinweis: Das Paket gstreamer-tools muss installiert sein, da es gst-launch enthält.
Dieses Skript streamt das Bild der Kamera (/dev/video0) an den PC mit der IP 192.100.11.2 über den Port 5434. Die Aufgabe ist es (ohne sich jetzt erstmal über die anderen Parameter Gedanken zu machen), diesen Befehl innerhalb unseres Programms zu starten. Dazu müssen wir die eingegebene IP-Adresse und den Port aus unserer Oberfläche auslesen:

ip = self.ui.lineEdit_IP.text()
port = self.ui.lineEdit_Port.text()

Die Textfelder hatten wir ja im 2.Teil mit sinnvollen Namen versehen, so dass wir jetzt die Zeilen gut lesen können. Ich wende auf das Oberflächenelement self.ui.lineEdit_IP die Methode text() an, die mir den Text zurückgibt, der in diesem Feld beim Aufruf von starte_stream steht. Dieser Text wird unter der Variable ip gespeichert. Dasselbe machen wir für den Port.

Nun bereiten wir den Befehl vor, so dass wir ihn mit python ausführen lassen können. Dazu verwenden wir aus dem Modul subprocess die Klasse Popen, die uns eben diese Option bietet. In der Dokumentation findet man, dass Popen eine Liste haben möchte, in der die Bestandteile des Befehls als Elemente vorkommen. Diese Liste erzeugen wir und speichern sie unter dem Variablennamen launch:

launch = ['gst-launch', 'v4l2camsrc', 'device=/dev/video0', '!', 'dsph264enc',
                '!', 'rtph264pay', '!', 'udpsink', 'host=%s' % ip, 'port=%s' % port]

Wenn man den Terminal-Befehl und die Liste vergleicht, sollte einem auffallen, dass die letzten beiden Einträge in der Liste nicht identisch mit dem Terminalbefehl sind. Klar, wir wollen ja auch das Skript mit den eingegebenen Werten aufrufen. ‚host=%s‘ % ip macht genau das, d.h. in dem String ‚host=%s‘ wird %s durch den String ersetzt, der in der Variable ip gespeichert ist, gleiches für den Port.

Als letztes übergeben wir den Befehl launch an Popen und geben diesem Objekt einen Name self.process:

self.process = subprocess.Popen(launch)

Wenn wir den Stream starten können, wollen wir ihn natürlich auf wieder beenden können. Dazu brauchen wir eine Methode stoppe_stream, die wir mit dem Ende-Knopf verbinden müssen. Der Befehl in der init-Methode lautet dafür:

self.ui.button_ende.clicked.connect(self.stoppe_stream)

und die Methode stoppe_stream:

def stoppe_stream(self):
        os.kill(self.process.pid, signal.SIGTERM)

os.kill() sendet an den Prozess mit der PID self.process.pid das Signal SIGTERM, was den Prozess beendet. Eigentlich sollte man nicht mehr os.kill() verwenden, da dass Modul subprocess ab Python 2.6 eine Methode terminate() hat. Da auf dem N900 aber noch Python 2.5 verwendet wird, muss man noch die alte Methode anwenden.

Damit ist das Programm fertig und kann ausprobiert werden. Dazu das Programm auf dem N900 aus dem Terminal mit

Nokia-N900-51-1:/home/user/webcam# python webcam.py

starten, die IP eures PC und den Port (z.B. 5434) eingeben und beherzt auf Start drücken. Auf eurem PC legt ihr eine Datei mit folgendem Inhalt an:

v=0
m=video 5434 RTP/AVP 96
c=IN IP4 192.99.12.3
a=rtpmap:96 H264/90000

wobei ihr die IP-Adresse 192.99.12.3 durch die IP-Adresse eures N900 ersetzt. Gebt der Datei einen aussagekräftigen Namen und öffnet sie  mit dem VLC-Player. Dann sollte das Bild eurer Kamera erscheinen. Drückt man auf den Ende-Knopf sollte der Stream stoppen.

So, damit ist das Programm in seiner Grundfunktion erstmal fertig. Allerdings kann man das Ganze noch weiter ausbauen:

  1. Was passiert, wenn der Benutzer keine oder eine nicht existente IP eingibt?
  2. Dem Benutzer wird eine Rückmeldung gegeben, ob der Stream gestartet/gestoppt wurde.
  3. Das Design der Oberfläche gefällt nicht mehr, die Knöpfe sollen z.B. Bilder statt Text bekommen.
  4. Ich möchte die Auflösung/den Codec des Streams als Benutzer verändern können.
  5. Es soll automatisch die Datei generiert werden (mit IP vom N900 und dem Port), die ich dann auf dem PC starte.
  6. Benutzung von pyqst

Ihr seht, man kann also noch vieles erweitern und verbessern. Vielleicht hat der ein oder andere ja jetzt Lust bekommen, das Programm selbst zu erweitern und ein wenig mit python zu experimentieren.

Ich habe am Ende nochmal den vollständigen Quelltext angehängt inklusive der ui-Datei, so dass man diese als Arbeitsgrundlage verwenden kann, wenn man denn will. Have fun!

Hier noch ein paar interessante Links, die einem vielleicht den Einstieg ein wenig erleichtern:

Ein allgemeineres Tutorial zu PyQt4 auf deutsch

PyQt by Example (englisch)

PyMaemo (Wiki auf maemo.org)

Falls jemand noch andere gute Tutorials kennt, immer her damit und in den Kommentaren posten.

webcam.zip

14 Kommentare

  1. Hi Boris,
    kennst Du PortaBase? Eine schlichte Datenbankanwendung die ich gerne und oft auf dem N900 nutze, nur dass keine Relationen möglich sind, schränkt den Komfort etwas ein.
    Wie groß sind die Chancen – quasi als „Programmierprojekt“ – eine ebensolche Anwendung zu realisieren – aber dann z. B. mySQL oder SQLite zugrunde zu legen?
    Dazu sei gesagt, ich habe Deine „Lehrgänge“ noch nicht durchgearbeitet und meine Programmiererfahrungen beschränken sich auf ein viele Jahre zurück liegendes VBA Projekt und irgendwelches Basic-Gestammel aus noch viel länger zurück liegender Schulzeit. (Also nur rudimentäres Grundwissen)

    • PortaBase kenne ich leider nicht, ich habe jetzt die Projektseite aber mal überflogen.
      Wenn du so eine Anwendung realisieren willst, würde ich erstmal ohne Oberfläche ein wenig mit Datenbanken arbeiten (Tabellen anlegen, Abfragen starten, …). Hier bietet sich sqlite3 an, da z.B. der Kalender auf dem N900 auch mit einer sqlite-DB läuft. Außerdem brauchst du keinen Server, da sqlite datei-basiert arbeitet.
      Soweit ich das sehe, kann man mit PortaBase „nur“ Datenbanken anlegen und abfragen. Das sollte mit ein wenig Ehrgeiz und Zeit machbar sein. Bei der grafischen Darstellung mit PyQt4 wirst du es dann zwangsweise mit TableViews zu tun bekommen und du solltest dir das MVC-Konzept (Model/Viev/Controller) aneignen.

  2. Hey, mein N900 ist da, alles funktioniert.

    Nur wie kann ich den quelltext zu nem icon im N900 menü machen, der das dann ausführt. Will nicht immer über console gehen, sondern wie ne richtige app die anwendung mit nem click im menü starten.

    Geht das?

    • Du legst dir für das Programm eine Datei unter /usr/share/applications/hildon/
      an, die die Endung desktop haben muss, also z.b. webcam.desktop. Der Inhalt der Datei sieht z.B. wie folgt aus (nicht getestet):

      [Desktop Entry]
      Encoding=UTF-8
      Version=0.1
      Type=Application
      Terminal=true
      Name=webcam
      Exec=python /home/user/webcam.py
      Icon=
      X-Window-Icon=
      X-HildonDesk-ShowInToolbar=true
      X-Osso-Type=application/x-executable

      Wenn du noch ein Icon hast, kannst du das auch eintragen, ist aber kein Muss. Ansonsten erscheint so ein blaues Quadrat.
      Ausführlichere Infos findest du hier: http://wiki.maemo.org/Desktop_file_format

    • Jetz fehlt nur noch das bild auf meinem linux laptop

      in Xterm kann ich das programm vom N900 ausführen, auch die datei auf pc kann ich mit vlc öffnen.

      Falls ich „Start“ drücke läuft das programm weiter, also kein absturz, jedoch steht im parallel laufenden xterm:

      File „home/user/webcam/webcam.py“, line 31, in start_stream
      self.progress = subprogress.Popen(launch)
      File „usr/lib/python2.5/subprogress.py“, line 594, in __init__erread, errwrite)
      File „usr/lib/python2.5/subprogress.py“, line 1153, in _execute_child raise child_exception

      OSError: [Errno 2] No such file or directory

      Brauch ich noch n paket oder liegt der fehler eher an der verbindung ala ip / port ?!

      Greetz

      • Das Modul subprogress gibt es nicht, es muss subprocess heißen. Im Zweifelsfall die Vorlagendatei webcam.py runterladen und ausprobieren, damit funktioniert es.

        Eigentlich sollte aber ne andere Fehlermeldung kommen, hast du die Fehlermeldung per Copy-Paste hier reingeschrieben oder abgetippt?

        Die Fehlermeldung deutet eher daraufhin, dass du gst-launch auf dem N900 noch nicht installiert hast.

        Installiere mal das Paket gstreamer-tools und probiere es dann nochmal.

  3. Fehler sind weg, hab das paket installiert.

    Nun das nächste prob. Wenn ich connecte und die kamera aufrufe, startet diese nicht, es steht nur da „Vorgang abgebrochen“, und lässt sich bis zum disconnect (ende button) nicht mehr öffnen. Hatte mal ganz kurz n bild im vlc bei 20 versuchen, aber nach 5sec war das weg.

    Im xterm stehen keine fehler, sondern nur das die pipe gestartet wird, bei ende kommt keine meldung, aber kamera lässt sich dann wieder starten. Bei laufender kamera, wenn ich dann das programm starte, und zurückswitche, beendet sie sich auch automatisch

    Hätte nicht gedacht das das so schwer ist und ich dich sooft nerven muss ;(

    • Das ist normal, da die Kamera ja jetzt von unserem Programm verwendet wird und die Standardkameraanwendung somit abgebrochen wird.

      Ansonsten würde ich mal auf Probleme mit der Netzwerkverbindung tippen und die Anzahl per Frames runterschrauben sowie das Bild skalieren. Test mal wahlweise, ob das läuft:
      launch_scale= [‚gst-launch‘, ‚v4l2camsrc‘, ‚device=/dev/video0‘, ‚!‘,
      ‚videoscale‘, ‚!‘ , ‚video/x-raw-yuv,width=320,height=240,framerate=25/1‘,
      ‚!‘, ‚dsph264enc‘, ‚!‘, ‚rtph264pay‘, ‚!‘, ‚udpsink‘,
      ‚host=%s‘ % ip, ‚port=%s‘ % port]

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.