From 45147c0a3a7436d4d97d63c87dff7bf6a61d4ffd Mon Sep 17 00:00:00 2001 From: Lucas Bocchi Date: Sun, 30 Aug 2026 19:17:01 -0300 Subject: [PATCH 1/5] Fix NHS serial packet framing and model handling Honor the packet length declared at byte 1 so embedded 0xFF and 0xFE payload bytes do not restart or truncate valid frames. Preserve partial packet state across serial-read timeouts and validate the declared length and final marker before dispatch. Add the compatibility initialization request, model description override, protocol metadata and per-model bypass alarm behavior. Correct the published input voltage extrema and document the changes. Signed-off-by: Lucas Bocchi --- NEWS.adoc | 21 + drivers/nhs_ser.c | 1415 ++++++++++++++------------------------------- 2 files changed, 454 insertions(+), 982 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index 8da073ce8e..62baf07e01 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -122,6 +122,27 @@ https://github.com/networkupstools/nut/milestone/13 rate, data bits, parity, stop bits, flow control, per-byte read timeout, and transmission pacing. Also improved serial resource handling and reconnection behavior. [issue #3510, PR #3543] + * Reworked incoming serial framing to use the total packet length declared + by byte `[1]`, instead of treating every `0xFF` as a new packet and every + `0xFE` as its end. A leading `0xFF` now starts a frame only while the + reader is idle; subsequent `0xFF` and `0xFE` values are preserved as + payload or checksum data. The frame is completed at its declared length + and accepted only when that final byte is `0xFE`. Declared lengths are + also bounded by the receive buffer before further bytes are stored. + This prevents valid 18-byte and 50-byte HWINFO replies containing an + internal `0xFF` from being split into unrecognized fragments, allowing + the driver to obtain model, firmware, serial number and battery-pack + information instead of repeatedly requesting HWINFO and delaying normal + polling. [issue #3592] + * Added the legacy five-byte compatibility initialization request + (`FF 05 01 06 FE`) as a third fallback. The driver first makes six + extended initialization attempts, then selects randomly among the + extended, normal and compatibility requests so older or otherwise + compatibility-dependent NHS units can answer with their supported + protocol variant. + * Consolidated the NHS model-code metadata into an indexed lookup table, + preserving the existing model descriptions and nominal VA mappings while + making unknown-code handling explicit. - `nutdrv_qx` driver updates: * Added the `richcomm-svc` USB communication subdriver for older diff --git a/drivers/nhs_ser.c b/drivers/nhs_ser.c index 79e7d9e027..ce6db6dea2 100644 --- a/drivers/nhs_ser.c +++ b/drivers/nhs_ser.c @@ -229,16 +229,22 @@ typedef struct { unsigned int upscode; char upsdesc[100]; unsigned int VA; + /* Protocol generation associated with the model; currently informational. */ + unsigned int pversion; + /* Set to 1 to publish the received bypass bit as a NUT alarm. */ + unsigned int bypassasalarm; } upsinfo; static const unsigned int string_initialization_long[9] = {0xFF, 0x09, 0x53, 0x83, 0x00, 0x00, 0x00, 0xDF, 0xFE}; static const unsigned int string_initialization_short[9] = {0xFF, 0x09, 0x53, 0x03, 0x00, 0x00, 0x00, 0x5F, 0xFE}; +static const unsigned int string_initialization_comptmode[5] = {0xFF, 0x05, 0x01, 0x06, 0xFE}; static int debug_pkt_data = 0, debug_pkt_hwinfo = 0, debug_pkt_raw = 0; static TYPE_FD_SER serial_fd = ERROR_FD_SER; static unsigned char chr; static size_t datapacket_index = 0; +static unsigned int datapacketsize = 0; static bool datapacketstart = false; static time_t lastdp = 0; static unsigned int checktime = 2000000; /* 2 seconds */ @@ -399,9 +405,8 @@ static int get_bit_in_position(void *ptr, size_t size, size_t bit_position, int size_t byte_index = bit_position / 8; size_t bit_index = bit_position % 8; - if (bit_position >= size * 8) { + if (bit_position >= size * 8) return -3; /* Invalid Position */ - } if (invertorder == 0) retval = (byte_ptr[byte_index] >> (7 - bit_index)) & 1 ? 1 : 0; @@ -411,7 +416,7 @@ static int get_bit_in_position(void *ptr, size_t size, size_t bit_position, int } static void print_pkt_hwinfo(pkt_hwinfo data) { - int i = 0; + int i = 0, retorno; if (!debug_pkt_hwinfo) return; @@ -429,10 +434,10 @@ static void print_pkt_hwinfo(pkt_hwinfo data) { upsdebugx(5, "Configuration Array: "); upsdebugx(5, "-----"); for (i = 0; i < 5; i++) { - int retorno = get_bit_in_position(&data.configuration, sizeof(data.configuration), i, 0); + retorno = get_bit_in_position(&data.configuration, sizeof(data.configuration), i, 0); upsdebugx(5, "Binary value is %d", retorno); upsdebugx(5, "%u ", data.configuration_array[i]); - } + } /* end for */ upsdebugx(5, "-----"); upsdebugx(5, "OEM Mode: %s", data.c_oem_mode ? "true" : "false"); @@ -454,7 +459,7 @@ static void print_pkt_hwinfo(pkt_hwinfo data) { for (i = 0; i < 6; i++) { upsdebugx(5, "Binary value is %d", get_bit_in_position(&data.statusval, sizeof(data.statusval), i, 0)); upsdebugx(5, "status %d --> %u ", i, data.status[i]); - } + } /* end for */ upsdebugx(5, "-----"); upsdebugx(5, "220V In: %s", data.s_220V_in ? "true" : "false"); @@ -527,7 +532,7 @@ static void print_pkt_data(pkt_data data) { for (i = 0; i < 8; i++) { upsdebugx(5, "Binary value is %d", get_bit_in_position(&data.statusval, sizeof(data.statusval), i, 0)); upsdebugx(5, "status %d --> %u ", i, data.status[i]); - } + } /* end for */ upsdebugx(5, "-----"); upsdebugx(5, "Nominal Tension: %u", data.nominaltension); @@ -563,8 +568,7 @@ static void parse_serial_options(void) serial_data_bits = DEFAULT_SERIAL_DATA_BITS; snprintf(serial_parity, sizeof(serial_parity), "%s", DEFAULT_SERIAL_PARITY); serial_stop_bits = DEFAULT_SERIAL_STOP_BITS; - snprintf(serial_flow_control, sizeof(serial_flow_control), "%s", - DEFAULT_SERIAL_FLOW_CONTROL); + snprintf(serial_flow_control, sizeof(serial_flow_control), "%s", DEFAULT_SERIAL_FLOW_CONTROL); serial_read_timeout_ms = DEFAULT_SERIAL_READ_TIMEOUT_MS; serial_send_pace_us = DEFAULT_SERIAL_SEND_PACE_US; @@ -577,26 +581,17 @@ static void parse_serial_options(void) errno = 0; endptr = NULL; number = strtol(value, &endptr, 10); - if (errno != 0 || endptr == value || *endptr != '\0' || number <= 0) { - fatalx(EXIT_FAILURE, - "Invalid baud value '%s': expected a supported baud rate", - value); - } + if (errno != 0 || endptr == value || *endptr != '\0' || number <= 0) + fatalx(EXIT_FAILURE, "Invalid baud value '%s': expected a supported baud rate", value); supported = 0; - for (i = 0; i < NUM_BAUD_RATES; i++) { - if (number == baud_rates[i].speed) { + for (i = 0; i < NUM_BAUD_RATES && !supported; i++) + if (number == baud_rates[i].speed) supported = 1; - break; - } - } - if (!supported) { - fatalx(EXIT_FAILURE, - "Invalid baud value '%s': rate is not available in this build", - value); - } + if (!supported) + fatalx(EXIT_FAILURE, "Invalid baud value '%s': rate is not available in this build", value); baudrate = (int)number; - } + } /* end if */ value = getval("serial_data_bits"); if (value) { @@ -604,54 +599,34 @@ static void parse_serial_options(void) errno = 0; endptr = NULL; number = strtol(value, &endptr, 10); - if (errno != 0 || endptr == value || *endptr != '\0' || - (number != 5 && number != 6 && number != 7 && number != 8)) { - fatalx(EXIT_FAILURE, - "Invalid serial_data_bits value '%s': expected 5, 6, 7 or 8", - value); - } + if (errno != 0 || endptr == value || *endptr != '\0' || (number != 5 && number != 6 && number != 7 && number != 8)) + fatalx(EXIT_FAILURE, "Invalid serial_data_bits value '%s': expected 5, 6, 7 or 8", value); serial_data_bits = (unsigned int)number; - } + } /* end if */ value = getval("serial_parity"); if (value) { - if (strcasecmp(value, "none") != 0 && - strcasecmp(value, "even") != 0 && - strcasecmp(value, "odd") != 0) { - fatalx(EXIT_FAILURE, - "Invalid serial_parity value '%s': expected none, even or odd", - value); - } + if (strcasecmp(value, "none") != 0 && strcasecmp(value, "even") != 0 && strcasecmp(value, "odd") != 0) + fatalx(EXIT_FAILURE, "Invalid serial_parity value '%s': expected none, even or odd", value); snprintf(serial_parity, sizeof(serial_parity), "%s", value); - } + } /* end if */ value = getval("serial_stop_bits"); if (value) { errno = 0; endptr = NULL; number = strtol(value, &endptr, 10); - if (errno != 0 || endptr == value || *endptr != '\0' || - (number != 1 && number != 2)) { - fatalx(EXIT_FAILURE, - "Invalid serial_stop_bits value '%s': expected 1 or 2", - value); - } + if (errno != 0 || endptr == value || *endptr != '\0' || (number != 1 && number != 2)) + fatalx(EXIT_FAILURE, "Invalid serial_stop_bits value '%s': expected 1 or 2", value); serial_stop_bits = (unsigned int)number; - } + } /* end if */ value = getval("serial_flow_control"); if (value) { - if (strcasecmp(value, "none") != 0 && - strcasecmp(value, "hardware") != 0 && - strcasecmp(value, "software") != 0 && - strcasecmp(value, "both") != 0) { - fatalx(EXIT_FAILURE, - "Invalid serial_flow_control value '%s': expected " - "none, hardware, software or both", - value); - } + if (strcasecmp(value, "none") != 0 && strcasecmp(value, "hardware") != 0 && strcasecmp(value, "software") != 0 && strcasecmp(value, "both") != 0) + fatalx(EXIT_FAILURE, "Invalid serial_flow_control value '%s': expected none, hardware, software or both", value); snprintf(serial_flow_control, sizeof(serial_flow_control), "%s", value); - } + } /* end if */ value = getval("serial_read_timeout_ms"); if (value) { @@ -659,14 +634,10 @@ static void parse_serial_options(void) errno = 0; endptr = NULL; number = strtol(value, &endptr, 10); - if (errno != 0 || endptr == value || *endptr != '\0' || - number < 0 || number > MAX_SERIAL_READ_TIMEOUT_MS) { - fatalx(EXIT_FAILURE, - "Invalid serial_read_timeout_ms value '%s': expected 0 to %d", - value, MAX_SERIAL_READ_TIMEOUT_MS); - } + if (errno != 0 || endptr == value || *endptr != '\0' || number < 0 || number > MAX_SERIAL_READ_TIMEOUT_MS) + fatalx(EXIT_FAILURE, "Invalid serial_read_timeout_ms value '%s': expected 0 to %d", value, MAX_SERIAL_READ_TIMEOUT_MS); serial_read_timeout_ms = (unsigned int)number; - } + } /* end if */ value = getval("serial_send_pace_us"); if (value) { @@ -674,26 +645,20 @@ static void parse_serial_options(void) errno = 0; endptr = NULL; number = strtol(value, &endptr, 10); - if (errno != 0 || endptr == value || *endptr != '\0' || - number < 0 || number > MAX_SERIAL_SEND_PACE_US) { - fatalx(EXIT_FAILURE, - "Invalid serial_send_pace_us value '%s': expected 0 to %d", - value, MAX_SERIAL_SEND_PACE_US); - } + if (errno != 0 || endptr == value || *endptr != '\0' || number < 0 || number > MAX_SERIAL_SEND_PACE_US) + fatalx(EXIT_FAILURE, "Invalid serial_send_pace_us value '%s': expected 0 to %d", value, MAX_SERIAL_SEND_PACE_US); serial_send_pace_us = (unsigned int)number; - } + } /* end if */ } static void close_serial_port(void) { /* Use NUT's portable descriptor checks and always invalidate after closing. */ if (VALID_FD_SER(serial_fd)) { - if (ser_close(serial_fd, porta) != 0) { - upsdebug_with_errno(1, "%s: Error closing serial port %s", - __func__, porta); - } + if (ser_close(serial_fd, porta) != 0) + upsdebug_with_errno(1, "%s: Error closing serial port %s", __func__, porta); serial_fd = ERROR_FD_SER; - } + } /* end if */ } static TYPE_FD_SER openfd(const char *portarg, int requested_baudrate) @@ -704,21 +669,17 @@ static TYPE_FD_SER openfd(const char *portarg, int requested_baudrate) size_t i; /* Translate the numeric ups.conf value to the matching termios constant. */ - for (i = 0; i < NUM_BAUD_RATES; i++) { + for (i = 0; i < NUM_BAUD_RATES && rate == 0; i++) { if (baud_rates[i].speed == requested_baudrate) { rate = baud_rates[i].rate; - upsdebugx(1, "%s: Selected baud rate %d -- %s", - __func__, baud_rates[i].speed, - baud_rates[i].description); - break; - } - } + upsdebugx(1, "%s: Selected baud rate %d -- %s", __func__, baud_rates[i].speed, baud_rates[i].description); + } /* end if */ + } /* end for */ if (rate == 0) { - upslogx(LOG_ERR, "%s: Unsupported baud rate %d", - __func__, requested_baudrate); + upslogx(LOG_ERR, "%s: Unsupported baud rate %d", __func__, requested_baudrate); return ERROR_FD_SER; - } + } /* end if */ /* * Establish NUT's standard raw serial baseline first, then customize only @@ -728,32 +689,36 @@ static TYPE_FD_SER openfd(const char *portarg, int requested_baudrate) if (INVALID_FD_SER(fd)) { upsdebug_with_errno(1, "%s: Error opening %s", __func__, portarg); return ERROR_FD_SER; - } + } /* end if */ if (ser_set_speed_nf(fd, portarg, rate) != 0) { - upsdebug_with_errno(1, "%s: Error setting baud rate on %s", - __func__, portarg); + upsdebug_with_errno(1, "%s: Error setting baud rate on %s", __func__, portarg); ser_close(fd, portarg); return ERROR_FD_SER; - } + } /* end if */ if (tcgetattr(fd, &tty) != 0) { - upsdebug_with_errno(1, "%s: Error reading serial settings from %s", - __func__, portarg); + upsdebug_with_errno(1, "%s: Error reading serial settings from %s", __func__, portarg); ser_close(fd, portarg); return ERROR_FD_SER; - } + } /* end if */ /* Replace the baseline character size with the validated selection. */ tty.c_cflag &= ~CSIZE; - if (serial_data_bits == 5) - tty.c_cflag |= CS5; - else if (serial_data_bits == 6) - tty.c_cflag |= CS6; - else if (serial_data_bits == 7) - tty.c_cflag |= CS7; - else - tty.c_cflag |= CS8; + switch (serial_data_bits) { + case 5: + tty.c_cflag |= CS5; + break; + case 6: + tty.c_cflag |= CS6; + break; + case 7: + tty.c_cflag |= CS7; + break; + default: + tty.c_cflag |= CS8; + break; + } /* end switch */ /* * Configure parity as a complete unit. Parity errors are ignored for @@ -761,19 +726,20 @@ static TYPE_FD_SER openfd(const char *portarg, int requested_baudrate) */ tty.c_cflag &= ~(PARENB | PARODD); tty.c_iflag &= ~INPCK; - if (strcasecmp(serial_parity, "none") == 0) { + if (strcasecmp(serial_parity, "none") == 0) tty.c_iflag |= IGNPAR; - } - else if (strcasecmp(serial_parity, "even") == 0) { - tty.c_iflag &= ~IGNPAR; - tty.c_cflag |= PARENB; - tty.c_iflag |= INPCK; - } else { - tty.c_iflag &= ~IGNPAR; - tty.c_cflag |= PARENB | PARODD; - tty.c_iflag |= INPCK; - } + if (strcasecmp(serial_parity, "even") == 0) { + tty.c_iflag &= ~IGNPAR; + tty.c_cflag |= PARENB; + tty.c_iflag |= INPCK; + } /* end if */ + else { + tty.c_iflag &= ~IGNPAR; + tty.c_cflag |= PARENB | PARODD; + tty.c_iflag |= INPCK; + } /* end else */ + } /* end else */ /* CSTOPB clear means one stop bit; set means two stop bits. */ if (serial_stop_bits == 2) @@ -791,40 +757,37 @@ static TYPE_FD_SER openfd(const char *portarg, int requested_baudrate) if (strcasecmp(serial_flow_control, "none") == 0) { /* Hardware and software flow control remain disabled. */ - } - else if (strcasecmp(serial_flow_control, "hardware") == 0) { - tty.c_cflag |= CRTSCTS; - } - else if (strcasecmp(serial_flow_control, "software") == 0) { - tty.c_iflag |= IXON | IXOFF; - } - else if (strcasecmp(serial_flow_control, "both") == 0) { - tty.c_cflag |= CRTSCTS; - tty.c_iflag |= IXON | IXOFF; - } + } /* end if */ + else { + if (strcasecmp(serial_flow_control, "hardware") == 0) + tty.c_cflag |= CRTSCTS; + else { + if (strcasecmp(serial_flow_control, "software") == 0) + tty.c_iflag |= IXON | IXOFF; + else { + if (strcasecmp(serial_flow_control, "both") == 0) { + tty.c_cflag |= CRTSCTS; + tty.c_iflag |= IXON | IXOFF; + } /* end if */ + } /* end else */ + } /* end else */ + } /* end else */ /* Apply the four conventional settings together. */ if (tcsetattr(fd, TCSANOW, &tty) != 0) { - upsdebug_with_errno(1, "%s: Error applying serial settings to %s", - __func__, portarg); + upsdebug_with_errno(1, "%s: Error applying serial settings to %s", __func__, portarg); ser_close(fd, portarg); return ERROR_FD_SER; - } + } /* end if */ /* Discard bytes queued under any previous port configuration. */ if (ser_flush_io(fd) != 0) { - upsdebug_with_errno(1, "%s: Error flushing serial port %s", - __func__, portarg); + upsdebug_with_errno(1, "%s: Error flushing serial port %s", __func__, portarg); ser_close(fd, portarg); return ERROR_FD_SER; - } + } /* end if */ - upsdebugx(1, "%s: Serial settings: %d baud, %u data bits, %s parity, " - "%u stop bit(s), %s flow control, read timeout %u ms, " - "send pace %u us", - __func__, requested_baudrate, serial_data_bits, serial_parity, - serial_stop_bits, serial_flow_control, - serial_read_timeout_ms, serial_send_pace_us); + upsdebugx(1, "%s: Serial settings: %d baud, %u data bits, %s parity, %u stop bit(s), %s flow control, read timeout %u ms, send pace %u us", __func__, requested_baudrate, serial_data_bits, serial_parity, serial_stop_bits, serial_flow_control, serial_read_timeout_ms, serial_send_pace_us); return fd; } @@ -832,9 +795,8 @@ static TYPE_FD_SER openfd(const char *portarg, int requested_baudrate) static unsigned char calculate_checksum(unsigned char *pacote, int inicio, int fim) { int soma = 0, i = 0; - for (i = inicio; i <= fim; i++) { + for (i = inicio; i <= fim; i++) soma += pacote[i]; - } return (soma & 0xFF); } @@ -848,10 +810,9 @@ static void pdatapacket(unsigned char *datapkt, size_t size) { /* FIXME: convert to upsdebug_hex()? */ upsdebugx(1, "%s: logging received data packet bytes at debug verbosity 5 or more", __func__); - for (i = 0; i < size; i++) { + for (i = 0; i < size; i++) upsdebugx(5, "\tPosition %" PRIuSIZE " -- 0x%02X -- Decimal %d -- Char %c", i, datapkt[i], datapkt[i], datapkt[i]); - } - } + } /* end if */ } static float createfloat(int integer, int decimal) { @@ -862,12 +823,10 @@ static float createfloat(int integer, int decimal) { static unsigned int get_vbat(void) { char *v = getval("vbat"); - if (v) { + if (v) return atoi(v); - } - else { + else return DEFAULTBATV; - } } static pkt_data mount_datapacket(unsigned char *datapkt, size_t size, double tempodecorrido, pkt_hwinfo pkt_upsinfo) { @@ -999,7 +958,7 @@ static pkt_data mount_datapacket(unsigned char *datapkt, size_t size, double tem if (debug_pkt_data) { pdatapacket(datapkt, size); print_pkt_data(pktdata); - } + } /* end if */ return pktdata; } @@ -1114,7 +1073,7 @@ static pkt_hwinfo mount_hwinfo(unsigned char *datapkt, size_t size) { else { pkthwinfo.checksum = datapkt[16]; checksum = calculate_checksum(datapkt, 1, 15); - } + } /* end else */ pkthwinfo.checksum_calc = checksum; if (pkthwinfo.checksum == checksum) pkthwinfo.checksum_ok = true; @@ -1122,21 +1081,23 @@ static pkt_hwinfo mount_hwinfo(unsigned char *datapkt, size_t size) { if (debug_pkt_hwinfo) { pdatapacket(datapkt, size); print_pkt_hwinfo(pkthwinfo); - } + } /* end if */ return pkthwinfo; } #if 0 static int write_serial(int fd, const char *dados, size_t size) { + ssize_t bytes_written; + if (fd > 0) { - ssize_t bytes_written = write(fd, dados, size); + bytes_written = write(fd, dados, size); if (bytes_written < 0) return -1; if (tcdrain(fd) != 0) return -2; return size; - } + } /* end if */ else return fd; } @@ -1164,8 +1125,7 @@ static int write_serial_int(TYPE_FD_SER fd, const unsigned int *data, size_t siz if (serial_send_pace_us == 0) sent = ser_send_buf(fd, message, size); else - sent = ser_send_buf_pace(fd, - (useconds_t)serial_send_pace_us, message, size); + sent = ser_send_buf_pace(fd, (useconds_t)serial_send_pace_us, message, size); free(message); @@ -1185,663 +1145,150 @@ static char * strtolow(char* s) { } #endif -static upsinfo getupsinfo(unsigned int upscode) { - upsinfo data; - switch (upscode) { - case 1: - data.upscode = 1; - strncpy(data.upsdesc, "NHS COMPACT PLUS", sizeof(data.upsdesc)); - data.VA = 1000; - break; - - case 2: - data.upscode = 2; - strncpy(data.upsdesc, "NHS COMPACT PLUS SENOIDAL", sizeof(data.upsdesc)); - data.VA = 1000; - break; - - case 3: - data.upscode = 3; - strncpy(data.upsdesc, "NHS COMPACT PLUS RACK", sizeof(data.upsdesc)); - data.VA = 1000; - break; - - case 4: - data.upscode = 4; - strncpy(data.upsdesc, "NHS PREMIUM PDV", sizeof(data.upsdesc)); - data.VA = 1500; - break; - - case 5: - data.upscode = 5; - strncpy(data.upsdesc, "NHS PREMIUM PDV SENOIDAL", sizeof(data.upsdesc)); - data.VA = 1500; - break; - - case 6: - data.upscode = 6; - strncpy(data.upsdesc, "NHS PREMIUM 1500VA", sizeof(data.upsdesc)); - data.VA = 1500; - break; - - case 7: - data.upscode = 7; - strncpy(data.upsdesc, "NHS PREMIUM 2200VA", sizeof(data.upsdesc)); - data.VA = 2200; - break; - - case 8: - data.upscode = 8; - strncpy(data.upsdesc, "NHS PREMIUM SENOIDAL", sizeof(data.upsdesc)); - data.VA = 1500; - break; - - case 9: - data.upscode = 9; - strncpy(data.upsdesc, "NHS LASER 2600VA", sizeof(data.upsdesc)); - data.VA = 2600; - break; - - case 10: - data.upscode = 10; - strncpy(data.upsdesc, "NHS LASER 3300VA", sizeof(data.upsdesc)); - data.VA = 3300; - break; - - case 11: - data.upscode = 11; - strncpy(data.upsdesc, "NHS LASER 2600VA ISOLADOR", sizeof(data.upsdesc)); - data.VA = 2600; - break; - - case 12: - data.upscode = 12; - strncpy(data.upsdesc, "NHS LASER SENOIDAL", sizeof(data.upsdesc)); - data.VA = 2600; - break; - - case 13: - data.upscode = 13; - strncpy(data.upsdesc, "NHS LASER ON-LINE", sizeof(data.upsdesc)); - data.VA = 2600; - break; - - case 15: - data.upscode = 15; - strncpy(data.upsdesc, "NHS COMPACT PLUS 2003", sizeof(data.upsdesc)); - data.VA = 1000; - break; - - case 16: - data.upscode = 16; - strncpy(data.upsdesc, "COMPACT PLUS SENOIDAL 2003", sizeof(data.upsdesc)); - data.VA = 1000; - break; - - case 17: - data.upscode = 17; - strncpy(data.upsdesc, "COMPACT PLUS RACK 2003", sizeof(data.upsdesc)); - data.VA = 1000; - break; - - case 18: - data.upscode = 18; - strncpy(data.upsdesc, "PREMIUM PDV 2003", sizeof(data.upsdesc)); - data.VA = 1500; - break; - - case 19: - data.upscode = 19; - strncpy(data.upsdesc, "PREMIUM PDV SENOIDAL 2003", sizeof(data.upsdesc)); - data.VA = 1500; - break; - - case 20: - data.upscode = 20; - strncpy(data.upsdesc, "PREMIUM 1500VA 2003", sizeof(data.upsdesc)); - data.VA = 1500; - break; - - case 21: - data.upscode = 21; - strncpy(data.upsdesc, "PREMIUM 2200VA 2003", sizeof(data.upsdesc)); - data.VA = 2200; - break; - - case 22: - data.upscode = 22; - strncpy(data.upsdesc, "PREMIUM SENOIDAL 2003", sizeof(data.upsdesc)); - data.VA = 1500; - break; - - case 23: - data.upscode = 23; - strncpy(data.upsdesc, "LASER 2600VA 2003", sizeof(data.upsdesc)); - data.VA = 2600; - break; - - case 24: - data.upscode = 24; - strncpy(data.upsdesc, "LASER 3300VA 2003", sizeof(data.upsdesc)); - data.VA = 3300; - break; - - case 25: - data.upscode = 25; - strncpy(data.upsdesc, "LASER 2600VA ISOLADOR 2003", sizeof(data.upsdesc)); - data.VA = 2600; - break; - - case 26: - data.upscode = 26; - strncpy(data.upsdesc, "LASER SENOIDAL 2003", sizeof(data.upsdesc)); - data.VA = 2600; - break; - - case 27: - data.upscode = 27; - strncpy(data.upsdesc, "PDV ONLINE 2003", sizeof(data.upsdesc)); - data.VA = 1500; - break; - - case 28: - data.upscode = 28; - strncpy(data.upsdesc, "LASER ONLINE 2003", sizeof(data.upsdesc)); - data.VA = 3300; - break; - - case 29: - data.upscode = 29; - strncpy(data.upsdesc, "EXPERT ONLINE 2003", sizeof(data.upsdesc)); - data.VA = 5000; - break; - - case 30: - data.upscode = 30; - strncpy(data.upsdesc, "MINI 2", sizeof(data.upsdesc)); - data.VA = 500; - break; - - case 31: - data.upscode = 31; - strncpy(data.upsdesc, "COMPACT PLUS 2", sizeof(data.upsdesc)); - data.VA = 1000; - break; - - case 32: - data.upscode = 32; - strncpy(data.upsdesc, "LASER ON-LINE", sizeof(data.upsdesc)); - data.VA = 2600; - break; - - case 33: - data.upscode = 33; - strncpy(data.upsdesc, "PDV SENOIDAL 1500VA", sizeof(data.upsdesc)); - data.VA = 1500; - break; - - case 34: - data.upscode = 34; - strncpy(data.upsdesc, "PDV SENOIDAL 1000VA", sizeof(data.upsdesc)); - data.VA = 1000; - break; - - case 36: - data.upscode = 36; - strncpy(data.upsdesc, "LASER ONLINE 3750VA", sizeof(data.upsdesc)); - data.VA = 3750; - break; - - case 37: - data.upscode = 37; - strncpy(data.upsdesc, "LASER ONLINE 5000VA", sizeof(data.upsdesc)); - data.VA = 5000; - break; - - case 38: - data.upscode = 38; - strncpy(data.upsdesc, "PREMIUM SENOIDAL 2000VA", sizeof(data.upsdesc)); - data.VA = 2000; - break; - - case 39: - data.upscode = 39; - strncpy(data.upsdesc, "LASER SENOIDAL 3500VA", sizeof(data.upsdesc)); - data.VA = 3500; - break; - - case 40: - data.upscode = 40; - strncpy(data.upsdesc, "PREMIUM PDV 1200VA", sizeof(data.upsdesc)); - data.VA = 1200; - break; - - case 41: - data.upscode = 41; - strncpy(data.upsdesc, "PREMIUM 1500VA", sizeof(data.upsdesc)); - data.VA = 1500; - break; - - case 42: - data.upscode = 42; - strncpy(data.upsdesc, "PREMIUM 2200VA", sizeof(data.upsdesc)); - data.VA = 2200; - break; - - case 43: - data.upscode = 43; - strncpy(data.upsdesc, "LASER 2600VA", sizeof(data.upsdesc)); - data.VA = 2600; - break; - - case 44: - data.upscode = 44; - strncpy(data.upsdesc, "LASER 3300VA", sizeof(data.upsdesc)); - data.VA = 3300; - break; - - case 45: - data.upscode = 45; - strncpy(data.upsdesc, "COMPACT PLUS SENOIDAL 700VA", sizeof(data.upsdesc)); - data.VA = 700; - break; - - case 46: - data.upscode = 46; - strncpy(data.upsdesc, "PREMIUM ONLINE 2000VA", sizeof(data.upsdesc)); - data.VA = 2000; - break; - - case 47: - data.upscode = 47; - strncpy(data.upsdesc, "EXPERT ONLINE 10000VA", sizeof(data.upsdesc)); - data.VA = 10000; - break; - - case 48: - data.upscode = 48; - strncpy(data.upsdesc, "LASER SENOIDAL 4200VA", sizeof(data.upsdesc)); - data.VA = 4200; - break; - - case 49: - data.upscode = 49; - strncpy(data.upsdesc, "NHS COMPACT PLUS EXTENDIDO 1500VA", sizeof(data.upsdesc)); - data.VA = 1500; - break; - - case 50: - data.upscode = 50; - strncpy(data.upsdesc, "LASER ONLINE 6000VA", sizeof(data.upsdesc)); - data.VA = 6000; - break; - - case 51: - data.upscode = 51; - strncpy(data.upsdesc, "LASER EXT 3300VA", sizeof(data.upsdesc)); - data.VA = 3300; - break; - - case 52: - data.upscode = 52; - strncpy(data.upsdesc, "NHS COMPACT PLUS 1200VA", sizeof(data.upsdesc)); - data.VA = 1200; - break; - - case 53: - data.upscode = 53; - strncpy(data.upsdesc, "LASER SENOIDAL 3000VA GII", sizeof(data.upsdesc)); - data.VA = 3000; - break; - - case 54: - data.upscode = 54; - strncpy(data.upsdesc, "LASER SENOIDAL 3500VA GII", sizeof(data.upsdesc)); - data.VA = 3500; - break; - - case 55: - data.upscode = 55; - strncpy(data.upsdesc, "LASER SENOIDAL 4200VA GII", sizeof(data.upsdesc)); - data.VA = 4200; - break; - - case 56: - data.upscode = 56; - strncpy(data.upsdesc, "LASER ONLINE 3000VA", sizeof(data.upsdesc)); - data.VA = 3000; - break; - - case 57: - data.upscode = 57; - strncpy(data.upsdesc, "LASER ONLINE 3750VA", sizeof(data.upsdesc)); - data.VA = 3750; - break; - - case 58: - data.upscode = 58; - strncpy(data.upsdesc, "LASER ONLINE 5000VA", sizeof(data.upsdesc)); - data.VA = 5000; - break; - - case 59: - data.upscode = 59; - strncpy(data.upsdesc, "LASER ONLINE 6000VA", sizeof(data.upsdesc)); - data.VA = 6000; - break; - - case 60: - data.upscode = 60; - strncpy(data.upsdesc, "PREMIUM ONLINE 2000VA", sizeof(data.upsdesc)); - data.VA = 2000; - break; - - case 61: - data.upscode = 61; - strncpy(data.upsdesc, "PREMIUM ONLINE 1500VA", sizeof(data.upsdesc)); - data.VA = 1500; - break; - - case 62: - data.upscode = 62; - strncpy(data.upsdesc, "PREMIUM ONLINE 1200VA", sizeof(data.upsdesc)); - data.VA = 1200; - break; - - case 63: - data.upscode = 63; - strncpy(data.upsdesc, "COMPACT PLUS II MAX 1400VA", sizeof(data.upsdesc)); - data.VA = 1400; - break; - - case 64: - data.upscode = 64; - strncpy(data.upsdesc, "PREMIUM PDV MAX 2200VA", sizeof(data.upsdesc)); - data.VA = 2200; - break; - - case 65: - data.upscode = 65; - strncpy(data.upsdesc, "PREMIUM PDV 3000VA", sizeof(data.upsdesc)); - data.VA = 3000; - break; - - case 66: - data.upscode = 66; - strncpy(data.upsdesc, "PREMIUM SENOIDAL 2200VA GII", sizeof(data.upsdesc)); - data.VA = 2200; - break; - - case 67: - data.upscode = 67; - strncpy(data.upsdesc, "LASER PRIME SENOIDAL 3200VA GII", sizeof(data.upsdesc)); - data.VA = 3200; - break; - - case 68: - data.upscode = 68; - strncpy(data.upsdesc, "PREMIUM RACK ONLINE 3000VA", sizeof(data.upsdesc)); - data.VA = 3000; - break; - - case 69: - data.upscode = 69; - strncpy(data.upsdesc, "PREMIUM ONLINE 3000VA", sizeof(data.upsdesc)); - data.VA = 3000; - break; - - case 70: - data.upscode = 70; - strncpy(data.upsdesc, "LASER ONLINE 4000VA", sizeof(data.upsdesc)); - data.VA = 4000; - break; - - case 71: - data.upscode = 71; - strncpy(data.upsdesc, "LASER ONLINE 7500VA", sizeof(data.upsdesc)); - data.VA = 7500; - break; - - case 72: - data.upscode = 72; - strncpy(data.upsdesc, "LASER ONLINE BIFASICO 5000VA", sizeof(data.upsdesc)); - data.VA = 5000; - break; - - case 73: - data.upscode = 73; - strncpy(data.upsdesc, "LASER ONLINE BIFASICO 6000VA", sizeof(data.upsdesc)); - data.VA = 6000; - break; - - case 74: - data.upscode = 74; - strncpy(data.upsdesc, "LASER ONLINE BIFASICO 7500VA", sizeof(data.upsdesc)); - data.VA = 7500; - break; - - case 75: - data.upscode = 75; - strncpy(data.upsdesc, "NHS MINI ST", sizeof(data.upsdesc)); - data.VA = 500; - break; - - case 76: - data.upscode = 76; - strncpy(data.upsdesc, "NHS MINI 120", sizeof(data.upsdesc)); - data.VA = 120; - break; - - case 77: - data.upscode = 77; - strncpy(data.upsdesc, "NHS MINI BIVOLT", sizeof(data.upsdesc)); - data.VA = 500; - break; - - case 78: - data.upscode = 78; - strncpy(data.upsdesc, "PDV 600", sizeof(data.upsdesc)); - data.VA = 600; - break; - - case 79: - data.upscode = 79; - strncpy(data.upsdesc, "NHS MINI MAX", sizeof(data.upsdesc)); - data.VA = 500; - break; - - case 80: - data.upscode = 80; - strncpy(data.upsdesc, "NHS MINI EXT", sizeof(data.upsdesc)); - data.VA = 500; - break; - - case 81: - data.upscode = 81; - strncpy(data.upsdesc, "NHS AUTONOMY PDV 4T", sizeof(data.upsdesc)); - data.VA = 4000; - break; - - case 82: - data.upscode = 82; - strncpy(data.upsdesc, "NHS AUTONOMY PDV 8T", sizeof(data.upsdesc)); - data.VA = 8000; - break; - - case 83: - data.upscode = 83; - strncpy(data.upsdesc, "NHS COMPACT PLUS RACK 1200VA", sizeof(data.upsdesc)); - data.VA = 1200; - break; - - case 84: - data.upscode = 84; - strncpy(data.upsdesc, "PDV SENOIDAL ISOLADOR 1500VA", sizeof(data.upsdesc)); - data.VA = 1500; - break; - - case 85: - data.upscode = 85; - strncpy(data.upsdesc, "NHS PDV RACK 1500VA", sizeof(data.upsdesc)); - data.VA = 1500; - break; - - case 86: - data.upscode = 86; - strncpy(data.upsdesc, "NHS PDV 1400VA S GII", sizeof(data.upsdesc)); - data.VA = 1400; - break; - - case 87: - data.upscode = 87; - strncpy(data.upsdesc, "PDV SENOIDAL ISOLADOR 1500VA", sizeof(data.upsdesc)); - data.VA = 1500; - break; - - case 88: - data.upscode = 88; - strncpy(data.upsdesc, "LASER PRIME SENOIDAL ISOLADOR 2000VA", sizeof(data.upsdesc)); - data.VA = 2000; - break; - - case 89: - data.upscode = 89; - strncpy(data.upsdesc, "PREMIUM SENOIDAL 2400VA GII", sizeof(data.upsdesc)); - data.VA = 2400; - break; - - case 90: - data.upscode = 90; - strncpy(data.upsdesc, "NHS PDV 1400VA S 8T GII", sizeof(data.upsdesc)); - data.VA = 1400; - break; - - case 91: - data.upscode = 91; - strncpy(data.upsdesc, "PREMIUM ONLINE 2000VA", sizeof(data.upsdesc)); - data.VA = 2000; - break; - - case 92: - data.upscode = 92; - strncpy(data.upsdesc, "LASER PRIME ONLINE 2200VA", sizeof(data.upsdesc)); - data.VA = 2200; - break; - - case 93: - data.upscode = 93; - strncpy(data.upsdesc, "PREMIUM RACK ONLINE 2200VA", sizeof(data.upsdesc)); - data.VA = 2200; - break; - - case 94: - data.upscode = 94; - strncpy(data.upsdesc, "PREMIUM SENOIDAL 2400VA GII", sizeof(data.upsdesc)); - data.VA = 2400; - break; - - case 95: - data.upscode = 95; - strncpy(data.upsdesc, "LASER ONLINE 10000VA", sizeof(data.upsdesc)); - data.VA = 10000; - break; - - case 96: - data.upscode = 96; - strncpy(data.upsdesc, "LASER ONLINE BIFASICO 10000VA", sizeof(data.upsdesc)); - data.VA = 10000; - break; - - case 97: - data.upscode = 97; - strncpy(data.upsdesc, "LASER SENOIDAL 3300VA GII", sizeof(data.upsdesc)); - data.VA = 3300; - break; - - case 98: - data.upscode = 98; - strncpy(data.upsdesc, "LASER SENOIDAL 2600VA GII", sizeof(data.upsdesc)); - data.VA = 2600; - break; - - case 99: - data.upscode = 99; - strncpy(data.upsdesc, "PREMIUM SENOIDAL 3000VA GII", sizeof(data.upsdesc)); - data.VA = 3000; - break; - - case 100: - data.upscode = 100; - strncpy(data.upsdesc, "PREMIUM SENOIDAL 2200VA GII", sizeof(data.upsdesc)); - data.VA = 2200; - break; - - case 101: - data.upscode = 101; - strncpy(data.upsdesc, "LASER ONLINE BIFASICO 4000VA", sizeof(data.upsdesc)); - data.VA = 4000; - break; - - case 102: - data.upscode = 102; - strncpy(data.upsdesc, "LASER ONLINE 12000VA", sizeof(data.upsdesc)); - data.VA = 12000; - break; - - case 103: - data.upscode = 103; - strncpy(data.upsdesc, "LASER ONLINE 8000VA", sizeof(data.upsdesc)); - data.VA = 8000; - break; - - case 104: - data.upscode = 104; - strncpy(data.upsdesc, "PDV SENOIDAL ISOLADOR 1000VA", sizeof(data.upsdesc)); - data.VA = 1000; - break; - - case 105: - data.upscode = 105; - strncpy(data.upsdesc, "MINI SENOIDAL 500VA", sizeof(data.upsdesc)); - data.VA = 500; - break; - - case 106: - data.upscode = 106; - strncpy(data.upsdesc, "LASER SENOIDAL 5000VA GII", sizeof(data.upsdesc)); - data.VA = 5000; - break; - - case 107: - data.upscode = 107; - strncpy(data.upsdesc, "COMPACT PLUS SENOIDAL 1000VA", sizeof(data.upsdesc)); - data.VA = 1000; - break; +/* + * Keep model metadata in an indexed table instead of a long switch to improve + * visualization of protocol codes, descriptions and nominal VA values while + * preserving direct lookup by the model code reported by the UPS. + */ +static const upsinfo ups_info_table[114] = { + [1] = { 1, "NHS COMPACT PLUS", 1000 }, + [2] = { 2, "NHS COMPACT PLUS SENOIDAL", 1000 }, + [3] = { 3, "NHS COMPACT PLUS RACK", 1000 }, + [4] = { 4, "NHS PREMIUM PDV", 1500 }, + [5] = { 5, "NHS PREMIUM PDV SENOIDAL", 1500 }, + [6] = { 6, "NHS PREMIUM 1500VA", 1500 }, + [7] = { 7, "NHS PREMIUM 2200VA", 2200 }, + [8] = { 8, "NHS PREMIUM SENOIDAL", 1500 }, + [9] = { 9, "NHS LASER 2600VA", 2600 }, + [10] = { 10, "NHS LASER 3300VA", 3300 }, + [11] = { 11, "NHS LASER 2600VA ISOLADOR", 2600 }, + [12] = { 12, "NHS LASER SENOIDAL", 2600 }, + [13] = { 13, "NHS LASER ON-LINE", 2600 }, + [15] = { 15, "NHS COMPACT PLUS 2003", 1000 }, + [16] = { 16, "COMPACT PLUS SENOIDAL 2003", 1000 }, + [17] = { 17, "COMPACT PLUS RACK 2003", 1000 }, + [18] = { 18, "PREMIUM PDV 2003", 1500 }, + [19] = { 19, "PREMIUM PDV SENOIDAL 2003", 1500 }, + [20] = { 20, "PREMIUM 1500VA 2003", 1500 }, + [21] = { 21, "PREMIUM 2200VA 2003", 2200 }, + [22] = { 22, "PREMIUM SENOIDAL 2003", 1500 }, + [23] = { 23, "LASER 2600VA 2003", 2600 }, + [24] = { 24, "LASER 3300VA 2003", 3300 }, + [25] = { 25, "LASER 2600VA ISOLADOR 2003", 2600 }, + [26] = { 26, "LASER SENOIDAL 2003", 2600 }, + [27] = { 27, "PDV ONLINE 2003", 1500 }, + [28] = { 28, "LASER ONLINE 2003", 3300 }, + [29] = { 29, "EXPERT ONLINE 2003", 5000 }, + [30] = { 30, "MINI 2", 500 }, + [31] = { 31, "COMPACT PLUS 2", 1000 }, + [32] = { 32, "LASER ON-LINE", 2600 }, + [33] = { 33, "PDV SENOIDAL 1500VA", 1500 }, + [34] = { 34, "PDV SENOIDAL 1000VA", 1000 }, + [36] = { 36, "LASER ONLINE 3750VA", 3750 }, + [37] = { 37, "LASER ONLINE 5000VA", 5000 }, + [38] = { 38, "PREMIUM SENOIDAL 2000VA", 2000 }, + [39] = { 39, "LASER SENOIDAL 3500VA", 3500 }, + [40] = { 40, "PREMIUM PDV 1200VA", 1200 }, + [41] = { 41, "PREMIUM 1500VA", 1500 }, + [42] = { 42, "PREMIUM 2200VA", 2200 }, + [43] = { 43, "LASER 2600VA", 2600 }, + [44] = { 44, "LASER 3300VA", 3300 }, + [45] = { 45, "COMPACT PLUS SENOIDAL 700VA", 700 }, + [46] = { 46, "PREMIUM ONLINE 2000VA", 2000 }, + [47] = { 47, "EXPERT ONLINE 10000VA", 10000 }, + [48] = { 48, "LASER SENOIDAL 4200VA", 4200 }, + [49] = { 49, "NHS COMPACT PLUS EXTENDIDO 1500VA", 1500 }, + [50] = { 50, "LASER ONLINE 6000VA", 6000 }, + [51] = { 51, "LASER EXT 3300VA", 3300 }, + [52] = { 52, "NHS COMPACT PLUS 1200VA", 1200 }, + [53] = { 53, "LASER SENOIDAL 3000VA GII", 3000 }, + [54] = { 54, "LASER SENOIDAL 3500VA GII", 3500 }, + [55] = { 55, "LASER SENOIDAL 4200VA GII", 4200 }, + [56] = { 56, "LASER ONLINE 3000VA", 3000 }, + [57] = { 57, "LASER ONLINE 3750VA", 3750 }, + [58] = { 58, "LASER ONLINE 5000VA", 5000 }, + [59] = { 59, "LASER ONLINE 6000VA", 6000 }, + [60] = { 60, "PREMIUM ONLINE 2000VA", 2000 }, + [61] = { 61, "PREMIUM ONLINE 1500VA", 1500 }, + [62] = { 62, "PREMIUM ONLINE 1200VA", 1200 }, + [63] = { 63, "COMPACT PLUS II MAX 1400VA", 1400 }, + [64] = { 64, "PREMIUM PDV MAX 2200VA", 2200 }, + [65] = { 65, "PREMIUM PDV 3000VA", 3000 }, + [66] = { 66, "PREMIUM SENOIDAL 2200VA GII", 2200 }, + [67] = { 67, "LASER PRIME SENOIDAL 3200VA GII", 3200 }, + [68] = { 68, "PREMIUM RACK ONLINE 3000VA", 3000 }, + [69] = { 69, "PREMIUM ONLINE 3000VA", 3000 }, + [70] = { 70, "LASER ONLINE 4000VA", 4000 }, + [71] = { 71, "LASER ONLINE 7500VA", 7500 }, + [72] = { 72, "LASER ONLINE BIFASICO 5000VA", 5000 }, + [73] = { 73, "LASER ONLINE BIFASICO 6000VA", 6000 }, + [74] = { 74, "LASER ONLINE BIFASICO 7500VA", 7500 }, + [75] = { 75, "NHS MINI ST", 500 }, + [76] = { 76, "NHS MINI 120", 120 }, + [77] = { 77, "NHS MINI BIVOLT", 500 }, + [78] = { 78, "PDV 600", 600 }, + [79] = { 79, "NHS MINI MAX", 500 }, + [80] = { 80, "NHS MINI EXT", 500 }, + [81] = { 81, "NHS AUTONOMY PDV 4T", 4000 }, + [82] = { 82, "NHS AUTONOMY PDV 8T", 8000 }, + [83] = { 83, "NHS COMPACT PLUS RACK 1200VA", 1200 }, + [84] = { 84, "PDV SENOIDAL ISOLADOR 1500VA", 1500 }, + [85] = { 85, "NHS PDV RACK 1500VA", 1500 }, + [86] = { 86, "NHS PDV 1400VA S GII", 1400 }, + [87] = { 87, "PDV SENOIDAL ISOLADOR 1500VA", 1500 }, + [88] = { 88, "LASER PRIME SENOIDAL ISOLADOR 2000VA", 2000 }, + [89] = { 89, "PREMIUM SENOIDAL 2400VA GII", 2400 }, + [90] = { 90, "NHS PDV 1400VA S 8T GII", 1400 }, + [91] = { 91, "PREMIUM ONLINE 2000VA", 2000 }, + [92] = { 92, "LASER PRIME ONLINE 2200VA", 2200 }, + [93] = { 93, "PREMIUM RACK ONLINE 2200VA", 2200 }, + [94] = { 94, "PREMIUM SENOIDAL 2400VA GII", 2400 }, + [95] = { 95, "LASER ONLINE 10000VA", 10000 }, + [96] = { 96, "LASER ONLINE BIFASICO 10000VA", 10000 }, + [97] = { 97, "LASER SENOIDAL 3300VA GII", 3300 }, + [98] = { 98, "LASER SENOIDAL 2600VA GII", 2600 }, + [99] = { 99, "PREMIUM SENOIDAL 3000VA GII", 3000 }, + [100] = { 100, "PREMIUM SENOIDAL 2200VA GII", 2200 }, + [101] = { 101, "LASER ONLINE BIFASICO 4000VA", 4000 }, + [102] = { 102, "LASER ONLINE 12000VA", 12000 }, + [103] = { 103, "LASER ONLINE 8000VA", 8000 }, + [104] = { 104, "PDV SENOIDAL ISOLADOR 1000VA", 1000 }, + [105] = { 105, "MINI SENOIDAL 500VA", 500 }, + [106] = { 106, "LASER SENOIDAL 5000VA GII", 5000 }, + [107] = { 107, "COMPACT PLUS SENOIDAL 1000VA", 1000 }, + [108] = { 108, "QUAD_COM 80A", 0 }, + [109] = { 109, "LASER ONLINE 5000VA", 5000 }, + [113] = { 113, "PDV SENOIDAL ISOLADOR 700VA", 700 }, +}; - case 108: - data.upscode = 108; - strncpy(data.upsdesc, "QUAD_COM 80A", sizeof(data.upsdesc)); - data.VA = 0; - break; +static upsinfo getupsinfo(unsigned int upscode) { + upsinfo data = { (unsigned int)-1, "NHS UNKNOWN", 0, 3, 1 }; + char *overridemodel = getval("overridemodel"); + char *protocolversion = getval("protocolversion"); + char *bypassasalarm = getval("bypassasalarm"); - case 109: - data.upscode = 109; - strncpy(data.upsdesc, "LASER ONLINE 5000VA", sizeof(data.upsdesc)); - data.VA = 5000; - break; + if (upscode > 0 && upscode < sizeof(ups_info_table) / sizeof(ups_info_table[0]) && ups_info_table[upscode].upscode == upscode) { + data = ups_info_table[upscode]; + data.pversion = 3; + data.bypassasalarm = 1; + } /* end if */ - case 113: - data.upscode = 113; - strncpy(data.upsdesc, "PDV SENOIDAL ISOLADOR 700VA", sizeof(data.upsdesc)); - data.VA = 700; - break; + /* + * Protocol version 3 and bypass-as-alarm are the defaults for the current + * model table. Values supplied in ups.conf customize only the returned copy, + * leaving the static table unchanged. This also completes the synthetic entry + * used for an unknown model code. Protocol version is retained as model + * metadata for now and does not select framing or initialization commands. + */ + if (overridemodel && overridemodel[0] != '\0') { + data.upscode = upscode; + snprintf(data.upsdesc, sizeof(data.upsdesc), "%s", overridemodel); + } /* end if */ + if (protocolversion) + data.pversion = (unsigned int)atoi(protocolversion); + if (bypassasalarm) + data.bypassasalarm = (unsigned int)atoi(bypassasalarm); - default: - data.upscode = -1; - strncpy(data.upsdesc, "NHS UNKNOWN", sizeof(data.upsdesc)); - data.VA = 0; - break; - } return data; } @@ -1857,7 +1304,7 @@ static unsigned int get_va(int equipment) { return atoi(va); else fatalx(EXIT_FAILURE, "Please set VA (Volt Ampere) nominal capacity value to your equipment in ups.conf."); - } + } /* end else */ } static float get_pf(void) { @@ -1925,11 +1372,11 @@ static unsigned int get_numbat(void) { static TYPE_FD_SER reconnect_ups_if_needed(void) { /* retries to open port until we declare "data stale" loudly */ static unsigned int retries = 0; + bool retry_limit_reached = false; /* If comms failed earlier, try to resuscitate */ if (INVALID_FD_SER(serial_fd)) { - upsdebugx(1, "%s: Serial port '%s' communications problem", - __func__, porta); + upsdebugx(1, "%s: Serial port '%s' communications problem", __func__, porta); /* Uh oh, got to reconnect! */ reconnect_trying(RECONNECT_TRYING); @@ -1937,31 +1384,30 @@ static TYPE_FD_SER reconnect_ups_if_needed(void) { /* Close any surviving handle and mark it invalid before reopening. */ close_serial_port(); - while (INVALID_FD_SER(serial_fd)) { + while (INVALID_FD_SER(serial_fd) && !retry_limit_reached) { upsdebugx(1, "%s: Trying to reopen serial...", __func__); serial_fd = openfd(porta, baudrate); retries++; - /* Try above at least once per main cycle */ if (retries >= MAXTRIES) { upsdebugx(1, "%s: serial port reopen failed", __func__); - break; - } - usleep(checktime); - } + retry_limit_reached = true; + } /* end if */ + else + usleep(checktime); + } /* end while */ if (VALID_FD_SER(serial_fd)) { - if (retries > MAXTRIES && may_log_reconnect_trying(1)) { + if (retries > MAXTRIES && may_log_reconnect_trying(1)) upslogx(LOG_NOTICE, "Communications with UPS re-established"); - } retries = 0; reconnect_trying(RECONNECT_SUCCESS); - } else { - if (retries == MAXTRIES && may_log_reconnect_trying(1)) { + } /* end if */ + else { + if (retries == MAXTRIES && may_log_reconnect_trying(1)) upslogx(LOG_WARNING, "Communications with UPS lost: port reopen failed!"); - } dstate_datastale(); - } - } + } /* end else */ + } /* end if */ return serial_fd; } @@ -1970,33 +1416,27 @@ static void interpret_pkt_hwinfo(void) { /* TOTHINK: Consider passing in the packet struct as parameter? */ upsinfo ups; char hw_scratch_buf[1024]; /* General-purpose string buffer */ + unsigned int i = 0; if (!lastpktdata.checksum_ok) { - upslogx(LOG_WARNING, "%s: bad lastpkthwinfo.checksum", - __func__); + upslogx(LOG_WARNING, "%s: bad lastpkthwinfo.checksum", __func__); return; - } + } /* end if */ if (lastpkthwinfo.size < 1) { - upslogx(LOG_WARNING, "%s: Pkt HWINFO is not OK. " - "See if will be requested next time!", - __func__); + upslogx(LOG_WARNING, "%s: Pkt HWINFO is not OK. See if will be requested next time!", __func__); return; - } + } /* end if */ /* checksum is OK, then use it to set values */ - upsdebugx(4, "Pkt HWINFO is OK. Model code is %u, hwversion is %u " - "and swversion is %u", - lastpkthwinfo.model, - lastpkthwinfo.hardwareversion, - lastpkthwinfo.softwareversion); + upsdebugx(4, "Pkt HWINFO is OK. Model code is %u, hwversion is %u and swversion is %u", lastpkthwinfo.model, lastpkthwinfo.hardwareversion, lastpkthwinfo.softwareversion); /* We need to set data on NUT with data * that I believe that I can calculate. * Now setting data on NUT */ ups = getupsinfo(lastpkthwinfo.model); - upsdebugx(4, "UPS Struct data: Code %u Model %s VA %u", ups.upscode, ups.upsdesc, ups.VA); + upsdebugx(4, "UPS Struct data: Code %u Model %s VA %u Protocol version %u Bypass as alarm %u", ups.upscode, ups.upsdesc, ups.VA, ups.pversion, ups.bypassasalarm); dstate_setinfo("device.model", "%s", ups.upsdesc); dstate_setinfo("device.mfr", "%s", MANUFACTURER); dstate_setinfo("device.serial", "%s", lastpkthwinfo.serial); @@ -2015,8 +1455,6 @@ static void interpret_pkt_hwinfo(void) { dstate_setinfo("ups.firmware.aux", "%u", lastpkthwinfo.hardwareversion); if (debug_pkt_hwinfo) { - unsigned int i = 0; - /* Now, creating a structure called NHS.HW, for latest HW * info packet contents and raw data points, including those * that were sorted above into NUT standard variables - @@ -2033,7 +1471,7 @@ static void interpret_pkt_hwinfo(void) { /* Reusing variable */ snprintf(hw_scratch_buf, sizeof(hw_scratch_buf), "experimental.nhs.hw.configuration_array_p%u", i); dstate_setinfo(hw_scratch_buf, "%u", lastpkthwinfo.configuration_array[i]); - } + } /* end for */ dstate_setinfo("experimental.nhs.hw.c_oem_mode", "%s", lastpkthwinfo.c_oem_mode ? "true" : "false"); dstate_setinfo("experimental.nhs.hw.c_buzzer_disable", "%s", lastpkthwinfo.c_buzzer_disable ? "true" : "false"); dstate_setinfo("experimental.nhs.hw.c_potmin_disable", "%s", lastpkthwinfo.c_potmin_disable ? "true" : "false"); @@ -2051,7 +1489,7 @@ static void interpret_pkt_hwinfo(void) { /* Reusing variable */ snprintf(hw_scratch_buf, sizeof(hw_scratch_buf), "experimental.nhs.hw.status_p%u", i); dstate_setinfo(hw_scratch_buf, "%u", lastpkthwinfo.status[i]); - } + } /* end for */ dstate_setinfo("experimental.nhs.hw.s_220V_in", "%s", lastpkthwinfo.s_220V_in ? "true" : "false"); dstate_setinfo("experimental.nhs.hw.s_220V_out", "%s", lastpkthwinfo.s_220V_out ? "true" : "false"); dstate_setinfo("experimental.nhs.hw.s_sealed_battery", "%s", lastpkthwinfo.s_sealed_battery ? "true" : "false"); @@ -2077,7 +1515,7 @@ static void interpret_pkt_hwinfo(void) { dstate_setinfo("experimental.nhs.hw.alarmminute", "%u", lastpkthwinfo.alarmminute); dstate_setinfo("experimental.nhs.hw.alarmsecond", "%u", lastpkthwinfo.alarmsecond); dstate_setinfo("experimental.nhs.hw.end_marker", "%u", lastpkthwinfo.end_marker); - } + } /* end if */ } static void interpret_pkt_data(void) { @@ -2108,22 +1546,20 @@ static void interpret_pkt_data(void) { long bcharge = 0; float abat = 0; float actual_current = 0; + unsigned int i = 0; + upsinfo ups; if (!lastpktdata.checksum_ok) { upslogx(LOG_WARNING, "%s: bad lastpktdata.checksum", __func__); return; - } + } /* end if */ /* checksum is OK, then use it to set values */ upsdebugx(4, "%s: Data Packet seems be OK", __func__); - if (!got_hwinfo) { - upsdebugx(2, "%s: Pkt HWINFO is not OK. " - "See if will be requested next time. " - "Some data points will not be set on this pass!", - __func__); - /* Not return, but we would miss some data points */ - } + /* Not return, but we would miss some data points */ + if (!got_hwinfo) + upsdebugx(2, "%s: Pkt HWINFO is not OK. See if will be requested next time. Some data points will not be set on this pass!", __func__); /* Setting UPS Status: * OL -- On line (mains is present): Code below @@ -2145,24 +1581,24 @@ static void interpret_pkt_data(void) { /* Decision Chain commented below */ /* First we check if system is on battery or not */ - upsdebugx(4, "Set UPS status as OFF and start checking. s_battery_mode is %d", - lastpktdata.s_battery_mode); + upsdebugx(4, "Set UPS status as OFF and start checking. s_battery_mode is %d", lastpktdata.s_battery_mode); if (got_hwinfo) { if (lastpkthwinfo.s_220V_in) { upsdebugx(4, "I'm on 220v IN!. My undervoltage is %u", lastpkthwinfo.undervoltagein220V); min_input_power = lastpkthwinfo.undervoltagein220V; - } + } /* end if */ else { upsdebugx(4, "I'm on 120v IN!. My undervoltage is %u", lastpkthwinfo.undervoltagein120V); min_input_power = lastpkthwinfo.undervoltagein120V; - } - } else { + } /* end else */ + } /* end if */ + else { if (!min_input_power) { min_input_power = 96; upsdebugx(4, "I'm on unknown input!. My undervoltage is default %d", min_input_power); - } - } + } /* end if */ + } /* end else */ /* No ups.status changes above this line */ status_init(); @@ -2181,12 +1617,7 @@ static void interpret_pkt_data(void) { /* Check if MAINS (power) is not preset. * Well, we can check pkt_data.s_network_failure too... */ if ((lastpktdata.vacinrms <= min_input_power) || (lastpktdata.s_network_failure)) { - upsdebugx(4, "UPS has power-in value %0.2f " - "and min_input_power is %d, " - "or network is in failure. Network failure is %d", - lastpktdata.vacinrms, - min_input_power, - lastpktdata.s_network_failure); + upsdebugx(4, "UPS has power-in value %0.2f and min_input_power is %d, or network is in failure. Network failure is %d", lastpktdata.vacinrms, min_input_power, lastpktdata.s_network_failure); status_set("DISCHRG"); } /* end if */ else { @@ -2200,7 +1631,7 @@ static void interpret_pkt_data(void) { if (lastpktdata.s_charger_on) { upsdebugx(4, "UPS Charging..."); status_set("CHRG"); - } + } /* end if */ else { if ((lastpktdata.s_network_failure) || (lastpktdata.s_fast_network_failure)) { upsdebugx(4, "UPS is on battery mode because network failure or fast network failure"); @@ -2217,10 +1648,9 @@ static void interpret_pkt_data(void) { * Nobreak is probably in battery mode... */ if (lastpktdata.s_battery_low) status_set("LB"); - else { + else /* ...or network failure */ status_set("OB"); - } /* end else */ } /* end else */ } /* end else */ } /* end else */ @@ -2245,14 +1675,20 @@ static void interpret_pkt_data(void) { numbat = lastpkthwinfo.numbatteries; else upsdebugx(4, "Number of batteries is set to %u", numbat); - } + } /* end if */ /* No ups.alarm changes above this line */ alarm_init(); + ups = getupsinfo(lastpkthwinfo.model); if (lastpktdata.s_battery_low) alarm_set("[LOW BATTERY]"); - if (lastpktdata.s_bypass_on) + /* + * Some line-interactive models keep the decoded bypass bit set during normal + * on-line operation. A zero bypassasalarm setting preserves the decoded bit + * for diagnostics without publishing a permanent [ON BYPASS] alarm. + */ + if (lastpktdata.s_bypass_on && ups.bypassasalarm == 1) alarm_set("[ON BYPASS]"); if (lastpktdata.s_network_failure) @@ -2303,12 +1739,13 @@ static void interpret_pkt_data(void) { minpowerperc = lastpktdata.potrms; dstate_setinfo("output.power.maximum.percent", "%u", maxpowerperc); dstate_setinfo("output.power.minimum.percent", "%u", minpowerperc); - } + } /* end if */ dstate_setinfo("output.voltage", "%0.2f", lastpktdata.vacoutrms); dstate_setinfo("input.voltage", "%0.2f", lastpktdata.vacinrms); - dstate_setinfo("input.voltage.maximum", "%0.2f", lastpktdata.vacinrmsmin); - dstate_setinfo("input.voltage.minimum", "%0.2f", lastpktdata.vacinrmsmax); + /* Map the packet's observed minimum and maximum to their matching NUT names. */ + dstate_setinfo("input.voltage.maximum", "%0.2f", lastpktdata.vacinrmsmax); + dstate_setinfo("input.voltage.minimum", "%0.2f", lastpktdata.vacinrmsmin); if (got_hwinfo) { dstate_setinfo("ups.beeper.status", "%d", !lastpkthwinfo.c_buzzer_disable); @@ -2334,7 +1771,7 @@ static void interpret_pkt_data(void) { dstate_setinfo("input.voltage.nominal", "%u", vin); vout = lastpkthwinfo.s_220V_out ? lastpkthwinfo.tensionout220V : lastpkthwinfo.tensionout120V; dstate_setinfo("output.voltage.nominal", "%u", vout); - } + } /* end if */ /* Battery electric info */ bcharge = lrint(round((lastpktdata.vdcmed_real * 100) / vbat)); @@ -2370,21 +1807,19 @@ static void interpret_pkt_data(void) { autonomy_secs = (ah / actual_current) * 3600; dstate_setinfo("battery.runtime", "%u", autonomy_secs); - } + } /* end if */ /* Battery charger status */ - if (lastpktdata.s_charger_on) { + if (lastpktdata.s_charger_on) dstate_setinfo("battery.charger.status", "%s", "CHARGING"); - } else { + else { if (lastpktdata.s_battery_mode) dstate_setinfo("battery.charger.status", "%s", "DISCHARGING"); else dstate_setinfo("battery.charger.status", "%s", "RESTING"); - } + } /* end else */ if (debug_pkt_data) { - unsigned int i = 0; - /* Now, creating a structure called NHS.DATA, for latest * data packet contents and raw data points, including those * that were sorted above into NUT standard variables - @@ -2423,7 +1858,7 @@ static void interpret_pkt_data(void) { /* Reusing variable */ snprintf(data_scratch_buf, sizeof(data_scratch_buf), "experimental.nhs.data.status_p%u", i); dstate_setinfo(data_scratch_buf, "%u", lastpktdata.status[i]); - } + } /* end for */ dstate_setinfo("experimental.nhs.data.nominaltension", "%u", lastpktdata.nominaltension); dstate_setinfo("experimental.nhs.data.timeremain", "%0.2f", lastpktdata.timeremain); dstate_setinfo("experimental.nhs.data.s_battery_mode", "%s", lastpktdata.s_battery_mode ? "true" : "false"); @@ -2445,12 +1880,15 @@ static void interpret_pkt_data(void) { dstate_setinfo("experimental.nhs.param.vin_low_crit_perc", "%0.2f", get_vin_perc("vin_low_crit_perc")); dstate_setinfo("experimental.nhs.param.vin_high_warn_perc", "%0.2f", get_vin_perc("vin_high_warn_perc")); dstate_setinfo("experimental.nhs.param.vin_high_crit_perc", "%0.2f", get_vin_perc("vin_high_crit_perc")); - } + } /* end if */ } void upsdrv_updateinfo(void) { double tempodecorrido = 0.0; - time_t now; + time_t now, timeout_sec; + useconds_t timeout_usec; + ssize_t read_result; + int randval = 0; upsdebugx(3, "%s: starting...", __func__); @@ -2459,96 +1897,101 @@ void upsdrv_updateinfo(void) { return; chr = '\0'; - { - ssize_t read_result; - /* ser_get_char accepts separate seconds and microseconds components. */ - time_t timeout_sec = (time_t)(serial_read_timeout_ms / 1000); - useconds_t timeout_usec = - (useconds_t)((serial_read_timeout_ms % 1000) * 1000); + /* ser_get_char accepts separate seconds and microseconds components. */ + timeout_sec = (time_t)(serial_read_timeout_ms / 1000); + timeout_usec = (useconds_t)((serial_read_timeout_ms % 1000) * 1000); - /* - * A positive result supplies one byte, zero is the configured normal - * timeout, and a negative result is handled as a communication error. + /* + * A positive result supplies one byte, zero is the configured normal + * timeout, and a negative result is handled as a communication error. + */ + read_result = ser_get_char(serial_fd, &chr, timeout_sec, timeout_usec); + while (read_result > 0) { + /* A 0xFF byte starts a packet only while the reader is idle. After + * the packet starts, byte [1] declares its total size, including the + * initial 0xFF, checksum and final 0xFE. Any 0xFF or 0xFE received + * before the declared final position belongs to the packet contents. + * The packet is interpreted only when byte [size - 1] is 0xFE. */ - while ((read_result = ser_get_char( - serial_fd, - &chr, - timeout_sec, - timeout_usec - )) > 0) { - if (chr == 0xFF) { /* DataPacket start */ - datapacketstart = true; - memset(datapacket, 0, sizeof(datapacket)); - datapacket_index = 0; - } - if (datapacketstart) { - datapacket[datapacket_index] = chr; - datapacket_index++; - if (chr == 0xFE) { /* DataPacket */ - break; - } - if (datapacket_index >= sizeof(datapacket)) { - upslogx(LOG_WARNING, "Incoming packet does not seem to end, discarding!"); - datapacketstart = false; - break; - } - } - } + if ((chr == 0xFF) && (!datapacketstart)) { + datapacketstart = true; + memset(datapacket, 0, sizeof(datapacket)); + datapacket_index = 0; + datapacketsize = 0; + } /* end if */ + if (datapacketstart) { + /* Assume that second position is PACKET SIZE. */ + if (datapacket_index == 1) + datapacketsize = chr; + datapacket[datapacket_index] = chr; + if ((datapacket_index == 1) && ((datapacketsize < 18) || (datapacketsize > sizeof(datapacket)))) { + upslogx(LOG_WARNING, "Incoming packet declares an invalid size, discarding!"); + datapacketstart = false; + } /* end if */ + else { + if ((datapacketsize > 0) && (datapacket_index == (datapacketsize - 1))) { + if (datapacket[datapacket_index] != 0xFE) + upslogx(LOG_WARNING, "Incoming packet does not end with 0xFE, discarding!"); + else { + now = time(NULL); + upsdebugx(4, "DATAPACKET SIZE IS %u", datapacketsize); + + if (lastdp != 0) + tempodecorrido = difftime(now, lastdp); + + lastdp = now; + + switch (datapacketsize) { + case 18: + case 50: + if (!lastpkthwinfo.checksum_ok) { + lastpkthwinfo = mount_hwinfo(datapacket, datapacketsize); + + if (lastpkthwinfo.checksum_ok) { + interpret_pkt_hwinfo(); + dstate_dataok(); + } /* end if */ + } /* end if */ + break; + + case 21: + lastpktdata = mount_datapacket(datapacket, datapacketsize, tempodecorrido, lastpkthwinfo); + + if (lastpktdata.checksum_ok) { + interpret_pkt_data(); + dstate_dataok(); + } /* end if */ + break; + + default: + upslogx(LOG_WARNING, "Incoming packet size not recognized, discarding!"); + break; + } /* end switch */ + } /* end else */ - if (read_result < 0) { - upsdebug_with_errno(1, "%s: Serial read failed on %s", - __func__, porta); - close_serial_port(); - dstate_datastale(); - return; - } - } + datapacketstart = false; + } /* end if */ + } /* end else */ + } /* end if */ + /* Advance only after consuming the current byte. Keeping both the + * index and declared size outside this function lets a packet resume + * at the correct position after a normal serial-read timeout. + */ + datapacket_index++; + read_result = ser_get_char(serial_fd, &chr, timeout_sec, timeout_usec); + } /* end while */ - if (chr != 0xFE || !datapacketstart) { + if (read_result < 0) { + upsdebug_with_errno(1, "%s: Serial read failed on %s", __func__, porta); + close_serial_port(); + dstate_datastale(); + return; + } /* end if */ + if (datapacketstart) { upsdebugx(2, "%s: packet reading did not finish, not interpreting yet", __func__); return; - } - - /* Interpret the just-finished packet buffer */ - now = time(NULL); - upsdebugx(4, "DATAPACKET INDEX IS %" PRIuSIZE, datapacket_index); - if (lastdp != 0) { - tempodecorrido = difftime(now, lastdp); - } - lastdp = now; - - /* Parse the bytes into a structure to handle below: - * If size is 18 or 50, may be an answer packet. - * Then check if doesn't have already a packet processed. - * We don't need to read all times these information. - * Can be a corrupted packet too. - */ - if (((datapacket_index == 18) || (datapacket_index == 50)) && (!lastpkthwinfo.checksum_ok)) { - /* Re-read HW info only if the old one is broken */ - lastpkthwinfo = mount_hwinfo(datapacket, datapacket_index); - if (lastpkthwinfo.checksum_ok) { - interpret_pkt_hwinfo(); - /* Refresh the healthy timer */ - dstate_dataok(); - } } /* end if */ - else if (datapacket_index == 21) { - lastpktdata = mount_datapacket(datapacket, datapacket_index, tempodecorrido, lastpkthwinfo); - if (lastpktdata.checksum_ok) { - interpret_pkt_data(); - /* Refresh the healthy timer */ - dstate_dataok(); - } - } /* end else-if */ - else { - upslogx(LOG_WARNING, "Incoming packet size not recognized, discarding!"); - } /* end else */ - - /* Clean datapacket structure to avoid problems for next parse */ - datapacket_index = 0; - memset(datapacket, 0, sizeof(datapacket)); - datapacketstart = false; /* Now the nobreak read buffer is empty. * We need a hw info packet to discover several variables, @@ -2559,8 +2002,8 @@ void upsdrv_updateinfo(void) { upsdebugx(4, "pkt_hwinfo loss -- Requesting"); /* If size == 0, packet maybe not initizated, * then send an initialization packet to obtain data. - * Send two times the extended initialization string, - * but, on fail, try randomly send extended or normal. + * Send six times the extended initialization string, + * but, on fail, try randomly send extended, normal or compatibility. */ if (send_extended < 6) { upsdebugx(4, "Sending extended initialization packet. Try %u", send_extended+1); @@ -2569,20 +2012,26 @@ void upsdrv_updateinfo(void) { } /* end if */ else { /* randomly send */ - if (rand() % 2 == 0) { - upsdebugx(4, "Sending long initialization packet"); - bwritten = write_serial_int(serial_fd, string_initialization_long, 9); - } /* end if */ - else { - upsdebugx(4, "Sending short initialization packet"); - bwritten = write_serial_int(serial_fd, string_initialization_short, 9); - } /* end else */ + randval = rand() % 3; + switch (randval) { + case 0: + upsdebugx(4, "Sending long initialization packet (random)"); + bwritten = write_serial_int(serial_fd, string_initialization_long, 9); + break; + case 1: + upsdebugx(4, "Sending short initialization packet (random)"); + bwritten = write_serial_int(serial_fd, string_initialization_short, 9); + break; + case 2: + upsdebugx(4, "Sending compatibility initialization packet (random)"); + bwritten = write_serial_int(serial_fd, string_initialization_comptmode, 5); + break; + } /* end switch */ } /* end else */ if (bwritten < 0) { upsdebugx(1, "%s: Problem to write data to %s", __func__, porta); - if (bwritten == -1) { + if (bwritten == -1) upsdebugx(1, "%s: Data problem", __func__); - } close_serial_port(); } /* end if */ else { @@ -2591,10 +2040,10 @@ void upsdrv_updateinfo(void) { else { upsdebugx(3, "Increase checktime to %u", checktime + 100000); checktime = checktime + 100000; - } + } /* end else */ usleep(checktime); } /* end else */ - } /* end if lastpkthwinfo good/bad checksum */ + } /* end if lastpkthwinfo good/bad checksum */ /* end if */ upsdebugx(3, "%s: finished", __func__); } @@ -2642,8 +2091,7 @@ void upsdrv_initups(void) { /* Validate the complete serial configuration before accessing the device. */ parse_serial_options(); - upsdebugx(1, "%s: Port is %s and baud_rate is %d", - __func__, device_path, baudrate); + upsdebugx(1, "%s: Port is %s and baud_rate is %d", __func__, device_path, baudrate); if (device_path) { if (strcasecmp(device_path, "auto") == 0) @@ -2652,13 +2100,10 @@ void upsdrv_initups(void) { strncpy(porta, device_path, sizeof(porta) - 1); serial_fd = openfd(porta, baudrate); if (INVALID_FD_SER(serial_fd)) - fatalx(EXIT_FAILURE, "Unable to open port %s with baud %d", - porta, baudrate); - else { - upsdebugx(1, "%s: Communication started on port %s, baud rate %d", - __func__, porta, baudrate); - } - } + fatalx(EXIT_FAILURE, "Unable to open port %s with baud %d", porta, baudrate); + else + upsdebugx(1, "%s: Communication started on port %s, baud rate %d", __func__, porta, baudrate); + } /* end if */ else fatalx(EXIT_FAILURE, "Unable to define port and baud"); @@ -2720,6 +2165,12 @@ void upsdrv_makevartable(void) { addvar(VAR_FLAG, "debug_pkt_data", "Enable debug logging of data packet decoding"); addvar(VAR_FLAG, "debug_pkt_hwinfo", "Enable debug logging of hwinfo packet decoding"); + + addvar(VAR_VALUE, "overridemodel", "Override the UPS model name reported by the driver"); + + addvar(VAR_VALUE, "protocolversion", "Override the NHS protocol version stored for the model (default: 3)"); + + addvar(VAR_VALUE, "bypassasalarm", "Report the UPS bypass bit as an alarm: 0 or 1 (default: 1)"); } void upsdrv_help(void) { From 6327e8adee0d2622d9cef6ecef2ec36a4a80b2c3 Mon Sep 17 00:00:00 2001 From: Lucas Bocchi Date: Sun, 30 Aug 2026 19:32:17 -0300 Subject: [PATCH 2/5] Fix strict NHS driver CI diagnostics Initialize protocol and bypass metadata explicitly for every known model so Clang's missing-field check remains clean. Add a documented defensive default to the initialization selector and teach the documentation spell checker the hexadecimal xFE token. Signed-off-by: Lucas Bocchi --- docs/nut.dict | 1 + drivers/nhs_ser.c | 246 ++++++++++++++++++++++++---------------------- 2 files changed, 132 insertions(+), 115 deletions(-) diff --git a/docs/nut.dict b/docs/nut.dict index 2fb4a9e1da..520879069a 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -3758,6 +3758,7 @@ xAAAA xCC xD xF +xFE xFF xXXXX xYYYY diff --git a/drivers/nhs_ser.c b/drivers/nhs_ser.c index ce6db6dea2..2a8f0060fc 100644 --- a/drivers/nhs_ser.c +++ b/drivers/nhs_ser.c @@ -1150,117 +1150,127 @@ static char * strtolow(char* s) { * visualization of protocol codes, descriptions and nominal VA values while * preserving direct lookup by the model code reported by the UPS. */ +/* + * Initialize behavior metadata explicitly for every known model. Keeping the + * defaults beside the model data documents the resolved behavior and prevents + * strict builds from treating omitted structure fields as compilation errors. + */ +#define UPS_INFO_ENTRY(code, description, nominal_va) \ + [code] = { code, description, nominal_va, 3, 1 } + static const upsinfo ups_info_table[114] = { - [1] = { 1, "NHS COMPACT PLUS", 1000 }, - [2] = { 2, "NHS COMPACT PLUS SENOIDAL", 1000 }, - [3] = { 3, "NHS COMPACT PLUS RACK", 1000 }, - [4] = { 4, "NHS PREMIUM PDV", 1500 }, - [5] = { 5, "NHS PREMIUM PDV SENOIDAL", 1500 }, - [6] = { 6, "NHS PREMIUM 1500VA", 1500 }, - [7] = { 7, "NHS PREMIUM 2200VA", 2200 }, - [8] = { 8, "NHS PREMIUM SENOIDAL", 1500 }, - [9] = { 9, "NHS LASER 2600VA", 2600 }, - [10] = { 10, "NHS LASER 3300VA", 3300 }, - [11] = { 11, "NHS LASER 2600VA ISOLADOR", 2600 }, - [12] = { 12, "NHS LASER SENOIDAL", 2600 }, - [13] = { 13, "NHS LASER ON-LINE", 2600 }, - [15] = { 15, "NHS COMPACT PLUS 2003", 1000 }, - [16] = { 16, "COMPACT PLUS SENOIDAL 2003", 1000 }, - [17] = { 17, "COMPACT PLUS RACK 2003", 1000 }, - [18] = { 18, "PREMIUM PDV 2003", 1500 }, - [19] = { 19, "PREMIUM PDV SENOIDAL 2003", 1500 }, - [20] = { 20, "PREMIUM 1500VA 2003", 1500 }, - [21] = { 21, "PREMIUM 2200VA 2003", 2200 }, - [22] = { 22, "PREMIUM SENOIDAL 2003", 1500 }, - [23] = { 23, "LASER 2600VA 2003", 2600 }, - [24] = { 24, "LASER 3300VA 2003", 3300 }, - [25] = { 25, "LASER 2600VA ISOLADOR 2003", 2600 }, - [26] = { 26, "LASER SENOIDAL 2003", 2600 }, - [27] = { 27, "PDV ONLINE 2003", 1500 }, - [28] = { 28, "LASER ONLINE 2003", 3300 }, - [29] = { 29, "EXPERT ONLINE 2003", 5000 }, - [30] = { 30, "MINI 2", 500 }, - [31] = { 31, "COMPACT PLUS 2", 1000 }, - [32] = { 32, "LASER ON-LINE", 2600 }, - [33] = { 33, "PDV SENOIDAL 1500VA", 1500 }, - [34] = { 34, "PDV SENOIDAL 1000VA", 1000 }, - [36] = { 36, "LASER ONLINE 3750VA", 3750 }, - [37] = { 37, "LASER ONLINE 5000VA", 5000 }, - [38] = { 38, "PREMIUM SENOIDAL 2000VA", 2000 }, - [39] = { 39, "LASER SENOIDAL 3500VA", 3500 }, - [40] = { 40, "PREMIUM PDV 1200VA", 1200 }, - [41] = { 41, "PREMIUM 1500VA", 1500 }, - [42] = { 42, "PREMIUM 2200VA", 2200 }, - [43] = { 43, "LASER 2600VA", 2600 }, - [44] = { 44, "LASER 3300VA", 3300 }, - [45] = { 45, "COMPACT PLUS SENOIDAL 700VA", 700 }, - [46] = { 46, "PREMIUM ONLINE 2000VA", 2000 }, - [47] = { 47, "EXPERT ONLINE 10000VA", 10000 }, - [48] = { 48, "LASER SENOIDAL 4200VA", 4200 }, - [49] = { 49, "NHS COMPACT PLUS EXTENDIDO 1500VA", 1500 }, - [50] = { 50, "LASER ONLINE 6000VA", 6000 }, - [51] = { 51, "LASER EXT 3300VA", 3300 }, - [52] = { 52, "NHS COMPACT PLUS 1200VA", 1200 }, - [53] = { 53, "LASER SENOIDAL 3000VA GII", 3000 }, - [54] = { 54, "LASER SENOIDAL 3500VA GII", 3500 }, - [55] = { 55, "LASER SENOIDAL 4200VA GII", 4200 }, - [56] = { 56, "LASER ONLINE 3000VA", 3000 }, - [57] = { 57, "LASER ONLINE 3750VA", 3750 }, - [58] = { 58, "LASER ONLINE 5000VA", 5000 }, - [59] = { 59, "LASER ONLINE 6000VA", 6000 }, - [60] = { 60, "PREMIUM ONLINE 2000VA", 2000 }, - [61] = { 61, "PREMIUM ONLINE 1500VA", 1500 }, - [62] = { 62, "PREMIUM ONLINE 1200VA", 1200 }, - [63] = { 63, "COMPACT PLUS II MAX 1400VA", 1400 }, - [64] = { 64, "PREMIUM PDV MAX 2200VA", 2200 }, - [65] = { 65, "PREMIUM PDV 3000VA", 3000 }, - [66] = { 66, "PREMIUM SENOIDAL 2200VA GII", 2200 }, - [67] = { 67, "LASER PRIME SENOIDAL 3200VA GII", 3200 }, - [68] = { 68, "PREMIUM RACK ONLINE 3000VA", 3000 }, - [69] = { 69, "PREMIUM ONLINE 3000VA", 3000 }, - [70] = { 70, "LASER ONLINE 4000VA", 4000 }, - [71] = { 71, "LASER ONLINE 7500VA", 7500 }, - [72] = { 72, "LASER ONLINE BIFASICO 5000VA", 5000 }, - [73] = { 73, "LASER ONLINE BIFASICO 6000VA", 6000 }, - [74] = { 74, "LASER ONLINE BIFASICO 7500VA", 7500 }, - [75] = { 75, "NHS MINI ST", 500 }, - [76] = { 76, "NHS MINI 120", 120 }, - [77] = { 77, "NHS MINI BIVOLT", 500 }, - [78] = { 78, "PDV 600", 600 }, - [79] = { 79, "NHS MINI MAX", 500 }, - [80] = { 80, "NHS MINI EXT", 500 }, - [81] = { 81, "NHS AUTONOMY PDV 4T", 4000 }, - [82] = { 82, "NHS AUTONOMY PDV 8T", 8000 }, - [83] = { 83, "NHS COMPACT PLUS RACK 1200VA", 1200 }, - [84] = { 84, "PDV SENOIDAL ISOLADOR 1500VA", 1500 }, - [85] = { 85, "NHS PDV RACK 1500VA", 1500 }, - [86] = { 86, "NHS PDV 1400VA S GII", 1400 }, - [87] = { 87, "PDV SENOIDAL ISOLADOR 1500VA", 1500 }, - [88] = { 88, "LASER PRIME SENOIDAL ISOLADOR 2000VA", 2000 }, - [89] = { 89, "PREMIUM SENOIDAL 2400VA GII", 2400 }, - [90] = { 90, "NHS PDV 1400VA S 8T GII", 1400 }, - [91] = { 91, "PREMIUM ONLINE 2000VA", 2000 }, - [92] = { 92, "LASER PRIME ONLINE 2200VA", 2200 }, - [93] = { 93, "PREMIUM RACK ONLINE 2200VA", 2200 }, - [94] = { 94, "PREMIUM SENOIDAL 2400VA GII", 2400 }, - [95] = { 95, "LASER ONLINE 10000VA", 10000 }, - [96] = { 96, "LASER ONLINE BIFASICO 10000VA", 10000 }, - [97] = { 97, "LASER SENOIDAL 3300VA GII", 3300 }, - [98] = { 98, "LASER SENOIDAL 2600VA GII", 2600 }, - [99] = { 99, "PREMIUM SENOIDAL 3000VA GII", 3000 }, - [100] = { 100, "PREMIUM SENOIDAL 2200VA GII", 2200 }, - [101] = { 101, "LASER ONLINE BIFASICO 4000VA", 4000 }, - [102] = { 102, "LASER ONLINE 12000VA", 12000 }, - [103] = { 103, "LASER ONLINE 8000VA", 8000 }, - [104] = { 104, "PDV SENOIDAL ISOLADOR 1000VA", 1000 }, - [105] = { 105, "MINI SENOIDAL 500VA", 500 }, - [106] = { 106, "LASER SENOIDAL 5000VA GII", 5000 }, - [107] = { 107, "COMPACT PLUS SENOIDAL 1000VA", 1000 }, - [108] = { 108, "QUAD_COM 80A", 0 }, - [109] = { 109, "LASER ONLINE 5000VA", 5000 }, - [113] = { 113, "PDV SENOIDAL ISOLADOR 700VA", 700 }, + UPS_INFO_ENTRY(1, "NHS COMPACT PLUS", 1000), + UPS_INFO_ENTRY(2, "NHS COMPACT PLUS SENOIDAL", 1000), + UPS_INFO_ENTRY(3, "NHS COMPACT PLUS RACK", 1000), + UPS_INFO_ENTRY(4, "NHS PREMIUM PDV", 1500), + UPS_INFO_ENTRY(5, "NHS PREMIUM PDV SENOIDAL", 1500), + UPS_INFO_ENTRY(6, "NHS PREMIUM 1500VA", 1500), + UPS_INFO_ENTRY(7, "NHS PREMIUM 2200VA", 2200), + UPS_INFO_ENTRY(8, "NHS PREMIUM SENOIDAL", 1500), + UPS_INFO_ENTRY(9, "NHS LASER 2600VA", 2600), + UPS_INFO_ENTRY(10, "NHS LASER 3300VA", 3300), + UPS_INFO_ENTRY(11, "NHS LASER 2600VA ISOLADOR", 2600), + UPS_INFO_ENTRY(12, "NHS LASER SENOIDAL", 2600), + UPS_INFO_ENTRY(13, "NHS LASER ON-LINE", 2600), + UPS_INFO_ENTRY(15, "NHS COMPACT PLUS 2003", 1000), + UPS_INFO_ENTRY(16, "COMPACT PLUS SENOIDAL 2003", 1000), + UPS_INFO_ENTRY(17, "COMPACT PLUS RACK 2003", 1000), + UPS_INFO_ENTRY(18, "PREMIUM PDV 2003", 1500), + UPS_INFO_ENTRY(19, "PREMIUM PDV SENOIDAL 2003", 1500), + UPS_INFO_ENTRY(20, "PREMIUM 1500VA 2003", 1500), + UPS_INFO_ENTRY(21, "PREMIUM 2200VA 2003", 2200), + UPS_INFO_ENTRY(22, "PREMIUM SENOIDAL 2003", 1500), + UPS_INFO_ENTRY(23, "LASER 2600VA 2003", 2600), + UPS_INFO_ENTRY(24, "LASER 3300VA 2003", 3300), + UPS_INFO_ENTRY(25, "LASER 2600VA ISOLADOR 2003", 2600), + UPS_INFO_ENTRY(26, "LASER SENOIDAL 2003", 2600), + UPS_INFO_ENTRY(27, "PDV ONLINE 2003", 1500), + UPS_INFO_ENTRY(28, "LASER ONLINE 2003", 3300), + UPS_INFO_ENTRY(29, "EXPERT ONLINE 2003", 5000), + UPS_INFO_ENTRY(30, "MINI 2", 500), + UPS_INFO_ENTRY(31, "COMPACT PLUS 2", 1000), + UPS_INFO_ENTRY(32, "LASER ON-LINE", 2600), + UPS_INFO_ENTRY(33, "PDV SENOIDAL 1500VA", 1500), + UPS_INFO_ENTRY(34, "PDV SENOIDAL 1000VA", 1000), + UPS_INFO_ENTRY(36, "LASER ONLINE 3750VA", 3750), + UPS_INFO_ENTRY(37, "LASER ONLINE 5000VA", 5000), + UPS_INFO_ENTRY(38, "PREMIUM SENOIDAL 2000VA", 2000), + UPS_INFO_ENTRY(39, "LASER SENOIDAL 3500VA", 3500), + UPS_INFO_ENTRY(40, "PREMIUM PDV 1200VA", 1200), + UPS_INFO_ENTRY(41, "PREMIUM 1500VA", 1500), + UPS_INFO_ENTRY(42, "PREMIUM 2200VA", 2200), + UPS_INFO_ENTRY(43, "LASER 2600VA", 2600), + UPS_INFO_ENTRY(44, "LASER 3300VA", 3300), + UPS_INFO_ENTRY(45, "COMPACT PLUS SENOIDAL 700VA", 700), + UPS_INFO_ENTRY(46, "PREMIUM ONLINE 2000VA", 2000), + UPS_INFO_ENTRY(47, "EXPERT ONLINE 10000VA", 10000), + UPS_INFO_ENTRY(48, "LASER SENOIDAL 4200VA", 4200), + UPS_INFO_ENTRY(49, "NHS COMPACT PLUS EXTENDIDO 1500VA", 1500), + UPS_INFO_ENTRY(50, "LASER ONLINE 6000VA", 6000), + UPS_INFO_ENTRY(51, "LASER EXT 3300VA", 3300), + UPS_INFO_ENTRY(52, "NHS COMPACT PLUS 1200VA", 1200), + UPS_INFO_ENTRY(53, "LASER SENOIDAL 3000VA GII", 3000), + UPS_INFO_ENTRY(54, "LASER SENOIDAL 3500VA GII", 3500), + UPS_INFO_ENTRY(55, "LASER SENOIDAL 4200VA GII", 4200), + UPS_INFO_ENTRY(56, "LASER ONLINE 3000VA", 3000), + UPS_INFO_ENTRY(57, "LASER ONLINE 3750VA", 3750), + UPS_INFO_ENTRY(58, "LASER ONLINE 5000VA", 5000), + UPS_INFO_ENTRY(59, "LASER ONLINE 6000VA", 6000), + UPS_INFO_ENTRY(60, "PREMIUM ONLINE 2000VA", 2000), + UPS_INFO_ENTRY(61, "PREMIUM ONLINE 1500VA", 1500), + UPS_INFO_ENTRY(62, "PREMIUM ONLINE 1200VA", 1200), + UPS_INFO_ENTRY(63, "COMPACT PLUS II MAX 1400VA", 1400), + UPS_INFO_ENTRY(64, "PREMIUM PDV MAX 2200VA", 2200), + UPS_INFO_ENTRY(65, "PREMIUM PDV 3000VA", 3000), + UPS_INFO_ENTRY(66, "PREMIUM SENOIDAL 2200VA GII", 2200), + UPS_INFO_ENTRY(67, "LASER PRIME SENOIDAL 3200VA GII", 3200), + UPS_INFO_ENTRY(68, "PREMIUM RACK ONLINE 3000VA", 3000), + UPS_INFO_ENTRY(69, "PREMIUM ONLINE 3000VA", 3000), + UPS_INFO_ENTRY(70, "LASER ONLINE 4000VA", 4000), + UPS_INFO_ENTRY(71, "LASER ONLINE 7500VA", 7500), + UPS_INFO_ENTRY(72, "LASER ONLINE BIFASICO 5000VA", 5000), + UPS_INFO_ENTRY(73, "LASER ONLINE BIFASICO 6000VA", 6000), + UPS_INFO_ENTRY(74, "LASER ONLINE BIFASICO 7500VA", 7500), + UPS_INFO_ENTRY(75, "NHS MINI ST", 500), + UPS_INFO_ENTRY(76, "NHS MINI 120", 120), + UPS_INFO_ENTRY(77, "NHS MINI BIVOLT", 500), + UPS_INFO_ENTRY(78, "PDV 600", 600), + UPS_INFO_ENTRY(79, "NHS MINI MAX", 500), + UPS_INFO_ENTRY(80, "NHS MINI EXT", 500), + UPS_INFO_ENTRY(81, "NHS AUTONOMY PDV 4T", 4000), + UPS_INFO_ENTRY(82, "NHS AUTONOMY PDV 8T", 8000), + UPS_INFO_ENTRY(83, "NHS COMPACT PLUS RACK 1200VA", 1200), + UPS_INFO_ENTRY(84, "PDV SENOIDAL ISOLADOR 1500VA", 1500), + UPS_INFO_ENTRY(85, "NHS PDV RACK 1500VA", 1500), + UPS_INFO_ENTRY(86, "NHS PDV 1400VA S GII", 1400), + UPS_INFO_ENTRY(87, "PDV SENOIDAL ISOLADOR 1500VA", 1500), + UPS_INFO_ENTRY(88, "LASER PRIME SENOIDAL ISOLADOR 2000VA", 2000), + UPS_INFO_ENTRY(89, "PREMIUM SENOIDAL 2400VA GII", 2400), + UPS_INFO_ENTRY(90, "NHS PDV 1400VA S 8T GII", 1400), + UPS_INFO_ENTRY(91, "PREMIUM ONLINE 2000VA", 2000), + UPS_INFO_ENTRY(92, "LASER PRIME ONLINE 2200VA", 2200), + UPS_INFO_ENTRY(93, "PREMIUM RACK ONLINE 2200VA", 2200), + UPS_INFO_ENTRY(94, "PREMIUM SENOIDAL 2400VA GII", 2400), + UPS_INFO_ENTRY(95, "LASER ONLINE 10000VA", 10000), + UPS_INFO_ENTRY(96, "LASER ONLINE BIFASICO 10000VA", 10000), + UPS_INFO_ENTRY(97, "LASER SENOIDAL 3300VA GII", 3300), + UPS_INFO_ENTRY(98, "LASER SENOIDAL 2600VA GII", 2600), + UPS_INFO_ENTRY(99, "PREMIUM SENOIDAL 3000VA GII", 3000), + UPS_INFO_ENTRY(100, "PREMIUM SENOIDAL 2200VA GII", 2200), + UPS_INFO_ENTRY(101, "LASER ONLINE BIFASICO 4000VA", 4000), + UPS_INFO_ENTRY(102, "LASER ONLINE 12000VA", 12000), + UPS_INFO_ENTRY(103, "LASER ONLINE 8000VA", 8000), + UPS_INFO_ENTRY(104, "PDV SENOIDAL ISOLADOR 1000VA", 1000), + UPS_INFO_ENTRY(105, "MINI SENOIDAL 500VA", 500), + UPS_INFO_ENTRY(106, "LASER SENOIDAL 5000VA GII", 5000), + UPS_INFO_ENTRY(107, "COMPACT PLUS SENOIDAL 1000VA", 1000), + UPS_INFO_ENTRY(108, "QUAD_COM 80A", 0), + UPS_INFO_ENTRY(109, "LASER ONLINE 5000VA", 5000), + UPS_INFO_ENTRY(113, "PDV SENOIDAL ISOLADOR 700VA", 700), }; +#undef UPS_INFO_ENTRY + static upsinfo getupsinfo(unsigned int upscode) { upsinfo data = { (unsigned int)-1, "NHS UNKNOWN", 0, 3, 1 }; char *overridemodel = getval("overridemodel"); @@ -1269,16 +1279,14 @@ static upsinfo getupsinfo(unsigned int upscode) { if (upscode > 0 && upscode < sizeof(ups_info_table) / sizeof(ups_info_table[0]) && ups_info_table[upscode].upscode == upscode) { data = ups_info_table[upscode]; - data.pversion = 3; - data.bypassasalarm = 1; } /* end if */ /* - * Protocol version 3 and bypass-as-alarm are the defaults for the current - * model table. Values supplied in ups.conf customize only the returned copy, - * leaving the static table unchanged. This also completes the synthetic entry - * used for an unknown model code. Protocol version is retained as model - * metadata for now and does not select framing or initialization commands. + * Protocol version 3 and bypass-as-alarm are initialized explicitly both in + * the model table and in the synthetic unknown-model entry. Values supplied + * in ups.conf customize only the returned copy, leaving the static table + * unchanged. Protocol version is retained as model metadata for now and does + * not select framing or initialization commands. */ if (overridemodel && overridemodel[0] != '\0') { data.upscode = upscode; @@ -2026,6 +2034,14 @@ void upsdrv_updateinfo(void) { upsdebugx(4, "Sending compatibility initialization packet (random)"); bwritten = write_serial_int(serial_fd, string_initialization_comptmode, 5); break; + default: + /* rand() % 3 currently limits this switch to cases 0..2. + * Keep a deterministic fallback so future changes to the + * selection range cannot leave bwritten with stale data. + */ + upsdebugx(4, "Initialization selection out of range; sending long packet"); + bwritten = write_serial_int(serial_fd, string_initialization_long, 9); + break; } /* end switch */ } /* end else */ if (bwritten < 0) { From ee98ac4ebc41e806d9a2c08a8bd8a04011d424a4 Mon Sep 17 00:00:00 2001 From: Lucas Bocchi Date: Sun, 30 Aug 2026 20:43:06 -0300 Subject: [PATCH 3/5] Apply NHS hardware feedback from freechurros Check the HWINFO checksum in its interpreter and publish the documented lower-case battery charger states. Log already-known HWINFO packets quietly and expose rejected frames through raw packet debugging. Credit the packet captures and follow-up analysis contributed in issue #3592 directly beside the affected code and in the release notes. Suggested-by: Free Churro Signed-off-by: Lucas Bocchi --- NEWS.adoc | 10 +++++++++- docs/nut.dict | 1 + drivers/nhs_ser.c | 37 +++++++++++++++++++++++++++++++------ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index 62baf07e01..2e53d6e588 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -133,7 +133,15 @@ https://github.com/networkupstools/nut/milestone/13 internal `0xFF` from being split into unrecognized fragments, allowing the driver to obtain model, firmware, serial number and battery-pack information instead of repeatedly requesting HWINFO and delaying normal - polling. [issue #3592] + polling. The raw packet captures and protocol analysis which made this + correction possible were contributed by GitHub user `freechurros`. + [issue #3592] + * Applied follow-up hardware-test feedback from `freechurros`: corrected + `interpret_pkt_hwinfo()` to validate the HWINFO checksum instead of the + DATA checksum, quietly ignored already-known periodic HWINFO replies, + exposed unrecognized packets through `debug_pkt_raw`, and published the + documented lower-case `battery.charger.status` values for compatibility + with Home Assistant. [issue #3592] * Added the legacy five-byte compatibility initialization request (`FF 05 01 06 FE`) as a third fallback. The driver first makes six extended initialization attempts, then selects randomly among the diff --git a/docs/nut.dict b/docs/nut.dict index 520879069a..4d9fdcf5cd 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -2238,6 +2238,7 @@ fosshost fp fprintf freebsd +freechurros freedesktop freeipmi freetype diff --git a/drivers/nhs_ser.c b/drivers/nhs_ser.c index 2a8f0060fc..08d03eef38 100644 --- a/drivers/nhs_ser.c +++ b/drivers/nhs_ser.c @@ -1426,7 +1426,11 @@ static void interpret_pkt_hwinfo(void) { char hw_scratch_buf[1024]; /* General-purpose string buffer */ unsigned int i = 0; - if (!lastpktdata.checksum_ok) { + /* @freechurros identified in issue #3592 that this function used to + * inspect lastpktdata here. Validate the HWINFO packet being interpreted, + * otherwise a valid HWINFO received before the first DATA packet is lost. + */ + if (!lastpkthwinfo.checksum_ok) { upslogx(LOG_WARNING, "%s: bad lastpkthwinfo.checksum", __func__); return; } /* end if */ @@ -1817,14 +1821,18 @@ static void interpret_pkt_data(void) { dstate_setinfo("battery.runtime", "%u", autonomy_secs); } /* end if */ - /* Battery charger status */ + /* Battery charger status + * @freechurros reported in issue #3592 that Home Assistant rejects the + * former upper-case strings. Use the lower-case vocabulary documented in + * docs/nut-names.txt for interoperability with NUT clients. + */ if (lastpktdata.s_charger_on) - dstate_setinfo("battery.charger.status", "%s", "CHARGING"); + dstate_setinfo("battery.charger.status", "%s", "charging"); else { if (lastpktdata.s_battery_mode) - dstate_setinfo("battery.charger.status", "%s", "DISCHARGING"); + dstate_setinfo("battery.charger.status", "%s", "discharging"); else - dstate_setinfo("battery.charger.status", "%s", "RESTING"); + dstate_setinfo("battery.charger.status", "%s", "resting"); } /* end else */ if (debug_pkt_data) { @@ -1916,7 +1924,11 @@ void upsdrv_updateinfo(void) { */ read_result = ser_get_char(serial_fd, &chr, timeout_sec, timeout_usec); while (read_result > 0) { - /* A 0xFF byte starts a packet only while the reader is idle. After + /* The length-aware framing and the need to preserve marker values inside + * packet payloads were demonstrated by raw captures contributed by + * @freechurros in issue #3592. + * + * A 0xFF byte starts a packet only while the reader is idle. After * the packet starts, byte [1] declares its total size, including the * initial 0xFF, checksum and final 0xFE. Any 0xFF or 0xFE received * before the declared final position belongs to the packet contents. @@ -1961,6 +1973,14 @@ void upsdrv_updateinfo(void) { dstate_dataok(); } /* end if */ } /* end if */ + else { + /* @freechurros noted in issue #3592 that + * periodic HWINFO replies after discovery are valid + * and should not look like unrecognized packets in + * operational logs. + */ + upsdebugx(4, "%s: HWINFO packet already known, ignoring", __func__); + } /* end else */ break; case 21: @@ -1974,6 +1994,11 @@ void upsdrv_updateinfo(void) { default: upslogx(LOG_WARNING, "Incoming packet size not recognized, discarding!"); + /* Suggested by @freechurros in issue #3592: expose the + * rejected bytes when raw packet debugging is enabled so + * future framing variants can be diagnosed from the log. + */ + pdatapacket(datapacket, datapacketsize); break; } /* end switch */ } /* end else */ From 2683471709354178bec4e08c302191b1066d16db Mon Sep 17 00:00:00 2001 From: Lucas Bocchi Date: Sun, 30 Aug 2026 21:42:18 -0300 Subject: [PATCH 4/5] Refresh NHS hardware discovery during driver lifecycle Signed-off-by: Lucas Bocchi --- NEWS.adoc | 14 +++++-- drivers/nhs_ser.c | 99 ++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 95 insertions(+), 18 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index 2e53d6e588..1b94f39259 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -138,10 +138,16 @@ https://github.com/networkupstools/nut/milestone/13 [issue #3592] * Applied follow-up hardware-test feedback from `freechurros`: corrected `interpret_pkt_hwinfo()` to validate the HWINFO checksum instead of the - DATA checksum, quietly ignored already-known periodic HWINFO replies, - exposed unrecognized packets through `debug_pkt_raw`, and published the - documented lower-case `battery.charger.status` values for compatibility - with Home Assistant. [issue #3592] + DATA checksum, exposed unrecognized packets through `debug_pkt_raw`, and + published the documented lower-case `battery.charger.status` values for + compatibility with Home Assistant. [issue #3592] + * Send the long, short and compatibility hardware-discovery requests from + `upsdrv_initinfo()`, checking the serial descriptor before every request. + Failed or partial writes now mark data stale and invalidate the port so + the normal update cycle reopens it before further communication. + * Refresh cached model-specific data from every valid HWINFO packet and + restart discovery after a serial reconnection, allowing a replaced UPS + to be identified without restarting the driver. * Added the legacy five-byte compatibility initialization request (`FF 05 01 06 FE`) as a third fallback. The driver first makes six extended initialization attempts, then selects randomly among the diff --git a/drivers/nhs_ser.c b/drivers/nhs_ser.c index 08d03eef38..637a6718d3 100644 --- a/drivers/nhs_ser.c +++ b/drivers/nhs_ser.c @@ -377,10 +377,13 @@ static float calculate_efficiency(float vacoutrms, float vacinrms); static void parse_serial_options(void); static void close_serial_port(void); static TYPE_FD_SER openfd(const char *portarg, int requested_baudrate); +static TYPE_FD_SER reconnect_ups_if_needed(void); #if 0 static int write_serial(int fd, const char * dados, int size); #endif static int write_serial_int(TYPE_FD_SER fd, const unsigned int *data, size_t size); +static bool send_initialization_packet( + const char *description, const unsigned int *data, size_t size); static void print_pkt_hwinfo(pkt_hwinfo data); static void print_pkt_data(pkt_data data); @@ -1339,6 +1342,58 @@ static float get_vin_perc(char * var) { return DEFAULTPERC; } +/* + * Send one hardware-discovery request only through a valid serial descriptor. + * If the descriptor is already invalid, try the driver's bounded reopen path. + * A failed or partial write closes and invalidates the descriptor so the next + * update cycle knows that it must reopen the port before communicating again. + */ +static bool send_initialization_packet( + const char *description, const unsigned int *data, size_t size) +{ + int written; + + if (INVALID_FD_SER(serial_fd)) { + upslogx(LOG_WARNING, + "%s: serial port %s is not open before the %s initialization request; trying to reopen it", + __func__, porta, description); + if (INVALID_FD_SER(reconnect_ups_if_needed())) { + upslogx(LOG_WARNING, + "%s: unable to send the %s initialization request because serial port %s could not be reopened", + __func__, description, porta); + dstate_datastale(); + return false; + } /* end if */ + } /* end if */ + + upsdebugx(3, + "%s: sending %s initialization request (%" PRIuSIZE " bytes) on %s", + __func__, description, size, porta); + errno = 0; + written = write_serial_int(serial_fd, data, size); + if (written < 0 || (size_t)written != size) { + if (errno != 0) + upslog_with_errno(LOG_WARNING, + "%s: failed to send the %s initialization request on %s", + __func__, description, porta); + else + upslogx(LOG_WARNING, + "%s: failed to send the complete %s initialization request on %s; wrote %d of %" PRIuSIZE " bytes", + __func__, description, porta, written, size); + + /* Mark the connection unusable. upsdrv_updateinfo() will invoke the + * normal reopen procedure before its next read or retry. + */ + close_serial_port(); + dstate_datastale(); + return false; + } /* end if */ + + upsdebugx(3, "%s: %s initialization request sent successfully on %s", + __func__, description, porta); + return true; +} + void upsdrv_initinfo(void) { /* From docs/new-drivers.txt: * Try to detect what kind of UPS is out there, @@ -1355,6 +1410,16 @@ void upsdrv_initinfo(void) { upsdebugx(3, "%s: starting...", __func__); /* TODO: Any instant commands? */ + if (!send_initialization_packet("long", string_initialization_long, 9)) + return; + usleep(250000); + if (!send_initialization_packet("short", string_initialization_short, 9)) + return; + usleep(250000); + if (!send_initialization_packet("compatibility", string_initialization_comptmode, 5)) + return; + + upsdebugx(3, "%s: initialization commands sent", __func__); upsdebugx(3, "%s: finished", __func__); } @@ -1407,6 +1472,14 @@ static TYPE_FD_SER reconnect_ups_if_needed(void) { if (VALID_FD_SER(serial_fd)) { if (retries > MAXTRIES && may_log_reconnect_trying(1)) upslogx(LOG_NOTICE, "Communications with UPS re-established"); + + /* A reopened serial port may now be connected to a different UPS. + * Invalidate the cached discovery result and restart the initialization + * sequence so model-specific data is obtained without restarting NUT. + */ + lastpkthwinfo.checksum_ok = false; + send_extended = 0; + checktime = 2000000; retries = 0; reconnect_trying(RECONNECT_SUCCESS); } /* end if */ @@ -1905,6 +1978,7 @@ void upsdrv_updateinfo(void) { useconds_t timeout_usec; ssize_t read_result; int randval = 0; + pkt_hwinfo received_hwinfo; upsdebugx(3, "%s: starting...", __func__); @@ -1965,22 +2039,19 @@ void upsdrv_updateinfo(void) { switch (datapacketsize) { case 18: case 50: - if (!lastpkthwinfo.checksum_ok) { - lastpkthwinfo = mount_hwinfo(datapacket, datapacketsize); + received_hwinfo = mount_hwinfo(datapacket, datapacketsize); - if (lastpkthwinfo.checksum_ok) { - interpret_pkt_hwinfo(); - dstate_dataok(); - } /* end if */ + /* Always accept a new valid HWINFO packet. The UPS may be + * replaced while the driver remains active, so retaining only + * the first packet would leave model-specific data stale until + * NUT is restarted. Preserve the last valid data if the newly + * received packet has a bad checksum. + */ + if (received_hwinfo.checksum_ok) { + lastpkthwinfo = received_hwinfo; + interpret_pkt_hwinfo(); + dstate_dataok(); } /* end if */ - else { - /* @freechurros noted in issue #3592 that - * periodic HWINFO replies after discovery are valid - * and should not look like unrecognized packets in - * operational logs. - */ - upsdebugx(4, "%s: HWINFO packet already known, ignoring", __func__); - } /* end else */ break; case 21: From 07f929ce7c5290dd3df26eca42c94a101e09fbab Mon Sep 17 00:00:00 2001 From: Lucas Bocchi Date: Sun, 30 Aug 2026 22:29:14 -0300 Subject: [PATCH 5/5] Simplify NHS initialization packet helper Pass the packet data, byte count and serial descriptor directly to the helper. Keep serial validation, reopening, write checks and diagnostics inside the helper so the initialization calls are easier to read. Signed-off-by: Lucas Bocchi --- drivers/nhs_ser.c | 43 +++++++++++++++---------------------------- 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/drivers/nhs_ser.c b/drivers/nhs_ser.c index 637a6718d3..0bc6f6af31 100644 --- a/drivers/nhs_ser.c +++ b/drivers/nhs_ser.c @@ -382,8 +382,7 @@ static TYPE_FD_SER reconnect_ups_if_needed(void); static int write_serial(int fd, const char * dados, int size); #endif static int write_serial_int(TYPE_FD_SER fd, const unsigned int *data, size_t size); -static bool send_initialization_packet( - const char *description, const unsigned int *data, size_t size); +static bool send_initialization_packet(const unsigned int *data, size_t size, TYPE_FD_SER fd); static void print_pkt_hwinfo(pkt_hwinfo data); static void print_pkt_data(pkt_data data); @@ -1348,38 +1347,27 @@ static float get_vin_perc(char * var) { * A failed or partial write closes and invalidates the descriptor so the next * update cycle knows that it must reopen the port before communicating again. */ -static bool send_initialization_packet( - const char *description, const unsigned int *data, size_t size) -{ +static bool send_initialization_packet(const unsigned int *data, size_t size, TYPE_FD_SER fd) { int written; - if (INVALID_FD_SER(serial_fd)) { - upslogx(LOG_WARNING, - "%s: serial port %s is not open before the %s initialization request; trying to reopen it", - __func__, porta, description); - if (INVALID_FD_SER(reconnect_ups_if_needed())) { - upslogx(LOG_WARNING, - "%s: unable to send the %s initialization request because serial port %s could not be reopened", - __func__, description, porta); + if (INVALID_FD_SER(fd)) { + upslogx(LOG_WARNING, "%s: serial port %s is not open before the initialization request; trying to reopen it", __func__, porta); + fd = reconnect_ups_if_needed(); + if (INVALID_FD_SER(fd)) { + upslogx(LOG_WARNING, "%s: unable to send the initialization request because serial port %s could not be reopened", __func__, porta); dstate_datastale(); return false; } /* end if */ } /* end if */ - upsdebugx(3, - "%s: sending %s initialization request (%" PRIuSIZE " bytes) on %s", - __func__, description, size, porta); + upsdebugx(3, "%s: sending initialization request (%" PRIuSIZE " bytes) on %s", __func__, size, porta); errno = 0; - written = write_serial_int(serial_fd, data, size); + written = write_serial_int(fd, data, size); if (written < 0 || (size_t)written != size) { if (errno != 0) - upslog_with_errno(LOG_WARNING, - "%s: failed to send the %s initialization request on %s", - __func__, description, porta); + upslog_with_errno(LOG_WARNING, "%s: failed to send the initialization request on %s", __func__, porta); else - upslogx(LOG_WARNING, - "%s: failed to send the complete %s initialization request on %s; wrote %d of %" PRIuSIZE " bytes", - __func__, description, porta, written, size); + upslogx(LOG_WARNING, "%s: failed to send the complete initialization request on %s; wrote %d of %" PRIuSIZE " bytes", __func__, porta, written, size); /* Mark the connection unusable. upsdrv_updateinfo() will invoke the * normal reopen procedure before its next read or retry. @@ -1389,8 +1377,7 @@ static bool send_initialization_packet( return false; } /* end if */ - upsdebugx(3, "%s: %s initialization request sent successfully on %s", - __func__, description, porta); + upsdebugx(3, "%s: initialization request sent successfully on %s", __func__, porta); return true; } @@ -1410,13 +1397,13 @@ void upsdrv_initinfo(void) { upsdebugx(3, "%s: starting...", __func__); /* TODO: Any instant commands? */ - if (!send_initialization_packet("long", string_initialization_long, 9)) + if (!send_initialization_packet(string_initialization_long, 9, serial_fd)) return; usleep(250000); - if (!send_initialization_packet("short", string_initialization_short, 9)) + if (!send_initialization_packet(string_initialization_short, 9, serial_fd)) return; usleep(250000); - if (!send_initialization_packet("compatibility", string_initialization_comptmode, 5)) + if (!send_initialization_packet(string_initialization_comptmode, 5, serial_fd)) return; upsdebugx(3, "%s: initialization commands sent", __func__);