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

"""package ddsctrl
author    Benoit Dubois
copyright FEMTO ENGINEERING, 2016, 2023
license   GPL v3.0+
brief     Main script of the DDS controller project.
"""

# Ctrl-c closes the application
import signal
signal.signal(signal.SIGINT, signal.SIG_DFL)

import sys
import argparse
import os.path as path
import logging
import usb.core
import serial
import serial.tools.list_ports
from decimal import Decimal
from PyQt5.QtCore import QSettings
from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox

from ad9xdds.interfaces.fx2intf import Fx2Intf
from ad9xdds.interfaces.stm32intf import Stm32Intf
import ad9xdds.sad9912dev as sad9912dev
import ad9xdds.ad9912dev as ad9912dev
import ad9xdds.sad9915dev_umr232hm as sad9915dev
import ad9xdds.ad9915dev_umr232hm as ad9915dev

from ddsctrl.ddsctrlui import DdsCtrlUi, DdsBoardSelection
from ddsctrl.constants import ORGANIZATION, APP_NAME, DEFAULT_AUTO_UPDATE, \
    DEFAULT_AD9912_OFREQ, DEFAULT_AD9912_AMP, DEFAULT_AD9912_PHASE, \
    DEFAULT_AD9912_PLL_EN, DEFAULT_AD9912_PLL_FACTOR, \
    DEFAULT_AD9912_CP_CURRENT, DEFAULT_AD9912_VCO_RANGE, \
    DEFAULT_AD9912_PLL_DOUBLER, DEFAULT_AD9912_HSTL_EN, \
    DEFAULT_AD9912_CMOS_EN, DEFAULT_AD9912_IFREQ, \
    DEFAULT_AD9912_HSTL_DOUBLER, \
    DDS_MODEL

CONSOLE_LOG_LEVEL = logging.DEBUG
FILE_LOG_LEVEL = logging.DEBUG


# =============================================================================
def configure_logging():
    """Configures logs.
    """
    home = path.expanduser("~")
    log_file = "." + APP_NAME + ".log"
    abs_log_file = path.join(home, log_file)
    date_fmt = "%d/%m/%Y %H:%M:%S"
    log_format = "%(asctime)s %(levelname) -8s %(filename)s " + \
                 " %(funcName)s (%(lineno)d): %(message)s"
    logging.basicConfig(level=FILE_LOG_LEVEL,
                        datefmt=date_fmt,
                        format=log_format,
                        filename=abs_log_file,
                        filemode='w')
    console = logging.StreamHandler()
    # define a Handler which writes messages to the sys.stderr
    console.setLevel(CONSOLE_LOG_LEVEL)
    # set a format which is simpler for console use
    console_format = '%(levelname) -8s %(filename)s (%(lineno)d): %(message)s'
    formatter = logging.Formatter(console_format)
    # tell the handler to use this format
    console.setFormatter(formatter)
    # add the handler to the root logger
    logging.getLogger('').addHandler(console)


# =============================================================================
def parse_cmd_line():
    """Parses command line: allow user to choose a different ini file.
    :returns: parser object (object)
    """
    parser = argparse.ArgumentParser(description='DdsController')
    parser.add_argument('-ini', '--ini_file', dest='ini_file',
                        type=str, default='', help='-ini=<str>')
    parser.add_argument('-wt', '--window_title', dest='wtitle',
                        type=str, default="DDS controller")
    parser.add_argument('-d', '--dds_model', choices=["AD9912_FX2", "AD9912_STM32", "AD9915"],
                        dest='model')
    parser.add_argument('-b', '--dds_bus', type=int, dest='bus')
    parser.add_argument('-a', '--dds_address', type=int, dest='address')
    parser.add_argument('-s', '--serial_device', type=str, dest='serial_device')
    args = parser.parse_args()
    if ( args.model and (args.bus is None or args.address is None) ) and \
        args.model=="AD9912_STM32" and args.serial is None:
        parser.error("--dds_model requires --dds_bus and --dds_adress or --serial_device.")
    if not args.model and (args.bus is not None or args.address is not None):
        parser.error("--dds_bus and --dds_adress requires --dds_model.")
    return args


# =============================================================================
def reset_ini(settings):
    """Resets the "settings" file with default values.
    We use AD9912 default values because these values are "conservative" values
    which could be used with AD9915 device (ofreq, ifreq, amp).
    Note: due to conversion problem from QVariant (type returned by QSettings)
    to boolean type in Python, boolean value are stored in integer:
    0 <-> False, 1 <-> True.
    :param ui: ddsctrlui class instance (object)
    :param settings: a valid QSettings object (QSettings)
    :returns: None
    """
    settings.clear()
    settings.setValue('dds_ctrl/auto_update', int(DEFAULT_AUTO_UPDATE))
    settings.setValue('dds_ctrl/ifreq', DEFAULT_AD9912_IFREQ)
    settings.setValue('dds_ctrl/ofreq', DEFAULT_AD9912_OFREQ)
    settings.setValue('dds_ctrl/amp', DEFAULT_AD9912_AMP)
    settings.setValue('dds_ctrl/phase', DEFAULT_AD9912_PHASE)
    settings.setValue('dds_ctrl/hstl_en', int(DEFAULT_AD9912_HSTL_EN))
    settings.setValue('dds_ctrl/cmos_en', int(DEFAULT_AD9912_CMOS_EN))
    settings.setValue('dds_ctrl/hstl_doubler', int(DEFAULT_AD9912_HSTL_DOUBLER))
    settings.setValue('dds_ctrl/pll_en', int(DEFAULT_AD9912_PLL_EN))
    settings.setValue('dds_ctrl/pll_doubler', int(DEFAULT_AD9912_PLL_DOUBLER))
    settings.setValue('dds_ctrl/pll_factor', DEFAULT_AD9912_PLL_FACTOR)
    settings.setValue('dds_ctrl/vco_range', DEFAULT_AD9912_VCO_RANGE)
    settings.setValue('dds_ctrl/cp_current', DEFAULT_AD9912_CP_CURRENT)


# =============================================================================
def save_all_settings(ui):
    """Save current setting to ini file.
    :returns: None
    """
    settings = QSettings()
    settings.setValue('dds_ctrl/auto_update',
                      ui.ocontroller_ui.auto_update_ckbox.isChecked())
    settings.setValue('dds_ctrl/ofreq', ui.ocontroller_ui.ofreq_tuning.value())
    settings.setValue('dds_ctrl/phase', ui.ocontroller_ui.phy_tuning.value())
    settings.setValue('dds_ctrl/amp', ui.ocontroller_ui.amp_tuning.value())
    settings.setValue('dds_ctrl/ifreq', ui.icontroller_ui.ifreq_dsb.value())
    if model == DDS_MODEL.AD9912_FX2 or model == DDS_MODEL.AD9912_STM32:
        settings.setValue('dds_ctrl/hstl_en',
                          int(ui.ocontroller_ui.out_hstl_ckbox.isChecked()))
        settings.setValue('dds_ctrl/cmos_en',
                          int(ui.ocontroller_ui.out_cmos_ckbox.isChecked()))
        settings.setValue(
            'dds_ctrl/hstl_doubler',
            int(ui.ocontroller_ui.hstl_doubler_ckbox.isChecked()))
        settings.setValue(
            'dds_ctrl/pll_en',
            int(ui.icontroller_ui.advanced_config_gbox.isChecked()))
        settings.setValue(
            'dds_ctrl/pll_doubler',
            int(ui.icontroller_ui.pll_doubler_ckbox.checkState()))
        settings.setValue('dds_ctrl/pll_factor',
                          ui.icontroller_ui.pll_factor_sbox.value())
        settings.setValue('dds_ctrl/vco_range',
                          ui.icontroller_ui.vco_range_cbox.currentIndex())
        settings.setValue('dds_ctrl/cp_current',
                          ui.icontroller_ui.cp_current_cbox.currentIndex())


