/*
 * al175.c - NUT support for Eltek AL175 alarm module.
 *           AL175 shall be in COMLI mode.
 *
 * Copyright (C) 2004-2013 Marine & Bridge Navigation Systems <http://mns.spb.ru>
 * Author: Kirill Smelkov <kirr@mns.spb.ru>
 *
 * 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
 */


/*
 * - NOTE the following document is referenced in this driver:
 *
 *	TE-36862-B4 "COMLI COMMUNICATION PROTOCOL IMPLEMENTED IN PRS SYSTEMS",
 *	by Eltek A/S
 *
 *
 * - AL175 debug levels:
 *
 *	1 user-level trace (status, instcmd, etc...)
 * 	2 status decode errors
 * 	3 COMLI proto handling errors
 * 	4 raw IO trace
 *
 */

#include "main.h"
#include "serial.h"
#include "timehead.h"

#include <stddef.h>
#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

#include "nut_stdint.h"
typedef	uint8_t byte_t;


#define DRIVER_NAME	"Eltek AL175/COMLI driver"
#define DRIVER_VERSION	"0.18"

/* driver description structure */
upsdrv_info_t upsdrv_info = {
	DRIVER_NAME,
	DRIVER_VERSION,
	"Kirill Smelkov <kirr@mns.spb.ru>\n" \
	"Marine & Bridge Navigation Systems <http://mns.spb.ru>",
	DRV_EXPERIMENTAL,
	{ NULL }
};


#define	STX	0x02
#define	ETX	0x03
#define	ACK	0x06


/************
 * RAW DATA *
 ************/

/**
 * raw_data buffer representation
 */
typedef struct {
	byte_t     *buf;	/*!< the whole buffer address	*/
	size_t      buf_size;	/*!< the whole buffer size	*/

	byte_t     *begin;	/*!< begin of content		*/
	byte_t     *end;	/*!< one-past-end of content	*/
} raw_data_t;


/**
 * pseudo-alloca raw_data buffer  (alloca is not in POSIX)
 * @param  varp		ptr-to local raw_data_t variable to which to alloca
 * @param  buf_array	array allocated on stack which will be used as storage
 *			(must be auto-variable)
 * @return alloca'ed memory as raw_data
 *
 * Example:
 *
 *	raw_data_t ack;
 *	byte_t     ack_buf[8];
 *
 *	raw_alloc_onstack(&ack, ack_buf);
 */
#define raw_alloc_onstack(varp, buf_array) do {		\
	(varp)->buf		= &(buf_array)[0];	\
	(varp)->buf_size	= sizeof(buf_array);	\
							\
	(varp)->begin		= (varp)->buf;		\
	(varp)->end		= (varp)->buf;		\
} while (0)


/**
 * xmalloc raw buffer
 * @param  size	size in bytes
 * @return xmalloc'ed memory as raw_data
 */
static raw_data_t raw_xmalloc(size_t size)
{
	raw_data_t data;

	data.buf	= (byte_t *)xmalloc(size);
	data.buf_size	= size;

	data.begin	= data.buf;
	data.end	= data.buf;

	return data;
}

/**
 * free raw_data buffer
 * @param  buf	raw_data buffer to free
 */
static void raw_free(raw_data_t *buf)
{
	free(buf->buf);

	buf->buf      = NULL;
	buf->buf_size = 0;
	buf->begin    = NULL;
	buf->end      = NULL;
}


/***************************************************************************/

/***************
 * COMLI types *
 ***************/

/**
 * COMLI message header info
 * @see 1. INTRODUCTION
 */
typedef struct {
	int  id;	/*!< Id[1:2]		*/
	int  stamp;	/*!< Stamp[3]		*/
	int  type;	/*!< Mess Type[4]	*/
} msg_head_t;

/**
 * COMLI IO header info
 * @see 1. INTRODUCTION
 */
typedef struct {
	size_t  addr;	/*!< Addr[5:8]		*/
	size_t  len;	/*!< NOB[9:10]		*/
} io_head_t;

/**
 * maximum allowed io.len value
 */
#define	IO_LEN_MAX	0xff

/**
 * COMLI header info
 * @see 1. INTRODUCTION
 */
typedef struct {
	msg_head_t  msg;	/*!< message header [1:4]	*/
	io_head_t   io;		/*!< io header [5:10]		*/
} comli_head_t;



/******************
 * MISC UTILITIES *
 ******************/

/**
 * convert hex string to int
 * @param  head  input string
 * @param  len   string length
 * @return parsed value (>=0) if success, -1 on error
 */
static long from_hex(const byte_t *head, unsigned len)
{
	long val=0;

	while (len-- != 0) {
		int ch = *head;

		if (!isxdigit(ch))
			return -1;	/* wrong character	*/

		val *= 0x10;

		if (isdigit(ch)) {
			val += (ch-'0');
		}
		else {
			/* ch = toupper(ch)  without locale-related problems */
			if (ch < 'A')
				ch += 'A' - 'a';

			val += 0x0A + (ch-'A');
		}

		++head;
	}

	return val;
}

/**
 * compute checksum of a buffer
 * @see 10. CHECKSUM BCC
 * @param   buf     buffer address
 * @param   count   no. of bytes in the buffer
 * @return  computed checksum
 */
static byte_t compute_bcc(const byte_t *buf, size_t count)
{
	byte_t bcc=0;
	size_t i;

	for (i=0; i<count; ++i)
		bcc ^= buf[i];

	return bcc;
}


