"""
    pyRepRap is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    pyRepRap is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with pyRepRap.  If not, see <http://www.gnu.org/licenses/>.
"""
"""    
    This is the main user imported module containing all end user functions    
"""

# add commands to switch to gcode mode to allow any script using this library to write gcode too.

try:
	import serial	# Import the pySerial modules.
except:
	print('You do not have pySerial installed, which is needed to control the serial port.')
	print('Information on pySerial is at:\nhttp://pyserial.wiki.sourceforge.net/pySerial')

import snap
import time


printDebug = False	# print debug info

# SNAP Control Commands - Taken from PIC code #

# extruder commands #
CMD_VERSION       =  0
CMD_FORWARD       =  1
CMD_REVERSE       =  2
CMD_SETPOS        =  3
CMD_GETPOS        =  4
CMD_SEEK          =  5
CMD_FREE          =  6
CMD_NOTIFY        =  7
CMD_ISEMPTY       =  8
CMD_SETHEAT       =  9
CMD_GETTEMP       = 10
CMD_SETCOOLER     = 11
CMD_PWMPERIOD     = 50
CMD_PRESCALER     = 51
CMD_SETVREF       = 52
CMD_SETTEMPSCALER = 53
CMD_GETDEBUGINFO  = 54
CMD_GETTEMPINFO   = 55

# stepper commands #
CMD_VERSION		=   0
CMD_FORWARD		=   1
CMD_REVERSE		=   2
CMD_SETPOS		=   3
CMD_GETPOS		=   4
CMD_SEEK		=   5
CMD_FREE		=   6
CMD_NOTIFY		=   7
CMD_SYNC		=   8
CMD_CALIBRATE		=   9
CMD_GETRANGE		=  10
CMD_DDA			=  11
CMD_FORWARD1		=  12
CMD_BACKWARD1		=  13
CMD_SETPOWER		=  14
CMD_GETSENSOR		=  15
CMD_HOMERESET		=  16
CMD_GETMODULETYPE	= 255

# sync modes #
sync_none	= 0	# no sync (default)
sync_seek	= 1	# synchronised seeking
sync_inc	= 2	# inc motor on each pulse
sync_dec	= 3	# dec motor on each pulse

snap.localAddress = 0		# local address of host PC. This will always be 0.
#global serialPort
#serialPort = False

def openSerial( port, rate, tout ):
	global serialPort
	try:
		serialPort = serial.Serial( port, rate, timeout = tout )
		return True
	except 13:
		print "You do not have permissions to use the serial port, try running as root"

def closeSerial():
	serialPort.close()

# Convert two 8 bit bytes to one integer
def bytes2int(LSB, MSB):		
	return int( (0x100 * int(MSB) ) | int(LSB) )

# Convert integer to two 8 bit bytes
def int2bytes(val):
	MSB = int( ( int(val) & 0xFF00) / 0x100 )
	LSB = int( int(val) & 0xFF )
	return LSB, MSB

#def loopTest():
#	p = snap.SNAPPacket( serial, snap.localAddress, snap.localAddress, 0, 1, [] )

# Scan reprap network for devices (incomplete) - this will be used by autoconfig functions when complete
def scanNetwork():
	devices = []
	for remoteAddress in range(1, 10):									# For every address in range. full range will be 255
		print "Trying address " + str(remoteAddress)
		p = snap.SNAPPacket( serialPort, remoteAddress, snap.localAddress, 0, 1, [CMD_GETMODULETYPE] )	# Create snap packet requesting module type
		#p = snap.SNAPPacket( serialPort, remoteAddress, snap.localAddress, 0, 1, [CMD_VERSION] )
		if p.send():											# Send snap packet, if sent ok then await reply
			rep = p.getReply()
			if rep:
				#devices[ rep.dataBytes[1] ] = remoteAddress
				devices.append( { 'address':remoteAddress, 'type':rep.dataBytes[1], 'subType':rep.dataBytes[2] } )	# If device replies then add to device list.
			else:
				"print na"
		else:
			print "scan no ack"
		time.sleep(0.5)
	for d in devices:
		#now get versions
		print "device", d

def getNotification(serialPort):
	return snap.getPacket(serialPort)

class extruderClass:
	def __init__(self):
		self.address = 8
		self.active = False

	def getModuleType(self):	#note: do pics not support this yet? I can't see it in code and get no reply from pic
		if self.active:
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_GETMODULETYPE] )	# Create SNAP packet requesting module type
			if p.send():
				rep = p.getReply()
				data = checkReplyPacket( rep, 2, CMD_GETMODULETYPE )						# If packet sent ok and was acknoledged then await reply, otherwise return False
				if data:
					return data[1]								# If valid reply is recieved then return it, otherwise return False
		return False

	def getVersion(self):
		if self.active:
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_VERSION] )
			if p.send():
				rep = p.getReply()
				data = checkReplyPacket( rep, 3, CMD_VERSION )
				if data:
					return data[1], data[2]
		return False

	def setMotor(self, direction, speed):
		if self.active:
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [int(direction), int(speed)] ) ##no command being sent, whats going on?
			if p.send():
				return True
		return False

	def getTemp(self):
		if self.active:		
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_GETTEMP] )
			if p.send():
				rep = p.getReply()
				data = checkReplyPacket( rep, 2, CMD_GETTEMP )
				if data:
					return data[1]
		return False

	def setVoltateReference(self, val):
		if self.active:
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_SETVREF, int(val)] )
			if p.send():
				return True
		return False

	def setHeat(self, lowHeat, highHeat, tempTarget, tempMax):
		if self.active:	
			tempTargetMSB, tempTargetLSB = int2bytes( tempTarget )
			tempMaxMSB ,tempMaxLSB = int2bytes( tempMax )
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_SETHEAT, int(lowHeat), int(highHeat), tempTargetMSB, tempTargetLSB, tempMaxMSB, tempMaxLSB] )	# assumes MSB first (don't know this!)
			if p.send():
				return True
		return False

	def setCooler(self, speed):
		if self.active:
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_SETCOOLER, int(speed)] )
			if p.send():
				return True
		return False

	def freeMotor(self):
		if self.active:
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_FREE] )
			if p.send():
				return True
		return False

extruder = extruderClass()


def checkReplyPacket (packet, numExpectedBytes, command):
	if packet:
		if len( packet.dataBytes ) == numExpectedBytes:		# check correct number of data bytes have been recieved
			if packet.dataBytes[0] == command:			# check reply is a reply to sent command
				return packet.dataBytes
	return False
				

class axisClass:
	def __init__(self, address):
		self.address = address
		self.active = False	# when scanning network, set this, then in each func below, check alive before doing anything
		self.limit = 100000	# limit effectively disabled unless set
	#move axis one step forward
	def forward1(self):
		if self.active:
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_FORWARD1] ) 
			if p.send():
				return True
		return False

	#move axis one step backward
	def backward1(self):
		if self.active:
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_BACKWARD1] ) 
			if p.send():
				return True
		return False

	#spin axis forward at given speed
	def forward(self, speed):
		if self.active:
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_FORWARD, int(speed)] ) 
			if p.send():
				return True
		return False

	#spin axis backward at given speed
	def backward(self, speed):
		if self.active:
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_REVERSE, int(speed)] ) 
			if p.send():
				return True
		return False

	#debug only
	def getSensors(self):
		if self.active:
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_GETSENSOR] )
			if p.send():
				rep = p.getReply()
				data = checkReplyPacket( rep, 3, CMD_GETSENSOR )		# replace this with a proper object in SNAP module?
				if data:
					print data[1], data[2]
		return False

	#get current axis position
	def getPos(self):
		if self.active:
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_GETPOS] )
			if p.send():
				rep = p.getReply()
				data = checkReplyPacket( rep, 3, CMD_GETPOS )
				if data:
					pos = bytes2int( data[1], data[2] )
					return pos 						# return value
		return False

	#set current position (set variable not robot position)
	def setPos(self, pos):
		if self.active:
			posMSB ,posLSB = int2bytes( pos )
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_SETPOS, posMSB, posLSB] )
			if p.send():
				return True
		return False

	#power off coils on stepper
	def free(self):
		if self.active:
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_FREE] ) 
			if p.send():
				return True
		return False

	#seek to axis location. When waitArrival is True, funtion does not return until seek is compete
	def seek(self, pos, speed, waitArrival = True):
		if self.active and pos <= self.limit:
			posMSB ,posLSB = int2bytes( pos )
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_SEEK, int(speed), posMSB ,posLSB] ) 
			if p.send():
				if waitArrival:
					if printDebug: print "    wait notify"
					notif = getNotification( serialPort )
					if notif.dataBytes[0] == CMD_SEEK:
						if printDebug: print "    valid notification for seek"
					else:
						return False
					if printDebug: print "    rec notif"
				return True
		return False
	
	#goto 0 position. When waitArrival is True, funtion does not return until reset is compete
	def homeReset(self, speed, waitArrival = True):
		if self.active:
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_HOMERESET, int(speed)] ) 
			if p.send():
				if waitArrival:
					if printDebug: print "reset wait"
					notif = getNotification( serialPort )
					if notif.dataBytes[0] == CMD_HOMERESET:
						if printDebug: print "    valid notification for reset"
					else:
						return False
					if printDebug: print "reset done"
				return True
		return False

	def setNotify(self):
		#global serialPort
		if self.active:
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_NOTIFY, snap.localAddress] ) 	# set notifications to be sent to host
			if p.send():
				return True
		return False

	def setSync( self, syncMode ):
		if self.active:
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_SYNC, int(syncMode)] )
			if p.send():
				return True
		return False

	def DDA( self, speed, seekTo, slaveDelta, waitArrival = True):
		if self.active and seekTo <= self.limit:
			masterPosMSB, masterPosLSB = int2bytes( seekTo )
			slaveDeltaMSB, slaveDeltaLSB = int2bytes( slaveDelta )
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_DDA, int(speed), masterPosMSB ,masterPosLSB, slaveDeltaMSB, slaveDeltaLSB] ) 	#start sync
			if p.send():
				if waitArrival:
					notif = getNotification( serialPort )
					if notif.dataBytes[0] == CMD_DDA:
						if printDebug: print "    valid notification for DDA"	# todo: add actual enforement on wrong notification
					else:
						return False
				return True
		return False
	
	def setPower( self, power ):
		if self.active:
			p = snap.SNAPPacket( serialPort, self.address, snap.localAddress, 0, 1, [CMD_SETPOWER, int( power * 0.63 )] ) # This is a value from 0 to 63 (6 bits)
			if p.send():
				return True
		return False

