Skip to content

Commit

Permalink
Merge pull request #51 from UMDBPP/develop
Browse files Browse the repository at this point in the history
fix case where float prediction fails after stop time; run descent-on…
  • Loading branch information
zacharyburnett authored Mar 21, 2021
2 parents 0e0dadd + 801f953 commit 033fdbe
Show file tree
Hide file tree
Showing 13 changed files with 251 additions and 77 deletions.
25 changes: 14 additions & 11 deletions client/cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from argparse import ArgumentParser
from datetime import datetime
from datetime import datetime, timedelta
from getpass import getpass
from pathlib import Path
import sys
Expand All @@ -10,7 +10,7 @@
from client import DEFAULT_INTERVAL_SECONDS
from client.gui import PacketRavenGUI
from client.retrieve import retrieve_packets
from packetraven.connections import APRSDatabaseTable, APRSfi, APRSis, SerialTNC, TextFileTNC
from packetraven.connections import APRSDatabaseTable, APRSfi, APRSis, PacketGeoJSON, RawAPRSTextFile, SerialTNC
from packetraven.predicts import PredictionAPIURL, PredictionError, get_predictions
from packetraven.utilities import get_logger, read_configuration, repository_root
from packetraven.writer import write_packet_tracks
Expand Down Expand Up @@ -58,7 +58,7 @@ def main():
'--prediction-float-altitude', help='float altitude to use for prediction (m)'
)
args_parser.add_argument(
'--prediction-float-end-time', help='float end time to use for prediction'
'--prediction-float-duration', help='duration of float (s)'
)
args_parser.add_argument(
'--prediction-api',
Expand Down Expand Up @@ -181,8 +181,8 @@ def main():
if args.prediction_float_altitude is not None:
kwargs['prediction_float_altitude'] = float(args.prediction_descent_rate)

if args.prediction_float_end_time is not None:
kwargs['prediction_float_end_time'] = parse_date(args.prediction_float_end_time)
if args.prediction_float_duration is not None:
kwargs['prediction_float_duration'] = timedelta(seconds=float(args.prediction_float_duration))

if args.prediction_api is not None:
kwargs['prediction_api_url'] = args.prediction_api
Expand Down Expand Up @@ -219,7 +219,7 @@ def main():
tnc_location = tnc_location.strip()
try:
if Path(tnc_location).suffix in ['.txt', '.log']:
tnc_location = TextFileTNC(tnc_location, callsigns)
tnc_location = RawAPRSTextFile(tnc_location, callsigns)
LOGGER.info(f'reading file {tnc_location.location}')
connections.append(tnc_location)
else:
Expand Down Expand Up @@ -313,6 +313,13 @@ def main():
else:
database = None

if len(connections) == 0:
if output_filename is not None and output_filename.exists():
connections.append(PacketGeoJSON(output_filename))
else:
LOGGER.error(f'no connections started')
sys.exit(1)

if using_igate:
try:
aprs_is = APRSis(callsigns)
Expand All @@ -321,10 +328,6 @@ def main():
else:
aprs_is = None

if len(connections) == 0:
LOGGER.error(f'no connections started')
sys.exit(1)

filter_message = 'retrieving packets'
if start_date is not None and end_date is None:
filter_message += f' sent after {start_date:%Y-%m-%d %H:%M:%S}'
Expand Down Expand Up @@ -355,7 +358,7 @@ def main():
logger=LOGGER,
)

