@@ -142,7 +142,7 @@ def shutdown(self):
142142# -------------------------------------------------------------------------------
143143
144144class TempControlGUI :
145- PROGRAM_VERSION = "9.2 -Step"
145+ PROGRAM_VERSION = "9.3 -Step"
146146 CLR_BG_DARK = '#B8A392'
147147 CLR_HEADER = '#E5DCD3'
148148 CLR_FG_LIGHT = '#2C2825'
@@ -171,10 +171,12 @@ def __init__(self, root):
171171 self .gui_queue = queue .Queue ()
172172 self .proceed_event = threading .Event ()
173173
174+ # Flag to safely pass live heater updates to the hardware thread
175+ self .live_heater_update = None
176+
174177 self .logo_image = None
175178 self .backend = Lakeshore_Backend ()
176179
177- # Added 'resistance' to data storage for persistence
178180 self .data_storage = {'time' : [], 'temperature' : [], 'target' : [],
179181 'resistance' : [], 'heater' : []}
180182
@@ -265,7 +267,6 @@ def _create_sequence_panel(self, parent, grid_row):
265267 frame .grid (row = grid_row , column = 0 , sticky = 'new' , pady = 5 , padx = 5 )
266268 for i in range (4 ): frame .grid_columnconfigure (i , weight = 1 )
267269
268- # Listbox with Scrollbar
269270 list_frame = ttk .Frame (frame )
270271 list_frame .grid (row = 0 , column = 0 , columnspan = 4 , sticky = 'nsew' , padx = 10 , pady = 5 )
271272
@@ -278,7 +279,6 @@ def _create_sequence_panel(self, parent, grid_row):
278279 self .listbox .pack (side = "left" , fill = "both" , expand = True )
279280 scrollbar .pack (side = "right" , fill = "y" )
280281
281- # Auto Generator
282282 ttk .Label (frame , text = "Start(K):" ).grid (row = 1 , column = 0 , sticky = 'e' , padx = 2 )
283283 self .entry_start = ttk .Entry (frame , width = 6 )
284284 self .entry_start .grid (row = 1 , column = 1 , sticky = 'w' , padx = 2 )
@@ -295,7 +295,6 @@ def _create_sequence_panel(self, parent, grid_row):
295295
296296 ttk .Separator (frame , orient = 'horizontal' ).grid (row = 3 , column = 0 , columnspan = 4 , sticky = 'ew' , pady = 5 , padx = 10 )
297297
298- # Sort Order & List Size
299298 ttk .Label (frame , text = "Order:" ).grid (row = 4 , column = 0 , sticky = 'e' , padx = 2 )
300299 self .sort_var = tk .StringVar (value = 'Ascending' )
301300 sort_cb = ttk .Combobox (frame , textvariable = self .sort_var , values = ['Ascending' , 'Descending' ], state = 'readonly' , width = 10 )
@@ -309,7 +308,6 @@ def _create_sequence_panel(self, parent, grid_row):
309308 size_spin .bind ('<Return>' , self ._update_list_size )
310309 size_spin .bind ('<FocusOut>' , self ._update_list_size )
311310
312- # Manual Addition & Clear
313311 ttk .Label (frame , text = "Manual(K):" ).grid (row = 5 , column = 0 , sticky = 'e' , padx = 2 , pady = 5 )
314312 self .entry_manual = ttk .Entry (frame , width = 6 )
315313 self .entry_manual .grid (row = 5 , column = 1 , sticky = 'w' , padx = 2 , pady = 5 )
@@ -328,14 +326,19 @@ def _create_settings_panel(self, parent, grid_row):
328326 self .entries = {}
329327
330328 self ._create_grid_entry (frame , "Tolerance (±K)" , "0.5" , 0 , 0 )
331- self ._create_grid_entry (frame , "Soak Time (s)" , "60 " , 0 , 2 )
329+ self ._create_grid_entry (frame , "Soak Time (s)" , "120 " , 0 , 2 ) # Updated to 120s
332330 self ._create_grid_entry (frame , "Ramp Rate (K/min)" , "2" , 1 , 0 )
333331 self ._create_grid_entry (frame , "Poll Delay (s)" , "1" , 1 , 2 )
334332
335333 ttk .Label (frame , text = "Heater Range:" ).grid (row = 2 , column = 0 , sticky = 'w' , padx = 10 , pady = 5 )
336334 self .heater_range_var = tk .StringVar (value = 'High' )
337- heater_cb = ttk .Combobox (frame , textvariable = self .heater_range_var , values = ['Off' , 'Low' , 'Medium' , 'High' ], state = 'readonly' , width = 10 )
338- heater_cb .grid (row = 2 , column = 1 , sticky = 'ew' , padx = 5 )
335+
336+ # Retain as self.heater_cb so we can easily access it.
337+ self .heater_cb = ttk .Combobox (frame , textvariable = self .heater_range_var , values = ['Off' , 'Low' , 'Medium' , 'High' ], state = 'readonly' , width = 10 )
338+ self .heater_cb .grid (row = 2 , column = 1 , sticky = 'ew' , padx = 5 )
339+
340+ # Bind the event to handle live changes
341+ self .heater_cb .bind ('<<ComboboxSelected>>' , self ._on_heater_range_changed )
339342
340343 ttk .Label (frame , text = "VISA Addr:" ).grid (row = 2 , column = 2 , sticky = 'w' , padx = 5 , pady = 5 )
341344 self .ls_cb = ttk .Combobox (frame , state = 'readonly' , width = 15 )
@@ -367,7 +370,6 @@ def _populate_right_panel(self, panel):
367370 panel .grid_rowconfigure (1 , weight = 1 )
368371 panel .grid_columnconfigure (0 , weight = 1 )
369372
370- # STATUS HANDSHAKE BAR
371373 status_frame = ttk .Frame (panel )
372374 status_frame .grid (row = 0 , column = 0 , sticky = 'ew' , pady = (0 , 10 ))
373375 status_frame .grid_columnconfigure (0 , weight = 1 )
@@ -378,7 +380,6 @@ def _populate_right_panel(self, panel):
378380 self .btn_proceed = ttk .Button (status_frame , text = "Measurement Complete - Proceed ➔" , style = 'Proceed.TButton' , state = 'disabled' , command = self ._on_proceed )
379381 self .btn_proceed .grid (row = 0 , column = 1 , sticky = 'ew' , padx = 10 , ipady = 5 )
380382
381- # PLOTTER
382383 container = ttk .LabelFrame (panel , text = 'Live Temperature Monitoring' )
383384 container .grid (row = 1 , column = 0 , sticky = 'nsew' )
384385 container .grid_rowconfigure (0 , weight = 1 )
@@ -388,7 +389,6 @@ def _populate_right_panel(self, panel):
388389 self .ax_temp = self .figure .add_subplot (211 )
389390 self .ax_heater = self .figure .add_subplot (212 , sharex = self .ax_temp )
390391
391- # Dual Plots for Temperature
392392 self .line_target = self .ax_temp .plot ([], [], color = self .CLR_ACCENT_GREEN , marker = '' , linestyle = '--' , label = 'Target Setpoint' )[0 ]
393393 self .line_temp = self .ax_temp .plot ([], [], color = self .CLR_ACCENT_RED , marker = 'o' , markersize = 3 , linestyle = '-' , label = 'Actual Temp' )[0 ]
394394 self .ax_temp .set_ylabel ("Temperature (K)" )
@@ -464,7 +464,6 @@ def _add_manual_step(self):
464464 messagebox .showerror ("Input Error" , "Enter a valid numeric temperature." )
465465
466466 def _remove_step (self ):
467- # Reverse iteration ensures index deletion doesn't shift remaining targets incorrectly
468467 selection = self .listbox .curselection ()
469468 for index in reversed (selection ):
470469 self .listbox .delete (index )
@@ -488,6 +487,13 @@ def _on_proceed(self):
488487 self ._update_status_ui ("INITIATING NEXT RAMP..." , self .CLR_HEADER )
489488 self .proceed_event .set ()
490489
490+ def _on_heater_range_changed (self , event = None ):
491+ """Captures mid-run updates to the heater range dropdown."""
492+ if self .is_running :
493+ new_range = self .heater_range_var .get ()
494+ self .log (f"Live heater update requested: { new_range } " )
495+ self .live_heater_update = new_range
496+
491497 def _beep (self ):
492498 def _ring ():
493499 try :
@@ -528,6 +534,7 @@ def start_sequence(self):
528534
529535 self .set_ui_state (running = True )
530536 self .is_running = True
537+ self .live_heater_update = None # Reset flag
531538
532539 for key in self .data_storage :
533540 self .data_storage [key ].clear ()
@@ -539,7 +546,6 @@ def start_sequence(self):
539546 self .start_time = time .time ()
540547 self .proceed_event .clear ()
541548
542- # --- Open persistent data file (flushed every point) ---
543549 os .makedirs ("data" , exist_ok = True )
544550 stamp = datetime .now ().strftime ("%Y%m%d_%H%M%S" )
545551 self .data_filepath = os .path .join ("data" , f"TStep_{ stamp } .csv" )
@@ -560,7 +566,7 @@ def stop_ramp(self):
560566 if not self .is_running : return
561567 self .log ("ABORT INITIATED BY USER." )
562568 self .is_running = False
563- self .proceed_event .set ()
569+ self .proceed_event .set () # Unblocks if stuck waiting for user click
564570 self .backend .stop_ramp ()
565571 self .set_ui_state (running = False )
566572 self ._update_status_ui ("SEQUENCE ABORTED" , self .CLR_ACCENT_RED )
@@ -592,6 +598,7 @@ def set_ui_state(self, running: bool):
592598 self .sort_var .set (self .sort_var .get ())
593599 self .ls_cb .config (state = state if state == 'normal' else 'readonly' )
594600 self .btn_proceed .config (state = 'disabled' )
601+ # Note: self.heater_cb is explicitly left as 'readonly' naturally so the user can interact mid-run.
595602
596603 def _scan_for_visa (self ):
597604 if self .backend .rm is None :
@@ -609,7 +616,6 @@ def _scan_for_visa(self):
609616 else :
610617 self .log ("No VISA instruments found." )
611618
612- # --- THREADING COMPONENTS ---
613619 def _put_gui_msg (self , msg_type , ** kwargs ):
614620 payload = {'type' : msg_type }
615621 payload .update (kwargs )
@@ -669,19 +675,33 @@ def _hardware_worker_loop(self):
669675 self .backend .configure_ramp (target , self .params ['rate' ], self .params ['heater_range' ])
670676
671677 stable_start_time = None
678+ phase = 'RAMPING' # Can be: RAMPING, SOAKING, or WAITING
679+
680+ self .proceed_event .clear ()
672681
673682 while self .is_running :
683+ # 1. Process Live Heater Updates (Mid-Run adjustments)
684+ if self .live_heater_update is not None :
685+ new_range = self .live_heater_update
686+ self .live_heater_update = None
687+ try :
688+ self .backend .set_heater_range (1 , new_range )
689+ self ._put_gui_msg ('log' , text = f"Heater successfully switched to: { new_range } " )
690+ except Exception as e :
691+ self ._put_gui_msg ('log' , text = f"Failed to switch heater range: { e } " )
692+
693+ # 2. Get hardware status
674694 temp , resistance , htr = self .backend .get_status ()
675695 elapsed = time .time () - self .start_time
676696 now_str = datetime .now ().strftime ("%Y-%m-%d %H:%M:%S" )
677697
698+ # 3. Store and commit data
678699 self .data_storage ['time' ].append (elapsed )
679700 self .data_storage ['temperature' ].append (temp )
680701 self .data_storage ['target' ].append (target )
681702 self .data_storage ['resistance' ].append (resistance )
682703 self .data_storage ['heater' ].append (htr )
683704
684- # Persist immediately so an abort/crash never loses a point
685705 try :
686706 self .csv_writer .writerow (
687707 [now_str , f"{ elapsed :.2f} " , f"{ target :.4f} " ,
@@ -693,38 +713,42 @@ def _hardware_worker_loop(self):
693713
694714 self ._put_gui_msg ('plot' )
695715
696- if abs (temp - target ) <= self .params ['tolerance' ]:
697- if stable_start_time is None :
698- stable_start_time = time .time ()
699- self ._put_gui_msg ('log' , text = f"Entered tolerance band (±{ self .params ['tolerance' ]} K). Starting soak timer..." )
700- self ._put_gui_msg ('status' , text = f"STABILIZING AT { target } K..." , color = self .CLR_STABLE_WAIT )
701-
702- elif time .time () - stable_start_time >= self .params ['soak_time' ]:
703- self ._put_gui_msg ('log' , text = f"Stable inside window for { self .params ['soak_time' ]} s." )
704- break
705- else :
706- if stable_start_time is not None :
707- self ._put_gui_msg ('log' , text = "Drifted outside tolerance band. Restarting soak timer." )
708- self ._put_gui_msg ('status' , text = f"RAMPING TO { target } K" , color = self .CLR_ACCENT_RED )
709- stable_start_time = None
716+ # 4. State Machine Logic (Never breaks the loop until user clicks proceed)
717+ if phase in ['RAMPING' , 'SOAKING' ]:
718+ if abs (temp - target ) <= self .params ['tolerance' ]:
719+ if phase == 'RAMPING' :
720+ stable_start_time = time .time ()
721+ phase = 'SOAKING'
722+ self ._put_gui_msg ('log' , text = f"Entered tolerance band (±{ self .params ['tolerance' ]} K). Starting soak timer..." )
723+ self ._put_gui_msg ('status' , text = f"STABILIZING AT { target } K..." , color = self .CLR_STABLE_WAIT )
724+
725+ elif phase == 'SOAKING' and (time .time () - stable_start_time >= self .params ['soak_time' ]):
726+ self ._put_gui_msg ('log' , text = f"Stable inside window for { self .params ['soak_time' ]} s. Ready for external measurement." )
727+ self ._put_gui_msg ('status' , text = f"STABLE AT { target } K | AWAITING MEASUREMENT" , color = self .CLR_ACCENT_GREEN )
728+ self ._put_gui_msg ('handshake_ready' )
729+ phase = 'WAITING'
730+ else :
731+ if phase == 'SOAKING' :
732+ self ._put_gui_msg ('log' , text = "Drifted outside tolerance band. Restarting soak timer." )
733+ self ._put_gui_msg ('status' , text = f"RAMPING TO { target } K" , color = self .CLR_ACCENT_RED )
734+ stable_start_time = None
735+ phase = 'RAMPING'
736+
737+ elif phase == 'WAITING' :
738+ # While waiting, we just monitor. Check if user clicked Proceed
739+ if self .proceed_event .is_set ():
740+ self .proceed_event .clear ()
741+ break # Exits the while loop, moving to the next target in the sequence!
710742
743+ # 5. Delay before next poll
711744 time .sleep (self .params ['delay_s' ])
712745
713- if not self .is_running : break
714-
715- self ._put_gui_msg ('status' , text = f"STABLE AT { target } K | AWAITING MEASUREMENT" , color = self .CLR_ACCENT_GREEN )
716- self ._put_gui_msg ('log' , text = "READY FOR EXTERNAL MEASUREMENT. Waiting for user acknowledgement." )
717- self ._put_gui_msg ('handshake_ready' )
718-
719- self .proceed_event .clear ()
720- self .proceed_event .wait ()
721-
722746 if self .is_running :
723747 self ._put_gui_msg ('log' , text = "Measurement Sequence Complete." )
724748 self ._put_gui_msg ('status' , text = "READY TO START" , color = self .CLR_HEADER )
725749 self ._put_gui_msg ('sequence_complete' )
726750 self .backend .stop_ramp ()
727- self .is_running = False # flip LAST, after messages are queued
751+ self .is_running = False
728752
729753 except Exception as e :
730754 self ._put_gui_msg ('log' , text = f"CRITICAL ERROR IN HARDWARE THREAD: { e } \n { traceback .format_exc ()} " )
0 commit comments