# Script to get the location from a NMEA bluetooth GPS. Once a valid
#  fix has been received (or a user overrides it), opens up a web
#  browser to a URL, including the location in the query string
#
# Required webbrowser.py to be installed on the phone. This may be
#  downloaded from http://gagravarr.org/code/
#
# GPL
#
# Nick Burch - v0.02 (05/02/2006)

import appuifw
import webbrowser
import socket

# Default bluetooth address to connect to
# If blank, will prompt you to pick one
def_gps_addr='00:15:4B:01:14:EE'

# What URL to connect to once we have a fix
# (This sample one will just print back the lat and long supplied)
connect_url = "http://gagravarr.org/cgi-bin/have_a_fix.pl"

# What extra URL parameters to tack on
# Seperate multiple parameters with an &
# (these are just here as an example!)
additional_qs = "foo=bar&bar=foo"

#############################################################################

# We want icons etc
# Set this to 'large' if you want the whole screen used
appuifw.app.screen='normal'

# Define title etc
appuifw.app.title=u"Where Am I?"

#############################################################################

gps_addr = ''

#############################################################################

# This is set to 0 to request a quit
going = 1
# This is set to 1 when we have a valid/accepted fix
fix = 0
# This is set to >0 when we have a valid location
location_ok = 0
# Our current location
location = {}
# What satellites we're seeing
satellites = {}

#############################################################################

# Generate the checksum for some data
# (Checksum is all the data XOR'd, then turned into hex)
def generate_checksum(data):
	"""Generate the NMEA checksum for the supplied data"""
	csum = 0
	for c in data:
		csum = csum ^ ord(c)
	hex_csum = "%02x" % csum
	return hex_csum.upper()

# Format a NMEA timestamp into something friendly
def format_time(time):
	"""Generate a friendly form of an NMEA timestamp"""
	hh = time[0:2]
	mm = time[2:4]
	ss = time[4:]
	return "%s:%s:%s UTC" % (hh,mm,ss)

# Format a NMEA date into something friendly
def format_date(date):
	"""Generate a friendly form of an NMEA date"""
	months = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')
	dd = date[0:2]
	mm = date[2:4]
	yy = date[4:6]
	yyyy = int(yy) + 2000
	return "%s %s %d" % (dd, months[(int(mm)-1)], yyyy)

# NMEA data is HHMM.nnnn where nnnn is decimal part of second
def format_latlong(data):
	"""Turn HHMM.nnnn into HH:MM.SS"""

	# Check to see if it's HMM.nnnn or HHMM.nnnn or HHHMM.nnnn
	if data[5:6] == '.':
		# It's HHHMM.nnnn
		hh_mm = data[0:3] + ":" + data[3:5]
		dddd = data[6:]
	elif data[3:4] == '.':
		# It's HMM.nnnn
		hh_mm = data[0:1] + ":" + data[1:3]
		dddd = data[4:]
	else:
		# Assume HHMM.nnnn
		hh_mm = data[0:2] + ":" + data[2:4]
		dddd = data[5:]

	# Turn from decimal into seconds, and strip off last 2 digits
	sec = int( float(dddd) / 100.0 * 60.0 / 100.0 )
	return hh_mm + ":" + str(sec)

def format_latlong_dec(data):
	"""Turn HHMM.nnnn into HH.ddddd"""
	
	# Check to see if it's HMM.nnnn or HHMM.nnnn or HHHMM.nnnn
	if data[5:6] == '.':
		hours = data[0:3]
		mins = float(data[3:])
	elif data[3:4] == '.':
		hours = data[0:1]
		mins = float(data[1:])
	else:
		hours = data[0:2]
		mins = float(data[2:])

	dec = mins / 60.0 * 100.0
	# Cap at 6 digits - currently nn.nnnnnnnn
	dec = dec * 10000.0
	str_dec = "%06d" % dec
	return hours + "." + str_dec

#############################################################################

def readline(sock):
	"""Read one single line from the socket"""
	line = ""
	while 1:
		char = sock.recv(1)
		if not char: break
		line += char
		if char == "\n": break
	return line

#############################################################################

def do_gga_location(data):
	"""Get the location from a GGA sentence"""
	global location
	global location_ok

	d = data.split(',')
	location['type'] = 'GGA'
	location['lat'] = "%s%s" % (format_latlong(d[1]),d[2])
	location['long'] = "%s%s" % (format_latlong(d[3]),d[4])
	location['lat_dec'] = "%s%s" % (format_latlong_dec(d[1]),d[2])
	location['long_dec'] = "%s%s" % (format_latlong_dec(d[3]),d[4])
	location['alt'] = "%s %s" % (d[8],d[9])
	location['time'] = format_time(d[0])
	if d[5] == '0':
		location_ok = 0
	else:
		location_ok += 1

