Control your Jandy equipment from your PC with a $15 adapter

Re: Control your Jandy equipment from your PC with a $15 ada

Not sure how much I mentioned this; I found this device that did exactly what I was looking for. Price isn't bad, it works great, and the customer service is excellent. They have added several features to the firmware just based on my suggestions. I can now control it from my whole-house remote by sending it TCP packets. It also excepts IR commands via its IR port. Of course the homebrew solution will be great too. I know it's possible, even with the cheap adapter because the Jandy program that I found works perfect, just lacks ability to control it via the net.
http://www.autelis.com/homeautomation/pool-spa.html
 
Re: Control your Jandy equipment from your PC with a $15 ada

Thanks for the info. I followed the link you posted. Price is double what I could build it for from scratch, and about 4x what I actually paid since I had a lot of hardware lying around, and I wouldn't get the same amount of fun out of it. ;)

There is also the fact that I can make it exactly the way I want it. I much prefer my screen layouts to the layouts in the images on the web site. In addition, I am a Software Engineer by profession, and the Aqualink's behavior, and a lack of an easy way to alter it really irritated me. I have a 40,000 gal. pool with an 8 person spa, 2 water falls, in-floor cleaning, and solar heater that requires 3 pumps (originally had 4), a blower, and 5 Jandy valves. Now I can program more complex behaviors. For example, turning the blower and the jet pump on with a single command since they always work together, or oscillate between in-floor cleaning zones with the filter pump instead of having two pumps, and making sure the in-floor cleaning system isn't pumping cold water into the spa as I am trying to heat it.

The commercial one works a lot like the one I designed. I can command and monitor from any device with a bowser, or directly using the RESTful interface (HTTP over TCP). I have tested it pretty well on my LAN and home WiFi. It should be a no-brainer to open up a port in my firewall or VPN to my LAN so I can see it anywhere. Don't really care for the way the iPad and iPhone browsers work for this. So, will be implementing in iOS as soon as I am happy with the daemon. It is just about good enough for a version 1.0, but there are a few features I would like to add like independent threads to manage some of the automated tasks.

As far as the cheap adapter. I expect I just got a bad one. I haven't given up on it yet, but initially it was cheaper to buy a better one than paying for shipping costs to return a defective one or try several other brands to hopefully find a good one. Saved me a lot of frustration as well.

Thanks again. I will let you know when I post to source forge. Probably be next weekend some time.
 
Re: Control your Jandy equipment from your PC with a $15 ada

agent-P said:
I will let you know when I post to source forge. Probably be next weekend some time.
I'm interested in where you've gotten with this. I'd love to work with the code and do something similar to what you've described.
 
Re: Control your Jandy equipment from your PC with a $15 ada

Sorry, got busy and lost track of time. I got impatient and took short cuts on the mini web server and logging. It is failing duration testing. I haven't had time to completely debug it. I also want to rewrite the code that gets the spa and pool heater set points. The threads get stuck every so often. Out of town right now. I will post the code as-is here when I get home.
 
Re: Control your Jandy equipment from your PC with a $15 ada

derossi,

Here is the main source file. There are a total of 16 files. I broke up the functionality into logical groupings to make it easier to work on and extend. I was in a hurry so it is a little sloppy. In any case, here is the first one.

Code:
/*
 * aqualinkd.c
 *
 *  Created on: Aug 13, 2012
 *
 * To test daemon:	ps -ef|grep aqualinkd (or ps -aux on BSD systems)
 * To test log:	tail -f /[running_dir]/log/aqualinkd.log
 * To test signal:	kill -HUP `cat /[running_dir]/aqualinkd.lock`
 * To terminate:	kill `cat /[running_dir]/aqualinkd.lock`
*/

#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <syslog.h>
#include <unistd.h>
#include <dirent.h>
#include <time.h>
#include <pthread.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <netinet/in.h>     //
#include <sys/socket.h>     // for socket system calls
#include <arpa/inet.h>      // for socket system calls (bind)

#include "aqualink.h"
#include "aqualinkd.h"
#include "globals.h"
#include "aqualink_time.h"
#include "aqualink_temps.h"
#include "logging.h"
#include "web_server.h"


#define RUNNING_DIR	"/tmp"
#define DEVICE_ID	"0a"
#define LOCK_FILE	"aqualinkd.lock"
#define CONFIG_FILE "/etc/aqualinkd.conf"

char log_filename[MAXLEN+1];

int RUNNING = TRUE;
int CONNECTED = TRUE;

int PUMP_STATUS = OFF;
int SPA_STATUS = OFF;


void set_socket_port(char* port)
{
	config_parameters.socket_port = atoi(port);
}

void set_log_level(char* level)
{
	if (strcmp(level, "DEBUG") == 0) {
		config_parameters.log_level = DEBUG;
	}
	else if (strcmp(level, "INFO") == 0) {
		config_parameters.log_level = INFO;
	}
	else if (strcmp(level, "WARNING") == 0) {
		config_parameters.log_level = WARNING;
	}
	else {
		config_parameters.log_level = ERROR;
	}
}

// The id of the Aqualink terminal device. Devices probed by RS8 master are:
// 08-0b, 10-13, 18-1b, 20-23
// If a match is not found with a valid device id, the default value of
// "device_id" is unchanged.
void set_device_id(char* id)
{
	int i;
	int found = FALSE;

	// Iterate through the valid device ids, and match against the specified
	// id.
	for(i=0; i<NUM_DEVICES; i++) {
		if (strcmp(id, DEVICE_STRINGS[i]) == 0) {
			// Found the device id, which means the specified device
			// id is valid. Set the device id parameter and break out
			// of the loop.
			found = TRUE;
			config_parameters.device_id = DEVICE_CODES[i];
			break;
		}
	}

	// Check if we found a valid id.
	if(!found) {
		// Not found, log that we are using the default.
		char dev_id[2];
		sprintf(dev_id, "%02x.", config_parameters.device_id);
		log_to_syslog(LOG_WARNING, "ssss", "Specified device id:", id, ", is not valid. Using default id: ", dev_id);
	}
}


void daemon_shutdown()
{
	log_to_syslog(LOG_NOTICE, "s", "Aqualink daemon shutting down...");
	//exit(0);
	RUNNING = FALSE;
	CONNECTED = FALSE;
}


void signal_handler(int sig)
{
	switch(sig) {
	case SIGHUP:
		log_message(INFO, "s", "hangup signal caught");
		break;
	case SIGINT:
		log_message(INFO, "s", "interrupt signal caught");
		daemon_shutdown();
		break;
	case SIGTERM:
		log_message(INFO, "s", "terminate signal caught");
		daemon_shutdown();
		break;
	}
}


void daemonize()
{
	int i, lfp;
	char str[10];

	// If it is already a daemon, simply return without
	// doing anything.
	if(getppid()==1) return;

	// For the process.
	i = fork();
	if (i < 0) {
		// fork error, exit.
		log_to_syslog(LOG_ERR, "s", "fork() error, exiting.");
		exit(1);
	}
	if (i > 0) {
		exit(0); // parent exits
	}

	// child (daemon) continues
	// get a new process group
	setsid();

	// close all descriptors
	for (i = getdtablesize(); i >= 0; --i) {
		close(i);
	}

	// handle standard I/O
	i = open("/dev/null",O_RDWR); dup(i); dup(i);

	// set newly created file permissions, and change running directory.
	umask(027);
	chdir(config_parameters.running_directory);

	lfp = open(LOCK_FILE, O_RDWR|O_CREAT, 0640);
	if (lfp < 0) {
		log_to_syslog(LOG_ERR, "sssss", "Can not open the lock file: ", config_parameters.running_directory, "/", LOCK_FILE, ", exiting.");
		// Can not open the lock file. Exit
		exit(EXIT_FAILURE);
	}
	if (lockf(lfp, F_TLOCK, 0) < 0) {
		log_to_syslog(LOG_ERR, "sssss", "Could not lock PID lock file: ", config_parameters.running_directory, "/", LOCK_FILE, ", exiting.");
		// Can not lock it.
		exit(EXIT_FAILURE);
	}

	// The first instance continues...
	// Get the pid, and record it to the lockfile.
	sprintf(str, "%d\n", getpid());
	write(lfp, str, strlen(str));

	// Setup signal handling...
	signal(SIGCHLD, SIG_IGN); // ignore child
	signal(SIGTSTP, SIG_IGN); // ignore tty signals
	signal(SIGTTOU, SIG_IGN);
	signal(SIGTTIN, SIG_IGN);
	signal(SIGHUP, signal_handler); // catch hangup signal
	signal(SIGTERM, signal_handler); // catch kill signal
	signal(SIGINT, signal_handler); // catch kill signal
}

/*
 * trim: get rid of trailing and leading whitespace...
 *       ...including the annoying "\n" from fgets()
 */
char* trim (char * s)
{
	/* Initialize start, end pointers */
	char *s1 = s, *s2 = &s[strlen (s) - 1];

	/* Trim and delimit right side */
	while ( (isspace (*s2)) && (s2 >= s1) )
		s2--;
	*(s2+1) = '\0';

	/* Trim left side */
	while ( (isspace (*s1)) && (s1 < s2) )
		s1++;

	/* Copy finished string */
	strcpy (s, s1);
	return s;
}

/*
 * initialize data to default values
 */
void init_parameters (struct CONFIG_PARAMETERS * parms)
{
	strncpy (parms->serial_port, "/dev/ttyUSB0", MAXLEN);
	set_log_level ("WARNING");
	set_socket_port("6500");
	strncpy (parms->running_directory, RUNNING_DIR, MAXLEN);
	set_device_id(DEVICE_ID);
}

/*
 * parse external parameters file
 *
 */
void parse_config (struct CONFIG_PARAMETERS * parms)
{
	char *s, buff[256];
	FILE *fp = fopen (CONFIG_FILE, "r");
	if (fp == NULL)
	{
		log_to_syslog(LOG_WARNING, "ss", "Couldn't open configuration file: ", CONFIG_FILE);
		return;
	}

	/* Read next line */
	while ((s = fgets (buff, sizeof buff, fp)) != NULL)
	{
		/* Skip blank lines and comments */
		if (buff[0] == '\n' || buff[0] == '#')
			continue;

		/* Parse name/value pair from line */
		char name[MAXLEN], value[MAXLEN];
		s = strtok (buff, "=");
		if (s==NULL)
			continue;
		else
			strncpy (name, s, MAXLEN);
		s = strtok (NULL, "=");
		if (s==NULL)
			continue;
		else
			strncpy (value, s, MAXLEN);
		trim (value);

		/* Copy into correct entry in parameters struct */
		if (strcmp(name, "log_level") == 0) {
			set_log_level(value);
		}
		else if (strcmp(name, "running_directory") == 0) {
			strncpy (parms->running_directory, value, MAXLEN);
		}
		else if (strcmp(name, "serial_port") == 0)
			strncpy (parms->serial_port, value, MAXLEN);
		else if (strcmp(name, "socket_port") == 0) {
			set_socket_port(value);
		}
		else if (strcmp(name, "device_id") == 0) {
			set_device_id(value);
		}
		else if (strcmp(name, LABEL_NOUNS[AUX1_LABEL]) == 0) {
			strncpy(aux_function_labels[AUX1_LABEL], value, LABEL_LENGTH);
		}
		else if (strcmp(name, LABEL_NOUNS[AUX2_LABEL]) == 0) {
			strncpy(aux_function_labels[AUX2_LABEL], value, LABEL_LENGTH);
		}
		else if (strcmp(name,LABEL_NOUNS[AUX3_LABEL]) == 0) {
			strncpy(aux_function_labels[AUX3_LABEL], value, LABEL_LENGTH);
		}
		else if (strcmp(name, LABEL_NOUNS[AUX4_LABEL]) == 0) {
			strncpy(aux_function_labels[AUX4_LABEL], value, LABEL_LENGTH);
		}
		else if (strcmp(name, LABEL_NOUNS[AUX5_LABEL]) == 0) {
			strncpy(aux_function_labels[AUX5_LABEL], value, LABEL_LENGTH);
		}
		else if (strcmp(name, LABEL_NOUNS[AUX6_LABEL]) == 0) {
			strncpy(aux_function_labels[AUX6_LABEL], value, LABEL_LENGTH);
		}
		else if (strcmp(name, LABEL_NOUNS[AUX7_LABEL]) == 0) {
			strncpy(aux_function_labels[AUX7_LABEL], value, LABEL_LENGTH);
		}
		else {
			log_to_syslog(LOG_WARNING, "ssss", name, "/", value, "Unknown configuration name/value pair!");
		}
	}

	// Set the logging parameters: running_directory, and level
	set_logging_parameters(parms->running_directory, parms->log_level);

	/* Close file */
	fclose (fp);
}


// **********************************************************************************************
// AQUALINK FUNCTIONS
// **********************************************************************************************


// Initialize the Aqualink data. Some data is state dependent, and will not be accurate
// until the Aqualink master device is in the proper state. For example, pool temperature
// and spa temperature are not available unless the filter pump is on.
void init_aqualink_data()
{
	aqualink_data.air_temp = -999;
	aqualink_data.pool_temp = -999;
	aqualink_data.spa_temp = -999;
	aqualink_data.version[0] = '\0';
	aqualink_data.date[0] = '\0';
	aqualink_data.time[0] = '\0';
	aqualink_data.temp_units = UNKNOWN;
	aqualink_data.freeze_protection = OFF;
}

/*
 Open and Initialize the serial communications port to the Aqualink RS8 device.
 Arg is tty or port designation string
 returns the file descriptor
 */
int init_serial_port(char* tty, struct termios* oldtio)
{
	long BAUD = B9600;
	long DATABITS = CS8;
	long STOPBITS = 0;
	long PARITYON = 0;
	long PARITY = 0;

	struct termios newtio;       //place for old and new port settings for serial port

    int file_descriptor = open(tty, O_RDWR | O_NOCTTY | O_NONBLOCK);
    if (file_descriptor < 0)  {
        log_to_syslog(LOG_ERR, "ss", "init_serial_port(): Unable to open port: ", tty);
    }
    else {
    	// Set the port to block on read() if there is no data.
    	fcntl(file_descriptor, F_SETFL, 0);

    	tcgetattr(file_descriptor, oldtio); // save current port settings
    	// set new port settings for canonical input processing
    	newtio.c_cflag = BAUD | DATABITS | STOPBITS | PARITYON | PARITY | CLOCAL | CREAD;
    	newtio.c_iflag = IGNPAR;
    	newtio.c_oflag = 0;
    	newtio.c_lflag = 0;       // ICANON;
    	newtio.c_cc[VMIN]= 1;
    	newtio.c_cc[VTIME]= 0;
    	tcflush(file_descriptor, TCIFLUSH);
    	tcsetattr(file_descriptor, TCSANOW, &newtio);
    }

    return file_descriptor;
}

/* close tty port */
void close_port(int file_descriptor, struct termios* oldtio)
{
	tcsetattr(file_descriptor, TCSANOW, oldtio);
    close(file_descriptor);
}


