// vguest.cpp
//
// A Rivendell switcher driver for the Logitek vGuest Protocol
//
//   (C) Copyright 2002-2019 Fred Gleason <fredg@paravelsystems.com>
//
//   This program is free software; you can redistribute it and/or modify
//   it under the terms of the GNU General Public License version 2 as
//   published by the Free Software Foundation.
//
//   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., 675 Mass Ave, Cambridge, MA 02139, USA.
//

#include <stdlib.h>
#include <syslog.h>

#include <qsignalmapper.h>

#include <rdapplication.h>
#include <rdescape_string.h>

#include "globals.h"
#include "vguest.h"

VGuest::VGuest(RDMatrix *matrix,QObject *parent)
  : Switcher(matrix,parent)
{
  RDTty *tty;
  QString sql;
  RDSqlQuery *q;
  int n;

  for(int i=0;i<2;i++) {
    vguest_device[i]=NULL;
    vguest_socket[i]=NULL;
    vguest_error_notified[i]=false;
  }

  //
  // Get Matrix Parameters
  //
  vguest_matrix=matrix->matrix();
  vguest_device[0]=NULL;
  vguest_device[1]=NULL;
  vguest_socket[0]=NULL;
  vguest_socket[1]=NULL;
  vguest_porttype[0]=matrix->portType(RDMatrix::Primary);
  vguest_porttype[1]=matrix->portType(RDMatrix::Backup);
  vguest_ipaddress[0]=matrix->ipAddress(RDMatrix::Primary);
  vguest_ipaddress[1]=matrix->ipAddress(RDMatrix::Backup);
  vguest_username[0]=PadString(matrix->username(RDMatrix::Primary),16);
  vguest_username[1]=PadString(matrix->username(RDMatrix::Backup),16);
  vguest_password[0]=PadString(matrix->password(RDMatrix::Primary),16);
  vguest_password[1]=PadString(matrix->password(RDMatrix::Backup),16);
  vguest_start_cart[0]=matrix->startCart(RDMatrix::Primary);
  vguest_start_cart[1]=matrix->startCart(RDMatrix::Backup);
  vguest_stop_cart[0]=matrix->stopCart(RDMatrix::Primary);
  vguest_stop_cart[1]=matrix->stopCart(RDMatrix::Backup);
  vguest_ipport[0]=matrix->ipPort(RDMatrix::Primary);
  vguest_ipport[1]=matrix->ipPort(RDMatrix::Backup);
  vguest_inputs=matrix->inputs();
  vguest_outputs=matrix->outputs();
  vguest_gpis=matrix->gpis();
  vguest_gpos=matrix->gpos();

  //
  // Load Engine Data - Inputs
  //
  sql=QString("select ")+
    "NUMBER,"+      // 00
    "ENGINE_NUM,"+  // 01
    "DEVICE_NUM "+  // 02
    "from INPUTS where "+
    "(STATION_NAME=\""+RDEscapeString(matrix->station())+"\")&&"+
    QString().sprintf("(MATRIX=%d) ",matrix->matrix())+
    "order by NUMBER";
  q=new RDSqlQuery(sql);
  n=1;
  while(q->next()) {
    while(q->value(0).toInt()>n) {
      vguest_input_engine_nums.push_back(-1);
      vguest_input_device_nums.push_back(-1);
      n++;
    }
    vguest_input_engine_nums.push_back(q->value(1).toInt());
    vguest_input_device_nums.push_back(q->value(2).toInt());
    n++;
  }
  delete q;

  //
  // Load Engine Data - Outputs
  //
  sql=
    QString("select ")+
    "NUMBER,"+      // 00
    "ENGINE_NUM,"+  // 01
    "DEVICE_NUM "+  // 02
    "from OUTPUTS where "+
    "(STATION_NAME=\""+RDEscapeString(matrix->station())+"\")&&"+
    QString().sprintf("(MATRIX=%d) ",matrix->matrix())+
    "order by NUMBER";
  q=new RDSqlQuery(sql);
  n=1;
  while(q->next()) {
    while(q->value(0).toInt()>n) {
      vguest_output_engine_nums.push_back(-1);
      vguest_output_device_nums.push_back(-1);
      n++;
    }
    vguest_output_engine_nums.push_back(q->value(1).toInt());
    vguest_output_device_nums.push_back(q->value(2).toInt());
    n++;
  }
  delete q;

  //
  // Load Engine Data - Relays
  //
  sql=
    QString("select ")+
    "NUMBER,"+       // 00
    "ENGINE_NUM,"+   // 01
    "DEVICE_NUM,"+   // 02
    "SURFACE_NUM,"+  // 03
    "RELAY_NUM "+    // 04
    "from VGUEST_RESOURCES where "+
    "(STATION_NAME=\""+RDEscapeString(matrix->station())+"\")&&"+
    QString().sprintf("(MATRIX_NUM=%d)&&",matrix->matrix())+
    QString().sprintf("(VGUEST_TYPE=%d) ",RDMatrix::VguestTypeRelay)+
    "order by NUMBER";
  q=new RDSqlQuery(sql);
  n=1;
  while(q->next()) {
    while(q->value(0).toInt()>n) {
      vguest_relays_engine_nums.push_back(-1);
      vguest_relays_device_nums.push_back(-1);
      vguest_relays_surface_nums.push_back(-1);
      vguest_relays_relay_nums.push_back(-1);
      n++;
    }
    vguest_relays_engine_nums.push_back(q->value(1).toInt());
    vguest_relays_device_nums.push_back(q->value(2).toInt());
    vguest_relays_surface_nums.push_back(q->value(3).toInt());
    vguest_relays_relay_nums.push_back(q->value(4).toInt());
    n++;
  }
  delete q;

  //
  // Load Engine Data - Displays
  //
  sql=QString("select ")+
    "NUMBER,"+       // 00
    "ENGINE_NUM,"+   // 01
    "DEVICE_NUM,"+   // 02
    "SURFACE_NUM "+  // 03
    "from VGUEST_RESOURCES where "+
    "(STATION_NAME=\""+RDEscapeString(matrix->station())+"\")&&"+
    QString().sprintf("(MATRIX_NUM=%d)&&",matrix->matrix())+
    QString().sprintf("(VGUEST_TYPE=%d) ",RDMatrix::VguestTypeDisplay)+
    "order by NUMBER";
  q=new RDSqlQuery(sql);
  n=1;
  while(q->next()) {
    while(q->value(0).toInt()>n) {
      vguest_displays_engine_nums.push_back(-1);
      vguest_displays_device_nums.push_back(-1);
      vguest_displays_surface_nums.push_back(-1);
      n++;
    }
    vguest_displays_engine_nums.push_back(q->value(1).toInt());
    vguest_displays_device_nums.push_back(q->value(2).toInt());
    vguest_displays_surface_nums.push_back(q->value(3).toInt());
    n++;
  }
  delete q;

  //
  // Ping Timers
  //
  vguest_ping_mapper=new QSignalMapper(this);
  connect(vguest_ping_mapper,SIGNAL(mapped(int)),this,SLOT(pingData(int)));
  vguest_ping_response_mapper=new QSignalMapper(this);
  connect(vguest_ping_response_mapper,SIGNAL(mapped(int)),
	  this,SLOT(pingResponseData(int)));
  for(int i=0;i<2;i++) {
    vguest_ping_timer[i]=new QTimer(this);
    vguest_ping_mapper->setMapping(vguest_ping_timer[i],i);
    connect(vguest_ping_timer[i],SIGNAL(timeout()),
	    vguest_ping_mapper,SLOT(map()));

    vguest_ping_response_timer[i]=new QTimer(this);
    vguest_ping_response_mapper->setMapping(vguest_ping_timer[i],i);
    connect(vguest_ping_response_timer[i],SIGNAL(timeout()),
	    vguest_ping_response_mapper,SLOT(map()));
  }

  //
  // Reconnection Timer
  //
  QSignalMapper *reconnect_mapper=new QSignalMapper(this);
  connect(reconnect_mapper,SIGNAL(mapped(int)),
	  this,SLOT(ipConnect(int)));
  for(int i=0;i<2;i++) {
    vguest_reconnect_timer[i]=new QTimer(this);
    reconnect_mapper->setMapping(vguest_reconnect_timer[i],i);
    connect(vguest_reconnect_timer[i],SIGNAL(timeout()),
	    reconnect_mapper,SLOT(map()));
  }

  //
  // Interval OneShots
  //
  vguest_gpio_oneshot=new RDOneShot(this);
  connect(vguest_gpio_oneshot,SIGNAL(timeout(int)),
	  this,SLOT(gpioOneshotData(int)));

  //
  // Initialize the connection
  //
  for(int i=0;i<2;i++) {
    if(vguest_porttype[i]==RDMatrix::TtyPort) {
      tty=new RDTty(rda->station()->name(),matrix->port((RDMatrix::Role)i));
      vguest_device[i]=new RDTTYDevice();
      if(tty->active()) {
	vguest_device[i]->setName(tty->port());
	vguest_device[i]->setSpeed(tty->baudRate());
	vguest_device[i]->setWordLength(tty->dataBits());
	vguest_device[i]->setParity(tty->parity());
	vguest_device[i]->open(QIODevice::Unbuffered|QIODevice::WriteOnly);
      }
      delete tty;
    }
    else {
      if(vguest_porttype[i]==RDMatrix::TcpPort) {
	vguest_socket[i]=new RDSocket(i,this);
	connect(vguest_socket[i],SIGNAL(connectedID(int)),
		this,SLOT(connectedData(int)));
	connect(vguest_socket[i],SIGNAL(connectionClosedID(int)),
		this,SLOT(connectionClosedData(int)));
	connect(vguest_socket[i],SIGNAL(readyReadID(int)),
		this,SLOT(readyReadData(int)));
	connect(vguest_socket[i],
		SIGNAL(errorID(QAbstractSocket::SocketError,int)),
		this,SLOT(errorData(QAbstractSocket::SocketError,int)));
	ipConnect(i);
      }
    }
  }
}


VGuest::~VGuest()
{
  delete vguest_gpio_oneshot;
  for(int i=0;i<2;i++) {
    delete vguest_reconnect_timer[i];
    delete vguest_ping_response_timer[i];
    delete vguest_ping_timer[i];
    if(vguest_device[i]!=NULL) {
      delete vguest_device[i];
    }
    if(vguest_socket[i]!=NULL) {
      delete vguest_socket[i];
    }
  }
}


RDMatrix::Type VGuest::type()
{
  return RDMatrix::LogitekVguest;
}


unsigned VGuest::gpiQuantity()
{
  return vguest_gpis;
}


unsigned VGuest::gpoQuantity()
{
  return vguest_gpos;
}


bool VGuest::primaryTtyActive()
{
  return vguest_porttype[0]==RDMatrix::TtyPort;
}


bool VGuest::secondaryTtyActive()
{
  return vguest_porttype[1]==RDMatrix::TtyPort;
}


void VGuest::processCommand(RDMacro *cmd)
{
  char buffer[VGUEST_MAX_COMMAND_LENGTH];
  char cmd_byte=0;
  QString label;

  switch(cmd->command()) {
      case RDMacro::SD:
	if((cmd->argQuantity()<5)||
	   (cmd->arg(1).toUInt()>vguest_displays_engine_nums.size())) {
	  cmd->acknowledge(false);
	  emit rmlEcho(cmd);
	  rda->syslog(LOG_WARNING,"*** not enough vGuest arguments ***");
	  return;
	}
	if((vguest_displays_engine_nums[cmd->arg(1).toInt()-1]<0)||
	   (vguest_displays_device_nums[cmd->arg(1).toInt()-1]<0)||
	   (vguest_displays_surface_nums[cmd->arg(1).toInt()-1]<0)) {
	  cmd->acknowledge(false);
	  emit rmlEcho(cmd);
	  rda->syslog(LOG_WARNING,"*** invalid vGuest hex parameters ***");
	  return;
	}
	label=cmd->rollupArgs(5).left(VGUEST_MAX_TEXT_LENGTH);
	sprintf(buffer,"\x02%c\x5C%c%c%c%c%c%c%c%s",8+label.length(),
		(char)vguest_displays_engine_nums[cmd->arg(1).toInt()-1],
		(char)(vguest_displays_device_nums[cmd->arg(1).toInt()-1]>>8),
		(char)(vguest_displays_device_nums[cmd->arg(1).toInt()-1]&0xFF),
		(char)vguest_displays_surface_nums[cmd->arg(1).toInt()-1],
		(char)(0xFF&cmd->arg(2).toInt()),
		(char)(0xFF&cmd->arg(3).toInt()),
		(char)(0xFF&cmd->arg(4).toInt()),
		(const char *)label);
	SendCommand(buffer,10+label.length());
	break;

      case RDMacro::ST:
	if((cmd->arg(1).toInt()<1)||(cmd->arg(1).toInt()>vguest_inputs)||
	   (cmd->arg(2).toInt()<1)||(cmd->arg(2).toInt()>vguest_outputs)) {
	  cmd->acknowledge(false);
	  emit rmlEcho(cmd);
	  return;
	}
	if((vguest_input_engine_nums[cmd->arg(1).toInt()-1]<0)||
	   (vguest_input_device_nums[cmd->arg(1).toInt()-1]<0)||
	   (vguest_output_engine_nums[cmd->arg(2).toInt()-1]<0)||
	   (vguest_output_device_nums[cmd->arg(2).toInt()-1]<0)) {
	  cmd->acknowledge(false);
	  emit rmlEcho(cmd);
	  return;
	}
	sprintf(buffer,"\x02\x08\x54%c%c%c%c%c%c%c",
		(char)vguest_output_engine_nums[cmd->arg(2).toInt()-1],
		(char)(vguest_output_device_nums[cmd->arg(2).toInt()-1]>>8),
		(char)(vguest_output_device_nums[cmd->arg(2).toInt()-1]&0xFF),
		VGUEST_DEFAULT_SURFACE_NUMBER,
		(char)vguest_input_engine_nums[cmd->arg(1).toInt()-1],
		(char)(vguest_input_device_nums[cmd->arg(1).toInt()-1]>>8),
		(char)(vguest_input_device_nums[cmd->arg(1).toInt()-1]&0xFF));
	SendCommand(buffer,10);
	cmd->acknowledge(true);
	emit rmlEcho(cmd);
	break;

      case RDMacro::GO:
	if(((cmd->arg(1).lower()!="i")&&
	    (cmd->arg(1).lower()!="o"))||
	   (cmd->arg(2).toInt()<1)||(cmd->arg(2).toInt()>vguest_gpos)||
	   (cmd->arg(3).toInt()<0)||(cmd->arg(3).toInt()>1)||
	   (cmd->arg(4).toInt()<0)) {
	  cmd->acknowledge(false);
	  emit rmlEcho(cmd);
	  return;
	}
	if(cmd->arg(4).toInt()>0) {
	  cmd_byte=0x51;
	}
	else {
	  switch(cmd->arg(3).toInt()) {
	      case 0:
		cmd_byte=0x53;
		break;

	      case 1:
		cmd_byte=0x52;
		break;
	  }
	}
	if((vguest_relays_engine_nums[cmd->arg(2).toInt()-1]<0)||
	   (vguest_relays_device_nums[cmd->arg(2).toInt()-1]<0)||
	   (vguest_relays_surface_nums[cmd->arg(2).toInt()-1]<0)||
	   (vguest_relays_relay_nums[cmd->arg(2).toInt()-1]<0)) {
	  cmd->acknowledge(false);
	  emit rmlEcho(cmd);
	  return;
	}
	switch(0xFF&cmd_byte) {
	    case 0x51:
	      sprintf(buffer,"\x02\x07\x51%c%c%c%c%c%c",
		      vguest_relays_engine_nums[cmd->arg(2).toInt()-1],
		      vguest_relays_device_nums[cmd->arg(2).toInt()-1]>>8,
		      vguest_relays_device_nums[cmd->arg(2).toInt()-1]&0xFF,
		      vguest_relays_surface_nums[cmd->arg(2).toInt()-1],
		      vguest_relays_relay_nums[cmd->arg(2).toInt()-1],
		      cmd->arg(4).toInt()/50);
	      SendCommand(buffer,9);
	      emit gpiChanged(vguest_matrix,cmd->arg(2).toInt()-1,true);
	      emit gpoChanged(vguest_matrix,cmd->arg(2).toInt()-1,true);
	      vguest_gpio_oneshot->start(cmd->arg(2).toInt()-1,2000);
	      break;

	    case 0x52:
	    case 0x53:
	      sprintf(buffer,"\x02\x06%c%c%c%c%c%c",
		      cmd_byte,
		      vguest_relays_engine_nums[cmd->arg(2).toInt()-1],
		      vguest_relays_device_nums[cmd->arg(2).toInt()-1]>>8,
		      vguest_relays_device_nums[cmd->arg(2).toInt()-1]&0xFF,
		      vguest_relays_surface_nums[cmd->arg(2).toInt()-1],
		      vguest_relays_relay_nums[cmd->arg(2).toInt()-1]);
	      SendCommand(buffer,8);
	      emit gpiChanged(vguest_matrix,cmd->arg(2).toInt()-1,
			      cmd->arg(3).toInt());
	      emit gpoChanged(vguest_matrix,cmd->arg(2).toInt()-1,
			      cmd->arg(3).toInt());
	}
	cmd->acknowledge(true);
	emit rmlEcho(cmd);
	break;

      default:
	cmd->acknowledge(false);
	emit rmlEcho(cmd);
	break;
  }
}


void VGuest::ipConnect(int id)
{
  if(!vguest_ipaddress[id].isNull()) {
    vguest_socket[id]->
      connectToHost(vguest_ipaddress[id].toString(),vguest_ipport[id]);
  }
}


void VGuest::connectedData(int id)
{
  vguest_istate[id]=0;
}


void VGuest::connectionClosedData(int id)
{
  int interval=GetHoldoff();
  if(!vguest_error_notified[id]) {
    rda->syslog(LOG_WARNING,
	   "connection to vGuest device at %s:%d closed, attempting reconnect",
		(const char *)vguest_ipaddress[id].toString().toUtf8(),
		vguest_ipport[id]);
    vguest_error_notified[id]=true;
  }
  if(vguest_stop_cart[id]>0) {
    ExecuteMacroCart(vguest_stop_cart[id]);
  }
  vguest_ping_timer[id]->stop();
  vguest_ping_response_timer[id]->stop();
  vguest_reconnect_timer[id]->start(interval,true);
}


void VGuest::readyReadData(int id)
{
  char buffer[255];
  int n=0;

  while((n=vguest_socket[id]->readBlock(buffer,255))>0) {
    for(int i=0;i<n;i++) {
      switch(vguest_istate[id]) {
      case 0:   // STX Command Start
	switch(buffer[i]) {
	case 0x02:
	  vguest_istate[id]=1;
	  break;

	case 0x04:
	  vguest_istate[id]=11;
	  break;
	}
	break;

      case 1:   // LP2 Command Length
	vguest_cmd_length[id]=buffer[i];
	vguest_cmd_buffer[id][0]=2;
	vguest_cmd_buffer[id][1]=buffer[i];
	vguest_cmd_ptr[id]=2;
	vguest_istate[id]=2;
	break;

      case 2:   // LP2 Command Body
	vguest_cmd_buffer[id][vguest_cmd_ptr[id]++]=buffer[i];
	if(vguest_cmd_ptr[id]==(vguest_cmd_length[id]+2)) {
	  DispatchCommand(vguest_cmd_buffer[id],vguest_cmd_length[id]+2,id);
	  vguest_istate[id]=0;
	}
	break;

      case 11:   // Metadata Command Length
	vguest_cmd_length[id]=buffer[i];
	vguest_cmd_buffer[id][0]=2;
	vguest_cmd_buffer[id][1]=buffer[i];
	vguest_cmd_ptr[id]=2;
	vguest_istate[id]=12;
	break;

      case 12:   // LP2 Command Body
	vguest_cmd_buffer[id][vguest_cmd_ptr[id]++]=buffer[i];
	if(vguest_cmd_ptr[id]==(vguest_cmd_length[id]+2)) {
	  MetadataCommand(vguest_cmd_buffer[id],vguest_cmd_length[id]+2,id);
	  vguest_istate[id]=0;
	}
	break;
      }
    }
  }
}


