/* TUM iLab course
 * 
 * Smart Space Orchestration
 * 
 * 
 * Author		: J. Dichtl
 * Version		: v0.5 (2013-12-11)
 * Description	: Demo code for the iLab "Smart Space Orchestration" exercise
 * History
        v0.6 (2013-12-12) - dichtl:
                            Added support for the photo diode.
 *      v0.5 (2013-12-11) - pahl:
 *                          Changed the template so that it is running with the red
 *                          LED only. Now the students must only add the yellow and
 *                          the green LED and the code for them.
 *                          Plus their peripherals. This should not be too difficult
 *                          as examples for everything are in the code.
 *      v0.4 (2013-07-03) - pahl:
 *                          Removed $ protocol artefact.
 *                          Made the @help output better understandable.
 *                          Highlighted the TODO sections better.
 * 		v0.3 (2013-06-25) - changes to the implementation of the protocol handler
 * 		v0.3 (2013-06-25) - changes to the implementation of the protocol handler
 * 		v0.2 (2013-06-18) - changes to ease implementation of alternative protocol handler
 * 		v0.1 (2013-06-11) - initial demo code
 * 
 * Notes
 * 		(1) the code uses the serial connection to print status and debug messages
 * 		(2) several string constants are wrapped inside the 'F' macro, like that: F("foo bar")
 * 			this is done to not copy the string into the ram, keeping more ram available for the
 * 			application itself. for more info about this topic, see:
 * 			http://playground.arduino.cc/Learning/Memory
 * 
 * TODO (iLab2):
 * 		(1) check if the I/O pins in the code match the wiring on your Arduino and breadboard. have a look at:
 * 			pin_out_table
 * 			pin_in_table
 *      (2) Add constants for your devices in the enumeration.
 *      (3) Implement the message handling to get and set the state of your peripherals.
 *          ! Make sure the buttons and the LEDs have a useful implementation to retrieve
 *          their state.
 *
 *  ATTENTION: The order in the array's matters.
 */





// H E A D E R S
//
//
#include <ctype.h>
#include <SPI.h>
#include <Ethernet.h>



// C O N S T A N T S
//
//
#define PORT					15002			// port number to listen at
#define BAUD_RATE				115200			// Serial baud rate
#define	IN_BUFFER_SIZE			256				// size of the input buffer. keep the memory restrictions in mind when changing this value

#define PIN_SENSOR_TEMP			A0				// A0 is a predifined constant referring to analog pin #0
#define PIN_SENSOR_PHOTO		A1				// anlog pin A1

#define PIN_OUT_COUNT			8				// number of (digital) output pins
#define PIN_IN_COUNT			3				// number of input pins (analog and digital)
#define TIMER_COUNT				2				// number of timers


// S T R U C T U R E S
//
//

/* enumeration to create unique names/ids for the connected devices. these are also used as the index
 * for the pin_out_table (c standard guarantees that these start at 0 and increment by one, unless
 * explicitley asigned otherwise)
 */
typedef enum pin_id_e {
	// output pins
	LED_RED,
	UNUSED_1,
	UNUSED_2,
	LED_SWITCH,
	UNUSED_3,
	UNUSED_4,
	
	// input pins
	SENSOR_TEMP,
	SENSOR_PHOTO,
	SWITCH
	
/**
 * =======================================================================================
 * TODO (iLab2):
 *      (2) Add constants for your devices in the enumeration.
 *   Do not forget to adjust the PIN_IN_COUNT and PIN_OUT_COUNT if needed...
 * =======================================================================================
 */
 
} pin_id_t;

/* enumeration for the parser states */
typedef enum {
	PARSER_INIT,							// should be 0, because we initialize the parser_t structure with zero
	PARSER_GET,
	PARSER_SET
} parser_state_t;

/* structure for the parser */
typedef struct parser_s {
	char			buffer[IN_BUFFER_SIZE];		// input buffer
	int				length;						// how many bytes in the buffer are currently in use
	parser_state_t	state;						// current state of the parser
	/* add your own parser related variables here (if you need any) */
} parser_t;




// V A R I A B L E S
//
//
byte			mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x44 };		// random mac address - must be unique
IPAddress		ip( 192, 168, 0, 46 );								// ip address (in case DHCP fails), check that this is a valid IP address for your network
IPAddress		gateway( 192, 168, 1, 1 );							// unused at the moment
IPAddress		subnet( 255, 255, 0, 0 );							// unused at the moment

