348 lines
11 KiB
Python
348 lines
11 KiB
Python
# aprslib - Python library for working with APRS
|
|
# Copyright (C) 2013-2014 Rossen Georgiev
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along
|
|
# with this program; if not, write to the Free Software Foundation, Inc.,
|
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
|
|
"""
|
|
IS class is used for connection to APRS-IS network
|
|
"""
|
|
import socket
|
|
import select
|
|
import time
|
|
import logging
|
|
|
|
from aprslib import __version__, string_type, is_py3
|
|
from aprslib.parsing import parse
|
|
from aprslib.packets.base import APRSPacket
|
|
from aprslib.exceptions import (
|
|
GenericError,
|
|
ConnectionDrop,
|
|
ConnectionError,
|
|
LoginError,
|
|
ParseError,
|
|
UnknownFormat,
|
|
)
|
|
|
|
__all__ = ['IS']
|
|
|
|
logging.addLevelName(11, "ParseError")
|
|
logging.addLevelName(9, "UnknownFormat")
|
|
|
|
|
|
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, passwd="-1", host="rotate.aprs.net", port=10152, skip_login=False):
|
|
"""
|
|
callsign - used when login in
|
|
passwd - for verification, or "-1" if only listening
|
|
Host & port - aprs-is server
|
|
"""
|
|
|
|
self.logger = logging.getLogger("%s.%s" % (__name__, self.__class__.__name__))
|
|
self._parse = parse
|
|
|
|
self.set_server(host, port)
|
|
self.set_login(callsign, passwd, skip_login)
|
|
|
|
self.sock = None
|
|
self.filter = "" # default filter, everything
|
|
|
|
self._connected = False
|
|
self.buf = b''
|
|
|
|
def _sendall(self, text):
|
|
if is_py3:
|
|
text = text.encode('utf-8')
|
|
self.sock.sendall(text)
|
|
|
|
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._sendall("#filter %s\r\n" % self.filter)
|
|
|
|
def set_login(self, callsign, passwd="-1", skip_login=False):
|
|
"""
|
|
Set callsign and password
|
|
"""
|
|
self.__dict__.update(locals())
|
|
|
|
def set_server(self, host, port):
|
|
"""
|
|
Set server ip/host and port to use
|
|
"""
|
|
self.server = (host, port)
|
|
|
|
def connect(self, blocking=False, retry=30):
|
|
"""
|
|
Initiate connection to APRS server and attempt to login
|
|
|
|
blocking = False - Should we block until connected and logged-in
|
|
retry = 30 - Retry interval in seconds
|
|
"""
|
|
|
|
if self._connected:
|
|
return
|
|
|
|
while True:
|
|
try:
|
|
self._connect()
|
|
if not self.skip_login:
|
|
self._send_login()
|
|
break
|
|
except (LoginError, ConnectionError):
|
|
if not blocking:
|
|
raise
|
|
|
|
self.logger.info("Retrying connection is %d seconds." % retry)
|
|
time.sleep(retry)
|
|
|
|
def close(self):
|
|
"""
|
|
Closes the socket
|
|
Called internally when Exceptions are raised
|
|
"""
|
|
|
|
self._connected = False
|
|
self.buf = b''
|
|
|
|
if self.sock is not None:
|
|
self.sock.close()
|
|
|
|
def sendall(self, line):
|
|
"""
|
|
Send a line, or multiple lines sperapted by '\\r\\n'
|
|
"""
|
|
if isinstance(line, APRSPacket):
|
|
line = str(line)
|
|
elif not isinstance(line, string_type):
|
|
raise TypeError("Expected line to be str or APRSPacket, got %s", type(line))
|
|
if not self._connected:
|
|
raise ConnectionError("not connected")
|
|
|
|
if line == "":
|
|
return
|
|
|
|
line = line.rstrip("\r\n") + "\r\n"
|
|
|
|
try:
|
|
self.sock.setblocking(1)
|
|
self.sock.settimeout(5)
|
|
self._sendall(line)
|
|
except socket.error as exp:
|
|
self.close()
|
|
raise ConnectionError(str(exp))
|
|
|
|
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")
|
|
|
|
line = ''
|
|
|
|
while True:
|
|
try:
|
|
for line in self._socket_readlines(blocking):
|
|
if line[0] != "#":
|
|
if raw:
|
|
callback(line)
|
|
else:
|
|
callback(self._parse(line))
|
|
else:
|
|
self.logger.debug("Server: %s", line)
|
|
except ParseError as exp:
|
|
self.logger.log(11, "%s\n Packet: %s", exp.message, exp.packet)
|
|
except UnknownFormat as exp:
|
|
self.logger.log(9, "%s\n Packet: %s", exp.message, exp.packet)
|
|
except LoginError as exp:
|
|
self.logger.error("%s: %s", exp.__class__.__name__, exp.message)
|
|
except (KeyboardInterrupt, SystemExit):
|
|
raise
|
|
except (ConnectionDrop, ConnectionError):
|
|
self.close()
|
|
|
|
if not immortal:
|
|
raise
|
|
else:
|
|
self.connect(blocking=blocking)
|
|
continue
|
|
except GenericError:
|
|
pass
|
|
except StopIteration:
|
|
break
|
|
except:
|
|
self.logger.error("APRS Packet: %s", line)
|
|
raise
|
|
|
|
if not blocking:
|
|
break
|
|
|
|
def _open_socket(self):
|
|
"""
|
|
Creates a socket
|
|
"""
|
|
self.sock = socket.create_connection(self.server, 15)
|
|
|
|
def _connect(self):
|
|
"""
|
|
Attemps connection to the server
|
|
"""
|
|
|
|
self.logger.info("Attempting connection to %s:%s", self.server[0], self.server[1])
|
|
|
|
try:
|
|
self._open_socket()
|
|
|
|
peer = self.sock.getpeername()
|
|
|
|
self.logger.info("Connected to %s", str(peer))
|
|
|
|
# 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)
|
|
|
|
banner = self.sock.recv(512)
|
|
if is_py3:
|
|
banner = banner.decode('latin-1')
|
|
|
|
if banner[0] == "#":
|
|
self.logger.debug("Banner: %s", banner.rstrip())
|
|
else:
|
|
raise ConnectionError("invalid banner from server")
|
|
|
|
except ConnectionError as e:
|
|
self.logger.error(str(e))
|
|
self.close()
|
|
raise
|
|
except (socket.error, socket.timeout) as e:
|
|
self.close()
|
|
|
|
self.logger.error("Socket error: %s" % str(e))
|
|
if str(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 aprslib {3}{2}\r\n"
|
|
login_str = login_str.format(
|
|
self.callsign,
|
|
self.passwd,
|
|
(" filter " + self.filter) if self.filter != "" else "",
|
|
__version__
|
|
)
|
|
|
|
self.logger.info("Sending login information")
|
|
|
|
try:
|
|
self._sendall(login_str)
|
|
self.sock.settimeout(5)
|
|
test = self.sock.recv(len(login_str) + 100)
|
|
if is_py3:
|
|
test = test.decode('latin-1')
|
|
test = test.rstrip()
|
|
|
|
self.logger.debug("Server: %s", test)
|
|
|
|
_, _, callsign, status, _ = test.split(' ', 4)
|
|
|
|
if callsign == "":
|
|
raise LoginError("Server responded with empty callsign???")
|
|
if callsign != self.callsign:
|
|
raise LoginError("Server: %s" % test)
|
|
if status != "verified," and self.passwd != "-1":
|
|
raise LoginError("Password is incorrect")
|
|
|
|
if self.passwd == "-1":
|
|
self.logger.info("Login successful (receive only)")
|
|
else:
|
|
self.logger.info("Login successful")
|
|
|
|
except LoginError as e:
|
|
self.logger.error(str(e))
|
|
self.close()
|
|
raise
|
|
except:
|
|
self.close()
|
|
self.logger.error("Failed to login")
|
|
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 as e:
|
|
self.logger.error("socket error when setblocking(0): %s" % str(e))
|
|
raise ConnectionDrop("connection dropped")
|
|
|
|
while True:
|
|
short_buf = b''
|
|
newline = b'\r\n'
|
|
|
|
select.select([self.sock], [], [], None if blocking else 0)
|
|
|
|
try:
|
|
short_buf = self.sock.recv(4096)
|
|
|
|
# sock.recv returns empty if the connection drops
|
|
if not short_buf:
|
|
self.logger.error("socket.recv(): returned empty")
|
|
raise ConnectionDrop("connection dropped")
|
|
except socket.error as e:
|
|
self.logger.error("socket error on recv(): %s" % str(e))
|
|
if "Resource temporarily unavailable" in str(e):
|
|
if not blocking:
|
|
if len(self.buf) == 0:
|
|
break
|
|
|
|
self.buf += short_buf
|
|
|
|
while newline in self.buf:
|
|
line, self.buf = self.buf.split(newline, 1)
|
|
|
|
yield line
|