__doc__ = """
Simple python script that talks to the built in GPS on a Nokia
 N95 (or similar), and then converts the data from it into NMEA
 and forwards on over bluetooth.
Also does some simple position display.

Requires a Series 60 v3 (3rd Edition) phone with a built in
 GPS, eg the Nokia N95.
Also needs S60 Python 1.4 (signed), LocationRequestor for Python
 <http://discussion.forum.nokia.com/forum/showpost.php?p=311182>
 (dev cert signed), and either this as a sis (and dev cert signed)
 or the S60 Python Shell (dev cert singed)
(It isn't possible to use the built in positioning module supplied
 with S60 python, as that has way too many bugs still)

When dev cert signing, you probably need to give all the
 possible permissions, as the S60 v3 code signing model is really
 rather crap :(

If you want to use symbian signed online, be sure to get versions of
 the sis files with the right UID ranges.
You can get the LocationRequestor sis file with the right UID
 from <http://gagravarr.org/code/locationrequestor_3rd_opensign.sis>

GPL

Nick Burch - v0.02 (04/05/2008)
"""

# Core imports - special ones occur later
import appuifw
import e32
import math
import socket
import time
import os
import thread

# All of our preferences live in a dictionary called 'pref'
pref = {}

# Default bluetooth address to connect to
# If blank, will prompt you to pick one
pref['def_gps_addr']='00:1A:7D:00:25:6C'

# Should we listen for connections?
pref['listen'] = True

# We want icons etc
# Set this to 'large' if you want the whole screen used
pref['app_screen'] = 'normal'

# Define title etc
pref['app_title'] = "N95 as a BT GPS"

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

# Ensure our helper libraries are found
import sys
sys.path.append('C:/Python')
sys.path.append('E:/Python')

has_locationrequestor = None
try:
    import locationrequestor
    has_locationrequestor = True
except ImportError:
    has_locationrequestor = False
except SymbianError:
    has_locationrequestor = False
if not has_locationrequestor:
    appuifw.note(u"LocationRequestor for python wasn't found\nPlease download from http://usa.dpeddi.com/locationrequestor_3rd_unsigned.sis and install before using this program\n", u"error")
    print "\n"
    print "LocationRequestor module not found\n"
    print "Download it from http://usa.dpeddi.com/locationrequestor_3rd_unsigned.sis before using this program"
    # Try to exit without a stack trace - doesn't always work!
    sys.__excepthook__=None
    sys.excepthook=None
    sys.exit()

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

# Set the screen size, and title
appuifw.app.screen=pref['app_screen']
appuifw.app.title=unicode(pref['app_title'])

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

# This is set to 0 to request a quit
going = 1
# Our current location
location = {}
location['valid'] = 1 # Default to valid, in case no GGA/GLL sentences
# Our current motion
motion = {}
# What satellites we're seeing
satellites = {}
# Warnings / errors
disp_notices = ''
disp_notices_count = 0

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

# 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"""
	dd = int(date[0:2])
	mm = int(date[2:4])
	yy = int(date[4:6])
	yyyy = yy + 2000
	return format_date_from_parts(yyyy,mm,dd)

def format_date_from_parts(yyyy,mm,dd):
	"""Generate a friendly date from yyyy,mm,dd"""
	months = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')
	return "%02d %s %d" % (dd, months[(int(mm)-1)], yyyy)

def nmea_format_latlong(lat,long):
	"""Turn lat + long into nmea format"""

	def format_ll(hour_digits,ll):
		abs_ll = abs(ll)
		hour_ll = int(abs_ll)
		min_ll = (abs_ll - hour_ll) * 60
		# 4dp, so *10,000
		str_ll = "%03d%02d.%04d" % ( hour_ll, int(min_ll), 10000*(min_ll-int(min_ll)) )
		if hour_digits == 2 and str_ll[0] == '0':
			str_ll = str_ll[1:]
		return str_ll

	if str(lat) == 'NaN':
		flat = '0000.000,N'
	else:
		flat = format_ll(2, lat) + ","
		if lat < 0:
			flat += "S"
		else:
			flat += "N"

	if str(long) == 'NaN':
		flong = '00000.000,E'
	else:
		flong = format_ll(3, long) + ","
		if long < 0:
			flong += "W"
		else:
			flong += "E"
	return (flat,flong)