void VGuest::errorData(QAbstractSocket::SocketError err,int id)
{
  int interval=VGUEST_RECONNECT_MIN_INTERVAL;

  switch(err) {
  case QAbstractSocket::ConnectionRefusedError:
    interval=GetHoldoff();
    if(!vguest_error_notified[id]) {
      rda->syslog(LOG_WARNING,
	   "connection to vGuest device at %s:%d refused, attempting reconnect",
		  (const char *)vguest_ipaddress[id].toString().toUtf8(),
		  vguest_ipport[id]);
      vguest_error_notified[id]=true;
    }
    vguest_reconnect_timer[id]->start(interval,true);
    break;

  case QAbstractSocket::HostNotFoundError:
    if(!vguest_error_notified[id]) {
      rda->syslog(LOG_WARNING,
		"error on connection to vGuest device at %s:%d: Host Not Found",
		  (const char *)vguest_ipaddress[id].toString().toUtf8(),
		  vguest_ipport[id]);
      vguest_error_notified[id]=true;
    }
    break;

  default:
    if(!vguest_error_notified[id]) {
      rda->syslog(LOG_WARNING,
	     "error %d on connection to vGuest device at %s:%d: Socket Read Error",
		  err,
		  (const char *)vguest_ipaddress[id].toString().toUtf8(),
		  vguest_ipport[id]);
      vguest_error_notified[id]=true;
    }
    break;
  }
}


void VGuest::gpioOneshotData(int value)
{
  emit gpiChanged(vguest_matrix,value,false);
  emit gpoChanged(vguest_matrix,value,false);
}


void VGuest::pingData(int id)
{
  char buffer[VGUEST_MAX_COMMAND_LENGTH];

  buffer[0]=0x04;
  buffer[1]=0x01;
  buffer[2]=0x03;   // LPCore Connection Ping
  vguest_socket[id]->writeBlock(buffer,3);
  vguest_ping_response_timer[id]->start(VGUEST_PING_INTERVAL,true);
}


void VGuest::pingResponseData(int id)
{
  vguest_socket[id]->close();
  rda->syslog(LOG_WARNING,"vGuest connection to "+
	      vguest_ipaddress[id].toString()+
	      " timed out, restarting connection");
}


void VGuest::SendCommand(char *str,int len)
{
  // LogLine(QString().sprintf("SENT: %s",(const char *)RenderCommand(str,len)));
  for(int i=0;i<2;i++) {
    switch(vguest_porttype[i]) {
      case RDMatrix::TtyPort:
	if(vguest_device[i]!=NULL) {
	  vguest_device[i]->write(str,len);
	}
	break;
	
      case RDMatrix::TcpPort:
	if(vguest_socket[i]!=NULL) {
	  vguest_socket[i]->writeBlock(str,len);
	}
	break;

      case RDMatrix::NoPort:
	break;
    }
  }
}


