2020-06-12 20:16:26 +00:00
|
|
|
# NanoVNASaver
|
2020-06-25 17:52:30 +00:00
|
|
|
#
|
2020-06-12 20:16:26 +00:00
|
|
|
# A python program to view and export Touchstone data from a NanoVNA
|
2020-06-25 17:52:30 +00:00
|
|
|
# Copyright (C) 2019, 2020 Rune B. Broberg
|
2021-06-30 05:21:14 +00:00
|
|
|
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
|
2019-09-01 21:13:21 +00:00
|
|
|
#
|
|
|
|
# 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/>.
|
2019-09-22 11:42:05 +00:00
|
|
|
import logging
|
2021-06-26 21:08:56 +00:00
|
|
|
|
2021-06-26 22:34:06 +00:00
|
|
|
from dataclasses import dataclass, replace
|
2021-06-26 22:55:43 +00:00
|
|
|
from typing import List, Set, Tuple, ClassVar, Any
|
2019-09-01 21:13:21 +00:00
|
|
|
|
|
|
|
from PyQt5 import QtWidgets, QtGui, QtCore
|
2019-10-13 08:37:14 +00:00
|
|
|
from PyQt5.QtCore import pyqtSignal
|
2019-09-01 21:13:21 +00:00
|
|
|
|
2022-05-24 15:05:59 +00:00
|
|
|
from NanoVNASaver import Defaults
|
2020-05-18 18:58:17 +00:00
|
|
|
from NanoVNASaver.RFTools import Datapoint
|
|
|
|
from NanoVNASaver.Marker import Marker
|
2019-09-01 21:13:21 +00:00
|
|
|
|
2021-06-26 21:08:56 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
2019-09-01 21:13:21 +00:00
|
|
|
|
2021-07-06 15:01:20 +00:00
|
|
|
|
2021-06-26 21:08:56 +00:00
|
|
|
@dataclass
|
2021-06-27 13:08:01 +00:00
|
|
|
class ChartColors: # pylint: disable=too-many-instance-attributes
|
2021-06-26 22:16:58 +00:00
|
|
|
background: QtGui.QColor = QtGui.QColor(QtCore.Qt.white)
|
|
|
|
foreground: QtGui.QColor = QtGui.QColor(QtCore.Qt.lightGray)
|
2021-07-06 15:01:20 +00:00
|
|
|
reference: QtGui.QColor = QtGui.QColor(0, 0, 255, 64)
|
|
|
|
reference_secondary: QtGui.QColor = QtGui.QColor(0, 0, 192, 48)
|
|
|
|
sweep: QtGui.QColor = QtGui.QColor(QtCore.Qt.darkYellow)
|
|
|
|
sweep_secondary: QtGui.QColor = QtGui.QColor(QtCore.Qt.darkMagenta)
|
|
|
|
swr: QtGui.QColor = QtGui.QColor(255, 0, 0, 128)
|
2021-06-26 22:16:58 +00:00
|
|
|
text: QtGui.QColor = QtGui.QColor(QtCore.Qt.black)
|
2021-07-06 15:01:20 +00:00
|
|
|
bands: QtGui.QColor = QtGui.QColor(128, 128, 128, 48)
|
2021-06-26 21:08:56 +00:00
|
|
|
|
2021-06-26 22:55:43 +00:00
|
|
|
@dataclass
|
|
|
|
class ChartDimensions:
|
|
|
|
height: int = 200
|
|
|
|
height_min: int = 200
|
|
|
|
width: int = 200
|
|
|
|
width_min: int = 200
|
|
|
|
line: int = 1
|
|
|
|
point: int = 2
|
|
|
|
|
2021-06-27 08:59:07 +00:00
|
|
|
@dataclass
|
|
|
|
class ChartDragBox:
|
|
|
|
pos: Tuple[int] = (-1, -1)
|
|
|
|
pos_start: Tuple[int] = (0, 0)
|
|
|
|
state: bool = False
|
|
|
|
move_x: int = -1
|
|
|
|
move_y: int = -1
|
2021-06-26 22:16:58 +00:00
|
|
|
|
2021-06-27 08:59:07 +00:00
|
|
|
@dataclass
|
|
|
|
class ChartFlags:
|
|
|
|
draw_lines: bool = False
|
|
|
|
is_popout: bool = False
|
2021-06-26 22:55:43 +00:00
|
|
|
|
2021-07-06 07:25:20 +00:00
|
|
|
|
2021-07-05 19:09:43 +00:00
|
|
|
class ChartMarker(QtWidgets.QWidget):
|
2021-07-06 15:01:20 +00:00
|
|
|
def __init__(self, qp: QtGui.QPaintDevice):
|
2021-07-05 19:09:43 +00:00
|
|
|
super().__init__()
|
|
|
|
self.qp = qp
|
|
|
|
|
2021-07-06 07:25:20 +00:00
|
|
|
def draw(self, x: int, y: int, color: QtGui.QColor, text: str = ""):
|
2022-05-24 15:05:59 +00:00
|
|
|
offset = Defaults.cfg.chart.marker_size // 2
|
|
|
|
if Defaults.cfg.chart.marker_at_tip:
|
2021-07-06 15:01:20 +00:00
|
|
|
y -= offset
|
2021-07-05 19:09:43 +00:00
|
|
|
pen = QtGui.QPen(color)
|
|
|
|
self.qp.setPen(pen)
|
|
|
|
qpp = QtGui.QPainterPath()
|
2021-07-06 15:01:20 +00:00
|
|
|
qpp.moveTo(x, y + offset)
|
|
|
|
qpp.lineTo(x - offset, y - offset)
|
|
|
|
qpp.lineTo(x + offset, y - offset)
|
|
|
|
qpp.lineTo(x, y + offset)
|
2021-07-05 19:09:43 +00:00
|
|
|
|
2022-05-24 15:05:59 +00:00
|
|
|
if Defaults.cfg.chart.marker_filled:
|
2021-07-05 19:09:43 +00:00
|
|
|
self.qp.fillPath(qpp, color)
|
|
|
|
else:
|
|
|
|
self.qp.drawPath(qpp)
|
|
|
|
|
2022-05-24 15:05:59 +00:00
|
|
|
if text and Defaults.cfg.chart.marker_label:
|
2021-07-06 15:01:20 +00:00
|
|
|
text_width = self.qp.fontMetrics().horizontalAdvance(text)
|
|
|
|
self.qp.drawText(x - text_width // 2, y - 3 - offset, text)
|
2021-07-05 19:09:43 +00:00
|
|
|
|
|
|
|
|
2021-06-27 08:59:07 +00:00
|
|
|
class Chart(QtWidgets.QWidget):
|
2021-06-26 22:55:43 +00:00
|
|
|
bands: ClassVar[Any] = None
|
|
|
|
popoutRequested: ClassVar[Any] = pyqtSignal(object)
|
2021-07-06 07:25:20 +00:00
|
|
|
color: ClassVar[ChartColors] = ChartColors()
|
2019-10-13 08:37:14 +00:00
|
|
|
|
2019-09-19 12:15:47 +00:00
|
|
|
def __init__(self, name):
|
|
|
|
super().__init__()
|
|
|
|
self.name = name
|
2022-05-14 09:00:34 +00:00
|
|
|
self.sweepTitle = ''
|
2021-06-27 08:59:07 +00:00
|
|
|
|
2022-05-28 20:50:07 +00:00
|
|
|
self.leftMargin = 30
|
|
|
|
self.rightMargin = 20
|
|
|
|
self.bottomMargin = 20
|
|
|
|
self.topMargin = 30
|
|
|
|
|
2021-06-26 22:55:43 +00:00
|
|
|
self.dim = ChartDimensions()
|
2021-06-27 08:59:07 +00:00
|
|
|
self.dragbox = ChartDragBox()
|
|
|
|
self.flag = ChartFlags()
|
|
|
|
|
|
|
|
self.draggedMarker = None
|
|
|
|
|
2021-06-26 21:08:56 +00:00
|
|
|
self.data: List[Datapoint] = []
|
|
|
|
self.reference: List[Datapoint] = []
|
2021-06-27 08:59:07 +00:00
|
|
|
|
2021-06-26 21:08:56 +00:00
|
|
|
self.markers: List[Marker] = []
|
|
|
|
self.swrMarkers: Set[float] = set()
|
|
|
|
|
2019-10-13 08:37:14 +00:00
|
|
|
self.action_popout = QtWidgets.QAction("Popout chart")
|
|
|
|
self.action_popout.triggered.connect(lambda: self.popoutRequested.emit(self))
|
|
|
|
self.addAction(self.action_popout)
|
2019-09-29 18:30:25 +00:00
|
|
|
|
2021-06-26 21:08:56 +00:00
|
|
|
self.action_save_screenshot = QtWidgets.QAction("Save image")
|
|
|
|
self.action_save_screenshot.triggered.connect(self.saveScreenshot)
|
|
|
|
self.addAction(self.action_save_screenshot)
|
|
|
|
|
|
|
|
self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
|
2019-10-13 15:35:32 +00:00
|
|
|
|
2019-09-01 21:13:21 +00:00
|
|
|
def setReference(self, data):
|
|
|
|
self.reference = data
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
def resetReference(self):
|
|
|
|
self.reference = []
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
def setData(self, data):
|
|
|
|
self.data = data
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
def setMarkers(self, markers):
|
|
|
|
self.markers = markers
|
2019-09-05 12:56:40 +00:00
|
|
|
|
2019-09-26 20:57:34 +00:00
|
|
|
def setBands(self, bands):
|
|
|
|
self.bands = bands
|
|
|
|
|
2019-10-14 17:34:59 +00:00
|
|
|
def setLineThickness(self, thickness):
|
2021-06-26 22:55:43 +00:00
|
|
|
self.dim.line = thickness
|
2019-10-14 17:34:59 +00:00
|
|
|
self.update()
|
|
|
|
|
|
|
|
def setPointSize(self, size):
|
2021-06-26 22:55:43 +00:00
|
|
|
self.dim.point = size
|
2019-10-14 17:34:59 +00:00
|
|
|
self.update()
|
|
|
|
|
2019-10-29 11:47:30 +00:00
|
|
|
def setMarkerSize(self, size):
|
2022-05-24 15:05:59 +00:00
|
|
|
Defaults.cfg.chart.marker_size = size
|
2019-10-29 11:47:30 +00:00
|
|
|
self.update()
|
|
|
|
|
2019-12-12 14:16:37 +00:00
|
|
|
def setSweepTitle(self, title):
|
|
|
|
self.sweepTitle = title
|
|
|
|
self.update()
|
|
|
|
|
2019-11-17 21:26:00 +00:00
|
|
|
def getActiveMarker(self) -> Marker:
|
2019-09-12 21:12:32 +00:00
|
|
|
if self.draggedMarker is not None:
|
|
|
|
return self.draggedMarker
|
2022-05-14 09:00:34 +00:00
|
|
|
return next(
|
|
|
|
(
|
|
|
|
m
|
|
|
|
for m in self.markers
|
|
|
|
if m.isMouseControlledRadioButton.isChecked()
|
|
|
|
),
|
|
|
|
None,
|
|
|
|
)
|
2019-09-11 19:59:48 +00:00
|
|
|
|
2019-09-12 12:30:50 +00:00
|
|
|
def getNearestMarker(self, x, y) -> Marker:
|
2019-09-12 21:12:32 +00:00
|
|
|
if len(self.data) == 0:
|
|
|
|
return None
|
2019-09-12 12:30:50 +00:00
|
|
|
shortest = 10**6
|
|
|
|
nearest = None
|
|
|
|
for m in self.markers:
|
|
|
|
mx, my = self.getPosition(self.data[m.location])
|
2021-07-05 09:37:48 +00:00
|
|
|
distance = abs(complex(x - mx, y - my))
|
2019-09-12 12:30:50 +00:00
|
|
|
if distance < shortest:
|
|
|
|
shortest = distance
|
|
|
|
nearest = m
|
|
|
|
return nearest
|
|
|
|
|
2021-06-22 20:07:36 +00:00
|
|
|
def getPosition(self, d: Datapoint) -> Tuple[int, int]:
|
2019-09-12 13:41:56 +00:00
|
|
|
return self.getXPosition(d), self.getYPosition(d)
|
2019-09-12 12:30:50 +00:00
|
|
|
|
2019-11-06 14:45:55 +00:00
|
|
|
def setDrawLines(self, draw_lines):
|
2021-06-27 08:59:07 +00:00
|
|
|
self.flag.draw_lines = draw_lines
|
2019-09-07 09:43:06 +00:00
|
|
|
self.update()
|
|
|
|
|
2019-09-12 21:12:32 +00:00
|
|
|
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
|
2019-09-24 21:29:26 +00:00
|
|
|
if event.buttons() == QtCore.Qt.RightButton:
|
|
|
|
event.ignore()
|
|
|
|
return
|
2020-06-15 11:27:00 +00:00
|
|
|
if event.buttons() == QtCore.Qt.MiddleButton:
|
2019-11-16 11:07:32 +00:00
|
|
|
# Drag event
|
|
|
|
event.accept()
|
2021-06-27 08:59:07 +00:00
|
|
|
self.dragbox.move_x = event.x()
|
|
|
|
self.dragbox.move_y = event.y()
|
2019-11-16 11:07:32 +00:00
|
|
|
return
|
2021-06-27 13:08:01 +00:00
|
|
|
if event.modifiers() == QtCore.Qt.ControlModifier:
|
2019-11-16 11:07:32 +00:00
|
|
|
event.accept()
|
2021-06-27 08:59:07 +00:00
|
|
|
self.dragbox.state = True
|
|
|
|
self.dragbox.pos_start = (event.x(), event.y())
|
2019-11-08 09:17:58 +00:00
|
|
|
return
|
2021-06-27 13:08:01 +00:00
|
|
|
if event.modifiers() == QtCore.Qt.ShiftModifier:
|
|
|
|
self.draggedMarker = self.getNearestMarker(event.x(), event.y())
|
2019-09-12 21:12:32 +00:00
|
|
|
self.mouseMoveEvent(event)
|
|
|
|
|
2021-07-05 09:37:48 +00:00
|
|
|
def mouseReleaseEvent(self, a0: QtGui.QMouseEvent):
|
2019-09-12 21:12:32 +00:00
|
|
|
self.draggedMarker = None
|
2021-06-27 08:59:07 +00:00
|
|
|
if self.dragbox.state:
|
|
|
|
self.zoomTo(self.dragbox.pos_start[0], self.dragbox.pos_start[1], a0.x(), a0.y())
|
|
|
|
self.dragbox.state = False
|
|
|
|
self.dragbox.pos = (-1, -1)
|
|
|
|
self.dragbox.pos_start = (0, 0)
|
2019-11-08 09:17:58 +00:00
|
|
|
self.update()
|
|
|
|
|
2022-05-28 19:48:13 +00:00
|
|
|
|
|
|
|
def wheelEvent(self, a0: QtGui.QWheelEvent) -> None:
|
|
|
|
delta = a0.angleDelta().y()
|
|
|
|
if not delta or (not self.data and not self.reference):
|
|
|
|
a0.ignore()
|
|
|
|
return
|
|
|
|
modifiers = a0.modifiers()
|
|
|
|
|
|
|
|
zoom_x = modifiers != QtCore.Qt.ShiftModifier
|
|
|
|
zoom_y = modifiers != QtCore.Qt.ControlModifier
|
|
|
|
rate = -delta / 120
|
|
|
|
# zooming in 10% increments and 9% complementary
|
|
|
|
divisor = 10 if delta > 0 else 9
|
|
|
|
|
|
|
|
factor_x = rate * self.dim.width / divisor if zoom_x else 0
|
|
|
|
factor_y = rate * self.dim.height / divisor if zoom_y else 0
|
|
|
|
|
|
|
|
abs_x = max(0, a0.x() - self.leftMargin)
|
|
|
|
abs_y = max(0, a0.y() - self.topMargin)
|
|
|
|
|
|
|
|
ratio_x = abs_x / self.dim.width
|
|
|
|
ratio_y = abs_y / self.dim.height
|
|
|
|
|
|
|
|
self.zoomTo(
|
|
|
|
int(self.leftMargin + ratio_x * factor_x),
|
|
|
|
int(self.topMargin + ratio_y * factor_y),
|
|
|
|
int(self.leftMargin + self.dim.width - (1 - ratio_x) * factor_x),
|
|
|
|
int(self.topMargin + self.dim.height - (1 - ratio_y) * factor_y)
|
|
|
|
)
|
|
|
|
a0.accept()
|
|
|
|
|
2019-11-08 09:17:58 +00:00
|
|
|
def zoomTo(self, x1, y1, x2, y2):
|
2021-07-05 09:37:48 +00:00
|
|
|
raise NotImplementedError()
|
2019-09-12 12:30:50 +00:00
|
|
|
|
2019-09-29 18:30:25 +00:00
|
|
|
def saveScreenshot(self):
|
|
|
|
logger.info("Saving %s to file...", self.name)
|
2021-06-27 13:08:01 +00:00
|
|
|
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
|
|
|
|
parent=self, caption="Save image",
|
|
|
|
filter="PNG (*.png);;All files (*.*)")
|
2019-09-29 18:30:25 +00:00
|
|
|
|
|
|
|
logger.debug("Filename: %s", filename)
|
2021-06-27 13:08:01 +00:00
|
|
|
if not filename:
|
|
|
|
return
|
|
|
|
if not QtCore.QFileInfo(filename).suffix():
|
|
|
|
filename += ".png"
|
|
|
|
self.grab().save(filename)
|
2019-09-29 18:30:25 +00:00
|
|
|
|
2019-10-13 08:37:14 +00:00
|
|
|
def copy(self):
|
|
|
|
new_chart = self.__class__(self.name)
|
|
|
|
new_chart.data = self.data
|
|
|
|
new_chart.reference = self.reference
|
2021-06-27 08:59:07 +00:00
|
|
|
new_chart.dim = replace(self.dim)
|
|
|
|
new_chart.flag = replace(self.flag)
|
2019-10-13 08:37:14 +00:00
|
|
|
new_chart.markers = self.markers
|
2019-10-13 15:35:32 +00:00
|
|
|
new_chart.swrMarkers = self.swrMarkers
|
2019-10-13 08:37:14 +00:00
|
|
|
new_chart.bands = self.bands
|
2021-06-27 08:59:07 +00:00
|
|
|
|
2019-10-13 08:37:14 +00:00
|
|
|
new_chart.resize(self.width(), self.height())
|
2021-06-26 22:55:43 +00:00
|
|
|
new_chart.setPointSize(self.dim.point)
|
|
|
|
new_chart.setLineThickness(self.dim.line)
|
2019-10-13 08:37:14 +00:00
|
|
|
return new_chart
|
|
|
|
|
2019-10-13 15:35:32 +00:00
|
|
|
def addSWRMarker(self, swr: float):
|
|
|
|
self.swrMarkers.add(swr)
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
def removeSWRMarker(self, swr: float):
|
|
|
|
try:
|
|
|
|
self.swrMarkers.remove(swr)
|
|
|
|
except KeyError:
|
|
|
|
logger.debug("KeyError from %s", self.name)
|
|
|
|
finally:
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
def clearSWRMarkers(self):
|
|
|
|
self.swrMarkers.clear()
|
|
|
|
self.update()
|
|
|
|
|
2022-05-28 19:48:13 +00:00
|
|
|
@staticmethod
|
|
|
|
def drawMarker(x: int, y: int,
|
|
|
|
qp: QtGui.QPainter, color: QtGui.QColor,
|
|
|
|
number: int=0):
|
2021-07-06 15:01:20 +00:00
|
|
|
cmarker = ChartMarker(qp)
|
2022-05-28 19:48:13 +00:00
|
|
|
cmarker.draw(x, y, color, f"{number}")
|
2019-10-29 13:21:22 +00:00
|
|
|
|
2019-12-12 14:16:37 +00:00
|
|
|
def drawTitle(self, qp: QtGui.QPainter, position: QtCore.QPoint = None):
|
2021-07-06 15:01:20 +00:00
|
|
|
qp.setPen(Chart.color.text)
|
2021-06-27 13:08:01 +00:00
|
|
|
if position is None:
|
|
|
|
qf = QtGui.QFontMetricsF(self.font())
|
|
|
|
width = qf.boundingRect(self.sweepTitle).width()
|
|
|
|
position = QtCore.QPointF(self.width()/2 - width/2, 15)
|
|
|
|
qp.drawText(position, self.sweepTitle)
|
2021-07-06 07:25:20 +00:00
|
|
|
|
|
|
|
def update(self):
|
|
|
|
pal = self.palette()
|
2021-07-06 15:01:20 +00:00
|
|
|
pal.setColor(QtGui.QPalette.Background, Chart.color.background)
|
2021-07-06 07:25:20 +00:00
|
|
|
self.setPalette(pal)
|
|
|
|
super().update()
|