/**
 * reverse bits in a buffer bytes from right to left
 * @see 6. CODING AND DECODING OF REGISTER VALUES
 * @param   buf     buffer address
 * @param   count   no. of bytes in the buffer
 */
static void reverse_bits(byte_t *buf, size_t count)
{
	byte_t x;

	while (count!=0) {
		x = *buf;
		x = (byte_t)( (x & 0x80) >> 7 )
		  | (byte_t)( (x & 0x40) >> 5 )
		  | (byte_t)( (x & 0x20) >> 3 )
		  | (byte_t)( (x & 0x10) >> 1 )
		  | (byte_t)( (x & 0x08) << 1 )
		  | (byte_t)( (x & 0x04) << 3 )
		  | (byte_t)( (x & 0x02) << 5 )
		  | (byte_t)( (x & 0x01) << 7 );
		*buf = x;

		++buf;
		--count;
	}
}


/********************************************************************/

/*
 * communication basics
 *
 * ME	(Monitor Equipment)
 * PRS	(Power Rectifier System)  /think of it as of UPS in common speak/
 *
 * there are 2 types of transactions:
 *
 * 'ACTIVATE COMMAND'
 *	ME -> PRS		(al_prep_activate)
 *	ME <- PRS [ack]		(al_check_ack)
 *
 *
 * 'READ REGISTER'
 *	ME -> PRS		(al_prep_read_req)
 *	ME <- PRS [data]	(al_parse_reply)
 *
 */

/********************
 * COMLI primitives *
 ********************/


/************************
 * COMLI: OUTPUT FRAMES *
 ************************/

/**
 * prepare COMLI sentence
 * @see 1. INTRODUCTION
 * @param   dest    [out] where to put the result
 * @param   h       COMLI header info
 * @param   buf     data part of the sentence
 * @param   count   amount of data bytes in the sentence
 *
 * @note: the data are copied into the sentence "as-is", there is no conversion is done.
 *  if the caller wants to reverse bits it is necessary to call reverse_bits(...) prior
 *  to comli_prepare.
 */
static void comli_prepare(raw_data_t *dest, const comli_head_t *h, const void *buf, size_t count)
{
/*
 *    0     1   2      3       4     5     6     7     8       9    10    11 - - -     N-1    N
 * +-----+---------+-------+------+-------------------------+-----------+------------+-----+-----+
 * | STX | IDh IDl | Stamp | type | addr1 addr2 addr3 addr4 | NOBh NOBl | ...data... | ETX | BCC |
 * +-----+---------+-------+------+-------------------------+-----------+------------+-----+-----+
 *
 * ^                                                                                             ^
 * |                                                                                             |
 *begin                                                                                         end
 */
	byte_t *out = dest->begin;


	/* it's caller responsibility to allocate enough space.
	 * else it is a bug in the program */
	if ( (out+11+count+2) > (dest->buf + dest->buf_size) )
		fatalx(EXIT_FAILURE, "too small dest in comli_prepare\n");

	out[0] = STX;
	snprintf((char *)out+1, 10+1, "%02X%1i%1i%04zX%02zX",
		(unsigned int)h->msg.id, h->msg.stamp, h->msg.type, h->io.addr, h->io.len);

	memcpy(out+11, buf, count);
	reverse_bits(out+11, count);


	out[11+count] = ETX;
	out[12+count] = compute_bcc(out+1, 10+count+1);

	dest->end = dest->begin + (11+count+2);
}




/**
 * prepare AL175 read data request
 * @see 2. MESSAGE TYPE 2 (COMMAND SENT FROM MONITORING EQUIPMENT)
 * @param   dest    [out] where to put the result
 * @param   addr    start address of requested area
 * @param   count   no. of requested bytes
 */
static void al_prep_read_req(raw_data_t *dest, size_t addr, size_t count)
{
	comli_head_t h;

	h.msg.id	= 0x14;
	h.msg.stamp	= 1;
	h.msg.type	= 2;

	h.io.addr	= addr;
	h.io.len	= count;

	comli_prepare(dest, &h, NULL, 0);
}


/**
 * prepare AL175 activate command
 * @see 4. MESSAGE TYPE 0 (ACTIVATE COMMAND)
 * @param   dest    [out] where to put the result
 * @param   cmd     command type        [11]
 * @param   subcmd  command subtype     [12]
 * @param   pr1     first parameter     [13:14]
 * @param   pr2     second parameter    [15:16]
 * @param   pr3     third parameter     [17:18]
 */
static void al_prep_activate(raw_data_t *dest, byte_t cmd, byte_t subcmd, uint16_t pr1, uint16_t pr2, uint16_t pr3)
{
	comli_head_t h;
	char data[8+1];

	h.msg.id	= 0x14;
	h.msg.stamp	= 1;
	h.msg.type	= 0;

	h.io.addr	= 0x4500;
	h.io.len	= 8;

	/* NOTE: doc says we should use ASCII coding here, but the actual
	 *       values are > 0x80, so we use binary coding. And have to
	 *       make this "fit" into the char array required by snprintf */
	data[0] = (char)cmd;
	data[1] = (char)subcmd;

	/* FIXME? One CI testcase builder claims here that
	 *   warning: '%2X' directive output may be truncated writing
	 *   between 2 and 4 bytes into a region of size between 3 and 5
	 *   [-Wformat-truncation=]
	 * but none others do, and I can't figure out how it thinks so :/
	 *
	 * Per https://stackoverflow.com/questions/51534284/how-to-circumvent-format-truncation-warning-in-gcc
	 * https://www.mail-archive.com/gcc-bugs@gcc.gnu.org/msg521037.html
	 * and simlar googlable sources, this seems to be a bug-or-feature
	 * linked to non-zero optimization level and/or not checking for the
	 * return value (conveys runtime errors if any do happen).
	 */
	assert (pr1 <= UINT8_MAX);
	assert (pr2 <= UINT8_MAX);
	assert (pr3 <= UINT8_MAX);
	if (0 > snprintf(data+2, 6+1, "%2X%2X%2X", pr1, pr2, pr3)) {
		data[8] = '\0';
	}

	comli_prepare(dest, &h, data, 8);
}

/***********************
 * COMLI: INPUT FRAMES *
 ***********************/

/**
 * check COMLI frame for correct layout and bcc
 * @param  f	frame to check
 *
 * @return 0 (ok)  -1 (error)
 */
static int comli_check_frame(/*const*/ raw_data_t f)
{
	int bcc;
	byte_t *tail;

	if ( (f.end - f.begin) < 2 )
		return -1;

	if (*f.begin!=STX)
		return -1;

	tail = f.end - 2;
	if (tail <= f.begin)
		return -1;

	if (tail[0]!=ETX)
		return -1;

	bcc = compute_bcc(f.begin+1, (size_t)(f.end - f.begin) - 2 /*STX & BCC*/);
	if (bcc!= tail[1])
		return -1;

	return 0;
}


/**
 * parse reply header from PRS
 * @see 3. MESSAGE TYPE 0 (REPLY FROM PRS ON MESSAGE TYPE 2)
 *
 * @param  io			[out] parsed io_header
 * @param  raw_reply_head	[in] raw reply header from PRS
 * @return 0 (ok),  -1 (error)
 *
 * @see al_parse_reply
 */
static int al_parse_reply_head(io_head_t *io, const raw_data_t raw_reply_head)
{
/*
 *    0     1   2      3       4     5     6     7     8       9    10
 * +-----+---------+-------+------+-------------------------+-----------+-----------+
 * | STX | IDh IDl | Stamp | type | addr1 addr2 addr3 addr4 | NOBh NOBl | ......... |
 * +-----+---------+-------+------+-------------------------+-----------+-----------+
 *
 *       ^                                                              ^
 *       |                                                              |
 *     begin							       end
 */

	size_t io_addr, io_len;
	const byte_t *reply_head = raw_reply_head.begin - 1;

	if ( (raw_reply_head.end - raw_reply_head.begin) != 10)  {
		upsdebugx(3, "%s: wrong size\t(%i != 10)", __func__, (int)(raw_reply_head.end - raw_reply_head.begin));
		return -1;		/* wrong size	*/
	}

	if (reply_head[1]!='0' || reply_head[2]!='0')  {
		upsdebugx(3, "%s: wrong id\t('%c%c' != '00')", __func__, reply_head[1], reply_head[2]);
		return -1;		/* wrong id	*/
	}

	if (reply_head[3]!='1')  {
		upsdebugx(3, "%s: wrong stamp\t('%c' != '1')", __func__, reply_head[3]);
		return -1;		/* wrong stamp	*/
	}

	if (reply_head[4]!='0')  {
		upsdebugx(3, "%s: wrong type\t('%c' != '0')", __func__, reply_head[4]);
		return -1;		/* wrong type	*/
	}

	/* Avoid signed/unsigned implicit conversion warnings
	 * At least, when shuffling a signed long into unsigned long,
	 * don't have to worry about overflows */
	io_addr = (size_t)from_hex(&reply_head[5], 4);
	if (io_addr == -1UL)  {
		upsdebugx(3, "%s: invalid addr\t('%c%c%c%c')", __func__,
			reply_head[5], reply_head[6], reply_head[7], reply_head[8]);
		return -1;		/* wrong addr	*/
	}

	io_len = (size_t)from_hex(&reply_head[9], 2);
	if (io_len == -1UL)   {
		upsdebugx(3, "%s: invalid nob\t('%c%c')", __func__, reply_head[9], reply_head[10]);
		return -1;		/* wrong NOB	*/
	}

	if (io_len > IO_LEN_MAX) {
		upsdebugx(3, "nob too big\t(%" PRIuSIZE " > %i)", io_len, IO_LEN_MAX);
		return -1;		/* too much data claimed */
	}

	io->addr = io_addr;
	io->len  = io_len;


	return 0;
}


/**
 * parse reply from PRS
 * @see 3. MESSAGE TYPE 0 (REPLY FROM PRS ON MESSAGE TYPE 2)
 * @param  io_head	[out] parsed io_header
 * @param  io_buf	[in] [out] raw_data where to place incoming data (see ...data... below)
 * @param  raw_reply	raw reply from PRS to check
 * @return  0 (ok), -1 (error)
 *
 * @see al_parse_reply_head
 */
