2017-04-29 02:00:10 +00:00
#!/usr/bin/env python
#
2018-05-26 09:18:53 +00:00
# Radiosonde Auto RX Service - V2.0
2017-04-29 02:00:10 +00:00
#
2018-05-26 09:18:53 +00:00
# Copyright (C) 2018 Mark Jessop <vk5qi@rfhead.net>
# Released under GNU GPL v3 or later
2017-04-29 02:00:10 +00:00
#
2018-05-26 09:18:53 +00:00
# Refer github page for instructions on setup and usage.
# https://github.com/projecthorus/radiosonde_auto_rx/
2017-04-29 14:47:26 +00:00
#
2017-05-05 12:56:41 +00:00
import argparse
2017-04-29 14:06:47 +00:00
import datetime
2018-05-26 09:18:53 +00:00
import logging
import re
import sys
2017-04-29 14:06:47 +00:00
import time
import traceback
2019-01-27 18:17:05 +00:00
import os
2017-05-06 23:42:46 +00:00
2018-06-18 11:17:38 +00:00
import autorx
2018-05-26 09:18:53 +00:00
from autorx . scan import SondeScanner
2019-04-24 12:17:20 +00:00
from autorx . decode import SondeDecoder , VALID_SONDE_TYPES , DRIFTY_SONDE_TYPES
2018-05-26 09:18:53 +00:00
from autorx . logger import TelemetryLogger
2018-07-04 08:13:58 +00:00
from autorx . email_notification import EmailNotification
2018-05-26 09:18:53 +00:00
from autorx . habitat import HabitatUploader
2018-05-26 11:51:55 +00:00
from autorx . aprs import APRSUploader
2018-05-27 10:59:49 +00:00
from autorx . ozimux import OziUploader
2018-10-04 11:45:18 +00:00
from autorx . rotator import Rotator
2019-03-24 06:18:19 +00:00
from autorx . utils import rtlsdr_test , position_info , check_rs_utils , check_autorx_version
2018-05-26 09:18:53 +00:00
from autorx . config import read_auto_rx_config
2018-06-29 13:02:58 +00:00
from autorx . web import start_flask , stop_flask , flask_emit_event , WebHandler , WebExporter
2019-06-22 08:15:46 +00:00
from autorx . gpsd import GPSDAdaptor
2017-07-16 10:11:13 +00:00
2018-05-26 09:18:53 +00:00
try :
# Python 2
from Queue import Queue
except ImportError :
# Python 3
from queue import Queue
2017-04-29 14:06:47 +00:00
2018-05-26 09:18:53 +00:00
# Logging level
# INFO = Basic status messages
# DEBUG = Adds detailed information on submodule operations.
logging_level = logging . INFO
2017-04-29 14:06:47 +00:00
2018-05-26 09:18:53 +00:00
#
# Global Variables
#
2017-04-29 14:06:47 +00:00
2018-05-26 09:18:53 +00:00
RS_PATH = " ./ "
2017-04-29 14:06:47 +00:00
2018-05-26 09:18:53 +00:00
# Optional override for RS92 ephemeris data.
rs92_ephemeris = None
2017-04-29 14:06:47 +00:00
2018-06-01 11:32:24 +00:00
# Global configuration dictionary. Populated on startup.
2018-05-26 09:18:53 +00:00
config = None
2017-04-29 14:06:47 +00:00
2018-05-26 09:18:53 +00:00
# Exporter Lists
exporter_objects = [ ] # This list will hold references to each exporter instance that is created.
exporter_functions = [ ] # This list will hold references to the exporter add functions, which will be passed onto the decoders.
2017-04-29 14:06:47 +00:00
2017-07-16 10:11:13 +00:00
2019-06-22 08:15:46 +00:00
# GPSDAdaptor Instance, if used.
gpsd_adaptor = None
2019-04-14 05:12:42 +00:00
# Temporary frequency block list
# This contains frequncies that should be blocked for a short amount of time.
temporary_block_list = { }
2017-04-29 14:06:47 +00:00
2017-04-29 02:00:10 +00:00
2018-05-26 09:18:53 +00:00
def allocate_sdr ( check_only = False , task_description = " " ) :
""" Allocate an un-used SDR for a task.
2017-05-06 23:42:46 +00:00
2018-05-26 09:18:53 +00:00
Args :
check_only ( bool ) : If True , don ' t set the free SDR as in-use. Used to check if there are any free SDRs.
2017-12-17 01:44:20 +00:00
2018-05-26 09:18:53 +00:00
Returns :
( str ) : The device index / serial number of the free / allocated SDR , if one is free , else None .
"""
2017-07-16 10:11:13 +00:00
2018-06-18 11:17:38 +00:00
for _idx in autorx . sdr_list . keys ( ) :
if autorx . sdr_list [ _idx ] [ ' in_use ' ] == False :
2018-05-26 09:18:53 +00:00
# Found a free SDR!
if check_only :
# If we are just checking to see if there are any SDRs free, we don't allocate it.
pass
else :
# Otherwise, set the SDR as in-use.
2018-06-18 11:17:38 +00:00
autorx . sdr_list [ _idx ] [ ' in_use ' ] = True
2018-05-26 13:52:46 +00:00
logging . info ( " SDR # %s has been allocated to %s . " % ( str ( _idx ) , task_description ) )
2019-01-27 18:17:05 +00:00
2018-05-26 09:18:53 +00:00
return _idx
# Otherwise, no SDRs are free.
return None
def start_scanner ( ) :
""" Start a scanner thread on the first available SDR """
2019-05-04 03:12:24 +00:00
global config , RS_PATH , temporary_block_list
2018-05-26 09:18:53 +00:00
2018-06-18 11:17:38 +00:00
if ' SCAN ' in autorx . task_list :
2018-05-26 09:18:53 +00:00
# Already a scanner running! Return.
logging . debug ( " Task Manager - Attempted to start a scanner, but one already running. " )
return
# Attempt to allocate a SDR.
_device_idx = allocate_sdr ( task_description = " Scanner " )
if _device_idx is None :
logging . debug ( " Task Manager - No SDRs free to run Scanner. " )
return
2017-12-17 01:44:20 +00:00
else :
2018-05-26 09:18:53 +00:00
# Create entry in task list.
2018-06-18 11:17:38 +00:00
autorx . task_list [ ' SCAN ' ] = { ' device_idx ' : _device_idx , ' task ' : None }
2018-05-26 09:18:53 +00:00
# Init Scanner using settings from the global config.
2018-06-01 11:32:24 +00:00
# TODO: Nicer way of passing in the huge list of args.
2018-06-18 11:17:38 +00:00
autorx . task_list [ ' SCAN ' ] [ ' task ' ] = SondeScanner (
2019-05-04 03:12:24 +00:00
callback = autorx . scan_results . put ,
2018-05-26 09:18:53 +00:00
auto_start = True ,
min_freq = config [ ' min_freq ' ] ,
max_freq = config [ ' max_freq ' ] ,
search_step = config [ ' search_step ' ] ,
whitelist = config [ ' whitelist ' ] ,
greylist = config [ ' greylist ' ] ,
blacklist = config [ ' blacklist ' ] ,
snr_threshold = config [ ' snr_threshold ' ] ,
min_distance = config [ ' min_distance ' ] ,
quantization = config [ ' quantization ' ] ,
scan_dwell_time = config [ ' scan_dwell_time ' ] ,
detect_dwell_time = config [ ' detect_dwell_time ' ] ,
max_peaks = config [ ' max_peaks ' ] ,
rs_path = RS_PATH ,
sdr_power = config [ ' sdr_power ' ] ,
sdr_fm = config [ ' sdr_fm ' ] ,
device_idx = _device_idx ,
2018-06-18 11:17:38 +00:00
gain = autorx . sdr_list [ _device_idx ] [ ' gain ' ] ,
ppm = autorx . sdr_list [ _device_idx ] [ ' ppm ' ] ,
2019-03-23 11:21:08 +00:00
bias = autorx . sdr_list [ _device_idx ] [ ' bias ' ] ,
2019-04-14 05:12:42 +00:00
save_detection_audio = config [ ' save_detection_audio ' ] ,
temporary_block_list = temporary_block_list ,
temporary_block_time = config [ ' temporary_block_time ' ]
2018-05-26 09:18:53 +00:00
)
# Add a reference into the sdr_list entry
2018-06-18 11:17:38 +00:00
autorx . sdr_list [ _device_idx ] [ ' task ' ] = autorx . task_list [ ' SCAN ' ] [ ' task ' ]
2019-01-27 18:17:05 +00:00
2018-06-29 13:02:58 +00:00
# Indicate to the web client that the task list has been updated.
flask_emit_event ( ' task_event ' )
2018-05-26 09:18:53 +00:00
def stop_scanner ( ) :
""" Stop a currently running scan thread, and release the SDR it was using. """
2018-06-18 11:17:38 +00:00
if ' SCAN ' not in autorx . task_list :
2018-05-26 09:18:53 +00:00
# No scanner thread running!
# This means we likely have a SDR free already.
return
2018-05-07 12:03:31 +00:00
else :
2018-05-26 09:18:53 +00:00
logging . info ( " Halting Scanner to decode detected radiosonde. " )
2018-06-18 11:17:38 +00:00
_scan_sdr = autorx . task_list [ ' SCAN ' ] [ ' device_idx ' ]
2018-05-26 09:18:53 +00:00
# Stop the scanner.
2018-06-18 11:17:38 +00:00
autorx . task_list [ ' SCAN ' ] [ ' task ' ] . stop ( )
2018-05-26 09:18:53 +00:00
# Relase the SDR.
2018-06-18 11:17:38 +00:00
autorx . sdr_list [ _scan_sdr ] [ ' in_use ' ] = False
autorx . sdr_list [ _scan_sdr ] [ ' task ' ] = None
2018-05-26 09:18:53 +00:00
# Remove the scanner task from the task list
2018-06-18 11:17:38 +00:00
autorx . task_list . pop ( ' SCAN ' )
2018-05-26 09:18:53 +00:00
2018-06-01 11:32:24 +00:00
2018-05-26 09:18:53 +00:00
def start_decoder ( freq , sonde_type ) :
2018-06-01 11:32:24 +00:00
""" Attempt to start a decoder thread for a given sonde.
Args :
freq ( float ) : Radiosonde frequency in Hz .
2019-03-17 11:28:09 +00:00
sonde_type ( str ) : The radiosonde type ( ' RS41 ' , ' RS92 ' , ' DFM ' , ' M10, ' iMet ' )
2018-06-01 11:32:24 +00:00
"""
2019-04-14 05:12:42 +00:00
global config , RS_PATH , exporter_functions , rs92_ephemeris , temporary_block_list
2018-05-26 09:18:53 +00:00
# Allocate a SDR.
_device_idx = allocate_sdr ( task_description = " Decoder ( %s , %.3f MHz) " % ( sonde_type , freq / 1e6 ) )
if _device_idx is None :
logging . error ( " Could not allocate SDR for decoder! " )
return
2017-04-29 14:06:47 +00:00
else :
2018-05-26 09:18:53 +00:00
# Add an entry to the task list
2018-06-18 11:17:38 +00:00
autorx . task_list [ freq ] = { ' device_idx ' : _device_idx , ' task ' : None }
2018-05-26 09:18:53 +00:00
# Set the SDR to in-use
2018-06-18 11:17:38 +00:00
autorx . sdr_list [ _device_idx ] [ ' in_use ' ] = True
2018-05-26 09:18:53 +00:00
2019-06-01 11:40:51 +00:00
if sonde_type . startswith ( ' - ' ) :
_exp_sonde_type = sonde_type [ 1 : ]
else :
_exp_sonde_type = sonde_type
2018-05-26 09:18:53 +00:00
# Initialise a decoder.
2018-06-18 11:17:38 +00:00
autorx . task_list [ freq ] [ ' task ' ] = SondeDecoder (
2018-05-26 09:18:53 +00:00
sonde_type = sonde_type ,
sonde_freq = freq ,
rs_path = RS_PATH ,
sdr_fm = config [ ' sdr_fm ' ] ,
device_idx = _device_idx ,
2018-06-18 11:17:38 +00:00
gain = autorx . sdr_list [ _device_idx ] [ ' gain ' ] ,
ppm = autorx . sdr_list [ _device_idx ] [ ' ppm ' ] ,
bias = autorx . sdr_list [ _device_idx ] [ ' bias ' ] ,
2019-03-23 11:21:08 +00:00
save_decode_audio = config [ ' save_decode_audio ' ] ,
save_decode_iq = config [ ' save_decode_iq ' ] ,
2018-05-26 09:18:53 +00:00
exporter = exporter_functions ,
timeout = config [ ' rx_timeout ' ] ,
telem_filter = telemetry_filter ,
2019-03-24 05:37:04 +00:00
rs92_ephemeris = rs92_ephemeris ,
2019-04-23 12:15:42 +00:00
imet_location = config [ ' station_code ' ] ,
rs41_drift_tweak = config [ ' rs41_drift_tweak ' ] ,
2019-06-01 11:40:51 +00:00
experimental_decoder = config [ ' experimental_decoders ' ] [ _exp_sonde_type ] ,
2019-04-26 09:14:06 +00:00
decoder_stats = config [ ' decoder_stats ' ]
2018-05-26 09:18:53 +00:00
)
2018-06-18 11:17:38 +00:00
autorx . sdr_list [ _device_idx ] [ ' task ' ] = autorx . task_list [ freq ] [ ' task ' ]
2018-05-26 09:18:53 +00:00
2018-06-29 13:02:58 +00:00
# Indicate to the web client that the task list has been updated.
flask_emit_event ( ' task_event ' )
2018-05-26 09:18:53 +00:00
def handle_scan_results ( ) :
""" Read in Scan results via the scan results Queue.
Depending on how many SDRs are available , two things can happen :
- If there is a free SDR , allocate it to a decoder .
- If there is no free SDR , but a scanner is running , stop the scanner and start decoding .
"""
2019-05-04 03:12:24 +00:00
global config , temporary_block_list
2018-06-01 11:32:24 +00:00
2019-05-04 03:12:24 +00:00
if autorx . scan_results . qsize ( ) > 0 :
2018-06-01 11:32:24 +00:00
# Grab the latest detections from the scan result queue.
2019-05-04 03:12:24 +00:00
_scan_data = autorx . scan_results . get ( )
2018-05-26 09:18:53 +00:00
for _sonde in _scan_data :
2018-06-01 11:32:24 +00:00
# Extract frequency & type info
2018-05-26 09:18:53 +00:00
_freq = _sonde [ 0 ]
_type = _sonde [ 1 ]
2018-06-18 11:17:38 +00:00
if _freq in autorx . task_list :
2018-05-26 09:18:53 +00:00
# Already decoding this sonde, continue.
2017-12-20 04:23:29 +00:00
continue
2018-05-26 09:18:53 +00:00
else :
2019-02-02 04:53:26 +00:00
2019-05-04 03:12:24 +00:00
# Check that we are not attempting to start a decoder too close to an existing decoder for known 'drifty' radiosonde types.
# 'Too close' is defined by the 'decoder_spacing_limit' advanced coniguration option.
_too_close = False
for _key in autorx . task_list . keys ( ) :
# Iterate through the task list, and only attempt to compare with those that are a decoder task.
# This is indicated by the task key being an integer (the sonde frequency).
if ( type ( _key ) == int ) or ( type ( _key ) == float ) :
# Extract the currently decoded sonde type from the currently running decoder.
_decoding_sonde_type = autorx . task_list [ _key ] [ ' task ' ] . sonde_type
# Only check the frequency spacing if we have a known 'drifty' sonde type, *and* the new sonde type is of the same type.
if ( _decoding_sonde_type in DRIFTY_SONDE_TYPES ) and ( _decoding_sonde_type == _type ) :
if ( abs ( _key - _freq ) < config [ ' decoder_spacing_limit ' ] ) :
# At this point, we can be pretty sure that there is another decoder already decoding this particular sonde ID.
# Without actually starting another decoder and matching IDs, we can't be 100% sure, but it's a good chance.
logging . error ( " Task Manager - Detected %s sonde on %.3f MHz, but this is within %d kHz of an already running decoder. (This limit can be set using the ' decoder_spacing_limit ' advanced config option.) " %
( _type ,
_freq / 1e6 ,
config [ ' decoder_spacing_limit ' ] / 1e3 ) )
_too_close = True
continue
# Continue to the next scan result if this one is too close to a currently running decoder.
if _too_close :
continue
# Check the frequency is not in our temporary block list
# (This may happen from time-to-time depending on the timing of the scan thread)
if _freq in temporary_block_list . keys ( ) :
if temporary_block_list [ _freq ] > ( time . time ( ) - config [ ' temporary_block_time ' ] * 60 ) :
logging . error ( " Task Manager - Attempted to start a decoder on a temporarily blocked frequency ( %.3f MHz) " % ( _freq / 1e6 ) )
continue
else :
# This frequency should not be blocked any more, remove it from the block list.
logging . info ( " Task Manager - Removed %.3f MHz from temporary block list. " % ( _freq / 1e6 ) )
temporary_block_list . pop ( _freq )
2019-02-02 04:53:26 +00:00
# Handle an inverted sonde detection.
if _type . startswith ( ' - ' ) :
_inverted = " (Inverted) "
_check_type = _type [ 1 : ]
else :
_check_type = _type
_inverted = " "
# Note: We don't indicate if it's been detected as inverted here.
2019-05-04 03:12:24 +00:00
logging . info ( " Task Manager - Detected new %s sonde on %.3f MHz! " % ( _check_type , _freq / 1e6 ) )
2018-08-01 11:37:55 +00:00
# Break if we don't support this sonde type.
2019-02-02 04:53:26 +00:00
if ( _check_type not in VALID_SONDE_TYPES ) :
2019-05-04 03:12:24 +00:00
logging . error ( " Task Manager - Unsupported sonde type: %s " % _check_type )
2019-04-14 05:12:42 +00:00
# TODO - Potentially add the frequency of the unsupported sonde to the temporary block list?
2018-08-01 11:37:55 +00:00
continue
2018-05-26 09:18:53 +00:00
if allocate_sdr ( check_only = True ) is not None :
# There is a SDR free! Start the decoder on that SDR
start_decoder ( _freq , _type )
2018-06-18 11:17:38 +00:00
elif ( allocate_sdr ( check_only = True ) is None ) and ( ' SCAN ' in autorx . task_list ) :
2018-05-26 09:18:53 +00:00
# We have run out of SDRs, but a scan thread is running.
# Stop the scan thread and take that receiver!
stop_scanner ( )
start_decoder ( _freq , _type )
else :
2018-06-01 11:32:24 +00:00
# We have no SDRs free.
# TODO: Alert the user that a sonde was detected, but no SDR was available,
# but don't do this EVERY time we detect the sonde...
2018-05-26 09:18:53 +00:00
pass
2017-05-06 23:42:46 +00:00
2017-07-18 12:39:55 +00:00
2018-06-01 11:32:24 +00:00
2018-05-26 09:18:53 +00:00
def clean_task_list ( ) :
""" Check the task list to see if any tasks have stopped running. If so, release the associated SDR """
2017-07-18 12:35:17 +00:00
2018-06-18 11:17:38 +00:00
for _key in autorx . task_list . keys ( ) :
2018-05-26 09:18:53 +00:00
# Attempt to get the state of the task
try :
2018-06-18 11:17:38 +00:00
_running = autorx . task_list [ _key ] [ ' task ' ] . running ( )
_task_sdr = autorx . task_list [ _key ] [ ' device_idx ' ]
2019-04-14 05:12:42 +00:00
_exit_state = autorx . task_list [ _key ] [ ' task ' ] . exit_state
2018-05-26 09:18:53 +00:00
except Exception as e :
logging . error ( " Task Manager - Error getting task %s state - %s " % ( str ( _key ) , str ( e ) ) )
continue
2017-05-06 23:42:46 +00:00
2018-05-26 09:18:53 +00:00
if _running == False :
2019-04-14 05:12:42 +00:00
# This task has stopped.
# Check the exit state of the task for any abnormalities:
if _exit_state == " Encrypted " :
# This task was a decoder, and it has encountered an encrypted sonde.
logging . info ( " Task Manager - Adding temporary block for frequency %.3f MHz " % ( _key / 1e6 ) )
# Add the sonde's frequency to the global temporary block-list
temporary_block_list [ _key ] = time . time ( )
# If there is a scanner currently running, add it to the scanners internal block list.
if ' SCAN ' in autorx . task_list :
2019-05-16 12:06:39 +00:00
autorx . task_list [ ' SCAN ' ] [ ' task ' ] . add_temporary_block ( _key )
2019-04-14 05:12:42 +00:00
# Release its associated SDR.
2018-06-18 11:17:38 +00:00
autorx . sdr_list [ _task_sdr ] [ ' in_use ' ] = False
autorx . sdr_list [ _task_sdr ] [ ' task ' ] = None
2019-04-14 05:12:42 +00:00
2018-05-26 09:18:53 +00:00
# Pop the task from the task list.
2018-06-18 11:17:38 +00:00
autorx . task_list . pop ( _key )
2018-06-29 13:02:58 +00:00
# Indicate to the web client that the task list has been updated.
flask_emit_event ( ' task_event ' )
2017-05-06 23:42:46 +00:00
2019-04-14 05:12:42 +00:00
# Clean out the temporary block list of old entries.
for _freq in temporary_block_list . keys ( ) :
if temporary_block_list [ _freq ] < ( time . time ( ) - config [ ' temporary_block_time ' ] * 60 ) :
temporary_block_list . pop ( _freq )
logging . info ( " Task Manager - Removed %.3f MHz from temporary block list. " % ( _freq / 1e6 ) )
2019-05-27 12:02:36 +00:00
# Check if there is a scanner thread still running.
# If not, and if there is a SDR free, start one up again.
# Also check for a global scan inhibit flag.
if ( ' SCAN ' not in autorx . task_list ) and ( not autorx . scan_inhibit ) and ( allocate_sdr ( check_only = True ) is not None ) :
2018-05-26 09:18:53 +00:00
# We have a SDR free, and we are not running a scan thread. Start one.
start_scanner ( )
2017-12-20 04:23:29 +00:00
2017-05-06 23:42:46 +00:00
2018-06-01 11:32:24 +00:00
2018-05-26 09:18:53 +00:00
def stop_all ( ) :
""" Shut-down all decoders, scanners, and exporters. """
2018-06-18 11:17:38 +00:00
global exporter_objects
2018-05-26 09:18:53 +00:00
logging . info ( " Starting shutdown of all threads. " )
2018-06-18 11:17:38 +00:00
for _task in autorx . task_list . keys ( ) :
2018-05-26 09:18:53 +00:00
try :
2018-06-18 11:17:38 +00:00
autorx . task_list [ _task ] [ ' task ' ] . stop ( )
2018-05-26 09:18:53 +00:00
except Exception as e :
logging . error ( " Error stopping task - %s " % str ( e ) )
2017-05-06 23:42:46 +00:00
2018-05-26 09:18:53 +00:00
for _exporter in exporter_objects :
try :
_exporter . close ( )
except Exception as e :
logging . error ( " Error stopping exporter - %s " % str ( e ) )
2017-05-06 23:42:46 +00:00
2019-06-22 08:15:46 +00:00
if gpsd_adaptor != None :
gpsd_adaptor . close ( )
2017-05-06 23:42:46 +00:00
2018-06-01 11:32:24 +00:00
2018-05-26 09:18:53 +00:00
def telemetry_filter ( telemetry ) :
2019-01-27 18:17:05 +00:00
""" Filter incoming radiosonde telemetry based on various factors,
2018-05-26 09:18:53 +00:00
- Invalid Position
- Invalid Altitude
- Abnormal range from receiver .
- Invalid serial number .
2017-12-22 12:45:40 +00:00
2018-06-01 11:32:24 +00:00
This function is defined within this script to avoid passing around large amounts of configuration data .
2017-12-22 12:45:40 +00:00
"""
global config
2018-05-09 12:00:56 +00:00
# First Check: zero lat/lon
2018-05-26 09:18:53 +00:00
if ( telemetry [ ' lat ' ] == 0.0 ) and ( telemetry [ ' lon ' ] == 0.0 ) :
logging . warning ( " Zero Lat/Lon. Sonde %s does not have GPS lock. " % telemetry [ ' id ' ] )
2018-05-09 12:00:56 +00:00
return False
# Second check: Altitude cap.
2018-05-26 09:18:53 +00:00
if telemetry [ ' alt ' ] > config [ ' max_altitude ' ] :
_altitude_breach = telemetry [ ' alt ' ] - config [ ' max_altitude ' ]
logging . warning ( " Sonde %s position breached altitude cap by %d m. " % ( telemetry [ ' id ' ] , _altitude_breach ) )
2017-12-22 12:45:40 +00:00
return False
2019-03-04 10:31:47 +00:00
# Third check: Number of satellites visible.
if ' sats ' in telemetry :
if telemetry [ ' sats ' ] < 4 :
logging . warning ( " Sonde %s can only see %d SVs - discarding position as bad. " % ( telemetry [ ' id ' ] , telemetry [ ' sats ' ] ) )
return False
# Fourth check - is the payload more than x km from our listening station.
2017-12-22 12:45:40 +00:00
# Only run this check if a station location has been provided.
if ( config [ ' station_lat ' ] != 0.0 ) and ( config [ ' station_lon ' ] != 0.0 ) :
# Calculate the distance from the station to the payload.
_listener = ( config [ ' station_lat ' ] , config [ ' station_lon ' ] , config [ ' station_alt ' ] )
2018-05-26 09:18:53 +00:00
_payload = ( telemetry [ ' lat ' ] , telemetry [ ' lon ' ] , telemetry [ ' alt ' ] )
2017-12-22 12:45:40 +00:00
# Calculate using positon_info function from rotator_utils.py
_info = position_info ( _listener , _payload )
if _info [ ' straight_distance ' ] > config [ ' max_radius_km ' ] * 1000 :
_radius_breach = _info [ ' straight_distance ' ] / 1000.0 - config [ ' max_radius_km ' ]
2018-05-26 09:18:53 +00:00
logging . warning ( " Sonde %s position breached radius cap by %.1f km. " % ( telemetry [ ' id ' ] , _radius_breach ) )
2017-12-22 12:45:40 +00:00
return False
2018-03-18 07:44:04 +00:00
# Payload Serial Number Checks
2018-05-26 09:18:53 +00:00
_serial = telemetry [ ' id ' ]
2018-03-18 07:44:04 +00:00
# Run a Regex to match known Vaisala RS92/RS41 serial numbers (YWWDxxxx)
# RS92: https://www.vaisala.com/sites/default/files/documents/Vaisala%20Radiosonde%20RS92%20Serial%20Number.pdf
# RS41: https://www.vaisala.com/sites/default/files/documents/Vaisala%20Radiosonde%20RS41%20Serial%20Number.pdf
# This will need to be re-evaluated if we're still using this code in 2021!
2018-09-28 09:35:42 +00:00
# UPDATE: Had some confirmation that Vaisala will continue to use the alphanumeric numbering up until
# ~2025-2030, so have expanded the regex to match (and also support some older RS92s)
vaisala_callsign_valid = re . match ( r ' [E-Z][0-5][ \ d][1-7] \ d {4} ' , _serial )
2018-03-18 07:44:04 +00:00
2019-07-31 12:59:35 +00:00
# Regex to check DFM06/09/15/17 callsigns. Also catches the 'unknown' types (xC, xD, etc)
dfm_callsign_valid = re . match ( r ' DFM[01x][5679CD]- \ d {6} ' , _serial )
2018-05-09 12:40:06 +00:00
2019-09-08 08:46:18 +00:00
# Check Meisei sonde callsigns for validity.
# meisei_ims returns a callsign of IMS100-0 until it receives the serial number, so we filter based on the 0 being present or not.
meisei_callsign_valid = int ( _serial . split ( ' - ' ) [ 1 ] ) != 0
2019-06-15 06:58:21 +00:00
# If Vaisala or DFMs, check the callsigns are valid. If M10, iMet or LMS6, just pass it through.
2019-09-08 08:46:18 +00:00
if vaisala_callsign_valid or dfm_callsign_valid or meisei_callsign_valid or ( ' M10 ' in telemetry [ ' type ' ] ) or ( ' MK2LMS ' in telemetry [ ' type ' ] ) or ( ' LMS6 ' in telemetry [ ' type ' ] ) or ( ' iMet ' in telemetry [ ' type ' ] ) :
2018-03-18 07:44:04 +00:00
return True
else :
2019-02-02 04:53:26 +00:00
_id_msg = " Payload ID %s is invalid. " % telemetry [ ' id ' ]
# Add in a note about DFM sondes and their oddness...
if ' DFM ' in telemetry [ ' id ' ] :
_id_msg + = " Note: DFM sondes may take a while to get an ID. "
logging . warning ( _id_msg )
2018-03-18 07:44:04 +00:00
return False
2019-06-22 08:15:46 +00:00
def station_position_update ( position ) :
''' Handle a callback from GPSDAdaptor object, and update each exporter object. '''
global exporter_objects
# Quick sanity check of the incoming data
if ' valid ' not in position :
return
for _exporter in exporter_objects :
try :
_exporter . update_station_position ( position [ ' latitude ' ] , position [ ' longitude ' ] , position [ ' altitude ' ] )
except AttributeError :
# This exporter does not require station position data.
pass
except Exception as e :
traceback . print_exc ( )
logging . error ( " Error updating exporter station position. " )
2018-05-26 09:18:53 +00:00
def main ( ) :
""" Main Loop """
2019-06-22 08:15:46 +00:00
global config , exporter_objects , exporter_functions , logging_level , rs92_ephemeris , gpsd_adaptor
2018-03-18 07:44:04 +00:00
2019-01-27 18:17:05 +00:00
# Command line arguments.
2018-05-26 09:18:53 +00:00
parser = argparse . ArgumentParser ( )
2018-06-01 11:32:24 +00:00
parser . add_argument ( " -c " , " --config " , default = " station.cfg " , help = " Receive Station Configuration File. Default: station.cfg " )
2019-01-27 18:17:05 +00:00
parser . add_argument ( " -l " , " --log " , default = " ./log/ " , help = " Receive Station Log Path. Default: ./log/ " )
2018-06-01 11:32:24 +00:00
parser . add_argument ( " -f " , " --frequency " , type = float , default = 0.0 , help = " Sonde Frequency Override (MHz). This overrides the scan whitelist with the supplied frequency. " )
2019-03-10 02:32:27 +00:00
parser . add_argument ( " -m " , " --type " , type = str , default = None , help = " Immediately start a decoder for a provided sonde type (RS41, RS92, DFM, M10, etc) " )
2018-06-01 11:32:24 +00:00
parser . add_argument ( " -t " , " --timeout " , type = int , default = 0 , help = " Close auto_rx system after N minutes. Use 0 to run continuously. " )
2018-05-26 09:18:53 +00:00
parser . add_argument ( " -v " , " --verbose " , help = " Enable debug output. " , action = " store_true " )
2018-06-01 11:32:24 +00:00
parser . add_argument ( " -e " , " --ephemeris " , type = str , default = " None " , help = " Use a manually obtained ephemeris file when decoding RS92 Sondes. " )
2019-03-05 10:38:28 +00:00
parser . add_argument ( " --systemlog " , action = ' store_true ' , default = False , help = " Write a auto_rx system log-file to ./log/ (default=False) " )
2018-05-26 09:18:53 +00:00
args = parser . parse_args ( )
2017-04-29 02:00:10 +00:00
2018-06-01 11:32:24 +00:00
# Copy out timeout value, and convert to seconds,
_timeout = args . timeout * 60
# Copy out RS92 ephemeris value, if provided.
if args . ephemeris != " None " :
rs92_ephemeris = args . ephemeris
2018-05-26 09:18:53 +00:00
# Set log-level to DEBUG if requested
if args . verbose :
logging_level = logging . DEBUG
2017-04-29 02:00:10 +00:00
2019-01-27 18:17:05 +00:00
# Define the default logging path
logging_path = " ./log/ "
# Validate the user supplied log path
if os . path . isdir ( args . log ) :
logging_path = os . path . abspath ( args . log )
else :
#Using print because logging may not be established yet
print ( " Invalid logging path, using default. Does the folder exist? " )
2018-06-17 12:40:43 +00:00
2018-06-01 11:32:24 +00:00
# Configure logging
2019-01-31 07:22:26 +00:00
_log_suffix = datetime . datetime . utcnow ( ) . strftime ( " % Y % m %d - % H % M % S_system.log " )
_log_path = os . path . join ( logging_path , _log_suffix )
2017-04-29 14:06:47 +00:00
2019-03-05 10:38:28 +00:00
if args . systemlog :
# Only write out a logs to a system log file if we have been asked to.
# Systemd will capture and logrotate our logs anyway, so writing to our own log file is less useful.
logging . basicConfig ( format = ' %(asctime)s %(levelname)s : %(message)s ' , filename = _log_path , level = logging_level )
logging . info ( " Opened new system log file: %s " % _log_path )
# Also add a separate stdout logger.
stdout_format = logging . Formatter ( ' %(asctime)s %(levelname)s : %(message)s ' )
stdout_handler = logging . StreamHandler ( sys . stdout )
stdout_handler . setFormatter ( stdout_format )
logging . getLogger ( ) . addHandler ( stdout_handler )
else :
# Otherwise, we only need the stdout logger, which if we don't specify a filename to logging.basicConfig,
# is the default...
logging . basicConfig ( format = ' %(asctime)s %(levelname)s : %(message)s ' , level = logging_level )
# Add the web interface logging handler.
2018-06-17 12:40:43 +00:00
web_handler = WebHandler ( )
logging . getLogger ( ) . addHandler ( web_handler )
2019-03-05 10:38:28 +00:00
# Set the requests/socketio loggers (and related) to only display critical log messages.
2018-06-17 12:40:43 +00:00
logging . getLogger ( " requests " ) . setLevel ( logging . CRITICAL )
logging . getLogger ( " urllib3 " ) . setLevel ( logging . CRITICAL )
logging . getLogger ( ' werkzeug ' ) . setLevel ( logging . ERROR )
logging . getLogger ( ' socketio ' ) . setLevel ( logging . ERROR )
logging . getLogger ( ' engineio ' ) . setLevel ( logging . ERROR )
2018-12-26 02:18:00 +00:00
logging . getLogger ( ' geventwebsocket ' ) . setLevel ( logging . ERROR )
2018-04-15 10:04:14 +00:00
2017-05-05 12:56:41 +00:00
2018-05-26 09:18:53 +00:00
# Attempt to read in config file
logging . info ( " Reading configuration file... " )
_temp_cfg = read_auto_rx_config ( args . config )
if _temp_cfg is None :
logging . critical ( " Error in configuration file! Exiting... " )
sys . exit ( 1 )
2017-12-20 09:28:24 +00:00
else :
2018-05-26 09:18:53 +00:00
config = _temp_cfg
2018-06-18 11:28:49 +00:00
autorx . sdr_list = config [ ' sdr_settings ' ]
2017-12-20 09:28:24 +00:00
2018-08-03 11:01:56 +00:00
# Check all the RS utilities exist.
if not check_rs_utils ( ) :
sys . exit ( 1 )
2018-06-21 12:59:10 +00:00
# Start up the flask server.
# This needs to occur AFTER logging is setup, else logging breaks horribly for some reason.
2019-03-17 18:35:57 +00:00
start_flask ( host = config [ ' web_host ' ] , port = config [ ' web_port ' ] )
2018-06-21 12:59:10 +00:00
2018-06-01 11:32:24 +00:00
# If we have been supplied a frequency via the command line, override the whitelist settings
# to only include the supplied frequency.
2018-05-26 09:18:53 +00:00
if args . frequency != 0.0 :
config [ ' whitelist ' ] = [ args . frequency ]
2017-05-05 12:56:41 +00:00
2018-05-26 09:18:53 +00:00
# Start our exporter options
# Telemetry Logger
if config [ ' per_sonde_log ' ] :
2019-01-27 18:17:05 +00:00
_logger = TelemetryLogger ( log_directory = logging_path )
2018-05-26 09:18:53 +00:00
exporter_objects . append ( _logger )
exporter_functions . append ( _logger . add )
2017-05-05 12:56:41 +00:00
2018-07-04 08:13:58 +00:00
if config [ ' email_enabled ' ] :
_email_notification = EmailNotification (
smtp_server = config [ ' email_smtp_server ' ] ,
2019-05-04 04:19:46 +00:00
smtp_port = config [ ' email_smtp_port ' ] ,
2019-06-22 08:15:46 +00:00
smtp_authentication = config [ ' email_smtp_authentication ' ] ,
2019-05-04 04:19:46 +00:00
smtp_login = config [ ' email_smtp_login ' ] ,
smtp_password = config [ ' email_smtp_password ' ] ,
2018-07-04 08:13:58 +00:00
mail_from = config [ ' email_from ' ] ,
2019-03-23 10:16:15 +00:00
mail_to = config [ ' email_to ' ] ,
2019-06-22 08:15:46 +00:00
mail_subject = config [ ' email_subject ' ] ,
station_position = ( config [ ' station_lat ' ] , config [ ' station_lon ' ] , config [ ' station_alt ' ] )
2018-07-04 08:13:58 +00:00
)
exporter_objects . append ( _email_notification )
exporter_functions . append ( _email_notification . add )
2018-05-26 09:18:53 +00:00
# Habitat Uploader
if config [ ' habitat_enabled ' ] :
if config [ ' habitat_payload_callsign ' ] == " <id> " :
_habitat_payload_call = None
else :
_habitat_payload_call = config [ ' habitat_payload_callsign ' ]
2017-05-05 12:56:41 +00:00
2018-05-26 09:18:53 +00:00
if config [ ' habitat_upload_listener_position ' ] is False :
2019-06-22 08:15:46 +00:00
_habitat_station_position = None
2018-05-26 09:18:53 +00:00
else :
2019-06-22 08:15:46 +00:00
_habitat_station_position = ( config [ ' station_lat ' ] , config [ ' station_lon ' ] , config [ ' station_alt ' ] )
2019-01-27 18:17:05 +00:00
2018-05-26 09:18:53 +00:00
_habitat = HabitatUploader (
user_callsign = config [ ' habitat_uploader_callsign ' ] ,
2018-06-16 12:34:18 +00:00
user_antenna = config [ ' habitat_uploader_antenna ' ] ,
2019-06-22 08:15:46 +00:00
station_position = _habitat_station_position ,
2018-05-26 09:18:53 +00:00
payload_callsign_override = _habitat_payload_call ,
synchronous_upload_time = config [ ' habitat_upload_rate ' ] ,
callsign_validity_threshold = config [ ' payload_id_valid ' ]
)
2017-04-29 14:06:47 +00:00
2018-05-26 09:18:53 +00:00
exporter_objects . append ( _habitat )
exporter_functions . append ( _habitat . add )
2018-01-06 12:04:47 +00:00
2017-12-17 04:25:20 +00:00
2018-06-01 11:32:24 +00:00
# APRS Uploader
2018-05-26 11:51:55 +00:00
if config [ ' aprs_enabled ' ] :
if config [ ' aprs_object_id ' ] == " <id> " :
_aprs_object = None
else :
_aprs_object = config [ ' aprs_object_id ' ]
_aprs = APRSUploader (
aprs_callsign = config [ ' aprs_user ' ] ,
aprs_passcode = config [ ' aprs_pass ' ] ,
object_name_override = _aprs_object ,
object_comment = config [ ' aprs_custom_comment ' ] ,
2018-12-05 08:21:06 +00:00
position_report = config [ ' aprs_position_report ' ] ,
2018-05-26 11:51:55 +00:00
aprsis_host = config [ ' aprs_server ' ] ,
synchronous_upload_time = config [ ' aprs_upload_rate ' ] ,
2018-12-02 07:07:14 +00:00
callsign_validity_threshold = config [ ' payload_id_valid ' ] ,
station_beacon = config [ ' station_beacon_enabled ' ] ,
station_beacon_rate = config [ ' station_beacon_rate ' ] ,
2019-06-22 08:15:46 +00:00
station_beacon_position = ( config [ ' station_lat ' ] , config [ ' station_lon ' ] , config [ ' station_alt ' ] ) ,
2018-12-02 07:07:14 +00:00
station_beacon_comment = config [ ' station_beacon_comment ' ] ,
station_beacon_icon = config [ ' station_beacon_icon ' ]
2018-05-26 11:51:55 +00:00
)
exporter_objects . append ( _aprs )
exporter_functions . append ( _aprs . add )
2017-07-16 10:11:13 +00:00
2019-01-27 18:17:05 +00:00
# OziExplorer
2018-05-27 10:59:49 +00:00
if config [ ' ozi_enabled ' ] or config [ ' payload_summary_enabled ' ] :
if config [ ' ozi_enabled ' ] :
_ozi_port = config [ ' ozi_port ' ]
else :
_ozi_port = None
if config [ ' payload_summary_enabled ' ] :
_summary_port = config [ ' payload_summary_port ' ]
else :
_summary_port = None
_ozimux = OziUploader (
ozimux_port = _ozi_port ,
payload_summary_port = _summary_port ,
2019-05-18 12:23:46 +00:00
update_rate = config [ ' ozi_update_rate ' ] ,
station = config [ ' habitat_uploader_callsign ' ] )
2018-05-27 10:59:49 +00:00
exporter_objects . append ( _ozimux )
exporter_functions . append ( _ozimux . add )
2017-04-29 02:00:10 +00:00
2018-10-04 11:45:18 +00:00
2019-01-27 18:17:05 +00:00
# Rotator
2018-10-04 11:45:18 +00:00
if config [ ' rotator_enabled ' ] :
_rotator = Rotator (
2019-06-22 08:15:46 +00:00
station_position = ( config [ ' station_lat ' ] , config [ ' station_lon ' ] , config [ ' station_alt ' ] ) ,
2018-10-04 11:45:18 +00:00
rotctld_host = config [ ' rotator_hostname ' ] ,
rotctld_port = config [ ' rotator_port ' ] ,
rotator_update_rate = config [ ' rotator_update_rate ' ] ,
rotator_update_threshold = config [ ' rotation_threshold ' ] ,
rotator_homing_enabled = config [ ' rotator_homing_enabled ' ] ,
rotator_homing_delay = config [ ' rotator_homing_delay ' ] ,
rotator_home_position = [ config [ ' rotator_home_azimuth ' ] , config [ ' rotator_home_elevation ' ] ]
)
2019-01-27 18:17:05 +00:00
2018-10-04 11:45:18 +00:00
exporter_objects . append ( _rotator )
exporter_functions . append ( _rotator . add )
2018-06-23 14:13:51 +00:00
_web_exporter = WebExporter ( max_age = config [ ' web_archive_age ' ] )
2018-06-17 13:16:30 +00:00
exporter_objects . append ( _web_exporter )
exporter_functions . append ( _web_exporter . add )
2019-06-22 08:15:46 +00:00
# GPSD Startup
if config [ ' gpsd_enabled ' ] :
gpsd_adaptor = GPSDAdaptor (
hostname = config [ ' gpsd_host ' ] ,
port = config [ ' gpsd_port ' ] ,
callback = station_position_update )
2018-03-18 07:44:04 +00:00
2019-03-24 06:18:19 +00:00
check_autorx_version ( )
2018-06-01 11:32:24 +00:00
# Note the start time.
_start_time = time . time ( )
2017-04-29 02:00:10 +00:00
2019-03-10 02:32:27 +00:00
# If a sonde type has been provided, insert an entry into the scan results,
# and immediately start a decoder. If decoding fails, then we continue into
# the main scanning loop.
if args . type != None :
2019-05-04 03:12:24 +00:00
autorx . scan_results . put ( [ [ args . frequency * 1e6 , args . type ] ] )
2019-03-10 02:32:27 +00:00
handle_scan_results ( )
2019-01-27 18:17:05 +00:00
# Loop.
2018-05-26 09:18:53 +00:00
while True :
2018-06-01 11:32:24 +00:00
# Check for finished tasks.
2018-05-26 09:18:53 +00:00
clean_task_list ( )
2018-06-01 11:32:24 +00:00
# Handle any new scan results.
2018-05-26 09:18:53 +00:00
handle_scan_results ( )
2018-06-01 11:32:24 +00:00
# Sleep a little bit.
2018-05-26 09:18:53 +00:00
time . sleep ( 2 )
2017-04-29 02:00:10 +00:00
2018-06-01 11:32:24 +00:00
# Allow a timeout after a set time, for users who wish to run auto_rx
# within a cronjob.
if ( _timeout > 0 ) and ( ( time . time ( ) - _start_time ) > _timeout ) :
logging . info ( " Shutdown time reached. Closing. " )
2019-03-17 18:35:57 +00:00
stop_flask ( host = config [ ' web_host ' ] , port = config [ ' web_port ' ] )
2018-06-01 11:32:24 +00:00
stop_all ( )
break
2018-05-07 12:03:31 +00:00
2017-12-20 04:23:29 +00:00
2018-05-26 09:18:53 +00:00
if __name__ == " __main__ " :
2017-12-20 04:23:29 +00:00
2018-05-26 09:18:53 +00:00
try :
main ( )
2017-12-20 04:23:29 +00:00
except KeyboardInterrupt :
2018-06-01 11:32:24 +00:00
# Upon CTRL+C, shutdown all threads and exit.
2019-03-17 18:35:57 +00:00
stop_flask ( host = config [ ' web_host ' ] , port = config [ ' web_port ' ] )
2018-05-26 09:18:53 +00:00
stop_all ( )
except Exception as e :
2018-06-01 11:32:24 +00:00
# Upon exceptions, attempt to shutdown threads and exit.
2018-05-26 09:18:53 +00:00
traceback . print_exc ( )
print ( " Main Loop Error - %s " % str ( e ) )
2019-03-17 18:35:57 +00:00
stop_flask ( host = config [ ' web_host ' ] , port = config [ ' web_port ' ] )
2018-05-26 09:18:53 +00:00
stop_all ( )