Skip to content

Commit

Permalink
Merge pull request #47 from UMDBPP/develop
Browse files Browse the repository at this point in the history
add slice indexing to linked list, freefall model to time-to-ground calculation
  • Loading branch information
zacharyburnett authored Mar 10, 2021
2 parents 8dcc8af + 7f3f77f commit 8a73c66
Show file tree
Hide file tree
Showing 14 changed files with 148 additions and 57 deletions.
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,45 +17,45 @@ pip install packetraven

#### listen to a TNC sending raw APRS strings over USB port COM4:

```cmd
```bash
packetraven --tnc COM4
```

### listen to APRS.fi, watching specific callsigns:

you need an API key to connect to APRS.fi; you can get one from https://aprs.fi/page/api

```cmd
```bash
packetraven --apikey <api_key> --callsigns W3EAX-8,W3EAX-14
```

#### listen to a PostGIS database table:

```cmd
```bash
packetraven --database <username>@<hostname>:5432/<database_name>/<table_name>
```

#### watch a text file for new lines containing raw APRS strings:

```cmd
```bash
packetraven --tnc http://bpp.umd.edu/archives/Launches/NS-95_2020-11-07/APRS/W3EAX-11/W3EAX-11_raw_NS95.txt
```

#### listen to a TNC on COM3, watching specific callsigns, and synchronize new packets with a database table via SSH tunnel:

```cmd
```bash
packetraven --tnc COM3 --callsigns W3EAX-8,W3EAX-14 --database <username>@<hostname>:5432/<database_name>/<table_name> --tunnel <ssh_username>@<hostname>:22
```

## Graphical User Interface

to start the GUI, add `--gui` to any `packetraven` command

```cmd
```bash
packetraven --gui
```

```cmd
```bash
packetraven --callsigns W3EAX-8,W3EAX-14 --apikey <api_key> --gui
```

Expand Down Expand Up @@ -94,7 +94,7 @@ optional arguments:
--prediction-api PREDICTION_API
API URL to use for prediction (one of ['https://predict.cusf.co.uk/api/v1/',
'https://predict.lukerenegar.com/api/v1.1/'])
--interval INTERVAL seconds between each main loop (default: 5)
--interval INTERVAL seconds between each main loop (default: 20)
--gui start the graphical interface
```

Expand Down
2 changes: 1 addition & 1 deletion client/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ def main():
for tnc_location in kwargs['tnc']:
tnc_location = tnc_location.strip()
try:
if 'txt' in tnc_location:
if Path(tnc_location).suffix in ['.txt', '.log']:
tnc_location = TextFileTNC(tnc_location, callsigns)
LOGGER.info(f'reading file {tnc_location.location}')
connections.append(tnc_location)
Expand Down
2 changes: 1 addition & 1 deletion client/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,7 @@ def toggle(self):
self.__elements['tnc'].configure(state=tkinter.DISABLED)
for tnc in tncs:
try:
if 'txt' in tnc:
if Path(tnc).suffix in ['.txt', '.log']:
tnc = TextFileTNC(tnc, self.callsigns)
LOGGER.info(f'reading file {tnc.location}')
else:
Expand Down
2 changes: 1 addition & 1 deletion examples/credentials.config
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[aprs_fi]
aprs_fi_key = <api key>
api_key = <api key>

[tnc]
tnc_location = <serial port>
Expand Down
15 changes: 15 additions & 0 deletions examples/read_text_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from packetraven import TextFileTNC
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)

packets = raw_packet_text_file.packets

packet_track = APRSTrack(callsign='W3EAX-11', packets=packets)

print(f'number of packets: {len(packet_track)}')
print(f'time to ground: {packet_track.time_to_ground}')
print(f'distance traveled (m): {packet_track.cumulative_overground_distances[-1]}')
print(f'maximum altitude (m): {max(packet_track.altitudes)}')
8 changes: 8 additions & 0 deletions packetraven/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import numpy

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

# 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)
16 changes: 13 additions & 3 deletions packetraven/structures.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from typing import Any, Iterable, List, Union


class DoublyLinkedList:
"""
A linked list is a series of node objects, each with a link (object reference) to the next node in the series.
Expand Down Expand Up @@ -147,7 +150,7 @@ def count(self, value) -> int:
return sum([1 for node_value in self if node_value == value])

@property
def difference(self) -> []:
def difference(self) -> [Any]:
"""
differences between each value
Expand Down Expand Up @@ -212,8 +215,15 @@ def _remove_node(self, node: Node):
else:
self.head = node.next_node

def __getitem__(self, index: int):
return self._node_at_index(index).value
def __getitem__(self, index: Union[int, Iterable[int], slice]) -> Union[Any, List[Any]]:
if isinstance(index, int):
return self._node_at_index(index).value
elif isinstance(index, Iterable):
return [self.__getitem__(value) for value in index]
elif isinstance(index, slice):
return self.__getitem__(range(*(value for value in (index.start, index.stop, index.step) if value is not None)))
else:
raise ValueError(f'unrecognized index: {index}')

def __setitem__(self, index: int, value):
self._node_at_index(index).value = value
Expand Down
53 changes: 45 additions & 8 deletions packetraven/tracks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import numpy
from pyproj import CRS

from .model import SECONDS_TO_GROUND
from .packets import APRSPacket, DEFAULT_CRS, LocationPacket
from .structures import DoublyLinkedList

Expand All @@ -22,9 +23,13 @@ def __init__(self, name: str, packets: [LocationPacket] = None, crs: CRS = None)
"""

self.name = name
self.packets = DoublyLinkedList(packets)
self.packets = DoublyLinkedList(None)
self.crs = crs if crs is not None else DEFAULT_CRS

if packets is not None:
for packet in packets:
self.append(packet)

def append(self, packet: LocationPacket):
if packet not in self.packets:
if packet.crs != self.crs:
Expand Down Expand Up @@ -109,16 +114,13 @@ def cumulative_overground_distances(self) -> numpy.ndarray:

@property
def time_to_ground(self) -> timedelta:
""" estimated time to reach the ground at the current ascent rate """
""" estimated time to reach the ground at the current rate of descent """

current_ascent_rate = self.ascent_rates[-1]

if current_ascent_rate < 0:
# TODO implement landing location as the intersection of the predicted descent track with a local DEM
# TODO implement a time to impact calc based off of standard atmo
return timedelta(
seconds=self.packets[-1].coordinates[2] / abs(current_ascent_rate)
)
return timedelta(seconds=self.altitudes[-1] / abs(current_ascent_rate))
else:
return timedelta(seconds=-1)

Expand Down Expand Up @@ -157,7 +159,33 @@ def __str__(self) -> str:
return str(list(self))


class APRSTrack(LocationPacketTrack):
class BalloonTrack(LocationPacketTrack):
def __init__(self, name: str, packets: [LocationPacket] = None, crs: CRS = None):
super().__init__(name, packets, crs)
self.__has_burst = False

@property
def time_to_ground(self) -> timedelta:
if self.has_burst:
# TODO implement landing location as the intersection of the predicted descent track with a local DEM
return timedelta(seconds=SECONDS_TO_GROUND(self.altitudes[-1]))
else:
return timedelta(seconds=-1)

@property
def has_burst(self) -> bool:
current_ascent_rate = self.ascent_rates[-1]
if current_ascent_rate > 0:
self.__has_burst = False
elif not self.__has_burst:
current_altitude = self.altitudes[-1]
max_altitude = numpy.max(self.altitudes)
if current_ascent_rate < -2 and max_altitude > current_altitude:
self.__has_burst = True
return self.__has_burst


class APRSTrack(BalloonTrack):
""" collection of APRS location packets """

def __init__(self, callsign: str, packets: [APRSPacket] = None, crs: CRS = None):
Expand All @@ -169,9 +197,18 @@ def __init__(self, callsign: str, packets: [APRSPacket] = None, crs: CRS = None)
:param crs: coordinate reference system to use
"""

self.callsign = callsign
if not isinstance(callsign, str):
callsign = str(callsign)
if len(callsign) > 9 or ' ' in callsign:
raise ValueError(f'unrecognized callsign format: "{callsign}"')

self.__callsign = callsign
super().__init__(self.callsign, packets, crs)

@property
def callsign(self) -> str:
return self.__callsign

def append(self, packet: APRSPacket):
packet_callsign = packet['callsign']

