From e43181dfab74e7b21d01fbf74de40dd111f9c870 Mon Sep 17 00:00:00 2001 From: juslex <66561713+juslex@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:01:17 -0300 Subject: [PATCH 1/4] ragtech: add read-only support for the Easy Way (family 14) units The Easy Way line (OEM device table family 14, "NEW/QUADRI", 21 models EASY WAY-TI/M1/M2 500..1600) shares the USB ids of the Easy Pro but speaks a different protocol: the UPS pushes, unasked, a 32-byte frame (55 55 AE, 27 registers, one tail byte, 8-bit sum) about every 1.3 s, and the PIC applies the line coding, so the port must really run at 2400 baud. Captures of the OEM software, including a mains cut and return, show no bulk OUT traffic at all, and the OEM table lists no actions for this family. Decode it without ever writing to the UPS: keep a small window of the newest received bytes, take the newest frame whose sync bytes and sum check out (the tail byte is not constant, so it is not tested), convert with the OEM formulas and publish voltages, frequency, load, battery voltage and charge, temperature, status (OL/OB, CHRG, LB, OVER) and ups.alarm. No instant commands are registered and upsdrv_shutdown fails; allow_shutdown is ignored with a warning. No valid frame for more than 5 s marks the data stale. A new ups.conf option "family" (auto, 10, 14) selects the protocol. In "auto", the default, the driver first listens for a few seconds, writing nothing, for a family 14 frame; if none shows up it restores the termios it had before the 2400 baud change and goes on with the family 10 poll as before. With family=10 the port is not touched beyond what earlier versions did. Driver version 0.10 -> 0.11. Co-Authored-By: Claude Opus 5.5 Signed-off-by: juslex <66561713+juslex@users.noreply.github.com> --- drivers/ragtech.c | 551 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 547 insertions(+), 4 deletions(-) diff --git a/drivers/ragtech.c b/drivers/ragtech.c index 9726dbe84b..f3fb9ed0ff 100644 --- a/drivers/ragtech.c +++ b/drivers/ragtech.c @@ -1,4 +1,5 @@ -/* ragtech.c - driver for Ragtech UPSes (Easy Pro family -- NEP/TORO/INNERGIE/OneUP) +/* ragtech.c - driver for Ragtech UPSes (Easy Pro family -- NEP/TORO/INNERGIE/OneUP, + * and the Easy Way family 14 "NEW/QUADRI") * * Target devices: Ragtech UPSes exposing a USB CDC-ACM serial interface * (PIC firmware, USB VID 0x04D8, PID 0x000A). Connected as /dev/ttyACM*. @@ -6,8 +7,8 @@ * Protocol notes (derived from Ragtech's OEM devices.xml -- family 10 -- and * cross-checked against UPS_ESP32_tinySrv, https://github.com/antunesls/UPS_ESP32_tinySrv). * - * Line config: 8N1 (the CDC-ACM endpoint ignores baud, but the OEM client - * sets it to 2560). DTR=0 and RTS=0 MUST be asserted right after opening -- + * Line config: 8N1 (on family 10 the CDC-ACM endpoint ignores baud, but the + * OEM client sets it to 2560; family 14 below is different). DTR=0 and RTS=0 MUST be asserted right after opening -- * non-zero levels are interpreted by some Ragtech families as a remote * shutdown signal. * @@ -86,6 +87,71 @@ * supsvc against /dev/ttyACM0 should reveal it. * * + * Family 14 (Easy Way, "NEW/QUADRI" in the OEM table) -- read only + * ----------------------------------------------------------------- + * + * Same USB ids (04D8:000A) as family 10 but a different, push-only + * protocol: the UPS never waits for a command. Nothing is ever written to + * it by this driver in this family (no bulk OUT traffic was seen from the + * OEM client either, and the OEM table lists no actions for family 14). + * + * Line config: 2400 8N1. Unlike family 10, the PIC does apply the line + * coding to its UART here: read at 4800 the stream turns into byte pairs + * such as 66 e6, and at 9600 or above into pulses of 4 or more bit times. + * DTR=0 and RTS=0 as for family 10. + * + * Every ~1.25 to 1.3 s the UPS sends one frame of 32 bytes: + * + * 0x55 0x55 0xAE | reg[0x00..0x1A] (27 bytes) | tail | CS + * + * CS is the 8-bit sum of the 31 preceding bytes (sync bytes included). + * The byte "tail" was seen as 0x09 on mains and 0x08 on battery (and + * sporadically after mains returned), so it is NOT checked; the sync bytes + * plus CS are what the driver trusts. The meaning of 0xAE is unknown. + * + * Register map (the register index is the offset into the 27 bytes): + * + * 0x00 version (x0.1) 0x01 model id (see f14_models[]) + * 0x02 OSC53, 0x03 OSC57 (frequency calibration) + * 0x04 FOUTCALIB, 0x05 VBATCALIB, 0x06 VINCALIB, 0x07 VOUTCALIB, 0x09 IOUTCALIB + * 0x0A bit2 F_VINSEL220 + * 0x0C bit0 SYNCIN, bit4 SHUTDOWNTIMERON, bit7 MAXBATTERY + * 0x0D bit0 NOVINPUT + * 0x0E bit1 FENDBATTERY, bit2 FOVERLOAD, bit4 FSHORTCIRCUIT, + * bit5 FABNORMALVOUT, bit6 FABNORMALVBAT, bit7 FINVERTER + * 0x0F bit1 LOFINPUT, bit2 HIFINPUT, bit3 NOSYNCIN, bit4 LOVINPUT, + * bit5 HIVINPUT, bit6 LOBATTERY, bit7 NOISEIN + * 0x10 bit3 OPBATTERY, bit4 OPSTANDBY, bit5 OPWARNING, bit6 OPSTARTUP + * 0x11 ACTUALTAP 0x12 VINPUT 0x13 VOUTPUT 0x14 IOUTPUT (a load ratio) + * 0x15 VBATTERY 0x16 SHUTDOWNTIMER 0x17 FOUTPUT + * 0x1A TEMPER (degrees C) + * + * Conversions (OEM table; "115" and the tap factors are model dependent, + * see f14_models[]): + * + * input.voltage = VINPUT / VINCALIB * 115 (0 when NOVINPUT); TI models + * strapped for 220 V on mains use + * VINPUT / VINCALIB * 230 * (IOUTPUT / IOUTCALIB * 0.05 + 1) + * output.voltage = VOUTPUT / VOUTCALIB * TAP[ACTUALTAP] * 115 (0 on OPSTANDBY) + * output.frequency = same OSC53/OSC57 interpolation as family 10 + * ups.load = IOUTPUT / IOUTCALIB * 100 + * battery.voltage = VBATTERY / VBATCALIB * 13.8 + * battery.charge = (VBATTERY / VBATCALIB - 0.725) / 0.275 * 100, kept in 0..100 + * + * Status: OB = OPBATTERY (confirmed on a mains cut: NOVINPUT and LOVINPUT + * agree); OL otherwise. CHRG = OL with MAXBATTERY clear (seen clear for + * the whole recharge after a cut; inferred, not verified). OVER = FOVERLOAD. + * LB = LOBATTERY or FENDBATTERY, and the bits of reg 0x0E (bits 4..7) go to + * ups.alarm; none of these bits has been observed set yet, so they come + * from the OEM bit names only. BOOST/TRIM are not reported (ACTUALTAP + * stayed at 3 on mains and on battery) and the beeper bit is unknown. + * + * Detection: with the "family" option at its default ("auto") the driver + * first listens, writing nothing, for a valid frame at 2400 baud for a few + * seconds. If one arrives the unit is family 14; otherwise the termios + * state is put back and the family 10 poll runs as before. + * + * * Copyright (C) 2026 juslex * * This program is free software; you can redistribute it and/or modify @@ -104,7 +170,7 @@ #include "nut_stdint.h" #define DRIVER_NAME "Ragtech UPS driver" -#define DRIVER_VERSION "0.10" +#define DRIVER_VERSION "0.11" upsdrv_info_t upsdrv_info = { DRIVER_NAME, @@ -434,6 +500,419 @@ static double compute_frequency(uint8_t v_fout) / ((double)osc57 - (double)osc53)) * 0.9806 + 1.46; } +/* ------------------------------------------------------------------------ + * Family 14 (Easy Way, "NEW/QUADRI") -- push-only, read-only. + * See the protocol notes in the header comment. Nothing here ever writes + * to the UPS. + * ------------------------------------------------------------------------ */ + +#define F14_SYNC0 0x55 +#define F14_SYNC1 0x55 +#define F14_SYNC2 0xAE +#define F14_FRAME_LEN 32 /* 3 sync + 27 registers + tail + checksum */ +#define F14_NREGS 27 +#define F14_BUF_LEN 256 /* receive window; keeps only the newest bytes */ +#define F14_DETECT_MS 3000 /* passive listen for family auto-detection */ +#define F14_FIRST_SEC 10 /* wait for the first frame when family=14 is forced */ +#define F14_STALE_SEC 5 /* no valid frame for this long: data stale */ +#define F14_DRAIN_MAX 64 /* safety bound for the drain loop */ + +/* register indexes inside the 27-byte block */ +#define R14_VERSION 0x00 +#define R14_MODEL 0x01 +#define R14_OSC53 0x02 +#define R14_OSC57 0x03 +#define R14_VBATCALIB 0x05 +#define R14_VINCALIB 0x06 +#define R14_VOUTCALIB 0x07 +#define R14_IOUTCALIB 0x09 +#define R14_VINSEL 0x0A /* bit2: 220 V selected */ +#define R14_MISC 0x0C /* bit7: MAXBATTERY (battery full) */ +#define R14_INPUT 0x0D /* bit0: NOVINPUT */ +#define R14_FAULT 0x0E +#define R14_WARN 0x0F /* bit6: LOBATTERY */ +#define R14_OP 0x10 /* bit3: OPBATTERY, bit4: OPSTANDBY */ +#define R14_TAP 0x11 +#define R14_VINPUT 0x12 +#define R14_VOUTPUT 0x13 +#define R14_IOUTPUT 0x14 +#define R14_VBATTERY 0x15 +#define R14_FOUTPUT 0x17 +#define R14_TEMPER 0x1A + +#define B14_VINSEL220 0x04 +#define B14_MAXBATTERY 0x80 +#define B14_NOVINPUT 0x01 +#define B14_FENDBATTERY 0x02 +#define B14_FOVERLOAD 0x04 +#define B14_FSHORTCIRCUIT 0x10 +#define B14_FABNORMALVOUT 0x20 +#define B14_FABNORMALVBAT 0x40 +#define B14_FINVERTER 0x80 +#define B14_LOBATTERY 0x40 +#define B14_OPBATTERY 0x08 +#define B14_OPSTANDBY 0x10 + +#define F14_VBAT_FACTOR 13.8 /* C_VBATNOMINAL * 13.8 / 12 with 12 V nominal */ +#define F14_BAT_EMPTY 0.725 /* C_BATFACTOR */ +#define F14_VEC_FACTOR 0.05 /* C_VECFACTOR (TI, 220 V strap) */ + +enum f14_variant { F14_TI = 0, F14_M1, F14_M2 }; + +struct f14_model { + uint8_t id; + const char *name; + unsigned int pnom; /* C_POUTNOMINAL, same unit convention as family 10 (VA) */ + enum f14_variant variant; +}; + +/* OEM table, family 14 -- ids are V_MODEL */ +static const struct f14_model f14_models[] = { + { 0, "Easy Way 500 TI", 500, F14_TI }, + { 1, "Easy Way 500 M1", 500, F14_M1 }, + { 2, "Easy Way 500 M2", 500, F14_M2 }, + { 3, "Easy Way 600 TI", 600, F14_TI }, + { 4, "Easy Way 600 M1", 600, F14_M1 }, + { 5, "Easy Way 600 M2", 600, F14_M2 }, + { 6, "Easy Way 700 TI", 700, F14_TI }, + { 7, "Easy Way 700 M1", 700, F14_M1 }, + { 8, "Easy Way 700 M2", 700, F14_M2 }, + { 9, "Easy Way 1200 TI", 1200, F14_TI }, + { 10, "Easy Way 1200 M1", 1200, F14_M1 }, + { 11, "Easy Way 1200 M2", 1200, F14_M2 }, + { 12, "Easy Way 1300 TI", 1300, F14_TI }, + { 13, "Easy Way 1300 M1", 1300, F14_M1 }, + { 14, "Easy Way 1300 M2", 1300, F14_M2 }, + { 15, "Easy Way 1400 TI", 1400, F14_TI }, + { 16, "Easy Way 1400 M1", 1400, F14_M1 }, + { 17, "Easy Way 1400 M2", 1400, F14_M2 }, + { 18, "Easy Way 1600 TI", 1600, F14_TI }, + { 19, "Easy Way 1600 M1", 1600, F14_M1 }, + { 20, "Easy Way 1600 M2", 1600, F14_M2 }, +}; + +/* output voltage tap factors (ACTUALTAP 0..3); TI and M1 share the first row */ +static const double f14_tap_ti[4] = { 1.2898, 1.1558, 1.0172, 0.8812 }; +static const double f14_tap_m2[4] = { 1.2460, 1.1620, 1.0705, 0.9998 }; + +/* Decoded values of one frame */ +struct f14_data { + double vin, vout, fout, load, vbat, cbat; + unsigned int temp; + unsigned int vin_nominal; /* 115 or 220 */ + int ob, chrg, lb, over; + unsigned int faults; /* reg 0x0E, bits 4..7 only */ +}; + +/* fallback profile for an id missing from the table: TI, nominal power unknown */ +static const struct f14_model f14_fallback = { 0xFF, "Unknown", 0, F14_TI }; + +static int family_opt = 0; /* ups.conf "family": 0 auto, 10, 14 */ +static int family = 10; /* family in use after initups */ +static const struct f14_model *f14_model; +static uint8_t f14_buf[F14_BUF_LEN]; /* newest received bytes */ +static size_t f14_len; +static uint8_t f14_regs[F14_NREGS]; /* registers of the newest valid frame */ +static int f14_have; +static time_t f14_last; /* when that frame was taken */ +#ifndef WIN32 +static struct termios f14_tio_saved; /* termios before the 2400 baud change */ +static int f14_tio_saved_ok; +#endif /* !WIN32 */ + +static const struct f14_model *f14_find_model(uint8_t id) +{ + size_t i; + for (i = 0; i < sizeof(f14_models) / sizeof(f14_models[0]); i++) + if (f14_models[i].id == id) + return &f14_models[i]; + return NULL; +} + +/* 1 if f points at a whole valid frame: sync bytes and 8-bit sum */ +static int f14_frame_ok(const uint8_t *f) +{ + unsigned int sum = 0; + size_t i; + + if (f[0] != F14_SYNC0 || f[1] != F14_SYNC1 || f[2] != F14_SYNC2) + return 0; + for (i = 0; i < F14_FRAME_LEN - 1; i++) + sum += f[i]; + return (sum & 0xFF) == f[F14_FRAME_LEN - 1]; +} + +/* Append received bytes to the window, dropping the oldest when full. */ +static void f14_append(const uint8_t *p, size_t n) +{ + size_t drop; + + if (n >= F14_BUF_LEN) { + p += n - F14_BUF_LEN; + n = F14_BUF_LEN; + } + if (f14_len + n > F14_BUF_LEN) { + drop = f14_len + n - F14_BUF_LEN; + memmove(f14_buf, f14_buf + drop, f14_len - drop); + f14_len -= drop; + } + memcpy(f14_buf + f14_len, p, n); + f14_len += n; +} + +/* Take the NEWEST valid frame out of the window: keep its registers and + * discard everything up to its end. Returns 1 if there was one. */ +static int f14_take(void) +{ + size_t start, end; + + if (f14_len < F14_FRAME_LEN) + return 0; + + for (start = f14_len - F14_FRAME_LEN + 1; start-- > 0; ) { + if (!f14_frame_ok(f14_buf + start)) + continue; + memcpy(f14_regs, f14_buf + start + 3, F14_NREGS); + f14_have = 1; + f14_last = time(NULL); + end = start + F14_FRAME_LEN; + memmove(f14_buf, f14_buf + end, f14_len - end); + f14_len -= end; + return 1; + } + return 0; +} + +/* Read whatever is queued on the port without waiting. */ +static void f14_drain(void) +{ + uint8_t tmp[64]; + ssize_t n; + int i; + + for (i = 0; i < F14_DRAIN_MAX; i++) { + n = ser_get_buf(upsfd, tmp, sizeof(tmp), 0, 0); + if (n <= 0) + break; + upsdebug_hex(5, "RX14", tmp, (size_t)n); + f14_append(tmp, (size_t)n); + } +} + +/* Listen up to `ms` milliseconds, returning as soon as a valid frame is in. */ +static int f14_listen(unsigned int ms) +{ + uint8_t tmp[64]; + ssize_t n; + struct timeval start, now; + long elapsed_ms; + + gettimeofday(&start, NULL); + for (;;) { + n = ser_get_buf(upsfd, tmp, sizeof(tmp), 0, 100000); + if (n > 0) { + upsdebug_hex(5, "RX14", tmp, (size_t)n); + f14_append(tmp, (size_t)n); + if (f14_take()) + return 1; + } + gettimeofday(&now, NULL); + elapsed_ms = (long)(now.tv_sec - start.tv_sec) * 1000L + + (long)(now.tv_usec - start.tv_usec) / 1000L; + if (elapsed_ms >= (long)ms) + return 0; + } +} + +/* Switch the port to 2400 baud. The termios state before the change is + * kept so that the family 10 path can be given back exactly what it had. */ +static void f14_set_line(void) +{ +#ifndef WIN32 + struct termios tio; + + if (tcgetattr(upsfd, &tio) != 0) + return; + f14_tio_saved = tio; + f14_tio_saved_ok = 1; + cfsetispeed(&tio, B2400); + cfsetospeed(&tio, B2400); + tcsetattr(upsfd, TCSANOW, &tio); +#endif /* !WIN32 */ +} + +static void f14_restore_line(void) +{ +#ifndef WIN32 + if (f14_tio_saved_ok) + tcsetattr(upsfd, TCSANOW, &f14_tio_saved); +#endif /* !WIN32 */ +} + +/* Convert the 27 registers of one frame. Formulas as in the OEM table. */ +static void f14_decode(const uint8_t *r, const struct f14_model *m, + struct f14_data *d) +{ + const double *tap = (m->variant == F14_M2) ? f14_tap_m2 : f14_tap_ti; + int novin = (r[R14_INPUT] & B14_NOVINPUT) != 0; + int opbat = (r[R14_OP] & B14_OPBATTERY) != 0; + int standby = (r[R14_OP] & B14_OPSTANDBY) != 0; + int sel220 = (r[R14_VINSEL] & B14_VINSEL220) != 0; + double ratio; + + memset(d, 0, sizeof(*d)); + + /* input voltage */ + if (!novin && r[R14_VINPUT] && r[R14_VINCALIB]) { + ratio = (double)r[R14_VINPUT] / r[R14_VINCALIB]; + if (m->variant != F14_TI || !sel220 || opbat) + d->vin = ratio * 115.0; + else if (r[R14_IOUTCALIB]) + d->vin = ratio * 230.0 * ((double)r[R14_IOUTPUT] + / r[R14_IOUTCALIB] * F14_VEC_FACTOR + 1.0); + } + if (m->variant == F14_M2) + d->vin_nominal = 220; + else if (m->variant == F14_TI && sel220) + d->vin_nominal = 220; + else + d->vin_nominal = 115; + + /* output voltage: tap index beyond 3 gives 0 */ + if (r[R14_VOUTCALIB] && !standby && r[R14_TAP] < 4) + d->vout = (double)r[R14_VOUTPUT] / r[R14_VOUTCALIB] + * tap[r[R14_TAP]] * 115.0; + + /* output frequency; OSC57 may be below OSC53 */ + if (r[R14_OSC57] != r[R14_OSC53]) + d->fout = (53.0 + 4.0 * ((double)r[R14_FOUTPUT] - r[R14_OSC53]) + / ((double)r[R14_OSC57] - r[R14_OSC53])) * 0.9806 + 1.46; + + /* load, battery */ + if (r[R14_IOUTCALIB]) + d->load = (double)r[R14_IOUTPUT] / r[R14_IOUTCALIB] * 100.0; + if (r[R14_VBATCALIB]) { + ratio = (double)r[R14_VBATTERY] / r[R14_VBATCALIB]; + d->vbat = ratio * F14_VBAT_FACTOR; + d->cbat = (ratio - F14_BAT_EMPTY) / (1.0 - F14_BAT_EMPTY) * 100.0; + if (d->cbat < 0.0) + d->cbat = 0.0; + if (d->cbat > 100.0) + d->cbat = 100.0; + } + d->temp = r[R14_TEMPER]; + + d->ob = opbat; + d->chrg = !opbat && !(r[R14_MISC] & B14_MAXBATTERY); + d->lb = (r[R14_WARN] & B14_LOBATTERY) || (r[R14_FAULT] & B14_FENDBATTERY); + d->over = (r[R14_FAULT] & B14_FOVERLOAD) != 0; + d->faults = r[R14_FAULT] & (B14_FSHORTCIRCUIT | B14_FABNORMALVOUT + | B14_FABNORMALVBAT | B14_FINVERTER); +} + +static void f14_publish(const struct f14_data *d) +{ + dstate_setinfo("input.voltage", "%.1f", d->vin); + dstate_setinfo("input.voltage.nominal", "%u", d->vin_nominal); + dstate_setinfo("output.voltage", "%.1f", d->vout); + dstate_setinfo("output.frequency", "%.2f", d->fout); + dstate_setinfo("ups.load", "%.1f", d->load); + dstate_setinfo("battery.voltage", "%.2f", d->vbat); + dstate_setinfo("battery.charge", "%.1f", d->cbat); + dstate_setinfo("ups.temperature", "%u", d->temp); + + alarm_init(); + if (d->faults & B14_FSHORTCIRCUIT) + alarm_set("Short circuit"); + if (d->faults & B14_FABNORMALVOUT) + alarm_set("Abnormal output voltage"); + if (d->faults & B14_FABNORMALVBAT) + alarm_set("Abnormal battery voltage"); + if (d->faults & B14_FINVERTER) + alarm_set("Inverter fault"); + alarm_commit(); + + status_init(); + status_set(d->ob ? "OB" : "OL"); + if (d->chrg) + status_set("CHRG"); + if (d->lb) + status_set("LB"); + if (d->over) + status_set("OVER"); + status_commit(); +} + +/* Static data, once, from the first frame. */ +static void f14_initinfo(void) +{ + struct f14_data d; + time_t t0 = time(NULL); + + dstate_setinfo("ups.mfr", "%s", "Ragtech"); + dstate_setinfo("ups.model", "%s", "Unknown"); + + if (shutdown_enabled) + upslogx(LOG_WARNING, + "allow_shutdown is ignored on family 14: no shutdown " + "command is known for these units, the driver is read-only"); + + while (!f14_have) { + f14_drain(); + if (f14_take()) + break; + if (time(NULL) - t0 > F14_FIRST_SEC) + fatalx(EXIT_FAILURE, + "no valid family 14 frame received in %d s -- is the UPS " + "connected and not held by another program (e.g. Ragtech supsvc)?", + F14_FIRST_SEC); + usleep(100000); + } + + f14_model = f14_find_model(f14_regs[R14_MODEL]); + if (!f14_model) { + upslogx(LOG_WARNING, "unknown family 14 model id 0x%02X, using a generic profile", + f14_regs[R14_MODEL]); + f14_model = &f14_fallback; + } + dstate_setinfo("ups.model", "%s", f14_model->name); + dstate_setinfo("ups.firmware", "%.1f", f14_regs[R14_VERSION] * 0.1); + if (f14_model->pnom) + dstate_setinfo("ups.power.nominal", "%u", f14_model->pnom); + dstate_setinfo("output.voltage.nominal", "%u", + f14_model->variant == F14_M2 ? 220U : 115U); + dstate_setinfo("output.frequency.nominal", "%u", 60U); + dstate_setinfo("battery.voltage.nominal", "%u", 12U); + + upslogx(LOG_INFO, "family 14 (Easy Way, read-only): %s, firmware %.1f", + f14_model->name, f14_regs[R14_VERSION] * 0.1); + + /* first values, from the frame that identified the unit */ + f14_decode(f14_regs, f14_model, &d); + f14_publish(&d); +} + +static void f14_updateinfo(void) +{ + struct f14_data d; + int fresh; + + f14_drain(); + fresh = f14_take(); + + if (!fresh && (!f14_have || time(NULL) - f14_last > F14_STALE_SEC)) { + upsdebugx(2, "family 14: no valid frame for more than %d s", F14_STALE_SEC); + dstate_datastale(); + return; + } + + if (fresh) { + f14_decode(f14_regs, f14_model, &d); + f14_publish(&d); + } + dstate_dataok(); +} + void upsdrv_initinfo(void) { uint8_t reply[64]; @@ -441,6 +920,11 @@ void upsdrv_initinfo(void) uint8_t calib; int tries; + if (family == 14) { + f14_initinfo(); + return; + } + dstate_setinfo("ups.mfr", "%s", "Ragtech"); dstate_setinfo("ups.model", "%s", "Unknown"); @@ -544,6 +1028,11 @@ void upsdrv_updateinfo(void) double vout, iout, bcharge; int attempt; + if (family == 14) { + f14_updateinfo(); + return; + } + /* CDC-ACM occasionally returns a short/corrupt reply, more often when * output current is high (bus noise). Retry the transaction a couple * of times before declaring data stale, so single-poll glitches do @@ -613,6 +1102,14 @@ void upsdrv_shutdown(void) { int handled; + if (family == 14) { + upslogx(LOG_ERR, + "upsdrv_shutdown invoked on a family 14 (Easy Way) unit: " + "no shutdown command is known for it, the driver is read-only"); + set_exit_flag(EF_EXIT_FAILURE); + return; + } + if (!shutdown_enabled) { upslogx(LOG_ERR, "upsdrv_shutdown invoked but 'allow_shutdown' is not set " @@ -638,6 +1135,10 @@ void upsdrv_tweak_prognames(void) void upsdrv_makevartable(void) { + addvar(VAR_VALUE, "family", + "UPS protocol family: \"auto\" (default; listen passively for the " + "Easy Way push stream first, then fall back to family 10), \"10\" " + "(Easy Pro, polled) or \"14\" (Easy Way, push-only, read-only)"); addvar(VAR_VALUE, "iout_calib", "Override per-unit output current calibration (default: read from reg 0xF3)"); addvar(VAR_VALUE, "va", @@ -656,6 +1157,19 @@ void upsdrv_initups(void) { const char *v; + v = getval("family"); + if (v) { + if (!strcasecmp(v, "auto")) + family_opt = 0; + else if (!strcmp(v, "10")) + family_opt = 10; + else if (!strcmp(v, "14")) + family_opt = 14; + else + fatalx(EXIT_FAILURE, "invalid 'family' value [%s]: " + "use auto, 10 or 14", v); + } + upsfd = ser_open(device_path); /* A freshly enumerated CDC-ACM tty defaults to canonical mode (ICANON), @@ -681,6 +1195,11 @@ void upsdrv_initups(void) } #endif /* !WIN32 */ + /* Family 14 needs the real line speed (2400); family 10 does not care, + * so with family=10 the port is left exactly as before. */ + if (family_opt != 10) + f14_set_line(); + /* Force the modem-control lines low after the tcsetattr (in case the tty * layer pulsed them); the OEM client keeps DTR/RTS at 0 and some Ragtech * families read a non-zero level as a remote-shutdown signal. */ @@ -704,6 +1223,30 @@ void upsdrv_initups(void) } shutdown_enabled = testvar("allow_shutdown") ? 1 : 0; + + if (family_opt == 14) { + family = 14; + } else if (family_opt == 0) { + /* Passive: nothing is written to the UPS while looking for the + * family 14 stream. */ + upsdebugx(1, "family auto: listening %u ms for a family 14 frame", + F14_DETECT_MS); + if (f14_listen(F14_DETECT_MS)) { + family = 14; + } else { + /* Give family 10 back the termios it had before the 2400 + * baud change, and re-assert DTR/RTS low after it. */ + f14_restore_line(); + ser_set_dtr(upsfd, 0); + ser_set_rts(upsfd, 0); + usleep(RAGTECH_POST_OPEN_MS * 1000); + f14_len = 0; + family = 10; + } + } else { + family = 10; + } + upsdebugx(1, "protocol family %d", family); } void upsdrv_cleanup(void) From 5521bd79da27df6e3c6b677edf4efc892453af0e Mon Sep 17 00:00:00 2001 From: juslex <66561713+juslex@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:01:17 -0300 Subject: [PATCH 2/4] docs/man/ragtech.txt: document the Easy Way (family 14) support Add the "family" option, a section on the read-only family 14 behaviour (push-only frames, 2400 baud, no commands, what was observed on hardware versus what is only inferred from the OEM bit names), list the 21 Easy Way models in data/driver.list.in, add a NEWS entry, and teach the spell-check dictionary the word QUADRI. Co-Authored-By: Claude Opus 5.5 Signed-off-by: juslex <66561713+juslex@users.noreply.github.com> --- NEWS.adoc | 11 +++++++ data/driver.list.in | 21 +++++++++++++ docs/man/ragtech.txt | 72 ++++++++++++++++++++++++++++++++++++++++++-- docs/nut.dict | 3 +- 4 files changed, 103 insertions(+), 4 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index 51c221fc52..e9ee05306a 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -505,6 +505,17 @@ https://github.com/networkupstools/nut/milestone/13 firmware model id is shared between product lines, correcting `ups.power.nominal`, `ups.realpower.nominal` and the computed `ups.load`. [issue #3500] + * Added read-only support for the Ragtech Easy Way units ("family 14", + "NEW/QUADRI" in the OEM device table; 21 models), which share the USB + ids of the Easy Pro but push, on their own, a 32-byte frame with a + checksum at 2400 baud instead of answering polls. The new `family` option + (`auto`, `10` or `14`) selects the protocol; by default the driver first + listens passively and only then falls back to the Easy Pro polling, + which is unchanged. No write command is known for these units, so + nothing is sent to them and no instant commands are offered. Only + the on-line and on-battery states, voltages, frequency, load, battery + and temperature were checked against captures of the OEM software. + [issue #3755] - `richcomm_usb` driver updates: * Completed support for NUT standard USB configuration options, and fixed diff --git a/data/driver.list.in b/data/driver.list.in index 2daf03dcdd..12a1f0e4a3 100644 --- a/data/driver.list.in +++ b/data/driver.list.in @@ -1322,6 +1322,27 @@ "Ragtech" "ups" "1" "Easy 2000 M2" "" "ragtech (experimental)" "Ragtech" "ups" "1" "Easy 2200 TI" "" "ragtech (experimental)" "Ragtech" "ups" "1" "Easy 2200 M2" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 500 TI" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 500 M1" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 500 M2" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 600 TI" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 600 M1" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 600 M2" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 700 TI" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 700 M1" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 700 M2" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 1200 TI" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 1200 M1" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 1200 M2" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 1300 TI" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 1300 M1" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 1300 M2" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 1400 TI" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 1400 M1" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 1400 M2" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 1600 TI" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 1600 M1" "" "ragtech (experimental)" +"Ragtech" "ups" "1" "Easy Way 1600 M2" "" "ragtech (experimental)" "Raritan" "pdu" "3" "Intelligent PDU - Dominion PX" "no report, but should be supported" "snmp-ups (experimental)" "Raritan" "pdu" "3" "Metered PDU - Raritan PM" "no report, but should be supported" "snmp-ups (experimental)" diff --git a/docs/man/ragtech.txt b/docs/man/ragtech.txt index bb31fea473..0422f38341 100644 --- a/docs/man/ragtech.txt +++ b/docs/man/ragtech.txt @@ -37,6 +37,14 @@ the Easy Pro "GT" series) speak the same protocol but may report a model id that collides with a table entry, so the driver can pick the wrong VA rating. Use the *va* option below to set the correct rating in that case. +A second protocol, "family 14", covers the Ragtech Easy Way units (21 +models, Easy Way 500/600/700/1200/1300/1400/1600 in TI, M1 and M2 +variants). They use the same USB ids but a different, push-only +protocol; see the *FAMILY 14 (EASY WAY)* section below. Support for +family 14 is *read-only* and was developed from captures of the OEM +software on one unit (an Easy Way TI 1400, sold as "Easy Way 1500 VA +GT"); the other 20 models come from the OEM device table only. + This driver is *experimental*. Polling, status decoding and the `shutdown.stayoff` / `shutdown.stop` instant commands have been verified on hardware; `shutdown.return` and `test.battery.start.deep` @@ -50,6 +58,19 @@ EXTRA ARGUMENTS This driver supports the following options in the *ups.conf*: +*family*='auto|10|14';; +Select the protocol family. The default, *auto*, first listens for a +few seconds without writing anything to the UPS: if it receives a valid +family 14 frame at 2400 baud, the unit is treated as family 14; +otherwise the driver continues with the family 10 protocol, with the +serial settings it had before the listen. Set *family=10* on an Easy Pro +unit to skip the listen (and its start-up delay of about three seconds, +which also applies to `-k`) and to leave the port settings exactly as +earlier versions did. Set *family=14* to require the Easy Way protocol; +owners of an Easy Way unit are advised to do so, because if the automatic +listen ever misses the stream, the driver falls back to polling it with +family 10 commands, which that protocol does not use. + *iout_calib*='number';; Per-unit output current calibration constant. If omitted (default), the driver reads the value from register `0xF3` at startup. Set this @@ -77,6 +98,49 @@ battery event. With this flag unset, `upsdrv_shutdown` logs the refusal and exits with failure instead of cutting the output. +FAMILY 14 (EASY WAY) +-------------------- + +The Easy Way units transmit a 32-byte frame about every 1.3 seconds on +their own, at 2400 baud 8N1, without being asked. The driver only +reads: it never writes anything to a family 14 unit, it registers no +instant commands, and `upsdrv_shutdown` fails (so the `FSD` handling of +`upsmon` cannot power such a UPS off through this driver). If +*allow_shutdown* is set it is ignored with a warning. No write command +is known for this family: the OEM device table lists no actions for it +and none was seen in captures of the OEM software. + +Unlike family 10, these units do use the line speed, so the port must be +at 2400 baud; the driver sets that itself. + +Published data: `ups.mfr`, `ups.model`, `ups.firmware`, +`ups.power.nominal`, `ups.temperature`, `ups.load`, `ups.status`, +`ups.alarm`, `input.voltage`, `input.voltage.nominal`, +`output.voltage`, `output.voltage.nominal`, `output.frequency`, +`output.frequency.nominal`, `battery.voltage`, `battery.voltage.nominal` +and `battery.charge`. There is no output current. `ups.load` is the +load ratio reported by the firmware. `ups.power.nominal` is the number +in the model name, with the same convention as for family 10; +`ups.realpower.nominal` is not published because the OEM table does not +say whether that number is VA or watts. `battery.charge` is an estimate +computed from the battery voltage, as in the OEM software. + +What was observed on hardware (mains cut and restored on one TI unit): +`OL` and `OB` follow the firmware's on-battery flag, and the voltages, +frequency, load, battery voltage and temperature match the values shown +by the OEM software. What is inferred from bit names in the OEM table +and was *not* observed: `CHRG` (shown while on mains and the +battery-full flag is clear), `LB`, `OVER`, and every `ups.alarm` entry. +`BOOST`, `TRIM` and the beeper are not reported. The M1 and M2 +variants follow the OEM table, but the output voltage formula of the M2 +(220 V) models in that table is the 115 V one with different tap factors, +which looks doubtful, so treat `output.voltage` on those with suspicion +until someone confirms it. + +If no valid frame arrives for more than five seconds the data is marked +stale. + + CAVEATS ------- @@ -86,9 +150,11 @@ are interpreted by the UPS as a remote shutdown signal -- never run this driver alongside other software that toggles those lines on the same device node. -Replies are framed by a 0xAA start byte and have no checksum; the -driver re-synchronizes by scanning for the start byte after any -short read. +On family 10, replies are framed by a 0xAA start byte and have no +checksum; the driver re-synchronizes by scanning for the start byte +after any short read. On family 14 the frames start with `55 55 AE` and +end with an 8-bit sum, and the driver keeps the newest frame whose sum +is right. AUTHOR diff --git a/docs/nut.dict b/docs/nut.dict index 5f351d8cd9..341e053dc8 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3917 utf-8 +personal_ws-1.1 en 3918 utf-8 AAC AAS ABI @@ -1146,6 +1146,7 @@ QQ QQQ QRI QTS +QUADRI QVFW QWS QinHeng From 255e1586547010e796e7f390940b4192665e075c Mon Sep 17 00:00:00 2001 From: juslex <66561713+juslex@users.noreply.github.com> Date: Wed, 28 Oct 2026 09:07:54 -0300 Subject: [PATCH 3/4] ragtech: family 14 runs at 2560 baud and validates the frames The Easy Way UART transmits at 2560 baud, the first speed listed for the family in the OEM devices.xml, not 2400. Read at 2400 the receiver samples bit 6 on the bit 6 / bit 7 boundary of the transmitter and bit 7 inside the stop bit, so bit 7 arrives set and bit 6 is unreliable: most frames failed the checksum and a few corrupted ones passed it, because the 8-bit sum does not notice two bytes that both gain bit 7. A real unit then showed ups.load 385, a 180 C temperature and an "Inverter fault". * New drivers/ragtech_speed.c sets an arbitrary speed: termios2 + BOTHER on Linux (kept in a file of its own, because cannot be included together with ), a numeric speed_t on the BSDs and macOS. If it fails, the driver warns and falls back to 2400. The termios state is restored exactly on the auto -> family 10 path. * A frame is accepted only if registers 0x00..0x09 equal a reference confirmed by two consecutive checksum-valid frames (a new reference is allowed after 30 s of mismatches), and ACTUALTAP <= 3, load <= 150 %, temperature <= 100 C and battery voltage within 6..18 V. * ups.alarm (reg 0x0E bits 4..7), OVER and LB need two consecutive accepted frames to appear or to disappear. OL/OB/CHRG are not debounced. * Every checksum-valid frame in the receive window is now examined in order (not only the newest), and the first-frame wait goes from 10 to 15 s. Co-Authored-By: Claude Opus 5.5 Signed-off-by: juslex <66561713+juslex@users.noreply.github.com> --- NEWS.adoc | 7 +- docs/man/ragtech.txt | 42 ++++- drivers/Makefile.am | 4 +- drivers/ragtech.c | 336 ++++++++++++++++++++++++++++++++-------- drivers/ragtech_speed.c | 152 ++++++++++++++++++ drivers/ragtech_speed.h | 35 +++++ 6 files changed, 501 insertions(+), 75 deletions(-) create mode 100644 drivers/ragtech_speed.c create mode 100644 drivers/ragtech_speed.h diff --git a/NEWS.adoc b/NEWS.adoc index e9ee05306a..8c2994e563 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -508,14 +508,17 @@ https://github.com/networkupstools/nut/milestone/13 * Added read-only support for the Ragtech Easy Way units ("family 14", "NEW/QUADRI" in the OEM device table; 21 models), which share the USB ids of the Easy Pro but push, on their own, a 32-byte frame with a - checksum at 2400 baud instead of answering polls. The new `family` option + checksum at 2560 baud instead of answering polls. The new `family` option (`auto`, `10` or `14`) selects the protocol; by default the driver first listens passively and only then falls back to the Easy Pro polling, which is unchanged. No write command is known for these units, so nothing is sent to them and no instant commands are offered. Only the on-line and on-battery states, voltages, frequency, load, battery and temperature were checked against captures of the OEM software. - [issue #3755] + The line speed is 2560 baud, set directly (no standard constant); frames + are accepted only if the constant registers match a reference confirmed + by two frames and the values are plausible, and the rarely seen status + bits need two consecutive frames. [issue #3755] - `richcomm_usb` driver updates: * Completed support for NUT standard USB configuration options, and fixed diff --git a/docs/man/ragtech.txt b/docs/man/ragtech.txt index 0422f38341..edcbc5b796 100644 --- a/docs/man/ragtech.txt +++ b/docs/man/ragtech.txt @@ -60,8 +60,9 @@ This driver supports the following options in the *ups.conf*: *family*='auto|10|14';; Select the protocol family. The default, *auto*, first listens for a -few seconds without writing anything to the UPS: if it receives a valid -family 14 frame at 2400 baud, the unit is treated as family 14; +few seconds without writing anything to the UPS: if it receives a +checksum-valid family 14 frame at 2560 baud, the unit is treated as +family 14; otherwise the driver continues with the family 10 protocol, with the serial settings it had before the listen. Set *family=10* on an Easy Pro unit to skip the listen (and its start-up delay of about three seconds, @@ -102,7 +103,7 @@ FAMILY 14 (EASY WAY) -------------------- The Easy Way units transmit a 32-byte frame about every 1.3 seconds on -their own, at 2400 baud 8N1, without being asked. The driver only +their own, at 2560 baud 8N1, without being asked. The driver only reads: it never writes anything to a family 14 unit, it registers no instant commands, and `upsdrv_shutdown` fails (so the `FSD` handling of `upsmon` cannot power such a UPS off through this driver). If @@ -111,7 +112,33 @@ is known for this family: the OEM device table lists no actions for it and none was seen in captures of the OEM software. Unlike family 10, these units do use the line speed, so the port must be -at 2400 baud; the driver sets that itself. +at 2560 baud (not the usual 2400: that is the first speed listed for this +family in the OEM device table, and the OEM software uses it); the driver +sets that itself. Read at 2400 the stream is corrupted in a systematic +way: bit 7 of the bytes arrives set and bit 6 is unreliable, so most +frames fail the checksum, and a few corrupted ones still pass it. A +speed like 2560 has no standard constant, so the driver asks the system +for it directly (the `termios2` interface on Linux, the numeric speed on +BSD and macOS). If that fails, for instance on a platform without such +support, the driver logs a warning ("could not set 2560 baud; falling +back to 2400, expect corrupted frames") and uses 2400; the checks below +then drop most frames and the data will often be marked stale. + +The 8-bit sum of the frame is a weak check (two bytes that both gain bit +7 add up to zero), so a frame is only accepted if it also passes these: + +* Registers 0x00 to 0x09 (version, model and calibration constants) are +fixed in the unit. They must equal a reference, which is taken from two +consecutive checksum-valid frames having identical values there; nothing +is published before that, so the start-up takes a few seconds longer. If +frames keep being dropped for this reason for more than 30 seconds and +two consecutive ones agree on other values, those become the new +reference. +* The tap index is at most 3, the load at most 150 %, the temperature at +most 100 degrees C and the battery voltage between 6 and 18 V. +* `ups.alarm` entries, `OVER` and `LB` appear, and disappear, only after +two consecutive accepted frames agree. `OL`, `OB` and `CHRG` follow +each accepted frame, so a mains cut is reported without that delay. Published data: `ups.mfr`, `ups.model`, `ups.firmware`, `ups.power.nominal`, `ups.temperature`, `ups.load`, `ups.status`, @@ -137,7 +164,7 @@ variants follow the OEM table, but the output voltage formula of the M2 which looks doubtful, so treat `output.voltage` on those with suspicion until someone confirms it. -If no valid frame arrives for more than five seconds the data is marked +If no accepted frame arrives for more than five seconds the data is marked stale. @@ -153,8 +180,9 @@ same device node. On family 10, replies are framed by a 0xAA start byte and have no checksum; the driver re-synchronizes by scanning for the start byte after any short read. On family 14 the frames start with `55 55 AE` and -end with an 8-bit sum, and the driver keeps the newest frame whose sum -is right. +end with an 8-bit sum; the driver checks every frame whose sum is right +against the constants and ranges described above, and keeps the newest +one that passes. AUTHOR diff --git a/drivers/Makefile.am b/drivers/Makefile.am index 8cc9bffd48..6364cd8d9c 100644 --- a/drivers/Makefile.am +++ b/drivers/Makefile.am @@ -252,7 +252,7 @@ bestfcom_SOURCES = bestfcom.c bestfortress_SOURCES = bestfortress.c bestuferrups_SOURCES = bestuferrups.c bestups_SOURCES = bestups.c -ragtech_SOURCES = ragtech.c +ragtech_SOURCES = ragtech.c ragtech_speed.c ragtech_LDADD = $(LDADD_DRIVERS_SERIAL) blazer_ser_SOURCES = blazer.c blazer_ser.c blazer_ser_LDADD = $(LDADD_DRIVERS_SERIAL) -lm @@ -557,7 +557,7 @@ dist_noinst_HEADERS = \ hidparser.h hidtypes.h ietf-mib.h libhid.h libshut.h nut_libusb.h libwinhid.h liebert-hid.h \ main.h mge-hid.h mge-mib.h mge-utalk.h \ mge-xml.h microdowell.h microsol-apc.h microsol-common.h netvision-mib.h netxml-ups.h nut-ipmi.h oneac.h \ - powercom.h powerpanel.h powerp-bin.h powerp-txt.h powervar_cx.h raritan-pdu-mib.h \ + powercom.h powerpanel.h powerp-bin.h powerp-txt.h powervar_cx.h ragtech_speed.h raritan-pdu-mib.h \ safenet.h serial.h sms_ser.h snmp-ups.h solis.h tripplite.h tripplite-hid.h \ upshandler.h usb-common.h usbhid-ups.h powercom-hid.h compaq-mib.h idowell-hid.h microdowell-hid.h \ apc_common.h apcmicrolink-maps.h apcmicrolink.h apcmicrolink-usb.h apcsmart.h apcsmart_tabs.h apcsmart-old.h apcupsd-ups.h cyberpower-mib.h riello.h openups-hid.h \ diff --git a/drivers/ragtech.c b/drivers/ragtech.c index f3fb9ed0ff..a48d0f6967 100644 --- a/drivers/ragtech.c +++ b/drivers/ragtech.c @@ -95,9 +95,18 @@ * it by this driver in this family (no bulk OUT traffic was seen from the * OEM client either, and the OEM table lists no actions for family 14). * - * Line config: 2400 8N1. Unlike family 10, the PIC does apply the line - * coding to its UART here: read at 4800 the stream turns into byte pairs - * such as 66 e6, and at 9600 or above into pulses of 4 or more bit times. + * Line config: 2560 8N1 -- NOT 2400. Unlike family 10, the PIC does apply + * the line coding to its UART here. The OEM devices.xml lists 2560 as the + * first serial speed of this family, and the OEM client used it (which is + * why USBPcap captures made with it are clean). Read at 2400 the receiver + * samples bit 6 right on the bit 6 / bit 7 boundary of the transmitter and + * bit 7 inside the stop bit: bit 7 arrives set where it should be clear, + * bit 6 is unreliable, and most frames fail the checksum. Read at 4800 the + * stream turns into byte pairs such as 66 e6, and at 9600 or above into + * pulses of 4 or more bit times. + * 2560 has no Bxxx constant: ragtech_speed.c sets it (termios2 + BOTHER on + * Linux, a numeric speed_t on the BSDs and macOS). If that fails the driver + * logs a warning and uses 2400, where it can only reject corrupted frames. * DTR=0 and RTS=0 as for family 10. * * Every ~1.25 to 1.3 s the UPS sends one frame of 32 bytes: @@ -106,8 +115,28 @@ * * CS is the 8-bit sum of the 31 preceding bytes (sync bytes included). * The byte "tail" was seen as 0x09 on mains and 0x08 on battery (and - * sporadically after mains returned), so it is NOT checked; the sync bytes - * plus CS are what the driver trusts. The meaning of 0xAE is unknown. + * sporadically after mains returned), so it is NOT checked. The meaning of + * 0xAE is unknown. + * + * The 8-bit sum is a weak check: two bytes that both gain bit 7 add up to + * 0x100 and cancel, so a corrupted frame can pass it (it did, at 2400 baud: + * ups.load 385, a 180 C temperature, "Inverter fault"). A frame is + * therefore only accepted if, besides sync and CS: + * + * - registers 0x00..0x09 (version, model, calibration constants) equal the + * reference. These are fixed in the unit. The reference is taken from + * two consecutive checksum-valid frames with identical 0x00..0x09; until + * then nothing is published. A frame whose 0x00..0x09 differ is dropped. + * If that goes on for more than 30 s and two consecutive valid frames + * agree on a new set, that set becomes the reference (so a wrong + * reference, or a unit that was changed, cannot lock the driver out); + * - ACTUALTAP <= 3, load (IOUTPUT / IOUTCALIB) <= 150 %, TEMPER <= 100, + * and battery voltage (VBATTERY / VBATCALIB * 13.8) within 6..18 V; + * - and the rare indications -- reg 0x0E bits 4..7 (ups.alarm), OVER and + * LB -- are published only when seen in two consecutive accepted frames + * (and cleared only when absent from two). OL/OB and CHRG are not + * debounced: they were confirmed on real transitions, a mains cut must + * not be delayed, and the corruption seen only ever touched bits 6 and 7. * * Register map (the register index is the offset into the 27 bytes): * @@ -147,8 +176,8 @@ * stayed at 3 on mains and on battery) and the beeper bit is unknown. * * Detection: with the "family" option at its default ("auto") the driver - * first listens, writing nothing, for a valid frame at 2400 baud for a few - * seconds. If one arrives the unit is family 14; otherwise the termios + * first listens, writing nothing, for a checksum-valid frame at 2560 baud for + * a few seconds. If one arrives the unit is family 14; otherwise the termios * state is put back and the family 10 poll runs as before. * * @@ -168,6 +197,7 @@ #include "main.h" #include "serial.h" #include "nut_stdint.h" +#include "ragtech_speed.h" #define DRIVER_NAME "Ragtech UPS driver" #define DRIVER_VERSION "0.11" @@ -513,7 +543,14 @@ static double compute_frequency(uint8_t v_fout) #define F14_NREGS 27 #define F14_BUF_LEN 256 /* receive window; keeps only the newest bytes */ #define F14_DETECT_MS 3000 /* passive listen for family auto-detection */ -#define F14_FIRST_SEC 10 /* wait for the first frame when family=14 is forced */ +#define F14_BAUD 2560 /* line speed of the Easy Way UART */ +#define F14_FIRST_SEC 15 /* wait for the first accepted frame (needs two valid ones) */ +#define F14_NCONST 10 /* regs 0x00..0x09 are fixed in the unit */ +#define F14_REF_RETRY_SEC 30 /* mismatching this long: allow a new reference */ +#define F14_LOAD_MAX_PCT 150 /* plausibility limits, see header comment */ +#define F14_TEMP_MAX 100 +#define F14_VBAT_MIN 6.0 +#define F14_VBAT_MAX 18.0 #define F14_STALE_SEC 5 /* no valid frame for this long: data stale */ #define F14_DRAIN_MAX 64 /* safety bound for the drain loop */ @@ -612,14 +649,30 @@ static int family = 10; /* family in use after initups */ static const struct f14_model *f14_model; static uint8_t f14_buf[F14_BUF_LEN]; /* newest received bytes */ static size_t f14_len; -static uint8_t f14_regs[F14_NREGS]; /* registers of the newest valid frame */ -static int f14_have; -static time_t f14_last; /* when that frame was taken */ +static uint8_t f14_regs[F14_NREGS]; /* registers of the newest ACCEPTED frame */ +static int f14_have; /* a frame was accepted at least once */ +static time_t f14_last; /* when the newest one was accepted */ +static unsigned int f14_accepted; /* number of frames accepted so far */ +static uint8_t f14_ref[F14_NCONST]; /* reference for regs 0x00..0x09 */ +static int f14_ref_ok; +static time_t f14_ref_last; /* newest frame that matched the reference */ +static uint8_t f14_cand[F14_NCONST]; /* regs 0x00..0x09 of the last odd frame */ +static int f14_cand_have; +static unsigned int f14_rare_pub; /* debounced rare indications (F14_RARE_*) */ +static unsigned int f14_rare_prev; /* the same, as seen in the previous frame */ +static int f14_rare_prev_ok; +static int f14_line_ok; /* the line is really at F14_BAUD */ #ifndef WIN32 -static struct termios f14_tio_saved; /* termios before the 2400 baud change */ +static struct termios f14_tio_saved; /* termios before the speed change */ static int f14_tio_saved_ok; #endif /* !WIN32 */ +/* Rare indications that are debounced: reg 0x0E bits 4..7 (kept in place) + * and these two. */ +#define F14_RARE_OVER 0x100 +#define F14_RARE_LB 0x200 +#define F14_RARE_ALL (0xF0U | F14_RARE_OVER | F14_RARE_LB) + static const struct f14_model *f14_find_model(uint8_t id) { size_t i; @@ -660,30 +713,155 @@ static void f14_append(const uint8_t *p, size_t n) f14_len += n; } -/* Take the NEWEST valid frame out of the window: keep its registers and - * discard everything up to its end. Returns 1 if there was one. */ -static int f14_take(void) +/* Publish the static data of the unit identified by the registers r. */ +static void f14_set_static(const uint8_t *r) +{ + f14_model = f14_find_model(r[R14_MODEL]); + if (!f14_model) { + upslogx(LOG_WARNING, "unknown family 14 model id 0x%02X, using a generic profile", + r[R14_MODEL]); + f14_model = &f14_fallback; + } + dstate_setinfo("ups.model", "%s", f14_model->name); + dstate_setinfo("ups.firmware", "%.1f", r[R14_VERSION] * 0.1); + if (f14_model->pnom) + dstate_setinfo("ups.power.nominal", "%u", f14_model->pnom); + dstate_setinfo("output.voltage.nominal", "%u", + f14_model->variant == F14_M2 ? 220U : 115U); + dstate_setinfo("output.frequency.nominal", "%u", 60U); + dstate_setinfo("battery.voltage.nominal", "%u", 12U); +} + +/* NULL if the values of a frame are plausible, else what is wrong with it. + * The limits are generous: they only exist to throw out corrupted frames that + * got past the 8-bit sum, not to police the UPS. Status bits are not judged. */ +static const char *f14_implausible(const uint8_t *r) { - size_t start, end; + double v; + + if (r[R14_TAP] > 3) + return "ACTUALTAP above 3"; + if (r[R14_IOUTCALIB] + && (unsigned int)r[R14_IOUTPUT] * 100U + > (unsigned int)F14_LOAD_MAX_PCT * r[R14_IOUTCALIB]) + return "load above 150 %"; + if (r[R14_TEMPER] > F14_TEMP_MAX) + return "temperature above 100 C"; + if (r[R14_VBATCALIB]) { + v = (double)r[R14_VBATTERY] / r[R14_VBATCALIB] * F14_VBAT_FACTOR; + if (v < F14_VBAT_MIN || v > F14_VBAT_MAX) + return "battery voltage outside 6..18 V"; + } + return NULL; +} + +/* The rare indications of a frame, as F14_RARE_* bits. */ +static unsigned int f14_rare_bits(const uint8_t *r) +{ + unsigned int b = r[R14_FAULT] & 0xF0U; + + if (r[R14_FAULT] & B14_FOVERLOAD) + b |= F14_RARE_OVER; + if ((r[R14_WARN] & B14_LOBATTERY) || (r[R14_FAULT] & B14_FENDBATTERY)) + b |= F14_RARE_LB; + return b; +} - if (f14_len < F14_FRAME_LEN) +/* One checksum-valid frame (r = its 27 registers) arrived: accept it as the + * new current data, or drop it. Returns 1 if accepted. */ +static int f14_accept(const uint8_t *r) +{ + time_t now = time(NULL); + const char *why; + unsigned int rare, agree; + + if (!f14_ref_ok) { + /* The first frame is only remembered; a second one with the + * same constants makes them the reference. */ + if (!f14_cand_have || memcmp(f14_cand, r, F14_NCONST)) { + memcpy(f14_cand, r, F14_NCONST); + f14_cand_have = 1; + upsdebugx(2, "family 14: waiting for a second frame with the same constants"); + return 0; + } + memcpy(f14_ref, r, F14_NCONST); + f14_ref_ok = 1; + f14_ref_last = now; + f14_cand_have = 0; + upsdebugx(1, "family 14: constant registers confirmed by two frames"); + } else if (memcmp(f14_ref, r, F14_NCONST) != 0) { + /* Corrupted, unless the reference itself was wrong or the unit + * changed: that is told by two consecutive frames agreeing on + * something else for a good while. */ + if (f14_cand_have && !memcmp(f14_cand, r, F14_NCONST) + && now - f14_ref_last > F14_REF_RETRY_SEC) { + memcpy(f14_ref, r, F14_NCONST); + f14_ref_last = now; + f14_cand_have = 0; + upslogx(LOG_NOTICE, "family 14: the constant registers changed " + "and stayed so; taking them as the new reference"); + f14_set_static(r); + } else { + memcpy(f14_cand, r, F14_NCONST); + f14_cand_have = 1; + upsdebugx(2, "family 14: frame dropped, registers 0x00..0x09 " + "differ from the reference"); + return 0; + } + } else { + f14_ref_last = now; + f14_cand_have = 0; + } + + why = f14_implausible(r); + if (why) { + upsdebugx(2, "family 14: frame dropped, %s", why); return 0; + } - for (start = f14_len - F14_FRAME_LEN + 1; start-- > 0; ) { - if (!f14_frame_ok(f14_buf + start)) - continue; - memcpy(f14_regs, f14_buf + start + 3, F14_NREGS); - f14_have = 1; - f14_last = time(NULL); - end = start + F14_FRAME_LEN; - memmove(f14_buf, f14_buf + end, f14_len - end); - f14_len -= end; - return 1; + /* A rare indication changes only when two accepted frames in a row + * agree on it. */ + rare = f14_rare_bits(r); + if (f14_rare_prev_ok) { + agree = ~(rare ^ f14_rare_prev) & F14_RARE_ALL; + f14_rare_pub = (f14_rare_pub & ~agree) | (rare & agree); } - return 0; + f14_rare_prev = rare; + f14_rare_prev_ok = 1; + + memcpy(f14_regs, r, F14_NREGS); + f14_have = 1; + f14_last = now; + f14_accepted++; + return 1; } -/* Read whatever is queued on the port without waiting. */ +/* Go through the window in order: every checksum-valid frame is offered to + * f14_accept() and removed; bytes that cannot start a frame any more are + * dropped, a partial frame at the end is kept. Returns how many + * checksum-valid frames there were (accepted or not; f14_accepted tells). */ +static int f14_take(void) +{ + size_t start = 0; + int seen = 0; + + while (start + F14_FRAME_LEN <= f14_len) { + if (f14_frame_ok(f14_buf + start)) { + seen++; + (void)f14_accept(f14_buf + start + 3); + start += F14_FRAME_LEN; + } else { + start++; + } + } + if (start) { + memmove(f14_buf, f14_buf + start, f14_len - start); + f14_len -= start; + } + return seen; +} + +/* Read what is pending (without waiting) into the window. */ static void f14_drain(void) { uint8_t tmp[64]; @@ -699,7 +877,8 @@ static void f14_drain(void) } } -/* Listen up to `ms` milliseconds, returning as soon as a valid frame is in. */ +/* Listen up to `ms` milliseconds, returning as soon as a checksum-valid + * frame is in (that is all the auto-detection needs). */ static int f14_listen(unsigned int ms) { uint8_t tmp[64]; @@ -724,8 +903,22 @@ static int f14_listen(unsigned int ms) } } -/* Switch the port to 2400 baud. The termios state before the change is - * kept so that the family 10 path can be given back exactly what it had. */ +/* Forget everything learned from the stream (the family 10 fallback). */ +static void f14_reset(void) +{ + f14_len = 0; + f14_have = 0; + f14_ref_ok = 0; + f14_cand_have = 0; + f14_rare_pub = 0; + f14_rare_prev = 0; + f14_rare_prev_ok = 0; +} + +/* Put the port at F14_BAUD. The termios state before the change is kept so + * that the family 10 path can be given back exactly what it had. If the speed + * cannot be applied the port goes to 2400 and f14_line_ok stays 0; the warning + * is logged only once the unit is known to be family 14. */ static void f14_set_line(void) { #ifndef WIN32 @@ -735,6 +928,12 @@ static void f14_set_line(void) return; f14_tio_saved = tio; f14_tio_saved_ok = 1; + if (ragtech_set_baud(upsfd, F14_BAUD) == 0) { + f14_line_ok = 1; + upsdebugx(1, "family 14: line set to %u baud", F14_BAUD); + return; + } + upsdebugx(1, "family 14: could not set %u baud, using 2400", F14_BAUD); cfsetispeed(&tio, B2400); cfsetospeed(&tio, B2400); tcsetattr(upsfd, TCSANOW, &tio); @@ -744,8 +943,11 @@ static void f14_set_line(void) static void f14_restore_line(void) { #ifndef WIN32 - if (f14_tio_saved_ok) + if (f14_tio_saved_ok) { tcsetattr(upsfd, TCSANOW, &f14_tio_saved); + ragtech_restore_baud(upsfd); + } + f14_line_ok = 0; #endif /* !WIN32 */ } @@ -810,6 +1012,16 @@ static void f14_decode(const uint8_t *r, const struct f14_model *m, | B14_FABNORMALVBAT | B14_FINVERTER); } +/* Values of the newest accepted frame, with the rare indications as + * debounced instead of as in the frame itself. */ +static void f14_current(struct f14_data *d) +{ + f14_decode(f14_regs, f14_model, d); + d->faults = f14_rare_pub & 0xF0U; + d->over = (f14_rare_pub & F14_RARE_OVER) != 0; + d->lb = (f14_rare_pub & F14_RARE_LB) != 0; +} + static void f14_publish(const struct f14_data *d) { dstate_setinfo("input.voltage", "%.1f", d->vin); @@ -843,7 +1055,7 @@ static void f14_publish(const struct f14_data *d) status_commit(); } -/* Static data, once, from the first frame. */ +/* Static data, once, from the first accepted frame. */ static void f14_initinfo(void) { struct f14_data d; @@ -857,57 +1069,53 @@ static void f14_initinfo(void) "allow_shutdown is ignored on family 14: no shutdown " "command is known for these units, the driver is read-only"); + if (!f14_line_ok) + upslogx(LOG_WARNING, + "could not set %u baud; falling back to 2400, expect " + "corrupted frames", F14_BAUD); + + /* An accepted frame needs the constants confirmed by two frames. */ while (!f14_have) { f14_drain(); - if (f14_take()) + f14_take(); + if (f14_have) break; if (time(NULL) - t0 > F14_FIRST_SEC) fatalx(EXIT_FAILURE, - "no valid family 14 frame received in %d s -- is the UPS " - "connected and not held by another program (e.g. Ragtech supsvc)?", - F14_FIRST_SEC); + "no acceptable family 14 frame received in %d s -- is the UPS " + "connected and not held by another program (e.g. Ragtech supsvc)? " + "Corrupted frames mean a wrong line speed (it must be %u baud)", + F14_FIRST_SEC, F14_BAUD); usleep(100000); } - f14_model = f14_find_model(f14_regs[R14_MODEL]); - if (!f14_model) { - upslogx(LOG_WARNING, "unknown family 14 model id 0x%02X, using a generic profile", - f14_regs[R14_MODEL]); - f14_model = &f14_fallback; - } - dstate_setinfo("ups.model", "%s", f14_model->name); - dstate_setinfo("ups.firmware", "%.1f", f14_regs[R14_VERSION] * 0.1); - if (f14_model->pnom) - dstate_setinfo("ups.power.nominal", "%u", f14_model->pnom); - dstate_setinfo("output.voltage.nominal", "%u", - f14_model->variant == F14_M2 ? 220U : 115U); - dstate_setinfo("output.frequency.nominal", "%u", 60U); - dstate_setinfo("battery.voltage.nominal", "%u", 12U); + f14_set_static(f14_regs); upslogx(LOG_INFO, "family 14 (Easy Way, read-only): %s, firmware %.1f", f14_model->name, f14_regs[R14_VERSION] * 0.1); - /* first values, from the frame that identified the unit */ - f14_decode(f14_regs, f14_model, &d); + /* first values, from the frame that completed the identification */ + f14_current(&d); f14_publish(&d); } static void f14_updateinfo(void) { struct f14_data d; - int fresh; + unsigned int before = f14_accepted; f14_drain(); - fresh = f14_take(); + f14_take(); - if (!fresh && (!f14_have || time(NULL) - f14_last > F14_STALE_SEC)) { - upsdebugx(2, "family 14: no valid frame for more than %d s", F14_STALE_SEC); + if (f14_accepted == before + && (!f14_have || time(NULL) - f14_last > F14_STALE_SEC)) { + upsdebugx(2, "family 14: no acceptable frame for more than %d s", F14_STALE_SEC); dstate_datastale(); return; } - if (fresh) { - f14_decode(f14_regs, f14_model, &d); + if (f14_accepted != before) { + f14_current(&d); f14_publish(&d); } dstate_dataok(); @@ -1195,7 +1403,7 @@ void upsdrv_initups(void) } #endif /* !WIN32 */ - /* Family 14 needs the real line speed (2400); family 10 does not care, + /* Family 14 needs the real line speed (2560); family 10 does not care, * so with family=10 the port is left exactly as before. */ if (family_opt != 10) f14_set_line(); @@ -1234,13 +1442,13 @@ void upsdrv_initups(void) if (f14_listen(F14_DETECT_MS)) { family = 14; } else { - /* Give family 10 back the termios it had before the 2400 - * baud change, and re-assert DTR/RTS low after it. */ + /* Give family 10 back the termios it had before the + * speed change, and re-assert DTR/RTS low after it. */ f14_restore_line(); ser_set_dtr(upsfd, 0); ser_set_rts(upsfd, 0); usleep(RAGTECH_POST_OPEN_MS * 1000); - f14_len = 0; + f14_reset(); family = 10; } } else { diff --git a/drivers/ragtech_speed.c b/drivers/ragtech_speed.c new file mode 100644 index 0000000000..839afeaeca --- /dev/null +++ b/drivers/ragtech_speed.c @@ -0,0 +1,152 @@ +/* ragtech_speed.c - arbitrary serial line speed helper for the ragtech driver + * + * Why a file of its own: the Linux way to ask for a speed that has no Bxxx + * constant is "struct termios2" with BOTHER and the TCGETS2/TCSETS2 ioctls, + * and those come from , which redefines "struct termios" and + * so cannot be included together with the that the rest of the + * driver (and NUT's serial.h) use. Keeping the Linux code in a translation + * unit that never sees avoids the clash without any tricks. + * + * Three variants, picked at compile time: + * + * - Linux with TCSETS2/BOTHER: the kernel termios2 interface; + * - systems where speed_t is the plain number (the BSDs, macOS): the + * number goes straight into cfsetispeed()/cfsetospeed(); + * - anything else (Windows, Linux without termios2): not supported, the + * caller is told so and falls back to a standard speed. + * + * Copyright (C) 2026 juslex + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include "config.h" + +#include "ragtech_speed.h" + +#if defined(__linux__) +/* No anywhere in this file on Linux, see above. */ +# include +# include +# include +# if defined(TCGETS2) && defined(TCSETS2) && defined(BOTHER) +# define RAGTECH_SPEED_TERMIOS2 1 +# endif +#elif !defined(_WIN32) && !defined(WIN32) +# include +# if defined(B2400) && (B2400 == 2400) +# define RAGTECH_SPEED_NUMERIC 1 +# endif +#endif + +#if defined(RAGTECH_SPEED_TERMIOS2) + +static struct termios2 saved_t2; +static int saved_ok = 0; + +int ragtech_set_baud(int fd, unsigned int baud) +{ + struct termios2 prev, t; + + if (ioctl(fd, TCGETS2, &prev) != 0) + return -1; + t = prev; + + t.c_cflag &= (tcflag_t)~(tcflag_t)CBAUD; +# ifdef CIBAUD + t.c_cflag &= (tcflag_t)~(tcflag_t)CIBAUD; /* input speed follows output speed */ +# endif + t.c_cflag |= BOTHER; + t.c_ispeed = baud; + t.c_ospeed = baud; + + if (ioctl(fd, TCSETS2, &t) != 0) + return -1; + + /* Ask what the port really has now: the tty layer may have rounded the + * speed, or the port driver may have refused it without an error. */ + if (ioctl(fd, TCGETS2, &t) != 0 || (t.c_cflag & CBAUD) != BOTHER + || t.c_ospeed != baud) { + (void)ioctl(fd, TCSETS2, &prev); + return -1; + } + + if (!saved_ok) { + saved_t2 = prev; + saved_ok = 1; + } + return 0; +} + +void ragtech_restore_baud(int fd) +{ + if (saved_ok) { + (void)ioctl(fd, TCSETS2, &saved_t2); + saved_ok = 0; + } +} + +#elif defined(RAGTECH_SPEED_NUMERIC) + +static struct termios saved_tio; +static int saved_ok = 0; + +int ragtech_set_baud(int fd, unsigned int baud) +{ + struct termios prev, t; + + if (tcgetattr(fd, &prev) != 0) + return -1; + t = prev; + + if (cfsetispeed(&t, (speed_t)baud) != 0 + || cfsetospeed(&t, (speed_t)baud) != 0 + || tcsetattr(fd, TCSANOW, &t) != 0) + return -1; + + /* tcsetattr() reports success if any part of the change took effect, + * so read the speed back. */ + if (tcgetattr(fd, &t) != 0 || cfgetospeed(&t) != (speed_t)baud + || cfgetispeed(&t) != (speed_t)baud) { + (void)tcsetattr(fd, TCSANOW, &prev); + return -1; + } + + if (!saved_ok) { + saved_tio = prev; + saved_ok = 1; + } + return 0; +} + +void ragtech_restore_baud(int fd) +{ + if (saved_ok) { + (void)tcsetattr(fd, TCSANOW, &saved_tio); + saved_ok = 0; + } +} + +#else /* no way to ask for an arbitrary speed on this platform */ + +int ragtech_set_baud(int fd, unsigned int baud) +{ + (void)fd; + (void)baud; + return -1; +} + +void ragtech_restore_baud(int fd) +{ + (void)fd; +} + +#endif diff --git a/drivers/ragtech_speed.h b/drivers/ragtech_speed.h new file mode 100644 index 0000000000..9375b07949 --- /dev/null +++ b/drivers/ragtech_speed.h @@ -0,0 +1,35 @@ +/* ragtech_speed.h - arbitrary serial line speed helper for the ragtech driver + * + * Copyright (C) 2026 juslex + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#ifndef RAGTECH_SPEED_H_SEEN +#define RAGTECH_SPEED_H_SEEN 1 + +/* Set a non-standard line speed (such as 2560 baud, which has no Bxxx + * constant) on the serial device `fd`, keeping every other termios setting. + * + * Returns 0 if the port now reports the requested speed, -1 if it could not + * be applied (platform without support, ioctl/tcsetattr failure, or the + * driver of the port quietly picked another speed). On failure the speed of + * the port is left as it was. + * + * The state before the first successful call is remembered, to be given back + * by ragtech_restore_baud(). */ +int ragtech_set_baud(int fd, unsigned int baud); + +/* Undo ragtech_set_baud(): put back the exact termios state the port had + * before it. Does nothing if ragtech_set_baud() did not succeed. */ +void ragtech_restore_baud(int fd); + +#endif /* RAGTECH_SPEED_H_SEEN */ From f4cfbf245045467b7014682d5c34175a7537f1d7 Mon Sep 17 00:00:00 2001 From: juslex <66561713+juslex@users.noreply.github.com> Date: Wed, 28 Oct 2026 09:29:43 -0300 Subject: [PATCH 4/4] ragtech: make the family 14 speed and listen-time macros unsigned They are printed with %u; clang -Wformat (fatal in CI) rejected the int constants. Co-Authored-By: Claude Opus 5.5 Signed-off-by: juslex <66561713+juslex@users.noreply.github.com> --- drivers/ragtech.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/ragtech.c b/drivers/ragtech.c index a48d0f6967..7b0e4096f3 100644 --- a/drivers/ragtech.c +++ b/drivers/ragtech.c @@ -542,8 +542,8 @@ static double compute_frequency(uint8_t v_fout) #define F14_FRAME_LEN 32 /* 3 sync + 27 registers + tail + checksum */ #define F14_NREGS 27 #define F14_BUF_LEN 256 /* receive window; keeps only the newest bytes */ -#define F14_DETECT_MS 3000 /* passive listen for family auto-detection */ -#define F14_BAUD 2560 /* line speed of the Easy Way UART */ +#define F14_DETECT_MS 3000U /* passive listen for family auto-detection */ +#define F14_BAUD 2560U /* line speed of the Easy Way UART */ #define F14_FIRST_SEC 15 /* wait for the first accepted frame (needs two valid ones) */ #define F14_NCONST 10 /* regs 0x00..0x09 are fixed in the unit */ #define F14_REF_RETRY_SEC 30 /* mismatching this long: allow a new reference */