Compare commits
10 Commits
58bb75158a
...
22c5b33d5e
| Author | SHA1 | Date | |
|---|---|---|---|
| 22c5b33d5e | |||
| 6695e63c9d | |||
| 2ffb2acc98 | |||
| 535435fb72 | |||
| 9fa9f2b361 | |||
| 1bc6db3f37 | |||
| 7422a3b4c3 | |||
| 7bae89a9d2 | |||
| 6c42d931b6 | |||
| ad01149daa |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
settings.json
|
||||||
@ -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"
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user