#!python
# -*- coding: utf-8 -*-

"""package fft_dev
author    Benoit Dubois
copyright FEMTO ENGINEERING, 2019
licence   GPL 3.0+
brief     Basic gui program to make transfert of data trace from HP35670A.
"""

import os
import sys
import signal
import threading
import array
import bisect
import logging
import logging.handlers
from time import strftime, gmtime, sleep
from PyQt5.QtCore import Qt, pyqtSignal, QObject, QDir, QSettings, QEventLoop
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import QApplication, QFileDialog, QDialog, QMessageBox
import pyqtgraph as pg
from pyqtgraph.parametertree import Parameter
from pyqtgraph.parametertree.parameterTypes import GroupParameter
import numpy as np

import fft_dev.fft35670a.fft35670a_dev_prologix as fft35670a
import fft_dev.fft35670a.fft35670a_form as form
from fft_dev.version import __version__

signal.signal(signal.SIGINT, signal.SIG_DFL) # For "Ctrl+C" works


DEFAULT_WORK_DIR = str(os.path.expanduser("~"))

DEFAULT_PORT = 1234  # 1234 for prologix

DEFAULT_CCOLOR = QColor(Qt.black)

DEFAULT_SENSITIVITY = 1.0  # Default setup sensitivity (rad/V)
DEFAULT_GAIN = 0.0  # Default setup gain (dB)

SEPARATOR = '\t'  # Default file separator

FFT35670A_ID = "HEWLETT-PACKARD,35670A"

ORGANIZATION = "FEMTO_Engineering"
APP_NAME = "fft35670a-gui"
APP_BRIEF = "GUI for 35670A signal analyzer device"
AUTHOR_NAME = "Benoit Dubois"
AUTHOR_MAIL = "benoit.dubois@femto-st.fr"
COPYRIGHT = "FEMTO ENGINEERING, 2019"
LICENSE = "GNU GPL v3.0 or upper."

# Defile logging level
# Note that CONSOLE_LOG_LEVEL must be set to the lowest level
CONSOLE_LOG_LEVEL = logging.INFO
FILE_LOG_LEVEL = logging.DEBUG

# Hardware option
OPTION_1D1 = False
CURRENT_INPUTS = [1, 2]

# Use white background and black foreground
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', DEFAULT_CCOLOR)


# =============================================================================
def MyQSettings():
    """Returns QSettings instance with appropriate configuration.
    :returns: QSettings instance (object)
    """
    settings = QSettings()
    settings.setIniCodec("UTF-8")
    return settings


# =============================================================================
def invert_dict(d):
    """Revert key/value if in the dictionary are unique and hashable
    :param d: input dictionary (dict)
    :returns: inverted dictionary (dict)
    """
    return dict([(v, k) for k, v in d.items()])


# =============================================================================
def iterparam(p):
    """Generator iterating over each node of a ParamTree.
    """
    for c in p.children():
        if isinstance(c, GroupParameter):
            yield from iterparam(c)
        else:
            yield c


# =============================================================================
def params_to_settings(params, settings):
    """Save the parameters tree to application settings.
    :returns: None
    """
    for p in iterparam(params):
        if p.type() == 'action':
            continue
        key = "param"
        for c in params.childPath(p):
            key += '/' +  c
        settings.setValue(key, p.value())

def settings_to_params(params, settings):
    """Load application settings to the parameters tree.
    :returns: None
    """
    for p in iterparam(params):
        if p.type() == 'action':
            continue
        cp = params.childPath(p)
        # Use "switch case" because child() method do no allow use of tupple
        # as argument (in contrary with the documentation).
        if len(cp) == 1:
            child = params.child(cp[0])
        elif len(cp) == 2:
            child = params.child(cp[0], cp[1])
        elif len(cp) == 3:
            child = params.child(cp[0], cp[1], cp[2])
        else:
            raise Exception
        #
        key = "param"
        for c in cp:
            key += '/' +  c
        # Handle boolean type
        value = settings.value(key)
        if 'false' in value:
            value = False
        elif 'true' in value:
            value = True
        #
        child.setValue(value)


# =============================================================================
def reset_ini():
    """Reset the .ini file with default values.
    :returns: None
    """
    params = Parameter.create(name='FFT parameters',
                              type='group',
                              children=form.ANALYZER_PARAM)
    params.setToDefault()
    settings = MyQSettings()
    settings.clear()
    settings.setValue("ui/work_dir", DEFAULT_WORK_DIR)
    settings.setValue("app/curve_color", DEFAULT_CCOLOR)
    params_to_settings(params, settings)
    logging.info("Create ini file with default values.")

def check_ini():
    """Basic check of .ini file integrity: we try to read essential parameters,
    if no exception raised, we assume that the file is OK.
    :returns: True if Ini file OK else False (bool)
    """
    settings = MyQSettings()
    params = Parameter.create(name='FFT parameters',
                              type='group',
                              children=form.ANALYZER_PARAM)
    retval = []
    try:
        retval.append(settings.contains("ui/work_dir"))
        retval.append(settings.contains("app/curve_color"))
        for p in iterparam(params):
            if p.type() == 'action':
                continue
            key = "param"
            for c in params.childPath(p):
                key += '/' +  c
            retval.append(settings.contains(key))
    except Exception as ex:
        logging.error("Broken or missing ini file: %r", ex)
        return False
    if False in retval:
        logging.error("Broken or missing ini file")
        return False
    return True


