/*
 *  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_thread.c  --  terminal program using threads.
 *
 *  $Id: term_thread.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 threads 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 */
#include <pthread.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                      */
/**********************************************************************/

void serial_output(void *);
void serial_input(void *);
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;
int            fdt;


/*
 * 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              iret;
  int              fd;
  struct sigaction sa;
  pthread_t        th;
  
  /* 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);
  }


  /* Create a thread for serial input */
  iret = pthread_create(&th, NULL, (void *(*)(void*)) serial_input, (void *) &fd);

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

  tcsetattr(STDIN_FILENO, TCSANOW, &otty);
  close(fd);
 
  return EXIT_SUCCESS;
}

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

  fd = *(int *)fdo;

  /* 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(void *fdi)
{
  unsigned char s[MAX_INPUT];
  int           cnt = 0;
  int           fd;

  fd = *(int *)fdi;

  /* get input from serial port and write it to stdout */
  while (1) {
    switch (cnt = read(fd, &s, MAX_INPUT)) {
    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);
	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;

  if (tcgetattr(fd, &tty)) {
    return -2;
  }

#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]  = 100;    /* wait for up to 100 characters   */
  tty.c_cc[VTIME] = 1;      /* 1/10th second interchar timeout */

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

  if (tcgetattr(fd, &tty)) {
    return -2;
  }
  
  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;
}