Version 2.3.2

pull/69/merge
sfeakes 2023-06-23 15:16:30 -05:00
parent 1e1e825829
commit 4a6c948a6a
22 changed files with 1352 additions and 336 deletions

View File

@ -1,8 +1,8 @@
#
# Options
#
# make // standard everything
# make debug // Give standard binary just with debugging
# make // standard build aqualinkd and serial_logger
# make debug // Compule standard aqualinkd binary just with debugging
# make aqdebug // Compile with extra aqualink debug information like timings
# make slog // Serial logger
# make <other> // not documenting
@ -13,6 +13,7 @@ AQ_RS16 = true
AQ_PDA = true
AQ_ONETOUCH = true
AQ_IAQTOUCH = true
AQ_MANAGER = false
#AQ_MEMCMP = true // Not implimented correctly yet.
# Turn off threadded net services
@ -22,8 +23,9 @@ AQ_NO_THREAD_NETSERVICE = false
CC = gcc
#LIBS := -lpthread -lm
LIBS := -l pthread -l m
#LIBS := -l pthread -l m
#LIBS := -l pthread -l m -static # Take out -static, just for dev
LIBS := -lpthread -lm
# Standard compile flags
GCCFLAGS = -Wall -O3
@ -100,8 +102,22 @@ ifeq ($(AQ_MEMCMP), true)
AQ_FLAGS := $(AQ_FLAGS) -D AQ_MEMCMP
endif
ifeq ($(AQ_NO_THREAD_NETSERVICE), true)
AQ_FLAGS := $(AQ_FLAGS) -D AQ_NO_THREAD_NETSERVICE
ifeq ($(AQ_MANAGER), true)
AQ_FLAGS := $(AQ_FLAGS) -D AQ_MANAGER
LIBS := $(LIBS) -lsystemd
# aq_manager requires threads, so make sure that's turned on.
ifeq ($(AQ_NO_THREAD_NETSERVICE), true)
# Show error
$(warning AQ_MANAGER requires threads, ignoring AQ_NO_THREAD_NETSERVICE)
endif
else
# No need for serial_logger without aq_manager
SRCS := $(filter-out serial_logger.c, $(SRCS))
# no threadded net service only valid without aq manager.
ifeq ($(AQ_NO_THREAD_NETSERVICE), true)
AQ_FLAGS := $(AQ_FLAGS) -D AQ_NO_THREAD_NETSERVICE
endif
endif

View File

@ -79,6 +79,9 @@ Designed to mimic AqualinkRS6 All Button keypad and (like the keypad) is used to
* Add set time to OneTouch protocol.
* Publish AqualinkD Management console. (Configure, Restart, run serial_logger) within AqualinkD.
# Update in Release 2.3.2 (Current Development)
* Added support for VSP on panel versions REV 0.1 & 0.2
# Update in Release 2.3.1
* Changed a lot of logic around different protocols.
* Added low latency support for FTDI usb driver.

View File

