diff --git a/NanoVNASaver/Widgets/MarkerControl.py b/NanoVNASaver/Widgets/MarkerControl.py new file mode 100644 index 0000000..8bad419 --- /dev/null +++ b/NanoVNASaver/Widgets/MarkerControl.py @@ -0,0 +1,849 @@ +# NanoVNASaver +# +# A python program to view and export Touchstone data from a NanoVNA +# Copyright (C) 2019, 2020 Rune B. Broberg +# Copyright (C) 2020 NanoVNA-Saver Authors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +import logging +import math +import sys +import threading +from collections import OrderedDict +from time import sleep, strftime, localtime +from typing import List + +from PyQt5 import QtWidgets, QtCore, QtGui + +from .Windows import ( + AboutWindow, AnalysisWindow, CalibrationWindow, + DeviceSettingsWindow, DisplaySettingsWindow, SweepSettingsWindow, + TDRWindow +) +from .Widgets import SweepControl +from .Formatting import format_frequency +from .Hardware.Hardware import Interface, get_interfaces, get_VNA +from .Hardware.VNA import VNA +from .RFTools import Datapoint, corr_att_data +from .Charts.Chart import Chart +from .Charts import ( + CapacitanceChart, + CombinedLogMagChart, GroupDelayChart, InductanceChart, + LogMagChart, PhaseChart, + MagnitudeChart, MagnitudeZChart, + QualityFactorChart, VSWRChart, PermeabilityChart, PolarChart, + RealImaginaryChart, + SmithChart, SParameterChart, TDRChart, +) +from .Calibration import Calibration +from .Marker import Marker +from .SweepWorker import SweepWorker +from .Settings import BandsModel +from .Touchstone import Touchstone +from .About import VERSION + +logger = logging.getLogger(__name__) + + +class NanoVNASaver(QtWidgets.QWidget): + version = VERSION + dataAvailable = QtCore.pyqtSignal() + scaleFactor = 1 + + sweepTitle = "" + + def __init__(self): + super().__init__() + self.s21att = 0.0 + if getattr(sys, 'frozen', False): + logger.debug("Running from pyinstaller bundle") + self.icon = QtGui.QIcon(f"{sys._MEIPASS}/icon_48x48.png") # pylint: disable=no-member + else: + self.icon = QtGui.QIcon("icon_48x48.png") + self.setWindowIcon(self.icon) + self.settings = QtCore.QSettings(QtCore.QSettings.IniFormat, + QtCore.QSettings.UserScope, + "NanoVNASaver", "NanoVNASaver") + print(f"Settings: {self.settings.fileName()}") + self.threadpool = QtCore.QThreadPool() + self.worker = SweepWorker(self) + + self.worker.signals.updated.connect(self.dataUpdated) + self.worker.signals.finished.connect(self.sweepFinished) + self.worker.signals.sweepError.connect(self.showSweepError) + self.worker.signals.fatalSweepError.connect(self.showFatalSweepError) + + self.sweep_control = SweepControl(self) + + self.bands = BandsModel() + + self.interface = Interface("serial", "None") + self.vna = VNA(self.interface) + + self.dataLock = threading.Lock() + # TODO: use Touchstone class as data container + self.data11: List[Datapoint] = [] + self.data21: List[Datapoint] = [] + self.referenceS11data: List[Datapoint] = [] + self.referenceS21data: List[Datapoint] = [] + + self.sweepSource = "" + self.referenceSource = "" + + self.calibration = Calibration() + + self.markers = [] + + logger.debug("Building user interface") + + self.baseTitle = f"NanoVNA Saver {NanoVNASaver.version}" + self.updateTitle() + layout = QtWidgets.QGridLayout() + scrollarea = QtWidgets.QScrollArea() + outer = QtWidgets.QVBoxLayout() + outer.addWidget(scrollarea) + self.setLayout(outer) + scrollarea.setWidgetResizable(True) + window_width = self.settings.value("WindowWidth", 1350, type=int) + window_height = self.settings.value("WindowHeight", 950, type=int) + self.resize(window_width, window_height) + scrollarea.setSizePolicy( + QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) + self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, + QtWidgets.QSizePolicy.MinimumExpanding) + widget = QtWidgets.QWidget() + widget.setLayout(layout) + scrollarea.setWidget(widget) + + # outer.setContentsMargins(2, 2, 2, 2) # Small screen mode, reduce margins? + + self.charts = { + "s11": OrderedDict(( + ("capacitance", CapacitanceChart("S11 Serial C")), + ("group_delay", GroupDelayChart("S11 Group Delay")), + ("inductance", InductanceChart("S11 Serial L")), + ("log_mag", LogMagChart("S11 Return Loss")), + ("magnitude", MagnitudeChart("|S11|")), + ("magnitude_z", MagnitudeZChart("S11 |Z|")), + ("permeability", PermeabilityChart( + "S11 R/\N{GREEK SMALL LETTER OMEGA} &" + " X/\N{GREEK SMALL LETTER OMEGA}")), + ("phase", PhaseChart("S11 Phase")), + ("q_factor", QualityFactorChart("S11 Quality Factor")), + ("real_imag", RealImaginaryChart("S11 R+jX")), + ("smith", SmithChart("S11 Smith Chart")), + ("s_parameter", SParameterChart("S11 Real/Imaginary")), + ("vswr", VSWRChart("S11 VSWR")), + )), + "s21": OrderedDict(( + ("group_delay", GroupDelayChart("S21 Group Delay", + reflective=False)), + ("log_mag", LogMagChart("S21 Gain")), + ("magnitude", MagnitudeChart("|S21|")), + ("phase", PhaseChart("S21 Phase")), + ("polar", PolarChart("S21 Polar Plot")), + ("s_parameter", SParameterChart("S21 Real/Imaginary")), + )), + "combined": OrderedDict(( + ("log_mag", CombinedLogMagChart("S11 & S21 LogMag")), + )), + } + self.tdr_chart = TDRChart("TDR") + self.tdr_mainwindow_chart = TDRChart("TDR") + + # List of all the S11 charts, for selecting + self.s11charts = list(self.charts["s11"].values()) + + # List of all the S21 charts, for selecting + self.s21charts = list(self.charts["s21"].values()) + + # List of all charts that use both S11 and S21 + self.combinedCharts = list(self.charts["combined"].values()) + + # List of all charts that can be selected for display + self.selectable_charts = self.s11charts + self.s21charts + self.combinedCharts + self.selectable_charts.append(self.tdr_mainwindow_chart) + + # List of all charts that subscribe to updates (including duplicates!) + self.subscribing_charts = [] + self.subscribing_charts.extend(self.selectable_charts) + self.subscribing_charts.append(self.tdr_chart) + + for c in self.subscribing_charts: + c.popoutRequested.connect(self.popoutChart) + + self.charts_layout = QtWidgets.QGridLayout() + + left_column = QtWidgets.QVBoxLayout() + self.marker_column = QtWidgets.QVBoxLayout() + self.marker_frame = QtWidgets.QFrame() + self.marker_column.setContentsMargins(0, 0, 0, 0) + self.marker_frame.setLayout(self.marker_column) + right_column = QtWidgets.QVBoxLayout() + right_column.addLayout(self.charts_layout) + self.marker_frame.setHidden(not self.settings.value("MarkersVisible", True, bool)) + + layout.addLayout(left_column, 0, 0) + layout.addWidget(self.marker_frame, 0, 1) + layout.addLayout(right_column, 0, 2) + + ############################################################### + # Windows + ############################################################### + + self.windows = { + "about": AboutWindow(self), + # "analysis": AnalysisWindow(self), + "calibration": CalibrationWindow(self), + "device_settings": DeviceSettingsWindow(self), + "file": QtWidgets.QWidget(), + "sweep_settings": SweepSettingsWindow(self), + "setup": DisplaySettingsWindow(self), + "tdr": TDRWindow(self), + } + + ############################################################### + # Sweep control + ############################################################### + + left_column.addWidget(self.sweep_control) + + # ############################################################### + # Marker control + ############################################################### + + marker_control_box = QtWidgets.QGroupBox() + marker_control_box.setTitle("Markers") + marker_control_box.setMaximumWidth(250) + self.marker_control_layout = QtWidgets.QFormLayout(marker_control_box) + + marker_count = max(self.settings.value("MarkerCount", 3, int), 1) + for i in range(marker_count): + marker = Marker("", self.settings) + marker.updated.connect(self.markerUpdated) + label, layout = marker.getRow() + self.marker_control_layout.addRow(label, layout) + self.markers.append(marker) + if i == 0: + marker.isMouseControlledRadioButton.setChecked(True) + + self.showMarkerButton = QtWidgets.QPushButton() + if self.marker_frame.isHidden(): + self.showMarkerButton.setText("Show data") + else: + self.showMarkerButton.setText("Hide data") + self.showMarkerButton.clicked.connect(self.toggleMarkerFrame) + lock_radiobutton = QtWidgets.QRadioButton("Locked") + lock_radiobutton.setLayoutDirection(QtCore.Qt.RightToLeft) + lock_radiobutton.setSizePolicy( + QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) + hbox = QtWidgets.QHBoxLayout() + hbox.addWidget(self.showMarkerButton) + hbox.addWidget(lock_radiobutton) + self.marker_control_layout.addRow(hbox) + + for c in self.subscribing_charts: + c.setMarkers(self.markers) + c.setBands(self.bands) + left_column.addWidget(marker_control_box) + + self.marker_data_layout = QtWidgets.QVBoxLayout() + self.marker_data_layout.setContentsMargins(0, 0, 0, 0) + + for m in self.markers: + self.marker_data_layout.addWidget(m.getGroupBox()) + + self.marker_column.addLayout(self.marker_data_layout) + + ############################################################### + # Statistics/analysis + ############################################################### + + s11_control_box = QtWidgets.QGroupBox() + s11_control_box.setTitle("S11") + s11_control_layout = QtWidgets.QFormLayout() + s11_control_box.setLayout(s11_control_layout) + + self.s11_min_swr_label = QtWidgets.QLabel() + s11_control_layout.addRow("Min VSWR:", self.s11_min_swr_label) + self.s11_min_rl_label = QtWidgets.QLabel() + s11_control_layout.addRow("Return loss:", self.s11_min_rl_label) + + self.marker_column.addWidget(s11_control_box) + + s21_control_box = QtWidgets.QGroupBox() + s21_control_box.setTitle("S21") + s21_control_layout = QtWidgets.QFormLayout() + s21_control_box.setLayout(s21_control_layout) + + self.s21_min_gain_label = QtWidgets.QLabel() + s21_control_layout.addRow("Min gain:", self.s21_min_gain_label) + + self.s21_max_gain_label = QtWidgets.QLabel() + s21_control_layout.addRow("Max gain:", self.s21_max_gain_label) + + self.marker_column.addWidget(s21_control_box) + + self.marker_column.addStretch(1) + + self.windows["analysis"] = AnalysisWindow(self) + btn_show_analysis = QtWidgets.QPushButton("Analysis ...") + btn_show_analysis.clicked.connect( + lambda: self.display_window("analysis")) + self.marker_column.addWidget(btn_show_analysis) + + ############################################################### + # TDR + ############################################################### + + self.tdr_chart.tdrWindow = self.windows["tdr"] + self.tdr_mainwindow_chart.tdrWindow = self.windows["tdr"] + self.windows["tdr"].updated.connect(self.tdr_chart.update) + self.windows["tdr"].updated.connect(self.tdr_mainwindow_chart.update) + + tdr_control_box = QtWidgets.QGroupBox() + tdr_control_box.setTitle("TDR") + tdr_control_layout = QtWidgets.QFormLayout() + tdr_control_box.setLayout(tdr_control_layout) + tdr_control_box.setMaximumWidth(250) + + self.tdr_result_label = QtWidgets.QLabel() + tdr_control_layout.addRow("Estimated cable length:", self.tdr_result_label) + + self.tdr_button = QtWidgets.QPushButton("Time Domain Reflectometry ...") + self.tdr_button.clicked.connect(lambda: self.display_window("tdr")) + + tdr_control_layout.addRow(self.tdr_button) + + left_column.addWidget(tdr_control_box) + + ############################################################### + # Spacer + ############################################################### + + left_column.addSpacerItem( + QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.Fixed, + QtWidgets.QSizePolicy.Expanding)) + + ############################################################### + # Reference control + ############################################################### + + reference_control_box = QtWidgets.QGroupBox() + reference_control_box.setMaximumWidth(250) + reference_control_box.setTitle("Reference sweep") + reference_control_layout = QtWidgets.QFormLayout(reference_control_box) + + btn_set_reference = QtWidgets.QPushButton("Set current as reference") + btn_set_reference.clicked.connect(self.setReference) + self.btnResetReference = QtWidgets.QPushButton("Reset reference") + self.btnResetReference.clicked.connect(self.resetReference) + self.btnResetReference.setDisabled(True) + + reference_control_layout.addRow(btn_set_reference) + reference_control_layout.addRow(self.btnResetReference) + + left_column.addWidget(reference_control_box) + + ############################################################### + # Serial control + ############################################################### + + serial_control_box = QtWidgets.QGroupBox() + serial_control_box.setMaximumWidth(250) + serial_control_box.setTitle("Serial port control") + serial_control_layout = QtWidgets.QFormLayout(serial_control_box) + self.serialPortInput = QtWidgets.QComboBox() + self.rescanSerialPort() + self.serialPortInput.setEditable(True) + btn_rescan_serial_port = QtWidgets.QPushButton("Rescan") + btn_rescan_serial_port.setFixedWidth(65) + btn_rescan_serial_port.clicked.connect(self.rescanSerialPort) + serial_port_input_layout = QtWidgets.QHBoxLayout() + serial_port_input_layout.addWidget(self.serialPortInput) + serial_port_input_layout.addWidget(btn_rescan_serial_port) + serial_control_layout.addRow( + QtWidgets.QLabel("Serial port"), serial_port_input_layout) + + serial_button_layout = QtWidgets.QHBoxLayout() + + self.btnSerialToggle = QtWidgets.QPushButton("Connect to device") + self.btnSerialToggle.clicked.connect(self.serialButtonClick) + serial_button_layout.addWidget(self.btnSerialToggle, stretch=1) + + self.btnDeviceSettings = QtWidgets.QPushButton("Manage") + self.btnDeviceSettings.setFixedWidth(65) + self.btnDeviceSettings.clicked.connect( + lambda: self.display_window("device_settings")) + serial_button_layout.addWidget(self.btnDeviceSettings, stretch=0) + serial_control_layout.addRow(serial_button_layout) + left_column.addWidget(serial_control_box) + + ############################################################### + # File control + ############################################################### + + self.windows["file"].setWindowTitle("Files") + self.windows["file"].setWindowIcon(self.icon) + self.windows["file"].setMinimumWidth(200) + QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self.windows["file"], + self.windows["file"].hide) + file_window_layout = QtWidgets.QVBoxLayout() + self.windows["file"].setLayout(file_window_layout) + + load_file_control_box = QtWidgets.QGroupBox("Import file") + load_file_control_box.setMaximumWidth(300) + load_file_control_layout = QtWidgets.QFormLayout(load_file_control_box) + + btn_load_sweep = QtWidgets.QPushButton("Load as sweep") + btn_load_sweep.clicked.connect(self.loadSweepFile) + btn_load_reference = QtWidgets.QPushButton("Load reference") + btn_load_reference.clicked.connect(self.loadReferenceFile) + load_file_control_layout.addRow(btn_load_sweep) + load_file_control_layout.addRow(btn_load_reference) + + file_window_layout.addWidget(load_file_control_box) + + save_file_control_box = QtWidgets.QGroupBox("Export file") + save_file_control_box.setMaximumWidth(300) + save_file_control_layout = QtWidgets.QFormLayout(save_file_control_box) + + btn_export_file = QtWidgets.QPushButton("Save 1-Port file (S1P)") + btn_export_file.clicked.connect(lambda: self.exportFile(1)) + save_file_control_layout.addRow(btn_export_file) + + btn_export_file = QtWidgets.QPushButton("Save 2-Port file (S2P)") + btn_export_file.clicked.connect(lambda: self.exportFile(4)) + save_file_control_layout.addRow(btn_export_file) + + file_window_layout.addWidget(save_file_control_box) + + btn_open_file_window = QtWidgets.QPushButton("Files ...") + btn_open_file_window.clicked.connect( + lambda: self.display_window("file")) + + ############################################################### + # Calibration + ############################################################### + + btnOpenCalibrationWindow = QtWidgets.QPushButton("Calibration ...") + self.calibrationWindow = CalibrationWindow(self) + btnOpenCalibrationWindow.clicked.connect( + lambda: self.display_window("calibration")) + + ############################################################### + # Display setup + ############################################################### + + btn_display_setup = QtWidgets.QPushButton("Display setup ...") + btn_display_setup.setMaximumWidth(250) + btn_display_setup.clicked.connect( + lambda: self.display_window("setup")) + + btn_about = QtWidgets.QPushButton("About ...") + btn_about.setMaximumWidth(250) + + btn_about.clicked.connect( + lambda: self.display_window("about")) + + button_grid = QtWidgets.QGridLayout() + button_grid.addWidget(btn_open_file_window, 0, 0) + button_grid.addWidget(btnOpenCalibrationWindow, 0, 1) + button_grid.addWidget(btn_display_setup, 1, 0) + button_grid.addWidget(btn_about, 1, 1) + left_column.addLayout(button_grid) + + logger.debug("Finished building interface") + + def rescanSerialPort(self): + self.serialPortInput.clear() + for iface in get_interfaces(): + self.serialPortInput.insertItem(1, f"{iface}", iface) + + def exportFile(self, nr_params: int = 1): + if len(self.data11) == 0: + QtWidgets.QMessageBox.warning( + self, "No data to save", "There is no data to save.") + return + if nr_params > 2 and len(self.data21) == 0: + QtWidgets.QMessageBox.warning( + self, "No S21 data to save", "There is no S21 data to save.") + return + + filedialog = QtWidgets.QFileDialog(self) + if nr_params == 1: + filedialog.setDefaultSuffix("s1p") + filedialog.setNameFilter( + "Touchstone 1-Port Files (*.s1p);;All files (*.*)") + else: + filedialog.setDefaultSuffix("s2p") + filedialog.setNameFilter( + "Touchstone 2-Port Files (*.s2p);;All files (*.*)") + filedialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave) + selected = filedialog.exec() + if not selected: + return + filename = filedialog.selectedFiles()[0] + if filename == "": + logger.debug("No file name selected.") + return + + ts = Touchstone(filename) + ts.sdata[0] = self.data11 + if nr_params > 1: + ts.sdata[1] = self.data21 + for dp in self.data11: + ts.sdata[2].append(Datapoint(dp.freq, 0, 0)) + ts.sdata[3].append(Datapoint(dp.freq, 0, 0)) + try: + ts.save(nr_params) + except IOError as e: + logger.exception("Error during file export: %s", e) + return + + def serialButtonClick(self): + if not self.vna.connected(): + self.connect_device() + else: + self.disconnect_device() + + def connect_device(self): + if not self.interface: + return + with self.interface.lock: + self.interface = self.serialPortInput.currentData() + logger.info("Connection %s", self.interface) + try: + self.interface.open() + self.interface.timeout = 0.05 + except (IOError, AttributeError) as exc: + logger.error("Tried to open %s and failed: %s", + self.interface, exc) + return + if not self.interface.isOpen(): + logger.error("Unable to open port %s", self.interface) + return + sleep(0.1) + try: + self.vna = get_VNA(self.interface) + except IOError as exc: + logger.error("Unable to connect to VNA: %s", exc) + + self.vna.validateInput = self.settings.value("SerialInputValidation", True, bool) + + # connected + self.btnSerialToggle.setText("Disconnect") + + frequencies = self.vna.readFrequencies() + if not frequencies: + logger.warning("No frequencies read") + return + logger.info("Read starting frequency %s and end frequency %s", + frequencies[0], frequencies[-1]) + self.sweep_control.set_start(frequencies[0]) + if frequencies[0] < frequencies[-1]: + self.sweep_control.set_end(frequencies[-1]) + else: + self.sweep_control.set_end( + frequencies[0] + + self.vna.datapoints * self.sweep_control.get_segments()) + + self.sweep_control.set_segments(1) # speed up things + self.sweep_control.update_center_span() + self.sweep_control.update_step_size() + + logger.debug("Starting initial sweep") + self.sweep_start() + + def disconnect_device(self): + with self.interface.lock: + logger.info("Closing connection to %s", self.interface) + self.interface.close() + self.btnSerialToggle.setText("Connect to device") + + def sweep_start(self): + # Run the device data update + if not self.vna.connected(): + return + self.worker.stopped = False + + self.sweep_control.progress_bar.setValue(0) + self.sweep_control.btn_start.setDisabled(True) + self.sweep_control.btn_stop.setDisabled(False) + self.sweep_control.toggle_settings(True) + + for m in self.markers: + m.resetLabels() + self.s11_min_rl_label.setText("") + self.s11_min_swr_label.setText("") + self.s21_min_gain_label.setText("") + self.s21_max_gain_label.setText("") + self.tdr_result_label.setText("") + + self.settings.setValue("Segments", self.sweep_control.get_segments()) + + logger.debug("Starting worker thread") + self.threadpool.start(self.worker) + + def sweep_stop(self): + self.worker.stopped = True + + def saveData(self, data, data21, source=None): + with self.dataLock: + self.data11 = data + self.data21 = data21 + if self.s21att > 0: + self.data21 = corr_att_data(self.data21, self.s21att) + if source is not None: + self.sweepSource = source + else: + self.sweepSource = ( + f"{self.sweepTitle}" + f" {strftime('%Y-%m-%d %H:%M:%S', localtime())}" + ).lstrip() + + def markerUpdated(self, marker: Marker): + with self.dataLock: + marker.findLocation(self.data11) + for m in self.markers: + m.resetLabels() + m.updateLabels(self.data11, self.data21) + + for c in self.subscribing_charts: + c.update() + + def dataUpdated(self): + with self.dataLock: + for m in self.markers: + m.resetLabels() + m.updateLabels(self.data11, self.data21) + + for c in self.s11charts: + c.setData(self.data11) + + for c in self.s21charts: + c.setData(self.data21) + + for c in self.combinedCharts: + c.setCombinedData(self.data11, self.data21) + + self.sweep_control.progress_bar.setValue(self.worker.percentage) + self.windows["tdr"].updateTDR() + + # Find the minimum S11 VSWR: + min_vswr = 100 + min_vswr_freq = -1 + for d in self.data11: + vswr = d.vswr + if min_vswr > vswr > 0: + min_vswr = vswr + min_vswr_freq = d.freq + + if min_vswr_freq > -1: + self.s11_min_swr_label.setText( + f"{round(min_vswr, 3)} @ {format_frequency(min_vswr_freq)}") + if min_vswr > 1: + self.s11_min_rl_label.setText( + f"{round(20*math.log10((min_vswr-1)/(min_vswr+1)), 3)} dB") + else: + # Infinite return loss? + self.s11_min_rl_label.setText("\N{INFINITY} dB") + else: + self.s11_min_swr_label.setText("") + self.s11_min_rl_label.setText("") + min_gain = 100 + min_gain_freq = -1 + max_gain = -100 + max_gain_freq = -1 + for d in self.data21: + gain = d.gain + if gain > max_gain: + max_gain = gain + max_gain_freq = d.freq + if gain < min_gain: + min_gain = gain + min_gain_freq = d.freq + + if max_gain_freq > -1: + self.s21_min_gain_label.setText( + f"{round(min_gain, 3)} dB @ {format_frequency(min_gain_freq)}") + self.s21_max_gain_label.setText( + f"{round(max_gain, 3)} dB @ {format_frequency(max_gain_freq)}") + else: + self.s21_min_gain_label.setText("") + self.s21_max_gain_label.setText("") + + self.updateTitle() + self.dataAvailable.emit() + + def sweepFinished(self): + self.sweep_control.progress_bar.setValue(100) + self.sweep_control.btn_start.setDisabled(False) + self.sweep_control.btn_stop.setDisabled(True) + self.sweep_control.toggle_settings(False) + + for marker in self.markers: + marker.frequencyInput.textEdited.emit( + marker.frequencyInput.text()) + + def setReference(self, s11data=None, s21data=None, source=None): + if not s11data: + s11data = self.data11[:] + s21data = self.data21[:] + + self.referenceS11data = s11data + for c in self.s11charts: + c.setReference(s11data) + + self.referenceS21data = s21data + for c in self.s21charts: + c.setReference(s21data) + + for c in self.combinedCharts: + c.setCombinedReference(s11data, s21data) + + self.btnResetReference.setDisabled(False) + + if source is not None: + # Save the reference source info + self.referenceSource = source + else: + self.referenceSource = self.sweepSource + self.updateTitle() + + def updateTitle(self): + title = self.baseTitle + insert = "" + if self.sweepSource != "": + insert += f"Sweep: {self.sweepSource} @ {len(self.data11)} points" + if self.referenceSource != "": + if insert != "": + insert += ", " + insert += f"Reference: {self.referenceSource} @ {len(self.referenceS11data)} points" + if insert != "": + title = title + " (" + insert + ")" + self.setWindowTitle(title) + + def toggleMarkerFrame(self): + if self.marker_frame.isHidden(): + self.marker_frame.setHidden(False) + self.settings.setValue("MarkersVisible", True) + self.showMarkerButton.setText("Hide data") + else: + self.marker_frame.setHidden(True) + self.settings.setValue("MarkersVisible", False) + self.showMarkerButton.setText("Show data") + + def resetReference(self): + self.referenceS11data = [] + self.referenceS21data = [] + self.referenceSource = "" + self.updateTitle() + for c in self.subscribing_charts: + c.resetReference() + self.btnResetReference.setDisabled(True) + + def loadReferenceFile(self): + filename, _ = QtWidgets.QFileDialog.getOpenFileName( + filter="Touchstone Files (*.s1p *.s2p);;All files (*.*)") + if filename != "": + self.resetReference() + t = Touchstone(filename) + t.load() + self.setReference(t.s11data, t.s21data, filename) + + def loadSweepFile(self): + filename, _ = QtWidgets.QFileDialog.getOpenFileName( + filter="Touchstone Files (*.s1p *.s2p);;All files (*.*)") + if filename != "": + self.data11 = [] + self.data21 = [] + t = Touchstone(filename) + t.load() + self.saveData(t.s11data, t.s21data, filename) + self.dataUpdated() + + def sizeHint(self) -> QtCore.QSize: + return QtCore.QSize(1100, 950) + + def display_window(self, name): + self.windows[name].show() + QtWidgets.QApplication.setActiveWindow(self.windows[name]) + + def showError(self, text): + QtWidgets.QMessageBox.warning(self, "Error", text) + + def showFatalSweepError(self): + self.showError(self.worker.error_message) + self.stopSerial() + + def showSweepError(self): + self.showError(self.worker.error_message) + self.vna.flushSerialBuffers() # Remove any left-over data + self.sweepFinished() + + def popoutChart(self, chart: Chart): + logger.debug("Requested popout for chart: %s", chart.name) + new_chart = self.copyChart(chart) + new_chart.isPopout = True + new_chart.show() + new_chart.setWindowTitle(new_chart.name) + + def copyChart(self, chart: Chart): + new_chart = chart.copy() + self.subscribing_charts.append(new_chart) + if chart in self.s11charts: + self.s11charts.append(new_chart) + if chart in self.s21charts: + self.s21charts.append(new_chart) + if chart in self.combinedCharts: + self.combinedCharts.append(new_chart) + new_chart.popoutRequested.connect(self.popoutChart) + return new_chart + + def closeEvent(self, a0: QtGui.QCloseEvent) -> None: + self.worker.stopped = True + self.settings.setValue("MarkerCount", Marker.count()) + for marker in self.markers: + marker.update_settings() + + self.settings.setValue("WindowHeight", self.height()) + self.settings.setValue("WindowWidth", self.width()) + self.settings.sync() + self.bands.saveSettings() + self.threadpool.waitForDone(2500) + a0.accept() + sys.exit() + + def changeFont(self, font: QtGui.QFont) -> None: + qf_new = QtGui.QFontMetricsF(font) + normal_font = QtGui.QFont(font) + normal_font.setPointSize(8) + qf_normal = QtGui.QFontMetricsF(normal_font) + # Characters we would normally display + standard_string = "0.123456789 0.123456789 MHz \N{OHM SIGN}" + new_width = qf_new.boundingRect(standard_string).width() + old_width = qf_normal.boundingRect(standard_string).width() + self.scaleFactor = new_width / old_width + logger.debug("New font width: %f, normal font: %f, factor: %f", + new_width, old_width, self.scaleFactor) + # TODO: Update all the fixed widths to account for the scaling + for m in self.markers: + m.getGroupBox().setFont(font) + m.setScale(self.scaleFactor) + + def setSweepTitle(self, title): + self.sweepTitle = title + for c in self.subscribing_charts: + c.setSweepTitle(title)