static int al_parse_reply(io_head_t *io_head, raw_data_t *io_buf, /*const*/ raw_data_t raw_reply)
{
/*
 *    0     1   2      3       4     5     6     7     8       9    10    11 - - -     N-1    N
 * +-----+---------+-------+------+-------------------------+-----------+------------+-----+-----+
 * | STX | IDh IDl | Stamp | type | addr1 addr2 addr3 addr4 | NOBh NOBl | ...data... | ETX | BCC |
 * +-----+---------+-------+------+-------------------------+-----------+------------+-----+-----+
 *
 *       ^                                                                           ^
 *       |                                                                           |
 *     begin                                                                        end
 */

	int	err;
	size_t	i, io_buf_len;
	const byte_t	*reply = NULL;

	/* 1: extract header and parse it */
	/*const*/ raw_data_t raw_reply_head = raw_reply;

	if (raw_reply_head.begin + 10 <= raw_reply_head.end)
		raw_reply_head.end = raw_reply_head.begin + 10;

	err = al_parse_reply_head(io_head, raw_reply_head);
	if (err==-1)
		return -1;


	/* 2: process data */
	reply = raw_reply.begin - 1;

	if ( (raw_reply.end - raw_reply.begin) != (ptrdiff_t)(10 + io_head->len))  {
		upsdebugx(3, "%s: corrupt sentence\t(%i != %" PRIuSIZE ")",
				__func__, (int)(raw_reply.end - raw_reply.begin), 10 + io_head->len);
		return -1;		/* corrupt sentence	*/
	}


	/* 3: extract the data */
	if (io_buf->buf_size < io_head->len)	{
		upsdebugx(3, "%s: too much data to fit in io_buf\t(%" PRIuSIZE " > %" PRIuSIZE ")",
				__func__, io_head->len, io_buf->buf_size);
		return -1;		/* too much data to fit in io_buf	*/
	}

	io_buf->begin = io_buf->buf;
	io_buf->end   = io_buf->begin;

	for (i=0; i<io_head->len; ++i)
		*(io_buf->end++) = reply[11+i];

	assert(io_buf->end - io_buf->begin >= 0);
	io_buf_len = (size_t)(io_buf->end - io_buf->begin);
	reverse_bits(io_buf->begin, io_buf_len );

	upsdebug_hex(3, "\t\t--> payload", io_buf->begin, io_buf_len);

	return 0;	/* all ok */
}


/**
 * check acknowledge from PRS
 * @see 5. ACKNOWLEDGE FROM PRS
 * @param   raw_ack  raw acknowledge from PRS to check
 * @return  0 on success, -1 on error
 */
static int al_check_ack(/*const*/ raw_data_t raw_ack)
{
/*
 *    0     1   2      3       4     5     6     7
 * +-----+---------+-------+------+-----+-----+-----+
 * | STX | IDh IDl | Stamp | type | ACK | ETX | BCC |
 * +-----+---------+-------+------+-----+-----+-----+
 *
 *       ^                              ^
 *       |                              |
 *     begin                           end
 */

	const byte_t *ack = raw_ack.begin - 1;

	if ( (raw_ack.end - raw_ack.begin) !=5)  {
		upsdebugx(3, "%s: wrong size\t(%i != 5)", __func__, (int)(raw_ack.end - raw_ack.begin));
		return -1;		/* wrong size	*/
	}

	if (ack[1]!='0' || ack[2]!='0')  {
		upsdebugx(3, "%s: wrong id\t('%c%c' != '00')", __func__, ack[1], ack[2]);
		return -1;		/* wrong id	*/
	}

	/* the following in not mandated. it is just said it will be
	 * "same as one received". but we always send '1' (0x31) as stamp
	 * (see 4. MESSAGE TYPE 0 (ACTIVATE COMMAND). Hence, stamp checking
	 * is hardcoded here.
	 */
	if (ack[3]!='1')  {
		upsdebugx(3, "%s: wrong stamp\t('%c' != '1')", __func__, ack[3]);
		return -1;		/* wrong stamp	*/
	}

	if (ack[4]!='1')  {
		upsdebugx(3, "%s: wrong type\t('%c' != '1')", __func__, ack[4]);
		return -1;		/* wrong type	*/
	}

	if (ack[5]!=ACK)  {
		upsdebugx(3, "%s: wrong ack\t(0x%02X != 0x%02X)", __func__, ack[5], (unsigned int)ACK);
		return -1;		/* wrong ack	*/
	}


	return 0;
}





/******************************************************************/


/**********
 * SERIAL *
 **********/

/* clear any flow control (copy from powercom.c) */
static void ser_disable_flow_control (void)
{
	struct termios	tio;
	tcflag_t	x;

	tcgetattr (upsfd, &tio);

	/* Clumsy rewrite of a one-liner
	 *   tio.c_iflag &= ~ (IXON | IXOFF);
	 * to avoid type conversion warnings */
	x = (IXON | IXOFF);
	tio.c_iflag &= ~ x;
	tio.c_cc[VSTART] = _POSIX_VDISABLE;
	tio.c_cc[VSTOP] = _POSIX_VDISABLE;

	upsdebugx(4, "Flow control disable");

	/* disable any flow control */
	tcsetattr(upsfd, TCSANOW, &tio);
}

static void flush_rx_queue(void)
{
	ser_flush_in(upsfd, "", /*verbose=*/nut_debug_level);
}

/**
 * transmit frame to PRS
 *
 * @param  dmsg   debug message prefix
 * @param  frame  the frame to tansmit
 * @return 0 (ok) -1 (error)
 */
static int tx(const char *dmsg, /*const*/ raw_data_t frame)
{
	ssize_t	err;
	size_t	frame_len;

	assert(frame.end - frame.begin >= 0);
	frame_len = (size_t)(frame.end - frame.begin);

	upsdebug_ascii(3, dmsg, frame.begin, frame_len);

	err = ser_send_buf(upsfd, frame.begin, frame_len );
	if (err==-1) {
		upslog_with_errno(LOG_ERR, "failed to send frame to PRS");
		return -1;
	}

	if (err != (ssize_t)frame_len) {
		upslogx(LOG_ERR, "sent incomplete frame to PRS");
		return -1;
	}

	return 0;
}

