Porównaj commity

...

36 Commity

Autor SHA1 Wiadomość Data
Mark Jessop edb3ccfbd1 Fix re-upload button 2024-05-17 19:46:09 +09:30
Mark Jessop 0ef8cd6fb1
Merge pull request #39 from projecthorus/actions_tests
Github actions for OSX builds
2024-04-27 09:47:22 +09:30
Mark Jessop 655279e26b Remote OSX Intel build 2024-04-26 14:44:56 +09:30
Mark Jessop 38df0377c8 Add test intel build using macos-13 2024-04-26 14:32:07 +09:30
Mark Jessop 7e8316e45e Try making a DMG 2024-04-26 14:22:54 +09:30
Mark Jessop 8d4918b460 another try 2024-04-26 14:05:13 +09:30
Mark Jessop 5cd4fff720 Try using homebrew to get dependencies 2024-04-26 14:00:02 +09:30
Mark Jessop 24bdf69360 Test OSX workflow build 2024-04-26 13:52:13 +09:30
Mark Jessop 35d42cf99e Merge branch 'master' of github.com:projecthorus/horus-gui 2024-04-25 17:40:19 +09:30
Mark Jessop f343e2a6a1 Increase SNR max window for RTTY modems 2024-04-25 17:39:56 +09:30
Mark Jessop f1d86851c1
Merge pull request #37 from teamtoadprojects/github-actions
Add initial Windows Github Actions binary build
2024-04-06 18:47:04 +10:30
Tom Wardill 608f0897a3
Fix generated filename
This is a zip file, so it should have a zip extension
2024-04-04 09:47:17 +01:00
Tom Wardill e92264923b
Fix capitalisation
pyAudio != pyaudio
2024-04-04 09:42:20 +01:00
Tom Wardill b364e842f0
Use python 3.11
Install pyaudio directly from pip now.
2024-04-04 09:40:01 +01:00
Tom Wardill e8e0e210ad Add artifact upload 2024-04-03 16:18:23 +01:00
Tom Wardill c9a208e700 PyInstaller steps 2024-04-03 16:18:20 +01:00
Tom Wardill 1c38f331bd Install python dependencies 2024-04-03 16:18:17 +01:00
Tom Wardill fd1886caff Build windows binary using Github Actions 2024-04-03 16:18:13 +01:00
Mark Jessop 55188fcca1 Automatically add microphone entry to Info.plist 2024-04-01 14:01:13 +10:30
Mark Jessop b0bb51cf2a Revert back to PyQt5 2024-03-31 14:10:13 +10:30
Mark Jessop f5d9e87db8 Remove ruamel.yaml dependency 2024-03-31 12:56:37 +10:30
Mark Jessop 356870b7b7 Fix audio error on startup 2024-03-31 11:20:32 +10:30
Mark Jessop 0bb9088917 Move to using Qt6 2024-03-31 10:35:46 +10:30
Mark Jessop e9a7b31dfb Add warning on starting with a uploader callsign of N0CALL 2024-02-03 16:31:33 +10:30
Mark Jessop ab470cb7c5 Fix issues with overrunning console log buffer 2024-01-29 10:32:16 +10:30
Mark Jessop 27c98174a0 Emit warning message if radio dial cannot be parsed 2023-09-22 14:15:25 +09:30
Mark Jessop c531572b2b v0.3.13 updates. Fix rotator pointing to 0,0. 2023-07-28 22:00:18 +09:30
Mark Jessop 6fb1e8bcba Readme tweaks 2023-07-15 14:43:34 +09:30
Mark Jessop dcb595036e Add --libfix argument, for local testing 2023-07-08 11:24:55 +09:30
Mark Jessop fbc4e73cc1 Update uploader callsign as soon as its modified 2023-07-08 10:57:02 +09:30
Mark Jessop afe2322509 Fix config invalidation for v0.3.11 2023-07-06 19:17:07 +09:30
Mark Jessop 387d91199f Add time-since-last-packet GUI element 2023-07-01 15:22:25 +09:30
Mark Jessop 24497428cc Update readme 2022-12-21 14:08:31 +10:30
Mark Jessop 5cc08d1e67 Try and force a refresh 2022-12-19 18:07:48 +10:30
Mark Jessop 3015ee555d Add JSON and CSV logging options, disable habitat upload 2022-12-19 17:55:37 +10:30
Mark Jessop a8223c5dd6 Pin pyqtgraph version for now 2022-11-06 11:19:39 +10:30
12 zmienionych plików z 797 dodań i 216 usunięć

Wyświetl plik

@ -0,0 +1,183 @@
name: Build
on:
push:
branches: ["master"]
pull_request:
branches: ["master"]
jobs:
build-windows:
runs-on: [windows-latest]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Checkout horusdemodlib
uses: actions/checkout@v4
with:
repository: "projecthorus/horusdemodlib"
ref: "master"
path: "horusdemodlib"
- name: Build horusdemodlib
run: |
cd horusdemodlib
mkdir build
cd build
cmake .. -G "MinGW Makefiles"
mingw32-make
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip' # caching pip dependencies
- name: Install pyAudio wheel
run: pip install pyaudio
- name: Install other dependencies
run: pip install -r requirements.txt
- name: Install pyinstaller
run: pip install pyinstaller
- name: Prep file locations
shell: bash
run: |
mkdir -p dist
cp horusdemodlib/build/src/libhorus.dll .
cp "C:\Program Files\Git\mingw64\bin\libgcc_s_seh-1.dll" .
cp "C:\Program Files\Git\mingw64\bin\libstdc++-6.dll" .
cp "C:\Program Files\Git\mingw64\bin\libwinpthread-1.dll" .
- name: Run pyinstaller
run: pyinstaller horus-gui_win.spec
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: horus-gui_WIN64.zip
path: dist/horus-gui.exe
retention-days: 2
build-osx:
runs-on: [macos-14]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Checkout horusdemodlib
uses: actions/checkout@v4
with:
repository: "projecthorus/horusdemodlib"
ref: "master"
path: "horusdemodlib"
- name: Build horusdemodlib
run: |
cd horusdemodlib
mkdir build
cd build
cmake ..
make
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip' # caching pip dependencies
- name: Install Homebrew dependencies
run: brew install portaudio
- name: Install pyAudio wheel
run: pip install pyaudio
- name: Install other dependencies
run: pip install -r requirements.txt
- name: Install pyinstaller
run: pip install pyinstaller
- name: Prep file locations
shell: bash
run: |
mkdir -p dist
cp horusdemodlib/build/src/libhorus.dylib .
- name: Run pyinstaller
run: pyinstaller horus-gui_osx_runner.spec
- name: Create the DMG file
run: hdiutil create -format UDZO -srcfolder dist/horus-gui.app dist/horus-gui_OSX-M1.dmg
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: horus-gui_OSX-M1.zip
path: dist/horus-gui_OSX-M1.dmg
retention-days: 2
# Currently having issues with portaudio and these builds...
# build-osx-intel:
# runs-on: [macos-13]
# steps:
# - name: Checkout code
# uses: actions/checkout@v4
# - name: Checkout horusdemodlib
# uses: actions/checkout@v4
# with:
# repository: "projecthorus/horusdemodlib"
# ref: "master"
# path: "horusdemodlib"
# - name: Build horusdemodlib
# run: |
# cd horusdemodlib
# mkdir build
# cd build
# cmake ..
# make
# - uses: actions/setup-python@v5
# with:
# python-version: '3.11'
# cache: 'pip' # caching pip dependencies
# - name: Install Homebrew dependencies
# run: brew install portaudio
# - name: Install pyAudio wheel
# run: pip install pyaudio
# - name: Install other dependencies
# run: pip install -r requirements.txt
# - name: Install pyinstaller
# run: pip install pyinstaller
# - name: Prep file locations
# shell: bash
# run: |
# mkdir -p dist
# cp horusdemodlib/build/src/libhorus.dylib .
# - name: Run pyinstaller
# run: pyinstaller horus-gui_osx_runner.spec
# - name: Create the DMG file
# run: hdiutil create -format UDZO -srcfolder dist/horus-gui.app dist/horus-gui_OSX-Intel.dmg
# - name: Upload Artifact
# uses: actions/upload-artifact@v4
# with:
# name: horus-gui_OSX-Intel.zip
# path: dist/horus-gui_OSX-Intel.dmg
# retention-days: 2

Wyświetl plik