if prediction_filename is not None:
if prediction_filename is not None and len(new_packets) > 0:
try:
predictions = get_predictions(
packet_tracks,
Expand Down
17 changes: 10 additions & 7 deletions client/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from client import DEFAULT_INTERVAL_SECONDS
from client.retrieve import retrieve_packets
from packetraven.base import available_serial_ports, next_open_serial_port
from packetraven.connections import APRSDatabaseTable, APRSfi, APRSis, SerialTNC, TextFileTNC
from packetraven.connections import APRSDatabaseTable, APRSfi, APRSis, PacketGeoJSON, RawAPRSTextFile, SerialTNC
from packetraven.packets import APRSPacket
from packetraven.plotting import LivePlot
from packetraven.predicts import PredictionError, get_predictions
Expand Down Expand Up @@ -63,7 +63,7 @@ def __init__(
'prediction_burst_altitude': None,
'prediction_sea_level_descent_rate': None,
'prediction_float_altitude': None,
'prediction_float_end_time': None,
'prediction_float_duration': None,
'prediction_api_url': None,
},
}
Expand Down Expand Up @@ -696,7 +696,7 @@ def toggle(self):
for tnc in tncs:
try:
if Path(tnc).suffix in ['.txt', '.log']:
tnc = TextFileTNC(tnc, self.callsigns)
tnc = RawAPRSTextFile(tnc, self.callsigns)
LOGGER.info(f'reading file {tnc.location}')
else:
tnc = SerialTNC(tnc, self.callsigns)
Expand All @@ -707,7 +707,7 @@ def toggle(self):
self.tncs = [
connection.location
for connection in self.__connections
if isinstance(connection, SerialTNC) or isinstance(connection, TextFileTNC)
if isinstance(connection, SerialTNC) or isinstance(connection, RawAPRSTextFile)
]

api_key = self.__configuration['aprs_fi']['aprs_fi_key']
Expand Down Expand Up @@ -833,8 +833,11 @@ def toggle(self):
self.aprs_is = None

if len(self.__connections) == 0:
connection_errors = '\n'.join(connection_errors)
raise ConnectionError(f'no connections started\n{connection_errors}')
if self.output_filename is not None and self.output_filename.exists():
self.__connections.append(PacketGeoJSON(self.output_filename))
else:
connection_errors = '\n'.join(connection_errors)
raise ConnectionError(f'no connections started\n{connection_errors}')

LOGGER.info(
f'listening for packets every {self.interval_seconds}s from {len(self.__connections)} '
Expand Down Expand Up @@ -912,7 +915,7 @@ def retrieve_packets(self):
logger=LOGGER,
)