@ -165,8 +165,8 @@ int save_schedules_js(char* inBuf, int inSize, char* outBuf, int outSize)
} else if ( inarray && inBuf[i] == '{') {
passJson_scObj( &inBuf[i], (inSize-i), &cline);
LOG(SCHD_LOG,LOG_DEBUG, "Write to cron Min:%s Hour:%s DayM:%s Month:%s DayW:%s URL:%s Value:%s\n",cline.minute,cline.hour,cline.daym,cline.month,cline.dayw,cline.url,cline.value);
LOG(SCHD_LOG,LOG_INFO, "%s%s %s %s %s %s curl localhost:%s%s -d value=%s -X PUT\n",(cline.enabled?"":"#"),cline.minute, cline.hour, cline.daym, cline.month, cline.dayw, _aqconfig_.socket_port, cline.url, cline.value);
fprintf(fp, "%s%s %s %s %s %s root curl localhost:%s%s -d value=%s -X PUT\n",(cline.enabled?"":"#"),cline.minute, cline.hour, cline.daym, cline.month, cline.dayw, _aqconfig_.socket_port, cline.url, cline.value);
LOG(SCHD_LOG,LOG_INFO, "%s%s %s %s %s %s curl -s -S --show-error -o /dev/null localhost:%s%s -d value=%s -X PUT\n",(cline.enabled?"":"#"),cline.minute, cline.hour, cline.daym, cline.month, cline.dayw, _aqconfig_.socket_port, cline.url, cline.value);
fprintf(fp, "%s%s %s %s %s %s root curl -s -S --show-error -o /dev/null localhost:%s%s -d value=%s -X PUT\n",(cline.enabled?"":"#"),cline.minute, cline.hour, cline.daym, cline.month, cline.dayw, _aqconfig_.socket_port, cline.url, cline.value);
} else if ( inarray && inBuf[i] == '}') {
//inobj=false;
//objed=i;

View File

@ -6,8 +6,9 @@
#include <stdbool.h>
#define CONNECTION_ERROR "ERROR No connection to RS control panel"
#ifdef AQ_MANAGER
#define CONNECTION_RUNNING_SLOG "Running serial_logger, this will take some time"
#endif
#define SERIAL_BLOCKING_TIME 50 // (1 to 255) in 1/10th second so 1 = 0.1 sec, 255 = 25.5 sec
@ -230,6 +231,10 @@
//#define LNG_MSG_FREEZE_PROTECTION_ACTIVATED "FREEZE PROTECTION ACTIVATED"
#define LNG_MSG_FREEZE_PROTECTION_ACTIVATED "FREEZE PROTECTION IS ACTIVATED"
// These are
#define LNG_MSG_CHEM_FEED_ON "CHEM FEED ON"
#define LNG_MSG_CHEM_FEED_OFF "CHEM FEED OFF"
#define MSG_AIR_TEMP "AIR TEMP"
#define MSG_POOL_TEMP "POOL TEMP"

View File

@ -42,7 +42,8 @@ void intHandler(int dummy);
#ifdef AQ_PDA
bool checkAqualinkTime(); // Only need to externalise this for PDA
#endif
// There are cases where SWG will read 80% in allbutton and 0% in onetouch/aqualinktouch, this will compile that in or our
// There are cases where SWG will read 80% in allbutton and 0% in onetouch/aqualinktouch, this will compile that in or out
//#define READ_SWG_FROM_EXTENDED_ID
//#define TOTAL_BUTTONS 12
@ -189,6 +190,7 @@ typedef struct clightd
struct aqualinkdata
{
char version[AQ_MSGLEN*2];
char revision[AQ_MSGLEN];
char date[AQ_MSGLEN];
char time[AQ_MSGLEN];
char last_message[AQ_MSGLONGLEN+1]; // Last ascii message from panel - allbutton (or PDA) protocol
@ -233,7 +235,10 @@ struct aqualinkdata
unsigned char raw_status[AQ_PSTLEN];
// Multiple threads update this value.
volatile bool updated;
#ifdef AQ_MANAGER
volatile bool run_slogger;
#endif
#ifdef AQ_RS16
int rs16_vbutton_start;

View File

@ -51,7 +51,10 @@
#include "rs_msg_utils.h"
#include "serialadapter.h"
#include "debug_timer.h"
#ifdef AQ_MANAGER
#include "serial_logger.h"
#endif
/*
#if defined AQ_DEBUG || defined AQ_TM_DEBUG
@ -590,6 +593,12 @@ void _processMessage(char *message, bool reset)
//freeze_msg_count = 0;
strcpy(_aqualink_data.last_display_message, msg); // Also display the message on web UI
}
/* // Not sure when to do with these for the moment, so no need to compile in the test.
else if (stristr(msg, LNG_MSG_CHEM_FEED_ON) != NULL) {
}
else if (stristr(msg, LNG_MSG_CHEM_FEED_OFF) != NULL) {
}
*/
else if (msg[2] == '/' && msg[5] == '/' && msg[8] == ' ')
{ // date in format '08/29/16 MON'
strcpy(_aqualink_data.date, msg);
@ -653,8 +662,10 @@ void _processMessage(char *message, bool reset)
{ // '8157 REV MMM'
// A master firmware revision message.
strcpy(_aqualink_data.version, msg);
rsm_get_revision(_aqualink_data.revision, msg, strlen(msg));
//_gotREV = true;
LOG(AQRS_LOG,LOG_NOTICE, "Control Panel %s\n", msg);
LOG(AQRS_LOG,LOG_NOTICE, "Control Panel version %s\n", _aqualink_data.version);
LOG(AQRS_LOG,LOG_NOTICE, "Control Panel revision %s\n", _aqualink_data.revision);
if (_initWithRS == false)
{
//LOG(ALLBUTTON,LOG_NOTICE, "Standard protocol initialization complete\n");
@ -1080,7 +1091,9 @@ int main(int argc, char *argv[])
// struct lws_context_creation_info info;
// Log only NOTICE messages and above. Debug and info messages
// will not be logged to syslog.
#ifndef AQ_MANAGER
setlogmask(LOG_UPTO(LOG_NOTICE));
#endif
if (getuid() != 0)
{
@ -1766,12 +1779,14 @@ void main_loop()
blank_read = 0;
}
#ifdef AQ_MANAGER
if (_aqualink_data.run_slogger) {
LOG(AQUA_LOG,LOG_WARNING, "Starting serial_logger, this will take some time!\n");
broadcast_aqualinkstate_error(CONNECTION_RUNNING_SLOG);
serial_logger(rs_fd, _aqconfig_.serial_port, getSystemLogLevel());
_aqualink_data.run_slogger = false;
}
#endif
packet_length = get_packet(rs_fd, packet_buffer);
@ -1816,7 +1831,7 @@ void main_loop()
} else {
// If we did not process the packet, above we need to record it for the caculate_ack_packet call.
// Should find a better place to put this, but since prioritize_ack is expermental it's ok for now.
// NSF The exact same needs to be done for onetouch / iaqtouch and probably rssaadapter.
// NSF We shouldn;t need to do the same for rssa / onetouch / iaqtouch and probably rssaadapter since they don;t queue commands & programming commands.
_aqualink_data.last_packet_type = packet_buffer[PKT_CMD];
}
#ifdef AQ_PDA

View File

@ -75,9 +75,9 @@ int json_chars(char *dest, const char *src, int dest_len, int src_len)
return i;
}
int build_logmsg_JSON(char *dest, const char *src, int dest_len, int src_len)
int build_logmsg_JSON(char *dest, int loglevel, const char *src, int dest_len, int src_len)
{
int length = sprintf(dest, "{\"logmsg\":\"");
int length = sprintf(dest, "{\"logmsg\":\"%-7s",elevel2text(loglevel));
length += json_chars(dest+length, src, (dest_len-20), src_len);
length += sprintf(dest+length, "\"}");
dest[length] = '\0';

View File

@ -48,7 +48,7 @@ int build_aqualink_status_JSON(struct aqualinkdata *aqdata, char* buffer, int si
int build_aux_labels_JSON(struct aqualinkdata *aqdata, char* buffer, int size);
bool parseJSONwebrequest(char *buffer, struct JSONwebrequest *request);
bool parseJSONrequest(char *buffer, struct JSONkvptr *request);
int build_logmsg_JSON(char *dest, const char *src, int dest_len, int src_len);
int build_logmsg_JSON(char *dest, int loglevel, const char *src, int dest_len, int src_len);
int build_mqtt_status_JSON(char* buffer, int size, int idx, int nvalue, float setpoint/*char *svalue*/);
bool parseJSONmqttrequest(const char *str, size_t len, int *idx, int *nvalue, char *svalue);
int build_aqualink_error_status_JSON(char* buffer, int size, char *msg);

View File

@ -22,6 +22,9 @@
#include <sys/time.h>
#include <syslog.h>
#ifdef AQ_MANAGER
#include <systemd/sd-journal.h>
#endif
#include "mongoose.h"
@ -39,14 +42,13 @@
#include "serialadapter.h"
#include "aq_timer.h"
#include "aq_scheduler.h"
#include "rs_msg_utils.h"
#include "version.h"
#ifdef AQ_PDA
#include "pda.h"
#endif
// NSF remove once aqmanager is released.
#define INCLUDE_OLD_DEBUG_HTML
/*
#if defined AQ_DEBUG || defined AQ_TM_DEBUG
#include "timespec_subtract.h"
@ -139,15 +141,11 @@ void _broadcast_aqualinkstate_error(struct mg_connection *nc, char *msg)
// Maybe enhacment in future to sent error messages to MQTT
}
#define MAX_LOGSTACK 30
#ifdef AQ_MANAGER
#define WS_LOG_LENGTH 200
char _logstack[MAX_LOGSTACK][WS_LOG_LENGTH];
int _logstack_place=0;
pthread_mutex_t logmsg_mutex;
// Send log message to any aqManager websocket.
void _ws_send_logmsg(struct mg_connection *nc, char *msg) {
void ws_send_logmsg(struct mg_connection *nc, char *msg) {
struct mg_connection *c;
for (c = mg_next(nc->mgr, NULL); c != NULL; c = mg_next(nc->mgr, c)) {
@ -157,26 +155,148 @@ void _ws_send_logmsg(struct mg_connection *nc, char *msg) {
}
}
void find_aqualinkd_startupmsg(sd_journal *journal)
{
static bool once=false;
const void *log;
size_t len;
// Only going to do this one time, incase re reset while reading.
if (once) {
return;
}
once=true;
sd_journal_previous_skip(journal, 200);
while ( sd_journal_next(journal) > 0) // need to capture return of this
{
if (sd_journal_get_data(journal, "MESSAGE", &log, &len) >= 0) {
if (rsm_strnstr((const char *)log+8, AQUALINKD_NAME, len-8) != NULL) {
// Go back one and return
sd_journal_previous_skip(journal, 1);
return;
}
}
}
// Blindly go back 100 messages since above didn;t find start
sd_journal_previous_skip(journal, 100);
}
bool broadcast_systemd_logmessages(bool aqMgrActive) {
static sd_journal *journal;
static bool active = false;
char msg[WS_LOG_LENGTH];
static int cnt=0;
static char *cursor = NULL;
if (!aqMgrActive) {
if (!active) {
return true;
} else {
sd_journal_close(journal);
active = false;
return true;
cursor = NULL;
}
}
// aqManager is active
if (!active) {
if ( sd_journal_open(&journal, SD_JOURNAL_LOCAL_ONLY) < 0) {
build_logmsg_JSON(msg, LOG_ERR, "Failed to open journal", WS_LOG_LENGTH,22);
ws_send_logmsg(_mgr.active_connections, msg);
return false;
}
if (sd_journal_add_match(journal, "SYSLOG_IDENTIFIER=aqualinkd", 0) < 0) {
build_logmsg_JSON(msg, LOG_ERR, "Failed to set journal filter", WS_LOG_LENGTH,27);
ws_send_logmsg(_mgr.active_connections, msg);
sd_journal_close(journal);
return false;
}
if (sd_journal_seek_tail(journal) < 0) {
build_logmsg_JSON(msg, LOG_ERR, "Failed to seek to journal end", WS_LOG_LENGTH,29);
ws_send_logmsg(_mgr.active_connections, msg);
sd_journal_close(journal);
return false;
}
//if we have cusror go to it, otherwise jump back and try to find startup message
if (cursor != NULL) {
sd_journal_seek_cursor(journal, cursor);
sd_journal_next(journal);
} else
find_aqualinkd_startupmsg(journal);
active = true;
}
const void *log;
size_t len;
const void *pri;
size_t plen;
int rtn;
while ( (rtn = sd_journal_next(journal)) > 0) // need to capture return of this
{
if (sd_journal_get_data(journal, "MESSAGE", &log, &len) < 0) {
build_logmsg_JSON(msg, LOG_ERR, "Failed to get journal message", WS_LOG_LENGTH,29);
ws_send_logmsg(_mgr.active_connections, msg);
} else if (sd_journal_get_data(journal, "PRIORITY", &pri, &plen) < 0) {
build_logmsg_JSON(msg, LOG_ERR, "Failed to seek to journal message priority", WS_LOG_LENGTH,42);
ws_send_logmsg(_mgr.active_connections, msg);
} else {
build_logmsg_JSON(msg, atoi((const char *)pri+9), (const char *)log+8, WS_LOG_LENGTH,(int)len-8);
ws_send_logmsg(_mgr.active_connections, msg);
cnt=0;
sd_journal_get_cursor(journal, &cursor);
}
}
if (rtn < 0) {
build_logmsg_JSON(msg, LOG_ERR, "Failed to get seen to next journal message", WS_LOG_LENGTH,42);
ws_send_logmsg(_mgr.active_connections, msg);
sd_journal_close(journal);
active = false;
} else if (rtn == 0) {
// Sometimes we get no errors, and nothing to read, even when their is.
// So if we get too many, restart but don;t reset the cursor.
// Could tesd moving sd_journal_get_cursor(journal, &cursor); line to here from above.
if (cnt++ == 100) {
//printf("**** %d Too many blank reads, resetting!! ****\n",cnt);
sd_journal_close(journal);
active = false;
}
}
return true;
}
#endif
/* superseded with systemd/sd-journal
#define MAX_LOGSTACK 30
#define WS_LOG_LENGTH 200
char _logstack[MAX_LOGSTACK][WS_LOG_LENGTH];
int _logstack_place=0;
pthread_mutex_t logmsg_mutex;
void send_ws_logmessages()
{
pthread_mutex_lock(&logmsg_mutex);
// This pulls them off in the wrong order.
while (_logstack_place > 0) {
_ws_send_logmsg(_mgr.active_connections, _logstack[0]);
ws_send_logmsg(_mgr.active_connections, _logstack[0]);
memmove(&_logstack[0], &_logstack[1], WS_LOG_LENGTH * _logstack_place ) ;
_logstack_place--;
}
pthread_mutex_unlock(&logmsg_mutex);
}
// This needs to be thread safe.
void broadcast_log(char *msg) {
// NSF This causes mongoose to core dump after a period of time due to number of messages
// so remove until get time to update to new mongoose version.
//return;
#ifdef AQ_NO_THREAD_NETSERVICE
if (_keepNetServicesRunning && !_aqconfig_.thread_netservices && _aqualink_data->aqManagerActive)
if (_keepNetServicesRunning && !_aqconfig_.thread_netservices && _aqualink_data != NULL && _aqualink_data->aqManagerActive)
{
char message[WS_LOG_LENGTH];
build_logmsg_JSON(message, msg, WS_LOG_LENGTH, strlen(msg));
@ -186,7 +306,7 @@ void broadcast_log(char *msg) {
#endif
// See if we have and manager runnig first so we return ASAP.
// Since this get's called long before net_Services is started, also check we are running.
if (_keepNetServicesRunning && _net_thread_id != 0 && _aqualink_data->aqManagerActive)
if (_keepNetServicesRunning && _net_thread_id != 0 && _aqualink_data != NULL && _aqualink_data->aqManagerActive)
{
pthread_mutex_lock(&logmsg_mutex);
if (_logstack_place < MAX_LOGSTACK)
@ -204,6 +324,7 @@ void broadcast_log(char *msg) {
}
}
*/
void _broadcast_aqualinkstate(struct mg_connection *nc)
{
@ -692,7 +813,7 @@ void mqtt_broadcast_aqualinkstate(struct mg_connection *nc)
}
typedef enum {uActioned, uBad, uDevices, uStatus, uHomebridge, uDynamicconf, uDebugStatus, uDebugDownload, uSimulator, uSchedules, uSetSchedules, uAQmanager} uriAtype;
typedef enum {uActioned, uBad, uDevices, uStatus, uHomebridge, uDynamicconf, uDebugStatus, uDebugDownload, uSimulator, uSchedules, uSetSchedules, uAQmanager, uNotAvailable} uriAtype;
//typedef enum {NET_MQTT=0, NET_API, NET_WS, DZ_MQTT} netRequest;
const char actionName[][5] = {"MQTT", "API", "WS", "DZ"};
@ -785,6 +906,7 @@ uriAtype action_URI(request_source from, const char *URI, int uri_length, float
} else if (strncmp(ri1, "rawcommand", 10) == 0 && from == NET_WS) { // Only valid from websocket.
aq_send_cmd((unsigned char)value);
return uActioned;
#ifdef AQ_MANAGER
} else if (strncmp(ri1, "aqmanager", 9) == 0 && from == NET_WS) { // Only valid from websocket.
return uAQmanager;
} else if (strncmp(ri1, "setloglevel", 11) == 0 && from == NET_WS) { // Only valid from websocket.
@ -802,14 +924,23 @@ uriAtype action_URI(request_source from, const char *URI, int uri_length, float
} else if (ri2 != NULL && strncmp(ri2, "stop", 4) == 0) {
stopInlineLog2File();
} else if (ri2 != NULL && strncmp(ri2, "clean", 5) == 0) {
cleanInlineLogFile();
cleanInlineLog2File();
} else if (ri2 != NULL && strncmp(ri2, "download", 8) == 0) {
return uDebugDownload;
}
return uAQmanager; // Want to resent updated status
// BELOW IS FOR OLD DEBUG.HTML, Need to remove in future release
#ifdef INCLUDE_OLD_DEBUG_HTML
} else if (strncmp(ri1, "restart", 7) == 0 && from == NET_WS) { // Only valid from websocket.
LOG(NET_LOG,LOG_NOTICE, "Received restart request!\n");
raise(SIGRESTART);
return uActioned;
} else if (strncmp(ri1, "seriallogger", 12) == 0 && from == NET_WS) { // Only valid from websocket.
LOG(NET_LOG,LOG_NOTICE, "Received request to run serial_logger!\n");
_aqualink_data->run_slogger = true;
return uActioned;
#else // AQ_MANAGER
} else if (strncmp(ri1, "aqmanager", 9) == 0 && from == NET_WS) { // Only valid from websocket.
return uNotAvailable;
// BELOW IS FOR OLD DEBUG.HTML, Need to remove in future release with aqmanager goes live
} else if (strncmp(ri1, "debug", 5) == 0) {
if (ri2 != NULL && strncmp(ri2, "start", 5) == 0) {
startInlineDebug();
@ -825,16 +956,8 @@ uriAtype action_URI(request_source from, const char *URI, int uri_length, float
return uDebugDownload;
}
return uDebugStatus;
#endif
#endif //AQ_MANAGER
// couple of debug items for testing
} else if (strncmp(ri1, "restart", 13) == 0 && from == NET_WS) { // Only valid from websocket.
/*
LOG(NET_LOG,LOG_NOTICE, "Received restart request!\n");
raise(SIGRESTART);
return uActioned;
*/
LOG(NET_LOG,LOG_WARNING, "Received restart request, not implimented in this release!\n");
return uBad;
} else if (strncmp(ri1, "set_date_time", 13) == 0) {
//aq_programmer(AQ_SET_TIME, NULL, _aqualink_data);
panel_device_request(_aqualink_data, DATE_TIME, 0, 0, from);
@ -1271,7 +1394,7 @@ void action_web_request(struct mg_connection *nc, struct http_message *http_msg)
mg_send(nc, message, size);
}
break;
#ifdef INCLUDE_OLD_DEBUG_HTML
#ifndef AQ_MANAGER
case uDebugStatus:
{
char message[JSON_BUFFER_SIZE];
@ -1391,6 +1514,11 @@ void action_websocket_request(struct mg_connection *nc, struct websocket_message
ws_send(nc, message);
}
break;
case uNotAvailable:
{
sprintf(buffer, "{\"na_message\":\"not available in this version!\"}");
ws_send(nc, buffer);
}
case uSchedules:
{
DEBUG_TIMER_START(&tid);
@ -1712,7 +1840,6 @@ void *net_services_thread( void *ptr )
{
struct aqualinkdata *aqdata = (struct aqualinkdata *) ptr;
//struct mg_mgr mgr;
if (!_start_net_services(&_mgr, aqdata)) {
//LOG(NET_LOG,LOG_ERR, "Failed to start network services\n");
// Not the best way to do this (have thread exit process), but forks for the moment.
@ -1726,17 +1853,19 @@ void *net_services_thread( void *ptr )
{
//poll_net_services(&_mgr, 10);
// Shorten poll cycle when logging messages to WS
mg_mgr_poll(&_mgr, (_aqualink_data->aqManagerActive)?5:100);
//mg_mgr_poll(&_mgr, 100);
//mg_mgr_poll(&_mgr, (_aqualink_data->aqManagerActive)?50:100);
mg_mgr_poll(&_mgr, 100);
if (aqdata->updated == true /*|| _broadcast == true*/) {
//LOG(NET_LOG,LOG_DEBUG, "********** Broadcast ************\n");
_broadcast_aqualinkstate(_mgr.active_connections);
aqdata->updated = false;
}
if (_aqualink_data->aqManagerActive) {
send_ws_logmessages();
#ifdef AQ_MANAGER
if ( ! broadcast_systemd_logmessages(_aqualink_data->aqManagerActive)) {
LOG(AQUA_LOG,LOG_ERR, "Couldn't open systemd journal log\n");
}
#endif
}
f_end:

View File

@ -29,7 +29,9 @@ void stop_net_services();
time_t poll_net_services(int timeout_ms);
void broadcast_aqualinkstate();
void broadcast_aqualinkstate_error(char *msg);
void broadcast_log(char *msg);
// superseded with systemd/sd-journal
//void broadcast_log(char *msg);
//#endif

View File

@ -40,6 +40,8 @@ static int _ot_hlightcharindexstart = -1;
static int _ot_hlightcharindexstop = -1;
static char _menu[ONETOUCH_LINES][AQ_MSGLEN+1];
static struct ot_macro _macros[3];
bool _panel_version_P2 = false; // Older panels REV 0.1 and 0.2
void set_macro_status();
void pump_update(struct aqualinkdata *aq_data, int updated);
@ -290,6 +292,17 @@ bool log_panelversion(struct aqualinkdata *aq_data)
end = aq_data->version + strlen(aq_data->version) - 1;
while(end > aq_data->version && isspace(*end)) end--;
rsm_get_revision(aq_data->revision, _menu[7], AQ_MSGLEN);
LOG(ONET_LOG,LOG_NOTICE, "Control Panel version %s\n", aq_data->version);
LOG(ONET_LOG,LOG_NOTICE, "Control Panel revision %s\n", aq_data->revision);
if ( strcmp(aq_data->revision, "0.1") == 0 || strcmp(aq_data->revision, "0.2") == 0 ) {
LOG(ONET_LOG,LOG_NOTICE, "Setting early version for OneTouch\n");
_panel_version_P2 = true;
}
// Probably should check the panel size here as well.
// One Touch: OneTouch Menu Line 5 = RS-16 Combo
// Write new null terminator
*(end+1) = 0;
@ -314,115 +327,214 @@ bool log_freeze_setpoints(struct aqualinkdata *aq_data)
}
bool get_pumpinfo_from_menu(struct aqualinkdata *aq_data, int menuLineIdx)
{
int rpm = 0;
int watts = 0;
int gpm = 0;
int pump_index = rsm_atoi(&_menu[menuLineIdx][14]);
if (pump_index <= 0)
pump_index = rsm_atoi(&_menu[menuLineIdx][12]); // Pump inxed is in different position on line ` ePump AC 4`
// RPM displays differently depending on 3 or 4 digit rpm.
if (rsm_strcmp(_menu[menuLineIdx + 1], "RPM:") == 0)
{
rpm = rsm_atoi(&_menu[menuLineIdx + 1][10]);
if (rsm_strcmp(_menu[menuLineIdx + 2], "Watts:") == 0){
watts = rsm_atoi(&_menu[menuLineIdx + 2][10]);
}
if (rsm_strcmp(_menu[menuLineIdx + 3], "GPM:") == 0){
gpm = rsm_atoi(&_menu[menuLineIdx + 3][10]);
}
}
else if (rsm_strcmp(_menu[menuLineIdx+1], "*** Priming ***") == 0){
rpm = PUMP_PRIMING;
}
else if (rsm_strcmp(_menu[menuLineIdx+1], "(Offline)") == 0){
rpm = PUMP_OFFLINE;
}
else if (rsm_strcmp(_menu[menuLineIdx+1], "(Priming Error)") == 0){
rpm = PUMP_ERROR;
}
LOG(ONET_LOG, LOG_DEBUG, "Found OneTouch Pump '%s', Index %d, RPM %d, Watts %d, GPM %d\n", _menu[menuLineIdx], pump_index, rpm, watts, gpm);
for (int i = 0; i < aq_data->num_pumps; i++)
{
if (aq_data->pumps[i].pumpIndex == pump_index)
{
// printf("**** FOUND PUMP %d at index %d *****\n",pump_index,i);
// aq_data->pumps[i].updated = true;
pump_update(aq_data, i);
aq_data->pumps[i].rpm = rpm;
aq_data->pumps[i].watts = watts;
aq_data->pumps[i].gpm = gpm;
// LOG(ONET_LOG,LOG_INFO, "Matched OneTouch Pump to Index %d, RPM %d, Watts %d, GPM %d\n",i,rpm,watts,gpm);
LOG(ONET_LOG, LOG_INFO, "Matched OneTouch Pump to '%s', Index %d, RPM %d, Watts %d, GPM %d\n", aq_data->pumps[i].button->name, i, rpm, watts, gpm);
if (aq_data->pumps[i].pumpType == PT_UNKNOWN)
{
if (rsm_strcmp(_menu[2], "Intelliflo VS") == 0)
aq_data->pumps[i].pumpType = VSPUMP;
else if (rsm_strcmp(_menu[2], "Intelliflo VF") == 0)
aq_data->pumps[i].pumpType = VFPUMP;
else if (rsm_strcmp(_menu[2], "Jandy ePUMP") == 0 ||
rsm_strcmp(_menu[2], "ePump AC") == 0)
aq_data->pumps[i].pumpType = EPUMP;
LOG(ONET_LOG, LOG_INFO, "OneTouch Pump index %d set PumpType to %d\n", i, aq_data->pumps[i].pumpType);
return true;
}
}
}
LOG(ONET_LOG, LOG_WARNING, "Did not find AqualinkD config for Pump '%s'\n",_menu[menuLineIdx]);
return false;
}
/*
Info: OneTouch Menu Line 1 =
Info: OneTouch Menu Line 2 = Chemlink 1
Info: OneTouch Menu Line 3 = ORP 750/PH 7.0
*/
bool get_chemlinkinfo_from_menu(struct aqualinkdata *aq_data, int menuLineIdx)
{
if (rsm_strcmp(_menu[menuLineIdx + 1], "ORP") == 0)
{
int orp = atoi(&_menu[menuLineIdx + 1][4]);
char *indx = strchr(_menu[menuLineIdx + 1], '/');
float ph = atof(indx + 3);
LOG(ONET_LOG, LOG_INFO, "OneTouch Cemlink ORP = %d PH = %f\n", orp, ph);
if (aq_data->ph != ph || aq_data->orp != orp)
{
aq_data->ph = ph;
aq_data->orp = orp;
return true;
}
return false;
}
LOG(ONET_LOG, LOG_WARNING, "Did not understand Chemlink message '%s'\n",_menu[menuLineIdx + 1]);
return false;
}
/*
Info: OneTouch Menu Line 2 = AQUAPURE 60%
Info: OneTouch Menu Line 3 = Salt 7600 PPM */
bool get_aquapureinfo_from_menu(struct aqualinkdata *aq_data, int menuLineIdx)
{
bool rtn = false;
#ifdef READ_SWG_FROM_EXTENDED_ID
int swgp = atoi(&_menu[menuLineIdx][10]);
if (aq_data->swg_percent != swgp)
{
changeSWGpercent(aq_data, swgp);
rtn = true;
}
if (rsm_strcmp(_menu[menuLineIdx+1], "Salt") == 0)
{
int ppm = atoi(&_menu[menuLineIdx+1][6]);
if (aq_data->swg_ppm != ppm)
{
aq_data->swg_ppm = ppm;
rtn = true;
}
LOG(ONET_LOG, LOG_INFO, "OneTouch Aquapure SWG %d%, %d PPM\n", swgp, ppm);
}
#endif
return rtn;
}
#ifdef AQ_RS16
bool get_RS16buttoninfo_from_menu(struct aqualinkdata *aq_data, int menuLineIdx)
{
for (int i = aq_data->rs16_vbutton_start; i <= aq_data->rs16_vbutton_end; i++)
{
if (rsm_strcmp(_menu[menuLineIdx], aq_data->aqbuttons[i].label) == 0)
{
// Matched must be on.
LOG(ONET_LOG, LOG_INFO, "OneTouch RS16 equiptment status '%s' matched '%s'\n", _menu[menuLineIdx], aq_data->aqbuttons[i].label);
rs16led_update(aq_data, i);
aq_data->aqbuttons[i].led->state = ON;
return true;
}
}
return false;
}
#endif
/*
For older Panel versions 0.1 and 0.2
These display information all on one page, kind'a limped together like PDA
OneTouch Menu Line 0 = EQUIPMENT STATUS
OneTouch Menu Line 1 =
OneTouch Menu Line 2 = AquaPure 35%
OneTouch Menu Line 3 = SALT 3200 PPM
OneTouch Menu Line 4 = FILTER PUMP
OneTouch Menu Line 5 = Intelliflo VS 1
OneTouch Menu Line 6 = RPM: 1750
OneTouch Menu Line 7 = WATTS: 330
OneTouch Menu Line 8 =
OneTouch Menu Line 9 =
OneTouch Menu Line 10 =
OneTouch Menu Line 11 =
*/
bool log_qeuiptment_status_VP2(struct aqualinkdata *aq_data)
{
bool rtn = false;
int i;
for (i = 0; i < ONETOUCH_LINES; i++)
{
if (rsm_strcmp(_menu[i], "Intelliflo VS") == 0 ||
rsm_strcmp(_menu[i], "Intelliflo VF") == 0 ||
rsm_strcmp(_menu[i], "Jandy ePUMP") == 0 ||
rsm_strcmp(_menu[i], "ePump AC") == 0)
{
rtn = get_pumpinfo_from_menu(aq_data, i);
} else if (rsm_strcmp(_menu[2],"AQUAPURE") == 0) {
rtn = get_aquapureinfo_from_menu(aq_data, i);
} else if (rsm_strcmp(_menu[i],"Chemlink") == 0) {
rtn = get_chemlinkinfo_from_menu(aq_data, i);
#ifdef AQ_RS16
} else if (PANEL_SIZE() >= 16 ) {
// Loop over RS 16 buttons.
get_RS16buttoninfo_from_menu(aq_data, i);
}
#endif
}
return rtn;
}
/*
Newer panels have a page per device and specific lines with information
*/
bool log_qeuiptment_status(struct aqualinkdata *aq_data)
{
int i;
if (_panel_version_P2)
return log_qeuiptment_status_VP2(aq_data);
bool rtn = false;
if (rsm_strcmp(_menu[2],"Intelliflo VS") == 0 ||
rsm_strcmp(_menu[2],"Intelliflo VF") == 0 ||
rsm_strcmp(_menu[2],"Jandy ePUMP") == 0 ||
rsm_strcmp(_menu[2],"ePump AC") == 0) {
rtn = true;
int rpm = 0;
int watts = 0;
int gpm = 0;
int pump_index = rsm_atoi(&_menu[2][14]);
if (pump_index <= 0)
pump_index = rsm_atoi(&_menu[2][12]); // Pump inxed is in different position on line ` ePump AC 4`
// RPM displays differently depending on 3 or 4 digit rpm.
if (rsm_strcmp(_menu[3],"RPM:") == 0){
rpm = rsm_atoi(&_menu[3][10]);
if (rsm_strcmp(_menu[4],"Watts:") == 0) {
watts = rsm_atoi(&_menu[4][10]);
}
if (rsm_strcmp(_menu[5],"GPM:") == 0) {
gpm = rsm_atoi(&_menu[5][10]);
}
} else if (rsm_strcmp(_menu[3],"*** Priming ***") == 0){
rpm = PUMP_PRIMING;
} else if (rsm_strcmp(_menu[3],"(Offline)") == 0){
rpm = PUMP_OFFLINE;
} else if (rsm_strcmp(_menu[3],"(Priming Error)") == 0){
rpm = PUMP_ERROR;
}
LOG(ONET_LOG,LOG_INFO, "OneTouch Pump %s, Index %d, RPM %d, Watts %d, GPM %d\n",_menu[2],pump_index,rpm,watts,gpm);
for (i=0; i < aq_data->num_pumps; i++) {
if (aq_data->pumps[i].pumpIndex == pump_index) {
//printf("**** FOUND PUMP %d at index %d *****\n",pump_index,i);
//aq_data->pumps[i].updated = true;
pump_update(aq_data, i);
aq_data->pumps[i].rpm = rpm;
aq_data->pumps[i].watts = watts;
aq_data->pumps[i].gpm = gpm;
if (aq_data->pumps[i].pumpType == PT_UNKNOWN){
if (rsm_strcmp(_menu[2],"Intelliflo VS") == 0)
aq_data->pumps[i].pumpType = VSPUMP;
else if (rsm_strcmp(_menu[2],"Intelliflo VF") == 0)
aq_data->pumps[i].pumpType = VFPUMP;
else if (rsm_strcmp(_menu[2],"Jandy ePUMP") == 0 ||
rsm_strcmp(_menu[2],"ePump AC") == 0)
aq_data->pumps[i].pumpType = EPUMP;
}
//printf ("Set Pump Type to %d\n",aq_data->pumps[i].pumpType);
}
}
#ifdef READ_SWG_FROM_EXTENDED_ID
rtn = get_pumpinfo_from_menu(aq_data, 2);
} else if (rsm_strcmp(_menu[2],"AQUAPURE") == 0) {
/* Info: OneTouch Menu Line 0 = Equipment Status
Info: OneTouch Menu Line 1 =
Info: OneTouch Menu Line 2 = AQUAPURE 60%
Info: OneTouch Menu Line 3 = Salt 7600 PPM */
int swgp = atoi(&_menu[2][10]);
if ( aq_data->swg_percent != swgp ) {
//aq_data->swg_percent = swgp;
if (changeSWGpercent(aq_data, swgp))
LOG(ONET_LOG,LOG_INFO, "OneTouch SWG = %d\n",swgp);
rtn = true;
}
if (rsm_strcmp(_menu[3],"Salt") == 0) {
int ppm = atoi(&_menu[3][6]);
if ( aq_data->swg_ppm != ppm ) {
aq_data->swg_ppm = ppm;
rtn = true;
}
LOG(ONET_LOG,LOG_INFO, "OneTouch PPM = %d\n",ppm);
}
#endif
rtn = get_aquapureinfo_from_menu(aq_data, 2);
} else if (rsm_strcmp(_menu[2],"Chemlink") == 0) {
/* Info: OneTouch Menu Line 0 = Equipment Status
Info: OneTouch Menu Line 1 =
Info: OneTouch Menu Line 2 = Chemlink 1
Info: OneTouch Menu Line 3 = ORP 750/PH 7.0 */
if (rsm_strcmp(_menu[3],"ORP") == 0) {
int orp = atoi(&_menu[3][4]);
char *indx = strchr(_menu[3], '/');
float ph = atof(indx+3);
if (aq_data->ph != ph || aq_data->orp != orp) {
aq_data->ph = ph;
aq_data->orp = orp;
return true;
}
LOG(ONET_LOG,LOG_INFO, "OneTouch Cemlink ORP = %d PH = %f\n",orp,ph);
}
rtn = get_chemlinkinfo_from_menu(aq_data, 2);
}
#ifdef AQ_RS16
#ifdef AQ_RS16
else if (PANEL_SIZE() >= 16 ) { // This fails on RS4, comeback and find out why. // Run over devices that have no status LED's on RS12&16 panels.
//else if ( 16 <= (int)PANEL_SIZE ) {
int j;
int i;
for (i=2; i <= ONETOUCH_LINES; i++) {
for (j = aq_data->rs16_vbutton_start; j <= aq_data->rs16_vbutton_end; j++) {
if ( rsm_strcmp(_menu[i], aq_data->aqbuttons[j].label) == 0 ) {
//Matched must be on.
LOG(ONET_LOG,LOG_DEBUG, "OneTouch equiptment status '%s' matched '%s'\n",_menu[i],aq_data->aqbuttons[j].label);
rs16led_update(aq_data, j);
aq_data->aqbuttons[j].led->state = ON;
}
}
get_RS16buttoninfo_from_menu(aq_data, i);
}
}
#endif
@ -456,7 +568,7 @@ ot_menu_type get_onetouch_menu_type()
return OTM_BOOST;
else if (rsm_strcmp(_menu[0],"Set AQUAPURE") == 0)
return OTM_SET_AQUAPURE;
else if (rsm_strcmp(_menu[7],"REV ") == 0) // NSF Need a better check.
else if (rsm_strcmp(_menu[7],"REV") == 0) // NSF Need a better check.
return OTM_VERSION;
return OTM_UNKNOWN;

8
pda.c
View File

@ -658,23 +658,23 @@ void process_pda_packet_msg_long_equiptment_status(const char *msg_line, int lin
// Check message for status of device
// Loop through all buttons and match the PDA text.
// Should probably use strncasestr
if ((index = rsm_strnstr(msg, "CHECK AquaPure", AQ_MSGLEN)) != NULL)
if ((index = rsm_strncasestr(msg, "CHECK AquaPure", AQ_MSGLEN)) != NULL)
{
LOG(PDA_LOG,LOG_DEBUG, "CHECK AquaPure\n");
}
else if ((index = rsm_strnstr(msg, "FREEZE PROTECT", AQ_MSGLEN)) != NULL)
else if ((index = rsm_strncasestr(msg, "FREEZE PROTECT", AQ_MSGLEN)) != NULL)
{
_aqualink_data->frz_protect_state = ON;
LOG(PDA_LOG,LOG_DEBUG, "Freeze Protect is on\n");
}
else if ((index = rsm_strnstr(msg, MSG_SWG_PCT, AQ_MSGLEN)) != NULL)
else if ((index = rsm_strncasestr(msg, MSG_SWG_PCT, AQ_MSGLEN)) != NULL)
{
changeSWGpercent(_aqualink_data, atoi(index + strlen(MSG_SWG_PCT)));
//_aqualink_data->swg_percent = atoi(index + strlen(MSG_SWG_PCT));
//if (_aqualink_data->ar_swg_status == SWG_STATUS_OFF) {_aqualink_data->ar_swg_status = SWG_STATUS_ON;}
LOG(PDA_LOG,LOG_DEBUG, "AquaPure = %d\n", _aqualink_data->swg_percent);
}
else if ((index = rsm_strnstr(msg, MSG_SWG_PPM, AQ_MSGLEN)) != NULL)
else if ((index = rsm_strncasestr(msg, MSG_SWG_PPM, AQ_MSGLEN)) != NULL)
{
_aqualink_data->swg_ppm = atoi(index + strlen(MSG_SWG_PPM));
//if (_aqualink_data->ar_swg_status == SWG_STATUS_OFF) {_aqualink_data->ar_swg_status = SWG_STATUS_ON;}

Binary file not shown.

Binary file not shown.

View File

@ -22,6 +22,7 @@
#include <ctype.h>
#include "utils.h"
#include "rs_msg_utils.h"
/*
int check_panel_conf(char *panel)
@ -44,6 +45,45 @@ int check_panel_conf(char *panel)
"RS-8 Combo"
}
*/
/*
Pull revision from string examples
'E0260801 REV. O.2'
' REV. O.2 '
'B0029221 REV T.0.1'
' REV T.0.1'
*/
bool rsm_get_revision(char *dest, const char *src, int src_len)
{
char *sp = NULL;
char *ep = NULL;
sp = rsm_strnstr(src, "REV", src_len);
if (sp == NULL) {
return false;
}
sp = sp+3;
while ( *sp == ' ' || *sp == '.') {
sp = sp+1;
}
// sp is now the start of string revision #
ep = sp;
while ( *ep != ' ' && *ep != '\0') {
ep = ep+1;
}
int len=ep-sp;
// Check we got something usefull
if (len > 5) {
return false;
}
memcpy(dest, sp, len);
dest[len] = '\0';
return true;
}
char *rsm_strstr(const char *haystack, const char *needle)
{
char *sp1 = (char *)haystack;
@ -60,12 +100,38 @@ char *rsm_strstr(const char *haystack, const char *needle)
//LOG(AQUA_LOG,LOG_DEBUG, "Compare (reset)%d chars of '%s' to '%s'\n",strlen(sp2),sp1,sp2);
return strcasestr(sp1, sp2);
}
char *rsm_strnstr(const char *haystack, const char *needle, int length)
char *rsm_strncasestr(const char *haystack, const char *needle, int length)
{
// NEED TO WRITE THIS MYSELF. Same as below but limit length
return strcasestr(haystack, needle);
}
/*
* Find the first occurrence of find in s, where the search is limited to the
* first slen characters of s.
*/
char *rsm_strnstr(const char *s, const char *find, size_t slen)
{
char c, sc;
size_t len;
if ((c = *find++) != '\0') {
len = strlen(find);
do {
do {
if (slen-- < 1 || (sc = *s++) == '\0')
return (NULL);
} while (sc != c);
if (len > slen)
return (NULL);
} while (strncmp(s, find, len) != 0);
s--;
}
return ((char *)s);
}
// Check s2 exists in s1
int rsm_strcmp(const char *haystack, const char *needle)
{

View File

@ -2,7 +2,9 @@
#define RS_MSG_UTILS_H_
char *rsm_strstr(const char *haystack, const char *needle);
char *rsm_strnstr(const char *haystack, const char *needle, int length);
//char *rsm_strnstr(const char *haystack, const char *needle, int length);
char *rsm_strnstr(const char *s, const char *find, size_t slen);
char *rsm_strncasestr(const char *haystack, const char *needle, int length);
int rsm_strncpy(char *dest, const unsigned char *src, int dest_len, int src_len);
int rsm_strcmp(const char *s1, const char *s2);
int rsm_strncmp(const char *haystack, const char *needle, int length);
@ -10,5 +12,6 @@ int rsm_strncpy_nul2sp(char *dest, const unsigned char *src, int dest_len, int s
int rsm_atoi(const char* str);
float rsm_atof(const char* str);
char *rsm_strncpycut(char *dest, const char *src, int dest_len, int src_len);
bool rsm_get_revision(char *dest, const char *src, int src_len);
#endif //RS_MSG_UTILS_H_

View File

@ -41,7 +41,7 @@
#define SLOG_MAX 80
#define PACKET_MAX 600
#define VERSION "serial_logger V1.8"
#define VERSION "serial_logger V2.0"
/*
typedef enum used {
@ -243,32 +243,6 @@ bool canUse(unsigned char ID) {
return false;
}
/*
bool canUse(unsigned char ID) {
int i;
for (i = 0; i < 4; i++) {
if (ID == _goodID[i])
return true;
}
for (i = 0; i < 4; i++) {
if (ID == _goodPDAID[i])
return true;
}
for (i = 0; i < 4; i++) {
if (ID == _goodONETID[i])
return true;
}
for (i = 0; i < 4; i++) {
if (ID == _goodIAQTID[i])
return true;
}
for (i = 0; i < 2; i++) {
if (ID == _goodRSSAID[i])
return true;
}
return false;
}
*/
char* canUseExtended(unsigned char ID) {
int i;
for (i = 0; i < 4; i++) {
@ -294,85 +268,11 @@ char* canUseExtended(unsigned char ID) {
return "";
}
void printHex(char *pk, int length)
{
int i=0;
for (i=0;i<length;i++)
{
printf("0x%02hhx|",pk[i]);
}
}
void printPacket(unsigned char ID, unsigned char *packet_buffer, int packet_length)
{
int i;
//if (_filter != 0x00 && ID != _filter && packet_buffer[PKT_DEST] != _filter )
// return;
if (_rawlog) {
printHex((char *)packet_buffer, packet_length);
printf("\n");
return;
}
if (_filters != 0)
{
//int i;
bool dest_match = false;
bool src_match = false;
for (i=0; i < _filters; i++) {
if ( packet_buffer[PKT_DEST] == _filter[i])
dest_match = true;
if ( ID == _filter[i] && packet_buffer[PKT_DEST] == 0x00 )
src_match = true;
}
if(dest_match == false && src_match == false)
return;
}
/*
if (_filter != 0x00) {
if ( packet_buffer[PKT_DEST]==0x00 && ID != _filter )
return;
if ( packet_buffer[PKT_DEST]!=0x00 && packet_buffer[PKT_DEST] != _filter )
return;
}
*/
if (getProtocolType(packet_buffer)==JANDY) {
if (packet_buffer[PKT_DEST] != 0x00)
printf("\n");
printf("Jandy %4.4s 0x%02hhx of type %16.16s", (packet_buffer[PKT_DEST]==0x00?"From":"To"), (packet_buffer[PKT_DEST]==0x00?ID:packet_buffer[PKT_DEST]), get_packet_type(packet_buffer, packet_length));
} else {
printf("Pentair From 0x%02hhx To 0x%02hhx ",packet_buffer[PEN_PKT_FROM],packet_buffer[PEN_PKT_DEST] );
}
printf(" | HEX: ");
printHex((char *)packet_buffer, packet_length);
if (packet_buffer[PKT_CMD] == CMD_MSG || packet_buffer[PKT_CMD] == CMD_MSG_LONG) {
printf(" Message : ");
//fwrite(packet_buffer + 4, 1, AQ_MSGLEN+1, stdout);
//fwrite(packet_buffer + 4, 1, packet_length-7, stdout);
for(i=4; i < packet_length-3; i++) {
if (packet_buffer[i] >= 32 && packet_buffer[i] <= 126)
printf("%c",packet_buffer[i]);
}
}
//if (packet_buffer[PKT_DEST]==0x00)
// printf("\n\n");
//else
printf("\n");
}
void getPanelInfo(int rs_fd, unsigned char *packet_buffer, int packet_length)
{
static unsigned char getPanelRev[] = {0x00,0x14,0x01};
static unsigned char getPanelType[] = {0x00,0x14,0x02};
static int msgcnt=0;
//int i;
if (packet_buffer[PKT_CMD] == CMD_PROBE) {
if (msgcnt == 0)
@ -388,55 +288,77 @@ void getPanelInfo(int rs_fd, unsigned char *packet_buffer, int packet_length)
rsm_strncpy(_panelRev, packet_buffer+4, AQ_MSGLEN, packet_length-5);
else if (msgcnt == 3)
rsm_strncpy(_panelType, packet_buffer+4, AQ_MSGLEN, packet_length-5);
/*
for(i=4; i < packet_length-3; i++) {
if (packet_buffer[i] == 0x00)
break;
else if (packet_buffer[i] >= 32 && packet_buffer[i] <= 126)
printf("%c",packet_buffer[i]);
}
printf("\n");
*/
}
}
#ifdef SERIAL_LOGGER
void printHex(char *pk, int length)
{
int i=0;
for (i=0;i<length;i++)
{
printf("0x%02hhx|",pk[i]);
}
}
void printPacket(unsigned char ID, unsigned char *packet_buffer, int packet_length)
{
int i;
if (_rawlog) {
printHex((char *)packet_buffer, packet_length);
printf("\n");
return;
}
if (_filters != 0)
{
bool dest_match = false;
bool src_match = false;
for (i=0; i < _filters; i++) {
if ( packet_buffer[PKT_DEST] == _filter[i])
dest_match = true;
if ( ID == _filter[i] && packet_buffer[PKT_DEST] == 0x00 )
src_match = true;
}
if(dest_match == false && src_match == false)
return;
}
if (getProtocolType(packet_buffer)==JANDY) {
if (packet_buffer[PKT_DEST] != 0x00)
printf("\n");
printf("Jandy %4.4s 0x%02hhx of type %16.16s", (packet_buffer[PKT_DEST]==0x00?"From":"To"), (packet_buffer[PKT_DEST]==0x00?ID:packet_buffer[PKT_DEST]), get_packet_type(packet_buffer, packet_length));
} else {
printf("Pentair From 0x%02hhx To 0x%02hhx ",packet_buffer[PEN_PKT_FROM],packet_buffer[PEN_PKT_DEST] );
}
printf(" | HEX: ");
printHex((char *)packet_buffer, packet_length);
if (packet_buffer[PKT_CMD] == CMD_MSG || packet_buffer[PKT_CMD] == CMD_MSG_LONG) {
printf(" Message : ");
for(i=4; i < packet_length-3; i++) {
if (packet_buffer[i] >= 32 && packet_buffer[i] <= 126)
printf("%c",packet_buffer[i]);
}
}
printf("\n");
}
int main(int argc, char *argv[]) {
int rs_fd;
//int packet_length;
//int last_packet_length = 0;
//unsigned char packet_buffer[AQ_MAXPKTLEN];
//unsigned char last_packet_buffer[AQ_MAXPKTLEN];
//unsigned char lastID = 0x00;
int i = 0;
//bool found;
//serial_id_log slog[SLOG_MAX];
//serial_id_log pent_slog[SLOG_MAX];
//int sindex = 0;
//int pent_sindex = 0;
//int received_packets = 0;
int logPackets = PACKET_MAX;
int logLevel = LOG_NOTICE;
bool panleProbe = true;
bool rsSerialSpeedTest = false;
bool serialBlocking = true;
bool errorMonitor = false;
//struct timespec start_time;
//struct timespec end_time;
//struct timespec elapsed;
//int blankReads = 0;
//bool returnError = false;
//bool monitorOnly = false;
//bool playback_file = false;
//int logLevel;
//char buffer[256];
//bool idMode = true;
// aq_serial.c uses the following
_aqconfig_.readahead_b4_write = false;
@ -611,9 +533,10 @@ int _serial_logger(int rs_fd, char *port_name, int logPackets, int logLevel, boo
} else if (packet_length > 0) {
blankReads = 0;
//LOG(RSSD_LOG, LOG_DEBUG_SERIAL, "Received Packet for ID 0x%02hhx of type %s\n", packet_buffer[PKT_DEST], get_packet_type(packet_buffer, packet_length));
#ifdef SERIAL_LOGGER
if (logLevel > LOG_NOTICE)
printPacket(lastID, packet_buffer, packet_length);
#endif
if (getProtocolType(packet_buffer) == PENTAIR) {
found = false;
for (i = 0; i <= pent_sindex; i++) {
@ -661,18 +584,6 @@ int _serial_logger(int rs_fd, char *port_name, int logPackets, int logLevel, boo
}
received_packets++;
// NSF TESTING
/*
if (packet_buffer[PKT_DEST] == 0x40) {
static int hex = 0;
//printf("Sent ack\n");
//printf("Sent ack hex 0x%02hhx\n",(unsigned char)hex);
//send_extended_ack (rs_fd, 0x8b, (unsigned char)hex);
send_extended_ack (rs_fd, 0x8b, 0x00);
hex++;
}*/
// NSF
// Test Serial speed & caching
if (rsSerialSpeedTest) {
packet_length = get_packet(rs_fd, packet_buffer);
@ -706,11 +617,6 @@ int _serial_logger(int rs_fd, char *port_name, int logPackets, int logLevel, boo
clock_gettime(CLOCK_REALTIME, &end_time);
//stopPacketLogger();
// Reset and close the port.
//close_serial_port(rs_fd);
// If we were monitoring errors, or filtering messages, or no panel probe, don;t print details
if (errorMonitor || panleProbe==false || _filters > 0) {
return 0;

View File

@ -14,5 +14,6 @@ int logPackets = PACKET_MAX;
//int serial_logger(int rs_fd, char *port_name, int logPackets, int logLevel, bool panleProbe, bool rsSerialSpeedTest, bool errorMonitor);
int serial_logger (int rs_fd, char *port_name, int logLevel);
void getPanelInfo(int rs_fd, unsigned char *packet_buffer, int packet_length);
#endif // SERIAL_LOGGER_H_

61
utils.c
View File

@ -34,6 +34,10 @@
#include <sys/time.h>
#endif
#ifdef AQ_MANAGER
#include <systemd/sd-journal.h>
#endif
#ifndef _UTILS_C_
#define _UTILS_C_
#endif
@ -101,6 +105,8 @@ int getLogLevel(int16_t from)
return _log_level;
}
#ifdef AQ_MANAGER
void startInlineLog2File()
{
_log2file = true;
@ -111,28 +117,12 @@ void stopInlineLog2File()
{
_log2file = _cfg_log2file;
}
char *getInlineLogFName()
{
return _log_filename;
}
void cleanInlineLogFile() {
void cleanInlineLog2File() {
if (_log_filename != NULL) {
fclose(fopen(_log_filename, "w"));
}
}
bool islogFileReady()
{
if (_log_filename != NULL) {
struct stat st;
stat(_log_filename, &st);
if ( st.st_size > 0)
return true;
}
return false;
}
#ifdef INCLUDE_OLD_DEBUG_HTML
#else // AQ_MANAGER
void startInlineDebug()
{
_log_level = LOG_DEBUG;
@ -159,7 +149,23 @@ void cleanInlineDebug() {
fclose(fopen(_log_filename, "w"));
}
}
#endif
#endif // AQ_MANAGER
char *getInlineLogFName()
{
return _log_filename;
}
bool islogFileReady()
{
if (_log_filename != NULL) {
struct stat st;
stat(_log_filename, &st);
if ( st.st_size > 0)
return true;
}
return false;
}
/*
@ -529,6 +535,17 @@ void _LOG(int16_t from, int msg_level, char *message)
closelog ();
}
#ifdef AQ_MANAGER // Always use syslog with aqmanager
//sd_journal_print()
//openlog("aqualinkd", 0, LOG_DAEMON);
if (msg_level > LOG_DEBUG) // Let's not confuse syslog with custom levels
sd_journal_print (LOG_DEBUG, "%s", &message[9]);
//sd_journal_print_with_location(LOG_DEBUG, "aqualinkd", "%s", &message[9]);
else
sd_journal_print (msg_level, "%s", &message[9]);
//sd_journal_print_with_location(msg_level, "aqualinkd", "%s", &message[9]);
//closelog ();
#else
if (_daemonise == TRUE)
{
if (msg_level > LOG_DEBUG) // Let's not confuse syslog with custom levels
@ -538,6 +555,7 @@ void _LOG(int16_t from, int msg_level, char *message)
closelog ();
//return;
}
#endif
//int len;
message[8] = ' ';
@ -551,8 +569,9 @@ void _LOG(int16_t from, int msg_level, char *message)
}
*/
// Send logs to any websocket that's interested.
broadcast_log(message);
// Superceded systemd/sd-journal
//with Send logs to any websocket that's interested.
//broadcast_log(message);
// Sent the log to the UI if configured.
if (msg_level <= LOG_ERR && _loq_display_message != NULL) {

13
utils.h
View File

@ -5,10 +5,7 @@
#ifndef UTILS_H_
#define UTILS_H_
// In future release, delete this and all code, it's been replaced with aqmanager
#define INCLUDE_OLD_DEBUG_HTML
#define LOG_DEBUG_SERIAL 8
#define LOG_DEBUG_SERIAL LOG_DEBUG+1
#ifndef EXIT_SUCCESS
#define EXIT_FAILURE 1
@ -103,18 +100,18 @@ char *prittyString(char *str);
//void writePacketLog(char *buff);
//void closePacketLog();
#ifdef AQ_MANAGER
void startInlineLog2File();
void stopInlineLog2File();
void cleanInlineLogFile();
#ifdef INCLUDE_OLD_DEBUG_HTML
void cleanInlineLog2File();
#else
void startInlineDebug();
void stopInlineDebug();
void startInlineSerialDebug();
void cleanInlineDebug();
#endif
char *getInlineLogFName();
bool islogFileReady();
//const char *logmask2name(int16_t from);

View File

@ -1,4 +1,4 @@
#define AQUALINKD_NAME "Aqualink Daemon"
#define AQUALINKD_VERSION "2.3.1"
#define AQUALINKD_VERSION "2.3.2"

737
web/aqmanager.html Normal file
View File

@ -0,0 +1,737 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<meta http-equiv='Content-Type' content='text/html; charset=windows-1252'>
<title>AqualinkD Managment Console</title>
<meta name='viewport' content='width=device-width'>
<meta name='apple-mobile-web-app-capable' content='yes'>
<meta name='apple-mobile-web-app-status-bar-style' content='black'>
<meta name='apple-mobile-web-app-status-bar-style' content='black'>
<link href="aqualinkd.png" rel="apple-touch-icon">
<link href="aqualinkd.png" rel="icon">
<style>
html {}
body {
font-family: 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif;
font-weight: 300;
background-color: white;
color: #000000;
margin: 0 !important;
padding: 0 !important;
}
.wrapper {
display: grid;
grid-template-columns: 300px 1fr;
grid-gap: 1rem;
}
.inner {
/*padding: 1rem;*/
display: flex;
align-items: center;
justify-content: center;
/*border: 1px solid red;*/
align-items: flex-start;
}
.aqualinkd {
grid-column: 1 / -1;
}
.commands {
/*padding: 1rem;*/
/*display: grid;*/
align-items: center;
justify-content: center;
/*border: 1px solid red;*/
row-gap: 20px;
}
/*
.loglevelstitle {
background-color: rgb(221, 221, 221);
}
.debugmaskstitle {
background-color: rgb(221, 221, 221);
}
*/
table {
background-color: rgb(221, 221, 221);
padding: 10px;
}
th {
background-color: white;
}
#title {
background-color: rgb(200, 200, 200);
font-weight: 600;
}
input[type=button],
input[type=submit],
input[type=reset] {
/*background-color: rgb(165, 165, 165);*/
background-color: #4CAAD9;
border: none;
color: rgb(0, 0, 0);
padding: 2px 2px;
text-decoration: none;
margin: 2px 2px 2px 2px;
min-width: 70px;
border-radius: 70px;
height: 20px;
/*max-width: 120px;*/
}
.disablebutton {
background-color: #ccc !important;
color: #777 !important;
}
.disabletoggle {
color: #777 !important;
}
.statusmsg {
}
.error {
background-color: rgb(255, 0, 0) !important;
font-weight: 600;
}
.logmsgerror {
color: rgb(255, 0, 0) !important;
}
.hidden {
display: none;
}
.commands {
vertical-align: top;
}
.loglevels {}
.logcontainer {
font-family: monospace;
height: 510px;
width: 100%;
overflow: auto;
display: flex;
flex-direction: column-reverse;
background-color: #2b2b2b;
color: white;
}
.toggle {
cursor: pointer;
display: inline-block;
margin: 1px 1px 1px 1px;
}
.logtoggle {
width: 130px;
}
.toggle-switch {
display: inline-block;
background: #ccc;
border-radius: 16px;
width: 35px;
height: 18px;
position: relative;
vertical-align: middle;
transition: background 0.25s;
}
.toggle-switch:before,
.toggle-switch:after {
content: "";
}
.toggle-switch:before {
display: block;
background: linear-gradient(to bottom, #fff 0%, #eee 100%);
border-radius: 50%;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
width: 15px;
height: 15px;
position: absolute;
top: 2px;
left: 2px;
transition: left 0.25s;
}
.toggle:hover .toggle-switch:before {
background: linear-gradient(to bottom, #fff 0%, #fff 100%);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
}
.toggle-checkbox:checked+.toggle-switch {
/*background: #56c080;*/
background: #4CAAD9;
}
.toggle-checkbox:checked+.toggle-switch:before {
left: 19px;
}
.toggle-checkbox {
position: absolute;
visibility: hidden;
}
.toggle-label {
margin-left: 5px;
position: relative;
top: 2px;
}
.collapsible {
background-color: #777;
color: white;
cursor: pointer;
/*padding: 18px;*/
width: 100%;
border: none;
text-align: left;
outline: none;
font-size: 16px;
}
.collapsible:after {
content: '\002B';
color: white;
font-weight: bold;
float: right;
margin-left: 5px;
}
.active:after {
content: "\2212";
}
.content {
display: grid;
/*padding: 0 18px;*/
max-height: 0;
overflow: hidden;
transition: max-height 0.2s ease-out;
background-color: #f1f1f1;
font-size: 14px;
}
</style>
<script type='text/javascript'>
var _panel_size = 6;
var _panel_set = 0;
function init_collapsible() {
var coll = document.getElementsByClassName("collapsible");
var i;
for (i = 0; i < coll.length; i++) {
coll[i].addEventListener("click", function () {
this.classList.toggle("active");
var content = this.nextElementSibling;
if (content.style.maxHeight) {
content.style.maxHeight = null;
} else {
content.style.maxHeight = content.scrollHeight + "px";
}
});
}
// open up the system information collapsable content
document.getElementById("systembutton").dispatchEvent(new Event('click'));
// disable the download log button on startup
disablebutton("downloadlog");
disabletoggle("log2file");
//disabletoggle("seriallog");
}
function disablebutton(id) {
console.log("disable " + id);
var button = document.getElementById(id);
if (button) {
button.disabled = true;
button.classList.add("disablebutton");
}
}
function enablebutton(id) {
console.log("enable " + id);
var button = document.getElementById(id);
if (button) {
button.disabled = false;
button.classList.remove("disablebutton");
}
}
function disabletoggle(id) {
disablebutton(id);
try {
document.getElementById(id).parentNode.classList.add("disabletoggle");
} catch (e) { console.log("error " + e) }
}
function enabletoggle(id) {
enablebutton(id);
try {
document.getElementById(id).parentNode.classList.remove("disabletoggle");
} catch (e) { console.log("error " + e) }
}
function tohex(value) {
var rtn = parseInt(value).toString(16);
if (rtn.length <= 1)
return "0x0" + rtn;
else
return "0x" + rtn;
}
function todec(value) {
let number = parseInt(value, 16);
return number;
}
var last_message_element = null;
function update_log_message(message) {
var element = document.createElement("div");
if (message.startsWith("Error:")) {
element.classList.add("logmsgerror");
}
element.appendChild(document.createTextNode(message));
document.getElementById("logs").insertBefore(element, last_message_element);
last_message_element = element;
}
function update_status(data) {
}
function setloglevel(caller) {
//console.log(caller.id);
var id = parseInt(caller.id.split('_')[1]);
var msg = {
uri: "setloglevel",
value: id
};
send_command(msg);
}
function setlogmask(caller) {
console.log(caller.id);
var id = parseInt(caller.id.split('_')[1]);
var addremove = "";
if (caller.checked) {
addremove = "addlogmask";
} else {
addremove = "removelogmask";
}
var msg = {
uri: addremove,
value: id
};
send_command(msg);
}
function setlogfile(caller) {
var msg = {};
if (caller.id == "log2file") {
var startstop = "";
if (caller.checked) {
startstop = "start";
} else {
startstop = "stop";
}
msg = {
uri: "log2file/" + startstop,
value: 0
};
} else if (caller.id == "cleanlog") {
msg = {
uri: "log2file/clean",
value: 0
};
} else if (caller.id == "downloadlog") {
//window.location = '/api/debug/download';
downloadFile('/api/debug/download/aqualinkd.log');
return;
}
send_command(msg);
}
function downloadFile(filePath) {
var link = document.createElement('a');
link.href = filePath;
link.download = filePath.substr(filePath.lastIndexOf('/') + 1);
link.click();
}
function settoggle(id, onoff) {
var element = document.getElementById(id);
if (onoff == "on") {
element.checked = true;
//console.log("set " + element.id + " on");
} else {
element.checked = false;
//console.log("set " + element.id + " off");
}
}
function update_status(data) {
/*
read panel_message, panel_type, version, aqualinkd_version
*/
if (data['aqualinkd_version']) {
var eCommands = document.getElementById("aqualinkdversion").innerHTML = data['aqualinkd_version'];
}
if (data['version']) {
var eCommands = document.getElementById("panelversion").innerHTML = data['version'];
}
if (data['panel_type']) {
var eCommands = document.getElementById("paneltype").innerHTML = data['panel_type'];
}
if (data['status']) {
update_status_message(data['status']);
}
/*
if (data['status'] == " ") {
document.getElementById("statusmsg").innerHTML = "Connected";
} else if (data['status'] != " ") {
document.getElementById("statusmsg").innerHTML = data['status'];
}
*/
}
function update_status_message(message, error=false) {
try{
if (error || message.substring(0, 5).toLowerCase() == "error" )
document.getElementById("statusmsg").classList.add("error");
else
document.getElementById("statusmsg").classList.remove("error");
if (message == " ") {
document.getElementById("statusmsg").innerHTML = "Connected";
} else if (message != " ") {
document.getElementById("statusmsg").innerHTML = message;
}
} catch (Error) {}
}
function setAqManagerOptions(data) {
/*
read deamonized logging2file logfilename debugmasks[] loglevels[]
if logfilename=NULL we can turn on/off logging to file.
logging2file will tell us if it's currently on or off
*/
console.log("deamonized=" + data['deamonized']);
console.log("logfilename=" + data['logfilename']);
console.log("logfileready=" + data['logfileready']);
if (data['logfilename'] == "(null)")
enabletoggle("log2file");
else
disabletoggle("log2file");
if (data['logfileready'] == "on") {
enablebutton("downloadlog");
enablebutton("cleanlog");
} else {
disablebutton("downloadlog");
disablebutton("cleanlog");
}
var eCommands = document.getElementById("loglevels");
for (var obj in data['loglevels']) {
//console.log(data['loglevels'][obj].name);
var element_id = "loglevel_" + data['loglevels'][obj].id;
var element = document.getElementById(element_id);
if (!element) {
element = document.createElement('label');
element.classList.add('toggle');
element.classList.add('logtoggle');
element.innerHTML = '<input class="toggle-checkbox" type="checkbox" id="' + element_id + '" onclick="setloglevel(this);">' +
'<div class="toggle-switch"></div>' +
'<span class="toggle-label">' + data['loglevels'][obj].name + '</span>';
eCommands.appendChild(element);
}
settoggle(element_id, data['loglevels'][obj].set);
}
var eCommands = document.getElementById("debugmasks");
for (var obj in data['debugmasks']) {
var element_id = "debugmask_" + data['debugmasks'][obj].id;
var element = document.getElementById(element_id);
if (!element) {
element = document.createElement('label');
element.classList.add('toggle');
element.classList.add('logtoggle');
element.innerHTML = '<input class="toggle-checkbox" type="checkbox" id="' + element_id + '" onclick="setlogmask(this);">' +
'<div class="toggle-switch"></div>' +
'<span class="toggle-label">' + data['debugmasks'][obj].name + '</span>';
eCommands.appendChild(element);
}
settoggle(element_id, data['debugmasks'][obj].set);
}
}
function get_appropriate_ws_url() {
var pcol;
var u = document.URL;
/*
* We open the websocket encrypted if this page came on an
* https:// url itself, otherwise unencrypted
*/
if (u.substring(0, 5) == "https") {
pcol = "wss://";
u = u.substr(8);
} else {
pcol = "ws://";
if (u.substring(0, 4) == "http")
u = u.substr(7);
}
u = u.split('/');
//alert (pcol + u[0] + ":6500");
return pcol + u[0];
}
/* dumb increment protocol */
var socket_di;
function startWebsockets() {
socket_di = new WebSocket(get_appropriate_ws_url());
try {
socket_di.onopen = function () {
// success!
start_manager();
//get_devices();
}
socket_di.onmessage = function got_packet(msg) {
//document.getElementById("status").classList.remove("error");
//console.log(msg.data);
var data
try {
data = JSON.parse(msg.data);
}
catch (error) {
console.error(error);
console.log(msg.data);
}
if (data.logmsg) {
update_log_message(data.logmsg);
} else if (data.na_message) {
set_unavailable(data.na_message);
} else if (data.message) {
update_status_message(data.message);
} else if (data.type == 'aqmanager') {
setAqManagerOptions(data);
//update_status(data);
} else if (data.type == 'devices') {
//update_device(data);
} else if (data.type == 'status') {
update_status(data);
} else if (data.type == 'aux_labels') {
//set_labels(data);
}
}
socket_di.onclose = function () {
// something went wrong
//document.getElementById("status").innerHTML = ' !!! Connection error !!! '
//document.getElementById("status").classList.add("error");
// Try to reconnect every 5 seconds.
update_status_message(' !!! Connection error !!! ', true);
setTimeout(function () {
startWebsockets();
}, 5000);
}
} catch (exception) {
alert('<p>Error' + exception);
}
}
function send_command(cmd) {
console.log("Send " + JSON.stringify(cmd));
socket_di.send(JSON.stringify(cmd));
}
function start_manager() {
var msg = {
//command: "simulator"
uri: "aqmanager"
};
socket_di.send(JSON.stringify(msg));
}
function get_devices() {
var msg = {
uri: "devices"
};
socket_di.send(JSON.stringify(msg));
}
/*
function reset() {
socket_di.send("reset\n");
}
*/
function init() {
startWebsockets();
}
function set_unavailable(message) {
console.log("!!! NOT Available !!!");
update_log_message("!!! AqualinkD Manager "+message+" !!!");
const demoClasses = document.querySelectorAll('.inner');
demoClasses.forEach(element => {
element.style.opacity = 0.2;
console.log('Element '+element);
});
disablebutton("restart");
disablebutton("seriallog");
disablebutton("downloadlog");
disabletoggle("log2file");
var coll = document.getElementsByClassName("collapsible");
var i;
for (i = 0; i < coll.length; i++) {
// Remove event listeners from Element by cloning it.
coll[i].replaceWith(coll[i].cloneNode(true));
}
//disabletoggle("seriallog");
}
function send(source) {
//console.log("from" + source.id);
var cmd = {};
//cmd.uri = "rawcommand"
switch (source.id) {
case "restart":
cmd.uri = "restart"
// NEED TO REGET aqmanager after restart.
break;
case "debugstart":
cmd.uri = "debug/start"
break;
case "debugstop":
cmd.uri = "debug/stop"
break;
case "seriallog":
cmd.uri = "seriallogger"
break;
default:
alert("Unknown button");
return;
break;
}
send_command(cmd);
}
</script>
<body onload="init();init_collapsible();">
<div class="wrapper">
<div class="aqualinkd">
<iframe id="aqd" src="/" width="100%" height="350px"></iframe>
</div>
<div class="inner">
<div class="commands">
<table border='0' id="deviceList">
<tr style="title">
<td colspan="2" style="title" align="center"><label id="title">&nbsp;&nbsp;AqualinkD Managment
Console&nbsp;&nbsp;</label></th>
</tr>
<tr>
<td colspan="2" align="center"><label id="statusmsg" class="statusmsg" >status</label></td>
</tr>
<tr>
<td align="center">
<input id="restart" type="button" onclick="send(this);" value="Restart AqualinkD">
</td>
<td>
<input id="seriallog" type="button" onclick="send(this);" value="Run Serial Logger">
</td>
</tr>
</table>
<button class="collapsible" id="systembutton">System Information</button>
<div class="content" id="system">
<table border='0'>
<tr>
<td>AqualinkD version:</td>
<td id="aqualinkdversion"></td>
</tr>
<tr>
<td>Panel version:</td>
<td id="panelversion"></td>
</tr>
<tr>
<td>Panel type:</td>
<td id="paneltype"></td>
</tr>
</table>
</div>
<!--&nbsp;-->
<button class="collapsible">Log Levels</button>
<div class="content" id="loglevels">
<!-- Will be populated on load -->
</div>
<button class="collapsible">Log File</button>
<div class="content" id="loglevels">
<label class="toggle logtoggle"><input class="toggle-checkbox" type="checkbox" id="log2file"
onclick="setlogfile(this);">
<div class="toggle-switch"></div><span class="toggle-label">Log to file</span>
</label>
<input id="downloadlog" type="button" onclick="setlogfile(this);" value="Download logfile"></input>
<input id="cleanlog" type="button" onclick="setlogfile(this);" value="Delete logfile"></input>
</div>
<!--&nbsp;-->
<button class="collapsible">Debug Masks</button>
<div class="debugmasks content" id="debugmasks">
<!-- Will be populated on load -->
</div>
<button class="collapsible">Config</button>
<div class="content" id="config">
<!-- Will be populated on load -->
<label>Not Implimented</label>
</div>
</div>
</div>
<div class="inner">
<div class="logcontainer" id="logs">
<!--<div class="inner">END</div>-->
</div>
</div>
</div>
</body>
</html>