clean up and updated comments
This commit is contained in:
parent
a1eac33630
commit
33eb7a457e
373
aprs.py
373
aprs.py
|
|
@ -1,4 +1,12 @@
|
|||
# Copyright 2013-2014 (C) Rossen Georgiev
|
||||
"""
|
||||
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
|
||||
|
|
@ -8,33 +16,45 @@ import math
|
|||
import logging
|
||||
import sys
|
||||
|
||||
__version__ = "0.5.1"
|
||||
__author__ = "Rossen Georgiev"
|
||||
__date__ = str(datetime.date.today())
|
||||
|
||||
__all__ = ['IS', 'base91', 'parse']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.addLevelName(11, "ParseError")
|
||||
|
||||
__all__ = ['IS', 'GenericError', 'ParseError', 'UnknownFormat',
|
||||
'LoginError', 'ConnectionError', 'ConnectionDrop', 'parse']
|
||||
|
||||
# IS class is used to connect to aprs-is servers and listen to the feed
|
||||
|
||||
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):
|
||||
"""
|
||||
APRS module that listens and parses sentences passed by aprs.net servers
|
||||
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.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
|
||||
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:
|
||||
|
|
@ -42,10 +62,13 @@ class IS(object):
|
|||
|
||||
return self.set_filter("b/%s" % "/".join(callsigns))
|
||||
|
||||
def set_filter(self, filter):
|
||||
self.filter = filter
|
||||
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)
|
||||
logger.info("Setting filter to: %s", self.filter)
|
||||
|
||||
if self._connected:
|
||||
self.sock.sendall("#filter %s\r\n" % self.filter)
|
||||
|
|
@ -59,7 +82,7 @@ class IS(object):
|
|||
self.callsign = callsign
|
||||
self.passwd = passwd
|
||||
|
||||
def set_server(self, host, port=14850):
|
||||
def set_server(self, host, port):
|
||||
"""
|
||||
Set server ip/host and port to use
|
||||
"""
|
||||
|
|
@ -73,13 +96,13 @@ class IS(object):
|
|||
if not self._connected:
|
||||
while True:
|
||||
try:
|
||||
logger.info("Attempting connection to %s:%s" % (self.server[0], self.server[1]))
|
||||
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)
|
||||
logger.info("Filter set to: %s", self.filter)
|
||||
|
||||
if self.passwd == "-1":
|
||||
logger.info("Login successful (receive only)")
|
||||
|
|
@ -115,7 +138,7 @@ class IS(object):
|
|||
immortal: When true, consumer will try to reconnect and stop propagation of Parse exceptions.
|
||||
if false (default), consumer will return
|
||||
|
||||
raw: when true, raw aprs sentence is passed to the callback, otherwise parsed data as dict
|
||||
raw: when true, raw packet is passed to callback, otherwise the result from aprs.parse()
|
||||
"""
|
||||
|
||||
if not self._connected:
|
||||
|
|
@ -144,11 +167,7 @@ class IS(object):
|
|||
except StopIteration:
|
||||
break
|
||||
except:
|
||||
#if not immortal:
|
||||
# raise
|
||||
#logger.exception(e)
|
||||
#continue
|
||||
logger.error("APRS Packet: " + line)
|
||||
logger.error("APRS Packet: %s", line)
|
||||
raise
|
||||
|
||||
if not blocking:
|
||||
|
|
@ -156,18 +175,20 @@ class IS(object):
|
|||
|
||||
def _connect(self):
|
||||
"""
|
||||
Attemps to open a connection to the server, retries if it fails
|
||||
Attemps to open a connection to the server
|
||||
"""
|
||||
|
||||
try:
|
||||
self.sock = socket.create_connection(self.server, 15) # 15 seconds connection timeout
|
||||
# 15 seconds connection timeout
|
||||
self.sock = socket.create_connection(self.server, 15)
|
||||
|
||||
self.sock.settimeout(5) # 5 second timeout to receive server banner
|
||||
# 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']:
|
||||
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)
|
||||
|
|
@ -175,7 +196,6 @@ class IS(object):
|
|||
if self.sock.recv(512)[0] != "#":
|
||||
raise ConnectionError("invalid banner from server")
|
||||
|
||||
|
||||
except Exception, e:
|
||||
self.close()
|
||||
|
||||
|
|
@ -190,7 +210,11 @@ class IS(object):
|
|||
"""
|
||||
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)
|
||||
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)
|
||||
|
|
@ -198,7 +222,7 @@ class IS(object):
|
|||
test = self.sock.recv(len(login_str) + 100)
|
||||
self.sock.setblocking(True)
|
||||
|
||||
(x, x, callsign, status, x) = test.split(' ',4)
|
||||
(x, x, callsign, status, x) = test.split(' ', 4)
|
||||
|
||||
if callsign != self.callsign:
|
||||
raise LoginError("login callsign does not match")
|
||||
|
|
@ -234,7 +258,7 @@ class IS(object):
|
|||
if "Resource temporarily unavailable" in e:
|
||||
if not blocking:
|
||||
if len(self.buf) == 0:
|
||||
break;
|
||||
break
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
|
|
@ -277,33 +301,41 @@ MTYPE_TABLE_CUSTOM = {
|
|||
"000": "Emergency",
|
||||
}
|
||||
|
||||
# routine for decoding base91 values of arbitrary length
|
||||
|
||||
def base91(text):
|
||||
"""
|
||||
Takes a base91 char string and returns decimal
|
||||
"""
|
||||
|
||||
decimal = 0
|
||||
length = len(text);
|
||||
length = len(text)
|
||||
for i in range(length):
|
||||
decimal += (ord(text[i]) - 33) * (91.0 ** (length-1-i))
|
||||
|
||||
return decimal if text != '' else ''
|
||||
|
||||
# parse a single packet
|
||||
# throws expcetions
|
||||
|
||||
def parse(raw_sentence):
|
||||
"""
|
||||
Parses position sentences and returns a dict with the useful data
|
||||
All attributes are in meteric units
|
||||
Parses an APRS packet and returns a dict with decoded data
|
||||
|
||||
- All attributes are in meteric units
|
||||
|
||||
Supports:
|
||||
* normal/compressed/mic-e position reports
|
||||
- including comment extentions for altitude and telemetry
|
||||
* messages including bulletins, announcements and telemetry
|
||||
* status message
|
||||
"""
|
||||
|
||||
raw_sentence = raw_sentence.rstrip("\r\n")
|
||||
logger.debug("Parsing: %s" % raw_sentence)
|
||||
logger.debug("Parsing: %s", raw_sentence)
|
||||
|
||||
if len(raw_sentence) == 0:
|
||||
raise ParseError("packet is empty", raw_sentence)
|
||||
|
||||
try:
|
||||
(header, body) = raw_sentence.split(':',1)
|
||||
(header, body) = raw_sentence.split(':', 1)
|
||||
except:
|
||||
raise ParseError("packet has no body", raw_sentence)
|
||||
|
||||
|
|
@ -314,7 +346,7 @@ def parse(raw_sentence):
|
|||
raise ParseError("packet header contains non-ascii characters ", raw_sentence)
|
||||
|
||||
try:
|
||||
(fromcall, path) = header.split('>',1)
|
||||
(fromcall, path) = header.split('>', 1)
|
||||
except:
|
||||
raise ParseError("invalid packet header", raw_sentence)
|
||||
|
||||
|
|
@ -325,19 +357,19 @@ def parse(raw_sentence):
|
|||
if len(path) < 1 or len(path[0]) == 0:
|
||||
raise ParseError("no tocallsign", raw_sentence)
|
||||
|
||||
tocall = path[0]
|
||||
tocall = path[0]
|
||||
path = path[1:]
|
||||
|
||||
parsed = {
|
||||
'raw': raw_sentence,
|
||||
'from': fromcall,
|
||||
'to': tocall,
|
||||
'path': path,
|
||||
}
|
||||
'raw': raw_sentence,
|
||||
'from': fromcall,
|
||||
'to': tocall,
|
||||
'path': path,
|
||||
}
|
||||
|
||||
try:
|
||||
viacall = path[-1] if re.match(r"^qA[CXUoOSrRRZI]$", path[-2]) else ""
|
||||
parsed.update({ 'via': viacall })
|
||||
parsed.update({'via': viacall})
|
||||
except:
|
||||
pass
|
||||
|
||||
|
|
@ -355,27 +387,39 @@ def parse(raw_sentence):
|
|||
# try to parse timestamp
|
||||
match = re.findall(r"^((\d{6})(.))$", body[0:7])
|
||||
if match:
|
||||
rawts,ts,form = match[0]
|
||||
rawts, ts, form = match[0]
|
||||
utc = datetime.datetime.utcnow()
|
||||
|
||||
if packet_type == '>' and form != 'z':
|
||||
raise ParseError("Time format for status reports should be zulu", raw_sentence)
|
||||
|
||||
parsed.update({ 'raw_timestamp': rawts })
|
||||
parsed.update({'raw_timestamp': rawts})
|
||||
|
||||
try:
|
||||
if form == 'h': # zulu hhmmss format
|
||||
timestamp = time.mktime(utc.strptime("%s %s %s %s" % (utc.year, utc.month, utc.day, ts), "%Y %m %d %H%M%S").timetuple())
|
||||
elif form == 'z': # zulu ddhhss format
|
||||
timestamp = time.mktime(utc.strptime("%s %s %s" % (utc.year, utc.month, ts), "%Y %m %d%M%S").timetuple())
|
||||
elif form == '/': # '/' local ddhhss format
|
||||
timestamp = time.mktime(utc.strptime("%s %s %s" % (utc.year, utc.month, ts), "%Y %m %d%M%S").timetuple())
|
||||
if form in "hz/":
|
||||
if form == 'h': # zulu hhmmss format
|
||||
timestamp = utc.strptime(
|
||||
"%s %s %s %s" % (utc.year, utc.month, utc.day, ts),
|
||||
"%Y %m %d %H%M%S"
|
||||
)
|
||||
elif form == 'z': # zulu ddhhss format
|
||||
timestamp = utc.strptime(
|
||||
"%s %s %s" % (utc.year, utc.month, ts),
|
||||
"%Y %m %d%M%S"
|
||||
)
|
||||
elif form == '/': # '/' local ddhhss format
|
||||
timestamp = utc.strptime(
|
||||
"%s %s %s" % (utc.year, utc.month, ts),
|
||||
"%Y %m %d%M%S"
|
||||
)
|
||||
|
||||
timestamp = time.mktime(timestamp.timetuple())
|
||||
else:
|
||||
timestamp = 0
|
||||
except:
|
||||
timestamp = 0
|
||||
|
||||
parsed.update({ 'timestamp': int(timestamp) })
|
||||
parsed.update({'timestamp': int(timestamp)})
|
||||
|
||||
# remove datetime from the body for further parsing
|
||||
body = body[7:]
|
||||
|
|
@ -403,20 +447,23 @@ def parse(raw_sentence):
|
|||
raise ParseError("invalid data format", raw_sentence)
|
||||
|
||||
# get symbol table and symbol
|
||||
parsed.update({ 'symbol': body[6], 'symbol_table': body[7] })
|
||||
parsed.update({
|
||||
'symbol': body[6],
|
||||
'symbol_table': body[7]
|
||||
})
|
||||
|
||||
# parse latitude
|
||||
# the routine translates each characters into a lat digit as described in
|
||||
# 'Mic-E Destination Address Field Encoding' table
|
||||
tmpdstcall = ""
|
||||
for i in dstcall:
|
||||
if i in "KLZ": # spaces
|
||||
if i in "KLZ": # spaces
|
||||
tmpdstcall += " "
|
||||
elif ord(i) > 76: # P-Y
|
||||
elif ord(i) > 76: # P-Y
|
||||
tmpdstcall += chr(ord(i) - 32)
|
||||
elif ord(i) > 57: # A-J
|
||||
elif ord(i) > 57: # A-J
|
||||
tmpdstcall += chr(ord(i) - 16)
|
||||
else: # 0-9
|
||||
else: # 0-9
|
||||
tmpdstcall += i
|
||||
|
||||
# determine position ambiguity
|
||||
|
|
@ -425,7 +472,9 @@ def parse(raw_sentence):
|
|||
raise ParseError("invalid latitude ambiguity", raw_sentence)
|
||||
|
||||
posambiguity = len(match[0])
|
||||
parsed.update({ 'posambiguity': posambiguity })
|
||||
parsed.update({
|
||||
'posambiguity': posambiguity
|
||||
})
|
||||
|
||||
# adjust the coordinates be in center of ambiguity box
|
||||
tmpdstcall = list(tmpdstcall)
|
||||
|
|
@ -437,7 +486,7 @@ def parse(raw_sentence):
|
|||
|
||||
tmpdstcall = "".join(tmpdstcall)
|
||||
|
||||
latminutes = float( ("%s.%s" % (tmpdstcall[2:4], tmpdstcall[4:6])).replace(" ", "0") )
|
||||
latminutes = float(("%s.%s" % (tmpdstcall[2:4], tmpdstcall[4:6])).replace(" ", "0"))
|
||||
|
||||
if latminutes >= 60:
|
||||
raise ParseError("Latitude minutes >= 60", raw_sentence)
|
||||
|
|
@ -447,27 +496,35 @@ def parse(raw_sentence):
|
|||
# determine the sign N/S
|
||||
latitude = -latitude if ord(dstcall[3]) <= 0x4c else latitude
|
||||
|
||||
parsed.update({ 'latitude': latitude })
|
||||
parsed.update({
|
||||
'latitude': latitude
|
||||
})
|
||||
|
||||
# parse message bits
|
||||
|
||||
mbits = re.sub(r"[0-9L]","0", dstcall[0:3])
|
||||
mbits = re.sub(r"[P-Z]","1", mbits)
|
||||
mbits = re.sub(r"[A-K]","2", mbits)
|
||||
mbits = re.sub(r"[0-9L]", "0", dstcall[0:3])
|
||||
mbits = re.sub(r"[P-Z]", "1", mbits)
|
||||
mbits = re.sub(r"[A-K]", "2", mbits)
|
||||
|
||||
parsed.update({ 'mbits': mbits })
|
||||
parsed.update({
|
||||
'mbits': mbits
|
||||
})
|
||||
|
||||
# resolve message type
|
||||
|
||||
if mbits.find("2") > -1:
|
||||
parsed.update({ 'mtype': MTYPE_TABLE_CUSTOM[mbits.replace("2","1")] })
|
||||
parsed.update({
|
||||
'mtype': MTYPE_TABLE_CUSTOM[mbits.replace("2", "1")]
|
||||
})
|
||||
else:
|
||||
parsed.update({ 'mtype': MTYPE_TABLE_STD[mbits] })
|
||||
parsed.update({
|
||||
'mtype': MTYPE_TABLE_STD[mbits]
|
||||
})
|
||||
|
||||
# parse longitude
|
||||
|
||||
longitude = ord(body[0]) - 28 # decimal part of longitude
|
||||
longitude += 100 if ord(dstcall[4]) >= 0x50 else 0 # apply lng offset
|
||||
longitude = ord(body[0]) - 28 # decimal part of longitude
|
||||
longitude += 100 if ord(dstcall[4]) >= 0x50 else 0 # apply lng offset
|
||||
longitude += -80 if longitude >= 180 and longitude <= 189 else 0
|
||||
longitude += -190 if longitude >= 190 and longitude <= 199 else 0
|
||||
|
||||
|
|
@ -496,7 +553,9 @@ def parse(raw_sentence):
|
|||
# apply E/W sign
|
||||
longitude = 0 - longitude if ord(dstcall[5]) >= 0x50 else longitude
|
||||
|
||||
parsed.update({ 'longitude': longitude })
|
||||
parsed.update({
|
||||
'longitude': longitude
|
||||
})
|
||||
|
||||
# parse speed and course
|
||||
speed = (ord(body[3]) - 28) * 10
|
||||
|
|
@ -509,8 +568,11 @@ def parse(raw_sentence):
|
|||
speed += -800 if speed >= 800 else 0
|
||||
course += -400 if course >= 400 else 0
|
||||
|
||||
speed *= 1.852 # knots * 1.852 = kmph
|
||||
parsed.update({'speed': speed, 'course': course })
|
||||
speed *= 1.852 # knots * 1.852 = kmph
|
||||
parsed.update({
|
||||
'speed': speed,
|
||||
'course': course
|
||||
})
|
||||
|
||||
# the rest of the packet can contain telemetry and comment
|
||||
|
||||
|
|
@ -535,7 +597,7 @@ def parse(raw_sentence):
|
|||
# check for optional altitude
|
||||
match = re.findall(r"^(.*)([!-{]{3})\}(.*)$", body)
|
||||
if match:
|
||||
body,altitude,extra = match[0]
|
||||
body, altitude, extra = match[0]
|
||||
|
||||
altitude = base91(altitude) - 10000
|
||||
parsed.update({'altitude': altitude})
|
||||
|
|
@ -552,7 +614,10 @@ def parse(raw_sentence):
|
|||
|
||||
elif packet_type == '>':
|
||||
logger.debug("Packet is just a status message")
|
||||
parsed.update({'format': 'status', 'comment': body })
|
||||
parsed.update({
|
||||
'format': 'status',
|
||||
'comment': body
|
||||
})
|
||||
|
||||
# MESSAGE PACKET
|
||||
#
|
||||
|
|
@ -571,39 +636,49 @@ def parse(raw_sentence):
|
|||
# the while loop is used to easily break out once a match is found
|
||||
while True:
|
||||
# try to match bulletin
|
||||
match = re.findall(r"^BLN([0-9])([a-zA-Z0-9 \-]{5}):(.{0,67})", body)
|
||||
match = re.findall(r"^BLN([0-9])([a-zA-Z0-9 \-]{5}):(.{0,67})", body)
|
||||
if match:
|
||||
bid, identifier, text = match[0]
|
||||
identifier = identifier.rstrip(' ')
|
||||
|
||||
mformat = 'bulletin' if identifier == "" else 'group-bulletin'
|
||||
|
||||
parsed.update({'format':mformat, 'message_text':text, 'bid':bid, 'identifier': identifier})
|
||||
parsed.update({
|
||||
'format': mformat,
|
||||
'message_text': text,
|
||||
'bid': bid,
|
||||
'identifier': identifier
|
||||
})
|
||||
break
|
||||
|
||||
# try to match announcement
|
||||
match = re.findall(r"^BLN([A-Z])([a-zA-Z0-9 \-]{5}):(.{0,67})", body)
|
||||
match = re.findall(r"^BLN([A-Z])([a-zA-Z0-9 \-]{5}):(.{0,67})", body)
|
||||
if match:
|
||||
aid, identifier, text = match[0]
|
||||
identifier = identifier.rstrip(' ')
|
||||
|
||||
parsed.update({'format':'announcement', 'message_text':text, 'aid':aid, 'identifier': identifier})
|
||||
parsed.update({
|
||||
'format': 'announcement',
|
||||
'message_text': text,
|
||||
'aid': aid,
|
||||
'identifier': identifier
|
||||
})
|
||||
break
|
||||
|
||||
# validate addresse
|
||||
match = re.findall(r"^([a-zA-Z0-9 \-]{9}):(.*)$", body)
|
||||
match = re.findall(r"^([a-zA-Z0-9 \-]{9}):(.*)$", body)
|
||||
if not match:
|
||||
raise ParseError("invalid addresse in message", raw_sentence)
|
||||
|
||||
addresse,body = match[0]
|
||||
addresse, body = match[0]
|
||||
|
||||
parsed.update({'addresse': addresse.rstrip(' ')})
|
||||
|
||||
# check if it's a telemetry configuration message
|
||||
match = re.findall(r"^(PARM|UNIT|EQNS|BITS)\.(.*)$", body)
|
||||
match = re.findall(r"^(PARM|UNIT|EQNS|BITS)\.(.*)$", body)
|
||||
if match:
|
||||
logger.debug("Attempting to parse telemetry-message packet")
|
||||
form,body = match[0]
|
||||
form, body = match[0]
|
||||
|
||||
parsed.update({'format': 'telemetry-message'})
|
||||
|
||||
|
|
@ -617,14 +692,16 @@ def parse(raw_sentence):
|
|||
defvals = [''] * 13
|
||||
defvals[:len(vals)] = vals
|
||||
|
||||
parsed.update({ 't%s' % form : defvals })
|
||||
parsed.update({
|
||||
't%s' % form: defvals
|
||||
})
|
||||
elif form == "EQNS":
|
||||
eqns = body.split(',')[:15]
|
||||
teqns = [0,1,0] * 5
|
||||
teqns = [0, 1, 0] * 5
|
||||
|
||||
for idx,val in zip(range(len(eqns)), eqns):
|
||||
for idx, val in enumerate(eqns):
|
||||
if not re.match("^([-]?\d*\.?\d+|)$", val):
|
||||
raise ParseError("value at %d is not a number in %s" % (idx+1,form), raw_sentence)
|
||||
raise ParseError("value at %d is not a number in %s" % (idx+1, form), raw_sentence)
|
||||
else:
|
||||
try:
|
||||
val = int(val)
|
||||
|
|
@ -636,7 +713,9 @@ def parse(raw_sentence):
|
|||
# group values in 5 list of 3
|
||||
teqns = [teqns[i*3:(i+1)*3] for i in range(5)]
|
||||
|
||||
parsed.update({ 't%s' % form : teqns })
|
||||
parsed.update({
|
||||
't%s' % form: teqns
|
||||
})
|
||||
elif form == "BITS":
|
||||
match = re.findall(r"^([01]{8}),(.{0,23})$", body)
|
||||
if not match:
|
||||
|
|
@ -644,29 +723,35 @@ def parse(raw_sentence):
|
|||
|
||||
bits, title = match[0]
|
||||
|
||||
parsed.update({ 't%s' % form : bits, 'title': title })
|
||||
parsed.update({
|
||||
't%s' % form: bits,
|
||||
'title': title
|
||||
})
|
||||
|
||||
# regular message
|
||||
else:
|
||||
logger.debug("Packet is just a regular message")
|
||||
parsed.update({'format': 'message'})
|
||||
|
||||
match = re.findall(r"^(ack|rej)\{([0-9]{1,5})$", body)
|
||||
match = re.findall(r"^(ack|rej)\{([0-9]{1,5})$", body)
|
||||
if match:
|
||||
response, number = match[0]
|
||||
|
||||
parsed.update({'response': response, 'msgNo': number })
|
||||
parsed.update({
|
||||
'response': response,
|
||||
'msgNo': number
|
||||
})
|
||||
else:
|
||||
body = body[0:70]
|
||||
|
||||
match = re.findall(r"\{([0-9]{1,5})$", body)
|
||||
match = re.findall(r"\{([0-9]{1,5})$", body)
|
||||
if match:
|
||||
msgid = match[0]
|
||||
body = body[:len(body) - 1 - len(msgid)]
|
||||
|
||||
parsed.update( {'msgNo': int(msgid)} )
|
||||
parsed.update({'msgNo': int(msgid)})
|
||||
|
||||
parsed.update({'message_text': body })
|
||||
parsed.update({'message_text': body})
|
||||
|
||||
break
|
||||
|
||||
|
|
@ -680,7 +765,7 @@ def parse(raw_sentence):
|
|||
# ./YYYYXXXX$csT Compressed (Used in any !=/@ format)
|
||||
|
||||
elif packet_type in '!=/@':
|
||||
parsed.update({ "messagecapable": packet_type in '@=' })
|
||||
parsed.update({"messagecapable": packet_type in '@='})
|
||||
|
||||
if len(body) == 0 and 'timestamp' in parsed:
|
||||
raise ParseError("invalid position report format", raw_sentence)
|
||||
|
|
@ -692,7 +777,7 @@ def parse(raw_sentence):
|
|||
if len(body) < 13:
|
||||
raise ParseError("Invalid compressed packet (less than 13 characters)", raw_sentence)
|
||||
|
||||
parsed.update({ 'format': 'compressed' })
|
||||
parsed.update({'format': 'compressed'})
|
||||
|
||||
packet = body[:13]
|
||||
extra = body[13:]
|
||||
|
|
@ -706,7 +791,7 @@ def parse(raw_sentence):
|
|||
# parse csT
|
||||
|
||||
# converts the relevant characters from base91
|
||||
c1,s1,ctype = [ord(x) - 33 for x in packet[10:13]]
|
||||
c1, s1, ctype = [ord(x) - 33 for x in packet[10:13]]
|
||||
|
||||
if c1 == -1:
|
||||
parsed.update({'gpsfixstatus': 1 if ctype & 0x20 == 0x20 else 0})
|
||||
|
|
@ -716,28 +801,28 @@ def parse(raw_sentence):
|
|||
elif ctype & 0x18 == 0x10:
|
||||
parsed.update({'altitude': (1.002 ** (c1 * 91 + s1)) * 0.3048})
|
||||
elif c1 >= 0 and c1 <= 89:
|
||||
parsed.update({'course': 360 if c1 == 0 else c1 * 4 })
|
||||
parsed.update({'speed': (1.08 ** s1 - 1) * 1.852 }) # mul = convert knts to kmh
|
||||
parsed.update({'course': 360 if c1 == 0 else c1 * 4})
|
||||
parsed.update({'speed': (1.08 ** s1 - 1) * 1.852}) # mul = convert knts to kmh
|
||||
elif c1 == 90:
|
||||
parsed.update({'radiorange': (2 * 1.08 ** s1) * 1.609344 }) # mul = convert mph to kmh
|
||||
parsed.update({'radiorange': (2 * 1.08 ** s1) * 1.609344}) # mul = convert mph to kmh
|
||||
|
||||
|
||||
# normal position report
|
||||
else:
|
||||
logger.debug("Attempting to parse as normal position report")
|
||||
parsed.update({ 'format': 'uncompressed' })
|
||||
parsed.update({'format': 'uncompressed'})
|
||||
|
||||
try:
|
||||
(
|
||||
lat_deg,
|
||||
lat_min,
|
||||
lat_dir,
|
||||
symbol_table,
|
||||
lon_deg,
|
||||
lon_min,
|
||||
lon_dir,
|
||||
symbol,
|
||||
extra
|
||||
lat_deg,
|
||||
lat_min,
|
||||
lat_dir,
|
||||
symbol_table,
|
||||
lon_deg,
|
||||
lon_min,
|
||||
lon_dir,
|
||||
symbol,
|
||||
extra
|
||||
) = re.match(r"^(\d{2})([0-9 ]{2}\.[0-9 ]{2})([NnSs])([\/\\0-9A-Z])(\d{3})([0-9 ]{2}\.[0-9 ]{2})([EeWw])([\x21-\x7e])(.*)$", body).groups()
|
||||
|
||||
# TODO: position ambiguity
|
||||
|
|
@ -748,19 +833,22 @@ def parse(raw_sentence):
|
|||
raise ParseError("latitude is out of range (0-90 degrees)", raw_sentence)
|
||||
if int(lon_deg) > 179 or int(lon_deg) < 0:
|
||||
raise ParseError("longitutde is out of range (0-180 degrees)", raw_sentence)
|
||||
#if float(lat_min) >= 60:
|
||||
# raise ParseError("latitude minutes are out of range (0-60)", raw_sentence)
|
||||
#if float(lon_min) >= 60:
|
||||
# raise ParseError("longitude minutes are out of range (0-60)", raw_sentence)
|
||||
# the above is commented out intentionally
|
||||
# apperantly aprs.fi doesn't bound check minutes
|
||||
# and there are actual packets that have >60min
|
||||
# i don't even know why that's the case
|
||||
"""
|
||||
f float(lat_min) >= 60:
|
||||
raise ParseError("latitude minutes are out of range (0-60)", raw_sentence)
|
||||
if float(lon_min) >= 60:
|
||||
raise ParseError("longitude minutes are out of range (0-60)", raw_sentence)
|
||||
|
||||
the above is commented out intentionally
|
||||
apperantly aprs.fi doesn't bound check minutes
|
||||
and there are actual packets that have >60min
|
||||
i don't even know why that's the case
|
||||
"""
|
||||
|
||||
# convert coordinates from DDMM.MM to decimal
|
||||
|
||||
latitude = int(lat_deg) + ( float(lat_min) / 60.0 )
|
||||
longitude = int(lon_deg) + ( float(lon_min) / 60.0 )
|
||||
latitude = int(lat_deg) + (float(lat_min) / 60.0)
|
||||
longitude = int(lon_deg) + (float(lon_min) / 60.0)
|
||||
|
||||
latitude *= -1 if lat_dir in 'Ss' else 1
|
||||
longitude *= -1 if lon_dir in 'Ww' else 1
|
||||
|
|
@ -771,7 +859,10 @@ def parse(raw_sentence):
|
|||
|
||||
# include symbol in the result
|
||||
|
||||
parsed.update({ 'symbol': symbol, 'symbol_table': symbol_table })
|
||||
parsed.update({
|
||||
'symbol': symbol,
|
||||
'symbol_table': symbol_table
|
||||
})
|
||||
|
||||
# include longitude and latitude in the result
|
||||
|
||||
|
|
@ -781,14 +872,14 @@ def parse(raw_sentence):
|
|||
|
||||
# try CRS/SPD/
|
||||
|
||||
match = re.findall(r"^([0-9]{3})/([0-9]{3})", extra)
|
||||
match = re.findall(r"^([0-9]{3})/([0-9]{3})", extra)
|
||||
if match:
|
||||
cse, spd = match[0]
|
||||
extra = extra[7:]
|
||||
parsed.update({'course': int(cse), 'speed': int(spd)*1.852}) # knots to kms
|
||||
parsed.update({'course': int(cse), 'speed': int(spd)*1.852}) # knots to kms
|
||||
|
||||
# try BRG/NRQ/
|
||||
match = re.findall(r"^([0-9]{3})/([0-9]{3})", extra)
|
||||
match = re.findall(r"^([0-9]{3})/([0-9]{3})", extra)
|
||||
if match:
|
||||
brg, nrq = match[0]
|
||||
extra = extra[7:]
|
||||
|
|
@ -800,15 +891,15 @@ def parse(raw_sentence):
|
|||
match = re.findall(r"^(.*?)/A=(\-\d{5}|\d{6})(.*)$", extra)
|
||||
|
||||
if match:
|
||||
extra,altitude,post = match[0]
|
||||
extra += post # glue front and back part together, DONT ASK
|
||||
extra, altitude, post = match[0]
|
||||
extra += post # glue front and back part together, DONT ASK
|
||||
|
||||
parsed.update({ 'altitude': int(altitude)*0.3048 })
|
||||
parsed.update({'altitude': int(altitude)*0.3048})
|
||||
|
||||
# try parse comment telemetry
|
||||
match = re.findall(r"^(.*?)\|([!-{]{2,14})\|(.*)$", extra)
|
||||
if match and len(match[0][2]) % 2 == 0:
|
||||
extra,telemetry,post = match[0]
|
||||
extra, telemetry, post = match[0]
|
||||
extra += post
|
||||
|
||||
temp = []
|
||||
|
|
@ -839,32 +930,56 @@ def parse(raw_sentence):
|
|||
|
||||
# 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))
|
||||
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))
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue