Public Access
fix return-value/deprecated API bugs, accumulation lock leaks, code cleanup
- raise instead of silent None for unsupported operands and empty negation - validate before acquiring lock in Accumulation operators (no RLock leak) - Accumulation._copy_provenance helper, ADC_Result._copy_metadata for all result paths - shared _sandbox_stdout_redirect contextmanager for sandboxed scripts - TemperatureMonitor dead code removal (gtk helper, traceback, unused properties) - new unit tests incl. lock-freedom regression test - gitignore sphinx generated doc/_autosummary/ and doc/damaris/
This commit is contained in:
1 parent
91ade4376c
commit
f9d8de7697
26 files changed
+590
-236
No files matched your search
@@ -232,8 +232,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
||||
job_id=self.job_id,
|
||||
job_date=self.job_date,
|
||||
)
|
||||
r.nChannels = self.nChannels
|
||||
r.is_clipped = self.is_clipped
|
||||
self._copy_metadata(r)
|
||||
finally:
|
||||
self.lock.release()
|
||||
|
||||
@@ -277,8 +276,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
||||
tmp_y.append(self.y[i][start:end+1].copy())
|
||||
|
||||
r = ADC_Result(x = tmp_x, y = tmp_y, index = [(0,len(tmp_y[0])-1)], sampl_freq = self.sampling_rate, desc = self.description.copy() if self.description is not None else None, job_id = self.job_id, job_date = self.job_date)
|
||||
r.nChannels = self.nChannels
|
||||
r.is_clipped = self.is_clipped
|
||||
self._copy_metadata(r)
|
||||
self.lock.release()
|
||||
return r
|
||||
|
||||
@@ -488,6 +486,11 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
||||
|
||||
return tmp_string
|
||||
|
||||
def _copy_metadata(self, r):
|
||||
"Copy channel metadata (nChannels, is_clipped) from self to the new result r"
|
||||
r.nChannels = self.nChannels
|
||||
r.is_clipped = self.is_clipped
|
||||
|
||||
def __add__(self, other):
|
||||
"Redefining self + other (scalar)"
|
||||
if isinstance(other, int) or isinstance(other, float):
|
||||
@@ -498,6 +501,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
||||
tmp_y.append(numpy.array(self.y[i], dtype="float32") + other)
|
||||
|
||||
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
|
||||
self._copy_metadata(r)
|
||||
self.lock.release()
|
||||
return r
|
||||
else:
|
||||
@@ -520,6 +524,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
||||
tmp_y.append(numpy.array(self.y[i], dtype="float32") - other)
|
||||
|
||||
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
|
||||
self._copy_metadata(r)
|
||||
self.lock.release()
|
||||
return r
|
||||
else:
|
||||
@@ -537,6 +542,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
||||
tmp_y.append(other - numpy.array(self.y[i], dtype="float32"))
|
||||
|
||||
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
|
||||
self._copy_metadata(r)
|
||||
self.lock.release()
|
||||
return r
|
||||
else:
|
||||
@@ -554,6 +560,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
||||
tmp_y.append(numpy.array(self.y[i], dtype="float32") * other)
|
||||
|
||||
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
|
||||
self._copy_metadata(r)
|
||||
self.lock.release()
|
||||
return r
|
||||
else:
|
||||
@@ -575,6 +582,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
||||
tmp_y.append(numpy.array(self.y[i], dtype="float32") ** other)
|
||||
|
||||
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
|
||||
self._copy_metadata(r)
|
||||
self.lock.release()
|
||||
return r
|
||||
else:
|
||||
@@ -589,6 +597,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
||||
tmp_y.append(numpy.array(self.y[i], dtype="float32") / other)
|
||||
|
||||
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
|
||||
self._copy_metadata(r)
|
||||
self.lock.release()
|
||||
return r
|
||||
else:
|
||||
@@ -602,6 +611,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
||||
for i in range(self.get_number_of_channels()):
|
||||
tmp_y.append(other / numpy.array(self.y[i], dtype="float32"))
|
||||
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
|
||||
self._copy_metadata(r)
|
||||
self.lock.release()
|
||||
return r
|
||||
else:
|
||||
@@ -618,6 +628,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
||||
for i in range(self.get_number_of_channels()):
|
||||
tmp_y.append(numpy.array(self.y[i], dtype="float32") // other)
|
||||
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
|
||||
self._copy_metadata(r)
|
||||
self.lock.release()
|
||||
return r
|
||||
else:
|
||||
@@ -634,6 +645,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
||||
for i in range(self.get_number_of_channels()):
|
||||
tmp_y.append(other // numpy.array(self.y[i], dtype="float32"))
|
||||
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
|
||||
self._copy_metadata(r)
|
||||
self.lock.release()
|
||||
return r
|
||||
else:
|
||||
@@ -649,6 +661,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
||||
tmp_y.append(numpy.array(-self.y[i]))
|
||||
|
||||
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
|
||||
self._copy_metadata(r)
|
||||
self.lock.release()
|
||||
return r
|
||||
|
||||
@@ -703,6 +716,7 @@ def read_from_hdf(hdf_node):
|
||||
adc.cont_data=True
|
||||
# now do the real data
|
||||
adc_data=hdf_node.adc_data.read()
|
||||
adc.set_nChannels(adc_data.shape[1])
|
||||
|
||||
adc.x=numpy.arange(adc_data.shape[0], dtype="float32")/adc.sampling_rate
|
||||
|
||||
|
||||
@@ -102,6 +102,14 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
||||
else:
|
||||
raise ValueError("Wrong usage of __init__!")
|
||||
|
||||
def _copy_provenance(self, r):
|
||||
r.is_clipped = self.is_clipped
|
||||
r.job_id = self.job_id
|
||||
r.job_ids = self.job_ids.copy()
|
||||
r.time_period = self.time_period[:] if self.time_period is not None else None
|
||||
if self.common_descriptions is not None:
|
||||
r.common_descriptions = self.common_descriptions.copy()
|
||||
|
||||
def get_accu_by_index(self, index: int):
|
||||
self.lock.acquire()
|
||||
try:
|
||||
@@ -637,12 +645,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
||||
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = self.use_error)
|
||||
else:
|
||||
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = self.use_error)
|
||||
r.is_clipped = self.is_clipped
|
||||
r.job_id = self.job_id
|
||||
r.job_ids = self.job_ids.copy()
|
||||
r.time_period = self.time_period[:] if self.time_period is not None else None
|
||||
if self.common_descriptions is not None:
|
||||
r.common_descriptions = self.common_descriptions.copy()
|
||||
self._copy_provenance(r)
|
||||
self.lock.release()
|
||||
return r
|
||||
|
||||
@@ -678,8 +681,6 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
||||
|
||||
# Other and self not empty (self + other ADC_Result)
|
||||
else:
|
||||
self.lock.acquire()
|
||||
|
||||
if self.sampling_rate != other.get_sampling_rate():
|
||||
raise ValueError("Accumulation: You cant add ADC_Results with different sampling-rates")
|
||||
if len(self.y[0]) != len(other):
|
||||
@@ -690,6 +691,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
||||
if self.index[i] != other.get_index_bounds(i):
|
||||
raise ValueError("Accumulation: You cant add ADC_Results with different indexing")
|
||||
|
||||
self.lock.acquire()
|
||||
|
||||
tmp_y = []
|
||||
tmp_ysquare = []
|
||||
|
||||
@@ -706,7 +709,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
||||
r.time_period=[min(self.time_period[0],other.job_date),
|
||||
max(self.time_period[1],other.job_date)]
|
||||
r.job_id = other.job_id
|
||||
r.job_ids = self.job_ids
|
||||
r.job_ids = self.job_ids.copy()
|
||||
r.job_ids[other.job_id] = other.get_job_date()
|
||||
|
||||
if self.common_descriptions is not None:
|
||||
@@ -725,33 +728,29 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
||||
|
||||
# Self empty (copy)
|
||||
if not self.contains_data():
|
||||
if self.uses_statistics() and not other.uses_statistics():
|
||||
raise ValueError("Accumulation: You cant add non-error accumulations to accumulations with error")
|
||||
|
||||
self.lock.acquire()
|
||||
|
||||
tmp_y = []
|
||||
tmp_ysquare = []
|
||||
|
||||
self.lock.acquire()
|
||||
for i in range(other.get_number_of_channels()):
|
||||
tmp_y.append(numpy.array(other.y[i], dtype="float32"))
|
||||
if other.uses_statistics():
|
||||
tmp_ysquare.append(numpy.array(other.y_square[i], dtype="float32"))
|
||||
|
||||
if self.uses_statistics():
|
||||
r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = other.n, index = other.index, sampl_freq = other.sampling_rate, error = True)
|
||||
else:
|
||||
r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, n = other.n, index = other.index, sampl_freq = other.sampling_rate, error = False)
|
||||
for i in range(other.get_number_of_channels()):
|
||||
tmp_y.append(other.y[i])
|
||||
tmp_ysquare.append(other.y_square[i])
|
||||
r.time_period=other.time_period[:]
|
||||
r.job_id = other.job_id
|
||||
r.job_ids = other.job_ids
|
||||
if other.common_descriptions is not None:
|
||||
r.common_descriptions=other.common_descriptions.copy()
|
||||
else:
|
||||
r.common_descriptions=None
|
||||
other._copy_provenance(r)
|
||||
self.lock.release()
|
||||
return r
|
||||
|
||||
# Other and self not empty (self + other)
|
||||
else:
|
||||
self.lock.acquire()
|
||||
|
||||
if self.sampling_rate != other.get_sampling_rate():
|
||||
raise ValueError("Accumulation: You cant add accumulations with different sampling-rates")
|
||||
if len(self.y[0]) != len(other):
|
||||
@@ -764,6 +763,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
||||
if self.uses_statistics() and not other.uses_statistics():
|
||||
raise ValueError("Accumulation: You cant add non-error accumulations to accumulations with error")
|
||||
|
||||
self.lock.acquire()
|
||||
|
||||
tmp_y = []
|
||||
tmp_ysquare = []
|
||||
|
||||
@@ -780,6 +781,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
||||
r.time_period=[min(self.time_period[0],other.time_period[0]),
|
||||
max(self.time_period[1],other.time_period[1])]
|
||||
r.job_id = other.job_id
|
||||
r.job_ids = self.job_ids.copy()
|
||||
for i in other.job_ids:
|
||||
r.job_ids[i] = other.job_ids[i]
|
||||
r.common_descriptions={}
|
||||
@@ -793,6 +795,12 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
||||
return r
|
||||
|
||||
|
||||
elif other is None:
|
||||
# Convenience: ignore add of None
|
||||
return self + 0
|
||||
else:
|
||||
raise ValueError("can not add "+repr(type(other))+" to Accumulation")
|
||||
|
||||
def __radd__(self, other):
|
||||
"""Redefining other + self"""
|
||||
return self.__add__(other)
|
||||
@@ -992,8 +1000,6 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
||||
|
||||
# Other and self not empty (self + other/ADC_Result)
|
||||
else:
|
||||
self.lock.acquire()
|
||||
|
||||
if self.sampling_rate != other.get_sampling_rate():
|
||||
raise ValueError("Accumulation: You can't add ADC-Results with different sampling-rates")
|
||||
if len(self.y[0]) != len(other):
|
||||
@@ -1004,6 +1010,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
||||
if self.index[i] != other.get_index_bounds(i):
|
||||
raise ValueError("Accumulation: You can't add ADC-Results with different indexing")
|
||||
|
||||
self.lock.acquire()
|
||||
|
||||
for i in range(self.get_number_of_channels()):
|
||||
self.y[i] += other.y[i]
|
||||
if self.uses_statistics():
|
||||
@@ -1062,7 +1070,6 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
||||
|
||||
# Other and self not empty (self + other)
|
||||
else:
|
||||
self.lock.acquire()
|
||||
if self.sampling_rate != other.get_sampling_rate():
|
||||
raise ValueError("Accumulation: You cant add accumulations with different sampling-rates")
|
||||
if len(self.y[0]) != len(other):
|
||||
@@ -1074,6 +1081,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
||||
raise ValueError("Accumulation: You cant add accumulations with different indexing")
|
||||
if self.uses_statistics() and not other.uses_statistics():
|
||||
raise ValueError("Accumulation: You cant add non-error accumulations to accumulations with error")
|
||||
|
||||
self.lock.acquire()
|
||||
|
||||
for i in range(self.get_number_of_channels()):
|
||||
self.y[i] += other.y[i]
|
||||
@@ -1123,7 +1132,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
||||
"""Redefining -self"""
|
||||
|
||||
if not self.contains_data():
|
||||
return
|
||||
raise ValueError("Accumulation: You cant negate an empty accumulation")
|
||||
|
||||
tmp_y = []
|
||||
|
||||
@@ -1135,6 +1144,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
||||
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = numpy.array(self.y_square), n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = True)
|
||||
else:
|
||||
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = False)
|
||||
self._copy_provenance(r)
|
||||
self.lock.release()
|
||||
return r
|
||||
|
||||
|
||||
@@ -17,8 +17,12 @@ class Config_Result(Resultable):
|
||||
|
||||
if config is None:
|
||||
self.config = { }
|
||||
else:
|
||||
self.config = config
|
||||
if desc is None:
|
||||
self.description = { }
|
||||
else:
|
||||
self.description = desc
|
||||
self.job_id = job_id
|
||||
self.job_date = job_date
|
||||
|
||||
|
||||
@@ -19,10 +19,6 @@ class DamarisFFT:
|
||||
:param float stop: end of clipping in s
|
||||
|
||||
"""
|
||||
# check if start/stop order is properly
|
||||
if start > stop:
|
||||
start, stop = stop, start
|
||||
|
||||
# do nothing if one uses clip as a "placeholder"
|
||||
if start is None and stop is None:
|
||||
return self
|
||||
@@ -32,6 +28,10 @@ class DamarisFFT:
|
||||
if stop is None:
|
||||
stop = self.x[ -1 ]
|
||||
|
||||
# check if start/stop order is properly
|
||||
if start > stop:
|
||||
start, stop = stop, start
|
||||
|
||||
# check if data is fft which changes the start/stop units
|
||||
if self.xlabel == "Frequency / Hz":
|
||||
start = self.x.size * (0.5 + start / self.sampling_rate)
|
||||
|
||||
@@ -155,7 +155,10 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
|
||||
|
||||
def __add__(self, right_value):
|
||||
if right_value==0:
|
||||
return self.copy()
|
||||
new_one=MeasurementResult(self.quantity_name)
|
||||
for key in self.data:
|
||||
new_one[key]=self.data[key].copy()
|
||||
return new_one
|
||||
else:
|
||||
raise Exception("not implemented")
|
||||
|
||||
|
||||
@@ -21,16 +21,16 @@ class Signalpath:
|
||||
if abs(reduced_angle[1]) < 1e-6:
|
||||
reduced_angle = reduced_angle[0] % 4
|
||||
if reduced_angle == 0:
|
||||
rotated.y = list(self.y)
|
||||
rotated.y = [a.copy() for a in self.y]
|
||||
|
||||
elif reduced_angle == 1:
|
||||
rotated.y = [-self.y[1], self.y[0]]
|
||||
rotated.y = [-self.y[1], self.y[0].copy()]
|
||||
|
||||
elif reduced_angle == 2:
|
||||
rotated.y = [-self.y[0], -self.y[1]]
|
||||
|
||||
elif reduced_angle == 3:
|
||||
rotated.y = [self.y[1], -self.y[0]]
|
||||
rotated.y = [self.y[1].copy(), -self.y[0]]
|
||||
else:
|
||||
rad_angle = N.deg2rad(degrees)
|
||||
sin_angle = N.sin(rad_angle)
|
||||
|
||||
@@ -120,7 +120,7 @@ class BackendDriver(threading.Thread):
|
||||
self.statefilename=None
|
||||
state_files=glob.glob(os.path.join(self.spool_dir,"*.state"))
|
||||
while len(state_files)==0:
|
||||
if timeout<0 or self.core_input is None or self.core_input.poll() is not None or self.quit_flag.isSet():
|
||||
if timeout<0 or self.core_input is None or self.core_input.poll() is not None or self.quit_flag.is_set():
|
||||
# look into core log file and include contents
|
||||
logger.error("Backend failed to start: timeout=%s poll=%s", timeout, self.core_input.poll())
|
||||
log_message=''
|
||||
@@ -156,10 +156,10 @@ class BackendDriver(threading.Thread):
|
||||
break
|
||||
|
||||
# wait on flag and look after backend
|
||||
while not self.quit_flag.isSet() and self.is_busy():
|
||||
while not self.quit_flag.is_set() and self.is_busy():
|
||||
self.quit_flag.wait(0.1)
|
||||
|
||||
if self.quit_flag.isSet():
|
||||
if self.quit_flag.is_set():
|
||||
self.stop_queue()
|
||||
while self.is_busy():
|
||||
time.sleep(0.1)
|
||||
|
||||
@@ -52,6 +52,31 @@ from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCan
|
||||
|
||||
max_points_to_display = 0
|
||||
|
||||
|
||||
def apply_font_css(widget, font_string):
|
||||
"""
|
||||
Non-deprecated replacement for the deprecated widget.modify_font():
|
||||
converts a pango font description string (family, optional size in pt,
|
||||
optional weight) into a CSS rule applied to the widget's style context.
|
||||
|
||||
Examples:
|
||||
>>> apply_font_css(textview, "Monospace 10")
|
||||
>>> apply_font_css(label, "sans bold 16")
|
||||
"""
|
||||
fd = pango.FontDescription(font_string)
|
||||
decl = "font-family: %s;" % fd.get_family()
|
||||
if fd.get_size() > 0 and not fd.get_size_is_absolute():
|
||||
decl += " font-size: %.6gpt;" % (fd.get_size() / float(pango.SCALE))
|
||||
if fd.get_weight() != pango.Weight.NORMAL:
|
||||
decl += " font-weight: %d;" % fd.get_weight()
|
||||
provider = gtk.CssProvider()
|
||||
provider.load_from_data(("%s { %s }" % (widget.get_css_name(), decl)).encode("utf-8"))
|
||||
context = widget.get_style_context()
|
||||
context.add_provider(provider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
||||
# keep a reference so the provider is not garbage collected
|
||||
setattr(widget, "_damaris_font_css_provider", provider)
|
||||
|
||||
|
||||
import matplotlib.axes
|
||||
import matplotlib.figure
|
||||
|
||||
@@ -267,12 +292,10 @@ class DamarisGUI:
|
||||
def run( self ):
|
||||
# prolong lifetime of clipboard till the very end (avoid error message)
|
||||
self.main_clipboard = self.sw.main_clipboard
|
||||
gdk.threads_enter( )
|
||||
|
||||
if self.start_immediately:
|
||||
self.toolbar_run_button.emit("clicked")
|
||||
gtk.main( )
|
||||
gdk.threads_leave( )
|
||||
|
||||
self.si = None
|
||||
self.sw = None
|
||||
@@ -822,14 +845,12 @@ class DamarisGUI:
|
||||
dump_size = os.stat( self.dump_filename ).st_size / 1e6
|
||||
logger.info("Done (%.1f s, %.1f MB)", time.time( ) - self.dump_start_time, dump_size)
|
||||
|
||||
gdk.threads_enter( )
|
||||
if e_text:
|
||||
self.experiment_script_statusbar_label.set_text( e_text )
|
||||
if r_text:
|
||||
self.data_handling_statusbar_label.set_text( r_text )
|
||||
if b_text:
|
||||
self.backend_statusbar_label.set_text( b_text )
|
||||
gdk.threads_leave( )
|
||||
|
||||
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:
|
||||
@@ -838,9 +859,7 @@ class DamarisGUI:
|
||||
if self.save_thread is None and self.dump_filename != "":
|
||||
logger.info("All subprocesses ended, saving data pool")
|
||||
# Disable run button during data saving to prevent UI confusion
|
||||
gdk.threads_enter()
|
||||
self.toolbar_run_button.set_sensitive(False)
|
||||
gdk.threads_leave()
|
||||
|
||||
# thread to save data...
|
||||
self.save_thread = threading.Thread( target=self.dump_states, name="dump states" )
|
||||
@@ -850,10 +869,8 @@ class DamarisGUI:
|
||||
self.state = DamarisGUI.Stop_State
|
||||
|
||||
if self.state == DamarisGUI.Stop_State:
|
||||
gdk.threads_enter( )
|
||||
self.toolbar_pause_button.set_sensitive( False )
|
||||
self.toolbar_stop_button.set_sensitive( False )
|
||||
gdk.threads_leave( )
|
||||
if len( still_running ) != 0:
|
||||
logger.warning("Subprocess(es) still running: %s", ', '.join( [s.getName( ) for s in still_running] ))
|
||||
return True
|
||||
@@ -878,12 +895,10 @@ class DamarisGUI:
|
||||
|
||||
# now everything is stopped
|
||||
self.state = DamarisGUI.Edit_State
|
||||
gdk.threads_enter( )
|
||||
self.sw.enable_editing( )
|
||||
self.toolbar_run_button.set_sensitive( True )
|
||||
self.toolbar_stop_button.set_sensitive( False )
|
||||
self.toolbar_pause_button.set_sensitive( False )
|
||||
gdk.threads_leave( )
|
||||
return False
|
||||
|
||||
# dump states?
|
||||
@@ -1334,8 +1349,7 @@ class LogWindow:
|
||||
self.damaris_gui = damaris_gui
|
||||
self.textview = self.xml_gui.get_object( "messages_textview" )
|
||||
self.textview.connect( "key-press-event", self.textview_keypress)
|
||||
font_desc = pango.FontDescription("Monospace 10")
|
||||
self.textview.modify_font(font_desc)
|
||||
apply_font_css(self.textview, "Monospace 10")
|
||||
self.textbuffer = self.textview.get_buffer( )
|
||||
self.last_timetag = None
|
||||
self( "Started in directory %s\n" % os.getcwd( ) )
|
||||
@@ -1351,11 +1365,9 @@ class LogWindow:
|
||||
timetag_int = math.floor( timetag )
|
||||
timetag_ms = int( (timetag - timetag_int) * 1000 )
|
||||
date_tag = time.strftime( "%Y%m%d %H:%M:%S.%%03d:\n", time.localtime( timetag_int ) ) % timetag_ms
|
||||
gdk.threads_enter( )
|
||||
self.textbuffer.place_cursor( self.textbuffer.get_end_iter( ) )
|
||||
self.textbuffer.insert_at_cursor( date_tag + str( message ) )
|
||||
self.textview.scroll_to_mark( self.textbuffer.get_insert( ), 0.1 , False, 0.5, 0.5)
|
||||
gdk.threads_leave( )
|
||||
|
||||
def textview_keypress( self, widget, event, data=None ):
|
||||
if event.state & gdk.ModifierType.CONTROL_MASK != 0:
|
||||
@@ -1449,22 +1461,20 @@ class ScriptWidgets:
|
||||
self.data_handling_textbuffer.set_language(langpython)
|
||||
self.data_handling_textbuffer.set_highlight_syntax(True)
|
||||
|
||||
fontdesc = pango.FontDescription("monospace")
|
||||
|
||||
self.experiment_script_textview.set_buffer( self.experiment_script_textbuffer )
|
||||
self.experiment_script_textview.set_show_line_numbers(True)
|
||||
self.experiment_script_textview.set_auto_indent(True)
|
||||
self.experiment_script_textview.set_insert_spaces_instead_of_tabs(True)
|
||||
self.experiment_script_textview.set_tab_width(4)
|
||||
self.experiment_script_textview.set_smart_home_end(True)
|
||||
self.experiment_script_textview.modify_font(fontdesc)
|
||||
apply_font_css(self.experiment_script_textview, "monospace")
|
||||
self.data_handling_textview.set_buffer( self.data_handling_textbuffer )
|
||||
self.data_handling_textview.set_show_line_numbers(True)
|
||||
self.data_handling_textview.set_auto_indent(True)
|
||||
self.data_handling_textview.set_insert_spaces_instead_of_tabs(True)
|
||||
self.data_handling_textview.set_tab_width(4)
|
||||
self.data_handling_textview.set_smart_home_end(True)
|
||||
self.data_handling_textview.modify_font(fontdesc)
|
||||
apply_font_css(self.data_handling_textview, "monospace")
|
||||
|
||||
keywords = """for if else elif in print try finally except global lambda not or pass def
|
||||
class import from as return yield while continue break assert None True False AccumulatedValue
|
||||
@@ -1918,7 +1928,6 @@ get_ylabel set_ylabel"""
|
||||
parent_window = self.xml_gui.get_object( "main_window" )
|
||||
dialog = gtk.FileChooserDialog( title=open_dialog_title,
|
||||
parent=parent_window,
|
||||
action=gtk.FileChooserAction.OPEN,
|
||||
buttons=("_Open", gtk.ResponseType.OK, "_Cancel", gtk.ResponseType.CANCEL)
|
||||
)
|
||||
dialog.set_default_response( gtk.ResponseType.OK )
|
||||
@@ -1944,7 +1953,6 @@ get_ylabel set_ylabel"""
|
||||
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 )
|
||||
@@ -2106,8 +2114,8 @@ get_ylabel set_ylabel"""
|
||||
parent_window = self.xml_gui.get_object( "main_window" )
|
||||
dialog = gtk.FileChooserDialog( title=dialog_title,
|
||||
parent=parent_window,
|
||||
action=gtk.FileChooserAction.SAVE,
|
||||
buttons=("_Save", gtk.ResponseType.OK, "_Cancel", gtk.ResponseType.CANCEL) )
|
||||
dialog.set_property("action", gtk.FileChooserAction.SAVE)
|
||||
|
||||
dialog.set_default_response( gtk.ResponseType.OK )
|
||||
dialog.set_select_multiple( False )
|
||||
@@ -2671,11 +2679,9 @@ pygobject version %(pygobject)s
|
||||
title="Select Temperature Controller Device",
|
||||
parent=None,
|
||||
action=gtk.FileChooserAction.OPEN,
|
||||
buttons=(
|
||||
gtk.STOCK_CANCEL, gtk.ResponseType.CANCEL,
|
||||
gtk.STOCK_OPEN, gtk.ResponseType.OK
|
||||
)
|
||||
)
|
||||
dialog.add_button("Cancel", gtk.ResponseType.CANCEL)
|
||||
dialog.add_button("Open", gtk.ResponseType.OK)
|
||||
|
||||
# Set default folder to /dev/serial/by-id/ if it exists
|
||||
default_folder = "/dev/serial/by-id/"
|
||||
@@ -2767,10 +2773,10 @@ pygobject version %(pygobject)s
|
||||
font = self.config_script_font_button.get_font_name( )
|
||||
experiment_script_textview = self.xml_gui.get_object( "experiment_script_textview" )
|
||||
if experiment_script_textview:
|
||||
experiment_script_textview.modify_font( pango.FontDescription( font ) )
|
||||
apply_font_css(experiment_script_textview, font)
|
||||
data_handling_textview = self.xml_gui.get_object( "data_handling_textview" )
|
||||
if data_handling_textview:
|
||||
data_handling_textview.modify_font( pango.FontDescription( font ) )
|
||||
apply_font_css(data_handling_textview, font)
|
||||
|
||||
def browse_backend_executable_dialog( self, widget ):
|
||||
"""
|
||||
@@ -3031,7 +3037,7 @@ class MonitorWidgets:
|
||||
try:
|
||||
# The coordinate label is stored in the toolbar's message attribute
|
||||
if hasattr(self.matplot_toolbar, 'message'):
|
||||
self.matplot_toolbar.message.modify_font(pango.FontDescription("sans bold 16"))
|
||||
apply_font_css(self.matplot_toolbar.message, "sans bold 16")
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -3214,50 +3220,46 @@ class MonitorWidgets:
|
||||
register a listener and save reference to data
|
||||
assume to be in gtk/gdk lock
|
||||
"""
|
||||
gdk.threads_enter( )
|
||||
try:
|
||||
if self.data_pool is not None:
|
||||
# maybe some extra cleanup needed
|
||||
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.__rescale = True
|
||||
if self.data_pool is not None:
|
||||
# maybe some extra cleanup needed
|
||||
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.display_source_path_label.set_label( "" )
|
||||
self.clear_display( )
|
||||
|
||||
self.data_pool.unregister_listener( self.datapool_listener )
|
||||
self.data_pool = None
|
||||
|
||||
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
|
||||
keys = sorted([k for k in data_pool if k != "__builtins__"])
|
||||
for key in keys:
|
||||
self.source_list_add(key)
|
||||
|
||||
# Select first data item if available
|
||||
keys = [k for k in data_pool 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( )
|
||||
self.update_counter_lock.acquire( )
|
||||
self.update_counter = 0
|
||||
self.update_counter_lock.release( )
|
||||
|
||||
# display states
|
||||
self.source_list_reset( )
|
||||
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 )
|
||||
|
||||
# Populate source list with existing data since the listener
|
||||
# was registered after the data was loaded
|
||||
keys = sorted([k for k in data_pool if k != "__builtins__"])
|
||||
for key in keys:
|
||||
self.source_list_add(key)
|
||||
|
||||
# Select first data item if available
|
||||
keys = [k for k in data_pool 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)
|
||||
|
||||
#################### observing data structures and produce idle events
|
||||
|
||||
@@ -3348,11 +3350,7 @@ class MonitorWidgets:
|
||||
if self.update_counter > 10:
|
||||
logger.warning("Update queue too long (%d>10): skipping one update", self.update_counter)
|
||||
else:
|
||||
gdk.threads_enter( )
|
||||
try:
|
||||
self.update_display( )
|
||||
finally:
|
||||
gdk.threads_leave( )
|
||||
self.update_display( )
|
||||
else:
|
||||
# unregister old one
|
||||
if self.displayed_data[ 1 ] is not None and hasattr( self.displayed_data[ 1 ],
|
||||
@@ -3366,20 +3364,13 @@ class MonitorWidgets:
|
||||
if self.update_counter > 10:
|
||||
logger.warning("Update queue too long (%d>10): skipping one update", self.update_counter)
|
||||
else:
|
||||
gdk.threads_enter( )
|
||||
try:
|
||||
self.renew_display( )
|
||||
finally:
|
||||
gdk.threads_leave( )
|
||||
self.renew_display( )
|
||||
new_data_struct = None
|
||||
elif event.what == DataPool.Event.new_key:
|
||||
# update combo-box by inserting and rely on consistent information
|
||||
gdk.threads_enter( )
|
||||
self.source_list_add( event.subject )
|
||||
gdk.threads_leave( )
|
||||
elif event.what == DataPool.Event.deleted_key:
|
||||
# update combo-box by removing and rely on consistent information
|
||||
gdk.threads_enter( )
|
||||
if (self.displayed_data[ 0 ] is not None and
|
||||
self.displayed_data[ 0 ] == event.subject):
|
||||
self.displayed_data = [ None, None ]
|
||||
@@ -3389,13 +3380,10 @@ class MonitorWidgets:
|
||||
# not necessary, because event will be submitted
|
||||
#self.clear_display()
|
||||
self.source_list_remove( event.subject )
|
||||
gdk.threads_leave( )
|
||||
elif event.what == DataPool.Event.destroy:
|
||||
gdk.threads_enter( )
|
||||
self.source_list_reset( )
|
||||
self.displayed_data = [ None, None ]
|
||||
self.clear_display( )
|
||||
gdk.threads_leave( )
|
||||
return
|
||||
|
||||
def update_display_idle_event( self, subject=None ):
|
||||
@@ -3409,11 +3397,7 @@ class MonitorWidgets:
|
||||
return
|
||||
if self.displayed_data[ 0 ] is None or subject != self.displayed_data[ 0 ]:
|
||||
return
|
||||
gdk.threads_enter( )
|
||||
try:
|
||||
self.update_display( )
|
||||
finally:
|
||||
gdk.threads_leave( )
|
||||
self.update_display( )
|
||||
|
||||
|
||||
######################## events from buttons
|
||||
@@ -4159,7 +4143,7 @@ class ScriptInterface:
|
||||
# start them
|
||||
if self.back_driver is not None:
|
||||
self.back_driver.start( )
|
||||
while (not self.back_driver.quit_flag.isSet( ) and \
|
||||
while (not self.back_driver.quit_flag.is_set( ) and \
|
||||
(self.back_driver.core_pid is None or self.back_driver.core_pid <= 0)):
|
||||
while gtk.events_pending():
|
||||
gtk.main_iteration_do(False)
|
||||
|
||||
@@ -5,6 +5,7 @@ import sys
|
||||
import time
|
||||
import logging
|
||||
import ast
|
||||
import contextlib
|
||||
from damaris.experiments.Experiment import Quit
|
||||
from damaris.experiments import Experiment
|
||||
|
||||
@@ -36,6 +37,19 @@ class _SandboxStdout:
|
||||
return False
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _sandbox_stdout_redirect(log_callback):
|
||||
"""Temporarily redirect process-global stdout/stderr to the sandbox log."""
|
||||
old_stdout, old_stderr = sys.stdout, sys.stderr
|
||||
sandbox = _SandboxStdout(log_callback)
|
||||
try:
|
||||
sys.stdout = sandbox
|
||||
sys.stderr = sandbox
|
||||
yield sandbox
|
||||
finally:
|
||||
sys.stdout, sys.stderr = old_stdout, old_stderr
|
||||
|
||||
|
||||
class ExperimentHandling(threading.Thread):
|
||||
"""
|
||||
runs the experiment script in sandbox
|
||||
@@ -52,17 +66,25 @@ class ExperimentHandling(threading.Thread):
|
||||
self.data["__recentexperiment"]=-1
|
||||
|
||||
def synchronize(self, before=0, waitsteps=0.1):
|
||||
while (self.data["__recentexperiment"]>self.data["__recentresult"]+before) and not self.quit_flag.isSet():
|
||||
while (self.data["__recentexperiment"]>self.data["__recentresult"]+before) and not self.quit_flag.is_set():
|
||||
self.quit_flag.wait(waitsteps)
|
||||
if self.quit_flag.isSet():
|
||||
if self.quit_flag.is_set():
|
||||
raise StopExperiment
|
||||
|
||||
def sleep(self, seconds):
|
||||
self.quit_flag.wait(seconds)
|
||||
if self.quit_flag.isSet():
|
||||
if self.quit_flag.is_set():
|
||||
raise StopExperiment
|
||||
|
||||
def run(self):
|
||||
# Redirect stdout/stderr so print() in sandboxed scripts goes to the GUI
|
||||
if self._log_callback is not None:
|
||||
with _sandbox_stdout_redirect(self._log_callback):
|
||||
self._run_script()
|
||||
else:
|
||||
self._run_script()
|
||||
|
||||
def _run_script(self):
|
||||
dataspace={}
|
||||
exp_classes = __import__('damaris.experiments', dataspace, dataspace, ['Experiment'])
|
||||
for name in dir(exp_classes):
|
||||
@@ -77,13 +99,6 @@ class ExperimentHandling(threading.Thread):
|
||||
self.location = None
|
||||
exp_iterator=None
|
||||
|
||||
# Redirect stdout/stderr so print() in sandboxed scripts goes to the GUI
|
||||
_orig_stdout = sys.stdout
|
||||
_orig_stderr = sys.stderr
|
||||
if self._log_callback is not None:
|
||||
sys.stdout = _SandboxStdout(self._log_callback)
|
||||
sys.stderr = _SandboxStdout(self._log_callback)
|
||||
|
||||
# check for time.sleep()
|
||||
try:
|
||||
tree = ast.parse(self.script)
|
||||
@@ -131,7 +146,7 @@ class ExperimentHandling(threading.Thread):
|
||||
traceback_file=None
|
||||
return
|
||||
|
||||
while exp_iterator is not None and not self.quit_flag.isSet():
|
||||
while exp_iterator is not None and not self.quit_flag.is_set():
|
||||
# get next experiment from script
|
||||
try:
|
||||
job=next(exp_iterator)
|
||||
@@ -171,7 +186,7 @@ class ExperimentHandling(threading.Thread):
|
||||
# relax for a short time
|
||||
if "__resultsinadvance" in self.data and self.data["__resultsinadvance"]+100<job.job_id:
|
||||
self.quit_flag.wait(0.05)
|
||||
if self.quit_flag.isSet():
|
||||
if self.quit_flag.is_set():
|
||||
dataspace=None
|
||||
exp_iterator=None
|
||||
break
|
||||
@@ -181,7 +196,3 @@ class ExperimentHandling(threading.Thread):
|
||||
dataspace=None
|
||||
self.exp_iterator=None
|
||||
self.writer=None
|
||||
|
||||
# Restore stdout/stderr
|
||||
sys.stdout = _orig_stdout
|
||||
sys.stderr = _orig_stderr
|
||||
@@ -7,7 +7,7 @@ import traceback
|
||||
import logging
|
||||
import ast
|
||||
from damaris.data import Resultable
|
||||
from damaris.gui.ExperimentHandling import StopExperiment, _SandboxStdout
|
||||
from damaris.gui.ExperimentHandling import StopExperiment, _sandbox_stdout_redirect
|
||||
|
||||
logger = logging.getLogger("damaris.result_handling")
|
||||
|
||||
@@ -28,12 +28,13 @@ class ResultHandling(threading.Thread):
|
||||
|
||||
def run(self):
|
||||
# Redirect stdout/stderr so print() in sandboxed scripts goes to the GUI
|
||||
_orig_stdout = sys.stdout
|
||||
_orig_stderr = sys.stderr
|
||||
if self._log_callback is not None:
|
||||
sys.stdout = _SandboxStdout(self._log_callback)
|
||||
sys.stderr = _SandboxStdout(self._log_callback)
|
||||
with _sandbox_stdout_redirect(self._log_callback):
|
||||
self._run_script()
|
||||
else:
|
||||
self._run_script()
|
||||
|
||||
def _run_script(self):
|
||||
# execute it
|
||||
dataspace={}
|
||||
data_classes = __import__('damaris.data', dataspace, dataspace, ['*'])
|
||||
@@ -96,30 +97,26 @@ class ResultHandling(threading.Thread):
|
||||
traceback_file=None
|
||||
dataspace=None
|
||||
|
||||
# Restore stdout/stderr
|
||||
sys.stdout = _orig_stdout
|
||||
sys.stderr = _orig_stderr
|
||||
|
||||
def sleep(self, seconds):
|
||||
self.quit_flag.wait(seconds)
|
||||
if self.quit_flag.isSet():
|
||||
if self.quit_flag.is_set():
|
||||
raise StopExperiment
|
||||
|
||||
def __iter__(self):
|
||||
if self.quit_flag.isSet():
|
||||
if self.quit_flag.is_set():
|
||||
self.results=None
|
||||
return
|
||||
for i in self.results:
|
||||
if hasattr(self.results, "in_advance"):
|
||||
self.data_space["__resultsinadvance"]=self.results.in_advance
|
||||
if self.quit_flag.isSet():
|
||||
if self.quit_flag.is_set():
|
||||
self.results=None
|
||||
return
|
||||
if isinstance(i, Resultable.Resultable):
|
||||
if self.data_space is not None:
|
||||
self.data_space["__recentresult"]=i.job_id+0
|
||||
yield i
|
||||
if self.quit_flag.isSet():
|
||||
if self.quit_flag.is_set():
|
||||
self.results=None
|
||||
return
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import base64
|
||||
import numpy
|
||||
|
||||
try:
|
||||
import xml.etree.cElementTree
|
||||
import xml.etree.ElementTree
|
||||
ELEMENT_TREE = True
|
||||
except:
|
||||
import xml.parsers.expat
|
||||
@@ -109,7 +109,7 @@ class ResultReader:
|
||||
self.result_job_number = None
|
||||
# Job Date is set in __read_file()
|
||||
self.__filetype = None
|
||||
for elem in xml.etree.cElementTree.ElementTree(file=in_file).iter():
|
||||
for elem in xml.etree.ElementTree.ElementTree(file=in_file).iter():
|
||||
if elem.tag == 'result':
|
||||
self.result_job_number = int(elem.get("job"))
|
||||
pass
|
||||
@@ -542,7 +542,7 @@ class BlockingResultReader(ResultReader):
|
||||
block until result is available
|
||||
"""
|
||||
expected_filename=os.path.join(self.spool_dir,self.result_pattern%(self.no))
|
||||
while (not self.quit_flag.isSet()) and (self.stop_no is None or self.stop_no>self.no):
|
||||
while (not self.quit_flag.is_set()) and (self.stop_no is None or self.stop_no>self.no):
|
||||
if not os.access(expected_filename,os.R_OK):
|
||||
# stop polling, if required
|
||||
if self.poll_time<0: break
|
||||
@@ -558,11 +558,11 @@ class BlockingResultReader(ResultReader):
|
||||
self.in_advance+=1
|
||||
in_advance_filename=os.path.join(self.spool_dir,self.result_pattern%(self.in_advance+1))
|
||||
|
||||
if self.quit_flag.isSet(): break
|
||||
if self.quit_flag.is_set(): break
|
||||
r=self.get_result_object(expected_filename)
|
||||
if self.quit_flag.isSet(): break
|
||||
if self.quit_flag.is_set(): break
|
||||
|
||||
if self.quit_flag.isSet(): break
|
||||
if self.quit_flag.is_set(): break
|
||||
yield r
|
||||
|
||||
if self.clear_results:
|
||||
|
||||
@@ -13,25 +13,6 @@ 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):
|
||||
"""
|
||||
@@ -72,6 +53,10 @@ class TemperatureMonitor(threading.Thread):
|
||||
self.result = TemperatureResult()
|
||||
self._running = False
|
||||
self._lock = threading.RLock()
|
||||
self._consecutive_failures = 0
|
||||
self._max_consecutive_failures = 10
|
||||
self._last_error = None
|
||||
self._connection_ok = True
|
||||
|
||||
# Initialize the result with metadata
|
||||
self.result.xlabel = "Time (s)"
|
||||
@@ -124,21 +109,29 @@ class TemperatureMonitor(threading.Thread):
|
||||
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
|
||||
# Store in data_pool (dict item assignment is atomic under the GIL)
|
||||
self.data_pool[self.data_key] = data_pool_result
|
||||
|
||||
# Reset consecutive failure counter on successful read
|
||||
self._consecutive_failures = 0
|
||||
self._last_error = None
|
||||
self._connection_ok = True
|
||||
|
||||
except Exception as e:
|
||||
print(f"TemperatureMonitor error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self._consecutive_failures += 1
|
||||
self._last_error = str(e)
|
||||
if self._consecutive_failures <= 3:
|
||||
print(f"TemperatureMonitor read error (attempt {self._consecutive_failures}): {e}")
|
||||
elif self._consecutive_failures == 5:
|
||||
print(f"TemperatureMonitor: repeated read errors, checking connection...")
|
||||
if hasattr(self.controller, 'check_connection'):
|
||||
self._connection_ok = self.controller.check_connection()
|
||||
if not self._connection_ok:
|
||||
print(f"TemperatureMonitor: WARNING - controller appears disconnected after {self._consecutive_failures} failures")
|
||||
elif self._consecutive_failures >= self._max_consecutive_failures:
|
||||
print(f"TemperatureMonitor: ERROR - {self._consecutive_failures} consecutive failures, controller may be disconnected")
|
||||
else:
|
||||
print(f"TemperatureMonitor error (failure #{self._consecutive_failures}): {e}")
|
||||
|
||||
# Sleep in small increments for responsive shutdown
|
||||
sleep_remaining = self.interval
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import serial
|
||||
import re
|
||||
import time
|
||||
import operator
|
||||
from functools import reduce
|
||||
|
||||
@@ -57,21 +58,22 @@ class Eurotherm(TemperatureController):
|
||||
def send_read_param(self, param):
|
||||
self.s.write(EOT + self.device + param + ENQ)
|
||||
|
||||
def read_param(self, param):
|
||||
self.s.flushInput()
|
||||
self.send_read_param(param)
|
||||
answer = self.s.read(self._expect_len)
|
||||
m = reply_pattern.search(answer)
|
||||
if m is None:
|
||||
# Reading _expect_len bytes was not enough...
|
||||
answer += self.s.read(200)
|
||||
def read_param(self, param, retries=3, retry_delay=0.05):
|
||||
for attempt in range(retries):
|
||||
self.s.flushInput()
|
||||
self.send_read_param(param)
|
||||
answer = self.s.read(self._expect_len)
|
||||
m = reply_pattern.search(answer)
|
||||
if m is not None:
|
||||
self._expect_len = len(answer)
|
||||
return m.group(1)
|
||||
else:
|
||||
print("received:", repr(answer))
|
||||
return None
|
||||
if m is None:
|
||||
answer += self.s.read(200)
|
||||
m = reply_pattern.search(answer)
|
||||
if m is not None:
|
||||
self._expect_len = len(answer)
|
||||
return m.group(1)
|
||||
if attempt < retries - 1:
|
||||
time.sleep(retry_delay)
|
||||
print("received:", repr(answer))
|
||||
return None
|
||||
|
||||
def write_param(self, mnemonic, data):
|
||||
if len(mnemonic) > 2:
|
||||
@@ -128,6 +130,14 @@ class Eurotherm(TemperatureController):
|
||||
"""Set target temperature in Celsius."""
|
||||
return self.write_param('SL', str(temperature))
|
||||
|
||||
def check_connection(self) -> bool:
|
||||
"""Check if the Eurotherm device is connected and responsive."""
|
||||
try:
|
||||
result = self.read_param('PV')
|
||||
return result is not None
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
"""Close the serial connection."""
|
||||
if hasattr(self, 's') and self.s is not None:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import serial
|
||||
import re
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
from .temperature import TemperatureController, register_controller
|
||||
@@ -91,13 +92,17 @@ class Lakeshore(TemperatureController):
|
||||
else:
|
||||
return 0.0
|
||||
|
||||
def read_param(self, param):
|
||||
def read_param(self, param, retries=3, retry_delay=0.05):
|
||||
"""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:]
|
||||
for attempt in range(retries):
|
||||
self.s.flushInput()
|
||||
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:]
|
||||
if attempt < retries - 1:
|
||||
time.sleep(retry_delay)
|
||||
return None
|
||||
|
||||
def get_value(self, name):
|
||||
@@ -134,9 +139,17 @@ class Lakeshore(TemperatureController):
|
||||
|
||||
def set_setpoint(self, temperature: float) -> bool:
|
||||
"""Set target temperature in Celsius."""
|
||||
self.s.write(f"SETP {temperature}\n")
|
||||
self.s.write(f"SETP 1,{temperature}\n")
|
||||
return True
|
||||
|
||||
def check_connection(self) -> bool:
|
||||
"""Check if the Lakeshore device is connected and responsive."""
|
||||
try:
|
||||
result = self.read_param('KRDG')
|
||||
return result is not None
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
"""Close the serial connection."""
|
||||
if hasattr(self, 's') and self.s is not None:
|
||||
|
||||
@@ -149,3 +149,13 @@ class TemperatureController(ABC):
|
||||
if they have resources to clean up.
|
||||
"""
|
||||
pass
|
||||
|
||||
def check_connection(self) -> bool:
|
||||
"""
|
||||
Check if the controller is connected and responsive.
|
||||
|
||||
Returns:
|
||||
bool: True if the controller is connected and responsive,
|
||||
False otherwise. Subclasses should override this method.
|
||||
"""
|
||||
return False
|
||||
Reference in new issue
Block a user