/**
* Copyright 2014 Comcast Cable Communications Management, LLC
*
* This file is part of CATS.
*
* CATS 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 3 of the License, or
* (at your option) any later version.
*
* CATS 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 CATS. If not, see <http://www.gnu.org/licenses/>.
*/
package com.comcast.cats.service.impl;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation of IRCommunicator for Global Cache GC-100 devices. Infos about
* the GC-100 can be found at
* <href>http://http://www.globalcache.com/files/Documentation
* /GC-100_DataSheet.pdf</href>.
* <href>http://www.globalcache.com/files/docs/API-GC-100.pdf</href>.
*/
public class GlobalCache6Communicator extends IRCommunicator {
private static final int GC_100_PORT = 4998;
private static final int GC100_SOCKET_TIMEOUT = 10000;
private static String[] portMappings = { "2:1", "2:2", "2:3" };
private static final int MAX_PORTS = 3;
public static final Integer RETRY_COUNT = 1;
private long lastIRTransmission = 0L;
/**
* Max duration in milliseconds since last ir transmission before resetting
* the GC100 connection. The initial socket acquiring seems to take longer
* (~3 seconds) than expected, but should be acceptable under normal
* operating scenarios. This avoids trying to perform an IR retransmission
* after a timeout.
*/
// private static final long MAX_IDLE_DURATION = 30L * 60000L;
// TODO - CEF set MAX_IDLE_DURATION to 15 seconds for testing.
private static final long MAX_IDLE_DURATION = 30L * 60000L;
// This is static because it should be unique for all (concurrent) requests
// to the GC-100
// Should be accessed only through getNextRequestId().
private int m_id;
// Network stuff
private InetAddress m_ip;
private Socket m_socket = null;
private BufferedReader m_sis;
private OutputStream m_sos;
private final Logger log = LoggerFactory.getLogger(GlobalCache6Communicator.class);
/**
* Creates a new instance of this communicator.
*
* @param ip
* The IP address of the GC-100.
* @param port
* IR port to be used.
*/
public GlobalCache6Communicator(final InetAddress ip) {
if (ip == null) {
throw new IllegalArgumentException("IP cannot be null");
}
m_id = 0;
m_ip = ip;
m_socket = null;
}
/**
* Initializes this communicator.
*
* @return <code>True</code> if the communicator was successfully
* initialized. <code>False</code> otherwise.
*/
public synchronized boolean init() {
boolean retVal = false;
long tsBeforeConnect = 0;
long tsAfterConnect = 0;
// Close existing connection, if any.
if (null != m_socket) {
log.warn("Closing existing connection via uninit()");
uninit();
}
// Open network connection to port 4998.
try {
InetSocketAddress addr = new InetSocketAddress(m_ip, GC_100_PORT);
m_socket = new Socket();
/*
* This connection should "always" be available for sending data so
* set keep alive to true.
*/
if (log.isTraceEnabled()) {
tsBeforeConnect = System.currentTimeMillis();
}
//m_socket.setKeepAlive(true);
m_socket.setSoTimeout(GC100_SOCKET_TIMEOUT);
m_socket.connect(addr, GC100_SOCKET_TIMEOUT);
if (log.isTraceEnabled()) {
tsAfterConnect = System.currentTimeMillis();
log.trace("Connection established to " + m_ip + ":"
+ GC_100_PORT + " took "
+ Long.toString(tsAfterConnect - tsBeforeConnect));
}
m_sis = new BufferedReader(new InputStreamReader(
m_socket.getInputStream()));
m_sos = m_socket.getOutputStream();
retVal = true;
} catch (IOException ioe) {
log.error("Failed to connect to " + m_ip + ":" + GC_100_PORT);
log.error(ioe.getMessage());
m_socket = null;
}
return retVal;
}
/**
* Finalizes this communicator.
*/
public void uninit() {
// Close network connection.
if (null != m_socket) {
try {
log.info("Socket currently exist, attempt to close it.");
m_sis.close();
m_sos.close();
m_socket.close();
} catch (IOException ioe) {
log.error("Failed to close socket to " + m_ip + ": "
+ GC_100_PORT);
log.error(ioe.getMessage());
} finally {
m_sis = null;
m_sos = null;
m_socket = null;
}
}
}
/**
* Translates an IR code from the PRONTO format into the GC-100 format and
* transmits the code.
*
* @param irCode
* The IR code in PRONTO format.
* @param port
* IR port on GC100 to send the code to.
* @return <code>True</code> if the code was transmitted successfully;
* <code>false</code> otherwise.
*/
@Override
public synchronized boolean transmitIR(String irCode, int port) {
String outlet = createOutlet(port);
return transmitIR(irCode, outlet, 1, 1);
}
@Override
public synchronized boolean transmitIR(String irCode, int port, int count,
int offset) {
String outlet = createOutlet(port);
return transmitIR(irCode, outlet, count, offset);
}
/**
* Create outlet string from module and connector.
*
* @param port
* - IR port to be referenced for GC100 device.
* @return outlet string combining the module and connector.
*/
protected String createOutlet(int port) {
if (port < 0 || port > MAX_PORTS) {
throw new IllegalArgumentException("Module value must be >= 0");
}
return portMappings[port - 1];
}
/**
* Create the next available request ID within the range of 0 ... 65535.
*
* @return The next id.
*/
private synchronized int getNextRequestId() {
int result = m_id;
m_id = (m_id + 1) & 0xFFFF;
return result;
}
/**
* Base implementation to transmit the IR code.
*
* @param irCode
* - The PRONTO based IR code to send.
* @param outlet
* - The outlet where this code should be sent to.
* @param count
* - Number of times to send ir key.
* @param offset
* - Offset that should be repeated.
* @return True if code was sent false otherwise.
*/
protected synchronized boolean transmitIR(String irCode, String outlet,
int count, int offset) {
if (null == irCode) {
log.error("IR code can't be null");
return false;
}
if (null == m_socket) {
// Try initialization
int retries = 0;
do {
init();
} while (null == m_socket && retries++ < 3);
// Still a problem?
if (null == m_socket) {
log.error("GlobalCacheCommunicator could not be initialized");
return false;
}
}
log.trace("Sending PRONTO code: " + irCode);
// The UIRT codes can start with a zone selector "Z[0-3]. Remove it.
String code = (irCode.startsWith("Z")) ? irCode.substring(2) : irCode;
try {
GlobalCacheCode gcCode = new GlobalCacheCode(code);
int requestId = getNextRequestId();
long startDecode = System.currentTimeMillis();
String gcCodeStr = gcCode.getSendIRCommand(count, offset,
requestId, outlet);
long endDecode = System.currentTimeMillis();
// Send the code and wait for response.
log.debug("Sending IR request: " + gcCodeStr);
String response = "";
Integer retries = 0;
// Let's loop to handle unkown commands.
do {
if (retries > 0) {
log.warn("Unkown Command Found Retrying");
}
m_sos.write(gcCodeStr.getBytes());
m_sos.flush();
long endSend = System.currentTimeMillis();
response = m_sis.readLine();
log.debug("Received: " + response);
if (null == response) {
log.error("Received null response from IR requestid"+requestId+ " dest GC "+m_socket.getInetAddress());
return false;
}
long endReceive = System.currentTimeMillis();
log.info("IP[" + m_ip.toString() + "]Outlet[" + outlet
+ "]:Decode[" + Long.toString(endDecode - startDecode)
+ "ms]:Send[" + Long.toString(endSend - endDecode)
+ "ms]:Read[" + Long.toString(endReceive - endSend)
+ "ms]:RequestId[" + requestId + "]");
lastIRTransmission = System.currentTimeMillis();
} while (GlobalCacheCode.isUnkownCommand(response)
&& retries < RETRY_COUNT);
return GlobalCacheCode.verifyCompleteIRCommand(response, requestId,
outlet);
} catch (IOException ioe) {
log.error("Failed to send IR code", ioe);
log.error(ioe.getMessage());
// Try re-initialized the connection for the next request.
uninit();
init();
return false;
} catch (NumberFormatException nfe) {
log.error("Unable to convert PRONTO code: + " + code + ": "
+ nfe.getMessage());
return false;
}
}
/*
* private boolean resetStaleConnection() { long currentTime =
* System.currentTimeMillis(); if((currentTime - lastIRTransmission) >=
* MAX_IDLE_DURATION) { log.info("Stale connection found!"); return true; }
* return false; }
*/
}