class syncAxis:
	def __init__( self, axis, seekTo, delta, direction ):
		self.axis = axis
		self.seekTo = seekTo
		self.delta = delta
		self.direction = direction

		if self.direction > 0:
			self.syncMode = sync_inc
		else:
			self.syncMode = sync_dec


class cartesianClass:
	def __init__(self):
		# initiate axies with addresses
		self.x = axisClass(2)
		self.y = axisClass(3)
		self.z = axisClass(4)

	# goto home position (all axies)
	def homeReset(self, speed, waitArrival = True):
		if self.x.homeReset( speed, waitArrival ):		#setting these to true breaks waitArrival convention. need to rework waitArrival and possibly have each axis storing it's arrival flag and pos as variables?
			print "X Reset"		
		if self.y.homeReset( speed, waitArrival ):
			print "Y Reset"
		if self.z.homeReset( speed, waitArrival ):
			print "Z Reset"
		# add a way to collect all three notifications (in whatever order) then check they are all there. this will allow symultanious axis movement and use of waitArrival

	# seek to location (all axies). When waitArrival is True, funtion does not return until all seeks are compete
	# seek will automatically use syncSeek when it is required. Always use the seek function
	def seek(self, pos, speed, waitArrival = True):
		curX, curY, curZ = self.x.getPos(), self.y.getPos(), self.z.getPos()
		x, y, z = pos
		if x <= self.x.limit and y <= self.y.limit and z <= self.z.limit:
			if printDebug: print "seek from [", curX, curY, curZ, "] to [", x, y, z, "]"
			if x == curX or y == curY:
				if printDebug: print "    standard seek"
				self.x.seek( x, speed, True )			#setting these to true breaks waitArrival convention. need to rework waitArrival and possibly have each axis storing it's arrival flag and pos as variables?
				self.y.seek( y, speed, True )
			else:
				if printDebug: print "    sync seek"
				self.syncSeek( pos, speed, waitArrival )
			if z != curZ:
				self.z.seek( z, speed, True )
		else:
			print "Trying to print outside of limit, aborting seek"
	
	# perform syncronised x/y movement. This is called by seek when needed.
	def syncSeek(self, pos, speed, waitArrival = True):
		curX, curY = self.x.getPos(), self.y.getPos()
		newX, newY, nullZ = pos
		deltaX = abs( curX - newX )		# calc delta movements
		deltaY = abs( curY - newY )
		directionX = ( curX - newX ) / -deltaX	# gives direction -1 or 1
		directionY = ( curY - newY ) / -deltaY	
		if printDebug: print "    dx", deltaX, "dy", deltaY, "dirX", directionX, "dirY", directionY
		if printDebug: print "    using x master"

		master = syncAxis( self.x, newX, deltaX, directionX )	# create two swapable data structures, set x as master, y as slave
		slave = syncAxis( self.y, newY, deltaY, directionY )
		
		if slave.delta > master.delta:		# if y has the greater movement then make y master
			slave, master = master, slave
			if printDebug: print "    switching to y master"
		if printDebug: print "    masterPos", master.seekTo, "slaveDelta", slave.delta
		slave.axis.setSync( slave.syncMode )
		master.axis.DDA( speed, master.seekTo, slave.delta, True )
		time.sleep(0.1)
		slave.axis.setSync( sync_none )
		if printDebug: print "    sync seek complete"
	
	# get current position of all three axies	
	def getPos(self):
		return self.x.getPos(), self.y.getPos(), self.z.getPos()
	
	# stop all motors
	def stop(self):
		self.x.forward( 0 )
		self.y.forward( 0 )
		self.z.forward( 0 )

	# free all motors (no current on coils)
	def free(self):
		self.x.free()
		self.y.free()
		self.z.free()
	def setPower(self, power):
		self.x.setPower( power )
		self.y.setPower( power )
		self.z.setPower( power )
	#def lockout():
	#keep sending power down commands to all board every second

	
cartesian = cartesianClass()

#wait on serial only when after somthing? or do pics send messages without pc request?






