From 9b81df20e8ccfd19acdad4f040b162ecb519588e Mon Sep 17 00:00:00 2001 From: Zach Date: Tue, 29 Sep 2020 15:50:22 -0400 Subject: [PATCH] filter callsigns (#10) * remove radio skip argument * remove extra args * synchronize with database (#8) * synchronize with postgres with option for SSH tunnel * update GUI and API * fix flake issue * rename test * add filtering to connection objects --- packetraven/connections.py | 112 ++++++++++++++++++++++++------------- packetraven/database.py | 2 +- packetraven/packets.py | 9 ++- packetraven/parsing.py | 6 -- packetraven/structures.py | 7 --- packetraven/tracks.py | 4 +- packetraven/writer.py | 2 +- 7 files changed, 84 insertions(+), 58 deletions(-) diff --git a/packetraven/connections.py b/packetraven/connections.py index 814f3638..99d2e9ed 100644 --- a/packetraven/connections.py +++ b/packetraven/connections.py @@ -1,9 +1,3 @@ -""" -Read serial connection from radio and extract APRS packets. - -__authors__ = [] -""" - from abc import ABC, abstractmethod from datetime import datetime import logging @@ -17,15 +11,22 @@ from shapely.geometry import Point from client import CREDENTIALS_FILENAME -from packetraven.database import DatabaseTable -from packetraven.packets import APRSLocationPacket, LocationPacket -from packetraven.utilities import get_logger, read_configuration +from .database import DatabaseTable +from .packets import APRSLocationPacket, LocationPacket +from .utilities import get_logger, read_configuration LOGGER = get_logger('packetraven.connection') class PacketConnection(ABC): - location: str + def __init__(self, location: str): + """ + Create a new generic packet connection. + + :param location: location of packets + """ + + self.location = location @property @abstractmethod @@ -33,7 +34,7 @@ def packets(self) -> [LocationPacket]: """ List the most recent packets available from this connection. - :return: list of APRS packets + :return: list of packets """ raise NotImplementedError @@ -51,27 +52,57 @@ def close(self): raise NotImplementedError -class APRSPacketRadio(PacketConnection): - def __init__(self, serial_port: str = None): +class APRSPacketConnection(PacketConnection): + def __init__(self, location: str, callsigns: [str]): + """ + Create a new generic APRS packet connection. + + :param location: + :param callsigns: list of callsigns to return from source + """ + + super().__init__(location) + self.callsigns = callsigns + + @property + @abstractmethod + def packets(self) -> [APRSLocationPacket]: + """ + List the most recent packets available from this connection. + + :return: list of APRS packets + """ + + raise NotImplementedError + + +class APRSPacketRadio(APRSPacketConnection): + def __init__(self, serial_port: str = None, callsigns: [str] = None): """ Connect to radio over given serial port. :param serial_port: port name + :param callsigns: list of callsigns to return from source """ if serial_port is None: try: - self.location = next_available_port() + serial_port = next_available_port() except ConnectionError: raise ConnectionError('could not find radio over serial connection') else: - self.location = serial_port.strip('"') + serial_port = serial_port.strip('"') - self.connection = Serial(self.location, baudrate=9600, timeout=1) + super().__init__(serial_port, callsigns) + self.connection = Serial(serial_port, baudrate=9600, timeout=1) @property def packets(self) -> [APRSLocationPacket]: - return [parse_packet(line, source=self) for line in self.connection.readlines()] + packets = [parse_packet(line, source=self) for line in self.connection.readlines()] + if self.callsigns is not None: + return [packet for packet in packets if packet.callsign in self.callsigns] + else: + return packets def __enter__(self): return self.connection @@ -86,13 +117,14 @@ def __repr__(self): return f'{self.__class__.__name__}("{self.location}")' -class APRSPacketTextFile(PacketConnection): - def __init__(self, filename: PathLike = None): +class APRSPacketTextFile(APRSPacketConnection): + 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 (`%Y-%m-%d %H:%M:%S`) followed by the raw APRS string :param filename: path to text file + :param callsigns: list of callsigns to return from source """ if not isinstance(filename, Path): @@ -100,15 +132,17 @@ def __init__(self, filename: PathLike = None): filename = filename.strip('"') filename = Path(filename) - self.location = filename - - # open text file - self.connection = open(self.location) + super().__init__(filename, callsigns) + self.connection = open(filename) @property def packets(self) -> [APRSLocationPacket]: - return [parse_packet(line[25:].strip('\n'), datetime.strptime(line[:19], '%Y-%m-%d %H:%M:%S'), source=self) - for line in self.connection.readlines() if len(line) > 0] + packets = [parse_packet(line[25:].strip('\n'), datetime.strptime(line[:19], '%Y-%m-%d %H:%M:%S'), source=self) + for line in self.connection.readlines() if len(line) > 0] + if self.callsigns is not None: + return [packet for packet in packets if packet.callsign in self.callsigns] + else: + return packets def __enter__(self): return self.connection @@ -123,19 +157,19 @@ def __repr__(self): return f'{self.__class__.__name__}("{self.location}")' -class APRSfiConnection(PacketConnection): - location = 'https://api.aprs.fi/api/get' - +class APRSfiConnection(APRSPacketConnection): def __init__(self, callsigns: [str], api_key: str = None): """ connect to https://aprs.fi - :param callsigns: list of callsigns to watch + :param callsigns: list of callsigns to return from source :param api_key: API key for aprs.fi """ + url = 'https://api.aprs.fi/api/get' if callsigns is None: - raise ConnectionError(f'queries to {self.location} require a list of callsigns') + raise ConnectionError(f'queries to {url} require a list of callsigns') + super().__init__(url, callsigns) if api_key is None or api_key == '': configuration = read_configuration(CREDENTIALS_FILENAME) @@ -145,7 +179,6 @@ def __init__(self, callsigns: [str], api_key: str = None): else: raise ConnectionError(f'no APRS.fi API key specified') - self.callsigns = callsigns self.api_key = api_key if not self.connected: @@ -213,8 +246,8 @@ def __init__(self, hostname: str, database: str, table: str, **kwargs): if 'fields' not in kwargs: kwargs['fields'] = {} kwargs['fields'] = {**self.__default_fields, **kwargs['fields']} - super().__init__(hostname, database, table, primary_key='time', **kwargs) - self.location = f'{self.hostname}:{self.port}/{self.database}/{self.table}' + DatabaseTable.__init__(self, hostname, database, table, primary_key='time', **kwargs) + PacketConnection.__init__(self, f'{self.hostname}:{self.port}/{self.database}/{self.table}') @property def packets(self) -> [LocationPacket]: @@ -247,7 +280,7 @@ def close(self): self.tunnel.stop() -class APRSPacketDatabaseTable(PacketDatabaseTable): +class APRSPacketDatabaseTable(PacketDatabaseTable, APRSPacketConnection): __aprs_fields = { 'from' : str, 'to' : str, @@ -265,8 +298,8 @@ def __init__(self, hostname: str, database: str, table: str, callsigns: [str] = if 'fields' not in kwargs: kwargs['fields'] = self.__aprs_fields kwargs['fields'] = {f'packet_{field}': field_type for field, field_type in kwargs['fields'].items()} - super().__init__(hostname, database, table, **kwargs) - self.callsigns = callsigns + PacketDatabaseTable.__init__(self, hostname, database, table, **kwargs) + APRSPacketConnection.__init__(self, f'{self.hostname}:{self.port}/{self.database}/{self.table}', callsigns) @property def packets(self) -> [APRSLocationPacket]: @@ -292,11 +325,14 @@ def __setitem__(self, time: datetime, packet: APRSLocationPacket): 'point' : Point(*packet.coordinates), **{f'packet_{field}': value for field, value in packet.attributes.items()} } - super(super()).__setitem__(time, record) + super().__setitem__(time, record) @property def records(self) -> [{str: Any}]: - return self.records_where({'callsign': self.callsigns} if self.callsigns is not None else None) + if self.callsigns is not None: + return self.records_where({'callsign': self.callsigns}) + else: + return self.records_where(None) def insert(self, packets: [APRSLocationPacket]): records = [{ diff --git a/packetraven/database.py b/packetraven/database.py index 3f97a933..6ba15aaf 100644 --- a/packetraven/database.py +++ b/packetraven/database.py @@ -13,7 +13,7 @@ from shapely.geometry.base import BaseGeometry from sshtunnel import SSHTunnelForwarder -from packetraven.utilities import get_logger +from .utilities import get_logger LOGGER = get_logger('packetraven.connection') diff --git a/packetraven/packets.py b/packetraven/packets.py index 41b26cf4..d4438889 100644 --- a/packetraven/packets.py +++ b/packetraven/packets.py @@ -1,11 +1,14 @@ from datetime import datetime, timedelta import math -from typing import Any, Union +from typing import Any, TYPE_CHECKING, Union import numpy from pyproj import CRS, Geod, Transformer -from packetraven.parsing import parse_raw_aprs +from .parsing import parse_raw_aprs + +if TYPE_CHECKING: + from .connections import PacketConnection DEFAULT_CRS = CRS.from_epsg(4326) @@ -13,7 +16,7 @@ class LocationPacket: """ location packet encoding (x, y, z) and time """ - def __init__(self, time: datetime, x: float, y: float, z: float = None, crs: CRS = None, source=None, + def __init__(self, time: datetime, x: float, y: float, z: float = None, crs: CRS = None, source: 'PacketConnection' = None, **kwargs): self.time = time self.coordinates = numpy.array((x, y, z if z is not None else 0)) diff --git a/packetraven/parsing.py b/packetraven/parsing.py index 3949b453..233366e1 100644 --- a/packetraven/parsing.py +++ b/packetraven/parsing.py @@ -1,9 +1,3 @@ -""" -Parse a APRS packets from raw strings. - -__authors__ = ['Zachary Burnett', 'Nick Rossomando'] -""" - from typing import Union import aprslib diff --git a/packetraven/structures.py b/packetraven/structures.py index 1a3d94cb..ce62fe0e 100644 --- a/packetraven/structures.py +++ b/packetraven/structures.py @@ -1,10 +1,3 @@ -""" -Doubly linked list for generic value types. - -__authors__ = ['Zachary Burnett'] -""" - - class DoublyLinkedList: """ A linked list is a series of node objects, each with a link (object reference) to the next node in the series. diff --git a/packetraven/tracks.py b/packetraven/tracks.py index 2a9aa9a8..6b4f0f01 100644 --- a/packetraven/tracks.py +++ b/packetraven/tracks.py @@ -3,8 +3,8 @@ import numpy from pyproj import CRS, Geod -from packetraven.packets import APRSLocationPacket, DEFAULT_CRS, LocationPacket -from packetraven.structures import DoublyLinkedList +from .packets import APRSLocationPacket, DEFAULT_CRS, LocationPacket +from .structures import DoublyLinkedList class LocationPacketTrack: diff --git a/packetraven/writer.py b/packetraven/writer.py index 2150d94c..f2f0c199 100644 --- a/packetraven/writer.py +++ b/packetraven/writer.py @@ -4,7 +4,7 @@ from geojson import Point from shapely.geometry import LineString -from packetraven.tracks import APRSTrack +from .tracks import APRSTrack KML_STANDARD = '{http://www.opengis.net/kml/2.2}'