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
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
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
106109upsdrv_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. */
127131static const uint8_t cmd_handshake [] = { 0xFF , 0xFE , 0x00 , 0x8E , 0x01 , 0x8F };
@@ -217,6 +221,8 @@ static struct ragtech_model fallback_model = {
217221static double iout_calib = 16.0 ; /* read from reg 0xF3 at init */
218222static uint8_t osc53 , osc57 ; /* read from 0x202..0x203 at init */
219223static 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
221227static 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