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