EthernetServer	server( PORT );										// create a server that listens on port 'PORT'
EthernetClient	client;												// the client that we are currently processing

unsigned long	bytes_recv = 0;										// total bytes read in send(), for statistics only

unsigned long	timestamp[TIMER_COUNT] = {0};						// array to store timestamps
unsigned long	out_state[PIN_OUT_COUNT] = {0};						// array to remember the current state of the leds (or output pins in general)
int				button_state = 0;									// state of the switch button. the state toggles

/**
 * =======================================================================================
 * TODO (iLab2):
 * 		(1) check if the I/O pins in the code match the wiring on your Arduino and breadboard. have a look at:
 * 			pin_out_table
 * 			pin_in_table
 * =======================================================================================
 */
 
int				pin_out_table[PIN_OUT_COUNT] = {
					/* pins reserved by the ethernet shield: 10, 50, 51, 52. don't use these here */
					5,			// led_red
					6,			// unused, available
					7,			// unused, available
					8,			// led_switch (not accessible via the default protocol)
					11,			// unused, available
					12,			// unused, available
					13,			// timer_1
					14			// timer_0, on-board LED
				};

int				pin_in_table[PIN_IN_COUNT] = {
					/* pins reserved by the ethernet shield: 10, 50, 51, 52. don't use these here
					 * note: don't add any pins here that are also in the pin_out_table. pins are either input
					 * or output, not both */
					20,			// digital input (switch). note: we attach an interrupt handler to this pin in setup()
					A0,			// analog input (temperature sensor)
					A1			// analog input (photo sensor)
				};


// F U N C T I O N S
//
//


/* called once at startup to initialize the programm */
void setup() {
	int			i;
	
	// initialize the Serial interface
	Serial.begin( BAUD_RATE );
	
	// initialize the output pins
	for( i=0; i<PIN_OUT_COUNT; i++ ) {
		pinMode( pin_out_table[i], OUTPUT );
	}
	// initialize the input pin for the switch
	pinMode( pin_in_table[0], INPUT );
	//digitalWrite( pin_in_table[0], HIGH );				// activate internal 20k pullup resistor
	attachInterrupt( 3, switchInterruptHandler, CHANGE );	// interrupt '3' is pin 20
	
	Serial.println( F("initializing network interface") );
	if( Ethernet.begin(mac) != 1 ) {
		// failed to get ip from DHCP - possibly no DHCP found
		Serial.println( F("failed to get an IP address from the DHCP, trying fixed IP address") );
		Ethernet.begin( mac, ip );		// note: this version does not return anything, we cannot detect if there's an error
	}
	
	Serial.print( F("ip address: ") );
	Serial.print( Ethernet.localIP() );
	Serial.print( F(" port: ") );
	Serial.println( PORT );
	
	Serial.println( F("starting server") );
	server.begin();
}


/* called perodically after setup() returned */
void loop() {
	int		c;			// the character that we received via client.read()
	
	client = server.available();
	
	if( !client )
		return;
	
	c = client.read();
	
	switch( c ) {
		
		case '@':		// the protocol starts with an '@' character
			defaultProtocolHandler();
			return;
			
		case -1:
			// this should never happen, because we loop only over the bytes that have already been received
			Serial.println( F("error -- unexpected end of input reached (-1)") );
			return;
	}
}


/* called when we receive a message using the default protocol
 * 
 * the default protocol starts with a '@' (which has already been read when this function is called).
 * the protocol is text based for easy debugging, each command consists of one or more tokens and ends
 * with a line feed and/or carriage return. that way it can be tested using a telnet terminal.
 * 
 * supported commands are:
 * 		hello			single-token command. returns info about the device
 * 		get				used to receive the value of a resource
 * 		set				used to set the value of a resource
 * 
 * example requests:
 * 		"@hello"
 * 		"@get:temp"
 * 		"@set:led_green=1"
 * 
 */
void defaultProtocolHandler() {
	int				in_bytes_count;				// counter how many bytes have been read
	int				i;							// loop counter
	int				c;							// the character that we received via client.read()
	
	parser_t		parser;						// structure for parsing the input
	
	client = server.available();
	
	if( !client )
		return;
	
	// clear the parser structure
	memset( &parser, 0, sizeof(parser) );
	
	Serial.println( F("reading network message") );

	while( (in_bytes_count = client.available()) ) {
		for( i=0; i<in_bytes_count; i++ ) {
			
			// read the input byte
			//
			//
			c = client.read();
			if( c == -1 ) {
				// this should never happen, because we loop only over the bytes that have already been received
				Serial.println( F("error -- unexpected end of input reached (-1)") );
				break;
			}
			bytes_recv++;
			
			// debug output to the console, non-printable characters are displayed as a dot
			if( isprint(c) ) {
				Serial.write( c );
			} else {
				Serial.write( "." );
			}
			
			// check if our buffer is full. if it is full we force a readBuffer() call
			if( parser.length >= IN_BUFFER_SIZE ) {
				parseInput( &parser );
			}
			
			// we break the loop on line breaks - we expect one command/request per line
			if( c == '\r'  ||  c == '\n' )
				break;
			
			// copy byte to buffer
			parser.buffer[parser.length++] = (unsigned char)(0xFF & c);
			
		}
		
		Serial.println( "" );
		parseInput( &parser );
	}
}


/* reads the buffer
 * 
 * note: if the buffer is full, this function MUST reduce the size before returning. otherwise we are caught
 *       in an endless loop
 */
void parseInput( void *ptr ) {
	parser_t	*parser = (parser_t*)ptr;		// dealing with the automatic function declaration of the IDE
	int		status = 0;
	
	Serial.println( F("parseInput") );
	do {
		Serial.print( F("state: ") ); Serial.println( parser->state );
		switch( parser->state ) {
			case PARSER_INIT:
				status = parserReadCmd( ptr );
				break;
			case PARSER_GET:
				status = parserReadGet( ptr );
				break;
			case PARSER_SET:
				status = parserReadSet( ptr );
				break;
			default:
				break;
		}
		Serial.print( F("status: ") );
		Serial.println( status );
	} while( status );
	parser->length = 0;
}


/* reads from the parse buffer, expecting a command token or line break. valid command tokens
 * are: "hello", "get" and "set"
 * 
 * parameter:
 * 		ptr			pointer to a parser_t structure
 * 
 * note (1): reads from the global 'client' variable
 * note (2): this function guarantees that the buffer length on returning will be shorter than before
 * 
 * returns:
 * 		1		read successfull
 * 		0		error (e.g. token incomplete)
 */
int parserReadCmd( void *ptr ) {
	parser_t	*parser = (parser_t*)ptr;		// dealing with the automatic function declaration of the IDE
	char		token[32];						// used for printing a debug message
	
	if( parser->length == 0 )
		return 0;
	
	parser->buffer[parser->length] = 0;
	Serial.println( F("parser buffer:") );		// debug message to the serial console
	Serial.println( parser->buffer );
	
	if( !parserStrcmp(parser, "get:") ) {
		parser->state = PARSER_GET;
		parser->length -= 4;
		memmove( parser->buffer, parser->buffer+4, parser->length );
		return 1;
	} else if( !parserStrcmp(parser, "set:") ) {
		parser->state = PARSER_SET;
		parser->length -= 4;
		memmove( parser->buffer, parser->buffer+4, parser->length );
		return 1;
	} else if( !parserStrcmp(parser, "hello") ) {
		parser->state = PARSER_INIT;
		parser->length -= 5;
		memmove( parser->buffer, parser->buffer+5, parser->length );
		client.println( F("Possible commands: @hello;@get:{timer_0,timer_1,switch,temp,photo,led_red,led_yellow,led_green};@set:{timer_0,timer_1,led_red,led_yellow,led_green}=[value]") );
		return 1;
	} else if( !parserStrcmp(parser, "\n")  ||  !parserStrcmp(parser, "\r") ) {
		// line break and carriage return are just used as end-of-command tokens and are ignored here
		parser->length -= 1;
		memmove( parser->buffer, parser->buffer+1, parser->length );
		return 1;
	} else {
		Serial.print( F("error - unrecognized command token:") );
		memset( token, 0, sizeof(token) );
		memmove( token, parser->buffer, sizeof(token) < parser->length ? sizeof(token)-1 : parser->length );
		Serial.println( token );
		
		parser->length = 0;		// error handling: buffer is completly purged
	}
	
	return 0;
}


/* reads from the parse buffer, expecting a resource identifier
 * 
 * parameter:
 * 		ptr			pointer to a parser_t structure
 * 
 * returns:
 * 		1		success
 * 		0		error
 */
