kopia lustrzana https://github.com/projecthorus/radiosonde_auto_rx
260 wiersze
8.9 KiB
Python
260 wiersze
8.9 KiB
Python
#!/usr/bin/env python
|
|
#
|
|
# radiosonde_auto_rx - OziMux Output
|
|
#
|
|
# Copyright (C) 2018 Mark Jessop <vk5qi@rfhead.net>
|
|
# Released under GNU GPL v3 or later
|
|
#
|
|
|
|
import json
|
|
import logging
|
|
import socket
|
|
import time
|
|
from threading import Thread
|
|
try:
|
|
# Python 2
|
|
from Queue import Queue
|
|
except ImportError:
|
|
# Python 3
|
|
from queue import Queue
|
|
|
|
|
|
class OziUploader(object):
|
|
""" Push radiosonde telemetry out via UDP broadcast to the Horus Chase-Car Utilities
|
|
|
|
Uploads to:
|
|
- OziMux / OziPlotter (UDP Broadcast, port 8942 by default) - Refer here for information on the data format:
|
|
https://github.com/projecthorus/oziplotter/wiki/3---Data-Sources
|
|
- "Payload Summary" (UDP Broadcast, on port 55672 by default)
|
|
Refer here for information: https://github.com/projecthorus/horus_utils/wiki/5.-UDP-Broadcast-Messages#payload-summary-payload_summary
|
|
|
|
NOTE: This class is designed to only handle telemetry from a single radiosonde at a time. It should only be operated in a
|
|
single-SDR configuration, where only one sonde is tracked at a time.
|
|
|
|
"""
|
|
|
|
# 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']
|
|
|
|
# Extra fields we can pass on to other programs.
|
|
EXTRA_FIELDS = ['bt', 'humidity', 'sats']
|
|
|
|
|
|
def __init__(self,
|
|
ozimux_port = None,
|
|
payload_summary_port = None,
|
|
update_rate = 5
|
|
):
|
|
""" Initialise an OziUploader Object.
|
|
|
|
Args:
|
|
ozimux_port (int): UDP port to push ozimux/oziplotter messages to. Set to None to disable.
|
|
payload_summary_port (int): UDP port to push payload summary messages to. Set to None to disable.
|
|
update_rate (int): Time in seconds between updates.
|
|
"""
|
|
|
|
self.ozimux_port = ozimux_port
|
|
self.payload_summary_port = payload_summary_port
|
|
self.update_rate = update_rate
|
|
|
|
# Input Queue.
|
|
self.input_queue = Queue()
|
|
|
|
# Start the input queue processing thread.
|
|
self.input_processing_running = True
|
|
self.input_thread = Thread(target=self.process_queue)
|
|
self.input_thread.start()
|
|
|
|
self.log_info("Started OziMux / Payload Summary Exporter")
|
|
|
|
|
|
def send_ozimux_telemetry(self, telemetry):
|
|
""" Send a packet of telemetry into the network in OziMux/OziPlotter-compatible format.
|
|
|
|
Args:
|
|
telemetry (dict): Telemetry dictionary to send.
|
|
|
|
"""
|
|
|
|
_short_time = telemetry['datetime_dt'].strftime("%H:%M:%S")
|
|
_sentence = "TELEMETRY,%s,%.5f,%.5f,%d\n" % (_short_time, telemetry['lat'], telemetry['lon'], telemetry['alt'])
|
|
|
|
try:
|
|
_ozisock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
|
|
|
|
# Set up socket for broadcast, and allow re-use of the address
|
|
_ozisock.setsockopt(socket.SOL_SOCKET,socket.SO_BROADCAST,1)
|
|
_ozisock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
# Under OSX we also need to set SO_REUSEPORT to 1
|
|
try:
|
|
_ozisock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
_ozisock.sendto(_sentence.encode('ascii'),('<broadcast>',self.ozimux_port))
|
|
# Catch any socket errors, that may occur when attempting to send to a broadcast address
|
|
# when there is no network connected. In this case, re-try and send to localhost instead.
|
|
except socket.error as e:
|
|
self.log_debug("Send to broadcast address failed, sending to localhost instead.")
|
|
_ozisock.sendto(_sentence.encode('ascii'),('127.0.0.1',self.ozimux_port))
|
|
|
|
_ozisock.close()
|
|
|
|
except Exception as e:
|
|
self.log_error("Failed to send OziMux packet: %s" % str(e))
|
|
|
|
|
|
def send_payload_summary(self, telemetry):
|
|
""" Send a payload summary message into the network via UDP broadcast.
|
|
|
|
Args:
|
|
telemetry (dict): Telemetry dictionary to send.
|
|
|
|
"""
|
|
|
|
try:
|
|
# Prepare heading & speed fields, if they are provided in the incoming telemetry blob.
|
|
if 'heading' in telemetry.keys():
|
|
_heading = telemetry['heading']
|
|
else:
|
|
_heading = -1
|
|
|
|
if 'vel_h' in telemetry.keys():
|
|
_speed = telemetry['vel_h']*3.6
|
|
else:
|
|
_speed = -1
|
|
|
|
# Generate 'short' time field.
|
|
_short_time = telemetry['datetime_dt'].strftime("%H:%M:%S")
|
|
|
|
|
|
packet = {
|
|
'type' : 'PAYLOAD_SUMMARY',
|
|
'callsign' : telemetry['id'],
|
|
'latitude' : telemetry['lat'],
|
|
'longitude' : telemetry['lon'],
|
|
'altitude' : telemetry['alt'],
|
|
'speed' : _speed,
|
|
'heading': _heading,
|
|
'time' : _short_time,
|
|
'comment' : 'Radiosonde',
|
|
# Additional fields specifically for radiosondes
|
|
'model': telemetry['type'],
|
|
'freq': telemetry['freq'],
|
|
'temp': telemetry['temp']
|
|
}
|
|
|
|
# Add in any extra fields we may care about.
|
|
for _field in self.EXTRA_FIELDS:
|
|
if _field in telemetry:
|
|
packet[_field] = telemetry[_field]
|
|
|
|
|
|
|
|
# Set up our UDP socket
|
|
_s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
|
|
_s.settimeout(1)
|
|
# Set up socket for broadcast, and allow re-use of the address
|
|
_s.setsockopt(socket.SOL_SOCKET,socket.SO_BROADCAST,1)
|
|
_s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
# Under OSX we also need to set SO_REUSEPORT to 1
|
|
try:
|
|
_s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
_s.sendto(json.dumps(packet).encode('ascii'), ('<broadcast>', self.payload_summary_port))
|
|
# Catch any socket errors, that may occur when attempting to send to a broadcast address
|
|
# when there is no network connected. In this case, re-try and send to localhost instead.
|
|
except socket.error as e:
|
|
self.log_debug("Send to broadcast address failed, sending to localhost instead.")
|
|
_s.sendto(json.dumps(packet).encode('ascii'), ('127.0.0.1', self.payload_summary_port))
|
|
|
|
_s.close()
|
|
|
|
except Exception as e:
|
|
self.log_error("Error sending Payload Summary: %s" % str(e))
|
|
|
|
|
|
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:
|
|
|
|
if self.input_queue.qsize() > 0:
|
|
# Dump the queue, keeping the most recent element.
|
|
while not self.input_queue.empty():
|
|
_telem = self.input_queue.get()
|
|
|
|
# Send!
|
|
if self.ozimux_port != None:
|
|
self.send_ozimux_telemetry(_telem)
|
|
|
|
if self.payload_summary_port != None:
|
|
self.send_payload_summary(_telem)
|
|
|
|
time.sleep(self.update_rate)
|
|
|
|
|
|
|
|
|
|
def add(self, telemetry):
|
|
""" Add a dictionary of telemetry to the input queue.
|
|
|
|
Args:
|
|
telemetry (dict): Telemetry dictionary to add to the input queue.
|
|
|
|
"""
|
|
|
|
# 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 processing thread. """
|
|
self.log_debug("Waiting for processing thread to close...")
|
|
self.input_processing_running = False
|
|
|
|
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("OziMux - %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("OziMux - %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("OziMux - %s" % line)
|