Author SHA1 Message Date
markusro 75863ffc19 fixed lakeshore to keep using the simple ASCII commands
Build Debian Packages / build (trixie, debian13) (pull_request) Successful in 14m22s
Build Debian Packages / build (bookworm, debian12) (pull_request) Successful in 14m24s
Build Debian Packages / build (bullseye, debian11) (pull_request) Has been cancelled
2026-07-12 20:15:50 +02:00
markusro 6f0b090d9d added lakeshore to temperature logging facility 2026-07-12 20:09:07 +02:00
markusro d9f2cc3f2c added lakeshore temperature controller 2026-07-12 20:00:39 +02:00
markusro e9e3f5a5f5 write Temperature data to hdf
Build Debian Packages / build (bookworm, debian12) (pull_request) Successful in 14m12s
Build Debian Packages / build (trixie, debian13) (pull_request) Successful in 14m18s
Build Debian Packages / build (bullseye, debian11) (pull_request) Has been cancelled
2026-07-12 13:14:05 +02:00
markusro 9441b5a4d9 added ignore 2026-07-12 13:00:14 +02:00
markusro 7f29f2d173 fix: replace ComboBoxText.set_active_text() with model-based lookup
Build Debian Packages / build (trixie, debian13) (push) Successful in 13m8s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 13m13s
Build Debian Packages / build (trixie, debian13) (pull_request) Successful in 13m14s
Build Debian Packages / build (bookworm, debian12) (pull_request) Successful in 13m53s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
Build Debian Packages / build (bullseye, debian11) (pull_request) Has been cancelled
set_active_text() does not exist in GTK 3.24.x (only in GTK 4.x).
When loading a saved config with temperature_controller_type='simulated',
the set() method crashed on startup with AttributeError.

Added _set_combo_active_text() helper that iterates the model to find
the matching entry and calls set_active(index) instead.
2026-07-12 10:11:58 +02:00
markusro 847ab4d89c working prototype: stopping with experiment, display data 2026-07-12 02:16:57 +02:00
markusro d6da2f2bda fix thread locking when reading temeprature 2026-07-11 22:18:08 +02:00
markusro 5338304ee8 added POC for temperature logging 2026-07-11 22:11:03 +02:00
markusro d48f96e498 added scipy and numpy to pyproject.toml, clarified dependencies when using uv run DAMARIS3 2026-07-11 22:08:17 +02:00
markusro 058716f3c0 merged sequential writing
Build Debian Packages / build (trixie, debian13) (push) Successful in 13m30s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 14m7s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-09 16:53:31 +02:00
markusro 6d9f55f788 implemented incremental HDF5 writing (Fixes #33) 2026-07-09 16:49:51 +02:00
markusro e8d0e45bfb doc string
Build Debian Packages / build (trixie, debian13) (push) Successful in 12m51s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 13m2s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-09 16:42:03 +02:00
markusro 08e1fe2a1a Small fixes
Merge branch 'main' of gitea.pkm.physik.tu-darmstadt.de:IPKM/python3-damaris
2026-07-09 14:45:28 +02:00
markusro 064036241b small fixes 2026-07-09 14:45:25 +02:00
markusro 97aa99c239 more files to ignore
Build Debian Packages / build (trixie, debian13) (push) Successful in 13m34s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 13m35s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-06 21:01:54 +02:00
markusro f26d203e79 ignore debian/files 2026-07-06 21:01:19 +02:00
markusro 594c0ee8d4 clean up residual gui files
Build Debian Packages / build (trixie, debian13) (push) Successful in 15m18s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 15m11s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-06 20:56:17 +02:00
markusro 68da261ab4 remove load successful message from hdf5 loader 2026-07-06 20:48:21 +02:00
markusro 00466cb559 Merge pull request 'Hdfreader' (#31) from hdfreader into main
Build Debian Packages / build (trixie, debian13) (push) Successful in 13m56s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 14m24s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
Reviewed-on: #31
2026-07-06 18:45:46 +00:00
markusro 26970ba201 clean up write_to_csv for MeasurmentResult (Fixs #30)
Build Debian Packages / build (trixie, debian13) (push) Successful in 13m56s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 14m7s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-06 16:55:49 +02:00
markusro eab49db8d4 gtksourceview: minimum-word-size needs to be >= 2 otherwise a warning is triggered. (Fixes #28)
Build Debian Packages / build (trixie, debian13) (push) Successful in 13m56s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 14m3s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-06 16:10:56 +02:00
20 changed files with 1329 additions and 4384 deletions

No files matched your search

+5
View File
@@ -6,3 +6,8 @@ __pycache__
*.egg-info
*.whl
.idea
debian/files
.pybuild
.junie
debian/debhelper-build-stamp
debian/.debhelper
+4 -4
View File
@@ -1,6 +1,6 @@
include src/gui/DAMARIS3.png
include src/gui/DAMARIS3.ico
include src/gui/damaris.glade
include src/gui/damaris.gladep
include src/damaris/gui/DAMARIS3.png
include src/damaris/gui/DAMARIS3.ico
include src/damaris/gui/damaris3.glade
include src/damaris/gui/python.xml
#recursive-include doc/reference-html *.html *.gif *.png *.css
#recursive-include doc/tutorial-html *.html *.gif *.png *.css *.tar.gz *.sh
+9
View File
@@ -15,3 +15,12 @@ Alternatively you can install withoout the system-site-packages:
pip install -e .
This will download newer packages and install them in the venv.
Using uv
========
Dependicies needed:
sudo apt install libgirepository1.0-dev pkg-config
Then you can start DAMARIS3:
uv run DAMARIS3
-2
View File
@@ -1,2 +0,0 @@
python3-damaris_0.20_all.deb science optional
python3-damaris_0.20_amd64.buildinfo science optional
+5 -4
View File
@@ -14,12 +14,13 @@ license = {text = "GPL-2.0"}
keywords = ["NMR", "data-processing"]
readme = {file = "README", content-type = "text/plain"}
dependencies = [
"scipy >= 0.14.0",
"numpy >= 1.8.2, < 2.0",
"scipy>=0.14.0",
"numpy>=1.8.2,<2.0",
"tables >= 3.1.1",
"matplotlib >= 1.4.1, <= 3.6",
"pyxdg",
"PyGObject >= 3.14.0, <= 3.51.0"
"PyGObject >= 3.14.0, <= 3.51.0",
"pyserial>=3.5",
]
authors = [
{name = "Achim Gädke"},
@@ -39,7 +40,7 @@ DAMARIS3 = "damaris.__main__:main"
where = ["src"]
[tool.setuptools.package-data]
"damaris.gui" = ["DAMARIS3.png", "DAMARIS3.ico", "damaris.xml", "python.xml", "damaris3.glade"]
"damaris.gui" = ["DAMARIS3.png", "DAMARIS3.ico", "python.xml", "damaris3.glade"]
[tool.setuptools.dynamic]
version = {attr = "damaris.__version__"}
+4 -4
View File
@@ -989,14 +989,14 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
self.lock.acquire()
if self.sampling_rate != other.get_sampling_rate():
raise ValueError("Accumulation: You cant add ADC-Results with diffrent sampling-rates")
raise ValueError("Accumulation: You can't add ADC-Results with different sampling-rates")
if len(self.y[0]) != len(other):
raise ValueError("Accumulation: You cant add ADC-Results with diffrent number of samples")
raise ValueError("Accumulation: You can't add ADC-Results with different number of samples")
if len(self.y) != other.get_number_of_channels():
raise ValueError("Accumulation: You cant add ADC-Results with diffrent number of channels")
raise ValueError("Accumulation: You can't add ADC-Results with different number of channels")
for i in range(len(self.index)):
if self.index[i] != other.get_index_bounds(i):
raise ValueError("Accumulation: You cant add ADC-Results with diffrent indexing")
raise ValueError("Accumulation: You can't add ADC-Results with different indexing")
for i in range(self.get_number_of_channels()):
self.y[i] += other.y[i]
+98 -12
View File
@@ -49,6 +49,7 @@ class DataPool(collections.abc.MutableMapping):
self.__mydict={}
self.__dictlock=threading.Lock()
self.__registered_listeners=[]
self.__dirty_keys=set()
def __getitem__(self, name):
try:
@@ -65,6 +66,7 @@ class DataPool(collections.abc.MutableMapping):
else:
e=DataPool.Event(DataPool.Event.new_key, name,self)
self.__mydict[name]=value
self.__dirty_keys.add(name)
finally:
self.__dictlock.release()
self.__send_event(e)
@@ -74,6 +76,7 @@ class DataPool(collections.abc.MutableMapping):
try:
self.__dictlock.acquire()
del self.__mydict[name]
self.__dirty_keys.add(name)
finally:
self.__dictlock.release()
self.__send_event(DataPool.Event(DataPool.Event.deleted_key,name,self))
@@ -107,7 +110,54 @@ class DataPool(collections.abc.MutableMapping):
self.__send_event(DataPool.Event(DataPool.Event.destroy))
self.__registered_listeners=None
def write_hdf5(self,hdffile,where="/",name="data_pool", complib=None, complevel=None):
def write_hdf5(self,hdffile,where="/",name="data_pool", complib=None, complevel=None, incremental=False):
"""
Writes the content of a DataPool object to an HDF5 file using the `tables` library.
This method serializes the entire content of the data pool into an HDF5 file, creating
appropriate groups and arrays based on the keys and values stored in the data pool.
Nested storage paths are supported, and group or array names are
converted into valid HDF5 names. Paths are prepended with "dir_", results with "dict_".
If duplicate names arise, extensions (like `_001`, `_002`)
are appended to ensure uniqueness.
Parameters:
hdffile : str | bytes | tables.File
Path to the HDF5 file or an already open `tables.File` object where the
data should be written. Strings or bytes are interpreted as file paths.
where : str, optional
The root HDF5 group path under which the data pool will be stored.
Defaults to "/".
name : str, optional
The name of the root HDF5 group under which the data pool is saved.
Defaults to "data_pool".
complib : str, optional
Compression library to be used when creating HDF5 arrays. Optional, defaults
to None, meaning no compression is applied.
complevel : int, optional
Compression level to use when applying compression. Has no effect if `complib`
is set to None.
Raises:
Exception
If `hdffile` is neither a string nor a valid `tables.File` object.
Notes:
- This method assumes that the content of the data pool does not change during the
write operation.
- Keys in the data pool starting with "__" (double underscores) are skipped.
- If a key contains nested paths (e.g., "group1/subgroup2/item"), intermediate
groups are created as necessary.
- Group or array names are converted to valid HDF5 names, and name conflicts
are resolved by appending extensions (e.g., "_001").
- Errors during the writing process are logged to the console, and traceback
information is displayed for debugging purposes.
"""
if isinstance(hdffile, (bytes, str)):
dump_file=tables.open_file(hdffile, mode="a")
elif isinstance(hdffile,tables.File):
@@ -115,10 +165,19 @@ class DataPool(collections.abc.MutableMapping):
else:
raise Exception("expecting hdffile or string")
dump_group=dump_file.create_group(where, name, "DAMARIS data pool")
if where in dump_file and name in dump_file.get_node(where):
dump_group = dump_file.get_node(where, name)
else:
dump_group = dump_file.create_group(where, name, "DAMARIS data pool")
self.__dictlock.acquire()
dict_keys=list(self.__mydict.keys())
if incremental:
dict_keys=list(self.__dirty_keys)
else:
# Full sync: current keys + any other dirty keys (e.g. deletions)
dict_keys=list(set(self.__mydict.keys()) | self.__dirty_keys)
self.__dictlock.release()
try:
for key in dict_keys:
if key.startswith("__"):
@@ -143,18 +202,34 @@ class DataPool(collections.abc.MutableMapping):
# convert last part of key to a valid name
group_keyname="dict_"+str(namelist[-1]).translate(DataPool.translation_table)
# avoid double names by adding number extension
# find existing node for this key
if group_keyname in dump_dir:
extension_count=0
while group_keyname+"_%03d"%extension_count in dump_dir:
extension_count+=1
group_keyname+="_%03d"%extension_count
if dump_dir._v_children[group_keyname]._v_title == key:
dump_file.remove_node(dump_dir, group_keyname, recursive=True)
else:
extension_count = 0
while group_keyname + "_%03d" % extension_count in dump_dir:
suffix_name = group_keyname + "_%03d" % extension_count
if dump_dir._v_children[suffix_name]._v_title == key:
dump_file.remove_node(dump_dir, suffix_name, recursive=True)
group_keyname = suffix_name
break
extension_count += 1
else:
# truly new key with collision
group_keyname += "_%03d" % extension_count
self.__dictlock.acquire()
if key not in self.__mydict:
# outdated ...
# Key was deleted
if key in self.__dirty_keys:
self.__dirty_keys.remove(key)
self.__dictlock.release()
continue
value=self.__mydict[key]
if key in self.__dirty_keys:
self.__dirty_keys.remove(key)
self.__dictlock.release()
# now write data, assuming, the object is constant during write operation
if hasattr(value, "write_to_hdf"):
@@ -194,7 +269,7 @@ class DataPool(collections.abc.MutableMapping):
def read_from_hdf(self, hdffile, where="/data_pool"):
"""
Read data from HDF5 file and populate the DataPool.
Read specified data from HDF5 file and populate the DataPool.
Parameters:
- hdffile: HDF5 file object or filename (bytes/str)
@@ -220,6 +295,11 @@ class DataPool(collections.abc.MutableMapping):
# Recursively process the group structure
self._read_hdf_group(data_pool_group, "")
# Clear dirty keys as they are now in sync with HDF5
self.__dictlock.acquire()
self.__dirty_keys.clear()
self.__dictlock.release()
finally:
if close_file:
hdf_file.close()
@@ -285,7 +365,13 @@ class DataPool(collections.abc.MutableMapping):
obj = read_from_hdf(node)
if obj is not None:
self[full_path] = obj
elif damaris_type == "TemperatureResult":
from damaris.data.Temperature import read_from_hdf
obj = read_from_hdf(node)
if obj is not None:
self[full_path] = obj
# Skip further processing of this group since we handled it
continue
@@ -337,7 +423,7 @@ class DataPool(collections.abc.MutableMapping):
@classmethod
def load_hdf5(cls, filename):
"""
Load a DAMARIS HDF5 file and return DataPool and metadata.
Load a complete DAMARIS HDF5 file and return DataPool and metadata, including scripts.
Parameters:
- filename: Path to HDF5 file
+5 -3
View File
@@ -193,7 +193,7 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
def uses_statistics(self):
"""
drawable interface method, returns True
drawable interface method returns True
"""
return True
@@ -203,10 +203,11 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
destination can be a file or a filename
suitable for further processing
"""
# write sorted
the_destination=destination
file_opened = False
if type(destination) in (str,):
the_destination=open(destination, "w")
file_opened = True
the_destination.write("# quantity:"+str(self.quantity_name)+"\n")
the_destination.write("# x y ysigma n\n")
@@ -222,7 +223,8 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
y.mean_error(),
delimiter,
y.n))
the_destination=None
if file_opened:
the_destination.close()
def write_to_hdf(self, hdffile, where, name, title, complib=None, complevel=None):
+218 -8
View File
@@ -2,6 +2,9 @@
from .Resultable import Resultable
from .Drawable import Drawable
import numpy
import threading
import tables
#############################################################################
# #
@@ -15,17 +18,224 @@ from .Drawable import Drawable
class TemperatureResult(Resultable, Drawable):
"""
Specialised class of Resultable and Drawable
Contains recorded temperature data
Contains recorded temperature data.
Attributes:
x: List of timestamps in seconds.
y: List of temperature readings in Celsius.
setpoint: Optional list of setpoint temperatures in Celsius.
xlabel: Label for x-axis (default: "Time (s)").
ylabel: Label for y-axis (default: "Temperature (C)").
"""
def __init__(self, x = None, y = None, desc = None, job_id = None, job_date = None):
def __init__(self, x = None, y = None, setpoint = None, desc = None, job_id = None, job_date = None):
Resultable.__init__(self)
Drawable.__init__(self)
# Set default labels
self.xlabel = "Time (s)"
self.ylabel = "Temperature (C)"
# Initialize data lists
self.x = [] if x is None else x
self.y = [] if y is None else y
self.setpoint = [] if setpoint is None else setpoint
# Set metadata if provided
if desc is not None:
self.description = desc
if job_id is not None:
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) 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) as numpy array."""
if channel == 0:
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 write_to_hdf(self, hdffile, where, name, title, complib=None, complevel=None):
"""
Write the temperature result to an HDF5 group.
if (x is None) and (y is None) and (desc is None) and (job_id is None) and (job_date is None):
pass
Stores x (timestamps), y (temperature readings), and setpoint arrays
as an HDF5 table, with metadata saved as group attributes.
"""
h5_table_format = {
"x": tables.Float64Col(),
"y": tables.Float64Col(),
"setpoint": tables.Float64Col(),
}
filter = None
if complib is not None:
if complevel is None:
complevel = 9
filter = tables.Filters(complevel=complevel, complib=complib, shuffle=1)
elif (x is not None) and (y is not None) and (desc is not None) and (job_id is not None) and (job_date is not None):
pass
with self.__listener_lock:
xdata = numpy.array(self.x, dtype=float) if self.x else numpy.array([], dtype=float)
ydata = numpy.array(self.y, dtype=float) if self.y else numpy.array([], dtype=float)
spdata = numpy.array(self.setpoint, dtype=float) if self.setpoint else numpy.array([], dtype=float)
else:
raise ValueError("Wrong usage of __init__!")
# Use the length of the longest array; shorter ones are padded with 0
n = max(len(xdata), len(ydata), 1)
if len(xdata) < n:
xdata = numpy.pad(xdata, (0, n - len(xdata)), constant_values=0.0)
if len(ydata) < n:
ydata = numpy.pad(ydata, (0, n - len(ydata)), constant_values=0.0)
if len(spdata) < n:
spdata = numpy.pad(spdata, (0, n - len(spdata)), constant_values=0.0)
temp_table = hdffile.create_table(
where=where, name=name,
description=h5_table_format,
title=title,
filters=filter,
expectedrows=n,
)
temp_table.flavor = "numpy"
temp_table.attrs.damaris_type = "TemperatureResult"
temp_table.attrs.xlabel = self.xlabel
temp_table.attrs.ylabel = self.ylabel
if hasattr(self, "job_id") and self.job_id is not None:
temp_table.attrs.job_id = self.job_id
if hasattr(self, "job_date") and self.job_date is not None:
temp_table.attrs.time = "%04d%02d%02d %02d:%02d:%02d.%03d" % (
self.job_date.year,
self.job_date.month,
self.job_date.day,
self.job_date.hour,
self.job_date.minute,
self.job_date.second,
self.job_date.microsecond // 1000,
)
if hasattr(self, "description") and self.description is not None:
for (key, value) in self.description.items():
temp_table.attrs.__setattr__("description_" + key, str(value))
row = temp_table.row
for i in range(n):
row["x"] = xdata[i]
row["y"] = ydata[i]
row["setpoint"] = spdata[i]
row.append()
temp_table.flush()
def __repr__(self):
"""String representation of the temperature result."""
return f"TemperatureResult(points={len(self)}, job_id={self.job_id})"
def read_from_hdf(hdf_node):
"""
Read a TemperatureResult object from an HDF5 table node.
Returns None if the node is not suitable.
"""
if not isinstance(hdf_node, tables.Table):
return None
if hdf_node._v_attrs.damaris_type != "TemperatureResult":
return None
tr = TemperatureResult()
tr.xlabel = getattr(hdf_node._v_attrs, "xlabel", "Time (s)")
tr.ylabel = getattr(hdf_node._v_attrs, "ylabel", "Temperature (C)")
for r in hdf_node.iterrows():
tr.x.append(r["x"])
tr.y.append(r["y"])
sp = r["setpoint"]
if sp != 0.0 or len(tr.setpoint) > 0:
tr.setpoint.append(sp)
return tr
+1 -1
View File
@@ -6,5 +6,5 @@ from damaris.data.Error_Result import Error_Result
from damaris.data.Config_Result import Config_Result
from damaris.data.Temperature import TemperatureResult
__all__=["ADC_Result", "Accumulation", "MeasurementResult", "AccumulatedValue", "DataPool", "FFT", "Error_Result", "Config_Result", "TemperatureResult" ]
__all__=["ADC_Result", "Accumulation", "MeasurementResult", "AccumulatedValue", "DataPool", "Error_Result", "Config_Result", "TemperatureResult" ]
+309 -29
View File
@@ -61,7 +61,8 @@ from damaris.gui import ResultReader, ResultHandling
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
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
@@ -88,7 +89,6 @@ class logstream:
def __del__( self ):
pass
global log
log = logstream( )
sys.stdout = log
@@ -134,9 +134,6 @@ class LockFile:
return False
class DamarisGUI:
ExpScript_Display = 0
ResScript_Display = 1
@@ -184,6 +181,10 @@ class DamarisGUI:
#to stop queued experiments
self.stop_experiment_flag = threading.Event()
# Temperature monitoring
self.temp_monitor = None
self.temp_controller = None
exp_script = ""
if exp_script_filename is not None and exp_script_filename != "":
@@ -516,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]
@@ -576,6 +579,7 @@ class DamarisGUI:
self.dump_filename = ""
if actual_config[ "data_pool_name" ] != "":
self.dump_states( init=True )
gobject.timeout_add( 200, self.observe_running_experiment )
def observe_running_experiment( self ):
@@ -688,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...
@@ -878,15 +884,6 @@ class DamarisGUI:
filters=tables.Filters( complevel=9, complib='zlib' ) )
if dump_file is None and os.path.isfile( self.dump_filename ) and tables.is_pytables_file( self.dump_filename ):
# take some data from dump file and repack
os.rename( self.dump_filename, self.dump_filename + ".bak" )
old_dump_file = tables.open_file( self.dump_filename + ".bak", mode="r+" )
if "data_pool" in old_dump_file.root:
old_dump_file.remove_node( where="/", name="data_pool", recursive=True )
old_dump_file.copy_file( self.dump_filename )
old_dump_file.close( )
del old_dump_file
os.remove( self.dump_filename + ".bak" )
# prepare for update
dump_file = tables.open_file( self.dump_filename, mode="r+" )
@@ -903,7 +900,8 @@ class DamarisGUI:
# save the data!
self.data.write_hdf5( dump_file, where="/", name="data_pool",
complib=self.dump_complib, complevel=self.dump_complevel )
complib=self.dump_complib, complevel=self.dump_complevel,
incremental=not init )
# now save additional information
e = self.si.data.get( "__recentexperiment", -1 ) + 1
@@ -986,8 +984,98 @@ class DamarisGUI:
still_running = [_f for _f in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver ] if _f]
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:
print("Temperature monitoring: No device configured for Eurotherm")
return
self.temp_controller = create_controller(
"eurotherm", temp_device, baudrate=temp_baudrate
)
print(f"Temperature monitoring started with Eurotherm on {temp_device} (interval: {temp_interval}s)")
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")
print(f"Temperature monitoring started with simulated controller (interval: {temp_interval}s)")
else:
print(f"Temperature monitoring: Unknown controller type: {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:
print(f"Failed to start temperature monitoring: {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()
print("Temperature monitoring stopped")
except Exception as e:
print(f"Error stopping temperature monitoring: {e}")
finally:
self.temp_monitor = None
if self.temp_controller is not None:
try:
self.temp_controller.close()
except Exception as e:
print(f"Error closing temperature controller: {e}")
finally:
self.temp_controller = None
def documentation_init( self ):
self.doc_urls = {
@@ -1184,7 +1272,7 @@ get_ylabel set_ylabel"""
self.compwgen = gtksourceview2.CompletionWords.new("Keywords", None)
self.compwgen.register(self.keywordsbuffer)
self.compwgen.set_property("minimum-word-size", 1)
self.compwgen.set_property("minimum-word-size", 2)
self.compwexp = gtksourceview2.CompletionWords.new("Words", None)
self.compwexp.register(self.experiment_script_textbuffer)
@@ -1698,15 +1786,6 @@ get_ylabel set_ylabel"""
if exp_script or res_script:
script_widget.set_scripts(exp_script, res_script)
# Show success message
success_dialog = gtk.MessageDialog(parent=script_widget.xml_gui.get_object( "main_window"),
type=gtk.MessageType.INFO,
flags=gtk.DialogFlags.MODAL,
buttons=("_OK", gtk.ResponseType.OK))
success_dialog.set_markup("Successfully loaded data from %s" % os.path.basename(file_name))
success_dialog.run()
success_dialog.destroy()
except Exception as e:
error_dialog = gtk.MessageDialog(parent=script_widget.xml_gui.get_object( "main_window"),
type=gtk.MessageType.ERROR,
@@ -2080,6 +2159,73 @@ 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.HBox(homogeneous=False, 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.HBox(homogeneous=False, 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_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.HBox(homogeneous=False, 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_hbox = gtk.HBox(homogeneous=False, 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.HBox(homogeneous=False, 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
@@ -2186,10 +2332,28 @@ 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()
"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_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:
@@ -2221,6 +2385,41 @@ pygobject version %(pygobject)s
ADC_Result.default_bit_depth = int(config["adc_bit_depth"])
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( )
@@ -2237,6 +2436,84 @@ 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,
buttons=(
gtk.STOCK_CANCEL, gtk.ResponseType.CANCEL,
gtk.STOCK_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)):
iter = model.iter_nth_child(None, i)
if iter and model.get(iter, 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 )
@@ -2455,7 +2732,9 @@ class MonitorWidgets:
# now let's add a button to the toolbar
self.matplot_cursor_button = gtk.ToggleButton(label='')
try:
image = gtk.Image.new_from_file("/usr/share/icons/breeze/actions/24/crosshairs-symbolic.svg")
icon_theme = gtk.IconTheme.get_default()
icon = icon_theme.load_icon("crosshairs", 24, 0)
image = gtk.Image.new_from_pixbuf(icon)
self.matplot_cursor_button.set_image(image)
except:
pass
@@ -2935,6 +3214,7 @@ class MonitorWidgets:
self.matplot_cursor = matplotlib.widgets.Cursor(self.matplot_axes,
useblit=False, color='red', linewidth=1)
self.matplot_canvas.draw()
# TODO: Deactivate the pane button
else:
# Deactivate cursor
@@ -3062,7 +3342,7 @@ class MonitorWidgets:
x_scale = self.display_x_scaling_combobox.get_active_text()
y_scale = self.display_y_scaling_combobox.get_active_text()
print(x_scale)
if x_scale == "lin":
self.matplot_axes.set_xscale("linear")
elif x_scale == "log10":
@@ -3133,7 +3413,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( )
+206
View File
@@ -0,0 +1,206 @@
# -*- 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
# 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):
"""
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()
# 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 in GTK thread (if GTK available)
gdk = _get_gdk()
if gdk is not None:
gdk.threads_enter()
try:
self.data_pool[self.data_key] = data_pool_result
finally:
gdk.threads_leave()
else:
self.data_pool[self.data_key] = data_pool_result
except Exception as e:
print(f"TemperatureMonitor error: {e}")
import traceback
traceback.print_exc()
# 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()
File diff suppressed because it is too large. Load diff
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
<!DOCTYPE glade-project SYSTEM "http://glade.gnome.org/glade-project-2.0.dtd">
<glade-project>
<name>damaris-gui</name>
<program_name>damaris</program_name>
<gnome_support>FALSE</gnome_support>
</glade-project>
File diff suppressed because it is too large. Load diff
+39 -1
View File
@@ -3,6 +3,8 @@ import re
import operator
from functools import reduce
from .temperature import TemperatureController, register_controller
DEBUG=False
@@ -40,7 +42,7 @@ def checksum(message):
bcc = (reduce(operator.xor, list(map(ord,message))))
return chr(bcc)
class Eurotherm(object):
class Eurotherm(TemperatureController):
def __init__(self, serial_device, baudrate = 19200):
self.device = standard_device
# timeout: 110 ms to get all answers.
@@ -100,6 +102,39 @@ class Eurotherm(object):
def get_setpoint_temperature(self):
return self.read_param('SL')
# TemperatureController interface implementation
def get_temperature(self) -> float:
"""Read current temperature in Celsius."""
temp = self.read_param('PV')
if temp is None:
return 0.0
try:
return float(temp)
except (ValueError, TypeError):
return 0.0
def get_setpoint(self) -> float:
"""Read target setpoint temperature in Celsius."""
temp = self.read_param('SL')
if temp is None:
return 0.0
try:
return float(temp)
except (ValueError, TypeError):
return 0.0
def set_setpoint(self, temperature: float) -> bool:
"""Set target temperature in Celsius."""
return self.write_param('SL', str(temperature))
def close(self):
"""Close the serial connection."""
if hasattr(self, 's') and self.s is not None:
if self.s.is_open:
self.s.close()
self.s = None
if __name__ == '__main__':
import time
delta=5
@@ -118,3 +153,6 @@ if __name__ == '__main__':
f.write(l)
f.flush()
f.write('# MARK -- %s --\n'%(time.asctime()))
# Register Eurotherm with the factory
register_controller('eurotherm', Eurotherm)
+149
View File
@@ -0,0 +1,149 @@
import serial
import re
import numpy as np
from .temperature import TemperatureController, register_controller
DEBUG = False
reply_pattern = re.compile(r"\x02..(.*)\x03.", re.DOTALL)
# example answer '\x02PV279.8\x03/'
# [EOT] = \x04
# [STX] = \x02
# [ENQ] = \x05
# [ETX] = \x03
# [ACK] = \x06
# BCC = checksum
standard_device = '/dev/ttyUSB0'
"""
Parameter read example:
Master: [EOT]0011PV[ENQ]
Instrument: [STX]PV16.4[ETX]{BCC}
Writing data:
Master: [EOT] {GID}{GID}{UID}{UID}[STX]{CHAN}(c1)(c2)<DATA>[ETX](BCC)
"""
class Lakeshore(TemperatureController):
def __init__(self, serial_device, baudrate=57600):
self.device = standard_device
# timeout: 110 ms to get all answers.
self.s = serial.Serial(serial_device,
baudrate=baudrate,
bytesize=7,
parity='O',
stopbits=1,
timeout=0.4)
self._expect_len = 50
def __del__(self):
if hasattr(self, 's') and self.s is not None:
if self.s.is_open:
self.s.close()
#def get_current_temperature(self): # Temp A
# self.s.write("KRDG? a\n")
# a = self.s.readline().strip()[1:]
# return float(a)
def get_temperature_A(self): # Temp A
self.s.write("KRDG? a\n")
a = self.s.readline().strip()[1:]
return float(a)
def get_temperature_B(self): # Temp B
self.s.write("KRDG? b\n")
b = self.s.readline().strip()[1:]
return float(b)
def set_temperature(self, temperature): # setze Setpoint
t = temperature
self.s.write(f"SETP 1,{t}\n")
def set_ramp(self, ramp):
return self.s.write(f'RAMP 1, 1, {ramp}\n')
def get_set_temperature(self): # Setpoint abfrage
self.s.write("SETP? 1\n")
s = self.s.readline().strip()[1:]
return float(s)
def get_output(self): # Heater Output
self.s.write("HTR? 1\n")
o = self.s.readline().strip()[1:]
return float(o)
def get_ramp(self):
self.s.write('RAMP? 1\n')
ret = self.s.readline().strip()
onoff, rate = ret.split(',')
if onoff:
return float(rate)
else:
return 0.0
def read_param(self, param):
"""Read a parameter using simple ASCII commands."""
self.s.write(f"{param}?\n")
answer = self.s.readline().strip()
if answer and len(answer) > 1:
# Remove the first character (command echo) and return the value
return answer[1:]
return None
def get_value(self, name):
temp = self.read_param(name)
if temp is None:
return np.nan
else:
try:
return float(temp)
except ValueError:
return np.nan
# TemperatureController interface implementation
def get_temperature(self) -> float:
"""Read current temperature in Celsius."""
temp = self.read_param('KRDG')
if temp is None:
return 0.0
try:
return float(temp)
except (ValueError, TypeError):
return 0.0
def get_setpoint(self) -> float:
"""Read target setpoint temperature in Celsius."""
temp = self.read_param('SETP')
if temp is None:
return 0.0
try:
return float(temp)
except (ValueError, TypeError):
return 0.0
def set_setpoint(self, temperature: float) -> bool:
"""Set target temperature in Celsius."""
self.s.write(f"SETP {temperature}\n")
return True
def close(self):
"""Close the serial connection."""
if hasattr(self, 's') and self.s is not None:
if self.s.is_open:
self.s.close()
self.s = None
# Register Lakeshore with the factory
register_controller('lakeshore', Lakeshore)
+151
View File
@@ -0,0 +1,151 @@
# -*- coding: iso-8859-1 -*-
"""
Temperature controller interface and base classes.
This module provides a consistent interface for temperature controllers
to enable pluggable temperature monitoring systems.
"""
from abc import ABC, abstractmethod
# Plugin registry for temperature controllers
_controller_registry = {}
def register_controller(name: str, controller_class):
"""
Register a temperature controller implementation.
Args:
name: Unique identifier for this controller type
controller_class: Class that implements TemperatureController
"""
_controller_registry[name] = controller_class
def create_controller(name: str, *args, **kwargs):
"""
Create a temperature controller instance by name.
Args:
name: Registered controller type name
*args: Positional arguments to pass to controller constructor
**kwargs: Keyword arguments to pass to controller constructor
Returns:
TemperatureController: Instance of the requested controller
Raises:
ValueError: If controller name is not registered and cannot be imported
"""
# If not found, try to auto-import the module
if name not in _controller_registry:
# Map known controller names to their modules
module_map = {
'simulated': 'damaris.tools.temperature_simulator',
'eurotherm': 'damaris.tools.eurotherm',
'lakeshore': 'damaris.tools.lakeshore',
}
if name in module_map:
try:
# Import the module which will register the controller
import importlib
importlib.import_module(module_map[name])
except ImportError as e:
raise ValueError(f"Failed to import controller '{name}': {e}. Available: {list(_controller_registry.keys())}")
# Check again after import
if name not in _controller_registry:
raise ValueError(f"Unknown temperature controller: {name}. Available: {list(_controller_registry.keys())}")
return _controller_registry[name](*args, **kwargs)
def get_controller_names() -> list:
"""
Get list of available controller names.
Returns:
list: List of registered controller type names
"""
return list(_controller_registry.keys())
# Pre-register built-in controllers by importing their modules
# This ensures they're available without explicit imports
try:
from . import eurotherm
except ImportError as e:
# Eurotherm may require serial module which might not be installed
# That's OK, it will be imported on-demand if needed
pass
try:
from . import lakeshore
except ImportError as e:
# Lakeshore may require serial module which might not be installed
# That's OK, it will be imported on-demand if needed
pass
try:
from . import temperature_simulator
except ImportError as e:
# Simulator should always work (no external dependencies)
# But if it fails, it will be imported on-demand
pass
class TemperatureController(ABC):
"""
Abstract base class for temperature controllers.
All temperature controller implementations (Eurotherm, Lakeshore, etc.)
should inherit from this class and implement the abstract methods.
"""
@abstractmethod
def get_temperature(self) -> float:
"""
Read current temperature in Celsius.
Returns:
float: Current temperature in degrees Celsius.
Raises:
IOError: If communication with the device fails.
RuntimeError: If the device is not ready or returns invalid data.
"""
pass
@abstractmethod
def get_setpoint(self) -> float:
"""
Read the target setpoint temperature in Celsius.
Returns:
float: Target temperature in degrees Celsius.
"""
pass
@abstractmethod
def set_setpoint(self, temperature: float) -> bool:
"""
Set the target temperature setpoint.
Args:
temperature: Target temperature in degrees Celsius.
Returns:
bool: True if the setpoint was successfully set, False otherwise.
"""
pass
def close(self):
"""
Clean up resources (e.g., close serial connections).
This method should be called when the controller is no longer needed.
The default implementation does nothing; subclasses should override
if they have resources to clean up.
"""
pass
+126
View File
@@ -0,0 +1,126 @@
# -*- coding: iso-8859-1 -*-
"""
Simulated temperature controller for testing.
Provides a temperature controller that simulates real temperature readings
with configurable parameters including sinusoidal variation, noise, and missing values.
"""
import time
import math
import random
import threading
from .temperature import TemperatureController, register_controller
class SimulatedTemperatureController(TemperatureController):
"""
A simulated temperature controller that generates realistic temperature readings.
This controller is useful for testing and development without requiring
physical hardware. It simulates temperature readings with:
- Base temperature around 290K (17C)
- Sinusoidal variation with amplitude of 10K
- 5% random noise
- 10% probability of missing values (returns previous value)
- 60 second period for the sine wave
Attributes:
base_temp: Base temperature in Kelvin (default: 290.0)
amplitude: Amplitude of sinusoidal variation in Kelvin (default: 10.0)
period: Period of sine wave in seconds (default: 60.0)
noise_level: Fraction of random noise (default: 0.05 = 5%)
missing_probability: Probability of missing value (default: 0.10 = 10%)
"""
def __init__(self, base_temp: float = 290.0, amplitude: float = 10.0,
period: float = 60.0, noise_level: float = 0.05,
missing_probability: float = 0.10):
"""
Initialize the simulated temperature controller.
Args:
base_temp: Base temperature in Kelvin (default: 290.0)
amplitude: Amplitude of sinusoidal variation in Kelvin (default: 10.0)
period: Period of sine wave in seconds (default: 60.0)
noise_level: Fraction of random noise (default: 0.05 = 5%)
missing_probability: Probability of missing value (default: 0.10 = 10%)
"""
self.base_temp = float(base_temp)
self.amplitude = float(amplitude)
self.period = float(period)
self.noise_level = float(noise_level)
self.missing_probability = float(missing_probability)
self._start_time = time.time()
self._lock = threading.RLock()
self._setpoint = self.base_temp
self._last_temp = self.base_temp
self._last_reading_time = self._start_time
def get_temperature(self) -> float:
"""
Get simulated current temperature in Kelvin.
Returns:
float: Simulated temperature in Kelvin
"""
with self._lock:
elapsed = time.time() - self._start_time
current_time = time.time()
# Calculate ideal sinusoidal temperature
temp = self.base_temp + self.amplitude * math.sin(2 * math.pi * elapsed / self.period)
# Add 5% random noise
noise = temp * self.noise_level * (random.random() * 2 - 1) # -5% to +5%
temp += noise
# 10% chance of missing value (return last good value)
if random.random() < self.missing_probability:
temp = self._last_temp
else:
self._last_temp = temp
self._last_reading_time = current_time
return temp
def get_setpoint(self) -> float:
"""
Get the target setpoint temperature in Kelvin.
Returns:
float: Target temperature in Kelvin
"""
with self._lock:
return self._setpoint
def set_setpoint(self, temperature: float) -> bool:
"""
Set the target temperature setpoint.
Args:
temperature: Target temperature in Kelvin
Returns:
bool: True if successful, False otherwise
"""
with self._lock:
self._setpoint = float(temperature)
return True
def close(self):
"""Clean up resources."""
pass
def reset(self):
"""Reset the simulation to initial state."""
with self._lock:
self._start_time = time.time()
self._last_temp = self.base_temp
self._last_reading_time = self._start_time
# Register the simulated controller with the factory
register_controller('simulated', SimulatedTemperatureController)
-1
View File
@@ -18,7 +18,6 @@ def result():
data["test"] = r
accu+=ts
accu.write_to_hdf(h5, "/", "name", "title")
print(accu)
try:
h5.close()
except: