Compare commits

...

99 Commits
v1.1 ... master

Author SHA1 Message Date
Michal Krenek (Mikos)
625ba9d698 Refactoring 2017-04-27 23:22:29 +02:00
Michal Krenek (Mikos)
6800f5bd8e Allow subtracting baseline 2017-04-27 23:01:15 +02:00
Michal Krenek (Mikos)
4571075bd9 Use np.linspace() instead of np.arange() in soapy_power, rtl_power and rx_power backends 2017-04-27 16:46:18 +02:00
Michal Krenek (Mikos)
5505f0dfd7 Write whole backend command with params to console 2017-04-21 17:20:29 +02:00
Michal Krenek (Mikos)
2544b9a164 Fix human readable time formatting 2017-04-12 17:49:26 +02:00
Michal Krenek (Mikos)
8aa6d50bb8 Show scan progress if interval is >= 1 s 2017-04-12 17:44:22 +02:00
Michal Krenek (Mikos)
31471b3386 Allow setting hackrf_sweep backend to -1 in GUI 2017-04-03 17:56:08 +02:00
Michal Krenek (Mikos)
1c9e7681b8 Allow manual setting of specific gains in hackrf_sweep backend 2017-04-03 17:50:38 +02:00
Michal Krenek (Mikos)
edf8ae17e2 Update setup.py to be compatible with latest version of subzero 2017-03-31 16:43:49 +02:00
Michal Krenek (Mikos)
c6e305343c Unhide console on Windows right before exit (we don't want to leave zombies behind) 2017-03-30 16:17:03 +02:00
Michal Krenek (Mikos)
c140e9313e Code cleanup 2017-03-30 16:08:47 +02:00
Michal Krenek (Mikos)
c93bbf3de5 Fix link in README.rst 2017-03-30 15:51:23 +02:00
Michal Krenek (Mikos)
36be8e8371 Update README.rst with informations about Windows installer 2017-03-30 15:47:37 +02:00
Michal Krenek (Mikos)
06956c4c1f Move classifiers down in setup.py 2017-03-30 14:55:15 +02:00
Michal Krenek (Mikos)
9b04a924f2 Update .desktop file 2017-03-30 14:51:14 +02:00
Michal Krenek (Mikos)
415687685a Update version to 2.2.0 2017-03-30 14:49:07 +02:00
Michal Krenek (Mikos)
9d21d49157 Always open console on Windows (but hide it)
Otherwise program crashes when trying to communicate with subprocess
(e.g. soapy_power) if started in pythonw.exe, bacause console is missing.
2017-03-30 14:27:33 +02:00
Michal Krenek (Mikos)
e9391aedc0 Split help command executable in settings 2017-03-28 15:52:49 +02:00
Michal Krenek (Mikos)
05f4a59b66 Hide console window for subprocesses on Windows 2017-03-28 12:26:45 +02:00
Michal Krenek (Mikos)
fa462f885d Make help commands in settings compatible with Python 3.4 2017-03-28 10:44:03 +02:00
Michal Krenek (Mikos)
9ee6f631dc Fix shortcut in MSI installer 2017-03-28 00:36:32 +02:00
Michal Krenek (Mikos)
9271e42766 Switch from cx_Freeze to PyInstaller / subzero for building frozen Windows executables 2017-03-28 00:32:11 +02:00
Michal Krenek (Mikos)
e75f9a98d9 Merge pull request #28 from michaellass/hackrf_ratelimit
hackrf_sweep: Improve low frequency range capabilities
2017-03-24 22:58:56 +01:00
Michael Lass
5985cb4bfe hackrf_sweep: fix race condition when stopping
When stopping, the process may die right before we try to read from its stdout,
generating the following error:

Traceback (most recent call last):
  File "/usr/lib/python3.6/site-packages/qspectrumanalyzer/backends/hackrf_sweep.py", line 146, in run
    buf = self.process.stdout.read(4)
AttributeError: 'NoneType' object has no attribute 'stdout'

Catch the error instead of crashing.
2017-03-24 21:57:52 +01:00
Michael Lass
bc3319ba82 hackrf_sweep: Reduce bin_size_min to 3 kHz
Using rate limiting, smaller bin sizes than 40 kHz can be used. This is useful
when using the hackrf_sweep backend with narrow frequency ranges.
2017-03-24 21:38:52 +01:00
Michael Lass
a49f1a3187 hackrf_sweep: Implement rate limiting by ignoring sweeps
For low frequency ranges the sweep range of hackrf_sweep is pretty high.
To allow using the hackrf_sweep backend with those small ranges, we can
simply ignore sweeps based on a user-set sweep interval.
2017-03-24 21:37:31 +01:00
Michal Krenek (Mikos)
12da1d6626 Merge pull request #22 from michaellass/check_bincount
Check bincount in data coming from backend
2017-03-24 21:02:10 +01:00
Michael Lass
9dc1433bdf Check bincount in data coming from backend
The backend may return fewer data points than expected. This can e.g. be
observed for the hackrf_sweep backend on high system load. Currently
this leads to an error when trying to insert the data into the
HistoryBuffer and causes the application to crash:

ValueError: cannot copy sequence with size 24750 to array axis with dimension 25000

As a workaround, just ignore data in this case.
2017-03-24 17:50:03 +01:00
Michal Krenek (Mikos)
8109c537f6 Allow setting gain with more precision; tune default parameters of backends 2017-03-24 16:51:32 +01:00
Michal Krenek (Mikos)
87585c106b Fix numbering in README.rst 2017-03-24 02:50:03 +01:00
Michal Krenek (Mikos)
02859deccf Add Windows installation instructions to README.rst 2017-03-24 02:48:21 +01:00
Michal Krenek (Mikos)
2385c228c6 Fix README.rst 2017-03-24 01:33:05 +01:00
Michal Krenek (Mikos)
11979ba641 Update README.rst, clarify backends section 2017-03-24 01:22:03 +01:00
Michal Krenek (Mikos)
9135dc58e4 Make soapy_power dependency optional (but don't remove it from setup.py and PKGBUILD, because it is default backend) 2017-03-24 00:15:56 +01:00
Michal Krenek (Mikos)
92e1771db5 Add support for creating frozen executables with cx_Freeze; use new setup_qt module for building Qt UI files 2017-03-23 18:11:05 +01:00
Michal Krenek (Mikos)
8ac3346c1e Fix some GUI issues on Windows 2017-03-22 02:16:26 +01:00
Michal Krenek (Mikos)
7e84cd7a1c Fix another Windows peculiarity. How can anyone sane use that fscking OS?!? 2017-03-21 23:39:30 +01:00
Michal Krenek (Mikos)
5d41c6c4a8 Update version to 2.1.0 2017-03-21 17:43:39 +01:00
Michal Krenek (Mikos)
07d504ab30 Don't show number of hops in status bar if it isn't available 2017-03-21 16:59:48 +01:00
Michal Krenek (Mikos)
d8d158c100 Make QSpectrumAnalyzer compatible with latest soapy_power 1.5.0 2017-03-21 16:39:26 +01:00
Michal Krenek (Mikos)
c574761a8c Allow setting bandwidth and LNB LO (for upconverters or downconverters); show info about device in settings 2017-03-21 10:36:08 +01:00
Michal Krenek (Mikos)
ea969b89e2 Add missing Qt.py dependency to PKGBUILD 2017-03-17 20:23:45 +01:00
Michal Krenek (Mikos)
b8666417b0 Update version to 2.0.0 2017-03-17 15:03:07 +01:00
Michal Krenek (Mikos)
e0f7202ff9 Update soapy_power_bin format to include both acquisition start time and stop time 2017-03-16 16:43:12 +01:00
Michal Krenek (Mikos)
9de4c25269 Fix pipe for communication with soapy_power on Windows 2017-03-15 00:58:10 +01:00
Michal Krenek (Mikos)
50ff8d517d Allow backend executable to be multiple words (e.g. python /path/to/soapy_power.py) 2017-03-15 00:38:10 +01:00
Michal Krenek (Mikos)
58ea054d70 Merge pull request #20 from xmikos/pyqt5
Switch from PyQt4 to Qt.py wrapper (supports PyQt4 / PyQt5 / PySide / PySide2)
2017-03-13 22:53:30 +01:00
Michal Krenek (Mikos)
228255b7e1 Switch from PyQt4 to Qt.py wrapper (supports PyQt4 / PyQt5 / PySide / PySide2) 2017-03-13 22:50:54 +01:00
Michal Krenek (Mikos)
d1c6ca4d14 Use pipe for communication with soapy_power process, to avoid reading garbage from stdout (some SoapySDR drivers like LimeSDR always outputs something to stdout) 2017-03-13 17:41:22 +01:00
Michal Krenek (Mikos)
47ea0b99b5 New version 1.5.0 (soapy_sdr backend now fully functioning) 2017-03-10 17:39:04 +01:00
Michal Krenek (Mikos)
dc27e90653 Update README.rst 2017-02-19 11:37:30 +01:00
Michal Krenek (Mikos)
e7b998f69d Fix typo in last commit (need some sleep) 2017-02-19 00:52:05 +01:00
Michal Krenek (Mikos)
98a8b6bd1d Fix setup.py (missing qspectrumanalyzer.backends package) 2017-02-19 00:38:47 +01:00
Michal Krenek (Mikos)
e9264886e8 Separate backends into independent files 2017-02-14 23:01:15 +01:00
Michal Krenek (Mikos)
d351a858da Merge pull request #11 from miek/hackrf_sweep_fix
Fix old references to device_index from hackrf_sweep merge
2017-02-14 15:57:02 +01:00
Mike Walters
e73e921440 Fix old references to device_index from hackrf_sweep merge 2017-02-14 13:40:23 +00:00
Michal Krenek (Mikos)
7fa0985133 Merge pull request #10 from xmikos/hackrf_sweep
Merge hackrf_sweep branch into master
2017-02-14 13:54:21 +01:00
Michal Krenek (Mikos)
b38c3b3e76 Merge branch 'miek-master' into hackrf_sweep 2017-02-14 12:17:10 +01:00
Michal Krenek (Mikos)
b49c772961 Merge hackrf_sweep fork and fix conflicts 2017-02-14 12:14:52 +01:00
Michal Krenek (Mikos)
2ffa466b58 Add preliminary support for soapy_power and rx_power 2017-02-13 22:00:10 +01:00
Mike Walters
71b3d6a4ca Merge pull request #4 from mossmann/master
Changed labels from dBm to dB.
2017-02-12 22:38:01 +00:00
Michael Ossmann
70b9219927 Changed labels from dBm to dB.
hackrf_sweep outputs dBFS, but rtl_power outputs dB with some weird offset.
In no case would dBm be correct without a calibration option.
The relative "dB" label should be acceptable for all cases.
2017-02-12 15:03:05 -07:00
Mike Walters
aba4f40bff Merge pull request #3 from mossmann/master
hackrf_sweep gain configuration support
2017-02-12 11:51:19 +00:00
Michael Ossmann
ba3e113e29 set sample rate when backend is selected 2017-02-11 23:05:08 -07:00
Michael Ossmann
3cc202de89 set backend-specific min/max/default values when backend is selected 2017-02-11 22:24:44 -07:00
Michael Ossmann
dc39b44c67 hackrf_sweep gain configuration support 2017-02-11 21:45:40 -07:00
Mike Walters
abc9dd2676 Merge pull request #2 from mossmann/master
compute bin width from number of input bins
2017-02-11 13:34:09 +00:00
Michael Ossmann
c060f82c25 fixed bug that can happen when frequency ranges are interleaved around the stop frequency 2017-02-10 22:18:46 -07:00
Michael Ossmann
76c3ad2b55 increased default and maximum bin size settings 2017-02-10 17:04:54 -07:00
Michael Ossmann
ab431554aa limit bin size configuration to acceptable bounds 2017-02-10 16:57:12 -07:00
Michael Ossmann
bb6412be69 removed bin width from hackrf_sweep output format 2017-02-10 16:38:01 -07:00
Michael Ossmann
eef73719f0 compute bin width from number of input bins 2017-02-10 16:29:47 -07:00
Mike Walters
cfe1b4578b Merge pull request #1 from mossmann/master
compatibility with updated hackrf_sweep binary output format
2017-02-10 13:11:02 +00:00
Michael Ossmann
71ff6cd950 cleanup 2017-02-09 19:17:24 -07:00
Michael Ossmann
adb3b53058 updated for recent changes in hackrf_sweep -B output format 2017-02-09 19:05:46 -07:00
Michael Ossmann
4e9ecdb64d updated for compatibility with new hackrf_sweep -B option 2017-02-09 08:27:53 -07:00
Mike Walters
713434d029 Let the processing thread stop the process
Fixes exception where the processing thread would try to read stdout
after the process had stopped, but before the processing thread stopped
2016-07-31 16:05:56 +01:00
Mike Walters
461e674f82 Ignore band-edge and DC bins 2016-07-30 14:55:28 +01:00
Mike Walters
2c76c5cbe3 Pass sweep freq argument 2016-07-30 14:51:34 +01:00
Mike Walters
1538f37132 Make it work 2016-07-30 10:51:29 +01:00
Mike Walters
a3ce184f5e Initial hackrf_sweep skeleton 2016-07-30 06:29:37 +01:00
Michal Krenek (Mikos)
6cf2694708 Add support for multiple RTL-SDR devices (select device index in settings) 2016-05-19 22:28:44 +02:00
Michal Krenek (Mikos)
4e7818536c Update screenshots in README.rst 2016-05-17 02:51:19 +02:00
Michal Krenek (Mikos)
18a033260c Updated version to 1.4.0 2016-05-17 02:39:58 +02:00
Michal Krenek (Mikos)
7112ce7888 Min. / Max. peak hold; recalculate curves from history; separate data backend; refactoring 2016-05-17 02:37:06 +02:00
Michal Krenek (Mikos)
53f5775ae4 Update README.md 2015-12-18 21:54:39 +01:00
Michal Krenek (Mikos)
acbca50eed Add average and spectrum persistence (RTSA fosphor-like effect); some refactoring and GUI changes 2015-12-18 21:43:40 +01:00
Michal Krenek (Mikos)
be2d3f6b62 Another refactoring 2015-12-15 02:37:47 +01:00
Michal Krenek (Mikos)
f046ba568a Code refactoring 2015-12-14 19:29:14 +01:00
Michal Krenek (Mikos)
712cb72157 Update version to 1.3.0 2015-12-14 02:24:42 +01:00
Michal Krenek (Mikos)
ea548bcaa0 Fix .desktop file to be valid 2015-12-14 02:21:32 +01:00
Michal Krenek (Mikos)
6832b9ddee Update README.md 2015-12-14 02:16:30 +01:00
Michal Krenek (Mikos)
8096c6e3c3 Add peak hold and smoothing; fix window maximization at startup; split dock widgets 2015-12-14 02:01:23 +01:00
Michal Krenek (Mikos)
0af92b3d16 Fix typo 2015-12-10 01:42:04 +01:00
Michal Krenek (Mikos)
4870df2d4a Installation instructions and todo 2015-12-10 01:38:17 +01:00
Michal Krenek (Mikos)
a0f6ca2432 Update README.rst 2015-12-10 00:08:26 +01:00
Michal Krenek (Mikos)
314118a7ff Add support for rtl_power_fftw backend 2015-12-07 03:36:43 +01:00
Michal Krenek (Mikos)
1f48ebf351 Updated version to 1.1.1 2015-04-26 22:18:04 +02:00
Michal Krenek (Mikos)
f5d97fab80 Switch from distutils to setuptools 2015-04-26 22:16:21 +02:00
43 changed files with 5410 additions and 947 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ __pycache__/
build/
dist/
MANIFEST
*.egg-info

View File

@ -1,2 +1,4 @@
include LICENSE
include README.rst
include qspectrumanalyzer.desktop
include qspectrumanalyzer.png

View File

@ -1,12 +1,20 @@
# Maintainer: Michal Krenek (Mikos) <m.krenek@gmail.com>
pkgname=qspectrumanalyzer
pkgver=1.1
pkgver=2.2.0
pkgrel=1
pkgdesc="Spectrum analyzer for RTL-SDR (GUI for rtl_power based on PyQtGraph)"
pkgdesc="Spectrum analyzer for multiple SDR platforms (PyQtGraph based GUI for soapy_power, hackrf_sweep, rtl_power, rx_power and other backends)"
arch=('any')
url="https://github.com/xmikos/qspectrumanalyzer"
license=('GPL3')
depends=('python-pyqt4' 'python-pyqtgraph' 'rtl-sdr')
depends=('python-qt.py' 'python-pyqt5' 'python-pyqtgraph' 'soapy_power>=1.6.0')
makedepends=('python-setuptools')
optdepends=(
'hackrf: hackrf_sweep backend (wideband spectrum monitoring with sweep rate of 8 GHz/s)'
'rtl_power_fftw-git: alternative RTL-SDR backend using FFTW library (much faster than rtl_power)'
'rtl-sdr-keenerd-git: better version of rtl_power backend'
'rtl-sdr: original rtl_power backend (slightly broken, use rtl-sdr-keenerd-git instead)'
'rx_tools: rx_power backend (universal SoapySDR based backend, but seems slow and buggy)'
)
source=(https://github.com/xmikos/qspectrumanalyzer/archive/v$pkgver.tar.gz)
build() {

View File

@ -1,41 +1,184 @@
QSpectrumAnalyzer
=================
Spectrum analyzer for RTL-SDR (GUI for rtl_power based on PyQtGraph)
Spectrum analyzer for multiple SDR platforms (PyQtGraph based GUI for soapy_power,
hackrf_sweep, rtl_power, rx_power and other backends)
Screenshot
----------
Screenshots
-----------
.. image:: https://xmikos.github.io/qspectrumanalyzer/qspectrumanalyzer_screenshot.png
.. image:: https://xmikos.github.io/qspectrumanalyzer/qspectrumanalyzer_screenshot2.png
Requirements
------------
- Python >= 3.3
- PyQt >= 4.5
- PyQt4 / PyQt5 / PySide / PySide2
- Qt.py (https://github.com/mottosso/Qt.py)
- PyQtGraph (http://www.pyqtgraph.org)
- rtl-sdr (https://github.com/keenerd/rtl-sdr)
- soapy_power (https://github.com/xmikos/soapy_power)
- Optional: hackrf / rtl-sdr / rtl_power_fftw / rx_tools
You should use Keenerds fork of rtl-sdr (latest Git revision),
bacause ``rtl_power`` in original rtl-sdr (from osmocom.org) is broken
(especially when used with cropping).
Backends
--------
Default backend
***************
- **soapy_power** (https://github.com/xmikos/soapy_power)
``soapy_power`` is the default and recommended universal SDR backend in QSpectrumAnalyzer.
It is based on `SoapySDR <https://github.com/pothosware/SoapySDR>`_ and supports
nearly all SDR platforms (RTL-SDR, HackRF, Airspy, SDRplay, LimeSDR, bladeRF,
USRP and some other SDR devices). It is highly configurable (see additional parameters
help in *Settings* menu) and supports short acquisition time for
near real-time continuous measurement.
Other backends
**************
- **hackrf_sweep** (https://github.com/mossmann/hackrf)
``hackrf_sweep`` backend enables wideband spectrum monitoring by rapidly retuning the radio
without requiring individual tuning requests from the host computer. This allows unprecedented
sweep rate of 8 GHz per second. Only HackRF is supported.
- **rtl_power_fftw** (https://github.com/AD-Vega/rtl-power-fftw)
``rtl_power_fftw`` is alternative backend for RTL-SDR devices and has various
benefits over ``rtl_power``. E.g. better FFT performance (thanks to
use of ``fftw`` library) and possibility to use short acquisition time
for near real-time continuous measurement (minimum interval in original
``rtl_power`` is 1 second).
- **rtl_power** (https://github.com/keenerd/rtl-sdr)
``rtl_power`` is original backend for RTL-SDR devices. There are better alternatives now, but
if you want to use it, you should use `Keenerds fork of rtl-sdr <https://github.com/keenerd/rtl-sdr>`_
(latest Git revision), because ``rtl_power`` in original rtl-sdr package (from osmocom.org)
is broken (especially when used with cropping).
- **rx_power** (https://github.com/rxseger/rx_tools) *[unsupported]*
``rx_power`` (part of ``rx_tools``) is also based on SoapySDR (like default ``soapy_power`` backend)
and therefore supports nearly all SDR platforms. But it is much slower than soapy_power, doesn't support
near real-time continuous measurement (minimum interval is 1 second, same as ``rtl_power``)
and is buggy. Backend is currently unsupported, if you want to fix it, patches are welcome.
Usage
-----
Start QSpectrumAnalyzer by running ``qspectrumanalyzer``.
If you don't have ``rtl_power`` executable in system path, you can specify
location of it manually in *File* -> *Settings*. You can also specify waterfall
plot history size in there. Default is 100 lines, be aware that really large
sweeps (with a lot of bins) would require a lot of system memory, so don't make
this number too big.
You can choose which backend you want to use in *File* -> *Settings*
(or *Application menu* -> *Preferences* on Mac OS X), default is
``soapy_power``. Device, sample rate, bandwidth, LNB LO, path to backend executable
and additional backend parameters can be also manually specified there. You can
also set waterfall plot history size. Default is 100 lines, be aware that
really large sweeps (with a lot of bins) would require a lot of system
memory, so don't make this number too big.
Controls should be intuitive, but if you want consistent results, you should
turn off automatic gain control (set it to some fixed number) and also set
turn off automatic gain control (set gain to some fixed number) and also set
crop to 20% or more. For finding out ppm correction factor for your rtl-sdr
stick, use `kalibrate-rtl <https://github.com/steve-m/kalibrate-rtl>`_.
You can move and zoom plot with mouse, change plot settings or export plots
from right-click menu. Waterfall plot black/white levels and color lookup
table can be changed in mini-histogram widget.
table can be changed in mini-histogram widget (on *Levels* tab).
Installation
------------
Arch Linux:
***********
Stable version:
::
git clone https://aur.archlinux.org/qspectrumanalyzer.git
cd qspectrumanalyzer
makepkg -sri
Git master branch:
::
git clone https://aur.archlinux.org/qspectrumanalyzer-git.git
cd qspectrumanalyzer-git
makepkg -sri
Or simply use `pacaur <https://aur.archlinux.org/packages/pacaur>`_ (or any other AUR helper)
which will also automatically install all QSpectrumAnalyzer dependencies:
::
pacaur -S qspectrumanalyzer
pacaur -S qspectrumanalyzer-git
Ubuntu:
*******
::
# Add SoapySDR PPA to your system
sudo add-apt-repository -y ppa:myriadrf/drivers
# Update list of packages
sudo apt-get update
# Install basic dependencies
sudo apt-get install python3-pip python3-pyqt5 python3-numpy python3-scipy soapysdr python3-soapysdr
# Install SoapySDR drivers for your hardware (e.g. RTL-SDR, Airspy, HackRF, LimeSDR, etc.)
sudo apt-get install soapysdr-module-rtlsdr soapysdr-module-airspy soapysdr-module-hackrf soapysdr-module-lms7
# Install QSpectrumAnalyzer locally for your current user
pip3 install --user qspectrumanalyzer
``qspectrumanalyzer`` and ``soapy_power`` executables will be then placed in
``~/.local/bin`` directory, you can add it to your PATH in ``~/.bashrc``.
If you want to install QSpectrumAnalyzer directly from Git master branch, you can use this procedure:
::
git clone https://github.com/xmikos/qspectrumanalyzer.git
cd qspectrumanalyzer
pip3 install --user .
Windows:
********
*Only 64-bit Windows are supported (there are no public 32-bit builds of SoapySDR
libraries and drivers).*
1. install `SoapySDR <https://github.com/pothosware/SoapySDR/wiki>`_ libraries and drivers
(bundled as part of Pothos SDR installer: `download <http://downloads.myriadrf.org/builds/PothosSDR/?C=M;O=D>`_).
This bundle also includes other great SDR apps like `CubicSDR <http://cubicsdr.com>`_, `GQRX <http://gqrx.dk>`_,
`GNU Radio Companion <https://gnuradio.org>`_, `Pothos GUI <https://github.com/pothosware/pothos/wiki>`_,
`Lime Suite <https://github.com/myriadrf/LimeSuite>`_ and `Zadig <http://zadig.akeo.ie>`_.
Utilities like ``hackrf_sweep`` and ``rtl_power`` are also included.
2. download QSpectrumAnalyzer installer or portable zip archive from GitHub
`releases <https://github.com/xmikos/qspectrumanalyzer/releases>`_ page
3. after you connect your SDR device, you have to run `Zadig <http://zadig.akeo.ie>`_ to install USB drivers
You can also install QSpectrumAnalyzer manually from `PyPI <https://pypi.python.org>`_:
1. install Python 3.6.x (64-bit) from `python.org <https://www.python.org>`_ and add Python to PATH
2. install `SoapySDR <https://github.com/pothosware/SoapySDR/wiki>`_ libraries and drivers
(bundled as part of Pothos SDR installer: `download <http://downloads.myriadrf.org/builds/PothosSDR/?C=M;O=D>`_)
3. Open ``cmd.exe`` and run::
pip install PyQt5
pip install QSpectrumAnalyzer
You should then be able to run it with ``qspectrumanalyzer`` (or ``python -m qspectrumanalyzer``
if it doesn't work for you).
Todo:
-----
- save FFT history (allow big waterfall plot saved to file)
- automatic peak detection / highlighting
- display average noise level
- frequency markers / bookmarks with notes (even importing and exporting .csv file with
predefined channels, etc.)

View File

@ -1,9 +1,7 @@
[Desktop Entry]
Encoding=UTF-8
Version=1.1
Name=QSpectrumAnalyzer
GenericName=Spectrum analyzer
Comment=Spectrum analyzer for RTL-SDR (GUI for rtl_power based on PyQtGraph)
Comment=Spectrum analyzer for multiple SDR platforms (PyQtGraph based GUI for soapy_power, hackrf_sweep, rtl_power, rx_power and other backends)
Exec=qspectrumanalyzer
Icon=qspectrumanalyzer
StartupNotify=true

BIN
qspectrumanalyzer.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

750
qspectrumanalyzer/__main__.py Executable file → Normal file
View File

@ -1,199 +1,228 @@
#!/usr/bin/env python
import sys, csv, subprocess, signal
import sys, os, signal, time, argparse
import numpy as np
import pyqtgraph as pg
from Qt import QtCore, QtGui, QtWidgets
from PyQt4 import QtCore, QtGui
from qspectrumanalyzer import backends
from qspectrumanalyzer.version import __version__
from qspectrumanalyzer.ui_qspectrumanalyzer_settings import Ui_QSpectrumAnalyzerSettings
from qspectrumanalyzer.data import DataStorage
from qspectrumanalyzer.plot import SpectrumPlotWidget, WaterfallPlotWidget
from qspectrumanalyzer.utils import str_to_color, human_time
from qspectrumanalyzer.settings import QSpectrumAnalyzerSettings
from qspectrumanalyzer.smoothing import QSpectrumAnalyzerSmoothing
from qspectrumanalyzer.persistence import QSpectrumAnalyzerPersistence
from qspectrumanalyzer.colors import QSpectrumAnalyzerColors
from qspectrumanalyzer.baseline import QSpectrumAnalyzerBaseline
from qspectrumanalyzer.ui_qspectrumanalyzer import Ui_QSpectrumAnalyzerMainWindow
# Basic settings
pg.setConfigOptions(antialias=True)
debug = False
# Allow CTRL+C and/or SIGTERM to kill us (PyQt blocks it otherwise)
signal.signal(signal.SIGINT, signal.SIG_DFL)
signal.signal(signal.SIGTERM, signal.SIG_DFL)
class QSpectrumAnalyzerSettings(QtGui.QDialog, Ui_QSpectrumAnalyzerSettings):
"""QSpectrumAnalyzer settings dialog"""
def __init__(self, parent=None):
# Initialize UI
super().__init__(parent)
self.setupUi(self)
# Load settings
settings = QtCore.QSettings()
self.rtlPowerExecutableEdit.setText(str(settings.value("rtl_power_executable") or "rtl_power"))
self.waterfallHistorySizeSpinBox.setValue(int(settings.value("waterfall_history_size") or 100))
@QtCore.pyqtSlot()
def on_rtlPowerExecutableButton_clicked(self):
"""Open file dialog when button is clicked"""
filename = QtGui.QFileDialog.getOpenFileName(self, "QSpectrumAnalyzer - rtl_power executable")
if filename:
self.rtlPowerExecutableEdit.setText(filename)
def accept(self):
"""Save settings when dialog is accepted"""
settings = QtCore.QSettings()
settings.setValue("rtl_power_executable", self.rtlPowerExecutableEdit.text())
settings.setValue("waterfall_history_size", self.waterfallHistorySizeSpinBox.value())
QtGui.QDialog.accept(self)
class QSpectrumAnalyzerMainWindow(QtGui.QMainWindow, Ui_QSpectrumAnalyzerMainWindow):
class QSpectrumAnalyzerMainWindow(QtWidgets.QMainWindow, Ui_QSpectrumAnalyzerMainWindow):
"""QSpectrumAnalyzer main window"""
def __init__(self, parent=None):
# Initialize UI
super().__init__(parent)
self.setupUi(self)
# Setup rtl_power thread and connect signals
self.waterfall_history_size = 100
self.datacounter = 0
self.rtl_power_thread = RtlPowerThread()
self.rtl_power_thread.dataUpdated.connect(self.update_data)
self.rtl_power_thread.rtlPowerStarted.connect(self.update_buttons)
self.rtl_power_thread.rtlPowerStopped.connect(self.update_buttons)
# Set window icon
icon_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "qspectrumanalyzer.svg")
self.setWindowIcon(QtGui.QIcon(icon_path))
# Create progress bar
self.progressbar = QtWidgets.QProgressBar()
self.progressbar.setMaximumWidth(250)
self.progressbar.setVisible(False)
self.statusbar.addPermanentWidget(self.progressbar)
# Create plot widgets and update UI
self.spectrumPlotWidget = SpectrumPlotWidget(self.mainPlotLayout)
self.waterfallPlotWidget = WaterfallPlotWidget(self.waterfallPlotLayout, self.histogramPlotLayout)
# Link main spectrum plot to waterfall plot
self.spectrumPlotWidget.plot.setXLink(self.waterfallPlotWidget.plot)
# Setup power thread and connect signals
self.update_status_timer = QtCore.QTimer()
self.update_status_timer.timeout.connect(self.update_status)
self.prev_sweep_time = None
self.prev_data_timestamp = None
self.start_timestamp = None
self.data_storage = None
self.power_thread = None
self.backend = None
self.setup_power_thread()
# Update UI
self.create_plot()
self.create_waterfall()
self.update_buttons()
self.load_settings()
def create_plot(self):
"""Create main spectrum plot"""
self.posLabel = self.mainPlotLayout.addLabel(row=0, col=0, justify="right")
self.mainPlotWidget = self.mainPlotLayout.addPlot(row=1, col=0)
self.mainPlotWidget.showGrid(x=True, y=True)
self.mainPlotWidget.setLabel("left", "Power", units="dBm")
self.mainPlotWidget.setLabel("bottom", "Frequency", units="Hz")
self.mainPlotWidget.setLimits(xMin=0)
self.mainPlotWidget.showButtons()
def setup_power_thread(self):
"""Create power_thread and connect signals to slots"""
if self.power_thread:
self.stop()
# Create spectrum curve
self.curve = self.mainPlotWidget.plot()
settings = QtCore.QSettings()
self.data_storage = DataStorage(max_history_size=settings.value("waterfall_history_size", 100, int))
self.data_storage.data_updated.connect(self.update_data)
self.data_storage.data_updated.connect(self.spectrumPlotWidget.update_plot)
self.data_storage.data_updated.connect(self.spectrumPlotWidget.update_persistence)
self.data_storage.data_recalculated.connect(self.spectrumPlotWidget.recalculate_plot)
self.data_storage.data_recalculated.connect(self.spectrumPlotWidget.recalculate_persistence)
self.data_storage.history_updated.connect(self.waterfallPlotWidget.update_plot)
self.data_storage.history_recalculated.connect(self.waterfallPlotWidget.recalculate_plot)
self.data_storage.average_updated.connect(self.spectrumPlotWidget.update_average)
self.data_storage.baseline_updated.connect(self.spectrumPlotWidget.update_baseline)
self.data_storage.peak_hold_max_updated.connect(self.spectrumPlotWidget.update_peak_hold_max)
self.data_storage.peak_hold_min_updated.connect(self.spectrumPlotWidget.update_peak_hold_min)
# Create crosshair
self.vLine = pg.InfiniteLine(angle=90, movable=False)
self.hLine = pg.InfiniteLine(angle=0, movable=False)
self.mainPlotWidget.addItem(self.vLine, ignoreBounds=True)
self.mainPlotWidget.addItem(self.hLine, ignoreBounds=True)
self.mouseProxy = pg.SignalProxy(self.mainPlotWidget.scene().sigMouseMoved,
rateLimit=60, slot=self.mouse_moved)
# Setup default values and limits in case that backend is changed
backend = settings.value("backend", "soapy_power")
try:
backend_module = getattr(backends, backend)
except AttributeError:
backend_module = backends.soapy_power
def mouse_moved(self, evt):
"""Update crosshair when mouse is moved"""
pos = evt[0]
if self.mainPlotWidget.sceneBoundingRect().contains(pos):
mousePoint = self.mainPlotWidget.vb.mapSceneToView(pos)
self.posLabel.setText(
"<span style='font-size: 12pt'>f={:0.3f} MHz, P={:0.3f} dBm</span>".format(mousePoint.x() / 1e6,
mousePoint.y())
)
self.vLine.setPos(mousePoint.x())
self.hLine.setPos(mousePoint.y())
if self.backend is None or backend != self.backend:
self.backend = backend
self.gainSpinBox.setMinimum(backend_module.Info.gain_min)
self.gainSpinBox.setMaximum(backend_module.Info.gain_max)
self.gainSpinBox.setValue(backend_module.Info.gain)
self.startFreqSpinBox.setMinimum(backend_module.Info.start_freq_min)
self.startFreqSpinBox.setMaximum(backend_module.Info.start_freq_max)
self.startFreqSpinBox.setValue(backend_module.Info.start_freq)
self.stopFreqSpinBox.setMinimum(backend_module.Info.stop_freq_min)
self.stopFreqSpinBox.setMaximum(backend_module.Info.stop_freq_max)
self.stopFreqSpinBox.setValue(backend_module.Info.stop_freq)
self.binSizeSpinBox.setMinimum(backend_module.Info.bin_size_min)
self.binSizeSpinBox.setMaximum(backend_module.Info.bin_size_max)
self.binSizeSpinBox.setValue(backend_module.Info.bin_size)
self.intervalSpinBox.setMinimum(backend_module.Info.interval_min)
self.intervalSpinBox.setMaximum(backend_module.Info.interval_max)
self.intervalSpinBox.setValue(backend_module.Info.interval)
self.ppmSpinBox.setMinimum(backend_module.Info.ppm_min)
self.ppmSpinBox.setMaximum(backend_module.Info.ppm_max)
self.ppmSpinBox.setValue(backend_module.Info.ppm)
self.cropSpinBox.setMinimum(backend_module.Info.crop_min)
self.cropSpinBox.setMaximum(backend_module.Info.crop_max)
self.cropSpinBox.setValue(backend_module.Info.crop)
def create_waterfall(self):
"""Create waterfall plot"""
self.waterfallPlotWidget = self.waterfallPlotLayout.addPlot()
self.waterfallPlotLayout.addItem(self.waterfallPlotWidget)
self.waterfallPlotWidget.setLabel("bottom", "Frequency", units="Hz")
self.waterfallPlotWidget.setLabel("left", "Time")
self.waterfallPlotWidget.setYRange(-self.waterfall_history_size, 0)
self.waterfallPlotWidget.setLimits(xMin=0, yMax=0)
self.waterfallPlotWidget.showButtons()
#self.waterfallPlotWidget.setAspectLocked(True)
#self.waterfallPlotWidget.setDownsampling(mode="peak")
#self.waterfallPlotWidget.setClipToView(True)
# Setup default values and limits in case that LNB LO is changed
lnb_lo = settings.value("lnb_lo", 0, float) / 1e6
# Link waterfall plot to main plot
self.mainPlotWidget.setXLink(self.waterfallPlotWidget)
start_freq_min = backend_module.Info.start_freq_min + lnb_lo
start_freq_max = backend_module.Info.start_freq_max + lnb_lo
start_freq = self.startFreqSpinBox.value()
stop_freq_min = backend_module.Info.stop_freq_min + lnb_lo
stop_freq_max = backend_module.Info.stop_freq_max + lnb_lo
stop_freq = self.stopFreqSpinBox.value()
# Setup histogram widget (for controlling waterfall plot levels and gradients)
self.waterfallHistogram = pg.HistogramLUTItem()
self.histogramPlotLayout.addItem(self.waterfallHistogram)
self.waterfallHistogram.gradient.loadPreset("flame")
#self.waterfallHistogram.setHistogramRange(-50, 0)
#self.waterfallHistogram.setLevels(-50, 0)
self.startFreqSpinBox.setMinimum(start_freq_min if start_freq_min > 0 else 0)
self.startFreqSpinBox.setMaximum(start_freq_max)
if start_freq < start_freq_min or start_freq > start_freq_max:
self.startFreqSpinBox.setValue(start_freq_min)
def update_buttons(self):
"""Update state of control buttons"""
self.startButton.setEnabled(not self.rtl_power_thread.alive)
self.singleShotButton.setEnabled(not self.rtl_power_thread.alive)
self.stopButton.setEnabled(self.rtl_power_thread.alive)
self.stopFreqSpinBox.setMinimum(stop_freq_min if stop_freq_min > 0 else 0)
self.stopFreqSpinBox.setMaximum(stop_freq_max)
if stop_freq < stop_freq_min or stop_freq > stop_freq_max:
self.stopFreqSpinBox.setValue(stop_freq_max)
def update_data(self, data):
"""Update plots when new data is received"""
self.datacounter += 1
self.update_plot(data)
self.update_waterfall(data)
self.power_thread = backend_module.PowerThread(self.data_storage)
self.power_thread.powerThreadStarted.connect(self.on_power_thread_started)
self.power_thread.powerThreadStopped.connect(self.on_power_thread_stopped)
def update_plot(self, data):
"""Update main spectrum plot"""
self.curve.setData(data["x"], data["y"])
def set_dock_size(self, dock, width, height):
"""Ugly hack for resizing QDockWidget (because it doesn't respect minimumSize / sizePolicy set in Designer)
Link: https://stackoverflow.com/questions/2722939/c-resize-a-docked-qt-qdockwidget-programmatically"""
old_min_size = dock.minimumSize()
old_max_size = dock.maximumSize()
def update_waterfall(self, data):
"""Update waterfall plot"""
# Create waterfall data array and waterfall image on first run
if self.datacounter == 1:
self.waterfallImgArray = np.zeros((self.waterfall_history_size, len(data["x"])))
self.waterfallImg = pg.ImageItem()
self.waterfallImg.scale((data["x"][-1] - data["x"][0]) / len(data["x"]), 1)
self.waterfallPlotWidget.clear()
self.waterfallPlotWidget.addItem(self.waterfallImg)
if width >= 0:
if dock.width() < width:
dock.setMinimumWidth(width)
else:
dock.setMaximumWidth(width)
# Roll down one and replace leading edge with new data
self.waterfallImgArray = np.roll(self.waterfallImgArray, -1, axis=0)
self.waterfallImgArray[-1] = data["y"]
self.waterfallImg.setImage(self.waterfallImgArray[-self.datacounter:].T,
autoLevels=False, autoRange=False)
if height >= 0:
if dock.height() < height:
dock.setMinimumHeight(height)
else:
dock.setMaximumHeight(height)
# Move waterfall image to always start at 0
self.waterfallImg.setPos(data["x"][0],
-self.datacounter if self.datacounter < self.waterfall_history_size
else -self.waterfall_history_size)
QtCore.QTimer.singleShot(0, lambda: self.set_dock_size_callback(dock, old_min_size, old_max_size))
# Link histogram widget to waterfall image on first run
# (must be done after first data is received or else levels would be wrong)
if self.datacounter == 1:
self.waterfallHistogram.setImageItem(self.waterfallImg)
def set_dock_size_callback(self, dock, old_min_size, old_max_size):
"""Return to original QDockWidget minimumSize and maximumSize after running set_dock_size()"""
dock.setMinimumSize(old_min_size)
dock.setMaximumSize(old_max_size)
def load_settings(self):
"""Restore spectrum analyzer settings and window geometry"""
settings = QtCore.QSettings()
self.startFreqSpinBox.setValue(float(settings.value("start_freq") or 87.0))
self.stopFreqSpinBox.setValue(float(settings.value("stop_freq") or 108.0))
self.binSizeSpinBox.setValue(float(settings.value("bin_size") or 10.0))
self.intervalSpinBox.setValue(int(settings.value("interval") or 10))
self.gainSpinBox.setValue(int(settings.value("gain") or 0))
self.ppmSpinBox.setValue(int(settings.value("ppm") or 0))
self.cropSpinBox.setValue(int(settings.value("crop") or 0))
self.startFreqSpinBox.setValue(settings.value("start_freq", 87.0, float))
self.stopFreqSpinBox.setValue(settings.value("stop_freq", 108.0, float))
self.binSizeSpinBox.setValue(settings.value("bin_size", 10.0, float))
self.intervalSpinBox.setValue(settings.value("interval", 10.0, float))
self.gainSpinBox.setValue(settings.value("gain", 0, float))
self.ppmSpinBox.setValue(settings.value("ppm", 0, int))
self.cropSpinBox.setValue(settings.value("crop", 0, int))
self.mainCurveCheckBox.setChecked(settings.value("main_curve", 1, int))
self.peakHoldMaxCheckBox.setChecked(settings.value("peak_hold_max", 0, int))
self.peakHoldMinCheckBox.setChecked(settings.value("peak_hold_min", 0, int))
self.averageCheckBox.setChecked(settings.value("average", 0, int))
self.smoothCheckBox.setChecked(settings.value("smooth", 0, int))
self.persistenceCheckBox.setChecked(settings.value("persistence", 0, int))
self.baselineCheckBox.setChecked(settings.value("baseline", 0, int))
self.subtractBaselineCheckBox.setChecked(settings.value("subtract_baseline", 0, int))
if settings.value("window_geometry"):
self.restoreGeometry(settings.value("window_geometry"))
# Restore window state
if settings.value("window_state"):
self.restoreState(settings.value("window_state"))
if settings.value("plotsplitter_state"):
self.plotSplitter.restoreState(settings.value("plotsplitter_state"))
# Migration from older version of config file
if settings.value("config_version", 1, int) < 2:
# Make tabs from docks when started for first time
self.tabifyDockWidget(self.settingsDockWidget, self.levelsDockWidget)
self.settingsDockWidget.raise_()
self.set_dock_size(self.controlsDockWidget, 0, 0)
self.set_dock_size(self.frequencyDockWidget, 0, 0)
# Update config version
settings.setValue("config_version", 2)
# Window geometry has to be restored only after show(), because initial
# maximization doesn't work otherwise (at least not in some window managers on X11)
self.show()
if settings.value("window_geometry"):
self.restoreGeometry(settings.value("window_geometry"))
def save_settings(self):
"""Save spectrum analyzer settings and window geometry"""
settings = QtCore.QSettings()
settings.setValue("start_freq", float(self.startFreqSpinBox.value()))
settings.setValue("stop_freq", float(self.stopFreqSpinBox.value()))
settings.setValue("bin_size", float(self.binSizeSpinBox.value()))
settings.setValue("interval", int(self.intervalSpinBox.value()))
settings.setValue("gain", int(self.gainSpinBox.value()))
settings.setValue("ppm", int(self.ppmSpinBox.value()))
settings.setValue("crop", int(self.cropSpinBox.value()))
settings.setValue("start_freq", self.startFreqSpinBox.value())
settings.setValue("stop_freq", self.stopFreqSpinBox.value())
settings.setValue("bin_size", self.binSizeSpinBox.value())
settings.setValue("interval", self.intervalSpinBox.value())
settings.setValue("gain", self.gainSpinBox.value())
settings.setValue("ppm", self.ppmSpinBox.value())
settings.setValue("crop", self.cropSpinBox.value())
settings.setValue("main_curve", int(self.mainCurveCheckBox.isChecked()))
settings.setValue("peak_hold_max", int(self.peakHoldMaxCheckBox.isChecked()))
settings.setValue("peak_hold_min", int(self.peakHoldMinCheckBox.isChecked()))
settings.setValue("average", int(self.averageCheckBox.isChecked()))
settings.setValue("smooth", int(self.smoothCheckBox.isChecked()))
settings.setValue("persistence", int(self.persistenceCheckBox.isChecked()))
settings.setValue("baseline", int(self.baselineCheckBox.isChecked()))
settings.setValue("subtract_baseline", int(self.subtractBaselineCheckBox.isChecked()))
# Save window state and geometry
settings.setValue("window_geometry", self.saveGeometry())
settings.setValue("window_state", self.saveState())
settings.setValue("plotsplitter_state", self.plotSplitter.saveState())
@ -202,184 +231,313 @@ class QSpectrumAnalyzerMainWindow(QtGui.QMainWindow, Ui_QSpectrumAnalyzerMainWin
"""Show message in status bar"""
self.statusbar.showMessage(message, timeout)
def update_buttons(self):
"""Update state of control buttons"""
self.startButton.setEnabled(not self.power_thread.alive)
self.singleShotButton.setEnabled(not self.power_thread.alive)
self.stopButton.setEnabled(self.power_thread.alive)
def update_data(self, data_storage):
"""Update GUI when new data is received"""
timestamp = time.time()
self.prev_sweep_time = timestamp - self.prev_data_timestamp
self.prev_data_timestamp = timestamp
self.update_status()
def update_status(self):
"""Update status bar"""
timestamp = time.time()
status = []
if self.power_thread.params["hops"]:
status.append(self.tr("Frequency hops: {}").format(self.power_thread.params["hops"]))
status.append(self.tr("Total time: {} | Sweep time: {:.2f} s ({:.2f} FPS)").format(
human_time(timestamp - self.start_timestamp),
self.prev_sweep_time,
(1 / self.prev_sweep_time) if self.prev_sweep_time else 0
))
self.show_status(" | ".join(status), timeout=0)
self.update_progress(timestamp - self.prev_data_timestamp)
def update_progress(self, value):
"""Update progress bar"""
value *= 1000
value_max = self.intervalSpinBox.value() * 1000
if value_max < 1000:
return
if value > value_max + 1000:
self.progressbar.setRange(0, 0)
value = value_max
elif value > value_max:
value = value_max
else:
self.progressbar.setRange(0, value_max)
self.progressbar.setValue(value)
def on_power_thread_started(self):
"""Update buttons state when power thread is started"""
self.update_buttons()
self.progressbar.setVisible(True)
def on_power_thread_stopped(self):
"""Update buttons state and status bar when power thread is stopped"""
self.update_buttons()
self.update_status_timer.stop()
self.update_status()
self.progressbar.setVisible(False)
def start(self, single_shot=False):
"""Start rtl_power thread"""
"""Start power thread"""
settings = QtCore.QSettings()
self.waterfall_history_size = int(settings.value("waterfall_history_size") or 100)
self.datacounter = 0
if not self.rtl_power_thread.alive:
self.rtl_power_thread.setup(float(self.startFreqSpinBox.value()),
float(self.stopFreqSpinBox.value()),
float(self.binSizeSpinBox.value()),
interval=int(self.intervalSpinBox.value()),
gain=int(self.gainSpinBox.value()),
ppm=int(self.ppmSpinBox.value()),
crop=int(self.cropSpinBox.value()) / 100.0,
single_shot=single_shot)
self.rtl_power_thread.start()
self.prev_sweep_time = 0
self.prev_data_timestamp = time.time()
self.start_timestamp = self.prev_data_timestamp
if self.intervalSpinBox.value() >= 1:
self.progressbar.setRange(0, self.intervalSpinBox.value() * 1000)
else:
self.progressbar.setRange(0, 0)
self.update_progress(0)
self.update_status_timer.start(100)
self.waterfallPlotWidget.history_size = settings.value("waterfall_history_size", 100, int)
self.waterfallPlotWidget.clear_plot()
self.spectrumPlotWidget.main_curve = bool(self.mainCurveCheckBox.isChecked())
self.spectrumPlotWidget.main_color = str_to_color(settings.value("main_color", "255, 255, 0, 255"))
self.spectrumPlotWidget.peak_hold_max = bool(self.peakHoldMaxCheckBox.isChecked())
self.spectrumPlotWidget.peak_hold_max_color = str_to_color(settings.value("peak_hold_max_color", "255, 0, 0, 255"))
self.spectrumPlotWidget.peak_hold_min = bool(self.peakHoldMinCheckBox.isChecked())
self.spectrumPlotWidget.peak_hold_min_color = str_to_color(settings.value("peak_hold_min_color", "0, 0, 255, 255"))
self.spectrumPlotWidget.average = bool(self.averageCheckBox.isChecked())
self.spectrumPlotWidget.average_color = str_to_color(settings.value("average_color", "0, 255, 255, 255"))
self.spectrumPlotWidget.baseline = bool(self.baselineCheckBox.isChecked())
self.spectrumPlotWidget.baseline_color = str_to_color(settings.value("baseline_color", "255, 0, 255, 255"))
self.spectrumPlotWidget.persistence = bool(self.persistenceCheckBox.isChecked())
self.spectrumPlotWidget.persistence_length = settings.value("persistence_length", 5, int)
self.spectrumPlotWidget.persistence_decay = settings.value("persistence_decay", "exponential")
self.spectrumPlotWidget.persistence_color = str_to_color(settings.value("persistence_color", "0, 255, 0, 255"))
self.spectrumPlotWidget.clear_plot()
self.spectrumPlotWidget.clear_peak_hold_max()
self.spectrumPlotWidget.clear_peak_hold_min()
self.spectrumPlotWidget.clear_average()
self.spectrumPlotWidget.clear_baseline()
self.spectrumPlotWidget.clear_persistence()
self.data_storage.reset()
self.data_storage.set_smooth(
bool(self.smoothCheckBox.isChecked()),
settings.value("smooth_length", 11, int),
settings.value("smooth_window", "hanning")
)
self.data_storage.set_subtract_baseline(
bool(self.subtractBaselineCheckBox.isChecked()),
settings.value("baseline_file", None)
)
if not self.power_thread.alive:
self.power_thread.setup(
float(self.startFreqSpinBox.value()),
float(self.stopFreqSpinBox.value()),
float(self.binSizeSpinBox.value()),
interval=float(self.intervalSpinBox.value()),
gain=float(self.gainSpinBox.value()),
ppm=int(self.ppmSpinBox.value()),
crop=int(self.cropSpinBox.value()) / 100.0,
single_shot=single_shot,
device=settings.value("device", ""),
sample_rate=settings.value("sample_rate", 2560000, float),
bandwidth=settings.value("bandwidth", 0, float),
lnb_lo=settings.value("lnb_lo", 0, float)
)
self.power_thread.start()
def stop(self):
"""Stop rtl_power thread"""
if self.rtl_power_thread.alive:
self.rtl_power_thread.stop()
"""Stop power thread"""
if self.power_thread.alive:
self.power_thread.stop()
@QtCore.pyqtSlot()
@QtCore.Slot()
def on_startButton_clicked(self):
self.start()
@QtCore.pyqtSlot()
@QtCore.Slot()
def on_singleShotButton_clicked(self):
self.start(single_shot=True)
@QtCore.pyqtSlot()
@QtCore.Slot()
def on_stopButton_clicked(self):
self.stop()
@QtCore.pyqtSlot()
@QtCore.Slot(bool)
def on_mainCurveCheckBox_toggled(self, checked):
self.spectrumPlotWidget.main_curve = checked
if self.spectrumPlotWidget.curve.xData is None:
self.spectrumPlotWidget.update_plot(self.data_storage)
self.spectrumPlotWidget.curve.setVisible(checked)
@QtCore.Slot(bool)
def on_peakHoldMaxCheckBox_toggled(self, checked):
self.spectrumPlotWidget.peak_hold_max = checked
if self.spectrumPlotWidget.curve_peak_hold_max.xData is None:
self.spectrumPlotWidget.update_peak_hold_max(self.data_storage)
self.spectrumPlotWidget.curve_peak_hold_max.setVisible(checked)
@QtCore.Slot(bool)
def on_peakHoldMinCheckBox_toggled(self, checked):
self.spectrumPlotWidget.peak_hold_min = checked
if self.spectrumPlotWidget.curve_peak_hold_min.xData is None:
self.spectrumPlotWidget.update_peak_hold_min(self.data_storage)
self.spectrumPlotWidget.curve_peak_hold_min.setVisible(checked)
@QtCore.Slot(bool)
def on_averageCheckBox_toggled(self, checked):
self.spectrumPlotWidget.average = checked
if self.spectrumPlotWidget.curve_average.xData is None:
self.spectrumPlotWidget.update_average(self.data_storage)
self.spectrumPlotWidget.curve_average.setVisible(checked)
@QtCore.Slot(bool)
def on_persistenceCheckBox_toggled(self, checked):
self.spectrumPlotWidget.persistence = checked
if self.spectrumPlotWidget.persistence_curves[0].xData is None:
self.spectrumPlotWidget.recalculate_persistence(self.data_storage)
for curve in self.spectrumPlotWidget.persistence_curves:
curve.setVisible(checked)
@QtCore.Slot(bool)
def on_smoothCheckBox_toggled(self, checked):
settings = QtCore.QSettings()
self.data_storage.set_smooth(
checked,
settings.value("smooth_length", 11, int),
settings.value("smooth_window", "hanning")
)
@QtCore.Slot(bool)
def on_baselineCheckBox_toggled(self, checked):
self.spectrumPlotWidget.baseline = checked
if self.spectrumPlotWidget.curve_baseline.xData is None:
self.spectrumPlotWidget.update_baseline(self.data_storage)
self.spectrumPlotWidget.curve_baseline.setVisible(checked)
@QtCore.Slot(bool)
def on_subtractBaselineCheckBox_toggled(self, checked):
settings = QtCore.QSettings()
self.data_storage.set_subtract_baseline(
checked,
settings.value("baseline_file", None)
)
@QtCore.Slot()
def on_baselineButton_clicked(self):
dialog = QSpectrumAnalyzerBaseline(self)
if dialog.exec_():
settings = QtCore.QSettings()
self.data_storage.set_subtract_baseline(
bool(self.subtractBaselineCheckBox.isChecked()),
settings.value("baseline_file", None)
)
@QtCore.Slot()
def on_smoothButton_clicked(self):
dialog = QSpectrumAnalyzerSmoothing(self)
if dialog.exec_():
settings = QtCore.QSettings()
self.data_storage.set_smooth(
bool(self.smoothCheckBox.isChecked()),
settings.value("smooth_length", 11, int),
settings.value("smooth_window", "hanning")
)
@QtCore.Slot()
def on_persistenceButton_clicked(self):
prev_persistence_length = self.spectrumPlotWidget.persistence_length
dialog = QSpectrumAnalyzerPersistence(self)
if dialog.exec_():
settings = QtCore.QSettings()
persistence_length = settings.value("persistence_length", 5, int)
self.spectrumPlotWidget.persistence_length = persistence_length
self.spectrumPlotWidget.persistence_decay = settings.value("persistence_decay", "exponential")
# If only decay function has been changed, just reset colors
if persistence_length == prev_persistence_length:
self.spectrumPlotWidget.set_colors()
else:
self.spectrumPlotWidget.recalculate_persistence(self.data_storage)
@QtCore.Slot()
def on_colorsButton_clicked(self):
dialog = QSpectrumAnalyzerColors(self)
if dialog.exec_():
settings = QtCore.QSettings()
self.spectrumPlotWidget.main_color = str_to_color(settings.value("main_color", "255, 255, 0, 255"))
self.spectrumPlotWidget.peak_hold_max_color = str_to_color(settings.value("peak_hold_max_color", "255, 0, 0, 255"))
self.spectrumPlotWidget.peak_hold_min_color = str_to_color(settings.value("peak_hold_min_color", "0, 0, 255, 255"))
self.spectrumPlotWidget.average_color = str_to_color(settings.value("average_color", "0, 255, 255, 255"))
self.spectrumPlotWidget.persistence_color = str_to_color(settings.value("persistence_color", "0, 255, 0, 255"))
self.spectrumPlotWidget.baseline_color = str_to_color(settings.value("baseline_color", "255, 0, 255, 255"))
self.spectrumPlotWidget.set_colors()
@QtCore.Slot()
def on_action_Settings_triggered(self):
dialog = QSpectrumAnalyzerSettings(self)
dialog.exec_()
if dialog.exec_():
self.setup_power_thread()
@QtCore.pyqtSlot()
@QtCore.Slot()
def on_action_About_triggered(self):
QtGui.QMessageBox.information(self, self.tr("About"), self.tr("QSpectrumAnalyzer {}").format(__version__))
QtWidgets.QMessageBox.information(self, self.tr("About - QSpectrumAnalyzer"),
self.tr("QSpectrumAnalyzer {}").format(__version__))
@QtCore.pyqtSlot()
@QtCore.Slot()
def on_action_Quit_triggered(self):
self.close()
def closeEvent(self, event):
"""Save settings when main window is closed"""
self.rtl_power_thread.stop()
self.stop()
self.save_settings()
class RtlPowerThread(QtCore.QThread):
"""Thread which runs rtl_power process"""
dataUpdated = QtCore.pyqtSignal(object)
rtlPowerStarted = QtCore.pyqtSignal()
rtlPowerStopped = QtCore.pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self.alive = False
self.process = None
self.params = {}
self.databuffer = {}
self.last_timestamp = ""
def stop(self):
"""Stop rtl_power thread"""
self.process_stop()
self.alive = False
self.wait()
def setup(self, start_freq, stop_freq, bin_size, interval=10,
gain=-1, ppm=0, crop=0, single_shot=False):
"""Setup rtl_power params"""
self.params = {
"start_freq": start_freq,
"stop_freq": stop_freq,
"bin_size": bin_size,
"interval": interval,
"gain": gain,
"ppm": ppm,
"crop": crop,
"single_shot": single_shot
}
def process_stop(self):
"""Terminate rtl_power process"""
if self.process:
try:
self.process.terminate()
except ProcessLookupError:
pass
self.process.wait()
self.process = None
def process_start(self):
"""Start rtl_power process"""
if not self.process and self.params:
settings = QtCore.QSettings()
cmdline = [
str(settings.value("rtl_power_executable") or "rtl_power"),
"-f", "{}M:{}M:{}k".format(self.params["start_freq"],
self.params["stop_freq"],
self.params["bin_size"]),
"-i", "{}".format(self.params["interval"]),
"-p", "{}".format(self.params["ppm"]),
"-c", "{}".format(self.params["crop"])
]
if self.params["gain"] >= 0:
cmdline.extend(["-g", "{}".format(self.params["gain"])])
if self.params["single_shot"]:
cmdline.append("-1")
self.process = subprocess.Popen(cmdline, stdout=subprocess.PIPE,
universal_newlines=True)
def parse_output(self, line):
"""Parse one preprocessed line of output from rtl_power"""
timestamp = " ".join(line[:2])
start_freq = int(line[2])
stop_freq = int(line[3])
step = float(line[4])
samples = float(line[5])
x_axis = list(np.arange(start_freq, stop_freq, step))
y_axis = [float(y) for y in line[6:]]
if len(x_axis) != len(y_axis):
print("ERROR: len(x_axis) != len(y_axis), use newer version of rtl_power!")
if len(x_axis) > len(y_axis):
print("Trimming x_axis...")
x_axis = x_axis[:len(y_axis)]
else:
print("Trimming y_axis...")
y_axis = y_axis[:len(x_axis)]
if timestamp != self.last_timestamp:
self.last_timestamp = timestamp
self.databuffer = {"timestamp": timestamp,
"x": x_axis,
"y": y_axis}
else:
self.databuffer["x"].extend(x_axis)
self.databuffer["y"].extend(y_axis)
# This have to be stupid like this to be compatible with old broken version of rtl_power. Right way is:
# if stop_freq == self.params["stop_freq"] * 1e6:
if stop_freq > (self.params["stop_freq"] * 1e6) - step:
self.dataUpdated.emit(self.databuffer)
return self.databuffer
def run(self):
"""Rtl_power thread main loop"""
self.process_start()
self.alive = True
self.rtlPowerStarted.emit()
reader = csv.reader(self.process.stdout, skipinitialspace=True,
delimiter=",", quoting=csv.QUOTE_NONE)
for line in reader:
if not self.alive:
break
self.parse_output(line)
self.process_stop()
self.alive = False
self.rtlPowerStopped.emit()
def main():
app = QtGui.QApplication(sys.argv)
app.setOrganizationName("QSpectrumAnalyzer")
app.setOrganizationDomain("qspectrumanalyzer.eutopia.cz")
app.setApplicationName("QSpectrumAnalyzer")
window = QSpectrumAnalyzerMainWindow()
window.show()
sys.exit(app.exec_())
global debug
# Parse command line arguments
parser = argparse.ArgumentParser(
prog="qspectrumanalyzer",
description="Spectrum analyzer for multiple SDR platforms",
)
parser.add_argument("--debug", action="store_true",
help="detailed debugging messages")
parser.add_argument("--version", action="version",
version="%(prog)s {}".format(__version__))
args, unparsed_args = parser.parse_known_args()
debug = args.debug
try:
# Hide console window on Windows
if sys.platform == 'win32' and not debug:
from qspectrumanalyzer import windows
windows.set_attached_console_visible(False)
# Start PyQt application
app = QtWidgets.QApplication(sys.argv[:1] + unparsed_args)
app.setOrganizationName("QSpectrumAnalyzer")
app.setOrganizationDomain("qspectrumanalyzer.eutopia.cz")
app.setApplicationName("QSpectrumAnalyzer")
window = QSpectrumAnalyzerMainWindow()
sys.exit(app.exec_())
finally:
# Unhide console window on Windows (we don't want to leave zombies behind)
if sys.platform == 'win32' and not debug:
windows.set_attached_console_visible(True)
if __name__ == "__main__":

View File

@ -0,0 +1,117 @@
import os, threading, shlex
from Qt import QtCore
from qspectrumanalyzer import subprocess
class BaseInfo:
"""Default device metadata"""
sample_rate_min = 0
sample_rate_max = 3200000
sample_rate = 2560000
bandwidth_min = 0
bandwidth_max = 0
bandwidth = 0
gain_min = -1
gain_max = 49.6
gain = 37
start_freq_min = 0
start_freq_max = 2200
start_freq = 87
stop_freq_min = 0
stop_freq_max = 2200
stop_freq = 108
bin_size_min = 0
bin_size_max = 2800
bin_size = 10
interval_min = 0
interval_max = 3600
interval = 1
ppm_min = -999
ppm_max = 999
ppm = 0
crop_min = 0
crop_max = 99
crop = 0
additional_params = ''
help_device = None
@classmethod
def help_params(cls, executable):
cmdline = shlex.split(executable)
try:
text = subprocess.check_output(cmdline + ['-h'], universal_newlines=True,
stderr=subprocess.STDOUT, env=dict(os.environ, COLUMNS='125'),
console=False)
except subprocess.CalledProcessError as e:
text = e.output
except OSError:
text = '{} executable not found!'.format(executable)
return text
class BasePowerThread(QtCore.QThread):
"""Thread which runs Power Spectral Density acquisition and calculation process"""
powerThreadStarted = QtCore.Signal()
powerThreadStopped = QtCore.Signal()
def __init__(self, data_storage, parent=None):
super().__init__(parent)
self.data_storage = data_storage
self.alive = False
self.process = None
self._shutdown_lock = threading.Lock()
def stop(self):
"""Stop power process thread"""
self.process_stop()
self.alive = False
self.wait()
def setup(self, start_freq, stop_freq, bin_size, interval=10.0, gain=-1, ppm=0, crop=0,
single_shot=False, device=0, sample_rate=2560000, bandwidth=0, lnb_lo=0):
"""Setup power process params"""
raise NotImplementedError
def process_start(self):
"""Start power process"""
raise NotImplementedError
def process_stop(self):
"""Terminate power process"""
with self._shutdown_lock:
if self.process:
if self.process.poll() is None:
try:
self.process.terminate()
except ProcessLookupError:
pass
self.process.wait()
self.process = None
def parse_output(self, line):
"""Parse one line of output from power process"""
raise NotImplementedError
def run(self):
"""Power process thread main loop"""
self.process_start()
self.alive = True
self.powerThreadStarted.emit()
for line in self.process.stdout:
if not self.alive:
break
self.parse_output(line)
self.process_stop()
self.alive = False
self.powerThreadStopped.emit()
# Build list of all backends
__all__ = ['soapy_power', 'hackrf_sweep', 'rtl_power', 'rtl_power_fftw', 'rx_power']
# Import all backends
from qspectrumanalyzer.backends import soapy_power, hackrf_sweep, rtl_power, rtl_power_fftw, rx_power

View File

@ -0,0 +1,171 @@
import struct, shlex, sys, time
import numpy as np
from Qt import QtCore
from qspectrumanalyzer import subprocess
from qspectrumanalyzer.backends import BaseInfo, BasePowerThread
class Info(BaseInfo):
"""hackrf_sweep device metadata"""
sample_rate_min = 20000000
sample_rate_max = 20000000
sample_rate = 20000000
gain_min = -1
gain_max = 102
gain = 40
start_freq_min = 0
start_freq_max = 7230
start_freq = 0
stop_freq_min = 0
stop_freq_max = 7250
stop_freq = 6000
bin_size_min = 3
bin_size_max = 5000
bin_size = 1000
interval = 0
ppm_min = 0
ppm_max = 0
ppm = 0
crop_min = 0
crop_max = 0
crop = 0
class PowerThread(BasePowerThread):
"""Thread which runs hackrf_sweep process"""
def setup(self, start_freq=0, stop_freq=6000, bin_size=1000,
interval=0.0, gain=40, ppm=0, crop=0, single_shot=False,
device=0, sample_rate=20000000, bandwidth=0, lnb_lo=0):
"""Setup hackrf_sweep params"""
# Small bin sizes (<40 kHz) are only suitable with an arbitrarily
# reduced sweep interval. Bin sizes smaller than 3 kHz showed to be
# infeasible also in these cases.
if bin_size < 3:
bin_size = 3
if bin_size > 5000:
bin_size = 5000
# We only support whole numbers of steps with bandwidth equal to the
# sample rate.
step_bandwidth = sample_rate / 1000000
total_bandwidth = stop_freq - start_freq
step_count = 1 + (total_bandwidth - 1) // step_bandwidth
total_bandwidth = step_count * step_bandwidth
stop_freq = start_freq + total_bandwidth
# distribute gain between two analog gain stages
if gain > 102:
gain = 102
lna_gain = 8 * (gain // 18) if gain >= 0 else 0
vga_gain = 2 * ((gain - lna_gain) // 2) if gain >= 0 else 0
self.params = {
"start_freq": start_freq, # MHz
"stop_freq": stop_freq, # MHz
"hops": 0,
"device": 0,
"sample_rate": 20e6, # sps
"bin_size": bin_size, # kHz
"interval": interval, # seconds
"gain": gain,
"lna_gain": lna_gain,
"vga_gain": vga_gain,
"ppm": 0,
"crop": 0,
"single_shot": single_shot
}
self.lnb_lo = lnb_lo
self.databuffer = {"timestamp": [], "x": [], "y": []}
self.lastsweep = 0
self.interval = interval
def process_start(self):
"""Start hackrf_sweep process"""
if not self.process and self.params:
settings = QtCore.QSettings()
cmdline = shlex.split(settings.value("executable", "hackrf_sweep"))
cmdline.extend([
"-f", "{}:{}".format(int(self.params["start_freq"] - self.lnb_lo / 1e6),
int(self.params["stop_freq"] - self.lnb_lo / 1e6)),
"-B",
"-w", "{}".format(int(self.params["bin_size"] * 1000)),
])
if self.params["gain"] >= 0:
cmdline.extend([
"-l", "{}".format(int(self.params["lna_gain"])),
"-g", "{}".format(int(self.params["vga_gain"])),
])
if self.params["single_shot"]:
cmdline.append("-1")
additional_params = settings.value("params", Info.additional_params)
if additional_params:
cmdline.extend(shlex.split(additional_params))
print('Starting backend:')
print(' '.join(cmdline))
print()
self.process = subprocess.Popen(cmdline, stdout=subprocess.PIPE,
universal_newlines=False, console=False)
def parse_output(self, buf):
"""Parse one buf of output from hackrf_sweep"""
(low_edge, high_edge) = struct.unpack('QQ', buf[:16])
data = np.fromstring(buf[16:], dtype='<f4')
step = (high_edge - low_edge) / len(data)
if (low_edge // 1000000) <= (self.params["start_freq"] - self.lnb_lo / 1e6):
# Reset databuffer at the start of each sweep even if we somehow
# did not complete the previous sweep.
self.databuffer = {"timestamp": [], "x": [], "y": []}
x_axis = list(np.arange(low_edge + self.lnb_lo + step / 2, high_edge + self.lnb_lo, step))
self.databuffer["x"].extend(x_axis)
for i in range(len(data)):
self.databuffer["y"].append(data[i])
if (high_edge / 1e6) >= (self.params["stop_freq"] - self.lnb_lo / 1e6):
# We've reached the end of a pass. If it went too fast for our sweep interval, ignore it
t_finish = time.time()
if (t_finish < self.lastsweep + self.interval):
return
self.lastsweep = t_finish
# otherwise sort and display the data.
sorted_data = sorted(zip(self.databuffer["x"], self.databuffer["y"]))
self.databuffer["x"], self.databuffer["y"] = [list(x) for x in zip(*sorted_data)]
self.data_storage.update(self.databuffer)
def run(self):
"""hackrf_sweep thread main loop"""
self.process_start()
self.alive = True
self.powerThreadStarted.emit()
while self.alive:
try:
buf = self.process.stdout.read(4)
except AttributeError as e:
print(e, file=sys.stderr)
continue
if buf:
(record_length,) = struct.unpack('I', buf)
try:
buf = self.process.stdout.read(record_length)
except AttributeError as e:
print(e, file=sys.stderr)
continue
if buf:
self.parse_output(buf)
else:
break
else:
break
self.process_stop()
self.alive = False
self.powerThreadStopped.emit()

View File

@ -0,0 +1,104 @@
import shlex
import numpy as np
from Qt import QtCore
from qspectrumanalyzer import subprocess
from qspectrumanalyzer.backends import BaseInfo, BasePowerThread
class Info(BaseInfo):
"""rtl_power device metadata"""
pass
class PowerThread(BasePowerThread):
"""Thread which runs rtl_power process"""
def setup(self, start_freq, stop_freq, bin_size, interval=10.0, gain=-1, ppm=0, crop=0,
single_shot=False, device=0, sample_rate=2560000, bandwidth=0, lnb_lo=0):
"""Setup rtl_power params"""
if bin_size > 2800:
bin_size = 2800
self.params = {
"start_freq": start_freq,
"stop_freq": stop_freq,
"bin_size": bin_size,
"interval": interval,
"device": device,
"sample_rate": sample_rate,
"hops": 0,
"gain": gain,
"ppm": ppm,
"crop": crop,
"single_shot": single_shot
}
self.lnb_lo = lnb_lo
self.databuffer = {}
self.last_timestamp = ""
def process_start(self):
"""Start rtl_power process"""
if not self.process and self.params:
settings = QtCore.QSettings()
cmdline = shlex.split(settings.value("executable", "rtl_power"))
cmdline.extend([
"-f", "{}M:{}M:{}k".format(self.params["start_freq"] - self.lnb_lo / 1e6,
self.params["stop_freq"] - self.lnb_lo / 1e6,
self.params["bin_size"]),
"-i", "{}".format(self.params["interval"]),
"-d", "{}".format(self.params["device"]),
"-p", "{}".format(self.params["ppm"]),
"-c", "{}".format(self.params["crop"])
])
if self.params["sample_rate"] > 0:
cmdline.extend(["-r", "{}M".format(self.params["sample_rate"] / 1e6)])
if self.params["gain"] >= 0:
cmdline.extend(["-g", "{}".format(self.params["gain"])])
if self.params["single_shot"]:
cmdline.append("-1")
additional_params = settings.value("params", Info.additional_params)
if additional_params:
cmdline.extend(shlex.split(additional_params))
print('Starting backend:')
print(' '.join(cmdline))
print()
self.process = subprocess.Popen(cmdline, stdout=subprocess.PIPE,
universal_newlines=True, console=False)
def parse_output(self, line):
"""Parse one line of output from rtl_power"""
line = [col.strip() for col in line.split(",")]
timestamp = " ".join(line[:2])
start_freq = int(line[2])
stop_freq = int(line[3])
step = float(line[4])
samples = float(line[5])
x_axis = list(np.linspace(start_freq + self.lnb_lo, stop_freq + self.lnb_lo,
round((stop_freq - start_freq) / step)))
y_axis = [float(y) for y in line[6:]]
if len(x_axis) != len(y_axis):
print("ERROR: len(x_axis) != len(y_axis), use newer version of rtl_power!")
if len(x_axis) > len(y_axis):
print("Trimming x_axis...")
x_axis = x_axis[:len(y_axis)]
else:
print("Trimming y_axis...")
y_axis = y_axis[:len(x_axis)]
if timestamp != self.last_timestamp:
self.last_timestamp = timestamp
self.databuffer = {"timestamp": timestamp,
"x": x_axis,
"y": y_axis}
else:
self.databuffer["x"].extend(x_axis)
self.databuffer["y"].extend(y_axis)
# This have to be stupid like this to be compatible with old broken version of rtl_power. Right way is:
# if stop_freq == (self.params["stop_freq"] - self.lnb_lo / 1e6) * 1e6:
if stop_freq > ((self.params["stop_freq"] - self.lnb_lo / 1e6) * 1e6) - step:
self.data_storage.update(self.databuffer)

View File

@ -0,0 +1,145 @@
import math, shlex
from Qt import QtCore
from qspectrumanalyzer import subprocess
from qspectrumanalyzer.backends import BaseInfo, BasePowerThread
class Info(BaseInfo):
"""rtl_power_fftw device metadata"""
pass
class PowerThread(BasePowerThread):
"""Thread which runs rtl_power_fftw process"""
def setup(self, start_freq, stop_freq, bin_size, interval=10.0, gain=-1, ppm=0, crop=0,
single_shot=False, device=0, sample_rate=2560000, bandwidth=0, lnb_lo=0):
"""Setup rtl_power_fftw params"""
crop = crop * 100
overlap = crop * 2
freq_range = stop_freq * 1e6 - start_freq * 1e6
min_overhang = sample_rate * overlap * 0.01
hops = math.ceil((freq_range - min_overhang) / (sample_rate - min_overhang))
overhang = (hops * sample_rate - freq_range) / (hops - 1) if hops > 1 else 0
if bin_size > 2800:
bin_size = 2800
bins = math.ceil(sample_rate / (bin_size * 1e3))
crop_freq = sample_rate * crop * 0.01
self.params = {
"start_freq": start_freq,
"stop_freq": stop_freq,
"freq_range": freq_range,
"device": device,
"sample_rate": int(sample_rate),
"bin_size": bin_size,
"bins": bins,
"interval": interval,
"hops": hops,
"time": interval / hops,
"gain": int(gain * 10),
"ppm": ppm,
"crop": crop,
"overlap": overlap,
"min_overhang": min_overhang,
"overhang": overhang,
"single_shot": single_shot
}
self.lnb_lo = lnb_lo
self.freqs = [self.get_hop_freq(hop) for hop in range(hops)]
self.freqs_crop = [(f[0] + crop_freq, f[1] - crop_freq) for f in self.freqs]
self.databuffer = {"timestamp": [], "x": [], "y": []}
self.databuffer_hop = {"timestamp": [], "x": [], "y": []}
self.hop = 0
self.prev_line = ""
def get_hop_freq(self, hop):
"""Get start and stop frequency for particular hop"""
start_freq = self.params["start_freq"] * 1e6 + (self.params["sample_rate"] - self.params["overhang"]) * hop
stop_freq = start_freq + self.params["sample_rate"] - (self.params["sample_rate"] / self.params["bins"])
return (start_freq, stop_freq)
def process_start(self):
"""Start rtl_power_fftw process"""
if not self.process and self.params:
settings = QtCore.QSettings()
cmdline = shlex.split(settings.value("executable", "rtl_power_fftw"))
cmdline.extend([
"-f", "{}M:{}M".format(self.params["start_freq"] - self.lnb_lo / 1e6,
self.params["stop_freq"] - self.lnb_lo / 1e6),
"-b", "{}".format(self.params["bins"]),
"-t", "{}".format(self.params["time"]),
"-d", "{}".format(self.params["device"]),
"-r", "{}".format(self.params["sample_rate"]),
"-p", "{}".format(self.params["ppm"]),
"-q",
])
if self.params["gain"] >= 0:
cmdline.extend(["-g", "{}".format(self.params["gain"])])
if self.params["overlap"] > 0:
cmdline.extend(["-o", "{}".format(self.params["overlap"])])
if not self.params["single_shot"]:
cmdline.append("-c")
additional_params = settings.value("params", Info.additional_params)
if additional_params:
cmdline.extend(shlex.split(additional_params))
print('Starting backend:')
print(' '.join(cmdline))
print()
self.process = subprocess.Popen(cmdline, stdout=subprocess.PIPE,
universal_newlines=True, console=False)
def parse_output(self, line):
"""Parse one line of output from rtl_power_fftw"""
line = line.strip()
# One empty line => new hop
if not line and self.prev_line:
self.hop += 1
self.databuffer["x"].extend(self.databuffer_hop["x"])
self.databuffer["y"].extend(self.databuffer_hop["y"])
self.databuffer_hop = {"timestamp": [], "x": [], "y": []}
# Two empty lines => new set
elif not line and not self.prev_line:
self.hop = 0
self.data_storage.update(self.databuffer)
self.databuffer = {"timestamp": [], "x": [], "y": []}
# Get timestamp for new hop and set
elif line.startswith("# Acquisition start:"):
timestamp = line.split(":", 1)[1].strip()
if not self.databuffer_hop["timestamp"]:
self.databuffer_hop["timestamp"] = timestamp
if not self.databuffer["timestamp"]:
self.databuffer["timestamp"] = timestamp
# Skip other comments
elif line.startswith("#"):
pass
# Parse frequency and power
elif line[0].isdigit():
freq, power = line.split()
freq, power = float(freq) + self.lnb_lo, float(power)
start_freq, stop_freq = self.freqs_crop[self.hop]
# Apply cropping
if freq >= start_freq and freq <= stop_freq:
# Skip overlapping frequencies
if not self.databuffer["x"] or freq > self.databuffer["x"][-1]:
#print(" {:.3f} MHz".format(freq / 1e6))
self.databuffer_hop["x"].append(freq)
self.databuffer_hop["y"].append(power)
else:
#print(" Overlapping {:.3f} MHz".format(freq / 1e6))
pass
else:
#print(" Cropping {:.3f} MHz".format(freq / 1e6))
pass
self.prev_line = line

View File

@ -0,0 +1,109 @@
import shlex
import numpy as np
from Qt import QtCore
from qspectrumanalyzer import subprocess
from qspectrumanalyzer.backends import BaseInfo, BasePowerThread
class Info(BaseInfo):
"""rx_power device metadata"""
sample_rate_min = 0
sample_rate_max = 0
sample_rate = 0
start_freq_min = 0
start_freq_max = 7250
stop_freq_min = 0
stop_freq_max = 7250
gain_min = -1
gain_max = 999
bin_size_min = 0
bin_size_max = 2800
class PowerThread(BasePowerThread):
"""Thread which runs rx_power process"""
def setup(self, start_freq, stop_freq, bin_size, interval=10.0, gain=-1, ppm=0, crop=0,
single_shot=False, device=0, sample_rate=2560000, bandwidth=0, lnb_lo=0):
"""Setup rx_power params"""
self.params = {
"start_freq": start_freq,
"stop_freq": stop_freq,
"bin_size": bin_size,
"interval": interval,
"device": device,
"hops": 0,
"gain": gain,
"ppm": ppm,
"crop": crop,
"single_shot": single_shot
}
self.lnb_lo = lnb_lo
self.databuffer = {}
self.last_timestamp = ""
def process_start(self):
"""Start rx_power process"""
if not self.process and self.params:
settings = QtCore.QSettings()
cmdline = shlex.split(settings.value("executable", "rx_power"))
cmdline.extend([
"-f", "{}M:{}M:{}k".format(self.params["start_freq"] - self.lnb_lo / 1e6,
self.params["stop_freq"] - self.lnb_lo / 1e6,
self.params["bin_size"]),
"-i", "{}".format(self.params["interval"]),
"-d", "{}".format(self.params["device"]),
"-p", "{}".format(self.params["ppm"]),
"-c", "{}".format(self.params["crop"])
])
if self.params["gain"] >= 0:
cmdline.extend(["-g", "{}".format(self.params["gain"])])
if self.params["single_shot"]:
cmdline.append("-1")
additional_params = settings.value("params", Info.additional_params)
if additional_params:
cmdline.extend(shlex.split(additional_params))
print('Starting backend:')
print(' '.join(cmdline))
print()
self.process = subprocess.Popen(cmdline, stdout=subprocess.PIPE,
universal_newlines=True, console=False)
def parse_output(self, line):
"""Parse one line of output from rx_power"""
line = [col.strip() for col in line.split(",")]
timestamp = " ".join(line[:2])
start_freq = int(line[2])
stop_freq = int(line[3])
step = float(line[4])
samples = float(line[5])
x_axis = list(np.linspace(start_freq + self.lnb_lo, stop_freq + self.lnb_lo,
round((stop_freq - start_freq) / step)))
y_axis = [float(y) for y in line[6:]]
if len(x_axis) != len(y_axis):
print("ERROR: len(x_axis) != len(y_axis)!")
if len(x_axis) > len(y_axis):
print("Trimming x_axis...")
x_axis = x_axis[:len(y_axis)]
else:
print("Trimming y_axis...")
y_axis = y_axis[:len(x_axis)]
if timestamp != self.last_timestamp:
self.last_timestamp = timestamp
self.databuffer = {"timestamp": timestamp,
"x": x_axis,
"y": y_axis}
else:
self.databuffer["x"].extend(x_axis)
self.databuffer["y"].extend(y_axis)
# This have to be stupid like this to be compatible with old broken version of rtl_power. Right way is:
# if stop_freq == (self.params["stop_freq"] - self.lnb_lo / 1e6) * 1e6:
if stop_freq > ((self.params["stop_freq"] - self.lnb_lo / 1e6) * 1e6) - step:
self.data_storage.update(self.databuffer)

View File

@ -0,0 +1,254 @@
import os, sys, shlex, signal
import numpy as np
from Qt import QtCore
from qspectrumanalyzer import subprocess
from qspectrumanalyzer.backends import BaseInfo, BasePowerThread
try:
from soapypower.writer import SoapyPowerBinFormat
formatter = SoapyPowerBinFormat()
except ImportError:
print('soapy_power module not found!')
formatter = None
class Info(BaseInfo):
"""soapy_power device metadata"""
sample_rate_min = 0
sample_rate_max = 61440000
bandwidth_min = 0
bandwidth_max = 61440000
start_freq_min = 0
start_freq_max = 7250
stop_freq_min = 0
stop_freq_max = 7250
gain_min = -1
gain_max = 999
bin_size_min = 0
bin_size_max = 10000
additional_params = '--even --fft-window boxcar --remove-dc'
@classmethod
def help_device(cls, executable, device):
cmdline = shlex.split(executable)
try:
text = subprocess.check_output(cmdline + ['--detect'], universal_newlines=True,
stderr=subprocess.DEVNULL, env=dict(os.environ, COLUMNS='125'),
console=False)
text += '\n'
text += subprocess.check_output(cmdline + ['--device', device, '--info'], universal_newlines=True,
stderr=subprocess.DEVNULL, env=dict(os.environ, COLUMNS='125'),
console=False)
except subprocess.CalledProcessError as e:
text = e.output
except OSError:
text = '{} executable not found!'.format(executable)
return text
class PowerThread(BasePowerThread):
"""Thread which runs soapy_power process"""
def setup(self, start_freq, stop_freq, bin_size, interval=10.0, gain=-1, ppm=0, crop=0,
single_shot=False, device="", sample_rate=2560000, bandwidth=0, lnb_lo=0):
"""Setup soapy_power params"""
self.params = {
"start_freq": start_freq,
"stop_freq": stop_freq,
"device": device,
"sample_rate": sample_rate,
"bandwidth": bandwidth,
"bin_size": bin_size,
"interval": interval,
"hops": 0,
"gain": gain,
"ppm": ppm,
"crop": crop * 100,
"single_shot": single_shot
}
self.lnb_lo = lnb_lo
self.databuffer = {"timestamp": [], "x": [], "y": []}
self.min_freq = None
self.pipe_read = None
self.pipe_read_fd = None
self.pipe_write_fd = None
self.pipe_write_handle = None
def process_start(self):
"""Start soapy_power process"""
if not self.process and self.params:
# Create pipe used for communication with soapy_power process
self.pipe_read_fd, self.pipe_write_fd = os.pipe()
self.pipe_read = open(self.pipe_read_fd, 'rb')
os.set_inheritable(self.pipe_write_fd, True)
if sys.platform == 'win32':
self.pipe_write_handle = subprocess.make_inheritable_handle(self.pipe_write_fd)
# Prepare soapy_power cmdline parameters
settings = QtCore.QSettings()
cmdline = shlex.split(settings.value("executable", "soapy_power"))
cmdline.extend([
"-f", "{}M:{}M".format(self.params["start_freq"],
self.params["stop_freq"]),
"-B", "{}k".format(self.params["bin_size"]),
"-T", "{}".format(self.params["interval"]),
"-d", "{}".format(self.params["device"]),
"-r", "{}".format(self.params["sample_rate"]),
"-p", "{}".format(self.params["ppm"]),
"-F", "soapy_power_bin",
"--output-fd", "{}".format(
int(self.pipe_write_handle) if sys.platform == 'win32' else self.pipe_write_fd
),
])
if self.lnb_lo != 0:
cmdline.extend(["--lnb-lo", "{}".format(self.lnb_lo)])
if self.params["bandwidth"] > 0:
cmdline.extend(["-w", "{}".format(self.params["bandwidth"])])
if self.params["gain"] >= 0:
cmdline.extend(["-g", "{}".format(self.params["gain"])])
if self.params["crop"] > 0:
cmdline.extend(["-k", "{}".format(self.params["crop"])])
if not self.params["single_shot"]:
cmdline.append("-c")
additional_params = settings.value("params", Info.additional_params)
if additional_params:
cmdline.extend(shlex.split(additional_params))
# Start soapy_power process and close write part of pipe
if sys.platform == 'win32':
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
else:
creationflags = 0
print('Starting backend:')
print(' '.join(cmdline))
print()
self.process = subprocess.Popen(cmdline, close_fds=False, universal_newlines=False,
creationflags=creationflags, console=False)
os.close(self.pipe_write_fd)
if sys.platform == 'win32':
self.pipe_write_handle.Close()
def process_stop(self):
"""Stop soapy_power process"""
with self._shutdown_lock:
if self.process:
if self.process.poll() is None:
try:
if sys.platform == 'win32':
self.process.send_signal(signal.CTRL_BREAK_EVENT)
else:
self.process.terminate()
except ProcessLookupError:
pass
self.process.wait()
self.process = None
# Close pipe used for communication with soapy_power process
self.pipe_read.close()
self.pipe_read = None
self.pipe_read_fd = None
self.pipe_write_fd = None
self.pipe_write_handle = None
def parse_output(self, data):
"""Parse data from soapy_power"""
header, y_axis = data
time_start = header.time_start
time_stop = header.time_stop
start_freq = header.start
stop_freq = header.stop
step = header.step
samples = header.samples
x_axis = np.linspace(start_freq, stop_freq, round((stop_freq - start_freq) / step))
if len(x_axis) != len(y_axis):
print("ERROR: len(x_axis) != len(y_axis)")
return
if self.min_freq is None:
self.min_freq = start_freq
if start_freq == self.min_freq:
self.databuffer = {"timestamp": time_stop,
"x": list(x_axis),
"y": list(y_axis)}
else:
self.databuffer["x"].extend(x_axis)
self.databuffer["y"].extend(y_axis)
if stop_freq > (self.params["stop_freq"] * 1e6) - step:
self.data_storage.update(self.databuffer)
def run(self):
"""soapy_power thread main loop"""
if not formatter:
return
self.process_start()
self.alive = True
self.powerThreadStarted.emit()
while self.alive:
try:
data = formatter.read(self.pipe_read)
except ValueError as e:
print(e, file=sys.stderr)
continue
if data:
self.parse_output(data)
else:
break
self.process_stop()
self.alive = False
self.powerThreadStopped.emit()
def read_from_file(f):
"""Generator for reading data from soapy_power binary files"""
if not formatter:
return
min_freq = None
databuffer = None
while True:
try:
data = formatter.read(f)
except ValueError as e:
print(e, file=sys.stderr)
continue
if not data:
if min_freq is not None:
yield databuffer
return
header, y_axis = data
x_axis = np.linspace(header.start, header.stop, round((header.stop - header.start) / header.step))
if len(x_axis) != len(y_axis):
print("ERROR: len(x_axis) != len(y_axis)")
continue
if min_freq is None:
min_freq = header.start
elif header.start == min_freq:
yield databuffer
if header.start == min_freq:
databuffer = {"timestamp": header.time_stop,
"x": list(x_axis),
"y": list(y_axis)}
else:
databuffer["x"].extend(x_axis)
databuffer["y"].extend(y_axis)

View File

@ -0,0 +1,28 @@
from Qt import QtCore, QtWidgets
from qspectrumanalyzer.ui_qspectrumanalyzer_baseline import Ui_QSpectrumAnalyzerBaseline
class QSpectrumAnalyzerBaseline(QtWidgets.QDialog, Ui_QSpectrumAnalyzerBaseline):
"""QSpectrumAnalyzer baseline dialog"""
def __init__(self, parent=None):
# Initialize UI
super().__init__(parent)
self.setupUi(self)
# Load settings
settings = QtCore.QSettings()
self.baselineFileEdit.setText(settings.value("baseline_file", ""))
@QtCore.Slot()
def on_baselineFileButton_clicked(self):
"""Open file dialog when button is clicked"""
filename = QtWidgets.QFileDialog.getOpenFileName(self, self.tr("Select baseline file - QSpectrumAnalyzer"))[0]
if filename:
self.baselineFileEdit.setText(filename)
def accept(self):
"""Save settings when dialog is accepted"""
settings = QtCore.QSettings()
settings.setValue("baseline_file", self.baselineFileEdit.text())
QtWidgets.QDialog.accept(self)

View File

@ -0,0 +1,33 @@
from Qt import QtCore, QtWidgets
from qspectrumanalyzer.utils import color_to_str, str_to_color
from qspectrumanalyzer.ui_qspectrumanalyzer_colors import Ui_QSpectrumAnalyzerColors
class QSpectrumAnalyzerColors(QtWidgets.QDialog, Ui_QSpectrumAnalyzerColors):
"""QSpectrumAnalyzer colors dialog"""
def __init__(self, parent=None):
# Initialize UI
super().__init__(parent)
self.setupUi(self)
# Load settings
settings = QtCore.QSettings()
self.mainColorButton.setColor(str_to_color(settings.value("main_color", "255, 255, 0, 255")))
self.peakHoldMaxColorButton.setColor(str_to_color(settings.value("peak_hold_max_color", "255, 0, 0, 255")))
self.peakHoldMinColorButton.setColor(str_to_color(settings.value("peak_hold_min_color", "0, 0, 255, 255")))
self.averageColorButton.setColor(str_to_color(settings.value("average_color", "0, 255, 255, 255")))
self.persistenceColorButton.setColor(str_to_color(settings.value("persistence_color", "0, 255, 0, 255")))
self.baselineColorButton.setColor(str_to_color(settings.value("baseline_color", "255, 0, 255, 255")))
def accept(self):
"""Save settings when dialog is accepted"""
settings = QtCore.QSettings()
settings.setValue("main_color", color_to_str(self.mainColorButton.color()))
settings.setValue("peak_hold_max_color", color_to_str(self.peakHoldMaxColorButton.color()))
settings.setValue("peak_hold_min_color", color_to_str(self.peakHoldMinColorButton.color()))
settings.setValue("average_color", color_to_str(self.averageColorButton.color()))
settings.setValue("persistence_color", color_to_str(self.persistenceColorButton.color()))
settings.setValue("baseline_color", color_to_str(self.baselineColorButton.color()))
QtWidgets.QDialog.accept(self)

296
qspectrumanalyzer/data.py Normal file
View File

@ -0,0 +1,296 @@
import time, sys, os
from Qt import QtCore
import numpy as np
from qspectrumanalyzer.utils import smooth
from qspectrumanalyzer.backends import soapy_power
class HistoryBuffer:
"""Fixed-size NumPy array ring buffer"""
def __init__(self, data_size, max_history_size, dtype=float):
self.data_size = data_size
self.max_history_size = max_history_size
self.history_size = 0
self.counter = 0
self.buffer = np.empty(shape=(max_history_size, data_size), dtype=dtype)
def append(self, data):
"""Append new data to ring buffer"""
self.counter += 1
if self.history_size < self.max_history_size:
self.history_size += 1
self.buffer = np.roll(self.buffer, -1, axis=0)
self.buffer[-1] = data
def get_buffer(self):
"""Return buffer stripped to size of actual data"""
if self.history_size < self.max_history_size:
return self.buffer[-self.history_size:]
else:
return self.buffer
def __getitem__(self, key):
return self.buffer[key]
class TaskSignals(QtCore.QObject):
"""Task signals emitter"""
result = QtCore.Signal(object)
class Task(QtCore.QRunnable):
"""Threaded task (run it with QThreadPool worker threads)"""
def __init__(self, task, *args, **kwargs):
super().__init__()
self.task = task
self.args = args
self.kwargs = kwargs
self.signals = TaskSignals()
def run(self):
"""Run task in worker thread and emit signal with result"""
#print('Running', self.task, 'in thread', QtCore.QThread.currentThreadId())
result = self.task(*self.args, **self.kwargs)
self.signals.result.emit(result)
class DataStorage(QtCore.QObject):
"""Data storage for spectrum measurements"""
history_updated = QtCore.Signal(object)
data_updated = QtCore.Signal(object)
history_recalculated = QtCore.Signal(object)
data_recalculated = QtCore.Signal(object)
average_updated = QtCore.Signal(object)
baseline_updated = QtCore.Signal(object)
peak_hold_max_updated = QtCore.Signal(object)
peak_hold_min_updated = QtCore.Signal(object)
def __init__(self, max_history_size=100, parent=None):
super().__init__(parent)
self.max_history_size = max_history_size
self.smooth = False
self.smooth_length = 11
self.smooth_window = "hanning"
self.subtract_baseline = False
self.prev_baseline = None
self.baseline = None
self.baseline_x = None
# Use only one worker thread because it is not faster
# with more threads (and memory consumption is much higher)
self.threadpool = QtCore.QThreadPool()
self.threadpool.setMaxThreadCount(1)
self.reset()
def reset(self):
"""Reset all data"""
self.wait()
self.x = None
self.history = None
self.reset_data()
def reset_data(self):
"""Reset current data"""
self.wait()
self.y = None
self.average_counter = 0
self.average = None
self.peak_hold_max = None
self.peak_hold_min = None
def start_task(self, fn, *args, **kwargs):
"""Run function asynchronously in worker thread"""
task = Task(fn, *args, **kwargs)
self.threadpool.start(task)
def wait(self):
"""Wait for worker threads to complete all running tasks"""
self.threadpool.waitForDone()
def update(self, data):
"""Update data storage"""
if self.y is not None and len(data["y"]) != len(self.y):
print("{:d} bins coming from backend, expected {:d}".format(len(data["y"]), len(self.y)))
return
self.average_counter += 1
if self.x is None:
self.x = data["x"]
# Subtract baseline from data
data["y"] = np.asarray(data["y"])
if self.subtract_baseline and self.baseline is not None and len(data["y"]) == len(self.baseline):
data["y"] -= self.baseline
self.start_task(self.update_history, data.copy())
self.start_task(self.update_data, data)
def update_data(self, data):
"""Update main spectrum data (and possibly apply smoothing)"""
if self.smooth:
data["y"] = self.smooth_data(data["y"])
self.y = data["y"]
self.data_updated.emit(self)
self.start_task(self.update_average, data)
self.start_task(self.update_peak_hold_max, data)
self.start_task(self.update_peak_hold_min, data)
def update_history(self, data):
"""Update spectrum measurements history"""
if self.history is None:
self.history = HistoryBuffer(len(data["y"]), self.max_history_size)
self.history.append(data["y"])
self.history_updated.emit(self)
def update_average(self, data):
"""Update average data"""
if self.average is None:
self.average = data["y"].copy()
else:
self.average = np.average((self.average, data["y"]), axis=0, weights=(self.average_counter - 1, 1))
self.average_updated.emit(self)
def update_peak_hold_max(self, data):
"""Update max. peak hold data"""
if self.peak_hold_max is None:
self.peak_hold_max = data["y"].copy()
else:
self.peak_hold_max = np.maximum(self.peak_hold_max, data["y"])
self.peak_hold_max_updated.emit(self)
def update_peak_hold_min(self, data):
"""Update min. peak hold data"""
if self.peak_hold_min is None:
self.peak_hold_min = data["y"].copy()
else:
self.peak_hold_min = np.minimum(self.peak_hold_min, data["y"])
self.peak_hold_min_updated.emit(self)
def smooth_data(self, y):
"""Apply smoothing function to data"""
return smooth(y, window_len=self.smooth_length, window=self.smooth_window)
def set_smooth(self, toggle, length=11, window="hanning"):
"""Toggle smoothing and set smoothing params"""
if toggle != self.smooth or length != self.smooth_length or window != self.smooth_window:
self.smooth = toggle
self.smooth_length = length
self.smooth_window = window
self.start_task(self.recalculate_data)
def set_subtract_baseline(self, toggle, baseline_file=None):
"""Toggle baseline subtraction and set baseline"""
baseline = None
baseline_x = None
# Load baseline from file (compute average if there are multiple PSD data in file)
if baseline_file and os.path.isfile(baseline_file):
average_counter = 0
with open(baseline_file, 'rb') as f:
for data in soapy_power.read_from_file(f):
average_counter += 1
if baseline is None:
baseline = data['y'].copy()
baseline_x = data['x'].copy()
else:
baseline = np.average((baseline, data['y']), axis=0, weights=(average_counter - 1, 1))
# Don't subtract baseline if number of bins in baseline differs from number of bins in data
if self.y is not None and baseline is not None and len(self.y) != len(baseline):
print("Can't subtract baseline (expected {:d} bins, but baseline has {:d} bins)".format(
len(self.y), len(baseline)
))
#baseline = None
if self.subtract_baseline:
self.prev_baseline = self.baseline
#if not np.array_equal(baseline, self.baseline):
self.baseline = baseline
self.baseline_x = baseline_x
self.baseline_updated.emit(self)
self.subtract_baseline = toggle
self.start_task(self.recalculate_history)
self.start_task(self.recalculate_data)
def recalculate_history(self):
"""Recalculate spectrum measurements history"""
if self.history is None:
return
history = self.history.get_buffer()
if self.prev_baseline is not None and len(history[-1]) == len(self.prev_baseline):
history += self.prev_baseline
self.prev_baseline = None
if self.subtract_baseline and self.baseline is not None and len(history[-1]) == len(self.baseline):
history -= self.baseline
self.history_recalculated.emit(self)
def recalculate_data(self):
"""Recalculate current data from history"""
if self.history is None:
return
history = self.history.get_buffer()
if self.smooth:
self.y = self.smooth_data(history[-1])
self.average_counter = 0
self.average = self.y.copy()
self.peak_hold_max = self.y.copy()
self.peak_hold_min = self.y.copy()
for y in history[:-1]:
self.average_counter += 1
y = self.smooth_data(y)
self.average = np.average((self.average, y), axis=0, weights=(self.average_counter - 1, 1))
self.peak_hold_max = np.maximum(self.peak_hold_max, y)
self.peak_hold_min = np.minimum(self.peak_hold_min, y)
else:
self.y = history[-1]
self.average_counter = self.history.history_size
self.average = np.average(history, axis=0)
self.peak_hold_max = history.max(axis=0)
self.peak_hold_min = history.min(axis=0)
self.data_recalculated.emit(self)
#self.data_updated.emit({"x": self.x, "y": self.y})
#self.average_updated.emit({"x": self.x, "y": self.average})
#self.peak_hold_max_updated.emit({"x": self.x, "y": self.peak_hold_max})
#self.peak_hold_min_updated.emit({"x": self.x, "y": self.peak_hold_min})
class Test:
"""Test data storage performance"""
def __init__(self, data_size=100000, max_history_size=100):
self.data_size = data_size
self.data = {"x": np.arange(data_size),
"y": None}
self.datastorage = DataStorage(max_history_size)
def run_one(self):
"""Generate random data and update data storage"""
self.data["y"] = np.random.normal(size=self.data_size)
self.datastorage.update(self.data)
def run(self, runs=1000):
"""Run performance test"""
t = time.time()
for i in range(runs):
self.run_one()
self.datastorage.wait()
total_time = time.time() - t
print("Total time:", total_time)
print("FPS:", runs / total_time)
if __name__ == "__main__":
test = Test(int(sys.argv[1]), int(sys.argv[2]))
test.run(int(sys.argv[3]))

View File

@ -1,163 +1,438 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS><TS version="2.0">
<context>
<name>QSpectrumAnalyzerBaseline</name>
<message>
<location filename="../baseline.py" line="20"/>
<source>Select baseline file - QSpectrumAnalyzer</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_baseline.py" line="50"/>
<source>Baseline - QSpectrumAnalyzer</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_baseline.py" line="51"/>
<source>Baseline &amp;file:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_baseline.py" line="52"/>
<source>...</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QSpectrumAnalyzerColors</name>
<message>
<location filename="../ui_qspectrumanalyzer_colors.py" line="112"/>
<source>Colors - QSpectrumAnalyzer</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_colors.py" line="124"/>
<source>...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_colors.py" line="113"/>
<source>&amp;Main curve color:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_colors.py" line="115"/>
<source>Max. peak &amp;hold color:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_colors.py" line="117"/>
<source>M&amp;in. peak hold color:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_colors.py" line="119"/>
<source>Average &amp;color:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_colors.py" line="121"/>
<source>Persistence co&amp;lor:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_colors.py" line="123"/>
<source>&amp;Baseline color:</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QSpectrumAnalyzerMainWindow</name>
<message>
<location filename="ui_qspectrumanalyzer.py" line="219"/>
<location filename="../ui_qspectrumanalyzer.py" line="317"/>
<source>QSpectrumAnalyzer</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="233"/>
<source>Settings</source>
<location filename="../__main__.py" line="496"/>
<source>About - QSpectrumAnalyzer</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="230"/>
<source> MHz</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="232"/>
<source> kHz</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="236"/>
<source>auto</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="226"/>
<source>Frequency</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="222"/>
<source>Controls</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="239"/>
<source>Levels</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qspectrumanalyzer.py" line="241"/>
<source>About</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qspectrumanalyzer.py" line="241"/>
<location filename="../__main__.py" line="496"/>
<source>QSpectrumAnalyzer {}</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="220"/>
<location filename="../ui_qspectrumanalyzer.py" line="318"/>
<source>&amp;File</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="221"/>
<location filename="../ui_qspectrumanalyzer.py" line="319"/>
<source>&amp;Help</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="223"/>
<source>&amp;Start</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="224"/>
<source>S&amp;top</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="225"/>
<source>Si&amp;ngle shot</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="240"/>
<location filename="../ui_qspectrumanalyzer.py" line="350"/>
<source>&amp;Settings...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="241"/>
<location filename="../ui_qspectrumanalyzer.py" line="351"/>
<source>&amp;Quit</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="242"/>
<location filename="../ui_qspectrumanalyzer.py" line="352"/>
<source>Ctrl+Q</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="243"/>
<location filename="../ui_qspectrumanalyzer.py" line="353"/>
<source>&amp;About</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="234"/>
<source>Interval [s]:</source>
<location filename="../__main__.py" line="253"/>
<source>Frequency hops: {}</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="235"/>
<source>Gain [dB]:</source>
<location filename="../__main__.py" line="255"/>
<source>Total time: {} | Sweep time: {:.2f} s ({:.2f} FPS)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="237"/>
<source>Corr. [ppm]:</source>
<location filename="../ui_qspectrumanalyzer.py" line="321"/>
<source>&amp;Start</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="238"/>
<source>Crop [%]:</source>
<location filename="../ui_qspectrumanalyzer.py" line="322"/>
<source>S&amp;top</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="227"/>
<location filename="../ui_qspectrumanalyzer.py" line="323"/>
<source>Si&amp;ngle shot</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="325"/>
<source>Start:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="229"/>
<location filename="../ui_qspectrumanalyzer.py" line="328"/>
<source> MHz</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="327"/>
<source>Stop:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer.py" line="231"/>
<source>Bin size:</source>
<location filename="../ui_qspectrumanalyzer.py" line="329"/>
<source>&amp;Bin size:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="330"/>
<source> kHz</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="332"/>
<source>&amp;Interval [s]:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="333"/>
<source>&amp;Gain [dB]:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="334"/>
<source>Corr. [ppm]:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="335"/>
<source>Crop [%]:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="336"/>
<source>Main curve</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="337"/>
<source>Colors...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="338"/>
<source>Max. hold</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="339"/>
<source>Min. hold</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="340"/>
<source>Average</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="341"/>
<source>Smoothing</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="347"/>
<source>...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="343"/>
<source>Persistence</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="345"/>
<source>auto</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="348"/>
<source>Subtract baseline</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="346"/>
<source>Baseline</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="320"/>
<source>Controls</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="324"/>
<source>Frequency</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="331"/>
<source>Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer.py" line="349"/>
<source>Levels</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QSpectrumAnalyzerPersistence</name>
<message>
<location filename="../ui_qspectrumanalyzer_persistence.py" line="55"/>
<source>Persistence - QSpectrumAnalyzer</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_persistence.py" line="56"/>
<source>Decay function:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_persistence.py" line="57"/>
<source>linear</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_persistence.py" line="58"/>
<source>exponential</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_persistence.py" line="59"/>
<source>Persistence length:</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QSpectrumAnalyzerSettings</name>
<message>
<location filename="ui_qspectrumanalyzer_settings.py" line="73"/>
<source>QSpectrumAnalyzer - Settings</source>
<location filename="../settings.py" line="57"/>
<source>Select executable - QSpectrumAnalyzer</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer_settings.py" line="74"/>
<source>Rtl_power executable:</source>
<location filename="../ui_qspectrumanalyzer_settings.py" line="148"/>
<source>Settings - QSpectrumAnalyzer</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer_settings.py" line="75"/>
<location filename="../ui_qspectrumanalyzer_settings.py" line="149"/>
<source>&amp;Backend:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="156"/>
<source>soapy_power</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="151"/>
<source>rx_power</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="152"/>
<source>rtl_power_fftw</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="153"/>
<source>rtl_power</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer_settings.py" line="76"/>
<location filename="../ui_qspectrumanalyzer_settings.py" line="154"/>
<source>hackrf_sweep</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="155"/>
<source>E&amp;xecutable:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="157"/>
<source>...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui_qspectrumanalyzer_settings.py" line="77"/>
<source>Waterfall history size:</source>
<location filename="../ui_qspectrumanalyzer_settings.py" line="159"/>
<source>Sa&amp;mple rate:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="160"/>
<source>&amp;Waterfall history size:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="158"/>
<source>&amp;Device:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="161"/>
<source>Bandwidt&amp;h:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="163"/>
<source>&amp;LNB LO:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="166"/>
<source> ? </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="169"/>
<source>Negative frequency for upconverters, positive frequency for downconverters.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="164"/>
<source>Add&amp;itional parameters:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="170"/>
<source> MHz</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QSpectrumAnalyzerSettingsHelp</name>
<message>
<location filename="../ui_qspectrumanalyzer_settings_help.py" line="36"/>
<source>Help - QSpectrumAnalyzer</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QSpectrumAnalyzerSmoothing</name>
<message>
<location filename="../ui_qspectrumanalyzer_smoothing.py" line="60"/>
<source>Smoothing - QSpectrumAnalyzer</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_smoothing.py" line="61"/>
<source>&amp;Window function:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_smoothing.py" line="62"/>
<source>rectangular</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_smoothing.py" line="63"/>
<source>hanning</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_smoothing.py" line="64"/>
<source>hamming</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_smoothing.py" line="65"/>
<source>bartlett</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_smoothing.py" line="66"/>
<source>blackman</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_smoothing.py" line="67"/>
<source>Window len&amp;gth:</source>
<translation type="unfinished"></translation>
</message>
</context>

View File

@ -0,0 +1,29 @@
from Qt import QtCore, QtWidgets
from qspectrumanalyzer.ui_qspectrumanalyzer_persistence import Ui_QSpectrumAnalyzerPersistence
class QSpectrumAnalyzerPersistence(QtWidgets.QDialog, Ui_QSpectrumAnalyzerPersistence):
"""QSpectrumAnalyzer spectrum persistence dialog"""
def __init__(self, parent=None):
# Initialize UI
super().__init__(parent)
self.setupUi(self)
# Load settings
settings = QtCore.QSettings()
self.persistenceLengthSpinBox.setValue(settings.value("persistence_length", 5, int))
decay_function = settings.value("persistence_decay", "exponential")
i = self.decayFunctionComboBox.findText(decay_function)
if i == -1:
self.decayFunctionComboBox.setCurrentIndex(0)
else:
self.decayFunctionComboBox.setCurrentIndex(i)
def accept(self):
"""Save settings when dialog is accepted"""
settings = QtCore.QSettings()
settings.setValue("persistence_length", self.persistenceLengthSpinBox.value())
settings.setValue("persistence_decay", self.decayFunctionComboBox.currentText())
QtWidgets.QDialog.accept(self)

348
qspectrumanalyzer/plot.py Normal file
View File

@ -0,0 +1,348 @@
import collections, math
from Qt import QtCore
import pyqtgraph as pg
# Basic PyQtGraph settings
pg.setConfigOptions(antialias=True)
class SpectrumPlotWidget:
"""Main spectrum plot"""
def __init__(self, layout):
if not isinstance(layout, pg.GraphicsLayoutWidget):
raise ValueError("layout must be instance of pyqtgraph.GraphicsLayoutWidget")
self.layout = layout
self.main_curve = True
self.main_color = pg.mkColor("y")
self.persistence = False
self.persistence_length = 5
self.persistence_decay = "exponential"
self.persistence_color = pg.mkColor("g")
self.persistence_data = None
self.persistence_curves = None
self.peak_hold_max = False
self.peak_hold_max_color = pg.mkColor("r")
self.peak_hold_min = False
self.peak_hold_min_color = pg.mkColor("b")
self.average = False
self.average_color = pg.mkColor("c")
self.baseline = False
self.baseline_color = pg.mkColor("m")
self.create_plot()
def create_plot(self):
"""Create main spectrum plot"""
self.posLabel = self.layout.addLabel(row=0, col=0, justify="right")
self.plot = self.layout.addPlot(row=1, col=0)
self.plot.showGrid(x=True, y=True)
self.plot.setLabel("left", "Power", units="dB")
self.plot.setLabel("bottom", "Frequency", units="Hz")
self.plot.setLimits(xMin=0)
self.plot.showButtons()
#self.plot.setDownsampling(mode="peak")
#self.plot.setClipToView(True)
self.create_baseline_curve()
self.create_persistence_curves()
self.create_average_curve()
self.create_peak_hold_min_curve()
self.create_peak_hold_max_curve()
self.create_main_curve()
# Create crosshair
self.vLine = pg.InfiniteLine(angle=90, movable=False)
self.vLine.setZValue(1000)
self.hLine = pg.InfiniteLine(angle=0, movable=False)
self.vLine.setZValue(1000)
self.plot.addItem(self.vLine, ignoreBounds=True)
self.plot.addItem(self.hLine, ignoreBounds=True)
self.mouseProxy = pg.SignalProxy(self.plot.scene().sigMouseMoved,
rateLimit=60, slot=self.mouse_moved)
def create_main_curve(self):
"""Create main spectrum curve"""
self.curve = self.plot.plot(pen=self.main_color)
self.curve.setZValue(900)
def create_peak_hold_max_curve(self):
"""Create max. peak hold curve"""
self.curve_peak_hold_max = self.plot.plot(pen=self.peak_hold_max_color)
self.curve_peak_hold_max.setZValue(800)
def create_peak_hold_min_curve(self):
"""Create min. peak hold curve"""
self.curve_peak_hold_min = self.plot.plot(pen=self.peak_hold_min_color)
self.curve_peak_hold_min.setZValue(800)
def create_average_curve(self):
"""Create average curve"""
self.curve_average = self.plot.plot(pen=self.average_color)
self.curve_average.setZValue(700)
def create_baseline_curve(self):
"""Create baseline curve"""
self.curve_baseline = self.plot.plot(pen=self.baseline_color)
self.curve_baseline.setZValue(500)
def create_persistence_curves(self):
"""Create spectrum persistence curves"""
z_index_base = 600
decay = self.get_decay()
self.persistence_curves = []
for i in range(self.persistence_length):
alpha = 255 * decay(i + 1, self.persistence_length + 1)
color = self.persistence_color
curve = self.plot.plot(pen=(color.red(), color.green(), color.blue(), alpha))
curve.setZValue(z_index_base - i)
self.persistence_curves.append(curve)
def set_colors(self):
"""Set colors of all curves"""
self.curve.setPen(self.main_color)
self.curve_peak_hold_max.setPen(self.peak_hold_max_color)
self.curve_peak_hold_min.setPen(self.peak_hold_min_color)
self.curve_average.setPen(self.average_color)
self.curve_baseline.setPen(self.baseline_color)
decay = self.get_decay()
for i, curve in enumerate(self.persistence_curves):
alpha = 255 * decay(i + 1, self.persistence_length + 1)
color = self.persistence_color
curve.setPen((color.red(), color.green(), color.blue(), alpha))
def decay_linear(self, x, length):
"""Get alpha value for persistence curve (linear decay)"""
return (-x / length) + 1
def decay_exponential(self, x, length, const=1 / 3):
"""Get alpha value for persistence curve (exponential decay)"""
return math.e**(-x / (length * const))
def get_decay(self):
"""Get decay function"""
if self.persistence_decay == 'exponential':
return self.decay_exponential
else:
return self.decay_linear
def update_plot(self, data_storage, force=False):
"""Update main spectrum curve"""
if data_storage.x is None:
return
if self.main_curve or force:
self.curve.setData(data_storage.x, data_storage.y)
if force:
self.curve.setVisible(self.main_curve)
def update_peak_hold_max(self, data_storage, force=False):
"""Update max. peak hold curve"""
if data_storage.x is None:
return
if self.peak_hold_max or force:
self.curve_peak_hold_max.setData(data_storage.x, data_storage.peak_hold_max)
if force:
self.curve_peak_hold_max.setVisible(self.peak_hold_max)
def update_peak_hold_min(self, data_storage, force=False):
"""Update min. peak hold curve"""
if data_storage.x is None:
return
if self.peak_hold_min or force:
self.curve_peak_hold_min.setData(data_storage.x, data_storage.peak_hold_min)
if force:
self.curve_peak_hold_min.setVisible(self.peak_hold_min)
def update_average(self, data_storage, force=False):
"""Update average curve"""
if data_storage.x is None:
return
if self.average or force:
self.curve_average.setData(data_storage.x, data_storage.average)
if force:
self.curve_average.setVisible(self.average)
def update_baseline(self, data_storage, force=False):
"""Update baseline curve"""
if data_storage.baseline_x is None or data_storage.baseline is None:
self.curve_baseline.clear()
return
if self.baseline or force:
self.curve_baseline.setData(data_storage.baseline_x, data_storage.baseline)
if force:
self.curve_baseline.setVisible(self.baseline)
def update_persistence(self, data_storage, force=False):
"""Update persistence curves"""
if data_storage.x is None:
return
if self.persistence or force:
if self.persistence_data is None:
self.persistence_data = collections.deque(maxlen=self.persistence_length)
else:
for i, y in enumerate(self.persistence_data):
curve = self.persistence_curves[i]
curve.setData(data_storage.x, y)
if force:
curve.setVisible(self.persistence)
self.persistence_data.appendleft(data_storage.y)
def recalculate_plot(self, data_storage):
"""Recalculate plot from history"""
if data_storage.x is None:
return
QtCore.QTimer.singleShot(0, lambda: self.update_plot(data_storage, force=True))
QtCore.QTimer.singleShot(0, lambda: self.update_average(data_storage, force=True))
QtCore.QTimer.singleShot(0, lambda: self.update_baseline(data_storage, force=True))
QtCore.QTimer.singleShot(0, lambda: self.update_peak_hold_max(data_storage, force=True))
QtCore.QTimer.singleShot(0, lambda: self.update_peak_hold_min(data_storage, force=True))
def recalculate_persistence(self, data_storage):
"""Recalculate persistence data and update persistence curves"""
if data_storage.x is None:
return
self.clear_persistence()
self.persistence_data = collections.deque(maxlen=self.persistence_length)
for i in range(min(self.persistence_length, data_storage.history.history_size - 1)):
data = data_storage.history[-i - 2]
if data_storage.smooth:
data = data_storage.smooth_data(data)
self.persistence_data.append(data)
QtCore.QTimer.singleShot(0, lambda: self.update_persistence(data_storage, force=True))
def mouse_moved(self, evt):
"""Update crosshair when mouse is moved"""
pos = evt[0]
if self.plot.sceneBoundingRect().contains(pos):
mousePoint = self.plot.vb.mapSceneToView(pos)
self.posLabel.setText(
"<span style='font-size: 12pt'>f={:0.3f} MHz, P={:0.3f} dB</span>".format(
mousePoint.x() / 1e6,
mousePoint.y()
)
)
self.vLine.setPos(mousePoint.x())
self.hLine.setPos(mousePoint.y())
def clear_plot(self):
"""Clear main spectrum curve"""
self.curve.clear()
def clear_peak_hold_max(self):
"""Clear max. peak hold curve"""
self.curve_peak_hold_max.clear()
def clear_peak_hold_min(self):
"""Clear min. peak hold curve"""
self.curve_peak_hold_min.clear()
def clear_average(self):
"""Clear average curve"""
self.curve_average.clear()
def clear_baseline(self):
"""Clear baseline curve"""
self.curve_baseline.clear()
def clear_persistence(self):
"""Clear spectrum persistence curves"""
self.persistence_data = None
for curve in self.persistence_curves:
curve.clear()
self.plot.removeItem(curve)
self.create_persistence_curves()
class WaterfallPlotWidget:
"""Waterfall plot"""
def __init__(self, layout, histogram_layout=None):
if not isinstance(layout, pg.GraphicsLayoutWidget):
raise ValueError("layout must be instance of pyqtgraph.GraphicsLayoutWidget")
if histogram_layout and not isinstance(histogram_layout, pg.GraphicsLayoutWidget):
raise ValueError("histogram_layout must be instance of pyqtgraph.GraphicsLayoutWidget")
self.layout = layout
self.histogram_layout = histogram_layout
self.history_size = 100
self.counter = 0
self.create_plot()
def create_plot(self):
"""Create waterfall plot"""
self.plot = self.layout.addPlot()
self.plot.setLabel("bottom", "Frequency", units="Hz")
self.plot.setLabel("left", "Time")
self.plot.setYRange(-self.history_size, 0)
self.plot.setLimits(xMin=0, yMax=0)
self.plot.showButtons()
#self.plot.setAspectLocked(True)
#self.plot.setDownsampling(mode="peak")
#self.plot.setClipToView(True)
# Setup histogram widget (for controlling waterfall plot levels and gradients)
if self.histogram_layout:
self.histogram = pg.HistogramLUTItem()
self.histogram_layout.addItem(self.histogram)
self.histogram.gradient.loadPreset("flame")
#self.histogram.setHistogramRange(-50, 0)
#self.histogram.setLevels(-50, 0)
def update_plot(self, data_storage):
"""Update waterfall plot"""
self.counter += 1
# Create waterfall image on first run
if self.counter == 1:
self.waterfallImg = pg.ImageItem()
self.waterfallImg.scale((data_storage.x[-1] - data_storage.x[0]) / len(data_storage.x), 1)
self.plot.clear()
self.plot.addItem(self.waterfallImg)
# Roll down one and replace leading edge with new data
self.waterfallImg.setImage(data_storage.history.buffer[-self.counter:].T,
autoLevels=False, autoRange=False)
# Move waterfall image to always start at 0
self.waterfallImg.setPos(
data_storage.x[0],
-self.counter if self.counter < self.history_size else -self.history_size
)
# Link histogram widget to waterfall image on first run
# (must be done after first data is received or else levels would be wrong)
if self.counter == 1 and self.histogram_layout:
self.histogram.setImageItem(self.waterfallImg)
def clear_plot(self):
"""Clear waterfall plot"""
self.counter = 0
def recalculate_plot(self, data_storage):
"""Recalculate waterfall plot"""
if data_storage.x is None:
return
self.waterfallImg.setImage(data_storage.history.buffer[-self.counter:].T,
autoLevels=False, autoRange=False)
self.waterfallImg.setPos(
data_storage.x[0],
-self.counter if self.counter < self.history_size else -self.history_size
)
self.histogram.setImageItem(self.waterfallImg)

View File

@ -0,0 +1,133 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="32"
height="32"
id="svg4290"
version="1.1"
inkscape:version="0.91 r13725"
viewBox="0 0 32 32"
inkscape:export-filename="qspectrumanalyzer.png"
inkscape:export-xdpi="135"
inkscape:export-ydpi="135"
sodipodi:docname="qspectrumanalyzer.svg">
<defs
id="defs4292" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6568543"
inkscape:cx="0.51171273"
inkscape:cy="17.429759"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:grid-bbox="true"
inkscape:document-units="px"
inkscape:snap-intersection-paths="true"
inkscape:snap-bbox="false"
inkscape:snap-nodes="true"
inkscape:object-nodes="true"
inkscape:snap-global="false"
inkscape:window-width="1366"
inkscape:window-height="709"
inkscape:window-x="-4"
inkscape:window-y="0"
inkscape:window-maximized="1">
<inkscape:grid
type="xygrid"
id="grid4298" />
</sodipodi:namedview>
<metadata
id="metadata4295">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer">
<rect
style="fill:#000000;stroke:#666666;stroke-opacity:1"
id="rect4300"
width="30"
height="30"
x="1"
y="1"
rx="3"
ry="3" />
<path
style="fill:none;fill-rule:evenodd;stroke:#666666;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 16,1 0,30"
id="path4302"
inkscape:connector-curvature="0" />
<path
style="fill:none;fill-rule:evenodd;stroke:#666666;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 31,16 1,16"
id="path4302-5"
inkscape:connector-curvature="0" />
<use
x="0"
y="0"
xlink:href="#path4302"
id="use4369"
transform="translate(-7.5,0)"
width="100%"
height="100%" />
<use
x="0"
y="0"
xlink:href="#path4302"
id="use4371"
transform="translate(7.5,0)"
width="100%"
height="100%" />
<path
inkscape:connector-curvature="0"
id="path4373"
d="M 31,8.5 1,8.5"
style="fill:none;fill-rule:evenodd;stroke:#666666;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<use
x="0"
y="0"
xlink:href="#path4302-5"
id="use4375"
transform="translate(0,7.5)"
width="100%"
height="100%" />
<path
style="fill:none;fill-rule:evenodd;stroke:#00ff00;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 1.0625,25.25 c 0,0 2.5545099,5.432144 4.3470654,0.903939 0.6415968,-1.620748 1.1843693,-10.505075 2.3294368,-10.451145 1.124605,0.05297 1.2928473,9.79388 3.0231028,11.399454 0.836834,0.735662 2.019611,-2.789575 2.96484,-2.691821 1.178304,0.121857 2.125956,3.657855 3.181223,2.383073 2.07617,-2.508052 1.527752,-21.6078848 2.588897,-21.5746877 1.200441,0.037555 1.333727,19.4989257 3.765313,21.7522057 1.533625,1.421166 1.791478,-2.106359 3.115303,-2.461012 0.660576,-0.176968 2.034012,3.420657 2.702589,3.52241 0.772278,0.117536 1.624099,-1.153766 2.01348,-0.813666"
id="path4394"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csscssssssc" />
<rect
style="fill:none;stroke:#666666;stroke-opacity:1"
id="rect4300-8"
width="30"
height="30"
x="1"
y="1"
rx="3"
ry="3" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>1100</width>
<height>680</height>
<width>1200</width>
<height>892</height>
</rect>
</property>
<property name="windowTitle">
@ -51,8 +51,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>1100</width>
<height>27</height>
<width>1200</width>
<height>32</height>
</rect>
</property>
<widget class="QMenu" name="menu_File">
@ -73,268 +73,487 @@
<addaction name="menu_Help"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QDockWidget" name="dockWidget">
<widget class="QDockWidget" name="controlsDockWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>190</width>
<height>130</height>
</size>
</property>
<property name="features">
<set>QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable</set>
</property>
<property name="windowTitle">
<string>Controls</string>
</property>
<attribute name="dockWidgetArea">
<number>2</number>
</attribute>
<widget class="QWidget" name="dockWidgetContents">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Controls</string>
<widget class="QWidget" name="controlsDockWidgetContents">
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QPushButton" name="startButton">
<property name="text">
<string>&amp;Start</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QPushButton" name="startButton">
<property name="text">
<string>&amp;Start</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="stopButton">
<property name="text">
<string>S&amp;top</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QPushButton" name="singleShotButton">
<property name="text">
<string>Si&amp;ngle shot</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Frequency</string>
<item row="0" column="1">
<widget class="QPushButton" name="stopButton">
<property name="text">
<string>S&amp;top</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Start:</string>
</property>
<property name="buddy">
<cstring>startFreqSpinBox</cstring>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="startFreqSpinBox">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="suffix">
<string> MHz</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="minimum">
<double>24.000000000000000</double>
</property>
<property name="maximum">
<double>1766.000000000000000</double>
</property>
<property name="value">
<double>87.000000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Stop:</string>
</property>
<property name="buddy">
<cstring>stopFreqSpinBox</cstring>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="stopFreqSpinBox">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="suffix">
<string> MHz</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="minimum">
<double>24.000000000000000</double>
</property>
<property name="maximum">
<double>1766.000000000000000</double>
</property>
<property name="value">
<double>108.000000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Bin size:</string>
</property>
<property name="buddy">
<cstring>binSizeSpinBox</cstring>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="binSizeSpinBox">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="suffix">
<string> kHz</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="maximum">
<double>2800.000000000000000</double>
</property>
<property name="value">
<double>10.000000000000000</double>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Settings</string>
<item row="1" column="0" colspan="2">
<widget class="QPushButton" name="singleShotButton">
<property name="text">
<string>Si&amp;ngle shot</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Interval [s]:</string>
</property>
<property name="buddy">
<cstring>intervalSpinBox</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Gain [dB]:</string>
</property>
<property name="buddy">
<cstring>gainSpinBox</cstring>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QSpinBox" name="intervalSpinBox">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>3600</number>
</property>
<property name="value">
<number>10</number>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="gainSpinBox">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="specialValueText">
<string>auto</string>
</property>
<property name="minimum">
<number>-1</number>
</property>
<property name="maximum">
<number>49</number>
</property>
<property name="value">
<number>-1</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Corr. [ppm]:</string>
</property>
<property name="buddy">
<cstring>ppmSpinBox</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Crop [%]:</string>
</property>
<property name="buddy">
<cstring>cropSpinBox</cstring>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QSpinBox" name="ppmSpinBox">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="minimum">
<number>-999</number>
</property>
<property name="maximum">
<number>999</number>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QSpinBox" name="cropSpinBox">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Levels</string>
<item row="2" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>561</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="frequencyDockWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>208</width>
<height>166</height>
</size>
</property>
<property name="features">
<set>QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable</set>
</property>
<property name="windowTitle">
<string>Frequency</string>
</property>
<attribute name="dockWidgetArea">
<number>2</number>
</attribute>
<widget class="QWidget" name="frequencyDockWidgetContents">
<layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Start:</string>
</property>
<property name="buddy">
<cstring>startFreqSpinBox</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QDoubleSpinBox" name="startFreqSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="showGroupSeparator" stdset="0">
<bool>true</bool>
</property>
<property name="suffix">
<string> MHz</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>2200.000000000000000</double>
</property>
<property name="value">
<double>87.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Stop:</string>
</property>
<property name="buddy">
<cstring>stopFreqSpinBox</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="stopFreqSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="showGroupSeparator" stdset="0">
<bool>true</bool>
</property>
<property name="suffix">
<string> MHz</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>2200.000000000000000</double>
</property>
<property name="value">
<double>108.000000000000000</double>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>&amp;Bin size:</string>
</property>
<property name="buddy">
<cstring>binSizeSpinBox</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDoubleSpinBox" name="binSizeSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="showGroupSeparator" stdset="0">
<bool>true</bool>
</property>
<property name="suffix">
<string> kHz</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>10000.000000000000000</double>
</property>
<property name="value">
<double>10.000000000000000</double>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="settingsDockWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="features">
<set>QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable</set>
</property>
<property name="windowTitle">
<string>Settings</string>
</property>
<attribute name="dockWidgetArea">
<number>2</number>
</attribute>
<widget class="QWidget" name="settingsDockWidgetContents">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>&amp;Interval [s]:</string>
</property>
<property name="buddy">
<cstring>intervalSpinBox</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_6">
<property name="text">
<string>&amp;Gain [dB]:</string>
</property>
<property name="buddy">
<cstring>gainSpinBox</cstring>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QDoubleSpinBox" name="intervalSpinBox">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="maximum">
<double>999.000000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Corr. [ppm]:</string>
</property>
<property name="buddy">
<cstring>ppmSpinBox</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Crop [%]:</string>
</property>
<property name="buddy">
<cstring>cropSpinBox</cstring>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QSpinBox" name="ppmSpinBox">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="minimum">
<number>-999</number>
</property>
<property name="maximum">
<number>999</number>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="mainCurveCheckBox">
<property name="text">
<string>Main curve</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<widget class="QPushButton" name="colorsButton">
<property name="text">
<string>Colors...</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="peakHoldMaxCheckBox">
<property name="text">
<string>Max. hold</string>
</property>
</widget>
</item>
<item row="5" column="1" colspan="2">
<widget class="QCheckBox" name="peakHoldMinCheckBox">
<property name="text">
<string>Min. hold</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="averageCheckBox">
<property name="text">
<string>Average</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QCheckBox" name="smoothCheckBox">
<property name="text">
<string>Smoothing</string>
</property>
</widget>
</item>
<item row="7" column="2">
<widget class="QToolButton" name="smoothButton">
<property name="text">
<string>...</string>
</property>
<property name="autoRaise">
<bool>false</bool>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QCheckBox" name="persistenceCheckBox">
<property name="text">
<string>Persistence</string>
</property>
</widget>
</item>
<item row="8" column="2">
<widget class="QToolButton" name="persistenceButton">
<property name="text">
<string>...</string>
</property>
<property name="autoRaise">
<bool>false</bool>
</property>
</widget>
</item>
<item row="11" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="1" colspan="2">
<widget class="QSpinBox" name="cropSpinBox">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QDoubleSpinBox" name="gainSpinBox">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="specialValueText">
<string>auto</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="minimum">
<double>-1.000000000000000</double>
</property>
<property name="maximum">
<double>999.000000000000000</double>
</property>
<property name="singleStep">
<double>1.000000000000000</double>
</property>
<property name="value">
<double>-1.000000000000000</double>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QCheckBox" name="baselineCheckBox">
<property name="text">
<string>Baseline</string>
</property>
</widget>
</item>
<item row="9" column="2">
<widget class="QToolButton" name="baselineButton">
<property name="text">
<string>...</string>
</property>
<property name="autoRaise">
<bool>false</bool>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QCheckBox" name="subtractBaselineCheckBox">
<property name="text">
<string>Subtract baseline</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="levelsDockWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="features">
<set>QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable</set>
</property>
<property name="windowTitle">
<string>Levels</string>
</property>
<attribute name="dockWidgetArea">
<number>2</number>
</attribute>
<widget class="QWidget" name="levelsDockWidgetContents">
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="GraphicsLayoutWidget" name="histogramPlotLayout">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="GraphicsLayoutWidget" name="histogramPlotLayout">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
@ -377,6 +596,18 @@
<tabstop>gainSpinBox</tabstop>
<tabstop>ppmSpinBox</tabstop>
<tabstop>cropSpinBox</tabstop>
<tabstop>mainCurveCheckBox</tabstop>
<tabstop>colorsButton</tabstop>
<tabstop>peakHoldMaxCheckBox</tabstop>
<tabstop>peakHoldMinCheckBox</tabstop>
<tabstop>averageCheckBox</tabstop>
<tabstop>smoothCheckBox</tabstop>
<tabstop>smoothButton</tabstop>
<tabstop>persistenceCheckBox</tabstop>
<tabstop>persistenceButton</tabstop>
<tabstop>baselineCheckBox</tabstop>
<tabstop>baselineButton</tabstop>
<tabstop>subtractBaselineCheckBox</tabstop>
<tabstop>histogramPlotLayout</tabstop>
<tabstop>mainPlotLayout</tabstop>
<tabstop>waterfallPlotLayout</tabstop>

View File

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QSpectrumAnalyzerBaseline</class>
<widget class="QDialog" name="QSpectrumAnalyzerBaseline">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>500</width>
<height>100</height>
</rect>
</property>
<property name="windowTitle">
<string>Baseline - QSpectrumAnalyzer</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Baseline &amp;file:</string>
</property>
<property name="buddy">
<cstring>baselineFileEdit</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="baselineFileEdit"/>
</item>
<item>
<widget class="QToolButton" name="baselineFileButton">
<property name="minimumSize">
<size>
<width>50</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>baselineFileEdit</tabstop>
<tabstop>baselineFileButton</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>QSpectrumAnalyzerBaseline</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>224</x>
<y>72</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>99</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>QSpectrumAnalyzerBaseline</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>292</x>
<y>78</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>99</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,234 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QSpectrumAnalyzerColors</class>
<widget class="QDialog" name="QSpectrumAnalyzerColors">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>253</width>
<height>266</height>
</rect>
</property>
<property name="windowTitle">
<string>Colors - QSpectrumAnalyzer</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Main curve color:</string>
</property>
<property name="buddy">
<cstring>mainColorButton</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="ColorButton" name="mainColorButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Max. peak &amp;hold color:</string>
</property>
<property name="buddy">
<cstring>peakHoldMaxColorButton</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="ColorButton" name="peakHoldMaxColorButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>M&amp;in. peak hold color:</string>
</property>
<property name="buddy">
<cstring>peakHoldMinColorButton</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="ColorButton" name="peakHoldMinColorButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Average &amp;color:</string>
</property>
<property name="buddy">
<cstring>averageColorButton</cstring>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="ColorButton" name="averageColorButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Persistence co&amp;lor:</string>
</property>
<property name="buddy">
<cstring>persistenceColorButton</cstring>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="ColorButton" name="persistenceColorButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>&amp;Baseline color:</string>
</property>
<property name="buddy">
<cstring>baselineColorButton</cstring>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="ColorButton" name="baselineColorButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>2</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ColorButton</class>
<extends>QPushButton</extends>
<header>pyqtgraph</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>mainColorButton</tabstop>
<tabstop>peakHoldMaxColorButton</tabstop>
<tabstop>peakHoldMinColorButton</tabstop>
<tabstop>averageColorButton</tabstop>
<tabstop>persistenceColorButton</tabstop>
<tabstop>baselineColorButton</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>QSpectrumAnalyzerColors</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>246</x>
<y>259</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>265</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>QSpectrumAnalyzerColors</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>246</x>
<y>259</y>
</hint>
<hint type="destinationlabel">
<x>252</x>
<y>265</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QSpectrumAnalyzerPersistence</class>
<widget class="QDialog" name="QSpectrumAnalyzerPersistence">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>250</width>
<height>130</height>
</rect>
</property>
<property name="windowTitle">
<string>Persistence - QSpectrumAnalyzer</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Decay function:</string>
</property>
<property name="buddy">
<cstring>decayFunctionComboBox</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="decayFunctionComboBox">
<property name="currentIndex">
<number>1</number>
</property>
<item>
<property name="text">
<string>linear</string>
</property>
</item>
<item>
<property name="text">
<string>exponential</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Persistence length:</string>
</property>
<property name="buddy">
<cstring>persistenceLengthSpinBox</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="persistenceLengthSpinBox">
<property name="value">
<number>5</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>decayFunctionComboBox</tabstop>
<tabstop>persistenceLengthSpinBox</tabstop>
<tabstop>buttonBox</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>QSpectrumAnalyzerPersistence</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>243</x>
<y>123</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>129</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>QSpectrumAnalyzerPersistence</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>243</x>
<y>123</y>
</hint>
<hint type="destinationlabel">
<x>249</x>
<y>129</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -6,34 +6,82 @@
<rect>
<x>0</x>
<y>0</y>
<width>500</width>
<height>150</height>
<width>600</width>
<height>388</height>
</rect>
</property>
<property name="windowTitle">
<string>QSpectrumAnalyzer - Settings</string>
<string>Settings - QSpectrumAnalyzer</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Rtl_power executable:</string>
<string>&amp;Backend:</string>
</property>
<property name="buddy">
<cstring>backendComboBox</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="backendComboBox">
<item>
<property name="text">
<string>soapy_power</string>
</property>
</item>
<item>
<property name="text">
<string>rx_power</string>
</property>
</item>
<item>
<property name="text">
<string>rtl_power_fftw</string>
</property>
</item>
<item>
<property name="text">
<string>rtl_power</string>
</property>
</item>
<item>
<property name="text">
<string>hackrf_sweep</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>E&amp;xecutable:</string>
</property>
<property name="buddy">
<cstring>executableEdit</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="rtlPowerExecutableEdit">
<widget class="QLineEdit" name="executableEdit">
<property name="text">
<string>rtl_power</string>
<string>soapy_power</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="rtlPowerExecutableButton">
<widget class="QToolButton" name="executableButton">
<property name="minimumSize">
<size>
<width>50</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
@ -41,14 +89,37 @@
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<item row="3" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Waterfall history size:</string>
<string>&amp;Device:</string>
</property>
<property name="buddy">
<cstring>deviceEdit</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<item row="4" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Sa&amp;mple rate:</string>
</property>
<property name="buddy">
<cstring>sampleRateSpinBox</cstring>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Waterfall history size:</string>
</property>
<property name="buddy">
<cstring>waterfallHistorySizeSpinBox</cstring>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QSpinBox" name="waterfallHistorySizeSpinBox">
<property name="minimum">
<number>1</number>
@ -61,6 +132,157 @@
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Bandwidt&amp;h:</string>
</property>
<property name="buddy">
<cstring>bandwidthSpinBox</cstring>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_8">
<property name="toolTip">
<string>Negative frequency for upconverters, positive frequency for downconverters.</string>
</property>
<property name="text">
<string>&amp;LNB LO:</string>
</property>
<property name="buddy">
<cstring>lnbSpinBox</cstring>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Add&amp;itional parameters:</string>
</property>
<property name="buddy">
<cstring>paramsEdit</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLineEdit" name="paramsEdit"/>
</item>
<item>
<widget class="QToolButton" name="paramsHelpButton">
<property name="minimumSize">
<size>
<width>50</width>
<height>0</height>
</size>
</property>
<property name="text">
<string> ? </string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLineEdit" name="deviceEdit"/>
</item>
<item>
<widget class="QToolButton" name="deviceHelpButton">
<property name="minimumSize">
<size>
<width>50</width>
<height>0</height>
</size>
</property>
<property name="text">
<string> ? </string>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="1">
<widget class="QDoubleSpinBox" name="sampleRateSpinBox">
<property name="showGroupSeparator" stdset="0">
<bool>true</bool>
</property>
<property name="suffix">
<string> MHz</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>999999.989999999990687</double>
</property>
<property name="singleStep">
<double>0.010000000000000</double>
</property>
<property name="value">
<double>61.439999999999998</double>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QDoubleSpinBox" name="bandwidthSpinBox">
<property name="showGroupSeparator" stdset="0">
<bool>true</bool>
</property>
<property name="suffix">
<string> MHz</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>999999.989999999990687</double>
</property>
<property name="singleStep">
<double>0.010000000000000</double>
</property>
<property name="value">
<double>0.000000000000000</double>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QDoubleSpinBox" name="lnbSpinBox">
<property name="toolTip">
<string>Negative frequency for upconverters, positive frequency for downconverters.</string>
</property>
<property name="showGroupSeparator" stdset="0">
<bool>true</bool>
</property>
<property name="suffix">
<string> MHz</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="minimum">
<double>-999999.998999999952503</double>
</property>
<property name="maximum">
<double>999999.998999999952503</double>
</property>
<property name="singleStep">
<double>0.010000000000000</double>
</property>
<property name="value">
<double>0.000000000000000</double>
</property>
</widget>
</item>
</layout>
</item>
<item>
@ -89,10 +311,17 @@
</layout>
</widget>
<tabstops>
<tabstop>rtlPowerExecutableEdit</tabstop>
<tabstop>rtlPowerExecutableButton</tabstop>
<tabstop>backendComboBox</tabstop>
<tabstop>executableEdit</tabstop>
<tabstop>executableButton</tabstop>
<tabstop>paramsEdit</tabstop>
<tabstop>paramsHelpButton</tabstop>
<tabstop>deviceEdit</tabstop>
<tabstop>deviceHelpButton</tabstop>
<tabstop>sampleRateSpinBox</tabstop>
<tabstop>bandwidthSpinBox</tabstop>
<tabstop>lnbSpinBox</tabstop>
<tabstop>waterfallHistorySizeSpinBox</tabstop>
<tabstop>buttonBox</tabstop>
</tabstops>
<resources/>
<connections>
@ -103,8 +332,8 @@
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>224</x>
<y>126</y>
<x>254</x>
<y>377</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
@ -119,8 +348,8 @@
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>292</x>
<y>132</y>
<x>322</x>
<y>377</y>
</hint>
<hint type="destinationlabel">
<x>286</x>

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QSpectrumAnalyzerSettingsHelp</class>
<widget class="QDialog" name="QSpectrumAnalyzerSettingsHelp">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1200</width>
<height>700</height>
</rect>
</property>
<property name="windowTitle">
<string>Help - QSpectrumAnalyzer</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPlainTextEdit" name="helpTextEdit">
<property name="undoRedoEnabled">
<bool>false</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>helpTextEdit</tabstop>
<tabstop>buttonBox</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>QSpectrumAnalyzerSettingsHelp</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>224</x>
<y>672</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>QSpectrumAnalyzerSettingsHelp</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>292</x>
<y>678</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QSpectrumAnalyzerSmoothing</class>
<widget class="QDialog" name="QSpectrumAnalyzerSmoothing">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>250</width>
<height>130</height>
</rect>
</property>
<property name="windowTitle">
<string>Smoothing - QSpectrumAnalyzer</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>&amp;Window function:</string>
</property>
<property name="buddy">
<cstring>windowFunctionComboBox</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="windowFunctionComboBox">
<property name="currentIndex">
<number>1</number>
</property>
<item>
<property name="text">
<string>rectangular</string>
</property>
</item>
<item>
<property name="text">
<string>hanning</string>
</property>
</item>
<item>
<property name="text">
<string>hamming</string>
</property>
</item>
<item>
<property name="text">
<string>bartlett</string>
</property>
</item>
<item>
<property name="text">
<string>blackman</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Window len&amp;gth:</string>
</property>
<property name="buddy">
<cstring>windowLengthSpinBox</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="windowLengthSpinBox">
<property name="minimum">
<number>3</number>
</property>
<property name="maximum">
<number>1001</number>
</property>
<property name="value">
<number>11</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>windowFunctionComboBox</tabstop>
<tabstop>windowLengthSpinBox</tabstop>
<tabstop>buttonBox</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>QSpectrumAnalyzerSmoothing</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>218</x>
<y>104</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>129</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>QSpectrumAnalyzerSmoothing</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>218</x>
<y>110</y>
</hint>
<hint type="destinationlabel">
<x>224</x>
<y>129</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,139 @@
from Qt import QtCore, QtGui, QtWidgets
from qspectrumanalyzer import backends
from qspectrumanalyzer.ui_qspectrumanalyzer_settings import Ui_QSpectrumAnalyzerSettings
from qspectrumanalyzer.ui_qspectrumanalyzer_settings_help import Ui_QSpectrumAnalyzerSettingsHelp
class QSpectrumAnalyzerSettings(QtWidgets.QDialog, Ui_QSpectrumAnalyzerSettings):
"""QSpectrumAnalyzer settings dialog"""
def __init__(self, parent=None):
# Initialize UI
super().__init__(parent)
self.setupUi(self)
self.params_help_dialog = None
self.device_help_dialog = None
# Load settings
settings = QtCore.QSettings()
self.executableEdit.setText(settings.value("executable", "soapy_power"))
self.deviceEdit.setText(settings.value("device", ""))
self.lnbSpinBox.setValue(settings.value("lnb_lo", 0, float) / 1e6)
self.waterfallHistorySizeSpinBox.setValue(settings.value("waterfall_history_size", 100, int))
backend = settings.value("backend", "soapy_power")
try:
backend_module = getattr(backends, backend)
except AttributeError:
backend_module = backends.soapy_power
self.paramsEdit.setText(settings.value("params", backend_module.Info.additional_params))
self.deviceHelpButton.setEnabled(bool(backend_module.Info.help_device))
self.sampleRateSpinBox.setMinimum(backend_module.Info.sample_rate_min / 1e6)
self.sampleRateSpinBox.setMaximum(backend_module.Info.sample_rate_max / 1e6)
self.sampleRateSpinBox.setValue(settings.value("sample_rate", backend_module.Info.sample_rate, float) / 1e6)
self.bandwidthSpinBox.setMinimum(backend_module.Info.bandwidth_min / 1e6)
self.bandwidthSpinBox.setMaximum(backend_module.Info.bandwidth_max / 1e6)
self.bandwidthSpinBox.setValue(settings.value("bandwidth", backend_module.Info.bandwidth, float) / 1e6)
self.backendComboBox.blockSignals(True)
self.backendComboBox.clear()
for b in sorted(backends.__all__):
self.backendComboBox.addItem(b)
i = self.backendComboBox.findText(backend)
if i == -1:
self.backendComboBox.setCurrentIndex(0)
else:
self.backendComboBox.setCurrentIndex(i)
self.backendComboBox.blockSignals(False)
@QtCore.Slot()
def on_executableButton_clicked(self):
"""Open file dialog when button is clicked"""
filename = QtWidgets.QFileDialog.getOpenFileName(self, self.tr("Select executable - QSpectrumAnalyzer"))[0]
if filename:
self.executableEdit.setText(filename)
@QtCore.Slot()
def on_paramsHelpButton_clicked(self):
"""Open additional parameters help dialog when button is clicked"""
try:
backend_module = getattr(backends, self.backendComboBox.currentText())
except AttributeError:
backend_module = backends.soapy_power
self.params_help_dialog = QSpectrumAnalyzerSettingsHelp(
backend_module.Info.help_params(self.executableEdit.text()),
parent=self
)
self.params_help_dialog.show()
self.params_help_dialog.raise_()
self.params_help_dialog.activateWindow()
@QtCore.Slot()
def on_deviceHelpButton_clicked(self):
"""Open device help dialog when button is clicked"""
try:
backend_module = getattr(backends, self.backendComboBox.currentText())
except AttributeError:
backend_module = backends.soapy_power
self.device_help_dialog = QSpectrumAnalyzerSettingsHelp(
backend_module.Info.help_device(self.executableEdit.text(), self.deviceEdit.text()),
parent=self
)
self.device_help_dialog.show()
self.device_help_dialog.raise_()
self.device_help_dialog.activateWindow()
@QtCore.Slot(str)
def on_backendComboBox_currentIndexChanged(self, text):
"""Change executable when backend is changed"""
self.executableEdit.setText(text)
self.deviceEdit.setText("")
try:
backend_module = getattr(backends, text)
except AttributeError:
backend_module = backends.soapy_power
self.paramsEdit.setText(backend_module.Info.additional_params)
self.deviceHelpButton.setEnabled(bool(backend_module.Info.help_device))
self.sampleRateSpinBox.setMinimum(backend_module.Info.sample_rate_min / 1e6)
self.sampleRateSpinBox.setMaximum(backend_module.Info.sample_rate_max / 1e6)
self.sampleRateSpinBox.setValue(backend_module.Info.sample_rate / 1e6)
self.bandwidthSpinBox.setMinimum(backend_module.Info.bandwidth_min / 1e6)
self.bandwidthSpinBox.setMaximum(backend_module.Info.bandwidth_max / 1e6)
self.bandwidthSpinBox.setValue(backend_module.Info.bandwidth / 1e6)
def accept(self):
"""Save settings when dialog is accepted"""
settings = QtCore.QSettings()
settings.setValue("backend", self.backendComboBox.currentText())
settings.setValue("executable", self.executableEdit.text())
settings.setValue("params", self.paramsEdit.text())
settings.setValue("device", self.deviceEdit.text())
settings.setValue("sample_rate", self.sampleRateSpinBox.value() * 1e6)
settings.setValue("bandwidth", self.bandwidthSpinBox.value() * 1e6)
settings.setValue("lnb_lo", self.lnbSpinBox.value() * 1e6)
settings.setValue("waterfall_history_size", self.waterfallHistorySizeSpinBox.value())
QtWidgets.QDialog.accept(self)
class QSpectrumAnalyzerSettingsHelp(QtWidgets.QDialog, Ui_QSpectrumAnalyzerSettingsHelp):
"""QSpectrumAnalyzer settings help dialog"""
def __init__(self, text, parent=None):
# Initialize UI
super().__init__(parent)
self.setupUi(self)
monospace_font = QtGui.QFont('monospace')
monospace_font.setStyleHint(QtGui.QFont.Monospace)
self.helpTextEdit.setFont(monospace_font)
self.helpTextEdit.setPlainText(text)

View File

@ -0,0 +1,29 @@
from Qt import QtCore, QtWidgets
from qspectrumanalyzer.ui_qspectrumanalyzer_smoothing import Ui_QSpectrumAnalyzerSmoothing
class QSpectrumAnalyzerSmoothing(QtWidgets.QDialog, Ui_QSpectrumAnalyzerSmoothing):
"""QSpectrumAnalyzer spectrum smoothing dialog"""
def __init__(self, parent=None):
# Initialize UI
super().__init__(parent)
self.setupUi(self)
# Load settings
settings = QtCore.QSettings()
self.windowLengthSpinBox.setValue(settings.value("smooth_length", 11, int))
window_function = settings.value("smooth_window", "hanning")
i = self.windowFunctionComboBox.findText(window_function)
if i == -1:
self.windowFunctionComboBox.setCurrentIndex(0)
else:
self.windowFunctionComboBox.setCurrentIndex(i)
def accept(self):
"""Save settings when dialog is accepted"""
settings = QtCore.QSettings()
settings.setValue("smooth_length", self.windowLengthSpinBox.value())
settings.setValue("smooth_window", self.windowFunctionComboBox.currentText())
QtWidgets.QDialog.accept(self)

View File

@ -0,0 +1,81 @@
import sys, subprocess
# Basic attributes and exceptions
PIPE = subprocess.PIPE
STDOUT = subprocess.STDOUT
DEVNULL = subprocess.DEVNULL
SubprocessError = subprocess.SubprocessError
TimeoutExpired = subprocess.TimeoutExpired
CalledProcessError = subprocess.CalledProcessError
# Windows-only attributes and functions
if sys.platform == 'win32':
import msvcrt
import _winapi
# creationflags
CREATE_NEW_CONSOLE = subprocess.CREATE_NEW_CONSOLE
CREATE_NEW_PROCESS_GROUP = subprocess.CREATE_NEW_PROCESS_GROUP
# startupinfo
STARTUPINFO = subprocess.STARTUPINFO
STARTF_USESTDHANDLES = subprocess.STARTF_USESTDHANDLES
STARTF_USESHOWWINDOW = subprocess.STARTF_USESHOWWINDOW
SW_HIDE = subprocess.SW_HIDE
# file handles
Handle = subprocess.Handle
STD_INPUT_HANDLE = subprocess.STD_INPUT_HANDLE
STD_OUTPUT_HANDLE = subprocess.STD_OUTPUT_HANDLE
STD_ERROR_HANDLE = subprocess.STD_ERROR_HANDLE
def make_inheritable_handle(fd):
"""Create inheritable duplicate of handle from file descriptor"""
h = _winapi.DuplicateHandle(
_winapi.GetCurrentProcess(),
msvcrt.get_osfhandle(fd),
_winapi.GetCurrentProcess(), 0, 1,
_winapi.DUPLICATE_SAME_ACCESS
)
return subprocess.Handle(h)
def hide_console_window(startupinfo=None):
"""Returns altered startupinfo to hide console window on Windows"""
if sys.platform != 'win32':
return None
if not startupinfo:
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
return startupinfo
class Popen(subprocess.Popen):
"""subprocess.Popen with ability to hide console window on Windows"""
def __init__(self, *pargs, console=True, **kwargs):
if not console:
kwargs['startupinfo'] = hide_console_window(kwargs.get('startupinfo'))
super().__init__(*pargs, **kwargs)
def call(*pargs, console=True, **kwargs):
"""subprocess.call with ability to hide console window on Windows"""
if not console:
kwargs['startupinfo'] = hide_console_window(kwargs.get('startupinfo'))
return subprocess.call(*pargs, **kwargs)
def check_call(*pargs, console=True, **kwargs):
"""subprocess.check_call with ability to hide console window on Windows"""
if not console:
kwargs['startupinfo'] = hide_console_window(kwargs.get('startupinfo'))
return subprocess.check_call(*pargs, **kwargs)
def check_output(*pargs, console=True, **kwargs):
"""subprocess.check_output with ability to hide console window on Windows"""
if not console:
kwargs['startupinfo'] = hide_console_window(kwargs.get('startupinfo'))
return subprocess.check_output(*pargs, **kwargs)

View File

@ -2,190 +2,275 @@
# Form implementation generated from reading ui file 'qspectrumanalyzer/qspectrumanalyzer.ui'
#
# Created: Mon Mar 2 23:12:42 2015
# by: PyQt4 UI code generator 4.11.3
# Created by: PyQt5 UI code generator 5.8
#
# WARNING! All changes made in this file will be lost!
from PyQt4 import QtCore, QtGui
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
def _fromUtf8(s):
return s
try:
_encoding = QtGui.QApplication.UnicodeUTF8
def _translate(context, text, disambig):
return QtGui.QApplication.translate(context, text, disambig, _encoding)
except AttributeError:
def _translate(context, text, disambig):
return QtGui.QApplication.translate(context, text, disambig)
from Qt import QtCore, QtGui, QtWidgets
class Ui_QSpectrumAnalyzerMainWindow(object):
def setupUi(self, QSpectrumAnalyzerMainWindow):
QSpectrumAnalyzerMainWindow.setObjectName(_fromUtf8("QSpectrumAnalyzerMainWindow"))
QSpectrumAnalyzerMainWindow.resize(1100, 680)
self.centralwidget = QtGui.QWidget(QSpectrumAnalyzerMainWindow)
self.centralwidget.setObjectName(_fromUtf8("centralwidget"))
self.horizontalLayout = QtGui.QHBoxLayout(self.centralwidget)
self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout"))
self.plotSplitter = QtGui.QSplitter(self.centralwidget)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
QSpectrumAnalyzerMainWindow.setObjectName("QSpectrumAnalyzerMainWindow")
QSpectrumAnalyzerMainWindow.resize(1200, 892)
self.centralwidget = QtWidgets.QWidget(QSpectrumAnalyzerMainWindow)
self.centralwidget.setObjectName("centralwidget")
self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget)
self.horizontalLayout.setObjectName("horizontalLayout")
self.plotSplitter = QtWidgets.QSplitter(self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.plotSplitter.sizePolicy().hasHeightForWidth())
self.plotSplitter.setSizePolicy(sizePolicy)
self.plotSplitter.setOrientation(QtCore.Qt.Vertical)
self.plotSplitter.setObjectName(_fromUtf8("plotSplitter"))
self.plotSplitter.setObjectName("plotSplitter")
self.mainPlotLayout = GraphicsLayoutWidget(self.plotSplitter)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.mainPlotLayout.sizePolicy().hasHeightForWidth())
self.mainPlotLayout.setSizePolicy(sizePolicy)
self.mainPlotLayout.setObjectName(_fromUtf8("mainPlotLayout"))
self.mainPlotLayout.setObjectName("mainPlotLayout")
self.waterfallPlotLayout = GraphicsLayoutWidget(self.plotSplitter)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.waterfallPlotLayout.sizePolicy().hasHeightForWidth())
self.waterfallPlotLayout.setSizePolicy(sizePolicy)
self.waterfallPlotLayout.setObjectName(_fromUtf8("waterfallPlotLayout"))
self.waterfallPlotLayout.setObjectName("waterfallPlotLayout")
self.horizontalLayout.addWidget(self.plotSplitter)
QSpectrumAnalyzerMainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtGui.QMenuBar(QSpectrumAnalyzerMainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 1100, 27))
self.menubar.setObjectName(_fromUtf8("menubar"))
self.menu_File = QtGui.QMenu(self.menubar)
self.menu_File.setObjectName(_fromUtf8("menu_File"))
self.menu_Help = QtGui.QMenu(self.menubar)
self.menu_Help.setObjectName(_fromUtf8("menu_Help"))
self.menubar = QtWidgets.QMenuBar(QSpectrumAnalyzerMainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 1200, 32))
self.menubar.setObjectName("menubar")
self.menu_File = QtWidgets.QMenu(self.menubar)
self.menu_File.setObjectName("menu_File")
self.menu_Help = QtWidgets.QMenu(self.menubar)
self.menu_Help.setObjectName("menu_Help")
QSpectrumAnalyzerMainWindow.setMenuBar(self.menubar)
self.statusbar = QtGui.QStatusBar(QSpectrumAnalyzerMainWindow)
self.statusbar.setObjectName(_fromUtf8("statusbar"))
self.statusbar = QtWidgets.QStatusBar(QSpectrumAnalyzerMainWindow)
self.statusbar.setObjectName("statusbar")
QSpectrumAnalyzerMainWindow.setStatusBar(self.statusbar)
self.dockWidget = QtGui.QDockWidget(QSpectrumAnalyzerMainWindow)
self.dockWidget.setFeatures(QtGui.QDockWidget.DockWidgetFloatable|QtGui.QDockWidget.DockWidgetMovable)
self.dockWidget.setObjectName(_fromUtf8("dockWidget"))
self.dockWidgetContents = QtGui.QWidget()
self.dockWidgetContents.setObjectName(_fromUtf8("dockWidgetContents"))
self.verticalLayout_3 = QtGui.QVBoxLayout(self.dockWidgetContents)
self.verticalLayout_3.setObjectName(_fromUtf8("verticalLayout_3"))
self.groupBox_2 = QtGui.QGroupBox(self.dockWidgetContents)
self.groupBox_2.setObjectName(_fromUtf8("groupBox_2"))
self.gridLayout = QtGui.QGridLayout(self.groupBox_2)
self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
self.startButton = QtGui.QPushButton(self.groupBox_2)
self.startButton.setObjectName(_fromUtf8("startButton"))
self.gridLayout.addWidget(self.startButton, 0, 0, 1, 1)
self.stopButton = QtGui.QPushButton(self.groupBox_2)
self.stopButton.setObjectName(_fromUtf8("stopButton"))
self.gridLayout.addWidget(self.stopButton, 0, 1, 1, 1)
self.singleShotButton = QtGui.QPushButton(self.groupBox_2)
self.singleShotButton.setObjectName(_fromUtf8("singleShotButton"))
self.gridLayout.addWidget(self.singleShotButton, 1, 0, 1, 2)
self.verticalLayout_3.addWidget(self.groupBox_2)
self.groupBox = QtGui.QGroupBox(self.dockWidgetContents)
self.groupBox.setObjectName(_fromUtf8("groupBox"))
self.verticalLayout = QtGui.QVBoxLayout(self.groupBox)
self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
self.label_2 = QtGui.QLabel(self.groupBox)
self.label_2.setObjectName(_fromUtf8("label_2"))
self.verticalLayout.addWidget(self.label_2)
self.startFreqSpinBox = QtGui.QDoubleSpinBox(self.groupBox)
self.controlsDockWidget = QtWidgets.QDockWidget(QSpectrumAnalyzerMainWindow)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.controlsDockWidget.sizePolicy().hasHeightForWidth())
self.controlsDockWidget.setSizePolicy(sizePolicy)
self.controlsDockWidget.setMinimumSize(QtCore.QSize(190, 130))
self.controlsDockWidget.setFeatures(QtWidgets.QDockWidget.DockWidgetFloatable|QtWidgets.QDockWidget.DockWidgetMovable)
self.controlsDockWidget.setObjectName("controlsDockWidget")
self.controlsDockWidgetContents = QtWidgets.QWidget()
self.controlsDockWidgetContents.setObjectName("controlsDockWidgetContents")
self.gridLayout_2 = QtWidgets.QGridLayout(self.controlsDockWidgetContents)
self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
self.gridLayout_2.setObjectName("gridLayout_2")
self.startButton = QtWidgets.QPushButton(self.controlsDockWidgetContents)
self.startButton.setObjectName("startButton")
self.gridLayout_2.addWidget(self.startButton, 0, 0, 1, 1)
self.stopButton = QtWidgets.QPushButton(self.controlsDockWidgetContents)
self.stopButton.setObjectName("stopButton")
self.gridLayout_2.addWidget(self.stopButton, 0, 1, 1, 1)
self.singleShotButton = QtWidgets.QPushButton(self.controlsDockWidgetContents)
self.singleShotButton.setObjectName("singleShotButton")
self.gridLayout_2.addWidget(self.singleShotButton, 1, 0, 1, 2)
spacerItem = QtWidgets.QSpacerItem(20, 561, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridLayout_2.addItem(spacerItem, 2, 0, 1, 1)
self.controlsDockWidget.setWidget(self.controlsDockWidgetContents)
QSpectrumAnalyzerMainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.controlsDockWidget)
self.frequencyDockWidget = QtWidgets.QDockWidget(QSpectrumAnalyzerMainWindow)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.frequencyDockWidget.sizePolicy().hasHeightForWidth())
self.frequencyDockWidget.setSizePolicy(sizePolicy)
self.frequencyDockWidget.setMinimumSize(QtCore.QSize(208, 166))
self.frequencyDockWidget.setFeatures(QtWidgets.QDockWidget.DockWidgetFloatable|QtWidgets.QDockWidget.DockWidgetMovable)
self.frequencyDockWidget.setObjectName("frequencyDockWidget")
self.frequencyDockWidgetContents = QtWidgets.QWidget()
self.frequencyDockWidgetContents.setObjectName("frequencyDockWidgetContents")
self.formLayout = QtWidgets.QFormLayout(self.frequencyDockWidgetContents)
self.formLayout.setFieldGrowthPolicy(QtWidgets.QFormLayout.ExpandingFieldsGrow)
self.formLayout.setContentsMargins(0, 0, 0, 0)
self.formLayout.setObjectName("formLayout")
self.label_2 = QtWidgets.QLabel(self.frequencyDockWidgetContents)
self.label_2.setObjectName("label_2")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label_2)
self.startFreqSpinBox = QtWidgets.QDoubleSpinBox(self.frequencyDockWidgetContents)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.startFreqSpinBox.sizePolicy().hasHeightForWidth())
self.startFreqSpinBox.setSizePolicy(sizePolicy)
self.startFreqSpinBox.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.startFreqSpinBox.setProperty("showGroupSeparator", True)
self.startFreqSpinBox.setDecimals(3)
self.startFreqSpinBox.setMinimum(24.0)
self.startFreqSpinBox.setMaximum(1766.0)
self.startFreqSpinBox.setMinimum(0.0)
self.startFreqSpinBox.setMaximum(2200.0)
self.startFreqSpinBox.setProperty("value", 87.0)
self.startFreqSpinBox.setObjectName(_fromUtf8("startFreqSpinBox"))
self.verticalLayout.addWidget(self.startFreqSpinBox)
self.label_3 = QtGui.QLabel(self.groupBox)
self.label_3.setObjectName(_fromUtf8("label_3"))
self.verticalLayout.addWidget(self.label_3)
self.stopFreqSpinBox = QtGui.QDoubleSpinBox(self.groupBox)
self.startFreqSpinBox.setObjectName("startFreqSpinBox")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.startFreqSpinBox)
self.label_3 = QtWidgets.QLabel(self.frequencyDockWidgetContents)
self.label_3.setObjectName("label_3")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.label_3)
self.stopFreqSpinBox = QtWidgets.QDoubleSpinBox(self.frequencyDockWidgetContents)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.stopFreqSpinBox.sizePolicy().hasHeightForWidth())
self.stopFreqSpinBox.setSizePolicy(sizePolicy)
self.stopFreqSpinBox.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.stopFreqSpinBox.setProperty("showGroupSeparator", True)
self.stopFreqSpinBox.setDecimals(3)
self.stopFreqSpinBox.setMinimum(24.0)
self.stopFreqSpinBox.setMaximum(1766.0)
self.stopFreqSpinBox.setMinimum(0.0)
self.stopFreqSpinBox.setMaximum(2200.0)
self.stopFreqSpinBox.setProperty("value", 108.0)
self.stopFreqSpinBox.setObjectName(_fromUtf8("stopFreqSpinBox"))
self.verticalLayout.addWidget(self.stopFreqSpinBox)
self.label = QtGui.QLabel(self.groupBox)
self.label.setObjectName(_fromUtf8("label"))
self.verticalLayout.addWidget(self.label)
self.binSizeSpinBox = QtGui.QDoubleSpinBox(self.groupBox)
self.stopFreqSpinBox.setObjectName("stopFreqSpinBox")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.stopFreqSpinBox)
self.label = QtWidgets.QLabel(self.frequencyDockWidgetContents)
self.label.setObjectName("label")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.label)
self.binSizeSpinBox = QtWidgets.QDoubleSpinBox(self.frequencyDockWidgetContents)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.binSizeSpinBox.sizePolicy().hasHeightForWidth())
self.binSizeSpinBox.setSizePolicy(sizePolicy)
self.binSizeSpinBox.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.binSizeSpinBox.setProperty("showGroupSeparator", True)
self.binSizeSpinBox.setDecimals(3)
self.binSizeSpinBox.setMaximum(2800.0)
self.binSizeSpinBox.setMinimum(0.0)
self.binSizeSpinBox.setMaximum(10000.0)
self.binSizeSpinBox.setProperty("value", 10.0)
self.binSizeSpinBox.setObjectName(_fromUtf8("binSizeSpinBox"))
self.verticalLayout.addWidget(self.binSizeSpinBox)
self.verticalLayout_3.addWidget(self.groupBox)
self.groupBox_3 = QtGui.QGroupBox(self.dockWidgetContents)
self.groupBox_3.setObjectName(_fromUtf8("groupBox_3"))
self.gridLayout_2 = QtGui.QGridLayout(self.groupBox_3)
self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2"))
self.label_4 = QtGui.QLabel(self.groupBox_3)
self.label_4.setObjectName(_fromUtf8("label_4"))
self.gridLayout_2.addWidget(self.label_4, 0, 0, 1, 1)
self.label_6 = QtGui.QLabel(self.groupBox_3)
self.label_6.setObjectName(_fromUtf8("label_6"))
self.gridLayout_2.addWidget(self.label_6, 0, 1, 1, 1)
self.intervalSpinBox = QtGui.QSpinBox(self.groupBox_3)
self.binSizeSpinBox.setObjectName("binSizeSpinBox")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.binSizeSpinBox)
spacerItem1 = QtWidgets.QSpacerItem(20, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.formLayout.setItem(3, QtWidgets.QFormLayout.SpanningRole, spacerItem1)
self.frequencyDockWidget.setWidget(self.frequencyDockWidgetContents)
QSpectrumAnalyzerMainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.frequencyDockWidget)
self.settingsDockWidget = QtWidgets.QDockWidget(QSpectrumAnalyzerMainWindow)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.settingsDockWidget.sizePolicy().hasHeightForWidth())
self.settingsDockWidget.setSizePolicy(sizePolicy)
self.settingsDockWidget.setFeatures(QtWidgets.QDockWidget.DockWidgetFloatable|QtWidgets.QDockWidget.DockWidgetMovable)
self.settingsDockWidget.setObjectName("settingsDockWidget")
self.settingsDockWidgetContents = QtWidgets.QWidget()
self.settingsDockWidgetContents.setObjectName("settingsDockWidgetContents")
self.gridLayout = QtWidgets.QGridLayout(self.settingsDockWidgetContents)
self.gridLayout.setContentsMargins(0, 0, 0, 0)
self.gridLayout.setObjectName("gridLayout")
self.label_4 = QtWidgets.QLabel(self.settingsDockWidgetContents)
self.label_4.setObjectName("label_4")
self.gridLayout.addWidget(self.label_4, 0, 0, 1, 1)
self.label_6 = QtWidgets.QLabel(self.settingsDockWidgetContents)
self.label_6.setObjectName("label_6")
self.gridLayout.addWidget(self.label_6, 0, 1, 1, 1)
self.intervalSpinBox = QtWidgets.QDoubleSpinBox(self.settingsDockWidgetContents)
self.intervalSpinBox.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.intervalSpinBox.setMinimum(1)
self.intervalSpinBox.setMaximum(3600)
self.intervalSpinBox.setProperty("value", 10)
self.intervalSpinBox.setObjectName(_fromUtf8("intervalSpinBox"))
self.gridLayout_2.addWidget(self.intervalSpinBox, 1, 0, 1, 1)
self.gainSpinBox = QtGui.QSpinBox(self.groupBox_3)
self.gainSpinBox.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.gainSpinBox.setMinimum(-1)
self.gainSpinBox.setMaximum(49)
self.gainSpinBox.setProperty("value", -1)
self.gainSpinBox.setObjectName(_fromUtf8("gainSpinBox"))
self.gridLayout_2.addWidget(self.gainSpinBox, 1, 1, 1, 1)
self.label_5 = QtGui.QLabel(self.groupBox_3)
self.label_5.setObjectName(_fromUtf8("label_5"))
self.gridLayout_2.addWidget(self.label_5, 2, 0, 1, 1)
self.label_7 = QtGui.QLabel(self.groupBox_3)
self.label_7.setObjectName(_fromUtf8("label_7"))
self.gridLayout_2.addWidget(self.label_7, 2, 1, 1, 1)
self.ppmSpinBox = QtGui.QSpinBox(self.groupBox_3)
self.intervalSpinBox.setMaximum(999.0)
self.intervalSpinBox.setProperty("value", 1.0)
self.intervalSpinBox.setObjectName("intervalSpinBox")
self.gridLayout.addWidget(self.intervalSpinBox, 1, 0, 1, 1)
self.label_5 = QtWidgets.QLabel(self.settingsDockWidgetContents)
self.label_5.setObjectName("label_5")
self.gridLayout.addWidget(self.label_5, 2, 0, 1, 1)
self.label_7 = QtWidgets.QLabel(self.settingsDockWidgetContents)
self.label_7.setObjectName("label_7")
self.gridLayout.addWidget(self.label_7, 2, 1, 1, 1)
self.ppmSpinBox = QtWidgets.QSpinBox(self.settingsDockWidgetContents)
self.ppmSpinBox.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.ppmSpinBox.setMinimum(-999)
self.ppmSpinBox.setMaximum(999)
self.ppmSpinBox.setObjectName(_fromUtf8("ppmSpinBox"))
self.gridLayout_2.addWidget(self.ppmSpinBox, 3, 0, 1, 1)
self.cropSpinBox = QtGui.QSpinBox(self.groupBox_3)
self.ppmSpinBox.setObjectName("ppmSpinBox")
self.gridLayout.addWidget(self.ppmSpinBox, 3, 0, 1, 1)
self.mainCurveCheckBox = QtWidgets.QCheckBox(self.settingsDockWidgetContents)
self.mainCurveCheckBox.setChecked(True)
self.mainCurveCheckBox.setObjectName("mainCurveCheckBox")
self.gridLayout.addWidget(self.mainCurveCheckBox, 4, 0, 1, 1)
self.colorsButton = QtWidgets.QPushButton(self.settingsDockWidgetContents)
self.colorsButton.setObjectName("colorsButton")
self.gridLayout.addWidget(self.colorsButton, 4, 1, 1, 2)
self.peakHoldMaxCheckBox = QtWidgets.QCheckBox(self.settingsDockWidgetContents)
self.peakHoldMaxCheckBox.setObjectName("peakHoldMaxCheckBox")
self.gridLayout.addWidget(self.peakHoldMaxCheckBox, 5, 0, 1, 1)
self.peakHoldMinCheckBox = QtWidgets.QCheckBox(self.settingsDockWidgetContents)
self.peakHoldMinCheckBox.setObjectName("peakHoldMinCheckBox")
self.gridLayout.addWidget(self.peakHoldMinCheckBox, 5, 1, 1, 2)
self.averageCheckBox = QtWidgets.QCheckBox(self.settingsDockWidgetContents)
self.averageCheckBox.setObjectName("averageCheckBox")
self.gridLayout.addWidget(self.averageCheckBox, 6, 0, 1, 1)
self.smoothCheckBox = QtWidgets.QCheckBox(self.settingsDockWidgetContents)
self.smoothCheckBox.setObjectName("smoothCheckBox")
self.gridLayout.addWidget(self.smoothCheckBox, 7, 0, 1, 1)
self.smoothButton = QtWidgets.QToolButton(self.settingsDockWidgetContents)
self.smoothButton.setAutoRaise(False)
self.smoothButton.setObjectName("smoothButton")
self.gridLayout.addWidget(self.smoothButton, 7, 2, 1, 1)
self.persistenceCheckBox = QtWidgets.QCheckBox(self.settingsDockWidgetContents)
self.persistenceCheckBox.setObjectName("persistenceCheckBox")
self.gridLayout.addWidget(self.persistenceCheckBox, 8, 0, 1, 1)
self.persistenceButton = QtWidgets.QToolButton(self.settingsDockWidgetContents)
self.persistenceButton.setAutoRaise(False)
self.persistenceButton.setObjectName("persistenceButton")
self.gridLayout.addWidget(self.persistenceButton, 8, 2, 1, 1)
spacerItem2 = QtWidgets.QSpacerItem(20, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridLayout.addItem(spacerItem2, 11, 0, 1, 1)
self.cropSpinBox = QtWidgets.QSpinBox(self.settingsDockWidgetContents)
self.cropSpinBox.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.cropSpinBox.setObjectName(_fromUtf8("cropSpinBox"))
self.gridLayout_2.addWidget(self.cropSpinBox, 3, 1, 1, 1)
self.verticalLayout_3.addWidget(self.groupBox_3)
self.groupBox_4 = QtGui.QGroupBox(self.dockWidgetContents)
self.groupBox_4.setObjectName(_fromUtf8("groupBox_4"))
self.verticalLayout_2 = QtGui.QVBoxLayout(self.groupBox_4)
self.verticalLayout_2.setObjectName(_fromUtf8("verticalLayout_2"))
self.histogramPlotLayout = GraphicsLayoutWidget(self.groupBox_4)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Ignored, QtGui.QSizePolicy.Expanding)
self.cropSpinBox.setObjectName("cropSpinBox")
self.gridLayout.addWidget(self.cropSpinBox, 3, 1, 1, 2)
self.gainSpinBox = QtWidgets.QDoubleSpinBox(self.settingsDockWidgetContents)
self.gainSpinBox.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.gainSpinBox.setDecimals(1)
self.gainSpinBox.setMinimum(-1.0)
self.gainSpinBox.setMaximum(999.0)
self.gainSpinBox.setSingleStep(1.0)
self.gainSpinBox.setProperty("value", -1.0)
self.gainSpinBox.setObjectName("gainSpinBox")
self.gridLayout.addWidget(self.gainSpinBox, 1, 1, 1, 2)
self.baselineCheckBox = QtWidgets.QCheckBox(self.settingsDockWidgetContents)
self.baselineCheckBox.setObjectName("baselineCheckBox")
self.gridLayout.addWidget(self.baselineCheckBox, 9, 0, 1, 1)
self.baselineButton = QtWidgets.QToolButton(self.settingsDockWidgetContents)
self.baselineButton.setAutoRaise(False)
self.baselineButton.setObjectName("baselineButton")
self.gridLayout.addWidget(self.baselineButton, 9, 2, 1, 1)
self.subtractBaselineCheckBox = QtWidgets.QCheckBox(self.settingsDockWidgetContents)
self.subtractBaselineCheckBox.setObjectName("subtractBaselineCheckBox")
self.gridLayout.addWidget(self.subtractBaselineCheckBox, 10, 0, 1, 1)
self.settingsDockWidget.setWidget(self.settingsDockWidgetContents)
QSpectrumAnalyzerMainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.settingsDockWidget)
self.levelsDockWidget = QtWidgets.QDockWidget(QSpectrumAnalyzerMainWindow)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.levelsDockWidget.sizePolicy().hasHeightForWidth())
self.levelsDockWidget.setSizePolicy(sizePolicy)
self.levelsDockWidget.setFeatures(QtWidgets.QDockWidget.DockWidgetFloatable|QtWidgets.QDockWidget.DockWidgetMovable)
self.levelsDockWidget.setObjectName("levelsDockWidget")
self.levelsDockWidgetContents = QtWidgets.QWidget()
self.levelsDockWidgetContents.setObjectName("levelsDockWidgetContents")
self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.levelsDockWidgetContents)
self.verticalLayout_6.setContentsMargins(0, 0, 0, 0)
self.verticalLayout_6.setObjectName("verticalLayout_6")
self.histogramPlotLayout = GraphicsLayoutWidget(self.levelsDockWidgetContents)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.histogramPlotLayout.sizePolicy().hasHeightForWidth())
self.histogramPlotLayout.setSizePolicy(sizePolicy)
self.histogramPlotLayout.setObjectName(_fromUtf8("histogramPlotLayout"))
self.verticalLayout_2.addWidget(self.histogramPlotLayout)
self.verticalLayout_3.addWidget(self.groupBox_4)
self.dockWidget.setWidget(self.dockWidgetContents)
QSpectrumAnalyzerMainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.dockWidget)
self.action_Settings = QtGui.QAction(QSpectrumAnalyzerMainWindow)
self.action_Settings.setObjectName(_fromUtf8("action_Settings"))
self.action_Quit = QtGui.QAction(QSpectrumAnalyzerMainWindow)
self.action_Quit.setObjectName(_fromUtf8("action_Quit"))
self.action_About = QtGui.QAction(QSpectrumAnalyzerMainWindow)
self.action_About.setObjectName(_fromUtf8("action_About"))
self.histogramPlotLayout.setObjectName("histogramPlotLayout")
self.verticalLayout_6.addWidget(self.histogramPlotLayout)
self.levelsDockWidget.setWidget(self.levelsDockWidgetContents)
QSpectrumAnalyzerMainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.levelsDockWidget)
self.action_Settings = QtWidgets.QAction(QSpectrumAnalyzerMainWindow)
self.action_Settings.setObjectName("action_Settings")
self.action_Quit = QtWidgets.QAction(QSpectrumAnalyzerMainWindow)
self.action_Quit.setObjectName("action_Quit")
self.action_About = QtWidgets.QAction(QSpectrumAnalyzerMainWindow)
self.action_About.setObjectName("action_About")
self.menu_File.addAction(self.action_Settings)
self.menu_File.addSeparator()
self.menu_File.addAction(self.action_Quit)
@ -211,35 +296,60 @@ class Ui_QSpectrumAnalyzerMainWindow(object):
QSpectrumAnalyzerMainWindow.setTabOrder(self.intervalSpinBox, self.gainSpinBox)
QSpectrumAnalyzerMainWindow.setTabOrder(self.gainSpinBox, self.ppmSpinBox)
QSpectrumAnalyzerMainWindow.setTabOrder(self.ppmSpinBox, self.cropSpinBox)
QSpectrumAnalyzerMainWindow.setTabOrder(self.cropSpinBox, self.histogramPlotLayout)
QSpectrumAnalyzerMainWindow.setTabOrder(self.cropSpinBox, self.mainCurveCheckBox)
QSpectrumAnalyzerMainWindow.setTabOrder(self.mainCurveCheckBox, self.colorsButton)
QSpectrumAnalyzerMainWindow.setTabOrder(self.colorsButton, self.peakHoldMaxCheckBox)
QSpectrumAnalyzerMainWindow.setTabOrder(self.peakHoldMaxCheckBox, self.peakHoldMinCheckBox)
QSpectrumAnalyzerMainWindow.setTabOrder(self.peakHoldMinCheckBox, self.averageCheckBox)
QSpectrumAnalyzerMainWindow.setTabOrder(self.averageCheckBox, self.smoothCheckBox)
QSpectrumAnalyzerMainWindow.setTabOrder(self.smoothCheckBox, self.smoothButton)
QSpectrumAnalyzerMainWindow.setTabOrder(self.smoothButton, self.persistenceCheckBox)
QSpectrumAnalyzerMainWindow.setTabOrder(self.persistenceCheckBox, self.persistenceButton)
QSpectrumAnalyzerMainWindow.setTabOrder(self.persistenceButton, self.baselineCheckBox)
QSpectrumAnalyzerMainWindow.setTabOrder(self.baselineCheckBox, self.baselineButton)
QSpectrumAnalyzerMainWindow.setTabOrder(self.baselineButton, self.subtractBaselineCheckBox)
QSpectrumAnalyzerMainWindow.setTabOrder(self.subtractBaselineCheckBox, self.histogramPlotLayout)
QSpectrumAnalyzerMainWindow.setTabOrder(self.histogramPlotLayout, self.mainPlotLayout)
QSpectrumAnalyzerMainWindow.setTabOrder(self.mainPlotLayout, self.waterfallPlotLayout)
def retranslateUi(self, QSpectrumAnalyzerMainWindow):
QSpectrumAnalyzerMainWindow.setWindowTitle(_translate("QSpectrumAnalyzerMainWindow", "QSpectrumAnalyzer", None))
self.menu_File.setTitle(_translate("QSpectrumAnalyzerMainWindow", "&File", None))
self.menu_Help.setTitle(_translate("QSpectrumAnalyzerMainWindow", "&Help", None))
self.groupBox_2.setTitle(_translate("QSpectrumAnalyzerMainWindow", "Controls", None))
self.startButton.setText(_translate("QSpectrumAnalyzerMainWindow", "&Start", None))
self.stopButton.setText(_translate("QSpectrumAnalyzerMainWindow", "S&top", None))
self.singleShotButton.setText(_translate("QSpectrumAnalyzerMainWindow", "Si&ngle shot", None))
self.groupBox.setTitle(_translate("QSpectrumAnalyzerMainWindow", "Frequency", None))
self.label_2.setText(_translate("QSpectrumAnalyzerMainWindow", "Start:", None))
self.startFreqSpinBox.setSuffix(_translate("QSpectrumAnalyzerMainWindow", " MHz", None))
self.label_3.setText(_translate("QSpectrumAnalyzerMainWindow", "Stop:", None))
self.stopFreqSpinBox.setSuffix(_translate("QSpectrumAnalyzerMainWindow", " MHz", None))
self.label.setText(_translate("QSpectrumAnalyzerMainWindow", "Bin size:", None))
self.binSizeSpinBox.setSuffix(_translate("QSpectrumAnalyzerMainWindow", " kHz", None))
self.groupBox_3.setTitle(_translate("QSpectrumAnalyzerMainWindow", "Settings", None))
self.label_4.setText(_translate("QSpectrumAnalyzerMainWindow", "Interval [s]:", None))
self.label_6.setText(_translate("QSpectrumAnalyzerMainWindow", "Gain [dB]:", None))
self.gainSpinBox.setSpecialValueText(_translate("QSpectrumAnalyzerMainWindow", "auto", None))
self.label_5.setText(_translate("QSpectrumAnalyzerMainWindow", "Corr. [ppm]:", None))
self.label_7.setText(_translate("QSpectrumAnalyzerMainWindow", "Crop [%]:", None))
self.groupBox_4.setTitle(_translate("QSpectrumAnalyzerMainWindow", "Levels", None))
self.action_Settings.setText(_translate("QSpectrumAnalyzerMainWindow", "&Settings...", None))
self.action_Quit.setText(_translate("QSpectrumAnalyzerMainWindow", "&Quit", None))
self.action_Quit.setShortcut(_translate("QSpectrumAnalyzerMainWindow", "Ctrl+Q", None))
self.action_About.setText(_translate("QSpectrumAnalyzerMainWindow", "&About", None))
_translate = QtCore.QCoreApplication.translate
QSpectrumAnalyzerMainWindow.setWindowTitle(_translate("QSpectrumAnalyzerMainWindow", "QSpectrumAnalyzer"))
self.menu_File.setTitle(_translate("QSpectrumAnalyzerMainWindow", "&File"))
self.menu_Help.setTitle(_translate("QSpectrumAnalyzerMainWindow", "&Help"))
self.controlsDockWidget.setWindowTitle(_translate("QSpectrumAnalyzerMainWindow", "Controls"))
self.startButton.setText(_translate("QSpectrumAnalyzerMainWindow", "&Start"))
self.stopButton.setText(_translate("QSpectrumAnalyzerMainWindow", "S&top"))
self.singleShotButton.setText(_translate("QSpectrumAnalyzerMainWindow", "Si&ngle shot"))
self.frequencyDockWidget.setWindowTitle(_translate("QSpectrumAnalyzerMainWindow", "Frequency"))
self.label_2.setText(_translate("QSpectrumAnalyzerMainWindow", "Start:"))
self.startFreqSpinBox.setSuffix(_translate("QSpectrumAnalyzerMainWindow", " MHz"))
self.label_3.setText(_translate("QSpectrumAnalyzerMainWindow", "Stop:"))
self.stopFreqSpinBox.setSuffix(_translate("QSpectrumAnalyzerMainWindow", " MHz"))
self.label.setText(_translate("QSpectrumAnalyzerMainWindow", "&Bin size:"))
self.binSizeSpinBox.setSuffix(_translate("QSpectrumAnalyzerMainWindow", " kHz"))
self.settingsDockWidget.setWindowTitle(_translate("QSpectrumAnalyzerMainWindow", "Settings"))
self.label_4.setText(_translate("QSpectrumAnalyzerMainWindow", "&Interval [s]:"))
self.label_6.setText(_translate("QSpectrumAnalyzerMainWindow", "&Gain [dB]:"))
self.label_5.setText(_translate("QSpectrumAnalyzerMainWindow", "Corr. [ppm]:"))
self.label_7.setText(_translate("QSpectrumAnalyzerMainWindow", "Crop [%]:"))
self.mainCurveCheckBox.setText(_translate("QSpectrumAnalyzerMainWindow", "Main curve"))
self.colorsButton.setText(_translate("QSpectrumAnalyzerMainWindow", "Colors..."))
self.peakHoldMaxCheckBox.setText(_translate("QSpectrumAnalyzerMainWindow", "Max. hold"))
self.peakHoldMinCheckBox.setText(_translate("QSpectrumAnalyzerMainWindow", "Min. hold"))
self.averageCheckBox.setText(_translate("QSpectrumAnalyzerMainWindow", "Average"))
self.smoothCheckBox.setText(_translate("QSpectrumAnalyzerMainWindow", "Smoothing"))
self.smoothButton.setText(_translate("QSpectrumAnalyzerMainWindow", "..."))
self.persistenceCheckBox.setText(_translate("QSpectrumAnalyzerMainWindow", "Persistence"))
self.persistenceButton.setText(_translate("QSpectrumAnalyzerMainWindow", "..."))
self.gainSpinBox.setSpecialValueText(_translate("QSpectrumAnalyzerMainWindow", "auto"))
self.baselineCheckBox.setText(_translate("QSpectrumAnalyzerMainWindow", "Baseline"))
self.baselineButton.setText(_translate("QSpectrumAnalyzerMainWindow", "..."))
self.subtractBaselineCheckBox.setText(_translate("QSpectrumAnalyzerMainWindow", "Subtract baseline"))
self.levelsDockWidget.setWindowTitle(_translate("QSpectrumAnalyzerMainWindow", "Levels"))
self.action_Settings.setText(_translate("QSpectrumAnalyzerMainWindow", "&Settings..."))
self.action_Quit.setText(_translate("QSpectrumAnalyzerMainWindow", "&Quit"))
self.action_Quit.setShortcut(_translate("QSpectrumAnalyzerMainWindow", "Ctrl+Q"))
self.action_About.setText(_translate("QSpectrumAnalyzerMainWindow", "&About"))
from pyqtgraph import GraphicsLayoutWidget

