2123 lines
63 KiB
C
2123 lines
63 KiB
C
|
/*
|
||
|
* huawei-ups2000.c - Driver for Huawei UPS2000 (1kVA-3kVA)
|
||
|
*
|
||
|
* Note: Huawei UPS2000 (1kVA-3kVA) can be accessed via RS-232,
|
||
|
* USB, or an optional RMS-MODBUS01B (RS-485) adapter. Only
|
||
|
* RS-232 and USB are supported, RS-485 is not.
|
||
|
*
|
||
|
* The USB port on the UPS is implemented via a MaxLinear RX21V1410
|
||
|
* USB-to-serial converter, and can be recongized as a standard
|
||
|
* USB-CDC serial device. Unfortunately, the generic USB-CDC driver
|
||
|
* is incompatible with the specific chip configuration and cannot
|
||
|
* be used. A device-specific driver, "xr_serial", must be used.
|
||
|
*
|
||
|
* The driver has only been merged to Linux 5.12 or later, via the
|
||
|
* "xr_serial" kernel module. When the UPS2000 is connected via USB
|
||
|
* to a supported Linux system, you should see the following logs in
|
||
|
* "dmesg".
|
||
|
*
|
||
|
* xr_serial 1-1.2:1.1: xr_serial converter detected
|
||
|
* usb 1-1.2: xr_serial converter now attached to ttyUSB0
|
||
|
*
|
||
|
* The driver must be "xr_serial". If your system doesn't have the
|
||
|
* necessary device driver, you will get this message instead:
|
||
|
*
|
||
|
* cdc_acm 1-1.2:1.0: ttyACM0: USB ACM device
|
||
|
*
|
||
|
* On other operating systems, USB cannot be used due to the absence
|
||
|
* of the driver. You must use connect UPS2000 to your computer via
|
||
|
* RS-232, either directly or using an USB-to-RS-232 converter supported
|
||
|
* by your Linux or BSD kernel.
|
||
|
*
|
||
|
* A document describing the protocol implemented by this driver can
|
||
|
* be found online at:
|
||
|
*
|
||
|
* https://support.huawei.com/enterprise/en/doc/EDOC1000110696
|
||
|
*
|
||
|
* Huawei UPS2000 driver implemented by
|
||
|
* Copyright (C) 2020, 2021 Yifeng Li <tomli@tomli.me>
|
||
|
* The author is not affiliated to Huawei or other manufacturers.
|
||
|
*
|
||
|
* This program is free software; you can redistribute it and/or modify
|
||
|
* it under the terms of the GNU General Public License as published by
|
||
|
* the Free Software Foundation; either version 2 of the License, or
|
||
|
* (at your option) any later version.
|
||
|
*
|
||
|
* This program is distributed in the hope that it will be useful,
|
||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
* GNU General Public License for more details.
|
||
|
*
|
||
|
* You should have received a copy of the GNU General Public License
|
||
|
* along with this program; if not, write to the Free Software
|
||
|
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||
|
*/
|
||
|
|
||
|
#include "config.h" /* must be the first header */
|
||
|
|
||
|
#include <stdbool.h>
|
||
|
#include <modbus.h>
|
||
|
#include "main.h"
|
||
|
#include "serial.h"
|
||
|
|
||
|
#define DRIVER_NAME "NUT Huawei UPS2000 (1kVA-3kVA) RS-232 Modbus driver"
|
||
|
#define DRIVER_VERSION "0.02"
|
||
|
|
||
|
#define CHECK_BIT(var,pos) ((var) & (1<<(pos)))
|
||
|
#define MODBUS_SLAVE_ID 1
|
||
|
|
||
|
/*
|
||
|
* Known UPS models. We only attempt to load the driver if
|
||
|
* the initial communication indicates the UPS is a known
|
||
|
* model of the UPS2000 series.
|
||
|
*/
|
||
|
static const char *supported_model[] = {
|
||
|
"UPS2000", "UPS2000A",
|
||
|
NULL
|
||
|
};
|
||
|
|
||
|
/*
|
||
|
* UPS2000 device identification. The information is obtained during
|
||
|
* initial communication using Modbus command 0x2B (read device identi-
|
||
|
* fication) to read the object 0x87 (device list). The object contains
|
||
|
* a list of fields, each with a type, length, and value. The object is
|
||
|
* parsed by ups2000_device_identification() and filled into the array
|
||
|
* of struct ups2000_ident.
|
||
|
*
|
||
|
* Fields of interest are:
|
||
|
*
|
||
|
* 0x87, int32 (Device Count): Only one UPS unit is supported,
|
||
|
* the driver aborts if more than one device is detected.
|
||
|
*
|
||
|
* 0x88, string (Device Description of the 1st unit): This is a
|
||
|
* ASCII string that contains information about the 1st UPS unit.
|
||
|
* This string, again, contains a list of fields. They are parsed
|
||
|
* further into the array ups2000_desc.
|
||
|
*
|
||
|
*/
|
||
|
#define UPS2000_IDENT_MAX_FIELDS 9
|
||
|
#define UPS2000_IDENT_MAX_LEN 128
|
||
|
#define UPS2000_IDENT_OFFSET
|
||
|
static struct {
|
||
|
uint8_t type;
|
||
|
uint8_t len;
|
||
|
uint8_t val[UPS2000_IDENT_MAX_LEN];
|
||
|
} ups2000_ident[UPS2000_IDENT_MAX_FIELDS];
|
||
|
|
||
|
/*
|
||
|
* UPS2000 device description. The information is initially obtained
|
||
|
* as field 0x88 in the UPS2000 device identification. This field is
|
||
|
* a semicolon seperated ASCII string that contains multiple fields.
|
||
|
* It is parsed again by ups2000_device_identification() and filled
|
||
|
* into the ups2000_desc[] 2D array. The first dimension is used as
|
||
|
* a key to select the wanted field (defined in the following enmu,
|
||
|
* the second dimension is a NULL-terminated ASCII string.
|
||
|
*
|
||
|
* Note that ups2000_desc[0] is deliberately unused, the array begins
|
||
|
* at one, allowing mapping from UPS2000_DESC_* to ups2000_desc[]
|
||
|
* directly without using offsets.
|
||
|
*/
|
||
|
#define UPS2000_DESC_MAX_FIELDS 9
|
||
|
#define UPS2000_DESC_MAX_LEN 128
|
||
|
enum {
|
||
|
UPS2000_DESC_MODEL = 1,
|
||
|
UPS2000_DESC_FIRMWARE_REV,
|
||
|
UPS2000_DESC_PROTOCOL_REV,
|
||
|
UPS2000_DESC_ESN,
|
||
|
UPS2000_DESC_DEVICE_ID, /* currently unused */
|
||
|
UPS2000_DESC_PARALLEL_ID /* currently unused */
|
||
|
};
|
||
|
static char ups2000_desc[UPS2000_DESC_MAX_FIELDS][UPS2000_DESC_MAX_LEN] = { { 0 } };
|
||
|
|
||
|
/* global variable for modbus communication */
|
||
|
static modbus_t *modbus_ctx = NULL;
|
||
|
|
||
|
/*
|
||
|
* How many seconds to wait before switching off/on/reboot the UPS?
|
||
|
*
|
||
|
* This can be set at startup time via a command-line argument,
|
||
|
* or at runtime by writing to RW variables "ups.delay.shutdown"
|
||
|
* and "ups.delay.start". See ups2000_delay_get/set.
|
||
|
*/
|
||
|
#define UPS2000_DELAY_INVALID 0xFFFF
|
||
|
static uint16_t ups2000_offdelay = UPS2000_DELAY_INVALID;
|
||
|
static uint16_t ups2000_ondelay = UPS2000_DELAY_INVALID;
|
||
|
static uint16_t ups2000_rebootdelay = UPS2000_DELAY_INVALID;
|
||
|
|
||
|
/*
|
||
|
* Time when the current shutdown/reboot request is expected
|
||
|
* to complete. This is used to calculate the ETA, See
|
||
|
* ups2000_update_timers().
|
||
|
*/
|
||
|
static time_t shutdown_at = 0;
|
||
|
static time_t reboot_at = 0;
|
||
|
static time_t start_at = 0;
|
||
|
|
||
|
/*
|
||
|
* Is it safe to enter bypass mode? It's checked by ups2000_update_alarm()
|
||
|
* and used by ups2000_instcmd_bypass_start().
|
||
|
*/
|
||
|
static bool bypass_available = 0;
|
||
|
|
||
|
/* function prototypes */
|
||
|
static int ups2000_update_info(void);
|
||
|
static int ups2000_update_status(void);
|
||
|
static int ups2000_update_alarm(void);
|
||
|
static int ups2000_update_timers(void);
|
||
|
static void ups2000_device_identification(void);
|
||
|
static size_t ups2000_read_serial(uint8_t *buf, size_t buf_len);
|
||
|
static int ups2000_read_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest);
|
||
|
static int ups2000_write_register(modbus_t *ctx, int addr, uint16_t val);
|
||
|
static int ups2000_write_registers(modbus_t *ctx, int addr, int nb, uint16_t *src);
|
||
|
static uint16_t crc16(uint8_t *buffer, uint16_t buffer_length);
|
||
|
static time_t time_seek(time_t t, int seconds);
|
||
|
|
||
|
/* rw variables function prototypes */
|
||
|
static int ups2000_update_rw_var(void);
|
||
|
static int setvar(const char *name, const char *val);
|
||
|
static int ups2000_autostart_set(const uint16_t reg, const char *string);
|
||
|
static int ups2000_autostart_get(const uint16_t reg);
|
||
|
static int ups2000_beeper_set(const uint16_t reg, const char *string);
|
||
|
static int ups2000_beeper_get(const uint16_t reg);
|
||
|
static void ups2000_delay_get(void);
|
||
|
static int ups2000_delay_set(const char *var, const char *string);
|
||
|
|
||
|
/* instant command function prototypes */
|
||
|
static void ups2000_init_instcmd(void);
|
||
|
static int instcmd(const char *cmd, const char *extra);
|
||
|
static int ups2000_instcmd_load_on(const uint16_t reg);
|
||
|
static int ups2000_instcmd_bypass_start(const uint16_t reg);
|
||
|
static int ups2000_instcmd_beeper_toggle(const uint16_t reg);
|
||
|
static int ups2000_instcmd_shutdown_stayoff(const uint16_t reg);
|
||
|
static int ups2000_instcmd_shutdown_return(const uint16_t reg);
|
||
|
static int ups2000_instcmd_shutdown_reboot(const uint16_t reg);
|
||
|
static int ups2000_instcmd_shutdown_reboot_graceful(const uint16_t reg);
|
||
|
|
||
|
/* driver description structure */
|
||
|
upsdrv_info_t upsdrv_info = {
|
||
|
DRIVER_NAME,
|
||
|
DRIVER_VERSION,
|
||
|
"Yifeng Li <tomli@tomli.me>\n",
|
||
|
DRV_EXPERIMENTAL,
|
||
|
{ NULL }
|
||
|
};
|
||
|
|
||
|
|
||
|
void upsdrv_initups(void)
|
||
|
{
|
||
|
int r;
|
||
|
|
||
|
upsdebugx(2, "upsdrv_initups");
|
||
|
|
||
|
/*
|
||
|
* This is an ugly workaround to a serious problem: libmodbus doesn't
|
||
|
* support device identification. Although there's a function called
|
||
|
* modbus_send_raw_request() for custom commands, but modbus_receive_
|
||
|
* confirmation() assumes a message length in the header, which is
|
||
|
* incompatible with device identification - It simply stops reading
|
||
|
* in the middle of the message and cannot receive our message. Worse,
|
||
|
* there's no public API to receive a raw response.
|
||
|
*
|
||
|
* See: https://github.com/stephane/libmodbus/issues/231
|
||
|
*
|
||
|
* Thus, the only thing we could do is opening it as a serial device
|
||
|
* for device identification, and reopen it via libmodbus for other
|
||
|
* commands as usual. We also have to copy the CRC-16 function from
|
||
|
* the libmodbus source code since there's no public API to use that...
|
||
|
*/
|
||
|
upsfd = ser_open(device_path);
|
||
|
ser_set_speed(upsfd, device_path, B9600);
|
||
|
ser_set_rts(upsfd, 0);
|
||
|
ser_set_dtr(upsfd, 0);
|
||
|
|
||
|
modbus_ctx = modbus_new_rtu(device_path, 9600, 'N', 8, 1);
|
||
|
if (modbus_ctx == NULL)
|
||
|
fatalx(EXIT_FAILURE, "Unable to create the libmodbus context");
|
||
|
|
||
|
#if LIBMODBUS_VERSION_CHECK(3, 1, 2)
|
||
|
/* It can take as slow as 1 sec. for the UPS to respond. */
|
||
|
modbus_set_response_timeout(modbus_ctx, 1, 0);
|
||
|
#else
|
||
|
{
|
||
|
struct timeval timeout;
|
||
|
timeout.tv_sec = 1;
|
||
|
timeout.tv_usec = 0;
|
||
|
modbus_set_response_timeout(modbus_ctx, &timeout);
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
r = modbus_set_slave(modbus_ctx, MODBUS_SLAVE_ID);
|
||
|
if (r < 0) {
|
||
|
modbus_free(modbus_ctx);
|
||
|
fatalx(EXIT_FAILURE, "Invalid slave ID %d", MODBUS_SLAVE_ID);
|
||
|
}
|
||
|
|
||
|
if (modbus_connect(modbus_ctx) == -1) {
|
||
|
modbus_free(modbus_ctx);
|
||
|
fatalx(EXIT_FAILURE, "modbus_connect: unable to connect: %s", modbus_strerror(errno));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
#define IDENT_REQUEST_LEN 7
|
||
|
#define IDENT_RESPONSE_MAX_LEN 128
|
||
|
#define IDENT_RESPONSE_HEADER_LEN 8
|
||
|
#define IDENT_RESPONSE_CRC_LEN 2
|
||
|
#define IDENT_FIELD_HEADER_LEN 2
|
||
|
static void ups2000_device_identification(void)
|
||
|
{
|
||
|
static const uint8_t ident_req[IDENT_REQUEST_LEN] = {
|
||
|
MODBUS_SLAVE_ID, /* addr */
|
||
|
0x2B, /* command: device identification */
|
||
|
0x0E, /* MEI type */
|
||
|
0x03, /* ReadDevID: extended identification */
|
||
|
0x87, /* Object ID: device list */
|
||
|
0x31, 0x75 /* CRC-16 */
|
||
|
};
|
||
|
|
||
|
/*
|
||
|
* Response header:
|
||
|
* 0x01, 0x2B, 0x0E, 0x03, 0x03, 0x00, 0x00, 0x02
|
||
|
*
|
||
|
* Response fields:
|
||
|
* header: 0x87, 0x04 // type (device counts), length
|
||
|
* data: uint32_t
|
||
|
* (e.g. 0x00, 0x00, 0x00, 0x01)
|
||
|
*
|
||
|
* header: 0x88, 0x?? // type (1st dev desc), length
|
||
|
* data: ASCII string
|
||
|
* (e.g. 1=UPS2000;2=V100R001C01SPC120;3=...)
|
||
|
*
|
||
|
* header: 0x89, 0x?? // type (2nd dev desc), length
|
||
|
* data: ASCII string
|
||
|
*
|
||
|
* ...
|
||
|
* header: 0xFF, 0x?? // type (120th dev desc), length
|
||
|
* data: ASCII string
|
||
|
*
|
||
|
* CRC-16:
|
||
|
* 0x??, 0x??
|
||
|
*/
|
||
|
static const uint8_t expected_header[IDENT_RESPONSE_HEADER_LEN] = {
|
||
|
MODBUS_SLAVE_ID,
|
||
|
0x2B, 0x0E, 0x03, 0x03, 0x00, 0x00, 0x02,
|
||
|
};
|
||
|
|
||
|
bool serial_fail = 0; /* unable to read from serial */
|
||
|
uint16_t crc16_recv, crc16_calc; /* resp CRC */
|
||
|
bool crc16_fail = 0; /* resp CRC failure */
|
||
|
uint32_t ups_count = 0; /* number of UPS in the resp list */
|
||
|
uint8_t ident_response[IDENT_RESPONSE_MAX_LEN]; /* resp buf */
|
||
|
size_t ident_response_len; /* buf len */
|
||
|
uint8_t *ident_response_end = NULL; /* buf end marker (excluding CRC) */
|
||
|
uint8_t *ptr = NULL; /* buf iteratior */
|
||
|
|
||
|
/* a desc string copied from ups2000_ident[] */
|
||
|
char *ups2000_ident_desc = NULL;
|
||
|
int i;
|
||
|
ssize_t r;
|
||
|
|
||
|
/* attempt to obtain a response header with valid CRC. */
|
||
|
for (i = 0; i < 3; i++) {
|
||
|
/* step 1: record response length and initialize ptr */
|
||
|
upsdebugx(2, "ser_send_buf");
|
||
|
|
||
|
ser_flush_in(upsfd, "", nut_debug_level);
|
||
|
r = ser_send_buf(upsfd, ident_req, IDENT_REQUEST_LEN);
|
||
|
if (r != IDENT_REQUEST_LEN) {
|
||
|
fatalx(EXIT_FAILURE, "unable to send request!\n");
|
||
|
}
|
||
|
|
||
|
ident_response_len = ups2000_read_serial(ident_response, IDENT_RESPONSE_MAX_LEN);
|
||
|
ptr = ident_response;
|
||
|
ident_response_end = ptr + ident_response_len - IDENT_RESPONSE_CRC_LEN;
|
||
|
|
||
|
/* step 2: check response length */
|
||
|
if (ident_response_len == 0) {
|
||
|
upslogx(LOG_ERR, "unable to read from serial port %s, retry...", device_path);
|
||
|
serial_fail = 1;
|
||
|
continue;
|
||
|
}
|
||
|
else
|
||
|
serial_fail = 0;
|
||
|
|
||
|
upsdebug_hex(2, "ups2000_read_serial() received", ptr, ident_response_len);
|
||
|
|
||
|
if (ptr + IDENT_RESPONSE_HEADER_LEN > ident_response_end) {
|
||
|
fatalx(EXIT_FAILURE, "response header too short! "
|
||
|
"expected %d, received %zu.",
|
||
|
IDENT_RESPONSE_HEADER_LEN, ident_response_len);
|
||
|
}
|
||
|
|
||
|
/* step 3: check response CRC-16 */
|
||
|
crc16_recv = (uint16_t)((uint16_t)(ident_response_end[0]) << 8) | (uint16_t)(ident_response_end[1]);
|
||
|
if (ident_response_len < IDENT_RESPONSE_CRC_LEN
|
||
|
|| (((uintmax_t)(ident_response_len) - IDENT_RESPONSE_CRC_LEN) > UINT16_MAX)
|
||
|
) {
|
||
|
fatalx(EXIT_FAILURE, "response header shorter than CRC "
|
||
|
"or longer than UINT16_MAX!");
|
||
|
}
|
||
|
|
||
|
crc16_calc = crc16(ident_response, (uint16_t)(ident_response_len - IDENT_RESPONSE_CRC_LEN));
|
||
|
if (crc16_recv == crc16_calc) {
|
||
|
crc16_fail = 0;
|
||
|
break;
|
||
|
}
|
||
|
crc16_fail = 1;
|
||
|
}
|
||
|
|
||
|
/* step 4: check serial & CRC-16 verification status */
|
||
|
if (serial_fail)
|
||
|
fatalx(EXIT_FAILURE, "unable to read from serial port %s!", device_path);
|
||
|
|
||
|
if (crc16_fail)
|
||
|
fatalx(EXIT_FAILURE, "response CRC verification failed!");
|
||
|
|
||
|
/* step 5: check response header */
|
||
|
if (memcmp(expected_header, ident_response, IDENT_RESPONSE_HEADER_LEN))
|
||
|
fatalx(EXIT_FAILURE, "unexpected response header!");
|
||
|
|
||
|
ptr += IDENT_RESPONSE_HEADER_LEN;
|
||
|
|
||
|
/* step 6: extract ident fields */
|
||
|
memset(ups2000_ident, 0x00, sizeof(ups2000_ident));
|
||
|
for (i = 0; i < UPS2000_IDENT_MAX_FIELDS; i++) {
|
||
|
uint8_t type, len;
|
||
|
|
||
|
if (ptr + 2 > ident_response_end)
|
||
|
break;
|
||
|
|
||
|
type = *ptr++;
|
||
|
len = *ptr++;
|
||
|
|
||
|
if (len + 1 > UPS2000_IDENT_MAX_LEN)
|
||
|
fatalx(EXIT_FAILURE, "response field too long!");
|
||
|
|
||
|
ups2000_ident[i].type = type;
|
||
|
ups2000_ident[i].len = len;
|
||
|
/*
|
||
|
* Always zero-terminate the bytes, in case the data
|
||
|
* is an ASCII string (i.e. device desc string), libc
|
||
|
* string functions can be used.
|
||
|
*/
|
||
|
ups2000_ident[i].val[len] = '\0';
|
||
|
|
||
|
if (ptr + len > ident_response_end)
|
||
|
fatalx(EXIT_FAILURE, "response field too short!");
|
||
|
|
||
|
memcpy(ups2000_ident[i].val, ptr, len);
|
||
|
ptr += len;
|
||
|
}
|
||
|
|
||
|
/* step 7: validate device identification field 0x87 and 0x88 */
|
||
|
for (i = 0; i < UPS2000_IDENT_MAX_FIELDS; i++) {
|
||
|
/* only one device is supported */
|
||
|
if (ups2000_ident[i].type == 0x87) {
|
||
|
/* so we assume 0x87 must be 1 */
|
||
|
ups_count =
|
||
|
(uint32_t)(ups2000_ident[i].val[0]) << 24 |
|
||
|
(uint32_t)(ups2000_ident[i].val[1]) << 16 |
|
||
|
(uint32_t)(ups2000_ident[i].val[2]) << 8 |
|
||
|
(uint32_t)(ups2000_ident[i].val[3]);
|
||
|
}
|
||
|
if (ups2000_ident[i].type == 0x88) {
|
||
|
/*
|
||
|
* And only check 0x88, not 0x89, etc. Also copy the
|
||
|
* string for later parsing via strtok().
|
||
|
*/
|
||
|
ups2000_ident_desc = strdup((char *) ups2000_ident[i].val);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if (ups_count != 1)
|
||
|
fatalx(EXIT_FAILURE, "only 1 UPS is supported, %u found", ups_count);
|
||
|
|
||
|
if (!ups2000_ident_desc)
|
||
|
fatalx(EXIT_FAILURE, "device desc string not found");
|
||
|
|
||
|
/*
|
||
|
* step 8: extract fields from the desc string.
|
||
|
* (1=UPS2000;2=V100R001C01SPC120;3=...)
|
||
|
*/
|
||
|
for (i = 0; i < UPS2000_DESC_MAX_FIELDS; i++) {
|
||
|
char *key; /* "1", "2", "3", ... */
|
||
|
char *val; /* "UPS2000", "V100R001C01SPC120", ... */
|
||
|
unsigned int idx = 0;
|
||
|
|
||
|
if (i == 0)
|
||
|
key = strtok(ups2000_ident_desc, "=");
|
||
|
else
|
||
|
key = strtok(NULL, "=");
|
||
|
if (!key)
|
||
|
break;
|
||
|
|
||
|
val = strtok(NULL, ";");
|
||
|
if (!val)
|
||
|
break;
|
||
|
|
||
|
r = str_to_uint_strict(key, &idx, 10);
|
||
|
if (!r || idx + 1 > UPS2000_DESC_MAX_FIELDS || idx < 1)
|
||
|
fatalx(EXIT_FAILURE, "desc index %d is invalid!", idx);
|
||
|
|
||
|
if (strlen(val) + 1 > UPS2000_DESC_MAX_LEN)
|
||
|
fatalx(EXIT_FAILURE, "desc field %d too long!", idx);
|
||
|
|
||
|
memcpy(ups2000_desc[idx], val, strlen(val) + 1);
|
||
|
}
|
||
|
free(ups2000_ident_desc);
|
||
|
|
||
|
/*
|
||
|
* step 9: Validate desc fields that we are going to use are valid.
|
||
|
*
|
||
|
* Note: UPS2000_DESC_DEVICE_ID and UPS2000_DESC_PARALLEL_ID are
|
||
|
* currently unused and unchecked.
|
||
|
*/
|
||
|
for (i = UPS2000_DESC_MODEL; i <= UPS2000_DESC_ESN; i++) {
|
||
|
if (strlen(ups2000_desc[i]) == 0)
|
||
|
fatalx(EXIT_FAILURE, "desc field %d is missing!", i);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void upsdrv_initinfo(void)
|
||
|
{
|
||
|
bool in_list = 0;
|
||
|
int i = 0;
|
||
|
|
||
|
upsdebugx(2, "upsdrv_initinfo");
|
||
|
|
||
|
ups2000_device_identification();
|
||
|
|
||
|
/* check whether the UPS is a known model */
|
||
|
for (i = 0; supported_model[i] != NULL; i++) {
|
||
|
if (!strcmp(supported_model[i],
|
||
|
ups2000_desc[UPS2000_DESC_MODEL])) {
|
||
|
in_list = 1;
|
||
|
}
|
||
|
}
|
||
|
if (!in_list) {
|
||
|
fatalx(EXIT_FAILURE, "Unknown UPS model %s",
|
||
|
ups2000_desc[UPS2000_DESC_MODEL]);
|
||
|
}
|
||
|
|
||
|
dstate_setinfo("device.mfr", "Huawei");
|
||
|
dstate_setinfo("device.type", "ups");
|
||
|
dstate_setinfo("device.model", "%s",
|
||
|
ups2000_desc[UPS2000_DESC_MODEL]);
|
||
|
dstate_setinfo("device.serial", "%s",
|
||
|
ups2000_desc[UPS2000_DESC_ESN]);
|
||
|
|
||
|
dstate_setinfo("ups.mfr", "Huawei");
|
||
|
dstate_setinfo("ups.model", "%s",
|
||
|
ups2000_desc[UPS2000_DESC_MODEL]);
|
||
|
dstate_setinfo("ups.firmware", "%s",
|
||
|
ups2000_desc[UPS2000_DESC_FIRMWARE_REV]);
|
||
|
dstate_setinfo("ups.firmware.aux", "%s",
|
||
|
ups2000_desc[UPS2000_DESC_PROTOCOL_REV]);
|
||
|
dstate_setinfo("ups.serial", "%s",
|
||
|
ups2000_desc[UPS2000_DESC_ESN]);
|
||
|
dstate_setinfo("ups.type", "online");
|
||
|
|
||
|
/* RW variables */
|
||
|
upsh.setvar = setvar;
|
||
|
|
||
|
/* instant commands */
|
||
|
ups2000_init_instcmd();
|
||
|
upsh.instcmd = instcmd;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
* All registers are uint16_t. But the data they represent can
|
||
|
* be either an integer or a float. This information is used for
|
||
|
* error checking (int and float have different invalid values).
|
||
|
*/
|
||
|
enum {
|
||
|
REG_UINT16,
|
||
|
REG_UINT32, /* occupies two registers */
|
||
|
REG_FLOAT, /* actually a misnomer, it should really be called
|
||
|
fixed-point number, but we follow the datasheet */
|
||
|
};
|
||
|
#define REG_UINT16_INVALID 0xFFFFU
|
||
|
#define REG_UINT32_INVALID 0xFFFFFFFFU
|
||
|
#define REG_FLOAT_INVALID 0x7FFFU
|
||
|
|
||
|
|
||
|
/*
|
||
|
* Declare UPS attribute variables, format strings, registers,
|
||
|
* and their scaling factors in a lookup table to avoid spaghetti
|
||
|
* code.
|
||
|
*/
|
||
|
static struct {
|
||
|
const char *name;
|
||
|
const char *fmt;
|
||
|
const uint16_t reg;
|
||
|
const int datatype; /* only UINT32 occupies 2 regs */
|
||
|
const float scaling; /* scale it down to get the original */
|
||
|
} ups2000_var[] =
|
||
|
{
|
||
|
{ "input.voltage", "%03.1f", 1000, REG_FLOAT, 10.0 },
|
||
|
{ "input.frequency", "%02.1f", 1003, REG_FLOAT, 10.0 },
|
||
|
{ "input.bypass.voltage", "%03.1f", 1004, REG_FLOAT, 10.0 },
|
||
|
{ "input.bypass.frequency", "%03.1f", 1007, REG_FLOAT, 10.0 },
|
||
|
{ "output.voltage", "%03.1f", 1008, REG_FLOAT, 10.0 },
|
||
|
{ "output.current", "%03.1f", 1011, REG_FLOAT, 10.0 },
|
||
|
{ "output.frequency", "%03.1f", 1014, REG_FLOAT, 10.0 },
|
||
|
{ "output.realpower", "%02.1f", 1015, REG_FLOAT, 0.01 }, /* 10 / 1 kW */
|
||
|
{ "output.power", "%03.1f", 1018, REG_FLOAT, 0.01 }, /* 10 / 1 kVA */
|
||
|
{ "ups.load", "%02.1f", 1021, REG_FLOAT, 10.0 },
|
||
|
{ "ups.temperature", "%02.1f", 1027, REG_FLOAT, 10.0 },
|
||
|
{ "battery.voltage", "%02.1f", 2000, REG_FLOAT, 10.0 },
|
||
|
{ "battery.charge", "%02.1f", 2003, REG_UINT16, 1.0 },
|
||
|
{ "battery.runtime", "%.0f", 2004, REG_UINT32, 1.0 },
|
||
|
{ "battery.packs", "%.0f", 2007, REG_UINT16, 1.0 },
|
||
|
{ "battery.capacity", "%.0f", 2033, REG_UINT16, 1.0 },
|
||
|
{ "ups.power.nominal", "%.0f", 9009, REG_FLOAT, 0.01 }, /* 10 / 1 kVA */
|
||
|
{ NULL, NULL, 0, 0, 0 },
|
||
|
};
|
||
|
|
||
|
|
||
|
static int ups2000_update_info(void)
|
||
|
{
|
||
|
uint16_t reg[3][34];
|
||
|
int i;
|
||
|
int r;
|
||
|
|
||
|
upsdebugx(2, "ups2000_update_info");
|
||
|
|
||
|
/*
|
||
|
* All status registers have an offset of 10000 * ups_number.
|
||
|
* We only support 1 UPS, thus it's always 10000. Register
|
||
|
* 1000 becomes 11000.
|
||
|
*/
|
||
|
r = ups2000_read_registers(modbus_ctx, 11000, 28, reg[0]);
|
||
|
if (r != 28)
|
||
|
return 1;
|
||
|
|
||
|
r = ups2000_read_registers(modbus_ctx, 12000, 34, reg[1]);
|
||
|
if (r != 34)
|
||
|
return 1;
|
||
|
|
||
|
r = ups2000_read_registers(modbus_ctx, 19009, 1, ®[2][9]);
|
||
|
if (r != 1)
|
||
|
return 1;
|
||
|
|
||
|
for (i = 0; ups2000_var[i].name != NULL; i++) {
|
||
|
uint16_t reg_id = ups2000_var[i].reg;
|
||
|
uint8_t page = (uint8_t)(reg_id / 1000 - 1);
|
||
|
uint8_t idx = (uint8_t)(reg_id % 1000);
|
||
|
uint32_t val;
|
||
|
bool invalid = 0;
|
||
|
|
||
|
if (page == 8) /* hack for the lonely register 9009 */
|
||
|
page = 2;
|
||
|
|
||
|
if (page > 2 || idx > 33) /* also suppress compiler warn */
|
||
|
fatalx(EXIT_FAILURE, "register calculation overflow!\n");
|
||
|
|
||
|
switch (ups2000_var[i].datatype) {
|
||
|
case REG_FLOAT:
|
||
|
val = reg[page][idx];
|
||
|
if (val == REG_FLOAT_INVALID)
|
||
|
invalid = 1;
|
||
|
break;
|
||
|
case REG_UINT16:
|
||
|
val = reg[page][idx];
|
||
|
if (val == REG_UINT16_INVALID)
|
||
|
invalid = 1;
|
||
|
break;
|
||
|
case REG_UINT32:
|
||
|
val = (uint32_t)(reg[page][idx]) << 16;
|
||
|
val |= (uint32_t)(reg[page][idx + 1]);
|
||
|
if (val == REG_UINT32_INVALID)
|
||
|
invalid = 1;
|
||
|
break;
|
||
|
default:
|
||
|
fatalx(EXIT_FAILURE, "invalid data type in register table!\n");
|
||
|
}
|
||
|
|
||
|
if (invalid) {
|
||
|
upslogx(LOG_ERR, "register %04d has invalid value %04x,", reg_id, val);
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL
|
||
|
#pragma GCC diagnostic push
|
||
|
#endif
|
||
|
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL
|
||
|
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
|
||
|
#endif
|
||
|
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_FORMAT_SECURITY
|
||
|
#pragma GCC diagnostic ignored "-Wformat-security"
|
||
|
#endif
|
||
|
dstate_setinfo(ups2000_var[i].name, ups2000_var[i].fmt,
|
||
|
(float) val / ups2000_var[i].scaling);
|
||
|
#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL
|
||
|
#pragma GCC diagnostic pop
|
||
|
#endif
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
* A lookup table of all the status registers and the list of
|
||
|
* corresponding flags they represent. A register may set multiple
|
||
|
* status flags, represented by an array of flags_t.
|
||
|
*
|
||
|
* There are two types of flags. If the flag is a "status flag"
|
||
|
* for status_set(), for example, "OL" or "OB", the field
|
||
|
* "status_name" is used. If the flag is a "data variable" for
|
||
|
* dstate_setinfo(), the variable name and value is written in
|
||
|
* "var_name" and "var_val" fields.
|
||
|
*
|
||
|
* For each flag, if it's indicated by a specific value in a
|
||
|
* register, the "val" field is used. If a flag is indicated by
|
||
|
* a bit, the "bit" field should be used. Fields "val" and "bit"
|
||
|
* cannot be used at the same time, at least one must be "-1".
|
||
|
*
|
||
|
* Also, some important registers indicate basic system status
|
||
|
* (e.g. whether the UPS is on line power or battery), this info
|
||
|
* must always be available, and they are always expected to set
|
||
|
* at least one flag. If the important register does not set any
|
||
|
* flag, it means we've received an invalid or unknown value,
|
||
|
* and we must report an error. The "must_set_flag" field is used
|
||
|
* for this purpose.
|
||
|
*/
|
||
|
static struct {
|
||
|
const uint16_t reg;
|
||
|
bool must_set_flag;
|
||
|
struct flags_t {
|
||
|
const char *status_name;
|
||
|
const int16_t val;
|
||
|
const int bit;
|
||
|
const char *var_name, *var_val;
|
||
|
} flags[10];
|
||
|
} ups2000_status_reg[] =
|
||
|
{
|
||
|
{ 1024, 1, {
|
||
|
{ "OFF", 0, -1, NULL, NULL },
|
||
|
{ "BYPASS", 1, -1, NULL, NULL },
|
||
|
{ "OL", 2, -1, NULL, NULL },
|
||
|
{ "OB", 3, -1, NULL, NULL },
|
||
|
{ "OL ECO", 5, -1, NULL, NULL },
|
||
|
{ NULL, -1, -1, NULL, NULL },
|
||
|
}},
|
||
|
{ 1043, 0, {
|
||
|
{ "CAL", -1, 2, NULL, NULL }, /* battery self-test */
|
||
|
{ "LB", -1, 6, NULL, NULL },
|
||
|
{ NULL, -1, -1, NULL, NULL },
|
||
|
}},
|
||
|
/*
|
||
|
* Note: 3 = float charging, 4 = equalization charging, but
|
||
|
* both of them are reported as "charging", not "floating".
|
||
|
* The definition of "floating" in NUT is: "battery has
|
||
|
* completed its charge cycle, and waiting to go to resting
|
||
|
* mode", which is not true for UPS2000.
|
||
|
*/
|
||
|
{ 2002, 1, {
|
||
|
{ "", 2, -1, "battery.charger.status", "resting" },
|
||
|
{ "CHRG", 3, -1, "battery.charger.status", "charging" },
|
||
|
{ "CHRG", 4, -1, "battery.charger.status", "charging" },
|
||
|
{ "DISCHRG", 5, -1, "battery.charger.status", "discharging" },
|
||
|
{ NULL, -1, -1, NULL, NULL },
|
||
|
}},
|
||
|
{ 0, 0, { { NULL, -1, -1, NULL, NULL } } }
|
||
|
};
|
||
|
|
||
|
|
||
|
static int ups2000_update_status(void)
|
||
|
{
|
||
|
int i, j;
|
||
|
int r;
|
||
|
|
||
|
upsdebugx(2, "ups2000_update_status");
|
||
|
|
||
|
for (i = 0; ups2000_status_reg[i].reg != 0; i++) {
|
||
|
uint16_t reg, val;
|
||
|
struct flags_t *flag;
|
||
|
int flag_count = 0;
|
||
|
|
||
|
reg = ups2000_status_reg[i].reg;
|
||
|
r = ups2000_read_registers(modbus_ctx, reg + 10000, 1, &val);
|
||
|
if (r != 1)
|
||
|
return 1;
|
||
|
|
||
|
if (val == REG_UINT16_INVALID) {
|
||
|
upslogx(LOG_ERR, "register %04d has invalid value %04x,", reg, val);
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
flag = ups2000_status_reg[i].flags;
|
||
|
for (j = 0; flag[j].status_name != NULL; j++) {
|
||
|
/*
|
||
|
* if the register is equal to the "val" we are looking
|
||
|
* for, or if register has its n-th "bit" set...
|
||
|
*/
|
||
|
if ((flag[j].val != -1 && flag[j].val == val) ||
|
||
|
(flag[j].bit != -1 && CHECK_BIT(val, flag[j].bit))) {
|
||
|
/* if it has a corresponding status flag */
|
||
|
if (strlen(flag[j].status_name) != 0)
|
||
|
status_set(flag[j].status_name);
|
||
|
/* or if it has a corresponding dstate variable (or both) */
|
||
|
if (flag[j].var_name && flag[j].var_val)
|
||
|
dstate_setinfo(flag[j].var_name, "%s", flag[j].var_val);
|
||
|
flag_count++;
|
||
|
}
|
||
|
}
|
||
|
if (ups2000_status_reg[i].must_set_flag && flag_count == 0) {
|
||
|
upslogx(LOG_ERR, "register %04d has invalid value %04x,", reg, val);
|
||
|
return 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
* A lookup table of all the alarm registers and the list of
|
||
|
* corresponding alarms they represent. Each alarm condition is
|
||
|
* listed by its register base address "reg" and its "bit"
|
||
|
* position.
|
||
|
*
|
||
|
* Each alarm condition has an "alarm_id", "alarm_cause_id",
|
||
|
* and "alarm_name". In addition, a few alarms conditions also
|
||
|
* indicates conditions related to batteries that is needed to
|
||
|
* be set via status_set(), those are listed in "status_name".
|
||
|
* Unused "status_name" is set to NULL.
|
||
|
*
|
||
|
* After an alarm is reported/cleared by the UPS, the "active"
|
||
|
* flag is changed to reflect its status. The error logging
|
||
|
* code uses this variable to issue warnings only when needed
|
||
|
* (i.e. only after a change, avoid issuing the same warning
|
||
|
* repeatedly).
|
||
|
*/
|
||
|
#define ALARM_CLEAR_AUTO 1
|
||
|
#define ALARM_CLEAR_MANUAL 2
|
||
|
#define ALARM_CLEAR_DEPENDING 3
|
||
|
|
||
|
static struct {
|
||
|
bool active; /* runtime: is this alarm currently active? */
|
||
|
const uint16_t reg; /* alarm register to check */
|
||
|
const int bit; /* alarm bit to check */
|
||
|
const int alarm_clear; /* auto or manual clear */
|
||
|
const int loglevel; /* warning or error */
|
||
|
const int alarm_id, alarm_cause_id;
|
||
|
const char *status_name; /* corresponding NUT status word */
|
||
|
const char *alarm_name; /* alarm string */
|
||
|
const char *alarm_desc; /* brief explanation */
|
||
|
} ups2000_alarm[] =
|
||
|
{
|
||
|
{
|
||
|
false, 40156, 3, ALARM_CLEAR_AUTO, LOG_ALERT,
|
||
|
30, 1, NULL, "UPS internal overtemperature",
|
||
|
"The ambient temperature is over 50-degree C. "
|
||
|
"Startup from standby mode is prohibited.",
|
||
|
},
|
||
|
{
|
||
|
false, 40161, 1, ALARM_CLEAR_AUTO, LOG_WARNING,
|
||
|
10, 1, NULL, "Abnormal bypass voltage",
|
||
|
"Bypass input is unavailable or out-of-range. Wait for "
|
||
|
"bypass input to recover, or change acceptable bypass "
|
||
|
"range via front panel.",
|
||
|
},
|
||
|
{
|
||
|
false, 40161, 2, ALARM_CLEAR_AUTO, LOG_WARNING,
|
||
|
10, 2, NULL, "Abnormal bypass frequency",
|
||
|
"Bypass input is unavailable or out-of-range. Wait for "
|
||
|
"bypass input to recover, or change acceptable bypass "
|
||
|
"range via front panel.",
|
||
|
},
|
||
|
{
|
||
|
false, 40163, 3, ALARM_CLEAR_DEPENDING, LOG_WARNING,
|
||
|
25, 1, NULL, "Battery overvoltage",
|
||
|
"When the UPS is started, voltage of each battery exceeds 15 V. "
|
||
|
"Or: current battery voltage exceeds 14.7 V.",
|
||
|
},
|
||
|
{
|
||
|
false, 40164, 1, ALARM_CLEAR_AUTO, LOG_WARNING,
|
||
|
29, 1, "RB", "Battery needs maintenance",
|
||
|
"During the last battery self-check, the battery voltage "
|
||
|
"was lower than the replacement threshold (11 V).",
|
||
|
},
|
||
|
{
|
||
|
false, 40164, 3, ALARM_CLEAR_AUTO, LOG_WARNING,
|
||
|
26, 1, NULL, "Battery undervoltage",
|
||
|
NULL,
|
||
|
},
|
||
|
{
|
||
|
false, 40170, 4, ALARM_CLEAR_AUTO, LOG_ALERT,
|
||
|
22, 1, NULL, "Battery disconnected",
|
||
|
"Battery is not connected, has loose connection, or faulty.",
|
||
|
},
|
||
|
{
|
||
|
false, 40173, 5, ALARM_CLEAR_AUTO, LOG_ALERT,
|
||
|
66, 1, "OVER", "Output overload (105%-110%)",
|
||
|
"UPS will shut down or transfer to bypass mode in 5-10 minutes.",
|
||
|
},
|
||
|
{
|
||
|
false, 40173, 3, ALARM_CLEAR_AUTO, LOG_ALERT,
|
||
|
66, 2, "OVER", "Output overload (110%-130%)",
|
||
|
"UPS will shut down or transfer to bypass mode in 30-60 seconds.",
|
||
|
},
|
||
|
{
|
||
|
false, 40174, 0, ALARM_CLEAR_DEPENDING, LOG_ALERT,
|
||
|
14, 1, NULL, "UPS startup timeout",
|
||
|
"The inverter output voltage is not within +/- 2 V of the "
|
||
|
"rated output. Or: battery is overdischarged.",
|
||
|
},
|
||
|
{
|
||
|
false, 40179, 14, ALARM_CLEAR_MANUAL, LOG_ALERT,
|
||
|
42, 15, NULL, "Rectifier fault (internal fault)",
|
||
|
"Bus voltage is lower than 320 V.",
|
||
|
},
|
||
|
{
|
||
|
false, 40179, 15, ALARM_CLEAR_MANUAL, LOG_ALERT,
|
||
|
42, 17, NULL, "Rectifier fault (internal fault)",
|
||
|
"Bus voltage is higher than 450 V.",
|
||
|
},
|
||
|
{
|
||
|
false, 40180, 1, ALARM_CLEAR_MANUAL, LOG_ALERT,
|
||
|
42, 18, NULL, "Rectifier fault (internal fault)",
|
||
|
"Bus voltage is lower than 260 V.",
|
||
|
},
|
||
|
{
|
||
|
false, 40180, 5, ALARM_CLEAR_AUTO, LOG_ALERT,
|
||
|
42, 24, NULL, "EEPROM fault (internal fault)",
|
||
|
"Faulty EEPROM. All settings are restored to "
|
||
|
"factory default and cannot be saved.",
|
||
|
},
|
||
|
{
|
||
|
false, 40180, 6, ALARM_CLEAR_MANUAL, LOG_ALERT,
|
||
|
42, 27, NULL, "Inverter fault (internal fault)",
|
||
|
"Inverter output overvoltage, undervoltage or "
|
||
|
"undercurrent.",
|
||
|
},
|
||
|
{
|
||
|
false, 40180, 7, ALARM_CLEAR_DEPENDING, LOG_ALERT,
|
||
|
42, 28, NULL, "Inverter fault (internal fault)",
|
||
|
"The inverter output voltage is lower than 100 V.",
|
||
|
},
|
||
|
{
|
||
|
false, 40180, 10, ALARM_CLEAR_MANUAL, LOG_ALERT,
|
||
|
42, 31, NULL, "Inverter fault (internal fault)",
|
||
|
"The difference between the absolute value of the positive bus "
|
||
|
"voltage and that of the negative bus voltage is 100 V.",
|
||
|
},
|
||
|
{
|
||
|
false, 40180, 11, ALARM_CLEAR_DEPENDING, LOG_ALERT,
|
||
|
42, 32, NULL, "UPS internal overtemperature",
|
||
|
"The ambient temperature is over 50 degree C, "
|
||
|
"switching to bypass mode.",
|
||
|
|
||
|
},
|
||
|
{
|
||
|
false, 40180, 13, ALARM_CLEAR_MANUAL, LOG_ALERT,
|
||
|
42, 36, NULL, "Charger fault (internal fault)",
|
||
|
"The charger has no output. Faulty internal connections.",
|
||
|
|
||
|
},
|
||
|
{
|
||
|
false, 40182, 4, ALARM_CLEAR_MANUAL, LOG_ALERT,
|
||
|
42, 42, NULL, "Charger fault (internal fault)",
|
||
|
"The charger has no output while the inverter is on, "
|
||
|
"battery undervoltage. Faulty switching transistor.",
|
||
|
},
|
||
|
{
|
||
|
false, 40182, 13, ALARM_CLEAR_MANUAL, LOG_ALERT,
|
||
|
66, 3, "OVER", "Output overload shutdown",
|
||
|
"UPS has shutdown or transferred to bypass mode.",
|
||
|
},
|
||
|
{
|
||
|
false, 40182, 14, ALARM_CLEAR_MANUAL, LOG_ALERT,
|
||
|
66, 4, "OVER", "Bypass output overload shutdown",
|
||
|
"UPS has shutdown, bypass output was overload and exceeded "
|
||
|
"time limit.",
|
||
|
},
|
||
|
{ false, 0, -1, -1, -1, -1, -1, NULL, NULL, NULL }
|
||
|
};
|
||
|
|
||
|
|
||
|
/* don't spam the syslog */
|
||
|
static time_t alarm_logged_since = 0;
|
||
|
#define UPS2000_LOG_INTERVAL 600 /* 10 minutes */
|
||
|
|
||
|
|
||
|
static int ups2000_update_alarm(void)
|
||
|
{
|
||
|
uint16_t val[27];
|
||
|
int i;
|
||
|
int r;
|
||
|
|
||
|
char alarm_buf[128];
|
||
|
size_t all_alarms_len = 0;
|
||
|
|
||
|
int alarm_count = 0;
|
||
|
bool alarm_logged = 0;
|
||
|
bool alarm_rtfm = 0;
|
||
|
time_t now = time(NULL);
|
||
|
|
||
|
upsdebugx(2, "ups2000_update_alarm");
|
||
|
|
||
|
/*
|
||
|
* All alarm registers have an offset of 1024 * ups_number.
|
||
|
* We only support 1 UPS, it's always 1024.
|
||
|
*/
|
||
|
r = ups2000_read_registers(modbus_ctx, ups2000_alarm[0].reg + 1024, 27, val);
|
||
|
if (r != 27)
|
||
|
return 1;
|
||
|
|
||
|
bypass_available = 1; /* register 40161 hack, see comments below */
|
||
|
|
||
|
for (i = 0; ups2000_alarm[i].alarm_id != -1; i++) {
|
||
|
int idx = ups2000_alarm[i].reg - ups2000_alarm[0].reg;
|
||
|
if (idx > 26 || idx < 0)
|
||
|
fatalx(EXIT_FAILURE, "register calculation overflow!\n");
|
||
|
|
||
|
if (CHECK_BIT(val[idx], ups2000_alarm[i].bit)) {
|
||
|
int gotlen;
|
||
|
if (ups2000_alarm[i].reg == 40161)
|
||
|
/*
|
||
|
* HACK: special treatment for register 40161. If this
|
||
|
* register indicates an alarm, we need to lock the
|
||
|
* "bypass.on" command as a software foolproof mechanism.
|
||
|
* It's written to the global "bypass_available" flag.
|
||
|
*/
|
||
|
bypass_available = 0;
|
||
|
|
||
|
alarm_count++;
|
||
|
|
||
|
gotlen = snprintf(alarm_buf, 128, "(ID %02d/%02d): %s!",
|
||
|
ups2000_alarm[i].alarm_id,
|
||
|
ups2000_alarm[i].alarm_cause_id,
|
||
|
ups2000_alarm[i].alarm_name);
|
||
|
|
||
|
if (gotlen < 0 || (uintmax_t)gotlen > SIZE_MAX) {
|
||
|
fatalx(EXIT_FAILURE, "alarm_buf preparation over/under-flow!\n");
|
||
|
}
|
||
|
|
||
|
all_alarms_len += (size_t)gotlen;
|
||
|
alarm_set(alarm_buf);
|
||
|
|
||
|
if (ups2000_alarm[i].status_name)
|
||
|
status_set(ups2000_alarm[i].status_name);
|
||
|
|
||
|
/*
|
||
|
* Log the warning only if it's a new alarm, or if a long time
|
||
|
* has paseed since we first warned it.
|
||
|
*/
|
||
|
if (!ups2000_alarm[i].active ||
|
||
|
difftime(now, alarm_logged_since) >= UPS2000_LOG_INTERVAL) {
|
||
|
int loglevel;
|
||
|
const char *alarm_word;
|
||
|
|
||
|
/*
|
||
|
* Most text editors have syntax highlighting, adding an
|
||
|
* alarm word makes the log more readable
|
||
|
*/
|
||
|
loglevel = ups2000_alarm[i].loglevel;
|
||
|
if (loglevel <= LOG_ERR) {
|
||
|
alarm_word = "ERROR";
|
||
|
/*
|
||
|
* If at least one error is serious, suggest reading
|
||
|
* manual.
|
||
|
*/
|
||
|
alarm_rtfm = 1;
|
||
|
}
|
||
|
else {
|
||
|
alarm_word = "WARNING";
|
||
|
}
|
||
|
|
||
|
upslogx(loglevel, "%s: alarm %02d, Cause %02d: %s!",
|
||
|
alarm_word,
|
||
|
ups2000_alarm[i].alarm_id,
|
||
|
ups2000_alarm[i].alarm_cause_id,
|
||
|
ups2000_alarm[i].alarm_name);
|
||
|
|
||
|
if (ups2000_alarm[i].alarm_desc)
|
||
|
upslogx(loglevel, "%s", ups2000_alarm[i].alarm_desc);
|
||
|
|
||
|
switch (ups2000_alarm[i].alarm_clear) {
|
||
|
case ALARM_CLEAR_AUTO:
|
||
|
upslogx(loglevel, "This alarm can be auto cleared.");
|
||
|
break;
|
||
|
case ALARM_CLEAR_MANUAL:
|
||
|
upslogx(loglevel, "This alarm can only be manual cleared "
|
||
|
"via front panel.");
|
||
|
break;
|
||
|
case ALARM_CLEAR_DEPENDING:
|
||
|
upslogx(loglevel, "This alarm is auto or manual cleared "
|
||
|
"depending on the specific problem.");
|
||
|
}
|
||
|
|
||
|
ups2000_alarm[i].active = 1;
|
||
|
alarm_logged = 1;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
else {
|
||
|
if (ups2000_alarm[i].active) {
|
||
|
upslogx(LOG_WARNING, "Cleared alarm %02d, Cause %02d: %s",
|
||
|
ups2000_alarm[i].alarm_id,
|
||
|
ups2000_alarm[i].alarm_cause_id,
|
||
|
ups2000_alarm[i].alarm_name);
|
||
|
ups2000_alarm[i].active = 0;
|
||
|
alarm_logged = 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
if (alarm_count > 0) {
|
||
|
/* append this to the alarm string as a friendly reminder */
|
||
|
int gotlen = snprintf(alarm_buf, 128, "Check log for details!");
|
||
|
|
||
|
if (gotlen < 0 || (uintmax_t)gotlen > SIZE_MAX) {
|
||
|
fatalx(EXIT_FAILURE, "alarm_buf preparation over/under-flow!\n");
|
||
|
}
|
||
|
|
||
|
all_alarms_len += (size_t)gotlen;
|
||
|
alarm_set(alarm_buf);
|
||
|
|
||
|
/* if the alarm string is too long, replace it with this */
|
||
|
if (all_alarms_len + 1 > ST_MAX_VALUE_LEN) {
|
||
|
alarm_init(); /* discard all original alarms */
|
||
|
snprintf(alarm_buf, 128, "UPS has %d alarms in effect, "
|
||
|
"check log for details!", alarm_count);
|
||
|
alarm_set(alarm_buf);
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* If we are doing a syslog, write the final message and refresh the
|
||
|
* do-not-spam-the-log timer "alarm_logged_since".
|
||
|
*/
|
||
|
if (alarm_logged) {
|
||
|
upslogx(LOG_WARNING, "UPS has %d alarms in effect.", alarm_count);
|
||
|
if (alarm_rtfm)
|
||
|
upslogx(LOG_WARNING, "Read Huawei User Manual for "
|
||
|
"troubleshooting information.");
|
||
|
alarm_logged_since = time(NULL);
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
upsdebugx(2, "UPS has 0 alarms in effect.");
|
||
|
|
||
|
if (alarm_logged) {
|
||
|
upslogx(LOG_WARNING, "UPS has cleared all alarms.");
|
||
|
alarm_logged_since = time(NULL);
|
||
|
}
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
void upsdrv_updateinfo(void)
|
||
|
{
|
||
|
int err = 0;
|
||
|
|
||
|
upsdebugx(2, "upsdrv_updateinfo");
|
||
|
status_init();
|
||
|
alarm_init();
|
||
|
|
||
|
err += ups2000_update_timers();
|
||
|
err += ups2000_update_alarm();
|
||
|
err += ups2000_update_info();
|
||
|
err += ups2000_update_status();
|
||
|
err += ups2000_update_rw_var();
|
||
|
|
||
|
if (err > 0) {
|
||
|
upsdebugx(2, "upsdrv_updateinfo failed, data stale.");
|
||
|
dstate_datastale();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
alarm_commit();
|
||
|
status_commit();
|
||
|
dstate_dataok();
|
||
|
upsdebugx(2, "upsdrv_updateinfo done");
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
* A lookup table of simple RW (configurable) variable "name", and their
|
||
|
* "getter" and "setter". A "getter" function reads the variable from
|
||
|
* the UPS, and a "setter" overwrites it.
|
||
|
*
|
||
|
* This struct only handles simple variables, delays are handled in another
|
||
|
* table.
|
||
|
*/
|
||
|
static struct {
|
||
|
const char *name;
|
||
|
const uint16_t reg;
|
||
|
int (*const getter)(const uint16_t);
|
||
|
int (*const setter)(const uint16_t, const char *);
|
||
|
} ups2000_rw_var[] =
|
||
|
{
|
||
|
{ "ups.start.auto", 1044, ups2000_autostart_get, ups2000_autostart_set },
|
||
|
{ "ups.beeper.status", 1046, ups2000_beeper_get, ups2000_beeper_set },
|
||
|
{ NULL, 0, NULL, NULL },
|
||
|
};
|
||
|
|
||
|
|
||
|
/*
|
||
|
* A specialized lookup table of startup, reboot and shutdown delays,
|
||
|
* represented by RW variables.
|
||
|
*/
|
||
|
static struct ups2000_delay_t {
|
||
|
const char *name; /* RW variable name */
|
||
|
uint16_t *const global_var; /* its corresponding global variable */
|
||
|
const char *varname_cmdline; /* cmdline argument passed to us */
|
||
|
const uint16_t min; /* minimum value allowed (seconds) */
|
||
|
const uint16_t max; /* maximum value allowed (seconds) */
|
||
|
const uint8_t step; /* can only be set in discrete steps */
|
||
|
const uint16_t dfault; /* default value */
|
||
|
} ups2000_rw_delay[] =
|
||
|
{
|
||
|
/* 5940 = 99 min. */
|
||
|
{ "ups.delay.shutdown", &ups2000_offdelay, "offdelay", 6, 5940, 6, 60 },
|
||
|
{ "ups.delay.reboot", &ups2000_rebootdelay, "rebootdelay", 6, 5940, 6, 60 },
|
||
|
{ "ups.delay.start", &ups2000_ondelay, "ondelay", 60, 5940, 60, 60 },
|
||
|
{ NULL, NULL, NULL, 0, 0, 0, 0 },
|
||
|
};
|
||
|
enum {
|
||
|
SHUTDOWN,
|
||
|
REBOOT,
|
||
|
START
|
||
|
};
|
||
|
|
||
|
|
||
|
static int ups2000_update_rw_var(void)
|
||
|
{
|
||
|
int i;
|
||
|
int r;
|
||
|
|
||
|
upsdebugx(2, "ups2000_update_rw_var");
|
||
|
|
||
|
for (i = 0; ups2000_rw_var[i].name != NULL; i++) {
|
||
|
r = ups2000_rw_var[i].getter(ups2000_rw_var[i].reg);
|
||
|
if (r != 0)
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
ups2000_delay_get();
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
static int setvar(const char *name, const char *val)
|
||
|
{
|
||
|
int i;
|
||
|
int r;
|
||
|
|
||
|
for (i = 0; ups2000_rw_var[i].name != NULL; i++) {
|
||
|
if (!strcasecmp(ups2000_rw_var[i].name, name)) {
|
||
|
r = ups2000_rw_var[i].setter(ups2000_rw_var[i].reg, val);
|
||
|
goto found;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for (i = 0; ups2000_rw_delay[i].name != NULL; i++) {
|
||
|
if (!strcasecmp(ups2000_rw_delay[i].name, name)) {
|
||
|
r = ups2000_rw_var[i].setter(ups2000_rw_var[i].reg, val);
|
||
|
goto found;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return STAT_SET_UNKNOWN;
|
||
|
|
||
|
found:
|
||
|
if (r == STAT_SET_FAILED)
|
||
|
upslogx(LOG_ERR, "setvar: setting variable [%s] to [%s] failed", name, val);
|
||
|
else if (r == STAT_SET_INVALID)
|
||
|
upslogx(LOG_WARNING, "setvar: [%s] is not valid for variable [%s]", val, name);
|
||
|
return r;
|
||
|
}
|
||
|
|
||
|
|
||
|
static int ups2000_autostart_get(const uint16_t reg)
|
||
|
{
|
||
|
/*
|
||
|
* "ups.start.auto" is not supported because it overcomplicates
|
||
|
* the logic. The driver changes "ups.start.auto" internally to
|
||
|
* allow shutdown and reboot commands to do their jobs. If we make
|
||
|
* "ups.start.auto" an user configuration, it means we must (1)
|
||
|
* watch for UPS front panel updates and apply the user setting to
|
||
|
* the driver, and (2) save the restart setting temporally before
|
||
|
* restarting, track the UPS restart process, and program the value
|
||
|
* back later.
|
||
|
*
|
||
|
* Not supporting it greatly simplifies the logic - upsdrv_shutdown
|
||
|
* always put the UPS in a restartable mode, following the standard
|
||
|
* NUT behavior. Worse is better. (To prevent user confusion, we
|
||
|
* don't even report this variable, otherwise the user may attempt
|
||
|
* to change it using the front panel.
|
||
|
*/
|
||
|
NUT_UNUSED_VARIABLE(reg);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
* Currently for internal use only, see comments above.
|
||
|
*/
|
||
|
static int ups2000_autostart_set(const uint16_t reg, const char *string)
|
||
|
{
|
||
|
uint16_t val;
|
||
|
int r;
|
||
|
|
||
|
if (!strcasecmp(string, "yes"))
|
||
|
val = 1;
|
||
|
else if (!strcasecmp(string, "no"))
|
||
|
val = 0;
|
||
|
else
|
||
|
return STAT_SET_INVALID;
|
||
|
|
||
|
r = ups2000_write_register(modbus_ctx, reg + 10000, val);
|
||
|
if (r != 1)
|
||
|
return STAT_SET_FAILED;
|
||
|
|
||
|
return STAT_SET_HANDLED;
|
||
|
}
|
||
|
|
||
|
|
||
|
static int ups2000_beeper_get(const uint16_t reg)
|
||
|
{
|
||
|
uint16_t val;
|
||
|
int r;
|
||
|
|
||
|
r = ups2000_read_registers(modbus_ctx, reg + 10000, 1, &val);
|
||
|
if (r != 1)
|
||
|
return -1;
|
||
|
|
||
|
if (val != 0 && val != 1)
|
||
|
return -1;
|
||
|
|
||
|
/*
|
||
|
* The register is "beeper disable", but we need to report whether it's
|
||
|
* enabled, thus we invert the boolean.
|
||
|
*/
|
||
|
if (val == 0)
|
||
|
dstate_setinfo("ups.beeper.status", "enabled");
|
||
|
else
|
||
|
dstate_setinfo("ups.beeper.status", "disabled");
|
||
|
|
||
|
dstate_setflags("ups.beeper.status", ST_FLAG_RW);
|
||
|
dstate_addenum("ups.beeper.status", "enabled");
|
||
|
dstate_addenum("ups.beeper.status", "disabled");
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
static int ups2000_beeper_set(const uint16_t reg, const char *string)
|
||
|
{
|
||
|
uint16_t val;
|
||
|
int r;
|
||
|
|
||
|
if (!strcasecmp(string, "disabled") || !strcasecmp(string, "muted")) {
|
||
|
/*
|
||
|
* Temporary "muted" is not supported. Only permanent "disabled"
|
||
|
* is. This is why we only support "beeper.disable" as an instant
|
||
|
* command, not "beeper.muted". But when setting it as a variable,
|
||
|
* we try to be robust here and treat both as synonyms.
|
||
|
*/
|
||
|
val = 1;
|
||
|
}
|
||
|
else if (!strcasecmp(string, "enabled"))
|
||
|
val = 0;
|
||
|
else
|
||
|
return STAT_SET_INVALID;
|
||
|
|
||
|
r = ups2000_write_register(modbus_ctx, reg + 10000, val);
|
||
|
if (r != 1)
|
||
|
return STAT_SET_FAILED;
|
||
|
|
||
|
return STAT_SET_HANDLED;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
* Note: variables "ups.delay.{shutdown,start,reboot}" are software-
|
||
|
* only variables. We only get the user settings, validate its value
|
||
|
* and store them as global variables. The actual hardware register
|
||
|
* are only programmed when a shutdown/reboot is issued.
|
||
|
*/
|
||
|
static void ups2000_delay_get(void)
|
||
|
{
|
||
|
char *cmdline;
|
||
|
int i;
|
||
|
int r;
|
||
|
|
||
|
for (i = 0; ups2000_rw_delay[i].name != NULL; i++) {
|
||
|
struct ups2000_delay_t *delay;
|
||
|
|
||
|
delay = &ups2000_rw_delay[i];
|
||
|
if (*delay->global_var == UPS2000_DELAY_INVALID) {
|
||
|
cmdline = getval(delay->varname_cmdline);
|
||
|
if (cmdline) {
|
||
|
r = ups2000_delay_set(delay->name, cmdline);
|
||
|
if (r != STAT_SET_HANDLED) {
|
||
|
upslogx(LOG_ERR, "servar: %s is invalid. "
|
||
|
"Reverting to default %s %d seconds",
|
||
|
delay->varname_cmdline,
|
||
|
delay->varname_cmdline,
|
||
|
delay->dfault);
|
||
|
*delay->global_var = delay->dfault;
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
*delay->global_var = delay->dfault;
|
||
|
upslogx(LOG_INFO, "setvar: use default %s %d seconds",
|
||
|
delay->varname_cmdline, delay->dfault);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
dstate_setinfo(delay->name, "%d", *delay->global_var);
|
||
|
dstate_setflags(delay->name, ST_FLAG_RW);
|
||
|
dstate_addrange(delay->name, delay->min, delay->max);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
static int ups2000_delay_set(const char *var, const char *string)
|
||
|
{
|
||
|
struct ups2000_delay_t *delay_schema = NULL;
|
||
|
uint16_t delay, delay_rounded;
|
||
|
int i;
|
||
|
int r;
|
||
|
|
||
|
r = str_to_ushort_strict(string, &delay, 10);
|
||
|
if (!r)
|
||
|
return STAT_SET_INVALID;
|
||
|
|
||
|
for (i = 0; ups2000_rw_delay[i].name != NULL; i++) {
|
||
|
if (!strcmp(ups2000_rw_delay[i].name, var)) {
|
||
|
delay_schema = &ups2000_rw_delay[i];
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!delay_schema)
|
||
|
return STAT_SET_UNKNOWN;
|
||
|
|
||
|
if (delay > delay_schema->max)
|
||
|
return STAT_SET_INVALID;
|
||
|
if (delay < delay_schema->min) {
|
||
|
upslogx(LOG_NOTICE, "setvar: %s [%u] is too low, "
|
||
|
"it has been set to %u seconds\n",
|
||
|
delay_schema->varname_cmdline, delay,
|
||
|
delay_schema->min);
|
||
|
delay = delay_schema->min;
|
||
|
}
|
||
|
|
||
|
if (delay % delay_schema->step != 0) {
|
||
|
delay_rounded = delay + delay_schema->step - delay % delay_schema->step;
|
||
|
upslogx(LOG_NOTICE, "setvar: %s [%u] is not a multiple of %d, "
|
||
|
"it has been rounded up to %u seconds\n",
|
||
|
delay_schema->varname_cmdline, delay,
|
||
|
delay_schema->step, delay_rounded);
|
||
|
delay = delay_rounded;
|
||
|
}
|
||
|
|
||
|
*delay_schema->global_var = delay;
|
||
|
return STAT_SET_HANDLED;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
* A lookup table of all instant commands "cmd" and their
|
||
|
* corresponding registers "reg". For each instant command,
|
||
|
* it's handled by...
|
||
|
*
|
||
|
* 1. One register write, by writing "val1" to "reg1", the
|
||
|
* simplest case.
|
||
|
*
|
||
|
* 2. Two register writes, by writing "val1" to "reg1", and
|
||
|
* writing "val2" to "reg2". One after another.
|
||
|
*
|
||
|
* 3. Calling "*handler_func" and passing "reg1". This is
|
||
|
* used to handle commands that needs additional processing.
|
||
|
* If "reg1" is not necessary or unsuitable, "-1" is used.
|
||
|
*/
|
||
|
#define REG_NONE -1, -1
|
||
|
|
||
|
static struct ups2000_cmd_t {
|
||
|
const char *cmd;
|
||
|
const int16_t reg1, val1, reg2, val2;
|
||
|
int (*const handler_func)(const uint16_t);
|
||
|
} ups2000_cmd[] =
|
||
|
{
|
||
|
{ "test.battery.start.quick", 2028, 1, REG_NONE, NULL },
|
||
|
{ "test.battery.start.deep", 2021, 1, REG_NONE, NULL },
|
||
|
{ "test.battery.stop", 2023, 1, REG_NONE, NULL },
|
||
|
{ "beeper.enable", 1046, 0, REG_NONE, NULL },
|
||
|
{ "beeper.disable", 1046, 1, REG_NONE, NULL },
|
||
|
{ "load.off", 1045, 0, 1030, 1, NULL },
|
||
|
{ "bypass.stop", 1029, 1, 1045, 0, NULL },
|
||
|
{ "load.on", 1029, -1, REG_NONE, ups2000_instcmd_load_on },
|
||
|
{ "bypass.start", REG_NONE, REG_NONE, ups2000_instcmd_bypass_start },
|
||
|
{ "beeper.toggle", 1046, -1, REG_NONE, ups2000_instcmd_beeper_toggle },
|
||
|
{ "shutdown.stayoff", 1049, -1, REG_NONE, ups2000_instcmd_shutdown_stayoff },
|
||
|
{ "shutdown.return", REG_NONE, REG_NONE, ups2000_instcmd_shutdown_return },
|
||
|
{ "shutdown.reboot", REG_NONE, REG_NONE, ups2000_instcmd_shutdown_reboot },
|
||
|
{ "shutdown.reboot.graceful", REG_NONE, REG_NONE, ups2000_instcmd_shutdown_reboot_graceful },
|
||
|
{ NULL, -1, -1, -1, -1, NULL },
|
||
|
};
|
||
|
|
||
|
|
||
|
static void ups2000_init_instcmd(void)
|
||
|
{
|
||
|
int i;
|
||
|
|
||
|
for (i = 0; ups2000_cmd[i].cmd != NULL; i++) {
|
||
|
dstate_addcmd(ups2000_cmd[i].cmd);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
static int instcmd(const char *cmd, const char *extra)
|
||
|
{
|
||
|
int i;
|
||
|
int status;
|
||
|
struct ups2000_cmd_t *cmd_action = NULL;
|
||
|
NUT_UNUSED_VARIABLE(extra);
|
||
|
|
||
|
for (i = 0; ups2000_cmd[i].cmd != NULL; i++) {
|
||
|
if (!strcasecmp(cmd, ups2000_cmd[i].cmd)) {
|
||
|
cmd_action = &ups2000_cmd[i];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!cmd_action) {
|
||
|
upslogx(LOG_WARNING, "instcmd: command [%s] unknown", cmd);
|
||
|
return STAT_INSTCMD_UNKNOWN;
|
||
|
}
|
||
|
|
||
|
if (cmd_action->handler_func) {
|
||
|
/* handled by a function */
|
||
|
if (cmd_action->reg1 < 0) {
|
||
|
upslogx(LOG_WARNING, "instcmd: command [%s] reg1 is negative", cmd);
|
||
|
return STAT_INSTCMD_UNKNOWN;
|
||
|
} else {
|
||
|
status = cmd_action->handler_func((uint16_t)cmd_action->reg1);
|
||
|
}
|
||
|
}
|
||
|
else if (cmd_action->reg1 >= 0 && cmd_action->val1 >= 0) {
|
||
|
/* handled by a register write */
|
||
|
int r = ups2000_write_register(modbus_ctx,
|
||
|
10000 + cmd_action->reg1,
|
||
|
(uint16_t)cmd_action->val1);
|
||
|
if (r == 1)
|
||
|
status = STAT_INSTCMD_HANDLED;
|
||
|
else
|
||
|
status = STAT_INSTCMD_FAILED;
|
||
|
|
||
|
/*
|
||
|
* if the previous write succeeds and there is an additional
|
||
|
* register to write.
|
||
|
*/
|
||
|
if (r == 1 && cmd_action->reg2 >= 0 && cmd_action->val2 >= 0) {
|
||
|
r = ups2000_write_register(modbus_ctx,
|
||
|
10000 + cmd_action->reg2,
|
||
|
(uint16_t)cmd_action->val2);
|
||
|
if (r == 1)
|
||
|
status = STAT_INSTCMD_HANDLED;
|
||
|
else
|
||
|
status = STAT_INSTCMD_FAILED;
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
fatalx(EXIT_FAILURE, "invalid ups2000_cmd table!");
|
||
|
}
|
||
|
|
||
|
if (status == STAT_INSTCMD_FAILED)
|
||
|
upslogx(LOG_ERR, "instcmd: command [%s] failed", cmd);
|
||
|
else if (status == STAT_INSTCMD_HANDLED)
|
||
|
upslogx(LOG_INFO, "instcmd: command [%s] handled", cmd);
|
||
|
return status;
|
||
|
}
|
||
|
|
||
|
|
||
|
static int ups2000_instcmd_load_on(const uint16_t reg)
|
||
|
{
|
||
|
int r;
|
||
|
const char *status;
|
||
|
|
||
|
/* force refresh UPS status */
|
||
|
status_init();
|
||
|
r = ups2000_update_status();
|
||
|
if (r != 0) {
|
||
|
/*
|
||
|
* When the UPS status is updated, the code must set either OL, OB, OL ECO,
|
||
|
* BYPASS, or OFF. These five options are mutually exclusive. If the register
|
||
|
* value is invalid and set none of these flags, failure code 1 is returned.
|
||
|
*/
|
||
|
dstate_datastale();
|
||
|
return STAT_INSTCMD_FAILED;
|
||
|
}
|
||
|
status_commit();
|
||
|
|
||
|
status = dstate_getinfo("ups.status");
|
||
|
if (strstr(status, "OFF")) {
|
||
|
/* no warning needed, continue at ups2000_write_register() below */
|
||
|
}
|
||
|
else if (strstr(status, "OL") || strstr(status, "OB")) {
|
||
|
/*
|
||
|
* "Turning it on" has no effect if it's already on. Log a warning
|
||
|
* while still accepting and executing the command.
|
||
|
*/
|
||
|
upslogx(LOG_WARNING, "load.on: UPS is already on.");
|
||
|
upslogx(LOG_WARNING, "load.on: still executing command anyway.");
|
||
|
}
|
||
|
else if (strstr(status, "BYPASS")) {
|
||
|
/*
|
||
|
* If it's in bypass mode, reject this command. The UPS would otherwise
|
||
|
* enter normal mode, but "load.on" is not supposed to affect the
|
||
|
* normal/bypass status. Also log an error and suggest "bypass.stop".
|
||
|
*/
|
||
|
upslogx(LOG_ERR, "load.on error: UPS is already on, and is in bypass mode. "
|
||
|
"To enter normal mode, use bypass.stop");
|
||
|
return STAT_INSTCMD_FAILED;
|
||
|
}
|
||
|
else {
|
||
|
/* unreachable, see comments for r != 0 at the beginning */
|
||
|
upslogx(LOG_ERR, "load.on error: invalid ups.status (%s) detected. "
|
||
|
"Please file a bug report!", status);
|
||
|
return STAT_INSTCMD_FAILED;
|
||
|
}
|
||
|
|
||
|
r = ups2000_write_register(modbus_ctx, 10000 + reg, 1);
|
||
|
if (r != 1)
|
||
|
return STAT_INSTCMD_FAILED;
|
||
|
return STAT_INSTCMD_HANDLED;
|
||
|
}
|
||
|
|
||
|
|
||
|
static int ups2000_instcmd_bypass_start(const uint16_t reg)
|
||
|
{
|
||
|
int r;
|
||
|
NUT_UNUSED_VARIABLE(reg);
|
||
|
|
||
|
/* force update alarms */
|
||
|
alarm_init();
|
||
|
r = ups2000_update_alarm();
|
||
|
if (r != 0)
|
||
|
return STAT_INSTCMD_FAILED;
|
||
|
alarm_commit();
|
||
|
|
||
|
/* bypass input has a power failure, refuse to bypass */
|
||
|
if (!bypass_available) {
|
||
|
upslogx(LOG_ERR, "bypass input is abnormal, refuse to enter bypass mode.");
|
||
|
return STAT_INSTCMD_FAILED;
|
||
|
}
|
||
|
|
||
|
/* enable "bypass on shutdown" */
|
||
|
r = ups2000_write_register(modbus_ctx, 10000 + 1045, 1);
|
||
|
if (r != 1)
|
||
|
return STAT_INSTCMD_FAILED;
|
||
|
|
||
|
/* shutdown */
|
||
|
r = ups2000_write_register(modbus_ctx, 10000 + 1030, 1);
|
||
|
if (r != 1)
|
||
|
return STAT_INSTCMD_FAILED;
|
||
|
|
||
|
return STAT_INSTCMD_HANDLED;
|
||
|
}
|
||
|
|
||
|
|
||
|
static int ups2000_instcmd_beeper_toggle(const uint16_t reg)
|
||
|
{
|
||
|
int r;
|
||
|
const char *string;
|
||
|
|
||
|
r = ups2000_beeper_get(reg);
|
||
|
if (r != 0)
|
||
|
return STAT_INSTCMD_FAILED;
|
||
|
|
||
|
string = dstate_getinfo("ups.beeper.status");
|
||
|
if (!strcasecmp(string, "enabled"))
|
||
|
r = ups2000_beeper_set(reg, "disabled");
|
||
|
else if (!strcasecmp(string, "disabled"))
|
||
|
r = ups2000_beeper_set(reg, "enabled");
|
||
|
else
|
||
|
return STAT_INSTCMD_FAILED;
|
||
|
|
||
|
if (r != STAT_SET_HANDLED)
|
||
|
return STAT_INSTCMD_FAILED;
|
||
|
|
||
|
return STAT_INSTCMD_HANDLED;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
* "ups.shutdown.stayoff": wait an optional offdelay and shutdown.
|
||
|
* When the grid power returns, stay off.
|
||
|
*/
|
||
|
static int ups2000_instcmd_shutdown_stayoff(const uint16_t reg)
|
||
|
{
|
||
|
uint16_t val;
|
||
|
int r;
|
||
|
|
||
|
r = setvar("ups.start.auto", "no");
|
||
|
if (r != STAT_SET_HANDLED)
|
||
|
return STAT_INSTCMD_FAILED;
|
||
|
|
||
|
val = ups2000_offdelay * 10; /* scaling factor */
|
||
|
val /= 60; /* convert to minutes */
|
||
|
|
||
|
r = ups2000_write_register(modbus_ctx, 10000 + reg, val);
|
||
|
if (r != 1)
|
||
|
return STAT_INSTCMD_FAILED;
|
||
|
|
||
|
return STAT_INSTCMD_HANDLED;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
* Wait for "offdelay" second, turn off the load. Then, wait
|
||
|
* for "ondelay" seconds. If the grid power still exists or
|
||
|
* has returned after the timer, turn on the load. Otherwise,
|
||
|
* shutdown the UPS. When combined with "ups.start.auto", it
|
||
|
* guarantees the server can always be restarted even if there
|
||
|
* is a power race.
|
||
|
*
|
||
|
* "shutdown.return", "shutdown.reboot" and "shutdown.reboot.
|
||
|
* graceful" all rely on this function.
|
||
|
*/
|
||
|
static int ups2000_shutdown_guaranteed_return(uint16_t offdelay, uint16_t ondelay)
|
||
|
{
|
||
|
int r;
|
||
|
uint16_t val[2];
|
||
|
|
||
|
r = setvar("ups.start.auto", "yes");
|
||
|
if (r != STAT_SET_HANDLED)
|
||
|
return STAT_INSTCMD_FAILED;
|
||
|
|
||
|
val[0] = (offdelay * 10) / 60;
|
||
|
val[1] = ondelay / 60;
|
||
|
|
||
|
r = ups2000_write_registers(modbus_ctx, 1047 + 10000, 2, val);
|
||
|
if (r != 2)
|
||
|
return STAT_INSTCMD_FAILED;
|
||
|
|
||
|
return STAT_INSTCMD_HANDLED;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
* "ups.shutdown.return": wait an optional "offdelay" and shutdown.
|
||
|
* When the grid power returns, power on the load.
|
||
|
*/
|
||
|
static int ups2000_instcmd_shutdown_return(const uint16_t reg)
|
||
|
{
|
||
|
int r;
|
||
|
NUT_UNUSED_VARIABLE(reg);
|
||
|
|
||
|
r = ups2000_shutdown_guaranteed_return(ups2000_offdelay,
|
||
|
ups2000_ondelay);
|
||
|
if (r == STAT_INSTCMD_HANDLED) {
|
||
|
shutdown_at = time_seek(time(NULL), ups2000_offdelay);
|
||
|
}
|
||
|
return r;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
* "ups.shutdown.reboot": shutdown as soon as possible using the
|
||
|
* smallest "rebootdelay" (inside the UPS, it's the same "ondelay"
|
||
|
* timer), restart after an "ondelay".
|
||
|
*
|
||
|
* In our implementation, it's like "ups.shutdown.return", just
|
||
|
* with a minimal "ondelay".
|
||
|
*/
|
||
|
static int ups2000_instcmd_shutdown_reboot(const uint16_t reg)
|
||
|
{
|
||
|
int r;
|
||
|
NUT_UNUSED_VARIABLE(reg);
|
||
|
|
||
|
r = ups2000_shutdown_guaranteed_return(ups2000_rw_delay[REBOOT].min,
|
||
|
ups2000_ondelay);
|
||
|
if (r == STAT_INSTCMD_HANDLED) {
|
||
|
reboot_at = time_seek(time(NULL), ups2000_rw_delay[REBOOT].min);
|
||
|
start_at = time_seek(reboot_at, ups2000_ondelay);
|
||
|
}
|
||
|
return r;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
* "ups.shutdown.reboot.graceful": shutdown after a "rebootdelay"
|
||
|
* (inside the UPS, it's the same "ondelay" timer), restart after
|
||
|
* an "ondelay".
|
||
|
*
|
||
|
* In our implementation, it's like "ups.shutdown.return", just
|
||
|
* with a "rebootdelay" instead of an "ondelay".
|
||
|
*/
|
||
|
static int ups2000_instcmd_shutdown_reboot_graceful(const uint16_t reg)
|
||
|
{
|
||
|
int r;
|
||
|
NUT_UNUSED_VARIABLE(reg);
|
||
|
|
||
|
r = ups2000_shutdown_guaranteed_return(ups2000_rebootdelay,
|
||
|
ups2000_ondelay);
|
||
|
if (r == STAT_INSTCMD_HANDLED) {
|
||
|
reboot_at = time_seek(time(NULL), ups2000_rebootdelay);
|
||
|
start_at = time_seek(reboot_at, ups2000_ondelay);
|
||
|
}
|
||
|
return r;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
* List of countdown timers and pointers to their corresponding
|
||
|
* global variables "at_time". They record estimated timestamps
|
||
|
* when the actions are supposed to be performed.
|
||
|
*/
|
||
|
static struct {
|
||
|
const char *name;
|
||
|
time_t *const at_time;
|
||
|
} ups2000_timers[] = {
|
||
|
{ "ups.timer.reboot", &reboot_at },
|
||
|
{ "ups.timer.shutdown", &shutdown_at },
|
||
|
{ "ups.timer.start", &start_at },
|
||
|
{ NULL, NULL },
|
||
|
};
|
||
|
|
||
|
|
||
|
static int ups2000_update_timers(void)
|
||
|
{
|
||
|
time_t now;
|
||
|
int eta;
|
||
|
int i;
|
||
|
|
||
|
now = time(NULL);
|
||
|
|
||
|
for (i = 0; ups2000_timers[i].name != NULL; i++) {
|
||
|
if (*ups2000_timers[i].at_time) {
|
||
|
eta = difftime(*ups2000_timers[i].at_time, now);
|
||
|
if (eta < 0)
|
||
|
eta = 0;
|
||
|
dstate_setinfo(ups2000_timers[i].name, "%d", eta);
|
||
|
}
|
||
|
else {
|
||
|
dstate_setinfo(ups2000_timers[i].name, "%d", -1);
|
||
|
}
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
void upsdrv_shutdown(void)
|
||
|
{
|
||
|
int r;
|
||
|
|
||
|
r = instcmd("shutdown.reboot", "");
|
||
|
if (r != STAT_INSTCMD_HANDLED)
|
||
|
fatalx(EXIT_FAILURE, "upsdrv_shutdown failed!");
|
||
|
}
|
||
|
|
||
|
|
||
|
void upsdrv_help(void)
|
||
|
{
|
||
|
}
|
||
|
|
||
|
|
||
|
/* list flags and values that you want to receive via -x */
|
||
|
void upsdrv_makevartable(void)
|
||
|
{
|
||
|
char msg[64];
|
||
|
|
||
|
snprintf(msg, 64, "Set shutdown delay, in seconds, 6-second step"
|
||
|
" (default=%d)", ups2000_rw_delay[SHUTDOWN].dfault);
|
||
|
addvar(VAR_VALUE, "offdelay", msg);
|
||
|
|
||
|
snprintf(msg, 64, "Set reboot delay, in seconds, 6-second step"
|
||
|
" (default=%d).", ups2000_rw_delay[REBOOT].dfault);
|
||
|
addvar(VAR_VALUE, "rebootdelay", msg);
|
||
|
|
||
|
snprintf(msg, 64, "Set start delay, in seconds, 60-second step"
|
||
|
" (default=%d).", ups2000_rw_delay[START].dfault);
|
||
|
addvar(VAR_VALUE, "ondelay", msg);
|
||
|
}
|
||
|
|
||
|
|
||
|
void upsdrv_cleanup(void)
|
||
|
{
|
||
|
if (modbus_ctx != NULL) {
|
||
|
modbus_close(modbus_ctx);
|
||
|
modbus_free(modbus_ctx);
|
||
|
}
|
||
|
ser_close(upsfd, device_path);
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
* Seek time "t" forward or backward by n "seconds" without assuming
|
||
|
* the underlying type and format of "time_t". This ensures maximum
|
||
|
* portability. Although on POSIX and many other systems, "time_t"
|
||
|
* is guaranteed to be in seconds.
|
||
|
*
|
||
|
* On error, abort the program.
|
||
|
*/
|
||
|
static time_t time_seek(time_t t, int seconds)
|
||
|
{
|
||
|
struct tm time_tm;
|
||
|
time_t time_output;
|
||
|
|
||
|
if (!t)
|
||
|
fatalx(EXIT_FAILURE, "time_seek() failed!");
|
||
|
|
||
|
if (!gmtime_r(&t, &time_tm))
|
||
|
fatalx(EXIT_FAILURE, "time_seek() failed!");
|
||
|
|
||
|
time_tm.tm_sec += seconds;
|
||
|
time_output = mktime(&time_tm);
|
||
|
|
||
|
if (time_output == (time_t) -1)
|
||
|
fatalx(EXIT_FAILURE, "time_seek() failed!");
|
||
|
|
||
|
return time_output;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
* Read bytes from the UPS2000 serial port, until the buffer has
|
||
|
* nothing left to read (after ser_get_buf() times out). The buffer
|
||
|
* size is limited to buf_len (inclusive).
|
||
|
*
|
||
|
* On error, return 0.
|
||
|
*
|
||
|
* In the serial library, ser_get_buf() can be a short read, and
|
||
|
* ser_get_buf_let() requires a precalculated length, necessiates
|
||
|
* our own read function.
|
||
|
*/
|
||
|
static size_t ups2000_read_serial(uint8_t *buf, size_t buf_len)
|
||
|
{
|
||
|
ssize_t bytes = 0;
|
||
|
size_t total = 0;
|
||
|
|
||
|
/* wait 400 ms for the device to process our command */
|
||
|
usleep(400 * 1000);
|
||
|
|
||
|
while (buf_len > 0) {
|
||
|
bytes = ser_get_buf(upsfd, buf, buf_len, 1, 0);
|
||
|
if (bytes < 0)
|
||
|
return 0; /* read failure */
|
||
|
else if (bytes == 0)
|
||
|
return total; /* nothing to read */
|
||
|
|
||
|
total += (size_t)bytes; /* increment byte counter */
|
||
|
buf += bytes; /* advance buffer position */
|
||
|
if ((size_t)bytes > buf_len) {
|
||
|
fatalx(EXIT_FAILURE, "ups2000_read_serial() read too much!");
|
||
|
}
|
||
|
buf_len -= (size_t)bytes; /* decrement limiter */
|
||
|
}
|
||
|
return 0; /* buffer exhaustion */
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
* Retry control. By default, we are in RETRY_ENABLE mode. For each
|
||
|
* register read, we retry three times (1 sec. between each attempt),
|
||
|
* before raising a fatal error and giving up. So far so good, but,
|
||
|
* if the link went down, all operation would fail and the program
|
||
|
* would become unresponsive due to excessive retrys.
|
||
|
*
|
||
|
* To prevent this problem, after the first fatal error, we stop all
|
||
|
* retry attempts by entering RETRY_DISABLE_TEMPORARY mode, allowing
|
||
|
* subsequent operation to fail without retry. Later, after the first
|
||
|
* success is encountered, we move back to RETRY_ENABLE mode.
|
||
|
*/
|
||
|
enum {
|
||
|
RETRY_ENABLE,
|
||
|
RETRY_DISABLE_TEMPORARY
|
||
|
};
|
||
|
static int retry_status = RETRY_ENABLE;
|
||
|
|
||
|
|
||
|
/*
|
||
|
* Read one or more registers using libmodbus.
|
||
|
*
|
||
|
* This is simply a wrapper for libmodbus's modbus_read_registers() with
|
||
|
* retry and workaround logic. When an error has occured, we retry 3 times
|
||
|
* before giving up, allowing us to recover from non-fatal failures without
|
||
|
* triggering a data stale.
|
||
|
*/
|
||
|
static int ups2000_read_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest)
|
||
|
{
|
||
|
int i;
|
||
|
int r = -1;
|
||
|
|
||
|
if (addr < 10000)
|
||
|
upslogx(LOG_ERR, "Invalid register read from %04d detected. "
|
||
|
"Please file a bug report!", addr);
|
||
|
|
||
|
for (i = 0; i < 3; i++) {
|
||
|
r = modbus_read_registers(ctx, addr, nb, dest);
|
||
|
|
||
|
/* generic retry for modbus read failures. */
|
||
|
if (retry_status == RETRY_ENABLE && r != nb) {
|
||
|
upslogx(LOG_WARNING, "Register %04d has a read failure. Retrying...", addr);
|
||
|
sleep(1);
|
||
|
continue;
|
||
|
}
|
||
|
else if (r == nb)
|
||
|
retry_status = RETRY_ENABLE;
|
||
|
|
||
|
/*
|
||
|
* Workaround for buggy register 2002 (battery status). Sometimes
|
||
|
* this register returns invalid values. This is a known problem
|
||
|
* and it's not fatal, so we use LOG_INFO.
|
||
|
*/
|
||
|
if (retry_status == RETRY_ENABLE &&
|
||
|
addr == 12002 && (dest[0] < 2 || dest[0] > 5)) {
|
||
|
upslogx(LOG_INFO, "Battery status has a non-fatal read failure, it's usually harmless. Retrying... ");
|
||
|
sleep(1);
|
||
|
continue;
|
||
|
}
|
||
|
else if (addr == 12002 && dest[0] >= 2 && dest[0] <= 5)
|
||
|
retry_status = RETRY_ENABLE;
|
||
|
|
||
|
return r;
|
||
|
}
|
||
|
|
||
|
/* Give up */
|
||
|
upslogx(LOG_WARNING, "Register %04d has a fatal read failure.", addr);
|
||
|
retry_status = RETRY_DISABLE_TEMPORARY;
|
||
|
return r;
|
||
|
}
|
||
|
|
||
|
|
||
|
static int ups2000_write_registers(modbus_t *ctx, int addr, int nb, uint16_t *src)
|
||
|
{
|
||
|
int i;
|
||
|
int r = -1;
|
||
|
|
||
|
if (addr < 10000)
|
||
|
upslogx(LOG_ERR, "Invalid register write to %04d detected. "
|
||
|
"Please file a bug report!", addr);
|
||
|
|
||
|
for (i = 0; i < 3; i++) {
|
||
|
r = modbus_write_registers(ctx, addr, nb, src);
|
||
|
|
||
|
/* generic retry for modbus write failures. */
|
||
|
if (retry_status == RETRY_ENABLE && r != nb) {
|
||
|
upslogx(LOG_WARNING, "Register %04d has a write failure. Retrying...", addr);
|
||
|
sleep(1);
|
||
|
continue;
|
||
|
}
|
||
|
else if (r == nb)
|
||
|
retry_status = RETRY_ENABLE;
|
||
|
|
||
|
return r;
|
||
|
}
|
||
|
|
||
|
/* Give up */
|
||
|
upslogx(LOG_WARNING, "Register %04d has a fatal write failure.", addr);
|
||
|
retry_status = RETRY_DISABLE_TEMPORARY;
|
||
|
return r;
|
||
|
}
|
||
|
|
||
|
|
||
|
static int ups2000_write_register(modbus_t *ctx, int addr, uint16_t val)
|
||
|
{
|
||
|
return ups2000_write_registers(ctx, addr, 1, &val);
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
* The following CRC-16 code was copied from libmodbus.
|
||
|
*
|
||
|
* Copyright (C) 2001-2011 Stéphane Raimbault <stephane.raimbault@gmail.com>
|
||
|
*
|
||
|
* This library is free software; you can redistribute it and/or
|
||
|
* modify it under the terms of the GNU Lesser General Public
|
||
|
* License as published by the Free Software Foundation; either
|
||
|
* version 2.1 of the License, or (at your option) any later version.
|
||
|
*
|
||
|
* This library is distributed in the hope that it will be useful,
|
||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||
|
* Lesser General Public License for more details.
|
||
|
*/
|
||
|
|
||
|
/* Table of CRC values for high-order byte */
|
||
|
static const uint8_t table_crc_hi[] = {
|
||
|
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
|
||
|
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
|
||
|
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
|
||
|
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
|
||
|
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
|
||
|
0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
|
||
|
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
|
||
|
0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
|
||
|
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
|
||
|
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
|
||
|
0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
|
||
|
0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
|
||
|
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
|
||
|
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
|
||
|
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
|
||
|
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
|
||
|
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
|
||
|
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
|
||
|
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
|
||
|
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
|
||
|
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
|
||
|
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
|
||
|
0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
|
||
|
0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
|
||
|
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
|
||
|
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40
|
||
|
};
|
||
|
|
||
|
/* Table of CRC values for low-order byte */
|
||
|
static const uint8_t table_crc_lo[] = {
|
||
|
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06,
|
||
|
0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD,
|
||
|
0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
|
||
|
0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A,
|
||
|
0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4,
|
||
|
0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
|
||
|
0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3,
|
||
|
0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4,
|
||
|
0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,
|
||
|
0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29,
|
||
|
0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED,
|
||
|
0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
|
||
|
0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60,
|
||
|
0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67,
|
||
|
0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,
|
||
|
0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68,
|
||
|
0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E,
|
||
|
0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
|
||
|
0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71,
|
||
|
0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92,
|
||
|
0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
|
||
|
0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B,
|
||
|
0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B,
|
||
|
0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
|
||
|
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42,
|
||
|
0x43, 0x83, 0x41, 0x81, 0x80, 0x40
|
||
|
};
|
||
|
|
||
|
|
||
|
static uint16_t crc16(uint8_t * buffer, uint16_t buffer_length)
|
||
|
{
|
||
|
uint8_t crc_hi = 0xFF; /* high CRC byte initialized */
|
||
|
uint8_t crc_lo = 0xFF; /* low CRC byte initialized */
|
||
|
unsigned int i; /* will index into CRC lookup */
|
||
|
|
||
|
/* pass through message buffer */
|
||
|
while (buffer_length--) {
|
||
|
i = crc_hi ^ *buffer++; /* calculate the CRC */
|
||
|
crc_hi = crc_lo ^ table_crc_hi[i];
|
||
|
crc_lo = table_crc_lo[i];
|
||
|
}
|
||
|
|
||
|
return ((uint16_t)((uint16_t)(crc_hi) << 8) | (uint16_t)crc_lo);
|
||
|
}
|