def parse_telemetry_config(body): parsed = {} match = re.findall(r"^(PARM|UNIT|EQNS|BITS)\.(.*)$", body) if match: logger.debug("Attempting to parse telemetry-message packet") form, body = match[0] parsed.update({'format': 'telemetry-message'}) if form in ["PARM", "UNIT"]: vals = body.rstrip().split(',')[:13] for val in vals: if not re.match(r"^(.{1,20}|)$", val): raise ParseError("incorrect format of %s (name too long?)" % form) defvals = [''] * 13 defvals[:len(vals)] = vals parsed.update({ 't%s' % form: defvals }) elif form == "EQNS": eqns = body.rstrip().split(',')[:15] teqns = [0, 1, 0] * 5 for idx, val in enumerate(eqns): if not re.match(r"^([-]?\d*\.?\d+|)$", val): raise ParseError("value at %d is not a number in %s" % (idx+1, form)) else: try: val = int(val) except: val = float(val) if val != "" else 0 teqns[idx] = val # 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 }) elif form == "BITS": match = re.findall(r"^([01]{8}),(.{0,23})$", body.rstrip()) if not match: raise ParseError("incorrect format of %s (title too long?)" % form) bits, title = match[0] parsed.update({ 't%s' % form: bits, 'title': title.strip(' ') }) return (body, parsed)
def parse_telemetry_config(body): parsed = {} match = re.findall(r"^(PARM|UNIT|EQNS|BITS)\.(.*)$", body) if match: logger.debug("Attempting to parse telemetry-message packet") form, body = match[0] parsed.update({'format': 'telemetry-message'}) if form in ["PARM", "UNIT"]: vals = body.split(',')[:13] for val in vals: if not re.match(r"^(.{1,20}|)$", val): raise ParseError("incorrect format of %s (name too long?)" % form) defvals = [''] * 13 defvals[:len(vals)] = vals parsed.update({ 't%s' % form: defvals }) elif form == "EQNS": eqns = body.split(',')[:15] teqns = [0, 1, 0] * 5 for idx, val in enumerate(eqns): if not re.match(r"^([-]?\d*\.?\d+|)$", val): raise ParseError("value at %d is not a number in %s" % (idx+1, form)) else: try: val = int(val) except: val = float(val) if val != "" else 0 teqns[idx] = val # 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 }) elif form == "BITS": match = re.findall(r"^([01]{8}),(.{0,23})$", body) if not match: raise ParseError("incorrect format of %s (title too long?)" % form) bits, title = match[0] parsed.update({ 't%s' % form: bits, 'title': title.strip(' ') }) return (body, parsed)
def parse_compressed(body): parsed = {} if re.match(r"^[\/\\A-Za-j][!-|]{8}[!-{}][ -|]{3}", body): logger.debug("Attempting to parse as compressed position report") if len(body) < 13: raise ParseError( "Invalid compressed packet (less than 13 characters)") parsed.update({'format': 'compressed'}) compressed = body[:13] body = body[13:] symbol_table = compressed[0] symbol = compressed[9] try: latitude = 90 - (base91.to_decimal(compressed[1:5]) / 380926.0) longitude = -180 + (base91.to_decimal(compressed[5:9]) / 190463.0) except ValueError: raise ParseError( "invalid characters in latitude/longitude encoding") # parse csT # converts the relevant characters from base91 c1, s1, ctype = [ord(x) - 33 for x in compressed[10:13]] if c1 == -1: parsed.update({'gpsfixstatus': 1 if ctype & 0x20 == 0x20 else 0}) if -1 in [c1, s1]: pass 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 elif c1 == 90: parsed.update({'radiorange': (2 * 1.08**s1) * 1.609344 }) # mul = convert mph to kmh parsed.update({ 'symbol': symbol, 'symbol_table': symbol_table, 'latitude': latitude, 'longitude': longitude, }) return (body, parsed)
def parse_compressed(body): parsed = {} if re.match(r"^[\/\\A-Za-j][!-|]{8}[!-{}][ -|]{3}", body): logger.debug("Attempting to parse as compressed position report") if len(body) < 13: raise ParseError("Invalid compressed packet (less than 13 characters)") parsed.update({'format': 'compressed'}) compressed = body[:13] body = body[13:] symbol_table = compressed[0] symbol = compressed[9] try: latitude = 90 - (base91.to_decimal(compressed[1:5]) / 380926.0) longitude = -180 + (base91.to_decimal(compressed[5:9]) / 190463.0) except ValueError: raise ParseError("invalid characters in latitude/longitude encoding") # parse csT # converts the relevant characters from base91 c1, s1, ctype = [ord(x) - 33 for x in compressed[10:13]] if c1 == -1: parsed.update({'gpsfixstatus': 1 if ctype & 0x20 == 0x20 else 0}) if -1 in [c1, s1]: pass 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 elif c1 == 90: parsed.update({'radiorange': (2 * 1.08 ** s1) * 1.609344}) # mul = convert mph to kmh parsed.update({ 'symbol': symbol, 'symbol_table': symbol_table, 'latitude': latitude, 'longitude': longitude, }) return (body, 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 else: body = body[7:] try: # zulu hhmmss format if form == 'h': timestamp = "%d%02d%02d%s" % (utc.year, utc.month, utc.day, ts) # zulu ddhhmm format # '/' local ddhhmm format elif form in 'z/': timestamp = "%d%02d%s%02d" % (utc.year, utc.month, ts, 0) else: timestamp = "19700101000000" td = utc.strptime(timestamp, "%Y%m%d%H%M%S") - datetime( 1970, 1, 1) timestamp = int( (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6) except Exception as exp: timestamp = 0 logger.debug(exp) parsed.update({ 'raw_timestamp': rawts, 'timestamp': int(timestamp), }) return (body, 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 else: body = body[7:] try: # zulu hhmmss format if form == 'h': timestamp = "%d%02d%02d%s" % (utc.year, utc.month, utc.day, ts) # zulu ddhhmm format # '/' local ddhhmm format elif form in 'z/': timestamp = "%d%02d%s%02d" % (utc.year, utc.month, ts, 0) else: timestamp = "19700101000000" timestamp = utc.strptime(timestamp, "%Y%m%d%H%M%S") timestamp = time.mktime(timestamp.timetuple()) except Exception as exp: timestamp = 0 logger.debug(exp) parsed.update({ 'raw_timestamp': rawts, 'timestamp': int(timestamp), }) return (body, parsed)
def parse_position(packet_type, body): parsed = {} if packet_type not in '!=/@;': _, body = body.split('!', 1) packet_type = '!' if packet_type == ';': logger.debug("Attempting to parse object report format") match = re.findall(r"^([ -~]{9})(\*|_)", body) if match: name, flag = match[0] parsed.update({ 'object_name': name, 'alive': flag == '*', }) body = body[10:] else: raise ParseError("invalid format") else: 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 body, result = parse_compressed(body) parsed.update(result) if len(result) > 0: logger.debug("Parsed as compressed position report") else: body, result = parse_normal(body) parsed.update(result) if len(result) > 0: logger.debug("Parsed as normal position report") else: raise ParseError("invalid format") # check comment for weather information # Page 62 of the spec if parsed['symbol'] == '_': logger.debug("Attempting to parse weather report from comment") body, result = parse_weather_data(body) parsed.update({ 'comment': body.strip(' '), 'weather': result, }) else: # decode comment parse_comment(body, parsed) if packet_type == ';': parsed.update({ 'object_format': parsed['format'], 'format': 'object', }) return ('', parsed)
def parse_message(body): parsed = {} # 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-z0-9_ \-]{5}):(.{0,67})", body, re.I) if match: bid, identifier, text = match[0] identifier = identifier.rstrip(' ') mformat = 'bulletin' if identifier == "" else 'group-bulletin' parsed.update({ 'format': mformat, 'message_text': text.strip(' '), 'bid': bid, 'identifier': identifier }) break # try to match announcement 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.strip(' '), 'aid': aid, 'identifier': identifier }) break # validate addresse match = re.findall(r"^([a-zA-Z0-9_ \-]{9}):(.*)$", body) if not match: break addresse, body = match[0] parsed.update({'addresse': addresse.rstrip(' ')}) # check if it's a telemetry configuration message body, result = parse_telemetry_config(body) if result: parsed.update(result) break # 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) if match: response, number = match[0] parsed.update({ 'response': response, 'msgNo': number }) else: body = body[0:70] 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({'message_text': body.strip(' ')}) break return ('', parsed)
def parse_message(body): parsed = {} # 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-z0-9_ \-]{5}):(.{0,67})", body, re.I) if match: bid, identifier, text = match[0] identifier = identifier.rstrip(' ') mformat = 'bulletin' if identifier == "" else 'group-bulletin' parsed.update({ 'format': mformat, 'message_text': text.strip(' '), 'bid': bid, 'identifier': identifier }) break # try to match announcement 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.strip(' '), 'aid': aid, 'identifier': identifier }) break # validate addresse match = re.findall(r"^([a-zA-Z0-9_ \-]{9}):(.*)$", body) if not match: break addresse, body = match[0] parsed.update({'addresse': addresse.rstrip(' ')}) # check if it's a telemetry configuration message body, result = parse_telemetry_config(body) if result: parsed.update(result) break # regular message # --------------------------- logger.debug("Packet is just a regular message") parsed.update({'format': 'message'}) # APRS supports two different message formats: # - the standard format which is described in 'aprs101.pdf': # http://www.aprs.org/doc/APRS101.PDF # - an addendum from 1999 which introduces a new format: # http://www.aprs.org/aprs11/replyacks.txt # # A message (ack/rej as well as a standard msg text body) can either have: # - no message number at all # - a message number in the old format (1..5 characters / digits) # - a message number in the new format (2 characters / digits) without trailing 'ack msg no' # - a message number in the new format with trailing 'free ack msg no' (2 characters / digits) # ack / rej # --------------------------- # NEW REPLAY-ACK # format: :AAAABBBBC:ackMM}AA match = re.findall(r"^(ack|rej)([A-Za-z0-9]{2})}([A-Za-z0-9]{2})?$", body) if match: parsed['response'], parsed['msgNo'], ackMsgNo = match[0] if ackMsgNo: parsed['ackMsgNo'] = ackMsgNo break # ack/rej standard format as per aprs101.pdf chapter 14 # format: :AAAABBBBC:ack12345 match = re.findall(r"^(ack|rej)([A-Za-z0-9]{1,5})$", body) if match: parsed['response'], parsed['msgNo'] = match[0] break # regular message body parser # --------------------------- parsed['message_text'] = body.strip(' ') # check for ACKs # new message format: http://www.aprs.org/aprs11/replyacks.txt # format: :AAAABBBBC:text.....{MM}AA match = re.findall(r"{([A-Za-z0-9]{2})}([A-Za-z0-9]{2})?$", body) if match: msgNo, ackMsgNo = match[0] parsed['message_text'] = body[:len(body) - 4 - len(ackMsgNo)].strip(' ') parsed['msgNo'] = msgNo if ackMsgNo: parsed['ackMsgNo'] = ackMsgNo break # old message format - see aprs101.pdf. # search for: msgNo present match = re.findall(r"{([A-Za-z0-9]{1,5})$", body) if match: msgNo = match[0] parsed['message_text'] = body[:len(body) - 1 - len(msgNo)].strip(' ') parsed['msgNo'] = msgNo break # break free from the eternal 'while' break return ('', parsed)
def parse_position(packet_type, body): parsed = {} if packet_type not in '!=/@;': _, body = body.split('!', 1) packet_type = '!' if packet_type == ';': logger.debug("Attempting to parse object report format") match = re.findall(r"^([ -~]{9})(\*|_)", body) if match: name, flag = match[0] parsed.update({ 'object_name': name, 'alive': flag == '*', }) body = body[10:] else: raise ParseError("invalid format") else: 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") # decode body body, result = parse_compressed(body) parsed.update(result) if len(result) > 0: logger.debug("Parsed as compressed position report") else: body, result = parse_normal(body) parsed.update(result) if len(result) > 0: logger.debug("Parsed as normal position report") else: raise ParseError("invalid format") # check comment for weather information # Page 62 of the spec if parsed['symbol'] == '_': logger.debug("Attempting to parse weather report from comment") body, result = parse_weather_data(body) parsed.update({ 'comment': body.strip(' '), 'weather': result, }) else: # decode comment parse_comment(body, parsed) if packet_type == ';': parsed.update({ 'object_format': parsed['format'], 'format': 'object', }) return ('', parsed)