Skip to content

nutdrv_qx / armac: Richcomm HID UPS (0925:1234, ExeGate ServerRM UNL) requires Interrupt-only transfers (Control msg times out) #3243

@Pugemon

Description

@Pugemon

I am working with a Richcomm-based USB HID UPS (VendorID 0925, ProductID 1234),
rebranded as ExeGate ServerRM UNL-1500.

The device responds correctly to Megatec/Q1 protocol, but only when commands
are sent via INTERRUPT OUT endpoint with a Richcomm-specific 0xA0 length header.
Control transfers consistently timeout.

Device:

  • VendorID: 0925
  • ProductID: 1234
  • Manufacturer: RICHCOMM
  • Product: UPS USB Mon V2.0
  • Brand/model: ExeGate ServerRM UNL-1500

USB:

  • Interface class: HID (03)
  • IN endpoint: 0x82 (Interrupt, 64 bytes)
  • OUT endpoint: 0x02 (Interrupt, 64 bytes)

NUT:

  • Version: 2.8.4
  • Driver: nutdrv_qx
  • Subdriver: armac (closest match protocol-wise)
  • libusb: libusb-0.1
  • OS: Linux x86_64

The Problem

The current implementation of armac_command in nutdrv_qx attempts to use usb_control_msg (Control Transfers) to send commands to the device.

On this specific device, Control Transfers result in a timeout (-110). The device does not acknowledge commands sent via Endpoint 0. It appears to strictly require Interrupt OUT for sending commands and Interrupt IN for reading responses.

Additionally, the device requires:

  1. A specific header byte: 0xA0 | length.
  2. Padding to 8 bytes.

Diagnosis

I analyzed the communication used by the vendor-provided software and verified communication using Python/PyUSB scripts. I confirmed that the device works perfectly using the Megatec/Q1 protocol, but only when wrapped in the specific HID Interrupt transport described above.

Working Solution (Proof of Concept)

As a proof of concept, I locally modified drivers/nutdrv_qx.c to force
interrupt-only communication inside armac_command.

This code is provided as a proof of concept only.
I understand that the final solution should preserve compatibility
with existing Armac devices and may require a quirk or detection logic
rather than unconditional interrupt-only mode.

Here is the modified function that works reliably with this device:

/* Modified armac_command to force Interrupt transfers */
static int armac_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen)
{
    unsigned char send_buf[8];
    unsigned char recv_buf[64];
    int ret, i, pos = 0;

    size_t len = strnlen(cmd, cmdlen);
    if (len > 7) len = 7;

    /* SEND (INTERRUPT OUT) */
    memset(send_buf, 0, sizeof(send_buf));
    send_buf[0] = 0xA0 | len;
    memcpy(send_buf + 1, cmd, len);

    upsdebugx(2, "INTERRUPT OUT: %02x %02x %02x %02x",
        send_buf[0], send_buf[1], send_buf[2], send_buf[3]);

    ret = usb_interrupt_write(udev, 0x02, (const char *)send_buf, 8, 1000);
    if (ret < 0) {
        upsdebugx(1, "interrupt write failed: %s", nut_usb_strerror(ret));
        return ret;
    }

    /* Wait for MCU processing */
    usleep(200000);

    /* READ (INTERRUPT IN) */
    usb_clear_halt(udev, 0x82);
    memset(recv_buf, 0, sizeof(recv_buf));

    ret = usb_interrupt_read(udev, 0x82, (char *)recv_buf, sizeof(recv_buf), 3000);
    if (ret < 0) {
        upsdebugx(1, "interrupt read error: %s", nut_usb_strerror(ret));
        return ret;
    }

    /* FILTER NULL BYTES & PARSE */
    for (i = 0; i < ret && pos + 1 < (int)buflen; i++) {

        /* Skip Richcomm HID report prefix 'af' until i find the start of Megatec string */
        if (pos == 0 && recv_buf[i] != '(')
            continue;

        if (recv_buf[i] == 0x00)
            continue;

        buf[pos++] = recv_buf[i];

        if (recv_buf[i] == '\r')
            break;
    }

    buf[pos] = '\0';

    upsdebugx(2, "Clean reply: %s", buf);
    return pos;
}

Successful Logs

With the code above, the driver successfully communicates and parses the Q1 status string:

   0.530464     [D2] INTERRUPT OUT: a3 51 31 0d
   0.832436     [D2] Clean reply: (226.0 000.0 226.0 015 49.0 13.5 30.8 00001001
   0.832461     [D5] send_to_all: SETINFO input.voltage "226.0"
   0.832464     Using protocol: Q1 0.08
   ...
   1.134440     [D5] send_to_all: SETINFO output.voltage "227.0"
   1.134457     [D5] send_to_all: SETINFO ups.load "15"
   1.134479     [D5] send_to_all: SETINFO battery.voltage "13.5"

Metadata

Metadata

Assignees

No one assigned

    Labels

    Connection stability issuesIssues about driver<->device and/or networked connections (upsd<->upsmon...) going AWOL over timeQx protocol driverDriver based on Megatec Q<number> such as new nutdrv_qx, or obsoleted blazer and some othersRichcommUSBimpacts-release-2.8.4Issues reported against NUT release 2.8.4 (maybe vanilla or with minor packaging tweaks)

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions