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


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

#include "main.h"
#include "serial.h"
#include "al175.h"

#define DEBUG	1

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

typedef	unsigned char	byte_t;

#if DEBUG
#define XTRACE(fmt, ...) do {					\
	upsdebugx(1, "%s: " fmt, __FUNCTION__ __VA_ARGS__);	\
} while (0)
#else
#define XTRACE(fmt, ...) do {} while (0)
#endif

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

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

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

/**
 * constant raw_data buffer representation
 */
#if 0
typedef struct {
    const byte_t *buf;		//!< the whole buffer address
    unsigned      buf_size;	//!< the whole buffer size

    const byte_t *begin;	//!< begin of content
    const byte_t *end;		//!< one-past-end of content
} const_raw_data_t;
#else
typedef	raw_data_t	const_raw_data_t;
#endif


/**
 * alloca raw_data buffer
 * @param  size the size in bytes
 * @return alloca'ed memory as raw_data
 */
#define raw_alloca(size)		\
({					\
	raw_data_t data;		\
					\
	data.buf       = alloca(size);	\
	data.buf_size  = size;		\
					\
	data.begin     = data.buf;	\
	data.end       = data.buf;	\
					\
	data;				\
})

/**
 * xmalloc raw buffer
 * @param  size	the size in bytes
 * @return xmalloc'ed memory as raw_data
 */
raw_data_t raw_xmalloc(size_t size)
{
	raw_data_t data = {
		.buf       = xmalloc(size),
		.buf_size  = size,
	
	/* XXX: how to handle this?
		.begin     = .buf,
		.end	   = .buf
	*/
	};

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

	return data;
}

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

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


/* taken from www.asciitable.com */
static const char* ascii_symb[] = {
	"NUL",  /*  0x00    */
	"SOH",  /*  0x01    */
	"STX",  /*  0x02    */
	"ETX",  /*  0x03    */
	"EOT",  /*  0x04    */
	"ENQ",  /*  0x05    */
	"ACK",  /*  0x06    */
	"BEL",  /*  0x07    */
	"BS",   /*  0x08    */
	"TAB",  /*  0x09    */
	"LF",   /*  0x0A    */
	"VT",   /*  0x0B    */
	"FF",   /*  0x0C    */
	"CR",   /*  0x0D    */
	"SO",   /*  0x0E    */
	"SI",   /*  0x0F    */
	"DLE",  /*  0x10    */
	"DC1",  /*  0x11    */
	"DC2",  /*  0x12    */
	"DC3",  /*  0x13    */
	"DC4",  /*  0x14    */
	"NAK",  /*  0x15    */
	"SYN",  /*  0x16    */
	"ETB",  /*  0x17    */
	"CAN",  /*  0x18    */
	"EM",   /*  0x19    */
	"SUB",  /*  0x1A    */
	"ESC",  /*  0x1B    */
	"FS",   /*  0x1C    */
	"GS",   /*  0x1D    */
	"RS",   /*  0x1E    */
	"US"    /*  0x1F    */
};

/**
 * dump raw_data buffer to file stream
 * @param  out  output stream
 * @param  buf  the buffer to dump
 */
void raw_dump(FILE *out, const_raw_data_t buf)
{
	byte_t *p;

	fprintf(out, "( ");

	for (p=buf.begin; p!=buf.end; ++p)
		if (*p < 0x20)
			fprintf(out, "%4s ", ascii_symb[*p]);
		else
			fprintf(out, "'%c' ", *p);

	fprintf(out, ")\t[ ");


	for (p=buf.begin; p!=buf.end; ++p)
		fprintf(out, "0x%02x ", *p);

	fprintf(out, "]\n");

}

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

/***************
 * 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 {
	unsigned  addr;	//!< Addr[5:8]
	unsigned  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  count string length
 * @return parsed value (>=0) if success, -1 on error
 */
static long from_hex(const char *head, unsigned len)
{
	long val=0;

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

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

		val *= 0x10;

		ch = toupper(ch);	/* ? locale ?		*/

		if (isdigit(ch))
			val += (ch-'0');
		else
			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 void *buf, size_t count)
{
	byte_t bcc=0;
	unsigned i;

	for (i=0; i<count; ++i)
		bcc ^= *((byte_t *)buf + i);

	return bcc;
}


/* XXX: not optimal */
/**
 * revers bits in a byte from right to left and vice-versa
 * @see 6. CODING AND DECODING OF REGISTER VALUES
 */
#define REVERSE_BITS(x)	(\
		( ((x) & 0x80) >> 7 )		|	\
		( ((x) & 0x40) >> 5 )		|	\
		( ((x) & 0x20) >> 3 )		|	\
		( ((x) & 0x10) >> 1 )		|	\
		( ((x) & 0x08) << 1 )		|	\
		( ((x) & 0x04) << 3 )		|	\
		( ((x) & 0x02) << 5 )		|	\
		( ((x) & 0x01) << 7 )	)

/**
 * reverse bits in a buffer
 * @param   buf     buffer address
 * @param   count   no. of bytes in the buffer
 */
static void reverse_bits(void *buf, size_t count)
{
	byte_t *mem = buf;

	while (count!=0) {
		*mem = REVERSE_BITS(*mem);
                ++mem;
		--count;
	}
}


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

/*
 * communication basics
 *
 * ME	(Monitor Equipment)
 * PRS	(? Power System ? )
 *
 * 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", that is no conversion is done.
 *  if the coller want to reevrse 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(out+1, 10+1, "%02X%1i%1i%04X%02X", 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, unsigned addr, size_t count)
{
	comli_head_t h = {
                .msg = {
		        .id	= 0x14,
		        .stamp	= 1,
		        .type	= 2,
                },

		.io = {
			.addr	= addr,
			.len	= count,
		},
	};

	return 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, int cmd, int subcmd, int pr1, int pr2, int pr3)
{
	comli_head_t h = {
                .msg = {
		        .id	= 0x14,
		        .stamp	= 1,
		        .type	= 0,
                },

		.io = {
			.addr	= 0x4500,
			.len	= 8,
		},
	};

	char data[8+1];

	data[0] = cmd;          // XXX: shall be ASCII here
	data[1] = subcmd;

	snprintf(data+2, 6+1, "%2X%2X%2X", pr1, pr2, pr3);

	return 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.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, (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
 */

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

	if ( (raw_reply_head.end - raw_reply_head.begin) != 10)  {
		XTRACE("wrong size");
		return -1;		/* wrong size	*/
        }

	if (reply_head[1]!='0' || reply_head[2]!='0')  {
		XTRACE("wrong id");
		return -1;		/* wrong id	*/
	}

	if (reply_head[3]!='1')  {
		XTRACE("wrong stamp");
		return -1;		/* wrong stamp	*/
	}

	if (reply_head[4]!='0')  {
		XTRACE("wrong type");
		return -1;		/* wrong type	*/
	}

	io_addr = from_hex(&reply_head[5], 4);
	if (io_addr==-1UL)  {
		XTRACE("wrong addr");
		return -1;		/* wrong addr	*/
	}

	io_len = from_hex(&reply_head[9], 2);
	if (io_len==-1UL)   {
		XTRACE("wrong nob");
		return -1;		/* wrong NOB	*/
	}

	if (io_len > IO_LEN_MAX) {
		XTRACE("nob too big");
		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;
	unsigned i;
	const byte_t *reply = raw_reply.begin - 1;

	/* 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))  {
		XTRACE("corrupt sentence");
		return -1;		/* corrupt sentence	*/
	}


	/* extract the data */
	if (io_buf->buf_size < io_head->len)	{  // XXX: maybe write from io_buf->begin ?
		XTRACE("too much data to fit in io_buf.");
		return -1;		/* too much data to fit in io_buf	*/
	}

	io_buf->begin = io_buf->buf;		// XXX: see ^^^
	io_buf->end   = io_buf->begin;

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

	reverse_bits(io_buf->begin, (io_buf->end - io_buf->begin) );

#if DEBUG
        fprintf(stderr, "rx_buf:\t\t");
        raw_dump(stderr, *io_buf);
#endif

	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)  {
		XTRACE("wrong size");
                return -1;		/* wrong size	*/
	}

	if (ack[1]!='0' || ack[2]!='0')  {
		XTRACE("wrong id");
		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')  {
		XTRACE("wrong stamp");
		return -1;		/* wrong stamp	*/
	}

	if (ack[4]!='1')  {
		XTRACE("wrong type");
		return -1;		/* wrong type	*/
	}

        if (ack[4]!=ACK)  {
		XTRACE("wrong ack");
                return -1;		/* wrong ack	*/
	}


	return 0;
}





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


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

static void alarm_handler(int sig)
{
	/* just do nothing */
	XTRACE("timeout...\n");		/* XXX: doing so is wrong from signal handler... */
}

/* clear any flow control (stolen from powercom.c) */
static void ser_disable_flow_control (void)
{
	struct termios tio;
	
	tcgetattr (upsfd, &tio);
	
	tio.c_iflag &= ~ (IXON | IXOFF);
	tio.c_cc[VSTART] = _POSIX_VDISABLE;
	tio.c_cc[VSTOP] = _POSIX_VDISABLE;
				
	upsdebugx(2, "Flow control disable");

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

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

/**
 * transmit frame to PRS
 *
 * @param  frame  the frame to tansmit
 * @return 0 (ok) -1 (error)
 */
static int tx(const_raw_data_t frame)
{
	int err;

#if DEBUG
	fprintf(stderr, "tx:\t\t");
	raw_dump(stderr, frame);
#endif

	err = ser_send_buf(upsfd, frame.begin, (frame.end - frame.begin) );
	if (err==-1) {
		upslogx(LOG_ERR, "failed to send frame to PRS: %s", strerror(errno));
		return -1;
	}

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


	return 0;
}

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

static int get_char(char *ch)
{
	/* 999 here means infinity.
	 * all timeouts are processed via alarm(2)
	 */
	return ser_get_char(upsfd, ch, 999, 0);
}

static int get_buf(char *buf, size_t len)
{
	return ser_get_buf_len(upsfd, buf, len, 999, 0);
}

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

	while (1) {
		err = get_char(&in);
		if (err==-1)
			return -1;

		if (in==c)
			break;
	}

	return 0;
}


/**
 * receive 'activate command' ACK from PRS
 *
 * @return 0 (ok) -1 (error)
 */
static int recv_command_ack()
{
	int err;
	raw_data_t ack;

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


	ack = raw_alloca(8);
	*(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 */
#if DEBUG
	fprintf(stderr, "rx:\t\t");
	raw_dump(stderr, ack);
#endif

	/* 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)
{
	int err;
	raw_data_t reply_head;
	raw_data_t reply;

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

	 reply_head = raw_alloca(11);
	 *(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;

#if DEBUG
	fprintf(stderr, "rx_head:\t");
	raw_dump(stderr, reply_head);
#endif


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

#if DEBUG
	fprintf(stderr, "io: %x/%x\n", io->addr, io->len);
#endif

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

	memcpy(reply.end, reply_head.begin, (reply_head.end - reply_head.begin));
	reply.end += (reply_head.end - reply_head.begin);
	
	/* 5:  receive tail of the frame */
	err = get_buf(reply.end, io->len + 2);
	if (err!=(int)(io->len+2)) {
		XTRACE("rx_tail failed");
		return -1;
	}

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


	/* frame constructed, let's verify it */
#if DEBUG
	fprintf(stderr, "rx:\t\t");
	raw_dump(stderr, reply);
#endif

	/* generic layout */
	err = comli_check_frame(reply);
	if (err==-1) {
		XTRACE("corrupt frame");
		return -1;
	}

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


	// XXX: a bit of processing duplication here
	return al_parse_reply(io, io_buf, reply);
}


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

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

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

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

	flush_rx_queue();		/*  DROP  */

	err = tx(CTRL_frame);		/*  TX    */
	if (err==-1)
		return -1;

	
	return recv_command_ack();	/*  RX    */
}


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

	al_prep_read_req(&REQ_frame, addr, count);

	flush_rx_queue();		/*  DROP  */

	err = tx(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.begin != dst)   // XXX: paranoia
            return -1;

        if ((rx_data.end - rx_data.begin) != (int)count)
            return -1;

	if ( (io.addr != addr) || (io.len != count) ) {
		XTRACE("io_head mismatch");
		return -1;
	}


	return 0;
}

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

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

// XXX: ?
typedef int mm_t;	/* minutes */
typedef int VV_t;	/* voltage */

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

#define	ACT static __attribute__((used)) int

ACT	TOGGLE_PRS_ONOFF	()		{ return al175_do(0x81, 0x80			Z3);	}
ACT	CANCEL_BOOST		()		{ return al175_do(0x82, 0x80			Z3);	}
ACT	STOP_BATTERY_TEST	()		{ return al175_do(0x83, 0x80			Z3);	}
ACT	START_BATTERY_TEST	(VV_t EndVolt, unsigned 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		()		{ return al175_do(0x88, 0x80			Z3);	}
ACT	CHANGE_COMM_PROTOCOL	()		{ 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	(int on)	{ return al175_do(0x8b, 0x80, on		Z2);	}

// XXX: ?
ACT	SWITCH_SYM_ALARM	()		{ 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; */
	/* char temp[256]; */

	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;


	fprintf(stderr, "\nUPDATEINFO\n");
	alarm(3);

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

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


        status_init();

#if 0					
/* non conformant with NUT naming */
				// 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[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:     status_set("NOT_APPLICABLE");   break;

            default:    status_set("OFF");  // XXX: "not applicable" == OFF ?
        }

				// 0x4028   SYSTEM ON OFF STATUS
	// XXX: 0x4028 is broken? (it reads as 0x55)
	switch (x4000[5]) {
	    case 0:                         break;
	    case 1:	status_set("OFF");  break;

	    default:    status_set("UNKNOWN...");
	}

        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;  // XXX: RB == remove battery?
/* AQU note: the below must be adapted! */
            default:    status_set("?T");
        }
        switch (x4000[7]) {	// 0x4038   BATTERY VOLTAGE STATUS
            case 0:     /* normal */        break;
            case 1:     status_set("LB");   break;
#if 0
/* non conformant to ups.status values */
            case 2:     status_set("HB");   break;

            default:    status_set("?B");
#endif
            default:    break;
	}
	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:    batt_current = 0.0;		// shouldn't happen
	}

	switch (x4048[0]) {	// 0x4048   BOOST STATUS
	    case 0:	/* no boost */;		break;
	    case 1:	status_set("BOOST");	break;
#if 0
/* non conformant to ups.status values */
	    default:	status_set("BOO??");
#endif
            default:    break;
	}

	{
	    const char *v;

	    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: v = "??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: */
	alarm(0);
	return;

}

