pull/13/head
Mark Jessop 2020-06-26 22:34:05 +09:30
rodzic 081ae9aa24
commit 3c48b578c2
8 zmienionych plików z 253 dodań i 264 usunięć

Wyświetl plik

@ -18,13 +18,13 @@ help:
# help: style.check - perform code format compliance check # help: style.check - perform code format compliance check
.PHONY: style.check .PHONY: style.check
style.check: style.check:
@black src/horusgui apps setup.py --check @black src/horusgui setup.py --check
# help: style - perform code format compliance changes # help: style - perform code format compliance changes
.PHONY: style .PHONY: style
style: style:
@black src/horusgui apps setup.py @black src/horusgui setup.py
# help: test - run tests # help: test - run tests

Wyświetl plik

@ -1,2 +1 @@
__version__ = "20.6.21" __version__ = "20.6.21"

Wyświetl plik

@ -9,6 +9,7 @@ audioStream = None
audioDevices = {} audioDevices = {}
def init_audio(widgets): def init_audio(widgets):
""" Initialise pyaudio object, and populate list of sound card in GUI """ """ Initialise pyaudio object, and populate list of sound card in GUI """
global pyAudio, audioDevices global pyAudio, audioDevices
@ -18,24 +19,24 @@ def init_audio(widgets):
audioDevices = {} audioDevices = {}
# Clear list # Clear list
widgets['audioDeviceSelector'].clear() widgets["audioDeviceSelector"].clear()
# Iterate through PyAudio devices # Iterate through PyAudio devices
for x in range(0, pyAudio.get_device_count()): for x in range(0, pyAudio.get_device_count()):
_dev = pyAudio.get_device_info_by_index(x) _dev = pyAudio.get_device_info_by_index(x)
# Does the device have inputs? # Does the device have inputs?
if _dev['maxInputChannels'] > 0: if _dev["maxInputChannels"] > 0:
# Get the name # Get the name
_name = _dev['name'] _name = _dev["name"]
# Add to local store of device info # Add to local store of device info
audioDevices[_name] = _dev audioDevices[_name] = _dev
# Add to audio device selection list. # Add to audio device selection list.
widgets['audioDeviceSelector'].addItem(_name) widgets["audioDeviceSelector"].addItem(_name)
# Select first item. # Select first item.
if len(list(audioDevices.keys())) > 0: if len(list(audioDevices.keys())) > 0:
widgets['audioDeviceSelector'].setCurrentIndex(0) widgets["audioDeviceSelector"].setCurrentIndex(0)
# Initial population of sample rates. # Initial population of sample rates.
populate_sample_rates(widgets) populate_sample_rates(widgets)
@ -48,35 +49,25 @@ def populate_sample_rates(widgets):
global audioDevices global audioDevices
# Clear list of sample rates. # Clear list of sample rates.
widgets['audioSampleRateSelector'].clear() widgets["audioSampleRateSelector"].clear()
# Get information on current audio device # Get information on current audio device
_dev_name = widgets['audioDeviceSelector'].currentText() _dev_name = widgets["audioDeviceSelector"].currentText()
if _dev_name in audioDevices: if _dev_name in audioDevices:
# TODO: Determine valid samples rates. For now, just use the default. # TODO: Determine valid samples rates. For now, just use the default.
_samp_rate = int(audioDevices[_dev_name]['defaultSampleRate']) _samp_rate = int(audioDevices[_dev_name]["defaultSampleRate"])
widgets['audioSampleRateSelector'].addItem(str(_samp_rate)) widgets["audioSampleRateSelector"].addItem(str(_samp_rate))
widgets['audioSampleRateSelector'].setCurrentIndex(0) widgets["audioSampleRateSelector"].setCurrentIndex(0)
else: else:
logging.error("Audio - Unknown Audio Device") logging.error("Audio - Unknown Audio Device")
class AudioStream(object): class AudioStream(object):
""" Start up a pyAudio input stream, and pass data around to different callbacks """ """ Start up a pyAudio input stream, and pass data around to different callbacks """
def __init__( def __init__(self, audio_device, fs, block_size=8192, fft_input=None, modem=None):
self,
audio_device,
fs,
block_size = 8192,
fft_input = None,
modem = None
):
self.audio_device = audio_device self.audio_device = audio_device
self.fs = fs self.fs = fs
self.block_size = block_size self.block_size = block_size
@ -84,7 +75,6 @@ class AudioStream(object):
self.fft_input = fft_input self.fft_input = fft_input
self.modem = modem self.modem = modem
# Start audio stream # Start audio stream
self.audio = pyaudio.PyAudio() self.audio = pyaudio.PyAudio()
@ -96,26 +86,20 @@ class AudioStream(object):
input=True, input=True,
input_device_index=self.audio_device, input_device_index=self.audio_device,
output=False, output=False,
stream_callback=self.handle_samples stream_callback=self.handle_samples,
) )
def handle_samples(self, data, frame_count, time_info="", status_flags=""): def handle_samples(self, data, frame_count, time_info="", status_flags=""):
""" Handle incoming samples from pyaudio """ """ Handle incoming samples from pyaudio """
# Pass samples directly into fft. # Pass samples directly into fft.
if self.fft_input: if self.fft_input:
self.fft_input(data) self.fft_input(data)
# TODO: Handle modem sample input. # TODO: Handle modem sample input.
return (None, pyaudio.paContinue) return (None, pyaudio.paContinue)
def stop(self): def stop(self):
""" Halt stream """ """ Halt stream """
self.stream.close() self.stream.close()

Wyświetl plik

