Skip to content

Commit 8122686

Browse files
authored
Merge pull request #3504 from juslex/feat/ragtech-fixes
ragtech: drain handshake reply, fix battery scaling, add va override
2 parents 2245e84 + f7a2a81 commit 8122686

4 files changed

Lines changed: 148 additions & 32 deletions

File tree

NEWS.adoc

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,35 @@ https://github.com/networkupstools/nut/milestone/13
7575
VP-1250-LCD) that return a 7-byte report instead of the standard 6 bytes,
7676
allowing the Megatec payload to be successfully reconstructed. [PR #3485]
7777

78+
- Introduced a new NUT driver named `ragtech` which provides support for the
79+
Ragtech "Easy Pro" family of line-interactive UPS units (also sold under
80+
the NEP, TORO, INNERGIE and OneUP brands in Brazil). Devices present a USB
81+
CDC-ACM serial interface (VID `0x04D8`, PID `0x000A`) and speak a proprietary
82+
6-byte register-access protocol with read/write/AND/OR opcodes; the driver
83+
covers the 20 models in the family-10 device table and was validated
84+
end-to-end against an Easy 2000 TI. Shutdown-related instant commands are
85+
opt-in via the `allow_shutdown` flag because the firmware does not
86+
auto-restart on mains return. [Initial PR #3447]
87+
* Put the CDC-ACM port into raw mode at startup. A freshly enumerated
88+
port defaults to canonical mode, where the kernel buffers the firmware's
89+
binary replies waiting for a newline that never arrives, so the first
90+
poll timed out as "no reply to initial status poll" unless the port had
91+
been set raw externally. [issue #3500]
92+
* Retry the initial status poll a few times before bailing out, so the
93+
driver starts cleanly under the stock systemd units when the device has
94+
only just enumerated at boot, instead of leaning on a fatal exit and a
95+
`Restart=on-failure` cycle. [issue #3500]
96+
* Drain the firmware's handshake reply before the first status poll.
97+
Some models (e.g. the Easy Pro 3200 VA GT) are half-duplex and dropped
98+
the read command that was sent while the handshake reply was still in
99+
flight. [issue #3500]
100+
* Compute `battery.charge` as `raw / 2.55` so a fully charged battery
101+
reports exactly 100 % instead of 100.2 %.
102+
* Added a `va` option to override the apparent-power rating when the
103+
firmware model id is shared between product lines, correcting
104+
`ups.power.nominal`, `ups.realpower.nominal` and the computed
105+
`ups.load`. [issue #3500]
106+
78107
- `richcomm_usb` driver updates:
79108
* Completed support for NUT standard USB configuration options, and fixed
80109
some problems possible when iterating devices. [issue #1768, PR #3477]
@@ -398,16 +427,6 @@ but the `nutshutdown` script would bail out quickly and quietly. [PR #3008]
398427
subdriver definition then). For consistency, these pointers are now
399428
assigned. [#1962]
400429
401-
- Introduced a new NUT driver named `ragtech` which provides support for the
402-
Ragtech "Easy Pro" family of line-interactive UPS units (also sold under
403-
the NEP, TORO, INNERGIE and OneUP brands in Brazil). Devices present a USB
404-
CDC-ACM serial interface (VID `0x04D8`, PID `0x000A`) and speak a proprietary
405-
6-byte register-access protocol with read/write/AND/OR opcodes; the driver
406-
covers the 20 models in the family-10 device table and was validated
407-
end-to-end against an Easy 2000 TI. Shutdown-related instant commands are
408-
opt-in via the `allow_shutdown` flag because the firmware does not
409-
auto-restart on mains return. [PR #3447]
410-
411430
- `riello_ser` and `riello_usb` driver updates:
412431
* Since the beginning, these drivers fenced availability of *either*
413432
`load.*` *or* `shutdown.return` instant commands based on current

docs/man/ragtech.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ and M2 (220 V) variants, also sold under the NEP, TORO, INNERGIE and
3232
OneUP brands in Brazil -- share the same wire protocol and register
3333
layout and are expected to work; please report results.
3434

35+
Units from other Ragtech lines that share the same USB id (for example
36+
the Easy Pro "GT" series) speak the same protocol but may report a model
37+
id that collides with a table entry, so the driver can pick the wrong VA
38+
rating. Use the *va* option below to set the correct rating in that case.
39+
3540
This driver is *experimental*. Polling, status decoding and the
3641
`shutdown.stayoff` / `shutdown.stop` instant commands have been
3742
verified on hardware; `shutdown.return` and `test.battery.start.deep`
@@ -50,6 +55,15 @@ Per-unit output current calibration constant. If omitted (default),
5055
the driver reads the value from register `0xF3` at startup. Set this
5156
only to override a wrong factory-stored calibration.
5257

58+
*va*='number';;
59+
Override the apparent-power (VA) rating of the unit. The model id byte
60+
reported by the firmware is shared between some product lines (for
61+
example a 3200 VA "GT" unit reports the same id as the Easy 2200 TI),
62+
so the driver cannot always tell them apart and may pick the wrong VA
63+
rating from its model table. Setting *va* to the nameplate rating
64+
corrects `ups.power.nominal`, `ups.realpower.nominal` (VA times the
65+
0.7 power factor) and the computed `ups.load`.
66+
5367
*allow_shutdown*;;
5468
Enable the `shutdown.return`, `shutdown.stayoff`, `shutdown.stop` and
5569
`test.battery.start.deep` instant commands. Disabled by default for

docs/nut.dict

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
personal_ws-1.1 en 3780 utf-8
1+
personal_ws-1.1 en 3781 utf-8
22
AAC
33
AAS
44
ABI
@@ -456,6 +456,7 @@ GPLv
456456
GPSER
457457
GRs
458458
GS
459+
GT
459460
GTK
460461
GTS
461462
GUESSTIMATION

drivers/ragtech.c

Lines changed: 103 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
* Family 10 register map (buf[i] = reg(0x80 + i - 1) for the main range):
5050
*
5151
* buf[ 1] 0x80 F_AUTOSTART(0) F_ICBATTERY(2) F_LINESENS(7)
52-
* buf[ 8] 0x87 V_CBATTERY battery.charge = raw * 0.3930
52+
* buf[ 8] 0x87 V_CBATTERY battery.charge = raw / 2.55 (0..100 %)
5353
* buf[11] 0x8A V_VBATTERY battery.voltage = raw * 0.0670 (or 0.1340 for 24V models)
5454
* buf[12] 0x8B V_VINPUT input.voltage = raw * 1.0600
5555
* buf[13] 0x8C V_IOUTPUT output.current = raw * model_imult / iout_calib
@@ -74,7 +74,10 @@
7474
* fOutput = (53.0 + 4.0 * (V_FOUTPUT - V_OSC53) / (V_OSC57 - V_OSC53)) * 0.9806 + 1.46
7575
*
7676
* The pre-poll handshake "0xFF 0xFE 0x00 0x8E 0x01 0x8F" sent by the OEM
77-
* firmware has unknown semantics; treating as opaque wake-up.
77+
* firmware has unknown semantics; treating as opaque wake-up. The firmware
78+
* answers it with a byte or two (e.g. 0xCA); upsdrv_initinfo drains that
79+
* reply before the first read, since half-duplex units otherwise drop a
80+
* read command that arrives while the handshake reply is still in flight.
7881
*
7982
* Write/action commands (shutdown, fullDischarge, setLineSens, ...) are
8083
* declared in the OEM XML as register-level writes (e.g. shutdown writes
@@ -101,7 +104,7 @@
101104
#include "nut_stdint.h"
102105

103106
#define DRIVER_NAME "Ragtech UPS driver"
104-
#define DRIVER_VERSION "0.07"
107+
#define DRIVER_VERSION "0.10"
105108

106109
upsdrv_info_t upsdrv_info = {
107110
DRIVER_NAME,
@@ -122,6 +125,7 @@ upsdrv_info_t upsdrv_info = {
122125
#define RAGTECH_TIMEOUT_USEC 0
123126
#define RAGTECH_POST_OPEN_MS 200
124127
#define RAGTECH_INTER_CMD_MS 100
128+
#define RAGTECH_INIT_RETRIES 10 /* initial-poll attempts before giving up */
125129

126130
/* Opaque wake-up that the OEM firmware sends once before the first poll. */
127131
static const uint8_t cmd_handshake[] = { 0xFF, 0xFE, 0x00, 0x8E, 0x01, 0x8F };
@@ -217,6 +221,8 @@ static struct ragtech_model fallback_model = {
217221
static double iout_calib = 16.0; /* read from reg 0xF3 at init */
218222
static uint8_t osc53, osc57; /* read from 0x202..0x203 at init */
219223
static int shutdown_enabled; /* opt-in via ups.conf "allow_shutdown" */
224+
static unsigned int va_override = 0; /* ups.conf "va" override (0 = use model table) */
225+
static unsigned int effective_va = 0; /* va_override if set, else model->va */
220226

221227
static const struct ragtech_model *find_model(uint8_t id)
222228
{
@@ -433,17 +439,50 @@ void upsdrv_initinfo(void)
433439
uint8_t reply[64];
434440
uint8_t osc[2];
435441
uint8_t calib;
442+
int tries;
436443

437444
dstate_setinfo("ups.mfr", "%s", "Ragtech");
438445
dstate_setinfo("ups.model", "%s", "Unknown");
439446

440-
if (ser_send_buf(upsfd, cmd_handshake, sizeof(cmd_handshake))
441-
!= (ssize_t)sizeof(cmd_handshake)) {
442-
upslogx(LOG_WARNING, "handshake TX failed");
443-
}
444-
usleep(RAGTECH_INTER_CMD_MS * 1000);
447+
/* Retry the wake-up handshake and first poll a few times before bailing.
448+
* Right after a cold boot the CDC-ACM device may have only just been
449+
* enumerated and the firmware can need a moment to answer the first read.
450+
* Retrying here lets the driver come up cleanly when started by the stock
451+
* NUT systemd units (which launch it as soon as the device node appears),
452+
* instead of relying on a fatal exit plus a Restart=on-failure cycle to
453+
* eventually recover -- which is noisy and only retries on a 5 s cadence. */
454+
for (tries = 0; tries < RAGTECH_INIT_RETRIES; tries++) {
455+
if (ser_send_buf(upsfd, cmd_handshake, sizeof(cmd_handshake))
456+
!= (ssize_t)sizeof(cmd_handshake)) {
457+
upslogx(LOG_WARNING, "handshake TX failed");
458+
}
459+
tcdrain(upsfd);
460+
461+
/* The firmware answers the handshake with a couple of bytes (e.g.
462+
* 0xCA). Some models are half-duplex and silently drop the following
463+
* command if it arrives while that reply is still being transmitted,
464+
* which surfaces as "no reply to initial status poll". Drain the
465+
* handshake reply (short per-chunk timeout) so the first read is only
466+
* sent once the UPS is idle again. Units that do not answer the
467+
* handshake just time out here. */
468+
{
469+
uint8_t hs[16];
470+
int i;
471+
for (i = 0; i < 4; i++) {
472+
if (ser_get_buf(upsfd, hs, sizeof(hs), 0,
473+
RAGTECH_INTER_CMD_MS * 1000) <= 0)
474+
break;
475+
}
476+
}
445477

446-
if (ragtech_read(RAGTECH_MAIN_BASE, RAGTECH_MAIN_LEN, reply) < 0)
478+
if (ragtech_read(RAGTECH_MAIN_BASE, RAGTECH_MAIN_LEN, reply) >= 0)
479+
break;
480+
481+
upsdebugx(1, "initial status poll attempt %d/%d got no reply",
482+
tries + 1, RAGTECH_INIT_RETRIES);
483+
usleep(RAGTECH_POST_OPEN_MS * 1000);
484+
}
485+
if (tries == RAGTECH_INIT_RETRIES)
447486
fatalx(EXIT_FAILURE, "no reply to initial status poll -- is the UPS connected and not held by another program (e.g. Ragtech supsvc)?");
448487

449488
model = find_model(reply[OFF_V_MODEL - 1]);
@@ -453,10 +492,16 @@ void upsdrv_initinfo(void)
453492
model = &fallback_model;
454493
}
455494
dstate_setinfo("ups.model", "%s", model->name);
456-
if (model->va) {
457-
dstate_setinfo("ups.power.nominal", "%u", model->va);
495+
496+
/* Some product lines (e.g. the GT series) reuse a family-10 model id, so
497+
* the id byte alone cannot tell a 2200 TI from a 3200 GT. When the user
498+
* knows the real VA rating they can set "va" in ups.conf; that value then
499+
* drives ups.power.nominal, ups.realpower.nominal and the computed load. */
500+
effective_va = va_override ? va_override : model->va;
501+
if (effective_va) {
502+
dstate_setinfo("ups.power.nominal", "%u", effective_va);
458503
dstate_setinfo("ups.realpower.nominal", "%u",
459-
(unsigned int)(model->va * model->pf + 0.5));
504+
(unsigned int)(effective_va * model->pf + 0.5));
460505
}
461506
{
462507
double v_in_now = reply[OFF_V_VINPUT - 1] * 1.0600;
@@ -515,10 +560,9 @@ void upsdrv_updateinfo(void)
515560

516561
vout = r[OFF_V_VOUTPUT - 1] * model->vmult;
517562
iout = (r[OFF_V_IOUTPUT - 1] * model->imult) / iout_calib;
518-
/* OEM scaling 0.3930 makes raw=255 yield 100.2; clamp so a fully
519-
* charged battery never crosses the [0,100] %-defined range. */
520-
bcharge = r[OFF_V_CBATTERY - 1] * 0.3930;
521-
if (bcharge > 100.0) bcharge = 100.0;
563+
/* Battery charge: raw / 2.55 maps the 0..255 byte onto 0..100 % and
564+
* yields exactly 100.0 at raw=255 (matches the OEM/Node-RED scaling). */
565+
bcharge = r[OFF_V_CBATTERY - 1] / 2.55;
522566

523567
dstate_setinfo("battery.charge", "%.1f", bcharge);
524568
dstate_setinfo("battery.voltage", "%.2f", r[OFF_V_VBATTERY - 1] * model->bmult);
@@ -528,7 +572,7 @@ void upsdrv_updateinfo(void)
528572
/* Reg 0x8D reports load as integer percent and floors to 0 at sub-1%
529573
* loads. Compute apparent-power load for accurate light-load readings. */
530574
dstate_setinfo("ups.load", "%.1f",
531-
model->va > 0 ? (vout * iout) / model->va * 100.0 : 0.0);
575+
effective_va > 0 ? (vout * iout) / effective_va * 100.0 : 0.0);
532576
dstate_setinfo("ups.temperature", "%u", r[OFF_V_TEMPER - 1]);
533577
dstate_setinfo("output.frequency", "%.2f",
534578
compute_frequency(r[OFF_V_FOUTPUT - 1]));
@@ -596,6 +640,11 @@ void upsdrv_makevartable(void)
596640
{
597641
addvar(VAR_VALUE, "iout_calib",
598642
"Override per-unit output current calibration (default: read from reg 0xF3)");
643+
addvar(VAR_VALUE, "va",
644+
"Override the apparent-power (VA) rating used for ups.power.nominal, "
645+
"ups.realpower.nominal and the computed ups.load. Useful when the "
646+
"model id is shared between product lines (e.g. a GT unit reporting "
647+
"the same id as an Easy 2200 TI).");
599648
addvar(VAR_FLAG, "allow_shutdown",
600649
"Enable the shutdown.* and test.battery.start.deep instcmds. "
601650
"WARNING: this UPS firmware does not auto-restart after a "
@@ -607,11 +656,37 @@ void upsdrv_initups(void)
607656
{
608657
const char *v;
609658

610-
/* CDC-ACM only: NEVER call tcsetattr (it pulses DTR on Linux and the
611-
* UPS interprets that as a shutdown signal on some Ragtech families).
612-
* Leave the port at whatever line settings the kernel set on enumeration
613-
* -- CDC-ACM ignores baud at the wire anyway. */
614659
upsfd = ser_open(device_path);
660+
661+
/* A freshly enumerated CDC-ACM tty defaults to canonical mode (ICANON),
662+
* where the kernel buffers incoming bytes until a newline arrives. The
663+
* firmware's replies are raw binary and contain no newline, so the first
664+
* poll never sees them and times out as "no reply to initial status poll".
665+
* Put the port into raw 8N1 mode. Baud is left untouched (CDC-ACM ignores
666+
* it at the wire); CLOCAL plus clearing HUPCL keep this reconfigure and
667+
* the eventual close from toggling DTR. */
668+
#ifndef WIN32
669+
{
670+
struct termios tio;
671+
672+
if (tcgetattr(upsfd, &tio) == 0) {
673+
tio.c_iflag &= ~(tcflag_t)(IGNBRK | BRKINT | PARMRK | ISTRIP
674+
| INLCR | IGNCR | ICRNL | IXON);
675+
tio.c_oflag &= ~(tcflag_t)OPOST;
676+
tio.c_lflag &= ~(tcflag_t)(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
677+
tio.c_cflag &= ~(tcflag_t)(CSIZE | PARENB | HUPCL);
678+
tio.c_cflag |= (tcflag_t)(CS8 | CLOCAL | CREAD);
679+
tcsetattr(upsfd, TCSANOW, &tio);
680+
}
681+
}
682+
#endif /* !WIN32 */
683+
684+
/* Force the modem-control lines low after the tcsetattr (in case the tty
685+
* layer pulsed them); the OEM client keeps DTR/RTS at 0 and some Ragtech
686+
* families read a non-zero level as a remote-shutdown signal. */
687+
ser_set_dtr(upsfd, 0);
688+
ser_set_rts(upsfd, 0);
689+
615690
usleep(RAGTECH_POST_OPEN_MS * 1000);
616691

617692
v = getval("iout_calib");
@@ -621,6 +696,13 @@ void upsdrv_initups(void)
621696
iout_calib = parsed;
622697
}
623698

699+
v = getval("va");
700+
if (v) {
701+
long parsed = atol(v);
702+
if (parsed > 0)
703+
va_override = (unsigned int)parsed;
704+
}
705+
624706
shutdown_enabled = testvar("allow_shutdown") ? 1 : 0;
625707
}
626708

0 commit comments

Comments
 (0)