|
| 1 | + # ------------------------------------------------------------------------------- |
| 2 | +# Name: Passive R-T Logger for Keithley 2400 |
| 3 | +# Purpose: Provide a GUI for passively logging R-T data using a K2400 |
| 4 | +# and LS350. This version does not control temperature. |
| 5 | +# Author: Prathamesh Deshmukh (Adapted from 6517B & 2400 scripts) |
| 6 | +# Created: 05/10/2025 |
| 7 | +# Version: 1.0 |
| 8 | +# ------------------------------------------------------------------------------- |
| 9 | + |
| 10 | +# --- GUI and Plotting Packages --- |
| 11 | +import tkinter as tk |
| 12 | +from tkinter import ttk, filedialog, messagebox, scrolledtext, Canvas |
| 13 | +import os |
| 14 | +import time |
| 15 | +import traceback |
| 16 | +from datetime import datetime; import csv |
| 17 | +from matplotlib.figure import Figure |
| 18 | +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg |
| 19 | +import matplotlib as mpl |
| 20 | + |
| 21 | +try: |
| 22 | + from PIL import Image, ImageTk |
| 23 | + PIL_AVAILABLE = True |
| 24 | +except ImportError: |
| 25 | + PIL_AVAILABLE = False |
| 26 | + |
| 27 | +try: |
| 28 | + import pyvisa |
| 29 | + from pymeasure.instruments.keithley import Keithley2400 |
| 30 | + PYMEASURE_AVAILABLE = True |
| 31 | +except ImportError: |
| 32 | + pyvisa, Keithley2400 = None, None |
| 33 | + PYMEASURE_AVAILABLE = False |
| 34 | + |
| 35 | +# ------------------------------------------------------------------------------- |
| 36 | +# --- BACKEND INSTRUMENT CONTROL --- |
| 37 | +# ------------------------------------------------------------------------------- |
| 38 | +class RT_Backend_Passive: |
| 39 | + """ Manages communication for passive monitoring. """ |
| 40 | + def __init__(self): |
| 41 | + self.k2400, self.lakeshore = None, None |
| 42 | + if pyvisa: |
| 43 | + try: self.rm = pyvisa.ResourceManager() |
| 44 | + except Exception as e: print(f"Could not initialize VISA: {e}"); self.rm = None |
| 45 | + |
| 46 | + def connect(self, k2400_visa, ls_visa): |
| 47 | + if not self.rm: raise ConnectionError("PyVISA is not available.") |
| 48 | + if not PYMEASURE_AVAILABLE: raise ImportError("Pymeasure is not available.") |
| 49 | + self.k2400 = Keithley2400(k2400_visa); print(f" K2400 Connected: {self.k2400.id}") |
| 50 | + self.lakeshore = self.rm.open_resource(ls_visa); print(f" Lakeshore Connected: {self.lakeshore.query('*IDN?').strip()}") |
| 51 | + |
| 52 | + def configure_instruments(self, current_ma, compliance_v): |
| 53 | + # Lakeshore setup for passive monitoring |
| 54 | + self.lakeshore.write('*RST'); time.sleep(0.5); self.lakeshore.write('*CLS') |
| 55 | + self.lakeshore.write('RANGE 1,0') # Ensure heater is OFF |
| 56 | + |
| 57 | + # Keithley 2400 setup |
| 58 | + self.k2400.reset(); self.k2400.use_front_terminals() |
| 59 | + self.k2400.apply_current() |
| 60 | + self.k2400.source_current_range = abs(current_ma * 1e-3) * 1.05 |
| 61 | + self.k2400.compliance_voltage = compliance_v |
| 62 | + self.k2400.source_current = current_ma * 1e-3 |
| 63 | + self.k2400.measure_voltage() |
| 64 | + self.k2400.enable_source() |
| 65 | + |
| 66 | + def get_measurement(self): |
| 67 | + voltage = self.k2400.voltage |
| 68 | + temperature = float(self.lakeshore.query('KRDG? A').strip()) |
| 69 | + return temperature, voltage |
| 70 | + |
| 71 | + def shutdown(self): |
| 72 | + if self.k2400: |
| 73 | + try: self.k2400.shutdown() |
| 74 | + except: pass |
| 75 | + if self.lakeshore: |
| 76 | + try: self.lakeshore.write("RANGE 1,0"); self.lakeshore.close() |
| 77 | + except: pass |
| 78 | + print(" Instruments shut down and disconnected.") |
| 79 | + |
| 80 | +# ------------------------------------------------------------------------------- |
| 81 | +# --- FRONT END (GUI) --- |
| 82 | +# ------------------------------------------------------------------------------- |
| 83 | +class RT_GUI_Passive: |
| 84 | + PROGRAM_VERSION = "1.0" |
| 85 | + CLR_BG = '#2B3D4F'; CLR_HEADER = '#3A506B'; CLR_FG = '#EDF2F4' |
| 86 | + CLR_FRAME_BG = '#3A506B'; CLR_INPUT_BG = '#4C566A' |
| 87 | + CLR_ACCENT_GREEN, CLR_ACCENT_RED, CLR_ACCENT_BLUE = '#A7C957', '#E74C3C', '#8D99AE' |
| 88 | + CLR_ACCENT_GOLD = '#FFC107'; CLR_CONSOLE_BG = '#1E2B38' |
| 89 | + FONT_BASE = ('Segoe UI', 11); FONT_TITLE = ('Segoe UI', 13, 'bold') |
| 90 | + |
| 91 | + def __init__(self, root): |
| 92 | + self.root = root; self.root.title("Passive R-T Logger (K2400 + LS350)") |
| 93 | + self.root.geometry("1600x950"); self.root.minsize(1400, 800); self.root.configure(bg=self.CLR_BG) |
| 94 | + self.is_running = False; self.logo_image = None |
| 95 | + self.backend = RT_Backend_Passive(); self.data_storage = {'temperature': [], 'voltage': [], 'resistance': []} |
| 96 | + self.setup_styles(); self.create_widgets(); self.root.protocol("WM_DELETE_WINDOW", self._on_closing) |
| 97 | + |
| 98 | + def setup_styles(self): |
| 99 | + style = ttk.Style(self.root); style.theme_use('clam') |
| 100 | + style.configure('.', background=self.CLR_BG, foreground=self.CLR_FG, font=self.FONT_BASE) |
| 101 | + style.configure('TFrame', background=self.CLR_BG); style.configure('TPanedWindow', background=self.CLR_BG) |
| 102 | + style.configure('TLabel', background=self.CLR_FRAME_BG, foreground=self.CLR_FG) |
| 103 | + style.configure('Header.TLabel', background=self.CLR_HEADER) |
| 104 | + style.configure('TEntry', fieldbackground=self.CLR_INPUT_BG, foreground=self.CLR_FG, insertcolor=self.CLR_FG) |
| 105 | + style.configure('TButton', font=self.FONT_BASE, padding=(10, 9), foreground=self.CLR_ACCENT_GOLD, background=self.CLR_HEADER) |
| 106 | + style.map('TButton', background=[('active', self.CLR_ACCENT_GOLD), ('hover', self.CLR_ACCENT_GOLD)], foreground=[('active', self.CLR_BG), ('hover', self.CLR_BG)]) |
| 107 | + style.configure('Start.TButton', background=self.CLR_ACCENT_GREEN, foreground=self.CLR_BG) |
| 108 | + style.map('Start.TButton', background=[('active', '#8AB845'), ('hover', '#8AB845')]) |
| 109 | + style.configure('Stop.TButton', background=self.CLR_ACCENT_RED, foreground=self.CLR_FG) |
| 110 | + style.map('Stop.TButton', background=[('active', '#D63C2A'), ('hover', '#D63C2A')]) |
| 111 | + style.configure('TLabelframe', background=self.CLR_FRAME_BG, bordercolor=self.CLR_ACCENT_BLUE) |
| 112 | + style.configure('TLabelframe.Label', background=self.CLR_FRAME_BG, foreground=self.CLR_FG, font=self.FONT_TITLE) |
| 113 | + mpl.rcParams.update({'font.family': 'Segoe UI', 'font.size': 11, 'axes.titlesize': 15, 'axes.labelsize': 13}) |
| 114 | + |
| 115 | + def create_widgets(self): |
| 116 | + header = tk.Frame(self.root, bg=self.CLR_HEADER); header.pack(side='top', fill='x') |
| 117 | + ttk.Label(header, text=f"Passive R-T Logger (K2400) v{self.PROGRAM_VERSION}", style='Header.TLabel', font=self.FONT_TITLE).pack(side='left', padx=20, pady=10) |
| 118 | + main_pane = ttk.PanedWindow(self.root, orient='horizontal'); main_pane.pack(fill='both', expand=True, padx=10, pady=10) |
| 119 | + left_panel = self._create_left_panel(main_pane); main_pane.add(left_panel, weight=2) |
| 120 | + right_panel = self._create_right_panel(main_pane); main_pane.add(right_panel, weight=3) |
| 121 | + |
| 122 | + def _create_left_panel(self, parent): |
| 123 | + panel = ttk.Frame(parent, padding=5); panel.grid_columnconfigure(0, weight=1); panel.grid_rowconfigure(3, weight=1) |
| 124 | + self._create_info_panel(panel, 0) |
| 125 | + self._create_params_panel(panel, 1); self._create_control_panel(panel, 2); self._create_console_panel(panel, 3) |
| 126 | + return panel |
| 127 | + |
| 128 | + def _create_info_panel(self, parent, grid_row): |
| 129 | + frame = ttk.LabelFrame(parent, text='Information'); frame.grid(row=grid_row, column=0, sticky='new', pady=5) |
| 130 | + frame.grid_columnconfigure(1, weight=1) |
| 131 | + logo_canvas = Canvas(frame, width=80, height=80, bg=self.CLR_FRAME_BG, highlightthickness=0) |
| 132 | + logo_canvas.grid(row=0, column=0, rowspan=2, padx=10, pady=10) |
| 133 | + try: |
| 134 | + script_dir = os.path.dirname(os.path.abspath(__file__)) |
| 135 | + logo_path = os.path.join(script_dir, "..", "_assets", "LOGO", "UGC_DAE_CSR.jpeg") |
| 136 | + if PIL_AVAILABLE and os.path.exists(logo_path): |
| 137 | + img = Image.open(logo_path).resize((80, 80), Image.Resampling.LANCZOS) |
| 138 | + self.logo_image = ImageTk.PhotoImage(img) |
| 139 | + logo_canvas.create_image(40, 40, image=self.logo_image) |
| 140 | + except Exception as e: self.log(f"Warning: Could not load logo. {e}") |
| 141 | + info_text = ("Institute: UGC DAE CSR, Mumbai\nMeasurement: R vs. T (Passive)\nInstruments: K2400, LS350") |
| 142 | + ttk.Label(frame, text=info_text, justify='left').grid(row=0, column=1, rowspan=2, sticky='w', padx=5) |
| 143 | + |
| 144 | + def _create_right_panel(self, parent): |
| 145 | + panel = ttk.Frame(parent, padding=5) |
| 146 | + container = ttk.LabelFrame(panel, text='Live R-T Curve'); container.pack(fill='both', expand=True) |
| 147 | + self.figure = Figure(dpi=100, facecolor='white') |
| 148 | + self.ax_main = self.figure.add_subplot(111) |
| 149 | + self.line_main, = self.ax_main.plot([], [], color=self.CLR_ACCENT_RED, marker='o', markersize=4, linestyle='-') |
| 150 | + self.ax_main.set_title("Waiting for logging...", fontweight='bold'); self.ax_main.set_xlabel("Temperature (K)"); self.ax_main.set_ylabel("Resistance (Ω)") |
| 151 | + self.ax_main.grid(True, linestyle='--', alpha=0.6); self.figure.tight_layout() |
| 152 | + self.canvas = FigureCanvasTkAgg(self.figure, container); self.canvas.get_tk_widget().pack(fill='both', expand=True, padx=5, pady=5) |
| 153 | + return panel |
| 154 | + |
| 155 | + def _create_params_panel(self, parent, grid_row): |
| 156 | + container = ttk.Frame(parent); container.grid(row=grid_row, column=0, sticky='new', pady=5) |
| 157 | + container.grid_columnconfigure((0, 1), weight=1); self.entries = {} |
| 158 | + iv_frame = ttk.LabelFrame(container, text='Measurement Settings'); iv_frame.grid(row=0, column=0, columnspan=2, sticky='nsew') |
| 159 | + iv_frame.grid_columnconfigure(1, weight=1) |
| 160 | + self._create_entry(iv_frame, "Source Current (mA)", "1", 0); self._create_entry(iv_frame, "Compliance (V)", "10", 1) |
| 161 | + self._create_entry(iv_frame, "Logging Delay (s)", "1", 2) |
| 162 | + self.ls_cb = self._create_combobox(iv_frame, "Lakeshore VISA", 3) |
| 163 | + self.k2400_cb = self._create_combobox(iv_frame, "Keithley 2400 VISA", 4) |
| 164 | + |
| 165 | + def _create_control_panel(self, parent, grid_row): |
| 166 | + frame = ttk.LabelFrame(parent, text='File Control'); frame.grid(row=grid_row, column=0, sticky='new', pady=5) |
| 167 | + frame.grid_columnconfigure(0, weight=1) |
| 168 | + self._create_entry(frame, "Sample Name", "Sample_RT_Passive", 0) |
| 169 | + self._create_entry(frame, "Save Location", "", 1, browse=True) |
| 170 | + button_frame = ttk.Frame(frame); button_frame.grid(row=2, column=0, columnspan=4, sticky='ew', pady=5) |
| 171 | + button_frame.grid_columnconfigure((0,1,2), weight=1) |
| 172 | + self.start_button = ttk.Button(button_frame, text="Start", style='Start.TButton', command=self.start_experiment) |
| 173 | + self.start_button.grid(row=0, column=0, sticky='ew', padx=5) |
| 174 | + self.stop_button = ttk.Button(button_frame, text="Stop", style='Stop.TButton', state='disabled', command=self.stop_experiment) |
| 175 | + self.stop_button.grid(row=0, column=1, sticky='ew', padx=5) |
| 176 | + ttk.Button(button_frame, text="Scan", command=self._scan_for_visa).grid(row=0, column=2, sticky='ew', padx=5) |
| 177 | + |
| 178 | + def _create_console_panel(self, parent, grid_row): |
| 179 | + frame = ttk.LabelFrame(parent, text='Console'); frame.grid(row=grid_row, column=0, sticky='nsew', pady=5) |
| 180 | + self.console = scrolledtext.ScrolledText(frame, state='disabled', bg=self.CLR_CONSOLE_BG, fg=self.CLR_FG, font=('Consolas', 9), wrap='word', borderwidth=0) |
| 181 | + self.console.pack(fill='both', expand=True, padx=5, pady=5) |
| 182 | + |
| 183 | + def log(self, message): |
| 184 | + ts = datetime.now().strftime("%H:%M:%S"); log_msg = f"[{ts}] {message}\n" |
| 185 | + self.console.config(state='normal'); self.console.insert('end', log_msg); self.console.see('end'); self.console.config(state='disabled') |
| 186 | + |
| 187 | + def start_experiment(self): |
| 188 | + try: |
| 189 | + self.params = self._validate_and_get_params() |
| 190 | + self.log("Connecting to instruments..."); self.backend.connect(self.params['k2400_visa'], self.params['ls_visa']) |
| 191 | + self.backend.configure_instruments(self.params['current_ma'], self.params['compliance_v']); self.log("All instruments connected and configured for passive logging.") |
| 192 | + |
| 193 | + ts = datetime.now().strftime("%Y%m%d_%H%M%S"); filename = f"{self.params['name']}_{ts}_RT_Passive.csv" |
| 194 | + self.data_filepath = os.path.join(self.params['save_path'], filename) |
| 195 | + with open(self.data_filepath, 'w', newline='') as f: |
| 196 | + writer = csv.writer(f); writer.writerow(["Temperature (K)", "Voltage (V)", "Resistance (Ohm)", "Elapsed Time (s)"]) |
| 197 | + |
| 198 | + self.set_ui_state(running=True) |
| 199 | + for key in self.data_storage: self.data_storage[key].clear() |
| 200 | + self.line_main.set_data([], []); self.ax_main.set_title(f"R-T Curve: {self.params['name']}"); self.canvas.draw() |
| 201 | + self.log("Starting passive logging..."); self.start_time = time.time() |
| 202 | + self.root.after(100, self._experiment_loop) |
| 203 | + except Exception as e: |
| 204 | + self.log(f"ERROR: {traceback.format_exc()}"); messagebox.showerror("Start Failed", f"{e}"); self.backend.shutdown() |
| 205 | + |
| 206 | + def stop_experiment(self, reason=""): |
| 207 | + if not self.is_running: return |
| 208 | + self.log(f"Stopping... {reason}" if reason else "Stopping by user request.") |
| 209 | + self.is_running = False; self.backend.shutdown(); self.set_ui_state(running=False) |
| 210 | + self.ax_main.set_title("Logging stopped."); self.canvas.draw() |
| 211 | + if reason: messagebox.showinfo("Experiment Finished", f"Reason: {reason}") |
| 212 | + |
| 213 | + def _experiment_loop(self): |
| 214 | + if not self.is_running: return |
| 215 | + try: |
| 216 | + temp, voltage = self.backend.get_measurement() |
| 217 | + resistance = voltage / (self.params['current_ma'] * 1e-3) if self.params['current_ma'] != 0 else float('inf') |
| 218 | + elapsed = time.time() - self.start_time |
| 219 | + self.log(f"T: {temp:.3f} K | R: {resistance:.4e} Ω") |
| 220 | + |
| 221 | + self.data_storage['temperature'].append(temp); self.data_storage['voltage'].append(voltage); self.data_storage['resistance'].append(resistance) |
| 222 | + with open(self.data_filepath, 'a', newline='') as f: csv.writer(f).writerow([f"{temp:.4f}", f"{voltage:.6e}", f"{resistance:.6e}", f"{elapsed:.2f}"]) |
| 223 | + self.line_main.set_data(self.data_storage['temperature'], self.data_storage['resistance']) |
| 224 | + self.ax_main.relim(); self.ax_main.autoscale_view(); self.figure.tight_layout(); self.canvas.draw() |
| 225 | + |
| 226 | + self.root.after(int(self.params['delay_s'] * 1000), self._experiment_loop) |
| 227 | + |
| 228 | + except Exception as e: |
| 229 | + self.log(f"CRITICAL ERROR: {traceback.format_exc()}"); messagebox.showerror("Runtime Error", f"{e}"); self.stop_experiment("Runtime Error") |
| 230 | + |
| 231 | + def _validate_and_get_params(self): |
| 232 | + try: |
| 233 | + params = {'name': self.entries["Sample Name"].get(), 'save_path': self.entries["Save Location"].get(), |
| 234 | + 'ls_visa': self.ls_cb.get(), 'current_ma': float(self.entries["Source Current (mA)"].get()), |
| 235 | + 'compliance_v': float(self.entries["Compliance (V)"].get()), 'delay_s': float(self.entries["Logging Delay (s)"].get()), |
| 236 | + 'k2400_visa': self.k2400_cb.get()} |
| 237 | + if not all(params.values()): raise ValueError("All fields must be filled.") |
| 238 | + return params |
| 239 | + except Exception as e: raise ValueError(f"Invalid parameter input: {e}") |
| 240 | + |
| 241 | + def set_ui_state(self, running: bool): |
| 242 | + self.is_running = running |
| 243 | + state = 'disabled' if running else 'normal' |
| 244 | + self.start_button.config(state=state) |
| 245 | + for w in self.entries.values(): w.config(state=state) |
| 246 | + for cb in [self.ls_cb, self.k2400_cb]: cb.config(state=state if state == 'normal' else 'readonly') |
| 247 | + self.stop_button.config(state='normal' if running else 'disabled') |
| 248 | + |
| 249 | + def _scan_for_visa(self): |
| 250 | + if self.backend.rm is None: self.log("ERROR: PyVISA library missing."); return |
| 251 | + self.log("Scanning for VISA instruments..."); resources = self.backend.rm.list_resources() |
| 252 | + if resources: |
| 253 | + self.log(f"Found: {resources}"); self.ls_cb['values'] = resources; self.k2400_cb['values'] = resources |
| 254 | + for r in resources: |
| 255 | + if '12' in r or '15' in r: self.ls_cb.set(r) |
| 256 | + if '2400' in r or 'GPIB::4' in r: self.k2400_cb.set(r) |
| 257 | + else: self.log("No VISA instruments found.") |
| 258 | + |
| 259 | + def _browse_file_location(self): |
| 260 | + path = filedialog.askdirectory() |
| 261 | + if path: |
| 262 | + self.entries["Save Location"].config(state='normal'); self.entries["Save Location"].delete(0, 'end') |
| 263 | + self.entries["Save Location"].insert(0, path); self.entries["Save Location"].config(state='disabled') |
| 264 | + |
| 265 | + def _create_entry(self, parent, label_text, default_value, row, browse=False): |
| 266 | + ttk.Label(parent, text=f"{label_text}:").grid(row=row, column=0, sticky='w', padx=10, pady=3) |
| 267 | + entry = ttk.Entry(parent, font=self.FONT_BASE) |
| 268 | + entry.grid(row=row, column=1, sticky='ew', padx=10, pady=3, columnspan=2 if browse else 1) |
| 269 | + entry.insert(0, default_value); self.entries[label_text] = entry |
| 270 | + if browse: |
| 271 | + btn = ttk.Button(parent, text="...", width=3, command=self._browse_file_location) |
| 272 | + btn.grid(row=row, column=3, sticky='e', padx=(0,10)) |
| 273 | + entry.config(state='disabled') |
| 274 | + |
| 275 | + def _create_combobox(self, parent, label_text, row): |
| 276 | + ttk.Label(parent, text=f"{label_text}:").grid(row=row, column=0, sticky='w', padx=10, pady=3) |
| 277 | + cb = ttk.Combobox(parent, font=self.FONT_BASE, state='readonly') |
| 278 | + cb.grid(row=row, column=1, sticky='ew', padx=10, pady=3, columnspan=3) |
| 279 | + return cb |
| 280 | + |
| 281 | + def _on_closing(self): |
| 282 | + if self.is_running and messagebox.askyesno("Exit", "Experiment is running. Stop and exit?"): |
| 283 | + self.stop_experiment("Application closed by user."); self.root.destroy() |
| 284 | + elif not self.is_running: self.root.destroy() |
| 285 | + |
| 286 | +if __name__ == '__main__': |
| 287 | + if not PYMEASURE_AVAILABLE: |
| 288 | + messagebox.showerror("Dependency Error", "Pymeasure or PyVISA is not installed. Please run 'pip install pymeasure'.") |
| 289 | + else: |
| 290 | + root = tk.Tk(); app = RT_GUI_Passive(root); root.mainloop() |
0 commit comments