kopia lustrzana https://github.com/NanoVNA-Saver/nanovna-saver
Feature/linting 220514 (#503)
* Unified Chart Code * New Defauls class for persistance of settings - fixes #491 * Removed non-interactive update checkspull/504/head
rodzic
6aa7aaa051
commit
3f4a262abe
|
@ -16,16 +16,7 @@
|
|||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import math
|
||||
import logging
|
||||
from decimal import InvalidOperation
|
||||
from typing import List
|
||||
|
||||
from PyQt5 import QtWidgets, QtGui
|
||||
|
||||
from NanoVNASaver.RFTools import Datapoint
|
||||
from NanoVNASaver.SITools import Format, Value
|
||||
from NanoVNASaver.Charts.Chart import Chart
|
||||
from NanoVNASaver.Charts.Frequency import FrequencyChart
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -34,264 +25,7 @@ logger = logging.getLogger(__name__)
|
|||
class CapacitanceChart(FrequencyChart):
|
||||
def __init__(self, name=""):
|
||||
super().__init__(name)
|
||||
self.leftMargin = 45
|
||||
self.dim.width = 250
|
||||
self.dim.height = 250
|
||||
self.minDisplayValue = 0
|
||||
self.maxDisplayValue = 100
|
||||
|
||||
self.minValue = -1
|
||||
self.maxValue = 1
|
||||
self.span = 1
|
||||
|
||||
self.setMinimumSize(self.dim.width + self.rightMargin + self.leftMargin,
|
||||
self.dim.height + self.topMargin + self.bottomMargin)
|
||||
self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
QtWidgets.QSizePolicy.MinimumExpanding))
|
||||
pal = QtGui.QPalette()
|
||||
pal.setColor(QtGui.QPalette.Background, Chart.color.background)
|
||||
self.setPalette(pal)
|
||||
self.setAutoFillBackground(True)
|
||||
|
||||
def drawChart(self, qp: QtGui.QPainter):
|
||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
qp.drawText(3, 15, self.name + " (F)")
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.dim.height+5)
|
||||
qp.drawLine(self.leftMargin-5, self.topMargin+self.dim.height,
|
||||
self.leftMargin+self.dim.width, self.topMargin + self.dim.height)
|
||||
self.drawTitle(qp)
|
||||
|
||||
def drawValues(self, qp: QtGui.QPainter):
|
||||
if len(self.data) == 0 and len(self.reference) == 0:
|
||||
return
|
||||
pen = QtGui.QPen(Chart.color.sweep)
|
||||
pen.setWidth(self.dim.point)
|
||||
line_pen = QtGui.QPen(Chart.color.sweep)
|
||||
line_pen.setWidth(self.dim.line)
|
||||
highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255))
|
||||
highlighter.setWidth(1)
|
||||
if not self.fixedSpan:
|
||||
if len(self.data) > 0:
|
||||
fstart = self.data[0].freq
|
||||
fstop = self.data[len(self.data)-1].freq
|
||||
else:
|
||||
fstart = self.reference[0].freq
|
||||
fstop = self.reference[len(self.reference) - 1].freq
|
||||
self.fstart = fstart
|
||||
self.fstop = fstop
|
||||
else:
|
||||
fstart = self.fstart = self.minFrequency
|
||||
fstop = self.fstop = self.maxFrequency
|
||||
|
||||
# Draw bands if required
|
||||
if self.bands.enabled:
|
||||
self.drawBands(qp, fstart, fstop)
|
||||
|
||||
if self.fixedValues:
|
||||
maxValue = self.maxDisplayValue / 10e11
|
||||
minValue = self.minDisplayValue / 10e11
|
||||
self.maxValue = maxValue
|
||||
self.minValue = minValue
|
||||
else:
|
||||
# Find scaling
|
||||
minValue = 1
|
||||
maxValue = -1
|
||||
for d in self.data:
|
||||
val = d.capacitiveEquivalent()
|
||||
if val > maxValue:
|
||||
maxValue = val
|
||||
if val < minValue:
|
||||
minValue = val
|
||||
for d in self.reference: # Also check min/max for the reference sweep
|
||||
if d.freq < self.fstart or d.freq > self.fstop:
|
||||
continue
|
||||
val = d.capacitiveEquivalent()
|
||||
if val > maxValue:
|
||||
maxValue = val
|
||||
if val < minValue:
|
||||
minValue = val
|
||||
self.maxValue = maxValue
|
||||
self.minValue = minValue
|
||||
|
||||
span = maxValue - minValue
|
||||
if span == 0:
|
||||
logger.info("Span is zero for CapacitanceChart, setting to a small value.")
|
||||
span = 1e-15
|
||||
self.span = span
|
||||
|
||||
target_ticks = math.floor(self.dim.height / 60)
|
||||
fmt = Format(max_nr_digits=3)
|
||||
for i in range(target_ticks):
|
||||
val = minValue + (i / target_ticks) * span
|
||||
try:
|
||||
y = self.topMargin + round((self.maxValue - val) / self.span * self.dim.height)
|
||||
qp.setPen(Chart.color.text)
|
||||
if val != minValue:
|
||||
valstr = str(Value(val, fmt=fmt))
|
||||
qp.drawText(3, y + 3, valstr)
|
||||
except (ValueError, InvalidOperation) as exc:
|
||||
logger.debug("Drawing wrent wrong: %s", exc)
|
||||
y = self.topMargin
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.dim.width, y)
|
||||
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, self.topMargin,
|
||||
self.leftMargin + self.dim.width, self.topMargin)
|
||||
qp.setPen(Chart.color.text)
|
||||
qp.drawText(3, self.topMargin + 4, str(Value(maxValue, fmt=fmt)))
|
||||
qp.drawText(3, self.dim.height+self.topMargin, str(Value(minValue, fmt=fmt)))
|
||||
self.drawFrequencyTicks(qp)
|
||||
|
||||
self.drawData(qp, self.data, Chart.color.sweep)
|
||||
self.drawData(qp, self.reference, Chart.color.reference)
|
||||
self.drawMarkers(qp)
|
||||
|
||||
def getYPosition(self, d: Datapoint) -> int:
|
||||
try:
|
||||
return (
|
||||
self.topMargin +
|
||||
round((self.maxValue - d.capacitiveEquivalent()) /
|
||||
self.span * self.dim.height))
|
||||
except ValueError:
|
||||
return self.topMargin
|
||||
|
||||
def valueAtPosition(self, y) -> List[float]:
|
||||
absy = y - self.topMargin
|
||||
val = -1 * ((absy / self.dim.height * self.span) - self.maxValue)
|
||||
return [val * 10e11]
|
||||
|
||||
def copy(self):
|
||||
new_chart: CapacitanceChart = super().copy()
|
||||
new_chart.span = self.span
|
||||
return new_chart
|
||||
|
||||
|
||||
class InductanceChart(FrequencyChart):
|
||||
def __init__(self, name=""):
|
||||
super().__init__(name)
|
||||
self.leftMargin = 30
|
||||
self.dim.width = 250
|
||||
self.dim.height = 250
|
||||
self.minDisplayValue = 0
|
||||
self.maxDisplayValue = 100
|
||||
|
||||
self.minValue = -1
|
||||
self.maxValue = 1
|
||||
self.span = 1
|
||||
|
||||
self.setMinimumSize(self.dim.width + self.rightMargin + self.leftMargin,
|
||||
self.dim.height + self.topMargin + self.bottomMargin)
|
||||
self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
QtWidgets.QSizePolicy.MinimumExpanding))
|
||||
pal = QtGui.QPalette()
|
||||
pal.setColor(QtGui.QPalette.Background, Chart.color.background)
|
||||
self.setPalette(pal)
|
||||
self.setAutoFillBackground(True)
|
||||
|
||||
def drawChart(self, qp: QtGui.QPainter):
|
||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
qp.drawText(3, 15, self.name + " (H)")
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.dim.height+5)
|
||||
qp.drawLine(self.leftMargin-5, self.topMargin+self.dim.height,
|
||||
self.leftMargin+self.dim.width, self.topMargin + self.dim.height)
|
||||
self.drawTitle(qp)
|
||||
|
||||
def drawValues(self, qp: QtGui.QPainter):
|
||||
if len(self.data) == 0 and len(self.reference) == 0:
|
||||
return
|
||||
pen = QtGui.QPen(Chart.color.sweep)
|
||||
pen.setWidth(self.dim.point)
|
||||
line_pen = QtGui.QPen(Chart.color.sweep)
|
||||
line_pen.setWidth(self.dim.line)
|
||||
highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255))
|
||||
highlighter.setWidth(1)
|
||||
if not self.fixedSpan:
|
||||
if len(self.data) > 0:
|
||||
fstart = self.data[0].freq
|
||||
fstop = self.data[len(self.data)-1].freq
|
||||
else:
|
||||
fstart = self.reference[0].freq
|
||||
fstop = self.reference[len(self.reference) - 1].freq
|
||||
self.fstart = fstart
|
||||
self.fstop = fstop
|
||||
else:
|
||||
fstart = self.fstart = self.minFrequency
|
||||
fstop = self.fstop = self.maxFrequency
|
||||
|
||||
# Draw bands if required
|
||||
if self.bands.enabled:
|
||||
self.drawBands(qp, fstart, fstop)
|
||||
|
||||
if self.fixedValues:
|
||||
maxValue = self.maxDisplayValue / 10e11
|
||||
minValue = self.minDisplayValue / 10e11
|
||||
self.maxValue = maxValue
|
||||
self.minValue = minValue
|
||||
else:
|
||||
# Find scaling
|
||||
minValue = 1
|
||||
maxValue = -1
|
||||
for d in self.data:
|
||||
val = d.inductiveEquivalent()
|
||||
if val > maxValue:
|
||||
maxValue = val
|
||||
if val < minValue:
|
||||
minValue = val
|
||||
for d in self.reference: # Also check min/max for the reference sweep
|
||||
if d.freq < self.fstart or d.freq > self.fstop:
|
||||
continue
|
||||
val = d.inductiveEquivalent()
|
||||
if val > maxValue:
|
||||
maxValue = val
|
||||
if val < minValue:
|
||||
minValue = val
|
||||
self.maxValue = maxValue
|
||||
self.minValue = minValue
|
||||
|
||||
span = maxValue - minValue
|
||||
if span == 0:
|
||||
logger.info("Span is zero for CapacitanceChart, setting to a small value.")
|
||||
span = 1e-15
|
||||
self.span = span
|
||||
|
||||
target_ticks = math.floor(self.dim.height / 60)
|
||||
fmt = Format(max_nr_digits=3)
|
||||
for i in range(target_ticks):
|
||||
val = minValue + (i / target_ticks) * span
|
||||
y = self.topMargin + round((self.maxValue - val) / self.span * self.dim.height)
|
||||
qp.setPen(Chart.color.text)
|
||||
if val != minValue:
|
||||
valstr = str(Value(val, fmt=fmt))
|
||||
qp.drawText(3, y + 3, valstr)
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.dim.width, y)
|
||||
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, self.topMargin,
|
||||
self.leftMargin + self.dim.width, self.topMargin)
|
||||
qp.setPen(Chart.color.text)
|
||||
qp.drawText(3, self.topMargin + 4, str(Value(maxValue, fmt=fmt)))
|
||||
qp.drawText(3, self.dim.height+self.topMargin, str(Value(minValue, fmt=fmt)))
|
||||
self.drawFrequencyTicks(qp)
|
||||
|
||||
self.drawData(qp, self.data, Chart.color.sweep)
|
||||
self.drawData(qp, self.reference, Chart.color.reference)
|
||||
self.drawMarkers(qp)
|
||||
|
||||
def getYPosition(self, d: Datapoint) -> int:
|
||||
return (self.topMargin +
|
||||
round((self.maxValue - d.inductiveEquivalent()) /
|
||||
self.span * self.dim.height))
|
||||
|
||||
def valueAtPosition(self, y) -> List[float]:
|
||||
absy = y - self.topMargin
|
||||
val = -1 * ((absy / self.dim.height * self.span) - self.maxValue)
|
||||
return [val * 10e11]
|
||||
|
||||
def copy(self):
|
||||
new_chart: InductanceChart = super().copy()
|
||||
new_chart.span = self.span
|
||||
return new_chart
|
||||
self.name_unit = "F"
|
||||
self.value_function = lambda x: x.capacitiveEquivalent()
|
||||
|
|
|
@ -24,6 +24,7 @@ from typing import List, Set, Tuple, ClassVar, Any
|
|||
from PyQt5 import QtWidgets, QtGui, QtCore
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
|
||||
from NanoVNASaver import Defaults
|
||||
from NanoVNASaver.RFTools import Datapoint
|
||||
from NanoVNASaver.Marker import Marker
|
||||
|
||||
|
@ -64,23 +65,15 @@ class ChartFlags:
|
|||
draw_lines: bool = False
|
||||
is_popout: bool = False
|
||||
|
||||
@dataclass
|
||||
class ChartMarkerConfig:
|
||||
draw_label: bool = False
|
||||
fill: bool = False
|
||||
at_tip: bool = False
|
||||
size: int = 3
|
||||
|
||||
class ChartMarker(QtWidgets.QWidget):
|
||||
cfg: ClassVar[ChartMarkerConfig] = ChartMarkerConfig()
|
||||
|
||||
def __init__(self, qp: QtGui.QPaintDevice):
|
||||
super().__init__()
|
||||
self.qp = qp
|
||||
|
||||
def draw(self, x: int, y: int, color: QtGui.QColor, text: str = ""):
|
||||
offset = self.cfg.size // 2
|
||||
if self.cfg.at_tip:
|
||||
offset = Defaults.cfg.chart.marker_size // 2
|
||||
if Defaults.cfg.chart.marker_at_tip:
|
||||
y -= offset
|
||||
pen = QtGui.QPen(color)
|
||||
self.qp.setPen(pen)
|
||||
|
@ -90,12 +83,12 @@ class ChartMarker(QtWidgets.QWidget):
|
|||
qpp.lineTo(x + offset, y - offset)
|
||||
qpp.lineTo(x, y + offset)
|
||||
|
||||
if self.cfg.fill:
|
||||
if Defaults.cfg.chart.marker_filled:
|
||||
self.qp.fillPath(qpp, color)
|
||||
else:
|
||||
self.qp.drawPath(qpp)
|
||||
|
||||
if text and self.cfg.draw_label:
|
||||
if text and Defaults.cfg.chart.marker_label:
|
||||
text_width = self.qp.fontMetrics().horizontalAdvance(text)
|
||||
self.qp.drawText(x - text_width // 2, y - 3 - offset, text)
|
||||
|
||||
|
@ -104,7 +97,6 @@ class Chart(QtWidgets.QWidget):
|
|||
bands: ClassVar[Any] = None
|
||||
popoutRequested: ClassVar[Any] = pyqtSignal(object)
|
||||
color: ClassVar[ChartColors] = ChartColors()
|
||||
marker_cfg: ClassVar[ChartMarkerConfig] = ChartMarkerConfig()
|
||||
|
||||
def __init__(self, name):
|
||||
super().__init__()
|
||||
|
@ -160,7 +152,7 @@ class Chart(QtWidgets.QWidget):
|
|||
self.update()
|
||||
|
||||
def setMarkerSize(self, size):
|
||||
ChartMarker.cfg.size = size
|
||||
Defaults.cfg.chart.marker_size = size
|
||||
self.update()
|
||||
|
||||
def setSweepTitle(self, title):
|
||||
|
@ -249,7 +241,6 @@ class Chart(QtWidgets.QWidget):
|
|||
new_chart.reference = self.reference
|
||||
new_chart.dim = replace(self.dim)
|
||||
new_chart.flag = replace(self.flag)
|
||||
new_chart.marker_cfg = replace(self.marker_cfg)
|
||||
new_chart.markers = self.markers
|
||||
new_chart.swrMarkers = self.swrMarkers
|
||||
new_chart.bands = self.bands
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
# A python program to view and export Touchstone data from a NanoVNA
|
||||
# Copyright (C) 2019, 2020 Rune B. Broberg
|
||||
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
|
||||
# Copyright (C) 2020ff NanoVNA-Saver Authors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -35,30 +35,22 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class FrequencyChart(Chart):
|
||||
fstart = 0
|
||||
fstop = 0
|
||||
|
||||
maxFrequency = 100000000
|
||||
minFrequency = 1000000
|
||||
|
||||
# TODO: use unscaled values instead of unit dependend ones
|
||||
minDisplayValue = -1
|
||||
maxDisplayValue = 1
|
||||
|
||||
fixedSpan = False
|
||||
fixedValues = False
|
||||
|
||||
logarithmicX = False
|
||||
logarithmicY = False
|
||||
|
||||
leftMargin = 30
|
||||
rightMargin = 20
|
||||
bottomMargin = 20
|
||||
topMargin = 30
|
||||
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self.maxFrequency = 100000000
|
||||
self.minFrequency = 1000000
|
||||
self.fixedSpan = False
|
||||
self.fixedValues = False
|
||||
self.logarithmicX = False
|
||||
self.logarithmicY = False
|
||||
|
||||
self.leftMargin = 30
|
||||
self.rightMargin = 20
|
||||
self.bottomMargin = 20
|
||||
self.topMargin = 30
|
||||
|
||||
self.dim.width = 250
|
||||
self.dim.height = 250
|
||||
self.fstart = 0
|
||||
|
@ -67,6 +59,10 @@ class FrequencyChart(Chart):
|
|||
self.name_unit = ""
|
||||
self.value_function = lambda x: 0.0
|
||||
|
||||
# TODO: use unscaled values instead of unit dependend ones
|
||||
self.minDisplayValue = -1
|
||||
self.maxDisplayValue = 1
|
||||
|
||||
self.minValue = -1
|
||||
self.maxValue = 1
|
||||
self.span = 1
|
||||
|
@ -189,17 +185,15 @@ class FrequencyChart(Chart):
|
|||
|
||||
def _set_start_stop(self):
|
||||
if self.fixedSpan:
|
||||
fstart = self.minFrequency
|
||||
fstop = self.maxFrequency
|
||||
else:
|
||||
if len(self.data) > 0:
|
||||
fstart = self.data[0].freq
|
||||
fstop = self.data[len(self.data) - 1].freq
|
||||
else:
|
||||
fstart = self.reference[0].freq
|
||||
fstop = self.reference[len(self.reference) - 1].freq
|
||||
self.fstart = fstart
|
||||
self.fstop = fstop
|
||||
self.fstart = self.minFrequency
|
||||
self.fstop = self.maxFrequency
|
||||
return
|
||||
if self.data:
|
||||
self.fstart = self.data[0].freq
|
||||
self.fstop = self.data[len(self.data) - 1].freq
|
||||
return
|
||||
self.fstart = self.reference[0].freq
|
||||
self.fstop = self.reference[len(self.reference) - 1].freq
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
self.action_set_fixed_start.setText(
|
||||
|
@ -336,10 +330,13 @@ class FrequencyChart(Chart):
|
|||
return math.floor(self.width() / 2)
|
||||
|
||||
def getYPosition(self, d: Datapoint) -> int:
|
||||
return (
|
||||
self.topMargin +
|
||||
round((self.maxValue - d.capacitiveEquivalent()) /
|
||||
self.span * self.dim.height))
|
||||
try:
|
||||
return (
|
||||
self.topMargin +
|
||||
round((self.maxValue - self.value_function(d) /
|
||||
self.span * self.dim.height)))
|
||||
except ValueError:
|
||||
return self.topMargin
|
||||
|
||||
def frequencyAtPosition(self, x, limit=True) -> int:
|
||||
"""
|
||||
|
@ -352,20 +349,21 @@ class FrequencyChart(Chart):
|
|||
and the value is before or after the chart,
|
||||
returns minimum or maximum frequencies.
|
||||
"""
|
||||
if self.fstop - self.fstart > 0:
|
||||
absx = x - self.leftMargin
|
||||
if limit and absx < 0:
|
||||
if self.fstop - self.fstart <= 0:
|
||||
return -1
|
||||
absx = x - self.leftMargin
|
||||
if limit:
|
||||
if absx < 0:
|
||||
return self.fstart
|
||||
if limit and absx > self.dim.width:
|
||||
if absx > self.dim.width:
|
||||
return self.fstop
|
||||
if self.logarithmicX:
|
||||
span = math.log(self.fstop) - math.log(self.fstart)
|
||||
step = span / self.dim.width
|
||||
return round(math.exp(math.log(self.fstart) + absx * step))
|
||||
span = self.fstop - self.fstart
|
||||
if self.logarithmicX:
|
||||
span = math.log(self.fstop) - math.log(self.fstart)
|
||||
step = span / self.dim.width
|
||||
return round(self.fstart + absx * step)
|
||||
return -1
|
||||
return round(math.exp(math.log(self.fstart) + absx * step))
|
||||
span = self.fstop - self.fstart
|
||||
step = span / self.dim.width
|
||||
return round(self.fstart + absx * step)
|
||||
|
||||
def valueAtPosition(self, y) -> List[float]:
|
||||
"""
|
||||
|
@ -574,26 +572,21 @@ class FrequencyChart(Chart):
|
|||
self.drawData(qp, self.reference, Chart.color.reference)
|
||||
self.drawMarkers(qp)
|
||||
|
||||
def _find_scaling(self) -> Tuple[int, int]:
|
||||
def _find_scaling(self) -> Tuple[float, float]:
|
||||
min_value = self.minDisplayValue / 10e11
|
||||
max_value = self.maxDisplayValue / 10e11
|
||||
if self.fixedValues:
|
||||
return (self.minDisplayValue / 10e11,
|
||||
self.maxDisplayValue / 10e11)
|
||||
min_value = 1
|
||||
max_value = -1
|
||||
return (min_value, max_value)
|
||||
for d in self.data:
|
||||
val = self.value_function(d)
|
||||
if val > max_value:
|
||||
max_value = val
|
||||
if val < min_value:
|
||||
min_value = val
|
||||
min_value = min(min_value, val)
|
||||
max_value = max(max_value, val)
|
||||
for d in self.reference: # Also check min/max for the reference sweep
|
||||
if d.freq < self.fstart or d.freq > self.fstop:
|
||||
continue
|
||||
val = self.value_function(d)
|
||||
if val > max_value:
|
||||
max_value = val
|
||||
if val < min_value:
|
||||
min_value = val
|
||||
min_value = min(min_value, val)
|
||||
max_value = max(max_value, val)
|
||||
return (min_value, max_value)
|
||||
|
||||
def drawFrequencyTicks(self, qp):
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
# A python program to view and export Touchstone data from a NanoVNA
|
||||
# Copyright (C) 2019, 2020 Rune B. Broberg
|
||||
# Copyright (C) 2020 NanoVNA-Saver Authors
|
||||
# Copyright (C) 2020ff NanoVNA-Saver Authors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -16,15 +16,8 @@
|
|||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import math
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from PyQt5 import QtWidgets, QtGui
|
||||
|
||||
from NanoVNASaver.RFTools import Datapoint
|
||||
from NanoVNASaver.SITools import Format, Value
|
||||
from NanoVNASaver.Charts.Chart import Chart
|
||||
from NanoVNASaver.Charts.Frequency import FrequencyChart
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -33,127 +26,7 @@ logger = logging.getLogger(__name__)
|
|||
class InductanceChart(FrequencyChart):
|
||||
def __init__(self, name=""):
|
||||
super().__init__(name)
|
||||
self.leftMargin = 45
|
||||
self.dim.width = 250
|
||||
self.dim.height = 250
|
||||
self.minDisplayValue = 0
|
||||
self.maxDisplayValue = 100
|
||||
|
||||
self.minValue = -1
|
||||
self.maxValue = 1
|
||||
self.span = 1
|
||||
|
||||
self.setMinimumSize(self.dim.width + self.rightMargin + self.leftMargin,
|
||||
self.dim.height + self.topMargin + self.bottomMargin)
|
||||
self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
QtWidgets.QSizePolicy.MinimumExpanding))
|
||||
pal = QtGui.QPalette()
|
||||
pal.setColor(QtGui.QPalette.Background, Chart.color.background)
|
||||
self.setPalette(pal)
|
||||
self.setAutoFillBackground(True)
|
||||
|
||||
def drawChart(self, qp: QtGui.QPainter):
|
||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
qp.drawText(3, 15, self.name + " (H)")
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.dim.height+5)
|
||||
qp.drawLine(self.leftMargin-5, self.topMargin+self.dim.height,
|
||||
self.leftMargin+self.dim.width, self.topMargin + self.dim.height)
|
||||
self.drawTitle(qp)
|
||||
|
||||
def drawValues(self, qp: QtGui.QPainter):
|
||||
if len(self.data) == 0 and len(self.reference) == 0:
|
||||
return
|
||||
pen = QtGui.QPen(Chart.color.sweep)
|
||||
pen.setWidth(self.dim.point)
|
||||
line_pen = QtGui.QPen(Chart.color.sweep)
|
||||
line_pen.setWidth(self.dim.line)
|
||||
highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255))
|
||||
highlighter.setWidth(1)
|
||||
if not self.fixedSpan:
|
||||
if len(self.data) > 0:
|
||||
fstart = self.data[0].freq
|
||||
fstop = self.data[len(self.data)-1].freq
|
||||
else:
|
||||
fstart = self.reference[0].freq
|
||||
fstop = self.reference[len(self.reference) - 1].freq
|
||||
self.fstart = fstart
|
||||
self.fstop = fstop
|
||||
else:
|
||||
fstart = self.fstart = self.minFrequency
|
||||
fstop = self.fstop = self.maxFrequency
|
||||
|
||||
# Draw bands if required
|
||||
if self.bands.enabled:
|
||||
self.drawBands(qp, fstart, fstop)
|
||||
|
||||
if self.fixedValues:
|
||||
maxValue = self.maxDisplayValue / 10e11
|
||||
minValue = self.minDisplayValue / 10e11
|
||||
self.maxValue = maxValue
|
||||
self.minValue = minValue
|
||||
else:
|
||||
# Find scaling
|
||||
minValue = 1
|
||||
maxValue = -1
|
||||
for d in self.data:
|
||||
val = d.inductiveEquivalent()
|
||||
if val > maxValue:
|
||||
maxValue = val
|
||||
if val < minValue:
|
||||
minValue = val
|
||||
for d in self.reference: # Also check min/max for the reference sweep
|
||||
if d.freq < self.fstart or d.freq > self.fstop:
|
||||
continue
|
||||
val = d.inductiveEquivalent()
|
||||
if val > maxValue:
|
||||
maxValue = val
|
||||
if val < minValue:
|
||||
minValue = val
|
||||
self.maxValue = maxValue
|
||||
self.minValue = minValue
|
||||
|
||||
span = maxValue - minValue
|
||||
if span == 0:
|
||||
logger.info("Span is zero for CapacitanceChart, setting to a small value.")
|
||||
span = 1e-15
|
||||
self.span = span
|
||||
|
||||
target_ticks = math.floor(self.dim.height / 60)
|
||||
fmt = Format(max_nr_digits=3)
|
||||
for i in range(target_ticks):
|
||||
val = minValue + (i / target_ticks) * span
|
||||
y = self.topMargin + round((self.maxValue - val) / self.span * self.dim.height)
|
||||
qp.setPen(Chart.color.text)
|
||||
if val != minValue:
|
||||
valstr = str(Value(val, fmt=fmt))
|
||||
qp.drawText(3, y + 3, valstr)
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.dim.width, y)
|
||||
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, self.topMargin,
|
||||
self.leftMargin + self.dim.width, self.topMargin)
|
||||
qp.setPen(Chart.color.text)
|
||||
qp.drawText(3, self.topMargin + 4, str(Value(maxValue, fmt=fmt)))
|
||||
qp.drawText(3, self.dim.height+self.topMargin, str(Value(minValue, fmt=fmt)))
|
||||
self.drawFrequencyTicks(qp)
|
||||
|
||||
self.drawData(qp, self.data, Chart.color.sweep)
|
||||
self.drawData(qp, self.reference, Chart.color.reference)
|
||||
self.drawMarkers(qp)
|
||||
|
||||
def getYPosition(self, d: Datapoint) -> int:
|
||||
return (self.topMargin +
|
||||
round((self.maxValue - d.inductiveEquivalent()) /
|
||||
self.span * self.dim.height))
|
||||
|
||||
def valueAtPosition(self, y) -> List[float]:
|
||||
absy = y - self.topMargin
|
||||
val = -1 * ((absy / self.dim.height * self.span) - self.maxValue)
|
||||
return [val * 10e11]
|
||||
|
||||
def copy(self):
|
||||
new_chart: InductanceChart = super().copy()
|
||||
new_chart.span = self.span
|
||||
return new_chart
|
||||
self.name_unit = "H"
|
||||
self.value_function = lambda x: x.inductiveEquivalent()
|
||||
|
|
|
@ -23,7 +23,8 @@ from typing import List
|
|||
from PyQt5 import QtGui
|
||||
|
||||
from NanoVNASaver.RFTools import Datapoint
|
||||
from NanoVNASaver.SITools import Format, Value
|
||||
from NanoVNASaver.SITools import (
|
||||
Format, Value, round_ceil, round_floor)
|
||||
from NanoVNASaver.Charts.Chart import Chart
|
||||
from NanoVNASaver.Charts.Frequency import FrequencyChart
|
||||
from NanoVNASaver.Charts.LogMag import LogMagChart
|
||||
|
@ -54,48 +55,34 @@ class MagnitudeZChart(FrequencyChart):
|
|||
self.drawBands(qp, self.fstart, self.fstop)
|
||||
|
||||
if self.fixedValues:
|
||||
maxValue = self.maxDisplayValue
|
||||
minValue = self.minDisplayValue
|
||||
self.maxValue = maxValue
|
||||
if self.logarithmicY and minValue <= 0:
|
||||
self.minValue = 0.01
|
||||
else:
|
||||
self.minValue = minValue
|
||||
self.maxValue = self.maxDisplayValue
|
||||
self.minValue = max(
|
||||
self.minDisplayValue, 0.01) if self.logarithmicY else self.minDisplayValue
|
||||
else:
|
||||
# Find scaling
|
||||
minValue = 100
|
||||
maxValue = 0
|
||||
self.minValue = 100
|
||||
self.maxValue = 0
|
||||
for d in self.data:
|
||||
mag = self.magnitude(d)
|
||||
if math.isinf(mag): # Avoid infinite scales
|
||||
if math.isinf(mag): # Avoid infinite scales
|
||||
continue
|
||||
if mag > maxValue:
|
||||
maxValue = mag
|
||||
if mag < minValue:
|
||||
minValue = mag
|
||||
self.maxValue = max(self.maxValue, mag)
|
||||
self.minValue = min(self.minValue, mag)
|
||||
for d in self.reference: # Also check min/max for the reference sweep
|
||||
if d.freq < self.fstart or d.freq > self.fstop:
|
||||
continue
|
||||
mag = self.magnitude(d)
|
||||
if math.isinf(mag): # Avoid infinite scales
|
||||
if math.isinf(mag): # Avoid infinite scales
|
||||
continue
|
||||
if mag > maxValue:
|
||||
maxValue = mag
|
||||
if mag < minValue:
|
||||
minValue = mag
|
||||
self.maxValue = max(self.maxValue, mag)
|
||||
self.minValue = min(self.minValue, mag)
|
||||
|
||||
minValue = 10*math.floor(minValue/10)
|
||||
if self.logarithmicY and minValue <= 0:
|
||||
minValue = 0.01
|
||||
self.minValue = minValue
|
||||
self.minValue = round_floor(self.minValue, 2)
|
||||
if self.logarithmicY and self.minValue <= 0:
|
||||
self.minValue = 0.01
|
||||
self.maxValue = round_ceil(self.maxValue, 2)
|
||||
|
||||
maxValue = 10*math.ceil(maxValue/10)
|
||||
self.maxValue = maxValue
|
||||
|
||||
span = maxValue-minValue
|
||||
if span == 0:
|
||||
span = 0.01
|
||||
self.span = span
|
||||
self.span = (self.maxValue - self.minValue) or 0.01
|
||||
|
||||
# We want one horizontal tick per 50 pixels, at most
|
||||
horizontal_ticks = math.floor(self.dim.height/50)
|
||||
|
|
|
@ -21,6 +21,7 @@ import logging
|
|||
from PyQt5 import QtWidgets, QtCore
|
||||
from PyQt5.QtWidgets import QCheckBox
|
||||
|
||||
from NanoVNASaver import Defaults
|
||||
from NanoVNASaver.Marker import Marker
|
||||
from NanoVNASaver.Controls.Control import Control
|
||||
|
||||
|
@ -28,13 +29,21 @@ from NanoVNASaver.Controls.Control import Control
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ShowButton(QtWidgets.QPushButton):
|
||||
def setText(self, text: str=''):
|
||||
if not text:
|
||||
text = ("Show data"
|
||||
if Defaults.cfg.gui.markers_hidden else "Hide data")
|
||||
super().setText(text)
|
||||
self.setToolTip("Toggle visibility of marker readings area")
|
||||
|
||||
|
||||
class MarkerControl(Control):
|
||||
|
||||
def __init__(self, app: QtWidgets.QWidget):
|
||||
super().__init__(app, "Markers")
|
||||
|
||||
marker_count = max(self.app.settings.value("MarkerCount", 3, int), 1)
|
||||
for i in range(marker_count):
|
||||
for i in range(Defaults.cfg.chart.marker_count):
|
||||
marker = Marker("", self.app.settings)
|
||||
# marker.setFixedHeight(20)
|
||||
marker.updated.connect(self.app.markerUpdated)
|
||||
|
@ -56,12 +65,9 @@ class MarkerControl(Control):
|
|||
|
||||
self.layout.addRow(layout2)
|
||||
|
||||
self.showMarkerButton = QtWidgets.QPushButton()
|
||||
self.showMarkerButton = ShowButton()
|
||||
self.showMarkerButton.setFixedHeight(20)
|
||||
if self.app.marker_frame.isHidden():
|
||||
self.showMarkerButton.setText("Show data")
|
||||
else:
|
||||
self.showMarkerButton.setText("Hide data")
|
||||
self.showMarkerButton.setText()
|
||||
self.showMarkerButton.clicked.connect(self.toggle_frame)
|
||||
|
||||
lock_radiobutton = QtWidgets.QRadioButton("Locked")
|
||||
|
@ -76,10 +82,10 @@ class MarkerControl(Control):
|
|||
|
||||
def toggle_frame(self):
|
||||
def settings(hidden: bool):
|
||||
self.app.marker_frame.setHidden(not hidden)
|
||||
self.app.settings.setValue("MarkersVisible", hidden)
|
||||
self.showMarkerButton.setText(
|
||||
"Hide data" if hidden else "Show data")
|
||||
Defaults.cfg.gui.markers_hidden = not hidden
|
||||
self.app.marker_frame.setHidden(
|
||||
Defaults.cfg.gui.markers_hidden)
|
||||
self.showMarkerButton.setText()
|
||||
self.showMarkerButton.repaint()
|
||||
settings(self.app.marker_frame.isHidden())
|
||||
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
# NanoVNASaver
|
||||
#
|
||||
# A python program to view and export Touchstone data from a NanoVNA
|
||||
# Copyright (C) 2019, 2020 Rune B. Broberg
|
||||
# Copyright (C) 2020ff NanoVNA-Saver Authors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import dataclasses as DC
|
||||
import logging
|
||||
import json
|
||||
|
||||
from PyQt5.QtCore import QSettings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
@DC.dataclass
|
||||
class GUI:
|
||||
window_height: int = 950
|
||||
window_width: int = 1433
|
||||
font_size: int = 8
|
||||
dark_mode: bool = False
|
||||
# TODO: implement QByteArray
|
||||
splitter_sizes: bytearray = DC.field(default_factory=bytearray)
|
||||
markers_hidden: bool = False
|
||||
|
||||
|
||||
@DC.dataclass
|
||||
class Chart:
|
||||
point_size: int = 2
|
||||
show_lines: bool = False
|
||||
line_thickness: int = 1
|
||||
marker_count: int = 3
|
||||
marker_label: bool = False
|
||||
marker_filled: bool = False
|
||||
marker_at_tip: bool = False
|
||||
marker_size: int = 8
|
||||
returnloss_is_positive: bool = False
|
||||
|
||||
|
||||
@DC.dataclass
|
||||
class CFG:
|
||||
gui: object = GUI()
|
||||
chart: object = Chart()
|
||||
|
||||
|
||||
cfg = CFG()
|
||||
|
||||
def restore(settings: 'AppSettings') -> CFG:
|
||||
result = CFG()
|
||||
for field in DC.fields(result):
|
||||
value = settings.restore_dataclass(field.name.upper(),
|
||||
getattr(result, field.name))
|
||||
setattr(result, field.name, value)
|
||||
logger.debug("restored\n(\n%s\n)", result)
|
||||
return result
|
||||
|
||||
|
||||
def store(settings: 'AppSettings', data: CFG) -> None:
|
||||
logger.debug("storing\n(\n%s\n)", data)
|
||||
assert isinstance(data, CFG)
|
||||
for field in DC.fields(data):
|
||||
data_class = getattr(data, field.name)
|
||||
assert DC.is_dataclass(data_class)
|
||||
settings.store_dataclass(field.name.upper(), data_class)
|
||||
|
||||
|
||||
class AppSettings(QSettings):
|
||||
def store_dataclass(self, name: str, data: object) -> None:
|
||||
assert DC.is_dataclass(data)
|
||||
self.beginGroup(name)
|
||||
for field in DC.fields(data):
|
||||
value = getattr(data, field.name)
|
||||
try:
|
||||
assert isinstance(value, field.type)
|
||||
except AssertionError:
|
||||
logger.error("%s: %s is not a %s", name, field.name,
|
||||
field.type)
|
||||
continue
|
||||
if field.type not in (int, float, str, bool):
|
||||
try:
|
||||
value = json.dumps(value)
|
||||
except TypeError:
|
||||
value = field.type(value).hex()
|
||||
self.setValue(field.name, value)
|
||||
self.endGroup()
|
||||
|
||||
def restore_dataclass(self, name: str, data: object) -> object:
|
||||
assert DC.is_dataclass(data)
|
||||
|
||||
result = DC.replace(data)
|
||||
self.beginGroup(name)
|
||||
for field in DC.fields(data):
|
||||
value = None
|
||||
if field.type in (int, float, str, bool):
|
||||
value = self.value(field.name,
|
||||
type=field.type,
|
||||
defaultValue=field.default)
|
||||
else:
|
||||
default = getattr(data, field.name)
|
||||
try:
|
||||
value = json.loads(
|
||||
self.value(field.name, type=str,
|
||||
defaultValue=json.dumps(default)))
|
||||
except TypeError:
|
||||
value = self.value(field.name)
|
||||
value = bytes.fromhex(value) if value is str else default
|
||||
setattr(result, field.name, field.type(value))
|
||||
self.endGroup()
|
||||
|
||||
return result
|
|
@ -24,6 +24,7 @@ from time import strftime, localtime
|
|||
|
||||
from PyQt5 import QtWidgets, QtCore, QtGui
|
||||
|
||||
from NanoVNASaver import Defaults
|
||||
from .Windows import (
|
||||
AboutWindow, AnalysisWindow, CalibrationWindow,
|
||||
DeviceSettingsWindow, DisplaySettingsWindow, SweepSettingsWindow,
|
||||
|
@ -69,10 +70,11 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
else:
|
||||
self.icon = QtGui.QIcon("icon_48x48.png")
|
||||
self.setWindowIcon(self.icon)
|
||||
self.settings = QtCore.QSettings(QtCore.QSettings.IniFormat,
|
||||
self.settings = Defaults.AppSettings(QtCore.QSettings.IniFormat,
|
||||
QtCore.QSettings.UserScope,
|
||||
"NanoVNASaver", "NanoVNASaver")
|
||||
logger.info("Settings from: %s", self.settings.fileName())
|
||||
Defaults.cfg = Defaults.restore(self.settings)
|
||||
self.threadpool = QtCore.QThreadPool()
|
||||
self.sweep = Sweep()
|
||||
self.worker = SweepWorker(self)
|
||||
|
@ -121,9 +123,8 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
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)
|
||||
self.resize(Defaults.cfg.gui.window_width,
|
||||
Defaults.cfg.gui.window_height)
|
||||
scrollarea.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding)
|
||||
self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
|
@ -203,18 +204,14 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
left_column = QtWidgets.QVBoxLayout()
|
||||
right_column = QtWidgets.QVBoxLayout()
|
||||
right_column.addLayout(self.charts_layout)
|
||||
self.marker_frame.setHidden(
|
||||
not self.settings.value("MarkersVisible", True, bool))
|
||||
self.marker_frame.setHidden(Defaults.cfg.gui.markers_hidden)
|
||||
chart_widget = QtWidgets.QWidget()
|
||||
chart_widget.setLayout(right_column)
|
||||
self.splitter = QtWidgets.QSplitter()
|
||||
self.splitter.addWidget(self.marker_frame)
|
||||
self.splitter.addWidget(chart_widget)
|
||||
|
||||
try:
|
||||
self.splitter.restoreState(self.settings.value("SplitterSizes"))
|
||||
except TypeError:
|
||||
pass
|
||||
self.splitter.restoreState(Defaults.cfg.gui.splitter_sizes)
|
||||
|
||||
layout.addLayout(left_column)
|
||||
layout.addWidget(self.splitter, 2)
|
||||
|
@ -638,17 +635,18 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
|
||||
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.setValue("SplitterSizes", self.splitter.saveState())
|
||||
|
||||
self.settings.sync()
|
||||
self.bands.saveSettings()
|
||||
self.threadpool.waitForDone(2500)
|
||||
|
||||
Defaults.cfg.chart.marker_count = Marker.count()
|
||||
Defaults.cfg.gui.window_width = self.width()
|
||||
Defaults.cfg.gui.window_height = self.height()
|
||||
Defaults.cfg.gui.splitter_sizes = bytearray(self.splitter.saveState())
|
||||
Defaults.store(self.settings, Defaults.cfg)
|
||||
|
||||
a0.accept()
|
||||
sys.exit()
|
||||
|
||||
|
|
|
@ -34,6 +34,13 @@ def clamp_value(value: Real, rmin: Real, rmax: Real) -> Real:
|
|||
return rmax
|
||||
return value
|
||||
|
||||
def round_ceil(value: Real, digits: int=0) -> Real:
|
||||
factor = 10 ** digits
|
||||
return factor * math.ceil(value / factor)
|
||||
|
||||
def round_floor(value: Real, digits: int=0) -> Real:
|
||||
factor = 10 ** digits
|
||||
return factor * math.floor(value / factor)
|
||||
|
||||
class Format(NamedTuple):
|
||||
max_nr_digits: int = 6
|
||||
|
@ -74,8 +81,8 @@ class Value:
|
|||
self._value = decimal.Decimal(value, context=Value.CTX)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (f"{self.__class__.__name__}(" + repr(self._value) +
|
||||
f", '{self._unit}', {self.fmt})")
|
||||
return (f"{self.__class__.__name__}("
|
||||
f"{repr(self._value)}, '{self._unit}', {self.fmt})")
|
||||
|
||||
def __str__(self) -> str:
|
||||
fmt = self.fmt
|
||||
|
@ -100,10 +107,10 @@ class Value:
|
|||
max_digits = fmt.max_nr_digits + (
|
||||
(1 if not fmt.fix_decimals and abs(real) < 10 else 0) +
|
||||
(1 if not fmt.fix_decimals and abs(real) < 100 else 0))
|
||||
formstr = "." + str(max_digits - 3) + "f"
|
||||
formstr = f".{max_digits - 3}f"
|
||||
|
||||
if self.fmt.allways_signed:
|
||||
formstr = "+" + formstr
|
||||
formstr = f"+{formstr}"
|
||||
result = format(real, formstr)
|
||||
|
||||
if float(result) == 0.0:
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import contextlib
|
||||
import logging
|
||||
from time import strftime, localtime
|
||||
from urllib import request, error
|
||||
|
@ -54,7 +55,7 @@ class AboutWindow(QtWidgets.QWidget):
|
|||
layout.addWidget(QtWidgets.QLabel(""))
|
||||
layout.addWidget(QtWidgets.QLabel(
|
||||
"\N{COPYRIGHT SIGN} Copyright 2019, 2020 Rune B. Broberg\n"
|
||||
"\N{COPYRIGHT SIGN} Copyright 2020 NanoVNA-Saver Authors"
|
||||
"\N{COPYRIGHT SIGN} Copyright 2020ff NanoVNA-Saver Authors"
|
||||
))
|
||||
layout.addWidget(QtWidgets.QLabel(
|
||||
"This program comes with ABSOLUTELY NO WARRANTY"))
|
||||
|
@ -79,21 +80,6 @@ class AboutWindow(QtWidgets.QWidget):
|
|||
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)
|
||||
|
@ -101,7 +87,6 @@ class AboutWindow(QtWidgets.QWidget):
|
|||
update_hbox.addLayout(update_form)
|
||||
update_hbox.addStretch()
|
||||
update_form.addRow(self.updateLabel)
|
||||
update_form.addRow(self.updateCheckBox)
|
||||
layout.addLayout(update_hbox)
|
||||
|
||||
layout.addStretch()
|
||||
|
@ -115,39 +100,10 @@ class AboutWindow(QtWidgets.QWidget):
|
|||
self.updateLabels()
|
||||
|
||||
def updateLabels(self):
|
||||
try:
|
||||
with contextlib.suppress(IOError, AttributeError):
|
||||
self.versionLabel.setText(
|
||||
f"NanoVNA Firmware Version: {self.app.vna.name} "
|
||||
f"v{self.app.vna.version}")
|
||||
except (IOError, AttributeError):
|
||||
pass
|
||||
|
||||
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):
|
||||
latest_version = Version()
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
# A python program to view and export Touchstone data from a NanoVNA
|
||||
# Copyright (C) 2019, 2020 Rune B. Broberg
|
||||
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
|
||||
# Copyright (C) 2020ff NanoVNA-Saver Authors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -21,8 +21,9 @@ from typing import List
|
|||
|
||||
from PyQt5 import QtWidgets, QtCore, QtGui
|
||||
|
||||
from NanoVNASaver import Defaults
|
||||
from NanoVNASaver.Charts.Chart import (
|
||||
Chart, ChartColors, ChartMarker, ChartMarkerConfig)
|
||||
Chart, ChartColors)
|
||||
from NanoVNASaver.Windows.Bands import BandsWindow
|
||||
from NanoVNASaver.Windows.MarkerSettings import MarkerSettingsWindow
|
||||
from NanoVNASaver.Marker import Marker
|
||||
|
@ -37,7 +38,6 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
self.app = app
|
||||
self.setWindowTitle("Display settings")
|
||||
self.setWindowIcon(self.app.icon)
|
||||
self.marker_cfg = ChartMarkerConfig()
|
||||
self.marker_window = MarkerSettingsWindow(self.app)
|
||||
self.callback_params = {}
|
||||
|
||||
|
@ -61,10 +61,10 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
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.setChecked(
|
||||
Defaults.cfg.chart.returnloss_is_positive)
|
||||
self.returnloss_is_negative.setChecked(
|
||||
not Defaults.cfg.chart.returnloss_is_positive)
|
||||
|
||||
self.returnloss_is_positive.toggled.connect(self.changeReturnLoss)
|
||||
self.changeReturnLoss()
|
||||
|
@ -83,7 +83,7 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
|
||||
self.pointSizeInput = QtWidgets.QSpinBox()
|
||||
self.pointSizeInput.setMinimumHeight(20)
|
||||
pointsize = self.app.settings.value("PointSize", 2, int)
|
||||
pointsize = Defaults.cfg.chart.point_size
|
||||
self.pointSizeInput.setValue(pointsize)
|
||||
self.changePointSize(pointsize)
|
||||
self.pointSizeInput.setMinimum(1)
|
||||
|
@ -95,7 +95,7 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
|
||||
self.lineThicknessInput = QtWidgets.QSpinBox()
|
||||
self.lineThicknessInput.setMinimumHeight(20)
|
||||
linethickness = self.app.settings.value("LineThickness", 1, int)
|
||||
linethickness = Defaults.cfg.chart.line_thickness
|
||||
self.lineThicknessInput.setValue(linethickness)
|
||||
self.changeLineThickness(linethickness)
|
||||
self.lineThicknessInput.setMinimum(1)
|
||||
|
@ -107,10 +107,9 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
|
||||
self.markerSizeInput = QtWidgets.QSpinBox()
|
||||
self.markerSizeInput.setMinimumHeight(20)
|
||||
markersize = self.app.settings.value("MarkerSize", 6, int)
|
||||
markersize = Defaults.cfg.chart.marker_size
|
||||
self.markerSizeInput.setValue(markersize)
|
||||
self.markerSizeInput.setMinimum(4)
|
||||
self.markerSizeInput.setMinimum(4)
|
||||
self.markerSizeInput.setMaximum(20)
|
||||
self.markerSizeInput.setSingleStep(2)
|
||||
self.markerSizeInput.setSuffix(" px")
|
||||
|
@ -137,10 +136,8 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
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.setChecked(Defaults.cfg.chart.marker_at_tip)
|
||||
self.marker_at_center.setChecked(not Defaults.cfg.chart.marker_at_tip)
|
||||
|
||||
self.marker_at_tip.toggled.connect(self.changeMarkerAtTip)
|
||||
self.changeMarkerAtTip()
|
||||
|
@ -162,10 +159,7 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
self.font_dropdown = QtWidgets.QComboBox()
|
||||
self.font_dropdown.setMinimumHeight(20)
|
||||
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.font_dropdown.setCurrentText(str(Defaults.cfg.gui.font_size))
|
||||
self.changeFont()
|
||||
|
||||
self.font_dropdown.currentTextChanged.connect(self.changeFont)
|
||||
|
@ -196,11 +190,8 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
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]
|
||||
# Single values from the .ini become floats rather than lists. Convert them.
|
||||
self.vswrMarkers = [] if self.vswrMarkers == 0.0 else [self.vswrMarkers]
|
||||
|
||||
vswr_marker_layout.addRow(
|
||||
"VSWR Markers",self.color_picker("VSWRColor", "swr"))
|
||||
|
@ -209,7 +200,7 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
self.vswr_marker_dropdown.setMinimumHeight(20)
|
||||
vswr_marker_layout.addRow(self.vswr_marker_dropdown)
|
||||
|
||||
if len(self.vswrMarkers) == 0:
|
||||
if not self.vswrMarkers:
|
||||
self.vswr_marker_dropdown.addItem("None")
|
||||
else:
|
||||
for m in self.vswrMarkers:
|
||||
|
@ -262,10 +253,7 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
# "S21 Phase",
|
||||
# "None"]
|
||||
|
||||
selections = []
|
||||
|
||||
for c in self.app.selectable_charts:
|
||||
selections.append(c.name)
|
||||
selections = [c.name for c in self.app.selectable_charts]
|
||||
|
||||
selections.append("None")
|
||||
chart00_selection = QtWidgets.QComboBox()
|
||||
|
@ -364,14 +352,11 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
"VSWRColor", defaultValue=ChartColors.swr,
|
||||
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.dark_mode_option.setChecked(Defaults.cfg.gui.dark_mode)
|
||||
self.show_lines_option.setChecked(Defaults.cfg.chart.show_lines)
|
||||
self.show_marker_number_option.setChecked(
|
||||
self.app.settings.value("ShowMarkerNumbers", ChartMarkerConfig.draw_label, bool))
|
||||
self.filled_marker_option.setChecked(
|
||||
self.app.settings.value("FilledMarkers", ChartMarkerConfig.fill, bool))
|
||||
Defaults.cfg.chart.marker_label)
|
||||
self.filled_marker_option.setChecked(Defaults.cfg.chart.marker_filled)
|
||||
|
||||
if self.app.settings.value("UseCustomColors",
|
||||
defaultValue=False, type=bool):
|
||||
|
@ -450,8 +435,7 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
|
||||
def changeReturnLoss(self):
|
||||
state = self.returnloss_is_positive.isChecked()
|
||||
self.app.settings.setValue("ReturnLossPositive", state)
|
||||
|
||||
Defaults.cfg.chart.returnloss_is_positive = bool(state)
|
||||
for m in self.app.markers:
|
||||
m.returnloss_is_positive = state
|
||||
m.updateLabels(self.app.data.s11, self.app.data.s21)
|
||||
|
@ -462,56 +446,51 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
|
||||
def changeShowLines(self):
|
||||
state = self.show_lines_option.isChecked()
|
||||
self.app.settings.setValue("ShowLines", state)
|
||||
Defaults.cfg.chart.show_lines = bool(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)
|
||||
ChartMarker.cfg.draw_label = state
|
||||
Defaults.cfg.chart.marker_label = bool(
|
||||
self.show_marker_number_option.isChecked())
|
||||
self.updateCharts()
|
||||
|
||||
def changeFilledMarkers(self):
|
||||
state = self.filled_marker_option.isChecked()
|
||||
self.app.settings.setValue("FilledMarkers", state)
|
||||
ChartMarker.cfg.fill = state
|
||||
Defaults.cfg.chart.marker_filled = bool(
|
||||
self.filled_marker_option.isChecked())
|
||||
self.updateCharts()
|
||||
|
||||
def changeMarkerAtTip(self):
|
||||
state = self.marker_at_tip.isChecked()
|
||||
self.app.settings.setValue("MarkerAtTip", state)
|
||||
ChartMarker.cfg.at_tip = state
|
||||
Defaults.cfg.chart.marker_at_tip = bool(
|
||||
self.marker_at_tip.isChecked())
|
||||
self.updateCharts()
|
||||
|
||||
def changePointSize(self, size: int):
|
||||
self.app.settings.setValue("PointSize", size)
|
||||
Defaults.cfg.chart.point_size = size
|
||||
for c in self.app.subscribing_charts:
|
||||
c.setPointSize(size)
|
||||
|
||||
def changeLineThickness(self, size: int):
|
||||
self.app.settings.setValue("LineThickness", size)
|
||||
Defaults.cfg.chart.line_thickness = int(size)
|
||||
for c in self.app.subscribing_charts:
|
||||
c.setLineThickness(size)
|
||||
|
||||
def changeMarkerSize(self, size: int):
|
||||
self.app.settings.setValue("MarkerSize", size)
|
||||
ChartMarker.cfg.size = size
|
||||
Defaults.cfg.chart.marker_size = size
|
||||
self.markerSizeInput.setValue(size)
|
||||
self.updateCharts()
|
||||
|
||||
def changeDarkMode(self):
|
||||
state = self.dark_mode_option.isChecked()
|
||||
self.app.settings.setValue("DarkMode", state)
|
||||
Defaults.cfg.gui.dark_mode = bool(state)
|
||||
Chart.color.foreground = QtGui.QColor(QtCore.Qt.lightGray)
|
||||
if state:
|
||||
Chart.color.background = QtGui.QColor(QtCore.Qt.black)
|
||||
Chart.color.text = QtGui.QColor(QtCore.Qt.white)
|
||||
Chart.color.swr = Chart.color.swr
|
||||
else:
|
||||
Chart.color.background = QtGui.QColor(QtCore.Qt.white)
|
||||
Chart.color.text = QtGui.QColor(QtCore.Qt.black)
|
||||
Chart.color.swr = Chart.color.swr
|
||||
Chart.color.swr = Chart.color.swr
|
||||
self.updateCharts()
|
||||
|
||||
def changeSetting(self, setting: str, value: str):
|
||||
|
@ -547,11 +526,11 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
c.update()
|
||||
|
||||
def changeFont(self):
|
||||
font_size = self.font_dropdown.currentText()
|
||||
self.app.settings.setValue("FontSize", font_size)
|
||||
font_size = int(self.font_dropdown.currentText())
|
||||
Defaults.cfg.gui.font_size = font_size
|
||||
app: QtWidgets.QApplication = QtWidgets.QApplication.instance()
|
||||
font = app.font()
|
||||
font.setPointSize(int(font_size))
|
||||
font.setPointSize(font_size)
|
||||
app.setFont(font)
|
||||
self.app.changeFont(font)
|
||||
|
||||
|
@ -628,3 +607,4 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
def updateCharts(self):
|
||||
for c in self.app.subscribing_charts:
|
||||
c.update()
|
||||
Defaults.store(self.app.settings, Defaults.cfg)
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
exec python -m debugpy --listen 5678 --wait-for-client $@
|
|
@ -15,10 +15,13 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
try:
|
||||
from contextlib import suppress
|
||||
|
||||
with suppress(ImportError):
|
||||
# pylint: disable=no-name-in-module,import-error,unused-import
|
||||
# pyright: reportMissingImports=false
|
||||
import pkg_resources.py2_warn
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from NanoVNASaver.__main__ import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
# NanoVNASaver
|
||||
#
|
||||
# A python program to view and export Touchstone data from a NanoVNA
|
||||
# Copyright (C) 2019, 2020 Rune B. Broberg
|
||||
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import unittest
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import NanoVNASaver.Defaults as CFG
|
||||
|
||||
|
||||
@dataclass
|
||||
class TConfig:
|
||||
my_int: int = 3
|
||||
my_float: float = 3.14
|
||||
my_str: str = "Hello World"
|
||||
my_bool: bool = True
|
||||
my_list: list = field(default_factory=lambda: [1, 2, 3])
|
||||
my_bytearray: bytearray = field(default_factory=lambda: bytearray((1,2,3)))
|
||||
|
||||
|
||||
class TestCases(unittest.TestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.settings_1 = CFG.AppSettings(
|
||||
CFG.QSettings.IniFormat,
|
||||
CFG.QSettings.UserScope,
|
||||
"NanoVNASaver", "Test_1")
|
||||
self.settings_2 = CFG.AppSettings(
|
||||
CFG.QSettings.IniFormat,
|
||||
CFG.QSettings.UserScope,
|
||||
"NanoVNASaver", "Test_2")
|
||||
self.config_1 = TConfig()
|
||||
self.config_2 = TConfig(
|
||||
my_int=4,
|
||||
my_float=3.0,
|
||||
my_str="Goodbye World",
|
||||
my_bool=False,
|
||||
my_list=[4, 5, 6])
|
||||
|
||||
def test_store_dataclass(self):
|
||||
self.settings_1.store_dataclass("Section1", self.config_1)
|
||||
self.settings_1.store_dataclass("Section2", self.config_2)
|
||||
illegal_config = TConfig(
|
||||
my_int=4, my_float=3.0, my_str="Goodbye World",
|
||||
my_bool="False", my_list=(4, 5, 6))
|
||||
with self.assertRaises(AssertionError):
|
||||
self.settings_1.store_dataclass("SectionX", illegal_config)
|
||||
|
||||
def test_restore_dataclass(self):
|
||||
tc_1 = self.settings_1.restore_dataclass("Section1", TConfig())
|
||||
tc_2 = self.settings_1.restore_dataclass("Section2", TConfig())
|
||||
self.assertNotEqual(tc_1, tc_2)
|
||||
self.assertEqual(tc_1, self.config_1)
|
||||
self.assertEqual(tc_2, self.config_2)
|
||||
self.assertEqual(tc_2.my_int, 4)
|
||||
self.assertEqual(tc_2.my_float, 3.0)
|
||||
self.assertEqual(tc_2.my_str, "Goodbye World")
|
||||
self.assertEqual(tc_2.my_bool, False)
|
||||
self.assertEqual(tc_2.my_list, [4, 5, 6])
|
||||
self.assertIsInstance(tc_2.my_int, int)
|
||||
self.assertIsInstance(tc_2.my_float, float)
|
||||
|
||||
def test_restore_empty(self):
|
||||
tc_3 = self.settings_1.restore_dataclass("Section3", TConfig())
|
||||
self.assertEqual(tc_3, TConfig())
|
||||
|
||||
def test_store(self):
|
||||
tc_1 = CFG.CFG()
|
||||
tc_1.gui.dark_mode = not tc_1.gui.dark_mode
|
||||
CFG.store(self.settings_2, tc_1)
|
||||
tc_2 = CFG.restore(self.settings_2)
|
||||
print(f"\n{tc_1}\n{tc_2}\n")
|
||||
self.assertEqual(tc_1, tc_2)
|
||||
self.assertNotEqual(tc_2.gui, CFG.GUI())
|
Ładowanie…
Reference in New Issue