Initial works of autorx v2 main loop.

pull/68/head
Mark Jessop 2018-05-25 23:59:16 +09:30
rodzic feba7351f1
commit ccba90388b
7 zmienionych plików z 737 dodań i 95 usunięć

Wyświetl plik

@ -5,4 +5,4 @@
# Copyright (C) 2018 Mark Jessop <vk5qi@rfhead.net>
# Released under GNU GPL v3 or later
#
__version__ = "20180507"
__version__ = "20180525"

Wyświetl plik

@ -10,142 +10,203 @@ import ConfigParser
import logging
import traceback
import json
from .utils import rtlsdr_test
def read_auto_rx_config(filename):
""" Read an Auto-RX Station Configuration File
""" Read an Auto-RX v2 Station Configuration File.
This function will attempt to parse a configuration file.
It will also confirm the accessibility of any SDRs specified in the config file.
Args:
filename (str): Filename of the configuration file to read.
Returns:
auto_rx_config (dict): The configuration dictionary.
sdr_config (dict): A dictionary with SDR parameters.
"""
# Configuration Defaults:
auto_rx_config = {
# Log Settings
'per_sonde_log' : True,
'sdr_fm_path': 'rtl_fm',
'sdr_power_path': 'rtl_power',
'sdr_ppm' : 0,
'sdr_gain' : -1,
'sdr_bias' : False,
'search_attempts': 5,
'search_delay' : 10,
# SDR Settings
'sdr_fm': 'rtl_fm',
'sdr_power': 'rtl_power',
'sdr_quantity': 1,
# Search Parameters
'min_freq' : 400.4,
'max_freq' : 404.0,
'search_step' : 800,
'min_snr' : 10,
'min_distance' : 1000,
'dwell_time' : 10,
'quantization' : 10000,
'rx_timeout' : 120,
'whitelist' : [],
'blacklist' : [],
'greylist' : [],
# Location Settings
'station_lat' : 0.0,
'station_lon' : 0.0,
'station_alt' : 0.0,
'upload_rate' : 30,
'synchronous_upload' : False,
'enable_aprs' : False,
'enable_habitat': False,
# Position Filter Settings
'max_altitude' : 50000,
'max_radius_km' : 1000,
# Habitat Settings
'habitat_enabled': False,
'habitat_upload_rate': 30,
'habitat_uploader_callsign': 'SONDE_AUTO_RX',
'habitat_upload_listener_position': False,
'habitat_payload_callsign': '<id>',
'habitat_payload_description': 'Meteorological Radiosonde',
# APRS Settings
'aprs_enabled' : False,
'aprs_upload_rate': 30,
'aprs_user' : 'N0CALL',
'aprs_pass' : '00000',
'aprs_server' : 'rotate.aprs2.net',
'aprs_object_id': '<id>',
'aprs_custom_comment': 'Radiosonde Auto-RX <freq>',
'payload_callsign': '<id>',
'payload_description': 'Meteorological Radiosonde',
'uploader_callsign': 'SONDE_AUTO_RX',
'upload_listener_position': False,
# Advanced Parameters
'search_step' : 800,
'snr_threshold' : 10,
'min_distance' : 1000,
'dwell_time' : 10,
'max_peaks' : 10,
'quantization' : 10000,
'synchronous_upload' : False,
'scan_dwell_time' : 20,
'detect_dwell_time' : 5,
'payload_id_valid' : 5,
# Rotator Settings
'enable_rotator': False,
'rotator_hostname': '127.0.0.1',
'rotator_port' : 4533,
'rotator_homing_enabled': False,
'rotator_home_azimuth': 0,
'rotator_home_elevation': 0,
# OziExplorer Settings
'ozi_enabled' : False,
'ozi_update_rate': 5,
'ozi_hostname' : '127.0.0.1',
'ozi_port' : 55681,
'mqtt_enabled' : False,
'mqtt_hostname' : '127.0.0.1',
'mqtt_port' : 1883,
'payload_summary_enabled': False,
'payload_summary_port' : 55672,
'whitelist' : [],
'blacklist' : [],
'greylist' : [],
'max_altitude' : 50000,
'max_radius_km' : 1000,
'payload_id_valid' : 5 # TODO: Add this to config file in next bulk update.
'payload_summary_port' : 55672
}
sdr_settings = {}#'0':{'ppm':0, 'gain':-1, 'bias': False}}
try:
config = ConfigParser.RawConfigParser(auto_rx_config)
config.read(filename)
# Log Settings
auto_rx_config['per_sonde_log'] = config.getboolean('logging', 'per_sonde_log')
auto_rx_config['sdr_fm_path'] = config.get('sdr','sdr_fm_path')
auto_rx_config['sdr_power_path'] = config.get('sdr','sdr_power_path')
auto_rx_config['sdr_ppm'] = int(config.getfloat('sdr', 'sdr_ppm'))
auto_rx_config['sdr_gain'] = config.getfloat('sdr', 'sdr_gain')
auto_rx_config['sdr_bias'] = config.getboolean('sdr', 'sdr_bias')
auto_rx_config['search_attempts'] = config.getint('search_params', 'search_attempts')
auto_rx_config['search_delay'] = config.getint('search_params', 'search_delay')
# SDR Settings
auto_rx_config['sdr_fm'] = config.get('sdr', 'sdr_fm_path')
auto_rx_config['sdr_power'] = config.get('sdr', 'sdr_power_path')
auto_rx_config['sdr_quantity'] = config.getint('sdr', 'sdr_quantity')
# Search Parameters
auto_rx_config['min_freq'] = config.getfloat('search_params', 'min_freq')
auto_rx_config['max_freq'] = config.getfloat('search_params', 'max_freq')
auto_rx_config['search_step'] = config.getfloat('search_params', 'search_step')
auto_rx_config['min_snr'] = config.getfloat('search_params', 'min_snr')
auto_rx_config['min_distance'] = config.getfloat('search_params', 'min_distance')
auto_rx_config['dwell_time'] = config.getint('search_params', 'dwell_time')
auto_rx_config['quantization'] = config.getint('search_params', 'quantization')
auto_rx_config['rx_timeout'] = config.getint('search_params', 'rx_timeout')
auto_rx_config['whitelist'] = json.loads(config.get('search_params', 'whitelist'))
auto_rx_config['blacklist'] = json.loads(config.get('search_params', 'blacklist'))
auto_rx_config['greylist'] = json.loads(config.get('search_params', 'greylist'))
# Location Settings
auto_rx_config['station_lat'] = config.getfloat('location', 'station_lat')
auto_rx_config['station_lon'] = config.getfloat('location', 'station_lon')
auto_rx_config['station_alt'] = config.getfloat('location', 'station_alt')
auto_rx_config['upload_rate'] = config.getint('upload', 'upload_rate')
auto_rx_config['synchronous_upload'] = config.getboolean('upload','synchronous_upload')
auto_rx_config['enable_aprs'] = config.getboolean('upload', 'enable_aprs')
auto_rx_config['enable_habitat'] = config.getboolean('upload', 'enable_habitat')
# Position Filtering
auto_rx_config['max_altitude'] = config.getint('filtering', 'max_altitude')
auto_rx_config['max_radius_km'] = config.getint('filtering', 'max_radius_km')
# Habitat Settings
auto_rx_config['habitat_enabled'] = config.getboolean('habitat', 'habitat_enabled')
auto_rx_config['habitat_upload_rate'] = config.getint('habitat', 'upload_rate')
auto_rx_config['habitat_payload_callsign'] = config.get('habitat', 'payload_callsign')
auto_rx_config['habitat_payload_description'] = config.get('habitat', 'payload_description')
auto_rx_config['habitat_uploader_callsign'] = config.get('habitat', 'uploader_callsign')
auto_rx_config['habitat_upload_listener_position'] = config.getboolean('habitat','upload_listener_position')
# APRS Settings
auto_rx_config['aprs_enabled'] = config.getboolean('aprs', 'aprs_enabled')
auto_rx_config['aprs_upload_rate'] = config.getint('aprs', 'upload_rate')
auto_rx_config['aprs_user'] = config.get('aprs', 'aprs_user')
auto_rx_config['aprs_pass'] = config.get('aprs', 'aprs_pass')
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['payload_callsign'] = config.get('habitat', 'payload_callsign')
auto_rx_config['payload_description'] = config.get('habitat', 'payload_description')
auto_rx_config['uploader_callsign'] = config.get('habitat', 'uploader_callsign')
auto_rx_config['upload_listener_position'] = config.getboolean('habitat','upload_listener_position')
auto_rx_config['enable_rotator'] = config.getboolean('rotator','enable_rotator')
auto_rx_config['rotator_hostname'] = config.get('rotator', 'rotator_hostname')
auto_rx_config['rotator_port'] = config.getint('rotator', 'rotator_port')
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')
# OziPlotter Settings
auto_rx_config['ozi_enabled'] = config.getboolean('oziplotter', 'ozi_enabled')
auto_rx_config['ozi_update_rate'] = config.getint('oziplotter', 'ozi_update_rate')
auto_rx_config['ozi_port'] = config.getint('oziplotter', 'ozi_port')
auto_rx_config['payload_summary_enabled'] = config.getboolean('oziplotter', 'payload_summary_enabled')
auto_rx_config['payload_summary_port'] = config.getint('oziplotter', 'payload_summary_port')
# Read in lists using a JSON parser.
auto_rx_config['whitelist'] = json.loads(config.get('search_params', 'whitelist'))
auto_rx_config['blacklist'] = json.loads(config.get('search_params', 'blacklist'))
auto_rx_config['greylist'] = json.loads(config.get('search_params', 'greylist'))
# Advanced Settings
auto_rx_config['search_step'] = config.getfloat('advanced', 'search_step')
auto_rx_config['snr_threshold'] = config.getfloat('advanced', 'snr_threshold')
auto_rx_config['min_distance'] = config.getfloat('advanced', 'min_distance')
auto_rx_config['dwell_time'] = config.getint('advanced', 'dwell_time')
auto_rx_config['quantization'] = config.getint('advanced', 'quantization')
auto_rx_config['max_peaks'] = config.getint('advanced', 'max_peaks')
auto_rx_config['scan_dwell_time'] = config.getint('advanced', 'scan_dwell_time')
auto_rx_config['detect_dwell_time'] = config.getint('advanced', 'detect_dwell_time')
auto_rx_config['payload_id_valid'] = config.getint('advanced', 'payload_id_valid')
auto_rx_config['synchronous_upload'] = config.getboolean('advanced', 'synchronous_upload')
# Position Filtering
auto_rx_config['max_altitude'] = config.getint('filtering', 'max_altitude')
auto_rx_config['max_radius_km'] = config.getint('filtering', 'max_radius_km')
# Rotator Settings (TBC)
auto_rx_config['rotator_enabled'] = config.getboolean('rotator','rotator_enabled')
auto_rx_config['rotator_hostname'] = config.get('rotator', 'rotator_hostname')
auto_rx_config['rotator_port'] = config.getint('rotator', 'rotator_port')
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')
# MQTT settings
auto_rx_config['mqtt_enabled'] = config.getboolean('mqtt', 'mqtt_enabled')
auto_rx_config['mqtt_hostname'] = config.get('mqtt', 'mqtt_hostname')
auto_rx_config['mqtt_port'] = config.getint('mqtt', 'mqtt_port')
# Now we attempt to read in the individual SDR parameters.
auto_rx_config['sdr_settings'] = {}
for _n in range(1,auto_rx_config['sdr_quantity']+1):
_section = "sdr_%d" % _n
try:
_device_idx = config.get(_section,'device_idx')
_ppm = config.getint(_section, 'ppm')
_gain = config.getfloat(_section, 'gain')
_bias = config.getboolean(_section, 'bias')
if (auto_rx_config['sdr_quantity'] > 1) and (_device_idx == '0'):
logging.error("Config - SDR Device ID of 0 used with a multi-SDR configuration. Go read the warning in the config file!")
return None
# See if the SDR exists.
_sdr_valid = rtlsdr_test(_device_idx)
if _sdr_valid:
auto_rx_config['sdr_settings'][_device_idx] = {'ppm':_ppm, 'gain':_gain, 'bias':_bias, 'in_use': False, 'task': None}
logging.info('Config - Tested SDR #%s OK' % _device_idx)
else:
logging.warning("Config - SDR #%s invalid." % _device_idx)
except Exception as e:
logging.error("Config - Error parsing SDR %d config - %s" % (_n,str(e)))
continue
if len(auto_rx_config['sdr_settings'].keys()) == 0:
# We have no SDRs to use!!
logging.error("Config - No working SDRs! Cannot run...")
return None
else:
return auto_rx_config
return auto_rx_config
except:
traceback.print_exc()
logging.error("Could not parse config file, using defaults.")
return auto_rx_config
logging.error("Could not parse config file.")
return None
if __name__ == '__main__':
''' Quick test script to attempt to read in a config file. '''
import sys
print(read_auto_rx_config(sys.argv[1]))
config = read_auto_rx_config(sys.argv[1])
print(config)

Wyświetl plik

@ -63,7 +63,7 @@ class SondeDecoder(object):
def __init__(self,
sonde_type="None",
sonde_freq=400000000,
sonde_freq=400000000.0,
rs_path = "./",
sdr_fm = "rtl_fm",
device_idx = 0,
@ -97,6 +97,9 @@ class SondeDecoder(object):
rs92_ephemeris (str): OPTIONAL - A fixed ephemeris file to use if decoding a RS92. If not supplied, one will be downloaded.
"""
# Thread running flag
self.decoder_running = True
# Local copy of init arguments
self.sonde_type = sonde_type
self.sonde_freq = sonde_freq
@ -112,15 +115,14 @@ class SondeDecoder(object):
self.timeout = timeout
self.rs92_ephemeris = rs92_ephemeris
# Thread running flag
self.decoder_running = False
# This will become out decoder thread.
# This will become our decoder thread.
self.decoder = None
# Check if the sonde type is valid.
if self.sonde_type not in self.VALID_SONDE_TYPES:
self.log_error("Unsupported sonde type: %s" % self.sonde_type)
raise ValueError("Unsupported sonde type: %s." % self.sonde_type)
self.decoder_running = False
return
# Test if the supplied RTLSDR is working.
_rtlsdr_ok = rtlsdr_test(device_idx)
@ -128,8 +130,8 @@ class SondeDecoder(object):
# TODO: How should this error be handled?
if not _rtlsdr_ok:
self.log_error("RTLSDR #%s non-functional - exiting." % device_idx)
self.decoder_running = False
return
#raise IOError("Could not open RTLSDR #%d" % device_idx)
# We can accept a few different types in the exporter argument..
# Nothing...
@ -159,6 +161,7 @@ class SondeDecoder(object):
if self.decoder_command is None:
self.log_error("Could not generate decoder command. Not starting decoder.")
self.decoder_running = False
else:
# Start up the decoder thread.
self.decode_process = None

Wyświetl plik

@ -11,6 +11,7 @@ import os
import platform
import subprocess
import time
import traceback
from threading import Thread
from types import FunctionType, MethodType
from .utils import detect_peaks, rtlsdr_test, rtlsdr_reset
@ -187,9 +188,9 @@ def detect_sonde(frequency, rs_path="./", dwell_time=10, sdr_fm='rtl_fm', device
else:
gain_param = ''
rx_test_command = "timeout %ds %s %s-p %d -d %d %s-M fm -F9 -s 15k -f %d 2>/dev/null |" % (dwell_time, sdr_fm, bias_option, int(ppm), str(device_idx), gain_param, frequency)
rx_test_command = "timeout %ds %s %s-p %d -d %s %s-M fm -F9 -s 15k -f %d 2>/dev/null |" % (dwell_time, sdr_fm, bias_option, int(ppm), str(device_idx), gain_param, frequency)
rx_test_command += "sox -t raw -r 15k -e s -b 16 -c 1 - -r 48000 -t wav - highpass 20 2>/dev/null |"
rx_test_command += os.path.join(rs_path,"rs_detect") + " -z -t 8 2>/dev/null"
rx_test_command += os.path.join(rs_path,"rs_detect") + " -z -t 8 2>/dev/null >/dev/null"
logging.info("Scanner - Attempting sonde detection on %.3f MHz" % (frequency/1e6))
logging.debug("Scanner - Running command: %s" % rx_test_command)
@ -254,6 +255,7 @@ class SondeScanner(object):
def __init__(self,
callback = None,
auto_start = True,
min_freq = 400.0,
max_freq = 403.0,
search_step = 800.0,
@ -279,7 +281,7 @@ class SondeScanner(object):
Args:
callback (function): A function to pass results from the sonde scanner to (when a sonde is found).
auto_start (bool): Start up the scanner automatically.
min_freq (float): Minimum search frequency, in MHz.
max_freq (float): Maximum search frequency, in MHz.
search_step (float): Search step, in *Hz*. Defaults to 800 Hz, which seems to work well.
@ -303,6 +305,9 @@ class SondeScanner(object):
bias (bool): If True, enable the bias tee on the SDR.
"""
# Thread flag. This is set to True when a scan is running.
self.sonde_scanner_running = True
# Copy parameters
self.min_freq = min_freq
@ -330,9 +335,6 @@ class SondeScanner(object):
# Error counter.
self.error_retries = 0
# Thread flag. This is set to True when a scan is running.
self.sonde_scanner_running = False
# This will become our scanner thread.
self.sonde_scan_thread = None
@ -341,14 +343,16 @@ class SondeScanner(object):
# TODO: How should this error be handled?
if not _rtlsdr_ok:
self.log_error("RTLSDR #%d non-functional - exiting." % device_idx)
raise IOError("Could not open RTLSDR #%d" % device_idx)
self.log_error("RTLSDR #%s non-functional - exiting." % device_idx)
self.sonde_scanner_running = False
return
if auto_start:
self.start()
def start(self):
# Start the scan loop (if not already running)
if self.sonde_scanner_running == False:
if self.sonde_scan_thread is None:
self.sonde_scanner_running = True
self.sonde_scan_thread = Thread(target=self.scan_loop)
self.sonde_scan_thread.start()
@ -379,6 +383,7 @@ class SondeScanner(object):
time.sleep(10)
continue
except Exception as e:
traceback.print_exc()
self.log_error("Caught other error: %s" % str(e))
time.sleep(10)
else:
@ -426,7 +431,7 @@ class SondeScanner(object):
run_rtl_power(self.min_freq*1e6,
self.max_freq*1e6,
self.search_step,
filename="log_power_%d.csv" % self.device_idx,
filename="log_power_%s.csv" % self.device_idx,
dwell=self.scan_dwell_time,
sdr_power=self.sdr_power,
device_idx=self.device_idx,
@ -440,7 +445,7 @@ class SondeScanner(object):
# Read in result.
# This step will throw an IOError if the file does not exist.
(freq, power, step) = read_rtl_power("log_power_%d.csv" % self.device_idx)
(freq, power, step) = read_rtl_power("log_power_%s.csv" % self.device_idx)
# Sanity check results.
if step == 0 or len(freq)==0 or len(power)==0:
# Otherwise, if a file has been written but contains no data, it can indicate
@ -570,7 +575,7 @@ class SondeScanner(object):
Args:
line (str): Message to be logged.
"""
logging.debug("Scanner - %s" % line)
logging.debug("Scanner #%s - %s" % (self.device_idx,line))
def log_info(self, line):
@ -578,7 +583,7 @@ class SondeScanner(object):
Args:
line (str): Message to be logged.
"""
logging.info("Scanner - %s" % line)
logging.info("Scanner #%s - %s" % (self.device_idx,line))
def log_error(self, line):
@ -586,14 +591,14 @@ class SondeScanner(object):
Args:
line (str): Message to be logged.
"""
logging.error("Scanner - %s" % line)
logging.error("Scanner #%s - %s" % (self.device_idx,line))
def log_warning(self, line):
""" Helper function to log a warning message with a descriptive heading.
Args:
line (str): Message to be logged.
"""
logging.warning("Scanner - %s" % line)
logging.warning("Scanner #%s - %s" % (self.device_idx,line))
if __name__ == "__main__":

Wyświetl plik

@ -10,6 +10,7 @@ from __future__ import division, print_function
import os
import subprocess
import threading
import time
import numpy as np
from math import radians, degrees, sin, cos, atan2, sqrt, pi
try:
@ -274,8 +275,10 @@ def rtlsdr_test(device_idx=0, rtl_sdr_path="rtl_sdr"):
FNULL.close()
except subprocess.CalledProcessError:
# This exception means the subprocess has returned an error code of one.
time.sleep(1)
return False
else:
time.sleep(1)
return True

370
auto_rx/autorx2.py 100644
Wyświetl plik

@ -0,0 +1,370 @@
#!/usr/bin/env python
#
# Radiosonde Auto RX Service - V2.0
#
# Copyright (C) 2018 Mark Jessop <vk5qi@rfhead.net>
# Released under GNU GPL v3 or later
#
# Refer github page for instructions on setup and usage.
# https://github.com/projecthorus/radiosonde_auto_rx/
#
import argparse
import datetime
import logging
import sys
import time
import traceback
from autorx.scan import SondeScanner
from autorx.decode import SondeDecoder
from autorx.logger import TelemetryLogger
from autorx.habitat import HabitatUploader
from autorx.utils import rtlsdr_test
from autorx.config import read_auto_rx_config
try:
# Python 2
from Queue import Queue
except ImportError:
# Python 3
from queue import Queue
# Logging level
# INFO = Basic status messages
# DEBUG = Adds detailed information on submodule operations.
logging_level = logging.DEBUG
#
# Global Variables
#
RS_PATH = "./"
# Optional override for RS92 ephemeris data.
rs92_ephemeris = None
# Global configuration dictionary
config = None
# 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.
# RTLSDR Usage Register - This dictionary holds information about each SDR and its currently running Decoder / Scanner
# Key = SDR device index / ID
# 'device_idx': {
# 'in_use' (bool) : True if the SDR is currently in-use by a decoder or scanner.
# 'task' (class) : If this SDR is in use, a reference to the task.
# 'bias' (bool) : True if the bias-tee should be enabled on this SDR, False otherwise.
# 'ppm' (int) : The PPM offset for this SDR.
# 'gain' (float) : The gain setting to use with this SDR. A setting of -1 turns on hardware AGC.
# }
#
#
sdr_list = {}
# Currently running task register.
# Keys will either be 'SCAN' (only one scanner shall be running at a time), or a sonde frequency in MHz.
# Each element contains:
# 'task' : (class) Reference to the currently running task.
# 'device_idx' (str): The allocated SDR.
#
task_list = {}
# Scan Result Queue
# Scan results are processed asynchronously from the main scanner object.
scan_results = Queue()
def allocate_sdr(check_only = False):
""" Allocate an un-used SDR for a task.
Args:
check_only (bool) : If True, don't set the free SDR as in-use. Used to check if there are any free SDRs.
Returns:
(str): The device index/serial number of the free/allocated SDR, if one is free, else None.
"""
global sdr_list
for _idx in sdr_list.keys():
if sdr_list[_idx]['in_use'] == False:
# 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.
sdr_list[_idx]['in_use'] = True
logging.info("SDR #%s has been allocated." % str(_idx))
return _idx
# Otherwise, no SDRs are free.
return None
def clean_task_list():
""" Routinely run to check the task list to see if any tasks have stopped running. If so, release the associated SDR """
global task_list, sdr_list
for _key in task_list.keys():
# Attempt to get the state of the task
try:
_running = task_list[_key]['task'].running()
_task_sdr = task_list[_key]['device_idx']
except Exception as e:
logging.error("Task Manager - Error getting task %s state - %s" % (str(_key),str(e)))
continue
if _running == False:
# This task has stopped. Release it's associated SDR.
sdr_list[_task_sdr]['in_use'] = False
sdr_list[_task_sdr]['task'] = None
# Pop the task from the task list.
task_list.pop(_key)
# Check if there is a scanner thread still running. If not, and if there is a SDR free, start one up again.
if ('SCAN' not in task_list) and (allocate_sdr(check_only=True) is not None):
# We have a SDR free, and we are not running a scan thread. Start one.
start_scanner()
def start_scanner():
""" Start a scanner thread on the first available SDR """
global task_list, sdr_list, config, scan_results, RS_PATH
if 'SCAN' in task_list:
# 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()
if _device_idx is None:
logging.debug("Task Manager - No SDRs free to run Scanner.")
return
else:
# Create entry in task list.
task_list['SCAN'] = {'device_idx': _device_idx, 'task': None}
# Init Scanner using settings from the global config.
task_list['SCAN']['task'] = SondeScanner(
callback = scan_results.put,
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,
gain = sdr_list[_device_idx]['gain'],
ppm = sdr_list[_device_idx]['ppm'],
bias = sdr_list[_device_idx]['bias']
)
# Add a reference into the sdr_list entry
sdr_list[_device_idx]['task'] = task_list['SCAN']['task']
def stop_scanner():
""" Stop a currently running scan thread, and release the SDR it was using. """
global task_list, sdr_list
if 'SCAN' not in task_list:
# No scanner thread running!
# This means we likely have a SDR free already.
return
else:
logging.info("Halting Scanner to decode detected radiosonde.")
_scan_sdr = task_list['SCAN']['device_idx']
# Stop the scanner.
task_list['SCAN']['task'].stop()
# Relase the SDR.
sdr_list[_scan_sdr]['in_use'] = False
sdr_list[_scan_sdr]['task'] = None
# Remove the scanner task from the task list
task_list.pop('SCAN')
def start_decoder(freq, sonde_type):
""" Attempt to start a decoder thread """
global config, task_list, sdr_list, RS_PATH, exporter_functions, rs92_ephemeris
# Allocate a SDR.
_device_idx = allocate_sdr()
if _device_idx is None:
logging.error("Could not allocate SDR for decoder!")
return
else:
# Add an entry to the task list
task_list[freq] = {'device_idx': _device_idx, 'task': None}
# Set the SDR to in-use
sdr_list[_device_idx]['in_use'] = True
# Initialise a decoder.
task_list[freq]['task'] = SondeDecoder(
sonde_type = sonde_type,
sonde_freq = freq,
rs_path = RS_PATH,
sdr_fm = config['sdr_fm'],
device_idx = _device_idx,
gain = sdr_list[_device_idx]['gain'],
ppm = sdr_list[_device_idx]['ppm'],
bias = sdr_list[_device_idx]['bias'],
exporter = exporter_functions,
timeout = config['rx_timeout'],
telem_filter = telemetry_filter,
rs92_ephemeris = rs92_ephemeris
)
sdr_list[_device_idx]['task'] = task_list[freq]['task']
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.
"""
global scan_results, task_list, sdr_list
if scan_results.qsize() > 0:
_scan_data = scan_results.get()
for _sonde in _scan_data:
_freq = _sonde[0]
_type = _sonde[1]
if _freq in task_list:
# Already decoding this sonde, continue.
continue
else:
if allocate_sdr(check_only=True) is not None :
# There is a SDR free! Start the decoder on that SDR
start_decoder(_freq, _type)
elif (allocate_sdr(check_only=True) is None) and ('SCAN' in task_list):
# 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:
# We have no SDRs free
pass
def stop_all():
""" Shut-down all decoders, scanners, and exporters. """
global task_list, exporter_objects
logging.info("Starting shutdown of all threads.")
for _task in task_list.keys():
try:
task_list[_task]['task'].stop()
except Exception as e:
logging.error("Error stopping task - %s" % str(e))
for _exporter in exporter_objects:
try:
_exporter.close()
except Exception as e:
logging.error("Error stopping exporter - %s" % str(e))
def telemetry_filter(telemetry):
""" Filter incoming radiosonde telemetry based on distance from the receiver """
global config
# TODO
return True
def main():
""" Main Loop """
global config, sdr_list, exporter_objects, exporter_functions
logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', filename=datetime.datetime.utcnow().strftime("log/%Y%m%d-%H%M%S_system.log"), level=logging_level)
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)
# Set the requests logger to only display WARNING messages or higher.
requests_log = logging.getLogger("requests")
requests_log.setLevel(logging.CRITICAL)
urllib3_log = logging.getLogger("urllib3")
urllib3_log.setLevel(logging.CRITICAL)
# Command line arguments.
parser = argparse.ArgumentParser()
parser.add_argument("-c" ,"--config", default="station_new.cfg", help="Receive Station Configuration File")
parser.add_argument("-f", "--frequency", type=float, default=0.0, help="Sonde Frequency (MHz) (bypass scan step, and quit if no sonde found).")
parser.add_argument("-e", "--ephemeris", type=str, default="None", help="Use a manually obtained ephemeris file.")
args = parser.parse_args()
# 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)
else:
config = _temp_cfg
sdr_list = config['sdr_settings']
# If we have been supplied a frequency via the command line, override the whitelist settings.
if args.frequency != 0.0:
config['whitelist'] = [args.frequency]
# Start our exporter options
if config['per_sonde_log']:
_logger = TelemetryLogger(log_directory="./testlog/")
exporter_objects.append(_logger)
exporter_functions.append(_logger.add)
# Habitat
# APRS
# OziExplorer
while True:
clean_task_list()
handle_scan_results()
time.sleep(5)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
stop_all()
except Exception as e:
traceback.print_exc()
print("Main Loop Error - %s" % str(e))
stop_all()

Wyświetl plik

@ -0,0 +1,200 @@
#
# Radiosonde Auto RX Station Configuration File
#
# Copy this file to station.cfg and modify as required.
#
# Logging Settings
[logging]
# If enabled, a log file will be written to ./log/ for each detected radiosonde.
per_sonde_log = True
# SDR Receiver Settings
[sdr]
# Paths to the rtl_fm and rtl_power utilities. If these are on your system path, then you don't need to change this.
sdr_fm_path = rtl_fm
sdr_power_path = rtl_power
# Number of RTLSDRs to use.
# If more than one RTLSDR is in use, multiple [sdr_X] sections must be populated below
sdr_quantity = 2
# Individual SDR Settings.
[sdr_1]
# Device Index / Serial
# If using a single RTLSDR, set this value to 0
# If using multiple SDRs, you MUST allocate each SDR a unique serial number using rtl_eeprom
# i.e. to set the serial number of a (single) connected RTLSDR: rtl_eeprom -s 00000002
# Then set the device_idx below to 00000002, and repeat for the other [sdr_n] sections below
device_idx = 00000001
# Frequency Correction (ppm offset)
# Refer here for a method of determining this correction: https://gist.github.com/darksidelemm/b517e6a9b821c50c170f1b9b7d65b824
ppm = 0
# SDR Gain Setting
# Gain settings can generally range between 0dB and 40dB depending on the tuner in use.
# Run rtl_test to confirm what gain settings are available, or use a value of -1 to use automatic gain control.
# Note that this is an overall gain value, not an individual mixer/tuner gain. This is a limitation of the rtl_power/rtl_fm utils.
gain = -1
# Bias Tee - Enable the bias tee in the RTLSDR v3 Dongles.
bias = False
[sdr_2]
# As above, for the next SDR. Note the warning about serial numbers.
device_idx = 00000002
ppm = 0
gain = -1
bias = False
# Add more SDR definitions here.
# Radiosonde Search Parameters
[search_params]
# Minimum and maximum search frequencies, in MHz.
# Australia: Use 400.05 - 403 MHz
# Europe: Use 400.05 - 406 MHz
min_freq = 400.05
max_freq = 403.0
# Have the decoder timeout after X seconds of no valid data.
rx_timeout = 20
# Frequency Lists - These must be provided as JSON-compatible lists of floats (in MHz), i.e. [400.50, 401.520, 403.200]
# White-List - Add values to this list to *only* scan on these frequencies.
# This is for when you only want to monitor a small set of launch frequencies.
whitelist = []
# Black-List - Any values added to this list will be removed from the list of detected peaks.
# This is used to remove known spurs or other interferers from the scan list, potentially speeding up detection of a sonde.
blacklist = []
# Grey-List - Any values in this list will be added to the start every scan run.
# This is useful when you know the regular frequency of a local sonde, but still want to allow detections on other frequencies.
greylist = []
# Settings for uploading to the Habitat HAB tracking database ( https://tracker.habhub.org/ )
# Note that the habitat upload will use a fixed string format of:
# `$$<payload_callsign>,<sequence number>,<time>,<lat>,<lon>,<alt>,<speed>,<temp>,<humidity>,<comment>*<CRC16>`
# Where callsign is set below. Temp values are only supported on the RS41 at this time.
# If you use a custom payload callsign, you will need to create an appropriate payload document for it to appear on the map
#
[habitat]
habitat_enabled = False
# Uploader callsign, as shown above. PLEASE CHANGE THIS TO SOMETHING UNIQUE.
uploader_callsign = SONDE_AUTO_RX
# Upload listener position to Habitat? (So you show up on the map)
upload_listener_position = True
# Upload Rate - Upload a packet every X seconds.
upload_rate = 30
# Payload callsign - if set to <id> will use the serial number of the sonde and create a payload document automatically
payload_callsign = <id>
payload_description = Meteorological Radiosonde
# Station Location (optional). Used by the Habitat Uploader, and by Rotator Control
[location]
station_lat = 0.0
station_lon = 0.0
station_alt = 0.0
# Settings for uploading to APRS-IS
[aprs]
aprs_enabled = False
# APRS-IS Login Information
aprs_user = N0CALL
aprs_pass = 00000
# Upload Rate - Upload a packet every X seconds.
upload_rate = 30
# APRS-IS server to upload to.
aprs_server = rotate.aprs2.net
# Object name to be used when uploading to APRS-IS (Max 9 chars)
# Should be either a callsign with a -11 or -12 suffix (i.e. N0CALL-12),
# or <id>, which will be replaced with the radiosondes serial number
aprs_object_id = <id>
# The APRS-IS beacon comment. The following fields can be included:
# <freq> - Sonde Frequency, i.e. 401.520 MHz
# <type> - Sonde Type (RS94/RS41)
# <id> - Sonde Serial Number (i.e. M1234567)
# <vel_v> - Sonde Vertical Velocity (i.e. -5.1m/s)
# <temp> - Sonde reported temperature. If no temp data available, this will report -273 degC. Only works for RS41s.
aprs_custom_comment = Radiosonde Auto-RX Testing
# Settings for pushing data into OziPlotter
# Oziplotter receives data via a basic CSV format, via UDP.
[oziplotter]
ozi_enabled = False
ozi_update_rate = 5
ozi_hostname = 127.0.0.1
ozi_port = 55681
# Payload summary output, which can be used by a few of the Horus Ground Station tools
payload_summary_enabled = False
payload_summary_port = 55672
# Position Filtering Options
# These are used to discard positions which are clearly bad, such as where the payload has jumped halfway around the world,
# or has suddenly ended up in orbit.
# Adjust only if absolutely necessary.
[filtering]
# Discard positions with an altitude greater than 50000 metres.
max_altitude = 50000
# Discard positions more than 1000 km from the observation station location (if set)
max_radius_km = 1000
# Advanced Settings
# These control low-level settings within various modules.
# Playing with them may result in odd behaviour.
[advanced]
# Scanner - Receive bin width (Hz)
search_step = 800
# Scanner - Minimum SNR for a peak to be detected. The lower the number, the more peaks detected.
snr_threshold = 10
# Scanner - Maximum number of peaks to search through during a scan pass.
# Increase this if you have lots of spurious signals, though this will increase scan times.
max_peaks = 10
# Scanner - Minimum distance between peaks (Hz)
min_distance = 1000
# Scanner - Scan Dwell Time - How long to observe the specified spectrum for.
scan_dwell_time = 20
# Scanner - Detection Dwell time - How long to wait for a sonde detection on each peak.
detect_dwell_time = 5
# Quantize search results to x Hz steps. Useful as most sondes are on 10 kHz frequency steps.
quantization = 10000
# Upload when (seconds_since_utc_epoch%upload_rate) == 0. Otherwise just delay upload_rate seconds between uploads.
# Setting this to True with multple uploaders should give a higher chance of all uploaders uploading the same frame,
# however the upload_rate should not be set too low, else there may be a chance of missing upload slots.
synchronous_upload = True
# Only accept a payload ID as valid until it has been seen N times.
# This helps avoid corrupted callsigns getting onto the map.
payload_id_valid = 5
# Rotator Settings
# auto_rx can communicate with an instance of rotctld, on either the local machine or elsewhere on the network.
# The update rate is tied to the upload_rate setting above, though internet upload does not need to be enabled
# for the rotator to be updated.
[rotator]
rotator_enabled = False
# Hostname / Port of the rotctld instance.
rotator_hostname = 127.0.0.1
rotator_port = 4533
# Rotator Homing.
# If enabled, turn to this location when scanning for sondes.
rotator_homing_enabled = False
rotator_home_azimuth = 0
rotator_home_elevation = 0