/*

 Aqualink daemon 
 Mark Lottor, June 1998

 This daemon monitors a serial port connected to an Aqualink PC interface.
 It writes out some html formatted files whenever something
 interesting changes.  These files can then be viewed directly from
 a web server.

 known to work with Aqualink RS8 system with one in house
 control panel and one spa link control panel and one aqualink PC
 interface connected to serial port defined below.

 To configure for your own system:
   read the #defines below and set for your system,
   in particular:
     set PCLINK_DEV to the serial port of your aqualink PC interface
     define what you set the AUX buttons to be.

 Protocol description of serial link
 Note: this was reverse engineered and may not be completely accurate.

 The Power Center (the main Aqualink control computer at your pool
 equipment breaker panel) is the link Master.  All other units are
 slave terminals.  The master continuously sends probe packets for
 all slaves.  The slave responds with unit status and any keypress
 codes waiting to be sent to master.

 Serial port runs at 9600 baud, no parity, 1 stop bit, 7-bit chars only.
 DTR must be HIGH.  RTS must be LOW to receive and HIGH to transmit!

 All packets are formatted as follows:

 DLE STX <data> <checksum> DLE ETX

 Sometimes NULs are sent before and after the packet.
 note: if DLE occurs in packet it is escaped as DLE NUL!!!

 <data>     := <dest><command><args>

 <checksum> := the 7-bit sum of bytes DLE, STX and <data>

 <dest>     := a single byte representing destination for packet

 <command>  := a single byte with command for destination device

 <args>     := optional string of bytes for command data


    

*/

/* packet offsets */
#define PKT_DEST        2
#define PKT_CMD         3
#define PKT_DATA        4

/* DEVICE CODES */
/* devices probed by master are 08-0b, 10-13, 18-1b, 20-23, 
                                28-2b, 30-33, 38-3b, 40-43  */
#define DEV_MASTER      0

#define DEV_HOME        DEV_HOME_2
#define DEV_HOME_0      0x08
#define DEV_HOME_1      0x09
#define DEV_HOME_2      0x0a
#define DEV_PC          0x0b

#define DEV_SPA         DEV_SPA_0
#define DEV_SPA_0       0x20
#define DEV_SPA_1       0x21
#define DEV_SPA_2       0x22



/* KEYPRESS CODES */
#define KEY_PUMP        0x02
#define KEY_SPA         0x01
#define KEY_AUX1        0x05
#define KEY_AUX2        0x0a
#define KEY_AUX3        0x0f
#define KEY_AUX4        0x06
#define KEY_AUX5        0x0b
#define KEY_AUX6        0x10
#define KEY_AUX7        0x15

#define KEY_HTR_POOL    0x12
#define KEY_HTR_SPA     0x17
#define KEY_HTR_SOLAR   0x1c

#define KEY_MENU        0x09
#define KEY_CANCEL      0x0e
#define KEY_LEFT        0x13
#define KEY_RIGHT       0x18

#define KEY_HOLD        0x19
#define KEY_OVERRIDE    0x1e


/* COMMANDS */
#define CMD_PROBE       0x00
#define CMD_ACK         0x01
#define CMD_STATUS      0x02
#define CMD_MSG         0x03
#define CMD_MSG_LONG    0x04

/*
CMD_COMMAND data is:
  <status> <keypress>
  status is 0 if idle, 1 if display is busy
  keypress is 0, or a keypress code
CMD_STATUS is sent in response to all probes from DEV_MASTER
DEV_MASTER continuously sends CMD_COMMAND probes for all devices
until it discovers a particular device.

CMD_STATUS data is 5 bytes long bitmask
defined as STAT_* below

CMD_MSG data is <line> followed by <msg>
  <msg> is ASCII message up to 16 chars (or null terminated).
  <line> is NUL if single line message, else
    1 meaning it is first line of multi-line message,
      if so, next two lines come as CMD_MSG_LONG with next byte being
    2 or 3 depending on second or third line of message.
*/

#define STAT_AUX1        1,0x01
#define STAT_AUX2        0,0x40
#define STAT_AUX3        0,0x10
#define STAT_AUX4        2,0x01
#define STAT_AUX5        1,0x40
#define STAT_AUX6        2,0x40
#define STAT_AUX7        0,0x01
  
