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
66import tkinter as tk
3232
3333
3434def 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
4845def 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
6157def 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
142138class 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 \n Scientific 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