@ -12,16 +12,16 @@ from . import __version__
default_config = { default_config = {
'audio_device': 'None', "audio_device": "None",
'modem': 'Horus Binary v1 (Legacy)', "modem": "Horus Binary v1 (Legacy)",
'habitat_upload_enabled': True, "habitat_upload_enabled": True,
'habitat_call': 'N0CALL', "habitat_call": "N0CALL",
'habitat_lat': 0.0, "habitat_lat": 0.0,
'habitat_lon': 0.0, "habitat_lon": 0.0,
'habitat_antenna': "", "habitat_antenna": "",
'habitat_radio': "Horus-GUI "+ __version__, "habitat_radio": "Horus-GUI " + __version__,
'horus_udp_enabled': True, "horus_udp_enabled": True,
'horus_udp_port': 55672 "horus_udp_port": 55672,
} }
@ -33,13 +33,12 @@ def init_config(filename="config.yml"):
yaml = YAML() yaml = YAML()
try: try:
with open(filename, 'w') as _outfile: with open(filename, "w") as _outfile:
yaml.dump(default_config, _outfile) yaml.dump(default_config, _outfile)
except Exception as e: except Exception as e:
logging.error(f"Could not write configuration file - {str(e)}") logging.error(f"Could not write configuration file - {str(e)}")
def read_config(widgets, filename="config.yml"): def read_config(widgets, filename="config.yml"):
""" Read in a configuration yml file, and set up all GUI widgets """ """ Read in a configuration yml file, and set up all GUI widgets """
if not os.path.exists(filename): if not os.path.exists(filename):
@ -50,7 +49,7 @@ def read_config(widgets, filename="config.yml"):
_config = None _config = None
try: try:
with open(filename, 'r') as _infile: with open(filename, "r") as _infile:
_config = yaml.load(_infile) _config = yaml.load(_infile)
except Exception as e: except Exception as e:
logging.error(f"Error reading config file - {str(e)}") logging.error(f"Error reading config file - {str(e)}")
@ -60,25 +59,22 @@ def read_config(widgets, filename="config.yml"):
if widgets: if widgets:
# Habitat Settings # Habitat Settings
widgets['habitatUploadSelector'].setChecked(_config['habitat_upload_enabled']) widgets["habitatUploadSelector"].setChecked(_config["habitat_upload_enabled"])
widgets['userCallEntry'].setText(str(_config['habitat_call'])) widgets["userCallEntry"].setText(str(_config["habitat_call"]))
widgets['userLatEntry'].setText(str(_config['habitat_lat'])) widgets["userLatEntry"].setText(str(_config["habitat_lat"]))
widgets['userLonEntry'].setText(str(_config['habitat_lon'])) widgets["userLonEntry"].setText(str(_config["habitat_lon"]))
widgets['userAntennaEntry'].setText(str(_config['habitat_antenna'])) widgets["userAntennaEntry"].setText(str(_config["habitat_antenna"]))
widgets['userRadioEntry'].setText(str(_config['habitat_radio'])) widgets["userRadioEntry"].setText(str(_config["habitat_radio"]))
# Horus Settings # Horus Settings
widgets['horusUploadSelector'].setChecked(_config['horus_udp_enabled']) widgets["horusUploadSelector"].setChecked(_config["horus_udp_enabled"])
widgets['horusUDPEntry'].setText(str(_config['horus_udp_port'])) widgets["horusUDPEntry"].setText(str(_config["horus_udp_port"]))
# Try and set the audio device. # Try and set the audio device.
# If the audio device is not in the available list of devices, this will fail silently. # If the audio device is not in the available list of devices, this will fail silently.
widgets['audioDeviceSelector'].setCurrentText(_config['audio_device']) widgets["audioDeviceSelector"].setCurrentText(_config["audio_device"])
# Try and set the modem. If the modem is not valid, this will fail silently. # Try and set the modem. If the modem is not valid, this will fail silently.
widgets['horusModemSelector'].setCurrentText(_config['modem']) widgets["horusModemSelector"].setCurrentText(_config["modem"])
def save_config(widgets, filename="config.yml"): def save_config(widgets, filename="config.yml"):
@ -86,22 +82,22 @@ def save_config(widgets, filename="config.yml"):
global default_config global default_config
if widgets: if widgets:
default_config['habitat_upload_enabled'] = widgets['habitatUploadSelector'].isChecked() default_config["habitat_upload_enabled"] = widgets[
default_config['habitat_call'] = widgets['userCallEntry'].text() "habitatUploadSelector"
default_config['habitat_lat'] = float(widgets['userLatEntry'].text()) ].isChecked()
default_config['habitat_lon'] = float(widgets['userLonEntry'].text()) default_config["habitat_call"] = widgets["userCallEntry"].text()
default_config['habitat_antenna'] = widgets['userAntennaEntry'].text() default_config["habitat_lat"] = float(widgets["userLatEntry"].text())
default_config['habitat_radio'] = widgets['userRadioEntry'].text() default_config["habitat_lon"] = float(widgets["userLonEntry"].text())
default_config['horus_udp_enabled'] = widgets['horusUploadSelector'].isChecked() default_config["habitat_antenna"] = widgets["userAntennaEntry"].text()
default_config['horus_udp_port'] = int(widgets['horusUDPEntry'].text()) default_config["habitat_radio"] = widgets["userRadioEntry"].text()
default_config['audio_device'] = widgets['audioDeviceSelector'].currentText() default_config["horus_udp_enabled"] = widgets["horusUploadSelector"].isChecked()
default_config['modem'] = widgets['horusModemSelector'].currentText() default_config["horus_udp_port"] = int(widgets["horusUDPEntry"].text())
default_config["audio_device"] = widgets["audioDeviceSelector"].currentText()
default_config["modem"] = widgets["horusModemSelector"].currentText()
# Write out to config file
init_config(filename) init_config(filename)
if __name__ == "__main__": if __name__ == "__main__":
read_config(None) read_config(None)

Wyświetl plik

@ -1,4 +1,4 @@
# FFT # FFT
import logging import logging
import time import time
import numpy as np import numpy as np
@ -6,6 +6,7 @@ import scipy.signal
from queue import Queue from queue import Queue
from threading import Thread from threading import Thread
class FFTProcess(object): class FFTProcess(object):
""" Process an incoming stream of samples, and calculate FFTs """ """ Process an incoming stream of samples, and calculate FFTs """
@ -15,8 +16,8 @@ class FFTProcess(object):
stride=4096, stride=4096,
fs=48000, fs=48000,
sample_width=2, sample_width=2,
range=[100,4000], range=[100, 4000],
callback=None callback=None,
): ):
self.nfft = nfft self.nfft = nfft
self.stride = stride self.stride = stride
@ -40,54 +41,46 @@ class FFTProcess(object):
def init_window(self): def init_window(self):
""" Initialise Window functions and FFT scales. """ """ Initialise Window functions and FFT scales. """
self.window = scipy.signal.blackmanharris(self.nfft) self.window = scipy.signal.blackmanharris(self.nfft)
self.fft_scale = np.fft.fftshift(np.fft.fftfreq(self.nfft))*self.fs self.fft_scale = np.fft.fftshift(np.fft.fftfreq(self.nfft)) * self.fs
self.mask = (self.fft_scale>self.range[0]) & (self.fft_scale<self.range[1]) self.mask = (self.fft_scale > self.range[0]) & (self.fft_scale < self.range[1])
def perform_fft(self): def perform_fft(self):
""" Perform a FFT on the first NFFT samples in the sample buffer, then shift the buffer along """ """ Perform a FFT on the first NFFT samples in the sample buffer, then shift the buffer along """
# Convert raw data to floats. # Convert raw data to floats.
raw_data = np.fromstring( raw_data = np.fromstring(
bytes(self.sample_buffer[ : self.nfft*self.sample_width]), dtype=np.int16 bytes(self.sample_buffer[: self.nfft * self.sample_width]), dtype=np.int16
) )
raw_data = raw_data.astype(np.float64) / (2**15) raw_data = raw_data.astype(np.float64) / (2 ** 15)
# Advance sample buffer # Advance sample buffer
self.sample_buffer = self.sample_buffer[self.stride * self.sample_width :] self.sample_buffer = self.sample_buffer[self.stride * self.sample_width :]
# Calculate FFT # Calculate FFT
_fft = 20*np.log10(np.abs(np.fft.fftshift(np.fft.fft(raw_data * self.window)))) - 20*np.log10(self.nfft) _fft = 20 * np.log10(
np.abs(np.fft.fftshift(np.fft.fft(raw_data * self.window)))
) - 20 * np.log10(self.nfft)
if self.callback != None: if self.callback != None:
self.callback( self.callback({"fft": _fft[self.mask], "scale": self.fft_scale[self.mask]})
{
'fft': _fft[self.mask],
'scale': self.fft_scale[self.mask]
}
)
def process_block(self, samples): def process_block(self, samples):
""" Add a block of samples to the input buffer. Calculate and process FFTs if the buffer is big enough """ """ Add a block of samples to the input buffer. Calculate and process FFTs if the buffer is big enough """
self.sample_buffer.extend(samples) self.sample_buffer.extend(samples)
while len(self.sample_buffer) > self.nfft*self.sample_width: while len(self.sample_buffer) > self.nfft * self.sample_width:
self.perform_fft() self.perform_fft()
def processing_thread(self): def processing_thread(self):
while self.processing_thread_running: while self.processing_thread_running:
if self.input_queue.qsize()>0: if self.input_queue.qsize() > 0:
data = self.input_queue.get() data = self.input_queue.get()
self.process_block(data) self.process_block(data)
else: else:
time.sleep(0.01) time.sleep(0.01)
def add_samples(self, samples): def add_samples(self, samples):
""" Add a block of samples to the input queue """ """ Add a block of samples to the input queue """
try: try:
@ -95,7 +88,6 @@ class FFTProcess(object):
except: except:
logging.error("Input overrun!") logging.error("Input overrun!")
def flush(self): def flush(self):
""" Clear the sample buffer """ """ Clear the sample buffer """
self.sample_buffer = bytearray(b"") self.sample_buffer = bytearray(b"")

Wyświetl plik

