/* * Copyright (c) 2017 Shaun Feakes - All rights reserved * * You may use redistribute and/or modify this code under the terms of * the GNU General Public License version 2 as published by the * Free Software Foundation. For the terms of this license, * see . * * You are free to use this software under the terms of the GNU General * Public License, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * https://github.com/sfeakes/aqualinkd */ #define _GNU_SOURCE 1 // for strcasestr & strptime #include #include #include #include #include "aqualink.h" #include "init_buttons.h" #include "pda.h" #include "pda_menu.h" #include "utils.h" // static struct aqualinkdata _aqualink_data; static struct aqualinkdata *_aqualink_data; static unsigned char _last_packet_type; static unsigned long _pda_loop_cnt = 0; static bool _initWithRS = false; // Each RS message is around 0.25 seconds apart #define PDA_SLEEP_FOR 120 // 30 seconds #define PDA_WAKE_FOR 6 // ~1 seconds #ifdef BETA_PDA_AUTOLABEL static struct aqconfig *_aqualink_config; void init_pda(struct aqualinkdata *aqdata, struct aqconfig *aqconfig) { _aqualink_data = aqdata; _aqualink_config = aqconfig; set_pda_mode(true); } #else void init_pda(struct aqualinkdata *aqdata) { _aqualink_data = aqdata; set_pda_mode(true); } #endif bool pda_shouldSleep() { //logMessage(LOG_DEBUG, "PDA loop count %d, will sleep at %d\n",_pda_loop_cnt,PDA_LOOP_COUNT); if (_pda_loop_cnt++ < PDA_WAKE_FOR) { return false; } else if (_pda_loop_cnt > PDA_WAKE_FOR + PDA_SLEEP_FOR) { _pda_loop_cnt = 0; return false; } // NSF NEED TO CHECK ACTIVE THREADS. if (_aqualink_data->active_thread.thread_id != 0) { logMessage(LOG_DEBUG, "PDA can't sleep as thread %d,%p is active", _aqualink_data->active_thread.ptype, _aqualink_data->active_thread.thread_id); _pda_loop_cnt = 0; return false; } // Last see if there are any open websockets. (don't sleep if the web UI is open) if ( _aqualink_data->open_websockets > 0 ) { logMessage(LOG_DEBUG, "PDA can't sleep as websocket is active"); return false; } return true; } /* bool pda_shouldSleep() { //logMessage(LOG_DEBUG, "PDA loop count %d, will sleep at %d\n",_pda_loop_cnt,PDA_LOOP_COUNT); if (_pda_loop_cnt++ < PDA_LOOP_COUNT) { return false; } else if (_pda_loop_cnt > PDA_LOOP_COUNT*2) { _pda_loop_cnt = 0; return false; } return true; } */ void pda_wake() { pda_reset_sleep(); // Add and specic code to run when wake is called. } void pda_reset_sleep() { _pda_loop_cnt = 0; } unsigned char get_last_pda_packet_type() { return _last_packet_type; } void set_pda_led(struct aqualinkled *led, char state) { aqledstate old_state = led->state; if (state == 'N') { led->state = ON; } else if (state == 'A') { led->state = ENABLE; } else if (state == '*') { led->state = FLASH; } else { led->state = OFF; } if (old_state != led->state) { logMessage(LOG_DEBUG, "set_pda_led from %d to %d\n", old_state, led->state); } } void pass_pda_equiptment_status_item(char *msg) { static char *index; int i; // EQUIPMENT STATUS // // AquaPure 100% // SALT 25500 PPM // FILTER PUMP // POOL HEAT // SPA HEAT ENA // EQUIPMENT STATUS // // FREEZE PROTECT // AquaPure 100% // SALT 25500 PPM // CHECK AquaPure // GENERAL FAULT // FILTER PUMP // CLEANER // // Equipment Status // // Intelliflo VS 1 // RPM: 1700 // Watts: 367 // // // // // // Check message for status of device // Loop through all buttons and match the PDA text. if ((index = strcasestr(msg, "CHECK AquaPure")) != NULL) { logMessage(LOG_DEBUG, "CHECK AquaPure\n"); } else if ((index = strcasestr(msg, "FREEZE PROTECT")) != NULL) { _aqualink_data->frz_protect_state = ON; } else if ((index = strcasestr(msg, MSG_SWG_PCT)) != NULL) { _aqualink_data->swg_percent = atoi(index + strlen(MSG_SWG_PCT)); if (_aqualink_data->ar_swg_status == SWG_STATUS_OFF) {_aqualink_data->ar_swg_status = SWG_STATUS_ON;} logMessage(LOG_DEBUG, "AquaPure = %d\n", _aqualink_data->swg_percent); } else if ((index = strcasestr(msg, MSG_SWG_PPM)) != NULL) { _aqualink_data->swg_ppm = atoi(index + strlen(MSG_SWG_PPM)); if (_aqualink_data->ar_swg_status == SWG_STATUS_OFF) {_aqualink_data->ar_swg_status = SWG_STATUS_ON;} logMessage(LOG_DEBUG, "SALT = %d\n", _aqualink_data->swg_ppm); } else if ((index = strcasestr(msg, MSG_PMP_RPM)) != NULL) { // Default to pump 0, should check for correct pump _aqualink_data->pumps[0].rpm = atoi(index + strlen(MSG_PMP_RPM)); logMessage(LOG_DEBUG, "RPM = %d\n", _aqualink_data->pumps[0].rpm); } else if ((index = strcasestr(msg, MSG_PMP_WAT)) != NULL) { // Default to pump 0, should check for correct pump _aqualink_data->pumps[0].watts = atoi(index + strlen(MSG_PMP_WAT)); logMessage(LOG_DEBUG, "Watts = %d\n", _aqualink_data->pumps[0].watts); } else { char labelBuff[AQ_MSGLEN + 1]; strncpy(labelBuff, msg, AQ_MSGLEN + 1); msg = stripwhitespace(labelBuff); if (strcasecmp(msg, "POOL HEAT ENA") == 0) { _aqualink_data->aqbuttons[POOL_HEAT_INDEX].led->state = ENABLE; } else if (strcasecmp(msg, "SPA HEAT ENA") == 0) { _aqualink_data->aqbuttons[SPA_HEAT_INDEX].led->state = ENABLE; } else { for (i = 0; i < TOTAL_BUTTONS; i++) { if (strcasecmp(msg, _aqualink_data->aqbuttons[i].pda_label) == 0) { logMessage(LOG_DEBUG, "*** Found Status for %s = '%.*s'\n", _aqualink_data->aqbuttons[i].pda_label, AQ_MSGLEN, msg); // It's on (or delayed) if it's listed here. if (_aqualink_data->aqbuttons[i].led->state != FLASH) { _aqualink_data->aqbuttons[i].led->state = ON; } break; } } } } } void process_pda_packet_msg_long_temp(const char *msg) { // 'AIR POOL' // ' 86` 86` ' // 'AIR SPA ' // ' 86` 86` ' // 'AIR ' // ' 86` ' // 'AIR WATER' // In case of single device. _aqualink_data->temp_units = FAHRENHEIT; // Force FAHRENHEIT if (stristr(pda_m_line(1), "AIR") != NULL) _aqualink_data->air_temp = atoi(msg); if (stristr(pda_m_line(1), "SPA") != NULL) { _aqualink_data->spa_temp = atoi(msg + 4); _aqualink_data->pool_temp = TEMP_UNKNOWN; } else if (stristr(pda_m_line(1), "POOL") != NULL) { _aqualink_data->pool_temp = atoi(msg + 7); _aqualink_data->spa_temp = TEMP_UNKNOWN; } else if (stristr(pda_m_line(1), "WATER") != NULL) { _aqualink_data->pool_temp = atoi(msg + 7); _aqualink_data->spa_temp = TEMP_UNKNOWN; } else { _aqualink_data->pool_temp = TEMP_UNKNOWN; _aqualink_data->spa_temp = TEMP_UNKNOWN; } // printf("Air Temp = %d | Water Temp = %d\n",atoi(msg),atoi(msg+7)); } void process_pda_packet_msg_long_time(const char *msg) { // message " SAT 8:46AM " // " SAT 10:29AM" // " SAT 4:23PM " // printf("TIME = '%.*s'\n",AQ_MSGLEN,msg ); // printf("TIME = '%c'\n",msg[AQ_MSGLEN-1] ); if (msg[AQ_MSGLEN - 1] == ' ') { strncpy(_aqualink_data->time, msg + 9, 6); } else { strncpy(_aqualink_data->time, msg + 9, 7); } strncpy(_aqualink_data->date, msg + 5, 3); // :TODO: NSF Come back and change the above to correctly check date and time in future } void process_pda_packet_msg_long_equipment_control(const char *msg) { // These are listed as "FILTER PUMP OFF" int i; char labelBuff[AQ_MSGLEN + 1]; strncpy(labelBuff, msg, AQ_MSGLEN - 4); labelBuff[AQ_MSGLEN - 4] = 0; logMessage(LOG_DEBUG, "*** Checking Equiptment '%s'\n", labelBuff); for (i = 0; i < TOTAL_BUTTONS; i++) { if (strcasecmp(stripwhitespace(labelBuff), _aqualink_data->aqbuttons[i].pda_label) == 0) { logMessage(LOG_DEBUG, "*** Found EQ CTL Status for %s = '%.*s'\n", _aqualink_data->aqbuttons[i].pda_label, AQ_MSGLEN, msg); set_pda_led(_aqualink_data->aqbuttons[i].led, msg[AQ_MSGLEN - 1]); } } // Force SWG off if pump is off. if (_aqualink_data->aqbuttons[0].led->state == OFF ) _aqualink_data->ar_swg_status = SWG_STATUS_OFF; // NSF I think we need to check TEMP1 and TEMP2 and set Pool HEater and Spa heater directly, to support single device. if (_aqualink_data->single_device){ if (strcasecmp(stripwhitespace(labelBuff), "TEMP1") == 0) set_pda_led(_aqualink_data->aqbuttons[POOL_HEAT_INDEX].led, msg[AQ_MSGLEN - 1]); if (strcasecmp(stripwhitespace(labelBuff), "TEMP2") == 0) set_pda_led(_aqualink_data->aqbuttons[SPA_HEAT_INDEX].led, msg[AQ_MSGLEN - 1]); } } void process_pda_packet_msg_long_home(const char *msg) { if (stristr(msg, "POOL MODE") != NULL) { // If pool mode is on the filter pump is on but if it is off the filter pump might be on if spa mode is on. if (msg[AQ_MSGLEN - 1] == 'N') { _aqualink_data->aqbuttons[PUMP_INDEX].led->state = ON; } else if (msg[AQ_MSGLEN - 1] == '*') { _aqualink_data->aqbuttons[PUMP_INDEX].led->state = FLASH; } } else if (stristr(msg, "POOL HEATER") != NULL) { set_pda_led(_aqualink_data->aqbuttons[POOL_HEAT_INDEX].led, msg[AQ_MSGLEN - 1]); } else if (stristr(msg, "SPA MODE") != NULL) { // when SPA mode is on the filter may be on or pending if (msg[AQ_MSGLEN - 1] == 'N') { _aqualink_data->aqbuttons[PUMP_INDEX].led->state = ON; _aqualink_data->aqbuttons[SPA_INDEX].led->state = ON; } else if (msg[AQ_MSGLEN - 1] == '*') { _aqualink_data->aqbuttons[PUMP_INDEX].led->state = FLASH; _aqualink_data->aqbuttons[SPA_INDEX].led->state = ON; } else { _aqualink_data->aqbuttons[SPA_INDEX].led->state = OFF; } } else if (stristr(msg, "SPA HEATER") != NULL) { set_pda_led(_aqualink_data->aqbuttons[SPA_HEAT_INDEX].led, msg[AQ_MSGLEN - 1]); } } void setSingleDeviceMode() { if (_aqualink_data->single_device != true) { _aqualink_data->single_device = true; logMessage(LOG_NOTICE, "AqualinkD set to 'Pool OR Spa Only' mode\n"); } } void process_pda_packet_msg_long_set_temp(const char *msg) { logMessage(LOG_DEBUG, "process_pda_packet_msg_long_set_temp\n"); if (stristr(msg, "POOL HEAT") != NULL) { _aqualink_data->pool_htr_set_point = atoi(msg + 10); logMessage(LOG_DEBUG, "pool_htr_set_point = %d\n", _aqualink_data->pool_htr_set_point); } else if (stristr(msg, "SPA HEAT") != NULL) { _aqualink_data->spa_htr_set_point = atoi(msg + 10); logMessage(LOG_DEBUG, "spa_htr_set_point = %d\n", _aqualink_data->spa_htr_set_point); } else if (stristr(msg, "TEMP1") != NULL) { setSingleDeviceMode(); _aqualink_data->pool_htr_set_point = atoi(msg + 10); logMessage(LOG_DEBUG, "pool_htr_set_point = %d\n", _aqualink_data->pool_htr_set_point); } else if (stristr(msg, "TEMP2") != NULL) { setSingleDeviceMode(); _aqualink_data->spa_htr_set_point = atoi(msg + 10); logMessage(LOG_DEBUG, "spa_htr_set_point = %d\n", _aqualink_data->spa_htr_set_point); } } void process_pda_packet_msg_long_spa_heat(const char *msg) { if (strncasecmp(msg, " ENABLED ", 16) == 0) { _aqualink_data->aqbuttons[SPA_HEAT_INDEX].led->state = ENABLE; } else if (strncasecmp(msg, " SET TO", 8) == 0) { _aqualink_data->spa_htr_set_point = atoi(msg + 8); logMessage(LOG_DEBUG, "spa_htr_set_point = %d\n", _aqualink_data->spa_htr_set_point); } } void process_pda_packet_msg_long_pool_heat(const char *msg) { if (strncasecmp(msg, " ENABLED ", 16) == 0) { _aqualink_data->aqbuttons[POOL_HEAT_INDEX].led->state = ENABLE; } else if (strncasecmp(msg, " SET TO", 8) == 0) { _aqualink_data->pool_htr_set_point = atoi(msg + 8); logMessage(LOG_DEBUG, "pool_htr_set_point = %d\n", _aqualink_data->pool_htr_set_point); } } void process_pda_packet_msg_long_freeze_protect(const char *msg) { if (strncasecmp(msg, "TEMP ", 10) == 0) { _aqualink_data->frz_protect_set_point = atoi(msg + 10); logMessage(LOG_DEBUG, "frz_protect_set_point = %d\n", _aqualink_data->frz_protect_set_point); } } void process_pda_packet_msg_long_SWG(const char *msg) { //PDA Line 0 = SET AquaPure //PDA Line 1 = //PDA Line 2 = //PDA Line 3 = SET POOL TO: 45% //PDA Line 4 = SET SPA TO: 0% // If spa is on, read SWG for spa, if not set SWG for pool if (_aqualink_data->aqbuttons[SPA_INDEX].led->state != OFF) { if (strncasecmp(msg, "SET SPA TO:", 11) == 0) { _aqualink_data->swg_percent = atoi(msg + 13); logMessage(LOG_DEBUG, "SPA swg_percent = %d\n", _aqualink_data->swg_percent); } } else { if (strncasecmp(msg, "SET POOL TO:", 12) == 0) { _aqualink_data->swg_percent = atoi(msg + 13); logMessage(LOG_DEBUG, "POOL swg_percent = %d\n", _aqualink_data->swg_percent); } } } void process_pda_packet_msg_long_unknown(const char *msg) { int i; // Lets make a guess here and just see if there is an ON/OFF/ENA/*** at the end of the line // When you turn on/off a piece of equiptment, a clear screen followed by single message is sent. // So we are not in any PDA menu, try to catch that message here so we catch new device state ASAP. if (msg[AQ_MSGLEN - 1] == 'N' || msg[AQ_MSGLEN - 1] == 'F' || msg[AQ_MSGLEN - 1] == 'A' || msg[AQ_MSGLEN - 1] == '*') { for (i = 0; i < TOTAL_BUTTONS; i++) { if (stristr(msg, _aqualink_data->aqbuttons[i].pda_label) != NULL) { printf("*** UNKNOWN Found Status for %s = '%.*s'\n", _aqualink_data->aqbuttons[i].pda_label, AQ_MSGLEN, msg); // set_pda_led(_aqualink_data->aqbuttons[i].led, msg[AQ_MSGLEN-1]); } } } } void process_pda_packet_msg_long_level_aux_device(const char *msg) { #ifdef BETA_PDA_AUTOLABEL int li=-1; char *str, *label; if (! _aqualink_config->use_panel_aux_labels) return; // NSF Need to check config for use_panel_aux_labels value and ignore if not set // Only care once we have the full menu, so check line // PDA Line 0 = LABEL AUX1 // PDA Line 1 = // PDA Line 2 = CURRENT LABEL // PDA Line 3 = AUX1 if ( (strlen(pda_m_line(3)) > 0 ) && (strncasecmp(pda_m_line(2)," CURRENT LABEL ", 16) == 0) && (strncasecmp(pda_m_line(0)," LABEL AUX", 12) == 0) ) { str = pda_m_line(0); li = atoi(&str[12] ); // 12 is a guess, need to check on real system if (li > 0) { str = cleanwhitespace(pda_m_line(3)); label = (char*)malloc(strlen(str)+1); strcpy ( label, str ); _aqualink_data->aqbuttons[li-1].pda_label = label; } else { logMessage(LOG_ERR, "PDA couldn't get AUX? number\n", pda_m_line(0)); } } #endif } void process_pda_freeze_protect_devices() { // PDA Line 0 = FREEZE PROTECT // PDA Line 1 = DEVICES // PDA Line 2 = // PDA Line 3 = FILTER PUMP X // PDA Line 4 = SPA // PDA Line 5 = CLEANER X // PDA Line 6 = POOL LIGHT // PDA Line 7 = SPA LIGHT // PDA Line 8 = EXTRA AUX // PDA Line 9 = int i; logMessage(LOG_DEBUG, "process_pda_freeze_protect_devices\n"); for (i = 1; i < PDA_LINES; i++) { if (pda_m_line(i)[AQ_MSGLEN - 1] == 'X') { logMessage(LOG_DEBUG, "PDA freeze protect enabled by %s\n", pda_m_line(i)); if (_aqualink_data->frz_protect_state == OFF) { _aqualink_data->frz_protect_state = ENABLE; break; } } } } bool process_pda_packet(unsigned char *packet, int length) { bool rtn = true; int i; char *msg; //static bool init = false; process_pda_menu_packet(packet, length); // NSF. //_aqualink_data->last_msg_was_status = false; // debugPacketPrint(0x00, packet, length); switch (packet[PKT_CMD]) { case CMD_ACK: logMessage(LOG_DEBUG, "RS Received ACK length %d.\n", length); //if (init == false) if (_initWithRS == false) { logMessage(LOG_DEBUG, "Running PDA_INIT\n"); aq_programmer(AQ_PDA_INIT, NULL, _aqualink_data); //init = true; } break; case CMD_STATUS: _aqualink_data->last_display_message[0] = '\0'; // If we get a status packet, and we are on the status menu, this is a list of what's on // or pending so unless flash turn everything off, and just turn on items that are listed. // This is the only way to update a device that's been turned off by a real PDA / keypad. // Note: if the last line of the status menu is present it may be cut off if (pda_m_type() == PM_EQUIPTMENT_STATUS) { if (_aqualink_data->frz_protect_state == ON) _aqualink_data->frz_protect_state = ENABLE; if (_aqualink_data->ar_swg_status == SWG_STATUS_ON) _aqualink_data->ar_swg_status = SWG_STATUS_OFF; if (pda_m_line(PDA_LINES - 1)[0] == '\0') { for (i = 0; i < TOTAL_BUTTONS; i++) { if (_aqualink_data->aqbuttons[i].led->state != FLASH) { _aqualink_data->aqbuttons[i].led->state = OFF; } } } else { logMessage(LOG_DEBUG, "PDA Equipment status may be truncated.\n"); } for (i = 1; i < PDA_LINES; i++) { pass_pda_equiptment_status_item(pda_m_line(i)); } } if (pda_m_type() == PM_FREEZE_PROTECT_DEVICES) { process_pda_freeze_protect_devices(); } break; case CMD_MSG_LONG: { //printf ("*******************************************************************************************\n"); //printf ("menu type %d\n",pda_m_type()); msg = (char *)packet + PKT_DATA + 1; //strcpy(_aqualink_data->last_message, msg); if (packet[PKT_DATA] == 0x82) { // Air & Water temp is always this ID process_pda_packet_msg_long_temp(msg); } else if (packet[PKT_DATA] == 0x40) { // Time is always on this ID process_pda_packet_msg_long_time(msg); // If it wasn't a specific msg, (above) then run through and see what kind // of message it is depending on the PDA menu. Note don't process EQUIPTMENT // STATUS menu here, wait until a CMD_STATUS is received. } else { switch (pda_m_type()) { case PM_EQUIPTMENT_CONTROL: process_pda_packet_msg_long_equipment_control(msg); break; case PM_HOME: case PM_BUILDING_HOME: process_pda_packet_msg_long_home(msg); break; case PM_SET_TEMP: process_pda_packet_msg_long_set_temp(msg); break; case PM_SPA_HEAT: process_pda_packet_msg_long_spa_heat(msg); break; case PM_POOL_HEAT: process_pda_packet_msg_long_pool_heat(msg); break; case PM_FREEZE_PROTECT: process_pda_packet_msg_long_freeze_protect(msg); break; case PM_AQUAPURE: process_pda_packet_msg_long_SWG(msg); break; case PM_AUX_LABEL_DEVICE: process_pda_packet_msg_long_level_aux_device(msg); break; //case PM_FW_VERSION: // process_pda_packet_msg_long_FW_version(msg); //break; case PM_UNKNOWN: default: process_pda_packet_msg_long_unknown(msg); break; } } // printf("** Line index='%d' Highligh='%s' Message='%.*s'\n",pda_m_hlightindex(), pda_m_hlight(), AQ_MSGLEN, msg); logMessage(LOG_INFO, "PDA Menu '%d' Selectedline '%s', Last line received '%.*s'\n", pda_m_type(), pda_m_hlight(), AQ_MSGLEN, msg); break; } case CMD_PDA_0x1B: { // We get two of these on startup, one with 0x00 another with 0x01 at index 4. Just act on one. // Think this is PDA finishd showing startup screen if (packet[4] == 0x00) { if (_initWithRS == false) { _initWithRS = true; logMessage(LOG_DEBUG, "**** PDA INIT ****"); aq_programmer(AQ_PDA_INIT, NULL, _aqualink_data); delay(50); // Make sure this one runs first. #ifdef BETA_PDA_AUTOLABEL if (_aqualink_config->use_panel_aux_labels) aq_programmer(AQ_GET_AUX_LABELS, NULL, _aqualink_data); #endif aq_programmer(AQ_PDA_WAKE_INIT, NULL, _aqualink_data); } else { logMessage(LOG_DEBUG, "**** PDA WAKE INIT ****"); aq_programmer(AQ_PDA_WAKE_INIT, NULL, _aqualink_data); } } } break; } if (packet[PKT_CMD] == CMD_MSG_LONG || packet[PKT_CMD] == CMD_PDA_HIGHLIGHT || packet[PKT_CMD] == CMD_PDA_SHIFTLINES || packet[PKT_CMD] == CMD_PDA_CLEAR || packet[PKT_CMD] == CMD_PDA_HIGHLIGHTCHARS) { // We processed the next message, kick any threads waiting on the message. kick_aq_program_thread(_aqualink_data); } return rtn; }