#include <termios.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <time.h>
#include <sys/time.h>
#include <sys/select.h>
#include <signal.h>
#include <stdlib.h>

/* getspeedbit - Returns the POSIX speed constant corresponding to the integer
 * baud rate argument. For example, for speed = 4800, returns B4800. The
 * result can be used as the argument to cfsetispeed() and friends.
 */
static const speed_t BAD_SPEED = ((speed_t)~0);
speed_t getspeedbit (int speed) {
  switch (speed) {
  case 0:  return B0;
  case 50:  return B50;
  case 75:  return B75;
  case 110:  return B110;
  case 134:  return B134;
  case 150:  return B150;
  case 200:  return B200;
  case 300:  return B300;
  case 600:  return B600;
  case 1200:  return B1200;
  case 1800:  return B1800;
  case 2400:  return B2400;
  case 4800:  return B4800;
  case 9600:  return B9600;
  case 19200: return B19200;
  case 38400: return B38400;
  case 57600:  return B57600;
  case 115200: return B115200;
  case 230400: return B230400;
  default: return BAD_SPEED; 
  }
}

/* setparams - Set the terminal speed, parity & software flow control as
 * specified for the serial line open on fd=TTYFD. Returns -1 on error,
 * 0 on success.
 */
int setparams (int ttyfd, struct termios *old_params, int speed, char parity,
	       int xonxoff) {
  struct termios flags;
  speed_t speed_bit;

  /* Get flags from tty */
  if (tcgetattr (ttyfd, &flags) < 0) {
    perror ("Error: tcgetattr");
    return -1;
  }
  memcpy (old_params, &flags, sizeof (struct termios));

  /* Reset to raw state */
  cfmakeraw (&flags);

  /* Set flow-control flags */
  flags.c_iflag |= IGNBRK;
  if (xonxoff)
    flags.c_iflag |= (IXON|IXOFF);
  else
    flags.c_iflag &= ~(IXON|IXOFF);

  /* Set 8 data bits, 1 stop bit */
  flags.c_cflag &= ~(CSIZE|CSTOPB);
  flags.c_cflag |= CS8;

  /* Set parity */
  switch (parity) {
  case 'n': flags.c_cflag &= ~PARENB; break;
  case 'e': flags.c_cflag = ((flags.c_cflag | PARENB) & ~PARODD); break;
  case 'o': flags.c_cflag |= (PARENB|PARODD); break;
  default:
    fprintf (stderr, "Error: Bad parity parameter '%c': must be n|e|o\n",
	     parity);
    return -1;
  }

  /* Set speed */
  speed_bit = getspeedbit(speed);
  if (speed_bit == BAD_SPEED) {
    fprintf (stderr, "Error: Unknown speed %d\n", speed);
    return -1;
  }
  cfsetispeed (&flags, speed_bit);
  cfsetospeed (&flags, speed_bit);

  /* Write back flags to tty */
  if (tcsetattr (ttyfd, TCSANOW, &flags) < 0) {
    perror ("Error: tcsetattr");
    return -1;
  }
  return 0;
}

/* int_handler - Notify copyloop that we've caught an interrupt.
 */
int caught_interrupt = 0;
void int_handler (int sig) {
  caught_interrupt = 1;
}

long long total;
int lately;
int max_rate;
void update_status_line (int sig) {
  int rate;
  rate = (8*lately/1000);
  if (rate > max_rate) max_rate = rate;
  printf ("\rnow: %d bytes/s (%d kbits/s) - total: %lld bytes - max: "
          "%d kbits/s    ", lately, (8*lately/1000), total, max_rate);
  fflush (stdout);
  lately = 0;
}

/* copyloop - Poll I/O line for 1 sec. Copy any chars received back to sender.
 * Count chars copied. Print status report (bits/sec) to stdout.
 */
