/****************************************************************************/
/*	Copyright (c) 2009, Bendor Research Pty. Ltd. All rights reserved.		*/
/*	This software is released under the GNU General Public License, version	*/
/*	3, with the exemptions that 1) the header files installed by this 	 	*/
/*	package can be included in closed-source code and 2) the libraries		*/
/*	built by this package can be statically linked into a closed-source 	*/
/*	binary. See the file LICENSE for the details.							*/
/*	This software is provided "AS IS", without any warranty or guarantee.	*/
/*	By using this software you acknowledge that neither Bendor Research nor	*/
/*	any of its personnel or affiliates can be held liable for any damages,	*/
/*	including, but not limited to, loss of business, loss of money, loss of */
/*	life, loss of reputation, arising out of this software being faulty or	*/
/*	not performing as advertised, even if Bendor Research was aware of such	*/
/*	faults in the software. You use this software at your own risk, any and */
/*	all liabilities that arise out of your using this software are strictly	*/
/*	yours.																	*/
/****************************************************************************/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/file.h>
#include <sys/ioctl.h>
#include "lpc-internals.h"

static void	_lpc_SerialLog( LPC lpc, const char *ptr, int len, int out );
static void	_lpc_SerialFlushlog( LPC lpc );

/****************************************************************************/
/*						Public functions									*/
/****************************************************************************/

/** @addtogroup lpc_prg_lib */
/** @{ */

/**
*	Open the serial port.
*
*	The function tries to open the serial port. It returns 0 on success
*	and a negative number on failure. In the latter case the error message
*	can be retrieved by a call to the _lpc_ErrorMsg() function.
*
*	@param	lpc		Pointer to the library state descriptor
*
*	@param	name	The name of the serial device to open, such as
*					<tt>/dev/ttyS0</tt> or <tt>/dev/ttyUSB2</tt>. The
*					caller must have read-write permissions on the device.
*
*	@param	lock	The name of the lockfile. If you pass NULL here, then
*					no locking will be used. If you pass a pointer that
*					points to an empty string (i.e. to a 0 byte), then
*					flock() will be called to request an exlusive advisory
*					lock on the file. If the argument points to a string,
*					then it is assumed that it is the full pathname of the
*					lockfile. The caller must have permissions to create
*					a file in in the lockfile directory.
*
*	@return			0 on success, a negative error code on failure.
*/

LPC_ERROR	LPC_SerialOpen( LPC lpc, const char *name, const char *lock )
{
struct termios	new;
int				lfile;
char			temp[ 32 ];

	if ( lpc->ser_port >= 0 ) {

		_lpc_Error( lpc, "Port is already open" );
		return( LPC_ERROR_CONFIG );
	}

	if ( lock && *lock ) {

		// Obtain the lockfile

		if ( ( lfile = open( lock, O_RDWR | O_CREAT | O_EXCL, 0644 ) ) < 0 ) {

			_lpc_Error( lpc, "Can't create lockfile %s:%s",
					    lock, strerror( errno ) );
			return( LPC_ERROR_SYSTEM );
		}

		sprintf( temp, "%ld\n", (long) getpid() );
		write( lfile, temp, strlen( temp ) );
		close( lfile );
	}

	if ( ( lpc->ser_port = open( name, O_RDWR | O_NONBLOCK ) ) < 0 ) {

		_lpc_Error( lpc, "Port open failed:%s", strerror( errno ) );
		if ( lock && *lock ) unlink( lock );
		return( LPC_ERROR_SYSTEM );
	}

	if ( lock && ~ *lock ) {

		if ( flock( lpc->ser_port, LOCK_EX | LOCK_NB ) < 0 ) {

			_lpc_Error( lpc, "Could not get exclusive lock on the port" );
			close( lpc->ser_port );
			lpc->ser_port = -1;
			return( LPC_ERROR_SYSTEM );
		}
	}

	if ( tcgetattr( lpc->ser_port, &lpc->ser_attr ) < 0 ) {

		_lpc_Error( lpc, "Port open failed:%s", strerror( errno ) );
		if ( lock && *lock != 0 ) unlink( lock );
		if ( lock && *lock == 0 ) flock( lpc->ser_port, LOCK_UN );
		close( lpc->ser_port );
		lpc->ser_port = -1;
		return( LPC_ERROR_SYSTEM );
	}

	new = lpc->ser_attr;

	new.c_lflag &= ~( ECHO | ICANON | IEXTEN | ISIG );
	new.c_iflag &= ~( BRKINT | ICRNL | INPCK | ISTRIP );
	new.c_iflag &= ~( IXANY | IGNCR | IUCLC | IXON | IXOFF );
	new.c_iflag |= IGNBRK | IGNPAR;
	new.c_cflag &= ~( CSIZE | PARENB | CRTSCTS );
	new.c_cflag |=  CS8 | CLOCAL | CREAD;
	new.c_oflag &= ~OPOST;
	new.c_oflag |= NL0 | NL1;
	new.c_cc[ VMIN  ] = 0;
	new.c_cc[ VTIME ] = 0;

	cfsetispeed( &new, _lpc_SerialBaud( lpc->ser_baud ) );

	if( tcsetattr( lpc->ser_port, TCSAFLUSH, &new ) < 0 ) {

		_lpc_Error( lpc, "Port open failed:%s", strerror( errno ) );
		if ( lock && *lock != 0 ) unlink( lock );
		if ( lock && *lock == 0 ) flock( lpc->ser_port, LOCK_UN );
		close( lpc->ser_port );
		lpc->ser_port = -1;
		return( LPC_ERROR_SYSTEM );
	}

	if ( lock ) {

		lpc->ser_lock = malloc( strlen( lock ) + 1 );
		strcpy( lpc->ser_lock, lock );
	}

	_lpc_Log( lpc, LPC_LOG_SERIAL, "Opened serial device %s", name );
	return ( 0 );
}

/**
*	Close the serial port.
*
*	If the serial port is open, then the function will close it. If the
*	target was connected, it will now be disconnected. The baud rate
*	settings. This function can not fail.
*
*	@param	lpc		Pointer to the library state descriptor
*/

void		LPC_SerialClose( LPC lpc )
{
	if ( lpc->ser_port >= 0 ) {

		tcsetattr( lpc->ser_port, TCSAFLUSH, &lpc->ser_attr );

		if ( lpc->ser_lock ) {

			if ( *lpc->ser_lock )

				unlink( lpc->ser_lock );
			else
				flock( lpc->ser_port, LOCK_UN );

			free( lpc->ser_lock );
			lpc->ser_lock = NULL;
		}

		close( lpc->ser_port );
		lpc->tgt_chip = NULL;
		lpc->ser_port = -1;
		_lpc_Log( lpc, LPC_LOG_SERIAL, "Closed the serial device" );
	}
}

/**
*	Sets the initial baud rate on the serial port.
*
*	Sets the baud rate of the serial port. After this call the
*	serial port will use the baud rate specified by the argument.
*	Note that if you are already connected to the target, you have
*	to change the target's baud rate <i>before</i> you call this
*	function. The function returns the new baud rate. If called with
*	the argument of 0, then no baud rate change is done, just the
*	current baud rate is returned.
*
*	@param	lpc		Pointer to the library state descriptor
*
*	@param	baud	The desired baud rate, one of 1200, 2400, 4800, 9600,
*					19200, 38400, 57600 or 115200 or 0, if no baud rate
*					change is required.
*
*	@return			The baud rate after the call, or a negative number
*					in case of error.
*/

LPC_ERROR	LPC_SerialBaud( LPC lpc, int baud )
{
struct termios	tio;
int				bval;

	if ( ! baud || lpc->ser_baud == baud ) return( lpc->ser_baud );

	if ( ! ( bval = _lpc_SerialBaud( baud ) ) ) {

		_lpc_Error( lpc, "Illegal baud rate %d", baud );
		return( LPC_ERROR_CONFIG );
	}

	if ( lpc->ser_port >= 0 ) {

		if ( ( tcgetattr( lpc->ser_port, &tio ) < 0 ) ||
			 ( cfsetispeed( &tio, bval ), 0 ) ||
			 ( tcsetattr( lpc->ser_port, TCSAFLUSH, &tio ) < 0 ) ) {

			_lpc_Error( lpc, "%s", strerror( errno ) );
			return( LPC_ERROR_SYSTEM );
		}
	}

	lpc->ser_baud = baud;
	_lpc_Log( lpc, LPC_LOG_SERIAL, "Baudrate changed to %d", baud );
	return( baud );
}

/**
*	Set a modem control signal to the given state.
*
*	Sets the given signal to active or inactive state, depending on the
*	<tt>val</tt> argument.
*
*	@param	lpc		Pointer to the library state descriptor
*
*	@param	rts		Selest which signal to change. If it is non-zero,
*					the the RTS signal is affected, if it is 0, then
*					the DTR is affected.
*
*	@param	val		A logical value. If it is non-zero, then the given
*					modem control signal vill be activated, that is, it
*					will be LOW on the TTL level and positive voltage on
*					the RS-232 line. If it is 0, then the signal will be
*					deactivated, that is, HIGH TTL level and negative voltage
*					on the RS-232 line.
*
*	@return			0 on success, a negative error code on failure.
*/

LPC_ERROR	LPC_SerialSignal( LPC lpc, int rts, int val )
{
	if ( lpc->ser_port < 0 ) {

		_lpc_Error( lpc, "Serial port is not open" );
		return( LPC_ERROR_CONFIG );
	}

	if ( val ) val  = 1;
	if ( rts ) val |= 2;

	switch ( val ) {

		case 0:

			_lpc_Log( lpc, LPC_LOG_SERIAL, "Setting nDTR to high" );
			val = TIOCM_DTR;
			val = ioctl( lpc->ser_port, TIOCMBIC, &val );
			break;

		case 1:

			_lpc_Log( lpc, LPC_LOG_SERIAL, "Setting nDTR to low" );
			val = TIOCM_DTR;
			val = ioctl( lpc->ser_port, TIOCMBIS, &val );
			break;

		case 2:

			_lpc_Log( lpc, LPC_LOG_SERIAL, "Setting nRTS to high" );
			val = TIOCM_RTS;
			val = ioctl( lpc->ser_port, TIOCMBIC, &val );
			break;

		case 3:

			_lpc_Log( lpc, LPC_LOG_SERIAL, "Setting nRTS to low" );
			val = TIOCM_RTS;
			val = ioctl( lpc->ser_port, TIOCMBIS, &val );
			break;

		default:

			_lpc_Error( lpc, "Control signals are not bound" );
			return( -1 );
	}

	if ( val < 0 ) {

		_lpc_Error( lpc, "Signals set ioctl():%s", strerror( errno ) );
		return( LPC_ERROR_SYSTEM );
	}

	return( 0 );
}

/**
*	Writes data to the serial port.
*
*	The function writes data to the serial port. It is pretty much
*	equivalent to a raw write() to the port, with some error checking.
*	The function always writes the entire length of the data.
*
*	@param	lpc		Pointer to the library state descriptor
*
*	@param	ptr		Pointer to the data to write
*
*	@param	len		The number of bytes to write
*
*	@return			The length on success, a negative error code on failure.
*/

LPC_ERROR	LPC_SerialWrite( LPC lpc, const void *ptr, int len )
{
int		res, ret;

	if ( len <= 0 ) return( 0 );

	ret = len;

	_lpc_SerialLog( lpc, ptr, len, 1 );

	for ( ;; ) {

		res = write( lpc->ser_port, ptr, len );

		if ( res < 0 && ( errno == EINTR || errno == EAGAIN ) ) res = 0;

		if ( res >= 0 ) {

			ptr  = (char *) ptr + res;
			len -= res;

			if ( ! len ) {

				_lpc_SerialFlushlog( lpc );
				return( ret );
			}

			usleep( 1000 );
		}
		else {

			len = errno;
			_lpc_SerialFlushlog( lpc );
			_lpc_Error( lpc, "Serial write failed:%s", strerror( len ) );
			return( LPC_ERROR_SYSTEM );
		}
	}
}

/**
*	Reads data from the serial port with a timeout.
*
*	Waits until the requested number of characters is received,
*	a newline was read or the timeout value (given in millisec)
*	elapses. Returns -1 on error or the number of characters
*	received otherwise.
*
*	@param	lpc		Pointer to the library state descriptor
*
*	@param	ptr		Pointer to the buffer where the result is stored.
*					The buffer must be one byte longer than the requested
*					read length, because a 0 byte will be placed after the
*					last received character (makes life easier if you deal
*					with strings, doesn't hurt otherwise).
*
*	@param	len		The max. number of bytes to receive
*
*	@param	tim		Read timeot, in millisecs. This time is the maximum
*					time the function waits from its start, that is, the
*					wait times between characters are accumulated. If the
*					timeout is 0, the the call returns immediately.
*
*	@return			The number of characters received on success, or
*					a negative error code on failure.
*/

LPC_ERROR	LPC_SerialRead( LPC lpc, void *ptr, int len, int tim )
{
struct timeval	tov, beg, end;
fd_set			set;
int				rec, ret, del;

	gettimeofday( &beg, NULL );
	tim *= 1000;

	for ( rec = 0 ;; ) {

		tov.tv_sec  = tim / 1000000;
		tov.tv_usec = tim % 1000000;

		FD_ZERO( &set );
		FD_SET( lpc->ser_port, &set );

		ret = select( lpc->ser_port + 1, &set, NULL, NULL, &tov );

		if ( ret == 1 ) {

			// We can attempt to read

			for ( ;; ) {

				*((char *) ptr) = 0;
				ret = read( lpc->ser_port, ptr, 1 );

				if ( ret == 1 ) {

					_lpc_SerialLog( lpc, ptr, ret, 0 );
					rec++;
					len--;

					if ( ! len || *((char *) ptr) == '\n' ) {

						((char *) ptr)[ 1 ] = 0;
						_lpc_SerialFlushlog( lpc );
						return( rec );
					}

					ptr = (char *) ptr + ret;
					continue;
				}

				if ( ret < 0 ) {

					if ( ( ret = errno ) != EAGAIN && errno != EINTR ) {

						_lpc_SerialFlushlog( lpc );
						_lpc_Error( lpc, "Serial read error:%s",
								    strerror( ret ) );
						return( LPC_ERROR_SYSTEM );
					}
				}

				break;
			}
		}
		else if ( ret == 0 ) {

			// Timeout

			_lpc_SerialFlushlog( lpc );

			if ( tim ) {

				_lpc_Log( lpc, LPC_LOG_SERIAL,
						  "Rx timeout (%dms)", tim / 1000 );
			}

			return( rec );
		}
		else {

			if ( ( ret = errno ) != EINTR ) {

				_lpc_SerialFlushlog( lpc );
				_lpc_Error( lpc, "Serial read error:%s", strerror( ret ) );
				return( LPC_ERROR_SYSTEM );
			}
		}

		gettimeofday( &end, NULL );
		del  = ( end.tv_sec - beg.tv_sec ) * 1000000;
		del += end.tv_usec - beg.tv_usec;

		if ( del > tim ) {

			_lpc_SerialFlushlog( lpc );
			return( rec );
		}
	}
}

