horus-gui/src/horusgui/gui.py

437 wiersze
13 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 *
from .fft import *
from .modem import *
2020-06-26 13:02:19 +00:00
from .config import *
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)
# List of audio devices and their info
audio_devices = {}
# Processor objects
audio_stream = None
fft_process = None
horus_modem = None
# 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)
win.setWindowTitle("Horus Telemetry GUI")
# 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))
d1 = Dock("Spectrum", size=(800, 400))
d2 = Dock("Modem Stats", size=(800, 300))
d3 = Dock("Data", size=(800, 50))
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-22 11:36:55 +00:00
area.addDock(d2, "bottom", d1)
area.addDock(d3, "bottom", d2)
2020-06-26 13:02:19 +00:00
area.addDock(d4, "bottom", d3)
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, 1)
w1_audio.addWidget(widgets["audioSampleRateLabel"], 1, 0, 1, 1)
w1_audio.addWidget(widgets["audioSampleRateSelector"], 1, 1, 1, 1)
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")
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__)
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-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-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:04:05 +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=0, xMax=4000, yMin=-120, yMax=0)
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",
),
pg.InfiniteLine(
pos=-1000,
pen=pg.mkPen(color="w", width=2, style=QtCore.Qt.DashLine),
label="F2",
),
pg.InfiniteLine(
pos=-1000,
pen=pg.mkPen(color="w", width=2, style=QtCore.Qt.DashLine),
label="F3",
),
pg.InfiniteLine(
pos=-1000,
pen=pg.mkPen(color="w", width=2, style=QtCore.Qt.DashLine),
label="F4",
),
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
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
# Waterfall - TBD
w3 = 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)
widgets["snrPlot"].setLimits(xMin=0, xMax=60, yMin=-100, yMax=40)
widgets["snrPlotRange"] = [-10, 30]
2020-06-26 13:02:19 +00:00
2020-06-26 13:04:05 +00:00
widgets["eyeDiagramPlot"] = pg.PlotWidget(title="Eye Diagram")
2020-06-26 13:02:19 +00:00
2020-06-26 13:04:05 +00:00
w3.addWidget(widgets["snrPlot"], 0, 0)
w3.addWidget(widgets["eyeDiagramPlot"], 0, 1)
2020-06-26 13:02:19 +00:00
2020-06-22 11:36:55 +00:00
d2.addWidget(w3)
# Telemetry Data
w4 = pg.LayoutWidget()
2020-06-26 13:04:05 +00:00
widgets["latestSentenceLabel"] = QtGui.QLabel("<b>Latest Sentence:</b>")
widgets["latestSentenceData"] = QtGui.QLabel("NO DATA")
widgets["latestSentenceData"].setFont(QtGui.QFont("Courier New", 18, QtGui.QFont.Bold))
w4.addWidget(widgets["latestSentenceLabel"], 0, 0, 1, 1)
w4.addWidget(widgets["latestSentenceData"], 0, 1, 1, 6)
2020-06-22 11:36:55 +00:00
d3.addWidget(w4)
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-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
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!")
def start_decoding():
2020-06-22 11:48:08 +00:00
global widgets, audio_stream, fft_process, horus_modem, 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()
_sample_rate = int(widgets["audioSampleRateSelector"].currentText())
_dev_index = audio_devices[_dev_name]["index"]
2020-06-22 11:36:55 +00:00
# TODO: Grab horus data here.
# Init FFT Processor
fft_process = FFTProcess(
2020-06-26 13:04:05 +00:00
nfft=8192, stride=4096, fs=_sample_rate, callback=add_fft_update
2020-06-22 11:36:55 +00:00
)
# TODO: Setup modem here
# Setup Audio
audio_stream = AudioStream(
_dev_index,
2020-06-26 13:04:05 +00:00
fs=_sample_rate,
2020-06-22 11:36:55 +00:00
block_size=fft_process.stride,
2020-06-26 13:04:05 +00:00
fft_input=fft_process.add_samples,
modem=None,
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-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
# 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
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()
# Handle Status updates here.
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-06-26 13:02:19 +00:00
def __init__(self, consolewidget):
logging.Handler.__init__(self)
self.consolewidget = consolewidget
def emit(self, record):
_time = datetime.datetime.now()
_text = f"{record.levelname} {_time.strftime('%H:%M:%S')}:\t{record.msg}"
self.consolewidget.appendPlainText(_text)
2020-06-26 13:04:05 +00:00
2020-06-26 13:02:19 +00:00
# Add console handler to top level logger.
2020-06-26 13:04:05 +00:00
console_handler = ConsoleHandler(widgets["console"])
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
if __name__ == "__main__":
main()