diff --git a/auto_rx/auto_rx.py b/auto_rx/auto_rx.py index 8bd1de0..3d8229f 100644 --- a/auto_rx/auto_rx.py +++ b/auto_rx/auto_rx.py @@ -135,7 +135,8 @@ def start_scanner(): device_idx = _device_idx, gain = autorx.sdr_list[_device_idx]['gain'], ppm = autorx.sdr_list[_device_idx]['ppm'], - bias = autorx.sdr_list[_device_idx]['bias'] + bias = autorx.sdr_list[_device_idx]['bias'], + save_detection_audio = config['save_detection_audio'] ) # Add a reference into the sdr_list entry @@ -198,6 +199,8 @@ def start_decoder(freq, sonde_type): gain = autorx.sdr_list[_device_idx]['gain'], ppm = autorx.sdr_list[_device_idx]['ppm'], bias = autorx.sdr_list[_device_idx]['bias'], + save_decode_audio = config['save_decode_audio'], + save_decode_iq = config['save_decode_iq'], exporter = exporter_functions, timeout = config['rx_timeout'], telem_filter = telemetry_filter, @@ -487,7 +490,8 @@ def main(): _email_notification = EmailNotification( smtp_server = config['email_smtp_server'], mail_from = config['email_from'], - mail_to = config['email_to'] + mail_to = config['email_to'], + mail_subject = config['email_subject'] ) exporter_objects.append(_email_notification) diff --git a/auto_rx/autorx/__init__.py b/auto_rx/autorx/__init__.py index bd75abc..db15df8 100644 --- a/auto_rx/autorx/__init__.py +++ b/auto_rx/autorx/__init__.py @@ -6,7 +6,7 @@ # Released under GNU GPL v3 or later # -__version__ = "20190322-beta" +__version__ = "20190323-beta" # Global Variables diff --git a/auto_rx/autorx/config.py b/auto_rx/autorx/config.py index ed49e60..62aa629 100644 --- a/auto_rx/autorx/config.py +++ b/auto_rx/autorx/config.py @@ -40,11 +40,12 @@ def read_auto_rx_config(filename): auto_rx_config = { # Log Settings 'per_sonde_log' : True, - # Email Settings - 'email_enabled': False, - 'email_smtp_server': 'localhost', - 'email_from': 'sonde@localhost', - 'email_to': None, + # Email Settings + 'email_enabled': False, + 'email_smtp_server': 'localhost', + 'email_from': 'sonde@localhost', + 'email_to': None, + 'email_subject': " Sonde launch detected on : ", # SDR Settings 'sdr_fm': 'rtl_fm', 'sdr_power': 'rtl_power', @@ -114,7 +115,11 @@ def read_auto_rx_config(filename): 'ozi_update_rate': 5, 'ozi_port' : 55681, 'payload_summary_enabled': False, - 'payload_summary_port' : 55672 + 'payload_summary_port' : 55672, + # Debugging settings + 'save_detection_audio' : False, + 'save_decode_audio' : False, + 'save_decode_iq' : False } sdr_settings = {}#'0':{'ppm':0, 'gain':-1, 'bias': False}} @@ -133,8 +138,9 @@ def read_auto_rx_config(filename): auto_rx_config['email_smtp_server'] = config.get('email', 'smtp_server') auto_rx_config['email_from'] = config.get('email', 'from') auto_rx_config['email_to'] = config.get('email', 'to') + auto_rx_config['email_subject'] = config.get('email', 'subject') except: - logging.error("Config - Invalid email settings. Disabling.") + logging.error("Config - Invalid or missing email settings. Disabling.") auto_rx_config['email_enabled'] = False # SDR Settings @@ -165,6 +171,7 @@ def read_auto_rx_config(filename): auto_rx_config['habitat_payload_callsign'] = config.get('habitat', 'payload_callsign') auto_rx_config['habitat_uploader_callsign'] = config.get('habitat', 'uploader_callsign') auto_rx_config['habitat_upload_listener_position'] = config.getboolean('habitat','upload_listener_position') + auto_rx_config['habitat_uploader_antenna'] = config.get('habitat', 'uploader_antenna').strip() # APRS Settings auto_rx_config['aprs_enabled'] = config.getboolean('aprs', 'aprs_enabled') @@ -174,6 +181,15 @@ def read_auto_rx_config(filename): auto_rx_config['aprs_server'] = config.get('aprs', 'aprs_server') auto_rx_config['aprs_object_id'] = config.get('aprs', 'aprs_object_id') auto_rx_config['aprs_custom_comment'] = config.get('aprs', 'aprs_custom_comment') + auto_rx_config['aprs_position_report'] = config.getboolean('aprs','aprs_position_report') + auto_rx_config['station_beacon_enabled'] = config.getboolean('aprs','station_beacon_enabled') + auto_rx_config['station_beacon_rate'] = config.getint('aprs', 'station_beacon_rate') + auto_rx_config['station_beacon_comment'] = config.get('aprs', 'station_beacon_comment') + auto_rx_config['station_beacon_icon'] = config.get('aprs', 'station_beacon_icon') + + if auto_rx_config['station_beacon_enabled'] and auto_rx_config['station_lat']==0.0 and auto_rx_config['station_lon'] == 0.0: + auto_rx_config['station_beacon_enabled'] = False + logging.error("Config - Disable APRS Station beacon, as no station lat/lon set.") # OziPlotter Settings auto_rx_config['ozi_enabled'] = config.getboolean('oziplotter', 'ozi_enabled') @@ -203,48 +219,21 @@ def read_auto_rx_config(filename): auto_rx_config['rotator_homing_enabled'] = config.getboolean('rotator', 'rotator_homing_enabled') auto_rx_config['rotator_home_azimuth'] = config.getfloat('rotator', 'rotator_home_azimuth') auto_rx_config['rotator_home_elevation'] = config.getfloat('rotator', 'rotator_home_elevation') + auto_rx_config['rotator_homing_delay'] = config.getint('rotator', 'rotator_homing_delay') + auto_rx_config['rotation_threshold'] = config.getfloat('rotator', 'rotation_threshold') + # Web interface settings. + auto_rx_config['web_host'] = config.get('web', 'web_host') + auto_rx_config['web_port'] = config.getint('web', 'web_port') + auto_rx_config['web_archive_age'] = config.getint('web', 'archive_age') - # New setting in this version (20180616). Keep it in a try-catch to avoid bombing out if the new setting isn't present. + # New debug settings - added 2019-03-23 try: - auto_rx_config['habitat_uploader_antenna'] = config.get('habitat', 'uploader_antenna').strip() + auto_rx_config['save_detection_audio'] = config.getboolean('debugging', 'save_detection_audio') + auto_rx_config['save_decode_audio'] = config.getboolean('debugging', 'save_decode_audio') + auto_rx_config['save_decode_iq'] = config.getboolean('debugging', 'save_decode_iq') except: - logging.error("Config - Missing uploader_antenna setting. Using default.") - auto_rx_config['habitat_uploader_antenna'] = '1/4-wave' - - # New settings added in 20180624. - try: - auto_rx_config['web_host'] = config.get('web', 'web_host') - auto_rx_config['web_port'] = config.getint('web', 'web_port') - auto_rx_config['web_archive_age'] = config.getint('web', 'archive_age') - except: - logging.error("Config - Missing Web Server settings. Using defaults.") - auto_rx_config['web_host'] = '0.0.0.0' - auto_rx_config['web_port'] = 5000 - auto_rx_config['web_archive_age'] = 120 - - # New setting added in 201810xx (Rotator updates) - try: - auto_rx_config['rotator_homing_delay'] = config.getint('rotator', 'rotator_homing_delay') - auto_rx_config['rotation_threshold'] = config.getfloat('rotator', 'rotation_threshold') - except: - logging.error("Config - Missing new rotator settings, using defaults.") - - - # New APRS Station Beaconing settings added in 201812xx - try: - auto_rx_config['aprs_position_report'] = config.getboolean('aprs','aprs_position_report') - auto_rx_config['station_beacon_enabled'] = config.getboolean('aprs','station_beacon_enabled') - auto_rx_config['station_beacon_rate'] = config.getint('aprs', 'station_beacon_rate') - auto_rx_config['station_beacon_comment'] = config.get('aprs', 'station_beacon_comment') - auto_rx_config['station_beacon_icon'] = config.get('aprs', 'station_beacon_icon') - - if auto_rx_config['station_beacon_enabled'] and auto_rx_config['station_lat']==0.0 and auto_rx_config['station_lon'] == 0.0: - auto_rx_config['station_beacon_enabled'] = False - logging.error("Config - Disable APRS Station beacon, as no station lat/lon set.") - except: - logging.error("Config - APRS Station Beacon settings missing, using defaults.") - + logging.error("Config - Could not find debugging settings - using defaults.") # Now we attempt to read in the individual SDR parameters. diff --git a/auto_rx/autorx/decode.py b/auto_rx/autorx/decode.py index b2be71c..81f889d 100644 --- a/auto_rx/autorx/decode.py +++ b/auto_rx/autorx/decode.py @@ -74,6 +74,8 @@ class SondeDecoder(object): ppm = 0, gain = -1, bias = False, + save_decode_audio = False, + save_decode_iq = False, exporter = None, timeout = 180, @@ -95,6 +97,11 @@ class SondeDecoder(object): gain (int): SDR Gain setting, in dB. A gain setting of -1 enables the RTLSDR AGC. bias (bool): If True, enable the bias tee on the SDR. + save_decode_audio (bool): If True, save the FM-demodulated audio to disk to decode_.wav. + Note: This may use up a lot of disk space! + save_decode_iq (bool): If True, save the decimated IQ stream (48 or 96k complex s16 samples) to disk to decode_IQ_.bin + Note: This will use up a lot of disk space! + exporter (function, list): Either a function, or a list of functions, which accept a single dictionary. Fields described above. timeout (int): Timeout after X seconds of no valid data received from the decoder. Defaults to 180. telem_filter (function): An optional filter function, which determines if a telemetry frame is valid. @@ -118,6 +125,8 @@ class SondeDecoder(object): self.ppm = ppm self.gain = gain self.bias = bias + self.save_decode_audio = save_decode_audio + self.save_decode_iq = save_decode_iq self.telem_filter = telem_filter self.timeout = timeout @@ -222,6 +231,11 @@ class SondeDecoder(object): # Note: Have removed a 'highpass 20' filter from the sox line, will need to re-evaluate if adding that is useful in the future. decode_cmd = "%s %s-p %d -d %s %s-M fm -F9 -s 15k -f %d 2>/dev/null |" % (self.sdr_fm, bias_option, int(self.ppm), str(self.device_idx), gain_param, self.sonde_freq) decode_cmd += "sox -t raw -r 15k -e s -b 16 -c 1 - -r 48000 -b 8 -t wav - lowpass 2600 2>/dev/null |" + + # Add in tee command to save audio to disk if debugging is enabled. + if self.save_decode_audio: + decode_cmd += " tee decode_%s.wav |" % str(self.device_idx) + decode_cmd += "./rs41ecc --crc --ecc --ptu --json 2>/dev/null" elif self.sonde_type == "RS92": @@ -262,6 +276,11 @@ class SondeDecoder(object): # rtl_fm -p 0 -g 26.0 -M fm -F9 -s 12k -f 400500000 | sox -t raw -r 12k -e s -b 16 -c 1 - -r 48000 -b 8 -t wav - highpass 20 lowpass 2500 2>/dev/null | ./rs92ecc -vx -v --crc --ecc --vel -e ephemeris.dat decode_cmd = "%s %s-p %d -d %s %s-M fm -F9 -s %d -f %d 2>/dev/null |" % (self.sdr_fm, bias_option, int(self.ppm), str(self.device_idx), gain_param, _rx_bw, self.sonde_freq) decode_cmd += "sox -t raw -r %d -e s -b 16 -c 1 - -r 48000 -b 8 -t wav - lowpass 2500 highpass 20 2>/dev/null |" % _rx_bw + + # Add in tee command to save audio to disk if debugging is enabled. + if self.save_decode_audio: + decode_cmd += " tee decode_%s.wav |" % str(self.device_idx) + decode_cmd += "./rs92ecc -vx -v --crc --ecc --vel --json %s 2>/dev/null" % _rs92_gps_data elif self.sonde_type == "DFM": @@ -273,6 +292,11 @@ class SondeDecoder(object): # Note: Have removed a 'highpass 20' filter from the sox line, will need to re-evaluate if adding that is useful in the future. decode_cmd = "%s %s-p %d -d %s %s-M fm -F9 -s 15k -f %d 2>/dev/null |" % (self.sdr_fm, bias_option, int(self.ppm), str(self.device_idx), gain_param, self.sonde_freq) decode_cmd += "sox -t raw -r 15k -e s -b 16 -c 1 - -r 48000 -b 8 -t wav - highpass 20 lowpass 2000 2>/dev/null |" + + # Add in tee command to save audio to disk if debugging is enabled. + if self.save_decode_audio: + decode_cmd += " tee decode_%s.wav |" % str(self.device_idx) + # DFM decoder decode_cmd += "./dfm09ecc -vv --ecc --json --dist --auto 2>/dev/null" @@ -281,6 +305,11 @@ class SondeDecoder(object): decode_cmd = "%s %s-p %d -d %s %s-M fm -F9 -s 22k -f %d 2>/dev/null |" % (self.sdr_fm, bias_option, int(self.ppm), str(self.device_idx), gain_param, self.sonde_freq) decode_cmd += "sox -t raw -r 22k -e s -b 16 -c 1 - -r 48000 -b 8 -t wav - highpass 20 2>/dev/null |" + + # Add in tee command to save audio to disk if debugging is enabled. + if self.save_decode_audio: + decode_cmd += " tee decode_%s.wav |" % str(self.device_idx) + # M10 decoder decode_cmd += "./m10 -b -b2 2>/dev/null" @@ -289,6 +318,11 @@ class SondeDecoder(object): decode_cmd = "%s %s-p %d -d %s %s-M fm -F9 -s 15k -f %d 2>/dev/null |" % (self.sdr_fm, bias_option, int(self.ppm), str(self.device_idx), gain_param, self.sonde_freq) decode_cmd += "sox -t raw -r 15k -e s -b 16 -c 1 - -r 48000 -b 8 -t wav - highpass 20 2>/dev/null |" + + # Add in tee command to save audio to disk if debugging is enabled. + if self.save_decode_audio: + decode_cmd += " tee decode_%s.wav |" % str(self.device_idx) + # iMet-4 (IMET1RS) decoder decode_cmd += "./imet1rs_dft --json 2>/dev/null" diff --git a/auto_rx/autorx/email_notification.py b/auto_rx/autorx/email_notification.py index 81bedff..387f579 100644 --- a/auto_rx/autorx/email_notification.py +++ b/auto_rx/autorx/email_notification.py @@ -32,11 +32,12 @@ class EmailNotification(object): # We require the following fields to be present in the input telemetry dict. REQUIRED_FIELDS = [ 'id', 'lat', 'lon', 'alt', 'type', 'freq'] - def __init__(self, smtp_server = 'localhost', mail_from = None, mail_to = None): + def __init__(self, smtp_server = 'localhost', mail_from = None, mail_to = None, mail_subject = None): """ Init a new E-Mail Notification Thread """ self.smtp_server = smtp_server self.mail_from = mail_from self.mail_to = mail_to + self.mail_subject = mail_subject # Dictionary to track sonde IDs self.sondes = {} @@ -103,7 +104,13 @@ class EmailNotification(object): msg += 'https://sondehub.org/%s\n' % _id msg = MIMEText(msg, 'plain', 'UTF-8') - msg['Subject'] = 'Sonde launch detected: ' + _id + + # Construct subject + msg['Subject'] = self.mail_subject + msg['Subject'].replace('', telemetry['id']) + msg['Subject'].replace('', telemetry['type']) + msg['Subject'].replace('', telemetry['freq']) + msg['From'] = self.mail_from msg['To'] = self.mail_to msg["Date"] = formatdate() diff --git a/auto_rx/autorx/scan.py b/auto_rx/autorx/scan.py index ce121a3..d945531 100644 --- a/auto_rx/autorx/scan.py +++ b/auto_rx/autorx/scan.py @@ -176,7 +176,7 @@ def read_rtl_power(filename): return (freq, power, freq_step) -def detect_sonde(frequency, rs_path="./", dwell_time=10, sdr_fm='rtl_fm', device_idx=0, ppm=0, gain=-1, bias=False): +def detect_sonde(frequency, rs_path="./", dwell_time=10, sdr_fm='rtl_fm', device_idx=0, ppm=0, gain=-1, bias=False, save_detection_audio = False): """ Receive some FM and attempt to detect the presence of a radiosonde. Args: @@ -188,6 +188,7 @@ def detect_sonde(frequency, rs_path="./", dwell_time=10, sdr_fm='rtl_fm', device ppm (int): SDR Frequency accuracy correction, in ppm. gain (int): SDR Gain setting, in dB. A gain setting of -1 enables the RTLSDR AGC. bias (bool): If True, enable the bias tee on the SDR. + save_detection_audio (bool): Save the audio used in detection to a file. Returns: str/None: Returns None if no sonde found, otherwise returns a sonde type, from the following: @@ -223,7 +224,12 @@ def detect_sonde(frequency, rs_path="./", dwell_time=10, sdr_fm='rtl_fm', device # Sample Source (rtl_fm) rx_test_command = "timeout %ds %s %s-p %d -d %s %s-M fm -F9 -s %d -f %d 2>/dev/null |" % (dwell_time*2, sdr_fm, bias_option, int(ppm), str(device_idx), gain_param, _rx_bw, frequency) # Sample filtering - rx_test_command += "sox -t raw -r %d -e s -b 16 -c 1 - -r 48000 -t wav - highpass 20 2>/dev/null |" % _rx_bw + rx_test_command += "sox -t raw -r %d -e s -b 16 -c 1 - -r 48000 -t wav - highpass 20 2>/dev/null | " % _rx_bw + + # Saving of Debug audio, if enabled, + if save_detection_audio: + rx_test_command += "tee detect_%s.wav | " % str(device_idx) + # Sample decoding / detection # Note that we detect for dwell_time seconds, and timeout after dwell_time*2, to catch if no samples are being passed through. rx_test_command += os.path.join(rs_path,"dft_detect") + " -t %d 2>/dev/null" % dwell_time @@ -351,7 +357,8 @@ class SondeScanner(object): device_idx = 0, gain = -1, ppm = 0, - bias = False): + bias = False, + save_detection_audio = False): """ Initialise a Sonde Scanner Object. Apologies for the huge number of args... @@ -382,6 +389,7 @@ class SondeScanner(object): ppm (int): SDR Frequency accuracy correction, in ppm. gain (int): SDR Gain setting, in dB. A gain setting of -1 enables the RTLSDR AGC. bias (bool): If True, enable the bias tee on the SDR. + save_detection_audio (bool): Save the audio used in each detecton to detect_.wav """ # Thread flag. This is set to True when a scan is running. @@ -410,6 +418,7 @@ class SondeScanner(object): self.ppm = ppm self.bias = bias self.callback = callback + self.save_detection_audio = save_detection_audio # Error counter. @@ -663,7 +672,8 @@ class SondeScanner(object): ppm=self.ppm, gain=self.gain, bias=self.bias, - dwell_time=self.detect_dwell_time) + dwell_time=self.detect_dwell_time, + save_detection_audio=self.save_detection_audio) if detected != None: # Add a detected sonde to the output array diff --git a/auto_rx/station.cfg.example b/auto_rx/station.cfg.example index 1033447..3047b07 100644 --- a/auto_rx/station.cfg.example +++ b/auto_rx/station.cfg.example @@ -219,6 +219,11 @@ email_enabled = False smtp_server = localhost from = sonde@localhost to = someone@example.com +# Custom subject field. The following fields can be included: +# - Sonde Frequency, i.e. 401.520 MHz +# - Sonde Type (RS94/RS41) +# - Sonde Serial Number (i.e. M1234567) +subject = Sonde launch detected on : @@ -273,6 +278,28 @@ web_port = 5000 archive_age = 120 +################## +# DEBUG SETTINGS # +################## +[debugging] +# WARNING - Enabling these settings can result in lots of SD-card IO, potentially +# reducing the life of the card. These should only be enabled to collect data for +# debugging purposes. + +# Save the audio that a detection pass is run over to: detect_.wav +# This file is over-written with every new detection. +save_detection_audio = False + +# Save the audio from the output from a sonde decode chain to decode_.wav +# This file is over-written with each new sonde decoded for a particular SDR. +save_decode_audio = False + +# Save the decimated IQ data from an experimental sonde decode chain to decode_IQ_.bin +# This will be in complex signed 16-bit int format, and may be either 48 kHz or 96 kHz. +# Note: This will use a LOT of disk space. +save_decode_iq = False + + ##################### # ADVANCED SETTINGS #