diff --git a/drivers/Makefile.am b/drivers/Makefile.am index d5ebd10229..09f577a311 100644 --- a/drivers/Makefile.am +++ b/drivers/Makefile.am @@ -311,10 +311,10 @@ nutdrv_qx_CFLAGS += -DQX_USB nutdrv_qx_SOURCES += $(LIBUSB_IMPL) usb-common.c nutdrv_qx_LDADD += $(LIBUSB_LIBS) endif -NUTDRV_QX_SUBDRIVERS = nutdrv_qx_bestups.c nutdrv_qx_blazer-common.c \ - nutdrv_qx_masterguard.c \ +NUTDRV_QX_SUBDRIVERS = common_voltronic-crc.c nutdrv_qx_bestups.c \ + nutdrv_qx_blazer-common.c nutdrv_qx_masterguard.c \ nutdrv_qx_mecer.c nutdrv_qx_megatec.c nutdrv_qx_megatec-old.c \ - nutdrv_qx_mustek.c nutdrv_qx_q1.c nutdrv_qx_voltronic.c \ + nutdrv_qx_mustek.c nutdrv_qx_q1.c nutdrv_qx_voltronic.c nutdrv_qx_voltronic-axpert.c \ nutdrv_qx_voltronic-qs.c nutdrv_qx_voltronic-qs-hex.c nutdrv_qx_zinto.c \ nutdrv_qx_hunnox.c nutdrv_qx_ablerex.c nutdrv_qx_SOURCES += $(NUTDRV_QX_SUBDRIVERS) @@ -334,11 +334,11 @@ dist_noinst_HEADERS = apc-mib.h apc-iem-mib.h apc-hid.h arduino-hid.h baytech-mi safenet.h serial.h snmp-ups.h solis.h tripplite.h tripplite-hid.h \ upshandler.h usb-common.h usbhid-ups.h powercom-hid.h compaq-mib.h idowell-hid.h \ apcsmart.h apcsmart_tabs.h apcsmart-old.h apcupsd-ups.h cyberpower-mib.h riello.h openups-hid.h \ - delta_ups-mib.h nutdrv_qx.h nutdrv_qx_bestups.h nutdrv_qx_blazer-common.h \ + delta_ups-mib.h nutdrv_qx.h common_voltronic-crc.h nutdrv_qx_bestups.h nutdrv_qx_blazer-common.h \ nutdrv_qx_masterguard.h \ nutdrv_qx_mecer.h nutdrv_qx_ablerex.h \ nutdrv_qx_megatec.h nutdrv_qx_megatec-old.h nutdrv_qx_mustek.h nutdrv_qx_q1.h nutdrv_qx_hunnox.h \ - nutdrv_qx_voltronic.h nutdrv_qx_voltronic-qs.h nutdrv_qx_voltronic-qs-hex.h nutdrv_qx_zinto.h \ + nutdrv_qx_voltronic.h nutdrv_qx_voltronic-axpert.h nutdrv_qx_voltronic-qs.h nutdrv_qx_voltronic-qs-hex.h nutdrv_qx_zinto.h \ xppc-mib.h huawei-mib.h eaton-ats16-nmc-mib.h eaton-ats16-nm2-mib.h apc-ats-mib.h raritan-px2-mib.h eaton-ats30-mib.h \ apc-pdu-mib.h apc-epdu-mib.h ever-hid.h eaton-pdu-genesis2-mib.h eaton-pdu-marlin-mib.h eaton-pdu-marlin-helpers.h \ eaton-pdu-pulizzi-mib.h eaton-pdu-revelation-mib.h emerson-avocent-pdu-mib.h eaton-ups-pwnm2-mib.h eaton-ups-pxg-mib.h legrand-hid.h \ diff --git a/drivers/common_voltronic-crc.c b/drivers/common_voltronic-crc.c new file mode 100644 index 0000000000..5ffbfe24d6 --- /dev/null +++ b/drivers/common_voltronic-crc.c @@ -0,0 +1,291 @@ +/* common_voltronic-crc.c - Common CRC routines for Voltronic Power devices + * + * Copyright (C) + * 2014 Daniele Pezzini + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#include "common_voltronic-crc.h" + +/* CRC table - filled at runtime by common_voltronic_crc_init() */ +static unsigned short crc_table[256]; + +/* Flag (0/1) just to be sure all is properly initialized */ +static int initialized = 0; + +/* Fill CRC table: this function MUST be called before anything else that needs to perform CRC operations */ +static void common_voltronic_crc_init(void) +{ + unsigned short dividend; + + /* Already been here */ + if (initialized) + return; + + /* Compute remainder of each possible dividend */ + for (dividend = 0; dividend < 256; dividend++) { + + unsigned short bit; + /* Start with dividend followed by zeros */ + unsigned long remainder = dividend << 8; + + /* Modulo 2 division, a bit at a time */ + for (bit = 0; bit < 8; bit++) + /* Try to divide the current data bit */ + if (remainder & 0x8000) + remainder = (remainder << 1) ^ 0x1021; + else + remainder <<= 1; + + /* Store the result into the table */ + crc_table[dividend] = remainder & 0xffff; + + } + + /* Ready, now */ + initialized = 1; +} + +/* See header file for details */ +unsigned short common_voltronic_crc_compute(const char *input, const size_t len) +{ + unsigned short crc, crc_MSB, crc_LSB; + unsigned long remainder = 0; + size_t byte; + + /* Make sure all is ready */ + if (!initialized) + common_voltronic_crc_init(); + + /* Divide *input* by the polynomial, a byte at a time */ + for (byte = 0; byte < len; byte++) + remainder = (remainder << 8) ^ crc_table[(input[byte] ^ (remainder >> 8)) & 0xff]; + + /* The final remainder is the CRC */ + crc = remainder & 0xffff; + + /* Escape characters with a special meaning */ + + crc_MSB = (crc >> 8) & 0xff; + if ( + crc_MSB == 10 || /* LF */ + crc_MSB == 13 || /* CR */ + crc_MSB == 40 /* ( */ + ) + crc_MSB++; + + crc_LSB = crc & 0xff; + if ( + crc_LSB == 10 || /* LF */ + crc_LSB == 13 || /* CR */ + crc_LSB == 40 /* ( */ + ) + crc_LSB++; + + crc = ((crc_MSB & 0xff) << 8) + crc_LSB; + + return crc; +} + +/* See header file for details */ +unsigned short common_voltronic_crc_calc(const char *input, const size_t inputlen) +{ + size_t len; + char *cr = memchr(input, '\r', inputlen); + + /* No CR, fall back to string length (and hope *input* doesn't contain inner '\0's) */ + if (cr == NULL) + len = strlen(input); + else + len = cr - input; + + /* At least 1 byte expected */ + if (!len) + return -1; + + /* Compute (and return) CRC */ + return common_voltronic_crc_compute(input, len); +} + +/* See header file for details */ +int common_voltronic_crc_calc_and_add(const char *input, const size_t inputlen, char *output, const size_t outputlen) +{ + unsigned short crc, crc_MSB, crc_LSB; + size_t len; + char *cr = memchr(input, '\r', inputlen); + + /* No CR, fall back to string length (and hope *input* doesn't contain inner '\0's) */ + if (cr == NULL) + len = strlen(input); + else + len = cr - input; + + /* At least 1 byte expected */ + if (!len) + return -1; + + /* To accomodate CRC, *output* must have space for at least 2 bytes more than the actually used size of *input*. + * Also, pretend that *input* is a valid null-terminated string and so reserve the final byte in *output* for the terminating '\0'. */ + if ( + (cr == NULL && outputlen < len + 3) || /* 2-bytes CRC + 1 byte for terminating '\0' */ + (cr != NULL && outputlen < len + 4) /* 2-bytes CRC + 1 byte for trailing CR + 1 byte for terminating '\0' */ + ) + return -1; + + /* Compute CRC */ + crc = common_voltronic_crc_compute(input, len); + crc_MSB = (crc >> 8) & 0xff; + crc_LSB = crc & 0xff; + + /* Clear *output* */ + memset(output, 0, outputlen); + + /* Copy *input* to *output* */ + memcpy(output, input, len); + + /* Write CRC to *output* */ + output[len++] = crc_MSB; + output[len++] = crc_LSB; + + /* Reinstate the trailing CR in *output*, if appropriate */ + if (cr != NULL) + output[len++] = '\r'; + + return (int)len; +} + +/* See header file for details */ +int common_voltronic_crc_calc_and_add_m(char *input, const size_t inputlen) +{ + int len; + char buf[inputlen]; + + /* Compute CRC and copy *input*, with CRC added to it, to buf */ + len = common_voltronic_crc_calc_and_add(input, inputlen, buf, inputlen); + + /* Failed */ + if (len == -1) + return -1; + + /* Successfully computed CRC and copied *input*, with CRC added to it, to buf */ + + /* Clear *input* */ + memset(input, 0, inputlen); + + /* Copy back buf to *input* */ + memcpy(input, buf, len); + + return len; +} + +/* See header file for details */ +int common_voltronic_crc_check(const char *input, const size_t inputlen) +{ + unsigned short crc, crc_MSB, crc_LSB; + char *cr = memchr(input, '\r', inputlen); + size_t len; + + /* No CR, fall back to string length (and hope *input* doesn't contain inner '\0's) */ + if (cr == NULL) + len = strlen(input); + else + len = cr - input; + + /* Minimum length: 1 byte + 2 bytes CRC -> 3 */ + if (len < 3) + return -1; + + /* Compute CRC */ + crc = common_voltronic_crc_compute(input, len - 2); + crc_MSB = (crc >> 8) & 0xff; + crc_LSB = crc & 0xff; + + /* Fail */ + if ( + crc_MSB != (unsigned char)input[len - 2] || + crc_LSB != (unsigned char)input[len - 1] + ) + return -1; + + /* Success */ + return 0; +} + +/* See header file for details */ +int common_voltronic_crc_check_and_remove(const char *input, const size_t inputlen, char *output, const size_t outputlen) +{ + char *cr; + size_t len; + + /* Failed to check *input* CRC */ + if (common_voltronic_crc_check(input, inputlen)) + return -1; + + /* *input* successfully validated -> remove CRC bytes */ + + cr = memchr(input, '\r', inputlen); + /* No CR, fall back to string length */ + if (cr == NULL) + len = strlen(input); + else + len = cr - input; + + /* *output* must have space for at least 2 bytes less than the actually used size of *input*. + * Also, pretend that *input* is a valid null-terminated string and so reserve the final byte in *output* for the terminating '\0'. */ + len -= 2; /* Consider 2-bytes CRC length */ + if ( + (cr == NULL && outputlen < len + 1) || /* 1 byte for terminating '\0' */ + (cr != NULL && outputlen < len + 2) /* 1 byte for terminating '\0' + 1 byte for trailing CR; 2-byte CRC */ + ) + return -1; + + /* Clear *output* */ + memset(output, 0, outputlen); + + /* Copy *input* to *output* */ + memcpy(output, input, len); + + /* Reinstate the trailing CR in *output*, if appropriate */ + if (cr != NULL) + output[len++] = '\r'; + + return (int)len; +} + +/* See header file for details */ +int common_voltronic_crc_check_and_remove_m(char *input, const size_t inputlen) +{ + int len; + char buf[inputlen]; + + /* Check CRC and copy *input*, purged of the CRC, to buf */ + len = common_voltronic_crc_check_and_remove(input, inputlen, buf, inputlen); + + /* Failed */ + if (len == -1) + return -1; + + /* Successfully checked CRC and copied *input*, purged of the CRC, to buf */ + + /* Clear *input* */ + memset(input, 0, inputlen); + + /* Copy back buf to *input* */ + memcpy(input, buf, len); + + return len; +} diff --git a/drivers/common_voltronic-crc.h b/drivers/common_voltronic-crc.h new file mode 100644 index 0000000000..3731127fe7 --- /dev/null +++ b/drivers/common_voltronic-crc.h @@ -0,0 +1,106 @@ +/* common_voltronic-crc.h - Common CRC routines for Voltronic Power devices + * + * Copyright (C) + * 2014 Daniele Pezzini + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef COMMON_VOLTRONIC_CRC_H +#define COMMON_VOLTRONIC_CRC_H + +/* NOTE + * ---- + * The provided functions implement a simple 2-bytes non-reflected CRC (polynomial: 0x1021) as used in some Voltronic Power devices (LF, CR and '(' are 'escaped'). + * + * Apart from the basic functions (that require you to know the exact length, *len*, of what you feed them) to compute CRC and check whether a given byte array, *input*, is CRC-valid or not, some helper functions are provided, that automagically calculate the length of the part of a given *input* (of max size *inputlen*) to be used. + * When using one of these functions (that, having been developed to work with Voltronic Power devices, expect a CR-terminated string), take into consideration that: + * - *input* is considered first as a CR-terminated byte array that may contain inner '\0's and its length is calculated till the position of the expected CR; + * - if a CR cannot be found, then, and only then, *input* is considered as a null-terminated byte string and its length calculated accordingly. + * Therefore, the presence of one (or more) CR automatically: + * - discards whatever stands after (the first CR); + * - includes whatever stands before (the first CR). + * So, as a CR takes precedence, if you know *input* is not (or may be not) CR-terminated, make sure the remaining bytes after the end of the string till *inputlen*-length are all 0'ed (or at least don't contain any CR), if you don't want to risk them being included. + * Also, if you know *input* may contain inner CRs, you better use one of the functions that don't try to guess the part of *input* to be used and cook up instead your own routines using only the provided basic functions to compute and check CRC. + * + * Don't forget, too, that CRC may contain a '\0' and hence null-terminated strings will be fooled. */ + +#include + +/* Compute CRC of a given *input* till the length of *len* bytes. + * Return: + * - the CRC computed from *len* bytes of *input*. */ +unsigned short common_voltronic_crc_compute(const char *input, const size_t len); + +/* Compute CRC (till the first CR, if any, or till the end of the string) of *input* (of max size *inputlen*). + * Please note that *input* must be: + * - at least 1 byte long (not counting the optional trailing CR and the terminating '\0' byte); + * - either a valid null-terminated byte string or CR-terminated. + * Return: + * - -1, on failure (i.e: *input* not fulfilling the aforementioned conditions); + * - the CRC computed from *input*, on success. */ +unsigned short common_voltronic_crc_calc(const char *input, const size_t inputlen); + +/* Compute CRC (till the first CR, if any, or till the end of the string) of *input* (of max size *inputlen*), then write to *output* (of max size *outputlen*): + * - *input* (minus the trailing CR, if any); + * - the computed 2-bytes CRC; + * - a trailing CR (if present in *input*). + * Please note that: + * - *input* must be at least 1 byte long (not counting the optional trailing CR and the terminating '\0' byte) and either be a valid null-terminated byte string or CR-terminated; + * - *output* must have space, to accomodate the computed 2-bytes CRC, for at least 2 bytes more (not counting the trailing, reserved, byte for the terminating '\0') than the actually used size of *input*. + * Return: + * - -1, on failure (i.e: either *input* or *output* not fulfilling the aforementioned conditions); + * - the number of bytes written to *output*, on success. */ +int common_voltronic_crc_calc_and_add(const char *input, const size_t inputlen, char *output, const size_t outputlen); + +/* Compute CRC (till the first CR, if any, or till the end of the string) of *input* (of max size *inputlen*), then add to it the computed 2-bytes CRC (before the trailing CR, if present). + * Please note that *input* must: + * - be at least 1 byte long (not counting the optional trailing CR and the terminating '\0' byte); + * - either be a valid null-terminated byte string or CR-terminated; + * - have space, to accomodate the computed 2-bytes CRC, for at least 2 bytes more (not counting the trailing, reserved, byte for the terminating '\0') than the actually used size. + * Return: + * - -1, on failure (i.e: *input* not fulfilling the aforementioned conditions); + * - the number of bytes that make up the modified *input*, on success. */ +int common_voltronic_crc_calc_and_add_m(char *input, const size_t inputlen); + +/* Check *input* (of max size *inputlen*) CRC. + * Please note that *input* must be: + * - at least 3 bytes long (not counting the optional trailing CR and the terminating '\0' byte); + * - either a valid null-terminated byte string or CR-terminated. + * Return: + * - -1, on failure (i.e: *input* not fulfilling the aforementioned conditions or not CRC-validated); + * - 0, on success (i.e.: *input* successfully validated). */ +int common_voltronic_crc_check(const char *input, const size_t inputlen); + +/* Check *input* (of max size *inputlen*) CRC and copy *input*, purged of the CRC, to *output* (of max size *outputlen*). + * Please note that: + * - *input* must be at least 3 bytes long (not counting the optional trailing CR and the terminating '\0' byte) and either be a valid null-terminated byte string or CR-terminated; + * - *output* must have space for at least 2 bytes less (not counting the trailing, reserved, byte for the terminating '\0') than the actually used size of *input*. + * Return: + * - -1, on failure (i.e: either *input* or *output* not fulfilling the aforementioned conditions or *input* not CRC-validated); + * - the number of bytes written to *output*, on success. */ +int common_voltronic_crc_check_and_remove(const char *input, const size_t inputlen, char *output, const size_t outputlen); + +/* Check *input* (of max size *inputlen*) CRC and remove it from *input*. + * Please note that *input* must be: + * - at least 3 bytes long (not counting the optional trailing CR and the terminating '\0' byte); + * - either a valid null-terminated byte string or CR-terminated. + * Return: + * - -1, on failure; + * - the number of bytes that make up the modified *input*, on success. */ +int common_voltronic_crc_check_and_remove_m(char *input, const size_t inputlen); + +#endif /* COMMON_VOLTRONIC_CRC_H */ diff --git a/drivers/nutdrv_qx.c b/drivers/nutdrv_qx.c index 61400b4cce..7587a1c125 100644 --- a/drivers/nutdrv_qx.c +++ b/drivers/nutdrv_qx.c @@ -78,6 +78,7 @@ #include "nutdrv_qx_voltronic.h" #include "nutdrv_qx_voltronic-qs.h" #include "nutdrv_qx_voltronic-qs-hex.h" +#include "nutdrv_qx_voltronic-axpert.h" #include "nutdrv_qx_zinto.h" #include "nutdrv_qx_masterguard.h" #include "nutdrv_qx_ablerex.h" @@ -85,6 +86,7 @@ /* Reference list of available non-USB subdrivers */ static subdriver_t *subdriver_list[] = { &voltronic_subdriver, + &voltronic_axpert_subdriver, &voltronic_qs_subdriver, &voltronic_qs_hex_subdriver, &mustek_subdriver, @@ -154,7 +156,7 @@ static struct { /* == Support functions == */ static int subdriver_matcher(void); -static ssize_t qx_command(const char *cmd, char *buf, size_t buflen); +static ssize_t qx_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen); static int qx_process_answer(item_t *item, const size_t len); /* returns just 0 or -1 */ static bool_t qx_ups_walk(walkmode_t mode); static void ups_status_set(void); @@ -461,12 +463,13 @@ static USBDeviceMatcher_t *reopen_matcher = NULL; static USBDeviceMatcher_t *regex_matcher = NULL; static int langid_fix = -1; -static int (*subdriver_command)(const char *cmd, char *buf, size_t buflen) = NULL; +static int (*subdriver_command)(const char *cmd, size_t cmdlen, char *buf, size_t buflen) = NULL; /* Cypress communication subdriver */ -static int cypress_command(const char *cmd, char *buf, size_t buflen) +static int cypress_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { char tmp[SMALLBUF]; + size_t tmplen; int ret = 0; size_t i; @@ -479,9 +482,10 @@ static int cypress_command(const char *cmd, char *buf, size_t buflen) /* Send command */ memset(tmp, 0, sizeof(tmp)); - snprintf(tmp, sizeof(tmp), "%s", cmd); + tmplen = cmdlen > sizeof(tmp) ? sizeof(tmp) : cmdlen; + memcpy(tmp, cmd, tmplen); - for (i = 0; i < strlen(tmp); i += (size_t)ret) { + for (i = 0; i < tmplen; i += (size_t)ret) { /* Write data in 8-byte chunks */ /* ret = usb->set_report(udev, 0, (unsigned char *)&tmp[i], 8); */ @@ -537,11 +541,11 @@ static int cypress_command(const char *cmd, char *buf, size_t buflen) } /* SGS communication subdriver */ -static int sgs_command(const char *cmd, char *buf, size_t buflen) +static int sgs_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { char tmp[SMALLBUF]; int ret = 0; - size_t cmdlen, i; + size_t i; if (buflen > INT_MAX) { upsdebugx(3, "%s: requested to read too much (%" PRIuSIZE "), " @@ -551,8 +555,6 @@ static int sgs_command(const char *cmd, char *buf, size_t buflen) } /* Send command */ - cmdlen = strlen(cmd); - for (i = 0; i < cmdlen; i += (size_t)ret) { memset(tmp, 0, sizeof(tmp)); @@ -642,9 +644,10 @@ static int sgs_command(const char *cmd, char *buf, size_t buflen) } /* Phoenix communication subdriver */ -static int phoenix_command(const char *cmd, char *buf, size_t buflen) +static int phoenix_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { char tmp[SMALLBUF]; + size_t tmplen; int ret; size_t i; @@ -689,9 +692,10 @@ static int phoenix_command(const char *cmd, char *buf, size_t buflen) /* Send command */ memset(tmp, 0, sizeof(tmp)); - snprintf(tmp, sizeof(tmp), "%s", cmd); + tmplen = cmdlen > sizeof(tmp) ? sizeof(tmp) : cmdlen; + memcpy(tmp, cmd, tmplen); - for (i = 0; i < strlen(tmp); i += (size_t)ret) { + for (i = 0; i < tmplen; i += (size_t)ret) { /* Write data in 8-byte chunks */ /* ret = usb->set_report(udev, 0, (unsigned char *)&tmp[i], 8); */ @@ -744,9 +748,10 @@ static int phoenix_command(const char *cmd, char *buf, size_t buflen) } /* Ippon communication subdriver */ -static int ippon_command(const char *cmd, char *buf, size_t buflen) +static int ippon_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { char tmp[64]; + size_t tmplen; int ret; size_t i, len; @@ -758,9 +763,11 @@ static int ippon_command(const char *cmd, char *buf, size_t buflen) } /* Send command */ - snprintf(tmp, sizeof(tmp), "%s", cmd); + memset(tmp, 0, sizeof(tmp)); + tmplen = cmdlen > sizeof(tmp) ? sizeof(tmp) : cmdlen; + memcpy(tmp, cmd, tmplen); - for (i = 0; i < strlen(tmp); i += (size_t)ret) { + for (i = 0; i < tmplen; i += (size_t)ret) { /* Write data in 8-byte chunks */ ret = usb_control_msg(udev, @@ -892,7 +899,7 @@ static int hunnox_protocol(int asking_for) } /* Krauler communication subdriver */ -static int krauler_command(const char *cmd, char *buf, size_t buflen) +static int krauler_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { /* Still not implemented: * 0x6 T (don't know how to pass the parameter) @@ -928,7 +935,7 @@ static int krauler_command(const char *cmd, char *buf, size_t buflen) int retry; - if (strcmp(cmd, command[i].str)) { + if (strncmp(cmd, command[i].str, cmdlen)) { continue; } @@ -1024,7 +1031,7 @@ static int krauler_command(const char *cmd, char *buf, size_t buflen) } /* Fabula communication subdriver */ -static int fabula_command(const char *cmd, char *buf, size_t buflen) +static int fabula_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { const struct { const char *str; /* Megatec command */ @@ -1050,7 +1057,7 @@ static int fabula_command(const char *cmd, char *buf, size_t buflen) for (i = 0; commands[i].str; i++) { - if (strcmp(cmd, commands[i].str)) + if (strncmp(cmd, commands[i].str, cmdlen)) continue; index = commands[i].index; @@ -1143,7 +1150,7 @@ static int fabula_command(const char *cmd, char *buf, size_t buflen) /* Hunnox communication subdriver, based on Fabula code above so repeats * much of it currently. Possible future optimization is to refactor shared * code into new routines to be called from both (or more) methods.*/ -static int hunnox_command(const char *cmd, char *buf, size_t buflen) +static int hunnox_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { /* The hunnox_patch was an argument in initial implementation of PR #638 * which added "hunnox" support; keeping it fixed here helps to visibly @@ -1175,7 +1182,7 @@ static int hunnox_command(const char *cmd, char *buf, size_t buflen) for (i = 0; commands[i].str; i++) { - if (strcmp(cmd, commands[i].str)) + if (strncmp(cmd, commands[i].str, cmdlen)) continue; index = commands[i].index; @@ -1309,7 +1316,7 @@ static int hunnox_command(const char *cmd, char *buf, size_t buflen) } /* Fuji communication subdriver */ -static int fuji_command(const char *cmd, char *buf, size_t buflen) +static int fuji_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { unsigned char tmp[8]; char command[SMALLBUF] = "", @@ -1381,7 +1388,7 @@ static int fuji_command(const char *cmd, char *buf, size_t buflen) if (strlen(command) > 3) { /* Be 'megatec-y': echo the unsupported command back */ upsdebugx(3, "%s: unsupported command %s", __func__, command); - return snprintf(buf, buflen, "%s", cmd); + return snprintf(buf, buflen, "%.*s", (int)cmdlen, cmd); } /* Expected length of the answer to the ongoing query @@ -1460,13 +1467,12 @@ static int fuji_command(const char *cmd, char *buf, size_t buflen) } /* Phoenixtec (Masterguard) communication subdriver */ -static int phoenixtec_command(const char *cmd, char *buf, size_t buflen) +static int phoenixtec_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { int ret; char *p, *e = NULL; char *l[] = { "T", "TL", "S", "C", "CT", "M", "N", "O", "SRC", "FCLR", "SS", "TUD", "SSN", NULL }; /* commands that don't return an answer */ char **lp; - size_t cmdlen = strlen(cmd); if (cmdlen > INT_MAX) { upsdebugx(3, "%s: requested command is too long (%" PRIuSIZE ")", @@ -1537,7 +1543,7 @@ static int phoenixtec_command(const char *cmd, char *buf, size_t buflen) } /* SNR communication subdriver */ -static int snr_command(const char *cmd, char *buf, size_t buflen) +static int snr_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { /*ATTENTION: This subdriver uses short buffer with length 102 byte*/ const struct { @@ -1579,7 +1585,7 @@ static int snr_command(const char *cmd, char *buf, size_t buflen) int retry; - if (strcmp(cmd, command[i].str)) { + if (strncmp(cmd, command[i].str, cmdlen)) { continue; } @@ -1655,7 +1661,7 @@ static int snr_command(const char *cmd, char *buf, size_t buflen) return snprintf(buf, buflen, "%s", cmd); } -static int ablerex_command(const char *cmd, char *buf, size_t buflen) +static int ablerex_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { int iii; int len; @@ -1759,12 +1765,11 @@ static void *ablerex_subdriver_fun(USBDevice_t *device) * Richcomm Technologies, Inc. Dec 27 2005 ver 1.1." Maybe other Richcomm UPSes * would work with this - better than with the richcomm_usb driver. */ -static int armac_command(const char *cmd, char *buf, size_t buflen) +static int armac_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { char tmpbuf[6]; int ret = 0; size_t i, bufpos; - const size_t cmdlen = strlen(cmd); /* UPS ignores (doesn't echo back) unsupported commands which makes * the initialization long. List commands tested to be unsupported: @@ -2594,7 +2599,7 @@ void upsdrv_shutdown(void) #ifndef TESTING static const struct { const char *name; - int (*command)(const char *cmd, char *buf, size_t buflen); + int (*command)(const char *cmd, size_t cmdlen, char *buf, size_t buflen); } usbsubdriver[] = { { "cypress", &cypress_command }, { "phoenixtec", &phoenixtec_command }, @@ -3142,7 +3147,7 @@ void upsdrv_cleanup(void) /* Generic command processing function: send a command and read a reply. * Returns < 0 on error, 0 on timeout and the number of bytes read on success. */ -static ssize_t qx_command(const char *cmd, char *buf, size_t buflen) +static ssize_t qx_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { /* NOTE: Could not find in which ifdef-ed codepath, but clang complained * about unused parameters here. Reference them just in case... @@ -3174,7 +3179,7 @@ static ssize_t qx_command(const char *cmd, char *buf, size_t buflen) dstate_setinfo("driver.state", "reconnect.updateinfo"); } - ret = (*subdriver_command)(cmd, buf, buflen); + ret = (*subdriver_command)(cmd, cmdlen, buf, buflen); if (ret >= 0) { return ret; @@ -3258,7 +3263,7 @@ static ssize_t qx_command(const char *cmd, char *buf, size_t buflen) ser_flush_io(upsfd); - ret = ser_send(upsfd, "%s", cmd); + ret = ser_send_buf(upsfd, cmd, cmdlen); if (ret <= 0) { upsdebugx(3, "send: %s (%" PRIiSIZE ")", @@ -3921,8 +3926,8 @@ static int qx_process_answer(item_t *item, const size_t len) /* Short reply */ if (item->answer_len && len < item->answer_len) { - upsdebugx(2, "%s: short reply (%s)", - __func__, item->info_type); + upsdebugx(2, "%s: short reply (%s) %zu<%zu", + __func__, item->info_type, len, item->answer_len); return -1; } @@ -3964,6 +3969,7 @@ int qx_process(item_t *item, const char *command) (strlen(command) >= SMALLBUF ? strlen(command) + 1 : SMALLBUF) : (item->command && strlen(item->command) >= SMALLBUF ? strlen(item->command) + 1 : SMALLBUF); size_t cmdsz = (sizeof(char) * cmdlen); /* in bytes, to be pedantic */ + int cmd_len; if ( !(cmd = xmalloc(cmdsz)) ) { upslogx(LOG_ERR, "qx_process() failed to allocate buffer"); @@ -3972,12 +3978,12 @@ int qx_process(item_t *item, const char *command) /* Prepare the command to be used */ memset(cmd, 0, cmdsz); - snprintf(cmd, cmdsz, "%s", command ? command : item->command); + snprintf(cmd, cmdsz, "%s%n", command ? command : item->command, &cmd_len); /* Preprocess the command */ if ( item->preprocess_command != NULL && - item->preprocess_command(item, cmd, cmdsz) == -1 + (cmd_len = item->preprocess_command(item, cmd, cmdsz)) == -1 ) { upsdebugx(4, "%s: failed to preprocess command [%s]", __func__, item->info_type); @@ -3986,7 +3992,7 @@ int qx_process(item_t *item, const char *command) } /* Send the command */ - len = qx_command(cmd, buf, sizeof(buf)); + len = qx_command(cmd, cmd_len, buf, sizeof(buf)); memset(item->answer, 0, sizeof(item->answer)); diff --git a/drivers/nutdrv_qx.h b/drivers/nutdrv_qx.h index 0fc18e425d..1a766c1c2a 100644 --- a/drivers/nutdrv_qx.h +++ b/drivers/nutdrv_qx.h @@ -91,7 +91,7 @@ typedef struct item_t { int (*preprocess_command)(struct item_t *item, char *command, const size_t commandlen); /* Last chance to preprocess the command to be sent to the UPS (e.g. to add CRC, ...). * This function is given the currently processed item (item), the command to be sent to the UPS (command) and its size_t (commandlen). - * Return -1 in case of errors, else 0. + * Return -1 in case of errors, else 0 if a NUL terminated string, else the length of the command in bytes. * command must be filled with the actual command to be sent to the UPS. */ int (*preprocess_answer)(struct item_t *item, const int len); diff --git a/drivers/nutdrv_qx_voltronic-axpert.c b/drivers/nutdrv_qx_voltronic-axpert.c new file mode 100644 index 0000000000..06624284c4 --- /dev/null +++ b/drivers/nutdrv_qx_voltronic-axpert.c @@ -0,0 +1,4099 @@ +/* nutdrv_qx_voltronic-axpert.c - Subdriver for Voltronic Power Axpert + * + * Copyright (C) + * 2014 Daniele Pezzini + * 2022 Graham Leggett + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#include "main.h" +#include "nutdrv_qx.h" + +#include "nutdrv_qx_voltronic-axpert.h" +#include "common_voltronic-crc.h" + +#define VOLTRONIC_AXPERT_VERSION "Voltronic-Axpert 0.01" + +/* Support functions */ +static int voltronic_sunny_claim(void); +static void voltronic_axpert_initinfo(void); +static void voltronic_sunny_makevartable(void); +static int voltronic_axpert_clear_flags(const char *varname, const unsigned long flag, const unsigned long noflag, const unsigned long clearflag); +static int voltronic_axpert_add_flags(const char *varname, const unsigned long flag, const unsigned long noflag, const unsigned long addflag); +static int voltronic_sunny_checksum(const char *string); +static void voltronic_sunny_update_related_vars_limits(item_t *item, const char *value); +static int voltronic_sunny_OEEPB(void); + +/* Range/enum functions */ +static int voltronic_sunny_pv_priority_enum(char *value, const size_t len); +static int voltronic_sunny_grid_inout_freq_max(char *value, const size_t len); +static int voltronic_sunny_grid_inout_freq_min(char *value, const size_t len); +static int voltronic_sunny_bc_v_bulk(char *value, const size_t len); +static int voltronic_sunny_pv_input_volt_max(char *value, const size_t len); + +/* Answer preprocess functions */ +static int voltronic_axpert_checkcrc(item_t *item, const int len); + +/* Command preprocess functions */ +static int voltronic_axpert_crc(item_t *item, char *command, const size_t commandlen); +static int voltronic_sunny_fault_query(item_t *item, char *command, const size_t commandlen); +static int voltronic_sunny_energy_hour(item_t *item, char *command, const size_t commandlen); +static int voltronic_sunny_energy_day(item_t *item, char *command, const size_t commandlen); +static int voltronic_sunny_energy_month(item_t *item, char *command, const size_t commandlen); +static int voltronic_sunny_energy_year(item_t *item, char *command, const size_t commandlen); + +/* Preprocess functions */ +static int voltronic_axpert_hex_preprocess(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_basic_preprocess(item_t *item, char *value, const size_t valuelen); +static int voltronic_axpert_protocol(item_t *item, char *value, const size_t valuelen); +static int voltronic_axpert_fw(item_t *item, char *value, const size_t valuelen); +static int voltronic_axpert_serial_numb(item_t *item, char *value, const size_t valuelen); +static int voltronic_axpert_capability(item_t *item, char *value, const size_t valuelen); +static int voltronic_axpert_capability_set(item_t *item, char *value, const size_t valuelen); +static int voltronic_axpert_capability_reset(item_t *item, char *value, const size_t valuelen); +static int voltronic_axpert_capability_set_nonut(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_01(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_01_set(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_pv_priority(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_pv_priority_set(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_unskip_setvar(item_t *item, char *value, const size_t valuelen); +static int voltronic_axpert_qpiri_battery_type(item_t *item, char *value, const size_t valuelen); +static int voltronic_axpert_qpiri_model_type(item_t *item, char *value, const size_t valuelen); +static int voltronic_axpert_transformer(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_volt_nom_set(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_process_setvar(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_basic_preprocess_and_update_related_vars_limits(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_yymmdd(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_hh_mm(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_lst(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_set_limits(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_unskip_setvar_and_update_related_vars_limits(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_charger_limits_set(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_discharging_limits_set(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_hhmm(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_hhmm_set(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_hhmm_x2_set(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_pf(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_pfc_set(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_date(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_date_set(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_time(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_time_set(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_process_sign(item_t *item, char *value, const size_t valuelen); +static int voltronic_axpert_status(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_warning(item_t *item, char *value, const size_t valuelen); +static int voltronic_axpert_mode(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_batt_runtime(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_fault(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_date_skip_me(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_time_skip_me(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_skip_me(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_fault_status(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_fault_id(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_self_test_result(item_t *item, char *value, const size_t valuelen); + + +/* == Global vars == */ + +/* Capability vars ("enabled"/"disabled") */ +static char *bypass_alarm, + *battery_alarm; + +/* Global flags */ +static int crc = 1, /* Whether device puts CRC in its replies or not */ + line_loss = 0, /* Whether device has lost connection to the grid or not */ + pv_loss = 0; /* Whether devoce has lost connection to PV or not */ + +static double fw = 0; /* Firmware version */ +static int protocol = 0, /* Protocol used by device */ + model_id = 0, /* Model ID */ + model_type = 0, /* Model type */ + fault_id; /* Fault ID */ + + +/* == Ranges/enums/lengths == */ + +/* Enumlist for battery type */ +static info_rw_t voltronic_axpert_e_battery_type[] = { + { "AGM", 0 }, /* Type: 0 */ + { "Flooded", 0 }, /* Type: 1 */ + { "User", 0 }, /* Type: 2 */ + { "", 0 } + +}; +/* Enumlist for device model type */ +static info_rw_t voltronic_axpert_e_model_type[] = { + { "Grid-tie", 0 }, /* Type: 0 */ + { "Off-grid", 0 }, /* Type: 1 */ + { "Hybrid", 0 }, /* Type: 10 */ + { "Off Grid with 2 Trackers", 0 }, /* Type: 11 */ + { "Off Grid with 3 Trackers", 0 }, /* Type: 20 */ + { "", 0 } +}; + +/* Enumlist for device capabilities that have a NUT var */ +static info_rw_t voltronic_axpert_e_cap[] = { + { "no", 0 }, + { "yes", 0 }, + { "", 0 } +}; + +/* Enumlist for NONUT capabilities */ +static info_rw_t voltronic_axpert_e_cap_nonut[] = { + { "disabled", 0 }, + { "enabled", 0 }, + { "", 0 } +}; + +/* Enumlist for PV energy supply priority */ +static info_rw_t voltronic_sunny_e_pv_priority[] = { + { "Battery-Load", voltronic_sunny_pv_priority_enum }, /* Priority: 01; Type: Off-grid (01) */ + { "Load-Battery", voltronic_sunny_pv_priority_enum }, /* Priority: 02; Type: Off-grid (01); Model: 150 */ + { "Load-Battery (grid relay disconnected)", voltronic_sunny_pv_priority_enum }, /* Priority: 02; Type: Off-grid (01); Model: 151 */ + { "Battery-Load-Grid", voltronic_sunny_pv_priority_enum }, /* Priority: 01; Type: Grid-tie with backup (10) */ + { "Load-Battery-Grid", voltronic_sunny_pv_priority_enum }, /* Priority: 02; Type: Grid-tie with backup (10) */ + { "Load-Grid-Battery", voltronic_sunny_pv_priority_enum }, /* Priority: 03; Type: Grid-tie with backup (10) */ + { "", 0 } +}; + +/* Preprocess enum value for PV energy supply priority */ +static int voltronic_sunny_pv_priority_enum(char *value, const size_t len) +{ + switch (model_type) + { + case 1: /* Off-grid */ + if ( + !strcasecmp(value, "Battery-Load") || /* Priority: 01 */ + !strcasecmp(value, "Load-Battery") || /* Priority: 02; Model: 150 */ + !strcasecmp(value, "Load-Battery (grid relay disconnected)") /* Priority: 02; Model: 151 */ + ) + return 0; + break; + case 10: /* Grid-tie with backup */ + if ( + !strcasecmp(value, "Battery-Load-Grid") || /* Priority: 01 */ + !strcasecmp(value, "Load-Battery-Grid") || /* Priority: 02 */ + !strcasecmp(value, "Load-Grid-Battery") /* Priority: 03 */ + ) + return 0; + break; + case 0: /* Grid-tie */ + case 11: /* Self-use */ + default: + break; + } + + return -1; +} + +/* Enumlist for nominal voltage */ +static info_rw_t voltronic_axpert_e_volt_nom[] = { + { "101", 0 }, /* Low voltage models */ + { "110", 0 }, /* Low voltage models */ + { "120", 0 }, /* Low voltage models */ + { "127", 0 }, /* Low voltage models */ + { "202", 0 }, /* High voltage models */ + { "208", 0 }, /* High voltage models */ + { "220", 0 }, /* High voltage models */ + { "230", 0 }, /* High voltage models */ + { "240", 0 }, /* High voltage models */ + { "", 0 } +}; + +/* Enumlist for nominal frequency */ +static info_rw_t voltronic_sunny_e_freq_nom[] = { + { "50", 0 }, + { "60", 0 }, + { "", 0 } +}; + +/* Range for number of MPP trackers in use */ +static info_rw_t voltronic_sunny_r_mpp_number[] = { + { "01", 0 }, + { "99", 0 }, + { "", 0 } +}; + +/* Range for maximum grid output voltage: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #1, #2) */ +static info_rw_t voltronic_sunny_r_grid_output_volt_max[] = { + { "240", 0 }, + { "276", 0 }, + { "", 0 } +}; + +/* Range for maximum grid input voltage: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #1, #2) */ +static info_rw_t voltronic_sunny_r_grid_input_volt_max[] = { + { "240", 0 }, + { "280", 0 }, + { "", 0 } +}; + +/* Range for minimum grid output voltage: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #3, #4) */ +static info_rw_t voltronic_sunny_r_grid_output_volt_min[] = { + { "176", 0 }, + { "220", 0 }, + { "", 0 } +}; + +/* Range for minimum grid input voltage: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #3, #4) */ +static info_rw_t voltronic_sunny_r_grid_input_volt_min[] = { + { "175", 0 }, + { "220", 0 }, + { "", 0 } +}; + +/* Range for maximum grid input/output frequency: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #5, #6) */ +static info_rw_t voltronic_sunny_r_grid_inout_freq_max[] = { /* FIXME: ranges only support ints */ + { "50.2", voltronic_sunny_grid_inout_freq_max }, /* Nominal frequency (QPIRI #2) == 50.0 */ + { "54.8", voltronic_sunny_grid_inout_freq_max }, /* Nominal frequency (QPIRI #2) == 50.0 */ + { "60.2", voltronic_sunny_grid_inout_freq_max }, /* Nominal frequency (QPIRI #2) == 60.0 */ + { "64.8", voltronic_sunny_grid_inout_freq_max }, /* Nominal frequency (QPIRI #2) == 60.0 */ + { "", 0 } +}; + +/* Preprocess range value for (not overwritten) maximum grid input/output frequency */ +static int voltronic_sunny_grid_inout_freq_max(char *value, const size_t len) +{ + char *ptr; + const int val = strtol(value, &ptr, 10) * 10 + (*ptr == '.' ? strtol(++ptr, NULL, 10) : 0); + double gfn; + const char *gridfreqnom = dstate_getinfo("grid.frequency.nominal"); + + if (!gridfreqnom) { + upsdebugx(2, "%s: unable to get grid.frequency.nominal", __func__); + return -1; + } + + gfn = strtod(gridfreqnom, NULL); + + switch (val) + { + case 502: + case 548: + /* Nominal frequency (QPIRI #2) == 50.0 */ + if (gfn == 50.0) + return 0; + break; + case 602: + case 648: + /* Nominal frequency (QPIRI #2) == 60.0 */ + if (gfn == 60.0) + return 0; + break; + default: + upsdebugx(2, "%s: unknown value (%s)", __func__, value); + break; + } + + return -1; +} + +/* Range for minimum grid input/output frequency: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #7, #8) */ +static info_rw_t voltronic_sunny_r_grid_inout_freq_min[] = { /* FIXME: ranges only support ints */ + { "45.0", voltronic_sunny_grid_inout_freq_min }, /* Nominal frequency (QPIRI #2) == 50.0 */ + { "49.8", voltronic_sunny_grid_inout_freq_min }, /* Nominal frequency (QPIRI #2) == 50.0 */ + { "55.0", voltronic_sunny_grid_inout_freq_min }, /* Nominal frequency (QPIRI #2) == 60.0 */ + { "59.8", voltronic_sunny_grid_inout_freq_min }, /* Nominal frequency (QPIRI #2) == 60.0 */ + { "", 0 } +}; + +/* Preprocess range value for (not overwritten) minimum grid input/output frequency */ +static int voltronic_sunny_grid_inout_freq_min(char *value, const size_t len) +{ + char *ptr; + const int val = strtol(value, &ptr, 10) * 10 + (*ptr == '.' ? strtol(++ptr, NULL, 10) : 0); + double gfn; + const char *gridfreqnom = dstate_getinfo("grid.frequency.nominal"); + + if (!gridfreqnom) { + upsdebugx(2, "%s: unable to get grid.frequency.nominal", __func__); + return -1; + } + + gfn = strtod(gridfreqnom, NULL); + + switch (val) + { + case 450: + case 498: + /* Nominal frequency (QPIRI #2) == 50.0 */ + if (gfn == 50.0) + return 0; + break; + case 550: + case 598: + /* Nominal frequency (QPIRI #2) == 60.0 */ + if (gfn == 60.0) + return 0; + break; + default: + upsdebugx(2, "%s: unknown value (%s)", __func__, value); + break; + } + + return -1; +} + +/* Range for waiting time before grid connection: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #9, #10) */ +static info_rw_t voltronic_sunny_r_grid_waiting_time[] = { + { "5", 0 }, + { "999", 0 }, + { "", 0 } +}; + +/* Range for maximum battery-charging current: filled at runtime by voltronic_sunny_set_limits() (QVFTR #11, #12), overwritten (if appropriate) by voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_bc_v_floating[] = { + { "", 0 }, + { "", 0 }, + { "", 0 } +}; + +/* Range for maximum battery-charging current: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #13, #14) and voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_bc_c_max[] = { /* FIXME: ranges only support ints */ + { "0.5", 0 }, + { "25.0", 0 }, + { "", 0 } +}; + +/* Range for bulk battery-charging voltage: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #25, #26) and voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_bc_v_bulk[] = { + { "47", 0 }, + { "50", voltronic_sunny_bc_v_bulk }, /* IFF FW version (QSVFW2) >= 0.3 */ + { "57", voltronic_sunny_bc_v_bulk }, /* IFF FW version (QSVFW2) < 0.3 */ + { "", 0 } +}; + +/* Preprocess range value for (not overwritten) bulk battery-charging voltage */ +static int voltronic_sunny_bc_v_bulk(char *value, const size_t len) +{ + const int val = strtol(value, NULL, 10); + + switch (val) + { + case 50: /* FW version (QSVFW2) >= 0.3 */ + if (fw >= 0.3) + return 0; + break; + case 57: /* FW version (QSVFW2) < 0.3 */ + if (fw < 0.3) + return 0; + break; + default: + upsdebugx(2, "%s: unknown value (%s)", __func__, value); + break; + } + + return -1; +} + +/* Range for minimum floating battery-charging current: filled at runtime by voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_bc_c_floating_low[] = { + { "0", 0 }, + { "", 0 }, + { "", 0 } +}; + +/* Range for restart battery-charging voltage: filled at runtime by voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_bc_v_restart[] = { + { "0", 0 }, + { "", 0 }, + { "", 0 } +}; + +/* Range for floating battery-charging current time thershold */ +static info_rw_t voltronic_sunny_r_bc_time_threshold[] = { + { "0", 0 }, + { "900", 0 }, + { "", 0 } +}; + +/* Range for cut-off battery-discharging voltage when grid is not available: overwritten (if appropriate) at runtime by voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_bd_v_cutoff_gridoff[] = { + { "40", 0 }, + { "48", 0 }, + { "", 0 } +}; + +/* Range for cut-off battery-discharging voltage when grid is available: overwritten (if appropriate) at runtime by voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_bd_v_cutoff_gridon[] = { + { "40", 0 }, + { "48", 0 }, + { "", 0 } +}; + +/* Range for restart battery-discharging voltage when grid is unavailable: filled at runtime by voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_bd_v_restart_gridoff[] = { + { "", 0 }, + { "", 0 }, + { "", 0 } +}; + +/* Range for restart battery-discharging voltage when grid is available: filled at runtime by voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_bd_v_restart_gridon[] = { + { "", 0 }, + { "", 0 }, + { "", 0 } +}; + +/* Range for maximum PV input voltage: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #15, #16) */ +static info_rw_t voltronic_sunny_r_pv_input_volt_max[] = { + { "450", 0 }, + { "510", voltronic_sunny_pv_input_volt_max }, /* P16 */ + { "550", voltronic_sunny_pv_input_volt_max }, /* P15 */ + { "", 0 } +}; + +/* Preprocess range value for (not overwritten) maximum PV input voltage */ +static int voltronic_sunny_pv_input_volt_max(char *value, const size_t len) +{ + const int val = strtol(value, NULL, 10); + + switch (val) + { + case 510: /* P16 */ + if (protocol == 16) + return 0; + break; + case 550: /* P15 */ + if (protocol == 15) + return 0; + break; + default: + upsdebugx(2, "%s: unknown value (%s)", __func__, value); + break; + } + + return -1; +} + +/* Range for minimum PV input voltage: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #17, #18) */ +static info_rw_t voltronic_sunny_r_pv_input_volt_min[] = { + { "90", 0 }, + { "200", 0 }, + { "", 0 } +}; + +/* Range for maximum MPP voltage: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #19, #20) */ +static info_rw_t voltronic_sunny_r_mpp_input_volt_max[] = { + { "400", 0 }, + { "450", 0 }, + { "", 0 } +}; + +/* Range for minimum MPP voltage: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #21, #22) */ +static info_rw_t voltronic_sunny_r_mpp_input_volt_min[] = { + { "110", 0 }, + { "200", 0 }, + { "", 0 } +}; + +/* Range for maximum output power: filled(/overwritten, if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #23, #24) and voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_output_realpower_max[] = { + { "0", 0 }, + { "", 0 }, + { "", 0 } +}; + +/* Range for maximum power feeding grid: max value filled at runtime by voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_grid_realpower_max[] = { + { "0", 0 }, + { "", 0 }, + { "", 0 } +}; + +/* Range for LCD sleep time (time after which LCD screen-saver starts) */ +static info_rw_t voltronic_sunny_r_lcd_sleep_time[] = { + { "0", 0 }, /* 00 */ + { "2970", 0 }, /* 99 */ + { "", 0 } +}; + +/* Time (hh:mm) length */ +static info_rw_t voltronic_sunny_l_hhmm[] = { + { "5", 0 }, + { "", 0 } +}; + +/* Range for maximum grid input average voltage */ +static info_rw_t voltronic_sunny_r_grid_in_avg_volt_max[] = { + { "235", 0 }, + { "265", 0 }, + { "", 0 } +}; + +/* Range for grid power deviation */ +static info_rw_t voltronic_sunny_r_grid_power_deviation[] = { + { "0", 0 }, + { "999", 0 }, + { "", 0 } +}; + +/* Range for power factor */ +static info_rw_t voltronic_sunny_r_output_powerfactor[] = { /* FIXME: 1. nutdrv_qx setvar+RANGE doesn't support negative values; 2. values should be divided by 100 */ + { "-99", 0 }, + { "-90", 0 }, + { "90", 0 }, + { "100", 0 }, + { "", 0 } +}; + +/* Range for power percent setting */ +static info_rw_t voltronic_sunny_r_powerpercent_setting[] = { + { "10", 0 }, + { "100", 0 }, + { "", 0 } +}; + +/* Range for power factor_percent */ +static info_rw_t voltronic_sunny_r_powerfactor_percent[] = { + { "50", 0 }, + { "100", 0 }, + { "", 0 } +}; + +/* Range for power factor curve */ +static info_rw_t voltronic_sunny_r_powerfactor_curve[] = { /* FIXME: nutdrv_qx setvar+RANGE doesn't support negative values */ + { "-99", 0 }, + { "-90", 0 }, + { "", 0 } +}; + +/* Date (YYYY/MM/DD) length */ +static info_rw_t voltronic_sunny_l_date[] = { + { "10", 0 }, + { "", 0 } +}; + +/* Time (hh:mm:ss) length */ +static info_rw_t voltronic_sunny_l_time[] = { + { "8", 0 }, + { "", 0 } +}; + + +/* == qx2nut lookup table == */ +static item_t voltronic_axpert_qx2nut[] = { + + /*#######################################################################################################################################################################################################################################################################################################################################################################################################* + *# info_type |info_flags |info_rw |command |answer |answer |leading|value |from |to |dfl |qxflags |preprocess_command |preprocess_answer |preprocess #* + *# | | | |_len | | | | | | | | | | #* + *#######################################################################################################################################################################################################################################################################################################################################################################################################*/ + + /* Query device for protocol + * > [QPI\r] + * < [(PI30\r] + * 012345 + * 0 + */ + + { "device.firmware.aux", 0, NULL, "QPI\r", "", 6, '(', "", 1, 4, "%s", QX_FLAG_STATIC, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_protocol }, + + /* Query device for firmware version + * > [QVFW\r] + * < [(VERFW:00074.50\r] + * 0123456789012345 + * 0 1 + */ + + { "device.firmware", 0, NULL, "QVFW\r", "", 16, '(', "", 7, 14, "0X%s", QX_FLAG_STATIC, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_fw }, + + /* Query device for main CPU processor version + * > [QVFW\r] + * < [(VERFW:00074.50\r] + * 0123456789012345 + * 0 1 + */ + + { "device.firmware.main", 0, NULL, "QVFW\r", "", 16, '(', "", 7, 14, "0X%s", QX_FLAG_STATIC, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_hex_preprocess }, + + /* Query device for SCC1 CPU Firmware version + * > [QVFW2\r] + * < [(VERFW2:00000.31\r] + * 01234567890123456 + * 0 1 + */ + + { "device.firmware.scc1", 0, NULL, "QVFW2\r", "", 17, '(', "", 8, 15, "0X%s", QX_FLAG_STATIC, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_hex_preprocess }, + + /* Query device for SCC2 CPU Firmware version + * > [QVFW3\r] + * < [(VERFW3:00000.31\r] + * 01234567890123456 + * 0 1 + */ + + { "device.firmware.scc2", 0, NULL, "QVFW3\r", "", 17, '(', "", 8, 15, "0X%s", QX_FLAG_STATIC, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_hex_preprocess }, + + /* Query device for SCC3 CPU Firmware version + * > [QVFW4\r] + * < [(VERFW4:00000.31\r] + * 01234567890123456 + * 0 1 + */ + + { "device.firmware.scc3", 0, NULL, "QVFW4\r", "", 17, '(', "", 8, 15, "0X%s", QX_FLAG_STATIC, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_hex_preprocess }, + + + /* Query device for serial number + * > [QID\r] + * < [(12345679012345\r] <- As far as I know it hasn't a fixed length -> min length = ( + \r = 2 + * 0123456789012345 + * 0 1 + */ + + { "device.serial", 0, NULL, "QID\r", "", 2, '(', "", 1, 0, "%s", QX_FLAG_STATIC, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_serial_numb }, + + /*#######################################################################################################################################################################################################################################################################################################################################################################################################################################*/ + + /* Query device for capability; only those capabilities whom the device is capable of are reported as Enabled or Disabled + * > [QFLAG\r] + * < [(EakxyDbjuvz\r] + * 01234567890123 + * 0 * min length = ( + E + D + \r = 4 + * + * A Enable/disable silence buzzer or open buzzer + * B Enable/Disable overload bypass function + * J Enable/Disable power saving + * K Enable/Disable LCD display escape to default page after 1min timeout + * U Enable/Disable overload restart + * V Enable/Disable over temperature restart + * X Enable/Disable backlight on + * Y Enable/Disable alarm on when primary source interrupt + * Z Enable/Disable fault code record + * L Enable/Disable data log pop-up + */ + + { "battery.energysave", ST_FLAG_RW, voltronic_axpert_e_cap, "QFLAG\r", "", 4, '(', "", 1, 0, "%s", QX_FLAG_ENUM | QX_FLAG_SEMI_STATIC, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_capability }, + { "ups.beeper.status", 0, NULL, "QFLAG\r", "", 4, '(', "", 1, 0, "%s", QX_FLAG_SEMI_STATIC, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_capability }, +#if 0 + /* Not available in NUT */ + { "bypass_alarm", 0, NULL, "QFLAG\r", "", 4, '(', "", 1, 0, "%s", QX_FLAG_SEMI_STATIC | QX_FLAG_NONUT, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_capability }, + { "battery_alarm", 0, NULL, "QFLAG\r", "", 4, '(', "", 1, 0, "%s", QX_FLAG_SEMI_STATIC | QX_FLAG_NONUT, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_capability }, +#endif + +#if 0 + /* Enable or Disable or Reset to safe default values capability options + * > [PEX\r] > [PDX\r] > [PF\r] + * < [(ACK\r] < [(ACK\r] < [(ACK\r] + * 01234 01234 01234 + * 0 0 0 + */ + + { "battery.energysave", 0, voltronic_axpert_e_cap, "P%sJ\r", "", 5, '(', "", 1, 3, NULL, QX_FLAG_SETVAR | QX_FLAG_ENUM | QX_FLAG_SKIP, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_capability_set }, + /* Not available in NUT */ + { "reset_to_default", 0, NULL, "PF\r", "", 5, '(', "", 1, 3, NULL, QX_FLAG_SETVAR | QX_FLAG_NONUT, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_capability_reset }, + { "bypass_alarm", 0, voltronic_axpert_e_cap_nonut, "P%sP\r", "", 5, '(', "", 1, 3, NULL, QX_FLAG_SETVAR | QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SKIP, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_capability_set_nonut }, + { "battery_alarm", 0, voltronic_axpert_e_cap_nonut, "P%sB\r", "", 5, '(', "", 1, 3, NULL, QX_FLAG_SETVAR | QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SKIP, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_capability_set_nonut }, +#endif + +#if 0 + /* Query device for operational options flag (P16 only) + * > [QENF\r] + * < [(A1B1C1D1E1F0G1\r] <- required options (length: 16) + * < [(A1B0C1D0E1F0G0H0I_J_\r] <- known available options + * 0123456789012345678901 + * 0 1 2 + */ + + { "charge_battery", ST_FLAG_RW, voltronic_axpert_e_cap_nonut, "QENF\r", "", 16, '(', "", 2, 2, "%s", QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SEMI_STATIC, NULL, voltronic_axpert_checkcrc, voltronic_sunny_01 }, /* A */ + { "charge_battery_from_ac", ST_FLAG_RW, voltronic_axpert_e_cap_nonut, "QENF\r", "", 16, '(', "", 4, 4, "%s", QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SEMI_STATIC, NULL, voltronic_axpert_checkcrc, voltronic_sunny_01 }, /* B */ + { "feed_grid", ST_FLAG_RW, voltronic_axpert_e_cap_nonut, "QENF\r", "", 16, '(', "", 6, 6, "%s", QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SEMI_STATIC, NULL, voltronic_axpert_checkcrc, voltronic_sunny_01 }, /* C */ + { "discharge_battery_when_pv_on", ST_FLAG_RW, voltronic_axpert_e_cap_nonut, "QENF\r", "", 16, '(', "", 8, 8, "%s", QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SEMI_STATIC, NULL, voltronic_axpert_checkcrc, voltronic_sunny_01 }, /* D */ + { "discharge_battery_when_pv_off", ST_FLAG_RW, voltronic_axpert_e_cap_nonut, "QENF\r", "", 16, '(', "", 10, 10, "%s", QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SEMI_STATIC, NULL, voltronic_axpert_checkcrc, voltronic_sunny_01 }, /* E */ + { "feed_grid_from_battery_when_pv_on", ST_FLAG_RW, voltronic_axpert_e_cap_nonut, "QENF\r", "", 16, '(', "", 12, 12, "%s", QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SEMI_STATIC, NULL, voltronic_axpert_checkcrc, voltronic_sunny_01 }, /* F */ + { "feed_grid_from_battery_when_pv_off", ST_FLAG_RW, voltronic_axpert_e_cap_nonut, "QENF\r", "", 16, '(', "", 14, 14, "%s", QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SEMI_STATIC, NULL, voltronic_axpert_checkcrc, voltronic_sunny_01 }, /* G */ +/* { "unknown.?", ST_FLAG_RW, voltronic_axpert_e_cap_nonut, "QENF\r", "", 18, '(', "", 16, 16, "%s", QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SEMI_STATIC, NULL, voltronic_axpert_checkcrc, voltronic_sunny_01 }, *//* H */ +/* { "unknown.?", ST_FLAG_RW, voltronic_axpert_e_cap_nonut, "QENF\r", "", 20, '(', "", 18, 18, "%s", QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SEMI_STATIC, NULL, voltronic_axpert_checkcrc, voltronic_sunny_01 }, *//* I */ +/* { "unknown.?", ST_FLAG_RW, voltronic_axpert_e_cap_nonut, "QENF\r", "", 22, '(', "", 20, 20, "%s", QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SEMI_STATIC, NULL, voltronic_axpert_checkcrc, voltronic_sunny_01 }, *//* J */ + + /* Enable (: 1) or disable (: 0) operational option