View File

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'qspectrumanalyzer/qspectrumanalyzer_baseline.ui'
#
# Created by: PyQt5 UI code generator 5.8
#
# WARNING! All changes made in this file will be lost!
from Qt import QtCore, QtGui, QtWidgets
class Ui_QSpectrumAnalyzerBaseline(object):
def setupUi(self, QSpectrumAnalyzerBaseline):
QSpectrumAnalyzerBaseline.setObjectName("QSpectrumAnalyzerBaseline")
QSpectrumAnalyzerBaseline.resize(500, 100)
self.verticalLayout = QtWidgets.QVBoxLayout(QSpectrumAnalyzerBaseline)
self.verticalLayout.setObjectName("verticalLayout")
self.formLayout = QtWidgets.QFormLayout()
self.formLayout.setObjectName("formLayout")
self.label = QtWidgets.QLabel(QSpectrumAnalyzerBaseline)
self.label.setObjectName("label")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.baselineFileEdit = QtWidgets.QLineEdit(QSpectrumAnalyzerBaseline)
self.baselineFileEdit.setObjectName("baselineFileEdit")
self.horizontalLayout.addWidget(self.baselineFileEdit)
self.baselineFileButton = QtWidgets.QToolButton(QSpectrumAnalyzerBaseline)
self.baselineFileButton.setMinimumSize(QtCore.QSize(50, 0))
self.baselineFileButton.setObjectName("baselineFileButton")
self.horizontalLayout.addWidget(self.baselineFileButton)
self.formLayout.setLayout(0, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout)
self.verticalLayout.addLayout(self.formLayout)
spacerItem = QtWidgets.QSpacerItem(20, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem)
self.buttonBox = QtWidgets.QDialogButtonBox(QSpectrumAnalyzerBaseline)
self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok)
self.buttonBox.setObjectName("buttonBox")
self.verticalLayout.addWidget(self.buttonBox)
self.label.setBuddy(self.baselineFileEdit)
self.retranslateUi(QSpectrumAnalyzerBaseline)
self.buttonBox.accepted.connect(QSpectrumAnalyzerBaseline.accept)
self.buttonBox.rejected.connect(QSpectrumAnalyzerBaseline.reject)
QtCore.QMetaObject.connectSlotsByName(QSpectrumAnalyzerBaseline)
QSpectrumAnalyzerBaseline.setTabOrder(self.baselineFileEdit, self.baselineFileButton)
def retranslateUi(self, QSpectrumAnalyzerBaseline):
_translate = QtCore.QCoreApplication.translate
QSpectrumAnalyzerBaseline.setWindowTitle(_translate("QSpectrumAnalyzerBaseline", "Baseline - QSpectrumAnalyzer"))
self.label.setText(_translate("QSpectrumAnalyzerBaseline", "Baseline &file:"))
self.baselineFileButton.setText(_translate("QSpectrumAnalyzerBaseline", "..."))

View File

@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'qspectrumanalyzer/qspectrumanalyzer_colors.ui'
#
# Created by: PyQt5 UI code generator 5.8
#
# WARNING! All changes made in this file will be lost!
from Qt import QtCore, QtGui, QtWidgets
class Ui_QSpectrumAnalyzerColors(object):
def setupUi(self, QSpectrumAnalyzerColors):
QSpectrumAnalyzerColors.setObjectName("QSpectrumAnalyzerColors")
QSpectrumAnalyzerColors.resize(253, 266)
self.verticalLayout = QtWidgets.QVBoxLayout(QSpectrumAnalyzerColors)
self.verticalLayout.setObjectName("verticalLayout")
self.formLayout = QtWidgets.QFormLayout()
self.formLayout.setObjectName("formLayout")
self.label_2 = QtWidgets.QLabel(QSpectrumAnalyzerColors)
self.label_2.setObjectName("label_2")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label_2)
self.mainColorButton = ColorButton(QSpectrumAnalyzerColors)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.mainColorButton.sizePolicy().hasHeightForWidth())
self.mainColorButton.setSizePolicy(sizePolicy)
self.mainColorButton.setObjectName("mainColorButton")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.mainColorButton)
self.label_4 = QtWidgets.QLabel(QSpectrumAnalyzerColors)
self.label_4.setObjectName("label_4")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.label_4)
self.peakHoldMaxColorButton = ColorButton(QSpectrumAnalyzerColors)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.peakHoldMaxColorButton.sizePolicy().hasHeightForWidth())
self.peakHoldMaxColorButton.setSizePolicy(sizePolicy)
self.peakHoldMaxColorButton.setObjectName("peakHoldMaxColorButton")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.peakHoldMaxColorButton)
self.label_6 = QtWidgets.QLabel(QSpectrumAnalyzerColors)
self.label_6.setObjectName("label_6")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.label_6)
self.peakHoldMinColorButton = ColorButton(QSpectrumAnalyzerColors)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.peakHoldMinColorButton.sizePolicy().hasHeightForWidth())
self.peakHoldMinColorButton.setSizePolicy(sizePolicy)
self.peakHoldMinColorButton.setObjectName("peakHoldMinColorButton")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.peakHoldMinColorButton)
self.label_5 = QtWidgets.QLabel(QSpectrumAnalyzerColors)
self.label_5.setObjectName("label_5")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.label_5)
self.averageColorButton = ColorButton(QSpectrumAnalyzerColors)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.averageColorButton.sizePolicy().hasHeightForWidth())
self.averageColorButton.setSizePolicy(sizePolicy)
self.averageColorButton.setObjectName("averageColorButton")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.averageColorButton)
self.label_3 = QtWidgets.QLabel(QSpectrumAnalyzerColors)
self.label_3.setObjectName("label_3")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.label_3)
self.persistenceColorButton = ColorButton(QSpectrumAnalyzerColors)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.persistenceColorButton.sizePolicy().hasHeightForWidth())
self.persistenceColorButton.setSizePolicy(sizePolicy)
self.persistenceColorButton.setObjectName("persistenceColorButton")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.persistenceColorButton)
self.label = QtWidgets.QLabel(QSpectrumAnalyzerColors)
self.label.setObjectName("label")
self.formLayout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.label)
self.baselineColorButton = ColorButton(QSpectrumAnalyzerColors)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.baselineColorButton.sizePolicy().hasHeightForWidth())
self.baselineColorButton.setSizePolicy(sizePolicy)
self.baselineColorButton.setObjectName("baselineColorButton")
self.formLayout.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.baselineColorButton)
self.verticalLayout.addLayout(self.formLayout)
spacerItem = QtWidgets.QSpacerItem(20, 2, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem)
self.buttonBox = QtWidgets.QDialogButtonBox(QSpectrumAnalyzerColors)
self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok)
self.buttonBox.setObjectName("buttonBox")
self.verticalLayout.addWidget(self.buttonBox)
self.label_2.setBuddy(self.mainColorButton)
self.label_4.setBuddy(self.peakHoldMaxColorButton)
self.label_6.setBuddy(self.peakHoldMinColorButton)
self.label_5.setBuddy(self.averageColorButton)
self.label_3.setBuddy(self.persistenceColorButton)
self.label.setBuddy(self.baselineColorButton)
self.retranslateUi(QSpectrumAnalyzerColors)
self.buttonBox.accepted.connect(QSpectrumAnalyzerColors.accept)
self.buttonBox.rejected.connect(QSpectrumAnalyzerColors.reject)
QtCore.QMetaObject.connectSlotsByName(QSpectrumAnalyzerColors)
QSpectrumAnalyzerColors.setTabOrder(self.mainColorButton, self.peakHoldMaxColorButton)
QSpectrumAnalyzerColors.setTabOrder(self.peakHoldMaxColorButton, self.peakHoldMinColorButton)
QSpectrumAnalyzerColors.setTabOrder(self.peakHoldMinColorButton, self.averageColorButton)
QSpectrumAnalyzerColors.setTabOrder(self.averageColorButton, self.persistenceColorButton)
QSpectrumAnalyzerColors.setTabOrder(self.persistenceColorButton, self.baselineColorButton)
def retranslateUi(self, QSpectrumAnalyzerColors):
_translate = QtCore.QCoreApplication.translate
QSpectrumAnalyzerColors.setWindowTitle(_translate("QSpectrumAnalyzerColors", "Colors - QSpectrumAnalyzer"))
self.label_2.setText(_translate("QSpectrumAnalyzerColors", "&Main curve color:"))
self.mainColorButton.setText(_translate("QSpectrumAnalyzerColors", "..."))
self.label_4.setText(_translate("QSpectrumAnalyzerColors", "Max. peak &hold color:"))
self.peakHoldMaxColorButton.setText(_translate("QSpectrumAnalyzerColors", "..."))
self.label_6.setText(_translate("QSpectrumAnalyzerColors", "M&in. peak hold color:"))
self.peakHoldMinColorButton.setText(_translate("QSpectrumAnalyzerColors", "..."))
self.label_5.setText(_translate("QSpectrumAnalyzerColors", "Average &color:"))
self.averageColorButton.setText(_translate("QSpectrumAnalyzerColors", "..."))
self.label_3.setText(_translate("QSpectrumAnalyzerColors", "Persistence co&lor:"))
self.persistenceColorButton.setText(_translate("QSpectrumAnalyzerColors", "..."))
self.label.setText(_translate("QSpectrumAnalyzerColors", "&Baseline color:"))
self.baselineColorButton.setText(_translate("QSpectrumAnalyzerColors", "..."))
from pyqtgraph import ColorButton

View File

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'qspectrumanalyzer/qspectrumanalyzer_persistence.ui'
#
# Created by: PyQt5 UI code generator 5.8
#
# WARNING! All changes made in this file will be lost!
from Qt import QtCore, QtGui, QtWidgets
class Ui_QSpectrumAnalyzerPersistence(object):
def setupUi(self, QSpectrumAnalyzerPersistence):
QSpectrumAnalyzerPersistence.setObjectName("QSpectrumAnalyzerPersistence")
QSpectrumAnalyzerPersistence.resize(250, 130)
self.verticalLayout = QtWidgets.QVBoxLayout(QSpectrumAnalyzerPersistence)
self.verticalLayout.setObjectName("verticalLayout")
self.formLayout = QtWidgets.QFormLayout()
self.formLayout.setObjectName("formLayout")
self.label_2 = QtWidgets.QLabel(QSpectrumAnalyzerPersistence)
self.label_2.setObjectName("label_2")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label_2)
self.decayFunctionComboBox = QtWidgets.QComboBox(QSpectrumAnalyzerPersistence)
self.decayFunctionComboBox.setObjectName("decayFunctionComboBox")
self.decayFunctionComboBox.addItem("")
self.decayFunctionComboBox.addItem("")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.decayFunctionComboBox)
self.label = QtWidgets.QLabel(QSpectrumAnalyzerPersistence)
self.label.setObjectName("label")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.label)
self.persistenceLengthSpinBox = QtWidgets.QSpinBox(QSpectrumAnalyzerPersistence)
self.persistenceLengthSpinBox.setProperty("value", 5)
self.persistenceLengthSpinBox.setObjectName("persistenceLengthSpinBox")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.persistenceLengthSpinBox)
self.verticalLayout.addLayout(self.formLayout)
spacerItem = QtWidgets.QSpacerItem(20, 5, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem)
self.buttonBox = QtWidgets.QDialogButtonBox(QSpectrumAnalyzerPersistence)
self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok)
self.buttonBox.setObjectName("buttonBox")
self.verticalLayout.addWidget(self.buttonBox)
self.label_2.setBuddy(self.decayFunctionComboBox)
self.label.setBuddy(self.persistenceLengthSpinBox)
self.retranslateUi(QSpectrumAnalyzerPersistence)
self.decayFunctionComboBox.setCurrentIndex(1)
self.buttonBox.accepted.connect(QSpectrumAnalyzerPersistence.accept)
self.buttonBox.rejected.connect(QSpectrumAnalyzerPersistence.reject)
QtCore.QMetaObject.connectSlotsByName(QSpectrumAnalyzerPersistence)
QSpectrumAnalyzerPersistence.setTabOrder(self.decayFunctionComboBox, self.persistenceLengthSpinBox)
QSpectrumAnalyzerPersistence.setTabOrder(self.persistenceLengthSpinBox, self.buttonBox)
def retranslateUi(self, QSpectrumAnalyzerPersistence):
_translate = QtCore.QCoreApplication.translate
QSpectrumAnalyzerPersistence.setWindowTitle(_translate("QSpectrumAnalyzerPersistence", "Persistence - QSpectrumAnalyzer"))
self.label_2.setText(_translate("QSpectrumAnalyzerPersistence", "Decay function:"))
self.decayFunctionComboBox.setItemText(0, _translate("QSpectrumAnalyzerPersistence", "linear"))
self.decayFunctionComboBox.setItemText(1, _translate("QSpectrumAnalyzerPersistence", "exponential"))
self.label.setText(_translate("QSpectrumAnalyzerPersistence", "Persistence length:"))

View File

@ -2,77 +2,170 @@
# Form implementation generated from reading ui file 'qspectrumanalyzer/qspectrumanalyzer_settings.ui'
#
# Created: Mon Mar 2 23:12:42 2015
# by: PyQt4 UI code generator 4.11.3
# Created by: PyQt5 UI code generator 5.8
#
# WARNING! All changes made in this file will be lost!
from PyQt4 import QtCore, QtGui
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
def _fromUtf8(s):
return s
try:
_encoding = QtGui.QApplication.UnicodeUTF8
def _translate(context, text, disambig):
return QtGui.QApplication.translate(context, text, disambig, _encoding)
except AttributeError:
def _translate(context, text, disambig):
return QtGui.QApplication.translate(context, text, disambig)
from Qt import QtCore, QtGui, QtWidgets
class Ui_QSpectrumAnalyzerSettings(object):
def setupUi(self, QSpectrumAnalyzerSettings):
QSpectrumAnalyzerSettings.setObjectName(_fromUtf8("QSpectrumAnalyzerSettings"))
QSpectrumAnalyzerSettings.resize(500, 150)
self.verticalLayout = QtGui.QVBoxLayout(QSpectrumAnalyzerSettings)
self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
self.formLayout = QtGui.QFormLayout()
self.formLayout.setObjectName(_fromUtf8("formLayout"))
self.label = QtGui.QLabel(QSpectrumAnalyzerSettings)
self.label.setObjectName(_fromUtf8("label"))
self.formLayout.setWidget(0, QtGui.QFormLayout.LabelRole, self.label)
self.horizontalLayout = QtGui.QHBoxLayout()
self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout"))
self.rtlPowerExecutableEdit = QtGui.QLineEdit(QSpectrumAnalyzerSettings)
self.rtlPowerExecutableEdit.setObjectName(_fromUtf8("rtlPowerExecutableEdit"))
self.horizontalLayout.addWidget(self.rtlPowerExecutableEdit)
self.rtlPowerExecutableButton = QtGui.QToolButton(QSpectrumAnalyzerSettings)
self.rtlPowerExecutableButton.setObjectName(_fromUtf8("rtlPowerExecutableButton"))
self.horizontalLayout.addWidget(self.rtlPowerExecutableButton)
self.formLayout.setLayout(0, QtGui.QFormLayout.FieldRole, self.horizontalLayout)
self.label_2 = QtGui.QLabel(QSpectrumAnalyzerSettings)
self.label_2.setObjectName(_fromUtf8("label_2"))
self.formLayout.setWidget(1, QtGui.QFormLayout.LabelRole, self.label_2)
self.waterfallHistorySizeSpinBox = QtGui.QSpinBox(QSpectrumAnalyzerSettings)
QSpectrumAnalyzerSettings.setObjectName("QSpectrumAnalyzerSettings")
QSpectrumAnalyzerSettings.resize(600, 388)
self.verticalLayout = QtWidgets.QVBoxLayout(QSpectrumAnalyzerSettings)
self.verticalLayout.setObjectName("verticalLayout")
self.formLayout = QtWidgets.QFormLayout()
self.formLayout.setObjectName("formLayout")
self.label_3 = QtWidgets.QLabel(QSpectrumAnalyzerSettings)
self.label_3.setObjectName("label_3")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label_3)
self.backendComboBox = QtWidgets.QComboBox(QSpectrumAnalyzerSettings)
self.backendComboBox.setObjectName("backendComboBox")
self.backendComboBox.addItem("")
self.backendComboBox.addItem("")
self.backendComboBox.addItem("")
self.backendComboBox.addItem("")
self.backendComboBox.addItem("")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.backendComboBox)
self.label = QtWidgets.QLabel(QSpectrumAnalyzerSettings)
self.label.setObjectName("label")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.label)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.executableEdit = QtWidgets.QLineEdit(QSpectrumAnalyzerSettings)
self.executableEdit.setObjectName("executableEdit")
self.horizontalLayout.addWidget(self.executableEdit)
self.executableButton = QtWidgets.QToolButton(QSpectrumAnalyzerSettings)
self.executableButton.setMinimumSize(QtCore.QSize(50, 0))
self.executableButton.setObjectName("executableButton")
self.horizontalLayout.addWidget(self.executableButton)
self.formLayout.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout)
self.label_5 = QtWidgets.QLabel(QSpectrumAnalyzerSettings)
self.label_5.setObjectName("label_5")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.label_5)
self.label_4 = QtWidgets.QLabel(QSpectrumAnalyzerSettings)
self.label_4.setObjectName("label_4")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.label_4)
self.label_2 = QtWidgets.QLabel(QSpectrumAnalyzerSettings)
self.label_2.setObjectName("label_2")
self.formLayout.setWidget(7, QtWidgets.QFormLayout.LabelRole, self.label_2)
self.waterfallHistorySizeSpinBox = QtWidgets.QSpinBox(QSpectrumAnalyzerSettings)
self.waterfallHistorySizeSpinBox.setMinimum(1)
self.waterfallHistorySizeSpinBox.setMaximum(10000000)
self.waterfallHistorySizeSpinBox.setProperty("value", 100)
self.waterfallHistorySizeSpinBox.setObjectName(_fromUtf8("waterfallHistorySizeSpinBox"))
self.formLayout.setWidget(1, QtGui.QFormLayout.FieldRole, self.waterfallHistorySizeSpinBox)
self.waterfallHistorySizeSpinBox.setObjectName("waterfallHistorySizeSpinBox")
self.formLayout.setWidget(7, QtWidgets.QFormLayout.FieldRole, self.waterfallHistorySizeSpinBox)
self.label_7 = QtWidgets.QLabel(QSpectrumAnalyzerSettings)
self.label_7.setObjectName("label_7")
self.formLayout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.label_7)
self.label_8 = QtWidgets.QLabel(QSpectrumAnalyzerSettings)
self.label_8.setObjectName("label_8")
self.formLayout.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.label_8)
self.label_6 = QtWidgets.QLabel(QSpectrumAnalyzerSettings)
self.label_6.setObjectName("label_6")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.label_6)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.paramsEdit = QtWidgets.QLineEdit(QSpectrumAnalyzerSettings)
self.paramsEdit.setObjectName("paramsEdit")
self.horizontalLayout_2.addWidget(self.paramsEdit)
self.paramsHelpButton = QtWidgets.QToolButton(QSpectrumAnalyzerSettings)
self.paramsHelpButton.setMinimumSize(QtCore.QSize(50, 0))
self.paramsHelpButton.setObjectName("paramsHelpButton")
self.horizontalLayout_2.addWidget(self.paramsHelpButton)
self.formLayout.setLayout(2, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout_2)
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
self.deviceEdit = QtWidgets.QLineEdit(QSpectrumAnalyzerSettings)
self.deviceEdit.setObjectName("deviceEdit")
self.horizontalLayout_3.addWidget(self.deviceEdit)
self.deviceHelpButton = QtWidgets.QToolButton(QSpectrumAnalyzerSettings)
self.deviceHelpButton.setMinimumSize(QtCore.QSize(50, 0))
self.deviceHelpButton.setObjectName("deviceHelpButton")
self.horizontalLayout_3.addWidget(self.deviceHelpButton)
self.formLayout.setLayout(3, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout_3)
self.sampleRateSpinBox = QtWidgets.QDoubleSpinBox(QSpectrumAnalyzerSettings)
self.sampleRateSpinBox.setProperty("showGroupSeparator", True)
self.sampleRateSpinBox.setDecimals(3)
self.sampleRateSpinBox.setMinimum(0.0)
self.sampleRateSpinBox.setMaximum(999999.99)
self.sampleRateSpinBox.setSingleStep(0.01)
self.sampleRateSpinBox.setProperty("value", 61.44)
self.sampleRateSpinBox.setObjectName("sampleRateSpinBox")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.sampleRateSpinBox)
self.bandwidthSpinBox = QtWidgets.QDoubleSpinBox(QSpectrumAnalyzerSettings)
self.bandwidthSpinBox.setProperty("showGroupSeparator", True)
self.bandwidthSpinBox.setDecimals(3)
self.bandwidthSpinBox.setMinimum(0.0)
self.bandwidthSpinBox.setMaximum(999999.99)
self.bandwidthSpinBox.setSingleStep(0.01)
self.bandwidthSpinBox.setProperty("value", 0.0)
self.bandwidthSpinBox.setObjectName("bandwidthSpinBox")
self.formLayout.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.bandwidthSpinBox)
self.lnbSpinBox = QtWidgets.QDoubleSpinBox(QSpectrumAnalyzerSettings)
self.lnbSpinBox.setProperty("showGroupSeparator", True)
self.lnbSpinBox.setDecimals(3)
self.lnbSpinBox.setMinimum(-999999.999)
self.lnbSpinBox.setMaximum(999999.999)
self.lnbSpinBox.setSingleStep(0.01)
self.lnbSpinBox.setProperty("value", 0.0)
self.lnbSpinBox.setObjectName("lnbSpinBox")
self.formLayout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.lnbSpinBox)
self.verticalLayout.addLayout(self.formLayout)
spacerItem = QtGui.QSpacerItem(20, 21, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
spacerItem = QtWidgets.QSpacerItem(20, 21, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem)
self.buttonBox = QtGui.QDialogButtonBox(QSpectrumAnalyzerSettings)
self.buttonBox = QtWidgets.QDialogButtonBox(QSpectrumAnalyzerSettings)
self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.Ok)
self.buttonBox.setObjectName(_fromUtf8("buttonBox"))
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok)
self.buttonBox.setObjectName("buttonBox")
self.verticalLayout.addWidget(self.buttonBox)
self.label_3.setBuddy(self.backendComboBox)
self.label.setBuddy(self.executableEdit)
self.label_5.setBuddy(self.deviceEdit)
self.label_4.setBuddy(self.sampleRateSpinBox)
self.label_2.setBuddy(self.waterfallHistorySizeSpinBox)
self.label_7.setBuddy(self.bandwidthSpinBox)
self.label_8.setBuddy(self.lnbSpinBox)
self.label_6.setBuddy(self.paramsEdit)
self.retranslateUi(QSpectrumAnalyzerSettings)
QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("accepted()")), QSpectrumAnalyzerSettings.accept)
QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("rejected()")), QSpectrumAnalyzerSettings.reject)
self.buttonBox.accepted.connect(QSpectrumAnalyzerSettings.accept)
self.buttonBox.rejected.connect(QSpectrumAnalyzerSettings.reject)
QtCore.QMetaObject.connectSlotsByName(QSpectrumAnalyzerSettings)
QSpectrumAnalyzerSettings.setTabOrder(self.rtlPowerExecutableEdit, self.rtlPowerExecutableButton)
QSpectrumAnalyzerSettings.setTabOrder(self.rtlPowerExecutableButton, self.waterfallHistorySizeSpinBox)
QSpectrumAnalyzerSettings.setTabOrder(self.waterfallHistorySizeSpinBox, self.buttonBox)
QSpectrumAnalyzerSettings.setTabOrder(self.backendComboBox, self.executableEdit)
QSpectrumAnalyzerSettings.setTabOrder(self.executableEdit, self.executableButton)
QSpectrumAnalyzerSettings.setTabOrder(self.executableButton, self.paramsEdit)
QSpectrumAnalyzerSettings.setTabOrder(self.paramsEdit, self.paramsHelpButton)
QSpectrumAnalyzerSettings.setTabOrder(self.paramsHelpButton, self.deviceEdit)
QSpectrumAnalyzerSettings.setTabOrder(self.deviceEdit, self.deviceHelpButton)
QSpectrumAnalyzerSettings.setTabOrder(self.deviceHelpButton, self.sampleRateSpinBox)
QSpectrumAnalyzerSettings.setTabOrder(self.sampleRateSpinBox, self.bandwidthSpinBox)
QSpectrumAnalyzerSettings.setTabOrder(self.bandwidthSpinBox, self.lnbSpinBox)
QSpectrumAnalyzerSettings.setTabOrder(self.lnbSpinBox, self.waterfallHistorySizeSpinBox)
def retranslateUi(self, QSpectrumAnalyzerSettings):
QSpectrumAnalyzerSettings.setWindowTitle(_translate("QSpectrumAnalyzerSettings", "QSpectrumAnalyzer - Settings", None))
self.label.setText(_translate("QSpectrumAnalyzerSettings", "Rtl_power executable:", None))
self.rtlPowerExecutableEdit.setText(_translate("QSpectrumAnalyzerSettings", "rtl_power", None))
self.rtlPowerExecutableButton.setText(_translate("QSpectrumAnalyzerSettings", "...", None))
self.label_2.setText(_translate("QSpectrumAnalyzerSettings", "Waterfall history size:", None))
_translate = QtCore.QCoreApplication.translate
QSpectrumAnalyzerSettings.setWindowTitle(_translate("QSpectrumAnalyzerSettings", "Settings - QSpectrumAnalyzer"))
self.label_3.setText(_translate("QSpectrumAnalyzerSettings", "&Backend:"))
self.backendComboBox.setItemText(0, _translate("QSpectrumAnalyzerSettings", "soapy_power"))
self.backendComboBox.setItemText(1, _translate("QSpectrumAnalyzerSettings", "rx_power"))
self.backendComboBox.setItemText(2, _translate("QSpectrumAnalyzerSettings", "rtl_power_fftw"))
self.backendComboBox.setItemText(3, _translate("QSpectrumAnalyzerSettings", "rtl_power"))
self.backendComboBox.setItemText(4, _translate("QSpectrumAnalyzerSettings", "hackrf_sweep"))
self.label.setText(_translate("QSpectrumAnalyzerSettings", "E&xecutable:"))
self.executableEdit.setText(_translate("QSpectrumAnalyzerSettings", "soapy_power"))
self.executableButton.setText(_translate("QSpectrumAnalyzerSettings", "..."))
self.label_5.setText(_translate("QSpectrumAnalyzerSettings", "&Device:"))
self.label_4.setText(_translate("QSpectrumAnalyzerSettings", "Sa&mple rate:"))
self.label_2.setText(_translate("QSpectrumAnalyzerSettings", "&Waterfall history size:"))
self.label_7.setText(_translate("QSpectrumAnalyzerSettings", "Bandwidt&h:"))
self.label_8.setToolTip(_translate("QSpectrumAnalyzerSettings", "Negative frequency for upconverters, positive frequency for downconverters."))
self.label_8.setText(_translate("QSpectrumAnalyzerSettings", "&LNB LO:"))
self.label_6.setText(_translate("QSpectrumAnalyzerSettings", "Add&itional parameters:"))
self.paramsHelpButton.setText(_translate("QSpectrumAnalyzerSettings", " ? "))
self.deviceHelpButton.setText(_translate("QSpectrumAnalyzerSettings", " ? "))
self.sampleRateSpinBox.setSuffix(_translate("QSpectrumAnalyzerSettings", " MHz"))
self.bandwidthSpinBox.setSuffix(_translate("QSpectrumAnalyzerSettings", " MHz"))
self.lnbSpinBox.setToolTip(_translate("QSpectrumAnalyzerSettings", "Negative frequency for upconverters, positive frequency for downconverters."))
self.lnbSpinBox.setSuffix(_translate("QSpectrumAnalyzerSettings", " MHz"))

View File

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'qspectrumanalyzer/qspectrumanalyzer_settings_help.ui'
#
# Created by: PyQt5 UI code generator 5.8
#
# WARNING! All changes made in this file will be lost!
from Qt import QtCore, QtGui, QtWidgets
class Ui_QSpectrumAnalyzerSettingsHelp(object):
def setupUi(self, QSpectrumAnalyzerSettingsHelp):
QSpectrumAnalyzerSettingsHelp.setObjectName("QSpectrumAnalyzerSettingsHelp")
QSpectrumAnalyzerSettingsHelp.resize(1200, 700)
self.verticalLayout = QtWidgets.QVBoxLayout(QSpectrumAnalyzerSettingsHelp)
self.verticalLayout.setObjectName("verticalLayout")
self.helpTextEdit = QtWidgets.QPlainTextEdit(QSpectrumAnalyzerSettingsHelp)
self.helpTextEdit.setUndoRedoEnabled(False)
self.helpTextEdit.setTextInteractionFlags(QtCore.Qt.TextSelectableByKeyboard|QtCore.Qt.TextSelectableByMouse)
self.helpTextEdit.setObjectName("helpTextEdit")
self.verticalLayout.addWidget(self.helpTextEdit)
self.buttonBox = QtWidgets.QDialogButtonBox(QSpectrumAnalyzerSettingsHelp)
self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Close)
self.buttonBox.setObjectName("buttonBox")
self.verticalLayout.addWidget(self.buttonBox)
self.retranslateUi(QSpectrumAnalyzerSettingsHelp)
self.buttonBox.accepted.connect(QSpectrumAnalyzerSettingsHelp.accept)
self.buttonBox.rejected.connect(QSpectrumAnalyzerSettingsHelp.reject)
QtCore.QMetaObject.connectSlotsByName(QSpectrumAnalyzerSettingsHelp)
QSpectrumAnalyzerSettingsHelp.setTabOrder(self.helpTextEdit, self.buttonBox)
def retranslateUi(self, QSpectrumAnalyzerSettingsHelp):
_translate = QtCore.QCoreApplication.translate
QSpectrumAnalyzerSettingsHelp.setWindowTitle(_translate("QSpectrumAnalyzerSettingsHelp", "Help - QSpectrumAnalyzer"))

