import tkinter as tk from tkinter import ttk, messagebox import customtkinter customtkinter.set_appearance_mode("light") customtkinter.set_default_color_theme("blue") import serial import serial.tools.list_ports import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import threading import time import numpy as np import csv import os class ArduinoGUI: def __init__(self, root): self.my_font = customtkinter.CTkFont(family="Calibri", size=15) self.root = root self.root.title("PRG 342 GUI") self.root.attributes('-fullscreen', True) # Setzt das Fenster in den Vollbildmodus self.ser = None self.arduino = None self.ports = list(serial.tools.list_ports.comports()) self.running = True self.current_point = None self.setpoints = [] # Flags for user actions self.setpoint_flag = False self.multi_setpoints_flag = False self.tare_flag = False self.demag_flag = False self.input_source_flag = False self.pid_flag = False # Flag for sending PID parameters self.pid_params = {} self.selected_pid = None self.create_widgets() self.load_configurations() # Start the communication thread self.communication_thread = threading.Thread(target=self.communication_loop) self.communication_thread.start() self.logging = False self.start_time = None self.log_file = None self.csv_writer = None def toggle_datalogger(self): if not self.logging: self.start_logging() else: self.stop_logging() def start_logging(self): self.logging = True self.start_time = time.time() self.log_file = open("Data.csv", mode='w', newline='', encoding='utf-8') self.csv_writer = csv.writer(self.log_file, delimiter='\t') # PID parameters under each other self.csv_writer.writerow(["Kp", f"{self.pid_params[self.selected_pid]['Kp']:.3f}".replace('.', ',')]) self.csv_writer.writerow(["Ki", f"{self.pid_params[self.selected_pid]['Ki']:.3f}".replace('.', ',')]) self.csv_writer.writerow(["Kd", f"{self.pid_params[self.selected_pid]['Kd']:.3f}".replace('.', ',')]) self.csv_writer.writerow(["Zeit in s", "Winkel in Grad", "Sollwert in Nm", "Istwert in Nm", "Erregerspannung in V"]) messagebox.showinfo("Info", "Datalogging gestartet") def stop_logging(self): self.logging = False if self.log_file: self.log_file.close() messagebox.showinfo("Info", "Datalogging gestoppt") def create_widgets(self): # Labels and ComboBox for Stellantriebstyp self.stellantriebstyp_label = customtkinter.CTkLabel(self.root, text="Stellantriebstyp:", font=self.my_font) self.stellantriebstyp_label.grid(row=0, column=0, padx=(10, 5), pady=(10, 0), sticky="w") self.config_combobox_value = customtkinter.StringVar() self.config_combobox = customtkinter.CTkComboBox(self.root, variable=self.config_combobox_value, state="readonly", font=self.my_font) self.config_combobox.grid(row=0, column=1, columnspan=1, padx=(0, 10), pady=(10, 0), sticky="ew") #self.config_combobox.bind("<>", self.on_config_selected) # Setzen Button für Stellantriebstyp self.set_pid_button = customtkinter.CTkButton(self.root, text="Setzen", command=self.set_pid_parameters, font=self.my_font) self.set_pid_button.grid(row=0, column=2, padx=(0, 10), pady=(10, 0), sticky="ew") # Labels and ComboBox for COM Port self.com_label = customtkinter.CTkLabel(self.root, text="COM Port:", font=self.my_font) self.com_label.grid(row=1, column=0, padx=(10, 5), pady=(10, 0), sticky="w") self.combobox_value = customtkinter.StringVar() self.combobox = customtkinter.CTkComboBox(self.root, variable=self.combobox_value, font=self.my_font) self.combobox.configure(values=[port.device for port in self.ports]) self.combobox.grid(row=1, column=1, padx=(0, 10), pady=(10, 0), sticky="ew") self.connect_button = customtkinter.CTkButton(self.root, text="Verbinden", command=self.connect_arduino, font=self.my_font) self.connect_button.grid(row=1, column=2, padx=(0, 10), pady=(10, 0), sticky="ew") # Setpoint entry self.setpoint_label = customtkinter.CTkLabel(self.root, text="Sollwerteingabe in Nm:", font=self.my_font) self.setpoint_label.grid(row=2, column=0, padx=(10, 5), pady=(10, 0), sticky="w") self.setpoint_entry = customtkinter.CTkEntry(self.root, font=self.my_font) self.setpoint_entry.grid(row=2, column=1, padx=(0, 10), pady=(10, 0), sticky="ew") self.set_setpoint_button = customtkinter.CTkButton(self.root, text="Setzen", command=self.set_setpoint, font=self.my_font) self.set_setpoint_button.grid(row=2, column=2, padx=(0, 10), pady=(10, 0), sticky="ew") # Multi Setpoints Textbox and Button self.multi_setpoints_label = customtkinter.CTkLabel(self.root, text="Sollwerteeingabe\n(Winkel;Drehmoment):", font=self.my_font) self.multi_setpoints_label.grid(row=3, column=0, padx=(10, 5), pady=(10, 0), sticky="w") self.multi_setpoints_text = customtkinter.CTkTextbox(self.root, font=self.my_font) self.multi_setpoints_text.grid(row=3, column=1, padx=(0, 10), pady=(10, 0), sticky="ew") self.send_multi_setpoints_button = customtkinter.CTkButton(self.root, text="Sollwerte senden", command=self.send_multi_setpoints, font=self.my_font) self.send_multi_setpoints_button.grid(row=3, column=2, padx=(0, 10), pady=(10, 0), sticky="ew") # Analog Control CheckBox self.checkbox_var = customtkinter.IntVar() self.checkbox = customtkinter.CTkCheckBox(self.root, text="Analogsteuerung", variable=self.checkbox_var, command=self.input_source_Switch, font=self.my_font) self.checkbox.grid(row=4, column=0, columnspan=3, padx=(10, 10), pady=(10, 0), sticky="w") # Tare and Demagnetize Buttons self.tare_button = customtkinter.CTkButton(self.root, text="Drehwinkel tarieren", command=self.tare_angle, font=self.my_font) self.tare_button.grid(row=5, column=0, padx=(10, 5), pady=(10, 0), sticky="sew") self.demag_button = customtkinter.CTkButton(self.root, text="Manuelle Entmagnetisierung", command=self.demagnetize, font=self.my_font) self.demag_button.grid(row=5, column=1, padx=(0, 5), pady=(10, 0), sticky="sew") # Exit Button self.exit_button = customtkinter.CTkButton(self.root, text="Beenden", command=self.on_closing, font=self.my_font) self.exit_button.grid(row=5, column=2, padx=(0, 10), pady=(10, 0), sticky="sew") #Datalogger self.datalogger_button = customtkinter.CTkButton(self.root, text="Datenlogger", command=self.toggle_datalogger, font=self.my_font) self.datalogger_button.grid(row=4, column=0, padx=(0, 10), pady=(10, 0), sticky="sew") # Plot self.figure, self.ax = plt.subplots() self.canvas = FigureCanvasTkAgg(self.figure, self.root) self.canvas.get_tk_widget().grid(row=0, column=3, rowspan=6, columnspan=4, padx=(10, 10), pady=(10, 0), sticky="nsew") self.ax.set_title("Drehmoment-Drehwinkel-Kennlinie") self.ax.set_xlabel("Winkel (°)") self.ax.set_ylabel("Drehmoment (Nm)") self.ax.set_xlim(0, 90) self.ax.set_ylim(0, 50) # Current readings labels self.current_angle = 0 self.current_torque = 0 self.analogInput = 0 self.currentSetpoint = 0 self.currentOutput = 0 self.angle_label_var = customtkinter.StringVar() self.angle_label_var.set("Drehwinkel: 0") self.angle_label = customtkinter.CTkLabel(self.root, textvariable=self.angle_label_var, font=self.my_font) self.angle_label.grid(row=6, column=3, padx=(10, 5), pady=(10, 0), sticky="sw") self.torque_label_var = customtkinter.StringVar() self.torque_label_var.set("Istwert: 0") self.torque_label = customtkinter.CTkLabel(self.root, textvariable=self.torque_label_var, font=self.my_font) self.torque_label.grid(row=6, column=4, padx=(10, 5), pady=(10, 0), sticky="sw") self.analogread_label_var = customtkinter.StringVar() self.analogread_label_var.set("Analogeingang: 0") self.analogread_label = customtkinter.CTkLabel(self.root, textvariable=self.analogread_label_var, font=self.my_font) self.analogread_label.grid(row=6, column=5, padx=(10, 5), pady=(10, 0), sticky="sw") self.currentSetpoint_label_var = customtkinter.StringVar() self.currentSetpoint_label_var.set("Aktueller Sollwert: 0") self.currentSetpoint_label = customtkinter.CTkLabel(self.root, textvariable=self.currentSetpoint_label_var, font=self.my_font) self.currentSetpoint_label.grid(row=6, column=6, padx=(10, 5), pady=(10, 0), sticky="sw") self.root.grid_columnconfigure(0, weight=0) self.root.grid_columnconfigure(1, weight=0) self.root.grid_columnconfigure(2, weight=0) self.root.grid_columnconfigure(3, weight=1) self.root.grid_columnconfigure(4, weight=1) self.root.grid_columnconfigure(5, weight=1) self.root.grid_columnconfigure(6, weight=1) self.root.grid_rowconfigure(0, weight=0) self.root.grid_rowconfigure(1, weight=0) self.root.grid_rowconfigure(2, weight=0) self.root.grid_rowconfigure(3, weight=0) self.root.grid_rowconfigure(4, weight=0) self.root.grid_rowconfigure(5, weight=1) self.root.grid_rowconfigure(6, weight=0) def set_pid_parameters(self): if self.arduino: self.selected_pid = self.config_combobox.get() if self.selected_pid in self.pid_params: self.pid_flag = True else: messagebox.showerror("Fehler", "Ausgewählte PID Konfiguration nicht gefunden.") else: messagebox.showwarning("Warnung", "Keine Verbindung zum Arduino hergestellt.") def load_configurations(self): try: with open("conf.txt", "r") as file: config_name = None for line in file: line = line.strip() if line.startswith("#") or not line: continue if "{" in line: config_name = line.split("{")[0].strip() self.pid_params[config_name] = {"Kp": 0.0, "Ki": 0.0, "Kd": 0.0} elif "}" in line: config_name = None else: if config_name: key, value = line.split(":") key = key.strip() value = round(float(value.strip()), 3) self.pid_params[config_name][key] = value self.config_combobox.configure(values=list(self.pid_params.keys())) if self.pid_params: self.config_combobox.set(list(self.pid_params.keys())[0]) self.selected_pid = list(self.pid_params.keys())[0] except Exception as e: messagebox.showerror("Fehler", f"Fehler beim Laden der Konfigurationen: {e}") def connect_arduino(self): com_port = self.combobox_value.get() if com_port: try: self.arduino = serial.Serial(com_port, 19200, timeout=1) messagebox.showinfo("Info", "Arduino verbunden an " + com_port) except Exception as e: messagebox.showerror("Fehler", str(e)) else: messagebox.showwarning("Warnung", "Bitte einen COM-Port auswählen") def connect_lsp(self): try: ports = serial.tools.list_ports.comports() used_ports = [port.device for port in ports] serPort = None if os.name == 'nt': for i in range(1, 257): port = f'(COM{i}' if (port not in used_ports) and (serPort == None): serPort = port pass else: for i in range(256): port = f'/dev/ttyS{i}' if (port not in used_ports) and (serPort == None): serPort = port pass for i in range(256): port = f'/dev/ttyUSB{i}' if (port not in used_ports) and (serPort == None): serPort = port pass if serPort != None: self.ser = serial.Serial(serPort, 9600, timeout=1) print(F"Slave Port bereit an {serPort}") except Exception as e: print(F"Fehler beim Öffnen des Slave Ports an {serPort}") pass def set_setpoint(self): if self.arduino: try: self.setpoint = float(self.setpoint_entry.get()) * 1000 self.setpoint_flag = True except ValueError: messagebox.showerror("Fehler", "Ungültiger Setpoint.") else: messagebox.showwarning("Warnung", "Keine Verbindung zum Arduino hergestellt") def send_multi_setpoints(self): if self.arduino: setpoints = self.multi_setpoints_text.get("1.0", tk.END).strip().split("\n") coordinates = [] for point in setpoints: try: angle, torque = map(float, point.split(";")) coordinates.append((angle, torque)) except ValueError: messagebox.showerror("Fehler", f"Ungültige Eingabe: {point}") return coordinates.sort() self.setpoints = coordinates self.plot_coordinates(coordinates) self.multi_setpoints_flag = True else: messagebox.showwarning("Warnung", "Keine Verbindung zum Arduino hergestellt") def plot_coordinates(self, coordinates): angles, torques = zip(*coordinates) self.ax.clear() self.ax.plot(angles, torques, 'bo-', label="Setpoints") self.ax.set_title("Drehmoment-Drehwinkel-Kennlinie") self.ax.set_xlabel("Winkel in °") self.ax.set_ylabel("Drehmoment in Nm") self.ax.set_xlim(0, 90) self.ax.set_ylim(0, 50) self.ax.legend() self.canvas.draw() def interpolate_coordinates(self, coordinates): interpolated_points = [] for i in range(len(coordinates) - 1): start_angle, start_torque = coordinates[i] end_angle, end_torque = coordinates[i + 1] num_points = int((end_angle - start_angle) * 2) angles = np.linspace(start_angle, end_angle, num_points + 1) torques = np.linspace(start_torque, end_torque, num_points + 1) interpolated_points.extend(zip(angles, torques)) full_interpolated_points = [(angle, 0) for angle in np.arange(0, 90.5, 0.5)] for angle, torque in interpolated_points: index = int(angle * 2) full_interpolated_points[index] = (angle, torque) return full_interpolated_points def send_interpolated_points(self): if self.arduino: setpoints_command = "u" for angle, torque in self.setpoints: torque_mnm = torque * 1000 setpoints_command += f"{int(angle * 2)},{torque_mnm:.1f};" setpoints_command = setpoints_command.rstrip(';') + "u\n" self.arduino.write(setpoints_command.encode()) timeout_ctr = time.time() while(self.arduino.in_waiting == 0 and time.time() - timeout_ctr < 2): pass try: print(self.arduino.readline()) #print(self.arduino.read_all()) except Exception as e: print(e) def tare_angle(self): if self.arduino: self.tare_flag = True else: messagebox.showwarning("Warnung", "Keine Verbindung zum Arduino hergestellt") def demagnetize(self): if self.arduino: self.demag_flag = True else: messagebox.showwarning("Warnung", "Keine Verbindung zum Arduino hergestellt") def input_source_Switch(self): if self.arduino: self.input_source_flag = True else: messagebox.showwarning("Warnung", "Keine Verbindung zum Arduino hergestellt") def communication_loop(self): while self.running: if self.arduino: self.arduino.read_all() if self.setpoint_flag: self.arduino.write(f"s{self.setpoint:.0f},0\n".encode()) self.setpoint_flag = False while self.arduino.out_waiting: pass if self.multi_setpoints_flag: self.send_interpolated_points() self.multi_setpoints_flag = False while self.arduino.out_waiting: pass if self.tare_flag: self.arduino.write(b'w\n') self.tare_flag = False while self.arduino.out_waiting: pass if self.demag_flag: self.arduino.write(b'e\n') self.demag_flag = False while self.arduino.out_waiting: pass if self.input_source_flag: self.arduino.write(b'S\n') self.input_source_flag = False while self.arduino.out_waiting: pass if self.pid_flag: self.send_pid_parameters() self.pid_flag = False self.arduino.write(b'a\n') timeout_ctr = time.time() while(self.arduino.in_waiting == 0 and time.time() - timeout_ctr < 2): pass try: data = self.arduino.readline().decode(errors='ignore').strip().split(";") if len(data) >= 4 and all(data): # Ensure all fields are not empty self.current_angle = float(data[0]) / 1000 if data[0] else 0 self.current_torque = float(data[1]) / 1000 if data[1] else 0 self.analogInput = float(data[2]) / 1000 if data[2] else 0 self.currentSetpoint = float(data[3]) / 1000 if data[3] else 0 self.currentOutput = float(data[4]) * 0.006349206 if data[4] else 0 self.angle_label_var.set(f"Drehwinkel: {self.current_angle:.1f} °") self.torque_label_var.set(f"Istwert: {self.current_torque:.1f} Nm") self.analogread_label_var.set(f"Analogeingang: {self.analogInput:.1f} V") self.currentSetpoint_label_var.set(f"Aktueller Sollwert: {self.currentSetpoint:.1f} Nm") if self.current_point is not None: self.current_point.remove() self.current_point, = self.ax.plot([self.current_angle], [self.current_torque], 'ro') self.canvas.draw() if self.logging: elapsed_time = time.time() - self.start_time formatted_time = f"{elapsed_time:.3f}".replace('.', ',') formatted_angle = f"{self.current_angle:.3f}".replace('.', ',') formatted_setpoint = f"{self.currentSetpoint:.3f}".replace('.', ',') formatted_torque = f"{self.current_torque:.3f}".replace('.', ',') formatted_output = f"{self.currentOutput:.3f}".replace('.', ',') self.csv_writer.writerow([formatted_time, formatted_angle, formatted_setpoint, formatted_torque, formatted_output]) except Exception as e: print(e) time.sleep(0.01) if self.ser: if self.ser.in_waiting: try: command = "" while self.ser.in_waiting: command += self.ser.read(1).decode(errors='ignore').strip() if command[0] == 'a': message = f"{self.current_angle};{self.current_torque};{self.analogInput};{self.currentSetpoint};{self.currentOutput}\n" self.ser.write(message.encode()) if command[0] == 'w': self.tare_angle() if command[0] == 'u': outCommand = F"{command}\n" self.arduino.write(outCommand.encode()) print(self.arduino.readline()) time.sleep(0.1) self.ser.read() except Exception as e: print(e) #self.ser.write(b'Hallo\n') else : try: pass self.connect_lsp() except Exception as e: print(e) def send_pid_parameters(self): if self.selected_pid in self.pid_params: pid_values = self.pid_params[self.selected_pid] kp = f"{pid_values['Kp']:.3f}" ki = f"{pid_values['Ki']:.3f}" kd = f"{pid_values['Kd']:.3f}" self.arduino.write(f"p{kp}\n".encode()) time.sleep(0.1) self.arduino.write(f"i{ki}\n".encode()) time.sleep(0.1) self.arduino.write(f"d{kd}\n".encode()) messagebox.showinfo("Info", f"PID-Parameter gesendet: Kp={kp}, Ki={ki}, Kd={kd}") else: messagebox.showerror("Fehler", "Ausgewählte PID Konfiguration nicht gefunden.") def on_closing(self): self.running = False if self.arduino: self.arduino.close() if self.ser: self.ser.close() self.root.quit() self.root.destroy() if __name__ == "__main__": root = customtkinter.CTk() app = ArduinoGUI(root) root.protocol("WM_DELETE_WINDOW", app.on_closing) root.mainloop()