diff --git a/README.md b/README.md index 27dbe88..3997966 100755 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ $ cd horusdemodlib && mkdir build && cd build $ cmake .. $ make $ sudo make install +$ sudo ldconfig ``` ### Grab this Repo diff --git a/horusgui/__init__.py b/horusgui/__init__.py index 08aad71..6a9beea 100755 --- a/horusgui/__init__.py +++ b/horusgui/__init__.py @@ -1 +1 @@ -__version__ = "0.3.19" +__version__ = "0.4.0" diff --git a/horusgui/audio.py b/horusgui/audio.py index 3aaf07b..043b489 100755 --- a/horusgui/audio.py +++ b/horusgui/audio.py @@ -1,6 +1,7 @@ # Audio Interfacing import logging import pyaudio +import time # Global PyAudio object @@ -125,8 +126,14 @@ def __init__(self, audio_device, fs, block_size=8192, fft_input=None, modem=None self.modem = modem self.stats_callback = stats_callback - # Start audio stream + self.audio_thread_running = True + self.audio = pyaudio.PyAudio() + + def start_stream(self, info_callback=None): + if info_callback: + self.stats_callback = info_callback + self.stream = self.audio.open( format=pyaudio.paInt16, channels=1, @@ -138,6 +145,11 @@ def __init__(self, audio_device, fs, block_size=8192, fft_input=None, modem=None stream_callback=self.handle_samples, ) + while self.audio_thread_running: + time.sleep(0.5) + + logging.debug("Stopped audio stream thread") + def handle_samples(self, data, frame_count, time_info="", status_flags=""): """ Handle incoming samples from pyaudio """ @@ -151,10 +163,11 @@ def handle_samples(self, data, frame_count, time_info="", status_flags=""): # Send any stats data back to the stats callback if _stats: if self.stats_callback: - self.stats_callback(_stats) + self.stats_callback.emit(_stats) return (None, pyaudio.paContinue) def stop(self): """ Halt stream """ - self.stream.close() + self.audio_thread_running = False + self.stream.close() \ No newline at end of file diff --git a/horusgui/config.py b/horusgui/config.py index 126ade6..7054be7 100644 --- a/horusgui/config.py +++ b/horusgui/config.py @@ -34,9 +34,11 @@ "rotator_type": "rotctld", "rotator_host": "localhost", "rotator_port": 4533, + "rotator_rangeinhibit": True, "logging_enabled": False, "log_format": "CSV", "log_directory": "", + "fft_smoothing": False, "payload_list": json.dumps(horusdemodlib.payloads.HORUS_PAYLOAD_LIST), "custom_field_list": json.dumps({}) } @@ -76,7 +78,7 @@ def read_config(widgets): global qt_settings, default_config # This is getting a bit ridiculous, need to re-think this approach. - OK_VERSIONS = [__version__, '0.3.18', '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'] + OK_VERSIONS = [__version__,'0.3.19', '0.3.18', '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: @@ -124,12 +126,15 @@ def read_config(widgets): widgets["rotatorTypeSelector"].setCurrentText(default_config["rotator_type"]) widgets["rotatorHostEntry"].setText(str(default_config["rotator_host"])) widgets["rotatorPortEntry"].setText(str(default_config["rotator_port"])) + widgets["rotatorRangeInhibit"].setChecked(ValueToBool(default_config["rotator_rangeinhibit"])) # 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"])) + widgets["fftSmoothingSelector"].setChecked(ValueToBool(default_config["fft_smoothing"])) + if default_config['baud_rate'] != -1: widgets["horusModemRateSelector"].setCurrentText(str(default_config['baud_rate'])) @@ -173,9 +178,11 @@ 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["rotator_rangeinhibit"] = widgets["rotatorRangeInhibit"].isChecked() default_config["logging_enabled"] = widgets["enableLoggingSelector"].isChecked() default_config["log_directory"] = widgets["loggingPathEntry"].text() default_config["log_format"] = widgets["loggingFormatSelector"].currentText() + default_config["fft_smoothing"] = widgets["fftSmoothingSelector"].isChecked() default_config["payload_list"] = json.dumps(horusdemodlib.payloads.HORUS_PAYLOAD_LIST) default_config["custom_field_list"] = json.dumps(horusdemodlib.payloads.HORUS_CUSTOM_FIELDS) diff --git a/horusgui/fft.py b/horusgui/fft.py index 319f1b4..cf0cb70 100644 --- a/horusgui/fft.py +++ b/horusgui/fft.py @@ -3,7 +3,7 @@ import time import numpy as np from queue import Queue -from threading import Thread +#from threading import Thread class FFTProcess(object): @@ -37,8 +37,8 @@ def __init__( self.processing_thread_running = True - self.t = Thread(target=self.processing_thread) - self.t.start() + #self.t = Thread(target=self.processing_thread) + #self.t.start() def init_window(self): """ Initialise Window functions and FFT scales. """ @@ -74,7 +74,7 @@ def perform_fft(self): if self.callback != None: if self.update_counter % self.update_decimation == 0: - self.callback({"fft": _fft[self.mask], "scale": self.fft_scale[self.mask], 'dbfs': _dbfs}) + self.callback.emit({"fft": _fft[self.mask], "scale": self.fft_scale[self.mask], 'dbfs': _dbfs}) self.update_counter += 1 @@ -86,7 +86,9 @@ def process_block(self, samples): while len(self.sample_buffer) > self.nfft * self.sample_width: self.perform_fft() - def processing_thread(self): + def processing_thread(self, info_callback=None): + if info_callback: + self.callback = info_callback while self.processing_thread_running: if self.input_queue.qsize() > 0: @@ -95,6 +97,8 @@ def processing_thread(self): else: time.sleep(0.01) + logging.debug("Stopped FFT processing thread") + def add_samples(self, samples): """ Add a block of samples to the input queue """ try: diff --git a/horusgui/gui.py b/horusgui/gui.py index fa5d1e0..7a2819b 100644 --- a/horusgui/gui.py +++ b/horusgui/gui.py @@ -15,17 +15,16 @@ import argparse import datetime -import glob +# 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 PyQt5 import QtWidgets, QtGui +from PyQt6.QtWidgets import * +from PyQt6.QtGui import * +from PyQt6.QtCore import * from pyqtgraph.dockarea import * -from threading import Thread from .widgets import * from .audio import * @@ -44,44 +43,6 @@ from horusdemodlib.sondehubamateur import * from . import __version__ - -# A few hardcoded defaults -DEFAULT_ESTIMATOR_MIN = 100 -DEFAULT_ESTIMATOR_MAX = 4000 - - -# Global widget store -widgets = {} - -# Queues for handling updates to image / status indications. -fft_update_queue = Queue(1024) -status_update_queue = Queue(1024) -log_update_queue = Queue(2048) - -# List of audio devices and their info -audio_devices = {} - -# Processor objects -audio_stream = None -fft_process = None -horus_modem = None -sondehub_uploader = None -telemetry_logger = None - -decoder_init = False - -last_packet_time = None - - -# Rotator object -rotator = None -rotator_current_az = 0.0 -rotator_current_el = 0.0 - - -# Global running indicator -running = False - # Read command-line arguments 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.") @@ -101,1344 +62,1581 @@ format="%(asctime)s %(levelname)s: %(message)s", level=_log_level ) -# -# GUI Creation - The Bad way. -# - -# Create a Qt App. -pg.mkQApp() - -# GUI LAYOUT - Gtk Style! -win = QtWidgets.QMainWindow() -area = DockArea() -win.setCentralWidget(area) -win.setWindowTitle(f"Horus Telemetry GUI - v{__version__}") -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("SondeHub", size=(300, 200)) -d0_other = Dock("Other", size=(300, 100)) -d0_rotator = Dock("Rotator", size=(300, 100)) -d1 = Dock("Spectrum", size=(800, 350)) -d2_stats = Dock("SNR (dB)", size=(50, 300)) -d2_snr = Dock("SNR Plot", size=(750, 300)) -d3_data = Dock("Data", size=(800, 50)) -d3_position = Dock("Position", size=(800, 50)) -d4 = Dock("Log", size=(800, 150)) -# Arrange docks. -area.addDock(d0) -area.addDock(d1, "right", d0) -area.addDock(d0_modem, "bottom", d0) -area.addDock(d0_habitat, "bottom", d0_modem) -area.addDock(d0_other, "below", d0_habitat) -area.addDock(d0_rotator, "below", d0_other) -area.addDock(d2_stats, "bottom", d1) -area.addDock(d3_data, "bottom", d2_stats) -area.addDock(d3_position, "bottom", d3_data) -area.addDock(d4, "bottom", d3_position) -area.addDock(d2_snr, "right", d2_stats) -d0_habitat.raiseDock() - - -# Controls -w1_audio = pg.LayoutWidget() -# TNC Connection -widgets["audioDeviceLabel"] = QtWidgets.QLabel("Audio Device:") -widgets["audioDeviceSelector"] = QtWidgets.QComboBox() - -widgets["audioSampleRateLabel"] = QtWidgets.QLabel("Sample Rate (Hz):") -widgets["audioSampleRateSelector"] = QtWidgets.QComboBox() - -widgets["audioDbfsLabel"] = QtWidgets.QLabel("Input Level (dBFS):") -widgets["audioDbfsValue"] = QtWidgets.QLabel("--") -widgets["audioDbfsValue_float"] = 0.0 - -w1_audio.addWidget(widgets["audioDeviceLabel"], 0, 0, 1, 1) -w1_audio.addWidget(widgets["audioDeviceSelector"], 0, 1, 1, 2) -w1_audio.addWidget(widgets["audioSampleRateLabel"], 1, 0, 1, 1) -w1_audio.addWidget(widgets["audioSampleRateSelector"], 1, 1, 1, 2) -w1_audio.addWidget(widgets["audioDbfsLabel"], 2, 0, 1, 1) -w1_audio.addWidget(widgets["audioDbfsValue"], 2, 1, 1, 2) -d0.addWidget(w1_audio) - -w1_modem = pg.LayoutWidget() - - -# Modem Parameters -widgets["horusModemLabel"] = QtWidgets.QLabel("Mode:") -widgets["horusModemSelector"] = QtWidgets.QComboBox() - -widgets["horusModemRateLabel"] = QtWidgets.QLabel("Baudrate:") -widgets["horusModemRateSelector"] = QtWidgets.QComboBox() - -widgets["horusMaskEstimatorLabel"] = QtWidgets.QLabel("Enable Mask Estim.:") -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"] = QtWidgets.QLabel("Tone Spacing (Hz):") -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"] = QtWidgets.QLabel("Manual Estim. Limits:") -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"\ - "range of interest, and help stop in-band CW interference from biasing the frequency\n"\ - "estimator. You can either click-and-drag the entire area, or click-and-drag the edges\n"\ - "to change the estimator frequency range." -) - -# Start/Stop -widgets["startDecodeButton"] = QtWidgets.QPushButton("Start") -widgets["startDecodeButton"].setEnabled(False) - -w1_modem.addWidget(widgets["horusModemLabel"], 0, 0, 1, 1) -w1_modem.addWidget(widgets["horusModemSelector"], 0, 1, 1, 1) -w1_modem.addWidget(widgets["horusModemRateLabel"], 1, 0, 1, 1) -w1_modem.addWidget(widgets["horusModemRateSelector"], 1, 1, 1, 1) -w1_modem.addWidget(widgets["horusMaskEstimatorLabel"], 2, 0, 1, 1) -w1_modem.addWidget(widgets["horusMaskEstimatorSelector"], 2, 1, 1, 1) -w1_modem.addWidget(widgets["horusMaskSpacingLabel"], 3, 0, 1, 1) -w1_modem.addWidget(widgets["horusMaskSpacingEntry"], 3, 1, 1, 1) -w1_modem.addWidget(widgets["horusManualEstimatorLabel"], 4, 0, 1, 1) -w1_modem.addWidget(widgets["horusManualEstimatorSelector"], 4, 1, 1, 1) -w1_modem.addWidget(widgets["startDecodeButton"], 5, 0, 2, 2) - -d0_modem.addWidget(w1_modem) - - -w1_habitat = pg.LayoutWidget() -# Listener Information -widgets["habitatHeading"] = QtWidgets.QLabel("SondeHub Settings") -widgets["sondehubUploadLabel"] = QtWidgets.QLabel("Enable SondeHub-Ham Upload:") -widgets["sondehubUploadSelector"] = QtWidgets.QCheckBox() -widgets["sondehubUploadSelector"].setChecked(True) -widgets["userCallLabel"] = QtWidgets.QLabel("Callsign:") -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, just something unique!" -) -widgets["userLocationLabel"] = QtWidgets.QLabel("Latitude / Longitude:") -widgets["userLatEntry"] = QtWidgets.QLineEdit("0.0") -widgets["userLatEntry"].setToolTip("Station Latitude in Decimal Degrees, e.g. -34.123456") -widgets["userLonEntry"] = QtWidgets.QLineEdit("0.0") -widgets["userLonEntry"].setToolTip("Station Longitude in Decimal Degrees, e.g. 138.123456") -widgets["userAltitudeLabel"] = QtWidgets.QLabel("Altitude:") -widgets["userAltEntry"] = QtWidgets.QLineEdit("0.0") -widgets["userAltEntry"].setToolTip("Station Altitude in Metres Above Sea Level.") -widgets["userAntennaLabel"] = QtWidgets.QLabel("Antenna:") -widgets["userAntennaEntry"] = QtWidgets.QLineEdit("") -widgets["userAntennaEntry"].setToolTip("A text description of your station's antenna.") -widgets["userRadioLabel"] = QtWidgets.QLabel("Radio:") -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\n"\ - "and the Horus-GUI software version." -) -widgets["habitatUploadPosition"] = QtWidgets.QPushButton("Re-upload Station Info") -widgets["habitatUploadPosition"].setToolTip( - "Manually re-upload your station information to SondeHub-Amateur.\n"\ -) -widgets["dialFreqLabel"] = QtWidgets.QLabel("Radio Dial Freq (MHz):") -widgets["dialFreqEntry"] = QtWidgets.QLineEdit("") -widgets["dialFreqEntry"].setToolTip( - "Optional entry of your radio's dial frequency in MHz (e.g. 437.600).\n"\ - "Used to provide frequency information on SondeHub-Amateur."\ -) -widgets["sondehubPositionNotesLabel"] = QtWidgets.QLabel("") - -widgets["saveSettingsButton"] = QtWidgets.QPushButton("Save Settings") - -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.addWidget(widgets["sondehubPositionNotesLabel"], 8, 0, 1, 3) -w1_habitat.layout.setRowStretch(9, 1) -w1_habitat.addWidget(widgets["saveSettingsButton"], 10, 0, 1, 3) - -d0_habitat.addWidget(w1_habitat) - -w1_other = pg.LayoutWidget() -widgets["horusHeaderLabel"] = QtWidgets.QLabel("Telemetry Forwarding") -widgets["horusUploadLabel"] = QtWidgets.QLabel("Enable Horus UDP Output:") -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"] = QtWidgets.QLabel("Horus UDP Port:") -widgets["horusUDPEntry"] = QtWidgets.QLineEdit("55672") -widgets["horusUDPEntry"].setMaxLength(5) -widgets["horusUDPEntry"].setToolTip( - "UDP Port to output 'Horus UDP' JSON messages to." -) -widgets["ozimuxUploadLabel"] = QtWidgets.QLabel("Enable OziMux UDP Output:") -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"] = QtWidgets.QLabel("Ozimux UDP Port:") -widgets["ozimuxUDPEntry"] = QtWidgets.QLineEdit("55683") -widgets["ozimuxUDPEntry"].setMaxLength(5) -widgets["ozimuxUDPEntry"].setToolTip( - "UDP Port to output 'OziMux' UDP messages to." -) -widgets["loggingHeaderLabel"] = QtWidgets.QLabel("Logging") -widgets["enableLoggingLabel"] = QtWidgets.QLabel("Enable Logging:") -widgets["enableLoggingSelector"] = QtWidgets.QCheckBox() -widgets["enableLoggingSelector"].setChecked(False) -widgets["enableLoggingSelector"].setToolTip( - "Enable logging of received telemetry to disk (JSON)" -) -widgets["loggingFormatLabel"] = QtWidgets.QLabel("Log Format:") -widgets["loggingFormatSelector"] = QtWidgets.QComboBox() -widgets["loggingFormatSelector"].addItem("CSV") -widgets["loggingFormatSelector"].addItem("JSON") -widgets["loggingPathLabel"] = QtWidgets.QLabel("Log Directory:") -widgets["loggingPathEntry"] = QtWidgets.QLineEdit("") -widgets["loggingPathEntry"].setToolTip( - "Logging Directory" -) -widgets["selectLogDirButton"] = QtWidgets.QPushButton("Select Directory") - -widgets["otherHeaderLabel"] = QtWidgets.QLabel("Other Settings") -widgets["inhibitCRCLabel"] = QtWidgets.QLabel("Hide Failed CRC Errors:") -widgets["inhibitCRCSelector"] = QtWidgets.QCheckBox() -widgets["inhibitCRCSelector"].setChecked(True) -widgets["inhibitCRCSelector"].setToolTip( - "Hide CRC Failed error messages." -) - -w1_other.addWidget(widgets["horusHeaderLabel"], 0, 0, 1, 2) -w1_other.addWidget(widgets["horusUploadLabel"], 1, 0, 1, 1) -w1_other.addWidget(widgets["horusUploadSelector"], 1, 1, 1, 1) -w1_other.addWidget(widgets["horusUDPLabel"], 2, 0, 1, 1) -w1_other.addWidget(widgets["horusUDPEntry"], 2, 1, 1, 1) -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["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"] = QtWidgets.QLabel("Rotator Control") - -widgets["rotatorTypeLabel"] = QtWidgets.QLabel("Rotator Type:") -widgets["rotatorTypeSelector"] = QtWidgets.QComboBox() -widgets["rotatorTypeSelector"].addItem("rotctld") -widgets["rotatorTypeSelector"].addItem("PSTRotator") - -widgets["rotatorHostLabel"] = QtWidgets.QLabel("Rotator Hostname:") -widgets["rotatorHostEntry"] = QtWidgets.QLineEdit("localhost") -widgets["rotatorHostEntry"].setToolTip( - "Hostname of the rotctld or PSTRotator Server.\n"\ -) - -widgets["rotatorPortLabel"] = QtWidgets.QLabel("Rotator TCP/UDP Port:") -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"] = QtWidgets.QLabel("Rotator Movement Threshold:") -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"] = QtWidgets.QPushButton("Start") - -widgets["rotatorCurrentStatusLabel"] = QtWidgets.QLabel("Status:") -widgets["rotatorCurrentStatusValue"] = QtWidgets.QLabel("Not Started.") - -widgets["rotatorCurrentPositionLabel"] = QtWidgets.QLabel("Commanded Az/El:") -widgets["rotatorCurrentPositionValue"] = QtWidgets.QLabel("---˚, --˚") - - - -w1_rotator.addWidget(widgets["rotatorHeaderLabel"], 0, 0, 1, 2) -w1_rotator.addWidget(widgets["rotatorTypeLabel"], 1, 0, 1, 1) -w1_rotator.addWidget(widgets["rotatorTypeSelector"], 1, 1, 1, 1) -w1_rotator.addWidget(widgets["rotatorHostLabel"], 2, 0, 1, 1) -w1_rotator.addWidget(widgets["rotatorHostEntry"], 2, 1, 1, 1) -w1_rotator.addWidget(widgets["rotatorPortLabel"], 3, 0, 1, 1) -w1_rotator.addWidget(widgets["rotatorPortEntry"], 3, 1, 1, 1) -#w1_rotator.addWidget(widgets["rotatorThresholdLabel"], 4, 0, 1, 1) -#w1_rotator.addWidget(widgets["rotatorThresholdEntry"], 4, 1, 1, 1) -w1_rotator.addWidget(widgets["rotatorConnectButton"], 4, 0, 1, 2) -w1_rotator.addWidget(widgets["rotatorCurrentStatusLabel"], 5, 0, 1, 1) -w1_rotator.addWidget(widgets["rotatorCurrentStatusValue"], 5, 1, 1, 1) -w1_rotator.addWidget(widgets["rotatorCurrentPositionLabel"], 6, 0, 1, 1) -w1_rotator.addWidget(widgets["rotatorCurrentPositionValue"], 6, 1, 1, 1) - -w1_rotator.layout.setRowStretch(7, 1) - -d0_rotator.addWidget(w1_rotator) - - -# Spectrum Display -widgets["spectrumPlot"] = pg.PlotWidget(title="Spectra") -widgets["spectrumPlot"].setLabel("left", "Power (dB)") -widgets["spectrumPlot"].setLabel("bottom", "Frequency (Hz)") -widgets["spectrumPlotData"] = widgets["spectrumPlot"].plot([0]) - -# Frequency Estiator Outputs -widgets["estimatorLines"] = [ - pg.InfiniteLine( - pos=-1000, - 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.PenStyle.DashLine), - label="F2", - labelOpts={'position':0.9} - ), - pg.InfiniteLine( - pos=-1000, - 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.PenStyle.DashLine), - label="F4", - labelOpts={'position':0.9} - ), -] -for _line in widgets["estimatorLines"]: - widgets["spectrumPlot"].addItem(_line) - -widgets["spectrumPlot"].setLabel("left", "Power (dBFs)") -widgets["spectrumPlot"].setLabel("bottom", "Frequency", units="Hz") -widgets["spectrumPlot"].setXRange(100, 4000) -widgets["spectrumPlot"].setYRange(-100, -20) -widgets["spectrumPlot"].setLimits(xMin=100, xMax=4000, yMin=-120, yMax=0) -widgets["spectrumPlot"].showGrid(True, True) - -widgets["estimatorRange"] = pg.LinearRegionItem([100,3000]) -widgets["estimatorRange"].setBounds([100,4000]) - -d1.addWidget(widgets["spectrumPlot"]) - -widgets["spectrumPlotRange"] = [-100, -20] - - -w3_stats = pg.LayoutWidget() -widgets["snrBar"] = QtWidgets.QProgressBar() -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.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) -w3_stats.layout.setColumnStretch(0, 2) -w3_stats.layout.setColumnStretch(2, 2) - -d2_stats.addWidget(w3_stats) - -# SNR Plot -w3_snr = pg.LayoutWidget() -widgets["snrPlot"] = pg.PlotWidget(title="SNR") -widgets["snrPlot"].setLabel("left", "SNR (dB)") -widgets["snrPlot"].setLabel("bottom", "Time (s)") -widgets["snrPlot"].setXRange(-60, 0) -widgets["snrPlot"].setYRange(-10, 30) -widgets["snrPlot"].setLimits(xMin=-60, xMax=0, yMin=-10, yMax=40) -widgets["snrPlot"].showGrid(True, True) -widgets["snrPlotRange"] = [-10, 30] -widgets["snrPlotTime"] = np.array([]) -widgets["snrPlotSNR"] = np.array([]) -widgets["snrPlotData"] = widgets["snrPlot"].plot(widgets["snrPlotTime"], widgets["snrPlotSNR"]) - -# TODO: Look into eye diagram more -# widgets["eyeDiagramPlot"] = pg.PlotWidget(title="Eye Diagram") -# widgets["eyeDiagramData"] = widgets["eyeDiagramPlot"].plot([0]) - -#w3_snr.addWidget(widgets["snrPlot"], 0, 1, 2, 1) - -#w3.addWidget(widgets["eyeDiagramPlot"], 0, 1) - -d2_snr.addWidget(widgets["snrPlot"]) - -# Telemetry Data -w4_data = pg.LayoutWidget() -widgets["latestRawSentenceLabel"] = QtWidgets.QLabel("Latest Packet (Raw):") -widgets["latestRawSentenceData"] = QtWidgets.QLineEdit("NO DATA") -widgets["latestRawSentenceData"].setReadOnly(True) -widgets["latestDecodedSentenceLabel"] = QtWidgets.QLabel("Latest Packet (Decoded):") -widgets["latestDecodedSentenceData"] = QtWidgets.QLineEdit("NO DATA") -widgets["latestDecodedSentenceData"].setReadOnly(True) -widgets["latestDecodedAgeLabel"] = QtWidgets.QLabel("Last Packet Age:") -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() # This font seems to look bigger in Windows... not sure why. if 'Windows' in platform.system(): POSITION_LABEL_FONT_SIZE = 14 else: POSITION_LABEL_FONT_SIZE = 16 -widgets["latestPacketCallsignLabel"] = QtWidgets.QLabel("Callsign") -widgets["latestPacketCallsignValue"] = QtWidgets.QLabel("---") -widgets["latestPacketCallsignValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Weight.Bold)) -widgets["latestPacketTimeLabel"] = QtWidgets.QLabel("Time") -widgets["latestPacketTimeValue"] = QtWidgets.QLabel("---") -widgets["latestPacketTimeValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Weight.Bold)) -widgets["latestPacketLatitudeLabel"] = QtWidgets.QLabel("Latitude") -widgets["latestPacketLatitudeValue"] = QtWidgets.QLabel("---") -widgets["latestPacketLatitudeValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Weight.Bold)) -widgets["latestPacketLongitudeLabel"] = QtWidgets.QLabel("Longitude") -widgets["latestPacketLongitudeValue"] = QtWidgets.QLabel("---") -widgets["latestPacketLongitudeValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Weight.Bold)) -widgets["latestPacketAltitudeLabel"] = QtWidgets.QLabel("Altitude") -widgets["latestPacketAltitudeValue"] = QtWidgets.QLabel("---") -widgets["latestPacketAltitudeValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Weight.Bold)) -widgets["latestPacketBearingLabel"] = QtWidgets.QLabel("Bearing") -widgets["latestPacketBearingValue"] = QtWidgets.QLabel("---") -widgets["latestPacketBearingValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Weight.Bold)) -widgets["latestPacketElevationLabel"] = QtWidgets.QLabel("Elevation") -widgets["latestPacketElevationValue"] = QtWidgets.QLabel("---") -widgets["latestPacketElevationValue"].setFont(QtGui.QFont("Courier New", POSITION_LABEL_FONT_SIZE, QtGui.QFont.Weight.Bold)) -widgets["latestPacketRangeLabel"] = QtWidgets.QLabel("Range (km)") -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) -w4_position.addWidget(widgets["latestPacketTimeLabel"], 0, 2, 1, 1) -w4_position.addWidget(widgets["latestPacketTimeValue"], 1, 2, 1, 1) -w4_position.addWidget(widgets["latestPacketLatitudeLabel"], 0, 3, 1, 1) -w4_position.addWidget(widgets["latestPacketLatitudeValue"], 1, 3, 1, 1) -w4_position.addWidget(widgets["latestPacketLongitudeLabel"], 0, 4, 1, 1) -w4_position.addWidget(widgets["latestPacketLongitudeValue"], 1, 4, 1, 1) -w4_position.addWidget(widgets["latestPacketAltitudeLabel"], 0, 5, 1, 1) -w4_position.addWidget(widgets["latestPacketAltitudeValue"], 1, 5, 1, 1) -w4_position.addWidget(widgets["latestPacketBearingLabel"], 0, 7, 1, 1) -w4_position.addWidget(widgets["latestPacketBearingValue"], 1, 7, 1, 1) -w4_position.addWidget(widgets["latestPacketElevationLabel"], 0, 8, 1, 1) -w4_position.addWidget(widgets["latestPacketElevationValue"], 1, 8, 1, 1) -w4_position.addWidget(widgets["latestPacketRangeLabel"], 0, 9, 1, 1) -w4_position.addWidget(widgets["latestPacketRangeValue"], 1, 9, 1, 1) -w4_position.layout.setRowStretch(1, 6) -d3_position.addWidget(w4_position) - -w5 = pg.LayoutWidget() -widgets["console"] = QtWidgets.QPlainTextEdit() -widgets["console"].setReadOnly(True) -w5.addWidget(widgets["console"]) -d4.addWidget(w5) - -# Resize window to final resolution, and display. -logging.info("Starting GUI.") -win.resize(1500, 800) -win.show() - -# Audio Initialization -audio_devices = init_audio(widgets) - - -def update_audio_sample_rates(): - """ Update the sample-rate dropdown when a different audio device is selected. """ - global widgets - # Pass widgets straight on to function from .audio - populate_sample_rates(widgets) - - -widgets["audioDeviceSelector"].currentIndexChanged.connect(update_audio_sample_rates) - -# Initialize modem list. -init_horus_modem(widgets) - - -def update_modem_settings(): - """ Update the modem setting widgets when a different modem is selected """ - 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 +PEN_WIDTH=1 -widgets["selectLogDirButton"].clicked.connect(select_log_directory) +# Establish signals and worker for multi-threaded use +class WorkerSignals(QObject): + # finished = pyqtSignal() + error = pyqtSignal(tuple) + # result = pyqtSignal(object) + info = pyqtSignal(object) +class Worker(QRunnable): + def __init__(self, fn, *args, **kwargs): + super(Worker, self).__init__() -def set_logging_state(): - global widgets + self.fn = fn + self.args = args + self.kwargs = kwargs + self.signals = WorkerSignals() - logging_enabled = widgets["enableLoggingSelector"].isChecked() + self.kwargs['info_callback'] = self.signals.info - 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 + @pyqtSlot() + def run(self): + try: + result = self.fn(*self.args, **self.kwargs) + except: + traceback.print_exc() + exctype, value = sys.exc_info()[:2] + self.signals.error.emit((exctype, value, traceback.format_exc())) + # else: + # self.signals.result.emit(result) + # finally: + # self.signals.finished.emit() + +def resource_path(relative_path): + try: + base_path = sys._MEIPASS + except Exception: + base_path = os.path.abspath(".") + + return os.path.join(base_path, relative_path) + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.resize(1500, 800) + + self.threadpool = QThreadPool() + self.stop_signal = False + + # A few hardcoded defaults + self.DEFAULT_ESTIMATOR_MIN = 100 + self.DEFAULT_ESTIMATOR_MAX = 4000 + + # Global widget store + self.widgets = {} + + # List of audio devices and their info + self.audio_devices = {} + + # Processor objects + self.audio_stream = None + self.fft_process = None + self.horus_modem = None + self.sondehub_uploader = None + self.telemetry_logger = None + + self.last_packet_time = None + + # Rotator object + self.rotator = None + self.rotator_current_az = 0.0 + self.rotator_current_el = 0.0 + + # Global running indicator + self.running = False + + # Decoded packet signal + self.new_packet_signal = WorkerSignals() + self.new_packet_signal.info.connect(self.handle_new_packet) + + self.initialize() + + def initialize(self): + # GUI LAYOUT - Gtk Style! + self.setWindowTitle(f"Horus Telemetry GUI - v{__version__}") + self.setWindowIcon(getHorusIcon()) + + # Left Column VBox + left_column = QVBoxLayout() + + # Controls + w1_audio_groupbox = QGroupBox('Audio') + w1_audio_groupbox.setObjectName("b1") + w1_audio_groupbox.setStyleSheet('QWidget#b1 { font-size: 15px; font-weight: bold}') + w1_audio = QGridLayout(w1_audio_groupbox) + + # Audio Parameters + self.widgets["audioDeviceLabel"] = QLabel("Audio Device:") + self.widgets["audioDeviceSelector"] = QComboBox() + # self.widgets["audioDeviceSelector"].setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContentsOnFirstShow) + # self.widgets["audioDeviceSelector"].setFixedWidth(275) # Dirty, but it needed to be done + self.widgets["audioDeviceSelector"].currentIndexChanged.connect(self.update_audio_sample_rates) + + self.widgets["audioSampleRateLabel"] = QLabel("Sample Rate (Hz):") + self.widgets["audioSampleRateSelector"] = QComboBox() + + self.widgets["audioDbfsLabel"] = QLabel("Input Level (dBFS):") + self.widgets["audioDbfsValue"] = QLabel("--") + self.widgets["audioDbfsValue_float"] = 0.0 + + w1_audio.addWidget(self.widgets["audioDeviceLabel"], 0, 0, 1, 3) + w1_audio.addWidget(self.widgets["audioDeviceSelector"], 1, 0, 1, 3) + w1_audio.addWidget(self.widgets["audioSampleRateLabel"], 2, 0, 1, 1) + w1_audio.addWidget(self.widgets["audioSampleRateSelector"], 2, 1, 1, 2) + w1_audio.addWidget(self.widgets["audioDbfsLabel"], 3, 0, 1, 1) + w1_audio.addWidget(self.widgets["audioDbfsValue"], 3, 1, 1, 2) + w1_audio_groupbox.setLayout(w1_audio) + + # Modem Parameters + w1_modem_groupbox = QGroupBox('Modem') + w1_modem_groupbox.setObjectName("b1") + w1_modem_groupbox.setStyleSheet('QWidget#b1 { font-size: 15px; font-weight: bold}') + w1_modem = QGridLayout(w1_modem_groupbox) + + self.widgets["horusModemLabel"] = QLabel("Mode:") + self.widgets["horusModemSelector"] = QComboBox() + self.widgets["horusModemSelector"].currentIndexChanged.connect(self.update_modem_settings) + + self.widgets["horusModemRateLabel"] = QLabel("Baudrate:") + self.widgets["horusModemRateSelector"] = QComboBox() + + self.widgets["horusMaskEstimatorLabel"] = QLabel("Enable Mask Estim.:") + self.widgets["horusMaskEstimatorSelector"] = QCheckBox() + self.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." + ) - return + self.widgets["horusMaskSpacingLabel"] = QLabel("Tone Spacing (Hz):") + self.widgets["horusMaskSpacingEntry"] = QLineEdit("270") + self.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." + ) + self.widgets["horusManualEstimatorLabel"] = QLabel("Manual Estim. Limits:") + self.widgets["horusManualEstimatorSelector"] = QCheckBox() + self.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"\ + "range of interest, and help stop in-band CW interference from biasing the frequency\n"\ + "estimator. You can either click-and-drag the entire area, or click-and-drag the edges\n"\ + "to change the estimator frequency range." + ) + self.widgets["horusManualEstimatorSelector"].clicked.connect(self.set_manual_estimator) + + # Start/Stop + self.widgets["startDecodeButton"] = QPushButton("Start") + self.widgets["startDecodeButton"].setEnabled(False) + self.widgets["startDecodeButton"].clicked.connect(self.start_decoding) + + w1_modem.addWidget(self.widgets["horusModemLabel"], 0, 0, 1, 1) + w1_modem.addWidget(self.widgets["horusModemSelector"], 0, 1, 1, 1) + w1_modem.addWidget(self.widgets["horusModemRateLabel"], 1, 0, 1, 1) + w1_modem.addWidget(self.widgets["horusModemRateSelector"], 1, 1, 1, 1) + w1_modem.addWidget(self.widgets["horusMaskEstimatorLabel"], 2, 0, 1, 1) + w1_modem.addWidget(self.widgets["horusMaskEstimatorSelector"], 2, 1, 1, 1) + w1_modem.addWidget(self.widgets["horusMaskSpacingLabel"], 3, 0, 1, 1) + w1_modem.addWidget(self.widgets["horusMaskSpacingEntry"], 3, 1, 1, 1) + w1_modem.addWidget(self.widgets["horusManualEstimatorLabel"], 4, 0, 1, 1) + w1_modem.addWidget(self.widgets["horusManualEstimatorSelector"], 4, 1, 1, 1) + w1_modem.addWidget(self.widgets["startDecodeButton"], 5, 0, 2, 2) + w1_modem_groupbox.setLayout(w1_modem) + + + w1_habitat_widget = QWidget() + w1_habitat = QGridLayout(w1_habitat_widget) + # Listener Information + self.widgets["habitatHeading"] = QLabel("SondeHub Settings") + self.widgets["sondehubUploadLabel"] = QLabel("Enable SondeHub Upload:") + self.widgets["sondehubUploadSelector"] = QCheckBox() + self.widgets["sondehubUploadSelector"].setChecked(True) + self.widgets["sondehubUploadSelector"].clicked.connect(self.habitat_inhibit) + self.widgets["userCallLabel"] = QLabel("Callsign:") + self.widgets["userCallEntry"] = QLineEdit("N0CALL") + self.widgets["userCallEntry"].setMaxLength(20) + self.widgets["userCallEntry"].setToolTip( + "Your station callsign, which doesn't necessarily need to be an\n"\ + "amateur radio callsign, just something unique!" + ) + self.widgets["userCallEntry"].textEdited.connect(self.update_uploader_details) + self.widgets["userLocationLabel"] = QLabel("Latitude / Longitude:") + self.widgets["userLatEntry"] = QLineEdit("0.0") + self.widgets["userLatEntry"].setToolTip("Station Latitude in Decimal Degrees, e.g. -34.123456") + self.widgets["userLatEntry"].textEdited.connect(self.update_uploader_details) + self.widgets["userLonEntry"] = QLineEdit("0.0") + self.widgets["userLonEntry"].setToolTip("Station Longitude in Decimal Degrees, e.g. 138.123456") + self.widgets["userLonEntry"].textEdited.connect(self.update_uploader_details) + self.widgets["userAltitudeLabel"] = QLabel("Altitude:") + self.widgets["userAltEntry"] = QLineEdit("0.0") + self.widgets["userAltEntry"].setToolTip("Station Altitude in Metres Above Sea Level.") + self.widgets["userAltEntry"].textEdited.connect(self.update_uploader_details) + self.widgets["userAntennaLabel"] = QLabel("Antenna:") + self.widgets["userAntennaEntry"] = QLineEdit("") + self.widgets["userAntennaEntry"].setToolTip("A text description of your station's antenna.") + self.widgets["userAntennaEntry"].textEdited.connect(self.update_uploader_details) + self.widgets["userRadioLabel"] = QLabel("Radio:") + self.widgets["userRadioEntry"] = QLineEdit("Horus-GUI " + __version__) + self.widgets["userRadioEntry"].setToolTip( + "A text description of your station's radio setup.\n"\ + "This field will be automatically prefixed with Horus-GUI\n"\ + "and the Horus-GUI software version." + ) + self.widgets["userRadioEntry"].textEdited.connect(self.update_uploader_details) + self.widgets["habitatUploadPosition"] = QPushButton("Re-upload Station Info") + self.widgets["habitatUploadPosition"].setToolTip( + "Manually re-upload your station information to SondeHub-Amateur.\n"\ + ) + # Connect the 'Re-upload Position' button to the above function. + self.widgets["habitatUploadPosition"].clicked.connect(self.habitat_position_reupload) + self.widgets["dialFreqLabel"] = QLabel("Radio Dial Freq (MHz):") + self.widgets["dialFreqEntry"] = QLineEdit("") + self.widgets["dialFreqEntry"].setToolTip( + "Optional entry of your radio's dial frequency in MHz (e.g. 437.600).\n"\ + "Used to provide frequency information on SondeHub-Amateur."\ + ) + self.widgets["sondehubPositionNotesLabel"] = QLabel("") + + self.widgets["saveSettingsButton"] = QPushButton("Save Settings") + self.widgets["saveSettingsButton"].clicked.connect(self.save_settings) + + w1_habitat.addWidget(self.widgets["sondehubUploadLabel"], 0, 0, 1, 1) + w1_habitat.addWidget(self.widgets["sondehubUploadSelector"], 0, 1, 1, 1) + w1_habitat.addWidget(self.widgets["userCallLabel"], 1, 0, 1, 1) + w1_habitat.addWidget(self.widgets["userCallEntry"], 1, 1, 1, 2) + w1_habitat.addWidget(self.widgets["userLocationLabel"], 2, 0, 1, 1) + w1_habitat.addWidget(self.widgets["userLatEntry"], 2, 1, 1, 1) + w1_habitat.addWidget(self.widgets["userLonEntry"], 2, 2, 1, 1) + w1_habitat.addWidget(self.widgets["userAltitudeLabel"], 3, 0, 1, 1) + w1_habitat.addWidget(self.widgets["userAltEntry"], 3, 1, 1, 2) + w1_habitat.addWidget(self.widgets["userAntennaLabel"], 4, 0, 1, 1) + w1_habitat.addWidget(self.widgets["userAntennaEntry"], 4, 1, 1, 2) + w1_habitat.addWidget(self.widgets["userRadioLabel"], 5, 0, 1, 1) + w1_habitat.addWidget(self.widgets["userRadioEntry"], 5, 1, 1, 2) + w1_habitat.addWidget(self.widgets["dialFreqLabel"], 6, 0, 1, 1) + w1_habitat.addWidget(self.widgets["dialFreqEntry"], 6, 1, 1, 2) + w1_habitat.addWidget(self.widgets["habitatUploadPosition"], 7, 0, 1, 3) + w1_habitat.addWidget(self.widgets["sondehubPositionNotesLabel"], 8, 0, 1, 3) + w1_habitat.setRowStretch(9, 1) + w1_habitat.addWidget(self.widgets["saveSettingsButton"], 10, 0, 1, 3) + w1_habitat_widget.setLayout(w1_habitat) + + w1_other_widget = QWidget() + w1_other = QGridLayout(w1_other_widget) + self.widgets["horusHeaderLabel"] = QLabel("Telemetry Forwarding") + self.widgets["horusUploadLabel"] = QLabel("Enable Horus UDP Output:") + self.widgets["horusUploadSelector"] = QCheckBox() + self.widgets["horusUploadSelector"].setChecked(True) + self.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"\ + ) + self.widgets["horusUDPLabel"] = QLabel("Horus UDP Port:") + self.widgets["horusUDPEntry"] = QLineEdit("55672") + self.widgets["horusUDPEntry"].setMaxLength(5) + self.widgets["horusUDPEntry"].setToolTip( + "UDP Port to output 'Horus UDP' JSON messages to." + ) + self.widgets["ozimuxUploadLabel"] = QLabel("Enable OziMux UDP Output:") + self.widgets["ozimuxUploadSelector"] = QCheckBox() + self.widgets["ozimuxUploadSelector"].setChecked(False) + self.widgets["ozimuxUploadSelector"].setToolTip( + "Output OziMux UDP messages. These are of the form:\n"\ + "'TELEMETRY,HH:MM:SS,lat,lon,alt\\n'" + ) + self.widgets["ozimuxUDPLabel"] = QLabel("Ozimux UDP Port:") + self.widgets["ozimuxUDPEntry"] = QLineEdit("55683") + self.widgets["ozimuxUDPEntry"].setMaxLength(5) + self.widgets["ozimuxUDPEntry"].setToolTip( + "UDP Port to output 'OziMux' UDP messages to." + ) + self.widgets["loggingHeaderLabel"] = QLabel("Logging") + self.widgets["enableLoggingLabel"] = QLabel("Enable Logging:") + self.widgets["enableLoggingSelector"] = QCheckBox() + self.widgets["enableLoggingSelector"].setChecked(False) + self.widgets["enableLoggingSelector"].setToolTip( + "Enable logging of received telemetry to disk (JSON)" + ) + self.widgets["enableLoggingSelector"].clicked.connect(self.set_logging_state) + self.widgets["loggingFormatLabel"] = QLabel("Log Format:") + self.widgets["loggingFormatSelector"] = QComboBox() + self.widgets["loggingFormatSelector"].addItem("CSV") + self.widgets["loggingFormatSelector"].addItem("JSON") + self.widgets["loggingFormatSelector"].currentIndexChanged.connect(self.set_logging_format) + self.widgets["loggingPathLabel"] = QLabel("Log Directory:") + self.widgets["loggingPathEntry"] = QLineEdit("") + self.widgets["loggingPathEntry"].setToolTip( + "Logging Directory" + ) + self.widgets["selectLogDirButton"] = QPushButton("Select Directory") + self.widgets["selectLogDirButton"].clicked.connect(self.select_log_directory) + + self.widgets["otherHeaderLabel"] = QLabel("Other Settings") + self.widgets["inhibitCRCLabel"] = QLabel("Hide Failed CRC Errors:") + self.widgets["inhibitCRCSelector"] = QCheckBox() + self.widgets["inhibitCRCSelector"].setChecked(True) + self.widgets["inhibitCRCSelector"].setToolTip( + "Hide CRC Failed error messages." + ) + self.widgets["fftSmoothingLabel"] = QLabel("Enable FFT smoothing:") + self.widgets["fftSmoothingSelector"] = QCheckBox() + self.widgets["fftSmoothingSelector"].setChecked(False) + self.widgets["fftSmoothingSelector"].setToolTip( + "Enable IIR filter on FFT with tc=0.25." + ) - # Enable logging - if telemetry_logger: - telemetry_logger.enabled = True - telemetry_logger.update_log_directory(widgets["loggingPathEntry"].text()) + w1_other.addWidget(self.widgets["horusHeaderLabel"], 0, 0, 1, 2) + w1_other.addWidget(self.widgets["horusUploadLabel"], 1, 0, 1, 1) + w1_other.addWidget(self.widgets["horusUploadSelector"], 1, 1, 1, 1) + w1_other.addWidget(self.widgets["horusUDPLabel"], 2, 0, 1, 1) + w1_other.addWidget(self.widgets["horusUDPEntry"], 2, 1, 1, 1) + w1_other.addWidget(self.widgets["ozimuxUploadLabel"], 3, 0, 1, 1) + w1_other.addWidget(self.widgets["ozimuxUploadSelector"], 3, 1, 1, 1) + w1_other.addWidget(self.widgets["ozimuxUDPLabel"], 4, 0, 1, 1) + w1_other.addWidget(self.widgets["ozimuxUDPEntry"], 4, 1, 1, 1) + w1_other.addWidget(self.widgets["loggingHeaderLabel"], 5, 0, 1, 2) + w1_other.addWidget(self.widgets["enableLoggingLabel"], 6, 0, 1, 1) + w1_other.addWidget(self.widgets["enableLoggingSelector"], 6, 1, 1, 1) + w1_other.addWidget(self.widgets["loggingFormatLabel"], 7, 0, 1, 1) + w1_other.addWidget(self.widgets["loggingFormatSelector"], 7, 1, 1, 1) + w1_other.addWidget(self.widgets["loggingPathLabel"], 8, 0, 1, 1) + w1_other.addWidget(self.widgets["loggingPathEntry"], 8, 1, 1, 1) + w1_other.addWidget(self.widgets["selectLogDirButton"], 9, 0, 1, 2) + w1_other.addWidget(self.widgets["otherHeaderLabel"], 10, 0, 1, 2) + w1_other.addWidget(self.widgets["inhibitCRCLabel"], 11, 0, 1, 1) + w1_other.addWidget(self.widgets["inhibitCRCSelector"], 11, 1, 1, 1) + w1_other.addWidget(self.widgets["fftSmoothingLabel"], 12, 0, 1, 1) + w1_other.addWidget(self.widgets["fftSmoothingSelector"], 12, 1, 1, 1) + w1_other.setRowStretch(13, 1) + w1_other_widget.setLayout(w1_other) + + + w1_rotator_widget = QWidget() + w1_rotator = QGridLayout(w1_rotator_widget) + self.widgets["rotatorHeaderLabel"] = QLabel("Rotator Control") + + self.widgets["rotatorTypeLabel"] = QLabel("Rotator Type:") + self.widgets["rotatorTypeSelector"] = QComboBox() + self.widgets["rotatorTypeSelector"].addItem("rotctld") + self.widgets["rotatorTypeSelector"].addItem("PSTRotator") + + self.widgets["rotatorHostLabel"] = QLabel("Rotator Hostname:") + self.widgets["rotatorHostEntry"] = QLineEdit("localhost") + self.widgets["rotatorHostEntry"].setToolTip( + "Hostname of the rotctld or PSTRotator Server.\n"\ + ) - else: - # Disable logging - if telemetry_logger: - telemetry_logger.enabled = False + self.widgets["rotatorPortLabel"] = QLabel("Rotator TCP/UDP Port:") + self.widgets["rotatorPortEntry"] = QLineEdit("4533") + self.widgets["rotatorPortEntry"].setMaxLength(5) + self.widgets["rotatorPortEntry"].setToolTip( + "TCP (rotctld) or UDP (PSTRotator) port to connect to.\n"\ + "Default for rotctld: 4533\n"\ + "Default for PSTRotator: 12000" + ) + self.widgets["rotatorRangeInhibitLabel"] = QLabel("Inhibit Local Movement:") + self.widgets["rotatorRangeInhibit"] = QCheckBox() + self.widgets["rotatorRangeInhibit"].setChecked(True) + self.widgets["rotatorRangeInhibit"].setToolTip( + "Inhibit Horus GUI from sending rotator position updates\n"\ + "if range is less than 250 meters. This is useful if testing\n"\ + "transmitter in close vicinity of receiver." + ) + self.widgets["rotatorThresholdLabel"] = QLabel("Rotator Movement Threshold:") + self.widgets["rotatorThresholdEntry"] = QLineEdit("2.5") + self.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["enableLoggingSelector"].clicked.connect(set_logging_state) + self.widgets["rotatorConnectButton"] = QPushButton("Start") + self.widgets["rotatorConnectButton"].clicked.connect(self.startstop_rotator) + + self.widgets["rotatorCurrentStatusLabel"] = QLabel("Status:") + self.widgets["rotatorCurrentStatusValue"] = QLabel("Not Started.") + + self.widgets["rotatorCurrentPositionLabel"] = QLabel("Commanded Az/El:") + self.widgets["rotatorCurrentPositionValue"] = QLabel("---˚, --˚") + + + + w1_rotator.addWidget(self.widgets["rotatorHeaderLabel"], 0, 0, 1, 2) + w1_rotator.addWidget(self.widgets["rotatorTypeLabel"], 1, 0, 1, 1) + w1_rotator.addWidget(self.widgets["rotatorTypeSelector"], 1, 1, 1, 1) + w1_rotator.addWidget(self.widgets["rotatorHostLabel"], 2, 0, 1, 1) + w1_rotator.addWidget(self.widgets["rotatorHostEntry"], 2, 1, 1, 1) + w1_rotator.addWidget(self.widgets["rotatorPortLabel"], 3, 0, 1, 1) + w1_rotator.addWidget(self.widgets["rotatorPortEntry"], 3, 1, 1, 1) + #w1_rotator.addWidget(self.widgets["rotatorThresholdLabel"], 4, 0, 1, 1) + #w1_rotator.addWidget(self.widgets["rotatorThresholdEntry"], 4, 1, 1, 1) + w1_rotator.addWidget(self.widgets["rotatorRangeInhibitLabel"], 5, 0, 1, 1) + w1_rotator.addWidget(self.widgets["rotatorRangeInhibit"], 5, 1, 1, 1) + w1_rotator.addWidget(self.widgets["rotatorConnectButton"], 6, 0, 1, 2) + w1_rotator.addWidget(self.widgets["rotatorCurrentStatusLabel"], 7, 0, 1, 1) + w1_rotator.addWidget(self.widgets["rotatorCurrentStatusValue"], 7, 1, 1, 1) + w1_rotator.addWidget(self.widgets["rotatorCurrentPositionLabel"], 8, 0, 1, 1) + w1_rotator.addWidget(self.widgets["rotatorCurrentPositionValue"], 8, 1, 1, 1) + w1_rotator.setRowStretch(9, 1) + + w1_rotator_widget.setLayout(w1_rotator) + + w1_tab_widget = QTabWidget() + w1_tab_widget.setTabPosition(QTabWidget.TabPosition.North) + w1_tab_widget.tabBar().setExpanding(True) + w1_tab_widget.addTab(w1_habitat_widget, "SondeHub") + w1_tab_widget.addTab(w1_other_widget, "Other") + w1_tab_widget.addTab(w1_rotator_widget, "Rotator") + w1_tab_widget.setStyleSheet("QTabBar {font: bold 14px;}") + + # Add widgets to left column + left_column.addWidget(w1_audio_groupbox) #, 0, 0, 1, 1) + left_column.addWidget(w1_modem_groupbox) #, 1, 0, 1, 1) + left_column.addWidget(w1_tab_widget) #, 2, 0, 1, 1) + + # Right Column QGrid (Grid for merged cells) + right_column = QGridLayout() + + # Spectrum Display + self.widgets["spectrumPlot"] = pg.PlotWidget(title="Spectra") + self.widgets["spectrumPlot"].setLabel("left", "Power (dB)") + self.widgets["spectrumPlot"].setLabel("bottom", "Frequency (Hz)") + self.widgets["spectrumPlotData"] = self.widgets["spectrumPlot"].plot([0], pen=pg.mkPen(width=PEN_WIDTH)) + + # Frequency Estiator Outputs + self.widgets["estimatorLines"] = [ + pg.InfiniteLine( + pos=-1000, + pen=pg.mkPen(color="grey", width=(PEN_WIDTH + 1), style=QtCore.Qt.PenStyle.DashLine), + label="F1", + labelOpts={'position':0.9} + ), + pg.InfiniteLine( + pos=-1000, + pen=pg.mkPen(color="grey", width=(PEN_WIDTH + 1), style=QtCore.Qt.PenStyle.DashLine), + label="F2", + labelOpts={'position':0.9} + ), + pg.InfiniteLine( + pos=-1000, + pen=pg.mkPen(color="grey", width=(PEN_WIDTH + 1), style=QtCore.Qt.PenStyle.DashLine), + label="F3", + labelOpts={'position':0.9} + ), + pg.InfiniteLine( + pos=-1000, + pen=pg.mkPen(color="grey", width=(PEN_WIDTH + 1), style=QtCore.Qt.PenStyle.DashLine), + label="F4", + labelOpts={'position':0.9} + ), + ] + for _line in self.widgets["estimatorLines"]: + self.widgets["spectrumPlot"].addItem(_line) + + self.widgets["spectrumPlot"].setLabel("left", "Power (dBFs)") + self.widgets["spectrumPlot"].setLabel("bottom", "Frequency", units="Hz") + self.widgets["spectrumPlot"].setXRange(100, 4000) + self.widgets["spectrumPlot"].setYRange(-100, -20) + self.widgets["spectrumPlot"].setLimits(xMin=100, xMax=4000, yMin=-120, yMax=0) + self.widgets["spectrumPlot"].showGrid(True, True) + + self.widgets["estimatorRange"] = pg.LinearRegionItem([100,3000]) + self.widgets["estimatorRange"].setBounds([100,4000]) + self.widgets["estimatorRange"].sigRegionChangeFinished.connect(self.update_manual_estimator) + + w2_spectrum_groupbox = QGroupBox("Spectrum") + w2_spectrum_groupbox.setObjectName("b1") + w2_spectrum_groupbox.setStyleSheet('QWidget#b1 { font-size: 15px; font-weight: bold}') + spectrum = QGridLayout(w2_spectrum_groupbox) + spectrum.addWidget(self.widgets["spectrumPlot"]) + + self.widgets["spectrumPlotRange"] = [-100, -20] + + w3_stats_groupbox = QGroupBox("SNR (dB)") + w3_stats_groupbox.setObjectName("b1") + w3_stats_groupbox.setStyleSheet('QWidget#b1 { font-size: 15px; font-weight: bold}') + w3_stats = QGridLayout(w3_stats_groupbox) + self.widgets["snrBar"] = QProgressBar() + self.widgets["snrBar"].setOrientation(QtCore.Qt.Orientation.Vertical) + self.widgets["snrBar"].setRange(-10, 15) + self.widgets["snrBar"].setValue(-10) + self.widgets["snrBar"].setTextVisible(False) + self.widgets["snrBar"].setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.widgets["snrLabel"] = QLabel("--.-") + self.widgets["snrLabel"].setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter); + self.widgets["snrLabel"].setFont(QFont("Courier New", 14)) + w3_stats.addWidget(self.widgets["snrBar"], 0, 1, 1, 1) + w3_stats.addWidget(self.widgets["snrLabel"], 1, 0, 1, 3) + w3_stats.setColumnStretch(0, 2) + w3_stats.setColumnStretch(2, 2) + + w3_stats_groupbox.setLayout(w3_stats) + + + # SNR Plot + w3_snr_groupbox = QGroupBox("SNR Plot") + w3_snr_groupbox.setObjectName("b1") + w3_snr_groupbox.setStyleSheet('QWidget#b1 { font-size: 15px; font-weight: bold}') + w3_snr = QGridLayout(w3_snr_groupbox) + self.widgets["snrPlot"] = pg.PlotWidget(title="SNR") + self.widgets["snrPlot"].setLabel("left", "SNR (dB)") + self.widgets["snrPlot"].setLabel("bottom", "Time (s)") + self.widgets["snrPlot"].setXRange(-60, 0) + self.widgets["snrPlot"].setYRange(-10, 30) + self.widgets["snrPlot"].setLimits(xMin=-60, xMax=0, yMin=-10, yMax=40) + self.widgets["snrPlot"].showGrid(True, True) + self.widgets["snrPlotRange"] = [-10, 30] + self.widgets["snrPlotTime"] = np.array([]) + self.widgets["snrPlotSNR"] = np.array([]) + self.widgets["snrPlotData"] = self.widgets["snrPlot"].plot(self.widgets["snrPlotTime"], self.widgets["snrPlotSNR"], pen=pg.mkPen(width=PEN_WIDTH)) + w3_snr.addWidget(self.widgets["snrPlot"]) + + w3_snr_groupbox.setLayout(w3_snr) + + # TODO: Look into eye diagram more + # self.widgets["eyeDiagramPlot"] = pg.PlotWidget(title="Eye Diagram") + # self.widgets["eyeDiagramData"] = self.widgets["eyeDiagramPlot"].plot([0]) + + #w3_snr.addWidget(self.widgets["snrPlot"], 0, 1, 2, 1) + + #w3.addWidget(self.widgets["eyeDiagramPlot"], 0, 1) + + # Telemetry Data + w4_data_groupbox = QGroupBox("Data") + w4_data_groupbox.setObjectName("b1") + w4_data_groupbox.setStyleSheet('QWidget#b1 { font-size: 15px; font-weight: bold}') + w4_data = QGridLayout(w4_data_groupbox) + self.widgets["latestRawSentenceLabel"] = QLabel("Latest Packet (Raw):") + self.widgets["latestRawSentenceData"] = QLineEdit("NO DATA") + self.widgets["latestRawSentenceData"].setReadOnly(True) + self.widgets["latestDecodedSentenceLabel"] = QLabel("Latest Packet (Decoded):") + self.widgets["latestDecodedSentenceData"] = QLineEdit("NO DATA") + self.widgets["latestDecodedSentenceData"].setReadOnly(True) + self.widgets["latestDecodedAgeLabel"] = QLabel("Last Packet Age:") + self.widgets["latestDecodedAgeData"] = QLabel("No packet yet!") + w4_data.addWidget(self.widgets["latestRawSentenceLabel"], 0, 0, 1, 1) + w4_data.addWidget(self.widgets["latestRawSentenceData"], 0, 1, 1, 6) + w4_data.addWidget(self.widgets["latestDecodedSentenceLabel"], 1, 0, 1, 1) + w4_data.addWidget(self.widgets["latestDecodedSentenceData"], 1, 1, 1, 6) + w4_data.addWidget(self.widgets["latestDecodedAgeLabel"], 2, 0, 1, 1) + w4_data.addWidget(self.widgets["latestDecodedAgeData"], 2, 1, 1, 2) + + w4_data_groupbox.setLayout(w4_data) + + w4_position_groupbox = QGroupBox("Position") + w4_position_groupbox.setObjectName("b1") + w4_position_groupbox.setStyleSheet('QWidget#b1 { font-size: 15px; font-weight: bold}') + w4_position = QGridLayout(w4_position_groupbox) + + self.widgets["latestPacketCallsignLabel"] = QLabel("Callsign") + self.widgets["latestPacketCallsignValue"] = QLabel("---") + self.widgets["latestPacketCallsignValue"].setFont(QFont("Courier New", POSITION_LABEL_FONT_SIZE, QFont.Weight.Bold)) + self.widgets["latestPacketTimeLabel"] = QLabel("Time") + self.widgets["latestPacketTimeValue"] = QLabel("---") + self.widgets["latestPacketTimeValue"].setFont(QFont("Courier New", POSITION_LABEL_FONT_SIZE, QFont.Weight.Bold)) + self.widgets["latestPacketLatitudeLabel"] = QLabel("Latitude") + self.widgets["latestPacketLatitudeValue"] = QLabel("---") + self.widgets["latestPacketLatitudeValue"].setFont(QFont("Courier New", POSITION_LABEL_FONT_SIZE, QFont.Weight.Bold)) + self.widgets["latestPacketLongitudeLabel"] = QLabel("Longitude") + self.widgets["latestPacketLongitudeValue"] = QLabel("---") + self.widgets["latestPacketLongitudeValue"].setFont(QFont("Courier New", POSITION_LABEL_FONT_SIZE, QFont.Weight.Bold)) + self.widgets["latestPacketAltitudeLabel"] = QLabel("Altitude") + self.widgets["latestPacketAltitudeValue"] = QLabel("---") + self.widgets["latestPacketAltitudeValue"].setFont(QFont("Courier New", POSITION_LABEL_FONT_SIZE, QFont.Weight.Bold)) + self.widgets["latestPacketBearingLabel"] = QLabel("Bearing") + self.widgets["latestPacketBearingValue"] = QLabel("---") + self.widgets["latestPacketBearingValue"].setFont(QFont("Courier New", POSITION_LABEL_FONT_SIZE, QFont.Weight.Bold)) + self.widgets["latestPacketElevationLabel"] = QLabel("Elevation") + self.widgets["latestPacketElevationValue"] = QLabel("---") + self.widgets["latestPacketElevationValue"].setFont(QFont("Courier New", POSITION_LABEL_FONT_SIZE, QFont.Weight.Bold)) + self.widgets["latestPacketRangeLabel"] = QLabel("Range (km)") + self.widgets["latestPacketRangeValue"] = QLabel("---") + self.widgets["latestPacketRangeValue"].setFont(QFont("Courier New", POSITION_LABEL_FONT_SIZE, QFont.Weight.Bold)) + + w4_position.addWidget(self.widgets["latestPacketCallsignLabel"], 0, 0, 1, 2) + w4_position.addWidget(self.widgets["latestPacketCallsignValue"], 1, 0, 1, 2) + w4_position.addWidget(self.widgets["latestPacketTimeLabel"], 0, 2, 1, 1) + w4_position.addWidget(self.widgets["latestPacketTimeValue"], 1, 2, 1, 1) + w4_position.addWidget(self.widgets["latestPacketLatitudeLabel"], 0, 3, 1, 1) + w4_position.addWidget(self.widgets["latestPacketLatitudeValue"], 1, 3, 1, 1) + w4_position.addWidget(self.widgets["latestPacketLongitudeLabel"], 0, 4, 1, 1) + w4_position.addWidget(self.widgets["latestPacketLongitudeValue"], 1, 4, 1, 1) + w4_position.addWidget(self.widgets["latestPacketAltitudeLabel"], 0, 5, 1, 1) + w4_position.addWidget(self.widgets["latestPacketAltitudeValue"], 1, 5, 1, 1) + w4_position.addWidget(self.widgets["latestPacketBearingLabel"], 0, 7, 1, 1) + w4_position.addWidget(self.widgets["latestPacketBearingValue"], 1, 7, 1, 1) + w4_position.addWidget(self.widgets["latestPacketElevationLabel"], 0, 8, 1, 1) + w4_position.addWidget(self.widgets["latestPacketElevationValue"], 1, 8, 1, 1) + w4_position.addWidget(self.widgets["latestPacketRangeLabel"], 0, 9, 1, 1) + w4_position.addWidget(self.widgets["latestPacketRangeValue"], 1, 9, 1, 1) + #w4_position.setRowStretch(1, 6) + + w4_position_groupbox.setLayout(w4_position) + + w5_telemetry_groupbox = QGroupBox("Telemetry") + w5_telemetry_groupbox.setObjectName("b1") + w5_telemetry_groupbox.setStyleSheet('QWidget#b1 { font-size: 15px; font-weight: bold}') + self.w5_telemetry = QGridLayout(w5_telemetry_groupbox) + w5_telemetry_groupbox.setLayout(self.w5_telemetry) + + # These are placeholders and will be updated when telemetry is received. + self.widgets["latestTelemBattVoltageLabel"] = QLabel("Batt Voltage") + self.widgets["latestTelemBattVoltageValue"] = QLabel("---") + self.widgets["latestTelemBattVoltageValue"].setFont(QFont("Courier New", POSITION_LABEL_FONT_SIZE, QFont.Weight.Bold)) + self.widgets["latestTelemSatellitesLabel"] = QLabel("Satellites") + self.widgets["latestTelemSatellitesValue"] = QLabel("---") + self.widgets["latestTelemSatellitesValue"].setFont(QFont("Courier New", POSITION_LABEL_FONT_SIZE, QFont.Weight.Bold)) + self.widgets["latestTelemTemperatureLabel"] = QLabel("Temperature") + self.widgets["latestTelemTemperatureValue"] = QLabel("---") + self.widgets["latestTelemTemperatureValue"].setFont(QFont("Courier New", POSITION_LABEL_FONT_SIZE, QFont.Weight.Bold)) + + self.w5_telemetry.addWidget(self.widgets[f"latestTelemBattVoltageLabel"], 0, 0, 1, 1) + self.w5_telemetry.addWidget(self.widgets[f"latestTelemBattVoltageValue"], 1, 0, 1, 1) + self.w5_telemetry.addWidget(self.widgets[f"latestTelemSatellitesLabel"], 0, 1, 1, 1) + self.w5_telemetry.addWidget(self.widgets[f"latestTelemSatellitesValue"], 1, 1, 1, 1) + self.w5_telemetry.addWidget(self.widgets[f"latestTelemTemperatureLabel"], 0, 2, 1, 1) + self.w5_telemetry.addWidget(self.widgets[f"latestTelemTemperatureValue"], 1, 2, 1, 1) + + self.widgets["latestTelem0Label"] = QLabel("Ascent Rate") + self.widgets["latestTelem0Value"] = QLabel("---") + self.widgets["latestTelem0Value"].setFont(QFont("Courier New", POSITION_LABEL_FONT_SIZE, QFont.Weight.Bold)) + self.widgets["latestTelem1Label"] = QLabel("External Temperature") + self.widgets["latestTelem1Value"] = QLabel("---") + self.widgets["latestTelem1Value"].setFont(QFont("Courier New", POSITION_LABEL_FONT_SIZE, QFont.Weight.Bold)) + self.widgets["latestTelem2Label"] = QLabel("External Humidity") + self.widgets["latestTelem2Value"] = QLabel("---") + self.widgets["latestTelem2Value"].setFont(QFont("Courier New", POSITION_LABEL_FONT_SIZE, QFont.Weight.Bold)) + self.widgets["latestTelem3Label"] = QLabel("External Pressure") + self.widgets["latestTelem3Value"] = QLabel("---") + self.widgets["latestTelem3Value"].setFont(QFont("Courier New", POSITION_LABEL_FONT_SIZE, QFont.Weight.Bold)) + for i in range(4,9): + self.widgets[f"latestTelem{i}Label"] = QLabel("") + self.widgets[f"latestTelem{i}Value"] = QLabel("") + self.widgets[f"latestTelem{i}Label"].hide() + self.widgets[f"latestTelem{i}Value"].hide() + self.widgets[f"latestTelem{i}Value"].setFont(QFont("Courier New", POSITION_LABEL_FONT_SIZE, QFont.Weight.Bold)) + + for i in range(0,9): + self.w5_telemetry.addWidget(self.widgets[f"latestTelem{i}Label"], 0, i+3, 1, 1) + self.w5_telemetry.addWidget(self.widgets[f"latestTelem{i}Value"], 1, i+3, 1, 1) + + for i in range(0,7): + self.w5_telemetry.setColumnStretch(i, 10) + + for i in range(7, 12): + self.w5_telemetry.setColumnStretch(i, 1) + + #self.w5_telemetry.setRowStretch(1, 6) + + w6_groupbox = QGroupBox("Log") + w6_groupbox.setObjectName("b1") + w6_groupbox.setStyleSheet('QWidget#b1 { font-size: 15px; font-weight: bold}') + w6 = QGridLayout(w6_groupbox) + self.widgets["console"] = QPlainTextEdit() + self.widgets["console"].setReadOnly(True) + self.widgets["console"].setMaximumBlockCount(1000) + w6.addWidget(self.widgets["console"]) + w6.setRowStretch(0, 1) + + w6_groupbox.setLayout(w6) -def set_logging_format(): - if telemetry_logger: - telemetry_logger.log_format = widgets["loggingFormatSelector"].currentText() + right_column.addWidget(w2_spectrum_groupbox, 0, 0, 1, 1) + right_column.addWidget(w3_snr_groupbox, 0, 1, 1, 1) + right_column.addWidget(w3_stats_groupbox, 0, 2, 1, 1) + right_column.addWidget(w4_data_groupbox, 1, 0, 1, 3) + right_column.addWidget(w4_position_groupbox, 2, 0, 1, 3) + right_column.addWidget(w5_telemetry_groupbox, 3, 0, 1, 3) + right_column.addWidget(w6_groupbox, 4, 0, 1, 3) -widgets["loggingFormatSelector"].currentIndexChanged.connect(set_logging_format) + right_column.setColumnStretch(0, 10) + right_column.setColumnStretch(1, 6) + right_column.setColumnStretch(2, 1) -# Clear the configuration if we have been asked to, otherwise read it in from Qt stores -if args.reset: - logging.info("Clearing configuration.") - write_config() -else: - read_config(widgets) - - -try: - if float(widgets["userLatEntry"].text()) == 0.0 and float(widgets["userLonEntry"].text()) == 0.0: - _sondehub_user_pos = None - else: - _sondehub_user_pos = [float(widgets["userLatEntry"].text()), float(widgets["userLonEntry"].text()), 0.0] -except: - _sondehub_user_pos = None - -sondehub_uploader = SondehubAmateurUploader( - upload_rate = 2, - user_callsign = widgets["userCallEntry"].text(), - user_position = _sondehub_user_pos, - user_radio = "Horus-GUI v" + __version__ + " " + widgets["userRadioEntry"].text(), - user_antenna = widgets["userAntennaEntry"].text(), - software_name = "Horus-GUI", - software_version = __version__, -) + left_column_widget = QWidget() + left_column_widget.setLayout(left_column) -telemetry_logger = TelemetryLogger( - log_directory = widgets["loggingPathEntry"].text(), - log_format = widgets["loggingFormatSelector"].currentText(), - enabled = widgets["enableLoggingSelector"].isChecked() -) + right_column_widget = QWidget() + right_column_widget.setLayout(right_column) -# Handlers for various checkboxes and push-buttons -def habitat_position_reupload(dummy_arg, upload=True): - """ - Trigger a re-upload of user position information - Note that this requires a dummy argument, as the Qt - 'connect' callback supplies an argument which we don't want. - """ - global widgets, sondehub_uploader - - 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() - try: - if float(widgets["userLatEntry"].text()) == 0.0 and float(widgets["userLonEntry"].text()) == 0.0: - sondehub_uploader.user_position = None - else: - sondehub_uploader.user_position = [ - float(widgets["userLatEntry"].text()), - float(widgets["userLonEntry"].text()), - float(widgets["userAltEntry"].text())] - except Exception as e: - logging.error(f"Error parsing station location - {str(e)}") - sondehub_uploader.user_position = None + splitter = QSplitter(Qt.Orientation.Horizontal) + splitter.addWidget(left_column_widget) + splitter.addWidget(right_column_widget) - if upload: - sondehub_uploader.last_user_position_upload = 0 - widgets["sondehubPositionNotesLabel"].setText("") - logging.info("Triggered user position re-upload.") + self.mainWidget = QWidget() + self.setCentralWidget(self.mainWidget) + self.mainLayout = QHBoxLayout() + self.mainWidget.setLayout(self.mainLayout) -# Connect the 'Re-upload Position' button to the above function. -widgets["habitatUploadPosition"].clicked.connect(habitat_position_reupload) + self.mainLayout.addWidget(splitter) + # self.mainLayout = QGridLayout() + # self.mainWidget.setLayout(self.mainLayout)? -# Update uploader info as soon as it's edited, to ensure we upload with the latest user callsign -def update_uploader_details(): - """ - Wrapper function for position re-upload, called when the user callsign entry is changed. - """ - #habitat_position_reupload("unused arg",upload=False) - widgets["sondehubPositionNotesLabel"].setText("