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

#include <time.h>

char version[]="0.2, 25-Jan-2009";
/*
  Spiros Ioannou, sivann at gmail.com 2009


  v0.2: compiles with -pedantic, fixes some segfaults.

  based on liebert.c from R.Gregory (http://www.csc.liv.ac.uk/~greg/projects/liebertserial/)
  Compile with: gcc -O2 upsesp2.c -o upsesp2
  Run with: ./upsesp2 /dev/ttyS0 Replace ttyS0 with your serial port (ttyS1, ...etc).

  TODO: support UPS commands (shutdown, restart-after x seconds, etc). More sniffing needed.

 */

#define BAUDRATE B2400
#define _POSIX_SOURCE 1 /* POSIX compliant source */
#define FALSE 0
#define TRUE 1

#define NCMD 500 /*max number of commands*/
#define MAXAGE 5 /*max seconds to accept data as current*/
#define IsBitSet(val, bit) ((val) & (1 << (bit)))

/* port stuff */
int fd;
struct termios oldtio,newtio;



char str[256];

/***************************************************/
typedef struct
{
  char descr[120];
  unsigned char type;   /*0:measurement, 1:ascii, 2:status*/
  unsigned char length; /*for ascii, how many cmds*/
  unsigned char rfmt;   /*unit divider for measurement, bit position for status*/
  unsigned char cmd[6]; /*UPS command bytes*/
  unsigned char rbyteH; /*msb result*/
  unsigned char rbyteL; /*lsb result*/
  char rstr[256]; /*string result from multiple commands (lenght>1)*/
  time_t when; /*time of last result*/
  unsigned char init;   /*used to check if struct block has something valid*/
  unsigned char supported;   /*if this feature is supported*/
} cmd_s;

/*fill struct*/
/*fs(0,&c[0],"lala1", 1,2,3,4,5,6, 11,22,33,44,55,66);*/
void fs( cmd_s * c, 
    char * descr, 
    unsigned char type,
    unsigned char length,
    unsigned int rfmt,
    unsigned int c0, unsigned int c1, 
    unsigned int c2, unsigned int c3,
    unsigned int c4, unsigned int c5)
{
  //strcpy(c->descr,descr);
  snprintf(c->descr,120,"%s",descr);
  c->cmd[0]=c0;c->cmd[1]=c1;c->cmd[2]=c2;c->cmd[3]=c3;c->cmd[4]=c4;c->cmd[5]=c5;
  c->rstr[0]=0;
  c->type=type;
  c->when=0;
  c->rfmt=rfmt;
  c->length=length;
  c->init=1;
  c->supported=1; /*supported until proven otherwise (UPS doesn't repond) */
}

