#!/usr/bin/python
'''
MidNite USB Firmware Update Utility
Copyright MidNite Solar Inc. 2012
Andrew Meares, andy@midnitesolar.com
12/7/2012

Upload Command Strings:
'uPlDcLSaLLfl' - for Classic update, everything but the bootloader (flash)
'uPlDrEMaLLfl' - for Remote update, everything but the bootloader (flash)
	
History:
10/30/2012	- Created
12/7/2012		- Added repeat-sleep option and converted sleep times to floats
'''
#-------------------------------------------------------------------------------
# Imports
#-------------------------------------------------------------------------------
import sys
import time
import argparse
import os.path
import Queue
from Tkinter import *
#import ImageTK

# Give pyserial special treatment
try:
	import serial
except Exception:
	print "Error: Unable to import python serial module (pyserial)."
	print "See README.txt for further information."
	exit(1)

#-------------------------------------------------------------------------------
# Settings
#-------------------------------------------------------------------------------
PROGRAM_DESCRIPTION = 'A utility for updating firmware via a USB COM port.'
DEFAULT_DEVICE_PATH = '/dev/ttyACM0'
DEFAULT_BAUDRATE = 9600
BAUDRATE_LIST = [9600, 19200, 57600]
DEFAULT_TIMEOUT = 1
DEFAULT_UPLOAD_MESSAGE = 'uPlDcLSaLLfl'
DEFAULT_SLEEP_TIME = 1.0
DEFAULT_REPEAT_SLEEP_TIME = 0.1
FAIL_MAX = 10

#-------------------------------------------------------------------------------
# Support Functions
#-------------------------------------------------------------------------------
# Convert a byte string to it's hex string representation e.g. for output.
def ByteToHex( byteStr ):
    # Uses list comprehension which is a fractionally faster implementation than
    # the alternative, more readable, implementation below
    #   
    #    hex = []
    #    for aChar in byteStr:
    #        hex.append( "%02X " % ord( aChar ) )
    #
    #    return ''.join( hex ).strip()
       
    return ''.join( [ "%02X " % ord( x ) for x in byteStr ] ).strip()


# Convert a string hex byte values into a byte string. The Hex Byte values may
# or may not be space separated.
def HexToByte( hexStr ):
    # The list comprehension implementation is fractionally slower in this case    
    #
    #    hexStr = ''.join( hexStr.split(" ") )
    #    return ''.join( ["%c" % chr( int ( hexStr[i:i+2],16 ) ) \
    #                                   for i in range(0, len( hexStr ), 2) ] )
 
    bytes = []

    hexStr = ''.join( hexStr.split(" ") )

    for i in range(0, len(hexStr), 2):
        bytes.append( chr( int (hexStr[i:i+2], 16 ) ) )

    return ''.join( bytes )


#-------------------------------------------------------------------------------
# Arguments Class
#-------------------------------------------------------------------------------
class Arguments:
	def __init__(self):
		# setup the command line arguments
		self.parser = argparse.ArgumentParser(description=PROGRAM_DESCRIPTION)

		# add the arguments
		self.parser.add_argument('filename', help='Firmware file path/name', type=str)
		self.parser.add_argument('-d','--device', help='Device path/name', type=str, default=DEFAULT_DEVICE_PATH)
		self.parser.add_argument('-b','--baudrate', help='Device baudrate', type=int, default=DEFAULT_BAUDRATE, choices=BAUDRATE_LIST)
		self.parser.add_argument('-t','--timeout', help='Device timeout in seconds', type=int, default=DEFAULT_TIMEOUT)
		self.parser.add_argument('-m','--message', help='Upload message', type=str, default=DEFAULT_UPLOAD_MESSAGE)
		self.parser.add_argument('-z','--sleep', help='Sleep time for serial port in seconds', type=float, default=DEFAULT_SLEEP_TIME)
		self.parser.add_argument('-r','--repeat', help='Repeat upload message?', action='store_true')
		self.parser.add_argument('-x','--repeat-sleep', help='Repeat upload sleep time in seconds', type=float, default=DEFAULT_REPEAT_SLEEP_TIME)
		self.parser.add_argument('-s','--silent', help='Silent mode?', action='store_true')

		# parse the command line arguments
		self.args = self.parser.parse_args()

	def get(self):
		return self.args



#-------------------------------------------------------------------------------
# Serial Monitor GUI
#-------------------------------------------------------------------------------
class UpdateGUI(Frame):

#-------------------------------------------------------------------------------
	def __init__(self, master, in_queue, out_queue, end_command):
		Frame.__init__(self, master)
		
		# Queues
		#self.in_queue = in_queue
		#self.out_queue = out_queue
		
		# Frames
		#self.pack()
		self.grid(row=0, column=0, columnspan=2, rowspan=1)
		
		#self.frame1 = Frame(self, relief=GROOVE, borderwidth=4)
		#self.frame1.pack()
		
		# Widgets
		# self.quit_button = Button(self.left_frame, text="Quit", fg="red", command=end_command, width=8)
		# self.quit_button.pack(side=LEFT, padx=10, pady=10)
		tmp_text = """
		xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
		1: stuff
		2: stuff
		3: find
		4: sell
		5: ERROR: THIS PROGRAM DOES NOT WORK YET
		6: 00000000000000
		---------------------------------------------------------------------------------
		"""
		self.canvas1 = Canvas( self, width=480, height=300 )
		self.canvas1.create_rectangle(0, 0, 478, 298, fill="blue")
		#self.label2 = Label(self, anchor=W, bg="blue", fg="white", relief=SUNKEN, borderwidth=8, justify=LEFT, text=tmp_text, width=120, height=10);
		#self.text1 = Text(self,state=DISABLED)
		#self.text1.insert(END, "hello, ")
		#self.text1.insert(END, "world")
		
		self.photoimage = PhotoImage(file="moon3.gif")
		self.label1 = Label(self, image=self.photoimage)
		
		#self.label2.pack(side=LEFT, fill=BOTH, expand=1)
		#self.label1.pack(side=LEFT)
		#self.label2.pack(side=LEFT, fill=X, expand=1)
		self.label1.grid(row=1,column=2)
		self.canvas1.grid(row=1,column=1)
		# Bind Enter key
		#self.message_entry.bind("<Return>", self.send_message)
		
#-------------------------------------------------------------------------------
	def send_message(self, Event=None):
		pass
			
#-------------------------------------------------------------------------------
	def process_incoming(self):
		pass

#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------





#-------------------------------------------------------------------------------
# Updater Class
#-------------------------------------------------------------------------------
class Updater:
	
#-------------------------------------------------------------------------------
	def __init__(self, master, args):
		self.master = master
		self.args = args
		# Create the queues
		self.out_queue = Queue.Queue()
		self.in_queue = Queue.Queue()
		# Set up the GUI part
		self.gui = UpdateGUI(master, self.out_queue, self.in_queue, self.endApplication)
		self.running = 1
		self.periodicCall()
		
#-------------------------------------------------------------------------------
	def endApplication(self):
		self.running = 0
		
#-------------------------------------------------------------------------------
	def periodicCall(self):
		self.gui.process_incoming()
		if not self.running:
			# This is the brutal stop of the system. You may want to do
			# some cleanup before actually shutting it down.
			sys.exit(1)
		# this is a never ending song
		self.master.after(100, self.periodicCall)
    
#-------------------------------------------------------------------------------		
	def print_welcome(self):
		if (not self.args.silent):
			print 'MidNite Firmware Updater'
			print 'Copyright MidNite Solar Inc. 2012'
		
#-------------------------------------------------------------------------------
	def read_file(self):
		# open the file
		try:
			self.file_handle = open(self.args.filename, 'r')
		except IOError, e:
			print e
			exit(1)
		
		# setup the raw buffer for output
		try:
			self.raw = HexToByte(self.file_handle.read())
		except:
			print 'Error: %s' % sys.exc_info()[0]
			exit(1)
			
		if (not self.args.silent):
			print 'Firmware size: %d bytes' % len(self.raw)
	
