Menü in Plotter Fenster (als Modul gestartet) mit Funktion (plotten) verbinden [pyqtgraph]

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Nietzsche
User
Beiträge: 20
Registriert: Dienstag 2. Mai 2023, 13:15

Hallo,

ich habe folgendes Anliegen und hoffe mir kann jemand weiter helfen. Ich habe folgende Skripte:

1.) Hauptskript (Importiert df von Skript 2. startet die Plotter Funktion von Skript 3 und übergibt df)
2.) Skript für BLE Kommunikation (erzeugt Pandas Dataframe, df und übergibt df an 1. indem die Variable von diesem importiert wird )
3.) Skript für Plotter Fenster (plottet die Daten von df indem eine Plotter Funktion integriert ist an die das Hauptskript die Daten des df übergibt wenn er sie startet)

Das klappt auch alles soweit. Das Plotter Fenster wurde über pyqtgraph realisiert. Zuerst wurde ein GUI Fenster im QT Designer gezeichnet und dann wurde das Widget hochgestuft und die entsprechende Klasse zugewiesen. In dem Skript wurde dann die UI importiert via:

"uiclass, baseclass = pg.Qt.loadUiType("plotter_window.ui")" Hier wurde ein Mainwindow von der uiclass erstellt und in dieser die Funktion für das Plotten. Soweit so gut. Das klappt auch und meine Daten werden geplottet, wobei ich seltsamerweise immer nur alle Daten oder die ersten Daten plotten kann, bei den letzten bekomme ich einen Error, dass der Index nicht gefunden würde. Der Code dazu:

Klappt:"spo2_list = df['Red'].astype(float) "
Klappt nicht: " spo2_list = df['Red'].tail(100).astype(float)"

Eventuell kann mir da wer helfen?

Mein Hauptproblem ist aber, dass ich nicht weiß wie ich den Plotter aktualisiere, wenn ich auf Datei > Aktualisieren im Fenster oben klicke (via qt Designer wurde ein Menü angelegt). Kann mir da jemand helfen? Ich habe verschiedene Wege versucht, nichts hat aber geklappt.  
Sirius3
User
Beiträge: 17710
Registriert: Sonntag 21. Oktober 2012, 17:20

Das ist jetzt schon der dritte Thread, indem du von verschiedenen Skripten schreibst. Hast du inzwischen __blackjacks__ Vorschläge umgesetzt? Ein GUI-Programm ist in Klassen gegliedert. Ein Programm kann mehrere Module haben, aber nicht aus mehreren Skripten bestehen.
Aus Modulen importiert man Funktionen oder Klassen, aber keine Variablen. Denn das würde ja bedeuten dass man globale Varianten hat ordentlichen Programm nicht vorkommen sollten.
Statt Fehlermeldung zu beschreiben, poste hier bitte den kompletten Traceback mit dem dazugehörigen Code.
Nietzsche
User
Beiträge: 20
Registriert: Dienstag 2. Mai 2023, 13:15

Hallo,
ich versuche mal alles aus dem Forum so gut umzusetzen wie ich es verstanden habe. :)

Zunächst zu den Modulen/ Skripten, vielleicht verstehe ich da was falsch. Soweit ich in meinen Python Buch gelesen habe, sind Module (auch) Skripte die beim Einbinden einmal gestartet werden, oder ist das falsch?

z.B.:
Jedes Mal, wenn ein Python-Skript ausgeführt wird, wird ein Bytecode erstellt. Wenn ein Python-Skript als Modul importiert wird, wird der Bytecode in der entsprechenden .pyc-Datei gespeichert
__blackjack__ hat hier geschrieben:
Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.
Jetzt bin ich mir da unsicher ob es zulässig ist, dass man in einem "Modul" auch einen Multithread startet, indem die Funktion aufgerufen wird. Weil man könnte das auch so interpretieren, dass die Threads nur im Hauptskript geöffnet werden sollen und die Funktionen und Klassen in den Modulen? Ich binde die .py Module jedenfalls als Modul ein bzw importiere from modul_xyz import xyz.

