Compare commits

...

10 Commits

3 changed files with 211 additions and 42 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
settings.json

View File

@ -5,3 +5,11 @@ SCPI viewer for OWON SPM6103 Powersupply/Multimeter
## SCPI command ## SCPI command
- [ ] [OWON SPM SCPI manual](SPM_Series_programming_manual.pdf) - [ ] [OWON SPM SCPI manual](SPM_Series_programming_manual.pdf)
## Autostart
Add following to autostart app (Ubuntu):
``` bash
bash -c "sleep 3 && python3 <path to script>/spm6103_viewer.py"
```

View File

@ -1,84 +1,244 @@
import tkinter as tk import tkinter as tk
from tkinter import messagebox
from tkinter.font import Font from tkinter.font import Font
import serial 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 # Serial configuration
port = '/dev/ttyUSB0' # Adjust to your specific port ser = SerialManager(port="/dev/ttyUSB0")
baud_rate = 115200 # Adjust to your device's baud rate
try:
ser = serial.Serial(port, baud_rate, timeout=1)
except serial.SerialException as e:
ser = None
print(f"Serial port error: {e}")
def send_command(command): def send_command(cmd):
if not ser: return ser.send_command(cmd)
return "Serial port not available"
ser.write((command + '\n').encode('utf-8'))
response = ser.readline().decode('utf-8').strip()
return response
def get_id(): def get_id():
return send_command('*IDN?') return send_command("*IDN?")
def get_multimeter_data(): def get_multimeter_data():
rawdata = send_command(f'CONFigure:ALL?') rawdata = send_command(f"CONFigure:ALL?")
return rawdata return rawdata
def get_powersupply_data(): def get_powersupply_data():
rawdata = send_command(f'MEASure:ALL:INFO?') rawdata = send_command(f"MEASure:ALL:INFO?")
return rawdata return rawdata
def set_res_mode(): def set_res_mode():
rawdata = send_command(f'[SENSe:]FUNCtion:RESistance') rawdata = send_command(f"[SENSe:]FUNCtion:RESistance")
time.sleep(0.1)
rawdata = send_command(f"[SENSe:]RESistance:RANGe:AUTO ON")
return rawdata return rawdata
def set_cont_mode():
rawdata = send_command(f'[SENSe:]FUNCtion:CONTinuity') 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 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 --- # --- GUI Setup ---
def fetch_device_info(): def fetch_device_info():
# Fetch the device ID and Channel data # Fetch the device ID and Channel data
device_id = get_id()
multimeter_data = get_multimeter_data() multimeter_data = get_multimeter_data()
# Update the labels if "," in multimeter_data:
multimeter_type_var.set(multimeter_data.split(",")[0]) if len(multimeter_data.split(",")) >= 4:
multimeter_data_var.set(multimeter_data.split(",")[1]) # Update the labels
multimeter_range_var.set(multimeter_data.split(",")[3]) 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 # Schedule this function to be called again after 100ms
app.after(100, fetch_device_info) app.after(100, fetch_device_info)
app = tk.Tk() app = tk.Tk()
app.title("OWON SPM6103") app.title("SPM6103")
app.geometry("1080x200") # Load and apply the saved geometry (size + position)
app.geometry(load_window_geometry())
app.configure(bg="black") 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_type_var = tk.StringVar()
multimeter_data_var = tk.StringVar() multimeter_data_var = tk.StringVar()
multimeter_range_var = tk.StringVar() multimeter_range_var = tk.StringVar()
# Create a font object # Create dynamic font objects
fontSmall = Font(size=10) fontSmall = Font(size=6)
fontMedium = Font(size=20) fontMedium = Font(size=14)
fontBig = Font(size=80) fontBig = Font(size=24)
app.rowconfigure(0, weight=1) resize_after_id = None
app.rowconfigure(1, weight=1)
app.rowconfigure(2, weight=1)
app.columnconfigure(0, weight=1) def apply_font_resize():
app.columnconfigure(1, weight=2) 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 # Labels
tk.Label(app, textvariable=multimeter_type_var, font=fontMedium, bg="black", fg="lightgrey").grid(column=0, row=0, sticky=tk.W) # Type and Range
tk.Label(app, textvariable=multimeter_range_var, font=fontMedium, bg="black", fg="lightgrey").grid(column=1, row=0, sticky=tk.E) tk.Label(
tk.Label(app, textvariable=multimeter_data_var, font=fontBig, bg="black", fg="lightgrey").grid(column=0, columnspan=2, row=1, sticky=tk.W) app, textvariable=multimeter_type_var, font=fontMedium, bg="black", fg="lightgrey"
tk.Button(app, text="RES", command=set_res_mode, font=fontSmall, bg="black", fg="lightgrey", border=False).grid(column=0, row=2, sticky=tk.W) ).grid(column=0, columnspan=1, row=1, sticky="nsw", padx=0, pady=0)
tk.Button(app, text="CONT", command=set_cont_mode, font=fontSmall, bg="black", fg="lightgrey", border=False).grid(column=1, row=2, sticky=tk.W)
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="#afd787"
).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 # Initial fetch to start the auto-update process
fetch_device_info() fetch_device_info()