kopia lustrzana https://github.com/projecthorus/horus-gui
Initial commit of horuslib
rodzic
671fb39454
commit
2b5fa0a13e
6
setup.py
6
setup.py
|
@ -41,9 +41,5 @@ if __name__ == "__main__":
|
|||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"horus-gui=horusgui.gui",
|
||||
]
|
||||
}
|
||||
entry_points={"console_scripts": ["horus-gui=horusgui.gui",]},
|
||||
)
|
||||
|
|
|
@ -164,7 +164,7 @@ 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)
|
||||
w1_habitat.addWidget(widgets["habitatUploadPosition"], 5, 0, 1, 3)
|
||||
w1_habitat.layout.setRowStretch(6,1)
|
||||
w1_habitat.layout.setRowStretch(6, 1)
|
||||
|
||||
d0_habitat.addWidget(w1_habitat)
|
||||
|
||||
|
@ -180,7 +180,7 @@ 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)
|
||||
w1_other.layout.setRowStretch(5,1)
|
||||
w1_other.layout.setRowStretch(5, 1)
|
||||
|
||||
d0_other.addWidget(w1_other)
|
||||
|
||||
|
@ -295,13 +295,14 @@ read_config(widgets)
|
|||
|
||||
# Start Habitat Uploader
|
||||
habitat_uploader = HabitatUploader(
|
||||
user_callsign = widgets["userCallEntry"].text(),
|
||||
listener_lat = widgets["userLatEntry"].text(),
|
||||
listener_lon = widgets["userLonEntry"].text(),
|
||||
listener_radio = widgets["userRadioEntry"].text(),
|
||||
listener_antenna = widgets["userAntennaEntry"].text()
|
||||
user_callsign=widgets["userCallEntry"].text(),
|
||||
listener_lat=widgets["userLatEntry"].text(),
|
||||
listener_lon=widgets["userLonEntry"].text(),
|
||||
listener_radio=widgets["userRadioEntry"].text(),
|
||||
listener_antenna=widgets["userAntennaEntry"].text(),
|
||||
)
|
||||
|
||||
|
||||
def habitat_position_reupload():
|
||||
""" Trigger a re-upload of user position information """
|
||||
global widgets, habitat_uploader
|
||||
|
@ -313,16 +314,17 @@ def habitat_position_reupload():
|
|||
habitat_uploader.listener_antenna = widgets["userAntennaEntry"].text()
|
||||
habitat_uploader.trigger_position_upload()
|
||||
|
||||
|
||||
widgets["habitatUploadPosition"].clicked.connect(habitat_position_reupload)
|
||||
|
||||
|
||||
def habitat_inhibit():
|
||||
""" Update the Habitat inhibit flag """
|
||||
global widgets, habitat_uploader
|
||||
habitat_uploader.inhibit = not widgets[
|
||||
"habitatUploadSelector"
|
||||
].isChecked()
|
||||
habitat_uploader.inhibit = not widgets["habitatUploadSelector"].isChecked()
|
||||
logging.debug(f"Updated Habitat Inhibit state: {habitat_uploader.inhibit}")
|
||||
|
||||
|
||||
widgets["habitatUploadSelector"].clicked.connect(habitat_inhibit)
|
||||
|
||||
|
||||
|
@ -369,8 +371,8 @@ def start_decoding():
|
|||
# TODO: Grab horus data here.
|
||||
|
||||
# Init FFT Processor
|
||||
NFFT = 2**14
|
||||
STRIDE = 2**13
|
||||
NFFT = 2 ** 14
|
||||
STRIDE = 2 ** 13
|
||||
fft_process = FFTProcess(
|
||||
nfft=NFFT, stride=STRIDE, fs=_sample_rate, callback=add_fft_update
|
||||
)
|
||||
|
|
|
@ -18,33 +18,34 @@ 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. '''
|
||||
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
|
||||
|
@ -69,34 +70,32 @@ class HabitatUploader(object):
|
|||
self.uploadthread = Thread(target=self.habitat_upload_thread)
|
||||
self.uploadthread.start()
|
||||
|
||||
|
||||
def habitat_upload(self, sentence):
|
||||
''' Upload a UKHAS-standard telemetry sentence to Habitat '''
|
||||
""" 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'))
|
||||
_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
|
||||
},
|
||||
"_raw": _sentence_b64.decode(
|
||||
"ascii"
|
||||
) # Convert back to a string to be serialisable
|
||||
},
|
||||
"receivers": {
|
||||
_user_call: {
|
||||
"time_created": _date,
|
||||
"time_uploaded": _date,
|
||||
},
|
||||
},
|
||||
_user_call: {"time_created": _date, "time_uploaded": _date,},
|
||||
},
|
||||
}
|
||||
|
||||
# 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)
|
||||
time.sleep(random.random() * self.upload_retry_interval * 2.0)
|
||||
|
||||
_retries = 0
|
||||
|
||||
|
@ -107,7 +106,9 @@ class HabitatUploader(object):
|
|||
while _retries < self.upload_retries:
|
||||
# Run the request.
|
||||
try:
|
||||
_req = requests.put(_url, data=json.dumps(_data), timeout=self.upload_timeout)
|
||||
_req = requests.put(
|
||||
_url, data=json.dumps(_data), timeout=self.upload_timeout
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error("Habitat - Upload Failed: %s" % str(e))
|
||||
break
|
||||
|
@ -120,20 +121,25 @@ class HabitatUploader(object):
|
|||
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)
|
||||
time.sleep(random.random() * self.upload_retry_interval)
|
||||
_retries += 1
|
||||
else:
|
||||
logging.error("Habitat - Error uploading to Habitat. Status Code: %d." % _req.status_code)
|
||||
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)
|
||||
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 '''
|
||||
""" Handle uploading of packets to Habitat """
|
||||
|
||||
logging.info("Started Habitat Uploader Thread.")
|
||||
|
||||
|
@ -145,7 +151,9 @@ class HabitatUploader(object):
|
|||
while not self.habitat_upload_queue.empty():
|
||||
sentence = self.habitat_upload_queue.get()
|
||||
|
||||
logging.warning("Habitat uploader queue was full - possible connectivity issue.")
|
||||
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()
|
||||
|
@ -161,22 +169,21 @@ class HabitatUploader(object):
|
|||
# Check for 'valid' position
|
||||
if (self.listener_lat != 0.0) or (self.listener_lon != 0.0):
|
||||
_success = self.uploadListenerPosition(
|
||||
self.user_callsign,
|
||||
self.listener_lat,
|
||||
self.listener_lon,
|
||||
self.listener_radio,
|
||||
self.listener_antenna)
|
||||
self.user_callsign,
|
||||
self.listener_lat,
|
||||
self.listener_lon,
|
||||
self.listener_radio,
|
||||
self.listener_antenna,
|
||||
)
|
||||
|
||||
# Set this flag regardless if the uplaod worked.
|
||||
# The user can trigger a re-upload.
|
||||
# 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 '''
|
||||
""" Add a sentence to the upload queue """
|
||||
|
||||
if self.inhibit:
|
||||
# We have upload inhibited. Return.
|
||||
|
@ -184,64 +191,66 @@ class HabitatUploader(object):
|
|||
|
||||
# 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]
|
||||
sentence = sentence.split("$")[-1]
|
||||
# Now add the *correct* number of $$s back on.
|
||||
sentence = '$$' +sentence
|
||||
sentence = "$$" + sentence
|
||||
|
||||
if not (sentence[-1] == '\n'):
|
||||
sentence += '\n'
|
||||
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. '''
|
||||
""" 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()
|
||||
doc["_id"] = self.uuids.pop()
|
||||
except IndexError:
|
||||
logging.error("Habitat - Unable to post listener data - no UUIDs available.")
|
||||
logging.error(
|
||||
"Habitat - Unable to post listener data - no UUIDs available."
|
||||
)
|
||||
return False
|
||||
|
||||
doc['time_uploaded'] = self.ISOStringNow()
|
||||
doc["time_uploaded"] = self.ISOStringNow()
|
||||
|
||||
try:
|
||||
_r = requests.post(f"{self.HABITAT_URL}{self.HABITAT_DB}/", json=doc, timeout=timeout)
|
||||
_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):
|
||||
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'])
|
||||
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))
|
||||
logging.error(
|
||||
"Habitat - Unable to fetch UUIDs, retrying in 2 seconds - %s"
|
||||
% str(e)
|
||||
)
|
||||
time.sleep(2)
|
||||
_retries = _retries - 1
|
||||
continue
|
||||
|
@ -249,17 +258,12 @@ class HabitatUploader(object):
|
|||
logging.error("Habitat - Gave up trying to get UUIDs.")
|
||||
return
|
||||
|
||||
|
||||
def initListenerCallsign(self, callsign, radio='', antenna=''):
|
||||
def initListenerCallsign(self, callsign, radio="", antenna=""):
|
||||
doc = {
|
||||
'type': 'listener_information',
|
||||
'time_created' : self.ISOStringNow(),
|
||||
'data': {
|
||||
'callsign': callsign,
|
||||
'antenna': antenna,
|
||||
'radio': radio,
|
||||
}
|
||||
}
|
||||
"type": "listener_information",
|
||||
"time_created": self.ISOStringNow(),
|
||||
"data": {"callsign": callsign, "antenna": antenna, "radio": radio,},
|
||||
}
|
||||
|
||||
resp = self.postListenerData(doc)
|
||||
|
||||
|
@ -270,8 +274,7 @@ class HabitatUploader(object):
|
|||
logging.error("Habitat - Unable to initialize callsign.")
|
||||
return False
|
||||
|
||||
|
||||
def uploadListenerPosition(self, callsign, lat, lon, radio='', antenna=''):
|
||||
def uploadListenerPosition(self, callsign, lat, lon, radio="", antenna=""):
|
||||
""" Initializer Listener Callsign, and upload Listener Position """
|
||||
|
||||
# Validate the lat/lon entries.
|
||||
|
@ -290,16 +293,16 @@ class HabitatUploader(object):
|
|||
return False
|
||||
|
||||
doc = {
|
||||
'type': 'listener_telemetry',
|
||||
'time_created': self.ISOStringNow(),
|
||||
'data': {
|
||||
'callsign': callsign,
|
||||
'chase': False,
|
||||
'latitude': _lat,
|
||||
'longitude': _lon,
|
||||
'altitude': 0,
|
||||
'speed': 0,
|
||||
}
|
||||
"type": "listener_telemetry",
|
||||
"time_created": self.ISOStringNow(),
|
||||
"data": {
|
||||
"callsign": callsign,
|
||||
"chase": False,
|
||||
"latitude": _lat,
|
||||
"longitude": _lon,
|
||||
"altitude": 0,
|
||||
"speed": 0,
|
||||
},
|
||||
}
|
||||
|
||||
# post position to habitat
|
||||
|
@ -310,7 +313,7 @@ class HabitatUploader(object):
|
|||
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
|
||||
|
@ -319,14 +322,16 @@ class HabitatUploader(object):
|
|||
if __name__ == "__main__":
|
||||
|
||||
# Setup Logging
|
||||
logging.basicConfig(format="%(asctime)s %(levelname)s: %(message)s", level=logging.INFO)
|
||||
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"
|
||||
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")
|
||||
|
@ -334,4 +339,4 @@ if __name__ == "__main__":
|
|||
time.sleep(10)
|
||||
habitat.trigger_position_upload()
|
||||
time.sleep(5)
|
||||
habitat.close()
|
||||
habitat.close()
|
||||
|
|
|
@ -0,0 +1,281 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import ctypes
|
||||
from ctypes import *
|
||||
import logging
|
||||
import sys
|
||||
from enum import Enum
|
||||
import os
|
||||
import logging
|
||||
|
||||
|
||||
# TODO
|
||||
# - Doc Strings
|
||||
# - frame error checking
|
||||
# - Modem Stats
|
||||
# - demodulate should return an object with the stats
|
||||
|
||||
MODEM_STATS_NR_MAX = 8
|
||||
MODEM_STATS_NC_MAX = 50
|
||||
MODEM_STATS_ET_MAX = 8
|
||||
MODEM_STATS_EYE_IND_MAX = 160
|
||||
MODEM_STATS_NSPEC = 512
|
||||
MODEM_STATS_MAX_F_EST = 4
|
||||
|
||||
|
||||
class COMP(Structure):
|
||||
_fields_ = [("real", c_float), ("imag", c_float)]
|
||||
|
||||
|
||||
class MODEM_STATS(Structure): # modem_stats.h
|
||||
_fields_ = [
|
||||
("Nc", c_int),
|
||||
("snr_est", c_float),
|
||||
(
|
||||
"rx_symbols",
|
||||
(COMP * MODEM_STATS_NR_MAX) * (MODEM_STATS_NC_MAX + 1),
|
||||
), # rx_symbols[MODEM_STATS_NR_MAX][MODEM_STATS_NC_MAX+1];
|
||||
("nr", c_int),
|
||||
("sync", c_int),
|
||||
("foff", c_float),
|
||||
("rx_timing", c_float),
|
||||
("clock_offset", c_float),
|
||||
("sync_metric", c_float),
|
||||
(
|
||||
"rx_eye",
|
||||
(c_float * MODEM_STATS_ET_MAX) * MODEM_STATS_EYE_IND_MAX,
|
||||
), # float rx_eye[MODEM_STATS_ET_MAX][MODEM_STATS_EYE_IND_MAX];
|
||||
("neyetr", c_int),
|
||||
("neyesamp", c_int),
|
||||
("f_est", c_float * MODEM_STATS_MAX_F_EST),
|
||||
("fft_buf", c_float * 2 * MODEM_STATS_NSPEC),
|
||||
("fft_cfg", POINTER(c_ubyte)),
|
||||
]
|
||||
|
||||
|
||||
class Mode(Enum):
|
||||
BINARY = 0
|
||||
BINARY_V1 = 0
|
||||
RTTY_7N2 = 99
|
||||
BINARY_V2_256BIT = 1
|
||||
BINARY_V2_128BIT = 2
|
||||
|
||||
|
||||
class Frame:
|
||||
def __init__(
|
||||
self,
|
||||
data: bytes,
|
||||
sync: bool,
|
||||
crc_pass: bool,
|
||||
snr: float,
|
||||
extended_stats: MODEM_STATS,
|
||||
):
|
||||
self.data = data
|
||||
self.sync = sync
|
||||
self.snr = snr
|
||||
self.crc_pass = crc_pass
|
||||
self.extended_stats = extended_stats
|
||||
|
||||
|
||||
class HorusLib:
|
||||
def __init__(
|
||||
self,
|
||||
libpath=f"",
|
||||
mode=Mode.BINARY,
|
||||
rate=-1,
|
||||
tone_spacing=-1,
|
||||
stereo_iq=False,
|
||||
verbose=False,
|
||||
):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
libpath : str
|
||||
Path to libhorus
|
||||
mode : Mode
|
||||
horuslib.Mode.BINARY, horuslib.Mode.BINARY_V2_256BIT, horuslib.Mode.BINARY_V2_128BIT, horuslib.Mode.RTTY_7N2
|
||||
rate : int
|
||||
Changes the modem rate for supported modems. -1 for default
|
||||
tone_spacing : int
|
||||
Spacing between tones (hz) -1 for default
|
||||
stereo_iq : bool
|
||||
use stereo (IQ) input (quadrature)
|
||||
verbose : bool
|
||||
Enabled horus_set_verbose
|
||||
"""
|
||||
|
||||
if sys.platform == "darwin":
|
||||
libpath = os.path.join(libpath, "libhorus.dylib")
|
||||
elif sys.platform == "win32":
|
||||
libpath = os.path.join(libpath, "libhorus.dll")
|
||||
else:
|
||||
libpath = os.path.join(libpath, "libhorus.so")
|
||||
|
||||
self.c_lib = ctypes.cdll.LoadLibrary(
|
||||
libpath
|
||||
) # future improvement would be to try a few places / names
|
||||
|
||||
# horus_open_advanced
|
||||
self.c_lib.horus_open_advanced.restype = POINTER(c_ubyte)
|
||||
|
||||
# horus_nin
|
||||
self.c_lib.horus_nin.restype = c_uint32
|
||||
|
||||
# horus_get_Fs
|
||||
self.c_lib.horus_get_Fs.restype = c_int
|
||||
|
||||
# horus_set_freq_est_limits - (struct horus *hstates, float fsk_lower, float fsk_upper)
|
||||
self.c_lib.horus_set_freq_est_limits.argtype = [
|
||||
POINTER(c_ubyte),
|
||||
c_float,
|
||||
c_float,
|
||||
]
|
||||
|
||||
# horus_get_max_demod_in
|
||||
self.c_lib.horus_get_max_demod_in.restype = c_int
|
||||
|
||||
# horus_get_max_ascii_out_len
|
||||
self.c_lib.horus_get_max_ascii_out_len.restype = c_int
|
||||
|
||||
# horus_crc_ok
|
||||
self.c_lib.horus_crc_ok.restype = c_int
|
||||
|
||||
# horus_get_modem_extended_stats - (struct horus *hstates, struct MODEM_STATS *stats)
|
||||
self.c_lib.horus_get_modem_extended_stats.argtype = [
|
||||
POINTER(MODEM_STATS),
|
||||
POINTER(c_ubyte),
|
||||
]
|
||||
|
||||
# horus_get_mFSK
|
||||
self.c_lib.horus_get_mFSK.restype = c_int
|
||||
|
||||
# horus_rx
|
||||
self.c_lib.horus_rx.restype = c_int
|
||||
|
||||
# struct horus *hstates, char ascii_out[], short demod_in[], int quadrature
|
||||
|
||||
if type(mode) != type(Mode(0)):
|
||||
raise ValueError("Must be of type horuslib.Mode")
|
||||
else:
|
||||
self.mode = mode
|
||||
|
||||
self.stereo_iq = stereo_iq
|
||||
|
||||
# intial nin
|
||||
self.nin = 0
|
||||
|
||||
# try to open the modem and set the verbosity
|
||||
self.hstates = self.c_lib.horus_open_advanced(
|
||||
self.mode.value, rate, tone_spacing
|
||||
)
|
||||
self.c_lib.horus_set_verbose(self.hstates, int(verbose))
|
||||
|
||||
# check that the modem was actually opened and we don't just have a null pointer
|
||||
if bool(self.hstates):
|
||||
logging.debug("Opened Horus API")
|
||||
else:
|
||||
logging.error("Couldn't open Horus API for some reason")
|
||||
raise EnvironmentError("Couldn't open Horus API")
|
||||
|
||||
# build some class types to fit the data for demodulation using ctypes
|
||||
max_demod_in = int(self.c_lib.horus_get_max_demod_in(self.hstates))
|
||||
max_ascii_out = int(self.c_lib.horus_get_max_ascii_out_len(self.hstates))
|
||||
self.DemodIn = c_short * (max_demod_in * (1 + int(self.stereo_iq)))
|
||||
self.DataOut = c_char * max_ascii_out
|
||||
self.c_lib.horus_rx.argtype = [
|
||||
POINTER(c_ubyte),
|
||||
c_char * max_ascii_out,
|
||||
c_short * max_demod_in,
|
||||
c_int,
|
||||
]
|
||||
|
||||
self.mfsk = int(self.c_lib.horus_get_mFSK(self.hstates))
|
||||
|
||||
# in case someone wanted to use `with` style. I'm not sure if closing the modem does a lot.
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *a):
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
self.c_lib.horus_close(self.hstates)
|
||||
logging.debug("Shutdown horus modem")
|
||||
|
||||
def update_nin(self):
|
||||
new_nin = int(self.c_lib.horus_nin(self.hstates))
|
||||
if self.nin != new_nin:
|
||||
logging.debug(f"Updated nin {new_nin}")
|
||||
self.nin = new_nin
|
||||
|
||||
def demodulate(self, demod_in: bytes):
|
||||
# from_buffer_copy requires exact size so we pad it out.
|
||||
buffer = bytearray(
|
||||
len(self.DemodIn()) * sizeof(c_short)
|
||||
) # create empty byte array
|
||||
buffer[: len(demod_in)] = demod_in # copy across what we have
|
||||
|
||||
modulation = self.DemodIn # get an empty modulation array
|
||||
modulation = modulation.from_buffer_copy(
|
||||
buffer
|
||||
) # copy buffer across and get a pointer to it.
|
||||
|
||||
data_out = self.DataOut() # initilize a pointer to where bytes will be outputed
|
||||
|
||||
self.c_lib.horus_rx(self.hstates, data_out, modulation, int(self.stereo_iq))
|
||||
|
||||
stats = MODEM_STATS()
|
||||
self.c_lib.horus_get_modem_extended_stats(self.hstates, byref(stats))
|
||||
|
||||
crc = bool(self.c_lib.horus_crc_ok(self.hstates))
|
||||
|
||||
data_out = bytes(data_out)
|
||||
self.update_nin()
|
||||
|
||||
# strip the null terminator out
|
||||
data_out = data_out[:-1]
|
||||
|
||||
if data_out == bytes(len(data_out)):
|
||||
data_out = (
|
||||
b"" # check if bytes is just null and return an empty bytes instead
|
||||
)
|
||||
elif self.mode != Mode.RTTY:
|
||||
try:
|
||||
data_out = bytes.fromhex(data_out.decode("ascii"))
|
||||
except ValueError:
|
||||
logging.debug(data_out)
|
||||
logging.error("💥Couldn't decode the hex from the modem")
|
||||
return bytes()
|
||||
else:
|
||||
data_out = bytes(data_out.decode("ascii"))
|
||||
|
||||
frame = Frame(
|
||||
data=data_out,
|
||||
snr=float(stats.snr_est),
|
||||
sync=bool(stats.sync),
|
||||
crc_pass=crc,
|
||||
extended_stats=stats,
|
||||
)
|
||||
return frame
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
filename = sys.argv[1]
|
||||
|
||||
# Setup Logging
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s %(levelname)s: %(message)s", level=logging.INFO
|
||||
)
|
||||
with HorusLib(libpath=".", mode=Mode.BINARY, verbose=True) as horus:
|
||||
with open(filename, "rb") as f:
|
||||
while True:
|
||||
data = f.read(horus.nin * 2)
|
||||
if horus.nin != 0 and data == b"": # detect end of file
|
||||
break
|
||||
output = horus.demodulate(data)
|
||||
if output.crc_pass and output.data:
|
||||
print(f"{output.data.hex()} SNR: {output.snr}")
|
||||
for x in range(horus.mfsk):
|
||||
print(f"F{str(x)}: {float(output.extended_stats.f_est[x])}")
|
|
@ -17,7 +17,7 @@ def crc16_ccitt(data):
|
|||
|
||||
(CRC16 CCITT: start 0xFFFF, poly 0x1021)
|
||||
"""
|
||||
crc16 = crcmod.predefined.mkCrcFun('crc-ccitt-false')
|
||||
crc16 = crcmod.predefined.mkCrcFun("crc-ccitt-false")
|
||||
return hex(crc16(data))[2:].upper().zfill(4)
|
||||
|
||||
|
||||
|
@ -38,23 +38,23 @@ def decode_ukhas_sentence(sentence):
|
|||
_sentence = sentence.strip()
|
||||
|
||||
# First, try and find the start of the sentence, which always starts with '$$''
|
||||
_sentence = _sentence.split('$$')[-1]
|
||||
_sentence = _sentence.split("$$")[-1]
|
||||
# Hack to handle odd numbers of $$'s at the start of a sentence
|
||||
if _sentence[0] == '$':
|
||||
if _sentence[0] == "$":
|
||||
_sentence = _sentence[1:]
|
||||
# Now try and split out the telemetry from the CRC16.
|
||||
_telem = _sentence.split('*')[0]
|
||||
_crc = _sentence.split('*')[1]
|
||||
_telem = _sentence.split("*")[0]
|
||||
_crc = _sentence.split("*")[1]
|
||||
|
||||
# Now check if the CRC matches.
|
||||
_calc_crc = crc16_ccitt(_telem.encode('ascii'))
|
||||
_calc_crc = crc16_ccitt(_telem.encode("ascii"))
|
||||
|
||||
if _calc_crc != _crc:
|
||||
logging.error("Could not parse ASCII Sentence - CRC Fail.")
|
||||
return None
|
||||
|
||||
# We now have a valid sentence! Extract fields..
|
||||
_fields = _telem.split(',')
|
||||
_fields = _telem.split(",")
|
||||
|
||||
_callsign = _fields[0]
|
||||
_time = _fields[2]
|
||||
|
@ -84,16 +84,16 @@ def decode_ukhas_sentence(sentence):
|
|||
|
||||
# Produce a dict output which is compatible with send_payload_summary below
|
||||
_telem = {
|
||||
'callsign': _callsign,
|
||||
'time': _time,
|
||||
'latitude': _latitude,
|
||||
'longitude': _longitude,
|
||||
'altitude': _altitude,
|
||||
'speed': -1,
|
||||
'heading': -1,
|
||||
'temp': -1,
|
||||
'sats': -1,
|
||||
'batt_voltage': -1
|
||||
"callsign": _callsign,
|
||||
"time": _time,
|
||||
"latitude": _latitude,
|
||||
"longitude": _longitude,
|
||||
"altitude": _altitude,
|
||||
"speed": -1,
|
||||
"heading": -1,
|
||||
"temp": -1,
|
||||
"sats": -1,
|
||||
"batt_voltage": -1,
|
||||
}
|
||||
|
||||
return _telem
|
||||
|
@ -114,30 +114,30 @@ def send_payload_summary(telemetry, port=55672, comment="Horus Binary"):
|
|||
|
||||
try:
|
||||
# Do a few checks before sending.
|
||||
if telemetry['latitude'] == 0.0 and telemetry['longitude'] == 0.0:
|
||||
if telemetry["latitude"] == 0.0 and telemetry["longitude"] == 0.0:
|
||||
logging.error("Horus UDP - Zero Latitude/Longitude, not sending.")
|
||||
return
|
||||
|
||||
packet = {
|
||||
'type' : 'PAYLOAD_SUMMARY',
|
||||
'callsign' : telemetry['callsign'],
|
||||
'latitude' : telemetry['latitude'],
|
||||
'longitude' : telemetry['longitude'],
|
||||
'altitude' : telemetry['altitude'],
|
||||
'speed' : telemetry['speed'],
|
||||
'heading': -1,
|
||||
'time' : telemetry['time'],
|
||||
'comment' : comment,
|
||||
'temp': telemetry['temp'],
|
||||
'sats': telemetry['sats'],
|
||||
'batt_voltage': telemetry['batt_voltage']
|
||||
"type": "PAYLOAD_SUMMARY",
|
||||
"callsign": telemetry["callsign"],
|
||||
"latitude": telemetry["latitude"],
|
||||
"longitude": telemetry["longitude"],
|
||||
"altitude": telemetry["altitude"],
|
||||
"speed": telemetry["speed"],
|
||||
"heading": -1,
|
||||
"time": telemetry["time"],
|
||||
"comment": comment,
|
||||
"temp": telemetry["temp"],
|
||||
"sats": telemetry["sats"],
|
||||
"batt_voltage": telemetry["batt_voltage"],
|
||||
}
|
||||
|
||||
# Set up our UDP socket
|
||||
_s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
|
||||
_s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
_s.settimeout(1)
|
||||
# Set up socket for broadcast, and allow re-use of the address
|
||||
_s.setsockopt(socket.SOL_SOCKET,socket.SO_BROADCAST,1)
|
||||
_s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||
_s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
# Under OSX we also need to set SO_REUSEPORT to 1
|
||||
try:
|
||||
|
@ -146,12 +146,14 @@ def send_payload_summary(telemetry, port=55672, comment="Horus Binary"):
|
|||
pass
|
||||
|
||||
try:
|
||||
_s.sendto(json.dumps(packet).encode('ascii'), ('<broadcast>',port))
|
||||
_s.sendto(json.dumps(packet).encode("ascii"), ("<broadcast>", port))
|
||||
# Catch any socket errors, that may occur when attempting to send to a broadcast address
|
||||
# when there is no network connected. In this case, re-try and send to localhost instead.
|
||||
except socket.error as e:
|
||||
logging.debug("Horus UDP - Send to broadcast address failed, sending to localhost instead.")
|
||||
_s.sendto(json.dumps(packet).encode('ascii'), ('127.0.0.1', port))
|
||||
logging.debug(
|
||||
"Horus UDP - Send to broadcast address failed, sending to localhost instead."
|
||||
)
|
||||
_s.sendto(json.dumps(packet).encode("ascii"), ("127.0.0.1", port))
|
||||
|
||||
_s.close()
|
||||
|
||||
|
@ -163,10 +165,12 @@ if __name__ == "__main__":
|
|||
# Test script for the above functions
|
||||
|
||||
# Setup Logging
|
||||
logging.basicConfig(format="%(asctime)s %(levelname)s: %(message)s", level=logging.INFO)
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s %(levelname)s: %(message)s", level=logging.INFO
|
||||
)
|
||||
|
||||
sentence = "$$TESTING,1,01:02:03,-34.0,138.0,1000"
|
||||
crc = crc16_ccitt(sentence[2:].encode('ascii'))
|
||||
crc = crc16_ccitt(sentence[2:].encode("ascii"))
|
||||
sentence = sentence + "*" + crc
|
||||
print("Sentence: " + sentence)
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue