Skip to content

Commit 8a73c66

Browse files
Merge pull request #47 from UMDBPP/develop
add slice indexing to linked list, freefall model to time-to-ground calculation
2 parents 8dcc8af + 7f3f77f commit 8a73c66

File tree

14 files changed

+148
-57
lines changed

14 files changed

+148
-57
lines changed

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,45 +17,45 @@ pip install packetraven
1717

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

20-
```cmd
20+
```bash
2121
packetraven --tnc COM4
2222
```
2323

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

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

28-
```cmd
28+
```bash
2929
packetraven --apikey <api_key> --callsigns W3EAX-8,W3EAX-14
3030
```
3131

3232
#### listen to a PostGIS database table:
3333

34-
```cmd
34+
```bash
3535
packetraven --database <username>@<hostname>:5432/<database_name>/<table_name>
3636
```
3737

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

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

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

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

5050
## Graphical User Interface
5151

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

54-
```cmd
54+
```bash
5555
packetraven --gui
5656
```
5757

58-
```cmd
58+
```bash
5959
packetraven --callsigns W3EAX-8,W3EAX-14 --apikey <api_key> --gui
6060
```
6161

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

client/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ def main():
206206
for tnc_location in kwargs['tnc']:
207207
tnc_location = tnc_location.strip()
208208
try:
209-
if 'txt' in tnc_location:
209+
if Path(tnc_location).suffix in ['.txt', '.log']:
210210
tnc_location = TextFileTNC(tnc_location, callsigns)
211211
LOGGER.info(f'reading file {tnc_location.location}')
212212
connections.append(tnc_location)

client/gui.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,7 @@ def toggle(self):
693693
self.__elements['tnc'].configure(state=tkinter.DISABLED)
694694
for tnc in tncs:
695695
try:
696-
if 'txt' in tnc:
696+
if Path(tnc).suffix in ['.txt', '.log']:
697697
tnc = TextFileTNC(tnc, self.callsigns)
698698
LOGGER.info(f'reading file {tnc.location}')
699699
else:

examples/credentials.config

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[aprs_fi]
2-
aprs_fi_key = <api key>
2+
api_key = <api key>
33

44
[tnc]
55
tnc_location = <serial port>

examples/read_text_file.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from packetraven import TextFileTNC
2+
from packetraven.tracks import APRSTrack
3+
4+
if __name__ == '__main__':
5+
filename = 'http://bpp.umd.edu/archives/Launches/NS-95_2020-11-07/APRS/W3EAX-11/W3EAX-11_raw_NS95.txt'
6+
raw_packet_text_file = TextFileTNC(filename)
7+
8+
packets = raw_packet_text_file.packets
9+
10+
packet_track = APRSTrack(callsign='W3EAX-11', packets=packets)
11+
12+
print(f'number of packets: {len(packet_track)}')
13+
print(f'time to ground: {packet_track.time_to_ground}')
14+
print(f'distance traveled (m): {packet_track.cumulative_overground_distances[-1]}')
15+
print(f'maximum altitude (m): {max(packet_track.altitudes)}')

packetraven/model.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import numpy
2+
3+
# `dh/dt` based on historical flight data
4+
DESCENT_RATE = lambda altitude: -5.8e-08 * altitude ** 2 - 6.001
5+
6+
# integration of `(1/(dh/dt)) dh` based on historical flight data
7+
# TODO make this model better via ML
8+
SECONDS_TO_GROUND = lambda altitude: 1695.02 * numpy.arctan(9.8311e-5 * altitude)

packetraven/structures.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from typing import Any, Iterable, List, Union
2+
3+
14
class DoublyLinkedList:
25
"""
36
A linked list is a series of node objects, each with a link (object reference) to the next node in the series.
@@ -147,7 +150,7 @@ def count(self, value) -> int:
147150
return sum([1 for node_value in self if node_value == value])
148151

149152
@property
150-
def difference(self) -> []:
153+
def difference(self) -> [Any]:
151154
"""
152155
differences between each value
153156
@@ -212,8 +215,15 @@ def _remove_node(self, node: Node):
212215
else:
213216
self.head = node.next_node
214217

215-
def __getitem__(self, index: int):
216-
return self._node_at_index(index).value
218+
def __getitem__(self, index: Union[int, Iterable[int], slice]) -> Union[Any, List[Any]]:
219+
if isinstance(index, int):
220+
return self._node_at_index(index).value
221+
elif isinstance(index, Iterable):
222+
return [self.__getitem__(value) for value in index]
223+
elif isinstance(index, slice):
224+
return self.__getitem__(range(*(value for value in (index.start, index.stop, index.step) if value is not None)))
225+
else:
226+
raise ValueError(f'unrecognized index: {index}')
217227

218228
def __setitem__(self, index: int, value):
219229
self._node_at_index(index).value = value

packetraven/tracks.py

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import numpy
66
from pyproj import CRS
77

8+
from .model import SECONDS_TO_GROUND
89
from .packets import APRSPacket, DEFAULT_CRS, LocationPacket
910
from .structures import DoublyLinkedList
1011

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

2425
self.name = name
25-
self.packets = DoublyLinkedList(packets)
26+
self.packets = DoublyLinkedList(None)
2627
self.crs = crs if crs is not None else DEFAULT_CRS
2728

29+
if packets is not None:
30+
for packet in packets:
31+
self.append(packet)
32+
2833
def append(self, packet: LocationPacket):
2934
if packet not in self.packets:
3035
if packet.crs != self.crs:
@@ -109,16 +114,13 @@ def cumulative_overground_distances(self) -> numpy.ndarray:
109114

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

114119
current_ascent_rate = self.ascent_rates[-1]
115-
116120
if current_ascent_rate < 0:
117121
# TODO implement landing location as the intersection of the predicted descent track with a local DEM
118122
# TODO implement a time to impact calc based off of standard atmo
119-
return timedelta(
120-
seconds=self.packets[-1].coordinates[2] / abs(current_ascent_rate)
121-
)
123+
return timedelta(seconds=self.altitudes[-1] / abs(current_ascent_rate))
122124
else:
123125
return timedelta(seconds=-1)
124126

@@ -157,7 +159,33 @@ def __str__(self) -> str:
157159
return str(list(self))
158160

159161

160-
class APRSTrack(LocationPacketTrack):
162+
class BalloonTrack(LocationPacketTrack):
163+
def __init__(self, name: str, packets: [LocationPacket] = None, crs: CRS = None):
164+
super().__init__(name, packets, crs)
165+
self.__has_burst = False
166+
167+
@property
168+
def time_to_ground(self) -> timedelta:
169+
if self.has_burst:
170+
# TODO implement landing location as the intersection of the predicted descent track with a local DEM
171+
return timedelta(seconds=SECONDS_TO_GROUND(self.altitudes[-1]))
172+
else:
173+
return timedelta(seconds=-1)
174+
175+
@property
176+
def has_burst(self) -> bool:
177+
current_ascent_rate = self.ascent_rates[-1]
178+
if current_ascent_rate > 0:
179+
self.__has_burst = False
180+
elif not self.__has_burst:
181+
current_altitude = self.altitudes[-1]
182+
max_altitude = numpy.max(self.altitudes)
183+
if current_ascent_rate < -2 and max_altitude > current_altitude:
184+
self.__has_burst = True
185+
return self.__has_burst
186+
187+
188+
class APRSTrack(BalloonTrack):
161189
""" collection of APRS location packets """
162190

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

172-
self.callsign = callsign
200+
if not isinstance(callsign, str):
201+
callsign = str(callsign)
202+
if len(callsign) > 9 or ' ' in callsign:
203+
raise ValueError(f'unrecognized callsign format: "{callsign}"')
204+
205+
self.__callsign = callsign
173206
super().__init__(self.callsign, packets, crs)
174207

208+
@property
209+
def callsign(self) -> str:
210+
return self.__callsign
211+
175212
def append(self, packet: APRSPacket):
176213
packet_callsign = packet['callsign']
177214

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +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"}}]}
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-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"}}]}

tests/reference/test_output.kml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +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>
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-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>

0 commit comments

Comments
 (0)