Replace temperature monitoring with proxy history

This commit is contained in:
markusro committed 2026-10-21 00:08:26 +02:00
1 parent bd060dc4d0
commit 0e796a800b
11 files changed
+170 -1124

No files matched your search

+12 -297
View File
@@ -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 )
-199
View File
@@ -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()
+53
View File
@@ -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