separated packet parsing code into functions
This commit is contained in:
parent
5153c38ec7
commit
fadb993939
681
aprslib/parse.py
681
aprslib/parse.py
|
|
@ -55,7 +55,7 @@ MTYPE_TABLE_CUSTOM = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def parse(raw_sentence):
|
def parse(packet):
|
||||||
"""
|
"""
|
||||||
Parses an APRS packet and returns a dict with decoded data
|
Parses an APRS packet and returns a dict with decoded data
|
||||||
|
|
||||||
|
|
@ -68,11 +68,11 @@ def parse(raw_sentence):
|
||||||
* status message
|
* status message
|
||||||
"""
|
"""
|
||||||
|
|
||||||
raw_sentence = raw_sentence.rstrip("\r\n")
|
packet = packet.rstrip("\r\n")
|
||||||
logger.debug("Parsing: %s", raw_sentence)
|
logger.debug("Parsing: %s", packet)
|
||||||
|
|
||||||
if len(raw_sentence) == 0:
|
if len(packet) == 0:
|
||||||
raise ParseError("packet is empty", raw_sentence)
|
raise ParseError("packet is empty", packet)
|
||||||
|
|
||||||
# typical packet format
|
# typical packet format
|
||||||
#
|
#
|
||||||
|
|
@ -80,39 +80,57 @@ def parse(raw_sentence):
|
||||||
# |--------header--------|-----body-------|
|
# |--------header--------|-----body-------|
|
||||||
#
|
#
|
||||||
try:
|
try:
|
||||||
(head, body) = raw_sentence.split(':', 1)
|
(head, body) = packet.split(':', 1)
|
||||||
except:
|
except:
|
||||||
raise ParseError("packet has no body", raw_sentence)
|
raise ParseError("packet has no body", packet)
|
||||||
|
|
||||||
if len(body) == 0:
|
if len(body) == 0:
|
||||||
raise ParseError("packet body is empty", raw_sentence)
|
raise ParseError("packet body is empty", packet)
|
||||||
|
|
||||||
parsed = {
|
parsed = {
|
||||||
'raw': raw_sentence,
|
'raw': packet,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
parsed.update(_parse_header(head))
|
parsed.update(_parse_header(head))
|
||||||
except ParseError, msg:
|
except ParseError, msg:
|
||||||
raise ParseError(str(msg), raw_sentence)
|
raise ParseError(str(msg), packet)
|
||||||
|
|
||||||
packet_type = body[0]
|
packet_type = body[0]
|
||||||
body = body[1:]
|
body = body[1:]
|
||||||
|
|
||||||
if len(body) == 0 and packet_type != '>':
|
if len(body) == 0 and packet_type != '>':
|
||||||
raise ParseError("packet body is empty after packet type character", raw_sentence)
|
raise ParseError("packet body is empty after packet type character", packet)
|
||||||
|
|
||||||
# attempt to parse the body
|
# attempt to parse the body
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
try:
|
||||||
|
# NOT SUPPORTED FORMATS
|
||||||
|
#
|
||||||
|
# % - agrelo
|
||||||
|
# , - invalid/test format
|
||||||
|
# { - user defined
|
||||||
|
# ? - general query format
|
||||||
|
# T - telemetry report
|
||||||
|
# * - complete weather report
|
||||||
|
# _ - positionless weather report
|
||||||
|
# # - raw weather report
|
||||||
|
# $
|
||||||
|
# ) - item report
|
||||||
|
# ; - object report
|
||||||
|
# [ - maidenhead locator beacon
|
||||||
|
if packet_type in '%,{?T*_#$);[<':
|
||||||
|
raise UnknownFormat("format is not supported", packet)
|
||||||
|
|
||||||
# STATUS PACKET
|
# STATUS PACKET
|
||||||
#
|
#
|
||||||
# >DDHHMMzComments
|
# >DDHHMMzComments
|
||||||
# >Comments
|
# >Comments
|
||||||
|
elif packet_type == '>':
|
||||||
if packet_type == '>':
|
|
||||||
logger.debug("Packet is just a status message")
|
logger.debug("Packet is just a status message")
|
||||||
|
|
||||||
result, body = _parse_timestamp(body, packet_type)
|
body, result = _parse_timestamp(body, packet_type)
|
||||||
|
|
||||||
parsed.update(result)
|
parsed.update(result)
|
||||||
parsed.update({
|
parsed.update({
|
||||||
|
|
@ -121,26 +139,290 @@ def parse(raw_sentence):
|
||||||
})
|
})
|
||||||
|
|
||||||
# Mic-encoded packet
|
# Mic-encoded packet
|
||||||
#
|
|
||||||
# 'lllc/s$/......... Mic-E no message capability
|
|
||||||
# 'lllc/s$/>........ Mic-E message capability
|
|
||||||
# `lllc/s$/>........ Mic-E old posit
|
|
||||||
|
|
||||||
elif packet_type in "`'":
|
elif packet_type in "`'":
|
||||||
logger.debug("Attempting to parse as mic-e packet")
|
logger.debug("Attempting to parse as mic-e packet")
|
||||||
parsed.update({'format': 'mic-e'})
|
|
||||||
|
|
||||||
dstcall = parsed['to'].split('-')[0]
|
body, result = _parse_mice(parsed['to'], body)
|
||||||
|
parsed.update(result)
|
||||||
|
|
||||||
|
# MESSAGE PACKET
|
||||||
|
elif packet_type == ':':
|
||||||
|
logger.debug("Attempting to parse as message packet")
|
||||||
|
|
||||||
|
body, result = _parse_message(body)
|
||||||
|
parsed.update(result)
|
||||||
|
|
||||||
|
# postion report (regular or compressed)
|
||||||
|
elif packet_type in '!=/@':
|
||||||
|
parsed.update({"messagecapable": packet_type in '@='})
|
||||||
|
|
||||||
|
# decode timestamp
|
||||||
|
if packet_type in "/@":
|
||||||
|
body, result = _parse_timestamp(body, packet_type)
|
||||||
|
parsed.update(result)
|
||||||
|
|
||||||
|
if len(body) == 0 and 'timestamp' in parsed:
|
||||||
|
raise ParseError("invalid position report format", packet)
|
||||||
|
|
||||||
|
# decode body
|
||||||
|
logger.debug("Attempting to parse as compressed position report")
|
||||||
|
body, result = _parse_compressed(body)
|
||||||
|
parsed.update(result)
|
||||||
|
|
||||||
|
if len(result) == 0:
|
||||||
|
logger.debug("Attempting to parse as normal position report")
|
||||||
|
body, result = _parse_normal(body)
|
||||||
|
parsed.update(result)
|
||||||
|
|
||||||
|
if len(result) == 0:
|
||||||
|
raise ParseError("invalid format")
|
||||||
|
|
||||||
|
# decode comment
|
||||||
|
body, result = _parse_comment(body)
|
||||||
|
parsed.update(result)
|
||||||
|
|
||||||
|
# capture ParseErrors and attach the packet
|
||||||
|
except ParseError as exp:
|
||||||
|
exp.packet = packet
|
||||||
|
raise
|
||||||
|
|
||||||
|
# if we fail all attempts to parse, try beacon packet
|
||||||
|
if 'format' not in parsed:
|
||||||
|
if not re.match(r"^(AIR.*|ALL.*|AP.*|BEACON|CQ.*|GPS.*|DF.*|DGPS.*|"
|
||||||
|
"DRILL.*|DX.*|ID.*|JAVA.*|MAIL.*|MICE.*|QST.*|QTH.*|"
|
||||||
|
"RTCM.*|SKY.*|SPACE.*|SPC.*|SYM.*|TEL.*|TEST.*|TLM.*|"
|
||||||
|
"WX.*|ZIP.*|UIDIGI)$", parsed['to']):
|
||||||
|
raise UnknownFormat("format is not supported", packet)
|
||||||
|
|
||||||
|
parsed.update({
|
||||||
|
'format': 'beacon',
|
||||||
|
'text': packet_type + body,
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.debug("Parsed ok.")
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Helper parse functions
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _validate_callsign(callsign, prefix=""):
|
||||||
|
prefix = '%s: ' % prefix if bool(prefix) else ''
|
||||||
|
|
||||||
|
match = re.findall(r"^([A-Z0-9]{1,6})(-(\d{1,2}))?$", callsign)
|
||||||
|
|
||||||
|
if not match:
|
||||||
|
raise ParseError("%sinvalid callsign" % prefix)
|
||||||
|
|
||||||
|
callsign, x, ssid = match[0]
|
||||||
|
|
||||||
|
if bool(ssid) and int(ssid) > 15:
|
||||||
|
raise ParseError("%sssid not in 0-15 range" % prefix)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_header(head):
|
||||||
|
"""
|
||||||
|
Parses the header part of packet
|
||||||
|
Returns a dict
|
||||||
|
"""
|
||||||
|
# CALL1>CALL2,CALL3,CALL4,CALL5:
|
||||||
|
# |from-|--to-|------path-------|
|
||||||
|
#
|
||||||
|
try:
|
||||||
|
(fromcall, path) = head.split('>', 1)
|
||||||
|
except:
|
||||||
|
raise ParseError("invalid packet header")
|
||||||
|
|
||||||
|
# looking at aprs.fi, the rules for from/src callsign
|
||||||
|
# are a lot looser, causing a lot of packets to fail
|
||||||
|
# this check.
|
||||||
|
#
|
||||||
|
# if len(fromcall) == 0:
|
||||||
|
# raise ParseError("no fromcallsign in header")
|
||||||
|
# _validate_callsign(fromcall, "fromcallsign")
|
||||||
|
|
||||||
|
if (not 1 <= len(fromcall) <= 9 or
|
||||||
|
not re.findall(r"^[a-z0-9]{0,9}(\-[a-z0-9]{1,8})?$", fromcall, re.I)):
|
||||||
|
|
||||||
|
raise ParseError("fromcallsign is invalid")
|
||||||
|
|
||||||
|
path = path.split(',')
|
||||||
|
|
||||||
|
if len(path) < 1 or len(path[0]) == 0:
|
||||||
|
raise ParseError("no tocallsign in header")
|
||||||
|
|
||||||
|
tocall = path[0]
|
||||||
|
path = path[1:]
|
||||||
|
|
||||||
|
_validate_callsign(tocall, "tocallsign")
|
||||||
|
|
||||||
|
for digi in path:
|
||||||
|
if not re.findall(r"^[A-Z0-9\-]{1,9}\*?$", digi, re.I):
|
||||||
|
raise ParseError("invalid callsign in path")
|
||||||
|
|
||||||
|
parsed = {
|
||||||
|
'from': fromcall,
|
||||||
|
'to': tocall,
|
||||||
|
'path': path,
|
||||||
|
}
|
||||||
|
|
||||||
|
# viacall is the callsign that gated the packet to the net
|
||||||
|
# it's located behind the q-contructed
|
||||||
|
#
|
||||||
|
# CALL1>CALL2,CALL3,qAR,CALL5:
|
||||||
|
# .....................|-via-|
|
||||||
|
#
|
||||||
|
viacall = ""
|
||||||
|
if len(path) >= 2 and re.match(r"^q..$", path[-2]):
|
||||||
|
viacall = path[-1]
|
||||||
|
|
||||||
|
parsed.update({'via': viacall})
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_timestamp(body, packet_type=''):
|
||||||
|
parsed = {}
|
||||||
|
|
||||||
|
match = re.findall(r"^((\d{6})(.))$", body[0:7])
|
||||||
|
if match:
|
||||||
|
rawts, ts, form = match[0]
|
||||||
|
utc = datetime.utcnow()
|
||||||
|
|
||||||
|
timestamp = 0
|
||||||
|
|
||||||
|
if packet_type == '>' and form != 'z':
|
||||||
|
pass
|
||||||
|
if form in "hz/":
|
||||||
|
body = body[7:]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# zulu hhmmss format
|
||||||
|
if form == 'h':
|
||||||
|
timestamp = "%s%s%s%s" % (utc.year, utc.month, utc.day, ts)
|
||||||
|
# zulu ddhhmm format
|
||||||
|
# '/' local ddhhmm format
|
||||||
|
elif form in 'z/':
|
||||||
|
timestamp = "%s%s%s%s" % (utc.year, utc.month, ts, utc.second)
|
||||||
|
|
||||||
|
timestamp = utc.strptime(timestamp, "%Y%m%d%H%M%S")
|
||||||
|
timestamp = time.mktime(timestamp.timetuple())
|
||||||
|
|
||||||
|
parsed.update({'raw_timestamp': rawts})
|
||||||
|
except Exception as exp:
|
||||||
|
timestamp = 0
|
||||||
|
logger.debug(exp)
|
||||||
|
|
||||||
|
parsed.update({'timestamp': int(timestamp)})
|
||||||
|
|
||||||
|
return (body, parsed)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_comment(body):
|
||||||
|
parsed = {}
|
||||||
|
|
||||||
|
# attempt to parse remaining part of the packet (comment field)
|
||||||
|
# try CRS/SPD/
|
||||||
|
match = re.findall(r"^([0-9]{3})/([0-9]{3})", body)
|
||||||
|
if match:
|
||||||
|
cse, spd = match[0]
|
||||||
|
body = body[7:]
|
||||||
|
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})", body)
|
||||||
|
if match:
|
||||||
|
brg, nrq = match[0]
|
||||||
|
body = body[7:]
|
||||||
|
parsed.update({'bearing': int(brg), 'nrq': int(nrq)})
|
||||||
|
else:
|
||||||
|
match = re.findall(r"^(PHG(\d[\x30-\x7e]\d\d[0-9A-Z]?))\/", body)
|
||||||
|
if match:
|
||||||
|
ext, phg = match[0]
|
||||||
|
body = body[len(ext):]
|
||||||
|
parsed.update({'phg': phg})
|
||||||
|
else:
|
||||||
|
match = re.findall(r"^(RNG(\d{4}))\/", body)
|
||||||
|
if match:
|
||||||
|
ext, rng = match[0]
|
||||||
|
body = body[len(ext):]
|
||||||
|
parsed.update({'rng': int(rng) * 1.609344}) # miles to km
|
||||||
|
|
||||||
|
# try find altitude in comment /A=dddddd
|
||||||
|
match = re.findall(r"^(.*?)/A=(\-\d{5}|\d{6})(.*)$", body)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
body, altitude, post = match[0]
|
||||||
|
body += post # glue front and back part together, DONT ASK
|
||||||
|
|
||||||
|
parsed.update({'altitude': int(altitude)*0.3048})
|
||||||
|
|
||||||
|
body, telemetry = _parse_comment_telemetry(body)
|
||||||
|
parsed.update(telemetry)
|
||||||
|
|
||||||
|
if len(body) > 0 and body[0] == "/":
|
||||||
|
body = body[1:]
|
||||||
|
|
||||||
|
parsed.update({'comment': body.strip(' ')})
|
||||||
|
|
||||||
|
return ('', parsed)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_comment_telemetry(text):
|
||||||
|
"""
|
||||||
|
Looks for base91 telemetry found in comment field
|
||||||
|
Returns [remaining_text, telemetry]
|
||||||
|
"""
|
||||||
|
parsed = {}
|
||||||
|
match = re.findall(r"^(.*?)\|([!-{]{4,14})\|(.*)$", text)
|
||||||
|
|
||||||
|
if match and len(match[0][1]) % 2 == 0:
|
||||||
|
text, telemetry, post = match[0]
|
||||||
|
text += post
|
||||||
|
|
||||||
|
temp = [0] * 7
|
||||||
|
for i in range(7):
|
||||||
|
temp[i] = base91.to_decimal(telemetry[i*2:i*2+2])
|
||||||
|
|
||||||
|
parsed.update({
|
||||||
|
'telemetry': {
|
||||||
|
'seq': temp[0],
|
||||||
|
'vals': temp[1:6]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if temp[6] != '':
|
||||||
|
parsed['telemetry'].update({
|
||||||
|
'bits': "{0:08b}".format(temp[6] & 0xFF)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (text, parsed)
|
||||||
|
|
||||||
|
|
||||||
|
# Mic-encoded packet
|
||||||
|
#
|
||||||
|
# 'lllc/s$/......... Mic-E no message capability
|
||||||
|
# 'lllc/s$/>........ Mic-E message capability
|
||||||
|
# `lllc/s$/>........ Mic-E old posit
|
||||||
|
def _parse_mice(dstcall, body):
|
||||||
|
parsed = {'format': 'mic-e'}
|
||||||
|
|
||||||
|
dstcall = dstcall.split('-')[0]
|
||||||
|
|
||||||
# verify mic-e format
|
# verify mic-e format
|
||||||
if len(dstcall) != 6:
|
if len(dstcall) != 6:
|
||||||
raise ParseError("dstcall has to be 6 characters", raw_sentence)
|
raise ParseError("dstcall has to be 6 characters")
|
||||||
if len(body) < 8:
|
if len(body) < 8:
|
||||||
raise ParseError("packet data field is too short", raw_sentence)
|
raise ParseError("packet data field is too short")
|
||||||
if not re.match(r"^[0-9A-Z]{3}[0-9L-Z]{3}$", dstcall):
|
if not re.match(r"^[0-9A-Z]{3}[0-9L-Z]{3}$", dstcall):
|
||||||
raise ParseError("invalid dstcall", raw_sentence)
|
raise ParseError("invalid dstcall")
|
||||||
if not re.match(r"^[&-\x7f][&-a][\x1c-\x7f]{2}[\x1c-\x7d][\x1c-\x7f][\x21-\x7e][\/\\0-9A-Z]", body):
|
if not re.match(r"^[&-\x7f][&-a][\x1c-\x7f]{2}[\x1c-\x7d]"
|
||||||
raise ParseError("invalid data format", raw_sentence)
|
r"[\x1c-\x7f][\x21-\x7e][\/\\0-9A-Z]", body):
|
||||||
|
raise ParseError("invalid data format")
|
||||||
|
|
||||||
# get symbol table and symbol
|
# get symbol table and symbol
|
||||||
parsed.update({
|
parsed.update({
|
||||||
|
|
@ -165,7 +447,7 @@ def parse(raw_sentence):
|
||||||
# determine position ambiguity
|
# determine position ambiguity
|
||||||
match = re.findall(r"^\d+( *)$", tmpdstcall)
|
match = re.findall(r"^\d+( *)$", tmpdstcall)
|
||||||
if not match:
|
if not match:
|
||||||
raise ParseError("invalid latitude ambiguity", raw_sentence)
|
raise ParseError("invalid latitude ambiguity")
|
||||||
|
|
||||||
posambiguity = len(match[0])
|
posambiguity = len(match[0])
|
||||||
parsed.update({
|
parsed.update({
|
||||||
|
|
@ -183,10 +465,6 @@ def parse(raw_sentence):
|
||||||
tmpdstcall = "".join(tmpdstcall)
|
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)
|
|
||||||
|
|
||||||
latitude = int(tmpdstcall[0:2]) + (latminutes / 60.0)
|
latitude = int(tmpdstcall[0:2]) + (latminutes / 60.0)
|
||||||
|
|
||||||
# determine the sign N/S
|
# determine the sign N/S
|
||||||
|
|
@ -242,7 +520,7 @@ def parse(raw_sentence):
|
||||||
elif posambiguity is 1:
|
elif posambiguity is 1:
|
||||||
lngminutes = (math.floor(lngminutes*10) + 0.5) / 10.0
|
lngminutes = (math.floor(lngminutes*10) + 0.5) / 10.0
|
||||||
elif posambiguity is not 0:
|
elif posambiguity is not 0:
|
||||||
raise ParseError("Unsupported position ambiguity: %d" % posambiguity, raw_sentence)
|
raise ParseError("Unsupported position ambiguity: %d" % posambiguity)
|
||||||
|
|
||||||
longitude += lngminutes / 60.0
|
longitude += lngminutes / 60.0
|
||||||
|
|
||||||
|
|
@ -300,7 +578,7 @@ def parse(raw_sentence):
|
||||||
|
|
||||||
body = body + extra
|
body = body + extra
|
||||||
|
|
||||||
# attempt to parsed comment telemetry
|
# attempt to parse comment telemetry
|
||||||
body, telemetry = _parse_comment_telemetry(body)
|
body, telemetry = _parse_comment_telemetry(body)
|
||||||
parsed.update(telemetry)
|
parsed.update(telemetry)
|
||||||
|
|
||||||
|
|
@ -309,21 +587,24 @@ def parse(raw_sentence):
|
||||||
# rest is a comment
|
# rest is a comment
|
||||||
parsed.update({'comment': body.strip(' ')})
|
parsed.update({'comment': body.strip(' ')})
|
||||||
|
|
||||||
|
return ('', parsed)
|
||||||
|
|
||||||
# MESSAGE PACKET
|
|
||||||
#
|
|
||||||
# :ADDRESSEE:Message text ........{XXXXX Up to 5 char line number
|
|
||||||
# :ADDRESSEE:ackXXXXX Ack for same line number
|
|
||||||
# :ADDRESSEE:Message text ........{MM}AA Line# with REPLY ACK
|
|
||||||
#
|
|
||||||
# TELEMETRY MESSAGES
|
|
||||||
#
|
|
||||||
# :N3MIM:PARM.Battery,BTemp,AirTemp,Pres,Altude,Camra,Chute,Sun,10m,ATV
|
|
||||||
# :N3MIM:UNIT.Volts,deg.F,deg.F,Mbar,Kfeet,Clik,OPEN!,on,on,high
|
|
||||||
# :N3MIM:EQNS.0,2.6,0,0,.53,-32,3,4.39,49,-32,3,18,1,2,3
|
|
||||||
# :N3MIM:BITS.10110101,PROJECT TITLE...
|
|
||||||
|
|
||||||
elif packet_type == ':':
|
# MESSAGE PACKET
|
||||||
|
#
|
||||||
|
# :ADDRESSEE:Message text ........{XXXXX Up to 5 char line number
|
||||||
|
# :ADDRESSEE:ackXXXXX Ack for same line number
|
||||||
|
# :ADDRESSEE:Message text ........{MM}AA Line# with REPLY ACK
|
||||||
|
#
|
||||||
|
# TELEMETRY MESSAGES
|
||||||
|
#
|
||||||
|
# :N3MIM:PARM.Battery,BTemp,AirTemp,Pres,Altude,Camra,Chute,Sun,10m,ATV
|
||||||
|
# :N3MIM:UNIT.Volts,deg.F,deg.F,Mbar,Kfeet,Clik,OPEN!,on,on,high
|
||||||
|
# :N3MIM:EQNS.0,2.6,0,0,.53,-32,3,4.39,49,-32,3,18,1,2,3
|
||||||
|
# :N3MIM:BITS.10110101,PROJECT TITLE...
|
||||||
|
def _parse_message(body):
|
||||||
|
parsed = {}
|
||||||
|
|
||||||
# the while loop is used to easily break out once a match is found
|
# the while loop is used to easily break out once a match is found
|
||||||
while True:
|
while True:
|
||||||
# try to match bulletin
|
# try to match bulletin
|
||||||
|
|
@ -359,7 +640,7 @@ def parse(raw_sentence):
|
||||||
# validate addresse
|
# 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:
|
if not match:
|
||||||
raise ParseError("invalid addresse in message", raw_sentence)
|
raise ParseError("invalid addresse in message")
|
||||||
|
|
||||||
addresse, body = match[0]
|
addresse, body = match[0]
|
||||||
|
|
||||||
|
|
@ -378,7 +659,7 @@ def parse(raw_sentence):
|
||||||
|
|
||||||
for val in vals:
|
for val in vals:
|
||||||
if not re.match(r"^(.{1,20}|)$", val):
|
if not re.match(r"^(.{1,20}|)$", val):
|
||||||
raise ParseError("incorrect format of %s (name too long?)" % form, raw_sentence)
|
raise ParseError("incorrect format of %s (name too long?)" % form)
|
||||||
|
|
||||||
defvals = [''] * 13
|
defvals = [''] * 13
|
||||||
defvals[:len(vals)] = vals
|
defvals[:len(vals)] = vals
|
||||||
|
|
@ -392,7 +673,7 @@ def parse(raw_sentence):
|
||||||
|
|
||||||
for idx, val in enumerate(eqns):
|
for idx, val in enumerate(eqns):
|
||||||
if not re.match(r"^([-]?\d*\.?\d+|)$", val):
|
if not re.match(r"^([-]?\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))
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
val = int(val)
|
val = int(val)
|
||||||
|
|
@ -410,7 +691,7 @@ def parse(raw_sentence):
|
||||||
elif form == "BITS":
|
elif form == "BITS":
|
||||||
match = re.findall(r"^([01]{8}),(.{0,23})$", body)
|
match = re.findall(r"^([01]{8}),(.{0,23})$", body)
|
||||||
if not match:
|
if not match:
|
||||||
raise ParseError("incorrect format of %s (title too long?)" % form, raw_sentence)
|
raise ParseError("incorrect format of %s (title too long?)" % form)
|
||||||
|
|
||||||
bits, title = match[0]
|
bits, title = match[0]
|
||||||
|
|
||||||
|
|
@ -446,50 +727,36 @@ def parse(raw_sentence):
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# postion report (regular or compressed)
|
return ('', parsed)
|
||||||
#
|
|
||||||
# !DDMM.hhN/DDDMM.hhW$... POSIT ( no APRS)
|
|
||||||
# =DDMM.hhN/DDDMM.hhW$... POSIT (APRS message capable)
|
|
||||||
# /DDHHMM/DDMM.hhN/DDDMM.hhW$... Time of last fix (No APRS)
|
|
||||||
# @DDHHMM/DDMM.hhN/DDDMM.hhW$CSE/SPD/... Moving (with APRS)
|
|
||||||
# @DDHHMM/DDMM.hhN/DDDMM.hhW\CSE/SPD/BRG/NRQ/.... DF report
|
|
||||||
# ./YYYYXXXX$csT Compressed (Used in any !=/@ format)
|
|
||||||
|
|
||||||
elif packet_type in '!=/@':
|
|
||||||
parsed.update({"messagecapable": packet_type in '@='})
|
|
||||||
|
|
||||||
if packet_type in "/@":
|
def _parse_compressed(body):
|
||||||
result, body = _parse_timestamp(body, packet_type)
|
parsed = {}
|
||||||
parsed.update(result)
|
|
||||||
|
|
||||||
if len(body) == 0 and 'timestamp' in parsed:
|
|
||||||
raise ParseError("invalid position report format", raw_sentence)
|
|
||||||
|
|
||||||
# comprossed packets start with /
|
|
||||||
if re.match(r"^[\/\\A-Za-j][!-|]{8}[!-{}][ -|]{3}", body):
|
if re.match(r"^[\/\\A-Za-j][!-|]{8}[!-{}][ -|]{3}", body):
|
||||||
logger.debug("Attempting to parse as compressed position report")
|
logger.debug("Attempting to parse as compressed position report")
|
||||||
|
|
||||||
if len(body) < 13:
|
if len(body) < 13:
|
||||||
raise ParseError("Invalid compressed packet (less than 13 characters)", raw_sentence)
|
raise ParseError("Invalid compressed packet (less than 13 characters)")
|
||||||
|
|
||||||
parsed.update({'format': 'compressed'})
|
parsed.update({'format': 'compressed'})
|
||||||
|
|
||||||
packet = body[:13]
|
compressed = body[:13]
|
||||||
extra = body[13:]
|
body = body[13:]
|
||||||
|
|
||||||
symbol_table = packet[0]
|
symbol_table = compressed[0]
|
||||||
symbol = packet[9]
|
symbol = compressed[9]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
latitude = 90 - (base91.to_decimal(packet[1:5]) / 380926.0)
|
latitude = 90 - (base91.to_decimal(compressed[1:5]) / 380926.0)
|
||||||
longitude = -180 + (base91.to_decimal(packet[5:9]) / 190463.0)
|
longitude = -180 + (base91.to_decimal(compressed[5:9]) / 190463.0)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ParseError("invalid characters in latitude/longitude encoding")
|
raise ParseError("invalid characters in latitude/longitude encoding")
|
||||||
|
|
||||||
# parse csT
|
# parse csT
|
||||||
|
|
||||||
# converts the relevant characters from base91
|
# 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 compressed[10:13]]
|
||||||
|
|
||||||
if c1 == -1:
|
if c1 == -1:
|
||||||
parsed.update({'gpsfixstatus': 1 if ctype & 0x20 == 0x20 else 0})
|
parsed.update({'gpsfixstatus': 1 if ctype & 0x20 == 0x20 else 0})
|
||||||
|
|
@ -504,12 +771,25 @@ def parse(raw_sentence):
|
||||||
elif c1 == 90:
|
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
|
parsed.update({
|
||||||
else:
|
'symbol': symbol,
|
||||||
logger.debug("Attempting to parse as normal position report")
|
'symbol_table': symbol_table,
|
||||||
|
'latitude': latitude,
|
||||||
|
'longitude': longitude,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (body, parsed)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_normal(body):
|
||||||
|
parsed = {}
|
||||||
|
|
||||||
|
match = re.findall(r"^(\d{2})([0-9 ]{2}\.[0-9 ]{2})([NnSs])([\/\\0-9A-Z])"
|
||||||
|
r"(\d{3})([0-9 ]{2}\.[0-9 ]{2})([EeWw])([\x21-\x7e])(.*)$", body)
|
||||||
|
|
||||||
|
if match:
|
||||||
parsed.update({'format': 'uncompressed'})
|
parsed.update({'format': 'uncompressed'})
|
||||||
|
|
||||||
try:
|
|
||||||
(
|
(
|
||||||
lat_deg,
|
lat_deg,
|
||||||
lat_min,
|
lat_min,
|
||||||
|
|
@ -519,22 +799,22 @@ def parse(raw_sentence):
|
||||||
lon_min,
|
lon_min,
|
||||||
lon_dir,
|
lon_dir,
|
||||||
symbol,
|
symbol,
|
||||||
extra
|
body
|
||||||
) = 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()
|
) = match[0]
|
||||||
|
|
||||||
# TODO: position ambiguity
|
# TODO: position ambiguity
|
||||||
|
|
||||||
# validate longitude and latitude
|
# validate longitude and latitude
|
||||||
|
|
||||||
if int(lat_deg) > 89 or int(lat_deg) < 0:
|
if int(lat_deg) > 89 or int(lat_deg) < 0:
|
||||||
raise ParseError("latitude is out of range (0-90 degrees)", raw_sentence)
|
raise ParseError("latitude is out of range (0-90 degrees)")
|
||||||
if int(lon_deg) > 179 or int(lon_deg) < 0:
|
if int(lon_deg) > 179 or int(lon_deg) < 0:
|
||||||
raise ParseError("longitutde is out of range (0-180 degrees)", raw_sentence)
|
raise ParseError("longitutde is out of range (0-180 degrees)")
|
||||||
"""
|
"""
|
||||||
f float(lat_min) >= 60:
|
f float(lat_min) >= 60:
|
||||||
raise ParseError("latitude minutes are out of range (0-60)", raw_sentence)
|
raise ParseError("latitude minutes are out of range (0-60)")
|
||||||
if float(lon_min) >= 60:
|
if float(lon_min) >= 60:
|
||||||
raise ParseError("longitude minutes are out of range (0-60)", raw_sentence)
|
raise ParseError("longitude minutes are out of range (0-60)")
|
||||||
|
|
||||||
the above is commented out intentionally
|
the above is commented out intentionally
|
||||||
apperantly aprs.fi doesn't bound check minutes
|
apperantly aprs.fi doesn't bound check minutes
|
||||||
|
|
@ -550,234 +830,11 @@ def parse(raw_sentence):
|
||||||
latitude *= -1 if lat_dir in 'Ss' else 1
|
latitude *= -1 if lat_dir in 'Ss' else 1
|
||||||
longitude *= -1 if lon_dir in 'Ww' else 1
|
longitude *= -1 if lon_dir in 'Ww' else 1
|
||||||
|
|
||||||
except:
|
|
||||||
# failed to match normal sentence sentence
|
|
||||||
raise ParseError("invalid format", raw_sentence)
|
|
||||||
|
|
||||||
# include symbol in the result
|
|
||||||
|
|
||||||
parsed.update({
|
parsed.update({
|
||||||
'symbol': symbol,
|
'symbol': symbol,
|
||||||
'symbol_table': symbol_table
|
'symbol_table': symbol_table,
|
||||||
|
'latitude': latitude,
|
||||||
|
'longitude': longitude,
|
||||||
})
|
})
|
||||||
|
|
||||||
# include longitude and latitude in the result
|
return (body, parsed)
|
||||||
|
|
||||||
parsed.update({'latitude': latitude, 'longitude': longitude})
|
|
||||||
|
|
||||||
# attempt to parse remaining part of the packet (comment field)
|
|
||||||
|
|
||||||
# try CRS/SPD/
|
|
||||||
|
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
# try BRG/NRQ/
|
|
||||||
match = re.findall(r"^([0-9]{3})/([0-9]{3})", extra)
|
|
||||||
if match:
|
|
||||||
brg, nrq = match[0]
|
|
||||||
extra = extra[7:]
|
|
||||||
parsed.update({'bearing': int(brg), 'nrq': int(nrq)})
|
|
||||||
|
|
||||||
#TODO parse PHG
|
|
||||||
|
|
||||||
# try find altitude in comment /A=dddddd
|
|
||||||
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
|
|
||||||
|
|
||||||
parsed.update({'altitude': int(altitude)*0.3048})
|
|
||||||
|
|
||||||
extra, telemetry = _parse_comment_telemetry(extra)
|
|
||||||
parsed.update(telemetry)
|
|
||||||
|
|
||||||
if len(extra) > 0 and extra[0] == "/":
|
|
||||||
extra = extra[1:]
|
|
||||||
|
|
||||||
parsed.update({'comment': extra.strip(' ')})
|
|
||||||
|
|
||||||
# NOT SUPPORTED FORMATS
|
|
||||||
#
|
|
||||||
# % - agrelo
|
|
||||||
# , - invalid/test format
|
|
||||||
# { - user defined
|
|
||||||
# ? - general query format
|
|
||||||
# T - telemetry report
|
|
||||||
# * - complete weather report
|
|
||||||
# _ - positionless weather report
|
|
||||||
# # - raw weather report
|
|
||||||
# $
|
|
||||||
# ) - item report
|
|
||||||
# ; - object report
|
|
||||||
# [ - maidenhead locator beacon
|
|
||||||
elif packet_type in '%,{?T*_#$);[<':
|
|
||||||
raise UnknownFormat("format is not supported", raw_sentence)
|
|
||||||
|
|
||||||
# if we fail all attempts to parse, try beacon packet
|
|
||||||
if 'format' not in parsed:
|
|
||||||
if not re.match(r"^(AIR.*|ALL.*|AP.*|BEACON|CQ.*|GPS.*|DF.*|DGPS.*|"
|
|
||||||
"DRILL.*|DX.*|ID.*|JAVA.*|MAIL.*|MICE.*|QST.*|QTH.*|"
|
|
||||||
"RTCM.*|SKY.*|SPACE.*|SPC.*|SYM.*|TEL.*|TEST.*|TLM.*|"
|
|
||||||
"WX.*|ZIP.*|UIDIGI)$", parsed['to']):
|
|
||||||
raise UnknownFormat("format is not supported", raw_sentence)
|
|
||||||
|
|
||||||
parsed.update({
|
|
||||||
'format': 'beacon',
|
|
||||||
'text': packet_type + body,
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.debug("Parsed ok.")
|
|
||||||
return parsed
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Helper parse functions
|
|
||||||
#
|
|
||||||
|
|
||||||
def _parse_timestamp(body, packet_type=''):
|
|
||||||
parsed = {}
|
|
||||||
|
|
||||||
match = re.findall(r"^((\d{6})(.))$", body[0:7])
|
|
||||||
if match:
|
|
||||||
rawts, ts, form = match[0]
|
|
||||||
utc = datetime.utcnow()
|
|
||||||
|
|
||||||
timestamp = 0
|
|
||||||
|
|
||||||
if packet_type == '>' and form != 'z':
|
|
||||||
pass
|
|
||||||
if form in "hz/":
|
|
||||||
body = body[7:]
|
|
||||||
|
|
||||||
try:
|
|
||||||
# zulu hhmmss format
|
|
||||||
if form == 'h':
|
|
||||||
timestamp = "%s%s%s%s" % (utc.year, utc.month, utc.day, ts)
|
|
||||||
# zulu ddhhmm format
|
|
||||||
# '/' local ddhhmm format
|
|
||||||
elif form in 'z/':
|
|
||||||
timestamp = "%s%s%s%s" % (utc.year, utc.month, ts, utc.second)
|
|
||||||
|
|
||||||
timestamp = utc.strptime(timestamp, "%Y%m%d%H%M%S")
|
|
||||||
timestamp = time.mktime(timestamp.timetuple())
|
|
||||||
|
|
||||||
parsed.update({'raw_timestamp': rawts})
|
|
||||||
except Exception as exp:
|
|
||||||
timestamp = 0
|
|
||||||
logger.debug(exp)
|
|
||||||
|
|
||||||
parsed.update({'timestamp': int(timestamp)})
|
|
||||||
|
|
||||||
return (parsed, body)
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_callsign(callsign, prefix=""):
|
|
||||||
prefix = '%s: ' % prefix if bool(prefix) else ''
|
|
||||||
|
|
||||||
match = re.findall(r"^([A-Z0-9]{1,6})(-(\d{1,2}))?$", callsign)
|
|
||||||
|
|
||||||
if not match:
|
|
||||||
raise ParseError("%sinvalid callsign" % prefix)
|
|
||||||
|
|
||||||
callsign, x, ssid = match[0]
|
|
||||||
|
|
||||||
if bool(ssid) and int(ssid) > 15:
|
|
||||||
raise ParseError("%sssid not in 0-15 range" % prefix)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_header(head):
|
|
||||||
"""
|
|
||||||
Parses the header part of packet
|
|
||||||
Returns a dict
|
|
||||||
"""
|
|
||||||
# CALL1>CALL2,CALL3,CALL4,CALL5:
|
|
||||||
# |from-|--to-|------path-------|
|
|
||||||
#
|
|
||||||
try:
|
|
||||||
(fromcall, path) = head.split('>', 1)
|
|
||||||
except:
|
|
||||||
raise ParseError("invalid packet header")
|
|
||||||
|
|
||||||
# looking at aprs.fi, the rules for from/src callsign
|
|
||||||
# are a lot looser, causing a lot of packets to fail
|
|
||||||
# this check.
|
|
||||||
#
|
|
||||||
# if len(fromcall) == 0:
|
|
||||||
# raise ParseError("no fromcallsign in header")
|
|
||||||
# _validate_callsign(fromcall, "fromcallsign")
|
|
||||||
|
|
||||||
if not 1 <= len(fromcall) <= 9 or not re.findall(r"^[a-z0-9]{0,9}(\-[a-z0-9]{1,8})?$", fromcall, re.I):
|
|
||||||
raise ParseError("fromcallsign is invalid")
|
|
||||||
|
|
||||||
path = path.split(',')
|
|
||||||
|
|
||||||
if len(path) < 1 or len(path[0]) == 0:
|
|
||||||
raise ParseError("no tocallsign in header")
|
|
||||||
|
|
||||||
tocall = path[0]
|
|
||||||
path = path[1:]
|
|
||||||
|
|
||||||
_validate_callsign(tocall, "tocallsign")
|
|
||||||
|
|
||||||
for digi in path:
|
|
||||||
if not re.findall(r"^[A-Z0-9\-]{1,9}\*?$", digi, re.I):
|
|
||||||
raise ParseError("invalid callsign in path")
|
|
||||||
|
|
||||||
parsed = {
|
|
||||||
'from': fromcall,
|
|
||||||
'to': tocall,
|
|
||||||
'path': path,
|
|
||||||
}
|
|
||||||
|
|
||||||
# viacall is the callsign that gated the packet to the net
|
|
||||||
# it's located behind the q-contructed
|
|
||||||
#
|
|
||||||
# CALL1>CALL2,CALL3,qAR,CALL5:
|
|
||||||
# .....................|-via-|
|
|
||||||
#
|
|
||||||
viacall = ""
|
|
||||||
if len(path) >= 2 and re.match(r"^q..$", path[-2]):
|
|
||||||
viacall = path[-1]
|
|
||||||
|
|
||||||
parsed.update({'via': viacall})
|
|
||||||
|
|
||||||
return parsed
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_comment_telemetry(text):
|
|
||||||
"""
|
|
||||||
Looks for base91 telemetry found in comment field
|
|
||||||
Returns [remaining_text, telemetry]
|
|
||||||
"""
|
|
||||||
parsed = {}
|
|
||||||
match = re.findall(r"^(.*?)\|([!-{]{4,14})\|(.*)$", text)
|
|
||||||
|
|
||||||
if match and len(match[0][1]) % 2 == 0:
|
|
||||||
text, telemetry, post = match[0]
|
|
||||||
text += post
|
|
||||||
|
|
||||||
temp = [0] * 7
|
|
||||||
for i in range(7):
|
|
||||||
temp[i] = base91.to_decimal(telemetry[i*2:i*2+2])
|
|
||||||
|
|
||||||
parsed.update({
|
|
||||||
'telemetry': {
|
|
||||||
'seq': temp[0],
|
|
||||||
'vals': temp[1:6]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if temp[6] != '':
|
|
||||||
parsed['telemetry'].update({
|
|
||||||
'bits': "{0:08b}".format(temp[6] & 0xFF)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (text, parsed)
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue