Linux
Application
Development

Michael K. Johnson
Erik W. Troan

robin.c

/* robin.c -- implements simple serial port interaction program */
#include <sys/time.h>
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <popt.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>             /* for strerror() */
#include <termios.h>
#include <unistd.h>

void usage(int exitcode, char *error, char *addl) {
   fprintf(stderr, "Usage: robin [options] <port>\n"
                   " [options] include:\n"
                   "    -H for this help\n"
                   "    -r for raw mode\n"
                   "    -c to add CR with NL on output\n"
                   "    -h for hardware flow control\n"
                   "    -s for software flow control\n"
                   "    -n for no flow control\n"
                   "    -b <bps> for signalling rate\n");
   if (error) fprintf(stderr, "%s: %s\n", error, addl);
   exit(exitcode);
}

speed_t symbolic_speed(int speednum) { 
   if (speednum >= 460800) return B460800;
   if (speednum >= 230400) return B230400;
   if (speednum >= 115200) return B115200;
   if (speednum >= 57600) return B57600;
   if (speednum >= 38400) return B38400;
   if (speednum >= 19200) return B19200;
   if (speednum >= 9600) return B9600;
   if (speednum >= 4800) return B4800;
   if (speednum >= 2400) return B2400;
   if (speednum >= 1800) return B1800;
   if (speednum >= 1200) return B1200;
   if (speednum >= 600) return B600;
   if (speednum >= 300) return B300;
   if (speednum >= 200) return B200;
   if (speednum >= 150) return B150;
   if (speednum >= 134) return B134;
   if (speednum >= 110) return B110;
   if (speednum >= 75) return B75;
   return B50;
}

/* These need to have file scope so that we can use them in
 * signal handlers */
/* old port termios settings to restore */
static struct termios pots; 
/* old stdout/in termios settings to restore */
static struct termios sots; 
/* port file descriptor */
int    pf;

/* restore original terminal settings on exit */
void cleanup_termios(int signal) {
   tcsetattr(pf, TCSANOW, &pots);
   tcsetattr(STDIN_FILENO, TCSANOW, &sots);
   exit(0);
} 

/* handle a single escape character */
void send_escape(int fd, char c) { 
   switch (c) {
   case 'q':
      /* restore termios settings and exit */
      cleanup_termios(0);
      break;
   case 'b':
      /* send a break */
      tcsendbreak(fd, 0);
      break;
   default:
      /* pass the character through */
      /* "C-\ C-\" sends "C-\" */
      write(fd, &c, 1);
      break;
   }
   return;
}

/* handle escape characters, writing to output */
void cook_buf(int fd, char *buf, int num) { 
   int current = 0;
   static int in_escape = 0;

   if (in_escape) {
      /* cook_buf last called with an incomplete escape sequence */
      send_escape(fd, buf[0]);
      num--;
      buf++;
      in_escape = 0;
   }
   while (current < num) {
#     define CTRLCHAR(c) ((c)-0x40)
      while ((current < num) && (buf[current] != CTRLCHAR('\\'))) current++;
      if (current) write (fd, buf, current);
      if (current < num) {
         /* found an escape character */
         current++;
         if (current >= num) {
            /* interpret first character of next sequence */
            in_escape = 1;
            return;
         }
         send_escape(fd, buf[current]);
      }
      num -= current;
      buf += current;
      current = 0;
   }
   return;
}

