Blog der Heimetli Software AG

GPS-Position mit Python auslesem

Dieser Post ist nur für Fans von Vintage-Geräten interessant. Ausser meinem Exemplar wird es wohl nur noch sehr wenige dieser GPS-Receiver geben.

Ein uralter GPS Receiver

Das Gerät ist mit einem Rockwell Jupiter Chipsatz ausgerüstet zu dem sogar noch Datenblätter im Internet zu finden sind. Es kommuniziert über eine RS232-Schnittstelle mit der Aussenwelt.

Das Protokoll

Während heutige Geräte hauptsächlich Text im NMEA-Format ausgeben, nutzt dieses GPS ein binäres Protokoll. Auch das ist im Datenblatt dokumentiert und erstaunlich gut designed. Sowohl der Header als auch der Datenteil sind per Checksum gesichert und haben einen sauberen Aufbau.

Earthmate hat das Protokoll anscheinend erweitert, denn es schickt undokumentierte Strings an den PC:

$ od -c /dev/ttyUSB0
0000000 377 201 363 003   5  \0  \0  \0 331   y   , 001  \0  \0 001  \0
0000020   1   2  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
0000040  \0  \0  \0  \0   0   1   .   9   8  \0  \0  \0  \0  \0  \0  \0
0000060  \0  \0  \0  \0  \0  \0  \0  \0   0   6   /   0   8   /   9   8
0000100  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0   0   0   0   3
0000120  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
0000140   9 002 016  \0 006  \0 316  \a  \0  \0  \0  \0  \0  \0  \0  \0
0000160  \0  \0  \0  \0 301   & 377 201 350 003   1  \0  \0  \0 350   y
0000200   - 001  \0  \0 002  \0 002  \0 004  \0  \0  \0  \0  \0  \0  \0
0000220 220 003 001  \0  \0  \0  \0  \0  \0  \0 034  \0 006  \0 315  \a
0000240 027  \0   ;  \0   +  \0  \n 002  \0  \0   P 270 354 004   <   2
0000260 321  \0   c 276  \0  \0 277 022  \0  \0  \0  \0  \0  \0 304 377
0000300  \0  \0  \0  \0 300   @ 331 022 220 320 003  \0  \0 243 341 021
0000320 020   '  \0  \0  \0  \0  \0 243 341 021  \0  \0  \0  \0 340 223
0000340 004  \0 263 346 377 201 352 003   -  \0  \0  \0 352   y   - 001
0000360  \0  \0 002  \0 002  \0 220 003 001  \0  \0  \0  \0  \0  \0  \0
0000400  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
*
0000500  \0  \0  \0  \0  \0  \0  \0  \0   > 373   E   A   R   T   H   A
0000520  \r  \n   E   A   R   T   H   A  \r  \n   E   A   R   T   H   A
*

Der erste Teil ist eine Startup-Meldung, aber die Bedeutung der "EARTHA\r\n"-Strings bleibt unklar. Wenn der PC nichts schickt wird dieser String endlos wiederholt.

Im Hex-Format ist die Startup-Meldung klar zu erkennen. 0xFF 0x81 markiert den Anfang eines Frames, darauf folgt der Meldungstyp und die Länge der Nutzdaten. Nach den Daten erkennt man den bekannten String

