org.openhab.io.transport.cul.internal.network.CULNetworkHandlerImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.io.transport.cul.internal.network.CULNetworkHandlerImpl.java

Source

/**
 * Copyright (c) 2010-2019 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();
        }
    }
}