#!/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.10 GUI" TOOL_INFO = { "name": "Time Base DAC - HP8648A/B/C/D", "author": "Robert / YO4HFU, 2025", "version": VERSION } LOG_FILE = "Time_Base_log.txt" DELAY = 0.05 DEFAULT_GPIB_NUMBER = 19 INITIAL_COARSE = 255 INITIAL_FINE = 128 # -------------------------------------------------------- # Utility # -------------------------------------------------------- def clear_log_file(): with open(LOG_FILE, "w", encoding="utf-8") as f: f.write(f"{TOOL_INFO['name']}\n") f.write(f"Version: {TOOL_INFO['version']}\n") f.write(f"Author: {TOOL_INFO['author']}\n") f.write("="*40 + "\n") 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 HP8648Controller: def __init__(self, log_callback=None, gpib_number=DEFAULT_GPIB_NUMBER): self.inst = None self.rm = None self.gpib_number = gpib_number self.log_callback = log_callback # callback to GUI 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"Instrument connected: {idn}") return idn def send(self, cmd: str): self.inst.write(cmd) self.log(f"-> {cmd}") time.sleep(DELAY) def query(self, cmd: str) -> str: self.log(f"-> {cmd}") reply = self.inst.query(cmd).strip() self.log(f"<- {reply}") time.sleep(DELAY) return reply def setup_instr(self): self.send("*RST") self.send("SERV:PRODUCTION:PUP") time.sleep(10) self.query("*IDN?") self.send("FREQ 1000.0000000000 MHZ") self.send("POWER:AMPL 0.0000000000") self.send("AM:STATE 0") self.send("FM:STATE 0") self.send("PM:STATE 0") self.send("OUTPUT 1") self.send('DIAG:LATCH:SELECT "refs_tbase_select"') self.send("DIAG:LATCH:VAL #H00") self.send('DIAG:LATCH:SELECT "refs_DAC_coarse"') self.send(f"DIAG:LATCH:VAL #H{INITIAL_COARSE:02X}") self.send('DIAG:LATCH:SELECT "refs_DAC_fine"') self.send(f"DIAG:LATCH:VAL #H{INITIAL_FINE:02X}") def apply_dac(self, coarse, fine): self.send('DIAG:LATCH:SELECT "refs_DAC_coarse"') self.send(f"DIAG:LATCH:VAL #H{coarse:02X}") self.send('DIAG:LATCH:SELECT "refs_DAC_fine"') self.send(f"DIAG:LATCH:VAL #H{fine:02X}") self.log(f"Applied DAC: coarse={coarse}, fine={fine}") def query_dac(self): self.send('DIAG:LATCH:SELECT "refs_DAC_coarse"') coarse = self.query("DIAG:LATCH:VAL?") self.send('DIAG:LATCH:SELECT "refs_DAC_fine"') fine = self.query("DIAG:LATCH:VAL?") return coarse, fine def store_calibration(self, coarse_val, fine_val): self.send("SERV:PRODUCTION:CAL:BEGIN") self.send(f"SERV:PRODUCTION:CAL timebase,0,{coarse_val}") self.send(f"SERV:PRODUCTION:CAL timebase,1,{fine_val}") self.send("SERV:PRODUCTION:CAL:END") self.send("SERV:PRODUCTION:CAL:STORE timebase") self.send("SERV:PRODUCTION:PUP") time.sleep(10) # -------------------------------------------------------- # Tkinter GUI # -------------------------------------------------------- class HP8648GUI: def __init__(self, root): self.root = root self.root.title(f"{TOOL_INFO['name']} - {TOOL_INFO['version']}") self.gpib_number = tk.IntVar(value=DEFAULT_GPIB_NUMBER) self.controller = HP8648Controller(log_callback=self.log, gpib_number=self.gpib_number.get()) self.coarse = tk.IntVar(value=INITIAL_COARSE) self.fine = tk.IntVar(value=INITIAL_FINE) clear_log_file() # clear log at startup self.build_ui() self.center_window() def center_window(self): self.root.update_idletasks() width = self.root.winfo_reqwidth() + 20 height = self.root.winfo_reqheight() + 20 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 show_popup(self, func, *args, **kwargs): self.root.lift() self.root.attributes('-topmost', True) self.root.after(0, lambda: self.root.attributes('-topmost', False)) func(*args, **kwargs) def build_ui(self): style = ttk.Style() style.configure("TNotebook.Tab", font=("Arial", 10, "bold"), padding=[8, 4]) style.configure("TButton", padding=5, font=("Helvetica", 10, "bold"), 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") main_frame = tk.Frame(notebook, bg="SystemButtonFace", padx=5, pady=5) info_frame = tk.Frame(notebook, bg="#FFFFE0", padx=5, pady=5) 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(9, weight=1) # ---------- Title + PDF Button ---------- 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_INFO['name'], font=("Helvetica", 12, "bold"), background="SystemButtonFace") title_label.pack(side="left", pady=5) pdf_button = ttk.Button(title_frame, text="PDF DOC", width=10, command=self.open_pdf) pdf_button.pack(side="right", padx=5) # -------------------------------------------------- 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_INFO['name']}\n", "bold") info_text.insert(tk.END, f"Version: {TOOL_INFO['version']}\n", "bold") info_text.insert(tk.END, f"Author: {TOOL_INFO['author']}\n", "bold") info_text.insert(tk.END, "="*30 + "\n") info_text.insert(tk.END, """Manual Adjustment Procedure for Time Base: - Warm-up for at least 45 minutes. - Use a frequency counter capable of 1 GHz, 1 Hz resolution. - Adjust GPIB address if needed and CONNECT. - QUERY current DAC values before changes. Note the old values. - Press SETUP INSTRUMENT to configure the instrument for Time Base DAC adjustment. - Adjust Coarse/Fine DACs then press APPLY DAC, adjust until 1000.000000 MHz +/- 1Hz is obtained. - STORE EEPROM to save calibration. """) info_text.config(state="disabled") info_text.pack(fill="both", expand=True) label_font = ("Helvetica", 10, "bold") ttk.Label(main_frame, text="GPIB #:", font=label_font, background="SystemButtonFace").grid(row=1, column=0, sticky="e", pady=2) gpib_spin = ttk.Spinbox(main_frame, from_=0, to=30, textvariable=self.gpib_number, width=8, font=label_font) gpib_spin.grid(row=1, column=1, sticky="w", pady=2) ttk.Button(main_frame, text="CONNECT", command=self.connect).grid(row=2, column=0, sticky="ew", pady=2, padx=2) ttk.Button(main_frame, text="QUERY DAC", command=self.query_dac).grid(row=2, column=1, sticky="ew", pady=2, padx=2) ttk.Button(main_frame, text="SETUP INSTRUMENT", command=self.setup_instr).grid(row=3, column=0, columnspan=2, sticky="ew", pady=2, padx=2) ttk.Label(main_frame, text="Coarse DAC:", font=label_font, background="SystemButtonFace").grid(row=4, column=0, sticky="e", pady=1) ttk.Spinbox(main_frame, from_=0, to=255, textvariable=self.coarse, width=8, font=label_font).grid(row=4, column=1, sticky="w", pady=1) ttk.Label(main_frame, text="Fine DAC:", font=label_font, background="SystemButtonFace").grid(row=5, column=0, sticky="e", pady=1) ttk.Spinbox(main_frame, from_=0, to=255, textvariable=self.fine, width=8, font=label_font).grid(row=5, column=1, sticky="w", pady=1) ttk.Button(main_frame, text="APPLY DAC", command=self.apply_dac).grid(row=6, column=0, columnspan=2, sticky="ew", pady=2, padx=2) ttk.Button(main_frame, text="STORE EEPROM", command=self.store).grid(row=7, column=0, columnspan=2, sticky="ew", pady=2, padx=2) self.log_box = tk.Text(main_frame, width=60, height=12, bg="#F5F5F5", fg="#000000", font=("Arial", 10)) self.log_box.grid(row=8, column=0, columnspan=2, pady=5, sticky="nsew") def connect(self): try: self.controller.gpib_number = self.gpib_number.get() idn = self.controller.connect() self.show_popup(messagebox.showinfo, "Connected", f"Instrument Info:\n{idn}", parent=self.root) except Exception as e: self.show_popup(messagebox.showerror, "Error", f"Cannot connect: {e}", parent=self.root) def setup_instr(self): def worker(): self.log("Setting up instrument...") try: self.controller.setup_instr() self.log("Instrument ready.") self.show_popup(messagebox.showinfo, "Setup Complete", "Instrument ready for Time Base DAC adjustment.", parent=self.root) except Exception as e: self.show_popup(messagebox.showerror, "Setup Error", str(e), parent=self.root) threading.Thread(target=worker).start() def apply_dac(self): coarse = self.coarse.get() fine = self.fine.get() self.controller.apply_dac(coarse, fine) self.show_popup(messagebox.showinfo, "DAC Applied", f"Applied DAC values:\nCoarse: {coarse}\nFine: {fine}", parent=self.root) def query_dac(self): c, f = self.controller.query_dac() self.show_popup(messagebox.showinfo, "Query Response", f"Coarse DAC: {c}\nFine DAC: {f}", parent=self.root) def store(self): def worker(): c, f = self.controller.query_dac() self.controller.store_calibration(c, f) self.log("Calibration stored and instrument rebooted.") self.show_popup(messagebox.showinfo, "Done", "Calibration stored.", parent=self.root) threading.Thread(target=worker).start() def open_pdf(self): pdf_files = glob.glob(os.path.join(os.getcwd(), "Time_Base_DAC*.pdf")) if pdf_files: webbrowser.open_new(pdf_files[0]) else: self.show_popup(messagebox.showerror, "Error", "No Time_Base_DAC PDF found in folder", parent=self.root) def log(self, text): self.log_box.insert(tk.END, text + "\n") self.log_box.see(tk.END) self.root.update_idletasks() log_to_file(text) # ------------------ Main ------------------ if __name__ == "__main__": root = tk.Tk() app = HP8648GUI(root) root.mainloop()