From 899886e8d828fda14c0248eaa38252a6685ae542 Mon Sep 17 00:00:00 2001 From: "Rune B. Broberg" Date: Sun, 8 Dec 2019 16:29:55 +0100 Subject: [PATCH 1/6] Added back k/m to impedance display Fixed some style issues. --- NanoVNASaver/Marker.py | 69 +++++++++++++++++++---------------------- NanoVNASaver/RFTools.py | 13 +++++--- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/NanoVNASaver/Marker.py b/NanoVNASaver/Marker.py index 94a5697..8996051 100644 --- a/NanoVNASaver/Marker.py +++ b/NanoVNASaver/Marker.py @@ -26,7 +26,7 @@ from NanoVNASaver import RFTools FMT_FREQ = SITools.Format(space_str=" ") FMT_Q_FACTOR = SITools.Format(max_nr_digits=4, assume_infinity=False, min_offset=0, max_offset=0, allow_strip=True) -FMT_GROUP_DELAY = SITools.Format(max_nr_digits=5) +FMT_GROUP_DELAY = SITools.Format(max_nr_digits=5, space_str=" ") FMT_REACT = SITools.Format(max_nr_digits=5, space_str=" ", allow_strip=True) @@ -37,7 +37,7 @@ def formatFrequency(freq: float) -> str: def format_gain(val: float, invert: bool = False) -> str: if invert: val = -val - return f"{val:.3f}dB" + return f"{val:.3f} dB" def format_q_factor(val: float) -> str: @@ -78,14 +78,23 @@ def format_phase(val: float) -> str: def format_complex_imp(z: complex) -> str: if z.real > 0: - s = f"{z.real:.4g}" + if z.real >= 1000: + s = f"{z.real/1000:.3g}k" + else: + s = f"{z.real:.4g}" else: s = "- " if z.imag < 0: - s += "-j" + s += " -j" else: - s += "+j" - return s + f"{abs(z.imag):.4g}\N{OHM SIGN}" + s += " +j" + if abs(z.imag) >= 1000: + s += f"{abs(z.imag)/1000:.3g}k" + elif abs(z.imag) < 0.1: + s += f"{abs(z.imag)*1000:.3g}m" + else: + s += f"{abs(z.imag):.3g}" + return s + " \N{OHM SIGN}" class Marker(QtCore.QObject): @@ -131,9 +140,9 @@ class Marker(QtCore.QObject): self.frequencyInput.setAlignment(QtCore.Qt.AlignRight) self.frequencyInput.textEdited.connect(self.setFrequency) - ############################################################### + ################################################################################################################ # Data display label - ############################################################### + ################################################################################################################ self.frequency_label = QtWidgets.QLabel("") self.frequency_label.setMinimumWidth(100) @@ -179,9 +188,9 @@ class Marker(QtCore.QObject): "s21groupdelay": ("S21 Group Delay:", self.s21_group_delay_label), } - ############################################################### + ################################################################################################################ # Marker control layout - ############################################################### + ################################################################################################################ self.btnColorPicker = QtWidgets.QPushButton("█") self.btnColorPicker.setFixedWidth(20) @@ -196,9 +205,9 @@ class Marker(QtCore.QObject): self.layout.addWidget(self.btnColorPicker) self.layout.addWidget(self.isMouseControlledRadioButton) - ############################################################### + ################################################################################################################ # Data display layout - ############################################################### + ################################################################################################################ self.group_box = QtWidgets.QGroupBox(self.name) self.group_box.setMaximumWidth(340) @@ -289,7 +298,7 @@ class Marker(QtCore.QObject): # self.right_form.addRow("S21 Phase:", self.s21_phase_label) def setFrequency(self, frequency): - f = RFTools.RFTools.parseFrequency(frequency) + f = RFTools.parseFrequency(frequency) self.frequency = max(f, 0) self.updated.emit(self) @@ -340,8 +349,7 @@ class Marker(QtCore.QObject): upper_stepsize = data[-1].freq - data[-2].freq # We are outside the bounds of the data, so we can't put in a marker - if (self.frequency + lower_stepsize/2 < min_freq or - self.frequency - upper_stepsize/2 > max_freq): + if self.frequency + lower_stepsize/2 < min_freq or self.frequency - upper_stepsize/2 > max_freq: return min_distance = max_freq @@ -384,9 +392,7 @@ class Marker(QtCore.QObject): self.s21_group_delay_label.setText("") self.quality_factor_label.setText("") - def updateLabels(self, - s11data: List[RFTools.Datapoint], - s21data: List[RFTools.Datapoint]): + def updateLabels(self, s11data: List[RFTools.Datapoint], s21data: List[RFTools.Datapoint]): if self.location == -1: return s11 = s11data[self.location] @@ -394,16 +400,12 @@ class Marker(QtCore.QObject): s21 = s21data[self.location] imp = s11.impedance() - cap_str = format_capacity( - RFTools.impedance_to_capacity(imp, s11.freq)) - ind_str = format_inductance( - RFTools.impedance_to_inductance(imp, s11.freq)) + cap_str = format_capacity(RFTools.impedance_to_capacity(imp, s11.freq)) + ind_str = format_inductance(RFTools.impedance_to_inductance(imp, s11.freq)) imp_p = RFTools.serial_to_parallel(imp) - cap_p_str = format_capacity( - RFTools.impedance_to_capacity(imp_p, s11.freq)) - ind_p_str = format_inductance( - RFTools.impedance_to_inductance(imp_p, s11.freq)) + cap_p_str = format_capacity(RFTools.impedance_to_capacity(imp_p, s11.freq)) + ind_p_str = format_inductance(RFTools.impedance_to_inductance(imp_p, s11.freq)) if imp.imag < 0: x_str = cap_str @@ -431,14 +433,10 @@ class Marker(QtCore.QObject): self.vswr_label.setText(format_vswr(s11.vswr)) self.s11_phase_label.setText(format_phase(s11.phase)) - self.quality_factor_label.setText( - format_q_factor(s11.qFactor())) + self.quality_factor_label.setText(format_q_factor(s11.qFactor())) - self.returnloss_label.setText( - format_gain(s11.gain, self.returnloss_is_positive)) - self.s11_group_delay_label.setText( - format_group_delay(RFTools.groupDelay(s11data, self.location)) - ) + self.returnloss_label.setText(format_gain(s11.gain, self.returnloss_is_positive)) + self.s11_group_delay_label.setText(format_group_delay(RFTools.groupDelay(s11data, self.location))) # skip if no valid s21 data if len(s21data) != len(s11data): @@ -446,7 +444,4 @@ class Marker(QtCore.QObject): self.s21_phase_label.setText(format_phase(s21.phase)) self.gain_label.setText(format_gain(s21.gain)) - # TODO: figure out if calculation is right (S11 no division by 2) - self.s21_group_delay_label.setText( - format_group_delay(RFTools.groupDelay(s21data, self.location) / 2) - ) + self.s21_group_delay_label.setText(format_group_delay(RFTools.groupDelay(s21data, self.location) / 2)) diff --git a/NanoVNASaver/RFTools.py b/NanoVNASaver/RFTools.py index c69086e..d6dee79 100644 --- a/NanoVNASaver/RFTools.py +++ b/NanoVNASaver/RFTools.py @@ -76,6 +76,13 @@ def gamma_to_impedance(gamma: complex, ref_impedance: float = 50) -> complex: return ((-gamma - 1) / (gamma - 1)) * ref_impedance +def parseFrequency(freq: str) -> int: + try: + return int(Value(freq, "Hz", FMT_PARSE)) + except (ValueError, IndexError): + return -1 + + class Datapoint(NamedTuple): freq: int re: float @@ -140,6 +147,7 @@ def groupDelay(data: List[Datapoint], index: int) -> float: class RFTools: + # TODO: Remove this class when unused @staticmethod def formatFrequency(freq: Number) -> str: return str(Value(freq, "Hz", FMT_FREQ)) @@ -154,7 +162,4 @@ class RFTools: @staticmethod def parseFrequency(freq: str) -> int: - try: - return int(Value(freq, "Hz", FMT_PARSE)) - except (ValueError, IndexError): - return -1 + return parseFrequency(freq) \ No newline at end of file From 28e95a25bcc9cb06a0f2ee76a44cb5f21934ec05 Mon Sep 17 00:00:00 2001 From: "Rune B. Broberg" Date: Sun, 8 Dec 2019 16:32:13 +0100 Subject: [PATCH 2/6] Minor niggles --- NanoVNASaver/Marker.py | 6 +++--- NanoVNASaver/RFTools.py | 6 +++--- NanoVNASaver/SITools.py | 2 +- test/test_rftools.py | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/NanoVNASaver/Marker.py b/NanoVNASaver/Marker.py index 8996051..2ac5cf4 100644 --- a/NanoVNASaver/Marker.py +++ b/NanoVNASaver/Marker.py @@ -56,7 +56,7 @@ def format_resistance(val: float) -> str: return str(SITools.Value(val, "\N{OHM SIGN}", FMT_REACT)) -def format_capacity(val: float, allow_negative: bool=True) -> str: +def format_capacitance(val: float, allow_negative: bool=True) -> str: if not allow_negative and val < 0: return "- pF" return str(SITools.Value(val, "F", FMT_REACT)) @@ -400,11 +400,11 @@ class Marker(QtCore.QObject): s21 = s21data[self.location] imp = s11.impedance() - cap_str = format_capacity(RFTools.impedance_to_capacity(imp, s11.freq)) + cap_str = format_capacitance(RFTools.impedance_to_capacitance(imp, s11.freq)) ind_str = format_inductance(RFTools.impedance_to_inductance(imp, s11.freq)) imp_p = RFTools.serial_to_parallel(imp) - cap_p_str = format_capacity(RFTools.impedance_to_capacity(imp_p, s11.freq)) + cap_p_str = format_capacitance(RFTools.impedance_to_capacitance(imp_p, s11.freq)) ind_p_str = format_inductance(RFTools.impedance_to_inductance(imp_p, s11.freq)) if imp.imag < 0: diff --git a/NanoVNASaver/RFTools.py b/NanoVNASaver/RFTools.py index d6dee79..f3fa418 100644 --- a/NanoVNASaver/RFTools.py +++ b/NanoVNASaver/RFTools.py @@ -40,8 +40,8 @@ def serial_to_parallel(z: complex) -> complex: z_sq_sum / z.imag) -def impedance_to_capacity(z: complex, freq: float) -> float: - """Calculate capacitve equivalent for reactance""" +def impedance_to_capacitance(z: complex, freq: float) -> float: + """Calculate capacitive equivalent for reactance""" if freq == 0: return -math.inf if z.imag == 0: @@ -122,7 +122,7 @@ class Datapoint(NamedTuple): return abs(imp.imag / imp.real) def capacitiveEquivalent(self, ref_impedance: float = 50) -> float: - return impedance_to_capacity( + return impedance_to_capacitance( self.impedance(ref_impedance), self.freq) def inductiveEquivalent(self, ref_impedance: float = 50) -> float: diff --git a/NanoVNASaver/SITools.py b/NanoVNASaver/SITools.py index d442be7..55a05ec 100644 --- a/NanoVNASaver/SITools.py +++ b/NanoVNASaver/SITools.py @@ -3,7 +3,7 @@ # 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 bynanovna +# 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. # diff --git a/test/test_rftools.py b/test/test_rftools.py index 8fbe06c..76e79ef 100644 --- a/test/test_rftools.py +++ b/test/test_rftools.py @@ -21,7 +21,7 @@ from NanoVNASaver.RFTools import Datapoint, \ norm_to_impedance, impedance_to_norm, \ reflection_coefficient, gamma_to_impedance, clamp_value, \ parallel_to_serial, serial_to_parallel, \ - impedance_to_capacity, impedance_to_inductance + impedance_to_capacitance, impedance_to_inductance import math @@ -83,10 +83,10 @@ class TestRFTools(unittest.TestCase): complex(52, 260)) def test_impedance_to_capacity(self): - self.assertEqual(impedance_to_capacity(0, 0), -math.inf) - self.assertEqual(impedance_to_capacity(0, 10), math.inf) + self.assertEqual(impedance_to_capacitance(0, 0), -math.inf) + self.assertEqual(impedance_to_capacitance(0, 10), math.inf) self.assertAlmostEqual( - impedance_to_capacity(complex(50, 159.1549), 100000), + impedance_to_capacitance(complex(50, 159.1549), 100000), 1e-8) def test_impedance_to_inductance(self): From c796536282b0aa65136305c21c557c5a4ed819f6 Mon Sep 17 00:00:00 2001 From: "Rune B. Broberg" Date: Sun, 8 Dec 2019 17:18:50 +0100 Subject: [PATCH 3/6] s21 detail --- NanoVNASaver/Marker.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/NanoVNASaver/Marker.py b/NanoVNASaver/Marker.py index 2ac5cf4..7859d18 100644 --- a/NanoVNASaver/Marker.py +++ b/NanoVNASaver/Marker.py @@ -56,13 +56,13 @@ def format_resistance(val: float) -> str: return str(SITools.Value(val, "\N{OHM SIGN}", FMT_REACT)) -def format_capacitance(val: float, allow_negative: bool=True) -> str: +def format_capacitance(val: float, allow_negative: bool = True) -> str: if not allow_negative and val < 0: return "- pF" return str(SITools.Value(val, "F", FMT_REACT)) -def format_inductance(val: float, allow_negative: bool=True) -> str: +def format_inductance(val: float, allow_negative: bool = True) -> str: if not allow_negative and val < 0: return "- nH" return str(SITools.Value(val, "H", FMT_REACT)) @@ -396,8 +396,6 @@ class Marker(QtCore.QObject): if self.location == -1: return s11 = s11data[self.location] - if s21data: - s21 = s21data[self.location] imp = s11.impedance() cap_str = format_capacitance(RFTools.impedance_to_capacitance(imp, s11.freq)) @@ -442,6 +440,8 @@ class Marker(QtCore.QObject): if len(s21data) != len(s11data): return + s21 = s21data[self.location] + self.s21_phase_label.setText(format_phase(s21.phase)) self.gain_label.setText(format_gain(s21.gain)) self.s21_group_delay_label.setText(format_group_delay(RFTools.groupDelay(s21data, self.location) / 2)) From 850c8d12918e0ca82c0284f06b57c2b53c43e092 Mon Sep 17 00:00:00 2001 From: Rune Broberg Date: Mon, 9 Dec 2019 16:22:25 +0100 Subject: [PATCH 4/6] TDR Chart zoom Faster group delay chart --- NanoVNASaver/Chart.py | 208 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 206 insertions(+), 2 deletions(-) diff --git a/NanoVNASaver/Chart.py b/NanoVNASaver/Chart.py index f5e439d..0836f47 100644 --- a/NanoVNASaver/Chart.py +++ b/NanoVNASaver/Chart.py @@ -2315,6 +2315,9 @@ class TDRChart(Chart): self.action_popout.triggered.connect(lambda: self.popoutRequested.emit(self)) self.menu.addAction(self.action_popout) + self.chartWidth = self.width() - self.leftMargin - self.rightMargin + self.chartHeight = self.height() - self.bottomMargin - self.topMargin + def contextMenuEvent(self, event): self.action_set_fixed_start.setText("Start (" + str(self.minDisplayLength) + ")") self.action_set_fixed_stop.setText("Stop (" + str(self.maxDisplayLength) + ")") @@ -2405,6 +2408,27 @@ class TDRChart(Chart): if a0.buttons() == QtCore.Qt.RightButton: a0.ignore() return + if a0.buttons() == QtCore.Qt.MiddleButton: + # Drag the display + a0.accept() + if self.moveStartX != -1 and self.moveStartY != -1: + dx = self.moveStartX - a0.x() + dy = self.moveStartY - a0.y() + self.zoomTo(self.leftMargin + dx, self.topMargin + dy, + self.leftMargin + self.chartWidth + dx, self.topMargin + self.chartHeight + dy) + + self.moveStartX = a0.x() + self.moveStartY = a0.y() + return + if a0.modifiers() == QtCore.Qt.ControlModifier: + # Dragging a box + if not self.draggedBox: + self.draggedBoxStart = (a0.x(), a0.y()) + self.draggedBoxCurrent = (a0.x(), a0.y()) + self.update() + a0.accept() + return + x = a0.x() absx = x - self.leftMargin if absx < 0 or absx > self.width() - self.rightMargin: @@ -2529,8 +2553,137 @@ class TDRChart(Chart): qp.drawText(marker_point.x() - 10, marker_point.y() - 5, str(round(self.tdrWindow.distance_axis[self.markerLocation] / 2, 2)) + "m") + if self.draggedBox and self.draggedBoxCurrent[0] != -1: + dashed_pen = QtGui.QPen(self.foregroundColor, 1, QtCore.Qt.DashLine) + qp.setPen(dashed_pen) + top_left = QtCore.QPoint(self.draggedBoxStart[0], self.draggedBoxStart[1]) + bottom_right = QtCore.QPoint(self.draggedBoxCurrent[0], self.draggedBoxCurrent[1]) + rect = QtCore.QRect(top_left, bottom_right) + qp.drawRect(rect) + qp.end() + def valueAtPosition(self, y): + if len(self.tdrWindow.td) > 0: + height = self.height() - self.topMargin - self.bottomMargin + absy = (self.height() - y) - self.bottomMargin + if self.fixedValues: + min_impedance = self.minImpedance + max_impedance = self.maxImpedance + else: + min_impedance = max(0, np.min(self.tdrWindow.step_response_Z) / 1.05) + max_impedance = min(1000, np.max(self.tdrWindow.step_response_Z) * 1.05) + y_step = (max_impedance - min_impedance) / height + return y_step * absy + min_impedance + else: + return 0 + + def lengthAtPosition(self, x, limit=True): + if len(self.tdrWindow.td) > 0: + width = self.width() - self.leftMargin - self.rightMargin + absx = x - self.leftMargin + if self.fixedSpan: + max_length = self.maxDisplayLength + min_length = self.minDisplayLength + x_step = (max_length - min_length) / width + else: + min_length = 0 + max_length = self.tdrWindow.distance_axis[math.ceil(len(self.tdrWindow.distance_axis) / 2)]/2 + x_step = max_length / width + if limit and absx < 0: + return min_length + if limit and absx > width: + return max_length + return absx * x_step + min_length + else: + return 0 + + def zoomTo(self, x1, y1, x2, y2): + logger.debug("Zoom to (x,y) by (x,y): (%d, %d) by (%d, %d)", x1, y1, x2, y2) + val1 = self.valueAtPosition(y1) + val2 = self.valueAtPosition(y2) + + if val1 != val2: + self.minImpedance = round(min(val1, val2), 3) + self.maxImpedance = round(max(val1, val2), 3) + self.setFixedValues(True) + + len1 = max(0, self.lengthAtPosition(x1, limit=False)) + len2 = max(0, self.lengthAtPosition(x2, limit=False)) + + if len1 >= 0 and len2 >= 0 and len1 != len2: + self.minDisplayLength = min(len1, len2) + self.maxDisplayLength = max(len1, len2) + self.setFixedSpan(True) + + self.update() + + def wheelEvent(self, a0: QtGui.QWheelEvent) -> None: + if len(self.tdrWindow.td) == 0: + a0.ignore() + return + chart_height = self.chartHeight + chart_width = self.chartWidth + do_zoom_x = do_zoom_y = True + if a0.modifiers() == QtCore.Qt.ShiftModifier: + do_zoom_x = False + if a0.modifiers() == QtCore.Qt.ControlModifier: + do_zoom_y = False + if a0.angleDelta().y() > 0: + # Zoom in + a0.accept() + # Center of zoom = a0.x(), a0.y() + # We zoom in by 1/10 of the width/height. + rate = a0.angleDelta().y() / 120 + if do_zoom_x: + zoomx = rate * chart_width / 10 + else: + zoomx = 0 + if do_zoom_y: + zoomy = rate * chart_height / 10 + else: + zoomy = 0 + absx = max(0, a0.x() - self.leftMargin) + absy = max(0, a0.y() - self.topMargin) + ratiox = absx/chart_width + ratioy = absy/chart_height + # TODO: Change zoom to center on the mouse if possible, or extend box to the side that has room if not. + p1x = int(self.leftMargin + ratiox * zoomx) + p1y = int(self.topMargin + ratioy * zoomy) + p2x = int(self.leftMargin + chart_width - (1 - ratiox) * zoomx) + p2y = int(self.topMargin + chart_height - (1 - ratioy) * zoomy) + self.zoomTo(p1x, p1y, p2x, p2y) + elif a0.angleDelta().y() < 0: + # Zoom out + a0.accept() + # Center of zoom = a0.x(), a0.y() + # We zoom out by 1/9 of the width/height, to match zoom in. + rate = -a0.angleDelta().y() / 120 + if do_zoom_x: + zoomx = rate * chart_width / 9 + else: + zoomx = 0 + if do_zoom_y: + zoomy = rate * chart_height / 9 + else: + zoomy = 0 + absx = max(0, a0.x() - self.leftMargin) + absy = max(0, a0.y() - self.topMargin) + ratiox = absx/chart_width + ratioy = absy/chart_height + p1x = int(self.leftMargin - ratiox * zoomx) + p1y = int(self.topMargin - ratioy * zoomy) + p2x = int(self.leftMargin + chart_width + (1 - ratiox) * zoomx) + p2y = int(self.topMargin + chart_height + (1 - ratioy) * zoomy) + self.zoomTo(p1x, p1y, p2x, p2y) + else: + a0.ignore() + + def resizeEvent(self, a0: QtGui.QResizeEvent) -> None: + super().resizeEvent(a0) + self.chartWidth = self.width() - self.leftMargin - self.rightMargin + self.chartHeight = self.height() - self.bottomMargin - self.topMargin + class RealImaginaryChart(FrequencyChart): def __init__(self, name=""): @@ -3776,8 +3929,56 @@ class GroupDelayChart(FrequencyChart): self.drawFrequencyTicks(qp) - self.drawData(qp, self.data, self.sweepColor) - self.drawData(qp, self.reference, self.referenceColor) + color = self.sweepColor + pen = QtGui.QPen(color) + pen.setWidth(self.pointSize) + line_pen = QtGui.QPen(color) + line_pen.setWidth(self.lineThickness) + qp.setPen(pen) + for i in range(len(self.data)): + x = self.getXPosition(self.data[i]) + y = self.getYPositionFromDelay(self.groupDelay[i]) + if self.isPlotable(x, y): + qp.drawPoint(int(x), int(y)) + if self.drawLines and i > 0: + prevx = self.getXPosition(self.data[i - 1]) + prevy = self.getYPositionFromDelay(self.groupDelay[i - 1]) + qp.setPen(line_pen) + if self.isPlotable(x, y) and self.isPlotable(prevx, prevy): + qp.drawLine(x, y, prevx, prevy) + elif self.isPlotable(x, y) and not self.isPlotable(prevx, prevy): + new_x, new_y = self.getPlotable(x, y, prevx, prevy) + qp.drawLine(x, y, new_x, new_y) + elif not self.isPlotable(x, y) and self.isPlotable(prevx, prevy): + new_x, new_y = self.getPlotable(prevx, prevy, x, y) + qp.drawLine(prevx, prevy, new_x, new_y) + qp.setPen(pen) + + color = self.referenceColor + pen = QtGui.QPen(color) + pen.setWidth(self.pointSize) + line_pen = QtGui.QPen(color) + line_pen.setWidth(self.lineThickness) + qp.setPen(pen) + for i in range(len(self.reference)): + x = self.getXPosition(self.reference[i]) + y = self.getYPositionFromDelay(self.groupDelayReference[i]) + if self.isPlotable(x, y): + qp.drawPoint(int(x), int(y)) + if self.drawLines and i > 0: + prevx = self.getXPosition(self.reference[i - 1]) + prevy = self.getYPositionFromDelay(self.groupDelayReference[i - 1]) + qp.setPen(line_pen) + if self.isPlotable(x, y) and self.isPlotable(prevx, prevy): + qp.drawLine(x, y, prevx, prevy) + elif self.isPlotable(x, y) and not self.isPlotable(prevx, prevy): + new_x, new_y = self.getPlotable(x, y, prevx, prevy) + qp.drawLine(x, y, new_x, new_y) + elif not self.isPlotable(x, y) and self.isPlotable(prevx, prevy): + new_x, new_y = self.getPlotable(prevx, prevy, x, y) + qp.drawLine(prevx, prevy, new_x, new_y) + qp.setPen(pen) + self.drawMarkers(qp) def getYPosition(self, d: Datapoint) -> int: @@ -3788,6 +3989,9 @@ class GroupDelayChart(FrequencyChart): delay = self.groupDelayReference[self.reference.index(d)] else: delay = 0 + return self.getYPositionFromDelay(delay) + + def getYPositionFromDelay(self, delay: float): return self.topMargin + round((self.maxDelay - delay) / self.span * self.chartHeight) def valueAtPosition(self, y) -> List[float]: From f772068b3837eab1be687d1f2e98ab02a473d95a Mon Sep 17 00:00:00 2001 From: Rune Broberg Date: Wed, 11 Dec 2019 10:36:48 +0100 Subject: [PATCH 5/6] Automatically detect screenshot capability --- NanoVNASaver/Hardware.py | 36 ++++++++++++++++++++++++++++++++++++ NanoVNASaver/NanoVNASaver.py | 9 ++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/NanoVNASaver/Hardware.py b/NanoVNASaver/Hardware.py index b86ef3b..b0fcc52 100644 --- a/NanoVNASaver/Hardware.py +++ b/NanoVNASaver/Hardware.py @@ -57,6 +57,18 @@ class VNA: logger.warning("Did not recognize NanoVNA type from firmware.") return NanoVNA(app, serial_port) + def readFeatures(self) -> List[str]: + features = [] + raw_help = self.readFromCommand("help") + logger.debug("Help command output:") + logger.debug(raw_help) + + # Detect features from the help command + if "capture" in raw_help: + features.append("Screenshots") + + return features + def readFrequencies(self) -> List[str]: pass @@ -113,6 +125,29 @@ class VNA: logger.error("Unable to acquire serial lock to read firmware.") return "" + def readFromCommand(self, command) -> str: + if self.app.serialLock.acquire(): + result = "" + try: + data = "a" + while data != "": + data = self.serial.readline().decode('ascii') + self.serial.write((command + "\r").encode('ascii')) + result = "" + data = "" + sleep(0.01) + while data != "ch> ": + data = self.serial.readline().decode('ascii') + result += data + except serial.SerialException as exc: + logger.exception("Exception while reading " + command + ": %s", exc) + finally: + self.app.serialLock.release() + return result + else: + logger.error("Unable to acquire serial lock to read " + command + ".") + return "" + def readValues(self, value) -> List[str]: logger.debug("VNA reading %s", value) if self.app.serialLock.acquire(): @@ -212,6 +247,7 @@ class NanoVNA(VNA): logger.debug("Older than 0.2.0, using old sweep command.") self.features.append("Original sweep method") self.useScan = False + self.features.extend(self.readFeatures()) def isValid(self): return True diff --git a/NanoVNASaver/NanoVNASaver.py b/NanoVNASaver/NanoVNASaver.py index fa42740..5ddd7d6 100644 --- a/NanoVNASaver/NanoVNASaver.py +++ b/NanoVNASaver/NanoVNASaver.py @@ -2623,13 +2623,20 @@ class DeviceSettingsWindow(QtWidgets.QWidget): self.featureList.clear() self.featureList.addItem(self.app.vna.name + " v" + str(self.app.vna.version)) - for item in self.app.vna.getFeatures(): + 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 From ecdbe217cdf1234c9125d6dd6df49c09418b8cee Mon Sep 17 00:00:00 2001 From: Rune Broberg Date: Thu, 12 Dec 2019 15:16:37 +0100 Subject: [PATCH 6/6] Don't disconnect when data validation fails. Added a basic "sweep title" to most charts. --- NanoVNASaver/Chart.py | 88 ++++++++++++++++++------------------ NanoVNASaver/NanoVNASaver.py | 29 +++++++++++- NanoVNASaver/SweepWorker.py | 29 +++++++++--- 3 files changed, 95 insertions(+), 51 deletions(-) diff --git a/NanoVNASaver/Chart.py b/NanoVNASaver/Chart.py index 0836f47..24eb4fa 100644 --- a/NanoVNASaver/Chart.py +++ b/NanoVNASaver/Chart.py @@ -48,6 +48,7 @@ class Chart(QtWidgets.QWidget): bands = None draggedMarker: Marker = None name = "" + sweepTitle = "" drawLines = False minChartHeight = 200 minChartWidth = 200 @@ -143,6 +144,10 @@ class Chart(QtWidgets.QWidget): self.markerSize = size self.update() + def setSweepTitle(self, title): + self.sweepTitle = title + self.update() + def getActiveMarker(self) -> Marker: if self.draggedMarker is not None: return self.draggedMarker @@ -309,6 +314,15 @@ class Chart(QtWidgets.QWidget): number_y = y - self.markerSize - 3 qp.drawText(number_x, number_y, str(number)) + def drawTitle(self, qp: QtGui.QPainter, position: QtCore.QPoint = None): + if self.sweepTitle != "": + qp.setPen(self.textColor) + 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) + class FrequencyChart(Chart): fstart = 0 @@ -691,6 +705,16 @@ class FrequencyChart(Chart): qp.drawRect(rect) qp.end() + def drawChart(self, qp: QtGui.QPainter): + qp.setPen(QtGui.QPen(self.textColor)) + qp.drawText(3, 15, self.name) + qp.setPen(QtGui.QPen(self.foregroundColor)) + qp.drawLine(self.leftMargin, self.topMargin - 5, + self.leftMargin, self.topMargin + self.chartHeight + 5) + qp.drawLine(self.leftMargin-5, self.topMargin + self.chartHeight, + self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight) + self.drawTitle(qp) + def drawFrequencyTicks(self, qp): fspan = self.fstop - self.fstart qp.setPen(self.textColor) @@ -896,14 +920,6 @@ class PhaseChart(FrequencyChart): self.unwrap = unwrap self.update() - def drawChart(self, qp: QtGui.QPainter): - qp.setPen(QtGui.QPen(self.textColor)) - qp.drawText(3, 15, self.name) - qp.setPen(QtGui.QPen(self.foregroundColor)) - qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin + self.chartHeight + 5) - qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight, - self.leftMargin + self.chartWidth, self.topMargin + self.chartHeight) - def drawValues(self, qp: QtGui.QPainter): if len(self.data) == 0 and len(self.reference) == 0: return @@ -1055,15 +1071,6 @@ class VSWRChart(FrequencyChart): new_chart.logarithmicY = self.logarithmicY return new_chart - def drawChart(self, qp: QtGui.QPainter): - qp.setPen(QtGui.QPen(self.textColor)) - qp.drawText(3, 15, self.name) - qp.setPen(QtGui.QPen(self.foregroundColor)) - qp.drawLine(self.leftMargin, self.topMargin - 5, - self.leftMargin, self.topMargin + self.chartHeight + 5) - qp.drawLine(self.leftMargin-5, self.topMargin + self.chartHeight, - self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight) - def drawValues(self, qp: QtGui.QPainter): if len(self.data) == 0 and len(self.reference) == 0: return @@ -1240,6 +1247,8 @@ class PolarChart(SquareChart): centerX - int(self.chartHeight / 2 * math.sin(math.pi / 4)), centerY + int(self.chartHeight / 2 * math.sin(math.pi / 4))) + self.drawTitle(qp) + def drawValues(self, qp: QtGui.QPainter): if len(self.data) == 0 and len(self.reference) == 0: return @@ -1388,6 +1397,8 @@ class SmithChart(SquareChart): qp.drawArc(centerX - self.chartWidth*2, centerY, self.chartWidth*5, -self.chartHeight*5, int(-93.85 * 16), int(-18.85 * 16)) # Im(Z) = 0.2 + self.drawTitle(qp) + qp.setPen(self.swrColor) for swr in self.swrMarkers: if swr <= 1: @@ -1515,9 +1526,11 @@ class LogMagChart(FrequencyChart): qp.setPen(QtGui.QPen(self.textColor)) qp.drawText(3, 15, self.name + " (dB)") qp.setPen(QtGui.QPen(self.foregroundColor)) - qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5) + qp.drawLine(self.leftMargin, self.topMargin - 5, + self.leftMargin, self.topMargin+self.chartHeight+5) qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight, self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight) + self.drawTitle(qp) def drawValues(self, qp: QtGui.QPainter): if len(self.data) == 0 and len(self.reference) == 0: @@ -1714,7 +1727,8 @@ class SParameterChart(FrequencyChart): qp.drawText(10, 15, "Real") qp.drawText(self.leftMargin + self.chartWidth - 15, 15, "Imag") qp.setPen(QtGui.QPen(self.foregroundColor)) - qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5) + qp.drawLine(self.leftMargin, self.topMargin - 5, + self.leftMargin, self.topMargin+self.chartHeight+5) qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight, self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight) @@ -1890,7 +1904,8 @@ class CombinedLogMagChart(FrequencyChart): qp.drawText(10, 15, "S11") qp.drawText(self.leftMargin + self.chartWidth - 8, 15, "S21") qp.setPen(QtGui.QPen(self.foregroundColor)) - qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5) + qp.drawLine(self.leftMargin, self.topMargin - 5, + self.leftMargin, self.topMargin+self.chartHeight+5) qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight, self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight) @@ -2129,12 +2144,7 @@ class QualityFactorChart(FrequencyChart): self.setAutoFillBackground(True) def drawChart(self, qp: QtGui.QPainter): - qp.setPen(QtGui.QPen(self.textColor)) - qp.drawText(3, 15, self.name) - qp.setPen(QtGui.QPen(self.foregroundColor)) - qp.drawLine(self.leftMargin, self.topMargin - 5, self.leftMargin, self.topMargin + self.chartHeight + 5) - qp.drawLine(self.leftMargin-5, self.topMargin + self.chartHeight, - self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight) + super().drawChart(qp) # Make up some sensible scaling here if self.fixedValues: @@ -2464,6 +2474,8 @@ class TDRChart(Chart): ticks = math.floor((self.width() - self.leftMargin)/100) # Number of ticks does not include the origin + self.drawTitle(qp) + if len(self.tdrWindow.td) > 0: if self.fixedSpan: max_index = np.searchsorted(self.tdrWindow.distance_axis, self.maxDisplayLength * 2) @@ -2772,6 +2784,7 @@ class RealImaginaryChart(FrequencyChart): qp.drawLine(self.leftMargin, self.topMargin - 5, self.leftMargin, self.topMargin + self.chartHeight + 5) qp.drawLine(self.leftMargin-5, self.topMargin + self.chartHeight, self.leftMargin + self.chartWidth + 5, self.topMargin + self.chartHeight) + self.drawTitle(qp) def drawValues(self, qp: QtGui.QPainter): if len(self.data) == 0 and len(self.reference) == 0: @@ -3165,14 +3178,6 @@ class MagnitudeChart(FrequencyChart): self.setPalette(pal) self.setAutoFillBackground(True) - def drawChart(self, qp: QtGui.QPainter): - qp.setPen(QtGui.QPen(self.textColor)) - qp.drawText(3, 15, self.name) - qp.setPen(QtGui.QPen(self.foregroundColor)) - qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5) - qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight, - self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight) - def drawValues(self, qp: QtGui.QPainter): if len(self.data) == 0 and len(self.reference) == 0: return @@ -3311,14 +3316,6 @@ class MagnitudeZChart(FrequencyChart): self.setPalette(pal) self.setAutoFillBackground(True) - def drawChart(self, qp: QtGui.QPainter): - qp.setPen(QtGui.QPen(self.textColor)) - qp.drawText(3, 15, self.name) - qp.setPen(QtGui.QPen(self.foregroundColor)) - qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5) - qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight, - self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight) - def drawValues(self, qp: QtGui.QPainter): if len(self.data) == 0 and len(self.reference) == 0: return @@ -3482,9 +3479,11 @@ class PermeabilityChart(FrequencyChart): qp.drawText(10, 15, "R") qp.drawText(self.leftMargin + self.chartWidth + 10, 15, "X") qp.setPen(QtGui.QPen(self.foregroundColor)) - qp.drawLine(self.leftMargin, self.topMargin - 5, self.leftMargin, self.topMargin + self.chartHeight + 5) + qp.drawLine(self.leftMargin, self.topMargin - 5, + self.leftMargin, self.topMargin + self.chartHeight + 5) qp.drawLine(self.leftMargin-5, self.topMargin + self.chartHeight, self.leftMargin + self.chartWidth + 5, self.topMargin + self.chartHeight) + self.drawTitle(qp) def drawValues(self, qp: QtGui.QPainter): if len(self.data) == 0 and len(self.reference) == 0: @@ -3861,6 +3860,7 @@ class GroupDelayChart(FrequencyChart): qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5) qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight, self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight) + self.drawTitle(qp) def drawValues(self, qp: QtGui.QPainter): if len(self.data) == 0 and len(self.reference) == 0: @@ -4029,6 +4029,7 @@ class CapacitanceChart(FrequencyChart): qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5) qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight, self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight) + self.drawTitle(qp) def drawValues(self, qp: QtGui.QPainter): if len(self.data) == 0 and len(self.reference) == 0: @@ -4155,6 +4156,7 @@ class InductanceChart(FrequencyChart): qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5) qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight, self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight) + self.drawTitle(qp) def drawValues(self, qp: QtGui.QPainter): if len(self.data) == 0 and len(self.reference) == 0: diff --git a/NanoVNASaver/NanoVNASaver.py b/NanoVNASaver/NanoVNASaver.py index 5ddd7d6..257d73a 100644 --- a/NanoVNASaver/NanoVNASaver.py +++ b/NanoVNASaver/NanoVNASaver.py @@ -59,6 +59,8 @@ class NanoVNASaver(QtWidgets.QWidget): dataAvailable = QtCore.pyqtSignal() scaleFactor = 1 + sweepTitle = "" + def __init__(self): super().__init__() if getattr(sys, 'frozen', False): @@ -651,6 +653,7 @@ class NanoVNASaver(QtWidgets.QWidget): sleep(0.05) self.vna = VNA.getVNA(self, self.serial) + self.vna.validateInput = self.settings.value("SerialInputValidation", True, bool) self.worker.setVNA(self.vna) logger.info(self.vna.readFirmware()) @@ -724,6 +727,8 @@ class NanoVNASaver(QtWidgets.QWidget): 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()) @@ -961,6 +966,7 @@ class NanoVNASaver(QtWidgets.QWidget): def showSweepError(self): self.showError(self.worker.error_message) + self.serial.flushInput() # Remove any left-over data self.sweepFinished() def popoutChart(self, chart: Chart): @@ -1011,6 +1017,11 @@ class NanoVNASaver(QtWidgets.QWidget): m.getGroupBox().setFont(font) m.setScale(self.scaleFactor) + def setSweepTitle(self, title): + self.sweepTitle = title + for c in self.subscribing_charts: + c.setSweepTitle(title) + class DisplaySettingsWindow(QtWidgets.QWidget): def __init__(self, app: NanoVNASaver): @@ -2020,6 +2031,20 @@ class SweepSettingsWindow(QtWidgets.QWidget): 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) @@ -2591,7 +2616,8 @@ class DeviceSettingsWindow(QtWidgets.QWidget): settings_layout = QtWidgets.QFormLayout(settings_box) self.chkValidateInputData = QtWidgets.QCheckBox("Validate received data") - self.chkValidateInputData.setChecked(True) + 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) @@ -2640,6 +2666,7 @@ class DeviceSettingsWindow(QtWidgets.QWidget): 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: diff --git a/NanoVNASaver/SweepWorker.py b/NanoVNASaver/SweepWorker.py index 8916e00..ce939d2 100644 --- a/NanoVNASaver/SweepWorker.py +++ b/NanoVNASaver/SweepWorker.py @@ -142,7 +142,12 @@ class SweepWorker(QtCore.QRunnable): self.error_message = str(e) self.stopped = True self.running = False - self.signals.fatalSweepError.emit() + self.signals.sweepError.emit() + except NanoVNASerialException as e: + self.error_message = str(e) + self.stopped = True + self.running = False + self.signals.sweepFatalError.emit() while self.continuousSweep and not self.stopped: logger.debug("Continuous sweeping") @@ -160,7 +165,12 @@ class SweepWorker(QtCore.QRunnable): self.error_message = str(e) self.stopped = True self.running = False - self.signals.fatalSweepError.emit() + self.signals.sweepError.emit() + except NanoVNASerialException as e: + self.error_message = str(e) + self.stopped = True + self.running = False + self.signals.sweepFatalError.emit() # Reset the device to show the full range logger.debug("Resetting NanoVNA sweep to full range: %d to %d", @@ -332,16 +342,16 @@ class SweepWorker(QtCore.QRunnable): tmpdata = self.vna.readValues(data) if not tmpdata: logger.warning("Read no values") - raise NanoVNAValueException("Failed reading data: Returned no values.") + raise NanoVNASerialException("Failed reading data: Returned no values.") logger.debug("Read %d values", len(tmpdata)) for d in tmpdata: a, b = d.split(" ") try: - if self.vna.validateInput and float(a) < -9.5 or float(a) > 9.5: + if self.vna.validateInput and (float(a) < -9.5 or float(a) > 9.5): logger.warning("Got a non-float data value: %s (%s)", d, a) logger.debug("Re-reading %s", data) done = False - elif self.vna.validateInput and float(b) < -9.5 or float(b) > 9.5: + elif self.vna.validateInput and (float(b) < -9.5 or float(b) > 9.5): logger.warning("Got a non-float data value: %s (%s)", d, b) logger.debug("Re-reading %s", data) done = False @@ -359,7 +369,8 @@ class SweepWorker(QtCore.QRunnable): if count >= 20: logger.critical("Tried and failed to read %s %d times. Giving up.", data, count) raise NanoVNAValueException("Failed reading " + str(data) + " " + str(count) + " times.\n" + - "Data outside expected valid ranges, or in an unexpected format.") + "Data outside expected valid ranges, or in an unexpected format.\n\n" + + "You can disable data validation on the device settings screen.") return returndata def readFreq(self): @@ -374,7 +385,7 @@ class SweepWorker(QtCore.QRunnable): tmpfreq = self.vna.readFrequencies() if not tmpfreq: logger.warning("Read no frequencies") - raise NanoVNAValueException("Failed reading frequencies: Returned no values.") + raise NanoVNASerialException("Failed reading frequencies: Returned no values.") for f in tmpfreq: if not f.isdigit(): logger.warning("Got a non-digit frequency: %s", f) @@ -407,3 +418,7 @@ class SweepWorker(QtCore.QRunnable): class NanoVNAValueException(Exception): pass + + +class NanoVNASerialException(Exception): + pass