# =============================================================================
def check_ini_file(user_ini_file=None):
    """Check validity of ini file. Ini file can be given by user or can be the
    system default ini file. If the system ini file is broken or missing,
    create an ini file with default values.
    :param user_ini_file: Ini file given by user (str)
    :returns: None
    """
    if user_ini_file is not None:
        settings = QSettings(user_ini_file)
    else:
        settings = QSettings()
    retval = list()
    try:
        retval.append(settings.value('dds_ctrl/auto_update'))
        retval.append(settings.value('dds_ctrl/ofreq'))
        retval.append(settings.value('dds_ctrl/phase'))
        retval.append(settings.value('dds_ctrl/amp'))
        retval.append(settings.value('dds_ctrl/hstl_en'))
        retval.append(settings.value('dds_ctrl/cmos_en'))
        retval.append(settings.value('dds_ctrl/hstl_doubler'))
        retval.append(settings.value('dds_ctrl/ifreq'))
        retval.append(settings.value('dds_ctrl/pll_en'))
        retval.append(settings.value('dds_ctrl/pll_doubler'))
        retval.append(settings.value('dds_ctrl/pll_factor'))
        retval.append(settings.value('dds_ctrl/vco_range'))
        retval.append(settings.value('dds_ctrl/cp_current'))
        if None in retval:
            # Test needded because QSettings does not catch exception when
            # a parameter is missing in ini file, instead QSettings return None.
            if user_ini_file is None:
                logging.warning("Missing or broken ini file")
                reset_ini(settings)
                logging.info("Create ini file with default values")
            else:
                logging.critical("Ini file given by user is not valid")
                QMessageBox().critical(None, "Ini file error", \
                                       "User ini file not valid")
                sys.exit()
    except Exception as ex:
        if user_ini_file is None:
            logging.warning("Missing or broken ini file")
            reset_ini(settings)
            logging.info("Create ini file with default values")
        else:
            logging.critical("User ini file not valid %r", ex)
            QMessageBox().critical(None, "Ini file error",
                                   "User ini file not valid: {}".format(ex))
            sys.exit()


# =============================================================================
def init_ui(ui, model, dds):
    """Init UI.
    :param ui: ddsctrlui class instance (object)
    :param model: dds model i.e. "AD9912" or "AD9915" (str)
    :returns: None
    """
    if model == DDS_MODEL.AD9912_FX2 or model == DDS_MODEL.AD9912_STM32:
        amax = ad9912dev.AMAX
        ifmax = ad9912dev.IFMAX
    else:
        amax = ad9915dev.AMAX
        ifmax = ad9915dev.IFMAX
    ui.icontroller_ui.set_ifmax(ifmax)
    ui.ocontroller_ui.set_ofmax(ifmax*0.4)
    ui.ocontroller_ui.set_amax(amax)
    # Init UI with respect to (ini file) actual dds configuration
    settings = QSettings()
    auto_update = bool(settings.value('dds_ctrl/auto_update'))
    ofreq = Decimal(settings.value('dds_ctrl/ofreq'))
    ifreq = Decimal(settings.value('dds_ctrl/ifreq'))
    phase = Decimal(settings.value('dds_ctrl/phase'))
    amp = int(settings.value('dds_ctrl/amp'))
    ##ui.ocontroller_ui.auto_update_ckbox.setChecked(False)
    ui.ocontroller_ui.auto_update_ckbox.setChecked(auto_update)
    ui.ocontroller_ui.ofreq_tuning.setValue(ofreq)
    ui.ocontroller_ui.phy_tuning.setValue(phase)
    ui.ocontroller_ui.amp_tuning.setValue(amp)
    ui.icontroller_ui.ifreq_dsb.setValue(ifreq)

    
    if model == DDS_MODEL.AD9912_FX2 or model == DDS_MODEL.AD9912_STM32:
        hstl_en = bool(int(settings.value('dds_ctrl/hstl_en')))
        cmos_en = bool(int(settings.value('dds_ctrl/cmos_en')))
        hstl_doubler = bool(int(settings.value('dds_ctrl/hstl_doubler')))
        pll_en = bool(int(settings.value('dds_ctrl/pll_en')))
        pll_doubler = bool(int(settings.value('dds_ctrl/pll_doubler')))
        pll_factor = int(settings.value('dds_ctrl/pll_factor'))
        vco_range = int(settings.value('dds_ctrl/vco_range'))
        cp_current = int(settings.value('dds_ctrl/cp_current'))
        ui.ocontroller_ui.out_hstl_ckbox.setChecked(hstl_en)
        ui.ocontroller_ui.hstl_doubler_ckbox.setChecked(hstl_doubler)
        ui.ocontroller_ui.out_cmos_ckbox.setChecked(cmos_en)
        ui.icontroller_ui.advanced_config_gbox.setChecked(pll_en)
        ui.icontroller_ui.pll_doubler_ckbox.setChecked(pll_doubler)
        ui.icontroller_ui.pll_factor_sbox.setValue(pll_factor)
        ui.icontroller_ui.vco_range_cbox.setCurrentIndex(vco_range)
        ui.icontroller_ui.cp_current_cbox.setCurrentIndex(cp_current)
        ui.icontroller_ui.update_sys_clock_label()
    
    elif model == DDS_MODEL.AD9915:
        # Update input PLL configuration
        ui.icontroller_ui.input_doubler_enable_ckbox.setChecked(dds.get_input_doubler_state())
        ui.icontroller_ui.input_divider_sbox.setValue(dds.get_input_divider_factor())
        # Update PLL attribute
        ui.icontroller_ui.pll_enable_ckbox.setChecked(dds.get_pll_state())
        ui.icontroller_ui.pll_multiplier_sbox.setValue(dds.get_pll_multiplier_factor())
        #
        if ui.icontroller_ui.pll_enable_ckbox.isChecked() is True:
            if dds.vco_calibration() is False:
                logging.error("PLL is not locked")
        #
        ui.icontroller_ui.update_sys_clock_label(dds.get_sysfreq())

