From d884f2e37c94ca1172f1025ae35d1f4df82167d1 Mon Sep 17 00:00:00 2001 From: shaun feakes Date: Sat, 25 May 2019 11:52:36 -0500 Subject: [PATCH] Version 1.3.0 --- Makefile | 2 +- README.md | 19 +- aq_programmer.c | 325 +++---------- aq_programmer.h | 14 +- aq_serial.c | 3 + aq_serial.h | 5 +- aqualink.h | 1 + aqualinkd.c | 927 +++++++++++++++++-------------------- config.c | 18 + config.h | 5 + net_services.c | 7 +- pda.c | 557 ++++++++++++++++++++++ pda.h | 12 + pda_aq_programmer.c | 770 ++++++++++++++++++++++++++++++ pda_aq_programmer.h | 21 + pda_menu.c | 164 ++++++- pda_menu.h | 54 ++- release/aqualinkd | Bin 257656 -> 272764 bytes release/aqualinkd.conf | 21 + release/aqualinkd.pda.conf | 15 +- release/serial_logger | Bin 29256 -> 29340 bytes serial_logger.c | 4 +- utils.c | 46 ++ utils.h | 4 +- version.h | 2 +- web/controller.html | 3 +- 26 files changed, 2206 insertions(+), 793 deletions(-) create mode 100644 pda.c create mode 100644 pda.h create mode 100644 pda_aq_programmer.c create mode 100644 pda_aq_programmer.h diff --git a/Makefile b/Makefile index 15fd2ff..c77d79f 100755 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ INCLUDES = -I/nas/data/Development/Raspberry/aqualink/aqualinkd # Add inputs and outputs from these tool invocations to the build variables # define the C source files -SRCS = aqualinkd.c utils.c config.c aq_serial.c init_buttons.c aq_programmer.c net_services.c json_messages.c pda_menu.c mongoose.c +SRCS = aqualinkd.c utils.c config.c aq_serial.c init_buttons.c aq_programmer.c net_services.c json_messages.c pda.c pda_menu.c pda_aq_programmer.c mongoose.c SL_SRC = serial_logger.c aq_serial.c utils.c PDA_SRC = pda_test.c pda_menu.c aq_serial.c utils.c diff --git a/README.md b/README.md index 54d65ef..5275094 100644 --- a/README.md +++ b/README.md @@ -59,15 +59,26 @@ Designed to mimic AqualinkRS6 All Button keypad, and just like the keypad you ca ## All Web interfaces. -* http://aqualink.ip/ <- (New UI) -* http://aqualink.ip/old <- (If you prefer the old UI, this is not maintained) -* http://aqualink.ip/simple.html <- (Anothr opion if you don't like the above) +* http://aqualink.ip/ <- (Standard WEB UI +* http://aqualink.ip/simple.html <- (Simple opion if you don't like the above) * http://aqualink.ip/simulator.html <- (RS8 All Button Control Panel simulator) # +## Update in Release 1.3.0 +* Large update for PDA only control panels (Majority of this is ballle98 work) +* Can distinguish between AquaPalm and PDA supported control panels. +* PDA Freeze & Heater setpoints now supported. +* Added PDA Sleep mode so AqualinkD can work inconjunction with a real Jandy PDA. +* Speeded up many PDA functions. +* Fixed many PDA bugs. +* Non PDA specific updates :- +* Can get button labels from control panel (not in PDA mode) +* RS485 Logging so users can submit information on Variable Speed Pumps & other devices for future support. +* Force SWG status on startup, rather than wait for pump to turn on. +* General bug fixes and improved code in many areas. ## Update in Release 1.2.6f * Solution to overcome bug in Mosquitto 1.6. * Fixed Salt Water Generator when % was set to 0. -* Added support for different SWG % for pool & spa. +* Added support for different SWG % for pool & spa. (SWG reports and sets the mode that's currently active) * Increased speed of SWG messages. * Few other bug fixes (Thanks to ballle98) ## Update in Release 1.2.6e (This is a quick update, please only use if you need one of the items below.) diff --git a/aq_programmer.c b/aq_programmer.c index b6b5185..97b883e 100644 --- a/aq_programmer.c +++ b/aq_programmer.c @@ -26,12 +26,14 @@ #include "utils.h" #include "aq_programmer.h" #include "aq_serial.h" +#include "pda.h" #include "pda_menu.h" #include "init_buttons.h" +#include "pda_aq_programmer.h" bool select_sub_menu_item(struct aqualinkdata *aq_data, char* item_string); bool select_menu_item(struct aqualinkdata *aq_data, char* item_string); -void send_cmd(unsigned char cmd, struct aqualinkdata *aq_data); +//void send_cmd(unsigned char cmd, struct aqualinkdata *aq_data); void cancel_menu(struct aqualinkdata *aq_data); @@ -43,17 +45,18 @@ void *get_aqualink_pool_spa_heater_temps( void *ptr ); void *get_aqualink_programs( void *ptr ); void *get_freeze_protect_temp( void *ptr ); void *get_aqualink_diag_model( void *ptr ); +void *get_aqualink_aux_labels( void *ptr ); void *threadded_send_cmd( void *ptr ); void *set_aqualink_light_colormode( void *ptr ); void *set_aqualink_PDA_init( void *ptr ); void *set_aqualink_SWG( void *ptr ); -void *get_aqualink_PDA_device_status( void *ptr ); -void *set_aqualink_PDA_device_on_off( void *ptr ); +//void *get_aqualink_PDA_device_status( void *ptr ); +//void *set_aqualink_PDA_device_on_off( void *ptr ); bool waitForButtonState(struct aqualinkdata *aq_data, aqkey* button, aqledstate state, int numMessageReceived); -bool waitForMessage(struct aqualinkdata *aq_data, char* message, int numMessageReceived); +//bool waitForMessage(struct aqualinkdata *aq_data, char* message, int numMessageReceived); bool waitForEitherMessage(struct aqualinkdata *aq_data, char* message1, char* message2, int numMessageReceived); bool push_aq_cmd(unsigned char cmd); @@ -204,12 +207,18 @@ void aq_programmer(program_type type, char *args, struct aqualinkdata *aq_data) struct programmingThreadCtrl *programmingthread = malloc(sizeof(struct programmingThreadCtrl)); if (pda_mode() == true) { + pda_reset_sleep(); if (type != AQ_PDA_INIT && + type != AQ_PDA_WAKE_INIT && type != AQ_PDA_DEVICE_STATUS && - type != AQ_PDA_DEVICE_ON_OFF) { + type != AQ_SET_POOL_HEATER_TEMP && + type != AQ_SET_SPA_HEATER_TEMP && + type != AQ_SET_SWG_PERCENT && + type != AQ_PDA_DEVICE_ON_OFF && + type != AQ_GET_POOL_SPA_HEATER_TEMPS ) { logMessage(LOG_ERR, "Selected Programming mode '%d' not supported with PDA mode control panel\n",type); return; - } + } } programmingthread->aq_data = aq_data; @@ -289,6 +298,12 @@ void aq_programmer(program_type type, char *args, struct aqualinkdata *aq_data) return; } break; + case AQ_PDA_WAKE_INIT: + if( pthread_create( &programmingthread->thread_id , NULL , set_aqualink_PDA_wakeinit, (void*)programmingthread) < 0) { + logMessage (LOG_ERR, "could not create thread\n"); + return; + } + break; case AQ_SET_SWG_PERCENT: if( pthread_create( &programmingthread->thread_id , NULL , set_aqualink_SWG, (void*)programmingthread) < 0) { logMessage (LOG_ERR, "could not create thread\n"); @@ -306,6 +321,12 @@ void aq_programmer(program_type type, char *args, struct aqualinkdata *aq_data) logMessage (LOG_ERR, "could not create thread\n"); return; } + break; + case AQ_GET_AUX_LABELS: + if( pthread_create( &programmingthread->thread_id , NULL , get_aqualink_aux_labels, (void*)programmingthread) < 0) { + logMessage (LOG_ERR, "could not create thread\n"); + return; + } break; default: logMessage (LOG_ERR, "Don't understand thread type\n"); @@ -478,17 +499,13 @@ void *set_aqualink_SWG( void *ptr ) int val = atoi((char*)threadCtrl->thread_args); val = setpoint_check(SWG_SETPOINT, val, aq_data); - // Just recheck it's in multiple of 5. - /* - if (0 != (val % 5) ) - val = ((val + 5) / 10) * 10; - if (val > SWG_PERCENT_MAX) { - val = SWG_PERCENT_MAX; - } else if ( val < SWG_PERCENT_MIN) { - val = SWG_PERCENT_MIN; + if (pda_mode() == true) { + set_PDA_aqualink_SWG_setpoint(aq_data, val); + cleanAndTerminateThread(threadCtrl); + return ptr; } - */ + logMessage(LOG_DEBUG, "programming SWG percent to %d\n", val); if ( select_menu_item(aq_data, "SET AQUAPURE") != true ) { @@ -541,258 +558,37 @@ void *set_aqualink_SWG( void *ptr ) return ptr; } -bool select_pda_main_menu(struct aqualinkdata *aq_data) -{ - int i=0; - // Check to see if we are at the main menu - if (pda_m_type() == PM_MAIN) { - return true; - } - // First send back - send_cmd(KEY_PDA_BACK, aq_data); - while (_pgm_command != NUL) { - delay(500); - if (i++ > 6) return false; - } - //delay(1000); - i=0; - while (pda_m_type() != PM_MAIN) { - delay(500); - if (i++ > 6) return false; - } - return true; -} -bool wait_pda_selected_item() -{ - int i=0; - - i=0; - while (pda_m_hlightindex() == -1){ - if (i++ > 10) - break; - delay(100); - } - - if (pda_m_hlightindex() == -1) - return false; - else - return true; -} - -bool select_pda_main_menu_item(struct aqualinkdata *aq_data, pda_menu_type menu_item) -{ - int i=0; - char *menu; - - if (! select_pda_main_menu(aq_data)) - return false; - - logMessage(LOG_DEBUG, "PDA Device programmer at main menu\n"); - - if (menu_item == PM_MAIN) - return true; - else if (menu_item == PM_SETTINGS) - menu = "MENU"; - else if (menu_item == PM_EQUIPTMENT_CONTROL) - menu = "EQUIPMENT ON/OFF"; - else - return false; - - if (!wait_pda_selected_item()){ - logMessage(LOG_ERR, "PDA Device programmer didn't find a selected item\n"); - return false; - } - - while ( strncmp(pda_m_hlight(), menu, strlen(menu)) != 0 ) { - if (_pgm_command == NUL) { - send_cmd(KEY_PDA_DOWN, aq_data); - logMessage(LOG_DEBUG, "PDA Device programmer selected sub menu\n"); - waitForMessage(aq_data, NULL, 1); - } - if (i++ > (PDA_LINES * 2)) - return false; - delay(500); - } - - send_cmd(KEY_PDA_SELECT, aq_data); - while (_pgm_command != NUL) { delay(500); } - - return true; - /* - send_cmd(KEY_PDA_DOWN, aq_data); - while (_pgm_command != NUL) { delay(500); } - */ -} - -void *set_aqualink_PDA_device_on_off( void *ptr ) +void *get_aqualink_aux_labels( void *ptr ) { struct programmingThreadCtrl *threadCtrl; threadCtrl = (struct programmingThreadCtrl *) ptr; struct aqualinkdata *aq_data = threadCtrl->aq_data; - int i=0; - int found; - waitForSingleThreadOrTerminate(threadCtrl, AQ_PDA_DEVICE_STATUS); - - char *buf = (char*)threadCtrl->thread_args; - int device = atoi(&buf[0]); - int state = atoi(&buf[5]); + waitForSingleThreadOrTerminate(threadCtrl, AQ_GET_AUX_LABELS); - if (device < 0 || device > TOTAL_BUTTONS) { - logMessage(LOG_ERR, "PDA Device On/Off :- bad device number '%d'\n",device); + if ( select_menu_item(aq_data, "REVIEW") != true ) { + logMessage(LOG_WARNING, "Could not select REVIEW menu\n"); + cancel_menu(aq_data); + cleanAndTerminateThread(threadCtrl); + return ptr; + } + + if (select_sub_menu_item(aq_data, "AUX LABELS") != true) { + logMessage(LOG_WARNING, "Could not select AUX LABELS menu\n"); + cancel_menu(aq_data); cleanAndTerminateThread(threadCtrl); return ptr; } - logMessage(LOG_INFO, "PDA Device On/Off, device '%s', state %d\n",aq_data->aqbuttons[device].pda_label,state); - - //printf("DEVICE LABEL = %s\n",aq_data->aqbuttons[device].pda_label); - - if (! select_pda_main_menu_item(aq_data, PM_EQUIPTMENT_CONTROL)) { - logMessage(LOG_ERR, "PDA Device On/Off :- can't find main menu\n"); - cleanAndTerminateThread(threadCtrl); - return ptr; - } -/* - i=0; - while (pda_m_hlightindex() == -1){ - if (i++ > 10) - break; - delay(100); - } -*/ - delay(500); -printf("Wait for select\n"); - if (!wait_pda_selected_item()){ - logMessage(LOG_ERR, "PDA Device programmer didn't find a selected item\n"); - return false; - } -printf("End wait select\n"); - i=0; - char labelBuff[AQ_MSGLEN]; - strncpy(labelBuff, pda_m_hlight(), AQ_MSGLEN-4); - labelBuff[AQ_MSGLEN-4] = 0; - - while ( (found = strcasecmp(stripwhitespace(labelBuff), aq_data->aqbuttons[device].pda_label)) != 0 ) { - if (_pgm_command == NUL) { - send_cmd(KEY_PDA_DOWN, aq_data); - //printf("*** Send Down for %s ***\n",pda_m_hlight()); - waitForMessage(aq_data, NULL, 1); - } - if (i++ > (PDA_LINES * 2)) { - break; - } - delay(500); - strncpy(labelBuff, pda_m_hlight(), AQ_MSGLEN-4); - labelBuff[AQ_MSGLEN-4] = 0; - } - - if (found == 0) { - //printf("*** FOUND ITEM %s ***\n",pda_m_hlight()); - if (aq_data->aqbuttons[device].led->state != state) { - //printf("*** Select State ***\n"); - logMessage(LOG_INFO, "PDA Device On/Off, found device '%s', changing state\n",aq_data->aqbuttons[device].pda_label,state); - send_cmd(KEY_PDA_SELECT, aq_data); - while (_pgm_command != NUL) { delay(500); } - } else { - logMessage(LOG_INFO, "PDA Device On/Off, found device '%s', not changing state, is same\n",aq_data->aqbuttons[device].pda_label,state); - } - } else { - //printf("*** NOT FOUND ITEM ***\n"); - logMessage(LOG_ERR, "PDA Device On/Off, device '%s' not found\n",aq_data->aqbuttons[device].pda_label); - } - - select_pda_main_menu_item(aq_data, PM_MAIN); - //while (_pgm_command != NUL) { delay(500); } + waitForMessage(aq_data, NULL, 5); // Receive 5 messages cleanAndTerminateThread(threadCtrl); // just stop compiler error, ptr is not valid as it's just been freed return ptr; } -void *get_aqualink_PDA_device_status( void *ptr ) -{ - struct programmingThreadCtrl *threadCtrl; - threadCtrl = (struct programmingThreadCtrl *) ptr; - struct aqualinkdata *aq_data = threadCtrl->aq_data; - int i; - - waitForSingleThreadOrTerminate(threadCtrl, AQ_PDA_DEVICE_STATUS); - - //int val = atoi((char*)threadCtrl->thread_args); - - logMessage(LOG_DEBUG, "PDA Device Status\n"); - - if (! select_pda_main_menu_item(aq_data, PM_EQUIPTMENT_CONTROL)) { - logMessage(LOG_ERR, "PDA Device Status :- can't find main menu\n"); - cleanAndTerminateThread(threadCtrl); - return ptr; - } - //select_pda_main_menu_item(aq_data, "EQUIPMENT ON/OFF"); - - // Just loop over all the dvices 18 times should do it. - for (i=0; i < 18; i++) { - send_cmd(KEY_PDA_DOWN, aq_data); - while (_pgm_command != NUL) { delay(100); } - } - - //printf("*** GET MAIN MENU ***\n"); - - select_pda_main_menu_item(aq_data, PM_MAIN); - - //printf("*** FINISHED ***\n"); - /* - send_cmd(KEY_PDA_BACK, aq_data); - while (_pgm_command != NUL) { delay(500); } - */ - cleanAndTerminateThread(threadCtrl); - - // just stop compiler error, ptr is not valid as it's just been freed - return ptr; -} - -void *set_aqualink_PDA_init( void *ptr ) -{ - struct programmingThreadCtrl *threadCtrl; - threadCtrl = (struct programmingThreadCtrl *) ptr; - struct aqualinkdata *aq_data = threadCtrl->aq_data; - int i=0; - - waitForSingleThreadOrTerminate(threadCtrl, AQ_PDA_INIT); - - //int val = atoi((char*)threadCtrl->thread_args); - - //logMessage(LOG_DEBUG, "PDA Init\n", val); - - logMessage(LOG_DEBUG, "PDA Init\n"); - - if (! select_pda_main_menu_item(aq_data, PM_EQUIPTMENT_CONTROL)) { - logMessage(LOG_ERR, "PDA Init :- can't find main menu\n"); - cleanAndTerminateThread(threadCtrl); - return ptr; - } - //select_pda_main_menu_item(aq_data, "EQUIPMENT ON/OFF"); - - // Just loop over all the dvices 20 times should do it. - for (i=0; i < 18; i++) { - send_cmd(KEY_PDA_DOWN, aq_data); - while (_pgm_command != NUL) { delay(500); } - } - - select_pda_main_menu_item(aq_data, PM_MAIN); - - printf("*** PDA Init :- add code to find setpoints ***\n"); - - // Run through menu and find freeze setpoints / heater setpoints etc. - - cleanAndTerminateThread(threadCtrl); - - // just stop compiler error, ptr is not valid as it's just been freed - return ptr; -} - void *set_aqualink_light_colormode( void *ptr ) { @@ -894,6 +690,13 @@ void *set_aqualink_pool_heater_temps( void *ptr ) } */ val = setpoint_check(POOL_HTR_SETOINT, val, aq_data); + + if (pda_mode() == true) { + set_PDA_aqualink_heater_setpoint(aq_data, val, true); + cleanAndTerminateThread(threadCtrl); + return ptr; + } + // NSF IF in TEMP1 / TEMP2 mode, we need C range of 1 to 40 is 2 to 40 for TEMP1, 1 to 39 TEMP2 if (aq_data->single_device == true ){ name = "TEMP1"; @@ -960,6 +763,13 @@ void *set_aqualink_spa_heater_temps( void *ptr ) val = MEATER_MIN; }*/ val = setpoint_check(SPA_HTR_SETOINT, val, aq_data); + + if (pda_mode() == true) { + set_PDA_aqualink_heater_setpoint(aq_data, val, true); + cleanAndTerminateThread(threadCtrl); + return ptr; + } + // NSF IF in TEMP1 / TEMP2 mode, we need C range of 1 to 40 is 2 to 40 for TEMP1, 1 to 39 TEMP2 if (aq_data->single_device == true ){ @@ -1148,6 +958,14 @@ void *get_aqualink_pool_spa_heater_temps( void *ptr ) waitForSingleThreadOrTerminate(threadCtrl, AQ_GET_POOL_SPA_HEATER_TEMPS); logMessage(LOG_NOTICE, "Getting pool & spa heat setpoints from aqualink\n"); + if (pda_mode() == true) { + if (!get_PDA_aqualink_pool_spa_heater_temps(aq_data)) { + logMessage(LOG_ERR, "Error Getting PDA pool & spa heat protection setpoints\n"); + } + cleanAndTerminateThread(threadCtrl); + return ptr; + } + if ( select_menu_item(aq_data, "REVIEW") != true ) { logMessage(LOG_WARNING, "Could not select REVIEW menu\n"); cancel_menu(aq_data); @@ -1182,6 +1000,15 @@ void *get_freeze_protect_temp( void *ptr ) waitForSingleThreadOrTerminate(threadCtrl, AQ_GET_FREEZE_PROTECT_TEMP); logMessage(LOG_NOTICE, "Getting freeze protection setpoints\n"); + + if (pda_mode() == true) { + if (! get_PDA_freeze_protect_temp(aq_data)) { + logMessage(LOG_ERR, "Error Getting PDA freeze protection setpoints\n"); + } + cleanAndTerminateThread(threadCtrl); + return ptr; + } + if ( select_menu_item(aq_data, "REVIEW") != true ) { logMessage(LOG_WARNING, "Could not select REVIEW menu\n"); cancel_menu(aq_data); diff --git a/aq_programmer.h b/aq_programmer.h index 75cad40..1335402 100644 --- a/aq_programmer.h +++ b/aq_programmer.h @@ -8,7 +8,7 @@ #define HEATER_MAX_F 104 #define HEATER_MIN_F 36 #define FREEZE_PT_MAX_F 42 -#define FREEZE_PT_MIN_F 36 +#define FREEZE_PT_MIN_F 34 #define HEATER_MAX_C 40 #define HEATER_MIN_C 0 @@ -36,7 +36,9 @@ typedef enum { AQ_PDA_INIT, AQ_SET_SWG_PERCENT, AQ_PDA_DEVICE_STATUS, - AQ_PDA_DEVICE_ON_OFF + AQ_PDA_DEVICE_ON_OFF, + AQ_GET_AUX_LABELS, + AQ_PDA_WAKE_INIT } program_type; struct programmingThreadCtrl { @@ -64,4 +66,12 @@ unsigned char pop_aq_cmd(struct aqualinkdata *aq_data); int get_aq_cmd_length(); int setpoint_check(int type, int value, struct aqualinkdata *aqdata); + +// These shouldn't be here, but just for the PDA AQ PROGRAMMER +void send_cmd(unsigned char cmd, struct aqualinkdata *aq_data); +bool push_aq_cmd(unsigned char cmd); +void waitForSingleThreadOrTerminate(struct programmingThreadCtrl *threadCtrl, program_type type); +void cleanAndTerminateThread(struct programmingThreadCtrl *threadCtrl); +bool waitForMessage(struct aqualinkdata *aq_data, char* message, int numMessageReceived); + #endif diff --git a/aq_serial.c b/aq_serial.c index bf93d23..9d56287 100644 --- a/aq_serial.c +++ b/aq_serial.c @@ -125,6 +125,9 @@ const char* get_packet_type(unsigned char* packet , int length) case CMD_PDA_0x05: return "PDA Unknown"; break; + case CMD_PDA_0x1B: + return "PDA Init (*guess*)"; + break; case CMD_PDA_HIGHLIGHT: return "PDA Hlight"; break; diff --git a/aq_serial.h b/aq_serial.h index 292e91f..a4946ee 100644 --- a/aq_serial.h +++ b/aq_serial.h @@ -53,8 +53,8 @@ #define KEY_PDA_DOWN 0x05 #define KEY_PDA_BACK 0x02 #define KEY_PDA_SELECT 0x04 -#define KEY_PDA_PGUP 0x01 -#define KEY_PDA_PGDN 0x03 +//#define KEY_PDA_PGUP 0x01 // Think these are hot key #1 +//#define KEY_PDA_PGDN 0x03 // Think these are hot key #2 /* KEY/BUTTON CODES */ #define KEY_PUMP 0x02 @@ -178,6 +178,7 @@ SPILLOVER IS DISABLED WHILE SPA IS ON #define SWG_STATUS_CHECK_PCB 0x80 // check PCB 0x80 #define CMD_PDA_0x05 0x05 +#define CMD_PDA_0x1B 0x1b #define CMD_PDA_HIGHLIGHT 0x08 #define CMD_PDA_CLEAR 0x09 #define CMD_PDA_SHIFTLINES 0x0F diff --git a/aqualink.h b/aqualink.h index c983c1c..2c7511c 100644 --- a/aqualink.h +++ b/aqualink.h @@ -97,6 +97,7 @@ struct aqualinkdata bool simulate_panel; aqledstate service_mode_state; aqledstate frz_protect_state; + unsigned char last_packet_type; //bool last_msg_was_status; //bool ar_swg_connected; }; diff --git a/aqualinkd.c b/aqualinkd.c index ef491ae..360f0f5 100644 --- a/aqualinkd.c +++ b/aqualinkd.c @@ -14,7 +14,6 @@ * https://github.com/sfeakes/aqualinkd */ - #define _GNU_SOURCE 1 // for strcasestr & strptime #define __USE_XOPEN 1 #include @@ -27,7 +26,7 @@ #include #include -#include // Need GNU_SOURCE & XOPEN defined for strptime +#include // Need GNU_SOURCE & XOPEN defined for strptime #include "mongoose.h" #include "aqualink.h" @@ -38,99 +37,101 @@ #include "aq_programmer.h" #include "net_services.h" #include "pda_menu.h" +#include "pda.h" #include "version.h" - #define DEFAULT_CONFIG_FILE "./aqualinkd.conf" - static volatile bool _keepRunning = true; static struct aqconfig _config_parameters; static struct aqualinkdata _aqualink_data; - void main_loop(); -void intHandler(int dummy) { +void intHandler(int dummy) +{ _keepRunning = false; logMessage(LOG_NOTICE, "Stopping!"); } - void processLEDstate() { - int i=0; + int i = 0; int byte; int bit; - - for (byte=0;byte<5;byte++) + + for (byte = 0; byte < 5; byte++) { - for (bit=0; bit<8; bit+=2) + for (bit = 0; bit < 8; bit += 2) { - if ( ((_aqualink_data.raw_status[byte] >> (bit+1) ) & 1) == 1 ) - _aqualink_data.aqualinkleds[i].state = FLASH; - else if ( ((_aqualink_data.raw_status[byte] >> bit) & 1) == 1 ) + if (((_aqualink_data.raw_status[byte] >> (bit + 1)) & 1) == 1) + _aqualink_data.aqualinkleds[i].state = FLASH; + else if (((_aqualink_data.raw_status[byte] >> bit) & 1) == 1) _aqualink_data.aqualinkleds[i].state = ON; else _aqualink_data.aqualinkleds[i].state = OFF; - + //logMessage(LOG_DEBUG,"Led %d state %d",i+1,_aqualink_data.aqualinkleds[i].state); i++; } } // Reset enabled state for heaters, as they take 2 led states - if ( _aqualink_data.aqualinkleds[POOL_HTR_LED_INDEX-1].state == OFF && _aqualink_data.aqualinkleds[POOL_HTR_LED_INDEX].state == ON) - _aqualink_data.aqualinkleds[POOL_HTR_LED_INDEX-1].state = ENABLE;\ - - if ( _aqualink_data.aqualinkleds[SPA_HTR_LED_INDEX-1].state == OFF && _aqualink_data.aqualinkleds[SPA_HTR_LED_INDEX].state == ON) - _aqualink_data.aqualinkleds[SPA_HTR_LED_INDEX-1].state = ENABLE; - - if ( _aqualink_data.aqualinkleds[SOLAR_HTR_LED_INDEX-1].state == OFF && _aqualink_data.aqualinkleds[SOLAR_HTR_LED_INDEX].state == ON) - _aqualink_data.aqualinkleds[SOLAR_HTR_LED_INDEX-1].state = ENABLE; -/* + if (_aqualink_data.aqualinkleds[POOL_HTR_LED_INDEX - 1].state == OFF && _aqualink_data.aqualinkleds[POOL_HTR_LED_INDEX].state == ON) + _aqualink_data.aqualinkleds[POOL_HTR_LED_INDEX - 1].state = ENABLE; + + if (_aqualink_data.aqualinkleds[SPA_HTR_LED_INDEX - 1].state == OFF && _aqualink_data.aqualinkleds[SPA_HTR_LED_INDEX].state == ON) + _aqualink_data.aqualinkleds[SPA_HTR_LED_INDEX - 1].state = ENABLE; + + if (_aqualink_data.aqualinkleds[SOLAR_HTR_LED_INDEX - 1].state == OFF && _aqualink_data.aqualinkleds[SOLAR_HTR_LED_INDEX].state == ON) + _aqualink_data.aqualinkleds[SOLAR_HTR_LED_INDEX - 1].state = ENABLE; + /* for (i=0; i < TOTAL_BUTTONS; i++) { logMessage(LOG_NOTICE, "%s = %d", _aqualink_data.aqbuttons[i].name, _aqualink_data.aqualinkleds[i].state); } */ } - bool checkAqualinkTime() { static time_t last_checked; - time_t now = time(0); // get time now + time_t now = time(0); // get time now int time_difference; struct tm aq_tm; time_t aqualink_time; - + time_difference = (int)difftime(now, last_checked); - if (time_difference < TIME_CHECK_INTERVAL) { + if (time_difference < TIME_CHECK_INTERVAL) + { logMessage(LOG_DEBUG, "time not checked, will check in %d seconds", TIME_CHECK_INTERVAL - time_difference); return true; - } else { + } + else + { last_checked = now; //return false; } - + char datestr[DATE_STRING_LEN]; strcpy(&datestr[0], _aqualink_data.date); strcpy(&datestr[12], " "); strcpy(&datestr[13], _aqualink_data.time); - - if (strptime(datestr, "%m/%d/%y %a %I:%M %p", &aq_tm) == NULL) { + + if (strptime(datestr, "%m/%d/%y %a %I:%M %p", &aq_tm) == NULL) + { logMessage(LOG_ERR, "Could not convert RS time string '%s'", datestr); last_checked = (time_t)NULL; return true; } - aq_tm.tm_isdst = -1; // Force mktime to use local timezone + aq_tm.tm_isdst = -1; // Force mktime to use local timezone aqualink_time = mktime(&aq_tm); time_difference = (int)difftime(now, aqualink_time); - - logMessage(LOG_INFO, "Aqualink time is off by %d seconds...\n", time_difference); - - if(abs(time_difference) <= ACCEPTABLE_TIME_DIFF) { + + logMessage(LOG_INFO, "Aqualink time is off by %d seconds...\n", time_difference); + + if (abs(time_difference) <= ACCEPTABLE_TIME_DIFF) + { // Time difference is less than or equal to 90 seconds (1 1/2 minutes). // Set the return value to true. return true; @@ -171,24 +172,27 @@ void queueGetProgramData() // Init string good time to get setpoints aq_programmer(AQ_GET_POOL_SPA_HEATER_TEMPS, NULL, &_aqualink_data); aq_programmer(AQ_GET_FREEZE_PROTECT_TEMP, NULL, &_aqualink_data); + if (_config_parameters.use_panel_aux_labels == true) + { + aq_programmer(AQ_GET_AUX_LABELS, NULL, &_aqualink_data); + } //aq_programmer(AQ_GET_PROGRAMS, NULL, &_aqualink_data); // only displays to log at present, also seems to confuse getting set_points } void setUnits(char *msg) { - logMessage(LOG_DEBUG, "Getting temp units from message %s, looking at %c", msg, msg[strlen(msg)-1]); + logMessage(LOG_DEBUG, "Getting temp units from message %s, looking at %c", msg, msg[strlen(msg) - 1]); - if (msg[strlen(msg)-1] == 'F') - _aqualink_data.temp_units = FAHRENHEIT; - else if (msg[strlen(msg)-1] == 'C') - _aqualink_data.temp_units = CELSIUS; + if (msg[strlen(msg) - 1] == 'F') + _aqualink_data.temp_units = FAHRENHEIT; + else if (msg[strlen(msg) - 1] == 'C') + _aqualink_data.temp_units = CELSIUS; else - _aqualink_data.temp_units = UNKNOWN; + _aqualink_data.temp_units = UNKNOWN; - logMessage(LOG_INFO, "Temp Units set to %d (F=0, C=1, Unknown=3)", _aqualink_data.temp_units); + logMessage(LOG_INFO, "Temp Units set to %d (F=0, C=1, Unknown=3)", _aqualink_data.temp_units); } - void processMessage(char *message) { char *msg; @@ -196,39 +200,42 @@ void processMessage(char *message) static bool _gotREV = false; static int freeze_msg_count = 0; static int service_msg_count = 0; - // NSF replace message with msg + // NSF replace message with msg msg = stripwhitespace(message); strcpy(_aqualink_data.last_message, msg); //_aqualink_data.last_message = _aqualink_data.message; //_aqualink_data.display_message = NULL; - + //aqualink_strcpy(_aqualink_data.message, msg); - - logMessage(LOG_INFO, "RS Message :- '%s'\n",msg); + + logMessage(LOG_INFO, "RS Message :- '%s'\n", msg); //logMessage(LOG_NOTICE, "RS Message :- '%s'\n",msg); - - // Check long messages in this if/elseif block first, as some messages are similar. + + // Check long messages in this if/elseif block first, as some messages are similar. // ie "POOL TEMP" and "POOL TEMP IS SET TO" so want correct match first. // - - if(stristr(msg, "JANDY AquaLinkRS") != NULL) { + + if (stristr(msg, "JANDY AquaLinkRS") != NULL) + { //_aqualink_data.display_message = NULL; _aqualink_data.last_display_message[0] = '\0'; } // If we have more than 10 messages without "Service Mode is active" assume it's off. - if (_aqualink_data.service_mode_state == ON && service_msg_count++ > 10) { + if (_aqualink_data.service_mode_state == ON && service_msg_count++ > 10) + { _aqualink_data.service_mode_state = OFF; service_msg_count = 0; } - + /* // If we have more than 10 messages without "Service Mode is active" assume it's off. if (_aqualink_data.service_mode_state == ON && service_msg_count++ > 10) { _aqualink_data.service_mode_state = OFF; service_msg_count = 0; } - +*/ // If we have more than 10 messages without "FREE PROTECT ACTIVATED" assume it's off. - if (_aqualink_data.frz_protect_state == ON && freeze_msg_count++ > 10) { + if (_aqualink_data.frz_protect_state == ON && freeze_msg_count++ > 10) + { _aqualink_data.frz_protect_state = ENABLE; freeze_msg_count = 0; } @@ -236,154 +243,194 @@ void processMessage(char *message) //else //_aqualink_data.display_last_message = false; - if(stristr(msg, LNG_MSG_BATTERY_LOW) != NULL) { + if (stristr(msg, LNG_MSG_BATTERY_LOW) != NULL) + { _aqualink_data.battery = LOW; strcpy(_aqualink_data.last_display_message, msg); // Also display the message on web UI } - else if(stristr(msg, LNG_MSG_POOL_TEMP_SET) != NULL) { + else if (stristr(msg, LNG_MSG_POOL_TEMP_SET) != NULL) + { //logMessage(LOG_DEBUG, "pool htr long message: %s", &message[20]); - _aqualink_data.pool_htr_set_point = atoi(message+20); + _aqualink_data.pool_htr_set_point = atoi(message + 20); if (_aqualink_data.temp_units == UNKNOWN) setUnits(msg); } - else if(stristr(msg, LNG_MSG_SPA_TEMP_SET) != NULL) { + else if (stristr(msg, LNG_MSG_SPA_TEMP_SET) != NULL) + { //logMessage(LOG_DEBUG, "spa htr long message: %s", &message[19]); - _aqualink_data.spa_htr_set_point = atoi(message+19); + _aqualink_data.spa_htr_set_point = atoi(message + 19); if (_aqualink_data.temp_units == UNKNOWN) setUnits(msg); } - else if(stristr(msg, LNG_MSG_FREEZE_PROTECTION_SET) != NULL) { + else if (stristr(msg, LNG_MSG_FREEZE_PROTECTION_SET) != NULL) + { //logMessage(LOG_DEBUG, "frz protect long message: %s", &message[28]); - _aqualink_data.frz_protect_set_point = atoi(message+28); + _aqualink_data.frz_protect_set_point = atoi(message + 28); _aqualink_data.frz_protect_state = ENABLE; if (_aqualink_data.temp_units == UNKNOWN) setUnits(msg); } - else if(strncasecmp(msg, MSG_AIR_TEMP, MSG_AIR_TEMP_LEN) == 0) { - _aqualink_data.air_temp = atoi(msg+MSG_AIR_TEMP_LEN); - - if (_aqualink_data.temp_units == UNKNOWN) - setUnits(msg); - } - else if(strncasecmp(msg, MSG_POOL_TEMP, MSG_POOL_TEMP_LEN) == 0) { - _aqualink_data.pool_temp = atoi(msg+MSG_POOL_TEMP_LEN); + else if (strncasecmp(msg, MSG_AIR_TEMP, MSG_AIR_TEMP_LEN) == 0) + { + _aqualink_data.air_temp = atoi(msg + MSG_AIR_TEMP_LEN); if (_aqualink_data.temp_units == UNKNOWN) setUnits(msg); } - else if(strncasecmp(msg, MSG_SPA_TEMP, MSG_SPA_TEMP_LEN) == 0) { - _aqualink_data.spa_temp = atoi(msg+MSG_SPA_TEMP_LEN); + else if (strncasecmp(msg, MSG_POOL_TEMP, MSG_POOL_TEMP_LEN) == 0) + { + _aqualink_data.pool_temp = atoi(msg + MSG_POOL_TEMP_LEN); + + if (_aqualink_data.temp_units == UNKNOWN) + setUnits(msg); + } + else if (strncasecmp(msg, MSG_SPA_TEMP, MSG_SPA_TEMP_LEN) == 0) + { + _aqualink_data.spa_temp = atoi(msg + MSG_SPA_TEMP_LEN); if (_aqualink_data.temp_units == UNKNOWN) setUnits(msg); } // NSF If get water temp rather than pool or spa in some cases, then we are in Pool OR Spa ONLY mode - else if(strncasecmp(msg, MSG_WATER_TEMP, MSG_WATER_TEMP_LEN) == 0) { - _aqualink_data.pool_temp = atoi(msg+MSG_WATER_TEMP_LEN); - _aqualink_data.spa_temp = atoi(msg+MSG_WATER_TEMP_LEN); + else if (strncasecmp(msg, MSG_WATER_TEMP, MSG_WATER_TEMP_LEN) == 0) + { + _aqualink_data.pool_temp = atoi(msg + MSG_WATER_TEMP_LEN); + _aqualink_data.spa_temp = atoi(msg + MSG_WATER_TEMP_LEN); if (_aqualink_data.temp_units == UNKNOWN) setUnits(msg); - - if (_aqualink_data.single_device != true) { + + if (_aqualink_data.single_device != true) + { _aqualink_data.single_device = true; - logMessage(LOG_NOTICE, "AqualinkD set to 'Pool OR Spa Only' mode\n"); + logMessage(LOG_NOTICE, "AqualinkD set to 'Pool OR Spa Only' mode\n"); } } - else if(stristr(msg, LNG_MSG_WATER_TEMP1_SET) != NULL) { - _aqualink_data.pool_htr_set_point = atoi(message+28); + else if (stristr(msg, LNG_MSG_WATER_TEMP1_SET) != NULL) + { + _aqualink_data.pool_htr_set_point = atoi(message + 28); if (_aqualink_data.temp_units == UNKNOWN) setUnits(msg); - if (_aqualink_data.single_device != true) { + if (_aqualink_data.single_device != true) + { _aqualink_data.single_device = true; - logMessage(LOG_NOTICE, "AqualinkD set to 'Pool OR Spa Only' mode\n"); + logMessage(LOG_NOTICE, "AqualinkD set to 'Pool OR Spa Only' mode\n"); } } - else if(stristr(msg, LNG_MSG_WATER_TEMP2_SET) != NULL) { - _aqualink_data.spa_htr_set_point = atoi(message+27); + else if (stristr(msg, LNG_MSG_WATER_TEMP2_SET) != NULL) + { + _aqualink_data.spa_htr_set_point = atoi(message + 27); if (_aqualink_data.temp_units == UNKNOWN) setUnits(msg); - if (_aqualink_data.single_device != true) { + if (_aqualink_data.single_device != true) + { _aqualink_data.single_device = true; - logMessage(LOG_NOTICE, "AqualinkD set to 'Pool OR Spa Only' mode\n"); - } + logMessage(LOG_NOTICE, "AqualinkD set to 'Pool OR Spa Only' mode\n"); + } } - else if (stristr(msg, LNG_MSG_SERVICE_ACTIVE) != NULL) { + else if (stristr(msg, LNG_MSG_SERVICE_ACTIVE) != NULL) + { if (_aqualink_data.service_mode_state == OFF) - logMessage(LOG_NOTICE, "AqualinkD set to Service Mode\n"); + logMessage(LOG_NOTICE, "AqualinkD set to Service Mode\n"); _aqualink_data.service_mode_state = ON; service_msg_count = 0; } - else if (stristr(msg, LNG_MSG_FREEZE_PROTECTION_ACTIVATED) != NULL) { + else if (stristr(msg, LNG_MSG_FREEZE_PROTECTION_ACTIVATED) != NULL) + { _aqualink_data.frz_protect_state = ON; freeze_msg_count = 0; strcpy(_aqualink_data.last_display_message, msg); // Also display the message on web UI } - else if(msg[2] == '/' && msg[5] == '/' && msg[8] == ' ') {// date in format '08/29/16 MON' + else if (msg[2] == '/' && msg[5] == '/' && msg[8] == ' ') + { // date in format '08/29/16 MON' strcpy(_aqualink_data.date, msg); } - else if(strncasecmp(msg, MSG_SWG_PCT, MSG_SWG_PCT_LEN) == 0) { - _aqualink_data.swg_percent = atoi(msg+MSG_SWG_PCT_LEN); + else if (strncasecmp(msg, MSG_SWG_PCT, MSG_SWG_PCT_LEN) == 0) + { + _aqualink_data.swg_percent = atoi(msg + MSG_SWG_PCT_LEN); //logMessage(LOG_DEBUG, "Stored SWG Percent as %d\n", _aqualink_data.swg_percent); } - else if(strncasecmp(msg, MSG_SWG_PPM, MSG_SWG_PPM_LEN) == 0) { - _aqualink_data.swg_ppm = atoi(msg+MSG_SWG_PPM_LEN); + else if (strncasecmp(msg, MSG_SWG_PPM, MSG_SWG_PPM_LEN) == 0) + { + _aqualink_data.swg_ppm = atoi(msg + MSG_SWG_PPM_LEN); //logMessage(LOG_DEBUG, "Stored SWG PPM as %d\n", _aqualink_data.swg_ppm); } - else if( (msg[1] == ':' || msg[2] == ':') && msg[strlen(msg)-1] == 'M') { // time in format '9:45 AM' + else if ((msg[1] == ':' || msg[2] == ':') && msg[strlen(msg) - 1] == 'M') + { // time in format '9:45 AM' strcpy(_aqualink_data.time, msg); // Setting time takes a long time, so don't try until we have all other programmed data. - if ( (_initWithRS == true) && strlen(_aqualink_data.date) > 1 && checkAqualinkTime() != true ) { + if ((_initWithRS == true) && strlen(_aqualink_data.date) > 1 && checkAqualinkTime() != true) + { logMessage(LOG_NOTICE, "RS time is NOT accurate '%s %s', re-setting on controller!\n", _aqualink_data.time, _aqualink_data.date); aq_programmer(AQ_SET_TIME, NULL, &_aqualink_data); - } else { + } + else + { logMessage(LOG_DEBUG, "RS time is accurate '%s %s'\n", _aqualink_data.time, _aqualink_data.date); } // If we get a time message before REV, the controller didn't see us as we started too quickly. - if ( _gotREV == false ) { - logMessage(LOG_NOTICE, "Getting control panel information\n",msg); + if (_gotREV == false) + { + logMessage(LOG_NOTICE, "Getting control panel information\n", msg); aq_programmer(AQ_GET_DIAGNOSTICS_MODEL, NULL, &_aqualink_data); _gotREV = true; // Force it to true just incase we don't understand the model# } } - else if(strstr(msg, " REV ") != NULL) { // '8157 REV MMM' + else if (strstr(msg, " REV ") != NULL) + { // '8157 REV MMM' // A master firmware revision message. strcpy(_aqualink_data.version, msg); _gotREV = true; - logMessage(LOG_NOTICE, "Control Panel %s\n",msg); - if ( _initWithRS == false) { + logMessage(LOG_NOTICE, "Control Panel %s\n", msg); + if (_initWithRS == false) + { queueGetProgramData(); _initWithRS = true; } } - else if(stristr(msg, " TURNS ON") != NULL) { - logMessage(LOG_NOTICE, "Program data '%s'\n",msg); + else if (stristr(msg, " TURNS ON") != NULL) + { + logMessage(LOG_NOTICE, "Program data '%s'\n", msg); } - else if(_config_parameters.override_freeze_protect == TRUE && - strncasecmp(msg, "Press Enter* to override Freeze Protection with", 47) == 0 ){ + else if (_config_parameters.override_freeze_protect == TRUE && strncasecmp(msg, "Press Enter* to override Freeze Protection with", 47) == 0) + { //send_cmd(KEY_ENTER, aq_data); aq_programmer(AQ_SEND_CMD, (char *)KEY_ENTER, &_aqualink_data); } - else { - logMessage(LOG_DEBUG_SERIAL, "Ignoring '%s'\n",msg); + else if ((msg[4] == ':') && (strncasecmp(msg, "AUX", 3) == 0)) + { // AUX label "AUX1:" + int labelid = atoi(msg + 3); + if (labelid > 0 && _config_parameters.use_panel_aux_labels == true) + { + // Aux1: on panel = Button 3 in aqualinkd (button 2 in array) + logMessage(LOG_NOTICE, "AUX LABEL %d '%s'\n", labelid + 1, msg); + _aqualink_data.aqbuttons[labelid+1].label = prittyString(cleanalloc(msg+5)); + //_aqualink_data.aqbuttons[labelid + 1].label = cleanalloc(msg + 5); + } + } + else + { + logMessage(LOG_DEBUG_SERIAL, "Ignoring '%s'\n", msg); //_aqualink_data.display_message = msg; if (_aqualink_data.active_thread.thread_id == 0 && stristr(msg, "JANDY AquaLinkRS") == NULL && stristr(msg, "PUMP O") == NULL /*&& // Catch 'PUMP ON' and 'PUMP OFF' but not 'PUMP WILL TURN ON' stristr(msg, "CLEANER O") == NULL && stristr(msg, "SPA O") == NULL && - stristr(msg, "AUX") == NULL*/) { // Catch all AUX1 AUX5 messages + stristr(msg, "AUX") == NULL*/ + ) + { // Catch all AUX1 AUX5 messages //_aqualink_data.display_last_message = true; strcpy(_aqualink_data.last_display_message, msg); } } - + // Send every message if we are in simulate panel mode if (_aqualink_data.simulate_panel) ascii(_aqualink_data.last_display_message, msg); @@ -393,285 +440,43 @@ void processMessage(char *message) } -void set_pda_led(struct aqualinkled *led, char state) -{ - if (state == 'N') - led->state = ON; - else if (state == 'A') - led->state = ENABLE; - else if (state == '*') - led->state = FLASH; - else - led->state = OFF; -} - -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 - - // Check message for status of device - // Loop through all buttons and match the PDA text. - if ((index = strcasestr(msg, MSG_SWG_PCT)) != NULL) - { - //int aq = atoi(index + strlen(MSG_SWG_PCT)); - //printf("Aquapure PERCENT message %d\n", aq); - _aqualink_data.swg_percent = atoi(index + strlen(MSG_SWG_PCT)); - } - else if ((index = strcasestr(msg, MSG_SWG_PPM)) != NULL) - { - //int aq = atoi(index + strlen(MSG_SWG_PPM)); - //printf("Aquapure SALT message %d\n", aq); - _aqualink_data.swg_ppm = atoi(index + strlen(MSG_SWG_PPM)); - } - else - { - char labelBuff[AQ_MSGLEN + 1]; - strncpy (labelBuff, msg, AQ_MSGLEN + 1); - msg = stripwhitespace (labelBuff); - - // These are listed as " FILTER PUMP " - - 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; - } - } - } - } -} - -bool process_pda_packet(unsigned char* packet, int length) -{ - bool rtn = true; - int i; - char *msg; - static bool init = false; - static time_t _lastStatus; - - 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); - break; - - case CMD_STATUS: - _aqualink_data.last_display_message[0] = '\0'; - /* - if (!init) { - aq_programmer(AQ_PDA_INIT, NULL, &_aqualink_data); - init=true; - }*/ - - // 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. - if (pda_m_type() == PM_EQUIPTMENT_STATUS) { - //printf("*** SET ALL OFF ****\n"); - for (i = 0; i < TOTAL_BUTTONS; i++) { - if (_aqualink_data.aqbuttons[i].led->state != FLASH) { - _aqualink_data.aqbuttons[i].led->state = OFF; - } - } - for (i = 1; i < PDA_LINES; i++) { - pass_pda_equiptment_status_item(pda_m_line(i)); - } - time(&_lastStatus); - } else { - time_t now; - time(&now); - if (init && difftime(now, _lastStatus) > 60){ - logMessage(LOG_DEBUG,"OVER 60 SECONDS SINCE LAST STATUS UPDATE, forcing refresh\n"); - // Reset aquapure to nothing since it must be off at this point - _aqualink_data.pool_temp = TEMP_UNKNOWN; - _aqualink_data.spa_temp = TEMP_UNKNOWN; - time(&_lastStatus); - aq_programmer(AQ_PDA_DEVICE_STATUS, NULL, &_aqualink_data); - } - } - break; - case CMD_MSG_LONG: { - msg = (char*)packet+PKT_DATA+1; - - if (packet[PKT_DATA] == 0x82) { // Air & Water temp is always this ID - // 'AIR POOL' - // ' 86` 86` ' - // 'AIR SPA ' - // ' 86` 86` ' - // 'AIR ' - // ' 86` ' - _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; - } - //printf("Air Temp = %d | Water Temp = %d\n",atoi(msg),atoi(msg+7)); - } else if (packet[PKT_DATA] == 0x40) { // Time is always on this ID - // 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); - // NSF Come back and change the above to correctly check date and time in future -// If it wasn't a specific msg, (above) then run through and see what kind of message it is depending on the PDA menu - } else if (pda_m_type() == PM_EQUIPTMENT_STATUS) { - pass_pda_equiptment_status_item(msg); - } else if (pda_m_type() == PM_EQUIPTMENT_CONTROL) { - // These are listed as "FILTER PUMP OFF" - char labelBuff[AQ_MSGLEN+1]; - strncpy(labelBuff, msg, AQ_MSGLEN-4); - labelBuff[AQ_MSGLEN-4] = 0; - - 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]); - } - } - } else if (pda_m_type() == PM_MAIN || pda_m_type() == PM_BUILDING_MAIN) { - 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]); - } - } else if (pda_m_type() == PM_UNKNOWN) { - // 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("*** 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]); - } - } - } - } - // If we haven't initilixed and we are on line 4, then initilize - if (! init && pda_m_hlightindex() == 4) { - //printf("** INITILIZE ADD LINE BACK\n"); - aq_programmer(AQ_PDA_INIT, NULL, &_aqualink_data); - time(&_lastStatus); - init = true; - } - - } - //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; - } - - if ( packet[PKT_CMD] == CMD_MSG_LONG || - packet[PKT_CMD] == CMD_PDA_HIGHLIGHT || - packet[PKT_CMD] == CMD_PDA_SHIFTLINES) { - // We processed the next message, kick any threads waiting on the message. - kick_aq_program_thread(&_aqualink_data); - } - return rtn; -} - -bool process_packet(unsigned char* packet, int length) +bool process_packet(unsigned char *packet, int length) { bool rtn = false; static unsigned char last_packet[AQ_MAXPKTLEN]; - static char message[AQ_MSGLONGLEN+1]; + static char message[AQ_MSGLONGLEN + 1]; static int processing_long_msg = 0; - + // Check packet against last check if different. - if (memcmp(packet, last_packet, length) == 0) { - logMessage(LOG_DEBUG_SERIAL, "RS Received duplicate, ignoring.\n",length); + if (memcmp(packet, last_packet, length) == 0) + { + logMessage(LOG_DEBUG_SERIAL, "RS Received duplicate, ignoring.\n", length); return rtn; - } else { + } + else + { memcpy(last_packet, packet, length); + _aqualink_data.last_packet_type = packet[PKT_CMD]; rtn = true; } - - if (_config_parameters.pda_mode == true) { + + if (_config_parameters.pda_mode == true) + { return process_pda_packet(packet, length); } - - if (processing_long_msg > 0 && packet[PKT_CMD] != CMD_MSG_LONG) { + + if (processing_long_msg > 0 && packet[PKT_CMD] != CMD_MSG_LONG) + { processing_long_msg = 0; //logMessage(LOG_ERR, "RS failed to receive complete long message, received '%s'\n",message); //logMessage(LOG_DEBUG, "RS didn't finished receiving of MSG_LONG '%s'\n",message); processMessage(message); } - - switch (packet[PKT_CMD]) { + + switch (packet[PKT_CMD]) + { case CMD_ACK: - logMessage(LOG_DEBUG, "RS Received ACK length %d.\n",length); + logMessage(LOG_DEBUG, "RS Received ACK length %d.\n", length); break; case CMD_STATUS: logMessage(LOG_DEBUG, "RS Received STATUS length %d.\n", length); @@ -694,8 +499,8 @@ bool process_packet(unsigned char* packet, int length) } break; case CMD_MSG: - memset(message, 0, AQ_MSGLONGLEN+1); - strncpy(message, (char*)packet+PKT_DATA+1, AQ_MSGLEN); + memset(message, 0, AQ_MSGLONGLEN + 1); + strncpy(message, (char *)packet + PKT_DATA + 1, AQ_MSGLEN); //logMessage(LOG_DEBUG_SERIAL, "RS Received message '%s'\n",message); if (packet[PKT_DATA] == 1) // Start of long message, get them all before processing { @@ -707,26 +512,27 @@ bool process_packet(unsigned char* packet, int length) break; case CMD_MSG_LONG: // First in sequence is normal message. - processing_long_msg++; - strncpy(&message[processing_long_msg*AQ_MSGLEN], (char*)packet+PKT_DATA+1, AQ_MSGLEN); - //logMessage(LOG_DEBUG_SERIAL, "RS Received long message '%s'\n",message); - if (processing_long_msg == 3) { - //logMessage(LOG_DEBUG, "RS Finished receiving of MSG_LONG '%s'\n",message); - processMessage(message); - processing_long_msg=0; - } + processing_long_msg++; + strncpy(&message[processing_long_msg * AQ_MSGLEN], (char *)packet + PKT_DATA + 1, AQ_MSGLEN); + //logMessage(LOG_DEBUG_SERIAL, "RS Received long message '%s'\n",message); + if (processing_long_msg == 3) + { + //logMessage(LOG_DEBUG, "RS Finished receiving of MSG_LONG '%s'\n",message); + processMessage(message); + processing_long_msg = 0; + } break; case CMD_PROBE: - logMessage(LOG_DEBUG, "RS Received PROBE length %d.\n",length); + logMessage(LOG_DEBUG, "RS Received PROBE length %d.\n", length); //logMessage(LOG_INFO, "Synch'ing with Aqualink master device...\n"); rtn = false; break; default: - logMessage(LOG_INFO, "RS Received unknown packet, 0x%02hhx\n",packet[PKT_CMD]); + logMessage(LOG_INFO, "RS Received unknown packet, 0x%02hhx\n", packet[PKT_CMD]); rtn = false; break; } - + return rtn; } @@ -739,42 +545,64 @@ void action_delayed_request() if (_aqualink_data.temp_units == UNKNOWN && _aqualink_data.unactioned.type != SWG_SETPOINT) return; - if (_aqualink_data.unactioned.type == POOL_HTR_SETOINT) { + if (_aqualink_data.unactioned.type == POOL_HTR_SETOINT) + { _aqualink_data.unactioned.value = setpoint_check(POOL_HTR_SETOINT, _aqualink_data.unactioned.value, &_aqualink_data); - if ( _aqualink_data.pool_htr_set_point != _aqualink_data.unactioned.value ) { + if (_aqualink_data.pool_htr_set_point != _aqualink_data.unactioned.value) + { aq_programmer(AQ_SET_POOL_HEATER_TEMP, sval, &_aqualink_data); - logMessage(LOG_NOTICE, "Setting pool heater setpoint to %d\n",_aqualink_data.unactioned.value); - } else { - logMessage(LOG_NOTICE, "Pool heater setpoint is already %d, not changing\n",_aqualink_data.unactioned.value); + logMessage(LOG_NOTICE, "Setting pool heater setpoint to %d\n", _aqualink_data.unactioned.value); } - } else if (_aqualink_data.unactioned.type == SPA_HTR_SETOINT) { + else + { + logMessage(LOG_NOTICE, "Pool heater setpoint is already %d, not changing\n", _aqualink_data.unactioned.value); + } + } + else if (_aqualink_data.unactioned.type == SPA_HTR_SETOINT) + { _aqualink_data.unactioned.value = setpoint_check(SPA_HTR_SETOINT, _aqualink_data.unactioned.value, &_aqualink_data); - if ( _aqualink_data.spa_htr_set_point != _aqualink_data.unactioned.value ) { + if (_aqualink_data.spa_htr_set_point != _aqualink_data.unactioned.value) + { aq_programmer(AQ_SET_SPA_HEATER_TEMP, sval, &_aqualink_data); - logMessage(LOG_NOTICE, "Setting spa heater setpoint to %d\n",_aqualink_data.unactioned.value); - } else { - logMessage(LOG_NOTICE, "Spa heater setpoint is already %d, not changing\n",_aqualink_data.unactioned.value); + logMessage(LOG_NOTICE, "Setting spa heater setpoint to %d\n", _aqualink_data.unactioned.value); } - } else if (_aqualink_data.unactioned.type == FREEZE_SETPOINT) { + else + { + logMessage(LOG_NOTICE, "Spa heater setpoint is already %d, not changing\n", _aqualink_data.unactioned.value); + } + } + else if (_aqualink_data.unactioned.type == FREEZE_SETPOINT) + { _aqualink_data.unactioned.value = setpoint_check(FREEZE_SETPOINT, _aqualink_data.unactioned.value, &_aqualink_data); - if ( _aqualink_data.frz_protect_set_point != _aqualink_data.unactioned.value ) { + if (_aqualink_data.frz_protect_set_point != _aqualink_data.unactioned.value) + { aq_programmer(AQ_SET_FRZ_PROTECTION_TEMP, sval, &_aqualink_data); - logMessage(LOG_NOTICE, "Setting freeze protect to %d\n",_aqualink_data.unactioned.value); - } else { - logMessage(LOG_NOTICE, "Freeze setpoint is already %d, not changing\n",_aqualink_data.unactioned.value); + logMessage(LOG_NOTICE, "Setting freeze protect to %d\n", _aqualink_data.unactioned.value); } - } else if (_aqualink_data.unactioned.type == SWG_SETPOINT) { + else + { + logMessage(LOG_NOTICE, "Freeze setpoint is already %d, not changing\n", _aqualink_data.unactioned.value); + } + } + else if (_aqualink_data.unactioned.type == SWG_SETPOINT) + { _aqualink_data.unactioned.value = setpoint_check(SWG_SETPOINT, _aqualink_data.unactioned.value, &_aqualink_data); - if (_aqualink_data.ar_swg_status == SWG_STATUS_OFF ) { + if (_aqualink_data.ar_swg_status == SWG_STATUS_OFF) + { // SWG is off, can't set %, so delay the set until it's on. _aqualink_data.swg_delayed_percent = _aqualink_data.unactioned.value; - } else { - if ( _aqualink_data.swg_percent != _aqualink_data.unactioned.value ) { + } + else + { + if (_aqualink_data.swg_percent != _aqualink_data.unactioned.value) + { aq_programmer(AQ_SET_SWG_PERCENT, sval, &_aqualink_data); - logMessage(LOG_NOTICE, "Setting SWG %% to %d\n",_aqualink_data.unactioned.value); - } else { - logMessage(LOG_NOTICE, "SWG % is already %d, not changing\n",_aqualink_data.unactioned.value); - } + logMessage(LOG_NOTICE, "Setting SWG %% to %d\n", _aqualink_data.unactioned.value); + } + else + { + logMessage(LOG_NOTICE, "SWG % is already %d, not changing\n", _aqualink_data.unactioned.value); + } } // Let's just tell everyone we set it, before we actually did. Makes homekit happy, and it will re-correct on error. _aqualink_data.swg_percent = _aqualink_data.unactioned.value; @@ -785,20 +613,22 @@ void action_delayed_request() _aqualink_data.unactioned.requested = 0; } -int main(int argc, char *argv[]) { +int main(int argc, char *argv[]) +{ // main_loop (); int i; char *cfgFile = DEFAULT_CONFIG_FILE; int cmdln_loglevel = -1; - + bool cmdln_debugRS485 = false; // struct lws_context_creation_info info; // Log only NOTICE messages and above. Debug and info messages // will not be logged to syslog. setlogmask(LOG_UPTO(LOG_NOTICE)); - if (getuid() != 0) { + if (getuid() != 0) + { //logMessage(LOG_ERR, "%s Can only be run as root\n", argv[0]); fprintf(stderr, "ERROR %s Can only be run as root\n", argv[0]); return EXIT_FAILURE; @@ -807,14 +637,24 @@ int main(int argc, char *argv[]) { // Initialize the daemon's parameters. init_parameters(&_config_parameters); - for (i = 1; i < argc; i++) { - if (strcmp(argv[i], "-d") == 0) { + for (i = 1; i < argc; i++) + { + if (strcmp(argv[i], "-d") == 0) + { _config_parameters.deamonize = false; - } else if (strcmp(argv[i], "-c") == 0) { + } + else if (strcmp(argv[i], "-c") == 0) + { cfgFile = argv[++i]; - } else if (strcmp(argv[i], "-v") == 0) { + } + else if (strcmp(argv[i], "-v") == 0) + { cmdln_loglevel = LOG_DEBUG; } + else if (strcmp(argv[i], "-rsd") == 0) + { + cmdln_debugRS485 = true; + } } initButtons(&_aqualink_data); @@ -824,6 +664,9 @@ int main(int argc, char *argv[]) { if (cmdln_loglevel != -1) _config_parameters.log_level = cmdln_loglevel; + if (cmdln_debugRS485) + _config_parameters.debug_RSProtocol_packets = true; + setLoggingPrms(_config_parameters.log_level, _config_parameters.deamonize, _config_parameters.log_file); logMessage(LOG_NOTICE, "%s v%s\n", AQUALINKD_NAME, AQUALINKD_VERSION); @@ -834,6 +677,7 @@ int main(int argc, char *argv[]) { logMessage(LOG_NOTICE, "Config web_directory = %s\n", _config_parameters.web_directory); logMessage(LOG_NOTICE, "Config device_id = 0x%02hhx\n", _config_parameters.device_id); logMessage(LOG_NOTICE, "Config read_all_devices = %s\n", bool2text(_config_parameters.read_all_devices)); + logMessage(LOG_NOTICE, "Config use_aux_labels = %s\n", bool2text(_config_parameters.use_panel_aux_labels)); logMessage(LOG_NOTICE, "Config override frz prot = %s\n", bool2text(_config_parameters.override_freeze_protect)); #ifndef MG_DISABLE_MQTT logMessage(LOG_NOTICE, "Config mqtt_server = %s\n", _config_parameters.mqtt_server); @@ -849,6 +693,8 @@ int main(int argc, char *argv[]) { logMessage(LOG_NOTICE, "Config idx SWG Percent = %d\n", _config_parameters.dzidx_swg_percent); logMessage(LOG_NOTICE, "Config idx SWG PPM = %d\n", _config_parameters.dzidx_swg_ppm); logMessage(LOG_NOTICE, "Config PDA Mode = %s\n", bool2text(_config_parameters.pda_mode)); + logMessage(LOG_NOTICE, "Config PDA Sleep Mode = %s\n", bool2text(_config_parameters.pda_sleep_mode)); + logMessage(LOG_NOTICE, "Config force SWG = %s\n", bool2text(_config_parameters.force_swg)); /* removed until domoticz has a better virtual thermostat logMessage(LOG_NOTICE, "Config idx pool thermostat = %d\n", _config_parameters.dzidx_pool_thermostat); logMessage(LOG_NOTICE, "Config idx spa thermostat = %d\n", _config_parameters.dzidx_spa_thermostat); @@ -857,60 +703,72 @@ int main(int argc, char *argv[]) { logMessage(LOG_NOTICE, "Config deamonize = %s\n", bool2text(_config_parameters.deamonize)); logMessage(LOG_NOTICE, "Config log_file = %s\n", _config_parameters.log_file); logMessage(LOG_NOTICE, "Config light_pgm_mode = %.2f\n", _config_parameters.light_programming_mode); + logMessage(LOG_NOTICE, "Debug RS485 protocol = %s\n", bool2text(_config_parameters.debug_RSProtocol_packets)); // logMessage (LOG_NOTICE, "Config serial_port = %s\n", config_parameters->serial_port); - for (i=0; i < TOTAL_BUTONS; i++) { - logMessage(LOG_NOTICE, "Config BTN %-13s = label %-15s | PDAlabel %-15s | dzidx %d\n", _aqualink_data.aqbuttons[i].name, _aqualink_data.aqbuttons[i].label , _aqualink_data.aqbuttons[i].pda_label, _aqualink_data.aqbuttons[i].dz_idx); + for (i = 0; i < TOTAL_BUTONS; i++) + { + logMessage(LOG_NOTICE, "Config BTN %-13s = label %-15s | PDAlabel %-15s | dzidx %d\n", _aqualink_data.aqbuttons[i].name, _aqualink_data.aqbuttons[i].label, _aqualink_data.aqbuttons[i].pda_label, _aqualink_data.aqbuttons[i].dz_idx); //logMessage(LOG_NOTICE, "Button %d\n", i+1, _aqualink_data.aqbuttons[i].label , _aqualink_data.aqbuttons[i].dz_idx); } - if (_config_parameters.deamonize == true) { + if (_config_parameters.deamonize == true) + { char pidfile[256]; // sprintf(pidfile, "%s/%s.pid",PIDLOCATION, basename(argv[0])); sprintf(pidfile, "%s/%s.pid", "/run", basename(argv[0])); daemonise(pidfile, main_loop); - } else { + } + else + { main_loop(); } exit(EXIT_SUCCESS); } - void debugPacketPrint(unsigned char ID, unsigned char *packet_buffer, int packet_length) { char buff[1000]; - int i=0; + int i = 0; int cnt = 0; - cnt = sprintf(buff, "%4.4s 0x%02hhx of type %8.8s", (packet_buffer[PKT_DEST]==0x00?"From":"To"), ID, get_packet_type(packet_buffer, packet_length)); - cnt += sprintf(buff+cnt, " | HEX: "); + cnt = sprintf(buff, "%4.4s 0x%02hhx of type %8.8s", (packet_buffer[PKT_DEST] == 0x00 ? "From" : "To"), ID, get_packet_type(packet_buffer, packet_length)); + cnt += sprintf(buff + cnt, " | HEX: "); //printHex(packet_buffer, packet_length); - for (i=0;i 0) { + // Send command and jump directly "busy but can receive message" + send_ack(rs_fd, pop_aq_cmd(&_aqualink_data)); + delayAckCnt = MAX_BUSY_ACK; // need to test jumping to MAX_BUSY_ACK here + } else { + logMessage(LOG_NOTICE, "Sending display busy due to Simulator mode \n"); + if (delayAckCnt < MAX_BLOCK_ACK) // block all incomming messages + send_extended_ack(rs_fd, ACK_SCREEN_BUSY_BLOCK, pop_aq_cmd(&_aqualink_data)); + else if (delayAckCnt < MAX_BUSY_ACK) // say we are pausing + send_extended_ack(rs_fd, ACK_SCREEN_BUSY, pop_aq_cmd(&_aqualink_data)); + else // We timed out pause, send normal ack (This should also reset the display message on next message received) + send_ack(rs_fd, pop_aq_cmd(&_aqualink_data)); + + delayAckCnt++; + } + } +} + +void main_loop() +{ struct mg_mgr mgr; int rs_fd; int packet_length; unsigned char packet_buffer[AQ_MAXPKTLEN]; bool interestedInNextAck; - int delayAckCnt = 0; + //int delayAckCnt = 0; // NSF need to find a better place to init this. //_aqualink_data.aq_command = 0x00; @@ -962,8 +867,11 @@ void main_loop() { _aqualink_data.service_mode_state = OFF; _aqualink_data.battery = OK; + if (_config_parameters.force_swg == true) + _aqualink_data.swg_percent = 0; - if (!start_net_services(&mgr, &_aqualink_data, &_config_parameters)) { + if (!start_net_services(&mgr, &_aqualink_data, &_config_parameters)) + { logMessage(LOG_ERR, "Can not start webserver on port %s.\n", _config_parameters.socket_port); exit(EXIT_FAILURE); } @@ -976,11 +884,16 @@ void main_loop() { logMessage(LOG_NOTICE, "Listening to Aqualink RS8 on serial port: %s\n", _config_parameters.serial_port); if (_config_parameters.pda_mode == true) - set_pda_mode(true); + { + init_pda(&_aqualink_data); + } - while (_keepRunning == true) { - while ((rs_fd < 0 || blank_read >= MAX_ZERO_READ_BEFORE_RECONNECT) && _keepRunning == true) { - if (rs_fd < 0) { + while (_keepRunning == true) + { + while ((rs_fd < 0 || blank_read >= MAX_ZERO_READ_BEFORE_RECONNECT) && _keepRunning == true) + { + if (rs_fd < 0) + { // sleep(1); sprintf(_aqualink_data.last_display_message, CONNECTION_ERROR); logMessage(LOG_ERR, "Aqualink daemon attempting to connect to master device...\n"); @@ -988,7 +901,9 @@ void main_loop() { mg_mgr_poll(&mgr, 1000); // Sevice messages mg_mgr_poll(&mgr, 3000); // should donothing for 3 seconds. // broadcast_aqualinkstate_error(mgr.active_connections, "No connection to RS control panel"); - } else { + } + else + { logMessage(LOG_ERR, "Aqualink daemon looks like serial error, resetting.\n"); } rs_fd = init_serial_port(_config_parameters.serial_port); @@ -996,99 +911,84 @@ void main_loop() { } packet_length = get_packet(rs_fd, packet_buffer); - if (packet_length == -1) { + if (packet_length == -1) + { // Unrecoverable read error. Force an attempt to reconnect. logMessage(LOG_ERR, "Bad packet length, reconnecting\n"); blank_read = MAX_ZERO_READ_BEFORE_RECONNECT; - } else if (packet_length == 0) { + } + else if (packet_length == 0) + { //logMessage(LOG_DEBUG_SERIAL, "Nothing read on serial\n"); blank_read++; - } else if (packet_length > 0) { + } + else if (packet_length > 0) + { blank_read = 0; -//debugPacket(packet_buffer, packet_length); + if (_config_parameters.debug_RSProtocol_packets) logPacket(packet_buffer, packet_length); + + if (packet_length > 0 && packet_buffer[PKT_DEST] == _config_parameters.device_id) + { - if (packet_length > 0 && packet_buffer[PKT_DEST] == _config_parameters.device_id) { - /* - send_ack(rs_fd, _aqualink_data.aq_command); - _aqualink_data.aq_command = NUL; - */ if (getLogLevel() >= LOG_DEBUG) - logMessage(LOG_DEBUG, "RS received packet of type %s length %d\n", get_packet_type(packet_buffer , packet_length), packet_length); + logMessage(LOG_DEBUG, "RS received packet of type %s length %d\n", get_packet_type(packet_buffer, packet_length), packet_length); - // **** NSF (Taken out while playing with Panel Simulator, put back in. ************) - // send_ack(rs_fd, pop_aq_cmd(&_aqualink_data)); -#define MAX_BLOCK_ACK 12 -#define MAX_BUSY_ACK (50 + MAX_BLOCK_ACK) - // Wrap the mess just for sanity, the pre-process will clean it up. - if (! _aqualink_data.simulate_panel || - _aqualink_data.active_thread.thread_id != 0) - { - // Can only send command to status message on PDA. - if (_config_parameters.pda_mode == true && packet_buffer[PKT_CMD] != CMD_STATUS) - send_ack(rs_fd, NUL); - else - send_ack(rs_fd, pop_aq_cmd(&_aqualink_data)); - } else { // We are in simlator mode, ack get's complicated now. - // If have a command to send, send a normal ack. - // If we last message is waiting for an input "SELECT xxxxx", then sent a pause ack - // pause ack strarts with around 12 ACK_SCREEN_BUSY_DISPLAY acks, then 50 ACK_SCREEN_BUSY acks - // if we send a command (ie keypress), the whole count needs to end and go back to sending normal ack. - // In code below, it jumps to sending ACK_SCREEN_BUSY, which still seems to work ok. - if ( strncasecmp(_aqualink_data.last_display_message, "SELECT", 6) != 0) - { // Nothing to wait for, send normal ack. - send_ack(rs_fd, pop_aq_cmd(&_aqualink_data)); - delayAckCnt = 0; - } else if ( get_aq_cmd_length() > 0 ) { - // Send command and jump directly "busy but can receive message" - send_ack(rs_fd, pop_aq_cmd(&_aqualink_data)); - delayAckCnt = MAX_BUSY_ACK; // need to test jumping to MAX_BUSY_ACK here - } else { - logMessage(LOG_NOTICE, "Sending display busy due to Simulator mode \n"); - if (delayAckCnt < MAX_BLOCK_ACK) // block all incomming messages - send_extended_ack(rs_fd, ACK_SCREEN_BUSY_BLOCK, pop_aq_cmd(&_aqualink_data)); - else if (delayAckCnt < MAX_BUSY_ACK) // say we are pausing - send_extended_ack(rs_fd, ACK_SCREEN_BUSY, pop_aq_cmd(&_aqualink_data)); - else // We timed out pause, send normal ack (This should also reset the display message on next message received) - send_ack(rs_fd, pop_aq_cmd(&_aqualink_data)); - - delayAckCnt++; - } - } + //logMessage(LOG_DEBUG, "RS received packet of type %s length %d\n", get_packet_type(packet_buffer, packet_length), packet_length); + //debugPacketPrint(0x00, packet_buffer, packet_length); + //unsigned char ID, unsigned char *packet_buffer, int packet_length) // Process the packet. This includes deriving general status, and identifying // warnings and errors. If something changed, notify any listeners - if (process_packet(packet_buffer, packet_length) != false) { + if (process_packet(packet_buffer, packet_length) != false) + { broadcast_aqualinkstate(mgr.active_connections); } - } else if (packet_length > 0 && _config_parameters.read_all_devices == true) { + // If we are not in PDA or Simulator mode, just sent ACK & any CMD, else caculate the ACK. + if (!_aqualink_data.simulate_panel && !_config_parameters.pda_mode) + send_ack(rs_fd, pop_aq_cmd(&_aqualink_data)); + else + caculate_ack_packet(rs_fd, packet_buffer); + } + else if (packet_length > 0 && _config_parameters.read_all_devices == true) + { //logPacket(packet_buffer, packet_length); - if (packet_buffer[PKT_DEST] == DEV_MASTER && interestedInNextAck == true) { - if ( packet_buffer[PKT_CMD] == CMD_PPM ) { + if (packet_buffer[PKT_DEST] == DEV_MASTER && interestedInNextAck == true) + { + if (packet_buffer[PKT_CMD] == CMD_PPM) + { _aqualink_data.ar_swg_status = packet_buffer[5]; - if (_aqualink_data.swg_delayed_percent != TEMP_UNKNOWN && _aqualink_data.ar_swg_status == 0x00) { // We have a delayed % to set. + if (_aqualink_data.swg_delayed_percent != TEMP_UNKNOWN && _aqualink_data.ar_swg_status == 0x00) + { // We have a delayed % to set. char sval[10]; snprintf(sval, 9, "%d", _aqualink_data.swg_delayed_percent); aq_programmer(AQ_SET_SWG_PERCENT, sval, &_aqualink_data); - logMessage(LOG_NOTICE, "Setting SWG %% to %d, from delayed message\n",_aqualink_data.swg_delayed_percent); + logMessage(LOG_NOTICE, "Setting SWG %% to %d, from delayed message\n", _aqualink_data.swg_delayed_percent); _aqualink_data.swg_delayed_percent = TEMP_UNKNOWN; } _aqualink_data.swg_ppm = packet_buffer[4] * 100; } interestedInNextAck = false; - } else if (interestedInNextAck == true && packet_buffer[PKT_DEST] != DEV_MASTER && _aqualink_data.ar_swg_status != 0x00 ) { + } + else if (interestedInNextAck == true && packet_buffer[PKT_DEST] != DEV_MASTER && _aqualink_data.ar_swg_status != 0x00) + { _aqualink_data.ar_swg_status = SWG_STATUS_OFF; interestedInNextAck = false; - } else if (packet_buffer[PKT_DEST] == SWG_DEV_ID) { + } + else if (packet_buffer[PKT_DEST] == SWG_DEV_ID) + { interestedInNextAck = true; - if ( packet_buffer[3] == CMD_PERCENT && _aqualink_data.active_thread.thread_id == 0 ) { + if (packet_buffer[3] == CMD_PERCENT && _aqualink_data.active_thread.thread_id == 0) + { // Only read SWG Percent if we are not programming, as we might be changing this _aqualink_data.swg_percent = (int)packet_buffer[4]; } - } else { + } + else + { interestedInNextAck = false; } } @@ -1097,15 +997,16 @@ void main_loop() { logMessage(LOG_DEBUG_SERIAL, "Received Packet for ID 0x%02hhx of type %s %s\n",packet_buffer[PKT_DEST], get_packet_type(packet_buffer, packet_length), (packet_buffer[PKT_DEST] == _config_parameters.device_id)?" <-- Aqualinkd ID":""); }*/ - } mg_mgr_poll(&mgr, 0); // Any unactioned commands - if (_aqualink_data.unactioned.type != NO_ACTION) { + if (_aqualink_data.unactioned.type != NO_ACTION) + { time_t now; time(&now); - if (difftime(now, _aqualink_data.unactioned.requested) > 2){ + if (difftime(now, _aqualink_data.unactioned.requested) > 2) + { logMessage(LOG_DEBUG, "Actioning delayed request\n"); action_delayed_request(); } @@ -1118,7 +1019,8 @@ void main_loop() { #endif //} } - + + if (_config_parameters.debug_RSProtocol_packets) closePacketLog(); // Reset and close the port. close_serial_port(rs_fd); // Clear webbrowser @@ -1129,4 +1031,3 @@ void main_loop() { logMessage(LOG_NOTICE, "Exit!\n"); exit(EXIT_FAILURE); } - diff --git a/config.c b/config.c index da0e5c9..5e5f6a2 100644 --- a/config.c +++ b/config.c @@ -75,11 +75,15 @@ void init_parameters (struct aqconfig * parms) parms->deamonize = true; parms->log_file = '\0'; parms->pda_mode = false; + parms->pda_sleep_mode = false; parms->convert_mqtt_temp = true; parms->convert_dz_temp = true; parms->report_zero_pool_temp = false; parms->report_zero_spa_temp = false; parms->read_all_devices = true; + parms->use_panel_aux_labels = false; + parms->debug_RSProtocol_packets = false; + parms->force_swg = false; generate_mqtt_id(parms->mqtt_ID, MQTT_ID_LEN); } @@ -377,6 +381,10 @@ bool setConfigValue(struct aqconfig *config_parameters, struct aqualinkdata *aqd config_parameters->pda_mode = text2bool(value); set_pda_mode(config_parameters->pda_mode); rtn=true; + } else if (strncasecmp(param, "pda_sleep_mode", 8) == 0) { + config_parameters->pda_sleep_mode = text2bool(value); + //set_pda_mode(config_parameters->pda_mode); + rtn=true; } else if (strncasecmp(param, "convert_mqtt_temp_to_c", 22) == 0) { config_parameters->convert_mqtt_temp = text2bool(value); rtn=true; @@ -396,7 +404,17 @@ bool setConfigValue(struct aqconfig *config_parameters, struct aqualinkdata *aqd } else if (strncasecmp (param, "read_all_devices", 16) == 0) { config_parameters->read_all_devices = text2bool(value); rtn=true; + } else if (strncasecmp (param, "use_panel_aux_labels", 20) == 0) { + config_parameters->use_panel_aux_labels = text2bool(value); + rtn=true; + } else if (strncasecmp (param, "force_SWG", 9) == 0) { + config_parameters->force_swg = text2bool(value); + rtn=true; + } else if (strncasecmp (param, "debug_RSProtocol_packets", 24) == 0) { + config_parameters->debug_RSProtocol_packets = text2bool(value); + rtn=true; } + // removed until domoticz has a better virtual thermostat /*else if (strncasecmp (param, "pool_thermostat_dzidx", 21) == 0) { config_parameters->dzidx_pool_thermostat = strtoul(value, NULL, 10); diff --git a/config.h b/config.h index 7ae6224..7e1e37b 100644 --- a/config.h +++ b/config.h @@ -50,12 +50,16 @@ struct aqconfig int light_programming_button; bool override_freeze_protect; bool pda_mode; + bool pda_sleep_mode; bool convert_mqtt_temp; bool convert_dz_temp; //bool flash_mqtt_buttons; bool report_zero_spa_temp; bool report_zero_pool_temp; bool read_all_devices; + bool use_panel_aux_labels; + bool force_swg; + bool debug_RSProtocol_packets; //int dzidx_pool_thermostat; // Domoticz virtual thermostats are crap removed until better //int dzidx_spa_thermostat; // Domoticz virtual thermostats are crap removed until better //char mqtt_pub_topic[250]; @@ -69,5 +73,6 @@ void init_parameters (struct aqconfig * parms); void readCfg (struct aqconfig *config_parameters, struct aqualinkdata *aqualink_data, char *cfgFile); bool writeCfg (struct aqconfig *config_parameters, struct aqualinkdata *aqdata); bool setConfigValue(struct aqconfig *config_parameters, struct aqualinkdata *aqdata, char *param, char *value); +char *cleanalloc(char*str); #endif diff --git a/net_services.c b/net_services.c index 168d37f..5838803 100644 --- a/net_services.c +++ b/net_services.c @@ -33,6 +33,7 @@ #include "json_messages.h" #include "domoticz.h" #include "aq_mqtt.h" +#include "pda.h" static struct aqconfig *_aqualink_config; @@ -761,7 +762,11 @@ void action_web_request(struct mg_connection *nc, struct http_message *http_msg) void action_websocket_request(struct mg_connection *nc, struct websocket_message *wm) { char buffer[50]; struct JSONwebrequest request; - + + // Any websocket request means UI is active, so don't let AqualinkD go to sleep if in PDA mode + if (pda_mode()) + pda_reset_sleep(); + strncpy(buffer, (char *)wm->data, wm->size); buffer[wm->size] = '\0'; // logMessage (LOG_DEBUG, "buffer '%s'\n", buffer); diff --git a/pda.c b/pda.c new file mode 100644 index 0000000..99cacfe --- /dev/null +++ b/pda.c @@ -0,0 +1,557 @@ + +#define _GNU_SOURCE 1 // for strcasestr & strptime +#include +#include +#include +#include + +#include "aqualink.h" +#include "init_buttons.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.5 seconds apart, so 2 mins = 120 seconds = 240 polls +#define PDA_LOOP_COUNT 240 // 2 mins in poll (sleep timer) + +void init_pda(struct aqualinkdata *aqdata) +{ + _aqualink_data = aqdata; + set_pda_mode(true); + //clock_gettime(CLOCK_REALTIME, &_aqualink_data->last_active_time); + //aq_programmer(AQ_PDA_INIT, NULL, _aqualink_data); // NEED TO MOVE THIS. Can't run this here incase serial connection fails / needs to be cleaned up. +} + + +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() { + +} + +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 + // + + // 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)); + 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)); + logMessage(LOG_DEBUG, "SALT = %d\n", _aqualink_data->swg_ppm); + } + 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` ' + _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 + { + _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]); + } + } +} + +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 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); + } +} + +void process_pda_packet_msg_long_spa_heat(const char *msg) +{ + if (strncmp(msg, " ENABLED ", 16) == 0) + { + _aqualink_data->aqbuttons[SPA_HEAT_INDEX].led->state = ENABLE; + } + else if (strncmp(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 (strncmp(msg, " ENABLED ", 16) == 0) + { + _aqualink_data->aqbuttons[POOL_HEAT_INDEX].led->state = ENABLE; + } + else if (strncmp(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 (strncmp(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 (strncmp(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 (strncmp(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_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; + static time_t _lastStatus = 0; + + if (_lastStatus == 0) + { + time(&_lastStatus); + } + + 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) + { + 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 (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)); + } + time(&_lastStatus); + } + else + { + time_t now; + time(&now); + if (init && difftime(now, _lastStatus) > 60) + { + logMessage(LOG_DEBUG, "OVER 60 SECONDS SINCE LAST STATUS UPDATE, forcing refresh\n"); + // Reset aquapure to nothing since it must be off at this point + _aqualink_data->pool_temp = TEMP_UNKNOWN; + _aqualink_data->spa_temp = TEMP_UNKNOWN; + time(&_lastStatus); + aq_programmer(AQ_PDA_DEVICE_STATUS, NULL, _aqualink_data); + } + } + 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_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); + } 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) + { + // We processed the next message, kick any threads waiting on the message. + kick_aq_program_thread(_aqualink_data); + } + return rtn; +} \ No newline at end of file diff --git a/pda.h b/pda.h new file mode 100644 index 0000000..88af360 --- /dev/null +++ b/pda.h @@ -0,0 +1,12 @@ + + +#ifndef PDA_H_ +#define PDA_H_ + +void init_pda(struct aqualinkdata *aqdata); +bool process_pda_packet(unsigned char* packet, int length); +bool pda_shouldSleep(); +void pda_wake(); +void pda_reset_sleep(); + +#endif // PDA_MESSAGES_H_ \ No newline at end of file diff --git a/pda_aq_programmer.c b/pda_aq_programmer.c new file mode 100644 index 0000000..e70b303 --- /dev/null +++ b/pda_aq_programmer.c @@ -0,0 +1,770 @@ +#include +#include +#include +#include +#include +#include + + +#include "aqualink.h" +#include "utils.h" +#include "aq_programmer.h" +#include "aq_serial.h" +#include "pda.h" +#include "pda_menu.h" +#include "pda_aq_programmer.h" + +#include "init_buttons.h" + +bool waitForPDAMessageHighlight(struct aqualinkdata *aq_data, int highlighIndex, int numMessageReceived); +bool waitForPDAMessageType(struct aqualinkdata *aq_data, unsigned char mtype, int numMessageReceived); +bool goto_pda_menu(struct aqualinkdata *aq_data, pda_menu_type menu); +bool wait_pda_selected_item(struct aqualinkdata *aq_data); +bool waitForPDAnextMenu(struct aqualinkdata *aq_data); +bool loopover_devices(struct aqualinkdata *aq_data); +bool find_pda_menu_item(struct aqualinkdata *aq_data, char *menuText, int charlimit); +bool select_pda_menu_item(struct aqualinkdata *aq_data, char *menuText, bool waitForNextMenu); + +static pda_type _PDA_Type; + +bool wait_pda_selected_item(struct aqualinkdata *aq_data) +{ + while (pda_m_hlightindex() == -1){ + waitForPDAMessageType(aq_data,CMD_PDA_HIGHLIGHT,10); + } + + if (pda_m_hlightindex() == -1) + return false; + else + return true; +} + +bool waitForPDAnextMenu(struct aqualinkdata *aq_data) { + waitForPDAMessageType(aq_data,CMD_PDA_CLEAR,10); + return waitForPDAMessageType(aq_data,CMD_PDA_HIGHLIGHT,15); +} + +bool loopover_devices(struct aqualinkdata *aq_data) { + int i; + + if (! goto_pda_menu(aq_data, PM_EQUIPTMENT_CONTROL)) { + //logMessage(LOG_ERR, "PDA :- can't find main menu\n"); + //cleanAndTerminateThread(threadCtrl); + return false; + } + + // Should look for message "ALL OFF", that's end of device list. + for (i=0; i < 18 && pda_find_m_index("ALL OFF") == -1 ; i++) { + send_cmd(KEY_PDA_DOWN, aq_data); + //while (get_aq_cmd_length() > 0) { delay(200); } + //waitForPDAMessageType(aq_data,CMD_PDA_HIGHLIGHT,3); + waitForMessage(aq_data, NULL, 1); + } + + return true; +} + +/* + if charlimit is set, use case insensitive match and limit chars. +*/ +bool find_pda_menu_item(struct aqualinkdata *aq_data, char *menuText, int charlimit) { + int i=pda_m_hlightindex(); + int index = (charlimit == 0)?pda_find_m_index(menuText):pda_find_m_index_case(menuText, charlimit); + + logMessage(LOG_DEBUG, "PDA Device programmer menu text '%s'\n",menuText); + + if (index < 0) { // No menu, is there a page down. "PDA Line 9 = ^^ MORE __" + if (strncmp(pda_m_line(9)," ^^ MORE", 10) == 0) { + int j; + for(j=0; j < 20; j++) { + send_cmd(KEY_PDA_DOWN, aq_data); + //delay(500); + //wait_for_empty_cmd_buffer(); + waitForPDAMessageType(aq_data,CMD_PDA_HIGHLIGHT,2); + //waitForMessage(aq_data, NULL, 1); + index = (charlimit == 0)?pda_find_m_index(menuText):pda_find_m_index_case(menuText, charlimit); + if (index >= 0) { + i=pda_m_hlightindex(); + break; + } + } + if (index < 0) { + logMessage(LOG_ERR, "PDA Device programmer couldn't find menu item on any page '%s'\n",menuText); + return false; + } + } else { + logMessage(LOG_ERR, "PDA Device programmer couldn't find menu item '%s'\n",menuText); + return false; + } + } + + // Found the text we want in the menu, now move to that position and select it. + //logMessage(LOG_DEBUG, "******************PDA Device programmer menu text '%s' is at index %d\n",menuText, index); + + if (i < index) { + for (i=pda_m_hlightindex(); i < index; i++) { + //logMessage(LOG_DEBUG, "******************PDA queue down index %d\n",i); + send_cmd(KEY_PDA_DOWN, aq_data); + } + } else if (i > index) { + for (i=pda_m_hlightindex(); i > index; i--) { + //logMessage(LOG_DEBUG, "******************PDA queue down index %d\n",i); + send_cmd(KEY_PDA_UP, aq_data); + } + } + + return waitForPDAMessageHighlight(aq_data, index, 10); +} + +bool select_pda_menu_item(struct aqualinkdata *aq_data, char *menuText, bool waitForNextMenu) { + + if ( find_pda_menu_item(aq_data, menuText, 0) ) { + send_cmd(KEY_PDA_SELECT, aq_data); + + logMessage(LOG_DEBUG, "PDA Device programmer selected menu item '%s'\n",menuText); + if (waitForNextMenu) + waitForPDAnextMenu(aq_data); + + return true; + } + + logMessage(LOG_ERR, "PDA Device programmer couldn't selected menu item '%s' at index %d\n",menuText, index); + return false; +} + +bool goto_pda_menu(struct aqualinkdata *aq_data, pda_menu_type menu) { + //int i = 0; + //char *menuText; + + logMessage(LOG_DEBUG, "PDA Device programmer request for menu %d\n",menu); + + // Keep going back, checking each time to get to home. + while (pda_m_type() == PM_FW_VERSION || pda_m_type() == PM_BUILDING_HOME) { + //logMessage(LOG_DEBUG, "******************PDA Device programmer delay on firmware or building home menu\n"); + delay(500); + } + + while ( pda_m_type() != menu && pda_m_type() != PM_HOME ) { + if (pda_m_type() != PM_BUILDING_HOME) { + send_cmd(KEY_PDA_BACK, aq_data); + //logMessage(LOG_DEBUG, "******************PDA Device programmer selected back button\n",menu); + waitForPDAnextMenu(aq_data); + } else { + waitForPDAMessageType(aq_data,CMD_PDA_HIGHLIGHT,15); + } + //logMessage(LOG_DEBUG, "******************PDA Device programmer menu type %d\n",pda_m_type()); + //if (!wait_for_empty_cmd_buffer() || i++ > 6) + // return false; + } + + if (pda_m_type() == menu) + return true; + + switch (menu) { + //case PM_SYSTEM_SETUP: + // select_pda_menu_item(aq_data, "MENU"); + //break; + case PM_EQUIPTMENT_CONTROL: + select_pda_menu_item(aq_data, "EQUIPMENT ON/OFF", true); + break; + case PM_PALM_OPTIONS: + select_pda_menu_item(aq_data, "MENU", true); + select_pda_menu_item(aq_data, "PALM OPTIONS", true); + case PM_AUX_LABEL: + if ( _PDA_Type == PDA) { + select_pda_menu_item(aq_data, "MENU", true); + select_pda_menu_item(aq_data, "SYSTEM SETUP", true); // This is a guess, (I have rev#) + select_pda_menu_item(aq_data, "LABEL AUX", true); + } else { + logMessage(LOG_ERR, "PDA in AquaPlalm mode, there is no SYSTEM SETUP / LABEL AUX menu\n"); + } + break; + case PM_SYSTEM_SETUP: + if ( _PDA_Type == PDA) { + select_pda_menu_item(aq_data, "MENU", true); + select_pda_menu_item(aq_data, "SYSTEM SETUP", true); // This is a guess, (I have rev#) + } else { + logMessage(LOG_ERR, "PDA in AquaPlalm mode, there is no SYSTEM SETUP menu\n"); + } + break; + case PM_FREEZE_PROTECT: + if ( _PDA_Type == PDA) { + select_pda_menu_item(aq_data, "MENU", true); + select_pda_menu_item(aq_data, "SYSTEM SETUP", true); // This is a guess, (I have rev#) + select_pda_menu_item(aq_data, "FREEZE PROTECT", true); // This is a guess, (I have rev#) + } else { + logMessage(LOG_ERR, "PDA in AquaPlalm mode, there is no SYSTEM SETUP / FREEZE PROTECT menu\n"); + } + break; + case PM_AQUAPURE: + select_pda_menu_item(aq_data, "MENU", true); + select_pda_menu_item(aq_data, "SET AquaPure", true); + //select_pda_menu_item(aq_data, "LABEL AUX"); + break; + case PM_SET_TEMP: + select_pda_menu_item(aq_data, "MENU", true); + select_pda_menu_item(aq_data, "SET TEMP", true); + //select_pda_menu_item(aq_data, "LABEL AUX"); + break; + case PM_SET_TIME: + select_pda_menu_item(aq_data, "MENU", true); + select_pda_menu_item(aq_data, "SET TIME", true); + //select_pda_menu_item(aq_data, "LABEL AUX"); + break; + default: + logMessage(LOG_ERR, "PDA Device programmer didn't understand requested menu\n"); + return false; + break; + } + + if (pda_m_type() != menu) { + logMessage(LOG_ERR, "PDA Device programmer didn't find a requested menu\n"); + return true; + } + + //logMessage(LOG_DEBUG, "******************PDA Device programmer request for menu %d found\n",menu); + + return true; +} + + + +void *set_aqualink_PDA_device_on_off( void *ptr ) +{ + struct programmingThreadCtrl *threadCtrl; + threadCtrl = (struct programmingThreadCtrl *) ptr; + struct aqualinkdata *aq_data = threadCtrl->aq_data; + //int i=0; + //int found; + char device_name[15]; + + waitForSingleThreadOrTerminate(threadCtrl, AQ_PDA_DEVICE_STATUS); + + char *buf = (char*)threadCtrl->thread_args; + int device = atoi(&buf[0]); + int state = atoi(&buf[5]); + + if (device < 0 || device > TOTAL_BUTTONS) { + logMessage(LOG_ERR, "PDA Device On/Off :- bad device number '%d'\n",device); + cleanAndTerminateThread(threadCtrl); + return ptr; + } + + logMessage(LOG_INFO, "PDA Device On/Off, device '%s', state %d\n",aq_data->aqbuttons[device].pda_label,state); + + if (! goto_pda_menu(aq_data, PM_EQUIPTMENT_CONTROL)) { + logMessage(LOG_ERR, "PDA Device On/Off :- can't find main menu\n"); + cleanAndTerminateThread(threadCtrl); + return ptr; + } + + //Pad name with spaces so something like "SPA" doesn't match "SPA BLOWER" + sprintf(device_name,"%-14s\n",aq_data->aqbuttons[device].pda_label); + if ( find_pda_menu_item(aq_data, device_name, 13) ) { + if (aq_data->aqbuttons[device].led->state != state) { + //printf("*** Select State ***\n"); + logMessage(LOG_INFO, "PDA Device On/Off, found device '%s', changing state\n",aq_data->aqbuttons[device].pda_label,state); + send_cmd(KEY_PDA_SELECT, aq_data); + while (get_aq_cmd_length() > 0) { delay(500); } + } else { + logMessage(LOG_INFO, "PDA Device On/Off, found device '%s', not changing state, is same\n",aq_data->aqbuttons[device].pda_label,state); + } + } else { + logMessage(LOG_ERR, "PDA Device On/Off, device '%s' not found\n",aq_data->aqbuttons[device].pda_label); + } + + goto_pda_menu(aq_data, PM_HOME); + //while (_pgm_command != NUL) { delay(500); } + + cleanAndTerminateThread(threadCtrl); + + // just stop compiler error, ptr is not valid as it's just been freed + return ptr; + +} + + + +void *get_aqualink_PDA_device_status( void *ptr ) +{ + struct programmingThreadCtrl *threadCtrl; + threadCtrl = (struct programmingThreadCtrl *) ptr; + struct aqualinkdata *aq_data = threadCtrl->aq_data; + //int i; + + waitForSingleThreadOrTerminate(threadCtrl, AQ_PDA_DEVICE_STATUS); + + if (! loopover_devices(aq_data)) { + logMessage(LOG_ERR, "PDA Device Status :- failed\n"); + } + + goto_pda_menu(aq_data, PM_HOME); + + cleanAndTerminateThread(threadCtrl); + + // just stop compiler error, ptr is not valid as it's just been freed + return ptr; +} + +void *set_aqualink_PDA_init( void *ptr ) +{ + struct programmingThreadCtrl *threadCtrl; + threadCtrl = (struct programmingThreadCtrl *) ptr; + struct aqualinkdata *aq_data = threadCtrl->aq_data; + //int i=0; + + waitForSingleThreadOrTerminate(threadCtrl, AQ_PDA_INIT); + + //int val = atoi((char*)threadCtrl->thread_args); + + //logMessage(LOG_DEBUG, "PDA Init\n", val); + + logMessage(LOG_DEBUG, "PDA Init\n"); + + if (pda_m_type() == PM_FW_VERSION) { + // check pda_m_line(1) to "AquaPalm" + if (strstr(pda_m_line(1), "AquaPalm") != NULL) { + _PDA_Type = AQUAPALM; + } else { + _PDA_Type = PDA; + } + char *ptr = pda_m_line(5); + ptr[AQ_MSGLEN+1] = '\0'; + strcpy(aq_data->version, stripwhitespace(ptr)); + } + + // Get status of all devices + if (! loopover_devices(aq_data)) { + logMessage(LOG_ERR, "PDA Init :- can't find menu\n"); + } + + // Get heater setpoints + if (! get_PDA_aqualink_pool_spa_heater_temps(aq_data)) { + logMessage(LOG_ERR, "PDA Init :- Error getting heater setpoints\n"); + } + + // Get freeze protect setpoint, AquaPalm doesn't have freeze protect in menu. + if (_PDA_Type != AQUAPALM && ! get_PDA_freeze_protect_temp(aq_data)) { + logMessage(LOG_ERR, "PDA Init :- Error getting freeze setpoints\n"); + } + + + goto_pda_menu(aq_data, PM_HOME); + + pda_reset_sleep(); + + cleanAndTerminateThread(threadCtrl); + + // just stop compiler error, ptr is not valid as it's just been freed + return ptr; +} + + +void *set_aqualink_PDA_wakeinit( void *ptr ) +{ + struct programmingThreadCtrl *threadCtrl; + threadCtrl = (struct programmingThreadCtrl *) ptr; + struct aqualinkdata *aq_data = threadCtrl->aq_data; + //int i=0; + + // At this point, we should probably just exit if there is a thread already going as + // it means the wake was called due to changing a device. + waitForSingleThreadOrTerminate(threadCtrl, AQ_PDA_INIT); + + logMessage(LOG_DEBUG, "PDA Wake Init\n"); + + // Get status of all devices + if (! loopover_devices(aq_data)) { + logMessage(LOG_ERR, "PDA Init :- can't find menu\n"); + } + + cleanAndTerminateThread(threadCtrl); + + // just stop compiler error, ptr is not valid as it's just been freed + return ptr; +} + + +bool get_PDA_freeze_protect_temp(struct aqualinkdata *aq_data) { + + if ( _PDA_Type == PDA) { + if (! goto_pda_menu(aq_data, PM_FREEZE_PROTECT)) { + return false; + } + } else { + logMessage(LOG_INFO, "In PDA AquaPalm mode, freezepoints not supported\n"); + return false; + } + + return true; +} + +bool get_PDA_aqualink_pool_spa_heater_temps(struct aqualinkdata *aq_data) { + + // Get heater setpoints + if (! goto_pda_menu(aq_data, PM_SET_TEMP)) { + return false; + } + + return true; +} + +bool waitForPDAMessageHighlight(struct aqualinkdata *aq_data, int highlighIndex, int numMessageReceived) +{ + logMessage(LOG_DEBUG, "waitForPDAMessageHighlight index %d\n",highlighIndex); + + if(pda_m_hlightindex() == highlighIndex) return true; + + int i=0; + pthread_mutex_init(&aq_data->active_thread.thread_mutex, NULL); + pthread_mutex_lock(&aq_data->active_thread.thread_mutex); + + while( ++i <= numMessageReceived) + { + logMessage(LOG_DEBUG, "waitForPDAMessageHighlight last = 0x%02hhx : index %d : (%d of %d)\n",aq_data->last_packet_type,pda_m_hlightindex(),i,numMessageReceived); + + if (aq_data->last_packet_type == CMD_PDA_HIGHLIGHT && pda_m_hlightindex() == highlighIndex) break; + + pthread_cond_init(&aq_data->active_thread.thread_cond, NULL); + pthread_cond_wait(&aq_data->active_thread.thread_cond, &aq_data->active_thread.thread_mutex); + } + + pthread_mutex_unlock(&aq_data->active_thread.thread_mutex); + + if (pda_m_hlightindex() != highlighIndex) { + //logMessage(LOG_ERR, "Could not select MENU of Aqualink control panel\n"); + logMessage(LOG_DEBUG, "waitForPDAMessageHighlight: did not receive index '%d'\n",highlighIndex); + return false; + } else + logMessage(LOG_DEBUG, "waitForPDAMessageHighlight: received index '%d'\n",highlighIndex); + + return true; +} + + +bool waitForPDAMessageType(struct aqualinkdata *aq_data, unsigned char mtype, int numMessageReceived) +{ + logMessage(LOG_DEBUG, "waitForPDAMessageType 0x%02hhx\n",mtype); + + int i=0; + pthread_mutex_init(&aq_data->active_thread.thread_mutex, NULL); + pthread_mutex_lock(&aq_data->active_thread.thread_mutex); + + while( ++i <= numMessageReceived) + { + logMessage(LOG_DEBUG, "waitForPDAMessageType 0x%02hhx, last message type was 0x%02hhx (%d of %d)\n",mtype,aq_data->last_packet_type,i,numMessageReceived); + + if (aq_data->last_packet_type == mtype) break; + + pthread_cond_init(&aq_data->active_thread.thread_cond, NULL); + pthread_cond_wait(&aq_data->active_thread.thread_cond, &aq_data->active_thread.thread_mutex); + } + + pthread_mutex_unlock(&aq_data->active_thread.thread_mutex); + + if (aq_data->last_packet_type != mtype) { + //logMessage(LOG_ERR, "Could not select MENU of Aqualink control panel\n"); + logMessage(LOG_DEBUG, "waitForPDAMessageType: did not receive 0x%02hhx\n",mtype); + return false; + } else + logMessage(LOG_DEBUG, "waitForPDAMessageType: received 0x%02hhx\n",mtype); + + return true; +} + +bool set_PDA_numeric_field_value(struct aqualinkdata *aq_data, int val, int *cur_val, char *select_label, int step) { + int i; + + // Should probably change below to call find_pda_menu_item(), rather than doing it here + // If we lease this, need to limit on the number of loops + while ( strncmp(pda_m_hlight(), select_label, 8) != 0 ) { + send_cmd(KEY_PDA_DOWN, aq_data); + delay(500); // Last message probably was CMD_PDA_HIGHLIGHT, so wait before checking. + waitForPDAMessageType(aq_data,CMD_PDA_HIGHLIGHT,2); + } + + send_cmd(KEY_PDA_SELECT, aq_data); + + if (val < *cur_val) { + logMessage(LOG_DEBUG, "PDA %s value : lower from %d to %d\n", select_label, *cur_val, val); + for (i = *cur_val; i > val; i=i-step) { + send_cmd(KEY_PDA_DOWN, aq_data); + } + } else if (val > *cur_val) { + logMessage(LOG_DEBUG, "PDA %s value : raise from %d to %d\n", select_label, *cur_val, val); + for (i = *cur_val; i < val; i=i+step) { + send_cmd(KEY_PDA_UP, aq_data); + } + } else { + logMessage(LOG_INFO, "PDA %s value : already at %d\n", select_label, val); + send_cmd(KEY_PDA_BACK, aq_data); + return true; + } + + send_cmd(KEY_PDA_SELECT, aq_data); + logMessage(LOG_DEBUG, "PDA %s value : set to %d\n", select_label, *cur_val); + + return true; +} + +bool set_PDA_aqualink_SWG_setpoint(struct aqualinkdata *aq_data, int val) { + + if (! goto_pda_menu(aq_data, PM_AQUAPURE)) { + logMessage(LOG_ERR, "Error getting setpoints menu\n"); + } + + if (aq_data->aqbuttons[SPA_INDEX].led->state != OFF) + set_PDA_numeric_field_value(aq_data, val, &aq_data->swg_percent, "SET SPA", 5); + else + set_PDA_numeric_field_value(aq_data, val, &aq_data->swg_percent, "SET POOL", 5); + + return true; +} + +bool set_PDA_aqualink_heater_setpoint(struct aqualinkdata *aq_data, int val, bool isPool) { + char label[10]; + int *cur_val; + + if (isPool) { + sprintf(label, "POOL HEAT"); + cur_val = &aq_data->pool_htr_set_point; + } else { + sprintf(label, "SPA HEAT"); + cur_val = &aq_data->spa_htr_set_point; + } + + if (val == *cur_val) { + logMessage(LOG_INFO, "PDA %s setpoint : temp already %d\n", label, val); + send_cmd(KEY_PDA_BACK, aq_data); + return true; + } + + if (! goto_pda_menu(aq_data, PM_SET_TEMP)) { + logMessage(LOG_ERR, "Error getting setpoints menu\n"); + } + + set_PDA_numeric_field_value(aq_data, val, cur_val, label, 1); + + return true; +} + +bool set_PDA_aqualink_freezeprotect_setpoint(struct aqualinkdata *aq_data, int val) { + + if (! goto_pda_menu(aq_data, PM_FREEZE_PROTECT)) { + logMessage(LOG_ERR, "Error getting setpoints menu\n"); + } + + set_PDA_numeric_field_value(aq_data, val, &aq_data->frz_protect_set_point, "TEMP", 1); + + return true; +} + + +/* +bool waitForPDAMessage(struct aqualinkdata *aq_data, int numMessageReceived, unsigned char packettype) +{ + logMessage(LOG_DEBUG, "waitForPDAMessage %s %d\n",message,numMessageReceived); + int i=0; + pthread_mutex_init(&aq_data->active_thread.thread_mutex, NULL); + pthread_mutex_lock(&aq_data->active_thread.thread_mutex); + char* msgS; + char* ptr; + + if (message != NULL) { + if (message[0] == '^') + msgS = &message[1]; + else + msgS = message; + } + + while( ++i <= numMessageReceived) + { + if (message != NULL) + logMessage(LOG_DEBUG, "Programming mode: loop %d of %d looking for '%s' received message '%s'\n",i,numMessageReceived,message,aq_data->last_message); + else + logMessage(LOG_DEBUG, "Programming mode: loop %d of %d waiting for next message, received '%s'\n",i,numMessageReceived,aq_data->last_message); + + if (message != NULL) { + ptr = stristr(aq_data->last_message, msgS); + if (ptr != NULL) { // match + logMessage(LOG_DEBUG, "Programming mode: String MATCH\n"); + if (msgS == message) // match & don't care if first char + break; + else if (ptr == aq_data->last_message) // match & do care if first char + break; + } + } + + //logMessage(LOG_DEBUG, "Programming mode: looking for '%s' received message '%s'\n",message,aq_data->last_message); + pthread_cond_init(&aq_data->active_thread.thread_cond, NULL); + pthread_cond_wait(&aq_data->active_thread.thread_cond, &aq_data->active_thread.thread_mutex); + //logMessage(LOG_DEBUG, "Programming mode: loop %d of %d looking for '%s' received message '%s'\n",i,numMessageReceived,message,aq_data->last_message); + } + + pthread_mutex_unlock(&aq_data->active_thread.thread_mutex); + + if (message != NULL && ptr == NULL) { + //logMessage(LOG_ERR, "Could not select MENU of Aqualink control panel\n"); + logMessage(LOG_DEBUG, "Programming mode: did not find '%s'\n",message); + return false; + } else if (message != NULL) + logMessage(LOG_DEBUG, "Programming mode: found message '%s' in '%s'\n",message,aq_data->last_message); + + return true; +} + +*/ + + +/* +Link to two different menu's used in PDA +http://www.poolequipmentpriceslashers.com.au/wp-content/uploads/2012/11/Jandy-Aqualink-RS-PDA-Wireless-Pool-Controller_manual.pdf +https://www.jandy.com/-/media/zodiac/global/downloads/h/h0574200.pdf +*/ + +/* + List of how menu's display + +PDA Line 0 = +PDA Line 1 = AquaPalm +PDA Line 2 = +PDA Line 3 = Firmware Version +PDA Line 4 = +PDA Line 5 = REV MMM +PDA Line 6 = +PDA Line 7 = +PDA Line 8 = +PDA Line 9 = + +***************** Think this is startup different rev ************* +Line 0 = +Line 1 = PDA-PS4 Combo +Line 2 = +Line 3 = Firmware Version +Line 4 = +Line 5 = PPD: PDA 1.2 + +PDA Line 0 = +PDA Line 1 = AIR POOL +PDA Line 2 = +PDA Line 3 = +PDA Line 4 = POOL MODE ON +PDA Line 5 = POOL HEATER OFF +PDA Line 6 = SPA MODE OFF +PDA Line 7 = SPA HEATER OFF +PDA Line 8 = MENU +PDA Line 9 = EQUIPMENT ON/OFF + +PDA Line 0 = MAIN MENU +PDA Line 1 = +PDA Line 2 = SET TEMP > +PDA Line 3 = SET TIME > +PDA Line 4 = SET AquaPure > +PDA Line 5 = PALM OPTIONS > +PDA Line 6 = +PDA Line 7 = BOOST POOL +PDA Line 8 = +PDA Line 9 = + +**************** OPTION 2 FOR THIS MENU ******************** +PDA Line 0 = MAIN MENU +PDA Line 1 = +PDA Line 2 = HELP > +PDA Line 3 = PROGRAM > +PDA Line 4 = SET TEMP > +PDA Line 5 = SET TIME > +PDA Line 6 = PDA OPTIONS > +PDA Line 7 = SYSTEM SETUP > +PDA Line 8 = +PDA Line 9 = BOOST + +********** Guess at SYSTEM SETUP Menu (not on Rev MMM or before)************ + +// PDA Line 0 = SYSTEM SETUP +// PDA Line 1 = LABEL AUX > +// PDA Line 2 = FREEZE PROTECT > +// PDA Line 3 = AIR TEMP > +// PDA Line 4 = DEGREES C/F > +// PDA Line 5 = TEMP CALIBRATE > +// PDA Line 6 = SOLAR PRIORITY > +// PDA Line 7 = PUMP LOCKOUT > +// PDA Line 8 = ASSIGN JVAs > +// PDA Line 9 = ^^ MORE __ +// PDA Line 5 = COLOR LIGHTS > +// PDA Line 6 = SPA SWITCH > +// PDA Line 7 = SERVICE INFO > +// PDA Line 8 = CLEAR MEMORY > + + + +PDA Line 0 = PALM OPTIONS +PDA Line 1 = +PDA Line 2 = +PDA Line 3 = SET AUTO-OFF > +PDA Line 4 = BACKLIGHT > +PDA Line 5 = ASSIGN HOTKEYS > +PDA Line 6 = +PDA Line 7 = Choose setting +PDA Line 8 = and press SELECT +PDA Line 9 = + +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% +PDA Line 5 = +PDA Line 6 = +PDA Line 7 = Highlight an +PDA Line 8 = item and press +PDA Line 9 = SELECT + +PDA Line 0 = SET TIME +PDA Line 1 = +PDA Line 2 = 05/22/19 WED +PDA Line 3 = 10:53 AM +PDA Line 4 = +PDA Line 5 = +PDA Line 6 = Use ARROW KEYS +PDA Line 7 = to set value. +PDA Line 8 = Press SELECT +PDA Line 9 = to continue. + +PDA Line 0 = SET TEMP +PDA Line 1 = +PDA Line 2 = POOL HEAT 70`F +PDA Line 3 = SPA HEAT 98`F +PDA Line 4 = +PDA Line 5 = +PDA Line 6 = +PDA Line 7 = Highlight an +PDA Line 8 = item and press +PDA Line 9 = SELECT + + + + + +PDA Line 0 = EQUIPMENT +PDA Line 1 = FILTER PUMP ON +PDA Line 2 = SPA OFF +PDA Line 3 = POOL HEAT OFF +PDA Line 4 = SPA HEAT OFF +PDA Line 5 = CLEANER ON +PDA Line 6 = WATERFALL OFF +PDA Line 7 = AIR BLOWER OFF +PDA Line 8 = LIGHT OFF +PDA Line 9 = ^^ MORE __ + +PDA Line 0 = EQUIPMENT +PDA Line 1 = WATERFALL OFF +PDA Line 2 = AIR BLOWER OFF +PDA Line 3 = LIGHT OFF +PDA Line 4 = AUX5 OFF +PDA Line 5 = EXTRA AUX OFF +PDA Line 6 = SPA MODE OFF +PDA Line 7 = CLEAN MODE OFF +PDA Line 8 = ALL OFF +PDA Line 9 = + +*/ \ No newline at end of file diff --git a/pda_aq_programmer.h b/pda_aq_programmer.h new file mode 100644 index 0000000..3408838 --- /dev/null +++ b/pda_aq_programmer.h @@ -0,0 +1,21 @@ + +#ifndef PDA_AQ_PROGRAMMER_H_ +#define PDA_AQ_PROGRAMMER_H_ + +typedef enum pda_type { + AQUAPALM, + PDA +} pda_type; + +void *get_aqualink_PDA_device_status( void *ptr ); +void *set_aqualink_PDA_device_on_off( void *ptr ); +void *set_aqualink_PDA_wakeinit( void *ptr ); + +bool set_PDA_aqualink_heater_setpoint(struct aqualinkdata *aq_data, int val, bool isPool); +bool set_PDA_aqualink_SWG_setpoint(struct aqualinkdata *aq_data, int val); +bool set_PDA_aqualink_freezeprotect_setpoint(struct aqualinkdata *aq_data, int val); + +bool get_PDA_aqualink_pool_spa_heater_temps(struct aqualinkdata *aq_data); +bool get_PDA_freeze_protect_temp(struct aqualinkdata *aq_data); + +#endif // AQ_PDA_PROGRAMMER_H_ \ No newline at end of file diff --git a/pda_menu.c b/pda_menu.c index 442afae..ef6f8e4 100644 --- a/pda_menu.c +++ b/pda_menu.c @@ -39,23 +39,81 @@ char *pda_m_line(int index) // return NULL; } +int pda_find_m_index(char *text) +{ + int i; + + for (i = 0; i < PDA_LINES; i++) { + if (strncmp(pda_m_line(i), text, strlen(text)) == 0) + return i; + } + + return -1; +} + +int pda_find_m_index_case(char *text, int limit) +{ + int i; + + for (i = 0; i < PDA_LINES; i++) { + //printf ("+++ Compare '%s' to '%s' index %d\n",text,pda_m_line(i),i); + if (strncasecmp(pda_m_line(i), text, limit) == 0) + return i; + } + + return -1; +} + pda_menu_type pda_m_type() { + if (strncmp(_menu[1],"AIR ", 5) == 0) - return PM_MAIN; + return PM_HOME; else if (strncmp(_menu[0],"EQUIPMENT STATUS", 16) == 0) return PM_EQUIPTMENT_STATUS; else if (strncmp(_menu[0]," EQUIPMENT ", 16) == 0) return PM_EQUIPTMENT_CONTROL; else if (strncmp(_menu[0]," MAIN MENU ", 16) == 0) - return PM_SETTINGS; + return PM_MAIN; //else if ((_menu[0] == '\0' && _hlightindex == -1) || strncmp(_menu[4], "POOL MODE", 9) == 0 )// IF we are building the main menu this may be valid - else if (strncmp(_menu[4], "POOL MODE", 9) == 0 ) - return PM_BUILDING_MAIN; - + else if (strncmp(_menu[4], "POOL MODE", 9) == 0 ) { + if (pda_m_hlightindex() == -1) + return PM_BUILDING_HOME; + else + return PM_HOME; + } + else if (strncmp(_menu[0]," SET TEMP ", 16) == 0) + return PM_SET_TEMP; + else if (strncmp(_menu[0]," SET TIME ", 16) == 0) + return PM_SET_TIME; + else if (strncmp(_menu[0]," SET AquaPure ", 16) == 0) + return PM_AQUAPURE; + else if (strncmp(_menu[0]," SPA HEAT ", 16) == 0) + return PM_SPA_HEAT; + else if (strncmp(_menu[0]," POOL HEAT ", 16) == 0) + return PM_POOL_HEAT; + else if (strncmp(_menu[6],"Use ARROW KEYS ", 16) == 0 && + strncmp(_menu[0]," FREEZE PROTECT ", 16) == 0) + return PM_FREEZE_PROTECT; + else if (strncmp(_menu[1]," DEVICES ", 16) == 0 && + strncmp(_menu[0]," FREEZE PROTECT ", 16) == 0) + return PM_FREEZE_PROTECT_DEVICES; + else if (strncmp(_menu[3],"Firmware Version", 16) == 0 || + strncmp(_menu[1]," AquaPalm", 12) == 0 || + strncmp(_menu[1]," PDA-PS4 Combo", 14) == 0) + return PM_FW_VERSION; + return PM_UNKNOWN; } + + + + + + + + /* --- Main Menu --- Line 0 = @@ -86,15 +144,105 @@ bool process_pda_menu_packet(unsigned char* packet, int length) if (getLogLevel() >= LOG_DEBUG){print_menu();} break; case CMD_PDA_HIGHLIGHT: - _hlightindex = packet[4],_menu[packet[4]]; + // when switching from hlight to hlightchars index 255 is sent to turn off hlight + if (packet[4] <= PDA_LINES) { + _hlightindex = packet[4]; + } else { + _hlightindex = -1; + } + if (getLogLevel() >= LOG_DEBUG){print_menu();} + break; + case CMD_PDA_HIGHLIGHTCHARS: + if (packet[4] <= PDA_LINES) { + _hlightindex = packet[4]; + } else { + _hlightindex = -1; + } if (getLogLevel() >= LOG_DEBUG){print_menu();} break; case CMD_PDA_SHIFTLINES: memcpy(_menu[1], _menu[2], (PDA_LINES-1) * (AQ_MSGLEN+1) ); if (getLogLevel() >= LOG_DEBUG){print_menu();} - break; - + break; } return rtn; } + + +#ifdef SOME_CRAP +bool NEW_process_pda_menu_packet_NEW(unsigned char* packet, int length) +{ + bool rtn = true; + signed char first_line; + signed char last_line; + signed char line_shift; + signed char i; + + pthread_mutex_lock(&_pda_menu_mutex); + switch (packet[PKT_CMD]) { + case CMD_STATUS: + pthread_cond_signal(&_pda_menu_update_complete_cond); + break; + case CMD_PDA_CLEAR: + rtn = pda_m_clear(); + break; + case CMD_MSG_LONG: + if (packet[PKT_DATA] < 10) { + memset(_menu[packet[PKT_DATA]], 0, AQ_MSGLEN); + strncpy(_menu[packet[PKT_DATA]], (char*)packet+PKT_DATA+1, AQ_MSGLEN); + _menu[packet[PKT_DATA]][AQ_MSGLEN] = '\0'; + } + if (packet[PKT_DATA] == _hlightindex) { + logMessage(LOG_DEBUG, "process_pda_menu_packet: hlight changed from shift or up/down value\n"); + pthread_cond_signal(&_pda_menu_hlight_change_cond); + } + if (getLogLevel() >= LOG_DEBUG){print_menu();} + update_pda_menu_type(); + break; + case CMD_PDA_HIGHLIGHT: + // when switching from hlight to hlightchars index 255 is sent to turn off hlight + if (packet[4] <= PDA_LINES) { + _hlightindex = packet[4]; + } else { + _hlightindex = -1; + } + pthread_cond_signal(&_pda_menu_hlight_change_cond); + if (getLogLevel() >= LOG_DEBUG){print_menu();} + break; + case CMD_PDA_HIGHLIGHTCHARS: + if (packet[4] <= PDA_LINES) { + _hlightindex = packet[4]; + } else { + _hlightindex = -1; + } + pthread_cond_signal(&_pda_menu_hlight_change_cond); + if (getLogLevel() >= LOG_DEBUG){print_menu();} + break; + case CMD_PDA_SHIFTLINES: + // press up from top - shift menu down by 1 + // PDA Shif | HEX: 0x10|0x02|0x62|0x0f|0x01|0x08|0x01|0x8d|0x10|0x03| + // press down from bottom - shift menu up by 1 + // PDA Shif | HEX: 0x10|0x02|0x62|0x0f|0x01|0x08|0xff|0x8b|0x10|0x03| + first_line = (signed char)(packet[4]); + last_line = (signed char)(packet[5]); + line_shift = (signed char)(packet[6]); + logMessage(LOG_DEBUG, "\n"); + if (line_shift < 0) { + for (i = first_line-line_shift; i <= last_line; i++) { + memcpy(_menu[i+line_shift], _menu[i], AQ_MSGLEN+1); + } + } else { + for (i = last_line; i >= first_line+line_shift; i--) { + memcpy(_menu[i], _menu[i-line_shift], AQ_MSGLEN+1); + } + } + if (getLogLevel() >= LOG_DEBUG){print_menu();} + break; + + } + pthread_mutex_unlock(&_pda_menu_mutex); + + return rtn; +} +#endif \ No newline at end of file diff --git a/pda_menu.h b/pda_menu.h index c07dec1..06e99b4 100644 --- a/pda_menu.h +++ b/pda_menu.h @@ -5,6 +5,38 @@ #define PDA_LINES 10 // There is only 9 lines, but add buffer to make shifting easier +typedef enum pda_menu_type { + PM_UNKNOWN, + PM_FW_VERSION, + PM_HOME, + PM_BUILDING_HOME, + PM_MAIN, + PM_DIAGNOSTICS, + PM_PROGRAM, + PM_SET_TEMP, + PM_SET_TIME, + PM_POOL_HEAT, + PM_SPA_HEAT, + PM_AQUAPURE, + PM_SYSTEM_SETUP, + PM_AUX_LABEL, + PM_FREEZE_PROTECT, + PM_FREEZE_PROTECT_DEVICES, + PM_VSP, + PM_SETTINGS, + PM_EQUIPTMENT_CONTROL, + PM_EQUIPTMENT_STATUS, + PM_PALM_OPTIONS // This seems to be only older revisions +} pda_menu_type; + +/* +typedef enum pda_home_menu_item { + PMI_MAIN, + PMI_EQUIPTMENT_CONTROL +} pda_home_menu_item; +*/ + +/* typedef enum pda_menu_type { PM_UNKNOWN, PM_MAIN, @@ -13,7 +45,25 @@ typedef enum pda_menu_type { PM_EQUIPTMENT_STATUS, PM_BUILDING_MAIN } pda_menu_type; - +*/ +/* +typedef enum pda_menu_type { + PM_UNKNOWN, + PM_FW_VERSION, + PM_HOME, + PM_MAIN_MENU, + PM_EQUIPTMENT_CONTROL, + PM_EQUIPTMENT_STATUS, + PM_BUILDING_HOME, + PM_SYSTEM_SETUP, + PM_SET_TEMP, + PM_POOL_HEAT, + PM_SPA_HEAT, + PM_FREEZE_PROTECT, + PM_FREEZE_PROTECT_DEVICES, + PM_SET_AQUAPURE +} pda_menu_type; +*/ bool pda_mode(); void set_pda_mode(bool val); bool process_pda_menu_packet(unsigned char* packet, int length); @@ -21,5 +71,7 @@ int pda_m_hlightindex(); char *pda_m_hlight(); char *pda_m_line(int index); pda_menu_type pda_m_type(); +int pda_find_m_index(char *text); +int pda_find_m_index_case(char *text, int limit); #endif diff --git a/release/aqualinkd b/release/aqualinkd index 169f968c4ab59994a5aa991c274b07d011314f7e..90b8442bb6f349a376078fd43504a5d44a3ebbd0 100755 GIT binary patch delta 79525 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 PddzE`g?>aE{rdj^^SObn diff --git a/release/aqualinkd.conf b/release/aqualinkd.conf index d2443e1..867a79b 100755 --- a/release/aqualinkd.conf +++ b/release/aqualinkd.conf @@ -60,10 +60,27 @@ report_zero_pool_temp = no # Working RS ID's are 0x0a 0x0b 0x09 0x08 <- 0x08 is usually taken device_id=0x0a +# Please see forum for this, only set to yes when logging information to support +# new devices. Inflrmation will be written to /tmp/RS485.log +#debug_RSProtocol_packets = no + +#Only for PDA mode +# set PDA mode +#pda_mode = yes +# +# Put AqualinkD to sleep when in PDA mode after inactivity. +# If you have Jandy PDA then this MUST be set to yes as the controller can only support one PDA. +# If you don't have a Jandy PDA leave this at no as AqualinkD will be a lot quicker. +# Sleep timer is around 2 mins of inactivity, then wake after 2 mins of sleep. +#pda_sleep_mode = yes + # Read status information from other devices on the RS485 bus. # At the moment just Salt Water Generators are supported. read_all_devices = yes +# If you have a SWG connected to the control panel, set this to yes. +# AqualinkD can only detect a SWG if it's on, so after a restart you will not see/access a SWG until the the next time the pump is on. +force_SWG = no # Button inxed light probramming button is assigned to. (look at your button labels below) light_programming_button = 0 @@ -91,6 +108,10 @@ spa_water_temp_dzidx=0 SWG_percent_dzidx=0 SWG_PPM_dzidx=0 + +# Try to use labels from Control Panel. +use_panel_aux_labels=yes + # Labels for standard butons (shown in web UI), and domoticz idx's button_01_label=Filter Pump #button_01_dzidx=37 diff --git a/release/aqualinkd.pda.conf b/release/aqualinkd.pda.conf index e507a56..3fc9490 100644 --- a/release/aqualinkd.pda.conf +++ b/release/aqualinkd.pda.conf @@ -19,8 +19,8 @@ web_directory=/nas/data/Development/Raspberry/AqualinkD/web # DEBUG would print everything possible #log_level=DEBUG_SERIAL -log_level=DEBUG -#log_level=INFO +#log_level=DEBUG +log_level=INFO #log_level=NOTICE # The socket port that the daemon listens to @@ -33,13 +33,13 @@ serial_port=/dev/ttyUSB0 override_freeze_protect = no # mqtt stuff -mqtt_address = trident:1883 +#mqtt_address = trident:1883 #mqtt_user = someusername #mqtt_passwd = somepassword #mqtt_dz_pub_topic = domoticz/in #mqtt_dz_sub_topic = domoticz/out -mqtt_aq_topic = aqualinkd_test +#mqtt_aq_topic = aqualinkd # The id of the Aqualink terminal device. Devices probed by RS8 master are: # 08-0b, 10-13, 18-1b, 20-23, 28-2b, 30-33, 38-3b, 40-43 @@ -49,6 +49,7 @@ mqtt_aq_topic = aqualinkd_test #device_id=0x09 device_id=0x60 pda_mode = yes +pda_sleep_mode = yes convert_mqtt_temp_to_c = yes convert_dz_temp_to_c = yes @@ -101,15 +102,15 @@ button_03_PDA_label=CLEANER button_04_label=Waterfall #button_04_dzidx=40 -button_04_PDA_label=AUX2 +button_04_PDA_label=WATERFALL button_05_label=Spa Blower #button_05_dzidx=41 -button_05_PDA_label=AUX3 +button_05_PDA_label=AIR BLOWER button_06_label=Pool Light #button_06_dzidx=42 -button_06_PDA_label=AUX4 +button_06_PDA_label=LIGHT button_07_label=NONE #button_07_dzidx=43 diff --git a/release/serial_logger b/release/serial_logger index c3593ce66ea7da75129367b3f06b6c26550ad776..d8f52252a22b4310e0dc2b6442de302586986417 100755 GIT binary patch delta 9693 zcmaJ{4RlmRmadm{LMJ5fl5|W+C*(mlkYGYOB#;1s1PCz-NcdR~3NwHU3n)T_sO-RO zi>SjW>;?x+I;))JxI!S9Jh;T$?G?@Lq_v$4tJ#*%r zQ>Wf{@2y+6Zr!T7^}3IDYE2QX(a_HA9+8n~)CghJXehg9+!|j1R8X_~*~^}nR&1F+ z;ifM~y*7AF(*CWF@0#`9mTj#EwC4sV?wh8G3#pnYpVoR?a;ld8Sc2#lA{2I=oZT*V zyxMiDE=D{fcMsW~@G>N?03B$}8Cs?dIRtnH@HA>yrMy*NALFiUeNX=vO)HXd=2XhFsSC9Ia(7x<@}BhO$fO^8yR|^5hIZqIeM_&v>$SQz}-)!y#x(&mT=$cSfzH?mRx`g7e6 zp8n<+E=AV@+uN`BD05%rLJ4YJM!Hy|jTV^M-UeQ5;Jq(mjZpY>tQ?;{F{sCj?(AZ3 z#D%65V^?o#qPsv0j_gQlj>L#i$6e@pE8d6VeIwo*@E(Wv_XmmYynr_nFBWz@AjDhC zW1Axjz`sw3vjw0F&|`#nb`IpBx8oS(>jSgfWA#wDKzJi*E-|A!fQ|wX*2Jcccp<;=U3Gov^{B$8imSXnrMLEt$sM16=N-fG9luO_*o*yF?;eGKCO{_z`6Qv2| z5DMe(6zbC`rPI+Kr542nU5ikQ+QKhgExX#?C+Bj09vYT7W)@)<1NXI^kyEqE@^!eYk1FnKOM*luT2g{@ z!En1DS{{{8WL=wbwzv0SpEM+Dis2T%^797Q>YlAUdp9=yBMyG^s}B+xx5XB_gU z)O)8@2m2JU(IpTWEhEtJ9@M$08&T(=-ig|adI#!ZsP9Lef%;z5>8Q7&PD8yJbqeYR z)I(9%qfSP>4)tKvYfvYkUWs~;2wMKOM^69N>CKTk(HzOcEY(7YGiL8`&%tjkCErpy$f_V=s#F7*MXVu$cCO1&|^T40X;EOws&w; zF2(x;cwBk20<*D^g*s{iv$G;p7E;Q(pll!Bspx`Sn{!>EP)&b|rc1f$K%Gsy2Q;?M zQ>~B7PjWJDUXGz|McIcUFyy1G9m|iwpNI8EEpGI)0$NxfLhVtSUf`e{uBV1=O%mbLFt1>;;mvSRoI;ETBtlV7eQIB-RiYL2Tj3eO~1oy~1SdJaggHgD3 zF3?c;WH0)Lqcme?JLxgdOZqc1oSQbFB$z)!0KNb8Gv#X|OVW((5jRQ_ijLwzkpbVj>9n#4>dXxpj^l<5$303X4uwhB zKtJ}7Ud6e$Vj$2r2chuO@)KWKvOVTp_4QC7&Qe*Y-g%Eor!E>pI;PtI=F zajq$+Kj@wc5uXPK#yTqG-r5H{?Y}LxJ-#gJ>VU)pM)$PK9H`~Eg zRcg(|vaQo4b#mOOu{q)&&g;L93Zz8MYv4!u>4 z&KKwAl~|+~D)@MylJ+_F&H=`$`Kq!4aO3Ej5t@PHAAZQg82#wbx$)r`eD)6c(dc>F z%`$&X?xdd7=7KFn?ey7^ER=jRqmm(^h_7>j971VBxguAMotJDoI{?c@$v4KPY3t<&W2eYdV|8g0_gb%(+`9JKW!i*`-}Srly#20Rc|>8lvQrHq845e))q>n%Avl%GgjwhE-rhty zsubqTmlFzoYq@XGvELwJrekTqN<|y4+`v7a-DrX{YAb} zn3mymR-dD0YVzyCoaB|*sJX*4^Is~5jTGGHip^iF6vRUy69NrNpw$*od|W;{m|b%pqXGXD z#g79YEBq`2i2NtzQ-Lw5wHU%;l;tREQ0lAYr-7`w&!KS=&IvBSH}GyJ^Snb0*V&Ob z@-jPcICC?a+uqQT`k9A0uUE3=^DZWnIY4YzP zU+x=Ut}T}Dj=xTuXHv^S4Qlh{yCq|8aUqfCEmg4QRpD?bd?ldX+d|g7rSf#E1&uBH#*z%aJCO!xrjXAU~Su4c-ka{$2I5NLgWG z>_#3~w%TFE9K|=SQv7A0QEnS*-n&`?R_w87cp=f0g}p6ms#igd-r@ZXyH;w1v%rf7 zO_~g5JyM~xAXFt*jHjO$njbZ)DUQ9%95%Y9^b)1YHph?HJADZ*g8IHCSo>;M(#c)z zysEoq;cUd-X6|oz=l;g1GB;|x+tKBjrNxSZS@N3l$z#*5ZS(l z$-W4F1)Mqq%b)rB7r-7&YCJIAa!&3pPs_RtYLaT{RV_aQ&Hb%>tvoBZ9;qXheLyju zD(DP%+>f~?skD)zP->n}mLXY&;f>fn)u0l2;aR3<)_u+!dB<#H8ASR{=wftUw(bWV z2f>GvIDlaHVF2eX1InIV4sX^f!=49!5%4cnqpwiY;BM40sJ}$*hv6mga-Zjs1JlE` z@H9QU67^1$CX^7$A(X=?owH(bah4xVx&!Crb(3@Q-$SPyZ51r$W-*VQswrSZ6)5`k za@XWB@d6(+T{ZIHV+mh+BNTUJh~ME}7lh3Sio=Ex(y?QOmie567~nF;Sz$v38?71%cdW&$!Mw#^N$ z$4r6yHzm5CDdJxJkh62}%#6w6+ewCev?MiH2RAZt zQCme;H72$iQ8r(x43LjI9o=oqaSdiFY@aHJmCNn?Wf7t-2@#;9^!eFtgMFjfl1~Wp z^EHL#$*F#Axcqi%NqmcrFJyY_xM{b=4!wT;+6E(U?CQH#ZQ3+8U$n~US7XyxV0S5M zxTC&k!NSt=3FFpnSS^I!x_j1N+_LA!fY#JnIImNaf%!9K!~B!uoAB+MPprT-7|;Gb zj13MCKgIKj6-ZL> zU+o{rpX0#fUvl6eM|j0Spg`W}feOkUn0$TgKz@M(lfSR9pMTVT1)VW<0+@R_$(Vwm z_z-VAHWk4P8Xm3IlOe)j5M#MQCPSP9u7kXVtsuM+*m^jpLbNyxph4y1sGW}$Bei`v zXhrFoK7xgLXR#hDs@FyF{kXhXkEO8Gf!X0=2c|*1^w{#`H`zF-ENZa{z!Yc<^f#b_ z{SHk21qUYok^_@}vUFhkD-KNlp)!k)__IcEP7#PHaQ~!%3R)bP{2h}A@*5qP{1XmL z15Y_H`T7*I{Y`ka49Kkz~rBHVDj4>_OL3?8x*8pA0X_EiNLV zkD4l~fZYmfB3)b$+;gMdDpf3%#Y-!L8$n?2jQUh~>^@-5=_S;}yHL{uhZTP>u>GXs z1L2j2X*dEYq?mf$O925@4=tKa_EVtAY8O zF+E0uHb%++0+>5*Ii{5IUjnaG`v?u*3(T{ok$f)Gr=k?R4BS7$4Dost{|b9n4@dYe z_}oX1#16@D5xG!N8xXP&pDzz4j(o9mvN>@ z0k$8u_W@rCB15WDBQ6Ai|6*ATMReJCV5VsGieCxLe-|)oCjSOt`=P>HfSGgJ@;(GH z{~kaeaM|4sz8@a2SII_T%l{Cdg6&ZX{Zt48?RCD_!8aiPPLv1!8+@L&b4-27;su91 z^}hkkESxzI2XKNva_=a{ulB0J83-`r*{X0CFq6zGg}(>3UCLFn6FXTovf!hNACD2+ zkAW8hnD++Tph95gl=q_@4XAMUjo9yyap zhqL`)kqU$DhwUGMJd<8-!PNg)5eWR}2(Gy196c4)!9jMw<5js#?p|5lTDo$DCLdpw zl_f8$U-8ekuWFdHVRg;gb*&dyg<^)>vvF<1sydt8+PLPdmb`AmCI@Nr zZ546aI{CW|xp6!$n&d|tdUHi;Sz!jko@WCCAo}jAsIpSJS$neukzM%Ds2` pv=sUKJI5qf&BuO61?RXg@ykALCc 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 lzXWe{FiRp<|4UiE&N=S#y+XV!8`h= SLOG_MAX) logMessage(LOG_ERR, "Ran out of storage, some ID's were not captured, please increase SLOG_MAX and recompile\n"); logMessage(LOG_NOTICE, "ID's found\n"); - for (i = 0; i <= sindex; i++) { + for (i = 0; i < sindex; i++) { //logMessage(LOG_NOTICE, "ID 0x%02hhx is %s %s\n", slog[i].ID, (slog[i].inuse == true) ? "in use" : "not used", // (slog[i].inuse == false && canUse(slog[i].ID) == true)? " <-- can use for Aqualinkd" : ""); diff --git a/utils.c b/utils.c index 39098b3..258d38c 100644 --- a/utils.c +++ b/utils.c @@ -482,4 +482,50 @@ int ascii(char *destination, char *source) { } destination[i] = '\0'; return i; +} + +char *prittyString(char *str) +{ + char *ptr = str; + char *end; + bool lastspace=true; + + end = str + strlen(str) - 1; + while(end >= ptr){ + //printf("%d %s ", *ptr, ptr); + if (lastspace && *ptr > 96 && *ptr < 123) { + *ptr = *ptr - 32; + lastspace=false; + //printf("to upper\n"); + } else if (lastspace == false && *ptr > 54 && *ptr < 91) { + *ptr = *ptr + 32; + lastspace=false; + //printf("to lower\n"); + } else if (*ptr == 32) { + lastspace=true; + //printf("space\n"); + } else { + lastspace=false; + //printf("leave\n"); + } + ptr++; + } + + //printf("-- %s --\n", str); + + return str; +} + +static FILE *_packetLogFile = NULL; + +void writePacketLog(char *buffer) { + if (_packetLogFile == NULL) + _packetLogFile = fopen("/tmp/RS485.log", "a"); + + if (_packetLogFile != NULL) { + fputs(buffer, _packetLogFile); + } +} +void closePacketLog() { + fclose(_packetLogFile); } \ No newline at end of file diff --git a/utils.h b/utils.h index a94bae1..c2bdee5 100644 --- a/utils.h +++ b/utils.h @@ -49,7 +49,9 @@ float degFtoC(float degF); float degCtoF(float degC); char* stristr(const char* haystack, const char* needle); int ascii(char *destination, char *source); - +char *prittyString(char *str); +void writePacketLog(char *buff); +void closePacketLog(); //#ifndef _UTILS_C_ extern bool _daemon_; diff --git a/version.h b/version.h index 584842b..e187202 100644 --- a/version.h +++ b/version.h @@ -1,4 +1,4 @@ #define AQUALINKD_NAME "Aqualink Daemon" -#define AQUALINKD_VERSION "1.2.6f" +#define AQUALINKD_VERSION "1.3.0" diff --git a/web/controller.html b/web/controller.html index 7d3f390..092396a 100644 --- a/web/controller.html +++ b/web/controller.html @@ -1178,7 +1178,8 @@ } else if (data.type == 'devices') { check_devices(data); resetBackgroundSize(); - window.setTimeout(get_devices, (300 * 1000)); // Check for new dvices ever 5 mins. + //window.setTimeout(get_devices, (300 * 1000)); // Check for new dvices ever 5 mins. + window.setTimeout(get_devices, (60 * 1000)); // Check for new dvices ever 1 mins. } } socket_di.onclose = function() {