def user_format_latlong(lat,long):
	"""Turn lat + long into a user facing format"""
	if str(lat) == 'NaN':
		flat = '00:00:00N'
	else:
		alat = abs(lat)
		mins = (alat-int(alat))*60
		secs = (mins-int(mins))*60
		flat = "%02d:%02d:%02d" % (int(alat),int(mins),int(secs))
		if lat < 0:
			flat += "S"
		else:
			flat += "N"
	if str(long) == 'NaN':
		flong = '000:00:00W'
	else:
		along = abs(long)
		mins = (along-int(along))*60
		secs = (mins-int(mins))*60
		flong = "%03d:%02d:%02d" % (int(along),int(mins),int(secs))
		if long < 0:
			flong += "W"
		else:
			flong += "E"
	return (flat,flong)

# 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 get_latlong_floats():
	global location
	wgs_lat = location['lat_dec'];
	wgs_long = location['long_dec'];
	if wgs_lat[-1:] == 'S':
		wgs_lat = '-' + wgs_lat;
	if wgs_long[-1:] == 'W':
		wgs_long = '-' + wgs_long;
	wgs_lat = float(wgs_lat[0:-1])
	wgs_long = float(wgs_long[0:-1])

	return (wgs_lat,wgs_long)

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

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

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

sock = None
def process_lr_update(args):
	"""Process a location update from LocationRequestor"""
	global satellites
	global location
	global motion
	global gps
	global sock
	#print args

	if len(args) > 2:
		latlong = user_format_latlong(args[1],args[2])
		location['lat']  = latlong[0]
		location['long']  = latlong[1]

		if str(args[1]) == 'NaN':
			location['lat_dec'] = "0.0"
		else:
			location['lat_dec'] = "%02.6f" % args[1]
		if str(args[2]) == 'NaN':
			location['long_dec'] = "0.0"
		else:
			location['long_dec'] = str(args[2])

		location['alt'] = "%3.1f m" % (args[3])

		satellites['horiz_dop'] = "%0.1f" % args[4]
		satellites['vert_dop'] = "%0.1f" % args[5]
		satellites['overall_dop'] = "%0.1f" % ((args[4]+args[5])/2)
	if len(args) > 8:
		if(str(args[8])) == 'NaN':
			if motion.has_key('speed'):
				del motion['speed']
		else:
			motion['speed_kmph'] = float(args[8])
			motion['speed_mph'] = float(args[8]) / 1.609344
		if(str(args[10])) == 'NaN':
			if motion.has_key('true_heading'):
				del motion['true_heading']
		else:
			motion['true_heading'] = "%0.1f" % args[10]

		location['tsecs'] = long(args[12]/1000) # ms not sec
		timeparts = time.gmtime( location['tsecs'] )
		location['time'] = "%02d:%02d:%02d" % (timeparts[3:6])
		location['date'] = format_date_from_parts(*timeparts[0:3])
		gga_time = "%02d%02d%02d.%02d" % (timeparts[3:7])

		if args[14] == 0:
			location['valid'] = 0
		else:
			location['valid'] = 1
	else:
		if str(args[1]) == 'NaN':
			location['valid'] = 0
		else:
			location['valid'] = 1

		location['tsecs'] = long(args[7]/1000) # ms not sec
		timeparts = time.gmtime( location['tsecs'] )
		location['time'] = "%02d:%02d:%02d" % (timeparts[3:6])
		location['date'] = format_date_from_parts(*timeparts[0:3])
		gga_time = "%02d%02d%02d.%02d" % (timeparts[3:7])


	# Figure out the satellites in view
	in_view = []
	in_use = []

	num_in_view = args[13] + 1 # 0 or 1 based?
	for i in range(num_in_view):
		try:
			sat = gps.lr.GetSatelliteData(i)
			if sat != None and len(sat) >= 5:
				prn = str(sat[0])
				satellites[prn] = {
					'prn': prn,
					'elevation': sat[2],
					'azimuth': sat[1],
					'sig_strength': sat[3]
				}
				in_view.append(sat[0])
				if sat[4]:
					in_use.append(sat[0])
		except Exception, reason:
			#print "%d - %s" % (i,reason)
			pass

	# Sometimes in random orders
	in_view.sort()
	in_use.sort()
	# Store, as strings
	satellites['in_view'] = [str(prn) for prn in in_view]
	satellites['in_use']  = [str(prn) for prn in in_use]

	# Send NMEA data over the socket
	if sock:
		# GGA - position
		nmea_latlong = nmea_format_latlong(args[1],args[2])
		sock.send(
			"$GPGGA,%s,%s,%s,%d,%02d,0.0,%0.2f,M,,,,\n" % \
				(gga_time,nmea_latlong[0],nmea_latlong[1],location['valid'],
					len(in_view),args[3])
		)
		# GSV - satellites in view
		# Needs to be in batches of 4
		gsv_strs = []
		for num in in_view:
			s_num = str(num)
			if satellites.has_key(s_num):
				sat = satellites[s_num]
				gsv_strs.append(
					"%02d,%02d,%03d,%02d" % \
					(num, sat['elevation'], sat['azimuth'], sat['sig_strength'])
				)
			else:
				gsv_strs.append(
					"%02d,%02d,%03d,%02d" % \
					(num, -1, -1, -1)
				)
		gsv_groups = []
		for gsv_str in gsv_strs:
			if not len(gsv_groups) or len(gsv_groups[-1]) == 4:
				gsv_groups.append([])
			gsv_groups[-1].append(gsv_str)
		for gsv_num in range(len(gsv_groups)):
			gsv = ",".join( gsv_groups[gsv_num] )
			sock.send(
				"$GPGSV,%d,%d,%d,%s\n" % \
				(len(gsv_groups), gsv_num+1, len(in_view), gsv)
			)
		# GSA - satellites used
		# Required to send 12 satellites
		in_use_str = ["%02d" % prn for prn in in_use]
		in_use_str += ['' for f in range(12-len(in_use_str))]
		sock.send(
				"$GPGSA,A,3,%s,%s,%s,%s\n" % (
				    ",".join(in_use_str),
					satellites['overall_dop'],
					satellites['horiz_dop'],
					satellites['vert_dop']
				)
		)
		# VTG - track made good (motion) 
		heading = ""
		speed = ""
		if motion.has_key('true_heading'):
			heading = motion['true_heading']
		if motion.has_key('speed_kmph'):
			speed = "%3.1f" % motion['speed_kmph']
		sock.send(
				"$GPVTG,%s,T,%s,M,%s,N,%s,K\n" % (
					heading,heading,"000.0",speed
				)
		)

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