void VGuest::DispatchCommand(char *cmd,int len,int id)
{
  char buffer[VGUEST_MAX_COMMAND_LENGTH];
  QString str;
  int linenum;

  //  LogLine(RDConfig::LogNotice,
  //  QString().sprintf("RCVD: %s",(const char *)RenderCommand(cmd,len)));

  switch(0xFF&cmd[2]) {
  case 0xF9:   // Username/Password Query
    buffer[0]=0x02;
    buffer[1]=0x22;
    buffer[2]=0xF9;
    buffer[3]=VGUEST_ID_BYTE;
    sprintf(buffer+4,"%s%s",
	    (const char *)vguest_username[id],
	    (const char *)vguest_password[id]);
    SendCommand(buffer,36);
    break;

  case 0xF0:   // Connect Status
    switch(0xFF&cmd[3]) {
    case 0x0A:  // Valid connection
    case 0x14:
      rda->syslog(LOG_INFO,
		  "connection to vGuest device at %s:%d established",
		  (const char *)vguest_ipaddress[id].toString().toUtf8(),
		  vguest_ipport[id]);
      vguest_error_notified[id]=false;
      if(vguest_start_cart[id]>0) {
	ExecuteMacroCart(vguest_start_cart[id]);
      }
      if(vguest_socket[id]!=NULL) {
	buffer[0]=0x04;
	buffer[1]=0x01;
	buffer[2]=0x03;   // LPCore Connection Ping
	vguest_socket[id]->writeBlock(buffer,3);
      }
      break;

    case 0x0B:  // Invalid Username
    case 0x15:
      rda->syslog(LOG_WARNING,
	     "connection to vGuest device at %s:%d refused: username invalid",
		  (const char *)vguest_ipaddress[id].toString().toUtf8(),
		  vguest_ipport[id]);
      vguest_socket[id]->close();
      connectionClosedData(id);
      break;
      
    case 0x0C:  // Invalid Password
    case 0x16:
      rda->syslog(LOG_WARNING,
	     "connection to vGuest device at %s:%d refused: password invalid",
		  (const char *)vguest_ipaddress[id].toString().toUtf8(),
		  vguest_ipport[id]);
      vguest_socket[id]->close();
      connectionClosedData(id);
      break;

    case 0x0D:  // No vGuest Permission
    case 0x17:
      rda->syslog(LOG_WARNING,
	   "connection to vGuest device at %s:%d refused: no vGuest permission",
		  (const char *)vguest_ipaddress[id].toString().toUtf8(),
		  vguest_ipport[id]);
      vguest_socket[id]->close();
      connectionClosedData(id);
      break;

    case 0x0E:  // No Profile
    case 0x18:
      rda->syslog(LOG_WARNING,
	    "connection to vGuest device at %s:%d refused: no profile assigned",
		  (const char *)vguest_ipaddress[id].toString().toUtf8(),
		  vguest_ipport[id]);
      vguest_socket[id]->close();
      connectionClosedData(id);
      break;
    }
    break;

  case 0x52:   // Turn On
    if((linenum=GetRelay(0xFF&cmd[3],256*(0xFF&cmd[4])+(0xFF&cmd[5]),
			 0xFF&cmd[6],0xFF&cmd[7]))>=0) {
      emit gpiChanged(vguest_matrix,linenum,true);
      emit gpoChanged(vguest_matrix,linenum,true);
    }
    else {
      rda->syslog(LOG_DEBUG,
		  "unhandled vGuest command received: %s",
		  (const char *)RenderCommand(cmd,len).toUtf8());
    }
    break;

  case 0x53:   // Turn Off
    if((linenum=GetRelay(0xFF&cmd[3],256*(0xFF&cmd[4])+(0xFF&cmd[5]),
			 0xFF&cmd[6],0xFF&cmd[7]))>=0) {
      emit gpiChanged(vguest_matrix,linenum,false);
      emit gpoChanged(vguest_matrix,linenum,false);
    }
    else {
      rda->syslog(LOG_DEBUG,
		  "unhandled vGuest command received: %s",
		  (const char *)RenderCommand(cmd,len).toUtf8());
    }
    break;

  case 0x54:   // Input Assign
    break;
    
  case 0x55:   // Input Mode
    break;
    
  case 0x56:   // Fader Level
    break;
    
  default:
    rda->syslog(LOG_DEBUG,"unrecognized vGuest command received: %s",
		(const char *)RenderCommand(cmd,len).toUtf8());
    break;
  }
}


