/** * 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.io.transport.cul.internal.network; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.ReadableByteChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.nio.channels.WritableByteChannel; import java.nio.charset.Charset; import java.util.Iterator; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.lang.StringUtils; import org.openhab.io.transport.cul.CULDeviceException; import org.openhab.io.transport.cul.internal.AbstractCULHandler; import org.openhab.io.transport.cul.internal.CULConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Implementation for culfw based devices which communicate via network port * (CUN for example). * * @author Markus Heberling * @author Till Klocke * @since 1.5.0 */ public class CULNetworkHandlerImpl extends AbstractCULHandler<CULNetworkConfig> implements Runnable { private static final int CUN_DEFAULT_PORT = 2323; protected static final Logger logger = LoggerFactory.getLogger(CULNetworkHandlerImpl.class); private static final long INITIAL_RECONNECT_INTERVAL = 500; // 500 ms. private static final long MAXIMUM_RECONNECT_INTERVAL = 30000; // 30 sec. private static final int READ_BUFFER_SIZE = 2048; private static final int WRITE_BUFFER_SIZE = 2048; private long reconnectInterval = INITIAL_RECONNECT_INTERVAL; private ByteBuffer readBuf = ByteBuffer.allocateDirect(READ_BUFFER_SIZE); private ByteBuffer writeBuf = ByteBuffer.allocateDirect(WRITE_BUFFER_SIZE); private final Thread thread = new Thread(this); private SocketAddress address; private Selector selector; private SocketChannel channel; private final AtomicBoolean connected = new AtomicBoolean(false); private StringBuffer commandBuffer = new StringBuffer(); // Encoding for text based CUN protocol private Charset cs = Charset.forName("ASCII"); /** * Constructor including property map for specific configuration. Just for * compatibility with CulSerialHandlerImpl * * @param config config for the handler */ public CULNetworkHandlerImpl(CULConfig config) { super((CULNetworkConfig) config); } @Override protected void openHardware() throws CULDeviceException { String deviceName = config.getDeviceAddress(); logger.debug("Trying to open CUN with deviceName {}", deviceName); URI uri; try { uri = new URI("cul://" + deviceName); String host = uri.getHost(); int port = uri.getPort() == -1 ? CUN_DEFAULT_PORT : uri.getPort(); if (uri.getHost() == null || uri.getPort() == -1) { throw new CULDeviceException("Could not parse host:port from " + deviceName); } this.address = new InetSocketAddress(host, port); } catch (URISyntaxException e) { throw new CULDeviceException("Could not parse host:port from " + deviceName, e); } thread.start(); // Only return when we are connected, as soon as this method returns this Handler // is considered ready. while (!connected.get()) { try { Thread.sleep(10); } catch (InterruptedException e) { // Ignore } } } @Override protected void closeHardware() { thread.interrupt(); } @Override protected void write(String command) { ByteBuffer buf = cs.encode(command); try { send(buf); } catch (InterruptedException e) { logger.warn("InterruptedException when sending command", e); } catch (IOException e) { logger.warn("IOException when sending command", e); } } private void send(ByteBuffer buffer) throws InterruptedException, IOException { if (!connected.get()) { throw new IOException("not connected"); } synchronized (writeBuf) { // try direct write of what's in the buffer to free up space if (writeBuf.remaining() < buffer.remaining()) { writeBuf.flip(); int bytesOp = 0, bytesTotal = 0; while (writeBuf.hasRemaining() && (bytesOp = channel.write(writeBuf)) > 0) { bytesTotal += bytesOp; } writeBuf.compact(); logger.debug("Written {} bytes to the network", bytesTotal); } // if didn't help, wait till some space appears if (Thread.currentThread().getId() != thread.getId()) { while (writeBuf.remaining() < buffer.remaining()) { writeBuf.wait(); } } else { if (writeBuf.remaining() < buffer.remaining()) { throw new IOException("send buffer full"); // TODO: add reallocation or buffers chain } } writeBuf.put(buffer); // try direct write to decrease the latency writeBuf.flip(); int bytesOp = 0, bytesTotal = 0; while (writeBuf.hasRemaining() && (bytesOp = channel.write(writeBuf)) > 0) { bytesTotal += bytesOp; } writeBuf.compact(); logger.debug("Written {} bytes to the network", bytesTotal); if (writeBuf.hasRemaining()) { SelectionKey key = channel.keyFor(selector); key.interestOps(key.interestOps() | SelectionKey.OP_WRITE); selector.wakeup(); } } } private void configureChannel(SocketChannel channel) throws IOException { channel.configureBlocking(false); channel.socket().setSendBufferSize(0x100000); // 1Mb channel.socket().setReceiveBufferSize(0x100000); // 1Mb channel.socket().setKeepAlive(true); channel.socket().setReuseAddress(true); channel.socket().setSoLinger(false, 0); channel.socket().setSoTimeout(0); channel.socket().setTcpNoDelay(true); } @Override public void run() { logger.info("event loop starting"); try { while (!Thread.interrupted()) { // reconnection loop try { selector = Selector.open(); channel = SocketChannel.open(); configureChannel(channel); channel.connect(address); channel.register(selector, SelectionKey.OP_CONNECT); while (!thread.isInterrupted() && channel.isOpen()) { // events multiplexing loop if (selector.select() > 0) { processSelectedKeys(selector.selectedKeys()); } } } catch (Exception e) { logger.warn("exception", e); } finally { connected.set(false); onDisconnected(); writeBuf.clear(); readBuf.clear(); if (channel != null) { channel.close(); } if (selector != null) { selector.close(); } logger.info("connection closed"); } try { Thread.sleep(reconnectInterval); if (reconnectInterval < MAXIMUM_RECONNECT_INTERVAL) { reconnectInterval *= 2; } logger.info("reconnecting to {}", address); } catch (InterruptedException e) { break; } } } catch (Exception e) { logger.warn("unrecoverable error", e); } logger.info("event loop terminated"); } private void onDisconnected() { logger.info("Disconnected from {}", address); } private void processSelectedKeys(Set<SelectionKey> keys) throws Exception { Iterator<SelectionKey> itr = keys.iterator(); while (itr.hasNext()) { SelectionKey key = itr.next(); if (key.isReadable()) { processRead(key); } if (key.isWritable()) { processWrite(key); } if (key.isConnectable()) { processConnect(key); } if (key.isAcceptable()) { ; } itr.remove(); } } private void processConnect(SelectionKey key) throws Exception { SocketChannel ch = (SocketChannel) key.channel(); if (ch.finishConnect()) { key.interestOps(key.interestOps() ^ SelectionKey.OP_CONNECT); key.interestOps(key.interestOps() | SelectionKey.OP_READ); reconnectInterval = INITIAL_RECONNECT_INTERVAL; connected.set(true); onConnected(); } } private void onConnected() { logger.info("Connected to CUN ({})", address); } private void processRead(SelectionKey key) throws Exception { ReadableByteChannel ch = (ReadableByteChannel) key.channel(); int bytesOp = 0, bytesTotal = 0; while (readBuf.hasRemaining() && (bytesOp = ch.read(readBuf)) > 0) { bytesTotal += bytesOp; } logger.debug("Read {} bytes from network", bytesTotal); if (bytesTotal > 0) { readBuf.flip(); onRead(readBuf); readBuf.compact(); } else if (bytesOp == -1) { logger.info("peer closed read channel"); ch.close(); } } private void onRead(ByteBuffer readBuf) { CharBuffer charBuf = cs.decode(readBuf); while (charBuf.hasRemaining()) { char currentChar = charBuf.get(); if (currentChar == '\r' || currentChar == '\n') { String command = commandBuffer.toString(); if (!StringUtils.isEmpty(command)) { processNextLine(command); } commandBuffer = new StringBuffer(); } else { commandBuffer.append(currentChar); } } } private void processWrite(SelectionKey key) throws IOException { WritableByteChannel ch = (WritableByteChannel) key.channel(); synchronized (writeBuf) { writeBuf.flip(); int bytesOp = 0, bytesTotal = 0; while (writeBuf.hasRemaining() && (bytesOp = ch.write(writeBuf)) > 0) { bytesTotal += bytesOp; } logger.debug("Written {} bytes to the network", bytesTotal); if (writeBuf.remaining() == 0) { key.interestOps(key.interestOps() ^ SelectionKey.OP_WRITE); } if (bytesTotal > 0) { writeBuf.notify(); } else if (bytesOp == -1) { logger.info("peer closed write channel"); ch.close(); } writeBuf.compact(); } } }