/* =================================================================== * 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; } }