[Nut-upsdev] [PATCH] al175: updated driver, please restore it
Kirill Smelkov
kirr at mns.spb.ru
Fri Dec 26 09:31:02 UTC 2008
Hi Arnaud,
> Hi Kirill,
>
> just to notify you that your al175 driver is being removed from the
> NUT tree, as of 2.4.0-pre1.
>
> if you wish to see it entering the tree again, please contact the
> Development mailing list to talk about it.
I wish it very much, that's why I'm here.
Yes, back in 2005 I was young and idealistic, that's why you finally marked
al175 as 'broken', but now I understand your points and that in NUT you need
good portability.
So this time I've checked everything with
CC="gcc -std=c89 -pedantic", and
CC="gcc -std=c99 -pedantic"
Though I'd like to, but could not use -pedantic-errors, becuase there is a
portablilty problem in drivers/main.h:
main.h|60| error: ISO C90 does not support flexible array members
----
Changes since al175 as recently removed from NUT tree:
- DEBUG macro replaced with runtime debugging switch (using nut_debug_level)
- 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.
> Merry Christmas and happy New Year.
> Arnaud
Merry Christmas and happy New Year to you too!
Kirill.
MAINTAINERS | 4 +
data/driver.list | 2 +
docs/al175-vars.txt | 28 +
drivers/Makefile.am | 3 +-
drivers/al175.c | 1372 +++++++++++++++++++++++++++++++++++++++++++++++++++
man/Makefile.am | 1 +
man/al175.8 | 53 ++
man/nutupsdrv.8 | 2 +-
8 files changed, 1463 insertions(+), 2 deletions(-)
diff --git a/MAINTAINERS b/MAINTAINERS
index c8c4e97..6c14603 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -53,6 +53,10 @@ P: Peter Selinger
M: selinger at users.sourceforge.net
S: Maintained: usbhid-ups belkinunv
+P: Kirill Smelkov
+M: kirr at mns.spb.ru
+S: Maintained: al175
+
Packaging
=========
diff --git a/data/driver.list b/data/driver.list
index 3b1f3f3..8950873 100644
--- a/data/driver.list
+++ b/data/driver.list
@@ -181,6 +181,8 @@
"Effekta" "MI/MT/MH" "2502 cable" "megatec"
"Effekta" "RM2000MH" "" "megatec"
+"Eltek" "any with AL175 alarm module" "" "al175"
+
"Energy Sistem" "(various)" "" "megatec"
"ETA" "mini+UPS" "WinNT/Upsoft cable" "genericups upstype=7"
diff --git a/docs/al175-vars.txt b/docs/al175-vars.txt
new file mode 100644
index 0000000..494f9da
--- /dev/null
+++ b/docs/al175-vars.txt
@@ -0,0 +1,28 @@
+Desc: Variables supported by al175 driver
+File: al175-vars.txt
+Date: 02 September 2005
+Auth: Kirill Smelkov <kirr at mns.spb.ru>
+
+* this is a draft *
+
+load.fuse
+battery.fuse FAIL if fuse on battery (set status.RB flag)
+battery.symmetric set status.RB flag
+
+battery.contactor
+load.contactor
+lvd.contactor
+
+
+
+STATUS = OL OB OFF RB LB HB TEST BOOST
+
+ups.test.result
+
+
+output.voltage.nominal
+
+battery.voltage.nominal
+input.transfer.boost.low
+
+battery.current
diff --git a/drivers/Makefile.am b/drivers/Makefile.am
index 952b4ba..fa2679e 100644
--- a/drivers/Makefile.am
+++ b/drivers/Makefile.am
@@ -34,7 +34,7 @@ if WITH_LIBPOWERMAN
AM_CFLAGS += $(LIBPOWERMAN_CFLAGS)
endif
-SERIAL_DRIVERLIST = apcsmart bcmxcp belkin belkinunv bestfcom \
+SERIAL_DRIVERLIST = al175 apcsmart bcmxcp belkin belkinunv bestfcom \
bestuferrups bestups cyberpower dummy-ups etapro everups \
gamatronic genericups isbmex liebert masterguard megatec metasys \
mge-shut mge-utalk newmge-shut oneac optiups powercom rhino \
@@ -92,6 +92,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
bcmxcp_SOURCES = bcmxcp.c bcmxcp_ser.c
belkin_SOURCES = belkin.c
diff --git a/drivers/al175.c b/drivers/al175.c
new file mode 100644
index 0000000..cff7b8f
--- /dev/null
+++ b/drivers/al175.c
@@ -0,0 +1,1372 @@
+/*
+ * al175.c - NUT support for Eltek AL175 alarm module.
+ * AL175 shall be in COMLI mode.
+ *
+ * Copyright (C) 2004-2008 Marine & Bridge Navigation Systems <http://mns.spb.ru>
+ * Copyright (C) 2004-2008 Kirill Smelkov <kirr at 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"
+
+#define DRIVER_NAME "Eltek AL175/COMLI driver"
+#define DRIVER_VERSION "0.10"
+
+
+/* driver description structure */
+upsdrv_info_t upsdrv_info = {
+ DRIVER_NAME,
+ DRIVER_VERSION,
+ "Kirill Smelkov <kirr at mns.spb.ru>\n" \
+ "Marine & Bridge Navigation Systems <http://mns.spb.ru>",
+ DRV_EXPERIMENTAL,
+ { NULL }
+};
+
+
+#define STX 0x02
+#define ETX 0x03
+#define ACK 0x06
+
+typedef unsigned char byte_t;
+
+/* it's a pity that anonymous variadic macros were introduced only in C99 */
+#define XTRACE(msg) do { \
+ upsdebugx(1, "%s: %s", __FUNCTION__, msg); \
+} while (0)
+
+/************
+ * 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 varp ptr-to local raw_data_t variable to which to alloca
+ * @param size the size in bytes
+ * @return alloca'ed memory as raw_data
+ *
+ * it's a pity ISO C forbids braced-groups within expressions
+ */
+#define raw_alloca(varp, size) do { \
+ (varp)->buf = alloca(size); \
+ (varp)->buf_size = size; \
+ \
+ (varp)->begin = (varp)->buf; \
+ (varp)->end = (varp)->buf; \
+} while (0)
+
+
+/**
+ * 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;
+
+ data.buf = xmalloc(size);
+ data.buf_size = size;
+
+ 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;
+
+ 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, int cmd, int subcmd, int pr1, int pr2, int 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;
+
+ data[0] = cmd; /* XXX: shall be ASCII here */
+ 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) {
+ 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) );
+
+ /* debug: dump rx data */
+ if (nut_debug_level > 0) {
+ fprintf(stderr, "rx_buf:\t\t");
+ raw_dump(stderr, *io_buf);
+ }
+
+ 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;
+
+ /* debug: dump tx frame */
+ if (nut_debug_level > 0) {
+ fprintf(stderr, "tx:\t\t");
+ raw_dump(stderr, frame);
+ }
+
+ 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;
+
+
+ raw_alloca(&ack, 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 (nut_debug_level > 0) {
+ fprintf(stderr, "rx:\t\t");
+ raw_dump(stderr, ack);
+ }
+
+ /* 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;
+
+ raw_alloca(&reply_head, 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 (nut_debug_level > 0) {
+ fprintf(stderr, "rx_head:\t");
+ raw_dump(stderr, reply_head);
+ }
+
+
+ /* 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 (nut_debug_level > 0)
+ fprintf(stderr, "io: %x/%x\n", io->addr, io->len);
+
+ /* 4: allocate space for full reply and copy header there */
+ raw_alloca(&reply, 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 (nut_debug_level > 0) {
+ fprintf(stderr, "rx:\t\t");
+ raw_dump(stderr, reply);
+ }
+
+ /* 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(&CTRL_frame, 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_data_t rx_data;
+ io_head_t io;
+
+ raw_alloca(&REQ_frame, 512);
+ 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 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[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: 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_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("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 */
diff --git a/man/Makefile.am b/man/Makefile.am
index 40918d1..d92add9 100644
--- a/man/Makefile.am
+++ b/man/Makefile.am
@@ -20,6 +20,7 @@ CGI_PAGES = hosts.conf.5 upsset.conf.5 upsstats.html.5 \
upsset.cgi.8 upsstats.cgi.8 upsimage.cgi.8
SERIAL_PAGES = \
+ al175.8 \
apcsmart.8 \
bcmxcp.8 \
belkin.8 \
diff --git a/man/al175.8 b/man/al175.8
new file mode 100644
index 0000000..3f7be91
--- /dev/null
+++ b/man/al175.8
@@ -0,0 +1,53 @@
+.TH AL175 8 "Wed Sep 21 2005" "" "Network UPS Tools (NUT)"
+.SH NAME
+al175 \- Driver for Eltek UPS models with AL175 alarm module
+.SH NOTE
+This man page only documents the hardware\(hyspecific features of the
+al175 driver. For information about the core driver, see
+\fBnutupsdrv\fR(8).
+
+.SH 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 haven't been tested.
+
+
+AL175 have to be configured to operate in COMLI mode.
+See documentation supplied with your hardware on how to do it.
+
+.SH EXTRA ARGUMENTS
+
+This driver does not support any extra settings in the
+\fBups.conf\fR(5).
+
+.SH UPS COMMANDS
+
+This driver supports some extra commands (see \fBupscmd\fR(8)):
+
+.IP "test.battery.start"
+Start a battery test.
+
+.IP "test.battery.stop"
+Stop a battery test.
+
+.SH USER VARIABLES
+
+None
+
+.SH BUGS
+
+Shutdown is not supported.
+
+.SH AUTHOR
+Kirill Smelkov <kirr at mns.spb.ru>
+
+.SH SEE ALSO
+
+.SS The core driver:
+\fBnutupsdrv\fR(8)
+
+.SS Internet resources:
+The NUT (Network UPS Tools) home page: http://www.networkupstools.org/
diff --git a/man/nutupsdrv.8 b/man/nutupsdrv.8
index 2b540b1..b61c29c 100644
--- a/man/nutupsdrv.8
+++ b/man/nutupsdrv.8
@@ -134,7 +134,7 @@ information.
\fBupsdrvctl\fR(8)
.SS Drivers:
-\fBapcsmart\fR(8), \fBbcmxcp\fR(8), \fBbcmxcp_usb\fR(8),
+-\fBal175\fR(8), \fBapcsmart\fR(8), \fBbcmxcp\fR(8), \fBbcmxcp_usb\fR(8),
\fBbelkin\fR(8), \fBbelkinunv\fR(8), \fBbestfcom\fR(8),
\fBbestuferrups\fR(8), \fBbestups\fR(8),
\fBcyberpower\fR(8), \fBdummy-ups\fR(8), \fBetapro\fR(8),
--
tg: (fe47a8f..) t/al175 (depends on: master)
More information about the Nut-upsdev
mailing list