Skip to content

Commit 4c63cfd

Browse files
List ascending descending option added
1 parent 64446bd commit 4c63cfd

1 file changed

Lines changed: 58 additions & 43 deletions

File tree

pica/lakeshore/T_Control_L350_Step_GUI.py

Lines changed: 58 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""
2-
Module: T_Control_L350_RangeControl_GUI.py
3-
Purpose: GUI module for T Control L350 Sequence Control GUI (Threaded).
2+
Module: T_Control_L350_Step_GUI.py
3+
Purpose: GUI module for T Control L350 Step Measurement GUI (Threaded).
44
"""
55

66
import tkinter as tk
@@ -32,10 +32,7 @@
3232

3333

3434
def run_script_process(script_path):
35-
"""
36-
Wrapper function to execute a script using runpy in its own directory.
37-
This becomes the target for the new, isolated process.
38-
"""
35+
"""Wrapper function to execute a script using runpy in its own directory."""
3936
try:
4037
os.chdir(os.path.dirname(script_path))
4138
runpy.run_path(script_path, run_name="__main__")
@@ -46,7 +43,6 @@ def run_script_process(script_path):
4643

4744

4845
def launch_plotter_utility():
49-
"""Finds and launches the plotter utility script in a new process."""
5046
try:
5147
script_dir = os.path.dirname(os.path.abspath(__file__))
5248
plotter_path = os.path.join(script_dir, "..", "utils", "PlotterUtil_GUI.py")
@@ -59,7 +55,6 @@ def launch_plotter_utility():
5955

6056

6157
def launch_gpib_scanner():
62-
"""Finds and launches the GPIB scanner utility in a new process."""
6358
try:
6459
script_dir = os.path.dirname(os.path.abspath(__file__))
6560
scanner_path = os.path.join(script_dir, "..", "utils", "GPIB_Instrument_Scanner_GUI.py")
@@ -102,7 +97,7 @@ def configure_ramp(self, setpoint, rate, heater_range):
10297
self.lakeshore.write('*CLS')
10398
self.set_heater_range(1, heater_range)
10499
self.lakeshore.write(f'SETP 1,{setpoint}')
105-
self.lakeshore.write(f'RAMP 1,1,{rate}') # Ramp ON
100+
self.lakeshore.write(f'RAMP 1,1,{rate}')
106101

107102
def set_heater_range(self, output, heater_range):
108103
range_map = {'off': 0, 'low': 2, 'medium': 4, 'high': 5}
@@ -119,7 +114,7 @@ def get_status(self):
119114
def stop_ramp(self):
120115
if self.lakeshore:
121116
try:
122-
self.lakeshore.write('RAMP 1,0,0') # Ramp OFF
117+
self.lakeshore.write('RAMP 1,0,0')
123118
self.set_heater_range(1, 'off')
124119
print(" Lakeshore ramp stopped and heater turned off.")
125120
except Exception as e:
@@ -135,12 +130,13 @@ def shutdown(self):
135130
finally:
136131
self.lakeshore = None
137132

133+
138134
# -------------------------------------------------------------------------------
139135
# --- FRONT END (GUI) ---
140136
# -------------------------------------------------------------------------------
141137

142138
class TempControlGUI:
143-
PROGRAM_VERSION = "9.0-Seq"
139+
PROGRAM_VERSION = "9.1-Step"
144140
CLR_BG_DARK = '#B8A392'
145141
CLR_HEADER = '#E5DCD3'
146142
CLR_FG_LIGHT = '#2C2825'
@@ -159,7 +155,7 @@ class TempControlGUI:
159155

160156
def __init__(self, root):
161157
self.root = root
162-
self.root.title(f"Lakeshore 350 Sequence Control v{self.PROGRAM_VERSION}")
158+
self.root.title(f"Lakeshore 350 Step Sequence Control v{self.PROGRAM_VERSION}")
163159
self.root.geometry("1450x850")
164160
self.root.minsize(1200, 750)
165161
self.root.configure(bg=self.CLR_BG_DARK)
@@ -223,7 +219,7 @@ def create_widgets(self):
223219

224220
def _populate_left_panel(self, panel):
225221
panel.grid_columnconfigure(0, weight=1)
226-
panel.grid_rowconfigure(3, weight=1) # Console expands
222+
panel.grid_rowconfigure(3, weight=1)
227223

228224
self._create_info_panel(panel, 0)
229225
self._create_sequence_panel(panel, 1)
@@ -247,16 +243,18 @@ def _create_info_panel(self, parent, grid_row):
247243
self.logo_image = ImageTk.PhotoImage(img)
248244
logo_canvas.create_image(LOGO_SIZE / 2, LOGO_SIZE / 2, image=self.logo_image)
249245
except Exception:
250-
pass # Silently fail if logo not present to keep console clean
246+
pass
251247

252-
institute_font = ('Segoe UI', self.FONT_BASE[1] + 2, 'bold')
253-
ttk.Label(frame, text="UGC-DAE Consortium", font=institute_font).grid(row=0, column=1, padx=5, pady=(15,0), sticky='sw')
248+
institute_font = ('Segoe UI', self.FONT_BASE[1] + 1, 'bold')
249+
ttk.Label(frame, text="UGC-DAE Consortium for\nScientific Research", font=institute_font, justify='left').grid(row=0, column=1, padx=5, pady=(15,0), sticky='sw')
254250
ttk.Label(frame, text="Mumbai Centre", font=institute_font).grid(row=1, column=1, padx=5, sticky='nw')
255251

256252
def _create_sequence_panel(self, parent, grid_row):
257253
frame = ttk.LabelFrame(parent, text='Measurement Sequence Builder')
258254
frame.grid(row=grid_row, column=0, sticky='new', pady=5, padx=5)
259255
frame.grid_columnconfigure(1, weight=1)
256+
frame.grid_columnconfigure(2, weight=1)
257+
frame.grid_columnconfigure(3, weight=1)
260258

261259
# Listbox for Temperatures
262260
self.listbox = tk.Listbox(frame, height=6, font=self.FONT_BASE, bg=self.CLR_INPUT_BG, fg=self.CLR_TEXT_DARK)
@@ -277,15 +275,24 @@ def _create_sequence_panel(self, parent, grid_row):
277275

278276
ttk.Button(frame, text="Generate Steps", command=self._generate_steps).grid(row=2, column=2, columnspan=2, sticky='ew', padx=5, pady=2)
279277

280-
# Manual Addition
278+
# Sort Order & Clear All
281279
ttk.Separator(frame, orient='horizontal').grid(row=3, column=0, columnspan=4, sticky='ew', pady=5, padx=10)
282280

283-
ttk.Label(frame, text="Manual(K):").grid(row=4, column=0, sticky='e', padx=2)
281+
ttk.Label(frame, text="Order:").grid(row=4, column=0, sticky='e', padx=2)
282+
self.sort_var = tk.StringVar(value='Ascending')
283+
sort_cb = ttk.Combobox(frame, textvariable=self.sort_var, values=['Ascending', 'Descending'], state='readonly', width=12)
284+
sort_cb.grid(row=4, column=1, columnspan=2, sticky='w', padx=2)
285+
sort_cb.bind('<<ComboboxSelected>>', lambda e: self._sort_listbox())
286+
287+
ttk.Button(frame, text="Clear All", command=self._clear_listbox).grid(row=4, column=3, sticky='ew', padx=2)
288+
289+
# Manual Addition
290+
ttk.Label(frame, text="Manual(K):").grid(row=5, column=0, sticky='e', padx=2, pady=5)
284291
self.entry_manual = ttk.Entry(frame, width=6)
285-
self.entry_manual.grid(row=4, column=1, sticky='w', padx=2)
292+
self.entry_manual.grid(row=5, column=1, sticky='w', padx=2, pady=5)
286293

287-
ttk.Button(frame, text="Add", command=self._add_manual_step).grid(row=4, column=2, sticky='ew', padx=2)
288-
ttk.Button(frame, text="Remove", command=self._remove_step).grid(row=4, column=3, sticky='ew', padx=2)
294+
ttk.Button(frame, text="Add", command=self._add_manual_step).grid(row=5, column=2, sticky='ew', padx=2, pady=5)
295+
ttk.Button(frame, text="Remove", command=self._remove_step).grid(row=5, column=3, sticky='ew', padx=2, pady=5)
289296

290297
def _create_settings_panel(self, parent, grid_row):
291298
frame = ttk.LabelFrame(parent, text='Instrument & Stability Settings')
@@ -345,7 +352,7 @@ def _populate_right_panel(self, panel):
345352
status_frame.grid(row=0, column=0, sticky='ew', pady=(0, 10))
346353
status_frame.grid_columnconfigure(0, weight=1)
347354

348-
self.lbl_status = tk.Label(status_frame, text="SYSTEM IDLE", font=('Segoe UI', 16, 'bold'), bg=self.CLR_FRAME_BG, fg=self.CLR_TEXT_DARK, pady=10)
355+
self.lbl_status = tk.Label(status_frame, text="READY TO START", font=('Segoe UI', 16, 'bold'), bg=self.CLR_FRAME_BG, fg=self.CLR_TEXT_DARK, pady=10)
349356
self.lbl_status.grid(row=0, column=0, sticky='ew')
350357

351358
self.btn_proceed = ttk.Button(status_frame, text="Measurement Complete - Proceed ➔", style='Proceed.TButton', state='disabled', command=self._on_proceed)
@@ -383,6 +390,20 @@ def _create_grid_entry(self, parent, label_text, default_value, row, col):
383390
entry.insert(0, default_value)
384391
self.entries[label_text] = entry
385392

393+
def _sort_listbox(self):
394+
"""Sorts the current listbox contents based on the selected order."""
395+
items = list(self.listbox.get(0, tk.END))
396+
if not items: return
397+
try:
398+
floats = [float(x) for x in items]
399+
is_desc = (self.sort_var.get() == 'Descending')
400+
floats.sort(reverse=is_desc)
401+
self.listbox.delete(0, tk.END)
402+
for val in floats:
403+
self.listbox.insert(tk.END, f"{val:.2f}")
404+
except Exception:
405+
pass # Ignore malformed data quietly
406+
386407
def _generate_steps(self):
387408
try:
388409
start = float(self.entry_start.get())
@@ -399,6 +420,7 @@ def _generate_steps(self):
399420
while current >= end:
400421
self.listbox.insert(tk.END, f"{current:.2f}")
401422
current -= step
423+
self._sort_listbox() # Auto-sort after generation
402424
except ValueError:
403425
messagebox.showerror("Input Error", "Please enter valid numeric values for Start, End, and Step.")
404426

@@ -407,6 +429,7 @@ def _add_manual_step(self):
407429
val = float(self.entry_manual.get())
408430
self.listbox.insert(tk.END, f"{val:.2f}")
409431
self.entry_manual.delete(0, tk.END)
432+
self._sort_listbox() # Auto-sort upon addition
410433
except ValueError:
411434
messagebox.showerror("Input Error", "Enter a valid numeric temperature.")
412435

@@ -415,6 +438,9 @@ def _remove_step(self):
415438
if selection:
416439
self.listbox.delete(selection[0])
417440

441+
def _clear_listbox(self):
442+
self.listbox.delete(0, tk.END)
443+
418444
def log(self, message):
419445
ts = datetime.now().strftime("%H:%M:%S")
420446
self.console.config(state='normal')
@@ -430,7 +456,7 @@ def _on_proceed(self):
430456
self.log("User confirmed measurement. Moving to next setpoint.")
431457
self.btn_proceed.config(state='disabled')
432458
self._update_status_ui("INITIATING NEXT RAMP...", self.CLR_HEADER)
433-
self.proceed_event.set() # Unlocks the wait() in the thread
459+
self.proceed_event.set()
434460

435461
# --- MAIN LOGIC ---
436462
def start_sequence(self):
@@ -449,7 +475,6 @@ def start_sequence(self):
449475
self.set_ui_state(running=True)
450476
self.is_running = True
451477

452-
# Clear Plot
453478
for key in self.data_storage:
454479
self.data_storage[key].clear()
455480
self.line_temp[0].set_data([], [])
@@ -459,18 +484,16 @@ def start_sequence(self):
459484
self.start_time = time.time()
460485
self.proceed_event.clear()
461486

462-
# Start GUI Queue Monitor
463487
self.root.after(100, self._process_gui_queue)
464488

465-
# Launch Background Thread
466489
self.measurement_thread = threading.Thread(target=self._hardware_worker_loop, daemon=True)
467490
self.measurement_thread.start()
468491

469492
def stop_ramp(self):
470493
if not self.is_running: return
471494
self.log("ABORT INITIATED BY USER.")
472495
self.is_running = False
473-
self.proceed_event.set() # Unblock thread if it's waiting for user
496+
self.proceed_event.set()
474497
self.backend.stop_ramp()
475498
self.set_ui_state(running=False)
476499
self._update_status_ui("SEQUENCE ABORTED", self.CLR_ACCENT_RED)
@@ -499,8 +522,9 @@ def set_ui_state(self, running: bool):
499522
self.entry_end.config(state=state)
500523
self.entry_step.config(state=state)
501524
self.entry_manual.config(state=state)
525+
self.sort_var.set(self.sort_var.get()) # Keeps combobox visible but we disable the widget below
502526
self.ls_cb.config(state=state if state == 'normal' else 'readonly')
503-
self.btn_proceed.config(state='disabled') # Explicitly disable until thread enables it
527+
self.btn_proceed.config(state='disabled')
504528

505529
def _scan_for_visa(self):
506530
if self.backend.rm is None:
@@ -520,13 +544,11 @@ def _scan_for_visa(self):
520544

521545
# --- THREADING COMPONENTS ---
522546
def _put_gui_msg(self, msg_type, **kwargs):
523-
"""Helper to safely send commands to the Tkinter thread."""
524547
payload = {'type': msg_type}
525548
payload.update(kwargs)
526549
self.gui_queue.put(payload)
527550

528551
def _process_gui_queue(self):
529-
"""Tkinter Main Thread loop. Processes UI update requests from the hardware thread."""
530552
try:
531553
while True:
532554
msg = self.gui_queue.get_nowait()
@@ -548,7 +570,7 @@ def _process_gui_queue(self):
548570

549571
elif msg_type == 'handshake_ready':
550572
self.btn_proceed.config(state='normal')
551-
winsound.Beep(1000, 500) # Audio Alert
573+
winsound.Beep(1000, 500)
552574

553575
elif msg_type == 'sequence_complete':
554576
self.set_ui_state(running=False)
@@ -561,7 +583,6 @@ def _process_gui_queue(self):
561583
self.root.after(100, self._process_gui_queue)
562584

563585
def _hardware_worker_loop(self):
564-
"""Background Thread. Controls VISA, monitors stability, and blocks on user handshake."""
565586
try:
566587
self._put_gui_msg('log', text="Connecting to Lakeshore...")
567588
self.backend.connect(self.params['ls_visa'])
@@ -572,10 +593,8 @@ def _hardware_worker_loop(self):
572593
self._put_gui_msg('log', text=f"--- Sequence Step {i+1}/{len(self.setpoint_floats)}: Target {target} K ---")
573594
self._put_gui_msg('status', text=f"RAMPING TO {target} K", color=self.CLR_ACCENT_RED)
574595

575-
# Command Lakeshore
576596
self.backend.configure_ramp(target, self.params['rate'], self.params['heater_range'])
577597

578-
# Monitoring & Soak Window Logic
579598
stable_start_time = None
580599

581600
while self.is_running:
@@ -585,7 +604,6 @@ def _hardware_worker_loop(self):
585604
self.data_storage['heater'].append(htr)
586605
self._put_gui_msg('plot')
587606

588-
# Check Tolerance Band
589607
if abs(temp - target) <= self.params['tolerance']:
590608
if stable_start_time is None:
591609
stable_start_time = time.time()
@@ -594,7 +612,7 @@ def _hardware_worker_loop(self):
594612

595613
elif time.time() - stable_start_time >= self.params['soak_time']:
596614
self._put_gui_msg('log', text=f"Stable inside window for {self.params['soak_time']}s.")
597-
break # Exits the monitoring while loop, moves to handshake
615+
break
598616
else:
599617
if stable_start_time is not None:
600618
self._put_gui_msg('log', text="Drifted outside tolerance band. Restarting soak timer.")
@@ -603,35 +621,32 @@ def _hardware_worker_loop(self):
603621

604622
time.sleep(self.params['delay_s'])
605623

606-
if not self.is_running: break # Sequence aborted
624+
if not self.is_running: break
607625

608-
# --- The Handshake ---
609626
self._put_gui_msg('status', text=f"STABLE AT {target} K | AWAITING MEASUREMENT", color=self.CLR_ACCENT_GREEN)
610627
self._put_gui_msg('log', text="READY FOR EXTERNAL MEASUREMENT. Waiting for user acknowledgement.")
611628
self._put_gui_msg('handshake_ready')
612629

613-
# Freeze this background thread until GUI calls self.proceed_event.set()
614630
self.proceed_event.clear()
615631
self.proceed_event.wait()
616632

617-
# Sequence Over
618633
if self.is_running:
619634
self.is_running = False
620635
self._put_gui_msg('log', text="Measurement Sequence Complete.")
621-
self._put_gui_msg('status', text="SEQUENCE COMPLETE", color=self.CLR_HEADER)
636+
self._put_gui_msg('status', text="READY TO START", color=self.CLR_HEADER)
622637
self._put_gui_msg('sequence_complete')
623638
self.backend.stop_ramp()
624639

625640
except Exception as e:
626641
self._put_gui_msg('log', text=f"CRITICAL ERROR IN HARDWARE THREAD: {e}\n{traceback.format_exc()}")
627642
self.is_running = False
628-
self._put_gui_msg('sequence_complete') # Resets UI
643+
self._put_gui_msg('sequence_complete')
629644
self.backend.stop_ramp()
630645

631646
def _on_closing(self):
632647
if self.is_running and messagebox.askyesno("Exit", "A sequence is active. Stop hardware and exit?"):
633648
self.stop_ramp()
634-
time.sleep(0.5) # Give the thread a moment to shut down cleanly
649+
time.sleep(0.5)
635650
self.root.destroy()
636651
elif not self.is_running:
637652
self.root.destroy()

0 commit comments

Comments
 (0)