#-------------------------------------------------------------------------------
	def wait_for_serial(self):
		if (not self.args.silent):
			print 'Waiting for serial device.'
			print '<Press Ctrl-C to Cancel>',
			sys.stdout.flush()
			
		count = 0
		while (not os.path.exists(self.args.device)):
			time.sleep(0.2)
			count = count + 1
			if (count >= 5):
				if (not self.args.silent):
					print '.',
					sys.stdout.flush()
				count = 0
				
		# a little extra sleep here
		if (not self.args.silent):
			print ''
			print 'Sleeping for serial port.'
		time.sleep(self.args.sleep)
			

#-------------------------------------------------------------------------------
	def open_serial(self):
		if (not self.args.silent):
			print 'Opening serial port: %s' % self.args.device
		self.ser = serial.Serial()
		self.ser.port = self.args.device
		self.ser.baudrate = self.args.baudrate
		self.ser.timeout = self.args.timeout
		try:
			self.ser.open()
		except serial.SerialException, e:
			print e
			exit(1)	

#-------------------------------------------------------------------------------
	def send_upload_command(self):
		if (not self.args.silent):
			print 'Sending upload command: %s' % self.args.message
		try:
			self.ser.write(self.args.message)
		except serial.SerialException, e:
			print e
			exit(1)
		except ValueError, e:
			print e
			exit(1)
			
		# repeat message for special updates
		if (self.args.repeat):
			# sleep a moment
			#time.sleep(self.args.sleep) - removed for MNGP support -AM 12/7/2012
			time.sleep(self.args.repeat_sleep)
			if (not self.args.silent):
				print 'Repeating upload command: %s' % self.args.message
			try:
				self.ser.write(self.args.message)
			except serial.SerialException, e:
				print e
				exit(1)
			except ValueError, e:
				print e
				exit(1)	

#-------------------------------------------------------------------------------
	def flash_sleep(self):
		if (not self.args.silent):
			print 'Sleeping for flash erase.'
		time.sleep(self.args.sleep)
	
#-------------------------------------------------------------------------------
	def upload_firmware(self):

		if (not self.args.silent):
			print '<Uploading Firmware>',
			sys.stdout.flush()
		
		idx = 0
		framesize = 0
		r = 0
		fail_count = 0

		# upload the firmware one frame at a time
		# resend lost frames
		while (idx < len(self.raw)):
			framesize = ord(self.raw[idx])*256+ord(self.raw[idx+1]) + 2
			try:
				self.ser.write(self.raw[idx:(idx+framesize)])
			except serial.SerialException, e:
				print e
				exit(1)
			except ValueError, e:
				print e
				exit(1)
			try:
				r = self.ser.read()
				if (ord(r) == 0x11):
					idx = idx + framesize
					if (not self.args.silent):
						print '.',
						sys.stdout.flush()
				elif (ord(r) == 0x22):
					if (not self.args.silent):
						print 'x',
						sys.stdout.flush()
					fail_count = fail_count + 1
					if (fail_count >= FAIL_MAX):
						if (not self.args.silent):
							print ''
							print 'Update Failed: Lost packets'
						exit(1)
				else:
					if (not self.args.silent):
						print ''
						print 'Update Failed: No ACK byte received'
					exit(1)
			except serial.SerialException, e:
				print ''
				print e
				exit(1)
			except TypeError, e:
				print ''
				print e
				exit(1)
			except ValueError, e:
				print ''
				print e
				exit(1)
		if (not self.args.silent):
			print ''
			print 'Finished.'

#-------------------------------------------------------------------------------
	def __del__(self):
		pass
    # close the serial port
		#try:
		#	if self.ser.isOpen():
		#		self.ser.close()
		#except serial.SerialException, e:
		#	print e
		#except ValueError, e:
		#	print e
		#except AttributeError, e:
		#	print e

		# close the file			
		#try:
		#	self.file_handle.close()
		#except IOError, e:
		#	print e
		#except AttributeError, e:
		#	print e




#-------------------------------------------------------------------------------
# Program Start
#-------------------------------------------------------------------------------
#args = Arguments()
#main = Updater(args.get())
#exit(0)

#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
root = Tk()
args = Arguments()
app = Updater(root, args.get())
root.mainloop()
#-------------------------------------------------------------------------------