$ od -t x1 /dev/ttyUSB0
0000000 ff 81 f3 03 35 00 00 00 d9 79 2c 01 00 00 01 00
0000020 31 32 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000040 00 00 00 00 30 31 2e 39 38 00 00 00 00 00 00 00
0000060 00 00 00 00 00 00 00 00 30 36 2f 30 38 2f 39 38
0000100 00 00 00 00 00 00 00 00 00 00 00 00 30 30 30 33
0000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000140 39 02 0e 00 06 00 ce 07 00 00 00 00 00 00 00 00
0000160 00 00 00 00 c1 26 ff 81 e8 03 31 00 00 00 e8 79
0000200 2d 01 00 00 02 00 02 00 04 00 00 00 00 00 00 00
0000220 90 03 01 00 00 00 00 00 00 00 1c 00 06 00 cd 07
0000240 17 00 3b 00 2b 00 0a 02 00 00 50 b8 ec 04 3c 32
0000260 d1 00 63 be 00 00 bf 12 00 00 00 00 00 00 c4 ff
0000300 00 00 00 00 c0 40 d9 12 90 d0 03 00 00 a3 e1 11
0000320 10 27 00 00 00 00 00 a3 e1 11 00 00 00 00 e0 93
0000340 04 00 b3 e6 ff 81 ea 03 2d 00 00 00 ea 79 2d 01
0000360 00 00 02 00 02 00 90 03 01 00 00 00 00 00 00 00
0000400 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
*
0000500 00 00 00 00 00 00 00 00 3e fb 45 41 52 54 48 41
0000520 0d 0a 45 41 52 54 48 41 0d 0a 45 41 52 54 48 41
*

Wenn der PC nur liest, werden keine Positionsdaten geschickt. Wenn er dagegen den EARTHA-String schickt dann antwortet das GPS mit zwei Frames, wovon der eine die Position angibt.

Das EARTHA wurde experimentell bestimmt, anscheinend ist das Verhalten nirgends dokumentiert. Es ist also möglich dass auch ganz andere Strings die Position aus dem Receiver herausholen.

Es dauert eine ganze Weile bis das GPS eine gültige Position erkannt hat, und es funktioniert fast nur unter freiem Himmel.

Die Zeit geht 19.6 Jahre nach, weil der Wochenzähler des GPS-Signals nur 10 Bit breit ist. Seit das Gerät produziert wurde ist dieser Zähler überlaufen, und deshalb wähnt es sich noch kurz nach der Jahrtausendwende.

Das Python-Script zum Auslesen der Position

import sys
import math
import serial
import struct

def detect_start( ser ):
    """ Look for the possible start of a frame """
    ch = ser.read()
    while ch != b"\xFF":
        if ch == b"":
            print( "timeout", file=sys.stderr )
        ch = ser.read()

    # Ensure that it is a frame
    return ser.read(1) == b"\x81"

def verify_checksum( words, init = 0 ):
    s = (init + sum(words[:-1])) & 0xFFFF

    if s != 0x8000:
        s = -s & 0xFFFF

    return s == words[-1]

def read_header( ser ):
    """ Reads and verifies the header """
    buffer = ser.read( 8 )

    if len(buffer) != 8:
        return None

    fields = struct.unpack( "<hhhh", buffer )
    if not verify_checksum(fields,0x81FF):
        return None

    return { "id": fields[0], "count": fields[1], "flags": fields[2] }

def read_position( ser ):
    """ Reads the frames and decodes the position frames """

    if not detect_start( ser ):
        return

    header = read_header( ser )

    if (header is not None) and (header["count"] != 0):
        size = header["count"] * 2 + 2
        data = ser.read( size )

        # Look for position frames
        if (len(data) == size) and (header["id"] == 1000):
            # Ensure that the checksum is correct
            if verify_checksum(struct.unpack("50H",data)):
                # Unpack the frame
                elements = struct.unpack( "<L7H2L6HL3IHL4H3LH4IH", data )

                # The flags are 0 for valid positions
                print( "valid" if elements[3] == 0 else "invalid" )
                # Convert the latitude to degrees
                print( elements[17] / 1e8 / (2 * math.pi) * 360 )
                # Convert the longitude to degrees
                print( elements[18] / 1e8 / (2 * math.pi) * 360 )

        # Request the next frame
        ser.write( b"EARTHA\r\n" )

if __name__ == "__main__":
    try:
        # Open the serial port
        ser = serial.Serial( '/dev/ttyUSB0', 9600, timeout=5 )

        while True:
            read_position( ser )
    except BaseException as e:
        print( e )