@ -31,9 +31,7 @@ from .config import *
from . import __version__ from . import __version__
# Setup Logging # Setup Logging
logging.basicConfig( logging.basicConfig(format="%(asctime)s %(levelname)s: %(message)s", level=logging.INFO)
format="%(asctime)s %(levelname)s: %(message)s", level=logging.INFO
)
# Global widget store # Global widget store
widgets = {} widgets = {}
@ -67,17 +65,17 @@ win.setCentralWidget(area)
win.setWindowTitle("Horus Telemetry GUI") win.setWindowTitle("Horus Telemetry GUI")
# Create multiple dock areas, for displaying our data. # Create multiple dock areas, for displaying our data.
d0 = Dock("Audio", size=(300,50)) d0 = Dock("Audio", size=(300, 50))
d0_modem = Dock("Modem", size=(300,80)) d0_modem = Dock("Modem", size=(300, 80))
d0_habitat = Dock("Habitat", size=(300,200)) d0_habitat = Dock("Habitat", size=(300, 200))
d0_other = Dock("Other", size=(300,100)) d0_other = Dock("Other", size=(300, 100))
d1 = Dock("Spectrum", size=(800,400)) d1 = Dock("Spectrum", size=(800, 400))
d2 = Dock("Modem Stats", size=(800,300)) d2 = Dock("Modem Stats", size=(800, 300))
d3 = Dock("Data",size=(800,50)) d3 = Dock("Data", size=(800, 50))
d4 = Dock("Log",size=(800,150)) d4 = Dock("Log", size=(800, 150))
# Arrange docks. # Arrange docks.
area.addDock(d0) area.addDock(d0)
area.addDock(d1, 'right', d0) area.addDock(d1, "right", d0)
area.addDock(d0_modem, "bottom", d0) area.addDock(d0_modem, "bottom", d0)
area.addDock(d0_habitat, "bottom", d0_modem) area.addDock(d0_habitat, "bottom", d0_modem)
area.addDock(d0_other, "below", d0_habitat) area.addDock(d0_other, "below", d0_habitat)
@ -90,152 +88,168 @@ d0_habitat.raiseDock()
# Controls # Controls
w1_audio = pg.LayoutWidget() w1_audio = pg.LayoutWidget()
# TNC Connection # TNC Connection
widgets['audioDeviceLabel'] = QtGui.QLabel("<b>Audio Device:</b>") widgets["audioDeviceLabel"] = QtGui.QLabel("<b>Audio Device:</b>")
widgets['audioDeviceSelector'] = QtGui.QComboBox() widgets["audioDeviceSelector"] = QtGui.QComboBox()
widgets['audioSampleRateLabel'] = QtGui.QLabel("<b>Sample Rate (Hz):</b>") widgets["audioSampleRateLabel"] = QtGui.QLabel("<b>Sample Rate (Hz):</b>")
widgets['audioSampleRateSelector'] = QtGui.QComboBox() widgets["audioSampleRateSelector"] = QtGui.QComboBox()
w1_audio.addWidget(widgets['audioDeviceLabel'], 0, 0, 1, 1) w1_audio.addWidget(widgets["audioDeviceLabel"], 0, 0, 1, 1)
w1_audio.addWidget(widgets['audioDeviceSelector'], 0, 1, 1, 1) w1_audio.addWidget(widgets["audioDeviceSelector"], 0, 1, 1, 1)
w1_audio.addWidget(widgets['audioSampleRateLabel'], 1, 0, 1, 1) w1_audio.addWidget(widgets["audioSampleRateLabel"], 1, 0, 1, 1)
w1_audio.addWidget(widgets['audioSampleRateSelector'], 1, 1, 1, 1) w1_audio.addWidget(widgets["audioSampleRateSelector"], 1, 1, 1, 1)
d0.addWidget(w1_audio) d0.addWidget(w1_audio)
w1_modem = pg.LayoutWidget() w1_modem = pg.LayoutWidget()
# Modem Parameters # Modem Parameters
widgets['horusModemLabel'] = QtGui.QLabel("<b>Mode:</b>") widgets["horusModemLabel"] = QtGui.QLabel("<b>Mode:</b>")
widgets['horusModemSelector'] = QtGui.QComboBox() widgets["horusModemSelector"] = QtGui.QComboBox()
widgets['horusModemRateLabel'] = QtGui.QLabel("<b>Baudrate:</b>") widgets["horusModemRateLabel"] = QtGui.QLabel("<b>Baudrate:</b>")
widgets['horusModemRateSelector'] = QtGui.QComboBox() widgets["horusModemRateSelector"] = QtGui.QComboBox()
widgets['horusMaskEstimatorLabel'] = QtGui.QLabel("<b>Enable Mask Estim.:</b>") widgets["horusMaskEstimatorLabel"] = QtGui.QLabel("<b>Enable Mask Estim.:</b>")
widgets['horusMaskEstimatorSelector'] = QtGui.QCheckBox() widgets["horusMaskEstimatorSelector"] = QtGui.QCheckBox()
widgets['horusMaskSpacingLabel'] = QtGui.QLabel("<b>Tone Spacing (Hz):</b>") widgets["horusMaskSpacingLabel"] = QtGui.QLabel("<b>Tone Spacing (Hz):</b>")
widgets['horusMaskSpacingEntry'] = QtGui.QLineEdit("270") widgets["horusMaskSpacingEntry"] = QtGui.QLineEdit("270")
# Start/Stop # Start/Stop
widgets['startDecodeButton'] = QtGui.QPushButton("Start") widgets["startDecodeButton"] = QtGui.QPushButton("Start")
w1_modem.addWidget(widgets['horusModemLabel'], 0, 0, 1, 1) w1_modem.addWidget(widgets["horusModemLabel"], 0, 0, 1, 1)
w1_modem.addWidget(widgets['horusModemSelector'], 0, 1, 1, 1) w1_modem.addWidget(widgets["horusModemSelector"], 0, 1, 1, 1)
w1_modem.addWidget(widgets['horusModemRateLabel'], 1, 0, 1, 1) w1_modem.addWidget(widgets["horusModemRateLabel"], 1, 0, 1, 1)
w1_modem.addWidget(widgets['horusModemRateSelector'], 1, 1, 1, 1) w1_modem.addWidget(widgets["horusModemRateSelector"], 1, 1, 1, 1)
w1_modem.addWidget(widgets['horusMaskEstimatorLabel'], 2, 0, 1, 1) w1_modem.addWidget(widgets["horusMaskEstimatorLabel"], 2, 0, 1, 1)
w1_modem.addWidget(widgets['horusMaskEstimatorSelector'], 2, 1, 1, 1) w1_modem.addWidget(widgets["horusMaskEstimatorSelector"], 2, 1, 1, 1)
w1_modem.addWidget(widgets['horusMaskSpacingLabel'], 3, 0, 1, 1) w1_modem.addWidget(widgets["horusMaskSpacingLabel"], 3, 0, 1, 1)
w1_modem.addWidget(widgets['horusMaskSpacingEntry'], 3, 1, 1, 1) w1_modem.addWidget(widgets["horusMaskSpacingEntry"], 3, 1, 1, 1)
w1_modem.addWidget(widgets['startDecodeButton'], 4, 0, 2, 2) w1_modem.addWidget(widgets["startDecodeButton"], 4, 0, 2, 2)
d0_modem.addWidget(w1_modem) d0_modem.addWidget(w1_modem)
w1_habitat = pg.LayoutWidget() w1_habitat = pg.LayoutWidget()
# Listener Information # Listener Information
widgets['habitatHeading'] = QtGui.QLabel("<b>Habitat Settings</b>") widgets["habitatHeading"] = QtGui.QLabel("<b>Habitat Settings</b>")
widgets['habitatUploadLabel'] = QtGui.QLabel("<b>Enable Habitat Upload:</b>") widgets["habitatUploadLabel"] = QtGui.QLabel("<b>Enable Habitat Upload:</b>")
widgets['habitatUploadSelector'] = QtGui.QCheckBox() widgets["habitatUploadSelector"] = QtGui.QCheckBox()
widgets['habitatUploadSelector'].setChecked(True) widgets["habitatUploadSelector"].setChecked(True)
widgets['userCallLabel'] = QtGui.QLabel("<b>Callsign:</b>") widgets["userCallLabel"] = QtGui.QLabel("<b>Callsign:</b>")
widgets['userCallEntry'] = QtGui.QLineEdit("N0CALL") widgets["userCallEntry"] = QtGui.QLineEdit("N0CALL")
widgets['userCallEntry'].setMaxLength(20) widgets["userCallEntry"].setMaxLength(20)
widgets['userLocationLabel'] = QtGui.QLabel("<b>Lat/Lon:</b>") widgets["userLocationLabel"] = QtGui.QLabel("<b>Lat/Lon:</b>")
widgets['userLatEntry'] = QtGui.QLineEdit("0.0") widgets["userLatEntry"] = QtGui.QLineEdit("0.0")
widgets['userLonEntry'] = QtGui.QLineEdit("0.0") widgets["userLonEntry"] = QtGui.QLineEdit("0.0")
widgets['userAntennaLabel'] = QtGui.QLabel("<b>Antenna:</b>") widgets["userAntennaLabel"] = QtGui.QLabel("<b>Antenna:</b>")
widgets['userAntennaEntry'] = QtGui.QLineEdit("") widgets["userAntennaEntry"] = QtGui.QLineEdit("")
widgets['userRadioLabel'] = QtGui.QLabel("<b>Radio:</b>") widgets["userRadioLabel"] = QtGui.QLabel("<b>Radio:</b>")
widgets['userRadioEntry'] = QtGui.QLineEdit("Horus-GUI " + __version__) widgets["userRadioEntry"] = QtGui.QLineEdit("Horus-GUI " + __version__)
w1_habitat.addWidget(widgets['habitatUploadLabel'],0, 0, 1, 1) w1_habitat.addWidget(widgets["habitatUploadLabel"], 0, 0, 1, 1)
w1_habitat.addWidget(widgets['habitatUploadSelector'],0, 1, 1, 1) w1_habitat.addWidget(widgets["habitatUploadSelector"], 0, 1, 1, 1)
w1_habitat.addWidget(widgets['userCallLabel'], 1, 0, 1, 1) w1_habitat.addWidget(widgets["userCallLabel"], 1, 0, 1, 1)
w1_habitat.addWidget(widgets['userCallEntry'], 1, 1, 1, 2) w1_habitat.addWidget(widgets["userCallEntry"], 1, 1, 1, 2)
w1_habitat.addWidget(widgets['userLocationLabel'], 2, 0, 1, 1) w1_habitat.addWidget(widgets["userLocationLabel"], 2, 0, 1, 1)
w1_habitat.addWidget(widgets['userLatEntry'], 2, 1, 1, 1) w1_habitat.addWidget(widgets["userLatEntry"], 2, 1, 1, 1)
w1_habitat.addWidget(widgets['userLonEntry'], 2, 2, 1, 1) w1_habitat.addWidget(widgets["userLonEntry"], 2, 2, 1, 1)
w1_habitat.addWidget(widgets['userAntennaLabel'], 3, 0, 1, 1) w1_habitat.addWidget(widgets["userAntennaLabel"], 3, 0, 1, 1)
w1_habitat.addWidget(widgets['userAntennaEntry'], 3, 1, 1, 2) w1_habitat.addWidget(widgets["userAntennaEntry"], 3, 1, 1, 2)
w1_habitat.addWidget(widgets['userRadioLabel'], 4, 0, 1, 1) w1_habitat.addWidget(widgets["userRadioLabel"], 4, 0, 1, 1)
w1_habitat.addWidget(widgets['userRadioEntry'], 4, 1, 1, 2) w1_habitat.addWidget(widgets["userRadioEntry"], 4, 1, 1, 2)
d0_habitat.addWidget(w1_habitat) d0_habitat.addWidget(w1_habitat)
w1_other = pg.LayoutWidget() w1_other = pg.LayoutWidget()
widgets['horusUploadLabel'] = QtGui.QLabel("<b>Enable Horus UDP Output:</b>") widgets["horusUploadLabel"] = QtGui.QLabel("<b>Enable Horus UDP Output:</b>")
widgets['horusUploadSelector'] = QtGui.QCheckBox() widgets["horusUploadSelector"] = QtGui.QCheckBox()
widgets['horusUploadSelector'].setChecked(True) widgets["horusUploadSelector"].setChecked(True)
widgets['horusUDPLabel'] = QtGui.QLabel("<b>Horus UDP Port:</b>") widgets["horusUDPLabel"] = QtGui.QLabel("<b>Horus UDP Port:</b>")
widgets['horusUDPEntry'] = QtGui.QLineEdit("55672") widgets["horusUDPEntry"] = QtGui.QLineEdit("55672")
widgets['horusUDPEntry'].setMaxLength(5) widgets["horusUDPEntry"].setMaxLength(5)
w1_other.addWidget(widgets['horusUploadLabel'], 0, 0, 1, 1) w1_other.addWidget(widgets["horusUploadLabel"], 0, 0, 1, 1)
w1_other.addWidget(widgets['horusUploadSelector'], 0, 1, 1, 1) w1_other.addWidget(widgets["horusUploadSelector"], 0, 1, 1, 1)
w1_other.addWidget(widgets['horusUDPLabel'], 1, 0, 1, 1) w1_other.addWidget(widgets["horusUDPLabel"], 1, 0, 1, 1)
w1_other.addWidget(widgets['horusUDPEntry'], 1, 1, 1, 1) w1_other.addWidget(widgets["horusUDPEntry"], 1, 1, 1, 1)
d0_other.addWidget(w1_other) d0_other.addWidget(w1_other)
# Spectrum Display # Spectrum Display
widgets['spectrumPlot'] = pg.PlotWidget(title="Spectra") widgets["spectrumPlot"] = pg.PlotWidget(title="Spectra")
widgets['spectrumPlot'].setLabel("left", "Power (dB)") widgets["spectrumPlot"].setLabel("left", "Power (dB)")
widgets['spectrumPlot'].setLabel("bottom", "Frequency (Hz)") widgets["spectrumPlot"].setLabel("bottom", "Frequency (Hz)")
widgets['spectrumPlotData']= widgets['spectrumPlot'].plot([0]) widgets["spectrumPlotData"] = widgets["spectrumPlot"].plot([0])
widgets['spectrumPlot'].setLabel('left', "Power (dBFs)") widgets["spectrumPlot"].setLabel("left", "Power (dBFs)")
widgets['spectrumPlot'].setLabel('bottom', "Frequency", units="Hz") widgets["spectrumPlot"].setLabel("bottom", "Frequency", units="Hz")
widgets['spectrumPlot'].setXRange(100,4000) widgets["spectrumPlot"].setXRange(100, 4000)
widgets['spectrumPlot'].setYRange(-100,-20) widgets["spectrumPlot"].setYRange(-100, -20)
widgets['spectrumPlot'].setLimits(xMin=0, xMax=4000, yMin=-120, yMax=0) widgets["spectrumPlot"].setLimits(xMin=0, xMax=4000, yMin=-120, yMax=0)
# Frequency Estiator Outputs # Frequency Estiator Outputs
widgets['estimatorLines'] = [ widgets["estimatorLines"] = [
pg.InfiniteLine(pos=-1000,pen=pg.mkPen(color='w', width=2, style=QtCore.Qt.DashLine), label='F1'), pg.InfiniteLine(
pg.InfiniteLine(pos=-1000,pen=pg.mkPen(color='w', width=2, style=QtCore.Qt.DashLine), label='F2'), pos=-1000,
pg.InfiniteLine(pos=-1000,pen=pg.mkPen(color='w', width=2, style=QtCore.Qt.DashLine), label='F3'), pen=pg.mkPen(color="w", width=2, style=QtCore.Qt.DashLine),
pg.InfiniteLine(pos=-1000,pen=pg.mkPen(color='w', width=2, style=QtCore.Qt.DashLine), label='F4') 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",
),
] ]
for _line in widgets['estimatorLines']: for _line in widgets["estimatorLines"]:
widgets['spectrumPlot'].addItem(_line) widgets["spectrumPlot"].addItem(_line)
d1.addWidget(widgets['spectrumPlot']) d1.addWidget(widgets["spectrumPlot"])
widgets['spectrumPlotRange'] = [-100, -20] widgets["spectrumPlotRange"] = [-100, -20]
# Waterfall - TBD # Waterfall - TBD
w3 = pg.LayoutWidget() w3 = pg.LayoutWidget()
widgets['snrPlot'] = pg.PlotWidget(title="SNR") widgets["snrPlot"] = pg.PlotWidget(title="SNR")
widgets['snrPlot'].setLabel("left", "SNR (dB)") widgets["snrPlot"].setLabel("left", "SNR (dB)")
widgets['snrPlot'].setLabel("bottom", "Time (s)") widgets["snrPlot"].setLabel("bottom", "Time (s)")
widgets['snrPlot'].setXRange(-60,0) widgets["snrPlot"].setXRange(-60, 0)
widgets['snrPlot'].setYRange(-10,30) widgets["snrPlot"].setYRange(-10, 30)
widgets['snrPlot'].setLimits(xMin=0, xMax=60, yMin=-100, yMax=40) widgets["snrPlot"].setLimits(xMin=0, xMax=60, yMin=-100, yMax=40)
widgets['snrPlotRange'] = [-10, 30] widgets["snrPlotRange"] = [-10, 30]
widgets['eyeDiagramPlot'] = pg.PlotWidget(title="Eye Diagram") widgets["eyeDiagramPlot"] = pg.PlotWidget(title="Eye Diagram")
w3.addWidget(widgets['snrPlot'],0,0) w3.addWidget(widgets["snrPlot"], 0, 0)
w3.addWidget(widgets['eyeDiagramPlot'],0,1) w3.addWidget(widgets["eyeDiagramPlot"], 0, 1)
d2.addWidget(w3) d2.addWidget(w3)
# Telemetry Data # Telemetry Data
w4 = pg.LayoutWidget() w4 = pg.LayoutWidget()
widgets['latestSentenceLabel'] = QtGui.QLabel("<b>Latest Sentence:</b>") widgets["latestSentenceLabel"] = QtGui.QLabel("<b>Latest Sentence:</b>")
widgets['latestSentenceData'] = QtGui.QLabel("NO DATA") widgets["latestSentenceData"] = QtGui.QLabel("NO DATA")
widgets['latestSentenceData'].setFont(QtGui.QFont("Courier New", 18, QtGui.QFont.Bold)) widgets["latestSentenceData"].setFont(QtGui.QFont("Courier New", 18, QtGui.QFont.Bold))
w4.addWidget(widgets['latestSentenceLabel'], 0, 0, 1, 1) w4.addWidget(widgets["latestSentenceLabel"], 0, 0, 1, 1)
w4.addWidget(widgets['latestSentenceData'], 0, 1, 1, 6) w4.addWidget(widgets["latestSentenceData"], 0, 1, 1, 6)
d3.addWidget(w4) d3.addWidget(w4)
w5 = pg.LayoutWidget() w5 = pg.LayoutWidget()
widgets['console'] = QtWidgets.QPlainTextEdit() widgets["console"] = QtWidgets.QPlainTextEdit()
widgets['console'].setReadOnly(True) widgets["console"].setReadOnly(True)
w5.addWidget(widgets['console']) w5.addWidget(widgets["console"])
d4.addWidget(w5) d4.addWidget(w5)
# Resize window to final resolution, and display. # Resize window to final resolution, and display.
@ -243,52 +257,56 @@ logging.info("Starting GUI.")
win.resize(1500, 800) win.resize(1500, 800)
win.show() win.show()
# Audio Initialization # Audio Initialization
audio_devices = init_audio(widgets) audio_devices = init_audio(widgets)
def update_audio_sample_rates(): def update_audio_sample_rates():
""" Update the sample-rate dropdown when a different audio device is selected. """ """ Update the sample-rate dropdown when a different audio device is selected. """
global widgets global widgets
# Pass widgets straight on to function from .audio # Pass widgets straight on to function from .audio
populate_sample_rates(widgets) populate_sample_rates(widgets)
widgets['audioDeviceSelector'].currentIndexChanged.connect(update_audio_sample_rates)
widgets["audioDeviceSelector"].currentIndexChanged.connect(update_audio_sample_rates)
# Initialize modem list. # Initialize modem list.
init_horus_modem(widgets) init_horus_modem(widgets)
def update_modem_settings(): def update_modem_settings():
""" Update the modem setting widgets when a different modem is selected """ """ Update the modem setting widgets when a different modem is selected """
global widgets global widgets
populate_modem_settings(widgets) populate_modem_settings(widgets)
widgets['horusModemSelector'].currentIndexChanged.connect(update_modem_settings)
widgets["horusModemSelector"].currentIndexChanged.connect(update_modem_settings)
# Read in configuration file settings # Read in configuration file settings
read_config(widgets) read_config(widgets)
def handle_fft_update(data): def handle_fft_update(data):
""" Handle a new FFT update """ """ Handle a new FFT update """
global widgets global widgets
_scale = data['scale'] _scale = data["scale"]
_data = data['fft'] _data = data["fft"]
widgets['spectrumPlotData'].setData(_scale, _data) widgets["spectrumPlotData"].setData(_scale, _data)
# Really basic IIR to smoothly adjust scale # Really basic IIR to smoothly adjust scale
_old_max = widgets['spectrumPlotRange'][1] _old_max = widgets["spectrumPlotRange"][1]
_tc = 0.1 _tc = 0.1
_new_max = float((_old_max*(1-_tc)) + (np.max(_data)*_tc)) _new_max = float((_old_max * (1 - _tc)) + (np.max(_data) * _tc))
# Store new max # Store new max
widgets['spectrumPlotRange'][1] = _new_max widgets["spectrumPlotRange"][1] = _new_max
widgets['spectrumPlot'].setYRange(widgets['spectrumPlotRange'][0], min(0,_new_max)+20)
widgets["spectrumPlot"].setYRange(
widgets["spectrumPlotRange"][0], min(0, _new_max) + 20
)
def add_fft_update(data): def add_fft_update(data):
@ -305,19 +323,15 @@ def start_decoding():
if not running: if not running:
# Grab settings off widgets # Grab settings off widgets
_dev_name = widgets['audioDeviceSelector'].currentText() _dev_name = widgets["audioDeviceSelector"].currentText()
_sample_rate = int(widgets['audioSampleRateSelector'].currentText()) _sample_rate = int(widgets["audioSampleRateSelector"].currentText())
_dev_index = audio_devices[_dev_name]['index'] _dev_index = audio_devices[_dev_name]["index"]
# TODO: Grab horus data here. # TODO: Grab horus data here.
# Init FFT Processor # Init FFT Processor
fft_process = FFTProcess( fft_process = FFTProcess(
nfft=8192, nfft=8192, stride=4096, fs=_sample_rate, callback=add_fft_update
stride=4096,
fs=_sample_rate,
callback=add_fft_update
) )
# TODO: Setup modem here # TODO: Setup modem here
@ -325,13 +339,13 @@ def start_decoding():
# Setup Audio # Setup Audio
audio_stream = AudioStream( audio_stream = AudioStream(
_dev_index, _dev_index,
fs = _sample_rate, fs=_sample_rate,
block_size=fft_process.stride, block_size=fft_process.stride,
fft_input = fft_process.add_samples, fft_input=fft_process.add_samples,
modem=None modem=None,
) )
widgets['startDecodeButton'].setText('Stop') widgets["startDecodeButton"].setText("Stop")
running = True running = True
logging.info("Started Audio Processing.") logging.info("Started Audio Processing.")
@ -341,7 +355,7 @@ def start_decoding():
audio_stream.stop() audio_stream.stop()
except Exception as e: except Exception as e:
logging.exception("Could not stop audio stream.", exc_info=e) logging.exception("Could not stop audio stream.", exc_info=e)
try: try:
fft_process.stop() fft_process.stop()
except Exception as e: except Exception as e:
@ -350,14 +364,13 @@ def start_decoding():
fft_update_queue = Queue(256) fft_update_queue = Queue(256)
status_update_queue = Queue(256) status_update_queue = Queue(256)
widgets['startDecodeButton'].setText('Start') widgets["startDecodeButton"].setText("Start")
running = False running = False
logging.info("Stopped Audio Processing.") logging.info("Stopped Audio Processing.")
widgets['startDecodeButton'].clicked.connect(start_decoding) widgets["startDecodeButton"].clicked.connect(start_decoding)
# GUI Update Loop # GUI Update Loop
@ -382,6 +395,7 @@ gui_update_timer.start(100)
class ConsoleHandler(logging.Handler): class ConsoleHandler(logging.Handler):
""" Logging handler to write to the GUI console """ """ Logging handler to write to the GUI console """
def __init__(self, consolewidget): def __init__(self, consolewidget):
logging.Handler.__init__(self) logging.Handler.__init__(self)
self.consolewidget = consolewidget self.consolewidget = consolewidget
@ -391,8 +405,9 @@ class ConsoleHandler(logging.Handler):
_text = f"{record.levelname} {_time.strftime('%H:%M:%S')}:\t{record.msg}" _text = f"{record.levelname} {_time.strftime('%H:%M:%S')}:\t{record.msg}"
self.consolewidget.appendPlainText(_text) self.consolewidget.appendPlainText(_text)
# Add console handler to top level logger. # Add console handler to top level logger.
console_handler = ConsoleHandler(widgets['console']) console_handler = ConsoleHandler(widgets["console"])
logging.getLogger().addHandler(console_handler) logging.getLogger().addHandler(console_handler)
@ -405,12 +420,12 @@ def main():
if (sys.flags.interactive != 1) or not hasattr(QtCore, "PYQT_VERSION"): if (sys.flags.interactive != 1) or not hasattr(QtCore, "PYQT_VERSION"):
QtGui.QApplication.instance().exec_() QtGui.QApplication.instance().exec_()
save_config(widgets) save_config(widgets)
try: try:
audio_stream.stop() audio_stream.stop()
except Exception as e: except Exception as e:
pass pass
try: try:
fft_process.stop() fft_process.stop()
except Exception as e: except Exception as e:
@ -419,5 +434,3 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
main() main()

