# GPS Display # Peter Hiscocks, Syscomp Electronic Design # phiscock@ee.ryerson.ca # 11 June 2010 # This program displays data from a Pharos GPS-500 hardware GPS unit, which # was obtained from a Microsoft 'Streets and Trips' software package. # It receives NMEA formatted messages and displays the information in Tk # widgets. # The code shown here is intended to be simple and easy to understand. # It's not terribly robust. There is no checksum calculation on the # sentences. It has no capability of switching to different USB ports or # being autodetected on a USB port. # This code should run on a Linux, Windows or Mac system. # To run this code, first ensure that the Tcl/Tk interpreter is installed on # your computer. If it's a Linux system, that's probably true. In any case, # you can download and install Tcl/Tk from the ActiveState web site: # http://www.activestate.com/activetcl The download is free. # Linux # On a Linux machine, plug in the GPS hardware and use the command 'dmesg' # to verify that the devices is assigned to the serial port '/dev/ttyUSB0'. # If that's not the case, either unplug other GPS devices or modify the # source code to match the assignment. In a terminal window type 'wish # gps-display.tcl' and the program should start. # Windows # On a Windows machine, plug in the GPS hardware and check device manager # (under Ports:COM and LPT) an ensure that the device is assigned to COM3. # If that's not the case, use the com port properties to change the # assignment to COM3 or modify the source code to point at the assigned # port. # Once the ActiveState Tcl interpreter is downloaded, any file ending in # '.tcl' should be associated with the Tcl interpreter. Then you can start # the program by double-clicking on the source code icon. Alternatively, # start the Tcl/Tk interpreter by double-clicking on the 'wish' icon. A # terminal window will appear. In that window, type 'source # gps-display.tcl'. The program should start. # Mac # The code has not been tested on a Mac but it should run. Ensure the Tcl # interpreter is downloaded and installed. Plug in the hardware. Determine # the port that the hardware is assigned to, which might be something like # /dev/cua0. Using the Linux and Windows port assignment code as a guide, # modify the source code to add detection of the port on a Mac. You could # also look at the source code for the Syscomp DVM-101 voltmeter to see how # that is done. # References # http://www.circuitcellar.com/library/print/0899/Cyliax109/2.htm # http://www.gpsinformation.org/dale/nmea.htm#GGA # http://www.developertutorials.com/tutorials/php/server-side-scripting-language-1411/ #--------------------------------------------------------------- # Preliminaries #======================================================================= # Variable Definitions #======================================================================= # Operating system: unix, windows, Darwin global osType set osType $tcl_platform(platform) # Handle of the serial port global portHandle # Uncomment the next line when testing for port detection # set osType "Darwin" #======================================================================= # Procedure Definitions #======================================================================= #------------------------------------------------------------------ # Gather Input Line # This procedure collects the input message as a single line. # For the 'while' loop see Welch, 4th ed, page 120. # It then calls the parser. # The first line is garbage, which is not thought to be fatal. #------------------------------------------------------------------ proc processInput {} { global portHandle global nmeaSentence while {[gets $portHandle nmeaSentence] >= 0} { puts "Input Line: $nmeaSentence" parseSentence } } #------------------------------------------------------------------ # Parse Sentence # This procedure parses a complete NMEA sentence into variables. # An example sentence: # $GPGGA,012614.000,4340.4216,N,07920.9167,W,1,07,1.1,120.3,M,-35.1,M,,0000*64 # Complete decoding information is here: # http://www.gpsinformation.org/dale/nmea.htm#GGA # For the method of decomposing a CSV (comma separated variables) string # into its consituents, see: # https://www.ibm.com/developerworks/library/l-script-survey/#h6 # For example, if 'last' is a sentence with CSV variables: # set fieldlist [split $last ,] # set a [lindex $fieldlist 0] # set b [lindex $fieldlist 1] # set c [lindex $fieldlist 2] # set d [lindex $fieldlist 3] # Then 'a', 'b' and so on contain the variables. #------------------------------------------------------------------ proc parseSentence {} { global nmeaSentence global sentenceList global UTC global latitude global dirlat global longitude global dirLong global totalSats global altitude global speedkts global trackAngle global date global magVar puts "nmeaSentence final: $nmeaSentence" # Trim away the leading $ sign to avoid confusion with a Tcl variable set nmeaSentence [string trimleft $nmeaSentence \$] puts "trimmed string; $nmeaSentence" # Ensure that the checksum '*' is preceeded by a comma so that it is # a separate item (the last item) when made into a list set nmeaSentence [string map {* ,*} $nmeaSentence] puts "checksum separated string: $nmeaSentence" # Here we parse the NMEA sentence into a series of variables in a list set sentenceList [split $nmeaSentence ,] # Remove the last item in the list, which is the checksum set sentenceList [lreplace $sentenceList end end] puts "sentenceList: $sentenceList" # The sentence name is something like 'GPGGA', it is the zeroth item in the # list set sentenceName [lindex $sentenceList 0] # The sentence name determines how the NMEA sentence is processed into variables switch $sentenceName { GPGGA {puts "found GPGGA" set UTC [lindex $sentenceList 1] puts "UTC Time $UTC" set latitude [lindex $sentenceList 2] puts "Latitude: $latitude" set dirLat [lindex $sentenceList 3] puts "dirLat: $dirLat" set longitude [lindex $sentenceList 4] puts "Longitude: $longitude" set dirLong [lindex $sentenceList 5] puts "dirLong: $dirLong" set totalSats [lindex $sentenceList 7] puts "totalSats: $totalSats" set altitude [lindex $sentenceList 9] puts "altitude: $altitude" } GPRMC {puts "found GPRMC" set speedkts [lindex $sentenceList 7] puts "Speed, kt: $speedkts" set trackAngle [lindex $sentenceList 8] puts "Track Angle: $trackAngle" set date [lindex $sentenceList 9] puts "UTC Date (DMY): $date" set magVar [lindex $sentenceList 10] puts "Magnetic Variation: $magVar" } GPGSV {puts "found GPGSV" showSats} } } #-------------------------------------------------------------------------------- # Show Satellites # This procedure extracts the satellite data from a GPGSV message, and # displays it. # GSV messages are contained in multiple sentences. # Satellites 1 through 4 are in sentence 1, sats 5 through 8 in sentence 2 # and sats 9 through 12 in sentence 3. proc showSats {} { global sentenceList global sat1Prn global sat1Elevation global sat1Azimuth global sat1SNR global sat2Prn global sat2Elevation global sat2Azimuth global sat2SNR global sat3Prn global sat3Elevation global sat3Azimuth global sat3SNR global sat4Prn global sat4Elevation global sat4Azimuth global sat4SNR global sat5Prn global sat5Elevation global sat5Azimuth global sat5SNR global sat6Prn global sat6Elevation global sat6Azimuth global sat6SNR global sat7Prn global sat7Elevation global sat7Azimuth global sat7SNR global sat8Prn global sat8Elevation global sat8Azimuth global sat8SNR global sat9Prn global sat9Elevation global sat9Azimuth global sat9SNR global sat10Prn global sat10Elevation global sat10Azimuth global sat10SNR global sat11Prn global sat11Elevation global sat11Azimuth global sat11SNR global sat12Prn global sat12Elevation global sat12Azimuth global sat12SNR # The satellite data is in multiple sentences, we assume 1 to 3. # This line extracts the number of the current sentence. set sentenceNum [lindex $sentenceList 2] switch $sentenceNum { 1 { set sat1Prn [lindex $sentenceList 4] set sat1Elevation [lindex $sentenceList 5] set sat1Azimuth [lindex $sentenceList 6] set sat1SNR [lindex $sentenceList 7] set sat2Prn [lindex $sentenceList 8] set sat2Elevation [lindex $sentenceList 9] set sat2Azimuth [lindex $sentenceList 10] set sat2SNR [lindex $sentenceList 11] set sat3Prn [lindex $sentenceList 12] set sat3Elevation [lindex $sentenceList 13] set sat3Azimuth [lindex $sentenceList 14] set sat3SNR [lindex $sentenceList 15] set sat4Prn [lindex $sentenceList 16] set sat4Elevation [lindex $sentenceList 17] set sat4Azimuth [lindex $sentenceList 18] set sat4SNR [lindex $sentenceList 19] } 2 { set sat5Prn [lindex $sentenceList 4] set sat5Elevation [lindex $sentenceList 5] set sat5Azimuth [lindex $sentenceList 6] set sat5SNR [lindex $sentenceList 7] set sat6Prn [lindex $sentenceList 8] set sat6Elevation [lindex $sentenceList 9] set sat6Azimuth [lindex $sentenceList 10] set sat6SNR [lindex $sentenceList 11] set sat7Prn [lindex $sentenceList 12] set sat7Elevation [lindex $sentenceList 13] set sat7Azimuth [lindex $sentenceList 14] set sat7SNR [lindex $sentenceList 15] set sat8Prn [lindex $sentenceList 16] set sat8Elevation [lindex $sentenceList 17] set sat8Azimuth [lindex $sentenceList 18] set sat8SNR [lindex $sentenceList 19] } 3 { set sat9Prn [lindex $sentenceList 4] set sat9Elevation [lindex $sentenceList 5] set sat9Azimuth [lindex $sentenceList 6] set sat9SNR [lindex $sentenceList 7] set sat10Prn [lindex $sentenceList 8] set sat10Elevation [lindex $sentenceList 9] set sat10Azimuth [lindex $sentenceList 10] set sat10SNR [lindex $sentenceList 11] set sat11Prn [lindex $sentenceList 12] set sat11Elevation [lindex $sentenceList 13] set sat11Azimuth [lindex $sentenceList 14] set sat11SNR [lindex $sentenceList 15] set sat12Prn [lindex $sentenceList 16] set sat12Elevation [lindex $sentenceList 17] set sat12Azimuth [lindex $sentenceList 18] set sat12SNR [lindex $sentenceList 19] } } } #----------------- #Close Serial Port #----------------- #This procedure closes any USB serial port currently open as #"portHandle". proc closeSerialPort {} { global portHandle variable serialStatus if {$portHandle != "stdout"} { catch { [close $portHandle]} } set portHandle stdout } #======================================================================= # GUI Definitions #======================================================================= # A new display window is required if this program is to play nice with an existing Tcl # window. Otherwise, it can use the default Tk window. # For a new display window use the following instructions to create a # toplevel window .gps-display. # Then the frame moves down one level to .gps-display.container, and so on. # Create the display window # toplevel .gps-display # wm resizable . 0 0 # wm title . "GPS Display" # wm protocol .measurements WM_DELETE_WINDOW {destroy .gps-display} #---------------------------------------------------------------------- # Create the Summary Window # # This window contains the summary data for the GPS user: lat, long, UTC etc # #---------------------------------------------------------------------- #In this case, we used the default Tk window. #Create a frame for the auto measurements frame .container \ -relief groove \ -borderwidth 2 #Set up the labels and readouts #------------------------------ #The labels must come *before* the grid description. #Fill in the grid labels label .container.latlabel \ -text "Latitude" label .container.longlabel \ -text "Longitude" label .container.altlabel \ -text "Altitude, metres" \ -width 16 label .container.spdlabel \ -text "Speed, Kts" \ -width 12 label .container.tracklabel \ -text "Track Angle" \ -width 12 label .container.datelabel \ -text "Date, DMY" \ -width 12 label .container.utclabel \ -text "Time, UTC" \ -width 12 label .container.numsatlabel \ -text "Num Satellites" \ -width 12 # Fill in the measurements label .container.latresult \ -relief sunken \ -textvariable latitude \ -width 12 label .container.longresult \ -relief sunken \ -textvariable longitude \ -width 12 label .container.altresult \ -relief sunken \ -textvariable altitude \ -width 12 label .container.spdresult \ -relief sunken \ -textvariable speedkts\ -width 12 label .container.trackresult \ -relief sunken \ -textvariable trackAngle \ -width 12 label .container.dateresult\ -relief sunken \ -textvariable date \ -width 12 label .container.utcresult \ -relief sunken \ -textvariable UTC \ -width 12 label .container.numsatresult \ -relief sunken \ -textvariable totalSats \ -width 12 #Latitude grid .container.latlabel -row 1 -column 1 grid .container.latresult -row 1 -column 2 #Longitude grid .container.longlabel -row 2 -column 1 grid .container.longresult -row 2 -column 2 #Altitude grid .container.altlabel -row 3 -column 1 grid .container.altresult -row 3 -column 2 #Speed, knots grid .container.spdlabel -row 4 -column 1 grid .container.spdresult -row 4 -column 2 #Track Angle grid .container.tracklabel -row 5 -column 1 grid .container.trackresult -row 5 -column 2 #Date grid .container.datelabel -row 6 -column 1 grid .container.dateresult -row 6 -column 2 #Time, UTC grid .container.utclabel -row 7 -column 1 grid .container.utcresult -row 7 -column 2 #Number of Satellites grid .container.numsatlabel -row 8 -column 1 grid .container.numsatresult -row 8 -column 2 # Show the GUI pack .container #---------------------------------------------------------------------- # Create the Satellite Window # # This window contains the data for individual satellites in view. # Each satellite entry the 'Prn number, elevation and azimuth and signal # strength'. # There may be more satellites listed here than in the summary data because # some satellite data is not used. # # The maximum possible number of satellites is assumed to be 12. # #---------------------------------------------------------------------- #In this case, we used the default Tk window. #Create a frame for the auto measurements frame .satdat \ -relief groove \ -borderwidth 2 #Set up the labels and readouts #------------------------------ #The labels must come *before* the grid description. #Fill in the grid labels label .satdat.prnTitle \ -text "PRN" label .satdat.elevTitle \ -text "Elevation, Deg" label .satdat.azTitle \ -text "Azimuth, Deg" label .satdat.sNRTitle \ -text "S/N Ratio" label .satdat.sat1 \ -text "1" \ -width 5 label .satdat.sat2 \ -text "2" label .satdat.sat3 \ -text "3" label .satdat.sat4 \ -text "4" label .satdat.sat5 \ -text "5" label .satdat.sat6 \ -text "6" label .satdat.sat7 \ -text "7" label .satdat.sat8 \ -text "8" label .satdat.sat9 \ -text "9" label .satdat.sat10 \ -text "10" label .satdat.sat11 \ -text "11" label .satdat.sat12 \ -text "12" # Attach the satellite data to GUI labels label .satdat.sat1Prn \ -relief sunken \ -textvariable sat1Prn \ -width 12 label .satdat.sat1Elevation \ -relief sunken \ -textvariable sat1Elevation \ -width 12 label .satdat.sat1Azimuth \ -relief sunken \ -textvariable sat1Azimuth \ -width 12 label .satdat.sat1SNR \ -relief sunken \ -textvariable sat1SNR\ -width 12 label .satdat.sat2Prn \ -relief sunken \ -textvariable sat2Prn \ -width 12 label .satdat.sat2Elevation \ -relief sunken \ -textvariable sat2Elevation \ -width 12 label .satdat.sat2Azimuth \ -relief sunken \ -textvariable sat2Azimuth \ -width 12 label .satdat.sat2SNR \ -relief sunken \ -textvariable sat2SNR\ -width 12 label .satdat.sat3Prn \ -relief sunken \ -textvariable sat3Prn \ -width 12 label .satdat.sat3Elevation \ -relief sunken \ -textvariable sat3Elevation \ -width 12 label .satdat.sat3Azimuth \ -relief sunken \ -textvariable sat3Azimuth \ -width 12 label .satdat.sat3SNR \ -relief sunken \ -textvariable sat3SNR\ -width 12 label .satdat.sat4Prn \ -relief sunken \ -textvariable sat4Prn \ -width 12 label .satdat.sat4Elevation \ -relief sunken \ -textvariable sat4Elevation \ -width 12 label .satdat.sat4Azimuth \ -relief sunken \ -textvariable sat4Azimuth \ -width 12 label .satdat.sat4SNR \ -relief sunken \ -textvariable sat4SNR\ -width 12 label .satdat.sat5Prn \ -relief sunken \ -textvariable sat5Prn \ -width 12 label .satdat.sat5Elevation \ -relief sunken \ -textvariable sat5Elevation \ -width 12 label .satdat.sat5Azimuth \ -relief sunken \ -textvariable sat5Azimuth \ -width 12 label .satdat.sat5SNR \ -relief sunken \ -textvariable sat5SNR\ -width 12 label .satdat.sat6Prn \ -relief sunken \ -textvariable sat6Prn \ -width 12 label .satdat.sat6Elevation \ -relief sunken \ -textvariable sat6Elevation \ -width 12 label .satdat.sat6Azimuth \ -relief sunken \ -textvariable sat6Azimuth \ -width 12 label .satdat.sat6SNR \ -relief sunken \ -textvariable sat6SNR\ -width 12 label .satdat.sat7Prn \ -relief sunken \ -textvariable sat7Prn \ -width 12 label .satdat.sat7Elevation \ -relief sunken \ -textvariable sat7Elevation \ -width 12 label .satdat.sat7Azimuth \ -relief sunken \ -textvariable sat7Azimuth \ -width 12 label .satdat.sat7SNR \ -relief sunken \ -textvariable sat7SNR\ -width 12 label .satdat.sat8Prn \ -relief sunken \ -textvariable sat8Prn \ -width 12 label .satdat.sat8Elevation \ -relief sunken \ -textvariable sat8Elevation \ -width 12 label .satdat.sat8Azimuth \ -relief sunken \ -textvariable sat8Azimuth \ -width 12 label .satdat.sat8SNR \ -relief sunken \ -textvariable sat8SNR\ -width 12 label .satdat.sat9Prn \ -relief sunken \ -textvariable sat9Prn \ -width 12 label .satdat.sat9Elevation \ -relief sunken \ -textvariable sat9Elevation \ -width 12 label .satdat.sat9Azimuth \ -relief sunken \ -textvariable sat9Azimuth \ -width 12 label .satdat.sat9SNR \ -relief sunken \ -textvariable sat9SNR\ -width 12 label .satdat.sat10Prn \ -relief sunken \ -textvariable sat10Prn \ -width 12 label .satdat.sat10Elevation \ -relief sunken \ -textvariable sat10Elevation \ -width 12 label .satdat.sat10Azimuth \ -relief sunken \ -textvariable sat10Azimuth \ -width 12 label .satdat.sat10SNR \ -relief sunken \ -textvariable sat10SNR\ -width 12 label .satdat.sat11Prn \ -relief sunken \ -textvariable sat11Prn \ -width 12 label .satdat.sat11Elevation \ -relief sunken \ -textvariable sat11Elevation \ -width 12 label .satdat.sat11Azimuth \ -relief sunken \ -textvariable sat11Azimuth \ -width 12 label .satdat.sat11SNR \ -relief sunken \ -textvariable sat11SNR\ -width 12 label .satdat.sat12Prn \ -relief sunken \ -textvariable sat12Prn \ -width 12 label .satdat.sat12Elevation \ -relief sunken \ -textvariable sat12Elevation \ -width 12 label .satdat.sat12Azimuth \ -relief sunken \ -textvariable sat12Azimuth \ -width 12 label .satdat.sat12SNR \ -relief sunken \ -textvariable sat12SNR\ -width 12 # Fill in the grid labels # Titles at the top of the grid grid .satdat.prnTitle -row 0 -column 2 grid .satdat.elevTitle -row 0 -column 3 grid .satdat.azTitle -row 0 -column 4 grid .satdat.sNRTitle -row 0 -column 5 # Now the satellite data # There is probably a much more compact and sane way to do this with a loop grid .satdat.sat1 -row 1 -column 1 grid .satdat.sat1Prn -row 1 -column 2 grid .satdat.sat1Elevation -row 1 -column 3 grid .satdat.sat1Azimuth -row 1 -column 4 grid .satdat.sat1SNR -row 1 -column 5 grid .satdat.sat2 -row 2 -column 1 grid .satdat.sat2Prn -row 2 -column 2 grid .satdat.sat2Elevation -row 2 -column 3 grid .satdat.sat2Azimuth -row 2 -column 4 grid .satdat.sat2SNR -row 2 -column 5 grid .satdat.sat3 -row 3 -column 1 grid .satdat.sat3Prn -row 3 -column 2 grid .satdat.sat3Elevation -row 3 -column 3 grid .satdat.sat3Azimuth -row 3 -column 4 grid .satdat.sat3SNR -row 3 -column 5 grid .satdat.sat4 -row 4 -column 1 grid .satdat.sat4Prn -row 4 -column 2 grid .satdat.sat4Elevation -row 4 -column 3 grid .satdat.sat4Azimuth -row 4 -column 4 grid .satdat.sat4SNR -row 4 -column 5 grid .satdat.sat5 -row 5 -column 1 grid .satdat.sat5Prn -row 5 -column 2 grid .satdat.sat5Elevation -row 5 -column 3 grid .satdat.sat5Azimuth -row 5 -column 4 grid .satdat.sat5SNR -row 5 -column 5 grid .satdat.sat6 -row 6 -column 1 grid .satdat.sat6Prn -row 6 -column 2 grid .satdat.sat6Elevation -row 6 -column 3 grid .satdat.sat6Azimuth -row 6 -column 4 grid .satdat.sat6SNR -row 6 -column 5 grid .satdat.sat7 -row 7 -column 1 grid .satdat.sat7Prn -row 7 -column 2 grid .satdat.sat7Elevation -row 7 -column 3 grid .satdat.sat7Azimuth -row 7 -column 4 grid .satdat.sat7SNR -row 7 -column 5 grid .satdat.sat8 -row 8 -column 1 grid .satdat.sat8Prn -row 8 -column 2 grid .satdat.sat8Elevation -row 8 -column 3 grid .satdat.sat8Azimuth -row 8 -column 4 grid .satdat.sat8SNR -row 8 -column 5 grid .satdat.sat9 -row 9 -column 1 grid .satdat.sat9Prn -row 9 -column 2 grid .satdat.sat9Elevation -row 9 -column 3 grid .satdat.sat9Azimuth -row 9 -column 4 grid .satdat.sat9SNR -row 9 -column 5 grid .satdat.sat10 -row 10 -column 1 grid .satdat.sat10Prn -row 10 -column 2 grid .satdat.sat10Elevation -row 10 -column 3 grid .satdat.sat10Azimuth -row 10 -column 4 grid .satdat.sat10SNR -row 10 -column 5 grid .satdat.sat11 -row 11 -column 1 grid .satdat.sat11Prn -row 11 -column 2 grid .satdat.sat11Elevation -row 11 -column 3 grid .satdat.sat11Azimuth -row 11 -column 4 grid .satdat.sat11SNR -row 11 -column 5 grid .satdat.sat12 -row 12 -column 1 grid .satdat.sat12Prn -row 12 -column 2 grid .satdat.sat12Elevation -row 12 -column 3 grid .satdat.sat12Azimuth -row 12 -column 4 grid .satdat.sat12SNR -row 12 -column 5 # Show the GUI pack .satdat #======================================================================= # Program Starts Here #======================================================================= #----------------------------------------------------------------------- # Open the serial port #----------------------------------------------------------------------- # If the operating system is Linux, the port is assumed to be /dev/ttyUSB0, # which will be the case if the hardware GPS is the first USB device to be # plugged in. This can be verified using 'dmesg'. # If the operating system is Windows, the port is assumed to be COM3. # In all cases the port format is 4800,8,N,1 # Once opened, the port can be accessed through the global variable # 'portHandle'. #Attempt to open the serial port puts $osType if {$osType=="unix"} then { puts "Unix (Linux) operating system" set serialPort "/dev/ttyUSB0" puts $serialPort } elseif {$osType=="windows"} { puts "Windows operating system" set serialPort "COM3" puts $serialPort } elseif {$osType=="Darwin"} { puts "Darwin (Mac OSX) operating system" set $serialPort "/dev/cua0" puts "Serial Port: $serialPort" puts "This might not work, Mac not tested yet" } else { puts "Unknown Operating System" } if { [catch {set portHandle [open $serialPort r+]} result] } { puts "Unable to open serial port!" } else { #Configure the serial port if {$osType!="Darwin"} { fconfigure $portHandle \ -mode 4800,n,8,1 \ -blocking 0 \ -buffering none \ -encoding binary \ -translation {binary lf} puts "Opened Linux or Windows serial port" } else { exec stty -f $usbSerial::serialPort 4800 cs8 -parenb -cstopb fconfigure $portHandle \ -blocking 0 \ -buffering none \ -encoding binary \ -translation {binary lf} puts "Opened Darwin serial port" } } #Set Up Fileevent #----------------- #This code initializes the fileevent handler for data received from the #instrument. Every time code arrives at the serial port, this routine is #triggered. It then calls the procedure to inhale the sentence string, which #in turn calls the parser. global portHandle fileevent $portHandle readable { processInput }