/*
x10d.c (c) Copyright Daniel D. Lanciani 1995-1998
All rights reserved.

x10d.c is licensed free of charge for personal and internal
business use only.  x10d.c may not be distributed for profit,
nor may it be included in products or otherwise distributed by
commercial entities to their clients or customers without the
prior written permission of the author.

TO THE EXTENT ALLOWED BY APPLICABLE LAW, x10d.c IS PROVIDED
"AS IS", WITH NO EXPRESS OR IMPLIED WARRANTY, INCLUDING, BUT
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE.

IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW WILL THE AUTHOR BE
LIABLE FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL
OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO
USE x10d.c EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY
OF SUCH DAMAGES.

These copyright, license, and disclaimer notices must be included
with all copies of x10d.c.  Modified versions of x10d.c must be
marked as such.

ddl@danlan.com
*/

#include <sys/types.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <stdio.h>
#include <signal.h>
#include <syslog.h>

#define WIN 2

#ifdef __linux__
#include <string.h>
#include <termios.h>
 
struct termios oldsb, newsb;
#else
struct sgttyb sgttyb;
#endif

char rbuf[128], buf[128];
int lsocket, n, nclient, maxclient = 20, on = 1, off = 0;
static struct sockaddr_in sin = { AF_INET };
struct linger linger = { 1, 0 };

char *ctime(), *memchr(), *malloc(), *realloc();

struct clients {
	int s, cnt, flags;
	char buf[128];
} *clients;

#define C_QUIET 0x0001

#define X10_ALL_UNITS_OFF 17
#define X10_ALL_LIGHTS_ON 18
#define X10_ON 19
#define X10_OFF 20
#define X10_DIM 21
#define X10_BRIGHT 22
#define X10_STATUS_ON 30
#define X10_STATUS_OFF 31
#define X10_STATUS_REQUEST 32

struct x10aliases {
	char *name;
	int code;
} x10aliases[] = {
	{ "ALLUNITSOFF", X10_ALL_UNITS_OFF },
	{ "ALLLIGHTSON", X10_ALL_LIGHTS_ON },
	{ "ON", X10_ON },
	{ "OFF", X10_OFF },
	{ "DIM", X10_DIM },
	{ "BRIGHT", X10_BRIGHT },
	{ 0, 0 }
};

char *x10longnames[] = {
	"",
	"Unit 1",
	"Unit 2",
	"Unit 3",
	"Unit 4",
	"Unit 5",
	"Unit 6",
	"Unit 7",
	"Unit 8",
	"Unit 9",
	"Unit 10",
	"Unit 11",
	"Unit 12",
	"Unit 13",
	"Unit 14",
	"Unit 15",
	"Unit 16",
	"All Units Off",
	"All Lights On",
	"On",
	"Off",
	"Dim",
	"Bright",
	"All Lights Off",
	"Extended Code",
	"Hail Request",
	"Hail Acknowledge",
	"Preset Dim 0",
	"Preset Dim 1",
	"Extended Data",
	"Status is On",
	"Status is Off",
	"Status Request"
};

#ifndef __linux__
#define FD_NULL		(struct fd_set *) 0
#define TIMEVAL_NULL	(struct timeval *) 0
#else
#define FD_NULL		(fd_set *) 0
#define TIMEVAL_NULL	(struct timeval *) 0
#endif

