diff --git a/NEWS.adoc b/NEWS.adoc index 16e339db4a..36a56190dd 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -83,6 +83,11 @@ https://github.com/networkupstools/nut/milestone/13 reconnection behavior. [issue #3510, PR #3543] - `nutdrv_qx` driver updates: + * Added the `richcomm-svc` USB communication subdriver for older + session-initialized Richcomm bridges. It configures the bridge before + reusing the existing Armac command framing and Megatec protocol. Polling + and a quick battery test were verified with an SVC V-1000F + (`0925:1234`, `UPS USB MON V1.4`). [issue #3562] * Only claim a USB device as "supported" during discovery out of the box if `subdriver_command` was assigned (`1A86:7523` is used by CH340/341 USB chips not only in UPSes). [issue #3410] diff --git a/data/driver.list.in b/data/driver.list.in index 0d00169dd7..0dd6edb64c 100644 --- a/data/driver.list.in +++ b/data/driver.list.in @@ -1453,6 +1453,7 @@ "SuperPower" "ups" "2" "HP360, Hope-550" "" "blazer_ser" "SVC" "ups" "2" "VP-1250-LCD 1250VA" "(USB ID 0925:1234, armac subdriver, megatec protocol)" "nutdrv_qx" # https://github.com/networkupstools/nut/pull/3485 +"SVC" "ups" "1" "V-1000F" "USB (USB ID 0925:1234, product UPS USB MON V1.4)" "nutdrv_qx port=auto vendorid=0925 productid=1234 protocol=megatec subdriver=richcomm-svc" # https://github.com/networkupstools/nut/issues/3562 ; polling and quick battery test verified on physical hardware; deep battery tests and load or shutdown commands untested "SVEN" "ups" "2" "Power Pro+ series" "USB" "blazer_usb (USB ID ffff:0000)" "SVEN" "ups" "2" "Power Pro+ series" "USB" "blazer_usb (USB ID 05b8:0000)" diff --git a/docs/man/nutdrv_qx.txt b/docs/man/nutdrv_qx.txt index 981d549097..04956306ea 100644 --- a/docs/man/nutdrv_qx.txt +++ b/docs/man/nutdrv_qx.txt @@ -487,7 +487,7 @@ include::nut_usb_addvars.txt[] *subdriver =* 'string':: Select a USB communication subdriver to use. -You have a choice between *ablerex*, *armac*, *cypress*, *fabula*, *fuji*, *gtec*, *hunnox*, *ippon*, *krauler*, *omron*, *phoenix*, *phoenixtec*, *sgs* and *snr*. +You have a choice between *ablerex*, *armac*, *cypress*, *fabula*, *fuji*, *gtec*, *hunnox*, *ippon*, *krauler*, *omron*, *phoenix*, *phoenixtec*, *richcomm-svc*, *sgs* and *snr*. + Run the driver program with the `--help` option to see the exact list of `subdriver` values it would currently recognize. @@ -514,6 +514,31 @@ specific: its banner is "2004 Richcomm Technologies, Inc. Dec 27 2005 ver 1.1." Maybe other Richcomm UPSes would work with this -- maybe better than with the older standalone `richcomm_usb` driver. +*'richcomm-svc' subdriver*:: +This USB communication subdriver supports older session-initialized Richcomm +USB-to-UART bridges. It was developed and tested with an SVC V-1000F whose +USB ID is `0925:1234` and product string is `UPS USB MON V1.4`. ++ +The bridge must be configured for 2400 baud and receive a session-opening +sequence before it accepts Megatec commands. After that initialization this +subdriver reuses the Armac command framing and the existing Megatec protocol, +with the 6-byte interrupt reads required by this hardware. +Because `0925:1234` is shared by devices with different communication details, +this mode is not selected automatically. Configure it explicitly, for example: ++ +---- +[svc-v1000f] + driver = "nutdrv_qx" + port = "auto" + subdriver = "richcomm-svc" + protocol = "megatec" + vendorid = "0925" + productid = "1234" +---- ++ +Polling and `test.battery.start.quick` were verified on physical hardware. +Deep battery tests and load or shutdown commands were not tested. + *'fabula' subdriver*:: 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. diff --git a/drivers/nutdrv_qx.c b/drivers/nutdrv_qx.c index b3d8868441..732dd427eb 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.54" +#define DRIVER_VERSION "0.55" #ifdef QX_SERIAL # include "serial.h" @@ -2239,6 +2239,9 @@ static struct { uint16_t out_wMaxPacketSize; } armac_endpoint_cache = { .initialized = FALSE, .ok = FALSE }; +static bool_t richcomm_svc_baud_initialized = FALSE; +static bool_t richcomm_svc_session_initialized = FALSE; + static void load_armac_endpoint_cache(void) { #if WITH_LIBUSB_1_0 @@ -2339,16 +2342,82 @@ static void load_armac_endpoint_cache(void) */ #define ARMAC_READ_SIZE_FOR_CONTROL 8 #define ARMAC_READ_SIZE_FOR_INTERRUPT 64 -static int armac_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) +/* V-1000F returns LIBUSB_ERROR_OVERFLOW when asked for 8 bytes. */ +#define RICHCOMM_SVC_READ_SIZE_FOR_CONTROL 6 + +static int richcomm_svc_initialize(void) +{ + char report[64]; + int ret; + size_t offset; + + if (!richcomm_svc_baud_initialized) { + report[0] = (char)0x80; + report[1] = (char)0xd0; + report[2] = 0x00; + report[3] = 0x00; + + ret = usb_control_msg(udev, + USB_ENDPOINT_OUT + USB_TYPE_CLASS + USB_RECIP_INTERFACE, + 0x09, 0x0200, 0, + (usb_ctrl_charbuf)report, 4, 5000); + if (ret <= 0) { + upsdebugx(1, "richcomm-svc: failed to configure 2400-baud transport: %s (%d)", + ret ? nut_usb_strerror(ret) : "timeout", ret); + return ret; + } + if (ret != 4) { + upsdebugx(1, "richcomm-svc: short baud-rate report transfer (%d of 4 bytes)", ret); + return -1; + } + + richcomm_svc_baud_initialized = TRUE; + usleep(100000); + } + + if (richcomm_svc_session_initialized) { + return 1; + } + + /* SVC V-1000F (0925:1234) requires this 64-byte session-opening + * sequence before its USB-to-UART bridge accepts Megatec commands. */ + report[0] = 0x40; + for (offset = 1; offset < sizeof(report); offset++) { + report[offset] = (char)(offset * 73u + 29u); + } + + for (offset = 0; offset < sizeof(report); offset += 4) { + ret = usb_control_msg(udev, + USB_ENDPOINT_OUT + USB_TYPE_CLASS + USB_RECIP_INTERFACE, + 0x09, 0x0200, 0, + (usb_ctrl_charbuf)(report + offset), 4, 5000); + if (ret <= 0) { + upsdebugx(1, "richcomm-svc: failed to send session-opening report: %s (%d)", + ret ? nut_usb_strerror(ret) : "timeout", ret); + return ret; + } + if (ret != 4) { + upsdebugx(1, "richcomm-svc: short session-opening report transfer (%d of 4 bytes)", ret); + return -1; + } + } + + richcomm_svc_session_initialized = TRUE; + upsdebugx(3, "richcomm-svc: session-opening sequence sent"); + usleep(100000); + return 1; +} + +static int armac_command_internal(const char *cmd, size_t cmdlen, char *buf, size_t buflen, bool_t use_richcomm_svc) { char tmpbuf[ARMAC_READ_SIZE_FOR_INTERRUPT]; int ret = 0; - size_t i, bufpos; + size_t i, bufpos, idle_reports = 0; const size_t cmdstrlen = strnlen(cmd, cmdlen); /* Length of cmd string (excluding terminating '\0'), or cmdlen if the string is too long */ const size_t cmddatalen = cmdstrlen >= cmdlen ? cmdlen : cmdstrlen + 1; /* Amount of useful/valid data bytes in cmd string (max=cmdlen, or length of cmd+'\0' if the string is short enough) */ const size_t tmplen = cmddatalen > sizeof(tmpbuf) ? sizeof(tmpbuf) : cmddatalen; /* How much of cmd[] we can copy into tmp[] so it fits (and remains useful), including the terminating '\0' */ bool_t use_interrupt = FALSE; - int read_size = ARMAC_READ_SIZE_FOR_CONTROL; + int read_size = use_richcomm_svc ? RICHCOMM_SVC_READ_SIZE_FOR_CONTROL : ARMAC_READ_SIZE_FOR_CONTROL; /* UPS ignores (doesn't echo back) unsupported commands which makes * the initialization long. List commands tested to be unsupported: @@ -2376,6 +2445,13 @@ static int armac_command(const char *cmd, size_t cmdlen, char *buf, size_t bufle load_armac_endpoint_cache(); } + if (use_richcomm_svc) { + ret = richcomm_svc_initialize(); + if (ret <= 0) { + return ret; + } + } + for (i = 0; unsupported[i] != NULL; i++) { if (strcmp(cmd, unsupported[i]) == 0) { upsdebugx(2, @@ -2492,6 +2568,15 @@ static int armac_command(const char *cmd, size_t cmdlen, char *buf, size_t bufle */ bytes_available = (unsigned char)tmpbuf[0] & 0x3f; if (bytes_available == 0) { + /* This bridge can emit idle HID reports before UART data. */ + if (use_richcomm_svc && bufpos == 0) { + if (++idle_reports > 5) { + upsdebugx(4, "richcomm-svc: no UART reply after idle HID reports"); + break; + } + upsdebugx(4, "richcomm-svc: ignoring idle HID report before response"); + continue; + } /* End of transfer */ break; } @@ -2556,6 +2641,18 @@ static int armac_command(const char *cmd, size_t cmdlen, char *buf, size_t bufle return (int)bufpos; } +static int armac_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) +{ + return armac_command_internal(cmd, cmdlen, buf, buflen, FALSE); +} + +static int richcomm_svc_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) +{ + /* The bridge needs one-time transport initialization, then uses the + * existing Armac command framing with the Megatec protocol. */ + return armac_command_internal(cmd, cmdlen, buf, buflen, TRUE); +} + static void *cypress_subdriver(USBDevice_t *device) { @@ -3387,6 +3484,7 @@ void upsdrv_shutdown(void) { "snr", &snr_command }, { "ablerex", &ablerex_command }, { "armac", &armac_command }, + { "richcomm-svc", &richcomm_svc_command }, { "gtec", >ec_command }, { NULL, NULL } }; @@ -4076,6 +4174,11 @@ static ssize_t qx_command(const char *cmd, size_t cmdlen, char *buf, size_t bufl return ret; } + if (subdriver_command == &richcomm_svc_command) { + richcomm_svc_baud_initialized = FALSE; + richcomm_svc_session_initialized = FALSE; + } + reconnect_trying(RECONNECT_UPDATEINFO); }