/*
 *  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_select.c  --  terminal program using select().
 *
 *  $Id: term_select.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 select() 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 <string.h>      /* prototype for strlen()    */
#include <termios.h>
#include <time.h>        /* prototype for nanosleep() */
#include <sys/time.h>    /* for select()              */
#include <sys/ioctl.h>   /* defines N_TTY             */
#include <sys/types.h>

/*************************   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                      */
/**********************************************************************/

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) {
    unsigned char  s[MAX_INPUT];
    int            rd_count = 0;
    int            wr_count = 0;
    unsigned int   wr_offset;
    int            retval;
    fd_set         rfds;
    struct timeval tv;

    /* Watch both stdin and serial port for data */
    FD_ZERO(&rfds);
    FD_SET(STDIN_FILENO, &rfds);
    FD_SET(fd, &rfds);
    /*
     * minimal wait time... which is enough to give up
     * our cpu time slice if we can't read yet.
     */
    tv.tv_sec  = 0;
    tv.tv_usec = 1;

    if (-1 == (retval = select(STDIN_FILENO > fd ? STDIN_FILENO + 1 : fd + 1,
			       &rfds, NULL, NULL, &tv))) {
      perror("select()");
      exit(EXIT_FAILURE);
    }

    if (retval &&  FD_ISSET(fd, &rfds)) {
      /* read input from serial port */
      if (-1 == (rd_count = read(fd, &s, MAX_INPUT))) {
	/* read error */
	perror("serial port read()");
	exit(EXIT_FAILURE);
      }
    }
    
    /* loop until all data is sent to stdout */
    wr_offset = 0;
    while (rd_count > 0) {
      if (-1 == (wr_count = write(STDOUT_FILENO, &s + wr_offset, rd_count))) {
	perror("write stdout");
	exit(EXIT_FAILURE);
      }
      rd_count  -= wr_count;
      wr_offset += wr_count;
    }

    if (retval && FD_ISSET(STDIN_FILENO, &rfds)) {
      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;
	}
	stop = 1;
	tcsetattr(STDIN_FILENO,  TCSANOW, &otty);
	fprintf(stderr, "\r\n");
	break;
      case EOF:
	break;
      default:
	{
	  unsigned char c;

	  c = (unsigned char) ch;
	  write(fd, &c, 1);
#if LOCALECHO
	  write(STDOUT_FILENO, &c, 1);
#endif
	}
      }
    }
  }
  return EXIT_SUCCESS;
}

/*
 *  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
 *    non-canonical input.
 */

#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 the 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 | 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;
}