# =============================================================================
def init_dev(dds, model):
    """Init device.
    :param dds: dds class instance (object)
    :param model: dds model i.e. "AD9912" or "AD9915" (str)
    :returns: None
    """
    settings = QSettings()
    ofreq = Decimal(settings.value('dds_ctrl/ofreq'))
    ifreq = Decimal(settings.value('dds_ctrl/ifreq'))
    phase = Decimal(settings.value('dds_ctrl/phase'))
    amp = int(settings.value('dds_ctrl/amp'))
    dds.set_ofreq(ofreq)
    dds.set_ifreq(ifreq)
    dds.set_phy(phase)
    dds.set_amp(amp)
    if model == DDS_MODEL.AD9912_FX2 or model == DDS_MODEL.AD9912_STM32:
        hstl_en = bool(int(settings.value('dds_ctrl/hstl_en')))
        cmos_en = bool(int(settings.value('dds_ctrl/cmos_en')))
        hstl_doubler = bool(int(settings.value('dds_ctrl/hstl_doubler')))
        pll_en = bool(int(settings.value('dds_ctrl/pll_en')))
        pll_doubler = bool(int(settings.value('dds_ctrl/pll_doubler')))
        pll_factor = int(settings.value('dds_ctrl/pll_factor'))
        vco_range = int(settings.value('dds_ctrl/vco_range'))
        cp_current = int(settings.value('dds_ctrl/cp_current'))
        dds.set_hstl_output_state(hstl_en)
        dds.set_cmos_output_state(cmos_en)
        dds.set_hstl_doubler_state(hstl_doubler)
        dds.set_pll_state(pll_en)
        dds.set_pll_doubler_state(pll_doubler)
        dds.set_pll_multiplier_factor(pll_factor)
        dds.set_vco_range(vco_range)
        dds.set_cp_current(cp_current)