Bzgl der globalen Variable. Das könnte ich so ändern, dass ich die Variable über eine Return Funktion aus dem Modul zurückgebe, dann kann ich diese lokal machen.


Ad 1: Aktualisieren des Plotter Fensters

Hier ist einmal mein Code aus dem Modul "plotter.py".

Code: Alles auswählen

import pyqtgraph as pg
from PyQt6.QtWidgets import QMainWindow
import numpy as np

uiclass, baseclass = pg.Qt.loadUiType("plotter_window.ui")

class PlotterWindow(QMainWindow, uiclass):
    def __init__(self, df):
        super().__init__()
        self.setupUi(self)

    def plot(self, df):
        print("Daten des DF in Plotter Modul")
        print(df)
        spo2_list = df['Red'].astype(float) # Gibt alle Werte aus, funktioniert

        # spo2_list = df['Red'][:100].astype(float) #gibt die ersten 100 Werte aus, funktioniert
        # spo2_list = df['Red'].tail(100).astype(float) #soll die letzten 100 Werte ausgeben, funktioniert nicht

        print("spo2_list = ")
        print(spo2_list)
        print("letzte 100 Werte")
        letzte_werte = df['Red'].tail(100).astype(float)
        print(letzte_werte) #Gibt die letzten 100 Werte an der Konsole aus, funktioniert
        spo2_list = spo2_list[np.isfinite(spo2_list)]  # Filtert ungültige Werte
        self.graphWidget.plot(spo2_list)


    def update_button_plotter(self):"
        print("Daten des DF in Plotter Modul")

Das starte ich nun in meinem Hauptprogramm über eine Funktion, wenn ein Button geklickt wird:

Code: Alles auswählen

plotter_window = PlotterWindow(df)

    # Überprüfen, ob das Plotter-Fenster bereits erstellt wurde
    if plotter_window is None:
        # Erstellen einer Instanz des Plotter-Fensters und übergeben von df
        plotter_window = PlotterWindow(df)
        plotter_window_ui = plotter_window.ui  # Referenz auf die UI des Plotter-Fensters
        uic.loadUi("plotter_window.ui", self)


    plotter_window.plot(df)  # Aufruf der plot-Funktion, um den Graphen anzuzeigen
    plotter_window.show()  # Annzeigen des Plotter Fensters
Das Fenster wird mir korrekt geöffnet und die Daten von df übergeben, wobei ich nun immer das Plotter Fenster neu öffnen muss, ich möchte es aber über den Menüpunkt Datei >> Aktualisieren aktualisieren. Wie bereits gesagt hat mein Plotter Fenster eine .ui im qt Designer erhalten, hier wurde ein Widget hochgestuft. Ich muss jetzt also den Aktualisieren Menüpunkt mit dem aktualisieren des Plotters verknüpfen. Fehler habe ich hier nie erhalten, aber einfach keine Reaktion. Ich hätte zuerst einmal versucht eine einfache Funktion mit print "Aktualisieren Butten gedrückt" zu verbinden, aber irgendwie klappt das nicht. Bei meinem anderen Fenster hatte ich dieses Problem nicht. Ich kann den Code dann nochmal reinschreiben. (Hab den Code gestern gelöscht um es nochmal neu zu versuchen)

Ad 2 Error beim Umstellen der Plotter Daten

Wenn ich alle Daten des df plotten will, oder die ersten, dann klappt es, nur wenn ich die letzten plotten will bekomme ich:

Code: Alles auswählen

Start Button wurde gedrückt
30-05-2023_11-26-59.xlsx
30-05-2023_11-26-59.xlsx
Datei angelegt
      Red    IR
0    1014  4900
1    1013  4900
2    1012  4897
3    1021  4895
4    1019  4888
..    ...   ...
295  1021  4887
296  1020  4886
297  1011  4882
298  1024  4885
299  1029  4890

