from typing import (
List, Dict, Optional
)
import copy
import time
import random
from math import sqrt, log10
from enum import Enum, IntFlag
import serial
import json
import asyncio
import numpy as np
from scipy import interpolate
from rfblocks import (
ad9552, ad9913, AD9913Controller,
ModulusConstraintException,
list_available_serial_ports, create_serial,
write_cmd, query_cmd, DEFAULT_SOCKET_URL
)
from qtrfblocks import (
ClkModule
)
from qasync import (
QEventLoop, QThreadExecutor, asyncSlot, asyncClose
)
from PyQt5.QtWidgets import (
QWidget, QLabel, QAbstractSpinBox, QDoubleSpinBox, QVBoxLayout,
QLineEdit, QHBoxLayout, QGroupBox, QMainWindow, QComboBox,
QCheckBox, QPushButton, QRadioButton, QButtonGroup, QMessageBox,
QFormLayout, QErrorMessage, QApplication, QScrollArea,
QMenu, QFileDialog, QSpinBox, QDialog, QDialogButtonBox,
QAction, QActionGroup
)
from PyQt5.QtCore import (
Qt, QObject, pyqtSlot
)
class ModulationType(IntFlag):
FREQUENCY = 0b0
PHASE = 0b1
[docs]class DDSChan(QObject):
MIN_FREQUENCY: float = 0.0
MAX_FREQUENCY: float = 100.0
MIN_RATE: float = 0.004
MAX_RATE: float = 262.0
MIN_PHASE: float = 0.0
MAX_PHASE: float = 360.0
FREQ_SUFFIX: str = ' MHz'
RATE_SUFFIX: str = ' uS'
PHASE_SUFFIX: str = u"\N{DEGREE SIGN}"
FREQ_RANGE: List[float] = [MIN_FREQUENCY, MAX_FREQUENCY]
PHASE_RANGE: List[float] = [MIN_PHASE, MAX_PHASE]
RATE_RANGE: List[float] = [MIN_RATE, MAX_RATE]
LVL_UNITS = Enum('Lvl_Units', 'DAC MV DBM')
# The following number is derived from measurement of
# the DDS_2 module output RMS voltages
# DAC_TO_MILLIVOLTS = 510/1013
DAC_CODE_RANGE: List[int] = [1, 1023]
# MILLIVOLTS_RANGE = [DAC_TO_MILLIVOLTS * 1, DAC_TO_MILLIVOLTS * 1023]
# DBM_RANGE = [10*log10( ((DAC_TO_MILLIVOLTS * 1e-3) * 1)**2 / 50e-3 ),
# 10*log10( ((DAC_TO_MILLIVOLTS * 1e-3) * 1023)**2 / 50e-3 )]
def __init__(self,
app: QWidget,
controller: AD9913Controller):
"""Create an instance of ``DDSChan``.
:param app: The parent application for the channel display.
:type app: QWidget
:param controller: AD9913 DDS controller
:type controller: :py:class:`rfblocks.AD9913Controller`
"""
super().__init__(app)
self._app: QWidget = app
self._controller: AD9913Controller = controller
self._min_freq: float = DDSChan.MIN_FREQUENCY
self._max_freq: float = DDSChan.MAX_FREQUENCY
self._level_units: Enum = DDSChan.LVL_UNITS.DBM
self.label: str = self._controller.label
self._modulation_type: ModulationType = ModulationType.FREQUENCY
self._modulation_tones: int = 2
self._modulating: bool = False
self._group_box: Optional[QGroupBox] = None
@classmethod
def level_units_control(cls,
app: QWidget,
parent: QObject) -> QPushButton:
level_units_btn: QPushButton = QPushButton("Units")
level_units_menu: QMenu = QMenu()
units_group: QActionGroup = QActionGroup(app)
dac_units_action: QAction = QAction('DAC Code', units_group,
triggered=parent.set_dac_units,
checkable=True)
mv_units_action: QAction = QAction('Millivolts', units_group,
triggered=parent.set_millivolt_units,
checkable=True)
dbm_units_action: QAction = QAction('dBm', units_group,
triggered=parent.set_dbm_units,
checkable=True)
dbm_units_action.setChecked(True)
level_units_menu.addAction(dac_units_action)
level_units_menu.addAction(mv_units_action)
level_units_menu.addAction(dbm_units_action)
level_units_btn.setMenu(level_units_menu)
return level_units_btn
@property
def ctl(self) -> AD9913Controller:
"""Return a reference to the :py:class:`AD9913Controller`.
"""
return self._controller
@classmethod
def level_units_control(cls,
app: QWidget,
parent: QObject) -> QPushButton:
level_units_btn: QPushButton = QPushButton("Units")
level_units_menu: QMenu = QMenu()
units_group: QActionGroup = QActionGroup(app)
dac_units_action: QAction = QAction('DAC Code', units_group,
triggered=parent.set_dac_units,
checkable=True)
mv_units_action: QAction = QAction('Millivolts', units_group,
triggered=parent.set_millivolt_units,
checkable=True)
dbm_units_action: QAction = QAction('dBm', units_group,
triggered=parent.set_dbm_units,
checkable=True)
dbm_units_action.setChecked(True)
level_units_menu.addAction(dac_units_action)
level_units_menu.addAction(mv_units_action)
level_units_menu.addAction(dbm_units_action)
level_units_btn.setMenu(level_units_menu)
return level_units_btn
[docs] def build_ui(self) -> QGroupBox:
"""Build on-screen UI components for a DDS_2 channel.
"""
self._group_box = QGroupBox("{} ({})".format(
self.label, self.ctl._ad9913.model))
vbox: QVBoxLayout = QVBoxLayout()
fbox: QFormLayout = QFormLayout()
self._freq_box = QDoubleSpinBox()
self._freq_box.setRange(self.min_freq, self.max_freq)
self._freq_box.setDecimals(5)
self._freq_box.setStyleSheet("""QDoubleSpinBox {
font-size: 25pt; }""")
self._freq_box.setValue(self.freq)
self._freq_box.setSuffix(DDSChan.FREQ_SUFFIX)
self._freq_box.valueChanged.connect(self.set_freq)
fbox.addRow(QLabel("Freq.:"), self._freq_box)
self._ph_box = QDoubleSpinBox()
self._ph_box.setRange(*DDSChan.PHASE_RANGE)
self._ph_box.setDecimals(2)
self._ph_box.setStyleSheet("""QDoubleSpinBox {
font-size: 25pt; }""")
self._ph_box.setValue(self.phase)
self._ph_box.setSuffix(DDSChan.PHASE_SUFFIX)
self._ph_box.valueChanged.connect(self.set_phase)
fbox.addRow(QLabel("Phase:"), self._ph_box)
hbox3: QHBoxLayout = QHBoxLayout()
self._lvl_box = QDoubleSpinBox()
self._lvl_box.setStyleSheet("""QDoubleSpinBox {
font-size: 25pt; }""")
self._lvl_box.setRange(*self.dbm_range)
self._lvl_box.setDecimals(1)
self._lvl_box.setSuffix(' dBm')
self._lvl_box.setValue(self.dbm)
self._lvl_box.valueChanged.connect(self.set_dbm)
hbox3.addWidget(self._lvl_box)
level_units_btn: QPushButton = DDSChan.level_units_control(
self._app, self)
hbox3.addWidget(level_units_btn)
fbox.addRow(QLabel("Level:"), hbox3)
self._pmod_box: QCheckBox = QCheckBox("")
self._pmod_box.setChecked(False)
self._pmod_box.stateChanged.connect(self.set_pmod)
lbl: QLabel = QLabel("Programmable:\nModulus")
lbl.setWordWrap(True)
fbox.addRow(lbl, self._pmod_box)
self._state_box: QCheckBox = QCheckBox("")
if self.set_state == ad9913.POWER_UP:
self._state_box.setChecked(True)
else:
self._state_box.setChecked(False)
self._state_box.stateChanged.connect(self.set_state)
fbox.addRow(QLabel("Active:"), self._state_box)
vbox.addLayout(fbox)
# ----------------------------------------------------
# Module hardware configure
#
hbox3 = QHBoxLayout()
self._config_btn = QPushButton("Configure")
self._config_btn.clicked.connect(self.configure)
hbox3.addWidget(self._config_btn)
hbox3.addStretch()
vbox.addLayout(hbox3)
hbox2: QHBoxLayout = QHBoxLayout()
self._sweep_btn: QPushButton = QPushButton("Sweep...")
self._sweep_btn.clicked.connect(self.show_sweep)
hbox2.addWidget(self._sweep_btn)
self._profile_btn = QPushButton("Profiles...")
self._profile_btn.clicked.connect(self.show_profiles)
hbox2.addWidget(self._profile_btn)
hbox2.addStretch()
vbox.addLayout(hbox2)
self.ctl.state_changed.connect(self.update_chan_state)
self.ctl.freq_changed.connect(self.update_chan_freq)
self.ctl.phase_changed.connect(self.update_chan_phase)
self.ctl.level_changed.connect(self.update_chan_level)
self.ctl.pmod_changed.connect(self.update_chan_pmod)
self._group_box.setLayout(vbox)
return self._group_box
def set_dac_units(self) -> None:
self._level_units = DDSChan.LVL_UNITS.DAC
self._lvl_box.disconnect()
self._lvl_box.setRange(*DDSChan.DAC_CODE_RANGE)
self._lvl_box.setDecimals(0)
self._lvl_box.setSuffix('')
self._lvl_box.setValue(self.level)
self._lvl_box.valueChanged.connect(self.set_level)
def set_millivolt_units(self) -> None:
self._level_units = DDSChan.LVL_UNITS.MV
self._lvl_box.disconnect()
self._lvl_box.setRange(*self.millivolts_range)
self._lvl_box.setDecimals(1)
self._lvl_box.setSuffix(' mV')
self._lvl_box.setValue(self.millivolts)
self._lvl_box.valueChanged.connect(self.set_millivolts)
def set_dbm_units(self) -> None:
self._level_units = DDSChan.LVL_UNITS.DBM
self._lvl_box.disconnect()
self._lvl_box.setRange(*self.dbm_range)
self._lvl_box.setDecimals(1)
self._lvl_box.setSuffix(' dBm')
self._lvl_box.setValue(self.dbm)
self._lvl_box.valueChanged.connect(self.set_dbm)
@property
def millivolts_range(self) -> List[float]:
return [self.ctl.level_to_millivolts(l) for l in DDSChan.DAC_CODE_RANGE]
@property
def dbm_range(self) -> List[float]:
return [self.ctl.level_to_dbm(l) for l in DDSChan.DAC_CODE_RANGE]
@property
def enabled(self) -> bool:
"""Enable/disable the on screen clock module UI components.
"""
if self._group_box:
return self._group_box.isEnabled()
else:
return False
@enabled.setter
def enabled(self, en: bool) -> None:
if self._group_box:
self._group_box.setEnabled(en)
@property
def state(self) -> int:
"""The state of the DDS channel. Either powered up or
powered down.
"""
return self.ctl.state
[docs] def set_state(self, flag: int):
"""Set the DDS channel state.
:param flag: This should be either :py:`ad9913.POWER_UP` or
:py:`ad9913.POWER_DOWN`.
:type flag: int
"""
self.ctl.state = flag
[docs] @pyqtSlot(int)
def update_chan_state(self, state: int) -> None:
"""Update the displayed DDS channel state.
"""
if state == ad9913.POWER_UP:
self._state_box.setChecked(True)
else:
self._state_box.setChecked(False)
@property
def chan_id(self) -> str:
"""The channel identifier.
"""
return self.ctl._controller_id
def write_chan_cmd(self, cmd: str) -> None:
try:
with create_serial(self._app.ctl_device,
self._app.baudrate) as ser:
write_cmd(ser, cmd)
except serial.serialutil.SerialException as se:
print(str(se))
@property
def freq(self) -> float:
"""Return the DDS output frequency (in MHz).
"""
return self.ctl.freq
[docs] def set_freq(self, f: float) -> None:
"""Set the DDS output frequency (in MHz).
Note that :py:meth:`configure` must be invoked in order to
update the DDS hardware
"""
self.ctl.freq = f
# If necessary, recalculate the output millivolts or dBm
if self._level_units == DDSChan.LVL_UNITS.MV:
self._lvl_box.setValue(self.ctl.millivolts)
self._lvl_box.setRange(*self.millivolts_range)
elif self._level_units == DDSChan.LVL_UNITS.DBM:
self._lvl_box.setValue(self.ctl.dbm)
self._lvl_box.setRange(*self.dbm_range)
[docs] @pyqtSlot(float)
def update_chan_freq(self, f: float) -> None:
"""Update the displayed DDS channel frequency.
"""
self._freq_box.setValue(f)
@property
def phase(self) -> float:
"""Return the DDS phase offset (in degrees).
"""
return self.ctl.phase
[docs] def set_phase(self, p: float) -> None:
"""Set the DDS phase offset (in degrees).
Note that :py:meth:`configure` must be invoked in order to
update the DDS hardware
"""
self.ctl.phase = p
[docs] @pyqtSlot(float)
def update_chan_phase(self, p: float) -> None:
"""Update the displayed DDS channel phase offset.
"""
self._ph_box.setValue(p)
@property
def level_units(self) -> Enum:
return self._level_units
@property
def level(self) -> float:
"""Return the DDS output level (in DDS DAC units).
"""
return self.ctl.level
[docs] def set_level(self, l: float) -> None:
"""Set the DDS output level (in DDS DAC units).
Note that :py:meth:`configure` must be invoked in order to
update the DDS hardware
"""
self.ctl.level = l
@property
def millivolts(self) -> float:
"""Return the DDS output level (in millivolts RMS)
"""
return self.ctl.millivolts
[docs] def set_millivolts(self, mv: float):
"""Set the DDS output level (in millivolts).
Note that :py:meth:`configure` must be invoked in order to
update the DDS hardware
"""
self.ctl.millivolts = mv
@property
def dbm(self) -> float:
"""Return the DDS output level (in dBm).
"""
return self.ctl.dbm
[docs] def set_dbm(self, d: float):
"""Set the DDS output level (in dBm)
Note that :py:meth:`configure` must be invoked in order to
update the DDS hardware
"""
self.ctl.dbm = d
[docs] @pyqtSlot(float)
def update_chan_level(self, l: float) -> None:
"""Update the displayed DDS channel output level.
"""
if self._level_units == DDSChan.LVL_UNITS.MV:
self._lvl_box.setValue(self.ctl.level_to_millivolts(l))
elif self._level_units == DDSChan.LVL_UNITS.DBM:
self._lvl_box.setValue(self.ctl.level_to_dbm(l))
else:
self._lvl_box.setValue(l)
@property
def min_freq(self) -> float:
return self._min_freq
@min_freq.setter
def min_freq(self, f: float) -> None:
self._min_freq = f
@property
def max_freq(self) -> float:
return self._max_freq
@max_freq.setter
def max_freq(self, f: float) -> None:
self._max_freq = f
[docs] def set_pmod(self, state: bool) -> None:
"""Set the DDS programmable modulus state.
Note that :py:meth:`configure` must be invoked in order to
update the DDS hardware
"""
self.ctl.pmod = state
if state:
# Disable direct switch mode when enabling
# programmable modulus
self.set_direct_switch_enabled(False)
[docs] @pyqtSlot(bool)
def update_chan_pmod(self, state: bool) -> None:
"""Update the displayed prog. modulus checkbox.
"""
self._pmod_box.setChecked(state)
@property
def direct_switch_enabled(self) -> bool:
return self.ctl.direct_switch_enabled
def set_direct_switch_enabled(self, enable: bool) -> None:
self.ctl.direct_switch_enabled = enable
@property
def selected_profile(self) -> int:
return self.ctl.selected_profile
def set_selected_profile(self,
state: bool,
profnum: int) -> None:
if state:
self.ctl.selected_profile = profnum
@property
def profile_type(self) -> ad9913.SweepType:
return self.ctl.profile_type
def set_profile_type(self,
state: bool,
ptype: ad9913.SweepType) -> None:
if state:
self.ctl.profile_type = ptype
@property
def modulation_type(self) -> ModulationType:
return self._modulation_type
def set_modulation_type(self,
state: bool,
mtype: ModulationType) -> None:
if state:
self._modulation_type = mtype
@property
def modulation_tones(self) -> int:
return self._modulation_tones
def set_modulation_tones(self,
state: bool,
mtones: int) -> None:
if state:
self._modulation_tones = mtones
@property
def modulating(self) -> bool:
return self._modulating
def set_modulating(self, m: bool) -> None:
self._modulating = m
def show_sweep(self) -> None:
sweep_dialog = SweepDialog(self._app, self)
sweep_dialog.exec_()
def show_profiles(self) -> None:
profiles_dialog = ProfilesDialog(self._app,
self, self.ctl.profiles)
profiles_dialog.exec_()
[docs] def dump_config(self) -> Dict:
"""Return the current configuration for this clock module.
:return: A dictionary containing the current clock modules
configuration:
:freq: The currently set output frequency (in MHz).
:channels: A dictionary containing the channel configurations
keyed using the channel id.
"""
return self.ctl.dump_config()
[docs] def load_config(self, config: Dict) -> None:
"""Set the current configuration for this clock module.
:param config: A dictionary containing the module configuration
to be set.
:type config: Dict
"""
self.ctl.load_config(config)
[docs] def initialize_hw(self, ser: serial.Serial) -> None:
"""Initialize the DDS board hardware.
"""
self.ctl.initialize(ser)
def configure_hw(self, ser: serial.Serial) -> None:
self.ctl.configure(ser)
[docs] def initialize_ui(self) -> None:
"""Initialize the user interface.
"""
self.configure_ui(self.ctl._initial_config)
def configure_ui(self, config: Dict) -> None:
self._freq_box.setValue(config['freq'])
self._ph_box.setValue(config['ph'])
self.set_level(config['lvl'])
if self._level_units == DDSChan.LVL_UNITS.DAC:
self._lvl_box.setValue(self.ctl.level)
elif self._level_units == DDSChan.LVL_UNITS.MV:
self._lvl_box.setValue(self.ctl.millivolts)
else:
self._lvl_box.setValue(self.ctl.dbm)
self._pmod_box.setChecked(config['pmod'])
self._sweep = copy.deepcopy(config['sweep'])
async def modulate(self) -> None:
#
# Use up to 8 profiles to generate the required modulation
# tones. We assume that the profile frequencies and/or
# phases have been set appropriately.
# The profiles are used as follows:
#
# Tones Profile numbers
# 2 00, 01
# 4 00, 01, 10, 11
# 8 000, 001, 010, 011, 100, 101, 110, 111
#
port = self.ctl._ad9913.ps_port
if not port:
print("Channel is not configured for modulation")
return
loop = asyncio.get_event_loop()
mask_str = '{:02X}'.format(self.ctl._ad9913.ps_mask)
tones = self.modulation_tones
rgen = random.Random()
self.ctl._ad9913.set_profile_control(ad9913.ProfileControl.PINS)
self.ctl._ad9913.set_direct_switch_mode_enable(True)
if self.modulation_type == ModulationType.FREQUENCY:
self.ctl._ad9913.set_sweep_type(ad9913.SweepType.FREQUENCY)
else:
self.ctl._ad9913.set_sweep_type(ad9913.SweepType.PHASE)
cmd = self.ctl._ad9913.config_cfr1()
await loop.run_in_executor(None, self.write_chan_cmd, cmd)
while self.modulating:
symbols = [round(rgen.random() * (tones-1)) for i in range(100)]
signal_str = ','.join(['{:02X}'.format(b) for b in
[self.ctl._ad9913.ps_bits(s) for s in symbols]])
cmd = 'M{},{},{}:'.format(port, mask_str, signal_str)
await loop.run_in_executor(None, self.write_chan_cmd, cmd)
# ----------------------------------------------------------
# DDS channel dialogs
[docs]class SweepDialog(QDialog):
"""
"""
def __init__(self, ddsApp, ddsChan, parent=None):
QDialog.__init__(self, parent)
self._app = ddsApp
self._chan = ddsChan
self._start_box = None
self._sweep_config = ddsChan.ctl.sweep_config
self._limit_suffix = DDSChan.FREQ_SUFFIX
self._limit_range = DDSChan.FREQ_RANGE
self._step_suffix = DDSChan.FREQ_SUFFIX
self._step_range = DDSChan.FREQ_RANGE
self._rate_suffix = DDSChan.RATE_SUFFIX
self._rate_range = DDSChan.RATE_RANGE
self.setWindowTitle("{} Sweep Settings".format(ddsChan.label))
self.build_ui()
@property
def ctl(self):
return self._chan.ctl
@property
def start(self):
return self._sweep_config['start']
def set_start(self, s):
self._sweep_config['start'] = s
@property
def end(self):
return self._sweep_config['end']
def set_end(self, e):
self._sweep_config['end'] = e
@property
def sweep_type(self):
return self._sweep_config['type']
def set_sweep_type(self, t):
self._sweep_config['type'] = t
self.set_sweep_range()
[docs] def set_sweep_range(self):
"""Set the sweep range and suffix.
"""
t = self._sweep_config['type']
if t in [ad9913.SweepType.FREQUENCY]:
self._limit_suffix = self._step_suffix = DDSChan.FREQ_SUFFIX
self._limit_range = self._step_range = DDSChan.FREQ_RANGE
else:
self._limit_suffix = self._step_suffix = DDSChan.PHASE_SUFFIX
self._limit_range = self._step_range = DDSChan.PHASE_RANGE
if self._start_box:
self._start_box.setSuffix(self._limit_suffix)
self._start_box.setRange(*self._limit_range)
self._end_box.setSuffix(self._limit_suffix)
self._end_box.setRange(*self._limit_range)
self._rising_step_box.setSuffix(self._limit_suffix)
self._rising_step_box.setRange(*self._step_range)
self._falling_step_box.setSuffix(self._limit_suffix)
self._falling_step_box.setRange(*self._step_range)
@property
def ramp_type(self):
return self._sweep_config['ramp']
def set_ramp_type(self, r):
self._sweep_config['ramp'] = r
@property
def rising_step(self):
return self._sweep_config['delta'][0]
def set_rising_step(self, r):
self._sweep_config['delta'][0] = r
@property
def falling_step(self):
return self._sweep_config['delta'][1]
def set_falling_step(self, r):
self._sweep_config['delta'][1] = r
@property
def rising_rate(self):
return self._sweep_config['rate'][0]
def set_rising_rate(self, r):
self._sweep_config['rate'][0] = r
@property
def falling_rate(self):
return self._sweep_config['rate'][1]
def set_falling_rate(self, r):
self._sweep_config['rate'][1] = r
def build_ui(self):
outer_vbox = QVBoxLayout()
outer_hbox = QHBoxLayout()
outer_vbox.addLayout(outer_hbox)
vbox1 = QVBoxLayout()
vbox2 = QVBoxLayout()
outer_hbox.addLayout(vbox1)
outer_hbox.addLayout(vbox2)
self._sweep_gbox = QGroupBox("Sweep Parameters")
fbox1 = QFormLayout()
# ----------------------------------------------------
# Sweep type
#
hbox1 = QHBoxLayout()
self._sweep_type_widgets = {}
type_group = QButtonGroup(hbox1)
rb = QRadioButton("Freq.")
type_group.addButton(rb)
rb.sweep_type = ad9913.SweepType.FREQUENCY
rb.toggled.connect(lambda state, w=rb: self.set_sweep_type(w.sweep_type))
hbox1.addWidget(rb)
self._sweep_type_widgets[rb.sweep_type] = rb
rb = QRadioButton("Phase")
type_group.addButton(rb)
rb.sweep_type = ad9913.SweepType.PHASE
rb.toggled.connect(lambda state, w=rb: self.set_sweep_type(w.sweep_type))
hbox1.addWidget(rb)
self._sweep_type_widgets[rb.sweep_type] = rb
self._sweep_type_widgets[self.sweep_type].setChecked(True)
fbox1.addRow(QLabel("Type:"), hbox1)
# ----------------------------------------------------
# Start and end
#
self._start_box = QDoubleSpinBox()
self._start_box.setRange(*self._limit_range)
self._start_box.setDecimals(5)
self._start_box.setSuffix(self._limit_suffix)
self._start_box.setValue(self.start)
self._start_box.valueChanged.connect(self.set_start)
fbox1.addRow(QLabel("Start:"), self._start_box)
self._end_box = QDoubleSpinBox()
self._end_box.setRange(*self._limit_range)
self._end_box.setDecimals(5)
self._end_box.setSuffix(self._limit_suffix)
self._end_box.setValue(self.end)
self._end_box.valueChanged.connect(self.set_end)
fbox1.addRow(QLabel("End:"), self._end_box)
# ----------------------------------------------------
# Ramp type
#
hbox2 = QHBoxLayout()
self._ramp_type_widgets = {}
ramp_group = QButtonGroup(hbox2)
rb = QRadioButton("Up")
ramp_group.addButton(rb)
rb.ramp_type = ad9913.SweepRampType.RAMP_UP
rb.toggled.connect(lambda state, w=rb: self.set_ramp_type(w.ramp_type))
hbox2.addWidget(rb)
self._ramp_type_widgets[rb.ramp_type] = rb
rb = QRadioButton("Down")
ramp_group.addButton(rb)
rb.ramp_type = ad9913.SweepRampType.RAMP_DOWN
rb.toggled.connect(lambda state, w=rb: self.set_ramp_type(w.ramp_type))
hbox2.addWidget(rb)
self._ramp_type_widgets[rb.ramp_type] = rb
rb = QRadioButton("BiDir.")
ramp_group.addButton(rb)
rb.ramp_type = ad9913.SweepRampType.RAMP_BIDIR
rb.toggled.connect(lambda state, w=rb: self.set_ramp_type(w.ramp_type))
hbox2.addWidget(rb)
self._ramp_type_widgets[rb.ramp_type] = rb
rb = QRadioButton("Sweep Off")
ramp_group.addButton(rb)
rb.ramp_type = ad9913.SweepRampType.SWEEP_OFF
rb.toggled.connect(lambda state, w=rb: self.set_ramp_type(w.ramp_type))
hbox2.addWidget(rb)
self._ramp_type_widgets[rb.ramp_type] = rb
self._ramp_type_widgets[self.ramp_type].setChecked(True)
fbox1.addRow(QLabel("Ramp:"), hbox2)
# ----------------------------------------------------
# Step
#
self._rising_step_box = QDoubleSpinBox()
self._rising_step_box.setRange(*self._step_range)
self._rising_step_box.setDecimals(5)
self._rising_step_box.setSuffix(self._limit_suffix)
self._rising_step_box.setValue(self.rising_step)
self._rising_step_box.valueChanged.connect(self.set_rising_step)
fbox1.addRow(QLabel("Rising Step:"), self._rising_step_box)
self._falling_step_box = QDoubleSpinBox()
self._falling_step_box.setRange(*self._step_range)
self._falling_step_box.setDecimals(5)
self._falling_step_box.setSuffix(self._limit_suffix)
self._falling_step_box.setValue(self.falling_step)
self._falling_step_box.valueChanged.connect(self.set_falling_step)
fbox1.addRow(QLabel("Falling Step:"), self._falling_step_box)
# ----------------------------------------------------
# Rate
#
self._rise_rate_box = QDoubleSpinBox()
self._rise_rate_box.setRange(*self._rate_range)
self._rise_rate_box.setDecimals(3)
self._rise_rate_box.setSuffix(self._rate_suffix)
self._rise_rate_box.setValue(self.rising_rate)
self._rise_rate_box.valueChanged.connect(self.set_rising_rate)
fbox1.addRow(QLabel("Rise Rate:"), self._rise_rate_box)
self._fall_rate_box = QDoubleSpinBox()
self._fall_rate_box.setRange(*self._rate_range)
self._fall_rate_box.setDecimals(3)
self._fall_rate_box.setSuffix(self._rate_suffix)
self._fall_rate_box.setValue(self.falling_rate)
self._fall_rate_box.valueChanged.connect(self.set_falling_rate)
fbox1.addRow(QLabel("Fall Rate:"), self._fall_rate_box)
hbox3 = QHBoxLayout()
config_btn = QPushButton("Configure")
start_btn = QPushButton("Start Sweep")
stop_btn = QPushButton("Stop Sweep")
config_btn.clicked.connect(self.configure)
start_btn.clicked.connect(self.start_sweep)
stop_btn.clicked.connect(self.stop_sweep)
hbox3.addStretch()
hbox3.addWidget(config_btn)
hbox3.addWidget(start_btn)
hbox3.addWidget(stop_btn)
fbox1.addRow(hbox3)
self._sweep_gbox.setLayout(fbox1)
vbox1.addWidget(self._sweep_gbox)
# ----------------------------------------------------
# self._modulation_gbox = QGroupBox("Modulation")
# vbox2.addWidget(self._modulation_gbox)
bbox = QDialogButtonBox(QDialogButtonBox.Close)
bbox.rejected.connect(self.reject)
outer_vbox.addWidget(bbox)
self.ctl.sweep_start_changed.connect(self.update_sweep_start)
self.ctl.sweep_end_changed.connect(self.update_sweep_end)
self.ctl.sweep_type_changed.connect(self.update_sweep_type)
self.ctl.ramp_type_changed.connect(self.update_ramp_type)
self.ctl.rising_step_changed.connect(self.update_rising_step)
self.ctl.falling_step_changed.connect(self.update_falling_step)
self.ctl.rising_rate_changed.connect(self.update_rising_rate)
self.ctl.falling_rate_changed.connect(self.update_falling_rate)
self.setLayout(outer_vbox)
[docs] @pyqtSlot(float)
def update_sweep_start(self, start):
"""Update the displayed sweep start value.
"""
self._start_box.setValue(start)
[docs] @pyqtSlot(float)
def update_sweep_end(self, end):
"""Update the displayed sweep end value.
"""
self._end_box.setValue(end)
[docs] @pyqtSlot(ad9913.SweepType)
def update_sweep_type(self, st):
"""Update the displayed sweep type.
This also sets the sweep range and suffix to the
values appropriate for the sweep type.
"""
self._sweep_type_widgets[st].setChecked(True)
self.set_sweep_range()
[docs] @pyqtSlot(ad9913.SweepRampType)
def update_ramp_type(self, rt):
"""Update the displayed sweep ramp type.
"""
self._ramp_type_widgets[rt].setChecked(True)
[docs] @pyqtSlot(float)
def update_rising_step(self, v):
"""Update the displayed sweep rising step value.
"""
self._rising_step_box.setValue(v)
[docs] @pyqtSlot(float)
def update_falling_step(self, v):
"""Update the displayed sweep falling step value.
"""
self._falling_step_box.setValue(v)
[docs] @pyqtSlot(float)
def update_rising_rate(self, v):
"""Update the displayed sweep rising rate value.
"""
self._rise_rate_box.setValue(v)
[docs] @pyqtSlot(float)
def update_falling_rate(self, v):
"""Update the displayed sweep falling rate value.
"""
self._fall_rate_box.setValue(v)
@asyncSlot()
async def configure(self):
try:
with create_serial(self._app.ctl_device,
self._app.baudrate) as ser:
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, self.ctl.configure_sweep, ser)
except serial.serialutil.SerialException as se:
error_dialog = QErrorMessage(self)
error_dialog.showMessage(str(se))
@asyncSlot()
async def start_sweep(self):
try:
with create_serial(self._app.ctl_device,
self._app.baudrate) as ser:
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, self.ctl.start_sweep, ser)
except serial.serialutil.SerialException as se:
error_dialog = QErrorMessage(self)
error_dialog.showMessage(str(se))
@asyncSlot()
async def stop_sweep(self):
try:
with create_serial(self._app.ctl_device,
self._app.baudrate) as ser:
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, self.ctl.stop_sweep, ser)
except serial.serialutil.SerialException as se:
error_dialog = QErrorMessage(self)
error_dialog.showMessage(str(se))
[docs] def reject(self):
QDialog.reject(self)
[docs]class ProfilesDialog(QDialog):
"""
"""
def __init__(self, ddsApp, ddsChan, profiles, parent=None):
QDialog.__init__(self, parent)
self._app = ddsApp
self._chan = ddsChan
self._profiles = profiles
self.setWindowTitle("{} Profiles Settings".format(ddsChan.label))
self.build_ui()
@property
def ctl(self):
return self._chan.ctl
def profile_freq(self, profnum):
return self._profiles[profnum][0]
def set_profile_freq(self, profnum, f):
self._profiles[profnum][0] = f
def profile_phase(self, profnum):
return self._profiles[profnum][1]
def set_profile_phase(self, profnum, p):
self._profiles[profnum][1] = p
def profiles_enable(self, enable):
if enable:
self._profile_select_gbox.setEnabled(True)
self._profiles_gbox.setEnabled(True)
else:
self._profiles_gbox.setEnabled(False)
self._profile_select_gbox.setEnabled(False)
@asyncSlot()
async def modulation_start(self):
self.profiles_enable(False)
self._chan.set_modulating(True)
await self._chan.modulate()
@asyncSlot()
async def modulation_stop(self):
self._chan.set_modulating(False)
self.profiles_enable(True)
def build_ui(self):
outer_vbox = QVBoxLayout()
outer_hbox = QHBoxLayout()
outer_vbox.addLayout(outer_hbox)
vbox1 = QVBoxLayout()
vbox2 = QVBoxLayout()
outer_hbox.addLayout(vbox1)
outer_hbox.addLayout(vbox2)
# --------------------------------------------------------------
self._profiles_gbox = QGroupBox('Profiles')
vbox1.addWidget(self._profiles_gbox)
self._profile_widgets = []
fbox = QFormLayout()
for profnum in range(8):
hbox = QHBoxLayout()
fw = QDoubleSpinBox()
fw.setRange(*DDSChan.FREQ_RANGE)
fw.setDecimals(5)
fw.setValue(self.profile_freq(profnum))
fw.setSuffix(DDSChan.FREQ_SUFFIX)
fw.valueChanged.connect(
lambda fval, n=profnum: self.set_profile_freq(n, fval))
hbox.addWidget(fw)
pw = QDoubleSpinBox()
pw.setRange(*DDSChan.PHASE_RANGE)
pw.setDecimals(2)
pw.setValue(self.profile_phase(profnum))
pw.setSuffix(DDSChan.PHASE_SUFFIX)
pw.valueChanged.connect(
lambda pval, n=profnum: self.set_profile_phase(n, pval))
hbox.addWidget(pw)
fbox.addRow(QLabel("Profile {}:".format(profnum)), hbox)
self._profile_widgets.append([fw, pw])
hbox = QHBoxLayout()
update_btn = QPushButton("Update")
update_btn.clicked.connect(self.update)
hbox.addStretch()
hbox.addWidget(update_btn)
fbox.addRow(hbox)
self._profiles_gbox.setLayout(fbox)
# --------------------------------------------------------------
self._profile_select_gbox = QGroupBox("Profile Selection")
vbox2.addWidget(self._profile_select_gbox)
hbox = QHBoxLayout()
fbox = QFormLayout()
self._enable_box = QCheckBox("")
self._enable_box.setChecked(self._chan.direct_switch_enabled)
self._enable_box.stateChanged.connect(
self._chan.set_direct_switch_enabled)
fbox.addRow(QLabel("Enable:"), self._enable_box)
hbox1 = QHBoxLayout()
type_group = QButtonGroup(hbox1)
rb = QRadioButton("Freq.")
type_group.addButton(rb)
rb.profile_type = ad9913.SweepType.FREQUENCY
rb.toggled.connect(
lambda state, w=rb: self._chan.set_profile_type(
state, w.profile_type))
self._freq_type_btn = rb
hbox1.addWidget(rb)
rb = QRadioButton("Phase")
type_group.addButton(rb)
rb.profile_type = ad9913.SweepType.PHASE
rb.toggled.connect(
lambda state, w=rb: self._chan.set_profile_type(
state, w.profile_type))
self._phase_type_btn = rb
hbox1.addWidget(rb)
fbox.addRow(QLabel("Type:"), hbox1)
if self._chan.profile_type == ad9913.SweepType.FREQUENCY:
self._freq_type_btn.setChecked(True)
else:
self._phase_type_btn.setChecked(True)
hbox2 = QHBoxLayout()
profGroup = QButtonGroup(hbox2)
self._profile_select_btns = []
for profnum in range(8):
rb = QRadioButton("{}".format(profnum))
rb.profile = profnum
rb.toggled.connect(
lambda state, w=rb: self._chan.set_selected_profile(
state, w.profile))
profGroup.addButton(rb)
self._profile_select_btns.append(rb)
hbox2.addWidget(rb)
self._profile_select_btns[self._chan.selected_profile].setChecked(True)
fbox.addRow(QLabel("Profile:"), hbox2)
select_btn = QPushButton("Select")
select_btn.clicked.connect(self.configure)
hbox.addStretch()
hbox.addWidget(select_btn)
fbox.addRow(hbox)
self.ctl.selected_profile_changed.connect(self.update_selected_profile)
self.ctl.profile_type_changed.connect(self.update_profile_type)
self.ctl.profile_freq_changed.connect(self.update_profile_freq)
self.ctl.profile_phase_changed.connect(self.update_profile_phase)
self._profile_select_gbox.setLayout(fbox)
# --------------------------------------------------------------
self._modulation_gbox = QGroupBox("Modulation")
vbox2.addWidget(self._modulation_gbox)
fbox = QFormLayout()
hbox1 = QHBoxLayout()
mod_group = QButtonGroup(hbox1)
rb = QRadioButton("PSK")
mod_group.addButton(rb)
rb.modulation_type = ModulationType.PHASE
rb.toggled.connect(
lambda state, w=rb: self._chan.set_modulation_type(
state, w.modulation_type))
self._psk_type_btn = rb
hbox1.addWidget(rb)
rb = QRadioButton("FSK")
mod_group.addButton(rb)
rb.modulation_type = ModulationType.FREQUENCY
rb.toggled.connect(
lambda state, w=rb: self._chan.set_modulation_type(
state, w.modulation_type))
self._fsk_type_btn = rb
hbox1.addWidget(rb)
fbox.addRow(QLabel("Modulation:"), hbox1)
if self._chan.modulation_type == ModulationType.FREQUENCY:
self._fsk_type_btn.setChecked(True)
else:
self._psk_type_btn.setChecked(True)
hbox1 = QHBoxLayout()
tones_group = QButtonGroup(hbox1)
self._tone_btns = {}
for tone in [2, 4, 8]:
rb = QRadioButton("{}".format(tone))
tones_group.addButton(rb)
rb.mod_tones = tone
rb.toggled.connect(
lambda state, w=rb: self._chan.set_modulation_tones(
state, w.mod_tones))
self._tone_btns[tone] = rb
hbox1.addWidget(rb)
fbox.addRow(QLabel("Tones:"), hbox1)
self._tone_btns[self._chan.modulation_tones].setChecked(True)
hbox = QHBoxLayout()
mod_start_btn = QPushButton("Start")
mod_start_btn.clicked.connect(self.modulation_start)
mod_stop_btn = QPushButton("Stop")
mod_stop_btn.clicked.connect(self.modulation_stop)
hbox.addStretch()
hbox.addWidget(mod_start_btn)
hbox.addWidget(mod_stop_btn)
fbox.addRow(hbox)
self._modulation_gbox.setLayout(fbox)
# --------------------------------------------------------------
bbox = QDialogButtonBox(QDialogButtonBox.Close)
outer_vbox.addWidget(bbox)
bbox.rejected.connect(self.reject)
# --------------------------------------------------------------
# If the channel modulation is active disable profile
# selections.
if self._chan.modulating:
self.profiles_enable(False)
self.setLayout(outer_vbox)
[docs] @pyqtSlot(int)
def update_selected_profile(self, p):
"""Update the displayed selected profile checkboxes.
"""
self._profile_select_btns[p].setChecked(True)
[docs] @pyqtSlot(ad9913.SweepType)
def update_profile_type(self, ptype):
"""Update the displayed profile type.
"""
if ptype == ad9913.SweepType.FREQUENCY:
self._freq_type_btn.setChecked(True)
else:
self._phase_type_btn.setChecked(True)
[docs] @pyqtSlot(int, float)
def update_profile_freq(self, profnum, f):
"""Update the displayed frequency for a profile.
"""
self._profile_widgets[profnum][0].setValue(f)
[docs] @pyqtSlot(int, float)
def update_profile_phase(self, profnum, p):
"""Update the displayed phase for a profile.
"""
self._profile_widgets[profnum][1].setValue(p)
[docs] @asyncSlot()
async def update(self):
# Update frequency tuning and phase offset words for
# each of the eight profile.
try:
for profnum in range(8):
with create_serial(self._app.ctl_device,
self._app.baudrate) as ser:
loop = asyncio.get_event_loop()
await loop.run_in_executor(
None, self.ctl.update_profile, ser, profnum)
except serial.serialutil.SerialException as se:
error_dialog = QErrorMessage(self)
error_dialog.showMessage(str(se))
@asyncSlot()
async def configure(self):
try:
with create_serial(self._app.ctl_device,
self._app.baudrate) as ser:
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, self.ctl.configure_profile, ser)
except serial.serialutil.SerialException as se:
error_dialog = QErrorMessage(self)
error_dialog.showMessage(str(se))
[docs] def reject(self):
QDialog.reject(self)