# =============================================================================
def handle_ocontroller_ui_behavior(ui, dds, model):
    """Handle behavior of output widget tab with respect to automatic update
    checkbox: if auto is true, modification are directly take into account,
    else modification are take into account after apply button is pressed.
    :param ui: ddsctrlui class instance (object)
    :param dds: dds class instance (object)
    :param model: dds model i.e. "AD9912" or "AD9915" (str)
    :returns: None
    """
    if ui.ocontroller_ui.auto_update_ckbox.isChecked() is True:
        ui.ocontroller_ui.ofreq_tuning.valueChanged[str].connect(
            lambda: ofreq_changed(ui, dds))
        ui.ocontroller_ui.amp_tuning.valueChanged[str].connect(
            lambda: amplitude_changed(ui, dds))
        ui.ocontroller_ui.phy_tuning.valueChanged[str].connect(
            lambda: phase_changed(ui, dds))
        if model == DDS_MODEL.AD9912_FX2 or model == DDS_MODEL.AD9912_STM32:
            ui.ocontroller_ui.out_hstl_ckbox.stateChanged.connect(
                lambda: hstl_output_changed(ui, dds))
            ui.ocontroller_ui.hstl_doubler_ckbox.stateChanged.connect(
                lambda: hstl_doubler_changed(ui, dds))
            ui.ocontroller_ui.out_cmos_ckbox.stateChanged.connect(
                lambda: cmos_output_changed(ui, dds))
        try:
            ui.ocontroller_ui.apply_btn.clicked.disconnect()
        except Exception as ex:
            logging.warning(ex)
    else:
        ui.ocontroller_ui.apply_btn.clicked.connect(
            lambda: apply_btn_octrl_clicked(ui, dds, model))
        try:
            ui.ocontroller_ui.ofreq_tuning.valueChanged[str].disconnect()
            ui.ocontroller_ui.amp_tuning.valueChanged[str].disconnect()
            ui.ocontroller_ui.phy_tuning.valueChanged[str].disconnect()
            if model == "AD9912":
                ui.ocontroller_ui.out_hstl_ckbox.stateChanged.disconnect()
                ui.ocontroller_ui.hstl_doubler_ckbox.stateChanged.disconnect()
                ui.ocontroller_ui.out_cmos_ckbox.stateChanged.disconnect()
        except Exception as ex:
            logging.warning(ex)


# =============================================================================
def get_reg_btn_clicked(ui, dds):
    """Get register value.
    :param ui: ddsctrlui class instance (object)
    :param dds: dds class instance (object)
    :returns: None
    """
    try:
        reg_val = dds._intf.get_reg(
            int(str(ui.debug_ui.reg_add_led.text()), 16),
            1
        )
    except ValueError as ex:
        pass # Error handled in dds.get_reg
    except Exception as ex:
        logging.warning("Problem when getting register value: %s", str(ex))
    else:
        ui.debug_ui.reg_val_led.setText(format(reg_val, '#02x'))


# =============================================================================
def set_reg_btn_clicked(ui, dds):
    """Set register value.
    :param ui: ddsctrlui class instance (object)
    :param dds: dds class instance (object)
    :returns: None
    """
    dds._intf.set_reg(
        int(str(ui.debug_ui.reg_add_led.text()), 16),
        1,
        int(str(ui.debug_ui.reg_val_led.text()), 16)
    )


# =============================================================================
def ofreq_changed(ui, dds, **kwargs):
    """Update output frequency
    :param ui: ddsctrlui class instance (object)
    :param dds: dds class instance (object)
    :returns: None
    """
    act_ofreq = dds.set_ofreq(ui.ocontroller_ui.ofreq_tuning.value())
    ui.ocontroller_ui.ofreq_tuning.setValue(Decimal(act_ofreq))
    QSettings().setValue('dds_ctrl/ofreq', act_ofreq)


# =============================================================================
def amplitude_changed(ui, dds):
    """Update output amplitude
    :param ui: ddsctrlui class instance (object)
    :param dds: dds class instance (object)
    :returns: None
    """
    act_amp = dds.set_amp(int(ui.ocontroller_ui.amp_tuning.value()))
    ui.ocontroller_ui.amp_tuning.setValue(Decimal(act_amp))
    QSettings().setValue('dds_ctrl/amp', act_amp)


# =============================================================================
def phase_changed(ui, dds):
    """Update output phase
    :param ui: ddsctrlui class instance (object)
    :param dds: dds class instance (object)
    :returns: None
    """
    act_phy = dds.set_phy(ui.ocontroller_ui.phy_tuning.value())
    ui.ocontroller_ui.phy_tuning.setValue(Decimal(act_phy))
    QSettings().setValue('dds_ctrl/phase', act_phy)


# =============================================================================
def hstl_output_changed(ui, dds):
    """Update HSTL outputs state
    :param ui: ddsctrlui class instance (object)
    :param dds: dds class instance (object)
    :returns: None
    """
    dds.set_hstl_output_state(ui.ocontroller_ui.out_hstl_ckbox.isChecked())
    QSettings().setValue('dds_ctrl/hstl_en',
                         int(ui.ocontroller_ui.out_hstl_ckbox.isChecked()))


# =============================================================================
def hstl_doubler_changed(ui, dds):
    """Update HSTL doubler state
    :param ui: ddsctrlui class instance (object)
    :param dds: dds class instance (object)
    :returns: None
    """
    dds.set_hstl_doubler_state(ui.ocontroller_ui.hstl_doubler_ckbox.isChecked())
    QSettings().setValue('dds_ctrl/hstl_doubler',
                         int(ui.ocontroller_ui.hstl_doubler_ckbox.isChecked()))


# =============================================================================
def cmos_output_changed(ui, dds):
    """Update CMOS outputs state
    :param ui: ddsctrlui class instance (object)
    :param dds: dds class instance (object)
    :returns: None
    """
    dds.set_cmos_output_state(ui.ocontroller_ui.out_cmos_ckbox.isChecked())
    QSettings().setValue('dds_ctrl/cmos_en',
                         int(ui.ocontroller_ui.out_cmos_ckbox.isChecked()))


# =============================================================================
def apply_btn_ictrl_clicked(ui, dds, model):
    """Define action when apply button of input control form is clicked.
    :param ui: ddsctrlui class instance (object)
    :param dds: dds class instance (object)
    :returns: None
    """
    # Update input frequency
    dds.set_ifreq(ui.icontroller_ui.ifreq_dsb.value())

    # Configure System Clock
    if model == DDS_MODEL.AD9912_FX2 or model == DDS_MODEL.AD9912_STM32:
        # Update PLL attribute
        dds.set_pll_state(ui.icontroller_ui.advanced_config_gbox.isChecked())
        dds.set_pll_doubler_state(
            ui.icontroller_ui.pll_doubler_ckbox.isChecked())
        dds.set_pll_multiplier_factor(
            ui.icontroller_ui.pll_factor_sbox.value())
        dds.set_cp_current(ui.icontroller_ui.cp_current_cbox.currentIndex())
        dds.set_vco_range(ui.icontroller_ui.vco_range_cbox.currentIndex())
    elif model == DDS_MODEL.AD9915:
        # Update input PLL configuration
        dds.set_input_doubler_state(ui.icontroller_ui.input_doubler_enable_ckbox.isChecked())
        dds.set_input_divider_factor(ui.icontroller_ui.input_divider_sbox.value())
        # Update PLL attribute
        dds.set_pll_state(ui.icontroller_ui.pll_enable_ckbox.isChecked())
        dds.set_pll_multiplier_factor(
            ui.icontroller_ui.pll_multiplier_sbox.value())
        if ui.icontroller_ui.pll_enable_ckbox.isChecked() is True:
            if dds.vco_calibration() is False:
                logging.error("PLL is not locked")

    # Update system clock label after system clock configuration
    ui.icontroller_ui.sfreq_led.setText(str(dds.get_sysfreq()))

    # Force update of output frequency in case of system clock is updated
    # because output is a function of system clock frequency value.
    ofreq_changed(ui, dds)

    # Save new settings in ini file.
    settings = QSettings()
    settings.setValue('dds_ctrl/ifreq', ui.icontroller_ui.ifreq_dsb.value())
    if model == "AD9912":
        settings.setValue(
            'dds_ctrl/pll_en',
            int(ui.icontroller_ui.advanced_config_gbox.isChecked()))
        settings.setValue('dds_ctrl/pll_doubler',
                          int(ui.icontroller_ui.pll_doubler_ckbox.isChecked()))
        settings.setValue('dds_ctrl/pll_factor',
                          ui.icontroller_ui.pll_factor_sbox.value())
        settings.setValue('dds_ctrl/vco_range',
                          ui.icontroller_ui.vco_range_cbox.currentIndex())
        settings.setValue('dds_ctrl/cp_current',
                          ui.icontroller_ui.cp_current_cbox.currentIndex())



