#!/usr/bin/env python3 # -*- coding: utf-8 -*- import pyvisa import time import threading import tkinter as tk from tkinter import ttk, messagebox import webbrowser import os import glob VERSION = "1.1 GUI" TOOL_NAME = "AM Level - HP8648B/C/D" TOOL_VERSION = f"{TOOL_NAME} v{VERSION}" AUTHOR = "Robert / YO4HFU, 2025" LOG_FILE = "AM_Level_log.txt" DELAY = 0.05 DEFAULT_GPIB_NUMBER = 19 # ------------------ Utility ------------------ def log_to_file(msg: str): with open(LOG_FILE, "a", encoding="utf-8") as f: f.write(f"{msg}\n") # ------------------ Backend Instrument Control ------------------ class HP8648AMController: def __init__(self, log_callback=None, gpib_number=DEFAULT_GPIB_NUMBER): self.inst = None self.rm = None self.log_callback = log_callback self.gpib_number = gpib_number def log(self, msg): if self.log_callback: self.log_callback(msg) else: log_to_file(msg) def connect(self): gpib_address = f"GPIB0::{self.gpib_number}::INSTR" self.rm = pyvisa.ResourceManager() self.inst = self.rm.open_resource(gpib_address) self.inst.write_termination = '\n' self.inst.read_termination = '\n' self.inst.timeout = 5000 idn = self.query("*IDN?") self.log(f"Connected: {idn}") return idn def send(self, cmd: str): """Send a command to the instrument and log immediately.""" self.inst.write(cmd) self.log(f"-> {cmd}") time.sleep(DELAY) def query(self, cmd: str) -> str: """Query the instrument and log command/reply in real time.""" self.log(f"-> {cmd}") reply = self.inst.query(cmd).strip() self.log(f"<- {reply}") time.sleep(DELAY) return reply def setup_instrument(self): self.send("SERV:PRODUCTION:PUP") time.sleep(10) self.send("FREQ 750.0000000000 MHZ") self.send("POWER:ATT:AUTO 1") self.send("AM:STATE 0") self.send("FM:STATE 0") self.send("PM:STATE 0") self.send("OUTPUT 1") self.send("POWER:AMPL 0.0000000000") self.send('DIAG:LATCH:SELECT "out_service_mux"') self.send("DIAG:LATCH:VAL #H03") # ------------------ GUI ------------------ class HP8648AMGUI: def __init__(self, root): self.root = root self.root.title(TOOL_VERSION) self.gpib_number = tk.IntVar(value=DEFAULT_GPIB_NUMBER) self.controller = HP8648AMController(log_callback=self.log, gpib_number=self.gpib_number.get()) self.build_ui() self.insert_log_header_to_file() self.center_window() def insert_log_header_to_file(self): # Clear log file at every program start with open(LOG_FILE, "w", encoding="utf-8"): pass header = f"{TOOL_NAME}\nVersion: {VERSION}\nAuthor: {AUTHOR}\n{'='*60}\n" log_to_file(header) def center_window(self): self.root.update_idletasks() width = self.root.winfo_reqwidth() height = self.root.winfo_reqheight() screen_width = self.root.winfo_screenwidth() screen_height = self.root.winfo_screenheight() x = (screen_width // 2) - (width // 2) y = (screen_height // 2) - (height // 2) self.root.geometry(f"{width}x{height}+{x}+{y}") self.root.minsize(width, height) def build_ui(self): style = ttk.Style() style.configure("TNotebook.Tab", font=("Arial", 10, "bold"), padding=[8, 4]) style.configure("TButton", font=("Arial", 10, "bold"), padding=[5, 3], relief="raised", borderwidth=2) style.map("TButton", background=[("active", "#32CD32")], foreground=[("active", "#000000")]) notebook = ttk.Notebook(self.root) notebook.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) main_frame = tk.Frame(notebook, bg="SystemButtonFace", padx=10, pady=10) info_frame = tk.Frame(notebook, bg="#FFFFE0", padx=10, pady=10) notebook.add(main_frame, text="Main") notebook.add(info_frame, text="Info") main_frame.grid_columnconfigure(0, weight=1) main_frame.grid_columnconfigure(1, weight=1) main_frame.grid_rowconfigure(5, weight=1) info_text = tk.Text(info_frame, wrap="word", font=("Arial", 13), bg="#FFFFE0", fg="#333333", relief="flat") info_text.tag_configure("bold", font=("Arial", 12, "bold")) info_text.insert(tk.END, f"{TOOL_NAME}\n", "bold") info_text.insert(tk.END, f"Version: {VERSION}\n", "bold") info_text.insert(tk.END, f"Author: {AUTHOR}\n", "bold") info_text.insert(tk.END, "="*60 + "\n") info_text.insert(tk.END, """Procedure for instruments with AM Level potentiometer on A6 Output Module: - Warm-up for at least 45 minutes. - Locate AM Level pot in 3rd access hole from rear of A6 Output Module. - Select GPIB address, default #19, and CONNECT. - Remove motherboard jumper J32. - Connect DVM to J31 pin 2. - Apply -4.0000V +/-0.0025V to J31 pin 1. - Press SETUP INSTRUMENT to configure the instrument for AM Level adjustment. - Press AM LEVEL ADJUST and follow instructions. - Adjust potentiometer until DVM reads 0.000V +/-0.002V. - Power OFF and reinstall J32 jumper when done. """) info_text.config(state="disabled") info_text.pack(fill="both", expand=True) title_frame = tk.Frame(main_frame, bg="SystemButtonFace") title_frame.grid(row=0, column=0, columnspan=2, sticky="nsew") title_label = ttk.Label(title_frame, text=TOOL_NAME, font=("Helvetica", 12, "bold")) title_label.pack(side="top", pady=6) pdf_button = ttk.Button(title_frame, text="PDF DOC", width=10, command=self.open_pdf) pdf_button.pack(side="right", padx=5) ttk.Label(main_frame, text="GPIB Address #:", font=("Arial", 10, "bold")).grid(row=1, column=0, sticky="e", pady=4) ttk.Spinbox(main_frame, from_=0, to=30, textvariable=self.gpib_number, width=8, font=("Arial", 10, "bold")).grid(row=1, column=1, sticky="w", pady=4) ttk.Button(main_frame, text="CONNECT", command=self.connect).grid(row=2, column=0, sticky="ew", pady=3, padx=2) ttk.Button(main_frame, text="SETUP INSTRUMENT", command=self.setup_instr).grid(row=2, column=1, sticky="ew", pady=3, padx=2) ttk.Button(main_frame, text="AM LEVEL ADJUST", command=self.am_adjust).grid(row=3, column=0, columnspan=2, sticky="ew", pady=3, padx=2) self.log_box = tk.Text(main_frame, width=70, height=20, bg="#F5F5F5", fg="#000000", font=("Arial", 10)) self.log_box.grid(row=5, column=0, columnspan=2, pady=8, sticky="nsew") # ------------------ Button Commands ------------------ def log(self, text): self.log_box.insert(tk.END, text + "\n") self.log_box.see(tk.END) log_to_file(text) def connect(self): try: self.controller.gpib_number = self.gpib_number.get() idn = self.controller.connect() messagebox.showinfo("Connected", f"Instrument Info:\n{idn}") except Exception as e: messagebox.showerror("Error", f"Cannot connect: {e}") def setup_instr(self): def worker(): self.log("Setting up instrument for AM Level adjustment...") try: self.controller.setup_instrument() self.log("Instrument setup completed.") messagebox.showinfo("Setup Complete", "Instrument configured for AM Level Adjustment.") except Exception as e: self.log(str(e)) messagebox.showerror("Error", str(e)) threading.Thread(target=worker).start() def am_adjust(self): messagebox.showinfo("Remove Jumper", "Please REMOVE J32 jumper on motherboard before proceeding.") confirmed = messagebox.askyesno("Voltage Check", "Ensure external voltage -4.0000V +/-0.0025V applied to J31 pin 1.\n \n Press YES if correct.") if not confirmed: self.log("Aborted: Incorrect voltage confirmation.") messagebox.showwarning("Aborted", "Aborted due to voltage setup error.") return messagebox.showinfo("Adjust Potentiometer", "Locate AM Level potentiometer on A6 Output Module. 3rd potentiometer counting from the back of the A6 Output Module.\n \n" "Connect DVM to J31 pin 2 and adjust potentiometer until DVM reads 0.000 V +/-0.002 V.\n \n" "Press OK once done.") self.log("Rebooting instrument for finalization...") try: self.controller.send("SERV:PRODUCTION:PUP") time.sleep(10) self.log("Instrument reboot completed.") messagebox.showinfo("AM Level Adjustment", "Adjustment completed successfully.\nPower OFF and reinstall J32 jumper.") except Exception as e: self.log(str(e)) messagebox.showerror("Error", str(e)) def open_pdf(self): pdf_files = glob.glob(os.path.join(os.getcwd(), "AM_Level*.pdf")) if pdf_files: webbrowser.open_new(pdf_files[0]) else: messagebox.showerror("Error", "No AM_Level PDF found in folder") # ------------------ Main ------------------ if __name__ == "__main__": root = tk.Tk() app = HP8648AMGUI(root) root.mainloop()