/***********
 * CHATTER *
 ***********/

static time_t T_io_begin;	/* start of current I/O transaction */
static int    T_io_timeout;	/* in seconds */

/* start new I/O transaction with maximum time limit */
static void io_new_transaction(int timeout)
{
	T_io_begin = time(NULL);
	T_io_timeout = timeout;
}

/**
 * get next character from input stream
 *
 * @param  ch   ptr-to where store result
 *
 * @return -1 (error)  0 (timeout)  >0 (got it)
 *
 */
static ssize_t get_char(char *ch)
{
	time_t now = time(NULL);
	long rx_timeout;

	rx_timeout = T_io_timeout - (now - T_io_begin);
	/* negative rx_timeout -> time already out */
	if (rx_timeout < 0)
		return 0;
	return ser_get_char(upsfd, ch, rx_timeout, 0);
}


/**
 * get next characters from input stream
 *
 * @param   buf ptr-to output buffer
 * @param   len buffer length
 *
 * @return -1 (error)  0 (timeout)  >0 (no. of characters actually read)
 *
 */
static ssize_t get_buf(byte_t *buf, size_t len)
{
	time_t now = time(NULL);
	long rx_timeout;

	rx_timeout = T_io_timeout - (now - T_io_begin);
	/* negative rx_timeout -> time already out */
	if (rx_timeout < 0)
		return 0;
	return ser_get_buf_len(upsfd, buf, len, rx_timeout, 0);
}

/**
 * scan incoming bytes for specific character
 *
 * @return 0 (got it) -1 (error)
 */
static int scan_for(char c)
{
	char in;
	ssize_t  err;

	while (1) {
		err = get_char(&in);
		if (err==-1 || err==0 /*timeout*/)
			return -1;

		if (in==c)
			break;
	}

	return 0;
}


/**
 * receive 'activate command' ACK from PRS
 *
 * @return 0 (ok) -1 (error)
 */
static int recv_command_ack(void)
{
	ssize_t	err;
	raw_data_t ack;
	byte_t     ack_buf[8];

	/* 1:  STX  */
	err = scan_for(STX);
	if (err==-1)
		return -1;


	raw_alloc_onstack(&ack, ack_buf);
	*(ack.end++) = STX;


	/* 2:  ID1 ID2 STAMP MSG_TYPE ACK ETX BCC */
	err = get_buf(ack.end, 7);
	if (err!=7)
		return -1;

	ack.end += 7;

	/* frame constructed - let's verify it */
	assert (ack.end - ack.begin >= 0);
	upsdebug_ascii(3, "rx (ack):\t\t", ack.begin, (size_t)(ack.end - ack.begin));

	/* generic layout */
	err = comli_check_frame(ack);
	if (err==-1)
		return -1;

	/* shrink frame */
	ack.begin += 1;
	ack.end   -= 2;

	return al_check_ack(ack);
}

/**
 * receive 'read register' data from PRS
 * @param  io		[out] io header of received data
 * @param  io_buf	[in] [out] where to place incoming data
 *
 * @return 0 (ok) -1 (error)
 */
static int recv_register_data(io_head_t *io, raw_data_t *io_buf)
{
	ssize_t	err;
	int	ret;
	size_t	reply_head_len;
	raw_data_t	reply_head;
	raw_data_t	reply;
	byte_t	reply_head_buf[11];

	/* 1:  STX  */
	err = scan_for(STX);
	if (err==-1)
		return -1;

	raw_alloc_onstack(&reply_head, reply_head_buf);
	*(reply_head.end++) = STX;


	/* 2:  ID1 ID2 STAMP MSG_TYPE ADDR1 ADDR2 ADDR3 ADDR4 LEN1 LEN2 */
	err = get_buf(reply_head.end, 10);
	if (err!=10)
		return -1;

	reply_head.end += 10;

	assert (reply_head.end - reply_head.begin >= 0);
	upsdebug_ascii(3, "rx (head):\t", reply_head.begin, (size_t)(reply_head.end - reply_head.begin));


	/* 3:  check header, extract IO info */
	reply_head.begin += 1;	/* temporarily strip STX */

	err = al_parse_reply_head(io, reply_head);
	if (err==-1)
		return -1;

	reply_head.begin -= 1;  /* restore STX */

	upsdebugx(4, "\t\t--> addr: 0x%" PRIxSIZE "  len: 0x%" PRIxSIZE, io->addr, io->len);

	/* 4:  allocate space for full reply and copy header there */
	reply = raw_xmalloc(11/*head*/ + io->len/*data*/ + 2/*ETX BCC*/);

	assert (reply_head.end - reply_head.begin >= 0);
	reply_head_len = (size_t)(reply_head.end - reply_head.begin);

	memcpy(reply.end, reply_head.begin, reply_head_len);
	reply.end += reply_head_len;

	/* 5:  receive tail of the frame */
	err = get_buf(reply.end, io->len + 2);
	if (err!=(int)(io->len+2)) {
		upsdebugx(4, "rx_tail failed, err=%" PRIiSIZE " (!= %" PRIuSIZE ")", err, io->len+2);
		ret = -1; goto out;
	}

	reply.end += io->len + 2;


	/* frame constructed, let's verify it */
	assert (reply.end - reply.begin >= 0);
	upsdebug_ascii(3, "rx (head+data):\t", reply.begin, (size_t)(reply.end - reply.begin));

	/* generic layout */
	err = comli_check_frame(reply);
	if (err==-1) {
		upsdebugx(3, "%s: corrupt frame", __func__);
		ret = -1; goto out;
	}

	/* shrink frame */
	reply.begin += 1;
	reply.end   -= 2;


	/* XXX: a bit of processing duplication here	*/
	ret = al_parse_reply(io, io_buf, reply);

out:
	raw_free(&reply);
	return ret;
}


