kopia lustrzana https://github.com/projecthorus/horus-gui
Porównaj commity
36 Commity
Autor | SHA1 | Data |
---|---|---|
Mark Jessop | edb3ccfbd1 | |
Mark Jessop | 0ef8cd6fb1 | |
Mark Jessop | 655279e26b | |
Mark Jessop | 38df0377c8 | |
Mark Jessop | 7e8316e45e | |
Mark Jessop | 8d4918b460 | |
Mark Jessop | 5cd4fff720 | |
Mark Jessop | 24bdf69360 | |
Mark Jessop | 35d42cf99e | |
Mark Jessop | f343e2a6a1 | |
Mark Jessop | f1d86851c1 | |
Tom Wardill | 608f0897a3 | |
Tom Wardill | e92264923b | |
Tom Wardill | b364e842f0 | |
Tom Wardill | e8e0e210ad | |
Tom Wardill | c9a208e700 | |
Tom Wardill | 1c38f331bd | |
Tom Wardill | fd1886caff | |
Mark Jessop | 55188fcca1 | |
Mark Jessop | b0bb51cf2a | |
Mark Jessop | f5d9e87db8 | |
Mark Jessop | 356870b7b7 | |
Mark Jessop | 0bb9088917 | |
Mark Jessop | e9a7b31dfb | |
Mark Jessop | ab470cb7c5 | |
Mark Jessop | 27c98174a0 | |
Mark Jessop | c531572b2b | |
Mark Jessop | 6fb1e8bcba | |
Mark Jessop | dcb595036e | |
Mark Jessop | fbc4e73cc1 | |
Mark Jessop | afe2322509 | |
Mark Jessop | 387d91199f | |
Mark Jessop | 24497428cc | |
Mark Jessop | 5cc08d1e67 | |
Mark Jessop | 3015ee555d | |
Mark Jessop | a8223c5dd6 |
|
@ -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
|
34
README.md
34
README.md
|
@ -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.
|
||||
|
|
|
@ -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.'
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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.'
|
||||
},
|
||||
)
|
|
@ -1 +1 @@
|
|||
__version__ = "0.3.9"
|
||||
__version__ = "0.3.18"
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
510
horusgui/gui.py
510
horusgui/gui.py
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -3,6 +3,5 @@ pyaudio
|
|||
crcmod
|
||||
PyQt5
|
||||
pyqtgraph
|
||||
ruamel.yaml
|
||||
requests
|
||||
horusdemodlib>=0.3.6
|
||||
horusdemodlib>=0.3.12
|
||||
|
|
Ładowanie…
Reference in New Issue