/* Copyright (C) 2003 Peter Selinger 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 */ /* A standalone C program for simulating a Belkin Universal UPS or compatible. This program will attach itself to a serial port master and behave (approximately!) as if it were a Belkin UPS - well, not really, actually; it ignores any commands that try to write to a register. You run e.g. simulator /dev/ptyp0 and then tell the driver to talk to /dev/ttyp0. There is also the notion of a "state file", which is a text file with entries of the form register value, as in: 0x18 1189 When starting the simulator as simulator device statefile it continually reads the statefile, which you may edit while the simulator runs. You can use this to figure out how different registers affect the driver. */ #include #include #include #include #include #include #include #include /* Open and prepare a serial port for communication with a Belkin Universal UPS. DEVICE is the name of the serial port. It will be opened in blocking read/write mode, and the appropriate communications parameters will be set. The device will also be sent a special signal (clear DTR, set RTS) to cause the UPS to switch from "dumb" to "smart" mode, and any pending data (=garbage) will be discarded. After this call, the device is ready for reading and writing via read(2) and write(2). Return a valid file descriptor on success, or else -1 with errno set. */ int belkin_open_tty(char *device) { int fd; struct termios tios; struct flock flock; char buf[128]; #if 0 const int tiocm_dtr = TIOCM_DTR; const int tiocm_rts = TIOCM_RTS; #endif int r; /* open the device */ fd = open(device, O_RDWR | O_NONBLOCK); if (fd == -1) { return -1; } /* set communications parameters: 2400 baud, 8 bits, 1 stop bit, no parity, enable reading, hang up when done, ignore modem control lines. */ memset(&tios, 0, sizeof(tios)); tios.c_cflag = B2400 | CS8 | CREAD | HUPCL | CLOCAL; tios.c_cc[VMIN] = 1; tios.c_cc[VTIME] = 0; r = tcsetattr(fd, TCSANOW, &tios); if (r == -1) { close(fd); return -1; } #if 0 /* signal the UPS to enter "smart" mode. This is done by setting RTS and dropping DTR for at least 0.25 seconds. RTS and DTR refer to two specific pins in the 9-pin serial connector. Note: this must be done for at least 0.25 seconds for the UPS to react. */ r = ioctl(fd, TIOCMBIC, &tiocm_dtr); if (r == -1) { close(fd); return -1; } r = ioctl(fd, TIOCMBIS, &tiocm_rts); if (r == -1) { close(fd); return -1; } #endif /* flush both directions of serial port: throw away all data in transit */ r = tcflush(fd, TCIOFLUSH); if (r == -1) { close(fd); return -1; } /* lock the port */ memset(&flock, 0, sizeof(flock)); flock.l_type = F_RDLCK; r = fcntl(fd, F_SETLK, &flock); if (r == -1) { close(fd); return -1; } /* sleep at least 0.25 seconds for the UPS to wake up. Belkin's own software sleeps 1 second, so that's what we do, too. */ usleep(1000000); /* flush incoming data again, and read any remaining garbage bytes. There should not be any. */ r = tcflush(fd, TCIFLUSH); if (r == -1) { close(fd); return -1; } r = read(fd, buf, 127); if (r == -1 && errno != EAGAIN) { close(fd); return -1; } /* finally, switch to blocking i/o, so that future read/write calls will read or write at least one byte */ r = fcntl(fd, F_SETFL, 0); /* clear O_NONBLOCK */ if (r == -1 && errno != EAGAIN) { close(fd); return -1; } return fd; } #define T_CHR 1 #define T_INT 2 #define T_STR 13 /* belkin register description */ struct reg_s { int n; /* register number */ char *name; /* name of register */ int r; /* readable */ int w; /* writable */ int typ; /* type: T_CHR, T_INT, T_STR */ int val; char *strval; }; typedef struct reg_s reg_t; reg_t reg[] = { { 0x00, "unknown", 0, 0, T_CHR, 0, NULL }, { 0x01, "voltage rating", 1, 0, T_CHR, 120, NULL }, // confirmed { 0x02, "frequency rating", 1, 0, T_CHR, 60, NULL }, // confirmed { 0x03, "power rating", 1, 0, T_INT, 800, NULL }, // confirmed { 0x04, "batt. voltage rating", 1, 0, T_CHR, 24, NULL },// confirmed { 0x05, "unknown", 1, 0, T_CHR, 100, NULL }, { 0x06, "unknown", 1, 1, T_INT, 90, NULL }, { 0x07, "unknown", 1, 0, T_INT, 95, NULL }, { 0x08, "unknown", 1, 0, T_INT, 85, NULL }, { 0x09, "unknown", 1, 1, T_INT, 136, NULL }, { 0x0a, "unknown", 1, 0, T_INT, 141, NULL }, { 0x0b, "unknown", 1, 0, T_INT, 131, NULL }, { 0x0c, "voltage sensitivity", 1, 1, T_CHR, 2, NULL }, // confirmed - only 0,1,2 make sense in Linux Monitor { 0x0d, "UPS model", 1, 0, T_STR, 0, "F6C800-UNV " }, // displayed by Linux Monitor - can be more bytes { 0x0e, "UPS model", 1, 0, T_STR, 0, "F6C800-UNV " }, { 0x0f, "firmware/ups type", 1, 0, T_CHR, 0x41, NULL }, // hi nibble confirmed - lo nibble weird: 0=online, 1=offline, 2=line-interactive, 3=online, diagram is messed-up, value is taken mod 3 { 0x10, "test status", 1, 1, T_CHR, 0, NULL }, // confirmed: 0=No Test Performed, 1=Test Passed (also used for: test canceled), 2=General Test Failed, 3=General Test Failed, 4=Test Aborted 5=Test In Progress, 6+: meaningless { 0x11, "audible alarm status", 1, 1, T_CHR, 2, NULL }, // Linux monitor ignores this? { 0x12, "unknown", 1, 1, T_CHR, 0, NULL }, { 0x13, "unknown", 1, 1, T_CHR, 0, NULL }, { 0x14, "unknown", 1, 1, T_CHR, 0, NULL }, { 0x15, "shutdown timer", 1, 1, T_INT, 0, NULL }, // note: monitor wants to shut down immediately when it sees this, but does not set "time to shutdown" timer. { 0x16, "restart timer", 1, 1, T_INT, 0, NULL }, { 0x17, "unknown/???", 1, 1, T_CHR, 0, NULL }, // if this is non-zero, then Bulldog wants to shut down immediately { 0x18, "AC input voltage", 1, 0, T_INT, 1190, NULL }, // confirmed2 { 0x19, "AC input frequency", 1, 0, T_INT, 549, NULL }, // confirmed2 { 0x1a, "temperature", 0, 0, T_CHR, 0, NULL }, // this is the temperature in C. My UPS does not have it, but the monitor shows it. { 0x1b, "AC output voltage", 1, 0, T_INT, 1075, NULL }, // confirmed2 { 0x1c, "AC output frequency", 1, 0, T_INT, 519, NULL }, // confirmed2 { 0x1d, "unknown", 1, 0, T_INT, 250, NULL }, { 0x1e, "loading level", 1, 0, T_CHR, 46, NULL }, // confirmed2 { 0x1f, "AC/battery operation", 1, 0, T_CHR, 0x10, NULL }, { 0x20, "battery voltage", 1, 0, T_INT, 398, NULL }, // confirmed2 { 0x21, "battery level", 1, 0, T_CHR, 91, NULL }, // confirmed2 { 0x22, "AC loss/alarm status", 1, 0, T_INT, 0x8000, NULL }, // see statefile { 0x23, "AC/battery operation", 1, 0, T_CHR, 0x10, NULL }, // see statefile { 0x3f, "time left", 0, 0, T_CHR, 0, NULL }, // see statefile { -1, NULL, 0, 0, 0, 0, NULL } }; /* a curiosity: if 0x1f=00 or 20 on startup, then UPS Health will be orange on 22=1 and 22=9. If 0x1f=10 on startup, then UPS Health will be green on 22=1 and orange on 22=9. */ typedef unsigned char byte; byte belkin_checksum(byte *buf, int n1) { int i; byte ck; ck = 0; for (i=0; i0) { reg[i].strval[strlen(reg[i].strval)-1] = 0; } } } fclose(fin); return; } #define TRY(x) if ((x)==-1) goto try_error #define BUFLEN 20 int main(int ac, char *av[]) { char *name = av[0]; char *dev; int fdin, fdout; byte buf[BUFLEN]; int n, i, ck, r; reg_t *p; int len; char *statefile = NULL; if (ac != 2 && ac != 3) { fprintf(stderr, "Usage: %s device [statefile]\n", name); fprintf(stderr, " %s - [statefile]\n", name); exit(1); } dev = av[1]; if (ac==3) { statefile = av[2]; } if (strcmp(dev, "-")==0) { fdin = 0; /* stdin */ fdout = 1; /* stdout */ } else { TRY(fdin = fdout = belkin_open_tty(dev)); } /* command loop */ while (1) { /* read state file */ if (statefile) { read_statefile(statefile); } TRY(r = read(fdin, &buf[0], 1)); if (r==0) { /* eof */ return 0; } if (buf[0] != 0x7e) { fprintf(stderr, "Bad byte: %02x\n", buf[0]); continue; } TRY(read(fdin, &buf[1], 1)); TRY(read(fdin, &buf[2], 1)); n = buf[2]+4; if (n > BUFLEN) { fprintf(stderr, "Bad length: %d\n", n); continue; } for (i=3; in != -1; p++) { if (p->n == buf[3]) { break; } } if (p->n == -1) { goto error_msg; } switch (p->typ) { case T_CHR: buf[1] = 5; buf[2] = 2; buf[4] = p->val; buf[5] = belkin_checksum(buf, 5); write(fdout, buf, 6); break; case T_INT: buf[1] = 5; buf[2] = 3; buf[4] = p->val & 0xff; buf[5] = (p->val>>8) & 0xff; buf[6] = belkin_checksum(buf, 6); write(fdout, buf, 7); break; case T_STR: len = strlen(p->strval); buf[1] = 5; buf[2] = 1+len; for (i=0; istrval[i]; } buf[4+len] = belkin_checksum(buf, 4+len); write(fdout, buf, 5+len); break; default: goto error_msg; } break; case 4: /* set */ /* find register */ for (p=reg; p->n != -1; p++) { if (p->n == buf[3]) { break; } } if (p->n == -1 || buf[2] != 3) { goto error_msg; } p->val = buf[4] + 8*buf[5]; buf[1] = 2; buf[6] = belkin_checksum(buf, 6); write(fdout, buf, 7); break; default: /* error */ error_msg: buf[1] = 1; buf[n-1] = belkin_checksum(buf, n-1); write(fdout, buf, n); } } try_error: fprintf(stderr, "%s: %s: %s\n", name, dev, strerror(errno)); exit(1); }