spm6103-viewer/spm6103_viewer.py

209 lines
6.3 KiB
Python

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():
return send_command('*IDN?')
def get_multimeter_data():
rawdata = send_command(f'CONFigure:ALL?')
return rawdata
def get_powersupply_data():
rawdata = send_command(f'MEASure:ALL:INFO?')
return rawdata
def set_res_mode():
rawdata = send_command(f'[SENSe:]FUNCtion:RESistance')
time.sleep(0.1)
rawdata = send_command(f'[SENSe:]RESistance:RANGe:AUTO ON')
return rawdata
def set_res200_mode():
rawdata = send_command(f'[SENSe:]FUNCtion:RESistance')
time.sleep(0.1)
rawdata = send_command(f'[SENSe:]RESistance:RANGe 200')
return rawdata
def set_cont_mode():
rawdata = send_command(f'[SENSe:]FUNCtion:CONTinuity')
return rawdata
def set_diod_mode():
rawdata = send_command(f'[SENSe:]FUNCtion:DIODe')
return rawdata
def set_voltdc_mode():
rawdata = send_command(f'[SENSe:]FUNCtion:VOLTage:DC')
return rawdata
def set_voltac_mode():
rawdata = send_command(f'[SENSe:]FUNCtion:VOLTage:AC')
return rawdata
def set_cap_mode():
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()