void VGuest::MetadataCommand(char *cmd,int len,int id)
{
  switch(0xFF&cmd[2]) {
  case 0x03:   // Connection Ping
    if(vguest_ping_response_timer[id]->isActive()) {
      vguest_ping_response_timer[id]->stop();
    }
    else {
      rda->syslog(LOG_INFO,
	  "vGuest system at %s understands ping, activating timeout monitoring",
		  (const char *)vguest_ipaddress[id].toString().toUtf8());
    }
    vguest_ping_timer[id]->start(VGUEST_PING_INTERVAL,true);
    break;
  }
}


QString VGuest::PadString(QString str,int len)
{
  QString out;
  out=str.left(len);
  while(out.length()<len) {
    out+=" ";
  }
  return out;
}


QString VGuest::RenderCommand(char *cmd,int len)
{
  QString str;

  for(int i=0;i<len;i++) {
    str+=QString().sprintf("%02X",0xFF&cmd[i]);
  }
  return str;
}


int VGuest::GetRelay(int enginenum,int devicenum,int surfacenum,int relaynum)
{
  for(unsigned i=0;i<vguest_relays_engine_nums.size();i++) {
/*
    LogLine(QString().sprintf("Checking Engine: %d | %d",vguest_relays_engine_nums[i],enginenum));
    LogLine(QString().sprintf("Checking Device: 0x%04X | 0x%04X",vguest_relays_device_nums[i],devicenum));
    LogLine(QString().sprintf("Checking Surface: 0x%04X | 0x%04X",vguest_relays_surface_nums[i],surfacenum));
    LogLine(QString().sprintf("Checking Relay: 0x%04X | 0x%04X",vguest_relays_relay_nums[i],relaynum));
*/
    if((vguest_relays_engine_nums[i]==enginenum)&&
       (vguest_relays_device_nums[i]==devicenum)&&
       (vguest_relays_surface_nums[i]==surfacenum)&&
       (vguest_relays_relay_nums[i]==relaynum)) {
//      LogLine("  MATCH!");
      return (int)i;
    }
  }
  return -1;
}


int VGuest::GetHoldoff()
{
  return (int)(VGUEST_RECONNECT_MIN_INTERVAL+
	       (VGUEST_RECONNECT_MAX_INTERVAL-VGUEST_RECONNECT_MIN_INTERVAL)*
	       (double)random()/(double)RAND_MAX);
}


void VGuest::ExecuteMacroCart(unsigned cartnum)
{
  RDMacro rml;
  rml.setRole(RDMacro::Cmd);
  rml.setCommand(RDMacro::EX);
  rml.setAddress(rda->station()->address());
  rml.setEchoRequested(false);
  rml.addArg(cartnum);
  emit rmlEcho(&rml);
}