diff --git a/NEWS.adoc b/NEWS.adoc index 51c221fc52..8c2994e563 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -505,6 +505,20 @@ 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 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. + 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/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..edcbc5b796 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,20 @@ 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 +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, +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 +99,75 @@ 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 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 +*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 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`, +`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 accepted frame arrives for more than five seconds the data is marked +stale. + + CAVEATS ------- @@ -86,9 +177,12 @@ 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; 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/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 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 9726dbe84b..7b0e4096f3 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,100 @@ * 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: 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: + * + * 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 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): + * + * 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 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. + * + * * Copyright (C) 2026 juslex * * This program is free software; you can redistribute it and/or modify @@ -102,9 +197,10 @@ #include "main.h" #include "serial.h" #include "nut_stdint.h" +#include "ragtech_speed.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 +530,597 @@ 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 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 */ +#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 */ + +/* 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 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 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; + 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; +} + +/* 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) +{ + 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; +} + +/* 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; + } + + /* 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); + } + 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; +} + +/* 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]; + 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 checksum-valid + * frame is in (that is all the auto-detection needs). */ +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; + } +} + +/* 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 + struct termios tio; + + if (tcgetattr(upsfd, &tio) != 0) + 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); +#endif /* !WIN32 */ +} + +static void f14_restore_line(void) +{ +#ifndef WIN32 + if (f14_tio_saved_ok) { + tcsetattr(upsfd, TCSANOW, &f14_tio_saved); + ragtech_restore_baud(upsfd); + } + f14_line_ok = 0; +#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); +} + +/* 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); + 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 accepted 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"); + + 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(); + f14_take(); + if (f14_have) + break; + if (time(NULL) - t0 > F14_FIRST_SEC) + fatalx(EXIT_FAILURE, + "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_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 completed the identification */ + f14_current(&d); + f14_publish(&d); +} + +static void f14_updateinfo(void) +{ + struct f14_data d; + unsigned int before = f14_accepted; + + f14_drain(); + f14_take(); + + 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 (f14_accepted != before) { + f14_current(&d); + f14_publish(&d); + } + dstate_dataok(); +} + void upsdrv_initinfo(void) { uint8_t reply[64]; @@ -441,6 +1128,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 +1236,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 +1310,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 +1343,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 +1365,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 +1403,11 @@ void upsdrv_initups(void) } #endif /* !WIN32 */ + /* 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(); + /* 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 +1431,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 + * 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_reset(); + family = 10; + } + } else { + family = 10; + } + upsdebugx(1, "protocol family %d", family); } void upsdrv_cleanup(void) 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 */