/*****************************************************************/

/*********************
 * AL175: DO COMMAND *
 *********************/

/**
 * do 'ACTIVATE COMMAND'
 *
 * @return 0 (ok)  -1 (error)
 */
static int al175_do(byte_t cmd, byte_t subcmd, uint16_t pr1, uint16_t pr2, uint16_t pr3)
{
	int err;
	raw_data_t CTRL_frame;
	byte_t	   CTRL_frame_buf[512];

	raw_alloc_onstack(&CTRL_frame, CTRL_frame_buf);
	al_prep_activate(&CTRL_frame, cmd, subcmd, pr1, pr2, pr3);

	flush_rx_queue();		        /*  DROP  */

	err = tx("tx (ctrl):\t", CTRL_frame);	/*  TX    */
	if (err==-1)
		return -1;


	return recv_command_ack();	        /*  RX    */
}


/**
 * 'READ REGISTER'
 *
 */
static int al175_read(byte_t *dst, size_t addr, size_t count)
{
	int err;
	raw_data_t REQ_frame;
	raw_data_t rx_data;
	io_head_t io;

	byte_t	REQ_frame_buf[512];

	raw_alloc_onstack(&REQ_frame, REQ_frame_buf);
	al_prep_read_req(&REQ_frame, addr, count);

	flush_rx_queue();			/*  DROP  */

	err = tx("tx (req):\t", REQ_frame);	/*  TX    */
	if (err==-1)
		return -1;


	rx_data.buf	 = dst;
	rx_data.buf_size = count;
	rx_data.begin	 = dst;
	rx_data.end	 = dst;

	err = recv_register_data(&io, &rx_data);
	if (err==-1)
		return -1;

	if ((rx_data.end - rx_data.begin) < 0
	 || (size_t)(rx_data.end - rx_data.begin) != count
	) {
		return -1;
	}

	if ( (io.addr != addr) || (io.len != count) ) {
		upsdebugx(3, "%s: io_head mismatch\t(%" PRIxSIZE ",%" PRIxSIZE " != %" PRIxSIZE ",%" PRIxSIZE ")",
				__func__, io.addr, io.len, addr, count);
		return -1;
	}


	return 0;
}

/*************
 * NUT STUFF *
 *************/

/****************************
 * ACTIVATE COMMANDS table
 *
 * see 8. ACTIVATE COMMANDS
 */

typedef uint16_t mm_t;	/* minutes */
typedef uint16_t VV_t;	/* voltage */

#define	Z1  , 0
#define Z2  , 0, 0
#define	Z3  , 0, 0, 0

#define	ACT int

/* Declare to keep compiler happy even if some routines below are not used currently */
ACT	TOGGLE_PRS_ONOFF	(void);
ACT	CANCEL_BOOST		(void);
ACT	STOP_BATTERY_TEST	(void);
ACT	START_BATTERY_TEST	(VV_t EndVolt, mm_t Minutes);
ACT	SET_FLOAT_VOLTAGE	(VV_t v);
ACT	SET_BOOST_VOLTAGE	(VV_t v);
ACT	SET_HIGH_BATTERY_LIMIT	(VV_t Vhigh);
ACT	SET_LOW_BATTERY_LIMIT	(VV_t Vlow);
ACT	SET_DISCONNECT_LEVEL_AND_DELAY	(VV_t level, mm_t delay);
ACT	RESET_ALARMS		(void);
ACT	CHANGE_COMM_PROTOCOL	(void);
ACT	SET_VOLTAGE_AT_ZERO_T	(VV_t v);
ACT	SET_SLOPE_AT_ZERO_T	(VV_t mv_per_degree);
ACT	SET_MAX_TCOMP_VOLTAGE	(VV_t v);
ACT	SET_MIN_TCOMP_VOLTAGE	(VV_t v);
ACT	SWITCH_TEMP_COMP	(uint16_t on);
ACT	SWITCH_SYM_ALARM	(void);

/* Implement */
ACT	TOGGLE_PRS_ONOFF	(void)		{ return al175_do(0x81, 0x80			Z3);	}
ACT	CANCEL_BOOST		(void)		{ return al175_do(0x82, 0x80			Z3);	}
ACT	STOP_BATTERY_TEST	(void)		{ return al175_do(0x83, 0x80			Z3);	}
ACT	START_BATTERY_TEST	(VV_t EndVolt, mm_t Minutes)
						{ return al175_do(0x83, 0x81, EndVolt, Minutes	Z1);	}

ACT	SET_FLOAT_VOLTAGE	(VV_t v)	{ return al175_do(0x87, 0x80, v			Z2);	}
ACT	SET_BOOST_VOLTAGE	(VV_t v)	{ return al175_do(0x87, 0x81, v			Z2);	}
ACT	SET_HIGH_BATTERY_LIMIT	(VV_t Vhigh)	{ return al175_do(0x87, 0x82, Vhigh		Z2);	}
ACT	SET_LOW_BATTERY_LIMIT	(VV_t Vlow)	{ return al175_do(0x87, 0x83, Vlow		Z2);	}

