diff --git a/NanoVNASaver/NanoVNASaver.py b/NanoVNASaver/NanoVNASaver.py
index 3ecedaa..8c49980 100644
--- a/NanoVNASaver/NanoVNASaver.py
+++ b/NanoVNASaver/NanoVNASaver.py
@@ -1,4 +1,5 @@
-# NanoVNASaver - a python program to view and export Touchstone data from a NanoVNA
+# NanoVNASaver
+# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
@@ -17,30 +18,31 @@ import logging
import math
import sys
import threading
-from collections import namedtuple
from time import sleep, strftime, localtime
-from typing import List, Tuple
+from typing import List
-import numpy as np
-import scipy.signal as signal
import serial
-import typing
from PyQt5 import QtWidgets, QtCore, QtGui
-from PyQt5.QtCore import QModelIndex
-from .Hardware import get_VNA, InvalidVNA, Version, get_interfaces
+from .Windows import AboutWindow, AnalysisWindow, \
+ DeviceSettingsWindow, DisplaySettingsWindow, SweepSettingsWindow, \
+ TDRWindow
+from .Hardware import get_interfaces, get_VNA, InvalidVNA
from .RFTools import RFTools, Datapoint
-from .Charts import Chart, CapacitanceChart, CombinedLogMagChart, \
- GroupDelayChart, InductanceChart, LogMagChart, MagnitudeChart, MagnitudeZChart, \
- PermeabilityChart, PhaseChart, PolarChart, QualityFactorChart, \
- RealImaginaryChart, SmithChart, SParameterChart, TDRChart, VSWRChart
+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 CalibrationWindow, Calibration
from .Inputs import FrequencyInputWidget
-from .Marker import Marker, MarkerSettingsWindow
+from .Marker import Marker
from .SweepWorker import SweepWorker
+from .Settings import BandsModel
from .Touchstone import Touchstone
-from .Analysis import Analysis, LowPassAnalysis, HighPassAnalysis, BandPassAnalysis, BandStopAnalysis, \
- PeakSearchAnalysis, VSWRAnalysis, SimplePeakSearchAnalysis
from .about import version as ver
logger = logging.getLogger(__name__)
@@ -79,7 +81,7 @@ class NanoVNASaver(QtWidgets.QWidget):
self.serialLock = threading.Lock()
self.serial = serial.Serial()
- self.vna: VNA = InvalidVNA(self, self.serial)
+ self.vna = InvalidVNA(self, serial)
self.dataLock = threading.Lock()
self.data: List[Datapoint] = []
@@ -235,7 +237,7 @@ class NanoVNASaver(QtWidgets.QWidget):
self.sweepCenterInput.textEdited.connect(self.updateStartEnd)
sweep_input_right_layout.addRow(QtWidgets.QLabel("Center"), self.sweepCenterInput)
-
+
self.sweepSpanInput = FrequencyInputWidget()
self.sweepSpanInput.setAlignment(QtCore.Qt.AlignRight)
self.sweepSpanInput.textEdited.connect(self.updateStartEnd)
@@ -458,7 +460,7 @@ class NanoVNASaver(QtWidgets.QWidget):
self.fileWindow.setWindowTitle("Files")
self.fileWindow.setWindowIcon(self.icon)
self.fileWindow.setMinimumWidth(200)
- shortcut = QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self.fileWindow, self.fileWindow.hide)
+ QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self.fileWindow, self.fileWindow.hide)
file_window_layout = QtWidgets.QVBoxLayout()
self.fileWindow.setLayout(file_window_layout)
@@ -552,15 +554,17 @@ class NanoVNASaver(QtWidgets.QWidget):
if len(self.data) == 0:
logger.warning("No data stored, nothing written.")
return
+ # TODO: should be delegated to Touchstone module
try:
logger.debug("Opening %s for writing", filename)
- file = open(filename, "w+")
- logger.debug("Writing file")
- file.write("# Hz S RI R 50\n")
- for i in range(len(self.data)):
- if i == 0 or self.data[i].freq != self.data[i-1].freq:
- file.write(str(self.data[i].freq) + " " + str(self.data[i].re) + " " + str(self.data[i].im) + "\n")
- file.close()
+ with open(filename, "w+") as tsfile:
+ logger.debug("Writing file")
+ tsfile.write("# Hz S RI R 50\n")
+ freq_prev = 0
+ for data in self.data:
+ if data.freq <= freq_prev:
+ continue
+ tsfile.write(f"{data.freq} {data.re} {data.im}\n")
logger.debug("File written")
except Exception as e:
logger.exception("Error during file export: %s", e)
@@ -588,6 +592,7 @@ class NanoVNASaver(QtWidgets.QWidget):
if len(self.data) == 0 or len(self.data21) == 0:
logger.warning("No data stored, nothing written.")
return
+ # TODO: should be delegated to Touchstone module
try:
logger.debug("Opening %s for writing", filename)
file = open(filename, "w+")
@@ -997,1531 +1002,3 @@ class NanoVNASaver(QtWidgets.QWidget):
self.sweepTitle = title
for c in self.subscribing_charts:
c.setSweepTitle(title)
-
-
-class DisplaySettingsWindow(QtWidgets.QWidget):
- def __init__(self, app: NanoVNASaver):
- super().__init__()
-
- self.app = app
- self.setWindowTitle("Display settings")
- self.setWindowIcon(self.app.icon)
-
- self.marker_window = MarkerSettingsWindow(self.app)
-
- shortcut = QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
-
- layout = QtWidgets.QHBoxLayout()
- self.setLayout(layout)
-
- left_layout = QtWidgets.QVBoxLayout()
- layout.addLayout(left_layout)
-
- display_options_box = QtWidgets.QGroupBox("Options")
- display_options_layout = QtWidgets.QFormLayout(display_options_box)
-
- self.returnloss_group = QtWidgets.QButtonGroup()
- self.returnloss_is_negative = QtWidgets.QRadioButton("Negative")
- self.returnloss_is_positive = QtWidgets.QRadioButton("Positive")
- self.returnloss_group.addButton(self.returnloss_is_positive)
- self.returnloss_group.addButton(self.returnloss_is_negative)
-
- display_options_layout.addRow("Return loss is:", self.returnloss_is_negative)
- display_options_layout.addRow("", self.returnloss_is_positive)
-
- if self.app.settings.value("ReturnLossPositive", False, bool):
- self.returnloss_is_positive.setChecked(True)
- else:
- self.returnloss_is_negative.setChecked(True)
-
- self.returnloss_is_positive.toggled.connect(self.changeReturnLoss)
- self.changeReturnLoss()
-
- self.show_lines_option = QtWidgets.QCheckBox("Show lines")
- show_lines_label = QtWidgets.QLabel("Displays a thin line between data points")
- self.show_lines_option.stateChanged.connect(self.changeShowLines)
- display_options_layout.addRow(self.show_lines_option, show_lines_label)
-
- self.dark_mode_option = QtWidgets.QCheckBox("Dark mode")
- dark_mode_label = QtWidgets.QLabel("Black background with white text")
- self.dark_mode_option.stateChanged.connect(self.changeDarkMode)
- display_options_layout.addRow(self.dark_mode_option, dark_mode_label)
-
- self.btnColorPicker = QtWidgets.QPushButton("█")
- self.btnColorPicker.setFixedWidth(20)
- self.sweepColor = self.app.settings.value("SweepColor", defaultValue=QtGui.QColor(160, 140, 20, 128),
- type=QtGui.QColor)
- self.setSweepColor(self.sweepColor)
- self.btnColorPicker.clicked.connect(lambda: self.setSweepColor(
- QtWidgets.QColorDialog.getColor(self.sweepColor, options=QtWidgets.QColorDialog.ShowAlphaChannel)))
-
- display_options_layout.addRow("Sweep color", self.btnColorPicker)
-
- self.btnSecondaryColorPicker = QtWidgets.QPushButton("█")
- self.btnSecondaryColorPicker.setFixedWidth(20)
- self.secondarySweepColor = self.app.settings.value("SecondarySweepColor",
- defaultValue=QtGui.QColor(20, 160, 140, 128),
- type=QtGui.QColor)
- self.setSecondarySweepColor(self.secondarySweepColor)
- self.btnSecondaryColorPicker.clicked.connect(lambda: self.setSecondarySweepColor(
- QtWidgets.QColorDialog.getColor(self.secondarySweepColor,
- options=QtWidgets.QColorDialog.ShowAlphaChannel)))
-
- display_options_layout.addRow("Second sweep color", self.btnSecondaryColorPicker)
-
- self.btnReferenceColorPicker = QtWidgets.QPushButton("█")
- self.btnReferenceColorPicker.setFixedWidth(20)
- self.referenceColor = self.app.settings.value("ReferenceColor", defaultValue=QtGui.QColor(0, 0, 255, 48),
- type=QtGui.QColor)
- self.setReferenceColor(self.referenceColor)
- self.btnReferenceColorPicker.clicked.connect(lambda: self.setReferenceColor(
- QtWidgets.QColorDialog.getColor(self.referenceColor, options=QtWidgets.QColorDialog.ShowAlphaChannel)))
-
- display_options_layout.addRow("Reference color", self.btnReferenceColorPicker)
-
- self.btnSecondaryReferenceColorPicker = QtWidgets.QPushButton("█")
- self.btnSecondaryReferenceColorPicker.setFixedWidth(20)
- self.secondaryReferenceColor = self.app.settings.value("SecondaryReferenceColor",
- defaultValue=QtGui.QColor(0, 0, 255, 48),
- type=QtGui.QColor)
- self.setSecondaryReferenceColor(self.secondaryReferenceColor)
- self.btnSecondaryReferenceColorPicker.clicked.connect(lambda: self.setSecondaryReferenceColor(
- QtWidgets.QColorDialog.getColor(self.secondaryReferenceColor,
- options=QtWidgets.QColorDialog.ShowAlphaChannel)))
-
- display_options_layout.addRow("Second reference color", self.btnSecondaryReferenceColorPicker)
-
- self.pointSizeInput = QtWidgets.QSpinBox()
- pointsize = self.app.settings.value("PointSize", 2, int)
- self.pointSizeInput.setValue(pointsize)
- self.changePointSize(pointsize)
- self.pointSizeInput.setMinimum(1)
- self.pointSizeInput.setMaximum(10)
- self.pointSizeInput.setSuffix(" px")
- self.pointSizeInput.setAlignment(QtCore.Qt.AlignRight)
- self.pointSizeInput.valueChanged.connect(self.changePointSize)
- display_options_layout.addRow("Point size", self.pointSizeInput)
-
- self.lineThicknessInput = QtWidgets.QSpinBox()
- linethickness = self.app.settings.value("LineThickness", 1, int)
- self.lineThicknessInput.setValue(linethickness)
- self.changeLineThickness(linethickness)
- self.lineThicknessInput.setMinimum(1)
- self.lineThicknessInput.setMaximum(10)
- self.lineThicknessInput.setSuffix(" px")
- self.lineThicknessInput.setAlignment(QtCore.Qt.AlignRight)
- self.lineThicknessInput.valueChanged.connect(self.changeLineThickness)
- display_options_layout.addRow("Line thickness", self.lineThicknessInput)
-
- self.markerSizeInput = QtWidgets.QSpinBox()
- markersize = self.app.settings.value("MarkerSize", 6, int)
- self.markerSizeInput.setValue(markersize)
- self.changeMarkerSize(markersize)
- self.markerSizeInput.setMinimum(4)
- self.markerSizeInput.setMaximum(20)
- self.markerSizeInput.setSingleStep(2)
- self.markerSizeInput.setSuffix(" px")
- self.markerSizeInput.setAlignment(QtCore.Qt.AlignRight)
- self.markerSizeInput.valueChanged.connect(self.changeMarkerSize)
- self.markerSizeInput.editingFinished.connect(self.validateMarkerSize)
- display_options_layout.addRow("Marker size", self.markerSizeInput)
-
- self.show_marker_number_option = QtWidgets.QCheckBox("Show marker numbers")
- show_marker_number_label = QtWidgets.QLabel("Displays the marker number next to the marker")
- self.show_marker_number_option.stateChanged.connect(self.changeShowMarkerNumber)
- display_options_layout.addRow(self.show_marker_number_option, show_marker_number_label)
-
- self.filled_marker_option = QtWidgets.QCheckBox("Filled markers")
- filled_marker_label = QtWidgets.QLabel("Shows the marker as a filled triangle")
- self.filled_marker_option.stateChanged.connect(self.changeFilledMarkers)
- display_options_layout.addRow(self.filled_marker_option, filled_marker_label)
-
- self.marker_tip_group = QtWidgets.QButtonGroup()
- self.marker_at_center = QtWidgets.QRadioButton("At the center of the marker")
- self.marker_at_tip = QtWidgets.QRadioButton("At the tip of the marker")
- self.marker_tip_group.addButton(self.marker_at_center)
- self.marker_tip_group.addButton(self.marker_at_tip)
-
- display_options_layout.addRow("Data point is:", self.marker_at_center)
- display_options_layout.addRow("", self.marker_at_tip)
-
- if self.app.settings.value("MarkerAtTip", False, bool):
- self.marker_at_tip.setChecked(True)
- else:
- self.marker_at_center.setChecked(True)
-
- self.marker_at_tip.toggled.connect(self.changeMarkerAtTip)
- self.changeMarkerAtTip()
-
- color_options_box = QtWidgets.QGroupBox("Chart colors")
- color_options_layout = QtWidgets.QFormLayout(color_options_box)
-
- self.use_custom_colors = QtWidgets.QCheckBox("Use custom chart colors")
- self.use_custom_colors.stateChanged.connect(self.changeCustomColors)
- color_options_layout.addRow(self.use_custom_colors)
-
- self.btn_background_picker = QtWidgets.QPushButton("█")
- self.btn_background_picker.setFixedWidth(20)
- self.btn_background_picker.clicked.connect(lambda: self.setColor("background",
- QtWidgets.QColorDialog.getColor(self.backgroundColor,
- options=QtWidgets.QColorDialog.ShowAlphaChannel)))
-
- color_options_layout.addRow("Chart background", self.btn_background_picker)
-
- self.btn_foreground_picker = QtWidgets.QPushButton("█")
- self.btn_foreground_picker.setFixedWidth(20)
- self.btn_foreground_picker.clicked.connect(lambda: self.setColor("foreground",
- QtWidgets.QColorDialog.getColor(self.foregroundColor,
- options=QtWidgets.QColorDialog.ShowAlphaChannel)))
-
- color_options_layout.addRow("Chart foreground", self.btn_foreground_picker)
-
- self.btn_text_picker = QtWidgets.QPushButton("█")
- self.btn_text_picker.setFixedWidth(20)
- self.btn_text_picker.clicked.connect(lambda: self.setColor("text",
- QtWidgets.QColorDialog.getColor(self.textColor,
- options=QtWidgets.QColorDialog.ShowAlphaChannel)))
-
- color_options_layout.addRow("Chart text", self.btn_text_picker)
-
- right_layout = QtWidgets.QVBoxLayout()
- layout.addLayout(right_layout)
-
- font_options_box = QtWidgets.QGroupBox("Font")
- font_options_layout = QtWidgets.QFormLayout(font_options_box)
- self.font_dropdown = QtWidgets.QComboBox()
- self.font_dropdown.addItems(["7", "8", "9", "10", "11", "12"])
- font_size = self.app.settings.value("FontSize",
- defaultValue="8",
- type=str)
- self.font_dropdown.setCurrentText(font_size)
- self.changeFont()
-
- self.font_dropdown.currentTextChanged.connect(self.changeFont)
- font_options_layout.addRow("Font size", self.font_dropdown)
-
- bands_box = QtWidgets.QGroupBox("Bands")
- bands_layout = QtWidgets.QFormLayout(bands_box)
-
- self.show_bands = QtWidgets.QCheckBox("Show bands")
- self.show_bands.setChecked(self.app.bands.enabled)
- self.show_bands.stateChanged.connect(lambda: self.setShowBands(self.show_bands.isChecked()))
- bands_layout.addRow(self.show_bands)
-
- self.btn_bands_picker = QtWidgets.QPushButton("█")
- self.btn_bands_picker.setFixedWidth(20)
- self.btn_bands_picker.clicked.connect(lambda: self.setColor("bands",
- QtWidgets.QColorDialog.getColor(self.bandsColor,
- options=QtWidgets.QColorDialog.ShowAlphaChannel)))
-
- bands_layout.addRow("Chart bands", self.btn_bands_picker)
-
- self.btn_manage_bands = QtWidgets.QPushButton("Manage bands")
-
- self.bandsWindow = BandsWindow(self.app)
- self.btn_manage_bands.clicked.connect(self.displayBandsWindow)
-
- bands_layout.addRow(self.btn_manage_bands)
-
- vswr_marker_box = QtWidgets.QGroupBox("VSWR Markers")
- vswr_marker_layout = QtWidgets.QFormLayout(vswr_marker_box)
-
- self.vswrMarkers: List[float] = self.app.settings.value("VSWRMarkers", [], float)
-
- if isinstance(self.vswrMarkers, float):
- if self.vswrMarkers == 0:
- self.vswrMarkers = []
- else:
- # Single values from the .ini become floats rather than lists. Convert them.
- self.vswrMarkers = [self.vswrMarkers]
-
- self.btn_vswr_picker = QtWidgets.QPushButton("█")
- self.btn_vswr_picker.setFixedWidth(20)
- self.btn_vswr_picker.clicked.connect(lambda: self.setColor("vswr",
- QtWidgets.QColorDialog.getColor(self.vswrColor,
- options=QtWidgets.QColorDialog.ShowAlphaChannel)))
-
- vswr_marker_layout.addRow("VSWR Markers", self.btn_vswr_picker)
-
- self.vswr_marker_dropdown = QtWidgets.QComboBox()
- vswr_marker_layout.addRow(self.vswr_marker_dropdown)
-
- if len(self.vswrMarkers) == 0:
- self.vswr_marker_dropdown.addItem("None")
- else:
- for m in self.vswrMarkers:
- self.vswr_marker_dropdown.addItem(str(m))
- for c in self.app.s11charts:
- c.addSWRMarker(m)
-
- self.vswr_marker_dropdown.setCurrentIndex(0)
- btn_add_vswr_marker = QtWidgets.QPushButton("Add ...")
- btn_remove_vswr_marker = QtWidgets.QPushButton("Remove")
- vswr_marker_btn_layout = QtWidgets.QHBoxLayout()
- vswr_marker_btn_layout.addWidget(btn_add_vswr_marker)
- vswr_marker_btn_layout.addWidget(btn_remove_vswr_marker)
- vswr_marker_layout.addRow(vswr_marker_btn_layout)
-
- btn_add_vswr_marker.clicked.connect(self.addVSWRMarker)
- btn_remove_vswr_marker.clicked.connect(self.removeVSWRMarker)
-
- markers_box = QtWidgets.QGroupBox("Markers")
- markers_layout = QtWidgets.QFormLayout(markers_box)
-
- btn_add_marker = QtWidgets.QPushButton("Add")
- btn_add_marker.clicked.connect(self.addMarker)
- self.btn_remove_marker = QtWidgets.QPushButton("Remove")
- self.btn_remove_marker.clicked.connect(self.removeMarker)
- btn_marker_settings = QtWidgets.QPushButton("Settings ...")
- btn_marker_settings.clicked.connect(self.displayMarkerWindow)
-
- marker_btn_layout = QtWidgets.QHBoxLayout()
- marker_btn_layout.addWidget(btn_add_marker)
- marker_btn_layout.addWidget(self.btn_remove_marker)
- marker_btn_layout.addWidget(btn_marker_settings)
-
- markers_layout.addRow(marker_btn_layout)
-
- charts_box = QtWidgets.QGroupBox("Displayed charts")
- charts_layout = QtWidgets.QGridLayout(charts_box)
-
- # selections = ["S11 Smith chart",
- # "S11 LogMag",
- # "S11 VSWR",
- # "S11 Phase",
- # "S21 Smith chart",
- # "S21 LogMag",
- # "S21 Phase",
- # "None"]
-
- selections = []
-
- for c in self.app.selectable_charts:
- selections.append(c.name)
-
- selections.append("None")
- chart00_selection = QtWidgets.QComboBox()
- chart00_selection.addItems(selections)
- chart00 = self.app.settings.value("Chart00", "S11 Smith Chart")
- if chart00_selection.findText(chart00) > -1:
- chart00_selection.setCurrentText(chart00)
- else:
- chart00_selection.setCurrentText("S11 Smith Chart")
- chart00_selection.currentTextChanged.connect(lambda: self.changeChart(0, 0, chart00_selection.currentText()))
- charts_layout.addWidget(chart00_selection, 0, 0)
-
- chart01_selection = QtWidgets.QComboBox()
- chart01_selection.addItems(selections)
- chart01 = self.app.settings.value("Chart01", "S11 Return Loss")
- if chart01_selection.findText(chart01) > -1:
- chart01_selection.setCurrentText(chart01)
- else:
- chart01_selection.setCurrentText("S11 Return Loss")
- chart01_selection.currentTextChanged.connect(lambda: self.changeChart(0, 1, chart01_selection.currentText()))
- charts_layout.addWidget(chart01_selection, 0, 1)
-
- chart02_selection = QtWidgets.QComboBox()
- chart02_selection.addItems(selections)
- chart02 = self.app.settings.value("Chart02", "None")
- if chart02_selection.findText(chart02) > -1:
- chart02_selection.setCurrentText(chart02)
- else:
- chart02_selection.setCurrentText("None")
- chart02_selection.currentTextChanged.connect(lambda: self.changeChart(0, 2, chart02_selection.currentText()))
- charts_layout.addWidget(chart02_selection, 0, 2)
-
- chart10_selection = QtWidgets.QComboBox()
- chart10_selection.addItems(selections)
- chart10 = self.app.settings.value("Chart10", "S21 Polar Plot")
- if chart10_selection.findText(chart10) > -1:
- chart10_selection.setCurrentText(chart10)
- else:
- chart10_selection.setCurrentText("S21 Polar Plot")
- chart10_selection.currentTextChanged.connect(lambda: self.changeChart(1, 0, chart10_selection.currentText()))
- charts_layout.addWidget(chart10_selection, 1, 0)
-
- chart11_selection = QtWidgets.QComboBox()
- chart11_selection.addItems(selections)
- chart11 = self.app.settings.value("Chart11", "S21 Gain")
- if chart11_selection.findText(chart11) > -1:
- chart11_selection.setCurrentText(chart11)
- else:
- chart11_selection.setCurrentText("S21 Gain")
- chart11_selection.currentTextChanged.connect(lambda: self.changeChart(1, 1, chart11_selection.currentText()))
- charts_layout.addWidget(chart11_selection, 1, 1)
-
- chart12_selection = QtWidgets.QComboBox()
- chart12_selection.addItems(selections)
- chart12 = self.app.settings.value("Chart12", "None")
- if chart12_selection.findText(chart12) > -1:
- chart12_selection.setCurrentText(chart12)
- else:
- chart12_selection.setCurrentText("None")
- chart12_selection.currentTextChanged.connect(lambda: self.changeChart(1, 2, chart12_selection.currentText()))
- charts_layout.addWidget(chart12_selection, 1, 2)
-
- self.changeChart(0, 0, chart00_selection.currentText())
- self.changeChart(0, 1, chart01_selection.currentText())
- self.changeChart(0, 2, chart02_selection.currentText())
- self.changeChart(1, 0, chart10_selection.currentText())
- self.changeChart(1, 1, chart11_selection.currentText())
- self.changeChart(1, 2, chart12_selection.currentText())
-
- self.backgroundColor = self.app.settings.value("BackgroundColor", defaultValue=QtGui.QColor("white"),
- type=QtGui.QColor)
- self.foregroundColor = self.app.settings.value("ForegroundColor", defaultValue=QtGui.QColor("lightgray"),
- type=QtGui.QColor)
- self.textColor = self.app.settings.value("TextColor", defaultValue=QtGui.QColor("black"),
- type=QtGui.QColor)
- self.bandsColor = self.app.settings.value("BandsColor", defaultValue=QtGui.QColor(128, 128, 128, 48),
- type=QtGui.QColor)
- self.app.bands.color = self.bandsColor
- self.vswrColor = self.app.settings.value("VSWRColor", defaultValue=QtGui.QColor(192, 0, 0, 128),
- type=QtGui.QColor)
-
- self.dark_mode_option.setChecked(self.app.settings.value("DarkMode", False, bool))
- self.show_lines_option.setChecked(self.app.settings.value("ShowLines", False, bool))
- self.show_marker_number_option.setChecked(self.app.settings.value("ShowMarkerNumbers", False, bool))
- self.filled_marker_option.setChecked(self.app.settings.value("FilledMarkers", False, bool))
-
- if self.app.settings.value("UseCustomColors", defaultValue=False, type=bool):
- self.dark_mode_option.setDisabled(True)
- self.dark_mode_option.setChecked(False)
- self.use_custom_colors.setChecked(True)
- else:
- self.btn_background_picker.setDisabled(True)
- self.btn_foreground_picker.setDisabled(True)
- self.btn_text_picker.setDisabled(True)
-
- self.changeCustomColors() # Update all the colours of all the charts
-
- p = self.btn_background_picker.palette()
- p.setColor(QtGui.QPalette.ButtonText, self.backgroundColor)
- self.btn_background_picker.setPalette(p)
-
- p = self.btn_foreground_picker.palette()
- p.setColor(QtGui.QPalette.ButtonText, self.foregroundColor)
- self.btn_foreground_picker.setPalette(p)
-
- p = self.btn_text_picker.palette()
- p.setColor(QtGui.QPalette.ButtonText, self.textColor)
- self.btn_text_picker.setPalette(p)
-
- p = self.btn_bands_picker.palette()
- p.setColor(QtGui.QPalette.ButtonText, self.bandsColor)
- self.btn_bands_picker.setPalette(p)
-
- p = self.btn_vswr_picker.palette()
- p.setColor(QtGui.QPalette.ButtonText, self.vswrColor)
- self.btn_vswr_picker.setPalette(p)
-
- left_layout.addWidget(display_options_box)
- left_layout.addWidget(charts_box)
- left_layout.addWidget(markers_box)
- left_layout.addStretch(1)
-
- right_layout.addWidget(color_options_box)
- right_layout.addWidget(font_options_box)
- right_layout.addWidget(bands_box)
- right_layout.addWidget(vswr_marker_box)
- right_layout.addStretch(1)
-
- def changeChart(self, x, y, chart):
- found = None
- for c in self.app.selectable_charts:
- if c.name == chart:
- found = c
-
- self.app.settings.setValue("Chart" + str(x) + str(y), chart)
-
- old_widget = self.app.charts_layout.itemAtPosition(x, y)
- if old_widget is not None:
- w = old_widget.widget()
- self.app.charts_layout.removeWidget(w)
- w.hide()
- if found is not None:
- if self.app.charts_layout.indexOf(found) > -1:
- logger.debug("%s is already shown, duplicating.", found.name)
- found = self.app.copyChart(found)
-
- self.app.charts_layout.addWidget(found, x, y)
- if found.isHidden():
- found.show()
-
- def changeReturnLoss(self):
- state = self.returnloss_is_positive.isChecked()
- self.app.settings.setValue("ReturnLossPositive", state)
-
- for m in self.app.markers:
- m.returnloss_is_positive = state
- m.updateLabels(self.app.data, self.app.data21)
- self.marker_window.exampleMarker.returnloss_is_positive = state
- self.marker_window.updateMarker()
- self.app.s11LogMag.isInverted = state
- self.app.s11LogMag.update()
-
- def changeShowLines(self):
- state = self.show_lines_option.isChecked()
- self.app.settings.setValue("ShowLines", state)
- for c in self.app.subscribing_charts:
- c.setDrawLines(state)
-
- def changeShowMarkerNumber(self):
- state = self.show_marker_number_option.isChecked()
- self.app.settings.setValue("ShowMarkerNumbers", state)
- for c in self.app.subscribing_charts:
- c.setDrawMarkerNumbers(state)
-
- def changeFilledMarkers(self):
- state = self.filled_marker_option.isChecked()
- self.app.settings.setValue("FilledMarkers", state)
- for c in self.app.subscribing_charts:
- c.setFilledMarkers(state)
-
- def changeMarkerAtTip(self):
- state = self.marker_at_tip.isChecked()
- self.app.settings.setValue("MarkerAtTip", state)
- for c in self.app.subscribing_charts:
- c.setMarkerAtTip(state)
-
- def changePointSize(self, size: int):
- self.app.settings.setValue("PointSize", size)
- for c in self.app.subscribing_charts:
- c.setPointSize(size)
-
- def changeLineThickness(self, size: int):
- self.app.settings.setValue("LineThickness", size)
- for c in self.app.subscribing_charts:
- c.setLineThickness(size)
-
- def changeMarkerSize(self, size: int):
- if size % 2 == 0:
- self.app.settings.setValue("MarkerSize", size)
- for c in self.app.subscribing_charts:
- c.setMarkerSize(int(size / 2))
-
- def validateMarkerSize(self):
- size = self.markerSizeInput.value()
- if size % 2 != 0:
- self.markerSizeInput.setValue(size + 1)
-
- def changeDarkMode(self):
- state = self.dark_mode_option.isChecked()
- self.app.settings.setValue("DarkMode", state)
- if state:
- for c in self.app.subscribing_charts:
- c.setBackgroundColor(QtGui.QColor(QtCore.Qt.black))
- c.setForegroundColor(QtGui.QColor(QtCore.Qt.lightGray))
- c.setTextColor(QtGui.QColor(QtCore.Qt.white))
- c.setSWRColor(self.vswrColor)
- else:
- for c in self.app.subscribing_charts:
- c.setBackgroundColor(QtGui.QColor(QtCore.Qt.white))
- c.setForegroundColor(QtGui.QColor(QtCore.Qt.lightGray))
- c.setTextColor(QtGui.QColor(QtCore.Qt.black))
- c.setSWRColor(self.vswrColor)
-
- def changeCustomColors(self):
- self.app.settings.setValue("UseCustomColors", self.use_custom_colors.isChecked())
- if self.use_custom_colors.isChecked():
- self.dark_mode_option.setDisabled(True)
- self.dark_mode_option.setChecked(False)
- self.btn_background_picker.setDisabled(False)
- self.btn_foreground_picker.setDisabled(False)
- self.btn_text_picker.setDisabled(False)
- for c in self.app.subscribing_charts:
- c.setBackgroundColor(self.backgroundColor)
- c.setForegroundColor(self.foregroundColor)
- c.setTextColor(self.textColor)
- c.setSWRColor(self.vswrColor)
- else:
- self.dark_mode_option.setDisabled(False)
- self.btn_background_picker.setDisabled(True)
- self.btn_foreground_picker.setDisabled(True)
- self.btn_text_picker.setDisabled(True)
- self.changeDarkMode() # Reset to the default colors depending on Dark Mode setting
-
- def setColor(self, name: str, color: QtGui.QColor):
- if name == "background":
- p = self.btn_background_picker.palette()
- p.setColor(QtGui.QPalette.ButtonText, color)
- self.btn_background_picker.setPalette(p)
- self.backgroundColor = color
- self.app.settings.setValue("BackgroundColor", color)
- elif name == "foreground":
- p = self.btn_foreground_picker.palette()
- p.setColor(QtGui.QPalette.ButtonText, color)
- self.btn_foreground_picker.setPalette(p)
- self.foregroundColor = color
- self.app.settings.setValue("ForegroundColor", color)
- elif name == "text":
- p = self.btn_text_picker.palette()
- p.setColor(QtGui.QPalette.ButtonText, color)
- self.btn_text_picker.setPalette(p)
- self.textColor = color
- self.app.settings.setValue("TextColor", color)
- elif name == "bands":
- p = self.btn_bands_picker.palette()
- p.setColor(QtGui.QPalette.ButtonText, color)
- self.btn_bands_picker.setPalette(p)
- self.bandsColor = color
- self.app.settings.setValue("BandsColor", color)
- self.app.bands.setColor(color)
- elif name == "vswr":
- p = self.btn_vswr_picker.palette()
- p.setColor(QtGui.QPalette.ButtonText, color)
- self.btn_vswr_picker.setPalette(p)
- self.vswrColor = color
- self.app.settings.setValue("VSWRColor", color)
- self.changeCustomColors()
-
- def setSweepColor(self, color: QtGui.QColor):
- if color.isValid():
- self.sweepColor = color
- p = self.btnColorPicker.palette()
- p.setColor(QtGui.QPalette.ButtonText, color)
- self.btnColorPicker.setPalette(p)
- self.app.settings.setValue("SweepColor", color)
- self.app.settings.sync()
- for c in self.app.subscribing_charts:
- c.setSweepColor(color)
-
- def setSecondarySweepColor(self, color: QtGui.QColor):
- if color.isValid():
- self.secondarySweepColor = color
- p = self.btnSecondaryColorPicker.palette()
- p.setColor(QtGui.QPalette.ButtonText, color)
- self.btnSecondaryColorPicker.setPalette(p)
- self.app.settings.setValue("SecondarySweepColor", color)
- self.app.settings.sync()
- for c in self.app.subscribing_charts:
- c.setSecondarySweepColor(color)
-
- def setReferenceColor(self, color):
- if color.isValid():
- self.referenceColor = color
- p = self.btnReferenceColorPicker.palette()
- p.setColor(QtGui.QPalette.ButtonText, color)
- self.btnReferenceColorPicker.setPalette(p)
- self.app.settings.setValue("ReferenceColor", color)
- self.app.settings.sync()
-
- for c in self.app.subscribing_charts:
- c.setReferenceColor(color)
-
- def setSecondaryReferenceColor(self, color):
- if color.isValid():
- self.secondaryReferenceColor = color
- p = self.btnSecondaryReferenceColorPicker.palette()
- p.setColor(QtGui.QPalette.ButtonText, color)
- self.btnSecondaryReferenceColorPicker.setPalette(p)
- self.app.settings.setValue("SecondaryReferenceColor", color)
- self.app.settings.sync()
-
- for c in self.app.subscribing_charts:
- c.setSecondaryReferenceColor(color)
-
- def setShowBands(self, show_bands):
- self.app.bands.enabled = show_bands
- self.app.bands.settings.setValue("ShowBands", show_bands)
- self.app.bands.settings.sync()
- for c in self.app.subscribing_charts:
- c.update()
-
- def changeFont(self):
- font_size = self.font_dropdown.currentText()
- self.app.settings.setValue("FontSize", font_size)
- app: QtWidgets.QApplication = QtWidgets.QApplication.instance()
- font = app.font()
- font.setPointSize(int(font_size))
- app.setFont(font)
- self.app.changeFont(font)
-
- def displayBandsWindow(self):
- self.bandsWindow.show()
- QtWidgets.QApplication.setActiveWindow(self.bandsWindow)
-
- def displayMarkerWindow(self):
- self.marker_window.show()
- QtWidgets.QApplication.setActiveWindow(self.marker_window)
-
- def addMarker(self):
- new_marker = Marker("", self.app.settings)
- new_marker.setScale(self.app.scaleFactor)
- self.app.markers.append(new_marker)
- self.app.marker_data_layout.addWidget(new_marker.getGroupBox())
-
- new_marker.updated.connect(self.app.markerUpdated)
- label, layout = new_marker.getRow()
- self.app.marker_control_layout.insertRow(Marker.count() - 1, label, layout)
- self.btn_remove_marker.setDisabled(False)
-
- def removeMarker(self):
- # keep at least one marker
- if Marker.count() <= 1:
- return
- if Marker.count() == 2:
- self.btn_remove_marker.setDisabled(True)
- last_marker = self.app.markers.pop()
-
- last_marker.updated.disconnect(self.app.markerUpdated)
- self.app.marker_data_layout.removeWidget(last_marker.getGroupBox())
- self.app.marker_control_layout.removeRow(Marker.count()-1)
- last_marker.getGroupBox().hide()
- last_marker.getGroupBox().destroy()
- label, layout = last_marker.getRow()
- label.hide()
-
- def addVSWRMarker(self):
- value, selected = QtWidgets.QInputDialog.getDouble(self, "Add VSWR Marker",
- "VSWR value to show:", min=1.001, decimals=3)
- if selected:
- self.vswrMarkers.append(value)
- if self.vswr_marker_dropdown.itemText(0) == "None":
- self.vswr_marker_dropdown.removeItem(0)
- self.vswr_marker_dropdown.addItem(str(value))
- self.vswr_marker_dropdown.setCurrentText(str(value))
- for c in self.app.s11charts:
- c.addSWRMarker(value)
- self.app.settings.setValue("VSWRMarkers", self.vswrMarkers)
-
- def removeVSWRMarker(self):
- value_str = self.vswr_marker_dropdown.currentText()
- if value_str != "None":
- value = float(value_str)
- self.vswrMarkers.remove(value)
- self.vswr_marker_dropdown.removeItem(self.vswr_marker_dropdown.currentIndex())
- if self.vswr_marker_dropdown.count() == 0:
- self.vswr_marker_dropdown.addItem("None")
- self.app.settings.remove("VSWRMarkers")
- else:
- self.app.settings.setValue("VSWRMarkers", self.vswrMarkers)
- for c in self.app.s11charts:
- c.removeSWRMarker(value)
-
-
-class AboutWindow(QtWidgets.QWidget):
- def __init__(self, app: NanoVNASaver):
- super().__init__()
- self.app = app
-
- self.setWindowTitle("About NanoVNASaver")
- self.setWindowIcon(self.app.icon)
- top_layout = QtWidgets.QHBoxLayout()
- self.setLayout(top_layout)
- #self.setAutoFillBackground(True)
- shortcut = QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
-
- icon_layout = QtWidgets.QVBoxLayout()
- top_layout.addLayout(icon_layout)
- icon = QtWidgets.QLabel()
- icon.setPixmap(self.app.icon.pixmap(128, 128))
- icon_layout.addWidget(icon)
- icon_layout.addStretch()
-
- layout = QtWidgets.QVBoxLayout()
- top_layout.addLayout(layout)
-
- layout.addWidget(QtWidgets.QLabel("NanoVNASaver version " + NanoVNASaver.version))
- layout.addWidget(QtWidgets.QLabel(""))
- layout.addWidget(QtWidgets.QLabel("\N{COPYRIGHT SIGN} Copyright 2019 Rune B. Broberg"))
- layout.addWidget(QtWidgets.QLabel("This program comes with ABSOLUTELY NO WARRANTY"))
- layout.addWidget(QtWidgets.QLabel("This program is licensed under the GNU General Public License version 3"))
- layout.addWidget(QtWidgets.QLabel(""))
- link_label = QtWidgets.QLabel("For further details, see: " +
- "" +
- "https://mihtjel.github.io/nanovna-saver/")
- link_label.setOpenExternalLinks(True)
- layout.addWidget(link_label)
- layout.addWidget(QtWidgets.QLabel(""))
-
- self.versionLabel = QtWidgets.QLabel("NanoVNA Firmware Version: Not connected.")
- layout.addWidget(self.versionLabel)
-
- layout.addStretch()
-
- btn_check_version = QtWidgets.QPushButton("Check for updates")
- btn_check_version.clicked.connect(self.findUpdates)
-
- self.updateLabel = QtWidgets.QLabel("Last checked: ")
- self.updateCheckBox = QtWidgets.QCheckBox("Check for updates on startup")
-
- self.updateCheckBox.toggled.connect(self.updateSettings)
-
- check_for_updates = self.app.settings.value("CheckForUpdates", "Ask")
- if check_for_updates == "Yes":
- self.updateCheckBox.setChecked(True)
- self.findUpdates(automatic=True)
- elif check_for_updates == "No":
- self.updateCheckBox.setChecked(False)
- else:
- logger.debug("Starting timer")
- QtCore.QTimer.singleShot(2000, self.askAboutUpdates)
-
- update_hbox = QtWidgets.QHBoxLayout()
- update_hbox.addWidget(btn_check_version)
- update_form = QtWidgets.QFormLayout()
- update_hbox.addLayout(update_form)
- update_hbox.addStretch()
- update_form.addRow(self.updateLabel)
- update_form.addRow(self.updateCheckBox)
- layout.addLayout(update_hbox)
-
- layout.addStretch()
-
- btn_ok = QtWidgets.QPushButton("Ok")
- btn_ok.clicked.connect(lambda: self.close())
- layout.addWidget(btn_ok)
-
- def show(self):
- super().show()
- self.updateLabels()
-
- def updateLabels(self):
- if self.app.vna.isValid():
- logger.debug("Valid VNA")
- v: Version = self.app.vna.version
- self.versionLabel.setText("NanoVNA Firmware Version: " + self.app.vna.name + " " + v.version_string)
-
- def updateSettings(self):
- if self.updateCheckBox.isChecked():
- self.app.settings.setValue("CheckForUpdates", "Yes")
- else:
- self.app.settings.setValue("CheckForUpdates", "No")
-
- def askAboutUpdates(self):
- logger.debug("Asking about automatic update checks")
- selection = QtWidgets.QMessageBox.question(self.app, "Enable checking for updates?",
- "Would you like NanoVNA-Saver to check for updates automatically?")
- if selection == QtWidgets.QMessageBox.Yes:
- self.updateCheckBox.setChecked(True)
- self.app.settings.setValue("CheckForUpdates", "Yes")
- self.findUpdates()
- elif selection == QtWidgets.QMessageBox.No:
- self.updateCheckBox.setChecked(False)
- self.app.settings.setValue("CheckForUpdates", "No")
- QtWidgets.QMessageBox.information(self.app, "Checking for updates disabled",
- "You can check for updates using the \"About\" window.")
- else:
- self.app.settings.setValue("CheckForUpdates", "Ask")
-
- def findUpdates(self, automatic=False):
- from urllib import request, error
- import json
- update_url = "http://mihtjel.dk/nanovna-saver/latest.json"
-
- try:
- req = request.Request(update_url)
- req.add_header('User-Agent', "NanoVNA-Saver/" + self.app.version)
- updates = json.load(request.urlopen(req, timeout=3))
- latest_version = Version(updates['version'])
- latest_url = updates['url']
- except error.HTTPError as e:
- logger.exception("Checking for updates produced an HTTP exception: %s", e)
- self.updateLabel.setText("Connection error.")
- return
- except json.JSONDecodeError as e:
- logger.exception("Checking for updates provided an unparseable file: %s", e)
- self.updateLabel.setText("Data error reading versions.")
- return
- except error.URLError as e:
- logger.exception("Checking for updates produced a URL exception: %s", e)
- self.updateLabel.setText("Connection error.")
- return
-
- logger.info("Latest version is " + latest_version.version_string)
- this_version = Version(NanoVNASaver.version)
- logger.info("This is " + this_version.version_string)
- if latest_version > this_version:
- logger.info("New update available: %s!", latest_version)
- if automatic:
- QtWidgets.QMessageBox.information(self, "Updates available",
- "There is a new update for NanoVNA-Saver available!\n" +
- "Version " + latest_version.version_string + "\n\n" +
- "Press \"About\" to find the update.")
- else:
- QtWidgets.QMessageBox.information(self, "Updates available",
- "There is a new update for NanoVNA-Saver available!")
- self.updateLabel.setText("New version available.")
- self.updateLabel.setOpenExternalLinks(True)
- else:
- # Probably don't show a message box, just update the screen?
- # Maybe consider showing it if not an automatic update.
- #
- # QtWidgets.QMessageBox.information(self, "No updates available", "There are no new updates available.")
- #
- self.updateLabel.setText("Last checked: " + strftime("%Y-%m-%d %H:%M:%S", localtime()))
- return
-
-
-class TDRWindow(QtWidgets.QWidget):
- updated = QtCore.pyqtSignal()
-
- def __init__(self, app: NanoVNASaver):
- super().__init__()
- self.app = app
-
- self.td = []
- self.distance_axis = []
- self.step_response = []
- self.step_response_Z = []
-
- self.setWindowTitle("TDR")
- self.setWindowIcon(self.app.icon)
-
- shortcut = QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
-
- layout = QtWidgets.QFormLayout()
- self.setLayout(layout)
-
- self.tdr_velocity_dropdown = QtWidgets.QComboBox()
- self.tdr_velocity_dropdown.addItem("Jelly filled (0.64)", 0.64)
- self.tdr_velocity_dropdown.addItem("Polyethylene (0.66)", 0.66)
- self.tdr_velocity_dropdown.addItem("PTFE (Teflon) (0.70)", 0.70)
- self.tdr_velocity_dropdown.addItem("Pulp Insulation (0.72)", 0.72)
- self.tdr_velocity_dropdown.addItem("Foam or Cellular PE (0.78)", 0.78)
- self.tdr_velocity_dropdown.addItem("Semi-solid PE (SSPE) (0.84)", 0.84)
- self.tdr_velocity_dropdown.addItem("Air (Helical spacers) (0.94)", 0.94)
- self.tdr_velocity_dropdown.insertSeparator(self.tdr_velocity_dropdown.count())
- # Lots of cable types added by Larry Goga, AE5CZ
- self.tdr_velocity_dropdown.addItem("RG-6/U PE 75\N{OHM SIGN} (Belden 8215) (0.66)", 0.66)
- self.tdr_velocity_dropdown.addItem("RG-6/U Foam 75\N{OHM SIGN} (Belden 9290) (0.81)", 0.81)
- self.tdr_velocity_dropdown.addItem("RG-8/U PE 50\N{OHM SIGN} (Belden 8237) (0.66)", 0.66)
- self.tdr_velocity_dropdown.addItem("RG-8/U Foam (Belden 8214) (0.78)", 0.78)
- self.tdr_velocity_dropdown.addItem("RG-8/U (Belden 9913) (0.84)", 0.84)
- self.tdr_velocity_dropdown.addItem("RG-8X (Belden 9258) (0.82)", 0.82)
- self.tdr_velocity_dropdown.addItem("RG-11/U 75\N{OHM SIGN} Foam HDPE (Belden 9292) (0.84)", 0.84)
- self.tdr_velocity_dropdown.addItem("RG-58/U 52\N{OHM SIGN} PE (Belden 9201) (0.66)", 0.66)
- self.tdr_velocity_dropdown.addItem("RG-58A/U 54\N{OHM SIGN} Foam (Belden 8219) (0.73)", 0.73)
- self.tdr_velocity_dropdown.addItem("RG-59A/U PE 75\N{OHM SIGN} (Belden 8241) (0.66)", 0.66)
- self.tdr_velocity_dropdown.addItem("RG-59A/U Foam 75\N{OHM SIGN} (Belden 8241F) (0.78)", 0.78)
- self.tdr_velocity_dropdown.addItem("RG-174 PE (Belden 8216)(0.66)", 0.66)
- self.tdr_velocity_dropdown.addItem("RG-174 Foam (Belden 7805R) (0.735)", 0.735)
- self.tdr_velocity_dropdown.addItem("RG-213/U PE (Belden 8267) (0.66)", 0.66)
- self.tdr_velocity_dropdown.addItem("RG316 (0.695)", 0.695)
- self.tdr_velocity_dropdown.addItem("RG402 (0.695)", 0.695)
- self.tdr_velocity_dropdown.addItem("LMR-240 (0.84)", 0.84)
- self.tdr_velocity_dropdown.addItem("LMR-240UF (0.80)", 0.80)
- self.tdr_velocity_dropdown.addItem("LMR-400 (0.85)", 0.85)
- self.tdr_velocity_dropdown.addItem("LMR400UF (0.83)", 0.83)
- self.tdr_velocity_dropdown.addItem("Davis Bury-FLEX (0.82)", 0.82)
- self.tdr_velocity_dropdown.insertSeparator(self.tdr_velocity_dropdown.count())
- self.tdr_velocity_dropdown.addItem("Custom", -1)
-
- self.tdr_velocity_dropdown.setCurrentIndex(1) # Default to PE (0.66)
-
- self.tdr_velocity_dropdown.currentIndexChanged.connect(self.updateTDR)
-
- layout.addRow(self.tdr_velocity_dropdown)
-
- self.tdr_velocity_input = QtWidgets.QLineEdit()
- self.tdr_velocity_input.setDisabled(True)
- self.tdr_velocity_input.setText("0.66")
- self.tdr_velocity_input.textChanged.connect(self.app.dataUpdated)
-
- layout.addRow("Velocity factor", self.tdr_velocity_input)
-
- self.tdr_result_label = QtWidgets.QLabel()
- layout.addRow("Estimated cable length:", self.tdr_result_label)
-
- layout.addRow(self.app.tdr_chart)
-
- def updateTDR(self):
- c = 299792458
- # TODO: Let the user select whether to use high or low resolution TDR?
- FFT_POINTS = 2**14
-
- if len(self.app.data) < 2:
- return
-
- if self.tdr_velocity_dropdown.currentData() == -1:
- self.tdr_velocity_input.setDisabled(False)
- else:
- self.tdr_velocity_input.setDisabled(True)
- self.tdr_velocity_input.setText(str(self.tdr_velocity_dropdown.currentData()))
-
- try:
- v = float(self.tdr_velocity_input.text())
- except ValueError:
- return
-
- step_size = self.app.data[1].freq - self.app.data[0].freq
- if step_size == 0:
- self.tdr_result_label.setText("")
- logger.info("Cannot compute cable length at 0 span")
- return
-
- s11 = []
- for d in self.app.data:
- s11.append(np.complex(d.re, d.im))
-
- window = np.blackman(len(self.app.data))
-
- windowed_s11 = window * s11
- self.td = np.abs(np.fft.ifft(windowed_s11, FFT_POINTS))
- step = np.ones(FFT_POINTS)
- self.step_response = signal.convolve(self.td, step)
-
- self.step_response_Z = 50 * (1 + self.step_response) / (1 - self.step_response)
-
- time_axis = np.linspace(0, 1/step_size, FFT_POINTS)
- self.distance_axis = time_axis * v * c
- # peak = np.max(td) # We should check that this is an actual *peak*, and not just a vague maximum
- index_peak = np.argmax(self.td)
-
- cable_len = round(self.distance_axis[index_peak]/2, 3)
- feet = math.floor(cable_len / 0.3048)
- inches = round(((cable_len / 0.3048) - feet)*12, 1)
-
- self.tdr_result_label.setText(str(cable_len) + " m (" + str(feet) + "ft " + str(inches) + "in)")
- self.app.tdr_result_label.setText(str(cable_len) + " m")
- self.updated.emit()
-
-
-class SweepSettingsWindow(QtWidgets.QWidget):
- def __init__(self, app: NanoVNASaver):
- super().__init__()
-
- self.app = app
- self.setWindowTitle("Sweep settings")
- self.setWindowIcon(self.app.icon)
-
- shortcut = QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
-
- layout = QtWidgets.QVBoxLayout()
- self.setLayout(layout)
-
- title_box = QtWidgets.QGroupBox("Sweep name")
- title_layout = QtWidgets.QFormLayout(title_box)
- self.sweep_title_input = QtWidgets.QLineEdit()
- title_layout.addRow("Sweep name", self.sweep_title_input)
- title_button_layout = QtWidgets.QHBoxLayout()
- btn_set_sweep_title = QtWidgets.QPushButton("Set")
- btn_set_sweep_title.clicked.connect(lambda: self.app.setSweepTitle(self.sweep_title_input.text()))
- btn_reset_sweep_title = QtWidgets.QPushButton("Reset")
- btn_reset_sweep_title.clicked.connect(lambda: self.app.setSweepTitle(""))
- title_button_layout.addWidget(btn_set_sweep_title)
- title_button_layout.addWidget(btn_reset_sweep_title)
- title_layout.addRow(title_button_layout)
- layout.addWidget(title_box)
-
- settings_box = QtWidgets.QGroupBox("Settings")
- settings_layout = QtWidgets.QFormLayout(settings_box)
-
- self.single_sweep_radiobutton = QtWidgets.QRadioButton("Single sweep")
- self.continuous_sweep_radiobutton = QtWidgets.QRadioButton("Continuous sweep")
- self.averaged_sweep_radiobutton = QtWidgets.QRadioButton("Averaged sweep")
-
- settings_layout.addWidget(self.single_sweep_radiobutton)
- self.single_sweep_radiobutton.setChecked(True)
- settings_layout.addWidget(self.continuous_sweep_radiobutton)
- settings_layout.addWidget(self.averaged_sweep_radiobutton)
-
- self.averages = QtWidgets.QLineEdit("3")
- self.truncates = QtWidgets.QLineEdit("0")
-
- settings_layout.addRow("Number of measurements to average", self.averages)
- settings_layout.addRow("Number to discard", self.truncates)
- settings_layout.addRow(QtWidgets.QLabel("Averaging allows discarding outlying samples to get better averages."))
- settings_layout.addRow(QtWidgets.QLabel("Common values are 3/0, 5/2, 9/4 and 25/6."))
-
- self.continuous_sweep_radiobutton.toggled.connect(
- lambda: self.app.worker.setContinuousSweep(self.continuous_sweep_radiobutton.isChecked()))
- self.averaged_sweep_radiobutton.toggled.connect(self.updateAveraging)
- self.averages.textEdited.connect(self.updateAveraging)
- self.truncates.textEdited.connect(self.updateAveraging)
-
- layout.addWidget(settings_box)
-
- band_sweep_box = QtWidgets.QGroupBox("Sweep band")
- band_sweep_layout = QtWidgets.QFormLayout(band_sweep_box)
-
- self.band_list = QtWidgets.QComboBox()
- self.band_list.setModel(self.app.bands)
- self.band_list.currentIndexChanged.connect(self.updateCurrentBand)
-
- band_sweep_layout.addRow("Select band", self.band_list)
-
- self.band_pad_group = QtWidgets.QButtonGroup()
- self.band_pad_0 = QtWidgets.QRadioButton("None")
- self.band_pad_10 = QtWidgets.QRadioButton("10%")
- self.band_pad_25 = QtWidgets.QRadioButton("25%")
- self.band_pad_100 = QtWidgets.QRadioButton("100%")
- self.band_pad_0.setChecked(True)
- self.band_pad_group.addButton(self.band_pad_0)
- self.band_pad_group.addButton(self.band_pad_10)
- self.band_pad_group.addButton(self.band_pad_25)
- self.band_pad_group.addButton(self.band_pad_100)
- self.band_pad_group.buttonClicked.connect(self.updateCurrentBand)
- band_sweep_layout.addRow("Pad band limits", self.band_pad_0)
- band_sweep_layout.addRow("", self.band_pad_10)
- band_sweep_layout.addRow("", self.band_pad_25)
- band_sweep_layout.addRow("", self.band_pad_100)
-
- self.band_limit_label = QtWidgets.QLabel()
-
- band_sweep_layout.addRow(self.band_limit_label)
-
- btn_set_band_sweep = QtWidgets.QPushButton("Set band sweep")
- btn_set_band_sweep.clicked.connect(self.setBandSweep)
- band_sweep_layout.addRow(btn_set_band_sweep)
-
- self.updateCurrentBand()
-
- layout.addWidget(band_sweep_box)
-
- def updateCurrentBand(self):
- index_start = self.band_list.model().index(self.band_list.currentIndex(), 1)
- index_stop = self.band_list.model().index(self.band_list.currentIndex(), 2)
- start = int(self.band_list.model().data(index_start, QtCore.Qt.ItemDataRole).value())
- stop = int(self.band_list.model().data(index_stop, QtCore.Qt.ItemDataRole).value())
-
- if self.band_pad_10.isChecked():
- padding = 10
- elif self.band_pad_25.isChecked():
- padding = 25
- elif self.band_pad_100.isChecked():
- padding = 100
- else:
- padding = 0
-
- if padding > 0:
- span = stop - start
- start -= round(span * padding / 100)
- start = max(1, start)
- stop += round(span * padding / 100)
-
- self.band_limit_label.setText("Sweep span: " + RFTools.formatShortFrequency(start) + " to " +
- RFTools.formatShortFrequency(stop))
-
- def setBandSweep(self):
- index_start = self.band_list.model().index(self.band_list.currentIndex(), 1)
- index_stop = self.band_list.model().index(self.band_list.currentIndex(), 2)
- start = int(self.band_list.model().data(index_start, QtCore.Qt.ItemDataRole).value())
- stop = int(self.band_list.model().data(index_stop, QtCore.Qt.ItemDataRole).value())
-
- if self.band_pad_10.isChecked():
- padding = 10
- elif self.band_pad_25.isChecked():
- padding = 25
- elif self.band_pad_100.isChecked():
- padding = 100
- else:
- padding = 0
-
- if padding > 0:
- span = stop - start
- start -= round(span * padding / 100)
- start = max(1, start)
- stop += round(span * padding / 100)
-
- self.app.sweepStartInput.setText(RFTools.formatSweepFrequency(start))
- self.app.sweepEndInput.setText(RFTools.formatSweepFrequency(stop))
- self.app.sweepEndInput.textEdited.emit(self.app.sweepEndInput.text())
-
- def updateAveraging(self):
- self.app.worker.setAveraging(self.averaged_sweep_radiobutton.isChecked(),
- self.averages.text(),
- self.truncates.text())
-
-
-class BandsWindow(QtWidgets.QWidget):
- def __init__(self, app):
- super().__init__()
-
- self.app: NanoVNASaver = app
- self.setWindowTitle("Manage bands")
- self.setWindowIcon(self.app.icon)
-
- shortcut = QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
-
- layout = QtWidgets.QVBoxLayout()
- self.setLayout(layout)
- self.setMinimumSize(500, 300)
-
- self.bands_table = QtWidgets.QTableView()
- self.bands_table.setModel(self.app.bands)
- self.bands_table.horizontalHeader().setStretchLastSection(True)
-
- layout.addWidget(self.bands_table)
-
- btn_add_row = QtWidgets.QPushButton("Add row")
- btn_delete_row = QtWidgets.QPushButton("Delete row")
- btn_reset_bands = QtWidgets.QPushButton("Reset bands")
- btn_layout = QtWidgets.QHBoxLayout()
- btn_layout.addWidget(btn_add_row)
- btn_layout.addWidget(btn_delete_row)
- btn_layout.addWidget(btn_reset_bands)
- layout.addLayout(btn_layout)
-
- btn_add_row.clicked.connect(self.app.bands.addRow)
- btn_delete_row.clicked.connect(self.deleteRows)
- btn_reset_bands.clicked.connect(self.resetBands)
-
- def deleteRows(self):
- rows = self.bands_table.selectedIndexes()
- for row in rows:
- self.app.bands.removeRow(row.row())
-
- def resetBands(self):
- confirm = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Warning,
- "Confirm reset",
- "Are you sure you want to reset the bands to default?",
- QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Cancel).exec()
- if confirm == QtWidgets.QMessageBox.Yes:
- self.app.bands.resetBands()
-
-
-class BandsModel(QtCore.QAbstractTableModel):
- bands: List[Tuple[str, int, int]] = []
- enabled = False
- color = QtGui.QColor(128, 128, 128, 48)
-
- # These bands correspond broadly to the Danish Amateur Radio allocation
- default_bands = ["2200 m;135700;137800",
- "630 m;472000;479000",
- "160 m;1800000;2000000",
- "80 m;3500000;3800000",
- "60 m;5250000;5450000",
- "40 m;7000000;7200000",
- "30 m;10100000;10150000",
- "20 m;14000000;14350000",
- "17 m;18068000;18168000",
- "15 m;21000000;21450000",
- "12 m;24890000;24990000",
- "10 m;28000000;29700000",
- "6 m;50000000;52000000",
- "4 m;69887500;70512500",
- "2 m;144000000;146000000",
- "70 cm;432000000;438000000",
- "23 cm;1240000000;1300000000"]
-
- def __init__(self):
- super().__init__()
- self.settings = QtCore.QSettings(QtCore.QSettings.IniFormat,
- QtCore.QSettings.UserScope,
- "NanoVNASaver", "Bands")
- self.settings.setIniCodec("UTF-8")
- self.enabled = self.settings.value("ShowBands", False, bool)
-
- stored_bands: List[str] = self.settings.value("bands", self.default_bands)
- if stored_bands:
- for b in stored_bands:
- (name, start, end) = b.split(";")
- self.bands.append((name, int(start), int(end)))
-
- def saveSettings(self):
- stored_bands = []
- for b in self.bands:
- stored_bands.append(b[0] + ";" + str(b[1]) + ";" + str(b[2]))
- self.settings.setValue("bands", stored_bands)
- self.settings.sync()
-
- def resetBands(self):
- self.bands = []
- for b in self.default_bands:
- (name, start, end) = b.split(";")
- self.bands.append((name, int(start), int(end)))
- self.layoutChanged.emit()
- self.saveSettings()
-
- def columnCount(self, parent: QModelIndex = ...) -> int:
- return 3
-
- def rowCount(self, parent: QModelIndex = ...) -> int:
- return len(self.bands)
-
- def data(self, index: QModelIndex, role: int = ...) -> QtCore.QVariant:
- if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.ItemDataRole or role == QtCore.Qt.EditRole:
- return QtCore.QVariant(self.bands[index.row()][index.column()])
- elif role == QtCore.Qt.TextAlignmentRole:
- if index.column() == 0:
- return QtCore.QVariant(QtCore.Qt.AlignCenter)
- else:
- return QtCore.QVariant(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
- else:
- return QtCore.QVariant()
-
- def setData(self, index: QModelIndex, value: typing.Any, role: int = ...) -> bool:
- if role == QtCore.Qt.EditRole and index.isValid():
- t = self.bands[index.row()]
- name = t[0]
- start = t[1]
- end = t[2]
- if index.column() == 0:
- name = value
- elif index.column() == 1:
- start = value
- elif index.column() == 2:
- end = value
- self.bands[index.row()] = (name, start, end)
- self.dataChanged.emit(index, index)
- self.saveSettings()
- return True
- return False
-
- def index(self, row: int, column: int, parent: QModelIndex = ...) -> QModelIndex:
- return self.createIndex(row, column)
-
- def addRow(self):
- self.bands.append(("New", 0, 0))
- self.dataChanged.emit(self.index(len(self.bands), 0), self.index(len(self.bands), 2))
- self.layoutChanged.emit()
-
- def removeRow(self, row: int, parent: QModelIndex = ...) -> bool:
- self.bands.remove(self.bands[row])
- self.layoutChanged.emit()
- self.saveSettings()
- return True
-
- def headerData(self, section: int, orientation: QtCore.Qt.Orientation, role: int = ...):
- if role == QtCore.Qt.DisplayRole and orientation == QtCore.Qt.Horizontal:
- if section == 0:
- return "Band"
- if section == 1:
- return "Start (Hz)"
- if section == 2:
- return "End (Hz)"
- else:
- return "Invalid"
- else:
- super().headerData(section, orientation, role)
-
- def flags(self, index: QModelIndex) -> QtCore.Qt.ItemFlags:
- if index.isValid():
- return QtCore.Qt.ItemFlags(QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable)
- else:
- super().flags(index)
-
- def setColor(self, color):
- self.color = color
-
-
-class AnalysisWindow(QtWidgets.QWidget):
- analyses = []
- analysis: Analysis = None
-
- def __init__(self, app):
- super().__init__()
-
- self.app: NanoVNASaver = app
- self.setWindowTitle("Sweep analysis")
- self.setWindowIcon(self.app.icon)
-
- shortcut = QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
-
- layout = QtWidgets.QVBoxLayout()
- self.setLayout(layout)
-
- select_analysis_box = QtWidgets.QGroupBox("Select analysis")
- select_analysis_layout = QtWidgets.QFormLayout(select_analysis_box)
- self.analysis_list = QtWidgets.QComboBox()
- self.analysis_list.addItem("Low-pass filter", LowPassAnalysis(self.app))
- self.analysis_list.addItem("Band-pass filter", BandPassAnalysis(self.app))
- self.analysis_list.addItem("High-pass filter", HighPassAnalysis(self.app))
- self.analysis_list.addItem("Band-stop filter", BandStopAnalysis(self.app))
- # self.analysis_list.addItem("Peak search", PeakSearchAnalysis(self.app))
- self.analysis_list.addItem("Peak search", SimplePeakSearchAnalysis(self.app))
- self.analysis_list.addItem("VSWR analysis", VSWRAnalysis(self.app))
- select_analysis_layout.addRow("Analysis type", self.analysis_list)
- self.analysis_list.currentIndexChanged.connect(self.updateSelection)
-
- btn_run_analysis = QtWidgets.QPushButton("Run analysis")
- btn_run_analysis.clicked.connect(self.runAnalysis)
- select_analysis_layout.addRow(btn_run_analysis)
-
- self.checkbox_run_automatically = QtWidgets.QCheckBox("Run automatically")
- self.checkbox_run_automatically.stateChanged.connect(self.toggleAutomaticRun)
- select_analysis_layout.addRow(self.checkbox_run_automatically)
-
- analysis_box = QtWidgets.QGroupBox("Analysis")
- analysis_box.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding)
-
- self.analysis_layout = QtWidgets.QVBoxLayout(analysis_box)
- self.analysis_layout.setContentsMargins(0, 0, 0, 0)
-
- layout.addWidget(select_analysis_box)
- layout.addWidget(analysis_box)
-
- self.updateSelection()
-
- def runAnalysis(self):
- if self.analysis is not None:
- self.analysis.runAnalysis()
-
- def updateSelection(self):
- self.analysis = self.analysis_list.currentData()
- old_item = self.analysis_layout.itemAt(0)
- if old_item is not None:
- old_widget = self.analysis_layout.itemAt(0).widget()
- self.analysis_layout.replaceWidget(old_widget, self.analysis.widget())
- old_widget.hide()
- else:
- self.analysis_layout.addWidget(self.analysis.widget())
- self.analysis.widget().show()
- self.update()
-
- def toggleAutomaticRun(self, state: QtCore.Qt.CheckState):
- if state == QtCore.Qt.Checked:
- self.analysis_list.setDisabled(True)
- self.app.dataAvailable.connect(self.runAnalysis)
- else:
- self.analysis_list.setDisabled(False)
- self.app.dataAvailable.disconnect(self.runAnalysis)
-
-
-class DeviceSettingsWindow(QtWidgets.QWidget):
- def __init__(self, app: NanoVNASaver):
- super().__init__()
-
- self.app = app
- self.setWindowTitle("Device settings")
- self.setWindowIcon(self.app.icon)
-
- shortcut = QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
-
- layout = QtWidgets.QVBoxLayout()
- self.setLayout(layout)
-
- status_box = QtWidgets.QGroupBox("Status")
- status_layout = QtWidgets.QFormLayout(status_box)
- self.statusLabel = QtWidgets.QLabel("Not connected.")
- status_layout.addRow("Status:", self.statusLabel)
-
- self.calibrationStatusLabel = QtWidgets.QLabel("Not connected.")
- status_layout.addRow("Calibration:", self.calibrationStatusLabel)
-
- status_layout.addRow(QtWidgets.QLabel("Features:"))
- self.featureList = QtWidgets.QListWidget()
- status_layout.addRow(self.featureList)
-
- settings_box = QtWidgets.QGroupBox("Settings")
- settings_layout = QtWidgets.QFormLayout(settings_box)
-
- self.chkValidateInputData = QtWidgets.QCheckBox("Validate received data")
- validate_input = self.app.settings.value("SerialInputValidation", True, bool)
- self.chkValidateInputData.setChecked(validate_input)
- self.chkValidateInputData.stateChanged.connect(self.updateValidation)
- settings_layout.addRow("Validation", self.chkValidateInputData)
-
- control_layout = QtWidgets.QHBoxLayout()
- self.btnRefresh = QtWidgets.QPushButton("Refresh")
- self.btnRefresh.clicked.connect(self.updateFields)
- control_layout.addWidget(self.btnRefresh)
-
- self.screenshotWindow = ScreenshotWindow()
- self.btnCaptureScreenshot = QtWidgets.QPushButton("Screenshot")
- self.btnCaptureScreenshot.clicked.connect(self.captureScreenshot)
- control_layout.addWidget(self.btnCaptureScreenshot)
-
- layout.addWidget(status_box)
- layout.addWidget(settings_box)
- layout.addLayout(control_layout)
-
- def show(self):
- super().show()
- self.updateFields()
-
- def updateFields(self):
- if self.app.vna.isValid():
- self.statusLabel.setText("Connected to " + self.app.vna.name + ".")
- if self.app.worker.running:
- self.calibrationStatusLabel.setText("(Sweep running)")
- else:
- self.calibrationStatusLabel.setText(self.app.vna.getCalibration())
-
- self.featureList.clear()
- self.featureList.addItem(self.app.vna.name + " v" + str(self.app.vna.version))
- features = self.app.vna.getFeatures()
- for item in features:
- self.featureList.addItem(item)
-
- if "Screenshots" in features:
- self.btnCaptureScreenshot.setDisabled(False)
- else:
- self.btnCaptureScreenshot.setDisabled(True)
- else:
- self.statusLabel.setText("Not connected.")
- self.calibrationStatusLabel.setText("Not connected.")
- self.featureList.clear()
- self.featureList.addItem("Not connected.")
- self.btnCaptureScreenshot.setDisabled(True)
-
- def updateValidation(self, validate_data: bool):
- self.app.vna.validateInput = validate_data
- self.app.settings.setValue("SerialInputValidation", validate_data)
-
- def captureScreenshot(self):
- if not self.app.worker.running:
- pixmap = self.app.vna.getScreenshot()
- self.screenshotWindow.setScreenshot(pixmap)
- self.screenshotWindow.show()
- else:
- # TODO: Tell the user no screenshots while sweep is running?
- # TODO: Consider having a list of widgets that want to be disabled when a sweep is running?
- pass
-
-
-class ScreenshotWindow(QtWidgets.QLabel):
- pix = None
-
- def __init__(self):
- super().__init__()
- self.setWindowTitle("Screenshot")
- # TODO : self.setWindowIcon(self.app.icon)
-
- shortcut = QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
- self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
-
- self.action_original_size = QtWidgets.QAction("Original size")
- self.action_original_size.triggered.connect(lambda: self.setScale(1))
- self.action_2x_size = QtWidgets.QAction("2x size")
- self.action_2x_size.triggered.connect(lambda: self.setScale(2))
- self.action_3x_size = QtWidgets.QAction("3x size")
- self.action_3x_size.triggered.connect(lambda: self.setScale(3))
- self.action_4x_size = QtWidgets.QAction("4x size")
- self.action_4x_size.triggered.connect(lambda: self.setScale(4))
- self.action_5x_size = QtWidgets.QAction("5x size")
- self.action_5x_size.triggered.connect(lambda: self.setScale(5))
-
- self.addAction(self.action_original_size)
- self.addAction(self.action_2x_size)
- self.addAction(self.action_3x_size)
- self.addAction(self.action_4x_size)
- self.addAction(self.action_5x_size)
- self.action_save_screenshot = QtWidgets.QAction("Save image")
- self.action_save_screenshot.triggered.connect(self.saveScreenshot)
- self.addAction(self.action_save_screenshot)
-
- def setScreenshot(self, pixmap: QtGui.QPixmap):
- if self.pix is None:
- self.resize(pixmap.size())
- self.pix = pixmap
- self.setPixmap(self.pix.scaled(self.size(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.FastTransformation))
- w, h = pixmap.width(), pixmap.height()
- self.action_original_size.setText("Original size (" + str(w) + "x" + str(h) + ")")
- self.action_2x_size.setText("2x size (" + str(w * 2) + "x" + str(h * 2) + ")")
- self.action_3x_size.setText("3x size (" + str(w * 3) + "x" + str(h * 3) + ")")
- self.action_4x_size.setText("4x size (" + str(w * 4) + "x" + str(h * 4) + ")")
- self.action_5x_size.setText("5x size (" + str(w * 5) + "x" + str(h * 5) + ")")
-
- def saveScreenshot(self):
- if self.pix is not None:
- logger.info("Saving screenshot to file...")
- filename, _ = QtWidgets.QFileDialog.getSaveFileName(parent=self, caption="Save image",
- filter="PNG (*.png);;All files (*.*)")
-
- logger.debug("Filename: %s", filename)
- if filename != "":
- self.pixmap().save(filename)
- else:
- logger.warning("The user got shown an empty screenshot window?")
-
- def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
- super().resizeEvent(a0)
- if self.pixmap() is not None:
- self.setPixmap(self.pix.scaled(self.size(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.FastTransformation))
-
- def setScale(self, scale):
- width, height = self.pix.size().width() * scale, self.pix.size().height() * scale
- self.resize(width, height)
diff --git a/NanoVNASaver/Settings.py b/NanoVNASaver/Settings.py
new file mode 100644
index 0000000..b58acc9
--- /dev/null
+++ b/NanoVNASaver/Settings.py
@@ -0,0 +1,154 @@
+# NanoVNASaver
+# A python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019. Rune B. Broberg
+#
+# 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 typing
+from typing import List, Tuple
+
+from PyQt5 import QtCore, QtGui
+from PyQt5.QtCore import QModelIndex
+
+logger = logging.getLogger(__name__)
+
+
+class BandsModel(QtCore.QAbstractTableModel):
+ bands: List[Tuple[str, int, int]] = []
+ enabled = False
+ color = QtGui.QColor(128, 128, 128, 48)
+
+ # These bands correspond broadly to the Danish Amateur Radio allocation
+ default_bands = ["2200 m;135700;137800",
+ "630 m;472000;479000",
+ "160 m;1800000;2000000",
+ "80 m;3500000;3800000",
+ "60 m;5250000;5450000",
+ "40 m;7000000;7200000",
+ "30 m;10100000;10150000",
+ "20 m;14000000;14350000",
+ "17 m;18068000;18168000",
+ "15 m;21000000;21450000",
+ "12 m;24890000;24990000",
+ "10 m;28000000;29700000",
+ "6 m;50000000;52000000",
+ "4 m;69887500;70512500",
+ "2 m;144000000;146000000",
+ "70 cm;432000000;438000000",
+ "23 cm;1240000000;1300000000",
+ "13 cm;2320000000;2450000000"]
+
+ def __init__(self):
+ super().__init__()
+ self.settings = QtCore.QSettings(QtCore.QSettings.IniFormat,
+ QtCore.QSettings.UserScope,
+ "NanoVNASaver", "Bands")
+ self.settings.setIniCodec("UTF-8")
+ self.enabled = self.settings.value("ShowBands", False, bool)
+
+ stored_bands: List[str] = self.settings.value("bands", self.default_bands)
+ if stored_bands:
+ for b in stored_bands:
+ (name, start, end) = b.split(";")
+ self.bands.append((name, int(start), int(end)))
+
+ def saveSettings(self):
+ stored_bands = []
+ for b in self.bands:
+ stored_bands.append(b[0] + ";" + str(b[1]) + ";" + str(b[2]))
+ self.settings.setValue("bands", stored_bands)
+ self.settings.sync()
+
+ def resetBands(self):
+ self.bands = []
+ for b in self.default_bands:
+ (name, start, end) = b.split(";")
+ self.bands.append((name, int(start), int(end)))
+ self.layoutChanged.emit()
+ self.saveSettings()
+
+ def columnCount(self, parent: QModelIndex = ...) -> int:
+ return 3
+
+ def rowCount(self, parent: QModelIndex = ...) -> int:
+ return len(self.bands)
+
+ def data(self, index: QModelIndex, role: int = ...) -> QtCore.QVariant:
+ if (role == QtCore.Qt.DisplayRole or
+ role == QtCore.Qt.ItemDataRole or role == QtCore.Qt.EditRole):
+ return QtCore.QVariant(self.bands[index.row()][index.column()])
+ elif role == QtCore.Qt.TextAlignmentRole:
+ if index.column() == 0:
+ return QtCore.QVariant(QtCore.Qt.AlignCenter)
+ else:
+ return QtCore.QVariant(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+ else:
+ return QtCore.QVariant()
+
+ def setData(self, index: QModelIndex, value: typing.Any, role: int = ...) -> bool:
+ if role == QtCore.Qt.EditRole and index.isValid():
+ t = self.bands[index.row()]
+ name = t[0]
+ start = t[1]
+ end = t[2]
+ if index.column() == 0:
+ name = value
+ elif index.column() == 1:
+ start = value
+ elif index.column() == 2:
+ end = value
+ self.bands[index.row()] = (name, start, end)
+ self.dataChanged.emit(index, index)
+ self.saveSettings()
+ return True
+ return False
+
+ def index(self, row: int, column: int, parent: QModelIndex = ...) -> QModelIndex:
+ return self.createIndex(row, column)
+
+ def addRow(self):
+ self.bands.append(("New", 0, 0))
+ self.dataChanged.emit(self.index(len(self.bands), 0), self.index(len(self.bands), 2))
+ self.layoutChanged.emit()
+
+ def removeRow(self, row: int, parent: QModelIndex = ...) -> bool:
+ self.bands.remove(self.bands[row])
+ self.layoutChanged.emit()
+ self.saveSettings()
+ return True
+
+ def headerData(self, section: int, orientation: QtCore.Qt.Orientation, role: int = ...):
+ if role == QtCore.Qt.DisplayRole and orientation == QtCore.Qt.Horizontal:
+ if section == 0:
+ return "Band"
+ if section == 1:
+ return "Start (Hz)"
+ if section == 2:
+ return "End (Hz)"
+ else:
+ return "Invalid"
+ else:
+ super().headerData(section, orientation, role)
+
+ def flags(self, index: QModelIndex) -> QtCore.Qt.ItemFlags:
+ if index.isValid():
+ return QtCore.Qt.ItemFlags(
+ QtCore.Qt.ItemIsEditable |
+ QtCore.Qt.ItemIsEnabled |
+ QtCore.Qt.ItemIsSelectable)
+ else:
+ super().flags(index)
+
+ def setColor(self, color):
+ self.color = color
diff --git a/NanoVNASaver/Windows.temp/About.py b/NanoVNASaver/Windows.temp/About.py
new file mode 100644
index 0000000..6f699e4
--- /dev/null
+++ b/NanoVNASaver/Windows.temp/About.py
@@ -0,0 +1,189 @@
+# NanoVNASaver
+# A python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019. Rune B. Broberg
+#
+# 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 json
+from time import strftime, localtime
+from urllib import request, error
+
+from PyQt5 import QtWidgets, QtCore
+
+from NanoVNASaver.Hardware import Version
+
+logger = logging.getLogger(__name__)
+
+class AboutWindow(QtWidgets.QWidget):
+ def __init__(self, app: QtWidgets.QWidget):
+ super().__init__()
+ self.app = app
+
+ self.setWindowTitle("About NanoVNASaver")
+ self.setWindowIcon(self.app.icon)
+ top_layout = QtWidgets.QHBoxLayout()
+ self.setLayout(top_layout)
+ QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
+
+ icon_layout = QtWidgets.QVBoxLayout()
+ top_layout.addLayout(icon_layout)
+ icon = QtWidgets.QLabel()
+ icon.setPixmap(self.app.icon.pixmap(128, 128))
+ icon_layout.addWidget(icon)
+ icon_layout.addStretch()
+
+ layout = QtWidgets.QVBoxLayout()
+ top_layout.addLayout(layout)
+
+ layout.addWidget(QtWidgets.QLabel(
+ f"NanoVNASaver version {self.app.version}"))
+ layout.addWidget(QtWidgets.QLabel(""))
+ layout.addWidget(QtWidgets.QLabel(
+ "\N{COPYRIGHT SIGN} Copyright 2019 Rune B. Broberg"))
+ layout.addWidget(QtWidgets.QLabel(
+ "This program comes with ABSOLUTELY NO WARRANTY"))
+ layout.addWidget(QtWidgets.QLabel(
+ "This program is licensed under the GNU General Public License version 3"))
+ layout.addWidget(QtWidgets.QLabel(""))
+ link_label = QtWidgets.QLabel(
+ "For further details, see: "
+ "https://mihtjel.github.io/nanovna-saver/")
+ link_label.setOpenExternalLinks(True)
+ layout.addWidget(link_label)
+ layout.addWidget(QtWidgets.QLabel(""))
+
+ self.versionLabel = QtWidgets.QLabel("NanoVNA Firmware Version: Not connected.")
+ layout.addWidget(self.versionLabel)
+
+ layout.addStretch()
+
+ btn_check_version = QtWidgets.QPushButton("Check for updates")
+ btn_check_version.clicked.connect(self.findUpdates)
+
+ self.updateLabel = QtWidgets.QLabel("Last checked: ")
+ self.updateCheckBox = QtWidgets.QCheckBox("Check for updates on startup")
+
+ self.updateCheckBox.toggled.connect(self.updateSettings)
+
+ check_for_updates = self.app.settings.value("CheckForUpdates", "Ask")
+ if check_for_updates == "Yes":
+ self.updateCheckBox.setChecked(True)
+ self.findUpdates(automatic=True)
+ elif check_for_updates == "No":
+ self.updateCheckBox.setChecked(False)
+ else:
+ logger.debug("Starting timer")
+ QtCore.QTimer.singleShot(2000, self.askAboutUpdates)
+
+ update_hbox = QtWidgets.QHBoxLayout()
+ update_hbox.addWidget(btn_check_version)
+ update_form = QtWidgets.QFormLayout()
+ update_hbox.addLayout(update_form)
+ update_hbox.addStretch()
+ update_form.addRow(self.updateLabel)
+ update_form.addRow(self.updateCheckBox)
+ layout.addLayout(update_hbox)
+
+ layout.addStretch()
+
+ btn_ok = QtWidgets.QPushButton("Ok")
+ btn_ok.clicked.connect(lambda: self.close()) # noqa
+ layout.addWidget(btn_ok)
+
+ def show(self):
+ super().show()
+ self.updateLabels()
+
+ def updateLabels(self):
+ if self.app.vna.isValid():
+ logger.debug("Valid VNA")
+ v: Version = self.app.vna.version
+ self.versionLabel.setText(
+ f"NanoVNA Firmware Version: {self.app.vna.name}"
+ f"{v.version_string}")
+
+ def updateSettings(self):
+ if self.updateCheckBox.isChecked():
+ self.app.settings.setValue("CheckForUpdates", "Yes")
+ else:
+ self.app.settings.setValue("CheckForUpdates", "No")
+
+ def askAboutUpdates(self):
+ logger.debug("Asking about automatic update checks")
+ selection = QtWidgets.QMessageBox.question(
+ self.app,
+ "Enable checking for updates?",
+ "Would you like NanoVNA-Saver to check for updates automatically?")
+ if selection == QtWidgets.QMessageBox.Yes:
+ self.updateCheckBox.setChecked(True)
+ self.app.settings.setValue("CheckForUpdates", "Yes")
+ self.findUpdates()
+ elif selection == QtWidgets.QMessageBox.No:
+ self.updateCheckBox.setChecked(False)
+ self.app.settings.setValue("CheckForUpdates", "No")
+ QtWidgets.QMessageBox.information(
+ self.app,
+ "Checking for updates disabled",
+ "You can check for updates using the \"About\" window.")
+ else:
+ self.app.settings.setValue("CheckForUpdates", "Ask")
+
+ def findUpdates(self, automatic=False):
+ update_url = "http://mihtjel.dk/nanovna-saver/latest.json"
+
+ try:
+ req = request.Request(update_url)
+ req.add_header('User-Agent', "NanoVNA-Saver/" + self.app.version)
+ updates = json.load(request.urlopen(req, timeout=3))
+ latest_version = Version(updates['version'])
+ latest_url = updates['url']
+ except error.HTTPError as e:
+ logger.exception("Checking for updates produced an HTTP exception: %s", e)
+ self.updateLabel.setText("Connection error.")
+ return
+ except json.JSONDecodeError as e:
+ logger.exception("Checking for updates provided an unparseable file: %s", e)
+ self.updateLabel.setText("Data error reading versions.")
+ return
+ except error.URLError as e:
+ logger.exception("Checking for updates produced a URL exception: %s", e)
+ self.updateLabel.setText("Connection error.")
+ return
+
+ logger.info("Latest version is %s", latest_version.version_string)
+ this_version = Version(self.app.version)
+ logger.info("This is %s", this_version)
+ if latest_version > this_version:
+ logger.info("New update available: %s!", latest_version)
+ if automatic:
+ QtWidgets.QMessageBox.information(
+ self,
+ "Updates available",
+ "There is a new update for NanoVNA-Saver available!\n" +
+ "Version " + latest_version.version_string + "\n\n" +
+ "Press \"About\" to find the update.")
+ else:
+ QtWidgets.QMessageBox.information(
+ self, "Updates available",
+ "There is a new update for NanoVNA-Saver available!")
+ self.updateLabel.setText(
+ f'New version available.')
+ self.updateLabel.setOpenExternalLinks(True)
+ else:
+ # Probably don't show a message box, just update the screen?
+ # Maybe consider showing it if not an automatic update.
+ #
+ self.updateLabel.setText(
+ f"Last checked: {strftime('%Y-%m-%d %H:%M:%S', localtime())}")
+ return
diff --git a/NanoVNASaver/Windows.temp/AnalysisWindow.py b/NanoVNASaver/Windows.temp/AnalysisWindow.py
new file mode 100644
index 0000000..fd5c842
--- /dev/null
+++ b/NanoVNASaver/Windows.temp/AnalysisWindow.py
@@ -0,0 +1,100 @@
+# NanoVNASaver
+# A python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019. Rune B. Broberg
+#
+# 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
+
+from PyQt5 import QtWidgets, QtCore
+
+from NanoVNASaver.Analysis import Analysis, LowPassAnalysis, HighPassAnalysis, \
+ BandPassAnalysis, BandStopAnalysis, VSWRAnalysis, \
+ SimplePeakSearchAnalysis
+
+logger = logging.getLogger(__name__)
+
+
+class AnalysisWindow(QtWidgets.QWidget):
+ analyses = []
+ analysis: Analysis = None
+
+ def __init__(self, app: QtWidgets.QWidget):
+ super().__init__()
+
+ self.app = app
+ self.setWindowTitle("Sweep analysis")
+ self.setWindowIcon(self.app.icon)
+
+ QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
+
+ layout = QtWidgets.QVBoxLayout()
+ self.setLayout(layout)
+
+ select_analysis_box = QtWidgets.QGroupBox("Select analysis")
+ select_analysis_layout = QtWidgets.QFormLayout(select_analysis_box)
+ self.analysis_list = QtWidgets.QComboBox()
+ self.analysis_list.addItem("Low-pass filter", LowPassAnalysis(self.app))
+ self.analysis_list.addItem("Band-pass filter", BandPassAnalysis(self.app))
+ self.analysis_list.addItem("High-pass filter", HighPassAnalysis(self.app))
+ self.analysis_list.addItem("Band-stop filter", BandStopAnalysis(self.app))
+ # self.analysis_list.addItem("Peak search", PeakSearchAnalysis(self.app))
+ self.analysis_list.addItem("Peak search", SimplePeakSearchAnalysis(self.app))
+ self.analysis_list.addItem("VSWR analysis", VSWRAnalysis(self.app))
+ select_analysis_layout.addRow("Analysis type", self.analysis_list)
+ self.analysis_list.currentIndexChanged.connect(self.updateSelection)
+
+ btn_run_analysis = QtWidgets.QPushButton("Run analysis")
+ btn_run_analysis.clicked.connect(self.runAnalysis)
+ select_analysis_layout.addRow(btn_run_analysis)
+
+ self.checkbox_run_automatically = QtWidgets.QCheckBox("Run automatically")
+ self.checkbox_run_automatically.stateChanged.connect(self.toggleAutomaticRun)
+ select_analysis_layout.addRow(self.checkbox_run_automatically)
+
+ analysis_box = QtWidgets.QGroupBox("Analysis")
+ analysis_box.setSizePolicy(
+ QtWidgets.QSizePolicy.MinimumExpanding,
+ QtWidgets.QSizePolicy.MinimumExpanding)
+
+ self.analysis_layout = QtWidgets.QVBoxLayout(analysis_box)
+ self.analysis_layout.setContentsMargins(0, 0, 0, 0)
+
+ layout.addWidget(select_analysis_box)
+ layout.addWidget(analysis_box)
+
+ self.updateSelection()
+
+ def runAnalysis(self):
+ if self.analysis is not None:
+ self.analysis.runAnalysis()
+
+ def updateSelection(self):
+ self.analysis = self.analysis_list.currentData()
+ old_item = self.analysis_layout.itemAt(0)
+ if old_item is not None:
+ old_widget = self.analysis_layout.itemAt(0).widget()
+ self.analysis_layout.replaceWidget(old_widget, self.analysis.widget())
+ old_widget.hide()
+ else:
+ self.analysis_layout.addWidget(self.analysis.widget())
+ self.analysis.widget().show()
+ self.update()
+
+ def toggleAutomaticRun(self, state: QtCore.Qt.CheckState):
+ if state == QtCore.Qt.Checked:
+ self.analysis_list.setDisabled(True)
+ self.app.dataAvailable.connect(self.runAnalysis)
+ else:
+ self.analysis_list.setDisabled(False)
+ self.app.dataAvailable.disconnect(self.runAnalysis)
diff --git a/NanoVNASaver/Windows.temp/Bands.py b/NanoVNASaver/Windows.temp/Bands.py
new file mode 100644
index 0000000..5a880a6
--- /dev/null
+++ b/NanoVNASaver/Windows.temp/Bands.py
@@ -0,0 +1,69 @@
+# NanoVNASaver
+# A python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019. Rune B. Broberg
+#
+# 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
+
+from PyQt5 import QtWidgets, QtCore
+
+logger = logging.getLogger(__name__)
+
+
+class BandsWindow(QtWidgets.QWidget):
+ def __init__(self, app: QtWidgets.QWidget):
+ super().__init__()
+
+ self.app = app
+ self.setWindowTitle("Manage bands")
+ self.setWindowIcon(self.app.icon)
+
+ QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
+
+ layout = QtWidgets.QVBoxLayout()
+ self.setLayout(layout)
+ self.setMinimumSize(500, 300)
+
+ self.bands_table = QtWidgets.QTableView()
+ self.bands_table.setModel(self.app.bands)
+ self.bands_table.horizontalHeader().setStretchLastSection(True)
+
+ layout.addWidget(self.bands_table)
+
+ btn_add_row = QtWidgets.QPushButton("Add row")
+ btn_delete_row = QtWidgets.QPushButton("Delete row")
+ btn_reset_bands = QtWidgets.QPushButton("Reset bands")
+ btn_layout = QtWidgets.QHBoxLayout()
+ btn_layout.addWidget(btn_add_row)
+ btn_layout.addWidget(btn_delete_row)
+ btn_layout.addWidget(btn_reset_bands)
+ layout.addLayout(btn_layout)
+
+ btn_add_row.clicked.connect(self.app.bands.addRow)
+ btn_delete_row.clicked.connect(self.deleteRows)
+ btn_reset_bands.clicked.connect(self.resetBands)
+
+ def deleteRows(self):
+ rows = self.bands_table.selectedIndexes()
+ for row in rows:
+ self.app.bands.removeRow(row.row())
+
+ def resetBands(self):
+ confirm = QtWidgets.QMessageBox(
+ QtWidgets.QMessageBox.Warning,
+ "Confirm reset",
+ "Are you sure you want to reset the bands to default?",
+ QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Cancel).exec()
+ if confirm == QtWidgets.QMessageBox.Yes:
+ self.app.bands.resetBands()
diff --git a/NanoVNASaver/Windows.temp/DeviceSettings.py b/NanoVNASaver/Windows.temp/DeviceSettings.py
new file mode 100644
index 0000000..94a1750
--- /dev/null
+++ b/NanoVNASaver/Windows.temp/DeviceSettings.py
@@ -0,0 +1,115 @@
+# NanoVNASaver
+# A python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019. Rune B. Broberg
+#
+# 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
+
+from PyQt5 import QtWidgets, QtCore
+
+from NanoVNASaver.Windows.Screenshot import ScreenshotWindow
+
+logger = logging.getLogger(__name__)
+
+class DeviceSettingsWindow(QtWidgets.QWidget):
+ def __init__(self, app: QtWidgets.QWidget):
+ super().__init__()
+
+ self.app = app
+ self.setWindowTitle("Device settings")
+ self.setWindowIcon(self.app.icon)
+
+ QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
+
+ layout = QtWidgets.QVBoxLayout()
+ self.setLayout(layout)
+
+ status_box = QtWidgets.QGroupBox("Status")
+ status_layout = QtWidgets.QFormLayout(status_box)
+ self.statusLabel = QtWidgets.QLabel("Not connected.")
+ status_layout.addRow("Status:", self.statusLabel)
+
+ self.calibrationStatusLabel = QtWidgets.QLabel("Not connected.")
+ status_layout.addRow("Calibration:", self.calibrationStatusLabel)
+
+ status_layout.addRow(QtWidgets.QLabel("Features:"))
+ self.featureList = QtWidgets.QListWidget()
+ status_layout.addRow(self.featureList)
+
+ settings_box = QtWidgets.QGroupBox("Settings")
+ settings_layout = QtWidgets.QFormLayout(settings_box)
+
+ self.chkValidateInputData = QtWidgets.QCheckBox("Validate received data")
+ validate_input = self.app.settings.value("SerialInputValidation", True, bool)
+ self.chkValidateInputData.setChecked(validate_input)
+ self.chkValidateInputData.stateChanged.connect(self.updateValidation)
+ settings_layout.addRow("Validation", self.chkValidateInputData)
+
+ control_layout = QtWidgets.QHBoxLayout()
+ self.btnRefresh = QtWidgets.QPushButton("Refresh")
+ self.btnRefresh.clicked.connect(self.updateFields)
+ control_layout.addWidget(self.btnRefresh)
+
+ self.screenshotWindow = ScreenshotWindow()
+ self.btnCaptureScreenshot = QtWidgets.QPushButton("Screenshot")
+ self.btnCaptureScreenshot.clicked.connect(self.captureScreenshot)
+ control_layout.addWidget(self.btnCaptureScreenshot)
+
+ layout.addWidget(status_box)
+ layout.addWidget(settings_box)
+ layout.addLayout(control_layout)
+
+ def show(self):
+ super().show()
+ self.updateFields()
+
+ def updateFields(self):
+ if self.app.vna.isValid():
+ self.statusLabel.setText("Connected to " + self.app.vna.name + ".")
+ if self.app.worker.running:
+ self.calibrationStatusLabel.setText("(Sweep running)")
+ else:
+ self.calibrationStatusLabel.setText(self.app.vna.getCalibration())
+
+ self.featureList.clear()
+ self.featureList.addItem(self.app.vna.name + " v" + str(self.app.vna.version))
+ features = self.app.vna.getFeatures()
+ for item in features:
+ self.featureList.addItem(item)
+
+ if "Screenshots" in features:
+ self.btnCaptureScreenshot.setDisabled(False)
+ else:
+ self.btnCaptureScreenshot.setDisabled(True)
+ else:
+ self.statusLabel.setText("Not connected.")
+ self.calibrationStatusLabel.setText("Not connected.")
+ self.featureList.clear()
+ self.featureList.addItem("Not connected.")
+ self.btnCaptureScreenshot.setDisabled(True)
+
+ def updateValidation(self, validate_data: bool):
+ self.app.vna.validateInput = validate_data
+ self.app.settings.setValue("SerialInputValidation", validate_data)
+
+ def captureScreenshot(self):
+ if not self.app.worker.running:
+ pixmap = self.app.vna.getScreenshot()
+ self.screenshotWindow.setScreenshot(pixmap)
+ self.screenshotWindow.show()
+ else:
+ # TODO: Tell the user no screenshots while sweep is running?
+ # TODO: Consider having a list of widgets that want to be
+ # disabled when a sweep is running?
+ pass
diff --git a/NanoVNASaver/Windows.temp/DisplaySettings.py b/NanoVNASaver/Windows.temp/DisplaySettings.py
new file mode 100644
index 0000000..6f489d0
--- /dev/null
+++ b/NanoVNASaver/Windows.temp/DisplaySettings.py
@@ -0,0 +1,741 @@
+# NanoVNASaver - a python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019. Rune B. Broberg
+#
+# 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
+from typing import List
+
+from PyQt5 import QtWidgets, QtCore, QtGui
+
+from NanoVNASaver.Windows.Bands import BandsWindow
+from NanoVNASaver.Windows.MarkerSettings import MarkerSettingsWindow
+from NanoVNASaver.Marker import Marker
+logger = logging.getLogger(__name__)
+
+class DisplaySettingsWindow(QtWidgets.QWidget):
+ def __init__(self, app: QtWidgets.QWidget):
+ super().__init__()
+
+ self.app = app
+ self.setWindowTitle("Display settings")
+ self.setWindowIcon(self.app.icon)
+
+ self.marker_window = MarkerSettingsWindow(self.app)
+
+ QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
+
+ layout = QtWidgets.QHBoxLayout()
+ self.setLayout(layout)
+
+ left_layout = QtWidgets.QVBoxLayout()
+ layout.addLayout(left_layout)
+
+ display_options_box = QtWidgets.QGroupBox("Options")
+ display_options_layout = QtWidgets.QFormLayout(display_options_box)
+
+ self.returnloss_group = QtWidgets.QButtonGroup()
+ self.returnloss_is_negative = QtWidgets.QRadioButton("Negative")
+ self.returnloss_is_positive = QtWidgets.QRadioButton("Positive")
+ self.returnloss_group.addButton(self.returnloss_is_positive)
+ self.returnloss_group.addButton(self.returnloss_is_negative)
+
+ display_options_layout.addRow("Return loss is:", self.returnloss_is_negative)
+ display_options_layout.addRow("", self.returnloss_is_positive)
+
+ if self.app.settings.value("ReturnLossPositive", False, bool):
+ self.returnloss_is_positive.setChecked(True)
+ else:
+ self.returnloss_is_negative.setChecked(True)
+
+ self.returnloss_is_positive.toggled.connect(self.changeReturnLoss)
+ self.changeReturnLoss()
+
+ self.show_lines_option = QtWidgets.QCheckBox("Show lines")
+ show_lines_label = QtWidgets.QLabel("Displays a thin line between data points")
+ self.show_lines_option.stateChanged.connect(self.changeShowLines)
+ display_options_layout.addRow(self.show_lines_option, show_lines_label)
+
+ self.dark_mode_option = QtWidgets.QCheckBox("Dark mode")
+ dark_mode_label = QtWidgets.QLabel("Black background with white text")
+ self.dark_mode_option.stateChanged.connect(self.changeDarkMode)
+ display_options_layout.addRow(self.dark_mode_option, dark_mode_label)
+
+ self.btnColorPicker = QtWidgets.QPushButton("█")
+ self.btnColorPicker.setFixedWidth(20)
+ self.sweepColor = self.app.settings.value("SweepColor", defaultValue=QtGui.QColor(160, 140, 20, 128),
+ type=QtGui.QColor)
+ self.setSweepColor(self.sweepColor)
+ self.btnColorPicker.clicked.connect(lambda: self.setSweepColor(
+ QtWidgets.QColorDialog.getColor(self.sweepColor, options=QtWidgets.QColorDialog.ShowAlphaChannel)))
+
+ display_options_layout.addRow("Sweep color", self.btnColorPicker)
+
+ self.btnSecondaryColorPicker = QtWidgets.QPushButton("█")
+ self.btnSecondaryColorPicker.setFixedWidth(20)
+ self.secondarySweepColor = self.app.settings.value("SecondarySweepColor",
+ defaultValue=QtGui.QColor(20, 160, 140, 128),
+ type=QtGui.QColor)
+ self.setSecondarySweepColor(self.secondarySweepColor)
+ self.btnSecondaryColorPicker.clicked.connect(lambda: self.setSecondarySweepColor(
+ QtWidgets.QColorDialog.getColor(self.secondarySweepColor,
+ options=QtWidgets.QColorDialog.ShowAlphaChannel)))
+
+ display_options_layout.addRow("Second sweep color", self.btnSecondaryColorPicker)
+
+ self.btnReferenceColorPicker = QtWidgets.QPushButton("█")
+ self.btnReferenceColorPicker.setFixedWidth(20)
+ self.referenceColor = self.app.settings.value("ReferenceColor", defaultValue=QtGui.QColor(0, 0, 255, 48),
+ type=QtGui.QColor)
+ self.setReferenceColor(self.referenceColor)
+ self.btnReferenceColorPicker.clicked.connect(lambda: self.setReferenceColor(
+ QtWidgets.QColorDialog.getColor(self.referenceColor, options=QtWidgets.QColorDialog.ShowAlphaChannel)))
+
+ display_options_layout.addRow("Reference color", self.btnReferenceColorPicker)
+
+ self.btnSecondaryReferenceColorPicker = QtWidgets.QPushButton("█")
+ self.btnSecondaryReferenceColorPicker.setFixedWidth(20)
+ self.secondaryReferenceColor = self.app.settings.value("SecondaryReferenceColor",
+ defaultValue=QtGui.QColor(0, 0, 255, 48),
+ type=QtGui.QColor)
+ self.setSecondaryReferenceColor(self.secondaryReferenceColor)
+ self.btnSecondaryReferenceColorPicker.clicked.connect(lambda: self.setSecondaryReferenceColor(
+ QtWidgets.QColorDialog.getColor(self.secondaryReferenceColor,
+ options=QtWidgets.QColorDialog.ShowAlphaChannel)))
+
+ display_options_layout.addRow("Second reference color", self.btnSecondaryReferenceColorPicker)
+
+ self.pointSizeInput = QtWidgets.QSpinBox()
+ pointsize = self.app.settings.value("PointSize", 2, int)
+ self.pointSizeInput.setValue(pointsize)
+ self.changePointSize(pointsize)
+ self.pointSizeInput.setMinimum(1)
+ self.pointSizeInput.setMaximum(10)
+ self.pointSizeInput.setSuffix(" px")
+ self.pointSizeInput.setAlignment(QtCore.Qt.AlignRight)
+ self.pointSizeInput.valueChanged.connect(self.changePointSize)
+ display_options_layout.addRow("Point size", self.pointSizeInput)
+
+ self.lineThicknessInput = QtWidgets.QSpinBox()
+ linethickness = self.app.settings.value("LineThickness", 1, int)
+ self.lineThicknessInput.setValue(linethickness)
+ self.changeLineThickness(linethickness)
+ self.lineThicknessInput.setMinimum(1)
+ self.lineThicknessInput.setMaximum(10)
+ self.lineThicknessInput.setSuffix(" px")
+ self.lineThicknessInput.setAlignment(QtCore.Qt.AlignRight)
+ self.lineThicknessInput.valueChanged.connect(self.changeLineThickness)
+ display_options_layout.addRow("Line thickness", self.lineThicknessInput)
+
+ self.markerSizeInput = QtWidgets.QSpinBox()
+ markersize = self.app.settings.value("MarkerSize", 6, int)
+ self.markerSizeInput.setValue(markersize)
+ self.changeMarkerSize(markersize)
+ self.markerSizeInput.setMinimum(4)
+ self.markerSizeInput.setMaximum(20)
+ self.markerSizeInput.setSingleStep(2)
+ self.markerSizeInput.setSuffix(" px")
+ self.markerSizeInput.setAlignment(QtCore.Qt.AlignRight)
+ self.markerSizeInput.valueChanged.connect(self.changeMarkerSize)
+ self.markerSizeInput.editingFinished.connect(self.validateMarkerSize)
+ display_options_layout.addRow("Marker size", self.markerSizeInput)
+
+ self.show_marker_number_option = QtWidgets.QCheckBox("Show marker numbers")
+ show_marker_number_label = QtWidgets.QLabel("Displays the marker number next to the marker")
+ self.show_marker_number_option.stateChanged.connect(self.changeShowMarkerNumber)
+ display_options_layout.addRow(self.show_marker_number_option, show_marker_number_label)
+
+ self.filled_marker_option = QtWidgets.QCheckBox("Filled markers")
+ filled_marker_label = QtWidgets.QLabel("Shows the marker as a filled triangle")
+ self.filled_marker_option.stateChanged.connect(self.changeFilledMarkers)
+ display_options_layout.addRow(self.filled_marker_option, filled_marker_label)
+
+ self.marker_tip_group = QtWidgets.QButtonGroup()
+ self.marker_at_center = QtWidgets.QRadioButton("At the center of the marker")
+ self.marker_at_tip = QtWidgets.QRadioButton("At the tip of the marker")
+ self.marker_tip_group.addButton(self.marker_at_center)
+ self.marker_tip_group.addButton(self.marker_at_tip)
+
+ display_options_layout.addRow("Data point is:", self.marker_at_center)
+ display_options_layout.addRow("", self.marker_at_tip)
+
+ if self.app.settings.value("MarkerAtTip", False, bool):
+ self.marker_at_tip.setChecked(True)
+ else:
+ self.marker_at_center.setChecked(True)
+
+ self.marker_at_tip.toggled.connect(self.changeMarkerAtTip)
+ self.changeMarkerAtTip()
+
+ color_options_box = QtWidgets.QGroupBox("Chart colors")
+ color_options_layout = QtWidgets.QFormLayout(color_options_box)
+
+ self.use_custom_colors = QtWidgets.QCheckBox("Use custom chart colors")
+ self.use_custom_colors.stateChanged.connect(self.changeCustomColors)
+ color_options_layout.addRow(self.use_custom_colors)
+
+ self.btn_background_picker = QtWidgets.QPushButton("█")
+ self.btn_background_picker.setFixedWidth(20)
+ self.btn_background_picker.clicked.connect(
+ lambda: self.setColor(
+ "background",
+ QtWidgets.QColorDialog.getColor(
+ self.backgroundColor,
+ options=QtWidgets.QColorDialog.ShowAlphaChannel)))
+
+ color_options_layout.addRow(
+ "Chart background", self.btn_background_picker)
+
+ self.btn_foreground_picker = QtWidgets.QPushButton("█")
+ self.btn_foreground_picker.setFixedWidth(20)
+ self.btn_foreground_picker.clicked.connect(
+ lambda: self.setColor(
+ "foreground",
+ QtWidgets.QColorDialog.getColor(
+ self.foregroundColor,
+ options=QtWidgets.QColorDialog.ShowAlphaChannel)))
+
+ color_options_layout.addRow("Chart foreground", self.btn_foreground_picker)
+
+ self.btn_text_picker = QtWidgets.QPushButton("█")
+ self.btn_text_picker.setFixedWidth(20)
+ self.btn_text_picker.clicked.connect(
+ lambda: self.setColor(
+ "text",
+ QtWidgets.QColorDialog.getColor(
+ self.textColor,
+ options=QtWidgets.QColorDialog.ShowAlphaChannel)))
+
+ color_options_layout.addRow("Chart text", self.btn_text_picker)
+
+ right_layout = QtWidgets.QVBoxLayout()
+ layout.addLayout(right_layout)
+
+ font_options_box = QtWidgets.QGroupBox("Font")
+ font_options_layout = QtWidgets.QFormLayout(font_options_box)
+ self.font_dropdown = QtWidgets.QComboBox()
+ self.font_dropdown.addItems(["7", "8", "9", "10", "11", "12"])
+ font_size = self.app.settings.value("FontSize",
+ defaultValue="8",
+ type=str)
+ self.font_dropdown.setCurrentText(font_size)
+ self.changeFont()
+
+ self.font_dropdown.currentTextChanged.connect(self.changeFont)
+ font_options_layout.addRow("Font size", self.font_dropdown)
+
+ bands_box = QtWidgets.QGroupBox("Bands")
+ bands_layout = QtWidgets.QFormLayout(bands_box)
+
+ self.show_bands = QtWidgets.QCheckBox("Show bands")
+ self.show_bands.setChecked(self.app.bands.enabled)
+ self.show_bands.stateChanged.connect(lambda: self.setShowBands(self.show_bands.isChecked()))
+ bands_layout.addRow(self.show_bands)
+
+ self.btn_bands_picker = QtWidgets.QPushButton("█")
+ self.btn_bands_picker.setFixedWidth(20)
+ self.btn_bands_picker.clicked.connect(
+ lambda: self.setColor(
+ "bands",
+ QtWidgets.QColorDialog.getColor(
+ self.bandsColor,
+ options=QtWidgets.QColorDialog.ShowAlphaChannel)))
+
+ bands_layout.addRow("Chart bands", self.btn_bands_picker)
+
+ self.btn_manage_bands = QtWidgets.QPushButton("Manage bands")
+
+ self.bandsWindow = BandsWindow(self.app)
+ self.btn_manage_bands.clicked.connect(self.displayBandsWindow)
+
+ bands_layout.addRow(self.btn_manage_bands)
+
+ vswr_marker_box = QtWidgets.QGroupBox("VSWR Markers")
+ vswr_marker_layout = QtWidgets.QFormLayout(vswr_marker_box)
+
+ self.vswrMarkers: List[float] = self.app.settings.value("VSWRMarkers", [], float)
+
+ if isinstance(self.vswrMarkers, float):
+ if self.vswrMarkers == 0:
+ self.vswrMarkers = []
+ else:
+ # Single values from the .ini become floats rather than lists. Convert them.
+ self.vswrMarkers = [self.vswrMarkers]
+
+ self.btn_vswr_picker = QtWidgets.QPushButton("█")
+ self.btn_vswr_picker.setFixedWidth(20)
+ self.btn_vswr_picker.clicked.connect(
+ lambda: self.setColor(
+ "vswr",
+ QtWidgets.QColorDialog.getColor(
+ self.vswrColor,
+ options=QtWidgets.QColorDialog.ShowAlphaChannel)))
+
+ vswr_marker_layout.addRow("VSWR Markers", self.btn_vswr_picker)
+
+ self.vswr_marker_dropdown = QtWidgets.QComboBox()
+ vswr_marker_layout.addRow(self.vswr_marker_dropdown)
+
+ if len(self.vswrMarkers) == 0:
+ self.vswr_marker_dropdown.addItem("None")
+ else:
+ for m in self.vswrMarkers:
+ self.vswr_marker_dropdown.addItem(str(m))
+ for c in self.app.s11charts:
+ c.addSWRMarker(m)
+
+ self.vswr_marker_dropdown.setCurrentIndex(0)
+ btn_add_vswr_marker = QtWidgets.QPushButton("Add ...")
+ btn_remove_vswr_marker = QtWidgets.QPushButton("Remove")
+ vswr_marker_btn_layout = QtWidgets.QHBoxLayout()
+ vswr_marker_btn_layout.addWidget(btn_add_vswr_marker)
+ vswr_marker_btn_layout.addWidget(btn_remove_vswr_marker)
+ vswr_marker_layout.addRow(vswr_marker_btn_layout)
+
+ btn_add_vswr_marker.clicked.connect(self.addVSWRMarker)
+ btn_remove_vswr_marker.clicked.connect(self.removeVSWRMarker)
+
+ markers_box = QtWidgets.QGroupBox("Markers")
+ markers_layout = QtWidgets.QFormLayout(markers_box)
+
+ btn_add_marker = QtWidgets.QPushButton("Add")
+ btn_add_marker.clicked.connect(self.addMarker)
+ self.btn_remove_marker = QtWidgets.QPushButton("Remove")
+ self.btn_remove_marker.clicked.connect(self.removeMarker)
+ btn_marker_settings = QtWidgets.QPushButton("Settings ...")
+ btn_marker_settings.clicked.connect(self.displayMarkerWindow)
+
+ marker_btn_layout = QtWidgets.QHBoxLayout()
+ marker_btn_layout.addWidget(btn_add_marker)
+ marker_btn_layout.addWidget(self.btn_remove_marker)
+ marker_btn_layout.addWidget(btn_marker_settings)
+
+ markers_layout.addRow(marker_btn_layout)
+
+ charts_box = QtWidgets.QGroupBox("Displayed charts")
+ charts_layout = QtWidgets.QGridLayout(charts_box)
+
+ # selections = ["S11 Smith chart",
+ # "S11 LogMag",
+ # "S11 VSWR",
+ # "S11 Phase",
+ # "S21 Smith chart",
+ # "S21 LogMag",
+ # "S21 Phase",
+ # "None"]
+
+ selections = []
+
+ for c in self.app.selectable_charts:
+ selections.append(c.name)
+
+ selections.append("None")
+ chart00_selection = QtWidgets.QComboBox()
+ chart00_selection.addItems(selections)
+ chart00 = self.app.settings.value("Chart00", "S11 Smith Chart")
+ if chart00_selection.findText(chart00) > -1:
+ chart00_selection.setCurrentText(chart00)
+ else:
+ chart00_selection.setCurrentText("S11 Smith Chart")
+ chart00_selection.currentTextChanged.connect(lambda: self.changeChart(0, 0, chart00_selection.currentText()))
+ charts_layout.addWidget(chart00_selection, 0, 0)
+
+ chart01_selection = QtWidgets.QComboBox()
+ chart01_selection.addItems(selections)
+ chart01 = self.app.settings.value("Chart01", "S11 Return Loss")
+ if chart01_selection.findText(chart01) > -1:
+ chart01_selection.setCurrentText(chart01)
+ else:
+ chart01_selection.setCurrentText("S11 Return Loss")
+ chart01_selection.currentTextChanged.connect(lambda: self.changeChart(0, 1, chart01_selection.currentText()))
+ charts_layout.addWidget(chart01_selection, 0, 1)
+
+ chart02_selection = QtWidgets.QComboBox()
+ chart02_selection.addItems(selections)
+ chart02 = self.app.settings.value("Chart02", "None")
+ if chart02_selection.findText(chart02) > -1:
+ chart02_selection.setCurrentText(chart02)
+ else:
+ chart02_selection.setCurrentText("None")
+ chart02_selection.currentTextChanged.connect(lambda: self.changeChart(0, 2, chart02_selection.currentText()))
+ charts_layout.addWidget(chart02_selection, 0, 2)
+
+ chart10_selection = QtWidgets.QComboBox()
+ chart10_selection.addItems(selections)
+ chart10 = self.app.settings.value("Chart10", "S21 Polar Plot")
+ if chart10_selection.findText(chart10) > -1:
+ chart10_selection.setCurrentText(chart10)
+ else:
+ chart10_selection.setCurrentText("S21 Polar Plot")
+ chart10_selection.currentTextChanged.connect(lambda: self.changeChart(1, 0, chart10_selection.currentText()))
+ charts_layout.addWidget(chart10_selection, 1, 0)
+
+ chart11_selection = QtWidgets.QComboBox()
+ chart11_selection.addItems(selections)
+ chart11 = self.app.settings.value("Chart11", "S21 Gain")
+ if chart11_selection.findText(chart11) > -1:
+ chart11_selection.setCurrentText(chart11)
+ else:
+ chart11_selection.setCurrentText("S21 Gain")
+ chart11_selection.currentTextChanged.connect(lambda: self.changeChart(1, 1, chart11_selection.currentText()))
+ charts_layout.addWidget(chart11_selection, 1, 1)
+
+ chart12_selection = QtWidgets.QComboBox()
+ chart12_selection.addItems(selections)
+ chart12 = self.app.settings.value("Chart12", "None")
+ if chart12_selection.findText(chart12) > -1:
+ chart12_selection.setCurrentText(chart12)
+ else:
+ chart12_selection.setCurrentText("None")
+ chart12_selection.currentTextChanged.connect(lambda: self.changeChart(1, 2, chart12_selection.currentText()))
+ charts_layout.addWidget(chart12_selection, 1, 2)
+
+ self.changeChart(0, 0, chart00_selection.currentText())
+ self.changeChart(0, 1, chart01_selection.currentText())
+ self.changeChart(0, 2, chart02_selection.currentText())
+ self.changeChart(1, 0, chart10_selection.currentText())
+ self.changeChart(1, 1, chart11_selection.currentText())
+ self.changeChart(1, 2, chart12_selection.currentText())
+
+ self.backgroundColor = self.app.settings.value("BackgroundColor", defaultValue=QtGui.QColor("white"),
+ type=QtGui.QColor)
+ self.foregroundColor = self.app.settings.value("ForegroundColor", defaultValue=QtGui.QColor("lightgray"),
+ type=QtGui.QColor)
+ self.textColor = self.app.settings.value("TextColor", defaultValue=QtGui.QColor("black"),
+ type=QtGui.QColor)
+ self.bandsColor = self.app.settings.value("BandsColor", defaultValue=QtGui.QColor(128, 128, 128, 48),
+ type=QtGui.QColor)
+ self.app.bands.color = self.bandsColor
+ self.vswrColor = self.app.settings.value("VSWRColor", defaultValue=QtGui.QColor(192, 0, 0, 128),
+ type=QtGui.QColor)
+
+ self.dark_mode_option.setChecked(self.app.settings.value("DarkMode", False, bool))
+ self.show_lines_option.setChecked(self.app.settings.value("ShowLines", False, bool))
+ self.show_marker_number_option.setChecked(self.app.settings.value("ShowMarkerNumbers", False, bool))
+ self.filled_marker_option.setChecked(self.app.settings.value("FilledMarkers", False, bool))
+
+ if self.app.settings.value("UseCustomColors", defaultValue=False, type=bool):
+ self.dark_mode_option.setDisabled(True)
+ self.dark_mode_option.setChecked(False)
+ self.use_custom_colors.setChecked(True)
+ else:
+ self.btn_background_picker.setDisabled(True)
+ self.btn_foreground_picker.setDisabled(True)
+ self.btn_text_picker.setDisabled(True)
+
+ self.changeCustomColors() # Update all the colours of all the charts
+
+ p = self.btn_background_picker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, self.backgroundColor)
+ self.btn_background_picker.setPalette(p)
+
+ p = self.btn_foreground_picker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, self.foregroundColor)
+ self.btn_foreground_picker.setPalette(p)
+
+ p = self.btn_text_picker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, self.textColor)
+ self.btn_text_picker.setPalette(p)
+
+ p = self.btn_bands_picker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, self.bandsColor)
+ self.btn_bands_picker.setPalette(p)
+
+ p = self.btn_vswr_picker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, self.vswrColor)
+ self.btn_vswr_picker.setPalette(p)
+
+ left_layout.addWidget(display_options_box)
+ left_layout.addWidget(charts_box)
+ left_layout.addWidget(markers_box)
+ left_layout.addStretch(1)
+
+ right_layout.addWidget(color_options_box)
+ right_layout.addWidget(font_options_box)
+ right_layout.addWidget(bands_box)
+ right_layout.addWidget(vswr_marker_box)
+ right_layout.addStretch(1)
+
+ def changeChart(self, x, y, chart):
+ found = None
+ for c in self.app.selectable_charts:
+ if c.name == chart:
+ found = c
+
+ self.app.settings.setValue("Chart" + str(x) + str(y), chart)
+
+ old_widget = self.app.charts_layout.itemAtPosition(x, y)
+ if old_widget is not None:
+ w = old_widget.widget()
+ self.app.charts_layout.removeWidget(w)
+ w.hide()
+ if found is not None:
+ if self.app.charts_layout.indexOf(found) > -1:
+ logger.debug("%s is already shown, duplicating.", found.name)
+ found = self.app.copyChart(found)
+
+ self.app.charts_layout.addWidget(found, x, y)
+ if found.isHidden():
+ found.show()
+
+ def changeReturnLoss(self):
+ state = self.returnloss_is_positive.isChecked()
+ self.app.settings.setValue("ReturnLossPositive", state)
+
+ for m in self.app.markers:
+ m.returnloss_is_positive = state
+ m.updateLabels(self.app.data, self.app.data21)
+ self.marker_window.exampleMarker.returnloss_is_positive = state
+ self.marker_window.updateMarker()
+ self.app.s11LogMag.isInverted = state
+ self.app.s11LogMag.update()
+
+ def changeShowLines(self):
+ state = self.show_lines_option.isChecked()
+ self.app.settings.setValue("ShowLines", state)
+ for c in self.app.subscribing_charts:
+ c.setDrawLines(state)
+
+ def changeShowMarkerNumber(self):
+ state = self.show_marker_number_option.isChecked()
+ self.app.settings.setValue("ShowMarkerNumbers", state)
+ for c in self.app.subscribing_charts:
+ c.setDrawMarkerNumbers(state)
+
+ def changeFilledMarkers(self):
+ state = self.filled_marker_option.isChecked()
+ self.app.settings.setValue("FilledMarkers", state)
+ for c in self.app.subscribing_charts:
+ c.setFilledMarkers(state)
+
+ def changeMarkerAtTip(self):
+ state = self.marker_at_tip.isChecked()
+ self.app.settings.setValue("MarkerAtTip", state)
+ for c in self.app.subscribing_charts:
+ c.setMarkerAtTip(state)
+
+ def changePointSize(self, size: int):
+ self.app.settings.setValue("PointSize", size)
+ for c in self.app.subscribing_charts:
+ c.setPointSize(size)
+
+ def changeLineThickness(self, size: int):
+ self.app.settings.setValue("LineThickness", size)
+ for c in self.app.subscribing_charts:
+ c.setLineThickness(size)
+
+ def changeMarkerSize(self, size: int):
+ if size % 2 == 0:
+ self.app.settings.setValue("MarkerSize", size)
+ for c in self.app.subscribing_charts:
+ c.setMarkerSize(int(size / 2))
+
+ def validateMarkerSize(self):
+ size = self.markerSizeInput.value()
+ if size % 2 != 0:
+ self.markerSizeInput.setValue(size + 1)
+
+ def changeDarkMode(self):
+ state = self.dark_mode_option.isChecked()
+ self.app.settings.setValue("DarkMode", state)
+ if state:
+ for c in self.app.subscribing_charts:
+ c.setBackgroundColor(QtGui.QColor(QtCore.Qt.black))
+ c.setForegroundColor(QtGui.QColor(QtCore.Qt.lightGray))
+ c.setTextColor(QtGui.QColor(QtCore.Qt.white))
+ c.setSWRColor(self.vswrColor)
+ else:
+ for c in self.app.subscribing_charts:
+ c.setBackgroundColor(QtGui.QColor(QtCore.Qt.white))
+ c.setForegroundColor(QtGui.QColor(QtCore.Qt.lightGray))
+ c.setTextColor(QtGui.QColor(QtCore.Qt.black))
+ c.setSWRColor(self.vswrColor)
+
+ def changeCustomColors(self):
+ self.app.settings.setValue("UseCustomColors", self.use_custom_colors.isChecked())
+ if self.use_custom_colors.isChecked():
+ self.dark_mode_option.setDisabled(True)
+ self.dark_mode_option.setChecked(False)
+ self.btn_background_picker.setDisabled(False)
+ self.btn_foreground_picker.setDisabled(False)
+ self.btn_text_picker.setDisabled(False)
+ for c in self.app.subscribing_charts:
+ c.setBackgroundColor(self.backgroundColor)
+ c.setForegroundColor(self.foregroundColor)
+ c.setTextColor(self.textColor)
+ c.setSWRColor(self.vswrColor)
+ else:
+ self.dark_mode_option.setDisabled(False)
+ self.btn_background_picker.setDisabled(True)
+ self.btn_foreground_picker.setDisabled(True)
+ self.btn_text_picker.setDisabled(True)
+ self.changeDarkMode() # Reset to the default colors depending on Dark Mode setting
+
+ def setColor(self, name: str, color: QtGui.QColor):
+ if name == "background":
+ p = self.btn_background_picker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, color)
+ self.btn_background_picker.setPalette(p)
+ self.backgroundColor = color
+ self.app.settings.setValue("BackgroundColor", color)
+ elif name == "foreground":
+ p = self.btn_foreground_picker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, color)
+ self.btn_foreground_picker.setPalette(p)
+ self.foregroundColor = color
+ self.app.settings.setValue("ForegroundColor", color)
+ elif name == "text":
+ p = self.btn_text_picker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, color)
+ self.btn_text_picker.setPalette(p)
+ self.textColor = color
+ self.app.settings.setValue("TextColor", color)
+ elif name == "bands":
+ p = self.btn_bands_picker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, color)
+ self.btn_bands_picker.setPalette(p)
+ self.bandsColor = color
+ self.app.settings.setValue("BandsColor", color)
+ self.app.bands.setColor(color)
+ elif name == "vswr":
+ p = self.btn_vswr_picker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, color)
+ self.btn_vswr_picker.setPalette(p)
+ self.vswrColor = color
+ self.app.settings.setValue("VSWRColor", color)
+ self.changeCustomColors()
+
+ def setSweepColor(self, color: QtGui.QColor):
+ if color.isValid():
+ self.sweepColor = color
+ p = self.btnColorPicker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, color)
+ self.btnColorPicker.setPalette(p)
+ self.app.settings.setValue("SweepColor", color)
+ self.app.settings.sync()
+ for c in self.app.subscribing_charts:
+ c.setSweepColor(color)
+
+ def setSecondarySweepColor(self, color: QtGui.QColor):
+ if color.isValid():
+ self.secondarySweepColor = color
+ p = self.btnSecondaryColorPicker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, color)
+ self.btnSecondaryColorPicker.setPalette(p)
+ self.app.settings.setValue("SecondarySweepColor", color)
+ self.app.settings.sync()
+ for c in self.app.subscribing_charts:
+ c.setSecondarySweepColor(color)
+
+ def setReferenceColor(self, color):
+ if color.isValid():
+ self.referenceColor = color
+ p = self.btnReferenceColorPicker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, color)
+ self.btnReferenceColorPicker.setPalette(p)
+ self.app.settings.setValue("ReferenceColor", color)
+ self.app.settings.sync()
+
+ for c in self.app.subscribing_charts:
+ c.setReferenceColor(color)
+
+ def setSecondaryReferenceColor(self, color):
+ if color.isValid():
+ self.secondaryReferenceColor = color
+ p = self.btnSecondaryReferenceColorPicker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, color)
+ self.btnSecondaryReferenceColorPicker.setPalette(p)
+ self.app.settings.setValue("SecondaryReferenceColor", color)
+ self.app.settings.sync()
+
+ for c in self.app.subscribing_charts:
+ c.setSecondaryReferenceColor(color)
+
+ def setShowBands(self, show_bands):
+ self.app.bands.enabled = show_bands
+ self.app.bands.settings.setValue("ShowBands", show_bands)
+ self.app.bands.settings.sync()
+ for c in self.app.subscribing_charts:
+ c.update()
+
+ def changeFont(self):
+ font_size = self.font_dropdown.currentText()
+ self.app.settings.setValue("FontSize", font_size)
+ app: QtWidgets.QApplication = QtWidgets.QApplication.instance()
+ font = app.font()
+ font.setPointSize(int(font_size))
+ app.setFont(font)
+ self.app.changeFont(font)
+
+ def displayBandsWindow(self):
+ self.bandsWindow.show()
+ QtWidgets.QApplication.setActiveWindow(self.bandsWindow)
+
+ def displayMarkerWindow(self):
+ self.marker_window.show()
+ QtWidgets.QApplication.setActiveWindow(self.marker_window)
+
+ def addMarker(self):
+ new_marker = Marker("", self.app.settings)
+ new_marker.setScale(self.app.scaleFactor)
+ self.app.markers.append(new_marker)
+ self.app.marker_data_layout.addWidget(new_marker.getGroupBox())
+
+ new_marker.updated.connect(self.app.markerUpdated)
+ label, layout = new_marker.getRow()
+ self.app.marker_control_layout.insertRow(Marker.count() - 1, label, layout)
+ self.btn_remove_marker.setDisabled(False)
+
+ def removeMarker(self):
+ # keep at least one marker
+ if Marker.count() <= 1:
+ return
+ if Marker.count() == 2:
+ self.btn_remove_marker.setDisabled(True)
+ last_marker = self.app.markers.pop()
+
+ last_marker.updated.disconnect(self.app.markerUpdated)
+ self.app.marker_data_layout.removeWidget(last_marker.getGroupBox())
+ self.app.marker_control_layout.removeRow(Marker.count()-1)
+ last_marker.getGroupBox().hide()
+ last_marker.getGroupBox().destroy()
+ label, _ = last_marker.getRow()
+ label.hide()
+
+ def addVSWRMarker(self):
+ value, selected = QtWidgets.QInputDialog.getDouble(self, "Add VSWR Marker",
+ "VSWR value to show:", min=1.001, decimals=3)
+ if selected:
+ self.vswrMarkers.append(value)
+ if self.vswr_marker_dropdown.itemText(0) == "None":
+ self.vswr_marker_dropdown.removeItem(0)
+ self.vswr_marker_dropdown.addItem(str(value))
+ self.vswr_marker_dropdown.setCurrentText(str(value))
+ for c in self.app.s11charts:
+ c.addSWRMarker(value)
+ self.app.settings.setValue("VSWRMarkers", self.vswrMarkers)
+
+ def removeVSWRMarker(self):
+ value_str = self.vswr_marker_dropdown.currentText()
+ if value_str != "None":
+ value = float(value_str)
+ self.vswrMarkers.remove(value)
+ self.vswr_marker_dropdown.removeItem(self.vswr_marker_dropdown.currentIndex())
+ if self.vswr_marker_dropdown.count() == 0:
+ self.vswr_marker_dropdown.addItem("None")
+ self.app.settings.remove("VSWRMarkers")
+ else:
+ self.app.settings.setValue("VSWRMarkers", self.vswrMarkers)
+ for c in self.app.s11charts:
+ c.removeSWRMarker(value)
+
diff --git a/NanoVNASaver/Windows.temp/MarkerSettings.py b/NanoVNASaver/Windows.temp/MarkerSettings.py
new file mode 100644
index 0000000..cb889f6
--- /dev/null
+++ b/NanoVNASaver/Windows.temp/MarkerSettings.py
@@ -0,0 +1,154 @@
+# NanoVNASaver
+# A python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019. Rune B. Broberg
+#
+# 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
+
+from PyQt5 import QtWidgets, QtCore, QtGui
+
+from NanoVNASaver.RFTools import Datapoint
+from NanoVNASaver.Marker import Marker
+from NanoVNASaver.Marker.Values import TYPES, default_label_ids
+
+logger = logging.getLogger(__name__)
+
+
+class MarkerSettingsWindow(QtWidgets.QWidget):
+ exampleData11 = [Datapoint(123000000, 0.89, -0.11),
+ Datapoint(123500000, 0.9, -0.1),
+ Datapoint(124000000, 0.91, -0.95)]
+ exampleData21 = [Datapoint(123000000, -0.25, 0.49),
+ Datapoint(123456000, -0.3, 0.5),
+ Datapoint(124000000, -0.2, 0.5)]
+
+ def __init__(self, app: QtWidgets.QWidget):
+ super().__init__()
+ self.app = app
+
+ self.setWindowTitle("Marker settings")
+ self.setWindowIcon(self.app.icon)
+
+ QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.cancelButtonClick)
+
+ self.exampleMarker = Marker("Example marker")
+ layout = QtWidgets.QVBoxLayout()
+ self.setLayout(layout)
+
+ settings_group_box = QtWidgets.QGroupBox("Settings")
+ settings_group_box_layout = QtWidgets.QFormLayout(settings_group_box)
+ self.checkboxColouredMarker = QtWidgets.QCheckBox("Colored marker name")
+ self.checkboxColouredMarker.setChecked(self.app.settings.value("ColoredMarkerNames", True, bool))
+ self.checkboxColouredMarker.stateChanged.connect(self.updateMarker)
+ settings_group_box_layout.addRow(self.checkboxColouredMarker)
+
+ fields_group_box = QtWidgets.QGroupBox("Displayed data")
+ fields_group_box_layout = QtWidgets.QFormLayout(fields_group_box)
+
+ self.savedFieldSelection = self.app.settings.value(
+ "MarkerFields", defaultValue=default_label_ids()
+ )
+
+ if self.savedFieldSelection == "":
+ self.savedFieldSelection = []
+
+ self.currentFieldSelection = self.savedFieldSelection[:]
+
+ self.active_labels_view = QtWidgets.QListView()
+ self.update_displayed_data_form()
+
+ fields_group_box_layout.addRow(self.active_labels_view)
+
+ layout.addWidget(settings_group_box)
+ layout.addWidget(fields_group_box)
+ layout.addWidget(self.exampleMarker.getGroupBox())
+
+ btn_layout = QtWidgets.QHBoxLayout()
+ layout.addLayout(btn_layout)
+ btn_ok = QtWidgets.QPushButton("OK")
+ btn_apply = QtWidgets.QPushButton("Apply")
+ btn_default = QtWidgets.QPushButton("Defaults")
+ btn_cancel = QtWidgets.QPushButton("Cancel")
+
+ btn_ok.clicked.connect(self.okButtonClick)
+ btn_apply.clicked.connect(self.applyButtonClick)
+ btn_default.clicked.connect(self.defaultButtonClick)
+ btn_cancel.clicked.connect(self.cancelButtonClick)
+
+ btn_layout.addWidget(btn_ok)
+ btn_layout.addWidget(btn_apply)
+ btn_layout.addWidget(btn_default)
+ btn_layout.addWidget(btn_cancel)
+
+ self.updateMarker()
+ for m in self.app.markers:
+ m.setFieldSelection(self.currentFieldSelection)
+ m.setColoredText(self.checkboxColouredMarker.isChecked())
+
+ def updateMarker(self):
+ self.exampleMarker.setFrequency(123456000)
+ self.exampleMarker.setColoredText(self.checkboxColouredMarker.isChecked())
+ self.exampleMarker.setFieldSelection(self.currentFieldSelection)
+ self.exampleMarker.findLocation(self.exampleData11)
+ self.exampleMarker.resetLabels()
+ self.exampleMarker.updateLabels(self.exampleData11, self.exampleData21)
+
+ def updateField(self, field: QtGui.QStandardItem):
+ if field.checkState() == QtCore.Qt.Checked:
+ if not field.data() in self.currentFieldSelection:
+ self.currentFieldSelection = []
+ for i in range(self.model.rowCount()):
+ field = self.model.item(i, 0)
+ if field.checkState() == QtCore.Qt.Checked:
+ self.currentFieldSelection.append(field.data())
+ else:
+ if field.data() in self.currentFieldSelection:
+ self.currentFieldSelection.remove(field.data())
+ self.updateMarker()
+
+ def applyButtonClick(self):
+ self.savedFieldSelection = self.currentFieldSelection[:]
+ self.app.settings.setValue("MarkerFields", self.savedFieldSelection)
+ self.app.settings.setValue("ColoredMarkerNames", self.checkboxColouredMarker.isChecked())
+ for m in self.app.markers:
+ m.setFieldSelection(self.savedFieldSelection)
+ m.setColoredText(self.checkboxColouredMarker.isChecked())
+
+ def okButtonClick(self):
+ self.applyButtonClick()
+ self.close()
+
+ def cancelButtonClick(self):
+ self.currentFieldSelection = self.savedFieldSelection[:]
+ self.update_displayed_data_form()
+ self.updateMarker()
+ self.close()
+
+ def defaultButtonClick(self):
+ self.currentFieldSelection = default_label_ids()
+ self.update_displayed_data_form()
+ self.updateMarker()
+
+ def update_displayed_data_form(self):
+ self.model = QtGui.QStandardItemModel()
+ for label in TYPES:
+ item = QtGui.QStandardItem(label.description)
+ item.setData(label.label_id)
+ item.setCheckable(True)
+ item.setEditable(False)
+ if label.label_id in self.currentFieldSelection:
+ item.setCheckState(QtCore.Qt.Checked)
+ self.model.appendRow(item)
+ self.active_labels_view.setModel(self.model)
+ self.model.itemChanged.connect(self.updateField)
diff --git a/NanoVNASaver/Windows.temp/NanoVNASaver.splitted.py b/NanoVNASaver/Windows.temp/NanoVNASaver.splitted.py
new file mode 100644
index 0000000..8c49980
--- /dev/null
+++ b/NanoVNASaver/Windows.temp/NanoVNASaver.splitted.py
@@ -0,0 +1,1004 @@
+# NanoVNASaver
+# A python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019. Rune B. Broberg
+#
+# 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 time import sleep, strftime, localtime
+from typing import List
+
+import serial
+from PyQt5 import QtWidgets, QtCore, QtGui
+
+from .Windows import AboutWindow, AnalysisWindow, \
+ DeviceSettingsWindow, DisplaySettingsWindow, SweepSettingsWindow, \
+ TDRWindow
+from .Hardware import get_interfaces, get_VNA, InvalidVNA
+from .RFTools import RFTools, Datapoint
+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 CalibrationWindow, Calibration
+from .Inputs import FrequencyInputWidget
+from .Marker import Marker
+from .SweepWorker import SweepWorker
+from .Settings import BandsModel
+from .Touchstone import Touchstone
+from .about import version as ver
+
+logger = logging.getLogger(__name__)
+
+
+class NanoVNASaver(QtWidgets.QWidget):
+ version = ver
+ dataAvailable = QtCore.pyqtSignal()
+ scaleFactor = 1
+
+ sweepTitle = ""
+
+ def __init__(self):
+ super().__init__()
+ if getattr(sys, 'frozen', False):
+ logger.debug("Running from pyinstaller bundle")
+ self.icon = QtGui.QIcon(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("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.bands = BandsModel()
+
+ self.noSweeps = 1 # Number of sweeps to run
+
+ self.serialLock = threading.Lock()
+ self.serial = serial.Serial()
+ self.vna = InvalidVNA(self, serial)
+
+ self.dataLock = threading.Lock()
+ self.data: List[Datapoint] = []
+ self.data21: List[Datapoint] = []
+ self.referenceS11data: List[Datapoint] = []
+ self.referenceS21data: List[Datapoint] = []
+
+ self.sweepSource = ""
+ self.referenceSource = ""
+
+ self.calibration = Calibration()
+
+ self.markers = []
+
+ self.serialPort = ""
+
+ logger.debug("Building user interface")
+
+ self.baseTitle = "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.s11SmithChart = SmithChart("S11 Smith Chart")
+ self.s21PolarChart = PolarChart("S21 Polar Plot")
+ self.s11SParameterChart = SParameterChart("S11 Real/Imaginary")
+ self.s21SParameterChart = SParameterChart("S21 Real/Imaginary")
+ self.s11LogMag = LogMagChart("S11 Return Loss")
+ self.s21LogMag = LogMagChart("S21 Gain")
+ self.s11Mag = MagnitudeChart("|S11|")
+ self.s21Mag = MagnitudeChart("|S21|")
+ self.s11MagZ = MagnitudeZChart("S11 |Z|")
+ self.s11Phase = PhaseChart("S11 Phase")
+ self.s21Phase = PhaseChart("S21 Phase")
+ self.s11GroupDelay = GroupDelayChart("S11 Group Delay")
+ self.s11CapacitanceChart = CapacitanceChart("S11 Serial C")
+ self.s11InductanceChart = InductanceChart("S11 Serial L")
+ self.s21GroupDelay = GroupDelayChart("S21 Group Delay", reflective=False)
+ self.permabilityChart = PermeabilityChart("S11 R/\N{GREEK SMALL LETTER OMEGA} & X/\N{GREEK SMALL LETTER OMEGA}")
+ self.s11VSWR = VSWRChart("S11 VSWR")
+ self.s11QualityFactor = QualityFactorChart("S11 Quality Factor")
+ self.s11RealImaginary = RealImaginaryChart("S11 R+jX")
+ self.tdr_chart = TDRChart("TDR")
+ self.tdr_mainwindow_chart = TDRChart("TDR")
+ self.combinedLogMag = CombinedLogMagChart("S11 & S21 LogMag")
+
+ # List of all the S11 charts, for selecting
+ self.s11charts: List[Chart] = []
+ self.s11charts.append(self.s11SmithChart)
+ self.s11charts.append(self.s11LogMag)
+ self.s11charts.append(self.s11Mag)
+ self.s11charts.append(self.s11MagZ)
+ self.s11charts.append(self.s11Phase)
+ self.s11charts.append(self.s11GroupDelay)
+ self.s11charts.append(self.s11VSWR)
+ self.s11charts.append(self.s11RealImaginary)
+ self.s11charts.append(self.s11QualityFactor)
+ self.s11charts.append(self.s11SParameterChart)
+ self.s11charts.append(self.s11CapacitanceChart)
+ self.s11charts.append(self.s11InductanceChart)
+ self.s11charts.append(self.permabilityChart)
+
+ # List of all the S21 charts, for selecting
+ self.s21charts: List[Chart] = []
+ self.s21charts.append(self.s21PolarChart)
+ self.s21charts.append(self.s21LogMag)
+ self.s21charts.append(self.s21Mag)
+ self.s21charts.append(self.s21Phase)
+ self.s21charts.append(self.s21GroupDelay)
+ self.s21charts.append(self.s21SParameterChart)
+
+ # List of all charts that use both S11 and S21
+ self.combinedCharts: List[Chart] = []
+ self.combinedCharts.append(self.combinedLogMag)
+
+ # 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)
+
+ ################################################################################################################
+ # Sweep control
+ ################################################################################################################
+
+ sweep_control_box = QtWidgets.QGroupBox()
+ sweep_control_box.setMaximumWidth(250)
+ sweep_control_box.setTitle("Sweep control")
+ sweep_control_layout = QtWidgets.QFormLayout(sweep_control_box)
+
+ line = QtWidgets.QFrame()
+ line.setFrameShape(QtWidgets.QFrame.VLine)
+
+ sweep_input_layout = QtWidgets.QHBoxLayout()
+ sweep_input_left_layout = QtWidgets.QFormLayout()
+ sweep_input_right_layout = QtWidgets.QFormLayout()
+ sweep_input_layout.addLayout(sweep_input_left_layout)
+ sweep_input_layout.addWidget(line)
+ sweep_input_layout.addLayout(sweep_input_right_layout)
+ sweep_control_layout.addRow(sweep_input_layout)
+
+ self.sweepStartInput = FrequencyInputWidget()
+ self.sweepStartInput.setMinimumWidth(60)
+ self.sweepStartInput.setAlignment(QtCore.Qt.AlignRight)
+ self.sweepStartInput.textEdited.connect(self.updateCenterSpan)
+ self.sweepStartInput.textChanged.connect(self.updateStepSize)
+ sweep_input_left_layout.addRow(QtWidgets.QLabel("Start"), self.sweepStartInput)
+
+ self.sweepEndInput = FrequencyInputWidget()
+ self.sweepEndInput.setAlignment(QtCore.Qt.AlignRight)
+ self.sweepEndInput.textEdited.connect(self.updateCenterSpan)
+ self.sweepEndInput.textChanged.connect(self.updateStepSize)
+ sweep_input_left_layout.addRow(QtWidgets.QLabel("Stop"), self.sweepEndInput)
+
+ self.sweepCenterInput = FrequencyInputWidget()
+ self.sweepCenterInput.setMinimumWidth(60)
+ self.sweepCenterInput.setAlignment(QtCore.Qt.AlignRight)
+ self.sweepCenterInput.textEdited.connect(self.updateStartEnd)
+
+ sweep_input_right_layout.addRow(QtWidgets.QLabel("Center"), self.sweepCenterInput)
+
+ self.sweepSpanInput = FrequencyInputWidget()
+ self.sweepSpanInput.setAlignment(QtCore.Qt.AlignRight)
+ self.sweepSpanInput.textEdited.connect(self.updateStartEnd)
+
+ sweep_input_right_layout.addRow(QtWidgets.QLabel("Span"), self.sweepSpanInput)
+
+ self.sweepCountInput = QtWidgets.QLineEdit(self.settings.value("Segments", "1"))
+ self.sweepCountInput.setAlignment(QtCore.Qt.AlignRight)
+ self.sweepCountInput.setFixedWidth(60)
+ self.sweepCountInput.textEdited.connect(self.updateStepSize)
+
+ self.sweepStepLabel = QtWidgets.QLabel("Hz/step")
+ self.sweepStepLabel.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+
+ segment_layout = QtWidgets.QHBoxLayout()
+ segment_layout.addWidget(self.sweepCountInput)
+ segment_layout.addWidget(self.sweepStepLabel)
+ sweep_control_layout.addRow(QtWidgets.QLabel("Segments"), segment_layout)
+
+ self.sweepSettingsWindow = SweepSettingsWindow(self)
+ btn_sweep_settings_window = QtWidgets.QPushButton("Sweep settings ...")
+ btn_sweep_settings_window.clicked.connect(self.displaySweepSettingsWindow)
+
+ sweep_control_layout.addRow(btn_sweep_settings_window)
+
+ self.sweepProgressBar = QtWidgets.QProgressBar()
+ self.sweepProgressBar.setMaximum(100)
+ self.sweepProgressBar.setValue(0)
+ sweep_control_layout.addRow(self.sweepProgressBar)
+
+ self.btnSweep = QtWidgets.QPushButton("Sweep")
+ self.btnSweep.clicked.connect(self.sweep)
+ self.btnSweep.setShortcut(QtCore.Qt.Key_W | QtCore.Qt.CTRL)
+ self.btnStopSweep = QtWidgets.QPushButton("Stop")
+ self.btnStopSweep.clicked.connect(self.stopSweep)
+ self.btnStopSweep.setShortcut(QtCore.Qt.Key_Escape)
+ self.btnStopSweep.setDisabled(True)
+ btn_layout = QtWidgets.QHBoxLayout()
+ btn_layout.addWidget(self.btnSweep)
+ btn_layout.addWidget(self.btnStopSweep)
+ btn_layout.setContentsMargins(0, 0, 0, 0)
+ btn_layout_widget = QtWidgets.QWidget()
+ btn_layout_widget.setLayout(btn_layout)
+ sweep_control_layout.addRow(btn_layout_widget)
+
+ left_column.addWidget(sweep_control_box)
+
+ ################################################################################################################
+ # 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.analysis_window = AnalysisWindow(self)
+
+ btn_show_analysis = QtWidgets.QPushButton("Analysis ...")
+ btn_show_analysis.clicked.connect(self.displayAnalysisWindow)
+ self.marker_column.addWidget(btn_show_analysis)
+
+ ################################################################################################################
+ # TDR
+ ################################################################################################################
+
+ self.tdr_window = TDRWindow(self)
+ self.tdr_chart.tdrWindow = self.tdr_window
+ self.tdr_mainwindow_chart.tdrWindow = self.tdr_window
+ self.tdr_window.updated.connect(self.tdr_chart.update)
+ self.tdr_window.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(self.displayTDRWindow)
+
+ 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(60)
+ 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.deviceSettingsWindow = DeviceSettingsWindow(self)
+ self.btnDeviceSettings = QtWidgets.QPushButton("Manage")
+ self.btnDeviceSettings.clicked.connect(self.displayDeviceSettingsWindow)
+ serial_button_layout.addWidget(self.btnDeviceSettings, stretch=0)
+ serial_control_layout.addRow(serial_button_layout)
+ left_column.addWidget(serial_control_box)
+
+ ################################################################################################################
+ # File control
+ ################################################################################################################
+
+ self.fileWindow = QtWidgets.QWidget()
+ self.fileWindow.setWindowTitle("Files")
+ self.fileWindow.setWindowIcon(self.icon)
+ self.fileWindow.setMinimumWidth(200)
+ QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self.fileWindow, self.fileWindow.hide)
+ file_window_layout = QtWidgets.QVBoxLayout()
+ self.fileWindow.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(self.exportFileS1P)
+ save_file_control_layout.addRow(btn_export_file)
+
+ btn_export_file = QtWidgets.QPushButton("Save 2-Port file (S2P)")
+ btn_export_file.clicked.connect(self.exportFileS2P)
+ 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(self.displayFileWindow)
+
+ ################################################################################################################
+ # Calibration
+ ################################################################################################################
+
+ btnOpenCalibrationWindow = QtWidgets.QPushButton("Calibration ...")
+ self.calibrationWindow = CalibrationWindow(self)
+ btnOpenCalibrationWindow.clicked.connect(self.displayCalibrationWindow)
+
+ ################################################################################################################
+ # Display setup
+ ################################################################################################################
+
+ btn_display_setup = QtWidgets.QPushButton("Display setup ...")
+ btn_display_setup.setMaximumWidth(250)
+ self.displaySetupWindow = DisplaySettingsWindow(self)
+ btn_display_setup.clicked.connect(self.displaySettingsWindow)
+
+ self.aboutWindow = AboutWindow(self)
+
+ btn_about = QtWidgets.QPushButton("About ...")
+ btn_about.setMaximumWidth(250)
+
+ btn_about.clicked.connect(self.displayAboutWindow)
+
+ 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 port in get_interfaces():
+ self.serialPortInput.insertItem(1, port[1], port[0])
+
+ def exportFileS1P(self):
+ if len(self.data) == 0:
+ # No data to save, alert the user
+ QtWidgets.QMessageBox.warning(self, "No data to save", "There is no data to save.")
+ return
+
+ filedialog = QtWidgets.QFileDialog(self)
+ filedialog.setDefaultSuffix("s1p")
+ filedialog.setNameFilter("Touchstone 1-Port Files (*.s1p);;All files (*.*)")
+ filedialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
+ selected = filedialog.exec()
+ if selected:
+ filename = filedialog.selectedFiles()[0]
+ else:
+ return
+ if filename == "":
+ logger.debug("No file name selected.")
+ return
+ logger.debug("Save S1P file to %s", filename)
+ if len(self.data) == 0:
+ logger.warning("No data stored, nothing written.")
+ return
+ # TODO: should be delegated to Touchstone module
+ try:
+ logger.debug("Opening %s for writing", filename)
+ with open(filename, "w+") as tsfile:
+ logger.debug("Writing file")
+ tsfile.write("# Hz S RI R 50\n")
+ freq_prev = 0
+ for data in self.data:
+ if data.freq <= freq_prev:
+ continue
+ tsfile.write(f"{data.freq} {data.re} {data.im}\n")
+ logger.debug("File written")
+ except Exception as e:
+ logger.exception("Error during file export: %s", e)
+ return
+
+ def exportFileS2P(self):
+ if len(self.data21) == 0:
+ # No S21 data to save, alert the user
+ QtWidgets.QMessageBox.warning(self, "No S21 data to save", "There is no S21 data to save.")
+ return
+
+ filedialog = QtWidgets.QFileDialog(self)
+ filedialog.setDefaultSuffix("s2p")
+ filedialog.setNameFilter("Touchstone 2-Port Files (*.s2p);;All files (*.*)")
+ filedialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
+ selected = filedialog.exec()
+ if selected:
+ filename = filedialog.selectedFiles()[0]
+ else:
+ return
+ if filename == "":
+ logger.debug("No file name selected.")
+ return
+ logger.debug("Save S2P file to %s", filename)
+ if len(self.data) == 0 or len(self.data21) == 0:
+ logger.warning("No data stored, nothing written.")
+ return
+ # TODO: should be delegated to Touchstone module
+ try:
+ logger.debug("Opening %s for writing", filename)
+ file = open(filename, "w+")
+ logger.debug("Writing file")
+ file.write("# Hz S RI R 50\n")
+ for i in range(len(self.data)):
+ if i == 0 or self.data[i].freq != self.data[i-1].freq:
+ file.write(str(self.data[i].freq) + " " + str(self.data[i].re) + " " + str(self.data[i].im) + " " +
+ str(self.data21[i].re) + " " + str(self.data21[i].im) + " 0 0 0 0\n")
+ file.close()
+ logger.debug("File written")
+ except Exception as e:
+ logger.exception("Error during file export: %s", e)
+ return
+
+ def serialButtonClick(self):
+ if self.serial.is_open:
+ self.stopSerial()
+ else:
+ self.startSerial()
+ return
+
+ def startSerial(self):
+ if self.serialLock.acquire():
+ self.serialPort = self.serialPortInput.currentData()
+ if self.serialPort == "":
+ self.serialPort = self.serialPortInput.currentText()
+ logger.info("Opening serial port %s", self.serialPort)
+ try:
+ self.serial = serial.Serial(port=self.serialPort, baudrate=115200)
+ self.serial.timeout = 0.05
+ except serial.SerialException as exc:
+ logger.error("Tried to open %s and failed: %s", self.serialPort, exc)
+ self.serialLock.release()
+ return
+ self.btnSerialToggle.setText("Disconnect")
+
+ self.serialLock.release()
+ sleep(0.05)
+
+ self.vna = get_VNA(self, self.serial)
+ self.vna.validateInput = self.settings.value("SerialInputValidation", True, bool)
+ self.worker.setVNA(self.vna)
+
+ logger.info(self.vna.readFirmware())
+
+ frequencies = self.vna.readFrequencies()
+ if frequencies:
+ logger.info("Read starting frequency %s and end frequency %s", frequencies[0], frequencies[100])
+ if int(frequencies[0]) == int(frequencies[100]) and (self.sweepStartInput.text() == "" or
+ self.sweepEndInput.text() == ""):
+ self.sweepStartInput.setText(RFTools.formatSweepFrequency(int(frequencies[0])))
+ self.sweepEndInput.setText(RFTools.formatSweepFrequency(int(frequencies[100]) + 100000))
+ elif self.sweepStartInput.text() == "" or self.sweepEndInput.text() == "":
+ self.sweepStartInput.setText(RFTools.formatSweepFrequency(int(frequencies[0])))
+ self.sweepEndInput.setText(RFTools.formatSweepFrequency(int(frequencies[100])))
+ self.sweepStartInput.textEdited.emit(self.sweepStartInput.text())
+ self.sweepStartInput.textChanged.emit(self.sweepStartInput.text())
+ else:
+ logger.warning("No frequencies read")
+ return
+ logger.debug("Starting initial sweep")
+ self.sweep()
+ return
+
+ def stopSerial(self):
+ if self.serialLock.acquire():
+ logger.info("Closing connection to NanoVNA")
+ self.serial.close()
+ self.serialLock.release()
+ self.btnSerialToggle.setText("Connect to NanoVNA")
+
+ def toggleSweepSettings(self, disabled):
+ self.sweepStartInput.setDisabled(disabled)
+ self.sweepEndInput.setDisabled(disabled)
+ self.sweepSpanInput.setDisabled(disabled)
+ self.sweepCenterInput.setDisabled(disabled)
+ self.sweepCountInput.setDisabled(disabled)
+
+ def sweep(self):
+ # Run the serial port update
+ if not self.serial.is_open:
+ return
+ self.worker.stopped = False
+
+ self.sweepProgressBar.setValue(0)
+ self.btnSweep.setDisabled(True)
+ self.btnStopSweep.setDisabled(False)
+ self.toggleSweepSettings(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("")
+
+ if self.sweepCountInput.text().isdigit():
+ self.settings.setValue("Segments", self.sweepCountInput.text())
+
+ logger.debug("Starting worker thread")
+ self.threadpool.start(self.worker)
+
+ def stopSweep(self):
+ self.worker.stopped = True
+
+ def saveData(self, data, data12, source=None):
+ if self.dataLock.acquire(blocking=True):
+ self.data = data
+ self.data21 = data12
+ else:
+ logger.error("Failed acquiring data lock while saving.")
+ self.dataLock.release()
+ if source is not None:
+ self.sweepSource = source
+ elif self.sweepTitle != "":
+ self.sweepSource = self.sweepTitle + " " + strftime("%Y-%m-%d %H:%M:%S", localtime())
+ else:
+ self.sweepSource = strftime("%Y-%m-%d %H:%M:%S", localtime())
+
+ def markerUpdated(self, marker: Marker):
+ if self.dataLock.acquire(blocking=True):
+ marker.findLocation(self.data)
+ for m in self.markers:
+ m.resetLabels()
+ m.updateLabels(self.data, self.data21)
+
+ for c in self.subscribing_charts:
+ c.update()
+ self.dataLock.release()
+
+ def dataUpdated(self):
+ if self.dataLock.acquire(blocking=True):
+ for m in self.markers:
+ m.findLocation(self.data)
+ m.resetLabels()
+ m.updateLabels(self.data, self.data21)
+
+ for c in self.s11charts:
+ c.setData(self.data)
+
+ for c in self.s21charts:
+ c.setData(self.data21)
+
+ for c in self.combinedCharts:
+ c.setCombinedData(self.data, self.data21)
+
+ self.sweepProgressBar.setValue(self.worker.percentage)
+ self.tdr_window.updateTDR()
+
+ # Find the minimum S11 VSWR:
+ min_vswr = 100
+ min_vswr_freq = -1
+ for d in self.data:
+ 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(str(round(min_vswr, 3)) + " @ " + RFTools.formatFrequency(min_vswr_freq))
+ if min_vswr > 1:
+ self.s11_min_rl_label.setText(str(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(
+ str(round(min_gain, 3)) + " dB @ " + RFTools.formatFrequency(min_gain_freq))
+ self.s21_max_gain_label.setText(
+ str(round(max_gain, 3)) + " dB @ " + RFTools.formatFrequency(max_gain_freq))
+ else:
+ self.s21_min_gain_label.setText("")
+ self.s21_max_gain_label.setText("")
+
+ else:
+ logger.error("Failed acquiring data lock while updating.")
+ self.updateTitle()
+ self.dataLock.release()
+ self.dataAvailable.emit()
+
+ def sweepFinished(self):
+ self.sweepProgressBar.setValue(100)
+ self.btnSweep.setDisabled(False)
+ self.btnStopSweep.setDisabled(True)
+ self.toggleSweepSettings(False)
+
+ def updateCenterSpan(self):
+ fstart = RFTools.parseFrequency(self.sweepStartInput.text())
+ fstop = RFTools.parseFrequency(self.sweepEndInput.text())
+ fspan = fstop - fstart
+ fcenter = int(round((fstart+fstop)/2))
+ if fspan < 0 or fstart < 0 or fstop < 0:
+ return
+ self.sweepSpanInput.setText(RFTools.formatSweepFrequency(fspan))
+ self.sweepCenterInput.setText(RFTools.formatSweepFrequency(fcenter))
+
+ def updateStartEnd(self):
+ fcenter = RFTools.parseFrequency(self.sweepCenterInput.text())
+ fspan = RFTools.parseFrequency(self.sweepSpanInput.text())
+ if fspan < 0 or fcenter < 0:
+ return
+ fstart = int(round(fcenter - fspan/2))
+ fstop = int(round(fcenter + fspan/2))
+ if fstart < 0 or fstop < 0:
+ return
+ self.sweepStartInput.setText(RFTools.formatSweepFrequency(fstart))
+ self.sweepEndInput.setText(RFTools.formatSweepFrequency(fstop))
+
+ def updateStepSize(self):
+ fspan = RFTools.parseFrequency(self.sweepSpanInput.text())
+ if fspan < 0:
+ return
+ if self.sweepCountInput.text().isdigit():
+ segments = int(self.sweepCountInput.text())
+ if segments > 0:
+ fstep = fspan / (segments * 101 - 1)
+ self.sweepStepLabel.setText(RFTools.formatShortFrequency(fstep) + "/step")
+
+ def setReference(self, s11data=None, s21data=None, source=None):
+ if not s11data:
+ s11data = self.data
+ if not s21data:
+ 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 += "Sweep: " + self.sweepSource + " @ " + str(len(self.data)) + " points"
+ if self.referenceSource != "":
+ if insert != "":
+ insert += ", "
+ insert += "Reference: " + self.referenceSource + " @ " + str(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.data = []
+ 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 displaySettingsWindow(self):
+ self.displaySetupWindow.show()
+ QtWidgets.QApplication.setActiveWindow(self.displaySetupWindow)
+
+ def displaySweepSettingsWindow(self):
+ self.sweepSettingsWindow.show()
+ QtWidgets.QApplication.setActiveWindow(self.sweepSettingsWindow)
+
+ def displayDeviceSettingsWindow(self):
+ self.deviceSettingsWindow.show()
+ QtWidgets.QApplication.setActiveWindow(self.deviceSettingsWindow)
+
+ def displayCalibrationWindow(self):
+ self.calibrationWindow.show()
+ QtWidgets.QApplication.setActiveWindow(self.calibrationWindow)
+
+ def displayFileWindow(self):
+ self.fileWindow.show()
+ QtWidgets.QApplication.setActiveWindow(self.fileWindow)
+
+ def displayTDRWindow(self):
+ self.tdr_window.show()
+ QtWidgets.QApplication.setActiveWindow(self.tdr_window)
+
+ def displayAnalysisWindow(self):
+ self.analysis_window.show()
+ QtWidgets.QApplication.setActiveWindow(self.analysis_window)
+
+ def displayAboutWindow(self):
+ self.aboutWindow.show()
+ QtWidgets.QApplication.setActiveWindow(self.aboutWindow)
+
+ 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.serial.flushInput() # 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)
+ standard_string = "0.123456789 0.123456789 MHz \N{OHM SIGN}" # Characters we would normally display
+ 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)
diff --git a/NanoVNASaver/Windows.temp/Screenshot.py b/NanoVNASaver/Windows.temp/Screenshot.py
new file mode 100644
index 0000000..5d68617
--- /dev/null
+++ b/NanoVNASaver/Windows.temp/Screenshot.py
@@ -0,0 +1,99 @@
+# NanoVNASaver
+# A python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019. Rune B. Broberg
+#
+# 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
+from PyQt5 import QtWidgets, QtCore, QtGui
+
+logger = logging.getLogger(__name__)
+
+
+class ScreenshotWindow(QtWidgets.QLabel):
+ pix = None
+
+ def __init__(self):
+ super().__init__()
+ self.setWindowTitle("Screenshot")
+ # TODO : self.setWindowIcon(self.app.icon)
+
+ QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
+ self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
+
+ self.action_original_size = QtWidgets.QAction("Original size")
+ self.action_original_size.triggered.connect(lambda: self.setScale(1))
+ self.action_2x_size = QtWidgets.QAction("2x size")
+ self.action_2x_size.triggered.connect(lambda: self.setScale(2))
+ self.action_3x_size = QtWidgets.QAction("3x size")
+ self.action_3x_size.triggered.connect(lambda: self.setScale(3))
+ self.action_4x_size = QtWidgets.QAction("4x size")
+ self.action_4x_size.triggered.connect(lambda: self.setScale(4))
+ self.action_5x_size = QtWidgets.QAction("5x size")
+ self.action_5x_size.triggered.connect(lambda: self.setScale(5))
+
+ self.addAction(self.action_original_size)
+ self.addAction(self.action_2x_size)
+ self.addAction(self.action_3x_size)
+ self.addAction(self.action_4x_size)
+ self.addAction(self.action_5x_size)
+ self.action_save_screenshot = QtWidgets.QAction("Save image")
+ self.action_save_screenshot.triggered.connect(self.saveScreenshot)
+ self.addAction(self.action_save_screenshot)
+
+ def setScreenshot(self, pixmap: QtGui.QPixmap):
+ if self.pix is None:
+ self.resize(pixmap.size())
+ self.pix = pixmap
+ self.setPixmap(
+ self.pix.scaled(
+ self.size(),
+ QtCore.Qt.KeepAspectRatio,
+ QtCore.Qt.FastTransformation))
+ w, h = pixmap.width(), pixmap.height()
+ self.action_original_size.setText(
+ "Original size (" + str(w) + "x" + str(h) + ")")
+ self.action_2x_size.setText(
+ "2x size (" + str(w * 2) + "x" + str(h * 2) + ")")
+ self.action_3x_size.setText(
+ "3x size (" + str(w * 3) + "x" + str(h * 3) + ")")
+ self.action_4x_size.setText(
+ "4x size (" + str(w * 4) + "x" + str(h * 4) + ")")
+ self.action_5x_size.setText(
+ "5x size (" + str(w * 5) + "x" + str(h * 5) + ")")
+
+ def saveScreenshot(self):
+ if self.pix is not None:
+ logger.info("Saving screenshot to file...")
+ filename, _ = QtWidgets.QFileDialog.getSaveFileName(
+ parent=self, caption="Save image",
+ filter="PNG (*.png);;All files (*.*)")
+
+ logger.debug("Filename: %s", filename)
+ if filename != "":
+ self.pixmap().save(filename)
+ else:
+ logger.warning("The user got shown an empty screenshot window?")
+
+ def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
+ super().resizeEvent(a0)
+ if self.pixmap() is not None:
+ self.setPixmap(
+ self.pix.scaled(
+ self.size(),
+ QtCore.Qt.KeepAspectRatio,
+ QtCore.Qt.FastTransformation))
+
+ def setScale(self, scale):
+ width, height = self.pix.size().width() * scale, self.pix.size().height() * scale
+ self.resize(width, height)
diff --git a/NanoVNASaver/Windows.temp/SweepSettings.py b/NanoVNASaver/Windows.temp/SweepSettings.py
new file mode 100644
index 0000000..99d002c
--- /dev/null
+++ b/NanoVNASaver/Windows.temp/SweepSettings.py
@@ -0,0 +1,176 @@
+# NanoVNASaver
+# A python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019. Rune B. Broberg
+#
+# 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
+
+from PyQt5 import QtWidgets, QtCore
+
+from NanoVNASaver.RFTools import RFTools
+
+logger = logging.getLogger(__name__)
+
+
+class SweepSettingsWindow(QtWidgets.QWidget):
+ def __init__(self, app: QtWidgets.QWidget):
+ super().__init__()
+
+ self.app = app
+ self.setWindowTitle("Sweep settings")
+ self.setWindowIcon(self.app.icon)
+
+ QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
+
+ layout = QtWidgets.QVBoxLayout()
+ self.setLayout(layout)
+
+ title_box = QtWidgets.QGroupBox("Sweep name")
+ title_layout = QtWidgets.QFormLayout(title_box)
+ self.sweep_title_input = QtWidgets.QLineEdit()
+ title_layout.addRow("Sweep name", self.sweep_title_input)
+ title_button_layout = QtWidgets.QHBoxLayout()
+ btn_set_sweep_title = QtWidgets.QPushButton("Set")
+ btn_set_sweep_title.clicked.connect(
+ lambda: self.app.setSweepTitle(self.sweep_title_input.text()))
+ btn_reset_sweep_title = QtWidgets.QPushButton("Reset")
+ btn_reset_sweep_title.clicked.connect(lambda: self.app.setSweepTitle(""))
+ title_button_layout.addWidget(btn_set_sweep_title)
+ title_button_layout.addWidget(btn_reset_sweep_title)
+ title_layout.addRow(title_button_layout)
+ layout.addWidget(title_box)
+
+ settings_box = QtWidgets.QGroupBox("Settings")
+ settings_layout = QtWidgets.QFormLayout(settings_box)
+
+ self.single_sweep_radiobutton = QtWidgets.QRadioButton("Single sweep")
+ self.continuous_sweep_radiobutton = QtWidgets.QRadioButton("Continuous sweep")
+ self.averaged_sweep_radiobutton = QtWidgets.QRadioButton("Averaged sweep")
+
+ settings_layout.addWidget(self.single_sweep_radiobutton)
+ self.single_sweep_radiobutton.setChecked(True)
+ settings_layout.addWidget(self.continuous_sweep_radiobutton)
+ settings_layout.addWidget(self.averaged_sweep_radiobutton)
+
+ self.averages = QtWidgets.QLineEdit("3")
+ self.truncates = QtWidgets.QLineEdit("0")
+
+ settings_layout.addRow("Number of measurements to average", self.averages)
+ settings_layout.addRow("Number to discard", self.truncates)
+ settings_layout.addRow(
+ QtWidgets.QLabel(
+ "Averaging allows discarding outlying samples to get better averages."))
+ settings_layout.addRow(
+ QtWidgets.QLabel("Common values are 3/0, 5/2, 9/4 and 25/6."))
+
+ self.continuous_sweep_radiobutton.toggled.connect(
+ lambda: self.app.worker.setContinuousSweep(
+ self.continuous_sweep_radiobutton.isChecked()))
+ self.averaged_sweep_radiobutton.toggled.connect(self.updateAveraging)
+ self.averages.textEdited.connect(self.updateAveraging)
+ self.truncates.textEdited.connect(self.updateAveraging)
+
+ layout.addWidget(settings_box)
+
+ band_sweep_box = QtWidgets.QGroupBox("Sweep band")
+ band_sweep_layout = QtWidgets.QFormLayout(band_sweep_box)
+
+ self.band_list = QtWidgets.QComboBox()
+ self.band_list.setModel(self.app.bands)
+ self.band_list.currentIndexChanged.connect(self.updateCurrentBand)
+
+ band_sweep_layout.addRow("Select band", self.band_list)
+
+ self.band_pad_group = QtWidgets.QButtonGroup()
+ self.band_pad_0 = QtWidgets.QRadioButton("None")
+ self.band_pad_10 = QtWidgets.QRadioButton("10%")
+ self.band_pad_25 = QtWidgets.QRadioButton("25%")
+ self.band_pad_100 = QtWidgets.QRadioButton("100%")
+ self.band_pad_0.setChecked(True)
+ self.band_pad_group.addButton(self.band_pad_0)
+ self.band_pad_group.addButton(self.band_pad_10)
+ self.band_pad_group.addButton(self.band_pad_25)
+ self.band_pad_group.addButton(self.band_pad_100)
+ self.band_pad_group.buttonClicked.connect(self.updateCurrentBand)
+ band_sweep_layout.addRow("Pad band limits", self.band_pad_0)
+ band_sweep_layout.addRow("", self.band_pad_10)
+ band_sweep_layout.addRow("", self.band_pad_25)
+ band_sweep_layout.addRow("", self.band_pad_100)
+
+ self.band_limit_label = QtWidgets.QLabel()
+
+ band_sweep_layout.addRow(self.band_limit_label)
+
+ btn_set_band_sweep = QtWidgets.QPushButton("Set band sweep")
+ btn_set_band_sweep.clicked.connect(self.setBandSweep)
+ band_sweep_layout.addRow(btn_set_band_sweep)
+
+ self.updateCurrentBand()
+
+ layout.addWidget(band_sweep_box)
+
+ def updateCurrentBand(self):
+ index_start = self.band_list.model().index(self.band_list.currentIndex(), 1)
+ index_stop = self.band_list.model().index(self.band_list.currentIndex(), 2)
+ start = int(self.band_list.model().data(index_start, QtCore.Qt.ItemDataRole).value())
+ stop = int(self.band_list.model().data(index_stop, QtCore.Qt.ItemDataRole).value())
+
+ if self.band_pad_10.isChecked():
+ padding = 10
+ elif self.band_pad_25.isChecked():
+ padding = 25
+ elif self.band_pad_100.isChecked():
+ padding = 100
+ else:
+ padding = 0
+
+ if padding > 0:
+ span = stop - start
+ start -= round(span * padding / 100)
+ start = max(1, start)
+ stop += round(span * padding / 100)
+
+ self.band_limit_label.setText(
+ f"Sweep span: {RFTools.formatShortFrequency(start)}"
+ f" to {RFTools.formatShortFrequency(stop)}")
+
+ def setBandSweep(self):
+ index_start = self.band_list.model().index(self.band_list.currentIndex(), 1)
+ index_stop = self.band_list.model().index(self.band_list.currentIndex(), 2)
+ start = int(self.band_list.model().data(index_start, QtCore.Qt.ItemDataRole).value())
+ stop = int(self.band_list.model().data(index_stop, QtCore.Qt.ItemDataRole).value())
+
+ if self.band_pad_10.isChecked():
+ padding = 10
+ elif self.band_pad_25.isChecked():
+ padding = 25
+ elif self.band_pad_100.isChecked():
+ padding = 100
+ else:
+ padding = 0
+
+ if padding > 0:
+ span = stop - start
+ start -= round(span * padding / 100)
+ start = max(1, start)
+ stop += round(span * padding / 100)
+
+ self.app.sweepStartInput.setText(RFTools.formatSweepFrequency(start))
+ self.app.sweepEndInput.setText(RFTools.formatSweepFrequency(stop))
+ self.app.sweepEndInput.textEdited.emit(self.app.sweepEndInput.text())
+
+ def updateAveraging(self):
+ self.app.worker.setAveraging(self.averaged_sweep_radiobutton.isChecked(),
+ self.averages.text(),
+ self.truncates.text())
diff --git a/NanoVNASaver/Windows.temp/TDR.py b/NanoVNASaver/Windows.temp/TDR.py
new file mode 100644
index 0000000..458313e
--- /dev/null
+++ b/NanoVNASaver/Windows.temp/TDR.py
@@ -0,0 +1,152 @@
+# NanoVNASaver - a python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019. Rune B. Broberg
+#
+# 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 numpy as np
+import scipy.signal as signal
+from PyQt5 import QtWidgets, QtCore
+
+
+logger = logging.getLogger(__name__)
+
+
+class TDRWindow(QtWidgets.QWidget):
+ updated = QtCore.pyqtSignal()
+
+ def __init__(self, app: QtWidgets.QWidget):
+ super().__init__()
+ self.app = app
+
+ self.td = []
+ self.distance_axis = []
+ self.step_response = []
+ self.step_response_Z = []
+
+ self.setWindowTitle("TDR")
+ self.setWindowIcon(self.app.icon)
+
+ QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
+
+ layout = QtWidgets.QFormLayout()
+ self.setLayout(layout)
+
+ self.tdr_velocity_dropdown = QtWidgets.QComboBox()
+ self.tdr_velocity_dropdown.addItem("Jelly filled (0.64)", 0.64)
+ self.tdr_velocity_dropdown.addItem("Polyethylene (0.66)", 0.66)
+ self.tdr_velocity_dropdown.addItem("PTFE (Teflon) (0.70)", 0.70)
+ self.tdr_velocity_dropdown.addItem("Pulp Insulation (0.72)", 0.72)
+ self.tdr_velocity_dropdown.addItem("Foam or Cellular PE (0.78)", 0.78)
+ self.tdr_velocity_dropdown.addItem("Semi-solid PE (SSPE) (0.84)", 0.84)
+ self.tdr_velocity_dropdown.addItem("Air (Helical spacers) (0.94)", 0.94)
+ self.tdr_velocity_dropdown.insertSeparator(self.tdr_velocity_dropdown.count())
+ # Lots of cable types added by Larry Goga, AE5CZ
+ self.tdr_velocity_dropdown.addItem("RG-6/U PE 75\N{OHM SIGN} (Belden 8215) (0.66)", 0.66)
+ self.tdr_velocity_dropdown.addItem("RG-6/U Foam 75\N{OHM SIGN} (Belden 9290) (0.81)", 0.81)
+ self.tdr_velocity_dropdown.addItem("RG-8/U PE 50\N{OHM SIGN} (Belden 8237) (0.66)", 0.66)
+ self.tdr_velocity_dropdown.addItem("RG-8/U Foam (Belden 8214) (0.78)", 0.78)
+ self.tdr_velocity_dropdown.addItem("RG-8/U (Belden 9913) (0.84)", 0.84)
+ self.tdr_velocity_dropdown.addItem("RG-8X (Belden 9258) (0.82)", 0.82)
+ self.tdr_velocity_dropdown.addItem(
+ "RG-11/U 75\N{OHM SIGN} Foam HDPE (Belden 9292) (0.84)", 0.84)
+ self.tdr_velocity_dropdown.addItem("RG-58/U 52\N{OHM SIGN} PE (Belden 9201) (0.66)", 0.66)
+ self.tdr_velocity_dropdown.addItem(
+ "RG-58A/U 54\N{OHM SIGN} Foam (Belden 8219) (0.73)", 0.73)
+ self.tdr_velocity_dropdown.addItem("RG-59A/U PE 75\N{OHM SIGN} (Belden 8241) (0.66)", 0.66)
+ self.tdr_velocity_dropdown.addItem(
+ "RG-59A/U Foam 75\N{OHM SIGN} (Belden 8241F) (0.78)", 0.78)
+ self.tdr_velocity_dropdown.addItem("RG-174 PE (Belden 8216)(0.66)", 0.66)
+ self.tdr_velocity_dropdown.addItem("RG-174 Foam (Belden 7805R) (0.735)", 0.735)
+ self.tdr_velocity_dropdown.addItem("RG-213/U PE (Belden 8267) (0.66)", 0.66)
+ self.tdr_velocity_dropdown.addItem("RG316 (0.695)", 0.695)
+ self.tdr_velocity_dropdown.addItem("RG402 (0.695)", 0.695)
+ self.tdr_velocity_dropdown.addItem("LMR-240 (0.84)", 0.84)
+ self.tdr_velocity_dropdown.addItem("LMR-240UF (0.80)", 0.80)
+ self.tdr_velocity_dropdown.addItem("LMR-400 (0.85)", 0.85)
+ self.tdr_velocity_dropdown.addItem("LMR400UF (0.83)", 0.83)
+ self.tdr_velocity_dropdown.addItem("Davis Bury-FLEX (0.82)", 0.82)
+ self.tdr_velocity_dropdown.insertSeparator(self.tdr_velocity_dropdown.count())
+ self.tdr_velocity_dropdown.addItem("Custom", -1)
+
+ self.tdr_velocity_dropdown.setCurrentIndex(1) # Default to PE (0.66)
+
+ self.tdr_velocity_dropdown.currentIndexChanged.connect(self.updateTDR)
+
+ layout.addRow(self.tdr_velocity_dropdown)
+
+ self.tdr_velocity_input = QtWidgets.QLineEdit()
+ self.tdr_velocity_input.setDisabled(True)
+ self.tdr_velocity_input.setText("0.66")
+ self.tdr_velocity_input.textChanged.connect(self.app.dataUpdated)
+
+ layout.addRow("Velocity factor", self.tdr_velocity_input)
+
+ self.tdr_result_label = QtWidgets.QLabel()
+ layout.addRow("Estimated cable length:", self.tdr_result_label)
+
+ layout.addRow(self.app.tdr_chart)
+
+ def updateTDR(self):
+ c = 299792458
+ # TODO: Let the user select whether to use high or low resolution TDR?
+ FFT_POINTS = 2**14
+
+ if len(self.app.data) < 2:
+ return
+
+ if self.tdr_velocity_dropdown.currentData() == -1:
+ self.tdr_velocity_input.setDisabled(False)
+ else:
+ self.tdr_velocity_input.setDisabled(True)
+ self.tdr_velocity_input.setText(str(self.tdr_velocity_dropdown.currentData()))
+
+ try:
+ v = float(self.tdr_velocity_input.text())
+ except ValueError:
+ return
+
+ step_size = self.app.data[1].freq - self.app.data[0].freq
+ if step_size == 0:
+ self.tdr_result_label.setText("")
+ logger.info("Cannot compute cable length at 0 span")
+ return
+
+ s11 = []
+ for d in self.app.data:
+ s11.append(np.complex(d.re, d.im))
+
+ window = np.blackman(len(self.app.data))
+
+ windowed_s11 = window * s11
+ self.td = np.abs(np.fft.ifft(windowed_s11, FFT_POINTS))
+ step = np.ones(FFT_POINTS)
+ self.step_response = signal.convolve(self.td, step)
+
+ self.step_response_Z = 50 * (1 + self.step_response) / (1 - self.step_response)
+
+ time_axis = np.linspace(0, 1/step_size, FFT_POINTS)
+ self.distance_axis = time_axis * v * c
+ # peak = np.max(td)
+ # We should check that this is an actual *peak*, and not just a vague maximum
+ index_peak = np.argmax(self.td)
+
+ cable_len = round(self.distance_axis[index_peak]/2, 3)
+ feet = math.floor(cable_len / 0.3048)
+ inches = round(((cable_len / 0.3048) - feet)*12, 1)
+
+ self.tdr_result_label.setText(f"{cable_len}m ({feet}ft {inches}in)")
+ self.app.tdr_result_label.setText(str(cable_len) + " m")
+ self.updated.emit()
diff --git a/NanoVNASaver/Windows/About.py b/NanoVNASaver/Windows/About.py
new file mode 100644
index 0000000..6f699e4
--- /dev/null
+++ b/NanoVNASaver/Windows/About.py
@@ -0,0 +1,189 @@
+# NanoVNASaver
+# A python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019. Rune B. Broberg
+#
+# 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 json
+from time import strftime, localtime
+from urllib import request, error
+
+from PyQt5 import QtWidgets, QtCore
+
+from NanoVNASaver.Hardware import Version
+
+logger = logging.getLogger(__name__)
+
+class AboutWindow(QtWidgets.QWidget):
+ def __init__(self, app: QtWidgets.QWidget):
+ super().__init__()
+ self.app = app
+
+ self.setWindowTitle("About NanoVNASaver")
+ self.setWindowIcon(self.app.icon)
+ top_layout = QtWidgets.QHBoxLayout()
+ self.setLayout(top_layout)
+ QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
+
+ icon_layout = QtWidgets.QVBoxLayout()
+ top_layout.addLayout(icon_layout)
+ icon = QtWidgets.QLabel()
+ icon.setPixmap(self.app.icon.pixmap(128, 128))
+ icon_layout.addWidget(icon)
+ icon_layout.addStretch()
+
+ layout = QtWidgets.QVBoxLayout()
+ top_layout.addLayout(layout)
+
+ layout.addWidget(QtWidgets.QLabel(
+ f"NanoVNASaver version {self.app.version}"))
+ layout.addWidget(QtWidgets.QLabel(""))
+ layout.addWidget(QtWidgets.QLabel(
+ "\N{COPYRIGHT SIGN} Copyright 2019 Rune B. Broberg"))
+ layout.addWidget(QtWidgets.QLabel(
+ "This program comes with ABSOLUTELY NO WARRANTY"))
+ layout.addWidget(QtWidgets.QLabel(
+ "This program is licensed under the GNU General Public License version 3"))
+ layout.addWidget(QtWidgets.QLabel(""))
+ link_label = QtWidgets.QLabel(
+ "For further details, see: "
+ "https://mihtjel.github.io/nanovna-saver/")
+ link_label.setOpenExternalLinks(True)
+ layout.addWidget(link_label)
+ layout.addWidget(QtWidgets.QLabel(""))
+
+ self.versionLabel = QtWidgets.QLabel("NanoVNA Firmware Version: Not connected.")
+ layout.addWidget(self.versionLabel)
+
+ layout.addStretch()
+
+ btn_check_version = QtWidgets.QPushButton("Check for updates")
+ btn_check_version.clicked.connect(self.findUpdates)
+
+ self.updateLabel = QtWidgets.QLabel("Last checked: ")
+ self.updateCheckBox = QtWidgets.QCheckBox("Check for updates on startup")
+
+ self.updateCheckBox.toggled.connect(self.updateSettings)
+
+ check_for_updates = self.app.settings.value("CheckForUpdates", "Ask")
+ if check_for_updates == "Yes":
+ self.updateCheckBox.setChecked(True)
+ self.findUpdates(automatic=True)
+ elif check_for_updates == "No":
+ self.updateCheckBox.setChecked(False)
+ else:
+ logger.debug("Starting timer")
+ QtCore.QTimer.singleShot(2000, self.askAboutUpdates)
+
+ update_hbox = QtWidgets.QHBoxLayout()
+ update_hbox.addWidget(btn_check_version)
+ update_form = QtWidgets.QFormLayout()
+ update_hbox.addLayout(update_form)
+ update_hbox.addStretch()
+ update_form.addRow(self.updateLabel)
+ update_form.addRow(self.updateCheckBox)
+ layout.addLayout(update_hbox)
+
+ layout.addStretch()
+
+ btn_ok = QtWidgets.QPushButton("Ok")
+ btn_ok.clicked.connect(lambda: self.close()) # noqa
+ layout.addWidget(btn_ok)
+
+ def show(self):
+ super().show()
+ self.updateLabels()
+
+ def updateLabels(self):
+ if self.app.vna.isValid():
+ logger.debug("Valid VNA")
+ v: Version = self.app.vna.version
+ self.versionLabel.setText(
+ f"NanoVNA Firmware Version: {self.app.vna.name}"
+ f"{v.version_string}")
+
+ def updateSettings(self):
+ if self.updateCheckBox.isChecked():
+ self.app.settings.setValue("CheckForUpdates", "Yes")
+ else:
+ self.app.settings.setValue("CheckForUpdates", "No")
+
+ def askAboutUpdates(self):
+ logger.debug("Asking about automatic update checks")
+ selection = QtWidgets.QMessageBox.question(
+ self.app,
+ "Enable checking for updates?",
+ "Would you like NanoVNA-Saver to check for updates automatically?")
+ if selection == QtWidgets.QMessageBox.Yes:
+ self.updateCheckBox.setChecked(True)
+ self.app.settings.setValue("CheckForUpdates", "Yes")
+ self.findUpdates()
+ elif selection == QtWidgets.QMessageBox.No:
+ self.updateCheckBox.setChecked(False)
+ self.app.settings.setValue("CheckForUpdates", "No")
+ QtWidgets.QMessageBox.information(
+ self.app,
+ "Checking for updates disabled",
+ "You can check for updates using the \"About\" window.")
+ else:
+ self.app.settings.setValue("CheckForUpdates", "Ask")
+
+ def findUpdates(self, automatic=False):
+ update_url = "http://mihtjel.dk/nanovna-saver/latest.json"
+
+ try:
+ req = request.Request(update_url)
+ req.add_header('User-Agent', "NanoVNA-Saver/" + self.app.version)
+ updates = json.load(request.urlopen(req, timeout=3))
+ latest_version = Version(updates['version'])
+ latest_url = updates['url']
+ except error.HTTPError as e:
+ logger.exception("Checking for updates produced an HTTP exception: %s", e)
+ self.updateLabel.setText("Connection error.")
+ return
+ except json.JSONDecodeError as e:
+ logger.exception("Checking for updates provided an unparseable file: %s", e)
+ self.updateLabel.setText("Data error reading versions.")
+ return
+ except error.URLError as e:
+ logger.exception("Checking for updates produced a URL exception: %s", e)
+ self.updateLabel.setText("Connection error.")
+ return
+
+ logger.info("Latest version is %s", latest_version.version_string)
+ this_version = Version(self.app.version)
+ logger.info("This is %s", this_version)
+ if latest_version > this_version:
+ logger.info("New update available: %s!", latest_version)
+ if automatic:
+ QtWidgets.QMessageBox.information(
+ self,
+ "Updates available",
+ "There is a new update for NanoVNA-Saver available!\n" +
+ "Version " + latest_version.version_string + "\n\n" +
+ "Press \"About\" to find the update.")
+ else:
+ QtWidgets.QMessageBox.information(
+ self, "Updates available",
+ "There is a new update for NanoVNA-Saver available!")
+ self.updateLabel.setText(
+ f'New version available.')
+ self.updateLabel.setOpenExternalLinks(True)
+ else:
+ # Probably don't show a message box, just update the screen?
+ # Maybe consider showing it if not an automatic update.
+ #
+ self.updateLabel.setText(
+ f"Last checked: {strftime('%Y-%m-%d %H:%M:%S', localtime())}")
+ return
diff --git a/NanoVNASaver/Windows/AnalysisWindow.py b/NanoVNASaver/Windows/AnalysisWindow.py
new file mode 100644
index 0000000..fd5c842
--- /dev/null
+++ b/NanoVNASaver/Windows/AnalysisWindow.py
@@ -0,0 +1,100 @@
+# NanoVNASaver
+# A python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019. Rune B. Broberg
+#
+# 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
+
+from PyQt5 import QtWidgets, QtCore
+
+from NanoVNASaver.Analysis import Analysis, LowPassAnalysis, HighPassAnalysis, \
+ BandPassAnalysis, BandStopAnalysis, VSWRAnalysis, \
+ SimplePeakSearchAnalysis
+
+logger = logging.getLogger(__name__)
+
+
+class AnalysisWindow(QtWidgets.QWidget):
+ analyses = []
+ analysis: Analysis = None
+
+ def __init__(self, app: QtWidgets.QWidget):
+ super().__init__()
+
+ self.app = app
+ self.setWindowTitle("Sweep analysis")
+ self.setWindowIcon(self.app.icon)
+
+ QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
+
+ layout = QtWidgets.QVBoxLayout()
+ self.setLayout(layout)
+
+ select_analysis_box = QtWidgets.QGroupBox("Select analysis")
+ select_analysis_layout = QtWidgets.QFormLayout(select_analysis_box)
+ self.analysis_list = QtWidgets.QComboBox()
+ self.analysis_list.addItem("Low-pass filter", LowPassAnalysis(self.app))
+ self.analysis_list.addItem("Band-pass filter", BandPassAnalysis(self.app))
+ self.analysis_list.addItem("High-pass filter", HighPassAnalysis(self.app))
+ self.analysis_list.addItem("Band-stop filter", BandStopAnalysis(self.app))
+ # self.analysis_list.addItem("Peak search", PeakSearchAnalysis(self.app))
+ self.analysis_list.addItem("Peak search", SimplePeakSearchAnalysis(self.app))
+ self.analysis_list.addItem("VSWR analysis", VSWRAnalysis(self.app))
+ select_analysis_layout.addRow("Analysis type", self.analysis_list)
+ self.analysis_list.currentIndexChanged.connect(self.updateSelection)
+
+ btn_run_analysis = QtWidgets.QPushButton("Run analysis")
+ btn_run_analysis.clicked.connect(self.runAnalysis)
+ select_analysis_layout.addRow(btn_run_analysis)
+
+ self.checkbox_run_automatically = QtWidgets.QCheckBox("Run automatically")
+ self.checkbox_run_automatically.stateChanged.connect(self.toggleAutomaticRun)
+ select_analysis_layout.addRow(self.checkbox_run_automatically)
+
+ analysis_box = QtWidgets.QGroupBox("Analysis")
+ analysis_box.setSizePolicy(
+ QtWidgets.QSizePolicy.MinimumExpanding,
+ QtWidgets.QSizePolicy.MinimumExpanding)
+
+ self.analysis_layout = QtWidgets.QVBoxLayout(analysis_box)
+ self.analysis_layout.setContentsMargins(0, 0, 0, 0)
+
+ layout.addWidget(select_analysis_box)
+ layout.addWidget(analysis_box)
+
+ self.updateSelection()
+
+ def runAnalysis(self):
+ if self.analysis is not None:
+ self.analysis.runAnalysis()
+
+ def updateSelection(self):
+ self.analysis = self.analysis_list.currentData()
+ old_item = self.analysis_layout.itemAt(0)
+ if old_item is not None:
+ old_widget = self.analysis_layout.itemAt(0).widget()
+ self.analysis_layout.replaceWidget(old_widget, self.analysis.widget())
+ old_widget.hide()
+ else:
+ self.analysis_layout.addWidget(self.analysis.widget())
+ self.analysis.widget().show()
+ self.update()
+
+ def toggleAutomaticRun(self, state: QtCore.Qt.CheckState):
+ if state == QtCore.Qt.Checked:
+ self.analysis_list.setDisabled(True)
+ self.app.dataAvailable.connect(self.runAnalysis)
+ else:
+ self.analysis_list.setDisabled(False)
+ self.app.dataAvailable.disconnect(self.runAnalysis)
diff --git a/NanoVNASaver/Windows/Bands.py b/NanoVNASaver/Windows/Bands.py
new file mode 100644
index 0000000..5a880a6
--- /dev/null
+++ b/NanoVNASaver/Windows/Bands.py
@@ -0,0 +1,69 @@
+# NanoVNASaver
+# A python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019. Rune B. Broberg
+#
+# 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
+
+from PyQt5 import QtWidgets, QtCore
+
+logger = logging.getLogger(__name__)
+
+
+class BandsWindow(QtWidgets.QWidget):
+ def __init__(self, app: QtWidgets.QWidget):
+ super().__init__()
+
+ self.app = app
+ self.setWindowTitle("Manage bands")
+ self.setWindowIcon(self.app.icon)
+
+ QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
+
+ layout = QtWidgets.QVBoxLayout()
+ self.setLayout(layout)
+ self.setMinimumSize(500, 300)
+
+ self.bands_table = QtWidgets.QTableView()
+ self.bands_table.setModel(self.app.bands)
+ self.bands_table.horizontalHeader().setStretchLastSection(True)
+
+ layout.addWidget(self.bands_table)
+
+ btn_add_row = QtWidgets.QPushButton("Add row")
+ btn_delete_row = QtWidgets.QPushButton("Delete row")
+ btn_reset_bands = QtWidgets.QPushButton("Reset bands")
+ btn_layout = QtWidgets.QHBoxLayout()
+ btn_layout.addWidget(btn_add_row)
+ btn_layout.addWidget(btn_delete_row)
+ btn_layout.addWidget(btn_reset_bands)
+ layout.addLayout(btn_layout)
+
+ btn_add_row.clicked.connect(self.app.bands.addRow)
+ btn_delete_row.clicked.connect(self.deleteRows)
+ btn_reset_bands.clicked.connect(self.resetBands)
+
+ def deleteRows(self):
+ rows = self.bands_table.selectedIndexes()
+ for row in rows:
+ self.app.bands.removeRow(row.row())
+
+ def resetBands(self):
+ confirm = QtWidgets.QMessageBox(
+ QtWidgets.QMessageBox.Warning,
+ "Confirm reset",
+ "Are you sure you want to reset the bands to default?",
+ QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Cancel).exec()
+ if confirm == QtWidgets.QMessageBox.Yes:
+ self.app.bands.resetBands()
diff --git a/NanoVNASaver/Windows/DeviceSettings.py b/NanoVNASaver/Windows/DeviceSettings.py
new file mode 100644
index 0000000..94a1750
--- /dev/null
+++ b/NanoVNASaver/Windows/DeviceSettings.py
@@ -0,0 +1,115 @@
+# NanoVNASaver
+# A python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019. Rune B. Broberg
+#
+# 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
+
+from PyQt5 import QtWidgets, QtCore
+
+from NanoVNASaver.Windows.Screenshot import ScreenshotWindow
+
+logger = logging.getLogger(__name__)
+
+class DeviceSettingsWindow(QtWidgets.QWidget):
+ def __init__(self, app: QtWidgets.QWidget):
+ super().__init__()
+
+ self.app = app
+ self.setWindowTitle("Device settings")
+ self.setWindowIcon(self.app.icon)
+
+ QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
+
+ layout = QtWidgets.QVBoxLayout()
+ self.setLayout(layout)
+
+ status_box = QtWidgets.QGroupBox("Status")
+ status_layout = QtWidgets.QFormLayout(status_box)
+ self.statusLabel = QtWidgets.QLabel("Not connected.")
+ status_layout.addRow("Status:", self.statusLabel)
+
+ self.calibrationStatusLabel = QtWidgets.QLabel("Not connected.")
+ status_layout.addRow("Calibration:", self.calibrationStatusLabel)
+
+ status_layout.addRow(QtWidgets.QLabel("Features:"))
+ self.featureList = QtWidgets.QListWidget()
+ status_layout.addRow(self.featureList)
+
+ settings_box = QtWidgets.QGroupBox("Settings")
+ settings_layout = QtWidgets.QFormLayout(settings_box)
+
+ self.chkValidateInputData = QtWidgets.QCheckBox("Validate received data")
+ validate_input = self.app.settings.value("SerialInputValidation", True, bool)
+ self.chkValidateInputData.setChecked(validate_input)
+ self.chkValidateInputData.stateChanged.connect(self.updateValidation)
+ settings_layout.addRow("Validation", self.chkValidateInputData)
+
+ control_layout = QtWidgets.QHBoxLayout()
+ self.btnRefresh = QtWidgets.QPushButton("Refresh")
+ self.btnRefresh.clicked.connect(self.updateFields)
+ control_layout.addWidget(self.btnRefresh)
+
+ self.screenshotWindow = ScreenshotWindow()
+ self.btnCaptureScreenshot = QtWidgets.QPushButton("Screenshot")
+ self.btnCaptureScreenshot.clicked.connect(self.captureScreenshot)
+ control_layout.addWidget(self.btnCaptureScreenshot)
+
+ layout.addWidget(status_box)
+ layout.addWidget(settings_box)
+ layout.addLayout(control_layout)
+
+ def show(self):
+ super().show()
+ self.updateFields()
+
+ def updateFields(self):
+ if self.app.vna.isValid():
+ self.statusLabel.setText("Connected to " + self.app.vna.name + ".")
+ if self.app.worker.running:
+ self.calibrationStatusLabel.setText("(Sweep running)")
+ else:
+ self.calibrationStatusLabel.setText(self.app.vna.getCalibration())
+
+ self.featureList.clear()
+ self.featureList.addItem(self.app.vna.name + " v" + str(self.app.vna.version))
+ features = self.app.vna.getFeatures()
+ for item in features:
+ self.featureList.addItem(item)
+
+ if "Screenshots" in features:
+ self.btnCaptureScreenshot.setDisabled(False)
+ else:
+ self.btnCaptureScreenshot.setDisabled(True)
+ else:
+ self.statusLabel.setText("Not connected.")
+ self.calibrationStatusLabel.setText("Not connected.")
+ self.featureList.clear()
+ self.featureList.addItem("Not connected.")
+ self.btnCaptureScreenshot.setDisabled(True)
+
+ def updateValidation(self, validate_data: bool):
+ self.app.vna.validateInput = validate_data
+ self.app.settings.setValue("SerialInputValidation", validate_data)
+
+ def captureScreenshot(self):
+ if not self.app.worker.running:
+ pixmap = self.app.vna.getScreenshot()
+ self.screenshotWindow.setScreenshot(pixmap)
+ self.screenshotWindow.show()
+ else:
+ # TODO: Tell the user no screenshots while sweep is running?
+ # TODO: Consider having a list of widgets that want to be
+ # disabled when a sweep is running?
+ pass
diff --git a/NanoVNASaver/Windows/DisplaySettings.py b/NanoVNASaver/Windows/DisplaySettings.py
new file mode 100644
index 0000000..6f489d0
--- /dev/null
+++ b/NanoVNASaver/Windows/DisplaySettings.py
@@ -0,0 +1,741 @@
+# NanoVNASaver - a python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019. Rune B. Broberg
+#
+# 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
+from typing import List
+
+from PyQt5 import QtWidgets, QtCore, QtGui
+
+from NanoVNASaver.Windows.Bands import BandsWindow
+from NanoVNASaver.Windows.MarkerSettings import MarkerSettingsWindow
+from NanoVNASaver.Marker import Marker
+logger = logging.getLogger(__name__)
+
+class DisplaySettingsWindow(QtWidgets.QWidget):
+ def __init__(self, app: QtWidgets.QWidget):
+ super().__init__()
+
+ self.app = app
+ self.setWindowTitle("Display settings")
+ self.setWindowIcon(self.app.icon)
+
+ self.marker_window = MarkerSettingsWindow(self.app)
+
+ QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
+
+ layout = QtWidgets.QHBoxLayout()
+ self.setLayout(layout)
+
+ left_layout = QtWidgets.QVBoxLayout()
+ layout.addLayout(left_layout)
+
+ display_options_box = QtWidgets.QGroupBox("Options")
+ display_options_layout = QtWidgets.QFormLayout(display_options_box)
+
+ self.returnloss_group = QtWidgets.QButtonGroup()
+ self.returnloss_is_negative = QtWidgets.QRadioButton("Negative")
+ self.returnloss_is_positive = QtWidgets.QRadioButton("Positive")
+ self.returnloss_group.addButton(self.returnloss_is_positive)
+ self.returnloss_group.addButton(self.returnloss_is_negative)
+
+ display_options_layout.addRow("Return loss is:", self.returnloss_is_negative)
+ display_options_layout.addRow("", self.returnloss_is_positive)
+
+ if self.app.settings.value("ReturnLossPositive", False, bool):
+ self.returnloss_is_positive.setChecked(True)
+ else:
+ self.returnloss_is_negative.setChecked(True)
+
+ self.returnloss_is_positive.toggled.connect(self.changeReturnLoss)
+ self.changeReturnLoss()
+
+ self.show_lines_option = QtWidgets.QCheckBox("Show lines")
+ show_lines_label = QtWidgets.QLabel("Displays a thin line between data points")
+ self.show_lines_option.stateChanged.connect(self.changeShowLines)
+ display_options_layout.addRow(self.show_lines_option, show_lines_label)
+
+ self.dark_mode_option = QtWidgets.QCheckBox("Dark mode")
+ dark_mode_label = QtWidgets.QLabel("Black background with white text")
+ self.dark_mode_option.stateChanged.connect(self.changeDarkMode)
+ display_options_layout.addRow(self.dark_mode_option, dark_mode_label)
+
+ self.btnColorPicker = QtWidgets.QPushButton("█")
+ self.btnColorPicker.setFixedWidth(20)
+ self.sweepColor = self.app.settings.value("SweepColor", defaultValue=QtGui.QColor(160, 140, 20, 128),
+ type=QtGui.QColor)
+ self.setSweepColor(self.sweepColor)
+ self.btnColorPicker.clicked.connect(lambda: self.setSweepColor(
+ QtWidgets.QColorDialog.getColor(self.sweepColor, options=QtWidgets.QColorDialog.ShowAlphaChannel)))
+
+ display_options_layout.addRow("Sweep color", self.btnColorPicker)
+
+ self.btnSecondaryColorPicker = QtWidgets.QPushButton("█")
+ self.btnSecondaryColorPicker.setFixedWidth(20)
+ self.secondarySweepColor = self.app.settings.value("SecondarySweepColor",
+ defaultValue=QtGui.QColor(20, 160, 140, 128),
+ type=QtGui.QColor)
+ self.setSecondarySweepColor(self.secondarySweepColor)
+ self.btnSecondaryColorPicker.clicked.connect(lambda: self.setSecondarySweepColor(
+ QtWidgets.QColorDialog.getColor(self.secondarySweepColor,
+ options=QtWidgets.QColorDialog.ShowAlphaChannel)))
+
+ display_options_layout.addRow("Second sweep color", self.btnSecondaryColorPicker)
+
+ self.btnReferenceColorPicker = QtWidgets.QPushButton("█")
+ self.btnReferenceColorPicker.setFixedWidth(20)
+ self.referenceColor = self.app.settings.value("ReferenceColor", defaultValue=QtGui.QColor(0, 0, 255, 48),
+ type=QtGui.QColor)
+ self.setReferenceColor(self.referenceColor)
+ self.btnReferenceColorPicker.clicked.connect(lambda: self.setReferenceColor(
+ QtWidgets.QColorDialog.getColor(self.referenceColor, options=QtWidgets.QColorDialog.ShowAlphaChannel)))
+
+ display_options_layout.addRow("Reference color", self.btnReferenceColorPicker)
+
+ self.btnSecondaryReferenceColorPicker = QtWidgets.QPushButton("█")
+ self.btnSecondaryReferenceColorPicker.setFixedWidth(20)
+ self.secondaryReferenceColor = self.app.settings.value("SecondaryReferenceColor",
+ defaultValue=QtGui.QColor(0, 0, 255, 48),
+ type=QtGui.QColor)
+ self.setSecondaryReferenceColor(self.secondaryReferenceColor)
+ self.btnSecondaryReferenceColorPicker.clicked.connect(lambda: self.setSecondaryReferenceColor(
+ QtWidgets.QColorDialog.getColor(self.secondaryReferenceColor,
+ options=QtWidgets.QColorDialog.ShowAlphaChannel)))
+
+ display_options_layout.addRow("Second reference color", self.btnSecondaryReferenceColorPicker)
+
+ self.pointSizeInput = QtWidgets.QSpinBox()
+ pointsize = self.app.settings.value("PointSize", 2, int)
+ self.pointSizeInput.setValue(pointsize)
+ self.changePointSize(pointsize)
+ self.pointSizeInput.setMinimum(1)
+ self.pointSizeInput.setMaximum(10)
+ self.pointSizeInput.setSuffix(" px")
+ self.pointSizeInput.setAlignment(QtCore.Qt.AlignRight)
+ self.pointSizeInput.valueChanged.connect(self.changePointSize)
+ display_options_layout.addRow("Point size", self.pointSizeInput)
+
+ self.lineThicknessInput = QtWidgets.QSpinBox()
+ linethickness = self.app.settings.value("LineThickness", 1, int)
+ self.lineThicknessInput.setValue(linethickness)
+ self.changeLineThickness(linethickness)
+ self.lineThicknessInput.setMinimum(1)
+ self.lineThicknessInput.setMaximum(10)
+ self.lineThicknessInput.setSuffix(" px")
+ self.lineThicknessInput.setAlignment(QtCore.Qt.AlignRight)
+ self.lineThicknessInput.valueChanged.connect(self.changeLineThickness)
+ display_options_layout.addRow("Line thickness", self.lineThicknessInput)
+
+ self.markerSizeInput = QtWidgets.QSpinBox()
+ markersize = self.app.settings.value("MarkerSize", 6, int)
+ self.markerSizeInput.setValue(markersize)
+ self.changeMarkerSize(markersize)
+ self.markerSizeInput.setMinimum(4)
+ self.markerSizeInput.setMaximum(20)
+ self.markerSizeInput.setSingleStep(2)
+ self.markerSizeInput.setSuffix(" px")
+ self.markerSizeInput.setAlignment(QtCore.Qt.AlignRight)
+ self.markerSizeInput.valueChanged.connect(self.changeMarkerSize)
+ self.markerSizeInput.editingFinished.connect(self.validateMarkerSize)
+ display_options_layout.addRow("Marker size", self.markerSizeInput)
+
+ self.show_marker_number_option = QtWidgets.QCheckBox("Show marker numbers")
+ show_marker_number_label = QtWidgets.QLabel("Displays the marker number next to the marker")
+ self.show_marker_number_option.stateChanged.connect(self.changeShowMarkerNumber)
+ display_options_layout.addRow(self.show_marker_number_option, show_marker_number_label)
+
+ self.filled_marker_option = QtWidgets.QCheckBox("Filled markers")
+ filled_marker_label = QtWidgets.QLabel("Shows the marker as a filled triangle")
+ self.filled_marker_option.stateChanged.connect(self.changeFilledMarkers)
+ display_options_layout.addRow(self.filled_marker_option, filled_marker_label)
+
+ self.marker_tip_group = QtWidgets.QButtonGroup()
+ self.marker_at_center = QtWidgets.QRadioButton("At the center of the marker")
+ self.marker_at_tip = QtWidgets.QRadioButton("At the tip of the marker")
+ self.marker_tip_group.addButton(self.marker_at_center)
+ self.marker_tip_group.addButton(self.marker_at_tip)
+
+ display_options_layout.addRow("Data point is:", self.marker_at_center)
+ display_options_layout.addRow("", self.marker_at_tip)
+
+ if self.app.settings.value("MarkerAtTip", False, bool):
+ self.marker_at_tip.setChecked(True)
+ else:
+ self.marker_at_center.setChecked(True)
+
+ self.marker_at_tip.toggled.connect(self.changeMarkerAtTip)
+ self.changeMarkerAtTip()
+
+ color_options_box = QtWidgets.QGroupBox("Chart colors")
+ color_options_layout = QtWidgets.QFormLayout(color_options_box)
+
+ self.use_custom_colors = QtWidgets.QCheckBox("Use custom chart colors")
+ self.use_custom_colors.stateChanged.connect(self.changeCustomColors)
+ color_options_layout.addRow(self.use_custom_colors)
+
+ self.btn_background_picker = QtWidgets.QPushButton("█")
+ self.btn_background_picker.setFixedWidth(20)
+ self.btn_background_picker.clicked.connect(
+ lambda: self.setColor(
+ "background",
+ QtWidgets.QColorDialog.getColor(
+ self.backgroundColor,
+ options=QtWidgets.QColorDialog.ShowAlphaChannel)))
+
+ color_options_layout.addRow(
+ "Chart background", self.btn_background_picker)
+
+ self.btn_foreground_picker = QtWidgets.QPushButton("█")
+ self.btn_foreground_picker.setFixedWidth(20)
+ self.btn_foreground_picker.clicked.connect(
+ lambda: self.setColor(
+ "foreground",
+ QtWidgets.QColorDialog.getColor(
+ self.foregroundColor,
+ options=QtWidgets.QColorDialog.ShowAlphaChannel)))
+
+ color_options_layout.addRow("Chart foreground", self.btn_foreground_picker)
+
+ self.btn_text_picker = QtWidgets.QPushButton("█")
+ self.btn_text_picker.setFixedWidth(20)
+ self.btn_text_picker.clicked.connect(
+ lambda: self.setColor(
+ "text",
+ QtWidgets.QColorDialog.getColor(
+ self.textColor,
+ options=QtWidgets.QColorDialog.ShowAlphaChannel)))
+
+ color_options_layout.addRow("Chart text", self.btn_text_picker)
+
+ right_layout = QtWidgets.QVBoxLayout()
+ layout.addLayout(right_layout)
+
+ font_options_box = QtWidgets.QGroupBox("Font")
+ font_options_layout = QtWidgets.QFormLayout(font_options_box)
+ self.font_dropdown = QtWidgets.QComboBox()
+ self.font_dropdown.addItems(["7", "8", "9", "10", "11", "12"])
+ font_size = self.app.settings.value("FontSize",
+ defaultValue="8",
+ type=str)
+ self.font_dropdown.setCurrentText(font_size)
+ self.changeFont()
+
+ self.font_dropdown.currentTextChanged.connect(self.changeFont)
+ font_options_layout.addRow("Font size", self.font_dropdown)
+
+ bands_box = QtWidgets.QGroupBox("Bands")
+ bands_layout = QtWidgets.QFormLayout(bands_box)
+
+ self.show_bands = QtWidgets.QCheckBox("Show bands")
+ self.show_bands.setChecked(self.app.bands.enabled)
+ self.show_bands.stateChanged.connect(lambda: self.setShowBands(self.show_bands.isChecked()))
+ bands_layout.addRow(self.show_bands)
+
+ self.btn_bands_picker = QtWidgets.QPushButton("█")
+ self.btn_bands_picker.setFixedWidth(20)
+ self.btn_bands_picker.clicked.connect(
+ lambda: self.setColor(
+ "bands",
+ QtWidgets.QColorDialog.getColor(
+ self.bandsColor,
+ options=QtWidgets.QColorDialog.ShowAlphaChannel)))
+
+ bands_layout.addRow("Chart bands", self.btn_bands_picker)
+
+ self.btn_manage_bands = QtWidgets.QPushButton("Manage bands")
+
+ self.bandsWindow = BandsWindow(self.app)
+ self.btn_manage_bands.clicked.connect(self.displayBandsWindow)
+
+ bands_layout.addRow(self.btn_manage_bands)
+
+ vswr_marker_box = QtWidgets.QGroupBox("VSWR Markers")
+ vswr_marker_layout = QtWidgets.QFormLayout(vswr_marker_box)
+
+ self.vswrMarkers: List[float] = self.app.settings.value("VSWRMarkers", [], float)
+
+ if isinstance(self.vswrMarkers, float):
+ if self.vswrMarkers == 0:
+ self.vswrMarkers = []
+ else:
+ # Single values from the .ini become floats rather than lists. Convert them.
+ self.vswrMarkers = [self.vswrMarkers]
+
+ self.btn_vswr_picker = QtWidgets.QPushButton("█")
+ self.btn_vswr_picker.setFixedWidth(20)
+ self.btn_vswr_picker.clicked.connect(
+ lambda: self.setColor(
+ "vswr",
+ QtWidgets.QColorDialog.getColor(
+ self.vswrColor,
+ options=QtWidgets.QColorDialog.ShowAlphaChannel)))
+
+ vswr_marker_layout.addRow("VSWR Markers", self.btn_vswr_picker)
+
+ self.vswr_marker_dropdown = QtWidgets.QComboBox()
+ vswr_marker_layout.addRow(self.vswr_marker_dropdown)
+
+ if len(self.vswrMarkers) == 0:
+ self.vswr_marker_dropdown.addItem("None")
+ else:
+ for m in self.vswrMarkers:
+ self.vswr_marker_dropdown.addItem(str(m))
+ for c in self.app.s11charts:
+ c.addSWRMarker(m)
+
+ self.vswr_marker_dropdown.setCurrentIndex(0)
+ btn_add_vswr_marker = QtWidgets.QPushButton("Add ...")
+ btn_remove_vswr_marker = QtWidgets.QPushButton("Remove")
+ vswr_marker_btn_layout = QtWidgets.QHBoxLayout()
+ vswr_marker_btn_layout.addWidget(btn_add_vswr_marker)
+ vswr_marker_btn_layout.addWidget(btn_remove_vswr_marker)
+ vswr_marker_layout.addRow(vswr_marker_btn_layout)
+
+ btn_add_vswr_marker.clicked.connect(self.addVSWRMarker)
+ btn_remove_vswr_marker.clicked.connect(self.removeVSWRMarker)
+
+ markers_box = QtWidgets.QGroupBox("Markers")
+ markers_layout = QtWidgets.QFormLayout(markers_box)
+
+ btn_add_marker = QtWidgets.QPushButton("Add")
+ btn_add_marker.clicked.connect(self.addMarker)
+ self.btn_remove_marker = QtWidgets.QPushButton("Remove")
+ self.btn_remove_marker.clicked.connect(self.removeMarker)
+ btn_marker_settings = QtWidgets.QPushButton("Settings ...")
+ btn_marker_settings.clicked.connect(self.displayMarkerWindow)
+
+ marker_btn_layout = QtWidgets.QHBoxLayout()
+ marker_btn_layout.addWidget(btn_add_marker)
+ marker_btn_layout.addWidget(self.btn_remove_marker)
+ marker_btn_layout.addWidget(btn_marker_settings)
+
+ markers_layout.addRow(marker_btn_layout)
+
+ charts_box = QtWidgets.QGroupBox("Displayed charts")
+ charts_layout = QtWidgets.QGridLayout(charts_box)
+
+ # selections = ["S11 Smith chart",
+ # "S11 LogMag",
+ # "S11 VSWR",
+ # "S11 Phase",
+ # "S21 Smith chart",
+ # "S21 LogMag",
+ # "S21 Phase",
+ # "None"]
+
+ selections = []
+
+ for c in self.app.selectable_charts:
+ selections.append(c.name)
+
+ selections.append("None")
+ chart00_selection = QtWidgets.QComboBox()
+ chart00_selection.addItems(selections)
+ chart00 = self.app.settings.value("Chart00", "S11 Smith Chart")
+ if chart00_selection.findText(chart00) > -1:
+ chart00_selection.setCurrentText(chart00)
+ else:
+ chart00_selection.setCurrentText("S11 Smith Chart")
+ chart00_selection.currentTextChanged.connect(lambda: self.changeChart(0, 0, chart00_selection.currentText()))
+ charts_layout.addWidget(chart00_selection, 0, 0)
+
+ chart01_selection = QtWidgets.QComboBox()
+ chart01_selection.addItems(selections)
+ chart01 = self.app.settings.value("Chart01", "S11 Return Loss")
+ if chart01_selection.findText(chart01) > -1:
+ chart01_selection.setCurrentText(chart01)
+ else:
+ chart01_selection.setCurrentText("S11 Return Loss")
+ chart01_selection.currentTextChanged.connect(lambda: self.changeChart(0, 1, chart01_selection.currentText()))
+ charts_layout.addWidget(chart01_selection, 0, 1)
+
+ chart02_selection = QtWidgets.QComboBox()
+ chart02_selection.addItems(selections)
+ chart02 = self.app.settings.value("Chart02", "None")
+ if chart02_selection.findText(chart02) > -1:
+ chart02_selection.setCurrentText(chart02)
+ else:
+ chart02_selection.setCurrentText("None")
+ chart02_selection.currentTextChanged.connect(lambda: self.changeChart(0, 2, chart02_selection.currentText()))
+ charts_layout.addWidget(chart02_selection, 0, 2)
+
+ chart10_selection = QtWidgets.QComboBox()
+ chart10_selection.addItems(selections)
+ chart10 = self.app.settings.value("Chart10", "S21 Polar Plot")
+ if chart10_selection.findText(chart10) > -1:
+ chart10_selection.setCurrentText(chart10)
+ else:
+ chart10_selection.setCurrentText("S21 Polar Plot")
+ chart10_selection.currentTextChanged.connect(lambda: self.changeChart(1, 0, chart10_selection.currentText()))
+ charts_layout.addWidget(chart10_selection, 1, 0)
+
+ chart11_selection = QtWidgets.QComboBox()
+ chart11_selection.addItems(selections)
+ chart11 = self.app.settings.value("Chart11", "S21 Gain")
+ if chart11_selection.findText(chart11) > -1:
+ chart11_selection.setCurrentText(chart11)
+ else:
+ chart11_selection.setCurrentText("S21 Gain")
+ chart11_selection.currentTextChanged.connect(lambda: self.changeChart(1, 1, chart11_selection.currentText()))
+ charts_layout.addWidget(chart11_selection, 1, 1)
+
+ chart12_selection = QtWidgets.QComboBox()
+ chart12_selection.addItems(selections)
+ chart12 = self.app.settings.value("Chart12", "None")
+ if chart12_selection.findText(chart12) > -1:
+ chart12_selection.setCurrentText(chart12)
+ else:
+ chart12_selection.setCurrentText("None")
+ chart12_selection.currentTextChanged.connect(lambda: self.changeChart(1, 2, chart12_selection.currentText()))
+ charts_layout.addWidget(chart12_selection, 1, 2)
+
+ self.changeChart(0, 0, chart00_selection.currentText())
+ self.changeChart(0, 1, chart01_selection.currentText())
+ self.changeChart(0, 2, chart02_selection.currentText())
+ self.changeChart(1, 0, chart10_selection.currentText())
+ self.changeChart(1, 1, chart11_selection.currentText())
+ self.changeChart(1, 2, chart12_selection.currentText())
+
+ self.backgroundColor = self.app.settings.value("BackgroundColor", defaultValue=QtGui.QColor("white"),
+ type=QtGui.QColor)
+ self.foregroundColor = self.app.settings.value("ForegroundColor", defaultValue=QtGui.QColor("lightgray"),
+ type=QtGui.QColor)
+ self.textColor = self.app.settings.value("TextColor", defaultValue=QtGui.QColor("black"),
+ type=QtGui.QColor)
+ self.bandsColor = self.app.settings.value("BandsColor", defaultValue=QtGui.QColor(128, 128, 128, 48),
+ type=QtGui.QColor)
+ self.app.bands.color = self.bandsColor
+ self.vswrColor = self.app.settings.value("VSWRColor", defaultValue=QtGui.QColor(192, 0, 0, 128),
+ type=QtGui.QColor)
+
+ self.dark_mode_option.setChecked(self.app.settings.value("DarkMode", False, bool))
+ self.show_lines_option.setChecked(self.app.settings.value("ShowLines", False, bool))
+ self.show_marker_number_option.setChecked(self.app.settings.value("ShowMarkerNumbers", False, bool))
+ self.filled_marker_option.setChecked(self.app.settings.value("FilledMarkers", False, bool))
+
+ if self.app.settings.value("UseCustomColors", defaultValue=False, type=bool):
+ self.dark_mode_option.setDisabled(True)
+ self.dark_mode_option.setChecked(False)
+ self.use_custom_colors.setChecked(True)
+ else:
+ self.btn_background_picker.setDisabled(True)
+ self.btn_foreground_picker.setDisabled(True)
+ self.btn_text_picker.setDisabled(True)
+
+ self.changeCustomColors() # Update all the colours of all the charts
+
+ p = self.btn_background_picker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, self.backgroundColor)
+ self.btn_background_picker.setPalette(p)
+
+ p = self.btn_foreground_picker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, self.foregroundColor)
+ self.btn_foreground_picker.setPalette(p)
+
+ p = self.btn_text_picker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, self.textColor)
+ self.btn_text_picker.setPalette(p)
+
+ p = self.btn_bands_picker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, self.bandsColor)
+ self.btn_bands_picker.setPalette(p)
+
+ p = self.btn_vswr_picker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, self.vswrColor)
+ self.btn_vswr_picker.setPalette(p)
+
+ left_layout.addWidget(display_options_box)
+ left_layout.addWidget(charts_box)
+ left_layout.addWidget(markers_box)
+ left_layout.addStretch(1)
+
+ right_layout.addWidget(color_options_box)
+ right_layout.addWidget(font_options_box)
+ right_layout.addWidget(bands_box)
+ right_layout.addWidget(vswr_marker_box)
+ right_layout.addStretch(1)
+
+ def changeChart(self, x, y, chart):
+ found = None
+ for c in self.app.selectable_charts:
+ if c.name == chart:
+ found = c
+
+ self.app.settings.setValue("Chart" + str(x) + str(y), chart)
+
+ old_widget = self.app.charts_layout.itemAtPosition(x, y)
+ if old_widget is not None:
+ w = old_widget.widget()
+ self.app.charts_layout.removeWidget(w)
+ w.hide()
+ if found is not None:
+ if self.app.charts_layout.indexOf(found) > -1:
+ logger.debug("%s is already shown, duplicating.", found.name)
+ found = self.app.copyChart(found)
+
+ self.app.charts_layout.addWidget(found, x, y)
+ if found.isHidden():
+ found.show()
+
+ def changeReturnLoss(self):
+ state = self.returnloss_is_positive.isChecked()
+ self.app.settings.setValue("ReturnLossPositive", state)
+
+ for m in self.app.markers:
+ m.returnloss_is_positive = state
+ m.updateLabels(self.app.data, self.app.data21)
+ self.marker_window.exampleMarker.returnloss_is_positive = state
+ self.marker_window.updateMarker()
+ self.app.s11LogMag.isInverted = state
+ self.app.s11LogMag.update()
+
+ def changeShowLines(self):
+ state = self.show_lines_option.isChecked()
+ self.app.settings.setValue("ShowLines", state)
+ for c in self.app.subscribing_charts:
+ c.setDrawLines(state)
+
+ def changeShowMarkerNumber(self):
+ state = self.show_marker_number_option.isChecked()
+ self.app.settings.setValue("ShowMarkerNumbers", state)
+ for c in self.app.subscribing_charts:
+ c.setDrawMarkerNumbers(state)
+
+ def changeFilledMarkers(self):
+ state = self.filled_marker_option.isChecked()
+ self.app.settings.setValue("FilledMarkers", state)
+ for c in self.app.subscribing_charts:
+ c.setFilledMarkers(state)
+
+ def changeMarkerAtTip(self):
+ state = self.marker_at_tip.isChecked()
+ self.app.settings.setValue("MarkerAtTip", state)
+ for c in self.app.subscribing_charts:
+ c.setMarkerAtTip(state)
+
+ def changePointSize(self, size: int):
+ self.app.settings.setValue("PointSize", size)
+ for c in self.app.subscribing_charts:
+ c.setPointSize(size)
+
+ def changeLineThickness(self, size: int):
+ self.app.settings.setValue("LineThickness", size)
+ for c in self.app.subscribing_charts:
+ c.setLineThickness(size)
+
+ def changeMarkerSize(self, size: int):
+ if size % 2 == 0:
+ self.app.settings.setValue("MarkerSize", size)
+ for c in self.app.subscribing_charts:
+ c.setMarkerSize(int(size / 2))
+
+ def validateMarkerSize(self):
+ size = self.markerSizeInput.value()
+ if size % 2 != 0:
+ self.markerSizeInput.setValue(size + 1)
+
+ def changeDarkMode(self):
+ state = self.dark_mode_option.isChecked()
+ self.app.settings.setValue("DarkMode", state)
+ if state:
+ for c in self.app.subscribing_charts:
+ c.setBackgroundColor(QtGui.QColor(QtCore.Qt.black))
+ c.setForegroundColor(QtGui.QColor(QtCore.Qt.lightGray))
+ c.setTextColor(QtGui.QColor(QtCore.Qt.white))
+ c.setSWRColor(self.vswrColor)
+ else:
+ for c in self.app.subscribing_charts:
+ c.setBackgroundColor(QtGui.QColor(QtCore.Qt.white))
+ c.setForegroundColor(QtGui.QColor(QtCore.Qt.lightGray))
+ c.setTextColor(QtGui.QColor(QtCore.Qt.black))
+ c.setSWRColor(self.vswrColor)
+
+ def changeCustomColors(self):
+ self.app.settings.setValue("UseCustomColors", self.use_custom_colors.isChecked())
+ if self.use_custom_colors.isChecked():
+ self.dark_mode_option.setDisabled(True)
+ self.dark_mode_option.setChecked(False)
+ self.btn_background_picker.setDisabled(False)
+ self.btn_foreground_picker.setDisabled(False)
+ self.btn_text_picker.setDisabled(False)
+ for c in self.app.subscribing_charts:
+ c.setBackgroundColor(self.backgroundColor)
+ c.setForegroundColor(self.foregroundColor)
+ c.setTextColor(self.textColor)
+ c.setSWRColor(self.vswrColor)
+ else:
+ self.dark_mode_option.setDisabled(False)
+ self.btn_background_picker.setDisabled(True)
+ self.btn_foreground_picker.setDisabled(True)
+ self.btn_text_picker.setDisabled(True)
+ self.changeDarkMode() # Reset to the default colors depending on Dark Mode setting
+
+ def setColor(self, name: str, color: QtGui.QColor):
+ if name == "background":
+ p = self.btn_background_picker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, color)
+ self.btn_background_picker.setPalette(p)
+ self.backgroundColor = color
+ self.app.settings.setValue("BackgroundColor", color)
+ elif name == "foreground":
+ p = self.btn_foreground_picker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, color)
+ self.btn_foreground_picker.setPalette(p)
+ self.foregroundColor = color
+ self.app.settings.setValue("ForegroundColor", color)
+ elif name == "text":
+ p = self.btn_text_picker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, color)
+ self.btn_text_picker.setPalette(p)
+ self.textColor = color
+ self.app.settings.setValue("TextColor", color)
+ elif name == "bands":
+ p = self.btn_bands_picker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, color)
+ self.btn_bands_picker.setPalette(p)
+ self.bandsColor = color
+ self.app.settings.setValue("BandsColor", color)
+ self.app.bands.setColor(color)
+ elif name == "vswr":
+ p = self.btn_vswr_picker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, color)
+ self.btn_vswr_picker.setPalette(p)
+ self.vswrColor = color
+ self.app.settings.setValue("VSWRColor", color)
+ self.changeCustomColors()
+
+ def setSweepColor(self, color: QtGui.QColor):
+ if color.isValid():
+ self.sweepColor = color
+ p = self.btnColorPicker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, color)
+ self.btnColorPicker.setPalette(p)
+ self.app.settings.setValue("SweepColor", color)
+ self.app.settings.sync()
+ for c in self.app.subscribing_charts:
+ c.setSweepColor(color)
+
+ def setSecondarySweepColor(self, color: QtGui.QColor):
+ if color.isValid():
+ self.secondarySweepColor = color
+ p = self.btnSecondaryColorPicker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, color)
+ self.btnSecondaryColorPicker.setPalette(p)
+ self.app.settings.setValue("SecondarySweepColor", color)
+ self.app.settings.sync()
+ for c in self.app.subscribing_charts:
+ c.setSecondarySweepColor(color)
+
+ def setReferenceColor(self, color):
+ if color.isValid():
+ self.referenceColor = color
+ p = self.btnReferenceColorPicker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, color)
+ self.btnReferenceColorPicker.setPalette(p)
+ self.app.settings.setValue("ReferenceColor", color)
+ self.app.settings.sync()
+
+ for c in self.app.subscribing_charts:
+ c.setReferenceColor(color)
+
+ def setSecondaryReferenceColor(self, color):
+ if color.isValid():
+ self.secondaryReferenceColor = color
+ p = self.btnSecondaryReferenceColorPicker.palette()
+ p.setColor(QtGui.QPalette.ButtonText, color)
+ self.btnSecondaryReferenceColorPicker.setPalette(p)
+ self.app.settings.setValue("SecondaryReferenceColor", color)
+ self.app.settings.sync()
+
+ for c in self.app.subscribing_charts:
+ c.setSecondaryReferenceColor(color)
+
+ def setShowBands(self, show_bands):
+ self.app.bands.enabled = show_bands
+ self.app.bands.settings.setValue("ShowBands", show_bands)
+ self.app.bands.settings.sync()
+ for c in self.app.subscribing_charts:
+ c.update()
+
+ def changeFont(self):
+ font_size = self.font_dropdown.currentText()
+ self.app.settings.setValue("FontSize", font_size)
+ app: QtWidgets.QApplication = QtWidgets.QApplication.instance()
+ font = app.font()
+ font.setPointSize(int(font_size))
+ app.setFont(font)
+ self.app.changeFont(font)
+
+ def displayBandsWindow(self):
+ self.bandsWindow.show()
+ QtWidgets.QApplication.setActiveWindow(self.bandsWindow)
+
+ def displayMarkerWindow(self):
+ self.marker_window.show()
+ QtWidgets.QApplication.setActiveWindow(self.marker_window)
+
+ def addMarker(self):
+ new_marker = Marker("", self.app.settings)
+ new_marker.setScale(self.app.scaleFactor)
+ self.app.markers.append(new_marker)
+ self.app.marker_data_layout.addWidget(new_marker.getGroupBox())
+
+ new_marker.updated.connect(self.app.markerUpdated)
+ label, layout = new_marker.getRow()
+ self.app.marker_control_layout.insertRow(Marker.count() - 1, label, layout)
+ self.btn_remove_marker.setDisabled(False)
+
+ def removeMarker(self):
+ # keep at least one marker
+ if Marker.count() <= 1:
+ return
+ if Marker.count() == 2:
+ self.btn_remove_marker.setDisabled(True)
+ last_marker = self.app.markers.pop()
+
+ last_marker.updated.disconnect(self.app.markerUpdated)
+ self.app.marker_data_layout.removeWidget(last_marker.getGroupBox())
+ self.app.marker_control_layout.removeRow(Marker.count()-1)
+ last_marker.getGroupBox().hide()
+ last_marker.getGroupBox().destroy()
+ label, _ = last_marker.getRow()
+ label.hide()
+
+ def addVSWRMarker(self):
+ value, selected = QtWidgets.QInputDialog.getDouble(self, "Add VSWR Marker",
+ "VSWR value to show:", min=1.001, decimals=3)
+ if selected:
+ self.vswrMarkers.append(value)
+ if self.vswr_marker_dropdown.itemText(0) == "None":
+ self.vswr_marker_dropdown.removeItem(0)
+ self.vswr_marker_dropdown.addItem(str(value))
+ self.vswr_marker_dropdown.setCurrentText(str(value))
+ for c in self.app.s11charts:
+ c.addSWRMarker(value)
+ self.app.settings.setValue("VSWRMarkers", self.vswrMarkers)
+
+ def removeVSWRMarker(self):
+ value_str = self.vswr_marker_dropdown.currentText()
+ if value_str != "None":
+ value = float(value_str)
+ self.vswrMarkers.remove(value)
+ self.vswr_marker_dropdown.removeItem(self.vswr_marker_dropdown.currentIndex())
+ if self.vswr_marker_dropdown.count() == 0:
+ self.vswr_marker_dropdown.addItem("None")
+ self.app.settings.remove("VSWRMarkers")
+ else:
+ self.app.settings.setValue("VSWRMarkers", self.vswrMarkers)
+ for c in self.app.s11charts:
+ c.removeSWRMarker(value)
+
diff --git a/NanoVNASaver/Windows/MarkerSettings.py b/NanoVNASaver/Windows/MarkerSettings.py
new file mode 100644
index 0000000..cb889f6
--- /dev/null
+++ b/NanoVNASaver/Windows/MarkerSettings.py
@@ -0,0 +1,154 @@
+# NanoVNASaver
+# A python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019. Rune B. Broberg
+#
+# 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
+
+from PyQt5 import QtWidgets, QtCore, QtGui
+
+from NanoVNASaver.RFTools import Datapoint
+from NanoVNASaver.Marker import Marker
+from NanoVNASaver.Marker.Values import TYPES, default_label_ids
+
+logger = logging.getLogger(__name__)
+
+
+class MarkerSettingsWindow(QtWidgets.QWidget):
+ exampleData11 = [Datapoint(123000000, 0.89, -0.11),
+ Datapoint(123500000, 0.9, -0.1),
+ Datapoint(124000000, 0.91, -0.95)]
+ exampleData21 = [Datapoint(123000000, -0.25, 0.49),
+ Datapoint(123456000, -0.3, 0.5),
+ Datapoint(124000000, -0.2, 0.5)]
+
+ def __init__(self, app: QtWidgets.QWidget):
+ super().__init__()
+ self.app = app
+
+ self.setWindowTitle("Marker settings")
+ self.setWindowIcon(self.app.icon)
+
+ QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.cancelButtonClick)
+
+ self.exampleMarker = Marker("Example marker")
+ layout = QtWidgets.QVBoxLayout()
+ self.setLayout(layout)
+
+ settings_group_box = QtWidgets.QGroupBox("Settings")
+ settings_group_box_layout = QtWidgets.QFormLayout(settings_group_box)
+ self.checkboxColouredMarker = QtWidgets.QCheckBox("Colored marker name")
+ self.checkboxColouredMarker.setChecked(self.app.settings.value("ColoredMarkerNames", True, bool))
+ self.checkboxColouredMarker.stateChanged.connect(self.updateMarker)
+ settings_group_box_layout.addRow(self.checkboxColouredMarker)
+
+ fields_group_box = QtWidgets.QGroupBox("Displayed data")
+ fields_group_box_layout = QtWidgets.QFormLayout(fields_group_box)
+
+ self.savedFieldSelection = self.app.settings.value(
+ "MarkerFields", defaultValue=default_label_ids()
+ )
+
+ if self.savedFieldSelection == "":
+ self.savedFieldSelection = []
+
+ self.currentFieldSelection = self.savedFieldSelection[:]
+
+ self.active_labels_view = QtWidgets.QListView()
+ self.update_displayed_data_form()
+
+ fields_group_box_layout.addRow(self.active_labels_view)
+
+ layout.addWidget(settings_group_box)
+ layout.addWidget(fields_group_box)
+ layout.addWidget(self.exampleMarker.getGroupBox())
+
+ btn_layout = QtWidgets.QHBoxLayout()
+ layout.addLayout(btn_layout)
+ btn_ok = QtWidgets.QPushButton("OK")
+ btn_apply = QtWidgets.QPushButton("Apply")
+ btn_default = QtWidgets.QPushButton("Defaults")
+ btn_cancel = QtWidgets.QPushButton("Cancel")
+
+ btn_ok.clicked.connect(self.okButtonClick)
+ btn_apply.clicked.connect(self.applyButtonClick)
+ btn_default.clicked.connect(self.defaultButtonClick)
+ btn_cancel.clicked.connect(self.cancelButtonClick)
+
+ btn_layout.addWidget(btn_ok)
+ btn_layout.addWidget(btn_apply)
+ btn_layout.addWidget(btn_default)
+ btn_layout.addWidget(btn_cancel)
+
+ self.updateMarker()
+ for m in self.app.markers:
+ m.setFieldSelection(self.currentFieldSelection)
+ m.setColoredText(self.checkboxColouredMarker.isChecked())
+
+ def updateMarker(self):
+ self.exampleMarker.setFrequency(123456000)
+ self.exampleMarker.setColoredText(self.checkboxColouredMarker.isChecked())
+ self.exampleMarker.setFieldSelection(self.currentFieldSelection)
+ self.exampleMarker.findLocation(self.exampleData11)
+ self.exampleMarker.resetLabels()
+ self.exampleMarker.updateLabels(self.exampleData11, self.exampleData21)
+
+ def updateField(self, field: QtGui.QStandardItem):
+ if field.checkState() == QtCore.Qt.Checked:
+ if not field.data() in self.currentFieldSelection:
+ self.currentFieldSelection = []
+ for i in range(self.model.rowCount()):
+ field = self.model.item(i, 0)
+ if field.checkState() == QtCore.Qt.Checked:
+ self.currentFieldSelection.append(field.data())
+ else:
+ if field.data() in self.currentFieldSelection:
+ self.currentFieldSelection.remove(field.data())
+ self.updateMarker()
+
+ def applyButtonClick(self):
+ self.savedFieldSelection = self.currentFieldSelection[:]
+ self.app.settings.setValue("MarkerFields", self.savedFieldSelection)
+ self.app.settings.setValue("ColoredMarkerNames", self.checkboxColouredMarker.isChecked())
+ for m in self.app.markers:
+ m.setFieldSelection(self.savedFieldSelection)
+ m.setColoredText(self.checkboxColouredMarker.isChecked())
+
+ def okButtonClick(self):
+ self.applyButtonClick()
+ self.close()
+
+ def cancelButtonClick(self):
+ self.currentFieldSelection = self.savedFieldSelection[:]
+ self.update_displayed_data_form()
+ self.updateMarker()
+ self.close()
+
+ def defaultButtonClick(self):
+ self.currentFieldSelection = default_label_ids()
+ self.update_displayed_data_form()
+ self.updateMarker()
+
+ def update_displayed_data_form(self):
+ self.model = QtGui.QStandardItemModel()
+ for label in TYPES:
+ item = QtGui.QStandardItem(label.description)
+ item.setData(label.label_id)
+ item.setCheckable(True)
+ item.setEditable(False)
+ if label.label_id in self.currentFieldSelection:
+ item.setCheckState(QtCore.Qt.Checked)
+ self.model.appendRow(item)
+ self.active_labels_view.setModel(self.model)
+ self.model.itemChanged.connect(self.updateField)
diff --git a/NanoVNASaver/Windows/Screenshot.py b/NanoVNASaver/Windows/Screenshot.py
new file mode 100644
index 0000000..5d68617
--- /dev/null
+++ b/NanoVNASaver/Windows/Screenshot.py
@@ -0,0 +1,99 @@
+# NanoVNASaver
+# A python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019. Rune B. Broberg
+#
+# 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
+from PyQt5 import QtWidgets, QtCore, QtGui
+
+logger = logging.getLogger(__name__)
+
+
+class ScreenshotWindow(QtWidgets.QLabel):
+ pix = None
+
+ def __init__(self):
+ super().__init__()
+ self.setWindowTitle("Screenshot")
+ # TODO : self.setWindowIcon(self.app.icon)
+
+ QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
+ self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
+
+ self.action_original_size = QtWidgets.QAction("Original size")
+ self.action_original_size.triggered.connect(lambda: self.setScale(1))
+ self.action_2x_size = QtWidgets.QAction("2x size")
+ self.action_2x_size.triggered.connect(lambda: self.setScale(2))
+ self.action_3x_size = QtWidgets.QAction("3x size")
+ self.action_3x_size.triggered.connect(lambda: self.setScale(3))
+ self.action_4x_size = QtWidgets.QAction("4x size")
+ self.action_4x_size.triggered.connect(lambda: self.setScale(4))
+ self.action_5x_size = QtWidgets.QAction("5x size")
+ self.action_5x_size.triggered.connect(lambda: self.setScale(5))
+
+ self.addAction(self.action_original_size)
+ self.addAction(self.action_2x_size)
+ self.addAction(self.action_3x_size)
+ self.addAction(self.action_4x_size)
+ self.addAction(self.action_5x_size)
+ self.action_save_screenshot = QtWidgets.QAction("Save image")
+ self.action_save_screenshot.triggered.connect(self.saveScreenshot)
+ self.addAction(self.action_save_screenshot)
+
+ def setScreenshot(self, pixmap: QtGui.QPixmap):
+ if self.pix is None:
+ self.resize(pixmap.size())
+ self.pix = pixmap
+ self.setPixmap(
+ self.pix.scaled(
+ self.size(),
+ QtCore.Qt.KeepAspectRatio,
+ QtCore.Qt.FastTransformation))
+ w, h = pixmap.width(), pixmap.height()
+ self.action_original_size.setText(
+ "Original size (" + str(w) + "x" + str(h) + ")")
+ self.action_2x_size.setText(
+ "2x size (" + str(w * 2) + "x" + str(h * 2) + ")")
+ self.action_3x_size.setText(
+ "3x size (" + str(w * 3) + "x" + str(h * 3) + ")")
+ self.action_4x_size.setText(
+ "4x size (" + str(w * 4) + "x" + str(h * 4) + ")")
+ self.action_5x_size.setText(
+ "5x size (" + str(w * 5) + "x" + str(h * 5) + ")")
+
+ def saveScreenshot(self):
+ if self.pix is not None:
+ logger.info("Saving screenshot to file...")
+ filename, _ = QtWidgets.QFileDialog.getSaveFileName(
+ parent=self, caption="Save image",
+ filter="PNG (*.png);;All files (*.*)")
+
+ logger.debug("Filename: %s", filename)
+ if filename != "":
+ self.pixmap().save(filename)
+ else:
+ logger.warning("The user got shown an empty screenshot window?")
+
+ def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
+ super().resizeEvent(a0)
+ if self.pixmap() is not None:
+ self.setPixmap(
+ self.pix.scaled(
+ self.size(),
+ QtCore.Qt.KeepAspectRatio,
+ QtCore.Qt.FastTransformation))
+
+ def setScale(self, scale):
+ width, height = self.pix.size().width() * scale, self.pix.size().height() * scale
+ self.resize(width, height)
diff --git a/NanoVNASaver/Windows/SweepSettings.py b/NanoVNASaver/Windows/SweepSettings.py
new file mode 100644
index 0000000..99d002c
--- /dev/null
+++ b/NanoVNASaver/Windows/SweepSettings.py
@@ -0,0 +1,176 @@
+# NanoVNASaver
+# A python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019. Rune B. Broberg
+#
+# 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
+
+from PyQt5 import QtWidgets, QtCore
+
+from NanoVNASaver.RFTools import RFTools
+
+logger = logging.getLogger(__name__)
+
+
+class SweepSettingsWindow(QtWidgets.QWidget):
+ def __init__(self, app: QtWidgets.QWidget):
+ super().__init__()
+
+ self.app = app
+ self.setWindowTitle("Sweep settings")
+ self.setWindowIcon(self.app.icon)
+
+ QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
+
+ layout = QtWidgets.QVBoxLayout()
+ self.setLayout(layout)
+
+ title_box = QtWidgets.QGroupBox("Sweep name")
+ title_layout = QtWidgets.QFormLayout(title_box)
+ self.sweep_title_input = QtWidgets.QLineEdit()
+ title_layout.addRow("Sweep name", self.sweep_title_input)
+ title_button_layout = QtWidgets.QHBoxLayout()
+ btn_set_sweep_title = QtWidgets.QPushButton("Set")
+ btn_set_sweep_title.clicked.connect(
+ lambda: self.app.setSweepTitle(self.sweep_title_input.text()))
+ btn_reset_sweep_title = QtWidgets.QPushButton("Reset")
+ btn_reset_sweep_title.clicked.connect(lambda: self.app.setSweepTitle(""))
+ title_button_layout.addWidget(btn_set_sweep_title)
+ title_button_layout.addWidget(btn_reset_sweep_title)
+ title_layout.addRow(title_button_layout)
+ layout.addWidget(title_box)
+
+ settings_box = QtWidgets.QGroupBox("Settings")
+ settings_layout = QtWidgets.QFormLayout(settings_box)
+
+ self.single_sweep_radiobutton = QtWidgets.QRadioButton("Single sweep")
+ self.continuous_sweep_radiobutton = QtWidgets.QRadioButton("Continuous sweep")
+ self.averaged_sweep_radiobutton = QtWidgets.QRadioButton("Averaged sweep")
+
+ settings_layout.addWidget(self.single_sweep_radiobutton)
+ self.single_sweep_radiobutton.setChecked(True)
+ settings_layout.addWidget(self.continuous_sweep_radiobutton)
+ settings_layout.addWidget(self.averaged_sweep_radiobutton)
+
+ self.averages = QtWidgets.QLineEdit("3")
+ self.truncates = QtWidgets.QLineEdit("0")
+
+ settings_layout.addRow("Number of measurements to average", self.averages)
+ settings_layout.addRow("Number to discard", self.truncates)
+ settings_layout.addRow(
+ QtWidgets.QLabel(
+ "Averaging allows discarding outlying samples to get better averages."))
+ settings_layout.addRow(
+ QtWidgets.QLabel("Common values are 3/0, 5/2, 9/4 and 25/6."))
+
+ self.continuous_sweep_radiobutton.toggled.connect(
+ lambda: self.app.worker.setContinuousSweep(
+ self.continuous_sweep_radiobutton.isChecked()))
+ self.averaged_sweep_radiobutton.toggled.connect(self.updateAveraging)
+ self.averages.textEdited.connect(self.updateAveraging)
+ self.truncates.textEdited.connect(self.updateAveraging)
+
+ layout.addWidget(settings_box)
+
+ band_sweep_box = QtWidgets.QGroupBox("Sweep band")
+ band_sweep_layout = QtWidgets.QFormLayout(band_sweep_box)
+
+ self.band_list = QtWidgets.QComboBox()
+ self.band_list.setModel(self.app.bands)
+ self.band_list.currentIndexChanged.connect(self.updateCurrentBand)
+
+ band_sweep_layout.addRow("Select band", self.band_list)
+
+ self.band_pad_group = QtWidgets.QButtonGroup()
+ self.band_pad_0 = QtWidgets.QRadioButton("None")
+ self.band_pad_10 = QtWidgets.QRadioButton("10%")
+ self.band_pad_25 = QtWidgets.QRadioButton("25%")
+ self.band_pad_100 = QtWidgets.QRadioButton("100%")
+ self.band_pad_0.setChecked(True)
+ self.band_pad_group.addButton(self.band_pad_0)
+ self.band_pad_group.addButton(self.band_pad_10)
+ self.band_pad_group.addButton(self.band_pad_25)
+ self.band_pad_group.addButton(self.band_pad_100)
+ self.band_pad_group.buttonClicked.connect(self.updateCurrentBand)
+ band_sweep_layout.addRow("Pad band limits", self.band_pad_0)
+ band_sweep_layout.addRow("", self.band_pad_10)
+ band_sweep_layout.addRow("", self.band_pad_25)
+ band_sweep_layout.addRow("", self.band_pad_100)
+
+ self.band_limit_label = QtWidgets.QLabel()
+
+ band_sweep_layout.addRow(self.band_limit_label)
+
+ btn_set_band_sweep = QtWidgets.QPushButton("Set band sweep")
+ btn_set_band_sweep.clicked.connect(self.setBandSweep)
+ band_sweep_layout.addRow(btn_set_band_sweep)
+
+ self.updateCurrentBand()
+
+ layout.addWidget(band_sweep_box)
+
+ def updateCurrentBand(self):
+ index_start = self.band_list.model().index(self.band_list.currentIndex(), 1)
+ index_stop = self.band_list.model().index(self.band_list.currentIndex(), 2)
+ start = int(self.band_list.model().data(index_start, QtCore.Qt.ItemDataRole).value())
+ stop = int(self.band_list.model().data(index_stop, QtCore.Qt.ItemDataRole).value())
+
+ if self.band_pad_10.isChecked():
+ padding = 10
+ elif self.band_pad_25.isChecked():
+ padding = 25
+ elif self.band_pad_100.isChecked():
+ padding = 100
+ else:
+ padding = 0
+
+ if padding > 0:
+ span = stop - start
+ start -= round(span * padding / 100)
+ start = max(1, start)
+ stop += round(span * padding / 100)
+
+ self.band_limit_label.setText(
+ f"Sweep span: {RFTools.formatShortFrequency(start)}"
+ f" to {RFTools.formatShortFrequency(stop)}")
+
+ def setBandSweep(self):
+ index_start = self.band_list.model().index(self.band_list.currentIndex(), 1)
+ index_stop = self.band_list.model().index(self.band_list.currentIndex(), 2)
+ start = int(self.band_list.model().data(index_start, QtCore.Qt.ItemDataRole).value())
+ stop = int(self.band_list.model().data(index_stop, QtCore.Qt.ItemDataRole).value())
+
+ if self.band_pad_10.isChecked():
+ padding = 10
+ elif self.band_pad_25.isChecked():
+ padding = 25
+ elif self.band_pad_100.isChecked():
+ padding = 100
+ else:
+ padding = 0
+
+ if padding > 0:
+ span = stop - start
+ start -= round(span * padding / 100)
+ start = max(1, start)
+ stop += round(span * padding / 100)
+
+ self.app.sweepStartInput.setText(RFTools.formatSweepFrequency(start))
+ self.app.sweepEndInput.setText(RFTools.formatSweepFrequency(stop))
+ self.app.sweepEndInput.textEdited.emit(self.app.sweepEndInput.text())
+
+ def updateAveraging(self):
+ self.app.worker.setAveraging(self.averaged_sweep_radiobutton.isChecked(),
+ self.averages.text(),
+ self.truncates.text())
diff --git a/NanoVNASaver/Windows/TDR.py b/NanoVNASaver/Windows/TDR.py
new file mode 100644
index 0000000..458313e
--- /dev/null
+++ b/NanoVNASaver/Windows/TDR.py
@@ -0,0 +1,152 @@
+# NanoVNASaver - a python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019. Rune B. Broberg
+#
+# 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 numpy as np
+import scipy.signal as signal
+from PyQt5 import QtWidgets, QtCore
+
+
+logger = logging.getLogger(__name__)
+
+
+class TDRWindow(QtWidgets.QWidget):
+ updated = QtCore.pyqtSignal()
+
+ def __init__(self, app: QtWidgets.QWidget):
+ super().__init__()
+ self.app = app
+
+ self.td = []
+ self.distance_axis = []
+ self.step_response = []
+ self.step_response_Z = []
+
+ self.setWindowTitle("TDR")
+ self.setWindowIcon(self.app.icon)
+
+ QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
+
+ layout = QtWidgets.QFormLayout()
+ self.setLayout(layout)
+
+ self.tdr_velocity_dropdown = QtWidgets.QComboBox()
+ self.tdr_velocity_dropdown.addItem("Jelly filled (0.64)", 0.64)
+ self.tdr_velocity_dropdown.addItem("Polyethylene (0.66)", 0.66)
+ self.tdr_velocity_dropdown.addItem("PTFE (Teflon) (0.70)", 0.70)
+ self.tdr_velocity_dropdown.addItem("Pulp Insulation (0.72)", 0.72)
+ self.tdr_velocity_dropdown.addItem("Foam or Cellular PE (0.78)", 0.78)
+ self.tdr_velocity_dropdown.addItem("Semi-solid PE (SSPE) (0.84)", 0.84)
+ self.tdr_velocity_dropdown.addItem("Air (Helical spacers) (0.94)", 0.94)
+ self.tdr_velocity_dropdown.insertSeparator(self.tdr_velocity_dropdown.count())
+ # Lots of cable types added by Larry Goga, AE5CZ
+ self.tdr_velocity_dropdown.addItem("RG-6/U PE 75\N{OHM SIGN} (Belden 8215) (0.66)", 0.66)
+ self.tdr_velocity_dropdown.addItem("RG-6/U Foam 75\N{OHM SIGN} (Belden 9290) (0.81)", 0.81)
+ self.tdr_velocity_dropdown.addItem("RG-8/U PE 50\N{OHM SIGN} (Belden 8237) (0.66)", 0.66)
+ self.tdr_velocity_dropdown.addItem("RG-8/U Foam (Belden 8214) (0.78)", 0.78)
+ self.tdr_velocity_dropdown.addItem("RG-8/U (Belden 9913) (0.84)", 0.84)
+ self.tdr_velocity_dropdown.addItem("RG-8X (Belden 9258) (0.82)", 0.82)
+ self.tdr_velocity_dropdown.addItem(
+ "RG-11/U 75\N{OHM SIGN} Foam HDPE (Belden 9292) (0.84)", 0.84)
+ self.tdr_velocity_dropdown.addItem("RG-58/U 52\N{OHM SIGN} PE (Belden 9201) (0.66)", 0.66)
+ self.tdr_velocity_dropdown.addItem(
+ "RG-58A/U 54\N{OHM SIGN} Foam (Belden 8219) (0.73)", 0.73)
+ self.tdr_velocity_dropdown.addItem("RG-59A/U PE 75\N{OHM SIGN} (Belden 8241) (0.66)", 0.66)
+ self.tdr_velocity_dropdown.addItem(
+ "RG-59A/U Foam 75\N{OHM SIGN} (Belden 8241F) (0.78)", 0.78)
+ self.tdr_velocity_dropdown.addItem("RG-174 PE (Belden 8216)(0.66)", 0.66)
+ self.tdr_velocity_dropdown.addItem("RG-174 Foam (Belden 7805R) (0.735)", 0.735)
+ self.tdr_velocity_dropdown.addItem("RG-213/U PE (Belden 8267) (0.66)", 0.66)
+ self.tdr_velocity_dropdown.addItem("RG316 (0.695)", 0.695)
+ self.tdr_velocity_dropdown.addItem("RG402 (0.695)", 0.695)
+ self.tdr_velocity_dropdown.addItem("LMR-240 (0.84)", 0.84)
+ self.tdr_velocity_dropdown.addItem("LMR-240UF (0.80)", 0.80)
+ self.tdr_velocity_dropdown.addItem("LMR-400 (0.85)", 0.85)
+ self.tdr_velocity_dropdown.addItem("LMR400UF (0.83)", 0.83)
+ self.tdr_velocity_dropdown.addItem("Davis Bury-FLEX (0.82)", 0.82)
+ self.tdr_velocity_dropdown.insertSeparator(self.tdr_velocity_dropdown.count())
+ self.tdr_velocity_dropdown.addItem("Custom", -1)
+
+ self.tdr_velocity_dropdown.setCurrentIndex(1) # Default to PE (0.66)
+
+ self.tdr_velocity_dropdown.currentIndexChanged.connect(self.updateTDR)
+
+ layout.addRow(self.tdr_velocity_dropdown)
+
+ self.tdr_velocity_input = QtWidgets.QLineEdit()
+ self.tdr_velocity_input.setDisabled(True)
+ self.tdr_velocity_input.setText("0.66")
+ self.tdr_velocity_input.textChanged.connect(self.app.dataUpdated)
+
+ layout.addRow("Velocity factor", self.tdr_velocity_input)
+
+ self.tdr_result_label = QtWidgets.QLabel()
+ layout.addRow("Estimated cable length:", self.tdr_result_label)
+
+ layout.addRow(self.app.tdr_chart)
+
+ def updateTDR(self):
+ c = 299792458
+ # TODO: Let the user select whether to use high or low resolution TDR?
+ FFT_POINTS = 2**14
+
+ if len(self.app.data) < 2:
+ return
+
+ if self.tdr_velocity_dropdown.currentData() == -1:
+ self.tdr_velocity_input.setDisabled(False)
+ else:
+ self.tdr_velocity_input.setDisabled(True)
+ self.tdr_velocity_input.setText(str(self.tdr_velocity_dropdown.currentData()))
+
+ try:
+ v = float(self.tdr_velocity_input.text())
+ except ValueError:
+ return
+
+ step_size = self.app.data[1].freq - self.app.data[0].freq
+ if step_size == 0:
+ self.tdr_result_label.setText("")
+ logger.info("Cannot compute cable length at 0 span")
+ return
+
+ s11 = []
+ for d in self.app.data:
+ s11.append(np.complex(d.re, d.im))
+
+ window = np.blackman(len(self.app.data))
+
+ windowed_s11 = window * s11
+ self.td = np.abs(np.fft.ifft(windowed_s11, FFT_POINTS))
+ step = np.ones(FFT_POINTS)
+ self.step_response = signal.convolve(self.td, step)
+
+ self.step_response_Z = 50 * (1 + self.step_response) / (1 - self.step_response)
+
+ time_axis = np.linspace(0, 1/step_size, FFT_POINTS)
+ self.distance_axis = time_axis * v * c
+ # peak = np.max(td)
+ # We should check that this is an actual *peak*, and not just a vague maximum
+ index_peak = np.argmax(self.td)
+
+ cable_len = round(self.distance_axis[index_peak]/2, 3)
+ feet = math.floor(cable_len / 0.3048)
+ inches = round(((cable_len / 0.3048) - feet)*12, 1)
+
+ self.tdr_result_label.setText(f"{cable_len}m ({feet}ft {inches}in)")
+ self.app.tdr_result_label.setText(str(cable_len) + " m")
+ self.updated.emit()
diff --git a/NanoVNASaver/Windows/__init__.py b/NanoVNASaver/Windows/__init__.py
new file mode 100644
index 0000000..36c3926
--- /dev/null
+++ b/NanoVNASaver/Windows/__init__.py
@@ -0,0 +1,9 @@
+from .About import AboutWindow
+from .AnalysisWindow import AnalysisWindow
+from .Bands import BandsWindow
+from .DeviceSettings import DeviceSettingsWindow
+from .DisplaySettings import DisplaySettingsWindow
+from .MarkerSettings import MarkerSettingsWindow
+from .Screenshot import ScreenshotWindow
+from .SweepSettings import SweepSettingsWindow
+from .TDR import TDRWindow