Remove horuslib dependency. Initial commit of serial GPS class.

bearings
Mark Jessop 2018-08-28 09:16:27 +09:30
rodzic 934929f9d0
commit af84d5442c
6 zmienionych plików z 915 dodań i 5 usunięć

Wyświetl plik

@ -0,0 +1,117 @@
#!/usr/bin/env python
#
# Project Horus - Atmosphere / Descent Rate Modelling
#
# Copyright (C) 2018 Mark Jessop <vk5qi@rfhead.net>
# Released under GNU GPL v3 or later
#
import math
def getDensity(altitude):
'''
Calculate the atmospheric density for a given altitude in metres.
This is a direct port of the oziplotter Atmosphere class
'''
#Constants
airMolWeight = 28.9644 # Molecular weight of air
densitySL = 1.225 # Density at sea level [kg/m3]
pressureSL = 101325 # Pressure at sea level [Pa]
temperatureSL = 288.15 # Temperature at sea level [deg K]
gamma = 1.4
gravity = 9.80665 # Acceleration of gravity [m/s2]
tempGrad = -0.0065 # Temperature gradient [deg K/m]
RGas = 8.31432 # Gas constant [kg/Mol/K]
R = 287.053
deltaTemperature = 0.0;
# Lookup Tables
altitudes = [0, 11000, 20000, 32000, 47000, 51000, 71000, 84852]
pressureRels = [1, 2.23361105092158e-1, 5.403295010784876e-2, 8.566678359291667e-3, 1.0945601337771144e-3, 6.606353132858367e-4, 3.904683373343926e-5, 3.6850095235747942e-6]
temperatures = [288.15, 216.65, 216.65, 228.65, 270.65, 270.65, 214.65, 186.946]
tempGrads = [-6.5, 0, 1, 2.8, 0, -2.8, -2, 0]
gMR = gravity * airMolWeight / RGas;
# Pick a region to work in
i = 0
if(altitude > 0):
while (altitude > altitudes[i+1]):
i = i + 1
# Lookup based on region
baseTemp = temperatures[i]
tempGrad = tempGrads[i] / 1000.0
pressureRelBase = pressureRels[i]
deltaAltitude = altitude - altitudes[i]
temperature = baseTemp + tempGrad * deltaAltitude
# Calculate relative pressure
if(math.fabs(tempGrad) < 1e-10):
pressureRel = pressureRelBase * math.exp(-1 *gMR * deltaAltitude / 1000.0 / baseTemp)
else:
pressureRel = pressureRelBase * math.pow(baseTemp / temperature, gMR / tempGrad / 1000.0)
# Add temperature offset
temperature = temperature + deltaTemperature
# Finally, work out the density...
speedOfSound = math.sqrt(gamma * R * temperature)
pressure = pressureRel * pressureSL
density = densitySL * pressureRel * temperatureSL / temperature
return density
def seaLevelDescentRate(descent_rate, altitude):
''' Calculate the descent rate at sea level, for a given descent rate at altitude '''
rho = getDensity(altitude)
return math.sqrt((rho / 1.22) * math.pow(descent_rate, 2))
def time_to_landing(current_altitude, current_descent_rate=-5.0, ground_asl=0.0, step_size=1):
''' Calculate an estimated time to landing (in seconds) of a payload, based on its current altitude and descent rate '''
# A few checks on the input data.
if current_descent_rate > 0.0:
# If we are still ascending, return none.
return None
if current_altitude <= ground_asl:
# If the current altitude is *below* ground level, we have landed.
return 0
# Calculate the sea level descent rate.
_desc_rate = math.fabs(seaLevelDescentRate(current_descent_rate, current_altitude))
_drag_coeff = _desc_rate*1.1045 # Magic multiplier from predict.php
_alt = current_altitude
_start_time = 0
# Now step through the flight in <step_size> second steps.
# Once the altitude is below our ground level, stop, and return the elapsed time.
while _alt >= ground_asl:
_alt += step_size * -1*(_drag_coeff/math.sqrt(getDensity(_alt)))
_start_time += step_size
return _start_time
if __name__ == '__main__':
# Test Cases
_altitudes = [1000, 10000, 30000, 1000, 10000, 30000]
_rates = [-10.0, -10.0, -10.0, -30.0, -30.0, -30.0]
for i in range(len(_altitudes)):
print("Altitude: %d m, Rate: %.2f m/s" % (_altitudes[i], _rates[i]))
print("Density: %.5f" % getDensity(_altitudes[i]))
print("Sea Level Descent Rate: %.2f m/s" % seaLevelDescentRate(_rates[i], _altitudes[i]))
_landing = time_to_landing(_altitudes[i],_rates[i])
_landing_min = _landing//60
_landing_sec = _landing%60
print("Time to landing: %d sec, %s:%s " % (_landing, _landing_min,_landing_sec))
print("")

