Saturday 16 March 2013

USB (serial) GPS Reader with pyserial, Redis, Ubuntu 12.10 on Beaglebone

Requirements for this script:
Make available: An easy to discover and understand, new-enough, and accurate-enough GPS location to other software

Process:

For this exercise we will write the scripts needed directly to the beaglebone using vim.  That way we may run and debug the scripts directly.  Backing up work is very simple and can be done when needed using SCP. 

Hardware:
  • Beaglebone rev A6, setup using this tutorial
  • Desktop PC running Ubuntu 11.10
  • USB GPS dongle
  • Linksys Nano WiFi Router
  • 4 port USB hub, model unknown
Software on Beaglebone:
  • Ubuntu 12.10
  • Redis
  • easy-install, pip, pyserial
Step One - log into Beaglebone

$ screen /dev/ttyUSB1 115200

Wait a few seconds then press 'enter' 
Login using your credentials ie: 'ubuntu' , 'temppwd'

Step Two - set-up the Beaglebone working environment

$ sudo apt-get install vim

Configure vim to be use-able.  Either SCP your current .vimrc file over to the beaglebone, or create a new one.  I only need a couple of small changes to vim so will create a new one.

cd ~/
mdkir tmp
vim .vimrc

Add these lines to the file.

set tabstop=2
set softtabstop=2
set number
set expandtab
et autoindent
set smarttab
set noswapfile
colorscheme pablo

These settings will shrink the tabs to two spaces.  Give you line numbers.  Make the tabs actually spaces (to avoid indentation errors)  Stop vim from annoying you with .swp files and give you a colorscheme that works OK with python.  Save the file and quit vim.

Step Three - set-up USB Devices

Plug in USB hub. 
Then into the hub plug in the GPS dongle and Router.
***My USB hub had issues (Unable to enumerate USB device) - fixed here.  Then restart Beaglebone

Step Four - use python to access GPS data

We need to find out the serial/USB port that the data will be coming into.

$ ls /dev | less

This give us a readable output of the /dev directory, press 'enter' until we find a result that looks  promising like 'ttyUSB0' then press 'q'.  Depending on your hardware set-up this may be different.

Create a .py file to hold our script, named with it's intended use in mind.

$ vim read_and_store_gps_data.py

Lets create the skeletion of this file, add the following lines:

#!/usr/bin/python
import sys
import serial

print 'hello world'

Save the file and quit vim ':wq!' Now lets test the file to see if everything might work.

$ ./read_and_store_gps_data.py

Great, time to read data from the GPS

We will want to make this script useful for different types of GPS units using different serial ports and baud rates.  Let's leave something to remind us how to use the script.  Replace the line print 'hello world' with:

print 'example usage: ./read_and_store_gps_data.py -baud 4800 -port /dev/ttyUSB0'  

Now that we can use the system args, we will check them and use them to connect to the GPS.
Edit the file to look like this:

  #!/usr/bin/python
  import sys
  import serial
  
  def read_line_from_serial():
    reading = serial_port.readline()
    if reading[:6] == '$GPGGA':    
      print reading
      
  def start_serial_connection(port,baud,timeout):
    try:
      serial_port = serial.Serial(port, baud, timeout=timeout)
    except Exception, e:
      print e
      sys.exit()
      
  def is_a_number(thing):
    try:
      return float(thing)
    except Exception, e:
      print e
      return None
  def start_gps_reader():
    baud = None
    port = None
    timeout = None
  
    args_length = len(sys.argv)
  
   for i in range(0,args_length-1):
      if (i+1) <= args_length:
        if sys.argv[i] == '-baud':
          baud = is_a_number(sys.argv[i+1])
        elif sys.argv[i] == '-timeout':
          timeout = is_a_number(sys.argv[i+1])
        elif sys.argv[i] == '-port':
          port = sys.argv[i+1]
  
    if baud and port and timeout:
      start_serial_connection(baud=baud,port=port,timeout=timeout)
    else:
      print 'example usage: ./read_and_store_gps_data.py -baud 4800 -port /dev
  /ttyUSB0 -timeout 0.5'
  
  start_gps_reader()
  
Now test the file

 $ ./read_and_store_gps_data.py -baud 4800 -port /dev/ttyUSB0 -timeout 0.5

Test the file using some incorrect details to see what happens.  If all is running thus far - we can now extract the useful data from the GPS.  GPS units output strings called NMEA strings.  An excellent reference to these strings is in this link.  For now we only want the GPGGA string.  

Here is a breakdown of the GPGGA string:

NameExample DataDescription
Sentence Identifier$GPGGAGlobal Positioning System Fix Data
Time17083417:08:34 Z
Latitude4124.8963, N41d 24.8963' N or 41d 24' 54" N
Longitude08151.6838, W81d 51.6838' W or 81d 51' 41" W
Fix Quality:
- 0 = Invalid
- 1 = GPS fix
- 2 = DGPS fix
1Data is from a GPS fix
Number of Satellites055 Satellites are in view
Horizontal Dilution of Precision (HDOP)1.5Relative accuracy of horizontal position
Altitude280.2, M280.2 meters above mean sea level
Height of geoid above WGS84 ellipsoid-34.0, M-34.0 meters
Time since last DGPS updateblankNo last update
DGPS reference station idblankNo station id
Checksum*75Used by program to check for transmission errors

Next we break down the string into its parts.  According to the table above.  The string is separated by commas.  We add a method to parse the GPGGA string, and a class to hold our newly found position.   And refactor a little.

   #!/usr/bin/python
   import sys
   import serial
   
   class Position:
    def __init__(self,time,latitude,lat_cardinal,longitude,
                               lon_cardinal,quality,altitude,precision):
      self.time = time
      self.latitude = latitude
      self.lat_cardinal = lat_cardinal
      self.longitude = longitude
      self.lon_cardinal = lon_cardinal
      self.quality = quality
      self.altitude = altitude
      self.precision = precision
    
  def parse_gpgga_string(gpgga_string):
    position = None
    gpgga_parts = gpgga_string.split(',')
    if not len(gpgga_parts) == 15:
      print 'malformed gpgga string'
      return False
    else:
      position = Position(time = gpgga_parts[1],
                      latitude = gpgga_parts[2],
                      lat_cardinal = gpgga_parts[3],
                      longitude = gpgga_parts[4],
                      lon_cardinal = gpgga_parts[5],
                      quality = gpgga_parts[6],
                      altitude = gpgga_parts[9],
                      precision = gpgga_parts[8])
      return position
  
  def get_gpgga_string_from_serial(serial_port):
    reading = serial_port.readline()
    if reading[:6] == '$GPGGA':
      return str(reading)
    else:
      return False
  
  def start_serial_connection(port,baud,timeout):
    try:
      serial_port = serial.Serial(port, baud, timeout=timeout)
      return serial_port
    except Exception, e:
      print e 
      sys.exit()
  
  def is_a_number(thing):
    try:
      return float(thing)
    except Exception, e:
      print e
      return None
   
  def get_serial_connection_with_system_args():
    baud = None
    port = None
    timeout = None
    serial_port = None
  
    args_length = len(sys.argv)
  
    for i in range(0,args_length-1):
      if (i+1) <= args_length:
        if sys.argv[i] == '-baud':
          baud = is_a_number(sys.argv[i+1])
        elif sys.argv[i] == '-timeout':
          timeout = is_a_number(sys.argv[i+1])
        elif sys.argv[i] == '-port':
          port = sys.argv[i+1]
    if baud and port and timeout:
      serial_port = start_serial_connection(baud=baud,
                                                    port=port,timeout=timeout)
    return serial_port
  
  def start_gps_reader():
    serial_port = get_serial_connection_with_system_args()
    if serial_port:
      while True:
        try:
          response = get_gpgga_string_from_serial(serial_port)
          if response:
            position = parse_gpgga_string(str(response))
            print "latitude %s" %  position.latitude
            print "lat cardinal %s" % position.lat_cardinal
            print "longitude %s" %  position.longitude
            print "lon cardinal %s" % position.lon_cardinal
            print "altitude %s" % position.altitude
            print "precision %s" % position.precision
            print "quality %s" % position.quality
        except Exception, e:
          print e
    else:
      print 'example usage: ./read_and_store_gps_data.py -baud 4800 -port /dev/ttyUSB0 -timeout 0.5'
      sys.exit()
  
 start_gps_reader()

Test the script, and see we now have position objects with useful attributes.

Step Five - use redis to make the latest position available

Why Redis? I'm doing this anyway - and have been hearing about redis all the time.  This script will only set the latest position.  The other parts of the solution will use the position.  Add to the script at the top:

import redis
import json

Now to use redis to store a position object as json, we only want a very recent position so we will set the time to live to 3 seconds. Directly underneath the start_gps_read() declaration insert the line:

redis_server = redis.Redis("localhost")

Replace the printing of the positions values in start_gps_reader() with the following lines:

redis.set("position:current",json.dumps(position.__dict__))
redis.expire("position:current",3)

Now exit vim and test the scripts' results


$ ./read_and_store_gps_data.py -baud 4800 -port /dev/ttyUSB0 -timeout 0.5 &
$ redis-cli
redis 127.0.0.1:6379> get position:current

$ ps-aux **look for the python process take note of its number
$ kill 1088 **where 1088 is the python processes number

Wait three seconds.

redis-cli
get position:current

Great there is only a 3 seconds or newer position available as position:current,  Thats all for this post, if you are interested to see what comes next then come back.

Thanks! Rimu Boddy.

No comments:

Post a Comment