/* 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 C program for simulating a Belkin Universal UPS or
   compatible. This program will attach itself to a serial port master
   and behave (approximately!) as if it were a Belkin UPS - well, not
   really, actually; it ignores any commands that try to write to a
   register. You run e.g.

   simulator /dev/ptyp0

   and then tell the driver to talk to /dev/ttyp0. There is also the
   notion of a "state file", which is a text file with entries of the
   form register value, as in:

   0x18 1189

   When starting the simulator as

   simulator device statefile
     
   it continually reads the statefile, which you may edit while the
   simulator runs. You can use this to figure out how different
   registers affect the driver.
*/

#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <string.h>
#include <errno.h>
#include <termios.h>
#include <unistd.h>
#include <stdlib.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 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];
#if 0
  const int tiocm_dtr = TIOCM_DTR;
  const int tiocm_rts = TIOCM_RTS;
#endif
  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;
  }

#if 0
  /* 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. */
  r = ioctl(fd, TIOCMBIC, &tiocm_dtr);
  if (r == -1) {
    close(fd);
    return -1;
  }
  r = ioctl(fd, TIOCMBIS, &tiocm_rts);
  if (r == -1) {
    close(fd);
    return -1;
  }
#endif

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

  /* finally, switch to blocking i/o, so that future read/write calls
     will read or write at least one byte */

  r = fcntl(fd, F_SETFL, 0);  /* clear O_NONBLOCK */
  if (r == -1 && errno != EAGAIN) {
    close(fd);
    return -1;
  }

  return fd;
}

#define T_CHR 1
#define T_INT 2
#define T_STR 13

/* belkin register description */
struct reg_s {
  int n;       /* register number */
  char *name;  /* name of register */
  int r;       /* readable */
  int w;       /* writable */
  int typ;     /* type: T_CHR, T_INT, T_STR */
  int val;
  char *strval;
};
typedef struct reg_s reg_t;

reg_t reg[] = {
  { 0x00, "unknown",              0, 0, T_CHR, 0, NULL },
  { 0x01, "voltage rating",       1, 0, T_CHR, 120, NULL }, // confirmed
  { 0x02, "frequency rating",     1, 0, T_CHR, 60, NULL }, // confirmed
  { 0x03, "power rating",         1, 0, T_INT, 800, NULL }, // confirmed
  { 0x04, "batt. voltage rating", 1, 0, T_CHR, 24, NULL },// confirmed
  { 0x05, "unknown",              1, 0, T_CHR, 100, NULL },
  { 0x06, "unknown",              1, 1, T_INT, 90, NULL },
  { 0x07, "unknown",              1, 0, T_INT, 95, NULL },
  { 0x08, "unknown",              1, 0, T_INT, 85, NULL },
  { 0x09, "unknown",              1, 1, T_INT, 136, NULL },
  { 0x0a, "unknown",              1, 0, T_INT, 141, NULL },
  { 0x0b, "unknown",              1, 0, T_INT, 131, NULL },
  { 0x0c, "voltage sensitivity",  1, 1, T_CHR, 2, NULL },  // confirmed - only 0,1,2 make sense in Linux Monitor
  { 0x0d, "UPS model",            1, 0, T_STR, 0, "F6C800-UNV   " }, // displayed by Linux Monitor - can be more bytes
  { 0x0e, "UPS model",            1, 0, T_STR, 0, "F6C800-UNV   " },
  { 0x0f, "firmware/ups type",    1, 0, T_CHR, 0x41, NULL },  // hi nibble confirmed - lo nibble weird: 0=online, 1=offline, 2=line-interactive, 3=online, diagram is messed-up, value is taken mod 3
  { 0x10, "test status",          1, 1, T_CHR, 0, NULL }, // confirmed: 0=No Test Performed, 1=Test Passed (also used for: test canceled), 2=General Test Failed, 3=General Test Failed, 4=Test Aborted 5=Test In Progress, 6+: meaningless
  { 0x11, "audible alarm status", 1, 1, T_CHR, 2, NULL }, // Linux monitor ignores this?
  { 0x12, "unknown",              1, 1, T_CHR, 0, NULL }, 
  { 0x13, "unknown",              1, 1, T_CHR, 0, NULL },
  { 0x14, "unknown",              1, 1, T_CHR, 0, NULL },
  { 0x15, "shutdown timer",       1, 1, T_INT, 0, NULL }, // note: monitor wants to shut down immediately when it sees this, but does not set "time to shutdown" timer.
  { 0x16, "restart timer",        1, 1, T_INT, 0, NULL },
  { 0x17, "unknown/???",          1, 1, T_CHR, 0, NULL }, // if this is non-zero, then Bulldog wants to shut down immediately
  { 0x18, "AC input voltage",     1, 0, T_INT, 1190, NULL }, // confirmed2
  { 0x19, "AC input frequency",   1, 0, T_INT, 549, NULL }, // confirmed2
  { 0x1a, "temperature",          0, 0, T_CHR, 0, NULL }, // this is the temperature in C. My UPS does not have it, but the monitor shows it.
  { 0x1b, "AC output voltage",    1, 0, T_INT, 1075, NULL }, // confirmed2
  { 0x1c, "AC output frequency",  1, 0, T_INT, 519, NULL }, // confirmed2
  { 0x1d, "unknown",              1, 0, T_INT, 250, NULL },
  { 0x1e, "loading level",        1, 0, T_CHR, 46, NULL }, // confirmed2
  { 0x1f, "AC/battery operation", 1, 0, T_CHR, 0x10, NULL },
  { 0x20, "battery voltage",      1, 0, T_INT, 398, NULL }, // confirmed2
  { 0x21, "battery level",        1, 0, T_CHR, 91, NULL }, // confirmed2
  { 0x22, "AC loss/alarm status", 1, 0, T_INT, 0x8000, NULL }, // see statefile
  { 0x23, "AC/battery operation", 1, 0, T_CHR, 0x10, NULL }, // see statefile
  { 0x3f, "time left",            0, 0, T_CHR, 0, NULL }, // see statefile
  { -1, NULL, 0, 0, 0, 0, NULL }
};

/* a curiosity: if 0x1f=00 or 20 on startup, then UPS Health will be
   orange on 22=1 and 22=9. If 0x1f=10 on startup, then UPS Health
   will be green on 22=1 and orange on 22=9. */


typedef unsigned char byte;
  
byte belkin_checksum(byte *buf, int n1) {
  int i;
  byte ck;

  ck = 0;
  for (i=0; i<n1; i++) {
    ck += buf[i];
  }
  return ck;
}  

void read_statefile(char *filename) {
  char buf[2000];
  int i;
  char *p, *l;
  char *r;
  int regn;
  FILE *fin;

  fin = fopen(filename, "r");
  if (!fin) {
    return;
  }
  
  /* update the reg data structure from a file */
  while (1) {
    /* read a line */
    r = fgets(buf, 2000, fin);
    if (!r) {
      break;
    }
    l = buf;
    while (*l==' '  || *l=='\t') {
      l++;
    }
    if (*l=='#') {
      continue;
    }
    regn = strtol(l, &p, 0);
    if (p==l) {
      continue;
    }
    while (*p==' '  || *p=='\t') {
      p++;
    }
    for (i=0; reg[i].n != -1; i++) {
      if (reg[i].n == regn) {
	break;
      }
    }
    if (reg[i].n == -1) {
      continue;
    }
    if (reg[i].typ == T_CHR || reg[i].typ == T_INT) {
      reg[i].val = strtol(p, NULL, 0);
    } else {
      reg[i].strval = strdup(p);
      if (strlen(reg[i].strval)>0) {
	reg[i].strval[strlen(reg[i].strval)-1] = 0;
      }
    }
  }
  fclose(fin);
  return;
}

#define TRY(x) if ((x)==-1) goto try_error
#define BUFLEN 20

int main(int ac, char *av[]) {
  char *name = av[0];
  char *dev;
  int fdin, fdout;
  byte buf[BUFLEN];
  int n, i, ck, r;
  reg_t *p;
  int len;
  char *statefile = NULL;

  if (ac != 2 && ac != 3) {
    fprintf(stderr, "Usage: %s device [statefile]\n", name);
    fprintf(stderr, "       %s - [statefile]\n", name);
    exit(1);
  }
  dev = av[1];
  if (ac==3) {
    statefile = av[2];
  }

  if (strcmp(dev, "-")==0) {
    fdin = 0; /* stdin */
    fdout = 1; /* stdout */
  } else {
    TRY(fdin = fdout = belkin_open_tty(dev));
  }

  /* command loop */
  while (1) {
    /* read state file */
    if (statefile) {
      read_statefile(statefile);
    }

    TRY(r = read(fdin, &buf[0], 1));
    if (r==0) {  /* eof */
      return 0;
    }       
    if (buf[0] != 0x7e) {
      fprintf(stderr, "Bad byte: %02x\n", buf[0]);
      continue;
    }
    TRY(read(fdin, &buf[1], 1));
    TRY(read(fdin, &buf[2], 1));
    n = buf[2]+4;
    if (n > BUFLEN) {
      fprintf(stderr, "Bad length: %d\n", n);
      continue;
    }
    for (i=3; i<n; i++) {
      TRY(read(fdin, &buf[i], 1));
    }
    
    /* compute checksum */
    ck = belkin_checksum(buf, n-1);
    if (buf[n-1] != ck) {
      fprintf(stderr, "Bad checksum\n");
      continue;
    }

    /* react to command */
    switch (buf[1]) {
    case 3:  /* query */
      /* find register */
      for (p=reg; p->n != -1; p++) {
	if (p->n == buf[3]) {
	  break;
	}
      }
      if (p->n == -1) {
	goto error_msg;
      }
      switch (p->typ) {
      case T_CHR:
	buf[1] = 5;
	buf[2] = 2;
	buf[4] = p->val;
	buf[5] = belkin_checksum(buf, 5);
	write(fdout, buf, 6);
	break;
      case T_INT:
	buf[1] = 5;
	buf[2] = 3;
	buf[4] = p->val & 0xff;
	buf[5] = (p->val>>8) & 0xff;
	buf[6] = belkin_checksum(buf, 6);
	write(fdout, buf, 7);
	break;
      case T_STR:
	len = strlen(p->strval);
	buf[1] = 5;
	buf[2] = 1+len;
	for (i=0; i<len; i++) {
	  buf[4+i] = p->strval[i];
	}
	buf[4+len] = belkin_checksum(buf, 4+len);
	write(fdout, buf, 5+len);
	break;
      default:
	goto error_msg;
      }
      break;
    case 4:  /* set */
      /* find register */
      for (p=reg; p->n != -1; p++) {
	if (p->n == buf[3]) {
	  break;
	}
      }
      if (p->n == -1 || buf[2] != 3) {
	goto error_msg;
      }
      p->val = buf[4] + 8*buf[5];
      buf[1] = 2;
      buf[6] = belkin_checksum(buf, 6);
      write(fdout, buf, 7);
      break;
    default: /* error */
    error_msg:
      buf[1] = 1;
      buf[n-1] = belkin_checksum(buf, n-1);
      write(fdout, buf, n);
    }
  }
 try_error:
  fprintf(stderr, "%s: %s: %s\n", name, dev, strerror(errno));
  exit(1);
}