#define STAT_PUMP        1,0x10
#define STAT_PUMP_BLINK  1,0x20
#define STAT_SPA         1,0x04
#define STAT_HTR_POOL_EN 3,0x40
#define STAT_HTR_POOL_ON 3,0x10
#define STAT_HTR_SPA_EN  4,0x04
#define STAT_HTR_SPA_ON  4,0x01
#define STAT_HTR_SOL_EN  4,0x40
#define STAT_HTR_SOL_ON  4,0x10

#define STAT_JETS       STAT_AUX1
#define STAT_AIR        STAT_AUX2
#define STAT_SPILLOVER  STAT_AUX3
#define STAT_POOL_LIGHT STAT_AUX4
#define STAT_SPA_LIGHT  STAT_AUX5
#define STAT_YARD_LIGHT STAT_AUX6
#define STAT_FILL_LINE  STAT_AUX7

#define HTML_PAGE   "/usr/home/ftp/mkl/pool.html"

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

#include <termios.h>
#include <sys/ioctl.h>
#define TCGETS TIOCGETA
#define TCSETS TIOCSETA

#define FALSE 0
#define TRUE  1

#define OFF   FALSE
#define ON    TRUE

#define NUL  0x00
#define DLE  0x10
#define STX  0x02
#define ETX  0x03


int debug = FALSE;

#define PCLINK_DEV    "/dev/tty05"
int ftty;

#define MINPKTLEN  5
#define MAXPKTLEN 64
unsigned char pktbuf[MAXPKTLEN];
int pktlen, datalen;


#define PSTLEN 5
unsigned char pool_status[PSTLEN];
unsigned char pool_status_old[PSTLEN];

#define MSGLEN 16
char msg[MSGLEN+1];
#define MSGLONGLEN 128
char msglong[MSGLONGLEN];
#define MSGHISTORY 50
char *msgs[MSGHISTORY];

#define TADLEN 13
int pool_temp, air_temp, spa_temp;
char tad_pool[TADLEN+1], tad_air[TADLEN+1], tad_spa[TADLEN+1];
time_t spa_time = 0;

/* how many seconds spa stays warm after shutdown */
#define SPA_COOLDOWN   (20*60)

int gotmsglong;


