Merge pull request #46 from ke5gdb/pyqt6

Update Horus GUI to PyQt6
pull/53/head v0.4.0
Mark Jessop 2025-01-31 09:49:37 +10:30 zatwierdzone przez GitHub
commit 6fec273eaf
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
13 zmienionych plików z 1547 dodań i 1673 usunięć

Wyświetl plik

@ -56,6 +56,7 @@ $ cd horusdemodlib && mkdir build && cd build
$ cmake ..
$ make
$ sudo make install
$ sudo ldconfig
```
### Grab this Repo

Wyświetl plik

@ -1 +1 @@
__version__ = "0.3.19"
__version__ = "0.4.0"

Wyświetl plik

@ -1,6 +1,7 @@
# Audio Interfacing
import logging
import pyaudio
import time
# Global PyAudio object
@ -125,8 +126,14 @@ class AudioStream(object):
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 @@ class AudioStream(object):
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 @@ class AudioStream(object):
# 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()

Wyświetl plik

@ -34,9 +34,11 @@ default_config = {
"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)

Wyświetl plik

@ -3,7 +3,7 @@ import logging
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 @@ class FFTProcess(object):
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 @@ class FFTProcess(object):
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 @@ class FFTProcess(object):
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 @@ class FFTProcess(object):
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:

Plik diff jest za duży Load Diff

Wyświetl plik

@ -1,353 +0,0 @@
#!/usr/bin/env python
#
# Horus Telemetry GUI - Habitat Uploader
#
# Mark Jessop <vk5qi@rfhead.net>
#
import datetime
import json
import logging
import random
import requests
import time
from base64 import b64encode
from hashlib import sha256
from queue import Queue
from threading import Thread
class HabitatUploader(object):
"""
Queued Habitat Telemetry Uploader class
Packets to be uploaded to Habitat are added to a queue for uploading.
If an upload attempt times out, the packet is discarded.
If the queue fills up (probably indicating no network connection, and a fast packet downlink rate),
it is immediately emptied, to avoid upload of out-of-date packets.
"""
HABITAT_URL = "http://habitat.habhub.org/"
HABITAT_DB = "habitat"
HABITAT_UUIDS = HABITAT_URL + "_uuids?count=%d"
HABITAT_DB_URL = HABITAT_URL + HABITAT_DB + "/"
def __init__(
self,
user_callsign="FSK_DEMOD",
listener_lat=0.0,
listener_lon=0.0,
listener_radio="",
listener_antenna="",
queue_size=64,
upload_timeout=10,
upload_retries=5,
upload_retry_interval=0.25,
inhibit=False,
):
""" Create a Habitat Uploader object. """
self.upload_timeout = upload_timeout
self.upload_retries = upload_retries
self.upload_retry_interval = upload_retry_interval
self.queue_size = queue_size
self.habitat_upload_queue = Queue(queue_size)
self.inhibit = inhibit
# Listener information
self.user_callsign = user_callsign
self.listener_lat = listener_lat
self.listener_lon = listener_lon
self.listener_radio = listener_radio
self.listener_antenna = listener_antenna
self.position_uploaded = False
self.last_freq_hz = None
self.callsign_init = False
self.uuids = []
# Start the uploader thread.
self.habitat_uploader_running = True
self.uploadthread = Thread(target=self.habitat_upload_thread)
self.uploadthread.start()
def habitat_upload(self, sentence):
""" Upload a UKHAS-standard telemetry sentence to Habitat """
# Generate payload to be uploaded
# b64encode accepts and returns bytes objects.
_sentence_b64 = b64encode(sentence.encode("ascii"))
_date = datetime.datetime.utcnow().isoformat("T") + "Z"
_user_call = self.user_callsign
_data = {
"type": "payload_telemetry",
"data": {
"_raw": _sentence_b64.decode(
"ascii"
) # Convert back to a string to be serialisable
},
"receivers": {
_user_call: {"time_created": _date, "time_uploaded": _date,},
},
}
if self.last_freq_hz:
# Add in frequency information if we have it.
_data["receivers"][_user_call]["rig_info"] = {"frequency": self.last_freq_hz}
# The URl to upload to.
_url = f"{self.HABITAT_URL}{self.HABITAT_DB}/_design/payload_telemetry/_update/add_listener/{sha256(_sentence_b64).hexdigest()}"
# Delay for a random amount of time between 0 and upload_retry_interval*2 seconds.
time.sleep(random.random() * self.upload_retry_interval * 2.0)
_retries = 0
# When uploading, we have three possible outcomes:
# - Can't connect. No point re-trying in this situation.
# - The packet is uploaded successfult (201 / 403)
# - There is a upload conflict on the Habitat DB end (409). We can retry and it might work.
while _retries < self.upload_retries:
# Run the request.
try:
_req = requests.put(
_url, data=json.dumps(_data), timeout=(self.upload_timeout, 6.1)
)
except Exception as e:
logging.error("Habitat - Upload Failed: %s" % str(e))
break
if _req.status_code == 201 or _req.status_code == 403:
# 201 = Success, 403 = Success, sentence has already seen by others.
logging.info(f"Habitat - Uploaded sentence: {sentence.strip()}")
_upload_success = True
break
elif _req.status_code == 409:
# 409 = Upload conflict (server busy). Sleep for a moment, then retry.
logging.debug("Habitat - Upload conflict.. retrying.")
time.sleep(random.random() * self.upload_retry_interval)
_retries += 1
else:
logging.error(
"Habitat - Error uploading to Habitat. Status Code: %d."
% _req.status_code
)
break
if _retries == self.upload_retries:
logging.error(
"Habitat - Upload conflict not resolved with %d retries."
% self.upload_retries
)
return
def habitat_upload_thread(self):
""" Handle uploading of packets to Habitat """
logging.info("Started Habitat Uploader Thread.")
while self.habitat_uploader_running:
if self.habitat_upload_queue.qsize() > 0:
# If the queue is completely full, jump to the most recent telemetry sentence.
if self.habitat_upload_queue.qsize() == self.queue_size:
while not self.habitat_upload_queue.empty():
sentence = self.habitat_upload_queue.get()
logging.warning(
"Habitat uploader queue was full - possible connectivity issue."
)
else:
# Otherwise, get the first item in the queue.
sentence = self.habitat_upload_queue.get()
# Attempt to upload it.
self.habitat_upload(sentence)
else:
# Wait for a short time before checking the queue again.
time.sleep(0.5)
#
# Habitat listener position update disabled 2022-09, due to Habitat going away...
#
# if not self.position_uploaded:
# # Validate the lat/lon entries.
# try:
# _lat = float(self.listener_lat)
# _lon = float(self.listener_lon)
# if (_lat != 0.0) or (_lon != 0.0):
# _success = self.uploadListenerPosition(
# self.user_callsign,
# _lat,
# _lon,
# self.listener_radio,
# self.listener_antenna,
# )
# else:
# logging.warning("Listener position set to 0.0/0.0 - not uploading.")
# except Exception as e:
# logging.error("Error uploading listener position: %s" % str(e))
# # Set this flag regardless if the upload worked.
# # The user can trigger a re-upload.
# self.position_uploaded = True
logging.info("Stopped Habitat Uploader Thread.")
def add(self, sentence):
""" Add a sentence to the upload queue """
if self.inhibit:
# We have upload inhibited. Return.
return
# Handling of arbitrary numbers of $$'s at the start of a sentence:
# Extract the data part of the sentence (i.e. everything after the $$'s')
sentence = sentence.split("$")[-1]
# Now add the *correct* number of $$s back on.
sentence = "$$" + sentence
if not (sentence[-1] == "\n"):
sentence += "\n"
try:
self.habitat_upload_queue.put_nowait(sentence)
except Exception as e:
logging.error("Error adding sentence to queue: %s" % str(e))
def close(self):
""" Shutdown uploader thread. """
self.habitat_uploader_running = False
def ISOStringNow(self):
return "%sZ" % datetime.datetime.utcnow().isoformat()
def postListenerData(self, doc, timeout=10):
# do we have at least one uuid, if not go get more
if len(self.uuids) < 1:
self.fetchUuids()
# Attempt to add UUID and time data to document.
try:
doc["_id"] = self.uuids.pop()
except IndexError:
logging.error(
"Habitat - Unable to post listener data - no UUIDs available."
)
return False
doc["time_uploaded"] = self.ISOStringNow()
try:
_r = requests.post(
f"{self.HABITAT_URL}{self.HABITAT_DB}/", json=doc, timeout=timeout
)
return True
except Exception as e:
logging.error("Habitat - Could not post listener data - %s" % str(e))
return False
def fetchUuids(self, timeout=10):
_retries = 5
while _retries > 0:
try:
_r = requests.get(self.HABITAT_UUIDS % 10, timeout=timeout)
self.uuids.extend(_r.json()["uuids"])
logging.debug("Habitat - Got UUIDs")
return
except Exception as e:
logging.error(
"Habitat - Unable to fetch UUIDs, retrying in 2 seconds - %s"
% str(e)
)
time.sleep(2)
_retries = _retries - 1
continue
logging.error("Habitat - Gave up trying to get UUIDs.")
return
def initListenerCallsign(self, callsign, radio="", antenna=""):
doc = {
"type": "listener_information",
"time_created": self.ISOStringNow(),
"data": {"callsign": callsign, "antenna": antenna, "radio": radio,},
}
resp = self.postListenerData(doc)
if resp is True:
logging.debug("Habitat - Listener Callsign Initialized.")
return True
else:
logging.error("Habitat - Unable to initialize callsign.")
return False
def uploadListenerPosition(self, callsign, lat, lon, radio="", antenna=""):
""" Initializer Listener Callsign, and upload Listener Position """
# Attempt to initialize the listeners callsign
resp = self.initListenerCallsign(callsign, radio=radio, antenna=antenna)
# If this fails, it means we can't contact the Habitat server,
# so there is no point continuing.
if resp is False:
return False
doc = {
"type": "listener_telemetry",
"time_created": self.ISOStringNow(),
"data": {
"callsign": callsign,
"chase": False,
"latitude": lat,
"longitude": lon,
"altitude": 0,
"speed": 0,
},
}
# post position to habitat
resp = self.postListenerData(doc)
if resp is True:
logging.info("Habitat - Listener information uploaded.")
return True
else:
logging.error("Habitat - Unable to upload listener information.")
return False
def trigger_position_upload(self):
""" Trigger a re-upload of the listener position """
self.position_uploaded = False
if __name__ == "__main__":
# Setup Logging
logging.basicConfig(
format="%(asctime)s %(levelname)s: %(message)s", level=logging.INFO
)
habitat = HabitatUploader(
user_callsign="HORUSGUI_TEST",
listener_lat=-34.0,
listener_lon=138.0,
listener_radio="Testing Habitat Uploader",
listener_antenna="Wet Noodle",
)
habitat.add("$$DUMMY,0,0.0,0.0*F000")
time.sleep(10)
habitat.trigger_position_upload()
time.sleep(5)
habitat.close()

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -9,7 +9,7 @@ import socket
import time
import logging
import traceback
from threading import Thread
# from threading import Thread
class ROTCTLD(object):
""" rotctld (hamlib) communication class """
@ -112,11 +112,11 @@ class PSTRotator(object):
self.poll_rate = poll_rate
self.azel_thread_running = True
self.t_rx = Thread(target=self.azel_rx_loop)
self.t_rx.start()
# self.t_rx = Thread(target=self.azel_rx_loop)
# self.t_rx.start()
self.t_poll = Thread(target=self.azel_poll_loop)
self.t_poll.start()
# self.t_poll = Thread(target=self.azel_poll_loop)
# self.t_poll.start()
def close(self):
@ -157,13 +157,13 @@ class PSTRotator(object):
except:
pass
def azel_poll_loop(self):
def azel_poll_loop(self, info_callback=None):
while self.azel_thread_running:
self.poll_azel()
logging.debug("Poll sent to PSTRotator.")
time.sleep(self.poll_rate)
def azel_rx_loop(self):
def azel_rx_loop(self, info_callback=None):
""" Listen for Azimuth and Elevation reports from PSTRotator"""
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.settimeout(1)

Wyświetl plik

@ -1,7 +1,8 @@
# UDP Audio Source (Obtaining audio from GQRX)
import socket
import traceback
from threading import Thread
#from threading import Thread
class UDPStream(object):
""" Listen for UDP Audio data from GQRX (s16, 48kHz), and pass data around to different callbacks """
@ -19,13 +20,16 @@ class UDPStream(object):
# Start audio stream
self.listen_thread_running = True
self.listen_thread = Thread(target=self.udp_listen_thread)
self.listen_thread.start()
#self.listen_thread = Thread(target=self.udp_listen_thread)
#self.listen_thread.start()
def udp_listen_thread(self):
def udp_listen_thread(self, info_callback=None):
""" Open a UDP socket and listen for incoming data """
if info_callback:
self.stats_callback = info_callback
self.s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
self.s.settimeout(1)
self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@ -50,7 +54,6 @@ class UDPStream(object):
self.s.close()
def handle_samples(self, data, frame_count, time_info="", status_flags=""):
""" Handle incoming samples from pyaudio """
@ -64,7 +67,7 @@ class UDPStream(object):
# 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, None)

Wyświetl plik

@ -1,9 +1,9 @@
# Useful widgets
from PyQt5 import QtWidgets
from PyQt6 import QtWidgets
# Useful class for adding horizontal lines.
class QHLine(QtWidgets.QFrame):
def __init__(self):
super(QHLine, self).__init__()
self.setFrameShape(QtWidgets.QFrame.HLine)
self.setFrameShadow(QtWidgets.QFrame.Sunken)
self.setFrameShape(QtWidgets.QFrame.Shape.HLine)
self.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)

BIN
libhorus.dll 100644

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -1,7 +1,8 @@
numpy
pyaudio
crcmod
PyQt5
PyQt6
pyqtgraph
requests
horusdemodlib>=0.3.12
audioop-lts; python_version>='3.13'