/*
 *  Terminal Program Examples
 *  Copyright 2003 by Floyd L. Davidson, floyd@barrow.com
 *
 *  This program is free software; you can redistribute it
 *  and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation;
 *  either version 2, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be
 *  useful, but WITHOUT ANY WARRANTY; without even the implied
 *  warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 *  PURPOSE.  See the GNU General Public License for details.
 *
 *  term_canon.c   --  terminal program using canonical input.
 *
 *  $Id: term_canon.c,v 1.1.0.1 2003/11/28 16:08:38 floyd Exp floyd $
 */

/*
 *  A simple terminal program useful for testing
 *  serial ports, devices connected to serial ports,
 *  and programs for serial ports.
 *
 *  While this is a useful program, its intended function
 *  is to demonstrate how to use fork() to multiplex
 *  canonical input and output when programming a serial port.
 */

/*
 * One of _BSD_SOURCE, _SVID_SOURCE, or _GNU_SOURCE
 * must be defined to allow invoking gcc with the
 * -ansi switch.  Otherwise, __USE_MISC is not defined
 * in /usr/include/features.h, and CRTSCTS is then
 * undefined in /usr/include/bits/termios.h.
 */

#define _GNU_SOURCE 1

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>      /* defines MAX_INPUT         */
#include <signal.h>
#include <string.h>      /* prototype for strlen()    */
#include <termios.h>
#include <time.h>        /* prototype for nanosleep() */
#include <sys/types.h>
#include <sys/ioctl.h>   /* defines N_TTY             */

/*************************   CONFIGURATION   **************************/
#define SERIALDEVICE "/dev/modem" /* serial port device special file  */
#define EXITSEQUENCE 3            /* ^C causes exit                   */

#define BAUD         B57600       /* B9600, B38400, B57600, B115000...*/
#define BITS         CS8          /* CS5, CS6, CS7, CS8               */
#define PARITY       IGNPAR       /* IGNPAR, PARENB, PARENB | PARODD  */
#define FLOWCTL      CRTSCTS      /* CRTSCTS, IXON | IXOFF | IXANY    */
#define CTLLOCAL     0            /* CLOCAL or 0                      */
/**********************************************************************/

void serial_output(int);
void serial_input(int);
void sig_handler(int);
int  serial_open(char *);
int  serial_cnfg(int);
int  stdin_cnfg(int);
int  device_init(int);
int  getch(void);
int  kbhit(void);

struct termios otty;

/*
 * device (e.g., modem) init strings.
 *
 * each 'istring' will be sent in order,
 *  with a 'idelay' seconds of sleep time following
 */
struct devinit {
  char *istring;
  int  idelay;
} devinit[] = {
  {"ATZ\r\n",           2},
  {"ATL3V1E1Q0&D2\r\n", 0}
};
#define NUMSTRINGS  (sizeof devinit / sizeof *devinit)


int
main(void)
{
  int   fd;
  pid_t pid;

  /* open the serial port */
  if (0 > (fd = serial_open(SERIALDEVICE))) {
    perror("open:  " SERIALDEVICE);
    exit(EXIT_FAILURE);
  }

  /* configure the serial port */
  if (0 != serial_cnfg(fd)) {
    perror("config:  " SERIALDEVICE);
    exit(EXIT_FAILURE);
  }

  /* re-configure stdin */
  if (0 != stdin_cnfg(STDIN_FILENO)) {
    perror("config:  stdin");
    exit(EXIT_FAILURE);
  }

  /* terminal settings done, now handle in/ouput */
  switch (pid = fork()) {
  case -1:  /* failed to fork */
    perror("fork");
    tcsetattr(STDIN_FILENO,  TCSANOW, &otty);
    close(fd);
    exit(EXIT_FAILURE);
  case 0:   /* child process */
    serial_input(fd);
    close(fd);
    exit(EXIT_SUCCESS);   /* sends SIGCHLD to parent */
  default:  /* parent */
    serial_output(fd);
    tcsetattr(STDIN_FILENO, TCSANOW, &otty);
    close(fd);
    kill(pid, SIGKILL);  /* kill the child if it still exists */
    break;
  }

  return EXIT_SUCCESS;
}

