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