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 7ba5c7d56a added HDF5 read capability. Closes #27
Build Debian Packages / build (trixie, debian13) (pull_request) Successful in 14m37s
Build Debian Packages / build (bookworm, debian12) (pull_request) Successful in 14m50s
Build Debian Packages / build (bullseye, debian11) (pull_request) Has been cancelled
2026-07-06 20:42:04 +02:00
markusro c181e15aa8 fixed some python2 to python3 changes as well as dat time formats 2026-07-06 20:40:59 +02: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
markusro 3bfb2670eb initial hdfreader implementation 2026-07-05 19:36:20 +02:00
markusro 83a01f1d83 new release
Build Debian Packages / build (trixie, debian13) (push) Successful in 13m25s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 13m22s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-05 14:13:32 +02:00
24 changed files with 1757 additions and 4384 deletions

No files matched your search

+5
View File
@@ -6,3 +6,8 @@ __pycache__
*.egg-info *.egg-info
*.whl *.whl
.idea .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/damaris/gui/DAMARIS3.png
include src/gui/DAMARIS3.ico include src/damaris/gui/DAMARIS3.ico
include src/gui/damaris.glade include src/damaris/gui/damaris3.glade
include src/gui/damaris.gladep include src/damaris/gui/python.xml
#recursive-include doc/reference-html *.html *.gif *.png *.css #recursive-include doc/reference-html *.html *.gif *.png *.css
#recursive-include doc/tutorial-html *.html *.gif *.png *.css *.tar.gz *.sh #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 . pip install -e .
This will download newer packages and install them in the venv. 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
+5 -2
View File
@@ -1,8 +1,11 @@
python3-damaris (0.21) UNRELEASED; urgency=medium python3-damaris (0.22) stable; urgency=medium
* fixed autocomplete in the editor tabs * fixed autocomplete in the editor tabs
* added clipping warning on ADC overload, ADC bit-depth settable in config
* Accumulation object keeps track of job_ids
* get_result_by_index keeps common description
-- Markus Rosenstihl <markus.rosenstihl@pkm.tu-darmstadt.de> Sat, 04 Jul 2026 20:00:13 +0200 -- Markus Rosenstihl <markus.rosenstihl@pkm.tu-darmstadt.de> Sun, 05 Jul 2026 14:09:34 +0200
python3-damaris (0.20) stable; urgency=medium python3-damaris (0.20) stable; urgency=medium
-2
View File
@@ -1,2 +0,0 @@
python3-damaris_0.20_all.deb science optional
python3-damaris_0.20_amd64.buildinfo science optional
+3 -2
View File
@@ -19,7 +19,8 @@ dependencies = [
"tables >= 3.1.1", "tables >= 3.1.1",
"matplotlib >= 1.4.1, <= 3.6", "matplotlib >= 1.4.1, <= 3.6",
"pyxdg", "pyxdg",
"PyGObject >= 3.14.0, <= 3.51.0" "PyGObject >= 3.14.0, <= 3.51.0",
"pyserial>=3.5",
] ]
authors = [ authors = [
{name = "Achim Gädke"}, {name = "Achim Gädke"},
@@ -39,7 +40,7 @@ DAMARIS3 = "damaris.__main__:main"
where = ["src"] where = ["src"]
[tool.setuptools.package-data] [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] [tool.setuptools.dynamic]
version = {attr = "damaris.__version__"} version = {attr = "damaris.__version__"}
+1 -1
View File
@@ -15,5 +15,5 @@
## module contents ## module contents
# #
__version__ = "0.21" __version__ = "0.22"
__all__=["experiments", "data", "gui"] __all__=["experiments", "data", "gui"]
+45 -5
View File
@@ -989,14 +989,14 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
self.lock.acquire() self.lock.acquire()
if self.sampling_rate != other.get_sampling_rate(): 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): 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(): 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)): for i in range(len(self.index)):
if self.index[i] != other.get_index_bounds(i): 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()): for i in range(self.get_number_of_channels()):
self.y[i] += other.y[i] self.y[i] += other.y[i]
@@ -1160,6 +1160,26 @@ def read_from_hdf(hdf_node):
earliest_time=None earliest_time=None
if "earliest_time" in dir(hdf_node._v_attrs): if "earliest_time" in dir(hdf_node._v_attrs):
timestring=hdf_node._v_attrs.__getattr__("earliest_time") timestring=hdf_node._v_attrs.__getattr__("earliest_time")
# Handle both formats: "20260705 19:13:59.162" and "2026-07-05 19:13:59.162"
if '-' in timestring:
# New format: "2026-07-05 19:13:59.162"
try:
earliest_time=datetime.datetime.strptime(str(timestring), '%Y-%m-%d %H:%M:%S.%f')
except ValueError:
# Fallback to manual parsing
parts = timestring.split()
if len(parts) >= 2:
date_parts = parts[0].split('-')
time_parts = parts[1].split(':')
if len(date_parts) == 3 and len(time_parts) >= 2:
year, month, day = int(date_parts[0]), int(date_parts[1]), int(date_parts[2])
hour, minute = int(time_parts[0]), int(time_parts[1])
second_parts = time_parts[2].split('.') if len(time_parts) > 2 else ['0']
second = int(second_parts[0])
microsecond = int(second_parts[1].ljust(6, '0')[:6]) if len(second_parts) > 1 else 0
earliest_time=datetime.datetime(year, month, day, hour, minute, second, microsecond)
else:
# Old format: "20260705 19:13:59.162"
earliest_time=datetime.datetime(int(timestring[:4]), # year earliest_time=datetime.datetime(int(timestring[:4]), # year
int(timestring[4:6]), # month int(timestring[4:6]), # month
int(timestring[6:8]), # day int(timestring[6:8]), # day
@@ -1172,6 +1192,26 @@ def read_from_hdf(hdf_node):
oldest_time=None oldest_time=None
if "oldest_time" in dir(hdf_node._v_attrs): if "oldest_time" in dir(hdf_node._v_attrs):
timestring=hdf_node._v_attrs.__getattr__("oldest_time") timestring=hdf_node._v_attrs.__getattr__("oldest_time")
# Handle both formats: "20260705 19:13:59.162" and "2026-07-05 19:13:59.162"
if '-' in timestring:
# New format: "2026-07-05 19:13:59.162"
try:
oldest_time=datetime.datetime.strptime(str(timestring), '%Y-%m-%d %H:%M:%S.%f')
except ValueError:
# Fallback to manual parsing
parts = timestring.split()
if len(parts) >= 2:
date_parts = parts[0].split('-')
time_parts = parts[1].split(':')
if len(date_parts) == 3 and len(time_parts) >= 2:
year, month, day = int(date_parts[0]), int(date_parts[1]), int(date_parts[2])
hour, minute = int(time_parts[0]), int(time_parts[1])
second_parts = time_parts[2].split('.') if len(time_parts) > 2 else ['0']
second = int(second_parts[0])
microsecond = int(second_parts[1].ljust(6, '0')[:6]) if len(second_parts) > 1 else 0
oldest_time=datetime.datetime(year, month, day, hour, minute, second, microsecond)
else:
# Old format: "20260705 19:13:59.162"
oldest_time=datetime.datetime(int(timestring[:4]), # year oldest_time=datetime.datetime(int(timestring[:4]), # year
int(timestring[4:6]), # month int(timestring[4:6]), # month
int(timestring[6:8]), # day int(timestring[6:8]), # day
@@ -1212,7 +1252,7 @@ def read_from_hdf(hdf_node):
accu.y_square=[] accu.y_square=[]
accu.use_error=False accu.use_error=False
for ch in range(accu_data.shape[1]/2): for ch in range(accu_data.shape[1] // 2):
accu.y.append(accu_data[:,ch*2]*accu.n) accu.y.append(accu_data[:,ch*2]*accu.n)
if accu.n<2 or numpy.all(accu_data[:,ch*2+1]==0.0): if accu.n<2 or numpy.all(accu_data[:,ch*2+1]==0.0):
accu.y_square.append(numpy.zeros((accu_data.shape[0]) ,dtype="float32")) accu.y_square.append(numpy.zeros((accu_data.shape[0]) ,dtype="float32"))
+334 -8
View File
@@ -7,6 +7,7 @@ import collections
import threading import threading
import traceback import traceback
import io import io
import numpy
class DataPool(collections.abc.MutableMapping): class DataPool(collections.abc.MutableMapping):
""" """
@@ -48,6 +49,7 @@ class DataPool(collections.abc.MutableMapping):
self.__mydict={} self.__mydict={}
self.__dictlock=threading.Lock() self.__dictlock=threading.Lock()
self.__registered_listeners=[] self.__registered_listeners=[]
self.__dirty_keys=set()
def __getitem__(self, name): def __getitem__(self, name):
try: try:
@@ -64,6 +66,7 @@ class DataPool(collections.abc.MutableMapping):
else: else:
e=DataPool.Event(DataPool.Event.new_key, name,self) e=DataPool.Event(DataPool.Event.new_key, name,self)
self.__mydict[name]=value self.__mydict[name]=value
self.__dirty_keys.add(name)
finally: finally:
self.__dictlock.release() self.__dictlock.release()
self.__send_event(e) self.__send_event(e)
@@ -73,6 +76,7 @@ class DataPool(collections.abc.MutableMapping):
try: try:
self.__dictlock.acquire() self.__dictlock.acquire()
del self.__mydict[name] del self.__mydict[name]
self.__dirty_keys.add(name)
finally: finally:
self.__dictlock.release() self.__dictlock.release()
self.__send_event(DataPool.Event(DataPool.Event.deleted_key,name,self)) self.__send_event(DataPool.Event(DataPool.Event.deleted_key,name,self))
@@ -106,18 +110,74 @@ class DataPool(collections.abc.MutableMapping):
self.__send_event(DataPool.Event(DataPool.Event.destroy)) self.__send_event(DataPool.Event(DataPool.Event.destroy))
self.__registered_listeners=None 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):
if type(hdffile) is bytes:
"""
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") dump_file=tables.open_file(hdffile, mode="a")
elif isinstance(hdffile,tables.File): elif isinstance(hdffile,tables.File):
dump_file=hdffile dump_file=hdffile
else: else:
raise Exception("expecting hdffile or string") raise Exception("expecting hdffile or string")
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") dump_group = dump_file.create_group(where, name, "DAMARIS data pool")
self.__dictlock.acquire() 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() self.__dictlock.release()
try: try:
for key in dict_keys: for key in dict_keys:
if key.startswith("__"): if key.startswith("__"):
@@ -142,21 +202,37 @@ class DataPool(collections.abc.MutableMapping):
# convert last part of key to a valid name # convert last part of key to a valid name
group_keyname="dict_"+str(namelist[-1]).translate(DataPool.translation_table) 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: if group_keyname in dump_dir:
if dump_dir._v_children[group_keyname]._v_title == key:
dump_file.remove_node(dump_dir, group_keyname, recursive=True)
else:
extension_count = 0 extension_count = 0
while group_keyname + "_%03d" % extension_count in dump_dir: 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 extension_count += 1
else:
# truly new key with collision
group_keyname += "_%03d" % extension_count group_keyname += "_%03d" % extension_count
self.__dictlock.acquire() self.__dictlock.acquire()
if key not in self.__mydict: if key not in self.__mydict:
# outdated ... # Key was deleted
if key in self.__dirty_keys:
self.__dirty_keys.remove(key)
self.__dictlock.release() self.__dictlock.release()
continue continue
value=self.__mydict[key] value=self.__mydict[key]
if key in self.__dirty_keys:
self.__dirty_keys.remove(key)
self.__dictlock.release() self.__dictlock.release()
# now write data, assuming, the object is constant during write operation # now write data, assuming, the object is constant during write operation
if "write_to_hdf" in dir(value): if hasattr(value, "write_to_hdf"):
try: try:
value.write_to_hdf(hdffile=dump_file, value.write_to_hdf(hdffile=dump_file,
where=dump_dir, where=dump_dir,
@@ -170,16 +246,266 @@ class DataPool(collections.abc.MutableMapping):
traceback.print_tb(sys.exc_info()[2], None, traceback_file) traceback.print_tb(sys.exc_info()[2], None, traceback_file)
print("detailed traceback: %s\n"%str(e)+traceback_file.getvalue()) print("detailed traceback: %s\n"%str(e)+traceback_file.getvalue())
traceback_file=None traceback_file=None
elif isinstance(value, (numpy.ndarray, int, float, str, bytes)):
try:
obj_to_save = value
if isinstance(value, str):
obj_to_save = value.encode('utf-8')
dump_file.create_array(dump_dir,
name=group_keyname,
obj=obj_to_save,
title=key)
except Exception as e:
print("failed to write data_pool[\"%s\"]: %s"%(key,str(e)))
else: else:
print("don't know how to store data_pool[\"%s\"]"%key) print("don't know how to store data_pool[\"%s\"] (type: %s)"%(key, type(value)))
value=None value=None
finally: finally:
dump_group=None dump_group=None
if type(hdffile) is bytes: if isinstance(hdffile, (bytes, str)):
dump_file.close() dump_file.close()
dump_file=None dump_file=None
def read_from_hdf(self, hdffile, where="/data_pool"):
"""
Read specified data from HDF5 file and populate the DataPool.
Parameters:
- hdffile: HDF5 file object or filename (bytes/str)
- where: Location within HDF5 file to read from (default: "/data_pool")
"""
# Handle both filename and file object
if type(hdffile) is bytes or isinstance(hdffile, str):
hdf_file = tables.open_file(hdffile, mode="r")
close_file = True
elif isinstance(hdffile, tables.File):
hdf_file = hdffile
close_file = False
else:
raise Exception("expecting hdffile or string")
try:
# Navigate to the data pool group
if where not in hdf_file:
raise Exception(f"HDF5 location '{where}' not found")
data_pool_group = hdf_file.get_node(where)
# 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()
def _read_hdf_group(self, group, path_prefix):
"""
Recursively read HDF5 group and populate DataPool.
Parameters:
- group: HDF5 group object
- path_prefix: Current path in DataPool hierarchy
"""
# Process all nodes in this group
for node in group:
# Get the node name (last part of the path)
node_name = node._v_name
# Try to get the original name from title
title = node._v_title
if title:
# For DAMARIS objects, the title is usually the full path
# For intermediate groups, it's just the part name
if "/" in title:
full_path = title
else:
if path_prefix:
full_path = f"{path_prefix}/{title}"
else:
full_path = title
else:
# Strip dir_ and dict_ prefixes from node name
clean_name = self._clean_hdf_name(node_name)
# Build the full path in DataPool hierarchy
if path_prefix:
full_path = f"{path_prefix}/{clean_name}"
else:
full_path = clean_name
full_path = full_path.replace("//", "/")
if isinstance(node, (tables.Group, tables.Table)):
# Check if this is a known DAMARIS object type
if hasattr(node._v_attrs, 'damaris_type'):
damaris_type = node._v_attrs.damaris_type
# Handle different DAMARIS object types
if damaris_type == "Accumulation":
from damaris.data.Accumulation import read_from_hdf
obj = read_from_hdf(node)
if obj is not None:
self[full_path] = obj
elif damaris_type == "ADC_Result":
from damaris.data.ADC_Result import read_from_hdf
obj = read_from_hdf(node)
if obj is not None:
self[full_path] = obj
elif damaris_type == "MeasurementResult":
from damaris.data.MeasurementResult import read_from_hdf
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
# Recursively process sub-groups
if isinstance(node, tables.Group):
self._read_hdf_group(node, full_path)
elif isinstance(node, (tables.Array, tables.CArray, tables.EArray, tables.Table)):
# Handle simple array data
try:
data = node.read()
# If it's a scalar array of bytes, decode it to string
if hasattr(data, 'decode'):
try:
data = data.decode('utf-8')
except:
pass
elif isinstance(data, numpy.ndarray) and data.dtype.kind in ('S', 'V'):
# For numpy byte arrays, try to decode if they are scalar or small
if data.size == 1:
try:
decoded = data.item().decode('utf-8')
data = decoded
except:
pass
self[full_path] = data
except Exception as e:
print(f"Warning: Could not read array {full_path}: {e}")
# Note: Other node types (Table, etc.) would be handled here as needed
def _clean_hdf_name(self, name):
"""
Clean HDF5 node name by removing dir_ and dict_ prefixes.
Parameters:
- name: HDF5 node name (may have dir_ or dict_ prefix)
Returns:
- Cleaned name with prefixes removed
"""
if name.startswith("dir_"):
return name[4:] # Remove "dir_" prefix
elif name.startswith("dict_"):
return name[5:] # Remove "dict_" prefix
else:
return name
@classmethod
def load_hdf5(cls, filename):
"""
Load a complete DAMARIS HDF5 file and return DataPool and metadata, including scripts.
Parameters:
- filename: Path to HDF5 file
Returns:
- tuple: (DataPool instance, metadata dict)
metadata dict contains:
- 'scripts': dict with experiment scripts and spool info
- 'log': log data if available
- 'timeline': timeline data if available
"""
# Validate file
if not tables.is_pytables_file(filename):
raise Exception(f"File {filename} is not a valid PyTables HDF5 file")
# Create DataPool and load data
data_pool = cls()
try:
hdf_file = tables.open_file(filename, mode="r")
# Load main data pool
if "/data_pool" in hdf_file:
data_pool.read_from_hdf(hdf_file, where="/data_pool")
# Extract metadata
metadata = {}
# Load scripts
if "/scripts" in hdf_file:
scripts_group = hdf_file.get_node("/scripts")
scripts_data = {}
# Use _v_children to get node names safely
if hasattr(scripts_group, '_v_children'):
for node_name, node in scripts_group._v_children.items():
try:
if hasattr(node, 'read'):
data = node.read()
# Handle numpy string arrays and other types
if hasattr(data, 'decode'):
# bytes or numpy bytes
scripts_data[node_name] = data.decode('utf-8', errors='replace')
elif hasattr(data, 'tolist'):
# numpy array
scripts_data[node_name] = str(data.tolist())
elif isinstance(data, (str, bytes)):
# string or bytes
if isinstance(data, bytes):
scripts_data[node_name] = data.decode('utf-8', errors='replace')
else:
scripts_data[node_name] = data
else:
scripts_data[node_name] = str(data)
except Exception as e:
print(f"Warning: Could not read script {node_name}: {e}")
metadata['scripts'] = scripts_data
# Load log
if "/log" in hdf_file:
try:
log_node = hdf_file.get_node("/log")
metadata['log'] = log_node.read()
except Exception as e:
print(f"Warning: Could not read log: {e}")
# Load timeline
if "/timeline" in hdf_file:
try:
timeline_node = hdf_file.get_node("/timeline")
metadata['timeline'] = timeline_node.read()
except Exception as e:
print(f"Warning: Could not read timeline: {e}")
return data_pool, metadata
except Exception as e:
raise Exception(f"Failed to load HDF5 file {filename}: {e}")
finally:
if 'hdf_file' in locals():
hdf_file.close()
def register_listener(self, listening_function): def register_listener(self, listening_function):
self.__registered_listeners.append(listening_function) self.__registered_listeners.append(listening_function)
+5 -3
View File
@@ -193,7 +193,7 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
def uses_statistics(self): def uses_statistics(self):
""" """
drawable interface method, returns True drawable interface method returns True
""" """
return True return True
@@ -203,10 +203,11 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
destination can be a file or a filename destination can be a file or a filename
suitable for further processing suitable for further processing
""" """
# write sorted
the_destination=destination the_destination=destination
file_opened = False
if type(destination) in (str,): if type(destination) in (str,):
the_destination=open(destination, "w") the_destination=open(destination, "w")
file_opened = True
the_destination.write("# quantity:"+str(self.quantity_name)+"\n") the_destination.write("# quantity:"+str(self.quantity_name)+"\n")
the_destination.write("# x y ysigma n\n") the_destination.write("# x y ysigma n\n")
@@ -222,7 +223,8 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
y.mean_error(), y.mean_error(),
delimiter, delimiter,
y.n)) y.n))
the_destination=None if file_opened:
the_destination.close()
def write_to_hdf(self, hdffile, where, name, title, complib=None, complevel=None): 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 .Resultable import Resultable
from .Drawable import Drawable from .Drawable import Drawable
import numpy
import threading
import tables
############################################################################# #############################################################################
# # # #
@@ -15,17 +18,224 @@ from .Drawable import Drawable
class TemperatureResult(Resultable, Drawable): class TemperatureResult(Resultable, Drawable):
""" """
Specialised class of Resultable and 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) Resultable.__init__(self)
Drawable.__init__(self) Drawable.__init__(self)
if (x is None) and (y is None) and (desc is None) and (job_id is None) and (job_date is None): # Set default labels
pass self.xlabel = "Time (s)"
self.ylabel = "Temperature (C)"
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): # Initialize data lists
pass self.x = [] if x is None else x
self.y = [] if y is None else y
self.setpoint = [] if setpoint is None else setpoint
else: # Set metadata if provided
raise ValueError("Wrong usage of __init__!") 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.
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)
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)
# 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.Config_Result import Config_Result
from damaris.data.Temperature import TemperatureResult 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" ]
+2 -4
View File
@@ -96,12 +96,10 @@ class Experiment:
#for tracking the experiment length: #for tracking the experiment length:
#because loops are possible we need to track the length for each loop level #because loops are possible we need to track the length for each loop level
self.total_time=[] self.total_time=[0.0]
self.total_time.append(0.0)
#and we need to know the number of iterations of the loops to multiply the state. #and we need to know the number of iterations of the loops to multiply the state.
self.loop_iterations=[] self.loop_iterations=[1]
self.loop_iterations.append(1)
# Commands ------------------------------------------------------------------------------------- # Commands -------------------------------------------------------------------------------------
+439 -27
View File
@@ -61,7 +61,8 @@ from damaris.gui import ResultReader, ResultHandling
from damaris.gui import BackendDriver from damaris.gui import BackendDriver
#from damaris.gui.gtkcodebuffer import CodeBuffer, SyntaxLoader #from damaris.gui.gtkcodebuffer import CodeBuffer, SyntaxLoader
#from damaris.data import Drawable # this is a base class, it should be used... #from damaris.data import Drawable # this is a base class, it should be used...
from damaris.data import DataPool, Accumulation, ADC_Result, MeasurementResult from damaris.data import DataPool, Accumulation, ADC_Result, MeasurementResult, TemperatureResult
# TemperatureMonitor imported lazily to avoid circular import issues
# default, can be set to true from start script # default, can be set to true from start script
debug = False debug = False
@@ -88,7 +89,6 @@ class logstream:
def __del__( self ): def __del__( self ):
pass pass
global log global log
log = logstream( ) log = logstream( )
sys.stdout = log sys.stdout = log
@@ -134,9 +134,6 @@ class LockFile:
return False return False
class DamarisGUI: class DamarisGUI:
ExpScript_Display = 0 ExpScript_Display = 0
ResScript_Display = 1 ResScript_Display = 1
@@ -169,7 +166,7 @@ class DamarisGUI:
self.log = LogWindow( self.xml_gui , self) self.log = LogWindow( self.xml_gui , self)
self.sw = ScriptWidgets( self.xml_gui ) self.sw = ScriptWidgets( self.xml_gui, self )
self.toolbar_init( ) self.toolbar_init( )
@@ -185,6 +182,10 @@ class DamarisGUI:
#to stop queued experiments #to stop queued experiments
self.stop_experiment_flag = threading.Event() self.stop_experiment_flag = threading.Event()
# Temperature monitoring
self.temp_monitor = None
self.temp_controller = None
exp_script = "" exp_script = ""
if exp_script_filename is not None and exp_script_filename != "": if exp_script_filename is not None and exp_script_filename != "":
self.sw.exp_script_filename = exp_script_filename[ : ] self.sw.exp_script_filename = exp_script_filename[ : ]
@@ -218,6 +219,7 @@ class DamarisGUI:
"on_experiment_script_line_textfield_value_changed": self.sw.column_line_widgets_changed_event, "on_experiment_script_line_textfield_value_changed": self.sw.column_line_widgets_changed_event,
"on_experiment_script_column_textfield_value_changed": self.sw.column_line_widgets_changed_event, "on_experiment_script_column_textfield_value_changed": self.sw.column_line_widgets_changed_event,
"on_toolbar_open_file_button_clicked": self.sw.open_file, "on_toolbar_open_file_button_clicked": self.sw.open_file,
"on_toolbar_open_hdf5_button_clicked": self.sw.open_hdf5,
"on_toolbar_new_button_clicked": self.sw.new_file, "on_toolbar_new_button_clicked": self.sw.new_file,
"on_toolbar_save_as_button_clicked": self.sw.save_file_as, "on_toolbar_save_as_button_clicked": self.sw.save_file_as,
"on_toolbar_save_file_button_clicked": self.sw.save_file, "on_toolbar_save_file_button_clicked": self.sw.save_file,
@@ -297,6 +299,14 @@ class DamarisGUI:
self.config = None self.config = None
self.xml_gui = None self.xml_gui = None
def observe_data_pool( self, data_pool ):
"""
Delegate to monitor widgets
"""
self.data = data_pool
if hasattr(self, "monitor"):
self.monitor.observe_data_pool(data_pool)
# event handling: the real acitons in gui programming # event handling: the real acitons in gui programming
# first global events # first global events
@@ -507,6 +517,8 @@ class DamarisGUI:
self.data = self.si.data self.data = self.si.data
# run frontend and script engines # run frontend and script engines
self.monitor.observe_data_pool( self.data ) self.monitor.observe_data_pool( self.data )
# Start temperature monitoring if configured (before scripts start)
self.start_temperature_monitoring(actual_config)
self.si.runScripts( ) self.si.runScripts( )
except Exception as e: except Exception as e:
#print "ToDo evaluate exception",str(e), "at",traceback.extract_tb(sys.exc_info()[2])[-1][1:3] #print "ToDo evaluate exception",str(e), "at",traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
@@ -567,6 +579,7 @@ class DamarisGUI:
self.dump_filename = "" self.dump_filename = ""
if actual_config[ "data_pool_name" ] != "": if actual_config[ "data_pool_name" ] != "":
self.dump_states( init=True ) self.dump_states( init=True )
gobject.timeout_add( 200, self.observe_running_experiment ) gobject.timeout_add( 200, self.observe_running_experiment )
def observe_running_experiment( self ): def observe_running_experiment( self ):
@@ -679,6 +692,8 @@ class DamarisGUI:
still_running = [_f for _f in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver, self.dump_thread ] if _f] still_running = [_f for _f in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver, self.dump_thread ] if _f]
if len( still_running ) == 0: if len( still_running ) == 0:
# Stop temperature monitoring when experiment finishes
self.stop_temperature_monitoring()
if self.save_thread is None and self.dump_filename != "": if self.save_thread is None and self.dump_filename != "":
print("all subprocesses ended, saving data pool") print("all subprocesses ended, saving data pool")
# thread to save data... # thread to save data...
@@ -869,15 +884,6 @@ class DamarisGUI:
filters=tables.Filters( complevel=9, complib='zlib' ) ) 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 ): 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 # prepare for update
dump_file = tables.open_file( self.dump_filename, mode="r+" ) dump_file = tables.open_file( self.dump_filename, mode="r+" )
@@ -894,7 +900,8 @@ class DamarisGUI:
# save the data! # save the data!
self.data.write_hdf5( dump_file, where="/", name="data_pool", 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 # now save additional information
e = self.si.data.get( "__recentexperiment", -1 ) + 1 e = self.si.data.get( "__recentexperiment", -1 ) + 1
@@ -977,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] 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: for r in still_running:
r.quit_flag.set( ) r.quit_flag.set( )
# Stop temperature monitoring
self.stop_temperature_monitoring()
self.state = DamarisGUI.Stop_State 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 ): def documentation_init( self ):
self.doc_urls = { self.doc_urls = {
@@ -1103,11 +1200,12 @@ class LogWindow:
class ScriptWidgets: class ScriptWidgets:
def __init__( self, xml_gui ): def __init__( self, xml_gui, outer_space ):
""" """
initialize text widgets with text initialize text widgets with text
""" """
self.xml_gui = xml_gui self.xml_gui = xml_gui
self.outer_space = outer_space
# my states # my states
# editing enabled/disabled # editing enabled/disabled
@@ -1174,7 +1272,7 @@ get_ylabel set_ylabel"""
self.compwgen = gtksourceview2.CompletionWords.new("Keywords", None) self.compwgen = gtksourceview2.CompletionWords.new("Keywords", None)
self.compwgen.register(self.keywordsbuffer) 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 = gtksourceview2.CompletionWords.new("Words", None)
self.compwexp.register(self.experiment_script_textbuffer) self.compwexp.register(self.experiment_script_textbuffer)
@@ -1572,7 +1670,7 @@ get_ylabel set_ylabel"""
elif response == gtk.ResponseType.CANCEL: elif response == gtk.ResponseType.CANCEL:
return 0 return 0
def response( self, response_id, script_widget ): def response( dialog, response_id, script_widget ):
if response_id == gtk.ResponseType.OK: if response_id == gtk.ResponseType.OK:
file_name = dialog.get_filename( ) file_name = dialog.get_filename( )
if file_name is None: if file_name is None:
@@ -1580,7 +1678,13 @@ get_ylabel set_ylabel"""
script_filename = os.path.abspath( file_name ) script_filename = os.path.abspath( file_name )
if not os.access( script_filename, os.R_OK ): if not os.access( script_filename, os.R_OK ):
self.outer_space.show_error_dialog( "File I/O Error", "Cannot read from file %s" % script_filename ) error_dialog = gtk.MessageDialog(parent=script_widget.xml_gui.get_object( "main_window"),
type=gtk.MessageType.ERROR,
flags=gtk.DialogFlags.MODAL,
buttons=("_OK", gtk.ResponseType.OK))
error_dialog.set_markup("Cannot read from file %s" % script_filename)
error_dialog.run()
error_dialog.destroy()
return True return True
script_string = script_widget.load_file_as_unicode( script_filename ) script_string = script_widget.load_file_as_unicode( script_filename )
@@ -1613,6 +1717,94 @@ get_ylabel set_ylabel"""
return True return True
def open_hdf5( self, widget, Data=None ):
"""
Open and load an HDF5 file written by DAMARIS.
"""
# ignore
if not self.editing_state:
return 0
parent_window = self.xml_gui.get_object( "main_window" )
dialog = gtk.FileChooserDialog( title="Open HDF5 File...",
parent=parent_window,
action=gtk.FileChooserAction.OPEN,
buttons=("_Open", gtk.ResponseType.OK, "_Cancel", gtk.ResponseType.CANCEL)
)
dialog.set_default_response( gtk.ResponseType.OK )
dialog.set_select_multiple( False )
# Add filter for HDF5 files
filter_hdf5 = gtk.FileFilter()
filter_hdf5.set_name("HDF5 files")
filter_hdf5.add_pattern("*.h5")
filter_hdf5.add_pattern("*.hdf5")
filter_hdf5.add_pattern("*.HDF5")
filter_hdf5.add_pattern("*.H5")
dialog.add_filter(filter_hdf5)
# Also allow all files
filter_all = gtk.FileFilter()
filter_all.set_name("All files")
filter_all.add_pattern("*")
dialog.add_filter(filter_all)
def response( dialog, response_id, script_widget ):
if response_id == gtk.ResponseType.OK:
file_name = dialog.get_filename( )
if file_name is None:
return
file_name = os.path.abspath( file_name )
if not os.access( file_name, os.R_OK ):
error_dialog = gtk.MessageDialog(parent=script_widget.xml_gui.get_object( "main_window"),
type=gtk.MessageType.ERROR,
flags=gtk.DialogFlags.MODAL,
buttons=("_OK", gtk.ResponseType.OK))
error_dialog.set_markup("Cannot read from file %s" % file_name)
error_dialog.run()
error_dialog.destroy()
return True
try:
# Load DataPool and metadata from HDF5
data_pool, metadata = DataPool.load_hdf5(file_name)
# Set the loaded data pool in the outer space (main application)
if hasattr(script_widget.outer_space, 'observe_data_pool'):
script_widget.outer_space.observe_data_pool(data_pool)
elif hasattr(script_widget.outer_space, 'monitor'):
# Fallback if delegate not present
script_widget.outer_space.data = data_pool
script_widget.outer_space.monitor.observe_data_pool(data_pool)
# If there are scripts in metadata, load them
if 'scripts' in metadata:
scripts = metadata['scripts']
exp_script = scripts.get('experiment_script', None)
res_script = scripts.get('result_script', None)
if exp_script or res_script:
script_widget.set_scripts(exp_script, res_script)
except Exception as e:
error_dialog = gtk.MessageDialog(parent=script_widget.xml_gui.get_object( "main_window"),
type=gtk.MessageType.ERROR,
flags=gtk.DialogFlags.MODAL,
buttons=("_OK", gtk.ResponseType.OK))
error_dialog.set_markup("Failed to load HDF5 file: %s" % str(e))
error_dialog.run()
error_dialog.destroy()
return True
# Event-Handler for response-signal (when one of the button is pressed)
dialog.connect( "response", response, self )
dialog.run( )
dialog.destroy( )
return True
def save_file( self, widget=None, Data=None ): def save_file( self, widget=None, Data=None ):
""" """
save file to associated filename save file to associated filename
@@ -1967,6 +2159,73 @@ class ConfigTab:
backend_vbox.pack_start(self.config_adc_bit_depth_hbox, False, False, 0) backend_vbox.pack_start(self.config_adc_bit_depth_hbox, False, False, 0)
self.config_adc_bit_depth_hbox.show_all() 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 # insert version informations
components_text = """ components_text = """
operating system %(os)s operating system %(os)s
@@ -2073,11 +2332,29 @@ pygobject version %(pygobject)s
"data_pool_complib": complib, "data_pool_complib": complib,
"data_pool_comprate": self.config_data_pool_comprate.get_value_as_int( ), "data_pool_comprate": self.config_data_pool_comprate.get_value_as_int( ),
"script_font": self.config_script_font_button.get_font_name( ), "script_font": self.config_script_font_button.get_font_name( ),
"adc_bit_depth": self.config_adc_bit_depth_spinbutton.get_value_as_int() "adc_bit_depth": self.config_adc_bit_depth_spinbutton.get_value_as_int(),
# Temperature monitoring configuration (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"] ADC_Result.default_bit_depth = actual_config["adc_bit_depth"]
return actual_config 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 ): def set( self, config ):
if "start_backend" in config: if "start_backend" in config:
self.config_start_backend_checkbutton.set_active( config[ "start_backend" ] ) self.config_start_backend_checkbutton.set_active( config[ "start_backend" ] )
@@ -2108,6 +2385,41 @@ pygobject version %(pygobject)s
ADC_Result.default_bit_depth = int(config["adc_bit_depth"]) ADC_Result.default_bit_depth = int(config["adc_bit_depth"])
except (ValueError, TypeError): except (ValueError, TypeError):
pass 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: if "data_pool_complib" in config:
# find combo-box entry and make it active... # find combo-box entry and make it active...
model = self.config_data_pool_complib.get_model( ) model = self.config_data_pool_complib.get_model( )
@@ -2124,6 +2436,84 @@ pygobject version %(pygobject)s
def on_adc_bit_depth_changed(self, widget): def on_adc_bit_depth_changed(self, widget):
ADC_Result.default_bit_depth = widget.get_value_as_int() 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 ): def load_config_handler( self, widget ):
if self.system_default_filename: if self.system_default_filename:
self.load_config( self.system_default_filename ) self.load_config( self.system_default_filename )
@@ -2342,7 +2732,9 @@ class MonitorWidgets:
# now let's add a button to the toolbar # now let's add a button to the toolbar
self.matplot_cursor_button = gtk.ToggleButton(label='') self.matplot_cursor_button = gtk.ToggleButton(label='')
try: 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) self.matplot_cursor_button.set_image(image)
except: except:
pass pass
@@ -2411,7 +2803,7 @@ class MonitorWidgets:
def source_list_find_one( self, model, iter, what ): def source_list_find_one( self, model, iter, what ):
"""find node in subcategory""" """find node in subcategory"""
while iter is not None: while iter is not None:
if model.get( iter, 0 )[ 0 ] == what: if str( model.get( iter, 0 )[ 0 ] ) == what:
return iter return iter
iter = model.iter_next( iter ) iter = model.iter_next( iter )
return iter return iter
@@ -2482,6 +2874,8 @@ class MonitorWidgets:
register a listener and save reference to data register a listener and save reference to data
assume to be in gtk/gdk lock assume to be in gtk/gdk lock
""" """
gdk.threads_enter( )
try:
if self.data_pool is not None: if self.data_pool is not None:
# maybe some extra cleanup needed # maybe some extra cleanup needed
print("ToDo: cleanup widgets") print("ToDo: cleanup widgets")
@@ -2492,12 +2886,12 @@ class MonitorWidgets:
self.data_pool.unregister_listener( self.datapool_listener ) self.data_pool.unregister_listener( self.datapool_listener )
self.data_pool = None self.data_pool = None
self.source_list_reset( )
self.update_counter_lock.acquire( ) self.update_counter_lock.acquire( )
self.update_counter = 0 self.update_counter = 0
self.update_counter_lock.release( ) self.update_counter_lock.release( )
# display states # display states
self.source_list_reset( )
self.__rescale = True self.__rescale = True
self.displayed_data = [ None, None ] self.displayed_data = [ None, None ]
self.display_source_path_label.set_label( "" ) self.display_source_path_label.set_label( "" )
@@ -2508,6 +2902,23 @@ class MonitorWidgets:
self.data_pool = data_pool self.data_pool = data_pool
self.data_pool.register_listener( self.datapool_listener ) self.data_pool.register_listener( self.datapool_listener )
# Populate source list with existing data since the listener
# was registered after the data was loaded
for key in sorted(data_pool.keys()):
if key != "__builtins__": # Skip internal keys
self.source_list_add(key)
# Select first data item if available
keys = [k for k in data_pool.keys() if not k.startswith("__")]
if keys:
keys.sort()
first_key = keys[0]
first_iter = self.source_list_find(first_key.split("/"))
if first_iter:
self.display_source_combobox.set_active_iter(first_iter)
finally:
gdk.threads_leave( )
#################### observing data structures and produce idle events #################### observing data structures and produce idle events
def datapool_listener( self, event ): def datapool_listener( self, event ):
@@ -2643,7 +3054,7 @@ class MonitorWidgets:
gdk.threads_leave( ) gdk.threads_leave( )
elif event.what == DataPool.Event.destroy: elif event.what == DataPool.Event.destroy:
gdk.threads_enter( ) gdk.threads_enter( )
self.source_list_reset( 'None' ) self.source_list_reset( )
self.displayed_data = [ None, None ] self.displayed_data = [ None, None ]
self.clear_display( ) self.clear_display( )
gdk.threads_leave( ) gdk.threads_leave( )
@@ -2803,6 +3214,7 @@ class MonitorWidgets:
self.matplot_cursor = matplotlib.widgets.Cursor(self.matplot_axes, self.matplot_cursor = matplotlib.widgets.Cursor(self.matplot_axes,
useblit=False, color='red', linewidth=1) useblit=False, color='red', linewidth=1)
self.matplot_canvas.draw() self.matplot_canvas.draw()
# TODO: Deactivate the pane button
else: else:
# Deactivate cursor # Deactivate cursor
@@ -2930,7 +3342,7 @@ class MonitorWidgets:
x_scale = self.display_x_scaling_combobox.get_active_text() x_scale = self.display_x_scaling_combobox.get_active_text()
y_scale = self.display_y_scaling_combobox.get_active_text() y_scale = self.display_y_scaling_combobox.get_active_text()
print(x_scale)
if x_scale == "lin": if x_scale == "lin":
self.matplot_axes.set_xscale("linear") self.matplot_axes.set_xscale("linear")
elif x_scale == "log10": elif x_scale == "log10":
@@ -3001,7 +3413,7 @@ class MonitorWidgets:
if in_result is None: if in_result is None:
self.clear_display( ) self.clear_display( )
return return
if isinstance( in_result, Accumulation ) or isinstance( in_result, ADC_Result ): if isinstance( in_result, Accumulation ) or isinstance( in_result, ADC_Result ) or isinstance( in_result, TemperatureResult ):
xmin = in_result.get_xmin( ) xmin = in_result.get_xmin( )
xmax = in_result.get_xmax( ) xmax = in_result.get_xmax( )
+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
+15
View File
@@ -491,6 +491,21 @@ Public License instead of this License.
<property name="homogeneous">True</property> <property name="homogeneous">True</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkToolButton" id="toolbar_open_hdf5_button">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Open DAMARIS HDF5 file</property>
<property name="label">Open HDF5</property>
<property name="use-underline">True</property>
<property name="icon-name">document-open</property>
<signal name="clicked" handler="on_toolbar_open_hdf5_button_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child> <child>
<object class="GtkToolButton" id="toolbar_save_file_button"> <object class="GtkToolButton" id="toolbar_save_file_button">
<property name="visible">True</property> <property name="visible">True</property>
+39 -1
View File
@@ -3,6 +3,8 @@ import re
import operator import operator
from functools import reduce from functools import reduce
from .temperature import TemperatureController, register_controller
DEBUG=False DEBUG=False
@@ -40,7 +42,7 @@ def checksum(message):
bcc = (reduce(operator.xor, list(map(ord,message)))) bcc = (reduce(operator.xor, list(map(ord,message))))
return chr(bcc) return chr(bcc)
class Eurotherm(object): class Eurotherm(TemperatureController):
def __init__(self, serial_device, baudrate = 19200): def __init__(self, serial_device, baudrate = 19200):
self.device = standard_device self.device = standard_device
# timeout: 110 ms to get all answers. # timeout: 110 ms to get all answers.
@@ -100,6 +102,39 @@ class Eurotherm(object):
def get_setpoint_temperature(self): def get_setpoint_temperature(self):
return self.read_param('SL') 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__': if __name__ == '__main__':
import time import time
delta=5 delta=5
@@ -118,3 +153,6 @@ if __name__ == '__main__':
f.write(l) f.write(l)
f.flush() f.flush()
f.write('# MARK -- %s --\n'%(time.asctime())) 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 data["test"] = r
accu+=ts accu+=ts
accu.write_to_hdf(h5, "/", "name", "title") accu.write_to_hdf(h5, "/", "name", "title")
print(accu)
try: try:
h5.close() h5.close()
except: except: