Files
quality_recticel/tray/src/qz/communication/SerialIO.java
2025-10-05 14:32:47 -04:00

298 lines
12 KiB
Java
Executable File

package qz.communication;
import jssc.*;
import org.apache.commons.codec.binary.StringUtils;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.common.ByteArrayBuilder;
import qz.utils.ByteUtilities;
import qz.utils.DeviceUtilities;
import qz.ws.SocketConnection;
import java.io.IOException;
/**
* @author Tres
*/
public class SerialIO implements DeviceListener {
private static final Logger log = LogManager.getLogger(SerialIO.class);
// Timeout to wait before giving up on reading the specified amount of bytes
private static final int TIMEOUT = 1200;
private String portName;
private SerialPort port;
private SerialOptions serialOpts;
private ByteArrayBuilder data = new ByteArrayBuilder();
private SocketConnection websocket;
/**
* Controller for serial communications
*
* @param portName Port name to open, such as "COM1" or "/dev/tty0/"
*/
public SerialIO(String portName, SocketConnection websocket) {
this.portName = portName;
this.websocket = websocket;
}
/**
* Open the specified port name.
*
* @param opts Parsed serial options
* @return Boolean indicating success.
* @throws SerialPortException If the port fails to open.
*/
public boolean open(SerialOptions opts) throws SerialPortException {
if (isOpen()) {
log.warn("Serial port [{}] is already open", portName);
return false;
}
port = new SerialPort(portName);
port.openPort();
serialOpts = new SerialOptions();
setOptions(opts);
return port.isOpened();
}
public void applyPortListener(SerialPortEventListener listener) throws SerialPortException {
port.addEventListener(listener);
}
/**
* @return Boolean indicating if port is currently open.
*/
public boolean isOpen() {
return port != null && port.isOpened();
}
public String processSerialEvent(SerialPortEvent event) {
SerialOptions.ResponseFormat format = serialOpts.getResponseFormat();
try {
// Receive data
if (event.isRXCHAR()) {
data.append(port.readBytes(event.getEventValue(), TIMEOUT));
String response = null;
if (format.isBoundNewline()) {
//process as line delimited
// check for CR AND NL
Integer endIdx = ByteUtilities.firstMatchingIndex(data.getByteArray(), new byte[] {'\r', '\n'});
int delimSize = 2;
// check for CR OR NL
if(endIdx == null) {
endIdx = min(
ByteUtilities.firstMatchingIndex(data.getByteArray(), new byte[] {'\r'}),
ByteUtilities.firstMatchingIndex(data.getByteArray(), new byte[] {'\n'}));
delimSize = 1;
}
if (endIdx != null) {
log.trace("Reading newline-delimited response");
byte[] output = new byte[endIdx];
System.arraycopy(data.getByteArray(), 0, output, 0, endIdx);
String buffer = new String(output, format.getEncoding());
if (!buffer.isEmpty()) {
//send non-empty string
response = buffer;
}
data.clearRange(0, endIdx + delimSize);
}
} else if (format.getBoundStart() != null && format.getBoundStart().length > 0) {
//process as formatted response
Integer startIdx = ByteUtilities.firstMatchingIndex(data.getByteArray(), format.getBoundStart());
if (startIdx != null) {
int startOffset = startIdx + format.getBoundStart().length;
int copyLength = 0;
int endIdx = 0;
if (format.getBoundEnd() != null && format.getBoundEnd().length > 0) {
//process as bounded response
Integer boundEnd = ByteUtilities.firstMatchingIndex(data.getByteArray(), format.getBoundEnd(), startIdx);
if (boundEnd != null) {
log.trace("Reading bounded response");
copyLength = boundEnd - startOffset;
endIdx = boundEnd + 1;
if (format.isIncludeStart()) {
//also include the ending bytes
copyLength += format.getBoundEnd().length;
}
}
} else if (format.getFixedWidth() > 0) {
//process as fixed length prefixed response
log.trace("Reading fixed length prefixed response");
copyLength = format.getFixedWidth();
endIdx = startOffset + format.getFixedWidth();
} else if (format.getLength() != null) {
//process as dynamic formatted response
SerialOptions.ByteParam lengthParam = format.getLength();
if (data.getLength() > startOffset + lengthParam.getIndex() + lengthParam.getLength()) { //ensure there's length bytes to read
log.trace("Reading dynamic formatted response");
int expectedLength = ByteUtilities.parseBytes(data.getByteArray(), startOffset + lengthParam.getIndex(), lengthParam.getLength(), lengthParam.getEndian());
log.trace("Found length byte, expecting {} bytes", expectedLength);
startOffset += lengthParam.getIndex() + lengthParam.getLength(); // don't include the length byte(s) in the response
copyLength = expectedLength;
endIdx = startOffset + copyLength;
if (format.getCrc() != null) {
SerialOptions.ByteParam crcParam = format.getCrc();
log.trace("Expecting {} crc bytes", crcParam.getLength());
int expand = crcParam.getIndex() + crcParam.getLength();
//include crc in copy
copyLength += expand;
endIdx += expand;
}
}
} else {
//process as header formatted raw response - high risk of lost data, likely unintended settings
log.warn("Reading header formatted raw response, are you missing an rx option?");
copyLength = data.getLength() - startOffset;
endIdx = data.getLength();
}
if (copyLength > 0 && data.getLength() >= endIdx) {
log.debug("Response format readable, starting copy");
if (format.isIncludeStart()) {
//increase length to account for header bytes and bump offset back to include in copy
copyLength += (startOffset - startIdx);
startOffset = startIdx;
}
byte[] responseData = new byte[copyLength];
System.arraycopy(data.getByteArray(), startOffset, responseData, 0, copyLength);
response = new String(responseData, format.getEncoding());
data.clearRange(startIdx, endIdx);
}
}
} else if (format.getFixedWidth() > 0) {
if (data.getLength() >= format.getFixedWidth()) {
//process as fixed width response
log.trace("Reading fixed length response");
byte[] output = new byte[format.getFixedWidth()];
System.arraycopy(data.getByteArray(), 0, output, 0, format.getFixedWidth());
response = StringUtils.newStringUtf8(output);
data.clearRange(0, format.getFixedWidth());
}
} else {
//no processing, return raw
log.trace("Reading raw response");
response = new String(data.getByteArray(), format.getEncoding());
data.clear();
}
return response;
}
}
catch(SerialPortException e) {
log.error("Exception occurred while reading data from port.", e);
}
catch(SerialPortTimeoutException e) {
log.error("Timeout occurred waiting for port to respond.", e);
}
return null;
}
/**
* Sets and caches the properties as to not set them every data call
*
* @throws SerialPortException If the properties fail to set
*/
private void setOptions(SerialOptions opts) throws SerialPortException {
if (opts == null) { return; }
SerialOptions.PortSettings ps = opts.getPortSettings();
if (ps != null && !ps.equals(serialOpts.getPortSettings())) {
log.debug("Applying new port settings");
port.setParams(ps.getBaudRate(), ps.getDataBits(), ps.getStopBits(), ps.getParity());
port.setFlowControlMode(ps.getFlowControl());
serialOpts.setPortSettings(ps);
}
SerialOptions.ResponseFormat rf = opts.getResponseFormat();
if (rf != null) {
log.debug("Applying new response formatting");
serialOpts.setResponseFormat(rf);
}
}
/**
* Applies the port parameters and writes the buffered data to the serial port.
*/
public void sendData(JSONObject params, SerialOptions opts) throws JSONException, IOException, SerialPortException {
if (opts != null) {
setOptions(opts);
}
log.debug("Sending data over [{}]", portName);
port.writeBytes(DeviceUtilities.getDataBytes(params, serialOpts.getPortSettings().getEncoding()));
}
/**
* Closes the serial port, if open.
*
* @throws SerialPortException If the port fails to close.
*/
@Override
public void close() {
// Remove orphaned reference
websocket.removeSerialPort(portName);
if (!isOpen()) {
log.warn("Serial port [{}] is not open.", portName);
}
try {
boolean closed = port.closePort();
if (closed) {
log.info("Serial port [{}] closed successfully.", portName);
} else {
// Handle ambiguity in JSSCs API
throw new SerialPortException(portName, "closePort", "Port not closed");
}
} catch(SerialPortException e) {
log.warn("Serial port [{}] was not closed properly.", portName);
}
port = null;
portName = null;
}
private Integer min(Integer a, Integer b) {
if (a == null) { return b; }
if (b == null) { return a; }
return Math.min(a, b);
}
}