main()
{
  int i, pos, refresh;
  char *p;

  if (!debug)
  {
    /* disconnect from controlling tty */
    if (fork())
      exit(0);
    for (i = 10; i >= 0; i--)
      (void) close(i);
    (void) open("/dev/null", O_RDONLY);
    (void) dup2(0, 1);
    (void) dup2(0, 2);
    i = open("/dev/tty", O_RDWR);
    if (i > 0)
    {
      (void) ioctl(i, TIOCNOTTY, (char *)NULL);
      (void) close(i);
    }
  }
  
  init_port(PCLINK_DEV);
  for (i = 0; i < PSTLEN; i++)
  {
    pool_status[i] = 0;
    pool_status_old[i] = 0;
  }
  for (i = 0; i < MSGHISTORY; i++)
    msgs[i] = NULL;
  pool_temp = -1;
  air_temp = -1;
  spa_temp = -1;
  *tad_air = '\0';
  *tad_pool = '\0';
  *tad_spa = '\0';
  gotmsglong = FALSE;

  for (;;)
  {
    refresh = FALSE;

    get_packet();

    /* ignore packets not for us */
    if (pktbuf[PKT_DEST] != DEV_HOME)
      continue;

    datalen = pktlen - 7;
    if (datalen < 0) continue;

    if (gotmsglong && (pktbuf[PKT_CMD] != CMD_MSG_LONG))
    {
      pos = strlen(msglong);
      for (i = 0; i < pos; i++)
	msglong[i] = msglong[i] & 0x7f;
      if (addmsg(msglong))
	refresh = TRUE;
      gotmsglong = FALSE;
      *msglong = '\0';
    }
    else if (pktbuf[PKT_CMD] == CMD_STATUS)
    {
      if (datalen != PSTLEN) continue;
      bcopy(pktbuf+PKT_DATA,pool_status,PSTLEN);
      for (i = 0; i < PSTLEN; i++)
      {
	if (pool_status[i] != pool_status_old[i])
	  refresh = TRUE;
      }
      if (refresh)
	bcopy(pool_status,pool_status_old,PSTLEN);
    }
    else if ((pktbuf[PKT_CMD] == CMD_MSG) && (pktbuf[PKT_DATA] == 0))
    {
      strncpy(msg,pktbuf+PKT_DATA+1,MSGLEN);
      msg[MSGLEN] = '\0';
      for (i = 0; i < MSGLEN; i++)
	msg[i] = msg[i] & 0x7f;
      p = msg;
      while ((*p != '\0') && (*p == ' ')) p++;

      if ((*(p+1) == ':') && (*(p+4) == ' '))	/* time, ignore it */
	continue;
      if ((*(p+2) == ':') && (*(p+5) == ' '))	/* time, ignore it */
	continue;
      if ((*(p+2) == '/') && (*(p+5) == '/'))	/* date, ignore it */
	continue;

      if (!strncmp(p,"POOL TEMP",9))
      {
	maketad(tad_pool);
	i = atoi(p+9);
	if (i != pool_temp)
	{
	  pool_temp = i;
	  refresh = TRUE;
	}
      }
      else if (!strncmp(p,"SPA TEMP",8))
      {
	maketad(tad_spa);
	i = atoi(p+8);
	if (i != spa_temp)
	{
	  spa_temp = i;
	  time(&spa_time);
	  refresh = TRUE;
	}
      }
      else if (!strncmp(p,"AIR TEMP",8))
      {
	maketad(tad_air);
	i = atoi(p+8);
	if (i != air_temp)
	{
	  air_temp = i;
	  refresh = TRUE;
	}
      }
      else if (addmsg(p))
	refresh = TRUE;
    }
    else if ((pktbuf[PKT_CMD] == CMD_MSG) && (pktbuf[PKT_DATA] == 1))
    {
      /* start of multi-line message */
      strncpy(msglong,pktbuf+PKT_DATA+1,MSGLEN);
      msglong[MSGLEN] = '\0';
      gotmsglong = TRUE;
    }
    else if (pktbuf[PKT_CMD] == CMD_MSG_LONG)
    {
      pos = (pktbuf[PKT_DATA] - 1) * 16;
      strncpy(msglong+pos,pktbuf+PKT_DATA+1,MSGLEN);
      pos = pos + MSGLEN;
      msglong[pos] = '\0';
    }
    else if (debug)
      display_packet();


    if (refresh)
      update_html();

    /*    if (pktbuf[PKT_CMD] == CMD_PROBE) */
    /* send_ack(); */
  }

  close_port();
}

send_ack()
{
  int len, cksum;
  char z[1];

  *z = 0;

  len = 0;
  pktbuf[len++] = DLE;
  pktbuf[len++] = STX;
  pktbuf[len++] = DEV_MASTER;
  pktbuf[len++] = CMD_ACK;
  pktbuf[len++] = 0;
  pktbuf[len++] = 0;
  pktbuf[len++] = 0;
  pktbuf[len++] = DLE;
  pktbuf[len++] = ETX;
  cksum = genchecksum(pktbuf,len);
  pktbuf[len-3] = cksum;
  rts(ON);
  write(ftty,z,1);
  write(ftty,pktbuf,len);
  write(ftty,z,1);
  usleep(26000);
  rts(OFF);
  if (debug)
  {
    printf("sending ");
    pktlen = len;
    display_packet();
  }
}

display_packet()
{
  int i;

  printf("got len %02d: ",pktlen);
  for (i = 0; i < pktlen; i++) printf("%02x ",pktbuf[i]);
  printf("\n");
}


/* generate and return checksum of packet in buf */
int genchecksum(buf,len)
  char *buf;
  int len;
{
  int i, sum, n;

  n = len - 3;
  sum = 0;
  for (i = 0; i < n; i++)
    sum += (int) buf[i];
  return(sum & 0x0ff);
}

/* reads the next incoming packet, validates checksum,
   returns when a good packet is available in pktbuf */
