Umsteiger und Anfänger sucht Hilfe

Plattformunabhängige GUIs mit wxWidgets.
Antworten
opccorsa
User
Beiträge: 34
Registriert: Samstag 20. Juli 2013, 07:43

Hallo Leute. Erstmal großes Lob an die ganzen Leute die hier wirklich brauchbare Tipps geben. Ich hoffe nun das ihr mir auch helfen könnt. Ich habe erst kurz mit der Sprache Python zu tun. Seit 3 Wochen geht es in das GUI mit wxPython. Ich möchte ein Programm erstellen wo ich per RS232 Daten versenden und empfangen kann. Das funktioniert schon sehr gut. Jetzt muss ich die Daten die angekommen sind aus dem TextCTRL auslesen und mit Werten vergleichen. Und genau da komme ich nicht weiter. Also hier mal meine Fragen.
Wie kann ich ich per evtHandler (Enter oder Update) die letzte Zeile in eine Variable einlesen? Kann ich die Vergleiche in eine "Unterroutine" legen (wie bei VB) und wie kann ich diese von einem ButtonClick Event aufrufen? Anbei mein Code bis jetzt ... ICh hoffe ihr könnt mir helfen :)

Code: Alles auswählen

#!/usr/bin/python
# -*- coding: utf-8 -*-

'''
RS232 senden und empfangen und auswerten Versuch 1

'''
#COMport = "/dev/ttyUSB0"
COMport = "COM37"
Baud = 38400

#*******Import Python Funktionen Anfang******************
import wx
from serialthread import  *
#*******Import Python Funktionen Ende********************

class Window(wx.Frame):
    
    def __init__(self, *args, **kwargs):
        super(Window, self).__init__(*args, **kwargs) 
        self.InitUI()


    def InitUI(self):
        
        self.textbox=wx.TextCtrl(self, style = wx.TE_MULTILINE, pos = (5,5),size=(385, 100))
        myfont = wx.Font(12, wx.MODERN, wx.NORMAL, wx.BOLD, False, u'Courier')
        self.textbox.SetFont(myfont)
        
        buttonOn=wx.Button(self, -1, "Start", pos=(100,230))
        buttonOff=wx.Button(self, -1, "Stop", pos=(200,230))


        menubar = wx.MenuBar()  #Menue erstellen
        self.SetMenuBar(menubar)#Menue zeichnen

#*******Filemenue Anfang*********************************
        fileMenu = wx.Menu()
        fileMenu.Append(wx.ID_NEW, '&New')
        fileMenu.Append(wx.ID_OPEN, '&Open')
        fileMenu.Append(wx.ID_SAVE, '&Save')
#*******Filemenue Ende***********************************
        
#*******Seperator Anfang********************************
        fileMenu.AppendSeparator()
#*******Seperator Ende**********************************
        
#*******Importmenue Anfang*******************************
        imp = wx.Menu()
        fileMenu.AppendMenu(wx.ID_ANY, 'I&mport', imp)
        imp.Append(wx.ID_ANY, 'Import Test1...')
        imp.Append(wx.ID_ANY, 'Import Test2...')
        imp.Append(wx.ID_ANY, 'Import TEst3...')
#*******Importmenue Ende*********************************

#*******Quitmenu Anfang**********************************
        qmi = wx.MenuItem(fileMenu, wx.ID_EXIT, '&Quit\tCtrl+W')
        fileMenu.AppendItem(qmi)
#*******Quitmenu Ende************************************

#*******Bindings Anfang**********************************
        self.Bind(wx.EVT_MENU, self.OnQuit, qmi)
        self.Bind(wx.EVT_IDLE, self.OnIdle)
        self.Bind(wx.EVT_WINDOW_DESTROY, self.OnQuit) 
        buttonOn.Bind(wx.EVT_BUTTON, self.OnButtonOn)
        buttonOff.Bind(wx.EVT_BUTTON, self.OnButtonOff)
#*******Bindings Ende************************************

        menubar.Append(fileMenu, '&File')

#*******Fenster zeigen Anfang*****************************
        self.SetSize((400, 400))
        self.SetTitle('Flow Displayboard')
        self.Centre()
        self.Show(True)
#*******Fenster zeigen Ende********************************
        
        # serial thread
        self.serial_thread = Serialthread(COMport, Baud)
        self.serial_thread.connect()
        self.textbox.AppendText ("bereit\n")
        self.textbox.AppendText ("ComPort: " + COMport + "\n")

    def OnIdle( self, event):
        # if nothing else to do, update text from message queue 
        while not self.serial_thread.msgQueue.empty():
            msg=self.serial_thread.msgQueue.get()
            self.textbox.AppendText(msg)    
   
    def OnButtonOn(self, event):
        self.textbox.AppendText ("Start\n")
        data = b"\xAA\x00\x00\x05\x00\x0A\x00\x00\x00\x00\x60\x01\x00\x00\x00\x00"
        self.serial_thread.sCOM.write(data)
        
    def OnButtonOff(self, event):
        self.textbox.AppendText ("Stop\n")
        data = b"\xAA\x00\x00\x05\x00\x0A\x00\x00\x00\x00\x60\x00\x00\x00\x00\x00"
        self.serial_thread.sCOM.write(data)
        #script2run = "c:\simple.py"
        #execfile( script2run )
        self.textbox.SetBackgroundColour("green")
        self.textbox.SetFocus()
        self.textbox.SetValue("Hallo Welt\n")
        time.sleep(10)
        value = self.textbox.GetValue()
        
        self.textbox.AppendText (value)

        
    def OnQuit(self, e):
        self.serial_thread.disconnect()
        time.sleep(0.5)
        self.Close()
        


def main():   
    ex = wx.App(redirect = False)
    Window(None)
    ex.MainLoop()    

if __name__ == '__main__':
    main()
Zuletzt geändert von Anonymous am Samstag 20. Juli 2013, 08:06, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Code-Tags gesetzt.
BlackJack

@opccorsa: Aus der GUI sollte man nur Werte einlesen, wenn sie vom Benutzer eingegeben wurden. GUIs sind eher nicht dazu gedacht um Werte von der Geschäftslogik zwischen zu speichern, sondern um mit dem Benutzer zu kommunizieren.

Die Kommentare sind in der Überzahl überflüssig, weil sie einfach nur noch einmal das aus dem Quelltext sowieso schon offensichtliche sagen. Zusätzlich sind diese „Sternchenlinien” nicht dem Quelltext entsprechend eingerückt und machen es so schwieriger die Code-Struktur zu erkennen.

Importe stehen normalerweise am Anfang des Moduls und Sternchenimporte sind keine gute Idee, weil man nicht mehr so einfach nachvollziehen kann was woher kommt.

Konstanten werden per Konvention komplett in Grossbuchstaben benannt.

Kryptische Abkürzungen sollte man vermeiden. `import_menu` ist wesentlich deutlicher als `imp`. Warum heisst das `App`-Exemplar `ex`?

Der Code in der GUI weiss IMHO zu viel über das Innenleben vom `serial_thread`-Objekt und es ist auch zu viel Geschäftslogik in der GUI. Man sollte von aussen weder auf die Queue noch auf das Dateiobjekt direkt zugreifen. Und die GUI sollte keine binären Kommandos kennen.

Wenn man das `time`-Modul verwenden möchte, sollte man es auch importieren. Allerdings hat `time.sleep()` mit mehreren Sekunden nichts in GUI-Methoden zu suchen, weil für diese Zeit die GUI „einfriert”.

Absolute Layouts sind nicht mehr zeitgemäss, weil die Anzeigegrössen, -auflösungen, und -einstellungen so stark variieren, das man besser das GUI-Toolkit entscheiden lässt wie/wo etwas platziert wird, damit alles ordentlich hinein passt. So sieht das zum Beispiel bei mir aus:
Bild
Mit Layouts wäre da nicht so eine grosse ungenutzte Fläche geblieben.

Zentrieren von Fenstern ist eine ähnlich schlechte Idee. Normalerweise platziert die Fensterverwaltung Fenster dort wo Platz ist, damit so wenig wie möglich andere (aktive) Fenster verdeckt werden. Stell Dir mal vor alle Anwendungen würden grundsätlich zentriert starten, wie viel man dann als Anwender Fenster in der Gegend herum schieben müsste.
opccorsa
User
Beiträge: 34
Registriert: Samstag 20. Juli 2013, 07:43

:oops: da bin ich wohl komplett vorbei. Leider habe ich auch nicht alles verstanden was du meintest. :K auf jeden Fall Danke für die große Mühe. Ich komme von der Basic Sprache und da waren Kommentare wichtig. Deswegen die ganzen Kommentare. Was den Code angeht der ist aus gefühlten 1000 Foren oder Tutorials. Kannst du mir einen besseren weg aufzeigen ? Verwenden möchte ich das gerne mit meinem Raspberry pi. Da ist über rs232 ein Mikrocontroller angeschlossen der befehle entgegen nimmt. Dieser dient als Steuerung einer Anlage. Also ich starte mit einem Button und er sendet 3 befehle. Nach jedem bekommt er die Antwort. Zum Beispiel messe Spannung Antwort 4.95V. Das möchte ich vergleichen ob in den Grenzen 4.5 V - 6V und wenn alles gut dann Anzeige ok. Das wollte ich als Grundlage nehmen. Nun hänge ich aber schon 3 Wochen dran. Das Ergebnis siehst du oben :) vielleicht kannst du mir ja helfen.
BlackJack

@opccorsa: In wiefern sind bei BASIC diese im Grunde sinnfreien Kommentare wichtig? Wäre mir neu, dass dort der Code nicht mehr läuft wenn Kommentare nicht vorhanden sind. Wobei es ja nicht grundsätzlich etwas gegen Kommentare zu sagen gibt. Nur sollten die dem Leser einen Mehrwert bieten zum Quelltext, also zusätzlich Sachen erklären, die nicht gleich aus dem Quelltext offensichtlich sind. Wobei es die Faustregel gibt, dass Kommentare nicht beschreiben sollten *was* der Quelltext tut, denn das sollte der Quelltext selbst ja schon beschreiben, sondern *warum* er das tut was er tut. Sofern das nicht auch schon aus dem Quelltext hervorgeht. Kommentare sind der Programmiersprache im allgemeinen egal. Es gibt Ausnahmen wenn in Kommentaren spezielle Anweisungen für den Compiler stecken. Das ist bei Python aber nur die Kodierungszeile die, wenn sie vorhanden ist, innerhalb der ersten paar Zeilen stehen muss. Üblicherweise steht die in der ersten oder zweiten Zeile.

Der bessere Weg als sich sein Programm aus Foren und Tutorials zusammen zu kopieren, ist das Programm selber zu entwickeln.

Normalerweise, also zum Beispiel im Gegensatz zu BASIC-Dialekten wo man im einem GUI-Designer eine Oberfläche zusammen klickt, und dann die Geschäftslogik eines Programms direkt als Aktionen zum Beispiel für die Schaltflächen hinterlegt, trennt man Geschäftslogik und GUI. Und üblicherweise fängt man bei der Umsetzung mit der Geschäftslogik an, und wenn die läuft, setzt man dort eine GUI drauf. Dann kann man die eigentliche Geschäftslogik zum Beispiel ohne GUI testen. Und mehrere Benutzeroberflächen für die gleiche Funktionalität verwenden. Neben wxWidgets zum Beispiel noch eine Kommandozeilenanwendung, und/oder eine Textoberfläche, und/oder eine Webanwendung.

Für Deinen Fall könnte man beispielsweise eine Klasse schreiben, welche die Anlage repräsentiert, mit entsprechenden Methoden und Attributen oder Properties.
opccorsa
User
Beiträge: 34
Registriert: Samstag 20. Juli 2013, 07:43

Danke für die Tipps ... Dann werde ich mir das mal zu Herzen nehmen ...
opccorsa
User
Beiträge: 34
Registriert: Samstag 20. Juli 2013, 07:43

OK ... Möchte Geschäftslogik und Darstellungslogik trennen !!! ... Welche Schnittstelle schaffe ich mir und bleiben beide Logiken Python? Wie steuere ich die GUI? Fragen über Fragen. :oops:
BlackJack

@opccorsa: Die Schnittstelle hängt so ziemlich komplett davon ab was Du machen willst. Das kann man so allgemein nicht beantworten. Halt ein ganz normaler Programmentwurf. Was für „Dinge” gibt es, welche Daten machen deren Zustand aus, und welche Aktionen muss man auf diesen „Dingen” durchführen können. Die üblichen Fragen bei einem objektorientierten Entwurf.
opccorsa
User
Beiträge: 34
Registriert: Samstag 20. Juli 2013, 07:43