/* Initialize cmd_s structure with UPS commands and expected reply format */
void  initcmd(cmd_s *c)
{
  /*Format:*/
  /*STRINGID, type, length, rfmt,   cmd*/
  /*length:string length for ascii info*/
  /*type:0 measurement, 1: ascii, 2: status */
  /*rmft:unit divider (measurement), bit position (status) */
  /*cmd: UPS commands. Checksums (last byte) will be recalculated on write */

  /*measurements*/
  fs(c++,"BATTERY_TIME_REMAIN", 0,0,1, 	1,149,2,1,1,161);
  fs(c++,"BATTERY_VOLTAGE", 0,0,10,  	1,149,2,1,2,161);
  fs(c++,"BATTERY_VOLTAGE_NOMINAL", 0,0,10,   	1,161,2,1,13,154);
  fs(c++,"BATTERY_CURRENT", 0,0,1,  	1,149,2,1,3,161);
  fs(c++,"BATTERY_CAPACITY", 0,0,1,  	1,149,2,1,4,161);
  fs(c++,"BYPASS_VOLTAGE", 0,0,10,  	1,144,2,1,1,149);
  fs(c++,"MAX_LOAD", 0,0,1,   		1,161,2,1,8,154);
  fs(c++,"LOAD_WATTS", 0,0,1,  		1,149,2,1,5,161);
  fs(c++,"LOAD_VA", 0,0,1,  		1,149,2,1,6,161);
  fs(c++,"LOAD_PERCENT", 0,0,1,  	1,149,2,1,7,161);
  fs(c++,"INPUT_FREQUENCY", 0,0,10,  	1,149,2,1,8,161);
  fs(c++,"OUTPUT_FREQUENCY", 0,0,10,  	1,149,2,1,9,161);
  fs(c++,"BYPASS_FREQUENCY", 0,0,10,  	1,149,2,1,10,161);
  fs(c++,"INVERTER_TEMP", 0,0,10,  	1,149,2,1,11,161);
  fs(c++,"BATTERY_TEMP", 0,0,10,  	1,149,2,1,12,161);
  fs(c++,"PFC_TEMP", 0,0,10,  		1,149,2,1,13,161);
  fs(c++,"AMBIENT_TEMP", 0,0,10,  	1,149,2,1,14,161);
  fs(c++,"OUTPUT_VOLTAGE", 0,0,10,   	1,144,2,1,3,151);
  fs(c++,"OUTPUT_CURRENT", 0,0,10,   	1,144,2,1,4,154);


  /*strings*/
  fs(c++,"MODEL_NAME", 1,15,0, 		1,136,2,1,4,144);
  fs(c++,"FW_VER", 1,8,0, 		1,136,2,1,19,144);
  fs(c++,"SN", 1,10,0, 			1,136,2,1,27,144);
  fs(c++,"MANUF_DATE", 1,4,0, 		1,136,2,1,37,144);

  /*status*/
  fs(c++,"PFC_ON", 2,0,0, 	 1,148,2,1,1,000);
  fs(c++,"DC_DC_CONVERTER_STATE", 2,0,1, 	 1,148,2,1,1,000);
  fs(c++,"ON_INVERTER", 2,0,2, 	 1,148,2,1,1,000);
  fs(c++,"UTILITY_STATE", 2,0,3, 	 1,148,2,1,1,000);
  fs(c++,"INRUSH_LIMIT_ON", 2,0,4, 	 1,148,2,1,1,000);
  fs(c++,"OVERTEMP_WARNING", 2,0,5, 	 1,148,2,1,1,000);
  fs(c++,"BATTERY_TEST_STATE", 2,0,6, 	 1,148,2,1,1,000);
  fs(c++,"INPUT_OVERVOLTAGE", 2,0,7, 	 1,148,2,1,1,000);
  fs(c++,"ON_BATTERY", 2,0,8, 	 1,148,2,1,1,000);
  fs(c++,"ON_BYPASS", 2,0,0, 	 1,148,2,1,2,000);
  fs(c++,"BATTERY_CHARGED", 2,0,1, 	 1,148,2,1,2,000);
  fs(c++,"BATTERY_LIFE_ENHANCER_ON", 2,0,4, 	 1,148,2,1,2,000);
  fs(c++,"REPLACE_BATTERY", 2,0,5, 	 1,148,2,1,2,000);
  fs(c++,"BOOST_ON", 2,0,6, 	 1,148,2,1,2,000);
  fs(c++,"DIAG_LINK_SET", 2,0,7, 	 1,148,2,1,2,000);
  fs(c++,"BUCK_ON", 2,0,9, 	 1,148,2,1,2,000);
  fs(c++,"UPS_OVERLOAD", 2,0,0, 	 1,148,2,1,3,000);
  fs(c++,"BAD_INPUT_FREQ", 2,0,1, 	 1,148,2,1,3,000);
  fs(c++,"SHUTDOWN_PENDING", 2,0,2, 	 1,148,2,1,3,000);
  fs(c++,"CHARGER_FAIL", 2,0,3, 	 1,148,2,1,3,000);
  fs(c++,"LOW_BATTERY", 2,0,5, 	 1,148,2,1,3,000);
  fs(c++,"OUTPUT_UNDERVOLTAGE", 2,0,6, 	 1,148,2,1,3,000);
  fs(c++,"OUTPUT_OVERVOLTAGE", 2,0,7, 	 1,148,2,1,3,000);
  fs(c++,"BAD_BYPASS_PWR", 2,0,8, 	 1,148,2,1,3,000);
  fs(c++,"CHECK_AIR_FILTER", 2,0,10, 	 1,148,2,1,3,000);
  fs(c++,"AMBIENT_OVERTEMP", 2,0,2, 	 1,148,2,1,7,000);
  fs(c++,"MAIN_CONTROL_MODULE_FAILED", 2,0,0, 	 1,148,2,1,19,000);
  fs(c++,"REDUNDANT_CONTROL_MODULE_FAILED", 2,0,1, 	 1,148,2,1,19,000);
  fs(c++,"UI_MODULE_FAILED", 2,0,2, 	 1,148,2,1,19,000);
  fs(c++,"REDUNDANT_POWER_MODULE_ALARM", 2,0,3, 	 1,148,2,1,19,000);
  fs(c++,"REDUNDANT_BATTERY_MODULE_ALARM", 2,0,4, 	 1,148,2,1,19,000);
  fs(c++,"USER_MAX_LOAD_ALARM", 2,0,5, 	 1,148,2,1,19,000);
  fs(c++,"TRANSFORMER_OVERTEMP_ALARM", 2,0,6, 	 1,148,2,1,19,000);
  fs(c++,"INTERNAL_COMMS_LOST", 2,0,7, 	 1,148,2,1,19,000);
  fs(c++,"PWR_MOD_FAILED", 2,0,9, 	 1,148,2,1,19,000);
  fs(c++,"BAT_MOD_FAILED", 2,0,10, 	 1,148,2,1,19,000);
  fs(c++,"OPTION_CARD_FAIL_1", 2,0,0, 	 1,148,2,1,20,000);
  fs(c++,"OPTION_CARD_FAIL_2", 2,0,1, 	 1,148,2,1,20,000);
  fs(c++,"OPTION_CARD_FAIL_3", 2,0,2, 	 1,148,2,1,20,000);
  fs(c++,"OPTION_CARD_FAIL_4", 2,0,3, 	 1,148,2,1,20,000);
  fs(c++,"OPTION_CARD_FAIL_5", 2,0,4, 	 1,148,2,1,20,000);
  fs(c++,"OPTION_CARD_FAIL_6", 2,0,5, 	 1,148,2,1,20,000);
  fs(c++,"OPTION_CARD_FAIL_7", 2,0,6, 	 1,148,2,1,20,000);
  fs(c++,"OPTION_CARD_FAIL_8", 2,0,7, 	 1,148,2,1,20,000);
  fs(c++,"SUMMARY_ALARM", 2,0,0, 	 1,148,2,1,22,000);
  fs(c++,"RECT_UV_STARTUP_FAIL", 2,0,0, 	 1,148,2,1,24,000);
  fs(c++,"RECT_FAULT", 2,0,1, 	 1,148,2,1,24,000);
  fs(c++,"RECT_OVER_CURRENT", 2,0,2, 	 1,148,2,1,24,000);
  fs(c++,"RECT_OVER_TEMP", 2,0,3, 	 1,148,2,1,24,000);
  fs(c++,"RECT_INDCTR_OVER_TEMP", 2,0,4, 	 1,148,2,1,24,000);
  fs(c++,"RECT_COMM_FAIL", 2,0,5, 	 1,148,2,1,24,000);
  fs(c++,"INV_SHUTDOWN_LOW_DC", 2,0,6, 	 1,148,2,1,24,000);
  fs(c++,"INV_FAULT", 2,0,7, 	 1,148,2,1,24,000);
  fs(c++,"INV_OVER_CURRENT", 2,0,8, 	 1,148,2,1,24,000);
  fs(c++,"INV_OVER_TEMP", 2,0,9, 	 1,148,2,1,24,000);
  fs(c++,"INV_INDCTR_OVER_TEMP", 2,0,10, 	 1,148,2,1,24,000);
  fs(c++,"INV_COMM_FAIL", 2,0,11, 	 1,148,2,1,24,000);
  fs(c++,"INV_DC_OFFSET_OVR", 2,0,12, 	 1,148,2,1,24,000);
  fs(c++,"INV_CONTACTOR_FAIL", 2,0,13, 	 1,148,2,1,24,000);
  fs(c++,"BAT_FAULT", 2,0,1, 	 1,148,2,1,25,000);
  fs(c++,"BAT_CONTACTOR_FAIL", 2,0,2, 	 1,148,2,1,25,000);
  fs(c++,"CONVERTER_OVER_TEMP", 2,0,4, 	 1,148,2,1,25,000);
  fs(c++,"CONVERTER_OVER_AMPS", 2,0,5, 	 1,148,2,1,25,000);
  fs(c++,"CONVERTER_FAIL", 2,0,6, 	 1,148,2,1,25,000);
  fs(c++,"BALANCER_OVER_TEMP", 2,0,7, 	 1,148,2,1,25,000);
  fs(c++,"BALANCER_FAULT", 2,0,8, 	 1,148,2,1,25,000);
  fs(c++,"BALANCER_OVER_CURRENT", 2,0,9, 	 1,148,2,1,25,000);
  fs(c++,"BY_CB_OPEN", 2,0,10, 	 1,148,2,1,25,000);
  fs(c++,"LOAD_IMPACT_XFER", 2,0,11, 	 1,148,2,1,25,000);
  fs(c++,"OPERATION_FAULT", 2,0,12, 	 1,148,2,1,25,000);
  fs(c++,"OUT_FUSE_BLOWN", 2,0,13, 	 1,148,2,1,25,000);
  fs(c++,"ON_JOINT_MODE", 2,0,14, 	 1,148,2,1,25,000);
  fs(c++,"MAIN_NEUTRAL_LOST", 2,0,4, 	 1,148,2,1,26,000);
  fs(c++,"PARALLEL_LOW_BAT_WARN", 2,0,4, 	 1,148,2,1,27,000);
  fs(c++,"PARALLEL_LOAD_SHARE_FAULT", 2,0,5, 	 1,148,2,1,27,000);
  fs(c++,"PARALLEL_FAULT", 2,0,6, 	 1,148,2,1,27,000);
  fs(c++,"PARALLEL_CONNECT_FAULT", 2,0,7, 	 1,148,2,1,27,000);
  fs(c++,"PARALLEL_COMM_FAIL", 2,0,8, 	 1,148,2,1,27,000);
  fs(c++,"PARALLEL_SYS_OVER_LOAD", 2,0,9, 	 1,148,2,1,27,000);
  fs(c++,"PARALLEL_SYS_XFER", 2,0,10, 	 1,148,2,1,27,000);

  c->init=0; /*on first non-init element */

}

