/******************************************************************************/
/* Copyright (C) 2020 Garz&Fricke GmbH						 			      */
/*		No use or disclosure of this information in any form without	      */
/*		the written permission of the author							      */
/******************************************************************************/

/******************************************************************************/
/*                                                                            */
/* File:        rs485echo.c                                                   */
/*                                                                            */
/* Description: This is an RS485 echo test designed for EMC verification.     */
/*                                                                            */
/* Total Tests: 1                                                             */
/*                                                                            */
/* Test Name:   rs485echo                                                     */
/*                                                                            */
/* Test Assertion                                                             */
/* & Strategy:  A random test string is sent on the serial port. The opposite */
/*              side is supposed to echo the string back to the sender. The   */
/*              sender compares the sent and received strings. If they are    */
/*              equal, the test is passed, else it fails.                     */
/*                                                                            */
/*              Preceeding the random test string, the sender sends a header  */
/*              consisting of a key word and the data length. The receiver    */
/*              runs as a state machine, waiting for the keyword in its idle  */
/*              state. As soon as the keyword is recognized, it switches to   */
/*              the receive state. When the data has been received completely */
/*              it echoes the string back to the sender and switches to idle  */
/*              state again. If the keyword is recognized within the data,    */
/*              the current string is discarded and the receive state is      */
/*              reset. This ensures that the receiver always goes back to a   */
/*              stable state, even if the data gets corrupted due to outside  */
/*              influences, e.g. EMI.                                         */
/*                                                                            */
/* Author:      Tim Jaacks <tim.jaacks@garz-fricke.com>                       */
/*                                                                            */
/******************************************************************************/

/* Standard Include Files */
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/param.h>
#include <sys/time.h>
#include <time.h>
#define TTYDEFCHARS
#include <termios.h>
#undef TTYDEFCHARS
#include <asm/ioctls.h>
#include <linux/serial.h>

/* Harness Specific Include Files. */
#include "old/test.h"

#include "config.h"

/* Local Defines */
#if !defined(TRUE) && !defined(FALSE)
#define TRUE  1
#define FALSE 0
#endif

#if LTP_VERSION >= 20140115
#define Tst_count tst_count
#endif

/* Possible receiver states */
enum state {
	IDLE,
	RECEIVE_HEADER,
	RECEIVE_DATA_LENGTH,
	RECEIVE_DATA,
};

/* The message structure, including a fixed header, data length and the data */
struct message {
	char header[4];
	int length;
	char data[];
};

/* Extern Global Variables */
extern int  Tst_count;               /* counter for tst_xxx routines.         */
extern char *TESTDIR;                /* temporary dir created by tst_tmpdir() */

/* Global Variables */
char *TCID     = "rs485echo      ";  /* test program identifier.              */
int  TST_TOTAL = 1;                  /* total number of tests in this file.   */
char HEADER[]  = "~.-";              /* data package header                   */

int  fd;                             /* Handle for open serial port           */
char* device_name = "/dev/ttymxc2";  /* String containing the device name     */
struct termios termios;              /* termios structure for serial port     */
int bytes = 8;                       /* Number of bytes to be sent            */
struct message *message;             /* Message to be sent                    */
int size_of_message;                 /* Size of message                       */
char* received_data = NULL;          /* Received string                       */
int repetitions = 1;                 /* Number of repetitions                 */
int timeout_ms = 1000;               /* Receive timeout in milliseconds       */
int baudrate = 9600;                 /* Baud rate in bps                      */
int use_rs485 = TRUE;                /* Use RS485 mode                        */
int use_rtscts = FALSE;                /* Use RTSCTS flowcontrol (not available in RS485 mode)  */
int echo_mode = FALSE;               /* Start in echo mode                    */
enum state state;                    /* Current echo mode state               */
int verbose = FALSE;                 /* Enable more debug messages            */


speed_t baud_constant[] = { B50, B75, B110, B134, B150, B200, B300, B600,
							B1200, B1800, B2400, B4800, B9600, B19200, B38400,
							B57600, B115200, B230400, B460800, B500000,
							B576000, B921600, B1000000, B1152000, B1500000,
							B2000000, B2500000, B3000000, B3500000
						   };

