diff --git a/horusdemodlib/__init__.py b/horusdemodlib/__init__.py index d31c31e..493f741 100755 --- a/horusdemodlib/__init__.py +++ b/horusdemodlib/__init__.py @@ -1 +1 @@ -__version__ = "0.2.3" +__version__ = "0.3.0" diff --git a/horusdemodlib/decoder.py b/horusdemodlib/decoder.py index 60b2770..90ee347 100644 --- a/horusdemodlib/decoder.py +++ b/horusdemodlib/decoder.py @@ -98,7 +98,8 @@ def decode_packet(data:bytes, packet_format:dict = None, ignore_crc:bool = False _output = { 'packet_format': packet_format, 'crc_ok': False, - 'payload_id': 0 + 'payload_id': 0, + 'raw': codecs.encode(data, 'hex').decode().upper() } # Check the length provided in the packet format matches up with the length defined by the struct. @@ -188,6 +189,7 @@ def parse_ukhas_string(sentence:str) -> dict: # Try and proceed through the following. If anything fails, we have a corrupt sentence. # Strip out any leading/trailing whitespace. _sentence = sentence.strip() + _raw = _sentence # First, try and find the start of the sentence, which always starts with '$$'' _sentence = _sentence.split('$')[-1] @@ -208,6 +210,7 @@ def parse_ukhas_string(sentence:str) -> dict: _fields = _telem.split(',') try: _callsign = _fields[0] + _sequence_number = int(_fields[1]) _time = _fields[2] _latitude = float(_fields[3]) _longitude = float(_fields[4]) @@ -245,16 +248,18 @@ def parse_ukhas_string(sentence:str) -> dict: # Produce a dict output which is compatible with the output of the binary decoder. _telem = { + 'raw': _raw, 'callsign': _callsign, + 'sequence_number': _sequence_number, 'time': _time, 'latitude': _latitude, 'longitude': _longitude, 'altitude': _altitude, - 'speed': -1, - 'heading': -1, - 'temp': -1, - 'sats': -1, - 'batt_voltage': -1 + # 'speed': -1, + # 'heading': -1, + # 'temperature': -1, + # 'satellites': -1, + # 'battery_voltage': -1 } return _telem diff --git a/horusdemodlib/delegates.py b/horusdemodlib/delegates.py index 62d7604..f003129 100644 --- a/horusdemodlib/delegates.py +++ b/horusdemodlib/delegates.py @@ -4,9 +4,11 @@ import struct import time - +import datetime +from dateutil.parser import parse import horusdemodlib.payloads + # Payload ID def decode_payload_id(data: int) -> str: @@ -236,6 +238,41 @@ def decode_custom_fields(data:bytes, payload_id:str): return (_output_dict, _output_fields_str) +def fix_datetime(datetime_str, local_dt_str=None): + """ + Given a HH:MM:SS string from a telemetry sentence, produce a complete timestamp, using the current system time as a guide for the date. + """ + + if local_dt_str is None: + _now = datetime.datetime.utcnow() + else: + _now = parse(local_dt_str) + + # Are we in the rollover window? + if _now.hour == 23 or _now.hour == 0: + _outside_window = False + else: + _outside_window = True + + # Parsing just a HH:MM:SS will return a datetime object with the year, month and day replaced by values in the 'default' + # argument. + _imet_dt = parse(datetime_str, default=_now) + + if _outside_window: + # We are outside the day-rollover window, and can safely use the current zulu date. + return _imet_dt + else: + # We are within the window, and need to adjust the day backwards or forwards based on the sonde time. + if _imet_dt.hour == 23 and _now.hour == 0: + # Assume system clock running slightly fast, and subtract a day from the telemetry date. + _imet_dt = _imet_dt - datetime.timedelta(days=1) + + elif _imet_dt.hour == 00 and _now.hour == 23: + # System clock running slow. Add a day. + _imet_dt = _imet_dt + datetime.timedelta(days=1) + + return _imet_dt + if __name__ == "__main__": diff --git a/horusdemodlib/demodstats.py b/horusdemodlib/demodstats.py index 531db65..8c7cdb1 100644 --- a/horusdemodlib/demodstats.py +++ b/horusdemodlib/demodstats.py @@ -56,6 +56,7 @@ class FSKDemodStats(object): # Output State variables. self.snr = -999.0 self.fest = [0.0,0.0, 0.0,0.0] + self.fest_mean = 0.0 self.fft = [] self.ppm = 0.0 @@ -110,6 +111,8 @@ class FSKDemodStats(object): self.fest[3] = _data['f4_est'] else: self.fest = self.fest[:2] + + self.fest_mean = np.mean(self.fest) # Time-series data self.in_times = np.append(self.in_times, _time) diff --git a/horusdemodlib/habitat.py b/horusdemodlib/habitat.py index d5e3a3d..490b4dd 100644 --- a/horusdemodlib/habitat.py +++ b/horusdemodlib/habitat.py @@ -90,7 +90,7 @@ class HabitatUploader(object): ) # 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}, }, } diff --git a/horusdemodlib/sondehubamateur.py b/horusdemodlib/sondehubamateur.py new file mode 100644 index 0000000..7aae7aa --- /dev/null +++ b/horusdemodlib/sondehubamateur.py @@ -0,0 +1,440 @@ +#!/usr/bin/env python +# +# HorusDemodLib - SondeHub Amateur Uploader +# +# Uploads telemetry to the SondeHub ElasticSearch cluster, +# in the new 'universal' format descried here: +# https://github.com/projecthorus/sondehub-infra/wiki/%5BDRAFT%5D-Amateur-Balloon-Telemetry-Format +# +# Copyright (C) 2022 Mark Jessop +# Released under GNU GPL v3 or later +# +import horusdemodlib +import datetime +import glob +import gzip +import json +import logging +import os +import requests +import time +from threading import Thread +from email.utils import formatdate +from .delegates import fix_datetime + +try: + # Python 2 + from Queue import Queue +except ImportError: + # Python 3 + from queue import Queue + + +class SondehubAmateurUploader(object): + """ Sondehub (Amateur) Uploader Class. + + Accepts telemetry dictionaries from a decoder, buffers them up, and then compresses and uploads + them to the Sondehub Elasticsearch cluster. + + """ + + # SondeHub API endpoint + SONDEHUB_AMATEUR_URL = "https://api.v2.sondehub.org/amateur/telemetry" + SONDEHUB_AMATEUR_STATION_POSITION_URL = "https://api.v2.sondehub.org/amateur/listeners" + + def __init__( + self, + upload_rate=30, + upload_timeout=20, + upload_retries=5, + user_callsign="N0CALL", + user_position=None, + user_radio="", + user_antenna="", + contact_email="", + user_position_update_rate=6, + software_name="horusdemodlib", + software_version="", + inhibit=False + ): + """ Initialise and start a Sondehub (Amateur) uploader + + Args: + upload_rate (int): How often to upload batches of data. + upload_timeout (int): Upload timeout. + + """ + + self.upload_rate = upload_rate + self.upload_timeout = upload_timeout + self.upload_retries = upload_retries + self.user_callsign = user_callsign + self.user_position = user_position + self.user_radio = user_radio + self.user_antenna = user_antenna + self.contact_email = contact_email + self.user_position_update_rate = user_position_update_rate + self.software_name = software_name + self.software_version = software_version + self.inhibit = inhibit + + if self.user_position is None: + self.inhibit_position_upload = True + else: + self.inhibit_position_upload = False + + # Input Queue. + self.input_queue = Queue() + + # Record of when we last uploaded a user station position to Sondehub. + self.last_user_position_upload = 0 + + try: + # Python 2 check. Python 2 doesnt have gzip.compress so this will throw an exception. + gzip.compress(b"\x00\x00") + + # Start queue processing thread. + if self.inhibit: + logging.info("SondeHub Amateur Uploader Inhibited.") + else: + self.input_processing_running = True + self.input_process_thread = Thread(target=self.process_queue) + self.input_process_thread.start() + + except: + logging.error( + "Detected Python 2.7, which does not support gzip.compress. Sondehub DB uploading will be disabled." + ) + self.input_processing_running = False + + def update_station_position(self, lat, lon, alt): + """ Update the internal station position record. Used when determining the station position by GPSD """ + if self.inhibit_position_upload: + # Don't update the internal position array if we aren't uploading our position. + return + else: + self.user_position = (lat, lon, alt) + + def add(self, telemetry): + """ Add a dictionary of telemetry to the input queue. + + Args: + telemetry (dict): Telemetry dictionary to add to the input queue. + """ + + if self.inhibit: + return + + # Attempt to reformat the data. + _telem = self.reformat_data(telemetry) + # self.log_debug("Telem: %s" % str(_telem)) + + # Add it to the queue if we are running. + if self.input_processing_running and _telem: + self.input_queue.put(_telem) + else: + self.log_debug("Processing not running, discarding.") + + def reformat_data(self, telemetry): + """ Take an input dictionary and convert it to the universal format """ + + # Init output dictionary + _output = { + "software_name": self.software_name, + "software_version": self.software_version, + "uploader_callsign": self.user_callsign, + "uploader_position": self.user_position, + "uploader_radio": self.user_radio, + "uploader_antenna": self.user_antenna, + "time_received": datetime.datetime.utcnow().strftime( + "%Y-%m-%dT%H:%M:%S.%fZ" + ), + } + + # Mandatory Fields + # Datetime + try: + _datetime = fix_datetime(telemetry['time']) + _output["datetime"] = _datetime.strftime( + "%Y-%m-%dT%H:%M:%S.%fZ" + ) + except Exception as e: + self.log_error( + "Error converting telemetry datetime to string - %s" % str(e) + ) + self.log_debug("Offending datetime_dt: %s" % str(telemetry["time"])) + return None + + # Callsign + _output['payload_callsign'] = telemetry["callsign"] + + # Frame Number + _output["frame"] = telemetry["sequence_number"] + + # Position + _output["lat"] = telemetry["latitude"] + _output["lon"] = telemetry["longitude"] + _output["alt"] = telemetry["altitude"] + + # # Optional Fields + if "temperature" in telemetry: + if telemetry["temperature"] > -273.15: + _output["temp"] = telemetry["temperature"] + + if "satellites" in telemetry: + _output["sats"] = telemetry["satellites"] + + if "battery_voltage" in telemetry: + if telemetry["battery_voltage"] >= 0.0: + _output["batt"] = telemetry["battery_voltage"] + + # Handle the additional SNR and frequency estimation if we have it + if "snr" in telemetry: + _output["snr"] = telemetry["snr"] + + if "f_centre" in telemetry: + _output["frequency"] = telemetry["f_centre"] / 1e6 # Hz -> MHz + + if "raw" in telemetry: + _output["raw"] = telemetry["raw"] + + logging.debug(f"Sondehub Amateur Uploader - Generated Packet: {str(_output)}") + + return _output + + def process_queue(self): + """ Process data from the input queue, and write telemetry to log files. + """ + self.log_info("Started Sondehub Amateur Uploader Thread.") + + while self.input_processing_running: + + # Process everything in the queue. + _to_upload = [] + + while self.input_queue.qsize() > 0: + try: + _to_upload.append(self.input_queue.get_nowait()) + except Exception as e: + self.log_error("Error grabbing telemetry from queue - %s" % str(e)) + + # Upload data! + if len(_to_upload) > 0: + self.upload_telemetry(_to_upload) + + # If we haven't uploaded our station position recently, re-upload it. + if ( + time.time() - self.last_user_position_upload + ) > self.user_position_update_rate * 3600: + self.station_position_upload() + + # Sleep while waiting for some new data. + for i in range(self.upload_rate): + time.sleep(1) + if self.input_processing_running == False: + break + + self.log_info("Stopped Sondehub Amateur Uploader Thread.") + + def upload_telemetry(self, telem_list): + """ Upload an list of telemetry data to Sondehub """ + + _data_len = len(telem_list) + + try: + _start_time = time.time() + _telem_json = json.dumps(telem_list).encode("utf-8") + _compressed_payload = gzip.compress(_telem_json) + except Exception as e: + self.log_error( + "Error serialising and compressing telemetry list for upload - %s" + % str(e) + ) + return + + _compression_time = time.time() - _start_time + self.log_debug( + "Pre-compression: %d bytes, post: %d bytes. %.1f %% compression ratio, in %.1f s" + % ( + len(_telem_json), + len(_compressed_payload), + (len(_compressed_payload) / len(_telem_json)) * 100, + _compression_time, + ) + ) + + _retries = 0 + _upload_success = False + + _start_time = time.time() + + while _retries < self.upload_retries: + # Run the request. + try: + headers = { + "User-Agent": "horusdemodlib-" + horusdemodlib.__version__, + "Content-Encoding": "gzip", + "Content-Type": "application/json", + "Date": formatdate(timeval=None, localtime=False, usegmt=True), + } + _req = requests.put( + self.SONDEHUB_AMATEUR_URL, + _compressed_payload, + # TODO: Revisit this second timeout value. + timeout=(self.upload_timeout, 6.1), + headers=headers, + ) + except Exception as e: + self.log_error("Upload Failed: %s" % str(e)) + return + + if _req.status_code == 200: + # 200 is the only status code that we accept. + _upload_time = time.time() - _start_time + self.log_info( + "Uploaded %d telemetry packets to Sondehub Amateur in %.1f seconds." + % (_data_len, _upload_time) + ) + _upload_success = True + break + + elif _req.status_code == 500: + # Server Error, Retry. + _retries += 1 + continue + + else: + self.log_error( + "Error uploading to Sondehub Amateur. Status Code: %d %s." + % (_req.status_code, _req.text) + ) + break + + if not _upload_success: + self.log_error("Upload failed after %d retries" % (_retries)) + + def station_position_upload(self): + """ + Upload a station position packet to SondeHub. + + This uses the PUT /listeners API described here: + https://github.com/projecthorus/sondehub-infra/wiki/API-(Beta) + + """ + + if self.inhibit_position_upload: + # Position upload inhibited. Ensure user position is set to None, and continue upload of other info. + self.log_debug("Sondehub station position upload inhibited.") + + _position = { + "software_name": self.software_name, + "software_version": self.software_version, + "uploader_callsign": self.user_callsign, + "uploader_position": self.user_position, + "uploader_radio": self.user_radio, + "uploader_antenna": self.user_antenna, + "uploader_contact_email": self.contact_email, + "mobile": False, # Hardcoded mobile=false setting - Mobile stations should be using Chasemapper. + } + + print(_position) + + _retries = 0 + _upload_success = False + + _start_time = time.time() + + while _retries < self.upload_retries: + # Run the request. + try: + headers = { + "User-Agent": "horusdemodlib-" + horusdemodlib.__version__, + "Content-Type": "application/json", + "Date": formatdate(timeval=None, localtime=False, usegmt=True), + } + _req = requests.put( + self.SONDEHUB_AMATEUR_STATION_POSITION_URL, + json=_position, + # TODO: Revisit this second timeout value. + timeout=(self.upload_timeout, 6.1), + headers=headers, + ) + except Exception as e: + self.log_error("Upload Failed: %s" % str(e)) + return + + if _req.status_code == 200: + # 200 is the only status code that we accept. + _upload_time = time.time() - _start_time + self.log_info("Uploaded station information to Sondehub.") + _upload_success = True + break + + elif _req.status_code == 500: + # Server Error, Retry. + _retries += 1 + continue + + elif _req.status_code == 404: + # API doesn't exist yet! + self.log_debug("Sondehub Amateur position upload API not implemented yet!") + _upload_success = True + break + + else: + self.log_error( + "Error uploading station information to Sondehub. Status Code: %d %s." + % (_req.status_code, _req.text) + ) + break + + if not _upload_success: + self.log_error( + "Station information upload failed after %d retries" % (_retries) + ) + self.log_debug(f"Attempted to upload {json.dumps(_position)}") + + self.last_user_position_upload = time.time() + + def close(self): + """ Close input processing thread. """ + self.input_processing_running = False + + def running(self): + """ Check if the uploader thread is running. + + Returns: + bool: True if the uploader thread is running. + """ + return self.input_processing_running + + def log_debug(self, line): + """ Helper function to log a debug message with a descriptive heading. + Args: + line (str): Message to be logged. + """ + logging.debug("Sondehub Amateur Uploader - %s" % line) + + def log_info(self, line): + """ Helper function to log an informational message with a descriptive heading. + Args: + line (str): Message to be logged. + """ + logging.info("Sondehub Amateur Uploader - %s" % line) + + def log_error(self, line): + """ Helper function to log an error message with a descriptive heading. + Args: + line (str): Message to be logged. + """ + logging.error("Sondehub Amateur Uploader - %s" % line) + + +if __name__ == "__main__": + # Test Script + logging.basicConfig( + format="%(asctime)s %(levelname)s:%(message)s", level=logging.DEBUG + ) + _test = SondehubAmateurUploader() + time.sleep(5) + _test.close() diff --git a/horusdemodlib/uploader.py b/horusdemodlib/uploader.py index 9620695..2539cd2 100644 --- a/horusdemodlib/uploader.py +++ b/horusdemodlib/uploader.py @@ -15,12 +15,14 @@ import traceback from configparser import RawConfigParser from .habitat import * +from .sondehubamateur import * from .decoder import decode_packet, parse_ukhas_string from .payloads import * from .horusudp import send_payload_summary from .payloads import init_custom_field_list, init_payload_id_list from .demodstats import FSKDemodStats import horusdemodlib.payloads +import horusdemodlib def read_config(filename): ''' Read in the user configuation file.''' @@ -68,6 +70,8 @@ def main(): parser.add_argument("--nodownload", action="store_true", default=False, help="Do not download new lists.") # parser.add_argument("--ozimux", type=int, default=-1, help="Override user.cfg OziMux output UDP port. (NOT IMPLEMENTED)") # parser.add_argument("--summary", type=int, default=-1, help="Override user.cfg UDP Summary output port. (NOT IMPLEMENTED)") + parser.add_argument("--freq_hz", type=float, default=None, help="Receiver IQ centre frequency in Hz, used in determine the absolute frequency of a telemetry burst.") + parser.add_argument("--freq_target_hz", type=float, default=None, help="Receiver 'target' frequency in Hz, used to add metadata to station position info.") parser.add_argument("-v", "--verbose", action="store_true", default=False, help="Verbose output (set logging level to DEBUG)") args = parser.parse_args() @@ -110,18 +114,40 @@ def main(): logging.info(f"Custom Field list contains {len(list(horusdemodlib.payloads.HORUS_CUSTOM_FIELDS.keys()))} entries.") # Start the Habitat uploader thread. + + if args.freq_target_hz: + _listener_freq_str = f" ({args.freq_target_hz/1e6:.3f} MHz)" + else: + _listener_freq_str = "" + habitat_uploader = HabitatUploader( user_callsign = user_config['user_call'], listener_lat = user_config['station_lat'], listener_lon = user_config['station_lon'], - listener_radio = user_config['radio_comment'], + listener_radio = user_config['radio_comment'] + _listener_freq_str, listener_antenna = user_config['antenna_comment'], inhibit=args.noupload ) + if user_config['station_lat'] == 0.0 and user_config['station_lon'] == 0.0: + _sondehub_user_pos = None + else: + _sondehub_user_pos = [user_config['station_lat'], user_config['station_lon'], 0.0] + + sondehub_uploader = SondehubAmateurUploader( + upload_rate = 2, + user_callsign = user_config['user_call'], + user_position = _sondehub_user_pos, + user_radio = user_config['radio_comment'], + user_antenna = user_config['antenna_comment'], + software_name = "horusdemodlib", + software_version = horusdemodlib.__version__, + inhibit=args.noupload + ) + logging.info("Using User Callsign: %s" % user_config['user_call']) - demod_stats = FSKDemodStats() + demod_stats = FSKDemodStats(peak_hold=True) logging.info("Started Horus Demod Uploader. Hit CTRL-C to exit.") # Main loop @@ -153,6 +179,10 @@ def main(): _snr = demod_stats.snr _decoded['snr'] = _snr + # Add in frequency estimate, if we have been supplied a receiver frequency. + if args.freq_hz: + _decoded['f_centre'] = int(demod_stats.fest_mean) + int(args.freq_hz) + # Send via UDP send_payload_summary(_decoded, port=user_config['summary_port']) @@ -160,6 +190,9 @@ def main(): _decoded_str = "$$" + data.split('$')[-1] + '\n' habitat_uploader.add(_decoded_str) + # Upload the string to Sondehub Amateur + sondehub_uploader.add(_decoded) + if _logfile: _logfile.write(_decoded_str) _logfile.flush() @@ -190,12 +223,19 @@ def main(): _snr = demod_stats.snr _decoded['snr'] = _snr + # Add in frequency estimate, if we have been supplied a receiver frequency. + if args.freq_hz: + _decoded['f_centre'] = int(demod_stats.fest_mean) + int(args.freq_hz) + # Send via UDP send_payload_summary(_decoded, port=user_config['summary_port']) # Upload to Habitat habitat_uploader.add(_decoded['ukhas_str']+'\n') + # Upload the string to Sondehub Amateur + sondehub_uploader.add(_decoded) + if _logfile: _logfile.write(_decoded['ukhas_str']+'\n') _logfile.flush() diff --git a/pyproject.toml b/pyproject.toml index 4ace315..42efc3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "horusdemodlib" -version = "0.2.3" +version = "0.3.0" description = "Project Horus HAB Telemetry Demodulators" authors = ["Mark Jessop"] license = "LGPL-2.1-or-later" @@ -10,6 +10,7 @@ python = "^3.6" requests = "^2.24.0" crcmod = "^1.7" numpy = "^1.17" +python-dateutil = "^2.8" [tool.poetry.dev-dependencies] diff --git a/requirements.txt b/requirements.txt index c80b783..d3deee0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ requests crcmod -numpy \ No newline at end of file +numpy +python-dateutil \ No newline at end of file diff --git a/start_dual_4fsk.sh b/start_dual_4fsk.sh index fc29178..5735af3 100755 --- a/start_dual_4fsk.sh +++ b/start_dual_4fsk.sh @@ -77,9 +77,11 @@ fi # Note - these are somewhat hard-coded for this dual-RX application. MFSK1_LOWER=$(echo "$MFSK1_SIGNAL - $RXBANDWIDTH/2" | bc) MFSK1_UPPER=$(echo "$MFSK1_SIGNAL + $RXBANDWIDTH/2" | bc) +MFSK1_CENTRE=$(echo "$RXFREQ + $MFSK1_SIGNAL" | bc) MFSK2_LOWER=$(echo "$MFSK2_SIGNAL - $RXBANDWIDTH/2" | bc) MFSK2_UPPER=$(echo "$MFSK2_SIGNAL + $RXBANDWIDTH/2" | bc) +MFSK2_CENTRE=$(echo "$RXFREQ + $MFSK2_SIGNAL" | bc) echo "Using SDR Centre Frequency: $RXFREQ Hz." echo "Using MFSK1 estimation range: $MFSK1_LOWER - $MFSK1_UPPER Hz" @@ -109,4 +111,6 @@ if [ "$STATS_OUTPUT" = "1" ]; then fi # Start the receive chain. -rtl_fm -M raw -F9 -s 48000 -p $PPM $GAIN_SETTING$BIAS_SETTING -f $RXFREQ | tee >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK1_LOWER --fsk_upper=$MFSK1_UPPER - - | python -m horusdemodlib.uploader) >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK2_LOWER --fsk_upper=$MFSK2_UPPER - - | python -m horusdemodlib.uploader) > /dev/null +# Note that we now pass in the SDR centre frequency ($RXFREQ) and 'target' signal frequency ($MFSK1_CENTRE) +# to enable providing additional metadata to Habitat / Sondehub. +rtl_fm -M raw -F9 -s 48000 -p $PPM $GAIN_SETTING$BIAS_SETTING -f $RXFREQ | tee >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK1_LOWER --fsk_upper=$MFSK1_UPPER - - | python -m horusdemodlib.uploader --freq_hz $RXFREQ --freq_target_hz $MFSK1_CENTRE ) >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK2_LOWER --fsk_upper=$MFSK2_UPPER - - | python -m horusdemodlib.uploader --freq_hz $RXFREQ ) > /dev/null diff --git a/start_dual_rtty_4fsk.sh b/start_dual_rtty_4fsk.sh index b120688..9432b0a 100755 --- a/start_dual_rtty_4fsk.sh +++ b/start_dual_rtty_4fsk.sh @@ -79,9 +79,11 @@ fi # Note - these are somewhat hard-coded for this dual-RX application. RTTY_LOWER=$(echo "$RTTY_SIGNAL - $RXBANDWIDTH/2" | bc) RTTY_UPPER=$(echo "$RTTY_SIGNAL + $RXBANDWIDTH/2" | bc) +RTTY_CENTRE=$(echo "$RXFREQ + $RTTY_SIGNAL" | bc) MFSK_LOWER=$(echo "$MFSK_SIGNAL - $RXBANDWIDTH/2" | bc) MFSK_UPPER=$(echo "$MFSK_SIGNAL + $RXBANDWIDTH/2" | bc) +MFSK_CENTRE=$(echo "$RXFREQ + $MFSK_SIGNAL" | bc) echo "Using SDR Centre Frequency: $RXFREQ Hz." echo "Using RTTY estimation range: $RTTY_LOWER - $RTTY_UPPER Hz" @@ -111,4 +113,6 @@ if [ "$STATS_OUTPUT" = "1" ]; then fi # Start the receive chain. -rtl_fm -M raw -F9 -s 48000 -p $PPM $GAIN_SETTING$BIAS_SETTING -f $RXFREQ | tee >($DECODER -q --stats=5 -g -m RTTY --fsk_lower=$RTTY_LOWER --fsk_upper=$RTTY_UPPER - - | python -m horusdemodlib.uploader --rtty) >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK_LOWER --fsk_upper=$MFSK_UPPER - - | python -m horusdemodlib.uploader) > /dev/null +# Note that we now pass in the SDR centre frequency ($RXFREQ) and 'target' signal frequency ($RTTY_CENTRE / $MFSK_CENTRE) +# to enable providing additional metadata to Habitat / Sondehub. +rtl_fm -M raw -F9 -s 48000 -p $PPM $GAIN_SETTING$BIAS_SETTING -f $RXFREQ | tee >($DECODER -q --stats=5 -g -m RTTY --fsk_lower=$RTTY_LOWER --fsk_upper=$RTTY_UPPER - - | python -m horusdemodlib.uploader --rtty --freq_hz $RXFREQ --freq_target_hz $RTTY_CENTRE ) >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK_LOWER --fsk_upper=$MFSK_UPPER - - | python -m horusdemodlib.uploader --freq_hz $RXFREQ --freq_target_hz $MFSK_CENTRE ) > /dev/null diff --git a/start_rtlsdr.sh b/start_rtlsdr.sh index ddaddcd..bae4920 100755 --- a/start_rtlsdr.sh +++ b/start_rtlsdr.sh @@ -89,4 +89,6 @@ else fi # Start the receive chain. -rtl_fm -M raw -F9 -s 48000 -p $PPM $GAIN_SETTING$BIAS_SETTING -f $SDR_RX_FREQ | $DECODER -q --stats=5 -g -m binary --fsk_lower=$FSK_LOWER --fsk_upper=$FSK_UPPER - - | python -m horusdemodlib.uploader $@ +# Note that we now pass in the SDR centre frequency ($SDR_RX_FREQ) and 'target' signal frequency ($RXFREQ) +# to enable providing additional metadata to Habitat / Sondehub. +rtl_fm -M raw -F9 -s 48000 -p $PPM $GAIN_SETTING$BIAS_SETTING -f $SDR_RX_FREQ | $DECODER -q --stats=5 -g -m binary --fsk_lower=$FSK_LOWER --fsk_upper=$FSK_UPPER - - | python -m horusdemodlib.uploader --freq_hz $SDR_RX_FREQ --freq_target_hz $RXFREQ $@ diff --git a/user.cfg.example b/user.cfg.example index 9232207..25067db 100644 --- a/user.cfg.example +++ b/user.cfg.example @@ -4,6 +4,7 @@ [user] # Your callsign - used when uploading to the HabHub Tracker. +# Note that we now also upload to the experimental SondeHub Amateur DB as well by default. callsign = YOUR_CALL_HERE # Your station latitude/longitude, which will show up on tracker.habhub.org.