/** * 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.insteonhub.internal.hardware.api.serial; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import org.apache.commons.codec.binary.Hex; import org.openhab.binding.insteonhub.internal.hardware.InsteonHubLevelUpdateType; import org.openhab.binding.insteonhub.internal.hardware.InsteonHubMsgConst; import org.openhab.binding.insteonhub.internal.hardware.InsteonHubProxyListener; import org.openhab.binding.insteonhub.internal.util.InsteonHubBindingLogUtil; import org.openhab.binding.insteonhub.internal.util.InsteonHubByteUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class does all of the heaving lifting for serial I/O communication with * the Insteon Hub. * * @author Eric Thill * @since 1.4.0 */ public class InsteonHubSerialTransport { private static final Logger logger = LoggerFactory.getLogger(InsteonHubSerialTransport.class); private final BlockingQueue<byte[]> commandQueue = new LinkedBlockingQueue<byte[]>(); private final Set<InsteonHubProxyListener> listeners = new HashSet<InsteonHubProxyListener>(); private final InsteonHubSerialProxy proxy; private volatile Listener listener; private volatile Sender sender; private InputStream inputStream; private OutputStream outputStream; public InsteonHubSerialTransport(InsteonHubSerialProxy proxy) { this.proxy = proxy; } public synchronized boolean isStarted() { return inputStream != null; } public synchronized void start(InputStream in, OutputStream out) { this.inputStream = in; this.outputStream = out; listener = new Listener(); sender = new Sender(); new Thread(listener, proxy.getConnectionString() + " listener").start(); new Thread(sender, proxy.getConnectionString() + " sender").start(); } public synchronized void stop() { inputStream = null; outputStream = null; listener = null; sender = null; } public void enqueueCommand(byte[] msg) { commandQueue.add(msg); } public void addListener(InsteonHubProxyListener listener) { synchronized (listeners) { listeners.add(listener); } } public void removeListener(InsteonHubProxyListener listener) { synchronized (listeners) { listeners.remove(listener); } } // Takes commands off the command queue and sends them to the Hub. private class Sender implements Runnable { @Override public void run() { try { // check run condition while (sender == this) { // take message off queue byte[] msg = null; try { msg = commandQueue.poll(5, TimeUnit.SECONDS); } catch (InterruptedException e) { // ignore: msg will be null and not processed } // process message if (msg != null) { outputStream.write(msg); outputStream.flush(); } } } catch (Throwable t) { t.printStackTrace(); InsteonHubBindingLogUtil.logCommunicationFailure(logger, proxy, t); proxy.reconnect(); } } }; // Listens for messages from the Hub and passes them to the handleMessage // method private class Listener implements Runnable { @Override public void run() { try { while (listener == this) { // read next messages byte[] msg = readMsg(inputStream); // if msg was read, pass to handleMessage if (msg != null) { handleMessage(msg); } } } catch (Throwable t) { InsteonHubBindingLogUtil.logCommunicationFailure(logger, proxy, t); proxy.reconnect(); } } private byte[] readMsg(InputStream in) throws IOException { // read to 0x02 "start of message" byte b; while ((b = InsteonHubByteUtil.readByte(in)) != InsteonHubMsgConst.STX) { if (logger.isDebugEnabled()) { logger.debug("Ignoring non STX byte: " + b); } } // read command type byte byte cmd = InsteonHubByteUtil.readByte(in); // based on command type, figure out number of messages to read Integer msgSize = InsteonHubMsgConst.REC_MSG_SIZES.get(cmd); if (msgSize == null) { // we may go out of sync... log this. We need to add/fix // REC_MSG_SIZES // FIXME change to warn. There is currently a known bug with extended message types showing this, so // it's debug for now. logger.debug("Received unknown command type '" + cmd + "' - If you see this frequently, " + "please save debug logs and report this as a bug."); return null; } byte[] msg = new byte[msgSize]; msg[0] = InsteonHubMsgConst.STX; msg[1] = cmd; InsteonHubByteUtil.fillBuffer(in, msg, 2); if (cmd == InsteonHubMsgConst.SND_CODE_SEND_INSTEON_STD_OR_EXT_MSG) { if (new InsteonHubStdMsgFlags(msg[5]).isExtended()) { // read 14 more bytes and add them to the end of the msg byte[] extendedBytes = new byte[14]; InsteonHubByteUtil.fillBuffer(in, extendedBytes, 0); byte[] extMsg = new byte[msg.length + extendedBytes.length]; System.arraycopy(msg, 0, extMsg, 0, msg.length); System.arraycopy(extendedBytes, 0, extMsg, msg.length, extendedBytes.length); msg = extMsg; } } if (logger.isDebugEnabled()) { logger.debug("Received Message from INSTEON Hub: " + Hex.encodeHexString(msg)); } return msg; } } private void handleMessage(byte[] msg) { byte cmd = msg[1]; if (cmd == InsteonHubMsgConst.REC_CODE_INSTEON_STD_MSG) { // INSTEON Standard Message // parse device and flag String device = InsteonHubByteUtil.encodeDeviceHex(msg, 2); InsteonHubStdMsgFlags flags = new InsteonHubStdMsgFlags(InsteonHubByteUtil.byteToUnsignedInt(msg[8])); if (flags.isAck() && msg[9] == InsteonHubMsgConst.CMD1_STATUS_REQUEST) { // ack flag => response to value check int level = InsteonHubByteUtil.byteToUnsignedInt(msg[10]); if (logger.isDebugEnabled()) { logger.debug("Alerting level update device='" + device + "' level=" + level); } alertLevelUpdate(device, level, InsteonHubLevelUpdateType.STATUS_RESPONSE); } else { // not an ack => check if this could have changed a value byte cmd1 = msg[9]; switch (cmd1) { case InsteonHubMsgConst.CMD1_OFF: case InsteonHubMsgConst.CMD1_ON: case InsteonHubMsgConst.CMD1_OFF_FAST: case InsteonHubMsgConst.CMD1_ON_FAST: // On or Off => 255 or 0 level alertLevelUpdate(device, cmd1 == InsteonHubMsgConst.CMD1_ON || cmd1 == InsteonHubMsgConst.CMD1_ON_FAST ? 255 : 0, InsteonHubLevelUpdateType.STATUS_CHANGE); case InsteonHubMsgConst.CMD1_DIM: case InsteonHubMsgConst.CMD1_BRT: case InsteonHubMsgConst.CMD1_STOP_DIM_BRT: // something analog changed => request level proxy.requestDeviceLevel(device); if (logger.isTraceEnabled()) { logger.trace("Requesting level for device " + device); } break; } } } else if (cmd == InsteonHubMsgConst.SND_CODE_SEND_INSTEON_STD_OR_EXT_MSG) { // INSTEON ACK/NAK Message byte ack = msg[8]; if (ack == InsteonHubMsgConst.ACK) { if (logger.isTraceEnabled()) { logger.trace("Received message with ACK: " + Hex.encodeHexString(msg)); } } else if (ack == InsteonHubMsgConst.NAK) { if (logger.isDebugEnabled()) { logger.debug("Received message with NAK: " + Hex.encodeHexString(msg) + " - Will resend!"); } // parse original message from the NAK message (NAK is an // added // last byte) byte[] originMsg = Arrays.copyOfRange(msg, 0, msg.length - 1); // re-send the message // (NAK means message could not be handled at that time) enqueueCommand(originMsg); } } } private void alertLevelUpdate(String device, int level, InsteonHubLevelUpdateType updateType) { synchronized (listeners) { for (InsteonHubProxyListener listener : listeners) { listener.onLevelUpdate(device.toUpperCase(), level, updateType); } } } }