/* 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 simple program to communicate with a Belkin UPS on a "console":
   the user types in the following commands:

   set <var> <value>
   get <var>
   state
   quit
   help
*/

#include <sys/ioctl.h>
#include <sys/termios.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <ctype.h>

/* ---------------------------------------------------------------------- */
/* low-level i/o */

/* 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];
  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, as this probably means we are not on a "real" serial
     port. */
  ioctl(fd, TIOCMBIC, &tiocm_dtr);
  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;
  }

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

/* blocking read */
int upsread(int fd, char *buf, int n) {
  int count = 0;
  int r;

  while (count < n) {
    r = read(fd, &buf[count], n-count);
    if (r == -1) {
      return -1;
    }
    count += r;
  }
  return count;
}

/* blocking write */
int upswrite(int fd, char *buf, int n) {
  int count = 0;
  int r;

  while (count < n) {
    r = write(fd, &buf[count], n-count);
    if (r == -1) {
      return -1;
    }
    count += r;
  }
  return count;
}

/* ---------------------------------------------------------------------- */
/* some private functions for talking to the UPS */

#define MAXMSGSIZE 25

/* calculate a Belkin checksum, i.e., add buf[0]...buf[n-1] */
static unsigned char belkin_checksum(unsigned char *buf, int n) {
  int i, res;

  res = 0;
  for (i=0; i<n; i++) {
    res += buf[i];
  }
  return res & 0xff;
}

/* receive Belkin message from UPS, check for well-formedness (leading
   byte, checksum). Return length of message, or -1 if not
   well-formed */
static int belkin_receive(int fd, unsigned char *buf, int bufsize) {
  int r;
  int n=0;
  int len;

  /* read 0x7e */
  if (n+1 > bufsize) {
    return -1;
  }
  r = upsread(fd, &buf[0], 1);
  if (r==-1 || buf[0]!=0x7e) {
    fprintf(stderr, "Garbage read from UPS\n");
    return -1;
  }
  n+=r;

  /* read instruction, size, and register */
  if (n+3 > bufsize) {
    return -1;
  }
  r = upsread(fd, &buf[1], 3);
  if (r!=3) {
    fprintf(stderr, "Short read from UPS\n");
    return -1;
  }
  n+=r;

  len = buf[2];

  /* read data and checksum */
  if (n+len > bufsize) {
    return -1;
  }
  r = upsread(fd, &buf[4], len);
  if (r!=len) {
    fprintf(stderr, "Short read from UPS\n");
    return -1;
  }
  n+=r;

  /* check checksum */
  if (belkin_checksum(buf, len+3) != buf[len+3]) {
    fprintf(stderr, "Bad checksum from UPS\n");
    return -1;
  }
  return n;
}

/* read the value of a string register from UPS. Return NULL on
   failure, else an allocated string. */
static char *belkin_read_str(int fd, int reg) {
  unsigned char buf[MAXMSGSIZE];
  int len, r;
  char *str;

  /* send the request */
  buf[0] = 0x7e;
  buf[1] = 0x03;
  buf[2] = 0x02;
  buf[3] = reg;
  buf[4] = 0;
  buf[5] = belkin_checksum(buf, 5);

  r = upswrite(fd, buf, 6);
  if (r<0) {
    fprintf(stderr, "Failed write to UPS\n");
    return NULL;
  }

  /* receive the answer */
  r = belkin_receive(fd, buf, MAXMSGSIZE);
  if (r<0) {
    return NULL;
  }
  if ((buf[1]!=0x05 && buf[1]!=0x01) || buf[3] != reg) {
    fprintf(stderr, "Invalid response from UPS\n");
    return NULL;
  }
  if (buf[1]==0x01) {
    return NULL;
  }

  /* convert the answer to a string */
  len = buf[2]-1;
  str = (char *)malloc(len+1);
  if (!str) {
    fprintf(stderr, "malloc: %s\n", strerror(errno));
    return NULL;
  }
  memcpy(str, &buf[4], len);
  str[len]=0;
  return str;
}

/* read the value of an integer register from UPS. Return -1 on
   failure. */
