From 54e20316c95d5692514f86da4ddd30abd64cbc4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnter=20Quast?= <guenter.quast@online.de> Date: Sun, 16 Jun 2024 18:10:00 +0200 Subject: [PATCH] renamed and extended main app: redPdaq.py --- redP_mimoCoRB.py | 2 +- redPoscdaq.py => redPdaq.py | 492 +++++++++++++++++++++++++++++++++--- 2 files changed, 460 insertions(+), 34 deletions(-) rename redPoscdaq.py => redPdaq.py (66%) diff --git a/redP_mimoCoRB.py b/redP_mimoCoRB.py index 46e65ad..c729eea 100644 --- a/redP_mimoCoRB.py +++ b/redP_mimoCoRB.py @@ -7,7 +7,7 @@ via callback of function data_sink() in class redP_mimoCoRB. import time import sys -import redPoscdaq as rp +import redPdaq as rp from mimocorb.buffer_control import rbPut class redP_mimocorb(): diff --git a/redPoscdaq.py b/redPdaq.py similarity index 66% rename from redPoscdaq.py rename to redPdaq.py index 73513ec..6bcbfba 100755 --- a/redPoscdaq.py +++ b/redPdaq.py @@ -3,15 +3,17 @@ application running on a RedPitaya FPGA board Contains a button to run the oscilloscope in daq mode, i.e. it is restarted - continously. Triggered wave forms can be exported via calling a callback - function stored in a numpy binary file in .npy format. + continously. If defined, data is exported via calling a callback function. + Optionally, triggered waveforms can be stored a numpy binary file + (.npy format). - This code is derived from mcpha.py by Pavel Demin and - compatible with release 20240204 of the alpine linux image + Code derived from mcpha.py by Pavel Demin + + This code is compatible with release 20240204 of the alpine linux image https://github.com/pavel-demin/red-pitaya-notes/releases/tag/20240204 """ -script_name = 'redPoscidaq.py' +script_name = 'redPdaq.py' # Communication with server process is achieved via command codes: # command(code, number, data) # number typically is channel number @@ -22,7 +24,7 @@ script_name = 'redPoscidaq.py' # code 11: read status # code 13: set trigger source chan 1/2 # code 14: set trigger slope rise/fall -# code 15: set trigger mode norm/auto +# code 15: set trigger mode norm/autoUi_HstDisplay, QWidget = loadUiType("mcpha_hst.ui") # code 16: set trigger level in ADC counts # code 17: set number of pre-trigger samples # code 18: set number of total samples @@ -80,6 +82,7 @@ plt.style.use(_style) Ui_MCPHA, QMainWindow = loadUiType("rpControl.ui") Ui_LogDisplay, QWidget = loadUiType("mcpha_log.ui") +Ui_HstDisplay, QWidget = loadUiType("mcpha_hst.ui") Ui_OscDisplay, QWidget = loadUiType("mcpha_daq.ui") Ui_GenDisplay, QWidget = loadUiType("mcpha_gen.ui") @@ -116,25 +119,38 @@ class rpControl(QMainWindow, Ui_MCPHA): self.confd = {} if conf_dict is None else conf_dict self.parse_confd() # get configuration from command line - if os.path.split(sys.argv[0])[1]=='redPoscdaq.py': + if os.path.split(sys.argv[0])[1] == script_name: + self.interactive = True self.parse_args() + else: + self.interactive = False self.callback = True if callback is not None else False # set physical units (for axis labels) self.get_physical_units() self.setupUi(self) # initialize variables - self.osc_ready = False self.idle = True + self.hst_waiting = [False for i in range(2)] + self.osc_ready = False self.osc_waiting = False + self.hst_reset = 0 self.state = 0 self.status = np.zeros(9, np.uint32) self.timers = self.status[:4].view(np.uint64) # create tabs self.log = LogDisplay() + if self.interactive: + self.hst1 = HstDisplay(self, self.log, 0) + self.hst2 = HstDisplay(self, self.log, 1) + else: + self.hst1 = None + self.hst2 = None self.osc_daq = OscDAQ(self, self.log) self.gen = GenDisplay(self, self.log) self.tabindex_log = self.tabWidget.addTab(self.log, "Messages") + self.tabWidget.addTab(self.hst1, "Spectrum Channel 1") + self.tabWidget.addTab(self.hst2, "Spectrum Channel 2") self.tabindex_osc = self.tabWidget.addTab(self.osc_daq, "Oscilloscope") self.tabindex_gen = self.tabWidget.addTab(self.gen, "Pulse generator") # configure controls @@ -163,7 +179,8 @@ class rpControl(QMainWindow, Ui_MCPHA): self.startTimer = QTimer(self) # timer for network connectin self.startTimer.timeout.connect(self.start_timeout) self.readTimer = QTimer(self) # for readout - self.readTimer.timeout.connect(self.update_oscData) + # self.readTimer.timeout.connect(self.update_oscDisplay) + self.readTimer.timeout.connect(self.read_timeout) # transfer command-line arguments to gui self.osc_daq.levelValue.setValue(self.trigger_level) @@ -243,7 +260,6 @@ class rpControl(QMainWindow, Ui_MCPHA): if self.filename: # data recording with npy_append_array() import_npy_append_array() - def get_physical_units(self): """get physical units corresponding to ADC units and channel numbers @@ -299,11 +315,13 @@ class rpControl(QMainWindow, Ui_MCPHA): self.osc_reset = False self.osc_start = False self.state = 0 + self.hst_reset = 0 + self.hst_waiting = [False for i in range(2)] self.set_rate(self.rateValue.currentIndex()) self.set_negator(0, self.neg1Check.isChecked()) self.set_negator(1, self.neg2Check.isChecked()) # - # finally, start readout timer calling update_oscData() + # finally, start readout timer calling update_oscData() read_timeout() self.readTimer.start(self.readInterval) #### start oscilloscope in DAQ mode @@ -325,36 +343,66 @@ class rpControl(QMainWindow, Ui_MCPHA): view[:] = np.frombuffer(self.socket.read(size), np.uint8) return True - def update_oscData(self): - """get new data from RP and update oscilloscope display (triggered by QTimer:readTimer) + def read_timeout(self): + """data transfer from RP, triggered by QTimer readTimer """ - if not self.osc_waiting: - return + # send reset commands for histograms + if self.hst_reset & 1: + self.command(0, 0, 0) # hst 1 + if self.hst_reset & 2: + self.command(0, 1, 0) # hst 2 + if self.hst_reset & 4: + self.command(1, 0, 0) # reset timer 1 + if self.hst_reset & 8: + self.command(1, 1, 0) # reset timer 2 + self.hst_reset = 0 if self.osc_reset: self.reset_osc() if self.osc_start: self.start_osc() - # check status - self.read_status() - if not self.read_data(self.status): - self.log.print("failed to read status") - return - if not self.status[8] & 1: - self.command(20, 0, 0) # read oscilloscope data - if self.read_data(self.osc_daq.buffer): - self.osc_daq.update_osci_display() - self.mark_reset_osc() - self.mark_start_osc() - else: - self.log.print("failed to read oscilloscope data") + # read histogram and oscilloscope data and update displays if not in daq mode + if not self.daq_waiting: + self.command(11, 0, 0) # read status + if not self.read_data(self.status): return + # spectrum ch1 + if self.hst_waiting[0]: + self.command(12, 0, 0) # code 12: read histogram + if self.read_data(self.hst1.buffer): + self.hst1.update(self.timers[0], False) + else: + return + # spectrum ch2 + if self.hst_waiting[1]: + self.command(12, 1, 0) + if self.read_data(self.hst2.buffer): + self.hst2.update(self.timers[1], False) + else: + return + # oscilloscope + if self.osc_waiting and not self.status[8] & 1: + self.command(20, 0, 0) # read oscilloscope data + if self.read_data(self.osc_daq.buffer): + self.osc_daq.update_osci_display() + self.mark_reset_osc() + self.mark_start_osc() + else: + self.log.print("failed to read oscilloscope data") + return 1 + def run_oscDaq(self): - """continuous data transfer from RedPitaya + """continuous fast data transfer from RedPitaya via mcpha oscilloscope """ # depends on # self.start_daq() # osc.process_data() + + # presently, not compatible with historgram mode, so pause + if self.hst_waiting[0]: + self.hst1.pause() + if self.hst_waiting[1]: + self.hst2.pause() # initialize trigger mode etc. self.osc_daq.start_daq() # @@ -379,6 +427,26 @@ class rpControl(QMainWindow, Ui_MCPHA): else: self.log.print("failed to read oscilloscope data") return + + def reset_hst(self, number): + self.hst_reset |= 1 << number + + def reset_timer(self, number): + self.hst_reset |= 4 << number + + def set_pha_delay(self, number, value): + self.command(6, number, value) + + def set_pha_thresholds(self, number, min, max): + self.command(7, number, min) + self.command(8, number, max) + + def set_timer(self, number, value): + self.command(9, number, value) + + def set_timer_mode(self, number, value): + self.command(10, number, value) + self.hst_waiting[number] = value def mark_start_osc(self): self.osc_start = True @@ -389,7 +457,7 @@ class rpControl(QMainWindow, Ui_MCPHA): def reset_osc(self): self.command(2, 0, 0) - self.osc_reset=False + self.osc_reset = False def start_osc(self): self.command(19, 0, 0) @@ -461,6 +529,355 @@ class LogDisplay(QWidget, Ui_LogDisplay): def print(self, text): self.logViewer.appendPlainText(text) +class HstDisplay(QWidget, Ui_HstDisplay): + def __init__(self, rpControl, log, number): + super(HstDisplay, self).__init__() + self.setupUi(self) + # initialize variables + self.rpControl = rpControl + self.log = log + self.number = number + self.sum = 0 + self.time = np.uint64([75e8, 0]) + self.factor = 1 + self.bins = 4096 + self.min = 0 + self.max = self.bins - 1 + self.buffer = np.zeros(self.bins, np.uint32) + if number == 0: + self.color = "#FFAA00" + else: + self.color = "#00CCCC" + # create figure + self.figure = Figure() + if sys.platform != "win32": + self.figure.set_facecolor("none") + # gq self.figure.subplots_adjust(left=0.18, bottom=0.08, right=0.98, top=0.95) + self.figure.subplots_adjust(left=0.10, bottom=0.08, right=0.98, top=0.92) + self.canvas = FigureCanvas(self.figure) + self.plotLayout.addWidget(self.canvas) + self.ax = self.figure.add_subplot(111) + self.ax.grid() + self.ax.set_ylabel("counts") + self.xunit = "[{:.3f} mV / channel]".format(1000/4096 * self.factor) + self.ax.set_xlabel("channel number " + self.xunit) +# self.ax.set_xlabel("channel number") + # !gq + self.ax_x2=self.ax.secondary_xaxis('top', + functions=(self.adc2mV, self.mV2adc)) + self.ax_x2.set_xlabel("Voltage [mV]", color='grey') + # !gq end + x = np.arange(self.bins) + (self.curve,) = self.ax.plot(x, self.buffer, drawstyle="steps-mid", color=self.color) + self.roi = [0, self.max] + self.line = [None, None] + self.active = [False, False] + self.releaser = [None, None] + # marker lines for roi + self.line[0] = self.ax.axvline(self.min, picker=True, pickradius=5) + self.line[1] = self.ax.axvline(self.max, picker=True, pickradius=5) + # create navigation toolbar + self.toolbar = NavigationToolbar(self.canvas, None, False) + self.toolbar.layout().setSpacing(6) + # remove subplots action + actions = self.toolbar.actions() + self.toolbar.removeAction(actions[6]) + self.toolbar.removeAction(actions[7]) + self.logCheck = QCheckBox("log scale") + self.logCheck.setChecked(False) + self.binsLabel = QLabel("rebin factor") + self.binsValue = QComboBox() + self.binsValue.addItems(["1", "2", "4", "8"]) + self.binsValue.setEditable(True) + self.binsValue.lineEdit().setReadOnly(True) + self.binsValue.lineEdit().setAlignment(Qt.AlignRight) + for i in range(self.binsValue.count()): + self.binsValue.setItemData(i, Qt.AlignRight, Qt.TextAlignmentRole) + self.toolbar.addSeparator() + self.toolbar.addWidget(self.logCheck) + self.toolbar.addSeparator() + self.toolbar.addWidget(self.binsLabel) + self.toolbar.addWidget(self.binsValue) + self.plotLayout.addWidget(self.toolbar) + # configure controls + actions[0].triggered.disconnect() + actions[0].triggered.connect(self.home) + self.logCheck.toggled.connect(self.set_scale) + self.binsValue.currentIndexChanged.connect(self.set_bins) + self.thrsCheck.toggled.connect(self.set_thresholds) + self.startButton.clicked.connect(self.start) + self.resetButton.clicked.connect(self.reset) + self.saveButton.clicked.connect(self.save) + self.loadButton.clicked.connect(self.load) + self.canvas.mpl_connect("motion_notify_event", self.on_motion) + self.canvas.mpl_connect("pick_event", self.on_pick) + # update controls + self.set_thresholds(self.thrsCheck.isChecked()) + self.set_time(self.time[0]) + self.set_scale(self.logCheck.isChecked()) + + # !gq helper functions to convert adc counts to physical units + def adc2mV(self, c): + """convert adc-count to Voltage in mV + """ + return c * self.rpControl.adc_unit * self.factor + + def mV2adc(self, v): + """convert voltage in mV to adc-count + """ + return v / (self.rpControl.adc_unit * self.factor) + # !gq end helper + + def start(self): + if self.rpControl.idle: + return + self.set_thresholds(self.thrsCheck.isChecked()) + self.set_enabled(False) + h = self.hoursValue.value() + m = self.minutesValue.value() + s = self.secondsValue.value() + value = (h * 3600000 + m * 60000 + s * 1000) * 125000 + self.sum = 0 + self.time[:] = [value, 0] + self.rpControl.reset_timer(self.number) + self.rpControl.set_pha_delay(self.number, 100) + self.rpControl.set_pha_thresholds(self.number, self.min, self.max) + self.rpControl.set_timer(self.number, value) +# + self.resume() + + def pause(self): + self.rpControl.set_timer_mode(self.number, 0) + self.startButton.setText("Resume") + self.startButton.clicked.disconnect() + self.startButton.clicked.connect(self.resume) + self.log.print("timer %d stopped" % (self.number + 1)) + + def resume(self): + self.rpControl.set_timer_mode(self.number, 1) + self.startButton.setText("Pause") + self.startButton.clicked.disconnect() + self.startButton.clicked.connect(self.pause) + self.log.print("timer %d started" % (self.number + 1)) + + def stop(self): + self.rpControl.set_timer_mode(self.number, 0) + self.set_enabled(True) + self.set_time(self.time[0]) + self.startButton.setText("Start") + self.startButton.clicked.disconnect() + self.startButton.clicked.connect(self.start) + self.log.print("timer %d stopped" % (self.number + 1)) + + def reset(self): + if self.rpControl.idle: + return + self.stop() + self.rpControl.reset_hst(self.number) + self.rpControl.reset_timer(self.number) + self.totalValue.setText("%.2e" % 0) + self.instValue.setText("%.2e" % 0) + self.avgValue.setText("%.2e" % 0) + self.buffer[:] = np.zeros(self.bins, np.uint32) + self.update_plot() + self.update_roi() + + def set_enabled(self, value): + if value: + self.set_thresholds(self.thrsCheck.isChecked()) + else: + self.minValue.setEnabled(False) + self.maxValue.setEnabled(False) + self.thrsCheck.setEnabled(value) + self.hoursValue.setEnabled(value) + self.minutesValue.setEnabled(value) + self.secondsValue.setEnabled(value) + + def home(self): + self.set_scale(self.logCheck.isChecked()) + + def set_xplot_range(self): + """set x-range for histogram plot + """ + mn = self.min // self.factor + mx = self.max // self.factor + xsize = mx - mn + self.ax.set_xlim(mn - 0.02*xsize, mx*1.02) + self.ax.relim() + self.ax.autoscale_view(scalex=True, scaley=True) + + def set_scale(self, checked): + self.toolbar.home() + self.toolbar.update() + if checked: + self.ax.set_ylim(1, 1e10) + self.ax.set_yscale("log") + else: + self.ax.set_ylim(auto=True) + self.ax.set_yscale("linear") + # !gq size = self.bins // self.factor + # self.ax.set_xlim(-0.05 * size, size * 1.05) + self.set_xplot_range() + self.canvas.draw() + + def set_bins(self, value): + factor = 1 << value + self.factor = factor + bins = self.bins // self.factor + self.xunit = "[{:.3f} mV / channel]".format(1000/4096 * self.factor) + self.ax.set_xlabel("channel number " + self.xunit) + x = np.arange(bins) + y = self.buffer.reshape(-1, self.factor).sum(-1) + self.curve.set_xdata(x) + self.curve.set_ydata(y) + self.set_scale(self.logCheck.isChecked()) + # gq update roi + self.roi[0] = self.min + self.roi[1] = self.max + self.update_roi() + + def set_thresholds(self, checked): + self.minValue.setEnabled(checked) + self.maxValue.setEnabled(checked) + if checked: + self.min = self.minValue.value() + self.max = self.maxValue.value() + else: + self.min = 0 + self.max = self.bins - 1 + + # !gq update xplot range and roi + self.set_xplot_range() + self.roi[0] = self.min + self.roi[1] = self.max + self.update_roi() + + def set_time(self, value): + value = value // 125000 + h, mod = divmod(value, 3600000) + m, mod = divmod(mod, 60000) + s = mod / 1000 + self.hoursValue.setValue(int(h)) + self.minutesValue.setValue(int(m)) + self.secondsValue.setValue(s) + + def update(self, value, sync): + self.update_rate(value) + if not sync: + self.update_time(value) + self.update_plot() + self.update_roi() + + def update_rate(self, value): + sum = self.buffer.sum() + self.totalValue.setText("%.2e" % sum) + if value > self.time[1]: + rate = (sum - self.sum) / (value - self.time[1]) * 125e6 + self.instValue.setText("%.2e" % rate) + if value > 0: + rate = sum / value * 125e6 + self.avgValue.setText("%.2e" % rate) + self.sum = sum + self.time[1] = value + + def update_time(self, value): + if value < self.time[0]: + self.set_time(self.time[0] - value) + else: + self.stop() + + def update_plot(self): + y = self.buffer.reshape(-1, self.factor).sum(-1) + self.curve.set_ydata(y) + self.ax.relim() + self.ax.autoscale_view(scalex=False, scaley=True) + self.canvas.draw() + + def update_roi(self): + y = self.buffer.reshape(-1, self.factor).sum(-1) + x0 = self.roi[0] // self.factor + x1 = self.roi[1] // self.factor + roi = y[x0 : x1 + 1] + y0 = roi[0] + y1 = roi[-1] + tot = roi.sum() + bkg = (x1 + 1 - x0) * (y0 + y1) / 2.0 + self.roistartValue.setText("%d" % x0) + self.roiendValue.setText("%d" % x1) + self.roitotValue.setText("%.2e" % tot) + self.roibkgValue.setText("%.2e" % bkg) + self.line[0].set_xdata([x0, x0]) + self.line[1].set_xdata([x1, x1]) + self.canvas.draw_idle() + + def on_motion(self, event): + if event.inaxes != self.ax: + return + x = int(event.xdata + 0.5) + if x < 0: + x = 0 + if x >= self.bins // self.factor: + x = self.bins // self.factor - 1 + y = self.curve.get_ydata(True)[x] + self.numberValue.setText("%d" % x) + self.entriesValue.setText("%d" % y) + delta = 40 + if self.active[0]: + x0 = x * self.factor + if x0 > self.roi[1] - delta: + self.roi[0] = self.roi[1] - delta + else: + self.roi[0] = x0 + self.update_roi() + if self.active[1]: + x1 = x * self.factor + if x1 < self.roi[0] + delta: + self.roi[1] = self.roi[0] + delta + else: + self.roi[1] = x1 + self.update_roi() + + def on_pick(self, event): + for i in range(2): + if event.artist == self.line[i]: + self.active[i] = True + self.releaser[i] = self.canvas.mpl_connect("button_release_event", partial(self.on_release, i)) + + def on_release(self, i, event): + self.active[i] = False + self.canvas.mpl_disconnect(self.releaser[i]) + + def save(self): + try: + dialog = QFileDialog(self, "Save hst file", path, "*.hst") + dialog.setDefaultSuffix("hst") + timestamp = time.strftime("%Y%m%d-%H%M%S") + name = "histogram-%s.hst" % timestamp + dialog.selectFile(name) + dialog.setAcceptMode(QFileDialog.AcceptSave) + if dialog.exec() == QDialog.Accepted: + name = dialog.selectedFiles() + np.savetxt(name[0], + self.buffer, + fmt="%u", + header ='mcpha spectrum ' + timestamp + '\n counts per channel', + newline=os.linesep) + self.log.print("histogram %d saved to file %s" % ((self.number + 1), name[0])) + except: + self.log.print("error: %s" % sys.exc_info()[1]) + + def load(self): + try: + dialog = QFileDialog(self, "Load hst file", path, "*.hst") + dialog.setDefaultSuffix("hst") + dialog.setAcceptMode(QFileDialog.AcceptOpen) + if dialog.exec() == QDialog.Accepted: + name = dialog.selectedFiles() + self.buffer[:] = np.loadtxt(name[0], np.uint32) + self.update_plot() + except: + self.log.print("error: %s" % sys.exc_info()[1]) + class OscDAQ(QWidget, Ui_OscDisplay): """Oscilloscope display with daq mode: @@ -598,11 +1015,15 @@ class OscDAQ(QWidget, Ui_OscDisplay): self.rpControl.set_osc_tot(self.l_tot) def set_gui4start(self): - self.startButton.setText("Start") + self.startButton.setText("Start monitor") self.startButton.setStyleSheet("") self.startButton.clicked.disconnect() self.startButton.clicked.connect(self.start) self.startDAQButton.setEnabled(True) + if self.rpControl.hst1 is not None: + self.rpControl.hst1.startButton.setEnabled(True) + if self.rpControl.hst2 is not None: + self.rpControl.hst2.startButton.setEnabled(True) def set_gui4stop(self): # set start and stop buttons @@ -611,7 +1032,7 @@ class OscDAQ(QWidget, Ui_OscDisplay): self.startButton.clicked.disconnect() self.startButton.clicked.connect(self.stop) self.startDAQButton.setEnabled(False) - + def start(self): """start oscilloscope display """ @@ -643,6 +1064,11 @@ class OscDAQ(QWidget, Ui_OscDisplay): # set-up trigger and GUI Buttons self.setup_trigger() self.set_gui4stop() + # disable spectrum update + if self.rpControl.hst1 is not None: + self.rpControl.hst1.startButton.setEnabled(False) + if self.rpControl.hst2 is not None: + self.rpControl.hst2.startButton.setEnabled(False) self.rpControl.daq_waiting = True self.log.print("daq started") @@ -671,7 +1097,7 @@ class OscDAQ(QWidget, Ui_OscDisplay): with NpyAppendArray(self.filename) as npa: npa.append( np.array([self.data]) ) # - # output status and update scope display once per second + # output status and update scope display once per readInterval if 1000*self.dT >= self.rpControl.readInterval: dN = self.NTrig - self.Nprev r = dN/self.dT -- GitLab