kopia lustrzana https://github.com/NanoVNA-Saver/nanovna-saver
Porównaj commity
710 Commity
v0.2.3.4-d
...
main
Autor | SHA1 | Data |
---|---|---|
Holger Müller | a04d6d9b39 | |
Holger Müller | 00dd59ffc6 | |
Holger Müller | d3216d2ddb | |
t52ta6ek | 96dd23211a | |
Holger Müller | 2f8c5346eb | |
t52ta6ek | 4257ac152a | |
Holger Müller | 21e85bdb49 | |
Holger Müller | b4800102d8 | |
Name | abb80a5160 | |
t52ta6ek | 5bed1bc6cc | |
t52ta6ek | 20c1e4ec7c | |
Name | 21ba0ef665 | |
t52ta6ek | eff83097f8 | |
Name | dbea311a02 | |
t52ta6ek | a4a923a649 | |
Martin | ce0c7dd226 | |
Martin | 546d3b188a | |
Martin | 1f233819d2 | |
Martin | a8ffbc3aee | |
Holger Müller | ce8a59d478 | |
Crispin Tschirky | aab2a15f69 | |
Sascha Silbe | 9b4575e307 | |
Henk Vergonet | 8f86722c1e | |
Henk Vergonet | d09b55e1ae | |
Name | 6eb24f2315 | |
Name | d89c9f9d94 | |
Name | f34f3d1f67 | |
Name | 1cd5c052db | |
Holger Müller | 52cdac4f52 | |
Holger Müller | fafe0b2536 | |
Holger Müller | c5e00666aa | |
Holger Müller | 8dec23296e | |
Holger Müller | d1592ac1a3 | |
Holger Müller | 4e06fc53cf | |
Holger Müller | 45c2338196 | |
Holger Müller | 2bab4d4b0d | |
Holger Müller | b3a9f6d8cb | |
Holger Müller | 3c752a9731 | |
Holger Müller | b2c2598d3c | |
dependabot[bot] | c18a6c226f | |
Holger Müller | dd2f5b8a5d | |
Holger Müller | b322d3dc09 | |
Holger Müller | 5b21315a11 | |
Holger Müller | 9ace7d8cd4 | |
Holger Müller | b768a8e01b | |
Holger Müller | 2c58b2ba8f | |
MarcFontaine | a45baea9e2 | |
Holger Müller | db5cd98e03 | |
Holger Müller | 74792b3192 | |
Holger Müller | 50b540a832 | |
Holger Müller | 094b0185e7 | |
Holger Müller | b0110002ec | |
Holger Müller | c0e177bf1a | |
Holger Müller | 185a64b5ae | |
Holger Müller | f7d72d4320 | |
Holger Müller | 82e582b9c0 | |
Holger Müller | 59e7e1809a | |
Holger Müller | 0b82754350 | |
Holger Müller | 6f6f6c65e1 | |
Holger Müller | 92a8a0e39d | |
Holger Müller | b47e665575 | |
Holger Müller | 7f920249b1 | |
Holger Müller | 5860b04ce6 | |
Holger Müller | 9231737b70 | |
Holger Müller | 29518eef00 | |
Holger Müller | 8e9976a540 | |
Holger Müller | 0fbb301435 | |
Holger Müller | 93ee51d236 | |
Holger Müller | 925cf6d4e1 | |
Holger Müller | 09246b6a34 | |
Roel Jordans | dc8874c1c9 | |
Roel Jordans | c4623ddd90 | |
Roel Jordans | 02371bc56b | |
Roel Jordans | 0ffe0eaf72 | |
Roel Jordans | ee3467e5ec | |
Roel Jordans | 3d3e31e176 | |
Martin | f377c999fa | |
Holger Müller | 4cebe94b87 | |
Holger Müller | e4bd720160 | |
Martin | a437029fcd | |
Holger Müller | d7867b7535 | |
Holger Müller | 09d8b2b866 | |
Roel Jordans | 69f5089c1f | |
Roel Jordans | 044c1c885e | |
Roel Jordans | 0c3f179303 | |
Roel Jordans | 9b199b53a9 | |
Roel Jordans | dc44d33786 | |
Roel Jordans | 3265d0368b | |
Holger Müller | a9d0e02e4d | |
ikatkov | 2c868d818f | |
Martin | f996ee9ceb | |
Holger Müller | d313911840 | |
Holger Müller | 7c86009b3e | |
Martin | c536de6dc8 | |
Holger Müller | d6b2f8119b | |
Holger Müller | d654ea0441 | |
Jaroslav Škarvada | 4d21d6dfdc | |
Holger Müller | ed362a0c4b | |
Holger Müller | 2a9a4101f0 | |
Holger Müller | 74d3ac7d07 | |
Holger Müller | f6e1868a95 | |
Holger Müller | fa03e7d753 | |
Holger Müller | fb50f4a01b | |
Attilio Panniello | 9c5b1e01ea | |
Holger Müller | 4d94bbec92 | |
Holger Müller | 1951388c71 | |
Martin | ad14650fc5 | |
Holger Müller | 10d786e787 | |
Holger Müller | 239edc1cd0 | |
Martin | 0485e2c8c2 | |
Martin | c5bee7f3e3 | |
Holger Müller | 533a543a1b | |
Holger Müller | c5a23fcd46 | |
Holger Müller | d57ae78efa | |
Holger Müller | 7b9dd5ab0a | |
Holger Müller | 114b815c72 | |
Holger Müller | 35686319cd | |
Holger Müller | 8e6ab89189 | |
Holger Müller | 62b5c5a1b2 | |
Holger Müller | 193711dc6a | |
Holger Müller | 0f19d5aa3c | |
Holger Müller | 8f224e0e37 | |
Holger Müller | 400ed54f9a | |
Holger Müller | ec23d1b3c8 | |
Holger Müller | 8f016399bb | |
Holger Müller | d33924511d | |
Holger Müller | 44e38515bc | |
Holger Müller | 0b1b73cfc1 | |
Holger Müller | d1ea20f989 | |
Holger Müller | 01eb028f9f | |
Holger Müller | 24a4ca0ffa | |
Holger Müller | a73028e2c3 | |
Holger Müller | a732aea84b | |
Holger Müller | a6c3ccc0d3 | |
Holger Müller | 8e73456668 | |
Holger Müller | 879d5ddea3 | |
Holger Müller | 6630568ed9 | |
Holger Müller | d163143356 | |
Holger Müller | 79a577ffe3 | |
Holger Müller | cabb8a4351 | |
Holger Müller | ef6a3c2d0a | |
Holger Müller | 1609295bd9 | |
Holger Müller | 3f8151aad7 | |
Holger Müller | 36bff6a09d | |
Holger Müller | 8bc452d48f | |
Holger Müller | d86cbec7c4 | |
Holger Müller | 92af43ae22 | |
Frank Kunz | c792c1bd69 | |
Holger Müller | 4159c70558 | |
Holger Müller | 2f69f5c154 | |
Holger Müller | 05f7b9bbf0 | |
Holger Müller | f0e51639b9 | |
Holger Müller | a8ccbce40f | |
Holger Müller | 51d0e318cd | |
Holger Müller | 7690f39c19 | |
mss | 06ffd48de0 | |
Holger Müller | 37007a650d | |
Holger Müller | 08635b0d4b | |
Holger Müller | 7dc3290fdd | |
Holger Müller | 75d2d1e11e | |
Holger Müller | 4da5ea3678 | |
Holger Müller | b2b64ab9cd | |
Holger Müller | b3f816e5f5 | |
Holger Müller | b4a55c6179 | |
Holger Müller | 8814e4f471 | |
Holger Müller | c14ca3c350 | |
Holger Müller | 611d00c551 | |
Holger Müller | f1e2041fbe | |
Holger Müller | 7baa870e75 | |
Oscilllator | ad1cfb5787 | |
Holger Müller | bd4eeb5e63 | |
Patrick Coleman | 32f88eba31 | |
Holger Müller | e452c055d5 | |
Holger Mueller | 32711ec6f7 | |
Holger Müller | 16f028e5ed | |
Holger Mueller | cc3795af51 | |
Holger Müller | f5afb78970 | |
Holger Mueller | 2821f4d08c | |
Holger Mueller | 50ded6dcf3 | |
Holger Müller | 01c83cd2f0 | |
Holger Müller | 7b9d803b35 | |
Holger Mueller | 00d9884d32 | |
Holger Müller | ee3048d985 | |
Holger Müller | 3f4a262abe | |
Holger Müller | 6aa7aaa051 | |
Holger Müller | 140ce4906c | |
Holger Müller | 06cd2de0a6 | |
Mauro Gaioni | f933027af5 | |
Mauro Gaioni | 134affab04 | |
Mauro Gaioni | a457d9c688 | |
Holger Müller | cd3d2b6c2c | |
Holger Müller | 582b442910 | |
Holger Müller | 6c82ff6ee3 | |
Holger Müller | f68d3c2fb7 | |
Holger Müller | 69cc2dcfb4 | |
Holger Müller | 02de5a1650 | |
Holger Müller | 23db45d6d7 | |
Dan Halbert | ed48c85e8e | |
Holger Müller | 747184e85f | |
Holger Müller | 630d6fafc3 | |
Holger Müller | d0dad2a746 | |
Holger Müller | 4d4ff52c15 | |
Sascha Silbe | ca97287fc4 | |
Holger Müller | 982dbe26ab | |
Holger Müller | d596ba7661 | |
Holger Müller | 8432dcfbd3 | |
Holger Müller | 884207d910 | |
Holger Müller | f613ee1a5a | |
Holger Müller | 1c8477f1a9 | |
Holger Müller | cb3122d632 | |
Holger Müller | 83d011122c | |
Holger Müller | 55d86acec1 | |
Holger Müller | e6d9b47f83 | |
Galileo | 55b7c4e42c | |
Galileo | 915da14ac1 | |
Randmental | 209a2e326b | |
Holger Müller | 0c179388d3 | |
Holger Müller | 3e78b490e9 | |
Holger Müller | 8208563ff3 | |
Holger Müller | c9ccaffa41 | |
Holger Müller | 28fd7e5478 | |
Mauro Gaioni | d09ab02201 | |
Holger Müller | cbcf61afb5 | |
Holger Müller | 4a620a5686 | |
Holger Müller | 3b35219d75 | |
Holger Müller | 700781288b | |
Holger Müller | fdb8f0ac43 | |
Holger Müller | 8cc635ffa3 | |
Holger Müller | c194a32eac | |
DiSlord | d03982af73 | |
Roel Jordans | 4bee354bf7 | |
Holger Müller | 43fd3b7d88 | |
Holger Müller | a3c9dea92f | |
Holger Müller | 7f161478d4 | |
Holger Müller | 1c9eeb6db3 | |
Holger Müller | 1c28b721ba | |
Holger Müller | 4b705d0d88 | |
Roel Jordans | 2e2ef886e9 | |
Holger Müller | cced02ebff | |
Holger Müller | 0cee90b49e | |
Holger Müller | f8dbb34f5b | |
Holger Müller | 8aa3b8af51 | |
Holger Müller | 404329570a | |
Holger Müller | 271549db9c | |
Holger Müller | 3131893f08 | |
Roel Jordans | 01c58b82ca | |
Roel Jordans | 8bece254a6 | |
Roel Jordans | 9ea8b7da84 | |
Kevin Zembower | f4fa649956 | |
Kevin Zembower | eaf64cd575 | |
Kevin Zembower | 6a5b662b05 | |
Holger Müller | ebfa3a99ad | |
Holger Müller | e112f25561 | |
Holger Müller | e3c1bbae84 | |
Holger Müller | f921914dd8 | |
Holger Müller | ca5a001356 | |
Holger Müller | fbd9ef731f | |
James Limbouris | 9aee9973ad | |
Kevin Zembower | c4f583ddb8 | |
Holger Müller | 03cc490a66 | |
Holger Müller | 82d825d299 | |
Holger Müller | 4ca66532a4 | |
Holger Müller | 8fa67dc679 | |
Holger Müller | d20137c2d5 | |
Holger Müller | a8144d458d | |
Holger Müller | 8d899e510b | |
Holger Müller | 371a1a16ed | |
Holger Müller | 5e46722955 | |
Holger Müller | ecead18ff5 | |
Holger Müller | 44e51fd4d6 | |
Holger Müller | 09f5ac3a93 | |
Holger Müller | ea06670f25 | |
Holger Müller | 00293fe204 | |
Holger Müller | 2d96aaa2f3 | |
Holger Müller | c60de7d836 | |
Holger Müller | 1ef0d4dfb9 | |
Holger Müller | 4882f29406 | |
Holger Müller | f9222a0ab5 | |
Holger Müller | f95f223656 | |
Holger Müller | 08c4a6dc2a | |
Holger Müller | f6881b0f0d | |
Holger Müller | dd3577509e | |
Holger Müller | bcc598b15a | |
Holger Müller | feedf672e0 | |
Holger Müller | 12180f342f | |
Holger Müller | ac1f06fd0c | |
Martin | 264f8d16ca | |
Martin | 0c8d636632 | |
Holger Müller | 6f6255bf05 | |
Holger Müller | 52d0068571 | |
Holger Müller | e34c89aded | |
Martin | 6f1db232b5 | |
Martin | 4429064aa4 | |
Holger Müller | 827be56a08 | |
Holger Müller | 43fef9816f | |
Holger Müller | e2460ff9e0 | |
Holger Müller | b86ddb95aa | |
Holger Müller | 3dfd9a5a4b | |
Holger Müller | aca61a6e04 | |
Holger Müller | 5b1666b7e9 | |
Holger Müller | 14858ab77d | |
Holger Müller | 33416ca684 | |
Holger Müller | f25057973e | |
Holger Müller | f21c102115 | |
Holger Müller | e3cba167da | |
Holger Müller | 4e7e9298e6 | |
Holger Müller | 80bb4a0936 | |
Holger Müller | 20b5334f3a | |
Holger Müller | 534b71a222 | |
Holger Müller | 7698d67fd2 | |
Holger Müller | 3794a86c12 | |
Holger Müller | ae88b7ca4d | |
Holger Müller | 6315bd06d6 | |
Holger Müller | 398404dcf9 | |
Holger Müller | 8763948697 | |
Holger Müller | 80418b5739 | |
Holger Müller | 91baa22a14 | |
Holger Müller | faf983c196 | |
Holger Müller | 4b90ef8498 | |
Holger Müller | c373fc6582 | |
Holger Müller | c4bb73d277 | |
Holger Müller | c6cd52afb2 | |
Holger Müller | 17b2742e2b | |
Holger Müller | dd81aa875b | |
Holger Müller | 5d2de92709 | |
Martin | e156253025 | |
Holger Müller | aeed3744ba | |
Holger Müller | 8da0d2bfcf | |
Holger Müller | da3bb3a0b8 | |
Holger Müller | 74a8bf41a8 | |
Holger Müller | 6cac48d536 | |
Holger Müller | 7a310e5139 | |
Holger Müller | ead5c32cc5 | |
Holger Müller | b953b8ea7d | |
Holger Müller | 9aee70a7f2 | |
Holger Müller | d71f5145c4 | |
Holger Müller | fc9b0b48d1 | |
Holger Müller | 9cdfe8d620 | |
Holger Müller | 9eea006663 | |
Holger Müller | b0a21bc164 | |
Ishmael Samuel | 81cfbc2137 | |
Holger Müller | e413baa482 | |
Holger Müller | 1eb3cda9ad | |
Holger Müller | 65d5d8b8b7 | |
Holger Müller | 0887e22477 | |
Holger Müller | 9d1ea35448 | |
Holger Müller | 0c352eed64 | |
Holger Müller | 28c62b707a | |
Holger Müller | 6be2730785 | |
Holger Müller | c821fb209d | |
Holger Müller | 252d0d2869 | |
Daniel Lingvay | b3068543a9 | |
Roel Jordans | 9e85730f14 | |
Sascha Silbe | a4f29565c4 | |
Sascha Silbe | dd8e86555c | |
Mauro Gaioni | 3200c1b445 | |
Mauro Gaioni | 503af5ff84 | |
Mauro Gaioni | 93e704330d | |
Mauro Gaioni | 9bfbaba0da | |
Mauro Gaioni | 7eadf8ba3f | |
Mauro Gaioni | 2ddde80dfc | |
Holger Müller | ba63ef5fc5 | |
Roel Jordans | dbe735a677 | |
Roel Jordans | 60ea55b6cf | |
Roel Jordans | 9c7f6a80f7 | |
Holger Müller | 34673a1548 | |
Holger Müller | 4204bad346 | |
Holger Müller | 911fb232bf | |
Roel Jordans | c6878fce8f | |
Galileo | 5fa16c65a3 | |
Holger Müller | 79ef110c82 | |
Holger Müller | 320a61f838 | |
Holger Müller | 90e4dc9332 | |
Holger Müller | 45294a1048 | |
Mauro Gaioni | 8ab2709bf2 | |
Mauro Gaioni | 69db5f14d7 | |
Mauro Gaioni | 654252e5bf | |
Mauro Gaioni | ec08d068c2 | |
Mauro Gaioni | 06912dac55 | |
Mauro Gaioni | 9c1031fa56 | |
Mauro Gaioni | 391b8881a8 | |
Mauro Gaioni | 2f2c0d621a | |
Mauro Gaioni | 922f24357f | |
Mauro Gaioni | 7feff63c5e | |
Mauro Gaioni | db284fd57a | |
Mauro Gaioni | e95fea4ccc | |
Mauro Gaioni | d06bb32082 | |
Mauro Gaioni | 2052718822 | |
Holger Müller | 97c7c8f3f2 | |
Holger Müller | 1151023176 | |
Holger Müller | ca5c7b0a07 | |
Holger Müller | b096bb80ba | |
Holger Müller | 3ef4f7309f | |
Holger Müller | 4a02c32bde | |
Holger Müller | e2ea2273ab | |
Holger Müller | 70c448af9d | |
Holger Müller | 1608a9261f | |
Holger Müller | fc18a94faf | |
Holger Müller | d379777b95 | |
Holger Müller | e4dce91db9 | |
Holger Müller | a24656dcaa | |
Holger Müller | 537dc7a33f | |
Holger Müller | 7fcc3c20b3 | |
Holger Müller | d54627f993 | |
Holger Müller | faf2686261 | |
Holger Müller | 9cb7175cf8 | |
Holger Müller | 7b8c40118a | |
Holger Müller | cd1f32e9dc | |
Holger Müller | ed8319a66c | |
Holger Müller | c4c2f2c6a8 | |
Holger Müller | b75209884b | |
Holger Müller | d1088dd931 | |
sysjoint-tek | 2880ba187d | |
sysjoint-tek | 767ccd0033 | |
sysjoint-tek | eb87006927 | |
sysjoint-tek | 5705842d43 | |
sysjoint-tek | de7d047ba2 | |
sysjoint-tek | ebe517a0a9 | |
sysjoint-tek | 418b0d05d8 | |
sysjoint-tek | c637522dc4 | |
sysjoint-tek | 610c86435f | |
sysjoint-tek | c17c0a7b96 | |
Holger Müller | 467d08c87b | |
Holger Müller | ec2ff37f74 | |
Holger Müller | d8ad21dc54 | |
Holger Müller | 13bfe102b7 | |
Mark Zachmann | 244fc41308 | |
Holger Müller | 6f0f55ab79 | |
Holger Müller | 6b554dca10 | |
Holger Müller | b38e4412b2 | |
Holger Müller | 3c801429d9 | |
Holger Müller | 1e884f7034 | |
Mark Zachmann | 15c1eec357 | |
Holger Müller | af823597d9 | |
Holger Müller | 05b71ee8e8 | |
Holger Müller | ba49f3ae31 | |
bicycleGuy | d36ff5d3b0 | |
Mike4U | dc0467ed78 | |
bicycleGuy | 7b17d714ea | |
bicycleGuy | 38108c1e8e | |
Holger Müller | 7c5e493be7 | |
Holger Müller | a7da9c1bfa | |
Holger Müller | 28ea171441 | |
Holger Müller | 21733f9234 | |
Holger Müller | 0d49b7e977 | |
Holger Müller | 42fb34fb4b | |
Holger Müller | a0b4762d6a | |
Holger Müller | 56facd3ea9 | |
Holger Müller | 87d73f8842 | |
Holger Müller | d8882c74bf | |
Holger Müller | 323db84ef6 | |
Holger Müller | 9f5f4b5870 | |
Holger Müller | ecd74d54db | |
Holger Müller | a713959245 | |
Holger Müller | 7205aba13d | |
Holger Müller | 2176919ac0 | |
Holger Müller | 0559540e30 | |
Holger Müller | 93678a0293 | |
Holger Müller | 80f5cadd22 | |
Holger Müller | c981069a16 | |
Holger Müller | c4eea708e2 | |
Holger Müller | 68eeabc4f1 | |
Holger Müller | 569fcdb2e6 | |
Holger Müller | 89c809cf33 | |
Holger Müller | f30f4f02e5 | |
Holger Müller | e93d17dce3 | |
Holger Müller | b178301435 | |
Holger Müller | 06b379afb4 | |
Holger Müller | fd6aad74cf | |
Holger Müller | ec643ebefc | |
Holger Müller | 0f5a24ed49 | |
Holger Müller | d79524bb12 | |
Holger Müller | 626b474bb6 | |
Holger Müller | 4f5ad6eeca | |
Holger Müller | 54326fb86d | |
Holger Müller | 28b424125a | |
Holger Müller | 0c217541e0 | |
Holger Müller | 6d74ae52a3 | |
Holger Müller | 3bfd99ad3d | |
Holger Müller | e6d3ea0c12 | |
Holger Müller | 684a01beb4 | |
Holger Müller | bf8d5a4544 | |
Holger Müller | de7ec1be7d | |
Holger Müller | ae75966bb3 | |
Holger Müller | 9a53c9155b | |
Holger Müller | 87d8fb504d | |
Holger Müller | 81751b3733 | |
Holger Müller | 2a66d00927 | |
Holger Müller | ea1de63c52 | |
Holger Müller | 29b8ed6760 | |
Holger Müller | a8065c1118 | |
Holger Müller | 3d665a4724 | |
Holger Müller | 6aa5fce09e | |
Holger Müller | a1c194d03c | |
Holger Müller | 6c4fbe6c8d | |
Holger Müller | e48870e13b | |
Holger Müller | cadf499a6f | |
Holger Müller | 8ca1606d5f | |
Holger Müller | b03ba0adf0 | |
Holger Müller | f05a2c40a5 | |
Holger Müller | 0ba80d092e | |
Holger Müller | 37c56a9692 | |
Holger Müller | 32004a8f99 | |
Holger Müller | ec5c0b5743 | |
Peter B Marks | b6b763cda1 | |
Holger Müller | 3aa968ed47 | |
Holger Müller | c05268a5b4 | |
Holger Müller | 8b6ae07ea5 | |
Holger Müller | 3578210a81 | |
Holger Müller | 30f7b868db | |
Holger Müller | ce717bad58 | |
Holger Müller | 8cc6431bc0 | |
Holger Müller | 635c692e93 | |
Holger Müller | 0c59600f4a | |
Holger Müller | 69d88139ed | |
Holger Müller | 66b07690ff | |
Holger Müller | 6052b09687 | |
Holger Müller | 74a99b758e | |
Holger Müller | 550b0a2937 | |
Holger Müller | 1a6dbe0d1f | |
Holger Müller | 3505552dfa | |
Holger Müller | 820ec3f82f | |
Holger Müller | 5f4fa53223 | |
Holger Müller | 202f1eaf10 | |
Holger Müller | b7dc4a66af | |
Holger Müller | 695485bbff | |
Holger Müller | 85e1374d0d | |
Holger Müller | 44cc56a61f | |
Holger Müller | 4d4c65a230 | |
Mike4U | e059136b02 | |
Holger Müller | 6a195fa90f | |
Holger Müller | 97f5083130 | |
Holger Müller | d287f6cddf | |
Holger Müller | 675512d1b7 | |
Holger Müller | 9d6b9485ca | |
Holger Müller | 0fb7cd6768 | |
Holger Müller | 785ef32e76 | |
Holger Müller | 4ef83a44f9 | |
Holger Müller | 8ac23b0872 | |
Holger Müller | 098bd26346 | |
Holger Müller | 23a7d5b553 | |
Holger Müller | 947e4f26cc | |
Holger Müller | 72aef86d4c | |
Holger Müller | b4ba24d42d | |
Holger Müller | 19493012b2 | |
Holger Müller | f3c7e71c1a | |
Holger Müller | a5a6287a4d | |
Holger Müller | 1a78c66a4c | |
Holger Müller | 6d38474f49 | |
Holger Müller | d8ac822963 | |
Holger Müller | fa4edfff56 | |
Holger Müller | 98ab43aead | |
Holger Müller | 22c4a320e6 | |
Holger Müller | 9d11730798 | |
Holger Müller | 099e1b2087 | |
Holger Müller | 66345c29d0 | |
Holger Müller | d7ec8dbf88 | |
Holger Müller | 5c684e9886 | |
Holger Müller | a174c72f8a | |
Holger Müller | 9a73e8ba81 | |
Holger Müller | 980fcf30ae | |
Holger Müller | 2244f57779 | |
Holger Müller | b28b010903 | |
Holger Müller | bf18d88bf7 | |
Holger Müller | 11082ff062 | |
Holger Müller | b1980b3bfd | |
Holger Müller | 8d8d350068 | |
Holger Müller | 1a88ef4791 | |
Holger Müller | 95a3f72a98 | |
Holger Müller | bef52233e1 | |
Holger Müller | 50b5c9e105 | |
Holger Müller | 58b41d7bba | |
Holger Müller | 717de3fb6d | |
Holger Müller | 8f95590669 | |
Holger Müller | d4621aad13 | |
Holger Müller | 8a8436dfc1 | |
Holger Müller | 0920761e9f | |
Holger Müller | e69349814b | |
Holger Müller | 9ba5f48a96 | |
Holger Müller | 430a970563 | |
Holger Müller | b6f36dc4a8 | |
Holger Müller | 8bb5a9821b | |
Holger Müller | 8fbe360778 | |
Holger Müller | b8b68d46a3 | |
Holger Müller | b365a6cbcd | |
Holger Müller | a686a20fad | |
Holger Müller | 51af621433 | |
Holger Müller | a6c3a20445 | |
Mauro Gaioni | f67e00ca2d | |
Mauro Gaioni | 4930519fd8 | |
Holger Müller | 5b71657e17 | |
Holger Müller | 675e7adc2c | |
Holger Müller | 6fa8929f94 | |
Holger Müller | 55dd5eda4f | |
Holger Müller | 28fb2e280f | |
Holger Müller | 27bd5ca2b6 | |
Holger Müller | 27d3f492d8 | |
Holger Müller | 948c04f154 | |
Holger Müller | 496494900f | |
Holger Müller | 71ba759ada | |
Holger Müller | 113be4d1db | |
Holger Müller | cd9b4da1af | |
Holger Müller | 764a9aaa0b | |
Holger Müller | ac050a0a4c | |
Holger Müller | d039a1192a | |
Holger Müller | 887a74d040 | |
Holger Müller | 3d57461a71 | |
Holger Müller | 399c893f4f | |
Holger Müller | d3fe370d80 | |
Holger Müller | 089455505b | |
Holger Müller | 0b8641c20a | |
Holger Müller | 8b6ce5e142 | |
Holger Müller | 5e32ff177b | |
RandMental | 22b5116573 | |
RandMental | 428aafa6ce | |
Holger Müller | b8b2a78ab4 | |
Holger Müller | 67334257a1 | |
Holger Müller | 408eda63cf | |
Holger Müller | a06b9191be | |
Holger Müller | 3dbbd165ac | |
Holger Müller | 88245d640d | |
RandMental | 64a7fd914d | |
Holger Müller | 8e7e6b81ac | |
Holger Müller | 935b58648e | |
Holger Müller | e743a4a5a3 | |
Holger Müller | 405f48166d | |
Holger Müller | 9ec71ebc8d | |
Holger Müller | da3bcbba6b | |
Holger Müller | 6941d237c4 | |
Holger Müller | 143e1dda02 | |
Holger Müller | 09a96ef45d | |
Mark Zachmann | 94421b80a8 | |
Holger Müller | 2252d78de4 | |
Holger Müller | 525cb75b72 | |
RandMental | 99375693c8 | |
RandMental | 4935b4a7ae | |
Holger Müller | 354a3d764e | |
Holger Müller | 7cb71c0ab0 | |
Mauro Gaioni | cec57bacb5 | |
Holger Müller | 2e62d13f0f | |
Holger Müller | 24b34d6f70 | |
Holger Müller | eae377bbe2 | |
Holger Müller | 1f193d4b00 | |
Holger Müller | 3d17260220 | |
Holger Müller | da729a3bab | |
Mauro Gaioni | 25df257462 | |
Holger Müller | 22be293ff2 | |
Holger Müller | 4819b25ac1 | |
Holger Müller | 61f3c652ac | |
Holger Müller | da5e5ba733 | |
Holger Müller | fa50984822 | |
Holger Müller | fa19aaab37 | |
Holger Müller | 907f417a3e | |
Holger Müller | 3341564cb2 | |
Holger Müller | 4045a18271 | |
Holger Müller | 30032a08fe | |
Holger Müller | 40f9e25944 | |
Holger Müller | 374924299d | |
Holger Müller | e70aa7d3e1 | |
Holger Müller | 945b28ffac | |
Holger Müller | 35353a34de | |
Holger Müller | 75d77c1703 | |
Holger Müller | 48f8aaea32 | |
Holger Müller | fbdf325b51 | |
Holger Müller | c0e1cfb310 | |
Holger Müller | 5dd8c1d20d | |
Holger Müller | 9cea9d05a1 | |
Holger Müller | 148e9a13d4 | |
Holger Müller | 398b1ac882 | |
Holger Müller | 6cb7b33aa3 | |
Holger Müller | 726dcf6436 | |
Holger Müller | ee4d3b6765 | |
Holger Müller | 790c8aac2b | |
Holger Müller | c139a531e7 | |
Holger Müller | 3742d24364 | |
Holger Müller | 9821fcb850 | |
Holger Müller | f347b12538 | |
Holger Müller | 4c04521d79 | |
Holger Müller | f05b7636d0 | |
Holger Müller | b33034acff | |
Holger Müller | 11e5d73559 | |
Holger Müller | 76420abd57 | |
Holger Müller | 3dca605297 | |
Holger Müller | c6da3c3364 | |
Randmental | 974610bdc3 | |
Holger Müller | 0a2bf51c63 | |
Holger Müller | c77d19780b | |
Holger Müller | 4a8195e189 | |
Holger Müller | 73f968a47b | |
Holger Müller | 298b5134bf | |
Holger Müller | 88c7ee35a7 | |
Holger Müller | 8bdf5ecb77 | |
Holger Müller | 718e894efc | |
Holger Müller | bf97feff1a | |
Holger Müller | f4bd371e51 | |
Holger Müller | 20a017b1ed | |
Holger Müller | ca146a2e3a | |
Holger Müller | 9e629f0350 | |
Holger Müller | d6acb7121c | |
Holger Müller | 5ec8c1e1c0 | |
Holger Müller | 3c77a86b58 | |
Holger Müller | cb568cf6ad | |
Holger Müller | 658cc6f231 | |
Holger Müller | aa9f1accc9 | |
Holger Müller | 5ce5de163c | |
Ohan Smit | 384818db27 | |
Holger Müller | 58c83fa26b | |
bicycleGuy | e113da4729 | |
Davide Gerhard | e9221af112 | |
bicycleGuy | 8c7506daec |
23
.coveragerc
23
.coveragerc
|
@ -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
|
||||
|
|
|
@ -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 +1,2 @@
|
|||
* @mihtjel
|
||||
* @zarath
|
||||
|
|
|
@ -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. -->
|
|
@ -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
|
|
@ -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: ""
|
||||
---
|
|
@ -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.
|
||||
|
|
|
@ -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. -->
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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!
|
|
@ -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
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
# Default ignored files
|
||||
/workspace.xml
|
|
@ -1,6 +0,0 @@
|
|||
<component name="CopyrightManager">
|
||||
<copyright>
|
||||
<option name="notice" value="Copyright 2019 Rune B. Broberg 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>
|
|
@ -1,6 +0,0 @@
|
|||
<component name="CopyrightManager">
|
||||
<copyright>
|
||||
<option name="notice" value="Copyright (c) &#36;year Rune B. Broberg" />
|
||||
<option name="myName" value="Copyright" />
|
||||
</copyright>
|
||||
</component>
|
|
@ -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 Copyright (C) &#36;today.year. 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/>. " />
|
||||
<option name="myName" value="GPL v3" />
|
||||
</copyright>
|
||||
</component>
|
|
@ -1,7 +0,0 @@
|
|||
<component name="CopyrightManager">
|
||||
<settings>
|
||||
<module2copyright>
|
||||
<element module="Project Files" copyright="GPL v3" />
|
||||
</module2copyright>
|
||||
</settings>
|
||||
</component>
|
|
@ -1,6 +0,0 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# Read the Docs configuration file
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
# Build documentation in the docs/ directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
# Build documentation with MkDocs
|
||||
#mkdocs:
|
||||
# configuration: mkdocs.yml
|
||||
|
||||
# Optionally build your docs in additional formats such as PDF
|
||||
formats:
|
||||
- pdf
|
||||
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
- {path: ., method: pip}
|
|
@ -0,0 +1,43 @@
|
|||
============
|
||||
Contributors
|
||||
============
|
||||
|
||||
* Attilio Panniello <attilio.panniello@gmail.com>
|
||||
* bicycleGuy <michaelrunyan@Michaels-iMac.home>
|
||||
* Carl Tremblay <cinosh07@hotmail.com>
|
||||
* cinosh07 <cinosh07@hotmail.com>
|
||||
* Dan Halbert <halbert@halwitz.org>
|
||||
* Daniel Lingvay <dlingvay@grubhub.com>
|
||||
* Davide Gerhard <rainbow@irh.it>
|
||||
* Denis Bondar <bondar.den@gmail.com>
|
||||
* dhunt1342 <dhunt1342@users.noreply.github.com>
|
||||
* DiSlord <dislord@mail.ru>
|
||||
* Frank Kunz <mailinglists@kunz-im-inter.net>
|
||||
* Galileo <galileo@pkm-inc.com>
|
||||
* Holger Mueller <zarath@gmx.de>
|
||||
* ikatkov <ikatkov@gmail.com>
|
||||
* Ishmael Samuel <ishmaelsamuel79@gmail.com>
|
||||
* James Limbouris <james@digitalmatter.com>
|
||||
* Jaroslav Škarvada <jskarvad@redhat.com>
|
||||
* Kevin Zembower <kevin@zembower.org>
|
||||
* Mark Zachmann <Mark.Zachmann@snug.dog>
|
||||
* Martin <Ho-Ro@users.noreply.github.com>
|
||||
* Mauro Gaioni <m.gaioni@asst-valcamonica.it>
|
||||
* Mauro <mauro@lenny.station>
|
||||
* mihtjel <mihtjel@gmail.com>
|
||||
* Mike4U <9957897+Mike4U@users.noreply.github.com>
|
||||
* mss <marcspeck@gmail.com>
|
||||
* Neil Katin <github2@askneil.com>
|
||||
* Ohan Smit <psynosaur@gmail.com>
|
||||
* Olgierd Pilarczyk <opilarczyk@egnyte.com>
|
||||
* Oscilllator <harry.dudleybestow@gmail.com>
|
||||
* Patrick Coleman <blinken@gmail.com>
|
||||
* Peter B Marks <peter.marks@pobox.com>
|
||||
* Psynosaur <psynosaur@gmail.com>
|
||||
* RandMental <RandMental@users.noreply.github.com>
|
||||
* Roel Jordans <r.jordans@tue.nl>
|
||||
* Rune B. Broberg <mihtjel@gmail.com>
|
||||
* Sascha Silbe <sascha-pgp@silbe.org>
|
||||
* sysjoint-tek <63992872+sysjoint-tek@users.noreply.github.com>
|
||||
* Thomas de Lellis <24543390+t52ta6ek@users.noreply.github.com>
|
||||
* zstadler <zeev.stadler@gmail.com>
|
|
@ -0,0 +1,322 @@
|
|||
============
|
||||
Contributing
|
||||
============
|
||||
|
||||
Welcome to ``nanovna-saver`` contributor's guide.
|
||||
|
||||
This document focuses on getting any potential contributor familiarized
|
||||
with the development processes, but `other kinds of contributions`_ are also
|
||||
appreciated.
|
||||
|
||||
If you are new to using git_ or have never collaborated in a project previously,
|
||||
please have a look at `contribution-guide.org`_. Other resources are also
|
||||
listed in the excellent `guide created by FreeCodeCamp`_ [#contrib1]_.
|
||||
|
||||
Please notice, all users and contributors are expected to be **open,
|
||||
considerate, reasonable, and respectful**. When in doubt, `Python Software
|
||||
Foundation's Code of Conduct`_ is a good reference in terms of behavior
|
||||
guidelines.
|
||||
|
||||
|
||||
Issue Reports
|
||||
=============
|
||||
|
||||
If you experience bugs or general issues with ``nanovna-saver``, please have a look
|
||||
on the `issue tracker`_. If you don't see anything useful there, please feel
|
||||
free to fire an issue report.
|
||||
|
||||
.. tip::
|
||||
Please don't forget to include the closed issues in your search.
|
||||
Sometimes a solution was already reported, and the problem is considered
|
||||
**solved**.
|
||||
|
||||
New issue reports should include information about your programming environment
|
||||
(e.g., operating system, Python version) and steps to reproduce the problem.
|
||||
Please try also to simplify the reproduction steps to a very minimal example
|
||||
that still illustrates the problem you are facing. By removing other factors,
|
||||
you help us to identify the root cause of the issue.
|
||||
|
||||
|
||||
Documentation Improvements
|
||||
==========================
|
||||
|
||||
You can help improve ``nanovna-saver`` docs by making them more readable and coherent, or
|
||||
by adding missing information and correcting mistakes.
|
||||
|
||||
``nanovna-saver`` documentation should use Sphinx_ as its main documentation compiler.
|
||||
This means that the docs are kept in the same repository as the project code, and
|
||||
that any documentation update is done in the same way was a code contribution.
|
||||
|
||||
.. tip::
|
||||
Please notice that the `GitHub web interface`_ provides a quick way of
|
||||
propose changes in ``nanovna-saver``'s files. While this mechanism can
|
||||
be tricky for normal code contributions, it works perfectly fine for
|
||||
contributing to the docs, and can be quite handy.
|
||||
|
||||
If you are interested in trying this method out, please navigate to
|
||||
the ``docs`` folder in the source repository_, find which file you
|
||||
would like to propose changes and click in the little pencil icon at the
|
||||
top, to open `GitHub's code editor`_. Once you finish editing the file,
|
||||
please write a message in the form at the bottom of the page describing
|
||||
which changes have you made and what are the motivations behind them and
|
||||
submit your proposal.
|
||||
|
||||
When working on documentation changes in your local machine, you can
|
||||
compile them using |tox|_::
|
||||
|
||||
tox -e docs
|
||||
|
||||
and use Python's built-in web server for a preview in your web browser
|
||||
(``http://localhost:8000``)::
|
||||
|
||||
python3 -m http.server --directory 'docs/_build/html'
|
||||
|
||||
|
||||
Code Contributions
|
||||
==================
|
||||
|
||||
.. todo:: Please include a reference or explanation about the internals of the project.
|
||||
|
||||
An architecture description, design principles or at least a summary of the
|
||||
main concepts will make it easy for potential contributors to get started
|
||||
quickly.
|
||||
|
||||
Submit an issue
|
||||
---------------
|
||||
|
||||
Before you work on any non-trivial code contribution it's best to first create
|
||||
a report in the `issue tracker`_ to start a discussion on the subject.
|
||||
This often provides additional considerations and avoids unnecessary work.
|
||||
|
||||
Create an environment
|
||||
---------------------
|
||||
|
||||
Before you start coding, we recommend creating an isolated `virtual
|
||||
environment`_ to avoid any problems with your installed Python packages.
|
||||
This can easily be done via either |virtualenv|_::
|
||||
|
||||
virtualenv <PATH TO VENV>
|
||||
source <PATH TO VENV>/bin/activate
|
||||
|
||||
or Miniconda_::
|
||||
|
||||
conda create -n nanovna-saver python=3 six virtualenv pytest pytest-cov
|
||||
conda activate nanovna-saver
|
||||
|
||||
Clone the repository
|
||||
--------------------
|
||||
|
||||
#. Create an user account on |the repository service| if you do not already have one.
|
||||
#. Fork the project repository_: click on the *Fork* button near the top of the
|
||||
page. This creates a copy of the code under your account on |the repository service|.
|
||||
#. Clone this copy to your local disk::
|
||||
|
||||
git clone git@github.com:YourLogin/nanovna-saver.git
|
||||
cd nanovna-saver
|
||||
|
||||
#. You should run::
|
||||
|
||||
pip install -U pip setuptools -e .
|
||||
|
||||
to be able to import the package under development in the Python REPL.
|
||||
|
||||
.. todo:: if you are not using pre-commit, please remove the following item:
|
||||
|
||||
#. Install |pre-commit|_::
|
||||
|
||||
pip install pre-commit
|
||||
pre-commit install
|
||||
|
||||
``nanovna-saver`` comes with a lot of hooks configured to automatically help the
|
||||
developer to check the code being written.
|
||||
|
||||
Implement your changes
|
||||
----------------------
|
||||
|
||||
#. Create a branch to hold your changes::
|
||||
|
||||
git checkout -b my-feature
|
||||
|
||||
and start making changes. Never work on the main branch!
|
||||
|
||||
#. Start your work on this branch. Don't forget to add docstrings_ to new
|
||||
functions, modules and classes, especially if they are part of public APIs.
|
||||
|
||||
#. Add yourself to the list of contributors in ``AUTHORS.rst``.
|
||||
|
||||
#. When you’re done editing, do::
|
||||
|
||||
git add <MODIFIED FILES>
|
||||
git commit
|
||||
|
||||
to record your changes in git_.
|
||||
|
||||
.. todo:: if you are not using pre-commit, please remove the following item:
|
||||
|
||||
Please make sure to see the validation messages from |pre-commit|_ and fix
|
||||
any eventual issues.
|
||||
This should automatically use flake8_/black_ to check/fix the code style
|
||||
in a way that is compatible with the project.
|
||||
|
||||
.. important:: Don't forget to add unit tests and documentation in case your
|
||||
contribution adds an additional feature and is not just a bugfix.
|
||||
|
||||
Moreover, writing a `descriptive commit message`_ is highly recommended.
|
||||
In case of doubt, you can check the commit history with::
|
||||
|
||||
git log --graph --decorate --pretty=oneline --abbrev-commit --all
|
||||
|
||||
to look for recurring communication patterns.
|
||||
|
||||
#. Please check that your changes don't break any unit tests with::
|
||||
|
||||
tox
|
||||
|
||||
(after having installed |tox|_ with ``pip install tox`` or ``pipx``).
|
||||
|
||||
You can also use |tox|_ to run several other pre-configured tasks in the
|
||||
repository. Try ``tox -av`` to see a list of the available checks.
|
||||
|
||||
Submit your contribution
|
||||
------------------------
|
||||
|
||||
#. If everything works fine, push your local branch to |the repository service| with::
|
||||
|
||||
git push -u origin my-feature
|
||||
|
||||
#. Go to the web page of your fork and click |contribute button|
|
||||
to send your changes for review.
|
||||
|
||||
.. todo:: if you are using GitHub, you can uncomment the following paragraph
|
||||
|
||||
Find more detailed information in `creating a PR`_. You might also want to open
|
||||
the PR as a draft first and mark it as ready for review after the feedbacks
|
||||
from the continuous integration (CI) system or any required fixes.
|
||||
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
The following tips can be used when facing problems to build or test the
|
||||
package:
|
||||
|
||||
#. Make sure to fetch all the tags from the upstream repository_.
|
||||
The command ``git describe --abbrev=0 --tags`` should return the version you
|
||||
are expecting. If you are trying to run CI scripts in a fork repository,
|
||||
make sure to push all the tags.
|
||||
You can also try to remove all the egg files or the complete egg folder, i.e.,
|
||||
``.eggs``, as well as the ``*.egg-info`` folders in the ``src`` folder or
|
||||
potentially in the root of your project.
|
||||
|
||||
#. Sometimes |tox|_ misses out when new dependencies are added, especially to
|
||||
``setup.cfg`` and ``docs/requirements.txt``. If you find any problems with
|
||||
missing dependencies when running a command with |tox|_, try to recreate the
|
||||
``tox`` environment using the ``-r`` flag. For example, instead of::
|
||||
|
||||
tox -e docs
|
||||
|
||||
Try running::
|
||||
|
||||
tox -r -e docs
|
||||
|
||||
#. Make sure to have a reliable |tox|_ installation that uses the correct
|
||||
Python version (e.g., 3.7+). When in doubt you can run::
|
||||
|
||||
tox --version
|
||||
# OR
|
||||
which tox
|
||||
|
||||
If you have trouble and are seeing weird errors upon running |tox|_, you can
|
||||
also try to create a dedicated `virtual environment`_ with a |tox|_ binary
|
||||
freshly installed. For example::
|
||||
|
||||
virtualenv .venv
|
||||
source .venv/bin/activate
|
||||
.venv/bin/pip install tox
|
||||
.venv/bin/tox -e all
|
||||
|
||||
#. `Pytest can drop you`_ in an interactive session in the case an error occurs.
|
||||
In order to do that you need to pass a ``--pdb`` option (for example by
|
||||
running ``tox -- -k <NAME OF THE FALLING TEST> --pdb``).
|
||||
You can also setup breakpoints manually instead of using the ``--pdb`` option.
|
||||
|
||||
|
||||
Maintainer tasks
|
||||
================
|
||||
|
||||
Releases
|
||||
--------
|
||||
|
||||
.. todo:: This section assumes you are using PyPI to publicly release your package.
|
||||
|
||||
If instead you are using a different/private package index, please update
|
||||
the instructions accordingly.
|
||||
|
||||
If you are part of the group of maintainers and have correct user permissions
|
||||
on PyPI_, the following steps can be used to release a new version for
|
||||
``nanovna-saver``:
|
||||
|
||||
#. Make sure all unit tests are successful.
|
||||
#. Tag the current commit on the main branch with a release tag, e.g., ``v1.2.3``.
|
||||
#. Push the new tag to the upstream repository_, e.g., ``git push upstream v1.2.3``
|
||||
#. Clean up the ``dist`` and ``build`` folders with ``tox -e clean``
|
||||
(or ``rm -rf dist build``)
|
||||
to avoid confusion with old builds and Sphinx docs.
|
||||
#. Run ``tox -e build`` and check that the files in ``dist`` have
|
||||
the correct version (no ``.dirty`` or git_ hash) according to the git_ tag.
|
||||
Also check the sizes of the distributions, if they are too big (e.g., >
|
||||
500KB), unwanted clutter may have been accidentally included.
|
||||
#. Run ``tox -e publish -- --repository pypi`` and check that everything was
|
||||
uploaded to PyPI_ correctly.
|
||||
|
||||
|
||||
|
||||
.. [#contrib1] Even though, these resources focus on open source projects and
|
||||
communities, the general ideas behind collaborating with other developers
|
||||
to collectively create software are general and can be applied to all sorts
|
||||
of environments, including private companies and proprietary code bases.
|
||||
|
||||
|
||||
.. <-- start -->
|
||||
.. todo:: Please review and change the following definitions:
|
||||
|
||||
.. |the repository service| replace:: GitHub
|
||||
.. |contribute button| replace:: "Create pull request"
|
||||
|
||||
.. _repository: https://github.com/<USERNAME>/nanovna-saver
|
||||
.. _issue tracker: https://github.com/<USERNAME>/nanovna-saver/issues
|
||||
.. <-- end -->
|
||||
|
||||
|
||||
.. |virtualenv| replace:: ``virtualenv``
|
||||
.. |pre-commit| replace:: ``pre-commit``
|
||||
.. |tox| replace:: ``tox``
|
||||
|
||||
|
||||
.. _black: https://pypi.org/project/black/
|
||||
.. _CommonMark: https://commonmark.org/
|
||||
.. _contribution-guide.org: https://www.contribution-guide.org/
|
||||
.. _creating a PR: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request
|
||||
.. _descriptive commit message: https://chris.beams.io/posts/git-commit
|
||||
.. _docstrings: https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html
|
||||
.. _first-contributions tutorial: https://github.com/firstcontributions/first-contributions
|
||||
.. _flake8: https://flake8.pycqa.org/en/stable/
|
||||
.. _git: https://git-scm.com
|
||||
.. _GitHub's fork and pull request workflow: https://guides.github.com/activities/forking/
|
||||
.. _guide created by FreeCodeCamp: https://github.com/FreeCodeCamp/how-to-contribute-to-open-source
|
||||
.. _Miniconda: https://docs.conda.io/en/latest/miniconda.html
|
||||
.. _MyST: https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html
|
||||
.. _other kinds of contributions: https://opensource.guide/how-to-contribute
|
||||
.. _pre-commit: https://pre-commit.com/
|
||||
.. _PyPI: https://pypi.org/
|
||||
.. _PyScaffold's contributor's guide: https://pyscaffold.org/en/stable/contributing.html
|
||||
.. _Pytest can drop you: https://docs.pytest.org/en/stable/how-to/failures.html#using-python-library-pdb-with-pytest
|
||||
.. _Python Software Foundation's Code of Conduct: https://www.python.org/psf/conduct/
|
||||
.. _reStructuredText: https://www.sphinx-doc.org/en/master/usage/restructuredtext/
|
||||
.. _Sphinx: https://www.sphinx-doc.org/en/master/
|
||||
.. _tox: https://tox.wiki/en/stable/
|
||||
.. _virtual environment: https://realpython.com/python-virtual-environments-a-primer/
|
||||
.. _virtualenv: https://virtualenv.pypa.io/en/stable/
|
||||
|
||||
.. _GitHub web interface: https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files
|
||||
.. _GitHub's code editor: https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files
|
|
@ -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
|
||||
|
|
@ -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=
|
|
@ -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)")
|
|
@ -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)")
|
|
@ -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)")
|
|
@ -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)")
|
|
@ -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
|
|
@ -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))
|
|
@ -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)) + "."))
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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]
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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]
|
|
@ -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
|
|
@ -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]
|
|
@ -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())
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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)
|
|
@ -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'
|
|
@ -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)
|
|
@ -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()
|
|
@ -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)
|
|
@ -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}"
|
|
@ -1,2 +0,0 @@
|
|||
from .Hardware import get_interfaces, get_VNA #noqa
|
||||
from .VNA import InvalidVNA, Version #noqa
|
|
@ -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)
|
|
@ -1,3 +0,0 @@
|
|||
from .Widget import Marker # noqa
|
||||
from .Settings import MarkerSettingsWindow # noqa
|
||||
from .Values import Value, default_label_ids # noqa
|
Plik diff jest za duży
Load Diff
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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))
|
|
@ -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)
|
|
@ -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())
|
|
@ -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()
|
|
@ -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()
|
|
@ -0,0 +1 @@
|
|||
icon_48x48.png
|
14
Pipfile
14
Pipfile
|
@ -1,14 +0,0 @@
|
|||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[packages]
|
||||
pyserial = "*"
|
||||
pyqt5 = "*"
|
||||
numpy = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
228
README.md
228
README.md
|
@ -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¤cy_code=EUR&source=url)
|
||||
|
||||
NanoVNASaver ============ A multiplatform tool to save Touchstone files from
|
||||
the NanoVNA, sweep frequency spans in segments to gain more than 101 data
|
||||
points, and generally display and analyze the resulting data.
|
||||
|
||||
Copyright 2019, 2020 Rune B. Broberg
|
||||
|
||||
## 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¤cy_code=EUR&source=url)
|
|
@ -0,0 +1,271 @@
|
|||
.. role:: raw-html-m2r(raw)
|
||||
:format: html
|
||||
|
||||
.. image:: https://img.shields.io/github/v/release/NanoVNA-Saver/nanovna-saver.svg
|
||||
:target: https://github.com/NanoVNA-Saver/nanovna-saver/releases/latest
|
||||
:alt: Latest Release
|
||||
|
||||
.. image:: https://img.shields.io/github/license/NanoVNA-Saver/nanovna-saver.svg
|
||||
:target: https://github.com/NanoVNA-Saver/nanovna-saver/blob/master/LICENSE.txt
|
||||
:alt: License
|
||||
|
||||
.. image:: https://img.shields.io/github/downloads/NanoVNA-Saver/nanovna-saver/total.svg
|
||||
:target: https://github.com/NanoVNA-Saver/nanovna-saver/releases/
|
||||
:alt: Downloads
|
||||
|
||||
.. image:: https://img.shields.io/github/downloads/NanoVNA-Saver/nanovna-saver/latest/total
|
||||
:target: https://github.com/NanoVNA-Saver/nanovna-saver/releases/latest
|
||||
:alt: GitHub Releases
|
||||
|
||||
.. image:: https://img.shields.io/badge/paypal-donate-yellow.svg
|
||||
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=T8KTGVDQF5K6E&item_name=NanoVNASaver+Development¤cy_code=EUR&source=url
|
||||
:alt: Donate
|
||||
|
||||
NanoVNASaver
|
||||
============
|
||||
|
||||
A multiplatform tool to save Touchstone files from the NanoVNA,
|
||||
sweep frequency spans in segments to gain more than 101 data
|
||||
points, and generally display and analyze the resulting data.
|
||||
|
||||
|
||||
* Copyright 2019, 2020 Rune B. Broberg
|
||||
* Copyright 2020ff NanoVNA-Saver Authors
|
||||
|
||||
It's developed in **Python 3 (>=3.8)** using **PyQt6**, **numpy** and
|
||||
**scipy**.
|
||||
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
This software connects to a NanoVNA and extracts the data for
|
||||
display on a computer and allows saving the sweep data to Touchstone files.
|
||||
|
||||
:raw-html-m2r:`<a href="#current-features"></a>`
|
||||
|
||||
Current features
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
|
||||
* Reading data from a NanoVNA -- Compatible devices: NanoVNA, NanoVNA-H,
|
||||
NanoVNA-H4, NanoVNA-F, AVNA via Teensy
|
||||
* Reading data from a TinySA
|
||||
* Splitting a frequency range into multiple segments to increase resolution
|
||||
(tried up to >10k points)
|
||||
* Averaging data for better results particularly at higher frequencies
|
||||
* Displaying data on multiple chart types, such as Smith, LogMag, Phase and
|
||||
VSWR-charts, for both S11 and S21
|
||||
* Displaying markers, and the impedance, VSWR, Q, equivalent
|
||||
capacitance/inductance etc. at these locations
|
||||
* Displaying customizable frequency bands as reference, for example amateur
|
||||
radio bands
|
||||
* Exporting and importing 1-port and 2-port Touchstone files
|
||||
* TDR function (measurement of cable length) - including impedance display
|
||||
* Filter analysis functions for low-pass, high-pass, band-pass and band-stop
|
||||
filters
|
||||
* Display of both an active and a reference trace
|
||||
* Live updates of data from the NanoVNA, including for multi-segment sweeps
|
||||
* In-application calibration, including compensation for non-ideal calibration
|
||||
standards
|
||||
* Customizable display options, including "dark mode"
|
||||
* Exporting images of plotted values
|
||||
|
||||
Screenshot
|
||||
^^^^^^^^^^
|
||||
|
||||
|
||||
.. image:: https://i.imgur.com/ZoFsV2V.png
|
||||
:target: https://i.imgur.com/ZoFsV2V.png
|
||||
:alt: Screenshot of version 0.1.4
|
||||
|
||||
|
||||
Running the application
|
||||
-----------------------
|
||||
|
||||
Main development is currently done on Linux (Mint 21 "Vanessa" Cinnamon)
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Binary releases
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
You can find current binary releases for Windows, Linux and MacOS under
|
||||
https://github.com/NanoVNA-Saver/nanovna-saver/releases/latest
|
||||
|
||||
The 32bit Windows binaries are somewhat smaller and seems to be a
|
||||
little bit more stable.
|
||||
|
||||
`Detailed installation instructions <docs/INSTALLATION.md>`_
|
||||
|
||||
Using the software
|
||||
------------------
|
||||
|
||||
Connect your NanoVNA to a serial port, and enter this serial port in the serial
|
||||
port box. If the NanoVNA is connected before the application starts, it should
|
||||
be automatically detected. Otherwise, click "Rescan". Click "Connect to device"
|
||||
to connect.
|
||||
|
||||
The app can collect multiple segments to get more accurate measurements. Enter
|
||||
the number of segments to be done in the "Segments" box. Each segment is 101
|
||||
data points, and takes about 1.5 seconds to complete.
|
||||
|
||||
Frequencies are entered in Hz, or suffixed with k or M. Scientific notation
|
||||
(6.5e6 for 6.5MHz) also works.
|
||||
|
||||
Markers can be manually entered, or controlled using the mouse. For mouse
|
||||
control, select the active marker using the radio buttons, or hold "shift"
|
||||
while clicking to drag the nearest marker. The marker readout boxes show the
|
||||
actual frequency where values are measured. Marker readouts can be hidden
|
||||
using the "hide data" button when not needed.
|
||||
|
||||
Display settings are available under "Display setup". These allow changing the
|
||||
chart colours, the application font size and which graphs are displayed. The
|
||||
settings are saved between program starts.
|
||||
|
||||
Calibration
|
||||
^^^^^^^^^^^
|
||||
|
||||
*Before using NanoVNA-Saver, please ensure that the device itself is in a
|
||||
reasonable calibration state.*
|
||||
|
||||
A calibration of both ports across the entire frequency span, saved to save
|
||||
slot 0, is sufficient. If the NanoVNA is completely uncalibrated, its readings
|
||||
may be outside the range accepted by the application.
|
||||
|
||||
In-application calibration is available, either assuming ideal standards or
|
||||
with relevant standard correction. To manually calibrate, sweep each standard
|
||||
in turn and press the relevant button in the calibration window.
|
||||
For assisted calibration, press the "Calibration Assistant" button. If desired,
|
||||
enter a note in the provided field describing the conditions under which the
|
||||
calibration was performed.
|
||||
|
||||
Calibration results may be saved and loaded using the provided buttons at the
|
||||
bottom of the window. Notes are saved and loaded along with the calibration
|
||||
data.
|
||||
|
||||
|
||||
.. image:: https://i.imgur.com/p94cxOX.png
|
||||
:target: https://i.imgur.com/p94cxOX.png
|
||||
:alt: Screenshot of Calibration Window
|
||||
|
||||
|
||||
Users of known characterized calibration standard sets can enter the data for
|
||||
these, and save the sets.
|
||||
|
||||
After pressing *Apply*\ , the calibration is immediately applied to the latest
|
||||
sweep data.
|
||||
|
||||
! *Currently, load capacitance is unsupported* !
|
||||
|
||||
TDR
|
||||
^^^
|
||||
|
||||
To get accurate TDR measurements, calibrate the device, and attach the cable to
|
||||
be measured at the calibration plane - i.e. at the same position where the
|
||||
calibration load would be attached. Open the "Time Domain Reflectometry"
|
||||
window, and select the correct cable type, or manually enter a propagation
|
||||
factor.
|
||||
|
||||
Measuring inductor core permeability
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The permeability (mu) of cores can be measured using a one-port measurement.
|
||||
Put one or more windings on a core of known dimensions and use the "S11 mu"
|
||||
plot from the "Display Setup". The core dimensions (cross section area in mm2,
|
||||
effective length in mm) and number of windings can be set in the context menu
|
||||
for the plot (right click on the plot).
|
||||
|
||||
Latest Changes
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
* Using PyQt6
|
||||
* Moved to PyScaffold project structure
|
||||
* Fixed crash in resonance analysis
|
||||
* Added TinySA readout and screenshot
|
||||
|
||||
|
||||
Changes in 0.5.5
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
* Measuring inductor core permeability
|
||||
* Bugfixes for calibration data loading and saving
|
||||
* Let V2 Devices more time for usb-serial setup
|
||||
* Make some windows scrollable
|
||||
|
||||
Changes in 0.5.4
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
* Bugfixes for Python3.11 compatability
|
||||
* Bugfix for Python3.8 compatability
|
||||
* use math instead of table for log step calculation
|
||||
* Support of NanoVNA V2 Plus5 on Windows
|
||||
* New SI prefixes added - Ronna, Quetta
|
||||
* addes a Makefile to build a packages
|
||||
* Simplyfied sweep worker
|
||||
* Fixed calibration data loading
|
||||
* Explicit import of scipy functions - #555
|
||||
* Refactoring of Analysis modules
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
First off, thanks for taking the time to contribute! Contributions are what
|
||||
make the open-source community such an amazing place to learn, inspire, and
|
||||
create. Any contributions you make will benefit everybody else and are
|
||||
**greatly appreciated**.
|
||||
|
||||
Please read `our contribution guidelines <docs/CONTRIBUTING.md>`_\ , and thank
|
||||
you for being involved!
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
This software is licensed under version 3 of the GNU General Public License. It
|
||||
comes with NO WARRANTY.
|
||||
|
||||
You can use it, commercially as well. You may make changes to the code, but I
|
||||
(and the license) ask that you give these changes back to the community.
|
||||
|
||||
References
|
||||
----------
|
||||
|
||||
|
||||
* Ohan Smit wrote an introduction to using the application:
|
||||
[https://zs1sci.com/blog/nanovnasaver/]
|
||||
* HexAndFlex wrote a 3-part (thus far) series on Getting Started with the
|
||||
NanoVNA:
|
||||
[https://hexandflex.com/2019/08/31/getting-started-with-the-nanovna-part-1/]
|
||||
- Part 3 is dedicated to NanoVNASaver:
|
||||
[https://hexandflex.com/2019/09/15/getting-started-with-the-nanovna-part-3-pc-software/]
|
||||
* Gunthard Kraus did documentation in English and German:
|
||||
[http://www.gunthard-kraus.de/fertig_NanoVNA/English/]
|
||||
[http://www.gunthard-kraus.de/fertig_NanoVNA/Deutsch/]
|
||||
|
||||
Acknowledgements
|
||||
----------------
|
||||
|
||||
Original application by Rune B. Broberg (5Q5R)
|
||||
|
||||
Contributions and changes by Holger Müller (DG5DBH), David Hunt and others.
|
||||
|
||||
TDR inspiration shamelessly stolen from the work of Salil (VU2CWA) at
|
||||
https://nuclearrambo.com/wordpress/accurately-measuring-cable-length-with-nanovna/
|
||||
|
||||
TDR cable types by Larry Goga.
|
||||
|
||||
Bugfixes and Python installation work by Ohan Smit.
|
||||
|
||||
Thanks to everyone who have tested, commented and inspired. Particular thanks
|
||||
go to the alpha testing crew who suffer the early instability of new versions.
|
||||
|
||||
This software is available free of charge. If you read all this way, and you
|
||||
*still* want to support it, you may donate to the developer using the button
|
||||
below:
|
||||
|
||||
|
||||
.. image:: https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif
|
||||
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=T8KTGVDQF5K6E&item_name=NanoVNASaver+Development¤cy_code=EUR&source=url
|
||||
:alt: Paypal
|
||||
|
|
@ -1 +0,0 @@
|
|||
theme: jekyll-theme-dinky
|
|
@ -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}
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
export PYTHONPATH="src"
|
||||
exec python -m debugpy --listen 5678 --wait-for-client $@
|
|
@ -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>
|
||||
|
|
@ -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)
|
|
@ -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
|
|
@ -0,0 +1,29 @@
|
|||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
AUTODOCDIR = api
|
||||
|
||||
# User-friendly check for sphinx-build
|
||||
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $?), 1)
|
||||
$(error "The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://sphinx-doc.org/")
|
||||
endif
|
||||
|
||||
.PHONY: help clean Makefile
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/* $(AUTODOCDIR)
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
@ -0,0 +1 @@
|
|||
# Empty directory
|
|
@ -0,0 +1,2 @@
|
|||
.. _authors:
|
||||
.. include:: ../AUTHORS.rst
|
|
@ -0,0 +1,286 @@
|
|||
# This file is execfile()d with the current directory set to its containing dir.
|
||||
#
|
||||
# This file only contains a selection of the most common options. For a full
|
||||
# list see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
__location__ = os.path.dirname(__file__)
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
sys.path.insert(0, os.path.join(__location__, "../src"))
|
||||
|
||||
# -- Run sphinx-apidoc -------------------------------------------------------
|
||||
# This hack is necessary since RTD does not issue `sphinx-apidoc` before running
|
||||
# `sphinx-build -b html . _build/html`. See Issue:
|
||||
# https://github.com/readthedocs/readthedocs.org/issues/1139
|
||||
# DON'T FORGET: Check the box "Install your project inside a virtualenv using
|
||||
# setup.py install" in the RTD Advanced Settings.
|
||||
# Additionally it helps us to avoid running apidoc manually
|
||||
|
||||
try: # for Sphinx >= 1.7
|
||||
from sphinx.ext import apidoc
|
||||
except ImportError:
|
||||
from sphinx import apidoc
|
||||
|
||||
output_dir = os.path.join(__location__, "api")
|
||||
module_dir = os.path.join(__location__, "../src/NanoVNASaver")
|
||||
try:
|
||||
shutil.rmtree(output_dir)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import sphinx
|
||||
|
||||
cmd_line = f"sphinx-apidoc --implicit-namespaces -f -o {output_dir} {module_dir}"
|
||||
|
||||
args = cmd_line.split(" ")
|
||||
if tuple(sphinx.__version__.split(".")) >= ("1", "7"):
|
||||
# This is a rudimentary parse_version to avoid external dependencies
|
||||
args = args[1:]
|
||||
|
||||
apidoc.main(args)
|
||||
except Exception as e:
|
||||
print("Running `sphinx-apidoc` failed!\n{}".format(e))
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx.ext.todo",
|
||||
"sphinx.ext.autosummary",
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinx.ext.coverage",
|
||||
"sphinx.ext.doctest",
|
||||
"sphinx.ext.ifconfig",
|
||||
"sphinx.ext.mathjax",
|
||||
"sphinx.ext.napoleon",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = ".rst"
|
||||
|
||||
# The encoding of source files.
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = "nanovna-saver"
|
||||
copyright = "2023, Holger Mueller"
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# version: The short X.Y version.
|
||||
# release: The full version, including alpha/beta/rc tags.
|
||||
# If you don’t need the separation provided between version and release,
|
||||
# just set them both to the same value.
|
||||
try:
|
||||
from NanoVNASaver import __version__ as version
|
||||
except ImportError:
|
||||
version = ""
|
||||
|
||||
if not version or version.lower() == "unknown":
|
||||
version = os.getenv("READTHEDOCS_VERSION", "unknown") # automatically set by RTD
|
||||
|
||||
release = version
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
# language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
# today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
# today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", ".venv"]
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
# default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
# add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
# add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = "sphinx"
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
# keep_warnings = False
|
||||
|
||||
# If this is True, todo emits a warning for each TODO entries. The default is False.
|
||||
todo_emit_warnings = True
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = "alabaster"
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
html_theme_options = {
|
||||
"sidebar_width": "300px",
|
||||
"page_width": "1200px"
|
||||
}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
# html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
# html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
# html_logo = ""
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
# html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ["_static"]
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
# html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
# html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
# html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
# html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
# html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
# html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
# html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
# html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
# html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
# html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
# html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
# html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = "nanovna-saver-doc"
|
||||
|
||||
|
||||
# -- Options for LaTeX output ------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ("letterpaper" or "a4paper").
|
||||
# "papersize": "letterpaper",
|
||||
# The font size ("10pt", "11pt" or "12pt").
|
||||
# "pointsize": "10pt",
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
# "preamble": "",
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
("index", "user_guide.tex", "nanovna-saver Documentation", "Holger Mueller", "manual")
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
# latex_logo = ""
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
# latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
# latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
# latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
# latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
# latex_domain_indices = True
|
||||
|
||||
# -- External mapping --------------------------------------------------------
|
||||
python_version = ".".join(map(str, sys.version_info[0:2]))
|
||||
intersphinx_mapping = {
|
||||
"sphinx": ("https://www.sphinx-doc.org/en/master", None),
|
||||
"python": ("https://docs.python.org/" + python_version, None),
|
||||
"matplotlib": ("https://matplotlib.org", None),
|
||||
"numpy": ("https://numpy.org/doc/stable", None),
|
||||
"sklearn": ("https://scikit-learn.org/stable", None),
|
||||
"pandas": ("https://pandas.pydata.org/pandas-docs/stable", None),
|
||||
"scipy": ("https://docs.scipy.org/doc/scipy/reference", None),
|
||||
"setuptools": ("https://setuptools.pypa.io/en/stable/", None),
|
||||
"pyscaffold": ("https://pyscaffold.org/en/stable", None),
|
||||
}
|
||||
|
||||
print(f"loading configurations for {project} {version} ...", file=sys.stderr)
|
|
@ -0,0 +1 @@
|
|||
.. include:: ../CONTRIBUTING.rst
|
|
@ -0,0 +1,60 @@
|
|||
=============
|
||||
nanovna-saver
|
||||
=============
|
||||
|
||||
This is the documentation of **nanovna-saver**.
|
||||
|
||||
.. note::
|
||||
|
||||
This is the main page of your project's `Sphinx`_ documentation.
|
||||
It is formatted in `reStructuredText`_. Add additional pages
|
||||
by creating rst-files in ``docs`` and adding them to the `toctree`_ below.
|
||||
Use then `references`_ in order to link them from this page, e.g.
|
||||
:ref:`authors` and :ref:`changes`.
|
||||
|
||||
It is also possible to refer to the documentation of other Python packages
|
||||
with the `Python domain syntax`_. By default you can reference the
|
||||
documentation of `Sphinx`_, `Python`_, `NumPy`_, `SciPy`_, `matplotlib`_,
|
||||
`Pandas`_, `Scikit-Learn`_. You can add more by extending the
|
||||
``intersphinx_mapping`` in your Sphinx's ``conf.py``.
|
||||
|
||||
The pretty useful extension `autodoc`_ is activated by default and lets
|
||||
you include documentation from docstrings. Docstrings can be written in
|
||||
`Google style`_ (recommended!), `NumPy style`_ and `classical style`_.
|
||||
|
||||
|
||||
Contents
|
||||
========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
Overview <readme>
|
||||
Contributions & Help <contributing>
|
||||
License <license>
|
||||
Authors <authors>
|
||||
Module Reference <api/modules>
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
.. _toctree: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html
|
||||
.. _reStructuredText: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html
|
||||
.. _references: https://www.sphinx-doc.org/en/stable/markup/inline.html
|
||||
.. _Python domain syntax: https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#the-python-domain
|
||||
.. _Sphinx: https://www.sphinx-doc.org/
|
||||
.. _Python: https://docs.python.org/
|
||||
.. _Numpy: https://numpy.org/doc/stable
|
||||
.. _SciPy: https://docs.scipy.org/doc/scipy/reference/
|
||||
.. _matplotlib: https://matplotlib.org/contents.html#
|
||||
.. _Pandas: https://pandas.pydata.org/pandas-docs/stable
|
||||
.. _Scikit-Learn: https://scikit-learn.org/stable
|
||||
.. _autodoc: https://www.sphinx-doc.org/en/master/ext/autodoc.html
|
||||
.. _Google style: https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings
|
||||
.. _NumPy style: https://numpydoc.readthedocs.io/en/latest/format.html
|
||||
.. _classical style: https://www.sphinx-doc.org/en/master/domains.html#info-field-lists
|
|
@ -0,0 +1,7 @@
|
|||
.. _license:
|
||||
|
||||
=======
|
||||
License
|
||||
=======
|
||||
|
||||
.. include:: ../LICENSE.txt
|
|
@ -0,0 +1,66 @@
|
|||
.\" English manual page for nanovna-saver
|
||||
.\"
|
||||
.\" Copyright (C) 2023-2023 Nicolas Boulenguez <nicolas@debian.org>
|
||||
.\"
|
||||
.\" This program is free software: you can redistribute it and/or
|
||||
.\" modify it under the terms of the GNU General Public License as
|
||||
.\" published by the Free Software Foundation, either version 3 of the
|
||||
.\" License, or (at your option) any later version.
|
||||
.\" This program is distributed in the hope that it will be useful, but
|
||||
.\" WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
.\" General Public License for more details.
|
||||
.\" You should have received a copy of the GNU General Public License
|
||||
.\" along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
.\"
|
||||
.TH NANOVNASAVER 1 "2023-03-19"
|
||||
.\"----------------------------------------------------------------------
|
||||
.SH NAME
|
||||
NANOVNASAVER \- save Touchstone files from the NanoVNA device
|
||||
.\"----------------------------------------------------------------------
|
||||
.SH SYNOPSIS
|
||||
.B NanoVNASaver
|
||||
.RB [\| \-h \|]
|
||||
.RB [\| \-d \|]
|
||||
.RB [\| \-D
|
||||
.IR DEBUG_FILE \|]
|
||||
.RB [\| \-f
|
||||
.IR FILE \|]
|
||||
.RB [\| \-r
|
||||
.IR REF_FILE \|]
|
||||
.RB [\| \-\-version \|]
|
||||
.\"----------------------------------------------------------------------
|
||||
.SH DESCRCIPTION
|
||||
The NanoVNASaver graphical tool saves Touchstone files from the
|
||||
NanoVNA, sweeps frequency spans in segments to gain more data points,
|
||||
and generally displays and analyzes the resulting data.
|
||||
.PP
|
||||
The authors expect most users to use a graphical launcher instead of
|
||||
the command line interface.
|
||||
.\"----------------------------------------------------------------------
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
\fB\-h\fR, \fB\-\-help\fR
|
||||
Show a summary of options and exit.
|
||||
.TP
|
||||
\fB\-d\fR, \fB\-\-debug\fR
|
||||
Set loglevel to debug.
|
||||
.TP
|
||||
\fB\-D \fIDEBUG_FILE\fR, \fB\-\-debug\-file \fIDEBUG_FILE\fR
|
||||
File to write debug logging output to.
|
||||
.TP
|
||||
\fB\-f \fIFILE\fR, \fB\-\-file \fIFILE\fR
|
||||
Touchstone file to load as sweep for off device usage.
|
||||
.TP
|
||||
\fB\-r \fIREF_FILE\fR, \fB\-\-ref\-file \fIREF_FILE\fR
|
||||
Touchstone file to load as reference for off device usage.
|
||||
.TP
|
||||
\fB\-\-version\fR
|
||||
Show program's version number and exit.
|
||||
.\"----------------------------------------------------------------------
|
||||
.SH SEE ALSO
|
||||
The documentation is installed at
|
||||
.BR /usr/share/doc/nanovna-saver/ .
|
||||
.\"----------------------------------------------------------------------
|
||||
.SH HISTORY
|
||||
This page has been written for Debian but may be reused by others.
|
|
@ -0,0 +1,2 @@
|
|||
.. _readme:
|
||||
.. include:: ../README.rst
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue