Public Access
Replace temperature monitoring with proxy history
This commit is contained in:
1 parent
bd060dc4d0
commit
0e796a800b
11 files changed
+170
-1124
No files matched your search
+12
-297
@@ -96,7 +96,7 @@ from damaris.gui import BackendDriver
|
||||
#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, TemperatureResult, FitResult
|
||||
# TemperatureMonitor imported lazily to avoid circular import issues
|
||||
from .TemperatureProxyClient import TemperatureProxyClient
|
||||
|
||||
# default, can be set to true from start script
|
||||
debug = False
|
||||
@@ -196,9 +196,7 @@ class DamarisGUI:
|
||||
#to stop queued experiments
|
||||
self.stop_experiment_flag = threading.Event()
|
||||
|
||||
# Temperature monitoring
|
||||
self.temp_monitor = None
|
||||
self.temp_controller = None
|
||||
self.temperature_history_start = None
|
||||
|
||||
exp_script = ""
|
||||
if exp_script_filename is not None and exp_script_filename != "":
|
||||
@@ -686,12 +684,9 @@ 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)
|
||||
temp_start = time.time()
|
||||
self.start_temperature_monitoring(actual_config)
|
||||
logger.debug("Temperature monitoring startup took %.3fs", time.time() - temp_start)
|
||||
|
||||
scripts_start = time.time()
|
||||
self.temperature_history_start = time.time()
|
||||
self.si.runScripts( )
|
||||
logger.debug("Script execution startup took %.3fs", time.time() - scripts_start)
|
||||
except Exception as e:
|
||||
@@ -869,8 +864,14 @@ 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.temperature_history_start is not None:
|
||||
temperature_history_start = self.temperature_history_start
|
||||
self.temperature_history_start = None
|
||||
self.data.pop("temperature", None)
|
||||
temperature = TemperatureProxyClient().get_history(
|
||||
temperature_history_start, time.time())
|
||||
if temperature is not None:
|
||||
self.data["temperature"] = temperature
|
||||
if self.save_thread is None and self.dump_filename != "":
|
||||
logger.info("All subprocesses ended, saving data pool")
|
||||
# Disable run button during data saving to prevent UI confusion
|
||||
@@ -1193,97 +1194,8 @@ class DamarisGUI:
|
||||
for r in still_running:
|
||||
r.quit_flag.set( )
|
||||
|
||||
# Stop temperature monitoring
|
||||
self.stop_temperature_monitoring()
|
||||
|
||||
self.state = DamarisGUI.Stop_State
|
||||
|
||||
def start_temperature_monitoring(self, config):
|
||||
"""
|
||||
Start temperature monitoring thread if configured.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary from self.config.get()
|
||||
"""
|
||||
# Check if temperature monitoring is enabled
|
||||
if not config.get("enable_temperature_monitoring", False):
|
||||
# Stop any existing monitor if temperature monitoring is disabled
|
||||
self.stop_temperature_monitoring()
|
||||
return
|
||||
|
||||
# Stop any existing monitor before starting a new one
|
||||
if self.temp_monitor is not None:
|
||||
self.stop_temperature_monitoring()
|
||||
|
||||
controller_type = config.get("temperature_controller_type", "eurotherm")
|
||||
temp_interval = config.get("temperature_interval", 2.0)
|
||||
|
||||
try:
|
||||
# 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":
|
||||
temp_device = config.get("temperature_device", "")
|
||||
temp_baudrate = config.get("temperature_baudrate", 19200)
|
||||
|
||||
if not temp_device:
|
||||
logger.warning("Temperature monitoring: No device configured for Eurotherm")
|
||||
return
|
||||
|
||||
self.temp_controller = create_controller(
|
||||
"eurotherm", temp_device, baudrate=temp_baudrate
|
||||
)
|
||||
logger.info("Temperature monitoring started with Eurotherm on %s (interval: %ss)", temp_device, temp_interval)
|
||||
|
||||
elif controller_type == "simulated":
|
||||
# Simulated controller doesn't need device configuration
|
||||
# Use default parameters: 290K base, 10K amplitude, 60s period, 5% noise, 10% missing
|
||||
self.temp_controller = create_controller("simulated")
|
||||
logger.info("Temperature monitoring started with simulated controller (interval: %ss)", temp_interval)
|
||||
|
||||
else:
|
||||
logger.warning("Temperature monitoring: Unknown controller type: %s", controller_type)
|
||||
return
|
||||
|
||||
# Create and start monitor thread
|
||||
self.temp_monitor = TemperatureMonitor(
|
||||
controller=self.temp_controller,
|
||||
interval=temp_interval,
|
||||
data_pool=self.data,
|
||||
data_key="temperature"
|
||||
)
|
||||
self.temp_monitor.start()
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to start temperature monitoring: %s", e)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self.temp_controller = None
|
||||
self.temp_monitor = None
|
||||
|
||||
def stop_temperature_monitoring(self):
|
||||
"""
|
||||
Stop the temperature monitoring thread if running.
|
||||
"""
|
||||
if self.temp_monitor is not None:
|
||||
try:
|
||||
self.temp_monitor.stop()
|
||||
logger.info("Temperature monitoring stopped")
|
||||
except Exception as e:
|
||||
logger.error("Error stopping temperature monitoring: %s", e)
|
||||
finally:
|
||||
self.temp_monitor = None
|
||||
|
||||
if self.temp_controller is not None:
|
||||
try:
|
||||
self.temp_controller.close()
|
||||
except Exception as e:
|
||||
logger.error("Error closing temperature controller: %s", e)
|
||||
finally:
|
||||
self.temp_controller = None
|
||||
|
||||
def documentation_init( self ):
|
||||
|
||||
doc_index_candidates = (
|
||||
@@ -2464,75 +2376,6 @@ class ConfigTab:
|
||||
backend_vbox.pack_start(self.config_adc_bit_depth_hbox, False, False, 0)
|
||||
self.config_adc_bit_depth_hbox.show_all()
|
||||
|
||||
# Temperature monitoring settings - single line layout
|
||||
self.config_temp_hbox = gtk.Box(orientation=gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
|
||||
# Controller type selector
|
||||
self.config_temp_controller_type_label = gtk.Label(label="Controller:")
|
||||
self.config_temp_controller_type_combobox = gtk.ComboBoxText()
|
||||
self.config_temp_controller_type_combobox.append_text("eurotherm")
|
||||
self.config_temp_controller_type_combobox.append_text("simulated")
|
||||
self.config_temp_controller_type_combobox.set_active(0) # Default to eurotherm
|
||||
self.config_temp_controller_type_combobox.set_tooltip_text("Select temperature controller type")
|
||||
self.config_temp_controller_type_combobox.connect("changed", self.on_temp_controller_type_changed)
|
||||
|
||||
self.config_temp_controller_type_hbox = gtk.Box(orientation=gtk.Orientation.HORIZONTAL, spacing=0)
|
||||
self.config_temp_controller_type_hbox.pack_start(self.config_temp_controller_type_label, False, False, 0)
|
||||
self.config_temp_controller_type_hbox.pack_start(self.config_temp_controller_type_combobox, False, False, 0)
|
||||
self.config_temp_hbox.pack_start(self.config_temp_controller_type_hbox, False, False, 0)
|
||||
|
||||
# Enable checkbox
|
||||
self.config_temp_monitoring_checkbutton = gtk.CheckButton(label="Temp:")
|
||||
self.config_temp_monitoring_checkbutton.set_tooltip_text("Enable periodic temperature reading from controller")
|
||||
self.config_temp_hbox.pack_start(self.config_temp_monitoring_checkbutton, False, False, 0)
|
||||
|
||||
# Device file chooser with entry (shown for eurotherm, hidden for simulated)
|
||||
self.config_temp_device_entry = gtk.Entry()
|
||||
self.config_temp_device_entry.set_tooltip_text("Serial port device (e.g., /dev/serial/by-id/...)")
|
||||
self.config_temp_device_entry.set_width_chars(30)
|
||||
self.config_temp_device_entry.connect("key-press-event", self.config_keypress)
|
||||
self.config_temp_device_button = gtk.Button(label="...")
|
||||
self.config_temp_device_button.set_tooltip_text("Browse for temperature controller device")
|
||||
self.config_temp_device_button.connect("clicked", self.on_temp_device_browse_clicked)
|
||||
|
||||
self.config_temp_device_hbox = gtk.Box(orientation=gtk.Orientation.HORIZONTAL, spacing=0)
|
||||
self.config_temp_device_hbox.pack_start(self.config_temp_device_entry, True, True, 0)
|
||||
self.config_temp_device_hbox.pack_start(self.config_temp_device_button, False, False, 0)
|
||||
self.config_temp_hbox.pack_start(self.config_temp_device_hbox, True, True, 0)
|
||||
|
||||
# Interval spinbutton
|
||||
self.config_temp_interval_label = gtk.Label(label="Interval (s):")
|
||||
self.config_temp_interval_spinbutton = gtk.SpinButton.new_with_range(0.1, 60, 0.1)
|
||||
self.config_temp_interval_spinbutton.set_value(2.0) # Default to 2 seconds
|
||||
self.config_temp_interval_spinbutton.set_tooltip_text("Time between temperature readings in seconds")
|
||||
self.config_temp_interval_spinbutton.connect("key-press-event", self.config_keypress)
|
||||
|
||||
self.config_temp_interval_hbox = gtk.Box(orientation=gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
self.config_temp_interval_hbox.pack_start(self.config_temp_interval_label, False, False, 0)
|
||||
self.config_temp_interval_hbox.pack_start(self.config_temp_interval_spinbutton, False, False, 0)
|
||||
self.config_temp_hbox.pack_start(self.config_temp_interval_hbox, False, False, 0)
|
||||
|
||||
# Baud rate (for eurotherm)
|
||||
self.config_temp_baudrate_combobox = gtk.ComboBoxText()
|
||||
self.config_temp_baudrate_combobox.append_text("19200")
|
||||
self.config_temp_baudrate_combobox.append_text("9600")
|
||||
self.config_temp_baudrate_combobox.append_text("38400")
|
||||
self.config_temp_baudrate_combobox.append_text("57600")
|
||||
self.config_temp_baudrate_combobox.append_text("115200")
|
||||
self.config_temp_baudrate_combobox.set_active(0)
|
||||
self.config_temp_baudrate_combobox.set_tooltip_text("Serial baud rate for temperature controller")
|
||||
self.config_temp_baudrate_hbox = gtk.Box(orientation=gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
self.config_temp_baudrate_hbox.pack_start(self.config_temp_baudrate_combobox, False, False, 0)
|
||||
self.config_temp_hbox.pack_start(self.config_temp_baudrate_hbox, False, False, 0)
|
||||
# Note: baud rate is shown for eurotherm, hidden for simulated
|
||||
|
||||
# Set initial visibility based on controller type
|
||||
self._update_temp_widgets_visibility()
|
||||
|
||||
if backend_vbox:
|
||||
backend_vbox.pack_start(self.config_temp_hbox, False, False, 0)
|
||||
self.config_temp_hbox.show_all()
|
||||
|
||||
# insert version informations
|
||||
components_text = """
|
||||
operating system %(os)s
|
||||
@@ -2639,28 +2482,10 @@ pygobject version %(pygobject)s
|
||||
"data_pool_complib": complib,
|
||||
"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 (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_bit_depth": self.config_adc_bit_depth_spinbutton.get_value_as_int()
|
||||
}
|
||||
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:
|
||||
return int(active_text)
|
||||
except ValueError:
|
||||
pass
|
||||
return 19200
|
||||
|
||||
def set( self, config ):
|
||||
if "start_backend" in config:
|
||||
@@ -2693,40 +2518,6 @@ pygobject version %(pygobject)s
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# 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._set_combo_active_text(self.config_temp_controller_type_combobox, 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._set_combo_active_text(self.config_temp_baudrate_combobox, 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):
|
||||
self.config_temp_interval_spinbutton.set_value(2.0)
|
||||
if "data_pool_complib" in config:
|
||||
# find combo-box entry and make it active...
|
||||
model = self.config_data_pool_complib.get_model( )
|
||||
@@ -2743,82 +2534,6 @@ pygobject version %(pygobject)s
|
||||
def on_adc_bit_depth_changed(self, widget):
|
||||
ADC_Result.default_bit_depth = widget.get_value_as_int()
|
||||
|
||||
def on_temp_device_browse_clicked(self, widget):
|
||||
"""
|
||||
Open file chooser dialog to select temperature controller device.
|
||||
Defaults to /dev/serial/by-id/ which is typically used for USB serial converters.
|
||||
"""
|
||||
dialog = gtk.FileChooserDialog(
|
||||
title="Select Temperature Controller Device",
|
||||
parent=None,
|
||||
action=gtk.FileChooserAction.OPEN,
|
||||
)
|
||||
dialog.add_button("Cancel", gtk.ResponseType.CANCEL)
|
||||
dialog.add_button("Open", gtk.ResponseType.OK)
|
||||
|
||||
# Set default folder to /dev/serial/by-id/ if it exists
|
||||
default_folder = "/dev/serial/by-id/"
|
||||
if os.path.isdir(default_folder):
|
||||
dialog.set_current_folder(default_folder)
|
||||
else:
|
||||
# Fallback to /dev
|
||||
dialog.set_current_folder("/dev/")
|
||||
|
||||
# Only show regular files (device files)
|
||||
dialog.set_select_multiple(False)
|
||||
|
||||
response = dialog.run()
|
||||
if response == gtk.ResponseType.OK:
|
||||
selected_file = dialog.get_filename()
|
||||
self.config_temp_device_entry.set_text(selected_file)
|
||||
|
||||
dialog.destroy()
|
||||
|
||||
def on_temp_controller_type_changed(self, widget):
|
||||
"""Update widget visibility when controller type changes."""
|
||||
self._update_temp_widgets_visibility()
|
||||
|
||||
def _update_temp_widgets_visibility(self):
|
||||
"""Show/hide widgets based on selected controller type."""
|
||||
# Defensive: check if combobox exists
|
||||
if not hasattr(self, 'config_temp_controller_type_combobox'):
|
||||
return
|
||||
|
||||
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 _set_combo_active_text(self, combobox, text):
|
||||
"""
|
||||
Set a ComboBoxText's active entry by text.
|
||||
|
||||
Gtk.ComboBoxText.set_active_text() does not exist in GTK 3.24.x,
|
||||
so we iterate the model to find the matching entry.
|
||||
"""
|
||||
if not text:
|
||||
return
|
||||
model = combobox.get_model()
|
||||
if model is None:
|
||||
return
|
||||
for i in range(model.iter_n_children(None)):
|
||||
treeiter = model.iter_nth_child(None, i)
|
||||
if treeiter and model.get(treeiter, 0)[0] == text:
|
||||
combobox.set_active(i)
|
||||
return
|
||||
|
||||
def load_config_handler( self, widget ):
|
||||
if self.system_default_filename:
|
||||
self.load_config( self.system_default_filename )
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
# -*- coding: iso-8859-1 -*-
|
||||
"""
|
||||
Temperature monitoring thread.
|
||||
|
||||
This module provides a thread that periodically reads temperature from a controller
|
||||
and stores it in a TemperatureResult object.
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
import datetime
|
||||
|
||||
from damaris.data import TemperatureResult
|
||||
from damaris.tools.temperature import TemperatureController
|
||||
|
||||
|
||||
class TemperatureMonitor(threading.Thread):
|
||||
"""
|
||||
Thread that periodically reads temperature from a controller
|
||||
and stores it in a TemperatureResult object.
|
||||
|
||||
This thread runs independently and can be configured with any
|
||||
TemperatureController implementation (Eurotherm, Lakeshore, etc.).
|
||||
|
||||
Attributes:
|
||||
controller: The TemperatureController instance to read from.
|
||||
interval: Polling interval in seconds.
|
||||
result: The TemperatureResult object storing collected data.
|
||||
quit_flag: Event to signal the thread to stop.
|
||||
data_pool: Optional DataPool to store results.
|
||||
data_key: Key to use when storing in data_pool.
|
||||
"""
|
||||
|
||||
def __init__(self, controller: TemperatureController,
|
||||
interval: float = 1.0,
|
||||
data_pool=None,
|
||||
data_key: str = "temperature"):
|
||||
"""
|
||||
Initialize the temperature monitor.
|
||||
|
||||
Args:
|
||||
controller: TemperatureController implementation (Eurotherm, etc.)
|
||||
interval: Polling interval in seconds (default: 1.0)
|
||||
data_pool: Optional DataPool to store results (default: None)
|
||||
data_key: Key to use when storing in data_pool (default: "temperature")
|
||||
"""
|
||||
threading.Thread.__init__(self, name="TemperatureMonitor")
|
||||
self.controller = controller
|
||||
self.interval = float(interval)
|
||||
self.data_pool = data_pool
|
||||
self.data_key = data_key
|
||||
self.quit_flag = threading.Event()
|
||||
self.result = TemperatureResult()
|
||||
self._running = False
|
||||
self._lock = threading.RLock()
|
||||
self._consecutive_failures = 0
|
||||
self._max_consecutive_failures = 10
|
||||
self._last_error = None
|
||||
self._connection_ok = True
|
||||
|
||||
# Initialize the result with metadata
|
||||
self.result.xlabel = "Time (s)"
|
||||
self.result.ylabel = "Temperature (C)"
|
||||
self.result.description = {"type": "temperature_monitoring"}
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Main thread loop: periodically reads temperature and stores it.
|
||||
"""
|
||||
self._running = True
|
||||
start_time = time.time()
|
||||
|
||||
while not self.quit_flag.is_set() and self._running:
|
||||
try:
|
||||
# Read temperature and setpoint
|
||||
timestamp = time.time() - start_time
|
||||
current_temp = self.controller.get_temperature()
|
||||
setpoint = self.controller.get_setpoint()
|
||||
|
||||
# Store in result
|
||||
with self._lock:
|
||||
if not hasattr(self.result, 'x'):
|
||||
self.result.x = []
|
||||
self.result.y = []
|
||||
self.result.setpoint = []
|
||||
|
||||
self.result.x.append(timestamp)
|
||||
self.result.y.append(current_temp)
|
||||
self.result.setpoint.append(setpoint)
|
||||
|
||||
# Update job metadata on first sample
|
||||
if len(self.result.x) == 1:
|
||||
self.result.job_date = datetime.datetime.now()
|
||||
self.result.job_id = id(self)
|
||||
|
||||
# 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:
|
||||
with self._lock:
|
||||
# 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
|
||||
|
||||
# Store in data_pool (dict item assignment is atomic under the GIL)
|
||||
self.data_pool[self.data_key] = data_pool_result
|
||||
|
||||
# Reset consecutive failure counter on successful read
|
||||
self._consecutive_failures = 0
|
||||
self._last_error = None
|
||||
self._connection_ok = True
|
||||
|
||||
except Exception as e:
|
||||
self._consecutive_failures += 1
|
||||
self._last_error = str(e)
|
||||
if self._consecutive_failures <= 3:
|
||||
print(f"TemperatureMonitor read error (attempt {self._consecutive_failures}): {e}")
|
||||
elif self._consecutive_failures == 5:
|
||||
print(f"TemperatureMonitor: repeated read errors, checking connection...")
|
||||
if hasattr(self.controller, 'check_connection'):
|
||||
self._connection_ok = self.controller.check_connection()
|
||||
if not self._connection_ok:
|
||||
print(f"TemperatureMonitor: WARNING - controller appears disconnected after {self._consecutive_failures} failures")
|
||||
elif self._consecutive_failures >= self._max_consecutive_failures:
|
||||
print(f"TemperatureMonitor: ERROR - {self._consecutive_failures} consecutive failures, controller may be disconnected")
|
||||
else:
|
||||
print(f"TemperatureMonitor error (failure #{self._consecutive_failures}): {e}")
|
||||
|
||||
# Sleep in small increments for responsive shutdown
|
||||
sleep_remaining = self.interval
|
||||
while sleep_remaining > 0 and not self.quit_flag.is_set() and self._running:
|
||||
sleep_time = min(0.1, sleep_remaining)
|
||||
self.quit_flag.wait(sleep_time)
|
||||
sleep_remaining -= sleep_time
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Graceful shutdown: signal the thread to stop and wait for it.
|
||||
"""
|
||||
self._running = False
|
||||
self.quit_flag.set()
|
||||
# Wait for the thread to finish, with a generous timeout
|
||||
# Use a longer timeout to account for potential delays
|
||||
if self.is_alive():
|
||||
self.join(timeout=5.0) # Wait up to 5 seconds
|
||||
if self.is_alive():
|
||||
print("Warning: TemperatureMonitor thread did not stop gracefully")
|
||||
# Thread is still running, but we can't force stop it
|
||||
# The next start will check if it's alive and handle it
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Clear the collected data and start fresh.
|
||||
"""
|
||||
with self._lock:
|
||||
self.result = TemperatureResult()
|
||||
self.result.xlabel = "Time (s)"
|
||||
self.result.ylabel = "Temperature (C)"
|
||||
self.result.description = {"type": "temperature_monitoring"}
|
||||
|
||||
def get_current_temperature(self) -> float:
|
||||
"""
|
||||
Get the latest temperature reading.
|
||||
|
||||
Returns:
|
||||
float: Latest temperature in Celsius, or 0.0 if no data.
|
||||
"""
|
||||
with self._lock:
|
||||
if hasattr(self.result, 'y') and len(self.result.y) > 0:
|
||||
return self.result.y[-1]
|
||||
return 0.0
|
||||
|
||||
def get_current_setpoint(self) -> float:
|
||||
"""
|
||||
Get the latest setpoint reading.
|
||||
|
||||
Returns:
|
||||
float: Latest setpoint in Celsius, or 0.0 if no data.
|
||||
"""
|
||||
with self._lock:
|
||||
if hasattr(self.result, 'setpoint') and len(self.result.setpoint) > 0:
|
||||
return self.result.setpoint[-1]
|
||||
return 0.0
|
||||
|
||||
def is_running(self) -> bool:
|
||||
"""
|
||||
Check if the monitor is currently running.
|
||||
|
||||
Returns:
|
||||
bool: True if the monitor thread is active.
|
||||
"""
|
||||
return self._running and not self.quit_flag.is_set()
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Read experiment temperature history from the local tempproxy service.
|
||||
|
||||
Example:
|
||||
client = TemperatureProxyClient()
|
||||
temperature = client.get_history(start_epoch, end_epoch)
|
||||
"""
|
||||
|
||||
import http.client
|
||||
import json
|
||||
import logging
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import urlopen
|
||||
|
||||
from damaris.data.Temperature import TemperatureResult
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TemperatureProxyClient:
|
||||
"""Retrieve experiment-bounded readings from ``localhost:8000``."""
|
||||
|
||||
URL = "http://localhost:8000/v1/readings"
|
||||
TIMEOUT = 5
|
||||
|
||||
def get_history(self, start_epoch: float, end_epoch: float) -> TemperatureResult | None:
|
||||
"""Return temperature history relative to the experiment start, or ``None``.
|
||||
|
||||
Readings retain the proxy's ``pv`` units; an empty result or failed
|
||||
request returns ``None``.
|
||||
|
||||
Example:
|
||||
temperature = TemperatureProxyClient().get_history(1000.0, 1060.0)
|
||||
"""
|
||||
query = urlencode({"start": start_epoch, "end": end_epoch})
|
||||
url = f"{self.URL}?{query}"
|
||||
|
||||
try:
|
||||
with urlopen(url, timeout=self.TIMEOUT) as response:
|
||||
payload = json.loads(response.read())
|
||||
|
||||
readings = payload["readings"]
|
||||
if not isinstance(readings, list):
|
||||
raise ValueError("response 'readings' must be a list")
|
||||
if not readings:
|
||||
return None
|
||||
|
||||
x = [float(reading["t"]) - start_epoch for reading in readings]
|
||||
y = [float(reading["pv"]) for reading in readings]
|
||||
return TemperatureResult(x=x, y=y)
|
||||
except (OSError, http.client.HTTPException, ValueError, TypeError, KeyError) as error:
|
||||
logger.warning("Unable to retrieve temperature history from tempproxy: %s", error)
|
||||
return None
|
||||
Reference in new issue
Block a user