Public Access
Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75863ffc19 | ||
|
|
6f0b090d9d | ||
|
|
d9f2cc3f2c | ||
|
|
e9e3f5a5f5 | ||
|
|
9441b5a4d9 | ||
|
|
7f29f2d173 | ||
|
|
847ab4d89c | ||
|
|
d6da2f2bda | ||
|
|
5338304ee8 | ||
|
|
d48f96e498 | ||
|
|
058716f3c0 | ||
|
|
6d9f55f788 | ||
|
|
e8d0e45bfb | ||
|
|
08e1fe2a1a | ||
|
|
064036241b | ||
|
|
97aa99c239 | ||
|
|
f26d203e79 | ||
|
|
594c0ee8d4 | ||
|
|
68da261ab4 | ||
|
|
00466cb559 | ||
|
|
7ba5c7d56a | ||
|
|
c181e15aa8 | ||
|
|
26970ba201 | ||
|
|
eab49db8d4 | ||
|
|
3bfb2670eb | ||
|
|
83a01f1d83 |
No files matched your search
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
Vendored
+5
-2
@@ -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
|
||||||
|
|
||||||
|
|||||||
Vendored
-2
@@ -1,2 +0,0 @@
|
|||||||
python3-damaris_0.20_all.deb science optional
|
|
||||||
python3-damaris_0.20_amd64.buildinfo science optional
|
|
||||||
+5
-4
@@ -14,12 +14,13 @@ license = {text = "GPL-2.0"}
|
|||||||
keywords = ["NMR", "data-processing"]
|
keywords = ["NMR", "data-processing"]
|
||||||
readme = {file = "README", content-type = "text/plain"}
|
readme = {file = "README", content-type = "text/plain"}
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"scipy >= 0.14.0",
|
"scipy>=0.14.0",
|
||||||
"numpy >= 1.8.2, < 2.0",
|
"numpy>=1.8.2,<2.0",
|
||||||
"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__"}
|
||||||
|
|||||||
@@ -15,5 +15,5 @@
|
|||||||
|
|
||||||
## module contents
|
## module contents
|
||||||
#
|
#
|
||||||
__version__ = "0.21"
|
__version__ = "0.22"
|
||||||
__all__=["experiments", "data", "gui"]
|
__all__=["experiments", "data", "gui"]
|
||||||
@@ -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"))
|
||||||
|
|||||||
+339
-13
@@ -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")
|
||||||
|
|
||||||
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()
|
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:
|
||||||
extension_count=0
|
if dump_dir._v_children[group_keyname]._v_title == key:
|
||||||
while group_keyname+"_%03d"%extension_count in dump_dir:
|
dump_file.remove_node(dump_dir, group_keyname, recursive=True)
|
||||||
extension_count+=1
|
else:
|
||||||
group_keyname+="_%03d"%extension_count
|
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()
|
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)
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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" ]
|
||||||
|
|
||||||
@@ -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
@@ -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( )
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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:
|
||||||
|
|||||||
Reference in new issue
Block a user