/* bestfortress.c - model specific routines for (very) old Best Power Fortress Copyright (C) 2002 Russell Kroll (skeleton) (C) 2002 Holger Dietze (C) 2009 Stuart D. Gathman 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "main.h" #include "serial.h" #include "nut_stdint.h" #define UPSDELAY 50000 /* 50 ms delay required for reliable operation */ #define SER_WAIT_SEC 2 /* allow 2.0 sec for ser_get calls */ #define SER_WAIT_USEC 0 #define ENDCHAR '\r' #define IGNCHARS " \n" #if defined(__sgi) && ! defined(__GNUC__) #define inline __inline #endif #define DRIVER_NAME "Best Fortress UPS driver" #define DRIVER_VERSION "0.06" /* driver description structure */ upsdrv_info_t upsdrv_info = { DRIVER_NAME, DRIVER_VERSION, "Holger Dietze \n" "Stuart D. Gathman \n", DRV_EXPERIMENTAL, { NULL } }; static int instcmd (const char *cmdname, const char *extra); static int upsdrv_setvar (const char *varname, const char *val); /* rated VA load if known */ static int maxload = 0; void upsdrv_initinfo(void) { dstate_setinfo("ups.mfr", "Best Power"); dstate_setinfo("ups.model", "Fortress"); dstate_setinfo("battery.voltage.nominal", "24"); /*dstate_setinfo ("alarm.overload", "0");*/ /* Flag */ /*dstate_setinfo ("alarm.temp", "0");*/ /* Flag */ if (maxload) dstate_setinfo("ups.load", "0"); dstate_setinfo("output.voltamps", "0"); dstate_setinfo("ups.delay.shutdown", "10"); /* write only */ /* tunable via front panel: (european voltage level) parameter factory default range INFO_LOWXFER 196 V p7=nnn 160-210 INFO_HIGHXFER 254 V p8=nnn 215-274 INFO_LOBATTIME 2 min p2=n 1-5 comm mode p6=0 dumb DONT USE (will lose access to parameter setting!) p6=1 B1200 p6=2 B2400 P6=3 B4800 p6=4 B9600 maybe cycle through speeds to autodetect? echo off e0 echo on e1 */ dstate_setinfo("input.transfer.low", "%s", ""); dstate_setflags("input.transfer.low", ST_FLAG_STRING | ST_FLAG_RW); dstate_setaux("input.transfer.low", 3); dstate_setinfo("input.transfer.high", "%s", ""); dstate_setflags("input.transfer.high", ST_FLAG_STRING | ST_FLAG_RW); dstate_setaux("input.transfer.high", 3); dstate_setinfo("battery.runtime.low", "%s", ""); dstate_setflags("battery.runtime.low", ST_FLAG_STRING | ST_FLAG_RW); dstate_setaux("battery.runtime.low", 3); upsh.instcmd = instcmd; upsh.setvar = upsdrv_setvar; dstate_addcmd("shutdown.return"); dstate_addcmd("load.off"); } /* convert hex digit to int */ static inline int fromhex (char c) { return (c >= '0' && c <= '9') ? c - '0' : (c >= 'A' && c <= 'F') ? c - 'A' + 10 : (c >= 'a' && c <= 'f') ? c - 'a' + 10 : 0; } /* do checksumming on UPS response */ static int checksum (char * s) { int i; int sum; for (i = 40, sum = 0; s[0] && s[1] && i > 0; i--, s += 2) { sum += (fromhex (s[0]) << 4) + fromhex (s[1]); } return sum; } /* set info to integer value */ static inline int setinfo_int (const char *key, const char * s, size_t len) { char buf[10]; int val; if (len > sizeof(buf)) len = sizeof(buf)-1; strncpy (buf, s, len); buf[len] = 0; val = atoi(buf); dstate_setinfo (key, "%d", val); return val; } /* set info to integer value (for runtime remaining) value is expressed in minutes, but desired in seconds */ static inline void setinfo_int_minutes (const char *key, const char * s, size_t len) { char buf[10]; if (len > sizeof(buf)) len = sizeof(buf)-1; strncpy (buf, s, len); buf[len] = 0; dstate_setinfo (key, "%d", 60*atoi (buf)); } /* set info to float value */ static inline void setinfo_float (const char *key, const char * fmt, const char * s, size_t len, double factor) { char buf[10]; if (len > sizeof(buf)) len = sizeof(buf)-1; strncpy (buf, s, len); buf[len] = 0; #ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL #pragma GCC diagnostic push #endif #ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL #pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif #ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_FORMAT_SECURITY #pragma GCC diagnostic ignored "-Wformat-security" #endif dstate_setinfo (key, fmt, factor * (double)(atoi (buf))); #ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL #pragma GCC diagnostic pop #endif } static int upssend(const char *fmt,...) { int ret; char buf[1024], *p; va_list ap; unsigned int sent = 0; useconds_t d_usec = UPSDELAY; va_start(ap, fmt); #ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL #pragma GCC diagnostic push #endif #ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL #pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif #ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_FORMAT_SECURITY #pragma GCC diagnostic ignored "-Wformat-security" #endif ret = vsnprintf(buf, sizeof(buf), fmt, ap); #ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL #pragma GCC diagnostic pop #endif va_end(ap); if ((ret < 1) || (ret >= (int) sizeof(buf))) upslogx(LOG_WARNING, "ser_send_pace: vsnprintf needed more " "than %d bytes", (int)sizeof(buf)); for (p = buf; *p && sent < INT_MAX - 1; p++) { if (write(upsfd, p, 1) != 1) return -1; /* Note: LGTM.com analysis warns that here * "Comparison is always true because d_usec >= 2" * since we initialize with UPSDELAY above. * Do not remove this check just in case that * initialization changes, or run-time value * becomes modified, in later iterations. */ if (d_usec > 0) usleep(d_usec); sent++; if (sent >= INT_MAX) { upslogx(LOG_WARNING, "ser_send_pace: sent more than INT_MAX, aborting"); } } return (int)sent; } static ssize_t upsrecv(char *buf,size_t bufsize,char ec,const char *ic) { return ser_get_line(upsfd, buf, bufsize - 1, ec, ic, SER_WAIT_SEC, SER_WAIT_USEC); } static ssize_t upsflushin(int f, int verbose, const char *ignset) { NUT_UNUSED_VARIABLE(f); return ser_flush_in(upsfd, ignset, verbose); } /* read out UPS and store info */ void upsdrv_updateinfo(void) { char temp[256]; char *p = NULL; int loadva; size_t len = 0; ssize_t recv; int retry; char ch; int checksum_ok = -1, is_online = 1, is_off, low_batt, trimming, boosting; upsdebugx(1, "upsdrv_updateinfo"); for (retry = 0; retry < 5; ++retry) { upsflushin (0, 0, "\r "); upssend ("f\r"); while (ser_get_char(upsfd, &ch, 0, UPSDELAY) > 0 && ch != '\n'); /* response starts with \r\n */ temp[2] = 0; do { if ((recv = upsrecv (temp+2, sizeof temp - 2, ENDCHAR, IGNCHARS)) <= 0) { upsflushin (0, 0, "\r "); upssend ("f\r"); while (ser_get_char(upsfd, &ch, 0, UPSDELAY) > 0 && ch != '\n'); /* response starts with \r\n */ } } while (temp[2] == 0); upsdebugx(1, "upsdrv_updateinfo: received %zi bytes (try %i)", recv, retry); upsdebug_hex(5, "buffer", temp, (size_t)recv); /* syslog (LOG_DAEMON | LOG_NOTICE,"ups: got %d chars '%s'\n", recv, temp + 2); */ /* status example: 000000000001000000000000012201210000001200014500000280600000990025000000000301BE 000000000001000000000000012401230000001200014800000280600000990025000000000301B7 |Vi||Vo| |Io||Psou| |Vb||f| |tr||Ti| CS 000000000001000000000000023802370000000200004700000267500000990030000000000301BD 1 1 2 2 3 3 4 4 5 5 6 6 7 7 78 0 5 0 5 0 5 0 5 0 5 0 5 0 5 0 5 90 */ /* last bytes are a checksum: interpret response as hex string, sum of all bytes must be zero */ checksum_ok = ( (checksum (temp+2) & 0xff) == 0 ); /* setinfo (INFO_, ""); */ /* I can't figure out why this is missing the first two chars. But the first two chars are not used, so just set them to zero when missing. */ len = strlen(temp+2); temp[0] = '0'; temp[1] = '0'; p = temp+2; if (len == 78) p = temp; else if (len != 80) checksum_ok = 0; if (checksum_ok) break; sleep(SER_WAIT_SEC); } if (!p || len < 1 || checksum_ok < 0) { upsdebugx(2, "pointer to data not initialized after processing"); dstate_datastale(); return; } if (!checksum_ok) { upsdebugx(2, "checksum corruption"); upsdebug_hex(3, "buffer", temp, (size_t)len); dstate_datastale(); return; } /* upslogx(LOG_INFO, "updateinfo: %s", p); */ setinfo_int ("input.voltage", p+24,4); setinfo_int ("output.voltage", p+28,4); setinfo_float ("battery.voltage", "%.1f", p+50,4, 0.1); setinfo_float ("output.current", "%.1f", p+36,4, 0.1); loadva = setinfo_int ("output.voltamps", p+40,6); if (maxload) dstate_setinfo ("ups.load", "%d", loadva * 100 / maxload); setinfo_float ("input.frequency", "%.1f", p+54,3, 0.1); setinfo_int_minutes ("battery.runtime", p+58,4); setinfo_int ("ups.temperature", p+62,4); is_online = p[17] == '0'; low_batt = fromhex(p[21]) & 8 || fromhex(p[20]) & 1; is_off = p[11] == '0'; trimming = p[33] == '1'; boosting = 0; /* FIXME, don't know which bit gets set (brownouts are very rare here and I can't simulate one) */ status_init(); if (low_batt) status_set("LB "); else if (trimming) status_set("TRIM"); else if (boosting) status_set("BOOST"); else status_set(is_online ? (is_off ? "OFF " : "OL ") : "OB "); /* setinfo(INFO_STATUS, "%s%s", * (util < lownorm) ? "BOOST ", "", * (util > highnorm) ? "TRIM ", "", * ((flags & TIOCM_CD) == 0) ? "" : "LB ", * ((flags & TIOCM_CTS) == TIOCM_CTS) ? "OB" : "OL"); */ status_commit(); dstate_dataok(); } /* Parameter setting */ /* all UPS tunable parameters are set with command 'p%d=%s' */ static int setparam (int parameter, int dlen, const char * data) { char reply[80]; /* Note the use of "%*s" - parameter (int)dlen specifies * the string width reserved for data */ upssend ("p%d=%*s\r", parameter, dlen, data); if (upsrecv (reply, sizeof(reply), ENDCHAR, "") < 0) return 0; return strncmp (reply, "OK", 2) == 0; } /* ups_setsuper: set super-user access (allows setting variables) */ static void ups_setsuper (int super) { setparam (999, super ? 4 : 0, super ? "2639" : ""); } /* sets whether UPS will reapply power after it has shut down and line * power returns. */ static void autorestart (int restart) { ups_setsuper (1); setparam (1, 1, restart ? "1" : "0"); ups_setsuper (0); } /* set UPS parameters */ static int upsdrv_setvar (const char *var, const char * data) { int parameter; size_t len = strlen(data); upsdebugx(1, "Setvar: %s %s", var, data); if (strcmp("input.transfer.low", var) == 0) { parameter = 7; } else if (strcmp("input.transfer.high", var) == 0) { parameter = 8; } else if (strcmp("battery.runtime.low", var) == 0) { parameter = 2; } else { upslogx(LOG_INFO, "Setvar: unsettable variable %s", var); return STAT_SET_UNKNOWN; } ups_setsuper (1); assert (len < INT_MAX); if (setparam (parameter, (int)len, data)) { dstate_setinfo (var, "%*s", (int)len, data); } ups_setsuper (0); return STAT_SET_HANDLED; } void upsdrv_shutdown(void) { const char *grace; grace = dstate_getinfo("ups.delay.shutdown"); if (!grace) grace = "1"; /* apparently, OFF0 does not work */ printf ("shutdown in %s seconds\n", grace); /* make power return when utility power returns */ autorestart (1); upssend ("OFF%s\r", grace); /* I'm nearly dead, Jim */ /* OFF will powercycle when line power is available again */ } static int instcmd (const char *cmdname, const char *extra) { const char *p; if (!strcasecmp(cmdname, "load.off")) { printf ("powering off\n"); autorestart (0); upssend ("OFF1\r"); return STAT_INSTCMD_HANDLED; } else if (!strcasecmp(cmdname, "shutdown.return")) { p = dstate_getinfo ("ups.delay.shutdown"); if (!p) p = "1"; printf ("shutdown in %s seconds\n", p); autorestart (1); upssend ("OFF%s\r", p); return STAT_INSTCMD_HANDLED; } upslogx(LOG_INFO, "instcmd: unknown command [%s] [%s]", cmdname, extra); return STAT_INSTCMD_UNKNOWN; } void upsdrv_help(void) { } /* list flags and values that you want to receive via -x */ void upsdrv_makevartable(void) { addvar (VAR_VALUE, "baudrate", "serial line speed"); addvar (VAR_VALUE, "max_load", "rated VA load VA"); } static struct { const char * val; speed_t speed; } speed_table[] = { {"1200", B1200}, {"2400", B2400}, {"4800", B4800}, {"9600", B9600}, {NULL, B1200}, }; void upsdrv_initups(void) { speed_t speed = B1200; char * speed_val = getval("baudrate"); char * max_load = getval("max_load"); if (max_load) maxload = atoi(max_load); if (speed_val) { int i; for (i=0; speed_table[i].val; i++) { if (strcmp (speed_val, speed_table[i].val) == 0) break; } speed = speed_table[i].speed; } upsfd = ser_open(device_path); ser_set_speed(upsfd, device_path, speed); /* TODO: probe ups type */ /* the upsh handlers can't be done here, as they get initialized * shortly after upsdrv_initups returns to main. */ } void upsdrv_cleanup(void) { }