On Tue, Jan 27, 2009 at 04:39:13PM +0100, Arnaud Quette wrote:
> Hi Kirill,
> 
> 2009/1/27 Kirill Smelkov <[email protected]>
> 
> > On Tue, Jan 13, 2009 at 05:58:23PM +0300, Kirill Smelkov wrote:
> > > Arjen, Arnaud,
> > > first of all, I'm sorry for my late reply.
> > >
> > > If it's not too late, here is updated al175:
> > >
> > >
> > > On Fri, Dec 26, 2008 at 11:37:04PM +0100, Arjen de Korte wrote:
> > > > Citeren Kirill Smelkov <[email protected]>:
> > > >
> > > > [...]
> > > >
> > > >> Yes, "All the world is not an x86 Linux box," and I've tried to make
> > all the
> > > >> world happy.
> > > >
> > > > I raised several other issues as well, of which the most important one
> > > > isn't addressed. The driver still uses 'alloca' which isn't portable
> > (it
> > > > is not in POSIX). I also indicated how this could be solved by using
> > > > automatic variables. In any case I'm not conviced there is no
> > > > alternative here, so this is a show stopper for now.
> > >
> > > I've reworked it not to use alloca and to use auto-variables + xmalloc
> > > in one place.
> > >
> > > Now it usually looks like
> > >
> > >     raw_data    reply;
> > >     byte_t      reply_buf[11];
> > >
> > >     ...
> > >
> > >     raw_alloc_onstack(&reply, reply_buf);
> > >
> > > > Also something I really don't like in this driver is the use of
> > alarm().
> > > > Although this is presently used in the 'net-xml' driver (in case an
> > older
> > > > neon library is detected), there is no need for this here. We have
> > > > timeouts on *all* ser_get* functions and these *must* be used instead.
> > > > Note that with using timeouts I mean something different than setting
> > > > them to '999' seconds and setting an alarm() to breakout early. Unless
> > > > there is a really good reason to use an alarm() (ie, there is no
> > > > alternative), this shouldn't be used.
> > >
> > > I've used alarm in the first place for the following reason:
> > >
> > > Let's look at e.g. upsdrv_updateinfo() -- it reads several al175
> > > registers, and each read_register consists of a chat between host and
> > > al175.
> > >
> > > And I want the whole transaction to be less than say 5 seconds. What
> > > should I do? manually calculate read/write time budget left for _each_
> > > read or write?
> > >
> > > I think this is not practical.
> > >
> > > That's why I simply
> > >
> > > 1. arm the timer via alarm(T) to fire after T seconds
> > > 2. do all the I/O, logic, etc...
> > > 3. if I/O would stale at *any* place, it will be *always* aborted right
> > >    after T seconds since the transaction start (thanks to alarm)
> > > 4. after all the I/O transaction is done -- reset the timer.
> > >
> > > And exactly for this reason 999 is used as infinity which means 'much
> > > more longer than _any_ T we'll pass to alarm() ever'. If there is a way
> > > to actually pass 'please never timeout on select', I'd be happy to
> > > rework the code.
> > >
> > > Also according to mans, alarm(2) is in SVr4, POSIX.1-2001 and 4.3BSD.
> > > That's why I think using alarm() makes sense.
> > >
> > > But if there is a similiar functionality in NUT to set whole transaction
> > > timeout, I'll be happy to rework the code to use what NUT provides.
> > >
> > > > Even in the updated driver, I see quite a couple of fprintf() lines.
> > > > Although these are in most (all?) cases encased in a 'if
> > > > (nut_debug_level > 0)', we have the upsdebug* functions for that. Do
> > > > yourself (and your fellow developers) a favor and use those instead.
> > >
> > > > Another thing related to this is the XTRACE macro. This will only use a
> > > > single (1) debug level to show output. Please use a layered approach,
> > > > where various levels of debugging information are used.
> > >
> > > I've split debug/trace output into several levels:
> > >
> > >     1       /* user-level trace (status, instcmd, etc */
> > >     2       /* proto handling info  */
> > >     3       /* IO trace             */
> > >
> > >
> > > so with -D the user will see only high level stuff, with -DD --
> > > high-level info + any information regarding COMLI proto handling, and
> > > with -DDD -- everything includiong full IO trace.
> > >
> > > Can't test it because I don't have the hardware now, but I hope this is
> > > ok.
> > >
> > > (one place still uses fprintf(stderr, ...) but that's because it is
> > >  difficult to construct one line through several calls to upsdebug,
> > >  although sure this is doable, like upsdebug_hex does. Mine has pretty
> > >  printing for control ascii characters, and I'd better leave it there,
> > >  because as I recall this was usefull for debugging. I hope this is not
> > >  a show-stopper.)
> > >
> > >
> > > > It also helps understanding the driver if you keep the number of
> > > > macro's limited.
> > >
> > > The reason XTRACE was introduced in the first place is to automatically
> > > put a function name as prefix into trace output through the use of
> > > __FUNCTION__. if it is (again) a protability problem, we could always
> > > workaround this as follows (taken from gcc manual):
> > >
> > >
> > >      #if __STDC_VERSION__ < 199901L
> > >      # if __GNUC__ >= 2
> > >      #  define __func__ __FUNCTION__
> > >      # else
> > >      #  define __func__ "<unknown>"
> > >      # endif
> > >      #endif
> > >
> > >
> > >
> > > Here is the interdiff wrt recent version as posted to the list, and the
> > > full patch:
> >
> > [...]
> >
> > Arnaud, Arjen,
> >
> > So what would we do about this patch?
> >
> > I've tried to address the issues you've raised, and to describe my
> > rationale behind alarm().
> >
> > If it can't go in -- that's ok with me, but could you please at least
> > tell me what you objections (if any) are now.
> >
> 
> sorry, both Arjen and I were caught by real life!
> 
> we've had a discussion not long ago about your driver.
> the conclusion is that there are still show stoppers for including it back
> in the tree.
> so it's a no-go for 2.4.0!
> Remember that our aim is not to bother drivers developers, but to ensure
> that their work will be maintainable and maintained even if they
> disappear...
> 
> Arjen should get back to you soon and point/help you in doing what is
> needed.
> Though he already stated most (if not all) the points in the previous mails.
> In the meantime, 2.2.2 will still be available, and you can always use a
> patched 2.4.0 including al175
> 
> so, keep your motivation, we'll end up in succeeding ;-)


On Fri, May 08, 2009 at 10:38:07AM +0400, Kirill Smelkov wrote:
> Arjen,
> 
> On Fri, Feb 06, 2009 at 05:40:56PM +0100, Arjen de Korte wrote:
> > Citeren Kirill Smelkov <[email protected]>:
> >
> >> I'm a bit overworked right now, so I'll try to have a rest and will
> >> reply with a fresh head.
> >
> > If you need any help in this area, please say so and I'll be willing to 
> > invest some time in this to get you started.
> 
> Sorry for so loooooong delay and thank you.
> 
> I just wanted to say that I'm still here -- I decided to get back to
> this issue when I'll have AL175 hardware at hand, so that I can test
> reworked driver. Otherwise, I'm risking too much to break our existing
> setups (located some 1000s km from main office).
> 
> Unfortunately, it could take a long time until live AL175 arives :(
> 
> 
> Hope I'll do it finally,
> Kirill


Arnaud, Arjen, everyone,

If it still makes sense, I'm here again, with reworked al175 driver.

I've tried to clean it up, but unfortunately no longer have hardware to
test and will not have any in foreseeable future, so the driver was
reworked to meet the project code quality criteria, without testing on
real hardware.  Some bugs may have crept in.

Please find main description in github issue tracker

    https://github.com/networkupstools/nut/issues/69

and, for completness, both patches attached to this mail.


Thanks beforehand,
Kirill
>From 7611c7a934ec125be1af303478f034fdcedf17c7 Mon Sep 17 00:00:00 2001
From: Kirill Smelkov <[email protected]>
Date: Mon, 18 Nov 2013 19:32:14 +0400
Subject: [PATCH] common: upsdebug_ascii() - to dump a message in ascii

For debugging ASCII-based protocols with control characters (e.g. COMLI)
it is handy to dump messages not in hex, but in ascii with human
readable codes.

Add utility function to do it.
---
 common/common.c     | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 docs/developers.txt |  4 ++--
 include/common.h    |  1 +
 3 files changed, 65 insertions(+), 2 deletions(-)

diff --git a/common/common.c b/common/common.c
index ef680b0..ce3c573 100644
--- a/common/common.c
+++ b/common/common.c
@@ -452,6 +452,68 @@ void upsdebug_hex(int level, const char *msg, const void *buf, int len)
 	upsdebugx(level, "%s", line);
 }
 
