kopia lustrzana https://github.com/projecthorus/radiosonde_auto_rx
659 wiersze
28 KiB
Python
659 wiersze
28 KiB
Python
#!/usr/bin/env python
|
|
#
|
|
# radiosonde_auto_rx - APRS Exporter
|
|
#
|
|
# Copyright (C) 2018 Mark Jessop <vk5qi@rfhead.net>
|
|
# Released under GNU GPL v3 or later
|
|
#
|
|
import datetime
|
|
import logging
|
|
import random
|
|
import time
|
|
import traceback
|
|
import socket
|
|
from threading import Thread
|
|
from . import __version__ as auto_rx_version
|
|
try:
|
|
# Python 2
|
|
from Queue import Queue
|
|
except ImportError:
|
|
# Python 3
|
|
from queue import Queue
|
|
|
|
|
|
|
|
def telemetry_to_aprs_position(sonde_data, object_name="<id>", aprs_comment="BOM Balloon", position_report=False):
|
|
""" Convert a dictionary containing Sonde telemetry into an APRS packet.
|
|
|
|
Args:
|
|
sonde_data (dict): Sonde telemetry dictionary. (refer autorx.decoder)
|
|
object_name (str): APRS Object Name. If <id>, the sonde's serial number will be used.
|
|
aprs_comment (str): Comment to use in the packet.
|
|
position_report (bool): if True, generate a position report instead of an APRS object packet.
|
|
|
|
"""
|
|
|
|
# Generate the APRS 'callsign' for the sonde.
|
|
if object_name == "<id>":
|
|
# Use the radiosonde ID as the object ID
|
|
if ('RS92' in sonde_data['type']) or ('RS41' in sonde_data['type']):
|
|
# We can use the Vaisala sonde ID directly.
|
|
_object_name = sonde_data["id"].strip()
|
|
elif sonde_data['type'] == 'DFM':
|
|
# The DFM sonde IDs are too long to use directly.
|
|
|
|
# Split out just the serial number part of the ID, and cast it to an int
|
|
_dfm_id = int(sonde_data['id'].split('-')[-1])
|
|
|
|
# Convert to upper-case hex, and take the last 5 nibbles.
|
|
_id_suffix = hex(_dfm_id).upper()[-5:]
|
|
|
|
# Append the hex ID to a shortened sonde-type.
|
|
if "DFM09" in sonde_data['id']:
|
|
_object_name = "DF9" + _id_suffix
|
|
sonde_data['type'] = 'DFM09'
|
|
elif "DFM06" in sonde_data['id']:
|
|
_object_name = "DF6" + _id_suffix
|
|
sonde_data['type'] = 'DFM06'
|
|
elif "DFM15" in sonde_data['id']:
|
|
_object_name = "DF5" + _id_suffix
|
|
sonde_data['type'] = 'DFM15'
|
|
elif "DFM17" in sonde_data['id']:
|
|
_object_name = "DF7" + _id_suffix
|
|
sonde_data['type'] = 'DFM17'
|
|
elif 'DFMx' in sonde_data['id']:
|
|
# Catch-all for the 'unknown' DFM types.
|
|
_object_name = "DF" + sonde_data['id'][4] + _id_suffix
|
|
sonde_data['type'] = sonde_data['id'].split('-')[0]
|
|
else:
|
|
return (None, None)
|
|
|
|
elif 'M10' in sonde_data['type']:
|
|
# Use the generated id same as dxlAPRS
|
|
_object_name = sonde_data['dxlid']
|
|
|
|
elif 'iMet' in sonde_data['type']:
|
|
# Use the last 5 characters of the unique ID we have generated.
|
|
_object_name = "IMET" + sonde_data['id'][-5:]
|
|
|
|
elif ('MK2LMS' in sonde_data['type']) or ('LMS6' in sonde_data['type']):
|
|
# Use the last 5 hex digits of the sonde ID.
|
|
_id_suffix = int(sonde_data['id'].split('-')[1])
|
|
_id_hex = hex(_id_suffix).upper()
|
|
_object_name = "LMS6" + _id_hex[-5:]
|
|
|
|
elif 'MEISEI' in sonde_data['type']:
|
|
# Convert the serial number to an int
|
|
_meisei_id = int(sonde_data['id'].split('-')[-1])
|
|
_id_suffix = hex(_meisei_id).upper().split('0X')[1]
|
|
# Clip to 6 hex digits, in case we end up with more for some reason.
|
|
if len(_id_suffix) > 6:
|
|
_id_suffix = _id_suffix[-6:]
|
|
_object_name = "IMS" + _id_suffix
|
|
|
|
# New Sonde types will be added in here.
|
|
else:
|
|
# Unknown sonde type, don't know how to handle this yet.
|
|
return (None, None)
|
|
else:
|
|
_object_name = object_name
|
|
|
|
# Pad or limit the object name to 9 characters, if it is to long or short.
|
|
if len(_object_name) > 9:
|
|
_object_name = _object_name[:9]
|
|
elif len(_object_name) < 9:
|
|
_object_name = _object_name + " "*(9-len(_object_name))
|
|
|
|
|
|
# Generate the comment field.
|
|
_aprs_comment = aprs_comment
|
|
_aprs_comment = _aprs_comment.replace("<freq>", sonde_data['freq'])
|
|
_aprs_comment = _aprs_comment.replace("<id>", sonde_data['id'])
|
|
_aprs_comment = _aprs_comment.replace("<temp>", "%.1fC" % sonde_data['temp'])
|
|
_aprs_comment = _aprs_comment.replace("<vel_v>", "%.1fm/s" % sonde_data['vel_v'])
|
|
_aprs_comment = _aprs_comment.replace("<type>", sonde_data['type'])
|
|
|
|
# TODO: RS41 Burst Timer
|
|
|
|
# Convert float latitude to APRS format (DDMM.MM)
|
|
lat = float(sonde_data["lat"])
|
|
lat_degree = abs(int(lat))
|
|
lat_minute = abs(lat - int(lat)) * 60.0
|
|
lat_min_str = ("%02.2f" % lat_minute).zfill(5)
|
|
lat_dir = "S"
|
|
if lat>0.0:
|
|
lat_dir = "N"
|
|
lat_str = "%02d%s" % (lat_degree,lat_min_str) + lat_dir
|
|
|
|
# Convert float longitude to APRS format (DDDMM.MM)
|
|
lon = float(sonde_data["lon"])
|
|
lon_degree = abs(int(lon))
|
|
lon_minute = abs(lon - int(lon)) * 60.0
|
|
lon_min_str = ("%02.2f" % lon_minute).zfill(5)
|
|
lon_dir = "E"
|
|
if lon<0.0:
|
|
lon_dir = "W"
|
|
lon_str = "%03d%s" % (lon_degree,lon_min_str) + lon_dir
|
|
|
|
# Generate the added digits of precision, as per http://www.aprs.org/datum.txt
|
|
# Base-91 can only encode decimal integers between 0 and 93 (otherwise we end up with non-printable characters)
|
|
# So, we have to scale the range 00-99 down to 0-90, being careful to avoid errors due to floating point math.
|
|
_lat_prec = int(round(float(("%02.4f" % lat_minute)[-2:])/1.10))
|
|
_lon_prec = int(round(float(("%02.4f" % lon_minute)[-2:])/1.10))
|
|
|
|
# Now we can add 33 to the 0-90 value to produce the Base-91 character.
|
|
_lat_prec = chr(_lat_prec + 33)
|
|
_lon_prec = chr(_lon_prec + 33)
|
|
|
|
# Produce Datum + Added precision string
|
|
# We currently assume all position data is using the WGS84 datum,
|
|
# which I believe is true for most (if not all?) radiosondes.
|
|
_datum = "!w%s%s!" % (_lat_prec, _lon_prec)
|
|
|
|
# Convert Alt (in metres) to feet
|
|
alt = int(float(sonde_data["alt"])/0.3048)
|
|
|
|
# Produce the timestamp
|
|
_aprs_timestamp = sonde_data['datetime_dt'].strftime("%H%M%S")
|
|
|
|
# Generate course/speed data, if provided in the telemetry dictionary
|
|
if ('heading' in sonde_data.keys()) and ('vel_h' in sonde_data.keys()):
|
|
course_speed = "%03d/%03d" % (int(sonde_data['heading']), int(sonde_data['vel_h']*1.944))
|
|
else:
|
|
course_speed = "000/000"
|
|
|
|
|
|
if position_report:
|
|
# Produce an APRS position report string
|
|
# Note, we are using the 'position with timestamp' data type, as per http://www.aprs.org/doc/APRS101.PDF
|
|
out_str = "/%sh%s/%sO%s/A=%06d %s %s" % (_aprs_timestamp,lat_str,lon_str,course_speed,alt,_aprs_comment,_datum)
|
|
|
|
else:
|
|
# Produce an APRS Object
|
|
out_str = ";%s*%sh%s/%sO%s/A=%06d %s %s" % (_object_name,_aprs_timestamp,lat_str,lon_str,course_speed,alt,_aprs_comment,_datum)
|
|
|
|
# Return both the packet, and the 'callsign'.
|
|
return (out_str, _object_name.strip())
|
|
|
|
|
|
|
|
def generate_station_object(callsign, lat, lon, comment="radiosonde_auto_rx SondeGate v<version>", icon='/r', position_report=False):
|
|
''' Generate a station object '''
|
|
|
|
# Pad or limit the station callsign to 9 characters, if it is to long or short.
|
|
if len(callsign) > 9:
|
|
callsign = callsign[:9]
|
|
elif len(callsign) < 9:
|
|
callsign = callsign + " "*(9-len(callsign))
|
|
|
|
|
|
# Convert float latitude to APRS format (DDMM.MM)
|
|
lat = float(lat)
|
|
lat_degree = abs(int(lat))
|
|
lat_minute = abs(lat - int(lat)) * 60.0
|
|
lat_min_str = ("%02.2f" % lat_minute).zfill(5)
|
|
lat_dir = "S"
|
|
if lat>0.0:
|
|
lat_dir = "N"
|
|
lat_str = "%02d%s" % (lat_degree,lat_min_str) + lat_dir
|
|
|
|
# Convert float longitude to APRS format (DDDMM.MM)
|
|
lon = float(lon)
|
|
lon_degree = abs(int(lon))
|
|
lon_minute = abs(lon - int(lon)) * 60.0
|
|
lon_min_str = ("%02.2f" % lon_minute).zfill(5)
|
|
lon_dir = "E"
|
|
if lon<0.0:
|
|
lon_dir = "W"
|
|
lon_str = "%03d%s" % (lon_degree,lon_min_str) + lon_dir
|
|
|
|
# Generate timestamp using current UTC time
|
|
_aprs_timestamp = datetime.datetime.utcnow().strftime("%H%M%S")
|
|
|
|
|
|
# Add version string to position comment, if requested.
|
|
_aprs_comment = comment
|
|
_aprs_comment = _aprs_comment.replace('<version>', auto_rx_version)
|
|
|
|
# Generate output string
|
|
if position_report:
|
|
# Produce a position report with no timestamp, as per page 32 of http://www.aprs.org/doc/APRS101.PDF
|
|
out_str = "!%s%s%s%s%s" % (lat_str, icon[0], lon_str, icon[1], _aprs_comment)
|
|
|
|
else:
|
|
# Produce an object string
|
|
out_str = ";%s*%sh%s%s%s%s%s" % (callsign, _aprs_timestamp, lat_str, icon[0], lon_str, icon[1], _aprs_comment)
|
|
|
|
return out_str
|
|
|
|
|
|
#
|
|
# APRS Uploader Class
|
|
#
|
|
|
|
class APRSUploader(object):
|
|
'''
|
|
Queued APRS Telemetry Uploader class
|
|
This performs uploads to the Habitat servers, and also handles generation of flight documents.
|
|
|
|
Incoming telemetry packets are fed into queue, which is checked regularly.
|
|
If a new callsign is sighted, a payload document is created in the Habitat DB.
|
|
The telemetry data is then converted into a UKHAS-compatible format, before being added to queue to be
|
|
uploaded as network speed permits.
|
|
|
|
If an upload attempt times out, the packet is discarded.
|
|
If the queue fills up (probably indicating no network connection, and a fast packet downlink rate),
|
|
it is immediately emptied, to avoid upload of out-of-date packets.
|
|
|
|
Note that this uploader object is intended to handle telemetry from multiple sondes
|
|
'''
|
|
|
|
# We require the following fields to be present in the incoming telemetry dictionary data
|
|
REQUIRED_FIELDS = ['frame', 'id', 'datetime', 'lat', 'lon', 'alt', 'temp', 'type', 'freq', 'freq_float', 'datetime_dt']
|
|
|
|
|
|
def __init__(self,
|
|
aprs_callsign = 'N0CALL',
|
|
aprs_passcode = "00000",
|
|
object_name_override = None,
|
|
object_comment = "RadioSonde",
|
|
position_report = False,
|
|
aprsis_host = 'rotate.aprs2.net',
|
|
aprsis_port = 14580,
|
|
station_beacon = False,
|
|
station_beacon_rate = 30,
|
|
station_beacon_position = (0.0,0.0,0.0),
|
|
station_beacon_comment = "radiosonde_auto_rx SondeGate v<version>",
|
|
station_beacon_icon = "/r",
|
|
synchronous_upload_time = 30,
|
|
callsign_validity_threshold = 5,
|
|
upload_queue_size = 16,
|
|
upload_timeout = 10,
|
|
inhibit = False
|
|
):
|
|
""" Initialise an APRS Uploader object.
|
|
|
|
Args:
|
|
aprs_callsign (str): Callsign of the uploader, used when logging into APRS-IS.
|
|
aprs_passcode (tuple): Optional - a tuple consisting of (lat, lon, alt), which if populated,
|
|
is used to plot the listener's position on the Habitat map, both when this class is initialised, and
|
|
when a new sonde ID is observed.
|
|
|
|
object_name_override (str): Override the object name in the uploaded sentence with this value.
|
|
WARNING: This will horribly break the aprs.fi map if multiple sondes are uploaded simultaneously under the same callsign.
|
|
USE WITH CAUTION!!!
|
|
object_comment (str): A comment to go with the object. Various fields will be replaced with telmetry data.
|
|
|
|
position_report (bool): If True, upload positions as APRS position reports, otherwise, upload as an Object.
|
|
|
|
aprsis_host (str): APRS-IS Server to upload packets to.
|
|
aprsis_port (int): APRS-IS TCP port number.
|
|
|
|
station_beacon (bool): Enable beaconing of station position.
|
|
station_beacon_rate (int): Time delay between beacon uploads (minutes)
|
|
station_beacon_position (tuple): (lat, lon, alt), in decimal degrees, of the station position.
|
|
station_beacon_comment (str): Comment field for the station beacon. <version> will be replaced with the current auto_rx version.
|
|
station_beacon_icon (str): The APRS icon to be used, as the two characters (symbol table, symbol index), as per http://www.aprs.org/symbols.html
|
|
|
|
synchronous_upload_time (int): Upload the most recent telemetry when time.time()%synchronous_upload_time == 0
|
|
This is done in an attempt to get multiple stations uploading the same telemetry sentence simultaneously,
|
|
and also acts as decimation on the number of sentences uploaded to APRS-IS.
|
|
|
|
callsign_validity_threshold (int): Only upload telemetry data if the callsign has been observed more than N times. Default = 5
|
|
|
|
upload_queue_size (int): Maximum number of sentences to keep in the upload queue. If the queue is filled,
|
|
it will be emptied (discarding the queue contents).
|
|
upload_timeout (int): Timeout (Seconds) when performing uploads to APRS-IS. Default: 10 seconds.
|
|
|
|
inhibit (bool): Inhibit all uploads. Mainly intended for debugging.
|
|
|
|
"""
|
|
|
|
self.aprs_callsign = aprs_callsign
|
|
self.aprs_passcode = aprs_passcode
|
|
self.object_comment = object_comment
|
|
self.position_report = position_report
|
|
self.aprsis_host = aprsis_host
|
|
self.aprsis_port = aprsis_port
|
|
self.upload_timeout = upload_timeout
|
|
self.upload_queue_size = upload_queue_size
|
|
self.synchronous_upload_time = synchronous_upload_time
|
|
self.callsign_validity_threshold = callsign_validity_threshold
|
|
self.inhibit = inhibit
|
|
|
|
self.station_beacon = {
|
|
'enabled': station_beacon,
|
|
'position': station_beacon_position,
|
|
'rate': station_beacon_rate,
|
|
'comment': station_beacon_comment,
|
|
'icon': station_beacon_icon
|
|
}
|
|
|
|
if object_name_override is None:
|
|
self.object_name_override = "<id>"
|
|
else:
|
|
self.object_name_override = object_name_override
|
|
|
|
# Our two Queues - one to hold sentences to be upload, the other to temporarily hold
|
|
# input telemetry dictionaries before they are converted and processed.
|
|
self.aprs_upload_queue = Queue(upload_queue_size)
|
|
self.input_queue = Queue()
|
|
|
|
# Dictionary where we store sorted telemetry data for upload when required.
|
|
# Elements will be named after payload IDs, and will contain:
|
|
# 'count' (int): Number of times this callsign has been observed. Uploads will only occur when
|
|
# this number rises above callsign_validity_threshold.
|
|
# 'data' (Queue): A queue of telemetry sentences to be uploaded. When the upload timer fires,
|
|
# this queue will be dumped, and the most recent telemetry uploaded.
|
|
self.observed_payloads = {}
|
|
|
|
# Record of when we last uploaded a user station position to Habitat.
|
|
self.last_user_position_upload = 0
|
|
|
|
# Start the uploader thread.
|
|
self.upload_thread_running = True
|
|
self.upload_thread = Thread(target=self.aprs_upload_thread)
|
|
self.upload_thread.start()
|
|
|
|
# Start the input queue processing thread.
|
|
self.input_processing_running = True
|
|
self.input_thread = Thread(target=self.process_queue)
|
|
self.input_thread.start()
|
|
|
|
self.timer_thread_running = True
|
|
self.timer_thread = Thread(target=self.upload_timer)
|
|
self.timer_thread.start()
|
|
|
|
self.log_info("APRS Uploader Started.")
|
|
|
|
|
|
def aprsis_upload(self, source, packet, igate=False):
|
|
""" Upload a packet to APRS-IS
|
|
|
|
Args:
|
|
source (str): Callsign of the packet source.
|
|
packet (str): APRS packet to upload.
|
|
igate (boolean): If True, iGate the packet into APRS-IS
|
|
(i.e. use the original source call, but add SONDEGATE and our callsign to the path.)
|
|
|
|
"""
|
|
|
|
# If we are inhibited, just return immediately.
|
|
if self.inhibit:
|
|
self.log_info("Upload Inhibited: %s" % packet)
|
|
return True
|
|
|
|
|
|
# Generate APRS packet
|
|
if igate:
|
|
# If we are emulating an IGATE, then we need to add in a path, a q-construct, and our own callsign.
|
|
# We have the TOCALL field 'APRARX' allocated by Bob WB4APR, so we can now use this to indicate
|
|
# that these packets have arrived via radiosonde_auto_rx!
|
|
_packet = '%s>APRARX,SONDEGATE,TCPIP,qAR,%s:%s\n' % (source, self.aprs_callsign, packet)
|
|
else:
|
|
# Otherwise, we are probably just placing an object, usually sourced by our own callsign
|
|
_packet = '%s>APRS:%s\n' % (source, packet)
|
|
|
|
# create socket & connect to server
|
|
_s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
_s.settimeout(self.upload_timeout)
|
|
try:
|
|
_s.connect((self.aprsis_host, self.aprsis_port))
|
|
# Send logon string
|
|
_logon = 'user %s pass %s vers VK5QI-AutoRX \n' % (self.aprs_callsign, self.aprs_passcode)
|
|
_s.send(_logon.encode('ascii'))
|
|
# send packet
|
|
_s.send(_packet.encode('ascii'))
|
|
# close socket
|
|
_s.shutdown(0)
|
|
_s.close()
|
|
self.log_info("Uploaded to APRS-IS: %s" % _packet)
|
|
return True
|
|
except Exception as e:
|
|
self.log_error("Upload to APRS-IS Failed - %s" % str(e))
|
|
return False
|
|
|
|
|
|
def beacon_station_position(self):
|
|
''' Send a station position beacon into APRS-IS '''
|
|
if self.station_beacon['enabled']:
|
|
if (self.station_beacon['position'][0] == 0.0) and (self.station_beacon['position'][1] == 0.0):
|
|
self.log_error("Station position is 0,0, not uploading position beacon.")
|
|
self.last_user_position_upload = time.time()
|
|
return
|
|
|
|
|
|
# Generate the station position packet
|
|
# Note - this is now generated as an APRS position report, for radiosondy.info compatability.
|
|
_packet = generate_station_object(self.aprs_callsign,
|
|
self.station_beacon['position'][0],
|
|
self.station_beacon['position'][1],
|
|
self.station_beacon['comment'],
|
|
self.station_beacon['icon'],
|
|
position_report=True)
|
|
|
|
# Send the packet as an iGated packet.
|
|
self.aprsis_upload(self.aprs_callsign, _packet, igate=True)
|
|
self.last_user_position_upload = time.time()
|
|
|
|
|
|
def update_station_position(self, lat, lon, alt):
|
|
""" Update the internal station position record. Used when determining the station position by GPSD """
|
|
self.station_beacon['position'] = (lat, lon, alt)
|
|
|
|
|
|
|
|
def aprs_upload_thread(self):
|
|
''' Handle uploading of packets to Habitat '''
|
|
|
|
self.log_debug("Started APRS Uploader Thread.")
|
|
|
|
while self.upload_thread_running:
|
|
|
|
if self.aprs_upload_queue.qsize() > 0:
|
|
# If the queue is completely full, jump to the most recent telemetry sentence.
|
|
if self.aprs_upload_queue.qsize() == self.upload_queue_size:
|
|
while not self.aprs_upload_queue.empty():
|
|
_telem = self.aprs_upload_queue.get()
|
|
|
|
self.log_warning("Uploader queue was full - possible connectivity issue.")
|
|
else:
|
|
# Otherwise, get the first item in the queue.
|
|
_telem = self.aprs_upload_queue.get()
|
|
|
|
# Convert to a packet.
|
|
try:
|
|
(_packet, _call) = telemetry_to_aprs_position(_telem,
|
|
object_name=self.object_name_override,
|
|
aprs_comment = self.object_comment,
|
|
position_report=self.position_report)
|
|
except Exception as e:
|
|
self.log_error("Error converting telemetry to APRS packet - %s" % str(e))
|
|
_packet = None
|
|
|
|
# Attempt to upload it.
|
|
if _packet is not None:
|
|
# If we are uploading position reports, the source call is the generated callsign
|
|
# usually based on the sonde serial number, and we iGate the position report.
|
|
# Otherwise, we upload APRS Objects, sourced by our own callsign, but still iGated via us.
|
|
if self.position_report:
|
|
self.aprsis_upload(_call,_packet,igate=True)
|
|
else:
|
|
self.aprsis_upload(self.aprs_callsign,_packet,igate=True)
|
|
|
|
else:
|
|
# Wait for a short time before checking the queue again.
|
|
time.sleep(0.1)
|
|
|
|
self.log_debug("Stopped APRS Uploader Thread.")
|
|
|
|
|
|
|
|
def upload_timer(self):
|
|
""" Add packets to the habitat upload queue if it is time for us to upload. """
|
|
|
|
while self.timer_thread_running:
|
|
if int(time.time()) % self.synchronous_upload_time == 0:
|
|
# Time to upload!
|
|
for _id in self.observed_payloads.keys():
|
|
# If no data, continue...
|
|
if self.observed_payloads[_id]['data'].empty():
|
|
continue
|
|
else:
|
|
# Otherwise, dump the queue and keep the latest telemetry.
|
|
while not self.observed_payloads[_id]['data'].empty():
|
|
_telem = self.observed_payloads[_id]['data'].get()
|
|
|
|
# Attept to add it to the habitat uploader queue.
|
|
try:
|
|
self.aprs_upload_queue.put_nowait(_telem)
|
|
except Exception as e:
|
|
self.log_error("Error adding sentence to queue: %s" % str(e))
|
|
|
|
# Sleep a second so we don't hit the synchronous upload time again.
|
|
time.sleep(1)
|
|
else:
|
|
# Not yet time to upload, wait for a bit.
|
|
time.sleep(0.1)
|
|
|
|
|
|
def process_queue(self):
|
|
""" Process packets from the input queue.
|
|
|
|
This thread handles packets from the input queue (provided by the decoders)
|
|
Packets are sorted by ID, and a dictionary entry is created.
|
|
|
|
"""
|
|
|
|
while self.input_processing_running:
|
|
# Process everything in the queue.
|
|
while self.input_queue.qsize() > 0:
|
|
# Grab latest telem dictionary.
|
|
_telem = self.input_queue.get_nowait()
|
|
|
|
_id = _telem['id']
|
|
|
|
if _id not in self.observed_payloads:
|
|
# We haven't seen this ID before, so create a new dictionary entry for it.
|
|
self.observed_payloads[_id] = {'count':1, 'data':Queue()}
|
|
self.log_debug("New Payload %s. Not observed enough to allow upload." % _id)
|
|
# However, we don't yet add anything to the queue for this payload...
|
|
else:
|
|
# We have seen this payload before!
|
|
# Increment the 'seen' counter.
|
|
self.observed_payloads[_id]['count'] += 1
|
|
|
|
# If we have seen this particular ID enough times, add the data to the ID's queue.
|
|
if self.observed_payloads[_id]['count'] >= self.callsign_validity_threshold:
|
|
# Add the telemetry to the queue
|
|
self.observed_payloads[_id]['data'].put(_telem)
|
|
|
|
else:
|
|
self.log_debug("Payload ID %s not observed enough to allow upload." % _id)
|
|
|
|
if (time.time() - self.last_user_position_upload) > self.station_beacon['rate']*60:
|
|
self.beacon_station_position()
|
|
|
|
|
|
time.sleep(0.1)
|
|
|
|
|
|
def add(self, telemetry):
|
|
""" Add a dictionary of telemetry to the input queue.
|
|
|
|
Args:
|
|
telemetry (dict): Telemetry dictionary to add to the input queue.
|
|
|
|
"""
|
|
|
|
# Discard any telemetry which is indicated to be encrypted.
|
|
if 'encrypted' in telemetry:
|
|
return
|
|
|
|
# Check the telemetry dictionary contains the required fields.
|
|
for _field in self.REQUIRED_FIELDS:
|
|
if _field not in telemetry:
|
|
self.log_error("JSON object missing required field %s" % _field)
|
|
return
|
|
|
|
# Add it to the queue if we are running.
|
|
if self.input_processing_running:
|
|
self.input_queue.put(telemetry)
|
|
else:
|
|
self.log_error("Processing not running, discarding.")
|
|
|
|
|
|
|
|
def close(self):
|
|
''' Shutdown uploader and processing threads. '''
|
|
self.log_debug("Waiting for threads to close...")
|
|
self.input_processing_running = False
|
|
self.timer_thread_running = False
|
|
self.upload_thread_running = False
|
|
|
|
# Wait for all threads to close.
|
|
if self.upload_thread is not None:
|
|
self.upload_thread.join()
|
|
|
|
if self.timer_thread is not None:
|
|
self.timer_thread.join()
|
|
|
|
if self.input_thread is not None:
|
|
self.input_thread.join()
|
|
|
|
|
|
def log_debug(self, line):
|
|
""" Helper function to log a debug message with a descriptive heading.
|
|
Args:
|
|
line (str): Message to be logged.
|
|
"""
|
|
logging.debug("APRS-IS - %s" % line)
|
|
|
|
|
|
def log_info(self, line):
|
|
""" Helper function to log an informational message with a descriptive heading.
|
|
Args:
|
|
line (str): Message to be logged.
|
|
"""
|
|
logging.info("APRS-IS - %s" % line)
|
|
|
|
|
|
def log_error(self, line):
|
|
""" Helper function to log an error message with a descriptive heading.
|
|
Args:
|
|
line (str): Message to be logged.
|
|
"""
|
|
logging.error("APRS-IS - %s" % 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("APRS-IS - %s" % line)
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Some unit tests for the APRS packet generation code.
|
|
# ['frame', 'id', 'datetime', 'lat', 'lon', 'alt', 'temp', 'type', 'freq', 'freq_float', 'datetime_dt']
|
|
test_telem = [
|
|
{'id':'DFM06-123456', 'frame':10, 'lat':-10.0, 'lon':10.0, 'alt':10000, 'temp':1.0, 'type':'DFM', 'freq':'401.520 MHz', 'freq_float':401.52, 'heading':0.0, 'vel_h':5.1, 'vel_v':-5.0, 'datetime_dt':datetime.datetime.utcnow()},
|
|
{'id':'DFM09-123456', 'frame':10, 'lat':-10.0, 'lon':10.0, 'alt':10000, 'temp':1.0, 'type':'DFM', 'freq':'401.520 MHz', 'freq_float':401.52, 'heading':0.0, 'vel_h':5.1, 'vel_v':-5.0, 'datetime_dt':datetime.datetime.utcnow()},
|
|
{'id':'DFM15-123456', 'frame':10, 'lat':-10.0, 'lon':10.0, 'alt':10000, 'temp':1.0, 'type':'DFM', 'freq':'401.520 MHz', 'freq_float':401.52, 'heading':0.0, 'vel_h':5.1, 'vel_v':-5.0, 'datetime_dt':datetime.datetime.utcnow()},
|
|
{'id':'DFM17-12345678', 'frame':10, 'lat':-10.0, 'lon':10.0, 'alt':10000, 'temp':1.0, 'type':'DFM', 'freq':'401.520 MHz', 'freq_float':401.52, 'heading':0.0, 'vel_h':5.1, 'vel_v':-5.0, 'datetime_dt':datetime.datetime.utcnow()},
|
|
{'id':'N1234567', 'frame':10, 'lat':-10.0, 'lon':10.0, 'alt':10000, 'temp':1.0, 'type':'RS41', 'freq':'401.520 MHz', 'freq_float':401.52, 'heading':0.0, 'vel_h':5.1, 'vel_v':-5.0, 'datetime_dt':datetime.datetime.utcnow()},
|
|
{'id':'M1234567', 'frame':10, 'lat':-10.0, 'lon':10.0, 'alt':10000, 'temp':1.0, 'type':'RS92', 'freq':'401.520 MHz', 'freq_float':401.52, 'heading':0.0, 'vel_h':5.1, 'vel_v':-5.0, 'datetime_dt':datetime.datetime.utcnow()},
|
|
]
|
|
|
|
|
|
comment_field = "Clb=<vel_v> t=<temp> <freq> Type=<type> Radiosonde http://bit.ly/2Bj4Sfk"
|
|
|
|
for _telem in test_telem:
|
|
out_str = telemetry_to_aprs_position(_telem, object_name="<id>", aprs_comment=comment_field, position_report=False)
|
|
print(out_str)
|
|
|
|
|
|
|