ACT	SET_DISCONNECT_LEVEL_AND_DELAY
				(VV_t level, mm_t delay)
						{ return al175_do(0x87, 0x84, level, delay	Z1);	}

ACT	RESET_ALARMS		(void)		{ return al175_do(0x88, 0x80			Z3);	}
ACT	CHANGE_COMM_PROTOCOL	(void)		{ return al175_do(0x89, 0x80			Z3);	}
ACT	SET_VOLTAGE_AT_ZERO_T	(VV_t v)	{ return al175_do(0x8a, 0x80, v			Z2);	}
ACT	SET_SLOPE_AT_ZERO_T	(VV_t mv_per_degree)
						{ return al175_do(0x8a, 0x81, mv_per_degree	Z2);	}

ACT	SET_MAX_TCOMP_VOLTAGE	(VV_t v)	{ return al175_do(0x8a, 0x82, v			Z2);	}
ACT	SET_MIN_TCOMP_VOLTAGE	(VV_t v)	{ return al175_do(0x8a, 0x83, v			Z2);	}
ACT	SWITCH_TEMP_COMP	(uint16_t on)	{ return al175_do(0x8b, 0x80, on		Z2);	}

ACT	SWITCH_SYM_ALARM	(void)		{ return al175_do(0x8c, 0x80			Z3);	}


/**
 * extract double value from a word
 */
static double d16(byte_t data[2])
{
	return (data[1] + 0x100*data[0]) / 100.0;
}

void upsdrv_updateinfo(void)
{
	/* int flags; */

	byte_t x4000[9];		/* registers from 0x4000 to 0x4040 inclusive */
	byte_t x4048[2];		/* 0x4048 - 0x4050	*/
	byte_t x4100[8];		/* 0x4100 - 0x4138	*/
	byte_t x4180[8];		/* 0x4180 - 0x41b8	*/
	byte_t x4300[2];		/* 0x4300 - 0x4308	*/
	int err;

	double batt_current = 0.0;


	upsdebugx(4, " ");
	upsdebugx(4, "UPDATEINFO");
	upsdebugx(4, "----------");
	io_new_transaction(/*timeout=*/3);

#define	RECV(reg) do { \
	err = al175_read(x ## reg, 0x ## reg, sizeof(x ## reg));	\
	if (err==-1) {							\
		dstate_datastale();					\
		return;							\
	}								\
} while (0)

	RECV(4000);
	RECV(4048);
	RECV(4100);
	RECV(4180);
	RECV(4300);


	status_init();

	/* XXX non conformant with NUT naming & not well understood what they mean */
#if 0
				/* 0x4000   DIGITAL INPUT 1-8	*/
	dstate_setinfo("load.fuse",		(x4000[0] & 0x80) ? "OK" : "BLOWN");
	dstate_setinfo("battery.fuse",		(x4000[0] & 0x40) ? "OK" : "BLOWN");
	dstate_setinfo("symalarm.fuse",		(x4000[0] & 0x20) ? "OK" : "BLOWN");

				/* 0x4008   BATTERY INFORMATION	*/
	dstate_setinfo("battery.contactor",	(x4000[1] & 0x80) ? "XX" : "YY");	/* FIXME */
	dstate_setinfo("load.contactor",	(x4000[1] & 0x40) ? "XX" : "YY");	/* FIXME */
	dstate_setinfo("lvd.contactor",		(x4000[1] & 0x20) ? "XX" : "YY");	/* FIXME */
#endif
	if (x4000[0] & 0x40){
		dstate_setinfo("battery.fuse",	"FAIL");
		status_set("RB");
	}else{
		dstate_setinfo("battery.fuse",	"OK");
	}

	if (x4000[0] & 0x20){
		dstate_setinfo("battery.symmetry",	"FAIL");
		status_set("RB");
	}else{
		dstate_setinfo("battery.symmetry",	"OK");
	}

	if (x4000[1] & 0x01)	/* battery test running	*/
		status_set("TEST");

	/* TODO: others from 0x4008 */

				/* 0x4010   NOT USED	*/
				/* 0x4018   NOT USED	*/

	switch (x4000[4]) {	/* 0x4020   MAINS VOLTAGE STATUS	*/
		case 0:     status_set("OL");   break;
		case 1:     status_set("OB");   break;

		case 2:     /* doc: "not applicable" */
		default:
			upsdebugx(2, "%s: invalid mains voltage status\t(%i)", __func__, x4000[4]);
	}

				/* 0x4028   SYSTEM ON OFF STATUS	*/
	switch (x4000[5]) {
		case 0: /* system on */     break;
		case 1:	status_set("OFF");  break;

		default:
			upsdebugx(2, "%s: invalid system on/off status\t(%i)", __func__, x4000[5]);
	}

	switch (x4000[6]) {	/* 0x4030   BATTERY TEST FAIL	*/
		case 0:     dstate_setinfo("ups.test.result", "OK");
			break;

		case 1:     status_set("RB");
			dstate_setinfo("ups.test.result", "FAIL");
			break;

		default:
			upsdebugx(2, "%s: invalid battery test fail\t(%i)", __func__, x4000[6]);
	}
	switch (x4000[7]) {	/* 0x4038   BATTERY VOLTAGE STATUS	*/
		case 0:     /* normal */	break;
		case 1:     status_set("LB");	break;
		case 2:     status_set("HB");   break;

		default:
			upsdebugx(2, "%s: invalid battery voltage status\t(%i)", __func__, x4000[7]);
	}
	switch (x4000[8]) {	/* 0x4040   POS./NEG. BATT. CURRENT	*/
		case 0:	batt_current = +1.0;	break;	/* positive	*/
		case 1:	batt_current = -1.0;	break;	/* negative	*/

		default:
			upsdebugx(2, "%s: invalid pos/neg battery current\t(%i)", __func__, x4000[8]);
	}

	switch (x4048[0]) {	/* 0x4048   BOOST STATUS	*/
		case 0:	/* no boost */;		break;
		case 1:	status_set("BOOST");	break;

		default:
			upsdebugx(2, "%s: invalid boost status\t(%i)", __func__, x4048[0]);
	}

	{
		const char *v=NULL;

		switch (x4048[1]) {	/* 0x4050   SYSTEM VOLTAGE STAT.	*/
			case 0:	v = "48";	break;
			case 1: v = "24";	break;
			case 2: v = "12";	break;
			case 3: v = "26";	break;
			case 4: v = "60";	break;

			default:
				upsdebugx(2, "%s: invalid system voltage status\t(%i)", __func__, x4048[1]);
		}

		if (v)
			dstate_setinfo("output.voltage.nominal",    "%s", v);
	}


				/* 0x4100   BATTERY VOLTAGE REF	*/
	dstate_setinfo("battery.voltage.nominal",	"%.2f", d16(x4100+0));

				/* 0x4110   BOOST VOLTAGE REF	*/
	dstate_setinfo("input.transfer.boost.low",	"%.2f", d16(x4100+2));	/* XXX: boost.high ?	*/

				/* 0x4120   HIGH BATT VOLT REF		XXX	*/
				/* 0x4130   LOW  BATT VOLT REF		XXX	*/

				/* 0x4180   FLOAT VOLTAGE		XXX	*/
				/* 0x4190   BATT CURRENT			*/
	batt_current *= d16(x4180+2);
	dstate_setinfo("battery.current",		"%.2f", batt_current);

				/* 0x41b0   LOAD CURRENT (output.current in NUT)	*/
	dstate_setinfo("output.current",		"%.2f", d16(x4180+6));

				/* 0x4300   BATTERY TEMPERATURE	*/
	dstate_setinfo("battery.temperature",		"%.2f", d16(x4300+0));


	status_commit();

	upsdebugx(1, "STATUS: %s", dstate_getinfo("ups.status"));
	dstate_dataok();


	/* out: */
	return;

}

void upsdrv_shutdown(void)
{
	/* Only implement "shutdown.default"; do not invoke
	 * general handling of other `sdcommands` here */

	/* TODO use TOGGLE_PRS_ONOFF for shutdown */

	/* tell the UPS to shut down, then return - DO NOT SLEEP HERE */

	/* maybe try to detect the UPS here, but try a shutdown even if
	 * it doesn't respond at first if possible */

	/* replace with a proper shutdown function */
	upslogx(LOG_ERR, "shutdown not supported");
	if (handling_upsdrv_shutdown > 0)
		set_exit_flag(EF_EXIT_FAILURE);

	/* you may have to check the line status since the commands
	 * for toggling power are frequently different for OL vs. OB */

	/* OL: this must power cycle the load if possible */

	/* OB: the load must remain off until the power returns */
}

static int instcmd(const char *cmdname, const char *extra)
{
	int err;

	/* May be used in logging below, but not as a command argument */
	NUT_UNUSED_VARIABLE(extra);
	upsdebug_INSTCMD_STARTING(cmdname, extra);

	io_new_transaction(/*timeout=*/5);

	/*
	 * test.battery.start
	 * test.battery.stop
	 */

	if (!strcasecmp(cmdname, "test.battery.start")) {
		upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra);
		err = START_BATTERY_TEST(24, 1);
		return (!err ? STAT_INSTCMD_HANDLED : STAT_INSTCMD_FAILED);
	}

	if (!strcasecmp(cmdname, "test.battery.stop")) {
		upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra);
		err = STOP_BATTERY_TEST();
		return (!err ? STAT_INSTCMD_HANDLED : STAT_INSTCMD_FAILED);
	}

	upslog_INSTCMD_UNKNOWN(cmdname, extra);
	return STAT_INSTCMD_UNKNOWN;
}

/* no help */
void upsdrv_help(void)
{
}

/* optionally tweak prognames[] entries */
void upsdrv_tweak_prognames(void)
{
}

/* no -x flags */
void upsdrv_makevartable(void)
{
}

void upsdrv_initups(void)
{
	upsfd = ser_open(device_path);
	ser_set_speed(upsfd, device_path, B9600);

	ser_disable_flow_control();
}

void upsdrv_cleanup(void)
{
	ser_close(upsfd, device_path);
}


void upsdrv_initinfo(void)
{
	/* TODO issue short io with UPS to detect it's presence */
	/* try to detect the UPS here - call fatal_with_errno(EXIT_FAILURE, ) if it fails */

	dstate_setinfo("ups.mfr", "Eltek");
	dstate_setinfo("ups.model", "AL175");
	/* ... */

	/* instant commands */
	dstate_addcmd ("test.battery.start");
	dstate_addcmd ("test.battery.stop");
	/* TODO rest instcmd(s) */

	upsh.instcmd = instcmd;
}
