from typing import (
Dict, Optional
)
import serial
from rfblocks import (
ad9552, AD9552Controller, AD9552Channel,
DividerRangeException, list_available_serial_ports,
create_serial, write_cmd, query_cmd
)
from PyQt5.QtWidgets import (
QGroupBox, QVBoxLayout, QHBoxLayout, QFormLayout, QButtonGroup,
QRadioButton, QLabel, QDoubleSpinBox, QPushButton, QCheckBox,
QMessageBox, QErrorMessage, QWidget
)
from PyQt5.QtCore import (
Qt, pyqtSignal, pyqtSlot, QObject
)
[docs]class ClkChannel(QObject):
def __init__(self,
channel: AD9552Channel,
has_ui: bool) -> None:
"""Create an instance of :py:`ClkChannel`.
:param channel: AD9552 channel configuration.
:type channel: :py:class:`rfblocks.AD9552Channel`
:param has_ui: Display channel GUI controls.
:type has_ui: bool
"""
super().__init__()
self._channel: AD9552Channel = channel
self._has_ui: bool = has_ui
@property
def chan(self) -> AD9552Channel:
"""Returns a reference to the AD9552Channel instance.
"""
return self._channel
[docs] def build_ui(self) -> QGroupBox:
"""Build the on-screen UI components for a clock channel.
"""
self._ctl_widgets: Dict = {}
group_box: QGroupBox = QGroupBox("Channel {}".format(self._channel.label))
vbox: QVBoxLayout = QVBoxLayout()
fbox: QFormLayout = QFormLayout()
# ----------------------------------------------------
# Channel state
#
cb: QCheckBox = QCheckBox("")
cb.setChecked(True)
cb.stateChanged.connect(self.set_state)
fbox.addRow(QLabel("Active:"), cb)
self._active_box = cb
# ----------------------------------------------------
# Channel mode
#
mode_widgets: Dict = {}
vbox2: QVBoxLayout = QVBoxLayout()
hbox1: QHBoxLayout = QHBoxLayout()
modeGroup: QButtonGroup = QButtonGroup(vbox2)
rb: QRadioButton = QRadioButton("LVDS")
modeGroup.addButton(rb)
rb.toggled.connect(lambda state, w=rb:
self.set_mode(ad9552.OutputMode.LVDS))
hbox1.addWidget(rb)
mode_widgets[ad9552.OutputMode.LVDS] = rb
rb = QRadioButton("LVPECL")
modeGroup.addButton(rb)
rb.toggled.connect(lambda state, w=rb:
self.set_mode(ad9552.OutputMode.LVPECL))
hbox1.addWidget(rb)
mode_widgets[ad9552.OutputMode.LVPECL] = rb
hbox1.addStretch(1)
vbox2.addLayout(hbox1)
hbox2: QHBoxLayout = QHBoxLayout()
rb = QRadioButton("CMOS (Both)")
modeGroup.addButton(rb)
rb.toggled.connect(lambda state, w=rb:
self.set_mode(ad9552.OutputMode.CMOS_BOTH_ACTIVE))
hbox2.addWidget(rb)
mode_widgets[ad9552.OutputMode.CMOS_BOTH_ACTIVE] = rb
rb = QRadioButton("CMOS (Tristated)")
modeGroup.addButton(rb)
rb.toggled.connect(lambda state, w=rb:
self.set_mode(ad9552.OutputMode.CMOS_TRISTATE))
hbox2.addWidget(rb)
mode_widgets[ad9552.OutputMode.CMOS_TRISTATE] = rb
vbox2.addLayout(hbox2)
hbox3: QHBoxLayout = QHBoxLayout()
rb = QRadioButton("CMOS (Pos)")
modeGroup.addButton(rb)
rb.toggled.connect(lambda state, w=rb:
self.set_mode(ad9552.OutputMode.CMOS_POS_ACTIVE))
hbox3.addWidget(rb)
mode_widgets[ad9552.OutputMode.CMOS_POS_ACTIVE] = rb
rb = QRadioButton("CMOS (Neg)")
modeGroup.addButton(rb)
rb.toggled.connect(lambda state, w=rb:
self.set_mode(ad9552.OutputMode.CMOS_NEG_ACTIVE))
hbox3.addWidget(rb)
mode_widgets[ad9552.OutputMode.CMOS_NEG_ACTIVE] = rb
vbox2.addLayout(hbox3)
self._ctl_widgets['mode'] = mode_widgets
self._ctl_widgets['mode'][ad9552.OutputMode.LVPECL].setChecked(True)
fbox.addRow(QLabel("Mode:"), vbox2)
# ----------------------------------------------------
# Channel drive
#
drive_widgets: Dict = {}
hbox1 = QHBoxLayout()
driveGroup: QButtonGroup = QButtonGroup(hbox1)
rb = QRadioButton("Strong")
driveGroup.addButton(rb)
rb.toggled.connect(lambda state, w=rb:
self.set_drive(ad9552.DriveStrength.STRONG))
hbox1.addWidget(rb)
drive_widgets[ad9552.DriveStrength.STRONG] = rb
rb = QRadioButton("Weak")
driveGroup.addButton(rb)
rb.toggled.connect(lambda state, w=rb:
self.set_drive(ad9552.DriveStrength.WEAK))
hbox1.addWidget(rb)
drive_widgets[ad9552.DriveStrength.WEAK] = rb
self._ctl_widgets['drive'] = drive_widgets
self._ctl_widgets['drive'][ad9552.DriveStrength.STRONG].setChecked(True)
fbox.addRow(QLabel("Drive:"), hbox1)
# ----------------------------------------------------
# Channel CMOS polarity
#
pol_widgets: Dict = {}
vbox3: QVBoxLayout = QVBoxLayout()
hbox1 = QHBoxLayout()
polarityGroup: QButtonGroup = QButtonGroup(vbox3)
rb = QRadioButton("Diff. (Pos)")
polarityGroup.addButton(rb)
rb.toggled.connect(lambda state, w=rb:
self.set_polarity(ad9552.CmosPolarity.DIFF_POS))
hbox1.addWidget(rb)
pol_widgets[ad9552.CmosPolarity.DIFF_POS] = rb
rb = QRadioButton("Diff. (Neg)")
polarityGroup.addButton(rb)
rb.toggled.connect(lambda state, w=rb:
self.set_polarity(ad9552.CmosPolarity.DIFF_NEG))
hbox1.addWidget(rb)
pol_widgets[ad9552.CmosPolarity.DIFF_NEG] = rb
vbox3.addLayout(hbox1)
hbox2 = QHBoxLayout()
rb = QRadioButton("Comm. (Pos)")
polarityGroup.addButton(rb)
rb.toggled.connect(lambda state, w=rb:
self.set_polarity(ad9552.CmosPolarity.COMM_POS))
hbox2.addWidget(rb)
pol_widgets[ad9552.CmosPolarity.COMM_POS] = rb
rb = QRadioButton("Comm. (Neg)")
polarityGroup.addButton(rb)
rb.toggled.connect(lambda state, w=rb:
self.set_polarity(ad9552.CmosPolarity.COMM_NEG))
hbox2.addWidget(rb)
pol_widgets[ad9552.CmosPolarity.COMM_NEG] = rb
vbox3.addLayout(hbox2)
self._ctl_widgets['polarity'] = pol_widgets
self._ctl_widgets['polarity'][ad9552.CmosPolarity.DIFF_POS].setChecked(True)
fbox.addRow(QLabel("CMOS Pol:"), vbox3)
vbox.addLayout(fbox)
group_box.setLayout(vbox)
self.chan.state_changed.connect(self.update_chan_state)
self.chan.mode_changed.connect(self.update_chan_mode)
self.chan.drive_changed.connect(self.update_chan_drive)
self.chan.polarity_changed.connect(self.update_chan_polarity)
return group_box
@property
def chan_id(self) -> int:
return self.chan.chan_id
@property
def state(self) -> ad9552.OutputState:
return self.chan.state
def set_state(self, flag: bool) -> None:
state = ad9552.OutputState.ACTIVE if flag \
else ad9552.OutputState.POWERED_DOWN
self.chan._state = state
@property
def has_ui(self) -> bool:
"""Return :py:`True` if this channel have an on screen representation,
:py:`False` otherwise.
Under some circumstances, it may not be desirable to have a user
interface component associated with a clock channel. An example
of this situation is where one of the two clock channels in a clock module
is used as an internal reference. In these circumstances the channel will
have a fixed configuration.
"""
return self._has_ui
@property
def mode(self) -> ad9552.OutputMode:
"""The current channel output mode (:py:class:`rfblocks.ad9552.OutputMode`).
"""
return self.chan.mode
[docs] def set_mode(self, mode: ad9552.OutputMode) -> None:
"""Set the channel output mode.
:param mode: The new value for the output mode.
:type mode: :py:class:`rfblocks.ad9552.OutputMode`
Note that this will set the value of the :py:attr:`mode`
property only. Updating the clock channel hardware should be
done separately. See, for example,
:py:meth:`ClkModule.configure_hw`.
"""
self.chan._mode = mode
@property
def drive(self) -> ad9552.DriveStrength:
"""The current channel drive strength
(:py:class:`rfblocks.ad9552.DriveStrength`).
"""
return self.chan.drive
[docs] def set_drive(self, drive: ad9552.DriveStrength) -> None:
"""Set the channel drive strength.
:param drive: The new value for the drive strength.
:type drive: :py:class:`rfblocks.ad9552.DriveStrength`
Note that this will set the value of the :py:attr:`drive`
property only. Updating the clock channel hardware should be
done separately. See, for example,
:py:meth:`ClkModule.configure_hw`.
"""
self.chan._drive = drive
@property
def polarity(self) -> ad9552.CmosPolarity:
"""The current channel CMOS polarity
(:py:class:`rfblocks.ad9552.CmosPolarity`).
"""
return self.chan.polarity
[docs] def set_polarity(self, polarity: ad9552.CmosPolarity) -> None:
"""Set the channel CMOS polarity.
:param polarity: The new value for the CMOS polarity.
:type polarity: :py:class:`rfblocks.ad9552.CmosPolarity`
Note that this will set the value of the :py:attr:`polarity`
property only. Updating the clock channel hardware should be
done separately. See, for example,
:py:meth:`ClkModule.configure_hw`.
"""
self.chan._polarity = polarity
@property
def source(self) -> ad9552.SourceControl:
"""The current channel source control setting
(:py:class:`rfblocks.ad9552.SourceControl`)
"""
return self.chan.source
@source.setter
def source(self, source: ad9552.SourceControl) -> None:
"""
Note that this will set the value of the :py:attr:`source`
property only. Updating the clock channel hardware should be
done separately. See, for example,
:py:meth:`ClkModule.configure_hw`.
"""
self.chan._source = source
[docs] def initialize_ui(self) -> None:
"""Configure the on screen channel user interface controls to
their initial values.
"""
config = self.chan.initial_config
if config['state'] == ad9552.OutputState.ACTIVE:
self._active_box.setChecked(True)
else:
self._active_box.setChecked(False)
self._ctl_widgets['mode'][config['mode']].setChecked(True)
self._ctl_widgets['drive'][config['drive']].setChecked(True)
self._ctl_widgets['polarity'][config['polarity']].setChecked(True)
[docs] @pyqtSlot(int, int)
def update_chan_state(self,
chan_id: int,
state: ad9552.OutputState) -> None:
"""Update the *Active* checkbox.
"""
if chan_id == self.chan_id:
if state == ad9552.OutputState.ACTIVE:
self._active_box.setChecked(True)
else:
self._active_box.setChecked(False)
[docs] @pyqtSlot(int, int)
def update_chan_mode(self,
chan_id: int,
mode: ad9552.OutputMode) -> None:
"""Update the *Mode* radio button group.
"""
if chan_id == self.chan_id:
self._ctl_widgets['mode'][mode].setChecked(True)
[docs] @pyqtSlot(int, int)
def update_chan_drive(self,
chan_id: int,
drive: ad9552.DriveStrength) -> None:
"""Update the *Drive* radio button group.
"""
if chan_id == self.chan_id:
self._ctl_widgets['drive'][drive].setChecked(True)
[docs] @pyqtSlot(int, int)
def update_chan_polarity(self,
chan_id: int,
polarity: ad9552.CmosPolarity) -> None:
"""Update the *CMOS Pol.* radio button group.
"""
if chan_id == self.chan_id:
self._ctl_widgets['polarity'][polarity].setChecked(True)
[docs]class ClkModule(QObject):
def __init__(self,
app: QWidget,
module_id: str,
controller: AD9552Controller,
ch1_ui_enable: bool = True,
ch2_ui_enable: bool = True) -> None:
"""Create an instance of :py:`ClkModule`.
:param app: The parent application for the clock module display.
:type app: QWidget
:param module_id: Clock module identifier (for example: 'clkmod1').
:type module_id: str
:param controller: AD9552 clock generator controller.
:type controller: :py:class:`rfblocks.AD9552Controller`
:param ch1_ui_enable: Display channel 1 GUI controls.
:type ch1_ui_enable: bool
:param ch2_ui_enable: Display channel 2 GUI controls.
:type ch2_ui_enable: bool
"""
super().__init__()
self._app: QWidget = app
self._module_id: str = module_id
self._controller: AD9552Controller = controller
chan_ids = self._controller.channels.keys()
chans = self._controller.channels.values()
ui_enable = (ch1_ui_enable, ch2_ui_enable)
self._channels: Dict = {
chan_id: ClkChannel(chan, has_ui)
for chan_id, chan, has_ui in zip(chan_ids, chans, ui_enable)}
self._group_box: Optional[QGroupBox] = None
self._freq_box: Optional[QDoubleSpinBox] = None
@property
def ctl(self) -> AD9552Controller:
"""Return a reference to the :py:class:`AD9552Controller`.
"""
return self._controller
[docs] def build_ui(self) -> QGroupBox:
"""Build on-screen UI components for a clock module.
"""
self._group_box = QGroupBox("")
vbox: QVBoxLayout = QVBoxLayout()
# ----------------------------------------------------
# Module frequency
#
hbox1: QHBoxLayout = QHBoxLayout()
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.valueChanged.connect(self.set_freq)
fbox.addRow(QLabel("Freq. (MHz):"), self._freq_box)
hbox1.addLayout(fbox)
hbox1.addStretch()
vbox.addLayout(hbox1)
self.ctl.freq_changed.connect(self.update_module_freq)
# ----------------------------------------------------
# Module channels
#
hbox2: QHBoxLayout = QHBoxLayout()
for chan in self._channels.values():
if chan.has_ui:
hbox2.addWidget(chan.build_ui())
vbox.addLayout(hbox2)
# ----------------------------------------------------
# Module PLL lock status
#
hbox3: QHBoxLayout = QHBoxLayout()
fbox = QFormLayout()
self._lockedBox = QCheckBox("")
self._lockedBox.setChecked(False)
self._lockedBox.setAttribute(Qt.WA_TransparentForMouseEvents, True)
self._lockedBox.setFocusPolicy(Qt.NoFocus)
fbox.addRow(QLabel("Locked:"), self._lockedBox)
hbox3.addLayout(fbox)
hbox3.addStretch()
# ----------------------------------------------------
# Module hardware configure
#
self._config_btn = QPushButton("Configure")
self._config_btn.clicked.connect(self.configure)
hbox3.addWidget(self._config_btn)
vbox.addLayout(hbox3)
self._group_box.setLayout(vbox)
return self._group_box
[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)
@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):
if self._group_box:
self._group_box.setEnabled(en)
@property
def freq(self) -> float:
"""Returns the current output frequency (in MHz)
"""
return self.ctl.freq
[docs] def set_freq(self, f: float) -> None:
"""Set the current output frequency.
:param f: The new output frequency (in MHz)
:type f: float
Note that this will set the value of the :py:attr:`freq`
property only. Updating the clock module hardware should be
done separately. See, for example,
:py:meth:`configure_freq`.
"""
self.ctl.freq = f
[docs] @pyqtSlot(float)
def update_module_freq(self, f: float) -> None:
"""Update the displayed clock generator frequency.
"""
self._freq_box.setValue(f)
@property
def min_freq(self) -> float:
return self.ctl.min_freq
@property
def max_freq(self) -> float:
return self.ctl.max_freq
@property
def refsrc(self) -> str:
return self.ctl.refsrc
@refsrc.setter
def refsrc(self, src: str) -> None:
self.ctl.refsrc = src
def initialize_ui(self) -> None:
if self._freq_box is not None:
self._freq_box.setValue(self.ctl.freq)
for chan in self._channels.values():
if chan.has_ui:
chan.initialize_ui()
def initialize_hw(self, ser: serial.Serial) -> None:
pll_lock = self.ctl.initialize(ser)
self._lockedBox.setChecked(pll_lock)
def set_locked_state(self, locked: int) -> None:
if locked:
self._lockedBox.setChecked(True)
else:
self._lockedBox.setChecked(False)
def set_channel_source(self,
chan_id: int,
src: ad9552.SourceControl) -> None:
self._channels[str(chan_id)].source = src