Wyświetl plik

@ -0,0 +1,137 @@
#!/usr/bin/env python
#
# Project Horus - Browser-Based Chase Mapper
# Listeners
#
# Copyright (C) 2018 Mark Jessop <vk5qi@rfhead.net>
# Released under GNU GPL v3 or later
#
from math import radians, degrees, sin, cos, atan2, sqrt, pi
def position_info(listener, balloon):
"""
Calculate and return information from 2 (lat, lon, alt) tuples
Copyright 2012 (C) Daniel Richman; GNU GPL 3
Returns a dict with:
- angle at centre
- great circle distance
- distance in a straight line
- bearing (azimuth or initial course)
- elevation (altitude)
Input and output latitudes, longitudes, angles, bearings and elevations are
in degrees, and input altitudes and output distances are in meters.
"""
# Earth:
#radius = 6371000.0
radius = 6364963.0 # Optimized for Australia :-)
(lat1, lon1, alt1) = listener
(lat2, lon2, alt2) = balloon
lat1 = radians(lat1)
lat2 = radians(lat2)
lon1 = radians(lon1)
lon2 = radians(lon2)
# Calculate the bearing, the angle at the centre, and the great circle
# distance using Vincenty's_formulae with f = 0 (a sphere). See
# http://en.wikipedia.org/wiki/Great_circle_distance#Formulas and
# http://en.wikipedia.org/wiki/Great-circle_navigation and
# http://en.wikipedia.org/wiki/Vincenty%27s_formulae
d_lon = lon2 - lon1
sa = cos(lat2) * sin(d_lon)
sb = (cos(lat1) * sin(lat2)) - (sin(lat1) * cos(lat2) * cos(d_lon))
bearing = atan2(sa, sb)
aa = sqrt((sa ** 2) + (sb ** 2))
ab = (sin(lat1) * sin(lat2)) + (cos(lat1) * cos(lat2) * cos(d_lon))
angle_at_centre = atan2(aa, ab)
great_circle_distance = angle_at_centre * radius
# Armed with the angle at the centre, calculating the remaining items
# is a simple 2D triangley circley problem:
# Use the triangle with sides (r + alt1), (r + alt2), distance in a
# straight line. The angle between (r + alt1) and (r + alt2) is the
# angle at the centre. The angle between distance in a straight line and
# (r + alt1) is the elevation plus pi/2.
# Use sum of angle in a triangle to express the third angle in terms
# of the other two. Use sine rule on sides (r + alt1) and (r + alt2),
# expand with compound angle formulae and solve for tan elevation by
# dividing both sides by cos elevation
ta = radius + alt1
tb = radius + alt2
ea = (cos(angle_at_centre) * tb) - ta
eb = sin(angle_at_centre) * tb
elevation = atan2(ea, eb)
# Use cosine rule to find unknown side.
distance = sqrt((ta ** 2) + (tb ** 2) - 2 * tb * ta * cos(angle_at_centre))
# Give a bearing in range 0 <= b < 2pi
if bearing < 0:
bearing += 2 * pi
return {
"listener": listener, "balloon": balloon,
"listener_radians": (lat1, lon1, alt1),
"balloon_radians": (lat2, lon2, alt2),
"angle_at_centre": degrees(angle_at_centre),
"angle_at_centre_radians": angle_at_centre,
"bearing": degrees(bearing),
"bearing_radians": bearing,
"great_circle_distance": great_circle_distance,
"straight_distance": distance,
"elevation": degrees(elevation),
"elevation_radians": elevation
}
def bearing_to_cardinal(bearing):
""" Convert a bearing in degrees to a 16-point cardinal direction """
bearing = bearing % 360.0
if bearing < 11.25:
cardinal = "N"
elif 11.25 <= bearing < 33.75:
cardinal = "NNE"
elif 33.75 <= bearing < 56.25:
cardinal = "NE"
elif 56.25 <= bearing < 78.75:
cardinal = "ENE"
elif 78.75 <= bearing < 101.25:
cardinal = "E"
elif 101.25 <= bearing < 123.75:
cardinal = "ESE"
elif 123.75 <= bearing < 146.25:
cardinal = "SE"
elif 146.25 <= bearing < 168.75:
cardinal = "SSE"
elif 168.75 <= bearing < 191.25:
cardinal = "S"
elif 191.25 <= bearing < 213.75:
cardinal = "SSW"
elif 213.75 <= bearing < 236.25:
cardinal = "SW"
elif 236.25 <= bearing < 258.75:
cardinal = "WSW"
elif 258.75 <= bearing < 281.25:
cardinal = "W"
elif 281.25 <= bearing < 303.75:
cardinal = "WNW"
elif 303.75 <= bearing < 326.25:
cardinal = "NW"
elif 326.25 <= bearing < 348.75:
cardinal = "NNW"
elif bearing >= 348.75:
cardinal = "N"
else:
cardinal = "?"
return cardinal

