import tkinter as tk from tkinter.font import Font import serial import os import json import threading import time class SerialManager: def __init__(self, port="/dev/ttyUSB0", baud_rate=115200, reconnect_interval=2): self.port_name = port self.baud_rate = baud_rate self.reconnect_interval = reconnect_interval self.ser = None self.lock = threading.Lock() self.running = True self.thread = threading.Thread(target=self._monitor_serial) self.thread.daemon = True self.thread.start() def _monitor_serial(self): while self.running: with self.lock: if self.ser is None or not self.ser.is_open: try: self.ser = serial.Serial(self.port_name, self.baud_rate, timeout=1) print(f"[INFO] Connected to {self.port_name}") except serial.SerialException as e: print(f"[WARN] Serial port error: {e}") self.ser = None time.sleep(self.reconnect_interval) def send_command(self, command): with self.lock: if self.ser and self.ser.is_open: try: self.ser.write((command + '\n').encode('utf-8')) response = self.ser.readline().decode('utf-8').strip() return response except Exception as e: print(f"[ERROR] Failed to send/receive: {e}") self.ser = None return "Serial not connected" def stop(self): self.running = False self.thread.join() if self.ser and self.ser.is_open: self.ser.close() # Set DPI scale for Wayland os.environ['GDK_SCALE'] = '2' # Load saved window geometry if exists def load_window_geometry(): try: with open("settings.json", "r") as f: settings = json.load(f) return settings.get("geometry", "1080x200+100+100") # Default geometry except FileNotFoundError: return "1080x200+100+100" # Default geometry if no settings file # Save the window geometry when closing the app def save_window_geometry(): geometry = app.geometry() settings = {"geometry": geometry} with open("settings.json", "w") as f: json.dump(settings, f) # Serial configuration ser = SerialManager(port='/dev/ttyUSB0') def send_command(cmd): return ser.send_command(cmd) def get_id(): if not ser: return "TEST" return send_command('*IDN?') def get_multimeter_data(): if not ser: return "TEST,+00.1234TEST,TEST,TEST" rawdata = send_command(f'CONFigure:ALL?') return rawdata def get_powersupply_data(): if not ser: return "0.1234,0.1234,0.1234" rawdata = send_command(f'MEASure:ALL:INFO?') return rawdata def set_res_mode(): if not ser: return "TEST" rawdata = send_command(f'[SENSe:]FUNCtion:RESistance') rawdata = send_command(f'[SENSe:]RESistance:RANGe:AUTO ON') return rawdata def set_res200_mode(): if not ser: return "TEST" rawdata = send_command(f'[SENSe:]FUNCtion:RESistance') rawdata = send_command(f'[SENSe:]RESistance:RANGe 200') return rawdata def set_cont_mode(): if not ser: return "TEST" rawdata = send_command(f'[SENSe:]FUNCtion:CONTinuity') return rawdata def set_diod_mode(): if not ser: return "TEST" rawdata = send_command(f'[SENSe:]FUNCtion:DIODe') return rawdata def set_voltdc_mode(): if not ser: return "TEST" rawdata = send_command(f'[SENSe:]FUNCtion:VOLTage:DC') return rawdata def set_voltac_mode(): if not ser: return "TEST" rawdata = send_command(f'[SENSe:]FUNCtion:VOLTage:AC') return rawdata def set_cap_mode(): if not ser: return "TEST" rawdata = send_command(f'[SENSe:]FUNCtion:CAPacitance') return rawdata # --- GUI Setup --- def fetch_device_info(): # Fetch the device ID and Channel data multimeter_data = get_multimeter_data() if "," in multimeter_data: if len(multimeter_data.split(",")) >= 4: # Update the labels multimeter_type_var.set(multimeter_data.split(",")[0]) multimeter_data_var.set(multimeter_data.split(",")[1].replace("+","").replace("Ohm",'\u2126')) multimeter_range_var.set(multimeter_data.split(",")[3].replace("Ohm",'\u2126')) # Schedule this function to be called again after 100ms app.after(100, fetch_device_info) app = tk.Tk() app.title("OWON SPM6103") # Load and apply the saved geometry (size + position) app.geometry(load_window_geometry()) app.configure(bg="black") # For cleanup on exit app.protocol("WM_DELETE_WINDOW", lambda: [save_window_geometry(), ser.stop(), app.quit()]) multimeter_type_var = tk.StringVar() multimeter_data_var = tk.StringVar() multimeter_range_var = tk.StringVar() # Create dynamic font objects fontSmall = Font(size=6) fontMedium = Font(size=14) fontBig = Font(size=24) resize_after_id = None def apply_font_resize(): width = app.winfo_width() fontSmall.configure(size=max(6, int(width * 0.01))) fontMedium.configure(size=max(14, int(width * 0.02))) fontBig.configure(size=max(24, int(width * 0.09))) def resize_loop(): apply_font_resize() app.after(200, resize_loop) resize_loop() # start it once # Configure grid to be fully responsive app.rowconfigure(0, weight=20) # Top - big app.rowconfigure(1, weight=1) # Bottom - small app.columnconfigure(0, weight=1, uniform="equal", minsize=100) for i in range(7): app.columnconfigure(i+1, weight=1, uniform="equal") # Labels # Type and Range tk.Label(app, textvariable=multimeter_type_var, font=fontMedium, bg="black", fg="lightgrey") \ .grid(column=0, columnspan=1, row=1, sticky="nsw", padx=0, pady=0) tk.Label(app, textvariable=multimeter_range_var, font=fontMedium, bg="black", fg="lightgrey") \ .grid(column=0, columnspan=1, row=0, sticky="nw", padx=0, pady=0) # Data tk.Label(app, textvariable=multimeter_data_var, font=fontBig, bg="black", fg="lightgrey") \ .grid(column=1, columnspan=6, row=0, rowspan=1, sticky="nsew", padx=0, pady=10) # Buttons buttons = [ ("RES", set_res_mode), ("RES200", set_res200_mode), ("CONT", set_cont_mode), ("VOLT:DC", set_voltdc_mode), ("VOLT:AC", set_voltac_mode), ("DIOD", set_diod_mode), ("CAP", set_cap_mode), ] for i, (label, cmd) in enumerate(buttons, start=1): tk.Button(app, text=label, command=cmd, font=fontSmall, bg="black", fg="#444444", highlightbackground="black", borderwidth=0) \ .grid(column=i, row=1, sticky="nsew", padx=0, pady=0) # Initial fetch to start the auto-update process fetch_device_info() # Start GUI app.mainloop()