/*
 *  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_async.c   --  terminal program using async IO.
 *
 *  $Id: term_async.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 asynchronous IO in one
 *  process 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>      /* 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 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                      */
/**********************************************************************/

volatile  sig_atomic_t  read_flag = 1;

void sig_handler(int);
int  serial_open(char *);
int  serial_cnfg(int);
int  stdin_cnfg(int);
int  device_init(int);

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, ch, stop = 0;

  /* 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);
  }

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

  while (!stop) {

    if (read_flag) {
      unsigned char s[MAX_INPUT];
      int cnt;

      /* sig_handler() has flagged input data is available */
      /* get input from serial port and write it to stdout */
      switch (cnt = read(fd, &s, MAX_INPUT)) {
      default:
	write(STDOUT_FILENO, &s, cnt);  /* write char to stdout */
	break;
      case -1:
	if (errno != EAGAIN) {
	  perror("serial port read()");
	  exit(EXIT_FAILURE);
	}
      case  0:
	break;
      }
      read_flag = 0;
    }

    /* get input from keyboard and write to serial port */
    ch = getchar();

    switch (ch) {
    case EXITSEQUENCE:
      fprintf(stderr, "Exit terminal program?  Y/n ?\b");
      if (('N' == (ch = getchar())) || 'n' == ch) {
	fprintf(stderr, "\rExit aborted... continuing.      \r\n");
	break;
      }
      stop = 1;
      tcsetattr(STDIN_FILENO,  TCSANOW, &otty);
      fprintf(stderr, "\n\r\n");
      break;
    case EOF:
      /*
       * getchar() was interupted by SIGIO, and returns an
       * error when it resumes.  We ignore it.
       */
      break;
    default:
      {
	unsigned char c;
	c = (unsigned char) ch;
        write(fd, &c, 1);
#if LOCALECHO
	write(STDOUT_FILENO, &c, 1);
#endif
      }
    }
  }
  return EXIT_SUCCESS;
}


/*
 * Provide the parent process a graceful exit from
 * its read/write loop.
 *
 * Note that read_flag could be set to 1, but s is used
 * to prevent gcc from complaining that s is unused.
 */
void
sig_handler(int sig)
{
 read_flag = 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);
	return -1;
      }

    } else {
      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;
  struct sigaction sa;

  sa.sa_handler  = sig_handler;
  sa.sa_flags    = 0;
  sa.sa_restorer = NULL;
  sigemptyset(&sa.sa_mask);
  sigaction(SIGIO, &sa, NULL);

  fcntl(fd, F_SETOWN, getpid()); /* allow the process to receive SIGIO    */
  fcntl(fd, F_SETFL,  O_ASYNC);  /* Make the file descriptor asynchronous */

  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

  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 | 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)
{
  int             oldflags;
  unsigned int    n;
  struct timespec tv;

  oldflags = fcntl(fd, F_GETFL);
  /* remove ASYNC flag if it exists */
  fcntl(fd, F_SETFL, oldflags & ~O_ASYNC);
  /*
   *  Note that nonsleep(), usleep() and sleep() all
   *  fail to work if O_ASYNC flag is set!  Unknown why.
   */
  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);
  }
  fcntl(fd, F_SETFL, oldflags);   /* restore flags */
  return 1;
}