import sys import ctypes import winbase import ceserial import time import re import httplib import math def Deg2Rad(x): "Degrees to radians." return x * (math.pi/180) def CalcRad(lat): "Radius of curvature in meters at specified latitude." a = 6378.137 e2 = 0.081082 * 0.081082 # the radius of curvature of an ellipsoidal Earth in the plane of a # meridian of latitude is given by # # R' = a * (1 - e^2) / (1 - e^2 * (sin(lat))^2)^(3/2) # # where a is the equatorial radius, # b is the polar radius, and # e is the eccentricity of the ellipsoid = sqrt(1 - b^2/a^2) # # a = 6378 km (3963 mi) Equatorial radius (surface to center distance) # b = 6356.752 km (3950 mi) Polar radius (surface to center distance) # e = 0.081082 Eccentricity #sc = math.sin(Deg2Rad(lat)) sc = math.sin(Deg2Rad(lat)) x = a * (1.0 - e2) z = 1.0 - e2 * sc * sc y = pow(z, 1.5) r = x / y r = r * 1000.0 # Convert to meters return r def EarthDistance((lat1, lon1), (lat2, lon2)): "Distance in meters between two points specified in degrees." x1 = CalcRad(lat1) * math.cos(Deg2Rad(lon1)) * math.sin(Deg2Rad(90-lat1)) x2 = CalcRad(lat2) * math.cos(Deg2Rad(lon2)) * math.sin(Deg2Rad(90-lat2)) y1 = CalcRad(lat1) * math.sin(Deg2Rad(lon1)) * math.sin(Deg2Rad(90-lat1)) y2 = CalcRad(lat2) * math.sin(Deg2Rad(lon2)) * math.sin(Deg2Rad(90-lat2)) z1 = CalcRad(lat1) * math.cos(Deg2Rad(90-lat1)) z2 = CalcRad(lat2) * math.cos(Deg2Rad(90-lat2)) a = (x1*x2 + y1*y2 + z1*z2)/pow(CalcRad((lat1+lat2)/2), 2) # a should be in [1, -1] but can sometimes fall outside it by # a very small amount due to rounding errors in the preceding # calculations (this is prone to happen when the argument points # are very close together). Thus we constrain it here. if abs(a) > 1: a = 1 elif a < -1: a = -1 return CalcRad((lat1+lat2) / 2) * math.acos(a) def read_serial_line(port): # Okay. This may be stupid, it seems ceserial has a readline method. # Had some issues with it though, so I'm keeping this until further.. try: # Start by flushing the input buffer of the serial port. # We are only interrested in fresh data. # Note: flushing the input causes a lot of misreading on # nonstable Bluetooth serial ports. Two solutions: # Quick-fix: restart GPS and app. # Perma-fix: rewrite the reading method ;) port.flushInput() text = "" t = port.read(1) while (len(t)>0): if ( t != '\n'): text = text + t else: return text t = port.read(1) return text except: print "Some kind of exception" time.sleep(5) sys.exit(1) # Checks if a GPS NMEA output string is a GPS RMC string with data. def is_gps_position (gpsdata): elements=gpsdata.split(',') # The following is a long chain of pattern-criteria based on sizes # and regexp'ed content that describes the NMEA GPRMC sentence... # To read this properly, you either need to be fluent in perlre, # or you should read the NMEA0183 documentation... if ( ( not len(elements) < 9 ) and ( len(elements[0]) == 6 ) and ( elements[0] == "$GPRMC" ) and ( re.match("^\d{6}", elements[1]) ) and ( re.match("^\w", elements[2]) ) and ( re.match("^\d+.\d+$", elements[3]) ) and ( re.match("^\w", elements[4]) ) and ( re.match("^\d+.\d+$", elements[5]) ) and ( re.match("^\w", elements[6]) ) and ( re.match("^\d+.\d+$", elements[7]) ) and ( re.match("^\d+.\d+$", elements[8]) ) and ( re.match("^\d{6}", elements[9]) ) ): return True # If the above does not match, the string read does not # match the GPRMC spec for a fixed position. return False # Converts a NMEA0183 RMC DegMinSec string to a decimal float position def rmc2decpos (pos, hemi): # The following is not pretty. TODO: Make this simpler! # Possible solution, treat pos as a float initially... # deg = int( float(pos) / 100 ) # min = float(pos) - deg * 100.0 # decpos = deg + (min / 60.0) # The above is NOT tested... try: if pos[5] == ".": # Latitude, 3digit degrees deg = pos[0:3] min = pos[3:] else: # Longitude, 2digit degrees deg = pos[0:2] min = pos[2:] # This bugger of a really simple calc had me beat for quite # a while. Tried all different complex calculations, until # discovering that the NMEA format is: [d]ddmm.mmmm, # meaning that seconds are represented as decimal fractions of minutes. # Thus, the translation is as simple as converting the minutes # to decimal fractions of degrees. value=round( float(deg) + ( float(min) / 60 ), 7) # Decimal position coordinates use negative values for # south&west postitions. if (hemi == "S") or (hemi == "W"): value = value * -1.0 return value # Really filthy "catch-all" exception handling... Yes, I know # that errors may occur in the above code, but no, I don't care.. # NMEA sentences are not always that reliable on cheap GPSes except (IndexError, TypeError, ValueError, FloatingPointError), e: t = str(e) raise ValueError("Value conversion failed, string: " + pos + " --> " + t) def get_position_data ( port ): # port is a serial-port handle as provided by ceserial, # and needs to be defined, but not opened. try: port.open() gpsdata = "" while not is_gps_position ( gpsdata ): gpsdata = read_serial_line( port ) port.close() except Exception, e: print "Error reading data!", e return False else: elements=gpsdata.split(',') tme=time.strftime("%H:%M:%S", time.strptime(elements[1][0:6], "%H%M%S")) dte=time.strftime("%Y-%m-%d", time.strptime(elements[9][0:6], "%d%m%y")) lat=rmc2decpos(elements[3], elements[4]) lng=rmc2decpos(elements[5], elements[6]) spd=float(elements[7]) hdg=float(elements[8]) return ( tme, lat, lng, spd, hdg, dte ) # TODO: Static definition of serial port in source code # is NOT good. Need some way of configuring this. # Maybe this should be done through the nonexistant GUI? gpsport=ceserial.Serial(port="COM2:",baudrate=4800) # TODO: The same goes here. Static definition of hostname... blargh. hostname = "defcon.no" baseurl = "/projects/gpsl/" # This stuff is used to limit testing and debugging runs. # After checking the code properly, and leak-testing it, the while # will be replaced by a "while 1:" .... # TODO: Run for a LONG time, and see how it performs # TODO: Maybe add a GUI and make it somewhat threaded? cnt=0 exceptioncount=0 prevlat = 0 prevlong = 0 while (cnt<10000) and (exceptioncount<150): try: (ftime, lat, long, speed, heading, fdate) = get_position_data( gpsport ) # TODO: Additional error-control needed print "Position data %d:"%cnt print "Position fix UTC:", ftime, fdate print " Lat:", lat print " Lng:", long print " Spd:", speed print " Hdg:", heading print # TODO: Add distance check! # If movement is too small do not perform the HTTP request distance = EarthDistance ( (lat, long), (prevlat, prevlong) ) if distance < 10.0: print "Distance diff too small:", distance else: print "Distance diff OK (%f), trying to update.."%distance url = baseurl + "update.php" url = url + "?lat=" + str(lat) url = url + "&long=" + str(long) url = url + "&date=" + str(fdate) url = url + "&time=" + str(ftime) url = url + "&speed=" + str(speed) url = url + "&heading=" + str(heading) # TODO: Remove the altitude log from the system, or find a way to read it. url = url + "&alt=0.0" httpc = httplib.HTTPConnection( hostname ) httpc.request( "GET", url ) res = httpc.getresponse() print "HTTP result: ", res.status, res.reason htdata = res.read() httpc.close() print htdata prevlat = lat prevlong = long time.sleep(2) cnt+=1 except Exception, e: # TODO: Evaluate the types of errors, check the performance # when using httplib connections, remove the counter. exceptioncount = exceptioncount + 1 print "%d. exception: "%exceptioncount print type(e), e