if self.toggles['prediction_file']:
if self.toggles['prediction_file'] and len(new_packets) > 0:
try:
self.__predictions = get_predictions(
self.packet_tracks,
Expand Down
43 changes: 27 additions & 16 deletions client/retrieve.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
from datetime import datetime, timedelta
from logging import Logger
from os import PathLike
from pathlib import Path

from aprslib.packets.base import APRSPacket
import numpy

from packetraven import APRSDatabaseTable
from packetraven.base import APRSPacketSource
from packetraven.connections import TimeIntervalError
from packetraven.tracks import APRSTrack
from packetraven.base import PacketSource
from packetraven.connections import PacketDatabaseTable, PacketGeoJSON, TimeIntervalError
from packetraven.tracks import APRSTrack, LocationPacketTrack
from packetraven.utilities import get_logger
from packetraven.writer import write_packet_tracks

LOGGER = get_logger('packetraven')


def retrieve_packets(
connections: [APRSPacketSource],
packet_tracks: [APRSTrack],
database: APRSDatabaseTable = None,
connections: [PacketSource],
packet_tracks: [LocationPacketTrack],
database: PacketDatabaseTable = None,
output_filename: PathLike = None,
start_date: datetime = None,
end_date: datetime = None,
logger: Logger = None,
) -> {str: APRSPacket}:
if output_filename is not None:
if not isinstance(output_filename, Path):
output_filename = Path(output_filename)

if logger is None:
logger = LOGGER

Expand Down Expand Up @@ -96,29 +100,36 @@ def retrieve_packets(
f'{coordinate:.3f}°' for coordinate in packet_track.coordinates[-1, :2]
)
message += (
f'{callsign:8} #{len(packet_track)} ({coordinate_string}, {packet_track.coordinates[-1, 2]:.2f}m); '
f'{(current_time - packet_time) / timedelta(seconds=1):.2f}s old; '
f'{packet_track.intervals[-1]:.2f}s since last packet; '
f'{packet_track.overground_distances[-1]:.2f}m distance over ground ({packet_track.ground_speeds[-1]:.2f}m/s), '
f'{callsign:8} #{len(packet_track)} ({coordinate_string}, {packet_track.coordinates[-1, 2]:.2f}m)'
f'; {(current_time - packet_time) / timedelta(seconds=1):.2f}s old'
f'; {packet_track.intervals[-1]:.2f}s since last packet'
f'; {packet_track.overground_distances[-1]:.2f}m distance over ground ({packet_track.ground_speeds[-1]:.2f}m/s), '
f'{packet_track.ascents[-1]:.2f}m ascent ({packet_track.ascent_rates[-1]:.2f}m/s)'
)

if packet_track.time_to_ground >= timedelta(seconds=0):
current_time_to_ground = (
packet_time + packet_track.time_to_ground - current_time
)
current_time_to_ground = packet_time + packet_track.time_to_ground - current_time
message += (
f'; currently falling from max altitude of {packet_track.coordinates[:, 2].max():.3f} m; '
f'{current_time_to_ground / timedelta(seconds=1):.2f} s to the ground'
f'; {packet_track} descending from max altitude of {packet_track.coordinates[:, 2].max():.3f} m'
f'; {current_time_to_ground / timedelta(seconds=1):.2f} s to the ground'
)
except Exception as error:
LOGGER.exception(f'{error.__class__.__name__} - {error}')
finally:
logger.info(message)

packet_track.sort()

if output_filename is not None:
write_packet_tracks(
[packet_tracks[callsign] for callsign in updated_callsigns], output_filename
)

output_filename_index = None
for index, connection in enumerate(connections):
if isinstance(connection, PacketGeoJSON):
output_filename_index = index
if output_filename_index is not None:
connections.pop(output_filename_index)

return new_packets
4 changes: 2 additions & 2 deletions examples/read_text_file.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from packetraven import TextFileTNC
from packetraven import RawAPRSTextFile
from packetraven.tracks import APRSTrack

if __name__ == '__main__':
filename = 'http://bpp.umd.edu/archives/Launches/NS-95_2020-11-07/APRS/W3EAX-11/W3EAX-11_raw_NS95.txt'
raw_packet_text_file = TextFileTNC(filename)
raw_packet_text_file = RawAPRSTextFile(filename)

packets = raw_packet_text_file.packets

Expand Down
2 changes: 1 addition & 1 deletion packetraven/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from packetraven.connections import APRSDatabaseTable, APRSfi, SerialTNC, TextFileTNC
from packetraven.connections import APRSDatabaseTable, APRSfi, RawAPRSTextFile, SerialTNC
89 changes: 82 additions & 7 deletions packetraven/connections.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from datetime import datetime, timedelta
from os import PathLike
from pathlib import Path
import re
from time import sleep
from typing import Any, Sequence
from urllib.parse import urlparse

import aprslib
from dateutil.parser import parse as parse_date
import geojson
import requests
from serial import Serial
from shapely.geometry import Point
Expand Down Expand Up @@ -80,10 +80,10 @@ def close(self):
self.serial_connection.close()

def __repr__(self):
return f'{self.__class__.__name__}("{self.location}")'
return f'{self.__class__.__name__}({repr(self.location)}, {repr(self.callsigns)})'


class TextFileTNC(APRSPacketSource):
class RawAPRSTextFile(APRSPacketSource):
def __init__(self, filename: PathLike = None, callsigns: str = None):
"""
read APRS packets from a given text file where each line consists of the time sent (`YYYY-MM-DDTHH:MM:SS`) followed by
Expand Down Expand Up @@ -153,7 +153,82 @@ def close(self):
pass

def __repr__(self):
return f'{self.__class__.__name__}("{self.location}")'
return f'{self.__class__.__name__}({repr(self.location)}, {repr(self.callsigns)})'


class PacketGeoJSON(PacketSource):
def __init__(self, filename: PathLike = None):
"""
read location packets from a given GeoJSON file
:param filename: path to GeoJSON file
"""

if not urlparse(str(filename)).scheme in ['http', 'https', 'ftp', 'sftp']:
if not isinstance(filename, Path):
if isinstance(filename, str):
filename = filename.strip('"')
filename = Path(filename)
filename = str(filename)

super().__init__(filename)
self.__last_access_time = None

@property
def packets(self) -> [LocationPacket]:
if self.__last_access_time is not None and self.interval is not None:
interval = datetime.now() - self.__last_access_time
if interval < self.interval:
raise TimeIntervalError(
f'interval {interval} less than minimum interval {self.interval}'
)

if Path(self.location).exists():
with open(Path(self.location).expanduser().resolve()) as file_connection:
features = geojson.load(file_connection)
else:
response = requests.get(self.location, stream=True)
features = geojson.loads(response.text)

packets = []
for feature in features['features']:
if feature['geometry']['type'] == 'Point':
properties = feature['properties']
time = parse_date(properties['time'])
del properties['time']

if 'from' in properties:
from_callsign = properties['from']
to_callsign = properties['to']
del properties['from'], properties['to']

packet = APRSPacket(
from_callsign,
to_callsign,
time,
*feature['geometry']['coordinates'],
source=self.location,
**properties,
)
else:
packet = LocationPacket(
time,
*feature['geometry']['coordinates'],
source=self.location,
**properties,
)

packets.append(packet)

self.__last_access_time = datetime.now()

return packets

def close(self):
pass

def __repr__(self):
return f'{self.__class__.__name__}({repr(self.location)})'


class APRSfi(APRSPacketSource, NetworkConnection):
Expand Down Expand Up @@ -240,7 +315,7 @@ def close(self):
pass

def __repr__(self):
return f'{self.__class__.__name__}({repr(self.callsigns)}, {repr(re.sub(".", "*", self.api_key))})'
return f'{self.__class__.__name__}({repr(self.callsigns)}, {repr("****")})'


class PacketDatabaseTable(PostGresTable, PacketSource, PacketSink):
Expand All @@ -254,10 +329,10 @@ class PacketDatabaseTable(PostGresTable, PacketSource, PacketSink):
}

def __init__(self, hostname: str, database: str, table: str, **kwargs):
if 'fields' not in kwargs:
kwargs['fields'] = {}
if 'primary_key' not in kwargs:
kwargs['primary_key'] = 'time'
if 'fields' not in kwargs:
kwargs['fields'] = {}
kwargs['fields'] = {**self.__default_fields, **kwargs['fields']}
PostGresTable.__init__(
self, hostname=hostname, database=database, table_name=table, **kwargs
Expand Down
7 changes: 4 additions & 3 deletions packetraven/model.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import numpy

# `dh/dt` based on historical flight data
DESCENT_RATE = lambda altitude: -5.8e-08 * altitude ** 2 - 6.001
FREEFALL_DESCENT_RATE = lambda altitude: -5.8e-08 * altitude ** 2 - 6.001
FREEFALL_DESCENT_RATE_UNCERTAINTY = lambda altitude: 0.2 * FREEFALL_DESCENT_RATE(altitude)

# integration of `(1/(dh/dt)) dh` based on historical flight data
# TODO make this model better via ML
SECONDS_TO_GROUND = lambda altitude: 1695.02 * numpy.arctan(9.8311e-5 * altitude)
# TODO make this model better with ML
FREEFALL_SECONDS_TO_GROUND = lambda altitude: 1695.02 * numpy.arctan(9.8311e-5 * altitude)
2 changes: 1 addition & 1 deletion packetraven/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def update(
lines = self.axis.plot(
getattr(packet_track, VARIABLES[self.variable]['x']),
getattr(packet_track, VARIABLES[self.variable]['y']),
linewidth=3,
linewidth=2,
marker='o',
label=packet_track.name,
)
Expand Down
Loading

0 comments on commit 033fdbe

Please sign in to comment.