def do_gll_location(data):
	"""Get the location from a GLL sentence"""
	global location
	global location_ok

	d = data.split(',')
	location['type'] = 'GLL'
	location['lat'] = "%s%s" % (format_latlong(d[0]),d[1])
	location['long'] = "%s%s" % (format_latlong(d[2]),d[3])
	location['lat_dec'] = "%s%s" % (format_latlong_dec(d[0]),d[1])
	location['long_dec'] = "%s%s" % (format_latlong_dec(d[2]),d[3])
	if d[5] == 'A':
		location_ok += 1
	elif d[5] == 'V':
		location_ok = 0

def do_rmc_location(data):
	"""Get the location from a RMC sentence"""
	global location

	d = data.split(',')
	location['type'] = 'RMC'
	location['lat'] = "%s%s" % (format_latlong(d[2]),d[3])
	location['long'] = "%s%s" % (format_latlong(d[4]),d[5])
	location['lat_dec'] = "%s%s" % (format_latlong_dec(d[2]),d[3])
	location['long_dec'] = "%s%s" % (format_latlong_dec(d[4]),d[5])
	location['time'] = format_time(d[0])

#############################################################################

def do_gsv_satellite_view(data):
	"""Get the list of satellites we can see from a GSV sentence"""
	global satellites
	d = data.split(',')

	# Are we starting a new set of sentences, or continuing one?
	full_view_in = d[0]
	sentence_no = d[1]
	tot_in_view = d[2]

	if int(sentence_no) == 1:
		satellites['building_list'] = []

	# Loop over the satellites in the sentence, grabbing their data
	sats = d[3:]
	while len(sats) > 0:
		prn_num = sats[0]
		elevation = sats[1]
		azimuth = sats[2]
		sig_strength = sats[3]

		satellites[prn_num] = {
			'prn':prn_num,
			'elevation':elevation,
			'azimuth':azimuth,
			'sig_strength':sig_strength
		}

		satellites['building_list'].append(prn_num)
		sats = sats[4:]

	# Have we got all the details from this set?
	if sentence_no == full_view_in:
		satellites['in_view'] = satellites['building_list']
		satellites['in_view'].sort()
		satellites['building_list'] = []
	# All done

def do_gsa_satellites_used(data):
	"""Get the list of satellites we are using to get the fix"""
	global satellites
	d = data.split(',')

	sats = d[2:13]
	overall_dop = d[14]
	horiz_dop = d[15]
	vert_dop = d[16]

	while (len(sats) > 0) and (not sats[-1]):
		sats.pop()

	satellites['in_use'] = sats
	satellites['in_use'].sort()
	satellites['overall_dop'] = overall_dop
	satellites['horiz_dop'] = horiz_dop
	satellites['vert_dop'] = vert_dop

#############################################################################

def exit_key_pressed():
	"""Function called when the user requests exit"""
	global going
	going = 0
	appuifw.app.exit_key_handler = None

def callback(event):
	global fix
	global going

	# Allow them to press 0 to accept this fix
	if event['type'] == appuifw.EEventKeyDown:
		if event['scancode'] == 48:
			fix = 1
			going = 0
	
	# Whatever happens request a re-draw
	draw_state()

def draw_state():
	global location
	global satellites
	global gps_addr
	global location_ok

	canvas.clear()
	yPos = 12

	canvas.text( (0,yPos), u'GPS', 0x008000)
	canvas.text( (60,yPos), unicode(gps_addr))

	yPos += 12
	canvas.text( (0,yPos), u'Time:', 0x008000)
	if not location.has_key('time'):
		cur_time = u'(unavailable)'
	else:
		cur_time = unicode(location['time'])
	canvas.text( (60,yPos), cur_time)

	yPos += 12
	canvas.text( (0,yPos), u'Location', 0x008000)
	if location.has_key('alt'):
		canvas.text( (105,yPos), unicode(location['alt']) )
	if (not location.has_key('lat')) or (not location.has_key('long')):
		cur_loc = u'(unavailable)'
	else:
		if location_ok == 0:
			cur_loc = u'(invalid location)'
		else:
			cur_loc = unicode(location['lat']) + '  ' + unicode(location['long'])
	canvas.text( (10,yPos+12), cur_loc)

	yPos += 24
	canvas.text( (0, yPos), u'Satellites in view', 0x008000)
	if satellites.has_key('in_view'):
		canvas.text( (105,yPos), unicode( len(satellites['in_view']) ))
		canvas.text( (10,yPos+12), unicode(' '.join(satellites['in_view'])) )
	else:
		canvas.text( (10,yPos+12), u'(unavailable)')

	yPos += 24
	canvas.text( (0, yPos), u'Satellites used', 0x008000)
	if satellites.has_key('in_use'):
		used = len(satellites['in_use'])
		if satellites.has_key('overall_dop'):
			used = str(used) + "  err " + satellites['overall_dop']
		canvas.text( (105,yPos), unicode(used) )
		canvas.text( (10,yPos+12), unicode(' '.join(satellites['in_use'])) )
	else:
		canvas.text( (10,yPos+12), u'(unavailable)')

	yPos += 24
	if location_ok >= 1:
		yPos += 12
		canvas.text( (0, yPos), u'Press 0 to accept this location', 0x800000)