static int belkin_read_int(int fd, int reg) {
  unsigned char buf[MAXMSGSIZE];
  int len, r;

  /* send the request */
  buf[0] = 0x7e;
  buf[1] = 0x03;
  buf[2] = 0x02;
  buf[3] = reg;
  buf[4] = 0;
  buf[5] = belkin_checksum(buf, 5);

  r = upswrite(fd, buf, 6);
  if (r<0) {
    fprintf(stderr, "Failed write to UPS\n");
    return -1;
  }

  /* receive the answer */
  r = belkin_receive(fd, buf, MAXMSGSIZE);
  if (r<0) {
    return -1;
  }
  if ((buf[1]!=0x05 && buf[1]!=0x01) || buf[3] != reg) {
    fprintf(stderr, "Invalid response from UPS\n");
    return -1;
  }
  if (buf[1]==0x01) {
    return -1;
  }

  /* convert the answer to an integer */
  len = buf[2]-1;
  if (len==1) {
    return buf[4];
  } else if (len==2) {
    return buf[4] + 256*buf[5];
  } else {
    fprintf(stderr, "Invalid response from UPS\n");
    return -1;
  }
}

/* write the value of an integer register to UPS. Return -1 on
   failure, else 0 */
static int belkin_write_int(int fd, int reg, int val) {
  unsigned char buf[MAXMSGSIZE];
  int r;

  /* send the request */
  buf[0] = 0x7e;
  buf[1] = 0x04;
  buf[2] = 0x03;
  buf[3] = reg;
  buf[4] = val & 0xff;
  buf[5] = (val>>8) & 0xff;
  buf[6] = belkin_checksum(buf, 6);
  
  r = upswrite(fd, buf, 7);
  if (r<0) {
    fprintf(stderr, "Failed write to UPS\n");
    return -1;
  }

  /* receive the acknowledgement */
  r = belkin_receive(fd, buf, MAXMSGSIZE);
  if (r<0) {
    return -1;
  }
  if ((buf[1]!=0x02 && buf[1]!=0x01) || buf[3] != reg) {
    fprintf(stderr, "Invalid response from UPS\n");
    return -1;
  }
  if (buf[1]==0x01) {
    return -1;
  }
  return 0;
}

/* ---------------------------------------------------------------------- */
/* register descriptions */

struct register_s {
  int n;                /* register number */
  int str;              /* is it a string? */
  char *desc;           /* one-line description */
};

struct register_s regs[] = {
  { 0x01, 0, "voltage rating" },
  { 0x02, 0, "frequency rating" },
  { 0x03, 0, "power rating" },
  { 0x04, 0, "battery voltage rating" },
  { 0x05, 0, "unknown" },
  { 0x06, 0, "low transfer point" },
  { 0x07, 0, "low transfer point upper bound" },
  { 0x08, 0, "low transfer point lower bound" },
  { 0x09, 0, "high transfer point" },
  { 0x0a, 0, "high transfer point upper bound" },
  { 0x0b, 0, "high transfer point lower bound" },
  { 0x0c, 0, "voltage sensitivity" },
  { 0x0d, 0, "UPS model" },
  { 0x0e, 0, "UPS model" },
  { 0x0f, 0, "firmware/ups type" },
  { 0x10, 0, "test status" },
  { 0x11, 0, "alarm status" },
  { 0x12, 0, "unknown" },
  { 0x13, 0, "unknown" },
  { 0x14, 0, "unknown" },
  { 0x15, 0, "shutdown timer" },
  { 0x16, 0, "restart timer" },
  { 0x17, 0, "unknown" },
  { 0x18, 0, "input voltage" },
  { 0x19, 0, "input frequency" },
  { 0x1a, 0, "temperature" },
  { 0x1b, 0, "output voltage" },
  { 0x1c, 0, "output frequency" },
  { 0x1d, 0, "unknown" },
  { 0x1e, 0, "loading level" },
  { 0x1f, 0, "battery status" },
  { 0x20, 0, "battery voltage" },
  { 0x21, 0, "battery level" },
  { 0x22, 0, "UPS status" },
  { 0x23, 0, "battery status" },
  { 0x3f, 0, "time remaining" },
  { -1, 0, NULL}
};

/* ---------------------------------------------------------------------- */
/* the user-level console */

/* strip whitespace from both ends of a string, descructively */
char *strip(char *s) {
  char *p;

  if (!s)
    return NULL;

  while (isspace(*s)) {
    s++;
  }

  p = s+strlen(s);
  while (p>s && isspace(p[-1]))
    p--;
  *p = 0;
  return s;
}

/* read an allocated, stripped line from stream. Return NULL on eof or
   error. */
char *my_getline(FILE *f) {
  char *buf;
  char *res;
  int size;     /* current allocated size of buf */
  int l;
  size = 200;   /* should be large enough to hold an average line from
                 * the shared library without having to realloc() the
                 * buffer */

  buf = (char *)malloc(size);

  res = fgets(buf, size, f);
  if (!res) {          /* end of file */
    free(buf);
    return NULL;
  }
  while (!strchr(buf, '\n')) {
    size *= 2;
    buf = (char *)realloc(buf, size);
    l = strlen(buf);
    res = fgets(buf+l, size-l, f);
    if (!res)          /* end of file */
      break;
  }

  /* strip whitespace from both ends */
  res = strdup(strip(buf));
  free(buf);
  return res;
}

int setcmd(int fd, char *args) {
  int reg, val, r;
  char *endptr;

  reg = strtol(args, &endptr, 16);
  if (endptr==args) {
    return -1;
  }
  while (*endptr==' ' || *endptr=='\t') {
    endptr++;
  }
  args = endptr;
  val = strtol(args, &endptr, 0);
  if (endptr==args) {
    return -1;
  }
  r = belkin_write_int(fd, reg, val);
  if (r==-1) {
    printf("Error\n");
  } else {
    printf("OK\n");
  }
  return 0;
}

int getcmd(int fd, char *args) {
  int reg, val;
  char *str;
  char *endptr;

  reg = strtol(args, &endptr, 16);
  if (endptr==args) {
    return -1;
  }
  if (reg==0x0d || reg==0x0e) {
    str = belkin_read_str(fd, reg);
    printf("%02x: '%s'\n", reg, str);
    free(str);
  } else {
    val = belkin_read_int(fd, reg);
    if (val==-1) {
      printf("%02x: invalid\n", reg);
    }	else {
      printf("%02x: 0x%02x = %d\n", reg, val, val);
    }
  }
  return 0;
}

int listcmd(int fd) {
  int reg, i;
  char *str;
  int val;

  for (i=0; regs[i].n!=-1; i++) {
    reg = regs[i].n;
    if (reg==0x0d || reg==0x0e) {
      str = belkin_read_str(fd, reg);
      printf("%02x: '%s' %s\n", reg, str, regs[i].desc);
      free(str);
    } else {
      val = belkin_read_int(fd, reg);
      if (val==-1) {
	printf("%02x: invalid         %s\n", reg, regs[i].desc);
      }	else {
	printf("%02x: 0x%04x = %5d  %s\n", reg, val, val, regs[i].desc);
      }
    }
  }
  return 0;
}    

int helpcmd() {
  printf("Available commands:\n");
  printf("set <reg> <value>   - set register to value\n");
  printf("get <reg>           - get value from register\n");
  printf("state               - get all registers\n");
  printf("help                - show this help text\n");
  printf("quit                - exit this program\n");
  return 0;
}    

int main(int ac, char *av[]) {
  int fd;
  char *name = av[0];
  char *dev;
  char *prompt = "ups> ";
  char *line;

  if (ac != 2) {
    fprintf(stderr, "Usage: %s device\n", name);
    exit(1);
  }

  dev = av[1];

  fd = belkin_open_tty(dev);
  if (fd==-1) {
    fprintf(stderr, "%s: %s\n", name, strerror(errno));
    exit(1);
  }

  /* command loop */
  while (1) {
    printf("%s", prompt);
    line = my_getline(stdin);
    if (line==NULL) {
      printf("\n");
      break;
    }
    if (strncasecmp(line, "set ", 4)==0) {
      setcmd(fd, line+4);
    } else if (strncasecmp(line, "get ", 4)==0) {
      getcmd(fd, line+4);
    } else if (strcasecmp(line, "state")==0) {
      listcmd(fd);
    } else if (strcasecmp(line, "help")==0) {
      helpcmd(fd);
    } else if (strcasecmp(line, "quit")==0) {
      break;
    } else {
      printf("Command not recognized.\n");
      helpcmd(fd);
    }
    free(line);
  }
  return 0;
}