int GetCmdCount(cmd_s * c)
{
  int i;
  for (i=0;(i<NCMD)&&c++->init;i++) ;
  return i;

}

/*return array position of command "descr"*/
int GetCmdIdbyDesc(cmd_s * c,char *descr)
{
  int i;
  for (i=0;i<NCMD;i++) {
    if (!strcmp(descr,c->descr))  return i;
    c++;
  }
  return -1;
}

/*return array position of the same status UPS command with the most recent data
 *this is since each status command includes several statuses, and we don't want
 *to re-query the UPS for each of those statuses if the status is recent enough*/
int GetRecentStatusId(cmd_s *c,unsigned char bit4)
{
  int i,r=-1;
  time_t maxt=0;
  
  for (i=0;(i<NCMD)&&c++->init;i++) {
    if ((c->cmd[1]!=148) || (c->cmd[4]!=bit4))  
      continue;
    else if (c->when>maxt)  {
      maxt=c->when;
      r=i;
    }
  }

  return r; 
  
}
/***************************************************/


/* print byte in binary*/
void bin_prnt_byte(int x)
{
   int n;
   for(n=0; n<8; n++)
   {
      if((x & 0x80) !=0) 
         printf("1");
      else 
         printf("0");
      if (n==3) 
         printf(" "); /* insert a space between nibbles */
      x = x<<1;
   }
}