get_packet()
{
  int i, len, cksum;

  for (;;)
  {
    len = 0;
    pktbuf[len] = '\0';
    for (;;)
    {
      read(ftty,pktbuf,1);
      if (pktbuf[len] == DLE) break;
      /* if (debug) printf("[%02x]",pktbuf[len]); */
    }
    len++;

    pktbuf[len] = '\0';
    read(ftty,pktbuf+len,1);
    if (pktbuf[len] != STX)
    {
      /* if (debug) printf("<10><%02x>",pktbuf[len]); */
      continue;
    }
    len++;

    for (;;)
    {
      pktbuf[len] = '\0';
      read(ftty,pktbuf+len,1);
      if (pktbuf[len] == DLE)
      {
	len++;
	read(ftty,pktbuf+len,1);
	if (pktbuf[len] == ETX) {len++; break; }
	if (pktbuf[len] == NUL) continue;
      }
      len++;
      if (len >= MAXPKTLEN) break;
    }
    if (len >= MAXPKTLEN) continue;
    
    /* got packet, now check sum */
    if (len <= MINPKTLEN)
    {
      if (debug) printf("small packet len %d\n",len);
      continue;
    }
    cksum = genchecksum(pktbuf,len);
    if (cksum == (int) pktbuf[len-3]) break;
    if (debug) printf("bad checksum %d != %d\n",cksum,pktbuf[len-3]);
  }
  pktlen = len;
}


/*
 Initialize or open comm port
 Arg is tty or port designation string.
*/
init_port(tty)
  char *tty;
{
  struct termios ttyargs;

  if ((ftty = open(tty,O_RDWR)) == -1)
  {
    fprintf(stderr,"?Can't open tty '%s'.\n",tty);
    exit(1);
  }

  if ((ioctl(ftty, TCGETS, (char *) &ttyargs)) == -1)
  {
    fprintf(stderr,"?Can't do ioctl TCGETS on tty '%s'.\n",tty);
    exit(1);
  }
  ttyargs.c_iflag = 0;
  ttyargs.c_oflag = 0;
  ttyargs.c_cflag = (CS8 | CREAD | CLOCAL);
  ttyargs.c_ispeed = B9600;
  ttyargs.c_ospeed = B9600;
  ttyargs.c_lflag = 0;
  if ((ioctl(ftty, TCSETS, (char *) &ttyargs)) == -1)
  {
    fprintf(stderr,"?Can't do ioctl TSETS on tty '%s'.\n",tty);
    exit(1);
  }
  pickup();
}

/* close tty port */
close_port()
{
  hangup();
  close(ftty);
}

pickup()
{
  if (debug) fprintf(stderr,"pickup\n");
  ioctl(ftty,TIOCSDTR,NULL);
  rts(OFF);
}

rts(action)
  int action;
{
  int state;

  ioctl(ftty,TIOCMGET,(char *) &state);
  if (!action)
    state = (state & ~TIOCM_RTS);
  else
    state = (state | TIOCM_RTS);
  ioctl(ftty,TIOCMSET,(char *)&state);
}

hangup()
{
  if (debug) fprintf(stderr,"hangup\n");
  ioctl(ftty,TIOCCDTR,NULL);
  sleep(2);
}

