Public Access
working prototype: stopping with experiment, display data
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
from .Resultable import Resultable
|
from .Resultable import Resultable
|
||||||
from .Drawable import Drawable
|
from .Drawable import Drawable
|
||||||
|
import numpy
|
||||||
|
import threading
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
# #
|
# #
|
||||||
@@ -45,24 +47,100 @@ class TemperatureResult(Resultable, Drawable):
|
|||||||
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})"
|
||||||
|
|||||||
@@ -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( )
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user