Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ as part of https://github.com/networkupstools/nut/issues/1410 solution.
- nutdrv_qx updates:
* the `voltronic_qs_protocol` should now accept both "V" (as before)
and newly "H" dialects, which otherwise seem interchangeable [#1623]
* the `armac` subdriver was enhanced to support devices with a different
response pattern than previously expected per initial contribution.
It was tested to work with Vultech V2000 and Armac PF1 series. [#1978]

- usbhid-ups updates:
* cps-hid subdriver now applies same report descriptor fixing logic to
Expand Down
2 changes: 1 addition & 1 deletion data/driver.list.in
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
"ARTronic" "ups" "2" "ARTon Platinium Combo 3.1 10/15/20 kVA" "USB" "blazer_usb"
"ARTronic" "ups" "2" "ARTon Platinium RT 1/2/3/6/10 kVA" "USB" "blazer_usb"

"Armac" "ups" "2" "R/2000I/PSW" "(USB ID 0925:1234)" "nutdrv_qx"
"Armac" "ups" "2" "R/2000I/PSW and PF1 series" "(USB ID 0925:1234)" "nutdrv_qx"

"ASEM SPA" "ups" "5" "PB1300 UPS" "i2c" "asem"

Expand Down
8 changes: 5 additions & 3 deletions docs/nut.dict
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
personal_ws-1.1 en 3196 utf-8
personal_ws-1.1 en 3198 utf-8
AAS
ABI
ACFAIL
Expand Down Expand Up @@ -449,6 +449,7 @@ HFILE
HIDIOCINITREPORT
HIDRDD
HITRANS
HL
HMAC
HNX
HOMEBREW
Expand Down Expand Up @@ -1365,6 +1366,7 @@ Viewsonic
Viktor
VirCIO
Vout
Vultech
Václav
WALKMODE
WARNFATAL
Expand Down Expand Up @@ -1613,8 +1615,8 @@ bsd
bsv
bt
bti
btnG
btn
btnG
btt
buckboosthyst
buckvolts
Expand Down Expand Up @@ -2649,10 +2651,10 @@ qx's
qxflags
rD
rackmount
raquo
raritan
ratedva
ratedwatts
raquo
rb
rcctl
readline
Expand Down
170 changes: 170 additions & 0 deletions docs/nutdrv_qx-subdrivers.txt
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,176 @@ Return the currently processed status so that it can be checked with one of the
If you need to edit the current status call this function with one of the NUT status (all but +OB+ are supported, simply set it as not +OL+); prefix them with an exclamation mark if you want to clear them from the status (e.g. +!OL+).


Armac Subdriver
~~~~~~~~~~~~~~~

Armac subdriver is based on reverse engineering of Power Manager II software by
Richcomm Technologies written in 2005 that is still (as of 2023) being
distributed as a valid software for freshly sold UPS of various manufacturers.
It uses commands as defined for Megatec protocol - but has a different
communication mechanism.

It uses two types of USB interrupt transfers:
- 4 bytes to send a command (usually single transfer).
- 6 byte chunk to read a reply (multiple transfers).

Transfers are similar to those of the richcomm nut driver, but the transferred
data is not short binary commands. Instead, serial text data is overlaid in
these transfers in a way that creates a badly made USB serial interface. UPS
reply looks similar to this:

0 1 2 3 4 5
HL 00 00 00 00 00

HL is a control byte. Its high nibble meaning is unknown. It changes between
two possible values during transmission. Low nibble encodes number of bytes
that have a meaning in the transaction. For example there are 5 bytes that
might contain ASCII serial data, but only some might be valid, and other might
be random, stale buffer data, etc.

What follows is set of observed transmissions by various UPSes gathered from
Github issues.

Transfer dumps
^^^^^^^^^^^^^^

#### Vultech V2000

----
419.987514 [D4] armac command Q1
419.988307 [D4] armac cleanup ret i=0 ret=6 ctrl=c0
420.119402 [D4] read: ret 6 buf 81: 28 30 31 30 30 >(0100<
420.130383 [D4] read: ret 6 buf c1: 32 30 31 30 30 >20100<
420.141408 [D4] read: ret 6 buf 82: 33 33 31 30 30 >33100<
420.152201 [D4] read: ret 6 buf c3: 2e 30 20 30 30 >.0 00<
420.153237 [D4] read: ret 6 buf 82: 30 30 20 30 30 >00 00<
420.164299 [D4] read: ret 6 buf c1: 30 30 20 30 30 >00 00<
420.175293 [D4] read: ret 6 buf 82: 2e 30 20 30 30 >.0 00<
420.186358 [D4] read: ret 6 buf c3: 20 32 33 30 30 > 2300<
420.190322 [D4] read: ret 6 buf 83: 33 2e 30 30 30 >3.000<
420.194323 [D4] read: ret 6 buf c1: 20 2e 30 30 30 > .000<
420.205358 [D4] read: ret 6 buf 81: 30 2e 30 30 30 >0.000<
420.216318 [D4] read: ret 6 buf c2: 31 34 30 30 30 >14000<
420.227445 [D4] read: ret 6 buf 83: 20 34 39 30 30 > 4900<
420.228334 [D4] read: ret 6 buf c2: 2e 30 39 30 30 >.0900<
420.239461 [D4] read: ret 6 buf 81: 20 30 39 30 30 > 0900<
420.250411 [D4] read: ret 6 buf c2: 32 37 39 30 30 >27900<
420.261405 [D4] read: ret 6 buf 83: 2e 30 20 30 30 >.0 00<
420.265468 [D4] read: ret 6 buf c3: 32 30 2e 30 30 >20.00<
420.269465 [D4] read: ret 6 buf 81: 38 30 2e 30 30 >80.00<
420.280322 [D4] read: ret 6 buf c1: 20 30 2e 30 30 > 0.00<
420.291469 [D4] read: ret 6 buf 82: 30 30 2e 30 30 >00.00<
420.302465 [D4] read: ret 6 buf c3: 30 30 31 30 30 >00100<
420.303511 [D4] read: ret 6 buf 82: 00 30 31 30 30 > <- This has 0x00 and '0', will be read as "00"
420.303515 [D3] found null byte in status bits at 43 byte, assuming 0.
420.314425 [D4] read: ret 6 buf c1: 31 30 31 30 30 >10100< <- this has '1'
420.325432 [D4] read: ret 6 buf 81: 0d 30 31 30 30 >.0100< <- and this finishes with `\r`.
420.325442 [D3] armac command Q1 response read: '(233.0 000.0 233.0 014 49.0 27.0 20.8 00001001'
----

----
1.185164 [D4] armac command ID
1.316257 [D4] read: ret 6 buf c1: 23 31 00 30 30 >#1
1.327309 [D4] read: ret 6 buf 81: 20 31 00 30 30 > 1
1.338264 [D4] read: ret 6 buf c2: 20 20 00 30 30 >
1.349151 [D4] read: ret 6 buf 83: 20 20 20 30 30 > 00<
1.360277 [D4] read: ret 6 buf c2: 20 20 20 30 30 > 00<
1.371322 [D4] read: ret 6 buf 83: 20 20 20 30 30 > 00<
1.382265 [D4] read: ret 6 buf c3: 20 20 20 30 30 > 00<
1.393156 [D4] read: ret 6 buf 82: 20 20 20 30 30 > 00<
1.404324 [D4] read: ret 6 buf c3: 20 20 20 30 30 > 00<
1.415342 [D4] read: ret 6 buf 83: 20 20 20 30 30 > 00<
1.426292 [D4] read: ret 6 buf c2: 20 20 20 30 30 > 00<
1.437203 [D4] read: ret 6 buf 83: 20 20 20 30 30 > 00<
1.448328 [D4] read: ret 6 buf c3: 56 34 2e 30 30 >V4.00<
1.459293 [D4] read: ret 6 buf 82: 31 30 2e 30 30 >10.00<
1.470274 [D4] read: ret 6 buf c3: 20 20 20 30 30 > 00<
1.481208 [D4] read: ret 6 buf 82: 20 20 20 30 30 > 00<
1.492261 [D4] read: ret 6 buf c1: 0d 20 20 30 30 >
1.492270 [D3] armac command ID response read: '# V4.10 '
----

----
4.749667 [D4] armac command F
4.876638 [D4] read: ret 6 buf 81: 23 31 00 30 30 >#1
4.887614 [D4] read: ret 6 buf c1: 32 31 00 30 30 >21
4.898644 [D4] read: ret 6 buf 82: 32 30 00 30 30 >20
4.909595 [D4] read: ret 6 buf c3: 2e 30 20 30 30 >.0 00<
4.920648 [D4] read: ret 6 buf 82: 30 30 20 30 30 >00 00<
4.931629 [D4] read: ret 6 buf c3: 35 20 32 30 30 >5 200<
4.942601 [D4] read: ret 6 buf 83: 34 2e 30 30 30 >4.000<
4.953666 [D4] read: ret 6 buf c2: 30 20 30 30 30 >0 000<
4.964535 [D4] read: ret 6 buf 83: 35 30 2e 30 30 >50.00<
4.975540 [D4] read: ret 6 buf c2: 30 0d 2e 30 30 >0
4.975546 [D3] armac command F response read: '#220.0 005 24.00 50.0'
----

#### Armac R/2000I/PSW

----
112.966856 [D4] armac command Q1
112.968197 [D4] armac cleanup ret i=0 ret=6 ctrl=c0 <- Cleanups required.
113.091193 [D4] read: ret 6 buf 81: 28 30 0d 2e 30 >(0 <- Usually 1-3 bytes available in transfer.
113.103211 [D4] read: ret 6 buf c1: 30 30 0d 2e 30 >00
113.115180 [D4] read: ret 6 buf 82: 30 30 0d 2e 30 >00
113.117144 [D4] read: ret 6 buf c3: 2e 30 20 2e 30 >.0 .0<
113.120150 [D4] read: ret 6 buf 81: 31 30 20 2e 30 >10 .0<
113.132178 [D4] read: ret 6 buf c1: 34 30 20 2e 30 >40 .0<
113.144159 [D4] read: ret 6 buf 82: 30 2e 20 2e 30 >0. .0<
113.146149 [D4] read: ret 6 buf c3: 30 20 32 2e 30 >0 2.0<
113.149173 [D4] read: ret 6 buf 81: 32 20 32 2e 30 >2 2.0<
113.161167 [D4] read: ret 6 buf c1: 37 20 32 2e 30 >7 2.0<
113.173159 [D4] read: ret 6 buf 82: 2e 30 32 2e 30 >.02.0<
113.175157 [D4] read: ret 6 buf c3: 20 30 30 2e 30 > 00.0<
113.178158 [D4] read: ret 6 buf 81: 32 30 30 2e 30 >200.0<
113.190157 [D4] read: ret 6 buf c1: 20 30 30 2e 30 > 00.0<
113.202161 [D4] read: ret 6 buf 82: 30 30 30 2e 30 >000.0<
113.204154 [D4] read: ret 6 buf c3: 2e 30 20 2e 30 >.0 .0<
113.207150 [D4] read: ret 6 buf 81: 34 30 20 2e 30 >40 .0<
113.219174 [D4] read: ret 6 buf c1: 36 30 20 2e 30 >60 .0<
113.231165 [D4] read: ret 6 buf 82: 2e 38 20 2e 30 >.8 .0<
113.233157 [D4] read: ret 6 buf c3: 20 35 36 2e 30 > 56.0<
113.237149 [D4] read: ret 6 buf 81: 2e 35 36 2e 30 >.56.0<
113.249168 [D4] read: ret 6 buf c1: 30 35 36 2e 30 >056.0<
113.261155 [D4] read: ret 6 buf 83: 20 31 30 2e 30 > 10.0<
113.263151 [D4] read: ret 6 buf c2: 30 30 30 2e 30 >000.0<
113.266152 [D4] read: ret 6 buf 81: 31 30 30 2e 30 >100.0<
113.278161 [D4] read: ret 6 buf c1: 30 30 30 2e 30 >000.0< <- No Null bytes.
113.290155 [D4] read: ret 6 buf 82: 30 30 30 2e 30 >000.0<
113.292159 [D4] read: ret 6 buf c1: 0d 30 30 2e 30 >
113.292169 [D3] armac command Q1 response read: '(000.0 140.0 227.0 002 00.0 46.8 56.0 10001000'
----

Next query would return 0x80 control byte - 0 available bytes. This used to
terminate transmission, but some UPS don't work like that.


#### Armac R/3000I/PF1

----
0.083301 [D4] armac command Q1
0.164847 [D4] read: ret 6 buf a6: 28 32 34 31 2e >(241.<
0.184839 [D4] read: ret 6 buf 86: 35 20 30 30 30 >5 000<
0.205851 [D4] read: ret 6 buf a6: 2e 30 20 32 33 >.0 23<
0.226849 [D4] read: ret 6 buf 86: 30 2e 33 20 30 >0.3 0<
0.247859 [D4] read: ret 6 buf a6: 30 30 20 34 39 >00 49<
0.268862 [D4] read: ret 6 buf 86: 2e 39 20 32 2e >.9 2.<
0.289857 [D4] read: ret 6 buf a6: 32 35 20 34 38 >25 48<
0.309866 [D4] read: ret 6 buf 86: 2e 30 20 30 30 >.0 00<
0.330863 [D4] read: ret 6 buf a6: 30 30 30 30 30 >00000<
0.827913 [D4] read: ret 6 buf 83: 31 0d 30 30 30 >1 000<
0.827927 [D3] armac command Q1 response read: '(241.5 000.0 230.3 000 49.9 2.25 48.0 00000001'
0.827954 [D4] armac command ID
1.394985 [D4] read: ret 6 buf a5: 4e 41 4b 0d 30 >NAK <
1.395001 [D3] armac command ID response read: 'NAK'
----

This UPS sends higher nibble set to 6 often, which exceeds available bytes.
Maybe means that more are available. Its serial-USB bridge is probably faster.
We read 5 bytes in case 6 nibble is sent. End of transmission is marked by `\r`,
no 0 nibble is sent.


Notes
~~~~~

Expand Down
84 changes: 73 additions & 11 deletions drivers/nutdrv_qx.c
Comment thread
jimklimov marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -1759,11 +1759,12 @@ static void *ablerex_subdriver_fun(USBDevice_t *device)
* Richcomm Technologies, Inc. Dec 27 2005 ver 1.1." Maybe other Richcomm UPSes
* would work with this - better than with the richcomm_usb driver.
*/
#define ARMAC_READ_SIZE 6
static int armac_command(const char *cmd, char *buf, size_t buflen)
{
char tmpbuf[6];
int ret = 0;
size_t i, bufpos;
char tmpbuf[ARMAC_READ_SIZE];
int ret = 0;
size_t i, bufpos;
const size_t cmdlen = strlen(cmd);

/* UPS ignores (doesn't echo back) unsupported commands which makes
Expand All @@ -1788,6 +1789,17 @@ static int armac_command(const char *cmd, char *buf, size_t buflen)
}
upsdebugx(4, "armac command %.*s", (int)strcspn(cmd, "\r"), cmd);

/* Cleanup buffer before sending a new command */
for (i = 0; i < 10; i++) {
ret = usb_interrupt_read(udev, 0x81,
(usb_ctrl_charbuf)tmpbuf, ARMAC_READ_SIZE, 100);
if (ret != ARMAC_READ_SIZE) {
// Timeout - buffer is clean.
break;
}
upsdebugx(4, "armac cleanup ret i=%" PRIuSIZE " ret=%d ctrl=%02hhx", i, ret, tmpbuf[0]);
}

/* Send command to the UPS in 3-byte chunks. Most fit 1 chunk, except for eg.
* parameterized tests. */
for (i = 0; i < cmdlen;) {
Expand All @@ -1810,21 +1822,25 @@ static int armac_command(const char *cmd, char *buf, size_t buflen)
return ret;
}

/* Wait for response to buffer */
usleep(2000);
memset(buf, 0, buflen);

bufpos = 0;
while (bufpos + 6 < buflen) {
while (bufpos + ARMAC_READ_SIZE < buflen) {
size_t bytes_available;

/* Read data in 6-byte chunks */
ret = usb_interrupt_read(udev,
0x81,
(usb_ctrl_charbuf)tmpbuf, 6, 1000);
ret = usb_interrupt_read(udev, 0x81,
(usb_ctrl_charbuf)tmpbuf, ARMAC_READ_SIZE, 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) {
if (ret != ARMAC_READ_SIZE) {
/* NOTE: If end condition is invalid for particular UPS we might make one
* request more and get this error. If bufpos > (say) 10 this could be ignored
* and the reply correctly read. */
upsdebugx(1,
"interrupt read error: %s (%d)",
ret ? nut_usb_strerror(ret) : "timeout",
Expand All @@ -1838,20 +1854,66 @@ static int armac_command(const char *cmd, char *buf, size_t buflen)
tmpbuf[0], tmpbuf[1], tmpbuf[2], tmpbuf[3], tmpbuf[4], tmpbuf[5],
tmpbuf[1], tmpbuf[2], tmpbuf[3], tmpbuf[4], tmpbuf[5]);

/*
* On most tested devices (including R/2000I/PSW) this was equal to the number of
* bytes returned in the buffer, but on some newer UPS (R/3000I/PF1) it was 1 more
* (1 control + 5 bytes transferred and bytes_available equal to 6 instead of 5).
*
* Current assumption is that this is number of bytes available on the UPS side
* with up to 5 (ret - 1) transferred.
*/
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 > ARMAC_READ_SIZE - 1) {
/* Single interrupt transfer has 1 control + 5 data bytes */
bytes_available = ARMAC_READ_SIZE - 1;
}

/* Copy bytes into the final buffer while detecting end of line - \r */
for (i = 0; i < bytes_available; i++) {
if (tmpbuf[i + 1] == 0x00 && bufpos == 0) {
/* Happens when a manually turned off UPS is connected to the USB */
upsdebugx(3, "null byte read - is UPS off?");
return 0;
}

/* Vultech V2000 seems to use 0x00 within status bits. This might mean "unsupported".
* or something else completely. */
if (tmpbuf[i + 1] == 0x00) {
if (bufpos >= 38) {
upsdebugx(3, "found null byte in status bits at %" PRIuSIZE " byte, assuming 0.", bufpos);
buf[bufpos++] = '0';
continue;
} else {
upsdebugx(3, "found null byte in data stream - interrupting read.");
/* Break through two loops */
goto end_of_message;
}
}

buf[bufpos++] = tmpbuf[i + 1];

if (tmpbuf[i + 1] == 0x0d) {
if (i + 1 != bytes_available) {
upsdebugx(3, "trailing bytes in serial transmission found: %" PRIuSIZE " copied out of %" PRIuSIZE,
i + 1, bytes_available
);
}
/* Break through two loops */
goto end_of_message;
}
}

if (bytes_available <= 2) {
/* Slow down, let the UPS buffer more bytes */
usleep(15000);
usleep(10000);
}
}
end_of_message:

if (bufpos + 6 >= buflen) {
upsdebugx(2, "Protocol error, too much data read.");
Expand Down