net.solarnetwork.node.io.serial.rxtx.SerialPortConnection.java Source code

Java tutorial

Introduction

Here is the source code for net.solarnetwork.node.io.serial.rxtx.SerialPortConnection.java

Source

/* ==================================================================
 * SerialPortConnection.java - Oct 23, 2014 2:21:31 PM
 * 
 * Copyright 2007-2014 SolarNetwork.net Dev Team
 * 
 * This program is free software; you can redistribute it and/or 
 * modify it under the terms of the GNU General Public License as 
 * published by the Free Software Foundation; either version 2 of 
 * the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License 
 * along with this program; if not, write to the Free Software 
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 
 * 02111-1307 USA
 * ==================================================================
 */

package net.solarnetwork.node.io.serial.rxtx;

import gnu.io.CommPortIdentifier;
import gnu.io.NoSuchPortException;
import gnu.io.PortInUseException;
import gnu.io.SerialPort;
import gnu.io.SerialPortEvent;
import gnu.io.SerialPortEventListener;
import gnu.io.UnsupportedCommOperationException;
import gnu.trove.list.array.TByteArrayList;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.TooManyListenersException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import net.solarnetwork.node.LockTimeoutException;
import net.solarnetwork.node.io.serial.SerialConnection;
import net.solarnetwork.node.support.SerialPortBeanParameters;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * RXTX implementation of {@link SerialConnection}.
 * 
 * @author matt
 * @version 1.2
 */
public class SerialPortConnection implements SerialConnection, SerialPortEventListener {

    /** A class-level logger. */
    private static final Logger log = LoggerFactory.getLogger(SerialPortConnection.class);

    /** A class-level logger with the suffix SERIAL_EVENT. */
    private static final Logger eventLog = LoggerFactory
            .getLogger(SerialPortConnection.class.getName() + ".SERIAL_EVENT");

    private final SerialPortBeanParameters serialParams;
    private final ExecutorService executor;

    private SerialPort serialPort;
    private InputStream in;
    private OutputStream out;
    private final boolean listening = false;
    private final boolean collecting = false;

    /**
     * Constructor.
     * 
     * @param serialParams
     *        the parameters to use with the SerialPort
     * @param executor
     *        A thread pool to use for I/O tasks with timeouts.
     * @param maxWait
     *        the maximum number of milliseconds to wait when waiting to read
     *        data
     */
    public SerialPortConnection(SerialPortBeanParameters params, ExecutorService executor) {
        this.serialParams = params;
        this.executor = executor;
    }

    @Override
    public void open() throws IOException, LockTimeoutException {
        CommPortIdentifier portId = getCommPortIdentifier(serialParams.getSerialPort());
        try {
            serialPort = (SerialPort) portId.open(serialParams.getCommPortAppName(), 2000);
            setupSerialPortParameters(serialPort, this);
        } catch (PortInUseException e) {
            throw new IOException("Serial port " + serialParams.getSerialPort() + " in use", e);
        } catch (TooManyListenersException e) {
            try {
                close();
            } catch (Exception e2) {
                // ignore this
            }
            throw new IOException("Serial port " + serialParams.getSerialPort() + " has too many listeners", e);
        }
    }

    /**
     * Test if the serial port has been opened.
     * 
     * @return boolean
     */
    public boolean isOpen() {
        return (serialPort != null);
    }