# =============================================================================
class Fft35670aGui(QObject):
    """UI of project
    """

    data_updated = pyqtSignal()
    acquisition_done = pyqtSignal()
    acquisition_canceled = pyqtSignal()
    ccolor_changed = pyqtSignal(object)  # curve color (QColor)
    write_done = pyqtSignal(str)  # output file name

    def __init__(self, parent):
        """Constructor.
        :param parent: parent of object (object)
        :returns: None
        """
        super().__init__(parent=parent)
        self._xdata = None
        self._ydata = [None, None]
        self._acq_flag = threading.Event()
        self._state = None
        self._acq_name = None
        self.ui = form.MainWindow()
        self.logic_handling()
        self.init_ui()

    def logic_handling(self):
        """Define basic logic of app.
        :returns: None
        """
        #
        self.ui.action_reset_dev.triggered.connect(
            lambda: self.ui.acquisition_state('ini'))
        self.ui.action_cancel.triggered.connect(
            lambda: self.ui.acquisition_state('ini'))
        self.ui.action_run.triggered.connect(
            lambda: self.ui.acquisition_state('running'))
        self.acquisition_done.connect(
            lambda: self.ui.acquisition_state('done'))
        self.acquisition_canceled.connect(
            lambda: self.ui.acquisition_state('ini'))
        #
        self.data_updated.connect(self.plot_data)
        self.acquisition_canceled.connect(self.stop_acquisition)
        self.acquisition_done.connect(self.terminate_acquisition)
        self.write_done.connect(self._data_writed)
        #
        self.ccolor_changed.connect(self.set_default_ccolor)
        #
        self.ui.action_run.triggered.connect(self.start_acquisition)
        self.ui.action_cancel.triggered.connect(self.stop_acquisition)
        self.ui.action_reset_dev.triggered.connect(self.reset_dev)
        self.ui.action_quit.triggered.connect(self.ui.close)
        self.ui.action_new.triggered.connect(self.add_tab)
        self.ui.action_save_as.triggered.connect(self.save_data_as)
        self.ui.action_pref.triggered.connect(self.preference)
        self.ui.action_set_default_param.triggered.connect(
            lambda: params_to_settings(self.ui.current_tab.params,
                                       MyQSettings()))
        self.ui.action_about.triggered.connect(self.about)
        #
        self.ui.workspace_changed.connect(
            lambda wdir: MyQSettings().setValue("ui/work_dir", wdir))
        #
        self.ui.current_tab.params.param('Device', 'Check connection').sigActivated. \
            connect(self._check_interface)
        #
        self.ui.current_tab.params.param('Scaling data graph 1', 'Multiplier'). \
            sigValueChanged.connect(self.plot_data)
        self.ui.current_tab.params.param('Scaling data graph 1', 'Offset'). \
            sigValueChanged.connect(self.plot_data)
        self.ui.current_tab.params.param('Scaling data graph 1', 'j\u03C9'). \
            sigValueChanged.connect(self.plot_data)
        self.ui.current_tab.params.param('Scaling data graph 1', 'Reset'). \
            sigActivated.connect(lambda: self.reset_scale(0))
        self.ui.current_tab.params.param('Scaling data graph 2', 'Multiplier'). \
            sigValueChanged.connect(self.plot_data)
        self.ui.current_tab.params.param('Scaling data graph 2', 'Offset'). \
            sigValueChanged.connect(self.plot_data)
        self.ui.current_tab.params.param('Scaling data graph 2', 'j\u03C9'). \
            sigValueChanged.connect(self.plot_data)
        self.ui.current_tab.params.param('Scaling data graph 2', 'Reset'). \
            sigActivated.connect(lambda: self.reset_scale(1))
        #
        self.ui.action_get_screen.triggered.connect(self.get_device_screen)

    def init_ui(self):
        """Initialize UI.
        :returns: None
        """
        self.ui.setVisible(True)
        self.ui.setWindowTitle(APP_NAME)
        settings_to_params(self.ui.current_tab.params, MyQSettings())

    def preference(self):
        """Displays the preference message box.
        :returns: None
        """
        settings = MyQSettings()
        dialog = PreferenceDialog(settings.value("app/curve_color"))
        dialog.setParent(self.ui, Qt.Dialog)
        retval = dialog.exec_()
        if retval == QDialog.Accepted:
            settings.setValue("app/curve_color", dialog.ccolor)
            self.ccolor_changed.emit(dialog.ccolor)

    def about(self):
        """Displays an about message box.
        :returns: None
        """
        QMessageBox.about(self.ui, "About " + APP_NAME,
                          "<b>" + APP_NAME +  " " + __version__ + "</b><br>" +
                          APP_BRIEF +
                          ".<br>" +
                          "Author " + AUTHOR_NAME + ", " + AUTHOR_MAIL +
                          " .<br>" +
                          "Copyright " + COPYRIGHT +
                          ".<br>" +
                          "Licensed under the " + LICENSE)

    def add_tab(self):
        """Add a new tab ready to acquiere data.
        :returns: None
        """
        index = self.ui.data_tab.addTab(form.FftWidget(), "New")
        widget = self.ui.data_tab.widget(index)
        current_state = self.ui.current_tab.params.saveState()
        widget.params.restoreState(current_state)

    def set_default_ccolor(self, ccolor):
        """Set default curve color value.
        :param ccolor: color of curves (QColor)
        :returns: None
        """
        # Store default value
        if ccolor.isValid() is False:
            raise "Bad argument type"
        settings = MyQSettings()
        settings.setValue("app/curve_color", ccolor)
        # Change color of curves
        for index in range(self.ui.data_tab.count()):
            # A tab with label "New" has no curve
            if "New" in self.ui.data_tab.tabText(index):
                continue
            self.ui.data_tab.plots[0].psd.setPen(pg.mkPen(ccolor))
            self.ui.data_tab.plots[1].psd.setPen(pg.mkPen(ccolor))
        logging.info("Change default curve color: %r", ccolor.value())

    def _check_interface(self):
        """Returns True if connection with device is OK.
        :returns: status of interface with device (boolean)
        """
        try:
            dev = self.connect()
            _id = dev.idn
        except Exception:
            _id = '' # To avoid error with 'find()' when '_id' is not defined
        if _id.find(FFT35670A_ID) >= 0:
            QMessageBox.information(self.ui, "Success",
                                "Connection to device succeed")
            return True
        QMessageBox.information(self.ui, "Fail",
                                "Connection to device failed.\n" \
                                "Did you check the GPIB address of " \
                                "the device and check that analyser " \
                                "is in \"address only\" mode.")
        return False

    def connect(self):
        """Connect to device.
        :returns: instance of device connected (object)
        """
        ip = self.ui.current_tab.params.param("Device", "IP").value()
        port = self.ui.current_tab.params.param("Device", "Port").value()
        gaddr = self.ui.current_tab.params.param("Device", "GPIB address").value()
        dev = fft35670a.Fft35670aDevPrologix(ip, port, gaddr)
        try:
            dev.connect()
        except Exception as ex:
            logging.error("Connection problem: %r", ex)
        logging.info("Connected")
        return dev

    def reset_dev(self):
        """Reset device (software reset).
        :returns: None
        """
        dev = self.connect()
        try:
            dev.reset()
            sleep(1.0)
            dev.clear_hpib_interface()
        except Exception as ex:
            logging.error("Connection problem: %r", ex)
            raise ex
        logging.info("Device reseted")

    def set_measurement(self, type_, trace_nb):
        """Set measurement type.
        :param type: measurement type (str)
        :param trace_nb: number of the trace (list)
        :returns: None
        """
        dev = self.connect()
        try:
            dev.set_measurement(type_, trace_nb)
        except Exception as ex:
            logging.error("%r", ex)
            return
        logging.info("Measurement set to %r", type_)

    def set_freq_resolution(self, resolution):
        """Set resolution along X axis. Resolution can be automaticaly selected
        or defined by user. Note that resolution is unitless (lines).
        :param resolution: resolution value (str)
        :returns: None
        """
        dev = self.connect()
        try:
            if 'Auto' in resolution:
                dev.set_resolution_mode('ON')
            else:
                dev.set_freq_resolution(resolution)
        except Exception as ex:
            logging.error("%r", ex)
            return
        logging.info("Frequency resolution set to %r", resolution)

    def set_averaging_mode(self):
        """Set averaging mode.
        :returns: None
        """
        dev = self.connect()
        try:
            mode = self.ui.current_tab.params. \
                param('Acquisition', 'Averaging mode').value()
            dev.set_averaging_type(mode)
            logging.info("Averaging mode set to %r", mode)
        except Exception as ex:
            logging.error("%r", ex)
            return

    def set_xunit(self, unit_id):
        dev = self.connect()
        try:
            dev.set_xunit(fft35670a.XAXIS_UNIT_MAP[unit_id])
        except Exception as ex:
            logging.error("%r", ex)
            return
        logging.info("X unit set to %r", unit_id)

    def set_xspace_display(self, log_space):
        """Set spacing (linear or log) along X axis.
        :param log_space: True if log spacing else False (bool)
        :returns: None
        """
        dev = self.connect()
        try:
            if log_space is True:
                dev.set_xspace_display("LOG")
            else:
                dev.set_xspace_display("LIN")
            if OPTION_1D1 is True:  # With option 1D3
                if log_space is True:
                    dev.set_xspace("LOG")
                else:
                    dev.set_xspace("LIN")
        except Exception as ex:
            logging.error("%r", ex)
            return
        logging.info("X log space display set to %r", log_space)

    def set_yformat(self, format_id, trace_nb=1):
        dev = self.connect()
        try:
            dev.set_yformat(fft35670a.YAXIS_FORMAT_MAP[format_id], trace_nb)
        except Exception as ex:
            logging.error("%r", ex)
            return
        logging.info("Y format of trace %r set to %r", trace_nb, format_id)

    def set_yunit(self, unit_id, trace_nb=1):
        dev = self.connect()
        try:
            dev.set_yunit(fft35670a.YAXIS_UNIT_MAP[unit_id], trace_nb)
        except Exception as ex:
            logging.error("%r", ex)
            return
        logging.info("Y unit for trace %rset to %r", trace_nb, unit_id)

    def set_yunit_amplitude(self, unit_id):
        dev = self.connect()
        try:
            dev.set_yunitamplitude(fft35670a.YAXIS_UNIT_AMPLITUDE_MAP[unit_id])
        except Exception as ex:
            logging.error("%r", ex)
            return
        logging.info("Y unit amplitude set to %r", unit_id)

    def set_yspace_display(self, log_space):
        """Set spacing (linear or log) along Y axis.
        :param log_space: True if log spacing else False (bool)
        :returns: None
        """
        dev = self.connect()
        try:
            if log_space is True:
                dev.set_yspace_display("LOG")
            else:
                dev.set_yspace_display("LIN")
        except Exception as ex:
            logging.error("%r", ex)
            return
        logging.info("Y log space display set to %r", log_space)

    def set_overload_reject_state(self, state):
        """Set input overload reject state.
        :param state: source state (bool)
        :returns: None
        """
        dev = self.connect()
        try:
            dev.set_overload_reject_state(state)
        except Exception as ex:
            logging.error("%r", ex)
            return
        logging.info("Source overload reject is %r", state)

    def set_input_state(self, inp, state):
        """Set input state.
        :param inp: number of the customized input (int)
        :param level: input state (bool)
        :returns: None
        """
        dev = self.connect()
        try:
            dev.set_input_state(inp, state)
        except Exception as ex:
            logging.error("%r", ex)
            return
        logging.info("Input %r state set to %r", inp, state)

    def set_input_coupling(self, inp, coupling):
        """Set input coupling.
        :param inp: number of the customized input (int)
        :param coupling: type of coupling 'AC' or 'DC' (str)
        :returns: None
        """
        dev = self.connect()
        try:
            dev.set_input_coupling(inp, coupling)
        except Exception as ex:
            logging.error("%r", ex)
            return
        logging.info("Input %r coupling set to %r", inp, coupling)

    def set_input_impedance(self, inp, imp):
        """Set input impedance.
        :param inp: number of the customized input (int)
        :param imp: type of impedance 'FLOat' or 'GROund' (str)
        :returns: None
        """
        dev = self.connect()
        try:
            dev.set_input_impedance(inp, imp)
        except Exception as ex:
            logging.error("%r", ex)
            return
        logging.info("Input %r impedance set to %r", inp, imp)

    def set_input_autorange_state(self, inp, state):
        """Set input autorange state.
        :param inp: number of the customized input (int)
        :param state: source state (bool)
        :returns: None
        """
        dev = self.connect()
        try:
            dev.set_input_autorange_state(inp, state)
        except Exception as ex:
            logging.error("%r", ex)
            return
        logging.info("Input %r autorange is %r", inp, state)

    def set_source_state(self, state):
        """Set output source state.
        :param state: source state (bool)
        :returns: None
        """
        dev = self.connect()
        try:
            dev.set_source_state(state)
        except Exception as ex:
            logging.error("%r", ex)
            return
        logging.info("Source state is %r", state)

    def set_source_shape(self, shape):
        """Set output source shape (see fft35670a_dev_xx.py file for shape
        availlables).
        :param shape: source shape (str)
        :returns: None
        """
        if self.ui.current_tab.params. \
           param("Source", "Enable").value() is False:
            QMessageBox.warning(self, "Configuration problem",
                                "Enable source before setting its shape")
            return
        dev = self.connect()
        try:
            dev.set_source_shape(shape)
        except Exception as ex:
            logging.error("%r", ex)
            return
        logging.info("Source shape set to %r", shape)

    def set_source_level(self, level):
        """Set output source level.
        :param level: source level (float)
        :returns: None
        """
        if self.ui.current_tab.params. \
           param("Source", "Enable").value() is False:
            QMessageBox.warning(self, "Configuration problem",
                                "Enable source before setting its level")
            return
        dev = self.connect()
        try:
            dev.set_source_level(level)
        except Exception as ex:
            logging.error("%r", ex)
            return
        logging.info("Source level set to %r", level)

    def set_source_offset(self, offset):
        """Set output source offset.
        :param offset: source offset (float)
        :returns: None
        """
        if self.ui.current_tab.params. \
           param("Source", "Enable").value() is False:
            QMessageBox.warning(self, "Configuration problem",
                                "Enable source before setting its offset")
            return
        dev = self.connect()
        try:
            dev.set_source_offset(offset)
        except Exception as ex:
            logging.error("%r", ex)
            return
        logging.info("Source offset set to %r", offset)

    def device_to_param(self):
        """Set UI parameters from device.
        :returns: None
        """
        dev = self.connect()
        #
        meas_id = (invert_dict(form.MEASUREMENT_MAP))[dev.get_measurement()]
        self.ui.current_tab.params. \
            param('Acquisition', 'Measurement').setValue(meas_id)
        #
        freq_start = dev.get_freq_start()
        self.ui.current_tab.params. \
            param('Acquisition', 'Start').setValue(freq_start)
        #
        freq_stop = dev.get_freq_stop()
        self.ui.current_tab.params. \
            param('Acquisition', 'Stop').setValue(freq_stop)
        #
        '''TODO: update
        resolution = dev.get_freq_resolution()
        self.ui.current_tab.params. \
            param('Acquisition', 'Resolution').setValue(resolution)'''
        #
        averaging_state = dev.get_averaging_state()
        self.ui.current_tab.params. \
            param('Acquisition', 'Averaging', 'Enable'). \
            setValue(averaging_state)
        #
        if averaging_state is True:
            averaging_type = dev.get_averaging_type()
            self.ui.current_tab.params. \
                param('Acquisition', 'Averaging mode'). \
                setValue(averaging_type)
            #
            '''TODO: update
            averaging_nb = dev.get_averaging_nb()
            self.ui.current_tab.params. \
                param('Acquisition', 'Averaging', 'Count number'). \
                setValue(averaging_nb)'''
        #
        xunit_id = dev.get_xunit()
        self.ui.current_tab.params. \
            param('Display', 'X axis', 'Unit').setValue(xunit_id)
        #
        xspacing = dev.get_xspace_display()
        if 'LIN' in xspacing:
            self.ui.current_tab.params. \
                param('Display', 'X axis', 'Log scale').setValue(False)
        else:
            self.ui.current_tab.params. \
                param('Display', 'X axis', 'Log scale').setValue(True)
        #
        yformat = dev.get_yformat()
        self.ui.current_tab.params. \
            param('Display', 'Y axis', 'Format').setValue(yformat)
        #
        yunit = dev.get_yunit()
        self.ui.current_tab.params. \
            param('Display', 'Y axis', 'Unit').setValue(yunit)
        #
        y_unit_amplitude = dev.get_yunitamplitude()
        self.ui.current_tab.params. \
            param('Display', 'Y axis', 'Unit amplitude'). \
            setValue(y_unit_amplitude)
        #
        yspacing = dev.get_yspace_display()
        if 'LIN' in yspacing:
            self.ui.current_tab.params. \
                param('Display', 'Y axis', 'Log scale').setValue(False)
        else:
            self.ui.current_tab.params. \
                param('Display', 'Y axis', 'Log scale').setValue(True)
        #
        input_state = [dev.get_input_state(i)
                       for i in range(1, len(form.INPUT_PARAM['children'])+1)]
        for idx, state in enumerate(input_state):
            input_ = idx + 1
            if input_ > 1:
                self.ui.current_tab.params. \
                    param("Inputs",
                          "Channel {:1d}".format(input_),
                          "Enable").setValue(state)
            #
            if state is True:
                overload = dev.get_overload_reject_state(input_)
                self.ui.current_tab.params. \
                    param("Inputs",
                          "Channel {:1d}".format(input_),
                          "Reject overload").setValue(overload)
                #
                coupling = dev.get_input_coupling(input_)
                self.ui.current_tab.params. \
                    param("Inputs",
                          "Channel {:1d}".format(input_),
                          "Coupling").setValue(coupling)
                #
                impedance = dev.get_input_impedance(input_)
                self.ui.current_tab.params. \
                    param("Inputs",
                          "Channel {:1d}".format(input_),
                          "Impedance").setValue(impedance)
                #
                autorange = dev.get_input_autorange_state(input_)
                self.ui.current_tab.params. \
                    param("Inputs",
                          "Channel {:1d}".format(input_),
                          "Autorange").setValue(autorange)
        #
        source_state = dev.get_source_state()
        self.ui.current_tab.params. \
            param("Source", "Enable").setValue(source_state)
        #
        if source_state is True:
            shape = dev.get_source_shape()
            self.ui.current_tab.params. \
                param("Source", "Shape").setValue(shape)
            #
            level = dev.get_source_level()
            self.ui.current_tab.params. \
                param("Source", "Level").setValue(level)
            #
            offset = dev.get_source_offset()
            self.ui.current_tab.params. \
                param("Source", "Offset").setValue(offset)

    def param_to_device(self):
        """Configure device with UI parameters.
        :returns: None
        """
        meas_id = self.ui.current_tab.params. \
            param('Acquisition', 'Measurement').value()
        if meas_id in ("Frequency response", "Cross spectrum"):
            dev = self.connect()
            dev.write("DISP:FORM ULOW")
            self.set_measurement(form.MEASUREMENT_MAP[meas_id], 1)
            self.set_measurement(form.MEASUREMENT_MAP[meas_id], 2)
            yformat_id = self.ui.current_tab.params. \
                param('Display', 'Y axis', 'Format').value()
            self.set_yformat(yformat_id, 1)
            yformat_id = self.ui.current_tab.params. \
                param('Display', 'Y axis 2', 'Format').value()
            self.set_yformat(yformat_id, 2)
            #
            ###yunit_id = self.ui.current_tab.params. \
            ###    param('Display', 'Y axis', 'Unit').value()
            ###self.set_yunit(yunit_id, 1)
            yunit_id = self.ui.current_tab.params. \
                param('Display', 'Y axis 2', 'Unit').value()
            self.set_yunit(yunit_id, 2)
        elif meas_id == "Power spectrum 1 & 2":
            dev = self.connect()
            dev.write("DISP:FORM ULOW")
            self.set_measurement(form.MEASUREMENT_MAP[meas_id][0], 1)
            self.set_measurement(form.MEASUREMENT_MAP[meas_id][1], 2)
            yformat_id = self.ui.current_tab.params. \
                param('Display', 'Y axis', 'Format').value()
            self.set_yformat(yformat_id, 1)
            self.set_yformat(yformat_id, 2)
            #
            yunit_id = self.ui.current_tab.params. \
                param('Display', 'Y axis', 'Unit').value()
            self.set_yunit(yunit_id, 1)
            self.set_yunit(yunit_id, 2)
        else:
            dev = self.connect()
            dev.write("DISP:FORM SING")
            self.set_measurement(form.MEASUREMENT_MAP[meas_id], 1)
            yformat_id = self.ui.current_tab.params. \
                param('Display', 'Y axis', 'Format').value()
            self.set_yformat(yformat_id)
            #
            yunit_id = self.ui.current_tab.params. \
                param('Display', 'Y axis', 'Unit').value()
            self.set_yunit(yunit_id)
        #
        self.set_averaging_mode()
        #
        xunit_id = self.ui.current_tab.params. \
            param('Display', 'X axis', 'Unit').value()
        self.set_xunit(xunit_id)
        #
        y_unitamplitude_id = self.ui.current_tab.params. \
            param('Display', 'Y axis', 'Unit amplitude').value()
        self.set_yunit_amplitude(y_unitamplitude_id)
        #
        yspacing = self.ui.current_tab.params. \
            param('Display', 'Y axis', 'Log scale').value()
        self.set_yspace_display(yspacing)
        #
        overload = self.ui.current_tab.params. \
            param("Inputs", "Reject overload").value()
        self.set_overload_reject_state(overload)
        #
        for input_ in CURRENT_INPUTS:
            if input_ > 1:
                if meas_id == "Power spectrum 1":
                    self.set_input_state(input_, False)
                    continue
                self.set_input_state(input_, True)
            #
            coupling = self.ui.current_tab.params. \
                param("Inputs",
                      "Channel {:1d}".format(input_),
                      "Coupling").value()
            self.set_input_coupling(input_, coupling)
            #
            impedance = self.ui.current_tab.params. \
                param("Inputs",
                      "Channel {:1d}".format(input_),
                      "Impedance").value()
            self.set_input_impedance(input_,
                                     fft35670a.INPUT_IMPEDANCE[impedance])
            #
            autorange = self.ui.current_tab.params. \
                param("Inputs",
                      "Channel {:1d}".format(input_),
                      "Autorange").value()
            self.set_input_autorange_state(input_, autorange)
        #
        source_state = self.ui.current_tab.params. \
            param("Source", "Enable").value()
        self.set_source_state(source_state)
        #
        if source_state is True:
            shape = self.ui.current_tab.params. \
                param("Source", "Shape").value()
            self.set_source_shape(shape)
            #
            level = self.ui.current_tab.params. \
                param("Source", "Level").value()
            self.set_source_level(level)
            #
            offset = self.ui.current_tab.params. \
                param("Source", "Offset").value()
            self.set_source_offset(offset)

    def get_device_screen(self):
        """Get screen display to UI.
        Not that method does not handle double display.
        returns: None
        """
        # Get configuration from device
        self.device_to_param()
        # Set UI display
        meas_id = self.ui.current_tab.params.param('Acquisition',
                                                   'Measurement').value()
        meas = form.MEASUREMENT_MAP[meas_id]
        xunit_id = self.ui.current_tab.params.param('Display',
                                                    'X axis',
                                                    'Unit').value()
        xunit = (invert_dict(fft35670a.XAXIS_UNIT_MAP))["\"" + xunit_id + "\""]
        yformat_id = self.ui.current_tab.params.param('Display',
                                                      'Y axis',
                                                      'Format').value()
        yformat = (invert_dict(fft35670a.YAXIS_FORMAT_MAP))[yformat_id]
        yunit_id = self.ui.current_tab.params.param('Display',
                                                    'Y axis',
                                                    'Unit').value()
        yunit = (invert_dict(fft35670a.YAXIS_UNIT_MAP))["\"" + yunit_id + "\""]
        if meas == "":
            xformat = "Time"
        else:
            xformat = "Fourrier frequency"
        self.ui.current_tab.plots[0].setTitle(meas_id)
        self.ui.current_tab.plots[0].set_ylabel(yformat, units=yunit)
        self.ui.current_tab.plots[0].set_xlabel(xformat, units=xunit)
        # Get data from device
        dev = self.connect()
        if dev is None:
            logging.error("Connection problem")
            return
        self._ydata[0] = dev.get_ydata()
        self._xdata = dev.get_xdata()[:len(self._ydata[0])]
        # Plot data
        self.data_updated.emit()
        # Write data to file
        # TODO: header???
        current_date = strftime("%Y%m%d-%H%M%S", gmtime())
        wdir = MyQSettings().value("ui/work_dir")
        ofile = wdir + '/' + current_date + "-AG35670A" + ".dat"
        self.write_data(ofile)
        self.acquisition_done.emit()

    def terminate_acquisition(self):
        """Routine when data are acquiered.
        :returns: None
        """
        self.stop_acquisition()
        self.save_data()
        # Add a new tab if there is no free plot widget.
        # To find free plot widget, we check if the text of the tab is "New",
        # i.e. the default text of a free plot widget.
        for index in range(self.ui.data_tab.count()):
            if "New" in self.ui.data_tab.tabText(index):
                return
        self.add_tab()

    def stop_acquisition(self):
        """Stop acquisition process.
        :returns: None
        """
        logging.info("Stop_acquisition")
        self._acq_flag.clear()

    def start_acquisition(self):
        """Start acquisition process.
        :returns: None
        """
        logging.info("Start_acquisition")
        # Check configuration
        meas_id = self.ui.current_tab.params. \
            param('Acquisition', 'Measurement').value()
        source_state = self.ui.current_tab.params. \
            param("Source", "Enable").value()
        # User can use external source generator but in case no,
        # query confirmation
        if meas_id in ("Frequency response") and source_state is not True:
            ret = QMessageBox.question(
                self.ui, "Validate configuration ",
                "You request Frequency response measurement but " \
                "source output is not enabled.\n" \
                "Do you want to enable source output before acquisition?")
            if ret == QMessageBox.Yes:
                self.acquisition_canceled.emit()
                return
        # Set a 'name' for acquisition (used as filename)
        current_date = strftime("%Y%m%d-%H%M%S", gmtime())
        self._acq_name = current_date + "-AG35670A"
        # Set configuration to device
        self.param_to_device()
        # Acquisition
        q_acq_loop = QEventLoop()
        acq_t = threading.Thread(target=self.acquisition)
        self.acquisition_done.connect(q_acq_loop.quit)
        self._acq_flag.set()
        acq_t.start()
        q_acq_loop.exec()

    def acquisition(self):
        """Acquire data from FFT analyzer.
        :returns: None
        """
        # Acquisition properties (resolution averaging) is different from
        # decade to decade:
        acquisition_properties_list = []
        for decade in self.ui.current_tab.params.param('Decades configuration').children():
            if decade.param('Enable').value() is True:
                [fmin_id, fmax_id] = decade.name().split(' to ')
                [fmin, fmax] = [fft35670a.LOG_FREQ_START_MAP[fmin_id],
                                fft35670a.LOG_FREQ_STOP_MAP[fmax_id]]
                resolution = decade.param('Resolution').value()
                average = decade.param('Averaging number').value()
                acquisition_properties_list.append([fmin, fmax, average, resolution])
        # Begin acquisition from upper decade:
        acquisition_properties_list.reverse()
        #
        meas_id = self.ui.current_tab.params. \
           param('Acquisition', 'Measurement').value()
        yformat = fft35670a.YAXIS_FORMAT_MAP[self.ui.current_tab.params. \
            param('Display', 'Y axis', 'Format').value()]
        if meas_id in ("Frequency response", "Cross spectrum"):
            yformat2 = fft35670a.YAXIS_FORMAT_MAP[self.ui.current_tab.params. \
                param('Display', 'Y axis 2', 'Format').value()]
            yunit2 = fft35670a.YAXIS_UNIT_ANGLE_MAP[self.ui.current_tab.params. \
                param('Display', 'Y axis 2', 'Unit').value()]
            yunit = "dB"
        elif meas_id == "Power spectrum 1 & 2":
            yformat = fft35670a.YAXIS_FORMAT_MAP[self.ui.current_tab.params. \
                param('Display', 'Y axis', 'Format').value()]
            yunit = fft35670a.YAXIS_UNIT_MAP[self.ui.current_tab.params. \
                param('Display', 'Y axis', 'Unit').value()]
            yformat2 = yformat
            yunit2 = yunit
        else:
            yunit = self.ui.current_tab.params. \
                param('Display', 'Y axis', 'Unit').value()
            yunit_amp = self.ui.current_tab.params. \
                param('Display', 'Y axis', 'Unit amplitude').value()
        #
        xdata = array.array("f")
        ydata = [array.array("f"), array.array("f")]
        new_y = [array.array("f"), array.array("f")]
        #
        dev = self.connect()
        if dev is None:
            logging.error("Connection problem")
            self.acquisition_canceled.emit()
            return
        # Basic configuration: Enable averaging
        dev.set_averaging_state(True)
        for f_start, f_stop, averaging, resolution in acquisition_properties_list:
            try:
                dev.set_freq_start(f_start)
                dev.set_freq_stop(f_stop)
                dev.set_averaging_nb(averaging)
                dev.set_freq_resolution(resolution)
                logging.info("Acquisition between %r Hz and %r Hz with " \
                    "averaging of %r cycles and a resolution of %r points.",
                    f_start, f_stop, averaging, resolution)
                retval = dev.acquisition(self._acq_flag)
                if retval is False:
                    self.acquisition_canceled.emit()
                    return
            except Exception as ex:
                logging.error("Exception: %r", ex)
                self.acquisition_canceled.emit()
                return
            # Get data from device
            if self._acq_flag.is_set() is not True:
                self.acquisition_canceled.emit()
                return
            if meas_id in ("Frequency response", "Cross spectrum") \
                or meas_id == "Power spectrum 1 & 2":
                try:
                    new_y[0] = dev.get_ydata(1)
                    new_y[1] = dev.get_ydata(2)
                    pass
                except Exception as ex:
                    logging.error("Exception: %r", ex)
                    self.acquisition_canceled.emit()
                    return
            else:
                try:
                    new_y[0] = dev.get_ydata()
                except Exception as ex:
                    logging.error("Exception: %r", ex)
                    self.acquisition_canceled.emit()
                    return
            #
            len_y = int(len(new_y[0]))
            new_x = dev.get_xdata()
            new_x = new_x[:len_y]
            if new_x != []: ## and new_y != []:
                try:
                    idx = bisect.bisect_left(xdata, new_x[-1])
                    xdata = new_x + xdata[idx:]
                    if meas_id in ("Frequency response", "Cross spectrum") \
                        or meas_id == "Power spectrum 1 & 2":
                        ydata[0] = np.concatenate((new_y[0], ydata[0][idx:]))
                        ydata[1] = np.concatenate((new_y[1], ydata[1][idx:]))
                    else:
                        ydata[0] = np.concatenate((new_y[0], ydata[0][idx:]))
                except Exception as ex:
                    logging.error("Exception: %r", ex)
                    self.acquisition_canceled.emit()
                    return
            self._xdata = xdata
            self._ydata = ydata
            self.data_updated.emit()
        self.acquisition_done.emit()
        logging.info("End of acquisition")

    def reset_scale(self, num):
        """Reset data scaling.
        :returns: None
        """
        self.ui.current_tab. \
            params.param('Scaling data graph {}'.format(num),
                         'Multiplier').setValue(1.0)
        self.ui.current_tab. \
            params.param('Scaling data graph {}'.format(num),
                         'Offset').setValue(0.0)
        self.ui.current_tab. \
            params.param('Scaling data graph {}'.format(num),
                         'j\u03C9').setValue(0)
        self.plot_data()

    def plot_data(self):
        """Plot current data to current figure.
        :returns: None
        """
        ccolor = MyQSettings().value("app/curve_color")
        m_factor = self.ui.current_tab.params.param('Scaling data graph 1',
                                                    'Multiplier').value()
        o_factor = self.ui.current_tab.params.param('Scaling data graph 1',
                                                    'Offset').value()
        johmega_factor = self.ui.current_tab.params.param('Scaling data graph 1',
                                                        'j\u03C9').value()
        self.ui.current_tab.plots[0].set_data(self._xdata,
                                        self._ydata[0]*m_factor+o_factor+self._xdata*johmega_factor,
                                        ccolor=ccolor)
        meas_id = self.ui.current_tab.params. \
            param('Acquisition', 'Measurement').value()
        if meas_id in ("Frequency response", "Cross spectrum"):
            self.ui.current_tab.plots[1].set_data(self._xdata,
                                                  self._ydata[1],
                                                  ccolor=ccolor)
        if meas_id ==  "Power spectrum 1 & 2":
            m_factor = self.ui.current_tab.params.param('Scaling data graph 2',
                                                        'Multiplier').value()
            o_factor = self.ui.current_tab.params.param('Scaling data graph 2',
                                                        'Offset').value()
            johmega_factor = self.ui.current_tab.params.param('Scaling data graph 2',
                                                        'j\u03C9').value()
            self.ui.current_tab.plots[1].set_data(self._xdata,
                                        self._ydata[1]*m_factor+o_factor+self._xdata*johmega_factor,
                                        ccolor=ccolor)

    def _data_writed(self, fname):
        """Action after data writing is done.
        :param fname: name of the file where data are written (str)
        :returns: None
        """
        self.ui.current_tab_text = os.path.basename(fname)
        self.ui.status_bar.showMessage("Data writed in output file: " + fname)

    def write_data(self, fname):
        """Write current data to file.
        :param fname: name of file where to write data (str)
        :returns: None
        """
        # Prepare header
        meas_id = self.ui.current_tab.params. \
            param('Acquisition', 'Measurement').value()
        header = "# Measurement: {}\n".format(meas_id)
        # Acquisition parameters is different from decade to decade
        header += "# [fmin; fmax]; resolution; averaging cycles\n"
        for decade in self.ui.current_tab.params. \
            param('Decades configuration').children():
            if decade.param('Enable').value() is True:
                [fmin, fmax] = decade.name().split(' to ')
                resolution = decade.param('Resolution').value()
                average = decade.param('Averaging number').value()
                header += "# [{}; {}]; {} ; {}\n".format(fmin, fmax, resolution, average)
        averaging_type = self.ui.current_tab.params. \
            param('Acquisition', 'Averaging mode').value()
        header += "# Averaging mode: {}\n".format(averaging_type)
        #
        m_factor = self.ui.current_tab.params. \
            param('Scaling data graph 1', 'Multiplier').value()
        o_factor = self.ui.current_tab.params. \
            param('Scaling data graph 1', 'Offset').value()
        jomega_factor = self.ui.current_tab.params. \
            param('Scaling data graph 1', 'j\u03C9').value()
        header += "# Scale factor 1: {}; " \
            "Offset 1: {}; " \
            "jomega 1: {}\n".format(m_factor, o_factor, jomega_factor)
        if meas_id == "Power spectrum 1 & 2":
            m_factor2 = self.ui.current_tab.params. \
                param('Scaling data graph 2', 'Multiplier').value()
            o_factor2 = self.ui.current_tab.params. \
                param('Scaling data graph 2', 'Offset').value()
            header += "# Scale factor 2: {}; " \
                "Offset 2: {}; " \
                "jomega 2: {}\n".format(m_factor, o_factor, jomega_factor)
        #
        xunit = self.ui.current_tab.params. \
                param('Display', 'X axis', 'Unit').value()
        header += "# X unit: {}\n".format(xunit)
        #
        yformat = self.ui.current_tab.params. \
            param('Display', 'Y axis', 'Format').value()
        yunit = self.ui.current_tab.params. \
            param('Display', 'Y axis', 'Unit').value()
        yunitamplitude = self.ui.current_tab.params. \
            param('Display', 'Y axis', 'Unit amplitude').value()
        header += "# Y format: {}; Y unit: {} {}\n".format(yformat,
                                                           yunit,
                                                           yunitamplitude)
        #
        source_state = self.ui.current_tab.params. \
            param("Source", "Enable").value()
        if source_state is True:
            source_shape = self.ui.current_tab.params. \
                param("Source", "Shape").value()
            source_level = self.ui.current_tab.params. \
                param("Source", "Level").value()
            source_offset = self.ui.current_tab.params. \
                param("Source", "Offset").value()
            header += "# Source shape: {}; " \
                "Source level: {:5e}; " \
                "Source offset: {:5e}\n".format(source_shape,
                                                source_level,
                                                source_offset)
        #
        overload_state = self.ui.current_tab.params. \
            param("Inputs", "Reject overload").value()
        header += "# Reject overload : {}\n".format(overload_state)
        #
        for input_ in CURRENT_INPUTS:
            if input_ > 1 and meas_id == "Power spectrum 1":
                continue
            coupling = self.ui.current_tab.params. \
                    param("Inputs",
                          "Channel {:1d}".format(input_),
                          "Coupling").value()
            impedance = self.ui.current_tab.params. \
                    param("Inputs",
                          "Channel {:1d}".format(input_),
                          "Impedance").value()
            header += "# Input {}; " \
                "Coupling: {}; " \
                "Input impedance: {}\n".format(input_,
                                               coupling,
                                               impedance)
        #
        notes = self.ui.current_tab.params.param('Notes').value()
        notes = notes.replace("\n", "\n#")
        if notes != "":
            header += "# Notes: {}\n".format(notes)
        # Write data
        with open(fname, "w", encoding="utf-8") as fd:
            fd.write(header)
            for i, x in enumerate(self._xdata):
                if meas_id in ("Frequency response", "Cross spectrum"):
                    line = '{:e}\t{:e}\t{:e}\n'.format(
                        x,
                        self._ydata[0][i] * m_factor + o_factor,
                        self._ydata[1][i])
                elif meas_id == "Power spectrum 1 & 2":
                    line = '{:e}\t{:e}\t{:e}\n'.format(
                        x,
                        self._ydata[0][i]  * m_factor + o_factor,
                        self._ydata[1][i] * m_factor2 + o_factor2)
                else:
                    line = '{:e}\t{:e}\n'.format(x, self._ydata[0][i])
                fd.write(line)
        self.write_done.emit(fname)

    def save_data(self):
        """Save data process. Data are saved in the working directory.
        :returns: None
        """
        wdir = MyQSettings().value("ui/work_dir")
        ofile = wdir + '/' + self._acq_name + ".dat"
        self.write_data(ofile)

    def save_data_as(self):
        """Save 'data as' process. Call a file dialog box to give a filename
        for the data file.
        :returns: None
        """
        (ofile, filter_) = QFileDialog(). \
            getSaveFileName(parent=None,
                            caption="Save data",
                            directory=QDir.currentPath(),
                            filter=";;Data files (*.dat);;Any files (*)")
        if ofile == "":
            return  # Abort if no file given
        self.write_data(ofile)