/*
 *  Serial port output process.
 *
 *  Data from stdin is output to the serial port.
 */
void
serial_output(int fd)
{
  unsigned char c;
  int           ch;
  int           stop = 0;

  /* init the device on the serial port */
  if (0 > device_init(fd)) {
    perror("initialize device");
    return;
  }

  /* get input from keyboard and write to serial port */
  while (!stop) {
    if (0 < kbhit()) {
      ch = getch();
    } else {
      continue;
    }
    switch (ch) {
    case EXITSEQUENCE:
      fprintf(stderr, "\b \r\nExit terminal program?  Y/n ?\b");
      if (('N' == (ch = getch())) || 'n' == ch) {
	fprintf(stderr, "\rExit aborted... continuing.      \r\n");
	break;
      }
      /* fall through */
    case EOF:
      stop = 1;
      fprintf(stderr, "\r\n");
      break;
    default:
      c = (unsigned char) ch;
      write(fd, &c, 1);
    }
  }
}


/*
 *  Serial port input process.
 *
 *  Input from the serial port is output to stdout.
 */
void
serial_input(int fd)
{
  char s[MAX_INPUT];
  FILE *fp;

  close(STDIN_FILENO);

  if (NULL == (fp = fdopen(fd, "r"))) {
    return;
  }

  /* get input from serial port and write it to stdout */
  while (fgets(s, sizeof s, fp)) {
    fprintf(stderr, "%s", s);
  }
}


/*
 *  Open serial port for reading and writing:
 *
 *     Flag it not to be a controlling tty to prevent
 *     killing the process with garble from the port.
 *
 *     Flag it as non-blocking to allow open() to
 *     return even if serial port DCD line indicates
 *     no carrier detect.
 *
 *     Remove non-blocking flag if successful, so that
 *     read() and write() can block.
 */
int
serial_open(char *device)
{
  int fd, oldflags;

  /* O_NONBLOCK allows open even with no carrier detect */
  if (-1 != (fd = open(device, O_RDWR | O_NOCTTY | O_NONBLOCK))) {
    /* clear O_NONBLOCK to allow read() and write() to block */
    if ((-1 != (oldflags = fcntl(fd, F_GETFL, 0))) &&
	(-1 != fcntl(fd, F_SETFL, oldflags & ~O_NONBLOCK))) {

      /* flush input and output */
      if (-1 == tcflush(fd, TCIOFLUSH)) {
	close(fd);
	return -1;
      }

    } else {
      close(fd);
      return -1;
    }
  } else {
    int x = errno;
    fprintf(stderr,"open failed, errno = %d\n", x);
  }

  return fd;
}

/*
 * configure the serial port
 *
 *    flow control, bitrate, parity, and the number
 *    of stop bits are set by #defines at the beginning
 *    of this file.
 *
 *    single character raw i/o with blocking enabled,
 *    plus output processing to change NL to CRNL.
 */

#define TERMSIZE (offsetof (struct termios, c_cc[NCCS]))

int
serial_cnfg(int fd)
{
  struct termios tty, stty;

  tcgetattr(fd, &tty);

#define IFLAGS  (IGNBRK  | PARITY)
#define CFLAGS  (FLOWCTL | BITS | CREAD | CTLLOCAL)
#define LFLAGS  (ICANON)
#define OFLAGS  (ONLCR   | OPOST)

  /* raw io, hardware flow control, 8n1 */
  tty.c_iflag = IFLAGS;     /* input flags          */
  tty.c_cflag = CFLAGS;     /* control flags        */
  tty.c_lflag = LFLAGS;     /* local flags          */
  tty.c_oflag = OFLAGS;     /* output flags         */
  tty.c_cc[VINTR]  = 0;     /* */
  tty.c_cc[VQUIT]  = 0;     /* */
  tty.c_cc[VERASE] = 0;     /* */
  tty.c_cc[VKILL]  = 0;     /* */
  tty.c_cc[VEOF]   = 0;     /* */
  tty.c_cc[VEOL]   = 0;     /* */
  tty.c_cc[VEOL2]  = 0;     /* */

#ifdef __linux__
  /* for linux only */
  tty.c_line      = N_TTY;  /* set line discipline  */
#endif

  cfsetospeed(&tty, BAUD);  /* set bit rate         */
  cfsetispeed(&tty, BAUD);

  if (tcsetattr(fd, TCSADRAIN, &tty) || tcgetattr(fd, &stty)) {
    return -1;
  }

  /* verify the changes were actually made */
  return memcmp(&tty, &stty, TERMSIZE) ? -1 : 0;
}


