/* 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 DLE ETX Sometimes NULs are sent before and after the packet. note: if DLE occurs in packet it is escaped as DLE NUL!!! := := the 7-bit sum of bytes DLE, STX and := a single byte representing destination for packet := a single byte with command for destination device := 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 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 followed by is ASCII message up to 16 chars (or null terminated). 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 #include #include #include #include #include #include #include #include #include #include #include #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,"\n"); fprintf(fp,"Pool Status\n"); fprintf(fp,"\n"); time(&now); if ((now - spa_time) < SPA_COOLDOWN) { if (spa_temp > 100) fprintf(fp,"\n"); else if (spa_temp > 90) fprintf(fp,"\n"); else fprintf(fp,"\n"); } else fprintf(fp,"\n"); fprintf(fp,"

Pool Status

\n"); fprintf(fp,"
\n");
  fprintf(fp,"device status: ");
  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,"temperature:\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,"console messages:\n",msg);
  fprintf(fp,"
"); fprintf(fp,"\n"); fprintf(fp,"
\n"); fprintf(fp,"
\n"); fprintf(fp,"\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); }