@ -44,7 +44,7 @@ Written by:
## Usage
### Binary Builds
Until we sort out automated release builds, binary releases of horus-gui for Windows and OSX (M1 and Intel) are available here: https://rfhead.net/horus/horusgui/
Binary builds for some platforms are available on the releases page: https://github.com/projecthorus/horus-gui/releases
Please let me know if you have issues!
@ -55,7 +55,7 @@ $ git clone https://github.com/projecthorus/horusdemodlib.git
$ cd horusdemodlib && mkdir build && cd build
$ cmake ..
$ make
$ make install
$ sudo make install
```
### Grab this Repo
@ -66,8 +66,6 @@ $ cd horus-gui
### (Optional) Create a Virtual Environment
**Warning - Python 3.10 will not work until a known compatability issue with pyaudio has been fixed. Use Python 3.9.**
Create a virtual environment and install dependencies.
```console
$ python3 -m venv venv
@ -83,10 +81,28 @@ $ pip install -r requirements.txt
```
NOTE: Under linux based distros, you may also need to install `python3-distutils` and `python-setuptools`. If you get errors relating to pyaudio when trying to install into a venv, make sure that portaudio is installed (`libportaudio-dev` or `portaudio19-dev` under Linux distros, or `portaudio` under Macports), and then install pyaudio pointing to the portaudio lib by running:
On Linux:
```
(Linux) $ pip install --global-option='build_ext' --global-option='-I/usr/include' --global-option='-L/usr/lib' pyaudio
(OSX) $ pip install --global-option='build_ext' --global-option='-I/opt/local/include' --global-option='-L/opt/local/lib' pyaudio
$ export CFLAGS="-I/usr/include"
$ export LDFLAGS="-L/usr/lib"
(venv) $ pip install pyaudio
```
On OSX using Macports:
```
$ export CFLAGS="-I/opt/local/include"
$ export LDFLAGS="-L/opt/local/lib"
(venv) $ pip install pyaudio
```
On OSX using Homebrew
```
$ export CFLAGS="-I/opt/homebrew/include"
$ export LDFLAGS="-L/opt/homebrew/lib"
(venv) $ pip install pyaudio
```
You should then be able to re-run the install requirements command above.
### Install Package
@ -101,12 +117,12 @@ entry points so it can be used like a normal install.
### Run
```console
$ python -m horusgui.gui
$ (venv) python -m horusgui.gui
```
Or run the helper startup script:
```console
$ python horus-gui.py
$ (venv) python horus-gui.py
```
### Updating
@ -127,4 +143,4 @@ $ . venv/bin/activate (if using a venv)
$ pip install horusdemodlib --upgrade
```
You should then be OK to run horusgui. Configuration settings will be reset when the version number of horus-gui is incremented, until I settle on on a configuration parameter set.
You should then be OK to run horusgui.

Wyświetl plik

@ -4,7 +4,7 @@ block_cipher = None
a = Analysis(['horus-gui.py'],
pathex=['/Users/darkside/Dev/horus-gui'],
pathex=['.'],
binaries=[('../horusdemodlib/build/src/libhorus.dylib','.')],
datas=[],
hiddenimports=[],
@ -38,4 +38,8 @@ coll = COLLECT(exe,
app = BUNDLE(coll,
name='horus-gui.app',
icon='doc/horus_logo.icns',
bundle_identifier=None)
bundle_identifier=None,
info_plist={
'NSMicrophoneUsageDescription': 'Horus-GUI needs audio access to receive telemetry.'
},
)

Wyświetl plik

@ -0,0 +1,45 @@
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(['horus-gui.py'],
pathex=['.'],
binaries=[('libhorus.dylib','.')],
datas=[],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='horus-gui',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False , icon='doc/horus_logo.icns')
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='horus-gui')
app = BUNDLE(coll,
name='horus-gui.app',
icon='doc/horus_logo.icns',
bundle_identifier=None,
info_plist={
'NSMicrophoneUsageDescription': 'Horus-GUI needs audio access to receive telemetry.'
},
)

Wyświetl plik

@ -1 +1 @@
__version__ = "0.3.9"
__version__ = "0.3.18"

Wyświetl plik

@ -48,6 +48,7 @@ def init_audio(widgets):
audioDevices[_name] = _dev
# Add to audio device selection list.
widgets["audioDeviceSelector"].addItem(_name)
logging.debug(f"Found audio device: {_name}")
# Select first item.
if len(list(audioDevices.keys())) > 0:
@ -75,6 +76,8 @@ def populate_sample_rates(widgets):
widgets["audioSampleRateSelector"].addItem(str(48000))
widgets["audioSampleRateSelector"].setCurrentIndex(0)
return
if _dev_name in audioDevices:
# Determine which sample rates from a common list are valid for this device.
_possible_rates = [8000.0, 22050.0, 44100.0, 48000.0, 96000.0]
@ -105,7 +108,7 @@ def populate_sample_rates(widgets):
_default_samp_rate = int(audioDevices[_dev_name]["defaultSampleRate"])
widgets["audioSampleRateSelector"].setCurrentText(str(_default_samp_rate))
else:
logging.error("Audio - Unknown Audio Device")
logging.error(f"Audio - Unknown Audio Device ({_dev_name})")
class AudioStream(object):

Wyświetl plik

@ -8,7 +8,6 @@ import json
import logging
import os
from pyqtgraph.Qt import QtCore
from ruamel.yaml import YAML
from . import __version__
from .modem import populate_modem_settings
from .audio import populate_sample_rates
@ -35,6 +34,9 @@ default_config = {
"rotator_type": "rotctld",
"rotator_host": "localhost",
"rotator_port": 4533,
"logging_enabled": False,
"log_format": "CSV",
"log_directory": "",
"payload_list": json.dumps(horusdemodlib.payloads.HORUS_PAYLOAD_LIST),
"custom_field_list": json.dumps({})
}
@ -73,7 +75,8 @@ def read_config(widgets):
""" Read in configuration settings from Qt """
global qt_settings, default_config
OK_VERSIONS = [__version__, '0.3.8', '0.3.7', '0.3.6', '0.3.5', '0.3.4', '0.3.1', '0.2.1']
# This is getting a bit ridiculous, need to re-think this approach.
OK_VERSIONS = [__version__, '0.3.17', '0.3.16', '0.3.15', '0.3.14', '0.3.13', '0.3.12', '0.3.11', '0.3.10', '0.3.9', '0.3.8', '0.3.7', '0.3.6', '0.3.5', '0.3.4', '0.3.1', '0.2.1']
# Try and read in the version parameter from QSettings
if qt_settings.value("version") not in OK_VERSIONS:
@ -83,14 +86,14 @@ def read_config(widgets):
for _setting in default_config:
try:
_new_setting = qt_settings.value(_setting)
if _new_setting:
if _new_setting is not None:
default_config[_setting] = _new_setting
except Exception as e:
logging.debug("Missing config setting: " + _setting)
if widgets:
# Habitat Settings
widgets["habitatUploadSelector"].setChecked(ValueToBool(default_config["habitat_upload_enabled"]))
widgets["sondehubUploadSelector"].setChecked(ValueToBool(default_config["habitat_upload_enabled"]))
widgets["userCallEntry"].setText(str(default_config["habitat_call"]))
widgets["userLatEntry"].setText(str(default_config["habitat_lat"]))
widgets["userLonEntry"].setText(str(default_config["habitat_lon"]))
@ -122,6 +125,11 @@ def read_config(widgets):
widgets["rotatorHostEntry"].setText(str(default_config["rotator_host"]))
widgets["rotatorPortEntry"].setText(str(default_config["rotator_port"]))
# Logging Settings
widgets["loggingPathEntry"].setText(str(default_config["log_directory"]))
widgets["loggingFormatSelector"].setCurrentText(default_config["log_format"])
widgets["enableLoggingSelector"].setChecked(ValueToBool(default_config["logging_enabled"]))
if default_config['baud_rate'] != -1:
widgets["horusModemRateSelector"].setCurrentText(str(default_config['baud_rate']))
@ -145,8 +153,9 @@ def save_config(widgets):
if widgets:
default_config["habitat_upload_enabled"] = widgets[
"habitatUploadSelector"
"sondehubUploadSelector"
].isChecked()
default_config["version"] = __version__
default_config["habitat_call"] = widgets["userCallEntry"].text()
default_config["habitat_lat"] = float(widgets["userLatEntry"].text())
default_config["habitat_lon"] = float(widgets["userLonEntry"].text())
@ -164,6 +173,9 @@ def save_config(widgets):
default_config["rotator_type"] = widgets["rotatorTypeSelector"].currentText()
default_config["rotator_host"] = widgets["rotatorHostEntry"].text()
default_config["rotator_port"] = int(widgets["rotatorPortEntry"].text())
default_config["logging_enabled"] = widgets["enableLoggingSelector"].isChecked()
default_config["log_directory"] = widgets["loggingPathEntry"].text()
default_config["log_format"] = widgets["loggingFormatSelector"].currentText()
default_config["payload_list"] = json.dumps(horusdemodlib.payloads.HORUS_PAYLOAD_LIST)
default_config["custom_field_list"] = json.dumps(horusdemodlib.payloads.HORUS_CUSTOM_FIELDS)

Wyświetl plik