Hallo Leute, Hallo BlackJack

Ich habe nun angefangen Module zu bauen für meine Funktion. Soweit so gut. Habe noch einige Probleme mit der Auswertung. Ich gebe euch mal den Stand durch.
eine Datei von der aus ich die Messungen mit Unter.-und Obergrenze an das Modul com_serial übergeben werden. Beide Codes im Anhang. Nun hatte ich das Problem das die Antwort immer 2x geprintet wird, dann das die letzte Antwort im Buffer bleibt und als erstes eingelesen wird wenn ich das Programm wieder starte und das ich den String den ich Empfang nicht auswerten kann. Ich weis auch nicht ob es ein String ist oder Byte ? DANKE !!!! :)

Code: Alles auswählen

#!/usr/bin/python
import com_serial
import time

com_serial.send("spsel0_on", "SPSEL0=1", "SPSEL0=1" )
time.sleep(2)
com_serial.send("ubdut_on", "DUT=1", "DUT=1")
time.sleep(2)
com_serial.send("mess0", "14.00", "25.00")
time.sleep(5)
com_serial.send("ubdut_off", "DUT=0", "DUT=0" )
com_serial.close()

Code: Alles auswählen

import time
import serial
#from serial import *


ser = serial.Serial(
    port='/dev/ttyUSB0',
    baudrate=38400,
    parity=serial.PARITY_NONE,
    stopbits=serial.STOPBITS_ONE,
    bytesize=serial.EIGHTBITS
)

#ser.open()
ser.isOpen()

def send(com, max, min):
    global last_received
    
    ser.write(com + '\r\n')
    buffer = ''
    buffer = buffer + ser.read(ser.inWaiting())
    if '\n' in buffer:
        lines = buffer.split('\n') # auf eine Zeile begrenzen
        last_received = lines[-2]
        buffer = lines[-1]
        print last_received
        if min < last_received > max:
            print "Fehler"
    ser.flushInput()
    return
    
def close():
    ser.close()
    exit()
BlackJack

@opccorsa: Die Verbindung sollte man in einem eigenen Datentyp kapseln und nicht versuchen ein Modul als Klasse zu missbrauchen. Module sollten ohne Seiteneffekte importierbar sein. Die Existenz von ``global`` solltest Du auch erst einmal vergessen. Dafür gibt es nur sehr selten sinnvolle Verwendung. Meistens deutet es darauf hin, dass man etwas in einer Klasse kapseln sollte.

Deine Verarbeitung der Antwort ist ziemlich kompliziert. Falls Du an der Stelle einfach nur eine Zeile lesen wolltest, hättest Du `next()` verwenden können, denn `Serial`-Objekte sind genau wie Dateiobjekte iterierbar, und zwar über die Zeilen.

Die ``if``-Bedingung zu den Grenzen ist falsch. Kleines Beispiel für min 1 und max 10, dann ist 5 ja eigentlich gültig:

Code: Alles auswählen

In [6]: 1 < 5 > 10
Out[6]: False
Die Rückgabewerte über die serielle Verbindung sind aber auch Zeichenketten (beziehungsweise Byteketten), da kann man numerische Werte sowieso nicht so einfach vergleichen:

Code: Alles auswählen

In [7]: '2' < '10'
Out[7]: False
Um eine sinnvolle API anbieten zu können, müsste man erst einmal das Protokoll kennen, welches über die Verbindung geht. Dann kann man die Anfragen und Antworten irgendwie klassifizieren. Vom Beispiel her sehe ich zwei verschiedene Kategorien: Ein-/Ausschalten von irgend etwas und abfragen von einem Wert. Die Antworten darauf sind auch unterschiedlich. Beim Schalten bekommt man eine „Variable” gesendet, an der man anscheinend den Zustand nach dem Schalten erkennen kann. Und beim Abfragen eines Wertes nur den Wert als Zeichenkette, die eine Gleitkommazahl darstellt.

Die beiden Fälle würde ich in der API unterschiedlich behandeln.

