A project that will consume all your time and money, but make you the envy of your nerd friends.
If you have not experienced driving with GPS navigation, you are missing out on a great experience. It does not make much difference for your daily commute, but when you are lost or just exploring a new area, a GPS navigation system can make a world of difference.
Right now, there are a number of ways to get GPS navigation in your car. On the cheap end of the scale, you can buy a handheld outdoor GPS like those made by Garmin or Magellan. For a little more money, there are units that sit on your dashboard and have a larger screen and more storage space for street data. If you have a laptop or PDA already, you can connect an external GPS to it and run mapping software that will track your position and provide driving directions. Finally, top-of-the-line navigation systems come as an option in some new cars and sometimes can be purchased in the form of aftermarket parts.
Of all these options, only two are easily hackable: the PDA and the laptop. A PDA is a convenient size but is difficult to see while driving. The laptop approach has a few drawbacks in the car, namely the difficulty of finding a good location for the laptop, cables getting tangled, potential for theft, and poor visibility in direct sunlight. On the positive side, a laptop has an enormous amount of power and storage and is highly configurable for navigation purposes. See Figure 5-38 for an example of navigation software running on a laptop.
Figure 5-38. Delorme Atlas USA running on a laptop with a GPS
In order to overcome the shortcomings of a laptop, people have learned to build small PCs into their cars. This impressive hack generally involves a small, low-power motherboard along with a small daylight-viewable LCD (6 or 7 inches) running one of the many navigation software packages available. For extra credit, you can mount the screen into your dashboard. Figure 5-39 shows a typical system diagram of a car computer.
Figure 5-39. A typical system diagram for a car computer
Running a computer in your car opens up a world of possibilities that extend beyond basic navigation. One popular application involves creating a mobile multimedia system. A connection to the car stereo allows for MP3 playback, and an LCD on the dashboard can display live television, DVDs, or any other supported video formats. Wireless cards are also common and allow wardriving [Hack #17], Internet surfing, and easy media changes when an access point is in range. Another use of a car computer involves a custom cable to interface with the OBDII (On Board Diagnostics System) port on more recent cars. This link allows the car computer to access operational and diagnostic information collected by the car's internal electronics.
One interesting concept discussed on a car-computer message board shows how customizable these systems are. The project involved a GPS, various video cameras, and engine-monitoring software, all set up inside a racing car. During a race, the driver is able to record all aspects of the car's position, the surrounding track, and the status of various vehicle systems. This information can later be analyzed in order to improve driving style or tweak aspects of the car.
The process of building a car computer can easily consume a large amount of time and money. Common pitfalls include accidental damage to hardware, accessories that end up not working well, and the ultimate bad luck: theft. Fortunately, during the process you are bound to expand your knowledge in several technical areas that will be invaluable in the future. You will also gain respect from your techie friends.
A key component of any car computer is the motherboard. In the early days of hobby car computers, the only option was full-sized ATX machines that consumed lots of power and required a good deal of space. Today, life is easier, thanks to a new breed of tiny low-power and low-heat motherboards. The company that has done the most in this area is VIA, with their line of Mini-ITX EPIA motherboards. This line's boards all measure 170x170 mm and commonly contain a fan-less CPU with onboard VGA, TV-out, sound, and LAN. Other accessories can be connected to a single PCI slot, or by using USB, serial, or parallel ports. Figure 5-40 shows a VIA EPIA ME 6000 motherboard alongside an Opus DC-DC power supply designed for automotive applications.
Figure 5-40. EPIA ME600 motherboard and Opus power supply (Courtesy of Nico Voss, http://www.mini-itx.com/projects/bmwpc/)
Figure 5-41. Seven-inch Lilliput LCD mounted in a Volvo
Probably the second most important part of any car computer is the display. In the last few years, online retailers and eBay sellers have started offering inexpensive seven-inch LCD screens that accept VGA and include a touch screen input (see Figure 5-41). These screens are brighter than laptop and desktop screens and often fit in the dashboard, simply by removing the factory stereo. Although these screens are getting better, many of today's models suffer from less-than-perfect reliability and quality control problems.
Once you nail down a hardware blueprint, you can start thinking about software. The easiest route is to run Windows with an off-the-shelf navigation package. Popular titles include Delorme Street Atlas, Microsoft Streets & Trips, and CoPilot by ALK Technologies. For the more adventurous, Linux makes for a stable and easily hackable platform. The major drawback of Linux is the lack of mapping software that comes with high-quality maps. This will likely change in the future, so keep an eye out for emerging projects.
Regardless of what operating system you choose, you can always have fun writing custom code to integrate with your mobile computer. When starting on a project like this, some of the newer high-level languages can make for a rapid and painless development experience. Python is a perfect example of this and, as you will see in the following examples, can be used to build a visual GPS plotter with only a few pages of code.
5.18.1. Connecting a GPS to Your Car Computer
If you have access to a GPS with a serial interface, it is quite easy to extract latitude and longitude into your computer. Just about all GPS devices support a protocol called NMEA-0183 (National Marine Electronics Association), which is streamed over the serial port at 4800 baud. The NMEA-0183 protocol is text-based and quite easy to parse. You can use the following Python code to connect with a GPS or explore gpsd, which provides a server interface to the GPS. gpsd is described in [Hack #57]. NMEA consists of various sentences with data fields separated by commas. Each line of a NMEA sentence starts with a $ and a sentence type. Each sentence type provides different information in the fields that follow the sentence-type designator. Here is what a typical stream looks like:
$GPVTG,269.6,T,250.8,M,019.9,N,0036.8,K*76 $GPRMC,011106,A,4742.1321,N,12221.2108,W,021.0,270.5,240804,018.9,E*67 $GPGGA,011106,4742.1321,N,12221.2108,W,1,10,1.3,115.5,M,-18.4,M,,*79 $GPGSA,A,3,07,08,10,11,,19,26,27,28,29,31,,1.8,1.3,1.3*3F $GPGSV,3,1,11,07,19,190,41,08,73,092,47,10,09,252,46,11,12,101,37*75 $GPGSV,3,2,11,13,01,161,,19,26,047,43,26,34,308,44,27,47,113,44*78 $GPGSV,3,3,11,28,69,254,49,29,42,299,45,31,32,180,35,,,,*40
Contained in these sentences is information including latitude, longitude, altitude, speed, heading, time, signal strength, satellite positions, positional error, and tons of other information. For a complete list, check out "The NMEA FAQ" written by Peter Bennett and available online at http://vancouver-webpages.com/pub/peter/nmeafaq.txt.
For our first example, we want to find the current position in latitude and longitude. This information is present in the $GPGGA sentences. We are interested in the second through fourth fields, which contain the information 4742.1321,N,12221.2108,W. This tells us that our position is 47 degrees and 42.1321 minutes north, and 122 degrees and 21.2108 minutes west. Unfortunately, providing separate values for degrees, minutes, and direction is not the preferred format. The following Python code takes a position in NMEA format as its input and then returns a decimal position, such as (47.702201, -122.3535):
def NMEA_to_DecDeg(nmea_coord, direction): """Convert a NMEA style coordinate into decimal degrees Keyword arguments: nmea_coord -- numeric part of a NMEA coordinate direction -- a character indicating [N]orth, [S]outh, [E]ast, [W]est """ sign = 1.0 # if we are west or south, the answer should be negative if (direction in ["S", "W"]): sign = -1.0 nmea_coord = abs(nmea_coord) # strip the input sign deg = math.floor(nmea_coord/100) # extract the degrees minutes = nmea_coord - (deg*100) # extract the minutes ret_value = deg + (minutes/60.0) # re-combine degrees and minutes return ret_value * sign # re-apply the sign
With a little more code, we can capture NMEA sentences from a serial port and display our latitude and longitude in real time:
def parse_NMEA_GPGGA(input_sentence): """Extract latitude and longitude from a NMEA $GPGGA string Keyword arguments: input_sentence -- string containing entire NMEA $GPGGA sentence """ lat_value, lat_dir, lon_value, lon_dir = string.split(input_sentence, ",")[2:6] lat = NMEA_to_DecDeg(float(lat_value), lat_dir) lon = NMEA_to_DecDeg(float(lon_value), lon_dir) return (lat, lon) def parse_NMEA_file(fp): """Parse a file containing NMEA sentences and extract positions Input is an open file handle""" #fp = open(source_name) position_list = [ ] while 1: line = fp.readline( ) if not line: break # check to see if this is a GGA sentence, which # contains a Lat/Lon position if(line[0:6] = = "$GPGGA"): # display the latitude / longitude position = parse_NMEA_GPGGA(line) print "Position: ", position , ",", position position_list.append(position) return position_list import serial fp = serial.Serial("COM3", baudrate=4800) parse_NMEA_file(fp)
This code requires a Python module called pySerial. This module is available at http://pyserial.sourceforge.net/.
5.18.2. Displaying GPS Data in a GUI Application
Text-based mapping software is not very exciting. A more useful approach is to render map data onto a 2-D window. In the following example, we will see how to build a cross-platform Python application that uses the Tkinter GUI library to display GPS data, with the ability to zoom and pan. Figure 5-42 shows this application displaying a captured GPS session along with Wi-Fi locations located using NetStumbler (a free wireless-network sniffer for Windows). You can also use this program to display waypoints captured on a hike, general points of interest, geocaches, and so on.
Figure 5-42. Our sample application displaying a GPS log and Wi-Fi hotspots in the area
It is easy to be intimidated by mapping software, but it is really quite simple after you master coordinate conversions. There are two coordinate planes involved in our application: the latitude/longitude plane and the x/y pixel plane on our GUI drawing surface. Real-world coordinates are bounded by (-90,-180) and (90,180), whereas GUI surfaces are bounded by (0,0) and (width,height). Translating between these is just a matter of scaling and shifting coordinate values. Since we need the ability to zoom and pan in our application, we must introduce an intermediate step to crop our world plane to fit a smaller viewing area. The complete coordinate conversion process is demonstrated in Figure 5-43.
Figure 5-43. Visual representation of coordinate conversion
The following Python class encapsulates all the work necessary to reproject coordinates in our application. The class needs to know the width and height of the output window upon creation. Internally, the class stores the latitude and longitude at the center of its real-world map along with a zoom variable that describes how large of a section to display. The zoom value is actually the distance from the center of the map to the left or right edge. Because of this, a large zoom value will display the entire world, and a small value will show a close-up map:
class MapCoords: """Translate between Lat/Lon coordinates and screen x,y positions""" def _ _init_ _(self, in_width, in_height): self.width = in_width; self.height = in_height; self.zoom = 0.02 self.ratio = float(self.height) / float(self.width) # place your starting position here: self.set_center(47.7000427246 , -122.351867676) def set_center(self, new_lat, new_lon): self.lat = new_lat self.lon = new_lon def zoom_in(self): self.zoom /= 2.0 def zoom_out(self): self.zoom *= 2.0 def latlon_to_pixels(self, (in_lat, in_lon)): x = math.floor( ( in_lon - (self.lon - (self.zoom / 2.0) ) ) / ( self.zoom ) * self.width ) y = math.floor( ( in_lat - (self.lat - (self.zoom * self.ratio / 2.0) ) ) / ( self.zoom * self.ratio ) * self.height ) return (x, y) def pixels_to_latlon(self, (in_x, in_y)): lon = (float(in_x)/self.width) * self.zoom + (self.lon - self.zoom/2.0) lat = (float(in_y)/self.height) * self.zoom * self.ratio + (self.lat - self.zoom * self.ratio / 2.0) return (lat, lon)
Once this class is instantiated, we can pan or zoom the map, as well as convert back and forth between lat/lon and screen pixels.
The next step is to create a few classes to store and draw our map data. The Waypoint class allows us to parse a CSV file and display waypoints with a red dot and a text label. The GpsTrack class parses and displays a file containing NMEA sentences. The GPS log is rendered as a multisegment line with blue dots on the intersections. The result is a smooth path showing our route history. Both of these classes have a draw() method that converts each data point into a pixel x,y location and then renders the object onto a Tkinter canvas object. For more information about the Tkinter library, see the Python documentation at http://docs.python.org.
class Waypoints: """Load and draw waypoints in a file with this format: 47.2543,-121.5324,Linksys 47.2534,-121.4634,Default """ def _ _init_ _(self, filename): fp = open(filename) self.waypoints = [ ] while(1): line = fp.readline( ) if not line: break lat, lon, name = string.split(line, ",") # store lat, lon, and waypoint name self.waypoints.append( (float(lat), float(lon), name) ) def draw(self, map, canvas): """Draw waypoints by translating coordinates and drawing onto a Tk canvas Keyword arguments: mapc -- an object of type MapCoords canvas -- a Tk canvas object """ for (lat, lon, name) in self.waypoints: # translate real-world coordinates to x,y locations x, y = map.latlon_to_pixels( (lat, lon) ) # draw a red circle and a text label canvas.create_oval((x-5, y-5, x+5, y+5), fill="red") canvas.create_text(x+5, y+5, text=name, anchor="nw") class GpsTrack: """Load and draw a GPS track file in NMEA format""" def _ _init_ _(self, filename): fp = open(filename) self.track = parse_NMEA_file(fp) def draw(self, map, canvas): old_x = 0 for position in self.track: # convert real-world to x,y pixel locations new_x, new_y = map.latlon_to_pixels(position) if old_x: # connect last position to this one with a black line canvas.create_line(old_x, old_y, new_x, new_y, width=2) # draw a blue circle at each intersection canvas.create_oval((new_x-2, new_y-2, new_x+2, new_y+2), fill="blue", outline="black") # store last coordinates old_x, old_y = new_x, new_y
Now that we have code to draw our map data, it is time to build a GUI interface. The following code creates a window with the ability to zoom and recenter by clicking a button or the map:
class App: """Created a TK GUI that allows viewing of a map containing a gps track and waypoints. Zooming and panning are supported.""" def _ _init_ _(self, master): """This ctor requires a Tk object to be passed in master""" self.width, self.height = 640, 480 self.map = MapCoords(self.width, self.height) # create the buttons at the top toolbar = Frame(master) toolbar.pack(side=TOP, fill=X) Button(toolbar, text="Zoom In", command=self.zoom_in).pack(side=LEFT) Button(toolbar, text="Zoom Out", command=self.zoom_out).pack(side=LEFT) # create the canvas we will use for drawing a map self.canvas = Canvas(root, height=self.height, width=self.width, background="white") # capture mouse click events on the map self.canvas.bind("", self.click) self.wpt = Waypoints("wifi.txt") self.track = GpsTrack("nmea.txt") self.draw_map( ) def click(self, event): """This is the callback for clicks in the canvas area. Clicks are translated into lat, lon and used to re-position the map""" # translate the on-screen x,y coordinates into real-world lat, lon lat, lon = self.map.pixels_to_latlon((event.x, event.y)) # re-center map and re-draw print lat, ",", lon self.map.set_center(lat, lon) self.draw_map( ) def zoom_out(self): self.map.zoom_out( ) self.draw_map( ) def zoom_in(self): self.map.zoom_in( ) self.draw_map( ) def draw_map(self): """Remove all drawing objects from the Tk canvas, and re-draw them with new coordinates""" # delete old drawing items self.canvas.delete("all") # show the current viewing position and zoom self.canvas.create_text(10,10,text="Location Lat=%f Lon=%f Zoom=%f" % (self.map.lat, self.map.lon, self.map.zoom), anchor="nw") self.wpt.draw(self.map, self.canvas) self.track.draw(self.map, self.canvas) # place a crosshair in the middle of the screen self.canvas.create_line(self.width/2-5, self.height/2, self.width /2+5, self.height/2) self.canvas.create_line(self.width/2, self.height/2-5, self.width/2, self.height/2+5) self.canvas.pack( ) from Tkinter import * root = Tk( ) app = App(root) root.mainloop( )
Since this application only displays data stored in a file, it does not take advantage of a mobile computer with a live GPS feed. In order to display live updates, the previous code would need an additional thread to populate new data into the GUI. If you are interested in adding this functionality, see http://www.mappinghacks.com/projects/carcomputer and the next hack, [Hack #63] .