@ -18,10 +18,12 @@ import datetime
import glob
import logging
import platform
import time
import pyqtgraph as pg
import numpy as np
from queue import Queue
from pyqtgraph.Qt import QtCore, QtGui, QtWidgets
#from pyqtgraph.Qt import QtCore, QtGui, QtWidgets
from PyQt5 import QtWidgets, QtGui
from pyqtgraph.dockarea import *
from threading import Thread
@ -31,10 +33,10 @@ from .udpaudio import *
from .fft import *
from .modem import *
from .config import *
from .habitat import *
from .utils import position_info
from .icon import getHorusIcon
from .rotators import ROTCTLD, PSTRotator
from .telemlogger import TelemetryLogger
from horusdemodlib.demod import HorusLib, Mode
from horusdemodlib.decoder import decode_packet, parse_ukhas_string
from horusdemodlib.payloads import *
@ -52,9 +54,9 @@ DEFAULT_ESTIMATOR_MAX = 4000
widgets = {}
# Queues for handling updates to image / status indications.
fft_update_queue = Queue(256)
status_update_queue = Queue(256)
log_update_queue = Queue(256)
fft_update_queue = Queue(1024)
status_update_queue = Queue(1024)
log_update_queue = Queue(2048)
# List of audio devices and their info
audio_devices = {}
@ -63,11 +65,13 @@ audio_devices = {}
audio_stream = None
fft_process = None
horus_modem = None
habitat_uploader = None
sondehub_uploader = None
telemetry_logger = None
decoder_init = False
last_packet_time = None
# Rotator object
rotator = None
@ -82,6 +86,7 @@ running = False
parser = argparse.ArgumentParser(description="Project Horus GUI", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--payload-id-list", type=str, default=None, help="Use supplied Payload ID List instead of downloading a new one.")
parser.add_argument("--custom-field-list", type=str, default=None, help="Use supplied Custom Field List instead of downloading a new one.")
parser.add_argument("--libfix", action="store_true", default=False, help="Search for libhorus.dll/so in ./ instead of on the path.")
parser.add_argument("-v", "--verbose", action="store_true", default=False, help="Verbose output (set logging level to DEBUG)")
args = parser.parse_args()
@ -103,7 +108,7 @@ logging.basicConfig(
pg.mkQApp()
# GUI LAYOUT - Gtk Style!
win = QtGui.QMainWindow()
win = QtWidgets.QMainWindow()
area = DockArea()
win.setCentralWidget(area)
win.setWindowTitle(f"Horus Telemetry GUI - v{__version__}")
@ -112,7 +117,7 @@ win.setWindowIcon(getHorusIcon())
# Create multiple dock areas, for displaying our data.
d0 = Dock("Audio", size=(300, 50))
d0_modem = Dock("Modem", size=(300, 80))
d0_habitat = Dock("Habitat", size=(300, 200))
d0_habitat = Dock("SondeHub", size=(300, 200))
d0_other = Dock("Other", size=(300, 100))
d0_rotator = Dock("Rotator", size=(300, 100))
d1 = Dock("Spectrum", size=(800, 350))
@ -139,14 +144,14 @@ d0_habitat.raiseDock()
# Controls
w1_audio = pg.LayoutWidget()
# TNC Connection
widgets["audioDeviceLabel"] = QtGui.QLabel("<b>Audio Device:</b>")
widgets["audioDeviceSelector"] = QtGui.QComboBox()
widgets["audioDeviceLabel"] = QtWidgets.QLabel("<b>Audio Device:</b>")
widgets["audioDeviceSelector"] = QtWidgets.QComboBox()
widgets["audioSampleRateLabel"] = QtGui.QLabel("<b>Sample Rate (Hz):</b>")
widgets["audioSampleRateSelector"] = QtGui.QComboBox()
widgets["audioSampleRateLabel"] = QtWidgets.QLabel("<b>Sample Rate (Hz):</b>")
widgets["audioSampleRateSelector"] = QtWidgets.QComboBox()
widgets["audioDbfsLabel"] = QtGui.QLabel("<b>Input Level (dBFS):</b>")
widgets["audioDbfsValue"] = QtGui.QLabel("--")
widgets["audioDbfsLabel"] = QtWidgets.QLabel("<b>Input Level (dBFS):</b>")
widgets["audioDbfsValue"] = QtWidgets.QLabel("--")
widgets["audioDbfsValue_float"] = 0.0
w1_audio.addWidget(widgets["audioDeviceLabel"], 0, 0, 1, 1)
@ -161,29 +166,29 @@ w1_modem = pg.LayoutWidget()
# Modem Parameters
widgets["horusModemLabel"] = QtGui.QLabel("<b>Mode:</b>")
widgets["horusModemSelector"] = QtGui.QComboBox()
widgets["horusModemLabel"] = QtWidgets.QLabel("<b>Mode:</b>")
widgets["horusModemSelector"] = QtWidgets.QComboBox()
widgets["horusModemRateLabel"] = QtGui.QLabel("<b>Baudrate:</b>")
widgets["horusModemRateSelector"] = QtGui.QComboBox()
widgets["horusModemRateLabel"] = QtWidgets.QLabel("<b>Baudrate:</b>")
widgets["horusModemRateSelector"] = QtWidgets.QComboBox()
widgets["horusMaskEstimatorLabel"] = QtGui.QLabel("<b>Enable Mask Estim.:</b>")
widgets["horusMaskEstimatorSelector"] = QtGui.QCheckBox()
widgets["horusMaskEstimatorLabel"] = QtWidgets.QLabel("<b>Enable Mask Estim.:</b>")
widgets["horusMaskEstimatorSelector"] = QtWidgets.QCheckBox()
widgets["horusMaskEstimatorSelector"].setToolTip(
"Enable the mask frequency estimator, which makes uses of the \n"\
"tone spacing value entered below as extra input to the frequency\n"\
"estimator. This can help decode performance in very weak signal conditions."
)
widgets["horusMaskSpacingLabel"] = QtGui.QLabel("<b>Tone Spacing (Hz):</b>")
widgets["horusMaskSpacingEntry"] = QtGui.QLineEdit("270")
widgets["horusMaskSpacingLabel"] = QtWidgets.QLabel("<b>Tone Spacing (Hz):</b>")
widgets["horusMaskSpacingEntry"] = QtWidgets.QLineEdit("270")
widgets["horusMaskSpacingEntry"].setToolTip(
"If the tone spacing of the transmitter is known, it can be entered here,\n"\
"and used with the mask estimator option above. The default tone spacing for\n"\
"a RS41-based transmitter is 270 Hz."
)
widgets["horusManualEstimatorLabel"] = QtGui.QLabel("<b>Manual Estim. Limits:</b>")
widgets["horusManualEstimatorSelector"] = QtGui.QCheckBox()
widgets["horusManualEstimatorLabel"] = QtWidgets.QLabel("<b>Manual Estim. Limits:</b>")
widgets["horusManualEstimatorSelector"] = QtWidgets.QCheckBox()
widgets["horusManualEstimatorSelector"].setToolTip(
"Enables manual selection of the frequency estimator limits. This will enable\n"\
"a slidable area on the spectrum display, which can be used to select the frequency\n"\
@ -193,7 +198,7 @@ widgets["horusManualEstimatorSelector"].setToolTip(
)
# Start/Stop
widgets["startDecodeButton"] = QtGui.QPushButton("Start")
widgets["startDecodeButton"] = QtWidgets.QPushButton("Start")
widgets["startDecodeButton"].setEnabled(False)
w1_modem.addWidget(widgets["horusModemLabel"], 0, 0, 1, 1)
@ -213,106 +218,119 @@ d0_modem.addWidget(w1_modem)
w1_habitat = pg.LayoutWidget()
# Listener Information
widgets["habitatHeading"] = QtGui.QLabel("<b>Habitat Settings</b>")
widgets["habitatUploadLabel"] = QtGui.QLabel("<b>Enable Habitat Upload:</b>")
widgets["habitatUploadSelector"] = QtGui.QCheckBox()
widgets["habitatUploadSelector"].setChecked(True)
widgets["sondehubUploadLabel"] = QtGui.QLabel("<b>Enable SondeHub-Ham Upload:</b>")
widgets["sondehubUploadSelector"] = QtGui.QCheckBox()
widgets["habitatHeading"] = QtWidgets.QLabel("<b>SondeHub Settings</b>")
widgets["sondehubUploadLabel"] = QtWidgets.QLabel("<b>Enable SondeHub-Ham Upload:</b>")
widgets["sondehubUploadSelector"] = QtWidgets.QCheckBox()
widgets["sondehubUploadSelector"].setChecked(True)
widgets["userCallLabel"] = QtGui.QLabel("<b>Callsign:</b>")
widgets["userCallEntry"] = QtGui.QLineEdit("N0CALL")
widgets["userCallLabel"] = QtWidgets.QLabel("<b>Callsign:</b>")
widgets["userCallEntry"] = QtWidgets.QLineEdit("N0CALL")
widgets["userCallEntry"].setMaxLength(20)
widgets["userCallEntry"].setToolTip(
"Your station callsign, which doesn't necessarily need to be an\n"\
"amateur radio callsign."
"amateur radio callsign, just something unique!"
)
widgets["userLocationLabel"] = QtGui.QLabel("<b>Lat/Lon:</b>")
widgets["userLatEntry"] = QtGui.QLineEdit("0.0")
widgets["userLocationLabel"] = QtWidgets.QLabel("<b>Lat/Lon:</b>")
widgets["userLatEntry"] = QtWidgets.QLineEdit("0.0")
widgets["userLatEntry"].setToolTip("Station Latitude in Decimal Degrees, e.g. -34.123456")
widgets["userLonEntry"] = QtGui.QLineEdit("0.0")
widgets["userLonEntry"] = QtWidgets.QLineEdit("0.0")
widgets["userLonEntry"].setToolTip("Station Longitude in Decimal Degrees, e.g. 138.123456")
widgets["userAltitudeLabel"] = QtGui.QLabel("<b>Altitude:</b>")
widgets["userAltEntry"] = QtGui.QLineEdit("0.0")
widgets["userAltitudeLabel"] = QtWidgets.QLabel("<b>Altitude:</b>")
widgets["userAltEntry"] = QtWidgets.QLineEdit("0.0")
widgets["userAltEntry"].setToolTip("Station Altitude in Metres Above Sea Level.")
widgets["userAntennaLabel"] = QtGui.QLabel("<b>Antenna:</b>")
widgets["userAntennaEntry"] = QtGui.QLineEdit("")
widgets["userAntennaLabel"] = QtWidgets.QLabel("<b>Antenna:</b>")
widgets["userAntennaEntry"] = QtWidgets.QLineEdit("")
widgets["userAntennaEntry"].setToolTip("A text description of your station's antenna.")
widgets["userRadioLabel"] = QtGui.QLabel("<b>Radio:</b>")
widgets["userRadioEntry"] = QtGui.QLineEdit("Horus-GUI " + __version__)
widgets["userRadioLabel"] = QtWidgets.QLabel("<b>Radio:</b>")
widgets["userRadioEntry"] = QtWidgets.QLineEdit("Horus-GUI " + __version__)
widgets["userRadioEntry"].setToolTip(
"A text description of your station's radio setup.\n"\
"This field will be automatically prefixed with Horus-GUI."
)
widgets["habitatUploadPosition"] = QtGui.QPushButton("Re-upload Position")
widgets["habitatUploadPosition"] = QtWidgets.QPushButton("Re-upload Position")
widgets["habitatUploadPosition"].setToolTip(
"Manually re-upload your position information to HabHub and SondeHub.\n"\
"Manually re-upload your position information to SondeHub-Amateur.\n"\
"Note that it can take a few minutes for your new information to\n"\
"appear on the map."
)
widgets["dialFreqLabel"] = QtGui.QLabel("<b>Radio Dial Freq (MHz):</b>")
widgets["dialFreqEntry"] = QtGui.QLineEdit("")
widgets["dialFreqLabel"] = QtWidgets.QLabel("<b>Radio Dial Freq (MHz):</b>")
widgets["dialFreqEntry"] = QtWidgets.QLineEdit("")
widgets["dialFreqEntry"].setToolTip(
"Optional entry of your radio's dial frequency in MHz.\n"\
"Used to provide frequency information on Habitat & SondeHub."\
"Optional entry of your radio's dial frequency in MHz (e.g. 437.600).\n"\
"Used to provide frequency information on SondeHub-Amateur."\
)
widgets["saveSettingsButton"] = QtGui.QPushButton("Save Settings")
widgets["saveSettingsButton"] = QtWidgets.QPushButton("Save Settings")
w1_habitat.addWidget(widgets["habitatUploadLabel"], 0, 0, 1, 1)
w1_habitat.addWidget(widgets["habitatUploadSelector"], 0, 1, 1, 1)
w1_habitat.addWidget(widgets["sondehubUploadLabel"], 1, 0, 1, 1)
w1_habitat.addWidget(widgets["sondehubUploadSelector"], 1, 1, 1, 1)
w1_habitat.addWidget(widgets["userCallLabel"], 2, 0, 1, 1)
w1_habitat.addWidget(widgets["userCallEntry"], 2, 1, 1, 2)
w1_habitat.addWidget(widgets["userLocationLabel"], 3, 0, 1, 1)
w1_habitat.addWidget(widgets["userLatEntry"], 3, 1, 1, 1)
w1_habitat.addWidget(widgets["userLonEntry"], 3, 2, 1, 1)
w1_habitat.addWidget(widgets["userAltitudeLabel"], 4, 0, 1, 1)
w1_habitat.addWidget(widgets["userAltEntry"], 4, 1, 1, 2)
w1_habitat.addWidget(widgets["userAntennaLabel"], 5, 0, 1, 1)
w1_habitat.addWidget(widgets["userAntennaEntry"], 5, 1, 1, 2)
w1_habitat.addWidget(widgets["userRadioLabel"], 6, 0, 1, 1)
w1_habitat.addWidget(widgets["userRadioEntry"], 6, 1, 1, 2)
w1_habitat.addWidget(widgets["dialFreqLabel"], 7, 0, 1, 1)
w1_habitat.addWidget(widgets["dialFreqEntry"], 7, 1, 1, 2)
w1_habitat.addWidget(widgets["habitatUploadPosition"], 8, 0, 1, 3)
w1_habitat.layout.setRowStretch(9, 1)
w1_habitat.addWidget(widgets["saveSettingsButton"], 10, 0, 1, 3)
w1_habitat.addWidget(widgets["sondehubUploadLabel"], 0, 0, 1, 1)
w1_habitat.addWidget(widgets["sondehubUploadSelector"], 0, 1, 1, 1)
w1_habitat.addWidget(widgets["userCallLabel"], 1, 0, 1, 1)
w1_habitat.addWidget(widgets["userCallEntry"], 1, 1, 1, 2)
w1_habitat.addWidget(widgets["userLocationLabel"], 2, 0, 1, 1)
w1_habitat.addWidget(widgets["userLatEntry"], 2, 1, 1, 1)
w1_habitat.addWidget(widgets["userLonEntry"], 2, 2, 1, 1)
w1_habitat.addWidget(widgets["userAltitudeLabel"], 3, 0, 1, 1)
w1_habitat.addWidget(widgets["userAltEntry"], 3, 1, 1, 2)
w1_habitat.addWidget(widgets["userAntennaLabel"], 4, 0, 1, 1)
w1_habitat.addWidget(widgets["userAntennaEntry"], 4, 1, 1, 2)
w1_habitat.addWidget(widgets["userRadioLabel"], 5, 0, 1, 1)
w1_habitat.addWidget(widgets["userRadioEntry"], 5, 1, 1, 2)
w1_habitat.addWidget(widgets["dialFreqLabel"], 6, 0, 1, 1)
w1_habitat.addWidget(widgets["dialFreqEntry"], 6, 1, 1, 2)
w1_habitat.addWidget(widgets["habitatUploadPosition"], 7, 0, 1, 3)
w1_habitat.layout.setRowStretch(8, 1)
w1_habitat.addWidget(widgets["saveSettingsButton"], 9, 0, 1, 3)
d0_habitat.addWidget(w1_habitat)
w1_other = pg.LayoutWidget()
widgets["horusHeaderLabel"] = QtGui.QLabel("<b><u>Telemetry Forwarding</u></b>")
widgets["horusUploadLabel"] = QtGui.QLabel("<b>Enable Horus UDP Output:</b>")
widgets["horusUploadSelector"] = QtGui.QCheckBox()
widgets["horusHeaderLabel"] = QtWidgets.QLabel("<b><u>Telemetry Forwarding</u></b>")
widgets["horusUploadLabel"] = QtWidgets.QLabel("<b>Enable Horus UDP Output:</b>")
widgets["horusUploadSelector"] = QtWidgets.QCheckBox()
widgets["horusUploadSelector"].setChecked(True)
widgets["horusUploadSelector"].setToolTip(
"Enable output of 'Horus UDP' JSON messages. These are emitted as a JSON object\n"\
"and contain the fields: callsign, time, latitude, longitude, altitude, snr"\
)
widgets["horusUDPLabel"] = QtGui.QLabel("<b>Horus UDP Port:</b>")
widgets["horusUDPEntry"] = QtGui.QLineEdit("55672")
widgets["horusUDPLabel"] = QtWidgets.QLabel("<b>Horus UDP Port:</b>")
widgets["horusUDPEntry"] = QtWidgets.QLineEdit("55672")
widgets["horusUDPEntry"].setMaxLength(5)
widgets["horusUDPEntry"].setToolTip(
"UDP Port to output 'Horus UDP' JSON messages to."
)
widgets["ozimuxUploadLabel"] = QtGui.QLabel("<b>Enable OziMux UDP Output:</b>")
widgets["ozimuxUploadSelector"] = QtGui.QCheckBox()
widgets["ozimuxUploadLabel"] = QtWidgets.QLabel("<b>Enable OziMux UDP Output:</b>")
widgets["ozimuxUploadSelector"] = QtWidgets.QCheckBox()
widgets["ozimuxUploadSelector"].setChecked(False)
widgets["ozimuxUploadSelector"].setToolTip(
"Output OziMux UDP messages. These are of the form:\n"\
"'TELEMETRY,HH:MM:SS,lat,lon,alt\\n'"
)
widgets["ozimuxUDPLabel"] = QtGui.QLabel("<b>Ozimux UDP Port:</b>")
widgets["ozimuxUDPEntry"] = QtGui.QLineEdit("55683")
widgets["ozimuxUDPLabel"] = QtWidgets.QLabel("<b>Ozimux UDP Port:</b>")
widgets["ozimuxUDPEntry"] = QtWidgets.QLineEdit("55683")
widgets["ozimuxUDPEntry"].setMaxLength(5)
widgets["ozimuxUDPEntry"].setToolTip(
"UDP Port to output 'OziMux' UDP messages to."
)
widgets["otherHeaderLabel"] = QtGui.QLabel("<b><u>Other Settings</u></b>")
widgets["inhibitCRCLabel"] = QtGui.QLabel("<b>Hide Failed CRC Errors:</b>")
widgets["inhibitCRCSelector"] = QtGui.QCheckBox()
widgets["loggingHeaderLabel"] = QtWidgets.QLabel("<b><u>Logging</u></b>")
widgets["enableLoggingLabel"] = QtWidgets.QLabel("<b>Enable Logging:</b>")
widgets["enableLoggingSelector"] = QtWidgets.QCheckBox()
widgets["enableLoggingSelector"].setChecked(False)
widgets["enableLoggingSelector"].setToolTip(
"Enable logging of received telemetry to disk (JSON)"
)
widgets["loggingFormatLabel"] = QtWidgets.QLabel("<b>Log Format:</b>")
widgets["loggingFormatSelector"] = QtWidgets.QComboBox()
widgets["loggingFormatSelector"].addItem("CSV")
widgets["loggingFormatSelector"].addItem("JSON")
widgets["loggingPathLabel"] = QtWidgets.QLabel("<b>Log Directory:</b>")
widgets["loggingPathEntry"] = QtWidgets.QLineEdit("")
widgets["loggingPathEntry"].setToolTip(
"Logging Directory"
)
widgets["selectLogDirButton"] = QtWidgets.QPushButton("Select Directory")
widgets["otherHeaderLabel"] = QtWidgets.QLabel("<b><u>Other Settings</u></b>")
widgets["inhibitCRCLabel"] = QtWidgets.QLabel("<b>Hide Failed CRC Errors:</b>")
widgets["inhibitCRCSelector"] = QtWidgets.QCheckBox()
widgets["inhibitCRCSelector"].setChecked(True)
widgets["inhibitCRCSelector"].setToolTip(
"Hide CRC Failed error messages."
@ -327,50 +345,58 @@ w1_other.addWidget(widgets["ozimuxUploadLabel"], 3, 0, 1, 1)
w1_other.addWidget(widgets["ozimuxUploadSelector"], 3, 1, 1, 1)
w1_other.addWidget(widgets["ozimuxUDPLabel"], 4, 0, 1, 1)
w1_other.addWidget(widgets["ozimuxUDPEntry"], 4, 1, 1, 1)
w1_other.addWidget(widgets["otherHeaderLabel"], 5, 0, 1, 2)
w1_other.addWidget(widgets["inhibitCRCLabel"], 6, 0, 1, 1)
w1_other.addWidget(widgets["inhibitCRCSelector"], 6, 1, 1, 1)
w1_other.layout.setRowStretch(7, 1)
w1_other.addWidget(widgets["loggingHeaderLabel"], 5, 0, 1, 2)
w1_other.addWidget(widgets["enableLoggingLabel"], 6, 0, 1, 1)
w1_other.addWidget(widgets["enableLoggingSelector"], 6, 1, 1, 1)
w1_other.addWidget(widgets["loggingFormatLabel"], 7, 0, 1, 1)
w1_other.addWidget(widgets["loggingFormatSelector"], 7, 1, 1, 1)
w1_other.addWidget(widgets["loggingPathLabel"], 8, 0, 1, 1)
w1_other.addWidget(widgets["loggingPathEntry"], 8, 1, 1, 1)
w1_other.addWidget(widgets["selectLogDirButton"], 9, 0, 1, 2)
w1_other.addWidget(widgets["otherHeaderLabel"], 10, 0, 1, 2)
w1_other.addWidget(widgets["inhibitCRCLabel"], 11, 0, 1, 1)
w1_other.addWidget(widgets["inhibitCRCSelector"], 11, 1, 1, 1)
w1_other.layout.setRowStretch(12, 1)
d0_other.addWidget(w1_other)
w1_rotator = pg.LayoutWidget()
widgets["rotatorHeaderLabel"] = QtGui.QLabel("<b><u>Rotator Control</u></b>")
widgets["rotatorHeaderLabel"] = QtWidgets.QLabel("<b><u>Rotator Control</u></b>")
widgets["rotatorTypeLabel"] = QtGui.QLabel("<b>Rotator Type:</b>")
widgets["rotatorTypeSelector"] = QtGui.QComboBox()
widgets["rotatorTypeLabel"] = QtWidgets.QLabel("<b>Rotator Type:</b>")
widgets["rotatorTypeSelector"] = QtWidgets.QComboBox()
widgets["rotatorTypeSelector"].addItem("rotctld")
widgets["rotatorTypeSelector"].addItem("PSTRotator")
widgets["rotatorHostLabel"] = QtGui.QLabel("<b>Rotator Hostname:</b>")
widgets["rotatorHostEntry"] = QtGui.QLineEdit("localhost")
widgets["rotatorHostLabel"] = QtWidgets.QLabel("<b>Rotator Hostname:</b>")
widgets["rotatorHostEntry"] = QtWidgets.QLineEdit("localhost")
widgets["rotatorHostEntry"].setToolTip(
"Hostname of the rotctld or PSTRotator Server.\n"\
)
widgets["rotatorPortLabel"] = QtGui.QLabel("<b>Rotator TCP/UDP Port:</b>")
widgets["rotatorPortEntry"] = QtGui.QLineEdit("4533")
widgets["rotatorPortLabel"] = QtWidgets.QLabel("<b>Rotator TCP/UDP Port:</b>")
widgets["rotatorPortEntry"] = QtWidgets.QLineEdit("4533")
widgets["rotatorPortEntry"].setMaxLength(5)
widgets["rotatorPortEntry"].setToolTip(
"TCP (rotctld) or UDP (PSTRotator) port to connect to.\n"\
"Default for rotctld: 4533\n"\
"Default for PSTRotator: 12000"
)
widgets["rotatorThresholdLabel"] = QtGui.QLabel("<b>Rotator Movement Threshold:</b>")
widgets["rotatorThresholdEntry"] = QtGui.QLineEdit("5.0")
widgets["rotatorThresholdLabel"] = QtWidgets.QLabel("<b>Rotator Movement Threshold:</b>")
widgets["rotatorThresholdEntry"] = QtWidgets.QLineEdit("5.0")
widgets["rotatorThresholdEntry"].setToolTip(
"Only move if the angle between the payload position and \n"\
"the current rotator position is more than this, in degrees."
)
widgets["rotatorConnectButton"] = QtGui.QPushButton("Start")
widgets["rotatorConnectButton"] = QtWidgets.QPushButton("Start")
widgets["rotatorCurrentStatusLabel"] = QtGui.QLabel("<b>Status:</b>")
widgets["rotatorCurrentStatusValue"] = QtGui.QLabel("Not Started.")
widgets["rotatorCurrentStatusLabel"] = QtWidgets.QLabel("<b>Status:</b>")
widgets["rotatorCurrentStatusValue"] = QtWidgets.QLabel("Not Started.")
widgets["rotatorCurrentPositionLabel"] = QtGui.QLabel("<b>Commanded Az/El:</b>")
widgets["rotatorCurrentPositionValue"] = QtGui.QLabel("---˚, --˚")
widgets["rotatorCurrentPositionLabel"] = QtWidgets.QLabel("<b>Commanded Az/El:</b>")
widgets["rotatorCurrentPositionValue"] = QtWidgets.QLabel("---˚, --˚")
@ -404,25 +430,25 @@ widgets["spectrumPlotData"] = widgets["spectrumPlot"].plot([0])
widgets["estimatorLines"] = [
pg.InfiniteLine(
pos=-1000,
pen=pg.mkPen(color="w", width=2, style=QtCore.Qt.DashLine),
pen=pg.mkPen(color="w", width=2, style=QtCore.Qt.PenStyle.DashLine),
label="F1",
labelOpts={'position':0.9}
),
pg.InfiniteLine(
pos=-1000,
pen=pg.mkPen(color="w", width=2, style=QtCore.Qt.DashLine),
pen=pg.mkPen(color="w", width=2, style=QtCore.Qt.PenStyle.DashLine),
label="F2",
labelOpts={'position':0.9}
),
pg.InfiniteLine(
pos=-1000,
pen=pg.mkPen(color="w", width=2, style=QtCore.Qt.DashLine),
pen=pg.mkPen(color="w", width=2, style=QtCore.Qt.PenStyle.DashLine),
label="F3",
labelOpts={'position':0.9}
),
pg.InfiniteLine(
pos=-1000,
pen=pg.mkPen(color="w", width=2, style=QtCore.Qt.DashLine),
pen=pg.mkPen(color="w", width=2, style=QtCore.Qt.PenStyle.DashLine),
label="F4",
labelOpts={'position':0.9}
),
@ -447,13 +473,13 @@ widgets["spectrumPlotRange"] = [-100, -20]
w3_stats = pg.LayoutWidget()
widgets["snrBar"] = QtWidgets.QProgressBar()
widgets["snrBar"].setOrientation(QtCore.Qt.Vertical)
widgets["snrBar"].setOrientation(QtCore.Qt.Orientation.Vertical)
widgets["snrBar"].setRange(-10, 15)
widgets["snrBar"].setValue(-10)
widgets["snrBar"].setTextVisible(False)
widgets["snrBar"].setAlignment(QtCore.Qt.AlignCenter)
widgets["snrLabel"] = QtGui.QLabel("--.-")
widgets["snrLabel"].setAlignment(QtCore.Qt.AlignCenter);
widgets["snrBar"].setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
widgets["snrLabel"] = QtWidgets.QLabel("--.-")
widgets["snrLabel"].setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter);
widgets["snrLabel"].setFont(QtGui.QFont("Courier New", 14))
w3_stats.addWidget(widgets["snrBar"], 0, 1, 1, 1)
w3_stats.addWidget(widgets["snrLabel"], 1, 0, 1, 3)
@ -488,16 +514,20 @@ d2_snr.addWidget(widgets["snrPlot"])
# Telemetry Data
w4_data = pg.LayoutWidget()
widgets["latestRawSentenceLabel"] = QtGui.QLabel("<b>Latest Packet (Raw):</b>")
widgets["latestRawSentenceData"] = QtGui.QLineEdit("NO DATA")
widgets["latestRawSentenceLabel"] = QtWidgets.QLabel("<b>Latest Packet (Raw):</b>")
widgets["latestRawSentenceData"] = QtWidgets.QLineEdit("NO DATA")
widgets["latestRawSentenceData"].setReadOnly(True)
widgets["latestDecodedSentenceLabel"] = QtGui.QLabel("<b>Latest Packet (Decoded):</b>")
widgets["latestDecodedSentenceData"] = QtGui.QLineEdit("NO DATA")
widgets["latestDecodedSentenceLabel"] = QtWidgets.QLabel("<b>Latest Packet (Decoded):</b>")
widgets["latestDecodedSentenceData"] = QtWidgets.QLineEdit("NO DATA")
widgets["latestDecodedSentenceData"].setReadOnly(True)
widgets["latestDecodedAgeLabel"] = QtWidgets.QLabel("<b>Last Packet Age:</b>")
widgets["latestDecodedAgeData"] = QtWidgets.QLabel("No packet yet!")
w4_data.addWidget(widgets["latestRawSentenceLabel"], 0, 0, 1, 1)
w4_data.addWidget(widgets["latestRawSentenceData"], 0, 1, 1, 6)
w4_data.addWidget(widgets["latestDecodedSentenceLabel"], 1, 0, 1, 1)
w4_data.addWidget(widgets["latestDecodedSentenceData"], 1, 1, 1, 6)
w4_data.addWidget(widgets["latestDecodedAgeLabel"], 2, 0, 1, 1)
w4_data.addWidget(widgets["latestDecodedAgeData"], 2, 1, 1, 2)
d3_data.addWidget(w4_data)
w4_position = pg.LayoutWidget()
@ -507,30 +537,30 @@ if 'Windows' in platform.system():
else:
POSITION_LABEL_FONT_SIZE = 16
widgets["latestPacketCallsignLabel"] = QtGui.QLabel("<b>Callsign</b>")
widgets["latestPacketCallsignValue"] = QtGui.QLabel("---")
widgets["latestPacketCallsignValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Bold))
widgets["latestPacketTimeLabel"] = QtGui.QLabel("<b>Time</b>")
widgets["latestPacketTimeValue"] = QtGui.QLabel("---")
widgets["latestPacketTimeValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Bold))
widgets["latestPacketLatitudeLabel"] = QtGui.QLabel("<b>Latitude</b>")
widgets["latestPacketLatitudeValue"] = QtGui.QLabel("---")
widgets["latestPacketLatitudeValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Bold))
widgets["latestPacketLongitudeLabel"] = QtGui.QLabel("<b>Longitude</b>")
widgets["latestPacketLongitudeValue"] = QtGui.QLabel("---")
widgets["latestPacketLongitudeValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Bold))
widgets["latestPacketAltitudeLabel"] = QtGui.QLabel("<b>Altitude</b>")
widgets["latestPacketAltitudeValue"] = QtGui.QLabel("---")
widgets["latestPacketAltitudeValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Bold))
widgets["latestPacketBearingLabel"] = QtGui.QLabel("<b>Bearing</b>")
widgets["latestPacketBearingValue"] = QtGui.QLabel("---")
widgets["latestPacketBearingValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Bold))
widgets["latestPacketElevationLabel"] = QtGui.QLabel("<b>Elevation</b>")
widgets["latestPacketElevationValue"] = QtGui.QLabel("---")
widgets["latestPacketElevationValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Bold))
widgets["latestPacketRangeLabel"] = QtGui.QLabel("<b>Range (km)</b>")
widgets["latestPacketRangeValue"] = QtGui.QLabel("---")
widgets["latestPacketRangeValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Bold))
widgets["latestPacketCallsignLabel"] = QtWidgets.QLabel("<b>Callsign</b>")
widgets["latestPacketCallsignValue"] = QtWidgets.QLabel("---")
widgets["latestPacketCallsignValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Weight.Bold))
widgets["latestPacketTimeLabel"] = QtWidgets.QLabel("<b>Time</b>")
widgets["latestPacketTimeValue"] = QtWidgets.QLabel("---")
widgets["latestPacketTimeValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Weight.Bold))
widgets["latestPacketLatitudeLabel"] = QtWidgets.QLabel("<b>Latitude</b>")
widgets["latestPacketLatitudeValue"] = QtWidgets.QLabel("---")
widgets["latestPacketLatitudeValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Weight.Bold))
widgets["latestPacketLongitudeLabel"] = QtWidgets.QLabel("<b>Longitude</b>")
widgets["latestPacketLongitudeValue"] = QtWidgets.QLabel("---")
widgets["latestPacketLongitudeValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Weight.Bold))
widgets["latestPacketAltitudeLabel"] = QtWidgets.QLabel("<b>Altitude</b>")
widgets["latestPacketAltitudeValue"] = QtWidgets.QLabel("---")
widgets["latestPacketAltitudeValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Weight.Bold))
widgets["latestPacketBearingLabel"] = QtWidgets.QLabel("<b>Bearing</b>")
widgets["latestPacketBearingValue"] = QtWidgets.QLabel("---")
widgets["latestPacketBearingValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Weight.Bold))
widgets["latestPacketElevationLabel"] = QtWidgets.QLabel("<b>Elevation</b>")
widgets["latestPacketElevationValue"] = QtWidgets.QLabel("---")
widgets["latestPacketElevationValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Weight.Bold))
widgets["latestPacketRangeLabel"] = QtWidgets.QLabel("<b>Range (km)</b>")
widgets["latestPacketRangeValue"] = QtWidgets.QLabel("---")
widgets["latestPacketRangeValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Weight.Bold))
w4_position.addWidget(widgets["latestPacketCallsignLabel"], 0, 0, 1, 2)
w4_position.addWidget(widgets["latestPacketCallsignValue"], 1, 0, 1, 2)
@ -584,21 +614,74 @@ def update_modem_settings():
global widgets
populate_modem_settings(widgets)
widgets["horusModemSelector"].currentIndexChanged.connect(update_modem_settings)
def select_log_directory():
global widgets
folder = str(QtWidgets.QFileDialog.getExistingDirectory(None, "Select Directory"))
if folder is None:
logging.info("No log directory selected.")
return False
else:
if folder == "":
logging.info("No log directory selected.")
return False
else:
widgets["loggingPathEntry"].setText(folder)
widgets["enableLoggingSelector"].setChecked(False)
if telemetry_logger:
widgets["enableLoggingSelector"].setChecked(True)
telemetry_logger.update_log_directory(widgets["loggingPathEntry"].text())
telemetry_logger.enabled = True
return True
widgets["selectLogDirButton"].clicked.connect(select_log_directory)
def set_logging_state():
global widgets
logging_enabled = widgets["enableLoggingSelector"].isChecked()
if logging_enabled:
if widgets["loggingPathEntry"].text() == "":
# No logging directory set, prompt user to select one.
_success = select_log_directory()
if not _success:
# User didn't select a directory, set checkbox to false again.
logging.error("No log directory selected, logging disabled.")
widgets["enableLoggingSelector"].setChecked(False)
# Disable logging.
if telemetry_logger:
telemetry_logger.enabled = False
return
# Enable logging
if telemetry_logger:
telemetry_logger.enabled = True
telemetry_logger.update_log_directory(widgets["loggingPathEntry"].text())
else:
# Disable logging
if telemetry_logger:
telemetry_logger.enabled = False
widgets["enableLoggingSelector"].clicked.connect(set_logging_state)
def set_logging_format():
if telemetry_logger:
telemetry_logger.log_format = widgets["loggingFormatSelector"].currentText()
widgets["loggingFormatSelector"].currentIndexChanged.connect(set_logging_format)
# Read in configuration file settings
read_config(widgets)
# Start Habitat Uploader
habitat_uploader = HabitatUploader(
user_callsign=widgets["userCallEntry"].text(),
listener_lat=widgets["userLatEntry"].text(),
listener_lon=widgets["userLonEntry"].text(),
listener_radio="Horus-GUI v" + __version__ + " " + widgets["userRadioEntry"].text(),
listener_antenna=widgets["userAntennaEntry"].text(),
)
try:
if float(widgets["userLatEntry"].text()) == 0.0 and float(widgets["userLonEntry"].text()) == 0.0:
@ -618,20 +701,18 @@ sondehub_uploader = SondehubAmateurUploader(
software_version = __version__,
)
telemetry_logger = TelemetryLogger(
log_directory = widgets["loggingPathEntry"].text(),
log_format = widgets["loggingFormatSelector"].currentText(),
enabled = widgets["enableLoggingSelector"].isChecked()
)
# Handlers for various checkboxes and push-buttons
def habitat_position_reupload():
def habitat_position_reupload(dummy_arg, upload=True):
""" Trigger a re-upload of user position information """
global widgets, habitat_uploader, sondehub_uploader
global widgets, sondehub_uploader
habitat_uploader.user_callsign = widgets["userCallEntry"].text()
habitat_uploader.listener_lat = widgets["userLatEntry"].text()
habitat_uploader.listener_lon = widgets["userLonEntry"].text()
habitat_uploader.listener_radio = "Horus-GUI v" + __version__ + " " + widgets["userRadioEntry"].text()
habitat_uploader.listener_antenna = widgets["userAntennaEntry"].text()
habitat_uploader.trigger_position_upload()
# Do the same for Sondehub.
sondehub_uploader.user_callsign = widgets["userCallEntry"].text()
sondehub_uploader.user_radio = "Horus-GUI v" + __version__ + " " + widgets["userRadioEntry"].text()
sondehub_uploader.user_antenna = widgets["userAntennaEntry"].text()
@ -643,20 +724,26 @@ def habitat_position_reupload():
except:
sondehub_uploader.user_position = None
sondehub_uploader.last_user_position_upload = 0
if upload:
sondehub_uploader.last_user_position_upload = 0
logging.info("Triggered user position re-upload.")
widgets["habitatUploadPosition"].clicked.connect(habitat_position_reupload)
# Update uploader info as soon as it's edited, to ensure we upload with the latest user callsign
def update_uploader_details():
habitat_position_reupload(upload=False)
widgets["userCallEntry"].textEdited.connect(update_uploader_details)
def habitat_inhibit():
""" Update the Habitat inhibit flag """
global widgets, habitat_uploader, sondehub_uploader
habitat_uploader.inhibit = not widgets["habitatUploadSelector"].isChecked()
global widgets, sondehub_uploader
sondehub_uploader.inhibit = not widgets["sondehubUploadSelector"].isChecked()
logging.debug(f"Updated Habitat Inhibit state: {habitat_uploader.inhibit}")
logging.debug(f"Updated Sondebub Inhibit state: {sondehub_uploader.inhibit}")
widgets["habitatUploadSelector"].clicked.connect(habitat_inhibit)
widgets["sondehubUploadSelector"].clicked.connect(habitat_inhibit)
@ -790,11 +877,20 @@ def handle_status_update(status):
def get_latest_snr():
global widgets
# Assume 2 Hz stats updates, and take the peak of the last 4 seconds.
SNR_LEN = 2*4
_current_modem = widgets["horusModemSelector"].currentText()
_snr_update_rate = 2 # Hz
if "RTTY" in _current_modem:
# RTTY needs a much longer lookback period to find the peak SNR
# This is because of a very long buffer used in the RTTY demod
_snr_lookback = _snr_update_rate * 15
else:
# For Horus Binary we can use a smaller lookback time
_snr_lookback = _snr_update_rate * 4
if len(widgets["snrPlotSNR"])>SNR_LEN:
return np.max(widgets["snrPlotSNR"][-1*SNR_LEN:])
if len(widgets["snrPlotSNR"])>_snr_lookback:
return np.max(widgets["snrPlotSNR"][-1*_snr_lookback:])
else:
return np.max(widgets["snrPlotSNR"])
@ -823,6 +919,7 @@ def add_stats_update(frame):
def handle_new_packet(frame):
""" Handle receipt of a newly decoded packet """
global last_packet_time
if len(frame.data) > 0:
if type(frame.data) == bytes:
@ -843,15 +940,19 @@ def handle_new_packet(frame):
# Grab other metadata out of the GUI
try:
_radio_dial = float(widgets["dialFreqEntry"].text())*1e6
if widgets["fest_float"]:
# Add on the centre frequency estimation onto the dial frequency.
_radio_dial += widgets["fest_float"]
_radio_dial = None
habitat_uploader.last_freq_hz = _radio_dial
except:
_radio_dial = None
if widgets["dialFreqEntry"].text() != "":
try:
_radio_dial = float(widgets["dialFreqEntry"].text())*1e6
if widgets["fest_float"]:
# Add on the centre frequency estimation onto the dial frequency.
_radio_dial += widgets["fest_float"]
except:
logging.warning("Could not parse radio dial frequency. This must be in MMM.KKK format e.g. 437.600")
_radio_dial = None
_baud_rate = int(widgets["horusModemRateSelector"].currentText())
_modulation_detail = HORUS_MODEM_LIST[widgets["horusModemSelector"].currentText()]['modulation_detail']
@ -870,12 +971,12 @@ def handle_new_packet(frame):
# If we get here, the string is valid!
widgets["latestRawSentenceData"].setText(f"{_packet} ({_snr:.1f} dB SNR)")
widgets["latestDecodedSentenceData"].setText(f"{_packet}")
# Upload the string to Habitat
_decoded_str = "$$" + frame.data.split('$')[-1] + '\n'
habitat_uploader.add(_decoded_str)
last_packet_time = time.time()
# Upload the string to Sondehub Amateur
if widgets["userCallEntry"].text() == "N0CALL":
logging.warning("Uploader callsign is set as N0CALL. Please change this, otherwise telemetry data may be discarded!")
sondehub_uploader.add(_decoded)
except Exception as e:
@ -899,8 +1000,11 @@ def handle_new_packet(frame):
widgets["latestRawSentenceData"].setText(f"{_packet} ({_snr:.1f} dB SNR)")
widgets["latestDecodedSentenceData"].setText(_decoded['ukhas_str'])
habitat_uploader.add(_decoded['ukhas_str']+'\n')
last_packet_time = time.time()
# Upload the string to Sondehub Amateur
if widgets["userCallEntry"].text() == "N0CALL":
logging.warning("Uploader callsign is set as N0CALL. Please change this, otherwise telemetry data may be discarded!")
sondehub_uploader.add(_decoded)
except Exception as e:
if "CRC Failure" in str(e) and widgets["inhibitCRCSelector"].isChecked():
@ -934,7 +1038,7 @@ def handle_new_packet(frame):
widgets['latestPacketElevationValue'].setText(f"{_position_info['elevation']:.1f}")
widgets['latestPacketRangeValue'].setText(f"{_position_info['straight_distance']/1000.0:.1f}")
if rotator:
if rotator and not ( _decoded['latitude'] == 0.0 and _decoded['longitude'] == 0.0 ):
try:
rotator.set_azel(_position_info['bearing'], _position_info['elevation'], check_response=False)
widgets["rotatorCurrentPositionValue"].setText(f"{_position_info['bearing']:3.1f}˚, {_position_info['elevation']:2.1f}˚")
@ -962,6 +1066,12 @@ def handle_new_packet(frame):
_udp_port = int(widgets["ozimuxUDPEntry"].text())
send_ozimux_message(_decoded, port=_udp_port)
# Log telemetry
if telemetry_logger:
telemetry_logger.add(_decoded)
# Try and force a refresh of the displays.
QtWidgets.QApplication.processEvents()
@ -972,9 +1082,22 @@ def start_decoding():
Start decoding!
(Or, stop decoding)
"""
global widgets, audio_stream, fft_process, horus_modem, habitat_uploader, audio_devices, running, fft_update_queue, status_update_queue
global widgets, audio_stream, fft_process, horus_modem, audio_devices, running, fft_update_queue, status_update_queue, last_packet_time, args
if not running:
# Reset last packet time
if widgets["userCallEntry"].text() == "N0CALL":
# We don't allow the decoder to start if the callsign is still at the default.
_error_msgbox = QtWidgets.QMessageBox()
_error_msgbox.setWindowTitle("Uploader Callsign Invalid")
_error_msgbox.setText("Please change your SondeHub uploader callsign before starting!")
_error_msgbox.exec_()
return
last_packet_time = None
widgets['latestDecodedAgeData'].setText("No packet yet!")
# Grab settings off widgets
_dev_name = widgets["audioDeviceSelector"].currentText()
if _dev_name != 'UDP Audio (127.0.0.1:7355)':
@ -1010,8 +1133,7 @@ def start_decoding():
widgets["latestPacketBearingValue"].setText("---")
widgets["latestPacketRangeValue"].setText("---")
# Ensure the Habitat upload is set correctly.
habitat_uploader.inhibit = not widgets["habitatUploadSelector"].isChecked()
# Ensure the SondeHub upload is set correctly.
sondehub_uploader.inhibit = not widgets["sondehubUploadSelector"].isChecked()
# Init FFT Processor
@ -1026,7 +1148,12 @@ def start_decoding():
)
# Setup Modem
_libpath = ""
if args.libfix:
_libpath = "./"
horus_modem = HorusLib(
libpath=_libpath,
mode=_modem_id,
rate=_modem_rate,
tone_spacing=_modem_tone_spacing,
@ -1122,7 +1249,7 @@ def handle_log_update(log_update):
# GUI Update Loop
def processQueues():
""" Read in data from the queues, this decouples the GUI and async inputs somewhat. """
global fft_update_queue, status_update_queue, decoder_init, widgets, args
global fft_update_queue, status_update_queue, decoder_init, widgets, args, running, last_packet_time
while fft_update_queue.qsize() > 0:
_data = fft_update_queue.get()
@ -1139,8 +1266,16 @@ def processQueues():
handle_log_update(_log)
if running:
if last_packet_time != None:
_time_delta = int(time.time() - last_packet_time)
_time_delta_seconds = int(_time_delta%60)
_time_delta_minutes = int((_time_delta/60) % 60)
_time_delta_hours = int((_time_delta/3600))
widgets['latestDecodedAgeData'].setText(f"{_time_delta_hours:02d}:{_time_delta_minutes:02d}:{_time_delta_seconds:02d}")
# Try and force a re-draw.
QtGui.QApplication.processEvents()
QtWidgets.QApplication.processEvents()
if not decoder_init:
# Initialise decoders, and other libraries here.
@ -1232,7 +1367,7 @@ class ConsoleHandler(logging.Handler):
try:
self.log_queue.put_nowait(_text)
except:
print("Queue full!")
print("Console Log Queue full!")
@ -1244,11 +1379,12 @@ logging.getLogger().addHandler(console_handler)
logging.info("Started GUI.")
# Main
def main():
# Start the Qt Loop
if (sys.flags.interactive != 1) or not hasattr(QtCore, "PYQT_VERSION"):
QtGui.QApplication.instance().exec_()
QtWidgets.QApplication.instance().exec()
save_config(widgets)
try:
@ -1262,12 +1398,12 @@ def main():
pass
try:
habitat_uploader.close()
sondehub_uploader.close()
except:
pass
try:
sondehub_uploader.close()
telemetry_logger.close()
except:
pass

Wyświetl plik

@ -0,0 +1,184 @@
# Telemetry Logging
import csv
import datetime
import json
import logging
import os.path
import time
from threading import Thread
from queue import Queue
class TelemetryLogger(object):
"""
Telemetry Logger Class
Queued telemetry logging class
"""
def __init__(
self,
log_directory = None,
log_format = "CSV",
enabled = False
):
self.log_directory = log_directory
self.log_format = log_format
self.enabled = enabled
self.log_directory_updated = False
self.input_queue = Queue()
self.json_filenames = {}
self.csv_filenames = {}
self.processing_running = True
self.processing_thread = Thread(target=self.process_telemetry)
self.processing_thread.start()
def write_json(self, telemetry):
# Remove detailed packet format information if it exists.
if 'packet_format' in telemetry:
telemetry.pop('packet_format')
if telemetry['callsign'] not in self.json_filenames:
_filename = datetime.datetime.utcnow().strftime("%Y%m%d-%H%M%S") + f"_{telemetry['callsign']}.json"
_filepath = os.path.join(self.log_directory, _filename)
try:
_current_f = open(_filepath, 'a')
self.json_filenames[telemetry['callsign']] = _filepath
logging.info(f"Telemetry Logger - Opened new log file: {_filepath}")
except Exception as e:
logging.error(f"Telemetry Logger - Could not open log file in directory {self.log_directory}. Disabling logger.")
self.enabled = False
return
else:
# Open the file we already have started writing to.
try:
_current_f = open(self.json_filenames[telemetry['callsign']], 'a')
except Exception as e:
# Couldn't open log file. Remove filename from local list so we try and make a new file on next telemetry.
logging.error(f"Telemetry Logger - Could not open existing log file {self.json_filenames[telemetry['callsign']]}.")
self.json_filenames.pop(telemetry['callsign'])
return
# Convert telemetry to JSON
_data = json.dumps(telemetry) + "\n"
# Write to file.
_current_f.write(_data)
# Close file.
_current_f.close()
def write_csv(self, telemetry):
# Remove detailed packet format information if it exists.
if 'packet_format' in telemetry:
telemetry.pop('packet_format')
if 'ukhas_str' in telemetry:
telemetry.pop('ukhas_str')
if 'custom_field_names' in telemetry:
telemetry.pop('custom_field_names')
csv_keys = list(telemetry.keys())
csv_keys.sort()
csv_keys.remove("time")
csv_keys.remove("callsign")
csv_keys.remove("latitude")
csv_keys.remove("longitude")
csv_keys.remove("altitude")
csv_keys.insert(0,"time") # datetime should be at the front of the CSV
csv_keys.insert(1,"callsign")
csv_keys.insert(2,"latitude")
csv_keys.insert(3,"longitude")
csv_keys.insert(4,"altitude")
if telemetry['callsign'] not in self.csv_filenames:
_filename = datetime.datetime.utcnow().strftime("%Y%m%d-%H%M%S") + f"_{telemetry['callsign']}.csv"
_filepath = os.path.join(self.log_directory, _filename)
try:
_current_f = open(_filepath, 'a')
self.csv_filenames[telemetry['callsign']] = _filepath
logging.info(f"Telemetry Logger - Opened new log file: {_filepath}")
fc = csv.DictWriter(_current_f, fieldnames=csv_keys)
fc.writeheader()
except Exception as e:
logging.error(f"Telemetry Logger - Could not open log file in directory {self.log_directory}. Disabling logger.")
self.enabled = False
return
else:
# Open the file we already have started writing to.
try:
_current_f = open(self.csv_filenames[telemetry['callsign']], 'a')
except Exception as e:
# Couldn't open log file. Remove filename from local list so we try and make a new file on next telemetry.
logging.error(f"Telemetry Logger - Could not open existing log file {self.csv_filenames[telemetry['callsign']]}.")
self.csv_filenames.pop(telemetry['callsign'])
return
fc = csv.DictWriter(_current_f, fieldnames=csv_keys)
fc.writerows([telemetry])
# Close file.
_current_f.close()
def handle_telemetry(self, telemetry):
if self.log_directory.strip() == "" or self.log_directory is None:
return
if self.log_directory_updated:
# Log directory has been moved, clear out existing filenames.
self.json_filenames = {}
self.csv_filenames = {}
self.log_directory_updated = False
if self.log_format == "JSON":
self.write_json(telemetry)
elif self.log_format == "CSV":
self.write_csv(telemetry)
else:
logging.error(f"Telemetry Logger - Unknown Logging Format {self.log_format}")
def process_telemetry(self):
logging.debug("Started Telemetry Logger Thread")
while self.processing_running:
while self.input_queue.qsize() > 0:
try:
self.handle_telemetry(self.input_queue.get())
except Exception as e:
logging.error(f"Telemetry Logger - Error handling telemetry - {str(e)}")
time.sleep(1)
logging.debug("Closed Telemetry Logger Thread")
def add(self, telemetry):
if self.enabled:
try:
self.input_queue.put_nowait(telemetry)
except Exception as e:
logging.error("Telemetry Logger - Error adding sentence to queue: %s" % str(e))
def update_log_directory(self, directory):
""" Update the log directory in a hopefully clean manner """
self.log_directory = directory
self.log_directory_updated = True
def close(self):
self.processing_running = False

Wyświetl plik

@ -1,9 +1,9 @@
# Useful widgets
from pyqtgraph.Qt import QtCore, QtGui, QtWidgets
from PyQt5 import QtWidgets
# Useful class for adding horizontal lines.
class QHLine(QtGui.QFrame):
class QHLine(QtWidgets.QFrame):
def __init__(self):
super(QHLine, self).__init__()
self.setFrameShape(QtGui.QFrame.HLine)
self.setFrameShadow(QtGui.QFrame.Sunken)
self.setFrameShape(QtWidgets.QFrame.HLine)
self.setFrameShadow(QtWidgets.QFrame.Sunken)

Wyświetl plik

@ -1,6 +1,6 @@
[tool.poetry]
name = "horusgui"
version = "0.3.9"
version = "0.3.18"
description = ""
authors = ["Mark Jessop <vk5qi@rfhead.net>"]
@ -8,11 +8,10 @@ authors = ["Mark Jessop <vk5qi@rfhead.net>"]
python = "^3.6"
requests = "^2.24.0"
crcmod = "^1.7"
PyQt5 = "^5.13.0"
pyqtgraph = "^0.11.0"
PyQt5 = "^5.15.0"
pyqtgraph = "^0.12.3"
pyaudio = "^0.2.11"
"ruamel.yaml" = "^0.16.10"
horusdemodlib = "^0.3.6"
horusdemodlib = "^0.3.13"
[tool.poetry.dev-dependencies]

Wyświetl plik

@ -3,6 +3,5 @@ pyaudio
crcmod
PyQt5
pyqtgraph
ruamel.yaml
requests
horusdemodlib>=0.3.6
horusdemodlib>=0.3.12