diff --git a/.github/workflows/release_linux.yml b/.github/workflows/release_linux.yml index aff68d41..da330909 100644 --- a/.github/workflows/release_linux.yml +++ b/.github/workflows/release_linux.yml @@ -28,9 +28,9 @@ jobs: run: | python3.11 -m venv build . build/bin/activate - python -m pip install pip==24.1 setuptools==70.1.0 + python -m pip install pip==24.2 setuptools==75.2.0 pip install -r requirements.txt - pip install PyInstaller==6.8.0 + pip install PyInstaller==6.11.0 python3 -m pip install -U PyQt6-sip python3 -m pip install -U PyQt6 - name: Build binary diff --git a/.github/workflows/release_macos.yml b/.github/workflows/release_macos.yml deleted file mode 100644 index ba5905dc..00000000 --- a/.github/workflows/release_macos.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Mac Release - -on: - push: - tags: - - v* - workflow_dispatch: - -jobs: - release: - runs-on: macos-latest - - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.11 - - name: Install dependencies and pyinstall - run: | - python -m pip install pip==23.3.2 setuptools==69.0.3 - pip install -r requirements.txt - pip install PyInstaller==6.3.0 - - name: Build binary - run: | - python setup.py -V - pyinstaller --onefile -p src -n nanovna-saver nanovna-saver.py - - - name: Archive production artifacts - uses: actions/upload-artifact@v1 - with: - name: NanoVNASaver.macos - path: dist/nanovna-saver diff --git a/.github/workflows/release_macos_app.yml b/.github/workflows/release_macos_app.yml deleted file mode 100644 index 135146ca..00000000 --- a/.github/workflows/release_macos_app.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Mac Release App - -on: - push: - tags: - - v* - workflow_dispatch: - -jobs: - release: - runs-on: macos-latest - - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.11 - - name: Get Target Environment - id: targetenv - run: | - echo "arch=`uname -m`" >> "$GITHUB_ENV" - - - name: Install dependencies and pyinstall - run: | - python -m pip install pip==23.3.2 setuptools==69.0.3 - pip install -r requirements.txt - pip install PyInstaller==6.3.0 - - - name: Build binary - run: | - python setup.py -V - pyinstaller --onedir -p src -n NanoVNASaver nanovna-saver.py --window --clean -y -i icon_48x48.icns - tar -C dist -zcf ./dist/NanoVNASaver.app-${{ env.arch }}.tar.gz NanoVNASaver.app - echo "Created: NanoVNASaver.app-${{ env.arch }}.tar.gz" - - - name: Archive production artifacts - uses: actions/upload-artifact@v1 - with: - name: NanoVNASaver.app-${{ env.arch }}.tar.gz - path: dist/NanoVNASaver.app-${{ env.arch }}.tar.gz diff --git a/.github/workflows/release_win.yml b/.github/workflows/release_win.yml index 536d4d28..1ed2d3b5 100644 --- a/.github/workflows/release_win.yml +++ b/.github/workflows/release_win.yml @@ -26,10 +26,10 @@ jobs: run: | python3 -m venv venv .\venv\Scripts\activate - python3 -m pip install pip==24.1 + python3 -m pip install pip==24.2 python3 -m pip install -U setuptools setuptools-scm python3 -m pip install -r requirements.txt - python3 -m pip install PyInstaller==6.8.0 + python3 -m pip install PyInstaller==6.11.0 python3 -m pip install -U PyQt6-sip python3 -m pip install -U PyQt6 - name: Build binary diff --git a/Makefile b/Makefile index f480badf..781da4b0 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ rpm: clean # remove all package build artifacts (keep the *.deb) .PHONY: clean clean: - python setup.py clean + python3 setup.py clean -rm -rf build deb_dist dist *.tar.gz *.egg* diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 72385751..784a66c7 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -58,11 +58,11 @@ or `make rpm` builds an (untested) rpm package that can be installed on your system the usual way. -## Ubuntu 20.04 / 22.04 +## Ubuntu 20.04 / 22.04 / 24.04 1. Install python3 and pip - sudo apt install python3 python3-pip + sudo apt install python3 python3-pip libxcb-cursor-dev python3 -m venv ~/.venv_nano . ~/.venv_nano/bin/activate pip install -U pip diff --git a/pyproject.toml b/pyproject.toml index cd97f78a..7fe18585 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,3 +17,6 @@ write_to = "src/NanoVNASaver/_version.py" pythonpath = [ ".", "src", ] + +[tool.black] +line-length = 80 diff --git a/requirements.txt b/requirements.txt index 2d4f3809..81353295 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ pyserial==3.5 -PyQt6==6.7.0 -PyQt6-sip==13.6.0 -sip==6.8.4 -numpy==1.26.4 -scipy==1.13.1 -Cython==3.0.10 -setuptools==70.1.0 +PyQt6==6.7.1 +PyQt6-sip==13.8.0 +sip==6.8.6 +numpy==2.1.2 +scipy==1.14.1 +Cython==3.0.11 +setuptools==75.2.0 setuptools-scm==8.1.0 diff --git a/src/NanoVNASaver/About.py b/src/NanoVNASaver/About.py index 8625a96d..af59ae9e 100644 --- a/src/NanoVNASaver/About.py +++ b/src/NanoVNASaver/About.py @@ -18,8 +18,9 @@ # along with this program. If not, see . from setuptools_scm import get_version + try: - version = get_version(root='..', relative_to=__file__) + version = get_version(root="../..", relative_to=__file__) except LookupError: from NanoVNASaver._version import version diff --git a/src/NanoVNASaver/Charts/TDR.py b/src/NanoVNASaver/Charts/TDR.py index a56faace..84fe88b9 100644 --- a/src/NanoVNASaver/Charts/TDR.py +++ b/src/NanoVNASaver/Charts/TDR.py @@ -37,14 +37,28 @@ logger = logging.getLogger(__name__) +MIN_IMPEDANCE = 0 +MAX_IMPEDANCE = 1000 + +MIN_S11 = -60 +MAX_S11 = 0 + +MIN_VSWR = 1 +MAX_VSWR = 10 + class TDRChart(Chart): maxDisplayLength = 50 minDisplayLength = 0 fixedSpan = False - minImpedance = 0 - maxImpedance = 1000 + minYlim = 0 + maxYlim = 1000 + + decimals = 1 + + formatString = "" + fixedValues = False markerLocation = -1 @@ -105,7 +119,7 @@ def __init__(self, name): self.x_menu.addAction(self.action_set_fixed_start) self.x_menu.addAction(self.action_set_fixed_stop) - self.y_menu = QMenu("Impedance axis") + self.y_menu = QMenu("Y axis") self.y_mode_group = QActionGroup(self.y_menu) self.y_action_automatic = QAction("Automatic") self.y_action_automatic.setCheckable(True) @@ -124,19 +138,11 @@ def __init__(self, name): self.y_menu.addAction(self.y_action_fixed) self.y_menu.addSeparator() - self.y_action_set_fixed_maximum = QAction( - f"Maximum ({self.maxImpedance})" - ) - self.y_action_set_fixed_maximum.triggered.connect( - self.setMaximumImpedance - ) + self.y_action_set_fixed_maximum = QAction(f"Maximum ({self.maxYlim})") + self.y_action_set_fixed_maximum.triggered.connect(self.setMaximumY) - self.y_action_set_fixed_minimum = QAction( - f"Minimum ({self.minImpedance})" - ) - self.y_action_set_fixed_minimum.triggered.connect( - self.setMinimumImpedance - ) + self.y_action_set_fixed_minimum = QAction(f"Minimum ({self.minYlim})") + self.y_action_set_fixed_minimum.triggered.connect(self.setMinimumY) self.y_menu.addAction(self.y_action_set_fixed_maximum) self.y_menu.addAction(self.y_action_set_fixed_minimum) @@ -157,12 +163,8 @@ def __init__(self, name): def contextMenuEvent(self, event): self.action_set_fixed_start.setText(f"Start ({self.minDisplayLength})") self.action_set_fixed_stop.setText(f"Stop ({self.maxDisplayLength})") - self.y_action_set_fixed_minimum.setText( - f"Minimum ({self.minImpedance})" - ) - self.y_action_set_fixed_maximum.setText( - f"Maximum ({self.maxImpedance})" - ) + self.y_action_set_fixed_minimum.setText(f"Minimum ({self.minYlim})") + self.y_action_set_fixed_maximum.setText(f"Maximum ({self.maxYlim})") self.menu.exec(event.globalPos()) def isPlotable(self, x, y): @@ -171,13 +173,30 @@ def isPlotable(self, x, y): and self.topMargin <= y <= self.height() - self.bottomMargin ) + def _configureGraphFromFormat(self): + TDR_format = self.tdrWindow.format_dropdown.currentText() + if TDR_format == "|Z|": + self.minYlim = MIN_IMPEDANCE + self.maxYlim = MAX_IMPEDANCE + self.formatString = "impedance (\N{OHM SIGN})" + self.decimals = 1 + elif TDR_format == "S11": + self.minYlim = MIN_S11 + self.maxYlim = MAX_S11 + self.formatString = "S11 (dB)" + self.decimals = 1 + elif TDR_format == "VSWR": + self.minYlim = MIN_VSWR + self.maxYlim = MAX_VSWR + self.formatString = "VSWR" + self.decimals = 2 + def resetDisplayLimits(self): + self._configureGraphFromFormat() self.fixedSpan = False self.minDisplayLength = 0 self.maxDisplayLength = 100 self.fixedValues = False - self.minImpedance = 0 - self.maxImpedance = 1000 self.update() def setFixedSpan(self, fixed_span): @@ -205,7 +224,7 @@ def setMaximumLength(self): self, "Stop length (m)", "Set stop length (m)", - value=self.minDisplayLength, + value=self.maxDisplayLength, min=0.1, decimals=1, ) @@ -220,35 +239,33 @@ def setFixedValues(self, fixed_values): self.fixedValues = fixed_values self.update() - def setMinimumImpedance(self): + def setMinimumY(self): min_val, selected = QInputDialog.getDouble( self, - "Minimum impedance (\N{OHM SIGN})", - "Set minimum impedance (\N{OHM SIGN})", - value=self.minDisplayLength, - min=0, - decimals=1, + "Minimum " + self.formatString, + "Set minimum " + self.formatString, + value=self.minYlim, + decimals=self.decimals, ) if not selected: return - if not (self.fixedValues and min_val >= self.maxImpedance): - self.minImpedance = min_val + if not (self.fixedValues and min_val >= self.maxYlim): + self.minYlim = min_val if self.fixedValues: self.update() - def setMaximumImpedance(self): + def setMaximumY(self): max_val, selected = QInputDialog.getDouble( self, - "Maximum impedance (\N{OHM SIGN})", - "Set maximum impedance (\N{OHM SIGN})", - value=self.minDisplayLength, - min=0.1, - decimals=1, + "Maximum " + self.formatString, + "Set maximum " + self.formatString, + value=self.maxYlim, + decimals=self.decimals, ) if not selected: return - if not (self.fixedValues and max_val <= self.minImpedance): - self.maxImpedance = max_val + if not (self.fixedValues and max_val <= self.minYlim): + self.maxYlim = max_val if self.fixedValues: self.update() @@ -258,8 +275,8 @@ def copy(self): new_chart.minDisplayLength = self.minDisplayLength new_chart.maxDisplayLength = self.maxDisplayLength new_chart.fixedSpan = self.fixedSpan - new_chart.minImpedance = self.minImpedance - new_chart.maxImpedance = self.maxImpedance + new_chart.minYlim = self.minYlim + new_chart.maxYlim = self.maxYlim new_chart.fixedValues = self.fixedValues self.tdrWindow.updated.connect(new_chart.update) return new_chart @@ -337,7 +354,7 @@ def _draw_ticks(self, height, width, x_step, min_index): qp.drawText( self.leftMargin - 10, self.topMargin + height + 15, - f"{str(round(self.tdrWindow.distance_axis[min_index] / 2, 1))}m", + f"{str(round(self.tdrWindow.distance_axis[min_index] / 2, self.decimals))}m", ) def _draw_y_ticks(self, height, width, min_impedance, max_impedance): @@ -351,10 +368,12 @@ def _draw_y_ticks(self, height, width, min_impedance, max_impedance): qp.drawLine(self.leftMargin, y, self.leftMargin + width, y) y_val = max_impedance - y_step * i * y_tick_step qp.setPen(Chart.color.text) - qp.drawText(3, y + 3, str(round(y_val, 1))) + qp.drawText(3, y + 3, str(round(y_val, self.decimals))) qp.setPen(Chart.color.text) qp.drawText( - 3, self.topMargin + height + 3, f"{round(min_impedance, 1)}" + 3, + self.topMargin + height + 3, + f"{round(min_impedance, self.decimals)}", ) def _draw_max_point(self, height, x_step, y_step, min_index): @@ -412,11 +431,14 @@ def _draw_graph(self, height, width): x_step = (max_index - min_index) / width # TODO: Limit the search to the selected span? - 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) + min_Z = np.min(self.tdrWindow.step_response_Z) + max_Z = np.max(self.tdrWindow.step_response_Z) + # Ensure that everything works even if limits are negative + min_impedance = max(self.minYlim, min_Z - 0.05 * np.abs(min_Z)) + max_impedance = min(self.maxYlim, max_Z + 0.05 * np.abs(max_Z)) if self.fixedValues: - min_impedance = max(0, self.minImpedance) - max_impedance = max(0.1, self.maxImpedance) + min_impedance = self.minYlim + max_impedance = self.maxYlim y_step = max(self.tdrWindow.td) * 1.1 / height or 1.0e-30 @@ -495,15 +517,14 @@ def valueAtPosition(self, y): height = self.height() - self.topMargin - self.bottomMargin absy = (self.height() - y) - self.bottomMargin if self.fixedValues: - min_impedance = self.minImpedance - max_impedance = self.maxImpedance + min_impedance = self.minYlim + max_impedance = self.maxYlim 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 - ) + min_Z = np.min(self.tdrWindow.step_response_Z) + max_Z = np.max(self.tdrWindow.step_response_Z) + # Ensure that everything works even if limits are negative + min_impedance = max(self.minYlim, min_Z - 0.05 * np.abs(min_Z)) + max_impedance = min(self.maxYlim, max_Z + 0.05 * np.abs(max_Z)) y_step = (max_impedance - min_impedance) / height return y_step * absy + min_impedance return 0 @@ -540,8 +561,8 @@ def zoomTo(self, x1, y1, x2, y2): val2 = self.valueAtPosition(y2) if val1 != val2: - self.minImpedance = round(min(val1, val2), 3) - self.maxImpedance = round(max(val1, val2), 3) + self.minYlim = round(min(val1, val2), 3) + self.maxYlim = round(max(val1, val2), 3) self.setFixedValues(True) len1 = max(0, self.lengthAtPosition(x1, limit=False)) diff --git a/src/NanoVNASaver/Marker/Values.py b/src/NanoVNASaver/Marker/Values.py index 96cb2030..a021756d 100644 --- a/src/NanoVNASaver/Marker/Values.py +++ b/src/NanoVNASaver/Marker/Values.py @@ -71,7 +71,7 @@ class Value: """Contains the data area to calculate marker values from""" def __init__( - self, + self, ): self.freq: int = 0 self.s11: list[Datapoint] = [] diff --git a/src/NanoVNASaver/NanoVNASaver.py b/src/NanoVNASaver/NanoVNASaver.py index 7c074956..26ecde0b 100644 --- a/src/NanoVNASaver/NanoVNASaver.py +++ b/src/NanoVNASaver/NanoVNASaver.py @@ -467,7 +467,9 @@ def __init__(self): logger.debug("Finished building interface") - def auto_connect(self): # connect if there is exactly one detected serial device + def auto_connect( + self, + ): # connect if there is exactly one detected serial device if self.serial_control.inp_port.count() == 1: self.serial_control.connect_device() @@ -510,9 +512,9 @@ def saveData(self, data, data21, source=None): if source is not None: self.sweepSource = source else: - time = strftime('%Y-%m-%d %H:%M:%S', localtime()) - name = self.sweep.properties.name or 'nanovna' - self.sweepSource = f'{name}_{time}' + time = strftime("%Y-%m-%d %H:%M:%S", localtime()) + name = self.sweep.properties.name or "nanovna" + self.sweepSource = f"{name}_{time}" def markerUpdated(self, marker: Marker): with self.dataLock: diff --git a/src/NanoVNASaver/RFTools.py b/src/NanoVNASaver/RFTools.py index 3a253873..d6a09dc8 100644 --- a/src/NanoVNASaver/RFTools.py +++ b/src/NanoVNASaver/RFTools.py @@ -125,9 +125,7 @@ def norm_to_impedance(z: complex, ref_impedance: float = 50) -> complex: def parallel_to_serial(z: complex) -> complex: """Convert parallel impedance to serial impedance equivalent""" z_sq_sum = z.real**2 + z.imag**2 or 10.0e-30 - return complex( - z.real * z.imag**2 / z_sq_sum, z.real**2 * z.imag / z_sq_sum - ) + return complex(z.real * z.imag**2 / z_sq_sum, z.real**2 * z.imag / z_sq_sum) def reflection_coefficient(z: complex, ref_impedance: float = 50) -> complex: diff --git a/src/NanoVNASaver/Settings/Sweep.py b/src/NanoVNASaver/Settings/Sweep.py index 62759927..c717db27 100644 --- a/src/NanoVNASaver/Settings/Sweep.py +++ b/src/NanoVNASaver/Settings/Sweep.py @@ -39,13 +39,14 @@ class Properties(NamedTuple): class Sweep: - def __init__(self, - start: int = 3600000, - end: int = 30000000, - points: int = 101, - segments: int = 1, - properties: "Properties" = Properties(), - ): + def __init__( + self, + start: int = 3600000, + end: int = 30000000, + points: int = 101, + segments: int = 1, + properties: "Properties" = Properties(), + ): self._start = start self._end = end self._points = points @@ -56,21 +57,41 @@ def __init__(self, logger.debug("%s", self) def __repr__(self) -> str: - return 'Sweep(' + ', '.join(map(str, ( - self.start, self.end, self.points, self.segments, self.properties - ))) + ')' + return ( + "Sweep(" + + ", ".join( + map( + str, + ( + self.start, + self.end, + self.points, + self.segments, + self.properties, + ), + ) + ) + + ")" + ) def __eq__(self, other) -> bool: - return (self.start == other.start - and self.end == other.end - and self.points == other.points - and self.segments == other.segments - and self.properties == other.properties) + return ( + self.start == other.start + and self.end == other.end + and self.points == other.points + and self.segments == other.segments + and self.properties == other.properties + ) def copy(self) -> "Sweep": with self._lock: - return Sweep(self.start, self.end, self.points, self.segments, - self._properties) + return Sweep( + self.start, + self.end, + self.points, + self.segments, + self._properties, + ) # Getters for attributes, either private or computed. @@ -128,7 +149,9 @@ def set_mode(self, mode: "SweepMode") -> None: def set_averages(self, amount: int, truncates: int) -> None: with self._lock: - self._properties = self.properties._replace(averages=(amount, truncates)) + self._properties = self.properties._replace( + averages=(amount, truncates) + ) def set_logarithmic(self, logarithmic: bool) -> None: with self._lock: @@ -145,7 +168,9 @@ def check(self): raise ValueError(f"Illegal sweep settings: {self}") def _exp_factor(self, index: int) -> float: - return exp(log((self.start + self.span)/self.start) / self.segments * index) + return exp( + log((self.start + self.span) / self.start) / self.segments * index + ) def get_index_range(self, index: int) -> tuple[int, int]: if self.properties.logarithmic: diff --git a/src/NanoVNASaver/Version.py b/src/NanoVNASaver/Version.py index 8ef790ce..4f75887c 100644 --- a/src/NanoVNASaver/Version.py +++ b/src/NanoVNASaver/Version.py @@ -22,13 +22,16 @@ logger = logging.getLogger(__name__) -_RXP = re.compile(r"""^ +_RXP = re.compile( + r"""^ \D* (?P\d+)\. (?P\d+)\.? (?P\d+)? (?P.*) - $""", re.VERBOSE) + $""", + re.VERBOSE, +) class _Version(typing.NamedTuple): @@ -38,18 +41,17 @@ class _Version(typing.NamedTuple): note: str def __str__(self) -> str: - return ( - f'{self.major}.{self.minor}' - f'.{self.revision}{self.note}' - ) + return f"{self.major}.{self.minor}" f".{self.revision}{self.note}" -def Version(vstring: str = "0.0.0") -> '_Version': +def Version(vstring: str = "0.0.0") -> "_Version": if (match := _RXP.search(vstring)) is None: logger.error("Unable to parse version: %s", vstring) - return _Version(0, 0, 0, '') - - return _Version(int(match.group('major')), - int(match.group('minor')), - int(match.group('revision') or '0'), - match.group('note')) + return _Version(0, 0, 0, "") + + return _Version( + int(match.group("major")), + int(match.group("minor")), + int(match.group("revision") or "0"), + match.group("note"), + ) diff --git a/src/NanoVNASaver/Windows/About.py b/src/NanoVNASaver/Windows/About.py index 10b4729c..d7024d11 100644 --- a/src/NanoVNASaver/Windows/About.py +++ b/src/NanoVNASaver/Windows/About.py @@ -89,7 +89,9 @@ def __init__(self, app: QtWidgets.QWidget): lower_layout = QtWidgets.QVBoxLayout() top_layout.addLayout(lower_layout) - btn_check_version = QtWidgets.QPushButton("Check for NanoVNASaver updates") + btn_check_version = QtWidgets.QPushButton( + "Check for NanoVNASaver updates" + ) btn_check_version.clicked.connect(self.findUpdates) self.updateLabel = QtWidgets.QLabel() @@ -129,14 +131,14 @@ def updateLabels(self): "NanoVNA Firmware Version: Not connected." ) -# attempt to scan the TAGS_URL web page for something that looks like -# a version tag. assume the first match with a line containing the TAGS_KEY -# will contain the latest version substring since it appears at the top -# of the web page. -# -# this routine can also allow the application to automatically perform a -# check-for-updates and display a pop-up if any are found when this -# function is called with automatic=True. + # attempt to scan the TAGS_URL web page for something that looks like + # a version tag. assume the first match with a line containing the TAGS_KEY + # will contain the latest version substring since it appears at the top + # of the web page. + # + # this routine can also allow the application to automatically perform a + # check-for-updates and display a pop-up if any are found when this + # function is called with automatic=True. def findUpdates(self, automatic=False): try: @@ -146,7 +148,9 @@ def findUpdates(self, automatic=False): line = line.decode("utf-8") found_latest_version = TAGS_KEY in line if found_latest_version: - latest_version = Version(re.search("(\d+\.\d+\.\d+)", line).group()) + latest_version = Version( + re.search(r"(\d+\.\d+\.\d+)", line).group() + ) break except error.HTTPError as e: logger.exception( @@ -205,7 +209,7 @@ def findUpdates(self, automatic=False): # if we get here, something may have changed in the way github creates # the .../latest web page. self.updateLabel.setText( - f"ERROR - Unable to determine what the latest version is! " + "ERROR - Unable to determine what the latest version is!" ) logger.error(f"Can't find {TAGS_KEY} in {TAGS_URL} content.") return diff --git a/src/NanoVNASaver/Windows/Bands.py b/src/NanoVNASaver/Windows/Bands.py index ee6cdceb..fcbdb00c 100644 --- a/src/NanoVNASaver/Windows/Bands.py +++ b/src/NanoVNASaver/Windows/Bands.py @@ -66,7 +66,8 @@ def resetBands(self): QtWidgets.QMessageBox.Icon.Warning, "Confirm reset", "Are you sure you want to reset the bands to default?", - QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.Cancel, + QtWidgets.QMessageBox.StandardButton.Yes + | QtWidgets.QMessageBox.StandardButton.Cancel, ).exec() if confirm == QtWidgets.QMessageBox.StandardButton.Yes: self.app.bands.resetBands() diff --git a/src/NanoVNASaver/Windows/CalibrationSettings.py b/src/NanoVNASaver/Windows/CalibrationSettings.py index 756bc1cb..dbe56ddc 100644 --- a/src/NanoVNASaver/Windows/CalibrationSettings.py +++ b/src/NanoVNASaver/Windows/CalibrationSettings.py @@ -282,8 +282,8 @@ def checkExpertUser(self): "If you are certain you know what you are doing, click" " Yes." ), - QtWidgets.QMessageBox.StandardButton.Yes | - QtWidgets.QMessageBox.StandardButton.Cancel, + QtWidgets.QMessageBox.StandardButton.Yes + | QtWidgets.QMessageBox.StandardButton.Cancel, QtWidgets.QMessageBox.StandardButton.Cancel, ) @@ -545,7 +545,7 @@ def calculate(self): " Please complete SOL calibration and try again." ) return - + cal_element.short_is_ideal = True cal_element.open_is_ideal = True cal_element.load_is_ideal = True @@ -649,8 +649,7 @@ def calculate(self): ) self.calibration_source_label.setText(self.app.calibration.source) self.app.showError( - f"{e}" - " Please complete SOL calibration and try again." + f"{e}" " Please complete SOL calibration and try again." ) self.reset() return @@ -733,7 +732,8 @@ def automaticCalibration(self): " in save slot 0 before starting.

" "Once you are ready to proceed, press Ok." ), - QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel, + QtWidgets.QMessageBox.StandardButton.Ok + | QtWidgets.QMessageBox.StandardButton.Cancel, ) response = introduction.exec() if response != QtWidgets.QMessageBox.StandardButton.Ok: @@ -772,7 +772,8 @@ def automaticCalibration(self): " NanoVNA.\n\n" "Press Ok when you are ready to continue." ), - QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel, + QtWidgets.QMessageBox.StandardButton.Ok + | QtWidgets.QMessageBox.StandardButton.Cancel, ) response = short_step.exec() @@ -808,8 +809,8 @@ def automaticCalibrationStep(self): " cable unconnected if desired.\n\n" "Press Ok when you are ready to continue." ), - QtWidgets.QMessageBox.StandardButton.Ok | - QtWidgets.QMessageBox.StandardButton.Cancel, + QtWidgets.QMessageBox.StandardButton.Ok + | QtWidgets.QMessageBox.StandardButton.Cancel, ) response = open_step.exec() @@ -835,8 +836,8 @@ def automaticCalibrationStep(self): " NanoVNA.\n\n" "Press Ok when you are ready to continue." ), - QtWidgets.QMessageBox.StandardButton.Ok | - QtWidgets.QMessageBox.StandardButton.Cancel, + QtWidgets.QMessageBox.StandardButton.Ok + | QtWidgets.QMessageBox.StandardButton.Cancel, ) response = load_step.exec() @@ -896,8 +897,8 @@ def automaticCalibrationStep(self): " port 0.\n\n" "Press Ok when you are ready to continue." ), - QtWidgets.QMessageBox.StandardButton.Ok | - QtWidgets.QMessageBox.StandardButton.Cancel, + QtWidgets.QMessageBox.StandardButton.Ok + | QtWidgets.QMessageBox.StandardButton.Cancel, ) response = isolation_step.exec() @@ -923,8 +924,8 @@ def automaticCalibrationStep(self): " port 0 and port 1 of the NanoVNA.\n\n" "Press Ok when you are ready to continue." ), - QtWidgets.QMessageBox.StandardButton.Ok | - QtWidgets.QMessageBox.StandardButton.Cancel, + QtWidgets.QMessageBox.StandardButton.Ok + | QtWidgets.QMessageBox.StandardButton.Cancel, ) response = through_step.exec() @@ -949,8 +950,8 @@ def automaticCalibrationStep(self): "The calibration process is now complete. Press" ' "Apply" to apply the calibration parameters.' ), - QtWidgets.QMessageBox.StandardButton.Apply | - QtWidgets.QMessageBox.StandardButton.Cancel, + QtWidgets.QMessageBox.StandardButton.Apply + | QtWidgets.QMessageBox.StandardButton.Cancel, ) response = apply_step.exec() diff --git a/src/NanoVNASaver/Windows/DeviceSettings.py b/src/NanoVNASaver/Windows/DeviceSettings.py index c45897ce..892813e3 100644 --- a/src/NanoVNASaver/Windows/DeviceSettings.py +++ b/src/NanoVNASaver/Windows/DeviceSettings.py @@ -101,8 +101,10 @@ def __init__(self, app: QtWidgets.QWidget): self.custom_points_checkBox.stateChanged.connect(self.customPoint_check) self.custom_points_Eidt = QtWidgets.QLineEdit("101") self.custom_points_Eidt.setValidator( - QIntValidator(self.app.vna.sweep_points_min, - self.app.vna.sweep_points_max)) + QIntValidator( + self.app.vna.sweep_points_min, self.app.vna.sweep_points_max + ) + ) self.custom_points_Eidt.textEdited.connect(self.updatecustomPoint) self.custom_points_Eidt.setDisabled(True) @@ -156,8 +158,10 @@ def updateFields(self): if "Customizable data points" in features: self.datapoints.clear() self.custom_points_Eidt.setValidator( - QIntValidator(self.app.vna.sweep_points_min, - self.app.vna.sweep_points_max)) + QIntValidator( + self.app.vna.sweep_points_min, self.app.vna.sweep_points_max + ) + ) cur_dps = self.app.vna.datapoints for d in sorted(self.app.vna.valid_datapoints): self.datapoints.addItem(str(d)) diff --git a/src/NanoVNASaver/Windows/DisplaySettings.py b/src/NanoVNASaver/Windows/DisplaySettings.py index fa001334..a530bcd9 100644 --- a/src/NanoVNASaver/Windows/DisplaySettings.py +++ b/src/NanoVNASaver/Windows/DisplaySettings.py @@ -544,7 +544,8 @@ def setColor(self): color = getattr(Chart.color, attr) color = QtWidgets.QColorDialog.getColor( - color, options=QtWidgets.QColorDialog.ColorDialogOption.ShowAlphaChannel + color, + options=QtWidgets.QColorDialog.ColorDialogOption.ShowAlphaChannel, ) if not color.isValid(): diff --git a/src/NanoVNASaver/Windows/TDR.py b/src/NanoVNASaver/Windows/TDR.py index a8c03049..237a4aca 100644 --- a/src/NanoVNASaver/Windows/TDR.py +++ b/src/NanoVNASaver/Windows/TDR.py @@ -89,6 +89,8 @@ def __init__(self, app: QtWidgets.QWidget): layout = QtWidgets.QFormLayout() make_scrollable(self, layout) + dropdown_layout = QtWidgets.QHBoxLayout() + self.tdr_velocity_dropdown = QtWidgets.QComboBox() for cable_name, velocity in CABLE_PARAMETERS: self.tdr_velocity_dropdown.addItem(cable_name, velocity) @@ -98,7 +100,19 @@ def __init__(self, app: QtWidgets.QWidget): self.tdr_velocity_dropdown.addItem("Custom", -1) self.tdr_velocity_dropdown.setCurrentIndex(1) # Default to PE (0.66) self.tdr_velocity_dropdown.currentIndexChanged.connect(self.updateTDR) - layout.addRow(self.tdr_velocity_dropdown) + + dropdown_layout.addWidget(self.tdr_velocity_dropdown) + + self.format_dropdown = QtWidgets.QComboBox() + self.format_dropdown.addItem("|Z|") + self.format_dropdown.addItem("S11") + self.format_dropdown.addItem("VSWR") + + self.format_dropdown.currentIndexChanged.connect(self.updateFormat) + + dropdown_layout.addWidget(self.format_dropdown) + + layout.addRow(dropdown_layout) self.tdr_velocity_input = QtWidgets.QLineEdit() self.tdr_velocity_input.setDisabled(True) @@ -111,6 +125,10 @@ def __init__(self, app: QtWidgets.QWidget): layout.addRow(self.app.tdr_chart) + def updateFormat(self): + self.app.tdr_chart.resetDisplayLimits() + self.updateTDR() + def updateTDR(self): # TODO: Let the user select whether to use high or low resolution TDR? FFT_POINTS = 2**14 @@ -138,15 +156,39 @@ def updateTDR(self): return s11 = [complex(d.re, d.im) for d in self.app.data.s11] - window = np.blackman(len(self.app.data.s11)) - windowed_s11 = window * s11 - td = np.abs(np.fft.ifft(windowed_s11, FFT_POINTS)) - step = np.ones(FFT_POINTS) - step_response = convolve(td, step) + s11 = np.array(s11) + s11 = np.concatenate( + [s11, np.conj(s11[-1:0:-1])] + ) # Include negative frequencies + s11 = np.fft.fftshift(s11) + + window = np.blackman(len(s11)) + windowed_s11 = ( + window * s11 + ) # Now windowing eliminates higher frequencies while leaving low frequencies untouched - self.step_response_Z = 50 * (1 + step_response) / (1 - step_response) + pad_points = (FFT_POINTS - len(windowed_s11)) // 2 + windowed_s11 = np.pad( + windowed_s11, [pad_points + 1, pad_points] + ) # Pad array to length FFT_POINTS + windowed_s11 = np.fft.ifftshift(windowed_s11) + td = np.fft.ifft(windowed_s11) + step = np.ones(FFT_POINTS) + step_response = convolve(td, step) + # calculate step response based on the format that the user selected + TDR_format = self.format_dropdown.currentText() + step_Z = 50 * (1 + step_response) / (1 - step_response) + step_refl_coefficient = np.abs((step_Z - 50) / (step_Z + 50)) + if TDR_format == "|Z|": + self.step_response_Z = np.abs(step_Z) + elif TDR_format == "S11": + self.step_response_Z = 20 * np.log10(step_refl_coefficient) + elif TDR_format == "VSWR": + self.step_response_Z = np.abs( + (1 + step_refl_coefficient) / (1 - step_refl_coefficient) + ) time_axis = np.linspace(0, 1 / step_size, FFT_POINTS) self.distance_axis = time_axis * v * speed_of_light # peak = np.max(td) diff --git a/src/NanoVNASaver/__main__.py b/src/NanoVNASaver/__main__.py index 6eecaf55..0eb3b595 100644 --- a/src/NanoVNASaver/__main__.py +++ b/src/NanoVNASaver/__main__.py @@ -47,7 +47,10 @@ def main(): "-D", "--debug-file", help="File to write debug logging output to" ) parser.add_argument( - "-a", "--auto-connect", action="store_true", help="Auto connect if one device detected" + "-a", + "--auto-connect", + action="store_true", + help="Auto connect if one device detected", ) parser.add_argument( "-f", diff --git a/tests/test_formatSweepFrequency.py b/tests/test_formatSweepFrequency.py index 925fe547..8dc342b2 100644 --- a/tests/test_formatSweepFrequency.py +++ b/tests/test_formatSweepFrequency.py @@ -18,7 +18,6 @@ # along with this program. If not, see . -import sys import unittest # Import targets to be tested