void upsdrv_shutdown(void)
{
	/* 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 */
	fatalx(EXIT_FAILURE, "shutdown not supported");	/* TODO: implement... */

	/* 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_worker(const char *cmdname)
{
	/*
	 * test.battary.start
	 * test.battary.stop
	 */

	if (!strcasecmp(cmdname, "test.battery.start")) {
		START_BATTERY_TEST(24, 1);
		return STAT_INSTCMD_HANDLED;
	}

	if (!strcasecmp(cmdname, "test.battery.stop")) {
		STOP_BATTERY_TEST();
		return STAT_INSTCMD_HANDLED;
	}

	upslogx(LOG_NOTICE, "instcmd: unknown command [%s]", cmdname);
	return STAT_INSTCMD_UNKNOWN;
}

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

        upsdebugx(1, "INSTCMD: %s", cmdname);

	alarm(5);
	err = instcmd_worker(cmdname);
	alarm(0);

	return err;
}

void upsdrv_help(void)
{
}

/* list flags and values that you want to receive via -x */
void upsdrv_makevartable(void)
{
	/* allow '-x xyzzy' */
	/* addvar(VAR_FLAG, "xyzzy", "Enable xyzzy mode"); */

	/* allow '-x foo=<some value>' */
	/* addvar(VAR_VALUE, "foo", "Override foo setting"); */
}

void upsdrv_banner(void)
{
	printf("Network UPS Tools - Eltek AL175/COMLI support module %s (%s)\n\n", 
		DRV_VERSION, UPS_VERSION);
	/*
	 * This driver does not support upsdrv_shutdown(), which makes
	 * it not very useful in a real world application. This alone
	 * warrants 'experimental' status, but for the below mentioned
	 * reasons (to name a few), it's flagged 'broken' instead.
	 *
	 * - ‘return’ with a value, in function returning void (2x)
	 * - anonymous variadic macros were introduced in C99
	 * - C++ style comments are not allowed in ISO C90
	 * - ISO C forbids braced-groups within expressions (5x)
	 * - ISO C90 forbids specifying subobject to initialize (16x)
	 * - ISO C99 requires rest arguments to be used (18x)
	 * - initializer element is not computable at load time (4x)
	 *
	 * In short, there is a lot of rewriting to be done. Not the
	 * whole world is a Linux box with the latest gcc on it.
	 */
	broken_driver = 1;
}

void upsdrv_initups(void)
{
	signal(SIGALRM, alarm_handler);
	alarm(0);

	upsfd = ser_open(device_path);
	ser_set_speed(upsfd, device_path, B9600);

	ser_disable_flow_control();

	/* probe ups type */

	/* to get variables and flags from the command line, use this:
	 *
	 * first populate with upsdrv_buildvartable above, then...
	 *
	 *                   set flag foo : /bin/driver -x foo
	 * set variable 'cable' to '1234' : /bin/driver -x cable=1234
	 *
	 * to test flag foo in your code:
	 *
	 * 	if (testvar("foo"))
	 * 		do_something();
	 *
	 * to show the value of cable:
	 *
	 *      if ((cable == getval("cable")))
	 *		printf("cable is set to %s\n", cable);
	 *	else
	 *		printf("cable is not set!\n");
	 *
	 * don't use NULL pointers - test the return result first!
	 */

	/* the upsh handlers can't be done here, as they get initialized
	 * shortly after upsdrv_initups returns to main.
	 */

	/* don't try to detect the UPS here */
}

void upsdrv_cleanup(void)
{
	/* free(dynamic_mem); */
	ser_close(upsfd, device_path);
}


void upsdrv_initinfo(void)
{
	/* try to detect the UPS here - call fatal_with_errno(EXIT_FAILURE, ) if it fails */

	dstate_setinfo("driver.version.internal", "%s", DRV_VERSION);
	dstate_setinfo("ups.mfr", "Eltek");
	dstate_setinfo("ups.model", "AL175");
        /* ... */

        /* instant commands */
	dstate_addcmd ("test.battery.start");
	dstate_addcmd ("test.battery.stop");
        /* XXX: more? */

	upsh.instcmd = instcmd;
}


/* vim: set ts=8 noet sts=8 sw=8 */


syntax highlighted by Code2HTML, v. 0.9.1