GUI wird bereits nach Sekunden immer langsamer

Fragen zu Tkinter.
Antworten
mrbbenni
User
Beiträge: 3
Registriert: Freitag 16. Juni 2023, 06:38

Guten Morgen zusammen,

ich bin gerade dabei ein GUI für ein Messsystem zu erstellen. Dabei werden CAN-Bus Signale vom Auto über eine CAN2USB Schnittstelle auf einen Raspberry 3 geladen, worauf das GUI auch laufen soll.
Meine Messdaten sollen ich Echtzeit in ein Label geladen werden, jeder neue Messwert soll in der Zeile darunter angezeigt werden, die Werte sollen "runterrattern". Funktioniert soweit auch alles.
Aber ich habe folgendes Problem: Bereits nach 1,2 Sekunde nimmt die Performance meines Programms stark ab und mit der Zeit wird es immer schlimmer. Messwerte verfallen nicht, aber werden mit einem steigendem Delay angezeigt.

Wäre genial, wenn mir jemand von Euch helfen könnte. Ich habe gehört, hier gibt es ganz helle Köpfchen :)

Bitte seid nicht zu hart mit mir, bin noch Anfänger. umgehe ich z.B. die globale Variable?

LG Benni

Code: Alles auswählen

import tkinter as tk
import can
import os

os.system('sudo ifconfig can0 down')
os.system('sudo ip link set can0 type can bitrate 500000')
os.system('sudo ifconfig can0 up')

# Funktionsdefinitionen
        
def get_new_data():
    global can_data
    message = can0.recv(timeout = None) # Diese Funktion liefert die Daten vom Bus
    if message is not None:
        ID = message.arbitration_id  # Filter nach der ID
            
        if ID == gesuchte_ID1:
                can_data += f"{message}\n"
                                                
        elif ID == gesuchte_ID2:
                can_data += f"{message}\n"

    can_label.config(text = can_data, bg = "white", fg = "black", relief = "solid")
        
    can_label.after(1, get_new_data)

# Instanz
fenster = tk.Tk()
fenster.title("Drohnenmesssystem-GUI")
fenster.geometry('1280x600')
fenster.configure(bg='white')    

# Variablen, Konstanten, Objekte
can_data = ""
can0 = can.interface.Bus(channel = 'can0', bustype = 'socketcan')

# IDs nach denen gefiltert wird: Klemmensteuerung: 0x3C0, Kessy: 0x1F1
gesuchte_ID1 = 0x3C0
gesuchte_ID2 = 0x1F1

#Labels
label_header = tk.Label(fenster, text = "Echtzeit-Diagnose MLBevo", font = ("Arial",20), bg = "white", fg = "black")
label_header.pack(padx = 100)

can_label = tk.Label(fenster, bg = "white", fg = "black",text = " Starten drücken um Messung zu beginnen")
can_label.pack(padx = 20, pady = 30)

# Funktionsaufrufe
get_new_data()

#os.system('sudo ifconfig can0 down')
fenster.mainloop()
__deets__
User
Beiträge: 14544
Registriert: Mittwoch 14. Oktober 2015, 14:29

Selbst auf meinem relativ fetten M1 Mac geht das immer langsamer nach ein paar Sekunden. Fuer eine Darstellung von 1000 zusaetzlichen Nachrichten pro Sekunde ist ein Label nicht gemacht. Und das ist ja auch inhaltlich quatsch. Du kannst an die Werte darueber ja gar nicht kommen. Das Label hat keine Scrollbar, nix. Und waehrend da bestaendig neue Werte eintrudeln, sucht man da auch nicht rum. Sondern analysiert die spaeter - weshalb sie natuerlich in eine Datei geschrieben werden sollten. Die Darstellung hingegen beschraenkt sich auf ein paar Nachrichten, die auf GUI passen. 10, 20, vielleicht 100 Nachrichten. Dazu ist die collections.deque gut geeignet.

Und auch die 1ms sind doch sehr sportlich. Das wird das System nicht schaffen. Und wozu auch? Hast du einen 1KHz-Monitor? Daten schneller abzuholen, als sie dargestellt werden koennen, bringt ja nichts. Also 16ms fuer 60fps sind ausreichend. Was sich dann natuerlich auch aendern muss, ist die Abfrage der Nachrichten. Statt einmal pro Callback muessen es so viele Abfragen sein, bis der Bus leer ist.