#############################################################################

sock=socket.socket(socket.AF_BT, socket.SOCK_STREAM)

if not def_gps_addr == '':
	gps_addr = def_gps_addr
	target=(gps_addr,1)

	# Alert them to the GPS we're going to connect to automatically
	appuifw.note(u"Will connect to GPS %s" % gps_addr, 'info')
else:
	# Prompt them to select a bluetooth GPS
	gps_addr,services=socket.bt_discover()
	target=(gps_addr,services.values()[0])

# Connect to the bluetooth GPS using the serial service
sock.connect(target)

appuifw.note(u"Connected to the GPS")

#############################################################################

# Enable these displays, no all prompts are over
canvas=appuifw.Canvas(event_callback=callback,
		redraw_callback=lambda rect:draw_state())
appuifw.app.body=canvas

#############################################################################

# Loop while active
appuifw.app.exit_key_handler = exit_key_pressed
while going == 1:
	rawdata = readline(sock)
	if not rawdata: break
	data = rawdata.strip()

	# Ensure it starts with $GP
	if not data[0:3] == '$GP':
		continue

	# If it has a checksum, ensure that's correct
	# (Checksum follows *, and is XOR of everything from
	#  the $ to the *, exclusive)
	if data[-3] == '*':
		exp_checksum = generate_checksum(data[1:-3])
		if not exp_checksum == data[-2:]:
			disp_notices = "Invalid checksum %s, expecting %s" % (data[-2:], exp_checksum)
			continue
		
		# Strip the checksum
		data = data[:-3]

	# Grab the parts of the sentence
	talker = data[1:3]
	sentence_id = data[3:6]
	sentence_data = data[7:]

	# Do we need to re-draw the screen?
	redraw = 0

	# The NMEA location sentences we're interested in are:
	#  GGA - Global Positioning System Fix Data
	#  GLL - Geographic Position
	#  RMC - GPS Transit Data
	if sentence_id == 'GGA':
		do_gga_location(sentence_data)
		redraw = 1
	if sentence_id == 'GLL':
		do_gll_location(sentence_data)
		redraw = 1
	if sentence_id == 'RMC':
		do_rmc_location(sentence_data)
		redraw = 1

	# The NMEA satellite sentences we're interested in are:
	#  GSV - Satellites in view
	#  GSA - Satellites used for positioning
	if sentence_id == 'GSV':
		do_gsv_satellite_view(sentence_data)
		redraw = 1
	if sentence_id == 'GSA':
		do_gsa_satellites_used(sentence_data)
		redraw = 1

	# Decide if we now have a valid fix or not
	if location_ok >= 20:
		# Do we have enough satellites in view?
		if satellites.has_key('in_view'):
			if len(satellites['in_view']) > 5:
				fix = 1
				going = 0

	# Update the state display
	if redraw == 1:
		draw_state()
else:
	# All done with bluetooth
	sock.close()

def do_nothing():
	"""Does nothing"""
	nothing = ""

# Did we get a fix?
if fix:
	if location_ok >= 1:
		lat = location['lat_dec']
		long = location['long_dec']
		appuifw.note(u"Accepted fix of %s %s" % (lat,long))

		# Report about the fix to the screen, to help with debugging
		print "Accepted lat is %s" % lat
		print "Accepted long is %s " % long
		print "(H:M:S lat is %s)" % location['lat']
		print "(H:M:S long is %s) " % location['long']
		if satellites.has_key('in_view'):
			print "Fix came from %d satellites" % len(satellites['in_view'])

		# Decide where to connect to
		url = connect_url + "?lat=" + lat + "&long=" + long
		if len(additional_qs) > 0:
			url = url + "&" + additional_qs

		# Now open up a web browser 
		print "Connecting to:"
		print url

		webbrowser.open(url)
	else:
		appuifw.note(u"Location wasn't valid, exiting")

print "All done"

# Un-comment this to have all of python exit when we're done
#appuifw.app.set_exit()
