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
|
||||
*.whl
|
||||
.idea
|
||||
debian/files
|
||||
.pybuild
|
||||
.junie
|
||||
debian/debhelper-build-stamp
|
||||
debian/.debhelper
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
include src/gui/DAMARIS3.png
|
||||
include src/gui/DAMARIS3.ico
|
||||
include src/gui/damaris.glade
|
||||
include src/gui/damaris.gladep
|
||||
include src/damaris/gui/DAMARIS3.png
|
||||
include src/damaris/gui/DAMARIS3.ico
|
||||
include src/damaris/gui/damaris3.glade
|
||||
include src/damaris/gui/python.xml
|
||||
#recursive-include doc/reference-html *.html *.gif *.png *.css
|
||||
#recursive-include doc/tutorial-html *.html *.gif *.png *.css *.tar.gz *.sh
|
||||
@@ -15,3 +15,12 @@ Alternatively you can install withoout the system-site-packages:
|
||||
pip install -e .
|
||||
|
||||
This will download newer packages and install them in the venv.
|
||||
|
||||
|
||||
Using uv
|
||||
========
|
||||
Dependicies needed:
|
||||
sudo apt install libgirepository1.0-dev pkg-config
|
||||
|
||||
Then you can start DAMARIS3:
|
||||
uv run DAMARIS3
|
||||
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
|
||||
* 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
|
||||
|
||||
|
||||
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"]
|
||||
readme = {file = "README", content-type = "text/plain"}
|
||||
dependencies = [
|
||||
"scipy >= 0.14.0",
|
||||
"numpy >= 1.8.2, < 2.0",
|
||||
"scipy>=0.14.0",
|
||||
"numpy>=1.8.2,<2.0",
|
||||
"tables >= 3.1.1",
|
||||
"matplotlib >= 1.4.1, <= 3.6",
|
||||
"pyxdg",
|
||||
"PyGObject >= 3.14.0, <= 3.51.0"
|
||||
"PyGObject >= 3.14.0, <= 3.51.0",
|
||||
"pyserial>=3.5",
|
||||
]
|
||||
authors = [
|
||||
{name = "Achim Gädke"},
|
||||
@@ -39,7 +40,7 @@ DAMARIS3 = "damaris.__main__:main"
|
||||
where = ["src"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"damaris.gui" = ["DAMARIS3.png", "DAMARIS3.ico", "damaris.xml", "python.xml", "damaris3.glade"]
|
||||
"damaris.gui" = ["DAMARIS3.png", "DAMARIS3.ico", "python.xml", "damaris3.glade"]
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "damaris.__version__"}
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
|
||||
## module contents
|
||||
#
|
||||
__version__ = "0.21"
|
||||
__version__ = "0.22"
|
||||
__all__=["experiments", "data", "gui"]
|
||||
@@ -989,14 +989,14 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
||||
self.lock.acquire()
|
||||
|
||||
if self.sampling_rate != other.get_sampling_rate():
|
||||
raise ValueError("Accumulation: You cant add ADC-Results with diffrent sampling-rates")
|
||||
raise ValueError("Accumulation: You can't add ADC-Results with different sampling-rates")
|
||||
if len(self.y[0]) != len(other):
|
||||
raise ValueError("Accumulation: You cant add ADC-Results with diffrent number of samples")
|
||||
raise ValueError("Accumulation: You can't add ADC-Results with different number of samples")
|
||||
if len(self.y) != other.get_number_of_channels():
|
||||
raise ValueError("Accumulation: You cant add ADC-Results with diffrent number of channels")
|
||||
raise ValueError("Accumulation: You can't add ADC-Results with different number of channels")
|
||||
for i in range(len(self.index)):
|
||||
if self.index[i] != other.get_index_bounds(i):
|
||||
raise ValueError("Accumulation: You cant add ADC-Results with diffrent indexing")
|
||||
raise ValueError("Accumulation: You can't add ADC-Results with different indexing")
|
||||
|
||||
for i in range(self.get_number_of_channels()):
|
||||
self.y[i] += other.y[i]
|
||||
@@ -1160,26 +1160,66 @@ def read_from_hdf(hdf_node):
|
||||
earliest_time=None
|
||||
if "earliest_time" in dir(hdf_node._v_attrs):
|
||||
timestring=hdf_node._v_attrs.__getattr__("earliest_time")
|
||||
earliest_time=datetime.datetime(int(timestring[:4]), # year
|
||||
int(timestring[4:6]), # month
|
||||
int(timestring[6:8]), # day
|
||||
int(timestring[9:11]), # hour
|
||||
int(timestring[12:14]), # minute
|
||||
int(timestring[15:17]), # second
|
||||
int(timestring[18:21])*1000 # microsecond
|
||||
)
|
||||
# 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
|
||||
int(timestring[4:6]), # month
|
||||
int(timestring[6:8]), # day
|
||||
int(timestring[9:11]), # hour
|
||||
int(timestring[12:14]), # minute
|
||||
int(timestring[15:17]), # second
|
||||
int(timestring[18:21])*1000 # microsecond
|
||||
)
|
||||
|
||||
oldest_time=None
|
||||
if "oldest_time" in dir(hdf_node._v_attrs):
|
||||
timestring=hdf_node._v_attrs.__getattr__("oldest_time")
|
||||
oldest_time=datetime.datetime(int(timestring[:4]), # year
|
||||
int(timestring[4:6]), # month
|
||||
int(timestring[6:8]), # day
|
||||
int(timestring[9:11]), # hour
|
||||
int(timestring[12:14]), # minute
|
||||
int(timestring[15:17]), # second
|
||||
int(timestring[18:21])*1000 # microsecond
|
||||
)
|
||||
# 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
|
||||
int(timestring[4:6]), # month
|
||||
int(timestring[6:8]), # day
|
||||
int(timestring[9:11]), # hour
|
||||
int(timestring[12:14]), # minute
|
||||
int(timestring[15:17]), # second
|
||||
int(timestring[18:21])*1000 # microsecond
|
||||
)
|
||||
|
||||
if oldest_time is None or earliest_time is None:
|
||||
accu.time_period=None
|
||||
@@ -1212,7 +1252,7 @@ def read_from_hdf(hdf_node):
|
||||
accu.y_square=[]
|
||||
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)
|
||||
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"))
|
||||
|
||||
+339
-13
@@ -7,6 +7,7 @@ import collections
|
||||
import threading
|
||||
import traceback
|
||||
import io
|
||||
import numpy
|
||||
|
||||
class DataPool(collections.abc.MutableMapping):
|
||||
"""
|
||||
@@ -48,6 +49,7 @@ class DataPool(collections.abc.MutableMapping):
|
||||
self.__mydict={}
|
||||
self.__dictlock=threading.Lock()
|
||||
self.__registered_listeners=[]
|
||||
self.__dirty_keys=set()
|
||||
|
||||
def __getitem__(self, name):
|
||||
try:
|
||||
@@ -64,6 +66,7 @@ class DataPool(collections.abc.MutableMapping):
|
||||
else:
|
||||
e=DataPool.Event(DataPool.Event.new_key, name,self)
|
||||
self.__mydict[name]=value
|
||||
self.__dirty_keys.add(name)
|
||||
finally:
|
||||
self.__dictlock.release()
|
||||
self.__send_event(e)
|
||||
@@ -73,6 +76,7 @@ class DataPool(collections.abc.MutableMapping):
|
||||
try:
|
||||
self.__dictlock.acquire()
|
||||
del self.__mydict[name]
|
||||
self.__dirty_keys.add(name)
|
||||
finally:
|
||||
self.__dictlock.release()
|
||||
self.__send_event(DataPool.Event(DataPool.Event.deleted_key,name,self))
|
||||
@@ -106,18 +110,74 @@ class DataPool(collections.abc.MutableMapping):
|
||||
self.__send_event(DataPool.Event(DataPool.Event.destroy))
|
||||
self.__registered_listeners=None
|
||||
|
||||
def write_hdf5(self,hdffile,where="/",name="data_pool", complib=None, complevel=None):
|
||||
if type(hdffile) is bytes:
|
||||
def write_hdf5(self,hdffile,where="/",name="data_pool", complib=None, complevel=None, incremental=False):
|
||||
|
||||
"""
|
||||
Writes the content of a DataPool object to an HDF5 file using the `tables` library.
|
||||
|
||||
This method serializes the entire content of the data pool into an HDF5 file, creating
|
||||
appropriate groups and arrays based on the keys and values stored in the data pool.
|
||||
Nested storage paths are supported, and group or array names are
|
||||
converted into valid HDF5 names. Paths are prepended with "dir_", results with "dict_".
|
||||
If duplicate names arise, extensions (like `_001`, `_002`)
|
||||
are appended to ensure uniqueness.
|
||||
|
||||
Parameters:
|
||||
hdffile : str | bytes | tables.File
|
||||
Path to the HDF5 file or an already open `tables.File` object where the
|
||||
data should be written. Strings or bytes are interpreted as file paths.
|
||||
|
||||
where : str, optional
|
||||
The root HDF5 group path under which the data pool will be stored.
|
||||
Defaults to "/".
|
||||
|
||||
name : str, optional
|
||||
The name of the root HDF5 group under which the data pool is saved.
|
||||
Defaults to "data_pool".
|
||||
|
||||
complib : str, optional
|
||||
Compression library to be used when creating HDF5 arrays. Optional, defaults
|
||||
to None, meaning no compression is applied.
|
||||
|
||||
complevel : int, optional
|
||||
Compression level to use when applying compression. Has no effect if `complib`
|
||||
is set to None.
|
||||
|
||||
Raises:
|
||||
Exception
|
||||
If `hdffile` is neither a string nor a valid `tables.File` object.
|
||||
|
||||
Notes:
|
||||
- This method assumes that the content of the data pool does not change during the
|
||||
write operation.
|
||||
- Keys in the data pool starting with "__" (double underscores) are skipped.
|
||||
- If a key contains nested paths (e.g., "group1/subgroup2/item"), intermediate
|
||||
groups are created as necessary.
|
||||
- Group or array names are converted to valid HDF5 names, and name conflicts
|
||||
are resolved by appending extensions (e.g., "_001").
|
||||
- Errors during the writing process are logged to the console, and traceback
|
||||
information is displayed for debugging purposes.
|
||||
"""
|
||||
if isinstance(hdffile, (bytes, str)):
|
||||
dump_file=tables.open_file(hdffile, mode="a")
|
||||
elif isinstance(hdffile,tables.File):
|
||||
dump_file=hdffile
|
||||
else:
|
||||
raise Exception("expecting hdffile or string")
|
||||
|
||||
dump_group=dump_file.create_group(where, name, "DAMARIS data pool")
|
||||
if where in dump_file and name in dump_file.get_node(where):
|
||||
dump_group = dump_file.get_node(where, name)
|
||||
else:
|
||||
dump_group = dump_file.create_group(where, name, "DAMARIS data pool")
|
||||
|
||||
self.__dictlock.acquire()
|
||||
dict_keys=list(self.__mydict.keys())
|
||||
if incremental:
|
||||
dict_keys=list(self.__dirty_keys)
|
||||
else:
|
||||
# Full sync: current keys + any other dirty keys (e.g. deletions)
|
||||
dict_keys=list(set(self.__mydict.keys()) | self.__dirty_keys)
|
||||
self.__dictlock.release()
|
||||
|
||||
try:
|
||||
for key in dict_keys:
|
||||
if key.startswith("__"):
|
||||
@@ -142,21 +202,37 @@ class DataPool(collections.abc.MutableMapping):
|
||||
|
||||
# convert last part of key to a valid name
|
||||
group_keyname="dict_"+str(namelist[-1]).translate(DataPool.translation_table)
|
||||
# avoid double names by adding number extension
|
||||
|
||||
# find existing node for this key
|
||||
if group_keyname in dump_dir:
|
||||
extension_count=0
|
||||
while group_keyname+"_%03d"%extension_count in dump_dir:
|
||||
extension_count+=1
|
||||
group_keyname+="_%03d"%extension_count
|
||||
if dump_dir._v_children[group_keyname]._v_title == key:
|
||||
dump_file.remove_node(dump_dir, group_keyname, recursive=True)
|
||||
else:
|
||||
extension_count = 0
|
||||
while group_keyname + "_%03d" % extension_count in dump_dir:
|
||||
suffix_name = group_keyname + "_%03d" % extension_count
|
||||
if dump_dir._v_children[suffix_name]._v_title == key:
|
||||
dump_file.remove_node(dump_dir, suffix_name, recursive=True)
|
||||
group_keyname = suffix_name
|
||||
break
|
||||
extension_count += 1
|
||||
else:
|
||||
# truly new key with collision
|
||||
group_keyname += "_%03d" % extension_count
|
||||
|
||||
self.__dictlock.acquire()
|
||||
if key not in self.__mydict:
|
||||
# outdated ...
|
||||
# Key was deleted
|
||||
if key in self.__dirty_keys:
|
||||
self.__dirty_keys.remove(key)
|
||||
self.__dictlock.release()
|
||||
continue
|
||||
value=self.__mydict[key]
|
||||
if key in self.__dirty_keys:
|
||||
self.__dirty_keys.remove(key)
|
||||
self.__dictlock.release()
|
||||
# now write data, assuming, the object is constant during write operation
|
||||
if "write_to_hdf" in dir(value):
|
||||
if hasattr(value, "write_to_hdf"):
|
||||
try:
|
||||
value.write_to_hdf(hdffile=dump_file,
|
||||
where=dump_dir,
|
||||
@@ -170,16 +246,266 @@ class DataPool(collections.abc.MutableMapping):
|
||||
traceback.print_tb(sys.exc_info()[2], None, traceback_file)
|
||||
print("detailed traceback: %s\n"%str(e)+traceback_file.getvalue())
|
||||
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:
|
||||
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
|
||||
|
||||
finally:
|
||||
dump_group=None
|
||||
if type(hdffile) is bytes:
|
||||
if isinstance(hdffile, (bytes, str)):
|
||||
dump_file.close()
|
||||
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):
|
||||
self.__registered_listeners.append(listening_function)
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
|
||||
|
||||
def uses_statistics(self):
|
||||
"""
|
||||
drawable interface method, returns True
|
||||
drawable interface method returns True
|
||||
"""
|
||||
return True
|
||||
|
||||
@@ -203,10 +203,11 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
|
||||
destination can be a file or a filename
|
||||
suitable for further processing
|
||||
"""
|
||||
# write sorted
|
||||
the_destination=destination
|
||||
file_opened = False
|
||||
if type(destination) in (str,):
|
||||
the_destination=open(destination, "w")
|
||||
file_opened = True
|
||||
|
||||
the_destination.write("# quantity:"+str(self.quantity_name)+"\n")
|
||||
the_destination.write("# x y ysigma n\n")
|
||||
@@ -222,7 +223,8 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
|
||||
y.mean_error(),
|
||||
delimiter,
|
||||
y.n))
|
||||
the_destination=None
|
||||
if file_opened:
|
||||
the_destination.close()
|
||||
|
||||
|
||||
def write_to_hdf(self, hdffile, where, name, title, complib=None, complevel=None):
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
from .Resultable import Resultable
|
||||
from .Drawable import Drawable
|
||||
import numpy
|
||||
import threading
|
||||
import tables
|
||||
|
||||
#############################################################################
|
||||
# #
|
||||
@@ -15,17 +18,224 @@ from .Drawable import Drawable
|
||||
class TemperatureResult(Resultable, Drawable):
|
||||
"""
|
||||
Specialised class of Resultable and Drawable
|
||||
Contains recorded temperature data
|
||||
Contains recorded temperature data.
|
||||
|
||||
Attributes:
|
||||
x: List of timestamps in seconds.
|
||||
y: List of temperature readings in Celsius.
|
||||
setpoint: Optional list of setpoint temperatures in Celsius.
|
||||
xlabel: Label for x-axis (default: "Time (s)").
|
||||
ylabel: Label for y-axis (default: "Temperature (C)").
|
||||
"""
|
||||
def __init__(self, x = None, y = None, desc = None, job_id = None, job_date = None):
|
||||
def __init__(self, x = None, y = None, setpoint = None, desc = None, job_id = None, job_date = None):
|
||||
Resultable.__init__(self)
|
||||
Drawable.__init__(self)
|
||||
|
||||
# Set default labels
|
||||
self.xlabel = "Time (s)"
|
||||
self.ylabel = "Temperature (C)"
|
||||
|
||||
# Initialize data lists
|
||||
self.x = [] if x is None else x
|
||||
self.y = [] if y is None else y
|
||||
self.setpoint = [] if setpoint is None else setpoint
|
||||
|
||||
# Set metadata if provided
|
||||
if desc is not None:
|
||||
self.description = desc
|
||||
if job_id is not None:
|
||||
self.job_id = job_id
|
||||
if job_date is not None:
|
||||
self.job_date = job_date
|
||||
|
||||
# Listener management for GUI updates
|
||||
self.__listeners = []
|
||||
self.__listener_lock = threading.Lock()
|
||||
|
||||
def get_number_of_channels(self):
|
||||
"""Returns 1 for temperature data."""
|
||||
return 1
|
||||
|
||||
def register_listener(self, listener):
|
||||
"""Register a listener to be notified when data changes."""
|
||||
with self.__listener_lock:
|
||||
if listener not in self.__listeners:
|
||||
self.__listeners.append(listener)
|
||||
|
||||
def unregister_listener(self, listener):
|
||||
"""Unregister a listener."""
|
||||
with self.__listener_lock:
|
||||
if listener in self.__listeners:
|
||||
self.__listeners.remove(listener)
|
||||
|
||||
def get_xdata(self):
|
||||
"""Returns the x data (timestamps) as numpy array."""
|
||||
if isinstance(self.x, numpy.ndarray):
|
||||
return self.x
|
||||
return numpy.array(self.x, dtype=float) if self.x else numpy.array([], dtype=float)
|
||||
|
||||
def get_ydata(self, channel=0):
|
||||
"""Returns the y data (temperature readings) as numpy array."""
|
||||
if channel == 0:
|
||||
if isinstance(self.y, numpy.ndarray):
|
||||
return self.y
|
||||
return numpy.array(self.y, dtype=float) if self.y else numpy.array([], dtype=float)
|
||||
return numpy.array([], dtype=float)
|
||||
|
||||
def __len__(self):
|
||||
"""Returns the number of data points."""
|
||||
return len(self.y) if self.y else 0
|
||||
|
||||
def uses_statistics(self):
|
||||
"""Returns False as temperature data doesn't use statistics."""
|
||||
return False
|
||||
|
||||
def ready_for_drawing_error(self):
|
||||
"""Returns False as temperature data doesn't have error bars."""
|
||||
return False
|
||||
|
||||
def get_yerr(self, channel=0):
|
||||
"""Returns empty error data."""
|
||||
return numpy.array([], dtype=float)
|
||||
|
||||
def get_errorplotdata(self):
|
||||
"""Returns empty error plot data."""
|
||||
return [[], [], []]
|
||||
|
||||
def get_lineplotdata(self):
|
||||
"""Returns line plot data (not used for temperature)."""
|
||||
return [[], []]
|
||||
|
||||
def get_xmin(self):
|
||||
"""Returns minimum of x."""
|
||||
xdata = self.get_xdata()
|
||||
return xdata.min() if len(xdata) > 0 else 0
|
||||
|
||||
def get_xmax(self):
|
||||
"""Returns maximum of x."""
|
||||
xdata = self.get_xdata()
|
||||
return xdata.max() if len(xdata) > 0 else 0
|
||||
|
||||
def get_ymin(self):
|
||||
"""Returns minimum of y."""
|
||||
ydata = self.get_ydata(0)
|
||||
return ydata.min() if len(ydata) > 0 else 0
|
||||
|
||||
def get_ymax(self):
|
||||
"""Returns maximum of y."""
|
||||
ydata = self.get_ydata(0)
|
||||
return ydata.max() if len(ydata) > 0 else 0
|
||||
|
||||
def get_xminpos(self):
|
||||
"""Returns smallest positive value of x."""
|
||||
xdata = self.get_xdata()
|
||||
mask = xdata > 0
|
||||
if numpy.any(mask):
|
||||
return xdata[mask].min()
|
||||
return 0
|
||||
|
||||
def get_yminpos(self):
|
||||
"""Returns smallest positive value of y."""
|
||||
ydata = self.get_ydata(0)
|
||||
mask = ydata > 0
|
||||
if numpy.any(mask):
|
||||
return ydata[mask].min()
|
||||
return 0
|
||||
|
||||
def write_to_hdf(self, hdffile, where, name, title, complib=None, complevel=None):
|
||||
"""
|
||||
Write the temperature result to an HDF5 group.
|
||||
|
||||
if (x is None) and (y is None) and (desc is None) and (job_id is None) and (job_date is None):
|
||||
pass
|
||||
Stores x (timestamps), y (temperature readings), and setpoint arrays
|
||||
as an HDF5 table, with metadata saved as group attributes.
|
||||
"""
|
||||
h5_table_format = {
|
||||
"x": tables.Float64Col(),
|
||||
"y": tables.Float64Col(),
|
||||
"setpoint": tables.Float64Col(),
|
||||
}
|
||||
filter = None
|
||||
if complib is not None:
|
||||
if complevel is None:
|
||||
complevel = 9
|
||||
filter = tables.Filters(complevel=complevel, complib=complib, shuffle=1)
|
||||
|
||||
elif (x is not None) and (y is not None) and (desc is not None) and (job_id is not None) and (job_date is not None):
|
||||
pass
|
||||
with self.__listener_lock:
|
||||
xdata = numpy.array(self.x, dtype=float) if self.x else numpy.array([], dtype=float)
|
||||
ydata = numpy.array(self.y, dtype=float) if self.y else numpy.array([], dtype=float)
|
||||
spdata = numpy.array(self.setpoint, dtype=float) if self.setpoint else numpy.array([], dtype=float)
|
||||
|
||||
else:
|
||||
raise ValueError("Wrong usage of __init__!")
|
||||
# Use the length of the longest array; shorter ones are padded with 0
|
||||
n = max(len(xdata), len(ydata), 1)
|
||||
if len(xdata) < n:
|
||||
xdata = numpy.pad(xdata, (0, n - len(xdata)), constant_values=0.0)
|
||||
if len(ydata) < n:
|
||||
ydata = numpy.pad(ydata, (0, n - len(ydata)), constant_values=0.0)
|
||||
if len(spdata) < n:
|
||||
spdata = numpy.pad(spdata, (0, n - len(spdata)), constant_values=0.0)
|
||||
|
||||
temp_table = hdffile.create_table(
|
||||
where=where, name=name,
|
||||
description=h5_table_format,
|
||||
title=title,
|
||||
filters=filter,
|
||||
expectedrows=n,
|
||||
)
|
||||
temp_table.flavor = "numpy"
|
||||
temp_table.attrs.damaris_type = "TemperatureResult"
|
||||
temp_table.attrs.xlabel = self.xlabel
|
||||
temp_table.attrs.ylabel = self.ylabel
|
||||
|
||||
if hasattr(self, "job_id") and self.job_id is not None:
|
||||
temp_table.attrs.job_id = self.job_id
|
||||
if hasattr(self, "job_date") and self.job_date is not None:
|
||||
temp_table.attrs.time = "%04d%02d%02d %02d:%02d:%02d.%03d" % (
|
||||
self.job_date.year,
|
||||
self.job_date.month,
|
||||
self.job_date.day,
|
||||
self.job_date.hour,
|
||||
self.job_date.minute,
|
||||
self.job_date.second,
|
||||
self.job_date.microsecond // 1000,
|
||||
)
|
||||
if hasattr(self, "description") and self.description is not None:
|
||||
for (key, value) in self.description.items():
|
||||
temp_table.attrs.__setattr__("description_" + key, str(value))
|
||||
|
||||
row = temp_table.row
|
||||
for i in range(n):
|
||||
row["x"] = xdata[i]
|
||||
row["y"] = ydata[i]
|
||||
row["setpoint"] = spdata[i]
|
||||
row.append()
|
||||
|
||||
temp_table.flush()
|
||||
|
||||
def __repr__(self):
|
||||
"""String representation of the temperature result."""
|
||||
return f"TemperatureResult(points={len(self)}, job_id={self.job_id})"
|
||||
|
||||
|
||||
def read_from_hdf(hdf_node):
|
||||
"""
|
||||
Read a TemperatureResult object from an HDF5 table node.
|
||||
Returns None if the node is not suitable.
|
||||
"""
|
||||
if not isinstance(hdf_node, tables.Table):
|
||||
return None
|
||||
|
||||
if hdf_node._v_attrs.damaris_type != "TemperatureResult":
|
||||
return None
|
||||
|
||||
tr = TemperatureResult()
|
||||
tr.xlabel = getattr(hdf_node._v_attrs, "xlabel", "Time (s)")
|
||||
tr.ylabel = getattr(hdf_node._v_attrs, "ylabel", "Temperature (C)")
|
||||
|
||||
for r in hdf_node.iterrows():
|
||||
tr.x.append(r["x"])
|
||||
tr.y.append(r["y"])
|
||||
sp = r["setpoint"]
|
||||
if sp != 0.0 or len(tr.setpoint) > 0:
|
||||
tr.setpoint.append(sp)
|
||||
|
||||
return tr
|
||||
@@ -6,5 +6,5 @@ from damaris.data.Error_Result import Error_Result
|
||||
from damaris.data.Config_Result import Config_Result
|
||||
from damaris.data.Temperature import TemperatureResult
|
||||
|
||||
__all__=["ADC_Result", "Accumulation", "MeasurementResult", "AccumulatedValue", "DataPool", "FFT", "Error_Result", "Config_Result", "TemperatureResult" ]
|
||||
__all__=["ADC_Result", "Accumulation", "MeasurementResult", "AccumulatedValue", "DataPool", "Error_Result", "Config_Result", "TemperatureResult" ]
|
||||
|
||||
@@ -96,12 +96,10 @@ class Experiment:
|
||||
|
||||
#for tracking the experiment length:
|
||||
#because loops are possible we need to track the length for each loop level
|
||||
self.total_time=[]
|
||||
self.total_time.append(0.0)
|
||||
self.total_time=[0.0]
|
||||
|
||||
#and we need to know the number of iterations of the loops to multiply the state.
|
||||
self.loop_iterations=[]
|
||||
self.loop_iterations.append(1)
|
||||
self.loop_iterations=[1]
|
||||
|
||||
# Commands -------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
+460
-48
@@ -61,7 +61,8 @@ from damaris.gui import ResultReader, ResultHandling
|
||||
from damaris.gui import BackendDriver
|
||||
#from damaris.gui.gtkcodebuffer import CodeBuffer, SyntaxLoader
|
||||
#from damaris.data import Drawable # this is a base class, it should be used...
|
||||
from damaris.data import DataPool, Accumulation, ADC_Result, MeasurementResult
|
||||
from damaris.data import DataPool, Accumulation, ADC_Result, MeasurementResult, TemperatureResult
|
||||
# TemperatureMonitor imported lazily to avoid circular import issues
|
||||
|
||||
# default, can be set to true from start script
|
||||
debug = False
|
||||
@@ -88,7 +89,6 @@ class logstream:
|
||||
def __del__( self ):
|
||||
pass
|
||||
|
||||
|
||||
global log
|
||||
log = logstream( )
|
||||
sys.stdout = log
|
||||
@@ -134,9 +134,6 @@ class LockFile:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class DamarisGUI:
|
||||
ExpScript_Display = 0
|
||||
ResScript_Display = 1
|
||||
@@ -169,7 +166,7 @@ class DamarisGUI:
|
||||
|
||||
self.log = LogWindow( self.xml_gui , self)
|
||||
|
||||
self.sw = ScriptWidgets( self.xml_gui )
|
||||
self.sw = ScriptWidgets( self.xml_gui, self )
|
||||
|
||||
self.toolbar_init( )
|
||||
|
||||
@@ -184,6 +181,10 @@ class DamarisGUI:
|
||||
|
||||
#to stop queued experiments
|
||||
self.stop_experiment_flag = threading.Event()
|
||||
|
||||
# Temperature monitoring
|
||||
self.temp_monitor = None
|
||||
self.temp_controller = None
|
||||
|
||||
exp_script = ""
|
||||
if exp_script_filename is not None and exp_script_filename != "":
|
||||
@@ -218,6 +219,7 @@ class DamarisGUI:
|
||||
"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_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_save_as_button_clicked": self.sw.save_file_as,
|
||||
"on_toolbar_save_file_button_clicked": self.sw.save_file,
|
||||
@@ -297,6 +299,14 @@ class DamarisGUI:
|
||||
self.config = 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
|
||||
|
||||
# first global events
|
||||
@@ -507,6 +517,8 @@ class DamarisGUI:
|
||||
self.data = self.si.data
|
||||
# run frontend and script engines
|
||||
self.monitor.observe_data_pool( self.data )
|
||||
# Start temperature monitoring if configured (before scripts start)
|
||||
self.start_temperature_monitoring(actual_config)
|
||||
self.si.runScripts( )
|
||||
except Exception as e:
|
||||
#print "ToDo evaluate exception",str(e), "at",traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
|
||||
@@ -567,6 +579,7 @@ class DamarisGUI:
|
||||
self.dump_filename = ""
|
||||
if actual_config[ "data_pool_name" ] != "":
|
||||
self.dump_states( init=True )
|
||||
|
||||
gobject.timeout_add( 200, self.observe_running_experiment )
|
||||
|
||||
def observe_running_experiment( self ):
|
||||
@@ -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]
|
||||
if len( still_running ) == 0:
|
||||
# Stop temperature monitoring when experiment finishes
|
||||
self.stop_temperature_monitoring()
|
||||
if self.save_thread is None and self.dump_filename != "":
|
||||
print("all subprocesses ended, saving data pool")
|
||||
# thread to save data...
|
||||
@@ -869,15 +884,6 @@ class DamarisGUI:
|
||||
filters=tables.Filters( complevel=9, complib='zlib' ) )
|
||||
|
||||
if dump_file is None and os.path.isfile( self.dump_filename ) and tables.is_pytables_file( self.dump_filename ):
|
||||
# take some data from dump file and repack
|
||||
os.rename( self.dump_filename, self.dump_filename + ".bak" )
|
||||
old_dump_file = tables.open_file( self.dump_filename + ".bak", mode="r+" )
|
||||
if "data_pool" in old_dump_file.root:
|
||||
old_dump_file.remove_node( where="/", name="data_pool", recursive=True )
|
||||
old_dump_file.copy_file( self.dump_filename )
|
||||
old_dump_file.close( )
|
||||
del old_dump_file
|
||||
os.remove( self.dump_filename + ".bak" )
|
||||
# prepare for update
|
||||
dump_file = tables.open_file( self.dump_filename, mode="r+" )
|
||||
|
||||
@@ -894,7 +900,8 @@ class DamarisGUI:
|
||||
|
||||
# save the data!
|
||||
self.data.write_hdf5( dump_file, where="/", name="data_pool",
|
||||
complib=self.dump_complib, complevel=self.dump_complevel )
|
||||
complib=self.dump_complib, complevel=self.dump_complevel,
|
||||
incremental=not init )
|
||||
|
||||
# now save additional information
|
||||
e = self.si.data.get( "__recentexperiment", -1 ) + 1
|
||||
@@ -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]
|
||||
for r in still_running:
|
||||
r.quit_flag.set( )
|
||||
|
||||
# Stop temperature monitoring
|
||||
self.stop_temperature_monitoring()
|
||||
|
||||
self.state = DamarisGUI.Stop_State
|
||||
|
||||
def start_temperature_monitoring(self, config):
|
||||
"""
|
||||
Start temperature monitoring thread if configured.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary from self.config.get()
|
||||
"""
|
||||
# Check if temperature monitoring is enabled
|
||||
if not config.get("enable_temperature_monitoring", False):
|
||||
# Stop any existing monitor if temperature monitoring is disabled
|
||||
self.stop_temperature_monitoring()
|
||||
return
|
||||
|
||||
# Stop any existing monitor before starting a new one
|
||||
if self.temp_monitor is not None:
|
||||
self.stop_temperature_monitoring()
|
||||
|
||||
controller_type = config.get("temperature_controller_type", "eurotherm")
|
||||
temp_interval = config.get("temperature_interval", 2.0)
|
||||
|
||||
try:
|
||||
# Import the factory and TemperatureMonitor (lazy import to avoid circular issues)
|
||||
from damaris.tools.temperature import create_controller
|
||||
from damaris.gui.TemperatureMonitor import TemperatureMonitor
|
||||
|
||||
# Create controller based on type
|
||||
if controller_type == "eurotherm":
|
||||
temp_device = config.get("temperature_device", "")
|
||||
temp_baudrate = config.get("temperature_baudrate", 19200)
|
||||
|
||||
if not temp_device:
|
||||
print("Temperature monitoring: No device configured for Eurotherm")
|
||||
return
|
||||
|
||||
self.temp_controller = create_controller(
|
||||
"eurotherm", temp_device, baudrate=temp_baudrate
|
||||
)
|
||||
print(f"Temperature monitoring started with Eurotherm on {temp_device} (interval: {temp_interval}s)")
|
||||
|
||||
elif controller_type == "simulated":
|
||||
# Simulated controller doesn't need device configuration
|
||||
# Use default parameters: 290K base, 10K amplitude, 60s period, 5% noise, 10% missing
|
||||
self.temp_controller = create_controller("simulated")
|
||||
print(f"Temperature monitoring started with simulated controller (interval: {temp_interval}s)")
|
||||
|
||||
else:
|
||||
print(f"Temperature monitoring: Unknown controller type: {controller_type}")
|
||||
return
|
||||
|
||||
# Create and start monitor thread
|
||||
self.temp_monitor = TemperatureMonitor(
|
||||
controller=self.temp_controller,
|
||||
interval=temp_interval,
|
||||
data_pool=self.data,
|
||||
data_key="temperature"
|
||||
)
|
||||
self.temp_monitor.start()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to start temperature monitoring: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self.temp_controller = None
|
||||
self.temp_monitor = None
|
||||
|
||||
def stop_temperature_monitoring(self):
|
||||
"""
|
||||
Stop the temperature monitoring thread if running.
|
||||
"""
|
||||
if self.temp_monitor is not None:
|
||||
try:
|
||||
self.temp_monitor.stop()
|
||||
print("Temperature monitoring stopped")
|
||||
except Exception as e:
|
||||
print(f"Error stopping temperature monitoring: {e}")
|
||||
finally:
|
||||
self.temp_monitor = None
|
||||
|
||||
if self.temp_controller is not None:
|
||||
try:
|
||||
self.temp_controller.close()
|
||||
except Exception as e:
|
||||
print(f"Error closing temperature controller: {e}")
|
||||
finally:
|
||||
self.temp_controller = None
|
||||
|
||||
def documentation_init( self ):
|
||||
|
||||
self.doc_urls = {
|
||||
@@ -1103,11 +1200,12 @@ class LogWindow:
|
||||
|
||||
|
||||
class ScriptWidgets:
|
||||
def __init__( self, xml_gui ):
|
||||
def __init__( self, xml_gui, outer_space ):
|
||||
"""
|
||||
initialize text widgets with text
|
||||
"""
|
||||
self.xml_gui = xml_gui
|
||||
self.outer_space = outer_space
|
||||
|
||||
# my states
|
||||
# editing enabled/disabled
|
||||
@@ -1174,7 +1272,7 @@ get_ylabel set_ylabel"""
|
||||
|
||||
self.compwgen = gtksourceview2.CompletionWords.new("Keywords", None)
|
||||
self.compwgen.register(self.keywordsbuffer)
|
||||
self.compwgen.set_property("minimum-word-size", 1)
|
||||
self.compwgen.set_property("minimum-word-size", 2)
|
||||
|
||||
self.compwexp = gtksourceview2.CompletionWords.new("Words", None)
|
||||
self.compwexp.register(self.experiment_script_textbuffer)
|
||||
@@ -1572,7 +1670,7 @@ get_ylabel set_ylabel"""
|
||||
elif response == gtk.ResponseType.CANCEL:
|
||||
return 0
|
||||
|
||||
def response( self, response_id, script_widget ):
|
||||
def response( dialog, response_id, script_widget ):
|
||||
if response_id == gtk.ResponseType.OK:
|
||||
file_name = dialog.get_filename( )
|
||||
if file_name is None:
|
||||
@@ -1580,7 +1678,13 @@ get_ylabel set_ylabel"""
|
||||
|
||||
script_filename = os.path.abspath( file_name )
|
||||
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
|
||||
|
||||
script_string = script_widget.load_file_as_unicode( script_filename )
|
||||
@@ -1613,6 +1717,94 @@ get_ylabel set_ylabel"""
|
||||
|
||||
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 ):
|
||||
"""
|
||||
save file to associated filename
|
||||
@@ -1967,6 +2159,73 @@ class ConfigTab:
|
||||
backend_vbox.pack_start(self.config_adc_bit_depth_hbox, False, False, 0)
|
||||
self.config_adc_bit_depth_hbox.show_all()
|
||||
|
||||
# Temperature monitoring settings - single line layout
|
||||
self.config_temp_hbox = gtk.HBox(homogeneous=False, spacing=6)
|
||||
|
||||
# Controller type selector
|
||||
self.config_temp_controller_type_label = gtk.Label(label="Controller:")
|
||||
self.config_temp_controller_type_combobox = gtk.ComboBoxText()
|
||||
self.config_temp_controller_type_combobox.append_text("eurotherm")
|
||||
self.config_temp_controller_type_combobox.append_text("simulated")
|
||||
self.config_temp_controller_type_combobox.set_active(0) # Default to eurotherm
|
||||
self.config_temp_controller_type_combobox.set_tooltip_text("Select temperature controller type")
|
||||
self.config_temp_controller_type_combobox.connect("changed", self.on_temp_controller_type_changed)
|
||||
|
||||
self.config_temp_controller_type_hbox = gtk.HBox(homogeneous=False, spacing=0)
|
||||
self.config_temp_controller_type_hbox.pack_start(self.config_temp_controller_type_label, False, False, 0)
|
||||
self.config_temp_controller_type_hbox.pack_start(self.config_temp_controller_type_combobox, False, False, 0)
|
||||
self.config_temp_hbox.pack_start(self.config_temp_controller_type_hbox, False, False, 0)
|
||||
|
||||
# Enable checkbox
|
||||
self.config_temp_monitoring_checkbutton = gtk.CheckButton(label="Temp:")
|
||||
self.config_temp_monitoring_checkbutton.set_tooltip_text("Enable periodic temperature reading from controller")
|
||||
self.config_temp_hbox.pack_start(self.config_temp_monitoring_checkbutton, False, False, 0)
|
||||
|
||||
# Device file chooser with entry (shown for eurotherm, hidden for simulated)
|
||||
self.config_temp_device_entry = gtk.Entry()
|
||||
self.config_temp_device_entry.set_tooltip_text("Serial port device (e.g., /dev/serial/by-id/...)")
|
||||
self.config_temp_device_entry.set_width_chars(30)
|
||||
self.config_temp_device_button = gtk.Button(label="...")
|
||||
self.config_temp_device_button.set_tooltip_text("Browse for temperature controller device")
|
||||
self.config_temp_device_button.connect("clicked", self.on_temp_device_browse_clicked)
|
||||
|
||||
self.config_temp_device_hbox = gtk.HBox(homogeneous=False, spacing=0)
|
||||
self.config_temp_device_hbox.pack_start(self.config_temp_device_entry, True, True, 0)
|
||||
self.config_temp_device_hbox.pack_start(self.config_temp_device_button, False, False, 0)
|
||||
self.config_temp_hbox.pack_start(self.config_temp_device_hbox, True, True, 0)
|
||||
|
||||
# Interval spinbutton
|
||||
self.config_temp_interval_label = gtk.Label(label="Interval (s):")
|
||||
self.config_temp_interval_spinbutton = gtk.SpinButton.new_with_range(0.1, 60, 0.1)
|
||||
self.config_temp_interval_spinbutton.set_value(2.0) # Default to 2 seconds
|
||||
self.config_temp_interval_spinbutton.set_tooltip_text("Time between temperature readings in seconds")
|
||||
|
||||
self.config_temp_interval_hbox = gtk.HBox(homogeneous=False, spacing=6)
|
||||
self.config_temp_interval_hbox.pack_start(self.config_temp_interval_label, False, False, 0)
|
||||
self.config_temp_interval_hbox.pack_start(self.config_temp_interval_spinbutton, False, False, 0)
|
||||
self.config_temp_hbox.pack_start(self.config_temp_interval_hbox, False, False, 0)
|
||||
|
||||
# Baud rate (for eurotherm)
|
||||
self.config_temp_baudrate_combobox = gtk.ComboBoxText()
|
||||
self.config_temp_baudrate_combobox.append_text("19200")
|
||||
self.config_temp_baudrate_combobox.append_text("9600")
|
||||
self.config_temp_baudrate_combobox.append_text("38400")
|
||||
self.config_temp_baudrate_combobox.append_text("57600")
|
||||
self.config_temp_baudrate_combobox.append_text("115200")
|
||||
self.config_temp_baudrate_combobox.set_active(0)
|
||||
self.config_temp_baudrate_combobox.set_tooltip_text("Serial baud rate for temperature controller")
|
||||
self.config_temp_baudrate_hbox = gtk.HBox(homogeneous=False, spacing=6)
|
||||
self.config_temp_baudrate_hbox.pack_start(self.config_temp_baudrate_combobox, False, False, 0)
|
||||
self.config_temp_hbox.pack_start(self.config_temp_baudrate_hbox, False, False, 0)
|
||||
# Note: baud rate is shown for eurotherm, hidden for simulated
|
||||
|
||||
# Set initial visibility based on controller type
|
||||
self._update_temp_widgets_visibility()
|
||||
|
||||
if backend_vbox:
|
||||
backend_vbox.pack_start(self.config_temp_hbox, False, False, 0)
|
||||
self.config_temp_hbox.show_all()
|
||||
|
||||
# insert version informations
|
||||
components_text = """
|
||||
operating system %(os)s
|
||||
@@ -2073,10 +2332,28 @@ pygobject version %(pygobject)s
|
||||
"data_pool_complib": complib,
|
||||
"data_pool_comprate": self.config_data_pool_comprate.get_value_as_int( ),
|
||||
"script_font": self.config_script_font_button.get_font_name( ),
|
||||
"adc_bit_depth": self.config_adc_bit_depth_spinbutton.get_value_as_int()
|
||||
"adc_bit_depth": self.config_adc_bit_depth_spinbutton.get_value_as_int(),
|
||||
# Temperature monitoring configuration (only if widgets exist)
|
||||
"enable_temperature_monitoring": self.config_temp_monitoring_checkbutton.get_active() if hasattr(self, 'config_temp_monitoring_checkbutton') else False,
|
||||
"temperature_controller_type": self.config_temp_controller_type_combobox.get_active_text() if hasattr(self, 'config_temp_controller_type_combobox') else "eurotherm",
|
||||
"temperature_device": self.config_temp_device_entry.get_text() if hasattr(self, 'config_temp_device_entry') else "",
|
||||
"temperature_baudrate": self._get_temp_baudrate() if hasattr(self, 'config_temp_baudrate_combobox') else 19200,
|
||||
"temperature_interval": self.config_temp_interval_spinbutton.get_value() if hasattr(self, 'config_temp_interval_spinbutton') else 2.0
|
||||
}
|
||||
ADC_Result.default_bit_depth = actual_config["adc_bit_depth"]
|
||||
return actual_config
|
||||
|
||||
def _get_temp_baudrate(self):
|
||||
"""Get selected baud rate from combobox."""
|
||||
if not hasattr(self, 'config_temp_baudrate_combobox'):
|
||||
return 19200
|
||||
active_text = self.config_temp_baudrate_combobox.get_active_text()
|
||||
if active_text:
|
||||
try:
|
||||
return int(active_text)
|
||||
except ValueError:
|
||||
pass
|
||||
return 19200
|
||||
|
||||
def set( self, config ):
|
||||
if "start_backend" in config:
|
||||
@@ -2108,6 +2385,41 @@ pygobject version %(pygobject)s
|
||||
ADC_Result.default_bit_depth = int(config["adc_bit_depth"])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Temperature monitoring configuration (only if widgets exist)
|
||||
if hasattr(self, 'config_temp_monitoring_checkbutton') and "enable_temperature_monitoring" in config:
|
||||
try:
|
||||
self.config_temp_monitoring_checkbutton.set_active(bool(config["enable_temperature_monitoring"]))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if hasattr(self, 'config_temp_controller_type_combobox') and "temperature_controller_type" in config:
|
||||
try:
|
||||
controller_type = config["temperature_controller_type"]
|
||||
if controller_type in ["eurotherm", "simulated"]:
|
||||
self._set_combo_active_text(self.config_temp_controller_type_combobox, controller_type)
|
||||
# Update widget visibility based on type
|
||||
self._update_temp_widgets_visibility()
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if hasattr(self, 'config_temp_device_entry') and "temperature_device" in config:
|
||||
try:
|
||||
self.config_temp_device_entry.set_text(str(config["temperature_device"]))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if hasattr(self, 'config_temp_baudrate_combobox') and "temperature_baudrate" in config:
|
||||
try:
|
||||
baudrate = str(config["temperature_baudrate"])
|
||||
if baudrate in ["9600", "19200", "38400", "57600", "115200"]:
|
||||
self._set_combo_active_text(self.config_temp_baudrate_combobox, baudrate)
|
||||
else:
|
||||
self.config_temp_baudrate_combobox.set_active(0) # Default to 19200
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if hasattr(self, 'config_temp_interval_spinbutton') and "temperature_interval" in config:
|
||||
try:
|
||||
self.config_temp_interval_spinbutton.set_value(float(config["temperature_interval"]))
|
||||
except (ValueError, TypeError):
|
||||
self.config_temp_interval_spinbutton.set_value(2.0)
|
||||
if "data_pool_complib" in config:
|
||||
# find combo-box entry and make it active...
|
||||
model = self.config_data_pool_complib.get_model( )
|
||||
@@ -2124,6 +2436,84 @@ pygobject version %(pygobject)s
|
||||
def on_adc_bit_depth_changed(self, widget):
|
||||
ADC_Result.default_bit_depth = widget.get_value_as_int()
|
||||
|
||||
def on_temp_device_browse_clicked(self, widget):
|
||||
"""
|
||||
Open file chooser dialog to select temperature controller device.
|
||||
Defaults to /dev/serial/by-id/ which is typically used for USB serial converters.
|
||||
"""
|
||||
dialog = gtk.FileChooserDialog(
|
||||
title="Select Temperature Controller Device",
|
||||
parent=None,
|
||||
action=gtk.FileChooserAction.OPEN,
|
||||
buttons=(
|
||||
gtk.STOCK_CANCEL, gtk.ResponseType.CANCEL,
|
||||
gtk.STOCK_OPEN, gtk.ResponseType.OK
|
||||
)
|
||||
)
|
||||
|
||||
# Set default folder to /dev/serial/by-id/ if it exists
|
||||
default_folder = "/dev/serial/by-id/"
|
||||
if os.path.isdir(default_folder):
|
||||
dialog.set_current_folder(default_folder)
|
||||
else:
|
||||
# Fallback to /dev
|
||||
dialog.set_current_folder("/dev/")
|
||||
|
||||
# Only show regular files (device files)
|
||||
dialog.set_select_multiple(False)
|
||||
|
||||
response = dialog.run()
|
||||
if response == gtk.ResponseType.OK:
|
||||
selected_file = dialog.get_filename()
|
||||
self.config_temp_device_entry.set_text(selected_file)
|
||||
|
||||
dialog.destroy()
|
||||
|
||||
def on_temp_controller_type_changed(self, widget):
|
||||
"""Update widget visibility when controller type changes."""
|
||||
self._update_temp_widgets_visibility()
|
||||
|
||||
def _update_temp_widgets_visibility(self):
|
||||
"""Show/hide widgets based on selected controller type."""
|
||||
# Defensive: check if combobox exists
|
||||
if not hasattr(self, 'config_temp_controller_type_combobox'):
|
||||
return
|
||||
|
||||
active_type = self.config_temp_controller_type_combobox.get_active_text()
|
||||
if active_type is None:
|
||||
active_type = "eurotherm"
|
||||
|
||||
# Defensive: check if widgets exist before showing/hiding
|
||||
show_device = active_type == "eurotherm"
|
||||
if hasattr(self, 'config_temp_device_hbox'):
|
||||
if show_device:
|
||||
self.config_temp_device_hbox.show()
|
||||
else:
|
||||
self.config_temp_device_hbox.hide()
|
||||
if hasattr(self, 'config_temp_baudrate_hbox'):
|
||||
if show_device:
|
||||
self.config_temp_baudrate_hbox.show()
|
||||
else:
|
||||
self.config_temp_baudrate_hbox.hide()
|
||||
|
||||
def _set_combo_active_text(self, combobox, text):
|
||||
"""
|
||||
Set a ComboBoxText's active entry by text.
|
||||
|
||||
Gtk.ComboBoxText.set_active_text() does not exist in GTK 3.24.x,
|
||||
so we iterate the model to find the matching entry.
|
||||
"""
|
||||
if not text:
|
||||
return
|
||||
model = combobox.get_model()
|
||||
if model is None:
|
||||
return
|
||||
for i in range(model.iter_n_children(None)):
|
||||
iter = model.iter_nth_child(None, i)
|
||||
if iter and model.get(iter, 0)[0] == text:
|
||||
combobox.set_active(i)
|
||||
return
|
||||
|
||||
def load_config_handler( self, widget ):
|
||||
if self.system_default_filename:
|
||||
self.load_config( self.system_default_filename )
|
||||
@@ -2342,7 +2732,9 @@ class MonitorWidgets:
|
||||
# now let's add a button to the toolbar
|
||||
self.matplot_cursor_button = gtk.ToggleButton(label='')
|
||||
try:
|
||||
image = gtk.Image.new_from_file("/usr/share/icons/breeze/actions/24/crosshairs-symbolic.svg")
|
||||
icon_theme = gtk.IconTheme.get_default()
|
||||
icon = icon_theme.load_icon("crosshairs", 24, 0)
|
||||
image = gtk.Image.new_from_pixbuf(icon)
|
||||
self.matplot_cursor_button.set_image(image)
|
||||
except:
|
||||
pass
|
||||
@@ -2411,7 +2803,7 @@ class MonitorWidgets:
|
||||
def source_list_find_one( self, model, iter, what ):
|
||||
"""find node in subcategory"""
|
||||
while iter is not None:
|
||||
if model.get( iter, 0 )[ 0 ] == what:
|
||||
if str( model.get( iter, 0 )[ 0 ] ) == what:
|
||||
return iter
|
||||
iter = model.iter_next( iter )
|
||||
return iter
|
||||
@@ -2482,31 +2874,50 @@ class MonitorWidgets:
|
||||
register a listener and save reference to data
|
||||
assume to be in gtk/gdk lock
|
||||
"""
|
||||
if self.data_pool is not None:
|
||||
# maybe some extra cleanup needed
|
||||
print("ToDo: cleanup widgets")
|
||||
if self.displayed_data[ 1 ] is not None and hasattr( self.displayed_data[ 1 ], "unregister_listener" ):
|
||||
self.displayed_data[ 1 ].unregister_listener( self.datastructures_listener )
|
||||
self.displayed_data[ 1 ] = None
|
||||
self.displayed_data = [ None, None ]
|
||||
self.data_pool.unregister_listener( self.datapool_listener )
|
||||
self.data_pool = None
|
||||
gdk.threads_enter( )
|
||||
try:
|
||||
if self.data_pool is not None:
|
||||
# maybe some extra cleanup needed
|
||||
print("ToDo: cleanup widgets")
|
||||
if self.displayed_data[ 1 ] is not None and hasattr( self.displayed_data[ 1 ], "unregister_listener" ):
|
||||
self.displayed_data[ 1 ].unregister_listener( self.datastructures_listener )
|
||||
self.displayed_data[ 1 ] = None
|
||||
self.displayed_data = [ None, None ]
|
||||
self.data_pool.unregister_listener( self.datapool_listener )
|
||||
self.data_pool = None
|
||||
|
||||
self.update_counter_lock.acquire( )
|
||||
self.update_counter = 0
|
||||
self.update_counter_lock.release( )
|
||||
|
||||
# display states
|
||||
self.source_list_reset( )
|
||||
self.update_counter_lock.acquire( )
|
||||
self.update_counter = 0
|
||||
self.update_counter_lock.release( )
|
||||
self.__rescale = True
|
||||
self.displayed_data = [ None, None ]
|
||||
self.display_source_path_label.set_label( "" )
|
||||
self.clear_display( )
|
||||
|
||||
# display states
|
||||
self.__rescale = True
|
||||
self.displayed_data = [ None, None ]
|
||||
self.display_source_path_label.set_label( "" )
|
||||
self.clear_display( )
|
||||
|
||||
if data_pool is not None:
|
||||
# keep track of data
|
||||
self.data_pool = data_pool
|
||||
self.data_pool.register_listener( self.datapool_listener )
|
||||
if data_pool is not None:
|
||||
# keep track of data
|
||||
self.data_pool = data_pool
|
||||
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
|
||||
|
||||
@@ -2643,7 +3054,7 @@ class MonitorWidgets:
|
||||
gdk.threads_leave( )
|
||||
elif event.what == DataPool.Event.destroy:
|
||||
gdk.threads_enter( )
|
||||
self.source_list_reset( 'None' )
|
||||
self.source_list_reset( )
|
||||
self.displayed_data = [ None, None ]
|
||||
self.clear_display( )
|
||||
gdk.threads_leave( )
|
||||
@@ -2803,6 +3214,7 @@ class MonitorWidgets:
|
||||
self.matplot_cursor = matplotlib.widgets.Cursor(self.matplot_axes,
|
||||
useblit=False, color='red', linewidth=1)
|
||||
self.matplot_canvas.draw()
|
||||
# TODO: Deactivate the pane button
|
||||
|
||||
else:
|
||||
# Deactivate cursor
|
||||
@@ -2930,7 +3342,7 @@ class MonitorWidgets:
|
||||
|
||||
x_scale = self.display_x_scaling_combobox.get_active_text()
|
||||
y_scale = self.display_y_scaling_combobox.get_active_text()
|
||||
print(x_scale)
|
||||
|
||||
if x_scale == "lin":
|
||||
self.matplot_axes.set_xscale("linear")
|
||||
elif x_scale == "log10":
|
||||
@@ -3001,7 +3413,7 @@ class MonitorWidgets:
|
||||
if in_result is None:
|
||||
self.clear_display( )
|
||||
return
|
||||
if isinstance( in_result, Accumulation ) or isinstance( in_result, ADC_Result ):
|
||||
if isinstance( in_result, Accumulation ) or isinstance( in_result, ADC_Result ) or isinstance( in_result, TemperatureResult ):
|
||||
|
||||
xmin = in_result.get_xmin( )
|
||||
xmax = in_result.get_xmax( )
|
||||
|
||||
@@ -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>
|
||||
</packing>
|
||||
</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>
|
||||
<object class="GtkToolButton" id="toolbar_save_file_button">
|
||||
<property name="visible">True</property>
|
||||
|
||||
@@ -3,6 +3,8 @@ import re
|
||||
import operator
|
||||
from functools import reduce
|
||||
|
||||
from .temperature import TemperatureController, register_controller
|
||||
|
||||
|
||||
DEBUG=False
|
||||
|
||||
@@ -40,7 +42,7 @@ def checksum(message):
|
||||
bcc = (reduce(operator.xor, list(map(ord,message))))
|
||||
return chr(bcc)
|
||||
|
||||
class Eurotherm(object):
|
||||
class Eurotherm(TemperatureController):
|
||||
def __init__(self, serial_device, baudrate = 19200):
|
||||
self.device = standard_device
|
||||
# timeout: 110 ms to get all answers.
|
||||
@@ -100,6 +102,39 @@ class Eurotherm(object):
|
||||
def get_setpoint_temperature(self):
|
||||
return self.read_param('SL')
|
||||
|
||||
# TemperatureController interface implementation
|
||||
|
||||
def get_temperature(self) -> float:
|
||||
"""Read current temperature in Celsius."""
|
||||
temp = self.read_param('PV')
|
||||
if temp is None:
|
||||
return 0.0
|
||||
try:
|
||||
return float(temp)
|
||||
except (ValueError, TypeError):
|
||||
return 0.0
|
||||
|
||||
def get_setpoint(self) -> float:
|
||||
"""Read target setpoint temperature in Celsius."""
|
||||
temp = self.read_param('SL')
|
||||
if temp is None:
|
||||
return 0.0
|
||||
try:
|
||||
return float(temp)
|
||||
except (ValueError, TypeError):
|
||||
return 0.0
|
||||
|
||||
def set_setpoint(self, temperature: float) -> bool:
|
||||
"""Set target temperature in Celsius."""
|
||||
return self.write_param('SL', str(temperature))
|
||||
|
||||
def close(self):
|
||||
"""Close the serial connection."""
|
||||
if hasattr(self, 's') and self.s is not None:
|
||||
if self.s.is_open:
|
||||
self.s.close()
|
||||
self.s = None
|
||||
|
||||
if __name__ == '__main__':
|
||||
import time
|
||||
delta=5
|
||||
@@ -118,3 +153,6 @@ if __name__ == '__main__':
|
||||
f.write(l)
|
||||
f.flush()
|
||||
f.write('# MARK -- %s --\n'%(time.asctime()))
|
||||
|
||||
# Register Eurotherm with the factory
|
||||
register_controller('eurotherm', Eurotherm)
|
||||
@@ -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
|
||||
accu+=ts
|
||||
accu.write_to_hdf(h5, "/", "name", "title")
|
||||
print(accu)
|
||||
try:
|
||||
h5.close()
|
||||
except:
|
||||
|
||||
Reference in new issue
Block a user