Globaler Zustand ist einfach vermieden durch die Nutzun von Objektorientierung, die bei GUIs im Grunde eh Pflicht ist. Eine simple Version zum testen:

Code: Alles auswählen

import tkinter as tk
import time
from collections import deque

TIMEOUT = 16

class CanGui:

    def __init__(self, can_label):
        self._counter = 0
        self._can_data = deque(maxlen=10)
        self._start = start = time.monotonic()
        self._can_label = can_label

    def get_new_data(self):
        elapsed = time.monotonic() - self._start
        self._can_data.append(f"{elapsed:.3}:{self._counter}")
        self._counter += 1
        self._can_label.config(text="\n".join(self._can_data), bg = "white", fg = "black", relief = "solid")
        self._can_label.after(TIMEOUT, self.get_new_data)

def main():
    # Instanz
    fenster = tk.Tk()
    fenster.title("Drohnenmesssystem-GUI")
    fenster.geometry('1280x600')
    fenster.configure(bg='white')

    #Labels
    label_header = tk.Label(fenster, text = "Echtzeit-Diagnose MLBevo", font = ("Arial",20), bg = "white", fg = "black")
    label_header.pack(padx = 100)

    can_label = tk.Label(fenster, bg = "white", fg = "black",text = " Starten drücken um Messung zu beginnen")
    can_label.pack(padx = 20, pady = 30)

    can_gui = CanGui(can_label)
    # Trigger timer invocation once, so
    # it schedules itself.
    can_gui.get_new_data()

    fenster.mainloop()



# main guard
if __name__ == '__main__':
    main()
Benutzeravatar
grubenfox
User
Beiträge: 432
Registriert: Freitag 2. Dezember 2022, 15:49

mrbbenni hat geschrieben: Freitag 16. Juni 2023, 06:53 Meine Messdaten sollen ich Echtzeit in ein Label geladen werden, jeder neue Messwert soll in der Zeile darunter angezeigt werden, die Werte sollen "runterrattern". Funktioniert soweit auch alles.
Aber ich habe folgendes Problem: Bereits nach 1,2 Sekunde nimmt die Performance meines Programms stark ab und mit der Zeit wird es immer schlimmer. Messwerte verfallen nicht, aber werden mit einem steigendem Delay angezeigt.
Sollen denn immer alle Messwerte angezeigt werden (also wenn z.b. pro Sekunde 1000 Messwerte auflaufen, dann nach der ersten Sekunde 1000 Messwerte anzeigen, nach 2 Sekunden 2000 Messwerte anzeigen, nach 3 Sekunden 3000, 4000, 5000, ...) oder nur die vielleicht 200 aktuellsten?
mrbbenni
User
Beiträge: 3
Registriert: Freitag 16. Juni 2023, 06:38

Hallo __deets__,

vielen Dank für deine super Antwort. Du hast alles richtig erfasst. Dank ChatGPT habe ich jetzt den gleichen Code objektorientiert. Obwohl das sicher in Eure Kreisen verpönt ist.

@grubenfox, genau, ich möchte lediglich immer die letzten 20 Nachrichten angezeigt bekommen, der Rest in mir erstmal egal.

Wisst ihr, wie ich das hinbekomme?

Danke für Eure Hilfe!

LG Benni
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mrbbenni: Das eigentliche Problem ist ja schon geklärt worden, darum von mir noch allgemeinere Anmerkungen zum Quelltext:

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst. Besonders unübersichtlich ist wenn man das Hauptrogramm mit Funktionsdefinitionen vermischt.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (PascalCase).

Kommentare sollen dem Leser einen Mehrwert über den Code geben. Faustregel: Kommentare beschreiben nicht *was* der Code macht, denn das steht da bereits als Code, sondern warum er das macht. Sofern das nicht offensichtlich ist. Offensichtlich ist in aller Regel auch was in der Dokumentation von Python und den verwendeten Bibliotheken steht.

Man nummeriert keine Namen. Dann will man sich entweder bessere Namen überlegen, oder gar keine Einzelnamen/-werte verwenden, sondern eine Datenstruktur. Oft eine Liste. Bei den gesuchten IDs aber bessere Namen. Ergänzend zu dem was man (nicht) kommentiert: Wenn man einen Kommentar benötigt um die Bedeutung einer Variablen oder Konstante zu erklären, dann ist das auch immer eine Gelegenheit über den Namen selbst nachzudenken. Die beiden IDs haben ihre Werte im Kommentar noch stehen und dort steht auch was sie bedeuten, statt dass man das an der Konstanten selbst ablesen kann. Wenn man statt `gesuchter_ID2` + Kommentar gleich `KESSY_ID` schreiben würde, könnte man sich a) den Kommentar sparen, und wüsste b) auch an allen Stellen wo die `KESSY_ID` benutzt wird, was das ist, und muss nicht erst nach der Definition mit dem Kommentar suchen.

