diff --git a/README.md b/README.md index 2a55970..5ef6572 100644 --- a/README.md +++ b/README.md @@ -7,60 +7,16 @@ [![PyPi Version](https://img.shields.io/pypi/v/ogn-python.svg)] (https://pypi.python.org/pypi/ogn-python) -A python module for the [Open Glider Network](http://wiki.glidernet.org/). -The submodule 'ogn.gateway' is an aprs client which could be invoked via a CLI -or used by other python projects. -The CLI allows to save all received beacons into a database with [SQLAlchemy](http://www.sqlalchemy.org/). -The [sqlite](https://www.sqlite.org/)-backend is sufficient for simple testing, -but some tasks (e.g. logbook generation) require a proper backend like [postgresql](http://www.postgresql.org/). -An external python project would instantiate ogn.gateway and register a custom callback, -called each time a beacon is received. +A database backend for the [Open Glider Network](http://wiki.glidernet.org/). +The ogn-python module saves all received beacons into a database with [SQLAlchemy](http://www.sqlalchemy.org/). +It connects to the OGN aprs servers with [python-ogn-client](https://github.com/glidernet/python-ogn-client). +For simple tests a [sqlite](https://www.sqlite.org/)-backend is sufficient, +but some tasks (e.g. logbook generation) require a proper database backend like [postgresql](http://www.postgresql.org/). [Examples](https://github.com/glidernet/ogn-python/wiki/Examples) -## Usage - python module -Implement your own gateway by using ogn.gateway with a custom callback function. -Each time a beacon is received, this function gets called and -lets you process the incoming data. - -Example: -```python -#!/usr/bin/env python3 -from ogn.gateway.client import ognGateway -from ogn.parser.parse import parse_aprs, parse_ogn_beacon -from ogn.parser.exceptions import ParseError - - -def process_beacon(raw_message): - if raw_message[0] == '#': - print('Server Status: {}'.format(raw_message)) - return - - try: - message = parse_aprs(raw_message) - message.update(parse_ogn_beacon(message['comment'])) - - print('Received {beacon_type} from {name}'.format(**message)) - except ParseError as e: - print('Error, {}'.format(e.message)) - - -if __name__ == '__main__': - gateway = ognGateway(aprs_user='N0CALL') - gateway.connect() - - try: - gateway.run(callback=process_beacon, autoreconnect=True) - except KeyboardInterrupt: - print('\nStop ogn gateway') - - gateway.disconnect() -``` - - -## Usage - CLI -### Installation and Setup +## Installation and Setup 1. Checkout the repository ``` @@ -85,6 +41,7 @@ if __name__ == '__main__': ./manage.py db.init ``` +## Usage ### Running the aprs client and task server To schedule tasks like takeoff/landing-detection (`logbook.compute`), [Celery](http://www.celeryproject.org/) with [Redis](http://www.redis.io/) is used. diff --git a/ogn/__init__.py b/ogn/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/ogn/gateway/client.py b/ogn/gateway/client.py deleted file mode 100644 index 69ab8ce..0000000 --- a/ogn/gateway/client.py +++ /dev/null @@ -1,76 +0,0 @@ -import socket -import logging -from time import time - -from ogn.gateway import settings - - -def create_aprs_login(user_name, pass_code, app_name, app_version, aprs_filter=None): - if not aprs_filter: - return "user {} pass {} vers {} {}\n".format(user_name, pass_code, app_name, app_version) - else: - return "user {} pass {} vers {} {} filter {}\n".format(user_name, pass_code, app_name, app_version, aprs_filter) - - -class ognGateway: - def __init__(self, aprs_user, aprs_filter=''): - self.logger = logging.getLogger(__name__) - self.logger.info("Connect to OGN as {} with filter '{}'".format(aprs_user, (aprs_filter if aprs_filter else 'full-feed'))) - self.aprs_user = aprs_user - self.aprs_filter = aprs_filter - - def connect(self): - # create socket, connect to server, login and make a file object associated with the socket - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - - if self.aprs_filter: - port = settings.APRS_SERVER_PORT_CLIENT_DEFINED_FILTERS - else: - port = settings.APRS_SERVER_PORT_FULL_FEED - - self.sock.connect((settings.APRS_SERVER_HOST, port)) - self.logger.debug('Server port {}'.format(port)) - - login = create_aprs_login(self.aprs_user, -1, settings.APRS_APP_NAME, settings.APRS_APP_VER, self.aprs_filter) - self.sock.send(login.encode()) - self.sock_file = self.sock.makefile('rw') - - def disconnect(self): - self.logger.info('Disconnect') - try: - # close everything - self.sock.shutdown(0) - self.sock.close() - except OSError: - self.logger.error('Socket close error', exc_info=True) - - def run(self, callback, autoreconnect=False): - while True: - try: - keepalive_time = time() - while True: - if time() - keepalive_time > settings.APRS_KEEPALIVE_TIME: - self.logger.info('Send keepalive') - self.sock.send('#keepalive'.encode()) - keepalive_time = time() - - # Read packet string from socket - packet_str = self.sock_file.readline().strip() - - # A zero length line should not be return if keepalives are being sent - # A zero length line will only be returned after ~30m if keepalives are not sent - if len(packet_str) == 0: - self.logger.warning('Read returns zero length string. Failure. Orderly closeout') - break - - callback(packet_str) - except BrokenPipeError: - self.logger.error('BrokenPipeError', exc_info=True) - except socket.error: - self.logger.error('socket.error', exc_info=True) - - if autoreconnect: - self.connect() - else: - return diff --git a/ogn/gateway/manage.py b/ogn/gateway/manage.py index e72a00c..50f0a07 100644 --- a/ogn/gateway/manage.py +++ b/ogn/gateway/manage.py @@ -1,6 +1,6 @@ import logging -from ogn.gateway.client import ognGateway +from ogn.client import AprsClient from ogn.gateway.process import process_beacon from manager import Manager @@ -30,13 +30,13 @@ def run(aprs_user='anon-dev', logfile='main.log', loglevel='INFO'): logging.basicConfig(format=logging_formatstr, level=loglevel, handlers=log_handlers) print('Start ogn gateway') - gateway = ognGateway(aprs_user) - gateway.connect() + client = AprsClient(aprs_user) + client.connect() try: - gateway.run(callback=process_beacon, autoreconnect=True) + client.run(callback=process_beacon, autoreconnect=True) except KeyboardInterrupt: print('\nStop ogn gateway') - gateway.disconnect() + client.disconnect() logging.shutdown() diff --git a/ogn/gateway/process.py b/ogn/gateway/process.py index 145cc7e..690b3b9 100644 --- a/ogn/gateway/process.py +++ b/ogn/gateway/process.py @@ -1,8 +1,7 @@ import logging from ogn.commands.dbutils import session from ogn.model import AircraftBeacon, ReceiverBeacon -from ogn.parser.parse import parse_aprs, parse_ogn_receiver_beacon, parse_ogn_aircraft_beacon -from ogn.parser.exceptions import AprsParseError, OgnParseError, AmbigousTimeError +from ogn.parser import parse_aprs, parse_ogn_receiver_beacon, parse_ogn_aircraft_beacon, ParseError logger = logging.getLogger(__name__) @@ -33,6 +32,6 @@ def process_beacon(raw_message): session.add(beacon) session.commit() logger.debug('Received message: {}'.format(raw_message)) - except (AprsParseError, OgnParseError, AmbigousTimeError) as e: + except ParseError as e: logger.error('Received message: {}'.format(raw_message)) logger.error('Drop packet, {}'.format(e.message)) diff --git a/ogn/gateway/settings.py b/ogn/gateway/settings.py deleted file mode 100644 index 50b72ca..0000000 --- a/ogn/gateway/settings.py +++ /dev/null @@ -1,10 +0,0 @@ -APRS_SERVER_HOST = 'aprs.glidernet.org' -APRS_SERVER_PORT_FULL_FEED = 10152 -APRS_SERVER_PORT_CLIENT_DEFINED_FILTERS = 14580 - -APRS_APP_NAME = 'ogn-gateway-python' - -PACKAGE_VERSION = '0.2.1' -APRS_APP_VER = PACKAGE_VERSION[:3] - -APRS_KEEPALIVE_TIME = 240 diff --git a/ogn/parser/__init__.py b/ogn/parser/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/ogn/parser/exceptions.py b/ogn/parser/exceptions.py deleted file mode 100644 index e388e88..0000000 --- a/ogn/parser/exceptions.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -exception definitions -""" -from datetime import datetime - - -class ParseError(Exception): - pass - - -class AprsParseError(ParseError): - """Parse error while parsing an aprs packet.""" - def __init__(self, aprs_string): - self.aprs_string = aprs_string - - self.message = "This is not a valid APRS packet: {}".format(aprs_string) - super(AprsParseError, self).__init__(self.message) - - -class OgnParseError(ParseError): - """Parse error while parsing an ogn message from aprs comment.""" - def __init__(self, aprs_comment): - self.aprs_comment = aprs_comment - - self.message = "This is not a valid OGN message: {}".format(aprs_comment) - super(OgnParseError, self).__init__(self.message) - - -class AmbigousTimeError(ParseError): - """Timstamp from the past/future, can't fully reconstruct datetime from timestamp.""" - def __init__(self, reference, packet_time): - self.reference = reference - self.packet_time = packet_time - self.timedelta = reference - datetime.combine(reference, packet_time) - - self.message = "Can't reconstruct timstamp, {:.0f}s from past.".format(self.timedelta.total_seconds()) - super(AmbigousTimeError, self).__init__(self.message) diff --git a/ogn/parser/parse.py b/ogn/parser/parse.py deleted file mode 100644 index 33c82b3..0000000 --- a/ogn/parser/parse.py +++ /dev/null @@ -1,84 +0,0 @@ -import re -from datetime import datetime - -from ogn.parser.utils import createTimestamp, dmsToDeg, kts2kmh, feet2m, fpm2ms -from ogn.parser.pattern import PATTERN_APRS, PATTERN_RECEIVER_BEACON, PATTERN_AIRCRAFT_BEACON -from ogn.parser.exceptions import AprsParseError, OgnParseError - - -def parse_aprs(message, reference_date=None): - if reference_date is None: - reference_date = datetime.utcnow() - - match = re.search(PATTERN_APRS, message) - if match: - return {'name': match.group('callsign'), - 'receiver_name': match.group('receiver'), - 'timestamp': createTimestamp(match.group('time'), reference_date), - 'latitude': dmsToDeg(float(match.group('latitude')) / 100) * - (-1 if match.group('latitude_sign') == 'S' else 1) + - (int(match.group('latitude_enhancement')) / 1000 / 60 if match.group('latitude_enhancement') else 0), - 'symboltable': match.group('symbol_table'), - 'longitude': dmsToDeg(float(match.group('longitude')) / 100) * - (-1 if match.group('longitude_sign') == 'W' else 1) + - (int(match.group('longitude_enhancement')) / 1000 / 60 if match.group('longitude_enhancement') else 0), - 'symbolcode': match.group('symbol'), - 'track': int(match.group('course')) if match.group('course_extension') else 0, - 'ground_speed': int(match.group('ground_speed')) * kts2kmh if match.group('ground_speed') else 0, - 'altitude': int(match.group('altitude')) * feet2m, - 'comment': match.group('comment')} - - raise AprsParseError(message) - - -def parse_ogn_aircraft_beacon(aprs_comment): - ac_match = re.search(PATTERN_AIRCRAFT_BEACON, aprs_comment) - if ac_match: - return {'address_type': int(ac_match.group('details'), 16) & 0b00000011, - 'aircraft_type': (int(ac_match.group('details'), 16) & 0b01111100) >> 2, - 'stealth': (int(ac_match.group('details'), 16) & 0b10000000) >> 7 == 1, - 'address': ac_match.group('id'), - 'climb_rate': int(ac_match.group('climb_rate')) * fpm2ms, - 'turn_rate': float(ac_match.group('turn_rate')), - 'flightlevel': float(ac_match.group('flight_level')) if ac_match.group('flight_level') else None, - 'signal_strength': float(ac_match.group('signal')), - 'error_count': float(ac_match.group('errors')), - 'frequency_offset': float(ac_match.group('frequency_offset')), - 'gps_status': ac_match.group('gps_accuracy'), - 'software_version': float(ac_match.group('flarm_software_version')) if ac_match.group('flarm_software_version') else None, - 'hardware_version': int(ac_match.group('flarm_hardware_version'), 16) if ac_match.group('flarm_hardware_version') else None, - 'real_address': ac_match.group('flarm_id')} - else: - return None - - -def parse_ogn_receiver_beacon(aprs_comment): - rec_match = re.search(PATTERN_RECEIVER_BEACON, aprs_comment) - if rec_match: - return {'version': rec_match.group('version'), - 'platform': rec_match.group('platform'), - 'cpu_load': float(rec_match.group('cpu_load')), - 'free_ram': float(rec_match.group('ram_free')), - 'total_ram': float(rec_match.group('ram_total')), - 'ntp_error': float(rec_match.group('ntp_offset')), - 'rt_crystal_correction': float(rec_match.group('ntp_correction')), - 'cpu_temp': float(rec_match.group('cpu_temperature')) if rec_match.group('cpu_temperature') else None, - 'rec_crystal_correction': int(rec_match.group('manual_correction')) if rec_match.group('manual_correction') else 0, - 'rec_crystal_correction_fine': float(rec_match.group('automatic_correction')) if rec_match.group('automatic_correction') else 0.0, - 'rec_input_noise': float(rec_match.group('input_noise')) if rec_match.group('input_noise') else None} - else: - return None - - -def parse_ogn_beacon(aprs_comment): - ac_data = parse_ogn_aircraft_beacon(aprs_comment) - if ac_data: - ac_data.update({'beacon_type': 'aircraft_beacon'}) - return ac_data - - rc_data = parse_ogn_receiver_beacon(aprs_comment) - if rc_data: - rc_data.update({'beacon_type': 'receiver_beacon'}) - return rc_data - - raise OgnParseError(aprs_comment) diff --git a/ogn/parser/pattern.py b/ogn/parser/pattern.py deleted file mode 100644 index 9cc04d2..0000000 --- a/ogn/parser/pattern.py +++ /dev/null @@ -1,63 +0,0 @@ -import re - - -PATTERN_APRS = re.compile(r"^(?P.+?)>APRS,.+,(?P.+?):/(?P