1 /*
2 * powerp-bin.c - Model specific routines for CyberPower binary
3 * protocol UPSes
4 *
5 * Copyright (C)
6 * 2007 Doug Reynolds <mav@wastegate.net>
7 * 2007-2009 Arjen de Korte <adkorte-guest@alioth.debian.org>
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22 */
23
24 /*
25 * Throughout this driver, READ and WRITE comments are shown. These are
26 * the typical commands to and replies from the UPS that was used for
27 * decoding the protocol (with a serial logger).
28 */
29
30 #include "main.h"
31 #include "serial.h"
32
33 #include "powerp-bin.h"
34
35 #include <math.h>
36
37 typedef struct {
38 unsigned char start;
39 unsigned char i_volt;
40 unsigned char o_volt;
41 unsigned char o_load;
42 unsigned char fill_4;
43 unsigned char b_chrg;
44 unsigned char u_temp;
45 unsigned char i_freq;
46 unsigned char fill_8;
47 unsigned char flags[4];
48 unsigned char stop;
49 } status_t;
50
51 typedef struct {
52 const char *val;
53 const char command;
54 } valtab_t;
55
56 static enum {
57 PR = 0,
58 OP = 1
59 } type = PR;
60
61 static unsigned char powpan_answer[SMALLBUF];
62
63 /* PR series */
64 static const valtab_t tran_high_pr[] = {
65 { "138", -9 }, { "139", -8 }, { "140", -7 }, { "141", -6 }, { "142", -5 },
66 { "143", -4 }, { "144", -3 }, { "145", -2 }, { "146", -1 }, { "147", 0 },
67 { NULL }
68 };
69
70 /* OP series */
71 static const valtab_t tran_high_op[] = {
72 { "140", -5 }, { "141", -4 }, { "142", -3 }, { "143", -2 }, { "144", -1 },
73 { "145", 0 }, { "146", +1 }, { "147", +2 }, { "148", +3 }, { "149", +4 },
74 { "150", +5 }, { NULL }
75 };
76
77 /* PR series */
78 static const valtab_t tran_low_pr[] = {
79 { "88", 0 }, { "89", +1 }, { "90", +2 }, { "91", +3 }, { "92", +4 },
80 { "93", +5 }, { "94", +6 }, { "95", +7 }, { "96", +8 }, { "97", +9 },
81 { NULL }
82 };
83
84 /* OP series */
85 static const valtab_t tran_low_op[] = {
86 { "85", -5 }, { "86", -4 }, { "87", -3 }, { "88", -2 }, { "89", -1 },
87 { "90", 0 }, { "91", +1 }, { "92", +2 }, { "93", +3 }, { "94", +4 },
88 { "95", +5 }, { NULL }
89 };
90
91 /* PR series */
92 static const valtab_t batt_low_pr[] = {
93 { "25", -6 }, { "30", -5 }, { "35", -3 }, { "40", -1 }, { "45", 0 },
94 { "50", +2 }, { "55", +4 }, { "60", +6 }, { NULL }
95 };
96
97 /* OP series */
98 static const valtab_t batt_low_op[] = {
99 { "15", -8 }, { "18", -7 }, { "19", -6 }, { "20", -5 }, { "22", -4 },
100 { "24", -3 }, { "25", -2 }, { "26", -1 }, { "28", 0 }, { "30", +1 },
101 { "32", +2 }, { "34", +3 }, { "35", +4 }, { "36", +5 }, { "38", +6 },
102 { "40", +7 }, { NULL }
103 };
104
105 /* PR series */
106 static const valtab_t out_volt_pr[] = {
107 { "110", -10 }, { "120", 0 }, { "130", +10 }, { NULL }
108 };
109
110 /* OP series */
111 static const valtab_t out_volt_op[] = {
112 { "110", -2 }, { "115", -1 }, { "120", 0 }, { "124", +1 }, { "128", +2 },
113 { "130", +3 }, { NULL }
114 };
115
116 static const valtab_t yes_no_info[] = {
117 { "yes", 2 }, { "no", 0 },
118 { NULL }
119 };
120
121 /* Older models report the model in a numeric format 'rOnn' */
122 static const struct {
123 const char *val;
124 const char *model;
125 } modeltab[] = {
126 { "rO10", "OP1000AVR" },
127 { "rO27", "OP320AVR" },
128 { "rO29", "OP500AVR" },
129 { "rO31", "OP800AVR" },
130 { "rO33", "OP850AVR" },
131 { "rO37", "OP900AVR" },
132 { "rO39", "OP650AVR" },
133 { "rO41", "OP700AVR" },
134 { "rO43", "OP1250AVR" },
135 { "rO45", "OP1500AVR" },
136 { NULL }
137 };
138
139 static const struct {
140 const char *var;
141 const char *get;
142 const char *set;
143 const valtab_t *map[2];
144 } vartab[] = {
145 { "input.transfer.high", "R\x02\r", "Q\x02%c\r", { tran_high_pr, tran_high_op } },
146 { "input.transfer.low", "R\x04\r", "Q\x04%c\r", { tran_low_pr, tran_low_op } },
147 { "battery.charge.low", "R\x08\r", "Q\x08%c\r", { batt_low_pr, batt_low_op } },
148 { "ups.start.battery", "R\x0F\r", "Q\x0F%c\r", { yes_no_info, yes_no_info } },
149 { "output.voltage.nominal", "R\x18\r", "Q\x18%c\r", { out_volt_pr, out_volt_op } },
150 { NULL }
151 };
152
153 static const struct {
154 const char *cmd;
155 const char *command;
156 const int len;
157 } cmdtab[] = {
158 { "test.battery.start.quick", "T\230\r", 3 }, /* 20 seconds test */
159 { "test.battery.stop", "CT\r", 3 },
160 { "beeper.toggle", "B\r", 2 },
161 { "shutdown.reboot", "S\0\0R\0\1W\r", 8},
162 /* the shutdown.stayoff command behaves
163 * as shutdown.return when on battery */
164 { "shutdown.stayoff", "S\0\0W\r", 5 },
165 { "shutdown.stop", "C\r", 2 },
166 { NULL }
167 };
168
169 /* map UPS data to (approximated) input/output voltage */
op_volt(unsigned char in)170 static int op_volt(unsigned char in)
171 {
172 if (in < 26) {
173 return 0;
174 }
175
176 return (((float)in * 200 / 230) + 6);
177 }
178
179 /* map UPS data to (approximated) load */
op_load(unsigned char in)180 static int op_load(unsigned char in)
181 {
182 if (in > 108) {
183 return 200;
184 }
185
186 return (in * 200) / 108;
187 }
188
189 /* map UPS data to (approximated) charge percentage */
op_chrg(unsigned char in)190 static int op_chrg(unsigned char in)
191 {
192 if (in > 197) {
193 return 100;
194 }
195
196 if (in < 151) {
197 return 0;
198 }
199
200 return ((in - 151) * 100) / 46;
201 }
202
203 /* map UPS data to (approximated) temperature */
op_temp(unsigned char in)204 static float op_temp(unsigned char in)
205 {
206 return (pow((float)in / 32, 2) + 10);
207 }
208
209 /* map UPS data to (approximated) frequency */
op_freq(unsigned char in)210 static float op_freq(unsigned char in)
211 {
212 return (12600.0 / (in + 32));
213 }
214
powpan_command(const char * buf,size_t bufsize)215 static int powpan_command(const char *buf, size_t bufsize)
216 {
217 int ret;
218
219 ser_flush_io(upsfd);
220
221 ret = ser_send_buf_pace(upsfd, UPSDELAY, buf, bufsize);
222
223 if (ret < 0) {
224 upsdebug_with_errno(3, "send");
225 return -1;
226 }
227
228 if (ret == 0) {
229 upsdebug_with_errno(3, "send: timeout");
230 return -1;
231 }
232
233 upsdebug_hex(3, "send", buf, bufsize);
234
235 usleep(100000);
236
237 ret = ser_get_buf_len(upsfd, powpan_answer, bufsize-1, SER_WAIT_SEC, SER_WAIT_USEC);
238
239 if (ret < 0) {
240 upsdebug_with_errno(3, "read");
241 upsdebug_hex(4, " \\_", buf, bufsize-1);
242 return -1;
243 }
244
245 if (ret == 0) {
246 upsdebugx(3, "read: timeout");
247 upsdebug_hex(4, " \\_", buf, bufsize-1);
248 return -1;
249 }
250
251 upsdebug_hex(3, "read", powpan_answer, ret);
252 return ret;
253 }
254
powpan_instcmd(const char * cmdname,const char * extra)255 static int powpan_instcmd(const char *cmdname, const char *extra)
256 {
257 int i;
258
259 for (i = 0; cmdtab[i].cmd != NULL; i++) {
260
261 if (strcasecmp(cmdname, cmdtab[i].cmd)) {
262 continue;
263 }
264
265 if ((powpan_command(cmdtab[i].command, cmdtab[i].len) ==
266 cmdtab[i].len - 1) &&
267 (!memcmp(powpan_answer, cmdtab[i].command, cmdtab[i].len - 1))) {
268 return STAT_INSTCMD_HANDLED;
269 }
270
271 upslogx(LOG_ERR, "%s: command [%s] failed", __func__, cmdname);
272 return STAT_INSTCMD_FAILED;
273 }
274
275 upslogx(LOG_ERR, "%s: command [%s] not found", __func__, cmdname);
276 return STAT_INSTCMD_UNKNOWN;
277 }
278
powpan_setvar(const char * varname,const char * val)279 static int powpan_setvar(const char *varname, const char *val)
280 {
281 char command[SMALLBUF];
282 int i, j;
283
284 for (i = 0; vartab[i].var != NULL; i++) {
285
286 if (strcasecmp(varname, vartab[i].var)) {
287 continue;
288 }
289
290 if (!strcasecmp(val, dstate_getinfo(varname))) {
291 upslogx(LOG_INFO, "powpan_setvar: [%s] no change for variable [%s]", val, varname);
292 return STAT_SET_HANDLED;
293 }
294
295 for (j = 0; vartab[i].map[type][j].val != NULL; j++) {
296
297 if (strcasecmp(val, vartab[i].map[type][j].val)) {
298 continue;
299 }
300
301 snprintf(command, sizeof(command), vartab[i].set,
302 vartab[i].map[type][j].command);
303
304 if ((powpan_command(command, 4) == 3) && (!memcmp(powpan_answer, command, 3))) {
305 dstate_setinfo(varname, "%s", val);
306 return STAT_SET_HANDLED;
307 }
308
309 upslogx(LOG_ERR, "powpan_setvar: setting variable [%s] to [%s] failed", varname, val);
310 return STAT_SET_UNKNOWN;
311 }
312
313 upslogx(LOG_ERR, "powpan_setvar: [%s] is not valid for variable [%s]", val, varname);
314 return STAT_SET_UNKNOWN;
315 }
316
317 upslogx(LOG_ERR, "powpan_setvar: variable [%s] not found", varname);
318 return STAT_SET_UNKNOWN;
319 }
320
powpan_initinfo(void)321 static void powpan_initinfo(void)
322 {
323 int i, j;
324 char *s;
325
326 dstate_setinfo("ups.delay.start", "%d", 45);
327 dstate_setinfo("ups.delay.shutdown", "%d", 0); /* almost immediate */
328
329 /*
330 * NOTE: The reply is already in the buffer, since the F\r command
331 * was used for autodetection of the UPS. No need to do it again.
332 */
333 if ((s = strtok((char *)&powpan_answer[1], ".")) != NULL) {
334 for (i = 0; modeltab[i].val != NULL; i++) {
335 if (!strncmp(s, modeltab[i].val, strlen(modeltab[i].val))) {
336 break;
337 }
338 }
339 if (modeltab[i].model) {
340 /* model found in table */
341 dstate_setinfo("ups.model", "%s", modeltab[i].model);
342 } else {
343 /* report model value as is */
344 dstate_setinfo("ups.model", "%s", str_rtrim(s, ' '));
345 }
346 }
347 if ((s = strtok(NULL, ".")) != NULL) {
348 dstate_setinfo("input.voltage.nominal", "%d", (unsigned char)s[0]);
349 }
350 if ((s = strtok(NULL, ".")) != NULL) {
351 dstate_setinfo("input.frequency.nominal", "%d", (unsigned char)s[0]);
352 }
353 if ((s = strtok(NULL, ".")) != NULL) {
354 dstate_setinfo("ups.firmware", "%c.%c%c%c", s[0], s[1], s[2], s[3]);
355 }
356
357 for (i = 0; cmdtab[i].cmd != NULL; i++) {
358 dstate_addcmd(cmdtab[i].cmd);
359 }
360
361 for (i = 0; vartab[i].var != NULL; i++) {
362
363 if (powpan_command(vartab[i].get, 3) < 2) {
364 continue;
365 }
366
367 for (j = 0; vartab[i].map[type][j].val != NULL; j++) {
368
369 if (vartab[i].map[type][j].command != powpan_answer[1]) {
370 continue;
371 }
372
373 dstate_setinfo(vartab[i].var, "%s", vartab[i].map[type][j].val);
374 break;
375 }
376
377 if (dstate_getinfo(vartab[i].var) == NULL) {
378 upslogx(LOG_WARNING, "warning: [%d] unknown value for [%s]!",
379 powpan_answer[1], vartab[i].var);
380 continue;
381 }
382
383 dstate_setflags(vartab[i].var, ST_FLAG_RW);
384
385 for (j = 0; vartab[i].map[type][j].val != NULL; j++) {
386 dstate_addenum(vartab[i].var, "%s", vartab[i].map[type][j].val);
387 }
388 }
389
390 /*
391 * FIXME: The following commands need to be reverse engineered. It
392 * looks like they are used in detecting the UPS model. To rule out
393 * any incompatibilities, only use them when running in debug mode.
394 */
395 if (nut_debug_level > 2) {
396 powpan_command("R\x29\r", 3);
397 powpan_command("R\x2B\r", 3);
398 powpan_command("R\x3D\r", 3);
399 }
400
401 dstate_addcmd("shutdown.stayoff");
402 dstate_addcmd("shutdown.reboot");
403 }
404
powpan_status(status_t * status)405 static int powpan_status(status_t *status)
406 {
407 int ret;
408
409 ser_flush_io(upsfd);
410
411 /*
412 * WRITE D\r
413 * READ #VVL.CTF.....\r
414 * 01234567890123
415 */
416 ret = ser_send_pace(upsfd, UPSDELAY, "D\r");
417
418 if (ret < 0) {
419 upsdebug_with_errno(3, "send");
420 return -1;
421 }
422
423 if (ret == 0) {
424 upsdebug_with_errno(3, "send: timeout");
425 return -1;
426 }
427
428 upsdebug_hex(3, "send", "D\r", 2);
429
430 usleep(200000);
431
432 ret = ser_get_buf_len(upsfd, status, sizeof(*status), SER_WAIT_SEC, SER_WAIT_USEC);
433
434 if (ret < 0) {
435 upsdebug_with_errno(3, "read");
436 upsdebug_hex(4, " \\_", status, sizeof(*status));
437 return -1;
438 }
439
440 if (ret == 0) {
441 upsdebugx(3, "read: timeout");
442 upsdebug_hex(4, " \\_", status, sizeof(*status));
443 return -1;
444 }
445
446 upsdebug_hex(3, "read", status, ret);
447
448 if ((status->flags[0] + status->flags[1]) != 255) {
449 upsdebugx(4, " \\_ : checksum flags[0..1] failed");
450 return -1;
451 }
452
453 if ((status->flags[2] + status->flags[3]) != 255) {
454 upsdebugx(4, " \\_ : checksum flags[2..3] failed");
455 return -1;
456 }
457
458 return 0;
459 }
460
powpan_updateinfo(void)461 static int powpan_updateinfo(void)
462 {
463 status_t status;
464
465 if (powpan_status(&status)) {
466 return -1;
467 }
468
469 switch (type)
470 {
471 case OP:
472 dstate_setinfo("input.voltage", "%d", op_volt(status.i_volt));
473 if (status.flags[0] & 0x84) {
474 dstate_setinfo("output.voltage", "%s", dstate_getinfo("output.voltage.nominal"));
475 } else {
476 dstate_setinfo("output.voltage", "%d", op_volt(status.i_volt));
477 }
478 dstate_setinfo("ups.load", "%d", op_load(status.o_load));
479 dstate_setinfo("battery.charge", "%d", op_chrg(status.b_chrg));
480 dstate_setinfo("ups.temperature", "%.1f", op_temp(status.u_temp));
481 dstate_setinfo("input.frequency", "%.1f", op_freq(status.i_freq));
482 break;
483
484 default:
485 dstate_setinfo("input.voltage", "%d", status.i_volt);
486 if (status.flags[0] & 0x84) {
487 dstate_setinfo("output.voltage", "%s", dstate_getinfo("output.voltage.nominal"));
488 } else {
489 dstate_setinfo("output.voltage", "%d", status.o_volt);
490 }
491 dstate_setinfo("ups.load", "%d", status.o_load);
492 dstate_setinfo("battery.charge", "%d", status.b_chrg);
493 dstate_setinfo("ups.temperature", "%d", status.u_temp);
494 dstate_setinfo("input.frequency", "%.1f", (status.i_freq == 0) ? 0.0 : 45.0 + (float)status.i_freq / 10.0);
495 break;
496 }
497
498 if (status.flags[0] & 0x01) {
499 dstate_setinfo("ups.beeper.status", "enabled");
500 } else {
501 dstate_setinfo("ups.beeper.status", "disabled");
502 }
503
504 status_init();
505
506 if (status.flags[0] & 0x80) {
507 status_set("OB");
508 } else {
509 status_set("OL");
510 }
511
512 if (status.flags[0] & 0x40) {
513 status_set("LB");
514 }
515
516 /* !OB && !TEST */
517 if (!(status.flags[0] & 0x84)) {
518
519 if (status.o_volt < 0.5 * status.i_volt) {
520 upsdebugx(2, "%s: output voltage too low", __func__);
521 } else if (status.o_volt < 0.95 * status.i_volt) {
522 status_set("TRIM");
523 } else if (status.o_volt < 1.05 * status.i_volt) {
524 upsdebugx(2, "%s: appears to be in BYPASS state", __func__);
525 } else if (status.o_volt < 1.5 * status.i_volt) {
526 status_set("BOOST");
527 } else {
528 upsdebugx(2, "%s: output voltage too high", __func__);
529 }
530 }
531
532 if (status.flags[0] & 0x04) {
533 status_set("TEST");
534 }
535
536 if (status.flags[0] == 0) {
537 status_set("OFF");
538 }
539
540 status_commit();
541
542 return (status.flags[0] & 0x80) ? 1 : 0;
543 }
544
powpan_initups(void)545 static int powpan_initups(void)
546 {
547 int ret, i;
548
549 upsdebugx(1, "Trying %s protocol...", powpan_binary.version);
550
551 ser_set_speed(upsfd, device_path, B1200);
552
553 /* This fails for many devices, so don't bother to complain */
554 ser_send_pace(upsfd, UPSDELAY, "\r\r");
555
556 for (i = 0; i < MAXTRIES; i++) {
557
558 ser_flush_io(upsfd);
559
560 /*
561 * WRITE F\r
562 * READ .PR2200 .x.<.1100
563 * 01234567890123456789
564 */
565 ret = ser_send_pace(upsfd, UPSDELAY, "F\r");
566
567 if (ret < 0) {
568 upsdebug_with_errno(3, "send");
569 continue;
570 }
571
572 if (ret == 0) {
573 upsdebug_with_errno(3, "send: timeout");
574 continue;
575 }
576
577 upsdebug_hex(3, "send", "F\r", 2);
578
579 usleep(200000);
580
581 ret = ser_get_line(upsfd, powpan_answer, sizeof(powpan_answer),
582 ENDCHAR, IGNCHAR, SER_WAIT_SEC, SER_WAIT_USEC);
583
584 if (ret < 0) {
585 upsdebug_with_errno(3, "read");
586 upsdebug_hex(4, " \\_", powpan_answer, strlen((char *)powpan_answer));
587 continue;
588 }
589
590 if (ret == 0) {
591 upsdebugx(3, "read: timeout");
592 upsdebug_hex(4, " \\_", powpan_answer, strlen((char *)powpan_answer));
593 continue;
594 }
595
596 upsdebug_hex(3, "read", powpan_answer, ret);
597
598 if (ret < 20) {
599 upsdebugx(2, "Expected 20 bytes but only got %d", ret);
600 continue;
601 }
602
603 if (powpan_answer[0] != '.') {
604 upsdebugx(2, "Expected start character '.' but got '%c'", (char)powpan_answer[0]);
605 continue;
606 }
607
608 /* See if we need to use the 'old' protocol for the OP series */
609 if (!strncmp((char *)&powpan_answer[1], "OP", 2)) {
610 type = OP;
611 }
612
613 /* This is for an older model series, that reports the model numerically */
614 if (!strncmp((char *)&powpan_answer[1], "rO", 2)) {
615 type = OP;
616 }
617
618 if (getval("ondelay")) {
619 fatalx(EXIT_FAILURE, "Setting 'ondelay' not supported by %s driver", powpan_binary.version);
620 }
621
622 if (getval("offdelay")) {
623 fatalx(EXIT_FAILURE, "Setting 'offdelay' not supported by %s driver", powpan_binary.version);
624 }
625
626 return ret;
627 }
628
629 return -1;
630 }
631
632 subdriver_t powpan_binary = {
633 "binary",
634 powpan_instcmd,
635 powpan_setvar,
636 powpan_initups,
637 powpan_initinfo,
638 powpan_updateinfo
639 };
640