`os.system()` sollte man nicht verwenden. Die Dokumentation von der Funktion verweist auf das `subprocess`-Modul.

Fenstergrössen gibt man nicht fest vor, die ergeben sich aus den Grössen der Anzeigeelemente in dem Fenster.

Bei dem `recv()`-Aufruf wird der Defaultwert für `timeout` gesetzt, der dafür sorgt, dass die Methode a) blockiert, was ein Problem in der GUI sein kann, und b) dass sie nie `None` liefert, was den Test darauf überflüssig macht.

Der `timeout` sollte auf 0 gesetzt werden, damit das nicht blockiert.

Bei den Tests auf IDs wird in beiden Zweigen das gleiche gemacht, da braucht man also gar kein ``if`` und ein ``elif`` sondern kann das in *einem* ``if`` erledigen in dem beide Teilbedingungen stehen:

Code: Alles auswählen

        if id_ == KLEMMENSTEUERUNG_ID or id_ == KESSY_ID:
            ...
        
        # oder mit ``in``:
        
        if message.arbitration_id in [KLEMMENSTEUERUNG_ID, KESSY_ID]:
            ...
Zwischenstand ohne Berücksichtigung des Problems, dass es natürlich keinen Sinn macht ein Label unendlich mit immer mehr Text zu füllen:

Code: Alles auswählen

#!/usr/bin/env python3
import subprocess
import tkinter as tk

import can

KLEMMENSTEUERUNG_ID = 0x3C0
KESSY_ID = 0x1F1


def get_new_data(can_bus, can_data, can_label):
    while True:
        message = can_bus.recv(timeout=0)
        if message is None:
            break

        if message.arbitration_id in [KLEMMENSTEUERUNG_ID, KESSY_ID]:
            can_data += f"{message}\n"

    can_label["text"] = can_data
    can_label.after(100, get_new_data, can_bus, can_data, can_label)


def main():
    for command in [
        ["sudo", "ifconfig", "can0", "down"],
        [
            "sudo",
            "ip",
            "link",
            "set",
            "can0",
            "type",
            "can",
            "bitrate",
            "500000",
        ],
        ["sudo", "ifconfig", "can0", "up"],
    ]:
        subprocess.run(command, check=True)

    can_bus = can.interface.Bus(channel="can0", bustype="socketcan")

    fenster = tk.Tk()
    fenster.title("Drohnenmesssystem-GUI")
    fenster.configure(bg="white")

    tk.Label(
        fenster,
        text="Echtzeit-Diagnose MLBevo",
        font=("Arial", 20),
        bg="white",
        fg="black",
    ).pack(padx=100)
    can_label = tk.Label(
        fenster,
        bg="white",
        fg="black",
        relief="solid",
        text="Starten drücken um Messung zu beginnen",
    )
    can_label.pack(padx=20, pady=30)

    get_new_data(can_bus, "", can_label)
    fenster.mainloop()


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
__deets__
User
Beiträge: 14544
Registriert: Mittwoch 14. Oktober 2015, 14:29

mrbbenni hat geschrieben: Freitag 16. Juni 2023, 11:55 vielen Dank für deine super Antwort. Du hast alles richtig erfasst. Dank ChatGPT habe ich jetzt den gleichen Code objektorientiert. Obwohl das sicher in Eure Kreisen verpönt ist.

@grubenfox, genau, ich möchte lediglich immer die letzten 20 Nachrichten angezeigt bekommen, der Rest in mir erstmal egal.

Wisst ihr, wie ich das hinbekomme?
Wie sieht der Code denn aus? Und ich verstehe die Frage nicht - wie du die letzten n Nachrichten angezeigt bekommst, habe ich doch gezeigt.
mrbbenni
User
Beiträge: 3
Registriert: Freitag 16. Juni 2023, 06:38

Hallo __deet__,

ich habe leider deinen code nicht ganz verstanden.
Wie genau mache ich das bei meinem Code? Sorry, bin noch Anfänger :)

import tkinter as tk
import can
import os
import sys
import subprocess
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.animation as animation

os.system('sudo ifconfig can0 down')
os.system('sudo ip link set can0 type can bitrate 500000')
os.system('sudo ifconfig can0 up')

class CANHandler:
def __init__(self, can_label, max_data_points):

self.can_label = can_label
self.can_data = []
self.can0 = can.interface.Bus(channel='can0', bustype='socketcan')
self.max_data_points = max_data_points
self.is_paused = True

# Visualisierung
self.time = []
self.data = []

self.fig = plt.Figure(figsize=(3, 2), dpi=100)
self.ax = self.fig.add_subplot(111)

self.canvas = FigureCanvasTkAgg(self.fig, master=fenster)
self.canvas.draw()
self.canvas.get_tk_widget().pack(side=tk.BOTTOM, fill=tk.BOTH, expand=False, padx=50)

self.ax.spines['bottom'].set_color('black')
self.ax.spines['top'].set_color('black')
self.ax.spines['left'].set_color('black')
self.ax.spines['right'].set_color('black')

self.ax.tick_params(axis='x', colors='black')
self.ax.tick_params(axis='y', colors='black')

self.line, = self.ax.plot(self.time, self.data)

def handle_data(self, message):

ID = message.arbitration_id

if ID == 0x3C0:
self.can_data.append(str(message))
self.handle_3C0_message(message)
elif ID == 0x1F1:
self.can_data.append(str(message))
self.handle_1F1_message(message)

if len(self.can_data) > self.max_data_points:
self.can_data.pop(0)

self.can_label.config(text="\n".join(self.can_data), bg="white", fg="black", relief="solid")

def handle_3C0_message(self, message):
IndexByte = 2
IndexBit = 1
hex_code = message.data

gesuchtes_byte = hex_code[IndexByte]
gesuchtes_bit_status = gesuchtes_byte & (1 << IndexBit) != 0

if gesuchtes_bit_status:
background = "green"
inhalt = "Klemme 15 aktiv"
y_1 = 1
else:
background = "red"
inhalt = "Klemme 15 aus"
y_1 = 0

label_zas15.config(bg=background, text=inhalt)

self.time.append(len(self.time) + 1)
self.data.append(y_1)

self.line.set_data(self.time, self.data)
self.ax.relim()
self.ax.autoscale_view()
self.canvas.draw()

def handle_1F1_message(self, message):
pass

def get_new_data(self):
if not self.is_paused:
message = self.can0.recv(timeout = None)
if message is not None:
self.handle_data(message)

fenster.after(16, self.get_new_data)

def toggle_pause(self):
self.is_paused = not self.is_paused

def reset(self):
python = sys.executable
script = sys.argv[0]
subprocess.Popen([python, script])
sys.exit()

# Instanz
fenster = tk.Tk()
fenster.title("Drohnenmesssystem-GUI")
fenster.geometry('1280x600')
fenster.configure(bg='white')

can_label = tk.Label(fenster, bg="white", fg="black", text=" Starten drücken um Messung zu beginnen")
can_label.pack(padx=20, pady=30)

can_handler = CANHandler(can_label, max_data_points=10)

button1 = tk.Button(fenster, text="Starten", command=can_handler.toggle_pause, activebackground='orange')
button1.pack()
button1.place(x=20, y=90, width=150, height=20)

button2 = tk.Button(fenster, text="Reset", command=can_handler.reset, activebackground='orange')
button2.pack()
button2.place(x=20, y=120, width=150, height=20)

label_zas15 = tk.Label(fenster, text = "ZAS_KL_15",)
label_zas15.pack()
label_zas15.place(x = 1125, y = 540, width = 150, height = 20)


can_handler.get_new_data()

fenster.mainloop()
__deets__
User
Beiträge: 14544
Registriert: Mittwoch 14. Oktober 2015, 14:29

@benni: du musst schon probieren zu verstehen, was ich da gemacht habe. Was nicht funktionieren wird, ist ChatGPT zu befragen, und dessen maessig funktionierenden Output dann von mir oder anderen umarbeiten zu lassen. ChatGPT kann gewisse Muehen der Ebene einfacher machen. Ohne eigenes Verstaendnis wird's aber nix.

Ich erwaehne doch explizit, dass ich zur Beschraenkung der vorgehaltenen Werte collections.deque verwende. Und viel einfacher als mein Beispiel geht's ja nun nicht. Was also an *meinem* Beispiel ist dir unklar?
Antworten