Für das Schalten bräuchte man eine Zuordnung von Schaltbefehl und Rückgabevariablennamen, um überprüfen zu können ob der erwartete Name mit dem erwarteten Wert zurück kommt. Falls das nicht der Fall ist, würde ich eine Ausnahme auslösen.

Beim Lesen eines Wertes möchte man als Ergebnis nicht nur den Wert haben, sondern auch wissen, ob der in einem gewissen Rahmen liegt. Das könnte man durch ein Tupel als Rückgabewert lösen, das die drei Werte noch mal enthält. Oder gleich als eigenen Datentyp, den man über ein Property fragen kann, ob der Wert innerhalb der Grenzwerte liegt. Dein Beispiel auf so eine API umgeschrieben könnte dann so aussehen:

Code: Alles auswählen

with Device() as device:
    device.switch('spsel0', True)
    device.switch('ubdut', True)
    value = device.get_value('mess0', 14, 25)
    print float(value)
    if not value.is_within_bounds:
        print 'Fehler: {0:f} nicht innerhalb {1}..{2}'.format(
            value, value.min, value.max
        )
    device.switch('ubdut', False)
BlackJack

Ungetestet:

Code: Alles auswählen

import serial


class Value(object):
    def __init__(self, value, minimum, maximum):
        self.value = value
        self.min = minimum
        self.max = maximum

    def __float__(self):
        return float(self.value)

    def __int__(self):
        return int(self.value)

    def __str__(self):
        return str(self.value)

    def __unicode__(self):
        return unicode(self.value)

    @property
    def is_within_bounds(self):
        return self.min <= self.value <= self.max


class Device(object):
    SWITCH_NAME_TO_VAR_NAME = {'spsel0': 'SPSEL0', 'ubdut': 'DUT'}

    def __init__(self, port='/dev/ttyUSB0'):
        self.connection = serial.Serial(
            port=port,
            baudrate=38400,
            parity=serial.PARITY_NONE,
            stopbits=serial.STOPBITS_ONE,
            bytesize=serial.EIGHTBITS
        )

    def __enter__(self):
        return self

    def __exit__(self, *_arguments):
        self.close()

    def close(self):
        self.connection.close()

    def _communicate(self, command):
        self.connection.write(command + '\r\n')
        self.connection.flush()
        return next(self.connection)

    def switch(self, switch_name, state):
        response = self._communicate(
            '{0}_{1}'.format(switch_name, 'on' if state else 'off')
        )
        name, _, value = response.partition('=')
        try:
            expected_name = self.SWITCH_NAME_TO_VAR_NAME[switch_name]
        except KeyError:
            raise ValueError('unknown switch {0!r}', switch_name)
        if name != expected_name:
            raise ValueError(
                'expected {0!r} for switch name {1!r}, got {2!r}'.format(
                    expected_name, switch_name, name
                )
            )
        if int(value) != state:
            raise ValueError('switch did not happen')

    def get_value(self, name, minimum, maximum):
        return Value(float(self._communicate(name)), minimum, maximum)
Das `switch()` für jede Art von Fehler einen `ValueError` verwendet ist nicht so gut, da sollte man sich vielleicht eine eigene kleine Ausnahmehierarchie überlegen um gezielter reagieren zu können.
opccorsa
User
Beiträge: 34
Registriert: Samstag 20. Juli 2013, 07:43

Hallo BlackJack --- Super deine Arbeit :) doch leider weis ich jetzt nicht mehr wie es aufrufen soll. Habe jetzt die ganze Sache ein wenig geändert und hoffe nochmal auf deine Hilfe. Da ich den Mikrocontroller selber programmiere habe ich jetzt das Protokoll abgeändert. Hauptsache es funktioniert erstmal und ich kann dann darauf aufbauen. Also nochmal zum Ursprung:

USB0 Verbindung
-Anfrage von Raspberry an Mikrocontroller
UBDUT_ON
µC meldet
1 oder 0 (1 pass - 0 fail)
-Anfrage von Raspberry an Mikrocontroller
MESS0
µC meldet
15.1 (10 < 17 > 25 pass)
-Anfrage von Raspberry an Mikrocontroller
GET R0
µC meldet
1 oder 0

Damit würde ich die anderen Fälle auch abdecken. :roll:
Antworten