/*
 * re-configure stdin
 *
 *    hardware flow control, 8n1, full duplex, and
 *    single character raw i/o with blocking enabled.
 */
int
stdin_cnfg(int fd)
{
  struct termios tty, stty;

  tcgetattr(fd, &tty);
  otty = tty;    /* save old settings      */

  /*
   * change only what we must...
   */
  tty.c_lflag      = (ICANON | ECHO);
  tty.c_iflag      = ICRNL;
  tty.c_cc[VERASE] = 8;
  tty.c_cc[VINTR]  = 127;

  if (tcsetattr(fd, TCSADRAIN, &tty) || tcgetattr(fd, &stty)) {
    return -1;
  }

  /* verify the changes were actually made */
  return memcmp(&tty, &stty, TERMSIZE) ? -1 : 0;
}


/*
 * send init strings to the device connected to the serial port
 */
int
device_init(int fd)
{
  unsigned int    n;
  struct timespec tv;

  for (n = 0; n < NUMSTRINGS; ++n) {
    write(fd, devinit[n].istring, strlen(devinit[n].istring));
    tv.tv_sec  =  devinit[n].idelay;
    tv.tv_nsec = 0;
    nanosleep(&tv, NULL);
  }
  return 1;
}

/*
 *  kbhit()  --  a keyboard lookahead monitor
 *
 *  returns the number of characters available to read.
 *
 *  Note that we do not verify the tcsetattr() calls in
 *  this function, and assume that since it worked when
 *  called in the configuration function that it will
 *  also work here.  We hope...
 */
int
kbhit(void)
{
  int		  count = 0;
  int		  error;
  struct timespec tv;
  struct termios  tty, ntty;

  tcgetattr(STDIN_FILENO, &tty);
  ntty = tty;
  ntty.c_lflag 	 &= ~ICANON;
  if (0 == (error = tcsetattr(STDIN_FILENO, TCSANOW, &ntty))) {
    error        += ioctl(STDIN_FILENO, FIONREAD, &count);
    error        += tcsetattr(STDIN_FILENO, TCSANOW, &tty);
    tv.tv_sec     = 0;
    tv.tv_nsec    = 10;
    nanosleep(&tv, NULL);
  }
  return error == 0 ? count : -1;
}

/*
 *  getch()  --  a blocking single character input from stdin
 *
 *  Returns a character, or -1 if an input error occurs.
 *
 *  Note that we do not verify the tcsetattr() calls in
 *  this function, and assume that since it worked when
 *  called in the configuration function that it will
 *  also work here.  We hope...
 */
int
getch(void)
{
  unsigned char  ch;
  int		 error;
  struct termios tty, ntty;

  tcgetattr(STDIN_FILENO, &tty);
  ntty = tty;

  ntty.c_lflag    &= ~ICANON;
  ntty.c_lflag    &= ~ECHO;
  ntty.c_cc[VMIN]  = 1;
  ntty.c_cc[VTIME] = 0;

  if (0 == (error = tcsetattr(STDIN_FILENO, TCSANOW, &ntty))) {
    error  = read(STDIN_FILENO, &ch, 1 );
    error += tcsetattr(STDIN_FILENO, TCSANOW, &tty);
  }
  return (error == 1 ? (int) ch : -1 );
}