#!/usr/bin/env python3
"""redPoscdaq: fast data acquistion using the oscilloscope client of the MCPHA 
    application running on a RedPitaya FPGA board

    Contains a button to run the oscilloscope in daq mode, i.e. it is restarted 
    continously. If defined, data is exported via calling a callback function. 
    Optionally, triggered waveforms can be stored a numpy binary file 
    (.npy format).

    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 = 'redPdaq.py'

# 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/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 
# 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 multiprocessing import Event

# !!! for conditional import from npy_append_array !!!
def import_npy_append_array():
    global NpyAppendArray
    from npy_append_array import NpyAppendArray

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

# define global graphics style
import matplotlib.pyplot as plt 
pref_style = "default"
_style = pref_style if pref_style in plt.style.available else "default"
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")

if sys.platform != "win32":
    path = "."
else:
    path = os.path.expanduser("~")


class rpControl(QMainWindow, Ui_MCPHA):
    """ control Pavel Demin's MCHPA application on RedPitaya

        Modes of operation:
           - in oscilloscope mode to just display data

           - in daq mode, i.e. read data at maximum rate, 
               optionally recording data to disk or calling an external function
    """

    rates = {0:1, 1: 4, 2: 8, 3: 16, 4: 32, 5: 64, 6: 128, 7: 256}
    def __init__(self, callback=None, conf_dict=None):
        """
           Args: 

             callback: function receiving recorded waveforms
             conf_dict: a configuration dictionary
        """
        super(rpControl, self).__init__()

        plt.style.use("default") # set graphics style

        self.callback_function = callback
        # initialize parameters
        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] == 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.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
        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)
        self.readTimer.timeout.connect(self.read_timeout)

        # 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)
        self.rateValue.setCurrentIndex(self.decimation_index)
        self.neg1Check.setChecked(self.invert1)
        self.neg2Check.setChecked(self.invert2)
        
        # 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_confd(self):
        # all relevant parameters are here
        self.daq_mode = False # True
        self.ip_address ='192.168.1.100' if "ip_address" not in self.confd \
            else self.confd["ip_address"]
        self.sample_size = 2048 if "number_of_samples" not in self.confd \
            else self.confd["number_of_samples"]
        self.pretrigger_fraction = 0.05 if "pre_trigger_samples" not in self.confd \
            else self.confd["pre_trigger_samples"]/self.sample_size
        self.trigger_mode = 0 if "trigger_mode" not in self.confd else \
            0 if (self.confd["trigger_mode"] == "norm" or self.confd["trigger_mode"] == 0) else 1
        self.trigger_source = 0  if "trigger_channel" not in self.confd \
            else int(self.confd["trigger_channel"])-1
        self.trigger_level = 500 if "trigger_level" not in self.confd else self.confd["trigger_level"]
        self.trigger_slope = 0 if "trigger_direction" not in self.confd else \
            0 if (self.confd["trigger_direction"]=="rising" or self.confd["trigger_direction"]=="0") else 1
        self.decimation_index = 1 if "decimation_index" not in self.confd else \
            self.confd["decimation_index"]
        self.invert1 = 0 if "invert_channel1" not in self.confd else self.confd["invert_channel1"]
        self.invert2 = 0 if "invert_channel2" not in self.confd else self.confd["invert_channel2"]
        self.readInterval = 1000 # used to control update of oscilloscope display
        # other parameters
        self.filename = ''  # file name for data export, '' means disable

    def parse_args(self):
        parser = argparse.ArgumentParser(description=__doc__)
        parser.add_argument('-c', '--connect_ip', type=str, default=self.ip_address,
            help='connect IP address of RedPitaya')
        # for oscilloscope display
        parser.add_argument('-i', '--interval', type=int, default=self.readInterval,
            help='interval for readout (in ms)')
        # trigger mode
        parser.add_argument('-t', '--trigger_level', type=int, default=self.trigger_level,
            help='trigger level in ADC counts')
        parser.add_argument('--trigger_slope', type=int, choices={0, 1},
                            default=self.trigger_slope, help='trigger slope')
        parser.add_argument('--trigger_source', type=int, choices={1, 2},
                            default=self.trigger_source+1, help='trigger channel')
        parser.add_argument('--trigger_mode', type=int, choices={0 , 1},
                            default=self.trigger_mode, help='trigger mode')
        parser.add_argument('-s', '--sample_size', type=int, default=self.sample_size,
            help='size of waveform sample')
        parser.add_argument('-p', '--pretrigger_fraction', type=float,
                            default=self.pretrigger_fraction, help='pretrigger fraction')
        parser.add_argument('-f', '--file', type=str, default='',
                            help='file name')        
        args = parser.parse_args()
        # all relevant parameters are here
        self.ip_address = args.connect_ip
        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 = args.trigger_mode
        self.trigger_slope = args.trigger_slope
        # other parameters
        self.filename = args.file
        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
        """
        # 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.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() read_timeout()
        self.readTimer.start(self.readInterval)

       #### start oscilloscope in DAQ mode
