Beispiel #1
0
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)
Beispiel #2
0
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, _, ssid = match[0]

    if bool(ssid) and int(ssid) > 15:
        raise ParseError("%sssid not in 0-15 range" % prefix)
Beispiel #3
0
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)
Beispiel #4
0
def parse_header(head):
    """
    Parses the header part of packet
    Returns a dict
    """
    try:
        (fromcall, path) = head.split('>', 1)
    except:
        raise ParseError("invalid packet header")

    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[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 = ""
    if len(path) >= 2 and re.match(r"^q..$", path[-2]):
        viacall = path[-1]

    parsed.update({'via': viacall})

    return parsed
Beispiel #5
0
def parse_weather(body):
    match = re.match("^(\d{8})c[\. \d]{3}s[\. \d]{3}g[\. \d]{3}t[\. \d]{3}", body)
    if not match:
        raise ParseError("invalid positionless weather report format")

    comment, weather = parse_weather_data(body[8:])

    parsed = {
        'format': 'wx',
        'wx_raw_timestamp': match.group(1),
        'comment': comment.strip(' '),
        'weather': weather,
        }

    return ('', parsed)
Beispiel #6
0
def parse_telemetry(body):
    parsed = {}

    match = re.match(r'#([0-9.]{3,4}|MIC,?),([0-9.]{3,4}),([0-9.]{3,4}),([0-9.]{3,4}),([0-9.]{3,4}),([0-9.]{3,4}),([0-1]{8,9})(.*)', body, flags=re.I)

    if not match:
        raise ParseError("Invalid telemetry format")

    data = match.groups()

    parsed.update({
        'format': 'telemetry',
        'sequence_number': 0 if data[0].lower().startswith('mic') else int(data[0][:3]),
        'analog_values': list(map(float, data[1:6])),
        'digital_value': int(data[6], 2)
        })

    return '', parsed
Beispiel #7
0
def parse_mice(dstcall, body):
    parsed = {'format': 'mic-e'}

    dstcall = dstcall.split('-')[0]

    # verify mic-e format
    if len(dstcall) != 6:
        raise ParseError("dstcall has to be 6 characters")
    if len(body) < 8:
        raise ParseError("packet data field is too short")
    if not re.match(r"^[0-9A-Z]{3}[0-9L-Z]{3}$", dstcall):
        raise ParseError("invalid dstcall")
    if not re.match(r"^[&-\x7f][&-a][\x1c-\x7f]{2}[\x1c-\x7d]"
                    r"[\x1c-\x7f][\x21-\x7e][\/\\0-9A-Z]", body):
        raise ParseError("invalid data format")

    # get symbol table and symbol
    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
            tmpdstcall += " "
        elif ord(i) > 76:  # P-Y
            tmpdstcall += chr(ord(i) - 32)
        elif ord(i) > 57:  # A-J
            tmpdstcall += chr(ord(i) - 17)
        else:  # 0-9
            tmpdstcall += i

    # determine position ambiguity
    match = re.findall(r"^\d+( *)$", tmpdstcall)
    if not match:
        raise ParseError("invalid latitude ambiguity")

    posambiguity = len(match[0])
    parsed.update({
        'posambiguity': posambiguity
        })

    # adjust the coordinates be in center of ambiguity box
    tmpdstcall = list(tmpdstcall)
    if posambiguity > 0:
        if posambiguity >= 4:
            tmpdstcall[2] = '3'
        else:
            tmpdstcall[6 - posambiguity] = '5'

    tmpdstcall = "".join(tmpdstcall)

    latminutes = float(("%s.%s" % (tmpdstcall[2:4], tmpdstcall[4:6])).replace(" ", "0"))
    latitude = int(tmpdstcall[0:2]) + (latminutes / 60.0)

    # determine the sign N/S
    latitude = -latitude if ord(dstcall[3]) <= 0x4c else 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)

    parsed.update({
        'mbits': mbits
        })

    # resolve message type

    if mbits.find("2") > -1:
        parsed.update({
            'mtype': MTYPE_TABLE_CUSTOM[mbits.replace("2", "1")]
            })
    else:
        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 += -80 if longitude >= 180 and longitude <= 189 else 0
    longitude += -190 if longitude >= 190 and longitude <= 199 else 0

    # long minutes
    lngminutes = ord(body[1]) - 28.0
    lngminutes += -60 if lngminutes >= 60 else 0

    # + (long hundredths of minutes)
    lngminutes += ((ord(body[2]) - 28.0) / 100.0)

    # apply position ambiguity
    # routines adjust longitude to center of the ambiguity box
    if posambiguity == 4:
        lngminutes = 30
    elif posambiguity == 3:
        lngminutes = (math.floor(lngminutes/10) + 0.5) * 10
    elif posambiguity == 2:
        lngminutes = math.floor(lngminutes) + 0.5
    elif posambiguity == 1:
        lngminutes = (math.floor(lngminutes*10) + 0.5) / 10.0
    elif posambiguity != 0:
        raise ParseError("Unsupported position ambiguity: %d" % posambiguity)

    longitude += lngminutes / 60.0

    # apply E/W sign
    longitude = 0 - longitude if ord(dstcall[5]) >= 0x50 else longitude

    parsed.update({
        'longitude': longitude
        })

    # parse speed and course
    speed = (ord(body[3]) - 28) * 10
    course = ord(body[4]) - 28
    quotient = int(course / 10.0)
    course += -(quotient * 10)
    course = course*100 + ord(body[5]) - 28
    speed += quotient

    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
        })

    # the rest of the packet can contain telemetry and comment

    if len(body) > 8:
        body = body[8:]

        # check for optional 2 or 5 channel telemetry
        match = re.findall(r"^('[0-9a-f]{10}|`[0-9a-f]{4})(.*)$", body)
        if match:
            hexdata, body = match[0]

            hexdata = hexdata[1:]             # remove telemtry flag
            channels = int(len(hexdata) / 2)  # determine number of channels
            hexdata = int(hexdata, 16)        # convert hex to int

            telemetry = []
            for i in range(channels):
                telemetry.insert(0, int(hexdata >> 8*i & 255))

            parsed.update({'telemetry': telemetry})

        # check for optional altitude
        match = re.findall(r"^(.*)([!-{]{3})\}(.*)$", body)
        if match:
            body, altitude, extra = match[0]

            altitude = base91.to_decimal(altitude) - 10000
            parsed.update({'altitude': altitude})

            body = body + extra

        # attempt to parse comment telemetry
        body, telemetry = parse_comment_telemetry(body)
        parsed.update(telemetry)

        # parse DAO extention
        body = parse_dao(body, parsed)

        # rest is a comment
        parsed.update({'comment': body.strip(' ')})

    return ('', parsed)
Beispiel #8
0
def parse(packet):
    """
    Parses an APRS packet and returns a dict with decoded data

    - All attributes are in metric units
    """

    if not isinstance(packet, string_type_parse):
        raise TypeError("Expected packet to be str/unicode/bytes, got %s", type(packet))

    if len(packet) == 0:
        raise ParseError("packet is empty", packet)

    # attempt to detect encoding
    if isinstance(packet, bytes):
        packet = _unicode_packet(packet)

    packet = packet.rstrip("\r\n")
    logger.debug("Parsing: %s", packet)

    # split into head and body
    try:
        (head, body) = packet.split(':', 1)
    except:
        raise ParseError("packet has no body", packet)

    if len(body) == 0:
        raise ParseError("packet body is empty", packet)

    parsed = {
        'raw': packet,
        }

    # parse head
    try:
        parsed.update(parse_header(head))
    except ParseError as msg:
        raise ParseError(str(msg), packet)

    # parse body
    packet_type = body[0]
    body = body[1:]

    if len(body) == 0 and packet_type != '>':
        raise ParseError("packet body is empty after packet type character", packet)

    # attempt to parse the body
    try:
        _try_toparse_body(packet_type, body, parsed)

    # capture ParseErrors and attach the packet
    except (UnknownFormat, 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
Beispiel #9
0
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)
Beispiel #10
0
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'})

        (
            lat_deg,
            lat_min,
            lat_dir,
            symbol_table,
            lon_deg,
            lon_min,
            lon_dir,
            symbol,
            body
        ) = match[0]

        # position ambiguity
        posambiguity = lat_min.count(' ')

        if posambiguity != lon_min.count(' '):
            raise ParseError("latitude and longitude ambiguity mismatch")

        parsed.update({'posambiguity': posambiguity})

        # we center the position inside the ambiguity box
        if posambiguity >= 4:
            lat_min = "30"
            lon_min = "30"
        else:
            lat_min = lat_min.replace(' ', '5', 1)
            lon_min = lon_min.replace(' ', '5', 1)

        # validate longitude and latitude

        if int(lat_deg) > 89 or int(lat_deg) < 0:
            raise ParseError("latitude is out of range (0-90 degrees)")
        if int(lon_deg) > 179 or int(lon_deg) < 0:
            raise ParseError("longitude is out of range (0-180 degrees)")
        """
        f float(lat_min) >= 60:
            raise ParseError("latitude minutes are out of range (0-60)")
        if float(lon_min) >= 60:
            raise ParseError("longitude minutes are out of range (0-60)")

        The above is commented out intentionally
        apparently 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 *= -1 if lat_dir in 'Ss' else 1
        longitude *= -1 if lon_dir in 'Ww' else 1

        parsed.update({
            'symbol': symbol,
            'symbol_table': symbol_table,
            'latitude': latitude,
            'longitude': longitude,
            })

    return (body, parsed)