Wyświetl plik

@ -0,0 +1,162 @@
#!/usr/bin/env python
#
# Project Horus - Flight Data to Geometry
#
# Copyright (C) 2018 Mark Jessop <vk5qi@rfhead.net>
# Released under GNU GPL v3 or later
#
import traceback
import logging
import numpy as np
from .atmosphere import *
from .earthmaths import position_info
class GenericTrack(object):
"""
A Generic 'track' object, which stores track positions for a payload or chase car.
Telemetry is added using the add_telemetry method, which takes a dictionary with time/lat/lon/alt keys (at minimum).
This object performs a running average of the ascent/descent rate, and calculates the predicted landing rate if the payload
is in descent.
The track history can be exported to a LineString using the to_line_string method.
"""
def __init__(self,
ascent_averaging = 6,
landing_rate = 5.0):
''' Create a GenericTrack Object. '''
# Averaging rate.
self.ASCENT_AVERAGING = ascent_averaging
# Payload state.
self.landing_rate = landing_rate
self.ascent_rate = 5.0
self.heading = 0.0
self.speed = 0.0
self.is_descending = False
# Internal store of track history data.
# Data is stored as a list-of-lists, with elements of [datetime, lat, lon, alt, comment]
self.track_history = []
def add_telemetry(self,data_dict):
'''
Accept telemetry data as a dictionary with fields
datetime, lat, lon, alt, comment
'''
try:
_datetime = data_dict['time']
_lat = data_dict['lat']
_lon = data_dict['lon']
_alt = data_dict['alt']
if 'comment' in data_dict.keys():
_comment = data_dict['comment']
else:
_comment = ""
self.track_history.append([_datetime, _lat, _lon, _alt, _comment])
self.update_states()
return self.get_latest_state()
except:
logging.error("Error reading input data: %s" % traceback.format_exc())
def get_latest_state(self):
''' Get the latest position of the payload '''
if len(self.track_history) == 0:
return None
else:
_latest_position = self.track_history[-1]
_state = {
'time' : _latest_position[0],
'lat' : _latest_position[1],
'lon' : _latest_position[2],
'alt' : _latest_position[3],
'ascent_rate': self.ascent_rate,
'is_descending': self.is_descending,
'landing_rate': self.landing_rate,
'heading': self.heading,
'speed': self.speed
}
return _state
def calculate_ascent_rate(self):
''' Calculate the ascent/descent rate of the payload based on the available data '''
if len(self.track_history) <= 1:
return 5.0
elif len(self.track_history) == 2:
# Basic ascent rate case - only 2 samples.
_time_delta = (self.track_history[-1][0] - self.track_history[-2][0]).total_seconds()
_altitude_delta = self.track_history[-1][3] - self.track_history[-2][3]
return _altitude_delta/_time_delta
else:
_num_samples = min(len(self.track_history), self.ASCENT_AVERAGING)
_asc_rates = []
for _i in range(-1*(_num_samples-1), 0):
_time_delta = (self.track_history[_i][0] - self.track_history[_i-1][0]).total_seconds()
_altitude_delta = self.track_history[_i][3] - self.track_history[_i-1][3]
_asc_rates.append(_altitude_delta/_time_delta)
return np.mean(_asc_rates)
def calculate_heading(self):
''' Calculate the heading of the payload '''
if len(self.track_history) <= 1:
return 0.0
else:
_pos_1 = self.track_history[-2]
_pos_2 = self.track_history[-1]
_pos_info = position_info((_pos_1[1],_pos_1[2],_pos_1[3]), (_pos_2[1],_pos_2[2],_pos_2[3]))
return _pos_info['bearing']
def calculate_speed(self):
""" Calculate Payload Speed in metres per second """
if len(self.track_history)<=1:
return 0.0
else:
_time_delta = (self.track_history[-1][0] - self.track_history[-2][0]).total_seconds()
_pos_1 = self.track_history[-2]
_pos_2 = self.track_history[-1]
_pos_info = position_info((_pos_1[1],_pos_1[2],_pos_1[3]), (_pos_2[1],_pos_2[2],_pos_2[3]))
_speed = _pos_info['great_circle_distance']/_time_delta
return _speed
def update_states(self):
''' Update internal states based on the current data '''
self.ascent_rate = self.calculate_ascent_rate()
self.heading = self.calculate_heading()
self.speed = self.calculate_speed()
self.is_descending = self.ascent_rate < 0.0
if self.is_descending:
_current_alt = self.track_history[-1][3]
self.landing_rate = seaLevelDescentRate(self.ascent_rate, _current_alt)
def to_polyline(self):
''' Generate and return a Leaflet PolyLine compatible array '''
# Copy array into a numpy representation for easier slicing.
if len(self.track_history) == 0:
return []
elif len(self.track_history) == 1:
# LineStrings need at least 2 points. If we only have a single point,
# fudge it by duplicating the single point.
_track_data_np = np.array([self.track_history[0], self.track_history[0]])
else:
_track_data_np = np.array(self.track_history)
# Produce new array
_track_points = np.column_stack((_track_data_np[:,1], _track_data_np[:,2], _track_data_np[:,3]))
return _track_points.tolist()

243
chasemapper/gps.py 100644
Wyświetl plik

@ -0,0 +1,243 @@
#!/usr/bin/env python
#
# Project Horus - Browser-Based Chase Mapper
# GPS Communication Classes
#
# Copyright (C) 2018 Mark Jessop <vk5qi@rfhead.net>
# Released under GNU GPL v3 or later
#
import logging
import re
import traceback
from datetime import datetime
from threading import Thread
class SerialGPS(object):
'''
Read NMEA strings from a serial-connected GPS receiver
'''
def __init__(self,
serial_port = '/dev/ttyUSB0',
serial_baud = 9600,
timeout = 5,
callback = None):
'''
Initialise a SerialGPS object.
This class assumes the serial-connected GPS outputs GPRMC and GPGGA NMEA strings
using 8N1 RS232 framing.
Args:
serial_port (str): Serial port (i.e. '/dev/ttyUSB0', or 'COM1') to receive data from.
serial_baud (int): Baud rate.
timeout (int): Serial port readline timeout (Seconds)
callback (function): function to pass valid GPS positions to.
GPS data is passed as a dictionary with fields matching the Horus UDP GPS message:
packet = {
'type' : 'GPS',
'latitude': lat,
'longitude': lon,
'altitude': alt,
'speed': speed*3.6, # Convert speed to kph.
'valid': position_valid
}
'''
self.serial_port = serial_port
self.serial_baud = serial_baud
self.timeout = timeout
self.callback = callback
# Current GPS state, in a format which matches the Horus UDP
# 'Chase Car Position' message.
self.gps_state = {
'type': 'GPS',
'latitude': 0.0,
'longitude': 0.0,
'altitude': 0.0,
'speed': 0.0,
'valid': False
}
self.serial_thread_running = False
self.serial_thread = None
self.ser = None
self.start()
def start(self):
''' Start the GPS thread '''
if self.serial_thread != None:
return
else:
self.serial_thread_running = True
self.serial_thread = Thread(target=self.gps_thread)
self.serial_thread.start()
def close(self):
''' Stop the GPS thread. '''
self.serial_thread_running = False
# Wait for the thread to close.
if self.serial_thread != None:
self.serial_thread.join()
def gps_thread(self):
'''
Attempt to connect to a serial port and read lines of text from it.
Pass all lines on to the NMEA parser function.
'''
try:
import serial
except ImportError:
logging.critical("Could not import pyserial library!")
return
while self.serial_thread_running:
# Attempt to connect to the serial port.
while self.ser == None and self.serial_thread_running:
try:
self.ser = serial.Serial(port=self.serial_port,baudrate=self.serial_baud,timeout=self.timeout)
logging.info("SerialGPS - Connected to serial port.")
except Exception as e:
# Continue re-trying until we can connect to the serial port.
# This should let the user connect the gps *after* this object if instantiated if required.
logging.error("SerialGPS - Serial Port Error: %s" % e)
logging.error("SerialGPS - Sleeping 10s before attempting re-connect.")
time.sleep(10)
self.ser = None
continue
# Read a line of (hopefully) NMEA from the serial port.
try:
data = self.ser.readline()
except:
# If we hit a serial read error, attempt to reconnect.
logging.error("SerialGPS - Error reading from serial device! Attempting to reconnect.")
self.ser = None
continue
# Attempt to parse data.
try:
self.parse_nmea(data.decode('ascii'))
except:
pass
# Clean up before exiting thread.
try:
self.ser.close()
except:
pass
logging.info("SerialGPS - Closing Thread.")
def dm_to_sd(self, dm):
'''
Converts a geographic coordiante given in "degres/minutes" dddmm.mmmm
format (ie, "12319.943281" = 123 degrees, 19.953281 minutes) to a signed
decimal (python float) format.
Courtesy of https://github.com/Knio/pynmea2/
'''
# '12319.943281'
if not dm or dm == '0':
return 0.
d, m = re.match(r'^(\d+)(\d\d\.\d+)$', dm).groups()
return float(d) + float(m) / 60
def parse_nmea(self, data):
'''
Attempt to parse a line of NMEA data.
If we have received a GPGGA string containing a position valid flag,
send the data on to the callback function.
'''
if "$GPRMC" in data:
logging.debug("SerialGPS - Got GPRMC.")
gprmc = data.split(",")
gprmc_lat = self.dm_to_sd(gprmc[3])
gprmc_latns = gprmc[4]
gprmc_lon = self.dm_to_sd(gprmc[5])
gprmc_lonew = gprmc[6]
gprmc_speed = float(gprmc[7])
if gprmc_latns == "S":
self.gps_state['latitude'] = gprmc_lat*-1.0
else:
self.gps_state['latitude'] = gprmc_lat
if gprmc_lon == "W":
self.gps_state['longitude'] = gprmc_lon*-1.0
else:
self.gps_state['longitude'] = gprmc_lon
self.gps_state['speed'] = gprmc_speed*0.51444*3.6
elif "$GPGGA" in data:
logging.debug("SerialGPS - Got GPGGA.")
gpgga = data.split(",")
gpgga_lat = self.dm_to_sd(gpgga[2])
gpgga_latns = gpgga[3]
gpgga_lon = self.dm_to_sd(gpgga[4])
gpgga_lonew = gpgga[5]
gpgga_fixstatus = gpgga[6]
self.gps_state['altitude'] = float(gpgga[9])
if gpgga_latns == "S":
self.gps_state['latitude'] = gpgga_lat*-1.0
else:
self.gps_state['latitude'] = gpgga_lat
if gpgga_lon == "W":
self.gps_state['longitude'] = gpgga_lon*-1.0
else:
self.gps_state['longitude'] = gpgga_lon
if gpgga_fixstatus == 0:
self.gps_state['valid'] = False
else:
self.gps_state['valid'] = True
self.send_to_callback()
else:
# Discard all other lines
pass
def send_to_callback(self):
'''
Send the current GPS data snapshot onto the callback function,
if one exists.
'''
# Generate a copy of the gps state
_state = self.gps_state.copy()
# Attempt to pass it onto the callback function.
if self.callback != None:
try:
self.callback(_state)
except Exception as e:
traceback.print_exc()
logging.error("SerialGPS - Error Passing data to callback - %s" % str(e))
if __name__ == '__main__':
import sys, time
logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG)
_port = sys.argv[1]
_baud = 9600
def print_data(data):
print(data)
_gps = SerialGPS(serial_port=_port, serial_baud=_baud, callback=print_data)
time.sleep(20)
_gps.close()