#define BUFSIZE 1024
static char buf[BUFSIZE];
void copyloop (int ttyfd) {
  struct itimerval timeo;
  int rv, readcnt;

  timeo.it_value.tv_sec = 1;
  timeo.it_value.tv_usec = 0;
  timeo.it_interval = timeo.it_value;
  signal (SIGALRM, update_status_line);
  setitimer (ITIMER_REAL, &timeo, 0);
  
  total = 0;
  lately = 0;
  while (!caught_interrupt) {
    readcnt = read (ttyfd, &buf, BUFSIZE);
    if (readcnt < 0) {
      if (errno == EAGAIN)
	continue;
      else if (errno != EINTR)
	fprintf (stderr, "Error: read: %s\n", strerror (errno));
      return; /* Terminate on error */
    } else if (readcnt > 0) {
      int chars_left = readcnt;
      while (chars_left > 0) {
        rv = write (ttyfd, &buf[readcnt - chars_left], chars_left);
        if (rv < 0) {
          if (errno == EAGAIN)
            continue;
          fprintf (stderr, "Error: write: %s\n", strerror (errno));
          return;
        }
        chars_left -= rv;
      }
      total += readcnt;
      lately += readcnt;
    }
  }
  printf ("\n\n");
}

void usage (char *myname) {
  printf
    ("Usage: %s [OPTIONS...]\n"
     "\n"
     "where OPTIONS may include any of the following:\n"
     "-l DEV       Communicate on serial device DEV (default /dev/ttyS0)\n"
     "-s NUM       Set baud rate to NUM (default 38400)\n"
     "-n, -o, -e   No, Odd, or Even parity (default No)\n"
     "--xonxoff    Turn on XON/XOFF software flow control (default off)\n"
     "\n"
     "Bugs to brg at dgate.org.\n", myname);
  exit (1);
}

int main (int argc, char **argv) {
  char *line = "/dev/ttyS0", parity = 'n';
  int i, ttyfd, xonxoff = 0, speed = 38400;
  struct termios old_params;

  /* Process arguments. */
  for (i = 1; i < argc && argv[i][0] == '-'; ++i)
    switch (argv[i][1]) {
    case 'l':
      if (argc <= i+1) {
	fprintf (stderr, "Error: Option requires an argument: -%c\n",
		 argv[i][1]);
	usage (argv[0]);
      }
      line = argv[i+1];
      ++i;
      break;
    case 's':
      if (argc <= i+1) {
	fprintf (stderr, "Error: Option requires an argument: -%c\n",
		 argv[i][1]);
	usage (argv[0]);
      }
      speed = atoi(argv[i+1]);
      ++i;
      break;
    case 'n':
    case 'o':
    case 'e':
      parity = argv[i][1];
      break;
    case '-':
      if (strcmp(&argv[i][2], "xonxoff")==0) {
	xonxoff = 1;
      } else {
        fprintf (stderr, "Error: Unknown option: --%s\n", &argv[i][2]);
	usage (argv[0]);
      }
      break;
    default:
      fprintf (stderr, "Error: Unknown option: -%c\n", argv[i][1]);
      usage (argv[0]);
    }

  /* Print status message. */
  printf ("Line is '%s', speed is %d, parity is '%c', xonxoff is %d\n",
	  line, speed, parity, xonxoff);
  printf ("Hit ^C to quit\n");

  /* Open serial line. */
  ttyfd = open (line, O_RDWR|O_NOCTTY|O_NONBLOCK);
  if (ttyfd < 0) {
    fprintf (stderr, "Error: Can't open '%s': %s\n", line, strerror (errno));
    return 1;
  }

  /* Set serial parameters as requested by user. */
  if (setparams (ttyfd, &old_params, speed, parity, xonxoff) < 0)
    return 1; /* Die if there was an invalid parameter or error. */

  /* Set up signal handler. */
  signal (SIGINT, int_handler);

  /* Start copying chars. */
  copyloop (ttyfd);
 
  /* Remove signal handler. */
  signal (SIGINT, SIG_DFL);

  /* Restore old saved flags to tty. */
  if (tcsetattr (ttyfd, TCSANOW, &old_params) < 0)
    perror ("Warning: Could not restore tty flags: tcsetattr");

  /* We're done. */
  close (ttyfd);
  return 0;
}
