diff --git a/.github/workflows/cmake.yaml b/.github/workflows/cmake.yaml index 1c4d7b0f6..d3eeee0bd 100644 --- a/.github/workflows/cmake.yaml +++ b/.github/workflows/cmake.yaml @@ -37,7 +37,8 @@ jobs: -DSLS_USE_PYTHON=ON \ -DSLS_USE_HDF5=ON \ -DSLS_USE_GUI=ON \ - -DSLS_USE_MOENCH=ON + -DSLS_USE_MOENCH=ON \ + -DSLS_TREAT_WARNINGS_AS_ERRORS=ON - name: Build # Build your program with the given configuration diff --git a/CMakeLists.txt b/CMakeLists.txt index 8830eb86e..d29e4782b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -236,6 +236,7 @@ option(SLS_USE_MOENCH "compile zmq and post processing for Moench" OFF) option(SLS_USE_JUNGFRAU "compile post processing for Jungfrau" OFF) option(SLS_USE_MATTERHORN "compile matterhorn server" OFF) option(SLS_INSTALL_VERSIONED_BINARIES "Add version number to binaries on install" OFF) #Needed for multi version RPM +option(SLS_TREAT_WARNINGS_AS_ERRORS "Treat warnings as errors" OFF) #Convenience option to switch off defaults when building Moench binaries only option(SLS_BUILD_ONLY_MOENCH "compile only Moench" OFF) @@ -347,7 +348,9 @@ if (NOT TARGET slsProjectWarnings) -Wno-missing-field-initializers) endif() - + if(SLS_TREAT_WARNINGS_AS_ERRORS) + target_compile_options(slsProjectWarnings INTERFACE -Werror) + endif() endif() @@ -376,6 +379,10 @@ if (NOT TARGET slsProjectCSettings) target_compile_options(slsProjectCSettings INTERFACE -fsanitize=address,undefined -fno-omit-frame-pointer) target_link_libraries(slsProjectCSettings INTERFACE -fsanitize=address,undefined) endif() + + if(SLS_TREAT_WARNINGS_AS_ERRORS) + target_compile_options(slsProjectCSettings INTERFACE -Werror) + endif() endif() @@ -483,13 +490,15 @@ if(SLS_BUILD_DOCS) add_subdirectory(docs) endif(SLS_BUILD_DOCS) -if(SLS_USE_MOENCH) +if(SLS_USE_MOENCH OR SLS_USE_JUNGFRAU) add_subdirectory(slsDetectorCalibration/tiffio) +endif() + +if(SLS_USE_MOENCH) add_subdirectory(slsDetectorCalibration/moenchExecutables) endif(SLS_USE_MOENCH) if(SLS_USE_JUNGFRAU) - add_subdirectory(slsDetectorCalibration/tiffio) add_subdirectory(slsDetectorCalibration/jungfrauExecutables) endif(SLS_USE_JUNGFRAU) diff --git a/pyctbgui/pyctbgui/services/Acquisition.py b/pyctbgui/pyctbgui/services/Acquisition.py index b32b8a659..dcf06707d 100644 --- a/pyctbgui/pyctbgui/services/Acquisition.py +++ b/pyctbgui/pyctbgui/services/Acquisition.py @@ -133,6 +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) case readoutMode.DIGITAL_ONLY: self.view.spinBoxAnalog.setDisabled(True) self.view.labelAnalog.setDisabled(True) @@ -140,6 +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) case readoutMode.ANALOG_AND_DIGITAL: self.view.spinBoxAnalog.setEnabled(True) self.view.labelAnalog.setEnabled(True) @@ -147,6 +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) case readoutMode.TRANSCEIVER_ONLY: self.view.spinBoxAnalog.setDisabled(True) self.view.labelAnalog.setDisabled(True) @@ -154,6 +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 case _: self.view.spinBoxAnalog.setDisabled(True) self.view.labelAnalog.setDisabled(True) @@ -161,6 +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.view.comboBoxROMode.currentIndexChanged.connect(self.setReadOut) self.view.spinBoxAnalog.editingFinished.connect(self.setAnalog) diff --git a/pyctbgui/pyctbgui/services/Plot.py b/pyctbgui/pyctbgui/services/Plot.py index a14ef1a0a..688265484 100644 --- a/pyctbgui/pyctbgui/services/Plot.py +++ b/pyctbgui/pyctbgui/services/Plot.py @@ -4,10 +4,10 @@ import random from pathlib import Path import numpy as np -from PyQt5 import QtWidgets, QtGui, uic +from PyQt5 import QtWidgets, QtGui, QtCore, uic from aare import transform, ReadoutMode -from aare._aare import Matterhorn10, Matterhorn02, Moench04 +from aare._aare import Matterhorn10, Matterhorn02, Moench04, Moench05 import pyqtgraph as pg from pyctbgui.utils import recordOrApplyPedestal @@ -43,6 +43,7 @@ class PlotTab(QtWidgets.QWidget): self.pedestalApply: bool = True self.__acqFrames = None self.logger = logging.getLogger('PlotTab') + self.plotSplitter = None def setup_ui(self): self.signalsTab = self.mainWindow.signalsTab @@ -51,10 +52,17 @@ class PlotTab(QtWidgets.QWidget): self.adcTab = self.mainWindow.adcTab self.initializeColorMaps() + # TODO use list comprehension + self.checkBoxCounters = [self.view.checkBoxCounter0, self.view.checkBoxCounter1, self.view.checkBoxCounter2, self.view.checkBoxCounter3] + + for checkBox in self.checkBoxCounters: + checkBox.setChecked(False) + checkBox.setEnabled(False) + self.imagePlots = ( self.mainWindow.plotAnalogImage, self.mainWindow.plotDigitalImage, - self.mainWindow.plotTransceiverImage, + self.transceiverTab.transceiverImageViews, ) def connect_ui(self): @@ -93,12 +101,54 @@ class PlotTab(QtWidgets.QWidget): self.view.radioButtonFixed.clicked.connect(partial(self.setColorRangeMode, Defines.colorRange.fixed)) self.view.radioButtonCenter.clicked.connect(partial(self.setColorRangeMode, Defines.colorRange.center)) - for plot in self.imagePlots: - plot.scene.sigMouseMoved.connect(partial(self.showPlotValues, plot)) - plot.getHistogramWidget().item.sigLevelChangeFinished.connect(partial(self.handleHistogramChange, plot)) + for index, checkBox in enumerate(self.checkBoxCounters): + checkBox.stateChanged.connect(partial(self.displayCounter, index)) + + # show image Values for analog image + nMaxY = lambda : self.mainWindow.nAnalogRows + nMaxX = lambda : self.mainWindow.nAnalogCols + 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)) + + # show image Values for digital image + nMaxY = lambda : self.mainWindow.nDigitalRows + nMaxX = lambda : self.mainWindow.nDigitalCols + 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)) + + # show image Values for transceiver image + nMaxY = lambda : self.transceiverTab.nTransceiverRows + nMaxX = lambda : self.transceiverTab.nTransceiverCols + + for index, image_view in enumerate(self.transceiverTab.transceiverImageViews): + frame = lambda : 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)) 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) + self.transceiverTab.update_ImageSplitter() + + 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): + for checkBox in self.checkBoxCounters: + checkBox.setEnabled(enabled) + def refresh(self): self.getZMQHWM() @@ -262,7 +312,11 @@ class PlotTab(QtWidgets.QWidget): updates UI views should be called after every change to cmin or cmax """ for plot in self.imagePlots: - plot.getHistogramWidget().item.setLevels(min=self.cmin, max=self.cmax) + 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) + else: + plot.getHistogramWidget().item.setLevels(min=self.cmin, max=self.cmax) self.view.cminSpinBox.setValue(self.cmin) self.view.cmaxSpinBox.setValue(self.cmax) @@ -271,7 +325,9 @@ class PlotTab(QtWidgets.QWidget): # print(f'color map:{self.comboBoxColorMap.currentText()}') self.mainWindow.plotAnalogImage.setColorMap(cm) self.mainWindow.plotDigitalImage.setColorMap(cm) - self.mainWindow.plotTransceiverImage.setColorMap(cm) + for i, showncounter in enumerate(self.transceiverTab.shownCounters): + if showncounter: + self.transceiverTab.transceiverImageViews[i].setColorMap(cm) def getZMQHWM(self): @@ -343,11 +399,11 @@ class PlotTab(QtWidgets.QWidget): self.mainWindow.transceiverPlots[i].hide() def addAllSelectedTransceiverPlots(self): - for i in range(Defines.transceiver.count): + for i in range(Defines.transceiver.maxcount): self.addSelectedTransceiverPlots(i) def removeAllTransceiverPlots(self): - for i in range(Defines.transceiver.count): + for i in range(Defines.transceiver.maxcount): self.mainWindow.transceiverPlots[i].hide() def showPlot(self): @@ -356,7 +412,7 @@ class PlotTab(QtWidgets.QWidget): self.mainWindow.plotTransceiverWaveform.hide() self.mainWindow.plotAnalogImage.hide() self.mainWindow.plotDigitalImage.hide() - self.mainWindow.plotTransceiverImage.hide() + self.transceiverTab.transceiverImageSplitter.hide() self.view.labelDigitalWaveformOption.setDisabled(True) self.view.radioButtonOverlay.setDisabled(True) self.view.radioButtonStripe.setDisabled(True) @@ -379,7 +435,10 @@ class PlotTab(QtWidgets.QWidget): if self.view.radioButtonWaveform.isChecked(): self.mainWindow.plotTransceiverWaveform.show() elif self.view.radioButtonImage.isChecked(): - self.mainWindow.plotTransceiverImage.show() + for i, showncounter in enumerate(self.transceiverTab.shownCounters): + self.transceiverTab.transceiverImageViews[i].setVisible(showncounter) + + self.transceiverTab.transceiverImageSplitter.show() def plotOptions(self): @@ -404,14 +463,18 @@ class PlotTab(QtWidgets.QWidget): 'bottom', "Transceiver Sample [#]") self.view.stackedWidgetPlotType.setCurrentIndex(0) + self.enableallCounterCheckBox(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.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 # enable plotting else: self.view.labelPlotOptions.show() @@ -419,45 +482,57 @@ class PlotTab(QtWidgets.QWidget): self.mainWindow.read_timer.start(Defines.Time_Plot_Refresh_ms) def setDecoder(self): + # TODO: really dont like to set attributes on the fly - hard to understand whats going on if self.view.comboBoxPlot.currentText() == "Matterhorn02": print("Initializing decoder for Matterhorn02") - self.mainWindow.nTransceiverRows = Matterhorn02.nRows - self.mainWindow.nTransceiverCols = Matterhorn02.nCols + self.transceiverTab.nTransceiverRows = Matterhorn02.nRows + self.transceiverTab.nTransceiverCols = Matterhorn02.nCols + self.transceiverTab.update_numCounters(1) self.mainWindow.decoder = transform.Matterhorn02TransceiverTransform() elif self.view.comboBoxPlot.currentText() == "Matterhorn1_16bit_1_counter": print("Initializing decoder for Matterhorn1 with 1 counter 16 bit dynamic range") - self.mainWindow.nTransceiverRows = Matterhorn10.nRows - self.mainWindow.nTransceiverCols = Matterhorn10.nCols + self.transceiverTab.nTransceiverRows = Matterhorn10.nRows + self.transceiverTab.nTransceiverCols = Matterhorn10.nCols + self.transceiverTab.update_numCounters(1) self.mainWindow.decoder = transform.Matterhorn10Transform(16, 1) elif self.view.comboBoxPlot.currentText() == "Matterhorn1_16bit_4_counters": print("Initializing decoder for Matterhorn1 with 4 counters 16 bit dynamic range") - self.mainWindow.nTransceiverRows = Matterhorn10.nRows*4 - self.mainWindow.nTransceiverCols = Matterhorn10.nCols + self.transceiverTab.nTransceiverRows = Matterhorn10.nRows + self.transceiverTab.nTransceiverCols = Matterhorn10.nCols + self.transceiverTab.update_numCounters(4) self.mainWindow.decoder = transform.Matterhorn10Transform(16, 4) elif self.view.comboBoxPlot.currentText() == "Matterhorn1_8bit_1_counter": print("Initializing decoder for Matterhorn1 with 1 counter 8 bit dynamic range") - self.mainWindow.nTransceiverRows = Matterhorn10.nRows - self.mainWindow.nTransceiverCols = Matterhorn10.nCols + self.transceiverTab.nTransceiverRows = Matterhorn10.nRows + self.transceiverTab.nTransceiverCols = Matterhorn10.nCols + self.transceiverTab.update_numCounters(1) self.mainWindow.decoder = transform.Matterhorn10Transform(8, 1) elif self.view.comboBoxPlot.currentText() == "Matterhorn1_8bit_4_counters": print("Initializing decoder for Matterhorn1 with 4 counters 8 bit dynamic range") - self.mainWindow.nTransceiverRows = Matterhorn10.nRows*4 - self.mainWindow.nTransceiverCols = Matterhorn10.nCols + self.transceiverTab.nTransceiverRows = Matterhorn10.nRows + self.transceiverTab.nTransceiverCols = Matterhorn10.nCols + self.transceiverTab.update_numCounters(4) self.mainWindow.decoder = transform.Matterhorn10Transform(8, 4) elif self.view.comboBoxPlot.currentText() == "Matterhorn1_4bit_4_counters": print("Initializing decoder for Matterhorn1 with 4 counters 4 bit dynamic range") - self.mainWindow.nTransceiverRows = Matterhorn10.nRows*4 - self.mainWindow.nTransceiverCols = Matterhorn10.nCols + self.transceiverTab.nTransceiverRows = Matterhorn10.nRows + self.transceiverTab.nTransceiverCols = Matterhorn10.nCols + self.transceiverTab.update_numCounters(4) self.mainWindow.decoder = transform.Matterhorn10Transform(4, 4) elif self.view.comboBoxPlot.currentText() == "Matterhorn1_4bit_1_counter": print("Initializing decoder for Matterhorn1 with 1 counter 4 bit dynamic range") - self.mainWindow.nTransceiverRows = Matterhorn10.nRows - self.mainWindow.nTransceiverCols = Matterhorn10.nCols + self.transceiverTab.nTransceiverRows = Matterhorn10.nRows + self.transceiverTab.nTransceiverCols = Matterhorn10.nCols + self.transceiverTab.update_numCounters(1) self.mainWindow.decoder = transform.Matterhorn10Transform(4, 1) elif self.view.comboBoxPlot.currentText() == "Moench04": self.mainWindow.nAnalogRows = Moench04.nRows self.mainWindow.nAnalogCols = Moench04.nCols self.mainWindow.decoder = transform.Moench04AnalogTransform() + elif self.view.comboBoxPlot.currentText() == "Moench05": + self.mainWindow.nAnalogRows = Moench05.nRows + self.mainWindow.nAnalogCols = Moench05.nCols + self.mainWindow.decoder = transform.Moench05Transform() try: if hasattr(self.mainWindow.decoder, "compatibility") and callable(getattr(self.mainWindow.decoder, "compatibility")): @@ -533,21 +608,13 @@ class PlotTab(QtWidgets.QWidget): # get the RGB Values # print(color.getRgb()) - def showPlotValues(self, sender, pos): + def showPlotValues(self, sender, get_nMaxX, get_nMaxY, get_frame, pos): x = sender.getImageItem().mapFromScene(pos).x() y = sender.getImageItem().mapFromScene(pos).y() val = 0 - nMaxY = self.mainWindow.nAnalogRows - nMaxX = self.mainWindow.nAnalogCols - frame = self.mainWindow.analog_frame - if sender == self.mainWindow.plotDigitalImage: - nMaxY = self.mainWindow.nDigitalRows - nMaxX = self.mainWindow.nDigitalCols - frame = self.mainWindow.digital_frame - elif sender == self.mainWindow.plotTransceiverImage: - nMaxY = self.mainWindow.nTransceiverRows - nMaxX = self.mainWindow.nTransceiverCols - frame = self.mainWindow.transceiver_frame + nMaxX = get_nMaxX() + nMaxY = get_nMaxY() + frame = get_frame() if 0 <= x < nMaxX and 0 <= y < nMaxY and not np.array_equal(frame, []): val = frame[int(y), int(x)] message = f'[row, col]: [{y:.2f}, {x:.2f}] = {val:.2f}' diff --git a/pyctbgui/pyctbgui/services/Transceiver.py b/pyctbgui/pyctbgui/services/Transceiver.py index 13bb72fca..828decd26 100644 --- a/pyctbgui/pyctbgui/services/Transceiver.py +++ b/pyctbgui/pyctbgui/services/Transceiver.py @@ -2,7 +2,7 @@ from functools import partial from pathlib import Path import numpy as np -from PyQt5 import QtWidgets, uic +from PyQt5 import QtWidgets, QtCore, uic import pyqtgraph as pg from pyqtgraph import LegendItem @@ -24,11 +24,18 @@ class TransceiverTab(QtWidgets.QWidget): self.plotTab = None self.legend: LegendItem | None = None self.acquisitionTab = None - + self.nCounters: int = Defines.transceiver.maxcount + 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 + def setup_ui(self): self.plotTab = self.mainWindow.plotTab self.acquisitionTab = self.mainWindow.acquisitionTab - for i in range(Defines.transceiver.count): + for i in range(Defines.transceiver.maxcount): self.setTransceiverButtonColor(i, self.plotTab.getRandomColor()) self.initializeAllTransceiverPlots() @@ -39,7 +46,7 @@ class TransceiverTab(QtWidgets.QWidget): self.plotTab.subscribeToggleLegend(self.updateLegend) def connect_ui(self): - for i in range(Defines.transceiver.count): + for i in range(Defines.transceiver.maxcount): 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)) @@ -55,7 +62,7 @@ class TransceiverTab(QtWidgets.QWidget): """ enabledPlots = [] self.legend.clear() - for i in range(Defines.transceiver.count): + for i in range(Defines.transceiver.maxcount): if getattr(self.view, f'checkBoxTransceiver{i}Plot').isChecked(): plotName = getattr(self.view, f"labelTransceiver{i}").text() enabledPlots.append((self.mainWindow.transceiverPlots[i], plotName)) @@ -104,7 +111,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.count): + for i in range(Defines.transceiver.maxcount): checkBoxPlot = getattr(self.view, f"checkBoxTransceiver{i}Plot") checkBoxEn = getattr(self.view, f"checkBoxTransceiver{i}") if checkBoxEn.isChecked() and checkBoxPlot.isChecked(): @@ -133,9 +140,7 @@ class TransceiverTab(QtWidgets.QWidget): transceiverOffset += nDBitEnabled * (nbitsPerDBit // 8) trans_array = np.array(np.frombuffer(data, offset=transceiverOffset, dtype=np.uint8)) - tmp = self.mainWindow.decoder(trans_array) - - return tmp + return self.mainWindow.decoder(trans_array) def processImageData(self, data, dSamples): """ @@ -145,28 +150,47 @@ class TransceiverTab(QtWidgets.QWidget): data: raw image data """ # get zoom state - viewBox = self.mainWindow.plotTransceiverImage.getView() - - state = viewBox.getState() + image_states = [image_view.getView().getState() for image_view in self.transceiverImageViews] + + transceiver_frame : np.ndarray = None try: - self.mainWindow.transceiver_frame = self._processImageData(data, dSamples, self.mainWindow.romode.value, + transceiver_frame = self._processImageData(data, dSamples, self.mainWindow.romode.value, self.mainWindow.nDBitEnabled) self.plotTab.ignoreHistogramSignal = True - self.mainWindow.plotTransceiverImage.setImage(self.mainWindow.transceiver_frame) + + for i in range(transceiver_frame.shape[0]): + self.transceiverImageViews[i].setImage(transceiver_frame[i]) 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(self.mainWindow.transceiver_frame) + self.plotTab.setFrameLimits(transceiver_frame) # keep the zoomed in state (not 1st image) - if self.mainWindow.firstTransceiverImage: - self.mainWindow.firstTransceiverImage = False - else: - viewBox.setState(state) - return self.mainWindow.transceiver_frame + for idx, image_view in enumerate(self.transceiverImageViews): + if(self.firstTransceiverImage[idx] and self.shownCounters[idx]): + self.firstTransceiverImage[idx] = False + else: + image_view.getView().setState(image_states[idx]) + + return transceiver_frame + + 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 + + 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): + self.transceiverImageViews[i].setVisible(showncounter) def initializeAllTransceiverPlots(self): self.mainWindow.plotTransceiverWaveform = pg.plot() @@ -174,7 +198,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.count): + for i in range(Defines.transceiver.maxcount): 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, @@ -182,16 +206,19 @@ class TransceiverTab(QtWidgets.QWidget): name=legendName) self.mainWindow.transceiverPlots[i].hide() - self.mainWindow.plotTransceiverImage = pg.ImageView() - self.mainWindow.nTransceiverRows = 0 - self.mainWindow.nTransceiverCols = 0 - self.mainWindow.transceiver_frame = np.zeros( - (self.mainWindow.nTransceiverRows, self.mainWindow.nTransceiverCols)) - self.mainWindow.plotTransceiverImage.setImage(self.mainWindow.transceiver_frame) - self.mainWindow.verticalLayoutPlot.addWidget(self.mainWindow.plotTransceiverImage, 6) - + # initialize image cm = pg.colormap.get('CET-L9') # prepare a linear color map - self.mainWindow.plotTransceiverImage.setColorMap(cm) + + for i in range(self.nCounters): + imageView = pg.ImageView() + imageView.setColorMap(cm) + self.transceiverImageViews.append(imageView) + self.transceiverImageSplitter.addWidget(imageView) + + self.update_ImageSplitter() # update the splitter + self.mainWindow.verticalLayoutPlot.addWidget(self.transceiverImageSplitter, 6) + + def getTransceiverEnableReg(self): retval = self.det.transceiverenable diff --git a/pyctbgui/pyctbgui/ui/plot.ui b/pyctbgui/pyctbgui/ui/plot.ui index 848d48f16..01d2b5ec1 100644 --- a/pyctbgui/pyctbgui/ui/plot.ui +++ b/pyctbgui/pyctbgui/ui/plot.ui @@ -6,8 +6,8 @@ 0 0 - 860 - 800 + 1937 + 1104 @@ -25,7 +25,7 @@ 10 50 841 - 141 + 161 @@ -35,13 +35,10 @@ QFrame::Raised - - - - true - + + - Type: + Options: @@ -50,6 +47,12 @@ false + + + 0 + 31 + + Distribution @@ -58,28 +61,193 @@ - - - - Qt::Vertical + + + + true - + - 20 - 40 + 0 + 31 - + + Waveform + + + true + + + buttonGroup + + + + + + + + 0 + 31 + + + + Counter 2 + + + + + + + + 0 + 31 + + + + Counter 3 + + + + + + + true + + + + 0 + 31 + + + + No Plot + + + false + + + buttonGroup + + + + + + + + 0 + 31 + + + + Counter 0 + + + + + + + true + + + + 0 + 31 + + + + <html><head/><body><p>If set to high readout, zmq HWM is set to 2 and buffer size to 1MB to drop zmq packets to catch up.</p><p>If set to low readout (default), zmq HWM is set to zmq default (1000) and buffer size to os default to not drop any zmq packets.</p></body></html> + + + + Low - drop no zmq packet + + + + + High - drop zmq packets to catch up + + + + + + + + true + + + + 0 + 31 + + + + Type: + + + + + + + + 150 + 16777215 + + + + <html><head/><body><p>If set to high readout, zmq HWM is set to 2 and buffer size to 1MB to drop zmq packets to catch up.</p><p>If set to low readout (default), zmq HWM is set to zmq default (1000) and buffer size to os default to not drop any zmq packets.</p></body></html> + + + Readout speed: + + + + + + + true + + + + 0 + 31 + + + + Image + + + buttonGroup + + + + + 16777215 + 31 + + - 2 + 0 + + 0 + + + 0 + + + + 0 + 31 + + Digital plot: @@ -87,6 +255,12 @@ + + + 0 + 31 + + Overlay @@ -110,6 +284,12 @@ + + + 0 + 31 + + show legend @@ -120,6 +300,12 @@ + + + 0 + 31 + + Stripe @@ -149,6 +335,42 @@ + + 0 + + + 0 + + + + + Decoder: + + + + + + + Color Map: + + + + + + + true + + + + 120 + 31 + + + + Color map of the image + + + @@ -202,6 +424,11 @@ Moench04 + + + + Moench05 + @@ -210,35 +437,6 @@ - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - true - - - - 120 - 31 - - - - Color map of the image - - - @@ -255,292 +453,661 @@ - - - - Decoder: + + + + Qt::Horizontal - - - - - - Color Map: + + + 40 + 20 + - + - - + + + + + 0 + 31 + + - Options: + Counter 1 - - - - true - - - No Plot - - - false - - - buttonGroup - - - - - - - true - + + 0 31 - - <html><head/><body><p>If set to high readout, zmq HWM is set to 2 and buffer size to 1MB to drop zmq packets to catch up.</p><p>If set to low readout (default), zmq HWM is set to zmq default (1000) and buffer size to os default to not drop any zmq packets.</p></body></html> - - - - Low - drop no zmq packet - - - - - High - drop zmq packets to catch up - - - - - - <html><head/><body><p>If set to high readout, zmq HWM is set to 2 and buffer size to 1MB to drop zmq packets to catch up.</p><p>If set to low readout (default), zmq HWM is set to zmq default (1000) and buffer size to os default to not drop any zmq packets.</p></body></html> - Readout speed: + Counters: - - - - true - - - Image - - - buttonGroup - - - - - - - true - - - Waveform - - - true - - - buttonGroup - - - - 0 - 480 + 10 + 440 841 - 311 + 400 + + + 0 + 400 + + + + + 16777215 + 16777215 + + QFrame::StyledPanel QFrame::Sunken - - - + + + false + + + 0 + 31 + + + + + 16777215 + 16777215 + + Fit Panel ADC: - - + + false - - Max: + + + 0 + 31 + - - - - - - false + + + 16777215 + 16777215 + - - Values - - - - - - - false + + QFrame::NoFrame Dynamic Range: - - + + false + + + 0 + 31 + + + + + 16777215 + 16777215 + + - Y: + Serial Offset: - - + + false + + + 100 + 31 + + + + + 45 + 16777215 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - + + false + + + 100 + 31 + + + + + 16777215 + 16777215 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - + + false + + + 0 + 0 + + + + + 16777215 + 16777215 + + Min: - - + + false + + + 245 + 31 + + + + + 200 + 16777215 + + - X: + Pedestal Subtract - + false + + + 100 + 31 + + + + + 45 + 16777215 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - + + false - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 0 + 31 + + + + Raw Data - - + + false - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 0 + 31 + + + + + 16777215 + 16777215 + + + + Min: - + + + + false + + + + 0 + 31 + + + + + 16777215 + 16777215 + + + + N Counters: + + + + + + + false + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Max: + + + + false + + + 100 + 31 + + + + + 45 + 16777215 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + false + + + 100 + 31 + + + + + 45 + 16777215 + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + false + + + + 0 + 31 + + Image Pixels: - + + + + false + + + + 100 + 31 + + + + + 45 + 16777215 + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + false + + + + 0 + 31 + + + + + 16777215 + 16777215 + + + + X: + + + + false + + + 100 + 31 + + + + + 16777215 + 16777215 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + + + + false + + + + 100 + 31 + + + + + 16777215 + 16777215 + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + + 100 + 31 + + + + + 45 + 16777215 + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + false + + + 100 + 31 + + + + + 45 + 16777215 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - + + + + false + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Plot Bit: + + + + + + + false + + + + 115 + 0 + + + + + 156 + 16777215 + + + + Max: + + + + + + + false + + + + 0 + 31 + + + + + 16777215 + 31 + + + + Y: + + + + + + + false + + + + 100 + 31 + + + + + 45 + 16777215 + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + false + + + X + + + + + false @@ -549,77 +1116,17 @@ - - + + false - Min: + Values - - - - false - - - X - - - - - - - false - - - Max: - - - - - - - false - - - Serial offset: - - - - - - - false - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - false - - - Plot Bit: - - - - - - - false - - - Raw Data - - - - + @@ -635,241 +1142,223 @@ - - - - false - - - Pedestal Subtract - - - - - - - false - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - false - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - false - - - N Counters: - - - 10 - 219 + 221 841 - 81 + 91 + + + 0 + 0 + + QFrame::StyledPanel QFrame::Raised - - - - 0 - -7 - 841 - 111 - + + + 9 - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 40 - 20 - - - - - - - - Color Range: - - - - - - - - - 0 - 0 - 121 - 91 - - - - - - - - 16777215 - 15 - - - - All - - - true - - - buttonGroup_4 - - - - - - - true - - - - 16777215 - 15 - - - - 3-97% - - - buttonGroup_4 - - - - - - - - 16777215 - 15 - - - - Fixed - - - buttonGroup_4 - - - - - - - - - - - - 50 - 16777215 - - - - min: - - - - - - - false - - - -1000.000000000000000 - - - 100000.000000000000000 - - - - - - - - 50 - 16777215 - - - - max: - - - - - - - false - - - 100000.000000000000000 - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 40 - 20 - - - - - - + + 9 + + + + + QFrame::NoFrame + + + QFrame::Plain + + + + + + true + + + + 16777215 + 15 + + + + 3-97% + + + buttonGroup_4 + + + + + + + + 16777215 + 15 + + + + All + + + true + + + buttonGroup_4 + + + + + + + + 16777215 + 15 + + + + Fixed + + + buttonGroup_4 + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + + 0 + 31 + + + + + 16777215 + 31 + + + + Color Range: + + + + + + + false + + + + 0 + 31 + + + + -1000.000000000000000 + + + 100000.000000000000000 + + + + + + + + 50 + 31 + + + + max: + + + + + + + false + + + + 0 + 31 + + + + 100000.000000000000000 + + + + + + + + 50 + 31 + + + + min: + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + 10 - 330 + 322 841 - 101 + 108 @@ -878,208 +1367,183 @@ QFrame::Raised - - - - 0 - 0 - 841 - 51 - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 40 - 20 - - - - - - - - true - - - Pedestal: - - - - - - - - 250 - 0 - - - - recorded frames: 0 - - - - - - - true - - - Record - - - true - - - false - - - buttonGroup_3 - - - - - - - true - - - Apply - - - true - - - buttonGroup_3 - - - - - - - true - - - - 0 - 31 - - - - background-color: rgb(199, 213, 207); - - - Reset - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 40 - 20 - - - - - - - - - - -1 - 49 - 841 - 51 - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 120 - 31 - - - - background-color: rgb(199, 213, 207); - - - Load Pedestal - - - - - - - - 120 - 31 - - - - background-color: rgb(199, 213, 207); - - - Save Pedestal - - - - - - - Qt::Horizontal - - - QSizePolicy::Preferred - - - - 40 - 20 - - - - - - + + + + + true + + + + 16777215 + 31 + + + + Apply + + + true + + + buttonGroup_3 + + + + + + + true + + + + 16777215 + 31 + + + + Record + + + true + + + false + + + buttonGroup_3 + + + + + + + + 0 + 0 + + + + + 16777215 + 31 + + + + recorded frames: 0 + + + + + + + true + + + + 0 + 31 + + + + background-color: rgb(199, 213, 207); + + + Reset + + + + + + + true + + + + 150 + 0 + + + + + 16777215 + 31 + + + + Pedestal: + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + + + + 120 + 31 + + + + background-color: rgb(199, 213, 207); + + + Save Pedestal + + + + + + + + 120 + 31 + + + + background-color: rgb(199, 213, 207); + + + Load Pedestal + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + + - diff --git a/pyctbgui/pyctbgui/utils/defines.py b/pyctbgui/pyctbgui/utils/defines.py index 2226b5c33..b4ecfc04e 100644 --- a/pyctbgui/pyctbgui/utils/defines.py +++ b/pyctbgui/pyctbgui/utils/defines.py @@ -35,8 +35,8 @@ class Defines: loops_count = 6 class transceiver: - count = 4 - tabIndex = 4 + maxcount = 4 + maxtabIndex = 4 class slowAdc: tabIndex = 2 diff --git a/slsDetectorCalibration/moenchExecutables/CMakeLists.txt b/slsDetectorCalibration/moenchExecutables/CMakeLists.txt index 44b212df3..55faa558d 100644 --- a/slsDetectorCalibration/moenchExecutables/CMakeLists.txt +++ b/slsDetectorCalibration/moenchExecutables/CMakeLists.txt @@ -76,6 +76,14 @@ foreach(exe ${MOENCH_EXECUTABLES}) slsProjectOptions ) + target_compile_options(${exe} PRIVATE + -Wno-unused-but-set-variable + -Wno-format-nonliteral + -Wno-format-security + -Wno-double-promotion + -Wno-unused-variable + -Wno-format-overflow) + set_target_properties(${exe} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin diff --git a/slsDetectorCalibration/moenchExecutables/moench03Interpolation.cpp b/slsDetectorCalibration/moenchExecutables/moench03Interpolation.cpp index cbbe32746..dc3e71583 100644 --- a/slsDetectorCalibration/moenchExecutables/moench03Interpolation.cpp +++ b/slsDetectorCalibration/moenchExecutables/moench03Interpolation.cpp @@ -204,9 +204,9 @@ int main(int argc, char *argv[]) { */ //#endif - if (totquad > cmin && cl.x >= xmin && cl.x <= xmax && + if (totquad > static_cast(cmin) && cl.x >= xmin && cl.x <= xmax && cl.y >= ymin && cl.y <= ymax && - totquad < cmax) { + totquad < static_cast(cmax)) { // if (sum > cmin && totquad / sum > 0.8 && totquad / sum < 1.2 && // sum < cmax) { diff --git a/slsSupportLib/include/sls/ClientSocket.h b/slsSupportLib/include/sls/ClientSocket.h index dcfc02b25..4ef8e5da4 100644 --- a/slsSupportLib/include/sls/ClientSocket.h +++ b/slsSupportLib/include/sls/ClientSocket.h @@ -21,6 +21,7 @@ class ClientSocket : public DataSocket { private: void readReply(int &ret, void *retval, size_t retval_size); + [[noreturn]] void throwError(const std::string &msg) const; struct sockaddr_in serverAddr {}; std::string socketType; }; diff --git a/slsSupportLib/include/sls/DataSocket.h b/slsSupportLib/include/sls/DataSocket.h index 31d9afa72..c8c7fc75e 100644 --- a/slsSupportLib/include/sls/DataSocket.h +++ b/slsSupportLib/include/sls/DataSocket.h @@ -77,8 +77,6 @@ class DataSocket { std::string Receive(size_t length); - int read(void *buffer, size_t size); - int write(void *buffer, size_t size); int setTimeOut(int t_seconds); int setReceiveTimeout(int us); void close(); @@ -88,6 +86,8 @@ class DataSocket { private: int sockfd_ = -1; int fnum_{0}; + + std::string_view errno_name(int e); }; }; // namespace sls diff --git a/slsSupportLib/include/sls/string_utils.h b/slsSupportLib/include/sls/string_utils.h index dc1c037ac..abeed79a7 100644 --- a/slsSupportLib/include/sls/string_utils.h +++ b/slsSupportLib/include/sls/string_utils.h @@ -96,4 +96,6 @@ bool replace_first(std::string *s, const std::string &substr, std::pair ParseHostPort(const std::string &s); +std::string to_lower(const std::string &s); + } // namespace sls diff --git a/slsSupportLib/src/ClientSocket.cpp b/slsSupportLib/src/ClientSocket.cpp index 117e7621c..6dc6aac42 100644 --- a/slsSupportLib/src/ClientSocket.cpp +++ b/slsSupportLib/src/ClientSocket.cpp @@ -5,9 +5,11 @@ #include "sls/sls_detector_defs.h" #include "sls/sls_detector_exceptions.h" #include "sls/sls_detector_funcs.h" +#include "sls/string_utils.h" #include #include #include +#include #include #include #include @@ -24,9 +26,10 @@ ClientSocket::ClientSocket(std::string stype, const std::string &host, hints.ai_flags |= AI_CANONNAME; if (getaddrinfo(host.c_str(), nullptr, &hints, &result) != 0) { - std::string msg = "ClientSocket cannot decode host:" + host + - " on port " + std::to_string(port) + "\n"; - throw SocketError(msg); + + + auto msg = fmt::format("Cannot resolve {} hostname: '{}'", to_lower(socketType), host); + throwError(msg); } // TODO! Erik, results could have multiple entries do we need to loop @@ -40,10 +43,11 @@ ClientSocket::ClientSocket(std::string stype, const std::string &host, if (::connect(getSocketId(), (struct sockaddr *)&serverAddr, sizeof(serverAddr)) != 0) { freeaddrinfo(result); - std::string msg = "ClientSocket: Cannot connect to " + socketType + - ":" + host + " on port " + std::to_string(port) + - "\n"; - throw SocketError(msg); + auto msg = fmt::format( + "Cannot connect to {} on {}:{}\n", + to_lower(socketType), host, port); + + throwError(msg); } freeaddrinfo(result); } @@ -54,10 +58,18 @@ ClientSocket::ClientSocket(std::string sType, struct sockaddr_in addr) if (::connect(getSocketId(), (struct sockaddr *)&addr, sizeof(addr)) != 0) { char address[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &addr.sin_addr, address, INET_ADDRSTRLEN); - std::string msg = "ClientSocket: Cannot connect to " + socketType + - ":" + address + " on port " + - std::to_string(addr.sin_port) + "\n"; - throw SocketError(msg); + auto msg = fmt::format("Cannot connect to {} on {}:{}", to_lower(socketType), address, addr.sin_port); + throwError(msg); + } +} + +void ClientSocket::throwError(const std::string &msg) const { + if (socketType == "Receiver") { + throw ReceiverError(msg); + } else if (socketType == "Detector") { + throw DetectorError(msg); + } else { + throw GuiError(msg); } } @@ -80,26 +92,15 @@ void ClientSocket::readReply(int &ret, void *retval, size_t retval_size) { std::string mess = readErrorMessage(); // Do we need to know hostname here? // In that case save it??? - if (socketType == "Receiver") { - throw ReceiverError("Receiver returned: " + std::string(mess)); - } else if (socketType == "Detector") { - throw DetectorError("Detector returned: " + std::string(mess)); - } else { - throw GuiError(mess); - } + throwError(socketType + " returned: " + mess); } // get retval Receive(retval, retval_size); } // debugging catch (SocketError &e) { - if (socketType == "Receiver") { - throw ReceiverError("Receiver returned: " + std::string(e.what())); - } else if (socketType == "Detector") { - throw DetectorError("Detector returned: " + std::string(e.what())); - } else { - throw GuiError(e.what()); - } + auto msg = fmt::format("While reading reply from {} {}", to_lower(socketType), e.what()); + throwError(msg); } } diff --git a/slsSupportLib/src/DataSocket.cpp b/slsSupportLib/src/DataSocket.cpp index 25d016d0b..a8b4d2bfd 100644 --- a/slsSupportLib/src/DataSocket.cpp +++ b/slsSupportLib/src/DataSocket.cpp @@ -1,12 +1,14 @@ // SPDX-License-Identifier: LGPL-3.0-or-other // Copyright (C) 2021 Contributors to the SLS Detector Package #include "sls/DataSocket.h" +#include "sls/Timer.h" #include "sls/logger.h" #include "sls/sls_detector_exceptions.h" #include "sls/sls_detector_funcs.h" #include #include #include +#include #include #include #include @@ -21,6 +23,14 @@ namespace sls { DataSocket::DataSocket(int socketId) : sockfd_(socketId) { int value = 1; setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value)); +#ifdef SO_NOSIGPIPE + // macOS/BSD: suppress SIGPIPE when sending to a peer that closed the + // connection, so a failed send returns an error instead of killing the + // process. On Linux we instead pass MSG_NOSIGNAL to send() (see Send()). + int nosigpipe = 1; + setsockopt(sockfd_, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, + sizeof(nosigpipe)); +#endif } DataSocket::~DataSocket() { @@ -36,6 +46,7 @@ DataSocket::~DataSocket() { void DataSocket::swap(DataSocket &other) noexcept { std::swap(sockfd_, other.sockfd_); + std::swap(fnum_, other.fnum_); } DataSocket::DataSocket(DataSocket &&move) noexcept { move.swap(*this); } @@ -48,23 +59,34 @@ void DataSocket::setFnum(const int fnum) { fnum_ = fnum; } int DataSocket::Receive(void *buffer, size_t size) { // TODO!(Erik) Add sleep? how many reties? - int bytes_expected = static_cast(size); // signed size - int bytes_read = 0; + ssize_t bytes_expected = static_cast(size); + ssize_t bytes_read = 0; + ssize_t this_read = 0; // last read result, kept for diagnostics + Timer timer; while (bytes_read < bytes_expected) { - auto this_read = + this_read = ::read(getSocketId(), reinterpret_cast(buffer) + bytes_read, bytes_expected - bytes_read); + if (this_read < 0 && errno == EINTR) + continue; // interrupted by a signal, retry if (this_read <= 0) break; bytes_read += this_read; } if (bytes_read == bytes_expected) { - return bytes_read; + return static_cast(bytes_read); } else { + int err = errno; // capture before any other call can clobber it std::ostringstream ss; ss << "TCP socket read " << bytes_read << " bytes instead of " << bytes_expected << " bytes (" << getFunctionNameFromEnum(static_cast(fnum_)) << ')'; + if (this_read == 0) + ss << ": connection closed by peer (EOF)"; + else if (this_read < 0) + ss << ": read error: " << std::strerror(err) << " (" + << errno_name(err) << ")"; + ss << " after " << timer.elapsed_ms() << " ms"; throw SocketError(ss.str()); } } @@ -78,38 +100,51 @@ std::string DataSocket::Receive(size_t length) { return buff; } int DataSocket::Send(const void *buffer, size_t size) { - int bytes_sent = 0; - int data_size = static_cast(size); // signed size - while (bytes_sent < (data_size)) { - auto this_send = ::write(getSocketId(), buffer, size); + ssize_t bytes_expected = static_cast(size); + ssize_t bytes_sent = 0; + ssize_t this_send = 0; // last write result, kept for diagnostics + // Linux: avoid SIGPIPE on a broken connection by using send() with + // MSG_NOSIGNAL. macOS/BSD lack the flag and use SO_NOSIGPIPE instead + // (set in the constructor). +#ifdef MSG_NOSIGNAL + const int send_flags = MSG_NOSIGNAL; +#else + const int send_flags = 0; +#endif + Timer timer; + while (bytes_sent < bytes_expected) { + this_send = ::send( + getSocketId(), + reinterpret_cast(buffer) + bytes_sent, + bytes_expected - bytes_sent, send_flags); + if (this_send < 0 && errno == EINTR) + continue; // interrupted by a signal, retry if (this_send <= 0) break; bytes_sent += this_send; } - if (bytes_sent != data_size) { + if (bytes_sent == bytes_expected) { + return static_cast(bytes_sent); + } else { + int err = errno; // capture before any other call can clobber it std::ostringstream ss; ss << "TCP socket sent " << bytes_sent << " bytes instead of " - << data_size << " bytes (" + << bytes_expected << " bytes (" << getFunctionNameFromEnum(static_cast(fnum_)) << ')'; + if (this_send < 0) + ss << ": write error: " << std::strerror(err) << " (" + << errno_name(err) << ")"; + ss << " after " << timer.elapsed_ms() << " ms"; throw SocketError(ss.str()); } - return bytes_sent; } int DataSocket::Send(const std::string &s) { return Send(&s[0], s.size()); } -int DataSocket::write(void *buffer, size_t size) { - return ::write(getSocketId(), buffer, size); -} - -int DataSocket::read(void *buffer, size_t size) { - return ::read(getSocketId(), buffer, size); -} - int DataSocket::setReceiveTimeout(int us) { timeval t{}; - t.tv_sec = 0; - t.tv_usec = us; + t.tv_sec = us / 1000000; + t.tv_usec = us % 1000000; return ::setsockopt(getSocketId(), SOL_SOCKET, SO_RCVTIMEO, &t, sizeof(struct timeval)); } @@ -140,7 +175,9 @@ int DataSocket::setTimeOut(int t_seconds) { void DataSocket::close() { if (sockfd_ > 0) { if (::close(sockfd_)) { - throw SocketError("could not close socket"); + std::stringstream ss; + ss << "could not close socket (fd: " << sockfd_ << ")"; + throw SocketError(ss.str()); } sockfd_ = -1; } else { @@ -155,4 +192,61 @@ void DataSocket::shutDownSocket() { void DataSocket::shutdown() { ::shutdown(sockfd_, SHUT_RDWR); } +std::string_view DataSocket::errno_name(int e) { + switch (e) { +#ifdef EACCES + case EACCES: + return "EACCES"; +#endif +#ifdef EAGAIN + case EAGAIN: + return "EAGAIN"; +#endif +#ifdef EBADF + case EBADF: + return "EBADF"; +#endif +#ifdef ECONNABORTED + case ECONNABORTED: + return "ECONNABORTED"; +#endif +#ifdef ECONNREFUSED + case ECONNREFUSED: + return "ECONNREFUSED"; +#endif +#ifdef ECONNRESET + case ECONNRESET: + return "ECONNRESET"; +#endif +#ifdef EINPROGRESS + case EINPROGRESS: + return "EINPROGRESS"; +#endif +#ifdef EINTR + case EINTR: + return "EINTR"; +#endif +#ifdef EINVAL + case EINVAL: + return "EINVAL"; +#endif +#ifdef EPIPE + case EPIPE: + return "EPIPE"; +#endif +#ifdef ETIMEDOUT + case ETIMEDOUT: + return "ETIMEDOUT"; +#endif +#ifdef EWOULDBLOCK +#if EWOULDBLOCK != EAGAIN + case EWOULDBLOCK: + return "EWOULDBLOCK"; +#endif +#endif + default: + return "UNKNOWN_ERRNO"; + } +} + } // namespace sls diff --git a/slsSupportLib/src/ServerInterface.cpp b/slsSupportLib/src/ServerInterface.cpp index 92e3d7a67..8e2d4a503 100644 --- a/slsSupportLib/src/ServerInterface.cpp +++ b/slsSupportLib/src/ServerInterface.cpp @@ -10,16 +10,16 @@ namespace sls { int ServerInterface::sendResult(int ret, void *retval, int retvalSize, char *mess) { - write(&ret, sizeof(ret)); + Send(&ret, sizeof(ret)); if (ret == defs::FAIL) { if (mess != nullptr) { - write(mess, MAX_STR_LENGTH); + Send(mess, MAX_STR_LENGTH); } else { LOG(logERROR) << "No error message provided for this " "failure. Will mess up TCP\n"; } } else { - write(retval, retvalSize); + Send(retval, retvalSize); } return ret; } diff --git a/slsSupportLib/src/string_utils.cpp b/slsSupportLib/src/string_utils.cpp index 1426049ff..68e389c99 100644 --- a/slsSupportLib/src/string_utils.cpp +++ b/slsSupportLib/src/string_utils.cpp @@ -75,4 +75,11 @@ std::pair ParseHostPort(const std::string &s) { return std::make_pair(host, port); } +std::string to_lower(const std::string &s) { + std::string result = s; + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char c) { return std::tolower(c); }); + return result; +} + }; // namespace sls \ No newline at end of file diff --git a/slsSupportLib/tests/test-Sockets.cpp b/slsSupportLib/tests/test-Sockets.cpp index b41c44cb4..211df5858 100644 --- a/slsSupportLib/tests/test-Sockets.cpp +++ b/slsSupportLib/tests/test-Sockets.cpp @@ -3,37 +3,117 @@ #include "catch.hpp" #include "sls/ClientSocket.h" #include "sls/ServerSocket.h" +#include "sls/Timer.h" +#include "sls/sls_detector_defs.h" +#include "sls/sls_detector_exceptions.h" +#include "sls/sls_detector_funcs.h" +#include #include #include #include +#include +#include +#include #include +#include namespace sls { -std::vector server() { - std::cout << "starting server\n"; - auto server = ServerSocket(1950); +// One configurable test server: accept a connection, read a 100-byte request, +// reply with `bytes_to_send` bytes (first two set to 'O','K'), optionally wait +// `hold` so the client can time out, then close. Returns the received request. +std::vector echo_server(uint16_t port, size_t bytes_to_send, + std::chrono::milliseconds hold) { + std::cout << "starting server on port " << port << '\n'; + auto server = ServerSocket(port); auto s = server.accept(); std::vector buffer(100, '\0'); s.Receive(buffer.data(), buffer.size()); - std::cout << "ServerReceived: " << std::string(buffer.begin(), buffer.end()) - << '\n'; - std::vector to_send(100, '\0'); - to_send[0] = 'O'; - to_send[1] = 'K'; - s.Send(to_send.data(), to_send.size()); + if (port==1960){ + struct linger ling = { + .l_onoff = 1, + .l_linger = 0 + }; + + auto fd = s.getSocketId(); + setsockopt(fd, SOL_SOCKET, SO_LINGER, &ling, sizeof ling); + ::close(fd); + return buffer; + } + + if (bytes_to_send > 0) { + std::vector to_send(bytes_to_send, '\0'); + to_send[0] = 'O'; + to_send[1] = 'K'; + s.Send(to_send.data(), to_send.size()); + } + std::this_thread::sleep_for(hold); s.close(); return buffer; } +// Minimal command server speaking the same protocol as sendCommandThenRead: +// accept a connection, read the function number and a single int argument, +// then reply via ServerInterface::sendResult with OK and (arg * 2). Returns +// the {fnum, arg} pair the server received so the test can verify them. +std::pair command_server(uint16_t port) { + auto server = ServerSocket(port); + auto s = server.accept(); + int fnum = -1; + int arg = 0; + s.Receive(&fnum, sizeof(fnum)); + s.Receive(&arg, sizeof(arg)); + int retval = arg * 2; + s.sendResult(slsDetectorDefs::OK, &retval, sizeof(retval)); + s.close(); + return {fnum, arg}; +} + +// Command server that replies with a truncated message: it reads the request, +// sends the OK return code, but then sends fewer retval bytes than the client +// expects before closing, so the client's Receive hits EOF mid-read. +void short_reply_server(uint16_t port, size_t retval_bytes_to_send) { + auto server = ServerSocket(port); + auto s = server.accept(); + int fnum = -1; + int arg = 0; + s.Receive(&fnum, sizeof(fnum)); + s.Receive(&arg, sizeof(arg)); + int ret = slsDetectorDefs::OK; + s.Send(&ret, sizeof(ret)); + if (retval_bytes_to_send > 0) { + std::vector partial(retval_bytes_to_send, '\0'); + s.Send(partial.data(), partial.size()); + } + s.close(); +} + +// Server that accepts a connection but never reads from it, so a client +// trying to send more than fits in the kernel buffers will stall. A small +// receive buffer keeps the amount the test must send modest. Stays open until +// the client signals it is done (or a safety timeout) so it never closes +// mid-transfer and races the client's Send. +void non_reading_server(uint16_t port, std::atomic *client_done) { + auto server = ServerSocket(port); + auto s = server.accept(); + int rcvbuf = 1024; + setsockopt(s.getSocketId(), SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); + // Intentionally never Receive(): let the client's send path back up. + Timer t; + while (!client_done->load() && t.elapsed_ms() < 10000) + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + s.close(); +} + TEST_CASE("The server recive the same message as we send", "[support]") { std::vector received_message(100, '\0'); std::vector sent_message(100, '\0'); const char m[]{"some message"}; std::copy(std::begin(m), std::end(m), sent_message.data()); - auto s = std::async(std::launch::async, server); + auto s = std::async(std::launch::async, echo_server, 1950, 100, + std::chrono::milliseconds(0)); std::this_thread::sleep_for(std::chrono::milliseconds(100)); auto client = DetectorSocket("localhost", 1950); client.Send(sent_message.data(), sent_message.size()); @@ -48,6 +128,207 @@ TEST_CASE("The server recive the same message as we send", "[support]") { TEST_CASE("throws on no server", "[support]") { CHECK_THROWS(DetectorSocket("localhost", 1950)); + CHECK_THROWS(ReceiverSocket("localhost", 1950)); + CHECK_THROWS(GuiSocket("localhost", 1950)); +} + +TEST_CASE("Receiving a too short message throws and reports EOF", "[support]") { + std::vector received_message(100, '\0'); + std::vector sent_message(100, '\0'); + const char m[]{"some message"}; + std::copy(std::begin(m), std::end(m), sent_message.data()); + + // Server replies with only 10 of the 100 expected bytes, then closes. + auto s = std::async(std::launch::async, echo_server, 1951, 10, + std::chrono::milliseconds(0)); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + auto client = DetectorSocket("localhost", 1951); + client.Send(sent_message.data(), sent_message.size()); + + client.setFnum(F_GET_SERVER_VERSION); + + // The server only sends 10 of the 100 expected bytes and then closes the + // connection, so Receive must throw a SocketError reporting the EOF. + std::string error_message; + try { + client.Receive(received_message.data(), received_message.size()); + FAIL("Receive should have thrown on a too short message"); + } catch (const SocketError &e) { + error_message = e.what(); + } + client.close(); + s.get(); + + CHECK_THAT(error_message, + Catch::Matchers::Contains("read 10 bytes instead of 100 bytes")); + CHECK_THAT(error_message, + Catch::Matchers::Contains("connection closed by peer (EOF)")); +} + +TEST_CASE("Receiving with a socket error throws and reports the error", + "[support]") { + std::vector received_message(100, '\0'); + std::vector sent_message(100, '\0'); + const char m[]{"some message"}; + std::copy(std::begin(m), std::end(m), sent_message.data()); + + // Server stays silent (sends nothing) but keeps the connection open long + // enough for the client to time out, so the read fails with an error. + auto s = std::async(std::launch::async, echo_server, 1952, 0, + std::chrono::milliseconds(500)); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + auto client = DetectorSocket("localhost", 1952); + client.Send(sent_message.data(), sent_message.size()); + + // Force read() to fail with EAGAIN/EWOULDBLOCK instead of returning EOF by + // setting a short receive timeout while the server stays silent. + client.setReceiveTimeout(100000); // 100 ms + + std::string error_message; + try { + client.Receive(received_message.data(), received_message.size()); + FAIL("Receive should have thrown on a socket error"); + } catch (const SocketError &e) { + error_message = e.what(); + } + client.close(); + s.get(); + + CHECK_THAT(error_message, + Catch::Matchers::Contains("read 0 bytes instead of 100 bytes")); + CHECK_THAT(error_message, Catch::Matchers::Contains("read error:")); +} + + +TEST_CASE("Socket crash?", "[support]") { + std::vector received_message(100, '\0'); + std::vector sent_message(100, '\0'); + const char m[]{"some message"}; + std::copy(std::begin(m), std::end(m), sent_message.data()); + + auto s = std::async(std::launch::async, echo_server, 1960, 100, + std::chrono::milliseconds(0)); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + auto client = DetectorSocket("localhost", 1960); + client.Send(sent_message.data(), sent_message.size()); + + + REQUIRE_THROWS(client.Receive(received_message.data(), received_message.size())); + + //Now try to send more + // client.Send(sent_message.data(), sent_message.size()); + + + +} + + +TEST_CASE("ClientSocket throws on invalid hostname", "[support]") { + CHECK_THROWS(ReceiverSocket("invalidhostname", 1950)); + CHECK_THROWS(DetectorSocket("invalidhostname", 1950)); + CHECK_THROWS(GuiSocket("invalidhostname", 1950)); +} + + +TEST_CASE("Using DetectorSocket to talk to a Server Socket", "[support]") { + constexpr uint16_t port = 1961; + constexpr int fnum = F_GET_DETECTOR_TYPE; + constexpr int arg = 21; + + auto s = std::async(std::launch::async, command_server, port); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto client = DetectorSocket("localhost", port); + int retval = 0; + int ret = + client.sendCommandThenRead(fnum, &arg, sizeof(arg), &retval, + sizeof(retval)); + client.close(); + + auto server_received = s.get(); + + // Client got OK and the expected return value back from the server + CHECK(ret == slsDetectorDefs::OK); + CHECK(retval == arg * 2); + // Server received the function number and argument we sent + CHECK(server_received.first == fnum); + CHECK(server_received.second == arg); + // close() resets the underlying fd + CHECK(client.getSocketId() == -1); +} + +TEST_CASE("ServerSocket replies with a too short message", "[support]") { + constexpr uint16_t port = 1962; + constexpr int fnum = F_GET_DETECTOR_TYPE; + constexpr int arg = 21; + + // Server sends the OK return code, then only 1 of the 4 expected retval + // bytes before closing. + auto s = std::async(std::launch::async, short_reply_server, port, 1); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto client = DetectorSocket("localhost", port); + int retval = 0; + + std::string error_message; + try { + client.sendCommandThenRead(fnum, &arg, sizeof(arg), &retval, + sizeof(retval)); + FAIL("sendCommandThenRead should have thrown on a too short message"); + } catch (const DetectorError &e) { + error_message = e.what(); + } + client.close(); + s.get(); + + // The client read only 1 of the 4 expected retval bytes and then hit EOF. + CHECK_THAT(error_message, + Catch::Matchers::Contains("read 1 bytes instead of 4 bytes")); + CHECK_THAT(error_message, + Catch::Matchers::Contains("connection closed by peer (EOF)")); +} + +TEST_CASE("Client cannot send the expected number of bytes", "[support]") { + constexpr uint16_t port = 1963; + + // Server accepts but never reads; it stays open until we tell it the + // client is done, so it cannot close mid-transfer. + std::atomic client_done{false}; + auto s = std::async(std::launch::async, non_reading_server, port, + &client_done); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto client = DetectorSocket("localhost", port); + + // Shrink the send buffer and add a short send timeout so the write stalls + // and returns before all the data is sent. + int sndbuf = 4096; + setsockopt(client.getSocketId(), SOL_SOCKET, SO_SNDBUF, &sndbuf, + sizeof(sndbuf)); + struct timeval tv {}; + tv.tv_sec = 0; + tv.tv_usec = 300000; // 300 ms + setsockopt(client.getSocketId(), SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + + // Much more than fits in the (shrunken) send + receive buffers, so the + // send cannot complete while the server refuses to read. + std::vector big_message(8 * 1024 * 1024, '\0'); + + std::string error_message; + try { + client.Send(big_message.data(), big_message.size()); + FAIL("Send should have thrown when it could not send all bytes"); + } catch (const SocketError &e) { + error_message = e.what(); + } + client_done = true; + client.close(); + s.get(); + + // Fewer bytes were sent than expected, reported as a write error (the + // send timed out with EAGAIN/EWOULDBLOCK). + CHECK_THAT(error_message, Catch::Matchers::Contains("bytes instead of")); + CHECK_THAT(error_message, Catch::Matchers::Contains("write error:")); } } // namespace sls diff --git a/slsSupportLib/tests/test-string_utils.cpp b/slsSupportLib/tests/test-string_utils.cpp index 15dcb7dbc..21200eca9 100644 --- a/slsSupportLib/tests/test-string_utils.cpp +++ b/slsSupportLib/tests/test-string_utils.cpp @@ -123,6 +123,33 @@ TEST_CASE("port missing") { REQUIRE(res.second == 0); } +TEST_CASE("to_lower converts uppercase to lowercase") { + REQUIRE(to_lower("HELLO") == "hello"); + REQUIRE(to_lower("Hello World") == "hello world"); +} + +TEST_CASE("to_lower leaves an already lowercase string unchanged") { + REQUIRE(to_lower("already lower") == "already lower"); +} + +TEST_CASE("to_lower only affects alphabetic characters") { + REQUIRE(to_lower("ABC123!?_-XYZ") == "abc123!?_-xyz"); +} + +TEST_CASE("to_lower on an empty string returns an empty string") { + REQUIRE(to_lower("").empty()); +} + +TEST_CASE("to_lower does not modify the original string") { + std::string original = "MixedCase"; + auto result = to_lower(original); + REQUIRE(result == "mixedcase"); + // the source string must be untouched + REQUIRE(original == "MixedCase"); +} + + + // TEST_CASE("concat things not being strings") } // namespace sls