Expand Down
2 changes: 1 addition & 1 deletion tests/reference/test_output.geojson
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-77.909211, 39.700356, 8201.8632]}, "properties": {"time": "20181111102013", "altitude": 8201.8632, "ascent_rate": 0.0, "ground_speed": 0.0, "callsign": "W3EAX-8"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-77.881058, 39.700099, 8645.652]}, "properties": {"time": "20181111102124", "altitude": 8645.652, "ascent_rate": 6.251, "ground_speed": 34.01, "callsign": "W3EAX-8"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-77.425253, 39.641857, 13130.784]}, "properties": {"time": "20190203143928", "altitude": 13130.784000000001, "ascent_rate": 0.001, "ground_speed": 0.005, "callsign": "W3EAX-13"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[-77.909211, 39.700356, 8201.8632], [-77.881058, 39.700099, 8645.652], [-77.425253, 39.641857, 13130.784]]}, "properties": {"time": "20190203143928", "altitude": 13130.784000000001, "ascent_rate": 0.001, "ground_speed": 0.005, "seconds_to_ground": -1.0, "callsign": "W3EAX-13"}}]}
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-77.909211, 39.700356, 8201.8632]}, "properties": {"time": "20181111102013", "altitude": 8201.8632, "ascent_rate": 0.0, "ground_speed": 0.0, "callsign": "W3EAX-8"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-77.881058, 39.700099, 8645.652]}, "properties": {"time": "20181111102124", "altitude": 8645.652, "ascent_rate": 6.251, "ground_speed": 34.01, "callsign": "W3EAX-8"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-77.425253, 39.641857, 13130.784]}, "properties": {"time": "20190203143928", "altitude": 13130.784000000001, "ascent_rate": 0.001, "ground_speed": 0.005, "callsign": "W3EAX-8"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[-77.909211, 39.700356, 8201.8632], [-77.881058, 39.700099, 8645.652], [-77.425253, 39.641857, 13130.784]]}, "properties": {"time": "20190203143928", "altitude": 13130.784000000001, "ascent_rate": 0.001, "ground_speed": 0.005, "seconds_to_ground": -1.0, "callsign": "W3EAX-8"}}]}
2 changes: 1 addition & 1 deletion tests/reference/test_output.kml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<kml:kml xmlns:kml="http://www.opengis.net/kml/2.2"><kml:Document id="1"><kml:name>root document</kml:name><kml:description>root document, containing geometries</kml:description><kml:visibility>1</kml:visibility><kml:Placemark id="1 0 0"><kml:name>20181111102013 W3EAX-13</kml:name><kml:description>altitude=8201.8632 ascent_rate=0.0 ground_speed=0.0</kml:description><kml:visibility>1</kml:visibility><kml:Point><kml:coordinates>-77.909211,39.700356,8201.863200</kml:coordinates></kml:Point></kml:Placemark><kml:Placemark id="1 0 1"><kml:name>20181111102124 W3EAX-13</kml:name><kml:description>altitude=8645.652 ascent_rate=6.251 ground_speed=34.01</kml:description><kml:visibility>1</kml:visibility><kml:Point><kml:coordinates>-77.881058,39.700099,8645.652000</kml:coordinates></kml:Point></kml:Placemark><kml:Placemark id="1 0 2"><kml:name>20190203143928 W3EAX-13</kml:name><kml:description>altitude=13130.784000000001 ascent_rate=0.001 ground_speed=0.005</kml:description><kml:visibility>1</kml:visibility><kml:Point><kml:coordinates>-77.425253,39.641857,13130.784000</kml:coordinates></kml:Point></kml:Placemark><kml:Placemark id="1 0"><kml:name>W3EAX-13</kml:name><kml:description>altitude=13130.784000000001 ascent_rate=0.001 ground_speed=0.005 seconds_to_ground=-1.0</kml:description><kml:visibility>1</kml:visibility><kml:LineString><kml:coordinates>-77.909211,39.700356,8201.863200 -77.881058,39.700099,8645.652000 -77.425253,39.641857,13130.784000</kml:coordinates></kml:LineString></kml:Placemark></kml:Document></kml:kml>
<kml:kml xmlns:kml="http://www.opengis.net/kml/2.2"><kml:Document id="1"><kml:name>root document</kml:name><kml:description>root document, containing geometries</kml:description><kml:visibility>1</kml:visibility><kml:Placemark id="1 0 0"><kml:name>20181111102013 W3EAX-8</kml:name><kml:description>altitude=8201.8632 ascent_rate=0.0 ground_speed=0.0</kml:description><kml:visibility>1</kml:visibility><kml:Point><kml:coordinates>-77.909211,39.700356,8201.863200</kml:coordinates></kml:Point></kml:Placemark><kml:Placemark id="1 0 1"><kml:name>20181111102124 W3EAX-8</kml:name><kml:description>altitude=8645.652 ascent_rate=6.251 ground_speed=34.01</kml:description><kml:visibility>1</kml:visibility><kml:Point><kml:coordinates>-77.881058,39.700099,8645.652000</kml:coordinates></kml:Point></kml:Placemark><kml:Placemark id="1 0 2"><kml:name>20190203143928 W3EAX-8</kml:name><kml:description>altitude=13130.784000000001 ascent_rate=0.001 ground_speed=0.005</kml:description><kml:visibility>1</kml:visibility><kml:Point><kml:coordinates>-77.425253,39.641857,13130.784000</kml:coordinates></kml:Point></kml:Placemark><kml:Placemark id="1 0"><kml:name>W3EAX-8</kml:name><kml:description>altitude=13130.784000000001 ascent_rate=0.001 ground_speed=0.005 seconds_to_ground=-1.0</kml:description><kml:visibility>1</kml:visibility><kml:LineString><kml:coordinates>-77.909211,39.700356,8201.863200 -77.881058,39.700099,8645.652000 -77.425253,39.641857,13130.784000</kml:coordinates></kml:LineString></kml:Placemark></kml:Document></kml:kml>
2 changes: 1 addition & 1 deletion tests/reference/test_output.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
2018-11-11 10:20:13 EST: W3EAX-8>APRS,WIDE1-1,WIDE2-1,qAR,K3DO-11:!/:Gh=:j)#O /A=026909|!Q| /W3EAX,262,0,18'C,http://www.umd.edu
2018-11-11 10:21:24 EST: W3EAX-8>APRS,N3TJJ-12,WIDE1*,WIDE2-1,qAR,N3FYI-2:!/:GiD:jcwO /A=028365|!R| /W3EAX,267,0,18'C,http://www.umd.edu
2019-02-03 14:39:28 EST: W3EAX-13>APRS,KC3FIT-1,WIDE1*,WIDE2-1,qAR,KC3AWP-10:!/:JL2:u4wO /A=043080|!j| /W3EAX,326,0,20'C,nearspace.umd.edu
2019-02-03 14:39:28 EST: W3EAX-8>APRS,KC3FIT-1,WIDE1*,WIDE2-1,qAR,KC3AWP-10:!/:JL2:u4wO /A=043080|!j| /W3EAX,326,0,20'C,nearspace.umd.edu
23 changes: 19 additions & 4 deletions tests/test_packets.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,28 +136,43 @@ def test_time_to_ground():
packet_time=datetime(2019, 2, 3, 14, 38, 23),
)
packet_3 = APRSPacket.from_frame(
"W3EAX-13>APRS,KC3FIT-1,WIDE1*,WIDE2-1,qAR,KC3AWP-10:!/:JL2:u4wO /A=043080|!j| /W3EAX,326,0,20'C,"
"W3EAX-13>APRS,N3KTX-10*,WIDE1,WIDE2-1,qAR,N3TJJ-11:!/:J..:sh'O /A=063614|!g| /W3EAX,313,0,21'C,"
'nearspace.umd.edu',
packet_time=datetime(2019, 2, 3, 14, 39, 28),
)
packet_4 = APRSPacket.from_frame(
"W3EAX-13>APRS,WIDE1-1,WIDE2-1,qAR,K3DO-11:!/:Gh=:j)#O /A=026909|!Q| /W3EAX,262,0,18'C,http://www.umd.edu"
"W3EAX-13>APRS,KC3FIT-1,WIDE1*,WIDE2-1,qAR,KC3AWP-10:!/:JL2:u4wO /A=043080|!j| /W3EAX,326,0,20'C,"
'nearspace.umd.edu',
packet_time=datetime(2019, 2, 3, 14, 41, 50),
)
packet_5 = APRSPacket.from_frame(
"W3EAX-13>APRS,N3KTX-10*,WIDE1,WIDE2-1,qAR,N3TJJ-11:!/:J..:sh'O /A=063614|!g| /W3EAX,313,0,21'C,"
'nearspace.umd.edu',
packet_time=datetime(2019, 2, 3, 14, 42, 34),
)

track = APRSTrack('W3EAX-13')

track.append(packet_1)

assert not track.has_burst
assert track.time_to_ground == timedelta(seconds=-1)

track.append(packet_2)

assert track.time_to_ground >= timedelta(seconds=798)
assert track.has_burst
assert track.time_to_ground == timedelta(seconds=1603.148748)

track.append(packet_3)

assert not track.has_burst
assert track.time_to_ground == timedelta(seconds=-1)

track.append(packet_4)
track.append(packet_5)

assert track.time_to_ground >= timedelta(days=1066)
assert track.has_burst
assert track.time_to_ground == timedelta(seconds=1545.354922)


def test_sorting():
Expand Down
2 changes: 2 additions & 0 deletions tests/test_structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ def test_index():
assert list_1[-2] == 5
assert list_1[-1] == 6
assert list_1[-1] is list_1.tail.value
assert list_1[:2] == [0, 5]
assert list_1[[1, 3, 0]] == [5, 'foo', 0]


def test_length():
Expand Down
Loading

0 comments on commit 8a73c66

Please sign in to comment.