+/* 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 message msg and len bytes from buf to upsdebugx(level) in ascii. */
+void upsdebug_ascii(int level, const char *msg, const void *buf, int len)
+{
+	char line[256];
+	int i;
+	unsigned char ch;
+
+	if (nut_debug_level < level)
+		return;	/* save cpu cycles */
+
+	snprintf(line, sizeof(line), "%s", msg);
+
+	for (i=0; i<len; ++i) {
+		ch = ((unsigned char *)buf)[i];
+
+		if (ch < 0x20)
+			snprintfcat(line, sizeof(line), "%3s ", ascii_symb[ch]);
+		else if (ch >= 0x80)
+			snprintfcat(line, sizeof(line), "%02Xh ", ch);
+		else
+			snprintfcat(line, sizeof(line), "'%c' ", ch);
+	}
+
+	upsdebugx(level, "%s", line);
+}
+
 static void vfatal(const char *fmt, va_list va, int use_strerror)
 {
 	if (xbit_test(upslog_flags, UPSLOG_STDERR_ON_FATAL))
diff --git a/docs/developers.txt b/docs/developers.txt
index 52d19dc..bc555b4 100644
--- a/docs/developers.txt
+++ b/docs/developers.txt
@@ -38,8 +38,8 @@ exit(EXIT_FAILURE) afterwards. Don't call exit() directly.
 Debugging information
 ~~~~~~~~~~~~~~~~~~~~~
 
-upsdebug_with_errno(), upsdebugx() and upsdebug_hex() use the
-global `nut_debug_level` so you don't have to mess around with
+upsdebug_with_errno(), upsdebugx(), upsdebug_hex() and upsdebug_ascii()
+use the global `nut_debug_level` so you don't have to mess around with
 printf()s yourself.  Use them.
 
 Memory allocation
diff --git a/include/common.h b/include/common.h
index 4de8504..fb5df62 100644
--- a/include/common.h
+++ b/include/common.h
@@ -103,6 +103,7 @@ void upsdebug_with_errno(int level, const char *fmt, ...)
 void upsdebugx(int level, const char *fmt, ...)
 	__attribute__ ((__format__ (__printf__, 2, 3)));
 void upsdebug_hex(int level, const char *msg, const void *buf, int len);
+void upsdebug_ascii(int level, const char *msg, const void *buf, int len);
 
 void fatal_with_errno(int status, const char *fmt, ...)
 	__attribute__ ((__format__ (__printf__, 2, 3))) __attribute__((noreturn));
-- 
1.8.5.rc2.317.g8721652

From: Kirill Smelkov <[email protected]>
Subject: [PATCH] al175: updated driver, please restore it

Back in 2005 I was young and idealistic, that's why you finally marked al175 as
'broken', but now I understand your points (some) and that in NUT you need good
portability.

So this time I've checked that al175 compiles with

	CC="gcc -std=c89 -pedantic", and
	CC="gcc -std=c99 -pedantic"

Also, I've tried to clean-up the driver based on feedback from 2009, but
unfortunately I no longer have hardware to test and will not have any in
foreseable future, so the driver was reworked to meet the project code quality
criteria, without testing on real hardware. Some bugs may have crept in.

Changes since last posting in 2009:

- patch rebased on top of current master (v2.6.5-400-g214c442);
- added reference to COMLI communication protocol document;
- status decode errors go to log, instead of setting non-conformant status like
  "?T", "?OOST", etc.  For such errors new loglevel is allocated;
- "High Battery" status is back;
- converted tracing macros to direct use of upsdebugx and numbers 1,2,3,4 for
  loglevels as requested (but now lines got longer because of explicit __func__ usage);
- lowered usage of other macros (e.g. REVERSE_BITS inlined);
- alarm(3) is not used anymore - instead whole I/O transaction time budget is
  maintained manually;
- man page converted to asciidoc and supported variables list is merged into it;

- upsdebug_ascii moved to common.c and to separate patch.


~~~~

Changes since al175 was removed from NUT tree in 2008:

- alloca was eliminated through the help of automatic variables
- debugging/tracing were reworked to (almost always) use NUT builtins
- al175 now uses 3 debug levels for (1=user-level info, 2=protocol debugging,
  3=I/O tracing)

- rechecked http://eu1.networkupstools.org/doc/2.2.0/developers.html and
  applied where apporpiate

Also

> 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.

Yes, at present shutdown is not supported, and unfortunately now I don't have
AL175 hardware at hand, so that I can't write it and verify the implementation.

I've marked the driver as DRV_EXPERIMENTAL, although it was tested by us as
part of our systems to work OK for more than three years in production
environment on ships (and we don't need shutdown there -- in critical
situations the system has to operate as long as possible, untill the battery is
empty)

Also, all of the previous issues listed below are now fixed in this al175
version:

- ‘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)

Yes, "All the world is not an x86 Linux box," and I've tried to make all the
world happy.

Please apply.

Thanks,
Kirill.

---
 MAINTAINERS            |    4 +
 docs/man/.gitignore    |    2 +
 docs/man/Makefile.am   |    3 +
 docs/man/al175.txt     |   73 +++
 docs/man/index.txt     |    1 +
 docs/man/nutupsdrv.txt |    1 +
 docs/new-drivers.txt   |    1 +
 drivers/Makefile.am    |    3 +-
 drivers/al175.c        | 1292 ++++++++++++++++++++++++++++++++++++++++++++++++
 9 files changed, 1379 insertions(+), 1 deletion(-)

diff --git a/MAINTAINERS b/MAINTAINERS
index c52d095..b86fd4b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -45,6 +45,10 @@ P: Fabio Di Niro
 M: [email protected]
 S: Maintained: metasys
 
+P: Kirill Smelkov
+M: [email protected]
+S: Maintained: al175
+
 Packaging
 =========
 
diff --git a/docs/man/.gitignore b/docs/man/.gitignore
index 4f64233..01501f8 100644
--- a/docs/man/.gitignore
+++ b/docs/man/.gitignore
@@ -1,3 +1,4 @@
+al175.html
 apcsmart.html
 apcupsd-ups.html
 bcmxcp.html
@@ -83,6 +84,7 @@ upscli_fd.html
 richcomm_usb.html
 Makefile
 Makefile.in
+al175.8
 apcsmart.8
 apcupsd-ups.8
 bcmxcp.8
diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am
index a5a4b4c..0306370 100644
--- a/docs/man/Makefile.am
+++ b/docs/man/Makefile.am
@@ -301,6 +301,7 @@ else
 
 # (--with-serial)
 SRC_SERIAL_PAGES = \
+	al175.txt	\
 	apcsmart.txt	\
 	apcsmart-old.txt	\
 	bcmxcp.txt 	\
@@ -343,6 +344,7 @@ SRC_SERIAL_PAGES = \
 	apcupsd-ups.txt
 
 MAN_SERIAL_PAGES = \
+	al175.8	\
 	apcsmart.8	\
 	apcsmart-old.8	\
 	bcmxcp.8 	\
@@ -388,6 +390,7 @@ if WITH_SERIAL
 endif
 
 HTML_SERIAL_MANS = \
+	al175.html	\
 	apcsmart.html	\
 	apcsmart-old.html	\
 	bcmxcp.html 	\
diff --git a/docs/man/al175.txt b/docs/man/al175.txt
new file mode 100644
index 0000000..fc4f202
--- /dev/null
+++ b/docs/man/al175.txt
@@ -0,0 +1,73 @@
+AL175(8)
+========
+
+NAME
+----
+al175 - Driver for Eltek UPS models with AL175 alarm module
+
+NOTE
+----
+This man page only documents the hardware-specific features of the
+*al175* driver. For information about the core driver, see
+linkman:nutupsdrv[8].
+
+SUPPORTED HARDWARE
+------------------
+The *al175* driver is known to work with the following UPSes:
+
+    Eltek MPSU4000 with AL175 alarm module
+
+It may also work with other UPSes equiped with AL175 alarm module,
+but they have not been tested.
+
+
+AL175 have to be configured to operate in COMLI mode.
+See documentation supplied with your hardware on how to do it.
+
+EXTRA ARGUMENTS
+---------------
+This driver does not support any extra settings in the
+linkman:ups.conf[5].
+
+INSTANT COMMANDS
+----------------
+This driver supports some extra commands (see linkman:upscmd[8]):
+
+*test.battery.start*::
+Start a battery test.
+
+*test.battery.stop*::
+Stop a battery test.
+
+VARIABLES
+---------
+Besides status, this driver reads UPS state into following variables:
+
+- *ups.test.result*
+- *output.voltage.nominal*
+- *output.current*
+- *battery.voltage.nominal*
+- *battery.current*
+- *battery.temperature*
+- *input.transfer.boost.low*
+
+KNOWN ISSUES AND BUGS
+---------------------
+* Shutdown is not supported.  FIXME
+* The driver was reworked to meet the project code quality criteria, without
+  testing on real hardware. Some bugs may have crept in.
+
+AUTHOR
+------
+Kirill Smelkov <[email protected]>
+
+SEE ALSO
+--------
+
+The core driver:
+~~~~~~~~~~~~~~~~
+linkman:nutupsdrv[8]
+
+Internet resources:
+~~~~~~~~~~~~~~~~~~~
+The NUT (Network UPS Tools) home page: http://www.networkupstools.org/
diff --git a/docs/man/index.txt b/docs/man/index.txt
index 3af9266..c6d7365 100644
--- a/docs/man/index.txt
+++ b/docs/man/index.txt
@@ -52,6 +52,7 @@ Drivers
 
 - linkman:upsdrvctl[8]
 
+- linkman:al175[8]
 - linkman:apcsmart[8]
 - linkman:apcupsd-ups[8]
 - linkman:bcmxcp[8]
diff --git a/docs/man/nutupsdrv.txt b/docs/man/nutupsdrv.txt
index 58d306c..88878cd 100644
--- a/docs/man/nutupsdrv.txt
+++ b/docs/man/nutupsdrv.txt
@@ -157,6 +157,7 @@ Driver control:
 linkman:upsdrvctl[8]
 
 Drivers:
+linkman:al175[8]
 linkman:apcsmart[8],
 linkman:bcmxcp[8],
 linkman:bcmxcp_usb[8],
diff --git a/docs/new-drivers.txt b/docs/new-drivers.txt
index c94e707..0300db5 100644
--- a/docs/new-drivers.txt
+++ b/docs/new-drivers.txt
@@ -200,6 +200,7 @@ Possible values for status_set:
 	OL      - On line (mains is present)
 	OB      - On battery (mains is not present)
 	LB      - Low battery
+	HB	- High battery
 	RB      - The battery needs to be replaced
 	CHRG    - The battery is charging
 	DISCHRG - The battery is discharging (inverter is providing load power)
diff --git a/drivers/Makefile.am b/drivers/Makefile.am
index a6cd30c..f8158d5 100644
--- a/drivers/Makefile.am
+++ b/drivers/Makefile.am
@@ -33,7 +33,7 @@ if WITH_IPMI
   AM_CFLAGS += $(LIBIPMI_CFLAGS)
 endif
 
-SERIAL_DRIVERLIST = bcmxcp belkin belkinunv bestfcom	\
+SERIAL_DRIVERLIST = al175 bcmxcp belkin belkinunv bestfcom	\
  bestfortress bestuferrups bestups dummy-ups etapro everups 	\
  gamatronic genericups isbmex liebert liebert-esp2 masterguard metasys	\
  oldmge-shut mge-utalk microdowell mge-shut oneac optiups powercom rhino 	\
@@ -98,6 +98,7 @@ upsdrvctl_SOURCES = upsdrvctl.c
 upsdrvctl_LDADD = $(LDADD_COMMON)
 
 # serial drivers: all of them use standard LDADD and CFLAGS
+al175_SOURCES = al175.c
 apcsmart_SOURCES = apcsmart.c apcsmart_tabs.c
 apcsmart_old_SOURCES = apcsmart-old.c
 bcmxcp_SOURCES = bcmxcp.c bcmxcp_ser.c