int parserReadGet( void *ptr ) {
	parser_t	*parser = (parser_t*)ptr;		// dealing with the automatic function declaration of the IDE
	char		token[32];
	int			id;
	char		*end_ptr;
	
	parser->state = PARSER_INIT;
	Serial.println( F("parserReadGet") );
	if( !parserStrcmp(parser, "timer_") ) {
		id = strtol( parser->buffer+6, &end_ptr, 10 );
		if( end_ptr == parser->buffer+6 ) {
			client.println( "error: invalid timer index");
			return 0;
		}
		parser->length -= (end_ptr - parser->buffer);
		memmove( parser->buffer, end_ptr, parser->length - (end_ptr - parser->buffer) );
		sendTimer( id );
		return 1;
        }
	if( !parserStrcmp(parser, "switch") ) {
                parser->length -= 6;
		memmove( parser->buffer, parser->buffer+6, parser->length );
		sendValue( SWITCH );
		return 1;
        }
	if( !parserStrcmp(parser, "temp") ) {
                parser->length -= 4;
		memmove( parser->buffer, parser->buffer+4, parser->length );
		sendValue( SENSOR_TEMP );
		return 1;
        }
	if( !parserStrcmp(parser, "photo") ) {
        parser->length -= 5;
		memmove( parser->buffer, parser->buffer+5, parser->length );
		sendValue( SENSOR_PHOTO );
		return 1;
        }
	if( !parserStrcmp(parser, "led_red") ) {
                parser->length -= 7;
		memmove( parser->buffer, parser->buffer+7, parser->length );
		sendValue( LED_RED );
		return 1;
        }

/**
 * =======================================================================================
 * TODO (iLab2):
 *      (3) Implement the message handling to get and set the state of your peripherals.
 *          ! Make sure the buttons and the LEDs have a useful implementation to retrieve
 *          their state.
 *   Implement the GET message handlers to access the state of your devices.
 * =======================================================================================
 */
 
	client.println( "error: unrecognized resource identifier" );
	return 0;
}


/* reads from the parse buffer, expecting a resource identifier
 * 
 * parameter:
 * 		ptr			pointer to a parser_t structure
 * 
 * returns:
 * 		1		success
 * 		0		error
 */
int parserReadSet( void *ptr ) {
	parser_t	*parser = (parser_t*)ptr;		// dealing with the automatic function declaration of the IDE
	char		token[32];
	int			id;
	char		*end_ptr;
	
	parser->state = PARSER_INIT;
	Serial.println( "parserReadSet" );
	
	if( !parserStrcmp(parser, "timer_") ) {
		id = strtol( parser->buffer+6, &end_ptr, 10 );
		if( end_ptr == parser->buffer+6 ) {
			client.println( "error: invalid timer index");
			return 0;
		}
		parser->length -= (end_ptr - parser->buffer);
		memmove( parser->buffer, end_ptr, parser->length );
		setTimer( id );
		return 1;
	}
	if( !parserStrcmp(parser, "led_red=") ) {
		parser->length -= 8;
		memmove( parser->buffer, parser->buffer+8, parser->length );
		return setValue( ptr, LED_RED );
	}

/**
 * =======================================================================================
 * TODO (iLab2):
 *      (3) Implement the message handling to get and set the state of your peripherals.
 *          ! Make sure the buttons and the LEDs have a useful implementation to retrieve
 *          their state.
 *   Implement the SET message handlers to access the state of your devices.
 * =======================================================================================
 */

	client.println( "error: unrecognized resource identifier" );
	return 0;
}


/* compares 'str' with the start of the buffer, returns 0 if they match.
 * 
 * parameter:
 * 		ptr			pointer to a parser_t structure
 * 		str			string to compare against
 * 
 * returns:
 * 		0		'str' is a prefix of buffer
 * 		!0		mismatch
 */
int parserStrcmp( void *ptr, char *str ) {
	parser_t	*parser = (parser_t*)ptr;		// dealing with the automatic function declaration of the IDE
	int			i;
	
	i=0;
	do {
		//Serial.print( "> " ); Serial.print( i ); Serial.print( str[i] ); Serial.print( parser->buffer[i] );
		if( str[i] == 0 )
			return 0;
		if( str[i] == parser->buffer[i] )
			continue;
		return str[i] - parser->buffer[i];
	} while( ++i < parser->length );
	
	return str[i];
}


/* sends the value to the client, the source is identified by the id (e.g. LED_RED)
 * 
 * parameter:
 * 		id			the id, as defined in pin_id_e
 */
