/* scan_eaton_serial.c: detect Eaton serial XCP, SHUT and Q1 devices * * Copyright (C) 2012 Arnaud Quette * * 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 "common.h" /* Need this on AIX when using xlc to get alloca */ #ifdef _AIX #pragma alloca #endif /* _AIX */ #include #include #include #include #include #include #include "nut-scan.h" #include "serial.h" #include "bcmxcp_io.h" #include "bcmxcp.h" #include "nutscan-serial.h" #ifdef HAVE_PTHREAD #include #endif /* SHUT header */ #define SHUT_SYNC 0x16 #define MAX_TRY 4 /* BCMXCP header */ extern unsigned char AUT[4]; extern struct pw_baud_rate { int rate; int name; } pw_baud_rates[]; /* Local list of found devices */ static nutscan_device_t * dev_ret = NULL; /* Remap some functions to avoid undesired behavior (drivers/main.c) */ char *getval(const char *var) { return NULL; } #ifdef HAVE_PTHREAD static pthread_mutex_t dev_mutex; #endif /* Drivers name */ #define SHUT_DRIVER_NAME "mge-shut" #define XCP_DRIVER_NAME "bcmxcp" #define Q1_DRIVER_NAME "blazer_ser" /* Fake driver main, for using serial functions, needed for bcmxcp_ser.c */ char *device_path; int upsfd; int exit_flag = 0; int do_lock_port; /* Functions extracted from drivers/bcmxcp.c, to avoid pulling too many things * lightweight function to calculate the 8-bit * two's complement checksum of buf, using XCP data length (including header) * the result must be 0 for the sequence data to be valid */ int checksum_test(const unsigned char *buf) { unsigned char checksum = 0; int i, length; /* buf[2] is the length of the XCP frame ; add 5 for the header */ length = (int)(buf[2]) + 5; for (i = 0; i < length; i++) { checksum += buf[i]; } /* Compute the 8-bit, Two's Complement checksum now and return it */ checksum = ((0x100 - checksum) & 0xFF); return (checksum == 0); } unsigned char calc_checksum(const unsigned char *buf) { unsigned char c; int i; c = 0; for(i = 0; i < 2 + buf[1]; i++) c -= buf[i]; return c; } /******************************************************************************* * SHUT functions (MGE legacy, but Eaton path forward) ******************************************************************************/ /* Light version of of drivers/libshut.c->shut_synchronise() * return 1 if OK, 0 otherwise */ int shut_synchronise(int upsfd) { int try; u_char reply = '\0'; /* Sync with the UPS according to notification */ for (try = 0; try < MAX_TRY; try++) { if ((ser_send_char(upsfd, SHUT_SYNC)) == -1) { continue; } ser_get_char(upsfd, &reply, 1, 0); if (reply == SHUT_SYNC) { return 1; } } return 0; } /* SHUT scan: * send SYNC token (0x16) and receive the SYNC token back * FIXME: maybe try to get device descriptor?! */ nutscan_device_t * nutscan_scan_eaton_serial_shut(const char* port_name) { nutscan_device_t * dev = NULL; int devfd = -1; if ( (devfd = ser_open_nf(port_name)) != -1 ) { /* set RTS to off and DTR to on to allow correct behavior * with UPS using PnP feature */ if (ser_set_dtr(devfd, 1) != -1) { ser_set_rts(devfd, 0); ser_set_speed_nf(devfd, port_name, B2400); if (shut_synchronise(devfd)) { /* Communication established successfully! */ dev = nutscan_new_device(); dev->type = TYPE_EATON_SERIAL; dev->driver = strdup(SHUT_DRIVER_NAME); dev->port = strdup(port_name); #ifdef HAVE_PTHREAD pthread_mutex_lock(&dev_mutex); #endif dev_ret = nutscan_add_device_to_device(dev_ret, dev); #ifdef HAVE_PTHREAD pthread_mutex_unlock(&dev_mutex); #endif } } /* Close the device */ ser_close(devfd, NULL); } return dev; } /******************************************************************************* * XCP functions (Eaton Powerware legacy) ******************************************************************************/ /* XCP scan: * baudrate nego (...) * Send ESC to take it out of menu * Wait 90ms * Send auth command (AUTHOR[4] = {0xCF, 0x69, 0xE8, 0xD5};) * Wait 500ms (or less?) * Send PW_SET_REQ_ONLY_MODE command (0xA0) and wait for response * [Get ID Block (PW_ID_BLOCK_REQ) (0x31)] */ nutscan_device_t * nutscan_scan_eaton_serial_xcp(const char* port_name) { nutscan_device_t * dev = NULL; int i, ret, devfd = -1; unsigned char answer[256]; unsigned char sbuf[128]; memset(sbuf, 0, 128); if ( (devfd = ser_open_nf(port_name)) != -1 ) { #ifdef HAVE_PTHREAD pthread_mutex_lock(&dev_mutex); #endif upsfd = devfd; #ifdef HAVE_PTHREAD pthread_mutex_unlock(&dev_mutex); #endif for (i=0; (pw_baud_rates[i].rate != 0) && (dev == NULL); i++) { memset(answer, 0, 256); if (ser_set_speed_nf(devfd, port_name, pw_baud_rates[i].rate) == -1) break; ret = ser_send_char(devfd, 0x1d); /* send ESC to take it out of menu */ if (ret <= 0) break; usleep(90000); send_write_command(AUT, 4); usleep(500000); /* Discovery with Baud Hunting (XCP protocol spec. ยง4.1.2) * sending PW_SET_REQ_ONLY_MODE should be enough, since * the unit should send back Identification block */ sbuf[0] = PW_COMMAND_START_BYTE; sbuf[1] = (unsigned char)1; sbuf[2] = PW_SET_REQ_ONLY_MODE; sbuf[3] = calc_checksum(sbuf); ret = ser_send_buf_pace(devfd, 1000, sbuf, 4); /* Read PW_COMMAND_START_BYTE byte */ ret = ser_get_char(devfd, answer, 1, 0); #if 0 /* FIXME: seems not needed, but requires testing with more devices! */ if (ret <= 0) { usleep(250000); /* 500000? */ memset(answer, 0, 256); ret = command_sequence(&id_command, 1, answer); } #endif if ( (ret > 0) && (answer[0] == PW_COMMAND_START_BYTE) ) { dev = nutscan_new_device(); dev->type = TYPE_EATON_SERIAL; dev->driver = strdup(XCP_DRIVER_NAME); dev->port = strdup(port_name); #ifdef HAVE_PTHREAD pthread_mutex_lock(&dev_mutex); #endif dev_ret = nutscan_add_device_to_device(dev_ret, dev); #ifdef HAVE_PTHREAD pthread_mutex_unlock(&dev_mutex); #endif break; } usleep(100000); } /* Close the device */ ser_close(devfd, NULL); } return dev; } /******************************************************************************* * Q1 functions (Phoenixtec/Centralion/Santak, still Eaton path forward) ******************************************************************************/ #define SER_WAIT_SEC 1 /* 3 seconds for Best UPS */ #define MAXTRIES 3 /* Q1 scan: * - open the serial port and set the speed to 2400 baud * - simply try to get Q1 (status) string * - check its size and first char. which should be '(' */ nutscan_device_t * nutscan_scan_eaton_serial_q1(const char* port_name) { nutscan_device_t * dev = NULL; struct termios tio; int ret = 0, retry; int devfd = -1; char buf[128]; if ( (devfd = ser_open_nf(port_name)) != -1 ) { if (ser_set_speed_nf(devfd, port_name, B2400) != -1) { if (!tcgetattr(devfd, &tio)) { /* Use canonical mode input processing (to read reply line) */ tio.c_lflag |= ICANON; /* Canonical input (erase and kill processing) */ tio.c_cc[VEOF] = _POSIX_VDISABLE; tio.c_cc[VEOL] = '\r'; tio.c_cc[VERASE] = _POSIX_VDISABLE; tio.c_cc[VINTR] = _POSIX_VDISABLE; tio.c_cc[VKILL] = _POSIX_VDISABLE; tio.c_cc[VQUIT] = _POSIX_VDISABLE; tio.c_cc[VSUSP] = _POSIX_VDISABLE; tio.c_cc[VSTART] = _POSIX_VDISABLE; tio.c_cc[VSTOP] = _POSIX_VDISABLE; if (!tcsetattr(devfd, TCSANOW, &tio)) { /* Set the default (normal) cablepower */ ser_set_dtr(devfd, 1); ser_set_rts(devfd, 0); /* Allow some time to settle for the cablepower */ usleep(100000); /* Only try pure 'Q1', not older ones like 'D' or 'QS' * > [Q1\r] * < [(226.0 195.0 226.0 014 49.0 27.5 30.0 00001000\r] */ for (retry = 1; retry <= MAXTRIES; retry++) { /* simplified code */ ser_flush_io(devfd); if ( (ret = ser_send(devfd, "Q1\r")) > 0) { /* Get Q1 reply */ if ( (ret = ser_get_buf(devfd, buf, sizeof(buf), SER_WAIT_SEC, 0)) > 0) { /* Check answer */ /* should at least (and most) be 46 chars */ if (ret >= 46) { if (buf[0] == '(') { dev = nutscan_new_device(); dev->type = TYPE_EATON_SERIAL; dev->driver = strdup(Q1_DRIVER_NAME); dev->port = strdup(port_name); #ifdef HAVE_PTHREAD pthread_mutex_lock(&dev_mutex); #endif dev_ret = nutscan_add_device_to_device(dev_ret, dev); #ifdef HAVE_PTHREAD pthread_mutex_unlock(&dev_mutex); #endif break; } } } } } } } } /* Close the device */ ser_close(devfd, NULL); } return dev; } static void * nutscan_scan_eaton_serial_device(void * port_arg) { nutscan_device_t * dev = NULL; char* port_name = (char*) port_arg; /* Try SHUT first */ if ( (dev = nutscan_scan_eaton_serial_shut(port_name)) == NULL) { usleep(100000); /* Else, try XCP */ if ( (dev = nutscan_scan_eaton_serial_xcp(port_name)) == NULL) { /* Else, try Q1 */ usleep(100000); dev = nutscan_scan_eaton_serial_q1(port_name); } /* Else try UTalk? */ } return dev; } nutscan_device_t * nutscan_scan_eaton_serial(const char* ports_range) { struct sigaction oldact; int change_action_handler = 0; char *current_port_name = NULL; char **serial_ports_list; int current_port_nb; int i; #ifdef HAVE_PTHREAD pthread_t thread; pthread_t * thread_array = NULL; int thread_count = 0; pthread_mutex_init(&dev_mutex,NULL); #endif /* 1) Get ports_list */ serial_ports_list = nutscan_get_serial_ports_list(ports_range); if( serial_ports_list == NULL ) { return NULL; } /* Ignore SIGPIPE if the caller hasn't set a handler for it yet */ if( sigaction(SIGPIPE, NULL, &oldact) == 0 ) { if( oldact.sa_handler == SIG_DFL ) { change_action_handler = 1; signal(SIGPIPE,SIG_IGN); } } /* port(s) iterator */ current_port_nb = 0; while(serial_ports_list[current_port_nb] != NULL) { current_port_name = serial_ports_list[current_port_nb]; #ifdef HAVE_PTHREAD if (pthread_create(&thread, NULL, nutscan_scan_eaton_serial_device, (void*)current_port_name) == 0){ thread_count++; thread_array = realloc(thread_array, thread_count*sizeof(pthread_t)); thread_array[thread_count-1] = thread; } #else nutscan_scan_eaton_serial_device(current_port_name); #endif current_port_nb++; } #ifdef HAVE_PTHREAD for ( i = 0; i < thread_count ; i++) { pthread_join(thread_array[i],NULL); } pthread_mutex_destroy(&dev_mutex); free(thread_array); #endif if(change_action_handler) { signal(SIGPIPE,SIG_DFL); } /* free everything... */ i=0; while(serial_ports_list[i] != NULL) { free(serial_ports_list[i]); i++; } free( serial_ports_list); return nutscan_rewind_device(dev_ret); }