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