diff --git a/NEWS b/NEWS index 11385cf61d..72dc477fb7 100644 --- a/NEWS +++ b/NEWS @@ -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 diff --git a/data/driver.list.in b/data/driver.list.in index e175a6bc4d..f51801fc0b 100644 --- a/data/driver.list.in +++ b/data/driver.list.in @@ -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" diff --git a/docs/nut.dict b/docs/nut.dict index 21f5195b53..028feaaea8 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3196 utf-8 +personal_ws-1.1 en 3198 utf-8 AAS ABI ACFAIL @@ -449,6 +449,7 @@ HFILE HIDIOCINITREPORT HIDRDD HITRANS +HL HMAC HNX HOMEBREW @@ -1365,6 +1366,7 @@ Viewsonic Viktor VirCIO Vout +Vultech Václav WALKMODE WARNFATAL @@ -1613,8 +1615,8 @@ bsd bsv bt bti -btnG btn +btnG btt buckboosthyst buckvolts @@ -2649,10 +2651,10 @@ qx's qxflags rD rackmount +raquo raritan ratedva ratedwatts -raquo rb rcctl readline diff --git a/docs/nutdrv_qx-subdrivers.txt b/docs/nutdrv_qx-subdrivers.txt index 8d786450b9..bc0dde9a61 100644 --- a/docs/nutdrv_qx-subdrivers.txt +++ b/docs/nutdrv_qx-subdrivers.txt @@ -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 ~~~~~ diff --git a/drivers/nutdrv_qx.c b/drivers/nutdrv_qx.c index 350c555a06..9abaa1de3b 100644 --- a/drivers/nutdrv_qx.c +++ b/drivers/nutdrv_qx.c @@ -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 @@ -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;) { @@ -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", @@ -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.");