unsigned long baud_value[] = {  50, 75, 110, 134, 150, 200, 300, 600,
								1200, 1800, 2400, 4800, 9600, 19200, 38400,
								57600, 115200, 230400, 460800, 500000,
								576000, 921600, 1000000, 1152000, 1500000,
								2000000, 2500000, 3000000, 3500000
							 };


/* Extern Global Functions */
/******************************************************************************/
/*                                                                            */
/* Function:    cleanup                                                       */
/*                                                                            */
/* Description: Performs all one time clean up for this test on successful    */
/*              completion,  premature exit or  failure. Closes all temporary */
/*              files, removes all temporary directories exits the test with  */
/*              appropriate return code by calling tst_exit() function.       */
/*                                                                            */
/* Input:       None.                                                         */
/*                                                                            */
/* Output:      None.                                                         */
/*                                                                            */
/* Return:      On failure - Exits calling tst_exit(). Non '0' return code.   */
/*              On success - Exits calling tst_exit(). With '0' return code.  */
/*                                                                            */
/******************************************************************************/
extern void
cleanup()
{
	/* Free all allocated memory. */

	if (message)
		free(message);
	if (received_data)
		free(received_data);

	/* Close all open file descriptors. */

	if (close(fd) == -1)
	{
		tst_resm(TWARN, "close(%s) Failed, errno=%d : %s",
		device_name, errno, strerror(errno));
	}

	/* Remove all temporary directories used by this test. */

	// Insert real code here

	/* kill child processes if any. */

	// Insert code here

	/* Exit with appropriate return code. */

	tst_exit();
}


