horus-gui/horusgui/gui.py

806 wiersze
28 KiB
Python
Czysty Zwykły widok Historia

2020-06-22 11:36:55 +00:00
#!/usr/bin/env python
#
# Horus Telemetry GUI
#
# Mark Jessop <vk5qi@rfhead.net>
#
# Python 3 check
import sys
if sys.version_info < (3, 0):
print("This script requires Python 3!")
sys.exit(1)
2020-06-26 13:02:19 +00:00
import datetime
2020-06-22 11:36:55 +00:00
import glob
import logging
import pyqtgraph as pg
import numpy as np
from queue import Queue
from pyqtgraph.Qt import QtCore, QtGui, QtWidgets
from pyqtgraph.dockarea import *
from threading import Thread
from .widgets import *
from .audio import *
2020-07-12 07:39:23 +00:00
from .udpaudio import *
2020-06-22 11:36:55 +00:00
from .fft import *
from .modem import *
2020-06-26 13:02:19 +00:00
from .config import *
2020-06-28 06:56:13 +00:00
from .habitat import *
2020-07-05 08:02:05 +00:00
from .utils import position_info
2020-07-10 11:50:04 +00:00
from .icon import getHorusIcon
from horusdemodlib.demod import HorusLib, Mode
2020-07-05 08:02:05 +00:00
from horusdemodlib.decoder import decode_packet, parse_ukhas_string
from horusdemodlib.payloads import *
2020-07-11 08:11:58 +00:00
from horusdemodlib.horusudp import send_payload_summary
2020-06-26 13:02:19 +00:00
from . import __version__
2020-06-22 11:36:55 +00:00
# Setup Logging
2020-06-26 13:04:05 +00:00
logging.basicConfig(format="%(asctime)s %(levelname)s: %(message)s", level=logging.INFO)
2020-06-22 11:36:55 +00:00
# Global widget store
widgets = {}
# Queues for handling updates to image / status indications.
fft_update_queue = Queue(256)
status_update_queue = Queue(256)
2020-07-10 12:29:55 +00:00
log_update_queue = Queue(256)
2020-06-22 11:36:55 +00:00
# List of audio devices and their info
audio_devices = {}
# Processor objects
audio_stream = None
fft_process = None
horus_modem = None
2020-06-28 06:56:13 +00:00
habitat_uploader = None
2020-06-22 11:36:55 +00:00
decoder_init = False
2020-06-22 11:36:55 +00:00
# Global running indicator
running = False
#
# GUI Creation - The Bad way.
#
# Create a Qt App.
pg.mkQApp()
# GUI LAYOUT - Gtk Style!
win = QtGui.QMainWindow()
area = DockArea()
win.setCentralWidget(area)
2020-07-12 07:39:23 +00:00
win.setWindowTitle(f"Horus Telemetry GUI - v{__version__}")
2020-07-10 11:50:04 +00:00
win.setWindowIcon(getHorusIcon())
2020-06-22 11:36:55 +00:00
# Create multiple dock areas, for displaying our data.
2020-06-26 13:04:05 +00:00
d0 = Dock("Audio", size=(300, 50))
d0_modem = Dock("Modem", size=(300, 80))
d0_habitat = Dock("Habitat", size=(300, 200))
d0_other = Dock("Other", size=(300, 100))
2020-07-05 08:02:05 +00:00
d1 = Dock("Spectrum", size=(800, 350))
2020-07-10 11:50:04 +00:00
d2_stats = Dock("SNR (dB)", size=(50, 300))
d2_snr = Dock("SNR Plot", size=(750, 300))
2020-07-05 08:02:05 +00:00
d3_data = Dock("Data", size=(800, 50))
d3_position = Dock("Position", size=(800, 50))
2020-06-26 13:04:05 +00:00
d4 = Dock("Log", size=(800, 150))
2020-06-26 13:02:19 +00:00
# Arrange docks.
area.addDock(d0)
2020-06-26 13:04:05 +00:00
area.addDock(d1, "right", d0)
2020-06-26 13:02:19 +00:00
area.addDock(d0_modem, "bottom", d0)
area.addDock(d0_habitat, "bottom", d0_modem)
area.addDock(d0_other, "below", d0_habitat)
2020-06-29 11:42:41 +00:00
area.addDock(d2_stats, "bottom", d1)
2020-07-05 08:02:05 +00:00
area.addDock(d3_data, "bottom", d2_stats)
area.addDock(d3_position, "bottom", d3_data)
area.addDock(d4, "bottom", d3_position)
2020-06-29 11:42:41 +00:00
area.addDock(d2_snr, "right", d2_stats)
2020-06-26 13:02:19 +00:00
d0_habitat.raiseDock()
2020-06-22 11:36:55 +00:00
# Controls
2020-06-26 13:02:19 +00:00
w1_audio = pg.LayoutWidget()
2020-06-22 11:36:55 +00:00
# TNC Connection
2020-06-26 13:04:05 +00:00
widgets["audioDeviceLabel"] = QtGui.QLabel("<b>Audio Device:</b>")
widgets["audioDeviceSelector"] = QtGui.QComboBox()
2020-06-22 11:36:55 +00:00
2020-06-26 13:04:05 +00:00
widgets["audioSampleRateLabel"] = QtGui.QLabel("<b>Sample Rate (Hz):</b>")
widgets["audioSampleRateSelector"] = QtGui.QComboBox()
2020-06-22 11:36:55 +00:00
2020-06-26 13:04:05 +00:00
w1_audio.addWidget(widgets["audioDeviceLabel"], 0, 0, 1, 1)
w1_audio.addWidget(widgets["audioDeviceSelector"], 0, 1, 1, 2)
2020-06-26 13:04:05 +00:00
w1_audio.addWidget(widgets["audioSampleRateLabel"], 1, 0, 1, 1)
w1_audio.addWidget(widgets["audioSampleRateSelector"], 1, 1, 1, 2)
2020-06-26 13:02:19 +00:00
d0.addWidget(w1_audio)
w1_modem = pg.LayoutWidget()
2020-06-22 11:36:55 +00:00
# Modem Parameters
2020-06-26 13:04:05 +00:00
widgets["horusModemLabel"] = QtGui.QLabel("<b>Mode:</b>")
widgets["horusModemSelector"] = QtGui.QComboBox()
2020-06-22 11:36:55 +00:00
2020-06-26 13:04:05 +00:00
widgets["horusModemRateLabel"] = QtGui.QLabel("<b>Baudrate:</b>")
widgets["horusModemRateSelector"] = QtGui.QComboBox()
2020-06-22 11:36:55 +00:00
2020-06-26 13:04:05 +00:00
widgets["horusMaskEstimatorLabel"] = QtGui.QLabel("<b>Enable Mask Estim.:</b>")
widgets["horusMaskEstimatorSelector"] = QtGui.QCheckBox()
2020-06-22 11:36:55 +00:00
2020-06-26 13:04:05 +00:00
widgets["horusMaskSpacingLabel"] = QtGui.QLabel("<b>Tone Spacing (Hz):</b>")
widgets["horusMaskSpacingEntry"] = QtGui.QLineEdit("270")
2020-06-22 11:36:55 +00:00
# Start/Stop
2020-06-26 13:04:05 +00:00
widgets["startDecodeButton"] = QtGui.QPushButton("Start")
widgets["startDecodeButton"].setEnabled(False)
2020-06-26 13:04:05 +00:00
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["startDecodeButton"], 4, 0, 2, 2)
2020-06-22 11:36:55 +00:00
2020-06-26 13:02:19 +00:00
d0_modem.addWidget(w1_modem)
2020-06-22 11:36:55 +00:00
2020-06-26 13:02:19 +00:00
w1_habitat = pg.LayoutWidget()
# Listener Information
2020-06-26 13:04:05 +00:00
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["userCallLabel"] = QtGui.QLabel("<b>Callsign:</b>")
widgets["userCallEntry"] = QtGui.QLineEdit("N0CALL")
widgets["userCallEntry"].setMaxLength(20)
widgets["userLocationLabel"] = QtGui.QLabel("<b>Lat/Lon:</b>")
widgets["userLatEntry"] = QtGui.QLineEdit("0.0")
widgets["userLonEntry"] = QtGui.QLineEdit("0.0")
widgets["userAntennaLabel"] = QtGui.QLabel("<b>Antenna:</b>")
widgets["userAntennaEntry"] = QtGui.QLineEdit("")
widgets["userRadioLabel"] = QtGui.QLabel("<b>Radio:</b>")
widgets["userRadioEntry"] = QtGui.QLineEdit("Horus-GUI " + __version__)
2020-06-28 06:56:13 +00:00
widgets["habitatUploadPosition"] = QtGui.QPushButton("Upload Position")
2020-06-26 13:04:05 +00:00
w1_habitat.addWidget(widgets["habitatUploadLabel"], 0, 0, 1, 1)
w1_habitat.addWidget(widgets["habitatUploadSelector"], 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["userAntennaLabel"], 3, 0, 1, 1)
w1_habitat.addWidget(widgets["userAntennaEntry"], 3, 1, 1, 2)
w1_habitat.addWidget(widgets["userRadioLabel"], 4, 0, 1, 1)
w1_habitat.addWidget(widgets["userRadioEntry"], 4, 1, 1, 2)
2020-06-28 06:56:13 +00:00
w1_habitat.addWidget(widgets["habitatUploadPosition"], 5, 0, 1, 3)
2020-06-29 08:06:16 +00:00
w1_habitat.layout.setRowStretch(6, 1)
2020-06-26 13:02:19 +00:00
d0_habitat.addWidget(w1_habitat)
w1_other = pg.LayoutWidget()
2020-06-26 13:04:05 +00:00
widgets["horusUploadLabel"] = QtGui.QLabel("<b>Enable Horus UDP Output:</b>")
widgets["horusUploadSelector"] = QtGui.QCheckBox()
widgets["horusUploadSelector"].setChecked(True)
widgets["horusUDPLabel"] = QtGui.QLabel("<b>Horus UDP Port:</b>")
widgets["horusUDPEntry"] = QtGui.QLineEdit("55672")
widgets["horusUDPEntry"].setMaxLength(5)
w1_other.addWidget(widgets["horusUploadLabel"], 0, 0, 1, 1)
w1_other.addWidget(widgets["horusUploadSelector"], 0, 1, 1, 1)
w1_other.addWidget(widgets["horusUDPLabel"], 1, 0, 1, 1)
w1_other.addWidget(widgets["horusUDPEntry"], 1, 1, 1, 1)
2020-06-29 08:06:16 +00:00
w1_other.layout.setRowStretch(5, 1)
2020-06-26 13:02:19 +00:00
d0_other.addWidget(w1_other)
2020-06-22 11:36:55 +00:00
# Spectrum Display
2020-06-26 13:04:05 +00:00
widgets["spectrumPlot"] = pg.PlotWidget(title="Spectra")
widgets["spectrumPlot"].setLabel("left", "Power (dB)")
widgets["spectrumPlot"].setLabel("bottom", "Frequency (Hz)")
widgets["spectrumPlotData"] = widgets["spectrumPlot"].plot([0])
2020-06-22 11:36:55 +00:00
2020-06-26 13:02:19 +00:00
# Frequency Estiator Outputs
2020-06-26 13:04:05 +00:00
widgets["estimatorLines"] = [
pg.InfiniteLine(
pos=-1000,
pen=pg.mkPen(color="w", width=2, style=QtCore.Qt.DashLine),
label="F1",
2020-06-29 11:42:41 +00:00
labelOpts={'position':0.9}
2020-06-26 13:04:05 +00:00
),
pg.InfiniteLine(
pos=-1000,
pen=pg.mkPen(color="w", width=2, style=QtCore.Qt.DashLine),
label="F2",
2020-06-29 11:42:41 +00:00
labelOpts={'position':0.9}
2020-06-26 13:04:05 +00:00
),
pg.InfiniteLine(
pos=-1000,
pen=pg.mkPen(color="w", width=2, style=QtCore.Qt.DashLine),
label="F3",
2020-06-29 11:42:41 +00:00
labelOpts={'position':0.9}
2020-06-26 13:04:05 +00:00
),
pg.InfiniteLine(
pos=-1000,
pen=pg.mkPen(color="w", width=2, style=QtCore.Qt.DashLine),
label="F4",
2020-06-29 11:42:41 +00:00
labelOpts={'position':0.9}
2020-06-26 13:04:05 +00:00
),
2020-06-26 13:02:19 +00:00
]
2020-06-26 13:04:05 +00:00
for _line in widgets["estimatorLines"]:
widgets["spectrumPlot"].addItem(_line)
2020-06-26 13:02:19 +00:00
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)
2020-06-29 11:42:41 +00:00
widgets["spectrumPlot"].showGrid(True, True)
2020-06-26 13:04:05 +00:00
d1.addWidget(widgets["spectrumPlot"])
2020-06-22 11:36:55 +00:00
2020-06-26 13:04:05 +00:00
widgets["spectrumPlotRange"] = [-100, -20]
2020-06-22 11:36:55 +00:00
2020-06-29 11:42:41 +00:00
w3_stats = pg.LayoutWidget()
2020-07-10 11:50:04 +00:00
widgets["snrBar"] = QtWidgets.QProgressBar()
widgets["snrBar"].setOrientation(QtCore.Qt.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["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)
2020-06-29 11:42:41 +00:00
d2_stats.addWidget(w3_stats)
# SNR Plot
w3_snr = pg.LayoutWidget()
2020-06-26 13:04:05 +00:00
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)
2020-06-29 11:42:41 +00:00
widgets["snrPlot"].setLimits(xMin=-60, xMax=0, yMin=-10, yMax=40)
widgets["snrPlot"].showGrid(True, True)
2020-06-26 13:04:05 +00:00
widgets["snrPlotRange"] = [-10, 30]
2020-06-29 11:42:41 +00:00
widgets["snrPlotTime"] = np.array([])
widgets["snrPlotSNR"] = np.array([])
widgets["snrPlotData"] = widgets["snrPlot"].plot(widgets["snrPlotTime"], widgets["snrPlotSNR"])
2020-06-26 13:02:19 +00:00
2020-06-29 11:42:41 +00:00
# TODO: Look into eye diagram more
# widgets["eyeDiagramPlot"] = pg.PlotWidget(title="Eye Diagram")
# widgets["eyeDiagramData"] = widgets["eyeDiagramPlot"].plot([0])
2020-06-26 13:02:19 +00:00
2020-06-29 11:42:41 +00:00
w3_snr.addWidget(widgets["snrPlot"], 0, 1, 2, 1)
2020-06-26 13:02:19 +00:00
2020-06-29 11:42:41 +00:00
#w3.addWidget(widgets["eyeDiagramPlot"], 0, 1)
2020-06-26 13:02:19 +00:00
2020-06-29 11:42:41 +00:00
d2_snr.addWidget(w3_snr)
2020-06-22 11:36:55 +00:00
# Telemetry Data
2020-07-05 08:02:05 +00:00
w4_data = pg.LayoutWidget()
widgets["latestRawSentenceLabel"] = QtGui.QLabel("<b>Latest Packet (Raw):</b>")
widgets["latestRawSentenceData"] = QtGui.QLineEdit("NO DATA")
widgets["latestRawSentenceData"].setReadOnly(True)
#widgets["latestRawSentenceData"].setFont(QtGui.QFont("Courier New", 18, QtGui.QFont.Bold))
#widgets["latestRawSentenceData"].setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
widgets["latestDecodedSentenceLabel"] = QtGui.QLabel("<b>Latest Packet (Decoded):</b>")
widgets["latestDecodedSentenceData"] = QtGui.QLineEdit("NO DATA")
widgets["latestDecodedSentenceData"].setReadOnly(True)
#widgets["latestDecodedSentenceData"].setFont(QtGui.QFont("Courier New", 18, QtGui.QFont.Bold))
#widgets["latestDecodedSentenceData"].setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
2020-07-05 08:02:05 +00:00
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)
d3_data.addWidget(w4_data)
w4_position = pg.LayoutWidget()
POSITION_LABEL_FONT_SIZE = 16
2020-07-05 08:02:05 +00:00
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))
2020-07-05 08:02:05 +00:00
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))
2020-07-05 08:02:05 +00:00
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))
2020-07-05 08:02:05 +00:00
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))
2020-07-05 08:02:05 +00:00
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))
2020-07-05 08:02:05 +00:00
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))
2020-07-05 08:02:05 +00:00
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))
2020-07-05 08:02:05 +00:00
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))
2020-07-05 08:02:05 +00:00
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)
2020-06-22 11:36:55 +00:00
2020-06-26 13:02:19 +00:00
w5 = pg.LayoutWidget()
2020-06-26 13:04:05 +00:00
widgets["console"] = QtWidgets.QPlainTextEdit()
widgets["console"].setReadOnly(True)
w5.addWidget(widgets["console"])
2020-06-26 13:02:19 +00:00
d4.addWidget(w5)
2020-06-22 11:36:55 +00:00
# Resize window to final resolution, and display.
logging.info("Starting GUI.")
win.resize(1500, 800)
win.show()
2020-06-26 13:04:05 +00:00
# Audio Initialization
2020-06-22 11:36:55 +00:00
audio_devices = init_audio(widgets)
2020-06-26 13:04:05 +00:00
2020-06-22 11:36:55 +00:00
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)
2020-06-26 13:04:05 +00:00
widgets["audioDeviceSelector"].currentIndexChanged.connect(update_audio_sample_rates)
2020-06-22 11:36:55 +00:00
# Initialize modem list.
init_horus_modem(widgets)
2020-06-26 13:04:05 +00:00
2020-06-22 11:36:55 +00:00
def update_modem_settings():
""" Update the modem setting widgets when a different modem is selected """
global widgets
populate_modem_settings(widgets)
2020-06-26 13:04:05 +00:00
widgets["horusModemSelector"].currentIndexChanged.connect(update_modem_settings)
2020-06-22 11:36:55 +00:00
2020-06-26 13:02:19 +00:00
# Read in configuration file settings
read_config(widgets)
2020-06-28 06:56:13 +00:00
# Start Habitat Uploader
habitat_uploader = HabitatUploader(
2020-06-29 08:06:16 +00:00
user_callsign=widgets["userCallEntry"].text(),
listener_lat=widgets["userLatEntry"].text(),
listener_lon=widgets["userLonEntry"].text(),
2020-07-10 11:50:04 +00:00
listener_radio="Horus-GUI v" + __version__ + " " + widgets["userRadioEntry"].text(),
2020-06-29 08:06:16 +00:00
listener_antenna=widgets["userAntennaEntry"].text(),
2020-06-28 06:56:13 +00:00
)
2020-06-29 08:06:16 +00:00
2020-06-28 06:56:13 +00:00
def habitat_position_reupload():
""" Trigger a re-upload of user position information """
global widgets, habitat_uploader
habitat_uploader.user_callsign = widgets["userCallEntry"].text()
habitat_uploader.listener_lat = widgets["userLatEntry"].text()
habitat_uploader.listener_lon = widgets["userLonEntry"].text()
2020-07-10 11:50:04 +00:00
habitat_uploader.listener_radio = "Horus-GUI v" + __version__ + " " + widgets["userRadioEntry"].text()
2020-06-28 06:56:13 +00:00
habitat_uploader.listener_antenna = widgets["userAntennaEntry"].text()
habitat_uploader.trigger_position_upload()
2020-06-29 08:06:16 +00:00
2020-06-28 06:56:13 +00:00
widgets["habitatUploadPosition"].clicked.connect(habitat_position_reupload)
2020-06-29 08:06:16 +00:00
2020-06-28 06:56:13 +00:00
def habitat_inhibit():
""" Update the Habitat inhibit flag """
global widgets, habitat_uploader
2020-06-29 08:06:16 +00:00
habitat_uploader.inhibit = not widgets["habitatUploadSelector"].isChecked()
2020-06-28 06:56:13 +00:00
logging.debug(f"Updated Habitat Inhibit state: {habitat_uploader.inhibit}")
2020-06-29 08:06:16 +00:00
2020-06-28 06:56:13 +00:00
widgets["habitatUploadSelector"].clicked.connect(habitat_inhibit)
2020-06-26 13:02:19 +00:00
2020-06-22 11:36:55 +00:00
def handle_fft_update(data):
""" Handle a new FFT update """
global widgets
2020-06-26 13:04:05 +00:00
_scale = data["scale"]
_data = data["fft"]
2020-06-22 11:36:55 +00:00
2020-06-26 13:04:05 +00:00
widgets["spectrumPlotData"].setData(_scale, _data)
2020-06-22 11:36:55 +00:00
# Really basic IIR to smoothly adjust scale
2020-06-26 13:04:05 +00:00
_old_max = widgets["spectrumPlotRange"][1]
2020-06-22 11:36:55 +00:00
_tc = 0.1
2020-06-26 13:04:05 +00:00
_new_max = float((_old_max * (1 - _tc)) + (np.max(_data) * _tc))
2020-06-22 11:36:55 +00:00
# Store new max
2020-06-26 13:04:05 +00:00
widgets["spectrumPlotRange"][1] = _new_max
2020-06-22 11:36:55 +00:00
2020-06-26 13:04:05 +00:00
widgets["spectrumPlot"].setYRange(
widgets["spectrumPlotRange"][0], min(0, _new_max) + 20
)
2020-06-22 11:36:55 +00:00
2020-06-29 11:42:41 +00:00
def handle_status_update(status):
""" Handle a new status frame """
global widgets, habitat
# Update Frequency estimator markers
for _i in range(len(status.extended_stats.f_est)):
_fest_pos = float(status.extended_stats.f_est[_i])
if _fest_pos != 0.0:
widgets["estimatorLines"][_i].setPos(_fest_pos)
# Update SNR Plot
_time = time.time()
# Roll Time/SNR
widgets["snrPlotTime"] = np.append(widgets["snrPlotTime"], _time)
widgets["snrPlotSNR"] = np.append(widgets["snrPlotSNR"], float(status.snr))
if len(widgets["snrPlotTime"]) > 200:
widgets["snrPlotTime"] = widgets["snrPlotTime"][1:]
widgets["snrPlotSNR"] = widgets["snrPlotSNR"][1:]
# Plot new SNR data
widgets["snrPlotData"].setData((widgets["snrPlotTime"]-_time), widgets["snrPlotSNR"])
_old_max = widgets["snrPlotRange"][1]
_tc = 0.1
_new_max = float((_old_max * (1 - _tc)) + (np.max(widgets["snrPlotSNR"]) * _tc))
widgets["snrPlotRange"][1] = _new_max
widgets["snrPlot"].setYRange(
widgets["snrPlotRange"][0], _new_max+10
)
2020-07-10 11:50:04 +00:00
# Update SNR bar and label
widgets["snrLabel"].setText(f"{float(status.snr):2.1f}")
widgets["snrBar"].setValue(int(status.snr))
2020-06-29 11:42:41 +00:00
2020-06-22 11:36:55 +00:00
def add_fft_update(data):
""" Try and insert a new set of FFT data into the update queue """
global fft_update_queue
try:
fft_update_queue.put_nowait(data)
except:
logging.error("FFT Update Queue Full!")
2020-06-29 11:42:41 +00:00
def add_stats_update(frame):
""" Try and insert modem statistics into the processing queue """
global status_update_queue
try:
status_update_queue.put_nowait(frame)
except:
logging.error("Status Update Queue Full!")
def handle_new_packet(frame):
""" Handle receipt of a newly decoded packet """
if len(frame.data) > 0:
if type(frame.data) == bytes:
2020-07-05 08:02:05 +00:00
# Packets from the binary decoders are provided as raw bytes.
# Conver them to a hexadecimal representation for display in the 'raw' area.
_packet = frame.data.hex().upper()
2020-06-29 11:42:41 +00:00
else:
2020-07-05 08:02:05 +00:00
# RTTY packets are provided as a string, and can be displayed directly
2020-06-29 11:42:41 +00:00
_packet = frame.data
2020-07-05 08:02:05 +00:00
# Update the raw display.
widgets["latestRawSentenceData"].setText(f"{_packet}")
2020-06-29 11:42:41 +00:00
2020-07-05 08:02:05 +00:00
_decoded = None
if type(frame.data) == str:
# RTTY packet handling.
# Attempt to extract fields from it:
try:
_decoded = parse_ukhas_string(frame.data)
# If we get here, the string is valid!
widgets["latestDecodedSentenceData"].setText(f"{_packet}")
# Upload the string to Habitat
_decoded_str = "$$" + frame.data.split('$')[-1] + '\n'
habitat_uploader.add(_decoded_str)
except Exception as e:
widgets["latestDecodedSentenceData"].setText("DECODE FAILED")
logging.error(f"Decode Failed: {str(e)}")
else:
# Handle binary packets
try:
_decoded = decode_packet(frame.data)
widgets["latestDecodedSentenceData"].setText(_decoded['ukhas_str'])
habitat_uploader.add(_decoded['ukhas_str']+'\n')
except Exception as e:
widgets["latestDecodedSentenceData"].setText("DECODE FAILED")
logging.error(f"Decode Failed: {str(e)}")
2020-07-05 08:02:05 +00:00
# If we have extracted data, update the decoded data display
if _decoded:
widgets["latestPacketCallsignValue"].setText(_decoded['callsign'])
widgets["latestPacketTimeValue"].setText(_decoded['time'])
widgets["latestPacketLatitudeValue"].setText(f"{_decoded['latitude']:.5f}")
widgets["latestPacketLongitudeValue"].setText(f"{_decoded['longitude']:.5f}")
widgets["latestPacketAltitudeValue"].setText(f"{_decoded['altitude']}")
# Attempt to update the range/elevation/bearing fields.
try:
_station_lat = float(widgets["userLatEntry"].text())
_station_lon = float(widgets["userLonEntry"].text())
_station_alt = 0.0
if (_station_lat != 0.0) or (_station_lon != 0.0):
_position_info = position_info(
(_station_lat, _station_lon, _station_alt),
(_decoded['latitude'], _decoded['longitude'], _decoded['altitude'])
)
widgets['latestPacketBearingValue'].setText(f"{_position_info['bearing']:.1f}")
widgets['latestPacketElevationValue'].setText(f"{_position_info['elevation']:.1f}")
widgets['latestPacketRangeValue'].setText(f"{_position_info['straight_distance']/1000.0:.1f}")
except Exception as e:
logging.error(f"Could not calculate relative position to payload - {str(e)}")
# Send data out via Horus UDP
if widgets["horusUploadSelector"].isChecked():
_udp_port = int(widgets["horusUDPEntry"].text())
# Add in SNR data
_snr = float(widgets["snrLabel"].text())
_decoded['snr'] = _snr
send_payload_summary(_decoded, port=_udp_port)
2020-07-05 08:02:05 +00:00
2020-06-29 11:42:41 +00:00
2020-06-22 11:36:55 +00:00
def start_decoding():
2020-07-12 04:58:10 +00:00
"""
Read settings from the GUI
Set up all elements of the decode chain
Start decoding!
(Or, stop decoding)
"""
2020-07-05 08:02:05 +00:00
global widgets, audio_stream, fft_process, horus_modem, habitat_uploader, audio_devices, running, fft_update_queue, status_update_queue
2020-06-22 11:36:55 +00:00
if not running:
# Grab settings off widgets
2020-06-26 13:04:05 +00:00
_dev_name = widgets["audioDeviceSelector"].currentText()
2020-07-12 07:39:23 +00:00
if _dev_name != 'GQRX UDP':
_sample_rate = int(widgets["audioSampleRateSelector"].currentText())
_dev_index = audio_devices[_dev_name]["index"]
else:
# Override sample rate for GQRX UDP input.
_sample_rate = 48000
2020-06-22 11:36:55 +00:00
2020-06-29 11:42:41 +00:00
# Grab Horus Settings
_modem_name = widgets["horusModemSelector"].currentText()
_modem_id = HORUS_MODEM_LIST[_modem_name]['id']
_modem_rate = int(widgets["horusModemRateSelector"].currentText())
_modem_mask_enabled = widgets["horusMaskEstimatorSelector"].isChecked()
if _modem_mask_enabled:
_modem_tone_spacing = int(widgets["horusMaskSpacingEntry"].text())
else:
_modem_tone_spacing = -1
# Reset Frequency Estimator indicators
for _line in widgets["estimatorLines"]:
_line.setPos(-1000)
# Reset data fields
widgets["latestRawSentenceData"].setText("NO DATA")
widgets["latestDecodedSentenceData"].setText("NO DATA")
2020-07-05 08:02:05 +00:00
widgets["latestPacketCallsignValue"].setText("---")
widgets["latestPacketTimeValue"].setText("---")
widgets["latestPacketLatitudeValue"].setText("---")
widgets["latestPacketLongitudeValue"].setText("---")
widgets["latestPacketAltitudeValue"].setText("---")
widgets["latestPacketElevationValue"].setText("---")
widgets["latestPacketBearingValue"].setText("---")
widgets["latestPacketRangeValue"].setText("---")
# Ensure the Habitat upload is set correctly.
habitat_uploader.inhibit = not widgets["habitatUploadSelector"].isChecked()
2020-06-22 11:36:55 +00:00
# Init FFT Processor
2020-07-05 08:02:05 +00:00
NFFT = 2 ** 13
2020-06-29 08:06:16 +00:00
STRIDE = 2 ** 13
2020-06-22 11:36:55 +00:00
fft_process = FFTProcess(
2020-07-05 08:02:05 +00:00
nfft=NFFT,
stride=STRIDE,
update_decimation=1,
fs=_sample_rate,
callback=add_fft_update
2020-06-22 11:36:55 +00:00
)
2020-06-29 11:42:41 +00:00
# Setup Modem
horus_modem = HorusLib(
mode=_modem_id,
rate=_modem_rate,
tone_spacing=_modem_tone_spacing,
2020-07-12 07:39:23 +00:00
callback=handle_new_packet,
sample_rate=_sample_rate
2020-06-29 11:42:41 +00:00
)
2020-06-22 11:36:55 +00:00
2020-07-12 07:39:23 +00:00
# Setup Audio (or UDP input)
if _dev_name == 'GQRX UDP':
audio_stream = UDPStream(
udp_port=7355,
fs=_sample_rate,
block_size=fft_process.stride,
fft_input=fft_process.add_samples,
modem=horus_modem,
stats_callback=add_stats_update
)
else:
audio_stream = AudioStream(
_dev_index,
fs=_sample_rate,
block_size=fft_process.stride,
fft_input=fft_process.add_samples,
modem=horus_modem,
stats_callback=add_stats_update
)
2020-06-22 11:36:55 +00:00
2020-06-26 13:04:05 +00:00
widgets["startDecodeButton"].setText("Stop")
2020-06-22 11:36:55 +00:00
running = True
2020-06-26 13:02:19 +00:00
logging.info("Started Audio Processing.")
2020-06-22 11:36:55 +00:00
else:
try:
audio_stream.stop()
except Exception as e:
logging.exception("Could not stop audio stream.", exc_info=e)
2020-06-26 13:04:05 +00:00
2020-06-22 11:36:55 +00:00
try:
fft_process.stop()
except Exception as e:
logging.exception("Could not stop fft processing.", exc_info=e)
2020-06-29 11:42:41 +00:00
try:
horus_modem.close()
except Exception as e:
logging.exception("Could not close horus modem.", exc_info=e)
2020-06-22 11:48:08 +00:00
fft_update_queue = Queue(256)
status_update_queue = Queue(256)
2020-06-26 13:04:05 +00:00
widgets["startDecodeButton"].setText("Start")
2020-06-22 11:36:55 +00:00
running = False
2020-06-26 13:02:19 +00:00
logging.info("Stopped Audio Processing.")
2020-06-22 11:36:55 +00:00
2020-06-26 13:04:05 +00:00
widgets["startDecodeButton"].clicked.connect(start_decoding)
2020-06-22 11:36:55 +00:00
2020-07-10 12:29:55 +00:00
def handle_log_update(log_update):
global widgets
widgets["console"].appendPlainText(log_update)
# Make sure the scroll bar is right at the bottom.
_sb = widgets["console"].verticalScrollBar()
_sb.setValue(_sb.maximum())
2020-06-22 11:36:55 +00:00
# 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
2020-06-22 11:36:55 +00:00
while fft_update_queue.qsize() > 0:
_data = fft_update_queue.get()
handle_fft_update(_data)
while status_update_queue.qsize() > 0:
_status = status_update_queue.get()
2020-06-29 11:42:41 +00:00
handle_status_update(_status)
2020-06-22 11:36:55 +00:00
2020-07-10 12:29:55 +00:00
while log_update_queue.qsize() > 0:
_log = log_update_queue.get()
handle_log_update(_log)
2020-07-10 12:18:12 +00:00
# Try and force a re-draw.
QtGui.QApplication.processEvents()
if not decoder_init:
# Initialise decoders, and other libraries here.
2020-07-12 04:58:10 +00:00
init_payloads()
decoder_init = True
# Once initialised, enable the start button
widgets["startDecodeButton"].setEnabled(True)
2020-06-22 11:36:55 +00:00
gui_update_timer = QtCore.QTimer()
gui_update_timer.timeout.connect(processQueues)
gui_update_timer.start(100)
2020-06-26 13:02:19 +00:00
class ConsoleHandler(logging.Handler):
""" Logging handler to write to the GUI console """
2020-06-26 13:04:05 +00:00
2020-07-10 12:29:55 +00:00
def __init__(self, log_queue):
2020-06-26 13:02:19 +00:00
logging.Handler.__init__(self)
2020-07-10 12:29:55 +00:00
self.log_queue = log_queue
2020-06-26 13:02:19 +00:00
def emit(self, record):
_time = datetime.datetime.now()
2020-06-26 13:10:02 +00:00
_text = f"{_time.strftime('%H:%M:%S')} [{record.levelname}] {record.msg}"
2020-07-10 12:29:55 +00:00
try:
self.log_queue.put_nowait(_text)
except:
print("Queue full!")
2020-06-26 13:02:19 +00:00
2020-06-26 13:04:05 +00:00
2020-06-26 13:02:19 +00:00
# Add console handler to top level logger.
2020-07-10 12:29:55 +00:00
console_handler = ConsoleHandler(log_update_queue)
2020-06-26 13:02:19 +00:00
logging.getLogger().addHandler(console_handler)
logging.info("Started GUI.")
2020-06-22 11:36:55 +00:00
# Main
def main():
# Start the Qt Loop
if (sys.flags.interactive != 1) or not hasattr(QtCore, "PYQT_VERSION"):
QtGui.QApplication.instance().exec_()
2020-06-26 13:02:19 +00:00
save_config(widgets)
2020-06-26 13:04:05 +00:00
2020-06-22 11:36:55 +00:00
try:
audio_stream.stop()
except Exception as e:
2020-06-26 13:02:19 +00:00
pass
2020-06-26 13:04:05 +00:00
2020-06-22 11:36:55 +00:00
try:
fft_process.stop()
except Exception as e:
2020-06-26 13:02:19 +00:00
pass
2020-06-22 11:36:55 +00:00
2020-06-28 06:56:13 +00:00
try:
habitat_uploader.close()
except:
pass
2020-06-22 11:36:55 +00:00
if __name__ == "__main__":
main()