Merge branch 'developer' into dev/mac-build
Build on RHEL9 docker image / build (push) Successful in 5m0s
Build on RHEL8 docker image / build (push) Successful in 5m32s
Run Simulator Tests on local RHEL9 / build (push) Failing after 8m32s
Run Simulator Tests on local RHEL8 / build (push) Failing after 10m22s

This commit is contained in:
Erik Fröjdh authored and GitHub committed 2026-11-05 14:09:47 +01:00
commit 709ab4c3ed
67 files changed
+29667 -488

No files matched your search

-9
View File
@@ -16,8 +16,6 @@
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/js/theme.js"></script>
<link rel="index" title="Index" href="genindex.html" />
<link rel="search" title="Search" href="search.html" />
<link rel="next" title="10.0.0" href="https://slsdetectorgroup.github.io/slsDetectorPackage/10.0.0/index.html" />
</head>
@@ -32,13 +30,6 @@
<a href="#" class="icon icon-home">
slsDetectorPackage
</a>
<div role="search">
<form id="rtd-search-form" class="wy-form" action="search.html" method="get">
<input type="text" name="q" placeholder="Search docs" aria-label="Search docs" />
<input type="hidden" name="check_keywords" value="yes" />
<input type="hidden" name="area" value="default" />
</form>
</div>
</div><div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="Navigation menu">
<p class="caption" role="heading"><span class="caption-text">Versions:</span></p>
<ul>
-9
View File
@@ -16,8 +16,6 @@
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/js/theme.js"></script>
<link rel="index" title="Index" href="genindex.html" />
<link rel="search" title="Search" href="search.html" />
<link rel="next" title="{{ versions[0]["version"] }}" href="https://slsdetectorgroup.github.io/slsDetectorPackage/{{ versions[0]["version"] }}/index.html" />
</head>
@@ -32,13 +30,6 @@
<a href="#" class="icon icon-home">
slsDetectorPackage
</a>
<div role="search">
<form id="rtd-search-form" class="wy-form" action="search.html" method="get">
<input type="text" name="q" placeholder="Search docs" aria-label="Search docs" />
<input type="hidden" name="check_keywords" value="yes" />
<input type="hidden" name="area" value="default" />
</form>
</div>
</div><div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="Navigation menu">
<p class="caption" role="heading"><span class="caption-text">Versions:</span></p>
<ul>
+2 -5
View File
@@ -179,9 +179,8 @@ class AdcTab(QtWidgets.QWidget):
viewBox = self.mainWindow.plotAnalogImage.getView()
state = viewBox.getState()
try:
self.mainWindow.analog_frame = self._processImageData(data, aSamples, self.mainWindow.nADCEnabled)
self.plotTab.ignoreHistogramSignal = True
self.mainWindow.plotAnalogImage.setImage(self.mainWindow.analog_frame)
self.mainWindow.analog_frame = self._processImageData(data, aSamples, self.mainWindow.nADCEnabled)
self.mainWindow.plotAnalogImage.setImage(self.mainWindow.analog_frame, autoLevels=False) # manually update image levels
except Exception:
self.logger.exception('Exception Caught')
self.mainWindow.statusbar.setStyleSheet("color:red")
@@ -193,8 +192,6 @@ class AdcTab(QtWidgets.QWidget):
self.mainWindow.statusbar.showMessage(message)
print(message)
self.plotTab.setFrameLimits(self.mainWindow.analog_frame)
# keep the zoomed in state (not 1st image)
if self.mainWindow.firstAnalogImage:
self.mainWindow.firstAnalogImage = False
+5 -5
View File
@@ -133,7 +133,7 @@ class AcquisitionTab(QtWidgets.QWidget):
self.view.labelDigital.setDisabled(True)
self.view.labelTransceiver.setDisabled(True)
self.view.spinBoxTransceiver.setDisabled(True)
self.plotTab.enableallCounterCheckBox(False)
self.plotTab.showallCounterCheckBox(False)
case readoutMode.DIGITAL_ONLY:
self.view.spinBoxAnalog.setDisabled(True)
self.view.labelAnalog.setDisabled(True)
@@ -141,7 +141,7 @@ class AcquisitionTab(QtWidgets.QWidget):
self.view.labelDigital.setEnabled(True)
self.view.labelTransceiver.setDisabled(True)
self.view.spinBoxTransceiver.setDisabled(True)
self.plotTab.enableallCounterCheckBox(False)
self.plotTab.showallCounterCheckBox(False)
case readoutMode.ANALOG_AND_DIGITAL:
self.view.spinBoxAnalog.setEnabled(True)
self.view.labelAnalog.setEnabled(True)
@@ -149,7 +149,7 @@ class AcquisitionTab(QtWidgets.QWidget):
self.view.labelDigital.setEnabled(True)
self.view.labelTransceiver.setDisabled(True)
self.view.spinBoxTransceiver.setDisabled(True)
self.plotTab.enableallCounterCheckBox(False)
self.plotTab.showallCounterCheckBox(False)
case readoutMode.TRANSCEIVER_ONLY:
self.view.spinBoxAnalog.setDisabled(True)
self.view.labelAnalog.setDisabled(True)
@@ -157,7 +157,7 @@ class AcquisitionTab(QtWidgets.QWidget):
self.view.labelDigital.setDisabled(True)
self.view.labelTransceiver.setEnabled(True)
self.view.spinBoxTransceiver.setEnabled(True)
self.plotTab.enableallCounterCheckBox(self.plotTab.view.radioButtonImage.isChecked()) # enable counter checkboxes for matterhorn
self.plotTab.showallCounterCheckBox(self.plotTab.view.radioButtonImage.isChecked()) # enable counter checkboxes for matterhorn
case _:
self.view.spinBoxAnalog.setDisabled(True)
self.view.labelAnalog.setDisabled(True)
@@ -165,7 +165,7 @@ class AcquisitionTab(QtWidgets.QWidget):
self.view.labelDigital.setEnabled(True)
self.view.labelTransceiver.setEnabled(True)
self.view.spinBoxTransceiver.setEnabled(True)
self.plotTab.enableallCounterCheckBox(self.plotTab.view.radioButtonImage.isChecked()) # enable counter checkboxes for matterhorn
self.plotTab.showallCounterCheckBox(self.plotTab.view.radioButtonImage.isChecked()) # enable counter checkboxes for matterhorn
self.view.comboBoxROMode.currentIndexChanged.connect(self.setReadOut)
self.view.spinBoxAnalog.editingFinished.connect(self.setAnalog)
+258 -108
View File
@@ -21,8 +21,6 @@ class PlotTab(QtWidgets.QWidget):
def __init__(self, parent):
super().__init__(parent)
pg.setConfigOptions(imageAxisOrder="row-major")
self.frame_min: float = 0.0
self.frame_max: float = 0.0
uic.loadUi(Path(__file__).parent.parent / 'ui' / "plot.ui", parent)
self.view = parent
self.mainWindow = None
@@ -31,11 +29,27 @@ class PlotTab(QtWidgets.QWidget):
self.transceiverTab = None
self.acquisitionTab = None
self.adcTab = None
self.cmin: float = 0.0
self.cmax: float = 0.0
self.colorRangeMode: Defines.colorRange = Defines.colorRange.all
self.ignoreHistogramSignal: bool = False
self.imagePlots: list[PlotWidget] = []
self.shownCounterImages: list[bool] = [True] * Defines.max_counters # per default show all 4 counter images
self.showtransceiverImage : bool = False
self.showdigitalImage : bool = False
self.showanalogImage : bool = False
# color range variables
self.colorRangeMode: Defines.colorRange = Defines.colorRange.auto
self.digitalcolorrange : list[float] = [0.0, 0.0]
self.analogcolorrange : list[float] = [0.0, 0.0]
self.transceivercolorrange : list[list[float]] = [[0.0, 0.0] for _ in range(Defines.max_counters)]
self.digitalcminSpinBoxes : QtWidgets.QSpinBox = None
self.digitalcmaxSpinBoxes : QtWidgets.QSpinBox = None
self.analogcminSpinBoxes : QtWidgets.QSpinBox = None
self.analogcmaxSpinBoxes : QtWidgets.QSpinBox = None
self.transceivercminSpinBoxes : list[QtWidgets.QSpinBox] = None
self.transceivercmaxSpinBoxes : list[QtWidgets.QSpinBox] = None
# list of callback functions to notify tabs when we should hide their legend
# follows the observer design pattern
self.hideLegendObservers = []
@@ -56,14 +70,16 @@ class PlotTab(QtWidgets.QWidget):
self.checkBoxCounters = [self.view.checkBoxCounter0, self.view.checkBoxCounter1, self.view.checkBoxCounter2, self.view.checkBoxCounter3]
for checkBox in self.checkBoxCounters:
checkBox.setChecked(False)
checkBox.setChecked(True) # per default enable all
checkBox.setEnabled(False)
self.imagePlots = (
self.mainWindow.plotAnalogImage,
self.mainWindow.plotDigitalImage,
self.transceiverTab.transceiverImageViews,
)
self.transceivercminSpinBoxes = [self.view.transceivercminSpinBox0, self.view.transceivercminSpinBox1, self.view.transceivercminSpinBox2, self.view.transceivercminSpinBox3]
self.transceivercmaxSpinBoxes = [self.view.transceivercmaxSpinBox0, self.view.transceivercmaxSpinBox1, self.view.transceivercmaxSpinBox2, self.view.transceivercmaxSpinBox3]
self.analogcminSpinBoxes = self.view.analogcminSpinBox
self.analogcmaxSpinBoxes = self.view.analogcmaxSpinBox
self.digitalcminSpinBoxes = self.view.digitalcminSpinBox
self.digitalcmaxSpinBoxes = self.view.digitalcmaxSpinBox
def connect_ui(self):
self.view.radioButtonNoPlot.clicked.connect(self.plotOptions)
@@ -95,14 +111,24 @@ class PlotTab(QtWidgets.QWidget):
self.view.spinBoxPlot.editingFinished.connect(self.setPlotBit)
self.view.pushButtonReferesh.clicked.connect(self.acquisitionTab.refresh)
# color range widgets
self.view.cminSpinBox.editingFinished.connect(self.setCmin)
self.view.cmaxSpinBox.editingFinished.connect(self.setCmax)
self.view.radioButtonAutomatic.clicked.connect(partial(self.setColorRangeMode, Defines.colorRange.all))
self.view.radioButtonAutomatic.clicked.connect(partial(self.setColorRangeMode, Defines.colorRange.auto))
self.view.radioButtonFixed.clicked.connect(partial(self.setColorRangeMode, Defines.colorRange.fixed))
self.view.radioButtonCenter.clicked.connect(partial(self.setColorRangeMode, Defines.colorRange.center))
for index, cminSpinBox in enumerate(self.transceivercminSpinBoxes):
cminSpinBox.editingFinished.connect(partial(self.setCmin, imagetype = Defines.ImageType.TRANSCEIVER, index=index))
for index, cmaxSpinBox in enumerate(self.transceivercmaxSpinBoxes):
cmaxSpinBox.editingFinished.connect(partial(self.setCmax, imagetype = Defines.ImageType.TRANSCEIVER, index=index))
self.analogcminSpinBoxes.editingFinished.connect(partial(self.setCmin, imagetype = Defines.ImageType.ANALOG, index=0))
self.analogcmaxSpinBoxes.editingFinished.connect(partial(self.setCmax, imagetype = Defines.ImageType.ANALOG, index=0))
self.digitalcminSpinBoxes.editingFinished.connect(partial(self.setCmin, imagetype = Defines.ImageType.DIGITAL, index=0))
self.digitalcmaxSpinBoxes.editingFinished.connect(partial(self.setCmax, imagetype = Defines.ImageType.DIGITAL, index=0))
for index, checkBox in enumerate(self.checkBoxCounters):
checkBox.stateChanged.connect(partial(self.displayCounter, index))
checkBox.stateChanged.connect(lambda state, index=index: self.setCounterCheckBox(index, is_checked=(state == QtCore.Qt.Checked)))
# show image Values for analog image
nMaxY = lambda : self.mainWindow.nAnalogRows
@@ -110,7 +136,9 @@ class PlotTab(QtWidgets.QWidget):
frame = lambda : self.mainWindow.analog_frame
plot = self.mainWindow.plotAnalogImage
plot.scene.sigMouseMoved.connect(partial(self.showPlotValues, plot, nMaxX, nMaxY, frame))
plot.getHistogramWidget().item.sigLevelChangeFinished.connect(partial(self.handleHistogramChange, plot))
plot.getImageItem().sigImageChanged.connect(partial(self.updateColorRange, imagetype = Defines.ImageType.ANALOG, plot=plot))
plot.getHistogramWidget().item.sigLevelChangeFinished.connect(lambda _histItem, plot=plot: self.handleHistogramChange(plot, imagetype=Defines.ImageType.ANALOG))
# show image Values for digital image
nMaxY = lambda : self.mainWindow.nDigitalRows
@@ -118,7 +146,9 @@ class PlotTab(QtWidgets.QWidget):
frame = lambda : self.mainWindow.digital_frame
plot = self.mainWindow.plotDigitalImage
plot.scene.sigMouseMoved.connect(partial(self.showPlotValues, plot, nMaxX, nMaxY, frame))
plot.getHistogramWidget().item.sigLevelChangeFinished.connect(partial(self.handleHistogramChange, plot))
plot.getImageItem().sigImageChanged.connect(partial(self.updateColorRange, imagetype = Defines.ImageType.DIGITAL, plot=plot))
plot.getHistogramWidget().item.sigLevelChangeFinished.connect(lambda _histItem, plot=plot: self.handleHistogramChange(plot, imagetype=Defines.ImageType.DIGITAL))
# show image Values for transceiver image
nMaxY = lambda : self.transceiverTab.nTransceiverRows
@@ -127,28 +157,39 @@ class PlotTab(QtWidgets.QWidget):
for index, image_view in enumerate(self.transceiverTab.transceiverImageViews):
frame = lambda image_view=image_view: image_view.getImageItem().image
image_view.scene.sigMouseMoved.connect(partial(self.showPlotValues, image_view, nMaxX, nMaxY, frame))
image_view.getHistogramWidget().item.sigLevelChangeFinished.connect(partial(self.handleHistogramChange, image_view))
image_view.getImageItem().sigImageChanged.connect(partial(self.updateColorRange, imagetype = Defines.ImageType.TRANSCEIVER, plot=image_view, index=index))
image_view.getHistogramWidget().item.sigLevelChangeFinished.connect(
lambda _histItem, image_view=image_view, index=index: self.handleHistogramChange(plot = image_view, imagetype=Defines.ImageType.TRANSCEIVER, index=index)) # sigLevelChangeFinished passes the histogram item as the first argument, we need to ignore it and pass the image_view instead
self.view.checkBoxShowLegend.stateChanged.connect(self.toggleLegend)
def displayCounter(self, index : int, state : int):
# toggle the display of the counter i and update the splitter
self.transceiverTab.shownCounters[index] = (state == QtCore.Qt.Checked)
def setCounterCheckBox(self, index : int, is_checked : bool):
"""
slot function for the counter checkboxes, enables/disables the cmin/cmax spinboxes for the counter at index, updates image splitter to show counter image
"""
self.transceivercmaxSpinBoxes[index].setEnabled(is_checked and self.colorRangeMode == Defines.colorRange.fixed)
self.transceivercminSpinBoxes[index].setEnabled(is_checked and self.colorRangeMode == Defines.colorRange.fixed)
self.shownCounterImages[index] = is_checked # TODO: maybe I dont need that
self.transceiverTab.update_ImageSplitter()
def enableCounterCheckBox(self, index : int, enabled : bool):
self.checkBoxCounters[index].setEnabled(enabled)
is_checked = self.checkBoxCounters[index].isChecked()
self.transceivercmaxSpinBoxes[index].setEnabled(enabled and is_checked and self.colorRangeMode == Defines.colorRange.fixed)
self.transceivercminSpinBoxes[index].setEnabled(enabled and is_checked and self.colorRangeMode == Defines.colorRange.fixed)
def setCounterCheckBox(self, index : int, check : bool):
self.checkBoxCounters[index].setChecked(check)
def enableCounterCheckBox(self, index : int, enabled : bool):
is_transceiver = self.mainWindow.romode.value in [3, 4]
is_image = self.view.radioButtonImage.isChecked()
self.checkBoxCounters[index].setEnabled(enabled and is_transceiver and is_image)
def enableallCounterCheckBox(self, enabled : bool):
def showallCounterCheckBox(self, enabled : bool):
for checkBox in self.checkBoxCounters:
checkBox.setEnabled(enabled)
checkBox.setVisible(enabled)
self.view.labelCounters.setVisible(enabled)
for cminSpinBox, cmaxSpinBox in zip(self.transceivercminSpinBoxes, self.transceivercmaxSpinBoxes):
cminSpinBox.setEnabled(enabled and self.colorRangeMode == Defines.colorRange.fixed and self.checkBoxCounters[self.transceivercminSpinBoxes.index(cminSpinBox)].isChecked())
cmaxSpinBox.setEnabled(enabled and self.colorRangeMode == Defines.colorRange.fixed and self.checkBoxCounters[self.transceivercmaxSpinBoxes.index(cmaxSpinBox)].isChecked())
def refresh(self):
self.getZMQHWM()
@@ -231,29 +272,81 @@ class PlotTab(QtWidgets.QWidget):
for notify_function in self.hideLegendObservers:
notify_function()
def setCmin(self, value=None):
def setCmin(self, imagetype : Defines.ImageType, value = None, index = 0, updateColorBar=True):
"""
slot for setting cmin from cminSpinBox
also used as a normal function
"""
if value is None:
self.cmin = self.view.cminSpinBox.value()
else:
self.cmin = value
self.updateColorRangeUI()
Set the minimum color range value
def setCmax(self, value=None):
Parameters
----------
imagetype : Defines.ImageType
The type of image (DIGITAL, ANALOG, or TRANSCEIVER).
value : float, optional
The value to set for the minimum color range (default is None).
index : int, optional
The index of the counter (default is 0).
updateColorBar : bool, optional
Whether to update the color bar (default is True).
"""
slot for setting cmax from cmaxSpinBox
also used as a normal function
"""
if value is None:
self.cmax = self.view.cmaxSpinBox.value()
else:
self.cmax = value
self.updateColorRangeUI()
def setColorRangeMode(self, mode):
if value is None:
if imagetype == Defines.ImageType.DIGITAL:
self.digitalcolorrange[0] = self.digitalcminSpinBoxes.value()
elif imagetype == Defines.ImageType.ANALOG:
self.analogcolorrange[0] = self.analogcminSpinBoxes.value()
else:
self.transceivercolorrange[index][0] = self.transceivercminSpinBoxes[index].value()
else:
if imagetype == Defines.ImageType.DIGITAL:
self.digitalcolorrange[0] = value
self.digitalcminSpinBoxes.setValue(self.digitalcolorrange[0])
elif imagetype == Defines.ImageType.ANALOG:
self.analogcolorrange[0] = value
self.analogcminSpinBoxes.setValue(self.analogcolorrange[0])
else:
self.transceivercolorrange[index][0] = value
self.transceivercminSpinBoxes[index].setValue(self.transceivercolorrange[index][0])
if updateColorBar:
self.updateColorBar(imagetype = imagetype, index=index)
def setCmax(self, imagetype : Defines.ImageType, value=None, index = 0, updateColorBar=True):
"""
Set the maximum color range value
Parameters
----------
imagetype : Defines.ImageType
The type of image (DIGITAL, ANALOG, or TRANSCEIVER).
value : float, optional
The value to set for the minimum color range (default is None).
index : int, optional
The index of the counter (default is 0).
updateColorBar : bool, optional
Whether to update the color bar (default is True).
"""
if value is None:
if imagetype == Defines.ImageType.DIGITAL:
self.digitalcolorrange[1] = self.digitalcmaxSpinBoxes.value()
elif imagetype == Defines.ImageType.ANALOG:
self.analogcolorrange[1] = self.analogcmaxSpinBoxes.value()
else:
self.transceivercolorrange[index][1] = self.transceivercmaxSpinBoxes[index].value()
else:
if imagetype == Defines.ImageType.DIGITAL:
self.digitalcolorrange[1] = value
self.digitalcmaxSpinBoxes.setValue(self.digitalcolorrange[1])
elif imagetype == Defines.ImageType.ANALOG:
self.analogcolorrange[1] = value
self.analogcmaxSpinBoxes.setValue(self.analogcolorrange[1])
else:
self.transceivercolorrange[index][1] = value
self.transceivercmaxSpinBoxes[index].setValue(self.transceivercolorrange[index][1])
if updateColorBar:
self.updateColorBar(imagetype=imagetype, index=index)
def setColorRangeMode(self, mode : Defines.colorRange):
"""
slot for setting the color range mode (all,fixed,3-97%)
"""
@@ -261,73 +354,123 @@ class PlotTab(QtWidgets.QWidget):
# disable or enable cmin/cmax spinboxes
enableSpinBoxes = mode == Defines.colorRange.fixed
self.view.cminSpinBox.setEnabled(enableSpinBoxes)
self.view.cmaxSpinBox.setEnabled(enableSpinBoxes)
self.updateColorRange()
self.updateColorRangeUI()
def handleHistogramChange(self, plot):
"""
slot called after changing the color bar
This is called even when pyqtgraph sets the image without any user intervention
the class attribute ignore_histogram_signal is set to False before setting the image
so that we can distinguish between the signal originates from pyqt or from the user
"""
if not self.ignoreHistogramSignal:
self.cmin, self.cmax = plot.getHistogramWidget().item.getLevels()
self.setCmin(self.cmin)
self.setCmax(self.cmax)
self.ignoreHistogramSignal = False
# self.setColorRangeMode(Defines.colorRange.fixed)
for cminSpinBox, cmaxSpinBox, checkBox in zip(self.transceivercminSpinBoxes, self.transceivercmaxSpinBoxes, self.checkBoxCounters):
cminSpinBox.setEnabled(enableSpinBoxes and (checkBox.isChecked() and checkBox.isEnabled() and self.showtransceiverImage)) # if the checkbox is disabled, we still want to enable the spinbox
cmaxSpinBox.setEnabled(enableSpinBoxes and (checkBox.isChecked() and checkBox.isEnabled() and self.showtransceiverImage)) # if the checkbox is disabled, we still want to enable the spinbox
def setFrameLimits(self, frame):
"""
function called from AcquisitionTab::read_zmq to get the maximum and minimum
values of the decoded frame and save them in frame_min/frame_max
"""
self.frame_min = np.min(frame)
self.frame_max = np.max(frame)
self.updateColorRange()
self.analogcminSpinBoxes.setEnabled(enableSpinBoxes and self.showanalogImage)
self.analogcmaxSpinBoxes.setEnabled(enableSpinBoxes and self.showanalogImage)
def updateColorRange(self):
self.digitalcminSpinBoxes.setEnabled(enableSpinBoxes and self.showdigitalImage)
self.digitalcmaxSpinBoxes.setEnabled(enableSpinBoxes and self.showdigitalImage)
for index in range(Defines.max_counters):
self.updateColorRange(imagetype=Defines.ImageType.TRANSCEIVER, index=index)
self.updateColorRange(imagetype=Defines.ImageType.ANALOG)
self.updateColorRange(imagetype=Defines.ImageType.DIGITAL)
def handleHistogramChange(self, plot, imagetype : Defines.ImageType, index=0):
"""
Parameters
----------
plot : Plot
The plot object whose histogram has changed.
imagetype : Defines.ImageType
The type of image (DIGITAL, ANALOG, or TRANSCEIVER).
index : int, optional
The index of the counter (default is 0).
"""
cmin, cmax = plot.getHistogramWidget().item.getLevels()
self.setCmin(imagetype=imagetype, value=cmin, index=index, updateColorBar=False)
self.setCmax(imagetype=imagetype, value=cmax, index=index, updateColorBar=False)
def updateColorRange(self, imagetype : Defines.ImageType, plot = None, index=0):
"""
Updates the color range of the plot at index based on the current color range mode.
for mode:
- all: sets cmin and cmax to the maximums/minimum values of the frame
- auto: sets cmin and cmax to the maximums/minimum values of the frame
- 3-97%: limits cmax and cmin so that we ignore 3% from each end of the whole range
- fixed: this function does not change cmin and cmax
"""
if self.colorRangeMode == Defines.colorRange.all:
self.cmin = self.frame_min
self.cmax = self.frame_max
elif self.colorRangeMode == Defines.colorRange.center:
self.cmin = self.frame_min + 0.03 * (self.frame_max - self.frame_min)
self.cmax = self.frame_max - 0.03 * (self.frame_max - self.frame_min)
self.updateColorRangeUI()
def updateColorRangeUI(self):
Parameters
----------
imagetype : Defines.ImageType
The type of image (DIGITAL, ANALOG, or TRANSCEIVER).
plot : Plot, optional
The plot object to update (default is None).
index : int, optional
The index of the counter (default is 0).
"""
updates UI views should be called after every change to cmin or cmax
"""
for plot in self.imagePlots:
if isinstance(plot, list): # hacky for now only transceiver image is a PlotSplitter
for p in plot:
p.getHistogramWidget().item.setLevels(min=self.cmin, max=self.cmax)
if imagetype == Defines.ImageType.TRANSCEIVER: # update transceiver
defaultPlot = self.transceiverTab.transceiverImageViews[index]
cmin, cmax = self.transceivercolorrange[index]
elif imagetype == Defines.ImageType.DIGITAL: # update digital
defaultPlot = self.mainWindow.plotDigitalImage
cmin, cmax = self.digitalcolorrange
elif imagetype == Defines.ImageType.ANALOG: # update analog
defaultPlot = self.mainWindow.plotAnalogImage
cmin, cmax = self.analogcolorrange
if plot is None:
plot = defaultPlot
if self.colorRangeMode == Defines.colorRange.auto:
if plot.getImageItem().image is None or plot.getImageItem().image.size == 0:
return
else:
plot.getHistogramWidget().item.setLevels(min=self.cmin, max=self.cmax)
self.view.cminSpinBox.setValue(self.cmin)
self.view.cmaxSpinBox.setValue(self.cmax)
frame_min = np.min(plot.getImageItem().image)
frame_max = np.max(plot.getImageItem().image)
self.setCmin(imagetype=imagetype, value=frame_min, index=index)
self.setCmax(imagetype=imagetype, value=frame_max, index=index)
elif self.colorRangeMode == Defines.colorRange.center:
if plot.getImageItem().image is None or plot.getImageItem().image.size == 0:
return
else:
frame_min = np.min(plot.getImageItem().image)
frame_max = np.max(plot.getImageItem().image)
self.setCmin(imagetype=imagetype, value=frame_min + 0.03 * (frame_max - frame_min), index=index)
self.setCmax(imagetype=imagetype, value=frame_max - 0.03 * (frame_max - frame_min), index=index)
elif self.colorRangeMode == Defines.colorRange.fixed:
self.setCmin(imagetype=imagetype, value=cmin, index=index)
self.setCmax(imagetype=imagetype, value=cmax, index=index)
def updateColorBar(self, imagetype : Defines.ImageType, index = 0):
"""
updates the color bar of the plot at index
"""
plot = None
cmin, cmax = 0.0, 0.0
if imagetype == Defines.ImageType.TRANSCEIVER:
plot = self.transceiverTab.transceiverImageViews[index]
cmin, cmax = self.transceivercolorrange[index]
elif imagetype == Defines.ImageType.DIGITAL: # update digital
plot = self.mainWindow.plotDigitalImage
cmin, cmax = self.digitalcolorrange
elif imagetype == Defines.ImageType.ANALOG: # update analog
plot = self.mainWindow.plotAnalogImage
cmin, cmax = self.analogcolorrange
if plot is not None:
histItem = plot.getHistogramWidget().item
histItem.blockSignals(True) # avoid triggering handleHistogramChange while setting the levels
histItem.setLevels(min=cmin, max=cmax)
histItem.blockSignals(False)
def setColorMap(self):
cm = pg.colormap.getFromMatplotlib(self.view.comboBoxColorMap.currentText())
# print(f'color map:{self.comboBoxColorMap.currentText()}')
self.mainWindow.plotAnalogImage.setColorMap(cm)
self.mainWindow.plotDigitalImage.setColorMap(cm)
for i, showncounter in enumerate(self.transceiverTab.shownCounters):
if showncounter:
self.transceiverTab.transceiverImageViews[i].setColorMap(cm)
for i, showncounter in enumerate(self.shownCounterImages):
self.transceiverTab.transceiverImageViews[i].setColorMap(cm)
def getZMQHWM(self):
@@ -399,11 +542,11 @@ class PlotTab(QtWidgets.QWidget):
self.mainWindow.transceiverPlots[i].hide()
def addAllSelectedTransceiverPlots(self):
for i in range(Defines.transceiver.maxcount):
for i in range(Defines.max_counters):
self.addSelectedTransceiverPlots(i)
def removeAllTransceiverPlots(self):
for i in range(Defines.transceiver.maxcount):
for i in range(Defines.max_counters):
self.mainWindow.transceiverPlots[i].hide()
def showPlot(self):
@@ -417,15 +560,21 @@ class PlotTab(QtWidgets.QWidget):
self.view.radioButtonOverlay.setDisabled(True)
self.view.radioButtonStripe.setDisabled(True)
self.showtransceiverImage = False
self.showanalogImage = False
self.showdigitalImage = False
if self.mainWindow.romode.value in [0, 2]:
if self.view.radioButtonWaveform.isChecked():
self.mainWindow.plotAnalogWaveform.show()
elif self.view.radioButtonImage.isChecked():
self.showanalogImage = True
self.mainWindow.plotAnalogImage.show()
if self.mainWindow.romode.value in [1, 2, 4]:
if self.view.radioButtonWaveform.isChecked():
self.mainWindow.plotDigitalWaveform.show()
elif self.view.radioButtonImage.isChecked():
self.showdigitalImage = True
self.mainWindow.plotDigitalImage.show()
self.view.labelDigitalWaveformOption.setEnabled(True)
self.view.radioButtonOverlay.setEnabled(True)
@@ -435,7 +584,8 @@ class PlotTab(QtWidgets.QWidget):
if self.view.radioButtonWaveform.isChecked():
self.mainWindow.plotTransceiverWaveform.show()
elif self.view.radioButtonImage.isChecked():
for i, showncounter in enumerate(self.transceiverTab.shownCounters):
self.showtransceiverImage = True
for i, showncounter in enumerate(self.shownCounterImages):
self.transceiverTab.transceiverImageViews[i].setVisible(showncounter)
self.transceiverTab.transceiverImageSplitter.show()
@@ -463,18 +613,18 @@ class PlotTab(QtWidgets.QWidget):
'bottom', "<span style=\"color:black;font-size:14px\">Transceiver Sample [#]</span>")
self.view.stackedWidgetPlotType.setCurrentIndex(0)
self.enableallCounterCheckBox(False) # disable counter checkboxes for waveform
self.showallCounterCheckBox(False) # disable counter checkboxes for waveform
elif self.view.radioButtonImage.isChecked():
self.view.stackedWidgetPlotType.setCurrentIndex(2)
is_transceiver = self.mainWindow.romode.value in [3, 4]
self.enableallCounterCheckBox(is_transceiver) # enable counter checkboxes for matterhorn
self.showallCounterCheckBox(is_transceiver) # enable counter checkboxes for matterhorn
self.setDecoder()
if self.view.radioButtonNoPlot.isChecked():
self.view.labelPlotOptions.hide()
self.view.stackedWidgetPlotType.hide()
self.enableallCounterCheckBox(False) # disable counter checkboxes when no plot is selected
self.showallCounterCheckBox(False) # disable counter checkboxes when no plot is selected
# enable plotting
else:
self.view.labelPlotOptions.show()
+1 -1
View File
@@ -193,7 +193,7 @@ class SignalsTab(QtWidgets.QWidget):
self.mainWindow.nDigitalRows = 0
self.mainWindow.nDigitalCols = 0
self.mainWindow.digital_frame = np.zeros((self.mainWindow.nDigitalRows, self.mainWindow.nDigitalCols))
self.mainWindow.plotDigitalImage.setImage(self.mainWindow.digital_frame)
self.mainWindow.plotDigitalImage.setImage(self.mainWindow.digital_frame, autoLevels=False)
self.mainWindow.verticalLayoutPlot.addWidget(self.mainWindow.plotDigitalImage, 4)
def updateSignalNames(self):
+14 -18
View File
@@ -24,18 +24,17 @@ class TransceiverTab(QtWidgets.QWidget):
self.plotTab = None
self.legend: LegendItem | None = None
self.acquisitionTab = None
self.nCounters: int = Defines.transceiver.maxcount
self.nCounters: int = Defines.max_counters
self.nTransceiverRows : int = 0
self.nTransceiverCols : int = 0
self.shownCounters: list[bool] = [True] * Defines.transceiver.maxcount # per default show all 4 counters
self.transceiverImageSplitter = QtWidgets.QSplitter(QtCore.Qt.Vertical)
self.transceiverImageViews : list = [] # pg image view for each counter
self.firstTransceiverImage : list[bool] = [True] * Defines.transceiver.maxcount # to keep track of first image for each counter to maintain zoom state
self.firstTransceiverImage : list[bool] = [True] * Defines.max_counters # to keep track of first image for each counter to maintain zoom state
def setup_ui(self):
self.plotTab = self.mainWindow.plotTab
self.acquisitionTab = self.mainWindow.acquisitionTab
for i in range(Defines.transceiver.maxcount):
for i in range(Defines.max_counters):
self.setTransceiverButtonColor(i, self.plotTab.getRandomColor())
self.initializeAllTransceiverPlots()
@@ -46,7 +45,7 @@ class TransceiverTab(QtWidgets.QWidget):
self.plotTab.subscribeToggleLegend(self.updateLegend)
def connect_ui(self):
for i in range(Defines.transceiver.maxcount):
for i in range(Defines.max_counters):
getattr(self.view, f"checkBoxTransceiver{i}").stateChanged.connect(partial(self.setTransceiverEnable, i))
getattr(self.view,
f"checkBoxTransceiver{i}Plot").stateChanged.connect(partial(self.setTransceiverEnablePlot, i))
@@ -62,7 +61,7 @@ class TransceiverTab(QtWidgets.QWidget):
"""
enabledPlots = []
self.legend.clear()
for i in range(Defines.transceiver.maxcount):
for i in range(Defines.max_counters):
if getattr(self.view, f'checkBoxTransceiver{i}Plot').isChecked():
plotName = getattr(self.view, f"labelTransceiver{i}").text()
enabledPlots.append((self.mainWindow.transceiverPlots[i], plotName))
@@ -111,7 +110,7 @@ class TransceiverTab(QtWidgets.QWidget):
trans_array = self._processWaveformData(data, dSamples, self.mainWindow.romode.value,
self.mainWindow.nDBitEnabled, self.nTransceiverEnabled)
idx = 0
for i in range(Defines.transceiver.maxcount):
for i in range(Defines.max_counters):
checkBoxPlot = getattr(self.view, f"checkBoxTransceiver{i}Plot")
checkBoxEn = getattr(self.view, f"checkBoxTransceiver{i}")
if checkBoxEn.isChecked() and checkBoxPlot.isChecked():
@@ -156,21 +155,18 @@ class TransceiverTab(QtWidgets.QWidget):
try:
transceiver_frame = self._processImageData(data, dSamples, self.mainWindow.romode.value,
self.mainWindow.nDBitEnabled)
self.plotTab.ignoreHistogramSignal = True
for i in range(transceiver_frame.shape[0]):
self.transceiverImageViews[i].setImage(transceiver_frame[i])
self.transceiverImageViews[i].setImage(transceiver_frame[i], autoLevels=False) #manually update image levels
except Exception as e:
self.mainWindow.statusbar.setStyleSheet("color:red")
self.acquisitionTab.updateCurrentFrame('Invalid Image')
self.mainWindow.statusbar.showMessage(str(e))
print("Error: ", str(e))
self.plotTab.setFrameLimits(transceiver_frame)
# keep the zoomed in state (not 1st image)
for idx, image_view in enumerate(self.transceiverImageViews):
if(self.firstTransceiverImage[idx] and self.shownCounters[idx]):
if(self.firstTransceiverImage[idx] and self.plotTab.shownCounterImages[idx]):
self.firstTransceiverImage[idx] = False
else:
image_view.getView().setState(image_states[idx])
@@ -180,16 +176,16 @@ class TransceiverTab(QtWidgets.QWidget):
def update_numCounters(self, num_counters):
# update the number of counters and adjust the image splitter accordingly
self.nCounters = num_counters
for i in range(Defines.transceiver.maxcount):
self.shownCounters[i] = i < self.nCounters
self.plotTab.setCounterCheckBox(i, self.shownCounters[i]) # check the counter checkbox
self.plotTab.enableCounterCheckBox(i, self.shownCounters[i]) # disable counter checkbox
for i in range(Defines.max_counters):
self.plotTab.shownCounterImages[i] = i < self.nCounters
self.plotTab.setCounterCheckBox(i, self.plotTab.shownCounterImages[i]) # check the counter checkbox
self.plotTab.enableCounterCheckBox(i, self.plotTab.shownCounterImages[i]) # disable counter checkbox
self.update_ImageSplitter() # update the splitter to show/hide image views based on the number of counters
def update_ImageSplitter(self):
for i, showncounter in enumerate(self.shownCounters):
for i, showncounter in enumerate(self.plotTab.shownCounterImages):
self.transceiverImageViews[i].setVisible(showncounter)
def initializeAllTransceiverPlots(self):
@@ -198,7 +194,7 @@ class TransceiverTab(QtWidgets.QWidget):
self.mainWindow.verticalLayoutPlot.addWidget(self.mainWindow.plotTransceiverWaveform, 5)
self.mainWindow.transceiverPlots = {}
waveform = np.zeros(1000)
for i in range(Defines.transceiver.maxcount):
for i in range(Defines.max_counters):
pen = pg.mkPen(color=self.getTransceiverButtonColor(i), width=1)
legendName = getattr(self.view, f"labelTransceiver{i}").text()
self.mainWindow.transceiverPlots[i] = self.mainWindow.plotTransceiverWaveform.plot(waveform,
+377 -184
View File
@@ -425,7 +425,7 @@
<string>Moench04</string>
</property>
</item>
<item>
<item>
<property name="text">
<string>Moench05</string>
</property>
@@ -505,7 +505,7 @@
<property name="geometry">
<rect>
<x>10</x>
<y>440</y>
<y>540</y>
<width>841</width>
<height>400</height>
</rect>
@@ -1150,7 +1150,7 @@
<x>10</x>
<y>221</y>
<width>841</width>
<height>91</height>
<height>191</height>
</rect>
</property>
<property name="sizePolicy">
@@ -1166,197 +1166,390 @@
<enum>QFrame::Raised</enum>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="topMargin">
<number>9</number>
</property>
<property name="bottomMargin">
<number>9</number>
</property>
<item row="0" column="1">
<widget class="QFrame" name="frame_3">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="0">
<widget class="QRadioButton" name="radioButtonCenter">
<property name="enabled">
<bool>true</bool>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>15</height>
</size>
</property>
<property name="text">
<string>3-97%</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup_4</string>
</attribute>
</widget>
</item>
<item row="0" column="0">
<widget class="QRadioButton" name="radioButtonAutomatic">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>15</height>
</size>
</property>
<property name="text">
<string>All</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup_4</string>
</attribute>
</widget>
</item>
<item row="2" column="0">
<widget class="QRadioButton" name="radioButtonFixed">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>15</height>
</size>
</property>
<property name="text">
<string>Fixed</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup_4</string>
</attribute>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="5">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="minimumSize">
<size>
<width>0</width>
<height>31</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>31</height>
</size>
</property>
<property name="text">
<string>Color Range:</string>
</property>
</widget>
<layout class="QHBoxLayout" name="horizontalLayoutColorRange">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Color Range:</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioButtonAutomatic">
<property name="text">
<string>Auto</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup_4</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioButtonCenter">
<property name="text">
<string>3-97%</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup_4</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioButtonFixed">
<property name="text">
<string>Fixed</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup_4</string>
</attribute>
</widget>
</item>
<item>
<spacer name="horizontalSpacerColorRange">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="0" column="4">
<widget class="QDoubleSpinBox" name="cminSpinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>31</height>
</size>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
<item row="1" column="0">
<widget class="QTabWidget" name="tabWidgetColorRange">
<property name="currentIndex">
<number>2</number>
</property>
<widget class="QWidget" name="tabColorRangeDigital">
<attribute name="title">
<string>Digital</string>
</attribute>
<layout class="QHBoxLayout" name="horizontalLayoutColorRangeDigital">
<item>
<widget class="QLabel" name="labelColorRangeValuesDigital">
<property name="text">
<string>Color range values:</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="digitalcminSpinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="prefix">
<string>min: </string>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="digitalcmaxSpinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="prefix">
<string>max: </string>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacerColorRangeDigital">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabColorRangeAnalog">
<attribute name="title">
<string>Analog</string>
</attribute>
<layout class="QHBoxLayout" name="horizontalLayoutColorRangeAnalog">
<item>
<widget class="QLabel" name="labelColorRangeValuesAnalog">
<property name="text">
<string>Color range values:</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="analogcminSpinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="prefix">
<string>min: </string>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="analogcmaxSpinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="prefix">
<string>max: </string>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacerColorRangeAnalog">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabColorRangeTransceiver">
<attribute name="title">
<string>Transceiver</string>
</attribute>
<layout class="QGridLayout" name="gridLayoutColorRangeCounters">
<item row="0" column="0" colspan="8">
<widget class="QLabel" name="labelColorRangeValuesTransceiver">
<property name="text">
<string>Color range values:</string>
</property>
</widget>
</item>
<item row="1" column="0" rowspan="2">
<widget class="QLabel" name="labelColorRangeCounter0">
<property name="text">
<string>Counter 0:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="transceivercminSpinBox0">
<property name="enabled">
<bool>false</bool>
</property>
<property name="prefix">
<string>min: </string>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDoubleSpinBox" name="transceivercmaxSpinBox0">
<property name="enabled">
<bool>false</bool>
</property>
<property name="prefix">
<string>max: </string>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="2" rowspan="2">
<widget class="QLabel" name="labelColorRangeCounter1">
<property name="text">
<string>Counter 1:</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QDoubleSpinBox" name="transceivercminSpinBox1">
<property name="enabled">
<bool>false</bool>
</property>
<property name="prefix">
<string>min: </string>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QDoubleSpinBox" name="transceivercmaxSpinBox1">
<property name="enabled">
<bool>false</bool>
</property>
<property name="prefix">
<string>max: </string>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="4" rowspan="2">
<widget class="QLabel" name="labelColorRangeCounter2">
<property name="text">
<string>Counter 2:</string>
</property>
</widget>
</item>
<item row="1" column="5">
<widget class="QDoubleSpinBox" name="transceivercminSpinBox2">
<property name="enabled">
<bool>false</bool>
</property>
<property name="prefix">
<string>min: </string>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
<item row="2" column="5">
<widget class="QDoubleSpinBox" name="transceivercmaxSpinBox2">
<property name="enabled">
<bool>false</bool>
</property>
<property name="prefix">
<string>max: </string>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="6" rowspan="2">
<widget class="QLabel" name="labelColorRangeCounter3">
<property name="text">
<string>Counter 3:</string>
</property>
</widget>
</item>
<item row="1" column="7">
<widget class="QDoubleSpinBox" name="transceivercminSpinBox3">
<property name="enabled">
<bool>false</bool>
</property>
<property name="prefix">
<string>min: </string>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
<item row="2" column="7">
<widget class="QDoubleSpinBox" name="transceivercmaxSpinBox3">
<property name="enabled">
<bool>false</bool>
</property>
<property name="prefix">
<string>max: </string>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="8" rowspan="2">
<spacer name="horizontalSpacerColorRange2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item row="0" column="6">
<widget class="QLabel" name="label_2">
<property name="maximumSize">
<size>
<width>50</width>
<height>31</height>
</size>
</property>
<property name="text">
<string>max:</string>
</property>
</widget>
</item>
<item row="0" column="7">
<widget class="QDoubleSpinBox" name="cmaxSpinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>31</height>
</size>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QLabel" name="label_3">
<property name="maximumSize">
<size>
<width>50</width>
<height>31</height>
</size>
</property>
<property name="text">
<string>min:</string>
</property>
</widget>
</item>
<item row="0" column="2">
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QFrame" name="frame_2">
<property name="geometry">
<rect>
<x>10</x>
<y>322</y>
<y>422</y>
<width>841</width>
<height>108</height>
</rect>
@@ -1541,9 +1734,9 @@
<resources/>
<connections/>
<buttongroups>
<buttongroup name="buttonGroup_4"/>
<buttongroup name="buttonGroup_2"/>
<buttongroup name="buttonGroup_3"/>
<buttongroup name="buttonGroup_4"/>
<buttongroup name="buttonGroup"/>
</buttongroups>
</ui>
+10 -5
View File
@@ -12,6 +12,15 @@ class Defines:
Acquisition_Tab_Index = 7
Max_Tabs = 9
max_counters = 4 # max number of counters
max_counters_tabIndex = 4
class ImageType(Enum):
DIGITAL = 0
ANALOG = 1
TRANSCEIVER = 2
class adc:
tabIndex = 5
count = 32
@@ -34,10 +43,6 @@ class Defines:
tabIndex = 6
loops_count = 6
class transceiver:
maxcount = 4
maxtabIndex = 4
class slowAdc:
tabIndex = 2
count = 8
@@ -88,6 +93,6 @@ class Defines:
LineStyles = ['-', '--', '-.', ':']
class colorRange(Enum):
all = 0
auto = 0
center = 1
fixed = 2
@@ -296,8 +296,8 @@ class jungfrauLGADStrixelsData : public slsDetectorData<uint16_t> {
/*nr_strixel*/ 2 * g1_nrows + 2 * g2_nrows + 2 * g3_nrows,
g1_ncols * (2 * g1_nrows + 2 * g2_nrows + 2 * g3_nrows) * 2 +
sizeof(header)) {
std::cout << "Jungfrau strixels 2X single chip with full module data "
<< std::endl;
// std::cout << "Jungfrau strixels 2X single chip with full module data "
// << std::endl;
// Fill all strixels with dummy values
for (int ix = 0; ix != g1_ncols; ++ix) {
@@ -307,9 +307,9 @@ class jungfrauLGADStrixelsData : public slsDetectorData<uint16_t> {
}
}
std::cout << "sizeofheader = " << sizeof(header) << std::endl;
std::cout << "Jungfrau strixels 2X single chip with full module data "
<< std::endl;
// std::cout << "sizeofheader = " << sizeof(header) << std::endl;
// std::cout << "Jungfrau strixels 2X single chip with full module data "
// << std::endl;
if (xmin < xmax && ymin < ymax) {
@@ -331,7 +331,7 @@ class jungfrauLGADStrixelsData : public slsDetectorData<uint16_t> {
}
iframe = 0;
std::cout << "data struct created" << std::endl;
// std::cout << "data struct created" << std::endl; // per frame: too noisy
};
/**
@@ -55,6 +55,13 @@ target_compile_definitions(jungfrauRawDataProcessStrxOldAldo PRIVATE JFSTRXOLD A
list(APPEND JUNGFRAU_EXECUTABLES jungfrauRawDataProcessStrxOldAldo)
# jf_online_display_gui (Qt5 + ROOT online display / event finder). Off by
# default: it needs Qt5 and ROOT, which the rest of this directory does not.
option(BUILD_JF_ONLINE_DISPLAY_GUI "build jf_online_display_gui" OFF)
if(BUILD_JF_ONLINE_DISPLAY_GUI)
add_subdirectory(jf_online_display_gui)
endif()
# others to be added if needed (might already be there in Makefile.cluster_finder TO BE CHECKED)
@@ -1 +0,0 @@
Makefile.zmqrootdisp
@@ -0,0 +1,22 @@
ROOTSYS=/opt/cern/v6/build
LIBZMQDIR = $(PWD)
LIBZMQ = -L$(LIBZMQDIR) -lzmq
SHLIB_PATH=/opt/cern/v6/root/lib
CMAKE_PREFIX_PATH=/opt/cern/v6/root
DYLD_LIBRARY_PATH=/opt/cern/v6/root/lib
QT_CXXFLAGS = $(shell pkg-config --cflags Qt5Core Qt5Gui Qt5Widgets 2>/dev/null)
QT_LIBS = $(shell pkg-config --libs Qt5Core Qt5Gui Qt5Widgets 2>/dev/null)
INCDIR= -I. -I../dataStructures ../tiffio/src/tiffIO.cpp -I../ -I../interpolations/ -I../../slsSupportLib/include/ -I../../slsReceiverSoftware/include/ -I../../libs/rapidjson/ -I../tiffio/include
LDFLAG= -L/usr/lib64/ -lpthread -lm -lstdc++ -lzmq -pthread -lrt -ltiff -O3 -std=c++17 -Wall -Wl,-rpath-link,$(ROOTSYS)/lib
default: onlinedisp_zmq
onlinedisp_zmq: onlinedisp_zmq.cpp onlinedisp_zmq.h ../dataStructures/jungfrauLGADStrixelsData_new.h
g++ -o onlinedisp_zmq onlinedisp_zmq.cpp $(QT_CXXFLAGS) $(QT_LIBS) -I. -I$(ROOTSYS)/include -Wall -g -lm -L. -lzmq -pthread -lrt -L$(ROOTSYS)/lib -lGui -lCore -lRIO -lNet -lHist -lGraf -lGraf3d -lGpad -lTree -lRint -lPostscript -lMatrix -lPhysics -lMathCore -lThread -m64 -L../../build/bin/ -lSlsSupportStatic ../../build/_deps/libzmq-build/lib/libzmq.a -lstdc++ -lsodium $(LDFLAG) $(INCDIR)
#-lCint
@@ -0,0 +1,16 @@
# build outputs (make and cmake)
cmake-build/
jf_online_display_gui
jf_online_display_gui_JFSTRX
gate_contig_test
*.o
*.JFSTRX.o
test/jf_feeder
test/xclick
# run artefacts
current_pede.dat
jfroi_*.json
*.json.lock
roi_list.dat
save_dest_list.dat
testPHmapsave/
@@ -0,0 +1,170 @@
# SPDX-License-Identifier: LGPL-3.0-or-other
# Copyright (C) 2021 Contributors to the SLS Detector Package
#
# jf_online_display_gui -- single multithreaded program merging
# jf_event_finder_cli / jf_event_finder_gui (ZMQ gate)
# onlinedisp_zmq (Qt5 + ROOT display)
#
# Standalone build (this directory):
# cmake -S . -B build && cmake --build build -j
# ./build/jf_online_display_gui (normal)
# ./build/jf_online_display_gui_JFSTRX (strip-xel)
#
# The Makefile in this directory builds the same two programs; this file exists
# so the program can be built like the rest of the package.
#
# Targets
# jf_online_display_gui normal geometry
# jf_online_display_gui_JFSTRX same sources with -DJFSTRX (strip-xel)
# gate_contig_test gate unit test (JF_BUILD_TESTS=ON, default)
# jf_feeder ZMQ test feeder (JF_BUILD_TESTS=ON, default)
#
# The two variants must never share object files: the geometry and the struct
# layouts differ, so they are compiled twice (JFSTRX/common object libraries).
cmake_minimum_required(VERSION 3.16)
project(jf_online_display_gui LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
endif()
option(JF_BUILD_TESTS "build the gate unit test and the ZMQ feeder" ON)
# ---------------------------------------------------------------------------
# paths relative to this directory (same layout as the Makefile)
# PKG = ../../.. package root
# CAL = ../.. slsDetectorCalibration
# ---------------------------------------------------------------------------
get_filename_component(PKG "${CMAKE_CURRENT_SOURCE_DIR}/../../.." ABSOLUTE)
get_filename_component(CAL "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)
# ---------------------------------------------------------------------------
# dependencies
# ---------------------------------------------------------------------------
find_package(Qt5 5.9 COMPONENTS Widgets Gui Core REQUIRED)
# online_jf.h includes Qt headers, and every translation unit of this program
# sees them (the core/root/h5 objects too), so the include paths are added to the
# flag list rather than left to the link interface.
set(JF_QT_INCLUDES
${Qt5Widgets_INCLUDE_DIRS} ${Qt5Gui_INCLUDE_DIRS} ${Qt5Core_INCLUDE_DIRS})
find_package(ROOT REQUIRED COMPONENTS Core RIO Hist Graf Gpad Thread)
# HDF5: CMake's own module finds both C and C++ (the C++ headers are not in the
# default include path on every distro).
find_package(HDF5 REQUIRED COMPONENTS C CXX)
# libzmq: prefer the prebuilt static library from the package build tree (that is
# what the Makefile links), then a system library as a fallback.
set(JF_ZMQ_STATIC "${PKG}/build/_deps/libzmq-build/lib/libzmq.a"
CACHE FILEPATH "static libzmq to link")
if(EXISTS "${JF_ZMQ_STATIC}")
set(JF_ZMQ_LIBRARY "${JF_ZMQ_STATIC}")
set(JF_ZMQ_INCLUDE "${PKG}/build/_deps/libzmq-src/include")
find_package(Threads REQUIRED) # the static build needs pthread/sodium
set(JF_ZMQ_EXTRA Threads::Threads sodium)
else()
find_package(PkgConfig REQUIRED)
pkg_check_modules(ZMQ REQUIRED IMPORTED_TARGET libzmq)
set(JF_ZMQ_LIBRARY PkgConfig::ZMQ)
set(JF_ZMQ_INCLUDE "")
set(JF_ZMQ_EXTRA "")
endif()
# ---------------------------------------------------------------------------
# includes / flags shared by every source of this program
# ---------------------------------------------------------------------------
set(JF_INCLUDES
"${CMAKE_CURRENT_SOURCE_DIR}"
"${CAL}/dataStructures"
"${CAL}/interpolations"
"${CAL}/tiffio/include"
"${PKG}/slsSupportLib/include"
"${PKG}/slsReceiverSoftware/include"
"${PKG}/libs/rapidjson"
"${JF_ZMQ_INCLUDE}"
${JF_QT_INCLUDES}
${HDF5_INCLUDE_DIRS}
${ROOT_INCLUDE_DIRS}
)
# The strip-xel decoder header is included by the -DJFSTRX build only; listing it
# as a dependency makes both build systems rebuild when it changes.
set(JF_STRIXEL_HDR "${CAL}/dataStructures/jungfrauLGADStrixelsData_new.h")
set(JF_SOURCES
online_jf_core.cpp
online_jf_root.cpp
online_jf_h5.cpp
)
set(JF_GUI_SOURCE online_jf_gui.cpp)
# Everything except the definition of JFSTRX: the two object libraries below
# differ only in that definition (and therefore in their compiled geometry).
set(JF_COMMON_WARN -Wall -O3 -pthread)
foreach(variant IN ITEMS "" "JFSTRX")
if(variant STREQUAL "")
set(objlib jf_common)
set(app jf_online_display_gui)
else()
set(objlib jf_common_jfstrx)
set(app jf_online_display_gui_JFSTRX)
endif()
add_library(${objlib} OBJECT ${JF_SOURCES})
target_include_directories(${objlib} PRIVATE ${JF_INCLUDES})
target_compile_options(${objlib} PRIVATE ${JF_COMMON_WARN})
if(NOT variant STREQUAL "")
target_compile_definitions(${objlib} PRIVATE ${variant})
endif()
set_source_files_properties(${JF_SOURCES} PROPERTIES
OBJECT_DEPENDS "${JF_STRIXEL_HDR}")
add_executable(${app} ${JF_GUI_SOURCE} $<TARGET_OBJECTS:${objlib}>)
target_include_directories(${app} PRIVATE ${JF_INCLUDES})
target_compile_options(${app} PRIVATE ${JF_COMMON_WARN})
if(NOT variant STREQUAL "")
target_compile_definitions(${app} PRIVATE ${variant})
endif()
target_link_libraries(${app} PRIVATE
Qt5::Widgets Qt5::Gui Qt5::Core
${ROOT_LIBRARIES}
${JF_ZMQ_LIBRARY}
${JF_ZMQ_EXTRA}
${HDF5_LIBRARIES}
pthread rt m
)
endforeach()
# ---------------------------------------------------------------------------
# tests / helpers
# ---------------------------------------------------------------------------
if(JF_BUILD_TESTS)
enable_testing()
add_test(NAME gate_contig COMMAND gate_contig_test)
# gate decision logic in isolation (no Qt, no ROOT usage at run time)
add_executable(gate_contig_test
test/gate_contig_test.cpp
$<TARGET_OBJECTS:jf_common>)
target_include_directories(gate_contig_test PRIVATE ${JF_INCLUDES})
target_compile_options(gate_contig_test PRIVATE ${JF_COMMON_WARN})
target_link_libraries(gate_contig_test PRIVATE
Qt5::Widgets Qt5::Gui Qt5::Core
${ROOT_LIBRARIES}
${JF_ZMQ_LIBRARY}
${JF_ZMQ_EXTRA}
${HDF5_LIBRARIES}
pthread rt m
)
# stand-in slsReceiver: single- or dual-interface ZMQ publisher
add_executable(jf_feeder test/jf_feeder.cpp)
target_include_directories(jf_feeder PRIVATE
"${PKG}/libs/rapidjson" "${JF_ZMQ_INCLUDE}")
target_compile_options(jf_feeder PRIVATE ${JF_COMMON_WARN})
target_link_libraries(jf_feeder PRIVATE ${JF_ZMQ_LIBRARY} ${JF_ZMQ_EXTRA})
endif()
@@ -0,0 +1,106 @@
# ===========================================================================
# jf_online_display_gui -- single multithreaded program merging
# jf_event_finder_cli / jf_event_finder_gui (ZMQ gate)
# onlinedisp_zmq (Qt5 + ROOT display)
#
# Usage:
# make -> ./jf_online_display_gui (normal)
# ./jf_online_display_gui_JFSTRX (strip-xel)
# make normal | make jfstrx single variant (aliases)
# ./jf_online_display_gui <rx_host> [rx_port] [gate_threshold] [min_pixels] [pede_file]
# [--pub PORT] [--contig] [--no-gate] [--pede-frames N]
#
# Deps: system ROOT (root-config), Qt5 (pkg-config), the prebuilt static
# libzmq from the package build tree, and rapidjson headers.
# ===========================================================================
QT_CXXFLAGS = $(shell pkg-config --cflags Qt5Core Qt5Gui Qt5Widgets 2>/dev/null)
QT_LIBS = $(shell pkg-config --libs Qt5Core Qt5Gui Qt5Widgets 2>/dev/null)
ROOT_CFLAGS = $(shell root-config --cflags 2>/dev/null)
ROOT_LIBS = $(shell root-config --libs 2>/dev/null)
# paths relative to this directory (jf_online_display_gui/)
PKG = ../../..
CAL = ../..
ZMQ_STATIC = $(PKG)/build/_deps/libzmq-build/lib/libzmq.a
# HDF5 C++ headers are NOT in the default include path on every distro (Debian/
# Ubuntu put them in /usr/include/hdf5/serial). Discover the include/library
# directories with pkg-config (the .pc may be "hdf5" or "hdf5-serial"), then the
# h5c++ wrapper, and let the user override both explicitly:
# make HDF5_INC=-I/usr/include/hdf5/serial \
# HDF5_LIB=-L/usr/lib/x86_64-linux-gnu/hdf5/serial
HDF5_PC := $(shell pkg-config --exists hdf5 && echo hdf5 || (pkg-config --exists hdf5-serial && echo hdf5-serial))
HDF5_INC ?= $(shell pkg-config --cflags $(HDF5_PC) 2>/dev/null) \
$(shell h5c++ -show 2>/dev/null | tr ' ' '\n' | grep '^-I' | tr '\n' ' ')
HDF5_LIB ?= $(shell pkg-config --libs $(HDF5_PC) 2>/dev/null) \
$(shell h5c++ -show 2>/dev/null | tr ' ' '\n' | grep '^-L' | tr '\n' ' ')
HDF5_LIBS = $(HDF5_LIB) -lhdf5_cpp -lhdf5
INCDIR = -I. \
$(HDF5_INC) \
-I$(CAL)/dataStructures \
-I$(CAL)/interpolations \
-I$(CAL)/tiffio/include \
-I$(PKG)/slsSupportLib/include \
-I$(PKG)/slsReceiverSoftware/include \
-I$(PKG)/libs/rapidjson \
-I$(PKG)/build/_deps/libzmq-src/include
CXX ?= g++
CXXFLAGS ?= -std=c++17 -O3 -Wall -pthread
LIBS = $(QT_LIBS) $(ROOT_LIBS) $(ZMQ_STATIC) $(HDF5_LIBS) -lsodium -lpthread -lrt -lm
OBJS = online_jf_core.o online_jf_root.o online_jf_gui.o online_jf_h5.o
# The strip-xel variant compiles the same sources with -DJFSTRX into its own
# objects: the two builds have different geometry and struct layouts, so they
# must never share object files (mixing them is a hard crash).
JFSTRX_OBJS = online_jf_core.JFSTRX.o online_jf_root.JFSTRX.o \
online_jf_gui.JFSTRX.o online_jf_h5.JFSTRX.o
BIN = jf_online_display_gui
BIN_JFSTRX = jf_online_display_gui_JFSTRX
ALL_CXXFLAGS = $(CXXFLAGS) $(ROOT_CFLAGS) $(QT_CXXFLAGS) $(INCDIR)
# `make` builds BOTH executables: the normal one and the LGAD strip-xel one
# (the decoder is header-only, no extra library).
all: $(BIN) $(BIN_JFSTRX)
# the strip-xel decoder header is header-only and lives outside this directory:
# touch it and the variant that uses it must be rebuilt
STRIXEL_HDR = $(CAL)/dataStructures/jungfrauLGADStrixelsData_new.h
%.JFSTRX.o: %.cpp online_jf.h $(STRIXEL_HDR)
$(CXX) $(ALL_CXXFLAGS) -DJFSTRX -c $< -o $@
%.o: %.cpp online_jf.h
$(CXX) $(ALL_CXXFLAGS) -c $< -o $@
gate_contig_test: test/gate_contig_test.cpp online_jf_core.o online_jf_root.o online_jf_h5.o
$(CXX) $(ALL_CXXFLAGS) -o $@ $^ $(LIBS)
$(BIN): $(OBJS)
$(CXX) -o $@ $(OBJS) $(LIBS)
$(BIN_JFSTRX): $(JFSTRX_OBJS)
$(CXX) -o $@ $(JFSTRX_OBJS) $(LIBS)
# kept so the old habits still work
normal: $(BIN)
jfstrx: $(BIN_JFSTRX)
# Synthetic slsReceiver stand-in used to exercise the program without
# hardware (see test/jf_feeder.cpp).
feeder: test/jf_feeder
test/jf_feeder: test/jf_feeder.cpp
$(CXX) -std=c++17 -O2 -pthread $(INCDIR) -o $@ $< $(ZMQ_STATIC) -lsodium -lpthread -lrt -lm
clean:
rm -f $(OBJS) $(JFSTRX_OBJS) $(BIN) $(BIN_JFSTRX) test/jf_feeder
FORCE:
.PHONY: all clean jfstrx normal feeder
@@ -0,0 +1,950 @@
# Notes
Working notes for `jf_online_display_gui`. The README is the reference for how the
program behaves; this file records provenance, verification and things that are
easy to trip over.
## 2026-09-23 — handoff from another session (verified here)
An all-night run showed the **display followed the pedestal drift but the saved
HDF5 did not**. Three changes were made by that session and are on disk; I
re-verified them in this workspace on 2026-09-23.
### What changed
| file | change |
|---|---|
| `online_jf_h5.cpp` | `RoiSaver::offer()` corrects each frame with the **live** pedestal (`PedestalStore::instance().get()`), not the snapshot taken by the GUI at Start Save. Periodic checkpoint: shrink `/data` + the 14 parameter datasets to exactly `framesWritten` (undoing the `kExtendBlock = 128` over-allocation), `H5Fflush(SCOPE_GLOBAL)`, rewrite the JSON. JSON writer factored into `writeJsonSidecar(bool complete)` writing `<name>.json.part` → `fsync` → `rename` → directory `fsync` (`syncDirOf`). File created as **v110 (SWMR)**: `H5Pset_libver_bounds(LATEST)` + `H5Fcreate`, all objects created up front, then `H5Fstart_swmr_write()` before the first frame. |
| `online_jf.h` | `StartInfo::checkpointMs = 30000` (0 disables) |
| `README.md` | pedestal tracking / saving / file format / JSON / crash-and-mid-run sections, test description, files table |
| `test/pede_live_test.cpp` | standalone check: live pedestal + checkpoint + SWMR live read |
Consequences of SWMR worth remembering: objects cannot be created after it
starts, so `/pedestal tracking` and `/pedestal tracking frame` are now created
(empty) for **every** save, and the baseline pedestal image is written just after
the SWMR switch. (The 10-minute cadence of that series, and its ROI-shaped
images, are unchanged.)
### Verification done here
* `make` — clean, binary current with the sources.
* `test/pede_live_test` (built with the README's line, plus `$(root-config --cflags)`
which that line omits) — **PASS**:
```
checkpoint1: periodic json (complete:false)=true live swmr read=true h5 extent=1 px=900.0
checkpoint2: live swmr read=true h5 extent=2 slice1 px=850.0
final: complete=true no .part left=true
PASS: live pedestal + periodic clean checkpoint
```
The two different pixel values are the point: the first frame was corrected with
pedestal A, the second with pedestal B installed mid-run, so the file follows the
tracked pedestal. Generated artefacts were deleted afterwards.
### Operational caveats
* **v110 needs HDF5 ≥ 1.10.** Older analysis installs cannot open these files at
all — this is the one to check before an overnight run.
* **Reading mid-run needs the SWMR read flag**: `h5py.File(path, "r",
libver="latest", swmr=True)` and `dset.refresh()` to see new frames.
`h5dump`/`h5ls` on the live file will not work. A closed file reads normally.
* `Pedestal corrected` is still **off by default** (raw data). Pedestal tracking
only reaches `/data` when it is ticked; in raw mode the only pedestal in the
file is slice 0.
* `/pedestal data` is the pedestal at save start; `/pedestal tracking`
(+ `/pedestal tracking frame`) is the history of what was actually used, one
image per checkpoint interval.
* `checkpointMs` defaults to 30 s and is **not exposed** in the GUI or on the
command line yet.
### Open / optional
* `--no-swmr` (or a `StartInfo` fallback) if a downstream reader turns out to be
on HDF5 < 1.10.
* Expose the checkpoint interval (`--checkpoint MS` and/or a GUI field).
* If an external reader does not see new frames after a checkpoint, add explicit
per-dataset `H5Dflush`; `H5Fflush(SCOPE_GLOBAL)` was enough in the test.
* Unrelated: the older `onlinedisp_zmq.cpp` extracts the ROI from
`zHeader.detSpec1`, which is a hack (that field is the bunch id) — the stream
header already carries `zHeader.rx_roi` (`std::array<int,4>`, filled from the
receiver's per-port ROI). That file was not touched.
## Workspace health
* `../_AGENT.txt` was rewritten (2026-09-23, on request): it now points at
`jf_online_display_gui/` as the active work, with the old `onlinedisp_zmq` +
`jf_event_finder_*` set kept in a clearly marked "legacy / superseded"
section. The design rules that both generations share (ROOT-thread rule,
photon map, display cadence, gate semantics, SWMR files) are listed there too.
* `test/_run/` was removed (2026-09-23, on request). For the record, what was
in it was not worth keeping: the `..._084120_...` run had only its sidecar left
(the HDF5 was already gone) and `..._090709_...` had `frames.saved = 0` — an
empty save written before the handoff, with no `complete` field. `test/` now
holds only `jf_feeder[.cpp]` and `pede_live_test.cpp`.
* `jf_online_display_gui/` and the h5/write path are untracked in git; nothing here is
committed.
* `.gitignore` in this directory excludes `*.o`, the `jf_online_display_gui` and
`test/jf_feeder` binaries, and runtime data (`current_pede.dat`,
`roi_list.dat`, `save_dest_list.dat`, `jfroi_*.h5/json`). Note it does *not*
cover `test/pede_live_test` (the test binary), so that one should be deleted
after use.
## 2026-09-30 — JFSTRX display + startup defaults
* `-DJFSTRX`: the **ADC and gain windows are now unmapped** (raw 1024x512), so
gate-ROI selection and the hover read-out stay in raw coordinates. The mapped
view is a separate, display-only window, **"ADC Strixel Panels (JFSTRX)"**,
with the six groups arranged like the ROOT canvas (left chip1 g3/g2/g1, right
chip6 g1/g2/g3).
* The six panels use the strixel **pixel aspect** `X = 75*(75/Y)`, `Y =
25/15/18.75 um` -> 9:1, 25:1, 16:1, via `FastImageView::setPixelAspectX`
(non-uniform x/y scaling; ADC and gain stay square).
* The receiver ROI is parsed from the JSON `rx_roi` array (the old `detSpec1`
hack is gone) and drawn as the **red** rectangle on the raw ADC image.
* Raw geometry is now `kRawWidth`/`kRawHeight` (1024x512); `nx`/`ny` under
JFSTRX are the strixel *display* geometry only. Fixed the assembled-frame
height, the multi-interface slab split, the gate-ROI defaults and the HDF5
pedestal extraction, which all wrongly used the mapped `nx`/`ny`.
* **Startup defaults changed**: the signal gate is **off** (`--gate` enables it)
and pedestal subtraction is **off** (`--use-pede` enables it; the "No pede"
button starts checked). README examples now pass `--gate` where gating is
expected.
* `test/panel_map_test.cpp` (JFSTRX objects) checks the six-panel fill geometry
against the decoder: all six panels fully covered, only the decoder's 183
extra right-edge columns dropped (same as the ROOT histograms).
* The strixel ROI-outline experiment (`buildReceiverRoiOutline`) was removed -
ROI on the mapped panels was judged too complex.
* At ~14:40 the on-disk `jf_online_display_gui` was found truncated to 0 bytes
(concurrent build in this directory); `make jfstrx` rebuilt it (14:41).
## 2026-09-30 (later) — three reported bugs
1. **Zoom-out stopped early** (`FastImageView::wheelEvent`): it called
`matchViewportToWindow()` on every wheel step, so the image dimension that
reached its limit first froze the other and the whole frame could never be
shown. Outward zoom now only `clampViewport()`s; the window-shape matching
still runs while zooming in and on resize.
2. **ROI PH (A7) empty**: the fill condition used `gate_.roiActive()`, which is
only true while "ROI only" is on, so a merely *selected* ROI left A7 empty.
The ROI snapshot/spectra now use `gate_.roiSet()`; "ROI only" still controls
the gate restriction (unrelated to showing the ROI).
3. **"Reset His" took 30-50 s**: the ROOT thread executed the reset and blocked
on `gHistoMtx`, which the analysis thread holds around every frame - under
the heavy JFSTRX fill it can starve. The reset is now performed by the
analysis thread at the top of its next frame (lock already held), with a
non-blocking `try_lock` fallback on the ROOT thread for the idle case. The
ROOT thread also now wakes immediately (`gCmdCv`) for commands and forced
redraws instead of waiting for the `ROOT update (ms)` tick, so a large
update interval no longer delays a button.
**Build footgun:** the Makefile does not track flags. After `make jfstrx`,
a plain `make` only rebuilds changed files and then links a mix of JFSTRX and
non-JFSTRX objects (I hit a link error that way). Always `make clean` before
switching between `make` and `make jfstrx`.
## HDF5 C++ headers (build dependency)
`#include "H5Cpp.h"` needs the HDF5 **C++ dev** package, not just the runtime:
| distro | package | where the header lands |
|---|---|---|
| RHEL / Rocky / Alma / Fedora | `hdf5-devel` | `/usr/include/H5Cpp.h` |
| Debian / Ubuntu | `libhdf5-dev` | `/usr/include/hdf5/serial/H5Cpp.h` |
| openSUSE / SLES | `hdf5-devel` | default include dir |
| Arch | `hdf5` | `/usr/include` |
| conda-forge | `hdf5` | `$CONDA_PREFIX/include` |
| macOS (brew) | `hdf5` | `$(brew --prefix hdf5)/include` |
The Makefile now discovers the include/library dirs via `pkg-config` (`hdf5` or
`hdf5-serial`) and `h5c++ -show`, and both can be overridden, e.g. on Ubuntu:
```
make HDF5_INC=-I/usr/include/hdf5/serial \
HDF5_LIB=-L/usr/lib/x86_64-linux-gnu/hdf5/serial
```
## 2026-10-06 — external review (`../../../REVIEW_CLAUDE.md`): triage and what was applied
A 39-item review by Claude covers this program plus the Qt6 port, the legacy
tools, `test_bench/` and repo hygiene. **Part of it was written against a
different checkout**: items 2, 3, 4 (mostly) and 9 are already fixed in *this*
tree, and the review's own item 37 mentions `build-qt6/` cache paths under
`~/swtests/slsDetectorPackage/...`, which does not exist here. Everything below
was checked against the code in this workspace before touching it.
### Applied and verified here
| item | what was wrong | fix |
|---|---|---|
| 4 (rest) | a header with **no data part** would make the data `recv` swallow the next message's header as pixels | require `ZMQ_RCVMORE` after a valid header, else count an error and drop the header (the header-length clamp and the drain-on-reject paths were already present) |
| 8 | the save recorded the **startup** gate settings, so `gate` in the HDF5 attribute and JSON sidecar was wrong after any live edit | read `enabled/threshold/minPixels/contiguous` from `GateEngine` (new getters) instead of `g_cfg`; verified end to end - a save started from the command line records the live values (`threshold 137`, `min_pixels 9`) |
| 11 (part) | the acq thread **busy-spun at 100 % CPU** waiting for a lagging partner: a held slab kept its socket polled and level-triggered `zmq_poll` fired forever | clear `events` for a socket whose slab is held, so it is not polled until it is released |
| 18 | ten shortcuts never fired: `QKeySequence("s") == QKeySequence("S")`, so s/S, b/B, r/R, o/O, p/P were ambiguous and Qt activated neither | upper-case actions are now `Shift+S/B/R/O/P` (verified: `Shift+S` opens Set Ranges, 352 → 366 windows) |
| 19 | the run examples still passed the removed `<energy>` argument, so `2000` was parsed as the host | README examples, the header comment and the Makefile comment corrected (the two remaining `<energy>` mentions are the *legacy* chain and a historical note, both correct) |
| 22 (part) | `--pede-tracking` left the button unchecked, and `--gate-roi` left `ROI only` unchecked so the first press toggled the wrong way | the tracking button is initialised from `gPedeTracking`, and a restricting `--gate-roi` now checks `ROI only` (through the `m_applyingRoi` guard, so nothing re-captures or warns) |
### Found while fixing item 22 — a bug of my own
The tracking button's enable rule lived in the 500 ms `updateFlow()` poll, which
also *unchecked* the button when the gate was off. During start-up that cleared
a `--pede-tracking` button (`ptrack=off`, reproduced headless). The force-off is
now event-driven (in the gate checkbox's own handler); the poll only enables.
After the change: `--pede-tracking` gives `ptrack=on`.
`test/jf_feeder` had also gone missing from the tree (the Makefile has no target
for the test programs — review item 34); rebuilt by hand for the verification.
### Already fixed in this tree (nothing to do)
| item | state |
|---|---|
| 2 (HDF5 never closed) | the file is built with the C++ constructor `H5File(h5Path, …)` so it owns the only reference; the code comments say so explicitly |
| 3 (budget/stop not enforced) | `offer()` returns early on `stopRequested`, and the writer stops at `saved >= maxFrames` |
| 4 (clamps) | `hn > hdr.size()` clamp, `truncated` data check and the drain-on-reject paths are all present |
| 9 (cumulative drop rate) | uses the 1 Hz window (`pedeTrackSignalDropRate(passCur, dropCur, …)`) — fixed earlier the same day |
### Checked and found *correct* (not defects)
While verifying item 8 I chased two values in a saved file that looked wrong and
are not: the file recorded `enabled: false` and `no_pede: true` for a run whose
command line had neither `--gate` nor `--use-pede`. Both are the documented
defaults - `online_jf.h:189` (`gateEnabled = false`) with `--gate` to enable, and
`gNoPede{true}` with `--use-pede` to clear. The review's item 22 is the flip
side of this (the widgets were not synced from the config); the config itself is
intentional. Since it is easy to lose an hour to this, both switches are now
spelled out in the README's options section, and a regression run with
`--gate --use-pede` behaves as expected (with a 100 ADU pedestal and a 250 ADU
threshold on 3000/200 ADU frames: `pass=0 drop=148`, i.e. the ROI half is
reduced to 100 and correctly rejected).
### Deliberately deferred, with reasons
* **13 — chip/ROI spectra saturate (top physics item).** `TH1I` bin contents
saturate at `INT_MAX` and the `uint32_t` accumulators wrap: ~55 k pixels per
chip per frame land in 1–2 noise bins, so at 500–1300 fps the spectra are
wrong within an hour, and an all-night run at 25 fps is close to the limit too.
The fix is mechanical but wide (37 `TH1I` sites across four files plus the
accumulator and sync signatures) and touches the core data path, so it gets
its own careful pass with a test rather than a rushed edit at the end of a
long session. Plan: accumulators → `uint64_t`, histograms/clones → `TH1D`,
then check that entries, means and the ADU/keV ratio (40.0000) are unchanged.
* **10** (the tracker can overwrite a just-loaded pedestal — re-prime from the
store under the lock, skip a stale install), **12** (per-port `rx_roi` with the
`rowBase` offset), **20** (closing the window should quit — the parentless ADC
/Gain windows keep the process alive), **21** (Delete in the ROI/save-destination
dialogs uses stale indices), **23–30** (races on `g_pheneThreshold`/the ROI,
`renderImage`'s 524 k `setPixel` calls and the 500 ms `roi_list.dat` re-read,
NaN pedestals, rapidjson asserts, the finish/start save race, frame 0, and the
small GUI defects).
* **11 (the rest)** — partial-frame assembly is a feature, not a fix; the README
now documents the real behaviour and calls the promise an open item.
* **Everything outside `jf_online_display_gui`** — the Qt6 port (1, 16, 17, 35, 36), the
legacy tools (5, 6, 7, 14, 15, 31 — the project's own `_AGENT.txt` marks them
superseded), `test_bench` (32), repo hygiene (33, 37, 38 — `git checkout --`
for the symlinks needs repository write access, which is still deferred) and
maintainability (39). These need decisions of their own; none of them were
changed here.
## 2026-10-14 — virtual photon counting: display side done, save side specified
Requested: take the photon map out of ROOT, give it its own Qt window filled the
same way but **not cumulative by default** and plotted every N images (default
2000), put those controls plus the photon threshold in a "Virtual photon
counting" panel left of the Display, and add a save mode that writes the
non-cumulative maps every M images (M >= 200) with the first frame number and
timestamp, M and the thresholds recorded.
### Done (build green, window present, no crashes)
* Core: `gPmapEvery` (default 2000) and `gPmapCumulative` (default **false**)
atomics; the photon map is published into the image mailbox every
`gPmapEvery` **analysed frames** (independent of the ADC image decimation),
and when not cumulative the accumulator is zeroed right after the publish, so
each image holds the photons counted since the previous one. The counting
itself is unchanged.
* GUI: a **"Virtual photon counting"** panel (left column, under the gates -
the Display panel is in the right column at that height) with the
`cumulative` checkbox, `plot every N images` (1..1000000, default 2000) and
the **photon threshold (ADU)** spin moved out of the Display group.
* GUI: a new top-level window **"Photon Map (virtual photon counting)"**
(1104x552, same colour palette as the ADC image, colour range 0..8 photons
per pixel), refreshed from the mailbox in `processImages()`.
* Fixed while doing it: the panel was built before `photonThrSpin_` existed, so
the grid received a null widget and the spin floated at (0,0) - the same
add-before-create mistake as the Quit button earlier. The panel is now
created after the spin and inserted with `leftCol->insertWidget(2, ...)`.
### Stage 3, part A — done and verified
* **The ROOT photon map is gone**: no `his3000` booking, no `disp3000` clone and
no `A5` canvas. All the remaining references are behind `if (his3000)` /
`if (disp3000)` / `if (A5)` guards, so they are simply inert; the two
unguarded dereferences (the clone and the z-range) were the only ones that
needed deleting. Verified: no `Plotting Canvas Photon Map` window appears.
* The **P.Map** range pair now reads and writes the Qt photon window's colour
range (`liveMapRange` -> `m_photonView->colorRange`, and the panel's opening
range), and the ADC zoom also sets the photon window's viewport.
* **Save with no gate ROI works**: the early refusal in `toggleRoiSave` is gone;
`roiSnapshot()` already reports the full frame when no ROI is set. Verified:
with no `--gate-roi`, `/data` came out `(151, 512, 1024)` - the pedestal slice
plus 150 full frames.
### Stage 3, part B — the photon-map save mode (next)
Design, so this can be written mechanically:
1. Save panel: a radio pair (`save frames` / `virtual photon counting`) plus an
`M` spinbox, minimum **200**, default 2000; the spinbox is only meaningful in
the photon mode.
2. Core: `gPmapSaveEvery` (int, default 2000) and `gSaveMode` (0 = frames,
1 = photon maps) atomics; a **second accumulator** `g_pmapSaveAccum` (NCH
floats) filled in the same worker pass (one extra add per counted photon
pixel, gated by a `FrameJob::pmapSave` flag so there is no atomic read per
pixel); every `gPmapSaveEvery` analysed frames it is copied into a single-slot
mailbox and zeroed - never cumulative.
3. Window provenance: the acq thread owns the saver, so let the acq loop keep the
window bookkeeping while in this mode: on the first gate-passed frame after a
publish record `firstFrame = hdr.frameNumber` and `firstStamp =
hdr.timestamp`, count frames, and when the analysis publishes a map, tag it
with those two values. The map's own frame count goes in too.
4. Writer: a new dataset `/virtual photon counting` (float32, 512x1024, one image
per publish, extendable) plus `/virtual photon counting frame` (uint64) and
`/virtual photon counting timestamp`; JSON gains `save_mode`,
`photon_map_every` (M), `photon_threshold` (ADU) and the window count.
`/data` stays frames-only and is not created in this mode.
5. `--pmap-every N` on the command line would make this testable without the GUI
(the display cadence is otherwise 2000 frames = 80 s at 25 fps).
### Stage 3, part B — plumbing in place, inert
Done in the tree and building:
* `gSaveMode` (0 = frames, 1 = virtual photon counting) and `gPmapSaveEvery`
(default 2000) atomics, plus `pmapSaveReset()`.
* A **second accumulator** `g_pmapSaveAccum` filled in the same worker pass
(`FrameJob::pmapSave`, so no atomic read per pixel), published into a
single-slot `PmapSlot` every `gPmapSaveEvery` analysed frames and zeroed
immediately - never cumulative.
* `Pipeline::offerToSaver()` replaces the two direct `saver_.offer()` calls: in
the frame mode it is the old path, in the photon mode it keeps the window
bookkeeping (first frame number, first timestamp, frame count) in the acq
thread and tags each published map with it.
* `RoiSaver::offerPhotonMap()` declared; its body currently only counts the map
in `pmapPending` and writes nothing - **deliberately inert**: `gSaveMode` is
still 0 everywhere, so no path can select the photon mode yet and the existing
behaviour is unchanged. Nothing can silently produce a frames-only file while
claiming the photon mode.
Still to write (the design is above):
1. The writer body: `/virtual photon counting` (float32, 512x1024, extendable,
**created up front** because SWMR forbids creating objects after the start)
plus `/virtual photon counting frame` and `/virtual photon counting timestamp`
(uint64), drained from a `pmapQueue` in the writer loop, and the JSON fields
(`save_mode`, `photon_map_every`, `photon_threshold`, window count).
2. The Save panel controls: the radio pair (`save frames` /
`virtual photon counting`) and the `M` spinbox (minimum 200), filling
`StartInfo::photonMode/photonEvery/photonThreshold` (the fields are already
there) and calling `pmapSaveReset()` when a save starts.
3. `--pmap-every N` and `--pmap-save-every N` (or one `--save-photon` switch) so
the mode can be exercised headlessly; with the display default of 2000 frames
a test needs 80 s at 25 fps otherwise.
### Stage 3, part B — finished and verified (this session)
Everything listed as "still to write" above is in the tree, plus the GUI
controls and the headless switches:
* Writer: `/virtual photon counting` (float32, {nmaps, h, w}, chunked, created
before SWMR starts) + `/virtual photon counting frame` + `/virtual photon
counting timestamp` (U64, one value per map), drained from `Impl::pmapQueue`
in the writer loop; `RoiSaver::offerPhotonMap()` is live and inert unless
`info.photonMode` and a save is running. JSON: `save_mode`,
`photon_map_every`, `photon_threshold`, and a `photon_maps` block with
`saved`, `every`, `threshold_adu`, `cumulative: false`, `pedestal_active`,
`dataset`. In photon mode `frames.saved` reports the **maps**.
* Budget: in photon mode "Number of frames to be saved" budgets the **maps**
(the GUI relabels the row to "Number of photon maps to be saved"), and the
run stops by itself with `stop_reason = "photon-map budget reached"`, leaving
a closed `complete: true` pair. Frame mode is unchanged
(`frame budget reached`).
* Save panel: radio pair + `photon map every N frames` spinbox (minimum 200,
greyed while "save frames" is selected). The radio also re-labels the budget
row. The photon-map mode is allowed with **no gate ROI** (full frame).
* Virtual photon counting panel (left column, under the gates): `cumulative`
(default **off**), `plot every N images` (default 2000), `photon threshold
(ADU)` (moved out of the Display panel). The map lives in its own Qt window
`Photon Map (virtual photon counting)`; it is **not** a ROOT histogram any
more (that was the old `his3000`, removed).
* CLI: `--pmap-every N`, `--pmap-save-every N` (min 200), `--save-photon`,
`--save-budget N`, `--start-save` — the same actions as the GUI controls, so
a save can run headless.
**Bug found and fixed while verifying (important).** Step 5a of the analysis
loop (the save-map publish) took `gHistoMtx` although the enclosing frame scope
already holds it (`histoLock`, top of the frame). `std::mutex` is not
recursive, so the **analysis thread deadlocked on the first frame** whenever
`gSaveMode == 1` — with `--save-photon`, and even with no save running. The
symptom was a program that kept counting frames (`in=` growing, because the acq
thread does not depend on the analysis thread) while nothing downstream ever
appeared: no photon map, no ROOT update, `save=0/0`. Two `gdb` snapshots
caught it (`analysisLoop` parked in `pthread_mutex_lock` at a call site that
only exists in the photon path). Fix: no second lock (the frame scope holds it)
and the whole save-map path is now gated on `saver_.running()` as well, so
`gSaveMode == 1` without a running save costs nothing and cannot block.
**Verified end to end** (Xvfb + mwm, `test/jf_feeder`, zero-filled pedestal —
the real `current_pede.dat` is ~2000 ADU, so the synthetic 100/300 ADU frames
would count nothing):
| check | result |
|---|---|
| `--save-photon --pmap-save-every 200 --start-save`, 40 s | maps at frames 0/201/401/601/801, `[pmap] map N written (200 frames, first frame F)` |
| `/virtual photon counting` | (5, 512, 1024) float32; every map 40 non-zero pixels, sum ~5320 (40 px x 133 counts = 2/3 of 200 frames), identical maps = non-cumulative |
| provenance | `/virtual photon counting frame` `[0 201 401 601 801]` |
| `--save-budget 2` | exactly 2 maps, self-stop `photon-map budget reached`, `complete: true` |
| frame mode `--save-budget 100` | `/data` (101, 512, 1024), 100 frames, `frame budget reached`, no photon datasets |
| GUI path | clicking `virtual photon counting` re-labels the row to *Number of photon maps to be saved*, enables the M spinbox and sets `Status: saving virtual photon counting maps`; `Start/Stop save` then writes maps (first map at the frame the save started, e.g. 431) |
| photon window | `Photon Map (virtual photon counting)` renders the 40-pixel cluster (green, ~133 counts on the 0..256 scale) |
| no pedestal | `--save-photon` with `--no-pede` (or no pedestal file) is **refused**: `[save] refusing virtual photon counting: "No pede" is on, ...`, no HDF5 file is created, and the scripted `--start-save` path is not blocked by a dialog; frame mode with no pedestal still saves |
The photon-map window draws **zero-count pixels white** (`FastImageView::
setZeroWhite`, opt-in and used only there), reproducing the ROOT `colz` look
where empty cells show the canvas background; the palette and the colour bar are
untouched, and single photons are still coloured (the special case is `v <= 0`,
not "below the range"). Verified on screen: white map, cluster still green.
Housekeeping: `test/_scratch/` (XTest driver, screenshots, run logs, HDF5
outputs, the synthetic pedestal) was removed after the runs; the tree keeps only
`test/jf_feeder[.cpp]`, `test/pede_live_test.cpp`, `test/panel_map_test.cpp`.
Two things left open for the next session, both decisions rather than bugs:
1. ~~**Photon counting needs a pedestal.**~~ **Decided by the operator: no
photon-map saving without a pedestal.** Counting still requires `g0`
(unchanged), and a virtual-photon-counting save now *refuses to start* when
no pedestal is active (`No pede`, or nothing loaded): the button drops back,
the status line and stdout say why, no file is created, and the modal warning
is skipped under `--start-save` so scripted runs never wait on a dialog.
Frame saving is unaffected.
2. The map's `first frame number`/`timestamp` come from the acq thread, i.e.
from the first **gate-passed** frame of the window (none is available if the
gate drops the whole window, in which case the previous values persist). The
analysed-frame count in `PmapItem::frames` is exact.
### ROI in photon mode, greying convention, control names (follow-up)
* **The gate ROI applies to the saved photon maps too** (operator's choice):
a map is the ROI window of the full-frame counting map, cropped in the same
data coordinates as `/data`. This fixed a real bug: `writePhotonMap` used to
hand `h*w` floats straight to HDF5, i.e. the first `h*w` pixels of the
1024x512 map, which is the *wrong region* whenever a ROI is set. Verified
against the full-frame map: `/virtual photon counting` for a `0 0 100 50` ROI
is bit-identical to `full[0:50, 0:100]`, with the 16 non-zero pixels exactly
where the feeder's wrapped cluster is (row 1, x 0..15); a frame save with the
same ROI gives `/data` `{nframes, 50, 100}`, so both modes crop alike.
* **Greying is a look, not an enablement** (`greyStage(QGroupBox*, bool,
std::initializer_list<QWidget*> keepActive)`): the box, its title and its
other contents dim, while the widget that *switches the stage back on* keeps
normal text - otherwise a bypassed stage looks stuck. Kept readable:
`Pede/Raw toggle` (Pedestal), `Gate enabled` (signal gate), `Pedestal
tracking`, `gain correction on/off` (Gain), and the storage-cell combo. The
`keV PH` checkbox stays disabled+greyed while the gain is off (earlier
request: it keeps its tick for next time).
* **Virtual photon counting box greys without a pedestal** (`greyStage(
pmapGroup_, !pede)`), matching the fact that the counting needs `g0` and the
save refuses to start: one rule, the panel and the file agree.
* Renames: `No pede (raw ADC)` -> **`Pede/Raw toggle`** (state still spelled out
by the label next to it), `Apply gain (g)` -> **`gain correction on/off (g)`**,
plus a read-only **`gain corrected`** indicator beside it (no mouse events, no
focus; it ticks with the flag in `updateFlow()` and dims with the panel).
* Pedestal tracking panel follow-up: the button is `pedestal tracking on/off`
and the box now greys on **selectability**, not on activity - `greyStage(
pedeTrackGroup_, !gateOn)`, matching the button's own `setEnabled(gateOn)`,
while the value label under it (`off` / `on: N samples, ...`) reports the
activation. Verified in three states: gate off -> box and button greyed;
gate on + tracking off -> box black and label `off`; after clicking the
toggle -> button pressed and label `on: 0 samples, delta 0.0 ADU, 0 updates`
(0 samples is correct with the feeder, which only drops a third of the frames
while the tracker needs >90 %).
### Photon threshold in keV while the gain correction is on
Requested: leave the photon-counting threshold in ADU with the gain off, switch
it to keV (default **4 keV**) with the gain on, and remember both values.
* Core: `g_pheneThresholdKev` (default `PHOTON_THRESHOLD_KEV_DEFAULT` 4.0) next
to `g_pheneThreshold` (150 ADU). The analysis picks by `gain_flag` and hands
the worker `FrameJob::pcGain` / `pcNominalGain`, so the counting value is
`adcpedecorr / (g0[i] or 40)` - the same ADU/keV the keV spectra use - and the
increment follows in the same unit: `(value + thr) / (2 x thr)`. The unit
follows the gain stage itself, not the `keV PH` checkbox (that one only
decides what the spectra display).
* GUI: the threshold row is a `QDoubleSpinBox` (integer ADU, one decimal keV)
whose label switches `(ADU)` <-> `(keV)`; `setApplyGain()` stores the value
that is on screen into its own unit and loads the other one (signals blocked),
so toggling never destroys either. `--apply-gain` added as the CLI mirror of
the Gain button (the gain stage was the only one without one) - needed to test
this headlessly.
* Reported: `--stats` prints `pthr=150ADU` / `pthr=4keV`; the JSON carries
`photon_threshold_unit` and `photon_maps.threshold` + `threshold_unit`
(`threshold_adu` is gone - the key was wrong as soon as the unit could
change).
* Verified with the synthetic feeder, a zero pedestal and a **g0 = 1.0
ADU/keV** gain map (float64 - the loader wants 6 x 1024x512 doubles; a float32
file is rejected with the byte count):
- gain off: `pthr=150ADU`, one 200-frame map with **40 non-zero pixels, sum
5320, max 133** - i.e. only the 300 ADU cluster, unchanged from before;
- `--apply-gain`: `pthr=4keV`, the **same frames** give **524288 non-zero
pixels** (every pixel: keV = ADC/1.0, so even the 100 ADU baseline is
100 keV > 4 keV) with max 5925 - proof that the counting really divides by
ADU/keV before comparing;
- GUI: starting with `--apply-gain` shows `photon threshold (keV): 4.0`;
clicking the gain off shows `photon threshold (ADU): 150`; clicking it back
on gives a screenshot byte-identical to the first keV one, so both values
survive the round trip.
* Mid-save guard: toggling the gain while a photon-map save runs changes the
unit of the maps written after it, so `setApplyGain()` prints
`[gain] warning: the photon-counting unit is now keV while a photon-map save
is running ...` (and the same for ADU). Verified with a photon save started
by `--start-save` and a gain click: the warning appears once, the save keeps
running and writes its map, the JSON keeps the start-of-save threshold/unit.
### Window handling, labels and the flow arrows
* **Quit closes every window.** The ADC / gain / photon-map windows are
separate top-levels and the ROOT canvases live in the ROOT thread, so
`quitApp()` and `closeEvent()` now call `closeSideWindows()` (hide + close
each) and `QApplication::quit()`. Closing only the control window used to
leave the side windows - and the process, because Qt quits on the *last*
window - on screen. Verified on all three paths with the window count taken
from `xwininfo -root -tree`:
- Quit button, dialog confirmed: 6 app windows -> **0**, process gone;
- Quit button with a photon-map save running: the save is flushed
(`[shutdown] flushing the ROI save ...`, JSON `complete: true`), then
**0** windows / no process;
- window-manager close (a `WM_DELETE_WINDOW` ClientMessage, the path that was
broken): confirmed -> **0** windows / no process.
* Gain panel label: the no-map case is now
`no gain map loaded : using defaults` (the "defaults" are the nominal
40 ADU/keV, used for the keV spectra *and* the keV photon threshold).
* Flow arrows: two new connectors - **Gain -> Virtual photon counting** (the
calibration; its threshold is keV while the gain is on) and **Virtual photon
counting -> Display** (the map has a window of its own) - and the save path is
now **orange**: the Gain -> ROI rail and the ROI -> Save arrow. Blue stays
the analysis/display chain. `FlowStrip::Path` gained a `save` flag and
`makeFlowArrow()` a colour argument (`#4673af` blue, `#de7d14` orange).
Verified on screen in the gain-on/pede-on state: both new arrows land on the
right panels, the rail and the ROI -> Save arrow are orange, the rest blue.
* The `JF_TIMING` `[rect]` diagnostic also prints `quit`, `track` and `pmap`
now, which is how the click-based tests above locate those widgets.
### Dynamic flow colours (arrows static, colours not)
Reworked after the operator's feedback: the geometry of the diagram is fixed,
only the colour class of each connector changes.
* Three classes (`FlowClass` + `flowColor()`): `FlowData` orange `#de7d14`,
`FlowNormal` light blue `#5b9bd5`, `FlowPedestal` lighter blue `#a8c8e8`.
* The **data path follows the save mode**: `FlowStrip::setSaveMode(photonMaps)`
flips the Gain -> ROI rail and the Gain -> counting connector, and
`updateFlow()` (called from the radio handler) flips the two label arrows
ROI -> Save and counting -> Save. So in `save frames` the orange pair is
rail + ROI -> Save, and in `virtual photon counting` it is Gain -> counting +
counting -> Save - exactly two arrows at a time.
* Completeness: every stage now has an incoming and an outgoing connector.
The missing one was **Virtual photon counting -> Save** (a new arrow under the
counting panel, the left-hand twin of ROI -> Save). Display and Save are the
sinks; the storage cell has no incoming connector because the receiver stream
enters there.
* The colour legend is in the flow strip's tooltip and in the README table.
* Verified on screen: frames mode -> rail + ROI -> Save orange, counting arrows
light blue; after clicking `virtual photon counting` (and the label switching
to "Number of photon maps to be saved") -> Gain -> counting and counting ->
Save orange, rail + ROI -> Save light blue. `[save] mode: ...` is logged on
every change.
* Test note: the `JF_TIMING` `[rect]` lines can be ~30 px stale (they are printed
on a layout pass that may run before the panels settle), so a scripted click
should target the widget with a small downward tolerance - found the hard way
when a radio click missed.
### The gate is a funnel on the line (not a stage)
Operator's rule: the signal gate does not modify the data, so no connector may
enter or leave its box. Reworked accordingly:
* The flow is now `Pedestal -> [funnel] -> Gain` (and `[funnel] -> tracking`),
i.e. one junction where the gate acts, drawn as a small trapezoid on the line.
The gate panel's only connector is a short blue **control arrow** into the
funnel's side; with the gate off the funnel and that arrow are drawn pale
(`FlowOff`, #c9c9c9) instead of disappearing.
* Two feeders reach the funnel and both are always drawn: through the **Pedestal**
(`FlowPedestal`) and straight from the **storage cell** (`FlowNormal`), the
raw bypass that avoids the greyed Pedestal box. `setStages(pede, gate, pmap)`
makes the live one coloured and the idle one pale grey - so in raw mode the
data visibly comes from block 1, as the operator asked.
* `FlowStrip::Path` grew `x` (vertical position), `head` (false = merge into the
junction) and `startAtJog` (begin on the vertical, for the funnel -> Gain and
funnel -> tracking branches); `cross()` honours them; `drawFunnel()` paints the
symbol plus its control arrow; `kFunnelX = 24` is where the flow passes the
gate.
* `updateFlow()` calls `setStages(pede, gateOn, gSaveMode == 1)`, so the four
states (pede/raw x gate on/off) are all live: verified with screenshots in all
four combinations - pedestal feeder live + funnel blue (pede+gate), funnel and
control arrow pale (pede+no gate), bypass live and pedestal feeder pale
(raw+gate), and the bypass stays live with the funnel pale (raw+no gate).
### Flow diagram, final pass (operator's corrections)
* **Lighter blue is now only the pedestal tracking pair** - the gate block ->
tracking input and the tracking -> Pedestal feedback. The storage cell ->
Pedestal and Pedestal -> funnel connectors are part of the data flow, so they
are orange like the rest of the route.
* **Orange is the whole save route**, entry point to Save panel, and it follows
the mode: frames = storage cell -> Pedestal -> funnel -> Gain -> ROI -> Save,
maps = ... -> Gain -> VPC -> Save. Display-only branches stay light blue, and
in photon mode the Gain -> ROI rail is light blue as well.
* **A route that carries no data is not drawn** (the operator found the pale
grey confusing): the raw bypass exists only in raw mode, the Pedestal path
only while the pedestal is in use, and only the selected mode's Save arrow
(ROI -> Save or VPC -> Save) is shown. The one thing that still greys is the
gate funnel when the gate is off.
* The **tracking input** now leaves the top-right corner of the gate block
(`yIn(sigGroup_, 6)`), not the funnel.
* Verified with screenshots in four states: pede+gate frames (orange: SC ->
Pedestal -> funnel -> Gain -> rail -> ROI -> Save; bypass absent; tracking
pair lighter blue), the same in photon mode (orange through Gain -> counting
and the counting -> Save arrow, rail light blue, ROI -> Save absent),
raw+gate frames (orange storage cell -> funnel -> Gain -> ROI -> Save; the
Pedestal box carries no in/out at all, only the tracking feedback),
and pede+no-gate (funnel and its control arrow pale, everything else
unchanged).
### The display path and the save path can differ (last refinement)
The operator pointed out that the orange route must follow the *save* logic, not
the `Pede/Raw` toggle: `flowStageStates()` now computes
* `displayPede = PedestalStore::loaded() && !gNoPede` (unchanged: the display,
gate, spectra and the map accumulation),
* `savePede = (gSaveMode == 1) || (saveCorrected_ && !gNoPede)` (the maps always
need `g0`; the frames are corrected only with the checkbox),
* and `FlowStrip::setStages(displayPede, savePede, gateOn, photonMaps)` draws:
| situation | what the diagram shows |
|---|---|
| display pede, `save frames`, `Pedestal corrected` **off** (the default!) | Pedestal feeder **light blue** (display only) + bypass **orange** (the file) - both live, the divergence visible |
| `Pedestal corrected` **on** (or photon maps) | Pedestal feeder **orange**, bypass not drawn |
| `Pede/Raw` = raw (both raw) | Pedestal path not drawn at all, bypass orange |
| photon mode | pedestal route orange through Gain -> counting -> Save; the Gain -> ROI rail is light blue and the ROI -> Save arrow is absent |
`saveCorrected_` also got a `toggled -> updateFlow()` connection (it had none) so
the diagram follows the checkbox, and a `saveCorrected` entry in the `[rect]`
geometry diagnostics for the click tests.
Verified with screenshots: `--use-pede --gate` in frames mode showed the blue
storage cell -> Pedestal -> funnel next to the orange storage cell -> funnel ->
Gain -> ROI -> Save (both feeders into one funnel), and clicking `Pedestal
corrected` turned the pedestal route orange and removed the bypass.
### ROI input arrow + "Unset ROI" button
* The ROI's incoming connector is now mode-dependent and always orange:
`Gain -> ROI` (the rail) while frames are written, `counting -> ROI` (a new
connector) while the maps are - they are cropped to the ROI on the way out, so
the ROI is only ever on the save path. Only one of the two is drawn at a
time; `setStages()` flips them with `p4_.on = !photonMaps` / `p9_.on =
photonMaps`.
* New **Unset ROI** button in the ROI panel (`unsetRoi()`): releases the "ROI
only" restriction if it is on, then `GateEngine::clearRoi()` (so `roiSet_` and
`roiOn_` are false and the rectangle is the full frame), clears the yellow
overlay, resets the counters and updates the read-outs. The ROI list file is
not touched.
* Verified: with `--gate-roi 100 50 200 100 --gate --stats` the live line showed
`roi=200x100@100,50`; clicking **Unset ROI** logged `[roi] ROI unset: full
frame` and the same line then showed `roi=off`, the panel reading
"no ROI selected | gate: full frame". In `--save-photon` (photon mode) the
screenshot shows the orange connector from the counting panel into the ROI and
no rail from the Gain.
### Save-time locking, truthful JSON, ROI->Save, and small labels
Operator requests, all in and verified with a running `--save-photon` run:
* **The arrow into the Save panel always leaves the ROI** (never the counting
panel): `arrPmapDown_` is gone, `arrRoiDown_` is always drawn and always
orange. The maps reach the file through the ROI, which is also why the ROI's
own input flips between the Gain (frames) and the counting (maps).
* `cumulative` -> **`cumulative (toggle to reset)`**, and the toggle now really
resets (`resetHis()`), so the label is true.
* **`Pedestal corrected` moved under the `save frames` radio** (it is a property
of the saved frames; the maps are always pedestal-subtracted) and is greyed
while the photon-map mode is selected.
* **The save counters no longer read 0 in photon mode**: `RoiSaver::Status`
gained `pmaps` + `photonMode` (Impl::pmapsInFile, fed by the writer loop), so
the GUI status line says "N maps written, M dropped" and `--stats` says
`save=1map/0` instead of `0/0`.
* **Settings are locked while a save runs** (`applySaveLock()`): the whole save
panel except `Start/Stop save` and the status label, the storage-cell filter,
the manual pedestal work, the gain correction (keV PH stays live) and the
ROI select/load/unset buttons. Live on purpose: everything display-only, the
pedestal tracker, the gate **threshold** and **min pixels**. Those two open
`Changing settings during a run, are you sure?`; No puts the old value back
(`pipeline_.gate().threshold()/minPixels()`), Yes applies it and sets
`gGateChangedDuringSave`, which makes `writeJsonSidecar()` add
`gate_settings_modified_during_run: true` and a `gate_settings_note` (the
`gate` block stays the criterion from save start). The lock is applied on
start, released on stop - including the automatic stop (budget/error), which
`updateStatus()` detects through `m_saveWasRunning`.
* Verified on screen during a running photon-map save: save panel greyed except
Start/Stop, SC/pede/gain greyed, Threshold 150.0 and Min pixels 5 live, the
Virtual photon counting panel's two display controls live with the new label,
the photon threshold greyed, `Pedestal corrected` greyed under the frames
radio, and the Save status reading "1 map written, 0 dropped".
### Layout bug (mine), destination list, "queue full"
* **The `virtual photon counting` radio was rendered outside the Save box.** When
I moved `Pedestal corrected` under the `save frames` radio I dropped the
`saveForm->addRow(saveModePhoton_);` line, so the radio had no layout slot and
Qt gave it a default geometry (measured: x=9, y=915, 100x30 - i.e. floating at
the left edge above the Save box, which is exactly what showed up as a stray
"Virtual phot..." fragment). Re-added; the `[rect]` diagnostic now reads
`saveFramesRadio 21,996 | saveCorrected 21,1025 | savePhotonRadio 21,1054`,
all 788 px wide inside the box. (`layoutFlow()` also repaints the central
widget after solving now, which is cheap insurance against leftovers in the
pads' wake.)
* **The previous-destinations list is no longer polluted by the default**: the
start-up `--h5-dir`/`--h5-base` pair is captured once (`saveDefaultDir_`,
`saveDefaultBase_`) and a save that uses exactly that pair logs
`[save] <dir>/<base> is the default destination, not added to the
previous-destinations list` instead of recording it. Verified: the list stayed
at 22 lines over a `--start-save` run with the default destination. My own
test entries (`*_scratch*`, 23 of them) were pruned from
`save_dest_list.dat`; the real `/mnt/sls_det_storage/...` destinations and the
`test/_run`/`testPHmapsave` entries were left alone.
* The dropped counter now reads `(writer behind: queue full)` and the README
explains it: the writer buffers at most 4096 items / 256 MB, so a slower disk
than the stream means the excess is dropped (and still counted by the gate
counters).
* The drop note was attached unconditionally, so a healthy run read
"0 dropped (queue full)" - a problem that was not there. It now says
`0 dropped (nothing lost)` while nothing is lost and only switches to
`(writer behind: queue full)` when the counter is non-zero. Verified on a
running photon-map save: "1 map written, 0 dropped (nothing lost)".
* The "Changing settings during a run, are you sure?" popup is now raised only
when **`Gate enabled` is ticked** (`confirmGateChangeDuringRun` returns early
on `!pipeline_.gate().enabled()`): with the gate off (passthrough) the
threshold and min-pixels change nothing about the saved frames, so they go
through silently and no `gate_settings_*` note is written. Verified with a
running save: gate on + clicking a gate spinbox -> dialog shown and declining
logged `[save] gate change refused during the run (Min pixels above threshold
-> 6)`; the same click with `--no-gate` -> no dialog, no log line.
### Image windows redrawn as plots (frame, axes, pixel grid, scale panel)
Operator request: replace the side colour bar with a panel and make the images
read as plots.
* `FastImageView::imageRect()` now reserves axis margins (52 left / 26 bottom /
6 top-right) instead of the old bar strip; `paintEvent()` draws the frame,
tick labels outside the plot (Y = data rows, row 0 at the bottom), a dotted
grid inside it, and keeps the gate-ROI / receiver-ROI overlays as before.
* `m_sx` (screen pixels per image pixel, from the last paint) drives the pixel
detail: thin outlines from 16 px, in-pixel values from 50 px
(`setIntegerValues(true)` for the photon map -> whole numbers, one decimal for
the real-valued images). The label colour is chosen from the luminance of the
pixel that was actually painted; the loops are capped (<=256 px visible for
the grid, <=64 for the labels) so a deep zoom can never stall the painter.
* The colour bar is off (`m_showBar = false`; `scaleRect()` returns an empty
rect so the old drag code is inert) and each image window now carries a
`ScalePanel` under it: `ScaleHisto` (the image's own 256-bin distribution with
the mapped range highlighted, refreshed at most every 300 ms from
`renderImage()`), a palette chooser wired to `paletteLut()` (kept in sync with
the `b`/`Shift+B`/`c` shortcuts), a lin/log switch, and min/max spin boxes
(also synced from `Set Ranges...`).
* log mode maps `log10(max(v, 1e-2))` against `log10(max(range, 1e-2))`, so
values <= 0 land at 1e-2 = -2 instead of -inf, and the "zero is white" rule
for the photon map still applies first.
* Verified on screen: the framed plot with the X labels (475..505) and Y labels
(97..90, high at the top) outside the frame, the dotted grid, the per-pixel
outlines at ~33 screen px/pixel, and the panel below with the histogram,
`palette: Col (jet)`, `lin`, `min -200.0`, `max 1000.0` (the ADC defaults).
Not yet seen on screen: the in-pixel numbers (they need >50 px/pixel, my
scripted wheel zoom stopped at ~33) and the histogram with a varied image
(a uniform test frame gives a single bin). The old bar's drag-to-stretch on
the scale is gone by design: the range is set with the spin boxes (dragging on
the histogram is a possible follow-up).
* Scale histogram refined to the operator's spec: fixed 38 px strip (the image
keeps the window stretch, the panel has a fixed height - about a tenth of the
window), one **solid** bar per bin whose width is the bin spacing on screen,
and the two range markers pinned at **1/4 and 3/4** of the width with the value
axis rescaling under them (the drawn window is the selected range widened by
half a range on each side, so dragging a marker moves that end of the range
and the marker snaps back to its fixed spot). The bars are draggable
(`onRangeChanged` -> the panel pushes the new range into the view and the spin
boxes) and the ends are printed next to the markers. A **second lin/log
switch** was added: `image` for the intensity mapping, `histo` for the bar
heights (log10(count+1)); the histogram's value axis always follows the image
switch. Verified on screen: the thin strip with the two orange markers at
1/4 and 3/4 labelled `702.7` and `1000`, and a drag of the left marker moving
`min` from -200 to 702.7 (the bars were off-window by then, which is the
fixed-marker behaviour).
### Palette list widened to the matplotlib family
The scale panel's palette chooser now offers 18 maps instead of 3 (the original
`Col (jet)`, the two ROOT-like greys, then the new ones). The `Palette` enum
keeps `Color/BW1/BW2` first so the `b` / `Shift+B` / `c` shortcuts and
`setPaletteChoice()` keep working, and `paletteName()` (new, in
`online_jf_root.cpp`, deliberately outside the anonymous namespace) feeds the
combo so the names live in one place.
* New: Turbo, Viridis, Plasma, Inferno, Magma, Cividis, Hot, Grey (linear),
Grey reversed, Coolwarm, BWR, Seismic, RdBu, YlGnBu, Sunset.
* Anchors: the perceptually uniform ones are matplotlib's published anchors and
Turbo is Google's; the diverging/sequential set uses matplotlib's definitions
and the ColorBrewer / Paul Tol anchors (YlGnBu, Sunset) as republished in
`bokeh/palettes.py`, which is where I looked them up - bokeh's licence header
attributes Viridis/Magma/Plasma/Inferno to Nathaniel Smith et al. (CC0),
Turbo to Google (Apache-2.0), the Brewer maps to Cynthia Brewer (Apache-2.0)
and Tol's to Paul Tol (BSD-3). Everything is interpolated by the existing
piecewise-linear `PaletteDef`, whose stop/colour arrays were grown from 5 to
24 entries to hold the 15-anchor turbo ramp; the LUTs are now one
`g_luts[kNumPalettes][256]` table built by the same `buildLut()` loop, so a new
palette is only an enum entry plus its anchors.
* Verified: the entry list builds and the app runs; selecting `Seismic
(diverging)` in the ADC window turned the image seismic-blue (raw ~100 ADU on
the -200..1000 range lands just below the white midpoint, as expected) with
the panel and histogram following.
* The **gain window** lost its scale panel entirely (no histogram, no palette /
lin-log / range controls): its image holds four valid numbers - the gain bits
0..3 - so there is neither a distribution to show nor a scale to choose. It
now uses a fixed `Palette::Gain4` map (0 muted blue, 1 muted green, 2 muted
yellow, 3 muted red, all desaturated on purpose so a large flat map does not
glare) with the range pinned to 0..3, and whole-number in-pixel labels. The
palette is in the enum but deliberately **not** in the chooser loop
(`i <= int(Palette::Sunset)`), so the three image windows keep their own
choices.
* In-pixel numbers: the font now also has to fit the *width* of the label, not
just the pixel height - five characters for the photon counts, six for the
real-valued images ("XXXXX.y"), at ~0.62 em per digit, still capped at 0.30 em
of the pixel so the "small tidy number" look is kept (range now 7..20 px).
At 50 px/pixel that is 12 px instead of 15, which is what makes five digits
fit; the widest label stays under the pixel edge for every zoom level above
the 50 px threshold.
### Zoom coordination switch (Display panel)
Operator request: zooming used to be ADC-master only (the ADC view propagated to
the gain/photon windows and the ROOT plots, never the other way round). Now
`zoomTo(FastImageView *src, ...)` takes the view that asked, and every image view
reports its zoom through the same wiring (`wireZoom()` for ADC, gain and photon),
so **any** window zooms all of them. A new `zoom coordination` checkbox in the
Display panel (default **on**) gates the propagation: when it is off, `zoomTo()`
returns immediately, so each window keeps its own zoom and nothing is pushed to
the others - including the ROOT canvases, which as a consequence no longer follow
the ADC while the box is off (say the word if they should keep following it
regardless). Both transitions are logged (`[zoom] coordination on/off`) and
every coordinated zoom logs its source (`[zoom] coordinated from ADC/gain/photon`).
Verified: with the default on, five wheel steps over the ADC window produced 10
`coordinated from ADC` events (two per step: wheel plus the drag path); the
checkbox is present at a sane place in the Display panel
(`[rect] zoomCoord 476,730 333x23`, ticked). The OFF case is a single
`if (!zoomCoord_) return;` on that same path.
### Makefile: both variants from one `make`
`make jfstrx` used to rebuild the *same* binary with `-DJFSTRX` (the old
`.buildflags` stamp existed only to force a full rebuild when switching), so the
two variants overwrote each other. Now the objects and the executables are
separate:
* `online_jf_*.o` + `jf_online_display_gui` (normal),
* `online_jf_*.JFSTRX.o` + `jf_online_display_gui_JFSTRX` (compiled with
`-DJFSTRX`),
and `all` builds both, so the two can never be mixed by accident (they have
different geometry and struct layouts). `make normal` and `make jfstrx` remain
as aliases, the flag stamp is gone, `clean` removes both sets, and `.gitignore`
covers the second binary and the `.JFSTRX.o` files.
* Zoomed images were painted over the axes: the scaled image is much larger than
the plot area (only the viewport window is meant to be visible), and the ROI
overlays were drawn in widget coordinates without a clip, so a ROI partly
outside the visible window ran across the tick labels and up to the window
edge. `paintEvent()` now sets `setClipRect(ir)` around the image and around
the two overlays (restoring before the axes so the frame, grid and labels are
drawn on top of a clean widget). Verified by sampling a deep-zoom screenshot:
the margin left of the frame is the widget's black background, not image
colour, while the plot interior still carries the image.
* The strip-xel decoder (../../dataStructures/jungfrauLGADStrixelsData_new.h)
dumped its header on every construction - "Jungfrau strixels 2X single chip
with full module data" / "sizeofheader = 112" - once per displayed frame in
the JFSTRX build. Those three `std::cout` statements are commented out (the
header is shared, outside this project, so the edit needed the wider sandbox
permission; it is a comment-only change and easy to revert).
* Deepest zoom is now build-dependent (`kMinViewport`): 8 image pixels across in
the plain build as before, **2** in the JFSTRX build, so single strixel pixels
can be inspected (the mapped panels are ~1000 x 1900). The same constant
guards `setViewport()` and `zoomTo()`, so a coordinated zoom propagates down
to the new limit.
### Dual-interface alignment: drain fast, pair index-aligned, shed on backlog
Operator report: after the machine had been busy for a while the two interfaces
stopped being aligned and never recovered, with the deltas fluctuating. There
were two causes, one on each side.
* **Holding a slab is back-pressure.** The old pairing stopped polling an
interface whose slab was waiting for its partner (`events = 0`) and accepted
only *equal* frame numbers. That fills slsReceiver's ZMQ ring, and the
receiver then skips frames **on its own side** - per interface, not per
frame - so the two frame numbers drift apart and, with exact matching, never
meet again. Waiting "until the partner catches up" made it worse.
What the code does now (`Pipeline::tryAssemble()` in `online_jf_core.cpp`):
* **Always drain.** Every socket is polled on every turn and drained down to
its newest message (bounded to 32 messages per socket and wake-up, so no
interface is starved). Nothing is ever held, so the receiver's buffer stays
empty - the whole point at 2 kHz.
* **Pair index-aligned or not at all.** When the newest slabs carry different
frame numbers (one interface skipped), the pair is **dropped** (`resync`) and
the next one is tried, so every frame that is displayed or saved is stitched
from a single detector frame. No chasing, no unbounded wait, no half frames.
* **Latch for a permanent offset.** If nothing pairs for 500 ms the offset is
structural (an interface that keeps skipping), so the newest slabs are paired
anyway (`skewedPairs_`, logged) to keep the stream alive; strict pairing
comes back by itself as soon as a pair matches again.
* **Decimation is manual by default**, `--decimate 2|4|8` (1 frame in N, dropped
by detector frame number so both interfaces drop the same ones). The automatic
ladder (1 -> 2 -> 4 -> 8, stepping back up after 5 s clean) only runs with
`--auto-decimate`: frames are queued in the *gate*, so the backlog is
`framesIn - framesPass - framesDrop - framesScDrop` - counting gate rejects as
backlog used to pin the ladder at 1/8 whenever the gate rejected anything.
* **A silent interface is reported, not waited for**, so a dead stream cannot
freeze the pipeline.
* Audit: `--stats` prints `resync` / `behind` / `incomplete` and the
per-second `[align] keep=1/<N> shed=<n>` line; ladder steps and "dropping the
pair" messages are rate-limited.
Verified against the live receiver `10.1.1.106 30026+30027` at ~2000 frames/s:
20 s runs alone and with eight CPU hogs (`yes`) both keep `drop=0`,
`in == pass`, `keep=1/1`, `shed=0`, with no unpaired-interface message. The
synthetic feeder with equal keys gives `resync=0`; with a permanent one-frame
offset (`dual ... 1`) the latch keeps ~16 frames/s at 20 fps instead of
stalling. At 2 kHz the interfaces are a frame apart often enough that roughly
one pair in four is dropped by the strict rule - the intended trade (aligned
frames only). A one-frame lookahead per interface (swap two buffers, no copy)
would recover them; it is the obvious follow-up if the saved-frame rate ever
matters.
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,184 @@
// Unit test for the gate's contiguity proxy ("Contiguous (N px within circle,
// r = N/2+1)").
//
// The rule must be: pass when SOME circle of radius r contains at least N
// above-threshold pixels. It used to require every above-threshold pixel to be
// within r of the centroid, which rejected perfectly valid events (two
// separated clusters) - the bug this test pins down.
//
// Build: make gate_contig_test Run: ./test/gate_contig_test
#include "online_jf.h"
#include <algorithm>
#include <cstdio>
#include <utility>
#include <vector>
namespace {
int gFail = 0;
void expect(const char *what, bool got, bool want) {
const bool ok = (got == want);
if (!ok)
gFail++;
std::printf("%-58s %s (got %s, want %s)\n", what, ok ? "ok " : "FAIL",
got ? "pass" : "drop", want ? "pass" : "drop");
}
struct Frame {
std::vector<short> px = std::vector<short>(NCH, 0);
void hit(int x, int y, short v = 2000) {
px[static_cast<size_t>(y) * kRawWidth + x] = v;
}
void clear() { std::fill(px.begin(), px.end(), 0); }
};
// Runs the real GateEngine over the frame, raw ADC values, no pedestals.
bool passes(GateEngine &g, Frame &f) {
uint64_t above = 0;
bool scDropped = false;
return g.evaluate(f.px.data(), NCH, 0, nullptr, 0, 0, above, &scDropped);
}
} // namespace
int main() {
GateEngine g;
g.setEnabled(true);
g.setThreshold(1000.0f); // the frame's hits are 2000 -> above threshold
g.setMinPixels(5);
g.setContiguous(true);
Frame f;
// 1. one tight cluster of exactly N hits: must pass
f.clear();
for (int i = 0; i < 5; i++)
f.hit(100 + i, 100);
expect("5 hits in a row (spread 4 px <= 2r)", passes(g, f), true);
// 2. a compact blob: must pass
f.clear();
f.hit(300, 200);
f.hit(301, 200);
f.hit(302, 200);
f.hit(301, 201);
f.hit(301, 199);
expect("5 hits in a compact cross", passes(g, f), true);
// 3. N hits far apart, no cluster: must be rejected
f.clear();
for (int i = 0; i < 5; i++)
f.hit(100 + 100 * i, 300);
expect("5 isolated hits 100 px apart", passes(g, f), false);
// 4. fewer than N hits, however tight: must be rejected
f.clear();
for (int i = 0; i < 4; i++)
f.hit(500 + i, 400);
expect("4 hits (below min pixels)", passes(g, f), false);
// 5. THE REPORTED BUG: two separated clusters, each with N hits.
// The centroid falls between them, which used to reject the frame.
f.clear();
for (int i = 0; i < 5; i++)
f.hit(100 + i, 50); // cluster A
for (int i = 0; i < 5; i++)
f.hit(700 + i, 450); // cluster B
expect("two clusters of 5, 700 px apart (must pass)", passes(g, f), true);
// 6. two groups of 4: no circle holds 5 -> reject
f.clear();
for (int i = 0; i < 4; i++)
f.hit(100 + i, 60);
for (int i = 0; i < 4; i++)
f.hit(600 + i, 440);
expect("two clusters of 4 (no circle holds 5)", passes(g, f), false);
// 7. a spread-out event with one dense core: pass
f.clear();
for (int i = 0; i < 5; i++)
f.hit(400 + i, 300);
f.hit(800, 100);
f.hit(900, 200);
expect("dense core plus 2 stragglers", passes(g, f), true);
// 8. N follows min_pixels: 3 hits pass when min_pixels is 3
g.setMinPixels(3);
f.clear();
for (int i = 0; i < 3; i++)
f.hit(200 + i, 200);
expect("3 hits with min_pixels = 3", passes(g, f), true);
f.clear();
for (int i = 0; i < 2; i++)
f.hit(200 + i, 200);
expect("2 hits with min_pixels = 3", passes(g, f), false);
// 9. contiguity off: the plain count decides (2 hits < 3 -> reject)
g.setContiguous(false);
f.clear();
f.hit(10, 10);
f.hit(900, 400);
expect("contiguity off, 2 hits with min_pixels = 3", passes(g, f), false);
f.clear();
for (int i = 0; i < 3; i++)
f.hit(10 + i, 10);
expect("contiguity off, 3 hits with min_pixels = 3", passes(g, f), true);
// ---- hot pixel mask taken from a photon map ---------------------------
{
GateEngine h;
h.setEnabled(true);
h.setThreshold(1000.0f);
h.setMinPixels(5);
h.setContiguous(false);
h.setHotSuppress(true);
// The map has two very bright pixels: those are the hot ones.
std::vector<float> map(NCH, 0.0f);
map[10 * kRawWidth + 10] = 400.0f;
map[10 * kRawWidth + 11] = 900.0f;
hotSetMaskFromMap(map.data(), map.size(), 100.0);
std::printf("mask from map > 100: %llu px (want 2)\n",
static_cast<unsigned long long>(hotPixelCount()));
if (hotPixelCount() != 2)
gFail++;
Frame f3;
// 5 hits, two of them on the masked pixels -> only 3 count -> drop
f3.clear();
for (int i = 0; i < 5; i++)
f3.hit(10 + i, 10);
expect("5 hits, 2 on hot pixels (min 5)", passes(h, f3), false);
// a real cluster away from the mask still passes
f3.clear();
for (int i = 0; i < 5; i++)
f3.hit(300 + i, 300);
expect("real cluster of 5 with the mask active", passes(h, f3), true);
// cleared mask: the hot pixels count again
hotClearMask();
expect("after hotClearMask, 5 hits count", passes(h, f3), true);
f3.clear();
for (int i = 0; i < 5; i++)
f3.hit(10 + i, 10);
expect("after hotClearMask, the 5 hits pass", passes(h, f3), true);
// suppression off: the mask is ignored by the gate
hotSetMaskFromMap(map.data(), map.size(), 100.0);
h.setHotSuppress(false);
f3.clear();
for (int i = 0; i < 5; i++)
f3.hit(10 + i, 10);
expect("suppression off, masked pixels count again", passes(h, f3),
true);
hotClearMask();
}
std::printf("\n%s (%d failure(s))\n", gFail ? "FAILED" : "all tests passed",
gFail);
return gFail ? 1 : 0;
}
@@ -0,0 +1,233 @@
// SPDX-License-Identifier: LGPL-3.0-or-other
// ===========================================================================
// test/jf_feeder.cpp -- synthetic slsReceiver stand-in.
//
// Publishes the same <json header><binary frame> ZMQ message pair that
// slsReceiver sends, so jf_online_display_gui can be exercised without hardware.
//
// Every 3rd frame carries NO signal; the other frames carry a 40-pixel
// cluster at ADC 300 on a baseline of 100. With
// ./jf_online_display_gui 400 127.0.0.1 30777 --stats --pede /nonexistent
// the gate threshold is 200, so exactly 2/3 of the frames must pass.
//
// make feeder
// ./test/jf_feeder tcp://*:30777 200 20 &
// ./jf_online_display_gui 400 127.0.0.1 30777 --stats --pede /nonexistent
//
// Subscriber mode (counts the frames republished by --pub):
// ./test/jf_feeder sub tcp://127.0.0.1:30778 8
//
// 4th positional arg = initial silent period in seconds (idle-receiver test).
// ===========================================================================
#include <zmq.h>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <thread>
#include <vector>
namespace {
int runSubscriber(const char *addr, int seconds) {
void *ctx = zmq_ctx_new();
void *sub = zmq_socket(ctx, ZMQ_SUB);
int rcvtimeo = 200;
zmq_setsockopt(sub, ZMQ_RCVTIMEO, &rcvtimeo, sizeof(rcvtimeo));
zmq_setsockopt(sub, ZMQ_SUBSCRIBE, "", 0);
if (zmq_connect(sub, addr) != 0) {
fprintf(stderr, "connect %s failed: %s\n", addr,
zmq_strerror(zmq_errno()));
return 1;
}
printf("subscriber: connected to %s for %d s\n", addr, seconds);
fflush(stdout);
std::vector<char> data(2 * 1024 * 1024);
char hdr[16384];
int frames = 0, endMarkers = 0;
int dumped = 0;
const auto deadline =
std::chrono::steady_clock::now() + std::chrono::seconds(seconds);
while (std::chrono::steady_clock::now() < deadline) {
const int hn = zmq_recv(sub, hdr, sizeof(hdr) - 1, 0);
if (hn < 0)
continue;
hdr[hn] = '\0';
const bool isData = (strstr(hdr, "\"data\":1") != nullptr);
if (isData)
frames++;
else if (strstr(hdr, "\"data\":0") != nullptr)
endMarkers++;
int more = 0;
size_t moreSize = sizeof(more);
zmq_getsockopt(sub, ZMQ_RCVMORE, &more, &moreSize);
int dn = -1;
if (more)
dn = zmq_recv(sub, data.data(), data.size(), 0);
if (dumped < 3) {
dumped++;
char head[400];
snprintf(head, sizeof(head), "%.399s", hdr);
printf("subscriber: hdr_len=%d data_len=%d more=%d head=%s\n", hn,
dn, more, head);
fflush(stdout);
}
}
printf("subscriber: received %d data frames, %d end markers\n", frames,
endMarkers);
fflush(stdout);
zmq_close(sub);
zmq_ctx_term(ctx);
return frames > 0 ? 0 : 1;
}
// Dual-interface stand-in: two PUB sockets on base and base+1, each sending
// one 1024x256 Y-slab of the same frame (the slsReceiver multi-UDP-interface
// layout). The bottom slab is filled with botVal, the top slab with topVal,
// so the assembled image can be verified from the gate counters.
// ./test/jf_feeder dual <basePort> <nframes> <fps> [delaySec] [botVal]
// [topVal] [topLagFrames]
int runDual(const char *baseStr, int nframes, int fps, int delayMs, int botVal,
int topVal, int topLag) {
const int base = atoi(baseStr);
const int W = 1024, H = 256;
const int bytes = W * H * 2;
void *ctx = zmq_ctx_new();
void *bot = zmq_socket(ctx, ZMQ_PUB);
void *top = zmq_socket(ctx, ZMQ_PUB);
int hwm = 1000;
zmq_setsockopt(bot, ZMQ_SNDHWM, &hwm, sizeof(hwm));
zmq_setsockopt(top, ZMQ_SNDHWM, &hwm, sizeof(hwm));
const std::string a0 = "tcp://*:" + std::to_string(base);
const std::string a1 = "tcp://*:" + std::to_string(base + 1);
if (zmq_bind(bot, a0.c_str()) != 0 || zmq_bind(top, a1.c_str()) != 0) {
fprintf(stderr, "dual bind failed: %s\n", zmq_strerror(zmq_errno()));
return 1;
}
printf("feeder: dual mode on %s and %s, %d frames at %d fps "
"(bot=%d top=%d topLag=%d)\n",
a0.c_str(), a1.c_str(), nframes, fps, botVal, topVal, topLag);
fflush(stdout);
std::vector<short> slab(W * H);
std::this_thread::sleep_for(std::chrono::milliseconds(delayMs));
auto sendSlab = [&](void *sock, int value, int frameNo) {
for (size_t i = 0; i < slab.size(); i++)
slab[i] = static_cast<short>(value);
char hdr[512];
// detSpec3 bits 8-11 carry the storage cell; cycle 0..15 so the SC
// filter can be exercised end to end
const unsigned detSpec3 = ((unsigned)(frameNo % 16) & 0xF) << 8;
const int hn = snprintf(
hdr, sizeof(hdr),
"{\"jsonversion\":5,\"data\":1,\"size\":%d,"
"\"shape\":[%d,%d],\"frameNumber\":%d,\"frameIndex\":%d,"
"\"detSpec1\":0,\"detSpec3\":%u}",
bytes, W, H, frameNo, frameNo, detSpec3);
zmq_send(sock, hdr, hn, ZMQ_SNDMORE);
zmq_send(sock, slab.data(), bytes, 0);
};
for (int f = 0; f < nframes; f++) {
sendSlab(bot, botVal, f);
const int topFrame = (f - topLag >= 0) ? f - topLag : f;
sendSlab(top, topVal, topFrame);
std::this_thread::sleep_for(
std::chrono::milliseconds(1000 / (fps > 0 ? fps : 1)));
}
const char *endhdr = "{\"jsonversion\":5,\"data\":0}";
zmq_send(bot, endhdr, strlen(endhdr), 0);
zmq_send(top, endhdr, strlen(endhdr), 0);
printf("feeder: dual done\n");
fflush(stdout);
zmq_close(bot);
zmq_close(top);
zmq_ctx_term(ctx);
return 0;
}
} // namespace
int main(int argc, char **argv) {
if (argc > 1 && std::string(argv[1]) == "sub") {
const char *addr = (argc > 2) ? argv[2] : "tcp://127.0.0.1:30778";
const int seconds = (argc > 3) ? atoi(argv[3]) : 8;
return runSubscriber(addr, seconds);
}
if (argc > 1 && std::string(argv[1]) == "dual") {
return runDual(argc > 2 ? argv[2] : "30777", argc > 3 ? atoi(argv[3]) : 60,
argc > 4 ? atoi(argv[4]) : 25,
argc > 5 ? atoi(argv[5]) * 1000 : 800,
argc > 6 ? atoi(argv[6]) : 100,
argc > 7 ? atoi(argv[7]) : 150,
argc > 8 ? atoi(argv[8]) : 0);
}
const char *addr = (argc > 1) ? argv[1] : "tcp://*:30777";
const int nframes = (argc > 2) ? atoi(argv[2]) : 200;
const int fps = (argc > 3) ? atoi(argv[3]) : 20;
// optional 4th arg: stay bound and connected but SILENT for N seconds
// first (checks that an idle-but-alive receiver is not called "stopped")
const int delayMs = (argc > 4) ? atoi(argv[4]) * 1000 : 800;
const int NCH = 524288;
void *ctx = zmq_ctx_new();
void *pub = zmq_socket(ctx, ZMQ_PUB);
int hwm = 1000;
zmq_setsockopt(pub, ZMQ_SNDHWM, &hwm, sizeof(hwm));
if (zmq_bind(pub, addr) != 0) {
fprintf(stderr, "bind %s failed: %s\n", addr,
zmq_strerror(zmq_errno()));
return 1;
}
printf("feeder: bound %s, sending %d frames at %d fps\n", addr, nframes,
fps);
fflush(stdout);
std::vector<short> frame(NCH);
std::this_thread::sleep_for(std::chrono::milliseconds(delayMs));
// detSpec1 packs the receiver ROI as 4 x uint16: xmin|xmax|ymin|ymax
const unsigned long long detSpec1 = ((unsigned long long)10u << 48) |
((unsigned long long)400u << 32) |
((unsigned long long)20u << 16) | 300u;
for (int f = 0; f < nframes; f++) {
const bool signal = (f % 3) != 0;
for (int i = 0; i < NCH; i++)
frame[i] = 100; // baseline, gain 0
if (signal)
for (int k = 0; k < 40; k++)
frame[1000 + k] = 300; // 40 px above the 200 threshold
char hdr[512];
const int hn = snprintf(
hdr, sizeof(hdr),
"{\"jsonversion\":5,\"data\":1,\"size\":%d,\"detSpec1\":%llu,"
"\"frameIndex\":%d,\"bitmode\":1,\"fileIndex\":0,"
"\"detshape\":[1,1],\"shape\":[1024,512],\"acqIndex\":%d,"
"\"frameNumber\":%d,\"expLength\":0,\"packetNumber\":0,"
"\"timestamp\":0,\"modId\":0,\"row\":0,\"column\":0,\"detSpec2\":0,"
"\"detSpec3\":%u,\"detSpec4\":0,\"detType\":0,\"version\":0,"
"\"flipRows\":0,\"quad\":0,\"completeImage\":1}",
NCH * 2, detSpec1, f, f, f,
((unsigned)(f % 16) & 0xF) << 8);
zmq_send(pub, hdr, hn, ZMQ_SNDMORE);
zmq_send(pub, frame.data(), NCH * 2, 0);
std::this_thread::sleep_for(
std::chrono::milliseconds(1000 / (fps > 0 ? fps : 1)));
}
const char *endhdr = "{\"jsonversion\":5,\"data\":0}";
zmq_send(pub, endhdr, strlen(endhdr), 0);
printf("feeder: done\n");
fflush(stdout);
zmq_close(pub);
zmq_ctx_term(ctx);
return 0;
}
@@ -0,0 +1,65 @@
// Standalone JFSTRX check that the six-panel fill geometry is consistent with
// the strixel decoder: every real (non-dummy) mapped pixel must land inside a
// valid panel at an in-range local coordinate, and each panel must be fully
// covered by the decoder map (so no panel stays empty / has holes).
#include "online_jf.h"
#include "jungfrauLGADStrixelsData_new.h"
#include "sls/sls_detector_defs.h"
#include "slsDetectorData.h"
#include <cstdio>
using header = sls::defs::sls_receiver_header;
int main() {
initGeometry();
jungfrauLGADStrixelsData *dec = new jungfrauLGADStrixelsData();
const int g1_nrows = 165, g2_nrows = 320, g3_nrows = 476;
const int mc6g3_start = g1_nrows + g2_nrows + g3_nrows;
static const int panelStartY[kNumAdcPanels] = {0, 165, 485,
1757, 1437, 961};
long perPanel[kNumAdcPanels] = {0};
long real = 0;
long dropped = 0; // real pixels outside the booked panel rectangles
bool ok = true;
for (int ipx = 0; ipx < nx; ++ipx) {
for (int ipy = 0; ipy < ny; ++ipy) {
if (dec->dataMap[ipy][ipx] == int(sizeof(header)))
continue; // dummy strixel
++real;
const int group = dec->groupmap[ipy][ipx];
if (group < 0 || group > 2) {
ok = false;
continue;
}
const int base = (ipy >= mc6g3_start) ? 3 : 0;
const int p = base + group;
const int lx = ipx;
const int ly = ipy - panelStartY[p];
if (lx < 0 || lx >= kAdcPanelW[p] || ly < 0 || ly >= kAdcPanelH[p]) {
// the decoder maps a couple of extra right-edge columns for
// chip1 g2/g3 that the ROOT histograms (and these panels) do
// not book; dropping them matches the original display
++dropped;
continue;
}
++perPanel[p];
}
}
delete dec;
std::printf("mapped %d x %d, real pixels %ld, dropped edge pixels %ld\n",
nx, ny, real, dropped);
for (int p = 0; p < kNumAdcPanels; ++p) {
const long need = long(kAdcPanelW[p]) * kAdcPanelH[p];
std::printf("panel %d: %dx%d filled %ld/%ld%s\n", p, kAdcPanelW[p],
kAdcPanelH[p], perPanel[p], need,
perPanel[p] == need ? "" : " <-- NOT FULL");
if (perPanel[p] != need)
ok = false;
}
std::printf("%s\n", ok ? "PASS: six-panel geometry"
: "FAIL: six-panel geometry");
return ok ? 0 : 1;
}
@@ -0,0 +1,171 @@
// Standalone check for the ROI writer (no hardware, no GUI):
//
// 1. a Pedestal corrected save follows the LIVE pedestal, not the snapshot
// taken at Start Save (one frame before, one after a mid-run swap);
// 2. the periodic checkpoint leaves a CLEAN pair on disk while the save is
// still running: the JSON sidecar is published atomically (.part ->
// rename) and the HDF5 is shrunk to exactly the frames written, so a
// copy taken at a checkpoint boundary is self-consistent.
//
// Not part of the build; compile by hand, see README "Testing without
// hardware".
#include "online_jf.h"
#include "H5Cpp.h"
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <fstream>
#include <memory>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
using namespace H5;
static bool fileExists(const std::string &p) {
std::ifstream f(p);
return f.good();
}
static std::string readFile(const std::string &p) {
std::ifstream f(p, std::ios::binary);
std::ostringstream ss;
ss << f.rdbuf();
return ss.str();
}
// Read the /data frame count and the first pixel of one slice from the LIVE
// file with the SWMR read flag - exactly what an external checker does while
// the save runs.
static bool readSlice(const std::string &path, hsize_t slice, hsize_t &frames,
float &px) {
hid_t f = H5Fopen(path.c_str(), H5F_ACC_RDONLY | H5F_ACC_SWMR_READ,
H5P_DEFAULT);
if (f < 0)
return false;
hid_t d = H5Dopen2(f, "/data", H5P_DEFAULT);
if (d < 0) {
H5Fclose(f);
return false;
}
hid_t s = H5Dget_space(d);
hsize_t dims[3] = {0, 0, 0};
H5Sget_simple_extent_dims(s, dims, nullptr);
frames = dims[0];
bool ok = false;
if (dims[0] > slice) {
hsize_t off[3] = {slice, 0, 0};
hsize_t cnt[3] = {1, 1, 1};
H5Sselect_hyperslab(s, H5S_SELECT_SET, off, nullptr, cnt, nullptr);
hid_t m = H5Screate_simple(3, cnt, nullptr);
ok = H5Dread(d, H5T_NATIVE_FLOAT, m, s, H5P_DEFAULT, &px) >= 0;
H5Sclose(m);
}
H5Sclose(s);
H5Dclose(d);
H5Fclose(f);
return ok;
}
// wait until the periodic JSON says at least `saved` frames are committed
static std::string waitJson(const std::string &jsonPath, uint64_t saved) {
const std::string needle = "\"saved\": " + std::to_string(saved);
for (int i = 0; i < 100; i++) {
if (fileExists(jsonPath)) {
const std::string t = readFile(jsonPath);
if (t.find(needle) != std::string::npos)
return t;
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
return {};
}
int main() {
const int W = 1024, H = 4;
const hsize_t h = 4, w = 8;
RoiSaver saver;
RoiSaver::StartInfo info;
info.x = 0;
info.y = 0;
info.w = static_cast<int>(w);
info.h = static_cast<int>(h);
info.dir = ".";
info.baseName = "pede_live_test";
info.corrected = true;
info.checkpointMs = 300; // fast checkpoints for the test
auto setA = std::make_shared<PedeSet>();
std::fill(setA->g0.begin(), setA->g0.end(), 100.0f);
PedestalStore::instance().install(setA);
info.peds = setA; // the snapshot the GUI would have taken at Start Save
std::string err;
if (!saver.start(info, err)) {
std::printf("FAIL start: %s\n", err.c_str());
return 1;
}
const std::string h5 = saver.status().h5Path;
const std::string js = saver.status().jsonPath;
std::vector<short> frame(static_cast<size_t>(W) * H, 1000);
SavedFrameHeader h1;
h1.frameNumber = 1;
saver.offer(frame.data(), W, H, h1);
// --- checkpoint 1: exactly one frame, corrected with pedestal A -------
const std::string j1 = waitJson(js, 1);
if (j1.empty()) {
std::printf("FAIL: no periodic JSON with 1 frame\n");
return 2;
}
const bool liveMid = j1.find("\"complete\": false") != std::string::npos;
hsize_t n1 = 0;
float p1 = -1.0f;
const bool live1 = readSlice(h5, 0, n1, p1); // live file, SWMR read
std::printf("checkpoint1: periodic json (complete:false)=%s live swmr "
"read=%s h5 extent=%llu px=%.1f (expect true, true, 1, 900)\n",
liveMid ? "true" : "false", live1 ? "true" : "false",
(unsigned long long)n1, p1);
// --- the tracker swaps the pedestal mid-run ---------------------------
auto setB = std::make_shared<PedeSet>();
std::fill(setB->g0.begin(), setB->g0.end(), 150.0f);
PedestalStore::instance().install(setB);
SavedFrameHeader h2;
h2.frameNumber = 2;
saver.offer(frame.data(), W, H, h2);
const std::string j2 = waitJson(js, 2);
if (j2.empty()) {
std::printf("FAIL: no periodic JSON with 2 frames\n");
return 3;
}
hsize_t n2 = 0;
float p2 = -1.0f;
const bool live2 = readSlice(h5, 1, n2, p2); // live file, SWMR read
std::printf("checkpoint2: live swmr read=%s h5 extent=%llu slice1 px=%.1f "
"(expect true, 2, 850)\n",
live2 ? "true" : "false", (unsigned long long)n2, p2);
const bool noPart = !fileExists(js + ".part");
saver.requestStop("test");
saver.finish();
const std::string jf = readFile(js);
const bool finalComplete =
jf.find("\"complete\": true") != std::string::npos;
const bool ok = liveMid && live1 && (n1 == 1) && (p1 == 900.0f) && live2 &&
(n2 == 2) && (p2 == 850.0f) && noPart && finalComplete;
std::printf("final: complete=%s no .part left=%s\n",
finalComplete ? "true" : "false", noPart ? "true" : "false");
std::printf("%s\n",
ok ? "PASS: live pedestal + periodic clean checkpoint"
: "FAIL");
return ok ? 0 : 4;
}
@@ -0,0 +1,69 @@
// Minimal XTest driver: xclick <display> click <x> <y> | key <keysym>
#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
extern int XTestFakeMotionEvent(Display *, int, int, int, unsigned long);
extern int XTestFakeButtonEvent(Display *, unsigned int, int, unsigned long);
extern int XTestFakeKeyEvent(Display *, unsigned int, int, unsigned long);
int main(int argc, char **argv) {
Display *d = XOpenDisplay(argc > 1 ? argv[1] : NULL);
if (!d) { fprintf(stderr, "no display\n"); return 1; }
if (argc > 3 && strcmp(argv[2], "click") == 0) {
int x = atoi(argv[3]), y = atoi(argv[4]);
XTestFakeMotionEvent(d, -1, x, y, 0);
XTestFakeButtonEvent(d, 1, True, 0);
XTestFakeButtonEvent(d, 1, False, 0);
} else if (argc > 3 && strcmp(argv[2], "press") == 0) {
XTestFakeMotionEvent(d, -1, atoi(argv[3]), atoi(argv[4]), 0);
XTestFakeButtonEvent(d, 1, True, 0);
} else if (argc > 3 && strcmp(argv[2], "move") == 0) {
XTestFakeMotionEvent(d, -1, atoi(argv[3]), atoi(argv[4]), 0);
} else if (argc > 2 && strcmp(argv[2], "release") == 0) {
XTestFakeButtonEvent(d, 1, False, 0);
} else if (argc > 2 && strcmp(argv[2], "raise") == 0) {
const char *want = argc > 3 ? argv[3] : "";
Window root = DefaultRootWindow(d), r2, par, *kids = NULL;
unsigned n = 0;
if (XQueryTree(d, root, &r2, &par, &kids, &n)) {
for (unsigned i = 0; i < n; i++) {
char *nm = NULL;
if (XFetchName(d, kids[i], &nm) && nm) {
if (strstr(nm, want)) {
XRaiseWindow(d, kids[i]);
XSetInputFocus(d, kids[i], RevertToParent, CurrentTime);
printf("raised: %s\n", nm);
}
XFree(nm);
}
}
if (kids) XFree(kids);
}
} else if (argc > 4 && strcmp(argv[2], "movewin") == 0) {
const char *want = argv[3];
Window root = DefaultRootWindow(d), r2, par, *kids = NULL;
unsigned n = 0;
if (XQueryTree(d, root, &r2, &par, &kids, &n)) {
for (unsigned i = 0; i < n; i++) {
char *nm = NULL;
if (XFetchName(d, kids[i], &nm) && nm) {
if (strstr(nm, want)) {
XMoveWindow(d, kids[i], atoi(argv[4]), atoi(argv[5]));
printf("moved: %s\n", nm);
}
XFree(nm);
}
}
if (kids) XFree(kids);
}
} else if (argc > 2 && strcmp(argv[2], "key") == 0) {
KeySym ks = XStringToKeysym(argc > 3 ? argv[3] : "Return");
KeyCode kc = XKeysymToKeycode(d, ks);
XTestFakeKeyEvent(d, kc, True, 0);
XTestFakeKeyEvent(d, kc, False, 0);
}
XFlush(d);
XCloseDisplay(d);
return 0;
}
@@ -10,8 +10,10 @@ script_dir = ../../etc/
CROSS = bfin-uclinux-
CC = $(CROSS)gcc
CFLAGS += -Wall -std=gnu99 -DCHIPTESTBOARDD -DSTOP_SERVER -I$(main_inc) -I$(support_lib) -I$(det_lib) -I$(current_dir) #-DDEBUG1 #-DVERBOSEI #-DVERBOSE
CFLAGS += -O2 -Wall -Wextra -Wno-unused-parameter -Wformat=2 -Wredundant-decls -Werror=return-type -std=gnu99 -DCHIPTESTBOARDD -DSTOP_SERVER -I$(main_inc) -I$(support_lib) -I$(det_lib) -I$(current_dir) #-DDEBUG1 #-DVERBOSEI #-DVERBOSE
LDLIBS += -lm -lrt -pthread
# FLAT stack size: elf2flt defaults to 4 KB, the update and startup paths need more than 8 KB
LDLIBS += -Wl,-elf2flt=-s262144
PROGS = ctbDetectorServer
DESTDIR ?= bin
INSTMODE = 0777
@@ -1355,7 +1355,7 @@ int setVLimit(int val, char *mess) {
}
int validateDACIndex(enum DACINDEX ind, char *mess) {
if (ind < 0 || ind >= NDAC_ONLY) {
if (ind >= NDAC_ONLY) {
sprintf(mess, "Could not set/get DAC. Invalid index %d\n", ind);
LOG(logERROR, (mess));
return FAIL;
@@ -1464,7 +1464,7 @@ int setDAC(enum DACINDEX ind, int val, bool mV, char *mess) {
}
int validatePowerDACIndex(enum powerIndex ind, char *mess) {
if (ind < 0 || ind > V_POWER_IO) {
if (ind > V_POWER_IO) {
sprintf(mess, "Could not set/get Power DAC. Invalid index %d\n", ind);
LOG(logERROR, (mess));
return FAIL;
@@ -2158,8 +2158,9 @@ int configureMAC() {
// start addr
uint32_t addr = RXR_ENDPOINT_START_REG;
// get struct memory
udp_header *udp = (udp_header *)(Blackfin_getBaseAddress() + addr / 2);
// build udp header
udp_header header;
udp_header *udp = &header;
memset(udp, 0, sizeof(udp_header));
// mac addresses
@@ -2191,6 +2192,13 @@ int configureMAC() {
calcChecksum(udp);
// force FPGA writes to be aligned and in order
uint32_t words[sizeof(udp_header) / sizeof(uint32_t)];
memcpy(words, udp, sizeof(udp_header));
for (size_t i = 0; i != sizeof(words) / sizeof(words[0]); ++i) {
bus_w(addr + i * 2, words[i]);
}
cleanFifos(); // FIXME: resetPerpheral() for ctb?
resetPeripheral();
LOG(logINFO, ("Waiting for %d s for mac to be up\n",
@@ -2330,7 +2338,7 @@ int validatePhaseinDegrees(enum CLKINDEX ind, int val, int retval) {
}
int setFrequency(enum CLKINDEX ind, int val) {
if (ind < 0 || ind >= NUM_CLOCKS) {
if (ind >= NUM_CLOCKS) {
LOG(logERROR, ("Unknown clock index %d to set frequency\n", ind));
return FAIL;
}
@@ -2380,7 +2388,7 @@ int setFrequency(enum CLKINDEX ind, int val) {
}
int getFrequency(enum CLKINDEX ind) {
if (ind < 0 || ind >= NUM_CLOCKS) {
if (ind >= NUM_CLOCKS) {
LOG(logERROR, ("Unknown clock index %d to get frequency\n", ind));
return -1;
}
+8 -50
View File
@@ -6,6 +6,7 @@
#include "xparameters.h"
#include <fcntl.h>
#include <stddef.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
@@ -14,20 +15,8 @@ extern int isControlServer;
#define MAX(x, y) (((x) > (y)) ? (x) : (y))
struct LocalLinkInterface ll_beb_local, *ll_beb;
struct udp_header_type udp_header;
int Beb_send_ndata;
unsigned int Beb_send_buffer_size;
unsigned int *Beb_send_data_raw;
unsigned int *Beb_send_data;
int Beb_recv_ndata;
unsigned int Beb_recv_buffer_size;
unsigned int *Beb_recv_data_raw;
unsigned int *Beb_recv_data;
short Beb_bit_mode;
int BEB_MMAP_SIZE = 0x1000;
@@ -47,28 +36,6 @@ int Beb_deactivated_right_datastream = 1;
int Beb_deactivated_num_destinations = 1;
int Beb_Beb() {
Beb_send_ndata = 0;
Beb_send_buffer_size = 1026;
Beb_send_data_raw =
malloc((Beb_send_buffer_size + 1) * sizeof(unsigned int));
if (Beb_send_data_raw == NULL) {
LOG(logERROR, ("Could not allocate memory for beb (send_data_raw)\n"));
return 0;
}
Beb_send_data = &Beb_send_data_raw[1];
Beb_recv_ndata = 0;
Beb_recv_buffer_size = 1026;
Beb_recv_data_raw =
malloc((Beb_recv_buffer_size + 1) * sizeof(unsigned int));
if (Beb_recv_data_raw == NULL) {
LOG(logERROR, ("Could not allocate memory for beb (recv_data_raw)\n"));
return 0;
}
Beb_recv_data = &Beb_recv_data_raw[1];
udp_header = (struct udp_header_type){
{0x00, 0x50, 0xc5, 0xb2, 0xcb, 0x46}, // DST MAC
{0x00, 0x50, 0xc2, 0x46, 0xd9, 0x02}, // SRC MAC
@@ -157,19 +124,12 @@ int Beb_SetHeaderData(uint64_t src_mac, uint32_t src_ip, uint16_t src_port,
memcpy(&(udp_header.dst_port[0]), &dst_port, sizeof(udp_header.dst_port));
Beb_AdjustIPChecksum(&udp_header);
unsigned int *base_ptr = (unsigned int *)&udp_header;
unsigned int num_words = (sizeof(struct udp_header_type) + 3) / 4;
for (unsigned int i = 0; i < num_words; i++)
Beb_send_data[i + 2] = base_ptr[i];
for (unsigned int i = num_words; i < 16; i++)
Beb_send_data[i + 2] = 0;
return 1;
}
void Beb_AdjustIPChecksum(struct udp_header_type *ip) {
unsigned char *cptr = (unsigned char *)ip->ver_headerlen;
unsigned char *cptr =
(unsigned char *)ip + offsetof(struct udp_header_type, ver_headerlen);
ip->ip_header_checksum[0] = 0;
ip->ip_header_checksum[1] = 0;
@@ -372,7 +332,7 @@ int Beb_SetTop(enum TOPINDEX ind) {
}
char *top_names[] = {TOP_NAMES};
int newval = Beb_Write32(csp0base, BEB_CONFIG_WR_OFST, value);
u_int32_t newval = Beb_Write32(csp0base, BEB_CONFIG_WR_OFST, value);
if (newval != value) {
LOG(logERROR,
("Could not set Top flag to %s in Beb\n", top_names[ind]));
@@ -417,7 +377,7 @@ int Beb_SetMaster(enum MASTERINDEX ind) {
}
char *master_names[] = {MASTER_NAMES};
int newval = Beb_Write32(csp0base, BEB_CONFIG_WR_OFST, value);
u_int32_t newval = Beb_Write32(csp0base, BEB_CONFIG_WR_OFST, value);
if (newval != value) {
LOG(logERROR,
("Could not set Master flag to %s in Beb\n", master_names[ind]));
@@ -1167,7 +1127,7 @@ int Beb_SetDetectorPosition(int pos[]) {
value | ((posval << UDP_HEADER_Y_OFST) & UDP_HEADER_Y_MSK));
value = Beb_Read32(csp0base, UDP_HEADER_B_LEFT_OFST);
if ((value & UDP_HEADER_Y_MSK) !=
((posval << UDP_HEADER_Y_OFST) & UDP_HEADER_Y_MSK)) {
(uint32_t)((posval << UDP_HEADER_Y_OFST) & UDP_HEADER_Y_MSK)) {
LOG(logERROR, ("Could not set column position for left port\n"));
ret = FAIL;
}
@@ -1180,7 +1140,7 @@ int Beb_SetDetectorPosition(int pos[]) {
value | ((posval << UDP_HEADER_Y_OFST) & UDP_HEADER_Y_MSK));
value = Beb_Read32(csp0base, UDP_HEADER_B_RIGHT_OFST);
if ((value & UDP_HEADER_Y_MSK) !=
((posval << UDP_HEADER_Y_OFST) & UDP_HEADER_Y_MSK)) {
(uint32_t)((posval << UDP_HEADER_Y_OFST) & UDP_HEADER_Y_MSK)) {
LOG(logERROR, ("Could not set column position for right port\n"));
ret = FAIL;
}
@@ -1379,9 +1339,7 @@ int Beb_open(u_int32_t **csp0base, u_int32_t offset) {
}
u_int32_t Beb_Read32(u_int32_t *baseaddr, u_int32_t offset) {
volatile u_int32_t value;
value = *(u_int32_t *)(baseaddr + offset / (sizeof(u_int32_t)));
return value;
return *(volatile u_int32_t *)(baseaddr + offset / sizeof(u_int32_t));
}
u_int32_t Beb_Write32(u_int32_t *baseaddr, u_int32_t offset, u_int32_t data) {
@@ -46,7 +46,6 @@ int64_t Feb_Control_RateTable_Tau_in_nsec = -1;
int64_t Feb_Control_RateTable_Period_in_nsec = -1;
unsigned int Feb_Control_rate_correction_table[1024] = {};
double Feb_Control_rate_meas[16384] = {};
double ratemax = -1;
// setup
void Feb_Control_activate(int activate) { Feb_Control_activated = activate; }
@@ -138,13 +137,6 @@ void Feb_Control_CloseSerialCommunication() {
int Feb_Control_CheckSetup(int master) {
LOG(logDEBUG1, ("Checking Set up\n"));
for (unsigned int j = 0; j < 4; j++) {
if (Feb_Control_idelay[j] < 0) {
LOG(logERROR, ("idelay chip %d not set.\n", j));
return 0;
}
}
int value = 0;
if ((Feb_Control_master) && (!Feb_Control_GetHighVoltage(&value))) {
LOG(logERROR, ("high voltage not set.\n"));
@@ -492,7 +484,7 @@ float Feb_Control_DACToVoltage(unsigned int digital, unsigned int nsteps,
int Feb_Control_SetDAC(unsigned int ch, int value) {
unsigned int dst_num = Feb_Control_rightAddress;
if (ch < 0 || ch > 15) {
if (ch > 15) {
LOG(logERROR, ("invalid ch for SetDAC.\n"));
return 0;
}
@@ -664,7 +656,7 @@ int Feb_Control_SetTrimbits(unsigned int *trimbits, int top) {
int Feb_Control_SaveAllTrimbitsTo(int value, int top) {
unsigned int chanregs[Feb_Control_trimbit_size];
for (int i = 0; i < Feb_Control_trimbit_size; i++)
for (int i = 0; i < (int)Feb_Control_trimbit_size; i++)
chanregs[i] = value;
return Feb_Control_SetTrimbits(chanregs, top);
}
@@ -1982,6 +1974,7 @@ int Feb_Control_SetRateCorrectionTau(int64_t tau_in_Nsec) {
unsigned int np = 16384; // max slope 16 * 1024
double b0[1024];
double m[1024];
double ratemax = -1;
if (tau_in_sec < 0 || period_in_sec < 0) {
if (dr == 32) {
@@ -1999,7 +1992,7 @@ int Feb_Control_SetRateCorrectionTau(int64_t tau_in_Nsec) {
LOG(logINFO, ("Changing Rate Correction Table tau:%0.8f sec, period:%f sec",
tau_in_sec, period_in_sec));
LOG(logINFO, ("\tCalculating table for tau of %lld ns.\n", tau_in_Nsec));
for (int i = 0; i < np; i++) {
for (int i = 0; i < (int)np; i++) {
Feb_Control_rate_meas[i] = i * exp(-i / period_in_sec * tau_in_sec);
if (Feb_Control_rate_meas[i] > ratemax)
ratemax = Feb_Control_rate_meas[i];
@@ -2022,7 +2015,7 @@ int Feb_Control_SetRateCorrectionTau(int64_t tau_in_Nsec) {
*/
int next_i = 0;
double beforemax;
double beforemax = 0;
b0[0] = 0;
m[0] = 1;
@@ -2032,8 +2025,9 @@ int Feb_Control_SetRateCorrectionTau(int64_t tau_in_Nsec) {
for (int b = 1; b < 1024; b++) {
if (m[b - 1] < 15) {
double s = 0, sx = 0, sy = 0, sxx = 0, sxy = 0;
int filled = 0;
for (;; next_i++) {
if (next_i >= np) {
if (next_i >= (int)np) {
for (; b < 1024; b++) {
if (beforemax > ratemax)
b0[b] = beforemax;
@@ -2044,7 +2038,7 @@ int Feb_Control_SetRateCorrectionTau(int64_t tau_in_Nsec) {
(((int)(m[b] + 0.5) & 0xf) << 14) |
((int)(b0[b] + 0.5) & 0x3fff);
}
b = 1024;
filled = 1;
break;
}
@@ -2069,6 +2063,8 @@ int Feb_Control_SetRateCorrectionTau(int64_t tau_in_Nsec) {
Feb_Control_rate_meas[next_i]: %f\n", x, y, s, sx, sy, sxx, sxy,
next_i, b, Feb_Control_rate_meas[next_i]));*/
}
if (filled)
break;
double delta = s * sxx - sx * sx;
b0[b] = (sxx * sy - sx * sxy) / delta;
m[b] = (s * sxy - sx * sy) / delta;
@@ -8,7 +8,10 @@ xfs_u8 HWIO_xfs_in8(xfs_u32 InAddress) {
*/
xfs_u8 IoContents;
__asm__ volatile("eieio; lbz %0,0(%1)" : "=r"(IoContents) : "b"(InAddress));
__asm__ volatile("eieio; lbz %0,0(%1)"
: "=r"(IoContents)
: "b"(InAddress)
: "memory");
return IoContents;
}
@@ -20,7 +23,10 @@ xfs_u16 HWIO_xfs_in16(xfs_u32 InAddress) {
*/
xfs_u16 IoContents;
__asm__ volatile("eieio; lhz %0,0(%1)" : "=r"(IoContents) : "b"(InAddress));
__asm__ volatile("eieio; lhz %0,0(%1)"
: "=r"(IoContents)
: "b"(InAddress)
: "memory");
return IoContents;
}
@@ -32,7 +38,10 @@ xfs_u32 HWIO_xfs_in32(xfs_u32 InAddress) {
*/
xfs_u32 IoContents;
__asm__ volatile("eieio; lwz %0,0(%1)" : "=r"(IoContents) : "b"(InAddress));
__asm__ volatile("eieio; lwz %0,0(%1)"
: "=r"(IoContents)
: "b"(InAddress)
: "memory");
return IoContents;
}
@@ -43,7 +52,8 @@ void HWIO_xfs_out8(xfs_u32 OutAddress, xfs_u8 Value) {
* such that the I/O operation completes before proceeding on
*/
__asm__ volatile("stb %0,0(%1); eieio" ::"r"(Value), "b"(OutAddress));
__asm__ volatile("stb %0,0(%1); eieio" ::"r"(Value), "b"(OutAddress)
: "memory");
}
/*****************************************************************************/
@@ -52,7 +62,8 @@ void HWIO_xfs_out16(xfs_u32 OutAddress, xfs_u16 Value) {
* such that the I/O operation completes before proceeding on
*/
__asm__ volatile("sth %0,0(%1); eieio" ::"r"(Value), "b"(OutAddress));
__asm__ volatile("sth %0,0(%1); eieio" ::"r"(Value), "b"(OutAddress)
: "memory");
}
/*****************************************************************************/
@@ -62,5 +73,6 @@ void HWIO_xfs_out32(xfs_u32 OutAddress, xfs_u32 Value) {
* such that the I/O operation completes before proceeding on
*/
__asm__ volatile("stw %0,0(%1); eieio" ::"r"(Value), "b"(OutAddress));
__asm__ volatile("stw %0,0(%1); eieio" ::"r"(Value), "b"(OutAddress)
: "memory");
}
@@ -95,7 +95,7 @@ int Local_Write(struct LocalLinkInterface *ll, unsigned int buffer_len,
LOG(logDEBUG1, ("LL Write - Len: %2d - If: %X - Data: ", buffer_len,
ll->ll_fifo_base));
for (int i = 0; i < buffer_len / 4; i++)
for (int i = 0; i < (int)buffer_len / 4; i++)
LOG(logDEBUG1, ("%.8X ", *(((unsigned *)buffer) + i)));
while (words_send <= last_word) {
@@ -10,7 +10,7 @@ script_dir = ../../etc/
BLACKFIN_CC = bfin-uclinux-gcc
CROSS = powerpc-4xx-softfloat-
CC = $(CROSS)gcc
CFLAGS += -Wall -std=gnu99 -DEIGERD -DSTOP_SERVER -I$(main_inc) -I$(support_lib) -I$(current_dir) #-DDEBUG1 #-DVERBOSEI #-DVERBOSE
CFLAGS += -O2 -Wall -Wextra -Wno-unused-parameter -Wformat=2 -Wredundant-decls -Werror=return-type -std=gnu99 -DEIGERD -DSTOP_SERVER -I$(main_inc) -I$(support_lib) -I$(current_dir) #-DDEBUG1 #-DVERBOSEI #-DVERBOSE
LDLIBS += -lm -lrt -pthread
PROGS = eigerDetectorServer
DESTDIR = bin
@@ -890,7 +890,7 @@ int getDefaultDac(enum DACINDEX index, enum detectorSettings sett,
if (sett != UNDEFINED) {
return FAIL;
}
if (index < 0 || index >= NDAC)
if (index >= NDAC)
return FAIL;
*retval = defaultDacValues[index];
return OK;
@@ -900,7 +900,7 @@ int setDefaultDac(enum DACINDEX index, enum detectorSettings sett, int value) {
if (sett != UNDEFINED) {
return FAIL;
}
if (index < 0 || index >= NDAC)
if (index >= NDAC)
return FAIL;
char *dac_names[] = {DAC_NAMES};
@@ -1428,7 +1428,7 @@ int setThresholdEnergy(int ev) {
/* parameters - dac, adc, hv */
int validateDACIndex(enum DACINDEX ind, char *mess) {
// including E_VTHRESHOLD
if (ind < 0 || ind >= NDAC + 1) {
if (ind >= NDAC + 1) {
sprintf(mess, "Could not set DAC. Invalid index %d\n", ind);
LOG(logERROR, (mess));
return FAIL;
@@ -9,7 +9,7 @@ script_dir = ../../etc/
CROSS = nios2-buildroot-linux-gnu-
CC = $(CROSS)gcc
CFLAGS += -Wall -std=gnu99 -DGOTTHARD2D -DSTOP_SERVER -I$(main_inc) -I$(support_lib) -I$(current_dir) #-DDEBUG1 #-DVERBOSEI #-DVERBOSE
CFLAGS += -O2 -Wall -Wextra -Wno-unused-parameter -Wformat=2 -Wredundant-decls -Werror=return-type -std=gnu99 -DGOTTHARD2D -DSTOP_SERVER -I$(main_inc) -I$(support_lib) -I$(current_dir) #-DDEBUG1 #-DVERBOSEI #-DVERBOSE
LDLIBS += -lm -lrt -pthread
PROGS = gotthard2DetectorServer
DESTDIR ?= bin
@@ -2052,9 +2052,9 @@ void setupHeader(int iRxEntry, int vetoInterface, uint32_t destip,
iRxEntry += RXR_ENDPOINTS_MAX;
}
addr += (iRxEntry * RXR_ENDPOINT_OFST);
// get struct memory
udp_header *udp =
(udp_header *)(Nios_getBaseAddress() + addr / (sizeof(u_int32_t)));
// build udp header
udp_header header;
udp_header *udp = &header;
memset(udp, 0, sizeof(udp_header));
// mac addresses
@@ -2085,6 +2085,14 @@ void setupHeader(int iRxEntry, int vetoInterface, uint32_t destip,
// total length is redefined in firmware
calcChecksum(udp);
// force FPGA writes to be aligned and in order
uint32_t words[sizeof(udp_header) / sizeof(uint32_t)];
memcpy(words, udp, sizeof(udp_header));
for (size_t i = 0; i != sizeof(words) / sizeof(words[0]); ++i) {
bus_w(addr + i * sizeof(uint32_t), words[i]);
}
if (iRxEntry < numUdpDestinations) {
LOG(logINFO, ("\tIP checksum : 0x%lx\n\n", udp->ip_checksum));
}
@@ -9,8 +9,10 @@ script_dir = ../../etc/
CROSS = bfin-uclinux-
CC = $(CROSS)gcc
CFLAGS += -Wall -std=gnu99 -DJUNGFRAUD -DSTOP_SERVER -I$(main_inc) -I$(support_lib) -I$(current_dir) #-DVERBOSEI #-DVERBOSE
CFLAGS += -O2 -Wall -Wextra -Wno-unused-parameter -Wformat=2 -Wredundant-decls -Werror=return-type -std=gnu99 -DJUNGFRAUD -DSTOP_SERVER -I$(main_inc) -I$(support_lib) -I$(current_dir) #-DVERBOSEI #-DVERBOSE
LDLIBS += -lm -lrt -pthread
# FLAT stack size: elf2flt defaults to 4 KB, the update and startup paths need more than 8 KB
LDLIBS += -Wl,-elf2flt=-s262144
PROGS = jungfrauDetectorServer
DESTDIR ?= bin
INSTMODE = 0777
@@ -416,7 +416,7 @@ int setChipIndex(enum CHIPINDEX ind, char *mess) {
enum CHIPINDEX getChipIndex() { return chipIndex; }
int validateChipIndex(enum CHIPINDEX ind, char *mess) {
if (ind < 0 || ind >= NUM_CHIP_INDICES) {
if (ind >= NUM_CHIP_INDICES) {
sprintf(mess, "Invalid chip index %d. Options: %s\n", (int)ind,
CHIP_VALS_HELP);
LOG(logERROR, (mess));
@@ -846,7 +846,7 @@ int getDefaultDac(enum DACINDEX index, enum detectorSettings sett,
return FAIL;
}
if (index < 0 || index >= NDAC)
if (index >= NDAC)
return FAIL;
*retval = defaultDacValues[index];
return OK;
@@ -882,7 +882,7 @@ int setDefaultDac(enum DACINDEX index, enum detectorSettings sett, int value) {
// not a special dac
return FAIL;
}
if (index < 0 || index >= NDAC)
if (index >= NDAC)
return FAIL;
LOG(logINFO, ("Setting Default Dac [%d - %s]: %d\n", (int)index,
dac_names[index], value));
@@ -1564,7 +1564,7 @@ void setGainMode(enum gainMode mode) {
/* parameters - dac, adc, hv */
int validateDACIndex(enum DACINDEX ind, char *mess) {
if (ind < 0 || ind >= NDAC) {
if (ind >= NDAC) {
sprintf(mess, "Could not set DAC. Invalid index %d\n", ind);
LOG(logERROR, (mess));
return FAIL;
@@ -1870,8 +1870,9 @@ void setupHeader(int iRxEntry, enum interfaceType type, uint32_t destip,
: RXR_ENDPOINT_OUTER_START_REG);
// calculate rxr endpoint offset
addr += (iRxEntry * RXR_ENDPOINT_OFST);
// get struct memory
udp_header *udp = (udp_header *)(Blackfin_getBaseAddress() + addr / 2);
// build udp header
udp_header header;
udp_header *udp = &header;
memset(udp, 0, sizeof(udp_header));
// mac addresses
@@ -1902,6 +1903,14 @@ void setupHeader(int iRxEntry, enum interfaceType type, uint32_t destip,
// total length is redefined in firmware
calcChecksum(udp);
// force FPGA writes to be aligned and in order
uint32_t words[sizeof(udp_header) / sizeof(uint32_t)];
memcpy(words, udp, sizeof(udp_header));
for (size_t i = 0; i != sizeof(words) / sizeof(words[0]); ++i) {
bus_w(addr + i * 2, words[i]);
}
if (iRxEntry < numUdpDestinations) {
LOG(logINFO, ("\tIP checksum : 0x%lx\n\n", udp->ip_checksum));
}
@@ -9,8 +9,10 @@ script_dir = ../../etc/
CROSS = bfin-uclinux-
CC = $(CROSS)gcc
CFLAGS += -Wall -std=gnu99 -DMOENCHD -DSTOP_SERVER -I$(main_inc) -I$(support_lib) -I$(current_dir) #-DVERBOSEI #-DVERBOSE
CFLAGS += -O2 -Wall -Wextra -Wno-unused-parameter -Wformat=2 -Wredundant-decls -Werror=return-type -std=gnu99 -DMOENCHD -DSTOP_SERVER -I$(main_inc) -I$(support_lib) -I$(current_dir) #-DVERBOSEI #-DVERBOSE
LDLIBS += -lm -lrt -pthread
# FLAT stack size: elf2flt defaults to 4 KB, the update and startup paths need more than 8 KB
LDLIBS += -Wl,-elf2flt=-s262144
PROGS = moenchDetectorServer
DESTDIR ?= bin
INSTMODE = 0777
@@ -548,7 +548,7 @@ int resetToDefaultDacs(int hardReset, char *mess) {
int getDefaultDac(enum DACINDEX index, enum detectorSettings sett,
int *retval) {
if (index < 0 || index >= NDAC)
if (index >= NDAC)
return FAIL;
*retval = defaultDacValues[index];
return OK;
@@ -556,7 +556,7 @@ int getDefaultDac(enum DACINDEX index, enum detectorSettings sett,
int setDefaultDac(enum DACINDEX index, enum detectorSettings sett, int value) {
char *dac_names[] = {DAC_NAMES};
if (index < 0 || index >= NDAC)
if (index >= NDAC)
return FAIL;
LOG(logINFO, ("Setting Default Dac [%d - %s]: %d\n", (int)index,
dac_names[index], value));
@@ -913,7 +913,7 @@ enum detectorSettings getSettings() {
/* parameters - dac, adc, hv */
/* parameters - dac, adc, hv */
int validateDACIndex(enum DACINDEX ind, char *mess) {
if (ind < 0 || ind >= NDAC) {
if (ind >= NDAC) {
sprintf(mess, "Could not set DAC. Invalid index %d\n", ind);
LOG(logERROR, (mess));
return FAIL;
@@ -1203,8 +1203,9 @@ void setupHeader(int iRxEntry, enum interfaceType type, uint32_t destip,
: RXR_ENDPOINT_OUTER_START_REG);
// calculate rxr endpoint offset
addr += (iRxEntry * RXR_ENDPOINT_OFST);
// get struct memory
udp_header *udp = (udp_header *)(Blackfin_getBaseAddress() + addr / 2);
// build udp header
udp_header header;
udp_header *udp = &header;
memset(udp, 0, sizeof(udp_header));
// mac addresses
@@ -1235,6 +1236,14 @@ void setupHeader(int iRxEntry, enum interfaceType type, uint32_t destip,
// total length is redefined in firmware
calcChecksum(udp);
// force FPGA writes to be aligned and in order
uint32_t words[sizeof(udp_header) / sizeof(uint32_t)];
memcpy(words, udp, sizeof(udp_header));
for (size_t i = 0; i != sizeof(words) / sizeof(words[0]); ++i) {
bus_w(addr + i * 2, words[i]);
}
if (iRxEntry < numUdpDestinations) {
LOG(logINFO, ("\tIP checksum : 0x%lx\n\n", udp->ip_checksum));
}
@@ -10,7 +10,7 @@ script_dir = ../../etc/
CROSS = nios2-buildroot-linux-gnu-
CC = $(CROSS)gcc
CFLAGS += -Wall -std=gnu99 -DMYTHEN3D -DSTOP_SERVER -I$(main_inc) -I$(support_lib) -I$(det_lib) -I$(current_dir) #-DDEBUG1 #-DVERBOSEI #-DVERBOSE
CFLAGS += -O2 -Wall -Wextra -Wno-unused-parameter -Wformat=2 -Wredundant-decls -Werror=return-type -std=gnu99 -DMYTHEN3D -DSTOP_SERVER -I$(main_inc) -I$(support_lib) -I$(det_lib) -I$(current_dir) #-DDEBUG1 #-DVERBOSEI #-DVERBOSE
LDLIBS += -lm -lrt -pthread
PROGS = mythen3DetectorServer
DESTDIR ?= bin
@@ -2225,9 +2225,9 @@ int configureMAC() {
uint32_t addr = BASE_UDP_RAM;
// calculate rxr endpoint offset
addr += (iRxEntry * RXR_ENDPOINT_OFST); // TODO: is there round robin
// get struct memory
udp_header *udp =
(udp_header *)(Nios_getBaseAddress() + addr / (sizeof(u_int32_t)));
// build udp header
udp_header header;
udp_header *udp = &header;
memset(udp, 0, sizeof(udp_header));
// mac addresses
@@ -2258,6 +2258,14 @@ int configureMAC() {
// total length is redefined in firmware
calcChecksum(udp);
// force FPGA writes to be aligned and in order
uint32_t words[sizeof(udp_header) / sizeof(uint32_t)];
memcpy(words, udp, sizeof(udp_header));
for (size_t i = 0; i != sizeof(words) / sizeof(words[0]); ++i) {
bus_w(addr + i * sizeof(uint32_t), words[i]);
}
if (iRxEntry < numUdpDestinations) {
LOG(logINFO, ("\tIP checksum : 0x%lx\n\n", udp->ip_checksum));
}
@@ -4,7 +4,7 @@
#include "sls/sls_detector_defs.h"
#define REQRD_FRMWRE_VRSN (0x241113)
#define KERNEL_DATE_VRSN "Mon May 10 18:00:21 CEST 2021"
#define KERNEL_DATE_VRSN "Mon Jan 18 14:24:30 CET 2021"
#define ID_FILE "detid_mythen3.txt"
#define NUM_HARDWARE_VERSIONS (2)
@@ -6,8 +6,10 @@
#include <sys/types.h>
void bus_w(u_int32_t offset, u_int32_t data);
void bus_w_csp1(u_int32_t offset, u_int32_t data);
void bus_w_csp2(u_int32_t offset, u_int32_t data);
u_int32_t bus_r(u_int32_t offset);
u_int32_t bus_r_csp1(u_int32_t offset);
u_int32_t bus_r_csp2(u_int32_t offset);
uint64_t getU64BitReg(int aLSB, int aMSB);
void setU64BitReg(uint64_t value, int aLSB, int aMSB);
@@ -120,5 +120,14 @@ uint32_t *Blackfin_getBaseAddress();
*/
int mapCSP0(void);
/** sleep for only usecs */
void usleep_bf(uint64_t i);
/**
* Prepare the core cycle counter used by usleep_bf (called from mapCSP0)
*/
int initCycleCounter(void);
/**
* busy-wait for usecs on the core cycle counter
* @returns 0, or -1 without sleeping if the wait does not fit the 32-bit
* cycle count (about 8 s at 525 MHz)
*/
int usleep_bf(uint32_t us);
@@ -94,16 +94,21 @@ int LTC2620_D_SetDacValue(int dacnum, int val, char *dacname, char *mess) {
// fname to write to
char fname[MAX_STR_LENGTH];
memset(fname, 0, MAX_STR_LENGTH);
char fnameFormat[MAX_STR_LENGTH];
memset(fnameFormat, 0, MAX_STR_LENGTH);
strcpy(fnameFormat, LTC2620_D_DriverFileName);
sprintf(fname, "%s%d", fnameFormat, dacnum);
#if defined(XILINX_CHIPTESTBOARDD)
// driver file names are printf formats with the dac number in the middle
// (CURRENT_BOARD_LINKS_FOLDER "/ao%d" and "/ao%d_pd"), so the format
// cannot be a string literal here
const char *fnameFormat = LTC2620_D_DriverFileName;
if (val == LTC2620_D_PWR_DOWN_VAL) {
strcpy(fnameFormat, LTC2620_D_PowerDownDriverFileName);
fnameFormat = LTC2620_D_PowerDownDriverFileName;
val = 1; //-100?:Invalid Argument
}
sprintf(fname, fnameFormat, dacnum);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
snprintf(fname, sizeof(fname), fnameFormat, dacnum);
#pragma GCC diagnostic pop
#else
snprintf(fname, sizeof(fname), "%s%d", LTC2620_D_DriverFileName, dacnum);
#endif
LOG(logDEBUG1, ("fname %s\n", fname));
@@ -28,6 +28,18 @@ void bus_w(u_int32_t offset, u_int32_t data) {
*ptr1 = data;
}
void bus_w_csp1(u_int32_t offset, u_int32_t data) {
volatile u_int32_t *ptr1;
ptr1 = (u_int32_t *)(csp1base + offset / (sizeof(u_int32_t)));
*ptr1 = data;
}
u_int32_t bus_r_csp1(u_int32_t offset) {
volatile u_int32_t *ptr1;
ptr1 = (u_int32_t *)(csp1base + offset / (sizeof(u_int32_t)));
return *ptr1;
}
u_int32_t bus_r(u_int32_t offset) {
volatile u_int32_t *ptr1;
ptr1 = (u_int32_t *)(csp0base + offset / (sizeof(u_int32_t)));
@@ -8,10 +8,11 @@
#include "sls/sls_detector_defs.h"
#include <fcntl.h> // open
#include <stdio.h> // fopen
#include <sys/mman.h> // mmap
#include <unistd.h> // usleep
/* global variables */
const uint64_t BFIN_CYCLES_1uSECOND = 20;
u_int32_t *csp0base = 0;
#define CSP0 0x20200000
@@ -125,6 +126,8 @@ int mapCSP0(void) {
#endif
LOG(logINFO, ("csp0base mapped from %p to %p\n", csp0base,
(csp0base + MEM_SIZE)));
if (initCycleCounter() == FAIL)
return FAIL;
LOG(logINFO, ("Status Register: %08x\n", bus_r(STATUS_REG)));
} else
LOG(logINFO, ("Memory already mapped before\n"));
@@ -133,10 +136,62 @@ int mapCSP0(void) {
uint32_t *Blackfin_getBaseAddress() { return csp0base; }
// usleep is not viable on blackfin
void usleep_bf(uint64_t i) {
uint64_t j = i * BFIN_CYCLES_1uSECOND;
while (--j) {
asm volatile("");
/* BF537 core clock; refined from /proc/cpuinfo in initCycleCounter() */
#define BFIN_CCLK_HZ_DEFAULT 525000000UL
static uint32_t bfinCyclesPerUs = BFIN_CCLK_HZ_DEFAULT / 1000000;
#ifdef __bfin__
static inline uint32_t get_cycles(void) {
uint32_t cycles;
__asm__ volatile("%0 = CYCLES;" : "=d"(cycles));
return cycles;
}
#endif
int initCycleCounter(void) {
#ifndef __bfin__
return OK; // virtual build: usleep_bf falls back to usleep
#else
// kernel reports "cpu MHz : 525.000/131.250" (core/system)
FILE *fp = fopen("/proc/cpuinfo", "r");
if (fp != NULL) {
char line[128];
double mhz = 0;
while (fgets(line, sizeof(line), fp) != NULL) {
if (sscanf(line, "cpu MHz : %lf", &mhz) == 1 && mhz > 0) {
bfinCyclesPerUs = (uint32_t)mhz;
break;
}
}
fclose(fp);
}
}
// CYCLES only counts if the kernel set SYSCFG.CCEN; refuse to run
// otherwise, because usleep_bf would spin forever
uint32_t c0 = get_cycles();
for (volatile int i = 0; i < 1000; ++i)
;
if (get_cycles() == c0) {
LOG(logERROR, ("Blackfin cycle counter is not running\n"));
return FAIL;
}
LOG(logINFO, ("Cycle counter running at %u cycles/us\n", bfinCyclesPerUs));
return OK;
#endif
}
// usleep is not viable on blackfin (10 ms tick); spin on the core cycle counter
int usleep_bf(uint32_t us) {
// the wait is counted in core cycles in a uint32_t: about 8 s at 525 MHz
if (us > UINT32_MAX / bfinCyclesPerUs) {
return -1;
}
#ifndef __bfin__
usleep(us);
#else
uint32_t start = get_cycles();
uint32_t wait = us * bfinCyclesPerUs;
while ((uint32_t)(get_cycles() - start) < wait)
;
#endif
return 0;
}
@@ -16,7 +16,7 @@ else
endif
CC = $(CROSS)gcc
#TODO: allow these warnings and fix code
CFLAGS += -Wall -std=gnu99 -Wno-format-overflow -Wno-format-truncation -DXILINX_CHIPTESTBOARDD -DARMPROCESSOR -DSTOP_SERVER -I$(main_inc) -I$(support_lib) -I$(det_lib) -I$(current_dir) #-DDEBUG1 #-DVERBOSEI #-DVERBOSE
CFLAGS += -O2 -Wall -Wextra -Wno-unused-parameter -Wformat=2 -Wredundant-decls -Werror=return-type -std=gnu99 -Wno-format-overflow -Wno-format-truncation -DXILINX_CHIPTESTBOARDD -DARMPROCESSOR -DSTOP_SERVER -I$(main_inc) -I$(support_lib) -I$(det_lib) -I$(current_dir) #-DDEBUG1 #-DVERBOSEI #-DVERBOSE
#CFLAGS += -Wall -std=gnu99 -DXILINX_CHIPTESTBOARDD -DARMPROCESSOR -DSTOP_SERVER -I$(main_inc) -I$(support_lib) -I$(det_lib) -I$(current_dir) #-DDEBUG1 #-DVERBOSEI #-DVERBOSE
LDLIBS += -lm -lrt -pthread
PROGS = xilinx_ctbDetectorServer
@@ -145,7 +145,7 @@ void basictests() {
}
// check for API compatibility - old server
if (sw_fw_apiversion > requiredfwversion) {
if ((uint64_t)sw_fw_apiversion > requiredfwversion) {
sprintf(initErrorMessage,
"This firmware-software api version (0x%lx) is incompatible "
"with the software's minimum required firmware version "
@@ -158,7 +158,7 @@ void basictests() {
}
// check for firmware compatibility - old firmware
if (requiredfwversion > fwversion) {
if (requiredfwversion > (uint64_t)fwversion) {
sprintf(initErrorMessage,
"This firmware version (0x%lx) is incompatible.\n"
"Please update firmware (min. 0x%lx) to be compatible with "
@@ -1502,8 +1502,9 @@ int configureMAC() {
}
#endif
// get struct memory
udp_header *udp = (udp_header *)(Arm_getUDPBaseAddress());
// build udp header
udp_header header;
udp_header *udp = &header;
memset(udp, 0, sizeof(udp_header));
// mac addresses
@@ -1535,6 +1536,13 @@ int configureMAC() {
calcChecksum(udp);
// force FPGA writes to be aligned and in order
uint32_t words[sizeof(udp_header) / sizeof(uint32_t)];
memcpy(words, udp, sizeof(udp_header));
for (size_t i = 0; i != sizeof(words) / sizeof(words[0]); ++i) {
bus_w_csp1(i * sizeof(uint32_t), words[i]);
}
return OK;
}
@@ -1037,6 +1037,9 @@ class Detector {
void clearRxROI();
/** Returns true if the current ROI covers the complete detector (default)*/
bool isCompleteRxRoi() const;
///@}
/** @name File */
+3
View File
@@ -1335,6 +1335,7 @@ Result<bool> Detector::getUDPDataStream(const defs::portPosition port,
void Detector::setUDPDataStream(const defs::portPosition port,
const bool enable, Positions pos) {
pimpl->validatePortEnable(port, enable, pos);
pimpl->setUDPDataStream(port, enable, pos);
}
@@ -1485,6 +1486,8 @@ void Detector::setRxROI(const std::vector<defs::ROI> &args) {
void Detector::clearRxROI() { pimpl->clearRxROI(); }
bool Detector::isCompleteRxRoi() const { return pimpl->isCompleteRxRoi(); }
// File
Result<defs::fileFormat> Detector::getFileFormat(Positions pos) const {
+82 -3
View File
@@ -48,6 +48,15 @@ bool DetectorImpl::isAllPositions(Positions pos) const {
(pos.size() == modules.size()));
}
std::vector<int> DetectorImpl::fillInPositions(Positions pos) const {
if (isAllPositions(pos)) {
std::vector<int> positions(modules.size());
std::iota(begin(positions), end(positions), 0);
return positions;
}
return pos;
}
void DetectorImpl::setAcquiringFlag(bool flag) { shm()->acquiringFlag = flag; }
int DetectorImpl::getDetectorIndex() const { return detectorIndex; }
@@ -1640,6 +1649,7 @@ Result<bool> DetectorImpl::getUDPDataStream(const defs::portPosition port,
void DetectorImpl::setUDPDataStream(const defs::portPosition port,
const bool enable, Positions pos) {
assertTwoUDPDataInterfaces("set enable/disable UDP ports");
validatePortEnable(port, enable, pos);
Parallel(&Module::setUDPDataStream, pos, port, enable);
updateRxUDPDatastreamMetadata();
}
@@ -1857,6 +1867,49 @@ void DetectorImpl::convertGlobalRoiToPortLevel(
}
}
void DetectorImpl::validatePortEnable(const defs::portPosition port,
const bool enable,
std::vector<int> pos) const {
if (isCompleteRxRoi())
return;
auto positions = fillInPositions(pos);
for (int i : positions) {
if (i < 0 || static_cast<size_t>(i) >= modules.size()) {
throw RuntimeError("Invalid module index: " + std::to_string(i));
}
auto modRois = modules[i]->getRxROI();
auto portList = getPortPositionList();
int portIndex = 0;
for (auto p : portList) {
if (p == port)
break;
portIndex++;
}
validatePortEnableRoiState(i, port, modRois[portIndex], enable);
}
}
/** Assumption:
* 1. Check only for Eiger because port disable is only for Eiger now
* 2. This function called only when an Roi being set (not complete detector)
* ie. complete ROI enabled and some ports disabled is allowed
*/
void DetectorImpl::validatePortEnableRoiState(const int moduleIndex,
defs::portPosition changedPort,
const defs::ROI &portRoi,
bool enabled) const {
// enabled or disabled and no roi
if (enabled || portRoi.noRoi())
return;
// diabled but ROI specified
std::ostringstream oss;
oss << ToString(changedPort) << " port of module " << moduleIndex << " ("
<< modules[moduleIndex]->getHostname()
<< ") is not active but ROI is specified for it: " << ToString(portRoi)
<< ". Please disable the ROI for this port or enable the port.";
throw RuntimeError(oss.str());
}
void DetectorImpl::setRxROI(const std::vector<defs::ROI> &args) {
if (shm()->detType == CHIPTESTBOARD ||
shm()->detType == defs::XILINX_CHIPTESTBOARD) {
@@ -1871,10 +1924,13 @@ void DetectorImpl::setRxROI(const std::vector<defs::ROI> &args) {
}
validateROIs(args);
int nPortsPerModule =
Parallel(&Module::getNumberofUDPInterfacesFromShm, {})
.tsquash("Inconsistent number of udp ports set up per module");
std::vector<std::vector<defs::ROI>> moduleRois;
// further validate and create port level rois for each module
for (size_t iModule = 0; iModule < modules.size(); ++iModule) {
auto moduleGlobalRoi = getModuleROI(iModule);
// at most 2 rois per module (for each port)
@@ -1886,15 +1942,30 @@ void DetectorImpl::setRxROI(const std::vector<defs::ROI> &args) {
roi.ymax = -1;
}
}
// check overlap with module
// check overlap of each roi with module
for (const auto &arg : args) {
if (arg.overlap(moduleGlobalRoi)) {
convertGlobalRoiToPortLevel(arg, moduleGlobalRoi, portRois);
}
}
modules[iModule]->setRxROI(portRois);
// validate roi against port enables
if (nPortsPerModule == 2) {
auto ports = getPortPositionList();
for (int iport = 0; iport != nPortsPerModule; ++iport) {
defs::portPosition port = ports[iport];
auto enable = modules[iModule]->getUDPDataStream(port);
validatePortEnableRoiState(iModule, port, portRois[iport],
enable);
}
}
moduleRois.push_back(portRois);
}
// set rois for each module
for (size_t iModule = 0; iModule < modules.size(); ++iModule) {
modules[iModule]->setRxROI(moduleRois[iModule]);
}
// metadata
modules[0]->setRxROIMetadata(args);
}
@@ -1909,6 +1980,14 @@ void DetectorImpl::clearRxROI() {
modules[0]->setRxROIMetadata(std::vector<defs::ROI>(1));
}
bool DetectorImpl::isCompleteRxRoi() const {
const auto rois = getRxROI();
if (rois.empty() || (rois.size() == 1 && rois[0].completeRoi())) {
return true;
}
return false;
}
void DetectorImpl::getBadChannels(const std::string &fname,
Positions pos) const {
auto res = Parallel(&Module::getBadChannels, pos);
+11 -1
View File
@@ -183,6 +183,8 @@ class DetectorImpl : public virtual slsDetectorDefs {
bool isAllPositions(Positions pos) const;
std::vector<int> fillInPositions(Positions pos) const;
inline bool isChipTestBoard() const {
return (shm()->detType == defs::CHIPTESTBOARD ||
shm()->detType == defs::XILINX_CHIPTESTBOARD);
@@ -320,8 +322,16 @@ class DetectorImpl : public virtual slsDetectorDefs {
defs::xy getPortGeometry() const;
std::vector<defs::ROI> getRxROI(int module_id = -1) const;
void validatePortEnable(const defs::portPosition port, const bool enable,
std::vector<int> pos) const;
void validatePortEnableRoiState(const int moduleIndex,
defs::portPosition changedPort,
const defs::ROI &portRoi,
bool enabled) const;
void setRxROI(const std::vector<defs::ROI> &args);
void clearRxROI();
bool isCompleteRxRoi() const;
void getBadChannels(const std::string &fname, Positions pos) const;
void setBadChannels(const std::string &fname, Positions pos);
@@ -510,4 +520,4 @@ class DetectorImpl : public virtual slsDetectorDefs {
void *pCallbackArg{nullptr};
};
} // namespace sls
} // namespace sls
@@ -855,6 +855,94 @@ TEST_CASE("rx_roi", "[.detectorintegration][.disable_check_data_file]") {
}
}
TEST_CASE("rx_roi_port_disabled", "[.detectorintegration]") {
Detector det;
auto det_type = det.getDetectorType().squash();
auto two_ports = (det_type == defs::EIGER);
if (det_type == defs::JUNGFRAU || det_type == defs::MOENCH) {
int numinterfaces = det.getNumberofUDPInterfaces().tsquash(
"inconsistent number of interfaces");
two_ports = (numinterfaces == 2);
}
if (two_ports) {
auto prev_roi = det.getRxROI();
auto ports = det.getPortPositionList();
auto prev_port0 = det.getUDPDataStream(ports[0], {0})[0];
auto prev_port1 = det.getUDPDataStream(ports[1], {0})[0];
bool prev_tengiga = false;
if (det_type == defs::EIGER) {
prev_tengiga = det.getTenGiga().squash(false);
}
std::vector<defs::ROI> rois(2);
switch (det_type) {
case defs::EIGER:
det.setTenGiga(true);
rois[0] = defs::ROI{0, 10, 0, 20};
rois[1] = defs::ROI{600, 610, 0, 30};
break;
case defs::JUNGFRAU:
rois[0] = defs::ROI{0, 10, 0, 10};
rois[1] = defs::ROI{0, 20, 300, 310};
break;
case defs::MOENCH:
rois[0] = defs::ROI{0, 10, 0, 10};
rois[1] = defs::ROI{0, 20, 300, 310};
break;
default:
REQUIRE(false);
break;
}
det.clearRxROI();
det.setUDPDataStream(ports[0], true, {0});
det.setUDPDataStream(ports[1], true, {0});
det.setRxROI(rois);
REQUIRE_THROWS(det.setUDPDataStream(ports[0], false));
REQUIRE_THROWS(det.setUDPDataStream(ports[1], false));
det.clearRxROI();
det.setUDPDataStream(ports[0], true, {0});
det.setUDPDataStream(ports[1], true, {0});
det.setRxROI({rois[0]});
REQUIRE_THROWS(det.setUDPDataStream(ports[0], false));
REQUIRE_NOTHROW(det.setUDPDataStream(ports[1], false));
det.clearRxROI();
det.setUDPDataStream(ports[0], true, {0});
det.setUDPDataStream(ports[1], true, {0});
det.setRxROI({rois[1]});
REQUIRE_NOTHROW(det.setUDPDataStream(ports[0], false));
REQUIRE_THROWS(det.setUDPDataStream(ports[1], false));
det.clearRxROI();
det.setUDPDataStream(ports[0], false, {0});
det.setUDPDataStream(ports[1], true, {0});
REQUIRE_THROWS(det.setRxROI({rois[0]}));
REQUIRE_NOTHROW(det.setRxROI({rois[1]}));
det.clearRxROI();
det.setUDPDataStream(ports[0], true, {0});
det.setUDPDataStream(ports[1], false, {0});
REQUIRE_NOTHROW(det.setRxROI({rois[0]}));
REQUIRE_THROWS(det.setRxROI({rois[1]}));
/* eiger virtual detector doesnt understand it cant disable in 1g mode,
* so cant test in 1g mode */
if (prev_roi.size() == 1 && prev_roi[0].completeRoi())
det.clearRxROI();
else
det.setRxROI(prev_roi);
det.setUDPDataStream(ports[0], prev_port0, {0});
det.setUDPDataStream(ports[1], prev_port1, {0});
if (det_type == defs::EIGER) {
det.setTenGiga(prev_tengiga);
}
}
}
TEST_CASE("rx_clearroi", "[.detectorintegration]") {
Detector det;
Caller caller(&det);
+4 -2
View File
@@ -1632,7 +1632,8 @@ int ClientInterface::set_threshold(Interface &socket) {
LOG(logDEBUG) << "Threshold: " << arg << " eV";
if (detType != EIGER)
functionNotImplemented();
verifyIdle(socket);
// allow setting threshold metadata on the fly
// without checking for idle state
impl()->setThresholdEnergy(arg);
return socket.Send(OK);
}
@@ -1659,7 +1660,8 @@ int ClientInterface::set_all_threshold(Interface &socket) {
LOG(logDEBUG) << "Threshold:" << ToString(eVs);
if (detType != MYTHEN3)
functionNotImplemented();
verifyIdle(socket);
// allow setting threshold metadata on the fly
// without checking for idle state
impl()->setThresholdEnergy(eVs);
return socket.Send(OK);
}
+7 -7
View File
@@ -3,11 +3,11 @@
/** API versions */
#define APILIB "0.0.0 0x250909"
#define APIRECEIVER "0.0.0 0x250822"
#define APICTB "0.0.0 0x260506"
#define APIGOTTHARD2 "0.0.0 0x260427"
#define APIMOENCH "0.0.0 0x260424"
#define APIEIGER "0.0.0 0x260807"
#define APIXILINXCTB "0.0.0 0x260813"
#define APIJUNGFRAU "0.0.0 0x261002"
#define APIMYTHEN3 "0.0.0 0x261022"
#define APICTB "0.0.0 0x261103"
#define APIGOTTHARD2 "0.0.0 0x261103"
#define APIMOENCH "0.0.0 0x261103"
#define APIEIGER "0.0.0 0x261027"
#define APIXILINXCTB "0.0.0 0x261103"
#define APIJUNGFRAU "0.0.0 0x261103"
#define APIMYTHEN3 "0.0.0 0x261103"
#define APIMATTERHORN "0.0.0 0x260529"