/**
* Copyright (c) 2010-2016 by the respective copyright holders.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.openhab.binding.onkyo.internal.eiscp;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import javax.xml.bind.DatatypeConverter;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.onkyo.internal.OnkyoEventListener;
import org.openhab.binding.onkyo.internal.OnkyoStatusUpdateEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* <b>Note:</b>This class was originally developed by Tom Gutwin but suffered heavy
* refactoring recently.
* </p>
* <br>
*
* A class that wraps the communication to Onkyo/Integra devices using the
* Ethernet Integra Serial Control Protocol (eISCP).
*
* @author Tom Gutwin P.Eng
* @author Thomas.Eichstaedt-Engelen (Refactoring)
* @author Pauli Anttila (Simplified, rewritten and added status update listener functionality)
*/
public class Eiscp implements EiscpInterface {
private static final Logger logger = LoggerFactory.getLogger(Eiscp.class);
/** Instantiated class IP for the receiver to communicate with. **/
private String receiverIP = "";
/** default eISCP port. **/
public static final int DEFAULT_EISCP_PORT = 60128;
/** Connection timeout in milliseconds **/
private static final int CONNECTION_TIMEOUT = 5000;
/** Connection test interval in milliseconds **/
private static final int CONNECTION_TEST_INTERVAL = 60000;
/** Socket timeout in milliseconds **/
private static final int SOCKET_TIMEOUT = CONNECTION_TEST_INTERVAL + 10000;
/** Instantiated class Port for the receiver to communicate with. **/
private int receiverPort = DEFAULT_EISCP_PORT;
private Socket eiscpSocket = null;
private DataListener dataListener = null;
private ObjectOutputStream outStream = null;
private DataInputStream inStream = null;
private boolean connected = false;
private List<OnkyoEventListener> _listeners = new ArrayList<OnkyoEventListener>();
private int retryCount = 1;
private ConnectionSupervisor connectionSupervisor = null;
/**
* Constructor that takes your receivers IP and port.
**/
public Eiscp(String ip, int eiscpPort) {
if (StringUtils.isNotBlank(ip)) {
receiverIP = ip;
}
if (eiscpPort >= 1) {
receiverPort = eiscpPort;
}
}
/**
* Add event listener, which will be invoked when status upadte is received from receiver.
**/
@Override
public synchronized void addEventListener(OnkyoEventListener listener) {
_listeners.add(listener);
}
/**
* Remove event listener.
**/
@Override
public synchronized void removeEventListener(OnkyoEventListener listener) {
_listeners.remove(listener);
}
/**
* Get retry count value.
**/
@Override
public int getRetryCount() {
return retryCount;
}
/**
* Set retry count value. How many times command is retried when error occurs.
**/
@Override
public void setRetryCount(int retryCount) {
this.retryCount = retryCount;
}
/**
* Connects to the receiver by opening a socket connection through the
* IP and port defined on constructor.
**/
@Override
public boolean connectSocket() {
return connectSocket(receiverIP, receiverPort);
}
/**
* Connects to the receiver by opening a socket connection through the
* IP and port.
**/
public boolean connectSocket(String ip, int port) {
if (eiscpSocket == null || !connected || !eiscpSocket.isConnected()) {
try {
// Creating a socket to connect to the server
eiscpSocket = new Socket();
eiscpSocket.connect(new InetSocketAddress(ip, port), CONNECTION_TIMEOUT);
logger.debug("Connected to {} on port {}", ip, port);
// Get Input and Output streams
outStream = new ObjectOutputStream(eiscpSocket.getOutputStream());
inStream = new DataInputStream(eiscpSocket.getInputStream());
eiscpSocket.setSoTimeout(SOCKET_TIMEOUT);
outStream.flush();
connected = true;
receiverIP = ip;
receiverPort = port;
// start status update listener
if (dataListener == null) {
dataListener = new DataListener();
dataListener.start();
}
// start connection tester
if (connectionSupervisor == null) {
connectionSupervisor = new ConnectionSupervisor(CONNECTION_TEST_INTERVAL);
}
} catch (UnknownHostException unknownHost) {
logger.error("You are trying to connect to an unknown host!", unknownHost);
} catch (IOException ioException) {
logger.error("Can't connect: " + ioException.getMessage());
}
}
return connected;
}
/**
* Closes the socket connection.
*
* @return true if the closed successfully
**/
@Override
public boolean closeSocket() {
try {
if (dataListener != null) {
dataListener.setInterrupted(true);
dataListener = null;
logger.debug("closed data listener!");
}
if (connectionSupervisor != null) {
connectionSupervisor.stopConnectionTester();
connectionSupervisor = null;
logger.debug("closed connection tester!");
}
if (inStream != null) {
inStream.close();
inStream = null;
logger.debug("closed input stream!");
}
if (outStream != null) {
outStream.close();
outStream = null;
logger.debug("closed output stream!");
}
if (eiscpSocket != null) {
eiscpSocket.close();
eiscpSocket = null;
logger.debug("closed socket!");
}
connected = false;
} catch (IOException ioException) {
logger.error("Closing connection throws an exception!", ioException);
}
return connected;
}
/**
* Wraps a command in a eiscp data message (data characters).
*
* @param eiscpCmd
* eISCP command.
* @return StringBuffer holing the full iscp message packet
**/
private StringBuilder getEiscpMessage(String eiscpCmd) {
StringBuilder sb = new StringBuilder();
int eiscpDataSize = 2 + eiscpCmd.length() + 1; // this is the eISCP data size
/*
* This is where I construct the entire message character by character.
* Each char is represented by a 2 digit hex value
*/
sb.append("ISCP");
// the following are all in HEX representing one char
// 4 char Big Endian Header
sb.append((char) 0x00);
sb.append((char) 0x00);
sb.append((char) 0x00);
sb.append((char) 0x10);
// 4 char Big Endian data size
sb.append((char) ((eiscpDataSize >> 24) & 0xFF));
sb.append((char) ((eiscpDataSize >> 16) & 0xFF));
sb.append((char) ((eiscpDataSize >> 8) & 0xFF));
sb.append((char) (eiscpDataSize & 0xFF));
// eiscp_version = "01";
sb.append((char) 0x01);
// 3 chars reserved = "00"+"00"+"00";
sb.append((char) 0x00);
sb.append((char) 0x00);
sb.append((char) 0x00);
// eISCP data
// Start Character
sb.append("!");
// eISCP data - unit type char '1' is receiver
sb.append("1");
// eISCP data - 3 char command and param ie PWR01
sb.append(eiscpCmd);
// msg end - EOF
sb.append((char) 0x0D);
return sb;
}
/**
* Sends to command to the receiver.
* It does not wait for a reply.
*
* @param eiscpCmd the eISCP command to send.
**/
@Override
public void sendCommand(String eiscpCmd) {
logger.debug("Send command: {} to {}:{} ({})", eiscpCmd, receiverIP, receiverPort, eiscpSocket);
StringBuilder sb = getEiscpMessage(eiscpCmd);
sendCommand(sb, false, retryCount);
}
/**
* Sends to command to the receiver and close the connection when done.
* It does not wait for a reply.
*
* @param eiscpCmd the eISCP command to send.
**/
public void sendCommandAndClose(String eiscpCmd) {
logger.debug("Send command: {}", eiscpCmd);
StringBuilder sb = getEiscpMessage(eiscpCmd);
sendCommand(sb, true, retryCount);
}
/**
* Sends to command to the receiver.
*
* @param eiscpCmd the eISCP command to send.
* @param closeSocket flag to close the connection when done or leave it open.
* @param retry retry count.
**/
private void sendCommand(StringBuilder eiscpCmd, boolean closeSocket, int retry) {
if (connectSocket()) {
try {
if (logger.isTraceEnabled()) {
logger.trace("Sending {} bytes: {}", eiscpCmd.length(),
DatatypeConverter.printHexBinary(eiscpCmd.toString().getBytes()));
}
outStream.writeBytes(eiscpCmd.toString());
outStream.flush();
} catch (IOException ioException) {
logger.error("Error occured when sending command", ioException);
if (retry > 0) {
logger.debug("Retry {}...", retry);
closeSocket();
sendCommand(eiscpCmd, closeSocket, retry--);
}
}
}
// finally close the socket if required ...
if (closeSocket) {
closeSocket();
}
}
/**
* This method wait any state messages form receiver.
*
* @throws IOException
* @throws InterruptedException
* @throws EiscpException
**/
private void waitStateMessages() throws NumberFormatException, IOException, InterruptedException, EiscpException {
if (connected) {
OnkyoStatusUpdateEvent event = new OnkyoStatusUpdateEvent(this);
logger.trace("Waiting status messages");
while (true) {
// 1st 4 chars are the leadIn
if (inStream.readByte() != 'I') {
continue;
}
if (inStream.readByte() != 'S') {
continue;
}
if (inStream.readByte() != 'C') {
continue;
}
if (inStream.readByte() != 'P') {
continue;
}
// header size
final int headerSize = (inStream.readByte() & 0xFF) << 24 | (inStream.readByte() & 0xFF) << 16
| (inStream.readByte() & 0xFF) << 8 | (inStream.readByte() & 0xFF);
logger.trace("Header size: {}", headerSize);
if (headerSize != 16) {
throw new EiscpException("Unsupported header size: " + headerSize);
}
// header size
final int dataSize = (inStream.readByte() & 0xFF) << 24 | (inStream.readByte() & 0xFF) << 16
| (inStream.readByte() & 0xFF) << 8 | (inStream.readByte() & 0xFF);
logger.trace("Data size: {}", dataSize);
// version
final byte versionChar = inStream.readByte();
if (versionChar != 1) {
throw new EiscpException("Unsupported version " + String.valueOf(versionChar));
}
// skip 3 reserved bytes
inStream.readByte();
inStream.readByte();
inStream.readByte();
byte[] data = new byte[dataSize];
final int bytesReceived = inStream.read(data, 0, data.length);
if (logger.isTraceEnabled()) {
logger.trace("Received {} bytes: {}", bytesReceived, DatatypeConverter.printHexBinary(data));
}
if (bytesReceived != dataSize) {
throw new EiscpException("Data missing: " + (dataSize - bytesReceived));
}
// start char
final byte startChar = data[0];
if (startChar != '!') {
throw new EiscpException("Illegal start char " + startChar);
}
// unit type
@SuppressWarnings("unused")
final byte unitType = data[1];
// data should be end to "[EOF]" or "[EOF][CR]" or
// "[EOF][CR][LF]" characters depend on model
// [EOF] End of File ASCII Code 0x1A
// [CR] Carriage Return ASCII Code 0x0D (\r)
// [LF] Line Feed ASCII Code 0x0A (\n)
int endBytes = 0;
if (data[dataSize - 4] == (byte) 0x1A && data[dataSize - 3] == '\r' && data[dataSize - 2] == '\n'
&& data[dataSize - 1] == 0x00) {
// skip "[EOF][CR][LF][NULL]"
endBytes = 4;
} else
if (data[dataSize - 3] == (byte) 0x1A && data[dataSize - 2] == '\r' && data[dataSize - 1] == '\n') {
// skip "[EOF][CR][LF]"
endBytes = 3;
} else if (data[dataSize - 2] == (byte) 0x1A && data[dataSize - 1] == '\r') {
// "[EOF][CR]"
endBytes = 2;
} else if (data[dataSize - 1] == (byte) 0x1A) {
// "[EOF]"
endBytes = 1;
} else {
throw new EiscpException("Illegal end of message");
}
int bytesToCopy = dataSize - 2 - endBytes;
byte[] message = new byte[bytesToCopy];
// skip 2 first bytes and copy all bytes before end bytes
System.arraycopy(data, 2, message, 0, bytesToCopy);
// send message to event listeners
try {
Iterator<OnkyoEventListener> iterator = _listeners.iterator();
while (iterator.hasNext()) {
iterator.next().statusUpdateReceived(event, receiverIP, new String(message));
}
} catch (Exception e) {
logger.error("Event listener invoking error", e);
}
}
} else {
throw new IOException("Not Connected to Receiver");
}
}
private class DataListener extends Thread {
private boolean interrupted = false;
DataListener() {
}
public void setInterrupted(boolean interrupted) {
this.interrupted = interrupted;
this.interrupt();
}
@Override
public void run() {
logger.debug("Data listener started");
boolean restartConnection = false;
// as long as no interrupt is requested, continue running
while (!interrupted) {
try {
waitStateMessages();
} catch (EiscpException e) {
logger.error("Error occured during message waiting", e);
} catch (SocketTimeoutException e) {
logger.error("No data received during supervision interval ({} sec)!", SOCKET_TIMEOUT);
restartConnection = true;
} catch (Exception e) {
if (interrupted != true && this.isInterrupted() != true) {
logger.error("Error occured during message waiting", e);
restartConnection = true;
// sleep a while, to prevent fast looping if error situation
// is permanent
mysleep(1000);
}
}
if (restartConnection) {
restartConnection = false;
// reopen connection
logger.debug("Reconnecting...");
try {
connected = false;
connectSocket();
} catch (Exception ex) {
logger.error("Reconnection invoking error", ex);
}
}
}
logger.debug("Data listener stopped");
}
private void mysleep(long milli) {
try {
sleep(milli);
} catch (InterruptedException e) {
interrupted = true;
}
}
}
private class ConnectionSupervisor {
private Timer timer;
public ConnectionSupervisor(int milliseconds) {
logger.debug("Connection supervisor started, interval {} milliseconds", milliseconds);
timer = new Timer();
timer.schedule(new Task(), milliseconds, milliseconds);
}
public void stopConnectionTester() {
timer.cancel();
}
class Task extends TimerTask {
@Override
public void run() {
if (connected) {
logger.debug("Test connection to {}:{}", receiverIP, receiverPort);
sendCommand("PWRQSTN");
}
}
}
}
}