Monday, June 9, 2014

A Python Program for the Raspberry Pi to Read TSIP GPS Data from a Copernicus II via USB

The Copernicus II GPS module can output data in binary TSIP (Trimble Standard Interface Protocol) format in addition to text-based NMEA format.  This example presents a Python program that reads and parses TSIP data to obtain current position data.

The Copernicus II has two serial interfaces with two sets of RX/TX pins.  You can connect a set to the GPIO 14/GPIO 15 pins on the Raspberry Pi or you can use a 3.3V FTDI adapter to connect a pair of RX/TX pins to the USB port on the RPi.  The GPIO pins connect to /dev/ttyAMA0.  The USB bus connects to /dev/ttyUSB0.  If you use /dev/ttyAMA0, don't forget that you may need to do some reconfiguration to free this serial port. I have found that it is possible to have a program that accesses both serial interfaces at the same time, but I don't have a use case for this scenario.  The example below uses an FTDI adapter to connect the Copernicus II to the USB port on the RPi. The Raspbian distribution should already have the FTDI-SIO driver needed to use /dev/ttyUSB0.


Copernicus II FTDI Adapter (3.3V)
GND           GND
TX-A          RXI
RX-A          TXO

Connect the VCC and GND on the Copernicus II to the 3.3 volt power and ground on the Raspberry Pi.

Python Code

The following code reads the current latitude, longitude, and altitude and prints them to the terminal.  In my experience, the altitude isn't very accurate.

import serial
import struct
import math

ser = serial.Serial("/dev/ttyUSB0", baudrate=38400)
tsip = []
last = ''
start = 0
dle_cnt = 0
id = 0

DLE = '\x10'
ETX = '\x03'

while True:
data =
# Test for data frame start marker DLE (0x10)
if start == 0 and data == DLE:
start = 1
# To avoid confusion, when the frame marker DLE (0x10) occurs in the sequence of data
        # bytes in the data frame, it is doubled.  We need to drop the extra 0x10 by skipping 
# the rest of the loop for the current iteration. Count DLEs to track whether 
# it is even or odd.  The count DLE that marks the end of the frame - before ETX (0x03 
# - is always odd. See p. 122 of the Copernicus II manual.
elif start == 1 and data == DLE:
dle_cnt = dle_cnt + 1
if last == DLE:
# If the last byte was DLE (0x10) and the current byte is not ETX (0x03),
# then the current byte is the packet ID
elif start == 1 and data != ETX and last == DLE:
id = data
# If the current byte is 0x03 (ETX), has come right after DLE (0x10), and the 
# DLE count is odd, we have reached the end of the data frame.
elif start == 1 and data == ETX and last == DLE and dle_cnt % 2 == 1:
dle_cnt = 0
last = ''
tsip.append( data )
# Packet 0x84 has the position data we need.
# See p. 163 of the Copernicus II manual for structure of this packet
if id == '\x84':
# Join bytes into string without added spaces and unpack as big-endian 
# double.  struct.unpack returns a tuple. Our value is in the first 
                        # element.
lat_rad = struct.unpack('>d', "".join(tsip[2:10]))[0]
lat_deg = lat_rad * 180.0 / math.pi
lat_dir = 'N' if lat_deg > 0 else 'S'
long_rad = struct.unpack('>d', "".join(tsip[10:18]))[0]
long_deg = long_rad * 180.0 / math.pi
long_dir = 'E' if long_deg > 0 else 'W'
alt_m    = struct.unpack('>d', "".join(tsip[18:26]))[0]
print "%02.6f %s  %03.6f %s  %4.1fm\n" % (abs(lat_deg), lat_dir, 
abs(long_deg), long_dir, alt_m)
id = 0
tsip = []
start = 0
last = data

Note about the output: The values for latitude and longitude are in degrees.  The value to the left of the decimal point represents whole degrees and the value to the right of the decimal point represents a fraction of a degree. This is different from the values in NMEA sentences, where the value to the left of the decimal point represents whole degree and whole minutes, and the value to the right of the decimal represents a fraction of a minute.

1 comment: