/*  generic_modbus.c - Driver for generic UPS connected via modbus RIO
 *
 *  Copyright (C)
 *    2021 Dimitris Economou <dimitris.s.economou@gmail.com>
 *
 *  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 <modbus.h>
#include <timehead.h>

#define DRIVER_NAME "NUT Generic Modbus driver"
#define DRIVER_VERSION  "0.03"

/* variables */
static modbus_t *mbctx = NULL;                             /* modbus memory context */
static sigattr_t sigar[NUMOF_SIG_STATES];                  /* array of ups signal attributes */
static int errcnt = 0;                                     /* modbus access error counter */

static char *device_mfr = DEVICE_MFR;                      /* device manufacturer */
static char *device_model = DEVICE_MODEL;                  /* device model */
static int ser_baud_rate = BAUD_RATE;                      /* serial port baud rate */
static char ser_parity = PARITY;                           /* serial port parity */
static int ser_data_bit = DATA_BIT;                        /* serial port data bit */
static int ser_stop_bit = STOP_BIT;                        /* serial port stop bit */
static int rio_slave_id = MODBUS_SLAVE_ID;                 /* set device ID to default value */
static int FSD_pulse_duration = SHTDOWN_PULSE_DURATION;    /* set the FSD pulse duration */
static uint32_t mod_resp_to_s = MODRESP_TIMEOUT_s;         /* set the modbus response time out (s) */
static uint32_t mod_resp_to_us = MODRESP_TIMEOUT_us;       /* set the modbus response time out (us) */
static uint32_t mod_byte_to_s = MODBYTE_TIMEOUT_s;         /* set the modbus byte time out (us) */
static uint32_t mod_byte_to_us = MODBYTE_TIMEOUT_us;       /* set the modbus byte time out (us) */


/* get config vars set by -x or defined in ups.conf driver section */
void get_config_vars(void);

/* create a new modbus context based on connection type (serial or TCP) */
modbus_t *modbus_new(const char *port);

/* reconnect upon communication error */
void modbus_reconnect(void);

/* 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);

int register_write(modbus_t *mb, int addr, regtype_t type, void *data);

/* driver description structure */
upsdrv_info_t upsdrv_info = {
	DRIVER_NAME,
	DRIVER_VERSION,
	"Dimitris Economou <dimitris.s.economou@gmail.com>\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 communication port */
	mbctx = modbus_new(device_path);
	if (mbctx == NULL) {
		fatalx(EXIT_FAILURE, "modbus_new_rtu: Unable to open communication port context");
	}

	/* set slave ID */
	rval = modbus_set_slave(mbctx, rio_slave_id);
	if (rval < 0) {
		modbus_free(mbctx);
		fatalx(EXIT_FAILURE, "modbus_set_slave: Invalid modbus slave ID %d", rio_slave_id);
	}

	/* connect to modbus device  */
	if (modbus_connect(mbctx) == -1) {
		modbus_free(mbctx);
		fatalx(EXIT_FAILURE, "modbus_connect: unable to connect: error(%s)", modbus_strerror(errno));
	}

	/* set modbus response timeout */
#if (defined NUT_MODBUS_TIMEOUT_ARG_sec_usec_uint32) || (defined NUT_MODBUS_TIMEOUT_ARG_sec_usec_uint32_cast_timeval_fields)
	rval = modbus_set_response_timeout(mbctx, mod_resp_to_s, mod_resp_to_us);
	if (rval < 0) {
		modbus_free(mbctx);
		fatalx(EXIT_FAILURE, "modbus_set_response_timeout: error(%s)", modbus_strerror(errno));
	}
#elif (defined NUT_MODBUS_TIMEOUT_ARG_timeval_numeric_fields)
	{
		/* Older libmodbus API (with timeval), and we have
		 * checked at configure time that we can put uint32_t
		 * into its fields. They are probably "long" on many
		 * systems as respectively time_t and suseconds_t -
		 * but that is not guaranteed; for more details see
		 * https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/sys_time.h.html
		 */
		struct timeval to;
		memset(&to, 0, sizeof(struct timeval));
		to.tv_sec = mod_resp_to_s;
		to.tv_usec = mod_resp_to_us;
		/* void */ modbus_set_response_timeout(mbctx, &to);
	}
/* #elif (defined NUT_MODBUS_TIMEOUT_ARG_timeval) // some un-castable type in fields */
#else
# error "Can not use libmodbus API for timeouts"
#endif /* NUT_MODBUS_TIMEOUT_ARG_* */

	/* set modbus byte time out */
#if (defined NUT_MODBUS_TIMEOUT_ARG_sec_usec_uint32) || (defined NUT_MODBUS_TIMEOUT_ARG_sec_usec_uint32_cast_timeval_fields)
	rval = modbus_set_byte_timeout(mbctx, mod_byte_to_s, mod_byte_to_us);
	if (rval < 0) {
		modbus_free(mbctx);
		fatalx(EXIT_FAILURE, "modbus_set_byte_timeout: error(%s)", modbus_strerror(errno));
	}
#elif (defined NUT_MODBUS_TIMEOUT_ARG_timeval_numeric_fields)
	{   /* see comments above */
		struct timeval to;
		memset(&to, 0, sizeof(struct timeval));
		to.tv_sec = mod_byte_to_s;
		to.tv_usec = mod_byte_to_us;
		/* void */ modbus_set_byte_timeout(mbctx, &to);
	}
/* #elif (defined NUT_MODBUS_TIMEOUT_ARG_timeval) // some un-castable type in fields */
#endif /* NUT_MODBUS_TIMEOUT_ARG_* */
}

/* 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, "mod_resp_to_s", "modbus response timeout (s)");
	addvar(VAR_VALUE, "mod_resp_to_us", "modbus response timeout (us)");
	addvar(VAR_VALUE, "mod_byte_to_s", "modbus byte timeout (s)");
	addvar(VAR_VALUE, "mod_byte_to_us", "modbus byte timeout (us)");
	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;

#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) )
# pragma GCC diagnostic push
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT
# pragma GCC diagnostic ignored "-Wcovered-switch-default"
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE
# pragma GCC diagnostic ignored "-Wunreachable-code"
#endif
/* Older CLANG (e.g. clang-3.4) seems to not support the GCC pragmas above */
#ifdef __clang__
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Wunreachable-code"
# pragma clang diagnostic ignored "-Wcovered-switch-default"
#endif
		/* All enum cases defined as of the time of coding
		 * have been covered above. Handle later definitions,
		 * memory corruptions and buggy inputs below...
		 */
		default:
			upsdebugx(2,"ERROR: register_read: invalid register type %d\n", type);
			break;
#ifdef __clang__
# pragma clang diagnostic pop
#endif
#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) )
# pragma GCC diagnostic pop
#endif
	}
	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
		);

		/* on BROKEN PIPE error try to reconnect */
		if (errno == EPIPE) {
			upsdebugx(2, "register_read: error(%s)", modbus_strerror(errno));
			modbus_reconnect();
		}
	}
	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;

		case INPUT_B:
		case INPUT_R:
#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT)
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wcovered-switch-default"
#endif
		/* All enum cases defined as of the time of coding
		 * have been covered above. Handle later definitions,
		 * memory corruptions and buggy inputs below...
		 */
		default:
#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT)
# pragma GCC diagnostic pop
#endif
			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
		);

		/* on BROKEN PIPE error try to reconnect */
		if (errno == EPIPE) {
			upsdebugx(2, "register_write: error(%s)", modbus_strerror(errno));
			modbus_reconnect();
		}
	}
	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) {
		suseconds_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) {
		suseconds_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;

		case BYPASS_T:
		case CAL_T:
		case FSD_T:
		case OFF_T:
		case OVER_T:
		case TRIM_T:
		case BOOST_T:
#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT)
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wcovered-switch-default"
#endif
		/* All enum cases defined as of the time of coding
		 * have been covered above. Handle later definitions,
		 * memory corruptions and buggy inputs below...
		 */
		default:
#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT)
# pragma GCC diagnostic pop
#endif
			break;
	}

	rval = register_read(mbctx, addr, rtype, &reg_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")) {
		/* Dereference the char* we get */
		char *sp = getval("ser_parity");
		if (sp) {
			/* TODO? Sanity-check the char we get? */
			ser_parity = *sp;
		} else {
			upsdebugx(2, "Could not determine ser_parity, will keep default");
		}
	}
	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 response time out (s) is set ang get the value */
	if (testvar("mod_resp_to_s")) {
		mod_resp_to_s = (uint32_t)strtol(getval("mod_resp_to_s"), NULL, 10);
	}
	upsdebugx(2, "mod_resp_to_s %d", mod_resp_to_s);

	/* check if response time out (us) is set ang get the value */
	if (testvar("mod_resp_to_us")) {
		mod_resp_to_us = (uint32_t) strtol(getval("mod_resp_to_us"), NULL, 10);
		if (mod_resp_to_us > 999999) {
			fatalx(EXIT_FAILURE, "get_config_vars: Invalid mod_resp_to_us %d", mod_resp_to_us);
		}
	}
	upsdebugx(2, "mod_resp_to_us %d", mod_resp_to_us);

	/* check if byte time out (s) is set ang get the value */
	if (testvar("mod_byte_to_s")) {
		mod_byte_to_s = (uint32_t)strtol(getval("mod_byte_to_s"), NULL, 10);
	}
	upsdebugx(2, "mod_byte_to_s %d", mod_byte_to_s);

	/* check if byte time out (us) is set ang get the value */
	if (testvar("mod_byte_to_us")) {
		mod_byte_to_us = (uint32_t) strtol(getval("mod_byte_to_us"), NULL, 10);
		if (mod_byte_to_us > 999999) {
			fatalx(EXIT_FAILURE, "get_config_vars: Invalid mod_byte_to_us %d", mod_byte_to_us);
		}
	}
	upsdebugx(2, "mod_byte_to_us %d", mod_byte_to_us);

	/* 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 = (unsigned 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 = (unsigned 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 = (unsigned 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 = (unsigned 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 = (unsigned 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 = (unsigned 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 = (unsigned 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 = (unsigned 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, 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;
}

/* reconnect to modbus server upon connection error */
void modbus_reconnect(void)
{
	int rval;

	upsdebugx(2, "modbus_reconnect, trying to reconnect to modbus server");

	/* clear current modbus context */
	modbus_close(mbctx);
	modbus_free(mbctx);

	/* open communication port */
	mbctx = modbus_new(device_path);
	if (mbctx == NULL) {
		fatalx(EXIT_FAILURE, "modbus_new_rtu: Unable to open communication port context");
	}

	/* set slave ID */
	rval = modbus_set_slave(mbctx, rio_slave_id);
	if (rval < 0) {
		modbus_free(mbctx);
		fatalx(EXIT_FAILURE, "modbus_set_slave: Invalid modbus slave ID %d", rio_slave_id);
	}

	/* connect to modbus device  */
	if (modbus_connect(mbctx) == -1) {
		modbus_free(mbctx);
		fatalx(EXIT_FAILURE, "modbus_connect: unable to connect: %s", modbus_strerror(errno));
	}

	/* set modbus response timeout */
#if (defined NUT_MODBUS_TIMEOUT_ARG_sec_usec_uint32) || (defined NUT_MODBUS_TIMEOUT_ARG_sec_usec_uint32_cast_timeval_fields)
	rval = modbus_set_response_timeout(mbctx, mod_resp_to_s, mod_resp_to_us);
	if (rval < 0) {
		modbus_free(mbctx);
		fatalx(EXIT_FAILURE, "modbus_set_response_timeout: error(%s)", modbus_strerror(errno));
	}
#elif (defined NUT_MODBUS_TIMEOUT_ARG_timeval_numeric_fields)
	{   /* see comments above */
		struct timeval to;
		memset(&to, 0, sizeof(struct timeval));
		to.tv_sec = mod_resp_to_s;
		to.tv_usec = mod_resp_to_us;
		/* void */ modbus_set_response_timeout(mbctx, &to);
	}
/* #elif (defined NUT_MODBUS_TIMEOUT_ARG_timeval) // some un-castable type in fields */
#endif /* NUT_MODBUS_TIMEOUT_ARG_* */

	/* set modbus byte timeout */
#if (defined NUT_MODBUS_TIMEOUT_ARG_sec_usec_uint32) || (defined NUT_MODBUS_TIMEOUT_ARG_sec_usec_uint32_cast_timeval_fields)
	rval = modbus_set_byte_timeout(mbctx, mod_byte_to_s, mod_byte_to_us);
	if (rval < 0) {
		modbus_free(mbctx);
		fatalx(EXIT_FAILURE, "modbus_set_byte_timeout: error(%s)", modbus_strerror(errno));
	}
#elif (defined NUT_MODBUS_TIMEOUT_ARG_timeval_numeric_fields)
	{   /* see comments above */
		struct timeval to;
		memset(&to, 0, sizeof(struct timeval));
		to.tv_sec = mod_byte_to_s;
		to.tv_usec = mod_byte_to_us;
		/* void */ modbus_set_byte_timeout(mbctx, &to);
	}
/* #elif (defined NUT_MODBUS_TIMEOUT_ARG_timeval) // some un-castable type in fields */
#endif /* NUT_MODBUS_TIMEOUT_ARG_* */
}