update_html()
{
  int i;
  FILE *fp;
  time_t now;

#if 0
  printf("pool status: ");
  for (i = 0; i < PSTLEN; i++)
    printf("%02x ",pool_status[i]);
  printf("\n");

  printf("temperatures:\n");
  if (air_temp != -1)
    printf(" %s  air   %3d\n",tad_air,air_temp);
  if (pool_temp != -1)
    printf(" %s  pool  %3d\n",tad_pool,pool_temp);
  if (spa_temp != -1)
    printf(" %s  spa   %3d\n",tad_spa,spa_temp);

  printf("msgs:\n",msg);
  for (i = 0; i < MSGHISTORY; i++)
  {
    if (msgs[i] == NULL) continue;
    printf(" %s\n",msgs[i]);
  }
  printf("\n");
#endif

  /*******************************************************/
  if ((fp = fopen(HTML_PAGE,"w")) == NULL) return;

  fprintf(fp,"<html><head>\n");
  fprintf(fp,"<title>Pool Status</title>\n");
  fprintf(fp,"<meta http-equiv=refresh content=\"60; URL=http://www.nw.com/mkl/pool.html\">\n");

  time(&now);
  if ((now - spa_time) < SPA_COOLDOWN)
  {
    if (spa_temp > 100)
      fprintf(fp,"<head><body bgcolor=red>\n");
    else if (spa_temp > 90)
      fprintf(fp,"<head><body bgcolor=orange>\n");
    else
      fprintf(fp,"<head><body bgcolor=aqua>\n");
  }
  else
    fprintf(fp,"<head><body bgcolor=aqua>\n");

  fprintf(fp,"<h2>Pool Status</h2>\n");

  fprintf(fp,"<pre>\n");
  fprintf(fp,"<b>device status: </b>");
  for (i = 0; i < PSTLEN; i++)
    fprintf(fp,"%02x ",pool_status[i]);
  fprintf(fp,"\n");

  if (pstat(STAT_PUMP))
    fprintf(fp," pump on\n");
  if (pstat(STAT_PUMP_BLINK))
    fprintf(fp," pump waiting\n");
  if (pstat(STAT_HTR_POOL_EN))
    fprintf(fp," pool heater enabled\n");
  if (pstat(STAT_HTR_POOL_ON))
    fprintf(fp," pool heater on\n");
  if (pstat(STAT_POOL_LIGHT))
    fprintf(fp," pool light on\n");

  if (pstat(STAT_SPA))
    fprintf(fp," spa on\n");
  if (pstat(STAT_JETS))
    fprintf(fp," spa jets on\n");
  if (pstat(STAT_AIR))
    fprintf(fp," spa air blower on\n");
  if (pstat(STAT_HTR_SPA_EN))
    fprintf(fp," spa heater enabled\n");
  if (pstat(STAT_HTR_SPA_ON))
    fprintf(fp," spa heater on\n");
  if (pstat(STAT_SPA_LIGHT))
    fprintf(fp," spa light on\n");

  if (pstat(STAT_HTR_SOL_ON))
    fprintf(fp," solar heater on\n");

  if (pstat(STAT_SPILLOVER))
    fprintf(fp," spillover on\n");

  if (pstat(STAT_YARD_LIGHT))
    fprintf(fp," yard light on\n");
  if (pstat(STAT_FILL_LINE))
    fprintf(fp," water fill line on\n");

  fprintf(fp,"\n");

  fprintf(fp,"<b>temperature:</b>\n");
  if (air_temp != -1)
    fprintf(fp," %s air   %3d\n",tad_air,air_temp);
  if (pool_temp != -1)
    fprintf(fp," %s pool  %3d\n",tad_pool,pool_temp);
  if (spa_temp != -1)
    fprintf(fp," %s spa   %3d\n",tad_spa,spa_temp);
  fprintf(fp,"\n");

  fprintf(fp,"<b>console messages:</b>\n",msg);
  fprintf(fp,"<form>");
  fprintf(fp,"<select name=history size=10>");
  for (i = 0; i < MSGHISTORY; i++)
  {
    if (msgs[i] == NULL) continue;
    fprintf(fp,"<option> %s\n",msgs[i]);
  }
  fprintf(fp,"</select>\n");
  fprintf(fp,"</form>\n");

  fprintf(fp,"</pre>\n");
  fprintf(fp,"</body></html>\n");

  fclose(fp);

}


/*
 add msg to msg history list if it is new 
 prepend timestamp to beginning of message
  format:  "Jul-04 17:22 "
*/
int addmsg(newmsg)
  char *newmsg;
{
  int i, j, ret;
  char *p;
  char timestamp[TADLEN+1];

  ret = TRUE;

  /* if message already is on list, remove it */
  for (i = 0; i < MSGHISTORY; i++)
  {
    if (msgs[i] == NULL) continue;
    if (!strcmp(msgs[i]+TADLEN,newmsg)) 
    {
       free(msgs[i]);

       /* shift msgs up */
       for (j = i; j < (MSGHISTORY-1); j++)
	 msgs[j] = msgs[j+1];
       msgs[MSGHISTORY-1] = NULL;
       ret = FALSE;
       break;
    }
  }

  /* delete last msg */
  if (msgs[MSGHISTORY-1] != NULL) free(msgs[MSGHISTORY-1]);

  /* shift msgs down */
  for (i = (MSGHISTORY-1); i > 0; i--)
    msgs[i] = msgs[i-1];
 
  /* add new msg to top */
  p = malloc(strlen(newmsg)+TADLEN+1);
  if (p == NULL) exit(1);
  maketad(timestamp);
  strcpy(p,timestamp);
  strcat(p,newmsg);
  msgs[0] = p;

  return(ret);
}

/* dump a timestamp into s (of len TADLEN+1) */
maketad(s)
  char *s;
{
  time_t now;
  struct tm *tmptr;

  time(&now);
  tmptr = localtime(&now);
  strftime(s,TADLEN+1,"%b-%d %H:%M ",tmptr);
}

/* return true if pool status bit in byte pos, bit mask is set */
pstat(pos,mask)
  int pos, mask;
{
  return((pool_status[pos] & mask) != 0);
}