void sendValue( int id ) {
	
	switch( id ) {
		case SWITCH:
			client.print( "switch=" );
			client.println( button_state );
			return;
		case LED_RED:
                        client.print( "led_red=" );
                        client.println( out_state[LED_RED] );
                        return;
/**
 * =======================================================================================
 * TODO (iLab2):
 *      (3) Implement the message handling to get and set the state of your peripherals.
 *          ! Make sure the buttons and the LEDs have a useful implementation to retrieve
 *          their state.
 *   Implement the setter to your devices here.
 * =======================================================================================
 */
		case SENSOR_TEMP:
			client.print( "temp=" );
			client.println( analogRead(pin_in_table[1]) );
			return;
		case SENSOR_PHOTO:
			client.print( "photo=" );
			client.println( analogRead(pin_in_table[2]) );
			return;
		default:
			client.println( "error" );
			return;
	}
}


/* reads from the parse buffer, expects to read the value to be set.
 * 
 * This is an example how a 'set-this-resource function' could look like. change it to your liking
 * or write a new one if you want.
 * 
 * parameters:
 * 		ptr			pointer to a parser_t structure
 * 		id			id of the resource, as defined in pin_id_e
 * 
 * returns:
 * 		1		success
 * 		0		error
 */
int setValue( void *ptr, int id ) {
	parser_t	*parser = (parser_t*)ptr;		// dealing with the automatic function declaration of the IDE
	char		*end_ptr;
	int			value;
	
	value = strtol( parser->buffer, &end_ptr, 10 );
	if( end_ptr == parser->buffer ) {
		// if end_ptr is the start of the string, strtol() failed to read a decimal number from the buffer
		client.println( F("error: failed to read value for set command") );
		return 0;
	}
	
	parser->length -= (end_ptr - parser->buffer);
	memmove( parser->buffer, end_ptr, parser->length );

	switch( id ) {
		case LED_RED:
			digitalWrite( pin_out_table[id], value ? HIGH : LOW );
			client.print( "set=" );
			client.println( value );
			out_state[id] = value ? 1 : 0;
			return 1;

/**
 * =======================================================================================
 * TODO (iLab2):
 *      (3) Implement the message handling to get and set the state of your peripherals.
 *          ! Make sure the buttons and the LEDs have a useful implementation to retrieve
 *          their state.
 *   Implement the setter to your devices here.
 * =======================================================================================
 */
 
		default:
			client.println( F("error: invalid set id") );
			return 0;
	}
}


/* sets the value in the timestamp variable at index 'id' to the current timestamp (returned from millis())
 * 
 * timers also set (toggle) the output pin states
 * 
 * parameter:
 * 		id			the id of the timer. an id of 0 translates into the highest available output pin, higher ids
 * 					automatically translate into lower indices in the out_pin_table
 * 
 * note: this function does not expect a 'value' to be passed by the protocol, because it will set the internal time,
 *       not a user-defined value.
 * 
 */
void setTimer( int id ) {
	int			out_id;				// id (index) in the out_pin_table array
	
	if( id < 0  ||  id >= TIMER_COUNT ) {
		Serial.print( "error: invalid timer id: " );
		Serial.println( id );
		return;
	}
	
	timestamp[id] = millis();
	
	client.print( "set:timer_" );
	client.print( id );
	client.print( "=" );
	client.println( timestamp[id] );
	
	out_id = PIN_OUT_COUNT - 1 - id;
	if( out_id < 0 ) {
		Serial.print( F("invalid timer index: ") );
		Serial.print( id );
		Serial.print( ", " );
		Serial.println( out_id );
		return;
	}
	
	out_state[out_id] ^= 1;
	digitalWrite( pin_out_table[out_id], (out_state[out_id] & 1) ? HIGH : LOW );
}


/* sends the current value stored in the timestamp variable at index 'id' to the client */
void sendTimer( int id ) {
	
	if( id < 0  ||  id >= TIMER_COUNT ) {
		client.print( F("error: invalid timer id: ") );
		client.println( id );
		return;
	}
	
	client.print( "timer_" );
	client.print( id );
	client.print( "=" );
	client.println( timestamp[id] );
	
}


/* interrupt handler for the switch, called on every detected falling or rising edge. the code here handles
 * the de-bouncing of the switch.
 * 
 * note: the button can bounce while pushing and releasing. this code should be able to handle both cases
 */
void switchInterruptHandler() {
	static unsigned long	time_last_interrupt = 0;
	unsigned long			time_now = millis();
	
	if( time_now - time_last_interrupt < 15 ) {
		return;
	}
	
	time_last_interrupt = time_now;
	button_state++;
	digitalWrite( pin_out_table[LED_SWITCH], button_state & 0x2 ? HIGH : LOW );
	Serial.println( button_state );
}
