4105 lines
101 KiB
C
4105 lines
101 KiB
C
/* nutdrv_qx.c - Driver for USB and serial UPS units with Q* protocols
|
|
*
|
|
* Copyright (C)
|
|
* 2013 Daniele Pezzini <hyouko@gmail.com>
|
|
* 2016 Eaton
|
|
* Based on:
|
|
* usbhid-ups.c - Copyright (C)
|
|
* 2003-2012 Arnaud Quette <arnaud.quette@gmail.com>
|
|
* 2005 John Stamp <kinsayder@hotmail.com>
|
|
* 2005-2006 Peter Selinger <selinger@users.sourceforge.net>
|
|
* 2007-2009 Arjen de Korte <adkorte-guest@alioth.debian.org>
|
|
* blazer.c - Copyright (C)
|
|
* 2008-2009 Arjen de Korte <adkorte-guest@alioth.debian.org>
|
|
* 2012 Arnaud Quette <ArnaudQuette@Eaton.com>
|
|
* blazer_ser.c - Copyright (C)
|
|
* 2008 Arjen de Korte <adkorte-guest@alioth.debian.org>
|
|
* blazer_usb.c - Copyright (C)
|
|
* 2003-2009 Arjen de Korte <adkorte-guest@alioth.debian.org>
|
|
* 2011-2012 Arnaud Quette <arnaud.quette@free.fr>
|
|
* Masterguard additions
|
|
* 2020-2021 Edgar Fuß, Mathematisches Institut der Universität Bonn <ef@math.uni-bonn.de>
|
|
* Armac (Richcomm-variant) additions
|
|
* 2021 Tomasz Fortuna <bla@thera.be>
|
|
*
|
|
* 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
|
|
*
|
|
*/
|
|
|
|
#define DRIVER_VERSION "0.32"
|
|
|
|
#include "config.h"
|
|
#include "main.h"
|
|
#include "attribute.h"
|
|
#include "nut_float.h"
|
|
#include "nut_stdint.h"
|
|
|
|
/* note: QX_USB/QX_SERIAL set through Makefile */
|
|
#ifdef QX_USB
|
|
#include "nut_libusb.h" /* also includes "usb-common.h" */
|
|
|
|
#ifdef QX_SERIAL
|
|
#define DRIVER_NAME "Generic Q* USB/Serial driver"
|
|
#else
|
|
#define DRIVER_NAME "Generic Q* USB driver"
|
|
#endif /* QX_SERIAL */
|
|
#else
|
|
#define DRIVER_NAME "Generic Q* Serial driver"
|
|
#endif /* QX_USB */
|
|
|
|
#ifdef QX_SERIAL
|
|
#include "serial.h"
|
|
#define SER_WAIT_SEC 1 /* 3 seconds for Best UPS */
|
|
#endif /* QX_SERIAL */
|
|
|
|
#include "nutdrv_qx.h"
|
|
|
|
/* == Subdrivers == */
|
|
/* Include all known subdrivers */
|
|
#include "nutdrv_qx_bestups.h"
|
|
#include "nutdrv_qx_hunnox.h"
|
|
#include "nutdrv_qx_mecer.h"
|
|
#include "nutdrv_qx_megatec.h"
|
|
#include "nutdrv_qx_megatec-old.h"
|
|
#include "nutdrv_qx_mustek.h"
|
|
#include "nutdrv_qx_q1.h"
|
|
#include "nutdrv_qx_voltronic.h"
|
|
#include "nutdrv_qx_voltronic-qs.h"
|
|
#include "nutdrv_qx_voltronic-qs-hex.h"
|
|
#include "nutdrv_qx_zinto.h"
|
|
#include "nutdrv_qx_masterguard.h"
|
|
#include "nutdrv_qx_ablerex.h"
|
|
|
|
/* Reference list of available non-USB subdrivers */
|
|
static subdriver_t *subdriver_list[] = {
|
|
&voltronic_subdriver,
|
|
&voltronic_qs_subdriver,
|
|
&voltronic_qs_hex_subdriver,
|
|
&mustek_subdriver,
|
|
&megatec_old_subdriver,
|
|
&bestups_subdriver,
|
|
&mecer_subdriver,
|
|
&megatec_subdriver,
|
|
&zinto_subdriver,
|
|
&masterguard_subdriver,
|
|
&hunnox_subdriver,
|
|
&ablerex_subdriver,
|
|
/* Fallback Q1 subdriver */
|
|
&q1_subdriver,
|
|
NULL
|
|
};
|
|
|
|
|
|
/* == Driver description structure == */
|
|
upsdrv_info_t upsdrv_info = {
|
|
DRIVER_NAME,
|
|
DRIVER_VERSION,
|
|
"Daniele Pezzini <hyouko@gmail.com>" \
|
|
"Arnaud Quette <arnaud.quette@gmail.com>" \
|
|
"John Stamp <kinsayder@hotmail.com>" \
|
|
"Peter Selinger <selinger@users.sourceforge.net>" \
|
|
"Arjen de Korte <adkorte-guest@alioth.debian.org>" \
|
|
"Edgar Fuß <ef@math.uni-bonn.de>",
|
|
DRV_BETA,
|
|
#ifdef QX_USB
|
|
{ &comm_upsdrv_info, NULL }
|
|
#else
|
|
{ NULL }
|
|
#endif /* QX_USB */
|
|
};
|
|
|
|
/* == Data walk modes == */
|
|
typedef enum {
|
|
QX_WALKMODE_INIT = 0,
|
|
QX_WALKMODE_QUICK_UPDATE,
|
|
QX_WALKMODE_FULL_UPDATE
|
|
} walkmode_t;
|
|
|
|
|
|
/* == Global vars == */
|
|
/* Pointer to the active subdriver object (changed in subdriver_matcher() function) */
|
|
static subdriver_t *subdriver = NULL;
|
|
|
|
static long pollfreq = DEFAULT_POLLFREQ;
|
|
static unsigned int ups_status = 0;
|
|
static bool_t data_has_changed = FALSE; /* for SEMI_STATIC data polling */
|
|
|
|
static time_t lastpoll; /* Timestamp the last polling */
|
|
|
|
#if defined(QX_USB) && !defined(TESTING)
|
|
static int hunnox_step = 0;
|
|
#endif /* QX_USB && !TESTING */
|
|
|
|
#if defined(QX_USB) && defined(QX_SERIAL)
|
|
static int is_usb = 0; /* Whether the device is connected through USB (1) or serial (0) */
|
|
#endif /* QX_USB && QX_SERIAL */
|
|
|
|
static struct {
|
|
char command[SMALLBUF]; /* Command sent to the UPS to get answer/to execute an instant command */
|
|
char answer[SMALLBUF]; /* Answer from the UPS, filled at runtime */
|
|
} previous_item = { "", "" }; /* Hold the values of the item processed just before the actual one */
|
|
|
|
|
|
/* == Support functions == */
|
|
static int subdriver_matcher(void);
|
|
static ssize_t qx_command(const char *cmd, char *buf, size_t buflen);
|
|
static int qx_process_answer(item_t *item, const size_t len); /* returns just 0 or -1 */
|
|
static bool_t qx_ups_walk(walkmode_t mode);
|
|
static void ups_status_set(void);
|
|
static void ups_alarm_set(void);
|
|
static void qx_set_var(item_t *item);
|
|
|
|
|
|
/* == Struct & data for status processing == */
|
|
typedef struct {
|
|
const char *status_str; /* UPS status string */
|
|
const unsigned int status_mask; /* UPS status mask */
|
|
} status_lkp_t;
|
|
|
|
static status_lkp_t status_info[] = {
|
|
/* Map status strings to bit masks */
|
|
{ "OL", STATUS(OL) },
|
|
{ "LB", STATUS(LB) },
|
|
{ "RB", STATUS(RB) },
|
|
{ "CHRG", STATUS(CHRG) },
|
|
{ "DISCHRG", STATUS(DISCHRG) },
|
|
{ "BYPASS", STATUS(BYPASS) },
|
|
{ "CAL", STATUS(CAL) },
|
|
{ "OFF", STATUS(OFF) },
|
|
{ "OVER", STATUS(OVER) },
|
|
{ "TRIM", STATUS(TRIM) },
|
|
{ "BOOST", STATUS(BOOST) },
|
|
{ "FSD", STATUS(FSD) },
|
|
{ NULL, 0 },
|
|
};
|
|
|
|
|
|
/* == battery.{charge,runtime} guesstimation == */
|
|
/* Support functions */
|
|
static int qx_battery(void);
|
|
static int qx_load(void);
|
|
static void qx_initbattery(void);
|
|
|
|
/* Battery data */
|
|
static struct {
|
|
double packs; /* Battery voltage multiplier */
|
|
struct {
|
|
double act; /* Actual runtime on battery */
|
|
double nom; /* Nominal runtime on battery (full load) */
|
|
double est; /* Estimated runtime remaining (full load) */
|
|
double exp; /* Load exponent */
|
|
} runt;
|
|
struct {
|
|
double act; /* Actual battery voltage */
|
|
double high; /* Battery float voltage */
|
|
double nom; /* Nominal battery voltage */
|
|
double low; /* Battery low voltage */
|
|
} volt;
|
|
struct {
|
|
double act; /* Actual battery charge */
|
|
long time; /* Recharge time from empty to full */
|
|
} chrg;
|
|
} batt = { 1, { -1, -1, 0, 0 }, { -1, -1, -1, -1 }, { -1, 43200 } };
|
|
|
|
/* Load data */
|
|
static struct {
|
|
double act; /* Actual load (reported by the UPS) */
|
|
double low; /* Idle load */
|
|
double eff; /* Effective load */
|
|
} load = { 0, 0.1, 1 };
|
|
|
|
static time_t battery_lastpoll = 0;
|
|
|
|
/* Fill batt.volt.act and guesstimate the battery charge
|
|
* if it isn't already available. */
|
|
static int qx_battery(void)
|
|
{
|
|
const char *val = dstate_getinfo("battery.voltage");
|
|
|
|
if (!val) {
|
|
upsdebugx(2, "%s: unable to get battery.voltage", __func__);
|
|
return -1;
|
|
}
|
|
|
|
batt.volt.act = batt.packs * strtod(val, NULL);
|
|
|
|
if (d_equal(batt.chrg.act, -1) && batt.volt.low > 0 && batt.volt.high > batt.volt.low) {
|
|
|
|
batt.chrg.act = 100 * (batt.volt.act - batt.volt.low) / (batt.volt.high - batt.volt.low);
|
|
|
|
if (batt.chrg.act < 0) {
|
|
batt.chrg.act = 0;
|
|
}
|
|
|
|
if (batt.chrg.act > 100) {
|
|
batt.chrg.act = 100;
|
|
}
|
|
|
|
dstate_setinfo("battery.charge", "%.0f", batt.chrg.act);
|
|
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Load for battery.{charge,runtime} from runtimecal */
|
|
static int qx_load(void)
|
|
{
|
|
const char *val = dstate_getinfo("ups.load");
|
|
|
|
if (!val) {
|
|
upsdebugx(2, "%s: unable to get ups.load", __func__);
|
|
return -1;
|
|
}
|
|
|
|
load.act = strtod(val, NULL);
|
|
|
|
load.eff = pow(load.act / 100, batt.runt.exp);
|
|
|
|
if (load.eff < load.low) {
|
|
load.eff = load.low;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Guesstimation: init */
|
|
static void qx_initbattery(void)
|
|
{
|
|
if (!dstate_getinfo("battery.charge") || !dstate_getinfo("battery.runtime")) {
|
|
|
|
const char *val;
|
|
|
|
val = dstate_getinfo("battery.voltage.high");
|
|
if (val) {
|
|
batt.volt.high = strtod(val, NULL);
|
|
}
|
|
|
|
val = dstate_getinfo("battery.voltage.low");
|
|
if (val) {
|
|
batt.volt.low = strtod(val, NULL);
|
|
}
|
|
|
|
val = dstate_getinfo("battery.voltage.nominal");
|
|
if (val) {
|
|
batt.volt.nom = strtod(val, NULL);
|
|
}
|
|
|
|
/* If no values are available for both battery.voltage.{low,high}
|
|
* either from the UPS or provided by the user in ups.conf,
|
|
* try to guesstimate them, but announce it! */
|
|
if ( (!d_equal(batt.volt.nom, -1)) && (d_equal(batt.volt.low, -1) || d_equal(batt.volt.high, -1))) {
|
|
|
|
upslogx(LOG_INFO, "No values for battery high/low voltages");
|
|
|
|
/* Basic formula, which should cover most cases */
|
|
batt.volt.low = 104 * batt.volt.nom / 120;
|
|
batt.volt.high = 130 * batt.volt.nom / 120;
|
|
|
|
/* Publish these data too */
|
|
dstate_setinfo("battery.voltage.low", "%.2f", batt.volt.low);
|
|
dstate_setinfo("battery.voltage.high", "%.2f", batt.volt.high);
|
|
|
|
upslogx(LOG_INFO, "Using 'guesstimation' (low: %f, high: %f)!",
|
|
batt.volt.low, batt.volt.high);
|
|
|
|
}
|
|
|
|
val = dstate_getinfo("battery.packs");
|
|
if (val && (strspn(val, "0123456789 .") == strlen(val))) {
|
|
batt.packs = strtod(val, NULL);
|
|
} else {
|
|
|
|
/* qx_battery -> batt.volt.act */
|
|
if (!qx_battery() && (!d_equal(batt.volt.nom, -1))) {
|
|
|
|
const double packs[] = { 120, 100, 80, 60, 48, 36, 30, 24, 18, 12, 8, 6, 4, 3, 2, 1, 0.5, -1 };
|
|
int i;
|
|
|
|
/* The battery voltage will quickly return to
|
|
* at least the nominal value after discharging them.
|
|
* For overlapping battery.voltage.low/high ranges
|
|
* therefore choose the one with the highest multiplier. */
|
|
for (i = 0; packs[i] > 0; i++) {
|
|
|
|
if (packs[i] * batt.volt.act > 1.2 * batt.volt.nom) {
|
|
continue;
|
|
}
|
|
|
|
if (packs[i] * batt.volt.act < 0.8 * batt.volt.nom) {
|
|
upslogx(LOG_INFO,
|
|
"Can't autodetect number of battery packs [%.0f/%.2f]",
|
|
batt.volt.nom, batt.volt.act);
|
|
break;
|
|
}
|
|
|
|
batt.packs = packs[i];
|
|
break;
|
|
|
|
}
|
|
|
|
} else {
|
|
upslogx(LOG_INFO,
|
|
"Can't autodetect number of battery packs [%.0f/%.2f]",
|
|
batt.volt.nom, batt.volt.act);
|
|
}
|
|
|
|
}
|
|
|
|
/* Update batt.{chrg,volt}.act */
|
|
qx_battery();
|
|
|
|
val = getval("runtimecal");
|
|
if (val) {
|
|
|
|
double rh, lh, rl, ll;
|
|
|
|
time(&battery_lastpoll);
|
|
|
|
if (sscanf(val, "%lf,%lf,%lf,%lf", &rh, &lh, &rl, &ll) < 4) {
|
|
fatalx(EXIT_FAILURE, "Insufficient parameters for runtimecal");
|
|
}
|
|
|
|
if ((rl < rh) || (rh <= 0)) {
|
|
fatalx(EXIT_FAILURE, "Parameter out of range (runtime)");
|
|
}
|
|
|
|
if ((lh > 100) || (ll > lh) || (ll <= 0)) {
|
|
fatalx(EXIT_FAILURE, "Parameter out of range (load)");
|
|
}
|
|
|
|
batt.runt.exp = log(rl / rh) / log(lh / ll);
|
|
upsdebugx(2, "%s: battery runtime exponent: %.3f",
|
|
__func__, batt.runt.exp);
|
|
|
|
batt.runt.nom = rh * pow(lh / 100, batt.runt.exp);
|
|
upsdebugx(2, "%s: battery runtime nominal: %.1f",
|
|
__func__, batt.runt.nom);
|
|
|
|
} else {
|
|
|
|
upslogx(LOG_INFO, "Battery runtime will not be calculated "
|
|
"(runtimecal not set)");
|
|
return;
|
|
|
|
}
|
|
|
|
val = dstate_getinfo("battery.charge");
|
|
if (!val && (!d_equal(batt.volt.nom, -1))) {
|
|
batt.volt.low = batt.volt.nom;
|
|
batt.volt.high = 1.15 * batt.volt.nom;
|
|
|
|
if (qx_battery())
|
|
fatalx(EXIT_FAILURE, "Initial battery charge undetermined");
|
|
|
|
val = dstate_getinfo("battery.charge");
|
|
}
|
|
|
|
if (val) {
|
|
batt.runt.est = batt.runt.nom * strtod(val, NULL) / 100;
|
|
upsdebugx(2, "%s: battery runtime estimate: %.1f",
|
|
__func__, batt.runt.est);
|
|
} else {
|
|
fatalx(EXIT_FAILURE, "Initial battery charge undetermined");
|
|
}
|
|
|
|
val = getval("chargetime");
|
|
if (val) {
|
|
batt.chrg.time = strtol(val, NULL, 10);
|
|
|
|
if (batt.chrg.time <= 0) {
|
|
fatalx(EXIT_FAILURE, "Charge time out of range [1..s]");
|
|
}
|
|
|
|
upsdebugx(2, "%s: battery charge time: %ld",
|
|
__func__, batt.chrg.time);
|
|
} else {
|
|
upslogx(LOG_INFO,
|
|
"No charge time specified, "
|
|
"using built in default [%ld seconds]",
|
|
batt.chrg.time);
|
|
}
|
|
|
|
val = getval("idleload");
|
|
if (val) {
|
|
load.low = strtod(val, NULL) / 100;
|
|
|
|
if ((load.low <= 0) || (load.low > 1)) {
|
|
fatalx(EXIT_FAILURE, "Idle load out of range [0..100]");
|
|
}
|
|
|
|
upsdebugx(2,
|
|
"%s: minimum load used (idle): %.3f",
|
|
__func__, load.low);
|
|
} else {
|
|
upslogx(LOG_INFO,
|
|
"No idle load specified, using built in default [%.1f %%]",
|
|
100 * load.low);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* == USB communication subdrivers == */
|
|
#if defined(QX_USB) && !defined(TESTING)
|
|
static usb_communication_subdriver_t *usb = &usb_subdriver;
|
|
static usb_dev_handle *udev = NULL;
|
|
static USBDevice_t usbdevice;
|
|
static USBDeviceMatcher_t *reopen_matcher = NULL;
|
|
static USBDeviceMatcher_t *regex_matcher = NULL;
|
|
static int langid_fix = -1;
|
|
|
|
static int (*subdriver_command)(const char *cmd, char *buf, size_t buflen) = NULL;
|
|
|
|
/* Cypress communication subdriver */
|
|
static int cypress_command(const char *cmd, char *buf, size_t buflen)
|
|
{
|
|
char tmp[SMALLBUF];
|
|
int ret = 0;
|
|
size_t i;
|
|
|
|
if (buflen > INT_MAX) {
|
|
upsdebugx(3, "%s: requested to read too much (%zu), "
|
|
"reducing buflen to (INT_MAX-1)",
|
|
__func__, buflen);
|
|
buflen = (INT_MAX - 1);
|
|
}
|
|
|
|
/* Send command */
|
|
memset(tmp, 0, sizeof(tmp));
|
|
snprintf(tmp, sizeof(tmp), "%s", cmd);
|
|
|
|
for (i = 0; i < strlen(tmp); i += (size_t)ret) {
|
|
|
|
/* Write data in 8-byte chunks */
|
|
/* ret = usb->set_report(udev, 0, (unsigned char *)&tmp[i], 8); */
|
|
ret = usb_control_msg(udev,
|
|
USB_ENDPOINT_OUT + USB_TYPE_CLASS + USB_RECIP_INTERFACE,
|
|
0x09, 0x200, 0,
|
|
(usb_ctrl_charbuf)&tmp[i], 8, 5000);
|
|
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "send: %s (%d)",
|
|
ret ? nut_usb_strerror(ret) : "timeout",
|
|
ret);
|
|
return ret;
|
|
}
|
|
|
|
}
|
|
|
|
upsdebugx(3, "send: %.*s", (int)strcspn(tmp, "\r"), tmp);
|
|
|
|
/* Read reply */
|
|
memset(buf, 0, buflen);
|
|
|
|
for (i = 0; (i <= buflen-8) && (memchr(buf, '\r', buflen) == NULL); i += (size_t)ret) {
|
|
|
|
/* Read data in 8-byte chunks */
|
|
/* ret = usb->get_interrupt(udev, (unsigned char *)&buf[i], 8, 1000); */
|
|
ret = usb_interrupt_read(udev,
|
|
0x81,
|
|
(usb_ctrl_charbuf)&buf[i], 8, 1000);
|
|
|
|
/* Any errors here mean that we are unable to read a reply
|
|
* (which will happen after successfully writing a command
|
|
* to the UPS) */
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "read: %s (%d)",
|
|
ret ? nut_usb_strerror(ret) : "timeout",
|
|
ret);
|
|
return ret;
|
|
}
|
|
|
|
snprintf(tmp, sizeof(tmp), "read [% 3d]", (int)i);
|
|
upsdebug_hex(5, tmp, &buf[i], (size_t)ret);
|
|
|
|
}
|
|
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(buf, "\r"), buf);
|
|
|
|
if (i > INT_MAX) {
|
|
upsdebugx(3, "%s: read too much (%zu)", __func__, i);
|
|
return -1;
|
|
}
|
|
return (int)i;
|
|
}
|
|
|
|
/* SGS communication subdriver */
|
|
static int sgs_command(const char *cmd, char *buf, size_t buflen)
|
|
{
|
|
char tmp[SMALLBUF];
|
|
int ret = 0;
|
|
size_t cmdlen, i;
|
|
|
|
if (buflen > INT_MAX) {
|
|
upsdebugx(3, "%s: requested to read too much (%zu), "
|
|
"reducing buflen to (INT_MAX-1)",
|
|
__func__, buflen);
|
|
buflen = (INT_MAX - 1);
|
|
}
|
|
|
|
/* Send command */
|
|
cmdlen = strlen(cmd);
|
|
|
|
for (i = 0; i < cmdlen; i += (size_t)ret) {
|
|
|
|
memset(tmp, 0, sizeof(tmp));
|
|
|
|
/* i and cmdlen are size_t nominally, but diff is not large */
|
|
ret = (int)((cmdlen - i) < 7 ? (cmdlen - i) : 7);
|
|
|
|
/* ret is between 0 and 7 */
|
|
tmp[0] = (char)ret;
|
|
memcpy(&tmp[1], &cmd[i], (unsigned char)ret);
|
|
|
|
/* Write data in 8-byte chunks */
|
|
ret = usb_control_msg(udev,
|
|
USB_ENDPOINT_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE,
|
|
0x09, 0x200, 0,
|
|
(usb_ctrl_charbuf)tmp, 8, 5000);
|
|
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "send: %s (%d)",
|
|
ret ? nut_usb_strerror(ret) : "timeout",
|
|
ret);
|
|
return ret;
|
|
}
|
|
|
|
ret--;
|
|
|
|
}
|
|
|
|
upsdebugx(3, "send: %.*s", (int)strcspn(cmd, "\r"), cmd);
|
|
|
|
/* Read reply */
|
|
memset(buf, 0, buflen);
|
|
|
|
for (i = 0; i <= buflen - 8; i += (size_t)ret) {
|
|
|
|
memset(tmp, 0, sizeof(tmp));
|
|
|
|
/* Read data in 8-byte chunks */
|
|
ret = usb_interrupt_read(udev,
|
|
0x81,
|
|
(usb_ctrl_charbuf)tmp, 8, 1000);
|
|
|
|
/* No error!!! */
|
|
/* if (ret == -110) */
|
|
if (ret == ERROR_TIMEOUT)
|
|
break;
|
|
|
|
/* Any errors here mean that we are unable to read a reply
|
|
* (which will happen after successfully writing a command
|
|
* to the UPS) */
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "read: %s (%d)",
|
|
ret ? nut_usb_strerror(ret) : "timeout",
|
|
ret);
|
|
return ret;
|
|
}
|
|
|
|
/* Every call to read returns 8 bytes
|
|
* -> actually returned bytes: */
|
|
ret = tmp[0] <= 7 ? tmp[0] : 7;
|
|
|
|
if (ret > 0)
|
|
memcpy(&buf[i], &tmp[1], (unsigned char)ret);
|
|
|
|
snprintf(tmp, sizeof(tmp), "read [% 3d]", (int)i);
|
|
upsdebug_hex(5, tmp, &buf[i], (size_t)ret);
|
|
|
|
}
|
|
|
|
/* If the reply lacks the expected terminating CR, add it (if there's enough space) */
|
|
if (i && memchr(buf, '\r', i) == NULL) {
|
|
upsdebugx(4, "%s: the reply lacks the expected terminating CR.", __func__);
|
|
if (i < buflen - 1) {
|
|
upsdebugx(4, "%s: adding missing terminating CR.", __func__);
|
|
buf[i++] = '\r';
|
|
buf[i] = 0;
|
|
}
|
|
}
|
|
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(buf, "\r"), buf);
|
|
|
|
if (i > INT_MAX) {
|
|
upsdebugx(3, "%s: read too much (%zu)", __func__, i);
|
|
return -1;
|
|
}
|
|
return (int)i;
|
|
}
|
|
|
|
/* Phoenix communication subdriver */
|
|
static int phoenix_command(const char *cmd, char *buf, size_t buflen)
|
|
{
|
|
char tmp[SMALLBUF];
|
|
int ret;
|
|
size_t i;
|
|
|
|
if (buflen > INT_MAX) {
|
|
upsdebugx(3, "%s: requested to read too much (%zu), "
|
|
"reducing buflen to (INT_MAX-1)",
|
|
__func__, buflen);
|
|
buflen = (INT_MAX - 1);
|
|
}
|
|
|
|
for (i = 0; i < 8; i++) {
|
|
|
|
/* Read data in 8-byte chunks */
|
|
/* ret = usb->get_interrupt(udev, (unsigned char *)tmp, 8, 1000); */
|
|
ret = usb_interrupt_read(udev,
|
|
0x81,
|
|
(usb_ctrl_charbuf)tmp, 8, 1000);
|
|
|
|
/* This USB to serial implementation is crappy.
|
|
* In order to read correct replies we need to flush the
|
|
* output buffers of the converter until we get no more
|
|
* data (e.g. it times out). */
|
|
switch (ret)
|
|
{
|
|
case ERROR_PIPE: /* Broken pipe */
|
|
usb_clear_halt(udev, 0x81);
|
|
break;
|
|
|
|
case ERROR_TIMEOUT: /* Connection timed out */
|
|
break;
|
|
}
|
|
|
|
if (ret < 0) {
|
|
upsdebugx(3, "flush: %s (%d)",
|
|
nut_usb_strerror(ret), ret);
|
|
break;
|
|
}
|
|
|
|
upsdebug_hex(4, "dump", tmp, (size_t)ret);
|
|
|
|
}
|
|
|
|
/* Send command */
|
|
memset(tmp, 0, sizeof(tmp));
|
|
snprintf(tmp, sizeof(tmp), "%s", cmd);
|
|
|
|
for (i = 0; i < strlen(tmp); i += (size_t)ret) {
|
|
|
|
/* Write data in 8-byte chunks */
|
|
/* ret = usb->set_report(udev, 0, (unsigned char *)&tmp[i], 8); */
|
|
ret = usb_control_msg(udev,
|
|
USB_ENDPOINT_OUT + USB_TYPE_CLASS + USB_RECIP_INTERFACE,
|
|
0x09, 0x200, 0, (usb_ctrl_charbuf)&tmp[i], 8, 1000);
|
|
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "send: %s (%d)",
|
|
ret ? nut_usb_strerror(ret) : "timeout", ret);
|
|
return ret;
|
|
}
|
|
|
|
}
|
|
|
|
upsdebugx(3, "send: %.*s", (int)strcspn(tmp, "\r"), tmp);
|
|
|
|
/* Read reply */
|
|
memset(buf, 0, buflen);
|
|
|
|
for (i = 0; (i <= buflen-8) && (memchr(buf, '\r', buflen) == NULL); i += (size_t)ret) {
|
|
|
|
/* Read data in 8-byte chunks */
|
|
/* ret = usb->get_interrupt(udev, (unsigned char *)&buf[i], 8, 1000); */
|
|
ret = usb_interrupt_read(udev,
|
|
0x81,
|
|
(usb_ctrl_charbuf)&buf[i], 8, 1000);
|
|
|
|
/* Any errors here mean that we are unable to read a reply
|
|
* (which will happen after successfully writing a command
|
|
* to the UPS) */
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "read: %s (%d)",
|
|
ret ? nut_usb_strerror(ret) : "timeout", ret);
|
|
return ret;
|
|
}
|
|
|
|
snprintf(tmp, sizeof(tmp), "read [% 3d]", (int)i);
|
|
upsdebug_hex(5, tmp, &buf[i], (size_t)ret);
|
|
|
|
}
|
|
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(buf, "\r"), buf);
|
|
|
|
if (i > INT_MAX) {
|
|
upsdebugx(3, "%s: read too much (%zu)", __func__, i);
|
|
return -1;
|
|
}
|
|
return (int)i;
|
|
}
|
|
|
|
/* Ippon communication subdriver */
|
|
static int ippon_command(const char *cmd, char *buf, size_t buflen)
|
|
{
|
|
char tmp[64];
|
|
int ret;
|
|
size_t i, len;
|
|
|
|
if (buflen > INT_MAX) {
|
|
upsdebugx(3, "%s: requested to read too much (%zu), "
|
|
"reducing buflen to (INT_MAX-1)",
|
|
__func__, buflen);
|
|
buflen = (INT_MAX - 1);
|
|
}
|
|
|
|
/* Send command */
|
|
snprintf(tmp, sizeof(tmp), "%s", cmd);
|
|
|
|
for (i = 0; i < strlen(tmp); i += (size_t)ret) {
|
|
|
|
/* Write data in 8-byte chunks */
|
|
ret = usb_control_msg(udev,
|
|
USB_ENDPOINT_OUT + USB_TYPE_CLASS + USB_RECIP_INTERFACE,
|
|
0x09, 0x2, 0, (usb_ctrl_charbuf)&tmp[i], 8, 1000);
|
|
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "send: %s (%d)",
|
|
(ret != ERROR_TIMEOUT) ? nut_usb_strerror(ret) : "Connection timed out",
|
|
ret);
|
|
return ret;
|
|
}
|
|
|
|
}
|
|
|
|
upsdebugx(3, "send: %.*s", (int)strcspn(tmp, "\r"), tmp);
|
|
|
|
/* Read all 64 bytes of the reply in one large chunk */
|
|
ret = usb_interrupt_read(udev,
|
|
0x81,
|
|
(usb_ctrl_charbuf)tmp, sizeof(tmp), 1000);
|
|
|
|
/* Any errors here mean that we are unable to read a reply
|
|
* (which will happen after successfully writing a command
|
|
* to the UPS) */
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "read: %s (%d)",
|
|
(ret != ERROR_TIMEOUT) ? nut_usb_strerror(ret) : "Connection timed out",
|
|
ret);
|
|
return ret;
|
|
}
|
|
|
|
/* As Ippon will always return 64 bytes in response,
|
|
* we have to calculate and return length of actual
|
|
* response data here.
|
|
* Empty response will look like 0x00 0x0D, otherwise
|
|
* it will be data string terminated by 0x0D. */
|
|
|
|
for (i = 0, len = 0; i < (size_t)ret; i++) {
|
|
|
|
if (tmp[i] != '\r')
|
|
continue;
|
|
|
|
len = ++i;
|
|
break;
|
|
|
|
}
|
|
|
|
/* Just in case there wasn't any '\r', fallback to string length, if any */
|
|
if (!len)
|
|
len = strlen(tmp);
|
|
|
|
upsdebug_hex(5, "read", tmp, (size_t)len);
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(tmp, "\r"), tmp);
|
|
|
|
len = len < buflen ? len : buflen - 1;
|
|
|
|
memset(buf, 0, buflen);
|
|
memcpy(buf, tmp, len);
|
|
|
|
/* If the reply lacks the expected terminating CR, add it (if there's enough space) */
|
|
if (len && memchr(buf, '\r', len) == NULL) {
|
|
upsdebugx(4, "%s: the reply lacks the expected terminating CR.", __func__);
|
|
if (len < buflen - 1) {
|
|
upsdebugx(4, "%s: adding missing terminating CR.", __func__);
|
|
buf[len++] = '\r';
|
|
buf[len] = 0;
|
|
}
|
|
}
|
|
|
|
if (len > INT_MAX) {
|
|
upsdebugx(3, "%s: read too much (%zu)", __func__, len);
|
|
return -1;
|
|
}
|
|
return (int)len;
|
|
}
|
|
|
|
static int hunnox_protocol(int asking_for)
|
|
{
|
|
char buf[1030];
|
|
|
|
int langid_fix_local = 0x0409;
|
|
|
|
if (langid_fix != -1) {
|
|
langid_fix_local = langid_fix;
|
|
}
|
|
|
|
switch (hunnox_step) {
|
|
case 0:
|
|
upsdebugx(3, "asking for: %02X", 0x00);
|
|
usb_get_string(udev, 0x00,
|
|
langid_fix_local, (usb_ctrl_charbuf)buf, 1026);
|
|
usb_get_string(udev, 0x00,
|
|
langid_fix_local, (usb_ctrl_charbuf)buf, 1026);
|
|
usb_get_string(udev, 0x01,
|
|
langid_fix_local, (usb_ctrl_charbuf)buf, 1026);
|
|
usleep(10000);
|
|
break;
|
|
case 1:
|
|
if (asking_for != 0x0d) {
|
|
upsdebugx(3, "asking for: %02X", 0x0d);
|
|
usb_get_string(udev, 0x0d,
|
|
langid_fix_local, (usb_ctrl_charbuf)buf, 102);
|
|
}
|
|
break;
|
|
case 2:
|
|
if (asking_for != 0x03) {
|
|
upsdebugx(3, "asking for: %02X", 0x03);
|
|
usb_get_string(udev, 0x03,
|
|
langid_fix_local, (usb_ctrl_charbuf)buf, 102);
|
|
}
|
|
break;
|
|
case 3:
|
|
if (asking_for != 0x0c) {
|
|
upsdebugx(3, "asking for: %02X", 0x0c);
|
|
usb_get_string(udev, 0x0c,
|
|
langid_fix_local, (usb_ctrl_charbuf)buf, 102);
|
|
}
|
|
break;
|
|
default:
|
|
hunnox_step = 0;
|
|
}
|
|
hunnox_step++;
|
|
if (hunnox_step > 3) {
|
|
hunnox_step = 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Krauler communication subdriver */
|
|
static int krauler_command(const char *cmd, char *buf, size_t buflen)
|
|
{
|
|
/* Still not implemented:
|
|
* 0x6 T<n> (don't know how to pass the parameter)
|
|
* 0x68 and 0x69 both cause shutdown after an undefined interval */
|
|
const struct {
|
|
const char *str; /* Megatec command */
|
|
const int index; /* Krauler string index for this command */
|
|
const char prefix; /* Character to replace the first byte in reply */
|
|
} command[] = {
|
|
{ "Q1\r", 0x03, '(' },
|
|
{ "F\r", 0x0d, '#' },
|
|
{ "I\r", 0x0c, '#' },
|
|
{ "T\r", 0x04, '\r' },
|
|
{ "TL\r", 0x05, '\r' },
|
|
{ "Q\r", 0x07, '\r' },
|
|
{ "C\r", 0x0b, '\r' },
|
|
{ "CT\r", 0x0b, '\r' },
|
|
{ NULL, 0, '\0' }
|
|
};
|
|
|
|
int i;
|
|
|
|
upsdebugx(3, "send: %.*s", (int)strcspn(cmd, "\r"), cmd);
|
|
|
|
if (buflen > INT_MAX) {
|
|
upsdebugx(3, "%s: requested to read too much (%zu), "
|
|
"reducing buflen to (INT_MAX-1)",
|
|
__func__, buflen);
|
|
buflen = (INT_MAX - 1);
|
|
}
|
|
|
|
for (i = 0; command[i].str; i++) {
|
|
|
|
int retry;
|
|
|
|
if (strcmp(cmd, command[i].str)) {
|
|
continue;
|
|
}
|
|
|
|
for (retry = 0; retry < 10; retry++) {
|
|
|
|
int ret;
|
|
|
|
if (langid_fix != -1) {
|
|
/* Apply langid_fix value */
|
|
ret = usb_get_string(udev,
|
|
command[i].index, langid_fix,
|
|
(usb_ctrl_charbuf)buf, buflen);
|
|
} else {
|
|
ret = usb_get_string_simple(udev,
|
|
command[i].index,
|
|
(usb_ctrl_charbuf)buf, buflen);
|
|
}
|
|
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "read: %s (%d)",
|
|
ret ? nut_usb_strerror(ret) : "timeout", ret);
|
|
return ret;
|
|
}
|
|
|
|
/* This may serve in the future */
|
|
upsdebugx(1, "received %d (%d)", ret, buf[0]);
|
|
|
|
if (langid_fix != -1) {
|
|
/* Limit this check, at least for now */
|
|
/* Invalid receive size - message corrupted */
|
|
if (ret != buf[0]) {
|
|
upsdebugx(1, "size mismatch: %d / %d", ret, buf[0]);
|
|
continue;
|
|
}
|
|
|
|
/* Simple unicode -> ASCII inplace conversion
|
|
* FIXME: this code is at least shared with mge-shut/libshut
|
|
* Create a common function? */
|
|
unsigned int di, si, size = (unsigned int)buf[0];
|
|
for (di = 0, si = 2; si < size; si += 2) {
|
|
|
|
if (di >= (buflen - 1))
|
|
break;
|
|
|
|
if (buf[si + 1]) /* high byte */
|
|
buf[di++] = '?';
|
|
else
|
|
buf[di++] = buf[si];
|
|
|
|
}
|
|
|
|
/* Note: effective range of di should be unsigned char */
|
|
buf[di] = 0;
|
|
ret = (int)di;
|
|
}
|
|
|
|
/* If the reply lacks the expected terminating CR, add it (if there's enough space) */
|
|
if (ret && memchr(buf, '\r', ret) == NULL) {
|
|
upsdebugx(4, "%s: the reply lacks the expected terminating CR.", __func__);
|
|
if ((size_t)ret < buflen - 1) {
|
|
upsdebugx(4, "%s: adding missing terminating CR.", __func__);
|
|
buf[ret++] = '\r';
|
|
buf[ret] = 0;
|
|
}
|
|
}
|
|
|
|
/* "UPS No Ack" has a special meaning */
|
|
if (
|
|
strcspn(buf, "\r") == 10 &&
|
|
!strncasecmp(buf, "UPS No Ack", 10)
|
|
) {
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(buf, "\r"), buf);
|
|
continue;
|
|
}
|
|
|
|
/* Replace the first byte of what we received with the correct one */
|
|
buf[0] = command[i].prefix;
|
|
|
|
upsdebug_hex(5, "read", buf, (size_t)ret);
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(buf, "\r"), buf);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
/* Echo the unknown command back */
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(cmd, "\r"), cmd);
|
|
return snprintf(buf, buflen, "%s", cmd);
|
|
}
|
|
|
|
/* Fabula communication subdriver */
|
|
static int fabula_command(const char *cmd, char *buf, size_t buflen)
|
|
{
|
|
const struct {
|
|
const char *str; /* Megatec command */
|
|
const int index; /* Fabula string index for this command */
|
|
} commands[] = {
|
|
{ "Q1\r", 0x03, }, /* Status */
|
|
{ "F\r", 0x0d, }, /* Ratings */
|
|
{ "I\r", 0x0c, }, /* Vendor infos */
|
|
{ "Q\r", 0x07, }, /* Beeper toggle */
|
|
{ "C\r", 0x0a, }, /* Cancel shutdown/Load on [0x(0..F)A]*/
|
|
{ NULL, 0 }
|
|
};
|
|
int i, ret, index = 0;
|
|
|
|
upsdebugx(3, "send: %.*s", (int)strcspn(cmd, "\r"), cmd);
|
|
|
|
if (buflen > INT_MAX) {
|
|
upsdebugx(3, "%s: requested to read too much (%zu), "
|
|
"reducing buflen to (INT_MAX-1)",
|
|
__func__, buflen);
|
|
buflen = (INT_MAX - 1);
|
|
}
|
|
|
|
for (i = 0; commands[i].str; i++) {
|
|
|
|
if (strcmp(cmd, commands[i].str))
|
|
continue;
|
|
|
|
index = commands[i].index;
|
|
break;
|
|
|
|
}
|
|
|
|
if (!index) {
|
|
|
|
int val2 = -1;
|
|
double val1 = -1;
|
|
|
|
/* Shutdowns */
|
|
if (
|
|
sscanf(cmd, "S%lfR%d\r", &val1, &val2) == 2 ||
|
|
sscanf(cmd, "S%lf\r", &val1) == 1
|
|
) {
|
|
|
|
double delay;
|
|
|
|
/* 0x(1+)0 -> shutdown.stayoff (SnR0000)
|
|
* 0x(1+)8 -> shutdown.return (Sn[Rm], m != 0)
|
|
* [delay before restart is always 10 seconds]
|
|
* +0x10 (16dec) = next megatec delay
|
|
* (min .5 = hex 0x1*; max 10 = hex 0xF*) -> n < 1 ? -> n += .1; n >= 1 ? -> n += 1 */
|
|
|
|
/* delay: [.5..10] (-> seconds: [30..600]) */
|
|
delay = val1 < .5 ? .5 : val1 > 10 ? 10 : val1;
|
|
|
|
if (delay < 1)
|
|
index = 16 + round((delay - .5) * 10) * 16;
|
|
else
|
|
index = 96 + (delay - 1) * 16;
|
|
|
|
/* shutdown.return (Sn[Rm], m != 0) */
|
|
if (val2)
|
|
index += 8;
|
|
|
|
/* Unknown commands */
|
|
} else {
|
|
|
|
/* Echo the unknown command back */
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(cmd, "\r"), cmd);
|
|
return snprintf(buf, buflen, "%s", cmd);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
upsdebugx(4, "command index: 0x%02x", index);
|
|
|
|
/* Send command/Read reply */
|
|
ret = usb_get_string_simple(udev, index, (usb_ctrl_charbuf)buf, buflen);
|
|
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "read: %s (%d)",
|
|
ret ? nut_usb_strerror(ret) : "timeout", ret);
|
|
return ret;
|
|
}
|
|
|
|
/* If the reply lacks the expected terminating CR, add it (if there's enough space) */
|
|
if (memchr(buf, '\r', ret) == NULL) {
|
|
upsdebugx(4, "%s: the reply lacks the expected terminating CR.", __func__);
|
|
if ((size_t)ret < buflen - 1) {
|
|
upsdebugx(4, "%s: adding missing terminating CR.", __func__);
|
|
buf[ret++] = '\r';
|
|
buf[ret] = 0;
|
|
}
|
|
}
|
|
|
|
upsdebug_hex(5, "read", buf, (size_t)ret);
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(buf, "\r"), buf);
|
|
|
|
/* The UPS always replies "UPS No Ack" when a supported command
|
|
* is issued (either if it fails or if it succeeds).. */
|
|
if (
|
|
strcspn(buf, "\r") == 10 &&
|
|
!strncasecmp(buf, "UPS No Ack", 10)
|
|
) {
|
|
/* ..because of that, always return 0 (with buf empty,
|
|
* as if it was a timeout): queries will see it as a failure,
|
|
* instant commands ('megatec' protocol) as a success */
|
|
memset(buf, 0, buflen);
|
|
return 0;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/* Hunnox communication subdriver, based on Fabula code above so repeats
|
|
* much of it currently. Possible future optimization is to refactor shared
|
|
* code into new routines to be called from both (or more) methods.*/
|
|
static int hunnox_command(const char *cmd, char *buf, size_t buflen)
|
|
{
|
|
/* The hunnox_patch was an argument in initial implementation of PR #638
|
|
* which added "hunnox" support; keeping it fixed here helps to visibly
|
|
* track the modifications compared to original fabula_command() e.g. to
|
|
* facilitate refactoring commented above, in the future.
|
|
*/
|
|
/* char hunnox_patch = 1; */
|
|
const struct {
|
|
const char *str; /* Megatec command */
|
|
const int index; /* Fabula string index for this command */
|
|
} commands[] = {
|
|
{ "Q1\r", 0x03, }, /* Status */
|
|
{ "F\r", 0x0d, }, /* Ratings */
|
|
{ "I\r", 0x0c, }, /* Vendor infos */
|
|
{ "Q\r", 0x07, }, /* Beeper toggle */
|
|
{ "C\r", 0x0a, }, /* Cancel shutdown/Load on [0x(0..F)A]*/
|
|
{ NULL, 0 }
|
|
};
|
|
int i, ret, index = 0;
|
|
|
|
upsdebugx(3, "send: %.*s", (int)strcspn(cmd, "\r"), cmd);
|
|
|
|
if (buflen > INT_MAX) {
|
|
upsdebugx(3, "%s: requested to read too much (%zu), "
|
|
"reducing buflen to (INT_MAX-1)",
|
|
__func__, buflen);
|
|
buflen = (INT_MAX - 1);
|
|
}
|
|
|
|
for (i = 0; commands[i].str; i++) {
|
|
|
|
if (strcmp(cmd, commands[i].str))
|
|
continue;
|
|
|
|
index = commands[i].index;
|
|
break;
|
|
|
|
}
|
|
|
|
if (!index) {
|
|
|
|
int val2 = -1;
|
|
double val1 = -1;
|
|
|
|
/* Shutdowns */
|
|
if (
|
|
sscanf(cmd, "S%lfR%d\r", &val1, &val2) == 2 ||
|
|
sscanf(cmd, "S%lf\r", &val1) == 1
|
|
) {
|
|
|
|
double delay;
|
|
|
|
/* 0x(1+)0 -> shutdown.stayoff (SnR0000)
|
|
* 0x(1+)8 -> shutdown.return (Sn[Rm], m != 0)
|
|
* [delay before restart is always 10 seconds]
|
|
* +0x10 (16dec) = next megatec delay
|
|
* (min .5 = hex 0x1*; max 10 = hex 0xF*) -> n < 1 ? -> n += .1; n >= 1 ? -> n += 1 */
|
|
|
|
/* delay: [.5..10] (-> seconds: [30..600]) */
|
|
delay = val1 < .5 ? .5 : val1 > 10 ? 10 : val1;
|
|
|
|
if (delay < 1)
|
|
index = 16 + round((delay - .5) * 10) * 16;
|
|
else
|
|
index = 96 + (delay - 1) * 16;
|
|
|
|
/* shutdown.return (Sn[Rm], m != 0) */
|
|
if (val2)
|
|
index += 8;
|
|
|
|
/* Unknown commands */
|
|
} else {
|
|
|
|
/* Echo the unknown command back */
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(cmd, "\r"), cmd);
|
|
return snprintf(buf, buflen, "%s", cmd);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
upsdebugx(4, "command index: 0x%02x", index);
|
|
|
|
/* if (hunnox_patch) { */
|
|
/* Enable lock-step protocol for Hunnox */
|
|
if (hunnox_protocol(index) != 0) {
|
|
return 0;
|
|
}
|
|
|
|
/* Seems that if we inform a large buffer, the USB locks.
|
|
* This value was captured from the Windows "official" client.
|
|
* Note this should not be a problem programmatically: it just
|
|
* means that the caller reserved a longer buffer that we need
|
|
* in practice to write a response into.
|
|
*/
|
|
if (buflen > 102) {
|
|
buflen = 102;
|
|
}
|
|
/* } */
|
|
|
|
/* Send command/Read reply */
|
|
if (langid_fix != -1) {
|
|
ret = usb_get_string(udev,
|
|
index, langid_fix, (usb_ctrl_charbuf)buf, buflen);
|
|
} else {
|
|
ret = usb_get_string_simple(udev,
|
|
index, (usb_ctrl_charbuf)buf, buflen);
|
|
}
|
|
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "read: %s (%d)",
|
|
ret ? nut_usb_strerror(ret) : "timeout",
|
|
ret);
|
|
return ret;
|
|
}
|
|
|
|
/* if (hunnox_patch) { */
|
|
if (langid_fix != -1) {
|
|
/* Limit this check, at least for now */
|
|
/* Invalid receive size - message corrupted */
|
|
if (ret != buf[0]) {
|
|
upsdebugx(1, "size mismatch: %d / %d", ret, buf[0]);
|
|
return 0;
|
|
}
|
|
|
|
/* Simple unicode -> ASCII inplace conversion
|
|
* FIXME: this code is at least shared with mge-shut/libshut
|
|
* Create a common function? */
|
|
unsigned int di, si, size = (unsigned int)buf[0];
|
|
for (di = 0, si = 2; si < size; si += 2) {
|
|
if (di >= (buflen - 1))
|
|
break;
|
|
|
|
if (buf[si + 1]) /* high byte */
|
|
buf[di++] = '?';
|
|
else
|
|
buf[di++] = buf[si];
|
|
}
|
|
|
|
/* Note: effective range of di should be unsigned char */
|
|
buf[di] = 0;
|
|
ret = (int)di;
|
|
}
|
|
/* } */
|
|
|
|
upsdebug_hex(5, "read", buf, (size_t)ret);
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(buf, "\r"), buf);
|
|
|
|
/* The UPS always replies "UPS No Ack" when a supported command
|
|
* is issued (either if it fails or if it succeeds).. */
|
|
if (
|
|
strcspn(buf, "\r") == 10 &&
|
|
!strncasecmp(buf, "UPS No Ack", 10)
|
|
) {
|
|
/* ..because of that, always return 0 (with buf empty,
|
|
* as if it was a timeout): queries will see it as a failure,
|
|
* instant commands ('megatec' protocol) as a success */
|
|
memset(buf, 0, buflen);
|
|
return 0;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/* Fuji communication subdriver */
|
|
static int fuji_command(const char *cmd, char *buf, size_t buflen)
|
|
{
|
|
unsigned char tmp[8];
|
|
char command[SMALLBUF] = "",
|
|
read[SMALLBUF] = "";
|
|
int ret, val2;
|
|
unsigned char answer_len;
|
|
double val1;
|
|
size_t i;
|
|
const struct {
|
|
const char *command; /* Megatec command */
|
|
const unsigned char answer_len; /* Expected length of the answer
|
|
* to the ongoing query */
|
|
} query[] = {
|
|
{ "Q1", 47 },
|
|
{ "F", 22 },
|
|
{ "I", 39 },
|
|
{ NULL, 0 }
|
|
};
|
|
|
|
if (buflen > INT_MAX) {
|
|
upsdebugx(3, "%s: requested to read too much (%zu), "
|
|
"reducing buflen to (INT_MAX-1)",
|
|
__func__, buflen);
|
|
buflen = (INT_MAX - 1);
|
|
}
|
|
|
|
/*
|
|
* Queries (b1..b8) sent (as a 8-bytes interrupt) to the UPS
|
|
* adopt the following scheme:
|
|
*
|
|
* b1: 0x80
|
|
* b2: 0x06
|
|
* b3: <LEN>
|
|
* b4: 0x03
|
|
* b5..bn: <COMMAND>
|
|
* bn+1..b7: [<PADDING>]
|
|
* b8: <ANSWER_LEN>
|
|
*
|
|
* Where:
|
|
* <LEN> Length (in Hex) of the command (without the trailing CR) + 1
|
|
* <COMMAND> Command/query (without the trailing CR)
|
|
* [<PADDING>] 0x00 padding to the 7th byte
|
|
* <ANSWER_LEN> Expected length (in Hex) of the answer to the ongoing
|
|
* query (0 when no reply is expected, i.e. commands)
|
|
*
|
|
* Replies to queries (commands are followed by action without
|
|
* any reply) are sent from the UPS (in 8-byte chunks) with
|
|
* 0x00 padding after the trailing CR to full 8 bytes.
|
|
*
|
|
*/
|
|
|
|
/* Send command */
|
|
|
|
/* Remove the CR */
|
|
snprintf(command, sizeof(command), "%.*s", (int)strcspn(cmd, "\r"), cmd);
|
|
|
|
/* Length of the command that will be sent to the UPS can be
|
|
* at most: 8 - 5 (0x80, 0x06, <LEN>, 0x03, <ANSWER_LEN>) = 3.
|
|
* As a consequence also 'SnRm' commands (shutdown.{return,stayoff}
|
|
* and load.off) are not supported.
|
|
* So, map all the 'SnRm' shutdown.returns (m != 0) as the
|
|
* corresponding 'Sn' commands, meanwhile ignoring ups.delay.start
|
|
* and making the UPS turn on the load as soon as power is back. */
|
|
if (sscanf(cmd, "S%lfR%d\r", &val1, &val2) == 2 && val2) {
|
|
upsdebugx(4, "%s: trimming '%s' to '%.*s'", __func__, command, 3, command);
|
|
command[3] = 0;
|
|
}
|
|
/* Too long command */
|
|
if (strlen(command) > 3) {
|
|
/* Be 'megatec-y': echo the unsupported command back */
|
|
upsdebugx(3, "%s: unsupported command %s", __func__, command);
|
|
return snprintf(buf, buflen, "%s", cmd);
|
|
}
|
|
|
|
/* Expected length of the answer to the ongoing query
|
|
* (0 when no reply is expected, i.e. commands) */
|
|
answer_len = 0;
|
|
for (i = 0; query[i].command; i++) {
|
|
|
|
if (strcmp(command, query[i].command))
|
|
continue;
|
|
|
|
answer_len = query[i].answer_len;
|
|
break;
|
|
|
|
}
|
|
|
|
memset(tmp, 0, sizeof(tmp));
|
|
|
|
/* 0x80 */
|
|
tmp[0] = 0x80;
|
|
/* 0x06 */
|
|
tmp[1] = 0x06;
|
|
/* <LEN>; per above under 3 */
|
|
tmp[2] = (unsigned char)strlen(command) + 1;
|
|
/* 0x03 */
|
|
tmp[3] = 0x03;
|
|
/* <COMMAND> */
|
|
memcpy(&tmp[4], command, strlen(command));
|
|
/* <ANSWER_LEN> */
|
|
tmp[7] = answer_len;
|
|
|
|
upsdebug_hex(4, "command", (char *)tmp, 8);
|
|
|
|
/* Write data */
|
|
ret = usb_interrupt_write(udev,
|
|
USB_ENDPOINT_OUT | 2,
|
|
(const usb_ctrl_charbuf)tmp,
|
|
8, USB_TIMEOUT);
|
|
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "send: %s (%d)",
|
|
ret ? nut_usb_strerror(ret) : "timeout", ret);
|
|
return ret;
|
|
}
|
|
|
|
upsdebugx(3, "send: %s", command);
|
|
|
|
/* Read reply */
|
|
|
|
memset(buf, 0, buflen);
|
|
|
|
for (i = 0; (i <= buflen - 8) && (memchr(buf, '\r', buflen) == NULL); i += (size_t)ret) {
|
|
|
|
/* Read data in 8-byte chunks */
|
|
ret = usb_interrupt_read(udev,
|
|
USB_ENDPOINT_IN | 1,
|
|
(usb_ctrl_charbuf)&buf[i], 8, 1000);
|
|
|
|
/* Any errors here mean that we are unable to read a reply
|
|
* (which will happen after successfully writing a command
|
|
* to the UPS) */
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "read: %s (%d)",
|
|
ret ? nut_usb_strerror(ret) : "timeout", ret);
|
|
return ret;
|
|
}
|
|
|
|
snprintf(read, sizeof(read), "read [%3d]", (int)i);
|
|
upsdebug_hex(5, read, &buf[i], (size_t)ret);
|
|
|
|
}
|
|
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(buf, "\r"), buf);
|
|
|
|
/* As Fuji units return the reply in 8-byte chunks always padded to the 8th byte with 0x00, we need to calculate and return the length of the actual response here. */
|
|
return (int)strlen(buf);
|
|
}
|
|
|
|
/* Phoenixtec (Masterguard) communication subdriver */
|
|
static int phoenixtec_command(const char *cmd, char *buf, size_t buflen)
|
|
{
|
|
int ret;
|
|
char *p, *e = NULL;
|
|
char *l[] = { "T", "TL", "S", "C", "CT", "M", "N", "O", "SRC", "FCLR", "SS", "TUD", "SSN", NULL }; /* commands that don't return an answer */
|
|
char **lp;
|
|
size_t cmdlen = strlen(cmd);
|
|
|
|
if (cmdlen > INT_MAX) {
|
|
upsdebugx(3, "%s: requested command is too long (%zu)",
|
|
__func__, cmdlen);
|
|
return 0;
|
|
}
|
|
|
|
if (buflen > INT_MAX) {
|
|
upsdebugx(3, "%s: requested to read too much (%zu), "
|
|
"reducing buflen to (INT_MAX-1)",
|
|
__func__, buflen);
|
|
buflen = (INT_MAX - 1);
|
|
}
|
|
|
|
if ((ret = usb_control_msg(udev,
|
|
USB_ENDPOINT_OUT | USB_TYPE_VENDOR | USB_RECIP_ENDPOINT,
|
|
0x0d, 0, 0, (usb_ctrl_charbuf)cmd, (int)cmdlen, 1000)) <= 0
|
|
) {
|
|
upsdebugx(3, "send: %s (%d)",
|
|
ret ? nut_usb_strerror(ret) : "timeout",
|
|
ret);
|
|
*buf = '\0';
|
|
return ret;
|
|
}
|
|
|
|
for (lp = l; *lp != NULL; lp++) {
|
|
const char *q;
|
|
int b;
|
|
|
|
p = *lp; q = cmd; b = 1;
|
|
while (*p != '\0') {
|
|
if (*p++ != *q++) {
|
|
b = 0;
|
|
break;
|
|
}
|
|
}
|
|
if (b && *q >= 'A' && *q <= 'Z') b = 0; /* "M" not to match "MSO" */
|
|
if (b) {
|
|
upsdebugx(4, "command %s returns no answer", *lp);
|
|
*buf = '\0';
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
for (p = buf; p < buf + buflen; p += ret) {
|
|
/* buflen constrained to INT_MAX above, so we can cast: */
|
|
if ((ret = usb_interrupt_read(udev,
|
|
USB_ENDPOINT_IN | 1,
|
|
(usb_ctrl_charbuf)p, (int)(buf + buflen - p), 1000)) <= 0
|
|
) {
|
|
upsdebugx(3, "read: %s (%d)",
|
|
ret ? nut_usb_strerror(ret) : "timeout",
|
|
ret);
|
|
*buf = '\0';
|
|
return ret;
|
|
}
|
|
if ((e = memchr(p, '\r', (size_t)ret)) != NULL) break;
|
|
}
|
|
if (e != NULL && ++e < buf + buflen) {
|
|
*e = '\0';
|
|
/* buflen constrained to INT_MAX above, so we can cast: */
|
|
return (int)(e - buf);
|
|
} else {
|
|
upsdebugx(3, "read: buflen %zu too small", buflen);
|
|
*buf = '\0';
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/* SNR communication subdriver */
|
|
static int snr_command(const char *cmd, char *buf, size_t buflen)
|
|
{
|
|
/*ATTENTION: This subdriver uses short buffer with length 102 byte*/
|
|
const struct {
|
|
const char *str; /* Megatec command */
|
|
const int index; /* String index for this command */
|
|
const char prefix; /* Character to replace the first byte in reply */
|
|
} command[] = {
|
|
{ "Q1\r", 0x03, '(' },
|
|
{ "F\r", 0x0d, '#' },
|
|
{ "I\r", 0x0c, '#' },
|
|
{ NULL, 0, '\0' }
|
|
};
|
|
|
|
int i;
|
|
|
|
upsdebugx(3, "send: %.*s", (int)strcspn(cmd, "\r"), cmd);
|
|
|
|
if (buflen > INT_MAX) {
|
|
upsdebugx(3, "%s: requested to read too much (%zu), "
|
|
"reducing buflen to (INT_MAX-1)",
|
|
__func__, buflen);
|
|
buflen = (INT_MAX - 1);
|
|
}
|
|
|
|
if (buflen < 102) {
|
|
upsdebugx(4, "size of buf less than 102 byte!");
|
|
return 0;
|
|
}
|
|
|
|
/* Prepare SNR-UPS for communication.
|
|
* Without the interrupt UPS returns zeros for some time,
|
|
* and afterwards NUT returns a communications error.
|
|
*/
|
|
usb_interrupt_read(udev,
|
|
0x81,
|
|
(usb_ctrl_charbuf)buf, 102, 1000);
|
|
|
|
for (i = 0; command[i].str; i++) {
|
|
|
|
int retry;
|
|
|
|
if (strcmp(cmd, command[i].str)) {
|
|
continue;
|
|
}
|
|
|
|
for (retry = 0; retry < 10; retry++) {
|
|
|
|
int ret;
|
|
|
|
ret = usb_get_string(udev,
|
|
command[i].index, langid_fix,
|
|
(usb_ctrl_charbuf)buf, 102);
|
|
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "read: %s (%d)",
|
|
ret ? nut_usb_strerror(ret) : "timeout",
|
|
ret);
|
|
return ret;
|
|
}
|
|
|
|
/* This may serve in the future */
|
|
upsdebugx(1, "received %d (%d)", ret, buf[0]);
|
|
|
|
|
|
if (ret != buf[0]) {
|
|
upsdebugx(1, "size mismatch: %d / %d", ret, buf[0]);
|
|
continue;
|
|
}
|
|
|
|
/* Simple unicode -> ASCII inplace conversion
|
|
* FIXME: this code is at least shared with mge-shut/libshut
|
|
* Create a common function? */
|
|
unsigned int di, si, size = (unsigned int)buf[0];
|
|
for (di = 0, si = 2; si < size; si += 2) {
|
|
|
|
if (di >= (buflen - 1))
|
|
break;
|
|
|
|
if (buf[si + 1]) /* high byte */
|
|
buf[di++] = '?';
|
|
else
|
|
buf[di++] = buf[si];
|
|
|
|
}
|
|
|
|
/* Note: effective range of di should be unsigned char */
|
|
buf[di] = 0;
|
|
ret = (int)di;
|
|
|
|
/* "UPS No Ack" has a special meaning */
|
|
if (
|
|
strcspn(buf, "\r") == 10 &&
|
|
!strncasecmp(buf, "UPS No Ack", 10)
|
|
) {
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(buf, "\r"), buf);
|
|
continue;
|
|
}
|
|
|
|
/* Replace the first byte of what we received with the correct one */
|
|
buf[0] = command[i].prefix;
|
|
|
|
upsdebug_hex(5, "read", buf, (size_t)ret);
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(buf, "\r"), buf);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
/* Echo the unknown command back */
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(cmd, "\r"), cmd);
|
|
return snprintf(buf, buflen, "%s", cmd);
|
|
}
|
|
|
|
static int ablerex_command(const char *cmd, char *buf, size_t buflen)
|
|
{
|
|
int iii;
|
|
int len;
|
|
int idx;
|
|
char tmp[64];
|
|
char tmpryy[64];
|
|
|
|
upsdebugx(3, "send: %.*s", (int)strcspn(cmd, "\r"), cmd);
|
|
|
|
if (buflen > INT_MAX) {
|
|
upsdebugx(3, "%s: requested to read too much (%zu), reducing buflen to (INT_MAX-1)",
|
|
__func__, buflen);
|
|
buflen = (INT_MAX - 1);
|
|
}
|
|
|
|
int retry;
|
|
|
|
for (retry = 0; retry < 3; retry++) {
|
|
int ret;
|
|
|
|
memset(buf, 0, buflen);
|
|
tmp[0] = 0x05;
|
|
tmp[1] = 0;
|
|
tmp[2] = 1 + (char)strcspn(cmd, "\r");
|
|
|
|
for (iii = 0 ; iii < tmp[2] ; iii++)
|
|
{
|
|
tmp[3+iii] = cmd[iii];
|
|
}
|
|
|
|
ret = usb_control_msg(udev,
|
|
0x21,
|
|
0x09, 0x305, 0,
|
|
(usb_ctrl_charbuf)tmp, 47, 1000);
|
|
|
|
upsdebugx(3, "R11 read: %s", ret ? nut_usb_strerror(ret) : "timeout");
|
|
|
|
usleep(500000);
|
|
tmpryy[0] = 0x05;
|
|
ret = usb_control_msg(udev,
|
|
0xA1,
|
|
0x01, 0x305, 0,
|
|
(usb_ctrl_charbuf)tmpryy, 47, 1000);
|
|
upsdebugx(3, "R2 read%d: %.*s", ret, ret, tmpryy);
|
|
|
|
len = 0;
|
|
for (idx = 0 ; idx < 47 ; idx++)
|
|
{
|
|
buf[idx] = tmpryy[idx];
|
|
if (tmpryy[idx] == '\r')
|
|
{
|
|
len = idx;
|
|
break;
|
|
}
|
|
}
|
|
upsdebugx(3, "R3 read%d: %.*s", len, len, tmpryy);
|
|
|
|
if (len > 0) {
|
|
len ++;
|
|
}
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "read: %s", ret ? nut_usb_strerror(ret) : "timeout");
|
|
return ret;
|
|
}
|
|
|
|
upsdebugx(1, "received %d (%d)", ret, buf[0]);
|
|
|
|
if ((!strcasecmp(cmd, "Q1\r")) && len != 47) continue;
|
|
if ((!strcasecmp(cmd, "I\r")) && len != 39) continue;
|
|
if ((!strcasecmp(cmd, "F\r")) && len != 22) continue;
|
|
if ((!strcasecmp(cmd, "Q5\r")) && len != 22)
|
|
{
|
|
buf[0] = '(';
|
|
for (idx = 1 ; idx < 47 ; idx++)
|
|
{
|
|
buf[idx] = 0;
|
|
}
|
|
upsdebugx(3, "read Q5 Fail...");
|
|
return 22;
|
|
}
|
|
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(buf, "\r"), buf);
|
|
return len;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void *ablerex_subdriver_fun(USBDevice_t *device)
|
|
{
|
|
NUT_UNUSED_VARIABLE(device);
|
|
|
|
subdriver_command = &ablerex_command;
|
|
return NULL;
|
|
}
|
|
|
|
/* Armac communication subdriver
|
|
*
|
|
* This reproduces a communication protocol used by an old PowerManagerII
|
|
* software, which doesn't seem to be Armac specific. The banner is: "2004
|
|
* Richcomm Technologies, Inc. Dec 27 2005 ver 1.1." Maybe other Richcomm UPSes
|
|
* would work with this - better than with the richcomm_usb driver.
|
|
*/
|
|
static int armac_command(const char *cmd, char *buf, size_t buflen)
|
|
{
|
|
char tmpbuf[6];
|
|
int ret = 0;
|
|
size_t i, bufpos;
|
|
const size_t cmdlen = strlen(cmd);
|
|
|
|
/* UPS ignores (doesn't echo back) unsupported commands which makes
|
|
* the initialization long. List commands tested to be unsupported:
|
|
*/
|
|
const char *unsupported[] = {
|
|
"QGS\r",
|
|
"QS\r",
|
|
"QPI\r",
|
|
"M\r",
|
|
"D\r",
|
|
NULL
|
|
};
|
|
|
|
for (i = 0; unsupported[i] != NULL; i++) {
|
|
if (strcmp(cmd, unsupported[i]) == 0) {
|
|
upsdebugx(2,
|
|
"armac: unsupported cmd: %.*s",
|
|
(int)strcspn(cmd, "\r"), cmd);
|
|
return snprintf(buf, buflen, "%s", cmd);
|
|
}
|
|
}
|
|
upsdebugx(4, "armac command %.*s", (int)strcspn(cmd, "\r"), cmd);
|
|
|
|
/* Send command to the UPS in 3-byte chunks. Most fit 1 chunk, except for eg.
|
|
* parameterized tests. */
|
|
for (i = 0; i < cmdlen;) {
|
|
const size_t bytes_to_send = (cmdlen <= (i + 3)) ? (cmdlen - i) : 3;
|
|
memset(tmpbuf, 0, sizeof(tmpbuf));
|
|
tmpbuf[0] = 0xa0 + bytes_to_send;
|
|
memcpy(tmpbuf + 1, cmd + i, bytes_to_send);
|
|
ret = usb_control_msg(udev,
|
|
USB_ENDPOINT_OUT + USB_TYPE_CLASS + USB_RECIP_INTERFACE,
|
|
0x09, 0x200, 0,
|
|
(usb_ctrl_charbuf)tmpbuf, 4, 5000);
|
|
i += bytes_to_send;
|
|
}
|
|
|
|
if (ret <= 0) {
|
|
upsdebugx(1,
|
|
"send control: %s (%d)",
|
|
ret ? nut_usb_strerror(ret) : "timeout",
|
|
ret);
|
|
return ret;
|
|
}
|
|
|
|
memset(buf, 0, buflen);
|
|
|
|
bufpos = 0;
|
|
while (bufpos + 6 < buflen) {
|
|
size_t bytes_available;
|
|
|
|
/* Read data in 6-byte chunks */
|
|
ret = usb_interrupt_read(udev,
|
|
0x81,
|
|
(usb_ctrl_charbuf)tmpbuf, 6, 1000);
|
|
|
|
/* Any errors here mean that we are unable to read a reply
|
|
* (which will happen after successfully writing a command
|
|
* to the UPS) */
|
|
if (ret != 6) {
|
|
upsdebugx(1,
|
|
"interrupt read error: %s (%d)",
|
|
ret ? nut_usb_strerror(ret) : "timeout",
|
|
ret);
|
|
return ret;
|
|
}
|
|
|
|
upsdebugx(4,
|
|
"read: ret %d buf %02hhx: %02hhx %02hhx %02hhx %02hhx %02hhx >%c%c%c%c%c<",
|
|
ret,
|
|
tmpbuf[0], tmpbuf[1], tmpbuf[2], tmpbuf[3], tmpbuf[4], tmpbuf[5],
|
|
tmpbuf[1], tmpbuf[2], tmpbuf[3], tmpbuf[4], tmpbuf[5]);
|
|
|
|
bytes_available = (unsigned char)tmpbuf[0] & 0x0f;
|
|
if (bytes_available == 0) {
|
|
/* End of transfer */
|
|
break;
|
|
}
|
|
|
|
memcpy(buf + bufpos, tmpbuf + 1, bytes_available);
|
|
bufpos += bytes_available;
|
|
|
|
if (bytes_available <= 2) {
|
|
/* Slow down, let the UPS buffer more bytes */
|
|
usleep(15000);
|
|
}
|
|
}
|
|
|
|
if (bufpos + 6 >= buflen) {
|
|
upsdebugx(2, "Protocol error, too much data read.");
|
|
return -1;
|
|
}
|
|
|
|
upsdebugx(3, "armac command %.*s response read: '%.*s'",
|
|
(int)strcspn(cmd, "\r"), cmd,
|
|
(int)strcspn(buf, "\r"), buf
|
|
);
|
|
|
|
return (int)bufpos;
|
|
}
|
|
|
|
|
|
static void *cypress_subdriver(USBDevice_t *device)
|
|
{
|
|
NUT_UNUSED_VARIABLE(device);
|
|
|
|
subdriver_command = &cypress_command;
|
|
return NULL;
|
|
}
|
|
|
|
static void *sgs_subdriver(USBDevice_t *device)
|
|
{
|
|
NUT_UNUSED_VARIABLE(device);
|
|
|
|
subdriver_command = &sgs_command;
|
|
return NULL;
|
|
}
|
|
|
|
static void *ippon_subdriver(USBDevice_t *device)
|
|
{
|
|
NUT_UNUSED_VARIABLE(device);
|
|
|
|
subdriver_command = &ippon_command;
|
|
return NULL;
|
|
}
|
|
|
|
static void *krauler_subdriver(USBDevice_t *device)
|
|
{
|
|
NUT_UNUSED_VARIABLE(device);
|
|
|
|
subdriver_command = &krauler_command;
|
|
return NULL;
|
|
}
|
|
|
|
static void *phoenix_subdriver(USBDevice_t *device)
|
|
{
|
|
NUT_UNUSED_VARIABLE(device);
|
|
|
|
subdriver_command = &phoenix_command;
|
|
return NULL;
|
|
}
|
|
|
|
static void *fabula_subdriver(USBDevice_t *device)
|
|
{
|
|
NUT_UNUSED_VARIABLE(device);
|
|
|
|
subdriver_command = &fabula_command;
|
|
return NULL;
|
|
}
|
|
|
|
static void *phoenixtec_subdriver(USBDevice_t *device)
|
|
{
|
|
NUT_UNUSED_VARIABLE(device);
|
|
|
|
subdriver_command = &phoenixtec_command;
|
|
return NULL;
|
|
}
|
|
|
|
/* Note: the "hunnox_subdriver" name is taken by the subdriver_t structure */
|
|
static void *fabula_hunnox_subdriver(USBDevice_t *device)
|
|
{
|
|
NUT_UNUSED_VARIABLE(device);
|
|
|
|
subdriver_command = &hunnox_command;
|
|
return NULL;
|
|
}
|
|
|
|
static void *fuji_subdriver(USBDevice_t *device)
|
|
{
|
|
NUT_UNUSED_VARIABLE(device);
|
|
|
|
subdriver_command = &fuji_command;
|
|
return NULL;
|
|
}
|
|
|
|
static void *snr_subdriver(USBDevice_t *device)
|
|
{
|
|
NUT_UNUSED_VARIABLE(device);
|
|
|
|
subdriver_command = &snr_command;
|
|
return NULL;
|
|
}
|
|
|
|
static void *armac_subdriver(USBDevice_t *device)
|
|
{
|
|
NUT_UNUSED_VARIABLE(device);
|
|
|
|
subdriver_command = &armac_command;
|
|
return NULL;
|
|
}
|
|
|
|
/* USB device match structure */
|
|
typedef struct {
|
|
const int vendorID; /* USB device's VendorID */
|
|
const int productID; /* USB device's ProductID */
|
|
const char *vendor; /* USB device's iManufacturer string */
|
|
const char *product; /* USB device's iProduct string */
|
|
void *(*fun)(USBDevice_t *); /* Handler for specific processing */
|
|
} qx_usb_device_id_t;
|
|
|
|
/* USB VendorID/ProductID/iManufacturer/iProduct match - note: rightmost comment is used for naming rules by tools/nut-usbinfo.pl */
|
|
static qx_usb_device_id_t qx_usb_id[] = {
|
|
{ USB_DEVICE(0x05b8, 0x0000), NULL, NULL, &cypress_subdriver }, /* Agiler UPS */
|
|
{ USB_DEVICE(0xffff, 0x0000), NULL, NULL, &ablerex_subdriver_fun }, /* Ablerex 625L USB (Note: earlier best-fit was "krauler_subdriver" before PR #1135) */
|
|
{ USB_DEVICE(0x1cb0, 0x0035), NULL, NULL, &krauler_subdriver }, /* Legrand Daker DK / DK Plus */
|
|
{ USB_DEVICE(0x0665, 0x5161), NULL, NULL, &cypress_subdriver }, /* Belkin F6C1200-UNV/Voltronic Power UPSes */
|
|
{ USB_DEVICE(0x06da, 0x0002), "Phoenixtec Power","USB Cable (V2.00)", &phoenixtec_subdriver },/* Masterguard A Series */
|
|
{ USB_DEVICE(0x06da, 0x0002), NULL, NULL, &cypress_subdriver }, /* Online Yunto YQ450 */
|
|
{ USB_DEVICE(0x06da, 0x0003), NULL, NULL, &ippon_subdriver }, /* Mustek Powermust */
|
|
{ USB_DEVICE(0x06da, 0x0004), NULL, NULL, &cypress_subdriver }, /* Phoenixtec Innova 3/1 T */
|
|
{ USB_DEVICE(0x06da, 0x0005), NULL, NULL, &cypress_subdriver }, /* Phoenixtec Innova RT */
|
|
{ USB_DEVICE(0x06da, 0x0201), NULL, NULL, &cypress_subdriver }, /* Phoenixtec Innova T */
|
|
{ USB_DEVICE(0x06da, 0x0601), NULL, NULL, &phoenix_subdriver }, /* Online Zinto A */
|
|
{ USB_DEVICE(0x0f03, 0x0001), NULL, NULL, &cypress_subdriver }, /* Unitek Alpha 1200Sx */
|
|
{ USB_DEVICE(0x14f0, 0x00c9), NULL, NULL, &phoenix_subdriver }, /* GE EP series */
|
|
{ USB_DEVICE(0x0483, 0x0035), NULL, NULL, &sgs_subdriver }, /* TS Shara UPSes; vendor ID 0x0483 is from ST Microelectronics - with product IDs delegated to different OEMs */
|
|
{ USB_DEVICE(0x0001, 0x0000), "MEC", "MEC0003", &fabula_subdriver }, /* Fideltronik/MEC LUPUS 500 USB */
|
|
{ USB_DEVICE(0x0001, 0x0000), NULL, "MEC0003", &fabula_hunnox_subdriver }, /* Hunnox HNX 850, reported to also help support Powercool and some other devices; closely related to fabula with tweaks */
|
|
{ USB_DEVICE(0x0001, 0x0000), "ATCL FOR UPS", "ATCL FOR UPS", &fuji_subdriver }, /* Fuji UPSes */
|
|
{ USB_DEVICE(0x0001, 0x0000), NULL, NULL, &krauler_subdriver }, /* Krauler UP-M500VA */
|
|
{ USB_DEVICE(0x0001, 0x0000), NULL, "MEC0003", &snr_subdriver }, /* SNR-UPS-LID-XXXX UPSes */
|
|
{ USB_DEVICE(0x0925, 0x1234), NULL, NULL, &armac_subdriver }, /* Armac UPS and maybe other richcomm-like or using old PowerManagerII software */
|
|
/* End of list */
|
|
{ -1, -1, NULL, NULL, NULL }
|
|
};
|
|
|
|
static int qx_is_usb_device_supported(qx_usb_device_id_t *usb_device_id_list, USBDevice_t *device)
|
|
{
|
|
int retval = NOT_SUPPORTED;
|
|
qx_usb_device_id_t *usbdev;
|
|
|
|
for (usbdev = usb_device_id_list; usbdev->vendorID != -1; usbdev++) {
|
|
|
|
if (usbdev->vendorID != device->VendorID)
|
|
continue;
|
|
|
|
/* Flag as possibly supported if we see a known vendor */
|
|
retval = POSSIBLY_SUPPORTED;
|
|
|
|
if (usbdev->productID != device->ProductID)
|
|
continue;
|
|
|
|
if (usbdev->vendor
|
|
&& (!device->Vendor || strcasecmp(usbdev->vendor, device->Vendor))
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (usbdev->product
|
|
&& (!device->Product || strcasecmp(usbdev->product, device->Product))
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
/* Call the specific handler, if it exists */
|
|
if (usbdev->fun != NULL)
|
|
(*usbdev->fun)(device);
|
|
|
|
return SUPPORTED;
|
|
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
static int device_match_func(USBDevice_t *hd, void *privdata)
|
|
{
|
|
NUT_UNUSED_VARIABLE(privdata);
|
|
|
|
if (subdriver_command) {
|
|
return 1;
|
|
}
|
|
|
|
switch (qx_is_usb_device_supported(qx_usb_id, hd))
|
|
{
|
|
case SUPPORTED:
|
|
return 1;
|
|
|
|
case POSSIBLY_SUPPORTED:
|
|
case NOT_SUPPORTED:
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static USBDeviceMatcher_t device_matcher = {
|
|
&device_match_func,
|
|
NULL,
|
|
NULL
|
|
};
|
|
#endif /* QX_USB && !TESTING */
|
|
|
|
|
|
/* == Driver functions implementations == */
|
|
|
|
/* See header file for details. */
|
|
int instcmd(const char *cmdname, const char *extradata)
|
|
{
|
|
item_t *item;
|
|
char value[SMALLBUF];
|
|
|
|
if (!strcasecmp(cmdname, "beeper.off")) {
|
|
/* Compatibility mode for old command */
|
|
upslogx(LOG_WARNING,
|
|
"The 'beeper.off' command has been renamed to 'beeper.disable'");
|
|
return instcmd("beeper.disable", NULL);
|
|
}
|
|
|
|
if (!strcasecmp(cmdname, "beeper.on")) {
|
|
/* Compatibility mode for old command */
|
|
upslogx(LOG_WARNING,
|
|
"The 'beeper.on' command has been renamed to 'beeper.enable'");
|
|
return instcmd("beeper.enable", NULL);
|
|
}
|
|
|
|
upslogx(LOG_INFO, "%s(%s, %s)",
|
|
__func__, cmdname,
|
|
extradata ? extradata : "[NULL]");
|
|
|
|
/* Retrieve item by command name */
|
|
item = find_nut_info(cmdname, QX_FLAG_CMD, QX_FLAG_SKIP);
|
|
|
|
/* Check for fallback if not found */
|
|
if (item == NULL) {
|
|
|
|
if (!strcasecmp(cmdname, "load.on")) {
|
|
return instcmd("load.on.delay", "0");
|
|
}
|
|
|
|
if (!strcasecmp(cmdname, "load.off")) {
|
|
return instcmd("load.off.delay", "0");
|
|
}
|
|
|
|
if (!strcasecmp(cmdname, "shutdown.return")) {
|
|
|
|
int ret;
|
|
|
|
/* Ensure "ups.start.auto" is set to "yes", if supported */
|
|
if (dstate_getinfo("ups.start.auto")) {
|
|
if (setvar("ups.start.auto", "yes") != STAT_SET_HANDLED) {
|
|
upslogx(LOG_ERR, "%s: FAILED", __func__);
|
|
return STAT_INSTCMD_FAILED;
|
|
}
|
|
}
|
|
|
|
ret = instcmd("load.on.delay", dstate_getinfo("ups.delay.start"));
|
|
if (ret != STAT_INSTCMD_HANDLED) {
|
|
return ret;
|
|
}
|
|
|
|
return instcmd("load.off.delay",
|
|
dstate_getinfo("ups.delay.shutdown"));
|
|
|
|
}
|
|
|
|
if (!strcasecmp(cmdname, "shutdown.stayoff")) {
|
|
|
|
int ret;
|
|
|
|
/* Ensure "ups.start.auto" is set to "no", if supported */
|
|
if (dstate_getinfo("ups.start.auto")) {
|
|
if (setvar("ups.start.auto", "no") != STAT_SET_HANDLED) {
|
|
upslogx(LOG_ERR, "%s: FAILED", __func__);
|
|
return STAT_INSTCMD_FAILED;
|
|
}
|
|
}
|
|
|
|
ret = instcmd("load.on.delay", "-1");
|
|
if (ret != STAT_INSTCMD_HANDLED) {
|
|
return ret;
|
|
}
|
|
|
|
return instcmd("load.off.delay",
|
|
dstate_getinfo("ups.delay.shutdown"));
|
|
|
|
}
|
|
|
|
upsdebugx(2, "%s: command %s unavailable", __func__, cmdname);
|
|
return STAT_INSTCMD_INVALID;
|
|
}
|
|
|
|
/* If extradata is empty, use the default value
|
|
* from the QX to NUT table, if any */
|
|
extradata = extradata ? extradata : item->dfl;
|
|
snprintf(value, sizeof(value), "%s", extradata ? extradata : "");
|
|
|
|
/* Preprocess command */
|
|
if (item->preprocess != NULL
|
|
&& item->preprocess(item, value, sizeof(value))
|
|
) {
|
|
/* Something went wrong */
|
|
upslogx(LOG_ERR, "%s: FAILED", __func__);
|
|
return STAT_INSTCMD_FAILED;
|
|
}
|
|
|
|
/* No preprocess function -> nothing to do with extradata */
|
|
if (item->preprocess == NULL)
|
|
snprintf(value, sizeof(value), "%s", "");
|
|
|
|
/* Send the command, get the reply */
|
|
if (qx_process(item, strlen(value) > 0 ? value : NULL)) {
|
|
/* Something went wrong */
|
|
upslogx(LOG_ERR, "%s: FAILED", __func__);
|
|
return STAT_INSTCMD_FAILED;
|
|
}
|
|
|
|
/* We got a reply from the UPS:
|
|
* either subdriver->accepted (-> command handled)
|
|
* or the command itself echoed back (-> command failed)
|
|
*/
|
|
if (strlen(item->value) > 0) {
|
|
|
|
if (subdriver->accepted != NULL
|
|
&& !strcasecmp(item->value, subdriver->accepted)
|
|
) {
|
|
upslogx(LOG_INFO, "%s: SUCCEED", __func__);
|
|
/* Set the status so that SEMI_STATIC vars are polled */
|
|
data_has_changed = TRUE;
|
|
return STAT_INSTCMD_HANDLED;
|
|
}
|
|
|
|
upslogx(LOG_ERR, "%s: FAILED", __func__);
|
|
return STAT_INSTCMD_FAILED;
|
|
|
|
}
|
|
|
|
/* No reply from the UPS -> command handled */
|
|
upslogx(LOG_INFO, "%s: SUCCEED", __func__);
|
|
/* Set the status so that SEMI_STATIC vars are polled */
|
|
data_has_changed = TRUE;
|
|
return STAT_INSTCMD_HANDLED;
|
|
}
|
|
|
|
/* See header file for details. */
|
|
int setvar(const char *varname, const char *val)
|
|
{
|
|
item_t *item;
|
|
char value[SMALLBUF];
|
|
st_tree_t *root = (st_tree_t *)dstate_getroot();
|
|
int ok = 0;
|
|
|
|
/* Retrieve variable */
|
|
item = find_nut_info(varname, QX_FLAG_SETVAR, QX_FLAG_SKIP);
|
|
|
|
if (item == NULL) {
|
|
upsdebugx(2, "%s: element %s unavailable", __func__, varname);
|
|
return STAT_SET_UNKNOWN;
|
|
}
|
|
|
|
/* No NUT variable is available for this item, so we're handling
|
|
* a one-time setvar from ups.conf */
|
|
if (item->qxflags & QX_FLAG_NONUT) {
|
|
|
|
const char *userval;
|
|
|
|
/* Nothing to do */
|
|
if (!testvar(item->info_type)) {
|
|
upsdebugx(2, "%s: nothing to do... [%s]",
|
|
__func__, item->info_type);
|
|
return STAT_SET_HANDLED;
|
|
}
|
|
|
|
userval = getval(item->info_type);
|
|
|
|
upslogx(LOG_INFO, "%s(%s, %s)",
|
|
__func__, varname,
|
|
userval ? userval : "[NULL]");
|
|
|
|
snprintf(value, sizeof(value), "%s", userval ? userval : "");
|
|
|
|
/* This item is available in NUT */
|
|
} else {
|
|
|
|
upslogx(LOG_INFO, "%s(%s, %s)",
|
|
__func__, varname,
|
|
strlen(val) ? val : "[NULL]");
|
|
|
|
if (!strlen(val)) {
|
|
upslogx(LOG_ERR, "%s: value not given for %s",
|
|
__func__, item->info_type);
|
|
return STAT_SET_UNKNOWN; /* TODO: HANDLED but FAILED, not UNKNOWN! */
|
|
}
|
|
|
|
snprintf(value, sizeof(value), "%s", val);
|
|
|
|
/* Nothing to do */
|
|
if (!strcasecmp(dstate_getinfo(item->info_type), value)) {
|
|
upslogx(LOG_INFO, "%s: nothing to do... [%s]",
|
|
__func__, item->info_type);
|
|
return STAT_SET_HANDLED;
|
|
}
|
|
|
|
}
|
|
|
|
/* Check if given value is in the range of accepted values (range) */
|
|
if (item->qxflags & QX_FLAG_RANGE) {
|
|
|
|
long valuetoset, min, max;
|
|
|
|
if (strspn(value, "0123456789 .") != strlen(value)) {
|
|
upslogx(LOG_ERR, "%s: non numerical value [%s: %s]",
|
|
__func__, item->info_type, value);
|
|
return STAT_SET_UNKNOWN; /* TODO: HANDLED but FAILED, not UNKNOWN! */
|
|
}
|
|
|
|
valuetoset = strtol(value, NULL, 10);
|
|
|
|
/* No NUT var is available for this item, so
|
|
* take its range from qx2nut table */
|
|
if (item->qxflags & QX_FLAG_NONUT) {
|
|
|
|
info_rw_t *rvalue;
|
|
|
|
if (!strlen(value)) {
|
|
upslogx(LOG_ERR, "%s: value not given for %s",
|
|
__func__, item->info_type);
|
|
return STAT_SET_UNKNOWN; /* TODO: HANDLED but FAILED, not UNKNOWN! */
|
|
}
|
|
|
|
min = max = -1;
|
|
|
|
/* Loop on all existing values */
|
|
for (rvalue = item->info_rw; rvalue != NULL && strlen(rvalue->value) > 0; rvalue++) {
|
|
|
|
if (rvalue->preprocess
|
|
&& rvalue->preprocess(rvalue->value, sizeof(rvalue->value))
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (min < 0) {
|
|
min = strtol(rvalue->value, NULL, 10);
|
|
continue;
|
|
}
|
|
|
|
max = strtol(rvalue->value, NULL, 10);
|
|
|
|
/* valuetoset is in the range */
|
|
if (min <= valuetoset && valuetoset <= max) {
|
|
ok = 1;
|
|
break;
|
|
}
|
|
|
|
min = -1;
|
|
max = -1;
|
|
|
|
}
|
|
|
|
/* We have a NUT var for this item, so check given value
|
|
* against the already set range */
|
|
} else {
|
|
|
|
const range_t *range = state_getrangelist(root, item->info_type);
|
|
|
|
/* Unable to find tree node for var */
|
|
if (!range) {
|
|
upsdebugx(2, "%s: unable to find tree node for %s",
|
|
__func__, item->info_type);
|
|
return STAT_SET_UNKNOWN;
|
|
}
|
|
|
|
while (range) {
|
|
|
|
min = range->min;
|
|
max = range->max;
|
|
|
|
/* valuetoset is in the range */
|
|
if (min <= valuetoset && valuetoset <= max) {
|
|
ok = 1;
|
|
break;
|
|
}
|
|
range = range->next;
|
|
}
|
|
|
|
}
|
|
|
|
if (!ok) {
|
|
upslogx(LOG_ERR, "%s: value out of range [%s: %s]",
|
|
__func__, item->info_type, value);
|
|
return STAT_SET_UNKNOWN; /* TODO: HANDLED but FAILED, not UNKNOWN! */
|
|
}
|
|
|
|
/* Check if given value is in the range of accepted values (enum) */
|
|
} else if (item->qxflags & QX_FLAG_ENUM) {
|
|
|
|
/* No NUT var is available for this item, so
|
|
* take its range from qx2nut table */
|
|
if (item->qxflags & QX_FLAG_NONUT) {
|
|
|
|
info_rw_t *envalue;
|
|
|
|
if (!strlen(value)) {
|
|
upslogx(LOG_ERR, "%s: value not given for %s",
|
|
__func__, item->info_type);
|
|
return STAT_SET_UNKNOWN; /* TODO: HANDLED but FAILED, not UNKNOWN! */
|
|
}
|
|
|
|
/* Loop on all existing values */
|
|
for (envalue = item->info_rw; envalue != NULL && strlen(envalue->value) > 0; envalue++) {
|
|
|
|
if (envalue->preprocess
|
|
&& envalue->preprocess(envalue->value, sizeof(envalue->value))
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (strcasecmp(envalue->value, value))
|
|
continue;
|
|
|
|
/* value found */
|
|
ok = 1;
|
|
break;
|
|
|
|
}
|
|
|
|
/* We have a NUT var for this item, so check given value
|
|
* against the already set range */
|
|
} else {
|
|
|
|
const enum_t *enumlist = state_getenumlist(root, item->info_type);
|
|
|
|
/* Unable to find tree node for var */
|
|
if (!enumlist) {
|
|
upsdebugx(2, "%s: unable to find tree node for %s",
|
|
__func__, item->info_type);
|
|
return STAT_SET_UNKNOWN;
|
|
}
|
|
|
|
while (enumlist) {
|
|
|
|
/* If this is not the right value, go on to the next */
|
|
if (strcasecmp(enumlist->val, value)) {
|
|
enumlist = enumlist->next;
|
|
continue;
|
|
}
|
|
|
|
/* value found in enumlist */
|
|
ok = 1;
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
if (!ok) {
|
|
upslogx(LOG_ERR, "%s: value out of range [%s: %s]",
|
|
__func__, item->info_type, value);
|
|
return STAT_SET_UNKNOWN; /* TODO: HANDLED but FAILED, not UNKNOWN! */
|
|
}
|
|
|
|
/* Check if given value is not too long (string) */
|
|
} else if (item->info_flags & ST_FLAG_STRING) {
|
|
|
|
const long aux = state_getaux(root, item->info_type);
|
|
|
|
/* Unable to find tree node for var */
|
|
if (aux < 0) {
|
|
upsdebugx(2, "%s: unable to find tree node for %s",
|
|
__func__, item->info_type);
|
|
return STAT_SET_UNKNOWN;
|
|
}
|
|
|
|
/* FIXME? Should this cast to "long"?
|
|
* An int-size string is quite a lot already,
|
|
* even on architectures with a moderate INTMAX
|
|
*/
|
|
if (aux < (int)strlen(value)) {
|
|
upslogx(LOG_ERR, "%s: value is too long [%s: %s]",
|
|
__func__, item->info_type, value);
|
|
return STAT_SET_UNKNOWN; /* TODO: HANDLED but FAILED, not UNKNOWN! */
|
|
}
|
|
|
|
}
|
|
|
|
/* Preprocess value: from NUT-compliant to UPS-compliant */
|
|
if (item->preprocess != NULL
|
|
&& item->preprocess(item, value, sizeof(value))
|
|
) {
|
|
/* Something went wrong */
|
|
upslogx(LOG_ERR, "%s: FAILED", __func__);
|
|
return STAT_SET_UNKNOWN; /* TODO: HANDLED but FAILED, not UNKNOWN! */
|
|
}
|
|
|
|
/* Handle server side variable */
|
|
if (item->qxflags & QX_FLAG_ABSENT) {
|
|
upsdebugx(2, "%s: setting server side variable %s",
|
|
__func__, item->info_type);
|
|
dstate_setinfo(item->info_type, "%s", value);
|
|
upslogx(LOG_INFO, "%s: SUCCEED", __func__);
|
|
return STAT_SET_HANDLED;
|
|
}
|
|
|
|
/* No preprocess function -> nothing to do with val */
|
|
if (item->preprocess == NULL)
|
|
snprintf(value, sizeof(value), "%s", "");
|
|
|
|
/* Actual variable setting */
|
|
if (qx_process(item, strlen(value) > 0 ? value : NULL)) {
|
|
/* Something went wrong */
|
|
upslogx(LOG_ERR, "%s: FAILED", __func__);
|
|
return STAT_SET_UNKNOWN; /* TODO: HANDLED but FAILED, not UNKNOWN! */
|
|
}
|
|
|
|
/* We got a reply from the UPS:
|
|
* either subdriver->accepted (-> command handled)
|
|
* or the command itself echoed back (-> command failed) */
|
|
if (strlen(item->value) > 0) {
|
|
|
|
if (subdriver->accepted != NULL
|
|
&& !strcasecmp(item->value, subdriver->accepted)
|
|
) {
|
|
upslogx(LOG_INFO, "%s: SUCCEED", __func__);
|
|
/* Set the status so that SEMI_STATIC vars are polled */
|
|
data_has_changed = TRUE;
|
|
return STAT_SET_HANDLED;
|
|
}
|
|
|
|
upslogx(LOG_ERR, "%s: FAILED", __func__);
|
|
return STAT_SET_UNKNOWN; /* TODO: HANDLED but FAILED, not UNKNOWN! */
|
|
|
|
}
|
|
|
|
/* No reply from the UPS -> command handled */
|
|
upslogx(LOG_INFO, "%s: SUCCEED", __func__);
|
|
/* Set the status so that SEMI_STATIC vars are polled */
|
|
data_has_changed = TRUE;
|
|
return STAT_SET_HANDLED;
|
|
}
|
|
|
|
/* Try to shutdown the UPS */
|
|
void upsdrv_shutdown(void)
|
|
__attribute__((noreturn));
|
|
|
|
void upsdrv_shutdown(void)
|
|
{
|
|
int retry;
|
|
item_t *item;
|
|
const char *val;
|
|
|
|
upsdebugx(1, "%s...", __func__);
|
|
|
|
/* Get user-defined delays */
|
|
|
|
/* Start delay */
|
|
item = find_nut_info("ups.delay.start", 0, QX_FLAG_SKIP);
|
|
|
|
/* Don't know what happened */
|
|
if (!item)
|
|
fatalx(EXIT_FAILURE, "Unable to set start delay");
|
|
|
|
/* Set the default value */
|
|
dstate_setinfo(item->info_type, "%s", item->dfl);
|
|
|
|
/* Set var flags/range/enum */
|
|
qx_set_var(item);
|
|
|
|
/* Retrieve user defined delay settings */
|
|
val = getval(QX_VAR_ONDELAY);
|
|
|
|
if (val && setvar(item->info_type, val) != STAT_SET_HANDLED) {
|
|
fatalx(EXIT_FAILURE, "Start delay '%s' out of range", val);
|
|
}
|
|
|
|
/* Shutdown delay */
|
|
item = find_nut_info("ups.delay.shutdown", 0, QX_FLAG_SKIP);
|
|
|
|
/* Don't know what happened */
|
|
if (!item)
|
|
fatalx(EXIT_FAILURE, "Unable to set shutdown delay");
|
|
|
|
/* Set the default value */
|
|
dstate_setinfo(item->info_type, "%s", item->dfl);
|
|
|
|
/* Set var flags/range/enum */
|
|
qx_set_var(item);
|
|
|
|
/* Retrieve user defined delay settings */
|
|
val = getval(QX_VAR_OFFDELAY);
|
|
|
|
if (val && setvar(item->info_type, val) != STAT_SET_HANDLED) {
|
|
fatalx(EXIT_FAILURE, "Shutdown delay '%s' out of range", val);
|
|
}
|
|
|
|
/* Stop pending shutdowns */
|
|
if (find_nut_info("shutdown.stop", QX_FLAG_CMD, QX_FLAG_SKIP)) {
|
|
|
|
for (retry = 1; retry <= MAXTRIES; retry++) {
|
|
|
|
if (instcmd("shutdown.stop", NULL) != STAT_INSTCMD_HANDLED) {
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (retry > MAXTRIES) {
|
|
upslogx(LOG_NOTICE, "No shutdown pending");
|
|
}
|
|
|
|
}
|
|
|
|
/* Shutdown */
|
|
for (retry = 1; retry <= MAXTRIES; retry++) {
|
|
|
|
if (testvar("stayoff")) {
|
|
|
|
if (instcmd("shutdown.stayoff", NULL) != STAT_INSTCMD_HANDLED) {
|
|
continue;
|
|
}
|
|
|
|
} else {
|
|
|
|
if (instcmd("shutdown.return", NULL) != STAT_INSTCMD_HANDLED) {
|
|
continue;
|
|
}
|
|
|
|
}
|
|
|
|
fatalx(EXIT_SUCCESS, "Shutting down in %s seconds",
|
|
dstate_getinfo("ups.delay.shutdown"));
|
|
}
|
|
|
|
fatalx(EXIT_FAILURE, "Shutdown failed!");
|
|
}
|
|
|
|
#ifdef QX_USB
|
|
#ifndef TESTING
|
|
static const struct {
|
|
const char *name;
|
|
int (*command)(const char *cmd, char *buf, size_t buflen);
|
|
} usbsubdriver[] = {
|
|
{ "cypress", &cypress_command },
|
|
{ "phoenixtec", &phoenixtec_command },
|
|
{ "phoenix", &phoenix_command },
|
|
{ "ippon", &ippon_command },
|
|
{ "krauler", &krauler_command },
|
|
{ "fabula", &fabula_command },
|
|
{ "hunnox", &hunnox_command },
|
|
{ "fuji", &fuji_command },
|
|
{ "sgs", &sgs_command },
|
|
{ "snr", &snr_command },
|
|
{ "ablerex", &ablerex_command },
|
|
{ "armac", &armac_command },
|
|
{ NULL, NULL }
|
|
};
|
|
#endif
|
|
#endif
|
|
|
|
|
|
void upsdrv_help(void)
|
|
{
|
|
#ifdef QX_USB
|
|
#ifndef TESTING
|
|
printf("\nAcceptable values for 'subdriver' via -x or ups.conf in this driver: ");
|
|
size_t i;
|
|
|
|
for (i = 0; usbsubdriver[i].name != NULL; i++) {
|
|
if (i>0)
|
|
printf(", ");
|
|
printf("%s", usbsubdriver[i].name);
|
|
}
|
|
printf("\n\n");
|
|
#endif
|
|
#endif
|
|
|
|
printf("Read The Fine Manual ('man 8 nutdrv_qx')\n");
|
|
}
|
|
|
|
/* Adding flags/vars */
|
|
void upsdrv_makevartable(void)
|
|
{
|
|
char temp[SMALLBUF];
|
|
int i;
|
|
|
|
upsdebugx(1, "%s...", __func__);
|
|
|
|
snprintf(temp, sizeof(temp),
|
|
"Set shutdown delay, in seconds (default=%s)", DEFAULT_OFFDELAY);
|
|
addvar(VAR_VALUE, QX_VAR_OFFDELAY, temp);
|
|
|
|
snprintf(temp, sizeof(temp),
|
|
"Set startup delay, in seconds (default=%s)", DEFAULT_ONDELAY);
|
|
addvar(VAR_VALUE, QX_VAR_ONDELAY, temp);
|
|
|
|
addvar(VAR_FLAG, "stayoff",
|
|
"If invoked the UPS won't return after a shutdown when FSD arises");
|
|
|
|
snprintf(temp, sizeof(temp),
|
|
"Set polling frequency, in seconds, to reduce data flow (default=%d)",
|
|
DEFAULT_POLLFREQ);
|
|
addvar(VAR_VALUE, QX_VAR_POLLFREQ, temp);
|
|
|
|
addvar(VAR_VALUE, "protocol",
|
|
"Preselect communication protocol (skip autodetection)");
|
|
|
|
/* battery.{charge,runtime} guesstimation */
|
|
addvar(VAR_VALUE, "runtimecal",
|
|
"Parameters used for runtime calculation");
|
|
addvar(VAR_VALUE, "chargetime",
|
|
"Nominal charge time for UPS battery");
|
|
addvar(VAR_VALUE, "idleload",
|
|
"Minimum load to be used for runtime calculation");
|
|
|
|
#ifdef QX_USB
|
|
addvar(VAR_VALUE, "subdriver", "Serial-over-USB subdriver selection");
|
|
/* allow -x vendor=X, vendorid=X, product=X, productid=X, serial=X */
|
|
nut_usb_addvars();
|
|
|
|
addvar(VAR_VALUE, "langid_fix",
|
|
"Apply the language ID workaround to the krauler subdriver "
|
|
"(0x409 or 0x4095)");
|
|
addvar(VAR_FLAG, "noscanlangid", "Don't autoscan valid range for langid");
|
|
#endif /* QX_USB */
|
|
|
|
#ifdef QX_SERIAL
|
|
addvar(VAR_VALUE, "cablepower", "Set cable power for serial interface");
|
|
#endif /* QX_SERIAL */
|
|
|
|
/* Subdrivers flags/vars */
|
|
for (i = 0; subdriver_list[i] != NULL; i++) {
|
|
|
|
if (subdriver_list[i]->makevartable != NULL)
|
|
subdriver_list[i]->makevartable();
|
|
|
|
}
|
|
}
|
|
|
|
/* Update UPS status/infos */
|
|
void upsdrv_updateinfo(void)
|
|
{
|
|
time_t now;
|
|
static int retry = 0;
|
|
|
|
upsdebugx(1, "%s...", __func__);
|
|
|
|
time(&now);
|
|
|
|
/* Clear status buffer before beginning */
|
|
status_init();
|
|
|
|
/* Do a full update (polling) every pollfreq or upon data change
|
|
* (i.e. setvar/instcmd) */
|
|
if ((now > (lastpoll + pollfreq)) || (data_has_changed == TRUE)) {
|
|
|
|
upsdebugx(1, "Full update...");
|
|
|
|
/* Clear ups_status */
|
|
ups_status = 0;
|
|
|
|
alarm_init();
|
|
|
|
if (qx_ups_walk(QX_WALKMODE_FULL_UPDATE) == FALSE) {
|
|
|
|
if (retry < MAXTRIES || retry == MAXTRIES) {
|
|
upsdebugx(1,
|
|
"Communications with the UPS lost: status read failed!");
|
|
retry++;
|
|
} else {
|
|
dstate_datastale();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
lastpoll = now;
|
|
data_has_changed = FALSE;
|
|
|
|
ups_alarm_set();
|
|
alarm_commit();
|
|
|
|
} else {
|
|
|
|
upsdebugx(1, "Quick update...");
|
|
|
|
/* Quick poll data only to see if the UPS is still connected */
|
|
if (qx_ups_walk(QX_WALKMODE_QUICK_UPDATE) == FALSE) {
|
|
|
|
if (retry < MAXTRIES || retry == MAXTRIES) {
|
|
upsdebugx(1,
|
|
"Communications with the UPS lost: status read failed!");
|
|
retry++;
|
|
} else {
|
|
dstate_datastale();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
}
|
|
|
|
ups_status_set();
|
|
status_commit();
|
|
|
|
if (retry > MAXTRIES) {
|
|
upslogx(LOG_NOTICE, "Communications with the UPS re-established");
|
|
}
|
|
|
|
retry = 0;
|
|
|
|
dstate_dataok();
|
|
}
|
|
|
|
/* Initialise data from UPS */
|
|
void upsdrv_initinfo(void)
|
|
{
|
|
char *val;
|
|
|
|
upsdebugx(1, "%s...", __func__);
|
|
|
|
dstate_setinfo("driver.version.data", "%s", subdriver->name);
|
|
|
|
/* Initialise data */
|
|
if (qx_ups_walk(QX_WALKMODE_INIT) == FALSE) {
|
|
fatalx(EXIT_FAILURE, "Can't initialise data from the UPS");
|
|
}
|
|
|
|
/* Init battery guesstimation */
|
|
qx_initbattery();
|
|
|
|
if (dstate_getinfo("ups.delay.start")) {
|
|
|
|
/* Retrieve user defined delay settings */
|
|
val = getval(QX_VAR_ONDELAY);
|
|
|
|
if (val && setvar("ups.delay.start", val) != STAT_SET_HANDLED) {
|
|
fatalx(EXIT_FAILURE, "Start delay '%s' out of range", val);
|
|
}
|
|
|
|
}
|
|
|
|
if (dstate_getinfo("ups.delay.shutdown")) {
|
|
|
|
/* Retrieve user defined delay settings */
|
|
val = getval(QX_VAR_OFFDELAY);
|
|
|
|
if (val && setvar("ups.delay.shutdown", val) != STAT_SET_HANDLED) {
|
|
fatalx(EXIT_FAILURE, "Shutdown delay '%s' out of range", val);
|
|
}
|
|
|
|
}
|
|
|
|
if (!find_nut_info("load.off", QX_FLAG_CMD, QX_FLAG_SKIP)
|
|
&& find_nut_info("load.off.delay", QX_FLAG_CMD, QX_FLAG_SKIP)
|
|
) {
|
|
/* Adds default with a delay value of '0' (= immediate) */
|
|
dstate_addcmd("load.off");
|
|
}
|
|
|
|
if (!find_nut_info("load.on", QX_FLAG_CMD, QX_FLAG_SKIP)
|
|
&& find_nut_info("load.on.delay", QX_FLAG_CMD, QX_FLAG_SKIP)
|
|
) {
|
|
/* Adds default with a delay value of '0' (= immediate) */
|
|
dstate_addcmd("load.on");
|
|
}
|
|
|
|
/* Init polling frequency */
|
|
val = getval(QX_VAR_POLLFREQ);
|
|
if (val)
|
|
pollfreq = strtol(val, NULL, 10);
|
|
|
|
dstate_setinfo("driver.parameter.pollfreq", "%ld", pollfreq);
|
|
|
|
time(&lastpoll);
|
|
|
|
/* Install handlers */
|
|
upsh.setvar = setvar;
|
|
upsh.instcmd = instcmd;
|
|
|
|
/* Subdriver initinfo */
|
|
if (subdriver->initinfo != NULL)
|
|
subdriver->initinfo();
|
|
}
|
|
|
|
/* Open the port and the like and choose the subdriver */
|
|
void upsdrv_initups(void)
|
|
{
|
|
upsdebugx(1, "%s...", __func__);
|
|
|
|
#if defined(QX_SERIAL) && defined(QX_USB)
|
|
|
|
/* Whether the device is connected through USB or serial */
|
|
if (
|
|
!strcasecmp(dstate_getinfo("driver.parameter.port"), "auto") ||
|
|
getval("subdriver") ||
|
|
getval("vendorid") ||
|
|
getval("productid") ||
|
|
getval("vendor") ||
|
|
getval("product") ||
|
|
getval("serial") ||
|
|
getval("bus") ||
|
|
getval("langid_fix")
|
|
) {
|
|
/* USB */
|
|
is_usb = 1;
|
|
} else {
|
|
/* Serial */
|
|
is_usb = 0;
|
|
}
|
|
|
|
#endif /* QX_SERIAL && QX_USB */
|
|
|
|
/* Serial */
|
|
#ifdef QX_SERIAL
|
|
|
|
#ifdef QX_USB
|
|
if (!is_usb) {
|
|
#endif /* QX_USB */
|
|
|
|
#ifndef TESTING
|
|
|
|
const struct {
|
|
const char *val;
|
|
const int dtr;
|
|
const int rts;
|
|
} cablepower[] = {
|
|
{ "normal", 1, 0 }, /* Default */
|
|
{ "reverse", 0, 1 },
|
|
{ "both", 1, 1 },
|
|
{ "none", 0, 0 },
|
|
{ NULL, 0, 0 }
|
|
};
|
|
|
|
int i;
|
|
const char *val;
|
|
struct termios tio;
|
|
|
|
/* Open and lock the serial port and set the speed to 2400 baud. */
|
|
upsfd = ser_open(device_path);
|
|
ser_set_speed(upsfd, device_path, B2400);
|
|
|
|
if (tcgetattr(upsfd, &tio)) {
|
|
fatal_with_errno(EXIT_FAILURE, "tcgetattr");
|
|
}
|
|
|
|
/* Use canonical mode input processing (to read reply line) */
|
|
tio.c_lflag |= ICANON; /* Canonical input (erase and kill processing) */
|
|
|
|
tio.c_cc[VEOF] = _POSIX_VDISABLE;
|
|
tio.c_cc[VEOL] = '\r';
|
|
tio.c_cc[VERASE] = _POSIX_VDISABLE;
|
|
tio.c_cc[VINTR] = _POSIX_VDISABLE;
|
|
tio.c_cc[VKILL] = _POSIX_VDISABLE;
|
|
tio.c_cc[VQUIT] = _POSIX_VDISABLE;
|
|
tio.c_cc[VSUSP] = _POSIX_VDISABLE;
|
|
tio.c_cc[VSTART] = _POSIX_VDISABLE;
|
|
tio.c_cc[VSTOP] = _POSIX_VDISABLE;
|
|
|
|
if (tcsetattr(upsfd, TCSANOW, &tio)) {
|
|
fatal_with_errno(EXIT_FAILURE, "tcsetattr");
|
|
}
|
|
|
|
val = getval("cablepower");
|
|
for (i = 0; val && cablepower[i].val; i++) {
|
|
|
|
if (!strcasecmp(val, cablepower[i].val)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!cablepower[i].val) {
|
|
fatalx(EXIT_FAILURE, "Value '%s' not valid for 'cablepower'", val);
|
|
}
|
|
|
|
ser_set_dtr(upsfd, cablepower[i].dtr);
|
|
ser_set_rts(upsfd, cablepower[i].rts);
|
|
|
|
/* Allow some time to settle for the cablepower */
|
|
usleep(100000);
|
|
|
|
#endif /* TESTING */
|
|
|
|
#ifdef QX_USB
|
|
} else { /* is_usb */
|
|
#endif /* QX_USB */
|
|
|
|
#endif /* QX_SERIAL */
|
|
|
|
/* USB */
|
|
#ifdef QX_USB
|
|
|
|
warn_if_bad_usb_port_filename(device_path);
|
|
|
|
#ifndef TESTING
|
|
int ret, langid;
|
|
char tbuf[255]; /* Some devices choke on size > 255 */
|
|
char *regex_array[7];
|
|
|
|
char *subdrv = getval("subdriver");
|
|
|
|
regex_array[0] = getval("vendorid");
|
|
regex_array[1] = getval("productid");
|
|
regex_array[2] = getval("vendor");
|
|
regex_array[3] = getval("product");
|
|
regex_array[4] = getval("serial");
|
|
regex_array[5] = getval("bus");
|
|
regex_array[6] = getval("device");
|
|
|
|
/* Check for language ID workaround (#1) */
|
|
if (getval("langid_fix")) {
|
|
/* Skip "0x" prefix and set back to hexadecimal */
|
|
unsigned int u_langid_fix;
|
|
if ( (sscanf(getval("langid_fix") + 2, "%x", &u_langid_fix) != 1)
|
|
|| (u_langid_fix > INT_MAX)
|
|
) {
|
|
upslogx(LOG_NOTICE, "Error enabling language ID workaround");
|
|
} else {
|
|
langid_fix = (int)u_langid_fix;
|
|
upsdebugx(2,
|
|
"Language ID workaround enabled (using '0x%x')",
|
|
langid_fix);
|
|
}
|
|
}
|
|
|
|
/* Pick up the subdriver name if set explicitly */
|
|
if (subdrv) {
|
|
|
|
int i;
|
|
|
|
if (!regex_array[0] || !regex_array[1]) {
|
|
fatalx(EXIT_FAILURE,
|
|
"When specifying a subdriver, "
|
|
"'vendorid' and 'productid' are mandatory.");
|
|
}
|
|
|
|
for (i = 0; usbsubdriver[i].name; i++) {
|
|
|
|
if (strcasecmp(subdrv, usbsubdriver[i].name)) {
|
|
continue;
|
|
}
|
|
|
|
subdriver_command = usbsubdriver[i].command;
|
|
break;
|
|
}
|
|
|
|
if (!subdriver_command) {
|
|
fatalx(EXIT_FAILURE, "Subdriver '%s' not found!", subdrv);
|
|
}
|
|
|
|
}
|
|
|
|
ret = USBNewRegexMatcher(®ex_matcher,
|
|
regex_array,
|
|
REG_ICASE | REG_EXTENDED);
|
|
switch (ret)
|
|
{
|
|
case -1:
|
|
fatal_with_errno(EXIT_FAILURE, "USBNewRegexMatcher");
|
|
case 0:
|
|
break; /* All is well */
|
|
default:
|
|
fatalx(EXIT_FAILURE,
|
|
"Invalid regular expression: %s",
|
|
regex_array[ret]);
|
|
}
|
|
|
|
/* Link the matchers */
|
|
regex_matcher->next = &device_matcher;
|
|
|
|
ret = usb->open(&udev, &usbdevice, regex_matcher, NULL);
|
|
if (ret < 0) {
|
|
fatalx(EXIT_FAILURE,
|
|
"No supported devices found. "
|
|
"Please check your device availability with 'lsusb'\n"
|
|
"and make sure you have an up-to-date version of NUT. "
|
|
"If this does not help,\n"
|
|
"try running the driver with at least 'subdriver', "
|
|
"'vendorid' and 'productid'\n"
|
|
"options specified. Please refer to the man page "
|
|
"for details about these options\n"
|
|
"(man 8 nutdrv_qx).\n");
|
|
}
|
|
|
|
if (!subdriver_command) {
|
|
fatalx(EXIT_FAILURE, "No subdriver selected");
|
|
}
|
|
|
|
/* Create a new matcher for later reopening */
|
|
ret = USBNewExactMatcher(&reopen_matcher, &usbdevice);
|
|
if (ret) {
|
|
fatal_with_errno(EXIT_FAILURE, "USBNewExactMatcher");
|
|
}
|
|
|
|
/* Link the matchers */
|
|
reopen_matcher->next = regex_matcher;
|
|
|
|
dstate_setinfo("ups.vendorid", "%04x", usbdevice.VendorID);
|
|
dstate_setinfo("ups.productid", "%04x", usbdevice.ProductID);
|
|
|
|
/* Check for language ID workaround (#2) */
|
|
if ((langid_fix != -1) && (!getval("noscanlangid"))) {
|
|
/* Future improvement:
|
|
* Asking for the zero'th index is special - it returns
|
|
* a string descriptor that contains all the language
|
|
* IDs supported by the device.
|
|
* Typically there aren't many - often only one.
|
|
* The language IDs are 16 bit numbers, and they start at
|
|
* the third byte in the descriptor.
|
|
* See USB 2.0 specification, section 9.6.7, for more
|
|
* information on this.
|
|
* This should allow automatic application of the workaround */
|
|
ret = usb_get_string(udev, 0, 0,
|
|
(usb_ctrl_charbuf)tbuf, sizeof(tbuf));
|
|
if (ret >= 4) {
|
|
langid = ((uint8_t)tbuf[2]) | (((uint8_t)tbuf[3]) << 8);
|
|
upsdebugx(1,
|
|
"First supported language ID: 0x%x "
|
|
"(please report to the NUT maintainer!)",
|
|
langid);
|
|
}
|
|
}
|
|
|
|
#endif /* TESTING */
|
|
|
|
#ifdef QX_SERIAL
|
|
} /* is_usb */
|
|
#endif /* QX_SERIAL */
|
|
|
|
#endif /* QX_USB */
|
|
|
|
/* Choose subdriver */
|
|
if (!subdriver_matcher())
|
|
fatalx(EXIT_FAILURE, "Device not supported!");
|
|
|
|
/* Subdriver initups */
|
|
if (subdriver->initups != NULL)
|
|
subdriver->initups();
|
|
}
|
|
|
|
/* Close the ports and the like */
|
|
void upsdrv_cleanup(void)
|
|
{
|
|
upsdebugx(1, "%s...", __func__);
|
|
|
|
#ifndef TESTING
|
|
|
|
#ifdef QX_SERIAL
|
|
|
|
#ifdef QX_USB
|
|
if (!is_usb) {
|
|
#endif /* QX_USB */
|
|
|
|
ser_set_dtr(upsfd, 0);
|
|
ser_close(upsfd, device_path);
|
|
|
|
#ifdef QX_USB
|
|
} else { /* is_usb */
|
|
#endif /* QX_USB */
|
|
|
|
#endif /* QX_SERIAL */
|
|
|
|
#ifdef QX_USB
|
|
|
|
usb->close(udev);
|
|
USBFreeExactMatcher(reopen_matcher);
|
|
USBFreeRegexMatcher(regex_matcher);
|
|
free(usbdevice.Vendor);
|
|
free(usbdevice.Product);
|
|
free(usbdevice.Serial);
|
|
free(usbdevice.Bus);
|
|
free(usbdevice.Device);
|
|
|
|
#ifdef QX_SERIAL
|
|
} /* is_usb */
|
|
#endif /* QX_SERIAL */
|
|
|
|
#endif /* QX_USB */
|
|
|
|
#endif /* TESTING */
|
|
|
|
}
|
|
|
|
|
|
/* == Support functions == */
|
|
|
|
/* Generic command processing function: send a command and read a reply.
|
|
* Returns < 0 on error, 0 on timeout and the number of bytes read on success. */
|
|
static ssize_t qx_command(const char *cmd, char *buf, size_t buflen)
|
|
{
|
|
/* NOTE: Could not find in which ifdef-ed codepath, but clang complained
|
|
* about unused parameters here. Reference them just in case...
|
|
*/
|
|
NUT_UNUSED_VARIABLE(cmd);
|
|
NUT_UNUSED_VARIABLE(buf);
|
|
NUT_UNUSED_VARIABLE(buflen);
|
|
|
|
#ifndef TESTING
|
|
|
|
ssize_t ret = -1;
|
|
|
|
# ifdef QX_USB
|
|
|
|
# ifdef QX_SERIAL
|
|
/* Communication: USB */
|
|
if (is_usb) {
|
|
# endif /* QX_SERIAL (&& QX_USB)*/
|
|
|
|
if (udev == NULL) {
|
|
ret = usb->open(&udev, &usbdevice, reopen_matcher, NULL);
|
|
|
|
if (ret < 1) {
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
ret = (*subdriver_command)(cmd, buf, buflen);
|
|
|
|
if (ret >= 0) {
|
|
return ret;
|
|
}
|
|
|
|
switch (ret)
|
|
{
|
|
case ERROR_BUSY: /* Device or resource busy */
|
|
fatal_with_errno(EXIT_FAILURE, "Got disconnected by another driver");
|
|
#ifndef HAVE___ATTRIBUTE__NORETURN
|
|
# if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE)
|
|
# pragma GCC diagnostic push
|
|
# pragma GCC diagnostic ignored "-Wunreachable-code"
|
|
# endif
|
|
exit(EXIT_FAILURE); /* Should not get here in practice, but compiler is afraid we can fall through */
|
|
# if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE)
|
|
# pragma GCC diagnostic pop
|
|
# endif
|
|
#endif
|
|
|
|
#if WITH_LIBUSB_0_1 /* limit to libusb 0.1 implementation */
|
|
case -EPERM: /* Operation not permitted */
|
|
fatal_with_errno(EXIT_FAILURE, "Permissions problem");
|
|
#ifndef HAVE___ATTRIBUTE__NORETURN
|
|
# if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE)
|
|
# pragma GCC diagnostic push
|
|
# pragma GCC diagnostic ignored "-Wunreachable-code"
|
|
# endif
|
|
exit(EXIT_FAILURE); /* Should not get here in practice, but compiler is afraid we can fall through */
|
|
# if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE)
|
|
# pragma GCC diagnostic pop
|
|
# endif
|
|
#endif
|
|
#endif /* WITH_LIBUSB_0_1 */
|
|
|
|
case ERROR_PIPE: /* Broken pipe */
|
|
if (usb_clear_halt(udev, 0x81) == 0) {
|
|
upsdebugx(1, "Stall condition cleared");
|
|
break;
|
|
}
|
|
#if (defined ETIME) && ETIME && WITH_LIBUSB_0_1 /* limit to libusb 0.1 implementation */
|
|
goto fallthrough_case_ETIME;
|
|
case -ETIME: /* Timer expired */
|
|
fallthrough_case_ETIME:
|
|
#endif /* ETIME && WITH_LIBUSB_0_1 */
|
|
if (usb_reset(udev) == 0) {
|
|
upsdebugx(1, "Device reset handled");
|
|
}
|
|
goto fallthrough_case_reconnect;
|
|
case ERROR_NO_DEVICE: /* No such device */
|
|
case ERROR_ACCESS: /* Permission denied */
|
|
case ERROR_IO: /* I/O error */
|
|
#if WITH_LIBUSB_0_1 /* limit to libusb 0.1 implementation */
|
|
case -ENXIO: /* No such device or address */
|
|
#endif /* WITH_LIBUSB_0_1 */
|
|
case ERROR_NOT_FOUND: /* No such file or directory */
|
|
fallthrough_case_reconnect:
|
|
/* Uh oh, got to reconnect! */
|
|
usb->close(udev);
|
|
udev = NULL;
|
|
break;
|
|
|
|
case ERROR_TIMEOUT: /* Connection timed out */
|
|
case ERROR_OVERFLOW: /* Value too large for defined data type */
|
|
#if EPROTO && WITH_LIBUSB_0_1 /* limit to libusb 0.1 implementation */
|
|
case -EPROTO: /* Protocol error */
|
|
#endif
|
|
default:
|
|
break;
|
|
}
|
|
|
|
# ifdef QX_SERIAL
|
|
/* Communication: serial */
|
|
} else { /* !is_usb */
|
|
# endif /* QX_SERIAL (&& QX_USB) */
|
|
|
|
# endif /* QX_USB (&& TESTING) */
|
|
|
|
# ifdef QX_SERIAL
|
|
|
|
ser_flush_io(upsfd);
|
|
|
|
ret = ser_send(upsfd, "%s", cmd);
|
|
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "send: %s (%zd)",
|
|
ret ? strerror(errno) : "timeout", ret);
|
|
return ret;
|
|
}
|
|
|
|
upsdebugx(3, "send: '%.*s'",
|
|
(int)strcspn(cmd, "\r"), cmd);
|
|
|
|
ret = ser_get_buf(upsfd, buf, buflen, SER_WAIT_SEC, 0);
|
|
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "read: %s (%zd)",
|
|
ret ? strerror(errno) : "timeout", ret);
|
|
return ret;
|
|
}
|
|
|
|
upsdebug_hex(5, "read", buf, (size_t)ret);
|
|
upsdebugx(3, "read: '%.*s'", (int)strcspn(buf, "\r"), buf);
|
|
|
|
# ifdef QX_USB
|
|
} /* !is_usb */
|
|
# endif /* QX_USB (&& QX_SERIAL) */
|
|
|
|
# endif /* QX_SERIAL (&& TESTING) */
|
|
|
|
return ret;
|
|
|
|
#else /* TESTING */
|
|
|
|
testing_t *testing = subdriver->testing;
|
|
int i;
|
|
|
|
memset(buf, 0, buflen);
|
|
|
|
upsdebugx(3, "send: '%.*s'", (int)strcspn(cmd, "\r"), cmd);
|
|
|
|
for (i = 0; cmd && testing[i].cmd; i++) {
|
|
|
|
if (strcasecmp(cmd, testing[i].cmd)) {
|
|
continue;
|
|
}
|
|
|
|
upsdebugx(3, "read: '%.*s'",
|
|
(int)strcspn(testing[i].answer, "\r"),
|
|
testing[i].answer);
|
|
|
|
/* If requested to do so and this is the case, try to preserve inner '\0's (treat answer as a sequence of bytes) */
|
|
if (testing[i].answer_len > 0 && strlen(testing[i].answer) < (size_t)testing[i].answer_len) {
|
|
|
|
size_t len;
|
|
|
|
len = buflen <= (size_t)testing[i].answer_len ? buflen - 1 : (size_t)testing[i].answer_len;
|
|
len = len <= sizeof(testing[i].answer) ? len : sizeof(testing[i].answer);
|
|
|
|
memcpy(buf, testing[i].answer, len);
|
|
upsdebug_hex(4, "read", buf, (int)len);
|
|
|
|
return len;
|
|
|
|
}
|
|
|
|
return snprintf(buf, buflen, "%s", testing[i].answer);
|
|
|
|
}
|
|
|
|
/* If the driver expects some kind of reply in case of error.. */
|
|
if (subdriver->rejected != NULL) {
|
|
|
|
/* ..fulfill its expectations.. */
|
|
upsdebugx(3, "read: '%.*s'",
|
|
(int)strcspn(subdriver->rejected, "\r"),
|
|
subdriver->rejected);
|
|
return snprintf(buf, buflen, "%s", subdriver->rejected);
|
|
|
|
/* ..otherwise.. */
|
|
} else {
|
|
|
|
/* ..echo back the command */
|
|
upsdebugx(3, "read: '%.*s'", (int)strcspn(cmd, "\r"), cmd);
|
|
return snprintf(buf, buflen, "%s", cmd);
|
|
|
|
}
|
|
|
|
#endif /* TESTING */
|
|
}
|
|
|
|
/* See header file for details.
|
|
* Interpretation is done in ups_status_set(). */
|
|
void update_status(const char *value)
|
|
{
|
|
status_lkp_t *status_item;
|
|
int clear = 0;
|
|
|
|
upsdebugx(5, "%s: %s", __func__, value);
|
|
|
|
if (*value == '!') {
|
|
value++;
|
|
clear = 1;
|
|
}
|
|
|
|
for (status_item = status_info; status_item->status_str != NULL ; status_item++) {
|
|
|
|
if (strcasecmp(status_item->status_str, value))
|
|
continue;
|
|
|
|
if (clear) {
|
|
ups_status &= ~status_item->status_mask;
|
|
} else {
|
|
ups_status |= status_item->status_mask;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
upsdebugx(5, "%s: Warning! %s not in list of known values",
|
|
__func__, value);
|
|
}
|
|
|
|
/* Choose subdriver */
|
|
static int subdriver_matcher(void)
|
|
{
|
|
const char *protocol = getval("protocol");
|
|
int i;
|
|
|
|
/* Select the subdriver for this device */
|
|
for (i = 0; subdriver_list[i] != NULL; i++) {
|
|
|
|
int j;
|
|
|
|
/* If protocol is set in ups.conf, use it */
|
|
if (protocol) {
|
|
|
|
char subdrv_name[SMALLBUF];
|
|
|
|
/* Get rid of subdriver version */
|
|
snprintf(subdrv_name, sizeof(subdrv_name), "%.*s",
|
|
(int)strcspn(subdriver_list[i]->name, " "),
|
|
subdriver_list[i]->name);
|
|
|
|
if (strcasecmp(subdrv_name, protocol)) {
|
|
upsdebugx(2, "Skipping protocol %s",
|
|
subdriver_list[i]->name);
|
|
continue;
|
|
}
|
|
|
|
}
|
|
|
|
/* Give every subdriver some tries */
|
|
for (j = 0; j < MAXTRIES; j++) {
|
|
|
|
subdriver = subdriver_list[i];
|
|
|
|
if (subdriver->claim()) {
|
|
break;
|
|
}
|
|
|
|
subdriver = NULL;
|
|
|
|
}
|
|
|
|
if (subdriver != NULL)
|
|
break;
|
|
|
|
}
|
|
|
|
if (!subdriver) {
|
|
upslogx(LOG_ERR, "Device not supported!");
|
|
return 0;
|
|
}
|
|
|
|
upslogx(LOG_INFO, "Using protocol: %s", subdriver->name);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* Set vars boundaries */
|
|
static void qx_set_var(item_t *item)
|
|
{
|
|
if (!(item->qxflags & QX_FLAG_NONUT))
|
|
dstate_setflags(item->info_type, item->info_flags);
|
|
|
|
/* Set max length for strings, if needed */
|
|
if (item->info_flags & ST_FLAG_STRING && !(item->qxflags & QX_FLAG_NONUT))
|
|
dstate_setaux(item->info_type, strtol(item->info_rw[0].value, NULL, 10));
|
|
|
|
/* Set enum list */
|
|
if (item->qxflags & QX_FLAG_ENUM) {
|
|
|
|
info_rw_t *envalue;
|
|
char buf[LARGEBUF] = "";
|
|
|
|
/* Loop on all existing values */
|
|
for (envalue = item->info_rw; envalue != NULL && strlen(envalue->value) > 0; envalue++) {
|
|
|
|
if (envalue->preprocess
|
|
&& envalue->preprocess(envalue->value, sizeof(envalue->value))
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
/* This item is not available yet in NUT, so publish these data in the logs */
|
|
if (item->qxflags & QX_FLAG_NONUT) {
|
|
|
|
snprintfcat(buf, sizeof(buf), " %s", envalue->value);
|
|
|
|
/* This item is available in NUT, add its enum to the variable */
|
|
} else {
|
|
|
|
dstate_addenum(item->info_type, "%s", envalue->value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (item->qxflags & QX_FLAG_NONUT)
|
|
upslogx(LOG_INFO, "%s, settable values:%s",
|
|
item->info_type,
|
|
strlen(buf) > 0 ? buf : " none");
|
|
|
|
}
|
|
|
|
/* Set range */
|
|
if (item->qxflags & QX_FLAG_RANGE) {
|
|
|
|
info_rw_t *rvalue, *from = NULL, *to = NULL;
|
|
int ok = 0;
|
|
|
|
/* Loop on all existing values */
|
|
for (rvalue = item->info_rw; rvalue != NULL && strlen(rvalue->value) > 0; rvalue++) {
|
|
|
|
if (rvalue->preprocess
|
|
&& rvalue->preprocess(rvalue->value, sizeof(rvalue->value))
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (!from) {
|
|
from = rvalue;
|
|
continue;
|
|
}
|
|
|
|
to = rvalue;
|
|
|
|
/* This item is not available yet in NUT, so
|
|
* publish these data in the logs */
|
|
if (item->qxflags & QX_FLAG_NONUT) {
|
|
|
|
upslogx(LOG_INFO, "%s, settable range: %s..%s",
|
|
item->info_type, from->value, to->value);
|
|
ok++;
|
|
|
|
/* This item is available in NUT, add its range to the variable */
|
|
} else {
|
|
long lFrom = strtol(from->value, NULL, 10),
|
|
lTo = strtol(to->value, NULL, 10);
|
|
|
|
if (lFrom > INT_MAX || lTo > INT_MAX) {
|
|
upslogx(LOG_INFO,
|
|
"%s, settable range exceeds INT_MAX: %ld..%ld",
|
|
item->info_type, lFrom, lTo);
|
|
} else {
|
|
dstate_addrange(item->info_type, (int)lFrom, (int)lTo);
|
|
}
|
|
}
|
|
|
|
from = NULL;
|
|
to = NULL;
|
|
|
|
}
|
|
|
|
/* This item is not available yet in NUT and we weren't able to
|
|
* get its range; let people know it */
|
|
if ((item->qxflags & QX_FLAG_NONUT) && !ok)
|
|
upslogx(LOG_INFO, "%s, settable range: none", item->info_type);
|
|
|
|
}
|
|
}
|
|
|
|
/* Walk UPS variables and set elements of the qx2nut array. */
|
|
static bool_t qx_ups_walk(walkmode_t mode)
|
|
{
|
|
item_t *item;
|
|
int retcode;
|
|
|
|
/* Clear batt.{chrg,runt}.act for guesstimation */
|
|
if (mode == QX_WALKMODE_FULL_UPDATE) {
|
|
batt.runt.act = -1;
|
|
batt.chrg.act = -1;
|
|
}
|
|
|
|
/* Clear data from previous_item */
|
|
memset(previous_item.command, 0, sizeof(previous_item.command));
|
|
memset(previous_item.answer, 0, sizeof(previous_item.answer));
|
|
|
|
/* 3 modes: QX_WALKMODE_INIT, QX_WALKMODE_QUICK_UPDATE
|
|
* and QX_WALKMODE_FULL_UPDATE */
|
|
|
|
/* Device data walk */
|
|
for (item = subdriver->qx2nut; item->info_type != NULL; item++) {
|
|
|
|
/* Skip this item */
|
|
if (item->qxflags & QX_FLAG_SKIP)
|
|
continue;
|
|
|
|
upsdebugx(10, "%s: processing: %s", __func__, item->info_type);
|
|
|
|
/* Filter data according to mode */
|
|
switch (mode)
|
|
{
|
|
/* Device capabilities enumeration */
|
|
case QX_WALKMODE_INIT:
|
|
|
|
/* Special case for handling server side variables */
|
|
if (item->qxflags & QX_FLAG_ABSENT) {
|
|
|
|
/* Already set */
|
|
if (dstate_getinfo(item->info_type))
|
|
continue;
|
|
|
|
dstate_setinfo(item->info_type, "%s", item->dfl);
|
|
|
|
/* Set var flags/range/enum */
|
|
qx_set_var(item);
|
|
|
|
continue;
|
|
}
|
|
|
|
/* Allow duplicates for these NUT variables */
|
|
if (!strncmp(item->info_type, "ups.alarm", 9)
|
|
|| !strncmp(item->info_type, "ups.status", 10)
|
|
) {
|
|
break;
|
|
}
|
|
|
|
/* This one doesn't exist yet */
|
|
if (dstate_getinfo(item->info_type) == NULL)
|
|
break;
|
|
|
|
continue;
|
|
|
|
case QX_WALKMODE_QUICK_UPDATE:
|
|
|
|
/* Quick update only deals with status and alarms! */
|
|
if (!(item->qxflags & QX_FLAG_QUICK_POLL))
|
|
continue;
|
|
|
|
break;
|
|
|
|
case QX_WALKMODE_FULL_UPDATE:
|
|
|
|
/* These don't need polling after initinfo() */
|
|
if (item->qxflags & (QX_FLAG_ABSENT | QX_FLAG_CMD | QX_FLAG_SETVAR | QX_FLAG_STATIC))
|
|
continue;
|
|
|
|
/* These need to be polled after user changes (setvar / instcmd) */
|
|
if ((item->qxflags & QX_FLAG_SEMI_STATIC)
|
|
&& (data_has_changed == FALSE)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
|
|
#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) )
|
|
# pragma GCC diagnostic push
|
|
#endif
|
|
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT
|
|
# pragma GCC diagnostic ignored "-Wcovered-switch-default"
|
|
#endif
|
|
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE
|
|
# pragma GCC diagnostic ignored "-Wunreachable-code"
|
|
#endif
|
|
/* Older CLANG (e.g. clang-3.4) seems to not support the GCC pragmas above */
|
|
#ifdef __clang__
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wunreachable-code"
|
|
#pragma clang diagnostic ignored "-Wcovered-switch-default"
|
|
#endif
|
|
/* All enum cases defined as of the time of coding
|
|
* have been covered above. Handle later definitions,
|
|
* memory corruptions and buggy inputs below...
|
|
*/
|
|
default:
|
|
fatalx(EXIT_FAILURE, "%s: unknown update mode!", __func__);
|
|
#ifdef __clang__
|
|
# pragma clang diagnostic pop
|
|
#endif
|
|
#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) )
|
|
# pragma GCC diagnostic pop
|
|
#endif
|
|
|
|
}
|
|
|
|
/* Instant commands */
|
|
if (item->qxflags & QX_FLAG_CMD) {
|
|
dstate_addcmd(item->info_type);
|
|
continue;
|
|
}
|
|
|
|
/* Setvars */
|
|
if (item->qxflags & QX_FLAG_SETVAR) {
|
|
|
|
if (item->qxflags & QX_FLAG_NONUT) {
|
|
setvar(item->info_type, NULL);
|
|
item->qxflags |= QX_FLAG_SKIP;
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
/* Check whether the previous item uses the same command
|
|
* and then use its answer, if available.. */
|
|
if (strlen(previous_item.command) > 0
|
|
&& strlen(previous_item.answer) > 0
|
|
&& !strcasecmp(previous_item.command, item->command)
|
|
) {
|
|
|
|
snprintf(item->answer, sizeof(item->answer), "%s",
|
|
previous_item.answer);
|
|
|
|
/* Process the answer */
|
|
retcode = qx_process_answer(item, strlen(item->answer));
|
|
|
|
/* ..otherwise: execute command to get answer from the UPS */
|
|
} else {
|
|
|
|
retcode = qx_process(item, NULL);
|
|
|
|
}
|
|
|
|
/* Record item as previous_item */
|
|
snprintf(previous_item.command, sizeof(previous_item.command), "%s",
|
|
item->command);
|
|
snprintf(previous_item.answer, sizeof(previous_item.answer), "%s",
|
|
item->answer);
|
|
|
|
if (retcode) {
|
|
|
|
/* Clear data from the item */
|
|
memset(item->answer, 0, sizeof(item->answer));
|
|
memset(item->value, 0, sizeof(item->value));
|
|
|
|
if (item->qxflags & QX_FLAG_QUICK_POLL)
|
|
return FALSE;
|
|
|
|
if (mode == QX_WALKMODE_INIT)
|
|
/* Skip this item from now on */
|
|
item->qxflags |= QX_FLAG_SKIP;
|
|
|
|
/* Don't know what happened, try again later... */
|
|
continue;
|
|
|
|
}
|
|
|
|
/* Process the value we got back (set status bits
|
|
* and set the value of other parameters) */
|
|
retcode = ups_infoval_set(item);
|
|
|
|
/* Clear data from the item */
|
|
memset(item->answer, 0, sizeof(item->answer));
|
|
memset(item->value, 0, sizeof(item->value));
|
|
|
|
/* Uh-oh! Some error! */
|
|
if (retcode == -1) {
|
|
|
|
if (item->qxflags & QX_FLAG_QUICK_POLL)
|
|
return FALSE;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
/* Set var flags/range/enum (not for ups.{alarm.status},
|
|
* hence the retcode check) */
|
|
if (retcode && mode == QX_WALKMODE_INIT) {
|
|
qx_set_var(item);
|
|
}
|
|
|
|
}
|
|
|
|
/* Update battery guesstimation */
|
|
if (mode == QX_WALKMODE_FULL_UPDATE
|
|
&& (d_equal(batt.runt.act, -1) || d_equal(batt.chrg.act, -1))
|
|
) {
|
|
|
|
if (getval("runtimecal")) {
|
|
|
|
time_t battery_now;
|
|
|
|
time(&battery_now);
|
|
|
|
/* OL */
|
|
if (ups_status & STATUS(OL)) {
|
|
|
|
batt.runt.est += batt.runt.nom * difftime(battery_now, battery_lastpoll) / batt.chrg.time;
|
|
if (batt.runt.est > batt.runt.nom) {
|
|
batt.runt.est = batt.runt.nom;
|
|
}
|
|
|
|
/* OB */
|
|
} else {
|
|
|
|
batt.runt.est -= load.eff * difftime(battery_now, battery_lastpoll);
|
|
if (batt.runt.est < 0) {
|
|
batt.runt.est = 0;
|
|
}
|
|
|
|
}
|
|
|
|
const char *val = dstate_getinfo("battery.voltage");
|
|
|
|
if (!val) {
|
|
upsdebugx(2, "%s: unable to get battery.voltage", __func__);
|
|
} else {
|
|
/* For age-corrected estimates below,
|
|
* see theory and experimental graphs at
|
|
* https://github.com/networkupstools/nut/pull/1027
|
|
*/
|
|
|
|
batt.volt.act = batt.packs * strtod(val, NULL);
|
|
|
|
if (batt.volt.act > 0 && batt.volt.low > 0 && batt.volt.high > batt.volt.low) {
|
|
|
|
double voltage_battery_charge = (batt.volt.act - batt.volt.low) / (batt.volt.high - batt.volt.low);
|
|
|
|
if (voltage_battery_charge < 0) {
|
|
voltage_battery_charge = 0;
|
|
}
|
|
|
|
if (voltage_battery_charge > 1) {
|
|
voltage_battery_charge = 1;
|
|
}
|
|
|
|
/* Correct estimated runtime remaining for old batteries:
|
|
* this value replacement only happens if the actual
|
|
* voltage_battery_charge is smaller than expected by
|
|
* previous (load-based) estimation, thus adapting to a
|
|
* battery too old and otherwise behaving non-linearly
|
|
*/
|
|
if (voltage_battery_charge < (batt.runt.est / batt.runt.nom)) {
|
|
double estPrev = batt.runt.est;
|
|
batt.runt.est = voltage_battery_charge * batt.runt.nom;
|
|
upsdebugx(3, "%s: updating batt.runt.est from '%g' to '%g'",
|
|
__func__, estPrev, batt.runt.est);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
if (d_equal(batt.chrg.act, -1))
|
|
dstate_setinfo("battery.charge", "%.0f",
|
|
100 * batt.runt.est / batt.runt.nom);
|
|
|
|
if (d_equal(batt.runt.act, -1) && !qx_load())
|
|
dstate_setinfo("battery.runtime", "%.0f",
|
|
batt.runt.est / load.eff);
|
|
|
|
battery_lastpoll = battery_now;
|
|
|
|
} else {
|
|
|
|
qx_battery();
|
|
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/* Convert the local status information to NUT format and set NUT alarms. */
|
|
static void ups_alarm_set(void)
|
|
{
|
|
if (ups_status & STATUS(RB)) {
|
|
alarm_set("Replace battery!");
|
|
}
|
|
if (ups_status & STATUS(FSD)) {
|
|
alarm_set("Shutdown imminent!");
|
|
}
|
|
}
|
|
|
|
/* Convert the local status information to NUT format and set NUT status. */
|
|
static void ups_status_set(void)
|
|
{
|
|
if (ups_status & STATUS(OL)) {
|
|
status_set("OL"); /* On line */
|
|
} else {
|
|
status_set("OB"); /* On battery */
|
|
}
|
|
if (ups_status & STATUS(DISCHRG)) {
|
|
status_set("DISCHRG"); /* Discharging */
|
|
}
|
|
if (ups_status & STATUS(CHRG)) {
|
|
status_set("CHRG"); /* Charging */
|
|
}
|
|
if (ups_status & STATUS(LB)) {
|
|
status_set("LB"); /* Low battery */
|
|
}
|
|
if (ups_status & STATUS(OVER)) {
|
|
status_set("OVER"); /* Overload */
|
|
}
|
|
if (ups_status & STATUS(RB)) {
|
|
status_set("RB"); /* Replace battery */
|
|
}
|
|
if (ups_status & STATUS(TRIM)) {
|
|
status_set("TRIM"); /* SmartTrim */
|
|
}
|
|
if (ups_status & STATUS(BOOST)) {
|
|
status_set("BOOST"); /* SmartBoost */
|
|
}
|
|
if (ups_status & STATUS(BYPASS)) {
|
|
status_set("BYPASS"); /* On bypass */
|
|
}
|
|
if (ups_status & STATUS(OFF)) {
|
|
status_set("OFF"); /* UPS is off */
|
|
}
|
|
if (ups_status & STATUS(CAL)) {
|
|
status_set("CAL"); /* Calibration */
|
|
}
|
|
if (ups_status & STATUS(FSD)) {
|
|
status_set("FSD"); /* Forced shutdown */
|
|
}
|
|
}
|
|
|
|
/* See header file for details. */
|
|
item_t *find_nut_info(const char *varname, const unsigned long flag, const unsigned long noflag)
|
|
{
|
|
item_t *item;
|
|
|
|
for (item = subdriver->qx2nut; item->info_type != NULL; item++) {
|
|
|
|
if (strcasecmp(item->info_type, varname))
|
|
continue;
|
|
|
|
if (flag && ((item->qxflags & flag) != flag))
|
|
continue;
|
|
|
|
if (noflag && (item->qxflags & noflag))
|
|
continue;
|
|
|
|
return item;
|
|
}
|
|
|
|
upsdebugx(2, "%s: info type %s not found", __func__, varname);
|
|
return NULL;
|
|
}
|
|
|
|
/* Process the answer we got back from the UPS
|
|
* Return -1 on errors, 0 on success */
|
|
static int qx_process_answer(item_t *item, const size_t len)
|
|
{
|
|
/* Query rejected by the UPS */
|
|
if (subdriver->rejected && !strcasecmp(item->answer, subdriver->rejected)) {
|
|
upsdebugx(2, "%s: query rejected by the UPS (%s)",
|
|
__func__, item->info_type);
|
|
return -1;
|
|
}
|
|
|
|
/* Short reply */
|
|
if (item->answer_len && len < item->answer_len) {
|
|
upsdebugx(2, "%s: short reply (%s)",
|
|
__func__, item->info_type);
|
|
return -1;
|
|
}
|
|
|
|
/* Wrong leading character */
|
|
if (item->leading && item->answer[0] != item->leading) {
|
|
upsdebugx(2,
|
|
"%s: %s - invalid start character [%02x], expected [%02x]",
|
|
__func__, item->info_type, item->answer[0], item->leading);
|
|
return -1;
|
|
}
|
|
|
|
/* Check boundaries */
|
|
if (item->to && item->to < item->from) {
|
|
upsdebugx(1,
|
|
"%s: in %s, starting char's position (%d) "
|
|
"follows ending char's one (%d)",
|
|
__func__, item->info_type, item->from, item->to);
|
|
return -1;
|
|
}
|
|
|
|
/* Get value */
|
|
if (strlen(item->answer)) {
|
|
snprintf(item->value, sizeof(item->value), "%.*s",
|
|
item->to ? 1 + item->to - item->from : (int)strcspn(item->answer, "\r") - item->from,
|
|
item->answer + item->from);
|
|
} else {
|
|
snprintf(item->value, sizeof(item->value), "%s", "");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* See header file for details. */
|
|
int qx_process(item_t *item, const char *command)
|
|
{
|
|
char buf[sizeof(item->answer) - 1] = "", *cmd;
|
|
ssize_t len;
|
|
size_t cmdlen = command ?
|
|
(strlen(command) >= SMALLBUF ? strlen(command) + 1 : SMALLBUF) :
|
|
(item->command && strlen(item->command) >= SMALLBUF ? strlen(item->command) + 1 : SMALLBUF);
|
|
size_t cmdsz = (sizeof(char) * cmdlen); /* in bytes, to be pedantic */
|
|
|
|
if ( !(cmd = xmalloc(cmdsz)) ) {
|
|
upslogx(LOG_ERR, "qx_process() failed to allocate buffer");
|
|
return -1;
|
|
}
|
|
|
|
/* Prepare the command to be used */
|
|
memset(cmd, 0, cmdsz);
|
|
snprintf(cmd, cmdsz, "%s", command ? command : item->command);
|
|
|
|
/* Preprocess the command */
|
|
if (
|
|
item->preprocess_command != NULL &&
|
|
item->preprocess_command(item, cmd, cmdsz) == -1
|
|
) {
|
|
upsdebugx(4, "%s: failed to preprocess command [%s]",
|
|
__func__, item->info_type);
|
|
free (cmd);
|
|
return -1;
|
|
}
|
|
|
|
/* Send the command */
|
|
len = qx_command(cmd, buf, sizeof(buf));
|
|
|
|
memset(item->answer, 0, sizeof(item->answer));
|
|
|
|
if (len < 0 || len > INT_MAX) {
|
|
upsdebugx(4, "%s: failed to preprocess answer [%s]",
|
|
__func__, item->info_type);
|
|
free (cmd);
|
|
return -1;
|
|
}
|
|
|
|
memcpy(item->answer, buf, sizeof(buf));
|
|
|
|
/* Preprocess the answer */
|
|
if (item->preprocess_answer != NULL) {
|
|
len = item->preprocess_answer(item, (int)len);
|
|
if (len < 0 || len > INT_MAX) {
|
|
upsdebugx(4, "%s: failed to preprocess answer [%s]",
|
|
__func__, item->info_type);
|
|
/* Clear the failed answer, preventing it from
|
|
* being reused by next items with same command */
|
|
memset(item->answer, 0, sizeof(item->answer));
|
|
free (cmd);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
free (cmd);
|
|
|
|
/* Process the answer to get the value */
|
|
return qx_process_answer(item, (size_t)len);
|
|
}
|
|
|
|
/* See header file for details. */
|
|
int ups_infoval_set(item_t *item)
|
|
{
|
|
char value[SMALLBUF] = "";
|
|
|
|
/* Item need to be preprocessed? */
|
|
if (item->preprocess != NULL){
|
|
|
|
/* Process the value returned by the UPS to NUT standards */
|
|
if (item->preprocess(item, value, sizeof(value))) {
|
|
upsdebugx(4, "%s: failed to preprocess value [%s: %s]",
|
|
__func__, item->info_type, item->value);
|
|
return -1;
|
|
}
|
|
|
|
/* Deal with status items */
|
|
if (!strncmp(item->info_type, "ups.status", 10)) {
|
|
if (strlen(value) > 0)
|
|
update_status(value);
|
|
return 0;
|
|
}
|
|
|
|
/* Deal with alarm items */
|
|
if (!strncmp(item->info_type, "ups.alarm", 9)) {
|
|
if (strlen(value) > 0)
|
|
alarm_set(value);
|
|
return 0;
|
|
}
|
|
|
|
} else {
|
|
|
|
snprintf(value, sizeof(value), "%s", item->value);
|
|
|
|
/* Cover most of the cases: either left/right filled with hashes,
|
|
* spaces or a mix of both */
|
|
if (item->qxflags & QX_FLAG_TRIM)
|
|
str_trim_m(value, "# ");
|
|
|
|
if (strcasecmp(item->dfl, "%s")) {
|
|
|
|
if (strspn(value, "0123456789 .") != strlen(value)) {
|
|
upsdebugx(2, "%s: non numerical value [%s: %s]",
|
|
__func__, item->info_type, value);
|
|
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
|
|
snprintf(value, sizeof(value), item->dfl, strtod(value, NULL));
|
|
#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL
|
|
#pragma GCC diagnostic pop
|
|
#endif
|
|
}
|
|
|
|
}
|
|
|
|
if (item->qxflags & QX_FLAG_NONUT) {
|
|
upslogx(LOG_INFO, "%s: %s", item->info_type, value);
|
|
return 1;
|
|
}
|
|
|
|
if (!strlen(value)) {
|
|
upsdebugx(1, "%s: non significant value [%s]",
|
|
__func__, item->info_type);
|
|
return -1;
|
|
}
|
|
|
|
dstate_setinfo(item->info_type, "%s", value);
|
|
|
|
/* Fill batt.{chrg,runt}.act for guesstimation */
|
|
if (!strcasecmp(item->info_type, "battery.charge"))
|
|
batt.chrg.act = strtol(value, NULL, 10);
|
|
else if (!strcasecmp(item->info_type, "battery.runtime"))
|
|
batt.runt.act = strtol(value, NULL, 10);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* See header file for details. */
|
|
unsigned int qx_status(void)
|
|
{
|
|
return ups_status;
|
|
}
|