From 4e1aa23b32376658f455110cf9d1c880c0176ff2 Mon Sep 17 00:00:00 2001 From: Yuriy_Y Date: Wed, 12 Aug 2026 14:07:14 +0500 Subject: [PATCH] nutdrv_qx: add richcomm-svc USB transport [#3562] Add an explicit USB communication subdriver for older session-initialized Richcomm USB-to-UART bridges. The tested SVC V-1000F uses the existing Megatec protocol and Armac command framing, but requires a 2400-baud report and a 64-byte session-opening sequence before it accepts commands. Ignore leading empty HID reports from this bridge and reset its transport state after USB reconnection. Use the 6-byte reads required by the V-1000F without changing the 8-byte reads used by normal armac devices. Keep the mode opt-in because 0925:1234 is shared by devices with different communication behavior. Document the mandatory configuration, tested hardware and untested command scope in the man page, HCL and NEWS. Development and documentation were assisted by OpenAI Codex. The submitter reviewed the changes and verified the hardware behavior. Signed-off-by: Yuriy_Y --- NEWS.adoc | 5 ++ data/driver.list.in | 1 + docs/man/nutdrv_qx.txt | 27 +++++++++- drivers/nutdrv_qx.c | 111 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 139 insertions(+), 5 deletions(-) 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); }