Porównaj commity

...

710 Commity

Autor SHA1 Wiadomość Data
Holger Müller a04d6d9b39 BIG BINARY BUILD TEST 2024-02-11 12:07:47 +01:00
Holger Müller 00dd59ffc6 library updates 2024-01-10 09:23:35 +01:00
Holger Müller d3216d2ddb
Bugfix/exec crash (#678)
* fixed crash on pop-menu #677
* updated libs
2023-11-14 18:04:18 +01:00
t52ta6ek 96dd23211a
Fix checking for updates (#674)
* Because the application version seems to be a generated value, a different
method needed to be devised to try and determine what the current version
nuber if. We now attempt scan the github release tags for the latest release
version. If a newer version is detected, the application will display the
current version and direct the user to click the link to view the latest
release page.

* Rather, should report error against TAGS_URL and not LATEST_URL at this stage.
2023-08-08 14:30:25 +02:00
Holger Müller 2f8c5346eb
Feature/actions (#672)
* use python3.11
* noconsole for windows binary
* no more 32-Bit versions
2023-08-03 17:01:03 +02:00
t52ta6ek 4257ac152a
Add the nanosaver project icon to resulting Windows application. (#671) 2023-08-03 14:35:03 +02:00
Holger Müller 21e85bdb49
fix swwep update crash #668 (#669) 2023-08-01 12:42:38 +02:00
Holger Müller b4800102d8
Feature/pip updates (#666)
* fix startup script for pyinstaller
* update 3rd party libs
* flatpak setup
* rely on requirements for pyqt6
* xcb fixes for linux binary build
* removed old linux build workflow
2023-07-31 13:31:27 +02:00
Name abb80a5160
Sweep.py: add getters and setters for private fields (#659)
* style, Sweep.py: remove a double negation

* style, NanoVNASaver.py: simplify sweepSource computation

* Sweep.py: add getters and setters for private fields

Beware that this commit removes a lock from
SweepSettings.update_tex_power, and adds one to
DeviceSettings.updatecustomPoint.
Both changse may be incorrect, depending on the role of the lock
(issue #657).

Follows: 6eb24f23 d09b55e1 dbea311a

Since d09b55e1, the Properties.name class attribute is overriden by
each assignment to the properties.name instance attribute.
This is most probably unwanted.

This commit

 * removes @dataclass, which is confusing as some attributes are
   managed because of the lock.
   Because of this, it has to restore __repr__ and __eq__.
 * provides getters and setters for private attributes, and
   protects each update by a thread lock
 * adds a regression test for the bug fixed by d09b55e1 (immutable
   properties).
2023-07-30 09:03:06 +02:00
t52ta6ek 5bed1bc6cc
Control panel width increased a few pixels to allow Markers -> "reference" checkbox to be capitalized like the other checkboxes and radio buttons. Now able to be displayed as "Reference" without cutting off the text of the "Enable Delta Marker" checkbox. (#665)
Added icon_48x48.ico so that pyinstaller/auto-py-to-exe can create a Windows executable with the actual NanoVNA-Saver project icon instead of a generic Python icon. The .ico was created by converting the icon_48x48.png image.

"Files" button in control panel changed to "Files ..." to be consistent with other "..." actions where a user will take additional action after pressing the button.

Minor corrections in the Calibration page "Are you sure?" and "Calibration assistant" pop-ups where text was being run together. e.g. "doso" instead of "do so" etc.

Fix issue: #663
2023-07-30 08:52:49 +02:00
t52ta6ek 20c1e4ec7c
With current firmware, the JNCRadio, SV4401A, SV6301A devices allows 1001 datapoints (#662)
* With current firmware, these JNCRadio_VNA_3G, SV4401A, SV6301A devices allow 1001 datapoints.

Updated datapoints dropdown to show their respective minimums, the 'typical' 101, device default 501 and maximum 1001 points. Users may choose any other values between min and max not seen in the dropdown list using the custom datapoints option.

These devices now use the factory default 501 datapoints as their initial datapoints value.

* cut-paste typo in sweep_points_min (facepalm)
2023-07-26 07:54:31 +02:00
Name 21ba0ef665
Simplify the in-source launcher script (#660)
The nanovna-saver.py script is ignored by setup.cfg, its only purpose
is to test the version in the source directory.

According to https://setuptools.pypa.io/en/latest/history.html,
pkg_resources.py2warn has been removed from setuptools in 2020
2023-07-17 14:21:16 +02:00
t52ta6ek eff83097f8
Added support for devices reporting as SV4401A and SV6301A using Sysj… (#655)
* Added support for devices reporting as SV4401A and SV6301A using Sysjoint's support for SV4401A as model.
2023-07-08 10:41:09 +02:00
Name dbea311a02
Adapt sweep settings imports and tests to source changes (#653)
6eb24f23 from merge request 625
made NamedTuple an ancestor of Properties, adapting the imports and
tests.

d09b55e1 from merge request 628
removed it but forgot to remove the related changes.
2023-07-08 10:39:35 +02:00
t52ta6ek a4a923a649
Jncradiovna3g - support Sysjoint-Tek / CHELEGANCE JNCRadio VNA 3G (#652)
* Added support for Sysjoint-Tek / CHELEGANCE JNCRadio VNA 3G
   Device reported as JNCRadio and Custom points added under Manage tab.
* Minor change to correct spelling or error msg.
2023-07-05 10:18:51 +02:00
Martin ce0c7dd226
Redesign of the About window (#648)
- Info and version check closer together
- More precise wording
- Reflect the connection status of the VNA

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2023-07-05 10:00:47 +02:00
Martin 546d3b188a
use correct URL for NanoVNASaver version check (#647)
Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2023-07-05 10:00:06 +02:00
Martin 1f233819d2
option --auto-connect, connect automatically if one device detected (#645)
* option --auto-connect, connect automatically to the 1st detected device
* autoconnect only if there is exactly one device

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2023-07-05 09:59:26 +02:00
Martin a8ffbc3aee
fix "Could not parse stylesheet of object QGroupBox..." error (#643)
* fix "Could not parse stylesheet of object QGroupBox..." error

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>

* fix https://github.com/NanoVNA-Saver/nanovna-saver/issues/596

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>

---------

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2023-06-21 14:08:19 +02:00
Holger Müller ce8a59d478 fixed typo in calibration code #637 2023-05-13 16:49:34 +02:00
Crispin Tschirky aab2a15f69
Build of MacOS app bundle NanoVNASaver.app (#634)
* add icon for MacOS based on icon_48x48.png
* add macos build script to build NanoVNASaver.app for current architecture
* add releas app action to build: NanoVNASaver.app-x86_64.tar.gz action
2023-05-05 10:40:54 +02:00
Sascha Silbe 9b4575e307
nanovna-saver.py: fix execution from outside source directory (#632)
nanovna-saver.py can be called from outside the source directory. The
module import path needs to be resolved relative to the source
directory, not relative to the current working directory of the
process.

Fixes: b0110002 ("moved to pyscaffold directory structure")
2023-05-02 11:12:21 +02:00
Henk Vergonet 8f86722c1e
fix: PyQt6 fixes (#629)
More fixes for PyQt6 should now be usable again with >python3.7 & PyQt6

Calibration - tested
Display Configuration - tested
2023-04-18 12:14:21 +02:00
Henk Vergonet d09b55e1ae
fix: sweep settings and hw version update (#628)
* Remove privacy breaches from the HTML documentation

Icons and screenshots should not inform distant websites each time an
user reads readme.html.

The suggestions in this draft are independent and will probably be
discussed separately, but they affect a single file so for a first
review a single commit is convenient.

* fix: sweep settings and hw version update

NamedTuples are inmutable either use _replace or use the class itself.

* Revert "Remove privacy breaches from the HTML documentation"

---------

Co-authored-by: Nicolas Boulenguez <nicolas@debian.org>
2023-04-15 19:23:28 +02:00
Name 6eb24f2315
Trivial style changes (#625)
* Style: update type annotations

* Style: simplify extraction of version from metadata

* Style: replace some handwritten classes with namedtuples or dataclasses

* RIZ.py: remove unused import

* Style: remove some redundant lambda constructs

* Marker/Values: remove __init__ parameters

Mutable default values imply some complexity. In this case, the
constructor is always called without arguments.
2023-03-22 15:56:59 +01:00
Name d89c9f9d94
tests/data/s2p: remove DOS end of line characters (#624)
Git sometimes replaces CRLF with a single characters, so the tar.gz
archives generated by github from tags differ from the tagged commit.

The tests also pass with a single line terminator.
2023-03-20 09:28:20 +01:00
Name f34f3d1f67
Add an UNIX-style manual page (#622)
Some redistributors want a manual page for each executable in path.

The installation path may differ accross systems, so the manual page
is not installed by default.
2023-03-20 09:27:06 +01:00
Name 1cd5c052db
__main__.py: remove /bin/env shebang (#623)
The file is not intended for direct execution,
not installed in the path,
and not marked as executable.
Its extension is sufficient for editors to trigger syntax highlighting.

The shebang seems to only trigger warnings on systems wher /bin/env is
unavailable (it may be in /usr/bin for example).
2023-03-20 09:25:55 +01:00
Holger Müller 52cdac4f52 back to python 3.9 for windows 2023-03-15 14:19:30 +01:00
Holger Müller fafe0b2536 setup fixes 2023-03-15 14:07:32 +01:00
Holger Müller c5e00666aa pip update 2023-03-15 13:52:29 +01:00
Holger Müller 8dec23296e updated workflows 2023-03-15 13:14:14 +01:00
Holger Müller d1592ac1a3 github actions fetch-depth 2023-03-15 13:02:49 +01:00
Holger Müller 4e06fc53cf generate version for binary builds 2023-03-15 12:55:56 +01:00
Holger Müller 45c2338196 Merge branch 'main' of github.com:NanoVNA-Saver/nanovna-saver 2023-03-15 12:14:10 +01:00
Holger Müller 2bab4d4b0d try to get version in about 2023-03-15 12:06:13 +01:00
Holger Müller b3a9f6d8cb
Delete _version.py 2023-03-14 21:18:02 +01:00
Holger Müller 3c752a9731 build fixes 2023-03-14 21:00:56 +01:00
Holger Müller b2c2598d3c requierements and workflow 2023-03-14 20:10:27 +01:00
dependabot[bot] c18a6c226f
Bump setuptools from 65.3.0 to 65.5.1 (#617)
Bumps [setuptools](https://github.com/pypa/setuptools) from 65.3.0 to 65.5.1.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/CHANGES.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v65.3.0...v65.5.1)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-14 19:24:46 +01:00
Holger Müller dd2f5b8a5d
Feature/tinysa (#616)
* TinySA and PyQt6 fixes
2023-03-14 19:22:46 +01:00
Holger Müller b322d3dc09 Updated changes in README.rst 2023-03-13 12:13:44 +01:00
Holger Müller 5b21315a11 PyQt6 updates 2023-03-13 12:13:44 +01:00
Holger Müller 9ace7d8cd4 PyQt6 fixes 2023-03-13 12:13:44 +01:00
Holger Müller b768a8e01b pyqt6 2023-03-13 12:13:44 +01:00
Holger Müller 2c58b2ba8f
Merge pull request #614 from MarcFontaine/main
restore options.entry_points
2023-03-10 19:02:58 +01:00
MarcFontaine a45baea9e2
restore options.entry_points 2023-03-10 18:46:50 +01:00
Holger Müller db5cd98e03
Merge pull request #612 from NanoVNA-Saver/feature/v0.6.0
Feature/v0.6.0
2023-03-08 13:57:16 +01:00
Holger Müller 74792b3192
Merge pull request #611 from zarath/updates
used black for reformatting/lintig
2023-03-08 09:52:38 +01:00
Holger Müller 50b540a832 used black for reformatting/lintig 2023-03-08 09:40:39 +01:00
Holger Müller 094b0185e7
Merge pull request #609 from zarath/updates
moved to pyscaffold directory structure
2023-03-07 21:18:17 +01:00
Holger Müller b0110002ec moved to pyscaffold directory structure
and fixed resonances analysys crash #608
2023-03-07 21:11:12 +01:00
Holger Müller c0e177bf1a linting 2023-03-05 13:33:05 +01:00
Holger Müller 185a64b5ae removed unused imports 2023-02-28 21:18:00 +01:00
Holger Müller f7d72d4320 fixed another python3.11 float int crash 2023-02-28 20:32:19 +01:00
Holger Müller 82e582b9c0 Release v0.5.5 2023-02-28 19:52:51 +01:00
Holger Müller 59e7e1809a library version updates 2023-02-28 19:39:46 +01:00
Holger Müller 0b82754350 Updated installation instructions 2023-02-28 19:07:44 +01:00
Holger Müller 6f6f6c65e1 Move cal data parsing to CalDataSet class 2023-02-28 18:50:18 +01:00
Holger Müller 92a8a0e39d removed duplicate code for scrollareas 2023-02-27 21:01:35 +01:00
Holger Müller b47e665575
Merge pull request #606 from zarath/feature/fixes_for_release
Feature/fixes for release
2023-02-26 19:48:52 +01:00
Holger Müller 7f920249b1 linting 2023-02-26 19:46:38 +01:00
Holger Müller 5860b04ce6 generate cal data file content from CalDataSet 2023-02-26 19:46:23 +01:00
Holger Müller 9231737b70
Merge pull request #605 from zarath/bugfix/#603_calibration
Fix calibration data loading
2023-02-26 10:14:55 +01:00
Holger Müller 29518eef00 Fix calibration data loading 2023-02-26 10:12:05 +01:00
Holger Müller 8e9976a540
Merge pull request #604 from NanoVNA-Saver/feature/release_0.5.5
Feature/release 0.5.5
2023-02-24 20:08:13 +01:00
Holger Müller 0fbb301435 fix for sol data load 2023-02-24 20:03:41 +01:00
Holger Müller 93ee51d236 Workaround for V2 serial int crash 2023-02-24 19:31:58 +01:00
Holger Müller 925cf6d4e1 Merge remote-tracking branch 'upstream/main' into feature/release_0.5.5 2023-02-24 19:31:32 +01:00
Holger Müller 09246b6a34 Linting 2023-02-19 09:08:14 +01:00
Roel Jordans dc8874c1c9 Stop sweeping with stop button in continous mode 2023-02-19 09:08:14 +01:00
Roel Jordans c4623ddd90 Added brief documentation of the new S11 mu option to README.md
Signed-off-by: Roel Jordans <r.jordans@tue.nl>
2023-02-19 09:08:14 +01:00
Roel Jordans 02371bc56b Clarify calculation and origin of np.conj() 2023-02-19 09:08:14 +01:00
Roel Jordans 0ffe0eaf72 Moved core dimension fields to toplevel menu
also added copy method for new settings
2023-02-19 09:08:14 +01:00
Roel Jordans ee3467e5ec Calculating mu of a core based on given dimensions 2023-02-19 09:08:14 +01:00
Roel Jordans 3d3e31e176 Re-organize RI chart to have RIZ specialization
- separate impedance plotting specific bits
- preparation new chart to plot mu' and mu'' for given core dimensions
2023-02-19 09:08:14 +01:00
Martin f377c999fa fix #592 (crash on mouse click in TDR window) (#593)
* fix #592 (crash on mouse click in TDR window)

AttributeError: 'list' object has no attribute 'size'

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>

* Apply suggestions from code review

Looks better, I was too much focused on the "size" and it was all about the emptiness.

Co-authored-by: Holger Müller <zarath@gmx.de>

---------

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
Co-authored-by: Holger Müller <zarath@gmx.de>
2023-02-19 09:08:14 +01:00
Holger Müller 4cebe94b87 Linting 2023-02-19 09:08:14 +01:00
Holger Müller e4bd720160 Updated github workflows and library versions 2023-02-19 09:08:14 +01:00
Martin a437029fcd Proposal for #485 - make all windows scrollable (#591)
Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2023-02-19 09:08:14 +01:00
Holger Müller d7867b7535 Linting 2023-02-19 09:06:23 +01:00
Holger Müller 09d8b2b866 Merge branch 'feature/release_0.5.5' of github.com:NanoVNA-Saver/nanovna-saver into feature/release_0.5.5 2023-02-19 08:55:26 +01:00
Roel Jordans 69f5089c1f Stop sweeping with stop button in continous mode 2023-02-19 08:54:49 +01:00
Roel Jordans 044c1c885e Added brief documentation of the new S11 mu option to README.md
Signed-off-by: Roel Jordans <r.jordans@tue.nl>
2023-02-19 08:54:49 +01:00
Roel Jordans 0c3f179303 Clarify calculation and origin of np.conj() 2023-02-19 08:54:49 +01:00
Roel Jordans 9b199b53a9 Moved core dimension fields to toplevel menu
also added copy method for new settings
2023-02-19 08:54:49 +01:00
Roel Jordans dc44d33786 Calculating mu of a core based on given dimensions 2023-02-19 08:54:49 +01:00
Roel Jordans 3265d0368b Re-organize RI chart to have RIZ specialization
- separate impedance plotting specific bits
- preparation new chart to plot mu' and mu'' for given core dimensions
2023-02-19 08:54:49 +01:00
Holger Müller a9d0e02e4d Merge remote-tracking branch 'upstream/main' into feature/release_0.5.5 2023-02-14 08:44:25 +01:00
ikatkov 2c868d818f getYPosition fix 2023-02-13 17:26:45 +01:00
Martin f996ee9ceb
fix #592 (crash on mouse click in TDR window) (#593)
* fix #592 (crash on mouse click in TDR window)

AttributeError: 'list' object has no attribute 'size'

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>

* Apply suggestions from code review

Looks better, I was too much focused on the "size" and it was all about the emptiness.

Co-authored-by: Holger Müller <zarath@gmx.de>

---------

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
Co-authored-by: Holger Müller <zarath@gmx.de>
2023-02-01 18:12:43 +01:00
Holger Müller d313911840 Linting 2023-02-01 11:38:34 +01:00
Holger Müller 7c86009b3e Updated github workflows and library versions 2023-02-01 09:04:09 +01:00
Martin c536de6dc8
Proposal for #485 - make all windows scrollable (#591)
Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2023-02-01 08:28:23 +01:00
Holger Müller d6b2f8119b
fix numpy related crash in tdr modules (#587) 2023-01-25 09:29:33 +01:00
Holger Müller d654ea0441
Removed Changelog.md from setup.py 2023-01-03 16:38:43 +01:00
Jaroslav Škarvada 4d21d6dfdc
desktop file: drop exec and fix it according to the spec (#582)
Fixes #580, fixes #581

Signed-off-by: Jaroslav Škarvada <jskarvad@redhat.com>

Signed-off-by: Jaroslav Škarvada <jskarvad@redhat.com>
2023-01-03 16:06:18 +01:00
Holger Müller ed362a0c4b
Merge pull request #577 from NanoVNA-Saver/feature/release_0.5.4
release 0.5.4
2023-01-01 16:37:13 +01:00
Holger Müller 2a9a4101f0 release 0.5.4 2023-01-01 16:31:38 +01:00
Holger Müller 74d3ac7d07
Feature/python3 11 (#576)
* updated requirements
* use python3.11 in release actions
* fixed np.complex deprecation
2023-01-01 16:06:43 +01:00
Holger Müller f6e1868a95
Merge pull request #572 from zarath/feature/python3_11
dataclass fix for python3.11 compatability
2022-12-18 11:41:11 +01:00
Holger Müller fa03e7d753 dataclass fix for python3.11 compatability 2022-12-08 17:26:17 +01:00
Holger Müller fb50f4a01b
New SI prefixes added - Ronna, Quetta - Yeah! (#570) 2022-11-24 16:42:52 +01:00
Attilio Panniello 9c5b1e01ea
Support of NanoVNA V2 Plus5 on Windows (#566)
(FW 20220814)

Co-authored-by: Attilio Panniello <63241631+apanniello@users.noreply.github.com>
2022-10-16 18:52:35 +02:00
Holger Müller 4d94bbec92 moved a static method to simple function 2022-10-16 08:14:55 +02:00
Holger Müller 1951388c71 suppress pylint import complaints 2022-10-16 08:14:55 +02:00
Martin ad14650fc5 experimental rpm build (works on a debian system)
Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2022-10-16 08:12:49 +02:00
Holger Müller 10d786e787 fixed packagename for purge in Makefile 2022-10-15 21:33:37 +02:00
Holger Müller 239edc1cd0 kept most setup info in setup.cfg
TODO: switch to pyproject.toml in future
2022-10-15 21:33:37 +02:00
Martin 0485e2c8c2 Feat: Improve deb build, add README, icon, desktop file to the deb package
Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2022-10-15 21:33:37 +02:00
Martin c5bee7f3e3
add a Makefile to build a simple debian package (#560)
Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2022-10-13 11:57:41 +02:00
Holger Müller 533a543a1b
Feature/documentation (#558)
* documentation updates
* github workflow files
2022-10-09 19:02:05 +02:00
Holger Müller c5a23fcd46
Merge pull request #557 from zarath/bugfix/calibration_crash
use math instead of table for log step calculation
2022-10-07 21:49:01 +02:00
Holger Müller d57ae78efa use math instead of table for log step calculation 2022-10-07 21:42:58 +02:00
Holger Müller 7b9dd5ab0a
Feature/refactoring (#556)
* github workflow naming for ubuntu 22.04
* refactored Hardware.py
* use contextlib instead of try
* use dataclass instead of userdict
* simplyfied sweep worker
* fixed calibration data loading
* explicit import of scipy functions - may fix #555
2022-10-06 18:15:59 +02:00
Holger Müller 114b815c72
Merge pull request #554 from zarath/bugfix/python3.8
bugfix for python3.8 compatability
2022-09-29 12:48:46 +02:00
Holger Müller 35686319cd bugfix for python3.8 compatability
fixes #522, #511
2022-09-29 12:47:02 +02:00
Holger Müller 8e6ab89189 added ubuntu 22.04 release 2022-09-25 18:12:32 +02:00
Holger Müller 62b5c5a1b2
Merge pull request #553 from zarath/bugfix/analysis_import_fix
fixed import pass for analyis module
2022-09-24 20:49:27 +02:00
Holger Müller 193711dc6a fixed import pass for analyis module 2022-09-24 20:47:17 +02:00
Holger Müller 0f19d5aa3c
Merge pull request #550 from zarath/feature/refactoring
Feature/refactoring
2022-09-21 22:37:11 +02:00
Holger Müller 8f224e0e37 refactored ResonanceAnalysis 2022-09-21 18:39:26 +02:00
Holger Müller 400ed54f9a Split history NanoVNASaver/Analysis/ResonanceAnalysis.py to NanoVNASaver/Analysis/EFHWAnalysis.py 2022-09-21 17:46:27 +02:00
Holger Müller ec23d1b3c8 Split history NanoVNASaver/Analysis/ResonanceAnalysis.py to NanoVNASaver/Analysis/EFHWAnalysis.py 2022-09-21 17:46:27 +02:00
Holger Müller 8f016399bb Split history NanoVNASaver/Analysis/ResonanceAnalysis.py to NanoVNASaver/Analysis/EFHWAnalysis.py 2022-09-21 17:46:27 +02:00
Holger Müller d33924511d Split history NanoVNASaver/Analysis/ResonanceAnalysis.py to NanoVNASaver/Analysis/EFHWAnalysis.py 2022-09-21 17:46:27 +02:00
Holger Müller 44e38515bc refactoring analytics 2022-09-21 17:45:08 +02:00
Holger Müller 0b1b73cfc1
fixed refactored attribute 2022-09-20 20:04:27 +02:00
Holger Müller d1ea20f989
Simplified analysis 2022-09-20 19:52:34 +02:00
Holger Müller 01eb028f9f
updated libraries 2022-09-20 07:26:50 +02:00
Holger Müller 24a4ca0ffa
refactored BandStopAnalysis 2022-09-19 19:21:22 +02:00
Holger Müller a73028e2c3
derive BandStop from BandPass 2022-09-18 20:09:56 +02:00
Holger Müller a732aea84b
formatting 2022-09-18 20:09:19 +02:00
Holger Müller a6c3ccc0d3
moved cable data to variable 2022-09-18 20:06:45 +02:00
Holger Müller 8e73456668
added test for printing NaN (Not a Number) 2022-09-18 18:32:35 +02:00
Holger Müller 879d5ddea3
Feature/v0.5.4 pre (#548)
* Simplified VSWR analysis
* Split history VSWRAnalysis.py to ResonanceAnalysis.py
* simplified BandPassAnalysis
2022-09-18 18:03:52 +02:00
Holger Müller 6630568ed9
Merge pull request #544 from zarath/bugfix/541_crash_in_ri
More int float issues fixed.
2022-09-16 08:20:55 +02:00
Holger Müller d163143356
Version bump 2022-09-16 08:15:16 +02:00
Holger Müller 79a577ffe3
pycodestyle fixes 2022-09-15 21:59:55 +02:00
Holger Müller cabb8a4351
pycodestyle fixes 2022-09-15 21:05:07 +02:00
Holger Müller ef6a3c2d0a
Another float glitched trough.. 2022-09-15 20:32:44 +02:00
Holger Müller 1609295bd9
pycodestyle changes 2022-09-15 20:08:23 +02:00
Holger Müller 3f8151aad7
ensure int type in draw coordinates 2022-09-15 17:37:41 +02:00
Holger Müller 36bff6a09d
fixed default for SITools::Value 2022-09-15 08:03:26 +02:00
Holger Müller 8bc452d48f
Linting Charts 2022-09-15 07:53:08 +02:00
Holger Müller d86cbec7c4
Cast varible type to int 2022-09-15 07:51:28 +02:00
Holger Müller 92af43ae22
Merge pull request #547 from fk0815/main
Fix test_restore_dataclass
2022-09-14 21:06:57 +02:00
Frank Kunz c792c1bd69 Fix test_restore_dataclass
Signed-off-by: Frank Kunz <mailinglists@kunz-im-inter.net>
2022-09-14 20:58:27 +02:00
Holger Müller 4159c70558
Imports and linebreaks adjusted 2022-09-14 19:00:25 +02:00
Holger Müller 2f69f5c154
More int float issues fixed. 2022-09-13 18:46:02 +02:00
Holger Müller 05f7b9bbf0
updated documentation 2022-09-11 20:10:44 +02:00
Holger Müller f0e51639b9
fixed typo in workflow 2022-09-11 19:16:25 +02:00
Holger Müller a8ccbce40f
push patch version 2022-09-11 19:09:08 +02:00
Holger Müller 51d0e318cd
Merge pull request #543 from zarath/bugfix/541_crash_in_draw
Use more integer devisions
2022-09-11 18:46:43 +02:00
Holger Müller 7690f39c19
Use more integer devisions
Using more integer divisions to get right type for QPainter
points
2022-09-11 18:29:03 +02:00
mss 06ffd48de0 fix type hint and typo 2022-09-11 17:14:13 +02:00
Holger Müller 37007a650d
fixed crashing polar charts on python3.10 #528 (#539) 2022-09-08 22:36:14 +02:00
Holger Müller 08635b0d4b
Code cleanups and refactorings in LogCharts (#538) 2022-09-08 20:32:01 +02:00
Holger Müller 7dc3290fdd
Update README.md
Finalized 0.5.0
2022-09-08 09:55:22 +02:00
Holger Müller 75d2d1e11e
Merge pull request #537 from zarath/feature/version_0_5_0
Version 0.5.0
2022-09-08 09:53:00 +02:00
Holger Müller 4da5ea3678
Version 0.5.0 2022-09-08 09:50:06 +02:00
Holger Müller b2b64ab9cd
Merge pull request #536 from zarath/bugfix/535_msspad
Made changes on suggestions of #535 by @msspad
2022-09-08 09:00:00 +02:00
Holger Müller b3f816e5f5
updated libraries for binary builds 2022-09-08 08:52:31 +02:00
Holger Müller b4a55c6179
Added CodeQL workflow 2022-09-08 08:42:13 +02:00
Holger Müller 8814e4f471
use numpy array for TDRWindow.td attribute 2022-09-07 21:12:54 +02:00
Holger Müller c14ca3c350
Linting and simplifying readValues in NanoVNA_V2 2022-09-07 20:26:58 +02:00
Holger Müller 611d00c551
raise IOError if read_board_revision fails 2022-09-07 20:13:09 +02:00
Holger Müller f1e2041fbe
split complex method 2022-09-07 20:12:29 +02:00
Holger Müller 7baa870e75
Merge pull request #530 from Oscilllator/main
Add negative real resistance for proper graph scaling
2022-08-27 07:57:44 +02:00
Oscilllator ad1cfb5787 Add negative real resistance for proper graph scaling 2022-08-25 19:01:18 -07:00
Holger Müller bd4eeb5e63
Merge pull request #518 from blinken/main
getPlotable, cast return values to int
2022-07-19 20:34:30 +02:00
Patrick Coleman 32f88eba31 getPlotable, cast return values to int
Closes #500

NanoVNA-saver crashes per below when getPlotable returns a float.
getPlotable is always followed by a call to drawLine(self, int, int,
int, int), so it needs to return ints.

Cast getPlotable return values to int in all cases.

Traceback (most recent call last):
  File "/home/blinken/co/nanovna-saver/NanoVNASaver/Charts/Frequency.py", line 447, in paintEvent
    self.drawValues(qp)
  File "/home/blinken/co/nanovna-saver/NanoVNASaver/Charts/VSWR.py", line 144, in drawValues
    self.drawData(qp, self.data, Chart.color.sweep)
  File "/home/blinken/co/nanovna-saver/NanoVNASaver/Charts/Frequency.py", line 641, in drawData
    qp.drawLine(prevx, prevy, new_x, new_y)
TypeError: arguments did not match any overloaded call:
  drawLine(self, QLineF): argument 1 has unexpected type 'int'
  drawLine(self, QLine): argument 1 has unexpected type 'int'
  drawLine(self, int, int, int, int): argument 3 has unexpected type 'numpy.float64'
  drawLine(self, QPoint, QPoint): argument 1 has unexpected type 'int'
  drawLine(self, Union[QPointF, QPoint], Union[QPointF, QPoint]): argument 1 has unexpected type 'int'
Aborted
2022-07-19 14:10:15 +01:00
Holger Müller e452c055d5
Merge pull request #515 from zarath/bugfix/update_numpy
Updated requieremens (security fixes)
2022-06-25 15:03:08 +02:00
Holger Mueller 32711ec6f7 Updated requieremens (security fixes) 2022-06-25 13:14:09 +02:00
Holger Müller 16f028e5ed
Merge pull request #510 from zarath/bugfix/dont_chrash_on_zoom
fix zoom crash when wheel on square charts
2022-05-28 22:51:57 +02:00
Holger Mueller cc3795af51 fix zoom crash when wheel on square charts 2022-05-28 22:50:07 +02:00
Holger Müller f5afb78970
Merge pull request #509 from zarath/bugfix/zoom
enable to zoom out (not perfect yet)
2022-05-28 21:54:02 +02:00
Holger Mueller 2821f4d08c enable to zoom out (not perfect yet) 2022-05-28 21:48:13 +02:00
Holger Mueller 50ded6dcf3 Hotfix type exception on exit 2022-05-27 18:39:36 +02:00
Holger Müller 01c83cd2f0
Feature/refactor settings (#507)
* fixed Default module
* fixed QByteArray parsing
* updated changelog
* refactored charts
2022-05-27 13:02:18 +02:00
Holger Müller 7b9d803b35
Merge pull request #506 from zarath/feature/linting_20220525
just linting
2022-05-27 11:01:05 +02:00
Holger Mueller 00d9884d32 just linting 2022-05-27 09:03:37 +02:00
Holger Müller ee3048d985
catch serial exception on device open - fixes #502 (#504) 2022-05-27 08:33:53 +02:00
Holger Müller 3f4a262abe
Feature/linting 220514 (#503)
* Unified Chart Code
* New Defauls class for persistance of settings - fixes #491
* Removed non-interactive update checks
2022-05-24 17:05:59 +02:00
Holger Müller 6aa7aaa051
Feature/linting 220402 (#499)
* added .flatpak-builder to .gitingnore
* GroupDelay Chart simplified
* allow numpy > 1.21 fixes #456
* Added flatpak manifest
2022-05-14 11:00:34 +02:00
Holger Müller 140ce4906c
Merge pull request #496 from gaionim/features/delta_reference
delta reference
2022-05-14 10:39:33 +02:00
Holger Müller 06cd2de0a6
Merge pull request #495 from gaionim/pull/narrow_span
use more digit in format frequency when span is narrow
2022-05-13 07:56:15 +02:00
Mauro Gaioni f933027af5 button in one row 2022-05-06 15:17:22 +02:00
Mauro Gaioni 134affab04 pulito, ma prende marker3 invece che reference 2022-05-06 14:49:31 +02:00
Mauro Gaioni a457d9c688 closes #438
use more digit in format frequency
when span is narrow
2022-05-05 12:34:21 +02:00
Holger Müller cd3d2b6c2c unused imports and linting 2022-04-01 18:58:11 +02:00
Holger Müller 582b442910
Merge pull request #469 from NanoVNA-Saver/testing
Release of 0.4.0
Enhanced through calibration
2022-04-01 16:51:11 +02:00
Holger Müller 6c82ff6ee3 Updated documentation for 0.4.0 2022-04-01 16:44:54 +02:00
Holger Müller f68d3c2fb7 Version 0.4.0
Allow 2 digit versions. Fixes #483
2022-04-01 16:29:19 +02:00
Holger Müller 69cc2dcfb4
Merge pull request #482 from NanoVNA-Saver/Development
github workflow fixes
2022-03-26 20:14:31 +01:00
Holger Müller 02de5a1650
Fix linux binary build 2022-03-26 20:11:33 +01:00
Holger Müller 23db45d6d7
Merge pull request #480 from dhalbert/patch-1
Fix command-line formatting in README.md
2022-03-26 20:09:39 +01:00
Dan Halbert ed48c85e8e
Fix command-line formatting in README.md
Add additional indentation to force code formatting of command lines. (Could use triple-backticks instead.)
2022-03-26 09:03:03 -04:00
Holger Müller 747184e85f use pyinstaller 4.10 2022-03-17 18:04:34 +01:00
Holger Müller 630d6fafc3 linting 2022-03-17 17:54:52 +01:00
Holger Müller d0dad2a746 Merge branch 'testing' into Development 2022-03-17 17:53:14 +01:00
Holger Müller 4d4ff52c15
Merge pull request #475 from silbe/version-recursion-fix
Version: avoid infinite recursion
2022-03-11 08:40:47 +01:00
Sascha Silbe ca97287fc4 Version: avoid infinite recursion
The comparison operators (<, >, etc.) are not translated 1:1 to
customisation methods (__lt__, __gt__, ...) in Python. Instead the
type of the operands plays a role in determining on which of the two
sides the customisation method is invoked (see Python Language
Reference section 3.3.1 [1]). This means 'a > b' can end up invoking
b.__lt__(a) rather than a.__gt__(b).

This behaviour can causes infinite recursion in Version.__lt__():

2022-03-07 13:47:52,087 - NanoVNASaver.Hardware.NanoVNA_V2 - ERROR - Timeout reading version registers
Traceback (most recent call last):
  File "/home/sascha/nanovna-saver/NanoVNASaver/Controls/SerialControl.py", line 73, in serialButtonClick
    self.connect_device()
  File "/home/sascha/nanovna-saver/NanoVNASaver/Controls/SerialControl.py", line 93, in connect_device
    self.app.vna = get_VNA(self.interface)
  File "/home/sascha/nanovna-saver/NanoVNASaver/Hardware/Hardware.py", line 101, in get_VNA
    return NAME2DEVICE[iface.comment](iface)
  File "/home/sascha/nanovna-saver/NanoVNASaver/Hardware/NanoVNA_V2.py", line 76, in __init__
    super().__init__(iface)
  File "/home/sascha/nanovna-saver/NanoVNASaver/Hardware/VNA.py", line 71, in __init__
    self.read_features()
  File "/home/sascha/nanovna-saver/NanoVNASaver/Hardware/NanoVNA_V2.py", line 107, in read_features
    if self.board_revision >= Version("2.0.4"):
  File "/home/sascha/nanovna-saver/NanoVNASaver/Version.py", line 63, in __le__
    return self < other or self == other
  File "/home/sascha/nanovna-saver/NanoVNASaver/Version.py", line 57, in __lt__
    return other > self
  File "/home/sascha/nanovna-saver/NanoVNASaver/Version.py", line 57, in __lt__
    return other > self
  File "/home/sascha/nanovna-saver/NanoVNASaver/Version.py", line 57, in __lt__
    return other > self
  [Previous line repeated 491 more times]
RecursionError: maximum recursion depth exceeded in comparison

Fix it by explicitly invoking the customisation methods we expect.

[1] https://docs.python.org/3/reference/datamodel.html#object.__lt__
2022-03-09 12:06:55 +01:00
Holger Müller 982dbe26ab version in workflow as string 2022-02-18 20:33:23 +01:00
Holger Müller d596ba7661 test build with python 3.10 2022-02-18 20:31:09 +01:00
Holger Müller 8432dcfbd3 use older setuptools for pyinstaller 2022-02-18 20:16:44 +01:00
Holger Müller 884207d910 updated pyinstaller 2022-02-18 19:36:56 +01:00
Holger Müller f613ee1a5a Version Bump 2022-02-18 19:32:43 +01:00
Holger Müller 1c8477f1a9 Linting and refactoring 2022-02-18 19:31:54 +01:00
Holger Müller cb3122d632
Merge pull request #465 from galileo-pkm/PA0JOZ_devel
PA0JOZ Enhanced Response Correction
2022-02-18 18:57:49 +01:00
Holger Müller 83d011122c
Merge pull request #461 from RandMental/Development
self.sweep_control.progress_bar.setValue() needs an int value, percen…
2022-01-17 08:50:41 +01:00
Holger Müller 55d86acec1
Update NanoVNASaver/NanoVNASaver.py 2022-01-17 08:50:04 +01:00
Holger Müller e6d9b47f83
Merge pull request #466 from galileo-pkm/i458
fix for issue #458
2022-01-17 08:47:48 +01:00
Galileo 55b7c4e42c fix for issue #458 2022-01-17 01:15:25 +01:00
Galileo 915da14ac1 PA0JOZ Enhanced Response Correction 2022-01-16 21:37:20 +01:00
Randmental 209a2e326b self.sweep_control.progress_bar.setValue() needs an int value, percentage sweep is not always guaranteed to be an int 2022-01-12 16:09:15 +02:00
Holger Müller 0c179388d3 Pyinstaller and numpy version fixes for binary builds 2022-01-11 17:22:33 +01:00
Holger Müller 3e78b490e9 linux workflows just knows python 3.9 2022-01-10 16:55:50 +01:00
Holger Müller 8208563ff3 Modified deps/workflows
- setup.cfg should now work flowlessly with fedoro 36 (#456)
 - github actions are updated to use more current software versions
2022-01-10 16:49:25 +01:00
Holger Müller c9ccaffa41
Merge pull request #454 from gaionim/pull/shebang
Add shebang and make nanovna-saver.py  executable
2022-01-09 15:28:18 +01:00
Holger Müller 28fd7e5478 fix bug #455
vswr of datapoint now returns inf instead of going negative
2022-01-09 15:24:04 +01:00
Mauro Gaioni d09ab02201 Add shebang and make nanovna-saver.py executable 2022-01-05 16:19:32 +01:00
Holger Müller cbcf61afb5
Merge pull request #452 from NanoVNA-Saver/testing
Version 0.3.10
2022-01-04 08:44:03 +01:00
Holger Müller 4a620a5686 Version 0.3.10 2022-01-04 08:30:40 +01:00
Holger Müller 3b35219d75 Use pyinstaller 4.7 on windows binary build 2022-01-04 08:18:38 +01:00
Holger Müller 700781288b
Merge pull request #451 from NanoVNA-Saver/Development
Development
2022-01-04 08:14:14 +01:00
Holger Müller fdb8f0ac43 prepare merge to testing 2022-01-04 08:13:49 +01:00
Holger Müller 8cc635ffa3
Merge pull request #450 from DiSlord/Development
Calibration standarts fix and improve
2022-01-04 08:09:45 +01:00
Holger Müller c194a32eac
Merge pull request #446 from rjordans/Development
Change calculation of admittance value and unit in marker plotting code
2022-01-04 08:09:33 +01:00
DiSlord d03982af73 Fix division on zero in Open calibration standard C0 value = 0
Implement Load C calibration use
2022-01-03 19:42:17 +03:00
Roel Jordans 4bee354bf7 Change calculation of admittance value and unit in marker plotting code
Signed-off-by: Roel Jordans <r.jordans@tue.nl>
2021-12-14 11:37:10 +01:00
Holger Müller 43fd3b7d88 increased pre version 2021-10-10 14:34:02 +02:00
Holger Müller a3c9dea92f pin required pyhon lib versions and use more curren pyinstaller 2021-10-10 14:33:25 +02:00
Holger Müller 7f161478d4
Merge pull request #432 from NanoVNA-Saver/Development
use python3.9 for coverage
2021-09-21 07:56:20 +02:00
Holger Müller 1c9eeb6db3 use python3.9 for coverage 2021-09-21 07:54:49 +02:00
Holger Müller 1c28b721ba
Merge pull request #430 from NanoVNA-Saver/Development
Development
2021-09-21 07:52:18 +02:00
Holger Müller 4b705d0d88
Merge pull request #431 from rjordans/Development
Fixing test runner
2021-09-21 07:51:15 +02:00
Roel Jordans 2e2ef886e9 Fixing test runner 2021-09-20 17:35:01 +02:00
Holger Müller cced02ebff
Merge pull request #428 from rjordans/Development
Some minor fixes in plotting
2021-09-20 12:42:26 +02:00
Holger Müller 0cee90b49e
Merge pull request #423 from kzembower/Development
More cable data
2021-09-20 12:41:33 +02:00
Holger Müller f8dbb34f5b
Update NanoVNASaver/Windows/TDR.py 2021-09-20 12:39:38 +02:00
Holger Müller 8aa3b8af51
Update NanoVNASaver/Windows/TDR.py 2021-09-20 12:39:21 +02:00
Holger Müller 404329570a
Update NanoVNASaver/Windows/TDR.py 2021-09-20 12:39:01 +02:00
Holger Müller 271549db9c
Update NanoVNASaver/Windows/TDR.py 2021-09-20 12:38:32 +02:00
Holger Müller 3131893f08
Update NanoVNASaver/Windows/TDR.py 2021-09-20 12:35:21 +02:00
Roel Jordans 01c58b82ca Proper plotting of close L values in L-series measurement
Fixes #408
2021-09-14 15:44:30 +02:00
Roel Jordans 8bece254a6 Improve plotting of R+jX charts
Fixes #426
2021-09-14 15:41:11 +02:00
Roel Jordans 9ea8b7da84 Resolve rounding issue on S11 C serial plot
Fixes #424

Sets reserved space and number format consistent to other plot styles
2021-09-14 15:41:00 +02:00
Kevin Zembower f4fa649956
Added Shireen cable and cleaned previous changes.
1. Added Shireen RFC 400 RG-8/U Low loss cable (from actual measurement.
2. Cleaned previous comments and shortened too-long line.
2021-08-03 13:00:43 -04:00
Kevin Zembower eaf64cd575
Delete TDR.py
Uploaded in wrong directory. Sorry.
2021-08-03 12:56:40 -04:00
Kevin Zembower 6a5b662b05
Added Shireen cable, cleaned previous changes.
1. Added the Shireen RFC 400 RG-8/U Low Loss cable (from actual measurement).
2. Cleaned previous comments and adjusted line length.
2021-08-03 12:55:41 -04:00
Holger Müller ebfa3a99ad
Merge pull request #421 from NanoVNA-Saver/Development
Development
2021-07-24 18:59:20 +02:00
Holger Müller e112f25561 Updated README.md 2021-07-24 13:31:04 +02:00
Holger Müller e3c1bbae84 Updated documentation 2021-07-24 13:30:09 +02:00
Holger Müller f921914dd8 SweepWorker: use gui_error method 2021-07-24 11:42:43 +02:00
Holger Müller ca5a001356 Merge remote-tracking branch 'jamesl-dm/master' into HEAD 2021-07-24 10:58:53 +02:00
Holger Müller fbd9ef731f
Merge pull request #417 from kzembower/master
Added three types of coax from The Wireman to NanoVNASaver/Windows/TD…
2021-07-24 10:39:55 +02:00
James Limbouris 9aee9973ad Apply offset delays _after_ calibration 2021-07-20 13:50:55 +08:00
Kevin Zembower c4f583ddb8 Added three types of coax from The Wireman to NanoVNASaver/Windows/TDR.py. 2021-07-14 14:51:17 -04:00
Holger Müller 03cc490a66 Refactoring Display settings 2021-07-06 17:01:20 +02:00
Holger Müller 82d825d299 Refactored colors in charts 2021-07-06 09:25:20 +02:00
Holger Müller 4ca66532a4 Refactoring charts 2021-07-05 21:09:43 +02:00
Holger Müller 8fa67dc679 temporary fix for exception in Cap Chart 2021-07-05 18:29:08 +02:00
Holger Müller d20137c2d5 Debugging CapChart 2021-07-05 18:18:06 +02:00
Holger Müller a8144d458d Refactoring SweepWorker 2021-07-05 18:17:51 +02:00
Holger Müller 8d899e510b removed unused signal from sweepworker 2021-07-05 17:43:27 +02:00
Holger Müller 371a1a16ed Refactoring Controls 2021-07-05 17:34:47 +02:00
Holger Müller 5e46722955 Split history NanoVNASaver/Controls/MarkerControl.py to NanoVNASaver/Controls/Control.py 2021-07-05 16:36:37 +02:00
Holger Müller ecead18ff5 Split history NanoVNASaver/Controls/MarkerControl.py to NanoVNASaver/Controls/Control.py 2021-07-05 16:36:37 +02:00
Holger Müller 44e51fd4d6 Split history NanoVNASaver/Controls/MarkerControl.py to NanoVNASaver/Controls/Control.py 2021-07-05 16:36:37 +02:00
Holger Müller 09f5ac3a93 Split history NanoVNASaver/Controls/MarkerControl.py to NanoVNASaver/Controls/Control.py 2021-07-05 16:36:37 +02:00
Holger Müller ea06670f25 Refactoring Controls 2021-07-05 16:35:36 +02:00
Holger Müller 00293fe204 Refactored Serial Control Widget 2021-07-05 16:16:33 +02:00
Holger Müller 2d96aaa2f3 Split history NanoVNASaver.py to Controls/SerialControl.py 2021-07-05 14:23:20 +02:00
Holger Müller c60de7d836 Split history NanoVNASaver.py to Controls/SerialControl.py 2021-07-05 14:23:20 +02:00
Holger Müller 1ef0d4dfb9 Split history NanoVNASaver.py to Controls/SerialControl.py 2021-07-05 14:23:20 +02:00
Holger Müller 4882f29406 Split history NanoVNASaver.py to Controls/SerialControl.py 2021-07-05 14:23:20 +02:00
Holger Müller f9222a0ab5 Refactoring Charts 2021-07-05 11:37:48 +02:00
Holger Müller f95f223656 Y Axis changes in charts 2021-07-02 19:25:46 +02:00
Holger Müller 08c4a6dc2a data axis input setting parsing 2021-07-02 19:10:39 +02:00
Holger Müller f6881b0f0d add cli option for file loading at startup 2021-07-02 19:10:31 +02:00
Holger Müller dd3577509e tinySA: fixed feature set for screenshot 2021-07-02 17:56:32 +02:00
Holger Müller bcc598b15a Merge branch 'Development' into testing 2021-07-01 21:36:48 +02:00
Holger Müller feedf672e0 Linting 2021-07-01 21:36:17 +02:00
Holger Müller 12180f342f reverted some Chart changes partly fixes #414 2021-07-01 20:52:54 +02:00
Holger Müller ac1f06fd0c
Merge pull request #415 from Ho-Ro/horo-devel
Format shrinking and better -H detection
2021-07-01 19:49:19 +02:00
Martin 264f8d16ca improve detection of NanoVNA-H devices
some devices respond to CR with two lines "?" followed by "ch> "

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2021-07-01 18:50:37 +02:00
Martin 0c8d636632 shrink the three marker value fields to get rid of Vscroll bar
Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2021-07-01 18:47:33 +02:00
Holger Müller 6f6255bf05 Merge branch 'Development' of github.com:NanoVNA-Saver/nanovna-saver into Development 2021-06-30 07:22:29 +02:00
Holger Müller 52d0068571 Update copyright notice 2021-06-30 07:21:14 +02:00
Holger Müller e34c89aded
Merge pull request #413 from Ho-Ro/devel-layout
More compact layout window layout
2021-06-30 07:17:12 +02:00
Martin 6f1db232b5 Merge branch 'Development' into devel-layout to be up-to-date
Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2021-06-29 21:46:04 +02:00
Martin 4429064aa4 even more layout compactness for sub windows
arrange buttons groups in rows to get less height

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2021-06-29 21:42:31 +02:00
Holger Müller 827be56a08 use python3.9 for windows build pipeline 2021-06-29 20:31:16 +02:00
Holger Müller 43fef9816f Merge branch 'Development' into testing 2021-06-29 20:23:36 +02:00
Holger Müller e2460ff9e0 Updated Changelog and Readme 2021-06-29 20:21:20 +02:00
Holger Müller b86ddb95aa Show VNA type in port selector 2021-06-29 20:16:06 +02:00
Holger Müller 3dfd9a5a4b Merge branch 'Development' into testing 2021-06-29 15:40:28 +02:00
Holger Müller aca61a6e04 updated pipeline setting for linux and macos 2021-06-29 15:39:50 +02:00
Holger Müller 5b1666b7e9
Merge pull request #412 from NanoVNA-Saver/Development
Development
2021-06-29 09:08:05 +02:00
Holger Müller 14858ab77d modified setup.cfg. should fix #411 2021-06-29 09:06:30 +02:00
Holger Müller 33416ca684 Better error handling in VNA init
Should avoid subsequent faults like in #396
2021-06-29 08:31:37 +02:00
Holger Müller f25057973e bump pre version 2021-06-28 21:54:38 +02:00
Holger Müller f21c102115 detect tinySA
- screenshot and frequency setting works
- no usefull graph now, but s21 gain shows at least an
  inverted curve
2021-06-28 21:52:52 +02:00
Holger Müller e3cba167da
Merge pull request #410 from NanoVNA-Saver/Development
Refactoring
2021-06-27 19:30:42 +02:00
Holger Müller 4e7e9298e6 bump pre version 2021-06-27 19:30:09 +02:00
Holger Müller 80bb4a0936 Charts: removed duplicate code 2021-06-27 19:28:29 +02:00
Holger Müller 20b5334f3a use less info in setup.py 2021-06-27 13:01:09 +02:00
Holger Müller 534b71a222
Merge pull request #409 from NanoVNA-Saver/Development
Pre 01, Chart refactoring
2021-06-27 11:54:31 +02:00
Holger Müller 7698d67fd2 bump version 2021-06-27 11:53:08 +02:00
Holger Müller 3794a86c12 refactoring: removed duplicate code in charts 2021-06-27 11:45:22 +02:00
Holger Müller ae88b7ca4d Refactoring Charts 2021-06-27 10:59:07 +02:00
Holger Müller 6315bd06d6 refactored chart dimensions 2021-06-27 00:55:43 +02:00
Holger Müller 398404dcf9 linting: unused variables and imports 2021-06-27 00:34:06 +02:00
Holger Müller 8763948697 linting: unused-argument 2021-06-27 00:23:54 +02:00
Holger Müller 80418b5739 refactor settings and fixed no else after return 2021-06-27 00:16:58 +02:00
Holger Müller 91baa22a14 chart refactoring 2021-06-26 23:08:56 +02:00
Holger Müller faf983c196 added format for frequencies in charts 2021-06-26 23:08:32 +02:00
Holger Müller 4b90ef8498 added swap method to touchstone 2021-06-26 23:07:03 +02:00
Holger Müller c373fc6582
Merge pull request #407 from NanoVNA-Saver/Development
Push from Development
2021-06-25 20:24:17 +02:00
Holger Müller c4bb73d277
Update CHANGELOG.md 2021-06-25 20:23:53 +02:00
Holger Müller c6cd52afb2 order of min and max in context menu 2021-06-25 20:17:01 +02:00
Holger Müller 17b2742e2b Merge branch 'Development' of github.com:NanoVNA-Saver/nanovna-saver into Development 2021-06-25 20:14:51 +02:00
Holger Müller dd81aa875b Freq chart fixed axis settings
Fixes: #404
2021-06-25 20:13:10 +02:00
Holger Müller 5d2de92709
Merge pull request #406 from Ho-Ro/development-small-screen
smaller gui that fits completely on smaller screen, e.g.HD1280x720
2021-06-25 19:12:16 +02:00
Martin e156253025 smaller gui that fits completely on smaller screen, e.g.HD1280x720
improve usability on older laptops that are often used as lab pc

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2021-06-25 12:21:29 +02:00
Holger Müller aeed3744ba tidy up chart code a little bit 2021-06-22 23:23:39 +02:00
Holger Müller 8da0d2bfcf use Touchstone datatype for s data. 2021-06-22 22:07:36 +02:00
Holger Müller da3bb3a0b8
Merge pull request #386 from gaionim/pull/magLoopAnalysis
Pull/mag loop analysis
2021-06-22 18:58:08 +02:00
Holger Müller 74a8bf41a8 splitted out file io 2021-06-22 16:26:31 +02:00
Holger Müller 6cac48d536 splitted out files window 2021-06-22 16:07:37 +02:00
Holger Müller 7a310e5139 Split history NanoVNASaver/NanoVNASaver.py to NanoVNASaver/Windows/Files.py 2021-06-22 15:27:35 +02:00
Holger Müller ead5c32cc5 Split history NanoVNASaver/NanoVNASaver.py to NanoVNASaver/Windows/Files.py 2021-06-22 15:27:35 +02:00
Holger Müller b953b8ea7d Split history NanoVNASaver/NanoVNASaver.py to NanoVNASaver/Windows/Files.py 2021-06-22 15:27:35 +02:00
Holger Müller 9aee70a7f2 Split history NanoVNASaver/NanoVNASaver.py to NanoVNASaver/Windows/Files.py 2021-06-22 15:27:35 +02:00
Holger Müller d71f5145c4 updated changelog 2021-06-22 15:26:44 +02:00
Holger Müller fc9b0b48d1 Merge branch 'Development' of github.com:NanoVNA-Saver/nanovna-saver into Development 2021-06-22 15:25:46 +02:00
Holger Müller 9cdfe8d620
Merge pull request #402 from J72IMS/master
Add 9 and 5 cm bands
2021-06-21 20:06:54 +02:00
Holger Müller 9eea006663 Merge branch 'testing' into Development 2021-06-21 18:36:09 +02:00
Holger Müller b0a21bc164 use pyinstaller 4.3 2021-06-21 18:32:47 +02:00
Ishmael Samuel 81cfbc2137 Add 9 and 5 cm bands 2021-06-21 10:03:10 -04:00
Holger Müller e413baa482 Updated README.md for v0.3.9 2021-06-20 13:42:58 +02:00
Holger Müller 1eb3cda9ad
Merge pull request #401 from NanoVNA-Saver/testing
v0.3.9
2021-06-20 13:40:27 +02:00
Holger Müller 65d5d8b8b7 increased version 2021-06-20 13:31:51 +02:00
Holger Müller 0887e22477 Merge branch 'master' into testing 2021-06-20 13:31:00 +02:00
Holger Müller 9d1ea35448
Merge pull request #400 from NanoVNA-Saver/testing
v0.3.9
2021-06-20 13:20:22 +02:00
Holger Müller 0c352eed64 v0.3.9 2021-06-20 13:16:18 +02:00
Holger Müller 28c62b707a v0.3.9-rc01 2021-05-19 15:52:48 +02:00
Holger Müller 6be2730785
Merge pull request #384 from akinad3/update_markers
Add new s21 markers for |Z| and R+jX
2021-05-08 09:26:05 +02:00
Holger Müller c821fb209d
Merge pull request #389 from rjordans/testing
Fixing minimum axis label value in RI charts
2021-05-08 09:25:33 +02:00
Holger Müller 252d0d2869
Merge pull request #387 from silbe/nanovna-v2-tx-power
Add support for setting TX power on NanoVNA v2
2021-05-07 11:24:47 +02:00
Daniel Lingvay b3068543a9 Add new s21 markers for |Z| and R+jX 2021-05-01 12:21:57 +03:00
Roel Jordans 9e85730f14 Fixing minimum axis label value in RI charts
Resolves plotting issue as discussed in #165
2021-04-23 21:32:59 +02:00
Sascha Silbe a4f29565c4 Add support for setting TX power on NanoVNA v2
As of firmware version 1.0.2 (tag 20210214) the output power of the TX
ADF4350 can be configured via USB (register 0x42). Add support for
this setting in NanoVNASaver.

The SI5351 does not support setting the output voltage / power (only
the maximum current which controls rise / fall time rather than output
power) so we can only affect frequencies from 140MHz up. Clearly show
this in the UI so there are no surprises.

Because we can only detect this capability by talking to the VNA, the
setting is populated during connect.

*WARNING*: The automatic first sweep after connecting to the VNA will
be performed at maximum power (default). Sensitive equipment should
not be connect until after NanoVNASaver has connected to the NanoVNA
v2 and the setting has been changed.
2021-04-06 11:34:01 +02:00
Sascha Silbe dd8e86555c Hardware: Add information about maximum stop frequency for all models
The various models of VNAs supported by NanoVNASaver support very
different frequency ranges. Make the maximum stop frequency for each
model available as an attribute so we can use it later on.

The settings are based on information from:

- AVNA: http://www.janbob.com/electron/AVNA1/AVNA-UsersManual.pdf
- NanoVNA (v1):
  https://www.dropbox.com/s/0t1ms1y0uyaa90s/NanoVNA-a1-20170115.pdf?dl=1 (linked
  from https://ttrf.tk/kit/nanovna/)
- NanoVNA-F: https://github.com/flyoob/NanoVNA-F
- NanoVNA-F V2: http://www.sysjoint.com/pdf/NanoVNA-F%2520V2%2520Portable%2520Vector%2520Network%2520Analyzer%2520User%2520Guide%2520V1.0.pdf
- NanoVNA-H: https://nanovna.com/, https://item.taobao.com/item.htm?id=588815021483
- NanoVNA-H4: https://nanovna.com/
- NanoVNA V2: https://nanorfe.com/nanovna-v2.html
2021-04-06 11:25:37 +02:00
Mauro Gaioni 3200c1b445 now magloopanalysis set and run new sweep
to center minimum
2021-04-05 13:36:59 +02:00
Mauro Gaioni 503af5ff84 vswr can not be <1
sometime device send R < 0 :-(
2021-04-05 13:35:01 +02:00
Mauro Gaioni 93e704330d retry if empty values
fix divisionbyzero error
2021-04-05 13:33:18 +02:00
Mauro Gaioni 9bfbaba0da insegue, ma non aggiorna ...
grafico che sembra aggiornato solo il primo cambio di freq.
Poi velocizzare inseguimento ?!?
es dopo 5 step, aumentare incremento o passare a limiti originari.
2021-04-03 12:11:35 +02:00
Mauro Gaioni 7eadf8ba3f read frequencies for all version 2021-04-03 08:52:46 +02:00
Mauro Gaioni 2ddde80dfc simply retry 2021-04-03 08:52:46 +02:00
Holger Müller ba63ef5fc5
Merge pull request #377 from rjordans/master
Adding support for |Z| plots with logarithmic scale and some cleanup
2021-03-19 15:34:29 +01:00
Roel Jordans dbe735a677 Fancy formatting (mkM) for impedance plots (|Z| and R+jX) 2021-02-21 22:52:04 +01:00
Roel Jordans 60ea55b6cf Cleaning up code duplication for log scale plotting 2021-02-21 21:52:09 +01:00
Roel Jordans 9c7f6a80f7 Adding support for plotting |Z| on logarithmic scale 2021-02-21 21:52:00 +01:00
Holger Müller 34673a1548
Merge pull request #372 from NanoVNA-Saver/Development
New test version
2021-02-19 19:18:48 +01:00
Holger Müller 4204bad346
Merge pull request #369 from galileo-pkm/H4_401
401 sweep points
2021-02-19 19:11:40 +01:00
Holger Müller 911fb232bf
Merge pull request #376 from rjordans/master
Added measurement graphs showing impedance measured via S21
2021-02-19 19:08:28 +01:00
Roel Jordans c6878fce8f Added measurement graphs showing impedance measured via S21
Fixes #165

- Allows for both |Z| and R + jX display
- Measurements of shunt impedance (accurate for low impedance values)
  as well as series impedance (accurate for high impedance values)
2021-02-19 16:18:57 +01:00
Galileo 5fa16c65a3 401 sweep points 2021-01-08 20:53:07 +01:00
Holger Müller 79ef110c82
Merge pull request #360 from gaionim/features/readFreq
try to read initial frequencies
2020-12-31 14:10:53 +01:00
Holger Müller 320a61f838
Merge pull request #359 from gaionim/fix/352
fix #352
2020-12-31 14:08:47 +01:00
Holger Müller 90e4dc9332
Merge pull request #357 from gaionim/features/crossingZero
Additinal analysis methods added / better peak search algorythm
2020-12-31 14:04:56 +01:00
Holger Müller 45294a1048 v0.3.9-pre 2020-12-31 14:01:55 +01:00
Mauro Gaioni 8ab2709bf2 ensure sorting 2020-12-24 20:05:22 +01:00
Mauro Gaioni 69db5f14d7 show peaks 2020-12-23 20:56:16 +01:00
Mauro Gaioni 654252e5bf better compare ?!? 2020-12-23 19:46:46 +01:00
Mauro Gaioni ec08d068c2 try to read initial frequencies 2020-12-23 16:15:08 +01:00
Mauro Gaioni 06912dac55 fix typo 2020-12-23 13:27:53 +01:00
Mauro Gaioni 9c1031fa56 show diff 2020-12-22 21:28:04 +01:00
Mauro Gaioni 391b8881a8 compares even if different number of resonances 2020-12-22 15:18:52 +01:00
Mauro Gaioni 2f2c0d621a compare previus analysis 2020-12-21 20:49:27 +01:00
Mauro Gaioni 922f24357f both Simple peak an fixed peak 2020-12-21 20:48:36 +01:00
Mauro Gaioni 7feff63c5e simple fix markers 2020-12-21 20:41:15 +01:00
Mauro Gaioni db284fd57a some tolerance 2020-12-20 18:24:03 +01:00
Mauro Gaioni e95fea4ccc add some analysis 2020-12-20 15:42:57 +01:00
Mauro Gaioni d06bb32082 some fix ?!? 2020-12-20 15:41:38 +01:00
Mauro Gaioni 2052718822 Refactoring 2020-12-19 13:38:53 +01:00
Holger Müller 97c7c8f3f2
v0.3.8 2020-11-01 14:28:10 +01:00
Holger Müller 1151023176
Merge pull request #342 from NanoVNA-Saver/testing
v0.3.8
2020-11-01 14:21:31 +01:00
Holger Müller ca5c7b0a07
Merge pull request #345 from zarath/test/pyinstaller
upgrade pyinstaller
2020-10-30 09:14:52 +01:00
Holger Müller b096bb80ba upgrade pyinstaller 2020-10-30 09:00:42 +01:00
Holger Müller 3ef4f7309f use older scipy on windows release
pyinstaller seem to strugle with scipy > 1.4.x
2020-10-29 19:30:27 +01:00
Holger Müller 4a02c32bde v0.3.8-rc01 2020-10-25 16:59:21 +01:00
Holger Müller e2ea2273ab
Merge pull request #341 from NanoVNA-Saver/Development
Development
2020-10-25 16:55:35 +01:00
Holger Müller 70c448af9d bump version 2020-10-25 16:54:12 +01:00
Holger Müller 1608a9261f Use String values in Bands to edit vals > 2e31 2020-10-25 16:53:01 +01:00
Holger Müller fc18a94faf removed leftover split artifact 2020-10-25 15:48:38 +01:00
Holger Müller d379777b95 Bump Version 2020-10-10 22:14:55 +02:00
Holger Müller e4dce91db9
Merge pull request #336 from NanoVNA-Saver/testing
Testing
2020-10-10 22:07:05 +02:00
Holger Müller a24656dcaa
Merge pull request #335 from zarath/feature/scipy_latest
use latest scipy (seems too work again)
2020-10-10 21:25:34 +02:00
Holger Müller 537dc7a33f use latest scipy (seems too work again) 2020-10-10 21:24:10 +02:00
Holger Müller 7fcc3c20b3 Merge remote-tracking branch 'upstream/testing' 2020-10-10 19:17:25 +02:00
Holger Müller d54627f993 Fix #323
Start freq of V2 with S21 hack cannot go negative any longer
2020-10-03 14:01:30 +02:00
Holger Müller faf2686261
Merge pull request #333 from zarath/bugfix/#323
Fix #323
2020-10-03 13:55:49 +02:00
Holger Müller 9cb7175cf8 Fix #323
Start freq of V2 with S21 hack cannot go negative any longer
2020-10-03 13:52:51 +02:00
Holger Müller 7b8c40118a Bump Version 2020-10-01 13:51:40 +02:00
Holger Müller cd1f32e9dc Version suffix for dev branch 2020-10-01 13:42:52 +02:00
Holger Müller ed8319a66c Splitted screenshot helper method
_capture_data splitted to remove code redundancy
2020-10-01 13:41:07 +02:00
Holger Müller c4c2f2c6a8 Removed unneeded code from NanoVNA_F
Regarding to @sysjoint-tek changes, getScreenshot method is identical
to base class "NanoVNA"
2020-10-01 13:33:40 +02:00
Holger Müller b75209884b
Merge pull request #330 from sysjoint-tek/master
NanoVNA-F screenshot and new NanoVNA-F_V2 support
2020-10-01 13:17:55 +02:00
Holger Müller d1088dd931
Merge branch 'Development' into master 2020-10-01 12:22:05 +02:00
sysjoint-tek 2880ba187d
Delete Hardware.py 2020-09-27 16:55:22 +08:00
sysjoint-tek 767ccd0033
Create Hardware.py 2020-09-27 16:54:20 +08:00
sysjoint-tek eb87006927
Update and rename NanoVNASaver/Hardware/Hardware.py to NanoVNASaver/硬件/Hardware.py
Added nanovna-F_v2 recognition and screenshots
2020-09-27 16:41:15 +08:00
sysjoint-tek 5705842d43
Add files via upload
Added NanoVNA-F_V2 recognition and screenshots
2020-09-27 16:38:39 +08:00
sysjoint-tek de7d047ba2
Update NanoVNA_F.py
Remove 41 to 64 lines of code from the Nanovna-F file (this will cause the screen to be split into four blocks, each evenly distributed across the full screen??)
2020-09-27 16:35:55 +08:00
sysjoint-tek ebe517a0a9
Delete NanoVNA_F_V2.py 2020-09-27 16:31:46 +08:00
sysjoint-tek 418b0d05d8
Delete NanoVNA_F.py 2020-09-27 16:31:30 +08:00
sysjoint-tek c637522dc4
Delete Hardware.py 2020-09-27 16:29:55 +08:00
sysjoint-tek 610c86435f
Delete NanoVNASaver.py 2020-09-27 16:29:29 +08:00
sysjoint-tek c17c0a7b96
Add files via upload
1. Modification of Nanovna-F:

Remove 41 to 64 lines of code from the Nanovna-F file (this will cause the screen to be split into four blocks, each evenly distributed across the full screen??)





2. About the new nanovna-f_v2

Added nanovna-F_v2 recognition and screenshots
2020-09-27 16:20:43 +08:00
Holger Müller 467d08c87b bump version 2020-08-23 18:45:35 +02:00
Holger Müller ec2ff37f74
Merge pull request #324 from MZachmann/fix_timeouts
Change the timeout handling by increasing the timeout and try to read missing values if the full dataset
cannot read in on first try.
2020-08-23 18:43:32 +02:00
Holger Müller d8ad21dc54
Update NanoVNA_V2.py
Only warn if first read didn't get the full dataset
2020-08-23 18:41:38 +02:00
Holger Müller 13bfe102b7
Update NanoVNA_V2.py
Ensure at least 0.1 of timeout
2020-08-23 18:37:00 +02:00
Mark Zachmann 244fc41308 Change the timeout handling by increasing the timeout and
by rereading on timeout via reading more bytes
2020-08-23 08:20:16 -04:00
Holger Müller 6f0f55ab79 Persist marker/chart geometry bug #314 2020-08-14 19:03:14 +02:00
Holger Müller 6b554dca10
Merge pull request #306 from NanoVNA-Saver/testing
v0.3.7
2020-08-13 19:01:29 +02:00
Holger Müller b38e4412b2 v0.3.7 2020-08-13 18:59:14 +02:00
Holger Müller 3c801429d9 Merge branch 'Development' into testing 2020-08-13 18:58:31 +02:00
Holger Müller 1e884f7034
Merge pull request #313 from MZachmann/mac_readme
Mac readme update
2020-08-13 18:54:35 +02:00
Mark Zachmann 15c1eec357 Add some more depth to the Mac installer readme portion 2020-08-06 07:47:33 -04:00
Holger Müller af823597d9
Merge pull request #311 from NanoVNA-Saver/Development
RC07
2020-08-04 17:39:53 +02:00
Holger Müller 05b71ee8e8 RC07 - linting README.md and CHANGELOG.md 2020-08-04 17:34:34 +02:00
Holger Müller ba49f3ae31
Merge pull request #310 from Mike4U/Development
Fix for issue #172 where Serial Connect buttons don't update.
2020-08-04 09:20:24 +02:00
bicycleGuy d36ff5d3b0 Merge branch 'Development' of https://github.com/Mike4U/nanovna-saver into Development 2020-08-03 23:36:13 -07:00
Mike4U dc0467ed78
Merge pull request #3 from NanoVNA-Saver/Development
Development
2020-08-03 23:32:03 -07:00
bicycleGuy 7b17d714ea Merge branch 'Development' of https://github.com/Mike4U/nanovna-saver into Development 2020-08-03 23:13:06 -07:00
bicycleGuy 38108c1e8e Fix serial connection status button updates 2020-08-03 23:12:36 -07:00
Holger Müller 7c5e493be7 RC06 2020-08-03 17:59:19 +02:00
Holger Müller a7da9c1bfa S21 first mag value error hack 2020-08-03 17:56:43 +02:00
Holger Müller 28ea171441 increased minimal retry time 2020-08-03 17:31:00 +02:00
Holger Müller 21733f9234 Fixed unitialized variable bug in get_info retry 2020-08-03 17:30:28 +02:00
Holger Müller 0d49b7e977 Remove not yet needed class and corrected test
test was failing as rounding for multiple segmets has changed
2020-08-01 10:46:15 +02:00
Holger Müller 42fb34fb4b
Merge pull request #299 from NanoVNA-Saver/Development 2020-08-01 10:35:03 +02:00
Holger Müller a0b4762d6a RC05 2020-08-01 10:28:04 +02:00
Holger Müller 56facd3ea9 Update selection of delta marker on settings change 2020-08-01 10:13:31 +02:00
Holger Müller 87d73f8842
Splitted github release actions (#305)
* splitted github release actions
* generate 32Bit windows binaries
2020-08-01 09:45:22 +02:00
Holger Müller d8882c74bf Better rounding with multiple segments 2020-07-31 12:52:07 +02:00
Holger Müller 323db84ef6 Assume every current H4 can do 201 datapoints 2020-07-31 10:26:04 +02:00
Holger Müller 9f5f4b5870 Use scan mask command for read_frequencies if possible 2020-07-31 10:19:29 +02:00
Holger Müller ecd74d54db Raise Error if no sweep attemp gets valid data. 2020-07-30 20:32:01 +02:00
Holger Müller a713959245 RC-01 2020-07-30 09:32:04 +02:00
Holger Müller 7205aba13d Removed old reference to a no mored existing button 2020-07-29 17:36:37 +02:00
Holger Müller 2176919ac0 fix empty min dataset 2020-07-29 15:55:13 +02:00
Holger Müller 0559540e30 Fixed -H4 starup crash 2020-07-29 13:04:42 +02:00
Holger Müller 93678a0293 Distribute initial markers evenly 2020-07-28 22:08:15 +02:00
Holger Müller 80f5cadd22 action again 2020-07-28 21:33:57 +02:00
Holger Müller c981069a16 onefile again 2020-07-28 21:23:16 +02:00
Holger Müller c4eea708e2 pyinstaller --onefile 2020-07-28 21:14:24 +02:00
Holger Müller 68eeabc4f1 NanoVNASaver.py: Use min and max functions 2020-07-28 21:04:43 +02:00
Holger Müller 569fcdb2e6 Do not validate inut by default.
As it crashes often with uncalibrated devices on connect
2020-07-28 17:34:16 +02:00
Holger Müller 89c809cf33 fixed truncating 2020-07-28 17:31:14 +02:00
Holger Müller f30f4f02e5 Explanation for logarithmic sweep 2020-07-28 17:22:50 +02:00
Holger Müller e93d17dce3 SweepControl refactoring 2020-07-28 17:07:53 +02:00
Holger Müller b178301435 Sweep code updates 2020-07-28 10:34:31 +02:00
Holger Müller 06b379afb4 Use Sweep Class to communicate with SweepWorker 2020-07-27 20:01:08 +02:00
Holger Müller fd6aad74cf Set sweep modes via sweep class 2020-07-27 18:36:09 +02:00
Holger Müller ec643ebefc Bump version 2020-07-27 17:07:55 +02:00
Holger Müller 0f5a24ed49 Test corrected 2020-07-27 17:06:46 +02:00
Holger Müller d79524bb12 Marker input editing signal change 2020-07-27 17:05:11 +02:00
Holger Müller 626b474bb6 V2: retry if bulk read has not all data 2020-07-27 16:29:59 +02:00
Holger Müller 4f5ad6eeca Fixed crash with multiple segments 2020-07-27 16:28:50 +02:00
Holger Müller 54326fb86d Version Bump 2020-07-26 12:04:58 +02:00
Holger Müller 28b424125a
Merge pull request #282 from zarath/development
Development
2020-07-26 12:03:58 +02:00
Holger Müller 0c217541e0 More test coverage 2020-07-26 11:52:02 +02:00
Holger Müller 6d74ae52a3 Phase of complex cannot be > 2 * pi 2020-07-26 11:47:12 +02:00
Holger Müller 3bfd99ad3d Increased test coverage 2020-07-25 21:34:49 +02:00
Holger Müller e6d3ea0c12 Added Wavelength to marker fields 2020-07-25 20:35:16 +02:00
Holger Müller 684a01beb4 Refactoring Sweep settings 2020-07-25 20:10:24 +02:00
Holger Müller bf8d5a4544 Split history NanoVNASaver/SweepWorker.py to NanoVNASaver/Settings/Sweep.py 2020-07-25 19:43:51 +02:00
Holger Müller de7ec1be7d Split history NanoVNASaver/SweepWorker.py to NanoVNASaver/Settings/Sweep.py 2020-07-25 19:43:51 +02:00
Holger Müller ae75966bb3 Split history NanoVNASaver/SweepWorker.py to NanoVNASaver/Settings/Sweep.py 2020-07-25 19:43:51 +02:00
Holger Müller 9a53c9155b Split history NanoVNASaver/SweepWorker.py to NanoVNASaver/Settings/Sweep.py 2020-07-25 19:43:51 +02:00
Holger Müller 87d8fb504d Split history NanoVNASaver/Settings/Bands.py to NanoVNASaver/Settings/Version.py 2020-07-25 19:41:20 +02:00
Holger Müller 81751b3733 Split history NanoVNASaver/Settings/Bands.py to NanoVNASaver/Settings/Version.py 2020-07-25 19:41:20 +02:00
Holger Müller 2a66d00927 Split history NanoVNASaver/Settings/Bands.py to NanoVNASaver/Settings/Version.py 2020-07-25 19:41:20 +02:00
Holger Müller ea1de63c52 Split history NanoVNASaver/Settings/Bands.py to NanoVNASaver/Settings/Version.py 2020-07-25 19:41:20 +02:00
Holger Müller 29b8ed6760 Split history NanoVNASaver/Settings/Bands.py to NanoVNASaver/Settings/Version.py 2020-07-25 19:39:15 +02:00
Holger Müller a8065c1118 Split history NanoVNASaver/Settings/Bands.py to NanoVNASaver/Settings/Version.py 2020-07-25 19:39:15 +02:00
Holger Müller 3d665a4724 Split history NanoVNASaver/Settings/Bands.py to NanoVNASaver/Settings/Version.py 2020-07-25 19:39:15 +02:00
Holger Müller 6aa5fce09e Split history NanoVNASaver/Settings/Bands.py to NanoVNASaver/Settings/Version.py 2020-07-25 19:39:15 +02:00
Holger Müller a1c194d03c Split history to 2020-07-25 19:37:49 +02:00
Holger Müller 6c4fbe6c8d Merge branch 'development' 2020-07-25 16:39:38 +02:00
Holger Müller e48870e13b updated README.md (pre01) 2020-07-24 22:09:47 +02:00
Holger Müller cadf499a6f
Merge pull request #273 from zarath/feature/delta_marker
Feature/delta marker
2020-07-24 22:02:25 +02:00
Holger Müller 8ca1606d5f Prepare for logarithmic sweeping 2020-07-24 21:14:48 +02:00
Holger Müller b03ba0adf0 added reconnect on sweep error 2020-07-24 21:11:34 +02:00
Holger Müller f05a2c40a5 Fixed Version less than comparison 2020-07-24 21:09:52 +02:00
Holger Müller 0ba80d092e Merge branch 'development' into feature/delta_marker 2020-07-24 12:59:26 +02:00
Holger Müller 37c56a9692 Fill marker labels after first sweep 2020-07-24 12:18:31 +02:00
Holger Müller 32004a8f99 markerUpdated only updates updated marker now 2020-07-24 12:17:03 +02:00
Holger Müller ec5c0b5743
Merge pull request #280 from peterbmarks/master
Fix typo in git clone for macOS
2020-07-24 11:52:27 +02:00
Peter B Marks b6b763cda1
Fix typo in git clone for macOS
Small error in home-brew installation instructions.
2020-07-24 19:47:26 +10:00
Holger Müller 3aa968ed47 fixed coverage checks 2020-07-23 16:07:44 +02:00
Holger Müller c05268a5b4 Make marker column resizable 2020-07-23 15:27:53 +02:00
Holger Müller 8b6ae07ea5 Modified layout to get room for more markers 2020-07-22 13:37:06 +02:00
Holger Müller 3578210a81 Merge branch 'development' into feature/delta_marker 2020-07-20 20:58:13 +02:00
Holger Müller 30f7b868db
Merge pull request #264 from zarath/development
Case-insensitive entry of frequency units
2020-07-20 20:52:47 +02:00
Holger Müller ce717bad58 Fixed segments rename / allow some more retries 2020-07-20 20:50:52 +02:00
Holger Müller 8cc6431bc0 Merge branch 'upstream_Development' into development 2020-07-20 11:12:16 +02:00
Holger Müller 635c692e93 Added Delta Marker 2020-07-19 16:38:03 +02:00
Holger Müller 0c59600f4a Added allow_negative to some formatting functions 2020-07-19 16:37:18 +02:00
Holger Müller 69d88139ed bugfix: Display settings data -> data11 rename 2020-07-18 15:59:01 +02:00
Holger Müller 66b07690ff
Fixed typo segements 2020-07-18 15:43:39 +02:00
Holger Müller 6052b09687 Refactored MarkerControl 2020-07-17 21:57:03 +02:00
Holger Müller 74a99b758e bugfix: Display settings data -> data11 rename 2020-07-17 21:56:35 +02:00
Holger Müller 550b0a2937 Split history NanoVNASaver/NanoVNASaver.py to NanoVNASaver/Widgets/MarkerControl.py 2020-07-17 21:12:45 +02:00
Holger Müller 1a6dbe0d1f Split history NanoVNASaver/NanoVNASaver.py to NanoVNASaver/Widgets/MarkerControl.py 2020-07-17 21:12:45 +02:00
Holger Müller 3505552dfa Split history NanoVNASaver/NanoVNASaver.py to NanoVNASaver/Widgets/MarkerControl.py 2020-07-17 21:12:45 +02:00
Holger Müller 820ec3f82f Split history NanoVNASaver/NanoVNASaver.py to NanoVNASaver/Widgets/MarkerControl.py 2020-07-17 21:12:45 +02:00
Holger Müller 5f4fa53223 Renamed count to segments for consistency 2020-07-17 20:45:21 +02:00
Holger Müller 202f1eaf10 Bump version 2020-07-17 20:45:21 +02:00
Holger Müller b7dc4a66af Enhancement: #147
Case-insensitive entry of frequency units
2020-07-17 20:45:21 +02:00
Holger Müller 695485bbff Bump version 2020-07-17 20:15:38 +02:00
Holger Müller 85e1374d0d
Merge pull request #261 from NanoVNA-Saver/Development
v0.3.6
2020-07-17 11:47:35 +02:00
Holger Müller 44cc56a61f Merge branch 'development' 2020-07-17 11:24:17 +02:00
Holger Müller 4d4c65a230 Bump version to v0.3.6 2020-07-17 09:05:01 +02:00
Mike4U e059136b02
Merge pull request #2 from NanoVNA-Saver/Development
Development
2020-07-16 00:00:15 -07:00
Holger Müller 6a195fa90f
Merge pull request #256 from zarath/development
v0.3.6-rc2
2020-07-15 19:42:33 +02:00
Holger Müller 97f5083130 more -H/-H4 tests 2020-07-15 17:13:37 +02:00
Holger Müller d287f6cddf
Merge pull request #254 from zarath/development
v0.3.6-rc1
2020-07-15 14:57:18 +02:00
Holger Müller 675512d1b7 v0.3.6-rc1 2020-07-15 14:53:43 +02:00
Holger Müller 9d6b9485ca
Merge pull request #253 from zarath/development
Implemented dislord bandwidth method
2020-07-15 14:52:40 +02:00
Holger Müller 0fb7cd6768 Implemented dislord bandwidth method 2020-07-15 14:47:06 +02:00
Holger Müller 785ef32e76
Merge pull request #246 from zarath/development
v0.3.6-rc
2020-07-15 08:17:39 +02:00
Holger Müller 4ef83a44f9 No VNA instance for info reading
this was needed as -H variants don't responds the same way classical do
2020-07-14 14:52:19 +02:00
Holger Müller 8ac23b0872 added __init__.py for Widgets modules 2020-07-14 13:30:43 +02:00
Holger Müller 098bd26346 bugfix: copy reference data 2020-07-14 12:00:03 +02:00
Holger Müller 23a7d5b553 Updated README.md 2020-07-14 11:38:13 +02:00
Holger Müller 947e4f26cc Updated CHANGELOG.md 2020-07-14 11:36:10 +02:00
Holger Müller 72aef86d4c Bumped version 2020-07-14 11:33:23 +02:00
Holger Müller b4ba24d42d Implemented bandwidth selection 2020-07-14 11:32:52 +02:00
Holger Müller 19493012b2 Read initial frequencies from NanoVNA
On first run get frequencies from VNA even with scan mask
2020-07-14 10:02:41 +02:00
Holger Müller f3c7e71c1a SweepControl widget enhancements 2020-07-14 10:01:36 +02:00
Holger Müller a5a6287a4d Input value validation defaul set to false
As it just crashes the program when scan values out of the expected
range I think it's more sane to ignore it atm.
2020-07-14 10:00:37 +02:00
Holger Müller 1a78c66a4c refactoring fixes 2020-07-13 17:41:27 +02:00
Holger Müller 6d38474f49 sweep refactor fix in calibration 2020-07-13 16:54:06 +02:00
Holger Müller d8ac822963 Increased retry count 2020-07-13 14:20:23 +02:00
Holger Müller fa4edfff56 Refresh markers to Input values after sweep 2020-07-13 13:49:23 +02:00
Holger Müller 98ab43aead refactorung issue: fix sweep_start in callibration 2020-07-13 13:19:21 +02:00
Holger Müller 22c4a320e6 Better retry logging in serial communication 2020-07-13 00:09:00 +02:00
Holger Müller 9d11730798 Added Widget module __init__.py 2020-07-12 23:52:29 +02:00
Holger Müller 099e1b2087 Refactored SweepControl Widget 2020-07-12 23:49:01 +02:00
Holger Müller 66345c29d0 Split history NanoVNASaver/NanoVNASaver.py to NanoVNASaver/SweepSettings.py 2020-07-12 21:49:22 +02:00
Holger Müller d7ec8dbf88 Split history NanoVNASaver/NanoVNASaver.py to NanoVNASaver/SweepSettings.py 2020-07-12 21:49:22 +02:00
Holger Müller 5c684e9886 Split history NanoVNASaver/NanoVNASaver.py to NanoVNASaver/SweepSettings.py 2020-07-12 21:49:22 +02:00
Holger Müller a174c72f8a Split history NanoVNASaver/NanoVNASaver.py to NanoVNASaver/SweepSettings.py 2020-07-12 21:49:22 +02:00
Holger Müller 9a73e8ba81 Tune timeouts in serial 2020-07-12 21:33:19 +02:00
Holger Müller 980fcf30ae
Merge pull request #241 from zarath/development
v0.3.6-pre
2020-07-11 17:23:31 +02:00
Holger Müller 2244f57779 scan mask if dislord firmware 2020-07-11 16:51:41 +02:00
Holger Müller b28b010903 Fixed scan mask with nanavna 2020-07-11 15:58:30 +02:00
Holger Müller bf18d88bf7
Merge pull request #240 from zarath/development
record runtime exceptions in debug log
2020-07-11 14:01:50 +02:00
Holger Müller 11082ff062 record runtime exceptions in debug log 2020-07-11 14:00:05 +02:00
Holger Müller b1980b3bfd
Merge pull request #236 from zarath/development
Fixed Bug in Firmware variant detection
2020-07-10 17:13:49 +02:00
Holger Müller 8d8d350068 Fixed Bug in Firmware variant detection 2020-07-10 17:11:36 +02:00
Holger Müller 1a88ef4791
Merge pull request #39 from zarath/development
Development
2020-07-10 13:45:47 +02:00
Holger Müller 95a3f72a98 Updated README.md 2020-07-10 13:25:57 +02:00
Holger Müller bef52233e1
Merge pull request #234 from zarath/development
Release v0.3.5
2020-07-10 13:23:22 +02:00
Holger Müller 50b5c9e105 assume -H4 has screenshot capability 2020-07-10 12:59:41 +02:00
Holger Müller 58b41d7bba Updated CHANGELOG and About 2020-07-10 12:14:26 +02:00
Holger Müller 717de3fb6d v0.3.5-rc 2020-07-10 11:35:50 +02:00
Holger Müller 8f95590669
Merge pull request #38 from zarath/development
0.3.5
2020-07-10 11:17:45 +02:00
Holger Müller d4621aad13 Refactored sweep worker
Data now initialized when sweep settings are done.
Thus no range risizing of charts in multi segment mode
2020-07-10 11:10:40 +02:00
Holger Müller 8a8436dfc1 added gui_error method to SweepWorker 2020-07-09 13:02:04 +02:00
Holger Müller 0920761e9f Refactored SweepWorker
should fix marker problem #230
2020-07-09 12:46:26 +02:00
Holger Müller e69349814b Added pause after each write in NanoVNA_V2 2020-07-09 10:52:05 +02:00
Holger Müller 9ba5f48a96 Refactored data -> data11 2020-07-09 10:45:43 +02:00
Holger Müller 430a970563 Only assume DiSlord firmware has 201 Datapoints 2020-07-08 17:39:35 +02:00
Holger Müller b6f36dc4a8 no need for excessive logging of drain_serial 2020-07-08 17:30:53 +02:00
Holger Müller 8bb5a9821b Assume -H4 has always 201 point sweep capability 2020-07-08 17:29:41 +02:00
Holger Müller 8fbe360778 Added properties to Version class 2020-07-08 17:14:16 +02:00
Holger Müller b8b68d46a3
Merge pull request #232 from gaionim/pull/cleanupMagloop
MagLoop Analysis from @gaionim
2020-07-08 15:51:24 +02:00
Holger Müller b365a6cbcd
Merge branch 'Development' into pull/cleanupMagloop 2020-07-08 15:50:28 +02:00
Holger Müller a686a20fad
Merge pull request #231 from zarath/development
Rely on SciPy for interpolation
2020-07-08 15:48:34 +02:00
Holger Müller 51af621433 Fixed version comparison glitches 2020-07-08 15:47:07 +02:00
Holger Müller a6c3a20445 readded MagLoopAnalysis 2020-07-08 15:47:07 +02:00
Mauro Gaioni f67e00ca2d cleanup and comments 2020-07-08 15:47:07 +02:00
Mauro Gaioni 4930519fd8 cleanup and comments 2020-07-07 21:43:24 +02:00
Holger Müller 5b71657e17 removed left free file 2020-07-07 20:38:40 +02:00
Holger Müller 675e7adc2c Use with for locking / linting 2020-07-07 20:14:10 +02:00
Holger Müller 6fa8929f94 fix crash when trying to connect empty interface 2020-07-07 16:56:09 +02:00
Holger Müller 55dd5eda4f fixed typo 2020-07-07 16:50:59 +02:00
Holger Müller 28fb2e280f Removed redundant code from hardware 2020-07-07 16:48:43 +02:00
Holger Müller 27bd5ca2b6 Disabled currently broken MagLoopAnalysis 2020-07-07 14:17:32 +02:00
Holger Müller 27d3f492d8 add interpolation for touchstone data 2020-07-07 14:09:58 +02:00
Holger Müller 948c04f154 Fix custom datapoints on H4 2020-07-07 12:33:32 +02:00
Holger Müller 496494900f
Merge pull request #226 from zarath/feature/refactor_hardware
This patchset refactors many hardware related functions.
It can break things especially with AVNA/NanoVNA-F/-H/-H4. As I don't have access to that hardware devices I need some feedback, if general functionality is given.

I personally was impressed, how much faster sweeping is with the original NanoVNA
2020-07-05 14:08:24 +02:00
Holger Müller 71ba759ada Fixed percentage in continous sweep 2020-07-05 14:06:57 +02:00
Holger Müller 113be4d1db Removed code doubling in sweep worker 2020-07-05 13:51:01 +02:00
Holger Müller cd9b4da1af Added connected() method to VNA class 2020-07-05 13:00:03 +02:00
Holger Müller 764a9aaa0b More robust scanning in NanoVNA.py 2020-07-05 12:32:20 +02:00
Holger Müller ac050a0a4c Fix merge to wrong line in NanoVNASaver.py 2020-07-04 23:43:40 +02:00
Holger Müller d039a1192a Merge branch 'development' into feature/refactor_hardware 2020-07-04 23:38:12 +02:00
Holger Müller 887a74d040
Merge pull request #35 from RandMental/patch-3
Update NanoVNASaver.py
2020-07-04 23:36:07 +02:00
Holger Müller 3d57461a71
Merge pull request #36 from RandMental/patch-2
Update Hardware.py
2020-07-04 23:35:33 +02:00
Holger Müller 399c893f4f
Merge pull request #37 from zarath/development
High speed scanning with NanoVNA >= 0.7.1
2020-07-04 23:34:36 +02:00
Holger Müller d3fe370d80 High speed scanning with mask for NanoVNA > 0.7.1 2020-07-04 23:30:14 +02:00
Holger Müller 089455505b Ensure that vna.readFrequencies always list of int 2020-07-04 20:37:37 +02:00
Holger Müller 0b8641c20a Device connection refactoring 2020-07-04 20:14:18 +02:00
Holger Müller 8b6ce5e142 Removed unneeded methods 2020-07-04 19:59:47 +02:00
Holger Müller 5e32ff177b Use exceptions instead of if in About.py 2020-07-04 19:58:20 +02:00
RandMental 22b5116573
Update NanoVNASaver.py
Format Serial Control Box
2020-07-04 16:55:52 +02:00
RandMental 428aafa6ce
Update Hardware.py
Spelling mistake
2020-07-04 16:51:11 +02:00
Holger Müller b8b2a78ab4
Merge pull request #34 from RandMental/patch-1
Formatting to increase readability
2020-07-04 14:44:44 +02:00
Holger Müller 67334257a1 Bump version 2020-07-04 14:32:45 +02:00
Holger Müller 408eda63cf
Merge pull request #225 from NanoVNA-Saver/Development
v0.3.4
2020-07-04 14:29:00 +02:00
Holger Müller a06b9191be Get rid of InvalidVNA 2020-07-04 14:26:20 +02:00
Holger Müller 3dbbd165ac Merge branch 'feature/refactor_hardware' of github.com:zarath/nanovna-saver into feature/refactor_hardware 2020-07-04 13:23:07 +02:00
Holger Müller 88245d640d
Merge pull request #33 from RandMental/patch-1
Formatting to increase readability - windows\About.py
2020-07-04 13:22:53 +02:00
RandMental 64a7fd914d
Formatting to increase readability
Copyright notices
Firmware version
2020-07-04 13:16:49 +02:00
Holger Müller 8e7e6b81ac Moved duplicate code to parent class 2020-07-04 13:10:29 +02:00
Holger Müller 935b58648e Refactored Hardware
- App opject no longer neened in VNA classes
- serial port locking moved in Interface class
2020-07-04 12:08:54 +02:00
Holger Müller e743a4a5a3 Fix 201 datapoint setting with -H4 2020-07-04 09:28:00 +02:00
Holger Müller 405f48166d
Merge pull request #223 from zarath/development
v0.3.4
2020-07-04 09:05:03 +02:00
Holger Müller 9ec71ebc8d Bump Version 2020-07-03 19:16:45 +02:00
Holger Müller da3bcbba6b
Merge pull request #31 from zarath/development
Development
2020-07-03 19:00:41 +02:00
Holger Müller 6941d237c4 Updated README.md / CHANGELOG.md 2020-07-03 18:52:59 +02:00
Holger Müller 143e1dda02 detect -H versions 2020-07-03 18:43:39 +02:00
Holger Müller 09a96ef45d
Merge pull request #221 from MZachmann/fixup_noserial
repair crash with no device attached
2020-07-03 15:57:44 +02:00
Mark Zachmann 94421b80a8 add checking for None in serial port for button click
repair the typo in AntennaAnalysis import
2020-07-03 07:27:22 -04:00
Holger Müller 2252d78de4
Merge pull request #212 from zarath/development
Bugfixes and simplifications
2020-07-03 09:22:26 +02:00
Holger Müller 525cb75b72
Merge pull request #218 from RandMental/Development
Updated README.md
2020-07-03 09:21:50 +02:00
RandMental 99375693c8
Update README.md 2020-07-02 17:40:41 +02:00
RandMental 4935b4a7ae
Update README.md 2020-07-02 17:37:45 +02:00
Holger Müller 354a3d764e fixed typo with drain_serial function call 2020-07-02 12:00:54 +02:00
Holger Müller 7cb71c0ab0
Merge pull request #213 from gaionim/pull/analizeMagLoop
refactoring Vswr Analysis
2020-07-01 21:19:36 +02:00
Mauro Gaioni cec57bacb5 MagLoopAnalysis 2020-07-01 20:47:15 +02:00
Holger Müller 2e62d13f0f fixed s21 save bug #211 2020-07-01 20:26:13 +02:00
Holger Müller 24b34d6f70 Simplified Serial port locking 2020-07-01 20:11:12 +02:00
Holger Müller eae377bbe2 Clean up calculations 2020-07-01 16:52:30 +02:00
Holger Müller 1f193d4b00 Merge branch 'bugfix/through-calibration' into development 2020-07-01 08:32:46 +02:00
Holger Müller 3d17260220
Merge pull request #209 from zarath/bugfix/through-calibration
Fixed through calibration
2020-07-01 07:46:10 +02:00
Holger Müller da729a3bab Bumped Version 2020-07-01 07:45:18 +02:00
Mauro Gaioni 25df257462 fix import error 2020-06-30 21:51:33 +02:00
Holger Müller 22be293ff2
Merge pull request #32 from zarath/bugfix/through-calibration
Fixed through calibration
2020-06-30 21:51:03 +02:00
Holger Müller 4819b25ac1 Fixed through calibration 2020-06-30 21:46:47 +02:00
Holger Müller 61f3c652ac
Merge pull request #207 from NanoVNA-Saver/Development
Development
2020-06-30 13:29:56 +02:00
Holger Müller da5e5ba733
Merge pull request #205 from zarath/development
Development
2020-06-30 13:19:02 +02:00
Holger Müller fa50984822
Merge pull request #30 from zarath/feature/v0.3.3
Version bump / CHANGELOG
2020-06-30 12:51:42 +02:00
Holger Müller fa19aaab37 Version bump / CHANGELOG 2020-06-30 12:23:23 +02:00
Holger Müller 907f417a3e
Merge pull request #29 from zarath/development
Refactored Calibration
Fixed V2 data acquisition
2020-06-30 12:14:20 +02:00
Holger Müller 3341564cb2 Refactored Calibration
Utilize scipy to implement spline interpolation between calibration
data points
2020-06-30 12:04:50 +02:00
Holger Müller 4045a18271 removed unneeded import 2020-06-30 12:01:03 +02:00
Holger Müller 30032a08fe FIxed V2 data acquivsion 2020-06-30 12:00:20 +02:00
Holger Müller 40f9e25944
Merge pull request #201 from zarath/development
Fixed cal crash and s21 cals
2020-06-30 10:19:48 +02:00
Holger Müller 374924299d Fix crash when marker outside new scan range 2020-06-29 18:55:00 +02:00
Holger Müller e70aa7d3e1 Fixed cal crash and s21 cals 2020-06-29 18:46:15 +02:00
Holger Müller 945b28ffac
Merge pull request #199 from zarath/master
Linting and tests
2020-06-29 09:39:02 +02:00
Holger Müller 35353a34de
Merge pull request #28 from zarath/development
Development
2020-06-28 19:02:47 +02:00
Holger Müller 75d77c1703 Refactoring NanoNVASaver window methods 2020-06-28 18:48:41 +02:00
Holger Müller 48f8aaea32 put charts in dictionary 2020-06-28 17:37:23 +02:00
Holger Müller fbdf325b51 Test for cor_att_data function 2020-06-28 16:57:52 +02:00
Holger Müller c0e1cfb310 reordered RFTools functions alphabetically
added some test cases
2020-06-28 15:43:47 +02:00
Holger Müller 5dd8c1d20d
Update README.md
Binary link fixed
2020-06-28 13:39:03 +02:00
Holger Müller 9cea9d05a1
Merge pull request #198 from NanoVNA-Saver/Development
v0.3.2
2020-06-28 13:23:21 +02:00
Holger Müller 148e9a13d4
Merge pull request #197 from zarath/master
v0.3.2
2020-06-28 13:21:29 +02:00
Holger Müller 398b1ac882
Merge pull request #27 from zarath/development
Updated CHANGELOG.md
2020-06-28 13:08:17 +02:00
Holger Müller 6cb7b33aa3 Updated CHANGELOG.md 2020-06-28 13:07:14 +02:00
Holger Müller 726dcf6436
Merge pull request #26 from zarath/development
Development
2020-06-28 13:02:52 +02:00
Holger Müller ee4d3b6765 Version bump 2020-06-28 12:57:16 +02:00
Holger Müller 790c8aac2b Linting SweepWorker 2020-06-28 12:55:07 +02:00
Holger Müller c139a531e7 Fixed average truncation
Also use geometrical distance to sort out extrema
2020-06-28 11:43:02 +02:00
Holger Müller 3742d24364
Merge pull request #196 from zarath/upstream_Development
Updated README.md
2020-06-28 09:45:01 +02:00
Holger Müller 9821fcb850 Updated README.md 2020-06-28 09:43:32 +02:00
Holger Müller f347b12538 Updated README.md 2020-06-28 09:40:23 +02:00
Holger Müller 4c04521d79
Merge pull request #171 from ra1nb0w/macports
add macports installation instruction
2020-06-28 09:29:07 +02:00
Holger Müller f05b7636d0
Merge pull request #193 from RandMental/master
Update README.md
2020-06-27 22:21:22 +02:00
Holger Müller b33034acff
Merge pull request #195 from NanoVNA-Saver/Development
v0.3.1 fixes assited calibration crash
2020-06-27 21:58:41 +02:00
Holger Müller 11e5d73559
Merge pull request #194 from zarath/master
Fix cal crash
2020-06-27 21:56:52 +02:00
Holger Müller 76420abd57 Updated version and changelog 2020-06-27 21:48:28 +02:00
Holger Müller 3dca605297 bump version 2020-06-27 21:37:55 +02:00
Holger Müller c6da3c3364
Merge pull request #25 from zarath/development
fixed calibration assitant save crash
2020-06-27 21:33:22 +02:00
Randmental 974610bdc3 Update README.md 2020-06-27 21:28:45 +02:00
Holger Müller 0a2bf51c63 fixed calibration assitant save crash 2020-06-27 21:27:52 +02:00
Holger Müller c77d19780b enable all os targets again 2020-06-27 16:16:54 +02:00
Holger Müller 4a8195e189 upgrade setuptools befor using PyInstaller 2020-06-27 16:09:36 +02:00
Holger Müller 73f968a47b Try build with python3.8 2020-06-27 15:47:41 +02:00
Holger Müller 298b5134bf
Merge pull request #24 from zarath/development
Development
2020-06-27 14:54:59 +02:00
Holger Müller 88c7ee35a7 Merge tag 'v0.3.0' into development 2020-06-27 14:50:34 +02:00
Holger Müller 8bdf5ecb77 NanoVNA-H4 now also supports 202 datapoint/scan 2020-06-27 14:49:21 +02:00
Holger Müller 718e894efc PyInstaller fixes (need older scipy) 2020-06-27 14:47:31 +02:00
Holger Müller bf97feff1a
Merge pull request #191 from zarath/feature/prep_for_v0.3.0
Feature/prep for v0.3.0
2020-06-25 22:03:40 +02:00
Holger Müller f4bd371e51
Merge pull request #23 from zarath/development
Development
2020-06-25 22:01:48 +02:00
Holger Müller 20a017b1ed
Merge pull request #22 from zarath/feature/prep_for_v0.3.0
Feature/prep for v0.3.0
2020-06-25 20:18:29 +02:00
Holger Müller ca146a2e3a Updated readme 2020-06-25 20:09:36 +02:00
Holger Müller 9e629f0350 Updated copyright headers 2020-06-25 19:52:30 +02:00
Holger Müller d6acb7121c Prepartion for new github orginisational location 2020-06-25 19:48:03 +02:00
Holger Müller 5ec8c1e1c0
Merge pull request #188 from NanoVNA-Saver/zarath-patch-1
Updated README.md
2020-06-25 12:01:18 +02:00
Holger Müller 3c77a86b58 fixed button connection in calibration 2020-06-24 23:54:59 +02:00
Holger Müller cb568cf6ad Linting 2020-06-24 23:10:35 +02:00
Holger Müller 658cc6f231 Removed no longer needed properties 2020-06-24 22:49:24 +02:00
Holger Müller aa9f1accc9
Merge pull request #21 from zarath/feature/refactor_calibration
Refactored Calibration Code
2020-06-24 22:14:27 +02:00
Holger Müller 5ce5de163c Refactored Calibration Code 2020-06-24 22:09:19 +02:00
Ohan Smit 384818db27
corrected 2020-06-21 09:44:01 +02:00
Holger Müller 58c83fa26b
Updated README.md
Updated install instructions for Ubuntu 18
2020-06-20 12:20:25 +02:00
bicycleGuy e113da4729 Revert "Update NanoVNASaver.py"
This reverts commit 8c7506daec.
2020-04-13 20:14:16 -07:00
Davide Gerhard e9221af112
add macports installation instruction 2020-04-06 11:39:14 +02:00
bicycleGuy 8c7506daec Update NanoVNASaver.py 2020-02-14 16:10:39 -08:00
238 zmienionych plików z 28828 dodań i 18452 usunięć

Wyświetl plik

@ -1,22 +1,9 @@
# .coveragerc to control coverage.py
[run]
# ignore GUI code atm.
omit =
NanoVNASaver/Analysis/*.py
NanoVNASaver/Calibration.py
NanoVNASaver/Charts/*.py
NanoVNASaver/Hardware/*.py
NanoVNASaver/Inputs.py
NanoVNASaver/Marker/Settings.py
NanoVNASaver/Marker/Values.py
NanoVNASaver/Marker/Widget.py
NanoVNASaver/Marker/__init__.py
NanoVNASaver/NanoVNASaver.py
NanoVNASaver/Settings.py
NanoVNASaver/SweepWorker.py
NanoVNASaver/Windows/*.py
NanoVNASaver/__init__.py
NanoVNASaver/__main__.py
NanoVNASaver/about.py
branch = True
source = tests
#omit = src/
[report]
fail_under = 90.0
show_missing = True

7
.gitattributes vendored 100644
Wyświetl plik

@ -0,0 +1,7 @@
# Default for all text files
* text=auto whitespace=trailing-space,tab-in-indent,tabwidth=2
*.py text=auto whitespace=trailing-space,tab-in-indent,tabwidth=4
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary

1
.github/CODEOWNERS vendored
Wyświetl plik

@ -1 +1,2 @@
* @mihtjel
* @zarath

Wyświetl plik

@ -0,0 +1,37 @@
---
name: Bug Report
about: Create a report to help NanoVNA-Saver to improve
title: "bug: "
labels: "bug"
assignees: ""
---
# Bug Report
**NanoVNA-Saver version:**
<!-- Please specify commit or tag version. -->
**Current behavior:**
<!-- Describe how the bug manifests. -->
**Expected behavior:**
<!-- Describe what you expect the behavior to be without the bug. -->
**Steps to reproduce:**
<!-- Explain the steps required to duplicate the issue, especially if you are able to provide a sample application. -->
**Related code:**
<!-- If you are able to illustrate the bug or feature request with an example, please provide it here. -->
```
insert short code snippets here
```
**Other information:**
<!-- List any other information that is relevant to your issue. Related issues, suggestions on how to fix, Stack Overflow links, forum links, etc. -->

Wyświetl plik

@ -0,0 +1,35 @@
---
name: Feature Request
about: Suggest an idea for this project
title: "feat: "
labels: "enhancement"
assignees: ""
---
# Feature Request
**Describe the Feature Request**
<!-- A clear and concise description of what the feature request is. Please include if your feature request is related to a problem. -->
**Describe Preferred Solution**
<!-- A clear and concise description of what you want to happen. -->
**Describe Alternatives**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Related Code**
<!-- If you are able to illustrate the bug or feature request with an example, please provide it here. -->
**Additional Context**
<!-- List any other information that is relevant to your issue. Stack traces, related issues, suggestions on how to add, use case, Stack Overflow links, forum links, screenshots, OS if applicable, etc. -->
**If the feature request is approved, would you be willing to submit a PR?**
_(Help can be provided if you need assistance submitting a PR)_
- [ ] Yes
- [ ] No

Wyświetl plik

@ -0,0 +1,7 @@
---
name: Codebase improvement
about: Provide your feedback for the existing codebase. Suggest a better solution for algorithms, development tools, etc.
title: "dev: "
labels: "enhancement"
assignees: ""
---

Wyświetl plik

@ -1,5 +1,6 @@
---
blank_issues_enabled: false
contact_links:
- name: nanovna-users groups.io group
url: https://groups.io/g/nanovna-users/
about: Please ask any questions about using the NanoVNA or NanoVNA-Saver on this mailing list.
- name: NanoVNA-Saver Community Support
url: https://github.com/zarath@gmx.de/nanovna-saver/discussions
about: Please ask and answer questions here.

Wyświetl plik

@ -0,0 +1,40 @@
<!--- Please provide a general summary of your changes in the title above -->
## Pull Request type
<!-- Please try to limit your pull request to one type; submit multiple pull requests if needed. -->
Please check the type of change your PR introduces:
- [] Bugfix
- [] Feature
- [] Code style update (formatting, renaming)
- [] Refactoring (no functional changes, no API changes)
- [] Build-related changes
- [] Documentation content changes
- [] Other (please describe):
## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
Issue Number: N/A
## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by this PR. -->
-
-
-
## Does this introduce a breaking change?
- [] Yes
- [] No
<!-- If this does introduce a breaking change, please describe the impact and migration path for existing applications below. -->
## Other information
<!-- Any other information that is important to this PR, such as screenshots of how the component looks before and after the change. -->

Wyświetl plik

@ -0,0 +1,72 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "main" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "main" ]
schedule:
- cron: '19 8 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

Wyświetl plik

@ -13,26 +13,25 @@ jobs:
strategy:
matrix:
# os: [ubuntu-latest, macos-latest, windows-latest]
os: [ubuntu-latest, ]
os: [ubuntu-latest]
# python-version: [3.7, 3.8]
python-version: [3.7, ]
python-version: [3.8, 3.9]
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Lint with pylint
run: |
pip install pylint
pylint --exit-zero NanoVNASaver
- name: Unittests / Coverage
run: |
pip install pytest-cov
pytest --cov=NanoVNASaver
- uses: actions/checkout@v3
- name: Set up Python 3
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Lint with pylint
run: |
pip install pylint
pylint --exit-zero NanoVNASaver
- name: Unittests / Coverage
run: |
pip install pytest-cov
pytest --cov=NanoVNASaver

Wyświetl plik

@ -1,35 +0,0 @@
name: Python Release
on:
push:
tags:
- v*
jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [3.7, ]
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies and pyinstall
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install PyInstaller
- name: Build binary
run: |
pyinstaller nanovna-saver.py
- name: Archive production artifacts
uses: actions/upload-artifact@v1
with:
name: NanoVNASaver.${{ matrix.os }}
path: dist/nanovna-saver

Wyświetl plik

@ -0,0 +1,48 @@
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
with:
fetch-depth: 0
- 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 \
'^libxcb.*-dev' libx11-xcb-dev \
libglu1-mesa-dev libxrender-dev libxi-dev \
libxkbcommon-dev libxkbcommon-x11-dev
- name: Install dependencies and pyinstall
run: |
python3.11 -m venv build
. build/bin/activate
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: |
. build/bin/activate
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
uses: actions/upload-artifact@v1
with:
name: NanoVNASaver.linux_modern
path: dist/nanovna-saver

Wyświetl plik

@ -0,0 +1,35 @@
name: Mac Release
on:
push:
tags:
- v*
workflow_dispatch:
jobs:
release:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.11
- name: Install dependencies and pyinstall
run: |
python -m pip install pip==23.3.2 setuptools==69.0.3
pip install -r requirements.txt
pip install PyInstaller==6.3.0
- name: Build binary
run: |
python setup.py -V
pyinstaller --onefile -p src -n nanovna-saver nanovna-saver.py
- name: Archive production artifacts
uses: actions/upload-artifact@v1
with:
name: NanoVNASaver.macos
path: dist/nanovna-saver

Wyświetl plik

@ -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

Wyświetl plik

@ -0,0 +1,44 @@
name: Windows Release
on:
push:
tags:
- v*
workflow_dispatch:
jobs:
release:
runs-on: windows-latest
strategy:
matrix:
arch: [x64, ]
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.11
architecture: ${{ matrix.arch }}
- name: Install dependencies and pyinstall
run: |
python3 -m venv venv
.\venv\Scripts\activate
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
run: |
.\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
uses: actions/upload-artifact@v1
with:
name: NanoVNASaver.${{ matrix.arch }}
path: dist/nanovna-saver.exe

44
.github/workflows/stale.yml vendored 100644
Wyświetl plik

@ -0,0 +1,44 @@
---
name: Stale
on:
schedule:
- cron: "0 8 * * *"
workflow_dispatch:
jobs:
stale:
name: 🧹 Clean up stale issues and PRs
runs-on: ubuntu-latest
steps:
- name: 🚀 Run stale
uses: actions/stale@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90
days-before-close: 30
remove-stale-when-updated: true
stale-issue-label: "stale"
exempt-issue-labels: "no-stale,help-wanted"
stale-issue-message: >
There hasn't been any activity on this issue recently, and in order
to prioritize active issues, it will be marked as stale.
Please make sure to update to the latest version and
check if that solves the issue. Let us know if that works for you
by leaving a 👍
Because this issue is marked as stale, it will be closed and locked
in 7 days if no further activity occurs.
Thank you for your contributions!
stale-pr-label: "stale"
exempt-pr-labels: "no-stale"
stale-pr-message: >
There hasn't been any activity on this pull request recently, and in
order to prioritize active work, it has been marked as stale.
This PR will be closed and locked in 7 days if no further activity
occurs.
Thank you for your contributions!

62
.gitignore vendored
Wyświetl plik

@ -1,12 +1,56 @@
/venv/
# Temporary and binary files
*~
*.py[cod]
*.so
*.cfg
!.isort.cfg
!setup.cfg
*.orig
*.log
*.pot
__pycache__/*
.cache/*
.*.swp
*/.ipynb_checkpoints/*
.DS_Store
# Project files
.ropeproject
.project
.pydevproject
.settings
.idea
.vscode
/build/
/dist/
/nanovna-saver.spec
*.egg-info/
*.pyc
*.cal
settings.json
.gitignore
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

2
.idea/.gitignore vendored
Wyświetl plik

@ -1,2 +0,0 @@
# Default ignored files
/workspace.xml

Wyświetl plik

@ -1,6 +0,0 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Copyright 2019 Rune B. Broberg&#10;This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA." />
<option name="myName" value="CC-BY-SA" />
</copyright>
</component>

Wyświetl plik

@ -1,6 +0,0 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Copyright (c) &amp;#36;year Rune B. Broberg" />
<option name="myName" value="Copyright" />
</copyright>
</component>

Wyświetl plik

@ -1,7 +0,0 @@
<component name="CopyrightManager">
<copyright>
<option name="allowReplaceRegexp" value="Copyright" />
<option name="notice" value="NanoVNASaver - a python program to view and export Touchstone data from a NanoVNA&#10;Copyright (C) &amp;#36;today.year. Rune B. Broberg&#10;&#10;This program is free software: you can redistribute it and/or modify&#10;it under the terms of the GNU General Public License as published by&#10;the Free Software Foundation, either version 3 of the License, or&#10;(at your option) any later version.&#10;&#10;This program is distributed in the hope that it will be useful,&#10;but WITHOUT ANY WARRANTY; without even the implied warranty of&#10;MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&#10;GNU General Public License for more details.&#10;&#10;You should have received a copy of the GNU General Public License&#10;along with this program. If not, see &lt;https://www.gnu.org/licenses/&gt;. " />
<option name="myName" value="GPL v3" />
</copyright>
</component>

Wyświetl plik

@ -1,7 +0,0 @@
<component name="CopyrightManager">
<settings>
<module2copyright>
<element module="Project Files" copyright="GPL v3" />
</module2copyright>
</settings>
</component>

Wyświetl plik

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

Wyświetl plik

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7 (nanovna-saver)" project-jdk-type="Python SDK" />
</project>

Wyświetl plik

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/nanovna-saver.iml" filepath="$PROJECT_DIR$/.idea/nanovna-saver.iml" />
</modules>
</component>
</project>

Wyświetl plik

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="Unittests" />
</component>
</module>

Wyświetl plik

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

Wyświetl plik

@ -12,4 +12,4 @@ disable=W0614,C0410,C0321,C0111,I0011,C0103
# 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
[MASTER]
extension-pkg-whitelist=PyQt5
extension-pkg-allow-list=PyQt6.QtWidgets,PyQt6.QtGui,PyQt6.QtCore

27
.readthedocs.yml 100644
Wyświetl plik

@ -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}

43
AUTHORS.rst 100644
Wyświetl plik

@ -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>

322
CONTRIBUTING.rst 100644
Wyświetl plik

@ -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 youre 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

60
Makefile 100644
Wyświetl plik

@ -0,0 +1,60 @@
.PHONY: info
info:
@echo "- type 'make deb' to build a debian package"
@echo "- type 'make rpm' to build an (experimental) rpm package"
@echo "- you need the debian packages"
@echo " fakeroot python3-setuptools python3-stdeb dh-python"
@echo
# build a new debian package and create a link in the current directory
.PHONY: deb
deb: clean
@# build the deb package
PYBUILD_DISABLE=test python3 setup.py \
--command-packages=stdeb.command \
sdist_dsc --compat 10 --package3 nanovnasaver --section electronics \
bdist_deb
@# create a link in the main directory
-@ rm nanovnasaver_*_all.deb
-@ln `ls deb_dist/nanovnasaver_*.deb | tail -1` .
@# and show the result
@ls -l nanovnasaver_*.deb
# build a new rpm package and create a link in the current directory
.PHONY: rpm
rpm: clean
@# build the rpm package
PYBUILD_DISABLE=test python3 setup.py bdist_rpm
@# create a link in the main directory
-@ rm NanoVNASaver-*.noarch.rpm
@ln `ls dist/NanoVNASaver-*.noarch.rpm | tail -1` .
@# and show the result
@ls -l NanoVNASaver-*.noarch.rpm
# remove all package build artifacts (keep the *.deb)
.PHONY: clean
clean:
python setup.py clean
-rm -rf build deb_dist dist *.tar.gz *.egg*
# remove all package build artefacts
.PHONY: distclean
distclean: clean
-rm -f *.deb *.rpm
# build and install a new debian package
.PHONY: debinstall
debinstall: deb
sudo apt install ./nanovnasaver_*.deb
# uninstall this debian package
.PHONY: debuninstall
debuninstall:
sudo apt purge nanovnasaver

Wyświetl plik

@ -0,0 +1,18 @@
[Desktop Entry]
Categories=Electronics;Education;
Comment[de_DE]=Programm das Daten vom NanoVNA liest, anzeigt und speichert
Comment=Tool for reading, displaying and saving data from the NanoVNA
Exec=NanoVNASaver
GenericName[de_DE]=
GenericName=
Icon=NanoVNASaver_48x48
MimeType=
Name[de_DE]=NanoVNASaver
Name=NanoVNASaver
StartupNotify=true
Terminal=false
Type=Application
X-DBUS-ServiceName=
X-DBUS-StartupType=
X-KDE-SubstituteUID=false
X-KDE-Username=

Wyświetl plik

@ -1,360 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
from PyQt5 import QtWidgets
from NanoVNASaver.Formatting import format_frequency
from NanoVNASaver.Analysis import Analysis
logger = logging.getLogger(__name__)
class BandPassAnalysis(Analysis):
def __init__(self, app):
super().__init__(app)
self._widget = QtWidgets.QWidget()
layout = QtWidgets.QFormLayout()
self._widget.setLayout(layout)
layout.addRow(QtWidgets.QLabel("Band pass filter analysis"))
layout.addRow(
QtWidgets.QLabel(
f"Please place {self.app.markers[0].name} in the filter passband."))
self.result_label = QtWidgets.QLabel()
self.lower_cutoff_label = QtWidgets.QLabel()
self.lower_six_db_label = QtWidgets.QLabel()
self.lower_sixty_db_label = QtWidgets.QLabel()
self.lower_db_per_octave_label = QtWidgets.QLabel()
self.lower_db_per_decade_label = QtWidgets.QLabel()
self.upper_cutoff_label = QtWidgets.QLabel()
self.upper_six_db_label = QtWidgets.QLabel()
self.upper_sixty_db_label = QtWidgets.QLabel()
self.upper_db_per_octave_label = QtWidgets.QLabel()
self.upper_db_per_decade_label = QtWidgets.QLabel()
layout.addRow("Result:", self.result_label)
layout.addRow(QtWidgets.QLabel(""))
self.center_frequency_label = QtWidgets.QLabel()
self.span_label = QtWidgets.QLabel()
self.six_db_span_label = QtWidgets.QLabel()
self.quality_label = QtWidgets.QLabel()
layout.addRow("Center frequency:", self.center_frequency_label)
layout.addRow("Bandwidth (-3 dB):", self.span_label)
layout.addRow("Quality factor:", self.quality_label)
layout.addRow("Bandwidth (-6 dB):", self.six_db_span_label)
layout.addRow(QtWidgets.QLabel(""))
layout.addRow(QtWidgets.QLabel("Lower side:"))
layout.addRow("Cutoff frequency:", self.lower_cutoff_label)
layout.addRow("-6 dB point:", self.lower_six_db_label)
layout.addRow("-60 dB point:", self.lower_sixty_db_label)
layout.addRow("Roll-off:", self.lower_db_per_octave_label)
layout.addRow("Roll-off:", self.lower_db_per_decade_label)
layout.addRow(QtWidgets.QLabel(""))
layout.addRow(QtWidgets.QLabel("Upper side:"))
layout.addRow("Cutoff frequency:", self.upper_cutoff_label)
layout.addRow("-6 dB point:", self.upper_six_db_label)
layout.addRow("-60 dB point:", self.upper_sixty_db_label)
layout.addRow("Roll-off:", self.upper_db_per_octave_label)
layout.addRow("Roll-off:", self.upper_db_per_decade_label)
def reset(self):
self.result_label.clear()
self.center_frequency_label.clear()
self.span_label.clear()
self.quality_label.clear()
self.six_db_span_label.clear()
self.upper_cutoff_label.clear()
self.upper_six_db_label.clear()
self.upper_sixty_db_label.clear()
self.upper_db_per_octave_label.clear()
self.upper_db_per_decade_label.clear()
self.lower_cutoff_label.clear()
self.lower_six_db_label.clear()
self.lower_sixty_db_label.clear()
self.lower_db_per_octave_label.clear()
self.lower_db_per_decade_label.clear()
def runAnalysis(self):
self.reset()
pass_band_location = self.app.markers[0].location
logger.debug("Pass band location: %d", pass_band_location)
if len(self.app.data21) == 0:
logger.debug("No data to analyse")
self.result_label.setText("No data to analyse.")
return
if pass_band_location < 0:
logger.debug("No location for %s", self.app.markers[0].name)
self.result_label.setText(
f"Please place {self.app.markers[0].name} in the passband.")
return
pass_band_db = self.app.data21[pass_band_location].gain
logger.debug("Initial passband gain: %d", pass_band_db)
initial_lower_cutoff_location = -1
for i in range(pass_band_location, -1, -1):
if (pass_band_db - self.app.data21[i].gain) > 3:
# We found a cutoff location
initial_lower_cutoff_location = i
break
if initial_lower_cutoff_location < 0:
self.result_label.setText("Lower cutoff location not found.")
return
initial_lower_cutoff_frequency = self.app.data21[initial_lower_cutoff_location].freq
logger.debug("Found initial lower cutoff frequency at %d", initial_lower_cutoff_frequency)
initial_upper_cutoff_location = -1
for i in range(pass_band_location, len(self.app.data21), 1):
if (pass_band_db - self.app.data21[i].gain) > 3:
# We found a cutoff location
initial_upper_cutoff_location = i
break
if initial_upper_cutoff_location < 0:
self.result_label.setText("Upper cutoff location not found.")
return
initial_upper_cutoff_frequency = self.app.data21[initial_upper_cutoff_location].freq
logger.debug("Found initial upper cutoff frequency at %d", initial_upper_cutoff_frequency)
peak_location = -1
peak_db = self.app.data21[initial_lower_cutoff_location].gain
for i in range(initial_lower_cutoff_location, initial_upper_cutoff_location, 1):
db = self.app.data21[i].gain
if db > peak_db:
peak_db = db
peak_location = i
logger.debug("Found peak of %f at %d", peak_db, self.app.data[peak_location].freq)
lower_cutoff_location = -1
pass_band_db = peak_db
for i in range(peak_location, -1, -1):
if (pass_band_db - self.app.data21[i].gain) > 3:
# We found the cutoff location
lower_cutoff_location = i
break
lower_cutoff_frequency = self.app.data21[lower_cutoff_location].freq
lower_cutoff_gain = self.app.data21[lower_cutoff_location].gain - pass_band_db
if lower_cutoff_gain < -4:
logger.debug("Lower cutoff frequency found at %f dB"
" - insufficient data points for true -3 dB point.",
lower_cutoff_gain)
logger.debug("Found true lower cutoff frequency at %d", lower_cutoff_frequency)
self.lower_cutoff_label.setText(
f"{format_frequency(lower_cutoff_frequency)}"
f" ({round(lower_cutoff_gain, 1)} dB)")
self.app.markers[1].setFrequency(str(lower_cutoff_frequency))
self.app.markers[1].frequencyInput.setText(str(lower_cutoff_frequency))
upper_cutoff_location = -1
pass_band_db = peak_db
for i in range(peak_location, len(self.app.data21), 1):
if (pass_band_db - self.app.data21[i].gain) > 3:
# We found the cutoff location
upper_cutoff_location = i
break
upper_cutoff_frequency = self.app.data21[upper_cutoff_location].freq
upper_cutoff_gain = self.app.data21[upper_cutoff_location].gain - pass_band_db
if upper_cutoff_gain < -4:
logger.debug("Upper cutoff frequency found at %f dB"
" - insufficient data points for true -3 dB point.",
upper_cutoff_gain)
logger.debug("Found true upper cutoff frequency at %d", upper_cutoff_frequency)
self.upper_cutoff_label.setText(
f"{format_frequency(upper_cutoff_frequency)}"
f" ({round(upper_cutoff_gain, 1)} dB)")
self.app.markers[2].setFrequency(str(upper_cutoff_frequency))
self.app.markers[2].frequencyInput.setText(str(upper_cutoff_frequency))
span = upper_cutoff_frequency - lower_cutoff_frequency
center_frequency = math.sqrt(
lower_cutoff_frequency * upper_cutoff_frequency)
q = center_frequency / span
self.span_label.setText(format_frequency(span))
self.center_frequency_label.setText(
format_frequency(center_frequency))
self.quality_label.setText(str(round(q, 2)))
self.app.markers[0].setFrequency(str(round(center_frequency)))
self.app.markers[0].frequencyInput.setText(str(round(center_frequency)))
# Lower roll-off
lower_six_db_location = -1
for i in range(lower_cutoff_location, -1, -1):
if (pass_band_db - self.app.data21[i].gain) > 6:
# We found 6dB location
lower_six_db_location = i
break
if lower_six_db_location < 0:
self.result_label.setText("Lower 6 dB location not found.")
return
lower_six_db_cutoff_frequency = self.app.data21[lower_six_db_location].freq
self.lower_six_db_label.setText(
format_frequency(lower_six_db_cutoff_frequency))
ten_db_location = -1
for i in range(lower_cutoff_location, -1, -1):
if (pass_band_db - self.app.data21[i].gain) > 10:
# We found 6dB location
ten_db_location = i
break
twenty_db_location = -1
for i in range(lower_cutoff_location, -1, -1):
if (pass_band_db - self.app.data21[i].gain) > 20:
# We found 6dB location
twenty_db_location = i
break
sixty_db_location = -1
for i in range(lower_six_db_location, -1, -1):
if (pass_band_db - self.app.data21[i].gain) > 60:
# We found 60dB location! Wow.
sixty_db_location = i
break
if sixty_db_location > 0:
if sixty_db_location > 0:
sixty_db_cutoff_frequency = self.app.data21[sixty_db_location].freq
self.lower_sixty_db_label.setText(
format_frequency(sixty_db_cutoff_frequency))
elif ten_db_location != -1 and twenty_db_location != -1:
ten = self.app.data21[ten_db_location].freq
twenty = self.app.data21[twenty_db_location].freq
sixty_db_frequency = ten * \
10 ** (5 * (math.log10(twenty) - math.log10(ten)))
self.lower_sixty_db_label.setText(
f"{format_frequency(sixty_db_frequency)} (derived)")
else:
self.lower_sixty_db_label.setText("Not calculated")
if ten_db_location > 0 and twenty_db_location > 0 and ten_db_location != twenty_db_location:
octave_attenuation, decade_attenuation = self.calculateRolloff(
ten_db_location, twenty_db_location)
self.lower_db_per_octave_label.setText(
str(round(octave_attenuation, 3)) + " dB / octave")
self.lower_db_per_decade_label.setText(
str(round(decade_attenuation, 3)) + " dB / decade")
else:
self.lower_db_per_octave_label.setText("Not calculated")
self.lower_db_per_decade_label.setText("Not calculated")
# Upper roll-off
upper_six_db_location = -1
for i in range(upper_cutoff_location, len(self.app.data21), 1):
if (pass_band_db - self.app.data21[i].gain) > 6:
# We found 6dB location
upper_six_db_location = i
break
if upper_six_db_location < 0:
self.result_label.setText("Upper 6 dB location not found.")
return
upper_six_db_cutoff_frequency = self.app.data21[upper_six_db_location].freq
self.upper_six_db_label.setText(
format_frequency(upper_six_db_cutoff_frequency))
six_db_span = upper_six_db_cutoff_frequency - lower_six_db_cutoff_frequency
self.six_db_span_label.setText(
format_frequency(six_db_span))
ten_db_location = -1
for i in range(upper_cutoff_location, len(self.app.data21), 1):
if (pass_band_db - self.app.data21[i].gain) > 10:
# We found 6dB location
ten_db_location = i
break
twenty_db_location = -1
for i in range(upper_cutoff_location, len(self.app.data21), 1):
if (pass_band_db - self.app.data21[i].gain) > 20:
# We found 6dB location
twenty_db_location = i
break
sixty_db_location = -1
for i in range(upper_six_db_location, len(self.app.data21), 1):
if (pass_band_db - self.app.data21[i].gain) > 60:
# We found 60dB location! Wow.
sixty_db_location = i
break
if sixty_db_location > 0:
sixty_db_cutoff_frequency = self.app.data21[sixty_db_location].freq
self.upper_sixty_db_label.setText(
format_frequency(sixty_db_cutoff_frequency))
elif ten_db_location != -1 and twenty_db_location != -1:
ten = self.app.data21[ten_db_location].freq
twenty = self.app.data21[twenty_db_location].freq
sixty_db_frequency = ten * \
10 ** (5 * (math.log10(twenty) - math.log10(ten)))
self.upper_sixty_db_label.setText(
f"{format_frequency(sixty_db_frequency)} (derived)")
else:
self.upper_sixty_db_label.setText("Not calculated")
if ten_db_location > 0 and twenty_db_location > 0 and ten_db_location != twenty_db_location:
octave_attenuation, decade_attenuation = self.calculateRolloff(
ten_db_location, twenty_db_location)
self.upper_db_per_octave_label.setText(
f"{round(octave_attenuation, 3)} dB / octave")
self.upper_db_per_decade_label.setText(
f"{round(decade_attenuation, 3)} dB / decade")
else:
self.upper_db_per_octave_label.setText("Not calculated")
self.upper_db_per_decade_label.setText("Not calculated")
if upper_cutoff_gain < -4 or lower_cutoff_gain < -4:
self.result_label.setText(
f"Analysis complete ({len(self.app.data)} points)\n"
f"Insufficient data for analysis. Increase segment count.")
else:
self.result_label.setText(
f"Analysis complete ({len(self.app.data)} points)")

Wyświetl plik

@ -1,314 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
from PyQt5 import QtWidgets
from NanoVNASaver.Analysis import Analysis
from NanoVNASaver.Formatting import format_frequency
logger = logging.getLogger(__name__)
class BandStopAnalysis(Analysis):
def __init__(self, app):
super().__init__(app)
self._widget = QtWidgets.QWidget()
layout = QtWidgets.QFormLayout()
self._widget.setLayout(layout)
layout.addRow(QtWidgets.QLabel("Band stop filter analysis"))
self.result_label = QtWidgets.QLabel()
self.lower_cutoff_label = QtWidgets.QLabel()
self.lower_six_db_label = QtWidgets.QLabel()
self.lower_sixty_db_label = QtWidgets.QLabel()
self.lower_db_per_octave_label = QtWidgets.QLabel()
self.lower_db_per_decade_label = QtWidgets.QLabel()
self.upper_cutoff_label = QtWidgets.QLabel()
self.upper_six_db_label = QtWidgets.QLabel()
self.upper_sixty_db_label = QtWidgets.QLabel()
self.upper_db_per_octave_label = QtWidgets.QLabel()
self.upper_db_per_decade_label = QtWidgets.QLabel()
layout.addRow("Result:", self.result_label)
layout.addRow(QtWidgets.QLabel(""))
self.center_frequency_label = QtWidgets.QLabel()
self.span_label = QtWidgets.QLabel()
self.six_db_span_label = QtWidgets.QLabel()
self.quality_label = QtWidgets.QLabel()
layout.addRow("Center frequency:", self.center_frequency_label)
layout.addRow("Bandwidth (-3 dB):", self.span_label)
layout.addRow("Quality factor:", self.quality_label)
layout.addRow("Bandwidth (-6 dB):", self.six_db_span_label)
layout.addRow(QtWidgets.QLabel(""))
layout.addRow(QtWidgets.QLabel("Lower side:"))
layout.addRow("Cutoff frequency:", self.lower_cutoff_label)
layout.addRow("-6 dB point:", self.lower_six_db_label)
layout.addRow("-60 dB point:", self.lower_sixty_db_label)
layout.addRow("Roll-off:", self.lower_db_per_octave_label)
layout.addRow("Roll-off:", self.lower_db_per_decade_label)
layout.addRow(QtWidgets.QLabel(""))
layout.addRow(QtWidgets.QLabel("Upper side:"))
layout.addRow("Cutoff frequency:", self.upper_cutoff_label)
layout.addRow("-6 dB point:", self.upper_six_db_label)
layout.addRow("-60 dB point:", self.upper_sixty_db_label)
layout.addRow("Roll-off:", self.upper_db_per_octave_label)
layout.addRow("Roll-off:", self.upper_db_per_decade_label)
def reset(self):
self.result_label.clear()
self.span_label.clear()
self.quality_label.clear()
self.six_db_span_label.clear()
self.upper_cutoff_label.clear()
self.upper_six_db_label.clear()
self.upper_sixty_db_label.clear()
self.upper_db_per_octave_label.clear()
self.upper_db_per_decade_label.clear()
self.lower_cutoff_label.clear()
self.lower_six_db_label.clear()
self.lower_sixty_db_label.clear()
self.lower_db_per_octave_label.clear()
self.lower_db_per_decade_label.clear()
def runAnalysis(self):
self.reset()
if len(self.app.data21) == 0:
logger.debug("No data to analyse")
self.result_label.setText("No data to analyse.")
return
peak_location = -1
peak_db = self.app.data21[0].gain
for i in range(len(self.app.data21)):
db = self.app.data21[i].gain
if db > peak_db:
peak_db = db
peak_location = i
logger.debug("Found peak of %f at %d", peak_db, self.app.data[peak_location].freq)
lower_cutoff_location = -1
pass_band_db = peak_db
for i in range(len(self.app.data21)):
if (pass_band_db - self.app.data21[i].gain) > 3:
# We found the cutoff location
lower_cutoff_location = i
break
lower_cutoff_frequency = self.app.data21[lower_cutoff_location].freq
lower_cutoff_gain = self.app.data21[lower_cutoff_location].gain - pass_band_db
if lower_cutoff_gain < -4:
logger.debug("Lower cutoff frequency found at %f dB"
" - insufficient data points for true -3 dB point.",
lower_cutoff_gain)
logger.debug("Found true lower cutoff frequency at %d", lower_cutoff_frequency)
self.lower_cutoff_label.setText(
f"{format_frequency(lower_cutoff_frequency)}"
f" ({round(lower_cutoff_gain, 1)} dB)")
self.app.markers[1].setFrequency(str(lower_cutoff_frequency))
self.app.markers[1].frequencyInput.setText(str(lower_cutoff_frequency))
upper_cutoff_location = -1
for i in range(len(self.app.data21)-1, -1, -1):
if (pass_band_db - self.app.data21[i].gain) > 3:
# We found the cutoff location
upper_cutoff_location = i
break
upper_cutoff_frequency = self.app.data21[upper_cutoff_location].freq
upper_cutoff_gain = self.app.data21[upper_cutoff_location].gain - pass_band_db
if upper_cutoff_gain < -4:
logger.debug("Upper cutoff frequency found at %f dB"
" - insufficient data points for true -3 dB point.",
upper_cutoff_gain)
logger.debug("Found true upper cutoff frequency at %d", upper_cutoff_frequency)
self.upper_cutoff_label.setText(
f"{format_frequency(upper_cutoff_frequency)}"
f" ({round(upper_cutoff_gain, 1)} dB)")
self.app.markers[2].setFrequency(str(upper_cutoff_frequency))
self.app.markers[2].frequencyInput.setText(str(upper_cutoff_frequency))
span = upper_cutoff_frequency - lower_cutoff_frequency
center_frequency = math.sqrt(lower_cutoff_frequency * upper_cutoff_frequency)
q = center_frequency / span
self.span_label.setText(format_frequency(span))
self.center_frequency_label.setText(
format_frequency(center_frequency))
self.quality_label.setText(str(round(q, 2)))
self.app.markers[0].setFrequency(str(round(center_frequency)))
self.app.markers[0].frequencyInput.setText(str(round(center_frequency)))
# Lower roll-off
lower_six_db_location = -1
for i in range(lower_cutoff_location, len(self.app.data21)):
if (pass_band_db - self.app.data21[i].gain) > 6:
# We found 6dB location
lower_six_db_location = i
break
if lower_six_db_location < 0:
self.result_label.setText("Lower 6 dB location not found.")
return
lower_six_db_cutoff_frequency = self.app.data21[lower_six_db_location].freq
self.lower_six_db_label.setText(
format_frequency(lower_six_db_cutoff_frequency))
ten_db_location = -1
for i in range(lower_cutoff_location, len(self.app.data21)):
if (pass_band_db - self.app.data21[i].gain) > 10:
# We found 6dB location
ten_db_location = i
break
twenty_db_location = -1
for i in range(lower_cutoff_location, len(self.app.data21)):
if (pass_band_db - self.app.data21[i].gain) > 20:
# We found 6dB location
twenty_db_location = i
break
sixty_db_location = -1
for i in range(lower_six_db_location, len(self.app.data21)):
if (pass_band_db - self.app.data21[i].gain) > 60:
# We found 60dB location! Wow.
sixty_db_location = i
break
if sixty_db_location > 0:
sixty_db_cutoff_frequency = self.app.data21[sixty_db_location].freq
self.lower_sixty_db_label.setText(
format_frequency(sixty_db_cutoff_frequency))
elif ten_db_location != -1 and twenty_db_location != -1:
ten = self.app.data21[ten_db_location].freq
twenty = self.app.data21[twenty_db_location].freq
sixty_db_frequency = ten * 10 ** (5 * (math.log10(twenty) - math.log10(ten)))
self.lower_sixty_db_label.setText(
f"{format_frequency(sixty_db_frequency)} (derived)")
else:
self.lower_sixty_db_label.setText("Not calculated")
if (ten_db_location > 0 and
twenty_db_location > 0 and
ten_db_location != twenty_db_location):
octave_attenuation, decade_attenuation = self.calculateRolloff(
ten_db_location, twenty_db_location)
self.lower_db_per_octave_label.setText(
f"{round(octave_attenuation, 3)} dB / octave")
self.lower_db_per_decade_label.setText(
f"{round(decade_attenuation, 3)} dB / decade")
else:
self.lower_db_per_octave_label.setText("Not calculated")
self.lower_db_per_decade_label.setText("Not calculated")
# Upper roll-off
upper_six_db_location = -1
for i in range(upper_cutoff_location, -1, -1):
if (pass_band_db - self.app.data21[i].gain) > 6:
# We found 6dB location
upper_six_db_location = i
break
if upper_six_db_location < 0:
self.result_label.setText("Upper 6 dB location not found.")
return
upper_six_db_cutoff_frequency = self.app.data21[upper_six_db_location].freq
self.upper_six_db_label.setText(
format_frequency(upper_six_db_cutoff_frequency))
six_db_span = upper_six_db_cutoff_frequency - lower_six_db_cutoff_frequency
self.six_db_span_label.setText(
format_frequency(six_db_span))
ten_db_location = -1
for i in range(upper_cutoff_location, -1, -1):
if (pass_band_db - self.app.data21[i].gain) > 10:
# We found 6dB location
ten_db_location = i
break
twenty_db_location = -1
for i in range(upper_cutoff_location, -1, -1):
if (pass_band_db - self.app.data21[i].gain) > 20:
# We found 6dB location
twenty_db_location = i
break
sixty_db_location = -1
for i in range(upper_six_db_location, -1, -1):
if (pass_band_db - self.app.data21[i].gain) > 60:
# We found 60dB location! Wow.
sixty_db_location = i
break
if sixty_db_location > 0:
sixty_db_cutoff_frequency = self.app.data21[sixty_db_location].freq
self.upper_sixty_db_label.setText(
format_frequency(sixty_db_cutoff_frequency))
elif ten_db_location != -1 and twenty_db_location != -1:
ten = self.app.data21[ten_db_location].freq
twenty = self.app.data21[twenty_db_location].freq
sixty_db_frequency = ten * 10 ** (
5 * (math.log10(twenty) - math.log10(ten)))
self.upper_sixty_db_label.setText(
f"{format_frequency(sixty_db_frequency)} (derived)")
else:
self.upper_sixty_db_label.setText("Not calculated")
if (ten_db_location > 0 and
twenty_db_location > 0 and
ten_db_location != twenty_db_location):
octave_attenuation, decade_attenuation = self.calculateRolloff(
ten_db_location, twenty_db_location)
self.upper_db_per_octave_label.setText(
f"{round(octave_attenuation, 3)} dB / octave")
self.upper_db_per_decade_label.setText(
f"{round(decade_attenuation, 3)} dB / decade")
else:
self.upper_db_per_octave_label.setText("Not calculated")
self.upper_db_per_decade_label.setText("Not calculated")
if upper_cutoff_gain < -4 or lower_cutoff_gain < -4:
self.result_label.setText(
f"Analysis complete ({len(self.app.data)} points)\n"
f"Insufficient data for analysis. Increase segment count.")
else:
self.result_label.setText(
f"Analysis complete ({len(self.app.data)} points)")

Wyświetl plik

@ -1,188 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
from PyQt5 import QtWidgets
from NanoVNASaver.Analysis import Analysis
from NanoVNASaver.Formatting import format_frequency
logger = logging.getLogger(__name__)
class HighPassAnalysis(Analysis):
def __init__(self, app):
super().__init__(app)
self._widget = QtWidgets.QWidget()
layout = QtWidgets.QFormLayout()
self._widget.setLayout(layout)
layout.addRow(QtWidgets.QLabel("High pass filter analysis"))
layout.addRow(QtWidgets.QLabel(
f"Please place {self.app.markers[0].name} in the filter passband."))
self.result_label = QtWidgets.QLabel()
self.cutoff_label = QtWidgets.QLabel()
self.six_db_label = QtWidgets.QLabel()
self.sixty_db_label = QtWidgets.QLabel()
self.db_per_octave_label = QtWidgets.QLabel()
self.db_per_decade_label = QtWidgets.QLabel()
layout.addRow("Result:", self.result_label)
layout.addRow("Cutoff frequency:", self.cutoff_label)
layout.addRow("-6 dB point:", self.six_db_label)
layout.addRow("-60 dB point:", self.sixty_db_label)
layout.addRow("Roll-off:", self.db_per_octave_label)
layout.addRow("Roll-off:", self.db_per_decade_label)
def reset(self):
self.result_label.clear()
self.cutoff_label.clear()
self.six_db_label.clear()
self.sixty_db_label.clear()
self.db_per_octave_label.clear()
self.db_per_decade_label.clear()
def runAnalysis(self):
self.reset()
pass_band_location = self.app.markers[0].location
logger.debug("Pass band location: %d", pass_band_location)
if len(self.app.data21) == 0:
logger.debug("No data to analyse")
self.result_label.setText("No data to analyse.")
return
if pass_band_location < 0:
logger.debug("No location for %s", self.app.markers[0].name)
self.result_label.setText(
f"Please place {self.app.markers[0].name } in the passband.")
return
pass_band_db = self.app.data21[pass_band_location].gain
logger.debug("Initial passband gain: %d", pass_band_db)
initial_cutoff_location = -1
for i in range(pass_band_location, -1, -1):
db = self.app.data21[i].gain
if (pass_band_db - db) > 3:
# We found a cutoff location
initial_cutoff_location = i
break
if initial_cutoff_location < 0:
self.result_label.setText("Cutoff location not found.")
return
initial_cutoff_frequency = self.app.data21[initial_cutoff_location].freq
logger.debug("Found initial cutoff frequency at %d", initial_cutoff_frequency)
peak_location = -1
peak_db = self.app.data21[initial_cutoff_location].gain
for i in range(len(self.app.data21) - 1, initial_cutoff_location - 1, -1):
if self.app.data21[i].gain > peak_db:
peak_db = db
peak_location = i
logger.debug("Found peak of %f at %d", peak_db, self.app.data[peak_location].freq)
self.app.markers[0].setFrequency(str(self.app.data21[peak_location].freq))
self.app.markers[0].frequencyInput.setText(str(self.app.data21[peak_location].freq))
cutoff_location = -1
pass_band_db = peak_db
for i in range(peak_location, -1, -1):
if (pass_band_db - self.app.data21[i].gain) > 3:
# We found the cutoff location
cutoff_location = i
break
cutoff_frequency = self.app.data21[cutoff_location].freq
cutoff_gain = self.app.data21[cutoff_location].gain - pass_band_db
if cutoff_gain < -4:
logger.debug("Cutoff frequency found at %f dB"
" - insufficient data points for true -3 dB point.",
cutoff_gain)
logger.debug("Found true cutoff frequency at %d", cutoff_frequency)
self.cutoff_label.setText(
f"{format_frequency(cutoff_frequency)}"
f" {round(cutoff_gain, 1)} dB)")
self.app.markers[1].setFrequency(str(cutoff_frequency))
self.app.markers[1].frequencyInput.setText(str(cutoff_frequency))
six_db_location = -1
for i in range(cutoff_location, -1, -1):
if (pass_band_db - self.app.data21[i].gain) > 6:
# We found 6dB location
six_db_location = i
break
if six_db_location < 0:
self.result_label.setText("6 dB location not found.")
return
six_db_cutoff_frequency = self.app.data21[six_db_location].freq
self.six_db_label.setText(
format_frequency(six_db_cutoff_frequency))
ten_db_location = -1
for i in range(cutoff_location, -1, -1):
if (pass_band_db - self.app.data21[i].gain) > 10:
# We found 6dB location
ten_db_location = i
break
twenty_db_location = -1
for i in range(cutoff_location, -1, -1):
if (pass_band_db - self.app.data21[i].gain) > 20:
# We found 6dB location
twenty_db_location = i
break
sixty_db_location = -1
for i in range(six_db_location, -1, -1):
if (pass_band_db - self.app.data21[i].gain) > 60:
# We found 60dB location! Wow.
sixty_db_location = i
break
if sixty_db_location > 0:
if sixty_db_location > 0:
sixty_db_cutoff_frequency = self.app.data21[sixty_db_location].freq
self.sixty_db_label.setText(
format_frequency(sixty_db_cutoff_frequency))
elif ten_db_location != -1 and twenty_db_location != -1:
ten = self.app.data21[ten_db_location].freq
twenty = self.app.data21[twenty_db_location].freq
sixty_db_frequency = ten * 10 ** (5 * (math.log10(twenty) - math.log10(ten)))
self.sixty_db_label.setText(
f"{format_frequency(sixty_db_frequency)} (derived)")
else:
self.sixty_db_label.setText("Not calculated")
if ten_db_location > 0 and twenty_db_location > 0 and ten_db_location != twenty_db_location:
octave_attenuation, decade_attenuation = self.calculateRolloff(
ten_db_location, twenty_db_location)
self.db_per_octave_label.setText(str(round(octave_attenuation, 3)) + " dB / octave")
self.db_per_decade_label.setText(str(round(decade_attenuation, 3)) + " dB / decade")
else:
self.db_per_octave_label.setText("Not calculated")
self.db_per_decade_label.setText("Not calculated")
self.result_label.setText("Analysis complete (" + str(len(self.app.data)) + " points)")

Wyświetl plik

@ -1,203 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
from PyQt5 import QtWidgets
from NanoVNASaver.Analysis import Analysis
from NanoVNASaver.Formatting import format_frequency
logger = logging.getLogger(__name__)
class LowPassAnalysis(Analysis):
def __init__(self, app):
super().__init__(app)
self._widget = QtWidgets.QWidget()
layout = QtWidgets.QFormLayout()
self._widget.setLayout(layout)
layout.addRow(QtWidgets.QLabel("Low pass filter analysis"))
layout.addRow(
QtWidgets.QLabel(
f"Please place {self.app.markers[0].name}"
f" in the filter passband."))
self.result_label = QtWidgets.QLabel()
self.cutoff_label = QtWidgets.QLabel()
self.six_db_label = QtWidgets.QLabel()
self.sixty_db_label = QtWidgets.QLabel()
self.db_per_octave_label = QtWidgets.QLabel()
self.db_per_decade_label = QtWidgets.QLabel()
layout.addRow("Result:", self.result_label)
layout.addRow("Cutoff frequency:", self.cutoff_label)
layout.addRow("-6 dB point:", self.six_db_label)
layout.addRow("-60 dB point:", self.sixty_db_label)
layout.addRow("Roll-off:", self.db_per_octave_label)
layout.addRow("Roll-off:", self.db_per_decade_label)
def reset(self):
self.result_label.clear()
self.cutoff_label.clear()
self.six_db_label.clear()
self.sixty_db_label.clear()
self.db_per_octave_label.clear()
self.db_per_decade_label.clear()
def runAnalysis(self):
self.reset()
pass_band_location = self.app.markers[0].location
logger.debug("Pass band location: %d", pass_band_location)
if len(self.app.data21) == 0:
logger.debug("No data to analyse")
self.result_label.setText("No data to analyse.")
return
if pass_band_location < 0:
logger.debug("No location for %s",
self.app.markers[0].name)
self.result_label.setText(
f"Please place {self.app.markers[0].name} in the passband.")
return
pass_band_db = self.app.data21[pass_band_location].gain
logger.debug("Initial passband gain: %d", pass_band_db)
initial_cutoff_location = -1
for i in range(pass_band_location, len(self.app.data21)):
db = self.app.data21[i].gain
if (pass_band_db - db) > 3:
# We found a cutoff location
initial_cutoff_location = i
break
if initial_cutoff_location < 0:
self.result_label.setText("Cutoff location not found.")
return
initial_cutoff_frequency = self.app.data21[initial_cutoff_location].freq
logger.debug("Found initial cutoff frequency at %d", initial_cutoff_frequency)
peak_location = -1
peak_db = self.app.data21[initial_cutoff_location].gain
for i in range(0, initial_cutoff_location):
db = self.app.data21[i].gain
if db > peak_db:
peak_db = db
peak_location = i
logger.debug("Found peak of %f at %d", peak_db, self.app.data[peak_location].freq)
self.app.markers[0].setFrequency(str(self.app.data21[peak_location].freq))
self.app.markers[0].frequencyInput.setText(str(self.app.data21[peak_location].freq))
cutoff_location = -1
pass_band_db = peak_db
for i in range(peak_location, len(self.app.data21)):
db = self.app.data21[i].gain
if (pass_band_db - db) > 3:
# We found the cutoff location
cutoff_location = i
break
cutoff_frequency = self.app.data21[cutoff_location].freq
cutoff_gain = self.app.data21[cutoff_location].gain - pass_band_db
if cutoff_gain < -4:
logger.debug(
"Cutoff frequency found at %f dB"
" - insufficient data points for true -3 dB point.",
cutoff_gain)
logger.debug("Found true cutoff frequency at %d", cutoff_frequency)
self.cutoff_label.setText(
f"{format_frequency(cutoff_frequency)}"
f" ({round(cutoff_gain, 1)} dB)")
self.app.markers[1].setFrequency(str(cutoff_frequency))
self.app.markers[1].frequencyInput.setText(str(cutoff_frequency))
six_db_location = -1
for i in range(cutoff_location, len(self.app.data21)):
db = self.app.data21[i].gain
if (pass_band_db - db) > 6:
# We found 6dB location
six_db_location = i
break
if six_db_location < 0:
self.result_label.setText("6 dB location not found.")
return
six_db_cutoff_frequency = self.app.data21[six_db_location].freq
self.six_db_label.setText(
format_frequency(six_db_cutoff_frequency))
ten_db_location = -1
for i in range(cutoff_location, len(self.app.data21)):
db = self.app.data21[i].gain
if (pass_band_db - db) > 10:
# We found 6dB location
ten_db_location = i
break
twenty_db_location = -1
for i in range(cutoff_location, len(self.app.data21)):
db = self.app.data21[i].gain
if (pass_band_db - db) > 20:
# We found 6dB location
twenty_db_location = i
break
sixty_db_location = -1
for i in range(six_db_location, len(self.app.data21)):
db = self.app.data21[i].gain
if (pass_band_db - db) > 60:
# We found 60dB location! Wow.
sixty_db_location = i
break
if sixty_db_location > 0:
sixty_db_cutoff_frequency = self.app.data21[sixty_db_location].freq
self.sixty_db_label.setText(
format_frequency(sixty_db_cutoff_frequency))
elif ten_db_location != -1 and twenty_db_location != -1:
ten = self.app.data21[ten_db_location].freq
twenty = self.app.data21[twenty_db_location].freq
sixty_db_frequency = ten * \
10 ** (5 * (math.log10(twenty) - math.log10(ten)))
self.sixty_db_label.setText(
f"{format_frequency(sixty_db_frequency)} (derived)")
else:
self.sixty_db_label.setText("Not calculated")
if (ten_db_location > 0 and
twenty_db_location > 0 and
ten_db_location != twenty_db_location):
octave_attenuation, decade_attenuation = self.calculateRolloff(
ten_db_location, twenty_db_location)
self.db_per_octave_label.setText(
str(round(octave_attenuation, 3)) + " dB / octave")
self.db_per_decade_label.setText(
str(round(decade_attenuation, 3)) + " dB / decade")
else:
self.db_per_octave_label.setText("Not calculated")
self.db_per_decade_label.setText("Not calculated")
self.result_label.setText(
"Analysis complete (" + str(len(self.app.data)) + " points)")

Wyświetl plik

@ -1,153 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 QtWidgets
from scipy import signal
import numpy as np
from NanoVNASaver.Analysis import Analysis
logger = logging.getLogger(__name__)
class PeakSearchAnalysis(Analysis):
class QHLine(QtWidgets.QFrame):
def __init__(self):
super().__init__()
self.setFrameShape(QtWidgets.QFrame.HLine)
def __init__(self, app):
super().__init__(app)
self._widget = QtWidgets.QWidget()
outer_layout = QtWidgets.QFormLayout()
self._widget.setLayout(outer_layout)
self.rbtn_data_group = QtWidgets.QButtonGroup()
self.rbtn_data_vswr = QtWidgets.QRadioButton("VSWR")
self.rbtn_data_resistance = QtWidgets.QRadioButton("Resistance")
self.rbtn_data_reactance = QtWidgets.QRadioButton("Reactance")
self.rbtn_data_s21_gain = QtWidgets.QRadioButton("S21 Gain")
self.rbtn_data_group.addButton(self.rbtn_data_vswr)
self.rbtn_data_group.addButton(self.rbtn_data_resistance)
self.rbtn_data_group.addButton(self.rbtn_data_reactance)
self.rbtn_data_group.addButton(self.rbtn_data_s21_gain)
self.rbtn_data_vswr.setChecked(True)
self.rbtn_peak_group = QtWidgets.QButtonGroup()
self.rbtn_peak_positive = QtWidgets.QRadioButton("Positive")
self.rbtn_peak_negative = QtWidgets.QRadioButton("Negative")
self.rbtn_peak_both = QtWidgets.QRadioButton("Both")
self.rbtn_peak_group.addButton(self.rbtn_peak_positive)
self.rbtn_peak_group.addButton(self.rbtn_peak_negative)
self.rbtn_peak_group.addButton(self.rbtn_peak_both)
self.rbtn_peak_positive.setChecked(True)
self.input_number_of_peaks = QtWidgets.QSpinBox()
self.input_number_of_peaks.setValue(1)
self.input_number_of_peaks.setMinimum(1)
self.input_number_of_peaks.setMaximum(10)
self.checkbox_move_markers = QtWidgets.QCheckBox()
outer_layout.addRow(QtWidgets.QLabel("<b>Settings</b>"))
outer_layout.addRow("Data source", self.rbtn_data_vswr)
outer_layout.addRow("", self.rbtn_data_resistance)
outer_layout.addRow("", self.rbtn_data_reactance)
outer_layout.addRow("", self.rbtn_data_s21_gain)
outer_layout.addRow(PeakSearchAnalysis.QHLine())
outer_layout.addRow("Peak type", self.rbtn_peak_positive)
outer_layout.addRow("", self.rbtn_peak_negative)
# outer_layout.addRow("", self.rbtn_peak_both)
outer_layout.addRow(PeakSearchAnalysis.QHLine())
outer_layout.addRow("Max number of peaks", self.input_number_of_peaks)
outer_layout.addRow("Move markers", self.checkbox_move_markers)
outer_layout.addRow(PeakSearchAnalysis.QHLine())
outer_layout.addRow(QtWidgets.QLabel("<b>Results</b>"))
def runAnalysis(self):
count = self.input_number_of_peaks.value()
if self.rbtn_data_vswr.isChecked():
data = []
for d in self.app.data:
data.append(d.vswr)
elif self.rbtn_data_s21_gain.isChecked():
data = []
for d in self.app.data21:
data.append(d.gain)
else:
logger.warning("Searching for peaks on unknown data")
return
if self.rbtn_peak_positive.isChecked():
peaks, _ = signal.find_peaks(data, width=3, distance=3, prominence=1)
elif self.rbtn_peak_negative.isChecked():
peaks, _ = signal.find_peaks(np.array(data)*-1, width=3, distance=3, prominence=1)
# elif self.rbtn_peak_both.isChecked():
# peaks_max, _ = signal.find_peaks(data, width=3, distance=3, prominence=1)
# peaks_min, _ = signal.find_peaks(np.array(data)*-1, width=3, distance=3, prominence=1)
# peaks = np.concatenate((peaks_max, peaks_min))
else:
# Both is not yet in
logger.warning(
"Searching for peaks,"
" but neither looking at positive nor negative?")
return
# Having found the peaks, get the prominence data
for p in peaks:
logger.debug("Peak at %d", p)
prominences = signal.peak_prominences(data, peaks)[0]
logger.debug("%d prominences", len(prominences))
# Find the peaks with the most extreme values
# Alternately, allow the user to select "most prominent"?
indices = np.argpartition(prominences, -count)[-count:]
logger.debug("%d indices", len(indices))
for i in indices:
logger.debug("Index %d", i)
logger.debug("Prominence %f", prominences[i])
logger.debug("Index in sweep %d", peaks[i])
logger.debug("Frequency %d", self.app.data[peaks[i]].freq)
logger.debug("Value %f", data[peaks[i]])
if self.checkbox_move_markers:
if count > len(self.app.markers):
logger.warning("More peaks found than there are markers")
for i in range(min(count, len(self.app.markers))):
self.app.markers[i].setFrequency(
str(self.app.data[peaks[indices[i]]].freq))
self.app.markers[i].frequencyInput.setText(
str(self.app.data[peaks[indices[i]]].freq))
max_val = -10**10
max_idx = -1
for p in peaks:
if data[p] > max_val:
max_val = data[p]
max_idx = p
logger.debug("Max peak at %d, value %f", max_idx, max_val)
def reset(self):
pass

Wyświetl plik

@ -1,124 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 QtWidgets
import numpy as np
from NanoVNASaver.Analysis import Analysis, PeakSearchAnalysis
from NanoVNASaver.Formatting import format_frequency
logger = logging.getLogger(__name__)
class SimplePeakSearchAnalysis(Analysis):
def __init__(self, app):
super().__init__(app)
self._widget = QtWidgets.QWidget()
outer_layout = QtWidgets.QFormLayout()
self._widget.setLayout(outer_layout)
self.rbtn_data_group = QtWidgets.QButtonGroup()
self.rbtn_data_vswr = QtWidgets.QRadioButton("VSWR")
self.rbtn_data_resistance = QtWidgets.QRadioButton("Resistance")
self.rbtn_data_reactance = QtWidgets.QRadioButton("Reactance")
self.rbtn_data_s21_gain = QtWidgets.QRadioButton("S21 Gain")
self.rbtn_data_group.addButton(self.rbtn_data_vswr)
self.rbtn_data_group.addButton(self.rbtn_data_resistance)
self.rbtn_data_group.addButton(self.rbtn_data_reactance)
self.rbtn_data_group.addButton(self.rbtn_data_s21_gain)
self.rbtn_data_s21_gain.setChecked(True)
self.rbtn_peak_group = QtWidgets.QButtonGroup()
self.rbtn_peak_positive = QtWidgets.QRadioButton("Highest value")
self.rbtn_peak_negative = QtWidgets.QRadioButton("Lowest value")
self.rbtn_peak_group.addButton(self.rbtn_peak_positive)
self.rbtn_peak_group.addButton(self.rbtn_peak_negative)
self.rbtn_peak_positive.setChecked(True)
self.checkbox_move_marker = QtWidgets.QCheckBox()
outer_layout.addRow(QtWidgets.QLabel("<b>Settings</b>"))
outer_layout.addRow("Data source", self.rbtn_data_vswr)
outer_layout.addRow("", self.rbtn_data_resistance)
outer_layout.addRow("", self.rbtn_data_reactance)
outer_layout.addRow("", self.rbtn_data_s21_gain)
outer_layout.addRow(PeakSearchAnalysis.QHLine())
outer_layout.addRow("Peak type", self.rbtn_peak_positive)
outer_layout.addRow("", self.rbtn_peak_negative)
outer_layout.addRow(PeakSearchAnalysis.QHLine())
outer_layout.addRow("Move marker to peak", self.checkbox_move_marker)
outer_layout.addRow(PeakSearchAnalysis.QHLine())
outer_layout.addRow(QtWidgets.QLabel("<b>Results</b>"))
self.peak_frequency = QtWidgets.QLabel()
self.peak_value = QtWidgets.QLabel()
outer_layout.addRow("Peak frequency:", self.peak_frequency)
outer_layout.addRow("Peak value:", self.peak_value)
def runAnalysis(self):
if self.rbtn_data_vswr.isChecked():
suffix = ""
data = []
for d in self.app.data:
data.append(d.vswr)
elif self.rbtn_data_resistance.isChecked():
suffix = " \N{OHM SIGN}"
data = []
for d in self.app.data:
data.append(d.impedance().real)
elif self.rbtn_data_reactance.isChecked():
suffix = " \N{OHM SIGN}"
data = []
for d in self.app.data:
data.append(d.impedance().imag)
elif self.rbtn_data_s21_gain.isChecked():
suffix = " dB"
data = []
for d in self.app.data21:
data.append(d.gain)
else:
logger.warning("Searching for peaks on unknown data")
return
if len(data) == 0:
return
if self.rbtn_peak_positive.isChecked():
idx_peak = np.argmax(data)
elif self.rbtn_peak_negative.isChecked():
idx_peak = np.argmin(data)
else:
# Both is not yet in
logger.warning(
"Searching for peaks,"
" but neither looking at positive nor negative?")
return
self.peak_frequency.setText(
format_frequency(self.app.data[idx_peak].freq))
self.peak_value.setText(str(round(data[idx_peak], 3)) + suffix)
if self.checkbox_move_marker.isChecked() and len(self.app.markers) >= 1:
self.app.markers[0].setFrequency(str(self.app.data[idx_peak].freq))
self.app.markers[0].frequencyInput.setText(
format_frequency(self.app.data[idx_peak].freq))

Wyświetl plik

@ -1,142 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 QtWidgets
import numpy as np
from NanoVNASaver.Analysis import Analysis, PeakSearchAnalysis
from NanoVNASaver.Formatting import format_frequency
logger = logging.getLogger(__name__)
class VSWRAnalysis(Analysis):
class QHLine(QtWidgets.QFrame):
def __init__(self):
super().__init__()
self.setFrameShape(QtWidgets.QFrame.HLine)
def __init__(self, app):
super().__init__(app)
self._widget = QtWidgets.QWidget()
self.layout = QtWidgets.QFormLayout()
self._widget.setLayout(self.layout)
self.input_vswr_limit = QtWidgets.QDoubleSpinBox()
self.input_vswr_limit.setValue(1.5)
self.input_vswr_limit.setSingleStep(0.1)
self.input_vswr_limit.setMinimum(1)
self.input_vswr_limit.setMaximum(25)
self.input_vswr_limit.setDecimals(2)
self.checkbox_move_marker = QtWidgets.QCheckBox()
self.layout.addRow(QtWidgets.QLabel("<b>Settings</b>"))
self.layout.addRow("VSWR limit", self.input_vswr_limit)
self.layout.addRow(VSWRAnalysis.QHLine())
self.results_label = QtWidgets.QLabel("<b>Results</b>")
self.layout.addRow(self.results_label)
def runAnalysis(self):
max_dips_shown = 3
data = []
for d in self.app.data:
data.append(d.vswr)
# min_idx = np.argmin(data)
#
# logger.debug("Minimum at %d", min_idx)
# logger.debug("Value at minimum: %f", data[min_idx])
# logger.debug("Frequency: %d", self.app.data[min_idx].freq)
#
# if self.checkbox_move_marker.isChecked():
# self.app.markers[0].setFrequency(str(self.app.data[min_idx].freq))
# self.app.markers[0].frequencyInput.setText(str(self.app.data[min_idx].freq))
minimums = []
min_start = -1
min_idx = -1
threshold = self.input_vswr_limit.value()
min_val = threshold
for i, d in enumerate(data):
if d < threshold and i < len(data)-1:
if d < min_val:
min_val = d
min_idx = i
if min_start == -1:
min_start = i
elif min_start != -1:
# We are above the threshold, and were in a section that was below
minimums.append((min_start, min_idx, i-1))
min_start = -1
min_idx = -1
min_val = threshold
logger.debug("Found %d sections under %f threshold", len(minimums), threshold)
results_header = self.layout.indexOf(self.results_label)
logger.debug("Results start at %d, out of %d", results_header, self.layout.rowCount())
for i in range(results_header, self.layout.rowCount()):
self.layout.removeRow(self.layout.rowCount()-1)
if len(minimums) > max_dips_shown:
self.layout.addRow(QtWidgets.QLabel("<b>More than " + str(max_dips_shown) +
" dips found. Lowest shown.</b>"))
dips = []
for m in minimums:
start, lowest, end = m
dips.append(data[lowest])
best_dips = []
for i in range(max_dips_shown):
min_idx = np.argmin(dips)
best_dips.append(minimums[min_idx])
dips.remove(dips[min_idx])
minimums.remove(minimums[min_idx])
minimums = best_dips
if len(minimums) > 0:
for m in minimums:
start, lowest, end = m
if start != end:
logger.debug(
"Section from %d to %d, lowest at %d", start, end, lowest)
self.layout.addRow("Start", QtWidgets.QLabel(
format_frequency(self.app.data[start].freq)))
self.layout.addRow(
"Minimum",
QtWidgets.QLabel(
f"{format_frequency(self.app.data[lowest].freq)}"
f" ({round(data[lowest], 2)})"))
self.layout.addRow("End", QtWidgets.QLabel(
format_frequency(self.app.data[end].freq)))
self.layout.addRow(
"Span",
QtWidgets.QLabel(
format_frequency(self.app.data[end].freq -
self.app.data[start].freq)))
self.layout.addWidget(PeakSearchAnalysis.QHLine())
else:
self.layout.addRow("Low spot", QtWidgets.QLabel(
format_frequency(self.app.data[lowest].freq)))
self.layout.addWidget(PeakSearchAnalysis.QHLine())
# Remove the final separator line
self.layout.removeRow(self.layout.rowCount()-1)
else:
self.layout.addRow(QtWidgets.QLabel(
"No areas found with VSWR below " + str(round(threshold, 2)) + "."))

Wyświetl plik

@ -1,8 +0,0 @@
from .Analysis import Analysis
from .BandPassAnalysis import BandPassAnalysis
from .BandStopAnalysis import BandStopAnalysis
from .HighPassAnalysis import HighPassAnalysis
from .LowPassAnalysis import LowPassAnalysis
from .PeakSearchAnalysis import PeakSearchAnalysis
from .SimplePeakSearchAnalysis import SimplePeakSearchAnalysis
from .VSWRAnalysis import VSWRAnalysis

Wyświetl plik

@ -1,345 +0,0 @@
# NanoVNASaver - a python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
import os
from typing import List
import numpy as np
from .RFTools import Datapoint
logger = logging.getLogger(__name__)
class Calibration:
notes = []
s11short: List[Datapoint] = []
s11open: List[Datapoint] = []
s11load: List[Datapoint] = []
s21through: List[Datapoint] = []
s21isolation: List[Datapoint] = []
frequencies = []
# 1-port
e00 = [] # Directivity
e11 = [] # Port match
deltaE = [] # Tracking
# 2-port
e30 = [] # Port match
e10e32 = [] # Transmission
shortIdeal = np.complex(-1, 0)
useIdealShort = True
shortL0 = 5.7 * 10E-12
shortL1 = -8960 * 10E-24
shortL2 = -1100 * 10E-33
shortL3 = -41200 * 10E-42
shortLength = -34.2 # Picoseconds
# These numbers look very large, considering what Keysight suggests their numbers are.
useIdealOpen = True
openIdeal = np.complex(1, 0)
openC0 = 2.1 * 10E-14 # Subtract 50fF for the nanoVNA calibration if nanoVNA is calibrated?
openC1 = 5.67 * 10E-23
openC2 = -2.39 * 10E-31
openC3 = 2.0 * 10E-40
openLength = 0
useIdealLoad = True
loadR = 25
loadL = 0
loadC = 0
loadLength = 0
loadIdeal = np.complex(0, 0)
useIdealThrough = True
throughLength = 0
isCalculated = False
source = "Manual"
def isValid2Port(self):
valid = len(self.s21through) > 0 and len(self.s21isolation) > 0 and self.isValid1Port()
valid &= len(self.s21through) == len(self.s21isolation) == len(self.s11short)
return valid
def isValid1Port(self):
valid = len(self.s11short) > 0 and len(self.s11open) > 0 and len(self.s11load) > 0
valid &= len(self.s11short) == len(self.s11open) == len(self.s11load)
return valid
def calculateCorrections(self) -> (bool, str):
if not self.isValid1Port():
logger.warning("Tried to calibrate from insufficient data.")
if len(self.s11short) == 0 or len(self.s11open) == 0 or len(self.s11load) == 0:
return (False,
"All of short, open and load calibration steps"
"must be completed for calibration to be applied.")
return False, "All calibration data sets must be the same size."
self.frequencies = [int] * len(self.s11short)
self.e00 = [np.complex] * len(self.s11short)
self.e11 = [np.complex] * len(self.s11short)
self.deltaE = [np.complex] * len(self.s11short)
self.e30 = [np.complex] * len(self.s11short)
self.e10e32 = [np.complex] * len(self.s11short)
logger.debug("Calculating calibration for %d points.", len(self.s11short))
if self.useIdealShort:
logger.debug("Using ideal values.")
else:
logger.debug("Using calibration set values.")
if self.isValid2Port():
logger.debug("Calculating 2-port calibration.")
else:
logger.debug("Calculating 1-port calibration.")
for i in range(len(self.s11short)):
self.frequencies[i] = self.s11short[i].freq
f = self.s11short[i].freq
pi = math.pi
if self.useIdealShort:
g1 = self.shortIdeal
else:
Zsp = np.complex(0, 1) * 2 * pi * f * (self.shortL0 +
self.shortL1 * f +
self.shortL2 * f**2 +
self.shortL3 * f**3)
gammaShort = ((Zsp/50) - 1) / ((Zsp/50) + 1)
# (lower case) gamma = 2*pi*f
# e^j*2*gamma*length
# Referencing https://arxiv.org/pdf/1606.02446.pdf (18) - (21)
g1 = gammaShort * np.exp(
np.complex(0, 1) * 2 * 2 * math.pi * f * self.shortLength * -1)
if self.useIdealOpen:
g2 = self.openIdeal
else:
divisor = (
2 * pi * f * (
self.openC0 + self.openC1 * f +
self.openC2 * f**2 + self.openC3 * f**3)
)
if divisor != 0:
Zop = np.complex(0, -1) / divisor
gammaOpen = ((Zop/50) - 1) / ((Zop/50) + 1)
g2 = gammaOpen * np.exp(
np.complex(0, 1) * 2 * 2 * math.pi * f * self.openLength * -1)
else:
g2 = self.openIdeal
if self.useIdealLoad:
g3 = self.loadIdeal
else:
Zl = self.loadR + (np.complex(0, 1) * 2 * math.pi * f * self.loadL)
g3 = ((Zl/50)-1) / ((Zl/50)+1)
g3 = g3 * np.exp(
np.complex(0, 1) * 2 * 2 * math.pi * f * self.loadLength * -1)
gm1 = np.complex(self.s11short[i].re, self.s11short[i].im)
gm2 = np.complex(self.s11open[i].re, self.s11open[i].im)
gm3 = np.complex(self.s11load[i].re, self.s11load[i].im)
try:
denominator = (
g1 * (g2 - g3) * gm1 +
g2 * g3 * gm2 -
g2 * g3 * gm3 -
(g2 * gm2 - g3 * gm3) * g1)
self.e00[i] = - (
(g2 * gm3 - g3 * gm3) * g1 * gm2 -
(g2 * g3 * gm2 - g2 * g3 * gm3 -
(g3 * gm2 - g2 * gm3) * g1) * gm1
) / denominator
self.e11[i] = (
(g2 - g3) * gm1 - g1 * (gm2 - gm3) +
g3 * gm2 - g2 * gm3
) / denominator
self.deltaE[i] = - (
(g1 * (gm2 - gm3) - g2 * gm2 + g3 * gm3) * gm1 +
(g2 * gm3 - g3 * gm3) * gm2
) / denominator
except ZeroDivisionError:
self.isCalculated = False
logger.error(
"Division error - did you use the same measurement"
" for two of short, open and load?")
logger.debug(
"Division error at index %d"
" Short == Load: %s"
" Short == Open: %s"
" Open == Load: %s",
i,
self.s11short[i] == self.s11load[i],
self.s11short[i] == self.s11open[i],
self.s11open[i] == self.s11load[i])
return (self.isCalculated,
f"Two of short, open and load returned the same"
f" values at frequency {self.s11open[i].freq}Hz.")
if self.isValid2Port():
self.e30[i] = np.complex(
self.s21isolation[i].re, self.s21isolation[i].im)
s21m = np.complex(self.s21through[i].re, self.s21through[i].im)
if not self.useIdealThrough:
gammaThrough = np.exp(
np.complex(0, 1) * 2 * math.pi * self.throughLength * f * -1)
s21m = s21m / gammaThrough
self.e10e32[i] = (s21m - self.e30[i]) * (1 - (self.e11[i]*self.e11[i]))
self.isCalculated = True
logger.debug("Calibration correctly calculated.")
return self.isCalculated, "Calibration successful."
def correct11(self, re, im, freq):
s11m = np.complex(re, im)
distance = 10**10
index = 0
for i in range(len(self.s11short)):
if abs(self.s11short[i].freq - freq) < distance:
index = i
distance = abs(self.s11short[i].freq - freq)
# TODO: Interpolate with the adjacent data point to get better corrections?
s11 = (s11m - self.e00[index]) / ((s11m * self.e11[index]) - self.deltaE[index])
return s11.real, s11.imag
def correct21(self, re, im, freq):
s21m = np.complex(re, im)
distance = 10**10
index = 0
for i in range(len(self.s21through)):
if abs(self.s21through[i].freq - freq) < distance:
index = i
distance = abs(self.s21through[i].freq - freq)
s21 = (s21m - self.e30[index]) / self.e10e32[index]
return s21.real, s21.imag
@staticmethod
def correctDelay11(d: Datapoint, delay):
input_val = np.complex(d.re, d.im)
output = input_val * np.exp(np.complex(0, 1) * 2 * 2 * math.pi * d.freq * delay * -1)
return Datapoint(d.freq, output.real, output.imag)
@staticmethod
def correctDelay21(d: Datapoint, delay):
input_val = np.complex(d.re, d.im)
output = input_val * np.exp(np.complex(0, 1) * 2 * math.pi * d.freq * delay * -1)
return Datapoint(d.freq, output.real, output.imag)
def saveCalibration(self, filename):
# Save the calibration data to file
if filename == "" or not self.isValid1Port():
return False
try:
file = open(filename, "w+")
file.write("# Calibration data for NanoVNA-Saver\n")
for note in self.notes:
file.write(f"! {note}\n")
file.write(
"# Hz ShortR ShortI OpenR OpenI LoadR LoadI"
" ThroughR ThroughI IsolationR IsolationI\n")
for i in range(len(self.s11short)):
freq = str(self.s11short[i].freq)
shortr = str(self.s11short[i].re)
shorti = str(self.s11short[i].im)
openr = str(self.s11open[i].re)
openi = str(self.s11open[i].im)
loadr = str(self.s11load[i].re)
loadi = str(self.s11load[i].im)
file.write(" ".join((freq, shortr, shorti, openr, openi, loadr, loadi)))
if self.isValid2Port():
throughr = str(self.s21through[i].re)
throughi = str(self.s21through[i].im)
isolationr = str(self.s21isolation[i].re)
isolationi = str(self.s21isolation[i].im)
file.write(" ".join((throughr, throughi, isolationr, isolationi)))
file.write("\n")
file.close()
return True
except Exception as e:
logger.exception("Error saving calibration data: %s", e)
return False
def loadCalibration(self, filename):
# Load calibration data from file
if filename == "":
return
self.source = os.path.basename(filename)
self.s11short = []
self.s11open = []
self.s11load = []
self.s21through = []
self.s21isolation = []
self.notes = []
try:
file = open(filename, "r")
lines = file.readlines()
parsed_header = False
for line in lines:
line = line.strip()
if line.startswith("!"):
note = line[2:]
self.notes.append(note)
continue
if line.startswith("#") and not parsed_header:
# Check that this is a valid header
if line == ("# Hz ShortR ShortI OpenR OpenI LoadR LoadI"
" ThroughR ThroughI IsolationR IsolationI"):
parsed_header = True
continue
if not parsed_header:
logger.warning(
"Warning: Read line without having read header: %s", line)
continue
try:
if line.count(" ") == 6:
freq, shortr, shorti, openr, openi, loadr, loadi = line.split(
" ")
self.s11short.append(
Datapoint(int(freq), float(shortr), float(shorti)))
self.s11open.append(
Datapoint(int(freq), float(openr), float(openi)))
self.s11load.append(
Datapoint(int(freq), float(loadr), float(loadi)))
else:
(freq, shortr, shorti, openr, openi, loadr, loadi,
throughr, throughi, isolationr, isolationi) = line.split(" ")
self.s11short.append(
Datapoint(int(freq), float(shortr), float(shorti)))
self.s11open.append(
Datapoint(int(freq), float(openr), float(openi)))
self.s11load.append(
Datapoint(int(freq), float(loadr), float(loadi)))
self.s21through.append(
Datapoint(int(freq), float(throughr), float(throughi)))
self.s21isolation.append(
Datapoint(int(freq), float(isolationr), float(isolationi)))
except ValueError as e:
logger.exception(
"Error parsing calibration data \"%s\": %s", line, e)
file.close()
except Exception as e:
logger.exception("Failed loading calibration data: %s", e)

Wyświetl plik

@ -1,312 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
import logging
from typing import List
from PyQt5 import QtWidgets, QtGui
from NanoVNASaver.RFTools import Datapoint
from .Frequency import FrequencyChart
from .LogMag import LogMagChart
logger = logging.getLogger(__name__)
class CombinedLogMagChart(FrequencyChart):
def __init__(self, name=""):
super().__init__(name)
self.leftMargin = 30
self.chartWidth = 250
self.chartHeight = 250
self.minDisplayValue = -80
self.maxDisplayValue = 10
self.data11: List[Datapoint] = []
self.data21: List[Datapoint] = []
self.reference11: List[Datapoint] = []
self.reference21: List[Datapoint] = []
self.minValue = 0
self.maxValue = 1
self.span = 1
self.isInverted = False
self.setMinimumSize(self.chartWidth + self.rightMargin + self.leftMargin,
self.chartHeight + self.topMargin + self.bottomMargin)
self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding))
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, self.backgroundColor)
self.setPalette(pal)
self.setAutoFillBackground(True)
def setCombinedData(self, data11, data21):
self.data11 = data11
self.data21 = data21
self.update()
def setCombinedReference(self, data11, data21):
self.reference11 = data11
self.reference21 = data21
self.update()
def resetReference(self):
self.reference11 = []
self.reference21 = []
self.update()
def resetDisplayLimits(self):
self.reference11 = []
self.reference21 = []
self.update()
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(self.textColor))
qp.drawText(int(round(self.chartWidth / 2)) - 20, 15, self.name + " (dB)")
qp.drawText(10, 15, "S11")
qp.drawText(self.leftMargin + self.chartWidth - 8, 15, "S21")
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin, self.topMargin - 5,
self.leftMargin, self.topMargin+self.chartHeight+5)
qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight,
self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
def drawValues(self, qp: QtGui.QPainter):
if len(self.data11) == 0 and len(self.reference11) == 0:
return
pen = QtGui.QPen(self.sweepColor)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(self.sweepColor)
line_pen.setWidth(self.lineThickness)
highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255))
highlighter.setWidth(1)
if not self.fixedSpan:
if len(self.data11) > 0:
fstart = self.data11[0].freq
fstop = self.data11[len(self.data11)-1].freq
else:
fstart = self.reference11[0].freq
fstop = self.reference11[len(self.reference11) - 1].freq
self.fstart = fstart
self.fstop = fstop
else:
fstart = self.fstart = self.minFrequency
fstop = self.fstop = self.maxFrequency
# Draw bands if required
if self.bands.enabled:
self.drawBands(qp, fstart, fstop)
if self.fixedValues:
maxValue = self.maxDisplayValue
minValue = self.minDisplayValue
self.maxValue = maxValue
self.minValue = minValue
else:
# Find scaling
minValue = 100
maxValue = 0
for d in self.data11:
logmag = self.logMag(d)
if math.isinf(logmag):
continue
if logmag > maxValue:
maxValue = logmag
if logmag < minValue:
minValue = logmag
for d in self.data21:
logmag = self.logMag(d)
if math.isinf(logmag):
continue
if logmag > maxValue:
maxValue = logmag
if logmag < minValue:
minValue = logmag
for d in self.reference11:
if d.freq < self.fstart or d.freq > self.fstop:
continue
logmag = self.logMag(d)
if math.isinf(logmag):
continue
if logmag > maxValue:
maxValue = logmag
if logmag < minValue:
minValue = logmag
for d in self.reference21:
if d.freq < self.fstart or d.freq > self.fstop:
continue
logmag = self.logMag(d)
if math.isinf(logmag):
continue
if logmag > maxValue:
maxValue = logmag
if logmag < minValue:
minValue = logmag
minValue = 10*math.floor(minValue/10)
self.minValue = minValue
maxValue = 10*math.ceil(maxValue/10)
self.maxValue = maxValue
span = maxValue-minValue
if span == 0:
span = 0.01
self.span = span
if self.span >= 50:
# Ticks per 10dB step
tick_count = math.floor(self.span/10)
first_tick = math.ceil(self.minValue/10) * 10
tick_step = 10
if first_tick == minValue:
first_tick += 10
elif self.span >= 20:
# 5 dB ticks
tick_count = math.floor(self.span/5)
first_tick = math.ceil(self.minValue/5) * 5
tick_step = 5
if first_tick == minValue:
first_tick += 5
elif self.span >= 10:
# 2 dB ticks
tick_count = math.floor(self.span/2)
first_tick = math.ceil(self.minValue/2) * 2
tick_step = 2
if first_tick == minValue:
first_tick += 2
elif self.span >= 5:
# 1dB ticks
tick_count = math.floor(self.span)
first_tick = math.ceil(minValue)
tick_step = 1
if first_tick == minValue:
first_tick += 1
elif self.span >= 2:
# .5 dB ticks
tick_count = math.floor(self.span*2)
first_tick = math.ceil(minValue*2) / 2
tick_step = .5
if first_tick == minValue:
first_tick += .5
else:
# .1 dB ticks
tick_count = math.floor(self.span*10)
first_tick = math.ceil(minValue*10) / 10
tick_step = .1
if first_tick == minValue:
first_tick += .1
for i in range(tick_count):
db = first_tick + i * tick_step
y = self.topMargin + round((maxValue - db)/span*self.chartHeight)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin-5, y, self.leftMargin+self.chartWidth, y)
if db > minValue and db != maxValue:
qp.setPen(QtGui.QPen(self.textColor))
if tick_step < 1:
dbstr = str(round(db, 1))
else:
dbstr = str(db)
qp.drawText(3, y + 4, dbstr)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, self.topMargin,
self.leftMargin + self.chartWidth, self.topMargin)
qp.setPen(self.textColor)
qp.drawText(3, self.topMargin + 4, str(maxValue))
qp.drawText(3, self.chartHeight+self.topMargin, str(minValue))
self.drawFrequencyTicks(qp)
qp.setPen(self.swrColor)
for vswr in self.swrMarkers:
if vswr <= 1:
continue
logMag = 20 * math.log10((vswr-1)/(vswr+1))
if self.isInverted:
logMag = logMag * -1
y = self.topMargin + round((self.maxValue - logMag) /
self.span * self.chartHeight)
qp.drawLine(self.leftMargin, y,
self.leftMargin + self.chartWidth, y)
qp.drawText(self.leftMargin + 3, y - 1, "VSWR: " + str(vswr))
if len(self.data11) > 0:
c = QtGui.QColor(self.sweepColor)
c.setAlpha(255)
pen = QtGui.QPen(c)
pen.setWidth(2)
qp.setPen(pen)
qp.drawLine(33, 9, 38, 9)
c = QtGui.QColor(self.secondarySweepColor)
c.setAlpha(255)
pen = QtGui.QPen(c)
pen.setWidth(2)
qp.setPen(pen)
qp.drawLine(self.leftMargin + self.chartWidth - 20, 9,
self.leftMargin + self.chartWidth - 15, 9)
if len(self.reference11) > 0:
c = QtGui.QColor(self.referenceColor)
c.setAlpha(255)
pen = QtGui.QPen(c)
pen.setWidth(2)
qp.setPen(pen)
qp.drawLine(33, 14, 38, 14)
c = QtGui.QColor(self.secondaryReferenceColor)
c.setAlpha(255)
pen = QtGui.QPen(c)
pen.setWidth(2)
qp.setPen(pen)
qp.drawLine(self.leftMargin + self.chartWidth - 20, 14,
self.leftMargin + self.chartWidth - 15, 14)
self.drawData(qp, self.data11, self.sweepColor)
self.drawData(qp, self.data21, self.secondarySweepColor)
self.drawData(qp, self.reference11, self.referenceColor)
self.drawData(qp, self.reference21, self.secondaryReferenceColor)
self.drawMarkers(qp, data=self.data11)
self.drawMarkers(qp, data=self.data21)
def getYPosition(self, d: Datapoint) -> int:
logMag = self.logMag(d)
if math.isinf(logMag):
return None
return self.topMargin + round((self.maxValue - logMag) / self.span * self.chartHeight)
def valueAtPosition(self, y) -> List[float]:
absy = y - self.topMargin
val = -1 * ((absy / self.chartHeight * self.span) - self.maxValue)
return [val]
def logMag(self, p: Datapoint) -> float:
if self.isInverted:
return -p.gain
return p.gain
def copy(self):
new_chart: LogMagChart = super().copy()
new_chart.isInverted = self.isInverted
new_chart.span = self.span
new_chart.data11 = self.data11
new_chart.data21 = self.data21
new_chart.reference11 = self.reference11
new_chart.reference21 = self.reference21
return new_chart

Wyświetl plik

@ -1,286 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
import logging
from typing import List
from PyQt5 import QtWidgets, QtGui
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.SITools import Format, Value
from .Frequency import FrequencyChart
logger = logging.getLogger(__name__)
class CapacitanceChart(FrequencyChart):
def __init__(self, name=""):
super().__init__(name)
self.leftMargin = 30
self.chartWidth = 250
self.chartHeight = 250
self.minDisplayValue = 0
self.maxDisplayValue = 100
self.minValue = -1
self.maxValue = 1
self.span = 1
self.setMinimumSize(self.chartWidth + self.rightMargin + self.leftMargin,
self.chartHeight + self.topMargin + self.bottomMargin)
self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding))
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, self.backgroundColor)
self.setPalette(pal)
self.setAutoFillBackground(True)
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(self.textColor))
qp.drawText(3, 15, self.name + " (F)")
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5)
qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight,
self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
self.drawTitle(qp)
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
return
pen = QtGui.QPen(self.sweepColor)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(self.sweepColor)
line_pen.setWidth(self.lineThickness)
highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255))
highlighter.setWidth(1)
if not self.fixedSpan:
if len(self.data) > 0:
fstart = self.data[0].freq
fstop = self.data[len(self.data)-1].freq
else:
fstart = self.reference[0].freq
fstop = self.reference[len(self.reference) - 1].freq
self.fstart = fstart
self.fstop = fstop
else:
fstart = self.fstart = self.minFrequency
fstop = self.fstop = self.maxFrequency
# Draw bands if required
if self.bands.enabled:
self.drawBands(qp, fstart, fstop)
if self.fixedValues:
maxValue = self.maxDisplayValue / 10e11
minValue = self.minDisplayValue / 10e11
self.maxValue = maxValue
self.minValue = minValue
else:
# Find scaling
minValue = 1
maxValue = -1
for d in self.data:
val = d.capacitiveEquivalent()
if val > maxValue:
maxValue = val
if val < minValue:
minValue = val
for d in self.reference: # Also check min/max for the reference sweep
if d.freq < self.fstart or d.freq > self.fstop:
continue
val = d.capacitiveEquivalent()
if val > maxValue:
maxValue = val
if val < minValue:
minValue = val
self.maxValue = maxValue
self.minValue = minValue
span = maxValue - minValue
if span == 0:
logger.info("Span is zero for CapacitanceChart, setting to a small value.")
span = 1e-15
self.span = span
target_ticks = math.floor(self.chartHeight / 60)
fmt = Format(max_nr_digits=1)
for i in range(target_ticks):
val = minValue + (i / target_ticks) * span
y = self.topMargin + round((self.maxValue - val) / self.span * self.chartHeight)
qp.setPen(self.textColor)
if val != minValue:
valstr = str(Value(val, fmt=fmt))
qp.drawText(3, y + 3, valstr)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.chartWidth, y)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, self.topMargin,
self.leftMargin + self.chartWidth, self.topMargin)
qp.setPen(self.textColor)
qp.drawText(3, self.topMargin + 4, str(Value(maxValue, fmt=fmt)))
qp.drawText(3, self.chartHeight+self.topMargin, str(Value(minValue, fmt=fmt)))
self.drawFrequencyTicks(qp)
self.drawData(qp, self.data, self.sweepColor)
self.drawData(qp, self.reference, self.referenceColor)
self.drawMarkers(qp)
def getYPosition(self, d: Datapoint) -> int:
return (
self.topMargin +
round((self.maxValue - d.capacitiveEquivalent()) /
self.span * self.chartHeight))
def valueAtPosition(self, y) -> List[float]:
absy = y - self.topMargin
val = -1 * ((absy / self.chartHeight * self.span) - self.maxValue)
return [val * 10e11]
def copy(self):
new_chart: CapacitanceChart = super().copy()
new_chart.span = self.span
return new_chart
class InductanceChart(FrequencyChart):
def __init__(self, name=""):
super().__init__(name)
self.leftMargin = 30
self.chartWidth = 250
self.chartHeight = 250
self.minDisplayValue = 0
self.maxDisplayValue = 100
self.minValue = -1
self.maxValue = 1
self.span = 1
self.setMinimumSize(self.chartWidth + self.rightMargin + self.leftMargin,
self.chartHeight + self.topMargin + self.bottomMargin)
self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding))
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, self.backgroundColor)
self.setPalette(pal)
self.setAutoFillBackground(True)
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(self.textColor))
qp.drawText(3, 15, self.name + " (H)")
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5)
qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight,
self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
self.drawTitle(qp)
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
return
pen = QtGui.QPen(self.sweepColor)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(self.sweepColor)
line_pen.setWidth(self.lineThickness)
highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255))
highlighter.setWidth(1)
if not self.fixedSpan:
if len(self.data) > 0:
fstart = self.data[0].freq
fstop = self.data[len(self.data)-1].freq
else:
fstart = self.reference[0].freq
fstop = self.reference[len(self.reference) - 1].freq
self.fstart = fstart
self.fstop = fstop
else:
fstart = self.fstart = self.minFrequency
fstop = self.fstop = self.maxFrequency
# Draw bands if required
if self.bands.enabled:
self.drawBands(qp, fstart, fstop)
if self.fixedValues:
maxValue = self.maxDisplayValue / 10e11
minValue = self.minDisplayValue / 10e11
self.maxValue = maxValue
self.minValue = minValue
else:
# Find scaling
minValue = 1
maxValue = -1
for d in self.data:
val = d.inductiveEquivalent()
if val > maxValue:
maxValue = val
if val < minValue:
minValue = val
for d in self.reference: # Also check min/max for the reference sweep
if d.freq < self.fstart or d.freq > self.fstop:
continue
val = d.inductiveEquivalent()
if val > maxValue:
maxValue = val
if val < minValue:
minValue = val
self.maxValue = maxValue
self.minValue = minValue
span = maxValue - minValue
if span == 0:
logger.info("Span is zero for CapacitanceChart, setting to a small value.")
span = 1e-15
self.span = span
target_ticks = math.floor(self.chartHeight / 60)
fmt = Format(max_nr_digits=1)
for i in range(target_ticks):
val = minValue + (i / target_ticks) * span
y = self.topMargin + round((self.maxValue - val) / self.span * self.chartHeight)
qp.setPen(self.textColor)
if val != minValue:
valstr = str(Value(val, fmt=fmt))
qp.drawText(3, y + 3, valstr)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.chartWidth, y)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, self.topMargin,
self.leftMargin + self.chartWidth, self.topMargin)
qp.setPen(self.textColor)
qp.drawText(3, self.topMargin + 4, str(Value(maxValue, fmt=fmt)))
qp.drawText(3, self.chartHeight+self.topMargin, str(Value(minValue, fmt=fmt)))
self.drawFrequencyTicks(qp)
self.drawData(qp, self.data, self.sweepColor)
self.drawData(qp, self.reference, self.referenceColor)
self.drawMarkers(qp)
def getYPosition(self, d: Datapoint) -> int:
return (self.topMargin +
round((self.maxValue - d.inductiveEquivalent()) /
self.span * self.chartHeight))
def valueAtPosition(self, y) -> List[float]:
absy = y - self.topMargin
val = -1 * ((absy / self.chartHeight * self.span) - self.maxValue)
return [val * 10e11]
def copy(self):
new_chart: InductanceChart = super().copy()
new_chart.span = self.span
return new_chart

Wyświetl plik

@ -1,322 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
from typing import List, Set
import logging
from PyQt5 import QtWidgets, QtGui, QtCore
from PyQt5.QtCore import pyqtSignal
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.Marker import Marker
logger = logging.getLogger(__name__)
class Chart(QtWidgets.QWidget):
sweepColor = QtCore.Qt.darkYellow
secondarySweepColor = QtCore.Qt.darkMagenta
referenceColor: QtGui.QColor = QtGui.QColor(QtCore.Qt.blue)
referenceColor.setAlpha(64)
secondaryReferenceColor: QtGui.QColor = QtGui.QColor(QtCore.Qt.blue)
secondaryReferenceColor.setAlpha(64)
backgroundColor: QtGui.QColor = QtGui.QColor(QtCore.Qt.white)
foregroundColor: QtGui.QColor = QtGui.QColor(QtCore.Qt.lightGray)
textColor: QtGui.QColor = QtGui.QColor(QtCore.Qt.black)
swrColor: QtGui.QColor = QtGui.QColor(QtCore.Qt.red)
swrColor.setAlpha(128)
data: List[Datapoint] = []
reference: List[Datapoint] = []
markers: List[Marker] = []
swrMarkers: Set[float] = set()
bands = None
draggedMarker: Marker = None
name = ""
sweepTitle = ""
drawLines = False
minChartHeight = 200
minChartWidth = 200
chartWidth = minChartWidth
chartHeight = minChartHeight
lineThickness = 1
pointSize = 2
markerSize = 3
drawMarkerNumbers = False
markerAtTip = False
filledMarkers = False
draggedBox = False
draggedBoxStart = (0, 0)
draggedBoxCurrent = (-1, -1)
moveStartX = -1
moveStartY = -1
isPopout = False
popoutRequested = pyqtSignal(object)
def __init__(self, name):
super().__init__()
self.name = name
self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
self.action_save_screenshot = QtWidgets.QAction("Save image")
self.action_save_screenshot.triggered.connect(self.saveScreenshot)
self.addAction(self.action_save_screenshot)
self.action_popout = QtWidgets.QAction("Popout chart")
self.action_popout.triggered.connect(lambda: self.popoutRequested.emit(self))
self.addAction(self.action_popout)
self.swrMarkers = set()
def setSweepColor(self, color: QtGui.QColor):
self.sweepColor = color
self.update()
def setSecondarySweepColor(self, color: QtGui.QColor):
self.secondarySweepColor = color
self.update()
def setReferenceColor(self, color: QtGui.QColor):
self.referenceColor = color
self.update()
def setSecondaryReferenceColor(self, color: QtGui.QColor):
self.secondaryReferenceColor = color
self.update()
def setBackgroundColor(self, color: QtGui.QColor):
self.backgroundColor = color
pal = self.palette()
pal.setColor(QtGui.QPalette.Background, color)
self.setPalette(pal)
self.update()
def setForegroundColor(self, color: QtGui.QColor):
self.foregroundColor = color
self.update()
def setTextColor(self, color: QtGui.QColor):
self.textColor = color
self.update()
def setReference(self, data):
self.reference = data
self.update()
def resetReference(self):
self.reference = []
self.update()
def setData(self, data):
self.data = data
self.update()
def setMarkers(self, markers):
self.markers = markers
def setBands(self, bands):
self.bands = bands
def setLineThickness(self, thickness):
self.lineThickness = thickness
self.update()
def setPointSize(self, size):
self.pointSize = size
self.update()
def setMarkerSize(self, size):
self.markerSize = size
self.update()
def setSweepTitle(self, title):
self.sweepTitle = title
self.update()
def getActiveMarker(self) -> Marker:
if self.draggedMarker is not None:
return self.draggedMarker
for m in self.markers:
if m.isMouseControlledRadioButton.isChecked():
return m
return None
def getNearestMarker(self, x, y) -> Marker:
if len(self.data) == 0:
return None
shortest = 10**6
nearest = None
for m in self.markers:
mx, my = self.getPosition(self.data[m.location])
dx = abs(x - mx)
dy = abs(y - my)
distance = math.sqrt(dx**2 + dy**2)
if distance < shortest:
shortest = distance
nearest = m
return nearest
def getYPosition(self, d: Datapoint) -> int:
return 0
def getXPosition(self, d: Datapoint) -> int:
return 0
def getPosition(self, d: Datapoint) -> (int, int):
return self.getXPosition(d), self.getYPosition(d)
def setDrawLines(self, draw_lines):
self.drawLines = draw_lines
self.update()
def setDrawMarkerNumbers(self, draw_marker_numbers):
self.drawMarkerNumbers = draw_marker_numbers
self.update()
def setMarkerAtTip(self, marker_at_tip):
self.markerAtTip = marker_at_tip
self.update()
def setFilledMarkers(self, filled_markers):
self.filledMarkers = filled_markers
self.update()
@staticmethod
def shortenFrequency(frequency: int) -> str:
if frequency < 50000:
return str(frequency)
if frequency < 5000000:
return str(round(frequency / 1000)) + "k"
if frequency < 50000000:
return str(round(frequency / 1000000, 2)) + "M"
return str(round(frequency / 1000000, 1)) + "M"
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
if event.buttons() == QtCore.Qt.RightButton:
event.ignore()
return
if event.buttons() == QtCore.Qt.MiddleButton:
# Drag event
event.accept()
self.moveStartX = event.x()
self.moveStartY = event.y()
return
if event.modifiers() == QtCore.Qt.ShiftModifier:
self.draggedMarker = self.getNearestMarker(event.x(), event.y())
elif event.modifiers() == QtCore.Qt.ControlModifier:
event.accept()
self.draggedBox = True
self.draggedBoxStart = (event.x(), event.y())
return
self.mouseMoveEvent(event)
def mouseReleaseEvent(self, a0: QtGui.QMouseEvent) -> None:
self.draggedMarker = None
if self.draggedBox:
self.zoomTo(self.draggedBoxStart[0], self.draggedBoxStart[1], a0.x(), a0.y())
self.draggedBox = False
self.draggedBoxCurrent = (-1, -1)
self.draggedBoxStart = (0, 0)
self.update()
def zoomTo(self, x1, y1, x2, y2):
pass
def saveScreenshot(self):
logger.info("Saving %s to file...", self.name)
filename, _ = QtWidgets.QFileDialog.getSaveFileName(parent=self, caption="Save image",
filter="PNG (*.png);;All files (*.*)")
logger.debug("Filename: %s", filename)
if filename != "":
if not QtCore.QFileInfo(filename).suffix():
filename += ".png"
self.grab().save(filename)
def copy(self):
new_chart = self.__class__(self.name)
new_chart.data = self.data
new_chart.reference = self.reference
new_chart.sweepColor = self.sweepColor
new_chart.secondarySweepColor = self.secondarySweepColor
new_chart.referenceColor = self.referenceColor
new_chart.secondaryReferenceColor = self.secondaryReferenceColor
new_chart.setBackgroundColor(self.backgroundColor)
new_chart.textColor = self.textColor
new_chart.foregroundColor = self.foregroundColor
new_chart.swrColor = self.swrColor
new_chart.markers = self.markers
new_chart.swrMarkers = self.swrMarkers
new_chart.bands = self.bands
new_chart.drawLines = self.drawLines
new_chart.markerSize = self.markerSize
new_chart.drawMarkerNumbers = self.drawMarkerNumbers
new_chart.filledMarkers = self.filledMarkers
new_chart.markerAtTip = self.markerAtTip
new_chart.resize(self.width(), self.height())
new_chart.setPointSize(self.pointSize)
new_chart.setLineThickness(self.lineThickness)
return new_chart
def addSWRMarker(self, swr: float):
self.swrMarkers.add(swr)
self.update()
def removeSWRMarker(self, swr: float):
try:
self.swrMarkers.remove(swr)
except KeyError:
logger.debug("KeyError from %s", self.name)
return
finally:
self.update()
def clearSWRMarkers(self):
self.swrMarkers.clear()
self.update()
def setSWRColor(self, color: QtGui.QColor):
self.swrColor = color
self.update()
def drawMarker(self, x, y, qp: QtGui.QPainter, color: QtGui.QColor, number=0):
if self.markerAtTip:
y -= self.markerSize
pen = QtGui.QPen(color)
qp.setPen(pen)
qpp = QtGui.QPainterPath()
qpp.moveTo(x, y + self.markerSize)
qpp.lineTo(x - self.markerSize, y - self.markerSize)
qpp.lineTo(x + self.markerSize, y - self.markerSize)
qpp.lineTo(x, y + self.markerSize)
if self.filledMarkers:
qp.fillPath(qpp, color)
else:
qp.drawPath(qpp)
if self.drawMarkerNumbers:
number_x = x - 3
number_y = y - self.markerSize - 3
qp.drawText(number_x, number_y, str(number))
def drawTitle(self, qp: QtGui.QPainter, position: QtCore.QPoint = None):
if self.sweepTitle != "":
qp.setPen(self.textColor)
if position is None:
qf = QtGui.QFontMetricsF(self.font())
width = qf.boundingRect(self.sweepTitle).width()
position = QtCore.QPointF(self.width()/2 - width/2, 15)
qp.drawText(position, self.sweepTitle)

Wyświetl plik

@ -1,607 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
import logging
from typing import List
import numpy as np
from PyQt5 import QtWidgets, QtGui, QtCore
from NanoVNASaver.Formatting import parse_frequency
from NanoVNASaver.RFTools import Datapoint
from .Chart import Chart
logger = logging.getLogger(__name__)
class FrequencyChart(Chart):
fstart = 0
fstop = 0
maxFrequency = 100000000
minFrequency = 1000000
minDisplayValue = -1
maxDisplayValue = 1
fixedSpan = False
fixedValues = False
logarithmicX = False
leftMargin = 30
rightMargin = 20
bottomMargin = 20
topMargin = 30
def __init__(self, name):
super().__init__(name)
self.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
mode_group = QtWidgets.QActionGroup(self)
self.menu = QtWidgets.QMenu()
self.reset = QtWidgets.QAction("Reset")
self.reset.triggered.connect(self.resetDisplayLimits)
self.menu.addAction(self.reset)
self.x_menu = QtWidgets.QMenu("Frequency axis")
self.action_automatic = QtWidgets.QAction("Automatic")
self.action_automatic.setCheckable(True)
self.action_automatic.setChecked(True)
self.action_automatic.changed.connect(
lambda: self.setFixedSpan(self.action_fixed_span.isChecked()))
self.action_fixed_span = QtWidgets.QAction("Fixed span")
self.action_fixed_span.setCheckable(True)
self.action_fixed_span.changed.connect(
lambda: self.setFixedSpan(self.action_fixed_span.isChecked()))
mode_group.addAction(self.action_automatic)
mode_group.addAction(self.action_fixed_span)
self.x_menu.addAction(self.action_automatic)
self.x_menu.addAction(self.action_fixed_span)
self.x_menu.addSeparator()
self.action_set_fixed_start = QtWidgets.QAction(
"Start (" + Chart.shortenFrequency(self.minFrequency) + ")")
self.action_set_fixed_start.triggered.connect(self.setMinimumFrequency)
self.action_set_fixed_stop = QtWidgets.QAction(
"Stop (" + Chart.shortenFrequency(self.maxFrequency) + ")")
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_stop)
self.x_menu.addSeparator()
frequency_mode_group = QtWidgets.QActionGroup(self.x_menu)
self.action_set_linear_x = QtWidgets.QAction("Linear")
self.action_set_linear_x.setCheckable(True)
self.action_set_logarithmic_x = QtWidgets.QAction("Logarithmic")
self.action_set_logarithmic_x.setCheckable(True)
frequency_mode_group.addAction(self.action_set_linear_x)
frequency_mode_group.addAction(self.action_set_logarithmic_x)
self.action_set_linear_x.triggered.connect(
lambda: self.setLogarithmicX(False))
self.action_set_logarithmic_x.triggered.connect(
lambda: self.setLogarithmicX(True))
self.action_set_linear_x.setChecked(True)
self.x_menu.addAction(self.action_set_linear_x)
self.x_menu.addAction(self.action_set_logarithmic_x)
self.y_menu = QtWidgets.QMenu("Data axis")
self.y_action_automatic = QtWidgets.QAction("Automatic")
self.y_action_automatic.setCheckable(True)
self.y_action_automatic.setChecked(True)
self.y_action_automatic.changed.connect(
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked()))
self.y_action_fixed_span = QtWidgets.QAction("Fixed span")
self.y_action_fixed_span.setCheckable(True)
self.y_action_fixed_span.changed.connect(
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked()))
mode_group = QtWidgets.QActionGroup(self)
mode_group.addAction(self.y_action_automatic)
mode_group.addAction(self.y_action_fixed_span)
self.y_menu.addAction(self.y_action_automatic)
self.y_menu.addAction(self.y_action_fixed_span)
self.y_menu.addSeparator()
self.action_set_fixed_maximum = QtWidgets.QAction(
f"Maximum ({self.maxDisplayValue})")
self.action_set_fixed_maximum.triggered.connect(self.setMaximumValue)
self.action_set_fixed_minimum = QtWidgets.QAction(
f"Minimum ({self.minDisplayValue})")
self.action_set_fixed_minimum.triggered.connect(self.setMinimumValue)
self.y_menu.addAction(self.action_set_fixed_maximum)
self.y_menu.addAction(self.action_set_fixed_minimum)
self.menu.addMenu(self.x_menu)
self.menu.addMenu(self.y_menu)
self.menu.addSeparator()
self.menu.addAction(self.action_save_screenshot)
self.action_popout = QtWidgets.QAction("Popout chart")
self.action_popout.triggered.connect(
lambda: self.popoutRequested.emit(self))
self.menu.addAction(self.action_popout)
self.setFocusPolicy(QtCore.Qt.ClickFocus)
def contextMenuEvent(self, event):
self.action_set_fixed_start.setText(
f"Start ({Chart.shortenFrequency(self.minFrequency)})")
self.action_set_fixed_stop.setText(
f"Stop ({Chart.shortenFrequency(self.maxFrequency)})")
self.action_set_fixed_minimum.setText(
f"Minimum ({self.minDisplayValue})")
self.action_set_fixed_maximum.setText(
f"Maximum ({self.maxDisplayValue})")
if self.fixedSpan:
self.action_fixed_span.setChecked(True)
else:
self.action_automatic.setChecked(True)
if self.fixedValues:
self.y_action_fixed_span.setChecked(True)
else:
self.y_action_automatic.setChecked(True)
self.menu.exec_(event.globalPos())
def setFixedSpan(self, fixed_span: bool):
self.fixedSpan = fixed_span
if fixed_span and self.minFrequency >= self.maxFrequency:
self.fixedSpan = False
self.action_automatic.setChecked(True)
self.action_fixed_span.setChecked(False)
self.update()
def setFixedValues(self, fixed_values: bool):
self.fixedValues = fixed_values
if fixed_values and self.minDisplayValue >= self.maxDisplayValue:
self.fixedValues = False
self.y_action_automatic.setChecked(True)
self.y_action_fixed_span.setChecked(False)
self.update()
def setLogarithmicX(self, logarithmic: bool):
self.logarithmicX = logarithmic
self.update()
def setMinimumFrequency(self):
min_freq_str, selected = QtWidgets.QInputDialog.getText(
self, "Start frequency",
"Set start frequency", text=str(self.minFrequency))
if not selected:
return
min_freq = parse_frequency(min_freq_str)
if min_freq > 0 and not (self.fixedSpan and min_freq >= self.maxFrequency):
self.minFrequency = min_freq
if self.fixedSpan:
self.update()
def setMaximumFrequency(self):
max_freq_str, selected = QtWidgets.QInputDialog.getText(
self, "Stop frequency",
"Set stop frequency", text=str(self.maxFrequency))
if not selected:
return
max_freq = parse_frequency(max_freq_str)
if max_freq > 0 and not (self.fixedSpan and max_freq <= self.minFrequency):
self.maxFrequency = max_freq
if self.fixedSpan:
self.update()
def setMinimumValue(self):
min_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Minimum value",
"Set minimum value", value=self.minDisplayValue,
decimals=3)
if not selected:
return
if not (self.fixedValues and min_val >= self.maxDisplayValue):
self.minDisplayValue = min_val
if self.fixedValues:
self.update()
def setMaximumValue(self):
max_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Maximum value",
"Set maximum value", value=self.maxDisplayValue,
decimals=3)
if not selected:
return
if not (self.fixedValues and max_val <= self.minDisplayValue):
self.maxDisplayValue = max_val
if self.fixedValues:
self.update()
def resetDisplayLimits(self):
self.fixedValues = False
self.y_action_automatic.setChecked(True)
self.fixedSpan = False
self.action_automatic.setChecked(True)
self.logarithmicX = False
self.action_set_linear_x.setChecked(True)
self.update()
def getXPosition(self, d: Datapoint) -> int:
span = self.fstop - self.fstart
if span > 0:
if self.logarithmicX:
span = math.log(self.fstop) - math.log(self.fstart)
return self.leftMargin + round(
self.chartWidth * (math.log(d.freq) -
math.log(self.fstart)) / span)
return self.leftMargin + round(
self.chartWidth * (d.freq - self.fstart) / span)
return math.floor(self.width()/2)
def frequencyAtPosition(self, x, limit=True) -> int:
"""
Calculates the frequency at a given X-position
:param limit: Determines whether frequencies outside the
currently displayed span can be returned.
:param x: The X position to calculate for.
:return: The frequency at the given position, if one
exists or -1 otherwise. If limit is True,
and the value is before or after the chart,
returns minimum or maximum frequencies.
"""
if self.fstop - self.fstart > 0:
absx = x - self.leftMargin
if limit and absx < 0:
return self.fstart
if limit and absx > self.chartWidth:
return self.fstop
if self.logarithmicX:
span = math.log(self.fstop) - math.log(self.fstart)
step = span/self.chartWidth
return round(math.exp(math.log(self.fstart) + absx * step))
span = self.fstop - self.fstart
step = span/self.chartWidth
return round(self.fstart + absx * step)
return -1
def valueAtPosition(self, y) -> List[float]:
"""
Returns the chart-specific value(s) at the specified Y-position
:param y: The Y position to calculate for.
:return: A list of the values at the Y-position, either
containing a single value, or the two values for the
chart from left to right Y-axis. If no value can be
found, returns the empty list. If the frequency
is above or below the chart, returns maximum
or minimum values.
"""
return []
def wheelEvent(self, a0: QtGui.QWheelEvent) -> None:
if len(self.data) == 0 and len(self.reference) == 0:
a0.ignore()
return
do_zoom_x = do_zoom_y = True
if a0.modifiers() == QtCore.Qt.ShiftModifier:
do_zoom_x = False
if a0.modifiers() == QtCore.Qt.ControlModifier:
do_zoom_y = False
if a0.angleDelta().y() > 0:
# Zoom in
a0.accept()
# Center of zoom = a0.x(), a0.y()
# We zoom in by 1/10 of the width/height.
rate = a0.angleDelta().y() / 120
if do_zoom_x:
zoomx = rate * self.chartWidth / 10
else:
zoomx = 0
if do_zoom_y:
zoomy = rate * self.chartHeight / 10
else:
zoomy = 0
absx = max(0, a0.x() - self.leftMargin)
absy = max(0, a0.y() - self.topMargin)
ratiox = absx/self.chartWidth
ratioy = absy/self.chartHeight
p1x = int(self.leftMargin + ratiox * zoomx)
p1y = int(self.topMargin + ratioy * zoomy)
p2x = int(self.leftMargin + self.chartWidth - (1 - ratiox) * zoomx)
p2y = int(self.topMargin + self.chartHeight - (1 - ratioy) * zoomy)
self.zoomTo(p1x, p1y, p2x, p2y)
elif a0.angleDelta().y() < 0:
# Zoom out
a0.accept()
# Center of zoom = a0.x(), a0.y()
# We zoom out by 1/9 of the width/height, to match zoom in.
rate = -a0.angleDelta().y() / 120
if do_zoom_x:
zoomx = rate * self.chartWidth / 9
else:
zoomx = 0
if do_zoom_y:
zoomy = rate * self.chartHeight / 9
else:
zoomy = 0
absx = max(0, a0.x() - self.leftMargin)
absy = max(0, a0.y() - self.topMargin)
ratiox = absx/self.chartWidth
ratioy = absy/self.chartHeight
p1x = int(self.leftMargin - ratiox * zoomx)
p1y = int(self.topMargin - ratioy * zoomy)
p2x = int(self.leftMargin + self.chartWidth + (1 - ratiox) * zoomx)
p2y = int(self.topMargin + self.chartHeight + (1 - ratioy) * zoomy)
self.zoomTo(p1x, p1y, p2x, p2y)
else:
a0.ignore()
def zoomTo(self, x1, y1, x2, y2):
val1 = self.valueAtPosition(y1)
val2 = self.valueAtPosition(y2)
if len(val1) == len(val2) == 1 and val1[0] != val2[0]:
self.minDisplayValue = round(min(val1[0], val2[0]), 3)
self.maxDisplayValue = round(max(val1[0], val2[0]), 3)
self.setFixedValues(True)
freq1 = max(1, self.frequencyAtPosition(x1, limit=False))
freq2 = max(1, self.frequencyAtPosition(x2, limit=False))
if freq1 > 0 and freq2 > 0 and freq1 != freq2:
self.minFrequency = min(freq1, freq2)
self.maxFrequency = max(freq1, freq2)
self.setFixedSpan(True)
self.update()
def mouseMoveEvent(self, a0: QtGui.QMouseEvent):
if a0.buttons() == QtCore.Qt.RightButton:
a0.ignore()
return
if a0.buttons() == QtCore.Qt.MiddleButton:
# Drag the display
a0.accept()
if self.moveStartX != -1 and self.moveStartY != -1:
dx = self.moveStartX - a0.x()
dy = self.moveStartY - a0.y()
self.zoomTo(self.leftMargin + dx, self.topMargin + dy,
self.leftMargin + self.chartWidth + dx,
self.topMargin + self.chartHeight + dy)
self.moveStartX = a0.x()
self.moveStartY = a0.y()
return
if a0.modifiers() == QtCore.Qt.ControlModifier:
# Dragging a box
if not self.draggedBox:
self.draggedBoxStart = (a0.x(), a0.y())
self.draggedBoxCurrent = (a0.x(), a0.y())
self.update()
a0.accept()
return
x = a0.x()
f = self.frequencyAtPosition(x)
if x == -1:
a0.ignore()
return
a0.accept()
m = self.getActiveMarker()
if m is not None:
m.setFrequency(str(f))
m.frequencyInput.setText(str(f))
def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
self.chartWidth = a0.size().width()-self.rightMargin-self.leftMargin
self.chartHeight = a0.size().height() - self.bottomMargin - self.topMargin
self.update()
def paintEvent(self, a0: QtGui.QPaintEvent) -> None:
qp = QtGui.QPainter(self)
self.drawChart(qp)
self.drawValues(qp)
if (len(self.data) > 0 and
(self.data[0].freq > self.fstop or
self.data[len(self.data)-1].freq < self.fstart)
and
(len(self.reference) == 0 or
self.reference[0].freq > self.fstop or
self.reference[len(self.reference)-1].freq < self.fstart)):
# Data outside frequency range
qp.setBackgroundMode(QtCore.Qt.OpaqueMode)
qp.setBackground(self.backgroundColor)
qp.setPen(self.textColor)
qp.drawText(self.leftMargin + self.chartWidth/2 - 70,
self.topMargin + self.chartHeight/2 - 20,
"Data outside frequency span")
if self.draggedBox and self.draggedBoxCurrent[0] != -1:
dashed_pen = QtGui.QPen(self.foregroundColor, 1, QtCore.Qt.DashLine)
qp.setPen(dashed_pen)
top_left = QtCore.QPoint(self.draggedBoxStart[0], self.draggedBoxStart[1])
bottom_right = QtCore.QPoint(self.draggedBoxCurrent[0], self.draggedBoxCurrent[1])
rect = QtCore.QRect(top_left, bottom_right)
qp.drawRect(rect)
qp.end()
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(self.textColor))
qp.drawText(3, 15, self.name)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin, self.topMargin - 5,
self.leftMargin, self.topMargin + self.chartHeight + 5)
qp.drawLine(self.leftMargin-5, self.topMargin + self.chartHeight,
self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
self.drawTitle(qp)
def drawFrequencyTicks(self, qp):
fspan = self.fstop - self.fstart
qp.setPen(self.textColor)
qp.drawText(self.leftMargin - 20,
self.topMargin + self.chartHeight + 15,
Chart.shortenFrequency(self.fstart))
ticks = math.floor(self.chartWidth / 100) # Number of ticks does not include the origin
for i in range(ticks):
x = self.leftMargin + round((i + 1) * self.chartWidth / ticks)
if self.logarithmicX:
fspan = math.log(self.fstop) - math.log(self.fstart)
freq = round(math.exp(((i + 1) * fspan / ticks) + math.log(self.fstart)))
else:
freq = round(fspan / ticks * (i + 1) + self.fstart)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(x, self.topMargin, x, self.topMargin + self.chartHeight + 5)
qp.setPen(self.textColor)
qp.drawText(x - 20,
self.topMargin + self.chartHeight + 15,
Chart.shortenFrequency(freq))
def drawBands(self, qp, fstart, fstop):
qp.setBrush(self.bands.color)
qp.setPen(QtGui.QColor(128, 128, 128, 0)) # Don't outline the bands
for (_, start, end) in self.bands.bands:
if fstart < start < fstop and fstart < end < fstop:
# The band is entirely within the chart
x_start = self.getXPosition(Datapoint(start, 0, 0))
x_end = self.getXPosition(Datapoint(end, 0, 0))
qp.drawRect(x_start,
self.topMargin,
x_end - x_start,
self.chartHeight)
elif fstart < start < fstop:
# Only the start of the band is within the chart
x_start = self.getXPosition(Datapoint(start, 0, 0))
qp.drawRect(x_start,
self.topMargin,
self.leftMargin + self.chartWidth - x_start,
self.chartHeight)
elif fstart < end < fstop:
# Only the end of the band is within the chart
x_end = self.getXPosition(Datapoint(end, 0, 0))
qp.drawRect(self.leftMargin + 1,
self.topMargin,
x_end - (self.leftMargin + 1),
self.chartHeight)
elif start < fstart < fstop < end:
# All the chart is in a band, we won't show it(?)
pass
def drawData(self, qp: QtGui.QPainter, data: List[Datapoint],
color: QtGui.QColor, y_function=None):
if y_function is None:
y_function = self.getYPosition
pen = QtGui.QPen(color)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(color)
line_pen.setWidth(self.lineThickness)
qp.setPen(pen)
for i, d in enumerate(data):
x = self.getXPosition(d)
y = y_function(d)
if y is None:
continue
if self.isPlotable(x, y):
qp.drawPoint(int(x), int(y))
if self.drawLines and i > 0:
prevx = self.getXPosition(data[i - 1])
prevy = y_function(data[i - 1])
if prevy is None:
continue
qp.setPen(line_pen)
if self.isPlotable(x, y) and self.isPlotable(prevx, prevy):
qp.drawLine(x, y, prevx, prevy)
elif self.isPlotable(x, y) and not self.isPlotable(prevx, prevy):
new_x, new_y = self.getPlotable(x, y, prevx, prevy)
qp.drawLine(x, y, new_x, new_y)
elif not self.isPlotable(x, y) and self.isPlotable(prevx, prevy):
new_x, new_y = self.getPlotable(prevx, prevy, x, y)
qp.drawLine(prevx, prevy, new_x, new_y)
qp.setPen(pen)
def drawMarkers(self, qp, data=None, y_function=None):
if data is None:
data = self.data
if y_function is None:
y_function = self.getYPosition
highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255))
highlighter.setWidth(1)
for m in self.markers:
if m.location != -1 and m.location < len(data):
x = self.getXPosition(data[m.location])
y = y_function(data[m.location])
if self.isPlotable(x, y):
self.drawMarker(x, y, qp, m.color, self.markers.index(m)+1)
def isPlotable(self, x, y):
return y is not None and x is not None and \
self.leftMargin <= x <= self.leftMargin + self.chartWidth and \
self.topMargin <= y <= self.topMargin + self.chartHeight
def getPlotable(self, x, y, distantx, distanty):
p1 = np.array([x, y])
p2 = np.array([distantx, distanty])
# First check the top line
if distanty < self.topMargin:
p3 = np.array([self.leftMargin, self.topMargin])
p4 = np.array([self.leftMargin + self.chartWidth, self.topMargin])
elif distanty > self.topMargin + self.chartHeight:
p3 = np.array([self.leftMargin, self.topMargin + self.chartHeight])
p4 = np.array([self.leftMargin + self.chartWidth, self.topMargin + self.chartHeight])
else:
return x, y
da = p2 - p1
db = p4 - p3
dp = p1 - p3
dap = np.array([-da[1], da[0]])
denom = np.dot(dap, db)
if denom != 0:
num = np.dot(dap, dp)
result = (num / denom.astype(float)) * db + p3
return result[0], result[1]
return x, y
def copy(self):
new_chart: FrequencyChart = super().copy()
new_chart.fstart = self.fstart
new_chart.fstop = self.fstop
new_chart.maxFrequency = self.maxFrequency
new_chart.minFrequency = self.minFrequency
new_chart.minDisplayValue = self.minDisplayValue
new_chart.maxDisplayValue = self.maxDisplayValue
new_chart.pointSize = self.pointSize
new_chart.lineThickness = self.lineThickness
new_chart.setFixedSpan(self.fixedSpan)
new_chart.action_automatic.setChecked(not self.fixedSpan)
new_chart.action_fixed_span.setChecked(self.fixedSpan)
new_chart.setFixedValues(self.fixedValues)
new_chart.y_action_automatic.setChecked(not self.fixedValues)
new_chart.y_action_fixed_span.setChecked(self.fixedValues)
new_chart.setLogarithmicX(self.logarithmicX)
new_chart.action_set_logarithmic_x.setChecked(self.logarithmicX)
new_chart.action_set_linear_x.setChecked(not self.logarithmicX)
return new_chart
def keyPressEvent(self, a0: QtGui.QKeyEvent) -> None:
m = self.getActiveMarker()
if m is not None and a0.modifiers() == QtCore.Qt.NoModifier:
if a0.key() == QtCore.Qt.Key_Down or a0.key() == QtCore.Qt.Key_Left:
m.frequencyInput.keyPressEvent(QtGui.QKeyEvent(
a0.type(), QtCore.Qt.Key_Down, a0.modifiers()))
elif a0.key() == QtCore.Qt.Key_Up or a0.key() == QtCore.Qt.Key_Right:
m.frequencyInput.keyPressEvent(QtGui.QKeyEvent(
a0.type(), QtCore.Qt.Key_Up, a0.modifiers()))
else:
super().keyPressEvent(a0)

Wyświetl plik

@ -1,273 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
import logging
from typing import List
import numpy as np
from PyQt5 import QtWidgets, QtGui
from NanoVNASaver.RFTools import Datapoint
from .Frequency import FrequencyChart
logger = logging.getLogger(__name__)
class GroupDelayChart(FrequencyChart):
def __init__(self, name="", reflective=True):
super().__init__(name)
self.leftMargin = 40
self.chartWidth = 250
self.chartHeight = 250
self.fstart = 0
self.fstop = 0
self.minDelay = 0
self.maxDelay = 0
self.span = 0
self.reflective = reflective
self.groupDelay = []
self.groupDelayReference = []
self.minDisplayValue = -180
self.maxDisplayValue = 180
self.setMinimumSize(self.chartWidth + self.rightMargin + self.leftMargin,
self.chartHeight + self.topMargin + self.bottomMargin)
self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding))
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, self.backgroundColor)
self.setPalette(pal)
self.setAutoFillBackground(True)
def copy(self):
new_chart: GroupDelayChart = super().copy()
new_chart.reflective = self.reflective
new_chart.groupDelay = self.groupDelay.copy()
new_chart.groupDelayReference = self.groupDelay.copy()
return new_chart
def setReference(self, data):
self.reference = data
self.calculateGroupDelay()
def setData(self, data):
self.data = data
self.calculateGroupDelay()
def calculateGroupDelay(self):
rawData = []
for d in self.data:
rawData.append(d.phase)
rawReference = []
for d in self.reference:
rawReference.append(d.phase)
if len(self.data) > 1:
unwrappedData = np.degrees(np.unwrap(rawData))
self.groupDelay = []
for i in range(len(self.data)):
# TODO: Replace with call to RFTools.groupDelay
if i == 0:
phase_change = unwrappedData[1] - unwrappedData[0]
freq_change = self.data[1].freq - self.data[0].freq
elif i == len(self.data)-1:
idx = len(self.data)-1
phase_change = unwrappedData[idx] - unwrappedData[idx-1]
freq_change = self.data[idx].freq - self.data[idx-1].freq
else:
phase_change = unwrappedData[i+1] - unwrappedData[i-1]
freq_change = self.data[i+1].freq - self.data[i-1].freq
delay = (-phase_change / (freq_change * 360)) * 10e8
if not self.reflective:
delay /= 2
self.groupDelay.append(delay)
if len(self.reference) > 1:
unwrappedReference = np.degrees(np.unwrap(rawReference))
self.groupDelayReference = []
for i in range(len(self.reference)):
if i == 0:
phase_change = unwrappedReference[1] - unwrappedReference[0]
freq_change = self.reference[1].freq - self.reference[0].freq
elif i == len(self.reference)-1:
idx = len(self.reference)-1
phase_change = unwrappedReference[idx] - unwrappedReference[idx-1]
freq_change = self.reference[idx].freq - self.reference[idx-1].freq
else:
phase_change = unwrappedReference[i+1] - unwrappedReference[i-1]
freq_change = self.reference[i+1].freq - self.reference[i-1].freq
delay = (-phase_change / (freq_change * 360)) * 10e8
if not self.reflective:
delay /= 2
self.groupDelayReference.append(delay)
self.update()
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(self.textColor))
qp.drawText(3, 15, self.name + " (ns)")
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5)
qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight,
self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
self.drawTitle(qp)
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
return
pen = QtGui.QPen(self.sweepColor)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(self.sweepColor)
line_pen.setWidth(self.lineThickness)
if self.fixedValues:
min_delay = self.minDisplayValue
max_delay = self.maxDisplayValue
elif self.data:
min_delay = math.floor(np.min(self.groupDelay))
max_delay = math.ceil(np.max(self.groupDelay))
elif self.reference:
min_delay = math.floor(np.min(self.groupDelayReference))
max_delay = math.ceil(np.max(self.groupDelayReference))
span = max_delay - min_delay
if span == 0:
span = 0.01
self.minDelay = min_delay
self.maxDelay = max_delay
self.span = span
tickcount = math.floor(self.chartHeight / 60)
for i in range(tickcount):
delay = min_delay + span * i / tickcount
y = self.topMargin + round((self.maxDelay - delay) / self.span * self.chartHeight)
if delay != min_delay and delay != max_delay:
qp.setPen(QtGui.QPen(self.textColor))
if delay != 0:
digits = max(0, min(2, math.floor(3 - math.log10(abs(delay)))))
if digits == 0:
delaystr = str(round(delay))
else:
delaystr = str(round(delay, digits))
else:
delaystr = "0"
qp.drawText(3, y + 3, delaystr)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.chartWidth, y)
qp.drawLine(self.leftMargin - 5,
self.topMargin,
self.leftMargin + self.chartWidth,
self.topMargin)
qp.setPen(self.textColor)
qp.drawText(3, self.topMargin + 5, str(max_delay))
qp.drawText(3, self.chartHeight + self.topMargin, str(min_delay))
if self.fixedSpan:
fstart = self.minFrequency
fstop = self.maxFrequency
else:
if len(self.data) > 0:
fstart = self.data[0].freq
fstop = self.data[len(self.data)-1].freq
else:
fstart = self.reference[0].freq
fstop = self.reference[len(self.reference) - 1].freq
self.fstart = fstart
self.fstop = fstop
# Draw bands if required
if self.bands.enabled:
self.drawBands(qp, fstart, fstop)
self.drawFrequencyTicks(qp)
color = self.sweepColor
pen = QtGui.QPen(color)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(color)
line_pen.setWidth(self.lineThickness)
qp.setPen(pen)
for i in range(len(self.data)):
x = self.getXPosition(self.data[i])
y = self.getYPositionFromDelay(self.groupDelay[i])
if self.isPlotable(x, y):
qp.drawPoint(int(x), int(y))
if self.drawLines and i > 0:
prevx = self.getXPosition(self.data[i - 1])
prevy = self.getYPositionFromDelay(self.groupDelay[i - 1])
qp.setPen(line_pen)
if self.isPlotable(x, y) and self.isPlotable(prevx, prevy):
qp.drawLine(x, y, prevx, prevy)
elif self.isPlotable(x, y) and not self.isPlotable(prevx, prevy):
new_x, new_y = self.getPlotable(x, y, prevx, prevy)
qp.drawLine(x, y, new_x, new_y)
elif not self.isPlotable(x, y) and self.isPlotable(prevx, prevy):
new_x, new_y = self.getPlotable(prevx, prevy, x, y)
qp.drawLine(prevx, prevy, new_x, new_y)
qp.setPen(pen)
color = self.referenceColor
pen = QtGui.QPen(color)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(color)
line_pen.setWidth(self.lineThickness)
qp.setPen(pen)
for i in range(len(self.reference)):
x = self.getXPosition(self.reference[i])
y = self.getYPositionFromDelay(self.groupDelayReference[i])
if self.isPlotable(x, y):
qp.drawPoint(int(x), int(y))
if self.drawLines and i > 0:
prevx = self.getXPosition(self.reference[i - 1])
prevy = self.getYPositionFromDelay(self.groupDelayReference[i - 1])
qp.setPen(line_pen)
if self.isPlotable(x, y) and self.isPlotable(prevx, prevy):
qp.drawLine(x, y, prevx, prevy)
elif self.isPlotable(x, y) and not self.isPlotable(prevx, prevy):
new_x, new_y = self.getPlotable(x, y, prevx, prevy)
qp.drawLine(x, y, new_x, new_y)
elif not self.isPlotable(x, y) and self.isPlotable(prevx, prevy):
new_x, new_y = self.getPlotable(prevx, prevy, x, y)
qp.drawLine(prevx, prevy, new_x, new_y)
qp.setPen(pen)
self.drawMarkers(qp)
def getYPosition(self, d: Datapoint) -> int:
# TODO: Find a faster way than these expensive "d in self.data" lookups
if d in self.data:
delay = self.groupDelay[self.data.index(d)]
elif d in self.reference:
delay = self.groupDelayReference[self.reference.index(d)]
else:
delay = 0
return self.getYPositionFromDelay(delay)
def getYPositionFromDelay(self, delay: float):
return self.topMargin + round((self.maxDelay - delay) / self.span * self.chartHeight)
def valueAtPosition(self, y) -> List[float]:
absy = y - self.topMargin
val = -1 * ((absy / self.chartHeight * self.span) - self.maxDelay)
return [val]

Wyświetl plik

@ -1,156 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
import logging
from typing import List
from PyQt5 import QtWidgets, QtGui
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.SITools import Format, Value
from .Frequency import FrequencyChart
logger = logging.getLogger(__name__)
class InductanceChart(FrequencyChart):
def __init__(self, name=""):
super().__init__(name)
self.leftMargin = 30
self.chartWidth = 250
self.chartHeight = 250
self.minDisplayValue = 0
self.maxDisplayValue = 100
self.minValue = -1
self.maxValue = 1
self.span = 1
self.setMinimumSize(self.chartWidth + self.rightMargin + self.leftMargin,
self.chartHeight + self.topMargin + self.bottomMargin)
self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding))
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, self.backgroundColor)
self.setPalette(pal)
self.setAutoFillBackground(True)
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(self.textColor))
qp.drawText(3, 15, self.name + " (H)")
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5)
qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight,
self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
self.drawTitle(qp)
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
return
pen = QtGui.QPen(self.sweepColor)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(self.sweepColor)
line_pen.setWidth(self.lineThickness)
highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255))
highlighter.setWidth(1)
if not self.fixedSpan:
if len(self.data) > 0:
fstart = self.data[0].freq
fstop = self.data[len(self.data)-1].freq
else:
fstart = self.reference[0].freq
fstop = self.reference[len(self.reference) - 1].freq
self.fstart = fstart
self.fstop = fstop
else:
fstart = self.fstart = self.minFrequency
fstop = self.fstop = self.maxFrequency
# Draw bands if required
if self.bands.enabled:
self.drawBands(qp, fstart, fstop)
if self.fixedValues:
maxValue = self.maxDisplayValue / 10e11
minValue = self.minDisplayValue / 10e11
self.maxValue = maxValue
self.minValue = minValue
else:
# Find scaling
minValue = 1
maxValue = -1
for d in self.data:
val = d.inductiveEquivalent()
if val > maxValue:
maxValue = val
if val < minValue:
minValue = val
for d in self.reference: # Also check min/max for the reference sweep
if d.freq < self.fstart or d.freq > self.fstop:
continue
val = d.inductiveEquivalent()
if val > maxValue:
maxValue = val
if val < minValue:
minValue = val
self.maxValue = maxValue
self.minValue = minValue
span = maxValue - minValue
if span == 0:
logger.info("Span is zero for CapacitanceChart, setting to a small value.")
span = 1e-15
self.span = span
target_ticks = math.floor(self.chartHeight / 60)
fmt = Format(max_nr_digits=1)
for i in range(target_ticks):
val = minValue + (i / target_ticks) * span
y = self.topMargin + round((self.maxValue - val) / self.span * self.chartHeight)
qp.setPen(self.textColor)
if val != minValue:
valstr = str(Value(val, fmt=fmt))
qp.drawText(3, y + 3, valstr)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.chartWidth, y)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, self.topMargin,
self.leftMargin + self.chartWidth, self.topMargin)
qp.setPen(self.textColor)
qp.drawText(3, self.topMargin + 4, str(Value(maxValue, fmt=fmt)))
qp.drawText(3, self.chartHeight+self.topMargin, str(Value(minValue, fmt=fmt)))
self.drawFrequencyTicks(qp)
self.drawData(qp, self.data, self.sweepColor)
self.drawData(qp, self.reference, self.referenceColor)
self.drawMarkers(qp)
def getYPosition(self, d: Datapoint) -> int:
return (self.topMargin +
round((self.maxValue - d.inductiveEquivalent()) /
self.span * self.chartHeight))
def valueAtPosition(self, y) -> List[float]:
absy = y - self.topMargin
val = -1 * ((absy / self.chartHeight * self.span) - self.maxValue)
return [val * 10e11]
def copy(self):
new_chart: InductanceChart = super().copy()
new_chart.span = self.span
return new_chart

Wyświetl plik

@ -1,226 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
import logging
from typing import List
from PyQt5 import QtWidgets, QtGui
from NanoVNASaver.RFTools import Datapoint
from .Frequency import FrequencyChart
logger = logging.getLogger(__name__)
class LogMagChart(FrequencyChart):
def __init__(self, name=""):
super().__init__(name)
self.leftMargin = 30
self.chartWidth = 250
self.chartHeight = 250
self.minDisplayValue = -80
self.maxDisplayValue = 10
self.minValue = 0
self.maxValue = 1
self.span = 1
self.isInverted = False
self.setMinimumSize(self.chartWidth + self.rightMargin + self.leftMargin,
self.chartHeight + self.topMargin + self.bottomMargin)
self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding))
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, self.backgroundColor)
self.setPalette(pal)
self.setAutoFillBackground(True)
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(self.textColor))
qp.drawText(3, 15, self.name + " (dB)")
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin, self.topMargin - 5,
self.leftMargin, self.topMargin+self.chartHeight+5)
qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight,
self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
self.drawTitle(qp)
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
return
pen = QtGui.QPen(self.sweepColor)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(self.sweepColor)
line_pen.setWidth(self.lineThickness)
highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255))
highlighter.setWidth(1)
if not self.fixedSpan:
if len(self.data) > 0:
fstart = self.data[0].freq
fstop = self.data[len(self.data)-1].freq
else:
fstart = self.reference[0].freq
fstop = self.reference[len(self.reference) - 1].freq
self.fstart = fstart
self.fstop = fstop
else:
fstart = self.fstart = self.minFrequency
fstop = self.fstop = self.maxFrequency
# Draw bands if required
if self.bands.enabled:
self.drawBands(qp, fstart, fstop)
if self.fixedValues:
maxValue = self.maxDisplayValue
minValue = self.minDisplayValue
self.maxValue = maxValue
self.minValue = minValue
else:
# Find scaling
minValue = 100
maxValue = -100
for d in self.data:
logmag = self.logMag(d)
if math.isinf(logmag):
continue
if logmag > maxValue:
maxValue = logmag
if logmag < minValue:
minValue = logmag
for d in self.reference: # Also check min/max for the reference sweep
if d.freq < self.fstart or d.freq > self.fstop:
continue
logmag = self.logMag(d)
if math.isinf(logmag):
continue
if logmag > maxValue:
maxValue = logmag
if logmag < minValue:
minValue = logmag
minValue = 10*math.floor(minValue/10)
self.minValue = minValue
maxValue = 10*math.ceil(maxValue/10)
self.maxValue = maxValue
span = maxValue-minValue
if span == 0:
span = 0.01
self.span = span
if self.span >= 50:
# Ticks per 10dB step
tick_count = math.floor(self.span/10)
first_tick = math.ceil(self.minValue/10) * 10
tick_step = 10
if first_tick == minValue:
first_tick += 10
elif self.span >= 20:
# 5 dB ticks
tick_count = math.floor(self.span/5)
first_tick = math.ceil(self.minValue/5) * 5
tick_step = 5
if first_tick == minValue:
first_tick += 5
elif self.span >= 10:
# 2 dB ticks
tick_count = math.floor(self.span/2)
first_tick = math.ceil(self.minValue/2) * 2
tick_step = 2
if first_tick == minValue:
first_tick += 2
elif self.span >= 5:
# 1dB ticks
tick_count = math.floor(self.span)
first_tick = math.ceil(minValue)
tick_step = 1
if first_tick == minValue:
first_tick += 1
elif self.span >= 2:
# .5 dB ticks
tick_count = math.floor(self.span*2)
first_tick = math.ceil(minValue*2) / 2
tick_step = .5
if first_tick == minValue:
first_tick += .5
else:
# .1 dB ticks
tick_count = math.floor(self.span*10)
first_tick = math.ceil(minValue*10) / 10
tick_step = .1
if first_tick == minValue:
first_tick += .1
for i in range(tick_count):
db = first_tick + i * tick_step
y = self.topMargin + round((maxValue - db)/span*self.chartHeight)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin-5, y, self.leftMargin+self.chartWidth, y)
if db > minValue and db != maxValue:
qp.setPen(QtGui.QPen(self.textColor))
if tick_step < 1:
dbstr = str(round(db, 1))
else:
dbstr = str(db)
qp.drawText(3, y + 4, dbstr)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, self.topMargin,
self.leftMargin + self.chartWidth, self.topMargin)
qp.setPen(self.textColor)
qp.drawText(3, self.topMargin + 4, str(maxValue))
qp.drawText(3, self.chartHeight+self.topMargin, str(minValue))
self.drawFrequencyTicks(qp)
qp.setPen(self.swrColor)
for vswr in self.swrMarkers:
if vswr <= 1:
continue
logMag = 20 * math.log10((vswr-1)/(vswr+1))
if self.isInverted:
logMag = logMag * -1
y = self.topMargin + round((self.maxValue - logMag) / self.span * self.chartHeight)
qp.drawLine(self.leftMargin, y, self.leftMargin + self.chartWidth, y)
qp.drawText(self.leftMargin + 3, y - 1, "VSWR: " + str(vswr))
self.drawData(qp, self.data, self.sweepColor)
self.drawData(qp, self.reference, self.referenceColor)
self.drawMarkers(qp)
def getYPosition(self, d: Datapoint) -> int:
logMag = self.logMag(d)
if math.isinf(logMag):
return None
return self.topMargin + round((self.maxValue - logMag) / self.span * self.chartHeight)
def valueAtPosition(self, y) -> List[float]:
absy = y - self.topMargin
val = -1 * ((absy / self.chartHeight * self.span) - self.maxValue)
return [val]
def logMag(self, p: Datapoint) -> float:
if self.isInverted:
return -p.gain
return p.gain
def copy(self):
new_chart: LogMagChart = super().copy()
new_chart.isInverted = self.isInverted
new_chart.span = self.span
return new_chart

Wyświetl plik

@ -1,167 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
import logging
from typing import List
from PyQt5 import QtWidgets, QtGui
from NanoVNASaver.RFTools import Datapoint
from .Frequency import FrequencyChart
logger = logging.getLogger(__name__)
class MagnitudeChart(FrequencyChart):
def __init__(self, name=""):
super().__init__(name)
self.leftMargin = 30
self.chartWidth = 250
self.chartHeight = 250
self.minDisplayValue = 0
self.maxDisplayValue = 1
self.fixedValues = True
self.y_action_fixed_span.setChecked(True)
self.y_action_automatic.setChecked(False)
self.minValue = 0
self.maxValue = 1
self.span = 1
self.setMinimumSize(self.chartWidth + self.rightMargin + self.leftMargin,
self.chartHeight + self.topMargin + self.bottomMargin)
self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding))
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, self.backgroundColor)
self.setPalette(pal)
self.setAutoFillBackground(True)
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
return
pen = QtGui.QPen(self.sweepColor)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(self.sweepColor)
line_pen.setWidth(self.lineThickness)
highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255))
highlighter.setWidth(1)
if not self.fixedSpan:
if len(self.data) > 0:
fstart = self.data[0].freq
fstop = self.data[len(self.data)-1].freq
else:
fstart = self.reference[0].freq
fstop = self.reference[len(self.reference) - 1].freq
self.fstart = fstart
self.fstop = fstop
else:
fstart = self.fstart = self.minFrequency
fstop = self.fstop = self.maxFrequency
# Draw bands if required
if self.bands.enabled:
self.drawBands(qp, fstart, fstop)
if self.fixedValues:
maxValue = self.maxDisplayValue
minValue = self.minDisplayValue
self.maxValue = maxValue
self.minValue = minValue
else:
# Find scaling
minValue = 100
maxValue = 0
for d in self.data:
mag = self.magnitude(d)
if mag > maxValue:
maxValue = mag
if mag < minValue:
minValue = mag
for d in self.reference: # Also check min/max for the reference sweep
if d.freq < self.fstart or d.freq > self.fstop:
continue
mag = self.magnitude(d)
if mag > maxValue:
maxValue = mag
if mag < minValue:
minValue = mag
minValue = 10*math.floor(minValue/10)
self.minValue = minValue
maxValue = 10*math.ceil(maxValue/10)
self.maxValue = maxValue
span = maxValue-minValue
if span == 0:
span = 0.01
self.span = span
target_ticks = math.floor(self.chartHeight / 60)
for i in range(target_ticks):
val = minValue + i / target_ticks * span
y = self.topMargin + round((self.maxValue - val) / self.span * self.chartHeight)
qp.setPen(self.textColor)
if val != minValue:
digits = max(0, min(2, math.floor(3 - math.log10(abs(val)))))
if digits == 0:
vswrstr = str(round(val))
else:
vswrstr = str(round(val, digits))
qp.drawText(3, y + 3, vswrstr)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.chartWidth, y)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, self.topMargin,
self.leftMargin + self.chartWidth, self.topMargin)
qp.setPen(self.textColor)
qp.drawText(3, self.topMargin + 4, str(maxValue))
qp.drawText(3, self.chartHeight+self.topMargin, str(minValue))
self.drawFrequencyTicks(qp)
qp.setPen(self.swrColor)
for vswr in self.swrMarkers:
if vswr <= 1:
continue
mag = (vswr-1)/(vswr+1)
y = self.topMargin + round((self.maxValue - mag) / self.span * self.chartHeight)
qp.drawLine(self.leftMargin, y, self.leftMargin + self.chartWidth, y)
qp.drawText(self.leftMargin + 3, y - 1, "VSWR: " + str(vswr))
self.drawData(qp, self.data, self.sweepColor)
self.drawData(qp, self.reference, self.referenceColor)
self.drawMarkers(qp)
def getYPosition(self, d: Datapoint) -> int:
mag = self.magnitude(d)
return self.topMargin + round((self.maxValue - mag) / self.span * self.chartHeight)
def valueAtPosition(self, y) -> List[float]:
absy = y - self.topMargin
val = -1 * ((absy / self.chartHeight * self.span) - self.maxValue)
return [val]
@staticmethod
def magnitude(p: Datapoint) -> float:
return math.sqrt(p.re**2 + p.im**2)
def copy(self):
new_chart = super().copy()
new_chart.span = self.span
return new_chart

Wyświetl plik

@ -1,157 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
import logging
from typing import List
from PyQt5 import QtWidgets, QtGui
from NanoVNASaver.RFTools import Datapoint
from .Frequency import FrequencyChart
from .LogMag import LogMagChart
logger = logging.getLogger(__name__)
class MagnitudeZChart(FrequencyChart):
def __init__(self, name=""):
super().__init__(name)
self.leftMargin = 30
self.chartWidth = 250
self.chartHeight = 250
self.minDisplayValue = 0
self.maxDisplayValue = 100
self.minValue = 0
self.maxValue = 1
self.span = 1
self.setMinimumSize(self.chartWidth + self.rightMargin + self.leftMargin,
self.chartHeight + self.topMargin + self.bottomMargin)
self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding))
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, self.backgroundColor)
self.setPalette(pal)
self.setAutoFillBackground(True)
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
return
pen = QtGui.QPen(self.sweepColor)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(self.sweepColor)
line_pen.setWidth(self.lineThickness)
highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255))
highlighter.setWidth(1)
if not self.fixedSpan:
if len(self.data) > 0:
fstart = self.data[0].freq
fstop = self.data[len(self.data)-1].freq
else:
fstart = self.reference[0].freq
fstop = self.reference[len(self.reference) - 1].freq
self.fstart = fstart
self.fstop = fstop
else:
fstart = self.fstart = self.minFrequency
fstop = self.fstop = self.maxFrequency
# Draw bands if required
if self.bands.enabled:
self.drawBands(qp, fstart, fstop)
if self.fixedValues:
maxValue = self.maxDisplayValue
minValue = self.minDisplayValue
self.maxValue = maxValue
self.minValue = minValue
else:
# Find scaling
minValue = 100
maxValue = 0
for d in self.data:
mag = self.magnitude(d)
if mag > maxValue:
maxValue = mag
if mag < minValue:
minValue = mag
for d in self.reference: # Also check min/max for the reference sweep
if d.freq < self.fstart or d.freq > self.fstop:
continue
mag = self.magnitude(d)
if mag > maxValue:
maxValue = mag
if mag < minValue:
minValue = mag
minValue = 10*math.floor(minValue/10)
self.minValue = minValue
maxValue = 10*math.ceil(maxValue/10)
self.maxValue = maxValue
span = maxValue-minValue
if span == 0:
span = 0.01
self.span = span
target_ticks = math.floor(self.chartHeight / 60)
for i in range(target_ticks):
val = minValue + (i / target_ticks) * span
y = self.topMargin + round((self.maxValue - val) / self.span * self.chartHeight)
qp.setPen(self.textColor)
if val != minValue:
digits = max(0, min(2, math.floor(3 - math.log10(abs(val)))))
if digits == 0:
vswrstr = str(round(val))
else:
vswrstr = str(round(val, digits))
qp.drawText(3, y + 3, vswrstr)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.chartWidth, y)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, self.topMargin,
self.leftMargin + self.chartWidth, self.topMargin)
qp.setPen(self.textColor)
qp.drawText(3, self.topMargin + 4, str(maxValue))
qp.drawText(3, self.chartHeight+self.topMargin, str(minValue))
self.drawFrequencyTicks(qp)
self.drawData(qp, self.data, self.sweepColor)
self.drawData(qp, self.reference, self.referenceColor)
self.drawMarkers(qp)
def getYPosition(self, d: Datapoint) -> int:
mag = self.magnitude(d)
return self.topMargin + round((self.maxValue - mag) / self.span * self.chartHeight)
def valueAtPosition(self, y) -> List[float]:
absy = y - self.topMargin
val = -1 * ((absy / self.chartHeight * self.span) - self.maxValue)
return [val]
@staticmethod
def magnitude(p: Datapoint) -> float:
return abs(p.impedance())
def copy(self):
new_chart: LogMagChart = super().copy()
new_chart.span = self.span
return new_chart

Wyświetl plik

@ -1,374 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
import logging
from typing import List
from PyQt5 import QtWidgets, QtGui
from NanoVNASaver.Marker import Marker
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.SITools import Format, Value
from .Frequency import FrequencyChart
logger = logging.getLogger(__name__)
class PermeabilityChart(FrequencyChart):
def __init__(self, name=""):
super().__init__(name)
self.leftMargin = 40
self.rightMargin = 30
self.chartWidth = 230
self.chartHeight = 250
self.fstart = 0
self.fstop = 0
self.span = 0.01
self.max = 0
self.logarithmicY = True
self.maxDisplayValue = 100
self.minDisplayValue = -100
#
# Set up size policy and palette
#
self.setMinimumSize(self.chartWidth + self.leftMargin +
self.rightMargin, self.chartHeight + 40)
self.setSizePolicy(QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding))
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, self.backgroundColor)
self.setPalette(pal)
self.setAutoFillBackground(True)
self.y_menu.addSeparator()
self.y_log_lin_group = QtWidgets.QActionGroup(self.y_menu)
self.y_action_linear = QtWidgets.QAction("Linear")
self.y_action_linear.setCheckable(True)
self.y_action_logarithmic = QtWidgets.QAction("Logarithmic")
self.y_action_logarithmic.setCheckable(True)
self.y_action_logarithmic.setChecked(True)
self.y_action_linear.triggered.connect(lambda: self.setLogarithmicY(False))
self.y_action_logarithmic.triggered.connect(lambda: self.setLogarithmicY(True))
self.y_log_lin_group.addAction(self.y_action_linear)
self.y_log_lin_group.addAction(self.y_action_logarithmic)
self.y_menu.addAction(self.y_action_linear)
self.y_menu.addAction(self.y_action_logarithmic)
def setLogarithmicY(self, logarithmic: bool):
self.logarithmicY = logarithmic
self.update()
def copy(self):
new_chart: PermeabilityChart = super().copy()
new_chart.logarithmicY = self.logarithmicY
return new_chart
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(self.textColor))
qp.drawText(self.leftMargin + 5, 15, self.name + " (\N{MICRO SIGN}\N{OHM SIGN} / Hz)")
qp.drawText(10, 15, "R")
qp.drawText(self.leftMargin + self.chartWidth + 10, 15, "X")
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin, self.topMargin - 5,
self.leftMargin, self.topMargin + self.chartHeight + 5)
qp.drawLine(self.leftMargin-5, self.topMargin + self.chartHeight,
self.leftMargin + self.chartWidth + 5, self.topMargin + self.chartHeight)
self.drawTitle(qp)
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
return
pen = QtGui.QPen(self.sweepColor)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(self.sweepColor)
line_pen.setWidth(self.lineThickness)
highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255))
highlighter.setWidth(1)
if self.fixedSpan:
fstart = self.minFrequency
fstop = self.maxFrequency
else:
if len(self.data) > 0:
fstart = self.data[0].freq
fstop = self.data[len(self.data)-1].freq
else:
fstart = self.reference[0].freq
fstop = self.reference[len(self.reference) - 1].freq
self.fstart = fstart
self.fstop = fstop
# Draw bands if required
if self.bands.enabled:
self.drawBands(qp, fstart, fstop)
# Find scaling
if self.fixedValues:
min_val = self.minDisplayValue
max_val = self.maxDisplayValue
else:
min_val = 1000
max_val = -1000
for d in self.data:
imp = d.impedance()
re, im = imp.real, imp.imag
re = re * 10e6 / d.freq
im = im * 10e6 / d.freq
if re > max_val:
max_val = re
if re < min_val:
min_val = re
if im > max_val:
max_val = im
if im < min_val:
min_val = im
for d in self.reference: # Also check min/max for the reference sweep
if d.freq < fstart or d.freq > fstop:
continue
imp = d.impedance()
re, im = imp.real, imp.imag
re = re * 10e6 / d.freq
im = im * 10e6 / d.freq
if re > max_val:
max_val = re
if re < min_val:
min_val = re
if im > max_val:
max_val = im
if im < min_val:
min_val = im
if self.logarithmicY:
min_val = max(0.01, min_val)
self.max = max_val
span = max_val - min_val
if span == 0:
span = 0.01
self.span = span
# We want one horizontal tick per 50 pixels, at most
horizontal_ticks = math.floor(self.chartHeight/50)
fmt = Format(max_nr_digits=4)
for i in range(horizontal_ticks):
y = self.topMargin + round(i * self.chartHeight / horizontal_ticks)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, y,
self.leftMargin + self.chartWidth + 5, y)
qp.setPen(QtGui.QPen(self.textColor))
val = Value(self.valueAtPosition(y)[0], fmt=fmt)
qp.drawText(3, y + 4, str(val))
qp.drawText(3,
self.chartHeight + self.topMargin,
str(Value(min_val, fmt=fmt)))
self.drawFrequencyTicks(qp)
primary_pen = pen
secondary_pen = QtGui.QPen(self.secondarySweepColor)
if len(self.data) > 0:
c = QtGui.QColor(self.sweepColor)
c.setAlpha(255)
pen = QtGui.QPen(c)
pen.setWidth(2)
qp.setPen(pen)
qp.drawLine(20, 9, 25, 9)
c = QtGui.QColor(self.secondarySweepColor)
c.setAlpha(255)
pen.setColor(c)
qp.setPen(pen)
qp.drawLine(
self.leftMargin + self.chartWidth, 9,
self.leftMargin + self.chartWidth + 5, 9)
primary_pen.setWidth(self.pointSize)
secondary_pen.setWidth(self.pointSize)
line_pen.setWidth(self.lineThickness)
for i in range(len(self.data)):
x = self.getXPosition(self.data[i])
y_re = self.getReYPosition(self.data[i])
y_im = self.getImYPosition(self.data[i])
qp.setPen(primary_pen)
if self.isPlotable(x, y_re):
qp.drawPoint(x, y_re)
qp.setPen(secondary_pen)
if self.isPlotable(x, y_im):
qp.drawPoint(x, y_im)
if self.drawLines and i > 0:
prev_x = self.getXPosition(self.data[i - 1])
prev_y_re = self.getReYPosition(self.data[i-1])
prev_y_im = self.getImYPosition(self.data[i-1])
# Real part first
line_pen.setColor(self.sweepColor)
qp.setPen(line_pen)
if self.isPlotable(x, y_re) and self.isPlotable(prev_x, prev_y_re):
qp.drawLine(x, y_re, prev_x, prev_y_re)
elif self.isPlotable(x, y_re) and not self.isPlotable(prev_x, prev_y_re):
new_x, new_y = self.getPlotable(x, y_re, prev_x, prev_y_re)
qp.drawLine(x, y_re, new_x, new_y)
elif not self.isPlotable(x, y_re) and self.isPlotable(prev_x, prev_y_re):
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
qp.drawLine(prev_x, prev_y_re, new_x, new_y)
# Imag part second
line_pen.setColor(self.secondarySweepColor)
qp.setPen(line_pen)
if self.isPlotable(x, y_im) and self.isPlotable(prev_x, prev_y_im):
qp.drawLine(x, y_im, prev_x, prev_y_im)
elif self.isPlotable(x, y_im) and not self.isPlotable(prev_x, prev_y_im):
new_x, new_y = self.getPlotable(x, y_im, prev_x, prev_y_im)
qp.drawLine(x, y_im, new_x, new_y)
elif not self.isPlotable(x, y_im) and self.isPlotable(prev_x, prev_y_im):
new_x, new_y = self.getPlotable(prev_x, prev_y_im, x, y_im)
qp.drawLine(prev_x, prev_y_im, new_x, new_y)
primary_pen.setColor(self.referenceColor)
line_pen.setColor(self.referenceColor)
secondary_pen.setColor(self.secondaryReferenceColor)
qp.setPen(primary_pen)
if len(self.reference) > 0:
c = QtGui.QColor(self.referenceColor)
c.setAlpha(255)
pen = QtGui.QPen(c)
pen.setWidth(2)
qp.setPen(pen)
qp.drawLine(20, 14, 25, 14)
c = QtGui.QColor(self.secondaryReferenceColor)
c.setAlpha(255)
pen = QtGui.QPen(c)
pen.setWidth(2)
qp.setPen(pen)
qp.drawLine(self.leftMargin + self.chartWidth, 14,
self.leftMargin + self.chartWidth + 5, 14)
for i in range(len(self.reference)):
if self.reference[i].freq < fstart or self.reference[i].freq > fstop:
continue
x = self.getXPosition(self.reference[i])
y_re = self.getReYPosition(self.reference[i])
y_im = self.getImYPosition(self.reference[i])
qp.setPen(primary_pen)
if self.isPlotable(x, y_re):
qp.drawPoint(x, y_re)
qp.setPen(secondary_pen)
if self.isPlotable(x, y_im):
qp.drawPoint(x, y_im)
if self.drawLines and i > 0:
prev_x = self.getXPosition(self.reference[i - 1])
prev_y_re = self.getReYPosition(self.reference[i-1])
prev_y_im = self.getImYPosition(self.reference[i-1])
line_pen.setColor(self.referenceColor)
qp.setPen(line_pen)
# Real part first
if self.isPlotable(x, y_re) and self.isPlotable(prev_x, prev_y_re):
qp.drawLine(x, y_re, prev_x, prev_y_re)
elif self.isPlotable(x, y_re) and not self.isPlotable(prev_x, prev_y_re):
new_x, new_y = self.getPlotable(x, y_re, prev_x, prev_y_re)
qp.drawLine(x, y_re, new_x, new_y)
elif not self.isPlotable(x, y_re) and self.isPlotable(prev_x, prev_y_re):
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
qp.drawLine(prev_x, prev_y_re, new_x, new_y)
line_pen.setColor(self.secondaryReferenceColor)
qp.setPen(line_pen)
# Imag part second
if self.isPlotable(x, y_im) and self.isPlotable(prev_x, prev_y_im):
qp.drawLine(x, y_im, prev_x, prev_y_im)
elif self.isPlotable(x, y_im) and not self.isPlotable(prev_x, prev_y_im):
new_x, new_y = self.getPlotable(x, y_im, prev_x, prev_y_im)
qp.drawLine(x, y_im, new_x, new_y)
elif not self.isPlotable(x, y_im) and self.isPlotable(prev_x, prev_y_im):
new_x, new_y = self.getPlotable(prev_x, prev_y_im, x, y_im)
qp.drawLine(prev_x, prev_y_im, new_x, new_y)
# Now draw the markers
for m in self.markers:
if m.location != -1:
x = self.getXPosition(self.data[m.location])
y_re = self.getReYPosition(self.data[m.location])
y_im = self.getImYPosition(self.data[m.location])
self.drawMarker(x, y_re, 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:
im = d.impedance().imag
im = im * 10e6 / d.freq
if self.logarithmicY:
min_val = self.max - self.span
if self.max > 0 and min_val > 0 and im > 0:
span = math.log(self.max) - math.log(min_val)
else:
return -1
return self.topMargin + round(
(math.log(self.max) - math.log(im)) /
span * self.chartHeight)
return self.topMargin + round(
(self.max - im) / self.span * self.chartHeight)
def getReYPosition(self, d: Datapoint) -> int:
re = d.impedance().real
re = re * 10e6 / d.freq
if self.logarithmicY:
min_val = self.max - self.span
if self.max > 0 and min_val > 0 and re > 0:
span = math.log(self.max) - math.log(min_val)
else:
return -1
return self.topMargin + round(
(math.log(self.max) - math.log(re)) /
span * self.chartHeight)
return self.topMargin + round(
(self.max - re) / self.span * self.chartHeight)
def valueAtPosition(self, y) -> List[float]:
absy = y - self.topMargin
if self.logarithmicY:
min_val = self.max - self.span
if self.max > 0 and min_val > 0:
span = math.log(self.max) - math.log(min_val)
step = span / self.chartHeight
val = math.exp(math.log(self.max) - absy * step)
else:
val = -1
else:
val = -1 * ((absy / self.chartHeight * self.span) - self.max)
return [val]
def getNearestMarker(self, x, y) -> Marker:
if len(self.data) == 0:
return None
shortest = 10**6
nearest = None
for m in self.markers:
mx, _ = self.getPosition(self.data[m.location])
myr = self.getReYPosition(self.data[m.location])
myi = self.getImYPosition(self.data[m.location])
dx = abs(x - mx)
dy = min(abs(y - myr), abs(y-myi))
distance = math.sqrt(dx**2 + dy**2)
if distance < shortest:
shortest = distance
nearest = m
return nearest

Wyświetl plik

@ -1,178 +0,0 @@
# NanoVNASaver - a python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
import logging
from typing import List
import numpy as np
from PyQt5 import QtWidgets, QtGui
from NanoVNASaver.RFTools import Datapoint
from .Frequency import FrequencyChart
logger = logging.getLogger(__name__)
class PhaseChart(FrequencyChart):
def __init__(self, name=""):
super().__init__(name)
self.leftMargin = 40
self.chartWidth = 250
self.chartHeight = 250
self.fstart = 0
self.fstop = 0
self.minAngle = 0
self.maxAngle = 0
self.span = 0
self.unwrap = False
self.unwrappedData = []
self.unwrappedReference = []
self.minDisplayValue = -180
self.maxDisplayValue = 180
self.setMinimumSize(self.chartWidth + self.rightMargin + self.leftMargin,
self.chartHeight + self.topMargin + self.bottomMargin)
self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding))
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, self.backgroundColor)
self.setPalette(pal)
self.setAutoFillBackground(True)
self.y_menu.addSeparator()
self.action_unwrap = QtWidgets.QAction("Unwrap")
self.action_unwrap.setCheckable(True)
self.action_unwrap.triggered.connect(lambda: self.setUnwrap(self.action_unwrap.isChecked()))
self.y_menu.addAction(self.action_unwrap)
def copy(self):
new_chart: PhaseChart = super().copy()
new_chart.setUnwrap(self.unwrap)
new_chart.action_unwrap.setChecked(self.unwrap)
return new_chart
def setUnwrap(self, unwrap: bool):
self.unwrap = unwrap
self.update()
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
return
pen = QtGui.QPen(self.sweepColor)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(self.sweepColor)
line_pen.setWidth(self.lineThickness)
if self.unwrap:
rawData = []
for d in self.data:
rawData.append(d.phase)
rawReference = []
for d in self.reference:
rawReference.append(d.phase)
self.unwrappedData = np.degrees(np.unwrap(rawData))
self.unwrappedReference = np.degrees(np.unwrap(rawReference))
if self.fixedValues:
minAngle = self.minDisplayValue
maxAngle = self.maxDisplayValue
elif self.unwrap and self.data:
minAngle = math.floor(np.min(self.unwrappedData))
maxAngle = math.ceil(np.max(self.unwrappedData))
elif self.unwrap and self.reference:
minAngle = math.floor(np.min(self.unwrappedReference))
maxAngle = math.ceil(np.max(self.unwrappedReference))
else:
minAngle = -180
maxAngle = 180
span = maxAngle - minAngle
if span == 0:
span = 0.01
self.minAngle = minAngle
self.maxAngle = maxAngle
self.span = span
tickcount = math.floor(self.chartHeight / 60)
for i in range(tickcount):
angle = minAngle + span * i / tickcount
y = self.topMargin + round((self.maxAngle - angle) / self.span * self.chartHeight)
if angle != minAngle and angle != maxAngle:
qp.setPen(QtGui.QPen(self.textColor))
if angle != 0:
digits = max(0, min(2, math.floor(3 - math.log10(abs(angle)))))
if digits == 0:
anglestr = str(round(angle))
else:
anglestr = str(round(angle, digits))
else:
anglestr = "0"
qp.drawText(3, y + 3, anglestr + "°")
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.chartWidth, y)
qp.drawLine(self.leftMargin - 5,
self.topMargin,
self.leftMargin + self.chartWidth,
self.topMargin)
qp.setPen(self.textColor)
qp.drawText(3, self.topMargin + 5, str(maxAngle) + "°")
qp.drawText(3, self.chartHeight + self.topMargin, str(minAngle) + "°")
if self.fixedSpan:
fstart = self.minFrequency
fstop = self.maxFrequency
else:
if len(self.data) > 0:
fstart = self.data[0].freq
fstop = self.data[len(self.data)-1].freq
else:
fstart = self.reference[0].freq
fstop = self.reference[len(self.reference) - 1].freq
self.fstart = fstart
self.fstop = fstop
# Draw bands if required
if self.bands.enabled:
self.drawBands(qp, fstart, fstop)
self.drawFrequencyTicks(qp)
self.drawData(qp, self.data, self.sweepColor)
self.drawData(qp, self.reference, self.referenceColor)
self.drawMarkers(qp)
def getYPosition(self, d: Datapoint) -> int:
if self.unwrap:
if d in self.data:
angle = self.unwrappedData[self.data.index(d)]
elif d in self.reference:
angle = self.unwrappedReference[self.reference.index(d)]
else:
angle = math.degrees(d.phase)
else:
angle = math.degrees(d.phase)
return self.topMargin + round((self.maxAngle - angle) / self.span * self.chartHeight)
def valueAtPosition(self, y) -> List[float]:
absy = y - self.topMargin
val = -1 * ((absy / self.chartHeight * self.span) - self.maxAngle)
return [val]

Wyświetl plik

@ -1,155 +0,0 @@
# NanoVNASaver - a python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
import logging
from PyQt5 import QtGui, QtCore
from NanoVNASaver.RFTools import Datapoint
from .Square import SquareChart
logger = logging.getLogger(__name__)
class PolarChart(SquareChart):
def __init__(self, name=""):
super().__init__(name)
self.chartWidth = 250
self.chartHeight = 250
self.setMinimumSize(self.chartWidth + 40, self.chartHeight + 40)
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, self.backgroundColor)
self.setPalette(pal)
self.setAutoFillBackground(True)
def paintEvent(self, a0: QtGui.QPaintEvent) -> None:
qp = QtGui.QPainter(self)
self.drawChart(qp)
self.drawValues(qp)
qp.end()
def drawChart(self, qp: QtGui.QPainter):
centerX = int(self.width()/2)
centerY = int(self.height()/2)
qp.setPen(QtGui.QPen(self.textColor))
qp.drawText(3, 15, self.name)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawEllipse(QtCore.QPoint(centerX, centerY),
int(self.chartWidth / 2),
int(self.chartHeight / 2))
qp.drawEllipse(QtCore.QPoint(centerX, centerY),
int(self.chartWidth / 4),
int(self.chartHeight / 4))
qp.drawLine(centerX - int(self.chartWidth / 2), centerY,
centerX + int(self.chartWidth / 2), centerY)
qp.drawLine(centerX, centerY - int(self.chartHeight / 2),
centerX, centerY + int(self.chartHeight / 2))
qp.drawLine(centerX + int(self.chartHeight / 2 * math.sin(math.pi / 4)),
centerY + int(self.chartHeight / 2 * math.sin(math.pi / 4)),
centerX - int(self.chartHeight / 2 * math.sin(math.pi / 4)),
centerY - int(self.chartHeight / 2 * math.sin(math.pi / 4)))
qp.drawLine(centerX + int(self.chartHeight / 2 * math.sin(math.pi / 4)),
centerY - int(self.chartHeight / 2 * math.sin(math.pi / 4)),
centerX - int(self.chartHeight / 2 * math.sin(math.pi / 4)),
centerY + int(self.chartHeight / 2 * math.sin(math.pi / 4)))
self.drawTitle(qp)
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
return
pen = QtGui.QPen(self.sweepColor)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(self.sweepColor)
line_pen.setWidth(self.lineThickness)
highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255))
highlighter.setWidth(1)
qp.setPen(pen)
for i in range(len(self.data)):
x = self.getXPosition(self.data[i])
y = self.height()/2 + self.data[i].im * -1 * self.chartHeight/2
qp.drawPoint(int(x), int(y))
if self.drawLines and i > 0:
prevx = self.getXPosition(self.data[i-1])
prevy = self.height() / 2 + self.data[i-1].im * -1 * self.chartHeight / 2
qp.setPen(line_pen)
qp.drawLine(x, y, prevx, prevy)
qp.setPen(pen)
pen.setColor(self.referenceColor)
line_pen.setColor(self.referenceColor)
qp.setPen(pen)
if len(self.data) > 0:
fstart = self.data[0].freq
fstop = self.data[len(self.data) - 1].freq
else:
fstart = self.reference[0].freq
fstop = self.reference[len(self.reference) - 1].freq
for i in range(len(self.reference)):
data = self.reference[i]
if data.freq < fstart or data.freq > fstop:
continue
x = self.getXPosition(self.reference[i])
y = self.height()/2 + data.im * -1 * self.chartHeight/2
qp.drawPoint(int(x), int(y))
if self.drawLines and i > 0:
prevx = self.getXPosition(self.reference[i-1])
prevy = self.height() / 2 + self.reference[i-1].im * -1 * self.chartHeight / 2
qp.setPen(line_pen)
qp.drawLine(x, y, prevx, prevy)
qp.setPen(pen)
# Now draw the markers
for m in self.markers:
if m.location != -1 and m.location < len(self.data):
x = self.getXPosition(self.data[m.location])
y = self.height() / 2 + self.data[m.location].im * -1 * self.chartHeight / 2
self.drawMarker(x, y, qp, m.color, self.markers.index(m)+1)
def getXPosition(self, d: Datapoint) -> int:
return self.width()/2 + d.re * self.chartWidth/2
def getYPosition(self, d: Datapoint) -> int:
return self.height()/2 + d.im * -1 * self.chartHeight/2
def mouseMoveEvent(self, a0: QtGui.QMouseEvent) -> None:
if a0.buttons() == QtCore.Qt.RightButton:
a0.ignore()
return
x = a0.x()
y = a0.y()
absx = x - (self.width() - self.chartWidth) / 2
absy = y - (self.height() - self.chartHeight) / 2
if absx < 0 or absx > self.chartWidth or absy < 0 or absy > self.chartHeight \
or len(self.data) == len(self.reference) == 0:
a0.ignore()
return
a0.accept()
if len(self.data) > 0:
target = self.data
else:
target = self.reference
positions = []
for d in target:
thisx = self.width() / 2 + d.re * self.chartWidth / 2
thisy = self.height() / 2 + d.im * -1 * self.chartHeight / 2
positions.append(math.sqrt((x - thisx)**2 + (y - thisy)**2))
minimum_position = positions.index(min(positions))
m = self.getActiveMarker()
if m is not None:
m.setFrequency(str(round(target[minimum_position].freq)))
m.frequencyInput.setText(str(round(target[minimum_position].freq)))
return

Wyświetl plik

@ -1,143 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
import logging
from typing import List
from PyQt5 import QtWidgets, QtGui
from NanoVNASaver.RFTools import Datapoint
from .Frequency import FrequencyChart
logger = logging.getLogger(__name__)
class QualityFactorChart(FrequencyChart):
def __init__(self, name=""):
super().__init__(name)
self.leftMargin = 35
self.chartWidth = 250
self.chartHeight = 250
self.fstart = 0
self.fstop = 0
self.minQ = 0
self.maxQ = 0
self.span = 0
self.minDisplayValue = 0
self.maxDisplayValue = 100
self.setMinimumSize(self.chartWidth + self.rightMargin + self.leftMargin,
self.chartHeight + self.topMargin + self.bottomMargin)
self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding))
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, self.backgroundColor)
self.setPalette(pal)
self.setAutoFillBackground(True)
def drawChart(self, qp: QtGui.QPainter):
super().drawChart(qp)
# Make up some sensible scaling here
if self.fixedValues:
maxQ = self.maxDisplayValue
minQ = self.minDisplayValue
else:
minQ = 0
maxQ = 0
for d in self.data:
Q = d.qFactor()
if Q > maxQ:
maxQ = Q
scale = 0
if maxQ > 0:
scale = max(scale, math.floor(math.log10(maxQ)))
maxQ = math.ceil(maxQ / 10 ** scale) * 10 ** scale
self.minQ = minQ
self.maxQ = maxQ
self.span = self.maxQ - self.minQ
if self.span == 0:
return # No data to draw the graph from
tickcount = math.floor(self.chartHeight / 60)
for i in range(tickcount):
q = self.minQ + i * self.span / tickcount
y = self.topMargin + round((self.maxQ - q) / self.span * self.chartHeight)
if q < 10:
q = round(q, 2)
elif q < 20:
q = round(q, 1)
else:
q = round(q)
qp.setPen(QtGui.QPen(self.textColor))
qp.drawText(3, y+3, str(q))
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin-5, y, self.leftMargin + self.chartWidth, y)
qp.drawLine(self.leftMargin - 5,
self.topMargin,
self.leftMargin + self.chartWidth, self.topMargin)
qp.setPen(self.textColor)
if maxQ < 10:
qstr = str(round(maxQ, 2))
elif maxQ < 20:
qstr = str(round(maxQ, 1))
else:
qstr = str(round(maxQ))
qp.drawText(3, 35, qstr)
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
return
if self.span == 0:
return
pen = QtGui.QPen(self.sweepColor)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(self.sweepColor)
line_pen.setWidth(self.lineThickness)
highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255))
highlighter.setWidth(1)
if self.fixedSpan:
fstart = self.minFrequency
fstop = self.maxFrequency
else:
if len(self.data) > 0:
fstart = self.data[0].freq
fstop = self.data[len(self.data)-1].freq
else:
fstart = self.reference[0].freq
fstop = self.reference[len(self.reference) - 1].freq
self.fstart = fstart
self.fstop = fstop
# Draw bands if required
if self.bands.enabled:
self.drawBands(qp, fstart, fstop)
self.drawFrequencyTicks(qp)
self.drawData(qp, self.data, self.sweepColor)
self.drawData(qp, self.reference, self.referenceColor)
self.drawMarkers(qp)
def getYPosition(self, d: Datapoint) -> int:
Q = d.qFactor()
return self.topMargin + round((self.maxQ - Q) / self.span * self.chartHeight)
def valueAtPosition(self, y) -> List[float]:
absy = y - self.topMargin
val = -1 * ((absy / self.chartHeight * self.span) - self.maxQ)
return [val]

Wyświetl plik

@ -1,520 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
import logging
from typing import List
from PyQt5 import QtWidgets, QtGui
from NanoVNASaver.Marker import Marker
from NanoVNASaver.RFTools import Datapoint
from .Chart import Chart
from .Frequency import FrequencyChart
logger = logging.getLogger(__name__)
class RealImaginaryChart(FrequencyChart):
def __init__(self, name=""):
super().__init__(name)
self.leftMargin = 45
self.rightMargin = 45
self.chartWidth = 230
self.chartHeight = 250
self.fstart = 0
self.fstop = 0
self.span_real = 0.01
self.span_imag = 0.01
self.max_real = 0
self.max_imag = 0
self.maxDisplayReal = 100
self.maxDisplayImag = 100
self.minDisplayReal = 0
self.minDisplayImag = -100
#
# Build the context menu
#
self.y_menu.clear()
self.y_action_automatic = QtWidgets.QAction("Automatic")
self.y_action_automatic.setCheckable(True)
self.y_action_automatic.setChecked(True)
self.y_action_automatic.changed.connect(
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked()))
self.y_action_fixed_span = QtWidgets.QAction("Fixed span")
self.y_action_fixed_span.setCheckable(True)
self.y_action_fixed_span.changed.connect(
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked()))
mode_group = QtWidgets.QActionGroup(self)
mode_group.addAction(self.y_action_automatic)
mode_group.addAction(self.y_action_fixed_span)
self.y_menu.addAction(self.y_action_automatic)
self.y_menu.addAction(self.y_action_fixed_span)
self.y_menu.addSeparator()
self.action_set_fixed_maximum_real = QtWidgets.QAction(
f"Maximum R ({self.maxDisplayReal})")
self.action_set_fixed_maximum_real.triggered.connect(
self.setMaximumRealValue)
self.action_set_fixed_minimum_real = QtWidgets.QAction(
f"Minimum R ({self.minDisplayReal})")
self.action_set_fixed_minimum_real.triggered.connect(
self.setMinimumRealValue)
self.action_set_fixed_maximum_imag = QtWidgets.QAction(
f"Maximum jX ({self.maxDisplayImag})")
self.action_set_fixed_maximum_imag.triggered.connect(
self.setMaximumImagValue)
self.action_set_fixed_minimum_imag = QtWidgets.QAction(
f"Minimum jX ({self.minDisplayImag})")
self.action_set_fixed_minimum_imag.triggered.connect(
self.setMinimumImagValue)
self.y_menu.addAction(self.action_set_fixed_maximum_real)
self.y_menu.addAction(self.action_set_fixed_minimum_real)
self.y_menu.addSeparator()
self.y_menu.addAction(self.action_set_fixed_maximum_imag)
self.y_menu.addAction(self.action_set_fixed_minimum_imag)
#
# Set up size policy and palette
#
self.setMinimumSize(
self.chartWidth + self.leftMargin + self.rightMargin,
self.chartHeight + 40)
self.setSizePolicy(
QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding))
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, self.backgroundColor)
self.setPalette(pal)
self.setAutoFillBackground(True)
def copy(self):
new_chart: RealImaginaryChart = super().copy()
new_chart.maxDisplayReal = self.maxDisplayReal
new_chart.maxDisplayImag = self.maxDisplayImag
new_chart.minDisplayReal = self.minDisplayReal
new_chart.minDisplayImag = self.minDisplayImag
return new_chart
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(self.textColor))
qp.drawText(self.leftMargin + 5, 15,
f"{self.name} (\N{OHM SIGN})")
qp.drawText(10, 15, "R")
qp.drawText(self.leftMargin + self.chartWidth + 10, 15, "X")
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin,
self.topMargin - 5,
self.leftMargin,
self.topMargin + self.chartHeight + 5)
qp.drawLine(self.leftMargin-5,
self.topMargin + self.chartHeight,
self.leftMargin + self.chartWidth + 5,
self.topMargin + self.chartHeight)
self.drawTitle(qp)
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
return
pen = QtGui.QPen(self.sweepColor)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(self.sweepColor)
line_pen.setWidth(self.lineThickness)
highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255))
highlighter.setWidth(1)
if self.fixedSpan:
fstart = self.minFrequency
fstop = self.maxFrequency
else:
if len(self.data) > 0:
fstart = self.data[0].freq
fstop = self.data[len(self.data)-1].freq
else:
fstart = self.reference[0].freq
fstop = self.reference[len(self.reference) - 1].freq
self.fstart = fstart
self.fstop = fstop
# Draw bands if required
if self.bands.enabled:
self.drawBands(qp, fstart, fstop)
# Find scaling
if self.fixedValues:
min_real = self.minDisplayReal
max_real = self.maxDisplayReal
min_imag = self.minDisplayImag
max_imag = self.maxDisplayImag
else:
min_real = 1000
min_imag = 1000
max_real = 0
max_imag = -1000
for d in self.data:
imp = d.impedance()
re, im = imp.real, imp.imag
if re > max_real:
max_real = re
if re < min_real:
min_real = re
if im > max_imag:
max_imag = im
if im < min_imag:
min_imag = im
for d in self.reference: # Also check min/max for the reference sweep
if d.freq < fstart or d.freq > fstop:
continue
imp = d.impedance()
re, im = imp.real, imp.imag
if re > max_real:
max_real = re
if re < min_real:
min_real = re
if im > max_imag:
max_imag = im
if im < min_imag:
min_imag = im
# Always have at least 8 numbered horizontal lines
max_real = max(8, math.ceil(max_real))
min_real = max(0, math.floor(min_real)) # Negative real resistance? No.
max_imag = math.ceil(max_imag)
min_imag = math.floor(min_imag)
if max_imag - min_imag < 8:
missing = 8 - (max_imag - min_imag)
max_imag += math.ceil(missing/2)
min_imag -= math.floor(missing/2)
if 0 > max_imag > -2:
max_imag = 0
if 0 < min_imag < 2:
min_imag = 0
if (max_imag - min_imag) > 8 and min_imag < 0 < max_imag:
# We should show a "0" line for the reactive part
span = max_imag - min_imag
step_size = span / 8
if max_imag < step_size:
# The 0 line is the first step after the top. Scale accordingly.
max_imag = -min_imag/7
elif -min_imag < step_size:
# The 0 line is the last step before the bottom. Scale accordingly.
min_imag = -max_imag/7
else:
# Scale max_imag to be a whole factor of min_imag
num_min = math.floor(min_imag/step_size * -1)
num_max = 8 - num_min
max_imag = num_max * (min_imag / num_min) * -1
self.max_real = max_real
self.max_imag = max_imag
span_real = max_real - min_real
if span_real == 0:
span_real = 0.01
self.span_real = span_real
span_imag = max_imag - min_imag
if span_imag == 0:
span_imag = 0.01
self.span_imag = span_imag
# We want one horizontal tick per 50 pixels, at most
horizontal_ticks = math.floor(self.chartHeight/50)
for i in range(horizontal_ticks):
y = self.topMargin + round(i * self.chartHeight / horizontal_ticks)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.chartWidth + 5, y)
qp.setPen(QtGui.QPen(self.textColor))
re = max_real - i * span_real / horizontal_ticks
im = max_imag - i * span_imag / horizontal_ticks
qp.drawText(3, y + 4, str(round(re, 1)))
qp.drawText(self.leftMargin + self.chartWidth + 8, y + 4, str(round(im, 1)))
qp.drawText(3, self.chartHeight + self.topMargin, str(round(min_real, 1)))
qp.drawText(self.leftMargin + self.chartWidth + 8,
self.chartHeight + self.topMargin,
str(round(min_imag, 1)))
self.drawFrequencyTicks(qp)
primary_pen = pen
secondary_pen = QtGui.QPen(self.secondarySweepColor)
if len(self.data) > 0:
c = QtGui.QColor(self.sweepColor)
c.setAlpha(255)
pen = QtGui.QPen(c)
pen.setWidth(2)
qp.setPen(pen)
qp.drawLine(20, 9, 25, 9)
c = QtGui.QColor(self.secondarySweepColor)
c.setAlpha(255)
pen.setColor(c)
qp.setPen(pen)
qp.drawLine(self.leftMargin + self.chartWidth, 9,
self.leftMargin + self.chartWidth + 5, 9)
primary_pen.setWidth(self.pointSize)
secondary_pen.setWidth(self.pointSize)
line_pen.setWidth(self.lineThickness)
for i in range(len(self.data)):
x = self.getXPosition(self.data[i])
y_re = self.getReYPosition(self.data[i])
y_im = self.getImYPosition(self.data[i])
qp.setPen(primary_pen)
if self.isPlotable(x, y_re):
qp.drawPoint(x, y_re)
qp.setPen(secondary_pen)
if self.isPlotable(x, y_im):
qp.drawPoint(x, y_im)
if self.drawLines and i > 0:
prev_x = self.getXPosition(self.data[i - 1])
prev_y_re = self.getReYPosition(self.data[i-1])
prev_y_im = self.getImYPosition(self.data[i-1])
# Real part first
line_pen.setColor(self.sweepColor)
qp.setPen(line_pen)
if self.isPlotable(x, y_re) and self.isPlotable(prev_x, prev_y_re):
qp.drawLine(x, y_re, prev_x, prev_y_re)
elif self.isPlotable(x, y_re) and not self.isPlotable(prev_x, prev_y_re):
new_x, new_y = self.getPlotable(x, y_re, prev_x, prev_y_re)
qp.drawLine(x, y_re, new_x, new_y)
elif not self.isPlotable(x, y_re) and self.isPlotable(prev_x, prev_y_re):
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
qp.drawLine(prev_x, prev_y_re, new_x, new_y)
# Imag part second
line_pen.setColor(self.secondarySweepColor)
qp.setPen(line_pen)
if self.isPlotable(x, y_im) and self.isPlotable(prev_x, prev_y_im):
qp.drawLine(x, y_im, prev_x, prev_y_im)
elif self.isPlotable(x, y_im) and not self.isPlotable(prev_x, prev_y_im):
new_x, new_y = self.getPlotable(x, y_im, prev_x, prev_y_im)
qp.drawLine(x, y_im, new_x, new_y)
elif not self.isPlotable(x, y_im) and self.isPlotable(prev_x, prev_y_im):
new_x, new_y = self.getPlotable(prev_x, prev_y_im, x, y_im)
qp.drawLine(prev_x, prev_y_im, new_x, new_y)
primary_pen.setColor(self.referenceColor)
line_pen.setColor(self.referenceColor)
secondary_pen.setColor(self.secondaryReferenceColor)
qp.setPen(primary_pen)
if len(self.reference) > 0:
c = QtGui.QColor(self.referenceColor)
c.setAlpha(255)
pen = QtGui.QPen(c)
pen.setWidth(2)
qp.setPen(pen)
qp.drawLine(20, 14, 25, 14)
c = QtGui.QColor(self.secondaryReferenceColor)
c.setAlpha(255)
pen = QtGui.QPen(c)
pen.setWidth(2)
qp.setPen(pen)
qp.drawLine(self.leftMargin + self.chartWidth, 14,
self.leftMargin + self.chartWidth + 5, 14)
for i in range(len(self.reference)):
if self.reference[i].freq < fstart or self.reference[i].freq > fstop:
continue
x = self.getXPosition(self.reference[i])
y_re = self.getReYPosition(self.reference[i])
y_im = self.getImYPosition(self.reference[i])
qp.setPen(primary_pen)
if self.isPlotable(x, y_re):
qp.drawPoint(x, y_re)
qp.setPen(secondary_pen)
if self.isPlotable(x, y_im):
qp.drawPoint(x, y_im)
if self.drawLines and i > 0:
prev_x = self.getXPosition(self.reference[i - 1])
prev_y_re = self.getReYPosition(self.reference[i-1])
prev_y_im = self.getImYPosition(self.reference[i-1])
line_pen.setColor(self.referenceColor)
qp.setPen(line_pen)
# Real part first
if self.isPlotable(x, y_re) and self.isPlotable(prev_x, prev_y_re):
qp.drawLine(x, y_re, prev_x, prev_y_re)
elif self.isPlotable(x, y_re) and not self.isPlotable(prev_x, prev_y_re):
new_x, new_y = self.getPlotable(x, y_re, prev_x, prev_y_re)
qp.drawLine(x, y_re, new_x, new_y)
elif not self.isPlotable(x, y_re) and self.isPlotable(prev_x, prev_y_re):
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
qp.drawLine(prev_x, prev_y_re, new_x, new_y)
line_pen.setColor(self.secondaryReferenceColor)
qp.setPen(line_pen)
# Imag part second
if self.isPlotable(x, y_im) and self.isPlotable(prev_x, prev_y_im):
qp.drawLine(x, y_im, prev_x, prev_y_im)
elif self.isPlotable(x, y_im) and not self.isPlotable(prev_x, prev_y_im):
new_x, new_y = self.getPlotable(x, y_im, prev_x, prev_y_im)
qp.drawLine(x, y_im, new_x, new_y)
elif not self.isPlotable(x, y_im) and self.isPlotable(prev_x, prev_y_im):
new_x, new_y = self.getPlotable(prev_x, prev_y_im, x, y_im)
qp.drawLine(prev_x, prev_y_im, new_x, new_y)
# Now draw the markers
for m in self.markers:
if m.location != -1:
x = self.getXPosition(self.data[m.location])
y_re = self.getReYPosition(self.data[m.location])
y_im = self.getImYPosition(self.data[m.location])
self.drawMarker(x, y_re, 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:
im = d.impedance().imag
return self.topMargin + round((self.max_imag - im) / self.span_imag * self.chartHeight)
def getReYPosition(self, d: Datapoint) -> int:
re = d.impedance().real
return self.topMargin + round((self.max_real - re) / self.span_real * self.chartHeight)
def valueAtPosition(self, y) -> List[float]:
absy = y - self.topMargin
valRe = -1 * ((absy / self.chartHeight * self.span_real) - self.max_real)
valIm = -1 * ((absy / self.chartHeight * self.span_imag) - self.max_imag)
return [valRe, valIm]
def zoomTo(self, x1, y1, x2, y2):
val1 = self.valueAtPosition(y1)
val2 = self.valueAtPosition(y2)
if len(val1) == len(val2) == 2 and val1[0] != val2[0]:
self.minDisplayReal = round(min(val1[0], val2[0]), 2)
self.maxDisplayReal = round(max(val1[0], val2[0]), 2)
self.minDisplayImag = round(min(val1[1], val2[1]), 2)
self.maxDisplayImag = round(max(val1[1], val2[1]), 2)
self.setFixedValues(True)
freq1 = max(1, self.frequencyAtPosition(x1, limit=False))
freq2 = max(1, self.frequencyAtPosition(x2, limit=False))
if freq1 > 0 and freq2 > 0 and freq1 != freq2:
self.minFrequency = min(freq1, freq2)
self.maxFrequency = max(freq1, freq2)
self.setFixedSpan(True)
self.update()
def getNearestMarker(self, x, y) -> Marker:
if len(self.data) == 0:
return None
shortest = 10**6
nearest = None
for m in self.markers:
mx, _ = self.getPosition(self.data[m.location])
myr = self.getReYPosition(self.data[m.location])
myi = self.getImYPosition(self.data[m.location])
dx = abs(x - mx)
dy = min(abs(y - myr), abs(y-myi))
distance = math.sqrt(dx**2 + dy**2)
if distance < shortest:
shortest = distance
nearest = m
return nearest
def setMinimumRealValue(self):
min_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Minimum real value",
"Set minimum real value", value=self.minDisplayReal,
decimals=2)
if not selected:
return
if not (self.fixedValues and min_val >= self.maxDisplayReal):
self.minDisplayReal = min_val
if self.fixedValues:
self.update()
def setMaximumRealValue(self):
max_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Maximum real value",
"Set maximum real value", value=self.maxDisplayReal,
decimals=2)
if not selected:
return
if not (self.fixedValues and max_val <= self.minDisplayReal):
self.maxDisplayReal = max_val
if self.fixedValues:
self.update()
def setMinimumImagValue(self):
min_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Minimum imaginary value",
"Set minimum imaginary value", value=self.minDisplayImag,
decimals=2)
if not selected:
return
if not (self.fixedValues and min_val >= self.maxDisplayImag):
self.minDisplayImag = min_val
if self.fixedValues:
self.update()
def setMaximumImagValue(self):
max_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Maximum imaginary value",
"Set maximum imaginary value", value=self.maxDisplayImag,
decimals=2)
if not selected:
return
if not (self.fixedValues and max_val <= self.minDisplayImag):
self.maxDisplayImag = max_val
if self.fixedValues:
self.update()
def setFixedValues(self, fixed_values: bool):
self.fixedValues = fixed_values
if (fixed_values and
(self.minDisplayReal >= self.maxDisplayReal or
self.minDisplayImag > self.maxDisplayImag)):
self.fixedValues = False
self.y_action_automatic.setChecked(True)
self.y_action_fixed_span.setChecked(False)
self.update()
def contextMenuEvent(self, event):
self.action_set_fixed_start.setText(
f"Start ({Chart.shortenFrequency(self.minFrequency)})")
self.action_set_fixed_stop.setText(
f"Stop ({Chart.shortenFrequency(self.maxFrequency)})")
self.action_set_fixed_minimum_real.setText(
f"Minimum R ({self.minDisplayReal})")
self.action_set_fixed_maximum_real.setText(
f"Maximum R ({self.maxDisplayReal})")
self.action_set_fixed_minimum_imag.setText(
f"Minimum jX ({self.minDisplayImag})")
self.action_set_fixed_maximum_imag.setText(
f"Maximum jX ({self.maxDisplayImag})")
self.menu.exec_(event.globalPos())

Wyświetl plik

@ -1,181 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
import logging
from typing import List
from PyQt5 import QtWidgets, QtGui
from NanoVNASaver.RFTools import Datapoint
from .Frequency import FrequencyChart
from .LogMag import LogMagChart
logger = logging.getLogger(__name__)
class SParameterChart(FrequencyChart):
def __init__(self, name=""):
super().__init__(name)
self.leftMargin = 30
self.chartWidth = 250
self.chartHeight = 250
self.minDisplayValue = -1
self.maxDisplayValue = 1
self.fixedValues = True
self.y_action_automatic.setChecked(False)
self.y_action_fixed_span.setChecked(True)
self.minValue = 0
self.maxValue = 1
self.span = 1
self.isInverted = False
self.setMinimumSize(self.chartWidth + self.rightMargin + self.leftMargin,
self.chartHeight + self.topMargin + self.bottomMargin)
self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding))
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, self.backgroundColor)
self.setPalette(pal)
self.setAutoFillBackground(True)
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(self.textColor))
qp.drawText(int(round(self.chartWidth / 2)) - 20, 15, self.name + "")
qp.drawText(10, 15, "Real")
qp.drawText(self.leftMargin + self.chartWidth - 15, 15, "Imag")
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin, self.topMargin - 5,
self.leftMargin, self.topMargin+self.chartHeight+5)
qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight,
self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
return
pen = QtGui.QPen(self.sweepColor)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(self.sweepColor)
line_pen.setWidth(self.lineThickness)
highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255))
highlighter.setWidth(1)
if not self.fixedSpan:
if len(self.data) > 0:
fstart = self.data[0].freq
fstop = self.data[len(self.data)-1].freq
else:
fstart = self.reference[0].freq
fstop = self.reference[len(self.reference) - 1].freq
self.fstart = fstart
self.fstop = fstop
else:
fstart = self.fstart = self.minFrequency
fstop = self.fstop = self.maxFrequency
# Draw bands if required
if self.bands.enabled:
self.drawBands(qp, fstart, fstop)
if self.fixedValues:
maxValue = self.maxDisplayValue
minValue = self.minDisplayValue
self.maxValue = maxValue
self.minValue = minValue
else:
# Find scaling
minValue = -1
maxValue = 1
self.maxValue = maxValue
self.minValue = minValue
# for d in self.data:
# val = d.re
# if val > maxValue:
# maxValue = val
# if val < minValue:
# minValue = val
# for d in self.reference: # Also check min/max for the reference sweep
# if d.freq < self.fstart or d.freq > self.fstop:
# continue
# logmag = self.logMag(d)
# if logmag > maxValue:
# maxValue = logmag
# if logmag < minValue:
# minValue = logmag
# minValue = 10*math.floor(minValue/10)
# self.minValue = minValue
# maxValue = 10*math.ceil(maxValue/10)
# self.maxValue = maxValue
span = maxValue-minValue
if span == 0:
span = 0.01
self.span = span
tick_count = math.floor(self.chartHeight / 60)
tick_step = self.span / tick_count
for i in range(tick_count):
val = minValue + i * tick_step
y = self.topMargin + round((maxValue - val)/span*self.chartHeight)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin-5, y, self.leftMargin+self.chartWidth, y)
if val > minValue and val != maxValue:
qp.setPen(QtGui.QPen(self.textColor))
qp.drawText(3, y + 4, str(round(val, 2)))
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, self.topMargin,
self.leftMargin + self.chartWidth, self.topMargin)
qp.setPen(self.textColor)
qp.drawText(3, self.topMargin + 4, str(maxValue))
qp.drawText(3, self.chartHeight+self.topMargin, str(minValue))
self.drawFrequencyTicks(qp)
self.drawData(qp, self.data, self.sweepColor, self.getReYPosition)
self.drawData(qp, self.reference, self.referenceColor, self.getReYPosition)
self.drawData(qp, self.data, self.secondarySweepColor, self.getImYPosition)
self.drawData(qp, self.reference, self.secondaryReferenceColor, self.getImYPosition)
self.drawMarkers(qp, y_function=self.getReYPosition)
self.drawMarkers(qp, y_function=self.getImYPosition)
def getYPosition(self, d: Datapoint) -> int:
return self.topMargin + round((self.maxValue - d.re) / self.span * self.chartHeight)
def getReYPosition(self, d: Datapoint) -> int:
return self.topMargin + round((self.maxValue - d.re) / self.span * self.chartHeight)
def getImYPosition(self, d: Datapoint) -> int:
return self.topMargin + round((self.maxValue - d.im) / self.span * self.chartHeight)
def valueAtPosition(self, y) -> List[float]:
absy = y - self.topMargin
val = -1 * ((absy / self.chartHeight * self.span) - self.maxValue)
return [val]
def logMag(self, p: Datapoint) -> float:
if self.isInverted:
return -p.gain
return p.gain
def copy(self):
new_chart: LogMagChart = super().copy()
new_chart.isInverted = self.isInverted
new_chart.span = self.span
return new_chart

Wyświetl plik

@ -1,203 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
import logging
from PyQt5 import QtGui, QtCore
from NanoVNASaver.RFTools import Datapoint
from .Square import SquareChart
logger = logging.getLogger(__name__)
class SmithChart(SquareChart):
def __init__(self, name=""):
super().__init__(name)
self.chartWidth = 250
self.chartHeight = 250
self.setMinimumSize(self.chartWidth + 40, self.chartHeight + 40)
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, self.backgroundColor)
self.setPalette(pal)
self.setAutoFillBackground(True)
def paintEvent(self, a0: QtGui.QPaintEvent) -> None:
qp = QtGui.QPainter(self)
# qp.begin(self) # Apparently not needed?
self.drawSmithChart(qp)
self.drawValues(qp)
qp.end()
def drawSmithChart(self, qp: QtGui.QPainter):
centerX = int(self.width()/2)
centerY = int(self.height()/2)
qp.setPen(QtGui.QPen(self.textColor))
qp.drawText(3, 15, self.name)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawEllipse(QtCore.QPoint(centerX, centerY),
int(self.chartWidth / 2),
int(self.chartHeight / 2))
qp.drawLine(
centerX - int(self.chartWidth / 2),
centerY,
centerX + int(self.chartWidth / 2),
centerY)
qp.drawEllipse(QtCore.QPoint(centerX + int(self.chartWidth/4), centerY),
int(self.chartWidth/4), int(self.chartHeight/4)) # Re(Z) = 1
qp.drawEllipse(QtCore.QPoint(centerX + int(2/3*self.chartWidth/2), centerY),
int(self.chartWidth/6), int(self.chartHeight/6)) # Re(Z) = 2
qp.drawEllipse(QtCore.QPoint(centerX + int(3 / 4 * self.chartWidth / 2), centerY),
int(self.chartWidth / 8), int(self.chartHeight / 8)) # Re(Z) = 3
qp.drawEllipse(QtCore.QPoint(centerX + int(5 / 6 * self.chartWidth / 2), centerY),
int(self.chartWidth / 12), int(self.chartHeight / 12)) # Re(Z) = 5
qp.drawEllipse(QtCore.QPoint(centerX + int(1 / 3 * self.chartWidth / 2), centerY),
int(self.chartWidth / 3), int(self.chartHeight / 3)) # Re(Z) = 0.5
qp.drawEllipse(QtCore.QPoint(centerX + int(1 / 6 * self.chartWidth / 2), centerY),
int(self.chartWidth / 2.4), int(self.chartHeight / 2.4)) # Re(Z) = 0.2
qp.drawArc(centerX + int(3/8*self.chartWidth), centerY, int(self.chartWidth/4),
int(self.chartWidth/4), 90*16, 152*16) # Im(Z) = -5
qp.drawArc(centerX + int(3/8*self.chartWidth), centerY, int(self.chartWidth/4),
-int(self.chartWidth/4), -90 * 16, -152 * 16) # Im(Z) = 5
qp.drawArc(centerX + int(self.chartWidth/4), centerY, int(self.chartWidth/2),
int(self.chartHeight/2), 90*16, 127*16) # Im(Z) = -2
qp.drawArc(centerX + int(self.chartWidth/4), centerY, int(self.chartWidth/2),
-int(self.chartHeight/2), -90*16, -127*16) # Im(Z) = 2
qp.drawArc(centerX, centerY,
self.chartWidth, self.chartHeight,
90*16, 90*16) # Im(Z) = -1
qp.drawArc(centerX, centerY,
self.chartWidth, -self.chartHeight,
-90 * 16, -90 * 16) # Im(Z) = 1
qp.drawArc(centerX - int(self.chartWidth / 2), centerY,
self.chartWidth * 2, self.chartHeight * 2,
int(99.5*16), int(43.5*16)) # Im(Z) = -0.5
qp.drawArc(centerX - int(self.chartWidth / 2), centerY,
self.chartWidth * 2, -self.chartHeight * 2,
int(-99.5 * 16), int(-43.5 * 16)) # Im(Z) = 0.5
qp.drawArc(centerX - self.chartWidth * 2, centerY,
self.chartWidth * 5, self.chartHeight * 5,
int(93.85 * 16), int(18.85 * 16)) # Im(Z) = -0.2
qp.drawArc(centerX - self.chartWidth*2, centerY,
self.chartWidth*5, -self.chartHeight*5,
int(-93.85 * 16), int(-18.85 * 16)) # Im(Z) = 0.2
self.drawTitle(qp)
qp.setPen(self.swrColor)
for swr in self.swrMarkers:
if swr <= 1:
continue
gamma = (swr - 1)/(swr + 1)
r = round(gamma * self.chartWidth/2)
qp.drawEllipse(QtCore.QPoint(centerX, centerY), r, r)
qp.drawText(
QtCore.QRect(centerX - 50, centerY - 4 + r, 100, 20),
QtCore.Qt.AlignCenter, str(swr))
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
return
pen = QtGui.QPen(self.sweepColor)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(self.sweepColor)
line_pen.setWidth(self.lineThickness)
highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255))
highlighter.setWidth(1)
qp.setPen(pen)
for i in range(len(self.data)):
x = self.getXPosition(self.data[i])
y = int(self.height()/2 + self.data[i].im * -1 * self.chartHeight/2)
qp.drawPoint(x, y)
if self.drawLines and i > 0:
prevx = self.getXPosition(self.data[i-1])
prevy = int(self.height() / 2 + self.data[i-1].im * -1 * self.chartHeight / 2)
qp.setPen(line_pen)
qp.drawLine(x, y, prevx, prevy)
qp.setPen(pen)
pen.setColor(self.referenceColor)
line_pen.setColor(self.referenceColor)
qp.setPen(pen)
if len(self.data) > 0:
fstart = self.data[0].freq
fstop = self.data[len(self.data)-1].freq
else:
fstart = self.reference[0].freq
fstop = self.reference[len(self.reference)-1].freq
for i in range(len(self.reference)):
data = self.reference[i]
if data.freq < fstart or data.freq > fstop:
continue
x = self.getXPosition(data)
y = int(self.height()/2 + data.im * -1 * self.chartHeight/2)
qp.drawPoint(x, y)
if self.drawLines and i > 0:
prevx = self.getXPosition(self.reference[i-1])
prevy = int(self.height() / 2 + self.reference[i-1].im * -1 * self.chartHeight / 2)
qp.setPen(line_pen)
qp.drawLine(x, y, prevx, prevy)
qp.setPen(pen)
# Now draw the markers
for m in self.markers:
if m.location != -1:
x = self.getXPosition(self.data[m.location])
y = self.height() / 2 + self.data[m.location].im * -1 * self.chartHeight / 2
self.drawMarker(x, y, qp, m.color, self.markers.index(m)+1)
def getXPosition(self, d: Datapoint) -> int:
return int(self.width()/2 + d.re * self.chartWidth/2)
def getYPosition(self, d: Datapoint) -> int:
return int(self.height()/2 + d.im * -1 * self.chartHeight/2)
def heightForWidth(self, a0: int) -> int:
return a0
def mouseMoveEvent(self, a0: QtGui.QMouseEvent) -> None:
if a0.buttons() == QtCore.Qt.RightButton:
a0.ignore()
return
x = a0.x()
y = a0.y()
absx = x - (self.width() - self.chartWidth) / 2
absy = y - (self.height() - self.chartHeight) / 2
if absx < 0 or absx > self.chartWidth or absy < 0 or absy > self.chartHeight \
or len(self.data) == len(self.reference) == 0:
a0.ignore()
return
a0.accept()
if len(self.data) > 0:
target = self.data
else:
target = self.reference
positions = []
for d in target:
thisx = self.width() / 2 + d.re * self.chartWidth / 2
thisy = self.height() / 2 + d.im * -1 * self.chartHeight / 2
positions.append(math.sqrt((x - thisx)**2 + (y - thisy)**2))
minimum_position = positions.index(min(positions))
m = self.getActiveMarker()
if m is not None:
m.setFrequency(str(round(target[minimum_position].freq)))
m.frequencyInput.setText(str(round(target[minimum_position].freq)))
return

Wyświetl plik

@ -1,44 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 QtWidgets, QtGui
from NanoVNASaver.Charts.Chart import Chart
logger = logging.getLogger(__name__)
class SquareChart(Chart):
def __init__(self, name):
super().__init__(name)
sizepolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Fixed,
QtWidgets.QSizePolicy.MinimumExpanding)
self.setSizePolicy(sizepolicy)
self.chartWidth = self.width()-40
self.chartHeight = self.height()-40
def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
if not self.isPopout:
self.setFixedWidth(a0.size().height())
self.chartWidth = a0.size().height()-40
self.chartHeight = a0.size().height()-40
else:
min_dimension = min(a0.size().height(), a0.size().width())
self.chartWidth = self.chartHeight = min_dimension - 40
self.update()

Wyświetl plik

@ -1,535 +0,0 @@
# NanoVNASaver - a python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
import logging
import numpy as np
from PyQt5 import QtWidgets, QtGui, QtCore
from .Chart import Chart
logger = logging.getLogger(__name__)
class TDRChart(Chart):
maxDisplayLength = 50
minDisplayLength = 0
fixedSpan = False
minImpedance = 0
maxImpedance = 1000
fixedValues = False
markerLocation = -1
def __init__(self, name):
super().__init__(name)
self.tdrWindow = None
self.leftMargin = 30
self.rightMargin = 20
self.bottomMargin = 25
self.topMargin = 20
self.setMinimumSize(300, 300)
self.setSizePolicy(
QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding))
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, self.backgroundColor)
self.setPalette(pal)
self.setAutoFillBackground(True)
self.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
self.menu = QtWidgets.QMenu()
self.reset = QtWidgets.QAction("Reset")
self.reset.triggered.connect(self.resetDisplayLimits)
self.menu.addAction(self.reset)
self.x_menu = QtWidgets.QMenu("Length axis")
self.mode_group = QtWidgets.QActionGroup(self.x_menu)
self.action_automatic = QtWidgets.QAction("Automatic")
self.action_automatic.setCheckable(True)
self.action_automatic.setChecked(True)
self.action_automatic.changed.connect(
lambda: self.setFixedSpan(self.action_fixed_span.isChecked()))
self.action_fixed_span = QtWidgets.QAction("Fixed span")
self.action_fixed_span.setCheckable(True)
self.action_fixed_span.changed.connect(
lambda: self.setFixedSpan(self.action_fixed_span.isChecked()))
self.mode_group.addAction(self.action_automatic)
self.mode_group.addAction(self.action_fixed_span)
self.x_menu.addAction(self.action_automatic)
self.x_menu.addAction(self.action_fixed_span)
self.x_menu.addSeparator()
self.action_set_fixed_start = QtWidgets.QAction(
f"Start ({self.minDisplayLength})")
self.action_set_fixed_start.triggered.connect(self.setMinimumLength)
self.action_set_fixed_stop = QtWidgets.QAction(
f"Stop ({self.maxDisplayLength})")
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_stop)
self.y_menu = QtWidgets.QMenu("Impedance axis")
self.y_mode_group = QtWidgets.QActionGroup(self.y_menu)
self.y_action_automatic = QtWidgets.QAction("Automatic")
self.y_action_automatic.setCheckable(True)
self.y_action_automatic.setChecked(True)
self.y_action_automatic.changed.connect(
lambda: self.setFixedValues(self.y_action_fixed.isChecked()))
self.y_action_fixed = QtWidgets.QAction("Fixed")
self.y_action_fixed.setCheckable(True)
self.y_action_fixed.changed.connect(
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_fixed)
self.y_menu.addAction(self.y_action_automatic)
self.y_menu.addAction(self.y_action_fixed)
self.y_menu.addSeparator()
self.y_action_set_fixed_maximum = QtWidgets.QAction(
f"Maximum ({self.maxImpedance})")
self.y_action_set_fixed_maximum.triggered.connect(self.setMaximumImpedance)
self.y_action_set_fixed_minimum = QtWidgets.QAction(
f"Minimum ({self.minImpedance})")
self.y_action_set_fixed_minimum.triggered.connect(self.setMinimumImpedance)
self.y_menu.addAction(self.y_action_set_fixed_maximum)
self.y_menu.addAction(self.y_action_set_fixed_minimum)
self.menu.addMenu(self.x_menu)
self.menu.addMenu(self.y_menu)
self.menu.addSeparator()
self.menu.addAction(self.action_save_screenshot)
self.action_popout = QtWidgets.QAction("Popout chart")
self.action_popout.triggered.connect(
lambda: self.popoutRequested.emit(self))
self.menu.addAction(self.action_popout)
self.chartWidth = self.width() - self.leftMargin - self.rightMargin
self.chartHeight = self.height() - self.bottomMargin - self.topMargin
def contextMenuEvent(self, event):
self.action_set_fixed_start.setText(
f"Start ({self.minDisplayLength})")
self.action_set_fixed_stop.setText(
f"Stop ({self.maxDisplayLength})")
self.y_action_set_fixed_minimum.setText(
f"Minimum ({self.minImpedance})")
self.y_action_set_fixed_maximum.setText(
f"Maximum ({self.maxImpedance})")
self.menu.exec_(event.globalPos())
def isPlotable(self, x, y):
return self.leftMargin <= x <= self.width() - self.rightMargin and \
self.topMargin <= y <= self.height() - self.bottomMargin
def resetDisplayLimits(self):
self.fixedSpan = False
self.minDisplayLength = 0
self.maxDisplayLength = 100
self.fixedValues = False
self.minImpedance = 0
self.maxImpedance = 1000
self.update()
def setFixedSpan(self, fixed_span):
self.fixedSpan = fixed_span
self.update()
def setMinimumLength(self):
min_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Start length (m)",
"Set start length (m)", value=self.minDisplayLength,
min=0, decimals=1)
if not selected:
return
if not (self.fixedSpan and min_val >= self.maxDisplayLength):
self.minDisplayLength = min_val
if self.fixedSpan:
self.update()
def setMaximumLength(self):
max_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Stop length (m)",
"Set stop length (m)", value=self.minDisplayLength,
min=0.1, decimals=1)
if not selected:
return
if not (self.fixedSpan and max_val <= self.minDisplayLength):
self.maxDisplayLength = max_val
if self.fixedSpan:
self.update()
def setFixedValues(self, fixed_values):
self.fixedValues = fixed_values
self.update()
def setMinimumImpedance(self):
min_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Minimum impedance (\N{OHM SIGN})",
"Set minimum impedance (\N{OHM SIGN})",
value=self.minDisplayLength,
min=0, decimals=1)
if not selected:
return
if not (self.fixedValues and min_val >= self.maxImpedance):
self.minImpedance = min_val
if self.fixedValues:
self.update()
def setMaximumImpedance(self):
max_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Maximum impedance (\N{OHM SIGN})",
"Set maximum impedance (\N{OHM SIGN})",
value=self.minDisplayLength,
min=0.1, decimals=1)
if not selected:
return
if not (self.fixedValues and max_val <= self.minImpedance):
self.maxImpedance = max_val
if self.fixedValues:
self.update()
def copy(self):
new_chart: TDRChart = super().copy()
new_chart.tdrWindow = self.tdrWindow
new_chart.minDisplayLength = self.minDisplayLength
new_chart.maxDisplayLength = self.maxDisplayLength
new_chart.fixedSpan = self.fixedSpan
new_chart.minImpedance = self.minImpedance
new_chart.maxImpedance = self.maxImpedance
new_chart.fixedValues = self.fixedValues
self.tdrWindow.updated.connect(new_chart.update)
return new_chart
def mouseMoveEvent(self, a0: QtGui.QMouseEvent) -> None:
if a0.buttons() == QtCore.Qt.RightButton:
a0.ignore()
return
if a0.buttons() == QtCore.Qt.MiddleButton:
# Drag the display
a0.accept()
if self.moveStartX != -1 and self.moveStartY != -1:
dx = self.moveStartX - a0.x()
dy = self.moveStartY - a0.y()
self.zoomTo(self.leftMargin + dx, self.topMargin + dy,
self.leftMargin + self.chartWidth + dx,
self.topMargin + self.chartHeight + dy)
self.moveStartX = a0.x()
self.moveStartY = a0.y()
return
if a0.modifiers() == QtCore.Qt.ControlModifier:
# Dragging a box
if not self.draggedBox:
self.draggedBoxStart = (a0.x(), a0.y())
self.draggedBoxCurrent = (a0.x(), a0.y())
self.update()
a0.accept()
return
x = a0.x()
absx = x - self.leftMargin
if absx < 0 or absx > self.width() - self.rightMargin:
a0.ignore()
return
a0.accept()
width = self.width() - self.leftMargin - self.rightMargin
if len(self.tdrWindow.td) > 0:
if self.fixedSpan:
max_index = np.searchsorted(self.tdrWindow.distance_axis, self.maxDisplayLength * 2)
min_index = np.searchsorted(self.tdrWindow.distance_axis, self.minDisplayLength * 2)
x_step = (max_index - min_index) / width
else:
max_index = math.ceil(len(self.tdrWindow.distance_axis) / 2)
x_step = max_index / width
self.markerLocation = int(round(absx * x_step))
self.update()
return
def paintEvent(self, a0: QtGui.QPaintEvent) -> None:
qp = QtGui.QPainter(self)
qp.setPen(QtGui.QPen(self.textColor))
qp.drawText(3, 15, self.name)
width = self.width() - self.leftMargin - self.rightMargin
height = self.height() - self.bottomMargin - self.topMargin
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5,
self.height() - self.bottomMargin,
self.width() - self.rightMargin,
self.height() - self.bottomMargin)
qp.drawLine(self.leftMargin,
self.topMargin - 5,
self.leftMargin,
self.height() - self.bottomMargin + 5)
# Number of ticks does not include the origin
ticks = math.floor((self.width() - self.leftMargin) / 100)
self.drawTitle(qp)
if len(self.tdrWindow.td) > 0:
if self.fixedSpan:
max_length = max(0.1, self.maxDisplayLength)
max_index = np.searchsorted(self.tdrWindow.distance_axis, max_length * 2)
min_index = np.searchsorted(self.tdrWindow.distance_axis, self.minDisplayLength * 2)
if max_index == min_index:
if max_index < len(self.tdrWindow.distance_axis) - 1:
max_index += 1
else:
min_index -= 1
x_step = (max_index - min_index) / width
else:
min_index = 0
max_index = math.ceil(len(self.tdrWindow.distance_axis) / 2)
x_step = max_index / width
if self.fixedValues:
min_impedance = max(0, self.minImpedance)
max_impedance = max(0.1, self.maxImpedance)
else:
# TODO: Limit the search to the selected span?
min_impedance = max(
0,
np.min(self.tdrWindow.step_response_Z) / 1.05)
max_impedance = min(
1000,
np.max(self.tdrWindow.step_response_Z) * 1.05)
y_step = np.max(self.tdrWindow.td) * 1.1 / height
y_impedance_step = (max_impedance - min_impedance) / height
for i in range(ticks):
x = self.leftMargin + round((i + 1) * width / ticks)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(x, self.topMargin, x, self.topMargin + height)
qp.setPen(QtGui.QPen(self.textColor))
qp.drawText(
x - 15,
self.topMargin + height + 15,
str(round(
self.tdrWindow.distance_axis[
min_index +
int((x - self.leftMargin) * x_step) - 1] / 2,
1)) + "m")
qp.setPen(QtGui.QPen(self.textColor))
qp.drawText(
self.leftMargin - 10,
self.topMargin + height + 15,
str(round(self.tdrWindow.distance_axis[min_index] / 2,
1)) + "m")
y_ticks = math.floor(height / 60)
y_tick_step = height/y_ticks
for i in range(y_ticks):
y = self.bottomMargin + int(i * y_tick_step)
qp.setPen(self.foregroundColor)
qp.drawLine(self.leftMargin, y, self.leftMargin + width, y)
y_val = max_impedance - y_impedance_step * i * y_tick_step
qp.setPen(self.textColor)
qp.drawText(3, y + 3, str(round(y_val, 1)))
qp.drawText(3, self.topMargin + height + 3, str(round(min_impedance, 1)))
pen = QtGui.QPen(self.sweepColor)
pen.setWidth(self.pointSize)
qp.setPen(pen)
for i in range(min_index, max_index):
if i < min_index or i > max_index:
continue
x = self.leftMargin + int((i - min_index) / x_step)
y = (self.topMargin + height) - int(self.tdrWindow.td[i] / y_step)
if self.isPlotable(x, y):
pen.setColor(self.sweepColor)
qp.setPen(pen)
qp.drawPoint(x, y)
x = self.leftMargin + int((i - min_index) / x_step)
y = (self.topMargin + height) -\
int((self.tdrWindow.step_response_Z[i]-min_impedance) / y_impedance_step)
if self.isPlotable(x, y):
pen.setColor(self.secondarySweepColor)
qp.setPen(pen)
qp.drawPoint(x, y)
id_max = np.argmax(self.tdrWindow.td)
max_point = QtCore.QPoint(
self.leftMargin + int((id_max - min_index) / x_step),
(self.topMargin + height) - int(self.tdrWindow.td[id_max] / y_step))
qp.setPen(self.markers[0].color)
qp.drawEllipse(max_point, 2, 2)
qp.setPen(self.textColor)
qp.drawText(max_point.x() - 10, max_point.y() - 5,
str(round(self.tdrWindow.distance_axis[id_max] / 2,
2)) + "m")
if self.markerLocation != -1:
marker_point = QtCore.QPoint(
self.leftMargin +
int((self.markerLocation - min_index) / x_step),
(self.topMargin + height) -
int(self.tdrWindow.td[self.markerLocation] / y_step))
qp.setPen(self.textColor)
qp.drawEllipse(marker_point, 2, 2)
qp.drawText(
marker_point.x() - 10,
marker_point.y() - 5,
str(round(self.tdrWindow.distance_axis[self.markerLocation] / 2,
2)) + "m")
if self.draggedBox and self.draggedBoxCurrent[0] != -1:
dashed_pen = QtGui.QPen(self.foregroundColor, 1, QtCore.Qt.DashLine)
qp.setPen(dashed_pen)
top_left = QtCore.QPoint(self.draggedBoxStart[0], self.draggedBoxStart[1])
bottom_right = QtCore.QPoint(self.draggedBoxCurrent[0], self.draggedBoxCurrent[1])
rect = QtCore.QRect(top_left, bottom_right)
qp.drawRect(rect)
qp.end()
def valueAtPosition(self, y):
if len(self.tdrWindow.td) > 0:
height = self.height() - self.topMargin - self.bottomMargin
absy = (self.height() - y) - self.bottomMargin
if self.fixedValues:
min_impedance = self.minImpedance
max_impedance = self.maxImpedance
else:
min_impedance = max(
0,
np.min(self.tdrWindow.step_response_Z) / 1.05)
max_impedance = min(
1000,
np.max(self.tdrWindow.step_response_Z) * 1.05)
y_step = (max_impedance - min_impedance) / height
return y_step * absy + min_impedance
return 0
def lengthAtPosition(self, x, limit=True):
if len(self.tdrWindow.td) > 0:
width = self.width() - self.leftMargin - self.rightMargin
absx = x - self.leftMargin
if self.fixedSpan:
max_length = self.maxDisplayLength
min_length = self.minDisplayLength
x_step = (max_length - min_length) / width
else:
min_length = 0
max_length = self.tdrWindow.distance_axis[
math.ceil(len(self.tdrWindow.distance_axis) / 2)] / 2
x_step = max_length / width
if limit and absx < 0:
return min_length
if limit and absx > width:
return max_length
return absx * x_step + min_length
return 0
def zoomTo(self, x1, y1, x2, y2):
logger.debug("Zoom to (x,y) by (x,y): (%d, %d) by (%d, %d)", x1, y1, x2, y2)
val1 = self.valueAtPosition(y1)
val2 = self.valueAtPosition(y2)
if val1 != val2:
self.minImpedance = round(min(val1, val2), 3)
self.maxImpedance = round(max(val1, val2), 3)
self.setFixedValues(True)
len1 = max(0, self.lengthAtPosition(x1, limit=False))
len2 = max(0, self.lengthAtPosition(x2, limit=False))
if len1 >= 0 and len2 >= 0 and len1 != len2:
self.minDisplayLength = min(len1, len2)
self.maxDisplayLength = max(len1, len2)
self.setFixedSpan(True)
self.update()
def wheelEvent(self, a0: QtGui.QWheelEvent) -> None:
if len(self.tdrWindow.td) == 0:
a0.ignore()
return
chart_height = self.chartHeight
chart_width = self.chartWidth
do_zoom_x = do_zoom_y = True
if a0.modifiers() == QtCore.Qt.ShiftModifier:
do_zoom_x = False
if a0.modifiers() == QtCore.Qt.ControlModifier:
do_zoom_y = False
if a0.angleDelta().y() > 0:
# Zoom in
a0.accept()
# Center of zoom = a0.x(), a0.y()
# We zoom in by 1/10 of the width/height.
rate = a0.angleDelta().y() / 120
if do_zoom_x:
zoomx = rate * chart_width / 10
else:
zoomx = 0
if do_zoom_y:
zoomy = rate * chart_height / 10
else:
zoomy = 0
absx = max(0, a0.x() - self.leftMargin)
absy = max(0, a0.y() - self.topMargin)
ratiox = absx/chart_width
ratioy = absy/chart_height
# TODO: Change zoom to center on the mouse if possible,
# or extend box to the side that has room if not.
p1x = int(self.leftMargin + ratiox * zoomx)
p1y = int(self.topMargin + ratioy * zoomy)
p2x = int(self.leftMargin + chart_width - (1 - ratiox) * zoomx)
p2y = int(self.topMargin + chart_height - (1 - ratioy) * zoomy)
self.zoomTo(p1x, p1y, p2x, p2y)
elif a0.angleDelta().y() < 0:
# Zoom out
a0.accept()
# Center of zoom = a0.x(), a0.y()
# We zoom out by 1/9 of the width/height, to match zoom in.
rate = -a0.angleDelta().y() / 120
if do_zoom_x:
zoomx = rate * chart_width / 9
else:
zoomx = 0
if do_zoom_y:
zoomy = rate * chart_height / 9
else:
zoomy = 0
absx = max(0, a0.x() - self.leftMargin)
absy = max(0, a0.y() - self.topMargin)
ratiox = absx/chart_width
ratioy = absy/chart_height
p1x = int(self.leftMargin - ratiox * zoomx)
p1y = int(self.topMargin - ratioy * zoomy)
p2x = int(self.leftMargin + chart_width + (1 - ratiox) * zoomx)
p2y = int(self.topMargin + chart_height + (1 - ratioy) * zoomy)
self.zoomTo(p1x, p1y, p2x, p2y)
else:
a0.ignore()
def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
super().resizeEvent(a0)
self.chartWidth = self.width() - self.leftMargin - self.rightMargin
self.chartHeight = self.height() - self.bottomMargin - self.topMargin

Wyświetl plik

@ -1,212 +0,0 @@
# NanoVNASaver - a python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
import logging
from typing import List
from PyQt5 import QtWidgets, QtGui
from NanoVNASaver.RFTools import Datapoint
from .Frequency import FrequencyChart
logger = logging.getLogger(__name__)
class VSWRChart(FrequencyChart):
logarithmicY = False
maxVSWR = 3
span = 2
def __init__(self, name=""):
super().__init__(name)
self.leftMargin = 30
self.chartWidth = 250
self.chartHeight = 250
self.fstart = 0
self.fstop = 0
self.maxDisplayValue = 25
self.minDisplayValue = 1
self.setMinimumSize(self.chartWidth + self.rightMargin + self.leftMargin,
self.chartHeight + self.topMargin + self.bottomMargin)
self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding))
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, self.backgroundColor)
self.setPalette(pal)
self.setAutoFillBackground(True)
self.y_menu.addSeparator()
self.y_log_lin_group = QtWidgets.QActionGroup(self.y_menu)
self.y_action_linear = QtWidgets.QAction("Linear")
self.y_action_linear.setCheckable(True)
self.y_action_linear.setChecked(True)
self.y_action_logarithmic = QtWidgets.QAction("Logarithmic")
self.y_action_logarithmic.setCheckable(True)
self.y_action_linear.triggered.connect(lambda: self.setLogarithmicY(False))
self.y_action_logarithmic.triggered.connect(lambda: self.setLogarithmicY(True))
self.y_log_lin_group.addAction(self.y_action_linear)
self.y_log_lin_group.addAction(self.y_action_logarithmic)
self.y_menu.addAction(self.y_action_linear)
self.y_menu.addAction(self.y_action_logarithmic)
def setLogarithmicY(self, logarithmic: bool):
self.logarithmicY = logarithmic
self.update()
def copy(self):
new_chart: VSWRChart = super().copy()
new_chart.logarithmicY = self.logarithmicY
return new_chart
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
return
pen = QtGui.QPen(self.sweepColor)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(self.sweepColor)
line_pen.setWidth(self.lineThickness)
highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255))
highlighter.setWidth(1)
if self.fixedSpan:
fstart = self.minFrequency
fstop = self.maxFrequency
elif len(self.data) > 0:
fstart = self.data[0].freq
fstop = self.data[len(self.data)-1].freq
else:
fstart = self.reference[0].freq
fstop = self.reference[len(self.reference) - 1].freq
self.fstart = fstart
self.fstop = fstop
# Draw bands if required
if self.bands.enabled:
self.drawBands(qp, fstart, fstop)
# Find scaling
if self.fixedValues:
minVSWR = max(1, self.minDisplayValue)
maxVSWR = self.maxDisplayValue
else:
minVSWR = 1
maxVSWR = 3
for d in self.data:
vswr = d.vswr
if vswr > maxVSWR:
maxVSWR = vswr
maxVSWR = min(self.maxDisplayValue, math.ceil(maxVSWR))
self.maxVSWR = maxVSWR
span = maxVSWR-minVSWR
if span == 0:
span = 0.01
self.span = span
target_ticks = math.floor(self.chartHeight / 60)
if self.logarithmicY:
for i in range(target_ticks):
y = int(self.topMargin + (i / target_ticks) * self.chartHeight)
vswr = self.valueAtPosition(y)[0]
qp.setPen(self.textColor)
if vswr != 0:
digits = max(0, min(2, math.floor(3 - math.log10(abs(vswr)))))
if digits == 0:
vswrstr = str(round(vswr))
else:
vswrstr = str(round(vswr, digits))
qp.drawText(3, y+3, vswrstr)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin-5, y, self.leftMargin+self.chartWidth, y)
qp.drawLine(self.leftMargin - 5, self.topMargin + self.chartHeight,
self.leftMargin + self.chartWidth, self.topMargin + self.chartHeight)
qp.setPen(self.textColor)
digits = max(0, min(2, math.floor(3 - math.log10(abs(minVSWR)))))
if digits == 0:
vswrstr = str(round(minVSWR))
else:
vswrstr = str(round(minVSWR, digits))
qp.drawText(3, self.topMargin + self.chartHeight, vswrstr)
else:
for i in range(target_ticks):
vswr = minVSWR + i * self.span/target_ticks
y = self.getYPositionFromValue(vswr)
qp.setPen(self.textColor)
if vswr != 0:
digits = max(0, min(2, math.floor(3 - math.log10(abs(vswr)))))
if digits == 0:
vswrstr = str(round(vswr))
else:
vswrstr = str(round(vswr, digits))
qp.drawText(3, y+3, vswrstr)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin-5, y, self.leftMargin+self.chartWidth, y)
qp.drawLine(self.leftMargin - 5,
self.topMargin,
self.leftMargin + self.chartWidth,
self.topMargin)
qp.setPen(self.textColor)
digits = max(0, min(2, math.floor(3 - math.log10(abs(maxVSWR)))))
if digits == 0:
vswrstr = str(round(maxVSWR))
else:
vswrstr = str(round(maxVSWR, digits))
qp.drawText(3, 35, vswrstr)
self.drawFrequencyTicks(qp)
qp.setPen(self.swrColor)
for vswr in self.swrMarkers:
y = self.getYPositionFromValue(vswr)
qp.drawLine(self.leftMargin, y, self.leftMargin + self.chartWidth, y)
qp.drawText(self.leftMargin + 3, y - 1, str(vswr))
self.drawData(qp, self.data, self.sweepColor)
self.drawData(qp, self.reference, self.referenceColor)
self.drawMarkers(qp)
def getYPositionFromValue(self, vswr) -> int:
if self.logarithmicY:
min_val = self.maxVSWR - self.span
if self.maxVSWR > 0 and min_val > 0 and vswr > 0:
span = math.log(self.maxVSWR) - math.log(min_val)
else:
return -1
return (
self.topMargin +
round((math.log(self.maxVSWR) - math.log(vswr)) / span * self.chartHeight))
return self.topMargin + round((self.maxVSWR - vswr) / self.span * self.chartHeight)
def getYPosition(self, d: Datapoint) -> int:
return self.getYPositionFromValue(d.vswr)
def valueAtPosition(self, y) -> List[float]:
absy = y - self.topMargin
if self.logarithmicY:
min_val = self.maxVSWR - self.span
if self.maxVSWR > 0 and min_val > 0:
span = math.log(self.maxVSWR) - math.log(min_val)
step = span / self.chartHeight
val = math.exp(math.log(self.maxVSWR) - absy * step)
else:
val = -1
else:
val = -1 * ((absy / self.chartHeight * self.span) - self.maxVSWR)
return [val]
def resetDisplayLimits(self):
self.maxDisplayValue = 25
self.logarithmicY = False
super().resetDisplayLimits()

Wyświetl plik

@ -1,19 +0,0 @@
from .Chart import Chart
from .Frequency import FrequencyChart
from .Polar import PolarChart
from .Square import SquareChart
from .Capacitance import CapacitanceChart
from .Inductance import InductanceChart
from .GroupDelay import GroupDelayChart
from .LogMag import LogMagChart
from .CLogMag import CombinedLogMagChart
from .Magnitude import MagnitudeChart
from .MagnitudeZ import MagnitudeZChart
from .Permeability import PermeabilityChart
from .Phase import PhaseChart
from .QFactor import QualityFactorChart
from .RI import RealImaginaryChart
from .Smith import SmithChart
from .SParam import SParameterChart
from .TDR import TDRChart
from .VSWR import VSWRChart

Wyświetl plik

@ -1,97 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 time import sleep
from typing import List
import serial
from NanoVNASaver.Hardware.VNA import VNA, Version
logger = logging.getLogger(__name__)
class AVNA(VNA):
name = "AVNA"
def __init__(self, app, serial_port):
super().__init__(app, serial_port)
self.version = Version(self.readVersion())
self.features.add("Customizable data points")
def isValid(self):
return True
def getCalibration(self) -> str:
logger.debug("Reading calibration info.")
if not self.serial.is_open:
return "Not connected."
if self.app.serialLock.acquire():
try:
data = "a"
while data != "":
data = self.serial.readline().decode('ascii')
self.serial.write("cal\r".encode('ascii'))
result = ""
data = ""
sleep(0.1)
while "ch>" not in data:
data = self.serial.readline().decode('ascii')
result += data
values = result.splitlines()
return values[1]
except serial.SerialException as exc:
logger.exception("Exception while reading calibration info: %s", exc)
finally:
self.app.serialLock.release()
return "Unknown"
def readFrequencies(self) -> List[str]:
return self.readValues("frequencies")
def resetSweep(self, start: int, stop: int):
self.writeSerial("sweep " + str(start) + " " + str(stop) + " " + str(self.datapoints))
self.writeSerial("resume")
def readVersion(self):
logger.debug("Reading version info.")
if not self.serial.is_open:
return
if self.app.serialLock.acquire():
try:
data = "a"
while data != "":
data = self.serial.readline().decode('ascii')
self.serial.write("version\r".encode('ascii'))
result = ""
data = ""
sleep(0.1)
while "ch>" not in data:
data = self.serial.readline().decode('ascii')
result += data
values = result.splitlines()
logger.debug("Found version info: %s", values[1])
return values[1]
except serial.SerialException as exc:
logger.exception("Exception while reading firmware version: %s", exc)
finally:
self.app.serialLock.release()
return
def setSweep(self, start, stop):
self.writeSerial("sweep " + str(start) + " " + str(stop) + " " + str(self.datapoints))
sleep(1)

Wyświetl plik

@ -1,132 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 platform
from typing import List, Tuple
from collections import namedtuple
import serial
from serial.tools import list_ports
from NanoVNASaver.Hardware.VNA import VNA
from NanoVNASaver.Hardware.AVNA import AVNA
from NanoVNASaver.Hardware.NanoVNA_F import NanoVNA_F
from NanoVNASaver.Hardware.NanoVNA_H import NanoVNA_H, NanoVNA_H4
from NanoVNASaver.Hardware.NanoVNA import NanoVNA
from NanoVNASaver.Hardware.NanoVNA_V2 import NanoVNAV2
logger = logging.getLogger(__name__)
Device = namedtuple("Device", "vid pid name")
DEVICETYPES = (
Device(0x0483, 0x5740, "NanoVNA"),
Device(0x16c0, 0x0483, "AVNA"),
Device(0x04b4, 0x0008, "NanaVNA-V2"),
)
# The USB Driver for NanoVNA V2 seems to deliver an
# incompatible hardware info like:
# 'PORTS\\VID_04B4&PID_0008\\DEMO'
# This function will fix it.
def _fix_v2_hwinfo(dev):
if dev.hwid == r'PORTS\VID_04B4&PID_0008\DEMO':
dev.vid, dev.pid = 0x04b4, 0x0008
return dev
# Get list of interfaces with VNAs connected
def get_interfaces() -> List[Tuple[str, str]]:
return_ports = []
for d in list_ports.comports():
if platform.system() == 'Windows' and d.vid is None:
d = _fix_v2_hwinfo(d)
for t in DEVICETYPES:
if d.vid == t.vid and d.pid == t.pid:
port = d.device
logger.info("Found %s (%04x %04x) on port %s",
t.name, d.vid, d.pid, d.device)
return_ports.append((port, f"{port}({t.name})"))
return return_ports
def get_VNA(app, serial_port: serial.Serial) -> 'VNA':
logger.info("Finding correct VNA type...")
for _ in range(3):
vnaType = detect_version(serial_port)
if vnaType != "unknown":
break
serial_port.timeout = 0.2
if vnaType == 'nanovnav2':
logger.info("Type: NanoVNA-V2")
return NanoVNAV2(app, serial_port)
logger.info("Finding firmware variant...")
tmp_vna = VNA(app, serial_port)
tmp_vna.flushSerialBuffers()
firmware = tmp_vna.readFirmware()
if firmware.find("AVNA + Teensy") > 0:
logger.info("Type: AVNA")
return AVNA(app, serial_port)
if firmware.find("NanoVNA-H 4") > 0:
logger.info("Type: NanoVNA-H4")
return NanoVNA_H4(app, serial_port)
if firmware.find("NanoVNA-H") > 0:
logger.info("Type: NanoVNA-H")
vna = NanoVNA_H(app, serial_port)
if firmware.find("sweep_points 201") > 0:
logger.info("VNA has 201 datapoints capability")
vna._datapoints = (201, 101)
return vna
if firmware.find("NanoVNA-F") > 0:
logger.info("Type: NanoVNA-F")
return NanoVNA_F(app, serial_port)
if firmware.find("NanoVNA") > 0:
logger.info("Type: Generic NanoVNA")
return NanoVNA(app, serial_port)
logger.warning("Did not recognize NanoVNA type from firmware.")
return NanoVNA(app, serial_port)
def detect_version(serialPort: serial.Serial) -> str:
serialPort.timeout = 0.1
# drain any outstanding data in the serial incoming buffer
data = "a"
while len(data) != 0:
data = serialPort.read(128)
# send a \r and see what we get
serialPort.write(b"\r")
# will wait up to 0.1 seconds
data = serialPort.readline().decode('ascii')
if data == 'ch> ':
# this is an original nanovna
return 'nanovna'
if data == '2':
# this is a nanovna v2
return 'nanovnav2'
logger.error('Unknown VNA type: hardware responded to CR with: %s', data)
return 'unknown'

Wyświetl plik

@ -1,157 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 struct
from time import sleep
from typing import List
import serial
import numpy as np
from PyQt5 import QtGui
from NanoVNASaver.Hardware.VNA import VNA, Version
logger = logging.getLogger(__name__)
class NanoVNA(VNA):
name = "NanoVNA"
screenwidth = 320
screenheight = 240
def __init__(self, app, serial_port):
super().__init__(app, serial_port)
self.version = Version(self.readVersion())
logger.debug("Testing against 0.2.0")
if self.version.version_string.find("extended with scan") > 0:
logger.debug("Incompatible scan command detected.")
self.features.add("Incompatible scan command")
self.useScan = False
elif self.version >= Version("0.2.0"):
logger.debug("Newer than 0.2.0, using new scan command.")
self.features.add("New scan command")
self.useScan = True
else:
logger.debug("Older than 0.2.0, using old sweep command.")
self.features.add("Original sweep method")
self.useScan = False
self.readFeatures()
def isValid(self):
return True
def getCalibration(self) -> str:
logger.debug("Reading calibration info.")
if not self.serial.is_open:
return "Not connected."
if self.app.serialLock.acquire():
try:
data = "a"
while data != "":
data = self.serial.readline().decode('ascii')
self.serial.write("cal\r".encode('ascii'))
result = ""
data = ""
sleep(0.1)
while "ch>" not in data:
data = self.serial.readline().decode('ascii')
result += data
values = result.splitlines()
return values[1]
except serial.SerialException as exc:
logger.exception("Exception while reading calibration info: %s", exc)
finally:
self.app.serialLock.release()
return "Unknown"
def getScreenshot(self) -> QtGui.QPixmap:
logger.debug("Capturing screenshot...")
if not self.serial.is_open:
return QtGui.QPixmap()
if self.app.serialLock.acquire():
try:
data = "a"
while data != "":
data = self.serial.readline().decode('ascii')
timeout = self.serial.timeout
self.serial.write("capture\r".encode('ascii'))
self.serial.timeout = 4
self.serial.readline()
image_data = self.serial.read(
self.screenwidth * self.screenheight * 2)
self.serial.timeout = timeout
rgb_data = struct.unpack(
f">{self.screenwidth * self.screenheight}H",
image_data)
rgb_array = np.array(rgb_data, dtype=np.uint32)
rgba_array = (0xFF000000 +
((rgb_array & 0xF800) << 8) +
((rgb_array & 0x07E0) << 5) +
((rgb_array & 0x001F) << 3))
image = QtGui.QImage(
rgba_array,
self.screenwidth,
self.screenheight,
QtGui.QImage.Format_ARGB32)
logger.debug("Captured screenshot")
return QtGui.QPixmap(image)
except serial.SerialException as exc:
logger.exception(
"Exception while capturing screenshot: %s", exc)
finally:
self.app.serialLock.release()
return QtGui.QPixmap()
def readFrequencies(self) -> List[str]:
return self.readValues("frequencies")
def resetSweep(self, start: int, stop: int):
self.writeSerial("sweep {start} {stop} {self.datapoints}")
self.writeSerial("resume")
def readVersion(self):
logger.debug("Reading version info.")
if not self.serial.is_open:
return ""
if self.app.serialLock.acquire():
try:
data = "a"
while data != "":
data = self.serial.readline().decode('ascii')
self.serial.write("version\r".encode('ascii'))
result = ""
data = ""
sleep(0.1)
while "ch>" not in data:
data = self.serial.readline().decode('ascii')
result += data
values = result.splitlines()
logger.debug("Found version info: %s", values[1])
return values[1]
except serial.SerialException as exc:
logger.exception("Exception while reading firmware version: %s", exc)
finally:
self.app.serialLock.release()
return ""
def setSweep(self, start, stop):
if self.useScan:
self.writeSerial("scan " + str(start) + " " + str(stop) + " " + str(self.datapoints))
else:
self.writeSerial("sweep " + str(start) + " " + str(stop) + " " + str(self.datapoints))
sleep(1)

Wyświetl plik

@ -1,93 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 struct
import serial
import numpy as np
from PyQt5 import QtGui
from NanoVNASaver.Hardware.NanoVNA import NanoVNA
logger = logging.getLogger(__name__)
class NanoVNA_F(NanoVNA):
name = "NanoVNA-F"
screenwidth = 800
screenheight = 480
def getScreenshot(self) -> QtGui.QPixmap:
logger.debug("Capturing screenshot...")
if not self.serial.is_open:
return QtGui.QPixmap()
if self.app.serialLock.acquire():
try:
data = "a"
while data != "":
data = self.serial.readline().decode('ascii')
self.serial.write("capture\r".encode('ascii'))
timeout = self.serial.timeout
self.serial.timeout = 4
self.serial.readline()
image_data = self.serial.read(
self.screenwidth * self.screenheight * 2)
self.serial.timeout = timeout
rgb_data = struct.unpack(
f"<{self.screenwidth * self.screenheight}H", image_data)
rgb_array = np.array(rgb_data, dtype=np.uint32)
rgba_array = (0xFF000000 +
((rgb_array & 0xF800) << 8) + # G?!
((rgb_array & 0x07E0) >> 3) + # B
((rgb_array & 0x001F) << 11)) # G
unwrapped_array = np.empty(
self.screenwidth*self.screenheight,
dtype=np.uint32)
for y in range(self.screenheight // 2):
for x in range(self.screenwidth // 2):
unwrapped_array[
2 * x + 2 * y * self.screenwidth
] = rgba_array[x + y * self.screenwidth]
unwrapped_array[
(2 * x) + 1 + 2 * y * self.screenwidth
] = rgba_array[
x + (self.screenheight//2 + y) * self.screenwidth
]
unwrapped_array[
2 * x + (2 * y + 1) * self.screenwidth
] = rgba_array[
x + self.screenwidth // 2 + y * self.screenwidth
]
unwrapped_array[
(2 * x) + 1 + (2 * y + 1) * self.screenwidth
] = rgba_array[
x + self.screenwidth // 2 +
(self.screenheight//2 + y) * self.screenwidth
]
image = QtGui.QImage(
unwrapped_array,
self.screenwidth, self.screenheight,
QtGui.QImage.Format_ARGB32)
logger.debug("Captured screenshot")
return QtGui.QPixmap(image)
except serial.SerialException as exc:
logger.exception("Exception while capturing screenshot: %s", exc)
finally:
self.app.serialLock.release()
return QtGui.QPixmap()

Wyświetl plik

@ -1,210 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 platform
from struct import pack, unpack_from
from typing import List
from NanoVNASaver.Hardware.VNA import VNA, Version
if platform.system() != 'Windows':
import tty
logger = logging.getLogger(__name__)
_CMD_NOP = 0x00
_CMD_INDICATE = 0x0d
_CMD_READ = 0x10
_CMD_READ2 = 0x11
_CMD_READ4 = 0x12
_CMD_READFIFO = 0x18
_CMD_WRITE = 0x20
_CMD_WRITE2 = 0x21
_CMD_WRITE4 = 0x22
_CMD_WRITE8 = 0x23
_CMD_WRITEFIFO = 0x28
_ADDR_SWEEP_START = 0x00
_ADDR_SWEEP_STEP = 0x10
_ADDR_SWEEP_POINTS = 0x20
_ADDR_SWEEP_VALS_PER_FREQ = 0x22
_ADDR_RAW_SAMPLES_MODE = 0x26
_ADDR_VALUES_FIFO = 0x30
_ADDR_DEVICE_VARIANT = 0xf0
_ADDR_PROTOCOL_VERSION = 0xf1
_ADDR_HARDWARE_REVISION = 0xf2
_ADDR_FW_MAJOR = 0xf3
_ADDR_FW_MINOR = 0xf4
class NanoVNAV2(VNA):
name = "NanoVNA-V2"
_datapoints = (303, 101, 203, 505, 1023)
screenwidth = 320
screenheight = 240
def __init__(self, app, serialPort):
super().__init__(app, serialPort)
if platform.system() != 'Windows':
tty.setraw(self.serial.fd)
# reset protocol to known state
self.serial.write(pack("<Q", 0))
self.version = self.readVersion()
self.firmware = self.readFirmware()
self.features.add("Customizable data points")
# TODO: more than one dp per freq
self.features.add("Multi data points")
# firmware major version of 0xff indicates dfu mode
if self.firmware.major == 0xff:
self._isDFU = True
return
self._isDFU = False
self.sweepStartHz = 200e6
self.sweepStepHz = 1e6
self._sweepdata = []
self._updateSweep()
# self.setSweep(200e6, 300e6)
def isValid(self):
if self.isDFU():
return False
return True
def isDFU(self):
return self._isDFU
def checkValid(self):
if self.isDFU():
raise IOError('Device is in DFU mode')
def readFirmware(self) -> str:
# read register 0xf3 and 0xf4 (firmware major and minor version)
cmd = pack("<BBBB",
_CMD_READ, _ADDR_FW_MAJOR,
_CMD_READ, _ADDR_FW_MINOR)
self.serial.write(cmd)
resp = self.serial.read(2)
if len(resp) != 2:
logger.error("Timeout reading version registers")
return None
return Version(f"{resp[0]}.{resp[1]}.0")
def readFrequencies(self) -> List[str]:
self.checkValid()
return [
str(int(self.sweepStartHz + i * self.sweepStepHz))
for i in range(self.datapoints)]
def readValues(self, value) -> List[str]:
self.checkValid()
self.serial.timeout = 8 # should be enough
# Actually grab the data only when requesting channel 0.
# The hardware will return all channels which we will store.
if value == "data 0":
# reset protocol to known state
self.serial.write(pack("<Q", 0))
# cmd: write register 0x30 to clear FIFO
self.serial.write(pack("<BBB",
_CMD_WRITE, _ADDR_VALUES_FIFO, 0))
# clear sweepdata
self._sweepdata = []
pointstodo = self.datapoints
while pointstodo > 0:
logger.info("reading values")
pointstoread = min(255, pointstodo)
# cmd: read FIFO, addr 0x30
self.serial.write(
pack("<BBB",
_CMD_READFIFO, _ADDR_VALUES_FIFO,
pointstoread))
# each value is 32 bytes
nBytes = pointstoread * 32
# serial .read() will wait for exactly nBytes bytes
arr = self.serial.read(nBytes)
if nBytes != len(arr):
logger.error("expected %d bytes, got %d",
nBytes, len(arr))
return []
for i in range(pointstoread):
(fwd_real, fwd_imag, rev0_real, rev0_imag, rev1_real,
rev1_imag, freq_index) = unpack_from(
"<iiiiiihxxxxxx", arr, i * 32)
fwd = complex(fwd_real, fwd_imag)
refl = complex(rev0_real, rev0_imag)
thru = complex(rev1_real, rev1_imag)
self._sweepdata.append((refl / fwd, thru / fwd))
pointstodo = pointstodo - pointstoread
ret = [x[0] for x in self._sweepdata]
ret = [str(x.real) + ' ' + str(x.imag) for x in ret]
return ret
if value == "data 1":
ret = [x[1] for x in self._sweepdata]
ret = [str(x.real) + ' ' + str(x.imag) for x in ret]
return ret
def resetSweep(self, start: int, stop: int):
self.setSweep(start, stop)
return
# returns device variant
def readVersion(self):
# read register 0xf0 (device type), 0xf2 (board revision)
cmd = b"\x10\xf0\x10\xf2"
self.serial.write(cmd)
resp = self.serial.read(2)
if len(resp) != 2:
logger.error("Timeout reading version registers")
return None
return Version(f"{resp[0]}.0.{resp[1]}")
def setSweep(self, start, stop):
step = (stop - start) / (self.datapoints - 1)
if start == self.sweepStartHz and step == self.sweepStepHz:
return
self.sweepStartHz = start
self.sweepStepHz = step
logger.info('NanoVNAV2: set sweep start %d step %d',
self.sweepStartHz, self.sweepStepHz)
self._updateSweep()
return
def _updateSweep(self):
self.checkValid()
cmd = pack("<BBQ", _CMD_WRITE8,
_ADDR_SWEEP_START, int(self.sweepStartHz))
cmd += pack("<BBQ", _CMD_WRITE8,
_ADDR_SWEEP_STEP, int(self.sweepStepHz))
cmd += pack("<BBH", _CMD_WRITE2,
_ADDR_SWEEP_POINTS, self.datapoints)
cmd += pack("<BBH", _CMD_WRITE2,
_ADDR_SWEEP_VALS_PER_FREQ, 1)
self.serial.write(cmd)

Wyświetl plik

@ -1,264 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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
from time import sleep
from typing import List
import serial
from PyQt5 import QtWidgets, QtGui
logger = logging.getLogger(__name__)
class VNA:
name = "VNA"
_datapoints = (101, )
def __init__(self, app: QtWidgets.QWidget, serial_port: serial.Serial):
self.app = app
self.serial = serial_port
self.version: Version = Version("0.0.0")
self.features = set()
self.validateInput = True
self.datapoints = self._datapoints[0]
def readFeatures(self) -> List[str]:
raw_help = self.readFromCommand("help")
logger.debug("Help command output:")
logger.debug(raw_help)
# Detect features from the help command
if "capture" in raw_help:
self.features.add("Screenshots")
if len(self._datapoints) > 1:
self.features.add("Customizable data points")
return self.features
# TODO: check return types
def readFrequencies(self) -> List[int]:
return []
def resetSweep(self, start: int, stop: int):
pass
def isValid(self):
return False
def isDFU(self):
return False
def getFeatures(self) -> List[str]:
return self.features
def getCalibration(self) -> str:
return "Unknown"
def getScreenshot(self) -> QtGui.QPixmap:
return QtGui.QPixmap()
def flushSerialBuffers(self):
if self.app.serialLock.acquire():
self.serial.write(b"\r\n\r\n")
sleep(0.1)
self.serial.reset_input_buffer()
self.serial.reset_output_buffer()
sleep(0.1)
self.app.serialLock.release()
def readFirmware(self) -> str:
if self.app.serialLock.acquire():
result = ""
try:
data = "a"
while data != "":
data = self.serial.readline().decode('ascii')
self.serial.write("info\r".encode('ascii'))
result = ""
data = ""
sleep(0.01)
while data != "ch> ":
data = self.serial.readline().decode('ascii')
result += data
except serial.SerialException as exc:
logger.exception(
"Exception while reading firmware data: %s", exc)
finally:
self.app.serialLock.release()
return result
logger.error("Unable to acquire serial lock to read firmware.")
return ""
def readFromCommand(self, command) -> str:
if self.app.serialLock.acquire():
result = ""
try:
data = "a"
while data != "":
data = self.serial.readline().decode('ascii')
self.serial.write((command + "\r").encode('ascii'))
result = ""
data = ""
sleep(0.01)
while data != "ch> ":
data = self.serial.readline().decode('ascii')
result += data
except serial.SerialException as exc:
logger.exception(
"Exception while reading %s: %s", command, exc)
finally:
self.app.serialLock.release()
return result
logger.error("Unable to acquire serial lock to read %s", command)
return ""
def readValues(self, value) -> List[str]:
logger.debug("VNA reading %s", value)
if self.app.serialLock.acquire():
try:
data = "a"
while data != "":
data = self.serial.readline().decode('ascii')
# Then send the command to read data
self.serial.write(str(value + "\r").encode('ascii'))
result = ""
data = ""
sleep(0.05)
while data != "ch> ":
data = self.serial.readline().decode('ascii')
result += data
values = result.split("\r\n")
except serial.SerialException as exc:
logger.exception(
"Exception while reading %s: %s", value, exc)
return []
finally:
self.app.serialLock.release()
logger.debug(
"VNA done reading %s (%d values)",
value, len(values)-2)
return values[1:-1]
logger.error("Unable to acquire serial lock to read %s", value)
return []
def writeSerial(self, command):
if not self.serial.is_open:
logger.warning("Writing without serial port being opened (%s)",
command)
return
if self.app.serialLock.acquire():
try:
self.serial.write(str(command + "\r").encode('ascii'))
self.serial.readline()
except serial.SerialException as exc:
logger.exception(
"Exception while writing to serial port (%s): %s",
command, exc)
finally:
self.app.serialLock.release()
return
def setSweep(self, start, stop):
self.writeSerial("sweep " + str(start) + " " + str(stop) + " " + str(self.datapoints))
# TODO: should be dropped and the serial part should be a connection class which handles
# unconnected devices
class InvalidVNA(VNA):
name = "Invalid"
_datapoints = (0, )
def __init__(self, app: QtWidgets.QWidget, serial_port: serial.Serial):
super().__init__(app, serial_port)
def setSweep(self, start, stop):
return
def resetSweep(self, start, stop):
return
def writeSerial(self, command):
return
def readFirmware(self):
return
def readFrequencies(self) -> List[int]:
return []
def readValues(self, value):
return
def flushSerialBuffers(self):
return
# TODO: should go to Settings.py and be generalized
class Version:
major = 0
minor = 0
revision = 0
note = ""
version_string = ""
def __init__(self, version_string):
self.version_string = version_string
results = re.match(
r"(.*\s+)?(\d+)\.(\d+)\.(\d+)(.*)",
version_string)
if results:
self.major = int(results.group(2))
self.minor = int(results.group(3))
self.revision = int(results.group(4))
self.note = results.group(5)
logger.debug(
"Parsed version as \"%d.%d.%d%s\"",
self.major, self.minor, self.revision, self.note)
def __gt__(self, other: "Version") -> bool:
if self.major > other.major:
return True
if self.major < other.major:
return False
if self.minor > other.minor:
return True
if self.minor < other.minor:
return False
if self.revision > other.revision:
return True
return False
def __lt__(self, other: "Version") -> bool:
return other > self
def __ge__(self, other: "Version") -> bool:
return self > other or self == other
def __le__(self, other: "Version") -> bool:
return self < other or self == other
def __eq__(self, other: "Version") -> bool:
return (
self.major == other.major and
self.minor == other.minor and
self.revision == other.revision and
self.note == other.note)
def __str__(self) -> str:
return f"{self.major}.{self.minor}.{self.revision}{self.note}"

Wyświetl plik

@ -1,2 +0,0 @@
from .Hardware import get_interfaces, get_VNA #noqa
from .VNA import InvalidVNA, Version #noqa

Wyświetl plik

@ -1,155 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 QtWidgets, QtCore, QtGui
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.Marker import Marker
from NanoVNASaver.Marker.Values import TYPES, default_label_ids
logger = logging.getLogger(__name__)
class MarkerSettingsWindow(QtWidgets.QWidget):
exampleData11 = [Datapoint(123000000, 0.89, -0.11),
Datapoint(123500000, 0.9, -0.1),
Datapoint(124000000, 0.91, -0.95)]
exampleData21 = [Datapoint(123000000, -0.25, 0.49),
Datapoint(123456000, -0.3, 0.5),
Datapoint(124000000, -0.2, 0.5)]
def __init__(self, app: QtWidgets.QWidget):
super().__init__()
self.app = app
self.setWindowTitle("Marker settings")
self.setWindowIcon(self.app.icon)
QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.cancelButtonClick)
self.exampleMarker = Marker("Example marker")
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
settings_group_box = QtWidgets.QGroupBox("Settings")
settings_group_box_layout = QtWidgets.QFormLayout(settings_group_box)
self.checkboxColouredMarker = QtWidgets.QCheckBox("Colored marker name")
self.checkboxColouredMarker.setChecked(
self.app.settings.value("ColoredMarkerNames", True, bool))
self.checkboxColouredMarker.stateChanged.connect(self.updateMarker)
settings_group_box_layout.addRow(self.checkboxColouredMarker)
fields_group_box = QtWidgets.QGroupBox("Displayed data")
fields_group_box_layout = QtWidgets.QFormLayout(fields_group_box)
self.savedFieldSelection = self.app.settings.value(
"MarkerFields", defaultValue=default_label_ids()
)
if self.savedFieldSelection == "":
self.savedFieldSelection = []
self.currentFieldSelection = self.savedFieldSelection[:]
self.active_labels_view = QtWidgets.QListView()
self.update_displayed_data_form()
fields_group_box_layout.addRow(self.active_labels_view)
layout.addWidget(settings_group_box)
layout.addWidget(fields_group_box)
layout.addWidget(self.exampleMarker.getGroupBox())
btn_layout = QtWidgets.QHBoxLayout()
layout.addLayout(btn_layout)
btn_ok = QtWidgets.QPushButton("OK")
btn_apply = QtWidgets.QPushButton("Apply")
btn_default = QtWidgets.QPushButton("Defaults")
btn_cancel = QtWidgets.QPushButton("Cancel")
btn_ok.clicked.connect(self.okButtonClick)
btn_apply.clicked.connect(self.applyButtonClick)
btn_default.clicked.connect(self.defaultButtonClick)
btn_cancel.clicked.connect(self.cancelButtonClick)
btn_layout.addWidget(btn_ok)
btn_layout.addWidget(btn_apply)
btn_layout.addWidget(btn_default)
btn_layout.addWidget(btn_cancel)
self.updateMarker()
for m in self.app.markers:
m.setFieldSelection(self.currentFieldSelection)
m.setColoredText(self.checkboxColouredMarker.isChecked())
def updateMarker(self):
self.exampleMarker.setFrequency(123456000)
self.exampleMarker.setColoredText(self.checkboxColouredMarker.isChecked())
self.exampleMarker.setFieldSelection(self.currentFieldSelection)
self.exampleMarker.findLocation(self.exampleData11)
self.exampleMarker.resetLabels()
self.exampleMarker.updateLabels(self.exampleData11, self.exampleData21)
def updateField(self, field: QtGui.QStandardItem):
if field.checkState() == QtCore.Qt.Checked:
if not field.data() in self.currentFieldSelection:
self.currentFieldSelection = []
for i in range(self.model.rowCount()):
field = self.model.item(i, 0)
if field.checkState() == QtCore.Qt.Checked:
self.currentFieldSelection.append(field.data())
else:
if field.data() in self.currentFieldSelection:
self.currentFieldSelection.remove(field.data())
self.updateMarker()
def applyButtonClick(self):
self.savedFieldSelection = self.currentFieldSelection[:]
self.app.settings.setValue("MarkerFields", self.savedFieldSelection)
self.app.settings.setValue("ColoredMarkerNames", self.checkboxColouredMarker.isChecked())
for m in self.app.markers:
m.setFieldSelection(self.savedFieldSelection)
m.setColoredText(self.checkboxColouredMarker.isChecked())
def okButtonClick(self):
self.applyButtonClick()
self.close()
def cancelButtonClick(self):
self.currentFieldSelection = self.savedFieldSelection[:]
self.update_displayed_data_form()
self.updateMarker()
self.close()
def defaultButtonClick(self):
self.currentFieldSelection = default_label_ids()
self.update_displayed_data_form()
self.updateMarker()
def update_displayed_data_form(self):
self.model = QtGui.QStandardItemModel()
for label in TYPES:
item = QtGui.QStandardItem(label.description)
item.setData(label.label_id)
item.setCheckable(True)
item.setEditable(False)
if label.label_id in self.currentFieldSelection:
item.setCheckState(QtCore.Qt.Checked)
self.model.appendRow(item)
self.active_labels_view.setModel(self.model)
self.model.itemChanged.connect(self.updateField)

Wyświetl plik

@ -1,3 +0,0 @@
from .Widget import Marker # noqa
from .Settings import MarkerSettingsWindow # noqa
from .Values import Value, default_label_ids # noqa

Wyświetl plik

@ -1,151 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 typing
from typing import List, Tuple
from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import QModelIndex
logger = logging.getLogger(__name__)
class BandsModel(QtCore.QAbstractTableModel):
bands: List[Tuple[str, int, int]] = []
enabled = False
color = QtGui.QColor(128, 128, 128, 48)
# These bands correspond broadly to the Danish Amateur Radio allocation
default_bands = ["2200 m;135700;137800",
"630 m;472000;479000",
"160 m;1800000;2000000",
"80 m;3500000;3800000",
"60 m;5250000;5450000",
"40 m;7000000;7200000",
"30 m;10100000;10150000",
"20 m;14000000;14350000",
"17 m;18068000;18168000",
"15 m;21000000;21450000",
"12 m;24890000;24990000",
"10 m;28000000;29700000",
"6 m;50000000;52000000",
"4 m;69887500;70512500",
"2 m;144000000;146000000",
"70 cm;432000000;438000000",
"23 cm;1240000000;1300000000",
"13 cm;2320000000;2450000000"]
def __init__(self):
super().__init__()
self.settings = QtCore.QSettings(QtCore.QSettings.IniFormat,
QtCore.QSettings.UserScope,
"NanoVNASaver", "Bands")
self.settings.setIniCodec("UTF-8")
self.enabled = self.settings.value("ShowBands", False, bool)
stored_bands: List[str] = self.settings.value("bands", self.default_bands)
if stored_bands:
for b in stored_bands:
(name, start, end) = b.split(";")
self.bands.append((name, int(start), int(end)))
def saveSettings(self):
stored_bands = []
for b in self.bands:
stored_bands.append(b[0] + ";" + str(b[1]) + ";" + str(b[2]))
self.settings.setValue("bands", stored_bands)
self.settings.sync()
def resetBands(self):
self.bands = []
for b in self.default_bands:
(name, start, end) = b.split(";")
self.bands.append((name, int(start), int(end)))
self.layoutChanged.emit()
self.saveSettings()
def columnCount(self, parent: QModelIndex = ...) -> int:
return 3
def rowCount(self, parent: QModelIndex = ...) -> int:
return len(self.bands)
def data(self, index: QModelIndex, role: int = ...) -> QtCore.QVariant:
if (role == QtCore.Qt.DisplayRole or
role == QtCore.Qt.ItemDataRole or role == QtCore.Qt.EditRole):
return QtCore.QVariant(self.bands[index.row()][index.column()])
if role == QtCore.Qt.TextAlignmentRole:
if index.column() == 0:
return QtCore.QVariant(QtCore.Qt.AlignCenter)
return QtCore.QVariant(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
return QtCore.QVariant()
def setData(self, index: QModelIndex, value: typing.Any, role: int = ...) -> bool:
if role == QtCore.Qt.EditRole and index.isValid():
t = self.bands[index.row()]
name = t[0]
start = t[1]
end = t[2]
if index.column() == 0:
name = value
elif index.column() == 1:
start = value
elif index.column() == 2:
end = value
self.bands[index.row()] = (name, start, end)
self.dataChanged.emit(index, index)
self.saveSettings()
return True
return False
def index(self, row: int, column: int, parent: QModelIndex = ...) -> QModelIndex:
return self.createIndex(row, column)
def addRow(self):
self.bands.append(("New", 0, 0))
self.dataChanged.emit(self.index(len(self.bands), 0), self.index(len(self.bands), 2))
self.layoutChanged.emit()
def removeRow(self, row: int, parent: QModelIndex = ...) -> bool:
self.bands.remove(self.bands[row])
self.layoutChanged.emit()
self.saveSettings()
return True
def headerData(self, section: int,
orientation: QtCore.Qt.Orientation, role: int = ...):
if (role == QtCore.Qt.DisplayRole and
orientation == QtCore.Qt.Horizontal):
if section == 0:
return "Band"
if section == 1:
return "Start (Hz)"
if section == 2:
return "End (Hz)"
return "Invalid"
super().headerData(section, orientation, role)
def flags(self, index: QModelIndex) -> QtCore.Qt.ItemFlags:
if index.isValid():
return QtCore.Qt.ItemFlags(
QtCore.Qt.ItemIsEditable |
QtCore.Qt.ItemIsEnabled |
QtCore.Qt.ItemIsSelectable)
super().flags(index)
def setColor(self, color):
self.color = color

Wyświetl plik

@ -1,433 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 time import sleep
from typing import List
import numpy as np
from PyQt5 import QtCore
from PyQt5.QtCore import pyqtSlot, pyqtSignal
import NanoVNASaver
from NanoVNASaver.Calibration import Calibration
from NanoVNASaver.Formatting import parse_frequency
from NanoVNASaver.RFTools import Datapoint
logger = logging.getLogger(__name__)
class WorkerSignals(QtCore.QObject):
updated = pyqtSignal()
finished = pyqtSignal()
sweepError = pyqtSignal()
fatalSweepError = pyqtSignal()
class SweepWorker(QtCore.QRunnable):
def __init__(self, app: NanoVNASaver):
super().__init__()
logger.info("Initializing SweepWorker")
self.signals = WorkerSignals()
self.app = app
self.vna: app.vna
self.noSweeps = 1
self.setAutoDelete(False)
self.percentage = 0
self.data11: List[Datapoint] = []
self.data21: List[Datapoint] = []
self.rawData11: List[Datapoint] = []
self.rawData21: List[Datapoint] = []
self.stopped = False
self.running = False
self.continuousSweep = False
self.averaging = False
self.averages = 3
self.truncates = 0
self.error_message = ""
self.offsetDelay = 0
@pyqtSlot()
def run(self):
logger.info("Initializing SweepWorker")
self.running = True
self.percentage = 0
if not self.app.serial.is_open:
logger.debug("Attempted to run without being connected to the NanoVNA")
self.running = False
return
if int(self.app.sweepCountInput.text()) > 0:
self.noSweeps = int(self.app.sweepCountInput.text())
logger.info("%d sweeps", self.noSweeps)
if self.averaging:
logger.info("%d averages", self.averages)
if self.app.sweepStartInput.text() == "" or self.app.sweepEndInput.text() == "":
logger.debug("First sweep - standard range")
# We should handle the first startup by reading frequencies?
sweep_from = 1000000
sweep_to = 800000000
else:
sweep_from = parse_frequency(self.app.sweepStartInput.text())
sweep_to = parse_frequency(self.app.sweepEndInput.text())
logger.debug("Parsed sweep range as %d to %d", sweep_from, sweep_to)
if sweep_from < 0 or sweep_to < 0 or sweep_from == sweep_to:
logger.warning("Can't sweep from %s to %s",
self.app.sweepStartInput.text(),
self.app.sweepEndInput.text())
self.error_message = \
"Unable to parse frequency inputs - check start and stop fields."
self.stopped = True
self.running = False
self.signals.sweepError.emit()
return
span = sweep_to - sweep_from
stepsize = int(span / (self.noSweeps * self.vna.datapoints - 1))
# Setup complete
values = []
values21 = []
frequencies = []
if self.averaging:
for i in range(self.noSweeps):
logger.debug("Sweep segment no %d averaged over %d readings", i, self.averages)
if self.stopped:
logger.debug("Stopping sweeping as signalled")
break
start = sweep_from + i * self.vna.datapoints * stepsize
freq, val11, val21 = self.readAveragedSegment(
start, start + (self.vna.datapoints - 1) * stepsize, self.averages)
frequencies += freq
values += val11
values21 += val21
self.percentage = (i + 1) * (self.vna.datapoints - 1) / self.noSweeps
logger.debug("Saving acquired data")
self.saveData(frequencies, values, values21)
else:
for i in range(self.noSweeps):
logger.debug("Sweep segment no %d", i)
if self.stopped:
logger.debug("Stopping sweeping as signalled")
break
start = sweep_from + i * self.vna.datapoints * stepsize
try:
freq, val11, val21 = self.readSegment(
start, start + (self.vna.datapoints - 1) * stepsize)
frequencies += freq
values += val11
values21 += val21
self.percentage = (i + 1) * 100 / self.noSweeps
logger.debug("Saving acquired data")
self.saveData(frequencies, values, values21)
except NanoVNAValueException as e:
self.error_message = str(e)
self.stopped = True
self.running = False
self.signals.sweepError.emit()
except NanoVNASerialException as e:
self.error_message = str(e)
self.stopped = True
self.running = False
self.signals.sweepFatalError.emit()
while self.continuousSweep and not self.stopped:
logger.debug("Continuous sweeping")
for i in range(self.noSweeps):
logger.debug("Sweep segment no %d", i)
if self.stopped:
logger.debug("Stopping sweeping as signalled")
break
start = sweep_from + i * self.vna.datapoints * stepsize
try:
_, values, values21 = self.readSegment(
start, start + (self.vna.datapoints-1) * stepsize)
logger.debug("Updating acquired data")
self.updateData(values, values21, i, self.vna.datapoints)
except NanoVNAValueException as e:
self.error_message = str(e)
self.stopped = True
self.running = False
self.signals.sweepError.emit()
except NanoVNASerialException as e:
self.error_message = str(e)
self.stopped = True
self.running = False
self.signals.sweepFatalError.emit()
# Reset the device to show the full range if we were multisegment
if self.noSweeps > 1:
logger.debug("Resetting NanoVNA sweep to full range: %d to %d",
parse_frequency(
self.app.sweepStartInput.text()),
parse_frequency(self.app.sweepEndInput.text()))
self.vna.resetSweep(parse_frequency(self.app.sweepStartInput.text()),
parse_frequency(self.app.sweepEndInput.text()))
self.percentage = 100
logger.debug("Sending \"finished\" signal")
self.signals.finished.emit()
self.running = False
return
def updateData(self, values11, values21, offset, segment_size=101):
# Update the data from (i*101) to (i+1)*101
logger.debug("Calculating data and inserting in existing data at offset %d", offset)
for i, val11 in enumerate(values11):
re, im = val11
re21, im21 = values21[i]
freq = self.data11[offset * segment_size + i].freq
raw_data11 = Datapoint(freq, re, im)
raw_data21 = Datapoint(freq, re21, im21)
data11, data21 = self.applyCalibration([raw_data11], [raw_data21])
self.data11[offset * segment_size + i] = data11[0]
self.data21[offset * segment_size + i] = data21[0]
self.rawData11[offset * segment_size + i] = raw_data11
self.rawData21[offset * segment_size + i] = raw_data21
logger.debug("Saving data to application (%d and %d points)",
len(self.data11), len(self.data21))
self.app.saveData(self.data11, self.data21)
logger.debug("Sending \"updated\" signal")
self.signals.updated.emit()
def saveData(self, frequencies, values11, values21):
raw_data11 = []
raw_data21 = []
logger.debug("Calculating data including corrections")
for i, freq in enumerate(frequencies):
logger.debug("Freqnr %i, len(%i)", i, len(frequencies))
logger.debug("Val11 %s", values11[i])
logger.debug("Val21 %s", values21[i])
re, im = values11[i]
re21, im21 = values21[i]
raw_data11 += [Datapoint(freq, re, im)]
raw_data21 += [Datapoint(freq, re21, im21)]
self.data11, self.data21 = self.applyCalibration(raw_data11, raw_data21)
self.rawData11 = raw_data11
self.rawData21 = raw_data21
logger.debug("Saving data to application (%d and %d points)",
len(self.data11), len(self.data21))
self.app.saveData(self.data11, self.data21)
logger.debug("Sending \"updated\" signal")
self.signals.updated.emit()
def applyCalibration(self, raw_data11: List[Datapoint], raw_data21: List[Datapoint]) ->\
(List[Datapoint], List[Datapoint]):
if self.offsetDelay != 0:
tmp = []
for d in raw_data11:
tmp.append(Calibration.correctDelay11(d, self.offsetDelay))
raw_data11 = tmp
tmp = []
for d in raw_data21:
tmp.append(Calibration.correctDelay21(d, self.offsetDelay))
raw_data21 = tmp
if not self.app.calibration.isCalculated:
return raw_data11, raw_data21
data11: List[Datapoint] = []
data21: List[Datapoint] = []
if self.app.calibration.isValid1Port():
for d in raw_data11:
re, im = self.app.calibration.correct11(d.re, d.im, d.freq)
data11.append(Datapoint(d.freq, re, im))
else:
data11 = raw_data11
if self.app.calibration.isValid2Port():
for d in raw_data21:
re, im = self.app.calibration.correct21(d.re, d.im, d.freq)
data21.append(Datapoint(d.freq, re, im))
else:
data21 = raw_data21
return data11, data21
def readAveragedSegment(self, start, stop, averages):
val11 = []
val21 = []
freq = []
logger.info("Reading %d averages from %d to %d", averages, start, stop)
for i in range(averages):
if self.stopped:
logger.debug("Stopping averaging as signalled")
break
logger.debug("Reading average no %d / %d", i+1, averages)
freq, tmp11, tmp21 = self.readSegment(start, stop)
val11.append(tmp11)
val21.append(tmp21)
self.percentage += 100/(self.noSweeps*averages)
self.signals.updated.emit()
logger.debug("Post-processing averages")
logger.debug("Truncating %d values by %d", len(val11), self.truncates)
val11 = self.truncate(val11, self.truncates)
val21 = self.truncate(val21, self.truncates)
logger.debug("Averaging %d values", len(val11))
return11 = np.average(val11, 0).tolist()
return21 = np.average(val21, 0).tolist()
return freq, return11, return21
@staticmethod
def truncate(values: List[List[tuple]], count):
logger.debug("Truncating from %d values to %d", len(values), len(values) - count)
if count < 1:
return values
values = np.swapaxes(values, 0, 1)
return_values = []
for valueset in values:
# avg becomes a 2-value array of the location of the average
avg = np.average(valueset, 0)
new_valueset = valueset
for _ in range(count):
max_deviance = 0
max_idx = -1
for i, valset in enumerate(new_valueset):
deviance = abs(valset[0] - avg[0])**2 + abs(valset[1] - avg[1])**2
if deviance > max_deviance:
max_deviance = deviance
max_idx = i
next_valueset = []
# TODO: find out if valset is always a two element array
for i, valset in enumerate(new_valueset):
if i != max_idx:
next_valueset.append((valset[0], valueset[1]))
new_valueset = next_valueset
return_values.append(new_valueset)
return_values = np.swapaxes(return_values, 0, 1)
return return_values.tolist()
def readSegment(self, start, stop):
logger.debug("Setting sweep range to %d to %d", start, stop)
self.vna.setSweep(start, stop)
# Let's check the frequencies first:
frequencies = self.readFreq()
# S11
values11 = self.readData("data 0")
# S21
values21 = self.readData("data 1")
return frequencies, values11, values21
def readData(self, data):
logger.debug("Reading %s", data)
done = False
returndata = []
count = 0
while not done:
done = True
returndata = []
tmpdata = self.vna.readValues(data)
logger.debug("Read %d values", len(tmpdata))
for d in tmpdata:
a, b = d.split(" ")
try:
if self.vna.validateInput and (float(a) < -9.5 or float(a) > 9.5):
logger.warning("Got a non-float data value: %s (%s)", d, a)
logger.debug("Re-reading %s", data)
done = False
elif self.vna.validateInput and (float(b) < -9.5 or float(b) > 9.5):
logger.warning("Got a non-float data value: %s (%s)", d, b)
logger.debug("Re-reading %s", data)
done = False
else:
returndata.append((float(a), float(b)))
except Exception as e:
logger.exception("An exception occurred reading %s: %s", data, e)
logger.debug("Re-reading %s", data)
done = False
if not done:
sleep(0.2)
count += 1
if count == 10:
logger.error("Tried and failed to read %s %d times.", data, count)
if count >= 20:
logger.critical("Tried and failed to read %s %d times. Giving up.", data, count)
raise NanoVNAValueException(
f"Failed reading {data} {count} times.\n"
f"Data outside expected valid ranges, or in an unexpected format.\n\n"
f"You can disable data validation on the device settings screen.")
return returndata
def readFreq(self):
# TODO: Figure out why frequencies sometimes arrive as non-integers
logger.debug("Reading frequencies")
returnfreq = []
done = False
count = 0
while not done:
done = True
returnfreq = []
tmpfreq = self.vna.readFrequencies()
if not tmpfreq:
logger.warning("Read no frequencies")
raise NanoVNASerialException("Failed reading frequencies: Returned no values.")
for f in tmpfreq:
if not f.isdigit():
logger.warning("Got a non-digit frequency: %s", f)
logger.debug("Re-reading frequencies")
done = False
count += 1
if count == 10:
logger.error("Tried and failed %d times to read frequencies.", count)
if count >= 20:
logger.critical(
"Tried and failed to read frequencies from the NanoVNA %d times.",
count)
raise NanoVNAValueException(
f"Failed reading frequencies {count} times.")
else:
returnfreq.append(int(f))
return returnfreq
def setContinuousSweep(self, continuous_sweep: bool):
self.continuousSweep = continuous_sweep
def setAveraging(self, averaging: bool, averages: str, truncates: str):
self.averaging = averaging
try:
self.averages = int(averages)
self.truncates = int(truncates)
except ValueError:
return
def setVNA(self, vna):
self.vna = vna
class NanoVNAValueException(Exception):
pass
class NanoVNASerialException(Exception):
pass

Wyświetl plik

@ -1,190 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 json
from time import strftime, localtime
from urllib import request, error
from PyQt5 import QtWidgets, QtCore
from NanoVNASaver.Hardware import Version
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()
self.setLayout(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 Rune B. Broberg"))
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(
"For further details, see: <a href=\"https://mihtjel.github.io/nanovna-saver/\">"
"https://mihtjel.github.io/nanovna-saver/</a>")
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: ")
self.updateCheckBox = QtWidgets.QCheckBox("Check for updates on startup")
self.updateCheckBox.toggled.connect(self.updateSettings)
check_for_updates = self.app.settings.value("CheckForUpdates", "Ask")
if check_for_updates == "Yes":
self.updateCheckBox.setChecked(True)
self.findUpdates(automatic=True)
elif check_for_updates == "No":
self.updateCheckBox.setChecked(False)
else:
logger.debug("Starting timer")
QtCore.QTimer.singleShot(2000, self.askAboutUpdates)
update_hbox = QtWidgets.QHBoxLayout()
update_hbox.addWidget(btn_check_version)
update_form = QtWidgets.QFormLayout()
update_hbox.addLayout(update_form)
update_hbox.addStretch()
update_form.addRow(self.updateLabel)
update_form.addRow(self.updateCheckBox)
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):
if self.app.vna.isValid():
logger.debug("Valid VNA")
v: Version = self.app.vna.version
self.versionLabel.setText(
f"NanoVNA Firmware Version: {self.app.vna.name}"
f"{v.version_string}")
def updateSettings(self):
if self.updateCheckBox.isChecked():
self.app.settings.setValue("CheckForUpdates", "Yes")
else:
self.app.settings.setValue("CheckForUpdates", "No")
def askAboutUpdates(self):
logger.debug("Asking about automatic update checks")
selection = QtWidgets.QMessageBox.question(
self.app,
"Enable checking for updates?",
"Would you like NanoVNA-Saver to check for updates automatically?")
if selection == QtWidgets.QMessageBox.Yes:
self.updateCheckBox.setChecked(True)
self.app.settings.setValue("CheckForUpdates", "Yes")
self.findUpdates()
elif selection == QtWidgets.QMessageBox.No:
self.updateCheckBox.setChecked(False)
self.app.settings.setValue("CheckForUpdates", "No")
QtWidgets.QMessageBox.information(
self.app,
"Checking for updates disabled",
"You can check for updates using the \"About\" window.")
else:
self.app.settings.setValue("CheckForUpdates", "Ask")
def findUpdates(self, automatic=False):
update_url = "http://mihtjel.dk/nanovna-saver/latest.json"
try:
req = request.Request(update_url)
req.add_header('User-Agent', "NanoVNA-Saver/" + self.app.version)
updates = json.load(request.urlopen(req, timeout=3))
latest_version = Version(updates['version'])
latest_url = updates['url']
except error.HTTPError as e:
logger.exception("Checking for updates produced an HTTP exception: %s", e)
self.updateLabel.setText("Connection error.")
return
except json.JSONDecodeError 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.version_string)
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",
"There is a new update for NanoVNA-Saver available!\n" +
"Version " + latest_version.version_string + "\n\n" +
"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: {strftime('%Y-%m-%d %H:%M:%S', localtime())}")
return

Wyświetl plik

@ -1,139 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 QtWidgets, QtCore
from NanoVNASaver.Windows.Screenshot import ScreenshotWindow
logger = logging.getLogger(__name__)
class DeviceSettingsWindow(QtWidgets.QWidget):
def __init__(self, app: QtWidgets.QWidget):
super().__init__()
self.app = app
self.setWindowTitle("Device settings")
self.setWindowIcon(self.app.icon)
QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
top_layout = QtWidgets.QHBoxLayout()
left_layout = QtWidgets.QVBoxLayout()
right_layout = QtWidgets.QVBoxLayout()
top_layout.addLayout(left_layout)
top_layout.addLayout(right_layout)
self.setLayout(top_layout)
status_box = QtWidgets.QGroupBox("Status")
status_layout = QtWidgets.QFormLayout(status_box)
self.statusLabel = QtWidgets.QLabel("Not connected.")
status_layout.addRow("Status:", self.statusLabel)
self.calibrationStatusLabel = QtWidgets.QLabel("Not connected.")
status_layout.addRow("Calibration:", self.calibrationStatusLabel)
status_layout.addRow(QtWidgets.QLabel("Features:"))
self.featureList = QtWidgets.QListWidget()
status_layout.addRow(self.featureList)
settings_box = QtWidgets.QGroupBox("Settings")
settings_layout = QtWidgets.QFormLayout(settings_box)
self.chkValidateInputData = QtWidgets.QCheckBox("Validate received data")
validate_input = self.app.settings.value("SerialInputValidation", True, bool)
self.chkValidateInputData.setChecked(validate_input)
self.chkValidateInputData.stateChanged.connect(self.updateValidation)
settings_layout.addRow("Validation", self.chkValidateInputData)
control_layout = QtWidgets.QHBoxLayout()
self.btnRefresh = QtWidgets.QPushButton("Refresh")
self.btnRefresh.clicked.connect(self.updateFields)
control_layout.addWidget(self.btnRefresh)
self.screenshotWindow = ScreenshotWindow()
self.btnCaptureScreenshot = QtWidgets.QPushButton("Screenshot")
self.btnCaptureScreenshot.clicked.connect(self.captureScreenshot)
control_layout.addWidget(self.btnCaptureScreenshot)
left_layout.addWidget(status_box)
left_layout.addLayout(control_layout)
self.datapoints = QtWidgets.QComboBox()
self.datapoints.addItem(str(self.app.vna.datapoints))
self.datapoints.currentIndexChanged.connect(self.updateNrDatapoints)
form_layout = QtWidgets.QFormLayout()
form_layout.addRow(QtWidgets.QLabel("Datapoints"), self.datapoints)
right_layout.addWidget(settings_box)
settings_layout.addRow(form_layout)
def _set_datapoint_index(self, dpoints: int):
self.datapoints.setCurrentIndex(
self.datapoints.findText(str(dpoints)))
def show(self):
super().show()
self.updateFields()
def updateFields(self):
if self.app.vna.isValid():
self.statusLabel.setText("Connected to " + self.app.vna.name + ".")
if self.app.worker.running:
self.calibrationStatusLabel.setText("(Sweep running)")
else:
self.calibrationStatusLabel.setText(self.app.vna.getCalibration())
self.featureList.clear()
self.featureList.addItem(self.app.vna.name + " v" + str(self.app.vna.version))
features = self.app.vna.getFeatures()
for item in features:
self.featureList.addItem(item)
self.btnCaptureScreenshot.setDisabled("Screenshots" not in features)
if "Customizable data points" in features:
self.datapoints.clear()
cur_dps = self.app.vna.datapoints
dplist = self.app.vna._datapoints[:]
for d in sorted(dplist):
self.datapoints.addItem(str(d))
self._set_datapoint_index(cur_dps)
else:
self.statusLabel.setText("Not connected.")
self.calibrationStatusLabel.setText("Not connected.")
self.featureList.clear()
self.featureList.addItem("Not connected.")
self.btnCaptureScreenshot.setDisabled(True)
def updateValidation(self, validate_data: bool):
self.app.vna.validateInput = validate_data
self.app.settings.setValue("SerialInputValidation", validate_data)
def captureScreenshot(self):
if not self.app.worker.running:
pixmap = self.app.vna.getScreenshot()
self.screenshotWindow.setScreenshot(pixmap)
self.screenshotWindow.show()
# TODO: Tell the user no screenshots while sweep is running?
# TODO: Consider having a list of widgets that want to be
# disabled when a sweep is running?
def updateNrDatapoints(self, i):
if i < 0 or self.app.worker.running:
return
logger.debug("DP: %s", self.datapoints.itemText(i))
self.app.vna.datapoints = int(self.datapoints.itemText(i))

Wyświetl plik

@ -1,767 +0,0 @@
# NanoVNASaver - a python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 List
from PyQt5 import QtWidgets, QtCore, QtGui
from NanoVNASaver.Windows.Bands import BandsWindow
from NanoVNASaver.Windows.MarkerSettings import MarkerSettingsWindow
from NanoVNASaver.Marker import Marker
logger = logging.getLogger(__name__)
class DisplaySettingsWindow(QtWidgets.QWidget):
def __init__(self, app: QtWidgets.QWidget):
super().__init__()
self.app = app
self.setWindowTitle("Display settings")
self.setWindowIcon(self.app.icon)
self.marker_window = MarkerSettingsWindow(self.app)
QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
layout = QtWidgets.QHBoxLayout()
self.setLayout(layout)
left_layout = QtWidgets.QVBoxLayout()
layout.addLayout(left_layout)
display_options_box = QtWidgets.QGroupBox("Options")
display_options_layout = QtWidgets.QFormLayout(display_options_box)
self.returnloss_group = QtWidgets.QButtonGroup()
self.returnloss_is_negative = QtWidgets.QRadioButton("Negative")
self.returnloss_is_positive = QtWidgets.QRadioButton("Positive")
self.returnloss_group.addButton(self.returnloss_is_positive)
self.returnloss_group.addButton(self.returnloss_is_negative)
display_options_layout.addRow("Return loss is:", self.returnloss_is_negative)
display_options_layout.addRow("", self.returnloss_is_positive)
if self.app.settings.value("ReturnLossPositive", False, bool):
self.returnloss_is_positive.setChecked(True)
else:
self.returnloss_is_negative.setChecked(True)
self.returnloss_is_positive.toggled.connect(self.changeReturnLoss)
self.changeReturnLoss()
self.show_lines_option = QtWidgets.QCheckBox("Show lines")
show_lines_label = QtWidgets.QLabel("Displays a thin line between data points")
self.show_lines_option.stateChanged.connect(self.changeShowLines)
display_options_layout.addRow(self.show_lines_option, show_lines_label)
self.dark_mode_option = QtWidgets.QCheckBox("Dark mode")
dark_mode_label = QtWidgets.QLabel("Black background with white text")
self.dark_mode_option.stateChanged.connect(self.changeDarkMode)
display_options_layout.addRow(self.dark_mode_option, dark_mode_label)
self.btnColorPicker = QtWidgets.QPushButton("")
self.btnColorPicker.setFixedWidth(20)
self.sweepColor = self.app.settings.value(
"SweepColor", defaultValue=QtGui.QColor(160, 140, 20, 128),
type=QtGui.QColor)
self.setSweepColor(self.sweepColor)
self.btnColorPicker.clicked.connect(lambda: self.setSweepColor(
QtWidgets.QColorDialog.getColor(
self.sweepColor, options=QtWidgets.QColorDialog.ShowAlphaChannel)))
display_options_layout.addRow("Sweep color", self.btnColorPicker)
self.btnSecondaryColorPicker = QtWidgets.QPushButton("")
self.btnSecondaryColorPicker.setFixedWidth(20)
self.secondarySweepColor = self.app.settings.value("SecondarySweepColor",
defaultValue=QtGui.QColor(
20, 160, 140, 128),
type=QtGui.QColor)
self.setSecondarySweepColor(self.secondarySweepColor)
self.btnSecondaryColorPicker.clicked.connect(lambda: self.setSecondarySweepColor(
QtWidgets.QColorDialog.getColor(self.secondarySweepColor,
options=QtWidgets.QColorDialog.ShowAlphaChannel)))
display_options_layout.addRow("Second sweep color", self.btnSecondaryColorPicker)
self.btnReferenceColorPicker = QtWidgets.QPushButton("")
self.btnReferenceColorPicker.setFixedWidth(20)
self.referenceColor = self.app.settings.value(
"ReferenceColor", defaultValue=QtGui.QColor(0, 0, 255, 48),
type=QtGui.QColor)
self.setReferenceColor(self.referenceColor)
self.btnReferenceColorPicker.clicked.connect(lambda: self.setReferenceColor(
QtWidgets.QColorDialog.getColor(
self.referenceColor, options=QtWidgets.QColorDialog.ShowAlphaChannel)))
display_options_layout.addRow("Reference color", self.btnReferenceColorPicker)
self.btnSecondaryReferenceColorPicker = QtWidgets.QPushButton("")
self.btnSecondaryReferenceColorPicker.setFixedWidth(20)
self.secondaryReferenceColor = self.app.settings.value(
"SecondaryReferenceColor",
defaultValue=QtGui.QColor(0, 0, 255, 48),
type=QtGui.QColor)
self.setSecondaryReferenceColor(self.secondaryReferenceColor)
self.btnSecondaryReferenceColorPicker.clicked.connect(
lambda: self.setSecondaryReferenceColor(
QtWidgets.QColorDialog.getColor(
self.secondaryReferenceColor,
options=QtWidgets.QColorDialog.ShowAlphaChannel)))
display_options_layout.addRow(
"Second reference color",
self.btnSecondaryReferenceColorPicker)
self.pointSizeInput = QtWidgets.QSpinBox()
pointsize = self.app.settings.value("PointSize", 2, int)
self.pointSizeInput.setValue(pointsize)
self.changePointSize(pointsize)
self.pointSizeInput.setMinimum(1)
self.pointSizeInput.setMaximum(10)
self.pointSizeInput.setSuffix(" px")
self.pointSizeInput.setAlignment(QtCore.Qt.AlignRight)
self.pointSizeInput.valueChanged.connect(self.changePointSize)
display_options_layout.addRow("Point size", self.pointSizeInput)
self.lineThicknessInput = QtWidgets.QSpinBox()
linethickness = self.app.settings.value("LineThickness", 1, int)
self.lineThicknessInput.setValue(linethickness)
self.changeLineThickness(linethickness)
self.lineThicknessInput.setMinimum(1)
self.lineThicknessInput.setMaximum(10)
self.lineThicknessInput.setSuffix(" px")
self.lineThicknessInput.setAlignment(QtCore.Qt.AlignRight)
self.lineThicknessInput.valueChanged.connect(self.changeLineThickness)
display_options_layout.addRow("Line thickness", self.lineThicknessInput)
self.markerSizeInput = QtWidgets.QSpinBox()
markersize = self.app.settings.value("MarkerSize", 6, int)
self.markerSizeInput.setValue(markersize)
self.changeMarkerSize(markersize)
self.markerSizeInput.setMinimum(4)
self.markerSizeInput.setMaximum(20)
self.markerSizeInput.setSingleStep(2)
self.markerSizeInput.setSuffix(" px")
self.markerSizeInput.setAlignment(QtCore.Qt.AlignRight)
self.markerSizeInput.valueChanged.connect(self.changeMarkerSize)
self.markerSizeInput.editingFinished.connect(self.validateMarkerSize)
display_options_layout.addRow("Marker size", self.markerSizeInput)
self.show_marker_number_option = QtWidgets.QCheckBox("Show marker numbers")
show_marker_number_label = QtWidgets.QLabel("Displays the marker number next to the marker")
self.show_marker_number_option.stateChanged.connect(self.changeShowMarkerNumber)
display_options_layout.addRow(self.show_marker_number_option, show_marker_number_label)
self.filled_marker_option = QtWidgets.QCheckBox("Filled markers")
filled_marker_label = QtWidgets.QLabel("Shows the marker as a filled triangle")
self.filled_marker_option.stateChanged.connect(self.changeFilledMarkers)
display_options_layout.addRow(self.filled_marker_option, filled_marker_label)
self.marker_tip_group = QtWidgets.QButtonGroup()
self.marker_at_center = QtWidgets.QRadioButton("At the center of the marker")
self.marker_at_tip = QtWidgets.QRadioButton("At the tip of the marker")
self.marker_tip_group.addButton(self.marker_at_center)
self.marker_tip_group.addButton(self.marker_at_tip)
display_options_layout.addRow("Data point is:", self.marker_at_center)
display_options_layout.addRow("", self.marker_at_tip)
if self.app.settings.value("MarkerAtTip", False, bool):
self.marker_at_tip.setChecked(True)
else:
self.marker_at_center.setChecked(True)
self.marker_at_tip.toggled.connect(self.changeMarkerAtTip)
self.changeMarkerAtTip()
color_options_box = QtWidgets.QGroupBox("Chart colors")
color_options_layout = QtWidgets.QFormLayout(color_options_box)
self.use_custom_colors = QtWidgets.QCheckBox("Use custom chart colors")
self.use_custom_colors.stateChanged.connect(self.changeCustomColors)
color_options_layout.addRow(self.use_custom_colors)
self.btn_background_picker = QtWidgets.QPushButton("")
self.btn_background_picker.setFixedWidth(20)
self.btn_background_picker.clicked.connect(
lambda: self.setColor(
"background",
QtWidgets.QColorDialog.getColor(
self.backgroundColor,
options=QtWidgets.QColorDialog.ShowAlphaChannel)))
color_options_layout.addRow(
"Chart background", self.btn_background_picker)
self.btn_foreground_picker = QtWidgets.QPushButton("")
self.btn_foreground_picker.setFixedWidth(20)
self.btn_foreground_picker.clicked.connect(
lambda: self.setColor(
"foreground",
QtWidgets.QColorDialog.getColor(
self.foregroundColor,
options=QtWidgets.QColorDialog.ShowAlphaChannel)))
color_options_layout.addRow("Chart foreground", self.btn_foreground_picker)
self.btn_text_picker = QtWidgets.QPushButton("")
self.btn_text_picker.setFixedWidth(20)
self.btn_text_picker.clicked.connect(
lambda: self.setColor(
"text",
QtWidgets.QColorDialog.getColor(
self.textColor,
options=QtWidgets.QColorDialog.ShowAlphaChannel)))
color_options_layout.addRow("Chart text", self.btn_text_picker)
right_layout = QtWidgets.QVBoxLayout()
layout.addLayout(right_layout)
font_options_box = QtWidgets.QGroupBox("Font")
font_options_layout = QtWidgets.QFormLayout(font_options_box)
self.font_dropdown = QtWidgets.QComboBox()
self.font_dropdown.addItems(["7", "8", "9", "10", "11", "12"])
font_size = self.app.settings.value("FontSize",
defaultValue="8",
type=str)
self.font_dropdown.setCurrentText(font_size)
self.changeFont()
self.font_dropdown.currentTextChanged.connect(self.changeFont)
font_options_layout.addRow("Font size", self.font_dropdown)
bands_box = QtWidgets.QGroupBox("Bands")
bands_layout = QtWidgets.QFormLayout(bands_box)
self.show_bands = QtWidgets.QCheckBox("Show bands")
self.show_bands.setChecked(self.app.bands.enabled)
self.show_bands.stateChanged.connect(lambda: self.setShowBands(self.show_bands.isChecked()))
bands_layout.addRow(self.show_bands)
self.btn_bands_picker = QtWidgets.QPushButton("")
self.btn_bands_picker.setFixedWidth(20)
self.btn_bands_picker.clicked.connect(
lambda: self.setColor(
"bands",
QtWidgets.QColorDialog.getColor(
self.bandsColor,
options=QtWidgets.QColorDialog.ShowAlphaChannel)))
bands_layout.addRow("Chart bands", self.btn_bands_picker)
self.btn_manage_bands = QtWidgets.QPushButton("Manage bands")
self.bandsWindow = BandsWindow(self.app)
self.btn_manage_bands.clicked.connect(self.displayBandsWindow)
bands_layout.addRow(self.btn_manage_bands)
vswr_marker_box = QtWidgets.QGroupBox("VSWR Markers")
vswr_marker_layout = QtWidgets.QFormLayout(vswr_marker_box)
self.vswrMarkers: List[float] = self.app.settings.value("VSWRMarkers", [], float)
if isinstance(self.vswrMarkers, float):
if self.vswrMarkers == 0:
self.vswrMarkers = []
else:
# Single values from the .ini become floats rather than lists. Convert them.
self.vswrMarkers = [self.vswrMarkers]
self.btn_vswr_picker = QtWidgets.QPushButton("")
self.btn_vswr_picker.setFixedWidth(20)
self.btn_vswr_picker.clicked.connect(
lambda: self.setColor(
"vswr",
QtWidgets.QColorDialog.getColor(
self.vswrColor,
options=QtWidgets.QColorDialog.ShowAlphaChannel)))
vswr_marker_layout.addRow("VSWR Markers", self.btn_vswr_picker)
self.vswr_marker_dropdown = QtWidgets.QComboBox()
vswr_marker_layout.addRow(self.vswr_marker_dropdown)
if len(self.vswrMarkers) == 0:
self.vswr_marker_dropdown.addItem("None")
else:
for m in self.vswrMarkers:
self.vswr_marker_dropdown.addItem(str(m))
for c in self.app.s11charts:
c.addSWRMarker(m)
self.vswr_marker_dropdown.setCurrentIndex(0)
btn_add_vswr_marker = QtWidgets.QPushButton("Add ...")
btn_remove_vswr_marker = QtWidgets.QPushButton("Remove")
vswr_marker_btn_layout = QtWidgets.QHBoxLayout()
vswr_marker_btn_layout.addWidget(btn_add_vswr_marker)
vswr_marker_btn_layout.addWidget(btn_remove_vswr_marker)
vswr_marker_layout.addRow(vswr_marker_btn_layout)
btn_add_vswr_marker.clicked.connect(self.addVSWRMarker)
btn_remove_vswr_marker.clicked.connect(self.removeVSWRMarker)
markers_box = QtWidgets.QGroupBox("Markers")
markers_layout = QtWidgets.QFormLayout(markers_box)
btn_add_marker = QtWidgets.QPushButton("Add")
btn_add_marker.clicked.connect(self.addMarker)
self.btn_remove_marker = QtWidgets.QPushButton("Remove")
self.btn_remove_marker.clicked.connect(self.removeMarker)
btn_marker_settings = QtWidgets.QPushButton("Settings ...")
btn_marker_settings.clicked.connect(self.displayMarkerWindow)
marker_btn_layout = QtWidgets.QHBoxLayout()
marker_btn_layout.addWidget(btn_add_marker)
marker_btn_layout.addWidget(self.btn_remove_marker)
marker_btn_layout.addWidget(btn_marker_settings)
markers_layout.addRow(marker_btn_layout)
charts_box = QtWidgets.QGroupBox("Displayed charts")
charts_layout = QtWidgets.QGridLayout(charts_box)
# selections = ["S11 Smith chart",
# "S11 LogMag",
# "S11 VSWR",
# "S11 Phase",
# "S21 Smith chart",
# "S21 LogMag",
# "S21 Phase",
# "None"]
selections = []
for c in self.app.selectable_charts:
selections.append(c.name)
selections.append("None")
chart00_selection = QtWidgets.QComboBox()
chart00_selection.addItems(selections)
chart00 = self.app.settings.value("Chart00", "S11 Smith Chart")
if chart00_selection.findText(chart00) > -1:
chart00_selection.setCurrentText(chart00)
else:
chart00_selection.setCurrentText("S11 Smith Chart")
chart00_selection.currentTextChanged.connect(
lambda: self.changeChart(0, 0, chart00_selection.currentText()))
charts_layout.addWidget(chart00_selection, 0, 0)
chart01_selection = QtWidgets.QComboBox()
chart01_selection.addItems(selections)
chart01 = self.app.settings.value("Chart01", "S11 Return Loss")
if chart01_selection.findText(chart01) > -1:
chart01_selection.setCurrentText(chart01)
else:
chart01_selection.setCurrentText("S11 Return Loss")
chart01_selection.currentTextChanged.connect(
lambda: self.changeChart(0, 1, chart01_selection.currentText()))
charts_layout.addWidget(chart01_selection, 0, 1)
chart02_selection = QtWidgets.QComboBox()
chart02_selection.addItems(selections)
chart02 = self.app.settings.value("Chart02", "None")
if chart02_selection.findText(chart02) > -1:
chart02_selection.setCurrentText(chart02)
else:
chart02_selection.setCurrentText("None")
chart02_selection.currentTextChanged.connect(
lambda: self.changeChart(0, 2, chart02_selection.currentText()))
charts_layout.addWidget(chart02_selection, 0, 2)
chart10_selection = QtWidgets.QComboBox()
chart10_selection.addItems(selections)
chart10 = self.app.settings.value("Chart10", "S21 Polar Plot")
if chart10_selection.findText(chart10) > -1:
chart10_selection.setCurrentText(chart10)
else:
chart10_selection.setCurrentText("S21 Polar Plot")
chart10_selection.currentTextChanged.connect(
lambda: self.changeChart(1, 0, chart10_selection.currentText()))
charts_layout.addWidget(chart10_selection, 1, 0)
chart11_selection = QtWidgets.QComboBox()
chart11_selection.addItems(selections)
chart11 = self.app.settings.value("Chart11", "S21 Gain")
if chart11_selection.findText(chart11) > -1:
chart11_selection.setCurrentText(chart11)
else:
chart11_selection.setCurrentText("S21 Gain")
chart11_selection.currentTextChanged.connect(
lambda: self.changeChart(1, 1, chart11_selection.currentText()))
charts_layout.addWidget(chart11_selection, 1, 1)
chart12_selection = QtWidgets.QComboBox()
chart12_selection.addItems(selections)
chart12 = self.app.settings.value("Chart12", "None")
if chart12_selection.findText(chart12) > -1:
chart12_selection.setCurrentText(chart12)
else:
chart12_selection.setCurrentText("None")
chart12_selection.currentTextChanged.connect(
lambda: self.changeChart(1, 2, chart12_selection.currentText()))
charts_layout.addWidget(chart12_selection, 1, 2)
self.changeChart(0, 0, chart00_selection.currentText())
self.changeChart(0, 1, chart01_selection.currentText())
self.changeChart(0, 2, chart02_selection.currentText())
self.changeChart(1, 0, chart10_selection.currentText())
self.changeChart(1, 1, chart11_selection.currentText())
self.changeChart(1, 2, chart12_selection.currentText())
self.backgroundColor = self.app.settings.value(
"BackgroundColor", defaultValue=QtGui.QColor("white"),
type=QtGui.QColor)
self.foregroundColor = self.app.settings.value(
"ForegroundColor", defaultValue=QtGui.QColor("lightgray"),
type=QtGui.QColor)
self.textColor = self.app.settings.value(
"TextColor", defaultValue=QtGui.QColor("black"),
type=QtGui.QColor)
self.bandsColor = self.app.settings.value(
"BandsColor", defaultValue=QtGui.QColor(128, 128, 128, 48),
type=QtGui.QColor)
self.app.bands.color = self.bandsColor
self.vswrColor = self.app.settings.value(
"VSWRColor", defaultValue=QtGui.QColor(192, 0, 0, 128),
type=QtGui.QColor)
self.dark_mode_option.setChecked(
self.app.settings.value("DarkMode", False, bool))
self.show_lines_option.setChecked(
self.app.settings.value("ShowLines", False, bool))
self.show_marker_number_option.setChecked(
self.app.settings.value("ShowMarkerNumbers", False, bool))
self.filled_marker_option.setChecked(
self.app.settings.value("FilledMarkers", False, bool))
if self.app.settings.value("UseCustomColors",
defaultValue=False, type=bool):
self.dark_mode_option.setDisabled(True)
self.dark_mode_option.setChecked(False)
self.use_custom_colors.setChecked(True)
else:
self.btn_background_picker.setDisabled(True)
self.btn_foreground_picker.setDisabled(True)
self.btn_text_picker.setDisabled(True)
self.changeCustomColors() # Update all the colours of all the charts
p = self.btn_background_picker.palette()
p.setColor(QtGui.QPalette.ButtonText, self.backgroundColor)
self.btn_background_picker.setPalette(p)
p = self.btn_foreground_picker.palette()
p.setColor(QtGui.QPalette.ButtonText, self.foregroundColor)
self.btn_foreground_picker.setPalette(p)
p = self.btn_text_picker.palette()
p.setColor(QtGui.QPalette.ButtonText, self.textColor)
self.btn_text_picker.setPalette(p)
p = self.btn_bands_picker.palette()
p.setColor(QtGui.QPalette.ButtonText, self.bandsColor)
self.btn_bands_picker.setPalette(p)
p = self.btn_vswr_picker.palette()
p.setColor(QtGui.QPalette.ButtonText, self.vswrColor)
self.btn_vswr_picker.setPalette(p)
left_layout.addWidget(display_options_box)
left_layout.addWidget(charts_box)
left_layout.addWidget(markers_box)
left_layout.addStretch(1)
right_layout.addWidget(color_options_box)
right_layout.addWidget(font_options_box)
right_layout.addWidget(bands_box)
right_layout.addWidget(vswr_marker_box)
right_layout.addStretch(1)
def changeChart(self, x, y, chart):
found = None
for c in self.app.selectable_charts:
if c.name == chart:
found = c
self.app.settings.setValue("Chart" + str(x) + str(y), chart)
old_widget = self.app.charts_layout.itemAtPosition(x, y)
if old_widget is not None:
w = old_widget.widget()
self.app.charts_layout.removeWidget(w)
w.hide()
if found is not None:
if self.app.charts_layout.indexOf(found) > -1:
logger.debug("%s is already shown, duplicating.", found.name)
found = self.app.copyChart(found)
self.app.charts_layout.addWidget(found, x, y)
if found.isHidden():
found.show()
def changeReturnLoss(self):
state = self.returnloss_is_positive.isChecked()
self.app.settings.setValue("ReturnLossPositive", state)
for m in self.app.markers:
m.returnloss_is_positive = state
m.updateLabels(self.app.data, self.app.data21)
self.marker_window.exampleMarker.returnloss_is_positive = state
self.marker_window.updateMarker()
self.app.s11LogMag.isInverted = state
self.app.s11LogMag.update()
def changeShowLines(self):
state = self.show_lines_option.isChecked()
self.app.settings.setValue("ShowLines", state)
for c in self.app.subscribing_charts:
c.setDrawLines(state)
def changeShowMarkerNumber(self):
state = self.show_marker_number_option.isChecked()
self.app.settings.setValue("ShowMarkerNumbers", state)
for c in self.app.subscribing_charts:
c.setDrawMarkerNumbers(state)
def changeFilledMarkers(self):
state = self.filled_marker_option.isChecked()
self.app.settings.setValue("FilledMarkers", state)
for c in self.app.subscribing_charts:
c.setFilledMarkers(state)
def changeMarkerAtTip(self):
state = self.marker_at_tip.isChecked()
self.app.settings.setValue("MarkerAtTip", state)
for c in self.app.subscribing_charts:
c.setMarkerAtTip(state)
def changePointSize(self, size: int):
self.app.settings.setValue("PointSize", size)
for c in self.app.subscribing_charts:
c.setPointSize(size)
def changeLineThickness(self, size: int):
self.app.settings.setValue("LineThickness", size)
for c in self.app.subscribing_charts:
c.setLineThickness(size)
def changeMarkerSize(self, size: int):
if size % 2 == 0:
self.app.settings.setValue("MarkerSize", size)
for c in self.app.subscribing_charts:
c.setMarkerSize(int(size / 2))
def validateMarkerSize(self):
size = self.markerSizeInput.value()
if size % 2 != 0:
self.markerSizeInput.setValue(size + 1)
def changeDarkMode(self):
state = self.dark_mode_option.isChecked()
self.app.settings.setValue("DarkMode", state)
if state:
for c in self.app.subscribing_charts:
c.setBackgroundColor(QtGui.QColor(QtCore.Qt.black))
c.setForegroundColor(QtGui.QColor(QtCore.Qt.lightGray))
c.setTextColor(QtGui.QColor(QtCore.Qt.white))
c.setSWRColor(self.vswrColor)
else:
for c in self.app.subscribing_charts:
c.setBackgroundColor(QtGui.QColor(QtCore.Qt.white))
c.setForegroundColor(QtGui.QColor(QtCore.Qt.lightGray))
c.setTextColor(QtGui.QColor(QtCore.Qt.black))
c.setSWRColor(self.vswrColor)
def changeCustomColors(self):
self.app.settings.setValue("UseCustomColors", self.use_custom_colors.isChecked())
if self.use_custom_colors.isChecked():
self.dark_mode_option.setDisabled(True)
self.dark_mode_option.setChecked(False)
self.btn_background_picker.setDisabled(False)
self.btn_foreground_picker.setDisabled(False)
self.btn_text_picker.setDisabled(False)
for c in self.app.subscribing_charts:
c.setBackgroundColor(self.backgroundColor)
c.setForegroundColor(self.foregroundColor)
c.setTextColor(self.textColor)
c.setSWRColor(self.vswrColor)
else:
self.dark_mode_option.setDisabled(False)
self.btn_background_picker.setDisabled(True)
self.btn_foreground_picker.setDisabled(True)
self.btn_text_picker.setDisabled(True)
self.changeDarkMode() # Reset to the default colors depending on Dark Mode setting
def setColor(self, name: str, color: QtGui.QColor):
if name == "background":
p = self.btn_background_picker.palette()
p.setColor(QtGui.QPalette.ButtonText, color)
self.btn_background_picker.setPalette(p)
self.backgroundColor = color
self.app.settings.setValue("BackgroundColor", color)
elif name == "foreground":
p = self.btn_foreground_picker.palette()
p.setColor(QtGui.QPalette.ButtonText, color)
self.btn_foreground_picker.setPalette(p)
self.foregroundColor = color
self.app.settings.setValue("ForegroundColor", color)
elif name == "text":
p = self.btn_text_picker.palette()
p.setColor(QtGui.QPalette.ButtonText, color)
self.btn_text_picker.setPalette(p)
self.textColor = color
self.app.settings.setValue("TextColor", color)
elif name == "bands":
p = self.btn_bands_picker.palette()
p.setColor(QtGui.QPalette.ButtonText, color)
self.btn_bands_picker.setPalette(p)
self.bandsColor = color
self.app.settings.setValue("BandsColor", color)
self.app.bands.setColor(color)
elif name == "vswr":
p = self.btn_vswr_picker.palette()
p.setColor(QtGui.QPalette.ButtonText, color)
self.btn_vswr_picker.setPalette(p)
self.vswrColor = color
self.app.settings.setValue("VSWRColor", color)
self.changeCustomColors()
def setSweepColor(self, color: QtGui.QColor):
if color.isValid():
self.sweepColor = color
p = self.btnColorPicker.palette()
p.setColor(QtGui.QPalette.ButtonText, color)
self.btnColorPicker.setPalette(p)
self.app.settings.setValue("SweepColor", color)
self.app.settings.sync()
for c in self.app.subscribing_charts:
c.setSweepColor(color)
def setSecondarySweepColor(self, color: QtGui.QColor):
if color.isValid():
self.secondarySweepColor = color
p = self.btnSecondaryColorPicker.palette()
p.setColor(QtGui.QPalette.ButtonText, color)
self.btnSecondaryColorPicker.setPalette(p)
self.app.settings.setValue("SecondarySweepColor", color)
self.app.settings.sync()
for c in self.app.subscribing_charts:
c.setSecondarySweepColor(color)
def setReferenceColor(self, color):
if color.isValid():
self.referenceColor = color
p = self.btnReferenceColorPicker.palette()
p.setColor(QtGui.QPalette.ButtonText, color)
self.btnReferenceColorPicker.setPalette(p)
self.app.settings.setValue("ReferenceColor", color)
self.app.settings.sync()
for c in self.app.subscribing_charts:
c.setReferenceColor(color)
def setSecondaryReferenceColor(self, color):
if color.isValid():
self.secondaryReferenceColor = color
p = self.btnSecondaryReferenceColorPicker.palette()
p.setColor(QtGui.QPalette.ButtonText, color)
self.btnSecondaryReferenceColorPicker.setPalette(p)
self.app.settings.setValue("SecondaryReferenceColor", color)
self.app.settings.sync()
for c in self.app.subscribing_charts:
c.setSecondaryReferenceColor(color)
def setShowBands(self, show_bands):
self.app.bands.enabled = show_bands
self.app.bands.settings.setValue("ShowBands", show_bands)
self.app.bands.settings.sync()
for c in self.app.subscribing_charts:
c.update()
def changeFont(self):
font_size = self.font_dropdown.currentText()
self.app.settings.setValue("FontSize", font_size)
app: QtWidgets.QApplication = QtWidgets.QApplication.instance()
font = app.font()
font.setPointSize(int(font_size))
app.setFont(font)
self.app.changeFont(font)
def displayBandsWindow(self):
self.bandsWindow.show()
QtWidgets.QApplication.setActiveWindow(self.bandsWindow)
def displayMarkerWindow(self):
self.marker_window.show()
QtWidgets.QApplication.setActiveWindow(self.marker_window)
def addMarker(self):
new_marker = Marker("", self.app.settings)
new_marker.setScale(self.app.scaleFactor)
self.app.markers.append(new_marker)
self.app.marker_data_layout.addWidget(new_marker.getGroupBox())
new_marker.updated.connect(self.app.markerUpdated)
label, layout = new_marker.getRow()
self.app.marker_control_layout.insertRow(Marker.count() - 1, label, layout)
self.btn_remove_marker.setDisabled(False)
def removeMarker(self):
# keep at least one marker
if Marker.count() <= 1:
return
if Marker.count() == 2:
self.btn_remove_marker.setDisabled(True)
last_marker = self.app.markers.pop()
last_marker.updated.disconnect(self.app.markerUpdated)
self.app.marker_data_layout.removeWidget(last_marker.getGroupBox())
self.app.marker_control_layout.removeRow(Marker.count()-1)
last_marker.getGroupBox().hide()
last_marker.getGroupBox().destroy()
label, _ = last_marker.getRow()
label.hide()
def addVSWRMarker(self):
value, selected = QtWidgets.QInputDialog.getDouble(
self, "Add VSWR Marker", "VSWR value to show:", min=1.001, decimals=3)
if selected:
self.vswrMarkers.append(value)
if self.vswr_marker_dropdown.itemText(0) == "None":
self.vswr_marker_dropdown.removeItem(0)
self.vswr_marker_dropdown.addItem(str(value))
self.vswr_marker_dropdown.setCurrentText(str(value))
for c in self.app.s11charts:
c.addSWRMarker(value)
self.app.settings.setValue("VSWRMarkers", self.vswrMarkers)
def removeVSWRMarker(self):
value_str = self.vswr_marker_dropdown.currentText()
if value_str != "None":
value = float(value_str)
self.vswrMarkers.remove(value)
self.vswr_marker_dropdown.removeItem(self.vswr_marker_dropdown.currentIndex())
if self.vswr_marker_dropdown.count() == 0:
self.vswr_marker_dropdown.addItem("None")
self.app.settings.remove("VSWRMarkers")
else:
self.app.settings.setValue("VSWRMarkers", self.vswrMarkers)
for c in self.app.s11charts:
c.removeSWRMarker(value)

Wyświetl plik

@ -1,202 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 QtWidgets, QtCore
from NanoVNASaver.Formatting import (
format_frequency_short, format_frequency_sweep,
)
logger = logging.getLogger(__name__)
class SweepSettingsWindow(QtWidgets.QWidget):
def __init__(self, app: QtWidgets.QWidget):
super().__init__()
self.app = app
self.setWindowTitle("Sweep settings")
self.setWindowIcon(self.app.icon)
QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
title_box = QtWidgets.QGroupBox("Sweep name")
title_layout = QtWidgets.QFormLayout(title_box)
self.sweep_title_input = QtWidgets.QLineEdit()
title_layout.addRow("Sweep name", self.sweep_title_input)
title_button_layout = QtWidgets.QHBoxLayout()
btn_set_sweep_title = QtWidgets.QPushButton("Set")
btn_set_sweep_title.clicked.connect(
lambda: self.app.setSweepTitle(self.sweep_title_input.text()))
btn_reset_sweep_title = QtWidgets.QPushButton("Reset")
btn_reset_sweep_title.clicked.connect(lambda: self.app.setSweepTitle(""))
title_button_layout.addWidget(btn_set_sweep_title)
title_button_layout.addWidget(btn_reset_sweep_title)
title_layout.addRow(title_button_layout)
layout.addWidget(title_box)
settings_box = QtWidgets.QGroupBox("Settings")
settings_layout = QtWidgets.QFormLayout(settings_box)
self.single_sweep_radiobutton = QtWidgets.QRadioButton("Single sweep")
self.continuous_sweep_radiobutton = QtWidgets.QRadioButton("Continuous sweep")
self.averaged_sweep_radiobutton = QtWidgets.QRadioButton("Averaged sweep")
settings_layout.addWidget(self.single_sweep_radiobutton)
self.single_sweep_radiobutton.setChecked(True)
settings_layout.addWidget(self.continuous_sweep_radiobutton)
settings_layout.addWidget(self.averaged_sweep_radiobutton)
self.averages = QtWidgets.QLineEdit("3")
self.truncates = QtWidgets.QLineEdit("0")
settings_layout.addRow("Number of measurements to average", self.averages)
settings_layout.addRow("Number to discard", self.truncates)
settings_layout.addRow(
QtWidgets.QLabel(
"Averaging allows discarding outlying samples to get better averages."))
settings_layout.addRow(
QtWidgets.QLabel("Common values are 3/0, 5/2, 9/4 and 25/6."))
self.s21att = QtWidgets.QLineEdit("0")
settings_layout.addRow(QtWidgets.QLabel(""))
settings_layout.addRow(QtWidgets.QLabel("Some times when you measure amplifiers you need to use an attenuator"))
settings_layout.addRow(QtWidgets.QLabel("in line with the S21 input (CH1) here you can specify it."))
settings_layout.addRow("Attenuator in port CH1 (s21) in dB", self.s21att)
settings_layout.addRow(QtWidgets.QLabel("Common values with un-un are 16.9 (49:1 2450) 9.54 (9:1 450)"))
self.continuous_sweep_radiobutton.toggled.connect(
lambda: self.app.worker.setContinuousSweep(
self.continuous_sweep_radiobutton.isChecked()))
self.averaged_sweep_radiobutton.toggled.connect(self.updateAveraging)
self.averages.textEdited.connect(self.updateAveraging)
self.truncates.textEdited.connect(self.updateAveraging)
self.s21att.textEdited.connect(self.setS21Attenuator)
layout.addWidget(settings_box)
band_sweep_box = QtWidgets.QGroupBox("Sweep band")
band_sweep_layout = QtWidgets.QFormLayout(band_sweep_box)
self.band_list = QtWidgets.QComboBox()
self.band_list.setModel(self.app.bands)
self.band_list.currentIndexChanged.connect(self.updateCurrentBand)
band_sweep_layout.addRow("Select band", self.band_list)
self.band_pad_group = QtWidgets.QButtonGroup()
self.band_pad_0 = QtWidgets.QRadioButton("None")
self.band_pad_10 = QtWidgets.QRadioButton("10%")
self.band_pad_25 = QtWidgets.QRadioButton("25%")
self.band_pad_100 = QtWidgets.QRadioButton("100%")
self.band_pad_0.setChecked(True)
self.band_pad_group.addButton(self.band_pad_0)
self.band_pad_group.addButton(self.band_pad_10)
self.band_pad_group.addButton(self.band_pad_25)
self.band_pad_group.addButton(self.band_pad_100)
self.band_pad_group.buttonClicked.connect(self.updateCurrentBand)
band_sweep_layout.addRow("Pad band limits", self.band_pad_0)
band_sweep_layout.addRow("", self.band_pad_10)
band_sweep_layout.addRow("", self.band_pad_25)
band_sweep_layout.addRow("", self.band_pad_100)
self.band_limit_label = QtWidgets.QLabel()
band_sweep_layout.addRow(self.band_limit_label)
btn_set_band_sweep = QtWidgets.QPushButton("Set band sweep")
btn_set_band_sweep.clicked.connect(self.setBandSweep)
band_sweep_layout.addRow(btn_set_band_sweep)
self.updateCurrentBand()
layout.addWidget(band_sweep_box)
def updateCurrentBand(self):
index_start = self.band_list.model().index(self.band_list.currentIndex(), 1)
index_stop = self.band_list.model().index(self.band_list.currentIndex(), 2)
start = int(self.band_list.model().data(index_start, QtCore.Qt.ItemDataRole).value())
stop = int(self.band_list.model().data(index_stop, QtCore.Qt.ItemDataRole).value())
if self.band_pad_10.isChecked():
padding = 10
elif self.band_pad_25.isChecked():
padding = 25
elif self.band_pad_100.isChecked():
padding = 100
else:
padding = 0
if padding > 0:
span = stop - start
start -= round(span * padding / 100)
start = max(1, start)
stop += round(span * padding / 100)
self.band_limit_label.setText(
f"Sweep span: {format_frequency_short(start)}"
f" to {format_frequency_short(stop)}")
def setS21Attenuator(self):
try:
s21att = float(self.s21att.text())
except:
s21att = 0
if (s21att < 0):
logger.warning("Values for attenuator are absolute and with no minus sign, resetting.")
self.s21att.setText("0")
else:
logger.info("Setting an attenuator of %.2f dB inline with the CH1/S21 input", s21att)
self.app.s21att = s21att
def setBandSweep(self):
index_start = self.band_list.model().index(self.band_list.currentIndex(), 1)
index_stop = self.band_list.model().index(self.band_list.currentIndex(), 2)
start = int(self.band_list.model().data(index_start, QtCore.Qt.ItemDataRole).value())
stop = int(self.band_list.model().data(index_stop, QtCore.Qt.ItemDataRole).value())
if self.band_pad_10.isChecked():
padding = 10
elif self.band_pad_25.isChecked():
padding = 25
elif self.band_pad_100.isChecked():
padding = 100
else:
padding = 0
if padding > 0:
span = stop - start
start -= round(span * padding / 100)
start = max(1, start)
stop += round(span * padding / 100)
self.app.sweepStartInput.setText(format_frequency_sweep(start))
self.app.sweepEndInput.setText(format_frequency_sweep(stop))
self.app.sweepEndInput.textEdited.emit(self.app.sweepEndInput.text())
def updateAveraging(self):
self.app.worker.setAveraging(self.averaged_sweep_radiobutton.isChecked(),
self.averages.text(),
self.truncates.text())

Wyświetl plik

@ -1,152 +0,0 @@
# NanoVNASaver - a python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 math
import numpy as np
import scipy.signal as signal
from PyQt5 import QtWidgets, QtCore
logger = logging.getLogger(__name__)
class TDRWindow(QtWidgets.QWidget):
updated = QtCore.pyqtSignal()
def __init__(self, app: QtWidgets.QWidget):
super().__init__()
self.app = app
self.td = []
self.distance_axis = []
self.step_response = []
self.step_response_Z = []
self.setWindowTitle("TDR")
self.setWindowIcon(self.app.icon)
QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
layout = QtWidgets.QFormLayout()
self.setLayout(layout)
self.tdr_velocity_dropdown = QtWidgets.QComboBox()
self.tdr_velocity_dropdown.addItem("Jelly filled (0.64)", 0.64)
self.tdr_velocity_dropdown.addItem("Polyethylene (0.66)", 0.66)
self.tdr_velocity_dropdown.addItem("PTFE (Teflon) (0.70)", 0.70)
self.tdr_velocity_dropdown.addItem("Pulp Insulation (0.72)", 0.72)
self.tdr_velocity_dropdown.addItem("Foam or Cellular PE (0.78)", 0.78)
self.tdr_velocity_dropdown.addItem("Semi-solid PE (SSPE) (0.84)", 0.84)
self.tdr_velocity_dropdown.addItem("Air (Helical spacers) (0.94)", 0.94)
self.tdr_velocity_dropdown.insertSeparator(self.tdr_velocity_dropdown.count())
# Lots of cable types added by Larry Goga, AE5CZ
self.tdr_velocity_dropdown.addItem("RG-6/U PE 75\N{OHM SIGN} (Belden 8215) (0.66)", 0.66)
self.tdr_velocity_dropdown.addItem("RG-6/U Foam 75\N{OHM SIGN} (Belden 9290) (0.81)", 0.81)
self.tdr_velocity_dropdown.addItem("RG-8/U PE 50\N{OHM SIGN} (Belden 8237) (0.66)", 0.66)
self.tdr_velocity_dropdown.addItem("RG-8/U Foam (Belden 8214) (0.78)", 0.78)
self.tdr_velocity_dropdown.addItem("RG-8/U (Belden 9913) (0.84)", 0.84)
self.tdr_velocity_dropdown.addItem("RG-8X (Belden 9258) (0.82)", 0.82)
self.tdr_velocity_dropdown.addItem(
"RG-11/U 75\N{OHM SIGN} Foam HDPE (Belden 9292) (0.84)", 0.84)
self.tdr_velocity_dropdown.addItem("RG-58/U 52\N{OHM SIGN} PE (Belden 9201) (0.66)", 0.66)
self.tdr_velocity_dropdown.addItem(
"RG-58A/U 54\N{OHM SIGN} Foam (Belden 8219) (0.73)", 0.73)
self.tdr_velocity_dropdown.addItem("RG-59A/U PE 75\N{OHM SIGN} (Belden 8241) (0.66)", 0.66)
self.tdr_velocity_dropdown.addItem(
"RG-59A/U Foam 75\N{OHM SIGN} (Belden 8241F) (0.78)", 0.78)
self.tdr_velocity_dropdown.addItem("RG-174 PE (Belden 8216)(0.66)", 0.66)
self.tdr_velocity_dropdown.addItem("RG-174 Foam (Belden 7805R) (0.735)", 0.735)
self.tdr_velocity_dropdown.addItem("RG-213/U PE (Belden 8267) (0.66)", 0.66)
self.tdr_velocity_dropdown.addItem("RG316 (0.695)", 0.695)
self.tdr_velocity_dropdown.addItem("RG402 (0.695)", 0.695)
self.tdr_velocity_dropdown.addItem("LMR-240 (0.84)", 0.84)
self.tdr_velocity_dropdown.addItem("LMR-240UF (0.80)", 0.80)
self.tdr_velocity_dropdown.addItem("LMR-400 (0.85)", 0.85)
self.tdr_velocity_dropdown.addItem("LMR400UF (0.83)", 0.83)
self.tdr_velocity_dropdown.addItem("Davis Bury-FLEX (0.82)", 0.82)
self.tdr_velocity_dropdown.insertSeparator(self.tdr_velocity_dropdown.count())
self.tdr_velocity_dropdown.addItem("Custom", -1)
self.tdr_velocity_dropdown.setCurrentIndex(1) # Default to PE (0.66)
self.tdr_velocity_dropdown.currentIndexChanged.connect(self.updateTDR)
layout.addRow(self.tdr_velocity_dropdown)
self.tdr_velocity_input = QtWidgets.QLineEdit()
self.tdr_velocity_input.setDisabled(True)
self.tdr_velocity_input.setText("0.66")
self.tdr_velocity_input.textChanged.connect(self.app.dataUpdated)
layout.addRow("Velocity factor", self.tdr_velocity_input)
self.tdr_result_label = QtWidgets.QLabel()
layout.addRow("Estimated cable length:", self.tdr_result_label)
layout.addRow(self.app.tdr_chart)
def updateTDR(self):
c = 299792458
# TODO: Let the user select whether to use high or low resolution TDR?
FFT_POINTS = 2**14
if len(self.app.data) < 2:
return
if self.tdr_velocity_dropdown.currentData() == -1:
self.tdr_velocity_input.setDisabled(False)
else:
self.tdr_velocity_input.setDisabled(True)
self.tdr_velocity_input.setText(str(self.tdr_velocity_dropdown.currentData()))
try:
v = float(self.tdr_velocity_input.text())
except ValueError:
return
step_size = self.app.data[1].freq - self.app.data[0].freq
if step_size == 0:
self.tdr_result_label.setText("")
logger.info("Cannot compute cable length at 0 span")
return
s11 = []
for d in self.app.data:
s11.append(np.complex(d.re, d.im))
window = np.blackman(len(self.app.data))
windowed_s11 = window * s11
self.td = np.abs(np.fft.ifft(windowed_s11, FFT_POINTS))
step = np.ones(FFT_POINTS)
self.step_response = signal.convolve(self.td, step)
self.step_response_Z = 50 * (1 + self.step_response) / (1 - self.step_response)
time_axis = np.linspace(0, 1/step_size, FFT_POINTS)
self.distance_axis = time_axis * v * c
# peak = np.max(td)
# We should check that this is an actual *peak*, and not just a vague maximum
index_peak = np.argmax(self.td)
cable_len = round(self.distance_axis[index_peak]/2, 3)
feet = math.floor(cable_len / 0.3048)
inches = round(((cable_len / 0.3048) - feet)*12, 1)
self.tdr_result_label.setText(f"{cable_len}m ({feet}ft {inches}in)")
self.app.tdr_result_label.setText(str(cable_len) + " m")
self.updated.emit()

Wyświetl plik

@ -1,79 +0,0 @@
#! /bin/env python
# NanoVNASaver - a python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published 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 sys
from PyQt5 import QtWidgets, QtCore
from .NanoVNASaver import NanoVNASaver
from .about import debug
def main():
print("NanoVNASaver " + NanoVNASaver.version)
print("Copyright (C) 2019 Rune B. Broberg")
print("This program comes with ABSOLUTELY NO WARRANTY")
print("This program is licensed under the GNU General Public License version 3")
print("")
print("See https://github.com/mihtjel/nanovna-saver for further details")
# Main code goes here
console_log_level = logging.WARNING
file_log_level = logging.DEBUG
log_file = ""
for i in range(len(sys.argv)):
if sys.argv[i] == "-d":
console_log_level = logging.DEBUG
elif sys.argv[i] == "-D" and i < len(sys.argv) - 1:
log_file = sys.argv[i+1]
elif sys.argv[i] == "-D":
print("You must enter a file name when using -D")
return
if debug:
console_log_level = logging.DEBUG
logger = logging.getLogger("NanoVNASaver")
logger.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setLevel(console_log_level)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
if log_file != "":
try:
fh = logging.FileHandler(log_file)
except Exception as e:
logger.exception("Error opening log file: %s", e)
return
fh.setLevel(file_log_level)
fh.setFormatter(formatter)
logger.addHandler(fh)
logger.info("Startup...")
QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)
app = QtWidgets.QApplication(sys.argv)
window = NanoVNASaver()
window.show()
app.exec_()
if __name__ == '__main__':
main()

Wyświetl plik

@ -0,0 +1 @@
icon_48x48.png

14
Pipfile
Wyświetl plik

@ -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"

228
README.md
Wyświetl plik

@ -1,228 +0,0 @@
[![Latest Release](https://img.shields.io/github/v/release/mihtjel/nanovna-saver.svg)](https://github.com/mihtjel/nanovna-saver/releases/latest)
[![License](https://img.shields.io/github/license/mihtjel/nanovna-saver.svg)](https://github.com/mihtjel/nanovna-saver/blob/master/LICENSE)
[![Downloads](https://img.shields.io/github/downloads/mihtjel/nanovna-saver/total.svg)](https://github.com/mihtjel/nanovna-saver/releases/)
[![GitHub Releases](https://img.shields.io/github/downloads/mihtjel/nanovna-saver/latest/total)](https://github.com/mihtjel/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&currency_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
## Changes in this fork
- This fork adds support for the saa2, a vna loosely
based on the original nanovna with frequency range up to 3Ghz.
- Added ability to add attenutor values in s11 sweep settings
for amplifier measuremnts
## Introduction This software connects to a NanoVNA and extracts the data for
display on a computer, and for saving to Touchstone files.
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
0.1.4:
![Screenshot of version 0.1.4](https://i.imgur.com/ZoFsV2V.png)
## Running the application
### Windows
The software was written in Python on Windows, using Pycharm, and the modules
PyQT5, numpy, scipy and pyserial.
#### Binary releases
You can find the latest binary (.exe) release for Windows at
https://github.com/mihtjel/nanovna-saver/releases/latest
The downloadable executable runs directly, and requires no installation. For
Windows 7, it does require Service Pack 1 and [Microsoft VC++
Redistributable](https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads).
For most users, this is already installed.
Windows versions older than Windows 7 are not known to work. It may be
possible to run on those directly from the python code:
#### Installation and Use with pip
1. Clone repo and cd into the directory
git clone https://github.com/mihtjel/nanovna-saver
cd nanovna-saver
3. Run the pip installation
pip3 install .
4. Once completed run with the following command
NanoVNASaver
### Linux
#### Ubuntu 18.04 & 19.04
##### Installation and Use with pip
1. Install python3.7 and pip
sudo apt install python3.7 python3-pip
3. Clone repo and cd into the directory
git clone https://github.com/mihtjel/nanovna-saver
cd nanovna-saver
4. Update pip and run the pip installation
python3.7 -m pip install -U pip
python3.7 -m pip install .
(You may need to install the additional packages python3-distutils,
python3-setuptools and python3-wheel for this command to work on some
distributions.)
5. Once completed run with the following command
python3.7 nanovna-saver.py
### Mac OS:
#### Homebrew
1. Install Homebrew
From : https://brew.sh/
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
2. Python :
brew install python
3. NanoVNASaver Installation
git clone https://github.com/mihtjel/nanovna-saver
cd nanovna-saver
4. Install local pip packages
python3 -m pip install .
NanoVNASaver
## 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 - ie. 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.
### Frequency bands
Open the "Display setup" window to configure the display of frequency bands. By
clicking "show bands", predefined frequency bands will be shown on the
frequency-based charts. Click manage bands to change which bands are shown,
and the frequency limits of each. Bands default and reset to European amateur
radio band frequencies.
## 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.
## Links
* 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/]
## Credits
Original application by Rune B. Broberg (5Q5R)
Contributions and changes by Holger Müller, 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&currency_code=EUR&source=url)

271
README.rst 100644
Wyświetl plik

@ -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&currency_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&currency_code=EUR&source=url
:alt: Paypal

Wyświetl plik

@ -1 +0,0 @@
theme: jekyll-theme-dinky

21
build-macos-app.sh 100755
Wyświetl plik

@ -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}

3
debug.sh 100755
Wyświetl plik

@ -0,0 +1,3 @@
#!/bin/sh
export PYTHONPATH="src"
exec python -m debugpy --listen 5678 --wait-for-client $@

Wyświetl plik

@ -0,0 +1,83 @@
Contributor Covenant Code of Conduct
====================================
Our Pledge
----------
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to make participation in our project and
our community a harassment-free experience for everyone, regardless of age,
body size, disability, ethnicity, sex characteristics, gender identity and
expression, level of experience, education, socio-economic status, nationality,
personal appearance, race, religion, or sexual identity and orientation.
Our Standards
-------------
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances Trolling, insulting/derogatory comments, and personal or political
attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission Other conduct which could reasonably be
considered inappropriate in a professional setting
Our Responsibilities
--------------------
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
Scope
-----
This Code of Conduct applies within all project spaces, and it also applies
when an individual is representing the project or its community in public
spaces. Examples of representing a project or community include using an
official project email address, posting via an official social media account,
or acting as an appointed representative at an online or offline event.
Representation of a project may be further defined and clarified by project
maintainers.
Enforcement
-----------
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project maintainer using any of the [private contact
addresses](https://github.com/Nanovna-Saver/nanovna-saver#support). All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an
incident. Further details of specific enforcement policies may be posted
separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor
Covenant](https://www.contributor-covenant.org), version 1.4, available at
<https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>
For answers to common questions about this code of conduct, see
<https://www.contributor-covenant.org/faq>

Wyświetl plik

@ -0,0 +1,51 @@
Contributing
============
When contributing to this repository, please first discuss the change you wish
to make via issue, email, or any other method with the owners of this
repository before making a change.
Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it
in all your interactions with the project.
Development environment setup
------------------------------
1. Clone the repo
```sh
git clone https://github.com/NanoVNA-Saver/nanovna-saver
```
2. TODO
## Issues and feature requests
You've found a bug in the source code, a mistake in the documentation or maybe
you'd like a new feature?Take a look at [GitHub
Discussions](https://github.com/NanoVNA-Saver/nanovna-saver/discussions) to see
if it's already being discussed. You can help us by [submitting an issue on
GitHub](https://github.com/NanoVNA-Saver/nanovna-saver/issues). Before you
create an issue, make sure to search the issue archive -- your issue may have
already been addressed!
Please try to create bug reports that are:
- _Reproducible._ Include steps to reproduce the problem.
- _Specific._ Include as much detail as possible: which version, what environment, etc.
- _Unique._ Do not duplicate existing opened issues.
- _Scoped to a Single Bug._ One bug per report.
**Even better: Submit a pull request with a fix or new feature!**
### How to submit a Pull Request
1. Search our repository for open or closed
[Pull Requests](https://github.com/NanoVNA-Saver/nanovna-saver/pulls)
that relate to your submission. You don't want to duplicate effort.
2. Fork the project
3. Create your feature branch (`git checkout -b feat/amazing_feature`)
4. Commit your changes (`git commit -m 'feat: add amazing_feature'`)
NanoVNA-Saver uses [conventional commits](https://www.conventionalcommits.org),
so please follow the specification in your commit messages. 5. Push to the
branch (`git push origin feat/amazing_feature`)
6. [Open a Pull Request](https://github.com/NanoVNA-Saver/nanovna-saver/compare?expand=1)

Wyświetl plik

@ -0,0 +1,129 @@
# Installation Instructions
## Installation and Use with pip
Copy the link of the tgz from latest relaese and install it with pip install. e.g.:
pip3 install https://github.com/NanoVNA-Saver/nanovna-saver/archive/refs/tags/v0.5.5.tar.gz
Once completed run with the following command: `NanoVNASaver`
The instructions omit the easiest way to get the program running under Linux - no installation - just start it in the git directory. This makes it difficult for pure users, e.g. hams, who therefore even try to run the Windows exe version under Wine.
Proposal - Add these sections below to the top README.md, e.g. between "Detailed installation instructions" and "Using the software" (Please review and add e.g. more necessary debian packages):
## Running on Linux without installation
The program simply works from the source directory without having to install it.
Simple step-by-step instruction, open a terminal window and type:
sudo apt install git python3-pyqt5 python3-numpy python3-scipy
git clone https://github.com/NanoVNA-Saver/nanovna-saver
cd nanovna-saver
Perhaps your system needs a few additional python modules:
- Run with `python nanovna-saver.py` and look at the response of (e.g. missing modules).
- Install the missing modules, preferably via `sudo apt install ...`
until `nanovna-saver.py` starts up.
Now the program can be used from the `nanovna-saver` directory.
## Installing via DEB for Debian (and Ubuntu)
The installation has the benefit that it allows you to run the program from anywhere, because the
main program is found via the regular `$PATH` and the modules are located in the Python module path.
If you're using a debian based distro you should consider to build your own `*.deb` package.
This has the advantage that NanoVNASaver can be installed and uninstalled cleanly in the system.
For this you need to install `python3-stdeb` - the module for converting Python code and modules into a Debian package:
apt install python3-stdeb
Then you can build the package via:
make deb
This package can be installed the usual way with
sudo dpkg -i nanovnasaver....deb
or
sudo apt install ./nanovnasaver....deb
### Installing via RPM (experimental)
`make rpm` builds an (untested) rpm package that can be installed on your system the usual way.
## Ubuntu 20.04 / 22.04
1. Install python3 and pip
sudo apt install python3 python3-pip
python3 -m venv ~/.venv_nano
. ~/.venv_nano/bin/activate
pip install -U pip
2. Clone repo and cd into the directory
git clone https://github.com/NanoVNA-Saver/nanovna-saver
cd nanovna-saver
3. Update pip and run the pip installation
python3 -m pip install .
(You may need to install the additional packages python3-distutils,
python3-setuptools and python3-wheel for this command to work on some
distributions.)
4. Once completed run with the following command
. ~/.venv_nano/bin/activate
python3 nanovna-saver.py
## MacPorts
Via a MacPorts distribution maintained by @ra1nb0w.
1. Install MacPorts following the [install guide](https://www.macports.org/install.php)
2. Install NanoVNASaver :
sudo port install NanoVNASaver
3. Now you can run the software from shell `NanoVNASaver` or run as app
`/Applications/MacPorts/NanoVNASaver.app`
## Homebrew
1. Install Homebrew from <https://brew.sh/> (This will ask for your password)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
2. Python :
brew install python
3. Pip :<br/>
Download the get-pip.py file and run it to install pip
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python3 get-pip.py
4. NanoVNASaver Installation : <br/>
clone the source code to the nanovna-saver folder
git clone https://github.com/NanoVNA-Saver/nanovna-saver
cd nanovna-saver
5. Install local pip packages
python3 -m pip install .
6. Run nanovna-saver in the nanovna-saver folder by:
python3 nanovna-saver.py

29
docs/Makefile 100644
Wyświetl plik

@ -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)

1
docs/_static/.gitignore vendored 100644
Wyświetl plik

@ -0,0 +1 @@
# Empty directory

2
docs/authors.rst 100644
Wyświetl plik

@ -0,0 +1,2 @@
.. _authors:
.. include:: ../AUTHORS.rst

286
docs/conf.py 100644
Wyświetl plik

@ -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 dont 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)

Wyświetl plik

@ -0,0 +1 @@
.. include:: ../CONTRIBUTING.rst

60
docs/index.rst 100644
Wyświetl plik

@ -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

7
docs/license.rst 100644
Wyświetl plik

@ -0,0 +1,7 @@
.. _license:
=======
License
=======
.. include:: ../LICENSE.txt

Wyświetl plik

@ -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.

2
docs/readme.rst 100644
Wyświetl plik

@ -0,0 +1,2 @@
.. _readme:
.. include:: ../README.rst

Some files were not shown because too many files have changed in this diff Show More