Wyświetl plik

@ -0,0 +1,251 @@
#!/usr/bin/env python
#
# Project Horus - Browser-Based Chase Mapper
# Listeners
#
# Copyright (C) 2018 Mark Jessop <vk5qi@rfhead.net>
# Released under GNU GPL v3 or later
#
# These classes have been pulled in from the horuslib library, to avoid
# requiring horuslib (hopefully soon-to-be retired) as a dependency.
import socket, json, sys, traceback
from threading import Thread
from dateutil.parser import parse
from datetime import datetime
MAX_JSON_LEN = 2048
class UDPListener(object):
''' UDP Broadcast Packet Listener
Listens for Horuslib UDP broadcast packets, and passes them onto a callback function
'''
def __init__(self,
callback=None,
summary_callback = None,
gps_callback = None,
port=55672):
self.udp_port = port
self.callback = callback
self.summary_callback = summary_callback
self.gps_callback = gps_callback
self.listener_thread = None
self.s = None
self.udp_listener_running = False
def handle_udp_packet(self, packet):
''' Process a received UDP packet '''
try:
packet_dict = json.loads(packet)
if self.callback is not None:
self.callback(packet_dict)
if packet_dict['type'] == 'PAYLOAD_SUMMARY':
if self.summary_callback is not None:
self.summary_callback(packet_dict)
if packet_dict['type'] == 'GPS':
if self.gps_callback is not None:
self.gps_callback(packet_dict)
except Exception as e:
print("Could not parse packet: %s" % str(e))
traceback.print_exc()
def udp_rx_thread(self):
''' Listen for Broadcast UDP packets '''
self.s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
self.s.settimeout(1)
self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
except:
pass
self.s.bind(('',self.udp_port))
print("Started UDP Listener Thread.")
self.udp_listener_running = True
while self.udp_listener_running:
try:
m = self.s.recvfrom(MAX_JSON_LEN)
except socket.timeout:
m = None
except:
traceback.print_exc()
if m != None:
self.handle_udp_packet(m[0])
print("Closing UDP Listener")
self.s.close()
def start(self):
if self.listener_thread is None:
self.listener_thread = Thread(target=self.udp_rx_thread)
self.listener_thread.start()
def close(self):
self.udp_listener_running = False
self.listener_thread.join()
class OziListener(object):
"""
Listen on a supplied UDP port for OziPlotter-compatible telemetry data.
Incoming sentences are of the form:
TELEMETRY.HH:MM:SS,latitude,longitude,altitude\n
WAYPOINT,waypoint_name,latitude,longitude,comment\n
"""
allowed_sentences = ['TELEMETRY', 'WAYPOINT']
def __init__(self,
hostname = '',
port = 8942,
telemetry_callback = None,
waypoint_callback = None):
self.input_host = hostname
self.input_port = port
self.telemetry_callback = telemetry_callback
self.waypoint_callback = waypoint_callback
self.start()
def start(self):
''' Start the UDP Listener Thread. '''
self.udp_listener_running = True
self.t = Thread(target=self.udp_rx_thread)
self.t.start()
def udp_rx_thread(self):
"""
Listen for incoming UDP packets, and pass them off to another function to be processed.
"""
self.s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
self.s.settimeout(1)
self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
except:
pass
self.s.bind((self.input_host, self.input_port))
while self.udp_listener_running:
try:
m = self.s.recvfrom(1024)
except socket.timeout:
m = None
except:
traceback.print_exc()
if m != None:
try:
self.handle_packet(m[0])
except:
traceback.print_exc()
print("ERROR: Couldn't handle packet correctly.")
pass
print("INFO: Closing UDP Listener Thread")
self.s.close()
def close(self):
"""
Close the UDP listener thread.
"""
self.udp_listener_running = False
try:
self.t.join()
except:
pass
def handle_telemetry_packet(self, packet):
''' Split a telemetry packet into time/lat/lon/alt, and pass it onto a callback '''
_fields = packet.split(',')
_short_time = _fields[1]
_lat = float(_fields[2])
_lon = float(_fields[3])
_alt = float(_fields[4])
# Timestamp Handling
# The 'short' timestamp (HH:MM:SS) is always assumed to be in UTC time.
# To build up a complete datetime object, we use the system's current UTC time, and replace the HH:MM:SS part.
_full_time = datetime.utcnow().strftime("%Y-%m-%dT") + _short_time + "Z"
_time_dt = parse(_full_time)
_output = {
'time' : _time_dt,
'lat' : _lat,
'lon' : _lon,
'alt' : _alt,
'comment' : 'Telemetry Data'
}
self.telemetry_callback(_output)
def handle_waypoint_packet(self, packet):
''' Split a 'Waypoint' packet into fields, and pass onto a callback '''
_fields = packet.split(',')
_waypoint_name = _fields[1]
_lat = float(_fields[2])
_lon = float(_fields[3])
_comment = _fields[4]
_time_dt = datetime.utcnow()
_output = {
'time' : _time_dt,
'name' : _waypoint_name,
'lat' : _lat,
'lon' : _lon,
'comment' : _comment
}
self.waypoint_callback(_output)
def handle_packet(self, packet):
"""
Check an incoming packet matches a valid type, and then forward it on.
"""
# Extract header (first field)
packet_type = packet.split(',')[0]
if packet_type not in self.allowed_sentences:
print("ERROR: Got unknown packet: %s" % packet)
return
try:
# Now send on the packet if we are allowed to.
if packet_type == "TELEMETRY" and (self.telemetry_callback != None):
self.handle_telemetry_packet(packet)
# Generally we always want to pass on waypoint data.
if packet_type == "WAYPOINT" and (self.waypoint_callback != None):
self.handle_waypoint_packet(packet)
except:
print("ERROR: Error when handling packet.")
traceback.print_exc()

Wyświetl plik

@ -16,12 +16,12 @@ import traceback
from threading import Thread
from datetime import datetime, timedelta
from dateutil.parser import parse
from horuslib import *
from horuslib.geometry import *
from horuslib.atmosphere import time_to_landing
from horuslib.listener import OziListener, UDPListener
from horuslib.earthmaths import *
from chasemapper.config import *
from chasemapper.earthmaths import *
from chasemapper.geometry import *
from chasemapper.atmosphere import time_to_landing
from chasemapper.listeners import OziListener, UDPListener
from chasemapper.predictor import predictor_spawn_download, model_download_running