diff --git a/drivers/al175.c b/drivers/al175.c
new file mode 100644
index 0000000..4b4b652
--- /dev/null
+++ b/drivers/al175.c
@@ -0,0 +1,1292 @@
+/*
+ * 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 <[email protected]>
+ *
+ * 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.11"
+
+/* driver description structure */
+upsdrv_info_t upsdrv_info = {
+	DRIVER_NAME,
+	DRIVER_VERSION,
+	"Kirill Smelkov <[email protected]>\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	*/
+	unsigned    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
+ */
+raw_data_t raw_xmalloc(size_t size)
+{
+	raw_data_t data;
+
+	data.buf	= 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
+ */
+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 {
+	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 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;
+	unsigned 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 = ( (x & 0x80) >> 7 )  |
+		    ( (x & 0x40) >> 5 )  |
+		    ( (x & 0x20) >> 3 )  |
+		    ( (x & 0x10) >> 1 )  |
+		    ( (x & 0x08) << 1 )  |
+		    ( (x & 0x04) << 3 )  |
+		    ( (x & 0x02) << 5 )  |
+		    ( (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%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;
+
+	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	*/
+	data[0] = cmd;
+	data[1] = subcmd;
+
+	snprintf(data+2, 6+1, "%2X%2X%2X", pr1, pr2, pr3);
+
+	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)  {
+		upsdebugx(3, "%s: wrong size\t(%i != 10)", __func__, (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	*/
+	}
+
+	io_addr = 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 = 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(%lu > %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;
+	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))  {
+		upsdebugx(3, "%s: corrupt sentence\t(%i != %i)",
+				__func__, raw_reply.end - raw_reply.begin, 10 + io_head->len);
+		return -1;		/* corrupt sentence	*/
+	}
+
+
+	/* extract the data */
+	if (io_buf->buf_size < io_head->len)	{
+		upsdebugx(3, "%s: too much data to fit in io_buf\t(%u > %u)",
+				__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];
+
+	reverse_bits(io_buf->begin, (io_buf->end - io_buf->begin) );
+
+	upsdebug_hex(3, "\t\t--> payload", io_buf->begin, (io_buf->end - io_buf->begin));
+
+	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__, 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], 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;
+
+	tcgetattr (upsfd, &tio);
+
+	tio.c_iflag &= ~ (IXON | IXOFF);
+	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()
+{
+	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)
+{
+	int err;
+
+	upsdebug_ascii(3, dmsg, frame.begin, (frame.end - frame.begin));
+
+	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 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 int 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 int 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;
+	int  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()
+{
+	int 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 */
+	upsdebug_ascii(3, "rx (ack):\t\t", ack.begin, (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)
+{
+	int err, ret;
+	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;
+
+	upsdebug_ascii(3, "rx (head):\t", reply_head.begin, (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%x  len: 0x%x", 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*/);
+
+	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)) {
+		upsdebugx(4, "rx_tail failed, err=%i (!= %i)", err, io->len+2);
+		ret = -1; goto out;
+	}
+
+	reply.end += io->len + 2;
+
+
+	/* frame constructed, let's verify it */
+	upsdebug_ascii(3, "rx (head+data):\t", reply.begin, (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, unsigned 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) != (int)count)
+		return -1;
+
+	if ( (io.addr != addr) || (io.len != count) ) {
+		upsdebugx(3, "%s: io_head mismatch\t(%x,%x != %x,%x)",
+				__func__, io.addr, io.len, addr, count);
+		return -1;
+	}
+
+
+	return 0;
+}
+
+/*************
+ * NUT STUFF *
+ *************/
+
+/****************************
+ * ACTIVATE COMMANDS table
+ *
+ * see 8. ACTIVATE COMMANDS
+ */
+
+typedef int mm_t;	/* minutes */
+typedef int VV_t;	/* voltage */
+
+#define	Z1  , 0
+#define Z2  , 0, 0
+#define	Z3  , 0, 0, 0
+
+#define	ACT 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);	}
+
+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; */
+
+	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)
+{
+	/* 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 */
+	fatalx(EXIT_FAILURE, "shutdown not supported");
+
+	/* 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;
+
+	upsdebugx(1, "INSTCMD: %s", cmdname);
+
+	io_new_transaction(/*timeout=*/5);
+
+	/*
+	 * test.battery.start
+	 * test.battery.stop
+	 */
+
+	if (!strcasecmp(cmdname, "test.battery.start")) {
+		err = START_BATTERY_TEST(24, 1);
+		return (!err ? STAT_INSTCMD_HANDLED : STAT_INSTCMD_FAILED);
+	}
+
+	if (!strcasecmp(cmdname, "test.battery.stop")) {
+		err = STOP_BATTERY_TEST();
+		return (!err ? STAT_INSTCMD_HANDLED : STAT_INSTCMD_FAILED);
+	}
+
+	upslogx(LOG_NOTICE, "instcmd: unknown command [%s]", cmdname);
+	return STAT_INSTCMD_UNKNOWN;
+}
+
+/* no help */
+void upsdrv_help(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;
+}
-- 
tg: (7611c7a..) t/al175 (depends on: master x/upsdebug_ascii)
_______________________________________________
Nut-upsdev mailing list
[email protected]
http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/nut-upsdev

Reply via email to