/**
* 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.satel.internal.protocol;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.openhab.binding.satel.command.IntegraVersionCommand;
import org.openhab.binding.satel.command.SatelCommand;
import org.openhab.binding.satel.command.SatelCommand.State;
import org.openhab.binding.satel.internal.event.ConnectionStatusEvent;
import org.openhab.binding.satel.internal.event.EventDispatcher;
import org.openhab.binding.satel.internal.event.IntegraVersionEvent;
import org.openhab.binding.satel.internal.event.SatelEvent;
import org.openhab.binding.satel.internal.event.SatelEventListener;
import org.openhab.binding.satel.internal.types.IntegraType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class represents abstract communication module and is responsible for
* exchanging data between the binding and connected physical module.
* Communication happens by sending commands and receiving response from the
* module. Each command class must extend {@link SatelCommand} and be added to
* <code>SatelModule.supportedCommands</code> map in
* <code>SatelModule.registerCommands</code> method.
*
* @author Krzysztof Goworek
* @since 1.7.0
*/
public abstract class SatelModule extends EventDispatcher implements SatelEventListener {
private static final Logger logger = LoggerFactory.getLogger(SatelModule.class);
private static final byte FRAME_SYNC = (byte) 0xfe;
private static final byte FRAME_SYNC_ESC = (byte) 0xf0;
private static final byte[] FRAME_START = { FRAME_SYNC, FRAME_SYNC };
private static final byte[] FRAME_END = { FRAME_SYNC, (byte) 0x0d };
private final BlockingQueue<SatelCommand> sendQueue = new LinkedBlockingQueue<SatelCommand>();
private IntegraType integraType;
private int timeout;
private String integraVersion;
private CommunicationChannel channel;
private Object channelLock;
private CommunicationWatchdog communicationWatchdog;
/*
* Helper interface for connecting and disconnecting to specific module
* type. Each module type should implement these methods to provide input
* and output streams and way to disconnect from the module.
*/
protected interface CommunicationChannel {
InputStream getInputStream() throws IOException;
OutputStream getOutputStream() throws IOException;
void disconnect();
}
/*
* Helper interface to handle communication timeouts.
*/
protected interface TimeoutTimer {
void start();
void stop();
}
/**
* Creates new instance of the class.
*
* @param timeout
* timeout value in milliseconds for connect/read/write
* operations
*/
public SatelModule(int timeout) {
this.integraType = IntegraType.UNKNOWN;
this.timeout = timeout;
this.channelLock = new Object();
addEventListener(this);
}
/**
* Returns type of Integra connected to the module.
*
* @return Integra type
*/
public IntegraType getIntegraType() {
return this.integraType;
}
/**
* Returns firmware revision of Integra connected to the module.
*
* @return version of Integra firmware
*/
public String getIntegraVersion() {
return this.integraVersion;
}
/**
* Returns configured timeout value.
*
* @return timeout value as milliseconds
*/
public int getTimeout() {
return this.timeout;
}
public boolean isConnected() {
return this.channel != null;
}
/**
* Returns status of initialization.
*
* @return <code>true</code> if module is properly initialized and ready for
* sending commands
*/
public boolean isInitialized() {
return this.integraType != IntegraType.UNKNOWN;
}
protected abstract CommunicationChannel connect();
/**
* Starts communication.
*/
public synchronized void open() {
if (this.communicationWatchdog == null) {
this.communicationWatchdog = new CommunicationWatchdog();
} else {
logger.warn("Module is already opened.");
}
}
/**
* Stops communication by disconnecting from the module and stopping all
* background tasks.
*/
public synchronized void close() {
if (this.communicationWatchdog != null) {
this.communicationWatchdog.close();
this.communicationWatchdog = null;
}
}
/**
* Enqueues specified command in send queue if not already enqueued.
*
* @param cmd
* command to enqueue
* @return <code>true</code> if operation succeeded
*/
public boolean sendCommand(SatelCommand cmd) {
return this.sendCommand(cmd, false);
}
/**
* Enqueues specified command in send queue.
*
* @param cmd
* command to enqueue
* @param force
* if <code>true</code> enqueues unconditionally
* @return <code>true</code> if operation succeeded
*/
public boolean sendCommand(SatelCommand cmd, boolean force) {
try {
if (force || !this.sendQueue.contains(cmd)) {
this.sendQueue.put(cmd);
cmd.setState(State.ENQUEUED);
logger.trace("Command enqueued: {}", cmd);
} else {
logger.debug("Command already in the queue: {}", cmd);
}
return true;
} catch (InterruptedException e) {
return false;
}
}
/**
* {@inheritDoc}
*/
@Override
public void incomingEvent(SatelEvent event) {
if (event instanceof IntegraVersionEvent) {
IntegraVersionEvent versionEvent = (IntegraVersionEvent) event;
this.integraType = IntegraType.valueOf(versionEvent.getType() & 0xFF);
this.integraVersion = versionEvent.getVersion();
logger.info("Connection to {} initialized. Version: {}.", this.integraType.getName(), this.integraVersion);
}
}
private SatelMessage readMessage() throws InterruptedException {
try {
InputStream is = this.channel.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
boolean inMessage = false;
int syncBytes = 0;
while (true) {
// if timed out, exit
int c = is.read();
if (c < 0) {
return null;
}
byte b = (byte) c;
if (b == FRAME_SYNC) {
if (inMessage) {
if (syncBytes == 0) {
// special sequence or end of message
// wait for next byte
} else {
logger.warn("Received frame sync bytes, discarding input: {}", baos.size());
// clear gathered bytes, we wait for new message
inMessage = false;
baos.reset();
}
}
++syncBytes;
} else {
if (inMessage) {
if (syncBytes == 0) {
// in sync, we have next message byte
baos.write(b);
} else if (syncBytes == 1) {
if (b == FRAME_SYNC_ESC) {
baos.write(FRAME_SYNC);
} else if (b == FRAME_END[1]) {
// end of message
break;
} else {
logger.warn("Received invalid byte {}, discarding input: {}", String.format("%02X", b),
baos.size());
// clear gathered bytes, we have new message
inMessage = false;
baos.reset();
}
} else {
logger.error("Sync bytes in message: {}", syncBytes);
}
} else if (syncBytes >= 2) {
// synced, we have first message byte
inMessage = true;
baos.write(b);
} else {
// discard all bytes until synced
}
syncBytes = 0;
}
// if meanwhile thread has been interrupted, exit the loop
if (Thread.interrupted()) {
throw new InterruptedException();
}
}
// return read message
return SatelMessage.fromBytes(baos.toByteArray());
} catch (IOException e) {
if (!Thread.currentThread().isInterrupted()) {
logger.error("Unexpected exception occurred during reading a message", e);
}
}
return null;
}
private boolean writeMessage(SatelMessage message) {
try {
OutputStream os = this.channel.getOutputStream();
os.write(FRAME_START);
for (byte b : message.getBytes()) {
os.write(b);
if (b == FRAME_SYNC) {
os.write(FRAME_SYNC_ESC);
}
}
os.write(FRAME_END);
os.flush();
return true;
} catch (IOException e) {
if (!Thread.currentThread().isInterrupted()) {
logger.error("Unexpected exception occurred during writing a message", e);
}
}
return false;
}
private synchronized void disconnect() {
// remove all pending commands from the queue
// notifying about send failure
while (!this.sendQueue.isEmpty()) {
SatelCommand cmd = this.sendQueue.poll();
cmd.setState(State.FAILED);
}
synchronized (this.channelLock) {
if (this.channel != null) {
this.channel.disconnect();
this.channel = null;
// notify about connection status change
this.dispatchEvent(new ConnectionStatusEvent(false));
}
}
}
private void communicationLoop(TimeoutTimer timeoutTimer) {
long reconnectionTime = 10 * 1000;
boolean receivedResponse = false;
SatelCommand command = null;
try {
while (!Thread.currentThread().isInterrupted()) {
// connect, if not connected yet
if (this.channel == null) {
long connectStartTime = System.currentTimeMillis();
synchronized (this) {
this.channel = connect();
}
if (this.channel == null) {
// notify about connection failure
this.dispatchEvent(new ConnectionStatusEvent(false));
// try to reconnect after a while, if connection hasn't
// been established
Thread.sleep(reconnectionTime - System.currentTimeMillis() + connectStartTime);
continue;
}
}
// get next command and send it
command = this.sendQueue.take();
logger.debug("Sending message: {}", command.getRequest());
timeoutTimer.start();
boolean sent = this.writeMessage(command.getRequest());
timeoutTimer.stop();
if (!sent) {
break;
}
command.setState(State.SENT);
// command sent, wait for response
logger.trace("Waiting for response");
timeoutTimer.start();
SatelMessage response = this.readMessage();
timeoutTimer.stop();
if (response == null) {
break;
}
logger.debug("Got response: {}", response);
if (!receivedResponse) {
receivedResponse = true;
// notify about connection success after first
// response from the module
this.dispatchEvent(new ConnectionStatusEvent(true));
}
if (command.handleResponse(this, response)) {
command.setState(State.SUCCEEDED);
} else {
command.setState(State.FAILED);
}
command = null;
}
} catch (InterruptedException e) {
// exit thread
} catch (Exception e) {
// unexpected error, log and exit thread
logger.info("Unhandled exception occurred in communication loop, disconnecting.", e);
} finally {
// stop counting if thread interrupted
timeoutTimer.stop();
}
// either send or receive failed
if (command != null) {
command.setState(State.FAILED);
}
disconnect();
}
/*
* Respawns communication thread in case on any error and interrupts it in
* case read/write operations take too long.
*/
private class CommunicationWatchdog extends Timer implements TimeoutTimer {
private Thread thread;
private volatile long lastActivity;
public CommunicationWatchdog() {
this.thread = null;
this.lastActivity = 0;
this.schedule(new TimerTask() {
@Override
public void run() {
CommunicationWatchdog.this.checkThread();
}
}, 0, 1000);
}
@Override
public void start() {
this.lastActivity = System.currentTimeMillis();
}
@Override
public void stop() {
this.lastActivity = 0;
}
public void close() {
// cancel timer first to prevent reconnect
this.cancel();
// then stop communication thread
if (this.thread != null) {
this.thread.interrupt();
try {
this.thread.join();
} catch (InterruptedException e) {
// ignore
}
}
}
private void startCommunication() {
if (this.thread != null && this.thread.isAlive()) {
logger.error("Start communication canceled: communication thread is still alive");
return;
}
// start new thread
this.thread = new Thread(new Runnable() {
@Override
public void run() {
logger.debug("Communication thread started");
SatelModule.this.communicationLoop(CommunicationWatchdog.this);
logger.debug("Communication thread stopped");
}
});
this.thread.start();
// if module is not initialized yet, send version command
if (!SatelModule.this.isInitialized()) {
SatelModule.this.sendCommand(new IntegraVersionCommand());
}
}
private void checkThread() {
logger.trace("Checking communication thread: {}, {}", this.thread != null,
Boolean.toString(this.thread != null && this.thread.isAlive()));
if (this.thread != null && this.thread.isAlive()) {
long timePassed = (this.lastActivity == 0) ? 0 : System.currentTimeMillis() - this.lastActivity;
if (timePassed > SatelModule.this.timeout) {
logger.error("Send/receive timeout, disconnecting module.");
stop();
this.thread.interrupt();
try {
// wait for the thread to terminate
this.thread.join(100);
} catch (InterruptedException e) {
// ignore
}
SatelModule.this.disconnect();
}
} else {
startCommunication();
}
}
}
}