-
-
Notifications
You must be signed in to change notification settings - Fork 414
Description
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:
- A specific header byte:
0xA0 | length. - 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"