From 1b958cf9bde57bc257e320e4e4c6b6c942285f97 Mon Sep 17 00:00:00 2001
From: Guenter Quast <guenter.quast@online.de>
Date: Sat, 1 Jun 2024 12:43:35 +0200
Subject: [PATCH] redPoscdaq: data acquistition with special version of MCPHA
 oscilloscope

---
 redPoscdaq.py | 783 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 783 insertions(+)
 create mode 100755 redPoscdaq.py

diff --git a/redPoscdaq.py b/redPoscdaq.py
new file mode 100755
index 0000000..7ece4d0
--- /dev/null
+++ b/redPoscdaq.py
@@ -0,0 +1,783 @@
+#!/usr/bin/env python3
+"""rpPoscidaq: fast data acquistion using the oscilloscope client of the MCPHA 
+    application running on a RedPitaya FPGA board
+
+    When the network connection is established, the oscilloscope window
+    is activated in data-acquisition mode
+
+    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
+"""
+
+# Communication with server process is achieved via command codes:
+#   command(code, number, data)  #    number typically is channel number
+#
+# code values
+# code  2:  reset/initialze oscilloscope
+# code  4:  set decimation factor
+# 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 16:  set trigger level in ADC counts
+# code 17:  set number of pre-trigger samples
+# code 18:  set number of total samples 
+# code 19:  start oscilloscope
+# code 20:  read oscilloscope data (two channels at once) 
+# code 21:  set pulse fall-time for generator in µs
+# code 22:  set pulse rise-time in ns 
+# code 25:  set pulse rate in kHz
+# code 26:  fixed frequency or poisson
+# code 27_  reset generator
+# code 28:  set bin for pulse distribution, as a histogram with 4096 channels, 0-500 mV
+# code 29:  start generator 
+# code 30:  stop generator 
+
+import argparse
+import os
+import sys
+import time
+import struct
+
+from functools import partial
+import numpy as np
+import matplotlib
+from matplotlib.figure import Figure
+
+from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
+from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
+
+if "PyQt5" in sys.modules:
+    from PyQt5.uic import loadUiType
+    from PyQt5.QtCore import Qt, QTimer, QEventLoop, QRegExp
+    from PyQt5.QtGui import QPalette, QColor, QRegExpValidator
+    from PyQt5.QtWidgets import QApplication, QMainWindow, QDialog, QFileDialog
+    from PyQt5.QtWidgets import QWidget, QLabel, QCheckBox, QComboBox
+    from PyQt5.QtNetwork import QAbstractSocket, QTcpSocket
+else:
+    from PySide2.QtUiTools import loadUiType
+    from PySide2.QtCore import Qt, QTimer, QEventLoop, QRegExp
+    from PySide2.QtGui import QPalette, QColor, QRegExpValidator
+    from PySide2.QtWidgets import QApplication, QMainWindow, QDialog, QFileDialog
+    from PySide2.QtWidgets import QWidget, QLabel, QCheckBox, QComboBox
+    from PySide2.QtNetwork import QAbstractSocket, QTcpSocket
+
+Ui_MCPHA, QMainWindow = loadUiType("rpControl.ui")
+Ui_LogDisplay, QWidget = loadUiType("mcpha_log.ui")
+Ui_OscDisplay, QWidget = loadUiType("mcpha_daq.ui")
+Ui_GenDisplay, QWidget = loadUiType("mcpha_gen.ui")
+
+if sys.platform != "win32":
+    path = "."
+else:
+    path = os.path.expanduser("~")
+
+
+class rpControl(QMainWindow, Ui_MCPHA):
+    rates = {0:1, 1: 4, 2: 8, 3: 16, 4: 32, 5: 64, 6: 128, 7: 256}
+
+    def __init__(self):
+        super(rpControl, self).__init__()
+        # get command line arguments
+        self.parse_args()
+        # set physical units (for axis labels)
+        self.get_physical_units()
+        self.setupUi(self)
+        # initialize variables
+        self.idle = True
+        self.osc_waiting = False
+        self.state = 0
+        self.status = np.zeros(9, np.uint32)
+        self.timers = self.status[:4].view(np.uint64)
+        # create tabs
+        self.log = LogDisplay()
+        self.osc_daq = OscDAQ(self, self.log)        
+        self.gen = GenDisplay(self, self.log)
+        self.tabindex_log = self.tabWidget.addTab(self.log, "Messages")
+        self.tabindex_osc = self.tabWidget.addTab(self.osc_daq, "Oscilloscope")
+        self.tabindex_gen = self.tabWidget.addTab(self.gen, "Pulse generator")
+        # configure controls
+        self.connectButton.clicked.connect(self.start)
+        self.neg1Check.toggled.connect(partial(self.set_negator, 0))
+        self.neg2Check.toggled.connect(partial(self.set_negator, 1))
+        self.rateValue.addItems(map(str, rpControl.rates.values()))
+        self.rateValue.setEditable(True)
+        self.rateValue.lineEdit().setReadOnly(True)
+        self.rateValue.lineEdit().setAlignment(Qt.AlignRight)
+        for i in range(self.rateValue.count()):
+            self.rateValue.setItemData(i, Qt.AlignRight, Qt.TextAlignmentRole)
+        self.rateValue.setCurrentIndex(1)
+        self.rateValue.currentIndexChanged.connect(self.set_rate)
+        rx = QRegExp(r"^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|rp-[0-9A-Fa-f]{6}\.local$")
+        self.addrValue.setValidator(QRegExpValidator(rx, self.addrValue))
+        # create TCP socket
+        self.socket = QTcpSocket(self)
+        self.socket.connected.connect(self.connected)
+        self.socket.error.connect(self.display_error)
+        # create event loop
+        self.loop = QEventLoop()
+        self.socket.readyRead.connect(self.loop.quit)
+        self.socket.error.connect(self.loop.quit)
+        # create timers 
+        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_oscDisplay)
+
+        # transfer command-line arguments to gui
+        self.osc_daq.levelValue.setValue(self.trigger_level)
+        self.osc_daq.autoButton.setChecked(self.trigger_mode)
+        self.osc_daq.ch2Button.setChecked(self.trigger_source)
+        self.osc_daq.fallingButton.setChecked(self.trigger_slope)
+        # automatically connect
+        if self.ip_address is not None:
+            self.addrValue.setText(self.ip_address)
+            self.start()
+        else:    
+            self.addrValue.setStyleSheet("color: darkorange")
+
+    def parse_args(self):
+        parser =argparse.ArgumentParser(description=__doc__)
+        parser.add_argument('--no-daq', dest='daq_mode', action='store_false',
+                            help='do not start in DAQ mode' )
+        parser.add_argument('-c', '--connect_ip', type=str,
+            help='connect IP address of RedPitaya')
+        parser.add_argument('-i', '--interval', type=int, default=100,
+            help='interval for readout (in ms)')
+        parser.add_argument('-s', '--sample_size', type=int, default=4096,
+            help='size of waveform sample')
+        parser.add_argument('-p', '--pretrigger_fraction', type=float, default=0.05,
+            help='pretrigger fraction')
+        parser.add_argument('-t', '--trigger_level', type=int, default=500,
+            help='trigger level in ADC counts')
+        parser.add_argument('--trigger_slope', type=str, choices={'rising','falling'},
+                            default='rising', help='trigger slope')
+        parser.add_argument('--trigger_source', type=int, choices={1, 2}, default=1, 
+            help='trigger channel')
+        parser.add_argument('--trigger_mode', type=str, choices={'norm','auto'}, default='norm', 
+            help='trigger mode')
+        
+        args = parser.parse_args()
+        # all relevant parameters are here
+        self.ip_address = args.connect_ip
+        self.daq_mode = args.daq_mode
+        self.sample_size = args.sample_size
+        self.pretrigger_fraction = args.pretrigger_fraction
+        self.readInterval = args.interval # used to control oscilloscope display
+        # 
+        self.trigger_level = args.trigger_level
+        self.trigger_source = args.trigger_source - 1
+        self.trigger_mode = 0 if args.trigger_mode == 'norm' else 1
+        self.trigger_slope = 0 if args.trigger_slope == 'rising' else 1
+        # other parameters
+                
+    def get_physical_units(self):
+        """get physical units corresponding to ADC units and channel numbers
+        """
+        # Voltages: 4096 channels/Volt
+        self.adc_unit = 1000/4096
+        # time per sample at sampling rate of 125 MHz / decimation_factor
+        self.time_bin = 0.008
+    # !gq end
+        
+    def start(self):
+        self.socket.connectToHost(self.addrValue.text(), 1001) # connect to port 1001 (mcpha_server on RP) 
+        self.startTimer.start(1000) # shorter time-out
+        self.connectButton.setText("Disconnect")
+        self.connectButton.clicked.disconnect()
+        self.connectButton.clicked.connect(self.stop)
+
+    def stop(self):
+        self.osc_daq.stop()
+        self.gen.stop()
+        self.readTimer.stop()
+        self.startTimer.stop()
+        self.loop.quit()
+        self.socket.abort()
+        self.connectButton.setText("Connect")
+        self.connectButton.clicked.disconnect()
+        self.connectButton.clicked.connect(self.start)
+        self.addrValue.setStyleSheet("color: red")
+        self.log.print("IO stopped")
+        self.idle = True
+
+    def closeEvent(self, event):
+        self.stop()
+
+    def start_timeout(self):
+        self.log.print("error: connection timeout")
+        self.stop()
+
+    def display_error(self):
+        self.log.print("error: %s" % self.socket.errorString())
+        self.stop()
+
+    def connected(self):
+        # coming here when connection is established
+        self.startTimer.stop()
+        self.log.print("IO started")
+        self.addrValue.setStyleSheet("color: green")
+        self.tabWidget.setCurrentIndex(self.tabindex_osc)
+        # initialize variables for readout
+        self.idle = False
+        self.osc_waiting = False
+        self.daq_waiting = False
+        self.osc_reset = False
+        self.osc_start = False
+        self.state = 0
+        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_oscDisplay()
+        self.readTimer.start(self.readInterval)
+
+       #### start oscilloscope in DAQ mode
+        if self.daq_mode:
+            self.osc_daq() # __call__ methood of osc_daq class 
+        
+    def command(self, code, number, value):
+        self.socket.write(struct.pack("<Q", code << 56 | number << 52 | (int(value) & 0xFFFFFFFFFFFFF)))
+
+    def read_data(self, data):
+        view = data.view(np.uint8)
+        size = view.size
+        while self.socket.state() == QAbstractSocket.ConnectedState and self.socket.bytesAvailable() < size:
+            self.loop.exec_()
+        if self.socket.bytesAvailable() < size:
+            return False
+        else:
+            view[:] = np.frombuffer(self.socket.read(size), np.uint8)
+            return True
+
+    def update_oscDisplay(self):
+        """get new data from RP and update oscilloscope display (triggered by QTimer:readTimer)
+        """
+        if not self.osc_waiting:
+            return
+        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")
+                return
+
+    def run_oscDaq(self):
+        """continuous data transfer from RedPitaya
+        """
+        # depends on
+        #   self.start_daq()
+        #   osc.process_data()
+        # initialize trigger mode etc. 
+        self.osc_daq.start_daq()
+        #
+        self.reset_osc()
+        self.start_osc()
+        while self.daq_waiting:
+            # 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.reset_osc()
+                    self.osc_daq.process_data()
+                    self.start_osc()
+                else:
+                    self.log.print("failed to read oscilloscope data")
+                    return        
+            
+    def mark_start_osc(self):
+        self.osc_start = True
+        # self.osc_waiting = True
+        
+    def mark_reset_osc(self):
+        self.osc_reset = True
+
+    def reset_osc(self):        
+        self.command(2, 0, 0)  
+        self.osc_reset=False
+
+    def start_osc(self):        
+        self.command(19, 0, 0)    
+        self.osc_start = False
+
+    def read_status(self):
+        self.command(11, 0, 0) 
+            
+    def stop_osc(self):
+        self.reset_osc()
+        self.osc_waiting = False
+        self.daq_waiting = False
+
+    def set_rate(self, index):
+        # set RP decimation factor 
+        self.command(4, 0, rpControl.rates[index])
+
+    def set_negator(self, number, value):
+        self.command(5, number, value)
+
+    def set_trg_source(self, number):
+        self.command(13, number, 0)
+
+    def set_trg_slope(self, value):
+        self.command(14, 0, value)
+
+    def set_trg_mode(self, value):
+        self.command(15, 0, value)
+
+    def set_trg_level(self, value):
+        self.command(16, 0, value)
+
+    def set_osc_pre(self, value):
+        self.command(17, 0, value)
+
+    def set_osc_tot(self, value):
+        self.command(18, 0, value)
+
+    def set_gen_fall(self, value):
+        self.command(21, 0, value)
+
+    def set_gen_rise(self, value):
+        self.command(22, 0, value)
+
+    def set_gen_rate(self, value):
+        self.command(25, 0, value)
+
+    def set_gen_dist(self, value):
+        self.command(26, 0, value)
+
+    def reset_gen(self):
+        self.command(27, 0)
+
+    def set_gen_bin(self, value):
+        self.command(28, 0, value)
+
+    def start_gen(self):
+        self.command(29, 0, 1)
+
+    def stop_gen(self):
+        self.command(30, 0, 0)
+
+
+class LogDisplay(QWidget, Ui_LogDisplay):
+    def __init__(self):
+        super(LogDisplay, self).__init__()
+        self.setupUi(self)
+
+    def print(self, text):
+        self.logViewer.appendPlainText(text)
+
+
+class OscDAQ(QWidget, Ui_OscDisplay):
+    def __init__(self, rpControl, log):
+        super(OscDAQ, self).__init__()
+        self.setupUi(self)
+        # initialize variables
+        self.rpControl = rpControl
+        self.log = log
+        self.l_tot = self.rpControl.sample_size
+        self.pre = self.rpControl.pretrigger_fraction * self.l_tot
+        self.buffer = np.zeros(self.l_tot * 2, np.int16)
+        # 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.90, 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_xlim(-0.02*self.l_tot, 1.02*self.l_tot)
+        self.ax.set_ylim(-4500, 4500)
+        self.xunit = "[{:d} ns / sample]".format(4*8)
+        self.ax.set_xlabel("sample number " + self.xunit)
+        self.yunit = "[{:.3f} mV]".format(1./4.096)
+        self.ax.set_ylabel("ADC units " + self.yunit)
+        # self.ax.set_xlabel("sample number")
+        # self.ax.set_ylabel("ADC units")
+        # gq
+        self.ax_x2=self.ax.secondary_xaxis('top', functions=(self.tbin2t, self.t2tbin))
+        self.ax_x2.set_xlabel("time [µs]", color='grey', size='x-large')
+        self.ax_y2=self.ax.secondary_yaxis('right',
+                    functions=(self.adc2mV, self.mV2adc))
+        self.ax_y2.set_ylabel("Voltage [mV]", color='grey', size='x-large')
+        # gq end
+        self.osctxt= self.ax.text(0.1, 0.96, ' ', transform=self.ax.transAxes,
+                                  color="darkblue", alpha=0.7)
+
+        x = np.arange(self.l_tot)
+        (self.curve2,) = self.ax.plot(x, self.buffer[1::2],
+                                      color="#00CCCC", label="chan 2")
+        (self.curve1,) = self.ax.plot(x, self.buffer[0::2],
+                                      color="#FFAA00", label="chan 1")
+        self.ax.legend(handles=[self.curve1, self.curve2])
+        self.line = [None, None]
+        self.line[0] = self.ax.axvline(self.pre, linestyle="dotted")
+        self.line[1] = self.ax.axhline(self.levelValue.value(), linestyle="dotted")
+        self.canvas.draw()
+        # 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])
+        # configure colors
+        self.plotLayout.addWidget(self.toolbar)
+        palette = QPalette(self.ch1Label.palette())
+        palette.setColor(QPalette.Window, QColor("#FFAA00"))
+        palette.setColor(QPalette.WindowText, QColor("black"))
+        self.ch1Label.setAutoFillBackground(True)
+        self.ch1Label.setPalette(palette)
+        self.ch1Value.setAutoFillBackground(True)
+        self.ch1Value.setPalette(palette)
+        palette.setColor(QPalette.Window, QColor("#00CCCC"))
+        palette.setColor(QPalette.WindowText, QColor("black"))
+        self.ch2Label.setAutoFillBackground(True)
+        self.ch2Label.setPalette(palette)
+        self.ch2Value.setAutoFillBackground(True)
+        self.ch2Value.setPalette(palette)
+        # configure controls
+        self.autoButton.toggled.connect(self.rpControl.set_trg_mode)
+        self.ch2Button.toggled.connect(self.rpControl.set_trg_source)
+        self.fallingButton.toggled.connect(self.rpControl.set_trg_slope)
+        self.levelValue.valueChanged.connect(self.set_trg_level)
+        self.startButton.clicked.connect(self.start)
+        self.startDAQButton.clicked.connect(self.rpControl.run_oscDaq)
+        self.saveButton.clicked.connect(self.save)
+        self.loadButton.clicked.connect(self.load)
+        self.canvas.mpl_connect("motion_notify_event", self.on_motion)
+
+    def __call__(self):
+        # run in data acquisition mode
+        self.rpControl.run_oscDaq()
+             
+    # gq helper functions to convert acd channels and sampling time to physical units
+    def tbin2t(self, tbin):
+        """convert time bin to time in µs
+        """
+        dfi = self.rpControl.rateValue.currentIndex()  # current decimation factor index
+        df = 4 if dfi == -1 else rpControl.rates[dfi]  # decimation factor
+        return (tbin-self.pre)*self.rpControl.time_bin * df
+ 
+    def t2tbin(self, t):
+        """convert time in µs to time bin 
+        """
+        dfi = self.rpControl.rateValue.currentIndex()  # current decimation factor index
+        df = 4 if dfi == -1 else rpControl.rates[dfi]  # decimation factor
+        return t/(self.rpControl.time_bin * df)+self.pre
+
+    def adc2mV(self, c):
+        """convert adc-count to Voltage in mV
+        """
+        return c * self.rpControl.adc_unit
+
+    def mV2adc(self, v):
+        """convert voltage in mV to adc-count 
+        """
+        return v / self.rpControl.adc_unit
+    # gq end
+    
+    def setup_trigger(self):
+        # extract trigger parameters from gui and send to server
+        self.trg_mode = int(self.autoButton.isChecked())
+        self.trg_source = int(self.ch2Button.isChecked())
+        self.trg_slope = int(self.fallingButton.isChecked())
+        self.trg_level = self.levelValue.value()
+        self.rpControl.set_trg_mode(self.trg_mode)
+        self.rpControl.set_trg_source(self.trg_source)
+        self.rpControl.set_trg_slope(self.trg_slope)
+        self.rpControl.set_trg_level(self.trg_level)
+        self.rpControl.set_osc_pre(self.pre)
+        self.rpControl.set_osc_tot(self.l_tot)
+
+    def set_gui4start(self):
+        self.startButton.setText("Start")
+        self.startButton.setStyleSheet("")
+        self.startButton.clicked.disconnect()
+        self.startButton.clicked.connect(self.start)
+        self.startDAQButton.setEnabled(True)
+
+    def set_gui4stop(self):
+        # set start and stop buttons
+        self.startButton.setText("Stop")
+        self.startButton.setStyleSheet("color: red")
+        self.startButton.clicked.disconnect()
+        self.startButton.clicked.connect(self.stop)
+        self.startDAQButton.setEnabled(False)
+        
+    def start(self):
+        """start oscilloscope display
+        """
+        if self.rpControl.idle:
+            return
+        #
+        self.setup_trigger()
+        self.set_gui4stop()
+        # 
+        self.rpControl.mark_reset_osc()
+        self.rpControl.mark_start_osc()
+        self.rpControl.osc_waiting = True
+        self.osctxt.set_text('')
+        self.log.print("oscilloscope started")
+
+    def start_daq(self):
+        """start oscilloscope in daq mode
+        """
+        if self.rpControl.idle:
+            return
+        # initialize daq statisics 
+        self.NTrig = 0
+        self.dN = 0
+        self.Nprev = self.NTrig
+        self.T0 = time.time()
+        self.Tprev = self.T0
+        self.dT = 0.
+        # set-up trigger and GUI Buttons
+        self.setup_trigger()
+        self.set_gui4stop()
+        self.rpControl.daq_waiting = True
+        self.log.print("daq started")
+       
+    def stop(self):
+        self.rpControl.stop_osc()
+        self.set_gui4start()
+        self.log.print("oscilloscope stopped")
+
+    def process_data(self):
+        """dummy processing of recorded data:
+
+           - count number of Triggers and keep account of timing;
+           - graphical display of a subset of the data 
+        """
+        self.NTrig += 1
+        t = time.time()
+        dt = t - self.Tprev
+        self.Tprev = t
+        self.dT += dt
+        # reshape to an array (2, self.ltot)
+        data = self.buffer.reshape(2,self.l_tot, order='F')
+#        chan1 = self.buffer[0::2]
+#        chan2 = self.buffer[1::2]
+        #
+        # do something with data (e.g. pass to sub-process via mpQueue)
+        #
+        # ....
+        #
+        # output status once per second
+        if self.dT >= 1. :
+            dN = self.NTrig - self.Nprev
+            r = dN/self.dT
+            self.Nprev = self.NTrig
+            T_active = t - self.T0
+            self.dT = 0.
+            status_txt = "active: {:.1f}s  trigger rate: {:.2f} Hz,  data rate: {:.4g} MB/s".format(T_active, r, r*self.l_tot*4e-6)
+            print(status_txt, end='\r')
+            self.osctxt.set_text(status_txt)
+            # update graph on display
+            self.curve1.set_ydata(data[0])
+            self.curve2.set_ydata(data[1])
+            self.xunit = "[{:d} ns / sample]".format(8*rpControl.rates[self.rpControl.rateValue.currentIndex()])
+            self.ax.set_xlabel("sample number " + self.xunit)
+            self.canvas.draw()
+
+    def update_osci_display(self):
+        self.curve1.set_ydata(self.buffer[0::2])
+        self.curve2.set_ydata(self.buffer[1::2])
+        self.xunit = "[{:d} ns / sample]".format(8*rpControl.rates[self.rpControl.rateValue.currentIndex()])
+        self.ax.set_xlabel("sample number " + self.xunit)
+        self.canvas.draw()
+
+    def set_trg_level(self, value):
+        self.line[1].set_ydata([value, value])
+        self.canvas.draw()
+        self.rpControl.set_trg_level(value)
+
+    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.l_tot:
+            x = self.l_tot - 1
+        y1 = self.curve1.get_ydata(True)[x]
+        y2 = self.curve2.get_ydata(True)[x]
+        self.timeValue.setText("%d" % x)
+        self.ch1Value.setText("%d" % y1)
+        self.ch2Value.setText("%d" % y2)
+
+    def save(self):
+        try:
+            dialog = QFileDialog(self, "Save osc file", path, "*.osc")
+            dialog.setDefaultSuffix("osc")
+            name = "oscillogram-%s.osc" % time.strftime("%Y%m%d-%H%M%S")
+            dialog.selectFile(name)
+            dialog.setAcceptMode(QFileDialog.AcceptSave)
+            if dialog.exec() == QDialog.Accepted:
+                name = dialog.selectedFiles()
+                self.buffer.tofile(name[0])
+                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 osc file", path, "*.osc")
+            dialog.setDefaultSuffix("osc")
+            dialog.setAcceptMode(QFileDialog.AcceptOpen)
+            if dialog.exec() == QDialog.Accepted:
+                name = dialog.selectedFiles()
+                self.buffer[:] = np.fromfile(name[0], np.int16)
+                self.update()
+        except:
+            self.log.print("error: %s" % sys.exc_info()[1])
+
+                                   
+class GenDisplay(QWidget, Ui_GenDisplay):
+    def __init__(self, rpControl, log):
+        super(GenDisplay, self).__init__()
+        self.setupUi(self)
+        # initialize variables
+        self.rpControl = rpControl
+        self.log = log
+        self.bins = 4096
+        self.buffer = np.zeros(self.bins, np.uint32)
+        for i in range(16): # initialize with delta-functions
+           self.buffer[(i+1)*256-1] = 1
+        # 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.08, 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 = "[0.122 mV /channel]"
+        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')
+        x = np.arange(self.bins)
+        (self.curve,) = self.ax.plot(x, self.buffer, drawstyle="steps-mid", color="#FFAA00")
+        # 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.toolbar.addSeparator()
+        self.toolbar.addWidget(self.logCheck)
+        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.startButton.clicked.connect(self.start)
+        self.loadButton.clicked.connect(self.load)
+
+    # gq
+    def adc2mV(self, c):
+        """convert adc-count to Voltage in mV 
+           !!! there is a factor of two betwenn channel definition here and in hstDisplay !
+        """
+        return c * self.rpControl.adc_unit/2
+
+    def mV2adc(self, v):
+        """convert voltage in mV to adc-count 
+        """
+        return v * self.rpControl.adc_unit*2
+    # gq end
+        
+    def start(self):
+        if self.rpControl.idle:
+            return
+        self.rpControl.set_gen_fall(self.fallValue.value())
+        self.rpControl.set_gen_rise(self.riseValue.value())
+        self.rpControl.set_gen_rate(self.rateValue.value() * 1000)
+        self.rpControl.set_gen_dist(self.poissonButton.isChecked())
+        for value in np.arange(self.bins, dtype=np.uint64) << 32 | self.buffer:
+            self.rpControl.set_gen_bin(value)
+        self.rpControl.start_gen()
+        self.startButton.setText("Stop")
+        self.startButton.clicked.disconnect()
+        self.startButton.clicked.connect(self.stop)
+        self.log.print("generator started")
+
+    def stop(self):
+        self.rpControl.stop_gen()
+        self.startButton.setText("Start")
+        self.startButton.clicked.disconnect()
+        self.startButton.clicked.connect(self.start)
+        self.log.print("generator stopped")
+
+    def home(self):
+        self.set_scale(self.logCheck.isChecked())
+
+    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")
+        self.ax.relim()
+        self.ax.autoscale_view(scalex=True, scaley=True)
+        self.canvas.draw()
+        
+    def load(self):
+        try:
+            dialog = QFileDialog(self, "Load gen file", path, "*.gen")
+            dialog.setDefaultSuffix("gen")
+            dialog.setAcceptMode(QFileDialog.AcceptOpen)
+            if dialog.exec() == QDialog.Accepted:
+                name = dialog.selectedFiles()
+                self.buffer[:] = np.loadtxt(name[0], np.uint32)
+                self.curve.set_ydata(self.buffer)
+                self.ax.relim()
+                self.ax.autoscale_view(scalex=False, scaley=True)
+                self.canvas.draw()
+        except:
+            self.log.print("error: %s" % sys.exc_info()[1])
+
+def runQt():
+    # start Qt5 
+    app = QApplication(sys.argv)
+    dpi = app.primaryScreen().logicalDotsPerInch()
+    matplotlib.rcParams["figure.dpi"] = dpi
+    application = rpControl()
+    application.show()
+    sys.exit(app.exec_())
+            
+if __name__ == '__main__': # --------------------------------------------
+
+    # run the application with Qt5 interface
+    runQt()
-- 
GitLab