diff --git a/src/damaris/data/Temperature.py b/src/damaris/data/Temperature.py index 67be0ba..9842340 100644 --- a/src/damaris/data/Temperature.py +++ b/src/damaris/data/Temperature.py @@ -2,6 +2,8 @@ from .Resultable import Resultable from .Drawable import Drawable +import numpy +import threading ############################################################################# # # @@ -44,25 +46,101 @@ class TemperatureResult(Resultable, Drawable): self.job_id = job_id if job_date is not None: self.job_date = job_date + + # Listener management for GUI updates + self.__listeners = [] + self.__listener_lock = threading.Lock() def get_number_of_channels(self): """Returns 1 for temperature data.""" return 1 + def register_listener(self, listener): + """Register a listener to be notified when data changes.""" + with self.__listener_lock: + if listener not in self.__listeners: + self.__listeners.append(listener) + + def unregister_listener(self, listener): + """Unregister a listener.""" + with self.__listener_lock: + if listener in self.__listeners: + self.__listeners.remove(listener) + def get_xdata(self): - """Returns the x data (timestamps).""" - return self.x + """Returns the x data (timestamps) as numpy array.""" + if isinstance(self.x, numpy.ndarray): + return self.x + return numpy.array(self.x, dtype=float) if self.x else numpy.array([], dtype=float) def get_ydata(self, channel=0): - """Returns the y data (temperature readings).""" + """Returns the y data (temperature readings) as numpy array.""" if channel == 0: - return self.y - return [] + if isinstance(self.y, numpy.ndarray): + return self.y + return numpy.array(self.y, dtype=float) if self.y else numpy.array([], dtype=float) + return numpy.array([], dtype=float) def __len__(self): """Returns the number of data points.""" return len(self.y) if self.y else 0 + def uses_statistics(self): + """Returns False as temperature data doesn't use statistics.""" + return False + + def ready_for_drawing_error(self): + """Returns False as temperature data doesn't have error bars.""" + return False + + def get_yerr(self, channel=0): + """Returns empty error data.""" + return numpy.array([], dtype=float) + + def get_errorplotdata(self): + """Returns empty error plot data.""" + return [[], [], []] + + def get_lineplotdata(self): + """Returns line plot data (not used for temperature).""" + return [[], []] + + def get_xmin(self): + """Returns minimum of x.""" + xdata = self.get_xdata() + return xdata.min() if len(xdata) > 0 else 0 + + def get_xmax(self): + """Returns maximum of x.""" + xdata = self.get_xdata() + return xdata.max() if len(xdata) > 0 else 0 + + def get_ymin(self): + """Returns minimum of y.""" + ydata = self.get_ydata(0) + return ydata.min() if len(ydata) > 0 else 0 + + def get_ymax(self): + """Returns maximum of y.""" + ydata = self.get_ydata(0) + return ydata.max() if len(ydata) > 0 else 0 + + def get_xminpos(self): + """Returns smallest positive value of x.""" + xdata = self.get_xdata() + mask = xdata > 0 + if numpy.any(mask): + return xdata[mask].min() + return 0 + + def get_yminpos(self): + """Returns smallest positive value of y.""" + ydata = self.get_ydata(0) + mask = ydata > 0 + if numpy.any(mask): + return ydata[mask].min() + return 0 + def __repr__(self): """String representation of the temperature result.""" return f"TemperatureResult(points={len(self)}, job_id={self.job_id})" diff --git a/src/damaris/gui/DamarisGUI.py b/src/damaris/gui/DamarisGUI.py index 23b205b..08224d3 100644 --- a/src/damaris/gui/DamarisGUI.py +++ b/src/damaris/gui/DamarisGUI.py @@ -59,10 +59,10 @@ import matplotlib.figure from damaris.gui import ExperimentWriter, ExperimentHandling from damaris.gui import ResultReader, ResultHandling from damaris.gui import BackendDriver -from damaris.gui.TemperatureMonitor import TemperatureMonitor #from damaris.gui.gtkcodebuffer import CodeBuffer, SyntaxLoader #from damaris.data import Drawable # this is a base class, it should be used... -from damaris.data import DataPool, Accumulation, ADC_Result, MeasurementResult +from damaris.data import DataPool, Accumulation, ADC_Result, MeasurementResult, TemperatureResult +# TemperatureMonitor imported lazily to avoid circular import issues # default, can be set to true from start script debug = False @@ -517,6 +517,8 @@ class DamarisGUI: self.data = self.si.data # run frontend and script engines self.monitor.observe_data_pool( self.data ) + # Start temperature monitoring if configured (before scripts start) + self.start_temperature_monitoring(actual_config) self.si.runScripts( ) except Exception as e: #print "ToDo evaluate exception",str(e), "at",traceback.extract_tb(sys.exc_info()[2])[-1][1:3] @@ -578,9 +580,6 @@ class DamarisGUI: if actual_config[ "data_pool_name" ] != "": self.dump_states( init=True ) - # Start temperature monitoring if configured - self.start_temperature_monitoring(actual_config) - gobject.timeout_add( 200, self.observe_running_experiment ) def observe_running_experiment( self ): @@ -693,6 +692,8 @@ class DamarisGUI: still_running = [_f for _f in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver, self.dump_thread ] if _f] if len( still_running ) == 0: + # Stop temperature monitoring when experiment finishes + self.stop_temperature_monitoring() if self.save_thread is None and self.dump_filename != "": print("all subprocesses ended, saving data pool") # thread to save data... @@ -1010,8 +1011,9 @@ class DamarisGUI: temp_interval = config.get("temperature_interval", 2.0) try: - # Import the factory + # Import the factory and TemperatureMonitor (lazy import to avoid circular issues) from damaris.tools.temperature import create_controller + from damaris.gui.TemperatureMonitor import TemperatureMonitor # Create controller based on type if controller_type == "eurotherm": @@ -2331,18 +2333,20 @@ pygobject version %(pygobject)s "data_pool_comprate": self.config_data_pool_comprate.get_value_as_int( ), "script_font": self.config_script_font_button.get_font_name( ), "adc_bit_depth": self.config_adc_bit_depth_spinbutton.get_value_as_int(), - # Temperature monitoring configuration - "enable_temperature_monitoring": self.config_temp_monitoring_checkbutton.get_active(), - "temperature_controller_type": self.config_temp_controller_type_combobox.get_active_text(), - "temperature_device": self.config_temp_device_entry.get_text(), - "temperature_baudrate": self._get_temp_baudrate(), - "temperature_interval": self.config_temp_interval_spinbutton.get_value() + # Temperature monitoring configuration (only if widgets exist) + "enable_temperature_monitoring": self.config_temp_monitoring_checkbutton.get_active() if hasattr(self, 'config_temp_monitoring_checkbutton') else False, + "temperature_controller_type": self.config_temp_controller_type_combobox.get_active_text() if hasattr(self, 'config_temp_controller_type_combobox') else "eurotherm", + "temperature_device": self.config_temp_device_entry.get_text() if hasattr(self, 'config_temp_device_entry') else "", + "temperature_baudrate": self._get_temp_baudrate() if hasattr(self, 'config_temp_baudrate_combobox') else 19200, + "temperature_interval": self.config_temp_interval_spinbutton.get_value() if hasattr(self, 'config_temp_interval_spinbutton') else 2.0 } ADC_Result.default_bit_depth = actual_config["adc_bit_depth"] return actual_config def _get_temp_baudrate(self): """Get selected baud rate from combobox.""" + if not hasattr(self, 'config_temp_baudrate_combobox'): + return 19200 active_text = self.config_temp_baudrate_combobox.get_active_text() if active_text: try: @@ -2382,24 +2386,36 @@ pygobject version %(pygobject)s except (ValueError, TypeError): pass - # Temperature monitoring configuration - if "enable_temperature_monitoring" in config: - self.config_temp_monitoring_checkbutton.set_active(config["enable_temperature_monitoring"]) - if "temperature_controller_type" in config: - controller_type = config["temperature_controller_type"] - if controller_type in ["eurotherm", "simulated"]: - self.config_temp_controller_type_combobox.set_active_text(controller_type) - # Update widget visibility based on type - self._update_temp_widgets_visibility() - if "temperature_device" in config: - self.config_temp_device_entry.set_text(config["temperature_device"]) - if "temperature_baudrate" in config: - baudrate = str(config["temperature_baudrate"]) - if baudrate in ["9600", "19200", "38400", "57600", "115200"]: - self.config_temp_baudrate_combobox.set_active_text(baudrate) - else: - self.config_temp_baudrate_combobox.set_active(0) # Default to 19200 - if "temperature_interval" in config: + # Temperature monitoring configuration (only if widgets exist) + if hasattr(self, 'config_temp_monitoring_checkbutton') and "enable_temperature_monitoring" in config: + try: + self.config_temp_monitoring_checkbutton.set_active(bool(config["enable_temperature_monitoring"])) + except (TypeError, ValueError): + pass + if hasattr(self, 'config_temp_controller_type_combobox') and "temperature_controller_type" in config: + try: + controller_type = config["temperature_controller_type"] + if controller_type in ["eurotherm", "simulated"]: + self.config_temp_controller_type_combobox.set_active_text(controller_type) + # Update widget visibility based on type + self._update_temp_widgets_visibility() + except (TypeError, ValueError): + pass + if hasattr(self, 'config_temp_device_entry') and "temperature_device" in config: + try: + self.config_temp_device_entry.set_text(str(config["temperature_device"])) + except (TypeError, ValueError): + pass + if hasattr(self, 'config_temp_baudrate_combobox') and "temperature_baudrate" in config: + try: + baudrate = str(config["temperature_baudrate"]) + if baudrate in ["9600", "19200", "38400", "57600", "115200"]: + self.config_temp_baudrate_combobox.set_active_text(baudrate) + else: + self.config_temp_baudrate_combobox.set_active(0) # Default to 19200 + except (TypeError, ValueError): + pass + if hasattr(self, 'config_temp_interval_spinbutton') and "temperature_interval" in config: try: self.config_temp_interval_spinbutton.set_value(float(config["temperature_interval"])) except (ValueError, TypeError): @@ -2459,20 +2475,26 @@ pygobject version %(pygobject)s def _update_temp_widgets_visibility(self): """Show/hide widgets based on selected controller type.""" - active_type = self.config_temp_controller_type_combobox.get_active_text() + # Defensive: check if combobox exists + if not hasattr(self, 'config_temp_controller_type_combobox'): + return - if active_type == "eurotherm": - # Show device selection widgets - self.config_temp_device_hbox.show() - self.config_temp_baudrate_hbox.show() - elif active_type == "simulated": - # Hide device-specific widgets for simulated controller - self.config_temp_device_hbox.hide() - self.config_temp_baudrate_hbox.hide() - else: - # Show everything for unknown types - self.config_temp_device_hbox.show() - self.config_temp_baudrate_hbox.show() + active_type = self.config_temp_controller_type_combobox.get_active_text() + if active_type is None: + active_type = "eurotherm" + + # Defensive: check if widgets exist before showing/hiding + show_device = active_type == "eurotherm" + if hasattr(self, 'config_temp_device_hbox'): + if show_device: + self.config_temp_device_hbox.show() + else: + self.config_temp_device_hbox.hide() + if hasattr(self, 'config_temp_baudrate_hbox'): + if show_device: + self.config_temp_baudrate_hbox.show() + else: + self.config_temp_baudrate_hbox.hide() def load_config_handler( self, widget ): if self.system_default_filename: @@ -3373,7 +3395,7 @@ class MonitorWidgets: if in_result is None: self.clear_display( ) return - if isinstance( in_result, Accumulation ) or isinstance( in_result, ADC_Result ): + if isinstance( in_result, Accumulation ) or isinstance( in_result, ADC_Result ) or isinstance( in_result, TemperatureResult ): xmin = in_result.get_xmin( ) xmax = in_result.get_xmax( ) diff --git a/src/damaris/gui/TemperatureMonitor.py b/src/damaris/gui/TemperatureMonitor.py index 1b71e83..d945b70 100644 --- a/src/damaris/gui/TemperatureMonitor.py +++ b/src/damaris/gui/TemperatureMonitor.py @@ -10,17 +10,28 @@ import threading import time import datetime -try: - import gi - gi.require_version('Gdk', '3.0') - from gi.repository import Gdk as gdk - HAS_GTK = True -except (ImportError, ValueError): - HAS_GTK = False - from damaris.data import TemperatureResult from damaris.tools.temperature import TemperatureController +# GTK imports are done lazily to avoid initialization issues +_HAS_GTK = None +_gdk = None + + +def _get_gdk(): + """Lazily import gdk to avoid GTK initialization issues.""" + global _HAS_GTK, _gdk + if _HAS_GTK is None: + try: + import gi + gi.require_version('Gdk', '3.0') + from gi.repository import Gdk as gdk + _gdk = gdk + _HAS_GTK = True + except (ImportError, ValueError): + _HAS_GTK = False + return _gdk if _HAS_GTK else None + class TemperatureMonitor(threading.Thread): """ @@ -97,42 +108,32 @@ class TemperatureMonitor(threading.Thread): self.result.job_date = datetime.datetime.now() self.result.job_id = id(self) - # Optional: store in data_pool (thread-safe for GTK) + # Store result in data_pool (thread-safe for GTK) + # Replace the entry on each update to trigger DataPool events if self.data_pool is not None: - # Make a deep copy of the result to avoid thread issues with self._lock: - # Copy the data lists - x_copy = self.result.x[:] if hasattr(self.result, 'x') and self.result.x else [] - y_copy = self.result.y[:] if hasattr(self.result, 'y') and self.result.y else [] - setpoint_copy = self.result.setpoint[:] if hasattr(self.result, 'setpoint') and self.result.setpoint else [] - job_id_copy = getattr(self.result, 'job_id', None) - job_date_copy = getattr(self.result, 'job_date', None) - desc_copy = getattr(self.result, 'description', {}).copy() if hasattr(self.result, 'description') and isinstance(getattr(self.result, 'description', None), dict) else {} - xlabel_copy = getattr(self.result, 'xlabel', "Time (s)") - ylabel_copy = getattr(self.result, 'ylabel', "Temperature (C)") + # Create a copy for the data_pool with current data + data_pool_result = TemperatureResult() + data_pool_result.xlabel = self.result.xlabel + data_pool_result.ylabel = self.result.ylabel + data_pool_result.description = self.result.description.copy() if isinstance(self.result.description, dict) else {} + # Copy the data from self.result + data_pool_result.x = list(self.result.x) if hasattr(self.result, 'x') else [] + data_pool_result.y = list(self.result.y) if hasattr(self.result, 'y') else [] + data_pool_result.setpoint = list(self.result.setpoint) if hasattr(self.result, 'setpoint') else [] + data_pool_result.job_id = self.result.job_id if hasattr(self.result, 'job_id') else None + data_pool_result.job_date = self.result.job_date if hasattr(self.result, 'job_date') else None - # Create copy outside the lock to minimize lock time - result_copy = TemperatureResult( - x=x_copy, - y=y_copy, - setpoint=setpoint_copy, - desc=desc_copy, - job_id=job_id_copy, - job_date=job_date_copy - ) - result_copy.xlabel = xlabel_copy - result_copy.ylabel = ylabel_copy - - # Update data_pool in GTK thread if GTK is available - if HAS_GTK: - # Use gdk.threads_enter/leave for thread-safe GTK access + # Store in data_pool in GTK thread (if GTK available) + gdk = _get_gdk() + if gdk is not None: gdk.threads_enter() try: - self.data_pool[self.data_key] = result_copy + self.data_pool[self.data_key] = data_pool_result finally: gdk.threads_leave() else: - self.data_pool[self.data_key] = result_copy + self.data_pool[self.data_key] = data_pool_result except Exception as e: print(f"TemperatureMonitor error: {e}")