    @Override
    public void close() {
        if (serialPort == null) {
            return;
        }
        try {
            log.debug("Closing serial port {}", this.serialPort);
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    // ignore this
                    log.warn("Exception closing serial port {} input stream: {}", this.serialPort, e.getMessage());
                }
            }
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    // ignore this
                    log.warn("Exception closing serial port {} output stream: {}", this.serialPort, e.getMessage());
                }
            }
            serialPort.close();
            log.trace("Serial port {} closed", this.serialPort);
        } finally {
            in = null;
            out = null;
            serialPort = null;
        }
    }

    @Override
    public byte[] readMarkedMessage(final byte[] startMarker, final int length) throws IOException {
        final TByteArrayList sink = new TByteArrayList(startMarker.length + length);
        final byte[] buf = new byte[64];
        boolean result = false;
        if (serialParams.getMaxWait() < 1) {
            do {
                result = readMarkedMessage(getInputStream(), sink, buf, startMarker, length);
            } while (!result);
            return sink.toArray();
        }
        AbortableCallable<Boolean> task = new AbortableCallable<Boolean>() {

            private boolean keepGoing = true;

            @Override
            public Boolean call() throws Exception {
                boolean found = false;
                do {
                    found = readMarkedMessage(getInputStream(), sink, buf, startMarker, length);
                } while (!found && keepGoing);
                return found;
            }

            @Override
            public void abort() {
                keepGoing = false;
            }

        };
        result = performIOTaskWithMaxWait(task);
        return (result ? sink.toArray() : null);
    }

    private boolean readMarkedMessage(final InputStream in, final TByteArrayList sink, final byte[] buf,
            final byte[] startMarker, final int length) throws IOException {
        boolean lookingForEndMarker = (sink.size() > startMarker.length);
        int max = (lookingForEndMarker ? length - sink.size() : startMarker.length);
        if (eventLog.isTraceEnabled()) {
            eventLog.trace("Sink contains {} bytes: {}", sink.size(), asciiDebugValue(sink.toArray()));
        }
        int len = -1;
        eventLog.trace("Attempting to read up to {} bytes from serial port", max);
        while (max > 0 && (len = in.read(buf, 0, max > buf.length ? buf.length : max)) > 0) {
            sink.add(buf, 0, len);
            if (lookingForEndMarker == false) {
                int foundMarkerByteCount = findMarkerBytes(sink, len, startMarker, false);
                if (foundMarkerByteCount == startMarker.length) {
                    lookingForEndMarker = true;
                }
            }
            if (lookingForEndMarker) {
                if (sink.size() == length) {
                    return true;
                }
                max = (length - sink.size());
                eventLog.debug("Looking for {} more message bytes, buffer: {}", max,
                        asciiDebugValue(sink.toArray()));
            }
        }
        return false;
    }

    @Override
    public void writeMessage(final byte[] message) throws IOException {
        if (eventLog.isTraceEnabled()) {
            eventLog.trace("Attempting to write {} bytes to serial port: {}", message.length,
                    asciiDebugValue(message));
        }
        if (serialParams.getMaxWait() < 1) {
            getOutputStream().write(message);
            return;
        }
        performIOTaskWithMaxWait(new NoResultUnabortableCallable() {

            @Override
            protected void doCall() throws Exception {
                OutputStream stream = getOutputStream();
                stream.write(message);
                stream.flush();
            }
        });
    }

    @Override
    public byte[] drainInputBuffer() throws IOException {
        InputStream in = getInputStream();
        int avail = in.available();
        if (avail < 1) {
            return new byte[0];
        }
        eventLog.trace("Attempting to drain {} bytes from serial port", avail);
        byte[] result = new byte[avail];
        int count = 0;
        while (count < result.length) {
            count += in.read(result, count, result.length - count);
        }
        eventLog.trace("Drained {} bytes from serial port", result.length);
        return result;
    }

    @Override
    public byte[] readMarkedMessage(final byte[] startMarker, final byte[] endMarker) throws IOException {
        final TByteArrayList sink = new TByteArrayList(1024);
        final byte[] buf = new byte[64];
        boolean result = false;
        if (serialParams.getMaxWait() < 1) {
            do {
                result = readMarkedMessage(getInputStream(), sink, buf, startMarker, endMarker);
            } while (!result);
            return sink.toArray();
        }

        AbortableCallable<Boolean> task = new AbortableCallable<Boolean>() {

            private boolean keepGoing = true;

            @Override
            public Boolean call() throws Exception {
                boolean found = false;
                do {
                    found = readMarkedMessage(getInputStream(), sink, buf, startMarker, endMarker);
                } while (!found && keepGoing);
                return found;
            }

            @Override
            public void abort() {
                keepGoing = false;
            }

        };
        result = performIOTaskWithMaxWait(task);
        return (result ? sink.toArray() : null);
    }

    private boolean readMarkedMessage(final InputStream in, final TByteArrayList sink, final byte[] buf,
            final byte[] startMarker, final byte[] endMarker) throws IOException {
        boolean lookingForEndMarker = (sink.size() > startMarker.length);
        int max = (lookingForEndMarker ? endMarker.length : startMarker.length);
        if (eventLog.isTraceEnabled()) {
            eventLog.trace("Sink contains {} bytes: {}", sink.size(), asciiDebugValue(sink.toArray()));
        }
        int len = -1;
        eventLog.trace("Attempting to read up to {} bytes from serial port", max);
        while (max > 0 && (len = in.read(buf, 0, max > buf.length ? buf.length : max)) > 0) {
            sink.add(buf, 0, len);
            int foundMarkerByteCount = findMarkerBytes(sink, len, (lookingForEndMarker ? endMarker : startMarker),
                    lookingForEndMarker);
            if (lookingForEndMarker == false && foundMarkerByteCount == startMarker.length) {
                lookingForEndMarker = true;
                // immediately look for end marker, might already be in the buffer
                foundMarkerByteCount = findMarkerBytes(sink, startMarker.length, endMarker, true);
            }
            if (lookingForEndMarker && foundMarkerByteCount == endMarker.length) {
                return true;
            }
        }
        if (eventLog.isTraceEnabled()) {
            eventLog.debug("Looking for marker {}, buffer: {}",
                    (lookingForEndMarker ? asciiDebugValue(endMarker) : asciiDebugValue(startMarker)),
                    asciiDebugValue(sink.toArray()));
        }
        return false;
    }

    private <T> T performIOTaskWithMaxWait(AbortableCallable<T> task) throws IOException {
        T result = null;
        Future<T> future = executor.submit(task);
        final long maxMs = Math.max(1, serialParams.getMaxWait());
        eventLog.trace("Waiting at most {}ms for data", maxMs);
        try {
            result = future.get(maxMs, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            log.debug("Interrupted communicating with serial port", e);
            throw new IOException("Interrupted communicating with serial port", e);
        } catch (ExecutionException e) {
            log.debug("Exception thrown communicating with serial port", e.getCause());
            throw new IOException("Exception thrown communicating with serial port", e.getCause());
        } catch (TimeoutException e) {
            log.warn("Timeout waiting {}ms for serial data, aborting operation", maxMs);
            future.cancel(true);
            throw new LockTimeoutException("Timeout waiting " + serialParams.getMaxWait() + "ms for serial data");
        } finally {
            task.abort();
        }
        return result;
    }

    private InputStream getInputStream() throws IOException {
        if (in != null) {
            return in;
        }
        if (!isOpen()) {
            open();
        }
        in = getSerialPort().getInputStream();
        return in;
    }

    private OutputStream getOutputStream() throws IOException {
        if (out != null) {
            return out;
        }
        if (!isOpen()) {
            open();
        }
        out = getSerialPort().getOutputStream();
        return out;
    }

    @SuppressWarnings("unchecked")
    private CommPortIdentifier getCommPortIdentifier(final String portId) throws IOException {
        // first try directly
        CommPortIdentifier commPortId = null;
        try {
            commPortId = CommPortIdentifier.getPortIdentifier(portId);
            if (commPortId != null) {
                log.debug("Found port identifier: {}", portId);
                return commPortId;
            }
        } catch (NoSuchPortException e) {
            log.debug("Port {} not found, inspecting available ports...", portId);
        }
        Enumeration<CommPortIdentifier> portIdentifiers = CommPortIdentifier.getPortIdentifiers();
        List<String> foundNames = new ArrayList<String>(5);
        while (portIdentifiers.hasMoreElements()) {
            CommPortIdentifier commPort = portIdentifiers.nextElement();
            log.trace("Inspecting available port identifier: {}", commPort.getName());
            foundNames.add(commPort.getName());
            if (commPort.getPortType() == CommPortIdentifier.PORT_SERIAL && portId.equals(commPort.getName())) {
                commPortId = commPort;
                log.debug("Found port identifier: {}", portId);
                break;
            }
        }
        if (commPortId == null) {
            throw new IOException(
                    "Couldn't find port identifier for [" + portId + "]; available ports: " + foundNames);
        }
        return commPortId;
    }

    @Override
    public void serialEvent(SerialPortEvent event) {
        if (eventLog.isTraceEnabled() && event.getEventType() != SerialPortEvent.DATA_AVAILABLE) {
            eventLog.trace("SerialPortEvent {}; listening {}; collecting {}",
                    new Object[] { event.getEventType(), listening, collecting });
        }
    }

    /**
     * Set up the SerialPort for use, configuring with class properties.
     * 
     * <p>
     * This method can be called once when wanting to start using the serial
     * port.
     * </p>
     * 
     * @param serialPort
     *        the serial port to setup
     * @param listener
     *        a listener to pass to
     *        {@link SerialPort#addEventListener(SerialPortEventListener)}
     */
    private void setupSerialPortParameters(SerialPort serialPort, SerialPortEventListener listener)
            throws TooManyListenersException {
        if (listener != null) {
            serialPort.addEventListener(listener);
        }

        serialPort.notifyOnDataAvailable(true);

        try {

            if (serialParams.getReceiveFraming() >= 0) {
                serialPort.enableReceiveFraming(serialParams.getReceiveFraming());
                if (!serialPort.isReceiveFramingEnabled()) {
                    log.warn("Receive framing configured as {} but not supported by driver.",
                            serialParams.getReceiveFraming());
                } else if (log.isDebugEnabled()) {
                    log.debug("Receive framing set to {}", serialParams.getReceiveFraming());
                }
            } else {
                serialPort.disableReceiveFraming();
            }

            if (serialParams.getReceiveTimeout() >= 0) {
                serialPort.enableReceiveTimeout(serialParams.getReceiveTimeout());
                if (!serialPort.isReceiveTimeoutEnabled()) {
                    log.warn("Receive timeout configured as {} but not supported by driver.",
                            serialParams.getReceiveTimeout());
                } else if (log.isDebugEnabled()) {
                    log.debug("Receive timeout set to {}", serialParams.getReceiveTimeout());
                }
            } else {
                serialPort.disableReceiveTimeout();
            }
            if (serialParams.getReceiveThreshold() >= 0) {
                serialPort.enableReceiveThreshold(serialParams.getReceiveThreshold());
                if (!serialPort.isReceiveThresholdEnabled()) {
                    log.warn("Receive threshold configured as [{}] but not supported by driver.",
                            serialParams.getReceiveThreshold());
                } else if (log.isDebugEnabled()) {
                    log.debug("Receive threshold set to {}", serialParams.getReceiveThreshold());
                }
            } else {
                serialPort.disableReceiveThreshold();
            }

            if (log.isDebugEnabled()) {
                log.debug("Setting serial port baud = {}, dataBits = {}, stopBits = {}, parity = {}",
                        new Object[] { serialParams.getBaud(), serialParams.getDataBits(),
                                serialParams.getStopBits(), serialParams.getParity() });
            }
            serialPort.setSerialPortParams(serialParams.getBaud(), serialParams.getDataBits(),
                    serialParams.getStopBits(), serialParams.getParity());

            if (serialParams.getFlowControl() >= 0) {
                log.debug("Setting flow control to {}", serialParams.getFlowControl());
                serialPort.setFlowControlMode(serialParams.getFlowControl());
            }

            if (serialParams.getDtrFlag() >= 0) {
                boolean mode = serialParams.getDtrFlag() > 0 ? true : false;
                log.debug("Setting DTR to {}", mode);
                serialPort.setDTR(mode);
            }
            if (serialParams.getRtsFlag() >= 0) {
                boolean mode = serialParams.getRtsFlag() > 0 ? true : false;
                log.debug("Setting RTS to {}", mode);
                serialPort.setRTS(mode);
            }

        } catch (UnsupportedCommOperationException e) {
            throw new RuntimeException(e);
        }
    }

    private int findMarkerBytes(final TByteArrayList sink, final int appendedLength, final byte[] marker,
            final boolean end) {
        //final byte[] sinkBuf = sink.toArray();
        final int sinkBufLength = sink.size();
        int markerIdx = Math.max(0, sinkBufLength - appendedLength - marker.length);
        boolean foundMarker = false;
        int j = 0;

        eventLog.trace("Looking for {} marker bytes {} in buffer {}",
                new Object[] { marker.length, asciiDebugValue(marker), asciiDebugValue(sink.toArray()) });
        for (; markerIdx < sinkBufLength; markerIdx++) {
            foundMarker = true;
            for (j = 0; j < marker.length && (j + markerIdx) < sinkBufLength; j++) {
                if (sink.getQuick(markerIdx + j) != marker[j]) {
                    foundMarker = false;
                    break;
                }
            }
            if (foundMarker) {
                break;
            }
        }
        // we may have only found a partial match at the end of the buffer, so test j here
        if (foundMarker && j == marker.length) {
            if (eventLog.isDebugEnabled()) {
                eventLog.debug("Found desired {} marker bytes at index {}", asciiDebugValue(marker), markerIdx);
            }
            if (end) {
                sink.remove(markerIdx + marker.length, sink.size() - markerIdx - marker.length);
            } else {
                // shift bytes to start at marker
                sink.remove(0, markerIdx);
            }
            if (eventLog.isDebugEnabled()) {
                eventLog.debug("Buffer message at marker: {}", asciiDebugValue(sink.toArray()));
            }
            return marker.length;
        } else if (!end) {
            // truncate sink to any partial match
            if (j > 0) {
                sink.remove(0, markerIdx);
            } else {
                sink.resetQuick();
            }
        }
        return j;
    }

    private String asciiDebugValue(byte[] data) {
        if (data == null || data.length < 1) {
            return "";
        }
        StringBuilder buf = new StringBuilder();
        buf.append(Hex.encodeHex(data)).append(" (");
        for (byte b : data) {
            if (b >= 32 && b < 126) {
                buf.append(Character.valueOf((char) b));
            } else {
                buf.append('~');
            }
        }
        buf.append(")");
        return buf.toString();
    }

    public SerialPort getSerialPort() {
        return serialPort;
    }

}