net.solarnetwork.node.io.rxtx.SerialPortSupport.java Source code

Java tutorial

Introduction

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

Source

/* ===================================================================
 * SerialPortSupport.java
 * 
 * Created Aug 19, 2009 11:25:27 AM
 * 
 * Copyright (c) 2009 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.rxtx;

import gnu.io.SerialPort;
import gnu.io.SerialPortEvent;
import gnu.io.SerialPortEventListener;
import gnu.io.UnsupportedCommOperationException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.TooManyListenersException;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import net.solarnetwork.node.support.SerialPortBean;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A base class with properties to support {@link SerialPort} communication.
 * 
 * @author matt
 * @version 1.1
 */
public abstract class SerialPortSupport extends SerialPortBean {

    /** The SerialPort. */
    protected SerialPort serialPort;

    private final long maxWait;
    private final ExecutorService executor;
    private long timeout = 0;

    /** A class-level logger. */
    protected final Logger log = LoggerFactory.getLogger(getClass());

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

    /**
     * Constructor.
     * 
     * @param serialPort
     *        the SerialPort to use
     * @param maxWait
     *        the maximum number of milliseconds to wait when waiting to read
     *        data
     */
    public SerialPortSupport(SerialPort serialPort, long maxWait) {
        this.serialPort = serialPort;
        this.maxWait = maxWait;
        if (maxWait > 0) {
            executor = Executors.newFixedThreadPool(1);
        } else {
            executor = null;
        }
    }

    /**
     * Set a "timeout" flag, so that all subsequent calls to
     * {@link #handleSerialEvent(SerialPortEvent, InputStream, ByteArrayOutputStream, byte[], int)}
     * use this as the reference point for calculating the maximum time to wait
     * for serial data.
     * 
     * <p>
     * When called, the {@code handleSerialEvent} method will treat the time
     * offset from the call to this method as the reference amount of time that
     * has passed before the {@code maxWait} value triggers a timeout.
     * </p>
     */
    protected void timeoutStart() {
        if (maxWait > 0) {
            timeout = System.currentTimeMillis();
        }
    }

    /**
     * Clear the timeout flag, so no timeout used.
     * 
     * @see #timeoutStart()
     */
    protected void timeoutClear() {
        timeout = 0;
    }

    /**
     * Close the connected serial port.
     */
    protected void closeSerialPort() {
        if (this.serialPort != null) {
            log.debug("Closing serial port {}", this.serialPort);
            this.serialPort.close();
            log.trace("Serial port closed");
            if (executor != null) {
                executor.shutdownNow();
            }
        }
    }

    /**
     * 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 listener
     *        a listener to pass to
     *        {@link SerialPort#addEventListener(SerialPortEventListener)}
     */
    protected void setupSerialPortParameters(SerialPortEventListener listener) {
        if (listener != null) {
            try {
                serialPort.addEventListener(listener);
            } catch (TooManyListenersException e) {
                throw new RuntimeException(e);
            }
        }

        serialPort.notifyOnDataAvailable(true);

        try {

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

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

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

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

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

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

    /**
     * Handle a SerialEvent, looking for "magic" data.
     * 
     * <p>
     * <b>Note</b> that the <em>magic</em> bytes are <em>not</em> returned by
     * this method, they are stripped from the output buffer.
     * </p>
     * 
     * @param event
     *        the event
     * @param in
     *        the InputStream to read data from
     * @param sink
     *        the output buffer to store the collected bytes
     * @param magicBytes
     *        the "magic" bytes to look for in the event stream
     * @param readLength
     *        the number of bytes, excluding the magic bytes, to read from the
     *        stream
     * @return <em>true</em> if the data has been found
     * @throws TimeoutException
     *         if {@code maxWait} is configured and that amount of time passes
     *         before the requested serial data is read
     */
    protected boolean handleSerialEvent(final SerialPortEvent event, final InputStream in,
            final ByteArrayOutputStream sink, final byte[] magicBytes, final int readLength)
            throws TimeoutException, InterruptedException, ExecutionException {
        if (timeout < 1) {
            return handleSerialEventWithoutTimeout(event, in, sink, magicBytes, readLength);
        }

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

            @Override
            public Boolean call() throws Exception {
                return handleSerialEventWithoutTimeout(event, in, sink, magicBytes, readLength);
            }
        };
        Future<Boolean> future = executor.submit(task);
        boolean result = false;
        final long maxMs = Math.max(1, this.maxWait - System.currentTimeMillis() + timeout);
        eventLog.trace("Waiting at most {}ms for data", maxMs);
        try {
            result = future.get(maxMs, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            log.debug("Interrupted waiting for serial data");
            throw e;
        } catch (ExecutionException e) {
            // log stack trace in DEBUG
            log.debug("Exception thrown reading from serial port", e.getCause());
            throw e;
        } catch (TimeoutException e) {
            log.warn("Timeout waiting {}ms for serial data, aborting read", maxMs);
            future.cancel(true);
            throw e;
        }
        return result;
    }

    private boolean handleSerialEventWithoutTimeout(SerialPortEvent event, InputStream in,
            ByteArrayOutputStream sink, byte[] magicBytes, int readLength) {
        int sinkSize = sink.size();
        boolean append = sinkSize > 0;
        byte[] buf = new byte[Math.min(readLength, 1024)];
        if (eventLog.isTraceEnabled()) {
            eventLog.trace("Sink contains {} bytes: {}", sinkSize, asciiDebugValue(sink.toByteArray()));
        }
        try {
            int len = -1;
            final int max = Math.min(in.available(), buf.length);
            eventLog.trace("Attempting to read {} bytes from serial port", max);
            while (max > 0 && (len = in.read(buf, 0, max)) > 0) {
                sink.write(buf, 0, len);
                sinkSize += len;

                if (append) {
                    // if we've collected at least desiredSize bytes, we're done
                    if (sinkSize >= readLength) {
                        if (eventLog.isDebugEnabled()) {
                            eventLog.debug("Got desired {}  bytes of data: {}", readLength,
                                    asciiDebugValue(sink.toByteArray()));
                        }
                        return true;
                    }
                    eventLog.debug("Looking for {} more bytes of data", (readLength - sinkSize));
                    return false;
                } else {
                    eventLog.trace("Looking for {} magic bytes 0x{}", magicBytes.length,
                            Hex.encodeHexString(magicBytes));
                }

                // look for magic in the buffer
                int magicIdx = 0;
                byte[] sinkBuf = sink.toByteArray();
                boolean found = false;
                for (; magicIdx < (sinkBuf.length - magicBytes.length + 1); magicIdx++) {
                    found = true;
                    for (int j = 0; j < magicBytes.length; j++) {
                        if (sinkBuf[magicIdx + j] != magicBytes[j]) {
                            found = false;
                            break;
                        }
                    }
                    if (found) {
                        break;
                    }
                }

                if (found) {
                    // magic found!
                    if (eventLog.isTraceEnabled()) {
                        eventLog.trace("Found magic bytes " + asciiDebugValue(magicBytes) + " at buffer index "
                                + magicIdx);
                    }

                    // skip over magic bytes
                    magicIdx += magicBytes.length;

                    int count = readLength;
                    count = Math.min(readLength, sinkBuf.length - magicIdx);
                    sink.reset();
                    sink.write(sinkBuf, magicIdx, count);
                    sinkSize = count;
                    if (eventLog.isTraceEnabled()) {
                        eventLog.trace("Sink contains {} bytes: {}", sinkSize, asciiDebugValue(sink.toByteArray()));
                    }
                    if (sinkSize >= readLength) {
                        // we got all the data here... we're done
                        return true;
                    }
                    eventLog.trace("Need {} more bytes of data", (readLength - sinkSize));
                    append = true;
                } else if (sinkBuf.length > magicBytes.length) {
                    // haven't found the magic yet, and the sink is larger than magic size, so 
                    // trim sink down to just magic size
                    sink.reset();
                    sink.write(sinkBuf, sinkBuf.length - magicBytes.length - 1, magicBytes.length);
                    sinkSize = magicBytes.length;
                }
            }
        } catch (IOException e) {
            log.error("Error reading from serial port: {}", e.getMessage());
            throw new RuntimeException(e);
        }

        if (eventLog.isTraceEnabled()) {
            eventLog.trace("Need {} more bytes of data, buffer: {}", (readLength - sinkSize),
                    asciiDebugValue(sink.toByteArray()));
        }
        return false;
    }

    protected final void readAvailable(InputStream in, ByteArrayOutputStream sink) {
        byte[] buf = new byte[1024];
        try {
            int len = -1;
            while (in.available() > 0 && (len = in.read(buf, 0, buf.length)) > 0) {
                sink.write(buf, 0, len);
            }
        } catch (IOException e) {
            log.warn("IOException reading serial data: {}", e.getMessage());
        }
        if (eventLog.isTraceEnabled()) {
            eventLog.trace("Finished reading data: {}", asciiDebugValue(sink.toByteArray()));
        }
    }

    private boolean findEOFBytes(ByteArrayOutputStream sink, int appendedLength, byte[] eofBytes) {
        byte[] sinkBuf = sink.toByteArray();
        int eofIdx = Math.max(0, sinkBuf.length - appendedLength - eofBytes.length);
        boolean foundEOF = false;
        for (; eofIdx < (sinkBuf.length - eofBytes.length); eofIdx++) {
            foundEOF = true;
            for (int j = 0; j < eofBytes.length; j++) {
                if (sinkBuf[eofIdx + j] != eofBytes[j]) {
                    foundEOF = false;
                    break;
                }
            }
            if (foundEOF) {
                break;
            }
        }
        if (foundEOF) {
            if (eventLog.isDebugEnabled()) {
                eventLog.debug("Found desired {} EOF bytes at index {}", asciiDebugValue(eofBytes), eofIdx);
            }
            sink.reset();
            sink.write(sinkBuf, 0, eofIdx + eofBytes.length);
            if (eventLog.isDebugEnabled()) {
                eventLog.debug("Buffer message at EOF: {}", asciiDebugValue(sink.toByteArray()));
            }
            return true;
        }
        eventLog.debug("Looking for EOF bytes {}", asciiDebugValue(eofBytes));
        return false;
    }

    /**
     * Handle a SerialEvent, looking for "magic" start and end data markers.
     * 
     * <p>
     * <b>Note</b> that the <em>magic</em> bytes <em>are</em> returned by this
     * method.
     * </p>
     * 
     * @param event
     *        the event
     * @param in
     *        the InputStream to read data from
     * @param sink
     *        the output buffer to store the collected bytes
     * @param magicBytes
     *        the "magic" bytes to look for in the event stream
     * @param eofBytes
     *        the "end of file" bytes, that signals the end of the message to
     *        read
     * @return <em>true</em> if the data has been found
     * @throws TimeoutException
     *         if {@code maxWait} is configured and that amount of time passes
     *         before the requested serial data is read
     */
    protected boolean handleSerialEvent(final SerialPortEvent event, final InputStream in,
            final ByteArrayOutputStream sink, final byte[] magicBytes, final byte[] eofBytes)
            throws TimeoutException, InterruptedException, ExecutionException {
        if (timeout < 1) {
            return handleSerialEventWithoutTimeout(event, in, sink, magicBytes, eofBytes);
        }

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

            @Override
            public Boolean call() throws Exception {
                return handleSerialEventWithoutTimeout(event, in, sink, magicBytes, eofBytes);
            }
        };
        Future<Boolean> future = executor.submit(task);
        boolean result = false;
        final long maxMs = Math.max(1, this.maxWait - System.currentTimeMillis() + timeout);
        eventLog.trace("Waiting at most {}ms for data", maxMs);
        try {
            result = future.get(maxMs, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            log.debug("Interrupted waiting for serial data");
            throw e;
        } catch (ExecutionException e) {
            // log stack trace in DEBUG
            log.debug("Exception thrown reading from serial port", e.getCause());
            throw e;
        } catch (TimeoutException e) {
            log.warn("Timeout waiting {}ms for serial data, aborting read", maxMs);
            future.cancel(true);
            throw e;
        }
        return result;
    }

    /**
     * Read from the InputStream until it is empty.
     * 
     * @param in
     */
    protected void drainInputStream(InputStream in) {
        byte[] buf = new byte[1024];
        int len = -1;
        int total = 0;
        try {
            final int max = Math.min(in.available(), buf.length);
            eventLog.trace("Attempting to drain {} bytes from serial port", max);
            while (max > 0 && (len = in.read(buf, 0, max)) > 0) {
                // keep draining
                total += len;
            }
        } catch (IOException e) {
            // ignore this
        }
        eventLog.trace("Drained {} bytes from serial port", total);
    }

    private boolean handleSerialEventWithoutTimeout(SerialPortEvent event, InputStream in,
            ByteArrayOutputStream sink, byte[] magicBytes, byte[] eofBytes) {
        int sinkSize = sink.size();
        boolean append = sinkSize > 0;
        byte[] buf = new byte[1024];
        if (eventLog.isTraceEnabled()) {
            eventLog.trace("Sink contains {} bytes: {}", sinkSize, asciiDebugValue(sink.toByteArray()));
        }
        try {
            int len = -1;
            final int max = Math.min(in.available(), buf.length);
            eventLog.trace("Attempting to read {} bytes from serial port", max);
            while (max > 0 && (len = in.read(buf, 0, max)) > 0) {
                sink.write(buf, 0, len);
                sinkSize += len;

                if (append) {
                    // look for eofBytes, starting where we last appended
                    if (findEOFBytes(sink, len, eofBytes)) {
                        if (eventLog.isDebugEnabled()) {
                            eventLog.debug("Found desired EOF bytes: {}", asciiDebugValue(eofBytes));
                        }
                        return true;
                    }
                    eventLog.debug("Looking for EOF bytes {}", asciiDebugValue(eofBytes));
                    return false;
                } else {
                    eventLog.trace("Looking for {} magic bytes {} in buffer {}", new Object[] { magicBytes.length,
                            asciiDebugValue(magicBytes), asciiDebugValue(sink.toByteArray()) });
                }

                // look for magic in the buffer
                int magicIdx = 0;
                byte[] sinkBuf = sink.toByteArray();
                boolean found = false;
                for (; magicIdx < (sinkBuf.length - magicBytes.length + 1); magicIdx++) {
                    found = true;
                    for (int j = 0; j < magicBytes.length; j++) {
                        if (sinkBuf[magicIdx + j] != magicBytes[j]) {
                            found = false;
                            break;
                        }
                    }
                    if (found) {
                        break;
                    }
                }

                if (found) {
                    // magic found!
                    if (eventLog.isTraceEnabled()) {
                        eventLog.trace("Found magic bytes " + asciiDebugValue(magicBytes) + " at buffer index "
                                + magicIdx);
                    }

                    int count = buf.length;
                    count = Math.min(buf.length, sinkBuf.length - magicIdx);
                    sink.reset();
                    sink.write(sinkBuf, magicIdx, count);
                    sinkSize = count;
                    if (eventLog.isTraceEnabled()) {
                        eventLog.trace("Sink contains {} bytes: {}", sinkSize, asciiDebugValue(sink.toByteArray()));
                    }
                    if (findEOFBytes(sink, len, eofBytes)) {
                        // we got all the data here... we're done
                        return true;
                    }
                    append = true;
                } else if (sinkBuf.length > magicBytes.length) {
                    // haven't found the magic yet, and the sink is larger than magic size, so 
                    // trim sink down to just magic size
                    sink.reset();
                    sink.write(sinkBuf, sinkBuf.length - magicBytes.length, magicBytes.length);
                    sinkSize = magicBytes.length;
                }
            }
        } catch (IOException e) {
            log.error("Error reading from serial port: {}", e.getMessage());
            throw new RuntimeException(e);
        }

        if (eventLog.isTraceEnabled()) {
            eventLog.debug("Looking for bytes {}, buffer: {}",
                    (append ? asciiDebugValue(eofBytes) : asciiDebugValue(magicBytes)),
                    asciiDebugValue(sink.toByteArray()));
        }
        return false;
    }

    protected final 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;
    }

    public long getMaxWait() {
        return maxWait;
    }

}