/*
 *  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_fork.c    --  terminal program using two processes.
 *
 *  $Id: term_fork.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
 *  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 <setjmp.h>
#include <signal.h>
#include <string.h>
#include <termios.h>
#include <time.h>        /* prototype for nanosleep() */
#include <sys/types.h>
#include <sys/ioctl.h>   /* defines N_TTY */

/*************************   CONFIGURATION   **************************/
#define SERIALDEVICE "/dev/ttyS0" /* serial port device special file  */
#define LOCALECHO    0            /* 1 enable local echo              */
#define EXITSEQUENCE 3            /* ^C causes exit                   */

#define BAUD         B57600       /* B9600, B38400, B57600, B11500... */
#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);

struct termios otty;
jmp_buf        env;

/*
 * 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;
  struct sigaction sa;
  
  /* 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 */
    /* set up sig handler and a graceful exit route */
    sa.sa_handler = sig_handler;
    sa.sa_flags   = 0;
    {
      int n;
      for (n = 0; n <= SIGUNUSED; ++n) {
	sigaction(n, &sa, NULL);
      }
    }

    if (0 == setjmp(env)) {
      serial_output(fd);
    }

    kill(pid, SIGKILL);  /* kill the child if it still exists */
    tcsetattr(STDIN_FILENO, TCSANOW, &otty);
    close(fd);
    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;

#if !LOCALECHO
    close(STDOUT_FILENO);              /* stdout not needed */
#endif

  /* 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) {
    switch (ch = getchar()) {
    case EXITSEQUENCE:
      fprintf(stderr, "Exit terminal program?  Y/n ?\b");
      if (('N' == (ch = getchar())) || 'n' == ch) {
	fprintf(stderr, "\rExit aborted... continuing.      \r\n");
	break;
      }
      /* fall through */
    case EOF:
      stop = 1;
      fprintf(stderr, "\n\r\n");
      break;
    default:
      c = (unsigned char) ch;
      write(fd, &c, 1);
#if LOCALECHO
      write(STDOUT_FILENO, &c, 1);
#endif
    }
  }
}


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

  /* get input from serial port and write it to stdout */
  while (1) {
    switch (cnt = read(fd, &s, 1)) {
    default: /* write char to stdout */
      write(STDOUT_FILENO, &s, cnt);
      break;
    case -1:
      if (errno != EAGAIN) {
	perror("serial port read()");
	return;
      }
    case  0:
      break;
    }
  }
}


/*
 * Provide the parent process a graceful exit from
 * its read/write loop.
 *
 * Note that arg2 to longjmp() causes setjmp() to return
 * sig.  There is no significance to using sig other than
 * to prevent gcc from complaining that sig is unused.
 */
void
sig_handler(int sig)
{
  longjmp(env, sig);
}

/*
 *  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);
	perror("tcflush: ");
	return -1;
      }

    } else {
      perror("fcntl:");
      close(fd);
      return -1;
    }
  }

  return fd;
}


/*
 * configure the serial port
 *
 *    hardware flow control, 8n1, full duplex, and
 *    single character raw i/o with blocking enabled.
 */

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

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

  tcgetattr(fd, &tty);       

#define CFLAGS  (FLOWCTL | BITS | CTLLOCAL | CREAD)
#define IFLAGS  (IGNBRK | PARITY)
  
  /* raw io, hardware flow control, 8n1 */
  tty.c_iflag     = IFLAGS; /* input flags          */
  tty.c_cflag     = CFLAGS; /* control flags        */
  tty.c_lflag     = 0;      /* local flags          */
  tty.c_oflag     = 0;      /* output flags         */
  tty.c_cc[VMIN]  = 1;      /* wait for 1 character */
  tty.c_cc[VTIME] = 0;      /* turn off timer       */

#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)) {
    perror("tcsetattr");
    return -1;
  }

  /* verify the changes were actually made */
  return memcmp(&tty, &stty, sizeof (struct termios)) ? -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 | ISIG  | ECHO | ECHOCTL);
  tty.c_iflag &= ~(INLCR  | IGNCR | ICRNL);

  tty.c_cc[VMIN]  = 1;  /* wait for 1 character   */
  tty.c_cc[VTIME] = 0;  /* turn off timer         */

  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 0;
}