working prototype: stopping with experiment, display data

This commit is contained in:
2026-07-12 02:16:57 +02:00
parent d6da2f2bda
commit 847ab4d89c
3 changed files with 186 additions and 85 deletions
+83 -5
View File
@@ -2,6 +2,8 @@
from .Resultable import Resultable from .Resultable import Resultable
from .Drawable import Drawable from .Drawable import Drawable
import numpy
import threading
############################################################################# #############################################################################
# # # #
@@ -44,25 +46,101 @@ class TemperatureResult(Resultable, Drawable):
self.job_id = job_id self.job_id = job_id
if job_date is not None: if job_date is not None:
self.job_date = job_date self.job_date = job_date
# Listener management for GUI updates
self.__listeners = []
self.__listener_lock = threading.Lock()
def get_number_of_channels(self): def get_number_of_channels(self):
"""Returns 1 for temperature data.""" """Returns 1 for temperature data."""
return 1 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): def get_xdata(self):
"""Returns the x data (timestamps).""" """Returns the x data (timestamps) as numpy array."""
return self.x 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): def get_ydata(self, channel=0):
"""Returns the y data (temperature readings).""" """Returns the y data (temperature readings) as numpy array."""
if channel == 0: if channel == 0:
return self.y if isinstance(self.y, numpy.ndarray):
return [] 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): def __len__(self):
"""Returns the number of data points.""" """Returns the number of data points."""
return len(self.y) if self.y else 0 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): def __repr__(self):
"""String representation of the temperature result.""" """String representation of the temperature result."""
return f"TemperatureResult(points={len(self)}, job_id={self.job_id})" return f"TemperatureResult(points={len(self)}, job_id={self.job_id})"
+66 -44
View File
@@ -59,10 +59,10 @@ import matplotlib.figure
from damaris.gui import ExperimentWriter, ExperimentHandling from damaris.gui import ExperimentWriter, ExperimentHandling
from damaris.gui import ResultReader, ResultHandling from damaris.gui import ResultReader, ResultHandling
from damaris.gui import BackendDriver from damaris.gui import BackendDriver
from damaris.gui.TemperatureMonitor import TemperatureMonitor
#from damaris.gui.gtkcodebuffer import CodeBuffer, SyntaxLoader #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 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 # default, can be set to true from start script
debug = False debug = False
@@ -517,6 +517,8 @@ class DamarisGUI:
self.data = self.si.data self.data = self.si.data
# run frontend and script engines # run frontend and script engines
self.monitor.observe_data_pool( self.data ) self.monitor.observe_data_pool( self.data )
# Start temperature monitoring if configured (before scripts start)
self.start_temperature_monitoring(actual_config)
self.si.runScripts( ) self.si.runScripts( )
except Exception as e: except Exception as e:
#print "ToDo evaluate exception",str(e), "at",traceback.extract_tb(sys.exc_info()[2])[-1][1:3] #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" ] != "": if actual_config[ "data_pool_name" ] != "":
self.dump_states( init=True ) self.dump_states( init=True )
# Start temperature monitoring if configured
self.start_temperature_monitoring(actual_config)
gobject.timeout_add( 200, self.observe_running_experiment ) gobject.timeout_add( 200, self.observe_running_experiment )
def observe_running_experiment( self ): 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] 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: 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 != "": if self.save_thread is None and self.dump_filename != "":
print("all subprocesses ended, saving data pool") print("all subprocesses ended, saving data pool")
# thread to save data... # thread to save data...
@@ -1010,8 +1011,9 @@ class DamarisGUI:
temp_interval = config.get("temperature_interval", 2.0) temp_interval = config.get("temperature_interval", 2.0)
try: try:
# Import the factory # Import the factory and TemperatureMonitor (lazy import to avoid circular issues)
from damaris.tools.temperature import create_controller from damaris.tools.temperature import create_controller
from damaris.gui.TemperatureMonitor import TemperatureMonitor
# Create controller based on type # Create controller based on type
if controller_type == "eurotherm": if controller_type == "eurotherm":
@@ -2331,18 +2333,20 @@ pygobject version %(pygobject)s
"data_pool_comprate": self.config_data_pool_comprate.get_value_as_int( ), "data_pool_comprate": self.config_data_pool_comprate.get_value_as_int( ),
"script_font": self.config_script_font_button.get_font_name( ), "script_font": self.config_script_font_button.get_font_name( ),
"adc_bit_depth": self.config_adc_bit_depth_spinbutton.get_value_as_int(), "adc_bit_depth": self.config_adc_bit_depth_spinbutton.get_value_as_int(),
# Temperature monitoring configuration # Temperature monitoring configuration (only if widgets exist)
"enable_temperature_monitoring": self.config_temp_monitoring_checkbutton.get_active(), "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(), "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(), "temperature_device": self.config_temp_device_entry.get_text() if hasattr(self, 'config_temp_device_entry') else "",
"temperature_baudrate": self._get_temp_baudrate(), "temperature_baudrate": self._get_temp_baudrate() if hasattr(self, 'config_temp_baudrate_combobox') else 19200,
"temperature_interval": self.config_temp_interval_spinbutton.get_value() "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"] ADC_Result.default_bit_depth = actual_config["adc_bit_depth"]
return actual_config return actual_config
def _get_temp_baudrate(self): def _get_temp_baudrate(self):
"""Get selected baud rate from combobox.""" """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() active_text = self.config_temp_baudrate_combobox.get_active_text()
if active_text: if active_text:
try: try:
@@ -2382,24 +2386,36 @@ pygobject version %(pygobject)s
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
# Temperature monitoring configuration # Temperature monitoring configuration (only if widgets exist)
if "enable_temperature_monitoring" in config: if hasattr(self, 'config_temp_monitoring_checkbutton') and "enable_temperature_monitoring" in config:
self.config_temp_monitoring_checkbutton.set_active(config["enable_temperature_monitoring"]) try:
if "temperature_controller_type" in config: self.config_temp_monitoring_checkbutton.set_active(bool(config["enable_temperature_monitoring"]))
controller_type = config["temperature_controller_type"] except (TypeError, ValueError):
if controller_type in ["eurotherm", "simulated"]: pass
self.config_temp_controller_type_combobox.set_active_text(controller_type) if hasattr(self, 'config_temp_controller_type_combobox') and "temperature_controller_type" in config:
# Update widget visibility based on type try:
self._update_temp_widgets_visibility() controller_type = config["temperature_controller_type"]
if "temperature_device" in config: if controller_type in ["eurotherm", "simulated"]:
self.config_temp_device_entry.set_text(config["temperature_device"]) self.config_temp_controller_type_combobox.set_active_text(controller_type)
if "temperature_baudrate" in config: # Update widget visibility based on type
baudrate = str(config["temperature_baudrate"]) self._update_temp_widgets_visibility()
if baudrate in ["9600", "19200", "38400", "57600", "115200"]: except (TypeError, ValueError):
self.config_temp_baudrate_combobox.set_active_text(baudrate) pass
else: if hasattr(self, 'config_temp_device_entry') and "temperature_device" in config:
self.config_temp_baudrate_combobox.set_active(0) # Default to 19200 try:
if "temperature_interval" in config: 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: try:
self.config_temp_interval_spinbutton.set_value(float(config["temperature_interval"])) self.config_temp_interval_spinbutton.set_value(float(config["temperature_interval"]))
except (ValueError, TypeError): except (ValueError, TypeError):
@@ -2459,20 +2475,26 @@ pygobject version %(pygobject)s
def _update_temp_widgets_visibility(self): def _update_temp_widgets_visibility(self):
"""Show/hide widgets based on selected controller type.""" """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": active_type = self.config_temp_controller_type_combobox.get_active_text()
# Show device selection widgets if active_type is None:
self.config_temp_device_hbox.show() active_type = "eurotherm"
self.config_temp_baudrate_hbox.show()
elif active_type == "simulated": # Defensive: check if widgets exist before showing/hiding
# Hide device-specific widgets for simulated controller show_device = active_type == "eurotherm"
self.config_temp_device_hbox.hide() if hasattr(self, 'config_temp_device_hbox'):
self.config_temp_baudrate_hbox.hide() if show_device:
else: self.config_temp_device_hbox.show()
# Show everything for unknown types else:
self.config_temp_device_hbox.show() self.config_temp_device_hbox.hide()
self.config_temp_baudrate_hbox.show() 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 ): def load_config_handler( self, widget ):
if self.system_default_filename: if self.system_default_filename:
@@ -3373,7 +3395,7 @@ class MonitorWidgets:
if in_result is None: if in_result is None:
self.clear_display( ) self.clear_display( )
return 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( ) xmin = in_result.get_xmin( )
xmax = in_result.get_xmax( ) xmax = in_result.get_xmax( )
+37 -36
View File
@@ -10,17 +10,28 @@ import threading
import time import time
import datetime 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.data import TemperatureResult
from damaris.tools.temperature import TemperatureController 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): class TemperatureMonitor(threading.Thread):
""" """
@@ -97,42 +108,32 @@ class TemperatureMonitor(threading.Thread):
self.result.job_date = datetime.datetime.now() self.result.job_date = datetime.datetime.now()
self.result.job_id = id(self) 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: if self.data_pool is not None:
# Make a deep copy of the result to avoid thread issues
with self._lock: with self._lock:
# Copy the data lists # Create a copy for the data_pool with current data
x_copy = self.result.x[:] if hasattr(self.result, 'x') and self.result.x else [] data_pool_result = TemperatureResult()
y_copy = self.result.y[:] if hasattr(self.result, 'y') and self.result.y else [] data_pool_result.xlabel = self.result.xlabel
setpoint_copy = self.result.setpoint[:] if hasattr(self.result, 'setpoint') and self.result.setpoint else [] data_pool_result.ylabel = self.result.ylabel
job_id_copy = getattr(self.result, 'job_id', None) data_pool_result.description = self.result.description.copy() if isinstance(self.result.description, dict) else {}
job_date_copy = getattr(self.result, 'job_date', None) # Copy the data from self.result
desc_copy = getattr(self.result, 'description', {}).copy() if hasattr(self.result, 'description') and isinstance(getattr(self.result, 'description', None), dict) else {} data_pool_result.x = list(self.result.x) if hasattr(self.result, 'x') else []
xlabel_copy = getattr(self.result, 'xlabel', "Time (s)") data_pool_result.y = list(self.result.y) if hasattr(self.result, 'y') else []
ylabel_copy = getattr(self.result, 'ylabel', "Temperature (C)") 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 # Store in data_pool in GTK thread (if GTK available)
result_copy = TemperatureResult( gdk = _get_gdk()
x=x_copy, if gdk is not None:
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
gdk.threads_enter() gdk.threads_enter()
try: try:
self.data_pool[self.data_key] = result_copy self.data_pool[self.data_key] = data_pool_result
finally: finally:
gdk.threads_leave() gdk.threads_leave()
else: else:
self.data_pool[self.data_key] = result_copy self.data_pool[self.data_key] = data_pool_result
except Exception as e: except Exception as e:
print(f"TemperatureMonitor error: {e}") print(f"TemperatureMonitor error: {e}")