void sgnl_ignore(int status)
 {
   fprintf(stderr,"Signaled\n");
 }

/*calculate checksum*/
unsigned char cksum(unsigned char *buf, int len)
 {
   unsigned char sum=0;
   while(len-->0)
      sum+=*buf++;
   return(sum);
 }


int timedwrite(int fd, unsigned char *buf, int len, int msec)
 {
   fd_set wfds;
   struct timeval tv;
   int retval;

   int writ=0;
   while(writ<len) {
      FD_ZERO(&wfds);
      FD_SET(fd, &wfds);
      tv.tv_sec = msec/1000;
      tv.tv_usec = (msec%1000)*1000;

      retval = select(fd+1, NULL, &wfds, NULL, &tv);
      if( retval>=0 && FD_ISSET(fd, &wfds) )
       {
         int res = write(fd,buf+writ,len-writ);
         if( res>0 )
	    writ+=res;
	 else
	    return(-1);
       }
      if( retval<=0 )
         return(-1);
   }
   return(writ);
 }


int timedread(int fd, unsigned char *buf, int len, int msec)
 {
   fd_set rfds;
   struct timeval tv;
   int retval;

   int red=0;
   while(red<len) {
      FD_ZERO(&rfds);
      FD_SET(fd, &rfds);

      tv.tv_sec = msec/1000;
      tv.tv_usec = (msec%1000)*1000;
      retval = select(fd+1, &rfds, NULL, NULL, &tv);

      if( retval>=0 && FD_ISSET(fd, &rfds) )
       {
         int res = read(fd,buf+red,len-red);
         if( res>0 )
	    red+=res;
	 else
	    return(-1);
       }
      if( retval<=0 )
         return(-1);
   }
   return(red);
 }


/* Send a command and read a measurement */
unsigned int SendCmd_M(int fd, unsigned char *cmd, unsigned char *rbyteH, unsigned char *rbyteL)
{
   unsigned char buf[8];
   int res;

   cmd[5]=cksum(cmd,5);

   res=timedwrite(fd,cmd,6,500);
   if(res!=6){
     fprintf(stderr,"port write error, cmd:");
     fprintf(stderr,"%03d,%03d,%03d,%03d,%03d,%03d\n",cmd[0],cmd[1],cmd[2],cmd[3],cmd[4],cmd[5]);
     return -1;
   }

   res=timedread(fd,buf,8,200);

   if(res!=8){
     fprintf(stderr,"read count error (%d<>8),cmd:",res);
     fprintf(stderr,"%03d,%03d,%03d,%03d,%03d,%03d\n",cmd[0],cmd[1],cmd[2],cmd[3],cmd[4],cmd[5]);
     return -1;
   }

   if(buf[7]!=cksum(buf,7)){
     fprintf(stderr,"checksum error for cmd:");
     fprintf(stderr,"%03d,%03d,%03d,%03d,%03d,%03d\n",cmd[0],cmd[1],cmd[2],cmd[3],cmd[4],cmd[5]);
     return -1;
   }

   *rbyteH=buf[5];
   *rbyteL=buf[6];
   return ((unsigned short)buf[5])*256+buf[6];
}

/* Send a series of consecutive commands and read text response (2 chars each)*/
int SendCmd_T(int fd, unsigned char *cmd, char *chrs, int len)
 {
   unsigned char buf[8], lcmd[7];
   int wres,rres;

   memcpy(lcmd,cmd,6);
   for(;len>0;len--)
    {
      lcmd[5]=cksum(lcmd,5);
      wres=timedwrite(fd,lcmd,6,400);
      if (wres!=6)
	return -1;
      rres=timedread(fd,buf,8,400);
      if (rres!=8)
	return -1;

      *chrs++=buf[6];
      *chrs++=buf[5];
    
      if (buf[7]!=cksum(buf,7)) 
	return -1;
      lcmd[4]++;
    }
   *chrs=0;
   
   return 1;
 }

void portsetup(char * port)
{

  fd = open(port, O_RDWR | O_NOCTTY );
  if (fd < 0) {
    fprintf(stderr,"error %d\n",errno);
    sprintf(str,"%s:%s",port,strerror(errno));
    fprintf(stderr,"%s\n",str);
    exit(errno); 
  }

  tcgetattr(fd,&oldtio); /* save current port settings */
  bzero(&newtio, sizeof(newtio));
  newtio.c_cflag = BAUDRATE /*| CRTSCTS*/ | CS8 | CLOCAL | CREAD;
   newtio.c_iflag = IGNPAR | IGNBRK;
  newtio.c_oflag = 0;
   /* set input mode (non-canonical, no echo,...) */
  newtio.c_lflag = 0;
  newtio.c_cc[VTIME]    = 0;   /* inter-character timer unused */
  newtio.c_cc[VMIN]     = 1;   /* blocking read until 5 chars received */
  tcflush(fd, TCIFLUSH);
  tcsetattr(fd,TCSANOW,&newtio);


}

