Skip to content

Commit 8128c4d

Browse files
stoping sensing fixed
1 parent e1d29ff commit 8128c4d

1 file changed

Lines changed: 65 additions & 41 deletions

File tree

pica/lakeshore/T_Control_L350_Step_GUI.py

Lines changed: 65 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def shutdown(self):
142142
# -------------------------------------------------------------------------------
143143

144144
class 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

Comments
 (0)