// Generate and return checksum of packet.
int generate_checksum(unsigned char* packet, int length)
{
    int i, sum, n;

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


// Send an ack packet to the Aqualink RS8 master device.
// file_descriptor: the file descriptor of the serial port connected to the device
// command: the command byte to send to the master device, NUL if no command
void send_ack(int file_descriptor, unsigned char command)
{
    const int length = 11;
    unsigned char ackPacket[] = { NUL, DLE, STX, DEV_MASTER, CMD_ACK, NUL, NUL, 0x13, DLE, ETX, NUL };

    // Update the packet and checksum if command argument is not NUL.
    if(command != NUL) {
    	ackPacket[6] = command;
    	ackPacket[7] = generate_checksum(ackPacket, length-1);

    	// NULL out the command byte if it is the same. Difference implies that
    	// a new command has come in, and is awaiting processing.
        if(aqualink_cmd == command) {
        	aqualink_cmd = NUL;
        }

        if(config_parameters.log_level == DEBUG) {
        	// In debug mode, log the packet to the private log file.
        	log_packet(ackPacket, length);
        }
    }

    // Send the packet to the master device.
    write(file_descriptor, ackPacket, length);
}


// Reads the bytes of the next incoming packet, and
// returns when a good packet is available in packet
// file_descriptor: the file descriptor to read the bytes from
// packet: the unsigned char buffer to store the bytes in
// returns the length of the packet
int get_packet(int file_descriptor, unsigned char* packet)
{
	unsigned char byte;
	int bytesRead;
	int index = 0;
	int endOfPacket = FALSE;
	int packetStarted = FALSE;
	int foundDLE = FALSE;

	while (!endOfPacket) {

		bytesRead = read(file_descriptor, &byte, 1);

		if (bytesRead == 1) {

			if (byte == DLE) {
				// Found a DLE byte. Set the flag, and record the byte.
				foundDLE = TRUE;
				packet[index] = byte;
			}
	        else if (byte == STX && foundDLE == TRUE) {
	            // Found the DLE STX byte sequence. Start of packet detected.
	            // Reset the DLE flag, and record the byte.
	            foundDLE = FALSE;
	            packetStarted = TRUE;
	            packet[index] = byte;
	        }
	        else if (byte == NUL && foundDLE == TRUE) {
	            // Found the DLE NUL byte sequence. Detected a delimited data byte.
	            // Reset the DLE flag, and decrement the packet index to offset the
	            // index increment at the end of the loop. The delimiter, [NUL], byte
	        	// is not recorded.
	            foundDLE = FALSE;
	            //trimmed = true;
	            index--;
	        }
	        else if (byte == ETX && foundDLE == TRUE) {
	            // Found the DLE ETX byte sequence. End of packet detected.
	            // Reset the DLE flag, set the end of packet flag, and record
	            // the byte.
	            foundDLE = FALSE;
	            packetStarted = FALSE;
	            endOfPacket = TRUE;
	            packet[index] = byte;
	        }
	        else if (packetStarted == TRUE) {
	            // Found a data byte. Reset the DLE flag just in case it is set
	            // to prevent anomalous detections, and record the byte.
	            foundDLE = FALSE;
	            packet[index] = byte;
	        }
	        else {
	        	// Found an extraneous byte. Probably a NUL between packets.
	        	// Ignore it, and decrement the packet index to offset the
	            // index increment at the end of the loop.
	            index--;
	        }

	        // Finished processing the byte. Increment the packet index for the
	        // next byte.
	        index++;

	        // Break out of the loop if we exceed maximum packet
	        // length.
	        if (index >= MAXPKTLEN) {
	            break;
	        }
		}
		else if(bytesRead <= 0) {
			// Got a read error. Wait one millisecond for the next byte to
			// arrive.
			log_message(WARNING, "siss", "Read error: ", errno, " - ", strerror(errno));
			if(errno == 9) {
				// Bad file descriptor. Port has been disconnected for some reason.
				// Return a -1.
				return -1;
			}
			usleep(1000);
		}
	}

	// Return the packet length.
	return index;
}


void log_packet(unsigned char* packet, int length)
{
    int i;
    char temp_string[64];
    char message_buffer[MAXLEN];

	sprintf(temp_string, "%02x ", packet[0]);
	strcpy(message_buffer, temp_string);

    for (i = 1; i < length; i++) {
    	sprintf(temp_string, "%02x ", packet[i]);
    	strcat(message_buffer, temp_string);
    }

    log_message(DEBUG, "s", message_buffer);
}


void process_long_message(char* message)
{
	log_message(DEBUG, "ss", "Processing long message: ", message);

	const char pool_setting_string[] = { "POOL TEMP IS SET TO" };
	const char spa_setting_string[] = { "SPA TEMP IS SET TO" };
	const char frz_protect_setting_string[] = { "FREEZE PROTECTION IS SET TO" };

	// Remove any leading white space.
	trim(message);

	// Extract data and warnings from long messages.
	if(strstr(message, "BATTERY LOW") != NULL) {
		aqualink_data.battery = LOW;
	}
	else if(strstr(message, pool_setting_string) != NULL) {
		//log_message(DEBUG, "ss", "pool htr long message: ", message+20);
		aqualink_data.pool_htr_set_point = atoi(message+20);
	}
	else if(strstr(message, spa_setting_string) != NULL) {
		//log_message(DEBUG, "ss", "spa htr long message: ", message+19);
		aqualink_data.spa_htr_set_point = atoi(message+19);
	}
	else if(strstr(message, frz_protect_setting_string) != NULL) {
		//log_message(DEBUG, "ss", "frz protect long message: ", message+28);
		aqualink_data.frz_protect_set_point = atoi(message+28);
	}

}


void process_packet(unsigned char* packet, int length)
{
	static int got_long_msg;
	static char message[MSGLEN+1];
	static unsigned char last_packet[MAXPKTLEN];

	pthread_t set_time_thread;
	pthread_t get_htr_set_pnts_thread;
	pthread_t get_frz_protect_set_pnt_thread;

	if(memcmp(packet, last_packet, MAXPKTLEN) == 0) {
		// Don't process redundant packets. They can occur for two reasons.
		// First, status doesn't change much so the vast majority of packets
		// are identical under normal circumstances. It is more efficient to
		// process only changes. Second, the master will send redundant packets
		// if it misses an ACK response up to 3 times before it sends a
		// command probe. Redundant message packets can corrupt long message
		// processing.

		// Log the redundant packets other than STATUS packets at DEBUG level.
		if(packet[PKT_CMD] != CMD_STATUS && config_parameters.log_level == DEBUG) {
			log_message(DEBUG, "s", "Trapped redundant packet...");
			log_packet(packet, length);
		}

		// Return without processing the packet.
		return;
	}
	else {
		// Normal packet. Copy it for testing against the next packet.
		memcpy(last_packet, packet, MAXPKTLEN);
	}

	// Process by packet type.
	if(packet[PKT_CMD] == CMD_STATUS) {
		// Update status. Copy it to last status buffer.
		memcpy(aqualink_data.status, packet+4, PSTLEN);

		// Set mode status. Accurate temperature processing is
		// dependent on this.
		if((aqualink_data.status[LED_STATES[STAT_BYTE][STAT_PUMP]] & LED_STATES[STAT_MASK][STAT_PUMP]) != 0) {
			PUMP_STATUS = ON;
		}
		else if(aqualink_data.status[LED_STATES[STAT_BYTE][STAT_PUMP_BLINK]] & LED_STATES[STAT_MASK][STAT_PUMP_BLINK]) {
			PUMP_STATUS = ENABLED;
		}
		else {
			PUMP_STATUS = OFF;
		}
		if((aqualink_data.status[LED_STATES[STAT_BYTE][STAT_SPA]] & LED_STATES[STAT_MASK][STAT_SPA]) != 0) {
			SPA_STATUS = ON;
		}
		else {
			SPA_STATUS = OFF;
		}
	}
	else if(packet[PKT_CMD] == CMD_MSG && (packet[PKT_DATA] == 0)) {
		// Packet is a single line message.
		if(got_long_msg) {
			got_long_msg = FALSE;
			process_long_message(aqualink_data.last_message);
			log_message(DEBUG, "s", aqualink_data.last_message);
		}
		// First, extract the message from the packet.
		memset(message, 0, MSGLEN+1);
		strncpy(message, (char*)packet+PKT_DATA+1, MSGLEN);
		log_message(DEBUG, "s", message);

		// Copy the message to the Aqualink data structure as the latest message.
		memset(aqualink_data.last_message, 0, MSGLONGLEN);
		strncpy(aqualink_data.last_message, message, MSGLEN+1);

		// Remove the leading white space so the pointer offsets are accurate
		// and consistent.
		trim(message);

		if(strstr(message, "AIR TEMP") != NULL) {
            aqualink_data.air_temp = atoi(message+8);
            // Check temperature units. Note AIR TEMP is always present.
            // So, get the temperature units here.
            if(strstr(message, "F") != NULL) {
            	aqualink_data.temp_units = FAHRENHEIT;
            }
            else if(strstr(message, "C") != NULL) {
            	aqualink_data.temp_units = CELSIUS;
            }
            else {
            	aqualink_data.temp_units = UNKNOWN;
            	log_message(WARNING, "ss", "Can't determine temperature units from message: ", message);
            }

            // Reset pool and spa temperatures to unknown values if the
            // corresponding modes are off.
            if(PUMP_STATUS == OFF) {
            	aqualink_data.pool_temp = -999;
            }
            if(SPA_STATUS == OFF) {
            	aqualink_data.spa_temp = -999;
            }
		}
		else if(strstr(message, "POOL TEMP") != NULL) {
            aqualink_data.pool_temp = atoi(message+9);
		}
		else if(strstr(message, "SPA TEMP") != NULL) {
            aqualink_data.spa_temp = atoi(message+8);
		}
		else if((message[1] == ':' && message[4] == ' ') || (message[2] == ':' && message[5] == ' ')) {
			// A time message.
			strcpy(aqualink_data.time, message);

			// If date is not an empty string, check the Aqualink time and date
			// against system time.
			if(aqualink_data.date[0] != '\0') {
				if(!check_aqualink_time(aqualink_data.date, aqualink_data.time)) {
					log_message(DEBUG, "sssss", "Aqualink time ", aqualink_data.date, " ", aqualink_data.time, " is incorrect");

					if(pthread_create(&set_time_thread, NULL, &set_aqualink_time, (void*)NULL)) {
						log_to_syslog(LOG_WARNING, "s", "Error creating SET TIME thread.");
					}
				}
			}
		}
		else if (message[2] == '/' && message[5] == '/') {
			// A date message.
			strcpy(aqualink_data.date, message);
		}
		else if(strstr(message, " REV ") != NULL) {
			// A master firmware revision message.
			strcpy(aqualink_data.version, message);

			// This is a good indicator that the daemon has just started
			// or has just been forced to resynch with the master. It is a
			// good time to initialize the daemon with some Aqualink set
			// point data.
			if(pthread_create(&get_htr_set_pnts_thread, NULL, &get_pool_spa_htr_temps, (void*)NULL)) {
				log_to_syslog(LOG_WARNING, "s", "Error creating get heater set points thread.");
			}
			if(pthread_create(&get_frz_protect_set_pnt_thread, NULL, &get_frz_protect_temp, (void*)NULL)) {
				log_to_syslog(LOG_WARNING, "s", "Error creating get freeze protection set point thread.");
			}

		}
	}
	else if(packet[PKT_CMD] == CMD_MSG && (packet[PKT_DATA] == 1)) {
		// Packet is the start of a multi-line message.
		if(got_long_msg) {
			got_long_msg = FALSE;
			process_long_message(aqualink_data.last_message);
			log_message(DEBUG, "s", aqualink_data.last_message);
		}
		got_long_msg = TRUE;
		memset(message, 0, MSGLEN+1);
		strncpy(message, (char*)packet+PKT_DATA+1, MSGLEN);
		strcpy(aqualink_data.last_message, message);
	}
	else if(packet[PKT_CMD] == CMD_MSG_LONG) {
		// Packet is continuation of a long message.
		strncpy(message, (char*)packet+PKT_DATA+1, MSGLEN);
		strcat(aqualink_data.last_message, message);
	}
	else if(packet[PKT_CMD] == CMD_PROBE) {
		// Packet is a command probe. The master is trying to find
		// this device.
		log_message(INFO, "s", "Synch'ing with Aqualink master device...");
	}
}




// **********************************************************************************************
// MAIN FUNCTION
// **********************************************************************************************

int main()
{
	// Reference to the server thread.
	pthread_t server_thread;

	// Log only NOTICE messages and above. Debug and info messages
	// will not be logged to syslog.
	setlogmask(LOG_UPTO (LOG_NOTICE));

	// Initialize the daemon's parameters.
	init_parameters(&config_parameters);

	// Initialize Aqualink data.
	init_aqualink_data();

	// Parse the external configuration file, and override initialization
	// of the default values of the specified parameters.
	parse_config(&config_parameters);

	// Log the start of the daemon to syslog.
	log_to_syslog(LOG_NOTICE, "s", "Aqualink daemon started...");

	// Turn this application into a daemon.
	daemonize();

	// Create a thread to serve socket connections to read and command the Aqualink RS8
	// remotely from other applications.
	if(pthread_create(&server_thread, NULL, &socket_server, (void*)NULL)) {
		log_to_syslog(LOG_ERR, "s", "Error creating socket server thread, exiting...");
		return EXIT_FAILURE;
	}

	// Start the main loop of the primary thread. It will make 10 connect attempts
	// before exiting on failure. It will sleep two (2) minutes between attempts.
	int init_attempts = 0;
	while(CONNECTED) {
		int packet_length;

		// Initialize the serial port connected to the Aqualink master device.
		// Save the old termios in case we want to reset when we close the port.
		struct termios oldtio;
		int file_descriptor = init_serial_port(config_parameters.serial_port, &oldtio);
		if(file_descriptor < 0) {
			if(init_attempts > 10) {
				log_to_syslog(LOG_ERR, "s", "Aqualink daemon exit on failure to connect to master device.");
				return EXIT_FAILURE;
			}
			else {
				log_to_syslog(LOG_NOTICE, "s", "Aqualink daemon attempting to connect to master device...");
				init_attempts++;
			}
		}
		else {
			// Reset attempts counter, and log the successful connection.
			init_attempts = 0;
			log_to_syslog(LOG_NOTICE, "ss", "Listening to Aqualink RS8 on serial port: ", config_parameters.serial_port);

			// The packet buffer.
			unsigned char packet_buffer[MAXPKTLEN];

			// The primary loop that reads and processes the Aqualink RS8 serial port data.
			while(RUNNING) {
				packet_length = get_packet(file_descriptor, packet_buffer);

				if(packet_length == -1) {
					// Unrecoverable read error. Break out of the inner loop,
					// and attempt to reconnect.
					break;
				}

				// Ignore packets not for this Aqualink terminal device.
				if (packet_buffer[PKT_DEST] == config_parameters.device_id) {

					// The packet was meant for this device. Acknowledge it, including any command
					// that might be waiting in the "aqualink_cmd" variable. Note "aqualink_cmd"
					// is reset to NUL after a command is sent.
					send_ack(file_descriptor, aqualink_cmd);

					//log_packet(packet_buffer, packet_length);

					// Process the packet. This includes deriving general status, and identifying
					// warnings and errors.
					process_packet(packet_buffer, packet_length);
				}
			}

			// Reset and close the port.
			close_port(file_descriptor, &oldtio);
		}

		if(packet_length == -1 || file_descriptor < 0) {
			// Wait 2 minutes before trying to reconnect.
			log_to_syslog(LOG_NOTICE, "ss", "Connection lost to Aqualink RS8 on serial port: ", config_parameters.serial_port);
			log_to_syslog(LOG_NOTICE, "s", "Pausing 2 minutes before attempting reconnect...");
			sleep(120);
		}
	}

	log_message(INFO, "s", "Aqualink daemon exiting...");
	return EXIT_SUCCESS;
}

/* EOF */
 
Re: Control your Jandy equipment from your PC with a $15 ada

The next file is an example of the config file for the daemon. It is hard coded to go in /etc. Note the socket port 6500 is only for test. I set it to 80 on my home automation server so I don't have to specify a port when I access it.

Code:
# aqualinkd.conf
#
#  Created on: Aug 17, 2012
#

# The directory where the lock and log files are stored
running_directory=/tmp

# The log level. [DEBUG, INFO, WARNING, ERROR]
log_level=DEBUG

# The socket port that the daemon listens to
socket_port=6500

# The serial port the daemon access to read the Aqualink RS8
serial_port=/dev/ttyUSB0

# The id of the Aqualink terminal device. Devices probed by RS8 master are:
# 08-0b, 10-13, 18-1b, 20-23,
device_id=0a

# The labels for the AUX functions 1 - 7
# They can be any string with a limit of 31 readable characters
aux1_label=Water Fall
aux2_label=Spa Jets
aux3_label=Spa Blower
aux4_label=Pool/Spa Lights
aux5_label=Lights Color Wheel
aux6_label=Unassigned
aux7_label=Unassigned
 
Re: Control your Jandy equipment from your PC with a $15 ada

File #3 - The header for the daemon's main source file:

Code:
/*
 * aqualinkd.h
 *
 *  Created on: Sep 22, 2012
 */

#ifndef AQUALINKD_H_
#define AQUALINKD_H_


void send_ack(int file_descriptor, unsigned char command);
void log_packet(unsigned char* packet, int length);
int genchecksum(unsigned char* buf, int len);
int get_packet(int file_descriptor, unsigned char* packet);
void process_packet(unsigned char* packet, int length);
int init_serial_port(char* tty, struct termios* oldtio);
void close_port(int file_descriptor, struct termios* oldtio);
int addmsg(char* newmsg);
void maketad(char* s);


#endif /* AQUALINKD_H_ */
 
Re: Control your Jandy equipment from your PC with a $15 ada

File #4 - The header file for the whole project:

Code:
/*
 * aqualink.h
 *
 *  Created on: Aug 17, 2012
 */

#ifndef AQUALINK_H_
#define AQUALINK_H_

// 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,
#define DEV_MASTER      0

#define NUM_DEVICES     16

// COMMAND KEYS
enum {
	KEY_PUMP = 0,
	KEY_SPA,
	KEY_AUX1,
	KEY_AUX2,
	KEY_AUX3,
	KEY_AUX4,
	KEY_AUX5,
	KEY_AUX6,
	KEY_AUX7,
	KEY_HTR_POOL,
	KEY_HTR_SPA,
	KEY_HTR_SOLAR,
	KEY_MENU,
	KEY_CANCEL,
	KEY_LEFT,
	KEY_RIGHT,
	KEY_HOLD,
	KEY_OVERRIDE,
	KEY_ENTER
};

#define NUM_KEYS   19

/* 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.
 */

enum {
	STAT_PUMP = 0,
	STAT_SPA,
	STAT_AUX1,
	STAT_AUX2,
	STAT_AUX3,
	STAT_AUX4,
	STAT_AUX5,
	STAT_AUX6,
	STAT_AUX7,
	STAT_HTR_POOL_ON,
	STAT_HTR_SPA_ON,
	STAT_HTR_SOLAR_ON,
	STAT_PUMP_BLINK,
	STAT_HTR_POOL_EN,
	STAT_HTR_SPA_EN,
	STAT_HTR_SOLAR_EN
};

enum {
	STAT_BYTE = 0,
	STAT_MASK
};

enum {
	OFF = 0,
	ON,
	ENABLED
};

#define MAX_LED_STAT_LENGTH 8

enum {
	VERSION_NOUN = 0,
	DATE_NOUN,
	TIME_NOUN,
	LAST_MESSAGE_NOUN,
	TEMP_UNITS_NOUN,
	AIR_TEMP_NOUN,
	POOL_TEMP_NOUN,
	SPA_TEMP_NOUN,
	BATTERY_NOUN,
	POOL_HTR_TEMP_NOUN,
	SPA_HTR_TEMP_NOUN,
	FRZ_PROTECT_NOUN,
	FRZ_PROTECT_TEMP_NOUN,
	LEDS_NOUN
};

enum {
	PUMP_NOUN = 0,
	SPA_NOUN,
	AUX1_NOUN,
	AUX2_NOUN,
	AUX3_NOUN,
	AUX4_NOUN,
	AUX5_NOUN,
	AUX6_NOUN,
	AUX7_NOUN,
	POOL_HTR_NOUN,
	SPA_HTR_NOUN,
	SOLAR_HTR_NOUN
};

#define NUM_RS8_NOUNS 14

#define NUM_LED_NOUNS 12

// TIME IDENTIFIERS
enum {
	YEAR = 0,
	MONTH,
	DAY,
	HOUR,
	MINUTE
};

#define MAX_TIME_FIELD_LENGTH  7

// Battery Status Identifiers
enum {
	OK = 0,
	LOW
};

#define FALSE 0
#define TRUE  1

//#define OFF   FALSE
//#define ON    TRUE

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

#define MINPKTLEN  5
#define MAXPKTLEN 64
#define PSTLEN 5
#define MSGLEN 16
#define MSGLONGLEN 128
#define TADLEN 13
/* how many seconds spa stays warm after shutdown */
#define SPA_COOLDOWN   (20*60)

#define FAHRENHEIT 0
#define CELSIUS    1
#define UNKNOWN    2

enum {
	DEBUG = 0,
	INFO,
	WARNING,
	ERROR
};

// GLOBALS

struct AQUALINK_DATA
{
	char version[MSGLEN];
	char date[MSGLEN];
	char time[MSGLEN];
	char last_message[MSGLONGLEN];
	unsigned char status[PSTLEN];
	int air_temp;
	int pool_temp;
	int spa_temp;
	int temp_units;
	int battery;
	int freeze_protection;
	int frz_protect_set_point;
	int pool_htr_set_point;
	int spa_htr_set_point;
};

#define MAXLEN      256
struct CONFIG_PARAMETERS
{
  char serial_port[MAXLEN];
  int log_level;
  int socket_port;
  char running_directory[MAXLEN];
  unsigned char device_id;
};


#endif /* AQUALINK_H_ */
 
Re: Control your Jandy equipment from your PC with a $15 ada

File #5 - Source file for handling the aqualink's menu functions. It is woefully incomplete at this point:

Code:
/*
 * aqualink_menu.c
 *
 *  Created on: Sep 23, 2012
 */
#include <string.h>
#include <unistd.h>
#include "aqualink.h"
#include "aqualink_menu.h"
#include "globals.h"


void cancel_menu()
{
	send_cmd("KEY_CANCEL");
}


int select_menu_item(char* item_string)
{
    // Select the MENU and wait 1 second to give the RS8 time to respond.
    send_cmd("KEY_MENU");
    sleep(1);

    int item = select_sub_menu_item(item_string);

    // Return TRUE if the mode specified by the argument is selected, FALSE, if not.
    return item;
}


int select_sub_menu_item(char* item_string)
{
    // Select the mode specified by the argument. Note only 10 attempts to
    // enter the mode are made.
	int item = FALSE;
    int iterations = 0;
    while(!item && iterations < 10) {
    	//log_message(DEBUG, aqualink_data.last_message);
    	if(strstr(aqualink_data.last_message, item_string) != NULL) {
    		// We found the specified mode. Set the flag to break out of
    		// the loop.
    		item = TRUE;

    		// Enter the mode specified by the argument.
    		send_cmd("KEY_ENTER");
        	sleep(1);
    	}
    	else {
    		// Next menu item and wait 1 second to give the RS8 time
    		// to respond.
    		send_cmd("KEY_RIGHT");
    		sleep(1);
    	}
    	iterations++;
    }

    // Return TRUE if the mode specified by the argument is selected, FALSE, if not.
    return item;
}
 

Enjoying this content?

Support TFP with a donation.

Give Support
Re: Control your Jandy equipment from your PC with a $15 ada

File #6 - Header file for handling the aqualink's menu functions.

Code:
/*
 * aqualink_menu.h
 *
 *  Created on: Sep 23, 2012
 */

#ifndef AQUALINK_MENU_H_
#define AQUALINK_MENU_H_

int select_menu_item(char* mode_string);
int select_sub_menu_item(char* item_string);
void cancel_menu();

#endif /* AQUALINK_MENU_H_ */
 
Re: Control your Jandy equipment from your PC with a $15 ada

File #7 - Source file for getting and setting heater temperature settings. This is also about 50% complete.

Code:
/*
 * aqualink_temps.c
 *
 *  Created on: Sep 23, 2012
 */

#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>

#include "aqualink.h"
#include "globals.h"
#include "aqualink_menu.h"

void* get_pool_spa_htr_temps(void* arg)
{
	static int thread_running;

	// Detach the thread so that it cleans up as it exits. Memory leak without it.
	pthread_detach(pthread_self());

	if(!thread_running) {
		thread_running = TRUE;

		int done = FALSE;
		while(!done) {
			if(!PROGRAMMING) {
				// Set the flag to let the daemon know a program of multiple
				// commands is being sent to the Aqualink RS8.
				PROGRAMMING = TRUE;

				log_message(INFO, "s", "Retrieving pool and spa heater set points...");

				// Select REVIEW mode.
				int review_mode = select_menu_item("REVIEW");

				if(review_mode) {
					// Retrieve the pool and spa heater temperature set points.
					select_sub_menu_item("TEMP SET");

					// Wait 5 seconds for the messages to be sent from the master device.
					sleep(5);
				}
				else {
					// Log the failure and take it back to a known state.
					log_message(WARNING, "s", "Could not select REVIEW to retrieve heater set points.");
					cancel_menu();
				}

				// Reset the programming state flag.
				PROGRAMMING = FALSE;
				done = TRUE;
			}
			else {
				log_message(DEBUG, "s", "Get Pool and Spa Heater Set Points waiting to execute...");
				sleep(5);
			}
		}

		thread_running = FALSE;
	}
	else {
		log_message(WARNING, "s", "Get Pool and Spa Heater Set Points thread instance already running...");
	}

    return 0;
}


void* set_pool_htr_temp(void* arg)
{
	// Detach the thread so that it cleans up as it exits. Memory leak without it.
	pthread_detach(pthread_self());

    // Set the flag to let the daemon know a program of multiple
    // commands is being sent to the Aqualink RS8.
    PROGRAMMING = TRUE;







    // Reset the programming state flag.
    PROGRAMMING = FALSE;
    return 0;
}


void* set_spa_htr_temp(void* arg)
{
	// Detach the thread so that it cleans up as it exits. Memory leak without it.
	pthread_detach(pthread_self());

    // Set the flag to let the daemon know a program of multiple
    // commands is being sent to the Aqualink RS8.
    PROGRAMMING = TRUE;






    // Reset the programming state flag.
    PROGRAMMING = FALSE;
    return 0;
}



void* get_frz_protect_temp(void* arg)
{
	static int thread_running;

	// Detach the thread so that it cleans up as it exits. Memory leak without it.
	pthread_detach(pthread_self());

	if(!thread_running) {
		thread_running = TRUE;

		int done = FALSE;
		while(!done) {
			if(!PROGRAMMING) {
				// Set the flag to let the daemon know a program of multiple
				// commands is being sent to the Aqualink RS8.
				PROGRAMMING = TRUE;

				log_message(INFO, "s", "Retrieving freeze protection set point...");

				// Select REVIEW mode.
				int review_mode = select_menu_item("REVIEW");

				if(review_mode) {
					// Retrieve the pool and spa heater temperature set points.
					select_sub_menu_item("FRZ PROTECT");

					// Wait 5 seconds for the messages to be sent from the master device.
					sleep(5);
				}
				else {
					// Log the failure and take it back to a known state.
					log_message(WARNING, "s", "Could not select REVIEW to retrieve freeze protection set point.");
					cancel_menu();
				}

				// Reset the programming state flag.
				PROGRAMMING = FALSE;
				done = TRUE;
			}
			else {
				log_message(DEBUG, "s", "Get Freeze Protect Set Point waiting to execute...");
				sleep(5);
			}
		}

		thread_running = FALSE;
	}
	else {
		log_message(WARNING, "s", "Get Pool and Spa Heater Set Points thread instance already running...");
	}

	return 0;
}


void* set_frz_protect_temp(void* arg)
{
	// Detach the thread so that it cleans up as it exits. Memory leak without it.
	pthread_detach(pthread_self());

    // Set the flag to let the daemon know a program of multiple
    // commands is being sent to the Aqualink RS8.
    PROGRAMMING = TRUE;




    // Reset the programming state flag.
    PROGRAMMING = FALSE;
    return 0;
}
 
Re: Control your Jandy equipment from your PC with a $15 ada

File #8 - Header file for getting and setting heater temperature settings.

Code:
/*
 * aqualink_temps.h
 *
 *  Created on: Sep 23, 2012
  */

#ifndef AQUALINK_TEMPS_H_
#define AQUALINK_TEMPS_H_

void* get_pool_spa_htr_temps(void* arg);
void* set_pool_htr_temp(void* arg);
void* set_spa_htr_temp(void* arg);

void* get_frz_protect_temp(void* arg);
void* set_frz_protect_temp(void* arg);

#endif /* AQUALINK_TEMPS_H_ */
 
Re: Control your Jandy equipment from your PC with a $15 ada

File #9 - Source file for getting and setting the aqualink's time.

Code:
/*
 * aqualink_time.c
 *
 *  Created on: Sep 22, 2012
  */

#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>

#include "aqualink.h"
#include "globals.h"

int check_aqualink_time(char* _date, char* _time)
{
    time_t now;
    struct tm *tmptr;
    int time_date_correct = FALSE;

    int a_hour;
    int a_min;
    int a_day;
    int a_month;
    int a_year;
    char a_pm[3];

    // Get the current time.
    time(&now);
    tmptr = localtime(&now);

    // Make sure time is at least 2 minutes apart to prevent HI/LO toggling.
    // Get the minutes for each time for a comparison.
    struct tm str_time;
    time_t aqualink_time;

    // Parse the time and date components to create a time value for
    // comparison to the current time.
    sscanf(_time, "%d:%d %s", &a_hour, &a_min, a_pm);
    sscanf(_date, "%d/%d/%d", &a_month, &a_day, &a_year);

    // Build a time structure from the Aqualink time.
    str_time.tm_year = a_year + 2000 - 1900;  // adjust for correct century
    str_time.tm_mon = a_month - 1;     // adjust or correct date 0-based index
    str_time.tm_mday = a_day;
    if(a_hour < 12 && (strstr(a_pm, "PM") != NULL)) {
    	str_time.tm_hour =  a_hour + 12;
    }
    else if(a_hour == 12 && (strstr(a_pm, "AM") != NULL)) {
    	str_time.tm_hour =  0;
    }
    else {
    	str_time.tm_hour = a_hour;
    }
    str_time.tm_min = a_min;
    str_time.tm_sec = 0;       // Aqualink time doesn't have seconds.
    str_time.tm_isdst = 0;     // always standard time in AZ

    // Make the Aqualink time value.
    aqualink_time = mktime(&str_time);
    //log_message(DEBUG, ctime(&now));
    //log_message(DEBUG, ctime(&aqualink_time));

    // Calculate the time difference between Aqualink time and local time.
    int time_difference = (int)difftime(now, aqualink_time);

    log_message(DEBUG, "sis", "Aqualink time is off by ",  time_difference, " seconds...");

    if(abs(time_difference) <= 90) {
    	// Time difference is less than or equal to 90 seconds (1 1/2 minutes).
    	// Set the return value to TRUE.
    	time_date_correct = TRUE;
    }


	return time_date_correct;
}


/* Set the Aqualink RS8's time's year to the local time's year.
 *
 */
void set_aqualink_time_field(int field, char* field_string)
{
	log_message(DEBUG, "s", "Attempting to set aqualink time field...");

	int local_field;
	char local_mod[MAX_TIME_FIELD_LENGTH];
	int aqualink_field;
	char field_id[MAX_TIME_FIELD_LENGTH];
	char field_mod[MAX_TIME_FIELD_LENGTH];

	// Parse the local time field string.
	sscanf(field_string, "%d %s", &local_field, local_mod);

	if(strstr(aqualink_data.last_message, TIME_FIELD_TEXT[field]) != NULL) {
		 do {
			 // Parse the numeric field from the Aqualink data string.
			sscanf(aqualink_data.last_message, "%s %d %s", field_id, &aqualink_field, field_mod);

			if(strcmp(local_mod, "AM") == 0 || strcmp(local_mod, "PM") == 0) {
				if(strcmp(local_mod, field_mod) == 0 && local_field == aqualink_field) {
					// Field is set correctly. Send the ENTER command
					// to move to the next time field, and break out
					// of the loop.
					send_cmd("KEY_ENTER");
					break;
				}
				else {
					// Increment the field.
					send_cmd("KEY_RIGHT");
				}
			}
			else {
				if(aqualink_field < local_field) {
					// Increment the field.
					send_cmd("KEY_RIGHT");
				}
				else if(aqualink_field > local_field) {
					// Decrement the field.
					send_cmd("KEY_LEFT");
				}
				else {
					// Field is set correctly. Send the ENTER command
					// to move to the next time field, and break out
					// of the loop.
					send_cmd("KEY_ENTER");
					break;
				}
			}

			// Wait one second.
			sleep(1);
		} while(aqualink_field != local_field);
	}
}


/* Set the Aqualink RS8's time to the local time.
 *
 */
void* set_aqualink_time(void* arg)
{
	log_message(INFO, "s", "Attempting to set aqualink time...");
	const int YEAR_LENGTH = 5;
	const int MONTH_LENGTH = 3;
	const int DAY_LENGTH = 3;
	const int HOUR_LENGTH = 6;
	const int MINUTE_LENGTH = 3;

	int set_time_mode = FALSE;

    time_t now;
    struct tm *tmptr;
    char year[YEAR_LENGTH];
    char month[MONTH_LENGTH];
    char day[DAY_LENGTH];
    char hour[HOUR_LENGTH];
    char minute[MINUTE_LENGTH];

	// Detach the thread so that it cleans up as it exits. Memory leak without it.
	pthread_detach(pthread_self());

    // Get the current time.
    time(&now);
    tmptr = localtime(&now);

    // Convert to time and date strings compatible with the Aqualink
    // time and date strings.
    strftime(year, YEAR_LENGTH, "%Y", tmptr);
    strftime(month, MONTH_LENGTH, "%m", tmptr);
    strftime(day, DAY_LENGTH, "%d", tmptr);
    strftime(hour, HOUR_LENGTH, "%I %p", tmptr);
    strftime(minute, MINUTE_LENGTH, "%M", tmptr);

    // Set the flag to let the daemon know a program of multiple
    // commands is being sent to the Aqualink RS8.
    PROGRAMMING = TRUE;

    // Select the MENU and wait 1 second to give the RS8 time to respond.
    send_cmd("KEY_MENU");
    sleep(1);

    // Select the SET TIME mode. Note only 10 attempts to enter the mode
    // are made.
    int iterations = 0;
    while(!set_time_mode && iterations < 10) {
    	//log_message(DEBUG, aqualink_data.last_message);
    	if(strstr(aqualink_data.last_message, "SET TIME") != NULL) {
    		// We found SET TIME mode. Set the flag to break out of the
    		// loop.
    		set_time_mode = TRUE;

    		// Enter SET TIME mode.
    		send_cmd("KEY_ENTER");
        	sleep(1);
    	}
    	else {
    		// Next menu item and wait 1 second to give the RS8 time
    		// to respond.
    		send_cmd("KEY_RIGHT");
    		sleep(1);
    	}
    	iterations++;
    }

    // If we are in set time mode, attempt to set the time, else the
    // function just falls through and returns. A warning is logged.
    if(set_time_mode) {

    	while(set_time_mode) {
    		if(strstr(aqualink_data.last_message, TIME_FIELD_TEXT[YEAR]) != NULL) {
    			set_aqualink_time_field(YEAR, year);
    		}
    		else if(strstr(aqualink_data.last_message, TIME_FIELD_TEXT[MONTH]) != NULL) {
    			set_aqualink_time_field(MONTH, month);
    		}
    		else if(strstr(aqualink_data.last_message, TIME_FIELD_TEXT[DAY]) != NULL) {
    			set_aqualink_time_field(DAY, day);
    		}
    		else if(strstr(aqualink_data.last_message, TIME_FIELD_TEXT[HOUR]) != NULL) {
    			set_aqualink_time_field(HOUR, hour);
    		}
    		else if(strstr(aqualink_data.last_message, TIME_FIELD_TEXT[MINUTE]) != NULL) {
    			set_aqualink_time_field(MINUTE, minute);
    		}
    		else {
    			set_time_mode = FALSE;
    		}
    		sleep(1);
    	}
    	log_message(INFO, "ssssssssss", "Aqualink time set to: ",
    			year, "-",
    			month, "-",
    			day, "  ",
    			hour, ":",
    			minute
    	);
    }
    else {
    	log_message(WARNING, "s", "Could not enter SET TIME mode.");
    }

    // Reset the programming state flag.
    PROGRAMMING = FALSE;
    return 0;
}
 
Re: Control your Jandy equipment from your PC with a $15 ada

File #10 - Header file for getting and setting the aqualink's time.

Code:
/*
 * aqualink_time.h
 *
 *  Created on: Sep 22, 2012
 */
#ifndef AQUALINK_TIME_H_
#define AQUALINK_TIME_H_

int check_aqualink_time(char* _date, char* _time);
void* set_aqualink_time(void* arg);

#endif /* AQUALINK_TIME_H_ */
 
Re: Control your Jandy equipment from your PC with a $15 ada

File #11 - Source for the global variables for the project.

Code:
/*
 * globals.c
 *
 *  Created on: Sep 22, 2012
  */

#include "aqualink.h"

struct AQUALINK_DATA aqualink_data;
struct CONFIG_PARAMETERS config_parameters;

int PROGRAMMING = FALSE;
const char* DEVICE_STRINGS[] = {
		"08",
		"09",
		"0a",
		"0b",
		"10",
		"11",
		"12",
		"13",
		"18",
		"19",
		"1a",
		"1b",
		"20",
		"21",
		"22",
		"23"
};

const unsigned char DEVICE_CODES[] = {
		0x08,
		0x09,
		0x0a,
		0x0b,
		0x10,
		0x11,
		0x12,
		0x13,
		0x18,
		0x19,
		0x1a,
		0x1b,
		0x20,
		0x21,
		0x22,
		0x23
};

const char* KEY_CMD_TEXT[] = {
	"KEY_PUMP",
	"KEY_SPA",
	"KEY_AUX1",
	"KEY_AUX2",
	"KEY_AUX3",
	"KEY_AUX4",
	"KEY_AUX5",
	"KEY_AUX6",
	"KEY_AUX7",
	"KEY_HTR_POOL",
	"KEY_HTR_SPA",
	"KEY_HTR_SOLAR",
	"KEY_MENU",
	"KEY_CANCEL",
	"KEY_LEFT",
	"KEY_RIGHT",
	"KEY_HOLD",
	"KEY_OVERRIDE",
	"KEY_ENTER"
};

const unsigned char KEY_CODES[] = {
		0x02,
		0x01,
		0x05,
		0x0a,
		0x0f,
		0x06,
		0x0b,
		0x10,
		0x15,
		0x12,
		0x17,
		0x1c,
		0x09,
		0x0e,
		0x13,
		0x18,
		0x19,
		0x1e,
		0x1d
};

const unsigned char LED_STATES[2][16] = {
		{ 1,    1,    1,    0,    0,    2,    1,    2,    0,    3,    4,    4,    1,    3,    4,    4    },
		{ 0x10, 0x04, 0x01, 0x40, 0x10, 0x01, 0x40, 0x40, 0x01, 0x10, 0x01, 0x10, 0x20, 0x40, 0x04, 0x40 }
};

const char* LED_STATES_TEXT[] = {
		"off",
		"on",
		"enabled"
};

const char ROOT_NOUN[] = { "AqualinkRS8" };

const int LEDS_INDEX = 13;
const char* RS8_NOUNS[] = {
		"version",
		"date",
		"time",
		"last_message",
		"temp_units",
		"air_temp",
		"pool_temp",
		"spa_temp",
		"battery",
		"pool_htr_set_pnt",
		"spa_htr_set_pnt",
		"freeze_protection",
		"frz_protect_set_pnt",
		"leds"
};

const char* LED_NOUNS[] = {
		"pump",
		"spa",
		"aux1",
		"aux2",
		"aux3",
		"aux4",
		"aux5",
		"aux6",
		"aux7",
		"pool_heater",
		"spa_heater",
		"solar_heater"
};

const char* TIME_FIELD_TEXT[] = {
		"YEAR",
		"MONTH",
		"DAY",
		"HOUR",
		"MINUTE"
};

const char* BATTERY_STATUS_TEXT[] = {
		"ok",
		"low"
};

const char* temp_units_strings[] = { "F", "C", "u" };

const char* level_strings[] = { "DEBUG", "INFO", "WARNING", "ERROR" };

const char PROGRAM_NAME[] = "aqualinkd";

unsigned char aqualink_cmd = 0x00;

#define LABEL_LENGTH 32
char aux_function_labels[7][LABEL_LENGTH];

const int NUM_FUNCTION_LABELS = 7;
const char* LABEL_NOUNS[] = {
		"aux1_label",
		"aux2_label",
		"aux3_label",
		"aux4_label",
		"aux5_label",
		"aux6_label",
		"aux7_label",
};
 
Re: Control your Jandy equipment from your PC with a $15 ada

File #12 - Header for the global variables for the project.

Code:
/*
 * globals.h
 *
 *  Created on: Sep 22, 2012
 */

#ifndef GLOBALS_H_
#define GLOBALS_H_

extern struct AQUALINK_DATA aqualink_data;
extern struct CONFIG_PARAMETERS config_parameters;
extern struct FUNCTION_LABELS function_labels;
extern int PROGRAMMING;
extern const char* DEVICE_STRINGS[];
extern const unsigned char DEVICE_CODES[];
extern const char* KEY_CMD_TEXT[];
extern const unsigned char KEY_CODES[];
extern const unsigned char LED_STATES[2][16];
extern const char* LED_STATES_TEXT[];
extern const char ROOT_NOUN[];
extern const int LEDS_INDEX;
extern const char* RS8_NOUNS[];
extern const char* LED_NOUNS[];
extern const char* LABEL_NOUNS[];
extern const int NUM_FUNCTION_LABELS;
extern const char* TIME_FIELD_TEXT[];
extern const char* BATTERY_STATUS_TEXT[];
extern const char* temp_units_strings[];
extern const char* level_strings[];
extern const char PROGRAM_NAME[];
extern unsigned char aqualink_cmd;

#define LABEL_LENGTH 32
extern char aux_function_labels[][LABEL_LENGTH];

enum {
	AUX1_LABEL = 0,
	AUX2_LABEL,
	AUX3_LABEL,
	AUX4_LABEL,
	AUX5_LABEL,
	AUX6_LABEL,
	AUX7_LABEL,
};

void log_message(int level, const char* format, ...);
int send_cmd(char* cmd);
void daemon_shutdown();


#endif /* GLOBALS_H_ */
 
Re: Control your Jandy equipment from your PC with a $15 ada

File #13 - Source file for the both private and system logging.

Code:
/*
 * logging.c
 *
 *  Created on: Sep 29, 2012
  */

#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <time.h>
#include <syslog.h>

#include "globals.h"

const int MAX_MESSAGE_LENGTH = 256;
const char LOG_FILE[] = {"aqualinkd.log"};

char log_filename[256];
int log_level;

void set_logging_parameters(char* running_directory, int level)
{
	strcpy(log_filename, running_directory);
	strcat(log_filename, "/log/");
	strcat(log_filename, LOG_FILE);

	log_level = level;
}


// Get a timestamp with the current local time.
// time_string - the string to put the timestamp in
//
void timestamp(char* time_string)
{
    time_t now;
    struct tm *tmptr;

    time(&now);
    tmptr = localtime(&now);
    strftime(time_string, MAX_MESSAGE_LENGTH, "%b-%d-%y %H:%M:%S %p ", tmptr);
}


// Logs a message to a syslog file building the message string
// from a variable list of arguments.
//
// int level - the log level (LOG_DEBUG, LOG_INFO, LOG_NOTICE, LOG_WARNING, LOG_ERR)
// const char* format - the format string for the remaining arguments:
//                     (s = string, i = integer, f = float or double)
// ... - a variable list of arguments (can be string, int, or float)
void log_to_syslog(int level, const char* format, ...)
{
	char message_buffer[1024];
	char temp_buffer[1024];


	va_list arguments;
	va_start(arguments, format);

	strcpy(message_buffer, "");
	int i;
	for(i=0; format[i]!='\0'; i++) {
        if(format[i] == 'f')
        {
              double FArg = va_arg(arguments, double);
              sprintf(temp_buffer, "%.3lf", FArg);
              strcat(message_buffer, temp_buffer);
        }
        else if(format[i] == 'i')
        {
              int IArg=va_arg(arguments, int);
              sprintf(temp_buffer, "%d", IArg);
              strcat(message_buffer, temp_buffer);
        }
        else if(format[i] == 's') {
        	char *str = va_arg (arguments, char *);
            sprintf(temp_buffer, "%s", str);
            strcat(message_buffer, temp_buffer);
        }
        else {
        	sprintf(message_buffer, "Invalid argument type for log message...");
        }
	}
    va_end(arguments);

	openlog(PROGRAM_NAME,  LOG_CONS | LOG_PID | LOG_NDELAY, LOG_LOCAL1);

	syslog(level, message_buffer);

	closelog();
}

// Logs a message to a private log file building the message string
// from a variable list of arguments.
//
// int level - the log level (DEBUG, INFO, WARNING, ERROR
// const char* format - the format string for the remaining arguments:
//                     (s = string, i = integer, f = float or double)
// ... - a variable list of arguments (can be string, int, or float)
void log_message(int level, const char* format, ...)
{
	if (level >= log_level) {
		// At or above the configured log level. Process the message.
		char message_buffer[1024];
		char temp_buffer[1024];

		va_list arguments;
		va_start(arguments, format);

		strcpy(message_buffer, "");
		int i;
		for(i=0; format[i]!='\0'; i++) {
			if(format[i] == 'f')
			{
				double FArg = va_arg(arguments, double);
				sprintf(temp_buffer, "%.3lf", FArg);
				strcat(message_buffer, temp_buffer);
			}
			else if(format[i] == 'i')
			{
				int IArg=va_arg(arguments, int);
				sprintf(temp_buffer, "%d", IArg);
				strcat(message_buffer, temp_buffer);
			}
			else if(format[i] == 's') {
				char *str = va_arg (arguments, char *);
				sprintf(temp_buffer, "%s", str);
				strcat(message_buffer, temp_buffer);
			}
	        else {
	        	sprintf(message_buffer, "Invalid argument type for log message...");
	        }
		}
		va_end(arguments);


		char time_string[MAX_MESSAGE_LENGTH];
		FILE *logfile;

		// Attempt to open the private log file.
		logfile = fopen(log_filename, "a");
		if(!logfile) {
			// Couldn't open the log file. Log log filename to system log and return.
			log_to_syslog(LOG_ERR, "ss", "Couldn't open private log file:  ", log_filename);
		}
		else {
			// Else the log file was opened successfully. Get the timestamp, and log the message.
			timestamp(time_string);
			fprintf(logfile,"%s- %s: %s\n", time_string, level_strings[level], message_buffer);
			fclose(logfile);
		}
	}
}
 
Re: Control your Jandy equipment from your PC with a $15 ada

File #14 - Header file for the both private and system logging.

Code:
/*
 * logging.h
 *
 *  Created on: Sep 29, 2012
 */

#ifndef LOGGING_H_
#define LOGGING_H_

// FUNCTION PROTOTYPES
void timestamp(char* time_string);
void log_to_syslog(int level, const char* format, ...);
void log_message(int level, const char* format, ...);
void set_logging_parameters(char* running_directory, int level);

#endif /* LOGGING_H_ */
 
Re: Control your Jandy equipment from your PC with a $15 ada

File #15 - Source for the mini web server. Pretty sure there is a memory leak in here somewhere. I haven't had time to track it down. Never lasts more than a couple of days of continuous operation. If you find the problem before I do please post it. I will post the solution if and when I find it.

Code:
/*
 * web_server.c
 *
 *  Created on: Sep 29, 2012
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <syslog.h>
#include <netinet/in.h>     //
#include <sys/socket.h>     // for socket system calls
#include <arpa/inet.h>      // for socket system calls (bind)
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

#include "aqualink.h"
#include "globals.h"
#include "logging.h"

// HTTP Status codes
const char HTTP_STATUS_CODE_400[] = { "HTTP/1.0 400 Bad Request\nContent-Type:text/html\n\n" };
const char HTTP_STATUS_MESSAGE_400_INVALID_VERB[] = { "<html><body><h1>INVALID HTTP VERB</h1></body></html>" };
const char HTTP_STATUS_MESSAGE_400_MISSING_HEADER[] = { "<html><body><h1>A required HTTP header was not specified.</h1></body></html>" };
const char HTTP_STATUS_CODE_404[] = { "HTTP/1.0 404 Not Found\nContent-Type:text/html\n\n" };
const char HTTP_STATUS_MESSAGE_404[] = { "<html><body><h1>The specified resource does not exist.</h1></body></html>" };
const char HTTP_STATUS_CODE_405[] = { "HTTP/1.0 405 Method Not Allowed\nContent-Type:text/html\n\n" };
const char HTTP_STATUS_MESSAGE_405_UNSUPPORTED_VERB[] = { "<html><body><h1>The resource doesn't support the specified HTTP verb.</h1></body></html>" };
const char HTTP_STATUS_CODE_415[] = { "HTTP/1.0 415 Unsupported Content-Type\nContent-Type:text/html\n\n" };
const char HTTP_STATUS_MESSAGE_415_UNSUPPORTED_CONTENT_TYPE[] = { "<html><body><h1>The resource doesn't support the specified Content-Type.</h1></body></html>" };
const char HTTP_STATUS_CODE_200_JSON[] = { "HTTP/1.0 200 OK\nContent-Type:application/json\n\n" };
const char HTTP_STATUS_CODE_200_TEXT[] = { "HTTP/1.0 200 OK\nContent-Type:text/html\n\n" };
const char HTTP_STATUS_CODE_200_IMAGE[] = { "HTTP/1.0 200 OK\nContent-Type:image/gif\n\n" };

char rs8_data[NUM_RS8_NOUNS][MSGLONGLEN];
char led_data[NUM_LED_NOUNS][MAX_LED_STAT_LENGTH];


char* build_aux_labels_JSON(char* json_buffer)
{
	char _buffer[256];

	strcpy(json_buffer, "{");

	int i;
	for(i=0; i<NUM_FUNCTION_LABELS; i++) {
		if(i != 0) {
			strcat(json_buffer, ",");
		}
		sprintf(_buffer, "\"%s\": \"%s\"", LABEL_NOUNS[i], aux_function_labels[i]);
		strcat(json_buffer, _buffer);
	}

	strcat(json_buffer, "}");

	return json_buffer;

}


int get_led_status(int led_id)
{
	int status = OFF;

	switch(led_id) {
	case STAT_PUMP:
		if((aqualink_data.status[LED_STATES[STAT_BYTE][led_id]] & LED_STATES[STAT_MASK][led_id]) != 0) {
			status = ON;
		}
		else if(aqualink_data.status[LED_STATES[STAT_BYTE][led_id+12]] & LED_STATES[STAT_MASK][led_id+12]) {
			status = ENABLED;
		}
		break;
	case STAT_HTR_POOL_ON:
	case STAT_HTR_SPA_ON:
	case STAT_HTR_SOLAR_ON:
		if((aqualink_data.status[LED_STATES[STAT_BYTE][led_id]] & LED_STATES[STAT_MASK][led_id]) != 0) {
			status = ON;
		}
		else if(aqualink_data.status[LED_STATES[STAT_BYTE][led_id+4]] & LED_STATES[STAT_MASK][led_id+4]) {
			status = ENABLED;
		}
		break;
	case STAT_SPA:
	case STAT_AUX1:
	case STAT_AUX2:
	case STAT_AUX3:
	case STAT_AUX4:
	case STAT_AUX5:
	case STAT_AUX6:
	case STAT_AUX7:
		if((aqualink_data.status[LED_STATES[STAT_BYTE][led_id]] & LED_STATES[STAT_MASK][led_id]) != 0) {
			status = ON;
		}
		break;
	default:
		break;
	}

	return status;
}


char* build_leds_JSON(char* buffer)
{
	char _buffer[64];

	strcpy(buffer, "{");

	int i;
	for(i=0; i<NUM_LED_NOUNS; i++) {
		if(i != 0) {
			strcat(buffer, ",");
		}
		sprintf(_buffer, "\"%s\": \"%s\"", LED_NOUNS[i], led_data[i]);
		strcat(buffer, _buffer);
	}

	strcat(buffer, "}");

	return buffer;
}


char* build_RS8_JSON(char* buffer)
{
	char _buffer[256];

	strcpy(buffer, "{");

	int i;
	for(i=0; i<NUM_RS8_NOUNS-1; i++) {
		if(i != 0) {
			strcat(buffer, ",");
		}
		sprintf(_buffer, "\"%s\": \"%s\"", RS8_NOUNS[i], rs8_data[i]);
		strcat(buffer, _buffer);
	}

	sprintf(_buffer, ",\"%s\": ", RS8_NOUNS[LEDS_INDEX]);
	strcat(buffer, _buffer);

	strcat(buffer, build_leds_JSON(_buffer));

	strcat(buffer, "}");

	return buffer;
}

int build_resource_JSON(char* json_buffer, char* uri)
{
	int valid = FALSE;

	// Copy the URI before breaking it into tokens. strtok()
	// breaks the original string for further use in some way.
	char _uri[1024];
	strcpy(_uri, uri);

	char* level1;
	char* level2;
	char* level3;
	char* level4;

	// Parse the URI.
	level1 = strtok(_uri, "/");
	level2 = strtok(NULL, "/");
	level3 = strtok(NULL, "/");
	level4 = strtok(NULL, "/");

	if(level4 == NULL) {
		// Level 4 is empty. That's good because there should be no
		// level 4.

		if(level1 != NULL) {
			if(strcmp(level1, ROOT_NOUN) == 0) {
				// Level 1 tested good.
				if(level2 != NULL) {
					// Iterate through RS8 nouns to validate level 2.
					int i;
					for(i=0; i<NUM_RS8_NOUNS; i++) {
						if(strcmp(level2, RS8_NOUNS[i]) == 0) {
							if(strcmp(level2, RS8_NOUNS[LEDS_INDEX]) == 0) {
								if(level3 != NULL) {
									// Level 2 is LEDS. Validate level 3.
									int j;
									for(j=0; j<NUM_LED_NOUNS; j++) {
										if(strcmp(level3, LED_NOUNS[j]) == 0) {
											valid = TRUE;
											sprintf(json_buffer, "{\"%s\": {\"%s\": \"%s\"}",
													RS8_NOUNS[LEDS_INDEX],
													LED_NOUNS[j],
													led_data[j]);
											break;
										}
									}
								}
								else {
									char _buffer[256];
									// do all leds
									valid = TRUE;
									strcpy(json_buffer, build_leds_JSON(_buffer));
									break;
								}
							}
							else {
								// Got a valid level 2 resource. Build the JSON
								// for it.
								valid = TRUE;
								sprintf(json_buffer, "{\"%s\": \"%s\"}",
										RS8_NOUNS[i],
										rs8_data[i]);
								break;
							}
						}
					}
				}
				else {
					valid = TRUE;
					build_RS8_JSON(json_buffer);
				}
			}
			else if(strcmp(level1, "Config") == 0) {
				if(strcmp(level2, "aux_labels") == 0) {
					valid = TRUE;
					build_aux_labels_JSON(json_buffer);
				}
			}
		}
	}

	return valid;
}


int send_REST_response(char* uri, unsigned int client_socket)
{
	int num_bytes = 0;
	int resource_found = FALSE;
	char msg_buffer[1024];
	char json_msg_buffer[1024];

	resource_found = build_resource_JSON(json_msg_buffer, uri);

	if(resource_found) {
		strcpy(msg_buffer, HTTP_STATUS_CODE_200_JSON);
		num_bytes = write(client_socket, msg_buffer, strlen(msg_buffer));

		strcpy(msg_buffer, json_msg_buffer);
		num_bytes = write(client_socket, msg_buffer, strlen(msg_buffer));
	}

	return resource_found;
}

// Convert the raw Aqualink data into strings that can be built
// into a JSON string.
void convert_aqualink_data()
{
	log_message(DEBUG, "s", "convert_aqualink_data()...");

	if(strlen(aqualink_data.version) + 1 > MSGLONGLEN) {
		log_message(WARNING, "ss", "Version string too long: ", aqualink_data.version);
	}
	else {
		strcpy(rs8_data[VERSION_NOUN], aqualink_data.version);
	}

	if(strlen(aqualink_data.date) + 1 > MSGLONGLEN) {
		log_message(WARNING, "ss", "Date string too long: ", aqualink_data.date);
	}
	else {
		strcpy(rs8_data[DATE_NOUN], aqualink_data.date);
	}

	if(strlen(aqualink_data.time) + 1 > MSGLONGLEN) {
		log_message(WARNING, "ss", "Time string too long: ", aqualink_data.time);
	}
	else {
		strcpy(rs8_data[TIME_NOUN], aqualink_data.time);
	}

	if(strlen(aqualink_data.last_message) + 1 > MSGLONGLEN) {
		log_message(WARNING, "ss", "Last Message string too long: ", aqualink_data.last_message);
	}
	else {
		strcpy(rs8_data[LAST_MESSAGE_NOUN], aqualink_data.last_message);
	}

	if(aqualink_data.temp_units >= FAHRENHEIT && aqualink_data.temp_units <= UNKNOWN) {
		strcpy(rs8_data[TEMP_UNITS_NOUN], temp_units_strings[aqualink_data.temp_units]);
	}
	else {
		log_message(WARNING, "sis", "Temperature units value, ", aqualink_data.temp_units, " invalid.");
	}

	sprintf(rs8_data[AIR_TEMP_NOUN], "%d", aqualink_data.air_temp);
	if(aqualink_data.pool_temp == -999) {
		sprintf(rs8_data[POOL_TEMP_NOUN], "%s", " ");
	}
	else {
		sprintf(rs8_data[POOL_TEMP_NOUN], "%d", aqualink_data.pool_temp);
	}
	if(aqualink_data.spa_temp == -999) {
		sprintf(rs8_data[SPA_TEMP_NOUN], "%s", " ");
	}
	else {
		sprintf(rs8_data[SPA_TEMP_NOUN], "%d", aqualink_data.spa_temp);
	}

	if(aqualink_data.battery >= OK && aqualink_data.battery <= LOW) {
		strcpy(rs8_data[BATTERY_NOUN], BATTERY_STATUS_TEXT[aqualink_data.battery]);
	}
	else {
		log_message(WARNING, "sis", "Battery Status value, ", aqualink_data.battery, " invalid.");
	}

	int i;
	for(i=0; i<NUM_LED_NOUNS; i++) {
		int state_value = get_led_status(i);
		if(state_value >= OFF && state_value <= ENABLED) {
			strcpy(led_data[i], LED_STATES_TEXT[state_value]);
		}
		else {
			log_message(WARNING, "sis", "LED State value, ", state_value, " invalid.");
		}
	}

	sprintf(rs8_data[POOL_HTR_TEMP_NOUN], "%d", aqualink_data.pool_htr_set_point);
	sprintf(rs8_data[SPA_HTR_TEMP_NOUN], "%d", aqualink_data.spa_htr_set_point);
	sprintf(rs8_data[FRZ_PROTECT_TEMP_NOUN], "%d", aqualink_data.frz_protect_set_point);

	if(aqualink_data.freeze_protection >= OFF  &&  aqualink_data.freeze_protection <= ENABLED) {
		strcpy(rs8_data[FRZ_PROTECT_NOUN],  LED_STATES_TEXT[aqualink_data.freeze_protection]);
	}
	else {
		log_message(WARNING, "sis", "Freeze Protection State value, ", aqualink_data.freeze_protection, " invalid.");
	}

}


int download_file(char* uri, unsigned int client_socket)
{
	log_message(DEBUG, "s", "download_file()...");
	int resource_not_found = TRUE;
	const int BUF_SIZE = 1024;
	char           out_buffer[BUF_SIZE];              // Output buffer for HTML response
	char           file_name[BUF_SIZE];               // File name
	unsigned int   fh;                            // File handle (file descriptor)
	unsigned int   buffer_length;                 // Buffer length for file reads

	log_message(DEBUG, "ssss", "RD: ", config_parameters.running_directory, "  -   URI: ", uri);

	// Build the filename of the file to download.
	strcpy(file_name, config_parameters.running_directory);
	strcat(file_name, uri);

	// Open the requested file.
	fh = open(file_name, O_RDONLY, S_IREAD | S_IWRITE);

	if(fh != -1) {
		log_message(DEBUG, "sss", "File ", file_name, " is being sent");

		// Select the correct response header based on the file type.
		if ((strstr(file_name, ".jpg") != NULL) ||
				(strstr(file_name, ".gif") != NULL) ||
				(strstr(file_name, ".png") != NULL)) {
			// Image files
			strcpy(out_buffer, HTTP_STATUS_CODE_200_IMAGE);
		}
		else {
			// Text files
			strcpy(out_buffer, HTTP_STATUS_CODE_200_TEXT);
		}

		// Send the response header.
		send(client_socket, out_buffer, strlen(out_buffer), 0);

		// Send the file.
		buffer_length = 1;
		while (buffer_length > 0)
		{
			buffer_length = read(fh, out_buffer, BUF_SIZE);
			if (buffer_length > 0)
			{
				send(client_socket, out_buffer, buffer_length, 0);
			}
		}

		// Set the the resource_not_found flag to FALSE, and
		// close the file.
		resource_not_found = FALSE;
		close(fh);
	}
	else {
		log_message(ERROR, "sss", "File ", file_name, " not found");
	}

	return resource_not_found;
}


int handle_http_get(char* uri, unsigned int client_socket)
{
	log_message(DEBUG, "s", "handle_http_get()...");
	int resource_not_found = TRUE;
	int num_bytes = 0;
	char msg_buffer[1024];

	log_message(DEBUG, "s", uri);

	convert_aqualink_data();

	// Process the URI.
	if(send_REST_response(uri, client_socket)) {
		resource_not_found = FALSE;
	}
	else {
		// The resource wasn't found. Check if a file is being requested.
		// Note files are transferred to download the web application that
		// reads and displays the JSON resources, and sends commands to the
		// Aqualink RS8.
		resource_not_found = download_file(uri, client_socket);
	}

	// Respond to the request.
	if(resource_not_found) {
		log_message(DEBUG, "s", "GET not found...");
		strcpy(msg_buffer, HTTP_STATUS_CODE_404);
		num_bytes = write(client_socket, msg_buffer, strlen(msg_buffer));

		strcpy(msg_buffer, HTTP_STATUS_MESSAGE_404);
		num_bytes = write(client_socket, msg_buffer, strlen(msg_buffer));
	}

	return num_bytes;
}


int send_cmd(char* cmd)
{
	int i;
	int not_found = TRUE;

	// Iterate through the valid command keys, and look for a match.
	for(i=0; i<NUM_KEYS; i++) {
		if(strstr(cmd, KEY_CMD_TEXT[i]) != NULL) {
			// Found a match to a valid command. Set the command
			// variable.
			not_found = FALSE;
			aqualink_cmd = KEY_CODES[i];
			log_message(DEBUG, "ss", KEY_CMD_TEXT[i], " command sent");
			break;
		}
	}

	return not_found;
}


int handle_http_post(char* uri, char* buffer, unsigned int client_socket)
{
	char* level1;
	char* level2;
	char* level3;

	int resource_not_found = TRUE;
	int wrong_mime_type = TRUE;
	int num_bytes = 0;
	char msg_buffer[1024];

	const char success_message[] = { "{\"command\":\"successful\"}" };

	log_message(DEBUG, "s", uri);

	// Parse the URI.
	level1 = strtok(uri, "/");
	level2 = strtok(NULL, "/");
	level3 = strtok(NULL, "/");

	if(level3 == NULL) {
		if((strcmp(level1, ROOT_NOUN) == 0) && (strcmp(level2, "command") == 0)) {
			char* token = strtok(buffer, "\n");
			while((token = strtok(NULL, "\n")) != NULL) {
				log_message(DEBUG, "s", token);
				if(strstr(token, "Content-Type") != NULL) {
					if(strstr(token, "application/json") != NULL) {
						wrong_mime_type = FALSE;
					}
					else {
						log_message(WARNING, "ss", "Wrong mime type: ", token);
						break;
					}
				}
				else if(strstr(token, "command") != NULL) {
					if(wrong_mime_type == FALSE) {
						char* cmd;
						char* cmd_n;

						cmd_n = strtok(token, "{\": }");
						cmd = strtok(NULL, "{\": }");

						log_message(DEBUG, "ssss",  "cmd_n: ", cmd_n, ", cmd: ", cmd);
						if(strcmp(cmd_n, "command") == 0) {
							resource_not_found = send_cmd(cmd);
							if(resource_not_found) {
								log_message(WARNING, "ss", "Resource not found: ", cmd);
							}
							break;
						}
					}
				}
			}
		}
	}

	// Respond to the request.
	if(wrong_mime_type) {
		strcpy(msg_buffer, HTTP_STATUS_CODE_415);
		num_bytes = write(client_socket, msg_buffer, strlen(msg_buffer));

		strcpy(msg_buffer, HTTP_STATUS_MESSAGE_415_UNSUPPORTED_CONTENT_TYPE);
		num_bytes = write(client_socket, msg_buffer, strlen(msg_buffer));
	}
	else if(resource_not_found) {
		log_message(DEBUG, "s", "POST resource not found...");
		strcpy(msg_buffer, HTTP_STATUS_CODE_404);
		num_bytes = write(client_socket, msg_buffer, strlen(msg_buffer));

		strcpy(msg_buffer, HTTP_STATUS_MESSAGE_404);
		num_bytes = write(client_socket, msg_buffer, strlen(msg_buffer));
	}
	else {
		strcpy(msg_buffer, HTTP_STATUS_CODE_200_JSON);
		num_bytes = write(client_socket, msg_buffer, strlen(msg_buffer));
		num_bytes = write(client_socket, success_message, strlen(success_message));
	}

	return num_bytes;
}


void* handle_socket_request(void* arg)
{
	log_message(DEBUG, "s", "handle_socket_request()...");
	const int BUFFER_LENGTH = 1024;
	const int VERB_LENGTH = 32;
	const int URI_LENGTH = 256;
	char buffer[BUFFER_LENGTH];
	char msg_buffer[1024];
	int num_bytes;

	unsigned int client_socket = *(unsigned int *)arg;

	// Initialize the buffer to all 0's. Clears any garbage, and
	// ensures it is cleaned up after the last socket request.
	memset(buffer, 0, BUFFER_LENGTH);

	// Detach the thread so that it cleans up as it exits. Memory leak without it.
	pthread_detach(pthread_self());

	// Read the request.
	num_bytes = read(client_socket, buffer, BUFFER_LENGTH);
	if(num_bytes < 0) {
		// Got an error reading the request. Log it and fall through.
		log_message(ERROR, "s", "error reading from socket.");
	}
	else {
		char http_verb[VERB_LENGTH];
		char uri[URI_LENGTH];
		char misc[BUFFER_LENGTH];

		// Got the request. Parse it.
		log_message(DEBUG, "s", buffer);
		sscanf(buffer, "%s %s %s", http_verb, uri, misc);

		// Process the request.
		if(strstr(misc, "HTTP") == NULL) {
			// Unrecognized HTTP message. Send error response.
			strcpy(msg_buffer, HTTP_STATUS_CODE_400);
			num_bytes = write(client_socket, msg_buffer, strlen(msg_buffer));

			strcpy(msg_buffer, HTTP_STATUS_MESSAGE_400_MISSING_HEADER);
			num_bytes = write(client_socket, msg_buffer, strlen(msg_buffer));
		}
		else if(strcmp(http_verb, "GET") == 0) {
			// Handle the HTTP GET request.
			num_bytes = handle_http_get(uri, client_socket);
		}
		else if(strcmp(http_verb, "POST") == 0) {
			// Handle the HTTP PUT request.
			num_bytes = handle_http_post(uri, buffer, client_socket);
		}
		else {
			// Unsupported HTTP verb. Send error response.
			strcpy(msg_buffer, HTTP_STATUS_CODE_405);
			num_bytes = write(client_socket, msg_buffer, strlen(msg_buffer));

			strcpy(msg_buffer, HTTP_STATUS_MESSAGE_405_UNSUPPORTED_VERB);
			num_bytes = write(client_socket, msg_buffer, strlen(msg_buffer));
		}

	}

	if(num_bytes < 0) {
		log_message(ERROR, "s", "error writing to socket.");
	}

	// Close the socket.
	close(client_socket);

	pthread_exit((void*)0);
	//return 0;
}


void* socket_server(void* arg)
{
	unsigned int          server_socket;          // Server socket descriptor
	struct sockaddr_in    server_addr;            // Server Internet address
	struct sockaddr_in    client_addr;            // Client Internet address
	unsigned int          addr_len;               // Internet address length

	unsigned int          client_s;               // holds thread args
	pthread_attr_t        attr;                   // pthread attributes
	pthread_t             threads;                // Thread ID (used by OS)

	// Create the socket.
	if((server_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
		// Can't open stream socket. Log it and set the RUNNING flag to false
		// to force the primary thread to exit, as well.
		log_to_syslog(LOG_ERR, "s", "server: can't open stream socket, exiting thread.");
		daemon_shutdown();
		return 0;
	}

	// Fill in address information, and then bind it.
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(config_parameters.socket_port);
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	if(bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
		// Can't bind the local address. Log it and set the RUNNING flag to false
		// to force the primary thread to exit, as well.
		log_to_syslog(LOG_ERR, "s", "server: can't bind local address, exiting thread.");
		daemon_shutdown();
		return 0;
	}

	// Listen for connections and then accept. Hold 100 pending connections.
	listen(server_socket, 100);

	// Socket ready, log it.
	log_to_syslog(LOG_NOTICE, "sis", "Server listening on port: ", config_parameters.socket_port, " ...");

	// The main socket server loop.
	pthread_attr_init(&attr);
	while(TRUE) {
		addr_len = sizeof(client_addr);
		client_s = accept(server_socket, (struct sockaddr *)&client_addr, &addr_len);

		log_message(DEBUG, "s", "new client connection request...");

		if (client_s == FALSE) {
			log_to_syslog(LOG_ERR, "s", "Unable to create socket");
			return 0;
		}
		else {
			// Create a child thread.
			pthread_create(&threads, &attr, &handle_socket_request, &client_s);
		}
	}

	// Close the primary socket
	close (server_socket);

	return 0;
}
 

Enjoying this content?

Support TFP with a donation.

Give Support
Thread Status
Hello , This thread has been inactive for over 60 days. New postings here are unlikely to be seen or responded to by other members. For better visibility, consider Starting A New Thread.