Source code for qtrfblocks.ad9552_qt

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] def configure_ui(self) -> None: """Configure the on screen channel user interface controls. """ if self.state == ad9552.OutputState.ACTIVE: self._active_box.setChecked(True) else: self._active_box.setChecked(False) self._ctl_widgets['mode'][self.mode].setChecked(True) self._ctl_widgets['drive'][self.drive].setChecked(True) self._ctl_widgets['polarity'][self.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)
[docs] def configure(self) -> None: """Update the clock module hardware using the currently set configuration. """ try: with create_serial(self._app.ctl_device, self._app.baudrate) as ser: self.configure_hw(ser) except serial.serialutil.SerialException as se: error_dialog = QErrorMessage(self._app) error_dialog.showMessage(str(se))
[docs] def configure_ui(self) -> None: """Configure the on screen clock module user interface controls. """ if self._freq_box is not None: self._freq_box.setValue(config['freq']) for chan_id, chan in self._channels.items(): if chan.has_ui: chan.configure_ui()
[docs] def configure_hw(self, ser: serial.Serial) -> bool: """Update the clock module hardware using the currently set configuration. :param ser: Device update commands will be sent via this serial device. :type serial.Serial: """ pll_lock = self.ctl.configure(ser) self._lockedBox.setChecked(pll_lock) return 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