# Lock, so python won't exit during non canvas graphical stuff
lock = e32.Ao_lock()

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

def callback(event):
	# Grab key events, and do something useful with them

	# Whatever happens request a re-draw
	draw_main()

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

def draw_main():
	global location
	global motion
	global satellites
	global gps
	global disp_notices
	global disp_notices_count

	canvas.clear()

	yPos = line_spacing

	canvas.text( (0,yPos), u'GPS', 0x008000, font)
	if gps.connected:
		canvas.text( (indent_data,yPos), unicode(str(gps)), font=font)
	else:
		indent = int(indent_data/2)
		canvas.text( (indent,yPos), u"-waiting-"+unicode(str(gps)), 0xdd0000, font)

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

	yPos += line_spacing
	canvas.text( (0,yPos), u'Speed', 0x008000, font)
	if motion.has_key('speed_mph'):
		cur_speed = "%0.1f mph" % motion['speed_mph']
		cur_speed = unicode(cur_speed)
	else:
		cur_speed = u'(unavailable)'
	canvas.text( (indent_data,yPos), cur_speed, font=font)

	yPos += line_spacing
	canvas.text( (0,yPos), u'Heading', 0x008000, font)
	if motion.has_key('true_heading'):
		if motion.has_key('mag_heading') and motion['mag_heading']:
			mag = 'True: ' + motion['true_heading']
			mag = mag + '    Mag: ' + motion['mag_heading']
		else:
			mag = "%s deg" % motion['true_heading']
		mag = unicode(mag)
	else:
		mag = u'(unavailable)'
	canvas.text( (indent_data,yPos), mag, font=font)

	yPos += line_spacing
	canvas.text( (0,yPos), u'Location', 0x008000, font)
	if location.has_key('alt'):
		canvas.text( (indent_large,yPos), unicode(location['alt']), font=font )
	if (not location.has_key('lat')) or (not location.has_key('long')):
		cur_loc = u'(unavailable)'
	else:
		if location['valid'] == 0:
			cur_loc = u'(invalid location)'
		else:
			cur_loc = unicode(location['lat']) + '  ' + unicode(location['long'])
	canvas.text( (indent_slight,yPos+line_spacing), cur_loc, font=font)

	yPos += (line_spacing*2)
	canvas.text( (0, yPos), u'Satellites in view', 0x008000, font)
	if satellites.has_key('in_view'):
		canvas.text( (indent_large,yPos), unicode( len(satellites['in_view']) ), font=font )
		canvas.text( (indent_slight,yPos+line_spacing), unicode(' '.join(satellites['in_view'])), font=font )
	else:
		canvas.text( (indent_slight,yPos+line_spacing), u'(unavailable)', font=font)

	yPos += (line_spacing*2)
	canvas.text( (0, yPos), u'Satellites used', 0x008000, font)
	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( (indent_large,yPos), unicode(used), font=font )
		canvas.text( (indent_slight,yPos+line_spacing), unicode(' '.join(satellites['in_use'])), font=font )
	else:
		canvas.text( (indent_slight,yPos+line_spacing), u'(unavailable)', font=font )

	yPos += (line_spacing*2)

	if not disp_notices == '':
		yPos += line_spacing
		canvas.text( (0,yPos), unicode(disp_notices), 0x000080, font)
		disp_notices_count = disp_notices_count + 1
		if disp_notices_count > 60:
			disp_notices = ''
			disp_notices_count = 0

# Handle config entry selections
config_lb = ""
def config_menu():
	# Do nothing for now
	global config_lb
	global canvas
	appuifw.body = canvas

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

class LocReq:
	def __init__(self):
		self.lr = locationrequestor.LocationRequestor()
		self.id = None
		self.default_id = self.lr.GetDefaultModuleId()
		self.type = "Unknown"
	def __repr__(self):
		return self.type
	def identify_gps(self):
		# Try for the internal first
		count = self.lr.GetNumModules()
		for i in range(count):
			info = self.lr.GetModuleInfoByIndex(i)
			if ((info[3] == locationrequestor.EDeviceInternal) and ((info[2] & locationrequestor.ETechnologyNetwork) == 0)):
				try:
					self.id = info[0]
					self.type = "Internal"
					self.lr.Open(self.id)
					print "Picked Internal GPS with ID %s" % self.id
					self.lr.Close
					return
				except Exception, reason:
					# This probably means that the GPS is disabled
					print "Error querying GPS %d - %s" % (i,reason)

		# Look for external if there's no internal one
		for i in range(count):
			info = self.lr.GetModuleInfoByIndex(i)
			if ((info[3] == locationrequestor.EDeviceExternal) and ((info[2] & locationrequestor.ETechnologyNetwork) == 0)):
				self.id = info[0]
				self.type = "External"
				print "Picked External GPS with ID %s" % self.id
				return
		# Go with the default
		self.id = self.default_id
	def connect(self):
		self.lr.SetUpdateOptions(1, 45, 0, 1)
		self.lr.Open(self.id)

		# Install the callback
		try:
			self.lr.InstallPositionCallback(process_lr_update)
			self.connected = True
			appuifw.note(u"Connected to the GPS Location Service")
			return True
		except Exception, reason:
			disp_notices = "Connect to GPS failed with %s, retrying" % reason
			self.connected = False
			return False
	def process(self):
		e32.ao_sleep(0.4)
		return 1
	def shutdown(self):
		self.lr.Close()

gps = LocReq()
gps.identify_gps()
# Not yet connected
gps.connected = False

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

# Enable these displays, now all prompts are over
canvas=appuifw.Canvas(event_callback=callback,
		redraw_callback=lambda rect:draw_main())

# Figure out how big our screen is
# Note - don't use sysinfo.display_pixels(), as we have phone furnature
screen_width,screen_height = canvas.size
if screen_width > (screen_height*1.25):
	appuifw.note(u"This application is optimised for portrait view, some things may look odd", "info")
print "Detected a resolution of %d by %d" % (screen_width,screen_height)

# Decide on font and line spacings. We want ~12 lines/page
line_spacing = int(screen_height/12)
all_fonts = appuifw.available_fonts()
font = None
if u'LatinBold%d' % line_spacing in all_fonts:
	font = u'LatinBold%d' % line_spacing
elif u'LatinPlain%d' % line_spacing in all_fonts:
	font = u'LatinPlain%d' % line_spacing
#elif u'Nokia Sans S60' in all_fonts:
#	font = u'Nokia Sans S60'
else:
	# Look for one with the right spacing, or close to it
	for f in all_fonts:
		if f.endswith(str(line_spacing)):
			font = f
	if font == None:
		for f in all_fonts:
			if f.endswith(str(line_spacing-1)):
				font = f
	if font == None:
		# Give up and go with the default
		font = "normal"
print "Selected line spacing of %d and the font %s" % (line_spacing,font)
# while converting
#font = u"LatinPlain12"

# How far across the screen to draw things in the list views
indent_slight = int(line_spacing * 0.85)   #v2=10
indent_data = int(line_spacing * 4.0) + 12 #v2=60
indent_large = int(line_spacing * 8.75)    #v2=105

# Make the canvas active
appuifw.app.body=canvas

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

# Start the lock, so python won't exit during non canvas graphical stuff
lock = e32.Ao_lock()
serv_sock = None

def connect_out():
	global sock
	global disp_notices

	if not pref['def_gps_addr'] == '':
		gps_addr = pref['def_gps_addr']
		target=(gps_addr,1)
		# Alert them to the device we're going to connect
		#  to automatically
		appuifw.note(u"Will connect to device %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
	sock = socket.socket(socket.AF_BT, socket.SOCK_STREAM)
	sock.connect(target)
	disp_notices = "Connected to device %s" % target
	appuifw.note(u"Connected to the device")

def listen_in():
	global sock
	global disp_notices

	serv_sock = socket.socket(socket.AF_BT, socket.SOCK_STREAM)
	port = socket.bt_rfcomm_get_available_server_channel(serv_sock._sock)
	serv_sock.bind(("",port))
	disp_notices = "Waiting for connections"
	appuifw.note(u"Waiting for connections")

	socket.bt_advertise_service(u"Serial", serv_sock, True, socket.RFCOMM)
	socket.set_security(serv_sock._sock, socket.AUTHOR)

	serv_sock.listen(1)
	sock, address = serv_sock.accept()
	disp_notices = "Accepted connection from %s" % address
	appuifw.note(u"Accepted Connection")

	socket.bt_advertise_service(u"Serial", serv_sock, False, socket.RFCOMM)
	serv_sock = None

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

# TODO: Make canvas and Listbox co-exist without crashing python
do_listen_in = e32.ao_callgate(listen_in)
do_connect_out = e32.ao_callgate(connect_out)

appuifw.app.menu=[
	(u'Accept Connection',do_listen_in),
	(u'Make Connection',do_connect_out),
]

# Loop while active
appuifw.app.exit_key_handler = exit_key_pressed
while going > 0:
	# Connect to the GPS, if we're not already connected
	if not gps.connected:
		worked = gps.connect()
		if not worked:
			# Sleep for a tiny bit, then retry
			e32.ao_sleep(0.2)
			continue

	# If we are connected to the GPS, read a line from it
	if gps.connected:
		redraw = gps.process()

		# Update the state display if required
		if redraw == 1:
			draw_main()
	else:
		# Sleep for a tiny bit before re-trying
		e32.ao_sleep(0.2)

else:
	# All done
	gps.shutdown()
	try:
		sock.close()
	except Exception:
		pass
	try:
		serv_sock.close()
	except Exception:
		pass

print "All done"
#appuifw.app.set_exit()