View File

@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'qspectrumanalyzer/qspectrumanalyzer_smoothing.ui'
#
# Created by: PyQt5 UI code generator 5.8
#
# WARNING! All changes made in this file will be lost!
from Qt import QtCore, QtGui, QtWidgets
class Ui_QSpectrumAnalyzerSmoothing(object):
def setupUi(self, QSpectrumAnalyzerSmoothing):
QSpectrumAnalyzerSmoothing.setObjectName("QSpectrumAnalyzerSmoothing")
QSpectrumAnalyzerSmoothing.resize(250, 130)
self.verticalLayout = QtWidgets.QVBoxLayout(QSpectrumAnalyzerSmoothing)
self.verticalLayout.setObjectName("verticalLayout")
self.formLayout = QtWidgets.QFormLayout()
self.formLayout.setObjectName("formLayout")
self.label = QtWidgets.QLabel(QSpectrumAnalyzerSmoothing)
self.label.setObjectName("label")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label)
self.windowFunctionComboBox = QtWidgets.QComboBox(QSpectrumAnalyzerSmoothing)
self.windowFunctionComboBox.setObjectName("windowFunctionComboBox")
self.windowFunctionComboBox.addItem("")
self.windowFunctionComboBox.addItem("")
self.windowFunctionComboBox.addItem("")
self.windowFunctionComboBox.addItem("")
self.windowFunctionComboBox.addItem("")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.windowFunctionComboBox)
self.label_2 = QtWidgets.QLabel(QSpectrumAnalyzerSmoothing)
self.label_2.setObjectName("label_2")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.label_2)
self.windowLengthSpinBox = QtWidgets.QSpinBox(QSpectrumAnalyzerSmoothing)
self.windowLengthSpinBox.setMinimum(3)
self.windowLengthSpinBox.setMaximum(1001)
self.windowLengthSpinBox.setProperty("value", 11)
self.windowLengthSpinBox.setObjectName("windowLengthSpinBox")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.windowLengthSpinBox)
self.verticalLayout.addLayout(self.formLayout)
spacerItem = QtWidgets.QSpacerItem(20, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem)
self.buttonBox = QtWidgets.QDialogButtonBox(QSpectrumAnalyzerSmoothing)
self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok)
self.buttonBox.setObjectName("buttonBox")
self.verticalLayout.addWidget(self.buttonBox)
self.label.setBuddy(self.windowFunctionComboBox)
self.label_2.setBuddy(self.windowLengthSpinBox)
self.retranslateUi(QSpectrumAnalyzerSmoothing)
self.windowFunctionComboBox.setCurrentIndex(1)
self.buttonBox.accepted.connect(QSpectrumAnalyzerSmoothing.accept)
self.buttonBox.rejected.connect(QSpectrumAnalyzerSmoothing.reject)
QtCore.QMetaObject.connectSlotsByName(QSpectrumAnalyzerSmoothing)
QSpectrumAnalyzerSmoothing.setTabOrder(self.windowFunctionComboBox, self.windowLengthSpinBox)
QSpectrumAnalyzerSmoothing.setTabOrder(self.windowLengthSpinBox, self.buttonBox)
def retranslateUi(self, QSpectrumAnalyzerSmoothing):
_translate = QtCore.QCoreApplication.translate
QSpectrumAnalyzerSmoothing.setWindowTitle(_translate("QSpectrumAnalyzerSmoothing", "Smoothing - QSpectrumAnalyzer"))
self.label.setText(_translate("QSpectrumAnalyzerSmoothing", "&Window function:"))
self.windowFunctionComboBox.setItemText(0, _translate("QSpectrumAnalyzerSmoothing", "rectangular"))
self.windowFunctionComboBox.setItemText(1, _translate("QSpectrumAnalyzerSmoothing", "hanning"))
self.windowFunctionComboBox.setItemText(2, _translate("QSpectrumAnalyzerSmoothing", "hamming"))
self.windowFunctionComboBox.setItemText(3, _translate("QSpectrumAnalyzerSmoothing", "bartlett"))
self.windowFunctionComboBox.setItemText(4, _translate("QSpectrumAnalyzerSmoothing", "blackman"))
self.label_2.setText(_translate("QSpectrumAnalyzerSmoothing", "Window len&gth:"))

View File

@ -0,0 +1,53 @@
import numpy as np
from Qt import QtGui
def smooth(x, window_len=11, window='hanning'):
"""Smooth 1D signal using specified window with given size"""
x = np.array(x)
if window_len < 3:
return x
if x.size < window_len:
raise ValueError("Input data length must be greater than window size")
if window not in ['rectangular', 'hanning', 'hamming', 'bartlett', 'blackman']:
raise ValueError("Window must be 'rectangular', 'hanning', 'hamming', 'bartlett' or 'blackman'")
if window == 'rectangular':
# Moving average
w = np.ones(window_len, 'd')
else:
w = getattr(np, window)(window_len)
s = np.r_[2 * x[0] - x[window_len:1:-1], x, 2 * x[-1] - x[-1:-window_len:-1]]
y = np.convolve(w / w.sum(), s, mode='same')
return y[window_len - 1:-window_len + 1]
def str_to_color(color_string):
"""Create QColor from comma sepparated RGBA string"""
return QtGui.QColor(*[int(c.strip()) for c in color_string.split(',')])
def color_to_str(color):
"""Create comma separated RGBA string from QColor"""
return ", ".join([str(color.red()), str(color.green()), str(color.blue()), str(color.alpha())])
def human_time(seconds):
"""Format time in seconds to human readable form (e.g. 1 h 2 min 3 s)"""
seconds = int(seconds)
m, s = divmod(seconds, 60)
h, m = divmod(m, 60)
if h > 0:
timestr = '{:.0f} h {:.0f} min {:.0f} s'.format(h, m, s)
elif m > 0:
timestr = '{:.0f} min {:.0f} s'.format(m, s)
else:
timestr = '{:.0f} s'.format(s)
return timestr

View File

@ -1 +1 @@
__version__ = "1.1"
__version__ = "2.2.0"

View File

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)
"""Windows-specific utilities"""
from ctypes import windll
# --- Window control ---
SW_SHOW = 5 # activate and display
SW_SHOWNA = 8 # show without activation
SW_HIDE = 0
GetConsoleWindow = windll.kernel32.GetConsoleWindow
ShowWindow = windll.user32.ShowWindow
IsWindowVisible = windll.user32.IsWindowVisible
# Handle to console window associated with current Python
# interpreter procss, 0 if there is no window
console_window_handle = GetConsoleWindow()
def set_attached_console_visible(state):
"""Show/hide system console window attached to current process.
Return it's previous state.
Availability: Windows"""
flag = {True: SW_SHOW, False: SW_HIDE}
return bool(ShowWindow(console_window_handle, flag[state]))
def is_attached_console_visible():
"""Return True if attached console window is visible"""
return IsWindowVisible(console_window_handle)
def set_windows_appusermodelid():
"""Make sure correct icon is used on Windows 7 taskbar"""
try:
return windll.shell32.SetCurrentProcessExplicitAppUserModelID("spyder.Spyder")
except AttributeError:
return "SetCurrentProcessExplicitAppUserModelID not found"
# [ ] the console state asks for a storage container
# [ ] reopen console on exit - better die open than become a zombie

View File

@ -1,4 +0,0 @@
#!/usr/bin/env python
from qspectrumanalyzer import __main__
__main__.main()

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python
import os, shutil
from glob import glob
package = "qspectrumanalyzer"
languages = ["cs"]
print("Rebuilding PyQt resource files...")
for f in glob("{}/*.qrc".format(package)):
os.system("pyrcc4 -o {}/qrc_{}.py {}".format(package, os.path.basename(f[:-4]), f))
print("Rebuilding PyQt UI files...")
for f in glob("{}/*.ui".format(package)):
os.system("pyuic4 -o {}/ui_{}.py {}".format(package, os.path.basename(f[:-3]), f))
print("Updating translations...")
lang_files = " ".join("{}/languages/{}_{}.ts".format(package, package, lang) for lang in languages)
os.system("pylupdate4 {}/*.py -ts {}".format(package, lang_files))
os.system("lrelease {}/languages/*.ts".format(package))
print("Regenerating .pyc files...")
shutil.rmtree("{}/__pycache__".format(package), ignore_errors=True)
for f in glob("{}/*.pyc".format(package)):
os.remove(f)
__import__("{}.__main__".format(package))

129
setup.py
View File

@ -1,32 +1,105 @@
#!/usr/bin/env python
from distutils.core import setup
import setuptools
from setuptools import setup
from qspectrumanalyzer.version import __version__
setup(name="QSpectrumAnalyzer",
version=__version__,
description="Spectrum analyzer for RTL-SDR (GUI for rtl_power based on PyQtGraph)",
author="Michal Krenek (Mikos)",
author_email="m.krenek@gmail.com",
url="https://github.com/xmikos/qspectrumanalyzer",
license="GNU GPLv3",
packages=["qspectrumanalyzer"],
data_files=[("share/applications", ["qspectrumanalyzer.desktop"]),
("share/pixmaps", ["qspectrumanalyzer.png"])],
scripts=["scripts/qspectrumanalyzer"],
requires=["PyQt4", "pyqtgraph"],
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: MacOS X',
'Environment :: Win32 (MS Windows)',
'Environment :: X11 Applications :: Qt',
'Intended Audience :: End Users/Desktop',
'Intended Audience :: Science/Research',
'Intended Audience :: Telecommunications Industry',
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'Topic :: Communications :: Ham Radio',
'Topic :: Scientific/Engineering :: Visualization'
])
setup_cmdclass = {}
setup_entry_points = {
"gui_scripts": [
"qspectrumanalyzer=qspectrumanalyzer.__main__:main",
],
}
# Allow compilation of Qt .qrc, .ui and .ts files (build_qt command)
try:
from setup_qt import build_qt
setup_cmdclass['build_qt'] = build_qt
except ImportError:
pass
# Allow building frozen executables with PyInstaller / subzero (build_exe command)
try:
from subzero import setup, Executable
setup_entry_points = {
"console_scripts": [
Executable('qspectrumanalyzer=qspectrumanalyzer.__main__:main',
console=True, icon_file='qspectrumanalyzer.ico'),
Executable('soapy_power=soapypower.__main__:main',
console=True),
],
}
except ImportError:
pass
setup(
name="QSpectrumAnalyzer",
version=__version__,
description=("Spectrum analyzer for multiple SDR platforms "
"(PyQtGraph based GUI for soapy_power, hackrf_sweep, rtl_power, rx_power and other backends)"),
long_description=open('README.rst').read(),
author="Michal Krenek (Mikos)",
author_email="m.krenek@gmail.com",
url="https://github.com/xmikos/qspectrumanalyzer",
license="GNU GPLv3",
packages=["qspectrumanalyzer", "qspectrumanalyzer.backends"],
package_data={
"qspectrumanalyzer": [
"qspectrumanalyzer.svg",
"*.ui",
"languages/*.qm",
"languages/*.ts",
],
},
data_files=[
("share/applications", ["qspectrumanalyzer.desktop"]),
("share/pixmaps", ["qspectrumanalyzer.png"]),
],
install_requires=[
"soapy_power>=1.6.0",
"pyqtgraph>=0.10.0",
"Qt.py",
],
options={
'build_qt': {
'packages': ['qspectrumanalyzer'],
'languages': ['cs'],
'replacement_bindings': 'Qt',
},
'build_exe': {
'datas': [
('qspectrumanalyzer/qspectrumanalyzer.svg', 'qspectrumanalyzer'),
('qspectrumanalyzer/*.ui', 'qspectrumanalyzer'),
('qspectrumanalyzer/languages/*.ts', 'qspectrumanalyzer/languages'),
('qspectrumanalyzer/languages/*.qm', 'qspectrumanalyzer/languages'),
('README.rst', '.'),
('LICENSE', '.'),
],
},
'bdist_msi': {
'upgrade_code': '30740ef4-84e7-4e67-8e4a-12b53492c387',
'shortcuts': [
'QSpectrumAnalyzer=qspectrumanalyzer',
],
},
},
classifiers=[
"Development Status :: 4 - Beta",
"Environment :: MacOS X",
"Environment :: Win32 (MS Windows)",
"Environment :: X11 Applications :: Qt",
"Intended Audience :: End Users/Desktop",
"Intended Audience :: Science/Research",
"Intended Audience :: Telecommunications Industry",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Topic :: Communications :: Ham Radio",
"Topic :: Scientific/Engineering :: Visualization",
],
entry_points=setup_entry_points,
cmdclass=setup_cmdclass,
)