[300 rows x 2 columns]
Daten des DF in Plotter Modul
      Red    IR
0    1014  4900
1    1013  4900
2    1012  4897
3    1021  4895
4    1019  4888
..    ...   ...
295  1021  4887
296  1020  4886
297  1011  4882
298  1024  4885
299  1029  4890

[300 rows x 2 columns]
spo2_list =
200    1023.0
201    1020.0
202    1019.0
203    1018.0
204    1015.0
        ...
295    1021.0
296    1020.0
297    1011.0
298    1024.0
299    1029.0
Name: Red, Length: 100, dtype: float64
letzte 100 Werte
200    1023.0
201    1020.0
202    1019.0
203    1018.0
204    1015.0
        ...
295    1021.0
296    1020.0
297    1011.0
298    1024.0
299    1029.0
Name: Red, Length: 100, dtype: float64
Traceback (most recent call last):
  File "C:\Users\emanu\miniconda34\lib\site-packages\pandas\core\indexes\base.py", line 3652, in get_loc
    return self._engine.get_loc(casted_key)
  File "pandas\_libs\index.pyx", line 147, in pandas._libs.index.IndexEngine.get_loc
  File "pandas\_libs\index.pyx", line 176, in pandas._libs.index.IndexEngine.get_loc
  File "pandas\_libs\hashtable_class_helper.pxi", line 2606, in pandas._libs.hashtable.Int64HashTable.get_item
  File "pandas\_libs\hashtable_class_helper.pxi", line 2630, in pandas._libs.hashtable.Int64HashTable.get_item
KeyError: 0

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "C:\Users\emanu\source\repos\PythonApplication3\PythonApplication3\PythonApplication3.py", line 369, in Plotter_clicked
    plotter_window.plot(df)  # Aufruf der plot-Funktion, um den Graphen anzuzeigen
  File "C:\Users\emanu\source\repos\PythonApplication3\PythonApplication3\plotter.py", line 27, in plot
    self.graphWidget.plot(spo2_list)
  File "C:\Users\emanu\miniconda34\lib\site-packages\pyqtgraph\graphicsItems\PlotItem\PlotItem.py", line 630, in plot
    item = PlotDataItem(*args, **kargs)
  File "C:\Users\emanu\miniconda34\lib\site-packages\pyqtgraph\graphicsItems\PlotDataItem.py", line 366, in __init__
    self.setData(*args, **kargs)
  File "C:\Users\emanu\miniconda34\lib\site-packages\pyqtgraph\graphicsItems\PlotDataItem.py", line 697, in setData
    dt = dataType(data)
  File "C:\Users\emanu\miniconda34\lib\site-packages\pyqtgraph\graphicsItems\PlotDataItem.py", line 1207, in dataType
    first = obj[0]
  File "C:\Users\emanu\miniconda34\lib\site-packages\pandas\core\series.py", line 1007, in __getitem__
    return self._get_value(key)
  File "C:\Users\emanu\miniconda34\lib\site-packages\pandas\core\series.py", line 1116, in _get_value
    loc = self.index.get_loc(label)
  File "C:\Users\emanu\miniconda34\lib\site-packages\pandas\core\indexes\base.py", line 3654, in get_loc
    raise KeyError(key) from err
KeyError: 0
Wenn ich das auf alle Daten oder nur die ersten umändere (im Beispiel oben plotte ich alle, siehe Kommentare) klappt es.

Ich hoffe ich habe nun alles nachvollziehbar beschrieben, sonst kann ich auch kurz ein Video machen und auf YT hochladen um die GUI zu erklären.

MFG
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Nietzsche Module werden beim ersten importieren einmal ausgeführt, aber das macht sie nicht zu Skripten. Ein Skript ist ein Modul das dafür *gedacht* ist, *direkt* ausgeführt zu werden.

`pg.Qt.loadUiType()` ist komisch, ich würde es sogar als falsch ansehen. Das ist die `PyQt6.uic.loadUiType()`-Funktion. Die von woanders zu importieren ist verwirrend, weil der Leser natürlich denkt `pg.Qt.loadUiType()` ist etwas anderes. Das `baseclass` nicht verwendet wird ist auch falsch. Das wird doch da extra zurückgegeben, damit man die Basisklasse nicht hart in den Quelltext schreiben muss, sondern darantiert die verwendet, die auch im Designer verwendet wurde.

Wenn `PlotterWindow.__init__()` nicht unsinnigerweise ein Argument entgegen nehmen würde mit dem dann überhaupt nichts gemacht wird, könnte man sich die `__init__()` komplett sparen und die der generierten Basisklasse verwenden.

Das die Plot-Methode die Daten erst in `float` umwandeln muss ist komisch. Die sollte die Daten schon in der passenden Form bekommen. Auch die Auswahl und das filtern würde ich da nicht rein schreiben. Das erwartet man bei einem generischen `plot()` nicht.

`spo2` ist eine kryptische Abkürzung und `_list` ist ein bisschen irreführend für etwas das keine Liste ist.

Das mein Erstellen des `PlotterWindow`-Exemplars dann noch mal `loadUi()` auf ein Attribut von dem Objekt mit der gleichen *.ui-Datei angewendet wird, ist sehr wirr und falsch.

Bei dem Fehler mit dem `tail()` ist das Problem anscheinend das `pyqtgraph` eigentlich eine Sequenz erwartet und auf die Werte per Index von 0 an aufwärts zugreift. Das funktioniert bei einem `Series`-Objekt dass einen solchen Index hat. Bei `tail()` fängt der Index vom `Series`-Objekt dann aber nicht mehr bei 0 an. Und das ausfiltern von nicht-endlichen Werten lässt auch Indexwerte verschwinden, was beim Plotten zu Problemen führen kann. Entweder Du nimmst da nur die Werte aus dem `Series`-Objekt, oder Du sorgst dafür dass das vor dem Plotten einen Index mit aufsteigenden Werten bekommt, die bei 0 anfangen.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Nietzsche
User
Beiträge: 20
Registriert: Dienstag 2. Mai 2023, 13:15

Hallo _blackjack__,

da hab ich nun einiges zu verbessern, danke. Ja, ich hab die Funktion 2,3 mal umgeschrieben: "Das mein Erstellen des `PlotterWindow`-Exemplars dann noch mal `loadUi()` auf ein Attribut von dem Objekt mit der gleichen *.ui-Datei angewendet wird, ist sehr wirr und falsch." - das ist mir in der Nacht "liegen" geblieben, habs gleich ausgebessert. Ich denke, dass ich hier "Bei dem Fehler mit dem `tail()`" mal grob verstanden habe, wo das Problem liegt, zur Lösung muss ich mir dann einige Gedanken machen.


"spo2` ist eine kryptische Abkürzung" Das sehe ich nicht so, ist aber eine andere Disziplin:

"Was ist SpO2? Die Sauerstoffsättigung Ihres Blutes (SpO2) bezeichnet den Prozentsatz des Blutes, der mit Sauerstoff gesättigt ist oder Sauerstoff enthält. "
Die restlichen Fehler bessere ich auch gleich mal aus.

Bzgl des Menüs, Datei > Aktualisieren > hat da jemand Tipps bzgl Signale / Solts? Ich versuche das gleich nochmal zu verbinden. Das Signal muss ich ja in meinem Modul in der Klasse des Fensters mit der Funktion des Fensters verbinden, oder? Oder in meinem Hauptprogramm?
Sirius3
User
Beiträge: 17710
Registriert: Sonntag 21. Oktober 2012, 17:20

Also ist doch spo2 eigentlich saturation_o2, denn o2 ist eine übliche Abkürzung für Sauerstoff, sp ist aber keine übliche Abkürzung.
Nietzsche
User
Beiträge: 20
Registriert: Dienstag 2. Mai 2023, 13:15

Sirius3 hat geschrieben: Mittwoch 31. Mai 2023, 10:24 Also ist doch spo2 eigentlich saturation_o2, denn o2 ist eine übliche Abkürzung für Sauerstoff, sp ist aber keine übliche Abkürzung.

sO2 ist die Sauerstoffsättigung allgemein, spO2 ist die pulsoxymetrisch gemessene Sauerstoffsättigung, das ist ein allgemein üblicher Begriff in der Medizin/ Medizintechnik.
Nietzsche
User
Beiträge: 20
Registriert: Dienstag 2. Mai 2023, 13:15

Das Verbinden mit dem Menü klappt jetzt, ich kann auch die letzten 100 Werte plotten mit;

Code: Alles auswählen

        spo2_list = df['Red'].tail(100).astype(float)  # Gibt die letzten 100 Werte aus
        spo2_list.reset_index(drop=True, inplace=True)  # Setzt den Index zurück
        self.plot_item.setData(y=np.array(spo2_list))  # Aktualisiere die Daten des Graphen
        print("Daten des DF in Plotter Modul")
Jetzt möchte ich die 100 Datenpunkte aber einzeln ausgeben und danach die neuen 100 Werte laden. Das laden kann ich über einen Timer machen, das klappt also schon, aber wie kann ich die 100 Werte einzeln plotten, kann mir da mal jemand weiterhelfen? Ich glaub ich brauche einen Timer der dann einen Datenpunkt von den 100 plottet und dann den Index um einen erhöht und nach einer kurzen Zeit von wenigen ms einen neuen Datenpunkt zu plottet (den alten aber nicht löscht) und den Index erhöht, solange bis alle 100 durch sind und dann den neuen Datensatz von 100 Daten ladet und hier den Index neu setzt, sowie die alten Daten löscht oder?
Nietzsche
User
Beiträge: 20
Registriert: Dienstag 2. Mai 2023, 13:15

Hab das gefunden, aber ganz hinbekommen hab ichs noch nicht: https://pypi.org/project/pglive/
Nietzsche
User
Beiträge: 20
Registriert: Dienstag 2. Mai 2023, 13:15

Ich hab das nun anders gelöst und hier die Erklärung. Zuerst wird ein Datensatz geplottet. Danach werden 100 Datenpunkte geladen (die letzten 100) und der Index wird auf 0 gesetzt. Dann wird ein Datenpunkt geplottet mit dem Index 0 und der Index der ausgelesen wird wird um eins erhöht. Jetzt folgt eine Pause und es wird der nächste Datenpunkt geprintet usw usf. Bis die 100 Daten ausgelesen wurden und dann werden neue 100 Datenpunkte geladen. Damit das dann flüssig läuft muss man sich nun etwas bei den ms der Pause spielen, dann klappt es einwandfrei.
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Nietzsche Wäre es nicht besser ein Fenster mit 100 Datenpunkten über den Datensatz laufen zu lassen, statt alle 100 Datenpunkte alle vorherigen Datenpunkte komplett aus der Anzeige zu werfen‽
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Nietzsche
User
Beiträge: 20
Registriert: Dienstag 2. Mai 2023, 13:15

__blackjack__ hat geschrieben: Donnerstag 1. Juni 2023, 12:22 @Nietzsche Wäre es nicht besser ein Fenster mit 100 Datenpunkten über den Datensatz laufen zu lassen, statt alle 100 Datenpunkte alle vorherigen Datenpunkte komplett aus der Anzeige zu werfen‽
Das kann man natürlich auch machen, ich habe es aktuell so gelöst, dass ich 2 Plotter Fenster erzeugt habe. Einmal einen live Plotter der mir jeweils die letzten 100 Datenpunkte gibt und einmal ein Fenster bei dem ich mit Aktualisieren bzw F5 jeweils die letzten 100, 1000, 10 000 oder 100 000 Daten lade. Wobei ich das später auf Zeitstempel umstellen will. Am Ende kann man das dann je nach eigenem Belieben anpassen. :)
Antworten