main(argc, argv)
char **argv;
{
	register FILE *m;
	register int i, c;
	register char *p;
	int l;
	struct servent *sp;
	fd_set rset, probe;

	if(fork())
		exit(0);
	close(0);
	close(1);
	close(2);
	if(open("/", 0)) {
		syslog(LOG_CRIT, "x10d: open / not 0: %m");
		exit(1);
	}
	dup2(0, 1);
	dup2(0, 2);
	signal(SIGPIPE, SIG_IGN);
#ifdef	LOG_LOCAL0
	openlog("x10d", LOG_PID, LOG_LOCAL0);
#else
	openlog("x10d", LOG_PID, 0);
#endif
	syslog(LOG_INFO, "starting");
	if(!(clients = (struct clients *)
		malloc(maxclient * sizeof(struct clients)))) {
		syslog(LOG_CRIT, "no memory for clients");
		exit(1);
	}
	FD_ZERO(&rset);
	if(!(sp = getservbyname("hamx10", "tcp"))) {
		syslog(LOG_ERR, "hamx10: unknown service");
		exit(1);
	}
	if((lsocket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
		syslog(LOG_ERR, "socket: %m");
		exit(1);
	}
	sin.sin_port = sp->s_port;
	if(bind(lsocket, &sin, sizeof(sin))) {
		syslog(LOG_ERR, "bind: %m");
		exit(1);
	}
	listen(lsocket, 5);
	ioctl(lsocket, FIONBIO, &on);
	FD_SET(lsocket, &rset);
	if((n = open("/dev/lynx10", 2)) < 0) {
		syslog(LOG_ERR, "/dev/lynx10: %m");
		exit(1);
	}
	if((c = open("/dev/tty", 2)) >= 0) {
		ioctl(c, TIOCNOTTY, 0);
		close(c);
	}
#ifndef __linux__
	ioctl(n, TIOCGETP, &sgttyb);
	sgttyb.sg_ispeed = sgttyb.sg_ospeed = B1200;
	sgttyb.sg_flags = CRMOD;
	ioctl(n, TIOCSETP, &sgttyb);
	l = LLITOUT;
	ioctl(n, TIOCLBIS, &l);
#else
 	/*
 	** This works for heyu (NJC)
 	*/
 
 	{
 	  i = tcgetattr(n, &oldsb);
 
 	  if (i < 0) {
 	    p = strerror(errno);
            syslog(LOG_CRIT, p);
 	    exit(errno);
 	  }
 
 	  newsb = oldsb;
 
 	  newsb.c_iflag = IGNBRK | IGNPAR;
          /*newsb.c_oflag = 0;*/
          newsb.c_oflag = ONLRET;
          newsb.c_lflag = 0;
          /*newsb.c_lflag = ISIG;*/
 	  newsb.c_cflag = (CLOCAL | B1200 | CS8 | CREAD);
 	  for (i = 0; i < NCC; i++)
 	    newsb.c_cc[i] = 0;
 	  newsb.c_cc[VMIN]   = 1;
 	  newsb.c_cc[VTIME]  = 0;
 
 	  tcsetattr(n, TCSADRAIN, &newsb);
 	}
#endif
	ioctl(n, FIONBIO, &on);
	FD_SET(n, &rset);
	while(1) {
		probe = rset;
		if(select(FD_SETSIZE, &probe, FD_NULL,
			FD_NULL, (struct timeval *)0) <= 0) {
			syslog(LOG_ERR, "select: %m");
			sleep(2);
			continue;
		}
		if(FD_ISSET(lsocket, &probe)) {
			l = sizeof(sin);
			if((c = accept(lsocket, &sin, &l)) < 0)
				goto badaccept;
			ioctl(c, FIONBIO, &on);
			if(m = fopen("/usr/local/lib/hcs/x10d.access", "r")) {
                                strcpy(buf,  "");	/* these are used elsewhere so clear them */
                                strcpy(rbuf, "");
				while(fscanf(m, "%s %s\n", buf, rbuf) == 2)
					if((sin.sin_addr.s_addr &
						inet_addr(rbuf)) ==
						inet_addr(buf))
						break;
				fclose(m);
				if((sin.sin_addr.s_addr & inet_addr(rbuf)) !=
					inet_addr(buf)) {
					close(c);
					goto badaccept;
				}
			}
			if(nclient >= maxclient) {
				maxclient += 20;
				if(!(clients = (struct clients *)realloc(
					(char *)clients, maxclient *
					sizeof(struct clients)))) {
					syslog(LOG_CRIT,"out of client memory");
					exit(1);
				}
			}
			setsockopt(c, SOL_SOCKET, SO_LINGER, &linger,
				sizeof(linger));
			clients[nclient].flags = clients[nclient].cnt = 0;
			clients[nclient++].s = c;
			FD_SET(c, &rset);
		badaccept:;
		}
		for(i = 0; i < nclient; i++)
			if(FD_ISSET(clients[i].s, &probe)) {
				c = sizeof(clients[i].buf) - clients[i].cnt - 1;
				if(c <= 0)
					goto badclient;
				c = read(clients[i].s,
					clients[i].buf + clients[i].cnt, c);
				if(c < 0) {
					if(errno != EWOULDBLOCK)
						goto badclient;
					continue;
				}
				if(c == 0) {
				badclient:
					close(clients[i].s);
					FD_CLR(clients[i].s, &rset);
					nclient--;
					for(c = i; c < nclient; c++)
						clients[c] = clients[c + 1];
					i--;
					continue;
				}
				clients[i].cnt += c;
				while(p = memchr(clients[i].buf, '\n',
					clients[i].cnt)) {
				c = p - clients[i].buf + 1;
				bcopy(clients[i].buf, rbuf, c);
				if(clients[i].cnt -= c)
					bcopy(clients[i].buf + c,
						clients[i].buf, clients[i].cnt);
				for(p = buf, l = c, c = 0; c < l; c++) {
					if(rbuf[c] < ' ') {
						*p = 0;
						break;
					}
					if(rbuf[c] == ' ')
						continue;
					if(rbuf[c] >= 'a' && rbuf[c] <= 'z')
						*p++ = rbuf[c] - 'a' + 'A';
					else
						*p++ = rbuf[c];
				}
				parse(buf, i);
				}
			}
		if(FD_ISSET(n, &probe)) {
			c = read(n, rbuf, sizeof(rbuf));
			if(c < 0) {
				if(errno != EWOULDBLOCK) {
					syslog(LOG_ERR, "read x10: %m");
					sleep(1);
				}
				goto badread;
			}
			if(c == 0) {
				syslog(LOG_ERR, "read x10: EOF");
				sleep(1);
				goto badread;
			}
			if(c == 1)
				goto badread;
			rbuf[c - 1] = 0;
			if(*rbuf == 'X' && strlen(rbuf) == 4)
				processx(rbuf + 1, 0);
			else
				syslog(LOG_ERR, "unrecognized cmd: %s", rbuf);
		badread:;
		}
	}
}

char modstate[16][16];

processx(cmd, flag)
char *cmd;
{
	register int i, c, func, house = xdtoi(cmd[1]) + 'A';
	register char *s = modstate[house - 'A'];
	char buf[128];
	FILE *m;
	u_long t;

	func = xdtoi(cmd[2]) + 1;
	if(*cmd == '1')
		func += 16;
	if(func <= 16) {
		for(i = 0; i < 16; i++)
			if(s[i] == 2)
				s[i] = 0;
		s[func - 1] = 1;
	}
	else switch(func) {

		case X10_ALL_UNITS_OFF:
			bzero(s, 16);
			break;

		case X10_ON:
		case X10_OFF:
		case X10_DIM:
		case X10_BRIGHT:
		case X10_ALL_LIGHTS_ON:
			for(i = 0; i < 16; i++)
				if(s[i] == 1)
					s[i] = 2;
			break;
	}
	for(i = c = 0; i < 16; i++)
		if(s[i])
			c |= (1 << i);
	if((m = fopen("/usr/local/lib/hcs/x10.log", "a"))) {
		time(&t);
		fprintf(m, "%c %c%02d %u %s %s", flag ? 'X' : 'R', house, func,
			c, x10longnames[func], ctime(&t));
		fclose(m);
	}
	sprintf(buf, "R %c%02d %u %s\r\n", house, func, c, x10longnames[func]);
	c = strlen(buf);
	for(i = 0; i < nclient; i++) {
		if(clients[i].flags & C_QUIET)
			continue;
		if(write(clients[i].s, buf, c) != c)
			shutdown(clients[i].s, 2);
	}
}

parse(i, c)
register char *i;
{
	register struct x10aliases *x;
	register int code, j;
	int repeat = 1, win = 0, gavemsg = 0, k;
	static char cmd[4] = "X";
	static struct timeval tv;
	fd_set xprobe;
	char dbuf[50], *si = 0;

	FD_ZERO(&xprobe);
top:
	while(*i) {
		if(*i == 'Q') {
			i++;
			clients[c].flags ^= C_QUIET;
			continue;
		}
		if(*i == 'R') {
			i++;
			if(*i > '9' || *i < '0')
				goto syntax;
			repeat = 0;
			while(*i <= '9' && *i >= '0')
				repeat = 10 * repeat + *i++ - '0';
			if(!repeat)
				repeat = 1;
			if(repeat > 15)
				repeat = 15;
			continue;
		}
		if(*i == 'S') {
			i++;
			if(*i < 'A' || *i > 'P')
				goto syntax;
			k = 0;
			code = *i++ - 'A';
			for(j = 0; j < 16; j++)
				if(modstate[code][j])
					k |= 1 << j;
			if(*i > '9' || *i < '0')
				goto syntax;
			j = 0;
			while(*i <= '9' && *i >= '0')
				j = 10 * j + *i++ - '0';
			if(j == k || !j)
				continue;
			k = j;
			code += 'A';
			si = dbuf;
			for(j = 0; j < 16; j++)
				if(k & (1 << j)) {
					sprintf(si, "%c%d", code, j + 1);
					si += strlen(si);
				}
			si = i;
			i = dbuf;
			continue;
		}
		if(*i < 'A' || *i > 'P') {
		syntax:
			write(clients[c].s, "E Syntax\r\n", 10);
			return;
		}
		cmd[2] = *i++ - 'A' + '0';
		if(*i <= '9' && *i >= '0') {
			code = 0;
			while(*i <= '9' && *i >= '0')
				code = 10 * code + *i++ - '0';
			if(!code || code > 32)
				goto syntax;
		}
		else {
			for(x = x10aliases; x->name; x++)
				if(!memcmp(i, x->name, strlen(x->name)))
					break;
			if(!x->name)
				goto syntax;
			code = x->code;
			i += strlen(x->name);
		}
		code--;
		if(code > 15) {
			code -= 16;
			cmd[1] = '1';
		}
		else
			cmd[1] = '0';
		if(code < 10)
			cmd[3] = code + '0';
		else
			cmd[3] = code + 'A' - 10;
		write(n, cmd, 4);
		processx(cmd + 1, 1);
		win++;
	again:
		FD_SET(n, &xprobe);
		if(win > WIN)
			tv.tv_sec = 10;
		else
			tv.tv_sec = 0;
		code = select(n + 1, &xprobe, FD_NULL,
			FD_NULL, &tv);
		if(code < 0) {
			syslog(LOG_ERR, "select r: %m");
			sleep(2);
			goto again;
		}
		if(code == 0) {
			if(win <= WIN)
				continue;
			syslog(LOG_ERR, "select x10 timed out");
			write(clients[c].s, "E device timeout\r\n", 18);
			return;
		}
		code = read(n, rbuf, sizeof(rbuf));
		if(code <= 0) {
			if(code) {
				if(errno == EWOULDBLOCK)
					goto again;
				syslog(LOG_ERR, "read wait x10: %m");
			}
			else
				syslog(LOG_ERR, "read wait x10: EOF");
			write(clients[c].s, "E bad device read\r\n", 19);
			sleep(1);
			return;
		}
		if(code == 1)
			goto again;
		rbuf[code - 1] = 0;
		if(*rbuf == 'X' && strlen(rbuf) == 4) {
			processx(rbuf + 1, 0);
			goto again;
		}
		if(*rbuf == 'E') {
			syslog(LOG_ERR, "device error: %s", rbuf);
			if(rbuf[1] != '1' && rbuf[1] != '2' && rbuf[1] != '4')
				goto again;
			win--;
			write(clients[c].s, "E device error: ", 16);
			write(clients[c].s, rbuf, strlen(rbuf));
			write(clients[c].s, "\r\n", 2);
			gavemsg = 1;
			break;
		}
		if(*rbuf != '*') {
			syslog(LOG_ERR, "unrecognized rsp: %s", rbuf);
			goto again;
		}
		win--;
	}
	if(si) {
		i = si;
		si = 0;
		goto top;
	}
	while(win) {
		FD_SET(n, &xprobe);
		tv.tv_sec = 10;
		code = select(n + 1, &xprobe, FD_NULL,
			FD_NULL, &tv);
		if(code < 0) {
			syslog(LOG_ERR, "wselect r: %m");
			sleep(2);
			continue;
		}
		if(code == 0) {
			syslog(LOG_ERR, "wselect x10 timed out");
			write(clients[c].s, "E device timeout\r\n", 18);
			return;
		}
		code = read(n, rbuf, sizeof(rbuf));
		if(code <= 0) {
			if(code) {
				if(errno == EWOULDBLOCK)
					continue;
				syslog(LOG_ERR, "wread wait x10: %m");
			}
			else
				syslog(LOG_ERR, "wread wait x10: EOF");
			write(clients[c].s, "E bad device read\r\n", 19);
			sleep(1);
			return;
		}
		if(code == 1)
			continue;
		rbuf[code - 1] = 0;
		if(*rbuf == 'X' && strlen(rbuf) == 4) {
			processx(rbuf + 1, 0);
			continue;
		}
		if(*rbuf == 'E') {
			syslog(LOG_ERR, "wdevice error: %s", rbuf);
			if(rbuf[1] != '1' && rbuf[1] != '2' && rbuf[1] != '4')
				continue;
			win--;
			if(!gavemsg) {
				write(clients[c].s, "E device error: ", 16);
				write(clients[c].s, rbuf, strlen(rbuf));
				write(clients[c].s, "\r\n", 2);
				gavemsg = 1;
			}
			continue;
		}
		if(*rbuf != '*') {
			syslog(LOG_ERR, "wunrecognized rsp: %s", rbuf);
			continue;
		}
		win--;
	}
	if(!gavemsg)
		write(clients[c].s, "A\r\n", 3);
}

xdtoi(c)
register int c;
{
	if(c >= '0' && c <= '9')
		return(c - '0');
	if(c >= 'A' && c <= 'F')
		return(c - 'A' + 10);
	if(c >= 'a' && c <= 'f')
		return(c - 'a' + 10);
	return(0);
}
