diff --git a/NEWS.adoc b/NEWS.adoc index 8da073ce8e..1b94f39259 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -122,6 +122,41 @@ 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. 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, 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 + 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/docs/nut.dict b/docs/nut.dict index 2fb4a9e1da..4d9fdcf5cd 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -2238,6 +2238,7 @@ fosshost fp fprintf freebsd +freechurros freedesktop freeipmi freetype @@ -3758,6 +3759,7 @@ xAAAA xCC xD xF +xFE xFF xXXXX xYYYY diff --git a/drivers/nhs_ser.c b/drivers/nhs_ser.c index 79e7d9e027..0bc6f6af31 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 */ @@ -371,10 +377,12 @@ 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 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); @@ -399,9 +407,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 +418,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 +436,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 +461,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 +534,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 +570,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 +583,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 +601,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 +636,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 +647,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 +671,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 +691,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 +728,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 +759,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 +797,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 +812,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 +825,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 +960,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 +1075,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 +1083,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 +1127,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 +1147,158 @@ 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; +/* + * 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. + */ +/* + * 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] = { + 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), +}; - case 106: - data.upscode = 106; - strncpy(data.upsdesc, "LASER SENOIDAL 5000VA GII", sizeof(data.upsdesc)); - data.VA = 5000; - break; +#undef UPS_INFO_ENTRY - case 107: - data.upscode = 107; - strncpy(data.upsdesc, "COMPACT PLUS SENOIDAL 1000VA", sizeof(data.upsdesc)); - data.VA = 1000; - 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 108: - data.upscode = 108; - strncpy(data.upsdesc, "QUAD_COM 80A", sizeof(data.upsdesc)); - data.VA = 0; - 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]; + } /* end if */ - case 109: - data.upscode = 109; - strncpy(data.upsdesc, "LASER ONLINE 5000VA", sizeof(data.upsdesc)); - data.VA = 5000; - break; - - 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 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; + 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 +1314,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) { @@ -1884,6 +1341,46 @@ 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 unsigned int *data, size_t size, TYPE_FD_SER fd) { + int written; + + 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 initialization request (%" PRIuSIZE " bytes) on %s", __func__, size, porta); + errno = 0; + 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 initialization request on %s", __func__, porta); + else + 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. + */ + close_serial_port(); + dstate_datastale(); + return false; + } /* end if */ + + upsdebugx(3, "%s: initialization request sent successfully on %s", __func__, porta); + return true; +} + void upsdrv_initinfo(void) { /* From docs/new-drivers.txt: * Try to detect what kind of UPS is out there, @@ -1900,6 +1397,16 @@ void upsdrv_initinfo(void) { upsdebugx(3, "%s: starting...", __func__); /* TODO: Any instant commands? */ + if (!send_initialization_packet(string_initialization_long, 9, serial_fd)) + return; + usleep(250000); + if (!send_initialization_packet(string_initialization_short, 9, serial_fd)) + return; + usleep(250000); + if (!send_initialization_packet(string_initialization_comptmode, 5, serial_fd)) + return; + + upsdebugx(3, "%s: initialization commands sent", __func__); upsdebugx(3, "%s: finished", __func__); } @@ -1925,11 +1432,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 +1444,38 @@ 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"); - } + + /* 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); - } 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 +1484,31 @@ 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__); + /* @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 */ 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 +1527,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 +1543,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 +1561,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 +1587,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 +1618,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 +1653,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 +1689,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 +1703,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 +1720,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 +1747,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 +1811,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 +1843,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 +1879,23 @@ 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) { - dstate_setinfo("battery.charger.status", "%s", "CHARGING"); - } else { + /* 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"); + 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) { - 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 +1934,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 +1956,16 @@ 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; + pkt_hwinfo received_hwinfo; upsdebugx(3, "%s: starting...", __func__); @@ -2459,96 +1974,115 @@ 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) { + /* 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. + * 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: + received_hwinfo = mount_hwinfo(datapacket, datapacketsize); + + /* 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 */ + 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!"); + /* 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 */ - 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 +2093,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 +2103,34 @@ 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; + 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) { 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 +2139,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 +2190,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 +2199,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 +2264,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) {