diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b948985 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.swp +*.pyc diff --git a/aprs/IS.py b/aprs/IS.py new file mode 100644 index 0000000..3d61ca9 --- /dev/null +++ b/aprs/IS.py @@ -0,0 +1,271 @@ +""" +IS class is used for connection to APRS-IS network +""" +import socket +import time +import logging +import sys + +from .version import __version__ +from .parse import parse +from .exceptions import ( + GenericError, + ConnectionDrop, + ConnectionError, + LoginError, + ) + +__all__ = ['IS'] + + +class IS(object): + """ + The IS class is used to connect to aprs-is network and listen to the stream + of packets. You can either run them through aprs.parse() or get them in raw + form. + + Note: sending of packets is not supported yet + + """ + def __init__(self, callsign, host="rotate.aprs.net", port=14580, passwd="-1"): + """ + Host & port - aprs-is server + callsign - used when login in + passwd - for verification, or "-1" if only listening + """ + + self.logger = logging.getLogger(__name__) + + self.set_server(host, port) + self.set_login(callsign, passwd) + + self.sock = None + self.filter = "t/poimqstunw" # default filter, everything + + self._connected = False + self.buf = '' + + def callsign_filter(self, callsigns): + """ + Sets a filter for the specified callsigns. + Only those will be sent to us by the server + """ + + if type(callsigns) is not list or len(callsigns) == 0: + return False + + return self.set_filter("b/%s" % "/".join(callsigns)) + + def set_filter(self, filter_text): + """ + Set a specified aprs-is filter for this connection + """ + self.filter = filter_text + + self.logger.info("Setting filter to: %s", self.filter) + + if self._connected: + self.sock.sendall("#filter %s\r\n" % self.filter) + + return True + + def set_login(self, callsign, passwd): + """ + Set callsign and password + """ + self.callsign = callsign + self.passwd = passwd + + def set_server(self, host, port): + """ + Set server ip/host and port to use + """ + self.server = (host, port) + + def connect(self, blocking=False): + """ + Initiate connection to APRS server and attempt to login + """ + + if not self._connected: + while True: + try: + self.logger.info("Attempting connection to %s:%s", self.server[0], self.server[1]) + self._connect() + + self.logger.info("Sending login information") + self._send_login() + + self.logger.info("Filter set to: %s", self.filter) + + if self.passwd == "-1": + self.logger.info("Login successful (receive only)") + else: + self.logger.info("Login successful") + + break + except: + if not blocking: + raise + + time.sleep(30) # attempt to reconnect after 30 seconds + + def close(self): + """ + Closes the socket + Called internally when Exceptions are raised + """ + + self._connected = False + self.buf = '' + + if self.sock is not None: + self.sock.close() + + def consumer(self, callback, blocking=True, immortal=False, raw=False): + """ + When a position sentence is received, it will be passed to the callback function + + blocking: if true (default), runs forever, otherwise will return after one sentence + You can still exit the loop, by raising StopIteration in the callback function + + immortal: When true, consumer will try to reconnect and stop propagation of Parse exceptions + if false (default), consumer will return + + raw: when true, raw packet is passed to callback, otherwise the result from aprs.parse() + """ + + if not self._connected: + raise ConnectionError("not connected to a server") + + while True: + try: + for line in self._socket_readlines(blocking): + if line[0] != "#": + if raw: + callback(line) + else: + callback(parse(line)) + except KeyboardInterrupt: + raise + except (ConnectionDrop, ConnectionError): + self.close() + + if not immortal: + raise + else: + self.connect(blocking=blocking) + continue + except GenericError: + continue + except StopIteration: + break + except: + self.logger.error("APRS Packet: %s", line) + raise + + if not blocking: + break + + def _connect(self): + """ + Attemps to open a connection to the server + """ + + try: + # 15 seconds connection timeout + self.sock = socket.create_connection(self.server, 15) + + # 5 second timeout to receive server banner + self.sock.setblocking(1) + self.sock.settimeout(5) + + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + + if sys.platform not in ['cygwin', 'win32']: + self.sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE, 15) + self.sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT, 3) + self.sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL, 5) + + if self.sock.recv(512)[0] != "#": + raise ConnectionError("invalid banner from server") + + except Exception, e: + self.close() + + if e == "timed out": + raise ConnectionError("no banner from server") + else: + raise ConnectionError(e) + + self._connected = True + + def _send_login(self): + """ + Sends login string to server + """ + login_str = "user {0} pass {1} vers pyaprs {3} filter {2}\r\n" + login_str = login_str.format( + self.callsign, + self.passwd, + self.filter, + __version__ + ) + + try: + self.sock.sendall(login_str) + self.sock.settimeout(5) + test = self.sock.recv(len(login_str) + 100) + + (x, x, callsign, status, x) = test.split(' ', 4) + + if callsign == "": + raise LoginError("No callsign provided") + if callsign != self.callsign: + raise LoginError("Server: %s" % test[2:]) + if status != "verified," and self.passwd != "-1": + raise LoginError("Password is incorrect") + + except LoginError, e: + self.close() + raise LoginError("failed to login: %s" % e) + except: + self.close() + raise LoginError("failed to login") + + def _socket_readlines(self, blocking=False): + """ + Generator for complete lines, received from the server + """ + try: + self.sock.setblocking(0) + except socket.error, e: + raise ConnectionDrop("connection dropped") + + while True: + short_buf = '' + + try: + short_buf = self.sock.recv(1024) + + # sock.recv returns empty if the connection drops + if not short_buf: + raise ConnectionDrop("connection dropped") + except socket.error, e: + if "Resource temporarily unavailable" in e: + if not blocking: + if len(self.buf) == 0: + break + except Exception: + raise + + self.buf += short_buf + + while "\r\n" in self.buf: + line, self.buf = self.buf.split("\r\n", 1) + + yield line + + # lets not hog the CPU when there's nothing to do + if blocking: + time.sleep(0.1) diff --git a/aprs/__init__.py b/aprs/__init__.py new file mode 100644 index 0000000..3676be2 --- /dev/null +++ b/aprs/__init__.py @@ -0,0 +1,25 @@ + +""" +APRS library in Python + +Currently the library provides facilities to: + - parse APRS packets + - Connect and listen to an aprs-is packet feed + +Copyright 2013-2014 (C), Rossen Georgiev +""" + +from datetime import date as _date +__date__ = str(_date.today()) +del _date + +import version +__version__ = version.__version__ +del version + +__author__ = "Rossen Georgiev" + +from .IS import IS +from .parse import parse + +__all__ = ['IS', 'parse'] diff --git a/aprs/exceptions.py b/aprs/exceptions.py new file mode 100644 index 0000000..4824951 --- /dev/null +++ b/aprs/exceptions.py @@ -0,0 +1,73 @@ +""" +Contains exception definitions for the module +""" +import logging + +__all__ = [ + "GenericError", + "UnknownFormat", + "ParseError", + "LoginError", + "ConnectionError", + "ConnectionDrop", + ] + +logger = logging.getLogger(__name__) +logging.raiseExceptions = False +logging.addLevelName(11, "ParseError") + + +class GenericError(Exception): + """ + Base exception class for the library. Logs information via logging module + """ + def __init__(self, message): + logger.debug("%s: %s", self.__class__.__name__, message) + self.message = message + + def __str__(self): + return self.message + + +class UnknownFormat(GenericError): + """ + Raised when aprs.parse() encounters an unsupported packet format + + """ + def __init__(self, message, packet=''): + logger.log(9, "%s\nPacket: %s", message, packet) + self.message = message + self.packet = packet + + +class ParseError(GenericError): + """ + Raised when unexpected format of a supported packet format is encountered + """ + def __init__(self, message, packet=''): + logger.log(11, "%s\nPacket: %s", message, packet) + self.message = message + self.packet = packet + + +class LoginError(GenericError): + """ + Raised when IS servers didn't respond correctly to our loging attempt + """ + def __init__(self, message): + logger.error("%s: %s", self.__class__.__name__, message) + self.message = message + + +class ConnectionError(GenericError): + """ + Riased when connection dies for some reason + """ + pass + + +class ConnectionDrop(ConnectionError): + """ + Raised when connetion drops or detected to be dead + """ + pass diff --git a/aprs.py b/aprs/parse.py similarity index 70% rename from aprs.py rename to aprs/parse.py index 0d0f442..9c78026 100644 --- a/aprs.py +++ b/aprs/parse.py @@ -1,282 +1,14 @@ -""" -APRS library in Python - -Currently the library provides facilities to: - - parse APRS packets - - Connect and listen to an aprs-is packet feed - -Copyright 2013-2014 (C), Rossen Georgiev -""" - -import socket import time -import datetime import re import math import logging -import sys +from datetime import datetime -__version__ = "0.5.1" -__author__ = "Rossen Georgiev" -__date__ = str(datetime.date.today()) +from .exceptions import * -__all__ = ['IS', 'base91', 'parse'] +__all__ = ['parse'] logger = logging.getLogger(__name__) -logging.addLevelName(11, "ParseError") - - -class IS(object): - """ - The IS class is used to connect to aprs-is network and listen to the stream - of packets. You can either run them through aprs.parse() or get them in raw - form. - - Note: sending of packets is not supported yet - - """ - def __init__(self, host, port, callsign, passwd): - """ - Host & port - aprs-is server - callsign - used when login in - passwd - for verification, or "-1" if only listening - """ - - self.set_server(host, port) - self.set_login(callsign, passwd) - - self.sock = None - self.filter = "b/" # empty bud filter - - self._connected = False - self.buf = '' - - def callsign_filter(self, callsigns): - """ - Sets a filter for the specified callsigns. - Only those will be sent to us by the server - """ - - if type(callsigns) is not list or len(callsigns) == 0: - return False - - return self.set_filter("b/%s" % "/".join(callsigns)) - - def set_filter(self, filter_text): - """ - Set a specified aprs-is filter for this connection - """ - self.filter = filter_text - - logger.info("Setting filter to: %s", self.filter) - - if self._connected: - self.sock.sendall("#filter %s\r\n" % self.filter) - - return True - - def set_login(self, callsign, passwd): - """ - Set callsign and password - """ - self.callsign = callsign - self.passwd = passwd - - def set_server(self, host, port): - """ - Set server ip/host and port to use - """ - self.server = (host, port) - - def connect(self, blocking=False): - """ - Initiate connection to APRS server and attempt to login - """ - - if not self._connected: - while True: - try: - logger.info("Attempting connection to %s:%s", self.server[0], self.server[1]) - self._connect() - - logger.info("Sending login information") - self._send_login() - - logger.info("Filter set to: %s", self.filter) - - if self.passwd == "-1": - logger.info("Login successful (receive only)") - else: - logger.info("Login successful") - - break - except: - if not blocking: - raise - - time.sleep(30) # attempt to reconnect after 30 seconds - - def close(self): - """ - Closes the socket - Called internally when Exceptions are raised - """ - - self._connected = False - self.buf = '' - - if self.sock is not None: - self.sock.close() - - def consumer(self, callback, blocking=True, immortal=False, raw=False): - """ - When a position sentence is received, it will be passed to the callback function - - blocking: if true (default), runs forever, otherwise will return after one sentence - You can still exit the loop, by raising StopIteration in the callback function - - immortal: When true, consumer will try to reconnect and stop propagation of Parse exceptions. - if false (default), consumer will return - - raw: when true, raw packet is passed to callback, otherwise the result from aprs.parse() - """ - - if not self._connected: - raise ConnectionError("not connected to a server") - - while True: - try: - for line in self._socket_readlines(blocking): - if line[0] != "#": - if raw: - callback(line) - else: - callback(parse(line)) - except KeyboardInterrupt: - raise - except (ConnectionDrop, ConnectionError): - self.close() - - if not immortal: - raise - else: - self.connect(blocking=blocking) - continue - except GenericError: - continue - except StopIteration: - break - except: - logger.error("APRS Packet: %s", line) - raise - - if not blocking: - break - - def _connect(self): - """ - Attemps to open a connection to the server - """ - - try: - # 15 seconds connection timeout - self.sock = socket.create_connection(self.server, 15) - - # 5 second timeout to receive server banner - self.sock.settimeout(5) - self.sock.setblocking(True) - - self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - - if sys.platform not in ['cygwin', 'win32']: - self.sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE, 15) - self.sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT, 3) - self.sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL, 5) - - if self.sock.recv(512)[0] != "#": - raise ConnectionError("invalid banner from server") - - except Exception, e: - self.close() - - if e == "timed out": - raise ConnectionError("no banner from server") - else: - raise ConnectionError(e) - - self._connected = True - - def _send_login(self): - """ - Sends login string to server - """ - login_str = "user {0} pass {1} vers pytinyaprs 0.2 filter {2}\r\n".format( - self.callsign, - self.passwd, - self.filter - ) - - try: - self.sock.sendall(login_str) - self.sock.settimeout(5) - test = self.sock.recv(len(login_str) + 100) - self.sock.setblocking(True) - - (x, x, callsign, status, x) = test.split(' ', 4) - - if callsign != self.callsign: - raise LoginError("login callsign does not match") - if status != "verified," and self.passwd != "-1": - raise LoginError("callsign is not 'verified'") - - except LoginError, e: - self.close() - raise LoginError("failed to login: %s" % e) - except: - self.close() - raise LoginError("failed to login") - - def _socket_readlines(self, blocking=False): - """ - Generator for complete lines, received from the server - """ - try: - self.sock.setblocking(False) - except socket.error, e: - raise ConnectionDrop("connection dropped") - - while True: - short_buf = '' - - try: - short_buf = self.sock.recv(1024) - - # sock.recv returns empty if the connection drops - if not short_buf: - raise ConnectionDrop("connection dropped") - except socket.error, e: - if "Resource temporarily unavailable" in e: - if not blocking: - if len(self.buf) == 0: - break - except Exception: - raise - - self.buf += short_buf - - while "\r\n" in self.buf: - line, self.buf = self.buf.split("\r\n", 1) - - yield line - - if not blocking: - raise StopIteration - - # in blocking mode this will fast if there is no data - # so we should sleep and not hog the CPU - if blocking: - time.sleep(0.5) - # Mic-e message type table @@ -388,7 +120,7 @@ def parse(raw_sentence): match = re.findall(r"^((\d{6})(.))$", body[0:7]) if match: rawts, ts, form = match[0] - utc = datetime.datetime.utcnow() + utc = datetime.utcnow() if packet_type == '>' and form != 'z': raise ParseError("Time format for status reports should be zulu", raw_sentence) @@ -622,7 +354,7 @@ def parse(raw_sentence): logger.debug("Packet is just a status message") parsed.update({ 'format': 'status', - 'comment': body + 'status': body }) # MESSAGE PACKET @@ -952,60 +684,3 @@ def _parse_comment_telemetry(text): return [text, parsed] else: return [text, {}] - - -# Exceptions -class GenericError(Exception): - """ - Base exception class for the library. Logs information via logging module - """ - def __init__(self, message): - logger.debug("%s: %s", self.__class__.__name__, message) - self.message = message - - def __str__(self): - return self.message - - -class UnknownFormat(GenericError): - """ - Raised when aprs.parse() encounters an unsupported packet format - - """ - def __init__(self, message, packet=''): - logger.log(9, "%s\nPacket: %s" % (message, packet)) - self.message = message - self.packet = packet - - -class ParseError(GenericError): - """ - Raised when aprs.parse() encounters unexpected formating of a supported packet format - """ - def __init__(self, message, packet=''): - logger.log(11, "%s\nPacket: %s" % (message, packet)) - self.message = message - self.packet = packet - - -class LoginError(GenericError): - """ - Raised when IS servers didn't respond correctly to our loging attempt - """ - def __init__(self, message): - logger.error("%s: %s", self.__class__.__name__, message) - self.message = message - - -class ConnectionError(GenericError): - """ - Riased when connection dies for some reason - """ - pass - - -class ConnectionDrop(ConnectionError): - """ - Raised when connetion drops or detected to be dead - """ - pass diff --git a/aprs/version.py b/aprs/version.py new file mode 100644 index 0000000..906d362 --- /dev/null +++ b/aprs/version.py @@ -0,0 +1 @@ +__version__ = "0.6.0"