# =============================================================================
def apply_btn_octrl_clicked(ui, dds, model):
    """Define action when apply button of output control form is clicked.
    :param ui: ddsctrlui class instance (object)
    :param dds: dds class instance (object)
    :param model: dds model i.e. "AD9912" or "AD9915" (str)
    :returns: None
    """
    ofreq_changed(ui, dds)
    amplitude_changed(ui, dds)
    phase_changed(ui, dds)
    if model == DDS_MODEL.AD9912_FX2 or model == DDS_MODEL.AD9912_STM32:
        hstl_output_changed(ui, dds)
        hstl_doubler_changed(ui, dds)
        cmos_output_changed(ui, dds)


# =============================================================================
def list_registers(dds: ad9915dev.Ad9915Dev) -> None:
    for reg_name in ad9915dev.REGNAME2ADDR:
        reg_address = ad9915dev.REGNAME2ADDR[reg_name]
        reg_value = dds.get_reg(reg_address)
        print(f"{reg_name} (0x{reg_address:02X}): {reg_value:04X}")


# =============================================================================
def find_dds_board():
    """Return dictionary of AD9912 and AD9915 dds development board connected to
    computer.
    :returns: dict of lists of AD9912 and AD9915 dds (list of usb.core.Device)
    """
    # if (FX2) firmware of AD9912Dev board is not loaded, display msgbox.
    if usb.core.find(idVendor=0x0456, idProduct=0xee08) is not None or \
       usb.core.find(idVendor=0x0456, idProduct=0xee24) is not None:
        QMessageBox().warning(None, title="AD9912 detected",
                              text="FX2 interface detected but firmware is not loaded")
    # AD9912 dev board with FX2 interface (native USB port of eval board)
    ad9912devFx2 = usb.core.find(idVendor=0x0456, idProduct=0xee09, find_all=True)
    # AD9912 dev board with STM32 uC interface (external control of eval board)
    ad9912devStm32 = usb.core.find(idVendor=0x0483, find_all=True)
    # AD9915 dev board (through FTDI UM232H)
    ad9915dev = usb.core.find(idVendor=0x0403, idProduct=0x6014, find_all=True)
    return {
        'ad9912devFx2': ad9912devFx2, 
        'ad9912devStm32': ad9912devStm32, 
        'ad9915dev': ad9915dev,
        }


