diff --git a/configure.ac b/configure.ac index 51dad7111e..90a51c863d 100644 --- a/configure.ac +++ b/configure.ac @@ -958,7 +958,7 @@ dnl not fail if we have no tools to generate it (so add to SKIP list). AC_MSG_CHECKING([if we can build ${nut_doc_build_target_base}]) can_build_doc_man=no if test "${nut_have_asciidoc}" = yes ; then - ( cd "$DOCTESTDIR" && ${A2X} --format manpage --destination-dir=. --xsltproc-opts "--nonet" "${abs_srcdir}"/docs/man/snmp-ups.txt && test -s snmp-ups.8 ) && can_build_doc_man=yes + ( cd "$DOCTESTDIR" && ${A2X} --format manpage --destination-dir=. --xsltproc-opts="--nonet" "${abs_srcdir}"/docs/man/snmp-ups.txt && test -s snmp-ups.8 ) && can_build_doc_man=yes rm -f "${DOCTESTDIR}"/snmp-ups.8 fi if test "${can_build_doc_man}" = yes ; then diff --git a/docs/Makefile.am b/docs/Makefile.am index f7b7ff33da..6f2f884f50 100644 --- a/docs/Makefile.am +++ b/docs/Makefile.am @@ -144,10 +144,10 @@ solaris-usb.html solaris-usb.chunked solaris-usb.pdf: solaris-usb.txt asciidoc.c # variable ASCIIDOC_VERBOSE to "-v", ie: # $ ASCIIDOC_VERBOSE=-v make A2X_COMMON_OPTS = $(ASCIIDOC_VERBOSE) --attribute icons \ - --xsltproc-opts "--nonet" \ - --xsltproc-opts "--stringparam nut.localdate \"`TZ=UTC date +%Y-%m-%d`\"" \ - --xsltproc-opts "--stringparam nut.localtime \"`TZ=UTC date +%H:%M:%S`\"" \ - --xsltproc-opts "--stringparam nut.nutversion \"@PACKAGE_VERSION@\"" \ + --xsltproc-opts="--nonet" \ + --xsltproc-opts="--stringparam nut.localdate \"`TZ=UTC date +%Y-%m-%d`\"" \ + --xsltproc-opts="--stringparam nut.localtime \"`TZ=UTC date +%H:%M:%S`\"" \ + --xsltproc-opts="--stringparam nut.nutversion \"@PACKAGE_VERSION@\"" \ --attribute iconsdir=$(srcdir)/images \ --attribute=badges \ --attribute=external_title \ diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index a70519f7d6..14c9bb66d2 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -616,16 +616,19 @@ endif HTML_MACOSX_MANS = macosx-ups.html -SRC_MODBUS_PAGES = phoenixcontact_modbus.txt +SRC_MODBUS_PAGES = phoenixcontact_modbus.txt \ + generic_modbus.txt if WITH_MANS -MAN_MODBUS_PAGES = phoenixcontact_modbus.8 +MAN_MODBUS_PAGES = phoenixcontact_modbus.8 \ + generic_modbus.8 endif if WITH_MODBUS man8_MANS += $(MAN_MODBUS_PAGES) endif -HTML_MODBUS_MANS = phoenixcontact_modbus.html +HTML_MODBUS_MANS = phoenixcontact_modbus.html \ + generic_modbus.html SRC_LINUX_I2C_PAGES = asem.txt pijuice.txt if WITH_MANS @@ -814,7 +817,7 @@ if HAVE_ASCIIDOC ### Prior to Asciidoc ~8.6.8, the --destination-dir flag didn't seem to affect the location of the intermediate .xml file. ### This parameter is currently required; see docs/Makefile.am for more detail. A2X_MANPAGE_OPTS = --doctype manpage --format manpage \ - --xsltproc-opts "--nonet" \ + --xsltproc-opts="--nonet" \ --attribute mansource="Network UPS Tools" \ --attribute manversion="@PACKAGE_VERSION@" \ --attribute manmanual="NUT Manual" \ diff --git a/docs/man/generic_modbus.txt b/docs/man/generic_modbus.txt new file mode 100644 index 0000000000..adba2b6cdf --- /dev/null +++ b/docs/man/generic_modbus.txt @@ -0,0 +1,222 @@ +GENERIC_MODBUS(8) +================= + +NAME +---- + +generic_modbus - Driver for contact (direct) signal UPS devices connected via modbus remote I/O gateways + +SYNOPSIS +-------- + +*generic_modbus* -h + +*generic_modbus* -a 'DEVICE_NAME' ['OPTIONS'] + +NOTE: This man page only documents the specific features of the *generic_modbus* driver. For information about the core driver, see linkman:nutupsdrv[8]. + +SUPPORTED HARDWARE +------------------ + +This is a generic modbus driver expected to work with contact (direct) signal UPS devices, connected via modbus RIO (remote I/O) either serial or TCP/IP. The driver has been tested against PULS UPS (model UB40.241) via MOXA ioLogikR1212 (RS485) and ioLogikE1212 (TCP/IP). + +More information about this UPS can be found here:: +https://products.pulspower.com/ca/ubc10-241-n1.html + +More information about Moxa ioLogik R1212, E1212 can be found here:: +https://www.moxa.com/en/products/industrial-edge-connectivity/controllers-and-ios + +The PULS UPS UB40.241 supports the following signals: + +[source, conf] +---- +Ready contact (DO) <--> HB +Buffering contact (DO) <--> OL | OB +Battery-low (DO) <--> LB +Replace Battery (DO) <--> RB +Inhibit (DI) <--> FSD +---- + +Digital port direction (DI/DO) assumes the device perspective + +The driver's concept is to map the UPS states (as defined in nut) onto UPS contacts' states. The driver has an extended configuration interface implemented using variables defined in ups.conf. + +HARDWARE INTERCONNECTION +------------------------ + +The commission of modbus remote I/O server as well as UPS device is carried out following the corresponding instruction manuals. The following figure depicts the anticipated communication path and hardware interconnection: + +[source, conf] +---- ++------+ +----------------+ +------------+ +------------+ +| UPSD | <---> | GENERIC_MODBUS | <---> | MODBUS RIO | <---> | UPS DEVICE | ++------+ (1) +----------------+ (2) +------------+ (3) +------------+ + | | + +-------------------+ + HOST CONTROLLER + +(1) Unix IPC +(2) RS232 | TCP/IP +(3) contacts +---- + +EXTRA ARGUMENTS +--------------- + +This driver supports the following optional settings in the linkman:ups.conf[5] file: + +Generic: +~~~~~~~ + +*device_mfr*='value':: +A string specifying the manufacturer of the UPS device (default UNKNOWN). + +*device_model*='value':: +A string specifying the model of the UPS device (default UNKNOWN). + +Serial: +~~~~~~ + +*ser_baud_rate*='value':: +A integer specifying the serial port baud rate (default 9600). + +*ser_data_bit*='value':: +A integer specifying the serial port data bit (default 8). + +*ser_parity*='value':: +A character specifying the serial port parity (default N). + +*ser_stop_bit*='value':: +An integer specifying the serial port stop bit (default 1). + +Modbus: +~~~~~~ + +*rio_slave_id*='value':: +An integer specifying the RIO modbus slave ID (default 1). + +States (X = OL, OB, LB, HB, RB, CHRG, DISCHRG, FSD) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +*_addr*='value':: +A number specifying the modbus address for the X state. + +*_regtype*='value':: +A number specifying the modbus register type for the X state +Default values::: ++ +[source, conf] +---- +1 for X = OL, OB, LB ,HB, RB, CHRG, DISCHRG +0 for X = FSD +---- +Valid values::: ++ +[source, conf] +---- +0:COIL, 1:INPUT_B, 2:INPUT_R, 3:HOLDING +---- + +*_noro*='value':: +A number specifying the contact configuration for the X state (default 1). +Valid values::: ++ +[source, conf] +---- +0:NC, 1:NO +---- ++ +NOTE: NO stands for normally open and NC for normally closed contact + +Shutdown +~~~~~~~~ + +*FSD_pulse_duration*='value':: +A number specifying the duration in ms for the inhibit pulse. If it's not defined, signal has only one transition depending on FSD_noro configuration. ++ +Examples for FSD signal configuration: +[source, conf] +---- +FSD_noro = 1 +FSD_pulse_duration = 150 + + +-----+ + | | +inhibit pulse >-----+ +------------------> + <---> + 150ms + + +FSD_noro = 0 + +inhibit pulse >-----+ + | + +------------------------> + +---- + +CONFIGURATION +------------- + +Here is an example of generic_modbus driver configuration in *ups.conf* file: +[source, conf] +---- +[generic_modbus] + driver = generic_modbus + port = /dev/ttyUSB0 + desc = "generic ups driver" + # device info + device_mfr = "PULS" + device_model = "UB40.241" + # serial settings + ser_baud_rate = 9600 + ser_parity = N + ser_data_bit = 8 + ser_stop_bit = 1 + # modbus slave id + rio_slave_id = 5 + # UPS signal state attributes + OB_addr = 0x0 + OB_regtype = 1 + OB_noro = 0 + LB_addr = 0x1 + LB_regtype = 1 + HB_addr = 0x2 + HB_regtype = 1 + RB_addr = 0x3 + RB_regtype = 1 + FSD_addr = 0x0 + FSD_regtype = 0 + FSD_pulse_duration = 150 +---- + +INSTANT COMMANDS +---------------- + +This driver support the following instant commands: + +load.off:: +executes "instant poweroff" + +INSTALLATION +------------ + +This driver is not built by default. You can build it by installing libmodbus and running `configure --with-modbus=yes`. + +You also need to give proper permissions on the local serial device file (/dev/ttyUSB0 for example) to allow the NUT user to access it. + +AUTHOR +------ +Dimitris Economou + +SEE ALSO +-------- +The core driver: +~~~~~~~~~~~~~~~~ +linkman:nutupsdrv[8], linkman:ups.conf[5] + +Internet resources: +~~~~~~~~~~~~~~~~~~~ +The NUT (Network UPS Tools) home page: http://www.networkupstools.org/ + +libmodbus home page: http://libmodbus.org diff --git a/docs/nut.dict b/docs/nut.dict index 7401c79413..040031f590 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -252,6 +252,7 @@ DiSplay Diehl Dietze Digitus +Dimitris Dly Dmitry DocBook @@ -276,6 +277,7 @@ ESXi ETIME EUROCASE EXtreme +Economou Edlman Edmundsson Edscott @@ -427,6 +429,7 @@ INV INVOLT IPAR IPBX +IPC IPs IPv IRIX @@ -601,6 +604,8 @@ Monett Morioka Morozov Moskovitch +Moxa +MOXA Mozilla Msg Multiplug @@ -754,6 +759,7 @@ PSKxn PSSENTR PSUs PSX +PULS PV PWLv PWR @@ -1755,6 +1761,9 @@ inverterlog inverterminutes invertervolts io +ioLogik +ioLogikE +ioLogikR iostream ip ipE @@ -2002,6 +2011,7 @@ nomdcvolts nomfrequency noout norating +noro noscanlangid notAfter notifyme @@ -2185,6 +2195,7 @@ reentrancy refactoring referencenominal regex +regtype reposurgeon repotec req @@ -2198,6 +2209,7 @@ rfc rh richcomm riello +rio rj rk rkm @@ -2431,6 +2443,7 @@ tx txt typedef uA +UB uD uM ua diff --git a/drivers/Makefile.am b/drivers/Makefile.am index cbbca4764b..6b63e44e4b 100644 --- a/drivers/Makefile.am +++ b/drivers/Makefile.am @@ -44,7 +44,7 @@ SERIAL_USB_DRIVERLIST = \ nutdrv_qx NEONXML_DRIVERLIST = netxml-ups MACOSX_DRIVERLIST = macosx-ups -MODBUS_DRIVERLIST = phoenixcontact_modbus +MODBUS_DRIVERLIST = phoenixcontact_modbus generic_modbus LINUX_I2C_DRIVERLIST = asem pijuice # distribute all drivers, even ones that are not built by default @@ -239,9 +239,11 @@ macosx_ups_LDADD = $(LDADD_DRIVERS) macosx_ups_LDFLAGS = $(LDFLAGS) -framework IOKit -framework CoreFoundation macosx_ups_SOURCES = macosx-ups.c -# Modbus driver +# Modbus drivers phoenixcontact_modbus_SOURCES = phoenixcontact_modbus.c phoenixcontact_modbus_LDADD = $(LDADD_DRIVERS) $(LIBMODBUS_LIBS) +generic_modbus_SOURCES = generic_modbus.c +generic_modbus_LDADD = $(LDADD_DRIVERS) $(LIBMODBUS_LIBS) # Linux I2C drivers asem_LDADD = $(LDADD_DRIVERS) @@ -293,7 +295,7 @@ dist_noinst_HEADERS = apc-mib.h apc-hid.h baytech-mib.h bcmxcp.h bcmxcp_ser.h \ xppc-mib.h huawei-mib.h eaton-ats16-nmc-mib.h eaton-ats16-nm2-mib.h apc-ats-mib.h raritan-px2-mib.h eaton-ats30-mib.h \ apc-pdu-mib.h eaton-pdu-genesis2-mib.h eaton-pdu-marlin-mib.h \ eaton-pdu-pulizzi-mib.h eaton-pdu-revelation-mib.h emerson-avocent-pdu-mib.h \ - hpe-pdu-mib.h powervar-hid.h delta_ups-hid.h + hpe-pdu-mib.h powervar-hid.h delta_ups-hid.h generic_modbus.h # Define a dummy library so that Automake builds rules for the # corresponding object files. This library is not actually built, diff --git a/drivers/generic_modbus.c b/drivers/generic_modbus.c new file mode 100644 index 0000000000..230484d29c --- /dev/null +++ b/drivers/generic_modbus.c @@ -0,0 +1,855 @@ +/* generic_modbus.c - Driver for generic UPS connected via modbus RIO + * + * Copyright (C) + * 2021 Dimitris Economou + * + * 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. + * + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#include "main.h" +#include "generic_modbus.h" +#include +#include + +#define DRIVER_NAME "NUT Generic Modbus driver" +#define DRIVER_VERSION "0.01" + +/* variables */ +modbus_t *mbctx = NULL; /* modbus memory context */ +sigattr_t sigar[NUMOF_SIG_STATES]; /* array of ups signal attributes */ +int errcnt = 0; /* modbus access error counter */ + +char *device_mfr = DEVICE_MFR; /* device manufacturer */ +char *device_model = DEVICE_MODEL; /* device model */ +int ser_baud_rate = BAUD_RATE; /* serial port baud rate */ +int ser_parity = PARITY; /* serial port parity */ +int ser_data_bit = DATA_BIT; /* serial port data bit */ +int ser_stop_bit = STOP_BIT; /* serial port stop bit */ +int rio_slave_id = MODBUS_SLAVE_ID; /* set device ID to default value */ +int FSD_pulse_duration = SHTDOWN_PULSE_DURATION; /* set the FSD pulse duration */ + +/* get config vars set by -x or defined in ups.conf driver section */ +void get_config_vars(); + +/* create a new modbus context based on connection type (serial or TCP) */ +modbus_t *modbus_new(const char *port); + +/* modbus register read function */ +int register_read(modbus_t *mb, int addr, regtype_t type, void *data); + +/* instant command triggered by upsd */ +int upscmd(const char *cmd, const char *arg); + +/* read signal status */ +int get_signal_state(devstate_t state); + +/* count the time elapsed since start */ +long time_elapsed(struct timeval *start); + +/* driver description structure */ +upsdrv_info_t upsdrv_info = { + DRIVER_NAME, + DRIVER_VERSION, + "Dimitris Economou \n", + DRV_BETA, + {NULL} +}; + +/* + * driver functions + */ + +/* initialize ups driver information */ +void upsdrv_initinfo(void) { + upsdebugx(2, "upsdrv_initinfo"); + + /* set device information */ + dstate_setinfo("device.mfr", "%s", device_mfr); + dstate_setinfo("device.model", "%s", device_model); + + /* register instant commands */ + if (sigar[FSD_T].addr != NOTUSED) { + dstate_addcmd("load.off"); + } + + /* set callback for instant commands */ + upsh.instcmd = upscmd; +} + +/* open serial connection and connect to modbus RIO */ +void upsdrv_initups(void) +{ + int rval; + upsdebugx(2, "upsdrv_initups"); + + get_config_vars(); + + /* open serial port */ + mbctx = modbus_new(device_path); + if (mbctx == NULL) { + fatalx(EXIT_FAILURE, "modbus_new_rtu: Unable to open serial port context"); + } + + /* set slave ID */ + rval = modbus_set_slave(mbctx, rio_slave_id); /* slave ID */ + if (rval < 0) { + modbus_free(mbctx); + fatalx(EXIT_FAILURE, "modbus_set_slave: Invalid modbus slave ID %d\n", rio_slave_id); + } + + /* connect to modbus device */ + if (modbus_connect(mbctx) == -1) { + modbus_free(mbctx); + fatalx(EXIT_FAILURE, "modbus_connect: unable to connect: %s\n", modbus_strerror(errno)); + } +} + +/* update UPS signal state */ +void upsdrv_updateinfo(void) +{ + int rval; + int online = -1; /* keep online state */ + errcnt = 0; + + upsdebugx(2, "upsdrv_updateinfo"); + status_init(); /* initialize ups.status update */ + alarm_init(); /* initialize ups.alarm update */ + + /* + * update UPS status regarding MAINS state either via OL | OB. + * if both statuses are mapped to contacts then only OL is evaluated. + */ + if (sigar[OL_T].addr != NOTUSED) { + rval = get_signal_state(OL_T); + upsdebugx(2, "OL value: %d", rval); + if (rval == -1) { + errcnt++; + } else if (rval == (1 ^ sigar[OL_T].noro)) { + status_set("OL"); + online = 1; + } else { + status_set("OB"); + online = 0; + /* if DISCHRG state is not mapped to a contact and UPS is on + batteries set status to DISCHRG state */ + if (sigar[DISCHRG_T].addr == NOTUSED) { + status_set("DISCHRG"); + dstate_setinfo("battery.charger.status", "discharging"); + } + + } + } else if (sigar[OB_T].addr != NOTUSED) { + rval = get_signal_state(OB_T); + upsdebugx(2, "OB value: %d", rval); + if (rval == -1) { + errcnt++; + } else if (rval == (1 ^ sigar[OB_T].noro)) { + status_set("OB"); + online = 0; + if (sigar[DISCHRG_T].addr == NOTUSED) { + status_set("DISCHRG"); + dstate_setinfo("battery.charger.status", "discharging"); + } + } else { + status_set("OL"); + online = 1; + } + } + + /* + * update UPS status regarding CHARGING state via HB. HB is usually + * mapped to "ready" contact when closed indicates a charging state > 85% + */ + if (sigar[HB_T].addr != NOTUSED) { + rval = get_signal_state(HB_T); + upsdebugx(2, "HB value: %d", rval); + if (rval == -1) { + errcnt++; + } else if (rval == (1 ^ sigar[HB_T].noro)) { + status_set("HB"); + dstate_setinfo("battery.charger.status", "resting"); + } else if (online == 1 && sigar[CHRG_T].addr == NOTUSED && errcnt == 0) { + status_set("CHRG"); + dstate_setinfo("battery.charger.status", "charging"); + } else if (online == 0 && sigar[DISCHRG_T].addr == NOTUSED && errcnt == 0) { + status_set("DISCHRG"); + dstate_setinfo("battery.charger.status", "discharging"); + } + } + + /* + * update UPS status regarding DISCHARGING state via LB. LB is mapped + * to "battery low" contact. + */ + if (sigar[LB_T].addr != NOTUSED) { + rval = get_signal_state(LB_T); + upsdebugx(2, "LB value: %d", rval); + if (rval == -1) { + errcnt++; + } else if (rval == (1 ^ sigar[LB_T].noro)) { + status_set("LB"); + alarm_set("Low Battery (Charge)"); + } + } + + /* + * update UPS status regarding battery HEALTH state via RB. RB is mapped + * to "replace battery" contact + */ + if (sigar[RB_T].addr != NOTUSED) { + rval = get_signal_state(RB_T); + upsdebugx(2, "RB value: %d", rval); + if (rval == -1) { + errcnt++; + } else if (rval == (1 ^ sigar[RB_T].noro)) { + status_set("RB"); + alarm_set("Replace Battery"); + } + } + + /* + * update UPS status regarding battery HEALTH state via RB. RB is mapped + * to "replace battery" contact + */ + if (sigar[CHRG_T].addr != NOTUSED) { + rval = get_signal_state(CHRG_T); + upsdebugx(2, "CHRG value: %d", rval); + if (rval == -1) { + errcnt++; + } else if (rval == (1 ^ sigar[CHRG_T].noro)) { + status_set("CHRG"); + dstate_setinfo("battery.charger.status", "charging"); + } + } else if (sigar[DISCHRG_T].addr != NOTUSED) { + rval = get_signal_state(DISCHRG_T); + upsdebugx(2, "DISCHRG value: %d", rval); + if (rval == -1) { + errcnt++; + } else if (rval == (1 ^ sigar[DISCHRG_T].noro)) { + status_set("DISCHRG"); + dstate_setinfo("battery.charger.status", "discharging"); + } + } + + /* check for communication errors */ + if (errcnt == 0) { + alarm_commit(); + status_commit(); + dstate_dataok(); + } else { + upsdebugx(2,"Communication errors: %d", errcnt); + dstate_datastale(); + } +} + +/* shutdown UPS */ +void upsdrv_shutdown(void) +{ + int rval; + int cnt = FSD_REPEAT_CNT; /* shutdown repeat counter */ + struct timeval start; + long etime; + + /* retry sending shutdown command on error */ + while ((rval = upscmd("load.off", NULL)) != STAT_INSTCMD_HANDLED && cnt > 0) { + rval = gettimeofday(&start, NULL); + if (rval < 0) { + upslogx(LOG_ERR, "upscmd: gettimeofday: %s", strerror(errno)); + } + + /* wait for an increasing time interval before sending shutdown command */ + while ((etime = time_elapsed(&start)) < ( FSD_REPEAT_INTRV / cnt)); + upsdebugx(2,"ERROR: load.off failed, wait for %lims, retries left: %d\n", etime, cnt - 1); + cnt--; + } + switch (rval) { + case STAT_INSTCMD_FAILED: + case STAT_INSTCMD_INVALID: + fatalx(EXIT_FAILURE, "shutdown failed"); + case STAT_INSTCMD_UNKNOWN: + fatalx(EXIT_FAILURE, "shutdown not supported"); + default: + break; + } + upslogx(LOG_INFO, "shutdown command executed"); +} + +/* print driver usage info */ +void upsdrv_help(void) +{ +} + +/* list flags and values that you want to receive via -x */ +void upsdrv_makevartable(void) +{ + addvar(VAR_VALUE, "device_mfr", "device manufacturer"); + addvar(VAR_VALUE, "device_model", "device model"); + addvar(VAR_VALUE, "ser_baud_rate", "serial port baud rate"); + addvar(VAR_VALUE, "ser_parity", "serial port parity"); + addvar(VAR_VALUE, "ser_data_bit", "serial port data bit"); + addvar(VAR_VALUE, "ser_stop_bit", "serial port stop bit"); + addvar(VAR_VALUE, "rio_slave_id", "RIO modbus slave ID"); + addvar(VAR_VALUE, "OL_addr", "modbus address for OL state"); + addvar(VAR_VALUE, "OB_addr", "modbus address for OB state"); + addvar(VAR_VALUE, "LB_addr", "modbus address for LB state"); + addvar(VAR_VALUE, "HB_addr", "modbus address for HB state"); + addvar(VAR_VALUE, "RB_addr", "modbus address for RB state"); + addvar(VAR_VALUE, "CHRG_addr", "modbus address for CHRG state"); + addvar(VAR_VALUE, "DISCHRG_addr", "modbus address for DISCHRG state"); + addvar(VAR_VALUE, "FSD_addr", "modbus address for FSD command"); + addvar(VAR_VALUE, "OL_regtype", "modbus register type for OL state"); + addvar(VAR_VALUE, "OB_regtype", "modbus register type for OB state"); + addvar(VAR_VALUE, "LB_regtype", "modbus register type for LB state"); + addvar(VAR_VALUE, "HB_regtype", "modbus register type for HB state"); + addvar(VAR_VALUE, "RB_regtype", "modbus register type for RB state"); + addvar(VAR_VALUE, "CHRG_regtype", "modbus register type for CHRG state"); + addvar(VAR_VALUE, "DISCHRG_regtype", "modbus register type for DISCHRG state"); + addvar(VAR_VALUE, "FSD_regtype", "modbus register type for FSD command"); + addvar(VAR_VALUE, "OL_noro", "NO/NC configuration for OL state"); + addvar(VAR_VALUE, "OB_noro", "NO/NC configuration for OB state"); + addvar(VAR_VALUE, "LB_noro", "NO/NC configuration for LB state"); + addvar(VAR_VALUE, "HB_noro", "NO/NC configuration for HB state"); + addvar(VAR_VALUE, "RB_noro", "NO/NC configuration for RB state"); + addvar(VAR_VALUE, "CHRG_noro", "NO/NC configuration for CHRG state"); + addvar(VAR_VALUE, "DISCHRG_noro", "NO/NC configuration for DISCHRG state"); + addvar(VAR_VALUE, "FSD_noro", "NO/NC configuration for FSD state"); + addvar(VAR_VALUE, "FSD_pulse_duration", "FSD pulse duration"); +} + +/* close modbus connection and free modbus context allocated memory */ +void upsdrv_cleanup(void) +{ + if (mbctx != NULL) { + modbus_close(mbctx); + modbus_free(mbctx); + } +} + +/* + * driver support functions + */ + +/* Read a modbus register */ +int register_read(modbus_t *mb, int addr, regtype_t type, void *data) +{ + int rval = -1; + + /* register bit masks */ + uint mask8 = 0x000F; + uint mask16 = 0x00FF; + + switch (type) { + case COIL: + rval = modbus_read_bits(mb, addr, 1, (uint8_t *)data); + *(uint *)data = *(uint *)data & mask8; + break; + case INPUT_B: + rval = modbus_read_input_bits(mb, addr, 1, (uint8_t *)data); + *(uint *)data = *(uint *)data & mask8; + break; + case INPUT_R: + rval = modbus_read_input_registers(mb, addr, 1, (uint16_t *)data); + *(uint *)data = *(uint *)data & mask16; + break; + case HOLDING: + rval = modbus_read_registers(mb, addr, 1, (uint16_t *)data); + *(uint *)data = *(uint *)data & mask16; + break; + default: + upsdebugx(2,"ERROR: register_read: invalid register type %d\n", type); + break; + } + if (rval == -1) { + upslogx(LOG_ERR,"ERROR:(%s) modbus_read: addr:0x%x, type:%8s, path:%s\n", + modbus_strerror(errno), + addr, + (type == COIL) ? "COIL" : + (type == INPUT_B) ? "INPUT_B" : + (type == INPUT_R) ? "INPUT_R" : "HOLDING", + device_path + ); + } + upsdebugx(3, "register addr: 0x%x, register type: %d read: %d",addr, type, *(uint *)data); + return rval; +} + +/* write a modbus register */ +int register_write(modbus_t *mb, int addr, regtype_t type, void *data) +{ + int rval = -1; + + /* register bit masks */ + uint mask8 = 0x000F; + uint mask16 = 0x00FF; + + switch (type) { + case COIL: + *(uint *)data = *(uint *)data & mask8; + rval = modbus_write_bit(mb, addr, *(uint8_t *)data); + break; + case HOLDING: + *(uint *)data = *(uint *)data & mask16; + rval = modbus_write_register(mb, addr, *(uint16_t *)data); + break; + default: + upsdebugx(2,"ERROR: register_write: invalid register type %d\n", type); + break; + } + if (rval == -1) { + upslogx(LOG_ERR,"ERROR:(%s) modbus_read: addr:0x%x, type:%8s, path:%s\n", + modbus_strerror(errno), + addr, + (type == COIL) ? "COIL" : + (type == INPUT_B) ? "INPUT_B" : + (type == INPUT_R) ? "INPUT_R" : "HOLDING", + device_path + ); + } + upsdebugx(3, "register addr: 0x%x, register type: %d read: %d",addr, type, *(uint *)data); + return rval; +} + +/* returns the time elapsed since start in milliseconds */ +long time_elapsed(struct timeval *start) +{ + long rval; + struct timeval end; + + rval = gettimeofday(&end, NULL); + if (rval < 0) { + upslogx(LOG_ERR, "time_elapsed: %s", strerror(errno)); + } + if (start->tv_usec < end.tv_usec) { + uint32_t nsec = (end.tv_usec - start->tv_usec) / 1000000 + 1; + end.tv_usec -= 1000000 * nsec; + end.tv_sec += nsec; + } + if (start->tv_usec - end.tv_usec > 1000000) { + uint32_t nsec = (start->tv_usec - end.tv_usec) / 1000000; + end.tv_usec += 1000000 * nsec; + end.tv_sec -= nsec; + } + rval = (end.tv_sec - start->tv_sec) * 1000 + (end.tv_usec - start->tv_usec) / 1000; + + return rval; +} + +/* instant command triggered by upsd */ +int upscmd(const char *cmd, const char *arg) +{ + int rval; + int data; + struct timeval start; + long etime; + + if (!strcasecmp(cmd, "load.off")) { + if (sigar[FSD_T].addr != NOTUSED && + (sigar[FSD_T].type == COIL || sigar[FSD_T].type == HOLDING)) { + data = 1 ^ sigar[FSD_T].noro; + rval = register_write(mbctx, sigar[FSD_T].addr, sigar[FSD_T].type, &data); + if (rval == -1) { + upslogx(2,"ERROR:(%s) modbus_write_register: addr:0x%08x, regtype: %d, path:%s\n", + modbus_strerror(errno), + sigar[FSD_T].addr, + sigar[FSD_T].type, + device_path + ); + upslogx(LOG_NOTICE, "load.off: failed (communication error) [%s] [%s]", cmd, arg); + rval = STAT_INSTCMD_FAILED; + } else { + upsdebugx(2, "load.off: addr: 0x%x, data: %d", sigar[FSD_T].addr, data); + rval = STAT_INSTCMD_HANDLED; + } + + /* if pulse has been defined and rising edge was successful */ + if (FSD_pulse_duration != NOTUSED && rval == STAT_INSTCMD_HANDLED) { + rval = gettimeofday(&start, NULL); + if (rval < 0) { + upslogx(LOG_ERR, "upscmd: gettimeofday: %s", strerror(errno)); + } + + /* wait for FSD_pulse_duration ms */ + while ((etime = time_elapsed(&start)) < FSD_pulse_duration); + + data = 0 ^ sigar[FSD_T].noro; + rval = register_write(mbctx, sigar[FSD_T].addr, sigar[FSD_T].type, &data); + if (rval == -1) { + upslogx(LOG_ERR, "ERROR:(%s) modbus_write_register: addr:0x%08x, regtype: %d, path:%s\n", + modbus_strerror(errno), + sigar[FSD_T].addr, + sigar[FSD_T].type, + device_path + ); + upslogx(LOG_NOTICE, "load.off: failed (communication error) [%s] [%s]", cmd, arg); + rval = STAT_INSTCMD_FAILED; + } else { + upsdebugx(2, "load.off: addr: 0x%x, data: %d, elapsed time: %lims", + sigar[FSD_T].addr, + data, + etime + ); + rval = STAT_INSTCMD_HANDLED; + } + } + } else { + upslogx(LOG_NOTICE,"load.off: failed (FSD address undefined or invalid register type) [%s] [%s]", + cmd, + arg + ); + rval = STAT_INSTCMD_FAILED; + } + } else { + upslogx(LOG_NOTICE, "instcmd: unknown command [%s] [%s]", cmd, arg); + rval = STAT_INSTCMD_UNKNOWN; + } + return rval; +} + +/* read signal state from modbus RIO, returns 0|1 state or -1 on communication error */ +int get_signal_state(devstate_t state) +{ + int rval = -1; + int reg_val; + regtype_t rtype = 0; /* register type */ + int addr = -1; /* register address */ + + /* assign register address and type */ + switch (state) { + case OL_T: + addr = sigar[OL_T].addr; + rtype = sigar[OL_T].type; + break; + case OB_T: + addr = sigar[OB_T].addr; + rtype = sigar[OB_T].type; + break; + case LB_T: + addr = sigar[LB_T].addr; + rtype = sigar[LB_T].type; + break; + case HB_T: + addr = sigar[HB_T].addr; + rtype = sigar[HB_T].type; + break; + case RB_T: + addr = sigar[RB_T].addr; + rtype = sigar[RB_T].type; + break; + case CHRG_T: + addr = sigar[CHRG_T].addr; + rtype = sigar[CHRG_T].type; + break; + case DISCHRG_T: + addr = sigar[DISCHRG_T].addr; + rtype = sigar[DISCHRG_T].type; + break; + default: + break; + } + + rval = register_read(mbctx, addr, rtype, ®_val); + if (rval > -1) { + rval = reg_val; + } + upsdebugx(3, "get_signal_state: state: %d", reg_val); + return rval; +} + +/* get driver configuration parameters */ +void get_config_vars() +{ + int i; /* local index */ + + /* initialize sigar table */ + for (i = 0; i < NUMOF_SIG_STATES; i++) { + sigar[i].addr = NOTUSED; + sigar[i].noro = 0; /* ON corresponds to 1 (closed contact) */ + } + + /* check if device manufacturer is set ang get the value */ + if (testvar("device_mfr")) { + device_mfr = getval("device_mfr"); + } + upsdebugx(2, "device_mfr %s", device_mfr); + + /* check if device model is set ang get the value */ + if (testvar("device_model")) { + device_model = getval("device_model"); + } + upsdebugx(2, "device_model %s", device_model); + + /* check if serial baud rate is set ang get the value */ + if (testvar("ser_baud_rate")) { + ser_baud_rate = (int )strtol(getval("ser_baud_rate"), NULL, 10); + } + upsdebugx(2, "ser_baud_rate %d", ser_baud_rate); + + /* check if serial parity is set ang get the value */ + if (testvar("ser_parity")) { + ser_parity = *(int *)getval("ser_parity"); + } + upsdebugx(2, "ser_parity %c", ser_parity); + + /* check if serial data bit is set ang get the value */ + if (testvar("ser_data_bit")) { + ser_data_bit = (int )strtol(getval("ser_data_bit"), NULL, 10); + } + upsdebugx(2, "ser_data_bit %d", ser_data_bit); + + /* check if serial stop bit is set ang get the value */ + if (testvar("ser_stop_bit")) { + ser_stop_bit = (int )strtol(getval("ser_stop_bit"), NULL, 10); + } + upsdebugx(2, "ser_stop_bit %d", ser_stop_bit); + + /* check if device ID is set ang get the value */ + if (testvar("rio_slave_id")) { + rio_slave_id = (int )strtol(getval("rio_slave_id"), NULL, 10); + } + upsdebugx(2, "rio_slave_id %d", rio_slave_id); + + /* check if OL address is set and get the value */ + if (testvar("OL_addr")) { + sigar[OL_T].addr = (int )strtol(getval("OL_addr"), NULL, 0); + if (testvar("OL_noro")) { + sigar[OL_T].noro = (int )strtol(getval("OL_noro"), NULL, 10); + if (sigar[OL_T].noro != 1) { + sigar[OL_T].noro = 0; + } + } + } + /* check if OL register type is set and get the value otherwise set to INPUT_B */ + if (testvar("OL_regtype")) { + sigar[OL_T].type = (int )strtol(getval("OL_regtype"), NULL, 10); + if (sigar[OL_T].type < COIL || sigar[OL_T].type > HOLDING) { + sigar[OL_T].type = INPUT_B; + } + } else { + sigar[OL_T].type = INPUT_B; + } + + /* check if OB address is set and get the value */ + if (testvar("OB_addr")) { + sigar[OB_T].addr = (int )strtol(getval("OB_addr"), NULL, 0); + } + if (testvar("OB_noro")) { + sigar[OB_T].noro = (int )strtol(getval("OB_noro"), NULL, 10); + if (sigar[OB_T].noro != 1) { + sigar[OB_T].noro = 0; + } + } + /* check if OB register type is set and get the value otherwise set to INPUT_B */ + if (testvar("OB_regtype")) { + sigar[OB_T].type = (int )strtol(getval("OB_regtype"), NULL, 10); + if (sigar[OB_T].type < COIL || sigar[OB_T].type > HOLDING) { + sigar[OB_T].type = INPUT_B; + } + } else { + sigar[OB_T].type = INPUT_B; + } + + /* check if LB address is set and get the value */ + if (testvar("LB_addr")) { + sigar[LB_T].addr = (int )strtol(getval("LB_addr"), NULL, 0); + if (testvar("LB_noro")) { + sigar[LB_T].noro = (int )strtol(getval("LB_noro"), NULL, 10); + if (sigar[LB_T].noro != 1) { + sigar[LB_T].noro = 0; + } + } + } + /* check if LB register type is set and get the value otherwise set to INPUT_B */ + if (testvar("LB_regtype")) { + sigar[LB_T].type = (int )strtol(getval("OB_regtype"), NULL, 10); + if (sigar[LB_T].type < COIL || sigar[LB_T].type > HOLDING) { + sigar[LB_T].type = INPUT_B; + } + } else { + sigar[LB_T].type = INPUT_B; + } + + /* check if HB address is set and get the value */ + if (testvar("HB_addr")) { + sigar[HB_T].addr = (int )strtol(getval("HB_addr"), NULL, 0); + if (testvar("HB_noro")) { + sigar[HB_T].noro = (int )strtol(getval("HB_noro"), NULL, 10); + if (sigar[HB_T].noro != 1) { + sigar[HB_T].noro = 0; + } + } + } + /* check if HB register type is set and get the value otherwise set to INPUT_B */ + if (testvar("HB_regtype")) { + sigar[HB_T].type = (int )strtol(getval("HB_regtype"), NULL, 10); + if (sigar[HB_T].type < COIL || sigar[HB_T].type > HOLDING) { + sigar[HB_T].type = INPUT_B; + } + } else { + sigar[HB_T].type = INPUT_B; + } + + /* check if RB address is set and get the value */ + if (testvar("RB_addr")) { + sigar[RB_T].addr = (int )strtol(getval("RB_addr"), NULL, 0); + if (testvar("RB_noro")) { + sigar[RB_T].noro = (int )strtol(getval("RB_noro"), NULL, 10); + if (sigar[RB_T].noro != 1) { + sigar[RB_T].noro = 0; + } + } + } + /* check if RB register type is set and get the value otherwise set to INPUT_B */ + if (testvar("RB_regtype")) { + sigar[RB_T].type = (int )strtol(getval("RB_regtype"), NULL, 10); + if (sigar[RB_T].type < COIL || sigar[RB_T].type > HOLDING) { + sigar[RB_T].type = INPUT_B; + } + } else { + sigar[RB_T].type = INPUT_B; + } + + /* check if CHRG address is set and get the value */ + if (testvar("CHRG_addr")) { + sigar[CHRG_T].addr = (int )strtol(getval("CHRG_addr"), NULL, 0); + if (testvar("CHRG_noro")) { + sigar[CHRG_T].noro = (int )strtol(getval("CHRG_noro"), NULL, 10); + if (sigar[CHRG_T].noro != 1) { + sigar[CHRG_T].noro = 0; + } + } + } + /* check if CHRG register type is set and get the value otherwise set to INPUT_B */ + if (testvar("CHRG_regtype")) { + sigar[CHRG_T].type = (int )strtol(getval("CHRG_regtype"), NULL, 10); + if (sigar[CHRG_T].type < COIL || sigar[CHRG_T].type > HOLDING) { + sigar[CHRG_T].type = INPUT_B; + } + } else { + sigar[CHRG_T].type = INPUT_B; + } + + /* check if DISCHRG address is set and get the value */ + if (testvar("DISCHRG_addr")) { + sigar[DISCHRG_T].addr = (int )strtol(getval("DISCHRG_addr"), NULL, 0); + if (testvar("DISCHRG_noro")) { + sigar[DISCHRG_T].noro = (int )strtol(getval("DISCHRG_noro"), NULL, 10); + if (sigar[DISCHRG_T].noro != 1) { + sigar[DISCHRG_T].noro = 0; + } + } + } + /* check if DISCHRG register type is set and get the value otherwise set to INPUT_B */ + if (testvar("DISCHRG_regtype")) { + sigar[DISCHRG_T].type = (int )strtol(getval("DISCHRG_regtype"), NULL, 10); + if (sigar[DISCHRG_T].type < COIL || sigar[DISCHRG_T].type > HOLDING) { + sigar[DISCHRG_T].type = INPUT_B; + } + } else { + sigar[DISCHRG_T].type = INPUT_B; + } + + /* check if FSD address is set and get the value */ + if (testvar("FSD_addr")) { + sigar[FSD_T].addr = (int )strtol(getval("FSD_addr"), NULL, 0); + if (testvar("FSD_noro")) { + sigar[FSD_T].noro = (int )strtol(getval("FSD_noro"), NULL, 10); + if (sigar[FSD_T].noro != 1) { + sigar[FSD_T].noro = 0; + } + } + } + /* check if FSD register type is set and get the value otherwise set to COIL */ + if (testvar("FSD_regtype")) { + sigar[FSD_T].type = (int )strtol(getval("FSD_regtype"), NULL, 10); + if (sigar[FSD_T].type < COIL || sigar[FSD_T].type > HOLDING) { + sigar[FSD_T].type = COIL; + } + } else { + sigar[FSD_T].type = COIL; + } + + /* check if FSD pulse duration is set and get the value */ + if (testvar("FSD_pulse_duration")) { + FSD_pulse_duration = (int) strtol(getval("FSD_pulse_duration"), NULL, 10); + } + upsdebugx(2, "FSD_pulse_duration %d", FSD_pulse_duration); + + /* debug loop over signal array */ + for (i = 0; i < NUMOF_SIG_STATES; i++) { + if (sigar[i].addr != NOTUSED) { + char *signame; + switch (i) { + case OL_T: + signame = "OL"; + break; + case OB_T: + signame = "OB"; + break; + case LB_T: + signame = "LB"; + break; + case HB_T: + signame = "HB"; + break; + case RB_T: + signame = "RB"; + break; + case FSD_T: + signame = "FSD"; + break; + case CHRG_T: + signame = "CHRG"; + break; + case DISCHRG_T: + signame = "DISCHRG"; + break; + default: + signame = "NOTUSED"; + break; + } + upsdebugx(2, "%s, addr:0x%x, type:%d", signame, sigar[i].addr, sigar[i].type); + } + } +} + +/* create a new modbus context based on connection type (serial or TCP) */ +modbus_t *modbus_new(const char *port) +{ + modbus_t *mb; + char *sp; + if (strstr(port, "/dev/tty") != NULL) { + mb = modbus_new_rtu(port, ser_baud_rate, (char )ser_parity, ser_data_bit, ser_stop_bit); + if (mb == NULL) { + upslogx(LOG_ERR, "modbus_new_rtu: Unable to open serial port context\n"); + } + } else if ((sp = strchr(port, ':')) != NULL) { + char *tcp_port = xmalloc(sizeof(sp)); + strcpy(tcp_port, sp + 1); + *sp = '\0'; + mb = modbus_new_tcp(port, (int )strtoul(tcp_port, NULL, 10)); + if (mb == NULL) { + upslogx(LOG_ERR, "modbus_new_tcp: Unable to connect to %s\n", port); + } + free(tcp_port); + } else { + mb = modbus_new_tcp(port, 502); + if (mb == NULL) { + upslogx(LOG_ERR, "modbus_new_tcp: Unable to connect to %s\n", port); + } + } + return mb; +} \ No newline at end of file diff --git a/drivers/generic_modbus.h b/drivers/generic_modbus.h new file mode 100644 index 0000000000..168c9de8b4 --- /dev/null +++ b/drivers/generic_modbus.h @@ -0,0 +1,101 @@ +/* generic_modbus.h - Driver for generic UPS connected via modbus RIO + * + * Copyright (C) + * 2021 Dimitris Economou + * + * 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. + * + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef NUT_GENERIC_MODBUS_H +#define NUT_GENERIC_MODBUS_H + +/* UPS device details */ +#define DEVICE_MFR "UNKNOWN" +#define DEVICE_MODEL "unknown" + +/* serial access parameters */ +#define BAUD_RATE 9600 +#define PARITY 'N' +#define DATA_BIT 8 +#define STOP_BIT 1 + +/* modbus access parameters */ +#define MODBUS_SLAVE_ID 5 + +/* shutdown repeat on error */ +#define FSD_REPEAT_CNT 3 + +/* shutdown repeat interval in ms */ +#define FSD_REPEAT_INTRV 1500 + +/* definition of register type */ +enum regtype { + COIL = 0, + INPUT_B, + INPUT_R, + HOLDING +}; +typedef enum regtype regtype_t; + +/* UPS device state enum */ +enum devstate { + OL_T = 0, + OB_T, + LB_T, + HB_T, + RB_T, + CHRG_T, + DISCHRG_T, + BYPASS_T, + CAL_T, + OFF_T, + OVER_T, + TRIM_T, + BOOST_T, + FSD_T +}; +typedef enum devstate devstate_t; + +/* UPS state signal attributes */ +struct sigattr { + int addr; /* register address */ + regtype_t type; /* register type */ + int noro; /* 1: normally open contact 0: normally closed contact. + noro is used to indicate the logical ON or OFF in regard + of the contact state. if noro is set to 1 then ON corresponds + to an open contact */ +}; +typedef struct sigattr sigattr_t; + +#define NUMOF_SIG_STATES 14 +#define NOTUSED -1 + +/* define the duration of the shutdown pulse */ +#define SHTDOWN_PULSE_DURATION NOTUSED + +/* + * associate PULS signals to NUT device states + * + * Ready contact <--> 1:HB, 0:CHRG + * Buffering contact <--> 1:OB, 0:OL + * Battery-low <--> 1:LB + * Replace Battery <--> 1:RB + * Inhibit buffering <--> 1:FSD + */ + + +#endif //NUT_GENERIC_MODBUS_H