/*Read data from UPS corresponding to c->cmd and fill-in result in struct cmd_s*/
int ReadCmd(cmd_s * c) {
  int r,res;
  int j;
  unsigned char rbyteH,rbyteL;
  char buf[255];

  if (c->supported==0) {      /*read failed on 1st try, thus marked as not supported*/
     /*printf("UNSUPPORTED item %s:skipping\n",c->descr);*/
     return 0;
  }
  if (c->type==0) {      /*measurement*/
    /*todo: check if time is fresh, and if not then SendCmd. Same for strings*/
    res=SendCmd_M(fd, c->cmd, &rbyteH, &rbyteL);
    if(res>=0) {
      c->when=time(0); 
      c->rbyteL=rbyteL;
      c->rbyteH=rbyteH; 
      printf("%s: %.1f\n",c->descr,(256.0*c->rbyteH+c->rbyteL)/c->rfmt); 
    }
     else  {
       printf("Error getting %s:marking as not supported\n",c->descr);
       c->supported=0;
     }
  }
  else if (c->type==1) { /*ascii*/
    res=SendCmd_T(fd, c->cmd, buf, c->length);  
    if (res) {
      strcpy(c->rstr,buf);
      c->when=time(0);
      printf("%s: %s\n",c->descr,c->rstr); 
    }
    else {
      printf("Error getting %s:marking as not supported\n",c->descr);
      c->supported=0;
    }
  }
  else if (c->type==2) { /*status*/
    r=GetRecentStatusId(&c[0],c->cmd[4]); 
    if ((r<0) || (c->when-time(0)>MAXAGE)) { /*not previous data or old data*/
       res=SendCmd_M(fd, c->cmd, &rbyteH, &rbyteL);
       if (res>=0) {
	 c->when=time(0);
	 c->rbyteL=rbyteL;
	 c->rbyteH=rbyteH;

	 if (c->rfmt<8)  (j=IsBitSet(c->rbyteL,c->rfmt));
	 else (j=IsBitSet(c->rbyteH,c->rfmt) );

	 if (j) printf("%s: YES\n",c->descr);
	 else printf("%s: NO\n",c->descr);
       }
       else {
	 printf("Error getting %s:marking as not supported\n",c->descr);
	 c->supported=0;
       }
    }
    else { /*found recent data from other poll*/
       if (c->rfmt<8)  (j=IsBitSet(c[r].rbyteL,c->rfmt));
       else (j=IsBitSet(c[r].rbyteH,c[r].rfmt) );
       if (j) printf("%s: YES\n",c->descr);
       else printf("%s: NO\n",c->descr);
    }
  }


  return c->supported;


}

int main(int argc, char **argv)
 {
  int i;
  unsigned int ncmd;
  cmd_s c[NCMD];
  int reqcmd=-1; /*if requesting specific command*/


  initcmd(&c[0]); /* fill in struct table */
  ncmd=GetCmdCount(&c[0]); /*count number of commands*/


  if (argc==3) {
    reqcmd=GetCmdIdbyDesc(&c[0],argv[2]); /*get position of requested command in c[]*/

  }
  else if ((argc!=2) && (argc!=3)) {
    fprintf(stderr,"upsesp2 version %s:\n",version);
    fprintf(stderr,"Usage:\n");
    fprintf(stderr,"\t\tupsesp2 <serial device>\n");
    fprintf(stderr,"or\t\tupsesp2 <serial device> <UPS parameter>\n");
    fprintf(stderr,"example:\tupsesp2 /dev/ttyS1 LOAD_WATTS\n");
    fprintf(stderr,"\nThe 1st syntax will report a list of supported parameters\n\n");
    exit(1);
  }


  portsetup(argv[1]); /*setup serial port*/
  signal( SIGALRM, &sgnl_ignore ); /*for select*/

  if (reqcmd>0) { /* if requesting a single ups command (measurement/status)*/
    ReadCmd(&c[reqcmd]);
  }
  else
    while (1) {
      for (i=0;i<ncmd;i++) { /*loop through all defined ups commands */
	ReadCmd(&c[i]);
        if (!c[i].supported) 
	  printf("%s not supported on this UPS\n",c[i].descr);
      }
      printf("Sleeping....\n\n");
      sleep(2);
    }

  tcsetattr(fd,TCSANOW,&oldtio);

  return 0;

} /* main() */