# =============================================================================
def ddscontroller(ini_file, window_title,
                  dds_model=None,
                  dds_bus=None, dds_address=None,
                  serial_device=None):
    """Main program of DDS controller project.
    :returns: None
    """
    app = QApplication(sys.argv)
    app.setOrganizationName(ORGANIZATION)
    app.setApplicationName(APP_NAME)

    if dds_model is None or dds_bus is None or dds_address is None or serial_device is None:
        # Select board to handle
        dds_boards = find_dds_board()
        if 'ad9912devStm32' in dds_boards:
            dds_serial = serial.tools.list_ports.grep("0483*")
            dds_boards_for_human =  [
                f"AD9912 with serial ID {dds.device}" for dds in dds_serial
            ]
        else:
            dds_boards_for_human = []
        dds_boards_for_human += \
            ["AD9912 on bus {} address {}".format(dds.bus, dds.address)
             for dds in dds_boards['ad9912devFx2']] + \
            ["AD9915 on bus {} address {}".format(dds.bus, dds.address)
             for dds in dds_boards['ad9915dev']]
        msg = DdsBoardSelection(dds_boards_for_human)
        retval = msg.exec_()
        if retval == 1:
            dds_item = msg.dds_selected()
            if dds_item.split()[0] == "AD9915":
                dds_model = DDS_MODEL.AD9915
                dds_bus = int(dds_item.split()[3])
                dds_address = int(dds_item.split()[5])
            elif dds_item.split()[0] == "AD9912Fx2":
                dds_model = DDS_MODEL.AD9912_FX2
                dds_bus = int(dds_item.split()[3])
                dds_address = int(dds_item.split()[5])
            else:
                dds_model= DDS_MODEL.AD9912_STM32
                print(f"dds_item: {dds_item}")
                serial_device = dds_item.split()[4]
        else:
            sys.exit()
    if dds_model == DDS_MODEL.AD9912_FX2:
        intf = Fx2Intf(
            vendor_id=0x0456,
            product_id=0xee09,
            bus=dds_bus,
            address=dds_address)
        dds = sad9912dev.SAd9912Dev(intf)
        retval = dds.connect()
        if retval is False:
            logging.critical('DDS connection problem')
            QMessageBox.critical(None, 'DDS connection error',
                                 'DDS connection problem\nCheck connection')
            sys.exit()
        ui = DdsCtrlUi(DDS_MODEL.AD9912_FX2, dds)
    elif dds_model == DDS_MODEL.AD9912_STM32:
            intf = Stm32Intf(port=serial_device)
            dds = sad9912dev.SAd9912Dev(intf)
            retval = dds.connect()
            if retval is False:
                logging.critical('DDS connection problem')
                QMessageBox.critical(None, 'DDS connection error',
                                     'DDS connection problem\nCheck connection')
                sys.exit()
            ui = DdsCtrlUi(DDS_MODEL.AD9912_STM32, dds)
    elif dds_model == DDS_MODEL.AD9915:
        dds = sad9915dev.SAd9915Dev()
        url = "ftdi://ftdi:232h:{:x}:{:x}/1".format(dds_bus, dds_address)
        retval = dds.connect(url)
        if retval is False:
            logging.critical('DDS connection problem')
            QMessageBox.critical(None, 'DDS connection error',
                                 'DDS connection problem\nCheck connection')
            sys.exit()
        dds.set_operation_mode(ad9915dev.OPMODE['modulus'])  # Fine output frequency tuning mode 
        ui = DdsCtrlUi(DDS_MODEL.AD9915, dds)
    else:
        logging.critical('DDS selection problem')
        QMessageBox.critical(None, 'DDS selection error',
                             'Problem with the DDS selection')
        sys.exit()

    if path.isfile(ini_file) is True:
        check_ini_file(ini_file)
    else:
        check_ini_file()

    init_dev(dds, dds_model)
    init_ui(ui, dds_model, dds)
    handle_ocontroller_ui_behavior(ui, dds, dds_model)

    # Logic
    # # Following parameters, input freq, pll state, pll doubler state and
    # # pll factor, modify system clock frequency which in turn modify
    # # output frequency. Behavior of UI can be:
    # # - update display of output frequency to new dds frequency
    # # - update dds frequency to current display of output frequency
    # # Choice was taken to used the last behavior.
    dds.ifreq_updated.connect(lambda **kwargs: ofreq_changed(ui, dds))
    if dds_model == DDS_MODEL.AD9912_FX2 \
        or dds_model == DDS_MODEL.AD9912_STM32:
        dds.pll_state_updated.connect(
            lambda **kwargs: ofreq_changed(ui, dds))
        dds.pll_doubler_updated.connect(
            lambda **kwargs: ofreq_changed(ui, dds))
        dds.pll_factor_updated.connect(
            lambda **kwargs: ofreq_changed(ui, dds))

    ui.ocontroller_ui.auto_update_ckbox.stateChanged.connect(
        lambda: handle_ocontroller_ui_behavior(ui, dds, dds_model))
    ui.ocontroller_ui.auto_update_ckbox.stateChanged.connect(
        lambda: QSettings().setValue('dds_ctrl/auto_update',
            ui.ocontroller_ui.auto_update_ckbox.isChecked()))

    ui.icontroller_ui.apply_btn.clicked.connect(
        lambda: apply_btn_ictrl_clicked(ui, dds, dds_model))

    ui.debug_ui.get_reg_btn.clicked.connect(
        lambda: get_reg_btn_clicked(ui, dds))
    ui.debug_ui.set_reg_btn.clicked.connect(
        lambda: set_reg_btn_clicked(ui, dds))

    form = QMainWindow()
    form.setCentralWidget(ui)
    form.setWindowTitle(window_title)
    form.show()

    sys.exit(app.exec_())


#==============================================================================
if __name__ == '__main__':
    # Setup logger
    configure_logging()
    # Parse command line
    args = parse_cmd_line()
    # Setup application
    ddscontroller(args.ini_file, args.wtitle,
                  args.model, args.bus, args.address)