Wyświetl plik

@ -4,38 +4,39 @@ import logging
# Modem paramers and defaults # Modem paramers and defaults
HORUS_MODEM_LIST = { HORUS_MODEM_LIST = {
'Horus Binary v1 (Legacy)': { "Horus Binary v1 (Legacy)": {
'id': 0, "id": 0,
'baud_rates': [50, 100, 300], "baud_rates": [50, 100, 300],
'default_baud_rate': 100, "default_baud_rate": 100,
'default_tone_spacing': 270, "default_tone_spacing": 270,
'use_mask_estimator': False "use_mask_estimator": False,
},
"RTTY (7N2)": {
"id": 99,
"baud_rates": [50, 100, 300, 600, 1000],
"default_baud_rate": 100,
"default_tone_spacing": 425,
"use_mask_estimator": False,
}, },
'RTTY (7N2)': {
'id': 99,
'baud_rates': [50, 100, 300, 600, 1000],
'default_baud_rate': 100,
'default_tone_spacing': 425,
'use_mask_estimator': False
}
} }
DEFAULT_MODEM = 'Horus Binary v1 (Legacy)' DEFAULT_MODEM = "Horus Binary v1 (Legacy)"
horusModem = None horusModem = None
def init_horus_modem(widgets): def init_horus_modem(widgets):
""" Initialise the modem drop-down lists """ """ Initialise the modem drop-down lists """
# Clear modem list. # Clear modem list.
widgets['horusModemSelector'].clear() widgets["horusModemSelector"].clear()
# Add items from modem list # Add items from modem list
for _modem in HORUS_MODEM_LIST: for _modem in HORUS_MODEM_LIST:
widgets['horusModemSelector'].addItem(_modem) widgets["horusModemSelector"].addItem(_modem)
# Select default modem # Select default modem
widgets['horusModemSelector'].setCurrentText(DEFAULT_MODEM) widgets["horusModemSelector"].setCurrentText(DEFAULT_MODEM)
populate_modem_settings(widgets) populate_modem_settings(widgets)
@ -43,21 +44,26 @@ def init_horus_modem(widgets):
def populate_modem_settings(widgets): def populate_modem_settings(widgets):
""" Populate the modem settings for the current selected modem """ """ Populate the modem settings for the current selected modem """
_current_modem = widgets['horusModemSelector'].currentText() _current_modem = widgets["horusModemSelector"].currentText()
# Clear baud rate dropdown. # Clear baud rate dropdown.
widgets['horusModemRateSelector'].clear() widgets["horusModemRateSelector"].clear()
# Populate # Populate
for _rate in HORUS_MODEM_LIST[_current_modem]['baud_rates']: for _rate in HORUS_MODEM_LIST[_current_modem]["baud_rates"]:
widgets['horusModemRateSelector'].addItem(str(_rate)) widgets["horusModemRateSelector"].addItem(str(_rate))
# Select default rate. # Select default rate.
widgets['horusModemRateSelector'].setCurrentText(str(HORUS_MODEM_LIST[_current_modem]['default_baud_rate'])) widgets["horusModemRateSelector"].setCurrentText(
str(HORUS_MODEM_LIST[_current_modem]["default_baud_rate"])
)
# Set Mask Estimator checkbox. # Set Mask Estimator checkbox.
widgets['horusMaskEstimatorSelector'].setChecked(HORUS_MODEM_LIST[_current_modem]['use_mask_estimator']) widgets["horusMaskEstimatorSelector"].setChecked(
HORUS_MODEM_LIST[_current_modem]["use_mask_estimator"]
)
# Set Tone Spacing Input Box # Set Tone Spacing Input Box
widgets['horusMaskSpacingEntry'].setText(str(HORUS_MODEM_LIST[_current_modem]['default_tone_spacing'])) widgets["horusMaskSpacingEntry"].setText(
str(HORUS_MODEM_LIST[_current_modem]["default_tone_spacing"])
)

Wyświetl plik

@ -7,4 +7,3 @@ class QHLine(QtGui.QFrame):
super(QHLine, self).__init__() super(QHLine, self).__init__()
self.setFrameShape(QtGui.QFrame.HLine) self.setFrameShape(QtGui.QFrame.HLine)
self.setFrameShadow(QtGui.QFrame.Sunken) self.setFrameShadow(QtGui.QFrame.Sunken)