diff --git a/NEWS.adoc b/NEWS.adoc index ea2def7bca..e3e84ec474 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -184,6 +184,8 @@ https://github.com/networkupstools/nut/milestone/11 * introduced `innovart31` protocol support for Innova RT 3/1 UPSes. [#2712, #2798] * introduced `q2` and `q6` protocol support; currently also based/tested on Innova devices, but other models than RT 3/1. [#2798] + * introduced a `gtec` subdriver and protocol, tested over USB with a + Gtec ZP120N device. [#2818] * extended Voltronic protocol to support longer numbers as remaining `battery.runtime` value. [#2765] diff --git a/docs/man/nutdrv_qx.txt b/docs/man/nutdrv_qx.txt index 810f6a0f45..4759486bf0 100644 --- a/docs/man/nutdrv_qx.txt +++ b/docs/man/nutdrv_qx.txt @@ -30,8 +30,8 @@ refers to this large family of similar dialects as the 'Megatec Q*' or The *nutdrv_qx* driver is known to work with various UPSes from 'Armac', 'Blazer', 'Energy Sistem', 'Fenton Technologies', 'General Electric', -'Hunnox', 'Masterguard', 'Mustek', 'Powercool', 'Voltronic Power', 'SKE' -(rebranded by many, many -- have I said many? -- others... +'Gtec', 'Hunnox', 'Masterguard', 'Mustek', 'Powercool', 'Voltronic Power', +'SKE' (rebranded by many, many -- have I said many? -- others... Long story short: if your UPS came with a software called 'Viewpower', chances are high that it works with this driver with one of the @@ -79,7 +79,7 @@ If you set stayoff in linkman:ups.conf[5] when FSD arises the UPS will call a *s *protocol =* 'string':: Skip autodetection of the protocol to use and only use the one specified. -Supported values: 'bestups', 'hunnox', 'innovart31', 'masterguard', 'mecer', 'megatec', 'megatec/old', 'mustek', 'q1', 'q2', 'q6', 'voltronic', 'voltronic-qs', 'voltronic-qs-hex' and 'zinto'. +Supported values: 'bestups', 'gtec', 'hunnox', 'innovart31', 'masterguard', 'mecer', 'megatec', 'megatec/old', 'mustek', 'q1', 'q2', 'q6', 'voltronic', 'voltronic-qs', 'voltronic-qs-hex' and 'zinto'. + Run the driver program with the `--help` option to see the exact list of `protocol` values it would currently recognize. @@ -374,7 +374,7 @@ include::nut_usb_addvars.txt[] *subdriver =* 'string':: Select a serial-over-USB subdriver to use. -You have a choice between *cypress*, *fabula*, *fuji*, *hunnox*, *ippon*, *krauler*, *phoenix*, *phoenixtec*, *sgs*, *snr*, *armac* and *ablerex*. +You have a choice between *ablerex*, *armac*, *cypress*, *fabula*, *fuji*, *gtec*, *hunnox*, *ippon*, *krauler*, *phoenix*, *phoenixtec*, *sgs* and *snr*. + Run the driver program with the `--help` option to see the exact list of `subdriver` values it would currently recognize. @@ -405,6 +405,38 @@ the older standalone `richcomm_usb` driver. This subdriver, meant to be used with the 'megatec' protocol, does *not* support the various *test.battery* commands. Plus, the *shutdown.return* command ignores the values set in 'ups.delay.start'/*ondelay* and makes the UPS turn on the load as soon as power is back. +*'gtec' subdriver*:: +Currently, the Gtec specific support is only known to work with USB devices +(tested with a Gtec ZP120N), and was not seen with Serial port. ++ +This mode is not automatically detected, and should be enabled manually in +your 'ups.conf', e.g.: ++ +---- +[gtec-ups] + driver = "nutdrv_qx" + port = "auto" + subdriver = "gtec" + protocol = "gtec" +---- ++ +Other subdrivers and protocol implementations (including linkman:blazer_usb[8]) +sort of work, but both have two problems: ++ +* They use the simple "Q1" query, which doesn't report the result of + the latest battery test. + - For some reason the UPS reports normal battery voltage even when + the battery is completely disconnected. + So you won't know the battery is dead until a power failure. + - This driver sends the more advanced "Q4" request instead. + Here the answer includes status letters with more information + than the Q1 binary flags, including battery status. +* USB reply is read in 8-byte chunks, which causes the UPS to disconnect + from USB. The UPS reconnects in a second, but it still breaks the + initialization of `nutdrv_qx`, where the query is sent twice in a + short time (unlike `blazer_usb`). + - The solution is simple: read the whole reply at once. + *'hunnox' subdriver*:: This protocol subdriver is closely related to 'fabula' one, with a few tweaks for devices not directly supported by that driver. diff --git a/docs/nut.dict b/docs/nut.dict index 75e3459665..c65adff6d7 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3317 utf-8 +personal_ws-1.1 en 3319 utf-8 AAC AAS ABI @@ -1440,6 +1440,7 @@ Yoyodyne Yukai Yunto ZFS +ZP ZProject Zaika Zampieri @@ -2035,6 +2036,7 @@ gpiochip graphviz groupadd groupname +gtec gtk guesstimate guesstimation diff --git a/drivers/Makefile.am b/drivers/Makefile.am index ab351d6134..39e9c0ee0a 100644 --- a/drivers/Makefile.am +++ b/drivers/Makefile.am @@ -384,7 +384,7 @@ NUTDRV_QX_SUBDRIVERS = nutdrv_qx_bestups.c nutdrv_qx_blazer-common.c \ nutdrv_qx_mecer.c nutdrv_qx_megatec.c nutdrv_qx_megatec-old.c \ nutdrv_qx_mustek.c nutdrv_qx_q1.c nutdrv_qx_q2.c nutdrv_qx_q6.c nutdrv_qx_voltronic.c \ nutdrv_qx_voltronic-qs.c nutdrv_qx_voltronic-qs-hex.c nutdrv_qx_zinto.c \ - nutdrv_qx_hunnox.c nutdrv_qx_ablerex.c + nutdrv_qx_hunnox.c nutdrv_qx_ablerex.c nutdrv_qx_gtec.c nutdrv_qx_SOURCES += $(NUTDRV_QX_SUBDRIVERS) # ---------------------------------------------------------------------- @@ -405,9 +405,7 @@ dist_noinst_HEADERS = \ upshandler.h usb-common.h usbhid-ups.h powercom-hid.h compaq-mib.h idowell-hid.h \ apcsmart.h apcsmart_tabs.h apcsmart-old.h apcupsd-ups.h cyberpower-mib.h riello.h openups-hid.h \ delta_ups-mib.h nutdrv_qx.h nutdrv_qx_bestups.h nutdrv_qx_blazer-common.h \ - nutdrv_qx_innovart31.h \ - nutdrv_qx_masterguard.h \ - nutdrv_qx_mecer.h nutdrv_qx_ablerex.h \ + nutdrv_qx_gtec.h nutdrv_qx_innovart31.h nutdrv_qx_masterguard.h nutdrv_qx_mecer.h nutdrv_qx_ablerex.h \ nutdrv_qx_megatec.h nutdrv_qx_megatec-old.h nutdrv_qx_mustek.h nutdrv_qx_q1.h nutdrv_qx_q2.h nutdrv_qx_q6.h nutdrv_qx_hunnox.h \ nutdrv_qx_voltronic.h nutdrv_qx_voltronic-qs.h nutdrv_qx_voltronic-qs-hex.h nutdrv_qx_zinto.h \ upsdrvquery.h \ diff --git a/drivers/nutdrv_qx.c b/drivers/nutdrv_qx.c index b614025eb0..62a9166ef5 100644 --- a/drivers/nutdrv_qx.c +++ b/drivers/nutdrv_qx.c @@ -58,7 +58,7 @@ # define DRIVER_NAME "Generic Q* Serial driver" #endif /* QX_USB */ -#define DRIVER_VERSION "0.40" +#define DRIVER_VERSION "0.41" #ifdef QX_SERIAL # include "serial.h" @@ -85,6 +85,7 @@ #include "nutdrv_qx_zinto.h" #include "nutdrv_qx_masterguard.h" #include "nutdrv_qx_ablerex.h" +#include "nutdrv_qx_gtec.h" /* Reference list of available non-USB subdrivers */ static subdriver_t *subdriver_list[] = { @@ -103,6 +104,7 @@ static subdriver_t *subdriver_list[] = { &innovart31_subdriver, &q2_subdriver, &q6_subdriver, + >ec_subdriver, /* Fallback Q1 subdriver */ &q1_subdriver, NULL @@ -1870,6 +1872,79 @@ static void *ablerex_subdriver_fun(USBDevice_t *device) return NULL; } +/* Gtec communication subdriver (based on Cypress) */ +static int gtec_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 (%" PRIuSIZE "), " + "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, 0x02, 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-128) && (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], 128, 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 (%" PRIuSIZE ")", __func__, i); + return -1; + } + return (int)i; +} + static struct { bool_t initialized; bool_t ok; @@ -2929,6 +3004,7 @@ void upsdrv_shutdown(void) { "snr", &snr_command }, { "ablerex", &ablerex_command }, { "armac", &armac_command }, + { "gtec", >ec_command }, { NULL, NULL } }; # endif diff --git a/drivers/nutdrv_qx_gtec.c b/drivers/nutdrv_qx_gtec.c new file mode 100644 index 0000000000..1f30be19af --- /dev/null +++ b/drivers/nutdrv_qx_gtec.c @@ -0,0 +1,231 @@ +/* nutdrv_qx_gtec.c - Subdriver for Gtec UPSes + * + * Copyright (C) + * 2025 Lukas Turek + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#include "main.h" +#include "nutdrv_qx.h" +#include "nutdrv_qx_blazer-common.h" + +#include "nutdrv_qx_gtec.h" + +#define GTEC_VERSION "Gtec 0.01" + +/* Process status letters */ +static int gtec_process_status(item_t *item, char *value, const size_t valuelen) +{ + char *val = "OFF"; + char *letters; + + upsdebugx(10, "%s: Processing status letters %s", __func__, item->value); + + /* NOTE: The main driver calls status_init() and alarm_init() * + * for us in nutdrv_qx.c::upsdrv_updateinfo() */ + /* TOTHINK: Should we status_set() the encountered letters where we + * assign "val" below? It is generally possible that states overlap + * e.g. "!OL + LB", but is it possible on this device? + */ + + for (letters = item->value; *letters; letters++) + { + switch (*letters) + { + case 'A': /* Utility fail */ + + /* FIXME: Isn't this same as the more common "OB"? */ + val = "!OL"; + break; + + case 'B': /* Battery low */ + + val = "LB"; + break; + + case 'C': /* Bypass/boost active */ + + val = "BYPASS"; + break; + + case 'D': /* UPS failed */ + + alarm_set("UPS failed"); + break; + + case 'E': /* Test in progress */ + + dstate_setinfo("battery.test.status", "Test in progress"); + break; + + case 'F': /* Shutdown active */ + + val = "FSD"; + break; + + case 'G': /* Site fault */ + + alarm_set("Site fault"); + break; + + case 'H': /* EPROM fail */ + + alarm_set("EPROM fail"); + break; + + case 'I': /* Test passed - Result: OK */ + dstate_setinfo("battery.test.status", "Test passed - Result: OK"); + break; + + case 'J': /* Test passed - Result: Failed */ + + dstate_setinfo("battery.test.status", "Test passed - Result: Failed"); + break; + + case 'K': /* Test not possible or inhibited */ + + dstate_setinfo("battery.test.status", "Test not possible or inhibited"); + break; + + case 'L': /* Test status unknown */ + + dstate_setinfo("battery.test.status", "Test status unknown"); + break; + + case 'M': /* UPS normal mode */ + + val = "OL"; + break; + + case 'N': /* UPS 110% overload */ + + val = "OVER"; + break; + + default: + upsdebugx(2, "%s: ignoring unknown status character %c", __func__, *letters); + } + } + + /* NOTE: The main driver calls status_commit() and alarm_commit() * + * for us in nutdrv_qx.c::upsdrv_updateinfo() */ + + snprintf(value, valuelen, "%s", val); + return 0; +} + +/* qx2nut lookup table */ +static item_t gtec_qx2nut[] = { + + /* + * > [Q4\r] + * < [(236.7 243.2 212.1 220.1 230.4 002 001 50.0 371 372 041.3 22.9 IM\r] + * 012345678901234567890123456789012345678901234567890123456789012345 + * 0 1 2 3 4 5 6 + */ + + { "input.voltage", 0, NULL, "Q4\r", "", 64, '(', "", 1, 5, "%.1f", 0, NULL, NULL, NULL }, + { "input.voltage.fault", 0, NULL, "Q4\r", "", 64, '(', "", 19, 23, "%.1f", 0, NULL, NULL, NULL }, + { "output.voltage", 0, NULL, "Q4\r", "", 64, '(', "", 25, 29, "%.1f", 0, NULL, NULL, NULL }, + { "ups.load", 0, NULL, "Q4\r", "", 64, '(', "", 35, 37, "%.0f", 0, NULL, NULL, NULL }, + { "input.frequency", 0, NULL, "Q4\r", "", 64, '(', "", 39, 42, "%.1f", 0, NULL, NULL, NULL }, + { "battery.voltage", 0, NULL, "Q4\r", "", 64, '(', "", 52, 56, "%.1f", 0, NULL, NULL, NULL }, + { "ups.temperature", 0, NULL, "Q4\r", "", 64, '(', "", 58, 61, "%.1f", 0, NULL, NULL, NULL }, + { "ups.status", 0, NULL, "Q4\r", "", 64, '(', "", 63, 0, NULL, 0, NULL, NULL, gtec_process_status }, + + /* + * > [F\r] + * < [#220.0 000 024.0 50.0\r] + * 0123456789012345678901 + * 0 1 2 + */ + + { "input.voltage.nominal", 0, NULL, "F\r", "", 22, '#', "", 1, 5, "%.0f", QX_FLAG_STATIC, NULL, NULL, NULL }, + { "input.current.nominal", 0, NULL, "F\r", "", 22, '#', "", 7, 9, "%.1f", QX_FLAG_STATIC, NULL, NULL, NULL }, + { "battery.voltage.nominal", 0, NULL, "F\r", "", 22, '#', "", 11, 15, "%.1f", QX_FLAG_STATIC, NULL, NULL, NULL }, + { "input.frequency.nominal", 0, NULL, "F\r", "", 22, '#', "", 17, 20, "%.0f", QX_FLAG_STATIC, NULL, NULL, NULL }, + + /* + * > [I\r] + * < [#------------- ------ VT12046Q \r] + * 012345678901234567890123456789012345678 + * 0 1 2 3 + */ + + { "device.mfr", 0, NULL, "I\r", "", 39, '#', "", 1, 15, "%s", QX_FLAG_STATIC | QX_FLAG_TRIM, NULL, NULL, NULL }, + { "device.model", 0, NULL, "I\r", "", 39, '#', "", 17, 26, "%s", QX_FLAG_STATIC | QX_FLAG_TRIM, NULL, NULL, NULL }, + { "ups.firmware", 0, NULL, "I\r", "", 39, '#', "", 28, 37, "%s", QX_FLAG_STATIC | QX_FLAG_TRIM, NULL, NULL, NULL }, + + /* Instant commands */ + { "beeper.toggle", 0, NULL, "Q\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, + { "load.off", 0, NULL, "S00R0000\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, + { "load.on", 0, NULL, "C\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, + { "shutdown.return", 0, NULL, "S%s\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, blazer_process_command }, + { "shutdown.stayoff", 0, NULL, "S%sR0000\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, blazer_process_command }, + { "shutdown.stop", 0, NULL, "C\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, + { "test.battery.start", 0, NULL, "T%02d\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, blazer_process_command }, + { "test.battery.start.deep", 0, NULL, "TL\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, + { "test.battery.start.quick", 0, NULL, "T\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, + { "test.battery.stop", 0, NULL, "CT\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, + + /* Server-side settable vars */ + { "ups.delay.start", ST_FLAG_RW, blazer_r_ondelay, NULL, "", 0, 0, "", 0, 0, DEFAULT_ONDELAY, QX_FLAG_ABSENT | QX_FLAG_SETVAR | QX_FLAG_RANGE, NULL, NULL, blazer_process_setvar }, + { "ups.delay.shutdown", ST_FLAG_RW, blazer_r_offdelay, NULL, "", 0, 0, "", 0, 0, DEFAULT_OFFDELAY, QX_FLAG_ABSENT | QX_FLAG_SETVAR | QX_FLAG_RANGE, NULL, NULL, blazer_process_setvar }, + + /* End of structure. */ + { NULL, 0, NULL, NULL, "", 0, 0, "", 0, 0, NULL, 0, NULL, NULL, NULL } +}; + +/* Testing table */ +#ifdef TESTING +static testing_t gtec_testing[] = { + { "Q4\r", "(236.7 243.2 212.1 220.1 230.4 002 001 50.0 371 372 041.3 22.9 IM\r", -1 }, + { "F\r", "#230.0 000 024.0 50.0\r", -1 }, + { "I\r", "#NOT_A_LIVE_UPS TESTING TESTING \r", -1 }, + { "Q\r", "", -1 }, + { "S03\r", "", -1 }, + { "C\r", "", -1 }, + { "S02R0005\r", "", -1 }, + { "S.5R0000\r", "", -1 }, + { "T04\r", "", -1 }, + { "TL\r", "", -1 }, + { "T\r", "", -1 }, + { "CT\r", "", -1 }, + { NULL } +}; +#endif /* TESTING */ + +/* Subdriver-specific initups */ +static void gtec_initups(void) +{ + blazer_initups(gtec_qx2nut); +} + +/* Subdriver interface */ +subdriver_t gtec_subdriver = { + GTEC_VERSION, + blazer_claim, + gtec_qx2nut, + gtec_initups, + NULL, + blazer_makevartable, + "ACK", + NULL, +#ifdef TESTING + gtec_testing, +#endif /* TESTING */ +}; diff --git a/drivers/nutdrv_qx_gtec.h b/drivers/nutdrv_qx_gtec.h new file mode 100644 index 0000000000..9655a833a6 --- /dev/null +++ b/drivers/nutdrv_qx_gtec.h @@ -0,0 +1,29 @@ +/* nutdrv_qx_gtec.h - Subdriver for Gtec UPSes + * + * Copyright (C) + * 2025 Lukas Turek + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef NUTDRV_QX_GTEC_H +#define NUTDRV_QX_GTEC_H + +#include "nutdrv_qx.h" + +extern subdriver_t gtec_subdriver; + +#endif /* NUTDRV_QX_GTEC_H */