/* Local  Functions */
static char *random_string(char *str, size_t length)
{
	const char charset[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

	for (size_t n = 0; n < length; n++) {
		int key = rand() % (int) (sizeof charset - 1);
		str[n] = charset[key];
	}
	str[length] = 0;

	return str;
}

void sigint_handler(int sig)
{
	cleanup();
	tst_exit();
	exit(0);
}


/******************************************************************************/
/*                                                                            */
/* Function:    setup                                                         */
/*                                                                            */
/* Description: Performs all one time setup for this test. This function is   */
/*              typically used to capture signals, create temporary dirs      */
/*              and temporary files that may be used in the course of this    */
/*              test.                                                         */
/*                                                                            */
/* Input:       None.                                                         */
/*                                                                            */
/* Output:      None.                                                         */
/*                                                                            */
/* Return:      On failure - Exits by calling cleanup().                      */
/*              On success - returns 0.                                       */
/*                                                                            */
/******************************************************************************/
void
setup()
{
	int i;
	struct serial_rs485 rs485;
	speed_t baud = B0;

	// Check if baudrate is supported
	for (i = 0; i < ARRAY_SIZE(baud_constant); i++)
	{
		if (baud_value[i] == baudrate)
		{
			baud = baud_constant[i];
			break;
		}
	}
	if (baud == B0)
		tst_brkm(TBROK, cleanup, "Baud rate not supported (%d)", baudrate);

	if (!echo_mode)
	{
		// Initialize random generator 

        struct timespec now;
        clock_gettime(CLOCK_MONOTONIC_RAW,&now);

		srand(now.tv_sec + now.tv_nsec); 

		// Allocate space for message and construct message header
		size_of_message = sizeof(struct message) + bytes;
		message = malloc(size_of_message + 1);
		memcpy(message->header, HEADER, sizeof(HEADER));
		message->length = bytes;
		message->data[bytes] = 0;
	}

	/* Initialize serial communication */

	// Open port
	fd = open (device_name, O_RDWR | O_SYNC);
	if (fd == -1) {
		tst_brkm(TBROK, cleanup,
			"Unable to open serial port %s.  Error:%d, %s\n",
			device_name, errno, strerror(errno));
	}

	if (use_rs485) {
		// Enable RS485 half duplex mode
		rs485.flags = SER_RS485_ENABLED | SER_RS485_RTS_ON_SEND;
		if (ioctl(fd, TIOCSRS485, &rs485) != 0)
		{
			tst_brkm(TBROK, cleanup,
				"Unable to set RS485 half duplex mode on serial port %s.  Error:%d, %s\n",
				device_name, errno, strerror(errno));
		}
	}

	// Get old attribute
	if (tcgetattr(fd, &termios)) {
		// This is not a serial device
		tst_brkm(TBROK, cleanup,
			"Can't read device configuration. Is this a serial device? \
			Error:%d, %s\n", errno, strerror(errno));
	}

	termios.c_cflag = TTYDEF_CFLAG;
	termios.c_iflag = TTYDEF_IFLAG;
	termios.c_oflag = TTYDEF_OFLAG;
	termios.c_lflag = TTYDEF_LFLAG;
	memcpy(termios.c_cc, ttydefchars, MIN(sizeof(termios.c_cc), sizeof(ttydefchars)));

	// Set Speed into termios structure
	if (cfsetspeed (&termios, baud)) {
		tst_brkm(TBROK, cleanup,
			"Can't set baud rate.  Error:%d, %s\n",
			errno, strerror(errno));
	}

	// Disable Echo, make Data=8, Stop=7, no parity, disable all controls
	// see "man cfmakeraw"
	cfmakeraw (&termios);

	// Set internal timeout value to 1 * 0.1 seconds, i.e. read() will
	// always return after 100 ms, even if there is no character present.
	termios.c_cc[VMIN] = 0;
	termios.c_cc[VTIME] = 1;

	if (use_rtscts) {
		// Enable RTS CTS flowcontrol
		termios.c_cflag |= CRTSCTS;
	}

	// set new attribute
	if (tcsetattr (fd, TCSANOW, &termios)) {
		tst_brkm(TBROK, cleanup,
			"Can't set device configuration.  Error:%d, %s\n",
			errno, strerror(errno));
	}

	return;
}


/******************************************************************************/
/*                                                                            */
/* Function:    help                                                          */
/*                                                                            */
/* Description: This function is called when the test is started with         */
/*              parameter -h. It displays all parameter options specifically  */
/*              available for this test.                                      */
/*                                                                            */
/* Input:       None.                                                         */
/*                                                                            */
/* Output:      None.                                                         */
/*                                                                            */
/* Return:      Nothing.                                                      */
/*                                                                            */
/******************************************************************************/
void help()
{
	printf("  -d s    Select serial port specified by device name s (default: /dev/ttymxc2)\n");
	printf("  -s x    Set baudrate to x bps (default: 9600)\n");
	printf("  -b x    Send x bytes (default: 8)\n");
	printf("  -r x    Repeat send and receive x times (default: 1)\n");
	printf("  -o x    Set receive timeout to x milliseconds (default: 1000)\n");
	printf("  -n      Do not use RS485 mode\n");
	printf("  -c      Enable rtscts flowcontrol, not to be used in RS485 mode\n");
	printf("  -e      Echo mode, wait for data and echo it back\n");
	printf("  -x x    Set delay time between receive and next send in us (default: 300)\n");
	printf("  -t x    Set delay time after receiving the first byte, used to test flow control (default: 0)\n");
	printf("  -v      Verbose\n");
}


/******************************************************************************/
/*                                                                            */
/* Function:    main                                                          */
/*                                                                            */
/* Description: Entry point to this test-case. It parses all the command line */
/*              inputs, calls the global setup and executes the test. It logs */
/*              the test status and results appropriately using the LTP API's */
/*              On successful completion or premature failure, cleanup() func */
/*              is called and test exits with an appropriate return code.     */
/*                                                                            */
/* Input:       Describe input arguments to this test-case                    */
/*               -l - Number of iteration                                     */
/*               -v - Prints verbose output                                   */
/*               -V - Prints the version number                               */
/*                                                                            */
/* Exit:       On failure - Exits by calling cleanup().                       */
/*             On success - exits with 0 exit value.                          */
/*                                                                            */
/******************************************************************************/
int
main(int   argc,    /* number of command line parameters                      */
	 char **argv)   /* pointer to the array of the command line parameters.   */
{
	int cycle_count = 0;
	int byte_count = 0;
	int send_delay = 300;
    int receive_sleep = 0;
	char received_byte;
	int received_state_bytes = 0; // Number of bytes received in current state
	int data_length = 0;
	struct timeval start, now;
	int elapsed_us;
	int timeout_happened = FALSE;
	int mismatch_happened = FALSE;

	int opt_device_name = 0, opt_repetitions = 0, opt_bytes = 0,
		opt_timeout = 0, opt_baudrate = 0, opt_nors485 = 0, opt_pingpong = 0,
		opt_delaytime = 0, opt_verbose = 0, opt_rtscts = 0, opt_receive_sleep = 0;
	char *str_device_name, *str_repetitions, *str_bytes, *str_timeout,
		*str_baudrate, *str_delaytime, *str_receive_sleep;

	option_t options[] = {
		{"d:", &opt_device_name, &str_device_name},
		{"r:", &opt_repetitions, &str_repetitions},
		{"b:", &opt_bytes, &str_bytes},
		{"o:", &opt_timeout, &str_timeout},
		{"s:", &opt_baudrate, &str_baudrate},
		{"x:", &opt_delaytime, &str_delaytime},
		{"e",  &opt_pingpong, NULL},
		{"n",  &opt_nors485, NULL},
		{"c",  &opt_rtscts, NULL},
		{"t:",  &opt_receive_sleep, &str_receive_sleep},
		{"v",  &opt_verbose, NULL},
		{NULL, NULL, NULL}
	};

	// Register ctrl+c handler
    signal(SIGINT, sigint_handler);

	// Parse options
#if LTP_VERSION > 20140115
	tst_parse_opts(argc, argv, options, &help);
#else
	char *msg;
	if((msg = parse_opts(argc, argv, options, &help)) != NULL)
		tst_brkm(TBROK, cleanup, "OPTION PARSING ERROR - %s", msg);
#endif

	if (opt_device_name)
		device_name = str_device_name;
	if (opt_repetitions) {
		repetitions = atoi(str_repetitions);
		if(repetitions < 1) {
			help();
			exit(1);
		}
	}
	if (opt_bytes)
		bytes = atoi(str_bytes);
	if (opt_timeout)
		timeout_ms = atoi(str_timeout);
	if (opt_baudrate)
		baudrate = atoi(str_baudrate);
	if (opt_delaytime)
	{
		send_delay = atoi(str_delaytime);
		tst_resm(TINFO, "Delay %d us", send_delay);
	}
	if (opt_receive_sleep)
	{
		receive_sleep = atoi(str_receive_sleep);
		tst_resm(TINFO, "Receive Delay %d s", receive_sleep );
	}
	if (opt_nors485)
	{
		tst_resm(TINFO, "No RS485 mode");
		use_rs485 = FALSE;
	}
	if (opt_rtscts)
	{
		if(use_rs485){
		    tst_resm(TINFO, "RTS CTS flowcontrol is not compatible with rs485, ignoring the setting.");
		}else{
			tst_resm(TINFO, "Enable RTS CTS flowcontrol");
			use_rtscts = TRUE;
		}
	}
	if (opt_pingpong){
		echo_mode = TRUE;
	}
	if (opt_verbose){
		verbose = TRUE;
	}

	// Perform global test setup
	setup();

	tst_resm(TINFO, "Starting RS485 echo test%s", echo_mode?" in echo mode.":".");

	if (!echo_mode)
	{
		tst_resm(TINFO, "Sending %d bytes to %s (%d,8N1) and waiting for echo.",
				 bytes, device_name, baudrate);
		tst_resm(TINFO, "(repetitions: %d, timeout: %d seconds)", repetitions, timeout_ms);

		received_data = malloc(message->length + 1);
		received_data[message->length] = 0;

		for (cycle_count = 0; cycle_count < repetitions && !timeout_happened &&
				!mismatch_happened; cycle_count++)
		{
			// Generate random string for message
			random_string(message->data, message->length);

			tst_resm(TINFO, "Cycle %d data (first 1000 bytes): %.1000s%s",
					 cycle_count+1, message->data, message->length > 1000 ? "[...]" : "");

			// Send message
			write(fd, (char*)message, size_of_message);

			// Get current time for measuring timeout
			gettimeofday(&start, NULL);
			byte_count = 0;
			while (byte_count < message->length && !timeout_happened)
			{
				// Read serial port
				byte_count += read(fd, &received_data[byte_count], 1);
				// Check if timeout occured
				gettimeofday(&now, NULL);
				
				elapsed_us = (now.tv_sec - start.tv_sec) * 1000000 + now.tv_usec - start.tv_usec;
				if (elapsed_us > timeout_ms * 1000) {
					timeout_happened = TRUE;
					break;
				}
			}

			if (strcmp(message->data, received_data) != 0)
				mismatch_happened = TRUE;

			usleep(send_delay);
		}

		/* Print results and exit test-case */
		if (timeout_happened)
		{
			tst_resm(TFAIL, "Timeout occured. Other side did not respond. Received "
						"only %d bytes in cycle %d", byte_count, cycle_count);
		}
		else if (mismatch_happened)
		{
			tst_resm(TFAIL, "Mismatch. Received data in cycle %d did not equal "
						"sent data.", cycle_count);
		}
		else
		{
			tst_resm(TPASS, "All received strings equaled sent strings. Test passed.");
		}
	}
	else
	{
		// Echo mode
		state = IDLE;
		timeout_happened = FALSE;
		byte_count = 0;

		tst_resm(TINFO, "Waiting for messages on %s (%d,8N1)", device_name, baudrate);

		while(TRUE)
		{
			// Read serial port
			byte_count = read(fd, &received_byte, 1);
			

			if (byte_count < 1)
				continue;

			// Detect header byte regardless of current state
			if (received_byte == HEADER[0])
			{
				if (verbose) {
					tst_resm(TINFO, "Start of message detected");
				}
                if(receive_sleep){ // Sleep X seconds to make the flow control hit in
					sleep(receive_sleep);
                }
				received_state_bytes = 1;
				state = RECEIVE_HEADER;
				if (received_data)
				{
					free(received_data);
					received_data = NULL;
				}
			}
			else switch(state)
			{
				case IDLE:
					// Just waiting for header byte
					break;

				case RECEIVE_HEADER:
					if (received_byte == HEADER[received_state_bytes])
					{
						received_state_bytes++;
						if (received_state_bytes == sizeof(HEADER))
						{
							// Received complete header, move on to next state
							received_state_bytes = 0;
							data_length = 0;
							state = RECEIVE_DATA_LENGTH;
							if (verbose) {
								tst_resm(TINFO, "Reading data length");
							}
						}
					}
					else
					{
						if (verbose) {
							tst_resm(TINFO, "Invalid header");
						}
						// Received wrong header byte, back to idle
						received_state_bytes = 0;
						state = IDLE;
					}
					break;

				case RECEIVE_DATA_LENGTH:
					data_length = data_length | (received_byte << (received_state_bytes * 8));
					received_state_bytes++;
					if (received_state_bytes == sizeof(data_length))
					{
						received_state_bytes = 0;
						// Allocate space for received string
						received_data = malloc(data_length + 1);
						received_data[data_length] = 0;
						state = RECEIVE_DATA;
						if (verbose) {
							tst_resm(TINFO, "Reading %d data bytes", data_length);
						}
					}
					break;

				case RECEIVE_DATA:
					received_data[received_state_bytes] = received_byte;
					received_state_bytes++;
					if (received_state_bytes == data_length)
					{
						// Received complete data, echo it back
						usleep(send_delay);
						tst_resm(TINFO, "Received data: %.1000s%s", received_data,
									data_length > 1000 ? "[...]" : "");
						write(fd, received_data, data_length);
						tst_resm(TINFO, "Echoed data back to sender");
						free(received_data);
						received_data = NULL;
						received_state_bytes = 0;
						state = IDLE;
					}
					break;
			}
		}
	}

	cleanup();
	tst_exit();
}