/**
*	Get the file descriptor of the serial port.
*
*	This function can be useful when you want to control the serial
*	communication with the target completely.
*
*	The function returns the (non-negative) file descriptor belonging to the
*	already open serial port of -1 if the port is not open. Note that if
*	you modify the serial port state without the knowledge of the library,
*	library functions might not work later.
*
*	@param	lpc		Pointer to the library state descriptor
*	@return			The serial port's file descriptor or -1 if it's not open.
*/

int		LPC_SerialPort( LPC lpc )
{
	return( lpc->ser_port );
}

/** @} */

/****************************************************************************/
/*				Library internally visible functions						*/
/****************************************************************************/

/*
*	Transforms an integer baud rate to a termios value
*	~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*	Returns the termios value or 0 if the baud rate is invalid.
*	Note that only baud rates that the LPC2xxx ISP supports are
*	accepted.
*/

int		_lpc_SerialBaud( int baud )
{
	switch ( baud ) {

		case 9600:		return( B9600 );
		case 19200:		return( B19200 );
		case 38400:		return( B38400 );
		case 57600:		return( B57600 );
		case 115200:	return( B115200 );
		default:		return( 0 );
	}
}

/*
*	Writes a string to the UART
*	~~~~~~~~~~~~~~~~~~~~~~~~~~~
*	It waits until the data is actually out.
*	Returns >=0 on success, -1 on error.
*/

int		_lpc_SerialPuts( LPC lpc, const char *str )
{
int			len;

	if ( ! ( len = strlen( str ) ) ) return( 0 );
	len = LPC_SerialWrite( lpc, str, len );

	// We would do a tcdrain() here, but if you use some USB to serial
	// converter, tcdrain() doesn't work. It is because it can not take the
	// the converter's internal buffers into account. Thus, we calculate the
	// time needed to send the data out and wait that much. Just to be on
	// the safe side, we still call tcdrain() anyway.

	usleep( (int) ( ( len + 1 ) * 1E7 / lpc->ser_baud ) );
	tcdrain( lpc->ser_port );
	usleep( 1000 );

	return( len );
}

/****************************************************************************/
/*							Local functions									*/
/****************************************************************************/

static	void	_lpc_SerialFlushlog( LPC lpc )
{
	if ( lpc->ser_tlog.len ) {

		lpc->ser_tlog.buf[ lpc->ser_tlog.len ] = 0;
		_lpc_Log( lpc, LPC_LOG_SERIAL, "Tx: %s", lpc->ser_tlog.buf );
		lpc->ser_tlog.len = 0;
	}

	if ( lpc->ser_rlog.len ) {

		lpc->ser_rlog.buf[ lpc->ser_rlog.len ] = 0;
		_lpc_Log( lpc, LPC_LOG_SERIAL, "Rx: %s", lpc->ser_rlog.buf );
		lpc->ser_rlog.len = 0;
	}
}

static void	_lpc_SerialLog( LPC lpc, const char *ptr, int len, int out )
{
LOGBUF	*log;
int		chr;

	if ( ! ( lpc->log_mask & ( 1 << LPC_LOG_SERIAL ) ) ) return;

	log = out ? &lpc->ser_tlog : &lpc->ser_rlog;

	while ( len-- ) {

		if ( log->len > BLEN - 6 ) {

			log->buf[ log->len ] = 0;
			_lpc_Log( lpc, LPC_LOG_SERIAL,
					 "%s: %s", out ? "Tx" : "Rx", log->buf );
			log->len = 0;
		}

		chr = *ptr++ & 255;

		if ( chr >= 0x20 && chr <= 0x7e ) {

			log->buf[ log->len++ ] = chr;
		}
		else {

			sprintf( log->buf + log->len, "<%02x>", chr );
			log->len += 4;
		}
	}
}

