kopia lustrzana https://github.com/NanoVNA-Saver/nanovna-saver
Porównaj commity
52 Commity
Autor | SHA1 | Data |
---|---|---|
Holger Müller | a04d6d9b39 | |
Holger Müller | 00dd59ffc6 | |
Holger Müller | d3216d2ddb | |
t52ta6ek | 96dd23211a | |
Holger Müller | 2f8c5346eb | |
t52ta6ek | 4257ac152a | |
Holger Müller | 21e85bdb49 | |
Holger Müller | b4800102d8 | |
Name | abb80a5160 | |
t52ta6ek | 5bed1bc6cc | |
t52ta6ek | 20c1e4ec7c | |
Name | 21ba0ef665 | |
t52ta6ek | eff83097f8 | |
Name | dbea311a02 | |
t52ta6ek | a4a923a649 | |
Martin | ce0c7dd226 | |
Martin | 546d3b188a | |
Martin | 1f233819d2 | |
Martin | a8ffbc3aee | |
Holger Müller | ce8a59d478 | |
Crispin Tschirky | aab2a15f69 | |
Sascha Silbe | 9b4575e307 | |
Henk Vergonet | 8f86722c1e | |
Henk Vergonet | d09b55e1ae | |
Name | 6eb24f2315 | |
Name | d89c9f9d94 | |
Name | f34f3d1f67 | |
Name | 1cd5c052db | |
Holger Müller | 52cdac4f52 | |
Holger Müller | fafe0b2536 | |
Holger Müller | c5e00666aa | |
Holger Müller | 8dec23296e | |
Holger Müller | d1592ac1a3 | |
Holger Müller | 4e06fc53cf | |
Holger Müller | 45c2338196 | |
Holger Müller | 2bab4d4b0d | |
Holger Müller | b3a9f6d8cb | |
Holger Müller | 3c752a9731 | |
Holger Müller | b2c2598d3c | |
dependabot[bot] | c18a6c226f | |
Holger Müller | dd2f5b8a5d | |
Holger Müller | b322d3dc09 | |
Holger Müller | 5b21315a11 | |
Holger Müller | 9ace7d8cd4 | |
Holger Müller | b768a8e01b | |
Holger Müller | 2c58b2ba8f | |
MarcFontaine | a45baea9e2 | |
Holger Müller | db5cd98e03 | |
Holger Müller | 74792b3192 | |
Holger Müller | 50b540a832 | |
Holger Müller | 094b0185e7 | |
Holger Müller | b0110002ec |
21
.coveragerc
21
.coveragerc
|
@ -1,20 +1,9 @@
|
||||||
|
# .coveragerc to control coverage.py
|
||||||
[run]
|
[run]
|
||||||
# ignore GUI code atm.
|
branch = True
|
||||||
omit =
|
source = tests
|
||||||
NanoVNASaver/About.py
|
#omit = src/
|
||||||
NanoVNASaver/Analysis/*.py
|
|
||||||
NanoVNASaver/Calibration.py
|
|
||||||
NanoVNASaver/Charts/*.py
|
|
||||||
NanoVNASaver/Controls/*.py
|
|
||||||
NanoVNASaver/Hardware/*.py
|
|
||||||
NanoVNASaver/Inputs.py
|
|
||||||
NanoVNASaver/Marker/*.py
|
|
||||||
NanoVNASaver/NanoVNASaver.py
|
|
||||||
NanoVNASaver/Settings/Bands.py
|
|
||||||
NanoVNASaver/SweepWorker.py
|
|
||||||
NanoVNASaver/Windows/*.py
|
|
||||||
**/__init__.py
|
|
||||||
NanoVNASaver/__main__.py
|
|
||||||
[report]
|
[report]
|
||||||
fail_under = 90.0
|
fail_under = 90.0
|
||||||
show_missing = True
|
show_missing = True
|
||||||
|
|
|
@ -6,13 +6,13 @@
|
||||||
|
|
||||||
Please check the type of change your PR introduces:
|
Please check the type of change your PR introduces:
|
||||||
|
|
||||||
- [ ] Bugfix
|
- [] Bugfix
|
||||||
- [ ] Feature
|
- [] Feature
|
||||||
- [ ] Code style update (formatting, renaming)
|
- [] Code style update (formatting, renaming)
|
||||||
- [ ] Refactoring (no functional changes, no API changes)
|
- [] Refactoring (no functional changes, no API changes)
|
||||||
- [ ] Build-related changes
|
- [] Build-related changes
|
||||||
- [ ] Documentation content changes
|
- [] Documentation content changes
|
||||||
- [ ] Other (please describe):
|
- [] Other (please describe):
|
||||||
|
|
||||||
## What is the current behavior?
|
## What is the current behavior?
|
||||||
|
|
||||||
|
@ -30,8 +30,8 @@ Issue Number: N/A
|
||||||
|
|
||||||
## Does this introduce a breaking change?
|
## Does this introduce a breaking change?
|
||||||
|
|
||||||
- [ ] Yes
|
- [] Yes
|
||||||
- [ ] No
|
- [] No
|
||||||
|
|
||||||
<!-- If this does introduce a breaking change, please describe the impact and migration path for existing applications below. -->
|
<!-- If this does introduce a breaking change, please describe the impact and migration path for existing applications below. -->
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
name: Linux Release
|
name: Modern Linux Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
@ -8,30 +8,41 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
- name: Install python
|
- name: Install python
|
||||||
run: |
|
run: |
|
||||||
|
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt install -y python3.9 python3-pip python3.9-venv \
|
sudo apt install -y python3.11 python3-pip python3.11-venv \
|
||||||
python3.9-dev \
|
python3.11-dev \
|
||||||
python3-pyqt5
|
'^libxcb.*-dev' libx11-xcb-dev \
|
||||||
|
libglu1-mesa-dev libxrender-dev libxi-dev \
|
||||||
|
libxkbcommon-dev libxkbcommon-x11-dev
|
||||||
- name: Install dependencies and pyinstall
|
- name: Install dependencies and pyinstall
|
||||||
run: |
|
run: |
|
||||||
python3.9 -m venv build
|
python3.11 -m venv build
|
||||||
. build/bin/activate
|
. build/bin/activate
|
||||||
python -m pip install pip==23.0.1 setuptools==67.4.0
|
python -m pip install pip==23.3.2 setuptools==69.0.3
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
pip install PyInstaller==5.8.0
|
pip install PyInstaller==6.3.0
|
||||||
- name: Build binary
|
- name: Build binary
|
||||||
run: |
|
run: |
|
||||||
. build/bin/activate
|
. build/bin/activate
|
||||||
pyinstaller --onefile -n nanovna-saver nanovna-saver.py
|
python setup.py -V
|
||||||
|
pyinstaller --onefile \
|
||||||
|
-p src \
|
||||||
|
--add-data "build/lib/python3.11/site-packages/PyQt6/sip.*.so:PyQt6/sip.so" \
|
||||||
|
--add-data "build/lib/python3.11/site-packages/PyQt6/Qt6:PyQt6/Qt6"
|
||||||
|
-n nanovna-saver \
|
||||||
|
nanovna-saver.py
|
||||||
- name: Archive production artifacts
|
- name: Archive production artifacts
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v1
|
||||||
with:
|
with:
|
||||||
name: NanoVNASaver.linux
|
name: NanoVNASaver.linux_modern
|
||||||
path: dist/nanovna-saver
|
path: dist/nanovna-saver
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
name: Modern Linux Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Install python
|
|
||||||
run: |
|
|
||||||
sudo add-apt-repository ppa:deadsnakes/ppa
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt install -y python3.11 python3-pip python3.11-venv \
|
|
||||||
python3.11-dev \
|
|
||||||
python3-pyqt5
|
|
||||||
- name: Install dependencies and pyinstall
|
|
||||||
run: |
|
|
||||||
python3.11 -m venv build
|
|
||||||
. build/bin/activate
|
|
||||||
python -m pip install pip==23.0.1 setuptools==67.4.0
|
|
||||||
pip install -r requirements.txt
|
|
||||||
pip install PyInstaller==5.8.0
|
|
||||||
- name: Build binary
|
|
||||||
run: |
|
|
||||||
. build/bin/activate
|
|
||||||
pyinstaller --onefile -n nanovna-saver nanovna-saver.py
|
|
||||||
- name: Archive production artifacts
|
|
||||||
uses: actions/upload-artifact@v1
|
|
||||||
with:
|
|
||||||
name: NanoVNASaver.linux_modern
|
|
||||||
path: dist/nanovna-saver
|
|
|
@ -12,18 +12,21 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
- name: Install dependencies and pyinstall
|
- name: Install dependencies and pyinstall
|
||||||
run: |
|
run: |
|
||||||
python -m pip install pip==23.0.1 setuptools==67.4.0
|
python -m pip install pip==23.3.2 setuptools==69.0.3
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
pip install PyInstaller==5.8.0
|
pip install PyInstaller==6.3.0
|
||||||
- name: Build binary
|
- name: Build binary
|
||||||
run: |
|
run: |
|
||||||
pyinstaller --onefile -n nanovna-saver nanovna-saver.py
|
python setup.py -V
|
||||||
|
pyinstaller --onefile -p src -n nanovna-saver nanovna-saver.py
|
||||||
|
|
||||||
- name: Archive production artifacts
|
- name: Archive production artifacts
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v1
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
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
|
|
@ -11,24 +11,32 @@ jobs:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
arch: [x64, x86]
|
arch: [x64, ]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
with:
|
||||||
python-version: 3.10
|
fetch-depth: 0
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: 3.11
|
||||||
architecture: ${{ matrix.arch }}
|
architecture: ${{ matrix.arch }}
|
||||||
- name: Install dependencies and pyinstall
|
- name: Install dependencies and pyinstall
|
||||||
run: |
|
run: |
|
||||||
python -m pip install pip==23.0.1 setuptools==67.4.0
|
python3 -m venv venv
|
||||||
pip install -r requirements.txt
|
.\venv\Scripts\activate
|
||||||
pip install PyInstaller==5.8.0
|
python3 -m pip install pip==23.3.2
|
||||||
|
python3 -m pip install -U setuptools setuptools-scm
|
||||||
|
python3 -m pip install -r requirements.txt
|
||||||
|
python3 -m pip install PyInstaller==6.3.0
|
||||||
|
python3 -m pip uninstall -y PyQt6-sip
|
||||||
|
python3 -m pip install PyQt6-sip==13.6.0
|
||||||
- name: Build binary
|
- name: Build binary
|
||||||
run: |
|
run: |
|
||||||
pyinstaller --onefile -n nanovna-saver.exe nanovna-saver.py
|
.\venv\Scripts\activate
|
||||||
|
python3 setup.py -V
|
||||||
|
pyinstaller --onefile --noconsole -i icon_48x48.ico -p src -n nanovna-saver.exe nanovna-saver.py
|
||||||
- name: Archive production artifacts
|
- name: Archive production artifacts
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v1
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -1,26 +1,56 @@
|
||||||
/venv/
|
# Temporary and binary files
|
||||||
/env/
|
|
||||||
.idea/
|
|
||||||
.tox/
|
|
||||||
.vscode/
|
|
||||||
/build/
|
|
||||||
/dist/
|
|
||||||
/nanovna-saver.spec
|
|
||||||
*.egg-info/
|
|
||||||
*.pyc
|
|
||||||
*.cal
|
|
||||||
settings.json
|
|
||||||
.gitignore
|
|
||||||
.coverage
|
|
||||||
.flatpak-builder
|
|
||||||
/nanovna-saver.exe.spec
|
|
||||||
/deb_dist/
|
|
||||||
*.deb
|
|
||||||
*.rpm
|
|
||||||
*.tar.gz
|
|
||||||
*~
|
*~
|
||||||
.*~
|
*.py[cod]
|
||||||
*.bak
|
*.so
|
||||||
*.new
|
*.cfg
|
||||||
*.old
|
!.isort.cfg
|
||||||
|
!setup.cfg
|
||||||
*.orig
|
*.orig
|
||||||
|
*.log
|
||||||
|
*.pot
|
||||||
|
__pycache__/*
|
||||||
|
.cache/*
|
||||||
|
.*.swp
|
||||||
|
*/.ipynb_checkpoints/*
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Project files
|
||||||
|
.ropeproject
|
||||||
|
.project
|
||||||
|
.pydevproject
|
||||||
|
.settings
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
tags
|
||||||
|
|
||||||
|
# Package files
|
||||||
|
*.egg
|
||||||
|
*.eggs/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Unittest and coverage
|
||||||
|
htmlcov/*
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.tox
|
||||||
|
junit*.xml
|
||||||
|
coverage.xml
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Build and docs folder/files
|
||||||
|
build/*
|
||||||
|
dist/*
|
||||||
|
sdist/*
|
||||||
|
docs/api/*
|
||||||
|
docs/_rst/*
|
||||||
|
docs/_build/*
|
||||||
|
cover/*
|
||||||
|
MANIFEST
|
||||||
|
**/_version.py
|
||||||
|
.flatpak-builder/*
|
||||||
|
|
||||||
|
# Per-project virtualenvs
|
||||||
|
.venv*/
|
||||||
|
.conda*/
|
||||||
|
.python-version
|
||||||
|
|
|
@ -12,4 +12,4 @@ disable=W0614,C0410,C0321,C0111,I0011,C0103
|
||||||
# allow ls for list
|
# allow ls for list
|
||||||
good-names=_,a,b,c,dt,db,e,f,fn,fd,i,j,k,v,kv,kw,l,m,n,ls,t,t0,t1,t2,t3,w,h,x,y,z,it,op
|
good-names=_,a,b,c,dt,db,e,f,fn,fd,i,j,k,v,kv,kw,l,m,n,ls,t,t0,t1,t2,t3,w,h,x,y,z,it,op
|
||||||
[MASTER]
|
[MASTER]
|
||||||
extension-pkg-whitelist=PyQt5
|
extension-pkg-allow-list=PyQt6.QtWidgets,PyQt6.QtGui,PyQt6.QtCore
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Read the Docs configuration file
|
||||||
|
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||||
|
|
||||||
|
# Required
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
# Build documentation in the docs/ directory with Sphinx
|
||||||
|
sphinx:
|
||||||
|
configuration: docs/conf.py
|
||||||
|
|
||||||
|
# Build documentation with MkDocs
|
||||||
|
#mkdocs:
|
||||||
|
# configuration: mkdocs.yml
|
||||||
|
|
||||||
|
# Optionally build your docs in additional formats such as PDF
|
||||||
|
formats:
|
||||||
|
- pdf
|
||||||
|
|
||||||
|
build:
|
||||||
|
os: ubuntu-22.04
|
||||||
|
tools:
|
||||||
|
python: "3.11"
|
||||||
|
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- requirements: docs/requirements.txt
|
||||||
|
- {path: ., method: pip}
|
|
@ -0,0 +1,43 @@
|
||||||
|
============
|
||||||
|
Contributors
|
||||||
|
============
|
||||||
|
|
||||||
|
* Attilio Panniello <attilio.panniello@gmail.com>
|
||||||
|
* bicycleGuy <michaelrunyan@Michaels-iMac.home>
|
||||||
|
* Carl Tremblay <cinosh07@hotmail.com>
|
||||||
|
* cinosh07 <cinosh07@hotmail.com>
|
||||||
|
* Dan Halbert <halbert@halwitz.org>
|
||||||
|
* Daniel Lingvay <dlingvay@grubhub.com>
|
||||||
|
* Davide Gerhard <rainbow@irh.it>
|
||||||
|
* Denis Bondar <bondar.den@gmail.com>
|
||||||
|
* dhunt1342 <dhunt1342@users.noreply.github.com>
|
||||||
|
* DiSlord <dislord@mail.ru>
|
||||||
|
* Frank Kunz <mailinglists@kunz-im-inter.net>
|
||||||
|
* Galileo <galileo@pkm-inc.com>
|
||||||
|
* Holger Mueller <zarath@gmx.de>
|
||||||
|
* ikatkov <ikatkov@gmail.com>
|
||||||
|
* Ishmael Samuel <ishmaelsamuel79@gmail.com>
|
||||||
|
* James Limbouris <james@digitalmatter.com>
|
||||||
|
* Jaroslav Škarvada <jskarvad@redhat.com>
|
||||||
|
* Kevin Zembower <kevin@zembower.org>
|
||||||
|
* Mark Zachmann <Mark.Zachmann@snug.dog>
|
||||||
|
* Martin <Ho-Ro@users.noreply.github.com>
|
||||||
|
* Mauro Gaioni <m.gaioni@asst-valcamonica.it>
|
||||||
|
* Mauro <mauro@lenny.station>
|
||||||
|
* mihtjel <mihtjel@gmail.com>
|
||||||
|
* Mike4U <9957897+Mike4U@users.noreply.github.com>
|
||||||
|
* mss <marcspeck@gmail.com>
|
||||||
|
* Neil Katin <github2@askneil.com>
|
||||||
|
* Ohan Smit <psynosaur@gmail.com>
|
||||||
|
* Olgierd Pilarczyk <opilarczyk@egnyte.com>
|
||||||
|
* Oscilllator <harry.dudleybestow@gmail.com>
|
||||||
|
* Patrick Coleman <blinken@gmail.com>
|
||||||
|
* Peter B Marks <peter.marks@pobox.com>
|
||||||
|
* Psynosaur <psynosaur@gmail.com>
|
||||||
|
* RandMental <RandMental@users.noreply.github.com>
|
||||||
|
* Roel Jordans <r.jordans@tue.nl>
|
||||||
|
* Rune B. Broberg <mihtjel@gmail.com>
|
||||||
|
* Sascha Silbe <sascha-pgp@silbe.org>
|
||||||
|
* sysjoint-tek <63992872+sysjoint-tek@users.noreply.github.com>
|
||||||
|
* Thomas de Lellis <24543390+t52ta6ek@users.noreply.github.com>
|
||||||
|
* zstadler <zeev.stadler@gmail.com>
|
|
@ -0,0 +1,322 @@
|
||||||
|
============
|
||||||
|
Contributing
|
||||||
|
============
|
||||||
|
|
||||||
|
Welcome to ``nanovna-saver`` contributor's guide.
|
||||||
|
|
||||||
|
This document focuses on getting any potential contributor familiarized
|
||||||
|
with the development processes, but `other kinds of contributions`_ are also
|
||||||
|
appreciated.
|
||||||
|
|
||||||
|
If you are new to using git_ or have never collaborated in a project previously,
|
||||||
|
please have a look at `contribution-guide.org`_. Other resources are also
|
||||||
|
listed in the excellent `guide created by FreeCodeCamp`_ [#contrib1]_.
|
||||||
|
|
||||||
|
Please notice, all users and contributors are expected to be **open,
|
||||||
|
considerate, reasonable, and respectful**. When in doubt, `Python Software
|
||||||
|
Foundation's Code of Conduct`_ is a good reference in terms of behavior
|
||||||
|
guidelines.
|
||||||
|
|
||||||
|
|
||||||
|
Issue Reports
|
||||||
|
=============
|
||||||
|
|
||||||
|
If you experience bugs or general issues with ``nanovna-saver``, please have a look
|
||||||
|
on the `issue tracker`_. If you don't see anything useful there, please feel
|
||||||
|
free to fire an issue report.
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Please don't forget to include the closed issues in your search.
|
||||||
|
Sometimes a solution was already reported, and the problem is considered
|
||||||
|
**solved**.
|
||||||
|
|
||||||
|
New issue reports should include information about your programming environment
|
||||||
|
(e.g., operating system, Python version) and steps to reproduce the problem.
|
||||||
|
Please try also to simplify the reproduction steps to a very minimal example
|
||||||
|
that still illustrates the problem you are facing. By removing other factors,
|
||||||
|
you help us to identify the root cause of the issue.
|
||||||
|
|
||||||
|
|
||||||
|
Documentation Improvements
|
||||||
|
==========================
|
||||||
|
|
||||||
|
You can help improve ``nanovna-saver`` docs by making them more readable and coherent, or
|
||||||
|
by adding missing information and correcting mistakes.
|
||||||
|
|
||||||
|
``nanovna-saver`` documentation should use Sphinx_ as its main documentation compiler.
|
||||||
|
This means that the docs are kept in the same repository as the project code, and
|
||||||
|
that any documentation update is done in the same way was a code contribution.
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Please notice that the `GitHub web interface`_ provides a quick way of
|
||||||
|
propose changes in ``nanovna-saver``'s files. While this mechanism can
|
||||||
|
be tricky for normal code contributions, it works perfectly fine for
|
||||||
|
contributing to the docs, and can be quite handy.
|
||||||
|
|
||||||
|
If you are interested in trying this method out, please navigate to
|
||||||
|
the ``docs`` folder in the source repository_, find which file you
|
||||||
|
would like to propose changes and click in the little pencil icon at the
|
||||||
|
top, to open `GitHub's code editor`_. Once you finish editing the file,
|
||||||
|
please write a message in the form at the bottom of the page describing
|
||||||
|
which changes have you made and what are the motivations behind them and
|
||||||
|
submit your proposal.
|
||||||
|
|
||||||
|
When working on documentation changes in your local machine, you can
|
||||||
|
compile them using |tox|_::
|
||||||
|
|
||||||
|
tox -e docs
|
||||||
|
|
||||||
|
and use Python's built-in web server for a preview in your web browser
|
||||||
|
(``http://localhost:8000``)::
|
||||||
|
|
||||||
|
python3 -m http.server --directory 'docs/_build/html'
|
||||||
|
|
||||||
|
|
||||||
|
Code Contributions
|
||||||
|
==================
|
||||||
|
|
||||||
|
.. todo:: Please include a reference or explanation about the internals of the project.
|
||||||
|
|
||||||
|
An architecture description, design principles or at least a summary of the
|
||||||
|
main concepts will make it easy for potential contributors to get started
|
||||||
|
quickly.
|
||||||
|
|
||||||
|
Submit an issue
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Before you work on any non-trivial code contribution it's best to first create
|
||||||
|
a report in the `issue tracker`_ to start a discussion on the subject.
|
||||||
|
This often provides additional considerations and avoids unnecessary work.
|
||||||
|
|
||||||
|
Create an environment
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Before you start coding, we recommend creating an isolated `virtual
|
||||||
|
environment`_ to avoid any problems with your installed Python packages.
|
||||||
|
This can easily be done via either |virtualenv|_::
|
||||||
|
|
||||||
|
virtualenv <PATH TO VENV>
|
||||||
|
source <PATH TO VENV>/bin/activate
|
||||||
|
|
||||||
|
or Miniconda_::
|
||||||
|
|
||||||
|
conda create -n nanovna-saver python=3 six virtualenv pytest pytest-cov
|
||||||
|
conda activate nanovna-saver
|
||||||
|
|
||||||
|
Clone the repository
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
#. Create an user account on |the repository service| if you do not already have one.
|
||||||
|
#. Fork the project repository_: click on the *Fork* button near the top of the
|
||||||
|
page. This creates a copy of the code under your account on |the repository service|.
|
||||||
|
#. Clone this copy to your local disk::
|
||||||
|
|
||||||
|
git clone git@github.com:YourLogin/nanovna-saver.git
|
||||||
|
cd nanovna-saver
|
||||||
|
|
||||||
|
#. You should run::
|
||||||
|
|
||||||
|
pip install -U pip setuptools -e .
|
||||||
|
|
||||||
|
to be able to import the package under development in the Python REPL.
|
||||||
|
|
||||||
|
.. todo:: if you are not using pre-commit, please remove the following item:
|
||||||
|
|
||||||
|
#. Install |pre-commit|_::
|
||||||
|
|
||||||
|
pip install pre-commit
|
||||||
|
pre-commit install
|
||||||
|
|
||||||
|
``nanovna-saver`` comes with a lot of hooks configured to automatically help the
|
||||||
|
developer to check the code being written.
|
||||||
|
|
||||||
|
Implement your changes
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
#. Create a branch to hold your changes::
|
||||||
|
|
||||||
|
git checkout -b my-feature
|
||||||
|
|
||||||
|
and start making changes. Never work on the main branch!
|
||||||
|
|
||||||
|
#. Start your work on this branch. Don't forget to add docstrings_ to new
|
||||||
|
functions, modules and classes, especially if they are part of public APIs.
|
||||||
|
|
||||||
|
#. Add yourself to the list of contributors in ``AUTHORS.rst``.
|
||||||
|
|
||||||
|
#. When you’re done editing, do::
|
||||||
|
|
||||||
|
git add <MODIFIED FILES>
|
||||||
|
git commit
|
||||||
|
|
||||||
|
to record your changes in git_.
|
||||||
|
|
||||||
|
.. todo:: if you are not using pre-commit, please remove the following item:
|
||||||
|
|
||||||
|
Please make sure to see the validation messages from |pre-commit|_ and fix
|
||||||
|
any eventual issues.
|
||||||
|
This should automatically use flake8_/black_ to check/fix the code style
|
||||||
|
in a way that is compatible with the project.
|
||||||
|
|
||||||
|
.. important:: Don't forget to add unit tests and documentation in case your
|
||||||
|
contribution adds an additional feature and is not just a bugfix.
|
||||||
|
|
||||||
|
Moreover, writing a `descriptive commit message`_ is highly recommended.
|
||||||
|
In case of doubt, you can check the commit history with::
|
||||||
|
|
||||||
|
git log --graph --decorate --pretty=oneline --abbrev-commit --all
|
||||||
|
|
||||||
|
to look for recurring communication patterns.
|
||||||
|
|
||||||
|
#. Please check that your changes don't break any unit tests with::
|
||||||
|
|
||||||
|
tox
|
||||||
|
|
||||||
|
(after having installed |tox|_ with ``pip install tox`` or ``pipx``).
|
||||||
|
|
||||||
|
You can also use |tox|_ to run several other pre-configured tasks in the
|
||||||
|
repository. Try ``tox -av`` to see a list of the available checks.
|
||||||
|
|
||||||
|
Submit your contribution
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
#. If everything works fine, push your local branch to |the repository service| with::
|
||||||
|
|
||||||
|
git push -u origin my-feature
|
||||||
|
|
||||||
|
#. Go to the web page of your fork and click |contribute button|
|
||||||
|
to send your changes for review.
|
||||||
|
|
||||||
|
.. todo:: if you are using GitHub, you can uncomment the following paragraph
|
||||||
|
|
||||||
|
Find more detailed information in `creating a PR`_. You might also want to open
|
||||||
|
the PR as a draft first and mark it as ready for review after the feedbacks
|
||||||
|
from the continuous integration (CI) system or any required fixes.
|
||||||
|
|
||||||
|
|
||||||
|
Troubleshooting
|
||||||
|
---------------
|
||||||
|
|
||||||
|
The following tips can be used when facing problems to build or test the
|
||||||
|
package:
|
||||||
|
|
||||||
|
#. Make sure to fetch all the tags from the upstream repository_.
|
||||||
|
The command ``git describe --abbrev=0 --tags`` should return the version you
|
||||||
|
are expecting. If you are trying to run CI scripts in a fork repository,
|
||||||
|
make sure to push all the tags.
|
||||||
|
You can also try to remove all the egg files or the complete egg folder, i.e.,
|
||||||
|
``.eggs``, as well as the ``*.egg-info`` folders in the ``src`` folder or
|
||||||
|
potentially in the root of your project.
|
||||||
|
|
||||||
|
#. Sometimes |tox|_ misses out when new dependencies are added, especially to
|
||||||
|
``setup.cfg`` and ``docs/requirements.txt``. If you find any problems with
|
||||||
|
missing dependencies when running a command with |tox|_, try to recreate the
|
||||||
|
``tox`` environment using the ``-r`` flag. For example, instead of::
|
||||||
|
|
||||||
|
tox -e docs
|
||||||
|
|
||||||
|
Try running::
|
||||||
|
|
||||||
|
tox -r -e docs
|
||||||
|
|
||||||
|
#. Make sure to have a reliable |tox|_ installation that uses the correct
|
||||||
|
Python version (e.g., 3.7+). When in doubt you can run::
|
||||||
|
|
||||||
|
tox --version
|
||||||
|
# OR
|
||||||
|
which tox
|
||||||
|
|
||||||
|
If you have trouble and are seeing weird errors upon running |tox|_, you can
|
||||||
|
also try to create a dedicated `virtual environment`_ with a |tox|_ binary
|
||||||
|
freshly installed. For example::
|
||||||
|
|
||||||
|
virtualenv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
.venv/bin/pip install tox
|
||||||
|
.venv/bin/tox -e all
|
||||||
|
|
||||||
|
#. `Pytest can drop you`_ in an interactive session in the case an error occurs.
|
||||||
|
In order to do that you need to pass a ``--pdb`` option (for example by
|
||||||
|
running ``tox -- -k <NAME OF THE FALLING TEST> --pdb``).
|
||||||
|
You can also setup breakpoints manually instead of using the ``--pdb`` option.
|
||||||
|
|
||||||
|
|
||||||
|
Maintainer tasks
|
||||||
|
================
|
||||||
|
|
||||||
|
Releases
|
||||||
|
--------
|
||||||
|
|
||||||
|
.. todo:: This section assumes you are using PyPI to publicly release your package.
|
||||||
|
|
||||||
|
If instead you are using a different/private package index, please update
|
||||||
|
the instructions accordingly.
|
||||||
|
|
||||||
|
If you are part of the group of maintainers and have correct user permissions
|
||||||
|
on PyPI_, the following steps can be used to release a new version for
|
||||||
|
``nanovna-saver``:
|
||||||
|
|
||||||
|
#. Make sure all unit tests are successful.
|
||||||
|
#. Tag the current commit on the main branch with a release tag, e.g., ``v1.2.3``.
|
||||||
|
#. Push the new tag to the upstream repository_, e.g., ``git push upstream v1.2.3``
|
||||||
|
#. Clean up the ``dist`` and ``build`` folders with ``tox -e clean``
|
||||||
|
(or ``rm -rf dist build``)
|
||||||
|
to avoid confusion with old builds and Sphinx docs.
|
||||||
|
#. Run ``tox -e build`` and check that the files in ``dist`` have
|
||||||
|
the correct version (no ``.dirty`` or git_ hash) according to the git_ tag.
|
||||||
|
Also check the sizes of the distributions, if they are too big (e.g., >
|
||||||
|
500KB), unwanted clutter may have been accidentally included.
|
||||||
|
#. Run ``tox -e publish -- --repository pypi`` and check that everything was
|
||||||
|
uploaded to PyPI_ correctly.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.. [#contrib1] Even though, these resources focus on open source projects and
|
||||||
|
communities, the general ideas behind collaborating with other developers
|
||||||
|
to collectively create software are general and can be applied to all sorts
|
||||||
|
of environments, including private companies and proprietary code bases.
|
||||||
|
|
||||||
|
|
||||||
|
.. <-- start -->
|
||||||
|
.. todo:: Please review and change the following definitions:
|
||||||
|
|
||||||
|
.. |the repository service| replace:: GitHub
|
||||||
|
.. |contribute button| replace:: "Create pull request"
|
||||||
|
|
||||||
|
.. _repository: https://github.com/<USERNAME>/nanovna-saver
|
||||||
|
.. _issue tracker: https://github.com/<USERNAME>/nanovna-saver/issues
|
||||||
|
.. <-- end -->
|
||||||
|
|
||||||
|
|
||||||
|
.. |virtualenv| replace:: ``virtualenv``
|
||||||
|
.. |pre-commit| replace:: ``pre-commit``
|
||||||
|
.. |tox| replace:: ``tox``
|
||||||
|
|
||||||
|
|
||||||
|
.. _black: https://pypi.org/project/black/
|
||||||
|
.. _CommonMark: https://commonmark.org/
|
||||||
|
.. _contribution-guide.org: https://www.contribution-guide.org/
|
||||||
|
.. _creating a PR: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request
|
||||||
|
.. _descriptive commit message: https://chris.beams.io/posts/git-commit
|
||||||
|
.. _docstrings: https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html
|
||||||
|
.. _first-contributions tutorial: https://github.com/firstcontributions/first-contributions
|
||||||
|
.. _flake8: https://flake8.pycqa.org/en/stable/
|
||||||
|
.. _git: https://git-scm.com
|
||||||
|
.. _GitHub's fork and pull request workflow: https://guides.github.com/activities/forking/
|
||||||
|
.. _guide created by FreeCodeCamp: https://github.com/FreeCodeCamp/how-to-contribute-to-open-source
|
||||||
|
.. _Miniconda: https://docs.conda.io/en/latest/miniconda.html
|
||||||
|
.. _MyST: https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html
|
||||||
|
.. _other kinds of contributions: https://opensource.guide/how-to-contribute
|
||||||
|
.. _pre-commit: https://pre-commit.com/
|
||||||
|
.. _PyPI: https://pypi.org/
|
||||||
|
.. _PyScaffold's contributor's guide: https://pyscaffold.org/en/stable/contributing.html
|
||||||
|
.. _Pytest can drop you: https://docs.pytest.org/en/stable/how-to/failures.html#using-python-library-pdb-with-pytest
|
||||||
|
.. _Python Software Foundation's Code of Conduct: https://www.python.org/psf/conduct/
|
||||||
|
.. _reStructuredText: https://www.sphinx-doc.org/en/master/usage/restructuredtext/
|
||||||
|
.. _Sphinx: https://www.sphinx-doc.org/en/master/
|
||||||
|
.. _tox: https://tox.wiki/en/stable/
|
||||||
|
.. _virtual environment: https://realpython.com/python-virtual-environments-a-primer/
|
||||||
|
.. _virtualenv: https://virtualenv.pypa.io/en/stable/
|
||||||
|
|
||||||
|
.. _GitHub web interface: https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files
|
||||||
|
.. _GitHub's code editor: https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files
|
|
@ -1,119 +0,0 @@
|
||||||
# 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 logging
|
|
||||||
from typing import Callable, List, Tuple
|
|
||||||
|
|
||||||
from PyQt5 import QtWidgets
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from NanoVNASaver.Analysis.Base import Analysis, QHLine
|
|
||||||
from NanoVNASaver.Formatting import (
|
|
||||||
format_frequency, format_gain, format_resistance, format_vswr)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class SimplePeakSearchAnalysis(Analysis):
|
|
||||||
def __init__(self, app):
|
|
||||||
super().__init__(app)
|
|
||||||
|
|
||||||
self.label['peak_freq'] = QtWidgets.QLabel()
|
|
||||||
self.label['peak_db'] = QtWidgets.QLabel()
|
|
||||||
|
|
||||||
self.button = {
|
|
||||||
'vswr': QtWidgets.QRadioButton("VSWR"),
|
|
||||||
'resistance': QtWidgets.QRadioButton("Resistance"),
|
|
||||||
'reactance': QtWidgets.QRadioButton("Reactance"),
|
|
||||||
'gain': QtWidgets.QRadioButton("S21 Gain"),
|
|
||||||
'peak_h': QtWidgets.QRadioButton("Highest value"),
|
|
||||||
'peak_l': QtWidgets.QRadioButton("Lowest value"),
|
|
||||||
'move_marker': QtWidgets.QCheckBox()
|
|
||||||
}
|
|
||||||
|
|
||||||
self.button['gain'].setChecked(True)
|
|
||||||
self.button['peak_h'].setChecked(True)
|
|
||||||
|
|
||||||
self.btn_group = {
|
|
||||||
'data': QtWidgets.QButtonGroup(),
|
|
||||||
'peak': QtWidgets.QButtonGroup(),
|
|
||||||
}
|
|
||||||
|
|
||||||
for btn in ('vswr', 'resistance', 'reactance', 'gain'):
|
|
||||||
self.btn_group['data'].addButton(self.button[btn])
|
|
||||||
self.btn_group['peak'].addButton(self.button['peak_h'])
|
|
||||||
self.btn_group['peak'].addButton(self.button['peak_l'])
|
|
||||||
|
|
||||||
layout = self.layout
|
|
||||||
layout.addRow(self.label['titel'])
|
|
||||||
layout.addRow(QHLine())
|
|
||||||
layout.addRow(QtWidgets.QLabel("<b>Settings</b>"))
|
|
||||||
layout.addRow("Data source", self.button['vswr'])
|
|
||||||
layout.addRow("", self.button['resistance'])
|
|
||||||
layout.addRow("", self.button['reactance'])
|
|
||||||
layout.addRow("", self.button['gain'])
|
|
||||||
layout.addRow(QHLine())
|
|
||||||
layout.addRow("Peak type", self.button['peak_h'])
|
|
||||||
layout.addRow("", self.button['peak_l'])
|
|
||||||
layout.addRow(QHLine())
|
|
||||||
layout.addRow("Move marker to peak", self.button['move_marker'])
|
|
||||||
layout.addRow(QHLine())
|
|
||||||
layout.addRow(self.label['result'])
|
|
||||||
layout.addRow("Peak frequency:", self.label['peak_freq'])
|
|
||||||
layout.addRow("Peak value:", self.label['peak_db'])
|
|
||||||
|
|
||||||
self.set_titel('Simple peak search')
|
|
||||||
|
|
||||||
def runAnalysis(self):
|
|
||||||
if not self.app.data.s11:
|
|
||||||
return
|
|
||||||
|
|
||||||
s11 = self.app.data.s11
|
|
||||||
data, fmt_fnc = self.data_and_format()
|
|
||||||
|
|
||||||
if self.button['peak_l'].isChecked():
|
|
||||||
idx_peak = np.argmin(data)
|
|
||||||
else:
|
|
||||||
self.button['peak_h'].setChecked(True)
|
|
||||||
idx_peak = np.argmax(data)
|
|
||||||
|
|
||||||
self.label['peak_freq'].setText(format_frequency(s11[idx_peak].freq))
|
|
||||||
self.label['peak_db'].setText(fmt_fnc(data[idx_peak]))
|
|
||||||
|
|
||||||
if self.button['move_marker'].isChecked() and self.app.markers:
|
|
||||||
self.app.markers[0].setFrequency(f"{s11[idx_peak].freq}")
|
|
||||||
|
|
||||||
def data_and_format(self) -> Tuple[List[float], Callable]:
|
|
||||||
s11 = self.app.data.s11
|
|
||||||
s21 = self.app.data.s21
|
|
||||||
|
|
||||||
if not s21:
|
|
||||||
self.button['gain'].setEnabled(False)
|
|
||||||
if self.button['gain'].isChecked():
|
|
||||||
self.button['vswr'].setChecked(True)
|
|
||||||
else:
|
|
||||||
self.button['gain'].setEnabled(True)
|
|
||||||
|
|
||||||
if self.button['gain'].isChecked():
|
|
||||||
return ([d.gain for d in s21], format_gain)
|
|
||||||
if self.button['resistance'].isChecked():
|
|
||||||
return ([d.impedance().real for d in s11], format_resistance)
|
|
||||||
if self.button['reactance'].isChecked():
|
|
||||||
return ([d.impedance().imag for d in s11], format_resistance)
|
|
||||||
# default
|
|
||||||
return ([d.vswr for d in s11], format_vswr)
|
|
|
@ -1,102 +0,0 @@
|
||||||
# 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 logging
|
|
||||||
from PyQt5 import QtGui, QtCore
|
|
||||||
|
|
||||||
from NanoVNASaver.Charts.Chart import Chart
|
|
||||||
from NanoVNASaver.Charts.Square import SquareChart
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class SmithChart(SquareChart):
|
|
||||||
def drawChart(self, qp: QtGui.QPainter) -> None:
|
|
||||||
center_x = self.width() // 2
|
|
||||||
center_y = self.height() // 2
|
|
||||||
width_2 = self.dim.width // 2
|
|
||||||
height_2 = self.dim.height // 2
|
|
||||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
|
||||||
qp.drawText(3, 15, self.name)
|
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
|
||||||
qp.drawEllipse(QtCore.QPoint(center_x, center_y), width_2, height_2)
|
|
||||||
qp.drawLine(center_x - width_2, center_y,
|
|
||||||
center_x + width_2, center_y)
|
|
||||||
|
|
||||||
qp.drawEllipse(
|
|
||||||
QtCore.QPoint(center_x + int(self.dim.width / 4), center_y),
|
|
||||||
self.dim.width // 4, self.dim.height // 4) # Re(Z) = 1
|
|
||||||
qp.drawEllipse(
|
|
||||||
QtCore.QPoint(center_x + self.dim.width // 3, center_y),
|
|
||||||
self.dim.width // 6, self.dim.height // 6) # Re(Z) = 2
|
|
||||||
qp.drawEllipse(
|
|
||||||
QtCore.QPoint(center_x + 3 * self.dim.width // 8, center_y),
|
|
||||||
self.dim.width // 8, self.dim.height // 8) # Re(Z) = 3
|
|
||||||
qp.drawEllipse(
|
|
||||||
QtCore.QPoint(center_x + 5 * self.dim.width // 12, center_y),
|
|
||||||
self.dim.width // 12, self.dim.height // 12) # Re(Z) = 5
|
|
||||||
qp.drawEllipse(
|
|
||||||
QtCore.QPoint(center_x + self.dim.width // 6, center_y),
|
|
||||||
self.dim.width // 3, self.dim.height // 3) # Re(Z) = 0.5
|
|
||||||
qp.drawEllipse(
|
|
||||||
QtCore.QPoint(center_x + self.dim.width // 12, center_y),
|
|
||||||
5 * self.dim.width // 12, 5 * self.dim.height // 12) # Re(Z) = 0.2
|
|
||||||
|
|
||||||
qp.drawArc(center_x + 3 * self.dim.width // 8, center_y,
|
|
||||||
self.dim.width // 4, self.dim.width // 4,
|
|
||||||
90 * 16, 152 * 16) # Im(Z) = -5
|
|
||||||
qp.drawArc(center_x + 3 * self.dim.width // 8, center_y,
|
|
||||||
self.dim.width // 4, -self.dim.width // 4,
|
|
||||||
-90 * 16, -152 * 16) # Im(Z) = 5
|
|
||||||
qp.drawArc(center_x + self.dim.width // 4, center_y,
|
|
||||||
width_2, height_2,
|
|
||||||
90 * 16, 127 * 16) # Im(Z) = -2
|
|
||||||
qp.drawArc(center_x + self.dim.width // 4, center_y,
|
|
||||||
width_2, -height_2,
|
|
||||||
-90 * 16, -127 * 16) # Im(Z) = 2
|
|
||||||
qp.drawArc(center_x, center_y,
|
|
||||||
self.dim.width, self.dim.height,
|
|
||||||
90 * 16, 90 * 16) # Im(Z) = -1
|
|
||||||
qp.drawArc(center_x, center_y,
|
|
||||||
self.dim.width, - self.dim.height,
|
|
||||||
-90 * 16, -90 * 16) # Im(Z) = 1
|
|
||||||
qp.drawArc(center_x - width_2, center_y,
|
|
||||||
self.dim.width * 2, self.dim.height * 2,
|
|
||||||
int(99.5 * 16), int(43.5 * 16)) # Im(Z) = -0.5
|
|
||||||
qp.drawArc(center_x - width_2, center_y,
|
|
||||||
self.dim.width * 2, -self.dim.height * 2,
|
|
||||||
int(-99.5 * 16), int(-43.5 * 16)) # Im(Z) = 0.5
|
|
||||||
qp.drawArc(center_x - self.dim.width * 2, center_y,
|
|
||||||
self.dim.width * 5, self.dim.height * 5,
|
|
||||||
int(93.85 * 16), int(18.85 * 16)) # Im(Z) = -0.2
|
|
||||||
qp.drawArc(center_x - self.dim.width * 2, center_y,
|
|
||||||
self.dim.width * 5, -self.dim.height * 5,
|
|
||||||
int(-93.85 * 16), int(-18.85 * 16)) # Im(Z) = 0.2
|
|
||||||
|
|
||||||
self.drawTitle(qp)
|
|
||||||
|
|
||||||
qp.setPen(Chart.color.swr)
|
|
||||||
for swr in self.swrMarkers:
|
|
||||||
if swr <= 1:
|
|
||||||
continue
|
|
||||||
gamma = (swr - 1) / (swr + 1)
|
|
||||||
r = int(gamma * self.dim.width / 2)
|
|
||||||
qp.drawEllipse(QtCore.QPoint(center_x, center_y), r, r)
|
|
||||||
qp.drawText(
|
|
||||||
QtCore.QRect(center_x - 50, center_y - 4 + r, 100, 20),
|
|
||||||
QtCore.Qt.AlignCenter, f"{swr}")
|
|
|
@ -1,117 +0,0 @@
|
||||||
# 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 logging
|
|
||||||
from enum import Enum
|
|
||||||
from math import log
|
|
||||||
from threading import Lock
|
|
||||||
from typing import Iterator, Tuple
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class SweepMode(Enum):
|
|
||||||
SINGLE = 0
|
|
||||||
CONTINOUS = 1
|
|
||||||
AVERAGE = 2
|
|
||||||
|
|
||||||
|
|
||||||
class Properties:
|
|
||||||
def __init__(self, name: str = "",
|
|
||||||
mode: 'SweepMode' = SweepMode.SINGLE,
|
|
||||||
averages: Tuple[int, int] = (3, 0),
|
|
||||||
logarithmic: bool = False):
|
|
||||||
self.name = name
|
|
||||||
self.mode = mode
|
|
||||||
self.averages = averages
|
|
||||||
self.logarithmic = logarithmic
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return (
|
|
||||||
f"Properties('{self.name}', {self.mode}, {self.averages},"
|
|
||||||
f" {self.logarithmic})")
|
|
||||||
|
|
||||||
|
|
||||||
class Sweep:
|
|
||||||
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
|
|
||||||
self.segments = segments
|
|
||||||
self.properties = properties
|
|
||||||
self.lock = Lock()
|
|
||||||
self.check()
|
|
||||||
logger.debug("%s", self)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return (
|
|
||||||
f"Sweep({self.start}, {self.end}, {self.points}, {self.segments},"
|
|
||||||
f" {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)
|
|
||||||
|
|
||||||
def copy(self) -> 'Sweep':
|
|
||||||
return Sweep(self.start, self.end, self.points, self.segments,
|
|
||||||
self.properties)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def span(self) -> int:
|
|
||||||
return self.end - self.start
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stepsize(self) -> int:
|
|
||||||
return round(self.span / (self.points * self.segments - 1))
|
|
||||||
|
|
||||||
def check(self):
|
|
||||||
if (
|
|
||||||
self.segments <= 0
|
|
||||||
or self.points <= 0
|
|
||||||
or self.start <= 0
|
|
||||||
or self.end <= 0
|
|
||||||
or self.stepsize < 1
|
|
||||||
):
|
|
||||||
raise ValueError(f"Illegal sweep settings: {self}")
|
|
||||||
|
|
||||||
def _exp_factor(self, index: int) -> float:
|
|
||||||
return 1 - log(self.segments + 1 - index) / log(self.segments + 1)
|
|
||||||
|
|
||||||
def get_index_range(self, index: int) -> Tuple[int, int]:
|
|
||||||
if not self.properties.logarithmic:
|
|
||||||
start = self.start + index * self.points * self.stepsize
|
|
||||||
end = start + (self.points - 1) * self.stepsize
|
|
||||||
else:
|
|
||||||
start = round(self.start + self.span * self._exp_factor(index))
|
|
||||||
end = round(self.start + self.span * self._exp_factor(index + 1))
|
|
||||||
logger.debug("get_index_range(%s) -> (%s, %s)", index, start, end)
|
|
||||||
return start, end
|
|
||||||
|
|
||||||
def get_frequencies(self) -> Iterator[int]:
|
|
||||||
for i in range(self.segments):
|
|
||||||
start, stop = self.get_index_range(i)
|
|
||||||
step = (stop - start) / self.points
|
|
||||||
freq = start
|
|
||||||
for _ in range(self.points):
|
|
||||||
yield round(freq)
|
|
||||||
freq += step
|
|
|
@ -1,88 +0,0 @@
|
||||||
# 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 logging
|
|
||||||
import re
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Version:
|
|
||||||
RXP = re.compile(r"""^
|
|
||||||
\D*
|
|
||||||
(?P<major>\d+)\.
|
|
||||||
(?P<minor>\d+)\.?
|
|
||||||
(?P<revision>\d+)?
|
|
||||||
(?P<note>.*)
|
|
||||||
$""", re.VERBOSE)
|
|
||||||
|
|
||||||
def __init__(self, vstring: str = "0.0.0"):
|
|
||||||
self.data = {
|
|
||||||
"major": 0,
|
|
||||||
"minor": 0,
|
|
||||||
"revision": 0,
|
|
||||||
"note": "",
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
self.data = Version.RXP.search(vstring).groupdict()
|
|
||||||
for name in ("major", "minor", "revision"):
|
|
||||||
self.data[name] = int(self.data[name])
|
|
||||||
except TypeError:
|
|
||||||
self.data["revision"] = 0
|
|
||||||
except AttributeError:
|
|
||||||
logger.error("Unable to parse version: %s", vstring)
|
|
||||||
|
|
||||||
def __gt__(self, other: "Version") -> bool:
|
|
||||||
l, r = self.data, other.data
|
|
||||||
for name in ("major", "minor", "revision"):
|
|
||||||
if l[name] > r[name]:
|
|
||||||
return True
|
|
||||||
if l[name] < r[name]:
|
|
||||||
return False
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __lt__(self, other: "Version") -> bool:
|
|
||||||
return other.__gt__(self)
|
|
||||||
|
|
||||||
def __ge__(self, other: "Version") -> bool:
|
|
||||||
return self.__gt__(other) or self.__eq__(other)
|
|
||||||
|
|
||||||
def __le__(self, other: "Version") -> bool:
|
|
||||||
return other.__gt__(self) or self.__eq__(other)
|
|
||||||
|
|
||||||
def __eq__(self, other: "Version") -> bool:
|
|
||||||
return self.data == other.data
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return (f'{self.data["major"]}.{self.data["minor"]}'
|
|
||||||
f'.{self.data["revision"]}{self.data["note"]}')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def major(self) -> int:
|
|
||||||
return self.data["major"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def minor(self) -> int:
|
|
||||||
return self.data["minor"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def revision(self) -> int:
|
|
||||||
return self.data["revision"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def note(self) -> str:
|
|
||||||
return self.data["note"]
|
|
|
@ -1,165 +0,0 @@
|
||||||
# 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 contextlib
|
|
||||||
import logging
|
|
||||||
from time import strftime, localtime
|
|
||||||
from urllib import request, error
|
|
||||||
|
|
||||||
from PyQt5 import QtWidgets, QtCore
|
|
||||||
|
|
||||||
from NanoVNASaver.About import VERSION_URL, INFO_URL
|
|
||||||
from NanoVNASaver.Version import Version
|
|
||||||
from NanoVNASaver.Windows.Defaults import make_scrollable
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class AboutWindow(QtWidgets.QWidget):
|
|
||||||
def __init__(self, app: QtWidgets.QWidget):
|
|
||||||
super().__init__()
|
|
||||||
self.app = app
|
|
||||||
|
|
||||||
self.setWindowTitle("About NanoVNASaver")
|
|
||||||
self.setWindowIcon(self.app.icon)
|
|
||||||
|
|
||||||
top_layout = QtWidgets.QHBoxLayout()
|
|
||||||
make_scrollable(self, top_layout)
|
|
||||||
|
|
||||||
QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
|
|
||||||
|
|
||||||
icon_layout = QtWidgets.QVBoxLayout()
|
|
||||||
top_layout.addLayout(icon_layout)
|
|
||||||
icon = QtWidgets.QLabel()
|
|
||||||
icon.setPixmap(self.app.icon.pixmap(128, 128))
|
|
||||||
icon_layout.addWidget(icon)
|
|
||||||
icon_layout.addStretch()
|
|
||||||
|
|
||||||
layout = QtWidgets.QVBoxLayout()
|
|
||||||
top_layout.addLayout(layout)
|
|
||||||
|
|
||||||
layout.addWidget(QtWidgets.QLabel(
|
|
||||||
f"NanoVNASaver version {self.app.version}"))
|
|
||||||
layout.addWidget(QtWidgets.QLabel(""))
|
|
||||||
layout.addWidget(QtWidgets.QLabel(
|
|
||||||
"\N{COPYRIGHT SIGN} Copyright 2019, 2020 Rune B. Broberg\n"
|
|
||||||
"\N{COPYRIGHT SIGN} Copyright 2020ff NanoVNA-Saver Authors"
|
|
||||||
))
|
|
||||||
layout.addWidget(QtWidgets.QLabel(
|
|
||||||
"This program comes with ABSOLUTELY NO WARRANTY"))
|
|
||||||
layout.addWidget(QtWidgets.QLabel(
|
|
||||||
"This program is licensed under the"
|
|
||||||
" GNU General Public License version 3"))
|
|
||||||
layout.addWidget(QtWidgets.QLabel(""))
|
|
||||||
link_label = QtWidgets.QLabel(
|
|
||||||
f'For further details, see: <a href="{INFO_URL}">'
|
|
||||||
f"{INFO_URL}")
|
|
||||||
link_label.setOpenExternalLinks(True)
|
|
||||||
layout.addWidget(link_label)
|
|
||||||
layout.addWidget(QtWidgets.QLabel(""))
|
|
||||||
|
|
||||||
self.versionLabel = QtWidgets.QLabel(
|
|
||||||
"NanoVNA Firmware Version: Not connected.")
|
|
||||||
layout.addWidget(self.versionLabel)
|
|
||||||
|
|
||||||
layout.addStretch()
|
|
||||||
|
|
||||||
btn_check_version = QtWidgets.QPushButton("Check for updates")
|
|
||||||
btn_check_version.clicked.connect(self.findUpdates)
|
|
||||||
|
|
||||||
self.updateLabel = QtWidgets.QLabel("Last checked: ")
|
|
||||||
|
|
||||||
update_hbox = QtWidgets.QHBoxLayout()
|
|
||||||
update_hbox.addWidget(btn_check_version)
|
|
||||||
update_form = QtWidgets.QFormLayout()
|
|
||||||
update_hbox.addLayout(update_form)
|
|
||||||
update_hbox.addStretch()
|
|
||||||
update_form.addRow(self.updateLabel)
|
|
||||||
layout.addLayout(update_hbox)
|
|
||||||
|
|
||||||
layout.addStretch()
|
|
||||||
|
|
||||||
btn_ok = QtWidgets.QPushButton("Ok")
|
|
||||||
btn_ok.clicked.connect(lambda: self.close()) # noqa
|
|
||||||
layout.addWidget(btn_ok)
|
|
||||||
|
|
||||||
def show(self):
|
|
||||||
super().show()
|
|
||||||
self.updateLabels()
|
|
||||||
|
|
||||||
def updateLabels(self):
|
|
||||||
with contextlib.suppress(IOError, AttributeError):
|
|
||||||
self.versionLabel.setText(
|
|
||||||
f"NanoVNA Firmware Version: {self.app.vna.name} "
|
|
||||||
f"v{self.app.vna.version}")
|
|
||||||
|
|
||||||
def findUpdates(self, automatic=False):
|
|
||||||
latest_version = Version()
|
|
||||||
latest_url = ""
|
|
||||||
try:
|
|
||||||
req = request.Request(VERSION_URL)
|
|
||||||
req.add_header('User-Agent', f'NanoVNA-Saver/{self.app.version}')
|
|
||||||
for line in request.urlopen(req, timeout=3):
|
|
||||||
line = line.decode("utf-8")
|
|
||||||
if line.startswith("VERSION ="):
|
|
||||||
latest_version = Version(line[8:].strip(" \"'"))
|
|
||||||
if line.startswith("RELEASE_URL ="):
|
|
||||||
latest_url = line[13:].strip(" \"'")
|
|
||||||
except error.HTTPError as e:
|
|
||||||
logger.exception(
|
|
||||||
"Checking for updates produced an HTTP exception: %s", e)
|
|
||||||
self.updateLabel.setText("Connection error.")
|
|
||||||
return
|
|
||||||
except TypeError as e:
|
|
||||||
logger.exception(
|
|
||||||
"Checking for updates provided an unparseable file: %s", e)
|
|
||||||
self.updateLabel.setText("Data error reading versions.")
|
|
||||||
return
|
|
||||||
except error.URLError as e:
|
|
||||||
logger.exception(
|
|
||||||
"Checking for updates produced a URL exception: %s", e)
|
|
||||||
self.updateLabel.setText("Connection error.")
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info("Latest version is %s", latest_version)
|
|
||||||
this_version = Version(self.app.version)
|
|
||||||
logger.info("This is %s", this_version)
|
|
||||||
if latest_version > this_version:
|
|
||||||
logger.info("New update available: %s!", latest_version)
|
|
||||||
if automatic:
|
|
||||||
QtWidgets.QMessageBox.information(
|
|
||||||
self,
|
|
||||||
"Updates available",
|
|
||||||
f"There is a new update for NanoVNA-Saver available!\n"
|
|
||||||
f"Version {latest_version}\n\n"
|
|
||||||
f'Press "About" to find the update.')
|
|
||||||
else:
|
|
||||||
QtWidgets.QMessageBox.information(
|
|
||||||
self, "Updates available",
|
|
||||||
"There is a new update for NanoVNA-Saver available!")
|
|
||||||
self.updateLabel.setText(
|
|
||||||
f'<a href="{latest_url}">New version available</a>.')
|
|
||||||
self.updateLabel.setOpenExternalLinks(True)
|
|
||||||
else:
|
|
||||||
# Probably don't show a message box, just update the screen?
|
|
||||||
# Maybe consider showing it if not an automatic update.
|
|
||||||
#
|
|
||||||
self.updateLabel.setText(
|
|
||||||
f"Last checked: "
|
|
||||||
f"{strftime('%Y-%m-%d %H:%M:%S', localtime())}")
|
|
||||||
return
|
|
14
Pipfile
14
Pipfile
|
@ -1,14 +0,0 @@
|
||||||
[[source]]
|
|
||||||
name = "pypi"
|
|
||||||
url = "https://pypi.org/simple"
|
|
||||||
verify_ssl = true
|
|
||||||
|
|
||||||
[dev-packages]
|
|
||||||
|
|
||||||
[packages]
|
|
||||||
pyserial = "*"
|
|
||||||
pyqt5 = "*"
|
|
||||||
numpy = "*"
|
|
||||||
|
|
||||||
[requires]
|
|
||||||
python_version = "3.7"
|
|
233
README.md
233
README.md
|
@ -1,233 +0,0 @@
|
||||||
[![Latest Release](https://img.shields.io/github/v/release/NanoVNA-Saver/nanovna-saver.svg)](https://github.com/NanoVNA-Saver/nanovna-saver/releases/latest)
|
|
||||||
[![License](https://img.shields.io/github/license/NanoVNA-Saver/nanovna-saver.svg)](https://github.com/NanoVNA-Saver/nanovna-saver/blob/master/LICENSE)
|
|
||||||
[![Downloads](https://img.shields.io/github/downloads/NanoVNA-Saver/nanovna-saver/total.svg)](https://github.com/NanoVNA-Saver/nanovna-saver/releases/)
|
|
||||||
[![GitHub Releases](https://img.shields.io/github/downloads/NanoVNA-Saver/nanovna-saver/latest/total)](https://github.com/NanoVNA-Saver/nanovna-saver/releases/latest)
|
|
||||||
[![Donate](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=T8KTGVDQF5K6E&item_name=NanoVNASaver+Development¤cy_code=EUR&source=url)
|
|
||||||
|
|
||||||
# NanoVNASaver
|
|
||||||
|
|
||||||
A multiplatform tool to save Touchstone files from the NanoVNA,
|
|
||||||
sweep frequency spans in segments to gain more than 101 data
|
|
||||||
points, and generally display and analyze the resulting data.
|
|
||||||
|
|
||||||
- Copyright 2019, 2020 Rune B. Broberg
|
|
||||||
- Copyright 2020ff NanoVNA-Saver Authors
|
|
||||||
|
|
||||||
<a href="#built-with"></a>
|
|
||||||
It's written in __Python 3__ using __PyQt5__ and __scipy__.
|
|
||||||
|
|
||||||
<details open="open">
|
|
||||||
<summary>Table of Contents</summary>
|
|
||||||
|
|
||||||
- [About](#nanovnasaver)
|
|
||||||
- [Built With](#built-with)
|
|
||||||
- [Introduction](#introduction)
|
|
||||||
- [Current Features](#current-features)
|
|
||||||
- [Screenshot](#screenshot)
|
|
||||||
- [Binary Releases](#binary-releases)
|
|
||||||
- [Installation](#installation)
|
|
||||||
- [Detailed Installation Instructions](docs/INSTALLATION.md)
|
|
||||||
- [Usage](#using-the-software)
|
|
||||||
- [Calibration](#calibration)
|
|
||||||
- [TDR](#tdr)
|
|
||||||
- [Measuring inductor core permeability](#measuring-inductor-core-permeability)
|
|
||||||
- [Latest Changes](#latest-changes)
|
|
||||||
- [Contributing](#contributing)
|
|
||||||
- [Contribution Guidlines](docs/CONTRIBUTING.md)
|
|
||||||
- [License](#license)
|
|
||||||
- [References](#references)
|
|
||||||
- [Acknowledgements](#acknowledgements)
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
This software connects to a NanoVNA and extracts the data for
|
|
||||||
display on a computer and allows saving the sweep data to Touchstone files.
|
|
||||||
|
|
||||||
<a href="#current-features"></a>
|
|
||||||
|
|
||||||
### Current features
|
|
||||||
|
|
||||||
- Reading data from a NanoVNA -- Compatible devices: NanoVNA, NanoVNA-H,
|
|
||||||
NanoVNA-H4, NanoVNA-F, AVNA via Teensy
|
|
||||||
- Splitting a frequency range into multiple segments to increase resolution
|
|
||||||
(tried up to >10k points)
|
|
||||||
- Averaging data for better results particularly at higher frequencies
|
|
||||||
- Displaying data on multiple chart types, such as Smith, LogMag, Phase and
|
|
||||||
VSWR-charts, for both S11 and S21
|
|
||||||
- Displaying markers, and the impedance, VSWR, Q, equivalent
|
|
||||||
capacitance/inductance etc. at these locations
|
|
||||||
- Displaying customizable frequency bands as reference, for example amateur
|
|
||||||
radio bands
|
|
||||||
- Exporting and importing 1-port and 2-port Touchstone files
|
|
||||||
- TDR function (measurement of cable length) - including impedance display
|
|
||||||
- Filter analysis functions for low-pass, high-pass, band-pass and band-stop
|
|
||||||
filters
|
|
||||||
- Display of both an active and a reference trace
|
|
||||||
- Live updates of data from the NanoVNA, including for multi-segment sweeps
|
|
||||||
- In-application calibration, including compensation for non-ideal calibration
|
|
||||||
standards
|
|
||||||
- Customizable display options, including "dark mode"
|
|
||||||
- Exporting images of plotted values
|
|
||||||
|
|
||||||
### Screenshot
|
|
||||||
|
|
||||||
![Screenshot of version 0.1.4](https://i.imgur.com/ZoFsV2V.png)
|
|
||||||
|
|
||||||
## Running the application
|
|
||||||
|
|
||||||
The software was written in Python on Windows, using Pycharm, and the modules
|
|
||||||
PyQT5, numpy, scipy and pyserial.
|
|
||||||
Main development is currently done on Linux (Mint 21 "Vanessa" Cinnamon)
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Binary releases
|
|
||||||
|
|
||||||
You can find current binary releases for Windows, Linux and MacOS under
|
|
||||||
<https://github.com/NanoVNA-Saver/nanovna-saver/releases/latest>
|
|
||||||
|
|
||||||
The 32bit Windows binaries are somewhat smaller and seems to be a
|
|
||||||
little bit more stable.
|
|
||||||
|
|
||||||
[Detailed installation instructions](docs/INSTALLATION.md)
|
|
||||||
|
|
||||||
## Using the software
|
|
||||||
|
|
||||||
Connect your NanoVNA to a serial port, and enter this serial port in the serial
|
|
||||||
port box. If the NanoVNA is connected before the application starts, it should
|
|
||||||
be automatically detected. Otherwise, click "Rescan". Click "Connect to device"
|
|
||||||
to connect.
|
|
||||||
|
|
||||||
The app can collect multiple segments to get more accurate measurements. Enter
|
|
||||||
the number of segments to be done in the "Segments" box. Each segment is 101
|
|
||||||
data points, and takes about 1.5 seconds to complete.
|
|
||||||
|
|
||||||
Frequencies are entered in Hz, or suffixed with k or M. Scientific notation
|
|
||||||
(6.5e6 for 6.5MHz) also works.
|
|
||||||
|
|
||||||
Markers can be manually entered, or controlled using the mouse. For mouse
|
|
||||||
control, select the active marker using the radio buttons, or hold "shift"
|
|
||||||
while clicking to drag the nearest marker. The marker readout boxes show the
|
|
||||||
actual frequency where values are measured. Marker readouts can be hidden
|
|
||||||
using the "hide data" button when not needed.
|
|
||||||
|
|
||||||
Display settings are available under "Display setup". These allow changing the
|
|
||||||
chart colours, the application font size and which graphs are displayed. The
|
|
||||||
settings are saved between program starts.
|
|
||||||
|
|
||||||
### Calibration
|
|
||||||
|
|
||||||
_Before using NanoVNA-Saver, please ensure that the device itself is in a
|
|
||||||
reasonable calibration state._
|
|
||||||
|
|
||||||
A calibration of both ports across the entire frequency span, saved to save
|
|
||||||
slot 0, is sufficient. If the NanoVNA is completely uncalibrated, its readings
|
|
||||||
may be outside the range accepted by the application.
|
|
||||||
|
|
||||||
In-application calibration is available, either assuming ideal standards or
|
|
||||||
with relevant standard correction. To manually calibrate, sweep each standard
|
|
||||||
in turn and press the relevant button in the calibration window.
|
|
||||||
For assisted calibration, press the "Calibration Assistant" button. If desired,
|
|
||||||
enter a note in the provided field describing the conditions under which the
|
|
||||||
calibration was performed.
|
|
||||||
|
|
||||||
Calibration results may be saved and loaded using the provided buttons at the
|
|
||||||
bottom of the window. Notes are saved and loaded along with the calibration
|
|
||||||
data.
|
|
||||||
|
|
||||||
![Screenshot of Calibration Window](https://i.imgur.com/p94cxOX.png)
|
|
||||||
|
|
||||||
Users of known characterized calibration standard sets can enter the data for
|
|
||||||
these, and save the sets.
|
|
||||||
|
|
||||||
After pressing _Apply_, the calibration is immediately applied to the latest
|
|
||||||
sweep data.
|
|
||||||
|
|
||||||
\! _Currently, load capacitance is unsupported_ \!
|
|
||||||
|
|
||||||
### TDR
|
|
||||||
|
|
||||||
To get accurate TDR measurements, calibrate the device, and attach the cable to
|
|
||||||
be measured at the calibration plane - i.e. at the same position where the
|
|
||||||
calibration load would be attached. Open the "Time Domain Reflectometry"
|
|
||||||
window, and select the correct cable type, or manually enter a propagation
|
|
||||||
factor.
|
|
||||||
|
|
||||||
### Measuring inductor core permeability
|
|
||||||
|
|
||||||
The permeability (mu) of cores can be measured using a one-port measurement. Put one or more windings on a core of known dimensions and use the "S11 mu" plot from the "Display Setup". The core dimensions (cross section area in mm2, effective length in mm) and number of windings can be set in the context menu for the plot (right click on the plot).
|
|
||||||
|
|
||||||
### Latest Changes
|
|
||||||
|
|
||||||
### Changes in 0.5.5
|
|
||||||
|
|
||||||
- Measuring inductor core permeability
|
|
||||||
- Bugfixes for calibration data loading and saving
|
|
||||||
- Let V2 Devices more time for usb-serial setup
|
|
||||||
- Make some windows scrollable
|
|
||||||
|
|
||||||
### Changes in 0.5.4
|
|
||||||
|
|
||||||
- Bugfixes for Python3.11 compatability
|
|
||||||
- Bugfix for Python3.8 compatability
|
|
||||||
- use math instead of table for log step calculation
|
|
||||||
- Support of NanoVNA V2 Plus5 on Windows
|
|
||||||
- New SI prefixes added - Ronna, Quetta
|
|
||||||
- addes a Makefile to build a packages
|
|
||||||
- Simplyfied sweep worker
|
|
||||||
- Fixed calibration data loading
|
|
||||||
- Explicit import of scipy functions - #555
|
|
||||||
- Refactoring of Analysis modules
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
First off, thanks for taking the time to contribute! Contributions are what
|
|
||||||
make the open-source community such an amazing place to learn, inspire, and
|
|
||||||
create. Any contributions you make will benefit everybody else and are
|
|
||||||
__greatly appreciated__.
|
|
||||||
|
|
||||||
Please read [our contribution guidelines](docs/CONTRIBUTING.md), and thank you
|
|
||||||
for being involved!
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This software is licensed under version 3 of the GNU General Public License. It
|
|
||||||
comes with NO WARRANTY.
|
|
||||||
|
|
||||||
You can use it, commercially as well. You may make changes to the code, but I
|
|
||||||
(and the license) ask that you give these changes back to the community.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- Ohan Smit wrote an introduction to using the application:
|
|
||||||
[https://zs1sci.com/blog/nanovnasaver/]
|
|
||||||
- HexAndFlex wrote a 3-part (thus far) series on Getting Started with the NanoVNA:
|
|
||||||
[https://hexandflex.com/2019/08/31/getting-started-with-the-nanovna-part-1/] - Part 3 is dedicated to NanoVNASaver:
|
|
||||||
[https://hexandflex.com/2019/09/15/getting-started-with-the-nanovna-part-3-pc-software/]
|
|
||||||
- Gunthard Kraus did documentation in English and German:
|
|
||||||
[http://www.gunthard-kraus.de/fertig_NanoVNA/English/]
|
|
||||||
[http://www.gunthard-kraus.de/fertig_NanoVNA/Deutsch/]
|
|
||||||
|
|
||||||
## Acknowledgements
|
|
||||||
|
|
||||||
Original application by Rune B. Broberg (5Q5R)
|
|
||||||
|
|
||||||
Contributions and changes by Holger Müller (DG5DBH), David Hunt and others.
|
|
||||||
|
|
||||||
TDR inspiration shamelessly stolen from the work of Salil (VU2CWA) at
|
|
||||||
<https://nuclearrambo.com/wordpress/accurately-measuring-cable-length-with-nanovna/>
|
|
||||||
|
|
||||||
TDR cable types by Larry Goga.
|
|
||||||
|
|
||||||
Bugfixes and Python installation work by Ohan Smit.
|
|
||||||
|
|
||||||
Thanks to everyone who have tested, commented and inspired. Particular thanks
|
|
||||||
go to the alpha testing crew who suffer the early instability of new versions.
|
|
||||||
|
|
||||||
This software is available free of charge. If you read all this way, and you
|
|
||||||
_still_ want to support it, you may donate to the developer using the button
|
|
||||||
below:
|
|
||||||
|
|
||||||
[![Paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=T8KTGVDQF5K6E&item_name=NanoVNASaver+Development¤cy_code=EUR&source=url)
|
|
|
@ -0,0 +1,271 @@
|
||||||
|
.. role:: raw-html-m2r(raw)
|
||||||
|
:format: html
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/github/v/release/NanoVNA-Saver/nanovna-saver.svg
|
||||||
|
:target: https://github.com/NanoVNA-Saver/nanovna-saver/releases/latest
|
||||||
|
:alt: Latest Release
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/github/license/NanoVNA-Saver/nanovna-saver.svg
|
||||||
|
:target: https://github.com/NanoVNA-Saver/nanovna-saver/blob/master/LICENSE.txt
|
||||||
|
:alt: License
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/github/downloads/NanoVNA-Saver/nanovna-saver/total.svg
|
||||||
|
:target: https://github.com/NanoVNA-Saver/nanovna-saver/releases/
|
||||||
|
:alt: Downloads
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/github/downloads/NanoVNA-Saver/nanovna-saver/latest/total
|
||||||
|
:target: https://github.com/NanoVNA-Saver/nanovna-saver/releases/latest
|
||||||
|
:alt: GitHub Releases
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/badge/paypal-donate-yellow.svg
|
||||||
|
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=T8KTGVDQF5K6E&item_name=NanoVNASaver+Development¤cy_code=EUR&source=url
|
||||||
|
:alt: Donate
|
||||||
|
|
||||||
|
NanoVNASaver
|
||||||
|
============
|
||||||
|
|
||||||
|
A multiplatform tool to save Touchstone files from the NanoVNA,
|
||||||
|
sweep frequency spans in segments to gain more than 101 data
|
||||||
|
points, and generally display and analyze the resulting data.
|
||||||
|
|
||||||
|
|
||||||
|
* Copyright 2019, 2020 Rune B. Broberg
|
||||||
|
* Copyright 2020ff NanoVNA-Saver Authors
|
||||||
|
|
||||||
|
It's developed in **Python 3 (>=3.8)** using **PyQt6**, **numpy** and
|
||||||
|
**scipy**.
|
||||||
|
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
------------
|
||||||
|
|
||||||
|
This software connects to a NanoVNA and extracts the data for
|
||||||
|
display on a computer and allows saving the sweep data to Touchstone files.
|
||||||
|
|
||||||
|
:raw-html-m2r:`<a href="#current-features"></a>`
|
||||||
|
|
||||||
|
Current features
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|
||||||
|
* Reading data from a NanoVNA -- Compatible devices: NanoVNA, NanoVNA-H,
|
||||||
|
NanoVNA-H4, NanoVNA-F, AVNA via Teensy
|
||||||
|
* Reading data from a TinySA
|
||||||
|
* Splitting a frequency range into multiple segments to increase resolution
|
||||||
|
(tried up to >10k points)
|
||||||
|
* Averaging data for better results particularly at higher frequencies
|
||||||
|
* Displaying data on multiple chart types, such as Smith, LogMag, Phase and
|
||||||
|
VSWR-charts, for both S11 and S21
|
||||||
|
* Displaying markers, and the impedance, VSWR, Q, equivalent
|
||||||
|
capacitance/inductance etc. at these locations
|
||||||
|
* Displaying customizable frequency bands as reference, for example amateur
|
||||||
|
radio bands
|
||||||
|
* Exporting and importing 1-port and 2-port Touchstone files
|
||||||
|
* TDR function (measurement of cable length) - including impedance display
|
||||||
|
* Filter analysis functions for low-pass, high-pass, band-pass and band-stop
|
||||||
|
filters
|
||||||
|
* Display of both an active and a reference trace
|
||||||
|
* Live updates of data from the NanoVNA, including for multi-segment sweeps
|
||||||
|
* In-application calibration, including compensation for non-ideal calibration
|
||||||
|
standards
|
||||||
|
* Customizable display options, including "dark mode"
|
||||||
|
* Exporting images of plotted values
|
||||||
|
|
||||||
|
Screenshot
|
||||||
|
^^^^^^^^^^
|
||||||
|
|
||||||
|
|
||||||
|
.. image:: https://i.imgur.com/ZoFsV2V.png
|
||||||
|
:target: https://i.imgur.com/ZoFsV2V.png
|
||||||
|
:alt: Screenshot of version 0.1.4
|
||||||
|
|
||||||
|
|
||||||
|
Running the application
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Main development is currently done on Linux (Mint 21 "Vanessa" Cinnamon)
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Binary releases
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
You can find current binary releases for Windows, Linux and MacOS under
|
||||||
|
https://github.com/NanoVNA-Saver/nanovna-saver/releases/latest
|
||||||
|
|
||||||
|
The 32bit Windows binaries are somewhat smaller and seems to be a
|
||||||
|
little bit more stable.
|
||||||
|
|
||||||
|
`Detailed installation instructions <docs/INSTALLATION.md>`_
|
||||||
|
|
||||||
|
Using the software
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Connect your NanoVNA to a serial port, and enter this serial port in the serial
|
||||||
|
port box. If the NanoVNA is connected before the application starts, it should
|
||||||
|
be automatically detected. Otherwise, click "Rescan". Click "Connect to device"
|
||||||
|
to connect.
|
||||||
|
|
||||||
|
The app can collect multiple segments to get more accurate measurements. Enter
|
||||||
|
the number of segments to be done in the "Segments" box. Each segment is 101
|
||||||
|
data points, and takes about 1.5 seconds to complete.
|
||||||
|
|
||||||
|
Frequencies are entered in Hz, or suffixed with k or M. Scientific notation
|
||||||
|
(6.5e6 for 6.5MHz) also works.
|
||||||
|
|
||||||
|
Markers can be manually entered, or controlled using the mouse. For mouse
|
||||||
|
control, select the active marker using the radio buttons, or hold "shift"
|
||||||
|
while clicking to drag the nearest marker. The marker readout boxes show the
|
||||||
|
actual frequency where values are measured. Marker readouts can be hidden
|
||||||
|
using the "hide data" button when not needed.
|
||||||
|
|
||||||
|
Display settings are available under "Display setup". These allow changing the
|
||||||
|
chart colours, the application font size and which graphs are displayed. The
|
||||||
|
settings are saved between program starts.
|
||||||
|
|
||||||
|
Calibration
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
*Before using NanoVNA-Saver, please ensure that the device itself is in a
|
||||||
|
reasonable calibration state.*
|
||||||
|
|
||||||
|
A calibration of both ports across the entire frequency span, saved to save
|
||||||
|
slot 0, is sufficient. If the NanoVNA is completely uncalibrated, its readings
|
||||||
|
may be outside the range accepted by the application.
|
||||||
|
|
||||||
|
In-application calibration is available, either assuming ideal standards or
|
||||||
|
with relevant standard correction. To manually calibrate, sweep each standard
|
||||||
|
in turn and press the relevant button in the calibration window.
|
||||||
|
For assisted calibration, press the "Calibration Assistant" button. If desired,
|
||||||
|
enter a note in the provided field describing the conditions under which the
|
||||||
|
calibration was performed.
|
||||||
|
|
||||||
|
Calibration results may be saved and loaded using the provided buttons at the
|
||||||
|
bottom of the window. Notes are saved and loaded along with the calibration
|
||||||
|
data.
|
||||||
|
|
||||||
|
|
||||||
|
.. image:: https://i.imgur.com/p94cxOX.png
|
||||||
|
:target: https://i.imgur.com/p94cxOX.png
|
||||||
|
:alt: Screenshot of Calibration Window
|
||||||
|
|
||||||
|
|
||||||
|
Users of known characterized calibration standard sets can enter the data for
|
||||||
|
these, and save the sets.
|
||||||
|
|
||||||
|
After pressing *Apply*\ , the calibration is immediately applied to the latest
|
||||||
|
sweep data.
|
||||||
|
|
||||||
|
! *Currently, load capacitance is unsupported* !
|
||||||
|
|
||||||
|
TDR
|
||||||
|
^^^
|
||||||
|
|
||||||
|
To get accurate TDR measurements, calibrate the device, and attach the cable to
|
||||||
|
be measured at the calibration plane - i.e. at the same position where the
|
||||||
|
calibration load would be attached. Open the "Time Domain Reflectometry"
|
||||||
|
window, and select the correct cable type, or manually enter a propagation
|
||||||
|
factor.
|
||||||
|
|
||||||
|
Measuring inductor core permeability
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The permeability (mu) of cores can be measured using a one-port measurement.
|
||||||
|
Put one or more windings on a core of known dimensions and use the "S11 mu"
|
||||||
|
plot from the "Display Setup". The core dimensions (cross section area in mm2,
|
||||||
|
effective length in mm) and number of windings can be set in the context menu
|
||||||
|
for the plot (right click on the plot).
|
||||||
|
|
||||||
|
Latest Changes
|
||||||
|
^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
* Using PyQt6
|
||||||
|
* Moved to PyScaffold project structure
|
||||||
|
* Fixed crash in resonance analysis
|
||||||
|
* Added TinySA readout and screenshot
|
||||||
|
|
||||||
|
|
||||||
|
Changes in 0.5.5
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
* Measuring inductor core permeability
|
||||||
|
* Bugfixes for calibration data loading and saving
|
||||||
|
* Let V2 Devices more time for usb-serial setup
|
||||||
|
* Make some windows scrollable
|
||||||
|
|
||||||
|
Changes in 0.5.4
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
* Bugfixes for Python3.11 compatability
|
||||||
|
* Bugfix for Python3.8 compatability
|
||||||
|
* use math instead of table for log step calculation
|
||||||
|
* Support of NanoVNA V2 Plus5 on Windows
|
||||||
|
* New SI prefixes added - Ronna, Quetta
|
||||||
|
* addes a Makefile to build a packages
|
||||||
|
* Simplyfied sweep worker
|
||||||
|
* Fixed calibration data loading
|
||||||
|
* Explicit import of scipy functions - #555
|
||||||
|
* Refactoring of Analysis modules
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
------------
|
||||||
|
|
||||||
|
First off, thanks for taking the time to contribute! Contributions are what
|
||||||
|
make the open-source community such an amazing place to learn, inspire, and
|
||||||
|
create. Any contributions you make will benefit everybody else and are
|
||||||
|
**greatly appreciated**.
|
||||||
|
|
||||||
|
Please read `our contribution guidelines <docs/CONTRIBUTING.md>`_\ , and thank
|
||||||
|
you for being involved!
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
This software is licensed under version 3 of the GNU General Public License. It
|
||||||
|
comes with NO WARRANTY.
|
||||||
|
|
||||||
|
You can use it, commercially as well. You may make changes to the code, but I
|
||||||
|
(and the license) ask that you give these changes back to the community.
|
||||||
|
|
||||||
|
References
|
||||||
|
----------
|
||||||
|
|
||||||
|
|
||||||
|
* Ohan Smit wrote an introduction to using the application:
|
||||||
|
[https://zs1sci.com/blog/nanovnasaver/]
|
||||||
|
* HexAndFlex wrote a 3-part (thus far) series on Getting Started with the
|
||||||
|
NanoVNA:
|
||||||
|
[https://hexandflex.com/2019/08/31/getting-started-with-the-nanovna-part-1/]
|
||||||
|
- Part 3 is dedicated to NanoVNASaver:
|
||||||
|
[https://hexandflex.com/2019/09/15/getting-started-with-the-nanovna-part-3-pc-software/]
|
||||||
|
* Gunthard Kraus did documentation in English and German:
|
||||||
|
[http://www.gunthard-kraus.de/fertig_NanoVNA/English/]
|
||||||
|
[http://www.gunthard-kraus.de/fertig_NanoVNA/Deutsch/]
|
||||||
|
|
||||||
|
Acknowledgements
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Original application by Rune B. Broberg (5Q5R)
|
||||||
|
|
||||||
|
Contributions and changes by Holger Müller (DG5DBH), David Hunt and others.
|
||||||
|
|
||||||
|
TDR inspiration shamelessly stolen from the work of Salil (VU2CWA) at
|
||||||
|
https://nuclearrambo.com/wordpress/accurately-measuring-cable-length-with-nanovna/
|
||||||
|
|
||||||
|
TDR cable types by Larry Goga.
|
||||||
|
|
||||||
|
Bugfixes and Python installation work by Ohan Smit.
|
||||||
|
|
||||||
|
Thanks to everyone who have tested, commented and inspired. Particular thanks
|
||||||
|
go to the alpha testing crew who suffer the early instability of new versions.
|
||||||
|
|
||||||
|
This software is available free of charge. If you read all this way, and you
|
||||||
|
*still* want to support it, you may donate to the developer using the button
|
||||||
|
below:
|
||||||
|
|
||||||
|
|
||||||
|
.. image:: https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif
|
||||||
|
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=T8KTGVDQF5K6E&item_name=NanoVNASaver+Development¤cy_code=EUR&source=url
|
||||||
|
:alt: Paypal
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Builds a NanoVNASaver.app on MacOS
|
||||||
|
# ensure you have pyqt >=6.4 installed (brew install pyqt)
|
||||||
|
#
|
||||||
|
export VENV_DIR=macbuildenv
|
||||||
|
|
||||||
|
# setup build venv
|
||||||
|
python3 -m venv ${VENV_DIR}
|
||||||
|
. ./${VENV_DIR}/bin/activate
|
||||||
|
|
||||||
|
# install required dependencies (pyqt libs must be installed on the system)
|
||||||
|
python3 -m pip install pip==23.0.1 setuptools==67.6.0
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install PyInstaller==5.9.0
|
||||||
|
|
||||||
|
python3 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-`uname -m`.tar.gz NanoVNASaver.app
|
||||||
|
|
||||||
|
deactivate
|
||||||
|
rm -rf ${VENV_DIR}
|
1
debug.sh
1
debug.sh
|
@ -1,2 +1,3 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
export PYTHONPATH="src"
|
||||||
exec python -m debugpy --listen 5678 --wait-for-client $@
|
exec python -m debugpy --listen 5678 --wait-for-client $@
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line, and also
|
||||||
|
# from the environment for the first two.
|
||||||
|
SPHINXOPTS ?=
|
||||||
|
SPHINXBUILD ?= sphinx-build
|
||||||
|
SOURCEDIR = .
|
||||||
|
BUILDDIR = _build
|
||||||
|
AUTODOCDIR = api
|
||||||
|
|
||||||
|
# User-friendly check for sphinx-build
|
||||||
|
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $?), 1)
|
||||||
|
$(error "The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://sphinx-doc.org/")
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: help clean Makefile
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
|
help:
|
||||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(BUILDDIR)/* $(AUTODOCDIR)
|
||||||
|
|
||||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
|
%: Makefile
|
||||||
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
@ -0,0 +1 @@
|
||||||
|
# Empty directory
|
|
@ -0,0 +1,2 @@
|
||||||
|
.. _authors:
|
||||||
|
.. include:: ../AUTHORS.rst
|
|
@ -0,0 +1,286 @@
|
||||||
|
# This file is execfile()d with the current directory set to its containing dir.
|
||||||
|
#
|
||||||
|
# This file only contains a selection of the most common options. For a full
|
||||||
|
# list see the documentation:
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||||
|
#
|
||||||
|
# All configuration values have a default; values that are commented out
|
||||||
|
# serve to show the default.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
# -- Path setup --------------------------------------------------------------
|
||||||
|
|
||||||
|
__location__ = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
sys.path.insert(0, os.path.join(__location__, "../src"))
|
||||||
|
|
||||||
|
# -- Run sphinx-apidoc -------------------------------------------------------
|
||||||
|
# This hack is necessary since RTD does not issue `sphinx-apidoc` before running
|
||||||
|
# `sphinx-build -b html . _build/html`. See Issue:
|
||||||
|
# https://github.com/readthedocs/readthedocs.org/issues/1139
|
||||||
|
# DON'T FORGET: Check the box "Install your project inside a virtualenv using
|
||||||
|
# setup.py install" in the RTD Advanced Settings.
|
||||||
|
# Additionally it helps us to avoid running apidoc manually
|
||||||
|
|
||||||
|
try: # for Sphinx >= 1.7
|
||||||
|
from sphinx.ext import apidoc
|
||||||
|
except ImportError:
|
||||||
|
from sphinx import apidoc
|
||||||
|
|
||||||
|
output_dir = os.path.join(__location__, "api")
|
||||||
|
module_dir = os.path.join(__location__, "../src/NanoVNASaver")
|
||||||
|
try:
|
||||||
|
shutil.rmtree(output_dir)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
import sphinx
|
||||||
|
|
||||||
|
cmd_line = f"sphinx-apidoc --implicit-namespaces -f -o {output_dir} {module_dir}"
|
||||||
|
|
||||||
|
args = cmd_line.split(" ")
|
||||||
|
if tuple(sphinx.__version__.split(".")) >= ("1", "7"):
|
||||||
|
# This is a rudimentary parse_version to avoid external dependencies
|
||||||
|
args = args[1:]
|
||||||
|
|
||||||
|
apidoc.main(args)
|
||||||
|
except Exception as e:
|
||||||
|
print("Running `sphinx-apidoc` failed!\n{}".format(e))
|
||||||
|
|
||||||
|
# -- General configuration ---------------------------------------------------
|
||||||
|
|
||||||
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
# needs_sphinx = '1.0'
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||||
|
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||||
|
extensions = [
|
||||||
|
"sphinx.ext.autodoc",
|
||||||
|
"sphinx.ext.intersphinx",
|
||||||
|
"sphinx.ext.todo",
|
||||||
|
"sphinx.ext.autosummary",
|
||||||
|
"sphinx.ext.viewcode",
|
||||||
|
"sphinx.ext.coverage",
|
||||||
|
"sphinx.ext.doctest",
|
||||||
|
"sphinx.ext.ifconfig",
|
||||||
|
"sphinx.ext.mathjax",
|
||||||
|
"sphinx.ext.napoleon",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ["_templates"]
|
||||||
|
|
||||||
|
# The suffix of source filenames.
|
||||||
|
source_suffix = ".rst"
|
||||||
|
|
||||||
|
# The encoding of source files.
|
||||||
|
# source_encoding = 'utf-8-sig'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = "index"
|
||||||
|
|
||||||
|
# General information about the project.
|
||||||
|
project = "nanovna-saver"
|
||||||
|
copyright = "2023, Holger Mueller"
|
||||||
|
|
||||||
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
# |version| and |release|, also used in various other places throughout the
|
||||||
|
# built documents.
|
||||||
|
#
|
||||||
|
# version: The short X.Y version.
|
||||||
|
# release: The full version, including alpha/beta/rc tags.
|
||||||
|
# If you don’t need the separation provided between version and release,
|
||||||
|
# just set them both to the same value.
|
||||||
|
try:
|
||||||
|
from NanoVNASaver import __version__ as version
|
||||||
|
except ImportError:
|
||||||
|
version = ""
|
||||||
|
|
||||||
|
if not version or version.lower() == "unknown":
|
||||||
|
version = os.getenv("READTHEDOCS_VERSION", "unknown") # automatically set by RTD
|
||||||
|
|
||||||
|
release = version
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
# language = None
|
||||||
|
|
||||||
|
# There are two options for replacing |today|: either, you set today to some
|
||||||
|
# non-false value, then it is used:
|
||||||
|
# today = ''
|
||||||
|
# Else, today_fmt is used as the format for a strftime call.
|
||||||
|
# today_fmt = '%B %d, %Y'
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", ".venv"]
|
||||||
|
|
||||||
|
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||||
|
# default_role = None
|
||||||
|
|
||||||
|
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||||
|
# add_function_parentheses = True
|
||||||
|
|
||||||
|
# If true, the current module name will be prepended to all description
|
||||||
|
# unit titles (such as .. function::).
|
||||||
|
# add_module_names = True
|
||||||
|
|
||||||
|
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||||
|
# output. They are ignored by default.
|
||||||
|
# show_authors = False
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = "sphinx"
|
||||||
|
|
||||||
|
# A list of ignored prefixes for module index sorting.
|
||||||
|
# modindex_common_prefix = []
|
||||||
|
|
||||||
|
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||||
|
# keep_warnings = False
|
||||||
|
|
||||||
|
# If this is True, todo emits a warning for each TODO entries. The default is False.
|
||||||
|
todo_emit_warnings = True
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
html_theme = "alabaster"
|
||||||
|
|
||||||
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
|
# further. For a list of options available for each theme, see the
|
||||||
|
# documentation.
|
||||||
|
html_theme_options = {
|
||||||
|
"sidebar_width": "300px",
|
||||||
|
"page_width": "1200px"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add any paths that contain custom themes here, relative to this directory.
|
||||||
|
# html_theme_path = []
|
||||||
|
|
||||||
|
# The name for this set of Sphinx documents. If None, it defaults to
|
||||||
|
# "<project> v<release> documentation".
|
||||||
|
# html_title = None
|
||||||
|
|
||||||
|
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||||
|
# html_short_title = None
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top
|
||||||
|
# of the sidebar.
|
||||||
|
# html_logo = ""
|
||||||
|
|
||||||
|
# The name of an image file (within the static path) to use as favicon of the
|
||||||
|
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||||
|
# pixels large.
|
||||||
|
# html_favicon = None
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = ["_static"]
|
||||||
|
|
||||||
|
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||||
|
# using the given strftime format.
|
||||||
|
# html_last_updated_fmt = '%b %d, %Y'
|
||||||
|
|
||||||
|
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||||
|
# typographically correct entities.
|
||||||
|
# html_use_smartypants = True
|
||||||
|
|
||||||
|
# Custom sidebar templates, maps document names to template names.
|
||||||
|
# html_sidebars = {}
|
||||||
|
|
||||||
|
# Additional templates that should be rendered to pages, maps page names to
|
||||||
|
# template names.
|
||||||
|
# html_additional_pages = {}
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
# html_domain_indices = True
|
||||||
|
|
||||||
|
# If false, no index is generated.
|
||||||
|
# html_use_index = True
|
||||||
|
|
||||||
|
# If true, the index is split into individual pages for each letter.
|
||||||
|
# html_split_index = False
|
||||||
|
|
||||||
|
# If true, links to the reST sources are added to the pages.
|
||||||
|
# html_show_sourcelink = True
|
||||||
|
|
||||||
|
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||||
|
# html_show_sphinx = True
|
||||||
|
|
||||||
|
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||||
|
# html_show_copyright = True
|
||||||
|
|
||||||
|
# If true, an OpenSearch description file will be output, and all pages will
|
||||||
|
# contain a <link> tag referring to it. The value of this option must be the
|
||||||
|
# base URL from which the finished HTML is served.
|
||||||
|
# html_use_opensearch = ''
|
||||||
|
|
||||||
|
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||||
|
# html_file_suffix = None
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = "nanovna-saver-doc"
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for LaTeX output ------------------------------------------------
|
||||||
|
|
||||||
|
latex_elements = {
|
||||||
|
# The paper size ("letterpaper" or "a4paper").
|
||||||
|
# "papersize": "letterpaper",
|
||||||
|
# The font size ("10pt", "11pt" or "12pt").
|
||||||
|
# "pointsize": "10pt",
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
# "preamble": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||||
|
latex_documents = [
|
||||||
|
("index", "user_guide.tex", "nanovna-saver Documentation", "Holger Mueller", "manual")
|
||||||
|
]
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top of
|
||||||
|
# the title page.
|
||||||
|
# latex_logo = ""
|
||||||
|
|
||||||
|
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||||
|
# not chapters.
|
||||||
|
# latex_use_parts = False
|
||||||
|
|
||||||
|
# If true, show page references after internal links.
|
||||||
|
# latex_show_pagerefs = False
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
# latex_show_urls = False
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
# latex_appendices = []
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
# latex_domain_indices = True
|
||||||
|
|
||||||
|
# -- External mapping --------------------------------------------------------
|
||||||
|
python_version = ".".join(map(str, sys.version_info[0:2]))
|
||||||
|
intersphinx_mapping = {
|
||||||
|
"sphinx": ("https://www.sphinx-doc.org/en/master", None),
|
||||||
|
"python": ("https://docs.python.org/" + python_version, None),
|
||||||
|
"matplotlib": ("https://matplotlib.org", None),
|
||||||
|
"numpy": ("https://numpy.org/doc/stable", None),
|
||||||
|
"sklearn": ("https://scikit-learn.org/stable", None),
|
||||||
|
"pandas": ("https://pandas.pydata.org/pandas-docs/stable", None),
|
||||||
|
"scipy": ("https://docs.scipy.org/doc/scipy/reference", None),
|
||||||
|
"setuptools": ("https://setuptools.pypa.io/en/stable/", None),
|
||||||
|
"pyscaffold": ("https://pyscaffold.org/en/stable", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"loading configurations for {project} {version} ...", file=sys.stderr)
|
|
@ -0,0 +1 @@
|
||||||
|
.. include:: ../CONTRIBUTING.rst
|
|
@ -0,0 +1,60 @@
|
||||||
|
=============
|
||||||
|
nanovna-saver
|
||||||
|
=============
|
||||||
|
|
||||||
|
This is the documentation of **nanovna-saver**.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This is the main page of your project's `Sphinx`_ documentation.
|
||||||
|
It is formatted in `reStructuredText`_. Add additional pages
|
||||||
|
by creating rst-files in ``docs`` and adding them to the `toctree`_ below.
|
||||||
|
Use then `references`_ in order to link them from this page, e.g.
|
||||||
|
:ref:`authors` and :ref:`changes`.
|
||||||
|
|
||||||
|
It is also possible to refer to the documentation of other Python packages
|
||||||
|
with the `Python domain syntax`_. By default you can reference the
|
||||||
|
documentation of `Sphinx`_, `Python`_, `NumPy`_, `SciPy`_, `matplotlib`_,
|
||||||
|
`Pandas`_, `Scikit-Learn`_. You can add more by extending the
|
||||||
|
``intersphinx_mapping`` in your Sphinx's ``conf.py``.
|
||||||
|
|
||||||
|
The pretty useful extension `autodoc`_ is activated by default and lets
|
||||||
|
you include documentation from docstrings. Docstrings can be written in
|
||||||
|
`Google style`_ (recommended!), `NumPy style`_ and `classical style`_.
|
||||||
|
|
||||||
|
|
||||||
|
Contents
|
||||||
|
========
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
Overview <readme>
|
||||||
|
Contributions & Help <contributing>
|
||||||
|
License <license>
|
||||||
|
Authors <authors>
|
||||||
|
Module Reference <api/modules>
|
||||||
|
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
||||||
|
|
||||||
|
.. _toctree: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html
|
||||||
|
.. _reStructuredText: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html
|
||||||
|
.. _references: https://www.sphinx-doc.org/en/stable/markup/inline.html
|
||||||
|
.. _Python domain syntax: https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#the-python-domain
|
||||||
|
.. _Sphinx: https://www.sphinx-doc.org/
|
||||||
|
.. _Python: https://docs.python.org/
|
||||||
|
.. _Numpy: https://numpy.org/doc/stable
|
||||||
|
.. _SciPy: https://docs.scipy.org/doc/scipy/reference/
|
||||||
|
.. _matplotlib: https://matplotlib.org/contents.html#
|
||||||
|
.. _Pandas: https://pandas.pydata.org/pandas-docs/stable
|
||||||
|
.. _Scikit-Learn: https://scikit-learn.org/stable
|
||||||
|
.. _autodoc: https://www.sphinx-doc.org/en/master/ext/autodoc.html
|
||||||
|
.. _Google style: https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings
|
||||||
|
.. _NumPy style: https://numpydoc.readthedocs.io/en/latest/format.html
|
||||||
|
.. _classical style: https://www.sphinx-doc.org/en/master/domains.html#info-field-lists
|
|
@ -0,0 +1,7 @@
|
||||||
|
.. _license:
|
||||||
|
|
||||||
|
=======
|
||||||
|
License
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. include:: ../LICENSE.txt
|
|
@ -0,0 +1,66 @@
|
||||||
|
.\" English manual page for nanovna-saver
|
||||||
|
.\"
|
||||||
|
.\" Copyright (C) 2023-2023 Nicolas Boulenguez <nicolas@debian.org>
|
||||||
|
.\"
|
||||||
|
.\" 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 <http://www.gnu.org/licenses/>.
|
||||||
|
.\"
|
||||||
|
.TH NANOVNASAVER 1 "2023-03-19"
|
||||||
|
.\"----------------------------------------------------------------------
|
||||||
|
.SH NAME
|
||||||
|
NANOVNASAVER \- save Touchstone files from the NanoVNA device
|
||||||
|
.\"----------------------------------------------------------------------
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B NanoVNASaver
|
||||||
|
.RB [\| \-h \|]
|
||||||
|
.RB [\| \-d \|]
|
||||||
|
.RB [\| \-D
|
||||||
|
.IR DEBUG_FILE \|]
|
||||||
|
.RB [\| \-f
|
||||||
|
.IR FILE \|]
|
||||||
|
.RB [\| \-r
|
||||||
|
.IR REF_FILE \|]
|
||||||
|
.RB [\| \-\-version \|]
|
||||||
|
.\"----------------------------------------------------------------------
|
||||||
|
.SH DESCRCIPTION
|
||||||
|
The NanoVNASaver graphical tool saves Touchstone files from the
|
||||||
|
NanoVNA, sweeps frequency spans in segments to gain more data points,
|
||||||
|
and generally displays and analyzes the resulting data.
|
||||||
|
.PP
|
||||||
|
The authors expect most users to use a graphical launcher instead of
|
||||||
|
the command line interface.
|
||||||
|
.\"----------------------------------------------------------------------
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
\fB\-h\fR, \fB\-\-help\fR
|
||||||
|
Show a summary of options and exit.
|
||||||
|
.TP
|
||||||
|
\fB\-d\fR, \fB\-\-debug\fR
|
||||||
|
Set loglevel to debug.
|
||||||
|
.TP
|
||||||
|
\fB\-D \fIDEBUG_FILE\fR, \fB\-\-debug\-file \fIDEBUG_FILE\fR
|
||||||
|
File to write debug logging output to.
|
||||||
|
.TP
|
||||||
|
\fB\-f \fIFILE\fR, \fB\-\-file \fIFILE\fR
|
||||||
|
Touchstone file to load as sweep for off device usage.
|
||||||
|
.TP
|
||||||
|
\fB\-r \fIREF_FILE\fR, \fB\-\-ref\-file \fIREF_FILE\fR
|
||||||
|
Touchstone file to load as reference for off device usage.
|
||||||
|
.TP
|
||||||
|
\fB\-\-version\fR
|
||||||
|
Show program's version number and exit.
|
||||||
|
.\"----------------------------------------------------------------------
|
||||||
|
.SH SEE ALSO
|
||||||
|
The documentation is installed at
|
||||||
|
.BR /usr/share/doc/nanovna-saver/ .
|
||||||
|
.\"----------------------------------------------------------------------
|
||||||
|
.SH HISTORY
|
||||||
|
This page has been written for Debian but may be reused by others.
|
|
@ -0,0 +1,2 @@
|
||||||
|
.. _readme:
|
||||||
|
.. include:: ../README.rst
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Requirements file for ReadTheDocs, check .readthedocs.yml.
|
||||||
|
# To build the module reference correctly, make sure every external package
|
||||||
|
# under `install_requires` in `setup.cfg` is also listed here!
|
||||||
|
sphinx>=3.2.1
|
||||||
|
# sphinx_rtd_theme
|
|
@ -1,6 +1,6 @@
|
||||||
app-id: io.github.zarath.nanovna-saver
|
app-id: io.github.zarath.nanovna-saver
|
||||||
runtime: org.kde.Platform
|
runtime: org.kde.Platform
|
||||||
runtime-version: '5.15-21.08'
|
runtime-version: '6.5'
|
||||||
sdk: org.kde.Sdk
|
sdk: org.kde.Sdk
|
||||||
command: /app/bin/NanoVNASaver
|
command: /app/bin/NanoVNASaver
|
||||||
build-options:
|
build-options:
|
||||||
|
@ -10,7 +10,7 @@ modules:
|
||||||
- name: nanonva-saver
|
- name: nanonva-saver
|
||||||
buildsystem: simple
|
buildsystem: simple
|
||||||
build-commands:
|
build-commands:
|
||||||
- pip3 install --prefix=/app wheel
|
- pip3 install --prefix=/app wheel setuptools setuptools-scm
|
||||||
- pip3 install --prefix=/app git+https://github.com/NanoVNA-Saver/nanovna-saver.git
|
- pip3 install --prefix=/app git+https://github.com/NanoVNA-Saver/nanovna-saver.git
|
||||||
finish-args:
|
finish-args:
|
||||||
# X11 + XShm access
|
# X11 + XShm access
|
||||||
|
|
Plik binarny nie jest wyświetlany.
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 109 KiB |
|
@ -16,15 +16,22 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from contextlib import suppress
|
# This launcher is ignored by setuptools. Its only purpose is direct
|
||||||
|
# execution from a source tree.
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
import os.path
|
||||||
with suppress(ImportError):
|
import sys
|
||||||
# pylint: disable=no-name-in-module,import-error,unused-import
|
|
||||||
# pyright: reportMissingImports=false
|
|
||||||
import pkg_resources.py2_warn
|
|
||||||
|
|
||||||
from NanoVNASaver.__main__ import main
|
# Ignore the current working directory.
|
||||||
|
src = os.path.join(os.path.dirname(__file__), "src")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if os.path.exists(src):
|
||||||
main()
|
sys.path.insert(0, src)
|
||||||
|
|
||||||
|
# pylint: disable-next=wrong-import-position
|
||||||
|
import NanoVNASaver.__main__
|
||||||
|
|
||||||
|
# The traditional test does not make sense here.
|
||||||
|
assert __name__ == "__main__"
|
||||||
|
|
||||||
|
NanoVNASaver.__main__.main()
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
[build-system]
|
||||||
|
# AVOID CHANGING REQUIRES: IT WILL BE UPDATED BY PYSCAFFOLD!
|
||||||
|
requires = ["setuptools>=46.1.0", "setuptools_scm[toml]>=6.2"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[NanoVNASaver]
|
||||||
|
dynamic = ["version"]
|
||||||
|
|
||||||
|
[tool.setuptools_scm]
|
||||||
|
# For smarter version schemes and other configuration options,
|
||||||
|
# check out https://github.com/pypa/setuptools_scm
|
||||||
|
root="."
|
||||||
|
version_scheme = "no-guess-dev"
|
||||||
|
write_to = "src/NanoVNASaver/_version.py"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
pythonpath = [
|
||||||
|
".", "src",
|
||||||
|
]
|
|
@ -1,5 +1,9 @@
|
||||||
pyserial==3.5
|
pyserial==3.5
|
||||||
PyQt5==5.15.9
|
PyQt6==6.5.2
|
||||||
numpy==1.24.2
|
PyQt6-sip==13.6.0
|
||||||
scipy==1.10.1
|
sip==6.8.1
|
||||||
Cython==0.29.33
|
numpy==1.26.3
|
||||||
|
scipy==1.12.0
|
||||||
|
Cython==3.0.8
|
||||||
|
setuptools==69.0.3
|
||||||
|
setuptools-scm==8.0.4
|
||||||
|
|
94
setup.cfg
94
setup.cfg
|
@ -1,3 +1,8 @@
|
||||||
|
# This file is used to configure your project.
|
||||||
|
# Read more about the various options under:
|
||||||
|
# https://setuptools.pypa.io/en/latest/userguide/declarative_config.html
|
||||||
|
# https://setuptools.pypa.io/en/latest/references/keywords.html
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
name = NanoVNASaver
|
name = NanoVNASaver
|
||||||
author = Rune B. Broberg
|
author = Rune B. Broberg
|
||||||
|
@ -5,26 +10,95 @@ author_email= NanoVNA-Saver@users.noreply.github.com
|
||||||
license = GNU GPL V3
|
license = GNU GPL V3
|
||||||
license_files = LICENSE,
|
license_files = LICENSE,
|
||||||
description = GUI for the NanoVNA and derivates
|
description = GUI for the NanoVNA and derivates
|
||||||
long_description = file: README.md
|
long_description = file: README.rst
|
||||||
url = https://github.com/NanoVNA-Saver/nanovna-saver
|
url = https://github.com/NanoVNA-Saver/nanovna-saver
|
||||||
version = attr: NanoVNASaver.About.VERSION
|
version = attr: NanoVNASaver.About.version
|
||||||
platforms= all
|
platforms= all
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
# do not use "find_namespace:" because this may recursively include "build"
|
zip_safe = False
|
||||||
packages = find:
|
packages = find_namespace:
|
||||||
install_requires=
|
include_package_data = True
|
||||||
|
package_dir =
|
||||||
|
=src
|
||||||
|
|
||||||
|
# Require a min/specific Python version (comma-separated conditions)
|
||||||
|
python_requires = >=3.8, <4
|
||||||
|
|
||||||
|
# Add here dependencies of your project (line-separated), e.g. requests>=2.2,<3.0.
|
||||||
|
# Version specifiers like >=2.2,<3.0 avoid problems due to API changes in
|
||||||
|
# new major versions. This works if the required packages follow Semantic Versioning.
|
||||||
|
# For more information, check out https://semver.org/.
|
||||||
|
install_requires =
|
||||||
pyserial>=3.5
|
pyserial>=3.5
|
||||||
PyQt5>=5.15.0
|
PyQt6>=5.15.0
|
||||||
numpy>=1.21.1
|
numpy>=1.21.1
|
||||||
scipy>=1.7.1
|
scipy>=1.7.1
|
||||||
Cython>=0.29.24
|
Cython>=0.29.24
|
||||||
python_requires = >=3.8, <4
|
setuptools-scm
|
||||||
|
|
||||||
|
[options.packages.find]
|
||||||
|
where = src
|
||||||
|
exclude =
|
||||||
|
tests
|
||||||
|
|
||||||
|
[options.extras_require]
|
||||||
|
# Add here additional requirements for extra features, to install with:
|
||||||
|
# `pip install nanovna-saver[PDF]` like:
|
||||||
|
# PDF = ReportLab; RXP
|
||||||
|
|
||||||
|
# Add here test requirements (semicolon/line-separated)
|
||||||
|
testing =
|
||||||
|
setuptools
|
||||||
|
pytest
|
||||||
|
pytest-cov
|
||||||
|
|
||||||
[options.entry_points]
|
[options.entry_points]
|
||||||
console_scripts =
|
console_scripts =
|
||||||
NanoVNASaver = NanoVNASaver.__main__:main
|
NanoVNASaver = NanoVNASaver.__main__:main
|
||||||
|
|
||||||
# without this option the rpm-build includes also the "test" directory
|
[tool:pytest]
|
||||||
[options.packages.find]
|
# Specify command line options as you would do when invoking pytest directly.
|
||||||
exclude = test
|
# e.g. --cov-report html (or xml) for html/xml output or --junitxml junit.xml
|
||||||
|
# in order to write a coverage file that can be read by Jenkins.
|
||||||
|
# CAUTION: --cov flags may prohibit setting breakpoints while debugging.
|
||||||
|
# Comment those flags to avoid this pytest issue.
|
||||||
|
addopts =
|
||||||
|
--cov NanoVNASaver --cov-report term-missing
|
||||||
|
--verbose
|
||||||
|
norecursedirs =
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
.tox
|
||||||
|
testpaths = tests
|
||||||
|
# Use pytest markers to select/deselect specific tests
|
||||||
|
# markers =
|
||||||
|
# slow: mark tests as slow (deselect with '-m "not slow"')
|
||||||
|
# system: mark end-to-end system tests
|
||||||
|
|
||||||
|
[devpi:upload]
|
||||||
|
# Options for the devpi: PyPI server and packaging tool
|
||||||
|
# VCS export must be deactivated since we are using setuptools-scm
|
||||||
|
no_vcs = 1
|
||||||
|
formats = bdist_wheel
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
# Some sane defaults for the code style checker flake8
|
||||||
|
max_line_length = 88
|
||||||
|
extend_ignore = E203, W503
|
||||||
|
# ^ Black-compatible
|
||||||
|
# E203 and W503 have edge cases handled by black
|
||||||
|
exclude =
|
||||||
|
.tox
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
.eggs
|
||||||
|
docs/conf.py
|
||||||
|
|
||||||
|
[pyscaffold]
|
||||||
|
# PyScaffold's parameters when the project was created.
|
||||||
|
# This will be used when updating. Do not change!
|
||||||
|
version = 4.4
|
||||||
|
package = NanoVNASaver
|
||||||
|
extensions =
|
||||||
|
no_skeleton
|
||||||
|
|
44
setup.py
44
setup.py
|
@ -1,27 +1,21 @@
|
||||||
# NanoVNASaver
|
"""
|
||||||
#
|
Setup file for nanovna-saver.
|
||||||
# A python program to view and export Touchstone data from a NanoVNA
|
Use setup.cfg to configure your project.
|
||||||
# Copyright (C) 2019, 2020 Rune B. Broberg
|
|
||||||
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
|
This file was generated with PyScaffold 4.4.
|
||||||
#
|
PyScaffold helps you to put up the scaffold of your new Python project.
|
||||||
# This program is free software: you can redistribute it and/or modify
|
Learn more under: https://pyscaffold.org/
|
||||||
# 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/>.
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
setup(
|
if __name__ == "__main__":
|
||||||
data_files=[
|
try:
|
||||||
("share/doc/nanovnasaver/", ["LICENSE", "README.md", ]),
|
setup(use_scm_version={"version_scheme": "no-guess-dev"})
|
||||||
("share/applications/", ["NanoVNASaver.desktop", ]),
|
except: # noqa
|
||||||
("share/icons/hicolor/48x48/apps/", ["NanoVNASaver_48x48.png", ]),
|
print(
|
||||||
]
|
"\n\nAn error occurred while building the project, "
|
||||||
)
|
"please ensure you have the most updated version of setuptools, "
|
||||||
|
"setuptools_scm and wheel with:\n"
|
||||||
|
" pip install -U setuptools setuptools_scm wheel\n\n"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
|
@ -17,13 +17,14 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
VERSION = "0.5.5"
|
from setuptools_scm import get_version
|
||||||
VERSION_URL = (
|
try:
|
||||||
"https://raw.githubusercontent.com/"
|
version = get_version(root='..', relative_to=__file__)
|
||||||
"NanoVNA-Saver/nanovna-saver/master/NanoVNASaver/About.py")
|
except LookupError:
|
||||||
|
from NanoVNASaver._version import version
|
||||||
|
|
||||||
INFO_URL = "https://github.com/NanoVNA-Saver/nanovna-saver"
|
INFO_URL = "https://github.com/NanoVNA-Saver/nanovna-saver"
|
||||||
INFO = f"""NanoVNASaver {VERSION}
|
INFO = f"""NanoVNASaver {version}
|
||||||
|
|
||||||
Copyright (C) 2019, 2020 Rune B. Broberg
|
Copyright (C) 2019, 2020 Rune B. Broberg
|
||||||
Copyright (C) 2020ff NanoVNA-Saver Authors
|
Copyright (C) 2020ff NanoVNA-Saver Authors
|
||||||
|
@ -34,4 +35,7 @@ This program is licensed under the GNU General Public License version 3
|
||||||
See {INFO_URL} for further details.
|
See {INFO_URL} for further details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RELEASE_URL = "https://github.com/NanoVNA-Saver/nanovna-saver"
|
TAGS_URL = "https://github.com/NanoVNA-Saver/nanovna-saver/tags"
|
||||||
|
TAGS_KEY = "/NanoVNA-Saver/nanovna-saver/releases/tag/v"
|
||||||
|
|
||||||
|
LATEST_URL = "https://github.com/NanoVNA-Saver/nanovna-saver/releases/latest"
|
|
@ -21,7 +21,7 @@
|
||||||
import logging
|
import logging
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from PyQt5 import QtWidgets
|
from PyQt6 import QtWidgets
|
||||||
|
|
||||||
from NanoVNASaver.Analysis.VSWRAnalysis import VSWRAnalysis
|
from NanoVNASaver.Analysis.VSWRAnalysis import VSWRAnalysis
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ class MagLoopAnalysis(VSWRAnalysis):
|
||||||
Useful for tuning magloop.
|
Useful for tuning magloop.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
max_dips_shown = 1
|
max_dips_shown = 1
|
||||||
|
|
||||||
vswr_bandwith_value = 2.56 # -3 dB ?!?
|
vswr_bandwith_value = 2.56 # -3 dB ?!?
|
||||||
|
@ -56,12 +57,17 @@ class MagLoopAnalysis(VSWRAnalysis):
|
||||||
if self.min_freq is None:
|
if self.min_freq is None:
|
||||||
self.min_freq = new_start
|
self.min_freq = new_start
|
||||||
self.max_freq = new_end
|
self.max_freq = new_end
|
||||||
logger.debug("setting hard limits to %s - %s",
|
logger.debug(
|
||||||
self.min_freq, self.max_freq)
|
"setting hard limits to %s - %s", self.min_freq, self.max_freq
|
||||||
|
)
|
||||||
|
|
||||||
if len(self.minimums) > 1:
|
if len(self.minimums) > 1:
|
||||||
self.layout.addRow("", QtWidgets.QLabel(
|
self.layout.addRow(
|
||||||
"Multiple minimums, not magloop or try to lower VSWR limit"))
|
"",
|
||||||
|
QtWidgets.QLabel(
|
||||||
|
"Multiple minimums, not magloop or try to lower VSWR limit"
|
||||||
|
),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if len(self.minimums) == 1:
|
if len(self.minimums) == 1:
|
||||||
|
@ -73,22 +79,25 @@ class MagLoopAnalysis(VSWRAnalysis):
|
||||||
logger.debug(" Zoom to %s-%s", new_start, new_end)
|
logger.debug(" Zoom to %s-%s", new_start, new_end)
|
||||||
|
|
||||||
elif self.vswr_limit_value == self.vswr_bandwith_value:
|
elif self.vswr_limit_value == self.vswr_bandwith_value:
|
||||||
Q = self.app.data.s11[lowest].freq / \
|
Q = self.app.data.s11[lowest].freq / (
|
||||||
(self.app.data.s11[end].freq -
|
self.app.data.s11[end].freq - self.app.data.s11[start].freq
|
||||||
self.app.data.s11[start].freq)
|
)
|
||||||
self.layout.addRow("Q", QtWidgets.QLabel(f"{int(Q)}"))
|
self.layout.addRow("Q", QtWidgets.QLabel(f"{int(Q)}"))
|
||||||
new_start = self.app.data.s11[start].freq - self.bandwith
|
new_start = self.app.data.s11[start].freq - self.bandwith
|
||||||
new_end = self.app.data.s11[end].freq + self.bandwith
|
new_end = self.app.data.s11[end].freq + self.bandwith
|
||||||
logger.debug("Single Spot, new scan on %s-%s",
|
logger.debug(
|
||||||
new_start, new_end)
|
"Single Spot, new scan on %s-%s", new_start, new_end
|
||||||
|
)
|
||||||
|
|
||||||
if self.vswr_limit_value > self.vswr_bandwith_value:
|
if self.vswr_limit_value > self.vswr_bandwith_value:
|
||||||
self.vswr_limit_value = max(
|
self.vswr_limit_value = max(
|
||||||
self.vswr_bandwith_value, self.vswr_limit_value - 1)
|
self.vswr_bandwith_value, self.vswr_limit_value - 1
|
||||||
|
)
|
||||||
self.input_vswr_limit.setValue(self.vswr_limit_value)
|
self.input_vswr_limit.setValue(self.vswr_limit_value)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"found higher minimum, lowering vswr search to %s",
|
"found higher minimum, lowering vswr search to %s",
|
||||||
self.vswr_limit_value)
|
self.vswr_limit_value,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
new_start = new_start - 5 * self.bandwith
|
new_start = new_start - 5 * self.bandwith
|
||||||
new_end = new_end + 5 * self.bandwith
|
new_end = new_end + 5 * self.bandwith
|
||||||
|
@ -100,14 +109,17 @@ class MagLoopAnalysis(VSWRAnalysis):
|
||||||
self.input_vswr_limit.setValue(self.vswr_limit_value)
|
self.input_vswr_limit.setValue(self.vswr_limit_value)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"no minimum found, looking for higher value %s",
|
"no minimum found, looking for higher value %s",
|
||||||
self.vswr_limit_value)
|
self.vswr_limit_value,
|
||||||
|
)
|
||||||
|
|
||||||
new_start = max(self.min_freq, new_start)
|
new_start = max(self.min_freq, new_start)
|
||||||
new_end = min(self.max_freq, new_end)
|
new_end = min(self.max_freq, new_end)
|
||||||
logger.debug("next search will be %s - %s for vswr %s",
|
logger.debug(
|
||||||
new_start,
|
"next search will be %s - %s for vswr %s",
|
||||||
new_end,
|
new_start,
|
||||||
self.vswr_limit_value)
|
new_end,
|
||||||
|
self.vswr_limit_value,
|
||||||
|
)
|
||||||
|
|
||||||
self.app.sweep_control.set_start(new_start)
|
self.app.sweep_control.set_start(new_start)
|
||||||
self.app.sweep_control.set_end(new_end)
|
self.app.sweep_control.set_end(new_end)
|
|
@ -18,9 +18,8 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
from typing import Dict, List
|
|
||||||
|
|
||||||
from PyQt5 import QtWidgets
|
from PyQt6 import QtWidgets
|
||||||
|
|
||||||
import NanoVNASaver.AnalyticTools as at
|
import NanoVNASaver.AnalyticTools as at
|
||||||
from NanoVNASaver.Analysis.Base import Analysis, CUTOFF_VALS
|
from NanoVNASaver.Analysis.Base import Analysis, CUTOFF_VALS
|
||||||
|
@ -33,42 +32,52 @@ class BandPassAnalysis(Analysis):
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
super().__init__(app)
|
super().__init__(app)
|
||||||
|
|
||||||
for label in ('octave_l', 'octave_r', 'decade_l', 'decade_r',
|
for label in (
|
||||||
'freq_center', 'span_3.0dB', 'span_6.0dB', 'q_factor'):
|
"octave_l",
|
||||||
|
"octave_r",
|
||||||
|
"decade_l",
|
||||||
|
"decade_r",
|
||||||
|
"freq_center",
|
||||||
|
"span_3.0dB",
|
||||||
|
"span_6.0dB",
|
||||||
|
"q_factor",
|
||||||
|
):
|
||||||
self.label[label] = QtWidgets.QLabel()
|
self.label[label] = QtWidgets.QLabel()
|
||||||
for attn in CUTOFF_VALS:
|
for attn in CUTOFF_VALS:
|
||||||
self.label[f"{attn:.1f}dB_l"] = QtWidgets.QLabel()
|
self.label[f"{attn:.1f}dB_l"] = QtWidgets.QLabel()
|
||||||
self.label[f"{attn:.1f}dB_r"] = QtWidgets.QLabel()
|
self.label[f"{attn:.1f}dB_r"] = QtWidgets.QLabel()
|
||||||
|
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
layout.addRow(self.label['titel'])
|
layout.addRow(self.label["titel"])
|
||||||
layout.addRow(
|
layout.addRow(
|
||||||
QtWidgets.QLabel(
|
QtWidgets.QLabel(
|
||||||
f"Please place {self.app.markers[0].name}"
|
f"Please place {self.app.markers[0].name}"
|
||||||
f" in the filter passband."))
|
f" in the filter passband."
|
||||||
layout.addRow("Result:", self.label['result'])
|
)
|
||||||
|
)
|
||||||
|
layout.addRow("Result:", self.label["result"])
|
||||||
layout.addRow(QtWidgets.QLabel(""))
|
layout.addRow(QtWidgets.QLabel(""))
|
||||||
|
|
||||||
layout.addRow("Center frequency:", self.label['freq_center'])
|
layout.addRow("Center frequency:", self.label["freq_center"])
|
||||||
layout.addRow("Bandwidth (-3 dB):", self.label['span_3.0dB'])
|
layout.addRow("Bandwidth (-3 dB):", self.label["span_3.0dB"])
|
||||||
layout.addRow("Quality factor:", self.label['q_factor'])
|
layout.addRow("Quality factor:", self.label["q_factor"])
|
||||||
layout.addRow("Bandwidth (-6 dB):", self.label['span_6.0dB'])
|
layout.addRow("Bandwidth (-6 dB):", self.label["span_6.0dB"])
|
||||||
layout.addRow(QtWidgets.QLabel(""))
|
layout.addRow(QtWidgets.QLabel(""))
|
||||||
|
|
||||||
layout.addRow(QtWidgets.QLabel("Lower side:"))
|
layout.addRow(QtWidgets.QLabel("Lower side:"))
|
||||||
layout.addRow("Cutoff frequency:", self.label['3.0dB_l'])
|
layout.addRow("Cutoff frequency:", self.label["3.0dB_l"])
|
||||||
layout.addRow("-6 dB point:", self.label['6.0dB_l'])
|
layout.addRow("-6 dB point:", self.label["6.0dB_l"])
|
||||||
layout.addRow("-60 dB point:", self.label['60.0dB_l'])
|
layout.addRow("-60 dB point:", self.label["60.0dB_l"])
|
||||||
layout.addRow("Roll-off:", self.label['octave_l'])
|
layout.addRow("Roll-off:", self.label["octave_l"])
|
||||||
layout.addRow("Roll-off:", self.label['decade_l'])
|
layout.addRow("Roll-off:", self.label["decade_l"])
|
||||||
layout.addRow(QtWidgets.QLabel(""))
|
layout.addRow(QtWidgets.QLabel(""))
|
||||||
|
|
||||||
layout.addRow(QtWidgets.QLabel("Upper side:"))
|
layout.addRow(QtWidgets.QLabel("Upper side:"))
|
||||||
layout.addRow("Cutoff frequency:", self.label['3.0dB_r'])
|
layout.addRow("Cutoff frequency:", self.label["3.0dB_r"])
|
||||||
layout.addRow("-6 dB point:", self.label['6.0dB_r'])
|
layout.addRow("-6 dB point:", self.label["6.0dB_r"])
|
||||||
layout.addRow("-60 dB point:", self.label['60.0dB_r'])
|
layout.addRow("-60 dB point:", self.label["60.0dB_r"])
|
||||||
layout.addRow("Roll-off:", self.label['octave_r'])
|
layout.addRow("Roll-off:", self.label["octave_r"])
|
||||||
layout.addRow("Roll-off:", self.label['decade_r'])
|
layout.addRow("Roll-off:", self.label["decade_r"])
|
||||||
|
|
||||||
self.set_titel("Band pass filter analysis")
|
self.set_titel("Band pass filter analysis")
|
||||||
|
|
||||||
|
@ -103,72 +112,90 @@ class BandPassAnalysis(Analysis):
|
||||||
self.derive_60dB(cutoff_pos, cutoff_freq)
|
self.derive_60dB(cutoff_pos, cutoff_freq)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'span_3.0dB': cutoff_freq['3.0dB_r'] - cutoff_freq['3.0dB_l'],
|
"span_3.0dB": cutoff_freq["3.0dB_r"] - cutoff_freq["3.0dB_l"],
|
||||||
'span_6.0dB': cutoff_freq['6.0dB_r'] - cutoff_freq['6.0dB_l'],
|
"span_6.0dB": cutoff_freq["6.0dB_r"] - cutoff_freq["6.0dB_l"],
|
||||||
'freq_center':
|
"freq_center": math.sqrt(
|
||||||
math.sqrt(cutoff_freq['3.0dB_l'] * cutoff_freq['3.0dB_r']),
|
cutoff_freq["3.0dB_l"] * cutoff_freq["3.0dB_r"]
|
||||||
|
),
|
||||||
}
|
}
|
||||||
result['q_factor'] = result['freq_center'] / result['span_3.0dB']
|
result["q_factor"] = result["freq_center"] / result["span_3.0dB"]
|
||||||
|
|
||||||
result['octave_l'], result['decade_l'] = at.calculate_rolloff(
|
result["octave_l"], result["decade_l"] = at.calculate_rolloff(
|
||||||
s21, cutoff_pos["10.0dB_l"], cutoff_pos["20.0dB_l"])
|
s21, cutoff_pos["10.0dB_l"], cutoff_pos["20.0dB_l"]
|
||||||
result['octave_r'], result['decade_r'] = at.calculate_rolloff(
|
)
|
||||||
s21, cutoff_pos["10.0dB_r"], cutoff_pos["20.0dB_r"])
|
result["octave_r"], result["decade_r"] = at.calculate_rolloff(
|
||||||
|
s21, cutoff_pos["10.0dB_r"], cutoff_pos["20.0dB_r"]
|
||||||
|
)
|
||||||
|
|
||||||
for label, val in cutoff_freq.items():
|
for label, val in cutoff_freq.items():
|
||||||
self.label[label].setText(
|
self.label[label].setText(
|
||||||
f"{format_frequency(val)}"
|
f"{format_frequency(val)}" f" ({cutoff_gain[label]:.1f} dB)"
|
||||||
f" ({cutoff_gain[label]:.1f} dB)")
|
)
|
||||||
for label in ('freq_center', 'span_3.0dB', 'span_6.0dB'):
|
for label in ("freq_center", "span_3.0dB", "span_6.0dB"):
|
||||||
self.label[label].setText(format_frequency(result[label]))
|
self.label[label].setText(format_frequency(result[label]))
|
||||||
self.label['q_factor'].setText(f"{result['q_factor']:.2f}")
|
self.label["q_factor"].setText(f"{result['q_factor']:.2f}")
|
||||||
|
|
||||||
for label in ('octave_l', 'decade_l', 'octave_r', 'decade_r'):
|
for label in ("octave_l", "decade_l", "octave_r", "decade_r"):
|
||||||
self.label[label].setText(f"{result[label]:.3f}dB/{label[:-2]}")
|
self.label[label].setText(f"{result[label]:.3f}dB/{label[:-2]}")
|
||||||
|
|
||||||
self.app.markers[0].setFrequency(f"{result['freq_center']}")
|
self.app.markers[0].setFrequency(f"{result['freq_center']}")
|
||||||
self.app.markers[1].setFrequency(f"{cutoff_freq['3.0dB_l']}")
|
self.app.markers[1].setFrequency(f"{cutoff_freq['3.0dB_l']}")
|
||||||
self.app.markers[2].setFrequency(f"{cutoff_freq['3.0dB_r']}")
|
self.app.markers[2].setFrequency(f"{cutoff_freq['3.0dB_r']}")
|
||||||
|
|
||||||
if cutoff_gain['3.0dB_l'] < -4 or cutoff_gain['3.0dB_r'] < -4:
|
if cutoff_gain["3.0dB_l"] < -4 or cutoff_gain["3.0dB_r"] < -4:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Data points insufficient for true -3 dB points."
|
"Data points insufficient for true -3 dB points."
|
||||||
"Cutoff gains: %fdB, %fdB", cutoff_gain['3.0dB_l'],
|
"Cutoff gains: %fdB, %fdB",
|
||||||
cutoff_gain['3.0dB_r'])
|
cutoff_gain["3.0dB_l"],
|
||||||
|
cutoff_gain["3.0dB_r"],
|
||||||
|
)
|
||||||
self.set_result(
|
self.set_result(
|
||||||
f"Analysis complete ({len(s21)} points)\n"
|
f"Analysis complete ({len(s21)} points)\n"
|
||||||
f"Insufficient data for analysis. Increase segment count.")
|
f"Insufficient data for analysis. Increase segment count."
|
||||||
|
)
|
||||||
return
|
return
|
||||||
self.set_result(f"Analysis complete ({len(s21)} points)")
|
self.set_result(f"Analysis complete ({len(s21)} points)")
|
||||||
|
|
||||||
def derive_60dB(self,
|
def derive_60dB(
|
||||||
cutoff_pos: Dict[str, int],
|
self, cutoff_pos: dict[str, int], cutoff_freq: dict[str, float]
|
||||||
cutoff_freq: Dict[str, float]):
|
):
|
||||||
"""derive 60dB cutoff if needed an possible
|
"""derive 60dB cutoff if needed an possible
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cutoff_pos (Dict[str, int])
|
cutoff_pos (dict[str, int])
|
||||||
cutoff_freq (Dict[str, float])
|
cutoff_freq (dict[str, float])
|
||||||
"""
|
"""
|
||||||
if (math.isnan(cutoff_freq['60.0dB_l']) and
|
if (
|
||||||
cutoff_pos['20.0dB_l'] != -1 and cutoff_pos['10.0dB_l'] != -1):
|
math.isnan(cutoff_freq["60.0dB_l"])
|
||||||
cutoff_freq['60.0dB_l'] = (
|
and cutoff_pos["20.0dB_l"] != -1
|
||||||
cutoff_freq["10.0dB_l"] *
|
and cutoff_pos["10.0dB_l"] != -1
|
||||||
10 ** (5 * (math.log10(cutoff_pos['20.0dB_l']) -
|
):
|
||||||
math.log10(cutoff_pos['10.0dB_l']))))
|
cutoff_freq["60.0dB_l"] = cutoff_freq["10.0dB_l"] * 10 ** (
|
||||||
if (math.isnan(cutoff_freq['60.0dB_r']) and
|
5
|
||||||
cutoff_pos['20.0dB_r'] != -1 and cutoff_pos['10.0dB_r'] != -1):
|
* (
|
||||||
cutoff_freq['60.0dB_r'] = (
|
math.log10(cutoff_pos["20.0dB_l"])
|
||||||
cutoff_freq["10.0dB_r"] *
|
- math.log10(cutoff_pos["10.0dB_l"])
|
||||||
10 ** (5 * (math.log10(cutoff_pos['20.0dB_r']) -
|
)
|
||||||
math.log10(cutoff_pos['10.0dB_r'])
|
)
|
||||||
)))
|
if (
|
||||||
|
math.isnan(cutoff_freq["60.0dB_r"])
|
||||||
|
and cutoff_pos["20.0dB_r"] != -1
|
||||||
|
and cutoff_pos["10.0dB_r"] != -1
|
||||||
|
):
|
||||||
|
cutoff_freq["60.0dB_r"] = cutoff_freq["10.0dB_r"] * 10 ** (
|
||||||
|
5
|
||||||
|
* (
|
||||||
|
math.log10(cutoff_pos["20.0dB_r"])
|
||||||
|
- math.log10(cutoff_pos["10.0dB_r"])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def find_center(self, gains: List[float]) -> int:
|
def find_center(self, gains: list[float]) -> int:
|
||||||
marker = self.app.markers[0]
|
marker = self.app.markers[0]
|
||||||
if marker.location <= 0 or marker.location >= len(gains) - 1:
|
if marker.location <= 0 or marker.location >= len(gains) - 1:
|
||||||
logger.debug("No valid location for %s (%s)",
|
logger.debug(
|
||||||
marker.name, marker.location)
|
"No valid location for %s (%s)", marker.name, marker.location
|
||||||
|
)
|
||||||
self.set_result(f"Please place {marker.name} in the passband.")
|
self.set_result(f"Please place {marker.name} in the passband.")
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
|
@ -178,13 +205,15 @@ class BandPassAnalysis(Analysis):
|
||||||
return -1
|
return -1
|
||||||
return peak
|
return peak
|
||||||
|
|
||||||
def find_bounderies(self,
|
def find_bounderies(
|
||||||
gains: List[float],
|
self, gains: list[float], peak: int, peak_db: float
|
||||||
peak: int, peak_db: float) -> Dict[str, int]:
|
) -> dict[str, int]:
|
||||||
cutoff_pos = {}
|
cutoff_pos = {}
|
||||||
for attn in CUTOFF_VALS:
|
for attn in CUTOFF_VALS:
|
||||||
cutoff_pos[f"{attn:.1f}dB_l"] = at.cut_off_left(
|
cutoff_pos[f"{attn:.1f}dB_l"] = at.cut_off_left(
|
||||||
gains, peak, peak_db, attn)
|
gains, peak, peak_db, attn
|
||||||
|
)
|
||||||
cutoff_pos[f"{attn:.1f}dB_r"] = at.cut_off_right(
|
cutoff_pos[f"{attn:.1f}dB_r"] = at.cut_off_right(
|
||||||
gains, peak, peak_db, attn)
|
gains, peak, peak_db, attn
|
||||||
|
)
|
||||||
return cutoff_pos
|
return cutoff_pos
|
|
@ -17,7 +17,6 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List
|
|
||||||
|
|
||||||
import NanoVNASaver.AnalyticTools as at
|
import NanoVNASaver.AnalyticTools as at
|
||||||
from NanoVNASaver.Analysis.Base import CUTOFF_VALS
|
from NanoVNASaver.Analysis.Base import CUTOFF_VALS
|
||||||
|
@ -31,14 +30,16 @@ class BandStopAnalysis(BandPassAnalysis):
|
||||||
super().__init__(app)
|
super().__init__(app)
|
||||||
self.set_titel("Band stop filter analysis")
|
self.set_titel("Band stop filter analysis")
|
||||||
|
|
||||||
def find_center(self, gains: List[float]) -> int:
|
def find_center(self, gains: list[float]) -> int:
|
||||||
return max(enumerate(gains), key=lambda i: i[1])[0]
|
return max(enumerate(gains), key=lambda i: i[1])[0]
|
||||||
|
|
||||||
def find_bounderies(self,
|
def find_bounderies(
|
||||||
gains: List[float],
|
self, gains: list[float], _: int, peak_db: float
|
||||||
_: int, peak_db: float) -> Dict[str, int]:
|
) -> dict[str, int]:
|
||||||
cutoff_pos = {}
|
cutoff_pos = {}
|
||||||
for attn in CUTOFF_VALS:
|
for attn in CUTOFF_VALS:
|
||||||
cutoff_pos[f"{attn:.1f}dB_l"], cutoff_pos[f"{attn:.1f}dB_r"] = (
|
(
|
||||||
at.dip_cut_offs(gains, peak_db, attn))
|
cutoff_pos[f"{attn:.1f}dB_l"],
|
||||||
|
cutoff_pos[f"{attn:.1f}dB_r"],
|
||||||
|
) = at.dip_cut_offs(gains, peak_db, attn)
|
||||||
return cutoff_pos
|
return cutoff_pos
|
|
@ -17,8 +17,7 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict
|
from PyQt6 import QtWidgets
|
||||||
from PyQt5 import QtWidgets
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -28,15 +27,15 @@ CUTOFF_VALS = (3.0, 6.0, 10.0, 20.0, 60.0)
|
||||||
class QHLine(QtWidgets.QFrame):
|
class QHLine(QtWidgets.QFrame):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.setFrameShape(QtWidgets.QFrame.HLine)
|
self.setFrameShape(QtWidgets.QFrame.Shape.HLine)
|
||||||
|
|
||||||
|
|
||||||
class Analysis:
|
class Analysis:
|
||||||
def __init__(self, app: QtWidgets.QWidget):
|
def __init__(self, app: QtWidgets.QWidget):
|
||||||
self.app = app
|
self.app = app
|
||||||
self.label: Dict[str, QtWidgets.QLabel] = {
|
self.label: dict[str, QtWidgets.QLabel] = {
|
||||||
'titel': QtWidgets.QLabel(),
|
"titel": QtWidgets.QLabel(),
|
||||||
'result': QtWidgets.QLabel(),
|
"result": QtWidgets.QLabel(),
|
||||||
}
|
}
|
||||||
self.layout = QtWidgets.QFormLayout()
|
self.layout = QtWidgets.QFormLayout()
|
||||||
self._widget = QtWidgets.QWidget()
|
self._widget = QtWidgets.QWidget()
|
||||||
|
@ -53,7 +52,7 @@ class Analysis:
|
||||||
label.clear()
|
label.clear()
|
||||||
|
|
||||||
def set_result(self, text):
|
def set_result(self, text):
|
||||||
self.label['result'].setText(text)
|
self.label["result"].setText(text)
|
||||||
|
|
||||||
def set_titel(self, text):
|
def set_titel(self, text):
|
||||||
self.label['titel'].setText(text)
|
self.label["titel"].setText(text)
|
|
@ -19,14 +19,18 @@
|
||||||
import csv
|
import csv
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PyQt5 import QtWidgets
|
from PyQt6 import QtWidgets
|
||||||
|
|
||||||
import NanoVNASaver.AnalyticTools as at
|
import NanoVNASaver.AnalyticTools as at
|
||||||
from NanoVNASaver.Analysis.ResonanceAnalysis import (
|
from NanoVNASaver.Analysis.ResonanceAnalysis import (
|
||||||
ResonanceAnalysis, format_resistence_neg
|
ResonanceAnalysis,
|
||||||
|
format_resistence_neg,
|
||||||
)
|
)
|
||||||
from NanoVNASaver.Formatting import (
|
from NanoVNASaver.Formatting import (
|
||||||
format_frequency, format_complex_imp, format_frequency_short)
|
format_frequency,
|
||||||
|
format_complex_imp,
|
||||||
|
format_frequency_short,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -43,11 +47,11 @@ class EFHWAnalysis(ResonanceAnalysis):
|
||||||
def do_resonance_analysis(self):
|
def do_resonance_analysis(self):
|
||||||
s11 = self.app.data.s11
|
s11 = self.app.data.s11
|
||||||
maximums = sorted(
|
maximums = sorted(
|
||||||
at.maxima([d.impedance().real for d in s11],
|
at.maxima([d.impedance().real for d in s11], threshold=500)
|
||||||
threshold=500))
|
)
|
||||||
extended_data = {}
|
extended_data = {}
|
||||||
logger.info("TO DO: find near data")
|
logger.info("TO DO: find near data")
|
||||||
for lowest in self.crossing:
|
for lowest in self.crossings:
|
||||||
my_data = self._get_data(lowest)
|
my_data = self._get_data(lowest)
|
||||||
if lowest in extended_data:
|
if lowest in extended_data:
|
||||||
extended_data[lowest].update(my_data)
|
extended_data[lowest].update(my_data)
|
||||||
|
@ -61,12 +65,14 @@ class EFHWAnalysis(ResonanceAnalysis):
|
||||||
extended_data[m].update(my_data)
|
extended_data[m].update(my_data)
|
||||||
else:
|
else:
|
||||||
extended_data[m] = my_data
|
extended_data[m] = my_data
|
||||||
fields = [("freq", format_frequency_short),
|
fields = [
|
||||||
("r", format_resistence_neg), ("lambda", lambda x: round(x, 2))]
|
("freq", format_frequency_short),
|
||||||
|
("r", format_resistence_neg),
|
||||||
|
("lambda", lambda x: round(x, 2)),
|
||||||
|
]
|
||||||
|
|
||||||
if self.old_data:
|
if self.old_data:
|
||||||
diff = self.compare(
|
diff = self.compare(self.old_data[-1], extended_data, fields=fields)
|
||||||
self.old_data[-1], extended_data, fields=fields)
|
|
||||||
else:
|
else:
|
||||||
diff = self.compare({}, extended_data, fields=fields)
|
diff = self.compare({}, extended_data, fields=fields)
|
||||||
self.old_data.append(extended_data)
|
self.old_data.append(extended_data)
|
||||||
|
@ -76,14 +82,17 @@ class EFHWAnalysis(ResonanceAnalysis):
|
||||||
QtWidgets.QLabel(
|
QtWidgets.QLabel(
|
||||||
f" ({diff[i]['freq']})"
|
f" ({diff[i]['freq']})"
|
||||||
f" {format_complex_imp(s11[idx].impedance())}"
|
f" {format_complex_imp(s11[idx].impedance())}"
|
||||||
f" ({diff[i]['r']}) {diff[i]['lambda']} m"))
|
f" ({diff[i]['r']}) {diff[i]['lambda']} m"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
if self.filename and extended_data:
|
if self.filename and extended_data:
|
||||||
with open(
|
with open(
|
||||||
self.filename, 'w', newline='', encoding='utf-8'
|
self.filename, "w", newline="", encoding="utf-8"
|
||||||
) as csvfile:
|
) as csvfile:
|
||||||
fieldnames = extended_data[sorted(
|
fieldnames = extended_data[
|
||||||
extended_data.keys())[0]].keys()
|
sorted(extended_data.keys())[0]
|
||||||
|
].keys()
|
||||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
for idx in sorted(extended_data.keys()):
|
for idx in sorted(extended_data.keys()):
|
||||||
|
@ -99,10 +108,11 @@ class EFHWAnalysis(ResonanceAnalysis):
|
||||||
:param old:
|
:param old:
|
||||||
:param new:
|
:param new:
|
||||||
"""
|
"""
|
||||||
fields = fields or [("freq", str), ]
|
fields = fields or [
|
||||||
|
("freq", str),
|
||||||
|
]
|
||||||
|
|
||||||
def no_compare():
|
def no_compare():
|
||||||
|
|
||||||
return {k: "-" for k, _ in fields}
|
return {k: "-" for k, _ in fields}
|
||||||
|
|
||||||
old_idx = sorted(old.keys())
|
old_idx = sorted(old.keys())
|
||||||
|
@ -113,8 +123,9 @@ class EFHWAnalysis(ResonanceAnalysis):
|
||||||
i_tot = max(len(old_idx), len(new_idx))
|
i_tot = max(len(old_idx), len(new_idx))
|
||||||
|
|
||||||
if i_max != i_tot:
|
if i_max != i_tot:
|
||||||
logger.warning("resonances changed from %s to %s",
|
logger.warning(
|
||||||
len(old_idx), len(new_idx))
|
"resonances changed from %s to %s", len(old_idx), len(new_idx)
|
||||||
|
)
|
||||||
|
|
||||||
split = 0
|
split = 0
|
||||||
max_delta_f = 1_000_000
|
max_delta_f = 1_000_000
|
||||||
|
@ -135,15 +146,19 @@ class EFHWAnalysis(ResonanceAnalysis):
|
||||||
logger.debug("Deltas %s", diff[i])
|
logger.debug("Deltas %s", diff[i])
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logger.debug("can't compare, %s is too much ",
|
logger.debug(
|
||||||
format_frequency(delta_f))
|
"can't compare, %s is too much ", format_frequency(delta_f)
|
||||||
|
)
|
||||||
|
|
||||||
if delta_f > 0:
|
if delta_f > 0:
|
||||||
logger.debug("possible missing band, ")
|
logger.debug("possible missing band, ")
|
||||||
if len(old_idx) > (i + split + 1):
|
if len(old_idx) > (i + split + 1):
|
||||||
if (abs(new[k]["freq"] -
|
if (
|
||||||
old[old_idx[i + split + 1]]["freq"]) <
|
abs(
|
||||||
max_delta_f):
|
new[k]["freq"] - old[old_idx[i + split + 1]]["freq"]
|
||||||
|
)
|
||||||
|
< max_delta_f
|
||||||
|
):
|
||||||
logger.debug("new is missing band, compare next ")
|
logger.debug("new is missing band, compare next ")
|
||||||
split += 1
|
split += 1
|
||||||
# FIXME: manage 2 or more band missing ?!?
|
# FIXME: manage 2 or more band missing ?!?
|
|
@ -18,9 +18,8 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
from typing import Dict, List
|
|
||||||
|
|
||||||
from PyQt5 import QtWidgets
|
from PyQt6 import QtWidgets
|
||||||
|
|
||||||
import NanoVNASaver.AnalyticTools as at
|
import NanoVNASaver.AnalyticTools as at
|
||||||
from NanoVNASaver.Analysis.Base import Analysis, CUTOFF_VALS
|
from NanoVNASaver.Analysis.Base import Analysis, CUTOFF_VALS
|
||||||
|
@ -41,9 +40,12 @@ class HighPassAnalysis(Analysis):
|
||||||
|
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
layout.addRow(self.label["titel"])
|
layout.addRow(self.label["titel"])
|
||||||
layout.addRow(QtWidgets.QLabel(
|
layout.addRow(
|
||||||
f"Please place {self.app.markers[0].name}"
|
QtWidgets.QLabel(
|
||||||
f" in the filter passband."))
|
f"Please place {self.app.markers[0].name}"
|
||||||
|
f" in the filter passband."
|
||||||
|
)
|
||||||
|
)
|
||||||
layout.addRow("Result:", self.label["result"])
|
layout.addRow("Result:", self.label["result"])
|
||||||
layout.addRow("Cutoff frequency:", self.label["3.0dB"])
|
layout.addRow("Cutoff frequency:", self.label["3.0dB"])
|
||||||
layout.addRow("-6 dB point:", self.label["6.0dB"])
|
layout.addRow("-6 dB point:", self.label["6.0dB"])
|
||||||
|
@ -51,7 +53,7 @@ class HighPassAnalysis(Analysis):
|
||||||
layout.addRow("Roll-off:", self.label["octave"])
|
layout.addRow("Roll-off:", self.label["octave"])
|
||||||
layout.addRow("Roll-off:", self.label["decade"])
|
layout.addRow("Roll-off:", self.label["decade"])
|
||||||
|
|
||||||
self.set_titel('Highpass analysis')
|
self.set_titel("Highpass analysis")
|
||||||
|
|
||||||
def runAnalysis(self):
|
def runAnalysis(self):
|
||||||
if not self.app.data.s21:
|
if not self.app.data.s21:
|
||||||
|
@ -81,29 +83,32 @@ class HighPassAnalysis(Analysis):
|
||||||
logger.debug("Cuttoff gains: %s", cutoff_gain)
|
logger.debug("Cuttoff gains: %s", cutoff_gain)
|
||||||
|
|
||||||
octave, decade = at.calculate_rolloff(
|
octave, decade = at.calculate_rolloff(
|
||||||
s21, cutoff_pos["10.0dB"], cutoff_pos["20.0dB"])
|
s21, cutoff_pos["10.0dB"], cutoff_pos["20.0dB"]
|
||||||
|
)
|
||||||
|
|
||||||
if cutoff_gain['3.0dB'] < -4:
|
if cutoff_gain["3.0dB"] < -4:
|
||||||
logger.debug("Cutoff frequency found at %f dB"
|
logger.debug(
|
||||||
" - insufficient data points for true -3 dB point.",
|
"Cutoff frequency found at %f dB"
|
||||||
cutoff_gain)
|
" - insufficient data points for true -3 dB point.",
|
||||||
logger.debug("Found true cutoff frequency at %d", cutoff_freq['3.0dB'])
|
cutoff_gain,
|
||||||
|
)
|
||||||
|
logger.debug("Found true cutoff frequency at %d", cutoff_freq["3.0dB"])
|
||||||
|
|
||||||
for label, val in cutoff_freq.items():
|
for label, val in cutoff_freq.items():
|
||||||
self.label[label].setText(
|
self.label[label].setText(
|
||||||
f"{format_frequency(val)}"
|
f"{format_frequency(val)}" f" ({cutoff_gain[label]:.1f} dB)"
|
||||||
f" ({cutoff_gain[label]:.1f} dB)")
|
)
|
||||||
|
|
||||||
self.label['octave'].setText(f'{octave:.3f}dB/octave')
|
self.label["octave"].setText(f"{octave:.3f}dB/octave")
|
||||||
self.label['decade'].setText(f'{decade:.3f}dB/decade')
|
self.label["decade"].setText(f"{decade:.3f}dB/decade")
|
||||||
|
|
||||||
self.app.markers[0].setFrequency(str(s21[peak].freq))
|
self.app.markers[0].setFrequency(str(s21[peak].freq))
|
||||||
self.app.markers[1].setFrequency(str(cutoff_freq['3.0dB']))
|
self.app.markers[1].setFrequency(str(cutoff_freq["3.0dB"]))
|
||||||
self.app.markers[2].setFrequency(str(cutoff_freq['6.0dB']))
|
self.app.markers[2].setFrequency(str(cutoff_freq["6.0dB"]))
|
||||||
|
|
||||||
self.set_result(f"Analysis complete ({len(s21)}) points)")
|
self.set_result(f"Analysis complete ({len(s21)}) points)")
|
||||||
|
|
||||||
def find_level(self, gains: List[float]) -> int:
|
def find_level(self, gains: list[float]) -> int:
|
||||||
marker = self.app.markers[0]
|
marker = self.app.markers[0]
|
||||||
logger.debug("Pass band location: %d", marker.location)
|
logger.debug("Pass band location: %d", marker.location)
|
||||||
if marker.location < 0:
|
if marker.location < 0:
|
||||||
|
@ -111,11 +116,10 @@ class HighPassAnalysis(Analysis):
|
||||||
return -1
|
return -1
|
||||||
return at.center_from_idx(gains, marker.location)
|
return at.center_from_idx(gains, marker.location)
|
||||||
|
|
||||||
def find_cutoffs(self,
|
def find_cutoffs(
|
||||||
gains: List[float],
|
self, gains: list[float], peak: int, peak_db: float
|
||||||
peak: int, peak_db: float) -> Dict[str, int]:
|
) -> dict[str, int]:
|
||||||
return {
|
return {
|
||||||
f"{attn:.1f}dB": at.cut_off_left(
|
f"{attn:.1f}dB": at.cut_off_left(gains, peak, peak_db, attn)
|
||||||
gains, peak, peak_db, attn)
|
|
||||||
for attn in CUTOFF_VALS
|
for attn in CUTOFF_VALS
|
||||||
}
|
}
|
|
@ -17,7 +17,6 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List
|
|
||||||
|
|
||||||
import NanoVNASaver.AnalyticTools as at
|
import NanoVNASaver.AnalyticTools as at
|
||||||
from NanoVNASaver.Analysis.Base import CUTOFF_VALS
|
from NanoVNASaver.Analysis.Base import CUTOFF_VALS
|
||||||
|
@ -30,13 +29,12 @@ class LowPassAnalysis(HighPassAnalysis):
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
super().__init__(app)
|
super().__init__(app)
|
||||||
|
|
||||||
self.set_titel('Lowpass filter analysis')
|
self.set_titel("Lowpass filter analysis")
|
||||||
|
|
||||||
def find_cutoffs(self,
|
def find_cutoffs(
|
||||||
gains: List[float],
|
self, gains: list[float], peak: int, peak_db: float
|
||||||
peak: int, peak_db: float) -> Dict[str, int]:
|
) -> dict[str, int]:
|
||||||
return {
|
return {
|
||||||
f"{attn:.1f}dB": at.cut_off_right(
|
f"{attn:.1f}dB": at.cut_off_right(gains, peak, peak_db, attn)
|
||||||
gains, peak, peak_db, attn)
|
|
||||||
for attn in CUTOFF_VALS
|
for attn in CUTOFF_VALS
|
||||||
}
|
}
|
|
@ -18,14 +18,16 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PyQt5 import QtWidgets
|
from PyQt6 import QtWidgets
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
# pylint: disable=import-error, no-name-in-module
|
# pylint: disable=import-error, no-name-in-module
|
||||||
from scipy.signal import find_peaks, peak_prominences
|
from scipy.signal import find_peaks, peak_prominences
|
||||||
|
|
||||||
from NanoVNASaver.Analysis.Base import QHLine
|
from NanoVNASaver.Analysis.Base import QHLine
|
||||||
from NanoVNASaver.Analysis.SimplePeakSearchAnalysis import (
|
from NanoVNASaver.Analysis.SimplePeakSearchAnalysis import (
|
||||||
SimplePeakSearchAnalysis)
|
SimplePeakSearchAnalysis,
|
||||||
|
)
|
||||||
|
|
||||||
from NanoVNASaver.Formatting import format_frequency_short
|
from NanoVNASaver.Formatting import format_frequency_short
|
||||||
|
|
||||||
|
@ -34,7 +36,6 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PeakSearchAnalysis(SimplePeakSearchAnalysis):
|
class PeakSearchAnalysis(SimplePeakSearchAnalysis):
|
||||||
|
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
super().__init__(app)
|
super().__init__(app)
|
||||||
|
|
||||||
|
@ -48,7 +49,7 @@ class PeakSearchAnalysis(SimplePeakSearchAnalysis):
|
||||||
self.layout.addRow(QtWidgets.QLabel("<b>Results</b>"))
|
self.layout.addRow(QtWidgets.QLabel("<b>Results</b>"))
|
||||||
self.results_header = self.layout.rowCount()
|
self.results_header = self.layout.rowCount()
|
||||||
|
|
||||||
self.set_titel('Peak search')
|
self.set_titel("Peak search")
|
||||||
|
|
||||||
def runAnalysis(self):
|
def runAnalysis(self):
|
||||||
if not self.app.data.s11:
|
if not self.app.data.s11:
|
||||||
|
@ -59,18 +60,18 @@ class PeakSearchAnalysis(SimplePeakSearchAnalysis):
|
||||||
data, fmt_fnc = self.data_and_format()
|
data, fmt_fnc = self.data_and_format()
|
||||||
|
|
||||||
inverted = False
|
inverted = False
|
||||||
if self.button['peak_l'].isChecked():
|
if self.button["peak_l"].isChecked():
|
||||||
inverted = True
|
inverted = True
|
||||||
peaks, _ = find_peaks(
|
peaks, _ = find_peaks(
|
||||||
-np.array(data), width=3, distance=3, prominence=1)
|
-np.array(data), width=3, distance=3, prominence=1
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.button['peak_h'].setChecked(True)
|
self.button["peak_h"].setChecked(True)
|
||||||
peaks, _ = find_peaks(
|
peaks, _ = find_peaks(data, width=3, distance=3, prominence=1)
|
||||||
data, width=3, distance=3, prominence=1)
|
|
||||||
|
|
||||||
# Having found the peaks, get the prominence data
|
# Having found the peaks, get the prominence data
|
||||||
for i, p in np.ndenumerate(peaks):
|
for i, p in np.ndenumerate(peaks):
|
||||||
logger.debug("Peak %i at %d", i, p)
|
logger.debug("Peak %s at %s", i, p)
|
||||||
prominences = peak_prominences(data, peaks)[0]
|
prominences = peak_prominences(data, peaks)[0]
|
||||||
logger.debug("%d prominences", len(prominences))
|
logger.debug("%d prominences", len(prominences))
|
||||||
|
|
||||||
|
@ -89,19 +90,24 @@ class PeakSearchAnalysis(SimplePeakSearchAnalysis):
|
||||||
f"Freq: {format_frequency_short(s11[pos].freq)}",
|
f"Freq: {format_frequency_short(s11[pos].freq)}",
|
||||||
QtWidgets.QLabel(
|
QtWidgets.QLabel(
|
||||||
f" Value: {fmt_fnc(-data[pos] if inverted else data[pos])}"
|
f" Value: {fmt_fnc(-data[pos] if inverted else data[pos])}"
|
||||||
))
|
),
|
||||||
|
)
|
||||||
|
|
||||||
if self.button['move_marker'].isChecked():
|
if self.button["move_marker"].isChecked():
|
||||||
if count > len(self.app.markers):
|
if count > len(self.app.markers):
|
||||||
logger.warning("More peaks found than there are markers")
|
logger.warning("More peaks found than there are markers")
|
||||||
for i in range(min(count, len(self.app.markers))):
|
for i in range(min(count, len(self.app.markers))):
|
||||||
self.app.markers[i].setFrequency(
|
self.app.markers[i].setFrequency(
|
||||||
str(s11[peaks[indices[i]]].freq))
|
str(s11[peaks[indices[i]]].freq)
|
||||||
|
)
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
super().reset()
|
super().reset()
|
||||||
logger.debug("Results start at %d, out of %d",
|
logger.debug(
|
||||||
self.results_header, self.layout.rowCount())
|
"Results start at %d, out of %d",
|
||||||
|
self.results_header,
|
||||||
|
self.layout.rowCount(),
|
||||||
|
)
|
||||||
for _ in range(self.results_header, self.layout.rowCount()):
|
for _ in range(self.results_header, self.layout.rowCount()):
|
||||||
logger.debug("deleting %s", self.layout.rowCount())
|
logger.debug("deleting %s", self.layout.rowCount())
|
||||||
self.layout.removeRow(self.layout.rowCount() - 1)
|
self.layout.removeRow(self.layout.rowCount() - 1)
|
|
@ -19,15 +19,12 @@
|
||||||
import os
|
import os
|
||||||
import csv
|
import csv
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from PyQt5 import QtWidgets
|
from PyQt6 import QtWidgets
|
||||||
|
|
||||||
import NanoVNASaver.AnalyticTools as at
|
import NanoVNASaver.AnalyticTools as at
|
||||||
from NanoVNASaver.Analysis.Base import Analysis, QHLine
|
from NanoVNASaver.Analysis.Base import Analysis, QHLine
|
||||||
from NanoVNASaver.Formatting import (
|
from NanoVNASaver.Formatting import format_frequency, format_resistance
|
||||||
format_frequency, format_complex_imp,
|
|
||||||
format_resistance)
|
|
||||||
from NanoVNASaver.RFTools import reflection_coefficient
|
from NanoVNASaver.RFTools import reflection_coefficient
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -44,10 +41,9 @@ def vswr_transformed(z, ratio=49) -> float:
|
||||||
|
|
||||||
|
|
||||||
class ResonanceAnalysis(Analysis):
|
class ResonanceAnalysis(Analysis):
|
||||||
|
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
super().__init__(app)
|
super().__init__(app)
|
||||||
self.crossing: List[int] = []
|
self.crossings: list[int] = []
|
||||||
self.filename = ""
|
self.filename = ""
|
||||||
self._widget = QtWidgets.QWidget()
|
self._widget = QtWidgets.QWidget()
|
||||||
self.layout = QtWidgets.QFormLayout()
|
self.layout = QtWidgets.QFormLayout()
|
||||||
|
@ -72,10 +68,8 @@ class ResonanceAnalysis(Analysis):
|
||||||
"impedance": s11[index].impedance(),
|
"impedance": s11[index].impedance(),
|
||||||
"vswr": s11[index].vswr,
|
"vswr": s11[index].vswr,
|
||||||
}
|
}
|
||||||
my_data["vswr_49"] = vswr_transformed(
|
my_data["vswr_49"] = vswr_transformed(my_data["impedance"], 49)
|
||||||
my_data["impedance"], 49)
|
my_data["vswr_4"] = vswr_transformed(my_data["impedance"], 4)
|
||||||
my_data["vswr_4"] = vswr_transformed(
|
|
||||||
my_data["impedance"], 4)
|
|
||||||
my_data["r"] = my_data["impedance"].real
|
my_data["r"] = my_data["impedance"].real
|
||||||
my_data["x"] = my_data["impedance"].imag
|
my_data["x"] = my_data["impedance"].imag
|
||||||
|
|
||||||
|
@ -83,52 +77,48 @@ class ResonanceAnalysis(Analysis):
|
||||||
|
|
||||||
def runAnalysis(self):
|
def runAnalysis(self):
|
||||||
self.reset()
|
self.reset()
|
||||||
self.filename = os.path.join(
|
self.filename = (
|
||||||
"/tmp/", f"{self.input_description.text()}.csv"
|
os.path.join("/tmp/", f"{self.input_description.text()}.csv")
|
||||||
) if self.input_description.text() else ""
|
if self.input_description.text()
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
results_header = self.layout.indexOf(self.results_label)
|
results_header = self.layout.indexOf(self.results_label)
|
||||||
logger.debug("Results start at %d, out of %d",
|
logger.debug(
|
||||||
results_header, self.layout.rowCount())
|
"Results start at %d, out of %d",
|
||||||
|
results_header,
|
||||||
|
self.layout.rowCount(),
|
||||||
|
)
|
||||||
|
|
||||||
for _ in range(results_header, self.layout.rowCount()):
|
for _ in range(results_header, self.layout.rowCount()):
|
||||||
self.layout.removeRow(self.layout.rowCount() - 1)
|
self.layout.removeRow(self.layout.rowCount() - 1)
|
||||||
|
|
||||||
self.crossing = at.zero_crossings([d.phase for d in self.app.data.s11])
|
self.crossings = sorted(
|
||||||
logger.debug("Found %d sections ",
|
set(at.zero_crossings([d.phase for d in self.app.data.s11]))
|
||||||
len(self.crossing))
|
)
|
||||||
if not self.crossing:
|
logger.debug("Found %d sections ", len(self.crossings))
|
||||||
self.layout.addRow(QtWidgets.QLabel(
|
if not self.crossings:
|
||||||
"No resonance found"))
|
self.layout.addRow(QtWidgets.QLabel("No resonance found"))
|
||||||
return
|
return
|
||||||
|
|
||||||
self.do_resonance_analysis()
|
self.do_resonance_analysis()
|
||||||
|
|
||||||
def do_resonance_analysis(self):
|
def do_resonance_analysis(self):
|
||||||
extended_data = []
|
extended_data = []
|
||||||
for m in self.crossing:
|
for crossing in self.crossings:
|
||||||
start, lowest, end = m
|
extended_data.append(self._get_data(crossing))
|
||||||
my_data = self._get_data(lowest)
|
self.layout.addRow(
|
||||||
s11_low = self.app.data.s11[lowest]
|
"Resonance",
|
||||||
extended_data.append(my_data)
|
QtWidgets.QLabel(
|
||||||
if start != end:
|
format_frequency(self.app.data.s11[crossing].freq)
|
||||||
logger.debug(
|
),
|
||||||
"Section from %d to %d, lowest at %d",
|
)
|
||||||
start, end, lowest)
|
self.layout.addWidget(QHLine())
|
||||||
self.layout.addRow(
|
|
||||||
"Resonance",
|
|
||||||
QtWidgets.QLabel(
|
|
||||||
f"{format_frequency(s11_low.freq)}"
|
|
||||||
f" ({format_complex_imp(s11_low.impedance())})"))
|
|
||||||
else:
|
|
||||||
self.layout.addRow("Resonance", QtWidgets.QLabel(
|
|
||||||
format_frequency(self.app.data.s11[lowest].freq)))
|
|
||||||
self.layout.addWidget(QHLine())
|
|
||||||
# Remove the final separator line
|
# Remove the final separator line
|
||||||
self.layout.removeRow(self.layout.rowCount() - 1)
|
self.layout.removeRow(self.layout.rowCount() - 1)
|
||||||
if self.filename and extended_data:
|
if self.filename and extended_data:
|
||||||
with open(
|
with open(
|
||||||
self.filename, 'w', encoding='utf-8', newline=''
|
self.filename, "w", encoding="utf-8", newline=""
|
||||||
) as csvfile:
|
) as csvfile:
|
||||||
fieldnames = extended_data[0].keys()
|
fieldnames = extended_data[0].keys()
|
||||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
|
@ -0,0 +1,123 @@
|
||||||
|
# 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 logging
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from PyQt6 import QtWidgets
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from NanoVNASaver.Analysis.Base import Analysis, QHLine
|
||||||
|
from NanoVNASaver.Formatting import (
|
||||||
|
format_frequency,
|
||||||
|
format_gain,
|
||||||
|
format_resistance,
|
||||||
|
format_vswr,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SimplePeakSearchAnalysis(Analysis):
|
||||||
|
def __init__(self, app):
|
||||||
|
super().__init__(app)
|
||||||
|
|
||||||
|
self.label["peak_freq"] = QtWidgets.QLabel()
|
||||||
|
self.label["peak_db"] = QtWidgets.QLabel()
|
||||||
|
|
||||||
|
self.button = {
|
||||||
|
"vswr": QtWidgets.QRadioButton("VSWR"),
|
||||||
|
"resistance": QtWidgets.QRadioButton("Resistance"),
|
||||||
|
"reactance": QtWidgets.QRadioButton("Reactance"),
|
||||||
|
"gain": QtWidgets.QRadioButton("S21 Gain"),
|
||||||
|
"peak_h": QtWidgets.QRadioButton("Highest value"),
|
||||||
|
"peak_l": QtWidgets.QRadioButton("Lowest value"),
|
||||||
|
"move_marker": QtWidgets.QCheckBox(),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.button["gain"].setChecked(True)
|
||||||
|
self.button["peak_h"].setChecked(True)
|
||||||
|
|
||||||
|
self.btn_group = {
|
||||||
|
"data": QtWidgets.QButtonGroup(),
|
||||||
|
"peak": QtWidgets.QButtonGroup(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for btn in ("vswr", "resistance", "reactance", "gain"):
|
||||||
|
self.btn_group["data"].addButton(self.button[btn])
|
||||||
|
self.btn_group["peak"].addButton(self.button["peak_h"])
|
||||||
|
self.btn_group["peak"].addButton(self.button["peak_l"])
|
||||||
|
|
||||||
|
layout = self.layout
|
||||||
|
layout.addRow(self.label["titel"])
|
||||||
|
layout.addRow(QHLine())
|
||||||
|
layout.addRow(QtWidgets.QLabel("<b>Settings</b>"))
|
||||||
|
layout.addRow("Data source", self.button["vswr"])
|
||||||
|
layout.addRow("", self.button["resistance"])
|
||||||
|
layout.addRow("", self.button["reactance"])
|
||||||
|
layout.addRow("", self.button["gain"])
|
||||||
|
layout.addRow(QHLine())
|
||||||
|
layout.addRow("Peak type", self.button["peak_h"])
|
||||||
|
layout.addRow("", self.button["peak_l"])
|
||||||
|
layout.addRow(QHLine())
|
||||||
|
layout.addRow("Move marker to peak", self.button["move_marker"])
|
||||||
|
layout.addRow(QHLine())
|
||||||
|
layout.addRow(self.label["result"])
|
||||||
|
layout.addRow("Peak frequency:", self.label["peak_freq"])
|
||||||
|
layout.addRow("Peak value:", self.label["peak_db"])
|
||||||
|
|
||||||
|
self.set_titel("Simple peak search")
|
||||||
|
|
||||||
|
def runAnalysis(self):
|
||||||
|
if not self.app.data.s11:
|
||||||
|
return
|
||||||
|
|
||||||
|
s11 = self.app.data.s11
|
||||||
|
data, fmt_fnc = self.data_and_format()
|
||||||
|
|
||||||
|
if self.button["peak_l"].isChecked():
|
||||||
|
idx_peak = np.argmin(data)
|
||||||
|
else:
|
||||||
|
self.button["peak_h"].setChecked(True)
|
||||||
|
idx_peak = np.argmax(data)
|
||||||
|
|
||||||
|
self.label["peak_freq"].setText(format_frequency(s11[idx_peak].freq))
|
||||||
|
self.label["peak_db"].setText(fmt_fnc(data[idx_peak]))
|
||||||
|
|
||||||
|
if self.button["move_marker"].isChecked() and self.app.markers:
|
||||||
|
self.app.markers[0].setFrequency(f"{s11[idx_peak].freq}")
|
||||||
|
|
||||||
|
def data_and_format(self) -> tuple[list[float], Callable]:
|
||||||
|
s11 = self.app.data.s11
|
||||||
|
s21 = self.app.data.s21
|
||||||
|
|
||||||
|
if not s21:
|
||||||
|
self.button["gain"].setEnabled(False)
|
||||||
|
if self.button["gain"].isChecked():
|
||||||
|
self.button["vswr"].setChecked(True)
|
||||||
|
else:
|
||||||
|
self.button["gain"].setEnabled(True)
|
||||||
|
|
||||||
|
if self.button["gain"].isChecked():
|
||||||
|
return ([d.gain for d in s21], format_gain)
|
||||||
|
if self.button["resistance"].isChecked():
|
||||||
|
return ([d.impedance().real for d in s11], format_resistance)
|
||||||
|
if self.button["reactance"].isChecked():
|
||||||
|
return ([d.impedance().imag for d in s11], format_resistance)
|
||||||
|
# default
|
||||||
|
return ([d.vswr for d in s11], format_vswr)
|
|
@ -17,9 +17,8 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from PyQt5 import QtWidgets
|
from PyQt6 import QtWidgets
|
||||||
|
|
||||||
import NanoVNASaver.AnalyticTools as at
|
import NanoVNASaver.AnalyticTools as at
|
||||||
from NanoVNASaver.Analysis.Base import Analysis, QHLine
|
from NanoVNASaver.Analysis.Base import Analysis, QHLine
|
||||||
|
@ -54,7 +53,7 @@ class VSWRAnalysis(Analysis):
|
||||||
self.results_label = QtWidgets.QLabel("<b>Results</b>")
|
self.results_label = QtWidgets.QLabel("<b>Results</b>")
|
||||||
self.layout.addRow(self.results_label)
|
self.layout.addRow(self.results_label)
|
||||||
|
|
||||||
self.minimums: List[int] = []
|
self.minimums: list[int] = []
|
||||||
|
|
||||||
def runAnalysis(self):
|
def runAnalysis(self):
|
||||||
if not self.app.data.s11:
|
if not self.app.data.s11:
|
||||||
|
@ -64,34 +63,50 @@ class VSWRAnalysis(Analysis):
|
||||||
data = [d.vswr for d in s11]
|
data = [d.vswr for d in s11]
|
||||||
threshold = self.input_vswr_limit.value()
|
threshold = self.input_vswr_limit.value()
|
||||||
|
|
||||||
minima = sorted(at.minima(data, threshold),
|
minima = sorted(at.minima(data, threshold), key=lambda i: data[i])[
|
||||||
key=lambda i: data[i])[:VSWRAnalysis.max_dips_shown]
|
: VSWRAnalysis.max_dips_shown
|
||||||
|
]
|
||||||
self.minimums = minima
|
self.minimums = minima
|
||||||
|
|
||||||
results_header = self.layout.indexOf(self.results_label)
|
results_header = self.layout.indexOf(self.results_label)
|
||||||
logger.debug("Results start at %d, out of %d",
|
logger.debug(
|
||||||
results_header, self.layout.rowCount())
|
"Results start at %d, out of %d",
|
||||||
|
results_header,
|
||||||
|
self.layout.rowCount(),
|
||||||
|
)
|
||||||
for _ in range(results_header, self.layout.rowCount()):
|
for _ in range(results_header, self.layout.rowCount()):
|
||||||
self.layout.removeRow(self.layout.rowCount() - 1)
|
self.layout.removeRow(self.layout.rowCount() - 1)
|
||||||
|
|
||||||
if not minima:
|
if not minima:
|
||||||
self.layout.addRow(QtWidgets.QLabel(
|
self.layout.addRow(
|
||||||
f"No areas found with VSWR below {format_vswr(threshold)}."))
|
QtWidgets.QLabel(
|
||||||
|
f"No areas found with VSWR below {format_vswr(threshold)}."
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
for idx in minima:
|
for idx in minima:
|
||||||
rng = at.take_from_idx(data, idx, lambda i: i[1] < threshold)
|
rng = at.take_from_idx(data, idx, lambda i: i[1] < threshold)
|
||||||
begin, end = rng[0], rng[-1]
|
begin, end = rng[0], rng[-1]
|
||||||
self.layout.addRow("Start", QtWidgets.QLabel(
|
|
||||||
format_frequency(s11[begin].freq)))
|
|
||||||
self.layout.addRow("Minimum", QtWidgets.QLabel(
|
|
||||||
f"{format_frequency(s11[idx].freq)}"
|
|
||||||
f" ({round(s11[idx].vswr, 2)})"))
|
|
||||||
self.layout.addRow("End", QtWidgets.QLabel(
|
|
||||||
format_frequency(s11[end].freq)))
|
|
||||||
self.layout.addRow(
|
self.layout.addRow(
|
||||||
"Span", QtWidgets.QLabel(format_frequency(
|
"Start", QtWidgets.QLabel(format_frequency(s11[begin].freq))
|
||||||
(s11[end].freq - s11[begin].freq))))
|
)
|
||||||
|
self.layout.addRow(
|
||||||
|
"Minimum",
|
||||||
|
QtWidgets.QLabel(
|
||||||
|
f"{format_frequency(s11[idx].freq)}"
|
||||||
|
f" ({round(s11[idx].vswr, 2)})"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.layout.addRow(
|
||||||
|
"End", QtWidgets.QLabel(format_frequency(s11[end].freq))
|
||||||
|
)
|
||||||
|
self.layout.addRow(
|
||||||
|
"Span",
|
||||||
|
QtWidgets.QLabel(
|
||||||
|
format_frequency((s11[end].freq - s11[begin].freq))
|
||||||
|
),
|
||||||
|
)
|
||||||
self.layout.addWidget(QHLine())
|
self.layout.addWidget(QHLine())
|
||||||
|
|
||||||
self.layout.removeRow(self.layout.rowCount() - 1)
|
self.layout.removeRow(self.layout.rowCount() - 1)
|
|
@ -18,23 +18,24 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import itertools as it
|
import itertools as it
|
||||||
import math
|
import math
|
||||||
from typing import Callable, List, Tuple
|
from typing import Callable
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
# pylint: disable=import-error, no-name-in-module
|
# pylint: disable=import-error, no-name-in-module
|
||||||
from scipy.signal import find_peaks
|
from scipy.signal import find_peaks
|
||||||
|
|
||||||
from NanoVNASaver.RFTools import Datapoint
|
from NanoVNASaver.RFTools import Datapoint
|
||||||
|
|
||||||
|
|
||||||
def zero_crossings(data: List[float]) -> List[int]:
|
def zero_crossings(data: list[float]) -> list[int]:
|
||||||
"""find zero crossings
|
"""find zero crossings
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data (List[float]): data list execute
|
data (list[float]): data list execute
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[int]: sorted indices of zero crossing points
|
list[int]: sorted indices of zero crossing points
|
||||||
"""
|
"""
|
||||||
if not data:
|
if not data:
|
||||||
return []
|
return []
|
||||||
|
@ -42,8 +43,9 @@ def zero_crossings(data: List[float]) -> List[int]:
|
||||||
np_data = np.array(data)
|
np_data = np.array(data)
|
||||||
|
|
||||||
# start with real zeros (ignore first and last element)
|
# start with real zeros (ignore first and last element)
|
||||||
real_zeros = [n for n in np.where(np_data == 0.0)[0] if
|
real_zeros = [
|
||||||
n not in {0, np_data.size - 1}]
|
n for n in np.where(np_data == 0.0)[0] if n not in {0, np_data.size - 1}
|
||||||
|
]
|
||||||
# now multipy elements to find change in signess
|
# now multipy elements to find change in signess
|
||||||
crossings = [
|
crossings = [
|
||||||
n if abs(np_data[n]) < abs(np_data[n + 1]) else n + 1
|
n if abs(np_data[n]) < abs(np_data[n + 1]) else n + 1
|
||||||
|
@ -52,69 +54,68 @@ def zero_crossings(data: List[float]) -> List[int]:
|
||||||
return sorted(real_zeros + crossings)
|
return sorted(real_zeros + crossings)
|
||||||
|
|
||||||
|
|
||||||
def maxima(data: List[float], threshold: float = 0.0) -> List[int]:
|
def maxima(data: list[float], threshold: float = 0.0) -> list[int]:
|
||||||
"""maxima
|
"""maxima
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data (List[float]): data list to execute
|
data (list[float]): data list to execute
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[int]: indices of maxima
|
list[int]: indices of maxima
|
||||||
"""
|
"""
|
||||||
peaks = find_peaks(
|
peaks = find_peaks(data, width=2, distance=3, prominence=1)[0].tolist()
|
||||||
data, width=2, distance=3, prominence=1)[0].tolist()
|
return [i for i in peaks if data[i] > threshold] if threshold else peaks
|
||||||
return [
|
|
||||||
i for i in peaks if data[i] > threshold
|
|
||||||
] if threshold else peaks
|
|
||||||
|
|
||||||
|
|
||||||
def minima(data: List[float], threshold: float = 0.0) -> List[int]:
|
def minima(data: list[float], threshold: float = 0.0) -> list[int]:
|
||||||
"""minima
|
"""minima
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data (List[float]): data list to execute
|
data (list[float]): data list to execute
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[int]: indices of minima
|
list[int]: indices of minima
|
||||||
"""
|
"""
|
||||||
bottoms = find_peaks(
|
bottoms = find_peaks(-np.array(data), width=2, distance=3, prominence=1)[
|
||||||
-np.array(data), width=2, distance=3, prominence=1)[0].tolist()
|
0
|
||||||
return [
|
].tolist()
|
||||||
i for i in bottoms if data[i] < threshold
|
return [i for i in bottoms if data[i] < threshold] if threshold else bottoms
|
||||||
] if threshold else bottoms
|
|
||||||
|
|
||||||
|
|
||||||
def take_from_idx(data: List[float],
|
def take_from_idx(
|
||||||
idx: int,
|
data: list[float], idx: int, predicate: Callable
|
||||||
predicate: Callable) -> List[int]:
|
) -> list[int]:
|
||||||
"""take_from_center
|
"""take_from_center
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data (List[float]): data list to execute
|
data (list[float]): data list to execute
|
||||||
idx (int): index of a start position
|
idx (int): index of a start position
|
||||||
predicate (Callable): predicate on which elements to take
|
predicate (Callable): predicate on which elements to take
|
||||||
from center. (e.g. lambda i: i[1] < threshold)
|
from center. (e.g. lambda i: i[1] < threshold)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[int]: indices of element matching predicate left
|
list[int]: indices of element matching predicate left
|
||||||
and right from index
|
and right from index
|
||||||
"""
|
"""
|
||||||
lower = list(reversed(
|
lower = list(
|
||||||
[i for i, _ in
|
reversed(
|
||||||
it.takewhile(predicate,
|
[
|
||||||
reversed(list(enumerate(data[:idx]))))]))
|
i
|
||||||
upper = [i for i, _ in
|
for i, _ in it.takewhile(
|
||||||
it.takewhile(predicate,
|
predicate, reversed(list(enumerate(data[:idx])))
|
||||||
enumerate(data[idx:], idx))]
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
upper = [i for i, _ in it.takewhile(predicate, enumerate(data[idx:], idx))]
|
||||||
return lower + upper
|
return lower + upper
|
||||||
|
|
||||||
|
|
||||||
def center_from_idx(gains: List[float],
|
def center_from_idx(gains: list[float], idx: int, delta: float = 3.0) -> int:
|
||||||
idx: int, delta: float = 3.0) -> int:
|
|
||||||
"""find maximum from index postion of gains in a attn dB gain span
|
"""find maximum from index postion of gains in a attn dB gain span
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
gains (List[float]): gain values
|
gains (list[float]): gain values
|
||||||
idx (int): start position to search from
|
idx (int): start position to search from
|
||||||
delta (float, optional): max gain delta from start. Defaults to 3.0.
|
delta (float, optional): max gain delta from start. Defaults to 3.0.
|
||||||
|
|
||||||
|
@ -122,18 +123,18 @@ def center_from_idx(gains: List[float],
|
||||||
int: position of highest gain from start in range (-1 if no data)
|
int: position of highest gain from start in range (-1 if no data)
|
||||||
"""
|
"""
|
||||||
peak_db = gains[idx]
|
peak_db = gains[idx]
|
||||||
rng = take_from_idx(gains, idx,
|
rng = take_from_idx(gains, idx, lambda i: abs(peak_db - i[1]) < delta)
|
||||||
lambda i: abs(peak_db - i[1]) < delta)
|
|
||||||
return max(rng, key=lambda i: gains[i]) if rng else -1
|
return max(rng, key=lambda i: gains[i]) if rng else -1
|
||||||
|
|
||||||
|
|
||||||
def cut_off_left(gains: List[float], idx: int,
|
def cut_off_left(
|
||||||
peak_gain: float, attn: float = 3.0) -> int:
|
gains: list[float], idx: int, peak_gain: float, attn: float = 3.0
|
||||||
|
) -> int:
|
||||||
"""find first position in list where gain in attn lower then peak
|
"""find first position in list where gain in attn lower then peak
|
||||||
left from index
|
left from index
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
gains (List[float]): gain values
|
gains (list[float]): gain values
|
||||||
idx (int): start position to search from
|
idx (int): start position to search from
|
||||||
peak_gain (float): reference gain value
|
peak_gain (float): reference gain value
|
||||||
attn (float, optional): attenuation to search position for.
|
attn (float, optional): attenuation to search position for.
|
||||||
|
@ -143,18 +144,18 @@ def cut_off_left(gains: List[float], idx: int,
|
||||||
int: position of attenuation point. (-1 if no data)
|
int: position of attenuation point. (-1 if no data)
|
||||||
"""
|
"""
|
||||||
return next(
|
return next(
|
||||||
(i for i in range(idx, -1, -1) if
|
(i for i in range(idx, -1, -1) if (peak_gain - gains[i]) > attn), -1
|
||||||
(peak_gain - gains[i]) > attn),
|
)
|
||||||
-1)
|
|
||||||
|
|
||||||
|
|
||||||
def cut_off_right(gains: List[float], idx: int,
|
def cut_off_right(
|
||||||
peak_gain: float, attn: float = 3.0) -> int:
|
gains: list[float], idx: int, peak_gain: float, attn: float = 3.0
|
||||||
|
) -> int:
|
||||||
"""find first position in list where gain in attn lower then peak
|
"""find first position in list where gain in attn lower then peak
|
||||||
right from index
|
right from index
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
gains (List[float]): gain values
|
gains (list[float]): gain values
|
||||||
idx (int): start position to search from
|
idx (int): start position to search from
|
||||||
peak_gain (float): reference gain value
|
peak_gain (float): reference gain value
|
||||||
attn (float, optional): attenuation to search position for.
|
attn (float, optional): attenuation to search position for.
|
||||||
|
@ -165,19 +166,20 @@ def cut_off_right(gains: List[float], idx: int,
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return next(
|
return next(
|
||||||
(i for i in range(idx, len(gains)) if
|
(i for i in range(idx, len(gains)) if (peak_gain - gains[i]) > attn), -1
|
||||||
(peak_gain - gains[i]) > attn),
|
)
|
||||||
-1)
|
|
||||||
|
|
||||||
|
|
||||||
def dip_cut_offs(gains: List[float], peak_gain: float,
|
def dip_cut_offs(
|
||||||
attn: float = 3.0) -> Tuple[int, int]:
|
gains: list[float], peak_gain: float, attn: float = 3.0
|
||||||
|
) -> tuple[int, int]:
|
||||||
rng = np.where(np.array(gains) < (peak_gain - attn))[0].tolist()
|
rng = np.where(np.array(gains) < (peak_gain - attn))[0].tolist()
|
||||||
return (rng[0], rng[-1]) if rng else (math.nan, math.nan)
|
return (rng[0], rng[-1]) if rng else (math.nan, math.nan)
|
||||||
|
|
||||||
|
|
||||||
def calculate_rolloff(s21: List[Datapoint],
|
def calculate_rolloff(
|
||||||
idx_1: int, idx_2: int) -> Tuple[float, float]:
|
s21: list[Datapoint], idx_1: int, idx_2: int
|
||||||
|
) -> tuple[float, float]:
|
||||||
if idx_1 == idx_2:
|
if idx_1 == idx_2:
|
||||||
return (math.nan, math.nan)
|
return (math.nan, math.nan)
|
||||||
freq_1, freq_2 = s21[idx_1].freq, s21[idx_2].freq
|
freq_1, freq_2 = s21[idx_1].freq, s21[idx_2].freq
|
|
@ -23,7 +23,6 @@ import os
|
||||||
import re
|
import re
|
||||||
from collections import defaultdict, UserDict
|
from collections import defaultdict, UserDict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from scipy.interpolate import interp1d
|
from scipy.interpolate import interp1d
|
||||||
|
|
||||||
|
@ -35,7 +34,8 @@ IDEAL_OPEN = complex(1, 0)
|
||||||
IDEAL_LOAD = complex(0, 0)
|
IDEAL_LOAD = complex(0, 0)
|
||||||
IDEAL_THROUGH = complex(1, 0)
|
IDEAL_THROUGH = complex(1, 0)
|
||||||
|
|
||||||
RXP_CAL_HEADER = re.compile(r"""
|
RXP_CAL_HEADER = re.compile(
|
||||||
|
r"""
|
||||||
^ \# \s+ Hz \s+
|
^ \# \s+ Hz \s+
|
||||||
ShortR \s+ ShortI \s+ OpenR \s+ OpenI \s+
|
ShortR \s+ ShortI \s+ OpenR \s+ OpenI \s+
|
||||||
LoadR \s+ LoadI
|
LoadR \s+ LoadI
|
||||||
|
@ -43,9 +43,12 @@ RXP_CAL_HEADER = re.compile(r"""
|
||||||
(?P<thrurefl> \s+ ThrureflR \s+ ThrureflI)?
|
(?P<thrurefl> \s+ ThrureflR \s+ ThrureflI)?
|
||||||
(?P<isolation> \s+ IsolationR \s+ IsolationI)?
|
(?P<isolation> \s+ IsolationR \s+ IsolationI)?
|
||||||
\s* $
|
\s* $
|
||||||
""", re.VERBOSE | re.IGNORECASE)
|
""",
|
||||||
|
re.VERBOSE | re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
RXP_CAL_LINE = re.compile(r"""
|
RXP_CAL_LINE = re.compile(
|
||||||
|
r"""
|
||||||
^ \s*
|
^ \s*
|
||||||
(?P<freq>\d+) \s+
|
(?P<freq>\d+) \s+
|
||||||
(?P<shortr>[-0-9Ee.]+) \s+ (?P<shorti>[-0-9Ee.]+) \s+
|
(?P<shortr>[-0-9Ee.]+) \s+ (?P<shorti>[-0-9Ee.]+) \s+
|
||||||
|
@ -55,7 +58,9 @@ RXP_CAL_LINE = re.compile(r"""
|
||||||
( \s+ (?P<thrureflr>[-0-9Ee.]+) \s+ (?P<thrurefli>[-0-9Ee.]+))?
|
( \s+ (?P<thrureflr>[-0-9Ee.]+) \s+ (?P<thrurefli>[-0-9Ee.]+))?
|
||||||
( \s+ (?P<isolationr>[-0-9Ee.]+) \s+ (?P<isolationi>[-0-9Ee.]+))?
|
( \s+ (?P<isolationr>[-0-9Ee.]+) \s+ (?P<isolationi>[-0-9Ee.]+))?
|
||||||
\s* $
|
\s* $
|
||||||
""", re.VERBOSE)
|
""",
|
||||||
|
re.VERBOSE,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -63,7 +68,8 @@ logger = logging.getLogger(__name__)
|
||||||
def correct_delay(d: Datapoint, delay: float, reflect: bool = False):
|
def correct_delay(d: Datapoint, delay: float, reflect: bool = False):
|
||||||
mult = 2 if reflect else 1
|
mult = 2 if reflect else 1
|
||||||
corr_data = d.z * cmath.exp(
|
corr_data = d.z * cmath.exp(
|
||||||
complex(0, 1) * 2 * math.pi * d.freq * delay * -1 * mult)
|
complex(0, 1) * 2 * math.pi * d.freq * delay * -1 * mult
|
||||||
|
)
|
||||||
return Datapoint(d.freq, corr_data.real, corr_data.imag)
|
return Datapoint(d.freq, corr_data.real, corr_data.imag)
|
||||||
|
|
||||||
|
|
||||||
|
@ -88,14 +94,16 @@ class CalData:
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return (
|
return (
|
||||||
f'{self.freq}'
|
f"{self.freq}"
|
||||||
f' {self.short.real} {self.short.imag}'
|
f" {self.short.real} {self.short.imag}"
|
||||||
f' {self.open.real} {self.open.imag}'
|
f" {self.open.real} {self.open.imag}"
|
||||||
f' {self.load.real} {self.load.imag}' + (
|
f" {self.load.real} {self.load.imag}"
|
||||||
f' {self.through.real} {self.through.imag}'
|
+ (
|
||||||
f' {self.thrurefl.real} {self.thrurefl.imag}'
|
f" {self.through.real} {self.through.imag}"
|
||||||
f' {self.isolation.real} {self.isolation.imag}'
|
f" {self.thrurefl.real} {self.thrurefl.imag}"
|
||||||
if self.through else ''
|
f" {self.isolation.real} {self.isolation.imag}"
|
||||||
|
if self.through
|
||||||
|
else ""
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -138,26 +146,32 @@ class CalDataSet(UserDict):
|
||||||
(
|
(
|
||||||
"# Calibration data for NanoVNA-Saver\n"
|
"# Calibration data for NanoVNA-Saver\n"
|
||||||
+ "\n".join([f"! {note}" for note in self.notes.splitlines()])
|
+ "\n".join([f"! {note}" for note in self.notes.splitlines()])
|
||||||
+ "\n" + "# Hz ShortR ShortI OpenR OpenI LoadR LoadI"
|
+ "\n"
|
||||||
+ (" ThroughR ThroughI ThrureflR"
|
+ "# Hz ShortR ShortI OpenR OpenI LoadR LoadI"
|
||||||
" ThrureflI IsolationR IsolationI\n"
|
+ (
|
||||||
if self.complete2port() else "\n")
|
" ThroughR ThroughI ThrureflR"
|
||||||
+ "\n".join([
|
" ThrureflI IsolationR IsolationI\n"
|
||||||
f"{self.data.get(freq)}" for freq in self.frequencies()
|
if self.complete2port()
|
||||||
]) + "\n"
|
else "\n"
|
||||||
|
)
|
||||||
|
+ "\n".join(
|
||||||
|
[f"{self.data.get(freq)}" for freq in self.frequencies()]
|
||||||
|
)
|
||||||
|
+ "\n"
|
||||||
)
|
)
|
||||||
if self.complete1port() else ""
|
if self.complete1port()
|
||||||
|
else ""
|
||||||
)
|
)
|
||||||
|
|
||||||
def _append_match(self, m: re.Match, header: str,
|
def _append_match(
|
||||||
line_nr: int, line: str) -> None:
|
self, m: re.Match, header: str, line_nr: int, line: str
|
||||||
|
) -> None:
|
||||||
cal = m.groupdict()
|
cal = m.groupdict()
|
||||||
columns = {
|
columns = {col[:-1] for col in cal.keys() if cal[col] and col != "freq"}
|
||||||
col[:-1] for col in cal.keys() if cal[col] and col != "freq"
|
|
||||||
}
|
|
||||||
if "through" in columns and header == "sol":
|
if "through" in columns and header == "sol":
|
||||||
logger.warning("Through data with sol header. %i: %s",
|
logger.warning(
|
||||||
line_nr, line)
|
"Through data with sol header. %i: %s", line_nr, line
|
||||||
|
)
|
||||||
# fix short data (without thrurefl)
|
# fix short data (without thrurefl)
|
||||||
if "thrurefl" in columns and "isolation" not in columns:
|
if "thrurefl" in columns and "isolation" not in columns:
|
||||||
cal["isolationr"] = cal["thrureflr"]
|
cal["isolationr"] = cal["thrureflr"]
|
||||||
|
@ -166,11 +180,14 @@ class CalDataSet(UserDict):
|
||||||
for name in columns:
|
for name in columns:
|
||||||
self.insert(
|
self.insert(
|
||||||
name,
|
name,
|
||||||
Datapoint(int(cal["freq"]),
|
Datapoint(
|
||||||
float(cal[f"{name}r"]),
|
int(cal["freq"]),
|
||||||
float(cal[f"{name}i"])))
|
float(cal[f"{name}r"]),
|
||||||
|
float(cal[f"{name}i"]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def from_str(self, text: str) -> 'CalDataSet':
|
def from_str(self, text: str) -> "CalDataSet":
|
||||||
# reset data
|
# reset data
|
||||||
self.notes = ""
|
self.notes = ""
|
||||||
self.data = defaultdict(CalData)
|
self.data = defaultdict(CalData)
|
||||||
|
@ -185,7 +202,8 @@ class CalDataSet(UserDict):
|
||||||
if m := RXP_CAL_HEADER.search(line):
|
if m := RXP_CAL_HEADER.search(line):
|
||||||
if header:
|
if header:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Duplicate header in cal data. %i: %s", i, line)
|
"Duplicate header in cal data. %i: %s", i, line
|
||||||
|
)
|
||||||
header = "through" if m.group("through") else "sol"
|
header = "through" if m.group("through") else "sol"
|
||||||
continue
|
continue
|
||||||
if not line or line.startswith("#"):
|
if not line or line.startswith("#"):
|
||||||
|
@ -197,19 +215,26 @@ class CalDataSet(UserDict):
|
||||||
continue
|
continue
|
||||||
if not header:
|
if not header:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Caldata without having read header: %i: %s", i, line)
|
"Caldata without having read header: %i: %s", i, line
|
||||||
|
)
|
||||||
self._append_match(m, header, line, i)
|
self._append_match(m, header, line, i)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def insert(self, name: str, dp: Datapoint):
|
def insert(self, name: str, dp: Datapoint):
|
||||||
if name not in {'short', 'open', 'load',
|
if name not in {
|
||||||
'through', 'thrurefl', 'isolation'}:
|
"short",
|
||||||
|
"open",
|
||||||
|
"load",
|
||||||
|
"through",
|
||||||
|
"thrurefl",
|
||||||
|
"isolation",
|
||||||
|
}:
|
||||||
raise KeyError(name)
|
raise KeyError(name)
|
||||||
freq = dp.freq
|
freq = dp.freq
|
||||||
setattr(self.data[freq], name, (dp.z))
|
setattr(self.data[freq], name, (dp.z))
|
||||||
self.data[freq].freq = freq
|
self.data[freq].freq = freq
|
||||||
|
|
||||||
def frequencies(self) -> List[int]:
|
def frequencies(self) -> list[int]:
|
||||||
return sorted(self.data.keys())
|
return sorted(self.data.keys())
|
||||||
|
|
||||||
def get(self, key: int, default: CalData = None) -> CalData:
|
def get(self, key: int, default: CalData = None) -> CalData:
|
||||||
|
@ -223,9 +248,7 @@ class CalDataSet(UserDict):
|
||||||
yield self.get(freq)
|
yield self.get(freq)
|
||||||
|
|
||||||
def size_of(self, name: str) -> int:
|
def size_of(self, name: str) -> int:
|
||||||
return len(
|
return len([True for val in self.data.values() if getattr(val, name)])
|
||||||
[True for val in self.data.values() if getattr(val, name)]
|
|
||||||
)
|
|
||||||
|
|
||||||
def complete1port(self) -> bool:
|
def complete1port(self) -> bool:
|
||||||
for val in self.data.values():
|
for val in self.data.values():
|
||||||
|
@ -244,7 +267,6 @@ class CalDataSet(UserDict):
|
||||||
|
|
||||||
class Calibration:
|
class Calibration:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
self.notes = []
|
self.notes = []
|
||||||
self.dataset = CalDataSet()
|
self.dataset = CalDataSet()
|
||||||
self.cal_element = CalElement()
|
self.cal_element = CalElement()
|
||||||
|
@ -253,7 +275,7 @@ class Calibration:
|
||||||
|
|
||||||
self.source = "Manual"
|
self.source = "Manual"
|
||||||
|
|
||||||
def insert(self, name: str, data: List[Datapoint]):
|
def insert(self, name: str, data: list[Datapoint]):
|
||||||
for dp in data:
|
for dp in data:
|
||||||
self.dataset.insert(name, dp)
|
self.dataset.insert(name, dp)
|
||||||
|
|
||||||
|
@ -278,18 +300,30 @@ class Calibration:
|
||||||
gm2 = cal.open
|
gm2 = cal.open
|
||||||
gm3 = cal.load
|
gm3 = cal.load
|
||||||
|
|
||||||
denominator = (g1 * (g2 - g3) * gm1 +
|
denominator = (
|
||||||
g2 * g3 * gm2 - g2 * g3 * gm3 -
|
g1 * (g2 - g3) * gm1
|
||||||
(g2 * gm2 - g3 * gm3) * g1)
|
+ g2 * g3 * gm2
|
||||||
cal.e00 = - ((g2 * gm3 - g3 * gm3) * g1 * gm2 -
|
- g2 * g3 * gm3
|
||||||
(g2 * g3 * gm2 - g2 * g3 * gm3 -
|
- (g2 * gm2 - g3 * gm3) * g1
|
||||||
(g3 * gm2 - g2 * gm3) * g1) * gm1
|
)
|
||||||
) / denominator
|
cal.e00 = (
|
||||||
cal.e11 = ((g2 - g3) * gm1 - g1 * (gm2 - gm3) +
|
-(
|
||||||
g3 * gm2 - g2 * gm3) / denominator
|
(g2 * gm3 - g3 * gm3) * g1 * gm2
|
||||||
cal.delta_e = - ((g1 * (gm2 - gm3) - g2 * gm2 + g3 *
|
- (g2 * g3 * gm2 - g2 * g3 * gm3 - (g3 * gm2 - g2 * gm3) * g1)
|
||||||
gm3) * gm1 + (g2 * gm3 - g3 * gm3) *
|
* gm1
|
||||||
gm2) / denominator
|
)
|
||||||
|
/ denominator
|
||||||
|
)
|
||||||
|
cal.e11 = (
|
||||||
|
(g2 - g3) * gm1 - g1 * (gm2 - gm3) + g3 * gm2 - g2 * gm3
|
||||||
|
) / denominator
|
||||||
|
cal.delta_e = (
|
||||||
|
-(
|
||||||
|
(g1 * (gm2 - gm3) - g2 * gm2 + g3 * gm3) * gm1
|
||||||
|
+ (g2 * gm3 - g3 * gm3) * gm2
|
||||||
|
)
|
||||||
|
/ denominator
|
||||||
|
)
|
||||||
|
|
||||||
def _calc_port_2(self, freq: int, cal: CalData):
|
def _calc_port_2(self, freq: int, cal: CalData):
|
||||||
gt = self.gamma_through(freq)
|
gt = self.gamma_through(freq)
|
||||||
|
@ -301,18 +335,16 @@ class Calibration:
|
||||||
|
|
||||||
cal.e30 = cal.isolation
|
cal.e30 = cal.isolation
|
||||||
cal.e10e01 = cal.e00 * cal.e11 - cal.delta_e
|
cal.e10e01 = cal.e00 * cal.e11 - cal.delta_e
|
||||||
cal.e22 = gm7 / (
|
cal.e22 = gm7 / (gm7 * cal.e11 * gt**2 + cal.e10e01 * gt**2)
|
||||||
gm7 * cal.e11 * gt ** 2 + cal.e10e01 * gt ** 2)
|
cal.e10e32 = (gm4 - gm6) * (1 - cal.e11 * cal.e22 * gt**2) / gt
|
||||||
cal.e10e32 = (gm4 - gm6) * (
|
|
||||||
1 - cal.e11 * cal.e22 * gt ** 2) / gt
|
|
||||||
|
|
||||||
def calc_corrections(self):
|
def calc_corrections(self):
|
||||||
if not self.isValid1Port():
|
if not self.isValid1Port():
|
||||||
logger.warning(
|
logger.warning("Tried to calibrate from insufficient data.")
|
||||||
"Tried to calibrate from insufficient data.")
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"All of short, open and load calibration steps"
|
"All of short, open and load calibration steps"
|
||||||
"must be completed for calibration to be applied.")
|
"must be completed for calibration to be applied."
|
||||||
|
)
|
||||||
logger.debug("Calculating calibration for %d points.", self.size())
|
logger.debug("Calculating calibration for %d points.", self.size())
|
||||||
|
|
||||||
for freq, caldata in self.dataset.items():
|
for freq, caldata in self.dataset.items():
|
||||||
|
@ -324,10 +356,12 @@ class Calibration:
|
||||||
self.isCalculated = False
|
self.isCalculated = False
|
||||||
logger.error(
|
logger.error(
|
||||||
"Division error - did you use the same measurement"
|
"Division error - did you use the same measurement"
|
||||||
" for two of short, open and load?")
|
" for two of short, open and load?"
|
||||||
|
)
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Two of short, open and load returned the same"
|
f"Two of short, open and load returned the same"
|
||||||
f" values at frequency {freq}Hz.") from exc
|
f" values at frequency {freq}Hz."
|
||||||
|
) from exc
|
||||||
|
|
||||||
self.gen_interpolation()
|
self.gen_interpolation()
|
||||||
self.isCalculated = True
|
self.isCalculated = True
|
||||||
|
@ -338,25 +372,47 @@ class Calibration:
|
||||||
return IDEAL_SHORT
|
return IDEAL_SHORT
|
||||||
logger.debug("Using short calibration set values.")
|
logger.debug("Using short calibration set values.")
|
||||||
cal_element = self.cal_element
|
cal_element = self.cal_element
|
||||||
Zsp = complex(0.0, 2.0 * math.pi * freq * (
|
Zsp = complex(
|
||||||
cal_element.short_l0 + cal_element.short_l1 * freq +
|
0.0,
|
||||||
cal_element.short_l2 * freq**2 + cal_element.short_l3 * freq**3))
|
2.0
|
||||||
|
* math.pi
|
||||||
|
* freq
|
||||||
|
* (
|
||||||
|
cal_element.short_l0
|
||||||
|
+ cal_element.short_l1 * freq
|
||||||
|
+ cal_element.short_l2 * freq**2
|
||||||
|
+ cal_element.short_l3 * freq**3
|
||||||
|
),
|
||||||
|
)
|
||||||
# Referencing https://arxiv.org/pdf/1606.02446.pdf (18) - (21)
|
# Referencing https://arxiv.org/pdf/1606.02446.pdf (18) - (21)
|
||||||
return (Zsp / 50.0 - 1.0) / (Zsp / 50.0 + 1.0) * cmath.exp(
|
return (
|
||||||
complex(0.0,
|
(Zsp / 50.0 - 1.0)
|
||||||
-4.0 * math.pi * freq * cal_element.short_length))
|
/ (Zsp / 50.0 + 1.0)
|
||||||
|
* cmath.exp(
|
||||||
|
complex(0.0, -4.0 * math.pi * freq * cal_element.short_length)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def gamma_open(self, freq: int) -> complex:
|
def gamma_open(self, freq: int) -> complex:
|
||||||
if self.cal_element.open_is_ideal:
|
if self.cal_element.open_is_ideal:
|
||||||
return IDEAL_OPEN
|
return IDEAL_OPEN
|
||||||
logger.debug("Using open calibration set values.")
|
logger.debug("Using open calibration set values.")
|
||||||
cal_element = self.cal_element
|
cal_element = self.cal_element
|
||||||
Zop = complex(0.0, 2.0 * math.pi * freq * (
|
Zop = complex(
|
||||||
cal_element.open_c0 + cal_element.open_c1 * freq +
|
0.0,
|
||||||
cal_element.open_c2 * freq**2 + cal_element.open_c3 * freq**3))
|
2.0
|
||||||
|
* math.pi
|
||||||
|
* freq
|
||||||
|
* (
|
||||||
|
cal_element.open_c0
|
||||||
|
+ cal_element.open_c1 * freq
|
||||||
|
+ cal_element.open_c2 * freq**2
|
||||||
|
+ cal_element.open_c3 * freq**3
|
||||||
|
),
|
||||||
|
)
|
||||||
return ((1.0 - 50.0 * Zop) / (1.0 + 50.0 * Zop)) * cmath.exp(
|
return ((1.0 - 50.0 * Zop) / (1.0 + 50.0 * Zop)) * cmath.exp(
|
||||||
complex(0.0,
|
complex(0.0, -4.0 * math.pi * freq * cal_element.open_length)
|
||||||
-4.0 * math.pi * freq * cal_element.open_length))
|
)
|
||||||
|
|
||||||
def gamma_load(self, freq: int) -> complex:
|
def gamma_load(self, freq: int) -> complex:
|
||||||
if self.cal_element.load_is_ideal:
|
if self.cal_element.load_is_ideal:
|
||||||
|
@ -367,11 +423,17 @@ class Calibration:
|
||||||
if cal_element.load_c > 0.0:
|
if cal_element.load_c > 0.0:
|
||||||
Zl = cal_element.load_r / complex(
|
Zl = cal_element.load_r / complex(
|
||||||
1.0,
|
1.0,
|
||||||
2.0 * cal_element.load_r * math.pi * freq * cal_element.load_c)
|
2.0 * cal_element.load_r * math.pi * freq * cal_element.load_c,
|
||||||
|
)
|
||||||
if cal_element.load_l > 0.0:
|
if cal_element.load_l > 0.0:
|
||||||
Zl = Zl + complex(0.0, 2 * math.pi * freq * cal_element.load_l)
|
Zl = Zl + complex(0.0, 2 * math.pi * freq * cal_element.load_l)
|
||||||
return (Zl / 50.0 - 1.0) / (Zl / 50.0 + 1.0) * cmath.exp(
|
return (
|
||||||
complex(0.0, -4 * math.pi * freq * cal_element.load_length))
|
(Zl / 50.0 - 1.0)
|
||||||
|
/ (Zl / 50.0 + 1.0)
|
||||||
|
* cmath.exp(
|
||||||
|
complex(0.0, -4 * math.pi * freq * cal_element.load_length)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def gamma_through(self, freq: int) -> complex:
|
def gamma_through(self, freq: int) -> complex:
|
||||||
if self.cal_element.through_is_ideal:
|
if self.cal_element.through_is_ideal:
|
||||||
|
@ -379,59 +441,103 @@ class Calibration:
|
||||||
logger.debug("Using through calibration set values.")
|
logger.debug("Using through calibration set values.")
|
||||||
cal_element = self.cal_element
|
cal_element = self.cal_element
|
||||||
return cmath.exp(
|
return cmath.exp(
|
||||||
complex(0.0, -2.0 * math.pi * cal_element.through_length * freq))
|
complex(0.0, -2.0 * math.pi * cal_element.through_length * freq)
|
||||||
|
)
|
||||||
|
|
||||||
def gen_interpolation(self):
|
def gen_interpolation(self):
|
||||||
(freq, e00, e11, delta_e, e10e01, e30, e22, e10e32) = zip(*[
|
(freq, e00, e11, delta_e, e10e01, e30, e22, e10e32) = zip(
|
||||||
(c.freq, c.e00, c.e11, c.delta_e, c.e10e01, c.e30, c.e22, c.e10e32)
|
*[
|
||||||
for c in self.dataset.values()])
|
(
|
||||||
|
c.freq,
|
||||||
|
c.e00,
|
||||||
|
c.e11,
|
||||||
|
c.delta_e,
|
||||||
|
c.e10e01,
|
||||||
|
c.e30,
|
||||||
|
c.e22,
|
||||||
|
c.e10e32,
|
||||||
|
)
|
||||||
|
for c in self.dataset.values()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
self.interp = {
|
self.interp = {
|
||||||
"e00": interp1d(freq, e00,
|
"e00": interp1d(
|
||||||
kind="slinear", bounds_error=False,
|
freq,
|
||||||
fill_value=(e00[0], e00[-1])),
|
e00,
|
||||||
"e11": interp1d(freq, e11,
|
kind="slinear",
|
||||||
kind="slinear", bounds_error=False,
|
bounds_error=False,
|
||||||
fill_value=(e11[0], e11[-1])),
|
fill_value=(e00[0], e00[-1]),
|
||||||
"delta_e": interp1d(freq, delta_e,
|
),
|
||||||
kind="slinear", bounds_error=False,
|
"e11": interp1d(
|
||||||
fill_value=(delta_e[0], delta_e[-1])),
|
freq,
|
||||||
"e10e01": interp1d(freq, e10e01,
|
e11,
|
||||||
kind="slinear", bounds_error=False,
|
kind="slinear",
|
||||||
fill_value=(e10e01[0], e10e01[-1])),
|
bounds_error=False,
|
||||||
"e30": interp1d(freq, e30,
|
fill_value=(e11[0], e11[-1]),
|
||||||
kind="slinear", bounds_error=False,
|
),
|
||||||
fill_value=(e30[0], e30[-1])),
|
"delta_e": interp1d(
|
||||||
"e22": interp1d(freq, e22,
|
freq,
|
||||||
kind="slinear", bounds_error=False,
|
delta_e,
|
||||||
fill_value=(e22[0], e22[-1])),
|
kind="slinear",
|
||||||
"e10e32": interp1d(freq, e10e32,
|
bounds_error=False,
|
||||||
kind="slinear", bounds_error=False,
|
fill_value=(delta_e[0], delta_e[-1]),
|
||||||
fill_value=(e10e32[0], e10e32[-1])),
|
),
|
||||||
|
"e10e01": interp1d(
|
||||||
|
freq,
|
||||||
|
e10e01,
|
||||||
|
kind="slinear",
|
||||||
|
bounds_error=False,
|
||||||
|
fill_value=(e10e01[0], e10e01[-1]),
|
||||||
|
),
|
||||||
|
"e30": interp1d(
|
||||||
|
freq,
|
||||||
|
e30,
|
||||||
|
kind="slinear",
|
||||||
|
bounds_error=False,
|
||||||
|
fill_value=(e30[0], e30[-1]),
|
||||||
|
),
|
||||||
|
"e22": interp1d(
|
||||||
|
freq,
|
||||||
|
e22,
|
||||||
|
kind="slinear",
|
||||||
|
bounds_error=False,
|
||||||
|
fill_value=(e22[0], e22[-1]),
|
||||||
|
),
|
||||||
|
"e10e32": interp1d(
|
||||||
|
freq,
|
||||||
|
e10e32,
|
||||||
|
kind="slinear",
|
||||||
|
bounds_error=False,
|
||||||
|
fill_value=(e10e32[0], e10e32[-1]),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
def correct11(self, dp: Datapoint):
|
def correct11(self, dp: Datapoint):
|
||||||
i = self.interp
|
i = self.interp
|
||||||
s11 = (dp.z - i["e00"](dp.freq)) / (
|
s11 = (dp.z - i["e00"](dp.freq)) / (
|
||||||
(dp.z * i["e11"](dp.freq)) - i["delta_e"](dp.freq))
|
(dp.z * i["e11"](dp.freq)) - i["delta_e"](dp.freq)
|
||||||
|
)
|
||||||
return Datapoint(dp.freq, s11.real, s11.imag)
|
return Datapoint(dp.freq, s11.real, s11.imag)
|
||||||
|
|
||||||
def correct21(self, dp: Datapoint, dp11: Datapoint):
|
def correct21(self, dp: Datapoint, dp11: Datapoint):
|
||||||
i = self.interp
|
i = self.interp
|
||||||
s21 = (dp.z - i["e30"](dp.freq)) / i["e10e32"](dp.freq)
|
s21 = (dp.z - i["e30"](dp.freq)) / i["e10e32"](dp.freq)
|
||||||
s21 = s21 * (i["e10e01"](dp.freq) / (i["e11"](dp.freq)
|
s21 = s21 * (
|
||||||
* dp11.z - i["delta_e"](dp.freq)))
|
i["e10e01"](dp.freq)
|
||||||
|
/ (i["e11"](dp.freq) * dp11.z - i["delta_e"](dp.freq))
|
||||||
|
)
|
||||||
return Datapoint(dp.freq, s21.real, s21.imag)
|
return Datapoint(dp.freq, s21.real, s21.imag)
|
||||||
|
|
||||||
def save(self, filename: str):
|
def save(self, filename: str):
|
||||||
self.dataset.notes = "\n".join(self.notes)
|
self.dataset.notes = "\n".join(self.notes)
|
||||||
if not self.isValid1Port():
|
if not self.isValid1Port():
|
||||||
raise ValueError("Not a valid calibration")
|
raise ValueError("Not a valid calibration")
|
||||||
with open(filename, mode="w", encoding='utf-8') as calfile:
|
with open(filename, mode="w", encoding="utf-8") as calfile:
|
||||||
calfile.write(str(self.dataset))
|
calfile.write(str(self.dataset))
|
||||||
|
|
||||||
def load(self, filename):
|
def load(self, filename):
|
||||||
self.source = os.path.basename(filename)
|
self.source = os.path.basename(filename)
|
||||||
with open(filename, encoding='utf-8') as calfile:
|
with open(filename, encoding="utf-8") as calfile:
|
||||||
self.dataset = CalDataSet().from_str(calfile.read())
|
self.dataset = CalDataSet().from_str(calfile.read())
|
||||||
self.notes = self.dataset.notes.splitlines()
|
self.notes = self.dataset.notes.splitlines()
|
|
@ -18,9 +18,8 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import math
|
import math
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from PyQt5 import QtGui
|
from PyQt6 import QtGui
|
||||||
|
|
||||||
from NanoVNASaver.RFTools import Datapoint
|
from NanoVNASaver.RFTools import Datapoint
|
||||||
from NanoVNASaver.Charts.Chart import Chart
|
from NanoVNASaver.Charts.Chart import Chart
|
||||||
|
@ -33,11 +32,11 @@ class CombinedLogMagChart(LogMagChart):
|
||||||
def __init__(self, name=""):
|
def __init__(self, name=""):
|
||||||
super().__init__(name)
|
super().__init__(name)
|
||||||
|
|
||||||
self.data11: List[Datapoint] = []
|
self.data11: list[Datapoint] = []
|
||||||
self.data21: List[Datapoint] = []
|
self.data21: list[Datapoint] = []
|
||||||
|
|
||||||
self.reference11: List[Datapoint] = []
|
self.reference11: list[Datapoint] = []
|
||||||
self.reference21: List[Datapoint] = []
|
self.reference21: list[Datapoint] = []
|
||||||
|
|
||||||
def setCombinedData(self, data11, data21):
|
def setCombinedData(self, data11, data21):
|
||||||
self.data11 = data11
|
self.data11 = data11
|
||||||
|
@ -61,20 +60,24 @@ class CombinedLogMagChart(LogMagChart):
|
||||||
|
|
||||||
def drawChart(self, qp: QtGui.QPainter):
|
def drawChart(self, qp: QtGui.QPainter):
|
||||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||||
qp.drawText(int(self.dim.width // 2) - 20,
|
qp.drawText(
|
||||||
15,
|
int(self.dim.width // 2) - 20, 15, f"{self.name} {self.name_unit}"
|
||||||
f"{self.name} {self.name_unit}")
|
)
|
||||||
qp.drawText(10, 15, "S11")
|
qp.drawText(10, 15, "S11")
|
||||||
qp.drawText(self.leftMargin + self.dim.width - 8, 15, "S21")
|
qp.drawText(self.leftMargin + self.dim.width - 8, 15, "S21")
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin,
|
qp.drawLine(
|
||||||
self.topMargin - 5,
|
self.leftMargin,
|
||||||
self.leftMargin,
|
self.topMargin - 5,
|
||||||
self.topMargin + self.dim.height + 5)
|
self.leftMargin,
|
||||||
qp.drawLine(self.leftMargin - 5,
|
self.topMargin + self.dim.height + 5,
|
||||||
self.topMargin + self.dim.height,
|
)
|
||||||
self.leftMargin + self.dim.width,
|
qp.drawLine(
|
||||||
self.topMargin + self.dim.height)
|
self.leftMargin - 5,
|
||||||
|
self.topMargin + self.dim.height,
|
||||||
|
self.leftMargin + self.dim.width,
|
||||||
|
self.topMargin + self.dim.height,
|
||||||
|
)
|
||||||
|
|
||||||
def drawValues(self, qp: QtGui.QPainter):
|
def drawValues(self, qp: QtGui.QPainter):
|
||||||
if len(self.data11) == 0 and len(self.reference11) == 0:
|
if len(self.data11) == 0 and len(self.reference11) == 0:
|
||||||
|
@ -117,8 +120,12 @@ class CombinedLogMagChart(LogMagChart):
|
||||||
pen = QtGui.QPen(c)
|
pen = QtGui.QPen(c)
|
||||||
pen.setWidth(2)
|
pen.setWidth(2)
|
||||||
qp.setPen(pen)
|
qp.setPen(pen)
|
||||||
qp.drawLine(self.leftMargin + self.dim.width - 20, 9,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width - 15, 9)
|
self.leftMargin + self.dim.width - 20,
|
||||||
|
9,
|
||||||
|
self.leftMargin + self.dim.width - 15,
|
||||||
|
9,
|
||||||
|
)
|
||||||
|
|
||||||
if self.reference11:
|
if self.reference11:
|
||||||
c = QtGui.QColor(Chart.color.reference)
|
c = QtGui.QColor(Chart.color.reference)
|
||||||
|
@ -132,8 +139,12 @@ class CombinedLogMagChart(LogMagChart):
|
||||||
pen = QtGui.QPen(c)
|
pen = QtGui.QPen(c)
|
||||||
pen.setWidth(2)
|
pen.setWidth(2)
|
||||||
qp.setPen(pen)
|
qp.setPen(pen)
|
||||||
qp.drawLine(self.leftMargin + self.dim.width - 20, 14,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width - 15, 14)
|
self.leftMargin + self.dim.width - 20,
|
||||||
|
14,
|
||||||
|
self.leftMargin + self.dim.width - 15,
|
||||||
|
14,
|
||||||
|
)
|
||||||
|
|
||||||
self.drawData(qp, self.data11, Chart.color.sweep)
|
self.drawData(qp, self.data11, Chart.color.sweep)
|
||||||
self.drawData(qp, self.data21, Chart.color.sweep_secondary)
|
self.drawData(qp, self.data21, Chart.color.sweep_secondary)
|
|
@ -19,11 +19,11 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dataclasses import dataclass, field, replace
|
from dataclasses import dataclass, field, replace
|
||||||
from typing import List, Set, Tuple, ClassVar, Any, Optional
|
from typing import ClassVar, Any
|
||||||
|
|
||||||
from PyQt5 import QtWidgets, QtGui, QtCore
|
from PyQt6 import QtWidgets, QtGui, QtCore
|
||||||
from PyQt5.QtCore import pyqtSignal
|
from PyQt6.QtCore import pyqtSignal, Qt
|
||||||
from PyQt5.QtGui import QColor
|
from PyQt6.QtGui import QColor, QColorConstants, QAction
|
||||||
|
|
||||||
from NanoVNASaver import Defaults
|
from NanoVNASaver import Defaults
|
||||||
from NanoVNASaver.RFTools import Datapoint
|
from NanoVNASaver.RFTools import Datapoint
|
||||||
|
@ -34,17 +34,24 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ChartColors: # pylint: disable=too-many-instance-attributes
|
class ChartColors: # pylint: disable=too-many-instance-attributes
|
||||||
background: QColor = field(default_factory=lambda: QColor(QtCore.Qt.white))
|
background: QColor = field(
|
||||||
|
default_factory=lambda: QColor(QColorConstants.White)
|
||||||
|
)
|
||||||
foreground: QColor = field(
|
foreground: QColor = field(
|
||||||
default_factory=lambda: QColor(QtCore.Qt.lightGray))
|
default_factory=lambda: QColor(QColorConstants.LightGray)
|
||||||
|
)
|
||||||
reference: QColor = field(default_factory=lambda: QColor(0, 0, 255, 64))
|
reference: QColor = field(default_factory=lambda: QColor(0, 0, 255, 64))
|
||||||
reference_secondary: QColor = field(
|
reference_secondary: QColor = field(
|
||||||
default_factory=lambda: QColor(0, 0, 192, 48))
|
default_factory=lambda: QColor(0, 0, 192, 48)
|
||||||
sweep: QColor = field(default_factory=lambda: QColor(QtCore.Qt.darkYellow))
|
)
|
||||||
|
sweep: QColor = field(
|
||||||
|
default_factory=lambda: QColor(QColorConstants.DarkYellow)
|
||||||
|
)
|
||||||
sweep_secondary: QColor = field(
|
sweep_secondary: QColor = field(
|
||||||
default_factory=lambda: QColor(QtCore.Qt.darkMagenta))
|
default_factory=lambda: QColor(QColorConstants.DarkMagenta)
|
||||||
|
)
|
||||||
swr: QColor = field(default_factory=lambda: QColor(255, 0, 0, 128))
|
swr: QColor = field(default_factory=lambda: QColor(255, 0, 0, 128))
|
||||||
text: QColor = field(default_factory=lambda: QColor(QtCore.Qt.black))
|
text: QColor = field(default_factory=lambda: QColor(QColorConstants.Black))
|
||||||
bands: QColor = field(default_factory=lambda: QColor(128, 128, 128, 48))
|
bands: QColor = field(default_factory=lambda: QColor(128, 128, 128, 48))
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,8 +67,8 @@ class ChartDimensions:
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ChartDragBox:
|
class ChartDragBox:
|
||||||
pos: Tuple[int] = (-1, -1)
|
pos: tuple[int] = (-1, -1)
|
||||||
pos_start: Tuple[int] = (0, 0)
|
pos_start: tuple[int] = (0, 0)
|
||||||
state: bool = False
|
state: bool = False
|
||||||
move_x: int = -1
|
move_x: int = -1
|
||||||
move_y: int = -1
|
move_y: int = -1
|
||||||
|
@ -97,8 +104,7 @@ class ChartMarker(QtWidgets.QWidget):
|
||||||
|
|
||||||
if text and Defaults.cfg.chart.marker_label:
|
if text and Defaults.cfg.chart.marker_label:
|
||||||
text_width = self.qp.fontMetrics().horizontalAdvance(text)
|
text_width = self.qp.fontMetrics().horizontalAdvance(text)
|
||||||
self.qp.drawText(x - int(text_width // 2),
|
self.qp.drawText(x - int(text_width // 2), y - 3 - offset, text)
|
||||||
y - 3 - offset, text)
|
|
||||||
|
|
||||||
|
|
||||||
class Chart(QtWidgets.QWidget):
|
class Chart(QtWidgets.QWidget):
|
||||||
|
@ -109,7 +115,7 @@ class Chart(QtWidgets.QWidget):
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.name = name
|
self.name = name
|
||||||
self.sweepTitle = ''
|
self.sweepTitle = ""
|
||||||
|
|
||||||
self.leftMargin = 30
|
self.leftMargin = 30
|
||||||
self.rightMargin = 20
|
self.rightMargin = 20
|
||||||
|
@ -122,22 +128,23 @@ class Chart(QtWidgets.QWidget):
|
||||||
|
|
||||||
self.draggedMarker = None
|
self.draggedMarker = None
|
||||||
|
|
||||||
self.data: List[Datapoint] = []
|
self.data: list[Datapoint] = []
|
||||||
self.reference: List[Datapoint] = []
|
self.reference: list[Datapoint] = []
|
||||||
|
|
||||||
self.markers: List[Marker] = []
|
self.markers: list[Marker] = []
|
||||||
self.swrMarkers: Set[float] = set()
|
self.swrMarkers: set[float] = set()
|
||||||
|
|
||||||
self.action_popout = QtWidgets.QAction("Popout chart")
|
self.action_popout = QAction("Popout chart")
|
||||||
self.action_popout.triggered.connect(
|
self.action_popout.triggered.connect(
|
||||||
lambda: self.popoutRequested.emit(self))
|
lambda: self.popoutRequested.emit(self)
|
||||||
|
)
|
||||||
self.addAction(self.action_popout)
|
self.addAction(self.action_popout)
|
||||||
|
|
||||||
self.action_save_screenshot = QtWidgets.QAction("Save image")
|
self.action_save_screenshot = QAction("Save image")
|
||||||
self.action_save_screenshot.triggered.connect(self.saveScreenshot)
|
self.action_save_screenshot.triggered.connect(self.saveScreenshot)
|
||||||
self.addAction(self.action_save_screenshot)
|
self.addAction(self.action_save_screenshot)
|
||||||
|
|
||||||
self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
|
self.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||||
|
|
||||||
def setReference(self, data):
|
def setReference(self, data):
|
||||||
self.reference = data
|
self.reference = data
|
||||||
|
@ -185,7 +192,7 @@ class Chart(QtWidgets.QWidget):
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
def getNearestMarker(self, x, y) -> Optional[Marker]:
|
def getNearestMarker(self, x, y) -> Marker | None:
|
||||||
if not self.data:
|
if not self.data:
|
||||||
return None
|
return None
|
||||||
shortest = 10**6
|
shortest = 10**6
|
||||||
|
@ -198,7 +205,7 @@ class Chart(QtWidgets.QWidget):
|
||||||
nearest = m
|
nearest = m
|
||||||
return nearest
|
return nearest
|
||||||
|
|
||||||
def getPosition(self, d: Datapoint) -> Tuple[int, int]:
|
def getPosition(self, d: Datapoint) -> tuple[int, int]:
|
||||||
return self.getXPosition(d), self.getYPosition(d)
|
return self.getXPosition(d), self.getYPosition(d)
|
||||||
|
|
||||||
def setDrawLines(self, draw_lines):
|
def setDrawLines(self, draw_lines):
|
||||||
|
@ -206,22 +213,27 @@ class Chart(QtWidgets.QWidget):
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
|
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
|
||||||
if event.buttons() == QtCore.Qt.RightButton:
|
if event.buttons() == Qt.MouseButton.RightButton:
|
||||||
event.ignore()
|
event.ignore()
|
||||||
return
|
return
|
||||||
if event.buttons() == QtCore.Qt.MiddleButton:
|
if event.buttons() == Qt.MouseButton.MiddleButton:
|
||||||
# Drag event
|
# Drag event
|
||||||
event.accept()
|
event.accept()
|
||||||
self.dragbox.move_x = event.x()
|
self.dragbox.move_x = event.position().x()
|
||||||
self.dragbox.move_y = event.y()
|
self.dragbox.move_y = event.position().y()
|
||||||
return
|
return
|
||||||
if event.modifiers() == QtCore.Qt.ControlModifier:
|
if event.modifiers() == Qt.KeyboardModifier.ControlModifier:
|
||||||
event.accept()
|
event.accept()
|
||||||
self.dragbox.state = True
|
self.dragbox.state = True
|
||||||
self.dragbox.pos_start = (event.x(), event.y())
|
self.dragbox.pos_start = (
|
||||||
|
event.position().x(),
|
||||||
|
event.position().y(),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
if event.modifiers() == QtCore.Qt.ShiftModifier:
|
if event.modifiers() == Qt.KeyboardModifier.ShiftModifier:
|
||||||
self.draggedMarker = self.getNearestMarker(event.x(), event.y())
|
self.draggedMarker = self.getNearestMarker(
|
||||||
|
event.position().x(), event.position().y()
|
||||||
|
)
|
||||||
self.mouseMoveEvent(event)
|
self.mouseMoveEvent(event)
|
||||||
|
|
||||||
def mouseReleaseEvent(self, a0: QtGui.QMouseEvent):
|
def mouseReleaseEvent(self, a0: QtGui.QMouseEvent):
|
||||||
|
@ -230,7 +242,9 @@ class Chart(QtWidgets.QWidget):
|
||||||
self.zoomTo(
|
self.zoomTo(
|
||||||
self.dragbox.pos_start[0],
|
self.dragbox.pos_start[0],
|
||||||
self.dragbox.pos_start[1],
|
self.dragbox.pos_start[1],
|
||||||
a0.x(), a0.y())
|
a0.position().x(),
|
||||||
|
a0.position().y(),
|
||||||
|
)
|
||||||
self.dragbox.state = False
|
self.dragbox.state = False
|
||||||
self.dragbox.pos = (-1, -1)
|
self.dragbox.pos = (-1, -1)
|
||||||
self.dragbox.pos_start = (0, 0)
|
self.dragbox.pos_start = (0, 0)
|
||||||
|
@ -243,8 +257,8 @@ class Chart(QtWidgets.QWidget):
|
||||||
return
|
return
|
||||||
modifiers = a0.modifiers()
|
modifiers = a0.modifiers()
|
||||||
|
|
||||||
zoom_x = modifiers != QtCore.Qt.ShiftModifier
|
zoom_x = modifiers != Qt.KeyboardModifier.ShiftModifier
|
||||||
zoom_y = modifiers != QtCore.Qt.ControlModifier
|
zoom_y = modifiers != Qt.KeyboardModifier.ControlModifier
|
||||||
rate = -delta / 120
|
rate = -delta / 120
|
||||||
# zooming in 10% increments and 9% complementary
|
# zooming in 10% increments and 9% complementary
|
||||||
divisor = 10 if delta > 0 else 9
|
divisor = 10 if delta > 0 else 9
|
||||||
|
@ -252,8 +266,8 @@ class Chart(QtWidgets.QWidget):
|
||||||
factor_x = rate * self.dim.width / divisor if zoom_x else 0
|
factor_x = rate * self.dim.width / divisor if zoom_x else 0
|
||||||
factor_y = rate * self.dim.height / divisor if zoom_y else 0
|
factor_y = rate * self.dim.height / divisor if zoom_y else 0
|
||||||
|
|
||||||
abs_x = max(0, a0.x() - self.leftMargin)
|
abs_x = max(0, a0.position().x() - self.leftMargin)
|
||||||
abs_y = max(0, a0.y() - self.topMargin)
|
abs_y = max(0, a0.position().y() - self.topMargin)
|
||||||
|
|
||||||
ratio_x = abs_x / self.dim.width
|
ratio_x = abs_x / self.dim.width
|
||||||
ratio_y = abs_y / self.dim.height
|
ratio_y = abs_y / self.dim.height
|
||||||
|
@ -262,7 +276,7 @@ class Chart(QtWidgets.QWidget):
|
||||||
int(self.leftMargin + ratio_x * factor_x),
|
int(self.leftMargin + ratio_x * factor_x),
|
||||||
int(self.topMargin + ratio_y * factor_y),
|
int(self.topMargin + ratio_y * factor_y),
|
||||||
int(self.leftMargin + self.dim.width - (1 - ratio_x) * factor_x),
|
int(self.leftMargin + self.dim.width - (1 - ratio_x) * factor_x),
|
||||||
int(self.topMargin + self.dim.height - (1 - ratio_y) * factor_y)
|
int(self.topMargin + self.dim.height - (1 - ratio_y) * factor_y),
|
||||||
)
|
)
|
||||||
a0.accept()
|
a0.accept()
|
||||||
|
|
||||||
|
@ -272,8 +286,10 @@ class Chart(QtWidgets.QWidget):
|
||||||
def saveScreenshot(self):
|
def saveScreenshot(self):
|
||||||
logger.info("Saving %s to file...", self.name)
|
logger.info("Saving %s to file...", self.name)
|
||||||
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
|
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
|
||||||
parent=self, caption="Save image",
|
parent=self,
|
||||||
filter="PNG (*.png);;All files (*.*)")
|
caption="Save image",
|
||||||
|
filter="PNG (*.png);;All files (*.*)",
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug("Filename: %s", filename)
|
logger.debug("Filename: %s", filename)
|
||||||
if not filename:
|
if not filename:
|
||||||
|
@ -314,9 +330,9 @@ class Chart(QtWidgets.QWidget):
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def drawMarker(x: int, y: int,
|
def drawMarker(
|
||||||
qp: QtGui.QPainter, color: QtGui.QColor,
|
x: int, y: int, qp: QtGui.QPainter, color: QtGui.QColor, number: int = 0
|
||||||
number: int = 0):
|
):
|
||||||
cmarker = ChartMarker(qp)
|
cmarker = ChartMarker(qp)
|
||||||
cmarker.draw(x, y, color, f"{number}")
|
cmarker.draw(x, y, color, f"{number}")
|
||||||
|
|
||||||
|
@ -330,6 +346,6 @@ class Chart(QtWidgets.QWidget):
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
pal = self.palette()
|
pal = self.palette()
|
||||||
pal.setColor(QtGui.QPalette.Background, Chart.color.background)
|
pal.setColor(QtGui.QPalette.ColorRole.Window, Chart.color.background)
|
||||||
self.setPalette(pal)
|
self.setPalette(pal)
|
||||||
super().update()
|
super().update()
|
|
@ -18,16 +18,19 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import math
|
import math
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Tuple
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PyQt5 import QtWidgets, QtGui, QtCore
|
from PyQt6 import QtWidgets, QtGui, QtCore
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
|
||||||
from NanoVNASaver.Charts.Chart import Chart
|
from NanoVNASaver.Charts.Chart import Chart
|
||||||
from NanoVNASaver.Formatting import (
|
from NanoVNASaver.Formatting import (
|
||||||
parse_frequency, parse_value,
|
parse_frequency,
|
||||||
format_frequency_chart, format_frequency_chart_2,
|
parse_value,
|
||||||
format_y_axis)
|
format_frequency_chart,
|
||||||
|
format_frequency_chart_2,
|
||||||
|
format_y_axis,
|
||||||
|
)
|
||||||
from NanoVNASaver.RFTools import Datapoint
|
from NanoVNASaver.RFTools import Datapoint
|
||||||
from NanoVNASaver.SITools import Format, Value
|
from NanoVNASaver.SITools import Format, Value
|
||||||
|
|
||||||
|
@ -35,7 +38,6 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class FrequencyChart(Chart):
|
class FrequencyChart(Chart):
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
super().__init__(name)
|
super().__init__(name)
|
||||||
self.maxFrequency = 100000000
|
self.maxFrequency = 100000000
|
||||||
|
@ -66,80 +68,90 @@ class FrequencyChart(Chart):
|
||||||
self.maxValue = 1
|
self.maxValue = 1
|
||||||
self.span = 1
|
self.span = 1
|
||||||
|
|
||||||
self.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
|
self.setContextMenuPolicy(Qt.ContextMenuPolicy.DefaultContextMenu)
|
||||||
mode_group = QtWidgets.QActionGroup(self)
|
mode_group = QtGui.QActionGroup(self)
|
||||||
self.menu = QtWidgets.QMenu()
|
self.menu = QtWidgets.QMenu()
|
||||||
|
|
||||||
self.reset = QtWidgets.QAction("Reset")
|
self.reset = QtGui.QAction("Reset")
|
||||||
self.reset.triggered.connect(self.resetDisplayLimits)
|
self.reset.triggered.connect(self.resetDisplayLimits)
|
||||||
self.menu.addAction(self.reset)
|
self.menu.addAction(self.reset)
|
||||||
|
|
||||||
self.x_menu = QtWidgets.QMenu("Frequency axis")
|
self.x_menu = QtWidgets.QMenu("Frequency axis")
|
||||||
self.action_automatic = QtWidgets.QAction("Automatic")
|
self.action_automatic = QtGui.QAction("Automatic")
|
||||||
self.action_automatic.setCheckable(True)
|
self.action_automatic.setCheckable(True)
|
||||||
self.action_automatic.setChecked(True)
|
self.action_automatic.setChecked(True)
|
||||||
self.action_automatic.changed.connect(
|
self.action_automatic.changed.connect(
|
||||||
lambda: self.setFixedSpan(self.action_fixed_span.isChecked()))
|
lambda: self.setFixedSpan(self.action_fixed_span.isChecked())
|
||||||
self.action_fixed_span = QtWidgets.QAction("Fixed span")
|
)
|
||||||
|
self.action_fixed_span = QtGui.QAction("Fixed span")
|
||||||
self.action_fixed_span.setCheckable(True)
|
self.action_fixed_span.setCheckable(True)
|
||||||
self.action_fixed_span.changed.connect(
|
self.action_fixed_span.changed.connect(
|
||||||
lambda: self.setFixedSpan(self.action_fixed_span.isChecked()))
|
lambda: self.setFixedSpan(self.action_fixed_span.isChecked())
|
||||||
|
)
|
||||||
mode_group.addAction(self.action_automatic)
|
mode_group.addAction(self.action_automatic)
|
||||||
mode_group.addAction(self.action_fixed_span)
|
mode_group.addAction(self.action_fixed_span)
|
||||||
self.x_menu.addAction(self.action_automatic)
|
self.x_menu.addAction(self.action_automatic)
|
||||||
self.x_menu.addAction(self.action_fixed_span)
|
self.x_menu.addAction(self.action_fixed_span)
|
||||||
self.x_menu.addSeparator()
|
self.x_menu.addSeparator()
|
||||||
|
|
||||||
self.action_set_fixed_start = QtWidgets.QAction(
|
self.action_set_fixed_start = QtGui.QAction(
|
||||||
f"Start ({format_frequency_chart(self.minFrequency)})")
|
f"Start ({format_frequency_chart(self.minFrequency)})"
|
||||||
|
)
|
||||||
self.action_set_fixed_start.triggered.connect(self.setMinimumFrequency)
|
self.action_set_fixed_start.triggered.connect(self.setMinimumFrequency)
|
||||||
|
|
||||||
self.action_set_fixed_stop = QtWidgets.QAction(
|
self.action_set_fixed_stop = QtGui.QAction(
|
||||||
f"Stop ({format_frequency_chart(self.maxFrequency)})")
|
f"Stop ({format_frequency_chart(self.maxFrequency)})"
|
||||||
|
)
|
||||||
self.action_set_fixed_stop.triggered.connect(self.setMaximumFrequency)
|
self.action_set_fixed_stop.triggered.connect(self.setMaximumFrequency)
|
||||||
|
|
||||||
self.x_menu.addAction(self.action_set_fixed_start)
|
self.x_menu.addAction(self.action_set_fixed_start)
|
||||||
self.x_menu.addAction(self.action_set_fixed_stop)
|
self.x_menu.addAction(self.action_set_fixed_stop)
|
||||||
|
|
||||||
self.x_menu.addSeparator()
|
self.x_menu.addSeparator()
|
||||||
frequency_mode_group = QtWidgets.QActionGroup(self.x_menu)
|
frequency_mode_group = QtGui.QActionGroup(self.x_menu)
|
||||||
self.action_set_linear_x = QtWidgets.QAction("Linear")
|
self.action_set_linear_x = QtGui.QAction("Linear")
|
||||||
self.action_set_linear_x.setCheckable(True)
|
self.action_set_linear_x.setCheckable(True)
|
||||||
self.action_set_logarithmic_x = QtWidgets.QAction("Logarithmic")
|
self.action_set_logarithmic_x = QtGui.QAction("Logarithmic")
|
||||||
self.action_set_logarithmic_x.setCheckable(True)
|
self.action_set_logarithmic_x.setCheckable(True)
|
||||||
frequency_mode_group.addAction(self.action_set_linear_x)
|
frequency_mode_group.addAction(self.action_set_linear_x)
|
||||||
frequency_mode_group.addAction(self.action_set_logarithmic_x)
|
frequency_mode_group.addAction(self.action_set_logarithmic_x)
|
||||||
self.action_set_linear_x.triggered.connect(
|
self.action_set_linear_x.triggered.connect(
|
||||||
lambda: self.setLogarithmicX(False))
|
lambda: self.setLogarithmicX(False)
|
||||||
|
)
|
||||||
self.action_set_logarithmic_x.triggered.connect(
|
self.action_set_logarithmic_x.triggered.connect(
|
||||||
lambda: self.setLogarithmicX(True))
|
lambda: self.setLogarithmicX(True)
|
||||||
|
)
|
||||||
self.action_set_linear_x.setChecked(True)
|
self.action_set_linear_x.setChecked(True)
|
||||||
self.x_menu.addAction(self.action_set_linear_x)
|
self.x_menu.addAction(self.action_set_linear_x)
|
||||||
self.x_menu.addAction(self.action_set_logarithmic_x)
|
self.x_menu.addAction(self.action_set_logarithmic_x)
|
||||||
|
|
||||||
self.y_menu = QtWidgets.QMenu("Data axis")
|
self.y_menu = QtWidgets.QMenu("Data axis")
|
||||||
self.y_action_automatic = QtWidgets.QAction("Automatic")
|
self.y_action_automatic = QtGui.QAction("Automatic")
|
||||||
self.y_action_automatic.setCheckable(True)
|
self.y_action_automatic.setCheckable(True)
|
||||||
self.y_action_automatic.setChecked(True)
|
self.y_action_automatic.setChecked(True)
|
||||||
self.y_action_automatic.changed.connect(
|
self.y_action_automatic.changed.connect(
|
||||||
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked()))
|
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked())
|
||||||
self.y_action_fixed_span = QtWidgets.QAction("Fixed span")
|
)
|
||||||
|
self.y_action_fixed_span = QtGui.QAction("Fixed span")
|
||||||
self.y_action_fixed_span.setCheckable(True)
|
self.y_action_fixed_span.setCheckable(True)
|
||||||
self.y_action_fixed_span.changed.connect(
|
self.y_action_fixed_span.changed.connect(
|
||||||
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked()))
|
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked())
|
||||||
mode_group = QtWidgets.QActionGroup(self)
|
)
|
||||||
|
mode_group = QtGui.QActionGroup(self)
|
||||||
mode_group.addAction(self.y_action_automatic)
|
mode_group.addAction(self.y_action_automatic)
|
||||||
mode_group.addAction(self.y_action_fixed_span)
|
mode_group.addAction(self.y_action_fixed_span)
|
||||||
self.y_menu.addAction(self.y_action_automatic)
|
self.y_menu.addAction(self.y_action_automatic)
|
||||||
self.y_menu.addAction(self.y_action_fixed_span)
|
self.y_menu.addAction(self.y_action_fixed_span)
|
||||||
self.y_menu.addSeparator()
|
self.y_menu.addSeparator()
|
||||||
|
|
||||||
self.action_set_fixed_minimum = QtWidgets.QAction(
|
self.action_set_fixed_minimum = QtGui.QAction(
|
||||||
f"Minimum ({self.minDisplayValue})")
|
f"Minimum ({self.minDisplayValue})"
|
||||||
|
)
|
||||||
self.action_set_fixed_minimum.triggered.connect(self.setMinimumValue)
|
self.action_set_fixed_minimum.triggered.connect(self.setMinimumValue)
|
||||||
|
|
||||||
self.action_set_fixed_maximum = QtWidgets.QAction(
|
self.action_set_fixed_maximum = QtGui.QAction(
|
||||||
f"Maximum ({self.maxDisplayValue})")
|
f"Maximum ({self.maxDisplayValue})"
|
||||||
|
)
|
||||||
self.action_set_fixed_maximum.triggered.connect(self.setMaximumValue)
|
self.action_set_fixed_maximum.triggered.connect(self.setMaximumValue)
|
||||||
|
|
||||||
self.y_menu.addAction(self.action_set_fixed_maximum)
|
self.y_menu.addAction(self.action_set_fixed_maximum)
|
||||||
|
@ -147,17 +159,19 @@ class FrequencyChart(Chart):
|
||||||
|
|
||||||
if self.logarithmicYAllowed(): # This only works for some plot types
|
if self.logarithmicYAllowed(): # This only works for some plot types
|
||||||
self.y_menu.addSeparator()
|
self.y_menu.addSeparator()
|
||||||
vertical_mode_group = QtWidgets.QActionGroup(self.y_menu)
|
vertical_mode_group = QtGui.QActionGroup(self.y_menu)
|
||||||
self.action_set_linear_y = QtWidgets.QAction("Linear")
|
self.action_set_linear_y = QtGui.QAction("Linear")
|
||||||
self.action_set_linear_y.setCheckable(True)
|
self.action_set_linear_y.setCheckable(True)
|
||||||
self.action_set_logarithmic_y = QtWidgets.QAction("Logarithmic")
|
self.action_set_logarithmic_y = QtGui.QAction("Logarithmic")
|
||||||
self.action_set_logarithmic_y.setCheckable(True)
|
self.action_set_logarithmic_y.setCheckable(True)
|
||||||
vertical_mode_group.addAction(self.action_set_linear_y)
|
vertical_mode_group.addAction(self.action_set_linear_y)
|
||||||
vertical_mode_group.addAction(self.action_set_logarithmic_y)
|
vertical_mode_group.addAction(self.action_set_logarithmic_y)
|
||||||
self.action_set_linear_y.triggered.connect(
|
self.action_set_linear_y.triggered.connect(
|
||||||
lambda: self.setLogarithmicY(False))
|
lambda: self.setLogarithmicY(False)
|
||||||
|
)
|
||||||
self.action_set_logarithmic_y.triggered.connect(
|
self.action_set_logarithmic_y.triggered.connect(
|
||||||
lambda: self.setLogarithmicY(True))
|
lambda: self.setLogarithmicY(True)
|
||||||
|
)
|
||||||
self.action_set_linear_y.setChecked(True)
|
self.action_set_linear_y.setChecked(True)
|
||||||
self.y_menu.addAction(self.action_set_linear_y)
|
self.y_menu.addAction(self.action_set_linear_y)
|
||||||
self.y_menu.addAction(self.action_set_logarithmic_y)
|
self.y_menu.addAction(self.action_set_logarithmic_y)
|
||||||
|
@ -166,20 +180,25 @@ class FrequencyChart(Chart):
|
||||||
self.menu.addMenu(self.y_menu)
|
self.menu.addMenu(self.y_menu)
|
||||||
self.menu.addSeparator()
|
self.menu.addSeparator()
|
||||||
self.menu.addAction(self.action_save_screenshot)
|
self.menu.addAction(self.action_save_screenshot)
|
||||||
self.action_popout = QtWidgets.QAction("Popout chart")
|
self.action_popout = QtGui.QAction("Popout chart")
|
||||||
self.action_popout.triggered.connect(
|
self.action_popout.triggered.connect(
|
||||||
lambda: self.popoutRequested.emit(self))
|
lambda: self.popoutRequested.emit(self)
|
||||||
|
)
|
||||||
self.menu.addAction(self.action_popout)
|
self.menu.addAction(self.action_popout)
|
||||||
self.setFocusPolicy(QtCore.Qt.ClickFocus)
|
self.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
|
||||||
|
|
||||||
self.setMinimumSize(
|
self.setMinimumSize(
|
||||||
self.dim.width + self.rightMargin + self.leftMargin,
|
self.dim.width + self.rightMargin + self.leftMargin,
|
||||||
self.dim.height + self.topMargin + self.bottomMargin)
|
self.dim.height + self.topMargin + self.bottomMargin,
|
||||||
|
)
|
||||||
self.setSizePolicy(
|
self.setSizePolicy(
|
||||||
QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
|
QtWidgets.QSizePolicy(
|
||||||
QtWidgets.QSizePolicy.MinimumExpanding))
|
QtWidgets.QSizePolicy.Policy.MinimumExpanding,
|
||||||
|
QtWidgets.QSizePolicy.Policy.MinimumExpanding,
|
||||||
|
)
|
||||||
|
)
|
||||||
pal = QtGui.QPalette()
|
pal = QtGui.QPalette()
|
||||||
pal.setColor(QtGui.QPalette.Background, Chart.color.background)
|
pal.setColor(QtGui.QPalette.ColorRole.Window, Chart.color.background)
|
||||||
self.setPalette(pal)
|
self.setPalette(pal)
|
||||||
self.setAutoFillBackground(True)
|
self.setAutoFillBackground(True)
|
||||||
|
|
||||||
|
@ -197,13 +216,17 @@ class FrequencyChart(Chart):
|
||||||
|
|
||||||
def contextMenuEvent(self, event):
|
def contextMenuEvent(self, event):
|
||||||
self.action_set_fixed_start.setText(
|
self.action_set_fixed_start.setText(
|
||||||
f"Start ({format_frequency_chart(self.minFrequency)})")
|
f"Start ({format_frequency_chart(self.minFrequency)})"
|
||||||
|
)
|
||||||
self.action_set_fixed_stop.setText(
|
self.action_set_fixed_stop.setText(
|
||||||
f"Stop ({format_frequency_chart(self.maxFrequency)})")
|
f"Stop ({format_frequency_chart(self.maxFrequency)})"
|
||||||
|
)
|
||||||
self.action_set_fixed_minimum.setText(
|
self.action_set_fixed_minimum.setText(
|
||||||
f"Minimum ({self.minDisplayValue})")
|
f"Minimum ({self.minDisplayValue})"
|
||||||
|
)
|
||||||
self.action_set_fixed_maximum.setText(
|
self.action_set_fixed_maximum.setText(
|
||||||
f"Maximum ({self.maxDisplayValue})")
|
f"Maximum ({self.maxDisplayValue})"
|
||||||
|
)
|
||||||
|
|
||||||
if self.fixedSpan:
|
if self.fixedSpan:
|
||||||
self.action_fixed_span.setChecked(True)
|
self.action_fixed_span.setChecked(True)
|
||||||
|
@ -215,7 +238,7 @@ class FrequencyChart(Chart):
|
||||||
else:
|
else:
|
||||||
self.y_action_automatic.setChecked(True)
|
self.y_action_automatic.setChecked(True)
|
||||||
|
|
||||||
self.menu.exec_(event.globalPos())
|
self.menu.exec(event.globalPos())
|
||||||
|
|
||||||
def setFixedSpan(self, fixed_span: bool):
|
def setFixedSpan(self, fixed_span: bool):
|
||||||
self.fixedSpan = fixed_span
|
self.fixedSpan = fixed_span
|
||||||
|
@ -242,8 +265,11 @@ class FrequencyChart(Chart):
|
||||||
|
|
||||||
def setMinimumFrequency(self):
|
def setMinimumFrequency(self):
|
||||||
min_freq_str, selected = QtWidgets.QInputDialog.getText(
|
min_freq_str, selected = QtWidgets.QInputDialog.getText(
|
||||||
self, "Start frequency",
|
self,
|
||||||
"Set start frequency", text=str(self.minFrequency))
|
"Start frequency",
|
||||||
|
"Set start frequency",
|
||||||
|
text=str(self.minFrequency),
|
||||||
|
)
|
||||||
if not selected:
|
if not selected:
|
||||||
return
|
return
|
||||||
span = abs(self.maxFrequency - self.minFrequency)
|
span = abs(self.maxFrequency - self.minFrequency)
|
||||||
|
@ -258,8 +284,11 @@ class FrequencyChart(Chart):
|
||||||
|
|
||||||
def setMaximumFrequency(self):
|
def setMaximumFrequency(self):
|
||||||
max_freq_str, selected = QtWidgets.QInputDialog.getText(
|
max_freq_str, selected = QtWidgets.QInputDialog.getText(
|
||||||
self, "Stop frequency",
|
self,
|
||||||
"Set stop frequency", text=str(self.maxFrequency))
|
"Stop frequency",
|
||||||
|
"Set stop frequency",
|
||||||
|
text=str(self.maxFrequency),
|
||||||
|
)
|
||||||
if not selected:
|
if not selected:
|
||||||
return
|
return
|
||||||
span = abs(self.maxFrequency - self.minFrequency)
|
span = abs(self.maxFrequency - self.minFrequency)
|
||||||
|
@ -274,27 +303,33 @@ class FrequencyChart(Chart):
|
||||||
|
|
||||||
def setMinimumValue(self):
|
def setMinimumValue(self):
|
||||||
text, selected = QtWidgets.QInputDialog.getText(
|
text, selected = QtWidgets.QInputDialog.getText(
|
||||||
self, "Minimum value",
|
self,
|
||||||
|
"Minimum value",
|
||||||
"Set minimum value",
|
"Set minimum value",
|
||||||
text=format_y_axis(self.minDisplayValue, self.name_unit))
|
text=format_y_axis(self.minDisplayValue, self.name_unit),
|
||||||
|
)
|
||||||
if not selected:
|
if not selected:
|
||||||
return
|
return
|
||||||
|
text = text.replace("dB", "")
|
||||||
min_val = parse_value(text)
|
min_val = parse_value(text)
|
||||||
yspan = abs(self.maxDisplayValue - self.minDisplayValue)
|
yspan = abs(self.maxDisplayValue - self.minDisplayValue)
|
||||||
self.minDisplayValue = min_val
|
self.minDisplayValue = min_val
|
||||||
if self.minDisplayValue >= self.maxDisplayValue:
|
if self.minDisplayValue >= self.maxDisplayValue:
|
||||||
self.maxDisplayValue = self.minDisplayValue + yspan
|
self.maxDisplayValue = self.minDisplayValue + yspan
|
||||||
# TODO: negativ logarythmical scale
|
# TODO: negativ logarythmical scale
|
||||||
if self.logarithmicY and min_val <= 0:
|
# if self.logarithmicY and min_val <= 0:
|
||||||
self.minDisplayValue = 0.01
|
# self.minDisplayValue = 0.01
|
||||||
self.fixedValues = True
|
self.fixedValues = True
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def setMaximumValue(self):
|
def setMaximumValue(self):
|
||||||
text, selected = QtWidgets.QInputDialog.getText(
|
text, selected = QtWidgets.QInputDialog.getText(
|
||||||
self, "Maximum value",
|
self,
|
||||||
|
"Maximum value",
|
||||||
"Set maximum value",
|
"Set maximum value",
|
||||||
text=format_y_axis(self.maxDisplayValue, self.name_unit))
|
text=format_y_axis(self.maxDisplayValue, self.name_unit),
|
||||||
|
)
|
||||||
|
text = text.replace("dB", "")
|
||||||
if not selected:
|
if not selected:
|
||||||
return
|
return
|
||||||
max_val = parse_value(text)
|
max_val = parse_value(text)
|
||||||
|
@ -323,18 +358,21 @@ class FrequencyChart(Chart):
|
||||||
if self.logarithmicX:
|
if self.logarithmicX:
|
||||||
span = math.log(self.fstop) - math.log(self.fstart)
|
span = math.log(self.fstop) - math.log(self.fstart)
|
||||||
return self.leftMargin + round(
|
return self.leftMargin + round(
|
||||||
self.dim.width * (math.log(d.freq) -
|
self.dim.width
|
||||||
math.log(self.fstart)) / span)
|
* (math.log(d.freq) - math.log(self.fstart))
|
||||||
|
/ span
|
||||||
|
)
|
||||||
return self.leftMargin + round(
|
return self.leftMargin + round(
|
||||||
self.dim.width * (d.freq - self.fstart) / span)
|
self.dim.width * (d.freq - self.fstart) / span
|
||||||
|
)
|
||||||
return math.floor(self.width() / 2)
|
return math.floor(self.width() / 2)
|
||||||
|
|
||||||
def getYPosition(self, d: Datapoint) -> int:
|
def getYPosition(self, d: Datapoint) -> int:
|
||||||
try:
|
try:
|
||||||
return (
|
return self.topMargin + round(
|
||||||
self.topMargin + round(
|
(self.maxValue - self.value_function(d))
|
||||||
(self.maxValue - self.value_function(d)) /
|
/ self.span
|
||||||
self.span * self.dim.height)
|
* self.dim.height
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return self.topMargin
|
return self.topMargin
|
||||||
|
@ -366,7 +404,7 @@ class FrequencyChart(Chart):
|
||||||
step = span / self.dim.width
|
step = span / self.dim.width
|
||||||
return round(self.fstart + absx * step)
|
return round(self.fstart + absx * step)
|
||||||
|
|
||||||
def valueAtPosition(self, y) -> List[float]:
|
def valueAtPosition(self, y) -> list[float]:
|
||||||
"""
|
"""
|
||||||
Returns the chart-specific value(s) at the specified Y-position
|
Returns the chart-specific value(s) at the specified Y-position
|
||||||
:param y: The Y position to calculate for.
|
:param y: The Y position to calculate for.
|
||||||
|
@ -401,31 +439,34 @@ class FrequencyChart(Chart):
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def mouseMoveEvent(self, a0: QtGui.QMouseEvent):
|
def mouseMoveEvent(self, a0: QtGui.QMouseEvent):
|
||||||
if a0.buttons() == QtCore.Qt.RightButton:
|
if a0.buttons() == Qt.MouseButton.RightButton:
|
||||||
a0.ignore()
|
a0.ignore()
|
||||||
return
|
return
|
||||||
if a0.buttons() == QtCore.Qt.MiddleButton:
|
if a0.buttons() == Qt.MouseButton.MiddleButton:
|
||||||
# Drag the display
|
# Drag the display
|
||||||
a0.accept()
|
a0.accept()
|
||||||
if self.dragbox.move_x != -1 and self.dragbox.move_y != -1:
|
if self.dragbox.move_x != -1 and self.dragbox.move_y != -1:
|
||||||
dx = self.dragbox.move_x - a0.x()
|
dx = self.dragbox.move_x - a0.position().x()
|
||||||
dy = self.dragbox.move_y - a0.y()
|
dy = self.dragbox.move_y - a0.position().y()
|
||||||
self.zoomTo(self.leftMargin + dx, self.topMargin + dy,
|
self.zoomTo(
|
||||||
self.leftMargin + self.dim.width + dx,
|
self.leftMargin + dx,
|
||||||
self.topMargin + self.dim.height + dy)
|
self.topMargin + dy,
|
||||||
|
self.leftMargin + self.dim.width + dx,
|
||||||
|
self.topMargin + self.dim.height + dy,
|
||||||
|
)
|
||||||
|
|
||||||
self.dragbox.move_x = a0.x()
|
self.dragbox.move_x = a0.position().x()
|
||||||
self.dragbox.move_y = a0.y()
|
self.dragbox.move_y = a0.position().y()
|
||||||
return
|
return
|
||||||
if a0.modifiers() == QtCore.Qt.ControlModifier:
|
if a0.modifiers() == Qt.KeyboardModifier.ControlModifier:
|
||||||
# Dragging a box
|
# Dragging a box
|
||||||
if not self.dragbox.state:
|
if not self.dragbox.state:
|
||||||
self.dragbox.pos_start = (a0.x(), a0.y())
|
self.dragbox.pos_start = (a0.position().x(), a0.position().y())
|
||||||
self.dragbox.pos = (a0.x(), a0.y())
|
self.dragbox.pos = (a0.position().x(), a0.position().y())
|
||||||
self.update()
|
self.update()
|
||||||
a0.accept()
|
a0.accept()
|
||||||
return
|
return
|
||||||
x = a0.x()
|
x = a0.position().x()
|
||||||
f = self.frequencyAtPosition(x)
|
f = self.frequencyAtPosition(x)
|
||||||
if x == -1:
|
if x == -1:
|
||||||
a0.ignore()
|
a0.ignore()
|
||||||
|
@ -436,10 +477,10 @@ class FrequencyChart(Chart):
|
||||||
m.setFrequency(str(f))
|
m.setFrequency(str(f))
|
||||||
|
|
||||||
def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
|
def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
|
||||||
self.dim.width = (
|
self.dim.width = a0.size().width() - self.rightMargin - self.leftMargin
|
||||||
a0.size().width() - self.rightMargin - self.leftMargin)
|
|
||||||
self.dim.height = (
|
self.dim.height = (
|
||||||
a0.size().height() - self.bottomMargin - self.topMargin)
|
a0.size().height() - self.bottomMargin - self.topMargin
|
||||||
|
)
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def paintEvent(self, _: QtGui.QPaintEvent) -> None:
|
def paintEvent(self, _: QtGui.QPaintEvent) -> None:
|
||||||
|
@ -451,25 +492,31 @@ class FrequencyChart(Chart):
|
||||||
self.drawDragbog(qp)
|
self.drawDragbog(qp)
|
||||||
qp.end()
|
qp.end()
|
||||||
|
|
||||||
def _data_oob(self, data: List[Datapoint]) -> bool:
|
def _data_oob(self, data: list[Datapoint]) -> bool:
|
||||||
return (data[0].freq > self.fstop or self.data[-1].freq < self.fstart)
|
return data[0].freq > self.fstop or self.data[-1].freq < self.fstart
|
||||||
|
|
||||||
def _check_frequency_boundaries(self, qp: QtGui.QPainter):
|
def _check_frequency_boundaries(self, qp: QtGui.QPainter):
|
||||||
if (self.data and self._data_oob(self.data) and
|
if (
|
||||||
(not self.reference or self._data_oob(self.reference))):
|
self.data
|
||||||
|
and self._data_oob(self.data)
|
||||||
|
and (not self.reference or self._data_oob(self.reference))
|
||||||
|
):
|
||||||
# Data outside frequency range
|
# Data outside frequency range
|
||||||
qp.setBackgroundMode(QtCore.Qt.OpaqueMode)
|
qp.setBackgroundMode(Qt.BGMode.OpaqueMode)
|
||||||
qp.setBackground(Chart.color.background)
|
qp.setBackground(Chart.color.background)
|
||||||
qp.setPen(Chart.color.text)
|
qp.setPen(Chart.color.text)
|
||||||
qp.drawText(self.leftMargin + int(self.dim.width // 2) - 70,
|
qp.drawText(
|
||||||
self.topMargin + int(self.dim.height // 2) - 20,
|
self.leftMargin + int(self.dim.width // 2) - 70,
|
||||||
"Data outside frequency span")
|
self.topMargin + int(self.dim.height // 2) - 20,
|
||||||
|
"Data outside frequency span",
|
||||||
|
)
|
||||||
|
|
||||||
def drawDragbog(self, qp: QtGui.QPainter):
|
def drawDragbog(self, qp: QtGui.QPainter):
|
||||||
dashed_pen = QtGui.QPen(Chart.color.foreground, 1, QtCore.Qt.DashLine)
|
dashed_pen = QtGui.QPen(Chart.color.foreground, 1, Qt.PenStyle.DashLine)
|
||||||
qp.setPen(dashed_pen)
|
qp.setPen(dashed_pen)
|
||||||
top_left = QtCore.QPoint(
|
top_left = QtCore.QPoint(
|
||||||
self.dragbox.pos_start[0], self.dragbox.pos_start[1])
|
self.dragbox.pos_start[0], self.dragbox.pos_start[1]
|
||||||
|
)
|
||||||
bottom_right = QtCore.QPoint(self.dragbox.pos[0], self.dragbox.pos[1])
|
bottom_right = QtCore.QPoint(self.dragbox.pos[0], self.dragbox.pos[1])
|
||||||
rect = QtCore.QRect(top_left, bottom_right)
|
rect = QtCore.QRect(top_left, bottom_right)
|
||||||
qp.drawRect(rect)
|
qp.drawRect(rect)
|
||||||
|
@ -481,14 +528,18 @@ class FrequencyChart(Chart):
|
||||||
headline += f" ({self.name_unit})"
|
headline += f" ({self.name_unit})"
|
||||||
qp.drawText(3, 15, headline)
|
qp.drawText(3, 15, headline)
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin,
|
qp.drawLine(
|
||||||
20,
|
self.leftMargin,
|
||||||
self.leftMargin,
|
20,
|
||||||
self.topMargin + self.dim.height + 5)
|
self.leftMargin,
|
||||||
qp.drawLine(self.leftMargin - 5,
|
self.topMargin + self.dim.height + 5,
|
||||||
self.topMargin + self.dim.height,
|
)
|
||||||
self.leftMargin + self.dim.width,
|
qp.drawLine(
|
||||||
self.topMargin + self.dim.height)
|
self.leftMargin - 5,
|
||||||
|
self.topMargin + self.dim.height,
|
||||||
|
self.leftMargin + self.dim.width,
|
||||||
|
self.topMargin + self.dim.height,
|
||||||
|
)
|
||||||
self.drawTitle(qp)
|
self.drawTitle(qp)
|
||||||
|
|
||||||
def drawValues(self, qp: QtGui.QPainter):
|
def drawValues(self, qp: QtGui.QPainter):
|
||||||
|
@ -514,7 +565,8 @@ class FrequencyChart(Chart):
|
||||||
if span == 0:
|
if span == 0:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Span is zero for %s-Chart, setting to a small value.",
|
"Span is zero for %s-Chart, setting to a small value.",
|
||||||
self.name)
|
self.name,
|
||||||
|
)
|
||||||
span = 1e-15
|
span = 1e-15
|
||||||
self.span = span
|
self.span = span
|
||||||
|
|
||||||
|
@ -522,30 +574,37 @@ class FrequencyChart(Chart):
|
||||||
fmt = Format(max_nr_digits=1)
|
fmt = Format(max_nr_digits=1)
|
||||||
for i in range(target_ticks):
|
for i in range(target_ticks):
|
||||||
val = min_value + (i / target_ticks) * span
|
val = min_value + (i / target_ticks) * span
|
||||||
y = self.topMargin + \
|
y = self.topMargin + round(
|
||||||
round((self.maxValue - val) / self.span * self.dim.height)
|
(self.maxValue - val) / self.span * self.dim.height
|
||||||
|
)
|
||||||
qp.setPen(Chart.color.text)
|
qp.setPen(Chart.color.text)
|
||||||
if val != min_value:
|
if val != min_value:
|
||||||
valstr = str(Value(val, fmt=fmt))
|
valstr = str(Value(val, fmt=fmt))
|
||||||
qp.drawText(3, y + 3, valstr)
|
qp.drawText(3, y + 3, valstr)
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin - 5, y,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width, y)
|
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
|
||||||
|
)
|
||||||
|
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin - 5, self.topMargin,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width, self.topMargin)
|
self.leftMargin - 5,
|
||||||
|
self.topMargin,
|
||||||
|
self.leftMargin + self.dim.width,
|
||||||
|
self.topMargin,
|
||||||
|
)
|
||||||
qp.setPen(Chart.color.text)
|
qp.setPen(Chart.color.text)
|
||||||
qp.drawText(3, self.topMargin + 4, str(Value(max_value, fmt=fmt)))
|
qp.drawText(3, self.topMargin + 4, str(Value(max_value, fmt=fmt)))
|
||||||
qp.drawText(3, self.dim.height + self.topMargin,
|
qp.drawText(
|
||||||
str(Value(min_value, fmt=fmt)))
|
3, self.dim.height + self.topMargin, str(Value(min_value, fmt=fmt))
|
||||||
|
)
|
||||||
self.drawFrequencyTicks(qp)
|
self.drawFrequencyTicks(qp)
|
||||||
|
|
||||||
self.drawData(qp, self.data, Chart.color.sweep)
|
self.drawData(qp, self.data, Chart.color.sweep)
|
||||||
self.drawData(qp, self.reference, Chart.color.reference)
|
self.drawData(qp, self.reference, Chart.color.reference)
|
||||||
self.drawMarkers(qp)
|
self.drawMarkers(qp)
|
||||||
|
|
||||||
def _find_scaling(self) -> Tuple[float, float]:
|
def _find_scaling(self) -> tuple[float, float]:
|
||||||
min_value = self.minDisplayValue / 10e11
|
min_value = self.minDisplayValue / 10e11
|
||||||
max_value = self.maxDisplayValue / 10e11
|
max_value = self.maxDisplayValue / 10e11
|
||||||
if self.fixedValues:
|
if self.fixedValues:
|
||||||
|
@ -569,32 +628,36 @@ class FrequencyChart(Chart):
|
||||||
ticks = math.floor(self.dim.width / 100)
|
ticks = math.floor(self.dim.width / 100)
|
||||||
|
|
||||||
# try to adapt format to span
|
# try to adapt format to span
|
||||||
if int(fspan / ticks / self.fstart * 10000) > 2:
|
if self.fstart == 0 or int(fspan / ticks / self.fstart * 10000) > 2:
|
||||||
my_format_frequency = format_frequency_chart
|
my_format_frequency = format_frequency_chart
|
||||||
else:
|
else:
|
||||||
my_format_frequency = format_frequency_chart_2
|
my_format_frequency = format_frequency_chart_2
|
||||||
|
|
||||||
qp.drawText(self.leftMargin - 20,
|
qp.drawText(
|
||||||
self.topMargin + self.dim.height + 15,
|
self.leftMargin - 20,
|
||||||
my_format_frequency(self.fstart))
|
self.topMargin + self.dim.height + 15,
|
||||||
|
my_format_frequency(self.fstart),
|
||||||
|
)
|
||||||
|
|
||||||
for i in range(ticks):
|
for i in range(ticks):
|
||||||
x = self.leftMargin + round((i + 1) * self.dim.width / ticks)
|
x = self.leftMargin + round((i + 1) * self.dim.width / ticks)
|
||||||
if self.logarithmicX:
|
if self.logarithmicX:
|
||||||
fspan = math.log(self.fstop) - math.log(self.fstart)
|
fspan = math.log(self.fstop) - math.log(self.fstart)
|
||||||
freq = round(
|
freq = round(
|
||||||
math.exp(
|
math.exp(((i + 1) * fspan / ticks) + math.log(self.fstart))
|
||||||
((i + 1) * fspan / ticks) +
|
)
|
||||||
math.log(self.fstart)))
|
|
||||||
else:
|
else:
|
||||||
freq = round(fspan / ticks * (i + 1) + self.fstart)
|
freq = round(fspan / ticks * (i + 1) + self.fstart)
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(x, self.topMargin, x,
|
qp.drawLine(
|
||||||
self.topMargin + self.dim.height + 5)
|
x, self.topMargin, x, self.topMargin + self.dim.height + 5
|
||||||
|
)
|
||||||
qp.setPen(Chart.color.text)
|
qp.setPen(Chart.color.text)
|
||||||
qp.drawText(x - 20,
|
qp.drawText(
|
||||||
self.topMargin + self.dim.height + 15,
|
x - 20,
|
||||||
my_format_frequency(freq))
|
self.topMargin + self.dim.height + 15,
|
||||||
|
my_format_frequency(freq),
|
||||||
|
)
|
||||||
|
|
||||||
def drawBands(self, qp, fstart, fstop):
|
def drawBands(self, qp, fstart, fstop):
|
||||||
qp.setBrush(self.bands.color)
|
qp.setBrush(self.bands.color)
|
||||||
|
@ -608,17 +671,24 @@ class FrequencyChart(Chart):
|
||||||
# don't draw if either band not in chart or completely in band
|
# don't draw if either band not in chart or completely in band
|
||||||
if start < fstart < fstop < end or end < fstart or start > fstop:
|
if start < fstart < fstop < end or end < fstart or start > fstop:
|
||||||
continue
|
continue
|
||||||
x_start = max(self.leftMargin + 1,
|
x_start = max(
|
||||||
self.getXPosition(Datapoint(start, 0, 0)))
|
self.leftMargin + 1, self.getXPosition(Datapoint(start, 0, 0))
|
||||||
x_stop = min(self.leftMargin + self.dim.width,
|
)
|
||||||
self.getXPosition(Datapoint(end, 0, 0)))
|
x_stop = min(
|
||||||
qp.drawRect(x_start,
|
self.leftMargin + self.dim.width,
|
||||||
self.topMargin,
|
self.getXPosition(Datapoint(end, 0, 0)),
|
||||||
x_stop - x_start,
|
)
|
||||||
self.dim.height)
|
qp.drawRect(
|
||||||
|
x_start, self.topMargin, x_stop - x_start, self.dim.height
|
||||||
|
)
|
||||||
|
|
||||||
def drawData(self, qp: QtGui.QPainter, data: List[Datapoint],
|
def drawData(
|
||||||
color: QtGui.QColor, y_function=None):
|
self,
|
||||||
|
qp: QtGui.QPainter,
|
||||||
|
data: list[Datapoint],
|
||||||
|
color: QtGui.QColor,
|
||||||
|
y_function=None,
|
||||||
|
):
|
||||||
if y_function is None:
|
if y_function is None:
|
||||||
y_function = self.getYPosition
|
y_function = self.getYPosition
|
||||||
pen = QtGui.QPen(color)
|
pen = QtGui.QPen(color)
|
||||||
|
@ -643,8 +713,7 @@ class FrequencyChart(Chart):
|
||||||
if self.isPlotable(prevx, prevy):
|
if self.isPlotable(prevx, prevy):
|
||||||
qp.drawLine(x, y, prevx, prevy)
|
qp.drawLine(x, y, prevx, prevy)
|
||||||
else:
|
else:
|
||||||
new_x, new_y = self.getPlotable(
|
new_x, new_y = self.getPlotable(x, y, prevx, prevy)
|
||||||
x, y, prevx, prevy)
|
|
||||||
qp.drawLine(x, y, new_x, new_y)
|
qp.drawLine(x, y, new_x, new_y)
|
||||||
elif self.isPlotable(prevx, prevy):
|
elif self.isPlotable(prevx, prevy):
|
||||||
new_x, new_y = self.getPlotable(prevx, prevy, x, y)
|
new_x, new_y = self.getPlotable(prevx, prevy, x, y)
|
||||||
|
@ -663,13 +732,17 @@ class FrequencyChart(Chart):
|
||||||
x = self.getXPosition(data[m.location])
|
x = self.getXPosition(data[m.location])
|
||||||
y = y_function(data[m.location])
|
y = y_function(data[m.location])
|
||||||
if self.isPlotable(x, y):
|
if self.isPlotable(x, y):
|
||||||
self.drawMarker(x, y, qp, m.color,
|
self.drawMarker(
|
||||||
self.markers.index(m) + 1)
|
x, y, qp, m.color, self.markers.index(m) + 1
|
||||||
|
)
|
||||||
|
|
||||||
def isPlotable(self, x, y):
|
def isPlotable(self, x, y):
|
||||||
return y is not None and x is not None and \
|
return (
|
||||||
self.leftMargin <= x <= self.leftMargin + self.dim.width and \
|
y is not None
|
||||||
self.topMargin <= y <= self.topMargin + self.dim.height
|
and x is not None
|
||||||
|
and self.leftMargin <= x <= self.leftMargin + self.dim.width
|
||||||
|
and self.topMargin <= y <= self.topMargin + self.dim.height
|
||||||
|
)
|
||||||
|
|
||||||
def getPlotable(self, x, y, distantx, distanty):
|
def getPlotable(self, x, y, distantx, distanty):
|
||||||
p1 = np.array([x, y])
|
p1 = np.array([x, y])
|
||||||
|
@ -680,8 +753,12 @@ class FrequencyChart(Chart):
|
||||||
p4 = np.array([self.leftMargin + self.dim.width, self.topMargin])
|
p4 = np.array([self.leftMargin + self.dim.width, self.topMargin])
|
||||||
elif distanty > self.topMargin + self.dim.height:
|
elif distanty > self.topMargin + self.dim.height:
|
||||||
p3 = np.array([self.leftMargin, self.topMargin + self.dim.height])
|
p3 = np.array([self.leftMargin, self.topMargin + self.dim.height])
|
||||||
p4 = np.array([self.leftMargin + self.dim.width,
|
p4 = np.array(
|
||||||
self.topMargin + self.dim.height])
|
[
|
||||||
|
self.leftMargin + self.dim.width,
|
||||||
|
self.topMargin + self.dim.height,
|
||||||
|
]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return x, y
|
return x, y
|
||||||
|
|
||||||
|
@ -728,12 +805,14 @@ class FrequencyChart(Chart):
|
||||||
|
|
||||||
def keyPressEvent(self, a0: QtGui.QKeyEvent) -> None:
|
def keyPressEvent(self, a0: QtGui.QKeyEvent) -> None:
|
||||||
m = self.getActiveMarker()
|
m = self.getActiveMarker()
|
||||||
if m is not None and a0.modifiers() == QtCore.Qt.NoModifier:
|
if m is not None and a0.modifiers() == Qt.KeyboardModifier.NoModifier:
|
||||||
if a0.key() in [QtCore.Qt.Key_Down, QtCore.Qt.Key_Left]:
|
if a0.key() in [Qt.Key.Key_Down, Qt.Key.Key_Left]:
|
||||||
m.frequencyInput.keyPressEvent(QtGui.QKeyEvent(
|
m.frequencyInput.keyPressEvent(
|
||||||
a0.type(), QtCore.Qt.Key_Down, a0.modifiers()))
|
QtGui.QKeyEvent(a0.type(), Qt.Key.Key_Down, a0.modifiers())
|
||||||
elif a0.key() in [QtCore.Qt.Key_Up, QtCore.Qt.Key_Right]:
|
)
|
||||||
m.frequencyInput.keyPressEvent(QtGui.QKeyEvent(
|
elif a0.key() in [Qt.Key.Key_Up, Qt.Key.Key_Right]:
|
||||||
a0.type(), QtCore.Qt.Key_Up, a0.modifiers()))
|
m.frequencyInput.keyPressEvent(
|
||||||
|
QtGui.QKeyEvent(a0.type(), Qt.Key.Key_Up, a0.modifiers())
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
super().keyPressEvent(a0)
|
super().keyPressEvent(a0)
|
|
@ -18,15 +18,15 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import math
|
import math
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from PyQt5 import QtGui
|
from PyQt6 import QtGui
|
||||||
|
|
||||||
from NanoVNASaver.Charts.Chart import Chart
|
from NanoVNASaver.Charts.Chart import Chart
|
||||||
from NanoVNASaver.RFTools import Datapoint
|
from NanoVNASaver.RFTools import Datapoint
|
||||||
from .Frequency import FrequencyChart
|
from .Frequency import FrequencyChart
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ class GroupDelayChart(FrequencyChart):
|
||||||
self.groupDelayReference = self.calc_data(self.reference)
|
self.groupDelayReference = self.calc_data(self.reference)
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def calc_data(self, data: List[Datapoint]):
|
def calc_data(self, data: list[Datapoint]):
|
||||||
data_len = len(data)
|
data_len = len(data)
|
||||||
if data_len <= 1:
|
if data_len <= 1:
|
||||||
return []
|
return []
|
||||||
|
@ -124,23 +124,30 @@ class GroupDelayChart(FrequencyChart):
|
||||||
tickcount = math.floor(self.dim.height / 60)
|
tickcount = math.floor(self.dim.height / 60)
|
||||||
for i in range(tickcount):
|
for i in range(tickcount):
|
||||||
delay = min_delay + span * i / tickcount
|
delay = min_delay + span * i / tickcount
|
||||||
y = self.topMargin + \
|
y = self.topMargin + round(
|
||||||
round((self.maxDelay - delay) / self.span * self.dim.height)
|
(self.maxDelay - delay) / self.span * self.dim.height
|
||||||
|
)
|
||||||
if delay not in {min_delay, max_delay}:
|
if delay not in {min_delay, max_delay}:
|
||||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||||
# TODO use format class
|
# TODO use format class
|
||||||
digits = 0 if delay == 0 else max(
|
digits = (
|
||||||
0, min(2, math.floor(3 - math.log10(abs(delay)))))
|
0
|
||||||
|
if delay == 0
|
||||||
|
else max(0, min(2, math.floor(3 - math.log10(abs(delay)))))
|
||||||
|
)
|
||||||
delaystr = str(round(delay, digits if digits != 0 else None))
|
delaystr = str(round(delay, digits if digits != 0 else None))
|
||||||
qp.drawText(3, y + 3, delaystr)
|
qp.drawText(3, y + 3, delaystr)
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin - 5, y,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width, y)
|
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
|
||||||
|
)
|
||||||
|
|
||||||
qp.drawLine(self.leftMargin - 5,
|
qp.drawLine(
|
||||||
self.topMargin,
|
self.leftMargin - 5,
|
||||||
self.leftMargin + self.dim.width,
|
self.topMargin,
|
||||||
self.topMargin)
|
self.leftMargin + self.dim.width,
|
||||||
|
self.topMargin,
|
||||||
|
)
|
||||||
qp.setPen(Chart.color.text)
|
qp.setPen(Chart.color.text)
|
||||||
qp.drawText(3, self.topMargin + 5, str(max_delay))
|
qp.drawText(3, self.topMargin + 5, str(max_delay))
|
||||||
qp.drawText(3, self.dim.height + self.topMargin, str(min_delay))
|
qp.drawText(3, self.dim.height + self.topMargin, str(min_delay))
|
||||||
|
@ -153,15 +160,20 @@ class GroupDelayChart(FrequencyChart):
|
||||||
|
|
||||||
self.drawFrequencyTicks(qp)
|
self.drawFrequencyTicks(qp)
|
||||||
|
|
||||||
self.draw_data(qp, Chart.color.sweep,
|
self.draw_data(qp, Chart.color.sweep, self.data, self.groupDelay)
|
||||||
self.data, self.groupDelay)
|
self.draw_data(
|
||||||
self.draw_data(qp, Chart.color.reference,
|
qp, Chart.color.reference, self.reference, self.groupDelayReference
|
||||||
self.reference, self.groupDelayReference)
|
)
|
||||||
|
|
||||||
self.drawMarkers(qp)
|
self.drawMarkers(qp)
|
||||||
|
|
||||||
def draw_data(self, qp: QtGui.QPainter, color: QtGui.QColor,
|
def draw_data(
|
||||||
data: List[Datapoint], delay: List[Datapoint]):
|
self,
|
||||||
|
qp: QtGui.QPainter,
|
||||||
|
color: QtGui.QColor,
|
||||||
|
data: list[Datapoint],
|
||||||
|
delay: list[Datapoint],
|
||||||
|
):
|
||||||
pen = QtGui.QPen(color)
|
pen = QtGui.QPen(color)
|
||||||
pen.setWidth(self.dim.point)
|
pen.setWidth(self.dim.point)
|
||||||
line_pen = QtGui.QPen(color)
|
line_pen = QtGui.QPen(color)
|
||||||
|
@ -200,9 +212,10 @@ class GroupDelayChart(FrequencyChart):
|
||||||
|
|
||||||
def getYPositionFromDelay(self, delay: float) -> int:
|
def getYPositionFromDelay(self, delay: float) -> int:
|
||||||
return self.topMargin + int(
|
return self.topMargin + int(
|
||||||
(self.maxDelay - delay) / self.span * self.dim.height)
|
(self.maxDelay - delay) / self.span * self.dim.height
|
||||||
|
)
|
||||||
|
|
||||||
def valueAtPosition(self, y) -> List[float]:
|
def valueAtPosition(self, y) -> list[float]:
|
||||||
absy = y - self.topMargin
|
absy = y - self.topMargin
|
||||||
val = -1 * ((absy / self.dim.height * self.span) - self.maxDelay)
|
val = -1 * ((absy / self.dim.height * self.span) - self.maxDelay)
|
||||||
return [val]
|
return [val]
|
|
@ -19,9 +19,8 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import math
|
import math
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from PyQt5 import QtGui
|
from PyQt6 import QtGui
|
||||||
|
|
||||||
from NanoVNASaver.Charts.Chart import Chart
|
from NanoVNASaver.Charts.Chart import Chart
|
||||||
from NanoVNASaver.Charts.Frequency import FrequencyChart
|
from NanoVNASaver.Charts.Frequency import FrequencyChart
|
||||||
|
@ -115,8 +114,12 @@ class LogMagChart(FrequencyChart):
|
||||||
self.draw_db_lines(qp, self.maxValue, self.minValue, ticks)
|
self.draw_db_lines(qp, self.maxValue, self.minValue, ticks)
|
||||||
|
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin - 5, self.topMargin,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width, self.topMargin)
|
self.leftMargin - 5,
|
||||||
|
self.topMargin,
|
||||||
|
self.leftMargin + self.dim.width,
|
||||||
|
self.topMargin,
|
||||||
|
)
|
||||||
qp.setPen(Chart.color.text)
|
qp.setPen(Chart.color.text)
|
||||||
qp.drawText(3, self.topMargin + 4, f"{self.maxValue}")
|
qp.drawText(3, self.topMargin + 4, f"{self.maxValue}")
|
||||||
qp.drawText(3, self.dim.height + self.topMargin, f"{self.minValue}")
|
qp.drawText(3, self.dim.height + self.topMargin, f"{self.minValue}")
|
||||||
|
@ -127,14 +130,17 @@ class LogMagChart(FrequencyChart):
|
||||||
for i in range(ticks.count):
|
for i in range(ticks.count):
|
||||||
db = ticks.first + i * ticks.step
|
db = ticks.first + i * ticks.step
|
||||||
y = self.topMargin + round(
|
y = self.topMargin + round(
|
||||||
(maxValue - db) / self.span * self.dim.height)
|
(maxValue - db) / self.span * self.dim.height
|
||||||
|
)
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin - 5, y,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width, y)
|
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
|
||||||
|
)
|
||||||
if db > minValue and db != maxValue:
|
if db > minValue and db != maxValue:
|
||||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||||
qp.drawText(3, y + 4,
|
qp.drawText(
|
||||||
f"{round(db, 1)}" if ticks.step < 1 else f"{db}")
|
3, y + 4, f"{round(db, 1)}" if ticks.step < 1 else f"{db}"
|
||||||
|
)
|
||||||
|
|
||||||
def draw_swr_markers(self, qp) -> None:
|
def draw_swr_markers(self, qp) -> None:
|
||||||
qp.setPen(Chart.color.swr)
|
qp.setPen(Chart.color.swr)
|
||||||
|
@ -145,9 +151,9 @@ class LogMagChart(FrequencyChart):
|
||||||
if self.isInverted:
|
if self.isInverted:
|
||||||
logMag = logMag * -1
|
logMag = logMag * -1
|
||||||
y = self.topMargin + round(
|
y = self.topMargin + round(
|
||||||
(self.maxValue - logMag) / self.span * self.dim.height)
|
(self.maxValue - logMag) / self.span * self.dim.height
|
||||||
qp.drawLine(self.leftMargin, y,
|
)
|
||||||
self.leftMargin + self.dim.width, y)
|
qp.drawLine(self.leftMargin, y, self.leftMargin + self.dim.width, y)
|
||||||
qp.drawText(self.leftMargin + 3, y - 1, f"VSWR: {vswr}")
|
qp.drawText(self.leftMargin + 3, y - 1, f"VSWR: {vswr}")
|
||||||
|
|
||||||
def getYPosition(self, d: Datapoint) -> int:
|
def getYPosition(self, d: Datapoint) -> int:
|
||||||
|
@ -155,9 +161,10 @@ class LogMagChart(FrequencyChart):
|
||||||
if math.isinf(logMag):
|
if math.isinf(logMag):
|
||||||
return self.topMargin
|
return self.topMargin
|
||||||
return self.topMargin + int(
|
return self.topMargin + int(
|
||||||
(self.maxValue - logMag) / self.span * self.dim.height)
|
(self.maxValue - logMag) / self.span * self.dim.height
|
||||||
|
)
|
||||||
|
|
||||||
def valueAtPosition(self, y) -> List[float]:
|
def valueAtPosition(self, y) -> list[float]:
|
||||||
absy = y - self.topMargin
|
absy = y - self.topMargin
|
||||||
val = -1 * ((absy / self.dim.height * self.span) - self.maxValue)
|
val = -1 * ((absy / self.dim.height * self.span) - self.maxValue)
|
||||||
return [val]
|
return [val]
|
|
@ -18,13 +18,13 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import math
|
import math
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from PyQt5 import QtGui
|
from PyQt6 import QtGui
|
||||||
|
|
||||||
from NanoVNASaver.RFTools import Datapoint
|
from NanoVNASaver.RFTools import Datapoint
|
||||||
from NanoVNASaver.Charts.Chart import Chart
|
from NanoVNASaver.Charts.Chart import Chart
|
||||||
from NanoVNASaver.Charts.Frequency import FrequencyChart
|
from NanoVNASaver.Charts.Frequency import FrequencyChart
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -78,21 +78,28 @@ class MagnitudeChart(FrequencyChart):
|
||||||
target_ticks = int(self.dim.height // 60)
|
target_ticks = int(self.dim.height // 60)
|
||||||
for i in range(target_ticks):
|
for i in range(target_ticks):
|
||||||
val = min_value + i / target_ticks * self.span
|
val = min_value + i / target_ticks * self.span
|
||||||
y = self.topMargin + int((self.maxValue - val) / self.span
|
y = self.topMargin + int(
|
||||||
* self.dim.height)
|
(self.maxValue - val) / self.span * self.dim.height
|
||||||
|
)
|
||||||
qp.setPen(Chart.color.text)
|
qp.setPen(Chart.color.text)
|
||||||
if val != min_value:
|
if val != min_value:
|
||||||
digits = max(0, min(2, math.floor(3 - math.log10(abs(val)))))
|
digits = max(0, min(2, math.floor(3 - math.log10(abs(val)))))
|
||||||
vswrstr = (str(round(val)) if digits == 0 else
|
vswrstr = (
|
||||||
str(round(val, digits)))
|
str(round(val)) if digits == 0 else str(round(val, digits))
|
||||||
|
)
|
||||||
qp.drawText(3, y + 3, vswrstr)
|
qp.drawText(3, y + 3, vswrstr)
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin - 5, y,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width, y)
|
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
|
||||||
|
)
|
||||||
|
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin - 5, self.topMargin,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width, self.topMargin)
|
self.leftMargin - 5,
|
||||||
|
self.topMargin,
|
||||||
|
self.leftMargin + self.dim.width,
|
||||||
|
self.topMargin,
|
||||||
|
)
|
||||||
qp.setPen(Chart.color.text)
|
qp.setPen(Chart.color.text)
|
||||||
qp.drawText(3, self.topMargin + 4, str(max_value))
|
qp.drawText(3, self.topMargin + 4, str(max_value))
|
||||||
qp.drawText(3, self.dim.height + self.topMargin, str(min_value))
|
qp.drawText(3, self.dim.height + self.topMargin, str(min_value))
|
||||||
|
@ -103,10 +110,10 @@ class MagnitudeChart(FrequencyChart):
|
||||||
if vswr <= 1:
|
if vswr <= 1:
|
||||||
continue
|
continue
|
||||||
mag = (vswr - 1) / (vswr + 1)
|
mag = (vswr - 1) / (vswr + 1)
|
||||||
y = self.topMargin + int((self.maxValue - mag) / self.span
|
y = self.topMargin + int(
|
||||||
* self.dim.height)
|
(self.maxValue - mag) / self.span * self.dim.height
|
||||||
qp.drawLine(self.leftMargin, y,
|
)
|
||||||
self.leftMargin + self.dim.width, y)
|
qp.drawLine(self.leftMargin, y, self.leftMargin + self.dim.width, y)
|
||||||
qp.drawText(self.leftMargin + 3, y - 1, f"VSWR: {vswr}")
|
qp.drawText(self.leftMargin + 3, y - 1, f"VSWR: {vswr}")
|
||||||
|
|
||||||
self.drawData(qp, self.data, Chart.color.sweep)
|
self.drawData(qp, self.data, Chart.color.sweep)
|
||||||
|
@ -116,9 +123,10 @@ class MagnitudeChart(FrequencyChart):
|
||||||
def getYPosition(self, d: Datapoint) -> int:
|
def getYPosition(self, d: Datapoint) -> int:
|
||||||
mag = self.magnitude(d)
|
mag = self.magnitude(d)
|
||||||
return self.topMargin + int(
|
return self.topMargin + int(
|
||||||
(self.maxValue - mag) / self.span * self.dim.height)
|
(self.maxValue - mag) / self.span * self.dim.height
|
||||||
|
)
|
||||||
|
|
||||||
def valueAtPosition(self, y) -> List[float]:
|
def valueAtPosition(self, y) -> list[float]:
|
||||||
absy = y - self.topMargin
|
absy = y - self.topMargin
|
||||||
val = -1 * ((absy / self.dim.height * self.span) - self.maxValue)
|
val = -1 * ((absy / self.dim.height * self.span) - self.maxValue)
|
||||||
return [val]
|
return [val]
|
|
@ -18,13 +18,11 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import math
|
import math
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from PyQt5 import QtGui
|
from PyQt6 import QtGui
|
||||||
|
|
||||||
from NanoVNASaver.RFTools import Datapoint
|
from NanoVNASaver.RFTools import Datapoint
|
||||||
from NanoVNASaver.SITools import (
|
from NanoVNASaver.SITools import Format, Value, round_ceil, round_floor
|
||||||
Format, Value, round_ceil, round_floor)
|
|
||||||
from NanoVNASaver.Charts.Chart import Chart
|
from NanoVNASaver.Charts.Chart import Chart
|
||||||
from NanoVNASaver.Charts.Frequency import FrequencyChart
|
from NanoVNASaver.Charts.Frequency import FrequencyChart
|
||||||
from NanoVNASaver.Charts.LogMag import LogMagChart
|
from NanoVNASaver.Charts.LogMag import LogMagChart
|
||||||
|
@ -57,8 +55,10 @@ class MagnitudeZChart(FrequencyChart):
|
||||||
if self.fixedValues:
|
if self.fixedValues:
|
||||||
self.maxValue = self.maxDisplayValue
|
self.maxValue = self.maxDisplayValue
|
||||||
self.minValue = (
|
self.minValue = (
|
||||||
max(self.minDisplayValue, 0.01) if self.logarithmicY else
|
max(self.minDisplayValue, 0.01)
|
||||||
self.minDisplayValue)
|
if self.logarithmicY
|
||||||
|
else self.minDisplayValue
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Find scaling
|
# Find scaling
|
||||||
self.minValue = 100
|
self.minValue = 100
|
||||||
|
@ -92,15 +92,18 @@ class MagnitudeZChart(FrequencyChart):
|
||||||
for i in range(horizontal_ticks):
|
for i in range(horizontal_ticks):
|
||||||
y = self.topMargin + round(i * self.dim.height / horizontal_ticks)
|
y = self.topMargin + round(i * self.dim.height / horizontal_ticks)
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin - 5, y,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width + 5, y)
|
self.leftMargin - 5, y, self.leftMargin + self.dim.width + 5, y
|
||||||
|
)
|
||||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||||
val = Value(self.valueAtPosition(y)[0], fmt=fmt)
|
val = Value(self.valueAtPosition(y)[0], fmt=fmt)
|
||||||
qp.drawText(3, y + 4, str(val))
|
qp.drawText(3, y + 4, str(val))
|
||||||
|
|
||||||
qp.drawText(3,
|
qp.drawText(
|
||||||
self.dim.height + self.topMargin,
|
3,
|
||||||
str(Value(self.minValue, fmt=fmt)))
|
self.dim.height + self.topMargin,
|
||||||
|
str(Value(self.minValue, fmt=fmt)),
|
||||||
|
)
|
||||||
|
|
||||||
self.drawFrequencyTicks(qp)
|
self.drawFrequencyTicks(qp)
|
||||||
|
|
||||||
|
@ -116,18 +119,22 @@ class MagnitudeZChart(FrequencyChart):
|
||||||
if self.logarithmicY:
|
if self.logarithmicY:
|
||||||
span = math.log(self.maxValue) - math.log(self.minValue)
|
span = math.log(self.maxValue) - math.log(self.minValue)
|
||||||
return self.topMargin + int(
|
return self.topMargin + int(
|
||||||
(math.log(self.maxValue) - math.log(mag)) /
|
(math.log(self.maxValue) - math.log(mag))
|
||||||
span * self.dim.height)
|
/ span
|
||||||
|
* self.dim.height
|
||||||
|
)
|
||||||
return self.topMargin + int(
|
return self.topMargin + int(
|
||||||
(self.maxValue - mag) / self.span * self.dim.height)
|
(self.maxValue - mag) / self.span * self.dim.height
|
||||||
|
)
|
||||||
return self.topMargin
|
return self.topMargin
|
||||||
|
|
||||||
def valueAtPosition(self, y) -> List[float]:
|
def valueAtPosition(self, y) -> list[float]:
|
||||||
absy = y - self.topMargin
|
absy = y - self.topMargin
|
||||||
if self.logarithmicY:
|
if self.logarithmicY:
|
||||||
span = math.log(self.maxValue) - math.log(self.minValue)
|
span = math.log(self.maxValue) - math.log(self.minValue)
|
||||||
val = math.exp(math.log(self.maxValue) -
|
val = math.exp(
|
||||||
absy * span / self.dim.height)
|
math.log(self.maxValue) - absy * span / self.dim.height
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
val = self.maxValue - (absy / self.dim.height * self.span)
|
val = self.maxValue - (absy / self.dim.height * self.span)
|
||||||
return [val]
|
return [val]
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
# NanoVNASaver
|
# NanoVNASaver
|
||||||
#
|
#
|
||||||
# A python program to view and export Touchstone data from a NanoVNA
|
# A python program to view and export Touchstone data from a NanoVNA
|
||||||
|
@ -27,7 +26,6 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MagnitudeZSeriesChart(MagnitudeZChart):
|
class MagnitudeZSeriesChart(MagnitudeZChart):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def magnitude(p: Datapoint) -> float:
|
def magnitude(p: Datapoint) -> float:
|
||||||
return abs(p.seriesImpedance())
|
return abs(p.seriesImpedance())
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
# NanoVNASaver
|
# NanoVNASaver
|
||||||
#
|
#
|
||||||
# A python program to view and export Touchstone data from a NanoVNA
|
# A python program to view and export Touchstone data from a NanoVNA
|
||||||
|
@ -26,7 +25,6 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MagnitudeZShuntChart(MagnitudeZChart):
|
class MagnitudeZShuntChart(MagnitudeZChart):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def magnitude(p: Datapoint) -> float:
|
def magnitude(p: Datapoint) -> float:
|
||||||
return abs(p.shuntImpedance())
|
return abs(p.shuntImpedance())
|
|
@ -18,15 +18,15 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import math
|
import math
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from PyQt5 import QtGui
|
from PyQt6 import QtGui
|
||||||
|
|
||||||
from NanoVNASaver.Marker.Widget import Marker
|
from NanoVNASaver.Marker.Widget import Marker
|
||||||
from NanoVNASaver.RFTools import Datapoint
|
from NanoVNASaver.RFTools import Datapoint
|
||||||
from NanoVNASaver.SITools import Format, Value
|
from NanoVNASaver.SITools import Format, Value
|
||||||
from NanoVNASaver.Charts.Chart import Chart
|
from NanoVNASaver.Charts.Chart import Chart
|
||||||
from NanoVNASaver.Charts.Frequency import FrequencyChart
|
from NanoVNASaver.Charts.Frequency import FrequencyChart
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,19 +50,26 @@ class PermeabilityChart(FrequencyChart):
|
||||||
|
|
||||||
def drawChart(self, qp: QtGui.QPainter):
|
def drawChart(self, qp: QtGui.QPainter):
|
||||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||||
qp.drawText(self.leftMargin + 5, 15, self.name +
|
qp.drawText(
|
||||||
" (\N{MICRO SIGN}\N{OHM SIGN} / Hz)")
|
self.leftMargin + 5,
|
||||||
|
15,
|
||||||
|
self.name + " (\N{MICRO SIGN}\N{OHM SIGN} / Hz)",
|
||||||
|
)
|
||||||
qp.drawText(10, 15, "R")
|
qp.drawText(10, 15, "R")
|
||||||
qp.drawText(self.leftMargin + self.dim.width + 10, 15, "X")
|
qp.drawText(self.leftMargin + self.dim.width + 10, 15, "X")
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin,
|
qp.drawLine(
|
||||||
self.topMargin - 5,
|
self.leftMargin,
|
||||||
self.leftMargin,
|
self.topMargin - 5,
|
||||||
self.topMargin + self.dim.height + 5)
|
self.leftMargin,
|
||||||
qp.drawLine(self.leftMargin - 5,
|
self.topMargin + self.dim.height + 5,
|
||||||
self.topMargin + self.dim.height,
|
)
|
||||||
self.leftMargin + self.dim.width + 5,
|
qp.drawLine(
|
||||||
self.topMargin + self.dim.height)
|
self.leftMargin - 5,
|
||||||
|
self.topMargin + self.dim.height,
|
||||||
|
self.leftMargin + self.dim.width + 5,
|
||||||
|
self.topMargin + self.dim.height,
|
||||||
|
)
|
||||||
self.drawTitle(qp)
|
self.drawTitle(qp)
|
||||||
|
|
||||||
def drawValues(self, qp: QtGui.QPainter):
|
def drawValues(self, qp: QtGui.QPainter):
|
||||||
|
@ -121,21 +128,22 @@ class PermeabilityChart(FrequencyChart):
|
||||||
for i in range(horizontal_ticks):
|
for i in range(horizontal_ticks):
|
||||||
y = self.topMargin + round(i * self.dim.height / horizontal_ticks)
|
y = self.topMargin + round(i * self.dim.height / horizontal_ticks)
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin - 5, y,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width + 5, y)
|
self.leftMargin - 5, y, self.leftMargin + self.dim.width + 5, y
|
||||||
|
)
|
||||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||||
val = Value(self.valueAtPosition(y)[0], fmt=fmt)
|
val = Value(self.valueAtPosition(y)[0], fmt=fmt)
|
||||||
qp.drawText(3, y + 4, str(val))
|
qp.drawText(3, y + 4, str(val))
|
||||||
|
|
||||||
qp.drawText(3,
|
qp.drawText(
|
||||||
self.dim.height + self.topMargin,
|
3, self.dim.height + self.topMargin, str(Value(min_val, fmt=fmt))
|
||||||
str(Value(min_val, fmt=fmt)))
|
)
|
||||||
|
|
||||||
self.drawFrequencyTicks(qp)
|
self.drawFrequencyTicks(qp)
|
||||||
|
|
||||||
primary_pen = pen
|
primary_pen = pen
|
||||||
secondary_pen = QtGui.QPen(Chart.color.sweep_secondary)
|
secondary_pen = QtGui.QPen(Chart.color.sweep_secondary)
|
||||||
if len(self.data) > 0:
|
if self.data:
|
||||||
c = QtGui.QColor(Chart.color.sweep)
|
c = QtGui.QColor(Chart.color.sweep)
|
||||||
c.setAlpha(255)
|
c.setAlpha(255)
|
||||||
pen = QtGui.QPen(c)
|
pen = QtGui.QPen(c)
|
||||||
|
@ -147,8 +155,11 @@ class PermeabilityChart(FrequencyChart):
|
||||||
pen.setColor(c)
|
pen.setColor(c)
|
||||||
qp.setPen(pen)
|
qp.setPen(pen)
|
||||||
qp.drawLine(
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width, 9,
|
self.leftMargin + self.dim.width,
|
||||||
self.leftMargin + self.dim.width + 5, 9)
|
9,
|
||||||
|
self.leftMargin + self.dim.width + 5,
|
||||||
|
9,
|
||||||
|
)
|
||||||
|
|
||||||
primary_pen.setWidth(self.dim.point)
|
primary_pen.setWidth(self.dim.point)
|
||||||
secondary_pen.setWidth(self.dim.point)
|
secondary_pen.setWidth(self.dim.point)
|
||||||
|
@ -177,7 +188,8 @@ class PermeabilityChart(FrequencyChart):
|
||||||
qp.drawLine(x, y_re, prev_x, prev_y_re)
|
qp.drawLine(x, y_re, prev_x, prev_y_re)
|
||||||
else:
|
else:
|
||||||
new_x, new_y = self.getPlotable(
|
new_x, new_y = self.getPlotable(
|
||||||
x, y_re, prev_x, prev_y_re)
|
x, y_re, prev_x, prev_y_re
|
||||||
|
)
|
||||||
qp.drawLine(x, y_re, new_x, new_y)
|
qp.drawLine(x, y_re, new_x, new_y)
|
||||||
elif self.isPlotable(prev_x, prev_y_re):
|
elif self.isPlotable(prev_x, prev_y_re):
|
||||||
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
|
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
|
||||||
|
@ -191,7 +203,8 @@ class PermeabilityChart(FrequencyChart):
|
||||||
qp.drawLine(x, y_im, prev_x, prev_y_im)
|
qp.drawLine(x, y_im, prev_x, prev_y_im)
|
||||||
else:
|
else:
|
||||||
new_x, new_y = self.getPlotable(
|
new_x, new_y = self.getPlotable(
|
||||||
x, y_im, prev_x, prev_y_im)
|
x, y_im, prev_x, prev_y_im
|
||||||
|
)
|
||||||
qp.drawLine(x, y_im, new_x, new_y)
|
qp.drawLine(x, y_im, new_x, new_y)
|
||||||
elif self.isPlotable(prev_x, prev_y_im):
|
elif self.isPlotable(prev_x, prev_y_im):
|
||||||
new_x, new_y = self.getPlotable(prev_x, prev_y_im, x, y_im)
|
new_x, new_y = self.getPlotable(prev_x, prev_y_im, x, y_im)
|
||||||
|
@ -201,7 +214,7 @@ class PermeabilityChart(FrequencyChart):
|
||||||
line_pen.setColor(Chart.color.reference)
|
line_pen.setColor(Chart.color.reference)
|
||||||
secondary_pen.setColor(Chart.color.reference_secondary)
|
secondary_pen.setColor(Chart.color.reference_secondary)
|
||||||
qp.setPen(primary_pen)
|
qp.setPen(primary_pen)
|
||||||
if len(self.reference) > 0:
|
if self.reference:
|
||||||
c = QtGui.QColor(Chart.color.reference)
|
c = QtGui.QColor(Chart.color.reference)
|
||||||
c.setAlpha(255)
|
c.setAlpha(255)
|
||||||
pen = QtGui.QPen(c)
|
pen = QtGui.QPen(c)
|
||||||
|
@ -213,8 +226,12 @@ class PermeabilityChart(FrequencyChart):
|
||||||
pen = QtGui.QPen(c)
|
pen = QtGui.QPen(c)
|
||||||
pen.setWidth(2)
|
pen.setWidth(2)
|
||||||
qp.setPen(pen)
|
qp.setPen(pen)
|
||||||
qp.drawLine(self.leftMargin + self.dim.width, 14,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width + 5, 14)
|
self.leftMargin + self.dim.width,
|
||||||
|
14,
|
||||||
|
self.leftMargin + self.dim.width + 5,
|
||||||
|
14,
|
||||||
|
)
|
||||||
|
|
||||||
for i, reference in enumerate(self.reference):
|
for i, reference in enumerate(self.reference):
|
||||||
if reference.freq < self.fstart or reference.freq > self.fstop:
|
if reference.freq < self.fstart or reference.freq > self.fstop:
|
||||||
|
@ -241,7 +258,8 @@ class PermeabilityChart(FrequencyChart):
|
||||||
qp.drawLine(x, y_re, prev_x, prev_y_re)
|
qp.drawLine(x, y_re, prev_x, prev_y_re)
|
||||||
else:
|
else:
|
||||||
new_x, new_y = self.getPlotable(
|
new_x, new_y = self.getPlotable(
|
||||||
x, y_re, prev_x, prev_y_re)
|
x, y_re, prev_x, prev_y_re
|
||||||
|
)
|
||||||
qp.drawLine(x, y_re, new_x, new_y)
|
qp.drawLine(x, y_re, new_x, new_y)
|
||||||
elif self.isPlotable(prev_x, prev_y_re):
|
elif self.isPlotable(prev_x, prev_y_re):
|
||||||
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
|
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
|
||||||
|
@ -255,7 +273,8 @@ class PermeabilityChart(FrequencyChart):
|
||||||
qp.drawLine(x, y_im, prev_x, prev_y_im)
|
qp.drawLine(x, y_im, prev_x, prev_y_im)
|
||||||
else:
|
else:
|
||||||
new_x, new_y = self.getPlotable(
|
new_x, new_y = self.getPlotable(
|
||||||
x, y_im, prev_x, prev_y_im)
|
x, y_im, prev_x, prev_y_im
|
||||||
|
)
|
||||||
qp.drawLine(x, y_im, new_x, new_y)
|
qp.drawLine(x, y_im, new_x, new_y)
|
||||||
elif self.isPlotable(prev_x, prev_y_im):
|
elif self.isPlotable(prev_x, prev_y_im):
|
||||||
new_x, new_y = self.getPlotable(prev_x, prev_y_im, x, y_im)
|
new_x, new_y = self.getPlotable(prev_x, prev_y_im, x, y_im)
|
||||||
|
@ -268,10 +287,8 @@ class PermeabilityChart(FrequencyChart):
|
||||||
y_re = self.getReYPosition(self.data[m.location])
|
y_re = self.getReYPosition(self.data[m.location])
|
||||||
y_im = self.getImYPosition(self.data[m.location])
|
y_im = self.getImYPosition(self.data[m.location])
|
||||||
|
|
||||||
self.drawMarker(x, y_re, qp, m.color,
|
self.drawMarker(x, y_re, qp, m.color, self.markers.index(m) + 1)
|
||||||
self.markers.index(m) + 1)
|
self.drawMarker(x, y_im, qp, m.color, self.markers.index(m) + 1)
|
||||||
self.drawMarker(x, y_im, qp, m.color,
|
|
||||||
self.markers.index(m) + 1)
|
|
||||||
|
|
||||||
def getImYPosition(self, d: Datapoint) -> int:
|
def getImYPosition(self, d: Datapoint) -> int:
|
||||||
im = d.impedance().imag
|
im = d.impedance().imag
|
||||||
|
@ -283,10 +300,12 @@ class PermeabilityChart(FrequencyChart):
|
||||||
else:
|
else:
|
||||||
return -1
|
return -1
|
||||||
return int(
|
return int(
|
||||||
self.topMargin + (math.log(self.max) - math.log(im)) /
|
self.topMargin
|
||||||
span * self.dim.height)
|
+ (math.log(self.max) - math.log(im)) / span * self.dim.height
|
||||||
return int(self.topMargin + (self.max - im) /
|
)
|
||||||
self.span * self.dim.height)
|
return int(
|
||||||
|
self.topMargin + (self.max - im) / self.span * self.dim.height
|
||||||
|
)
|
||||||
|
|
||||||
def getReYPosition(self, d: Datapoint) -> int:
|
def getReYPosition(self, d: Datapoint) -> int:
|
||||||
re = d.impedance().real
|
re = d.impedance().real
|
||||||
|
@ -298,12 +317,14 @@ class PermeabilityChart(FrequencyChart):
|
||||||
else:
|
else:
|
||||||
return -1
|
return -1
|
||||||
return int(
|
return int(
|
||||||
self.topMargin + (math.log(self.max) - math.log(re)) /
|
self.topMargin
|
||||||
span * self.dim.height)
|
+ (math.log(self.max) - math.log(re)) / span * self.dim.height
|
||||||
|
)
|
||||||
return int(
|
return int(
|
||||||
self.topMargin + (self.max - re) / self.span * self.dim.height)
|
self.topMargin + (self.max - re) / self.span * self.dim.height
|
||||||
|
)
|
||||||
|
|
||||||
def valueAtPosition(self, y) -> List[float]:
|
def valueAtPosition(self, y) -> list[float]:
|
||||||
absy = y - self.topMargin
|
absy = y - self.topMargin
|
||||||
if self.logarithmicY:
|
if self.logarithmicY:
|
||||||
min_val = self.max - self.span
|
min_val = self.max - self.span
|
|
@ -19,10 +19,9 @@
|
||||||
import math
|
import math
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from typing import List
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from PyQt5 import QtWidgets, QtGui
|
from PyQt6.QtGui import QAction, QPainter, QPen
|
||||||
|
|
||||||
from NanoVNASaver.RFTools import Datapoint
|
from NanoVNASaver.RFTools import Datapoint
|
||||||
from NanoVNASaver.Charts.Chart import Chart
|
from NanoVNASaver.Charts.Chart import Chart
|
||||||
|
@ -47,10 +46,11 @@ class PhaseChart(FrequencyChart):
|
||||||
self.maxDisplayValue = 180
|
self.maxDisplayValue = 180
|
||||||
|
|
||||||
self.y_menu.addSeparator()
|
self.y_menu.addSeparator()
|
||||||
self.action_unwrap = QtWidgets.QAction("Unwrap")
|
self.action_unwrap = QAction("Unwrap")
|
||||||
self.action_unwrap.setCheckable(True)
|
self.action_unwrap.setCheckable(True)
|
||||||
self.action_unwrap.triggered.connect(
|
self.action_unwrap.triggered.connect(
|
||||||
lambda: self.setUnwrap(self.action_unwrap.isChecked()))
|
lambda: self.setUnwrap(self.action_unwrap.isChecked())
|
||||||
|
)
|
||||||
self.y_menu.addAction(self.action_unwrap)
|
self.y_menu.addAction(self.action_unwrap)
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
|
@ -63,7 +63,7 @@ class PhaseChart(FrequencyChart):
|
||||||
self.unwrap = unwrap
|
self.unwrap = unwrap
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def drawValues(self, qp: QtGui.QPainter):
|
def drawValues(self, qp: QPainter):
|
||||||
if len(self.data) == 0 and len(self.reference) == 0:
|
if len(self.data) == 0 and len(self.reference) == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -98,24 +98,32 @@ class PhaseChart(FrequencyChart):
|
||||||
for i in range(tickcount):
|
for i in range(tickcount):
|
||||||
angle = minAngle + span * i / tickcount
|
angle = minAngle + span * i / tickcount
|
||||||
y = self.topMargin + int(
|
y = self.topMargin + int(
|
||||||
(self.maxAngle - angle) / self.span * self.dim.height)
|
(self.maxAngle - angle) / self.span * self.dim.height
|
||||||
|
)
|
||||||
if angle not in [minAngle, maxAngle]:
|
if angle not in [minAngle, maxAngle]:
|
||||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
qp.setPen(QPen(Chart.color.text))
|
||||||
if angle != 0:
|
if angle != 0:
|
||||||
digits = max(
|
digits = max(
|
||||||
0, min(2, math.floor(3 - math.log10(abs(angle)))))
|
0, min(2, math.floor(3 - math.log10(abs(angle))))
|
||||||
anglestr = str(round(angle)) if digits == 0 else str(
|
)
|
||||||
round(angle, digits))
|
anglestr = (
|
||||||
|
str(round(angle))
|
||||||
|
if digits == 0
|
||||||
|
else str(round(angle, digits))
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
anglestr = "0"
|
anglestr = "0"
|
||||||
qp.drawText(3, y + 3, f"{anglestr}°")
|
qp.drawText(3, y + 3, f"{anglestr}°")
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin - 5, y,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width, y)
|
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
|
||||||
qp.drawLine(self.leftMargin - 5,
|
)
|
||||||
self.topMargin,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width,
|
self.leftMargin - 5,
|
||||||
self.topMargin)
|
self.topMargin,
|
||||||
|
self.leftMargin + self.dim.width,
|
||||||
|
self.topMargin,
|
||||||
|
)
|
||||||
qp.setPen(Chart.color.text)
|
qp.setPen(Chart.color.text)
|
||||||
qp.drawText(3, self.topMargin + 5, f"{maxAngle}°")
|
qp.drawText(3, self.topMargin + 5, f"{maxAngle}°")
|
||||||
qp.drawText(3, self.dim.height + self.topMargin, f"{minAngle}°")
|
qp.drawText(3, self.dim.height + self.topMargin, f"{minAngle}°")
|
||||||
|
@ -139,9 +147,10 @@ class PhaseChart(FrequencyChart):
|
||||||
else:
|
else:
|
||||||
angle = math.degrees(d.phase)
|
angle = math.degrees(d.phase)
|
||||||
return self.topMargin + int(
|
return self.topMargin + int(
|
||||||
(self.maxAngle - angle) / self.span * self.dim.height)
|
(self.maxAngle - angle) / self.span * self.dim.height
|
||||||
|
)
|
||||||
|
|
||||||
def valueAtPosition(self, y) -> List[float]:
|
def valueAtPosition(self, y) -> list[float]:
|
||||||
absy = y - self.topMargin
|
absy = y - self.topMargin
|
||||||
val = -1 * ((absy / self.dim.height * self.span) - self.maxAngle)
|
val = -1 * ((absy / self.dim.height * self.span) - self.maxAngle)
|
||||||
return [val]
|
return [val]
|
|
@ -17,7 +17,7 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import logging
|
import logging
|
||||||
from PyQt5 import QtGui, QtCore
|
from PyQt6 import QtGui, QtCore
|
||||||
|
|
||||||
from NanoVNASaver.Charts.Chart import Chart
|
from NanoVNASaver.Charts.Chart import Chart
|
||||||
from NanoVNASaver.Charts.Square import SquareChart
|
from NanoVNASaver.Charts.Square import SquareChart
|
||||||
|
@ -39,16 +39,25 @@ class PolarChart(SquareChart):
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
|
|
||||||
qp.drawEllipse(QtCore.QPoint(center_x, center_y), width_2, height_2)
|
qp.drawEllipse(QtCore.QPoint(center_x, center_y), width_2, height_2)
|
||||||
qp.drawEllipse(QtCore.QPoint(center_x, center_y),
|
qp.drawEllipse(
|
||||||
width_2 // 2, height_2 // 2)
|
QtCore.QPoint(center_x, center_y), width_2 // 2, height_2 // 2
|
||||||
|
)
|
||||||
|
|
||||||
qp.drawLine(center_x - width_2, center_y,
|
qp.drawLine(center_x - width_2, center_y, center_x + width_2, center_y)
|
||||||
center_x + width_2, center_y)
|
qp.drawLine(
|
||||||
qp.drawLine(center_x, center_y - height_2,
|
center_x, center_y - height_2, center_x, center_y + height_2
|
||||||
center_x, center_y + height_2)
|
)
|
||||||
qp.drawLine(center_x + width_45, center_y + height_45,
|
qp.drawLine(
|
||||||
center_x - width_45, center_y - height_45)
|
center_x + width_45,
|
||||||
qp.drawLine(center_x + width_45, center_y - height_45,
|
center_y + height_45,
|
||||||
center_x - width_45, center_y + height_45)
|
center_x - width_45,
|
||||||
|
center_y - height_45,
|
||||||
|
)
|
||||||
|
qp.drawLine(
|
||||||
|
center_x + width_45,
|
||||||
|
center_y - height_45,
|
||||||
|
center_x - width_45,
|
||||||
|
center_y + height_45,
|
||||||
|
)
|
||||||
|
|
||||||
self.drawTitle(qp)
|
self.drawTitle(qp)
|
|
@ -18,9 +18,8 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import math
|
import math
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from PyQt5 import QtGui
|
from PyQt6 import QtGui
|
||||||
|
|
||||||
from NanoVNASaver.RFTools import Datapoint
|
from NanoVNASaver.RFTools import Datapoint
|
||||||
from NanoVNASaver.Charts.Chart import Chart
|
from NanoVNASaver.Charts.Chart import Chart
|
||||||
|
@ -57,7 +56,7 @@ class QualityFactorChart(FrequencyChart):
|
||||||
scale = 0
|
scale = 0
|
||||||
if maxQ > 0:
|
if maxQ > 0:
|
||||||
scale = max(scale, math.floor(math.log10(maxQ)))
|
scale = max(scale, math.floor(math.log10(maxQ)))
|
||||||
maxQ = math.ceil(maxQ / 10 ** scale) * 10 ** scale
|
maxQ = math.ceil(maxQ / 10**scale) * 10**scale
|
||||||
|
|
||||||
self.minQ = self.minDisplayValue
|
self.minQ = self.minDisplayValue
|
||||||
self.maxQ = maxQ
|
self.maxQ = maxQ
|
||||||
|
@ -69,8 +68,9 @@ class QualityFactorChart(FrequencyChart):
|
||||||
|
|
||||||
for i in range(tickcount):
|
for i in range(tickcount):
|
||||||
q = self.minQ + i * self.span / tickcount
|
q = self.minQ + i * self.span / tickcount
|
||||||
y = self.topMargin + int((self.maxQ - q) / self.span *
|
y = self.topMargin + int(
|
||||||
self.dim.height)
|
(self.maxQ - q) / self.span * self.dim.height
|
||||||
|
)
|
||||||
q = round(q)
|
q = round(q)
|
||||||
if q < 10:
|
if q < 10:
|
||||||
q = round(q, 2)
|
q = round(q, 2)
|
||||||
|
@ -79,12 +79,15 @@ class QualityFactorChart(FrequencyChart):
|
||||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||||
qp.drawText(3, y + 3, str(q))
|
qp.drawText(3, y + 3, str(q))
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin - 5, y,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width, y)
|
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
|
||||||
qp.drawLine(self.leftMargin - 5,
|
)
|
||||||
self.topMargin,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width,
|
self.leftMargin - 5,
|
||||||
self.topMargin)
|
self.topMargin,
|
||||||
|
self.leftMargin + self.dim.width,
|
||||||
|
self.topMargin,
|
||||||
|
)
|
||||||
qp.setPen(Chart.color.text)
|
qp.setPen(Chart.color.text)
|
||||||
|
|
||||||
max_q = round(maxQ)
|
max_q = round(maxQ)
|
||||||
|
@ -119,10 +122,11 @@ class QualityFactorChart(FrequencyChart):
|
||||||
|
|
||||||
def getYPosition(self, d: Datapoint) -> int:
|
def getYPosition(self, d: Datapoint) -> int:
|
||||||
Q = d.qFactor()
|
Q = d.qFactor()
|
||||||
return self.topMargin + int((self.maxQ - Q) / self.span *
|
return self.topMargin + int(
|
||||||
self.dim.height)
|
(self.maxQ - Q) / self.span * self.dim.height
|
||||||
|
)
|
||||||
|
|
||||||
def valueAtPosition(self, y) -> List[float]:
|
def valueAtPosition(self, y) -> list[float]:
|
||||||
absy = y - self.topMargin
|
absy = y - self.topMargin
|
||||||
val = -1 * ((absy / self.dim.height * self.span) - self.maxQ)
|
val = -1 * ((absy / self.dim.height * self.span) - self.maxQ)
|
||||||
return [val]
|
return [val]
|
|
@ -18,9 +18,8 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import math
|
import math
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from PyQt5 import QtWidgets, QtGui
|
from PyQt6 import QtWidgets, QtGui
|
||||||
|
|
||||||
from NanoVNASaver.Formatting import format_frequency_chart
|
from NanoVNASaver.Formatting import format_frequency_chart
|
||||||
from NanoVNASaver.Marker.Widget import Marker
|
from NanoVNASaver.Marker.Widget import Marker
|
||||||
|
@ -58,16 +57,18 @@ class RealImaginaryChart(FrequencyChart):
|
||||||
|
|
||||||
self.y_menu.clear()
|
self.y_menu.clear()
|
||||||
|
|
||||||
self.y_action_automatic = QtWidgets.QAction("Automatic")
|
self.y_action_automatic = QtGui.QAction("Automatic")
|
||||||
self.y_action_automatic.setCheckable(True)
|
self.y_action_automatic.setCheckable(True)
|
||||||
self.y_action_automatic.setChecked(True)
|
self.y_action_automatic.setChecked(True)
|
||||||
self.y_action_automatic.changed.connect(
|
self.y_action_automatic.changed.connect(
|
||||||
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked()))
|
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked())
|
||||||
self.y_action_fixed_span = QtWidgets.QAction("Fixed span")
|
)
|
||||||
|
self.y_action_fixed_span = QtGui.QAction("Fixed span")
|
||||||
self.y_action_fixed_span.setCheckable(True)
|
self.y_action_fixed_span.setCheckable(True)
|
||||||
self.y_action_fixed_span.changed.connect(
|
self.y_action_fixed_span.changed.connect(
|
||||||
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked()))
|
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked())
|
||||||
mode_group = QtWidgets.QActionGroup(self)
|
)
|
||||||
|
mode_group = QtGui.QActionGroup(self)
|
||||||
mode_group.addAction(self.y_action_automatic)
|
mode_group.addAction(self.y_action_automatic)
|
||||||
mode_group.addAction(self.y_action_fixed_span)
|
mode_group.addAction(self.y_action_fixed_span)
|
||||||
self.y_menu.addAction(self.y_action_automatic)
|
self.y_menu.addAction(self.y_action_automatic)
|
||||||
|
@ -110,11 +111,14 @@ class RealImaginaryChart(FrequencyChart):
|
||||||
self.drawHorizontalTicks(qp)
|
self.drawHorizontalTicks(qp)
|
||||||
|
|
||||||
fmt = Format(max_nr_digits=3)
|
fmt = Format(max_nr_digits=3)
|
||||||
qp.drawText(3, self.dim.height + self.topMargin,
|
qp.drawText(
|
||||||
str(Value(min_real, fmt=fmt)))
|
3, self.dim.height + self.topMargin, str(Value(min_real, fmt=fmt))
|
||||||
qp.drawText(self.leftMargin + self.dim.width + 8,
|
)
|
||||||
self.dim.height + self.topMargin,
|
qp.drawText(
|
||||||
str(Value(min_imag, fmt=fmt)))
|
self.leftMargin + self.dim.width + 8,
|
||||||
|
self.dim.height + self.topMargin,
|
||||||
|
str(Value(min_imag, fmt=fmt)),
|
||||||
|
)
|
||||||
|
|
||||||
self.drawFrequencyTicks(qp)
|
self.drawFrequencyTicks(qp)
|
||||||
|
|
||||||
|
@ -131,8 +135,12 @@ class RealImaginaryChart(FrequencyChart):
|
||||||
c.setAlpha(255)
|
c.setAlpha(255)
|
||||||
pen.setColor(c)
|
pen.setColor(c)
|
||||||
qp.setPen(pen)
|
qp.setPen(pen)
|
||||||
qp.drawLine(self.leftMargin + self.dim.width, 9,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width + 5, 9)
|
self.leftMargin + self.dim.width,
|
||||||
|
9,
|
||||||
|
self.leftMargin + self.dim.width + 5,
|
||||||
|
9,
|
||||||
|
)
|
||||||
|
|
||||||
primary_pen.setWidth(self.dim.point)
|
primary_pen.setWidth(self.dim.point)
|
||||||
secondary_pen.setWidth(self.dim.point)
|
secondary_pen.setWidth(self.dim.point)
|
||||||
|
@ -161,7 +169,8 @@ class RealImaginaryChart(FrequencyChart):
|
||||||
qp.drawLine(x, y_re, prev_x, prev_y_re)
|
qp.drawLine(x, y_re, prev_x, prev_y_re)
|
||||||
else:
|
else:
|
||||||
new_x, new_y = self.getPlotable(
|
new_x, new_y = self.getPlotable(
|
||||||
x, y_re, prev_x, prev_y_re)
|
x, y_re, prev_x, prev_y_re
|
||||||
|
)
|
||||||
qp.drawLine(x, y_re, new_x, new_y)
|
qp.drawLine(x, y_re, new_x, new_y)
|
||||||
elif self.isPlotable(prev_x, prev_y_re):
|
elif self.isPlotable(prev_x, prev_y_re):
|
||||||
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
|
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
|
||||||
|
@ -175,7 +184,8 @@ class RealImaginaryChart(FrequencyChart):
|
||||||
qp.drawLine(x, y_im, prev_x, prev_y_im)
|
qp.drawLine(x, y_im, prev_x, prev_y_im)
|
||||||
else:
|
else:
|
||||||
new_x, new_y = self.getPlotable(
|
new_x, new_y = self.getPlotable(
|
||||||
x, y_im, prev_x, prev_y_im)
|
x, y_im, prev_x, prev_y_im
|
||||||
|
)
|
||||||
qp.drawLine(x, y_im, new_x, new_y)
|
qp.drawLine(x, y_im, new_x, new_y)
|
||||||
elif self.isPlotable(prev_x, prev_y_im):
|
elif self.isPlotable(prev_x, prev_y_im):
|
||||||
new_x, new_y = self.getPlotable(prev_x, prev_y_im, x, y_im)
|
new_x, new_y = self.getPlotable(prev_x, prev_y_im, x, y_im)
|
||||||
|
@ -197,8 +207,12 @@ class RealImaginaryChart(FrequencyChart):
|
||||||
pen = QtGui.QPen(c)
|
pen = QtGui.QPen(c)
|
||||||
pen.setWidth(2)
|
pen.setWidth(2)
|
||||||
qp.setPen(pen)
|
qp.setPen(pen)
|
||||||
qp.drawLine(self.leftMargin + self.dim.width, 14,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width + 5, 14)
|
self.leftMargin + self.dim.width,
|
||||||
|
14,
|
||||||
|
self.leftMargin + self.dim.width + 5,
|
||||||
|
14,
|
||||||
|
)
|
||||||
|
|
||||||
for i, reference in enumerate(self.reference):
|
for i, reference in enumerate(self.reference):
|
||||||
if reference.freq < self.fstart or reference.freq > self.fstop:
|
if reference.freq < self.fstart or reference.freq > self.fstop:
|
||||||
|
@ -225,7 +239,8 @@ class RealImaginaryChart(FrequencyChart):
|
||||||
qp.drawLine(x, y_re, prev_x, prev_y_re)
|
qp.drawLine(x, y_re, prev_x, prev_y_re)
|
||||||
else:
|
else:
|
||||||
new_x, new_y = self.getPlotable(
|
new_x, new_y = self.getPlotable(
|
||||||
x, y_re, prev_x, prev_y_re)
|
x, y_re, prev_x, prev_y_re
|
||||||
|
)
|
||||||
qp.drawLine(x, y_re, new_x, new_y)
|
qp.drawLine(x, y_re, new_x, new_y)
|
||||||
elif self.isPlotable(prev_x, prev_y_re):
|
elif self.isPlotable(prev_x, prev_y_re):
|
||||||
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
|
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
|
||||||
|
@ -239,7 +254,8 @@ class RealImaginaryChart(FrequencyChart):
|
||||||
qp.drawLine(x, y_im, prev_x, prev_y_im)
|
qp.drawLine(x, y_im, prev_x, prev_y_im)
|
||||||
else:
|
else:
|
||||||
new_x, new_y = self.getPlotable(
|
new_x, new_y = self.getPlotable(
|
||||||
x, y_im, prev_x, prev_y_im)
|
x, y_im, prev_x, prev_y_im
|
||||||
|
)
|
||||||
qp.drawLine(x, y_im, new_x, new_y)
|
qp.drawLine(x, y_im, new_x, new_y)
|
||||||
elif self.isPlotable(prev_x, prev_y_im):
|
elif self.isPlotable(prev_x, prev_y_im):
|
||||||
new_x, new_y = self.getPlotable(prev_x, prev_y_im, x, y_im)
|
new_x, new_y = self.getPlotable(prev_x, prev_y_im, x, y_im)
|
||||||
|
@ -252,10 +268,8 @@ class RealImaginaryChart(FrequencyChart):
|
||||||
y_re = self.getReYPosition(self.data[m.location])
|
y_re = self.getReYPosition(self.data[m.location])
|
||||||
y_im = self.getImYPosition(self.data[m.location])
|
y_im = self.getImYPosition(self.data[m.location])
|
||||||
|
|
||||||
self.drawMarker(x, y_re, qp, m.color,
|
self.drawMarker(x, y_re, qp, m.color, self.markers.index(m) + 1)
|
||||||
self.markers.index(m) + 1)
|
self.drawMarker(x, y_im, qp, m.color, self.markers.index(m) + 1)
|
||||||
self.drawMarker(x, y_im, qp, m.color,
|
|
||||||
self.markers.index(m) + 1)
|
|
||||||
|
|
||||||
def drawHorizontalTicks(self, qp):
|
def drawHorizontalTicks(self, qp):
|
||||||
# We want one horizontal tick per 50 pixels, at most
|
# We want one horizontal tick per 50 pixels, at most
|
||||||
|
@ -264,8 +278,9 @@ class RealImaginaryChart(FrequencyChart):
|
||||||
for i in range(horizontal_ticks):
|
for i in range(horizontal_ticks):
|
||||||
y = self.topMargin + i * self.dim.height // horizontal_ticks
|
y = self.topMargin + i * self.dim.height // horizontal_ticks
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin - 5, y,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width + 5, y)
|
self.leftMargin - 5, y, self.leftMargin + self.dim.width + 5, y
|
||||||
|
)
|
||||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||||
re = self.max_real - i * self.span_real / horizontal_ticks
|
re = self.max_real - i * self.span_real / horizontal_ticks
|
||||||
im = self.max_imag - i * self.span_imag / horizontal_ticks
|
im = self.max_imag - i * self.span_imag / horizontal_ticks
|
||||||
|
@ -273,7 +288,8 @@ class RealImaginaryChart(FrequencyChart):
|
||||||
qp.drawText(
|
qp.drawText(
|
||||||
self.leftMargin + self.dim.width + 8,
|
self.leftMargin + self.dim.width + 8,
|
||||||
y + 4,
|
y + 4,
|
||||||
f"{Value(im, fmt=fmt)}")
|
f"{Value(im, fmt=fmt)}",
|
||||||
|
)
|
||||||
|
|
||||||
def find_scaling(self):
|
def find_scaling(self):
|
||||||
# Find scaling
|
# Find scaling
|
||||||
|
@ -350,20 +366,24 @@ class RealImaginaryChart(FrequencyChart):
|
||||||
|
|
||||||
def getImYPosition(self, d: Datapoint) -> int:
|
def getImYPosition(self, d: Datapoint) -> int:
|
||||||
im = self.value(d).imag
|
im = self.value(d).imag
|
||||||
return int(self.topMargin + (self.max_imag - im) / self.span_imag
|
return int(
|
||||||
* self.dim.height)
|
self.topMargin
|
||||||
|
+ (self.max_imag - im) / self.span_imag * self.dim.height
|
||||||
|
)
|
||||||
|
|
||||||
def getReYPosition(self, d: Datapoint) -> int:
|
def getReYPosition(self, d: Datapoint) -> int:
|
||||||
re = self.value(d).real
|
re = self.value(d).real
|
||||||
return int(self.topMargin + (self.max_real - re) / self.span_real
|
return int(
|
||||||
* self.dim.height if math.isfinite(re) else self.topMargin)
|
self.topMargin
|
||||||
|
+ (self.max_real - re) / self.span_real * self.dim.height
|
||||||
|
if math.isfinite(re)
|
||||||
|
else self.topMargin
|
||||||
|
)
|
||||||
|
|
||||||
def valueAtPosition(self, y) -> List[float]:
|
def valueAtPosition(self, y) -> list[float]:
|
||||||
absy = y - self.topMargin
|
absy = y - self.topMargin
|
||||||
valRe = -1 * ((absy / self.dim.height *
|
valRe = -1 * ((absy / self.dim.height * self.span_real) - self.max_real)
|
||||||
self.span_real) - self.max_real)
|
valIm = -1 * ((absy / self.dim.height * self.span_imag) - self.max_imag)
|
||||||
valIm = -1 * ((absy / self.dim.height *
|
|
||||||
self.span_imag) - self.max_imag)
|
|
||||||
return [valRe, valIm]
|
return [valRe, valIm]
|
||||||
|
|
||||||
def zoomTo(self, x1, y1, x2, y2):
|
def zoomTo(self, x1, y1, x2, y2):
|
||||||
|
@ -387,7 +407,7 @@ class RealImaginaryChart(FrequencyChart):
|
||||||
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def getNearestMarker(self, x, y) -> Optional[Marker]:
|
def getNearestMarker(self, x, y) -> Marker | None:
|
||||||
if not self.data:
|
if not self.data:
|
||||||
return None
|
return None
|
||||||
shortest = 10e6
|
shortest = 10e6
|
||||||
|
@ -406,9 +426,12 @@ class RealImaginaryChart(FrequencyChart):
|
||||||
|
|
||||||
def setMinimumRealValue(self):
|
def setMinimumRealValue(self):
|
||||||
min_val, selected = QtWidgets.QInputDialog.getDouble(
|
min_val, selected = QtWidgets.QInputDialog.getDouble(
|
||||||
self, "Minimum real value",
|
self,
|
||||||
"Set minimum real value", value=self.minDisplayReal,
|
"Minimum real value",
|
||||||
decimals=2)
|
"Set minimum real value",
|
||||||
|
value=self.minDisplayReal,
|
||||||
|
decimals=2,
|
||||||
|
)
|
||||||
if not selected:
|
if not selected:
|
||||||
return
|
return
|
||||||
if not (self.fixedValues and min_val >= self.maxDisplayReal):
|
if not (self.fixedValues and min_val >= self.maxDisplayReal):
|
||||||
|
@ -418,9 +441,12 @@ class RealImaginaryChart(FrequencyChart):
|
||||||
|
|
||||||
def setMaximumRealValue(self):
|
def setMaximumRealValue(self):
|
||||||
max_val, selected = QtWidgets.QInputDialog.getDouble(
|
max_val, selected = QtWidgets.QInputDialog.getDouble(
|
||||||
self, "Maximum real value",
|
self,
|
||||||
"Set maximum real value", value=self.maxDisplayReal,
|
"Maximum real value",
|
||||||
decimals=2)
|
"Set maximum real value",
|
||||||
|
value=self.maxDisplayReal,
|
||||||
|
decimals=2,
|
||||||
|
)
|
||||||
if not selected:
|
if not selected:
|
||||||
return
|
return
|
||||||
if not (self.fixedValues and max_val <= self.minDisplayReal):
|
if not (self.fixedValues and max_val <= self.minDisplayReal):
|
||||||
|
@ -430,9 +456,12 @@ class RealImaginaryChart(FrequencyChart):
|
||||||
|
|
||||||
def setMinimumImagValue(self):
|
def setMinimumImagValue(self):
|
||||||
min_val, selected = QtWidgets.QInputDialog.getDouble(
|
min_val, selected = QtWidgets.QInputDialog.getDouble(
|
||||||
self, "Minimum imaginary value",
|
self,
|
||||||
"Set minimum imaginary value", value=self.minDisplayImag,
|
"Minimum imaginary value",
|
||||||
decimals=2)
|
"Set minimum imaginary value",
|
||||||
|
value=self.minDisplayImag,
|
||||||
|
decimals=2,
|
||||||
|
)
|
||||||
if not selected:
|
if not selected:
|
||||||
return
|
return
|
||||||
if not (self.fixedValues and min_val >= self.maxDisplayImag):
|
if not (self.fixedValues and min_val >= self.maxDisplayImag):
|
||||||
|
@ -442,9 +471,12 @@ class RealImaginaryChart(FrequencyChart):
|
||||||
|
|
||||||
def setMaximumImagValue(self):
|
def setMaximumImagValue(self):
|
||||||
max_val, selected = QtWidgets.QInputDialog.getDouble(
|
max_val, selected = QtWidgets.QInputDialog.getDouble(
|
||||||
self, "Maximum imaginary value",
|
self,
|
||||||
"Set maximum imaginary value", value=self.maxDisplayImag,
|
"Maximum imaginary value",
|
||||||
decimals=2)
|
"Set maximum imaginary value",
|
||||||
|
value=self.maxDisplayImag,
|
||||||
|
decimals=2,
|
||||||
|
)
|
||||||
if not selected:
|
if not selected:
|
||||||
return
|
return
|
||||||
if not (self.fixedValues and max_val <= self.minDisplayImag):
|
if not (self.fixedValues and max_val <= self.minDisplayImag):
|
||||||
|
@ -454,9 +486,10 @@ class RealImaginaryChart(FrequencyChart):
|
||||||
|
|
||||||
def setFixedValues(self, fixed_values: bool):
|
def setFixedValues(self, fixed_values: bool):
|
||||||
self.fixedValues = fixed_values
|
self.fixedValues = fixed_values
|
||||||
if (fixed_values and
|
if fixed_values and (
|
||||||
(self.minDisplayReal >= self.maxDisplayReal or
|
self.minDisplayReal >= self.maxDisplayReal
|
||||||
self.minDisplayImag > self.maxDisplayImag)):
|
or self.minDisplayImag > self.maxDisplayImag
|
||||||
|
):
|
||||||
self.fixedValues = False
|
self.fixedValues = False
|
||||||
self.y_action_automatic.setChecked(True)
|
self.y_action_automatic.setChecked(True)
|
||||||
self.y_action_fixed_span.setChecked(False)
|
self.y_action_fixed_span.setChecked(False)
|
||||||
|
@ -464,18 +497,24 @@ class RealImaginaryChart(FrequencyChart):
|
||||||
|
|
||||||
def contextMenuEvent(self, event):
|
def contextMenuEvent(self, event):
|
||||||
self.action_set_fixed_start.setText(
|
self.action_set_fixed_start.setText(
|
||||||
f"Start ({format_frequency_chart(self.minFrequency)})")
|
f"Start ({format_frequency_chart(self.minFrequency)})"
|
||||||
|
)
|
||||||
self.action_set_fixed_stop.setText(
|
self.action_set_fixed_stop.setText(
|
||||||
f"Stop ({format_frequency_chart(self.maxFrequency)})")
|
f"Stop ({format_frequency_chart(self.maxFrequency)})"
|
||||||
|
)
|
||||||
self.action_set_fixed_minimum_real.setText(
|
self.action_set_fixed_minimum_real.setText(
|
||||||
f"Minimum R ({self.minDisplayReal})")
|
f"Minimum R ({self.minDisplayReal})"
|
||||||
|
)
|
||||||
self.action_set_fixed_maximum_real.setText(
|
self.action_set_fixed_maximum_real.setText(
|
||||||
f"Maximum R ({self.maxDisplayReal})")
|
f"Maximum R ({self.maxDisplayReal})"
|
||||||
|
)
|
||||||
self.action_set_fixed_minimum_imag.setText(
|
self.action_set_fixed_minimum_imag.setText(
|
||||||
f"Minimum jX ({self.minDisplayImag})")
|
f"Minimum jX ({self.minDisplayImag})"
|
||||||
|
)
|
||||||
self.action_set_fixed_maximum_imag.setText(
|
self.action_set_fixed_maximum_imag.setText(
|
||||||
f"Maximum jX ({self.maxDisplayImag})")
|
f"Maximum jX ({self.maxDisplayImag})"
|
||||||
self.menu.exec_(event.globalPos())
|
)
|
||||||
|
self.menu.exec(event.globalPos())
|
||||||
|
|
||||||
def value(self, p: Datapoint) -> complex:
|
def value(self, p: Datapoint) -> complex:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
|
@ -21,7 +21,7 @@ import numpy as np
|
||||||
import logging
|
import logging
|
||||||
from scipy.constants import mu_0
|
from scipy.constants import mu_0
|
||||||
|
|
||||||
from PyQt5 import QtWidgets, QtGui
|
from PyQt6 import QtWidgets, QtGui
|
||||||
|
|
||||||
from NanoVNASaver.Formatting import format_frequency_chart
|
from NanoVNASaver.Formatting import format_frequency_chart
|
||||||
from NanoVNASaver.RFTools import Datapoint
|
from NanoVNASaver.RFTools import Datapoint
|
||||||
|
@ -34,30 +34,37 @@ MU = "\N{GREEK SMALL LETTER MU}"
|
||||||
|
|
||||||
|
|
||||||
class RealImaginaryMuChart(RealImaginaryChart):
|
class RealImaginaryMuChart(RealImaginaryChart):
|
||||||
|
|
||||||
def __init__(self, name=""):
|
def __init__(self, name=""):
|
||||||
super().__init__(name)
|
super().__init__(name)
|
||||||
self.y_menu.addSeparator()
|
self.y_menu.addSeparator()
|
||||||
|
|
||||||
self.action_set_fixed_maximum_real = QtWidgets.QAction(
|
self.action_set_fixed_maximum_real = QtGui.QAction(
|
||||||
f"Maximum {MU}' ({self.maxDisplayReal})")
|
f"Maximum {MU}' ({self.maxDisplayReal})"
|
||||||
|
)
|
||||||
self.action_set_fixed_maximum_real.triggered.connect(
|
self.action_set_fixed_maximum_real.triggered.connect(
|
||||||
self.setMaximumRealValue)
|
self.setMaximumRealValue
|
||||||
|
)
|
||||||
|
|
||||||
self.action_set_fixed_minimum_real = QtWidgets.QAction(
|
self.action_set_fixed_minimum_real = QtGui.QAction(
|
||||||
f"Minimum {MU}' ({self.minDisplayReal})")
|
f"Minimum {MU}' ({self.minDisplayReal})"
|
||||||
|
)
|
||||||
self.action_set_fixed_minimum_real.triggered.connect(
|
self.action_set_fixed_minimum_real.triggered.connect(
|
||||||
self.setMinimumRealValue)
|
self.setMinimumRealValue
|
||||||
|
)
|
||||||
|
|
||||||
self.action_set_fixed_maximum_imag = QtWidgets.QAction(
|
self.action_set_fixed_maximum_imag = QtGui.QAction(
|
||||||
f"Maximum {MU}'' ({self.maxDisplayImag})")
|
f"Maximum {MU}'' ({self.maxDisplayImag})"
|
||||||
|
)
|
||||||
self.action_set_fixed_maximum_imag.triggered.connect(
|
self.action_set_fixed_maximum_imag.triggered.connect(
|
||||||
self.setMaximumImagValue)
|
self.setMaximumImagValue
|
||||||
|
)
|
||||||
|
|
||||||
self.action_set_fixed_minimum_imag = QtWidgets.QAction(
|
self.action_set_fixed_minimum_imag = QtGui.QAction(
|
||||||
f"Minimum {MU}'' ({self.minDisplayImag})")
|
f"Minimum {MU}'' ({self.minDisplayImag})"
|
||||||
|
)
|
||||||
self.action_set_fixed_minimum_imag.triggered.connect(
|
self.action_set_fixed_minimum_imag.triggered.connect(
|
||||||
self.setMinimumImagValue)
|
self.setMinimumImagValue
|
||||||
|
)
|
||||||
|
|
||||||
self.y_menu.addAction(self.action_set_fixed_maximum_real)
|
self.y_menu.addAction(self.action_set_fixed_maximum_real)
|
||||||
self.y_menu.addAction(self.action_set_fixed_minimum_real)
|
self.y_menu.addAction(self.action_set_fixed_minimum_real)
|
||||||
|
@ -67,25 +74,19 @@ class RealImaginaryMuChart(RealImaginaryChart):
|
||||||
|
|
||||||
# Manage core parameters
|
# Manage core parameters
|
||||||
# TODO pick some sane default values?
|
# TODO pick some sane default values?
|
||||||
self.coreLength = 1.
|
self.coreLength = 1.0
|
||||||
self.coreArea = 1.
|
self.coreArea = 1.0
|
||||||
self.coreWindings = 1
|
self.coreWindings = 1
|
||||||
|
|
||||||
self.menu.addSeparator()
|
self.menu.addSeparator()
|
||||||
self.action_set_core_length = QtWidgets.QAction(
|
self.action_set_core_length = QtGui.QAction("Core effective length")
|
||||||
"Core effective length")
|
self.action_set_core_length.triggered.connect(self.setCoreLength)
|
||||||
self.action_set_core_length.triggered.connect(
|
|
||||||
self.setCoreLength)
|
|
||||||
|
|
||||||
self.action_set_core_area = QtWidgets.QAction(
|
self.action_set_core_area = QtGui.QAction("Core area")
|
||||||
"Core area")
|
self.action_set_core_area.triggered.connect(self.setCoreArea)
|
||||||
self.action_set_core_area.triggered.connect(
|
|
||||||
self.setCoreArea)
|
|
||||||
|
|
||||||
self.action_set_core_windings = QtWidgets.QAction(
|
self.action_set_core_windings = QtGui.QAction("Core number of windings")
|
||||||
"Core number of windings")
|
self.action_set_core_windings.triggered.connect(self.setCoreWindings)
|
||||||
self.action_set_core_windings.triggered.connect(
|
|
||||||
self.setCoreWindings)
|
|
||||||
|
|
||||||
self.menu.addAction(self.action_set_core_length)
|
self.menu.addAction(self.action_set_core_length)
|
||||||
self.menu.addAction(self.action_set_core_area)
|
self.menu.addAction(self.action_set_core_area)
|
||||||
|
@ -102,41 +103,53 @@ class RealImaginaryMuChart(RealImaginaryChart):
|
||||||
|
|
||||||
def drawChart(self, qp: QtGui.QPainter):
|
def drawChart(self, qp: QtGui.QPainter):
|
||||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||||
qp.drawText(self.leftMargin + 5, 15,
|
qp.drawText(self.leftMargin + 5, 15, f"{self.name}")
|
||||||
f"{self.name}")
|
|
||||||
qp.drawText(5, 15, f"{MU}'")
|
qp.drawText(5, 15, f"{MU}'")
|
||||||
qp.drawText(self.leftMargin + self.dim.width + 10, 15, f"{MU}''")
|
qp.drawText(self.leftMargin + self.dim.width + 10, 15, f"{MU}''")
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin,
|
qp.drawLine(
|
||||||
self.topMargin - 5,
|
self.leftMargin,
|
||||||
self.leftMargin,
|
self.topMargin - 5,
|
||||||
self.topMargin + self.dim.height + 5)
|
self.leftMargin,
|
||||||
qp.drawLine(self.leftMargin - 5,
|
self.topMargin + self.dim.height + 5,
|
||||||
self.topMargin + self.dim.height,
|
)
|
||||||
self.leftMargin + self.dim.width + 5,
|
qp.drawLine(
|
||||||
self.topMargin + self.dim.height)
|
self.leftMargin - 5,
|
||||||
|
self.topMargin + self.dim.height,
|
||||||
|
self.leftMargin + self.dim.width + 5,
|
||||||
|
self.topMargin + self.dim.height,
|
||||||
|
)
|
||||||
self.drawTitle(qp)
|
self.drawTitle(qp)
|
||||||
|
|
||||||
def contextMenuEvent(self, event):
|
def contextMenuEvent(self, event):
|
||||||
self.action_set_fixed_start.setText(
|
self.action_set_fixed_start.setText(
|
||||||
f"Start ({format_frequency_chart(self.minFrequency)})")
|
f"Start ({format_frequency_chart(self.minFrequency)})"
|
||||||
|
)
|
||||||
self.action_set_fixed_stop.setText(
|
self.action_set_fixed_stop.setText(
|
||||||
f"Stop ({format_frequency_chart(self.maxFrequency)})")
|
f"Stop ({format_frequency_chart(self.maxFrequency)})"
|
||||||
|
)
|
||||||
self.action_set_fixed_minimum_real.setText(
|
self.action_set_fixed_minimum_real.setText(
|
||||||
f"Minimum {MU}' ({self.minDisplayReal})")
|
f"Minimum {MU}' ({self.minDisplayReal})"
|
||||||
|
)
|
||||||
self.action_set_fixed_maximum_real.setText(
|
self.action_set_fixed_maximum_real.setText(
|
||||||
f"Maximum {MU}' ({self.maxDisplayReal})")
|
f"Maximum {MU}' ({self.maxDisplayReal})"
|
||||||
|
)
|
||||||
self.action_set_fixed_minimum_imag.setText(
|
self.action_set_fixed_minimum_imag.setText(
|
||||||
f"Minimum {MU}'' ({self.minDisplayImag})")
|
f"Minimum {MU}'' ({self.minDisplayImag})"
|
||||||
|
)
|
||||||
self.action_set_fixed_maximum_imag.setText(
|
self.action_set_fixed_maximum_imag.setText(
|
||||||
f"Maximum {MU}'' ({self.maxDisplayImag})")
|
f"Maximum {MU}'' ({self.maxDisplayImag})"
|
||||||
self.menu.exec_(event.globalPos())
|
)
|
||||||
|
self.menu.exec(event.globalPos())
|
||||||
|
|
||||||
def setCoreLength(self):
|
def setCoreLength(self):
|
||||||
val, selected = QtWidgets.QInputDialog.getDouble(
|
val, selected = QtWidgets.QInputDialog.getDouble(
|
||||||
self, "Core effective length",
|
self,
|
||||||
"Set core effective length in mm", value=self.coreLength,
|
"Core effective length",
|
||||||
decimals=2)
|
"Set core effective length in mm",
|
||||||
|
value=self.coreLength,
|
||||||
|
decimals=2,
|
||||||
|
)
|
||||||
if not selected:
|
if not selected:
|
||||||
return
|
return
|
||||||
if not (self.fixedValues and val >= 0):
|
if not (self.fixedValues and val >= 0):
|
||||||
|
@ -146,9 +159,12 @@ class RealImaginaryMuChart(RealImaginaryChart):
|
||||||
|
|
||||||
def setCoreArea(self):
|
def setCoreArea(self):
|
||||||
val, selected = QtWidgets.QInputDialog.getDouble(
|
val, selected = QtWidgets.QInputDialog.getDouble(
|
||||||
self, "Core effective area",
|
self,
|
||||||
|
"Core effective area",
|
||||||
"Set core cross section area length in mm\N{SUPERSCRIPT TWO}",
|
"Set core cross section area length in mm\N{SUPERSCRIPT TWO}",
|
||||||
value=self.coreArea, decimals=2)
|
value=self.coreArea,
|
||||||
|
decimals=2,
|
||||||
|
)
|
||||||
if not selected:
|
if not selected:
|
||||||
return
|
return
|
||||||
if not (self.fixedValues and val >= 0):
|
if not (self.fixedValues and val >= 0):
|
||||||
|
@ -158,8 +174,11 @@ class RealImaginaryMuChart(RealImaginaryChart):
|
||||||
|
|
||||||
def setCoreWindings(self):
|
def setCoreWindings(self):
|
||||||
val, selected = QtWidgets.QInputDialog.getInt(
|
val, selected = QtWidgets.QInputDialog.getInt(
|
||||||
self, "Core number of windings",
|
self,
|
||||||
"Set core number of windings", value=self.coreWindings)
|
"Core number of windings",
|
||||||
|
"Set core number of windings",
|
||||||
|
value=self.coreWindings,
|
||||||
|
)
|
||||||
if not selected:
|
if not selected:
|
||||||
return
|
return
|
||||||
if not (self.fixedValues and val >= 0):
|
if not (self.fixedValues and val >= 0):
|
||||||
|
@ -176,6 +195,7 @@ class RealImaginaryMuChart(RealImaginaryChart):
|
||||||
# Core length and core area are in mm and mm2 respectively
|
# Core length and core area are in mm and mm2 respectively
|
||||||
# note: mu_r = mu' - j * mu ''
|
# note: mu_r = mu' - j * mu ''
|
||||||
return np.conj(
|
return np.conj(
|
||||||
inductance * (self.coreLength / 1e3) /
|
inductance
|
||||||
(mu_0 * self.coreWindings**2 * (self.coreArea / 1e6))
|
* (self.coreLength / 1e3)
|
||||||
|
/ (mu_0 * self.coreWindings**2 * (self.coreArea / 1e6))
|
||||||
)
|
)
|
|
@ -18,7 +18,7 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PyQt5 import QtWidgets, QtGui
|
from PyQt6 import QtGui
|
||||||
|
|
||||||
from NanoVNASaver.Formatting import format_frequency_chart
|
from NanoVNASaver.Formatting import format_frequency_chart
|
||||||
from NanoVNASaver.RFTools import Datapoint
|
from NanoVNASaver.RFTools import Datapoint
|
||||||
|
@ -34,25 +34,33 @@ class RealImaginaryZChart(RealImaginaryChart):
|
||||||
super().__init__(name)
|
super().__init__(name)
|
||||||
self.y_menu.addSeparator()
|
self.y_menu.addSeparator()
|
||||||
|
|
||||||
self.action_set_fixed_maximum_real = QtWidgets.QAction(
|
self.action_set_fixed_maximum_real = QtGui.QAction(
|
||||||
f"Maximum R ({self.maxDisplayReal})")
|
f"Maximum R ({self.maxDisplayReal})"
|
||||||
|
)
|
||||||
self.action_set_fixed_maximum_real.triggered.connect(
|
self.action_set_fixed_maximum_real.triggered.connect(
|
||||||
self.setMaximumRealValue)
|
self.setMaximumRealValue
|
||||||
|
)
|
||||||
|
|
||||||
self.action_set_fixed_minimum_real = QtWidgets.QAction(
|
self.action_set_fixed_minimum_real = QtGui.QAction(
|
||||||
f"Minimum R ({self.minDisplayReal})")
|
f"Minimum R ({self.minDisplayReal})"
|
||||||
|
)
|
||||||
self.action_set_fixed_minimum_real.triggered.connect(
|
self.action_set_fixed_minimum_real.triggered.connect(
|
||||||
self.setMinimumRealValue)
|
self.setMinimumRealValue
|
||||||
|
)
|
||||||
|
|
||||||
self.action_set_fixed_maximum_imag = QtWidgets.QAction(
|
self.action_set_fixed_maximum_imag = QtGui.QAction(
|
||||||
f"Maximum jX ({self.maxDisplayImag})")
|
f"Maximum jX ({self.maxDisplayImag})"
|
||||||
|
)
|
||||||
self.action_set_fixed_maximum_imag.triggered.connect(
|
self.action_set_fixed_maximum_imag.triggered.connect(
|
||||||
self.setMaximumImagValue)
|
self.setMaximumImagValue
|
||||||
|
)
|
||||||
|
|
||||||
self.action_set_fixed_minimum_imag = QtWidgets.QAction(
|
self.action_set_fixed_minimum_imag = QtGui.QAction(
|
||||||
f"Minimum jX ({self.minDisplayImag})")
|
f"Minimum jX ({self.minDisplayImag})"
|
||||||
|
)
|
||||||
self.action_set_fixed_minimum_imag.triggered.connect(
|
self.action_set_fixed_minimum_imag.triggered.connect(
|
||||||
self.setMinimumImagValue)
|
self.setMinimumImagValue
|
||||||
|
)
|
||||||
|
|
||||||
self.y_menu.addAction(self.action_set_fixed_maximum_real)
|
self.y_menu.addAction(self.action_set_fixed_maximum_real)
|
||||||
self.y_menu.addAction(self.action_set_fixed_minimum_real)
|
self.y_menu.addAction(self.action_set_fixed_minimum_real)
|
||||||
|
@ -62,35 +70,44 @@ class RealImaginaryZChart(RealImaginaryChart):
|
||||||
|
|
||||||
def drawChart(self, qp: QtGui.QPainter):
|
def drawChart(self, qp: QtGui.QPainter):
|
||||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||||
qp.drawText(self.leftMargin + 5, 15,
|
qp.drawText(self.leftMargin + 5, 15, f"{self.name} (\N{OHM SIGN})")
|
||||||
f"{self.name} (\N{OHM SIGN})")
|
|
||||||
qp.drawText(10, 15, "R")
|
qp.drawText(10, 15, "R")
|
||||||
qp.drawText(self.leftMargin + self.dim.width + 10, 15, "X")
|
qp.drawText(self.leftMargin + self.dim.width + 10, 15, "X")
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin,
|
qp.drawLine(
|
||||||
self.topMargin - 5,
|
self.leftMargin,
|
||||||
self.leftMargin,
|
self.topMargin - 5,
|
||||||
self.topMargin + self.dim.height + 5)
|
self.leftMargin,
|
||||||
qp.drawLine(self.leftMargin - 5,
|
self.topMargin + self.dim.height + 5,
|
||||||
self.topMargin + self.dim.height,
|
)
|
||||||
self.leftMargin + self.dim.width + 5,
|
qp.drawLine(
|
||||||
self.topMargin + self.dim.height)
|
self.leftMargin - 5,
|
||||||
|
self.topMargin + self.dim.height,
|
||||||
|
self.leftMargin + self.dim.width + 5,
|
||||||
|
self.topMargin + self.dim.height,
|
||||||
|
)
|
||||||
self.drawTitle(qp)
|
self.drawTitle(qp)
|
||||||
|
|
||||||
def contextMenuEvent(self, event):
|
def contextMenuEvent(self, event):
|
||||||
self.action_set_fixed_start.setText(
|
self.action_set_fixed_start.setText(
|
||||||
f"Start ({format_frequency_chart(self.minFrequency)})")
|
f"Start ({format_frequency_chart(self.minFrequency)})"
|
||||||
|
)
|
||||||
self.action_set_fixed_stop.setText(
|
self.action_set_fixed_stop.setText(
|
||||||
f"Stop ({format_frequency_chart(self.maxFrequency)})")
|
f"Stop ({format_frequency_chart(self.maxFrequency)})"
|
||||||
|
)
|
||||||
self.action_set_fixed_minimum_real.setText(
|
self.action_set_fixed_minimum_real.setText(
|
||||||
f"Minimum R ({self.minDisplayReal})")
|
f"Minimum R ({self.minDisplayReal})"
|
||||||
|
)
|
||||||
self.action_set_fixed_maximum_real.setText(
|
self.action_set_fixed_maximum_real.setText(
|
||||||
f"Maximum R ({self.maxDisplayReal})")
|
f"Maximum R ({self.maxDisplayReal})"
|
||||||
|
)
|
||||||
self.action_set_fixed_minimum_imag.setText(
|
self.action_set_fixed_minimum_imag.setText(
|
||||||
f"Minimum jX ({self.minDisplayImag})")
|
f"Minimum jX ({self.minDisplayImag})"
|
||||||
|
)
|
||||||
self.action_set_fixed_maximum_imag.setText(
|
self.action_set_fixed_maximum_imag.setText(
|
||||||
f"Maximum jX ({self.maxDisplayImag})")
|
f"Maximum jX ({self.maxDisplayImag})"
|
||||||
self.menu.exec_(event.globalPos())
|
)
|
||||||
|
self.menu.exec(event.globalPos())
|
||||||
|
|
||||||
def value(self, p: Datapoint) -> complex:
|
def value(self, p: Datapoint) -> complex:
|
||||||
return self.impedance(p)
|
return self.impedance(p)
|
|
@ -25,6 +25,5 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RealImaginaryZSeriesChart(RealImaginaryZChart):
|
class RealImaginaryZSeriesChart(RealImaginaryZChart):
|
||||||
|
|
||||||
def impedance(self, p: Datapoint) -> complex:
|
def impedance(self, p: Datapoint) -> complex:
|
||||||
return p.seriesImpedance()
|
return p.seriesImpedance()
|
|
@ -25,6 +25,5 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RealImaginaryZShuntChart(RealImaginaryZChart):
|
class RealImaginaryZShuntChart(RealImaginaryZChart):
|
||||||
|
|
||||||
def impedance(self, p: Datapoint) -> complex:
|
def impedance(self, p: Datapoint) -> complex:
|
||||||
return p.shuntImpedance()
|
return p.shuntImpedance()
|
|
@ -17,9 +17,8 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from PyQt5 import QtGui
|
from PyQt6 import QtGui
|
||||||
|
|
||||||
from NanoVNASaver.RFTools import Datapoint
|
from NanoVNASaver.RFTools import Datapoint
|
||||||
from NanoVNASaver.Charts.Chart import Chart
|
from NanoVNASaver.Charts.Chart import Chart
|
||||||
|
@ -52,14 +51,18 @@ class SParameterChart(FrequencyChart):
|
||||||
qp.drawText(10, 15, "Real")
|
qp.drawText(10, 15, "Real")
|
||||||
qp.drawText(self.leftMargin + self.dim.width - 15, 15, "Imag")
|
qp.drawText(self.leftMargin + self.dim.width - 15, 15, "Imag")
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin,
|
qp.drawLine(
|
||||||
self.topMargin - 5,
|
self.leftMargin,
|
||||||
self.leftMargin,
|
self.topMargin - 5,
|
||||||
self.topMargin + self.dim.height + 5)
|
self.leftMargin,
|
||||||
qp.drawLine(self.leftMargin - 5,
|
self.topMargin + self.dim.height + 5,
|
||||||
self.topMargin + self.dim.height,
|
)
|
||||||
self.leftMargin + self.dim.width,
|
qp.drawLine(
|
||||||
self.topMargin + self.dim.height)
|
self.leftMargin - 5,
|
||||||
|
self.topMargin + self.dim.height,
|
||||||
|
self.leftMargin + self.dim.width,
|
||||||
|
self.topMargin + self.dim.height,
|
||||||
|
)
|
||||||
|
|
||||||
def drawValues(self, qp: QtGui.QPainter):
|
def drawValues(self, qp: QtGui.QPainter):
|
||||||
if len(self.data) == 0 and len(self.reference) == 0:
|
if len(self.data) == 0 and len(self.reference) == 0:
|
||||||
|
@ -85,46 +88,60 @@ class SParameterChart(FrequencyChart):
|
||||||
val = int(minValue + i * tick_step)
|
val = int(minValue + i * tick_step)
|
||||||
y = self.topMargin + (maxValue - val) // span * self.dim.height
|
y = self.topMargin + (maxValue - val) // span * self.dim.height
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin - 5, y,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width, y)
|
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
|
||||||
|
)
|
||||||
if val > minValue and val != maxValue:
|
if val > minValue and val != maxValue:
|
||||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||||
qp.drawText(3, y + 4, str(round(val, 2)))
|
qp.drawText(3, y + 4, str(round(val, 2)))
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin - 5, self.topMargin,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width, self.topMargin)
|
self.leftMargin - 5,
|
||||||
|
self.topMargin,
|
||||||
|
self.leftMargin + self.dim.width,
|
||||||
|
self.topMargin,
|
||||||
|
)
|
||||||
|
|
||||||
qp.setPen(Chart.color.text)
|
qp.setPen(Chart.color.text)
|
||||||
qp.drawText(3, self.topMargin + 4, f"{maxValue}")
|
qp.drawText(3, self.topMargin + 4, f"{maxValue}")
|
||||||
qp.drawText(3, self.dim.height + self.topMargin, f"{minValue}")
|
qp.drawText(3, self.dim.height + self.topMargin, f"{minValue}")
|
||||||
self.drawFrequencyTicks(qp)
|
self.drawFrequencyTicks(qp)
|
||||||
self.drawData(qp, self.data, Chart.color.sweep, self.getReYPosition)
|
self.drawData(qp, self.data, Chart.color.sweep, self.getReYPosition)
|
||||||
self.drawData(qp, self.reference, Chart.color.reference,
|
self.drawData(
|
||||||
self.getReYPosition)
|
qp, self.reference, Chart.color.reference, self.getReYPosition
|
||||||
self.drawData(qp, self.data, Chart.color.sweep_secondary,
|
)
|
||||||
self.getImYPosition)
|
self.drawData(
|
||||||
self.drawData(qp, self.reference,
|
qp, self.data, Chart.color.sweep_secondary, self.getImYPosition
|
||||||
Chart.color.reference_secondary, self.getImYPosition)
|
)
|
||||||
|
self.drawData(
|
||||||
|
qp,
|
||||||
|
self.reference,
|
||||||
|
Chart.color.reference_secondary,
|
||||||
|
self.getImYPosition,
|
||||||
|
)
|
||||||
|
|
||||||
self.drawMarkers(qp, y_function=self.getReYPosition)
|
self.drawMarkers(qp, y_function=self.getReYPosition)
|
||||||
self.drawMarkers(qp, y_function=self.getImYPosition)
|
self.drawMarkers(qp, y_function=self.getImYPosition)
|
||||||
|
|
||||||
def getYPosition(self, d: Datapoint) -> int:
|
def getYPosition(self, d: Datapoint) -> int:
|
||||||
return int(
|
return int(
|
||||||
self.topMargin + (self.maxValue - d.re) / self.span *
|
self.topMargin
|
||||||
self.dim.height)
|
+ (self.maxValue - d.re) / self.span * self.dim.height
|
||||||
|
)
|
||||||
|
|
||||||
def getReYPosition(self, d: Datapoint) -> int:
|
def getReYPosition(self, d: Datapoint) -> int:
|
||||||
return int(
|
return int(
|
||||||
self.topMargin + (self.maxValue - d.re) / self.span *
|
self.topMargin
|
||||||
self.dim.height)
|
+ (self.maxValue - d.re) / self.span * self.dim.height
|
||||||
|
)
|
||||||
|
|
||||||
def getImYPosition(self, d: Datapoint) -> int:
|
def getImYPosition(self, d: Datapoint) -> int:
|
||||||
return int(
|
return int(
|
||||||
self.topMargin + (self.maxValue - d.im) / self.span *
|
self.topMargin
|
||||||
self.dim.height)
|
+ (self.maxValue - d.im) / self.span * self.dim.height
|
||||||
|
)
|
||||||
|
|
||||||
def valueAtPosition(self, y) -> List[float]:
|
def valueAtPosition(self, y) -> list[float]:
|
||||||
absy = y - self.topMargin
|
absy = y - self.topMargin
|
||||||
val = -1 * ((absy / self.dim.height * self.span) - self.maxValue)
|
val = -1 * ((absy / self.dim.height * self.span) - self.maxValue)
|
||||||
return [val]
|
return [val]
|
|
@ -0,0 +1,165 @@
|
||||||
|
# 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 logging
|
||||||
|
from PyQt6 import QtGui, QtCore
|
||||||
|
|
||||||
|
from NanoVNASaver.Charts.Chart import Chart
|
||||||
|
from NanoVNASaver.Charts.Square import SquareChart
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SmithChart(SquareChart):
|
||||||
|
def drawChart(self, qp: QtGui.QPainter) -> None:
|
||||||
|
center_x = self.width() // 2
|
||||||
|
center_y = self.height() // 2
|
||||||
|
width_2 = self.dim.width // 2
|
||||||
|
height_2 = self.dim.height // 2
|
||||||
|
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||||
|
qp.drawText(3, 15, self.name)
|
||||||
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
|
qp.drawEllipse(QtCore.QPoint(center_x, center_y), width_2, height_2)
|
||||||
|
qp.drawLine(center_x - width_2, center_y, center_x + width_2, center_y)
|
||||||
|
|
||||||
|
qp.drawEllipse(
|
||||||
|
QtCore.QPoint(center_x + int(self.dim.width / 4), center_y),
|
||||||
|
self.dim.width // 4,
|
||||||
|
self.dim.height // 4,
|
||||||
|
) # Re(Z) = 1
|
||||||
|
qp.drawEllipse(
|
||||||
|
QtCore.QPoint(center_x + self.dim.width // 3, center_y),
|
||||||
|
self.dim.width // 6,
|
||||||
|
self.dim.height // 6,
|
||||||
|
) # Re(Z) = 2
|
||||||
|
qp.drawEllipse(
|
||||||
|
QtCore.QPoint(center_x + 3 * self.dim.width // 8, center_y),
|
||||||
|
self.dim.width // 8,
|
||||||
|
self.dim.height // 8,
|
||||||
|
) # Re(Z) = 3
|
||||||
|
qp.drawEllipse(
|
||||||
|
QtCore.QPoint(center_x + 5 * self.dim.width // 12, center_y),
|
||||||
|
self.dim.width // 12,
|
||||||
|
self.dim.height // 12,
|
||||||
|
) # Re(Z) = 5
|
||||||
|
qp.drawEllipse(
|
||||||
|
QtCore.QPoint(center_x + self.dim.width // 6, center_y),
|
||||||
|
self.dim.width // 3,
|
||||||
|
self.dim.height // 3,
|
||||||
|
) # Re(Z) = 0.5
|
||||||
|
qp.drawEllipse(
|
||||||
|
QtCore.QPoint(center_x + self.dim.width // 12, center_y),
|
||||||
|
5 * self.dim.width // 12,
|
||||||
|
5 * self.dim.height // 12,
|
||||||
|
) # Re(Z) = 0.2
|
||||||
|
|
||||||
|
qp.drawArc(
|
||||||
|
center_x + 3 * self.dim.width // 8,
|
||||||
|
center_y,
|
||||||
|
self.dim.width // 4,
|
||||||
|
self.dim.width // 4,
|
||||||
|
90 * 16,
|
||||||
|
152 * 16,
|
||||||
|
) # Im(Z) = -5
|
||||||
|
qp.drawArc(
|
||||||
|
center_x + 3 * self.dim.width // 8,
|
||||||
|
center_y,
|
||||||
|
self.dim.width // 4,
|
||||||
|
-self.dim.width // 4,
|
||||||
|
-90 * 16,
|
||||||
|
-152 * 16,
|
||||||
|
) # Im(Z) = 5
|
||||||
|
qp.drawArc(
|
||||||
|
center_x + self.dim.width // 4,
|
||||||
|
center_y,
|
||||||
|
width_2,
|
||||||
|
height_2,
|
||||||
|
90 * 16,
|
||||||
|
127 * 16,
|
||||||
|
) # Im(Z) = -2
|
||||||
|
qp.drawArc(
|
||||||
|
center_x + self.dim.width // 4,
|
||||||
|
center_y,
|
||||||
|
width_2,
|
||||||
|
-height_2,
|
||||||
|
-90 * 16,
|
||||||
|
-127 * 16,
|
||||||
|
) # Im(Z) = 2
|
||||||
|
qp.drawArc(
|
||||||
|
center_x,
|
||||||
|
center_y,
|
||||||
|
self.dim.width,
|
||||||
|
self.dim.height,
|
||||||
|
90 * 16,
|
||||||
|
90 * 16,
|
||||||
|
) # Im(Z) = -1
|
||||||
|
qp.drawArc(
|
||||||
|
center_x,
|
||||||
|
center_y,
|
||||||
|
self.dim.width,
|
||||||
|
-self.dim.height,
|
||||||
|
-90 * 16,
|
||||||
|
-90 * 16,
|
||||||
|
) # Im(Z) = 1
|
||||||
|
qp.drawArc(
|
||||||
|
center_x - width_2,
|
||||||
|
center_y,
|
||||||
|
self.dim.width * 2,
|
||||||
|
self.dim.height * 2,
|
||||||
|
int(99.5 * 16),
|
||||||
|
int(43.5 * 16),
|
||||||
|
) # Im(Z) = -0.5
|
||||||
|
qp.drawArc(
|
||||||
|
center_x - width_2,
|
||||||
|
center_y,
|
||||||
|
self.dim.width * 2,
|
||||||
|
-self.dim.height * 2,
|
||||||
|
int(-99.5 * 16),
|
||||||
|
int(-43.5 * 16),
|
||||||
|
) # Im(Z) = 0.5
|
||||||
|
qp.drawArc(
|
||||||
|
center_x - self.dim.width * 2,
|
||||||
|
center_y,
|
||||||
|
self.dim.width * 5,
|
||||||
|
self.dim.height * 5,
|
||||||
|
int(93.85 * 16),
|
||||||
|
int(18.85 * 16),
|
||||||
|
) # Im(Z) = -0.2
|
||||||
|
qp.drawArc(
|
||||||
|
center_x - self.dim.width * 2,
|
||||||
|
center_y,
|
||||||
|
self.dim.width * 5,
|
||||||
|
-self.dim.height * 5,
|
||||||
|
int(-93.85 * 16),
|
||||||
|
int(-18.85 * 16),
|
||||||
|
) # Im(Z) = 0.2
|
||||||
|
|
||||||
|
self.drawTitle(qp)
|
||||||
|
|
||||||
|
qp.setPen(Chart.color.swr)
|
||||||
|
for swr in self.swrMarkers:
|
||||||
|
if swr <= 1:
|
||||||
|
continue
|
||||||
|
gamma = (swr - 1) / (swr + 1)
|
||||||
|
r = int(gamma * self.dim.width / 2)
|
||||||
|
qp.drawEllipse(QtCore.QPoint(center_x, center_y), r, r)
|
||||||
|
qp.drawText(
|
||||||
|
QtCore.QRect(center_x - 50, center_y - 4 + r, 100, 20),
|
||||||
|
QtCore.Qt.AlignmentFlag.AlignCenter,
|
||||||
|
f"{swr}",
|
||||||
|
)
|
|
@ -18,9 +18,8 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
from PyQt6 import QtGui, QtCore, QtWidgets
|
||||||
|
|
||||||
from NanoVNASaver.Charts.Chart import Chart
|
from NanoVNASaver.Charts.Chart import Chart
|
||||||
from NanoVNASaver.RFTools import Datapoint
|
from NanoVNASaver.RFTools import Datapoint
|
||||||
|
@ -29,18 +28,19 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SquareChart(Chart):
|
class SquareChart(Chart):
|
||||||
def __init__(self, name=''):
|
def __init__(self, name=""):
|
||||||
super().__init__(name)
|
super().__init__(name)
|
||||||
sizepolicy = QtWidgets.QSizePolicy(
|
sizepolicy = QtWidgets.QSizePolicy(
|
||||||
QtWidgets.QSizePolicy.Fixed,
|
QtWidgets.QSizePolicy.Policy.Fixed,
|
||||||
QtWidgets.QSizePolicy.MinimumExpanding)
|
QtWidgets.QSizePolicy.Policy.MinimumExpanding,
|
||||||
|
)
|
||||||
self.setSizePolicy(sizepolicy)
|
self.setSizePolicy(sizepolicy)
|
||||||
self.dim.width = 250
|
self.dim.width = 250
|
||||||
self.dim.height = 250
|
self.dim.height = 250
|
||||||
self.setMinimumSize(self.dim.width + 40, self.dim.height + 40)
|
self.setMinimumSize(self.dim.width + 40, self.dim.height + 40)
|
||||||
|
|
||||||
pal = QtGui.QPalette()
|
pal = QtGui.QPalette()
|
||||||
pal.setColor(QtGui.QPalette.Background, Chart.color.background)
|
pal.setColor(QtGui.QPalette.ColorRole.Window, Chart.color.background)
|
||||||
self.setPalette(pal)
|
self.setPalette(pal)
|
||||||
self.setAutoFillBackground(True)
|
self.setAutoFillBackground(True)
|
||||||
|
|
||||||
|
@ -53,8 +53,14 @@ class SquareChart(Chart):
|
||||||
def drawChart(self, qp: QtGui.QPainter) -> None:
|
def drawChart(self, qp: QtGui.QPainter) -> None:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def draw_data(self, qp: QtGui.QPainter, color: QtGui.QColor,
|
def draw_data(
|
||||||
data: List[Datapoint], fstart: int = 0, fstop: int = 0):
|
self,
|
||||||
|
qp: QtGui.QPainter,
|
||||||
|
color: QtGui.QColor,
|
||||||
|
data: list[Datapoint],
|
||||||
|
fstart: int = 0,
|
||||||
|
fstop: int = 0,
|
||||||
|
):
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
fstop = fstop or data[-1].freq
|
fstop = fstop or data[-1].freq
|
||||||
|
@ -65,8 +71,7 @@ class SquareChart(Chart):
|
||||||
|
|
||||||
qp.setPen(pen)
|
qp.setPen(pen)
|
||||||
prev_x = self.getXPosition(data[0])
|
prev_x = self.getXPosition(data[0])
|
||||||
prev_y = int(self.height() / 2 + data[0].im * -1 *
|
prev_y = int(self.height() / 2 + data[0].im * -1 * self.dim.height / 2)
|
||||||
self.dim.height / 2)
|
|
||||||
for i, d in enumerate(data):
|
for i, d in enumerate(data):
|
||||||
x = self.getXPosition(d)
|
x = self.getXPosition(d)
|
||||||
y = int(self.height() / 2 + d.im * -1 * self.dim.height / 2)
|
y = int(self.height() / 2 + d.im * -1 * self.dim.height / 2)
|
||||||
|
@ -85,14 +90,15 @@ class SquareChart(Chart):
|
||||||
|
|
||||||
fstart = self.data[0].freq if self.data else 0
|
fstart = self.data[0].freq if self.data else 0
|
||||||
fstop = self.data[-1].freq if self.data else 0
|
fstop = self.data[-1].freq if self.data else 0
|
||||||
self.draw_data(qp, Chart.color.reference,
|
self.draw_data(qp, Chart.color.reference, self.reference, fstart, fstop)
|
||||||
self.reference, fstart, fstop)
|
|
||||||
|
|
||||||
for m in self.markers:
|
for m in self.markers:
|
||||||
if m.location != -1 and m.location < len(self.data):
|
if m.location != -1 and m.location < len(self.data):
|
||||||
x = self.getXPosition(self.data[m.location])
|
x = self.getXPosition(self.data[m.location])
|
||||||
y = int(self.height() // 2 -
|
y = int(
|
||||||
self.data[m.location].im * self.dim.height // 2)
|
self.height() // 2
|
||||||
|
- self.data[m.location].im * self.dim.height // 2
|
||||||
|
)
|
||||||
self.drawMarker(x, y, qp, m.color, self.markers.index(m) + 1)
|
self.drawMarker(x, y, qp, m.color, self.markers.index(m) + 1)
|
||||||
|
|
||||||
def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
|
def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
|
||||||
|
@ -106,19 +112,21 @@ class SquareChart(Chart):
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def mouseMoveEvent(self, a0: QtGui.QMouseEvent):
|
def mouseMoveEvent(self, a0: QtGui.QMouseEvent):
|
||||||
if a0.buttons() == QtCore.Qt.RightButton:
|
if a0.buttons() == QtCore.Qt.MouseButton.RightButton:
|
||||||
a0.ignore()
|
a0.ignore()
|
||||||
return
|
return
|
||||||
|
|
||||||
x = a0.x()
|
x = a0.position().x()
|
||||||
y = a0.y()
|
y = a0.position().y()
|
||||||
absx = x - (self.width() - self.dim.width) / 2
|
absx = x - (self.width() - self.dim.width) / 2
|
||||||
absy = y - (self.height() - self.dim.height) / 2
|
absy = y - (self.height() - self.dim.height) / 2
|
||||||
if (absx < 0 or
|
if (
|
||||||
absx > self.dim.width or
|
absx < 0
|
||||||
absy < 0 or
|
or absx > self.dim.width
|
||||||
absy > self.dim.height or
|
or absy < 0
|
||||||
(not self.data and not self.reference)):
|
or absy > self.dim.height
|
||||||
|
or (not self.data and not self.reference)
|
||||||
|
):
|
||||||
a0.ignore()
|
a0.ignore()
|
||||||
return
|
return
|
||||||
a0.accept()
|
a0.accept()
|
||||||
|
@ -133,8 +141,9 @@ class SquareChart(Chart):
|
||||||
|
|
||||||
positions = [
|
positions = [
|
||||||
math.sqrt(
|
math.sqrt(
|
||||||
(x - (width_2 + d.re * dim_x_2))**2 +
|
(x - (width_2 + d.re * dim_x_2)) ** 2
|
||||||
(y - (height_2 - d.im * dim_y_2))**2)
|
+ (y - (height_2 - d.im * dim_y_2)) ** 2
|
||||||
|
)
|
||||||
for d in target
|
for d in target
|
||||||
]
|
]
|
||||||
|
|
|
@ -20,7 +20,18 @@ import math
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PyQt5 import QtWidgets, QtGui, QtCore
|
from PyQt6.QtCore import QPoint, QRect, Qt
|
||||||
|
from PyQt6.QtGui import (
|
||||||
|
QAction,
|
||||||
|
QActionGroup,
|
||||||
|
QMouseEvent,
|
||||||
|
QPalette,
|
||||||
|
QPainter,
|
||||||
|
QPaintEvent,
|
||||||
|
QPen,
|
||||||
|
QResizeEvent,
|
||||||
|
)
|
||||||
|
from PyQt6.QtWidgets import QInputDialog, QMenu, QSizePolicy
|
||||||
|
|
||||||
from NanoVNASaver.Charts.Chart import Chart
|
from NanoVNASaver.Charts.Chart import Chart
|
||||||
|
|
||||||
|
@ -47,75 +58,85 @@ class TDRChart(Chart):
|
||||||
|
|
||||||
self.setMinimumSize(300, 300)
|
self.setMinimumSize(300, 300)
|
||||||
self.setSizePolicy(
|
self.setSizePolicy(
|
||||||
QtWidgets.QSizePolicy(
|
QSizePolicy(
|
||||||
QtWidgets.QSizePolicy.MinimumExpanding,
|
QSizePolicy.Policy.MinimumExpanding,
|
||||||
QtWidgets.QSizePolicy.MinimumExpanding))
|
QSizePolicy.Policy.MinimumExpanding,
|
||||||
pal = QtGui.QPalette()
|
)
|
||||||
pal.setColor(QtGui.QPalette.Background, Chart.color.background)
|
)
|
||||||
|
pal = QPalette()
|
||||||
|
pal.setColor(QPalette.ColorRole.Window, Chart.color.background)
|
||||||
self.setPalette(pal)
|
self.setPalette(pal)
|
||||||
self.setAutoFillBackground(True)
|
self.setAutoFillBackground(True)
|
||||||
|
|
||||||
self.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
|
self.setContextMenuPolicy(Qt.ContextMenuPolicy.DefaultContextMenu)
|
||||||
self.menu = QtWidgets.QMenu()
|
self.menu = QMenu()
|
||||||
|
|
||||||
self.reset = QtWidgets.QAction("Reset")
|
self.reset = QAction("Reset")
|
||||||
self.reset.triggered.connect(self.resetDisplayLimits)
|
self.reset.triggered.connect(self.resetDisplayLimits)
|
||||||
self.menu.addAction(self.reset)
|
self.menu.addAction(self.reset)
|
||||||
|
|
||||||
self.x_menu = QtWidgets.QMenu("Length axis")
|
self.x_menu = QMenu("Length axis")
|
||||||
self.mode_group = QtWidgets.QActionGroup(self.x_menu)
|
self.mode_group = QActionGroup(self.x_menu)
|
||||||
self.action_automatic = QtWidgets.QAction("Automatic")
|
self.action_automatic = QAction("Automatic")
|
||||||
self.action_automatic.setCheckable(True)
|
self.action_automatic.setCheckable(True)
|
||||||
self.action_automatic.setChecked(True)
|
self.action_automatic.setChecked(True)
|
||||||
self.action_automatic.changed.connect(
|
self.action_automatic.changed.connect(
|
||||||
lambda: self.setFixedSpan(self.action_fixed_span.isChecked()))
|
lambda: self.setFixedSpan(self.action_fixed_span.isChecked())
|
||||||
self.action_fixed_span = QtWidgets.QAction("Fixed span")
|
)
|
||||||
|
self.action_fixed_span = QAction("Fixed span")
|
||||||
self.action_fixed_span.setCheckable(True)
|
self.action_fixed_span.setCheckable(True)
|
||||||
self.action_fixed_span.changed.connect(
|
self.action_fixed_span.changed.connect(
|
||||||
lambda: self.setFixedSpan(self.action_fixed_span.isChecked()))
|
lambda: self.setFixedSpan(self.action_fixed_span.isChecked())
|
||||||
|
)
|
||||||
self.mode_group.addAction(self.action_automatic)
|
self.mode_group.addAction(self.action_automatic)
|
||||||
self.mode_group.addAction(self.action_fixed_span)
|
self.mode_group.addAction(self.action_fixed_span)
|
||||||
self.x_menu.addAction(self.action_automatic)
|
self.x_menu.addAction(self.action_automatic)
|
||||||
self.x_menu.addAction(self.action_fixed_span)
|
self.x_menu.addAction(self.action_fixed_span)
|
||||||
self.x_menu.addSeparator()
|
self.x_menu.addSeparator()
|
||||||
|
|
||||||
self.action_set_fixed_start = QtWidgets.QAction(
|
self.action_set_fixed_start = QAction(
|
||||||
f"Start ({self.minDisplayLength})")
|
f"Start ({self.minDisplayLength})"
|
||||||
|
)
|
||||||
self.action_set_fixed_start.triggered.connect(self.setMinimumLength)
|
self.action_set_fixed_start.triggered.connect(self.setMinimumLength)
|
||||||
|
|
||||||
self.action_set_fixed_stop = QtWidgets.QAction(
|
self.action_set_fixed_stop = QAction(f"Stop ({self.maxDisplayLength})")
|
||||||
f"Stop ({self.maxDisplayLength})")
|
|
||||||
self.action_set_fixed_stop.triggered.connect(self.setMaximumLength)
|
self.action_set_fixed_stop.triggered.connect(self.setMaximumLength)
|
||||||
|
|
||||||
self.x_menu.addAction(self.action_set_fixed_start)
|
self.x_menu.addAction(self.action_set_fixed_start)
|
||||||
self.x_menu.addAction(self.action_set_fixed_stop)
|
self.x_menu.addAction(self.action_set_fixed_stop)
|
||||||
|
|
||||||
self.y_menu = QtWidgets.QMenu("Impedance axis")
|
self.y_menu = QMenu("Impedance axis")
|
||||||
self.y_mode_group = QtWidgets.QActionGroup(self.y_menu)
|
self.y_mode_group = QActionGroup(self.y_menu)
|
||||||
self.y_action_automatic = QtWidgets.QAction("Automatic")
|
self.y_action_automatic = QAction("Automatic")
|
||||||
self.y_action_automatic.setCheckable(True)
|
self.y_action_automatic.setCheckable(True)
|
||||||
self.y_action_automatic.setChecked(True)
|
self.y_action_automatic.setChecked(True)
|
||||||
self.y_action_automatic.changed.connect(
|
self.y_action_automatic.changed.connect(
|
||||||
lambda: self.setFixedValues(self.y_action_fixed.isChecked()))
|
lambda: self.setFixedValues(self.y_action_fixed.isChecked())
|
||||||
self.y_action_fixed = QtWidgets.QAction("Fixed")
|
)
|
||||||
|
self.y_action_fixed = QAction("Fixed")
|
||||||
self.y_action_fixed.setCheckable(True)
|
self.y_action_fixed.setCheckable(True)
|
||||||
self.y_action_fixed.changed.connect(
|
self.y_action_fixed.changed.connect(
|
||||||
lambda: self.setFixedValues(self.y_action_fixed.isChecked()))
|
lambda: self.setFixedValues(self.y_action_fixed.isChecked())
|
||||||
|
)
|
||||||
self.y_mode_group.addAction(self.y_action_automatic)
|
self.y_mode_group.addAction(self.y_action_automatic)
|
||||||
self.y_mode_group.addAction(self.y_action_fixed)
|
self.y_mode_group.addAction(self.y_action_fixed)
|
||||||
self.y_menu.addAction(self.y_action_automatic)
|
self.y_menu.addAction(self.y_action_automatic)
|
||||||
self.y_menu.addAction(self.y_action_fixed)
|
self.y_menu.addAction(self.y_action_fixed)
|
||||||
self.y_menu.addSeparator()
|
self.y_menu.addSeparator()
|
||||||
|
|
||||||
self.y_action_set_fixed_maximum = QtWidgets.QAction(
|
self.y_action_set_fixed_maximum = QAction(
|
||||||
f"Maximum ({self.maxImpedance})")
|
f"Maximum ({self.maxImpedance})"
|
||||||
|
)
|
||||||
self.y_action_set_fixed_maximum.triggered.connect(
|
self.y_action_set_fixed_maximum.triggered.connect(
|
||||||
self.setMaximumImpedance)
|
self.setMaximumImpedance
|
||||||
|
)
|
||||||
|
|
||||||
self.y_action_set_fixed_minimum = QtWidgets.QAction(
|
self.y_action_set_fixed_minimum = QAction(
|
||||||
f"Minimum ({self.minImpedance})")
|
f"Minimum ({self.minImpedance})"
|
||||||
|
)
|
||||||
self.y_action_set_fixed_minimum.triggered.connect(
|
self.y_action_set_fixed_minimum.triggered.connect(
|
||||||
self.setMinimumImpedance)
|
self.setMinimumImpedance
|
||||||
|
)
|
||||||
|
|
||||||
self.y_menu.addAction(self.y_action_set_fixed_maximum)
|
self.y_menu.addAction(self.y_action_set_fixed_maximum)
|
||||||
self.y_menu.addAction(self.y_action_set_fixed_minimum)
|
self.y_menu.addAction(self.y_action_set_fixed_minimum)
|
||||||
|
@ -124,28 +145,31 @@ class TDRChart(Chart):
|
||||||
self.menu.addMenu(self.y_menu)
|
self.menu.addMenu(self.y_menu)
|
||||||
self.menu.addSeparator()
|
self.menu.addSeparator()
|
||||||
self.menu.addAction(self.action_save_screenshot)
|
self.menu.addAction(self.action_save_screenshot)
|
||||||
self.action_popout = QtWidgets.QAction("Popout chart")
|
self.action_popout = QAction("Popout chart")
|
||||||
self.action_popout.triggered.connect(
|
self.action_popout.triggered.connect(
|
||||||
lambda: self.popoutRequested.emit(self))
|
lambda: self.popoutRequested.emit(self)
|
||||||
|
)
|
||||||
self.menu.addAction(self.action_popout)
|
self.menu.addAction(self.action_popout)
|
||||||
|
|
||||||
self.dim.width = self.width() - self.leftMargin - self.rightMargin
|
self.dim.width = self.width() - self.leftMargin - self.rightMargin
|
||||||
self.dim.height = self.height() - self.bottomMargin - self.topMargin
|
self.dim.height = self.height() - self.bottomMargin - self.topMargin
|
||||||
|
|
||||||
def contextMenuEvent(self, event):
|
def contextMenuEvent(self, event):
|
||||||
self.action_set_fixed_start.setText(
|
self.action_set_fixed_start.setText(f"Start ({self.minDisplayLength})")
|
||||||
f"Start ({self.minDisplayLength})")
|
self.action_set_fixed_stop.setText(f"Stop ({self.maxDisplayLength})")
|
||||||
self.action_set_fixed_stop.setText(
|
|
||||||
f"Stop ({self.maxDisplayLength})")
|
|
||||||
self.y_action_set_fixed_minimum.setText(
|
self.y_action_set_fixed_minimum.setText(
|
||||||
f"Minimum ({self.minImpedance})")
|
f"Minimum ({self.minImpedance})"
|
||||||
|
)
|
||||||
self.y_action_set_fixed_maximum.setText(
|
self.y_action_set_fixed_maximum.setText(
|
||||||
f"Maximum ({self.maxImpedance})")
|
f"Maximum ({self.maxImpedance})"
|
||||||
self.menu.exec_(event.globalPos())
|
)
|
||||||
|
self.menu.exec(event.globalPos())
|
||||||
|
|
||||||
def isPlotable(self, x, y):
|
def isPlotable(self, x, y):
|
||||||
return self.leftMargin <= x <= self.width() - self.rightMargin and \
|
return (
|
||||||
self.topMargin <= y <= self.height() - self.bottomMargin
|
self.leftMargin <= x <= self.width() - self.rightMargin
|
||||||
|
and self.topMargin <= y <= self.height() - self.bottomMargin
|
||||||
|
)
|
||||||
|
|
||||||
def resetDisplayLimits(self):
|
def resetDisplayLimits(self):
|
||||||
self.fixedSpan = False
|
self.fixedSpan = False
|
||||||
|
@ -161,10 +185,14 @@ class TDRChart(Chart):
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def setMinimumLength(self):
|
def setMinimumLength(self):
|
||||||
min_val, selected = QtWidgets.QInputDialog.getDouble(
|
min_val, selected = QInputDialog.getDouble(
|
||||||
self, "Start length (m)",
|
self,
|
||||||
"Set start length (m)", value=self.minDisplayLength,
|
"Start length (m)",
|
||||||
min=0, decimals=1)
|
"Set start length (m)",
|
||||||
|
value=self.minDisplayLength,
|
||||||
|
min=0,
|
||||||
|
decimals=1,
|
||||||
|
)
|
||||||
if not selected:
|
if not selected:
|
||||||
return
|
return
|
||||||
if not (self.fixedSpan and min_val >= self.maxDisplayLength):
|
if not (self.fixedSpan and min_val >= self.maxDisplayLength):
|
||||||
|
@ -173,10 +201,14 @@ class TDRChart(Chart):
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def setMaximumLength(self):
|
def setMaximumLength(self):
|
||||||
max_val, selected = QtWidgets.QInputDialog.getDouble(
|
max_val, selected = QInputDialog.getDouble(
|
||||||
self, "Stop length (m)",
|
self,
|
||||||
"Set stop length (m)", value=self.minDisplayLength,
|
"Stop length (m)",
|
||||||
min=0.1, decimals=1)
|
"Set stop length (m)",
|
||||||
|
value=self.minDisplayLength,
|
||||||
|
min=0.1,
|
||||||
|
decimals=1,
|
||||||
|
)
|
||||||
if not selected:
|
if not selected:
|
||||||
return
|
return
|
||||||
if not (self.fixedSpan and max_val <= self.minDisplayLength):
|
if not (self.fixedSpan and max_val <= self.minDisplayLength):
|
||||||
|
@ -189,11 +221,14 @@ class TDRChart(Chart):
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def setMinimumImpedance(self):
|
def setMinimumImpedance(self):
|
||||||
min_val, selected = QtWidgets.QInputDialog.getDouble(
|
min_val, selected = QInputDialog.getDouble(
|
||||||
self, "Minimum impedance (\N{OHM SIGN})",
|
self,
|
||||||
|
"Minimum impedance (\N{OHM SIGN})",
|
||||||
"Set minimum impedance (\N{OHM SIGN})",
|
"Set minimum impedance (\N{OHM SIGN})",
|
||||||
value=self.minDisplayLength,
|
value=self.minDisplayLength,
|
||||||
min=0, decimals=1)
|
min=0,
|
||||||
|
decimals=1,
|
||||||
|
)
|
||||||
if not selected:
|
if not selected:
|
||||||
return
|
return
|
||||||
if not (self.fixedValues and min_val >= self.maxImpedance):
|
if not (self.fixedValues and min_val >= self.maxImpedance):
|
||||||
|
@ -202,11 +237,14 @@ class TDRChart(Chart):
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def setMaximumImpedance(self):
|
def setMaximumImpedance(self):
|
||||||
max_val, selected = QtWidgets.QInputDialog.getDouble(
|
max_val, selected = QInputDialog.getDouble(
|
||||||
self, "Maximum impedance (\N{OHM SIGN})",
|
self,
|
||||||
|
"Maximum impedance (\N{OHM SIGN})",
|
||||||
"Set maximum impedance (\N{OHM SIGN})",
|
"Set maximum impedance (\N{OHM SIGN})",
|
||||||
value=self.minDisplayLength,
|
value=self.minDisplayLength,
|
||||||
min=0.1, decimals=1)
|
min=0.1,
|
||||||
|
decimals=1,
|
||||||
|
)
|
||||||
if not selected:
|
if not selected:
|
||||||
return
|
return
|
||||||
if not (self.fixedValues and max_val <= self.minImpedance):
|
if not (self.fixedValues and max_val <= self.minImpedance):
|
||||||
|
@ -226,32 +264,35 @@ class TDRChart(Chart):
|
||||||
self.tdrWindow.updated.connect(new_chart.update)
|
self.tdrWindow.updated.connect(new_chart.update)
|
||||||
return new_chart
|
return new_chart
|
||||||
|
|
||||||
def mouseMoveEvent(self, a0: QtGui.QMouseEvent) -> None:
|
def mouseMoveEvent(self, a0: QMouseEvent) -> None:
|
||||||
if a0.buttons() == QtCore.Qt.RightButton:
|
if a0.buttons() == Qt.MouseButton.RightButton:
|
||||||
a0.ignore()
|
a0.ignore()
|
||||||
return
|
return
|
||||||
if a0.buttons() == QtCore.Qt.MiddleButton:
|
if a0.buttons() == Qt.MouseButton.MiddleButton:
|
||||||
# Drag the display
|
# Drag the display
|
||||||
a0.accept()
|
a0.accept()
|
||||||
if self.dragbox.move_x != -1 and self.dragbox.move_y != -1:
|
if self.dragbox.move_x != -1 and self.dragbox.move_y != -1:
|
||||||
dx = self.dragbox.move_x - a0.x()
|
dx = self.dragbox.move_x - a0.position().x()
|
||||||
dy = self.dragbox.move_y - a0.y()
|
dy = self.dragbox.move_y - a0.position().y()
|
||||||
self.zoomTo(self.leftMargin + dx, self.topMargin + dy,
|
self.zoomTo(
|
||||||
self.leftMargin + self.dim.width + dx,
|
self.leftMargin + dx,
|
||||||
self.topMargin + self.dim.height + dy)
|
self.topMargin + dy,
|
||||||
self.dragbox.move_x = a0.x()
|
self.leftMargin + self.dim.width + dx,
|
||||||
self.dragbox.move_y = a0.y()
|
self.topMargin + self.dim.height + dy,
|
||||||
|
)
|
||||||
|
self.dragbox.move_x = a0.position().x()
|
||||||
|
self.dragbox.move_y = a0.position().y()
|
||||||
return
|
return
|
||||||
if a0.modifiers() == QtCore.Qt.ControlModifier:
|
if a0.modifiers() == Qt.KeyboardModifier.ControlModifier:
|
||||||
# Dragging a box
|
# Dragging a box
|
||||||
if not self.dragbox.state:
|
if not self.dragbox.state:
|
||||||
self.dragbox.pos_start = (a0.x(), a0.y())
|
self.dragbox.pos_start = (a0.position().x(), a0.position().y())
|
||||||
self.dragbox.pos = (a0.x(), a0.y())
|
self.dragbox.pos = (a0.position().x(), a0.position().y())
|
||||||
self.update()
|
self.update()
|
||||||
a0.accept()
|
a0.accept()
|
||||||
return
|
return
|
||||||
|
|
||||||
x = a0.x()
|
x = a0.position().x()
|
||||||
absx = x - self.leftMargin
|
absx = x - self.leftMargin
|
||||||
if absx < 0 or absx > self.width() - self.rightMargin:
|
if absx < 0 or absx > self.width() - self.rightMargin:
|
||||||
a0.ignore()
|
a0.ignore()
|
||||||
|
@ -261,13 +302,14 @@ class TDRChart(Chart):
|
||||||
if self.tdrWindow.td:
|
if self.tdrWindow.td:
|
||||||
if self.fixedSpan:
|
if self.fixedSpan:
|
||||||
max_index = np.searchsorted(
|
max_index = np.searchsorted(
|
||||||
self.tdrWindow.distance_axis, self.maxDisplayLength * 2)
|
self.tdrWindow.distance_axis, self.maxDisplayLength * 2
|
||||||
|
)
|
||||||
min_index = np.searchsorted(
|
min_index = np.searchsorted(
|
||||||
self.tdrWindow.distance_axis, self.minDisplayLength * 2)
|
self.tdrWindow.distance_axis, self.minDisplayLength * 2
|
||||||
|
)
|
||||||
x_step = (max_index - min_index) / width
|
x_step = (max_index - min_index) / width
|
||||||
else:
|
else:
|
||||||
max_index = math.ceil(
|
max_index = math.ceil(len(self.tdrWindow.distance_axis) / 2)
|
||||||
len(self.tdrWindow.distance_axis) / 2)
|
|
||||||
x_step = max_index / width
|
x_step = max_index / width
|
||||||
|
|
||||||
self.markerLocation = int(round(absx * x_step))
|
self.markerLocation = int(round(absx * x_step))
|
||||||
|
@ -276,26 +318,30 @@ class TDRChart(Chart):
|
||||||
|
|
||||||
def _draw_ticks(self, height, width, x_step, min_index):
|
def _draw_ticks(self, height, width, x_step, min_index):
|
||||||
ticks = (self.width() - self.leftMargin) // 100
|
ticks = (self.width() - self.leftMargin) // 100
|
||||||
qp = QtGui.QPainter(self)
|
qp = QPainter(self)
|
||||||
for i in range(ticks):
|
for i in range(ticks):
|
||||||
x = self.leftMargin + round((i + 1) * width / ticks)
|
x = self.leftMargin + round((i + 1) * width / ticks)
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QPen(Chart.color.foreground))
|
||||||
qp.drawLine(x, self.topMargin, x, self.topMargin + height)
|
qp.drawLine(x, self.topMargin, x, self.topMargin + height)
|
||||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
qp.setPen(QPen(Chart.color.text))
|
||||||
distance = self.tdrWindow.distance_axis[
|
distance = (
|
||||||
min_index +
|
self.tdrWindow.distance_axis[
|
||||||
int((x - self.leftMargin) * x_step) - 1] / 2
|
min_index + int((x - self.leftMargin) * x_step) - 1
|
||||||
qp.drawText(x - 15, self.topMargin + height + 15,
|
]
|
||||||
f"{round(distance, 1)}m")
|
/ 2
|
||||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
)
|
||||||
|
qp.drawText(
|
||||||
|
x - 15, self.topMargin + height + 15, f"{round(distance, 1)}m"
|
||||||
|
)
|
||||||
|
qp.setPen(QPen(Chart.color.text))
|
||||||
qp.drawText(
|
qp.drawText(
|
||||||
self.leftMargin - 10,
|
self.leftMargin - 10,
|
||||||
self.topMargin + height + 15,
|
self.topMargin + height + 15,
|
||||||
str(round(self.tdrWindow.distance_axis[min_index] / 2,
|
f"{str(round(self.tdrWindow.distance_axis[min_index] / 2, 1))}m",
|
||||||
1)) + "m")
|
)
|
||||||
|
|
||||||
def _draw_y_ticks(self, height, width, min_impedance, max_impedance):
|
def _draw_y_ticks(self, height, width, min_impedance, max_impedance):
|
||||||
qp = QtGui.QPainter(self)
|
qp = QPainter(self)
|
||||||
y_step = (max_impedance - min_impedance) / height
|
y_step = (max_impedance - min_impedance) / height
|
||||||
y_ticks = math.floor(height / 60)
|
y_ticks = math.floor(height / 60)
|
||||||
y_tick_step = height / y_ticks
|
y_tick_step = height / y_ticks
|
||||||
|
@ -308,30 +354,34 @@ class TDRChart(Chart):
|
||||||
qp.drawText(3, y + 3, str(round(y_val, 1)))
|
qp.drawText(3, y + 3, str(round(y_val, 1)))
|
||||||
qp.setPen(Chart.color.text)
|
qp.setPen(Chart.color.text)
|
||||||
qp.drawText(
|
qp.drawText(
|
||||||
3, self.topMargin + height + 3, f"{round(min_impedance, 1)}")
|
3, self.topMargin + height + 3, f"{round(min_impedance, 1)}"
|
||||||
|
)
|
||||||
|
|
||||||
def _draw_max_point(self, height, x_step, y_step, min_index):
|
def _draw_max_point(self, height, x_step, y_step, min_index):
|
||||||
qp = QtGui.QPainter(self)
|
qp = QPainter(self)
|
||||||
id_max = np.argmax(self.tdrWindow.td)
|
id_max = np.argmax(self.tdrWindow.td)
|
||||||
|
|
||||||
max_point = QtCore.QPoint(
|
max_point = QPoint(
|
||||||
self.leftMargin + int((id_max - min_index) / x_step),
|
self.leftMargin + int((id_max - min_index) / x_step),
|
||||||
(self.topMargin + height) - int(
|
(self.topMargin + height) - int(self.tdrWindow.td[id_max] / y_step),
|
||||||
self.tdrWindow.td[id_max] / y_step))
|
)
|
||||||
|
|
||||||
qp.setPen(self.markers[0].color)
|
qp.setPen(self.markers[0].color)
|
||||||
qp.drawEllipse(max_point, 2, 2)
|
qp.drawEllipse(max_point, 2, 2)
|
||||||
qp.setPen(Chart.color.text)
|
qp.setPen(Chart.color.text)
|
||||||
qp.drawText(max_point.x() - 10, max_point.y() - 5,
|
qp.drawText(
|
||||||
f"{round(self.tdrWindow.distance_axis[id_max] / 2, 2)}m")
|
max_point.x() - 10,
|
||||||
|
max_point.y() - 5,
|
||||||
|
f"{round(self.tdrWindow.distance_axis[id_max] / 2, 2)}m",
|
||||||
|
)
|
||||||
|
|
||||||
def _draw_marker(self, height, x_step, y_step, min_index):
|
def _draw_marker(self, height, x_step, y_step, min_index):
|
||||||
qp = QtGui.QPainter(self)
|
qp = QPainter(self)
|
||||||
marker_point = QtCore.QPoint(
|
marker_point = QPoint(
|
||||||
self.leftMargin +
|
self.leftMargin + int((self.markerLocation - min_index) / x_step),
|
||||||
int((self.markerLocation - min_index) / x_step),
|
(self.topMargin + height)
|
||||||
(self.topMargin + height) -
|
- int(self.tdrWindow.td[self.markerLocation] / y_step),
|
||||||
int(self.tdrWindow.td[self.markerLocation] / y_step))
|
)
|
||||||
qp.setPen(Chart.color.text)
|
qp.setPen(Chart.color.text)
|
||||||
qp.drawEllipse(marker_point, 2, 2)
|
qp.drawEllipse(marker_point, 2, 2)
|
||||||
qp.drawText(
|
qp.drawText(
|
||||||
|
@ -339,19 +389,21 @@ class TDRChart(Chart):
|
||||||
marker_point.y() - 5,
|
marker_point.y() - 5,
|
||||||
f"""{round(
|
f"""{round(
|
||||||
self.tdrWindow.distance_axis[self.markerLocation] / 2,
|
self.tdrWindow.distance_axis[self.markerLocation] / 2,
|
||||||
2)}m""")
|
2)}m""",
|
||||||
|
)
|
||||||
|
|
||||||
def _draw_graph(self, height, width):
|
def _draw_graph(self, height, width):
|
||||||
min_index = 0
|
min_index = 0
|
||||||
max_index = math.ceil(
|
max_index = math.ceil(len(self.tdrWindow.distance_axis) / 2)
|
||||||
len(self.tdrWindow.distance_axis) / 2)
|
|
||||||
|
|
||||||
if self.fixedSpan:
|
if self.fixedSpan:
|
||||||
max_length = max(0.1, self.maxDisplayLength)
|
max_length = max(0.1, self.maxDisplayLength)
|
||||||
max_index = np.searchsorted(
|
max_index = np.searchsorted(
|
||||||
self.tdrWindow.distance_axis, max_length * 2)
|
self.tdrWindow.distance_axis, max_length * 2
|
||||||
|
)
|
||||||
min_index = np.searchsorted(
|
min_index = np.searchsorted(
|
||||||
self.tdrWindow.distance_axis, self.minDisplayLength * 2)
|
self.tdrWindow.distance_axis, self.minDisplayLength * 2
|
||||||
|
)
|
||||||
if max_index == min_index:
|
if max_index == min_index:
|
||||||
if max_index < len(self.tdrWindow.distance_axis) - 1:
|
if max_index < len(self.tdrWindow.distance_axis) - 1:
|
||||||
max_index += 1
|
max_index += 1
|
||||||
|
@ -361,8 +413,7 @@ class TDRChart(Chart):
|
||||||
|
|
||||||
# TODO: Limit the search to the selected span?
|
# TODO: Limit the search to the selected span?
|
||||||
min_impedance = max(0, np.min(self.tdrWindow.step_response_Z) / 1.05)
|
min_impedance = max(0, np.min(self.tdrWindow.step_response_Z) / 1.05)
|
||||||
max_impedance = min(1000, np.max(
|
max_impedance = min(1000, np.max(self.tdrWindow.step_response_Z) * 1.05)
|
||||||
self.tdrWindow.step_response_Z) * 1.05)
|
|
||||||
if self.fixedValues:
|
if self.fixedValues:
|
||||||
min_impedance = max(0, self.minImpedance)
|
min_impedance = max(0, self.minImpedance)
|
||||||
max_impedance = max(0.1, self.maxImpedance)
|
max_impedance = max(0.1, self.maxImpedance)
|
||||||
|
@ -370,10 +421,10 @@ class TDRChart(Chart):
|
||||||
y_step = max(self.tdrWindow.td) * 1.1 / height or 1.0e-30
|
y_step = max(self.tdrWindow.td) * 1.1 / height or 1.0e-30
|
||||||
|
|
||||||
self._draw_ticks(height, width, x_step, min_index)
|
self._draw_ticks(height, width, x_step, min_index)
|
||||||
self._draw_y_ticks(height, width, min_impedance, max_impedance)
|
self._draw_y_ticks(height, width, min_impedance, max_impedance)
|
||||||
|
|
||||||
qp = QtGui.QPainter(self)
|
qp = QPainter(self)
|
||||||
pen = QtGui.QPen(Chart.color.sweep)
|
pen = QPen(Chart.color.sweep)
|
||||||
pen.setWidth(self.dim.point)
|
pen.setWidth(self.dim.point)
|
||||||
qp.setPen(pen)
|
qp.setPen(pen)
|
||||||
|
|
||||||
|
@ -388,7 +439,8 @@ class TDRChart(Chart):
|
||||||
|
|
||||||
x = self.leftMargin + int((i - min_index) / x_step)
|
x = self.leftMargin + int((i - min_index) / x_step)
|
||||||
y = (self.topMargin + height) - int(
|
y = (self.topMargin + height) - int(
|
||||||
(self.tdrWindow.step_response_Z[i] - min_impedance) / y_step)
|
(self.tdrWindow.step_response_Z[i] - min_impedance) / y_step
|
||||||
|
)
|
||||||
if self.isPlotable(x, y):
|
if self.isPlotable(x, y):
|
||||||
pen.setColor(Chart.color.sweep_secondary)
|
pen.setColor(Chart.color.sweep_secondary)
|
||||||
qp.setPen(pen)
|
qp.setPen(pen)
|
||||||
|
@ -399,23 +451,27 @@ class TDRChart(Chart):
|
||||||
if self.markerLocation != -1:
|
if self.markerLocation != -1:
|
||||||
self._draw_marker(height, x_step, y_step, min_index)
|
self._draw_marker(height, x_step, y_step, min_index)
|
||||||
|
|
||||||
def paintEvent(self, _: QtGui.QPaintEvent) -> None:
|
def paintEvent(self, _: QPaintEvent) -> None:
|
||||||
qp = QtGui.QPainter(self)
|
qp = QPainter(self)
|
||||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
qp.setPen(QPen(Chart.color.text))
|
||||||
qp.drawText(3, 15, self.name)
|
qp.drawText(3, 15, self.name)
|
||||||
|
|
||||||
width = self.width() - self.leftMargin - self.rightMargin
|
width = self.width() - self.leftMargin - self.rightMargin
|
||||||
height = self.height() - self.bottomMargin - self.topMargin
|
height = self.height() - self.bottomMargin - self.topMargin
|
||||||
|
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin - 5,
|
qp.drawLine(
|
||||||
self.height() - self.bottomMargin,
|
self.leftMargin - 5,
|
||||||
self.width() - self.rightMargin,
|
self.height() - self.bottomMargin,
|
||||||
self.height() - self.bottomMargin)
|
self.width() - self.rightMargin,
|
||||||
qp.drawLine(self.leftMargin,
|
self.height() - self.bottomMargin,
|
||||||
self.topMargin - 5,
|
)
|
||||||
self.leftMargin,
|
qp.drawLine(
|
||||||
self.height() - self.bottomMargin + 5)
|
self.leftMargin,
|
||||||
|
self.topMargin - 5,
|
||||||
|
self.leftMargin,
|
||||||
|
self.height() - self.bottomMargin + 5,
|
||||||
|
)
|
||||||
# Number of ticks does not include the origin
|
# Number of ticks does not include the origin
|
||||||
self.drawTitle(qp)
|
self.drawTitle(qp)
|
||||||
|
|
||||||
|
@ -423,13 +479,12 @@ class TDRChart(Chart):
|
||||||
self._draw_graph(height, width)
|
self._draw_graph(height, width)
|
||||||
|
|
||||||
if self.dragbox.state and self.dragbox.pos[0] != -1:
|
if self.dragbox.state and self.dragbox.pos[0] != -1:
|
||||||
dashed_pen = QtGui.QPen(
|
dashed_pen = QPen(Chart.color.foreground, 1, Qt.PenStyle.DashLine)
|
||||||
Chart.color.foreground, 1, QtCore.Qt.DashLine)
|
|
||||||
qp.setPen(dashed_pen)
|
qp.setPen(dashed_pen)
|
||||||
qp.drawRect(
|
qp.drawRect(
|
||||||
QtCore.QRect(
|
QRect(
|
||||||
QtCore.QPoint(*self.dragbox.pos_start),
|
QPoint(*self.dragbox.pos_start),
|
||||||
QtCore.QPoint(*self.dragbox.pos)
|
QPoint(*self.dragbox.pos),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -444,11 +499,11 @@ class TDRChart(Chart):
|
||||||
max_impedance = self.maxImpedance
|
max_impedance = self.maxImpedance
|
||||||
else:
|
else:
|
||||||
min_impedance = max(
|
min_impedance = max(
|
||||||
0,
|
0, np.min(self.tdrWindow.step_response_Z) / 1.05
|
||||||
np.min(self.tdrWindow.step_response_Z) / 1.05)
|
)
|
||||||
max_impedance = min(
|
max_impedance = min(
|
||||||
1000,
|
1000, np.max(self.tdrWindow.step_response_Z) * 1.05
|
||||||
np.max(self.tdrWindow.step_response_Z) * 1.05)
|
)
|
||||||
y_step = (max_impedance - min_impedance) / height
|
y_step = (max_impedance - min_impedance) / height
|
||||||
return y_step * absy + min_impedance
|
return y_step * absy + min_impedance
|
||||||
return 0
|
return 0
|
||||||
|
@ -459,20 +514,28 @@ class TDRChart(Chart):
|
||||||
width = self.width() - self.leftMargin - self.rightMargin
|
width = self.width() - self.leftMargin - self.rightMargin
|
||||||
absx = x - self.leftMargin
|
absx = x - self.leftMargin
|
||||||
min_length = self.minDisplayLength if self.fixedSpan else 0
|
min_length = self.minDisplayLength if self.fixedSpan else 0
|
||||||
max_length = self.maxDisplayLength if self.fixedSpan else (
|
max_length = (
|
||||||
self.tdrWindow.distance_axis[
|
self.maxDisplayLength
|
||||||
math.ceil(len(self.tdrWindow.distance_axis) / 2)
|
if self.fixedSpan
|
||||||
] / 2)
|
else (
|
||||||
|
self.tdrWindow.distance_axis[
|
||||||
|
math.ceil(len(self.tdrWindow.distance_axis) / 2)
|
||||||
|
]
|
||||||
|
/ 2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
x_step = (max_length - min_length) / width
|
x_step = (max_length - min_length) / width
|
||||||
if limit and absx < 0:
|
if limit and absx < 0:
|
||||||
return min_length
|
return min_length
|
||||||
return (max_length if limit and absx > width else
|
return (
|
||||||
absx * x_step + min_length)
|
max_length if limit and absx > width else absx * x_step + min_length
|
||||||
|
)
|
||||||
|
|
||||||
def zoomTo(self, x1, y1, x2, y2):
|
def zoomTo(self, x1, y1, x2, y2):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Zoom to (x,y) by (x,y): (%d, %d) by (%d, %d)", x1, y1, x2, y2)
|
"Zoom to (x,y) by (x,y): (%d, %d) by (%d, %d)", x1, y1, x2, y2
|
||||||
|
)
|
||||||
val1 = self.valueAtPosition(y1)
|
val1 = self.valueAtPosition(y1)
|
||||||
val2 = self.valueAtPosition(y2)
|
val2 = self.valueAtPosition(y2)
|
||||||
|
|
||||||
|
@ -491,7 +554,7 @@ class TDRChart(Chart):
|
||||||
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
|
def resizeEvent(self, a0: QResizeEvent) -> None:
|
||||||
super().resizeEvent(a0)
|
super().resizeEvent(a0)
|
||||||
self.dim.width = self.width() - self.leftMargin - self.rightMargin
|
self.dim.width = self.width() - self.leftMargin - self.rightMargin
|
||||||
self.dim.height = self.height() - self.bottomMargin - self.topMargin
|
self.dim.height = self.height() - self.bottomMargin - self.topMargin
|
|
@ -18,9 +18,8 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import math
|
import math
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from PyQt5 import QtGui
|
from PyQt6 import QtGui
|
||||||
|
|
||||||
from NanoVNASaver.RFTools import Datapoint
|
from NanoVNASaver.RFTools import Datapoint
|
||||||
from NanoVNASaver.Charts.Chart import Chart
|
from NanoVNASaver.Charts.Chart import Chart
|
||||||
|
@ -30,7 +29,6 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class VSWRChart(FrequencyChart):
|
class VSWRChart(FrequencyChart):
|
||||||
|
|
||||||
def __init__(self, name=""):
|
def __init__(self, name=""):
|
||||||
super().__init__(name)
|
super().__init__(name)
|
||||||
|
|
||||||
|
@ -90,19 +88,22 @@ class VSWRChart(FrequencyChart):
|
||||||
qp.setPen(Chart.color.text)
|
qp.setPen(Chart.color.text)
|
||||||
if vswr != 0:
|
if vswr != 0:
|
||||||
digits = max(
|
digits = max(
|
||||||
0, min(2, math.floor(3 - math.log10(abs(vswr)))))
|
0, min(2, math.floor(3 - math.log10(abs(vswr))))
|
||||||
|
)
|
||||||
v_text = f"{round(vswr, digits)}" if digits else "0"
|
v_text = f"{round(vswr, digits)}" if digits else "0"
|
||||||
qp.drawText(3, y + 3, v_text)
|
qp.drawText(3, y + 3, v_text)
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin - 5, y,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width, y)
|
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
|
||||||
qp.drawLine(self.leftMargin - 5,
|
)
|
||||||
self.topMargin + self.dim.height,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width,
|
self.leftMargin - 5,
|
||||||
self.topMargin + self.dim.height)
|
self.topMargin + self.dim.height,
|
||||||
|
self.leftMargin + self.dim.width,
|
||||||
|
self.topMargin + self.dim.height,
|
||||||
|
)
|
||||||
qp.setPen(Chart.color.text)
|
qp.setPen(Chart.color.text)
|
||||||
digits = max(
|
digits = max(0, min(2, math.floor(3 - math.log10(abs(minVSWR)))))
|
||||||
0, min(2, math.floor(3 - math.log10(abs(minVSWR)))))
|
|
||||||
v_text = f"{round(minVSWR, digits)}" if digits else "0"
|
v_text = f"{round(minVSWR, digits)}" if digits else "0"
|
||||||
qp.drawText(3, self.topMargin + self.dim.height, v_text)
|
qp.drawText(3, self.topMargin + self.dim.height, v_text)
|
||||||
else:
|
else:
|
||||||
|
@ -112,16 +113,20 @@ class VSWRChart(FrequencyChart):
|
||||||
qp.setPen(Chart.color.text)
|
qp.setPen(Chart.color.text)
|
||||||
if vswr != 0:
|
if vswr != 0:
|
||||||
digits = max(
|
digits = max(
|
||||||
0, min(2, math.floor(3 - math.log10(abs(vswr)))))
|
0, min(2, math.floor(3 - math.log10(abs(vswr))))
|
||||||
|
)
|
||||||
vswrstr = f"{round(vswr, digits)}" if digits else "0"
|
vswrstr = f"{round(vswr, digits)}" if digits else "0"
|
||||||
qp.drawText(3, y + 3, vswrstr)
|
qp.drawText(3, y + 3, vswrstr)
|
||||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||||
qp.drawLine(self.leftMargin - 5, y,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width, y)
|
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
|
||||||
qp.drawLine(self.leftMargin - 5,
|
)
|
||||||
self.topMargin,
|
qp.drawLine(
|
||||||
self.leftMargin + self.dim.width,
|
self.leftMargin - 5,
|
||||||
self.topMargin)
|
self.topMargin,
|
||||||
|
self.leftMargin + self.dim.width,
|
||||||
|
self.topMargin,
|
||||||
|
)
|
||||||
qp.setPen(Chart.color.text)
|
qp.setPen(Chart.color.text)
|
||||||
digits = max(0, min(2, math.floor(3 - math.log10(abs(maxVSWR)))))
|
digits = max(0, min(2, math.floor(3 - math.log10(abs(maxVSWR)))))
|
||||||
v_text = f"{round(maxVSWR, digits)}" if digits else "0"
|
v_text = f"{round(maxVSWR, digits)}" if digits else "0"
|
||||||
|
@ -130,8 +135,7 @@ class VSWRChart(FrequencyChart):
|
||||||
qp.setPen(Chart.color.swr)
|
qp.setPen(Chart.color.swr)
|
||||||
for vswr in self.swrMarkers:
|
for vswr in self.swrMarkers:
|
||||||
y = self.getYPositionFromValue(vswr)
|
y = self.getYPositionFromValue(vswr)
|
||||||
qp.drawLine(self.leftMargin, y,
|
qp.drawLine(self.leftMargin, y, self.leftMargin + self.dim.width, y)
|
||||||
self.leftMargin + self.dim.width, y)
|
|
||||||
qp.drawText(self.leftMargin + 3, y - 1, str(vswr))
|
qp.drawText(self.leftMargin + 3, y - 1, str(vswr))
|
||||||
|
|
||||||
self.drawFrequencyTicks(qp)
|
self.drawFrequencyTicks(qp)
|
||||||
|
@ -146,20 +150,22 @@ class VSWRChart(FrequencyChart):
|
||||||
span = math.log(self.maxVSWR) - math.log(min_val)
|
span = math.log(self.maxVSWR) - math.log(min_val)
|
||||||
else:
|
else:
|
||||||
return -1
|
return -1
|
||||||
return (
|
return self.topMargin + int(
|
||||||
self.topMargin + int(
|
(math.log(self.maxVSWR) - math.log(vswr))
|
||||||
(math.log(self.maxVSWR) - math.log(vswr)) /
|
/ span
|
||||||
span * self.dim.height))
|
* self.dim.height
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
return self.topMargin + int(
|
return self.topMargin + int(
|
||||||
(self.maxVSWR - vswr) / self.span * self.dim.height)
|
(self.maxVSWR - vswr) / self.span * self.dim.height
|
||||||
|
)
|
||||||
except OverflowError:
|
except OverflowError:
|
||||||
return self.topMargin
|
return self.topMargin
|
||||||
|
|
||||||
def getYPosition(self, d: Datapoint) -> int:
|
def getYPosition(self, d: Datapoint) -> int:
|
||||||
return self.getYPositionFromValue(d.vswr)
|
return self.getYPositionFromValue(d.vswr)
|
||||||
|
|
||||||
def valueAtPosition(self, y) -> List[float]:
|
def valueAtPosition(self, y) -> list[float]:
|
||||||
absy = y - self.topMargin
|
absy = y - self.topMargin
|
||||||
if self.logarithmicY:
|
if self.logarithmicY:
|
||||||
min_val = self.maxVSWR - self.span
|
min_val = self.maxVSWR - self.span
|
|
@ -23,3 +23,31 @@ from .Smith import SmithChart
|
||||||
from .SParam import SParameterChart
|
from .SParam import SParameterChart
|
||||||
from .TDR import TDRChart
|
from .TDR import TDRChart
|
||||||
from .VSWR import VSWRChart
|
from .VSWR import VSWRChart
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Chart",
|
||||||
|
"FrequencyChart",
|
||||||
|
"PolarChart",
|
||||||
|
"SquareChart",
|
||||||
|
"CapacitanceChart",
|
||||||
|
"InductanceChart",
|
||||||
|
"GroupDelayChart",
|
||||||
|
"LogMagChart",
|
||||||
|
"CombinedLogMagChart",
|
||||||
|
"MagnitudeChart",
|
||||||
|
"MagnitudeZChart",
|
||||||
|
"MagnitudeZShuntChart",
|
||||||
|
"MagnitudeZSeriesChart",
|
||||||
|
"PermeabilityChart",
|
||||||
|
"PhaseChart",
|
||||||
|
"QualityFactorChart",
|
||||||
|
"RealImaginaryChart",
|
||||||
|
"RealImaginaryMuChart",
|
||||||
|
"RealImaginaryZChart",
|
||||||
|
"RealImaginaryZShuntChart",
|
||||||
|
"RealImaginaryZSeriesChart",
|
||||||
|
"SmithChart",
|
||||||
|
"SParameterChart",
|
||||||
|
"TDRChart",
|
||||||
|
"VSWRChart",
|
||||||
|
]
|
|
@ -18,7 +18,7 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PyQt5 import QtWidgets, QtCore
|
from PyQt6 import QtWidgets, QtCore
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -29,6 +29,6 @@ class Control(QtWidgets.QGroupBox):
|
||||||
def __init__(self, app: QtWidgets.QWidget, title: str = ""):
|
def __init__(self, app: QtWidgets.QWidget, title: str = ""):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.app = app
|
self.app = app
|
||||||
self.setMaximumWidth(240)
|
self.setMaximumWidth(250)
|
||||||
self.setTitle(title)
|
self.setTitle(title)
|
||||||
self.layout = QtWidgets.QFormLayout(self)
|
self.layout = QtWidgets.QFormLayout(self)
|
|
@ -18,8 +18,8 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PyQt5 import QtWidgets, QtCore
|
from PyQt6 import QtWidgets, QtCore
|
||||||
from PyQt5.QtWidgets import QCheckBox
|
from PyQt6.QtWidgets import QCheckBox, QSizePolicy
|
||||||
|
|
||||||
from NanoVNASaver import Defaults
|
from NanoVNASaver import Defaults
|
||||||
from NanoVNASaver.Marker.Widget import Marker
|
from NanoVNASaver.Marker.Widget import Marker
|
||||||
|
@ -29,16 +29,16 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ShowButton(QtWidgets.QPushButton):
|
class ShowButton(QtWidgets.QPushButton):
|
||||||
def setText(self, text: str = ''):
|
def setText(self, text: str = ""):
|
||||||
if not text:
|
if not text:
|
||||||
text = ("Show data"
|
text = (
|
||||||
if Defaults.cfg.gui.markers_hidden else "Hide data")
|
"Show data" if Defaults.cfg.gui.markers_hidden else "Hide data"
|
||||||
|
)
|
||||||
super().setText(text)
|
super().setText(text)
|
||||||
self.setToolTip("Toggle visibility of marker readings area")
|
self.setToolTip("Toggle visibility of marker readings area")
|
||||||
|
|
||||||
|
|
||||||
class MarkerControl(Control):
|
class MarkerControl(Control):
|
||||||
|
|
||||||
def __init__(self, app: QtWidgets.QWidget):
|
def __init__(self, app: QtWidgets.QWidget):
|
||||||
super().__init__(app, "Markers")
|
super().__init__(app, "Markers")
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ class MarkerControl(Control):
|
||||||
self.check_delta = QCheckBox("Enable Delta Marker")
|
self.check_delta = QCheckBox("Enable Delta Marker")
|
||||||
self.check_delta.toggled.connect(self.toggle_delta)
|
self.check_delta.toggled.connect(self.toggle_delta)
|
||||||
|
|
||||||
self.check_delta_reference = QCheckBox("reference")
|
self.check_delta_reference = QCheckBox("Reference")
|
||||||
self.check_delta_reference.toggled.connect(self.toggle_delta_reference)
|
self.check_delta_reference.toggled.connect(self.toggle_delta_reference)
|
||||||
|
|
||||||
layout2 = QtWidgets.QHBoxLayout()
|
layout2 = QtWidgets.QHBoxLayout()
|
||||||
|
@ -70,9 +70,12 @@ class MarkerControl(Control):
|
||||||
self.showMarkerButton.clicked.connect(self.toggle_frame)
|
self.showMarkerButton.clicked.connect(self.toggle_frame)
|
||||||
|
|
||||||
lock_radiobutton = QtWidgets.QRadioButton("Locked")
|
lock_radiobutton = QtWidgets.QRadioButton("Locked")
|
||||||
lock_radiobutton.setLayoutDirection(QtCore.Qt.RightToLeft)
|
lock_radiobutton.setLayoutDirection(
|
||||||
|
QtCore.Qt.LayoutDirection.RightToLeft
|
||||||
|
)
|
||||||
lock_radiobutton.setSizePolicy(
|
lock_radiobutton.setSizePolicy(
|
||||||
QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred)
|
QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred
|
||||||
|
)
|
||||||
|
|
||||||
hbox = QtWidgets.QHBoxLayout()
|
hbox = QtWidgets.QHBoxLayout()
|
||||||
hbox.addWidget(self.showMarkerButton)
|
hbox.addWidget(self.showMarkerButton)
|
||||||
|
@ -82,8 +85,7 @@ class MarkerControl(Control):
|
||||||
def toggle_frame(self):
|
def toggle_frame(self):
|
||||||
def settings(hidden: bool):
|
def settings(hidden: bool):
|
||||||
Defaults.cfg.gui.markers_hidden = not hidden
|
Defaults.cfg.gui.markers_hidden = not hidden
|
||||||
self.app.marker_frame.setHidden(
|
self.app.marker_frame.setHidden(Defaults.cfg.gui.markers_hidden)
|
||||||
Defaults.cfg.gui.markers_hidden)
|
|
||||||
self.showMarkerButton.setText()
|
self.showMarkerButton.setText()
|
||||||
self.showMarkerButton.repaint()
|
self.showMarkerButton.repaint()
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
import logging
|
import logging
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from PyQt5 import QtWidgets
|
from PyQt6 import QtWidgets
|
||||||
|
|
||||||
from NanoVNASaver.Hardware.Hardware import Interface, get_interfaces, get_VNA
|
from NanoVNASaver.Hardware.Hardware import Interface, get_interfaces, get_VNA
|
||||||
from NanoVNASaver.Controls.Control import Control
|
from NanoVNASaver.Controls.Control import Control
|
||||||
|
@ -28,7 +28,6 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SerialControl(Control):
|
class SerialControl(Control):
|
||||||
|
|
||||||
def __init__(self, app: QtWidgets.QWidget):
|
def __init__(self, app: QtWidgets.QWidget):
|
||||||
super().__init__(app, "Serial port control")
|
super().__init__(app, "Serial port control")
|
||||||
|
|
||||||
|
@ -58,7 +57,8 @@ class SerialControl(Control):
|
||||||
self.btn_settings.setMinimumHeight(20)
|
self.btn_settings.setMinimumHeight(20)
|
||||||
self.btn_settings.setFixedWidth(60)
|
self.btn_settings.setFixedWidth(60)
|
||||||
self.btn_settings.clicked.connect(
|
self.btn_settings.clicked.connect(
|
||||||
lambda: self.app.display_window("device_settings"))
|
lambda: self.app.display_window("device_settings")
|
||||||
|
)
|
||||||
|
|
||||||
button_layout.addWidget(self.btn_settings, stretch=0)
|
button_layout.addWidget(self.btn_settings, stretch=0)
|
||||||
self.layout.addRow(button_layout)
|
self.layout.addRow(button_layout)
|
||||||
|
@ -82,8 +82,9 @@ class SerialControl(Control):
|
||||||
try:
|
try:
|
||||||
self.interface.open()
|
self.interface.open()
|
||||||
except (IOError, AttributeError) as exc:
|
except (IOError, AttributeError) as exc:
|
||||||
logger.error("Tried to open %s and failed: %s",
|
logger.error(
|
||||||
self.interface, exc)
|
"Tried to open %s and failed: %s", self.interface, exc
|
||||||
|
)
|
||||||
return
|
return
|
||||||
if not self.interface.isOpen():
|
if not self.interface.isOpen():
|
||||||
logger.error("Unable to open port %s", self.interface)
|
logger.error("Unable to open port %s", self.interface)
|
||||||
|
@ -96,7 +97,8 @@ class SerialControl(Control):
|
||||||
logger.error("Unable to connect to VNA: %s", exc)
|
logger.error("Unable to connect to VNA: %s", exc)
|
||||||
|
|
||||||
self.app.vna.validateInput = self.app.settings.value(
|
self.app.vna.validateInput = self.app.settings.value(
|
||||||
"SerialInputValidation", True, bool)
|
"SerialInputValidation", False, bool
|
||||||
|
)
|
||||||
|
|
||||||
# connected
|
# connected
|
||||||
self.btn_toggle.setText("Disconnect")
|
self.btn_toggle.setText("Disconnect")
|
||||||
|
@ -106,16 +108,20 @@ class SerialControl(Control):
|
||||||
if not frequencies:
|
if not frequencies:
|
||||||
logger.warning("No frequencies read")
|
logger.warning("No frequencies read")
|
||||||
return
|
return
|
||||||
logger.info("Read starting frequency %s and end frequency %s",
|
logger.info(
|
||||||
frequencies[0], frequencies[-1])
|
"Read starting frequency %s and end frequency %s",
|
||||||
|
frequencies[0],
|
||||||
|
frequencies[-1],
|
||||||
|
)
|
||||||
self.app.sweep_control.set_start(frequencies[0])
|
self.app.sweep_control.set_start(frequencies[0])
|
||||||
if frequencies[0] < frequencies[-1]:
|
if frequencies[0] < frequencies[-1]:
|
||||||
self.app.sweep_control.set_end(frequencies[-1])
|
self.app.sweep_control.set_end(frequencies[-1])
|
||||||
else:
|
else:
|
||||||
self.app.sweep_control.set_end(
|
self.app.sweep_control.set_end(
|
||||||
frequencies[0] +
|
frequencies[0]
|
||||||
self.app.vna.datapoints *
|
+ self.app.vna.datapoints
|
||||||
self.app.sweep_control.get_segments())
|
* self.app.sweep_control.get_segments()
|
||||||
|
)
|
||||||
|
|
||||||
self.app.sweep_control.set_segments(1) # speed up things
|
self.app.sweep_control.set_segments(1) # speed up things
|
||||||
self.app.sweep_control.update_center_span()
|
self.app.sweep_control.update_center_span()
|
|
@ -18,11 +18,13 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PyQt5 import QtWidgets, QtCore
|
from PyQt6 import QtWidgets, QtCore
|
||||||
|
|
||||||
from NanoVNASaver.Formatting import (
|
from NanoVNASaver.Formatting import (
|
||||||
format_frequency_sweep, format_frequency_short,
|
format_frequency_sweep,
|
||||||
parse_frequency)
|
format_frequency_short,
|
||||||
|
parse_frequency,
|
||||||
|
)
|
||||||
from NanoVNASaver.Inputs import FrequencyInputWidget
|
from NanoVNASaver.Inputs import FrequencyInputWidget
|
||||||
from NanoVNASaver.Controls.Control import Control
|
from NanoVNASaver.Controls.Control import Control
|
||||||
|
|
||||||
|
@ -30,12 +32,11 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SweepControl(Control):
|
class SweepControl(Control):
|
||||||
|
|
||||||
def __init__(self, app: QtWidgets.QWidget):
|
def __init__(self, app: QtWidgets.QWidget):
|
||||||
super().__init__(app, "Sweep control")
|
super().__init__(app, "Sweep control")
|
||||||
|
|
||||||
line = QtWidgets.QFrame()
|
line = QtWidgets.QFrame()
|
||||||
line.setFrameShape(QtWidgets.QFrame.VLine)
|
line.setFrameShape(QtWidgets.QFrame.Shape.VLine)
|
||||||
|
|
||||||
input_layout = QtWidgets.QHBoxLayout()
|
input_layout = QtWidgets.QHBoxLayout()
|
||||||
input_left_layout = QtWidgets.QFormLayout()
|
input_left_layout = QtWidgets.QFormLayout()
|
||||||
|
@ -48,14 +49,14 @@ class SweepControl(Control):
|
||||||
self.input_start = FrequencyInputWidget()
|
self.input_start = FrequencyInputWidget()
|
||||||
self.input_start.setFixedHeight(20)
|
self.input_start.setFixedHeight(20)
|
||||||
self.input_start.setMinimumWidth(60)
|
self.input_start.setMinimumWidth(60)
|
||||||
self.input_start.setAlignment(QtCore.Qt.AlignRight)
|
self.input_start.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
|
||||||
self.input_start.textEdited.connect(self.update_center_span)
|
self.input_start.textEdited.connect(self.update_center_span)
|
||||||
self.input_start.textChanged.connect(self.update_step_size)
|
self.input_start.textChanged.connect(self.update_step_size)
|
||||||
input_left_layout.addRow(QtWidgets.QLabel("Start"), self.input_start)
|
input_left_layout.addRow(QtWidgets.QLabel("Start"), self.input_start)
|
||||||
|
|
||||||
self.input_end = FrequencyInputWidget()
|
self.input_end = FrequencyInputWidget()
|
||||||
self.input_end.setFixedHeight(20)
|
self.input_end.setFixedHeight(20)
|
||||||
self.input_end.setAlignment(QtCore.Qt.AlignRight)
|
self.input_end.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
|
||||||
self.input_end.textEdited.connect(self.update_center_span)
|
self.input_end.textEdited.connect(self.update_center_span)
|
||||||
self.input_end.textChanged.connect(self.update_step_size)
|
self.input_end.textChanged.connect(self.update_step_size)
|
||||||
input_left_layout.addRow(QtWidgets.QLabel("Stop"), self.input_end)
|
input_left_layout.addRow(QtWidgets.QLabel("Stop"), self.input_end)
|
||||||
|
@ -63,29 +64,31 @@ class SweepControl(Control):
|
||||||
self.input_center = FrequencyInputWidget()
|
self.input_center = FrequencyInputWidget()
|
||||||
self.input_center.setFixedHeight(20)
|
self.input_center.setFixedHeight(20)
|
||||||
self.input_center.setMinimumWidth(60)
|
self.input_center.setMinimumWidth(60)
|
||||||
self.input_center.setAlignment(QtCore.Qt.AlignRight)
|
self.input_center.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
|
||||||
self.input_center.textEdited.connect(self.update_start_end)
|
self.input_center.textEdited.connect(self.update_start_end)
|
||||||
|
|
||||||
input_right_layout.addRow(QtWidgets.QLabel(
|
input_right_layout.addRow(QtWidgets.QLabel("Center"), self.input_center)
|
||||||
"Center"), self.input_center)
|
|
||||||
|
|
||||||
self.input_span = FrequencyInputWidget()
|
self.input_span = FrequencyInputWidget()
|
||||||
self.input_span.setFixedHeight(20)
|
self.input_span.setFixedHeight(20)
|
||||||
self.input_span.setAlignment(QtCore.Qt.AlignRight)
|
self.input_span.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
|
||||||
self.input_span.textEdited.connect(self.update_start_end)
|
self.input_span.textEdited.connect(self.update_start_end)
|
||||||
|
|
||||||
input_right_layout.addRow(QtWidgets.QLabel("Span"), self.input_span)
|
input_right_layout.addRow(QtWidgets.QLabel("Span"), self.input_span)
|
||||||
|
|
||||||
self.input_segments = QtWidgets.QLineEdit(
|
self.input_segments = QtWidgets.QLineEdit(
|
||||||
self.app.settings.value("Segments", "1"))
|
self.app.settings.value("Segments", "1")
|
||||||
self.input_segments.setAlignment(QtCore.Qt.AlignRight)
|
)
|
||||||
|
self.input_segments.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
|
||||||
self.input_segments.setFixedHeight(20)
|
self.input_segments.setFixedHeight(20)
|
||||||
self.input_segments.setFixedWidth(60)
|
self.input_segments.setFixedWidth(60)
|
||||||
self.input_segments.textEdited.connect(self.update_step_size)
|
self.input_segments.textEdited.connect(self.update_step_size)
|
||||||
|
|
||||||
self.label_step = QtWidgets.QLabel("Hz/step")
|
self.label_step = QtWidgets.QLabel("Hz/step")
|
||||||
self.label_step.setAlignment(
|
self.label_step.setAlignment(
|
||||||
QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
QtCore.Qt.AlignmentFlag.AlignRight
|
||||||
|
| QtCore.Qt.AlignmentFlag.AlignVCenter
|
||||||
|
)
|
||||||
|
|
||||||
segment_layout = QtWidgets.QHBoxLayout()
|
segment_layout = QtWidgets.QHBoxLayout()
|
||||||
segment_layout.addWidget(self.input_segments)
|
segment_layout.addWidget(self.input_segments)
|
||||||
|
@ -95,7 +98,8 @@ class SweepControl(Control):
|
||||||
btn_settings_window = QtWidgets.QPushButton("Sweep settings ...")
|
btn_settings_window = QtWidgets.QPushButton("Sweep settings ...")
|
||||||
btn_settings_window.setFixedHeight(20)
|
btn_settings_window.setFixedHeight(20)
|
||||||
btn_settings_window.clicked.connect(
|
btn_settings_window.clicked.connect(
|
||||||
lambda: self.app.display_window("sweep_settings"))
|
lambda: self.app.display_window("sweep_settings")
|
||||||
|
)
|
||||||
|
|
||||||
self.layout.addRow(btn_settings_window)
|
self.layout.addRow(btn_settings_window)
|
||||||
|
|
||||||
|
@ -107,11 +111,13 @@ class SweepControl(Control):
|
||||||
self.btn_start = QtWidgets.QPushButton("Sweep")
|
self.btn_start = QtWidgets.QPushButton("Sweep")
|
||||||
self.btn_start.setFixedHeight(20)
|
self.btn_start.setFixedHeight(20)
|
||||||
self.btn_start.clicked.connect(self.app.sweep_start)
|
self.btn_start.clicked.connect(self.app.sweep_start)
|
||||||
self.btn_start.setShortcut(QtCore.Qt.Key_W | QtCore.Qt.CTRL)
|
self.btn_start.setShortcut(
|
||||||
|
QtCore.Qt.Key.Key_Control + QtCore.Qt.Key.Key_W
|
||||||
|
)
|
||||||
self.btn_stop = QtWidgets.QPushButton("Stop")
|
self.btn_stop = QtWidgets.QPushButton("Stop")
|
||||||
self.btn_stop.setFixedHeight(20)
|
self.btn_stop.setFixedHeight(20)
|
||||||
self.btn_stop.clicked.connect(self.app.sweep_stop)
|
self.btn_stop.clicked.connect(self.app.sweep_stop)
|
||||||
self.btn_stop.setShortcut(QtCore.Qt.Key_Escape)
|
self.btn_stop.setShortcut(QtCore.Qt.Key.Key_Escape)
|
||||||
self.btn_stop.setDisabled(True)
|
self.btn_stop.setDisabled(True)
|
||||||
btn_layout = QtWidgets.QHBoxLayout()
|
btn_layout = QtWidgets.QHBoxLayout()
|
||||||
btn_layout.addWidget(self.btn_start)
|
btn_layout.addWidget(self.btn_start)
|
||||||
|
@ -206,14 +212,13 @@ class SweepControl(Control):
|
||||||
segments = self.get_segments()
|
segments = self.get_segments()
|
||||||
if segments > 0:
|
if segments > 0:
|
||||||
fstep = fspan / (segments * self.app.vna.datapoints - 1)
|
fstep = fspan / (segments * self.app.vna.datapoints - 1)
|
||||||
self.label_step.setText(
|
self.label_step.setText(f"{format_frequency_short(fstep)}/step")
|
||||||
f"{format_frequency_short(fstep)}/step")
|
|
||||||
self.update_sweep()
|
self.update_sweep()
|
||||||
|
|
||||||
def update_sweep(self):
|
def update_sweep(self):
|
||||||
sweep = self.app.sweep
|
self.app.sweep.update(
|
||||||
with sweep.lock:
|
start=self.get_start(),
|
||||||
sweep.start = self.get_start()
|
end=self.get_end(),
|
||||||
sweep.end = self.get_end()
|
segments=self.get_segments(),
|
||||||
sweep.segments = self.get_segments()
|
points=self.app.vna.datapoints,
|
||||||
sweep.points = self.app.vna.datapoints
|
)
|
|
@ -21,9 +21,8 @@ import dataclasses as DC
|
||||||
import logging
|
import logging
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
|
|
||||||
from PyQt5 import QtCore
|
from PyQt6.QtCore import QSettings, QByteArray
|
||||||
from PyQt5.QtCore import QSettings, QByteArray
|
from PyQt6.QtGui import QColor, QColorConstants
|
||||||
from PyQt5.QtGui import QColor
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -43,12 +42,12 @@ class GUI:
|
||||||
|
|
||||||
@DC.dataclass
|
@DC.dataclass
|
||||||
class ChartsSelected:
|
class ChartsSelected:
|
||||||
chart_00: str = 'S11 Smith Chart'
|
chart_00: str = "S11 Smith Chart"
|
||||||
chart_01: str = 'S11 Return Loss'
|
chart_01: str = "S11 Return Loss"
|
||||||
chart_02: str = 'None'
|
chart_02: str = "None"
|
||||||
chart_10: str = 'S21 Polar Plot'
|
chart_10: str = "S21 Polar Plot"
|
||||||
chart_11: str = 'S21 Gain'
|
chart_11: str = "S21 Gain"
|
||||||
chart_12: str = 'None'
|
chart_12: str = "None"
|
||||||
|
|
||||||
|
|
||||||
@DC.dataclass
|
@DC.dataclass
|
||||||
|
@ -63,39 +62,57 @@ class Chart:
|
||||||
marker_size: int = 8
|
marker_size: int = 8
|
||||||
returnloss_is_positive: bool = False
|
returnloss_is_positive: bool = False
|
||||||
show_bands: bool = False
|
show_bands: bool = False
|
||||||
vswr_lines: list = DC.field(default_factory=lambda: [])
|
vswr_lines: list = DC.field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
@DC.dataclass
|
@DC.dataclass
|
||||||
class ChartColors: # pylint: disable=too-many-instance-attributes
|
class ChartColors: # pylint: disable=too-many-instance-attributes
|
||||||
background: QColor = DC.field(
|
background: QColor = DC.field(
|
||||||
default_factory=lambda: QColor(QtCore.Qt.white))
|
default_factory=lambda: QColor(QColorConstants.White)
|
||||||
|
)
|
||||||
foreground: QColor = DC.field(
|
foreground: QColor = DC.field(
|
||||||
default_factory=lambda: QColor(QtCore.Qt.lightGray))
|
default_factory=lambda: QColor(QColorConstants.LightGray)
|
||||||
|
)
|
||||||
reference: QColor = DC.field(default_factory=lambda: QColor(0, 0, 255, 64))
|
reference: QColor = DC.field(default_factory=lambda: QColor(0, 0, 255, 64))
|
||||||
reference_secondary: QColor = DC.field(
|
reference_secondary: QColor = DC.field(
|
||||||
default_factory=lambda: QColor(0, 0, 192, 48))
|
default_factory=lambda: QColor(0, 0, 192, 48)
|
||||||
|
)
|
||||||
sweep: QColor = DC.field(
|
sweep: QColor = DC.field(
|
||||||
default_factory=lambda: QColor(QtCore.Qt.darkYellow))
|
default_factory=lambda: QColor(QColorConstants.DarkYellow)
|
||||||
|
)
|
||||||
sweep_secondary: QColor = DC.field(
|
sweep_secondary: QColor = DC.field(
|
||||||
default_factory=lambda: QColor(QtCore.Qt.darkMagenta))
|
default_factory=lambda: QColor(QColorConstants.DarkMagenta)
|
||||||
swr: QColor = DC.field(
|
)
|
||||||
default_factory=lambda: QColor(255, 0, 0, 128))
|
swr: QColor = DC.field(default_factory=lambda: QColor(255, 0, 0, 128))
|
||||||
text: QColor = DC.field(
|
text: QColor = DC.field(
|
||||||
default_factory=lambda: QColor(QtCore.Qt.black))
|
default_factory=lambda: QColor(QColorConstants.Black)
|
||||||
bands: QColor = DC.field(
|
)
|
||||||
default_factory=lambda: QColor(128, 128, 128, 48))
|
bands: QColor = DC.field(default_factory=lambda: QColor(128, 128, 128, 48))
|
||||||
|
|
||||||
|
|
||||||
@DC.dataclass
|
@DC.dataclass
|
||||||
class Markers:
|
class Markers:
|
||||||
active_labels: list = DC.field(default_factory=lambda: [
|
active_labels: list = DC.field(
|
||||||
"actualfreq", "impedance", "serr", "serl", "serc", "parr", "parlc",
|
default_factory=lambda: [
|
||||||
"vswr", "returnloss", "s11q", "s11phase", "s21gain", "s21phase",
|
"actualfreq",
|
||||||
])
|
"impedance",
|
||||||
|
"serr",
|
||||||
|
"serl",
|
||||||
|
"serc",
|
||||||
|
"parr",
|
||||||
|
"parlc",
|
||||||
|
"vswr",
|
||||||
|
"returnloss",
|
||||||
|
"s11q",
|
||||||
|
"s11phase",
|
||||||
|
"s21gain",
|
||||||
|
"s21phase",
|
||||||
|
]
|
||||||
|
)
|
||||||
colored_names: bool = True
|
colored_names: bool = True
|
||||||
color_0: QColor = DC.field(
|
color_0: QColor = DC.field(
|
||||||
default_factory=lambda: QColor(QtCore.Qt.darkGray))
|
default_factory=lambda: QColor(QColorConstants.DarkGray)
|
||||||
|
)
|
||||||
color_1: QColor = DC.field(default_factory=lambda: QColor(255, 0, 0))
|
color_1: QColor = DC.field(default_factory=lambda: QColor(255, 0, 0))
|
||||||
color_2: QColor = DC.field(default_factory=lambda: QColor(0, 255, 0))
|
color_2: QColor = DC.field(default_factory=lambda: QColor(0, 255, 0))
|
||||||
color_3: QColor = DC.field(default_factory=lambda: QColor(0, 0, 255))
|
color_3: QColor = DC.field(default_factory=lambda: QColor(0, 0, 255))
|
||||||
|
@ -103,37 +120,34 @@ class Markers:
|
||||||
color_5: QColor = DC.field(default_factory=lambda: QColor(255, 0, 255))
|
color_5: QColor = DC.field(default_factory=lambda: QColor(255, 0, 255))
|
||||||
color_6: QColor = DC.field(default_factory=lambda: QColor(255, 255, 0))
|
color_6: QColor = DC.field(default_factory=lambda: QColor(255, 255, 0))
|
||||||
color_7: QColor = DC.field(
|
color_7: QColor = DC.field(
|
||||||
default_factory=lambda: QColor(QtCore.Qt.lightGray))
|
default_factory=lambda: QColor(QColorConstants.LightGray)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@DC.dataclass
|
@DC.dataclass
|
||||||
class CFG:
|
class CFG:
|
||||||
gui: object = DC.field(
|
gui: object = DC.field(default_factory=GUI)
|
||||||
default_factory=lambda: GUI())
|
charts_selected: object = DC.field(default_factory=ChartsSelected)
|
||||||
charts_selected: object = DC.field(
|
chart: object = DC.field(default_factory=Chart)
|
||||||
default_factory=lambda: ChartsSelected())
|
chart_colors: object = DC.field(default_factory=ChartColors)
|
||||||
chart: object = DC.field(
|
markers: object = DC.field(default_factory=Markers)
|
||||||
default_factory=lambda: Chart())
|
|
||||||
chart_colors: object = DC.field(
|
|
||||||
default_factory=lambda: ChartColors())
|
|
||||||
markers: object = DC.field(
|
|
||||||
default_factory=lambda: Markers())
|
|
||||||
|
|
||||||
|
|
||||||
cfg = CFG()
|
cfg = CFG()
|
||||||
|
|
||||||
|
|
||||||
def restore(settings: 'AppSettings') -> CFG:
|
def restore(settings: "AppSettings") -> CFG:
|
||||||
result = CFG()
|
result = CFG()
|
||||||
for field in DC.fields(result):
|
for field in DC.fields(result):
|
||||||
value = settings.restore_dataclass(field.name.upper(),
|
value = settings.restore_dataclass(
|
||||||
getattr(result, field.name))
|
field.name.upper(), getattr(result, field.name)
|
||||||
|
)
|
||||||
setattr(result, field.name, value)
|
setattr(result, field.name, value)
|
||||||
logger.debug("restored\n(\n%s\n)", result)
|
logger.debug("restored\n(\n%s\n)", result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def store(settings: 'AppSettings', data: CFG = None) -> None:
|
def store(settings: "AppSettings", data: CFG = None) -> None:
|
||||||
data = data or cfg
|
data = data or cfg
|
||||||
logger.debug("storing\n(\n%s\n)", data)
|
logger.debug("storing\n(\n%s\n)", data)
|
||||||
assert isinstance(data, CFG)
|
assert isinstance(data, CFG)
|
||||||
|
@ -145,27 +159,27 @@ def store(settings: 'AppSettings', data: CFG = None) -> None:
|
||||||
|
|
||||||
def from_type(data) -> str:
|
def from_type(data) -> str:
|
||||||
type_map = {
|
type_map = {
|
||||||
bytearray: lambda x: x.hex(),
|
bytearray: bytearray.hex,
|
||||||
QColor: lambda x: x.getRgb(),
|
QColor: QColor.getRgb,
|
||||||
QByteArray: lambda x: x.toHex()
|
QByteArray: QByteArray.toHex,
|
||||||
}
|
}
|
||||||
return (f"{type_map[type(data)](data)}" if
|
return (
|
||||||
type(data) in type_map else
|
f"{type_map[type(data)](data)}" if type(data) in type_map else f"{data}"
|
||||||
f"{data}")
|
)
|
||||||
|
|
||||||
|
|
||||||
def to_type(data: object, data_type: type) -> object:
|
def to_type(data: object, data_type: type) -> object:
|
||||||
type_map = {
|
type_map = {
|
||||||
bool: lambda x: x.lower() == 'true',
|
bool: lambda x: x.lower() == "true",
|
||||||
bytearray: bytearray.fromhex,
|
bytearray: bytearray.fromhex,
|
||||||
list: literal_eval,
|
list: literal_eval,
|
||||||
tuple: literal_eval,
|
tuple: literal_eval,
|
||||||
QColor: lambda x: QColor.fromRgb(*literal_eval(x)),
|
QColor: lambda x: QColor.fromRgb(*literal_eval(x)),
|
||||||
QByteArray: lambda x: QByteArray.fromHex(literal_eval(x))
|
QByteArray: lambda x: QByteArray.fromHex(literal_eval(x)),
|
||||||
}
|
}
|
||||||
return (type_map[data_type](data) if
|
return (
|
||||||
data_type in type_map else
|
type_map[data_type](data) if data_type in type_map else data_type(data)
|
||||||
data_type(data))
|
)
|
||||||
|
|
||||||
|
|
||||||
# noinspection PyDataclass
|
# noinspection PyDataclass
|
||||||
|
@ -178,8 +192,13 @@ class AppSettings(QSettings):
|
||||||
try:
|
try:
|
||||||
assert isinstance(value, field.type)
|
assert isinstance(value, field.type)
|
||||||
except AssertionError as exc:
|
except AssertionError as exc:
|
||||||
logger.error("%s: %s of type %s is not a %s",
|
logger.error(
|
||||||
name, field.name, type(value), field.type)
|
"%s: %s of type %s is not a %s",
|
||||||
|
name,
|
||||||
|
field.name,
|
||||||
|
type(value),
|
||||||
|
field.type,
|
||||||
|
)
|
||||||
raise TypeError from exc
|
raise TypeError from exc
|
||||||
self.setValue(field.name, from_type(value))
|
self.setValue(field.name, from_type(value))
|
||||||
self.endGroup()
|
self.endGroup()
|
|
@ -18,7 +18,6 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import math
|
import math
|
||||||
from numbers import Number
|
from numbers import Number
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
from NanoVNASaver import SITools
|
from NanoVNASaver import SITools
|
||||||
|
|
||||||
|
@ -27,22 +26,27 @@ FMT_FREQ_SHORT = SITools.Format(max_nr_digits=4)
|
||||||
FMT_FREQ_SPACE = SITools.Format(space_str=" ")
|
FMT_FREQ_SPACE = SITools.Format(space_str=" ")
|
||||||
FMT_FREQ_SWEEP = SITools.Format(max_nr_digits=9, allow_strip=True)
|
FMT_FREQ_SWEEP = SITools.Format(max_nr_digits=9, allow_strip=True)
|
||||||
FMT_FREQ_INPUTS = SITools.Format(
|
FMT_FREQ_INPUTS = SITools.Format(
|
||||||
max_nr_digits=10, allow_strip=True,
|
max_nr_digits=10, allow_strip=True, printable_min=0, unprintable_under="- "
|
||||||
printable_min=0, unprintable_under="- ")
|
)
|
||||||
FMT_Q_FACTOR = SITools.Format(
|
FMT_Q_FACTOR = SITools.Format(
|
||||||
max_nr_digits=4, assume_infinity=False,
|
max_nr_digits=4,
|
||||||
min_offset=0, max_offset=0, allow_strip=True)
|
assume_infinity=False,
|
||||||
|
min_offset=0,
|
||||||
|
max_offset=0,
|
||||||
|
allow_strip=True,
|
||||||
|
)
|
||||||
FMT_GROUP_DELAY = SITools.Format(max_nr_digits=5, space_str=" ")
|
FMT_GROUP_DELAY = SITools.Format(max_nr_digits=5, space_str=" ")
|
||||||
FMT_REACT = SITools.Format(max_nr_digits=5, space_str=" ", allow_strip=True)
|
FMT_REACT = SITools.Format(max_nr_digits=5, space_str=" ", allow_strip=True)
|
||||||
FMT_COMPLEX = SITools.Format(max_nr_digits=3, allow_strip=True,
|
FMT_COMPLEX = SITools.Format(
|
||||||
printable_min=0, unprintable_under="- ")
|
max_nr_digits=3, allow_strip=True, printable_min=0, unprintable_under="- "
|
||||||
|
)
|
||||||
FMT_COMPLEX_NEG = SITools.Format(max_nr_digits=3, allow_strip=True)
|
FMT_COMPLEX_NEG = SITools.Format(max_nr_digits=3, allow_strip=True)
|
||||||
FMT_SHORT = SITools.Format(max_nr_digits=4)
|
FMT_SHORT = SITools.Format(max_nr_digits=4)
|
||||||
FMT_WAVELENGTH = SITools.Format(max_nr_digits=4, space_str=" ")
|
FMT_WAVELENGTH = SITools.Format(max_nr_digits=4, space_str=" ")
|
||||||
FMT_PARSE = SITools.Format(parse_sloppy_unit=True, parse_sloppy_kilo=True,
|
FMT_PARSE = SITools.Format(
|
||||||
parse_clamp_min=0)
|
parse_sloppy_unit=True, parse_sloppy_kilo=True, parse_clamp_min=0
|
||||||
FMT_PARSE_VALUE = SITools.Format(
|
)
|
||||||
parse_sloppy_unit=True, parse_sloppy_kilo=True)
|
FMT_PARSE_VALUE = SITools.Format(parse_sloppy_unit=True, parse_sloppy_kilo=True)
|
||||||
FMT_VSWR = SITools.Format(max_nr_digits=3)
|
FMT_VSWR = SITools.Format(max_nr_digits=3)
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,7 +54,7 @@ def format_frequency(freq: Number) -> str:
|
||||||
return str(SITools.Value(freq, "Hz", FMT_FREQ))
|
return str(SITools.Value(freq, "Hz", FMT_FREQ))
|
||||||
|
|
||||||
|
|
||||||
def format_frequency_inputs(freq: Union[Number, str]) -> str:
|
def format_frequency_inputs(freq: Number | str) -> str:
|
||||||
return str(SITools.Value(freq, "Hz", FMT_FREQ_INPUTS))
|
return str(SITools.Value(freq, "Hz", FMT_FREQ_INPUTS))
|
||||||
|
|
||||||
|
|
||||||
|
@ -117,7 +121,7 @@ def format_group_delay(val: float) -> str:
|
||||||
|
|
||||||
|
|
||||||
def format_phase(val: float) -> str:
|
def format_phase(val: float) -> str:
|
||||||
return f"{math.degrees(val):.2f}""\N{DEGREE SIGN}"
|
return f"{math.degrees(val):.2f}" "\N{DEGREE SIGN}"
|
||||||
|
|
||||||
|
|
||||||
def format_complex_adm(z: complex, allow_negative: bool = False) -> str:
|
def format_complex_adm(z: complex, allow_negative: bool = False) -> str:
|
||||||
|
@ -135,7 +139,7 @@ def format_complex_imp(z: complex, allow_negative: bool = False) -> str:
|
||||||
fmt_re = FMT_COMPLEX_NEG if allow_negative else FMT_COMPLEX
|
fmt_re = FMT_COMPLEX_NEG if allow_negative else FMT_COMPLEX
|
||||||
re = SITools.Value(z.real, fmt=fmt_re)
|
re = SITools.Value(z.real, fmt=fmt_re)
|
||||||
im = SITools.Value(abs(z.imag), fmt=FMT_COMPLEX)
|
im = SITools.Value(abs(z.imag), fmt=FMT_COMPLEX)
|
||||||
return f"{re}{'-' if z.imag < 0 else '+'}j{im} ""\N{OHM SIGN}"
|
return f"{re}{'-' if z.imag < 0 else '+'}j{im} " "\N{OHM SIGN}"
|
||||||
|
|
||||||
|
|
||||||
def format_wavelength(length: Number) -> str:
|
def format_wavelength(length: Number) -> str:
|
||||||
|
@ -153,10 +157,11 @@ def parse_frequency(freq: str) -> int:
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
|
|
||||||
def parse_value(val: str, unit: str = "",
|
def parse_value(
|
||||||
fmt: SITools.Format = FMT_PARSE_VALUE) -> float:
|
val: str, unit: str = "", fmt: SITools.Format = FMT_PARSE_VALUE
|
||||||
|
) -> float:
|
||||||
try:
|
try:
|
||||||
val.replace(',', '.')
|
val.replace(",", ".")
|
||||||
return float(SITools.Value(val, unit, fmt))
|
return float(SITools.Value(val, unit, fmt))
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
return 0.0
|
return 0.0
|
|
@ -20,7 +20,6 @@ import logging
|
||||||
import platform
|
import platform
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import List
|
|
||||||
|
|
||||||
import serial
|
import serial
|
||||||
from serial.tools import list_ports
|
from serial.tools import list_ports
|
||||||
|
@ -34,7 +33,10 @@ from NanoVNASaver.Hardware.NanoVNA_F_V2 import NanoVNA_F_V2
|
||||||
from NanoVNASaver.Hardware.NanoVNA_H import NanoVNA_H
|
from NanoVNASaver.Hardware.NanoVNA_H import NanoVNA_H
|
||||||
from NanoVNASaver.Hardware.NanoVNA_H4 import NanoVNA_H4
|
from NanoVNASaver.Hardware.NanoVNA_H4 import NanoVNA_H4
|
||||||
from NanoVNASaver.Hardware.NanoVNA_V2 import NanoVNA_V2
|
from NanoVNASaver.Hardware.NanoVNA_V2 import NanoVNA_V2
|
||||||
from NanoVNASaver.Hardware.TinySA import TinySA
|
from NanoVNASaver.Hardware.TinySA import TinySA, TinySA_Ultra
|
||||||
|
from NanoVNASaver.Hardware.JNCRadio_VNA_3G import JNCRadio_VNA_3G
|
||||||
|
from NanoVNASaver.Hardware.SV4401A import SV4401A
|
||||||
|
from NanoVNASaver.Hardware.SV6301A import SV6301A
|
||||||
from NanoVNASaver.Hardware.Serial import drain_serial, Interface
|
from NanoVNASaver.Hardware.Serial import drain_serial, Interface
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -43,8 +45,8 @@ USBDevice = namedtuple("Device", "vid pid name")
|
||||||
|
|
||||||
USBDEVICETYPES = (
|
USBDEVICETYPES = (
|
||||||
USBDevice(0x0483, 0x5740, "NanoVNA"),
|
USBDevice(0x0483, 0x5740, "NanoVNA"),
|
||||||
USBDevice(0x16c0, 0x0483, "AVNA"),
|
USBDevice(0x16C0, 0x0483, "AVNA"),
|
||||||
USBDevice(0x04b4, 0x0008, "S-A-A-2"),
|
USBDevice(0x04B4, 0x0008, "S-A-A-2"),
|
||||||
)
|
)
|
||||||
RETRIES = 3
|
RETRIES = 3
|
||||||
TIMEOUT = 0.2
|
TIMEOUT = 0.2
|
||||||
|
@ -59,6 +61,10 @@ NAME2DEVICE = {
|
||||||
"F": NanoVNA_F,
|
"F": NanoVNA_F,
|
||||||
"NanoVNA": NanoVNA,
|
"NanoVNA": NanoVNA,
|
||||||
"tinySA": TinySA,
|
"tinySA": TinySA,
|
||||||
|
"tinySA_Ultra": TinySA_Ultra,
|
||||||
|
"JNCRadio": JNCRadio_VNA_3G,
|
||||||
|
"SV4401A": SV4401A,
|
||||||
|
"SV6301A": SV6301A,
|
||||||
"Unknown": NanoVNA,
|
"Unknown": NanoVNA,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,30 +77,41 @@ NAME2DEVICE = {
|
||||||
|
|
||||||
def _fix_v2_hwinfo(dev):
|
def _fix_v2_hwinfo(dev):
|
||||||
# if dev.hwid == r'PORTS\VID_04B4&PID_0008\DEMO':
|
# if dev.hwid == r'PORTS\VID_04B4&PID_0008\DEMO':
|
||||||
if r'PORTS\VID_04B4&PID_0008' in dev.hwid:
|
if r"PORTS\VID_04B4&PID_0008" in dev.hwid:
|
||||||
dev.vid, dev.pid = 0x04b4, 0x0008
|
dev.vid, dev.pid = 0x04B4, 0x0008
|
||||||
return dev
|
return dev
|
||||||
|
|
||||||
|
|
||||||
def usb_typename(device: ListPortInfo) -> str:
|
def usb_typename(device: ListPortInfo) -> str:
|
||||||
return next((t.name for t in USBDEVICETYPES if
|
return next(
|
||||||
device.vid == t.vid and device.pid == t.pid),
|
(
|
||||||
"")
|
t.name
|
||||||
|
for t in USBDEVICETYPES
|
||||||
|
if device.vid == t.vid and device.pid == t.pid
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Get list of interfaces with VNAs connected
|
# Get list of interfaces with VNAs connected
|
||||||
|
|
||||||
|
|
||||||
def get_interfaces() -> List[Interface]:
|
def get_interfaces() -> list[Interface]:
|
||||||
interfaces = []
|
interfaces = []
|
||||||
# serial like usb interfaces
|
# serial like usb interfaces
|
||||||
for d in list_ports.comports():
|
for d in list_ports.comports():
|
||||||
if platform.system() == 'Windows' and d.vid is None:
|
if platform.system() == "Windows" and d.vid is None:
|
||||||
d = _fix_v2_hwinfo(d)
|
d = _fix_v2_hwinfo(d)
|
||||||
if not (typename := usb_typename(d)):
|
if not (typename := usb_typename(d)):
|
||||||
continue
|
continue
|
||||||
logger.debug("Found %s USB:(%04x:%04x) on port %s",
|
logger.debug(
|
||||||
typename, d.vid, d.pid, d.device)
|
"Found %s USB:(%04x:%04x) on port %s",
|
||||||
iface = Interface('serial', typename)
|
typename,
|
||||||
|
d.vid,
|
||||||
|
d.pid,
|
||||||
|
d.device,
|
||||||
|
)
|
||||||
|
iface = Interface("serial", typename)
|
||||||
iface.port = d.device
|
iface.port = d.device
|
||||||
iface.open()
|
iface.open()
|
||||||
iface.comment = get_comment(iface)
|
iface.comment = get_comment(iface)
|
||||||
|
@ -105,13 +122,12 @@ def get_interfaces() -> List[Interface]:
|
||||||
return interfaces
|
return interfaces
|
||||||
|
|
||||||
|
|
||||||
def get_portinfos() -> List[str]:
|
def get_portinfos() -> list[str]:
|
||||||
portinfos = []
|
portinfos = []
|
||||||
# serial like usb interfaces
|
# serial like usb interfaces
|
||||||
for d in list_ports.comports():
|
for d in list_ports.comports():
|
||||||
logger.debug("Found USB:(%04x:%04x) on port %s",
|
logger.debug("Found USB:(%04x:%04x) on port %s", d.vid, d.pid, d.device)
|
||||||
d.vid, d.pid, d.device)
|
iface = Interface("serial", "DEBUG")
|
||||||
iface = Interface('serial', "DEBUG")
|
|
||||||
iface.port = d.device
|
iface.port = d.device
|
||||||
iface.open()
|
iface.open()
|
||||||
version = detect_version(iface)
|
version = detect_version(iface)
|
||||||
|
@ -130,19 +146,23 @@ def get_comment(iface: Interface) -> str:
|
||||||
with iface.lock:
|
with iface.lock:
|
||||||
vna_version = detect_version(iface)
|
vna_version = detect_version(iface)
|
||||||
|
|
||||||
if vna_version == 'v2':
|
if vna_version == "v2":
|
||||||
return "S-A-A-2"
|
return "S-A-A-2"
|
||||||
|
|
||||||
logger.info("Finding firmware variant...")
|
logger.info("Finding firmware variant...")
|
||||||
info = get_info(iface)
|
info = get_info(iface)
|
||||||
for search, name in (
|
for search, name in (
|
||||||
("AVNA + Teensy", "AVNA"),
|
("AVNA + Teensy", "AVNA"),
|
||||||
("NanoVNA-H 4", "H4"),
|
("NanoVNA-H 4", "H4"),
|
||||||
("NanoVNA-H", "H"),
|
("NanoVNA-H", "H"),
|
||||||
("NanoVNA-F_V2", "F_V2"),
|
("NanoVNA-F_V2", "F_V2"),
|
||||||
("NanoVNA-F", "F"),
|
("NanoVNA-F", "F"),
|
||||||
("NanoVNA", "NanoVNA"),
|
("NanoVNA", "NanoVNA"),
|
||||||
("tinySA", "tinySA"),
|
("tinySA4", "tinySA_Ultra"),
|
||||||
|
("tinySA", "tinySA"),
|
||||||
|
("JNCRadio_VNA_3G", "JNCRadio"),
|
||||||
|
("SV4401A", "SV4401A"),
|
||||||
|
("SV6301A", "SV6301A"),
|
||||||
):
|
):
|
||||||
if info.find(search) >= 0:
|
if info.find(search) >= 0:
|
||||||
return name
|
return name
|
||||||
|
@ -171,7 +191,7 @@ def detect_version(serial_port: serial.Serial) -> str:
|
||||||
if data.startswith("2"):
|
if data.startswith("2"):
|
||||||
return "v2"
|
return "v2"
|
||||||
logger.debug("Retry detection: %s", i + 1)
|
logger.debug("Retry detection: %s", i + 1)
|
||||||
logger.error('No VNA detected. Hardware responded to CR with: %s', data)
|
logger.error("No VNA detected. Hardware responded to CR with: %s", data)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
# 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 logging
|
||||||
|
|
||||||
|
import serial
|
||||||
|
from PyQt6.QtGui import QImage, QPixmap
|
||||||
|
|
||||||
|
from NanoVNASaver.Hardware.NanoVNA import NanoVNA
|
||||||
|
from NanoVNASaver.Hardware.Serial import Interface
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class JNCRadio_VNA_3G(NanoVNA):
|
||||||
|
name = "JNCRadio_VNA_3G"
|
||||||
|
screenwidth = 800
|
||||||
|
screenheight = 480
|
||||||
|
valid_datapoints = (501, 11, 101, 1001)
|
||||||
|
sweep_points_min = 11
|
||||||
|
sweep_points_max = 1001
|
||||||
|
|
||||||
|
def __init__(self, iface: Interface):
|
||||||
|
super().__init__(iface)
|
||||||
|
self.sweep_max_freq_Hz = 3e9
|
||||||
|
|
||||||
|
def getScreenshot(self) -> QPixmap:
|
||||||
|
logger.debug("Capturing screenshot...")
|
||||||
|
self.serial.timeout = 8
|
||||||
|
if not self.connected():
|
||||||
|
return QPixmap()
|
||||||
|
try:
|
||||||
|
rgba_array = self._capture_data()
|
||||||
|
image = QImage(
|
||||||
|
rgba_array,
|
||||||
|
self.screenwidth,
|
||||||
|
self.screenheight,
|
||||||
|
QImage.Format.Format_RGB16,
|
||||||
|
)
|
||||||
|
logger.debug("Captured screenshot")
|
||||||
|
return QPixmap(image)
|
||||||
|
except serial.SerialException as exc:
|
||||||
|
logger.exception("Exception while capturing screenshot: %s", exc)
|
||||||
|
return QPixmap()
|
||||||
|
|
||||||
|
def setSweep(self, start, stop):
|
||||||
|
self.start = start
|
||||||
|
self.stop = stop
|
||||||
|
list(self.exec_command(f"scan {start} {stop} {self.datapoints}"))
|
|
@ -18,11 +18,10 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
from typing import List
|
|
||||||
|
|
||||||
import serial
|
import serial
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PyQt5 import QtGui
|
from PyQt6.QtGui import QImage, QPixmap
|
||||||
|
|
||||||
from NanoVNASaver.Hardware.Serial import drain_serial, Interface
|
from NanoVNASaver.Hardware.Serial import drain_serial, Interface
|
||||||
from NanoVNASaver.Hardware.VNA import VNA
|
from NanoVNASaver.Hardware.VNA import VNA
|
||||||
|
@ -46,7 +45,6 @@ class NanoVNA(VNA):
|
||||||
self._sweepdata = []
|
self._sweepdata = []
|
||||||
|
|
||||||
def _get_running_frequencies(self):
|
def _get_running_frequencies(self):
|
||||||
|
|
||||||
logger.debug("Reading values: frequencies")
|
logger.debug("Reading values: frequencies")
|
||||||
try:
|
try:
|
||||||
frequencies = super().readValues("frequencies")
|
frequencies = super().readValues("frequencies")
|
||||||
|
@ -61,42 +59,45 @@ class NanoVNA(VNA):
|
||||||
timeout = self.serial.timeout
|
timeout = self.serial.timeout
|
||||||
with self.serial.lock:
|
with self.serial.lock:
|
||||||
drain_serial(self.serial)
|
drain_serial(self.serial)
|
||||||
self.serial.write("capture\r".encode('ascii'))
|
self.serial.write("capture\r".encode("ascii"))
|
||||||
self.serial.readline()
|
self.serial.readline()
|
||||||
self.serial.timeout = 4
|
self.serial.timeout = 4
|
||||||
image_data = self.serial.read(
|
image_data = self.serial.read(
|
||||||
self.screenwidth * self.screenheight * 2)
|
self.screenwidth * self.screenheight * 2
|
||||||
|
)
|
||||||
self.serial.timeout = timeout
|
self.serial.timeout = timeout
|
||||||
self.serial.timeout = timeout
|
self.serial.timeout = timeout
|
||||||
return image_data
|
return image_data
|
||||||
|
|
||||||
def _convert_data(self, image_data: bytes) -> bytes:
|
def _convert_data(self, image_data: bytes) -> bytes:
|
||||||
rgb_data = struct.unpack(
|
rgb_data = struct.unpack(
|
||||||
f">{self.screenwidth * self.screenheight}H",
|
f">{self.screenwidth * self.screenheight}H", image_data
|
||||||
image_data)
|
)
|
||||||
rgb_array = np.array(rgb_data, dtype=np.uint32)
|
rgb_array = np.array(rgb_data, dtype=np.uint32)
|
||||||
return (0xFF000000 +
|
return (
|
||||||
((rgb_array & 0xF800) << 8) +
|
0xFF000000
|
||||||
((rgb_array & 0x07E0) << 5) +
|
+ ((rgb_array & 0xF800) << 8)
|
||||||
((rgb_array & 0x001F) << 3))
|
+ ((rgb_array & 0x07E0) << 5)
|
||||||
|
+ ((rgb_array & 0x001F) << 3)
|
||||||
|
)
|
||||||
|
|
||||||
def getScreenshot(self) -> QtGui.QPixmap:
|
def getScreenshot(self) -> QPixmap:
|
||||||
logger.debug("Capturing screenshot...")
|
logger.debug("Capturing screenshot...")
|
||||||
if not self.connected():
|
if not self.connected():
|
||||||
return QtGui.QPixmap()
|
return QPixmap()
|
||||||
try:
|
try:
|
||||||
rgba_array = self._convert_data(self._capture_data())
|
rgba_array = self._convert_data(self._capture_data())
|
||||||
image = QtGui.QImage(
|
image = QImage(
|
||||||
rgba_array,
|
rgba_array,
|
||||||
self.screenwidth,
|
self.screenwidth,
|
||||||
self.screenheight,
|
self.screenheight,
|
||||||
QtGui.QImage.Format_ARGB32)
|
QImage.Format.Format_ARGB32,
|
||||||
|
)
|
||||||
logger.debug("Captured screenshot")
|
logger.debug("Captured screenshot")
|
||||||
return QtGui.QPixmap(image)
|
return QPixmap(image)
|
||||||
except serial.SerialException as exc:
|
except serial.SerialException as exc:
|
||||||
logger.exception(
|
logger.exception("Exception while capturing screenshot: %s", exc)
|
||||||
"Exception while capturing screenshot: %s", exc)
|
return QPixmap()
|
||||||
return QtGui.QPixmap()
|
|
||||||
|
|
||||||
def resetSweep(self, start: int, stop: int):
|
def resetSweep(self, start: int, stop: int):
|
||||||
list(self.exec_command(f"sweep {start} {stop} {self.datapoints}"))
|
list(self.exec_command(f"sweep {start} {stop} {self.datapoints}"))
|
||||||
|
@ -121,14 +122,18 @@ class NanoVNA(VNA):
|
||||||
self.features.add("Scan command")
|
self.features.add("Scan command")
|
||||||
self.sweep_method = "scan"
|
self.sweep_method = "scan"
|
||||||
|
|
||||||
def readFrequencies(self) -> List[int]:
|
def readFrequencies(self) -> list[int]:
|
||||||
logger.debug("readFrequencies: %s", self.sweep_method)
|
logger.debug("readFrequencies: %s", self.sweep_method)
|
||||||
if self.sweep_method != "scan_mask":
|
if self.sweep_method != "scan_mask":
|
||||||
return super().readFrequencies()
|
return super().readFrequencies()
|
||||||
return [int(line) for line in self.exec_command(
|
return [
|
||||||
f"scan {self.start} {self.stop} {self.datapoints} 0b001")]
|
int(line)
|
||||||
|
for line in self.exec_command(
|
||||||
|
f"scan {self.start} {self.stop} {self.datapoints} 0b001"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
def readValues(self, value) -> List[str]:
|
def readValues(self, value) -> list[str]:
|
||||||
if self.sweep_method != "scan_mask":
|
if self.sweep_method != "scan_mask":
|
||||||
return super().readValues(value)
|
return super().readValues(value)
|
||||||
logger.debug("readValue with scan mask (%s)", value)
|
logger.debug("readValue with scan mask (%s)", value)
|
||||||
|
@ -137,11 +142,12 @@ class NanoVNA(VNA):
|
||||||
if value == "data 0":
|
if value == "data 0":
|
||||||
self._sweepdata = []
|
self._sweepdata = []
|
||||||
for line in self.exec_command(
|
for line in self.exec_command(
|
||||||
f"scan {self.start} {self.stop} {self.datapoints} 0b110"):
|
f"scan {self.start} {self.stop} {self.datapoints} 0b110"
|
||||||
|
):
|
||||||
data = line.split()
|
data = line.split()
|
||||||
self._sweepdata.append((
|
self._sweepdata.append(
|
||||||
f"{data[0]} {data[1]}",
|
(f"{data[0]} {data[1]}", f"{data[2]} {data[3]}")
|
||||||
f"{data[2]} {data[3]}"))
|
)
|
||||||
if value == "data 0":
|
if value == "data 0":
|
||||||
return [x[0] for x in self._sweepdata]
|
return [x[0] for x in self._sweepdata]
|
||||||
if value == "data 1":
|
if value == "data 1":
|
|
@ -19,7 +19,7 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import serial
|
import serial
|
||||||
from PyQt5 import QtGui
|
from PyQt6.QtGui import QImage, QPixmap
|
||||||
|
|
||||||
from NanoVNASaver.Hardware.NanoVNA import NanoVNA
|
from NanoVNASaver.Hardware.NanoVNA import NanoVNA
|
||||||
from NanoVNASaver.Hardware.Serial import Interface
|
from NanoVNASaver.Hardware.Serial import Interface
|
||||||
|
@ -36,20 +36,20 @@ class NanoVNA_F_V2(NanoVNA):
|
||||||
super().__init__(iface)
|
super().__init__(iface)
|
||||||
self.sweep_max_freq_Hz = 3e9
|
self.sweep_max_freq_Hz = 3e9
|
||||||
|
|
||||||
def getScreenshot(self) -> QtGui.QPixmap:
|
def getScreenshot(self) -> QPixmap:
|
||||||
logger.debug("Capturing screenshot...")
|
logger.debug("Capturing screenshot...")
|
||||||
if not self.connected():
|
if not self.connected():
|
||||||
return QtGui.QPixmap()
|
return QPixmap()
|
||||||
try:
|
try:
|
||||||
rgba_array = self._capture_data()
|
rgba_array = self._capture_data()
|
||||||
image = QtGui.QImage(
|
image = QImage(
|
||||||
rgba_array,
|
rgba_array,
|
||||||
self.screenwidth,
|
self.screenwidth,
|
||||||
self.screenheight,
|
self.screenheight,
|
||||||
QtGui.QImage.Format_RGB16)
|
QImage.Format.Format_RGB16,
|
||||||
|
)
|
||||||
logger.debug("Captured screenshot")
|
logger.debug("Captured screenshot")
|
||||||
return QtGui.QPixmap(image)
|
return QPixmap(image)
|
||||||
except serial.SerialException as exc:
|
except serial.SerialException as exc:
|
||||||
logger.exception(
|
logger.exception("Exception while capturing screenshot: %s", exc)
|
||||||
"Exception while capturing screenshot: %s", exc)
|
return QPixmap()
|
||||||
return QtGui.QPixmap()
|
|
|
@ -20,19 +20,18 @@ import logging
|
||||||
import platform
|
import platform
|
||||||
from struct import pack, unpack_from
|
from struct import pack, unpack_from
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from NanoVNASaver.Hardware.Serial import Interface
|
from NanoVNASaver.Hardware.Serial import Interface
|
||||||
from NanoVNASaver.Hardware.VNA import VNA
|
from NanoVNASaver.Hardware.VNA import VNA
|
||||||
from NanoVNASaver.Version import Version
|
from NanoVNASaver.Version import Version
|
||||||
|
|
||||||
if platform.system() != 'Windows':
|
if platform.system() != "Windows":
|
||||||
import tty
|
import tty
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_CMD_NOP = 0x00
|
_CMD_NOP = 0x00
|
||||||
_CMD_INDICATE = 0x0d
|
_CMD_INDICATE = 0x0D
|
||||||
_CMD_READ = 0x10
|
_CMD_READ = 0x10
|
||||||
_CMD_READ2 = 0x11
|
_CMD_READ2 = 0x11
|
||||||
_CMD_READ4 = 0x12
|
_CMD_READ4 = 0x12
|
||||||
|
@ -49,22 +48,23 @@ _ADDR_SWEEP_POINTS = 0x20
|
||||||
_ADDR_SWEEP_VALS_PER_FREQ = 0x22
|
_ADDR_SWEEP_VALS_PER_FREQ = 0x22
|
||||||
_ADDR_RAW_SAMPLES_MODE = 0x26
|
_ADDR_RAW_SAMPLES_MODE = 0x26
|
||||||
_ADDR_VALUES_FIFO = 0x30
|
_ADDR_VALUES_FIFO = 0x30
|
||||||
_ADDR_DEVICE_VARIANT = 0xf0
|
_ADDR_DEVICE_VARIANT = 0xF0
|
||||||
_ADDR_PROTOCOL_VERSION = 0xf1
|
_ADDR_PROTOCOL_VERSION = 0xF1
|
||||||
_ADDR_HARDWARE_REVISION = 0xf2
|
_ADDR_HARDWARE_REVISION = 0xF2
|
||||||
_ADDR_FW_MAJOR = 0xf3
|
_ADDR_FW_MAJOR = 0xF3
|
||||||
_ADDR_FW_MINOR = 0xf4
|
_ADDR_FW_MINOR = 0xF4
|
||||||
|
|
||||||
WRITE_SLEEP = 0.05
|
WRITE_SLEEP = 0.05
|
||||||
|
|
||||||
_ADF4350_TXPOWER_DESC_MAP = {
|
_ADF4350_TXPOWER_DESC_MAP = {
|
||||||
0: '9dB attenuation',
|
0: "9dB attenuation",
|
||||||
1: '6dB attenuation',
|
1: "6dB attenuation",
|
||||||
2: '3dB attenuation',
|
2: "3dB attenuation",
|
||||||
3: 'Maximum',
|
3: "Maximum",
|
||||||
}
|
}
|
||||||
_ADF4350_TXPOWER_DESC_REV_MAP = {
|
_ADF4350_TXPOWER_DESC_REV_MAP = {
|
||||||
value: key for key, value in _ADF4350_TXPOWER_DESC_MAP.items()}
|
value: key for key, value in _ADF4350_TXPOWER_DESC_MAP.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class NanoVNA_V2(VNA):
|
class NanoVNA_V2(VNA):
|
||||||
|
@ -76,7 +76,7 @@ class NanoVNA_V2(VNA):
|
||||||
def __init__(self, iface: Interface):
|
def __init__(self, iface: Interface):
|
||||||
super().__init__(iface)
|
super().__init__(iface)
|
||||||
|
|
||||||
if platform.system() != 'Windows':
|
if platform.system() != "Windows":
|
||||||
tty.setraw(self.serial.fd)
|
tty.setraw(self.serial.fd)
|
||||||
|
|
||||||
# reset protocol to known state
|
# reset protocol to known state
|
||||||
|
@ -85,8 +85,8 @@ class NanoVNA_V2(VNA):
|
||||||
sleep(WRITE_SLEEP)
|
sleep(WRITE_SLEEP)
|
||||||
|
|
||||||
# firmware major version of 0xff indicates dfu mode
|
# firmware major version of 0xff indicates dfu mode
|
||||||
if self.version.data["major"] == 0xff:
|
if self.version.major == 0xFF:
|
||||||
raise IOError('Device is in DFU mode')
|
raise IOError("Device is in DFU mode")
|
||||||
|
|
||||||
if "S21 hack" in self.features:
|
if "S21 hack" in self.features:
|
||||||
self.valid_datapoints = (101, 11, 51, 201, 301, 501, 1021)
|
self.valid_datapoints = (101, 11, 51, 201, 301, 501, 1021)
|
||||||
|
@ -116,8 +116,13 @@ class NanoVNA_V2(VNA):
|
||||||
self.features.update({"Set TX power partial", "Set Average"})
|
self.features.update({"Set TX power partial", "Set Average"})
|
||||||
# Can only set ADF4350 power, i.e. for >= 140MHz
|
# Can only set ADF4350 power, i.e. for >= 140MHz
|
||||||
self.txPowerRanges = [
|
self.txPowerRanges = [
|
||||||
((140e6, self.sweep_max_freq_Hz),
|
(
|
||||||
[_ADF4350_TXPOWER_DESC_MAP[value] for value in (3, 2, 1, 0)]),
|
(140e6, self.sweep_max_freq_Hz),
|
||||||
|
[
|
||||||
|
_ADF4350_TXPOWER_DESC_MAP[value]
|
||||||
|
for value in (3, 2, 1, 0)
|
||||||
|
],
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def readFirmware(self) -> str:
|
def readFirmware(self) -> str:
|
||||||
|
@ -125,7 +130,7 @@ class NanoVNA_V2(VNA):
|
||||||
logger.debug("readFirmware: %s", result)
|
logger.debug("readFirmware: %s", result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def readFrequencies(self) -> List[int]:
|
def readFrequencies(self) -> list[int]:
|
||||||
return [
|
return [
|
||||||
int(self.sweepStartHz + i * self.sweepStepHz)
|
int(self.sweepStartHz + i * self.sweepStepHz)
|
||||||
for i in range(self.datapoints)
|
for i in range(self.datapoints)
|
||||||
|
@ -135,9 +140,15 @@ class NanoVNA_V2(VNA):
|
||||||
freq_index = -1
|
freq_index = -1
|
||||||
|
|
||||||
for i in range(pointstoread):
|
for i in range(pointstoread):
|
||||||
(fwd_real, fwd_imag, rev0_real, rev0_imag, rev1_real,
|
(
|
||||||
rev1_imag, freq_index) = unpack_from(
|
fwd_real,
|
||||||
"<iiiiiihxxxxxx", arr, i * 32)
|
fwd_imag,
|
||||||
|
rev0_real,
|
||||||
|
rev0_imag,
|
||||||
|
rev1_real,
|
||||||
|
rev1_imag,
|
||||||
|
freq_index,
|
||||||
|
) = unpack_from("<iiiiiihxxxxxx", arr, i * 32)
|
||||||
fwd = complex(fwd_real, fwd_imag)
|
fwd = complex(fwd_real, fwd_imag)
|
||||||
refl = complex(rev0_real, rev0_imag)
|
refl = complex(rev0_real, rev0_imag)
|
||||||
thru = complex(rev1_real, rev1_imag)
|
thru = complex(rev1_real, rev1_imag)
|
||||||
|
@ -147,7 +158,7 @@ class NanoVNA_V2(VNA):
|
||||||
|
|
||||||
logger.debug("Freq index to: %i", freq_index)
|
logger.debug("Freq index to: %i", freq_index)
|
||||||
|
|
||||||
def readValues(self, value) -> List[str]:
|
def readValues(self, value) -> list[str]:
|
||||||
# Actually grab the data only when requesting channel 0.
|
# Actually grab the data only when requesting channel 0.
|
||||||
# The hardware will return all channels which we will store.
|
# The hardware will return all channels which we will store.
|
||||||
if value == "data 0":
|
if value == "data 0":
|
||||||
|
@ -158,12 +169,14 @@ class NanoVNA_V2(VNA):
|
||||||
self.serial.write(pack("<Q", 0))
|
self.serial.write(pack("<Q", 0))
|
||||||
sleep(WRITE_SLEEP)
|
sleep(WRITE_SLEEP)
|
||||||
# cmd: write register 0x30 to clear FIFO
|
# cmd: write register 0x30 to clear FIFO
|
||||||
self.serial.write(pack("<BBB",
|
self.serial.write(
|
||||||
_CMD_WRITE, _ADDR_VALUES_FIFO, 0))
|
pack("<BBB", _CMD_WRITE, _ADDR_VALUES_FIFO, 0)
|
||||||
|
)
|
||||||
sleep(WRITE_SLEEP)
|
sleep(WRITE_SLEEP)
|
||||||
# clear sweepdata
|
# clear sweepdata
|
||||||
self._sweepdata = [(complex(), complex())] * (
|
self._sweepdata = [(complex(), complex())] * (
|
||||||
self.datapoints + s21hack)
|
self.datapoints + s21hack
|
||||||
|
)
|
||||||
pointstodo = self.datapoints + s21hack
|
pointstodo = self.datapoints + s21hack
|
||||||
# we read at most 255 values at a time and the time required
|
# we read at most 255 values at a time and the time required
|
||||||
# empirically is just over 3 seconds for 101 points or
|
# empirically is just over 3 seconds for 101 points or
|
||||||
|
@ -174,9 +187,13 @@ class NanoVNA_V2(VNA):
|
||||||
pointstoread = min(255, pointstodo)
|
pointstoread = min(255, pointstodo)
|
||||||
# cmd: read FIFO, addr 0x30
|
# cmd: read FIFO, addr 0x30
|
||||||
self.serial.write(
|
self.serial.write(
|
||||||
pack("<BBB",
|
pack(
|
||||||
_CMD_READFIFO, _ADDR_VALUES_FIFO,
|
"<BBB",
|
||||||
pointstoread))
|
_CMD_READFIFO,
|
||||||
|
_ADDR_VALUES_FIFO,
|
||||||
|
pointstoread,
|
||||||
|
)
|
||||||
|
)
|
||||||
sleep(WRITE_SLEEP)
|
sleep(WRITE_SLEEP)
|
||||||
# each value is 32 bytes
|
# each value is 32 bytes
|
||||||
nBytes = pointstoread * 32
|
nBytes = pointstoread * 32
|
||||||
|
@ -185,8 +202,9 @@ class NanoVNA_V2(VNA):
|
||||||
# timeout secs
|
# timeout secs
|
||||||
arr = self.serial.read(nBytes)
|
arr = self.serial.read(nBytes)
|
||||||
if nBytes != len(arr):
|
if nBytes != len(arr):
|
||||||
logger.warning("expected %d bytes, got %d",
|
logger.warning(
|
||||||
nBytes, len(arr))
|
"expected %d bytes, got %d", nBytes, len(arr)
|
||||||
|
)
|
||||||
# the way to retry on timeout is keep the data
|
# the way to retry on timeout is keep the data
|
||||||
# already read then try to read the rest of
|
# already read then try to read the rest of
|
||||||
# the data into the array
|
# the data into the array
|
||||||
|
@ -205,8 +223,7 @@ class NanoVNA_V2(VNA):
|
||||||
|
|
||||||
idx = 1 if value == "data 1" else 0
|
idx = 1 if value == "data 1" else 0
|
||||||
return [
|
return [
|
||||||
f'{str(x[idx].real)} {str(x[idx].imag)}'
|
f"{str(x[idx].real)} {str(x[idx].imag)}" for x in self._sweepdata
|
||||||
for x in self._sweepdata
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def resetSweep(self, start: int, stop: int):
|
def resetSweep(self, start: int, stop: int):
|
||||||
|
@ -225,15 +242,15 @@ class NanoVNA_V2(VNA):
|
||||||
raise IOError("Timeout reading version registers")
|
raise IOError("Timeout reading version registers")
|
||||||
return Version(f"{resp[0]}.0.{resp[1]}")
|
return Version(f"{resp[0]}.0.{resp[1]}")
|
||||||
|
|
||||||
def readVersion(self) -> 'Version':
|
def readVersion(self) -> "Version":
|
||||||
result = self._read_version(_ADDR_FW_MAJOR,
|
result = self._read_version(_ADDR_FW_MAJOR, _ADDR_FW_MINOR)
|
||||||
_ADDR_FW_MINOR)
|
|
||||||
logger.debug("readVersion: %s", result)
|
logger.debug("readVersion: %s", result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def read_board_revision(self) -> 'Version':
|
def read_board_revision(self) -> "Version":
|
||||||
result = self._read_version(_ADDR_DEVICE_VARIANT,
|
result = self._read_version(
|
||||||
_ADDR_HARDWARE_REVISION)
|
_ADDR_DEVICE_VARIANT, _ADDR_HARDWARE_REVISION
|
||||||
|
)
|
||||||
logger.debug("read_board_revision: %s", result)
|
logger.debug("read_board_revision: %s", result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -243,34 +260,41 @@ class NanoVNA_V2(VNA):
|
||||||
return
|
return
|
||||||
self.sweepStartHz = start
|
self.sweepStartHz = start
|
||||||
self.sweepStepHz = step
|
self.sweepStepHz = step
|
||||||
logger.info('NanoVNAV2: set sweep start %d step %d',
|
logger.info(
|
||||||
self.sweepStartHz, self.sweepStepHz)
|
"NanoVNAV2: set sweep start %d step %d",
|
||||||
|
self.sweepStartHz,
|
||||||
|
self.sweepStepHz,
|
||||||
|
)
|
||||||
self._updateSweep()
|
self._updateSweep()
|
||||||
return
|
return
|
||||||
|
|
||||||
def _updateSweep(self):
|
def _updateSweep(self):
|
||||||
s21hack = "S21 hack" in self.features
|
s21hack = "S21 hack" in self.features
|
||||||
cmd = pack("<BBQ", _CMD_WRITE8, _ADDR_SWEEP_START,
|
cmd = pack(
|
||||||
max(50000,
|
"<BBQ",
|
||||||
int(self.sweepStartHz - (self.sweepStepHz * s21hack))))
|
_CMD_WRITE8,
|
||||||
cmd += pack("<BBQ", _CMD_WRITE8,
|
_ADDR_SWEEP_START,
|
||||||
_ADDR_SWEEP_STEP, int(self.sweepStepHz))
|
max(50000, int(self.sweepStartHz - (self.sweepStepHz * s21hack))),
|
||||||
cmd += pack("<BBH", _CMD_WRITE2,
|
)
|
||||||
_ADDR_SWEEP_POINTS, self.datapoints + s21hack)
|
cmd += pack(
|
||||||
cmd += pack("<BBH", _CMD_WRITE2,
|
"<BBQ", _CMD_WRITE8, _ADDR_SWEEP_STEP, int(self.sweepStepHz)
|
||||||
_ADDR_SWEEP_VALS_PER_FREQ, 1)
|
)
|
||||||
|
cmd += pack(
|
||||||
|
"<BBH", _CMD_WRITE2, _ADDR_SWEEP_POINTS, self.datapoints + s21hack
|
||||||
|
)
|
||||||
|
cmd += pack("<BBH", _CMD_WRITE2, _ADDR_SWEEP_VALS_PER_FREQ, 1)
|
||||||
with self.serial.lock:
|
with self.serial.lock:
|
||||||
self.serial.write(cmd)
|
self.serial.write(cmd)
|
||||||
sleep(WRITE_SLEEP)
|
sleep(WRITE_SLEEP)
|
||||||
|
|
||||||
def setTXPower(self, freq_range, power_desc):
|
def setTXPower(self, freq_range, power_desc):
|
||||||
if freq_range[0] != 140e6:
|
if freq_range[0] != 140e6:
|
||||||
raise ValueError('Invalid TX power frequency range')
|
raise ValueError("Invalid TX power frequency range")
|
||||||
# 140MHz..max => ADF4350
|
# 140MHz..max => ADF4350
|
||||||
self._set_register(0x42, _ADF4350_TXPOWER_DESC_REV_MAP[power_desc], 1)
|
self._set_register(0x42, _ADF4350_TXPOWER_DESC_REV_MAP[power_desc], 1)
|
||||||
|
|
||||||
def _set_register(self, addr, value, size):
|
def _set_register(self, addr, value, size):
|
||||||
packet = b''
|
packet = b""
|
||||||
if size == 1:
|
if size == 1:
|
||||||
packet = pack("<BBB", _CMD_WRITE, addr, value)
|
packet = pack("<BBB", _CMD_WRITE, addr, value)
|
||||||
elif size == 2:
|
elif size == 2:
|
|
@ -0,0 +1,64 @@
|
||||||
|
# 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 logging
|
||||||
|
|
||||||
|
import serial
|
||||||
|
from PyQt6.QtGui import QImage, QPixmap
|
||||||
|
|
||||||
|
from NanoVNASaver.Hardware.NanoVNA import NanoVNA
|
||||||
|
from NanoVNASaver.Hardware.Serial import Interface
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SV4401A(NanoVNA):
|
||||||
|
name = "SV4401A"
|
||||||
|
screenwidth = 1024
|
||||||
|
screenheight = 600
|
||||||
|
valid_datapoints = (501, 101, 1001)
|
||||||
|
sweep_points_min = 101
|
||||||
|
sweep_points_max = 1001
|
||||||
|
|
||||||
|
def __init__(self, iface: Interface):
|
||||||
|
super().__init__(iface)
|
||||||
|
self.sweep_max_freq_Hz = 4.4e9
|
||||||
|
|
||||||
|
def getScreenshot(self) -> QPixmap:
|
||||||
|
logger.debug("Capturing screenshot...")
|
||||||
|
self.serial.timeout = 8
|
||||||
|
if not self.connected():
|
||||||
|
return QPixmap()
|
||||||
|
try:
|
||||||
|
rgba_array = self._capture_data()
|
||||||
|
image = QImage(
|
||||||
|
rgba_array,
|
||||||
|
self.screenwidth,
|
||||||
|
self.screenheight,
|
||||||
|
QImage.Format.Format_RGB16,
|
||||||
|
)
|
||||||
|
logger.debug("Captured screenshot")
|
||||||
|
return QPixmap(image)
|
||||||
|
except serial.SerialException as exc:
|
||||||
|
logger.exception("Exception while capturing screenshot: %s", exc)
|
||||||
|
return QPixmap()
|
||||||
|
|
||||||
|
def setSweep(self, start, stop):
|
||||||
|
self.start = start
|
||||||
|
self.stop = stop
|
||||||
|
list(self.exec_command(f"scan {start} {stop} {self.datapoints}"))
|
|
@ -0,0 +1,64 @@
|
||||||
|
# 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 logging
|
||||||
|
|
||||||
|
import serial
|
||||||
|
from PyQt6.QtGui import QImage, QPixmap
|
||||||
|
|
||||||
|
from NanoVNASaver.Hardware.NanoVNA import NanoVNA
|
||||||
|
from NanoVNASaver.Hardware.Serial import Interface
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SV6301A(NanoVNA):
|
||||||
|
name = "SV6301A"
|
||||||
|
screenwidth = 1024
|
||||||
|
screenheight = 600
|
||||||
|
valid_datapoints = (501, 101, 1001)
|
||||||
|
sweep_points_min = 101
|
||||||
|
sweep_points_max = 1001
|
||||||
|
|
||||||
|
def __init__(self, iface: Interface):
|
||||||
|
super().__init__(iface)
|
||||||
|
self.sweep_max_freq_Hz = 6.3e9
|
||||||
|
|
||||||
|
def getScreenshot(self) -> QPixmap:
|
||||||
|
logger.debug("Capturing screenshot...")
|
||||||
|
self.serial.timeout = 8
|
||||||
|
if not self.connected():
|
||||||
|
return QPixmap()
|
||||||
|
try:
|
||||||
|
rgba_array = self._capture_data()
|
||||||
|
image = QImage(
|
||||||
|
rgba_array,
|
||||||
|
self.screenwidth,
|
||||||
|
self.screenheight,
|
||||||
|
QImage.Format.Format_RGB16,
|
||||||
|
)
|
||||||
|
logger.debug("Captured screenshot")
|
||||||
|
return QPixmap(image)
|
||||||
|
except serial.SerialException as exc:
|
||||||
|
logger.exception("Exception while capturing screenshot: %s", exc)
|
||||||
|
return QPixmap()
|
||||||
|
|
||||||
|
def setSweep(self, start, stop):
|
||||||
|
self.start = start
|
||||||
|
self.stop = stop
|
||||||
|
list(self.exec_command(f"scan {start} {stop} {self.datapoints}"))
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue