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