#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <termios.h>
#include <fcntl.h>
#include <signal.h>
#include <syslog.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>

// Compile command:
// gcc -o serial serial.c

int init_port(const char *pathname, long int speed);
int init_tty(struct termios *old_termios);
void serial2tty(int port);
void sigint(int signal);

char buf[128];
int port;
struct termios old_stdin_termios;

int main(int argc, char **argv)
{
   long int speed = 115200;

   openlog("serial", LOG_CONS | LOG_PID, LOG_USER);
   if(argc < 2 || argc > 3)
   {
      syslog(LOG_CRIT, "Usage: serial <port path> [<baud rate>]");
      exit(1);
   }

   // If we do have a baud rate parameter, use it. Else use default value.
   if(argc == 3) speed = atol(argv[2]);

   if((port = init_port(argv[1], speed)) == -1)
   {
      syslog(LOG_CRIT, "Error opening serial port (quitting): %m");
      perror("init_port");
      exit(1);
   }
#if defined(DEBUG)
   else
   {
      puts("Serial port opened successfully.");
   }
#endif

   if(init_tty(&old_stdin_termios))
   {
      syslog(LOG_CRIT, "Error initializing the tty (continuing): %m");
   }
#if defined(DEBUG)
   else
   {
      puts("tty initialized successfully.");
   }
#endif

   signal(SIGINT, sigint);
   serial2tty(port); // main loop
   exit(0); // probably never happens
}

/* init_port: Initialize the serial port.
 * Accepts a string for the filename to open and initialize.
 * Accepts a long int for baud rate.
 * Returns a filehandle for the initialized serial port, or -1 on failure.
 */
int init_port(const char *pathname, long int speed)
{
   int port;
   speed_t realspeed;
   struct termios port_termios;
   
   switch(speed)
   {
      case 50: realspeed = B50; break;
      case 75: realspeed = B75; break;
      case 110: realspeed = B110; break;
      case 134: realspeed = B134; break;
      case 150: realspeed = B150; break;
      case 200: realspeed = B200; break;
      case 300: realspeed = B300; break;
      case 600: realspeed = B600; break;
      case 1200: realspeed = B1200; break;
      case 1800: realspeed = B1800; break;
      case 2400: realspeed = B2400; break;
      case 4800: realspeed = B4800; break;
      case 9600: realspeed = B9600; break;
      case 19200: realspeed = B19200; break;
      case 38400: realspeed = B38400; break;
      case 57600: realspeed = B57600; break;
      case 115200: realspeed = B115200; break;
      default: // error
         syslog(LOG_CRIT, "unrecognized baud rate: %d", speed);
         close(port);
         return(-1);
   }

   // Open the port/file.
   if((port = open(pathname, O_RDWR)) < 0)
   {
      // Couldn't do that, give up.
      syslog(LOG_CRIT, "failed to open serial port %s", pathname);
      return(-1);
   }

   // Initialize the termios structure.
   if(tcgetattr(port, &port_termios))
   {
      // Couldn't do that, give up.
      syslog(LOG_CRIT, "failed to get attributes for serial port %s", pathname);
      close(port);
      return(-1);
   }

   cfmakeraw(&port_termios);
   cfsetispeed(&port_termios, realspeed);
   cfsetospeed(&port_termios, realspeed);

   // Apply the new configuration to the port.
   if(tcsetattr(port, TCSANOW, &port_termios))
   {
      syslog(LOG_CRIT, "failed to set attributes for serial port %s", pathname);
      close(port);
      return(-1);
   }

   // At this point I think I've got a valid serial port.
   return(port); // file descriptor on success
}

/* init_tty: initialize stdin and stdout as needed.
 * Accepts a pointer to a struct termios for storing the old termios.
 * Returns 0 on success, -1 on failure. */
int init_tty(struct termios *old_termios)
{
   struct termios stdin_termios;

   // Initialize the termios structure.
   if(tcgetattr(STDIN_FILENO, &stdin_termios))
   {
      // Couldn't do that, give up.
      syslog(LOG_CRIT, "failed to get attributes for stdin");
      return(-1);
   }

   *old_termios = stdin_termios;
   stdin_termios.c_lflag &= ~(ECHO | ICANON);

   // Apply the new configuration to the port.
   if(tcsetattr(STDIN_FILENO, TCSANOW, &stdin_termios))
   {
      syslog(LOG_CRIT, "failed to set attributes for stdin");
      return(-1);
   }

   return(0);
}

/* serial2tty: perform the actual data transfer between the serial port and
 * stdin/stdout. */
void serial2tty(int port)
{
   int pid, n;

   pid = fork();
   if (pid == -1) {
      syslog(LOG_CRIT, "Error in fork (quitting): %m");
      perror("fork");
      exit(1);
   }
   if (pid == 0) {
      /* child copies serial to stdout */
      while(1) {
         char *bufp = buf;
         n = read(port, buf, sizeof buf);
         if (n == -1) {
            syslog(LOG_ERR, "serial port read error: %m");
            continue;
         }
         while (n > 0) {
            int m = write(1, bufp, n);
            if (m == -1)
               syslog(LOG_ERR, "stdout write error: %m");
            else if (m == 0)
               syslog(LOG_WARNING, "stdout 0 write??");
            n -= m;
            bufp += m;
         }
      }
   } else {
      /* parent copies stdin to serial  */
      while(1) {
         char *bufp = buf;
         n = read(0, buf, sizeof buf);
         if (n == -1) {
            syslog(LOG_ERR, "stdin read error: %m");
            continue;
         }
         if (n == 0) {
           kill(pid, SIGTERM);
           kill(getpid(), SIGINT);
         }
         while (n > 0) {
            int m = write(port, bufp, n);
            if (m == -1)
               syslog(LOG_ERR, "serial write error: %m");
            else if (m == 0)
               syslog(LOG_WARNING, "serial 0 write??");
            n -= m;
            bufp += m;
         }
      }
   }
}

/* sigint: Handle a ctrl-c (SIGINT) signal. */
void sigint(int signal)
{
#if defined(DEBUG)
   puts("Caught SIGINT.");
#endif
   if(tcsetattr(STDIN_FILENO, TCSANOW, &old_stdin_termios))
   {
      syslog(LOG_CRIT, "failed to set attributes for stdin");
      exit(1);
   }
   exit(0);
}

