1010import traceback
1111import threading
1212import queue
13- import winsound
1413from datetime import datetime
1514from matplotlib .figure import Figure
1615from matplotlib .backends .backend_tkagg import FigureCanvasTkAgg
1716import matplotlib as mpl
1817import runpy
1918from multiprocessing import Process
19+ import csv
20+ import platform
2021
2122# --- Optional Packages ---
23+ try :
24+ import winsound
25+ except ImportError :
26+ pass
27+
2228try :
2329 from PIL import Image , ImageTk
2430 PIL_AVAILABLE = True
@@ -87,29 +93,29 @@ def connect(self, visa_address):
8793 raise ConnectionError ("PyVISA is not available." )
8894 self .lakeshore = self .rm .open_resource (visa_address )
8995 self .lakeshore .timeout = 10000
96+ self .lakeshore .write ('*CLS' ) # clear status once; do NOT *RST mid-run
9097 idn = self .lakeshore .query ('*IDN?' ).strip ()
9198 print (f" Lakeshore Connected: { idn } " )
9299 return idn
93100
94101 def configure_ramp (self , setpoint , rate , heater_range ):
95- self .lakeshore .write ('*RST' )
96- time .sleep (0.5 )
97- self .lakeshore .write ('*CLS' )
98- self .set_heater_range (1 , heater_range )
99- self .lakeshore .write (f'SETP 1,{ setpoint } ' )
100- self .lakeshore .write (f'RAMP 1,1,{ rate } ' )
102+ self .set_heater_range (1 , heater_range ) # ensure heater on at desired range
103+ self .lakeshore .write (f'RAMP 1,1,{ rate } ' ) # enable ramp FIRST
104+ time .sleep (0.1 )
105+ self .lakeshore .write (f'SETP 1,{ setpoint } ' ) # now the change is ramped
101106
102107 def set_heater_range (self , output , heater_range ):
103- range_map = {'off' : 0 , 'low' : 2 , 'medium' : 4 , 'high' : 5 }
108+ range_map = {'off' : 0 , 'low' : 1 , 'medium' : 3 , 'high' : 5 }
104109 range_code = range_map .get (heater_range .lower ())
105110 if range_code is None :
106111 raise ValueError ("Invalid heater range." )
107112 self .lakeshore .write (f'RANGE { output } ,{ range_code } ' )
108113
109114 def get_status (self ):
110- temp = float (self .lakeshore .query ('KRDG? A' ).strip ())
111- htr_output = float (self .lakeshore .query ('HTR? 1' ).strip ())
112- return temp , htr_output
115+ temp = float (self .lakeshore .query ('KRDG? A' ).strip ()) # Kelvin, input A
116+ resistance = float (self .lakeshore .query ('SRDG? A' ).strip ()) # sensor units (ohms)
117+ htr_output = float (self .lakeshore .query ('HTR? 1' ).strip ()) # heater %, output 1
118+ return temp , resistance , htr_output
113119
114120 def stop_ramp (self ):
115121 if self .lakeshore :
@@ -168,8 +174,9 @@ def __init__(self, root):
168174 self .logo_image = None
169175 self .backend = Lakeshore_Backend ()
170176
171- # Added 'target' to data storage for plotting expectations
172- self .data_storage = {'time' : [], 'temperature' : [], 'target' : [], 'heater' : []}
177+ # Added 'resistance' to data storage for persistence
178+ self .data_storage = {'time' : [], 'temperature' : [], 'target' : [],
179+ 'resistance' : [], 'heater' : []}
173180
174181 self .setup_styles ()
175182 self .create_widgets ()
@@ -481,6 +488,30 @@ def _on_proceed(self):
481488 self ._update_status_ui ("INITIATING NEXT RAMP..." , self .CLR_HEADER )
482489 self .proceed_event .set ()
483490
491+ def _beep (self ):
492+ def _ring ():
493+ try :
494+ if platform .system () == 'Windows' :
495+ import winsound
496+ winsound .Beep (1000 , 500 )
497+ else :
498+ self .root .bell ()
499+ except Exception :
500+ pass
501+ threading .Thread (target = _ring , daemon = True ).start ()
502+
503+ def _close_data_file (self ):
504+ f = getattr (self , 'data_file' , None )
505+ if f :
506+ try :
507+ f .flush ()
508+ f .close ()
509+ self ._put_gui_msg ('log' , text = f"Data file closed: { self .data_filepath } " )
510+ except Exception :
511+ pass
512+ finally :
513+ self .data_file = None
514+
484515 # --- MAIN LOGIC ---
485516 def start_sequence (self ):
486517 setpoints = list (self .listbox .get (0 , tk .END ))
@@ -508,6 +539,18 @@ def start_sequence(self):
508539 self .start_time = time .time ()
509540 self .proceed_event .clear ()
510541
542+ # --- Open persistent data file (flushed every point) ---
543+ os .makedirs ("data" , exist_ok = True )
544+ stamp = datetime .now ().strftime ("%Y%m%d_%H%M%S" )
545+ self .data_filepath = os .path .join ("data" , f"TStep_{ stamp } .csv" )
546+ self .data_file = open (self .data_filepath , 'w' , newline = '' )
547+ self .csv_writer = csv .writer (self .data_file )
548+ self .csv_writer .writerow (
549+ ["Timestamp" , "Elapsed_s" , "Target_K" , "Temperature_K" ,
550+ "Resistance_Ohm" , "Heater_pct" ])
551+ self .data_file .flush ()
552+ self .log (f"Logging data to: { self .data_filepath } " )
553+
511554 self .root .after (100 , self ._process_gui_queue )
512555
513556 self .measurement_thread = threading .Thread (target = self ._hardware_worker_loop , daemon = True )
@@ -585,17 +628,22 @@ def _process_gui_queue(self):
585628 self ._update_status_ui (msg ['text' ], msg ['color' ])
586629
587630 elif msg_type == 'plot' :
588- self .line_target .set_data (self .data_storage ['time' ], self .data_storage ['target' ])
589- self .line_temp .set_data (self .data_storage ['time' ], self .data_storage ['temperature' ])
590- self .line_heater .set_data (self .data_storage ['time' ], self .data_storage ['heater' ])
631+ n = min (len (self .data_storage ['time' ]),
632+ len (self .data_storage ['temperature' ]),
633+ len (self .data_storage ['heater' ]),
634+ len (self .data_storage ['target' ]))
635+ t = self .data_storage ['time' ][:n ]
636+ self .line_target .set_data (t , self .data_storage ['target' ][:n ])
637+ self .line_temp .set_data (t , self .data_storage ['temperature' ][:n ])
638+ self .line_heater .set_data (t , self .data_storage ['heater' ][:n ])
591639 for ax in [self .ax_temp , self .ax_heater ]:
592640 ax .relim ()
593641 ax .autoscale_view ()
594642 self .canvas .draw_idle ()
595643
596644 elif msg_type == 'handshake_ready' :
597645 self .btn_proceed .config (state = 'normal' )
598- winsound . Beep ( 1000 , 500 )
646+ self . _beep ()
599647
600648 elif msg_type == 'sequence_complete' :
601649 self .set_ui_state (running = False )
@@ -604,7 +652,7 @@ def _process_gui_queue(self):
604652 except queue .Empty :
605653 pass
606654
607- if self .is_running :
655+ if self .is_running or not self . gui_queue . empty () :
608656 self .root .after (100 , self ._process_gui_queue )
609657
610658 def _hardware_worker_loop (self ):
@@ -623,11 +671,25 @@ def _hardware_worker_loop(self):
623671 stable_start_time = None
624672
625673 while self .is_running :
626- temp , htr = self .backend .get_status ()
627- self .data_storage ['time' ].append (time .time () - self .start_time )
674+ temp , resistance , htr = self .backend .get_status ()
675+ elapsed = time .time () - self .start_time
676+ now_str = datetime .now ().strftime ("%Y-%m-%d %H:%M:%S" )
677+
678+ self .data_storage ['time' ].append (elapsed )
628679 self .data_storage ['temperature' ].append (temp )
629- self .data_storage ['target' ].append (target ) # Track target for graph
680+ self .data_storage ['target' ].append (target )
681+ self .data_storage ['resistance' ].append (resistance )
630682 self .data_storage ['heater' ].append (htr )
683+
684+ # Persist immediately so an abort/crash never loses a point
685+ try :
686+ self .csv_writer .writerow (
687+ [now_str , f"{ elapsed :.2f} " , f"{ target :.4f} " ,
688+ f"{ temp :.4f} " , f"{ resistance :.6g} " , f"{ htr :.2f} " ])
689+ self .data_file .flush ()
690+ os .fsync (self .data_file .fileno ())
691+ except Exception as e :
692+ self ._put_gui_msg ('log' , text = f"WARN: data write failed: { e } " )
631693
632694 self ._put_gui_msg ('plot' )
633695
@@ -658,22 +720,26 @@ def _hardware_worker_loop(self):
658720 self .proceed_event .wait ()
659721
660722 if self .is_running :
661- self .is_running = False
662723 self ._put_gui_msg ('log' , text = "Measurement Sequence Complete." )
663724 self ._put_gui_msg ('status' , text = "READY TO START" , color = self .CLR_HEADER )
664725 self ._put_gui_msg ('sequence_complete' )
665726 self .backend .stop_ramp ()
727+ self .is_running = False # flip LAST, after messages are queued
666728
667729 except Exception as e :
668730 self ._put_gui_msg ('log' , text = f"CRITICAL ERROR IN HARDWARE THREAD: { e } \n { traceback .format_exc ()} " )
669731 self .is_running = False
670- self ._put_gui_msg ('sequence_complete' )
732+ self ._put_gui_msg ('sequence_complete' )
671733 self .backend .stop_ramp ()
734+ finally :
735+ self ._close_data_file ()
672736
673737 def _on_closing (self ):
674738 if self .is_running and messagebox .askyesno ("Exit" , "A sequence is active. Stop hardware and exit?" ):
675739 self .stop_ramp ()
676- time .sleep (0.5 )
740+ time .sleep (0.5 )
741+ if self .measurement_thread and self .measurement_thread .is_alive ():
742+ self .measurement_thread .join (timeout = 2.0 )
677743 self .root .destroy ()
678744 elif not self .is_running :
679745 self .root .destroy ()
0 commit comments