/* Copyright (C) 2003 Peter Selinger <selinger@users.sourceforge.net>

   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 of the License, 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 more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
   USA
*/

/* A standalone program which can be used to listen to communication
   on a serial port. The idea is to connect two serial ports together.

   Scenario 1: your UPS is connected at /dev/ttyS1, and you tell the
   UPS driver (which runs on the same machine) to connect to
   /dev/ttyp0. Then you run
   
   ttyspy /dev/ptyp0 /dev/ttyS1

   and start the UPS driver program, and voila! The communication
   between the driver and the UPS is established, while ttyspy prints
   a copy of everything to stdout. Note: /dev/ttyp0 is a so-called
   pseudo-terminal, and it is internally connected to /dev/ptyp0, a
   so-called "master". Thus anything you write on /dev/ptyp0 can be
   read on /dev/ttyp0 and vice versa (just like two "real" serial
   ports which have been connected by a null-modem cable). Also note:
   the master must be opened first, so you must start ttyspy before
   starting the UPS driver.

   Scenario 2: your UPS is connected to /dev/ttyS1 on machine A. The
   driver you are testing runs on machine B (which might be running a
   different operating system), and talks to serial port X on machine
   B. You use a null-modem cable to connect serial port X on machine B
   to serial port /dev/ttyS0 on machine A. Then you run

   ttyspy /dev/ttyS0 /dev/ttyS1

   on machine A, and voila! You are listening to the communication
   between the driver and the UPS.  */

/* Note: depending on the nature of the equipment you are connecting,
   you may need to edit the function belkin_open_tty to set the serial
   port parameters correctly. You can use the command "stty -a
   --file=<device>" while another program is running to figure out
   which serial port settings it is using! */

#include <stdio.h>
#include <termio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <time.h>

#define BUFSIZE 4096

#include <termios.h>
#include <unistd.h>

/* Open and prepare a serial port for communication with a Belkin
   Universal UPS.  DEVICE is the name of the serial port. It will be
   opened in non-blocking read/write mode, and the appropriate
   communications parameters will be set.  The device will also be
   sent a special signal (clear DTR, set RTS) to cause the UPS to
   switch from "dumb" to "smart" mode, and any pending data (=garbage)
   will be discarded. After this call, the device is ready for reading
   and writing via read(2) and write(2). Return a valid file
   descriptor on success, or else -1 with errno set. */

int belkin_open_tty(char *device) {
  int fd;
  struct termios tios;
  struct flock flock;
  char buf[128];
  const int tiocm_dtr = TIOCM_DTR;
  const int tiocm_rts = TIOCM_RTS;
  int r;

  /* open the device */
  fd = open(device, O_RDWR | O_NONBLOCK);
  if (fd == -1) {
    return -1;
  }

  /* set communications parameters: 2400 baud, 8 bits, 1 stop bit, no
     parity, enable reading, hang up when done, ignore modem control
     lines. */
  memset(&tios, 0, sizeof(tios));
  tios.c_cflag = B2400 | CS8 | CREAD | HUPCL | CLOCAL;
  tios.c_cc[VMIN] = 1;
  tios.c_cc[VTIME] = 0;
  r = tcsetattr(fd, TCSANOW, &tios);
  if (r == -1) {
    close(fd);
    return -1;
  }

  /* signal the UPS to enter "smart" mode. This is done by setting RTS
     and dropping DTR for at least 0.25 seconds. RTS and DTR refer to
     two specific pins in the 9-pin serial connector. Note: this must
     be done for at least 0.25 seconds for the UPS to react. Ignore
     any errors, in case this is not a "real" serial port. */
  r = ioctl(fd, TIOCMBIC, &tiocm_dtr);
  r = ioctl(fd, TIOCMBIS, &tiocm_rts);

  /* flush both directions of serial port: throw away all data in
     transit */
  r = tcflush(fd, TCIOFLUSH);
  if (r == -1) {
    close(fd);
    return -1;
  }

  /* lock the port */
  memset(&flock, 0, sizeof(flock));
  flock.l_type = F_RDLCK;
  r = fcntl(fd, F_SETLK, &flock);
  if (r == -1) {
    close(fd);
    return -1;
  }

  /* sleep at least 0.25 seconds for the UPS to wake up. Belkin's own
     software sleeps 1 second, so that's what we do, too. */
  usleep(1000000);

  /* flush incoming data again, and read any remaining garbage
     bytes. There should not be any. */
  r = tcflush(fd, TCIFLUSH);
  if (r == -1) {
    close(fd);
    return -1;
  }

  r = read(fd, buf, 127);
  if (r == -1 && errno != EAGAIN) {
    close(fd);
    return -1;
  }

  return fd;
}

int main(int ac, char *av[]) {
  int fdm, fds;  /* master, slave file descriptor */
  char *filem, *files;
  char *name = av[0];
  char inbuf[BUFSIZE];
  char outbuf[BUFSIZE];
  char incount;
  char outcount;
  time_t tm;
  int i;
  int r;

  if (ac != 3) {
    fprintf(stderr, "Usage: %s <tty1> <tty2>\n", name);
    fprintf(stderr, "Example: %s /dev/ptyp0 /dev/ttyS1\n", name);
    exit(1);
  }
  
  filem = av[1];
  files = av[2];

  tm = time(NULL);
  fprintf(stdout, "\n");
  fprintf(stdout, "%s: LOG %s", name, ctime(&tm));
  fprintf(stdout, "IN  = %s to %s\n", files, filem);
  fprintf(stdout, "OUT = %s to %s\n", filem, files);
  fprintf(stdout, "\n");
  fflush(stdout);

  fdm = -1;
  fds = -1;
  goto fail;

 repeat:

  while (1) {
    /* read the next "burst" of characters */
    incount = 0;
    outcount = 0;

    /* read from slave */
    r = read(fds, inbuf, BUFSIZE);
    if (r==-1 && errno==EAGAIN) {  /* non-blocking i/o, no data available */
      /* do nothing */
    } else if (r==0) {
      fprintf(stdout, "%s: 0 characters read (end of file)\n", files);
      fflush(stdout);
    } else if (r==-1) {
      fprintf(stdout, "%s: read error: %s\n", files, strerror(errno));
      fflush(stdout);
      close(fds);
      fds = -1;
      goto fail;
    } else {
      incount = r;
    }
    
    /* read from master */
    r = read(fdm, outbuf, BUFSIZE);
    if (r==-1 && errno==EAGAIN) {  /* non-blocking i/o, no data available */
      /* do nothing */
    } else if (r==0) {
      fprintf(stdout, "%s: 0 characters read (end of file)\n", filem);
      fflush(stdout);
    } else if (r==-1) {
      fprintf(stdout, "%s: read error: %s\n", filem, strerror(errno));
      fflush(stdout);
      close(fdm);
      fdm = -1;
      goto fail;
    } else {
      outcount = r;
    }

    /* log bytes */
    if (incount != 0 || outcount != 0) {
      tm = time(NULL);
      fprintf(stdout, "%s", ctime(&tm));
      fprintf(stdout, "IN:  ");
      for (i=0; i<incount; i++) {
	fprintf(stdout, "%02x ", (unsigned char)inbuf[i]);
      }
      fprintf(stdout, "\n");
      fprintf(stdout, "OUT: ");
      for (i=0; i<outcount; i++) {
	fprintf(stdout, "%02x ", (unsigned char)outbuf[i]);
      }
      fprintf(stdout, "\n");
      fprintf(stdout, "\n");
      fflush(stdout);
    }

    /* write to master */
    if (incount != 0) {
      r = write(fdm, inbuf, incount);
      if (r==-1) {
	fprintf(stdout, "%s: write error: %s\n", filem, strerror(errno));
	fflush(stdout);
	close(fdm);
	fdm = -1;
	goto fail;
      } else if (r<incount) {
	fprintf(stdout, "%s: %d of %d characters written\n", filem, r, incount);
	fflush(stdout);
      }
    }
    
    /* write to slave */
    if (outcount != 0) {
      r = write(fds, outbuf, outcount);
      if (r==-1) {
	fprintf(stdout, "%s: write error: %s\n", files, strerror(errno));
	fflush(stdout);
	close(fds);
	fds = -1;
	goto fail;
      } else if (r<outcount) {
	fprintf(stdout, "%s: %d of %d characters written\n", files, r, outcount);
	fflush(stdout);
      }
    }
    
    if (incount==0 && outcount==0) {
      usleep(1000);
    }
  }
  
 fail:  
  /* we get here if there was a read/write error. This might have
     happened because one of the two sides closed the connection. We
     try to re-open the file which caused the trouble, then flush both
     streams. */

  sleep(1);
  while (fdm==-1) {
    fdm = belkin_open_tty(filem);
    if (fdm==-1) {
      fprintf(stdout, "%s: %s\n", filem, strerror(errno));
      fflush(stdout);
      sleep(1);
    }
  }
  while (fds==-1) {
    fds = belkin_open_tty(files);
    if (fds==-1) {
      fprintf(stdout, "%s: %s\n", files, strerror(errno));
      fflush(stdout);
      sleep(1);
    }
  }

  tcflush(fdm, TCIOFLUSH);
  tcflush(fds, TCIOFLUSH);

  goto repeat;

}