#!        if self.interactive and self.daq_mode:
        if self.daq_mode:
            self.osc_daq() # __call__ method 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 read_timeout(self):
        """data transfer from RP,  triggered by QTimer readTimer
        """
        # 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()
        # 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 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()
        #
        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.timestamp = time.time()    
                    self.reset_osc()
                    self.osc_daq.process_data()
                    if self.callback:
                        self.callback_function(self.osc_daq.data)
                    self.start_osc()
                    self.osc_daq.deadT += time.time() - self.timestamp
                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
        # 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 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:
       in daq_mode:
         - data is read from the RedPitaya at maximum rate
         - statistics is accumulated and displayed
         - optinally, data is output to a file or sent to an external function
    """
    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)
        # same memory with different view of shape(2, samples_per_channel)
        self.data = self.buffer.reshape(2, self.l_tot, order='F')
        self.filename = rpControl.filename if self.rpControl.filename != '' else None
        # 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)

        self.rpControl.osc_ready = True
        
    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 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
        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.deadT = 0.
        self.Tprev = self.T0
        self.dT = 0.
        # 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")
       
    def stop(self):
        self.rpControl.stop_osc()
        self.set_gui4start()
        self.log.print("oscilloscope stopped")

    def process_data(self):
        """ statistics 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
        dead_time_fraction = self.deadT/dt
        self.deadT = 0.
        #
        # do something with data (e.g. pass to sub-process via mpQueue)
        # write to file:
        if self.filename is not None:
            with NpyAppendArray(self.filename) as npa:
                npa.append( np.array([self.data]) )
        #        
        # 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
            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(self.data[0])
            self.curve2.set_ydata(self.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])

class redP_consumer():            
    def __init__(self):
        self.NTrig = 0
        self.dN = 0
        self.Nprev = self.NTrig
        self.T0 = time.time()
        self.Tprev = self.T0
        self.dT = 0.

    def data_sink(self, data):
        """function called by redPoscdaq 
           this simple version calculates statistics only
        """
        self.databuffer = data    
        # analyze data
        self.NTrig += 1
        t = time.time()
        dt = t - self.Tprev
        self.Tprev = t
        self.dT += dt
        l_tot = len(self.databuffer[0])
        
        # output status and update scope display 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*l_tot*4e-6)
            print(status_txt, end='\r')

def run_rpControl(callback=None, conf_dict=None):
    # start redPidaya GUI under Qt5 
    app = QApplication(sys.argv)
    dpi = app.primaryScreen().logicalDotsPerInch()
    matplotlib.rcParams["figure.dpi"] = dpi
    application = rpControl(callback=callback, conf_dict=conf_dict)
    application.show()
    sys.exit(app.exec_())


if __name__ == '__main__': # --------------------------------------------
#    run_rpControl()

    data_processor = redP_consumer()
    run_rpControl(callback=data_processor.data_sink)