# =============================================================================
def configure_logging():
    """Configure logging:
    - (debug) messages logged to console
    - messages logged in a file located in the home directory
    Note that CONSOLE_LOG_LEVEL must be set to the lowest level
    """
    log_path = os.path.expanduser("~")
    log_filename = "." + APP_NAME + ".log"
    log_abs_filename = os.path.join(log_path, log_filename)

    log_file_format = "%(asctime)s %(levelname) -8s %(filename)s " + \
        "%(funcName)s (%(lineno)d): %(message)s"
    file_formatter = logging.Formatter(log_file_format)

    log_console_format = "%(asctime)s [%(threadName)-12.12s]" + \
        "[%(levelname)-6.6s] %(filename)s %(funcName)s (%(lineno)d): " + \
        "%(message)s"
    console_formatter = logging.Formatter(log_console_format)

    root_logger = logging.getLogger()
    root_logger.setLevel(CONSOLE_LOG_LEVEL)

    file_handler = logging.handlers.RotatingFileHandler(log_abs_filename,
                                                        maxBytes=(1048576*5),
                                                        backupCount=7)
    file_handler.setFormatter(file_formatter)
    file_handler.setLevel(FILE_LOG_LEVEL)

    console_handler = logging.StreamHandler()
    console_handler.setFormatter(console_formatter)
    console_handler.setLevel(CONSOLE_LOG_LEVEL)

    root_logger.addHandler(file_handler)
    root_logger.addHandler(console_handler)


#==============================================================================
APP = QApplication(sys.argv)
APP.setOrganizationName(ORGANIZATION)
APP.setApplicationName(APP_NAME)

configure_logging()

if check_ini() is False:
    reset_ini()
    QMessageBox.warning(None, "Broken or missing .ini file",
                        "Create .ini file with default values created")

UI = Fft35670aGui(APP)
sys.exit(APP.exec_())