int main(int argc, char *argv[]) {
   char    c;            /* used for argument parsing */
   struct  termios pts;  /* termios settings on port */
   struct  termios sts;  /* termios settings on stdout/in */
   char   *portname;
   int    speed = 0;     /* used in argument parsing to set speed */
   struct sigaction sact;/* used to initialize the signal handler */
   fd_set  ready;        /* used for select */
   int     raw = 0;      /* raw mode? */ 
   int     i = 0;        /* used in the multiplex loop */
   int     done = 0;
#  define BUFSIZE 1024
   char    buf[BUFSIZE];
   poptContext optCon;   /* context for parsing command-line options */
   struct poptOption optionsTable[] = {
	    { "bps", 'b', POPT_ARG_INT, &speed, 0 },
	    { "crnl", 'c', 0, 0, 'c' },
	    { "help", 'H', 0, 0, 'H' },
	    { "hwflow", 'h', 0, 0, 'h' },
	    { "noflow", 'n', 0, 0, 'n' },
	    { "raw", 'r', 0, &raw, 0 }, 
	    { "swflow", 's', 0, 0, 's' },
	    { NULL, 0, 0, NULL, 0 }
   };

#ifdef DSLEEP
   /* wait 10 minutes so we can attach a debugger */
   sleep(600);
#endif

   if (argc < 2) usage(1, "Not enough arguments", "");

   /* Normally, we'd let popt figure out which argument is the
      port name. Here we're forcing it to be the last one as it
      eases the rest of the option parsing. */
   portname = argv[argc - 1];
   pf = open(portname, O_RDWR);
   if (pf < 0)
      usage(1, strerror(errno), portname);

   /* modify the port configuration */
   tcgetattr(pf, &pts);
   pots = pts;
   /* some things we want to set arbitrarily */
   pts.c_lflag &= ~ICANON; 
   pts.c_lflag &= ~(ECHO | ECHOCTL | ECHONL);
   pts.c_cflag |= HUPCL;
   pts.c_cc[VMIN] = 1;
   pts.c_cc[VTIME] = 0;

   /* Standard CR/LF handling: this is a dumb terminal.
    * Do no translation:
    *  no NL -> CR/NL mapping on output, and
    *  no CR -> NL mapping on input.
    */
   pts.c_oflag &= ~ONLCR;
   pts.c_iflag &= ~ICRNL;

   /* Now deal with the local terminal side */
   tcgetattr(STDIN_FILENO, &sts);
   sots = sts;
   /* again, some arbitrary things */
   sts.c_iflag &= ~(BRKINT | ICRNL);
   sts.c_iflag |= IGNBRK;
   sts.c_lflag &= ~ISIG;
   sts.c_cc[VMIN] = 1;
   sts.c_cc[VTIME] = 0;
   sts.c_lflag &= ~ICANON;
   /* no local echo: allow the other end to do the echoing */
   sts.c_lflag &= ~(ECHO | ECHOCTL | ECHONL);

   optCon = poptGetContext("robin", argc, argv, optionsTable, 0);

   /* option processing will now modify pts and sts */
   while ((c = poptGetNextOpt(optCon)) >= 0) {
      switch (c) {
         case 'H':
            usage(0, NULL, NULL);
            break;
         case 'c': 
            /* send CR with NL */
            pts.c_oflag |= ONLCR;
            break;
         case 'h': 
            /* hardware flow control */
            pts.c_cflag |= CRTSCTS;
            pts.c_iflag &= ~(IXON | IXOFF | IXANY);
            break;
         case 's':
            /* software flow control */
            pts.c_cflag &= ~CRTSCTS;
            pts.c_iflag |= IXON | IXOFF | IXANY;
            break;
         case 'n':
            /* no flow control */
            pts.c_cflag &= ~CRTSCTS;
            pts.c_iflag &= ~(IXON | IXOFF | IXANY);
            break;
      }
   }

   if (c < -1) {
      /* an error occurred during option processing */
      fprintf(stderr, "%s: %s\n", 
              poptBadOption(optCon, POPT_BADOPTION_NOALIAS),
              poptStrerror(c));
      return 1;
   }
   poptFreeContext(optCon);

   /* speed is not modified unless -b is specified */ 
   if (speed) {
      cfsetospeed(&pts, symbolic_speed(speed));
      cfsetispeed(&pts, symbolic_speed(speed));
   }

   /* set the signal handler to restore the old
    * termios handler */
   sact.sa_handler = cleanup_termios; 
   sigaction(SIGHUP, &sact, NULL);
   sigaction(SIGINT, &sact, NULL);
   sigaction(SIGPIPE, &sact, NULL);
   sigaction(SIGTERM, &sact, NULL);

   /* Now set the modified termios settings */
   tcsetattr(pf, TCSANOW, &pts);
   tcsetattr(STDIN_FILENO, TCSANOW, &sts);

   do {
      FD_ZERO(&ready);
      FD_SET(STDIN_FILENO, &ready);
      FD_SET(pf, &ready);
      select(pf+1, &ready, NULL, NULL, NULL);
      if (FD_ISSET(pf, &ready)) {
         /* pf has characters for us */
         i = read(pf, buf, BUFSIZE);
         if (i >= 1) {
            write(STDOUT_FILENO, buf, i);
         } else {
            done = 1;
         }
      }
      if (FD_ISSET(STDIN_FILENO, &ready)) {
         /* standard input has characters for us */
         i = read(STDIN_FILENO, buf, BUFSIZE);
         if (i >= 1) {
            if (raw) {
               write(pf, buf, i);
            } else {
               cook_buf(pf, buf, i);
            }
         } else {
            done = 1;
         }
      }
   } while (!done);

   /* restore original terminal settings and exit */
   tcsetattr(pf, TCSANOW, &pots);
   tcsetattr(STDIN_FILENO, TCSANOW, &sots);
   exit(0);
}