Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RockBLOCK interface to download IRIDIUM packets on send #77

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,23 @@ pip install packetraven
virtualenv packetraven_env
```
2. activate your new virtual environment
- On Linux:
- On Linux:
```bash
source packetraven_env/bin/activate
```
- On Windows native command prompt (`cmd`):
- On Windows native command prompt (`cmd`):
```cmd
.\packetraven_env\Scripts\activate.bat
```
- On Windows PowerShell:
- On Windows PowerShell:
```cmd
.\packetraven_env\Scripts\activate.ps1
```
3. install `packetraven`
```bash
pip install packetraven
```

# Usage

## Command-line Options
Expand Down
55 changes: 54 additions & 1 deletion packetraven/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
TimeIntervalError,
)
from packetraven.connections.base import PacketSource
from packetraven.connections.internet import RockBLOCK
from packetraven.packets import APRSPacket
from packetraven.packets.tracks import APRSTrack, LocationPacketTrack
from packetraven.packets.writer import write_packet_tracks
Expand All @@ -44,6 +45,10 @@ def main():
help='comma-separated list of serial ports / text files of a TNC parsing APRS packets from analog audio to ASCII'
' (set to `auto` to use the first open serial port)',
)
args_parser.add_argument(
'--rockblock', help='listen to incoming POST requests (defaults to `localhost:80`)',
)
args_parser.add_argument('--rockblock-imei', help='IMEI of RockBLOCK modem')
args_parser.add_argument(
'--database', help='PostGres database table `user@hostname:port/database/table`'
)
Expand Down Expand Up @@ -110,6 +115,28 @@ def main():
if args.tnc is not None:
kwargs['tnc'] = [tnc.strip() for tnc in args.tnc.split(',')]

if args.rockblock is not None:
try:
if Path(args.rockblock).exists():
kwargs['rockblock_csv'] = args.rockblock
else:
raise FileNotFoundError
except FileNotFoundError:
listen_hostname = parse_hostname(args.rockblock)

hostname = listen_hostname['hostname']
port = listen_hostname['port']
username = listen_hostname['username']
password = listen_hostname['password']

if port is not None:
hostname = f'{hostname}:{port}'

kwargs['rockblock_hostname'] = hostname
kwargs['rockblock_imei'] = args.rockblock_imei
kwargs['rockblock_username'] = username
kwargs['rockblock_password'] = password

if args.database is not None:
database = parse_hostname(args.database)

Expand Down Expand Up @@ -247,7 +274,9 @@ def main():
elif start_date is None and end_date is not None:
filter_message += f' sent before {end_date:%Y-%m-%d %H:%M:%S}'
elif start_date is not None and end_date is not None:
filter_message += f' sent between {start_date:%Y-%m-%d %H:%M:%S} and {end_date:%Y-%m-%d %H:%M:%S}'
filter_message += (
f' sent between {start_date:%Y-%m-%d %H:%M:%S} and {end_date:%Y-%m-%d %H:%M:%S}'
)
if callsigns is not None:
filter_message += f' from {len(callsigns)} callsigns: {callsigns}'
LOGGER.info(filter_message)
Expand Down Expand Up @@ -295,6 +324,30 @@ def main():
except ConnectionError as error:
LOGGER.warning(f'{error.__class__.__name__} - {error}')

if 'rockblock_hostname' in kwargs:
rockblock_kwargs = {
key: kwargs[key]
for key in [
'rockblock_hostname',
'rockblock_imei',
'rockblock_username',
'rockblock_password',
]
if key in kwargs
}

try:
rockblock = RockBLOCK(
imei=None,
username=rockblock_kwargs['rockblock_username'],
password=rockblock_kwargs['rockblock_password'],
)
rockblock.start_listening(rockblock_kwargs['rockblock_hostname'])
LOGGER.info(f'connected to {rockblock.location}')
connections.append(rockblock)
except ConnectionError:
pass

if 'database_hostname' in kwargs:
database_kwargs = {
key: kwargs[key]
Expand Down
219 changes: 141 additions & 78 deletions packetraven/connections/file.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from datetime import datetime
from io import BytesIO, StringIO
from os import PathLike
from pathlib import Path
from typing import Union
from urllib.parse import urlparse

from dateutil.parser import parse as parse_date
import geojson
import pandas
import requests

from packetraven.connections.base import (
Expand All @@ -14,9 +17,49 @@
TimeIntervalError,
)
from packetraven.packets import APRSPacket, LocationPacket
from packetraven.packets.base import IridiumPacket


class RawAPRSTextFile(APRSPacketSource):
class WatchedFile:
def __init__(self, file: Union[PathLike, StringIO, BytesIO]):
if not isinstance(file, (StringIO, BytesIO)):
if not urlparse(str(file)).scheme in ['http', 'https', 'ftp', 'sftp']:
if not isinstance(file, Path):
if isinstance(file, str):
file = file.strip('"')
file = Path(file)
file = str(file)

self.file = file
self.__parsed_lines = None

def new_lines(self) -> [str]:
new_lines = []

if Path(self.file).exists():
file_connection = open(Path(self.file).expanduser().resolve())
lines = file_connection.readlines()
else:
file_connection = requests.get(self.file, stream=True)
lines = file_connection.iter_lines()

for line in lines:
if len(line) > 0:
if isinstance(line, bytes):
line = line.decode()
if line not in self.__parsed_lines:
self.__parsed_lines.append(line)
new_lines.append(line)

file_connection.close()

return new_lines

def close(self):
pass


class RawAPRSTextFile(APRSPacketSource, WatchedFile):
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 All @@ -26,14 +69,9 @@ def __init__(self, filename: PathLike = None, callsigns: str = None):
:param callsigns: list of callsigns to return from source
"""

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)
WatchedFile.__init__(self, filename)
APRSPacketSource.__init__(self, self.file, callsigns)

super().__init__(filename, callsigns)
self.__last_access_time = None
self.__parsed_lines = []

Expand All @@ -46,35 +84,23 @@ def packets(self) -> [APRSPacket]:
f'interval {interval} less than minimum interval {self.interval}'
)

if Path(self.location).exists():
file_connection = open(Path(self.location).expanduser().resolve())
lines = file_connection.readlines()
else:
file_connection = requests.get(self.location, stream=True)
lines = file_connection.iter_lines()
new_lines = self.new_lines()

packets = []
for line in lines:
if len(line) > 0:
if isinstance(line, bytes):
line = line.decode()
if line not in self.__parsed_lines:
self.__parsed_lines.append(line)
try:
packet_time, raw_aprs = line.split(': ', 1)
packet_time = parse_date(packet_time)
except:
raw_aprs = line
packet_time = datetime.now()
raw_aprs = raw_aprs.strip()
try:
packets.append(
APRSPacket.from_frame(raw_aprs, packet_time, source=self.location)
)
except Exception as error:
LOGGER.error(f'{error.__class__.__name__} - {error}')

file_connection.close()
for line in new_lines:
try:
packet_time, raw_aprs = line.split(': ', 1)
packet_time = parse_date(packet_time)
except:
raw_aprs = line
packet_time = datetime.now()
raw_aprs = raw_aprs.strip()
try:
packets.append(
APRSPacket.from_frame(raw_aprs, packet_time, source=self.location)
)
except Exception as error:
LOGGER.error(f'{error.__class__.__name__} - {error}')

if self.callsigns is not None:
packets = [packet for packet in packets if packet.from_callsign in self.callsigns]
Expand All @@ -89,22 +115,62 @@ def __repr__(self):
return f'{self.__class__.__name__}({repr(self.location)}, {repr(self.callsigns)})'


class PacketGeoJSON(PacketSource):
class RockBLOCKtoolsCSV(PacketSource, WatchedFile):
def __init__(self, csv_filename: PathLike = None):
"""
watch a CSV file being written to by a listening instance of `rockblock-tools`
```
rockblock listen csv localhost 80
```

:param csv_filename: path to CSV file
"""

if not isinstance(csv_filename, Path):
csv_filename = Path(csv_filename)

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

@property
def packets(self) -> [LocationPacket]:
new_lines = self.new_lines()

packets = []
for line in new_lines:
line = [entry.strip() for entry in line.split(',')]
packet = IridiumPacket(
time=line[3],
x=float(line[1]),
y=float(line[0]),
device_type=line[2],
momsn=line[4],
imei=line[5],
serial=line[6],
data=line[7],
cep=line[8],
)
packets.append(packet)

self.__last_access_time = datetime.now()

return packets

def close(self):
pass


class PacketGeoJSON(PacketSource, WatchedFile):
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)
WatchedFile.__init__(self, filename)
PacketSource.__init__(self, self.file)

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

@property
Expand All @@ -116,42 +182,39 @@ def packets(self) -> [LocationPacket]:
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)
new_lines = self.new_lines()
if len(new_lines) > 0:
features = geojson.loads(new_lines)

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()

Expand Down
Loading