Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion src/qz/communication/SerialOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ public class SerialOptions {

private PortSettings portSettings = null;
private ResponseFormat responseFormat = null;
private boolean rxExplicitlySet = false;
private boolean portSettingsExplicitlySet = false;
private boolean encodingExplicitlySet = false;

/**
* Creates an empty/default options object
Expand All @@ -41,8 +44,11 @@ public SerialOptions(JSONObject serialOpts, boolean isOpening) {
if (serialOpts == null) { return; }

//only apply port settings if opening or explicitly set in a send data call
if (isOpening || serialOpts.has("baudRate") || serialOpts.has("dataBits") || serialOpts.has("stopBits") || serialOpts.has("parity") || serialOpts.has("flowControl")) {
boolean hasCoreSettings = serialOpts.has("baudRate") || serialOpts.has("dataBits") || serialOpts.has("stopBits") || serialOpts.has("parity") || serialOpts.has("flowControl");
boolean hasEncoding = serialOpts.has("encoding");
if (isOpening || hasCoreSettings || hasEncoding) {
portSettings = new PortSettings();
portSettingsExplicitlySet = hasCoreSettings;

if (!serialOpts.isNull("baudRate")) {
try { portSettings.baudRate = SerialUtilities.parseBaudRate(serialOpts.getString("baudRate")); }
Expand Down Expand Up @@ -70,13 +76,15 @@ public SerialOptions(JSONObject serialOpts, boolean isOpening) {
}

if (!serialOpts.isNull("encoding") && !serialOpts.optString("encoding").isEmpty()) {
encodingExplicitlySet = true;
try { portSettings.encoding = Charset.forName(serialOpts.getString("encoding")); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "encoding", serialOpts.opt("encoding")); }
}
}

if (!serialOpts.isNull("rx")) {
responseFormat = new ResponseFormat();
rxExplicitlySet = true;
//Make the response encoding default to the port encoding. If this is removed it will default to UTF-8
responseFormat.encoding = portSettings.encoding;

Expand Down Expand Up @@ -190,6 +198,11 @@ public SerialOptions(JSONObject serialOpts, boolean isOpening) {
// legacy support - only applies on port open
responseFormat = new ResponseFormat();

// Mark as explicit if any legacy rx option is provided
if (serialOpts.has("start") || serialOpts.has("end") || serialOpts.has("width")) {
rxExplicitlySet = true;
}

// legacy start only supports string, not an array
if (!serialOpts.isNull("start")) {
responseFormat.boundStart = DeviceUtilities.characterBytes(serialOpts.optString("start", DEFAULT_BEGIN), responseFormat.encoding);
Expand Down Expand Up @@ -223,6 +236,18 @@ public ResponseFormat getResponseFormat() {
return responseFormat;
}

public boolean isRxExplicitlySet() {
return rxExplicitlySet;
}

public boolean isPortSettingsExplicitlySet() {
return portSettingsExplicitlySet;
}

public boolean isEncodingExplicitlySet() {
return encodingExplicitlySet;
}

public void setPortSettings(PortSettings portSettings) {
this.portSettings = portSettings;
}
Expand Down Expand Up @@ -325,6 +350,20 @@ public boolean isIncludeStart() {
public boolean isBoundNewline() {
return boundNewline;
}

@Override
public boolean equals(Object o) {
if (!(o instanceof ResponseFormat)) return false;
ResponseFormat that = (ResponseFormat)o;
return getEncoding().equals(that.getEncoding()) &&
java.util.Arrays.equals(getBoundStart(), that.getBoundStart()) &&
java.util.Arrays.equals(getBoundEnd(), that.getBoundEnd()) &&
isBoundNewline() == that.isBoundNewline() &&
getFixedWidth() == that.getFixedWidth() &&
isIncludeStart() == that.isIncludeStart() &&
java.util.Objects.equals(getLength(), that.getLength()) &&
java.util.Objects.equals(getCrc(), that.getCrc());
}
}

public class ByteParam {
Expand All @@ -345,6 +384,15 @@ public int getLength() {
public ByteUtilities.Endian getEndian() {
return endian;
}

@Override
public boolean equals(Object o) {
if (!(o instanceof ByteParam)) return false;
ByteParam that = (ByteParam)o;
return getIndex() == that.getIndex() &&
getLength() == that.getLength() &&
getEndian() == that.getEndian();
}
}

}
168 changes: 168 additions & 0 deletions src/qz/communication/SerialPortMonitor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package qz.communication;

import jssc.SerialPortException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.eclipse.jetty.websocket.api.Session;
import qz.ws.SocketConnection;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class SerialPortMonitor {
private static final Logger log = LogManager.getLogger(SerialPortMonitor.class);

private static final HashMap<String, SharedSerialPort> openPorts = new HashMap<>();
private static final HashMap<SocketConnection, SerialSession> serialSessions = new HashMap<>();
private static final HashMap<String, List<SocketConnection>> portListeners = new HashMap<>();

public synchronized static boolean startListening(SocketConnection connection, Session session,
String portName, SerialOptions opts) throws SerialPortException {
if (isListening(connection, portName)) {
log.warn("Connection already listening to port [{}]", portName);
return true;
}

SerialSession serialSession = serialSessions.computeIfAbsent(connection, c -> new SerialSession(session));

SharedSerialPort sharedPort = openPorts.get(portName);
if (sharedPort == null) {
sharedPort = new SharedSerialPort(portName);
final String pn = portName;
sharedPort.setOnCloseCallback(() -> {
synchronized (SerialPortMonitor.class) {
openPorts.remove(pn);
portListeners.remove(pn);
log.info("Shared port [{}] removed from monitor", pn);
}
});

if (!sharedPort.open(opts)) {
log.error("Failed to open serial port [{}]", portName);
return false;
}

openPorts.put(portName, sharedPort);
log.info("Opened new shared port [{}]", portName);
} else {
if (opts != null && opts.isPortSettingsExplicitlySet()) {
SerialOptions.PortSettings existingSettings = sharedPort.getPortSettings();
SerialOptions.PortSettings requestedSettings = opts.getPortSettings();

if (requestedSettings != null && existingSettings != null && !requestedSettings.equals(existingSettings)) {
throw new SerialPortException(portName, "openPort",
"Port is already open with different settings. Remove explicit settings to join the shared port.");
}
}

if (opts != null && opts.isEncodingExplicitlySet()) {
SerialOptions.PortSettings existingSettings = sharedPort.getPortSettings();
SerialOptions.PortSettings requestedSettings = opts.getPortSettings();

if (requestedSettings != null && existingSettings != null &&
!requestedSettings.getEncoding().equals(existingSettings.getEncoding())) {
throw new SerialPortException(portName, "openPort",
"Port is already open with different encoding. Remove encoding option to join the shared port.");
}
}

if (opts != null && opts.isRxExplicitlySet()) {
SerialOptions.ResponseFormat existingFormat = sharedPort.getResponseFormat();
SerialOptions.ResponseFormat requestedFormat = opts.getResponseFormat();

if (requestedFormat != null && existingFormat != null && !requestedFormat.equals(existingFormat)) {
throw new SerialPortException(portName, "openPort",
"Port is already open with different rx settings. Remove rx options to join the shared port.");
}
}
}

sharedPort.addListener(connection, serialSession);
portListeners.computeIfAbsent(portName, k -> new ArrayList<>()).add(connection);

log.info("Connection now listening to port [{}], total listeners: {}", portName, sharedPort.getListenerCount());
return true;
}

public synchronized static void stopListening(SocketConnection connection, String portName) {
SharedSerialPort sharedPort = openPorts.get(portName);
if (sharedPort == null) {
return;
}

boolean portClosed = sharedPort.removeListener(connection);

List<SocketConnection> listeners = portListeners.get(portName);
if (listeners != null) {
listeners.remove(connection);
if (listeners.isEmpty()) {
portListeners.remove(portName);
}
}

if (portClosed) {
log.info("Port [{}] closed (last listener removed)", portName);
}
}

public synchronized static void stopListening(SocketConnection connection) {
List<String> portsToRemove = new ArrayList<>();
for (Map.Entry<String, List<SocketConnection>> entry : portListeners.entrySet()) {
if (entry.getValue().contains(connection)) {
portsToRemove.add(entry.getKey());
}
}

for (String portName : portsToRemove) {
stopListening(connection, portName);
}

serialSessions.remove(connection);
}

public synchronized static boolean isListening(SocketConnection connection, String portName) {
List<SocketConnection> listeners = portListeners.get(portName);
return listeners != null && listeners.contains(connection);
}

public synchronized static void sendData(SocketConnection connection, String portName,
JSONObject params, SerialOptions opts)
throws JSONException, IOException, SerialPortException {

if (!isListening(connection, portName)) {
throw new SerialPortException(portName, "sendData", "Connection is not listening to this port");
}

SharedSerialPort sharedPort = openPorts.get(portName);
if (sharedPort == null || !sharedPort.isOpen()) {
throw new SerialPortException(portName, "sendData", "Port is not open");
}

sharedPort.sendData(params, opts);
}

public synchronized static int getListenerCount(String portName) {
SharedSerialPort sharedPort = openPorts.get(portName);
return sharedPort != null ? sharedPort.getListenerCount() : 0;
}

public synchronized static boolean isPortOpen(String portName) {
SharedSerialPort sharedPort = openPorts.get(portName);
return sharedPort != null && sharedPort.isOpen();
}

public synchronized static List<String> getPortsForConnection(SocketConnection connection) {
List<String> ports = new ArrayList<>();
for (Map.Entry<String, List<SocketConnection>> entry : portListeners.entrySet()) {
if (entry.getValue().contains(connection)) {
ports.add(entry.getKey());
}
}
return ports;
}
}
28 changes: 28 additions & 0 deletions src/qz/communication/SerialSession.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package qz.communication;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.jetty.websocket.api.Session;
import qz.ws.PrintSocketClient;
import qz.ws.StreamEvent;

public class SerialSession {
private static final Logger log = LogManager.getLogger(SerialSession.class);

private final Session session;

public SerialSession(Session session) {
this.session = session;
}

public Session getSession() {
return session;
}

public void sendSerialEvent(String portName, String output, Runnable closeHandler) {
StreamEvent event = new StreamEvent(StreamEvent.Stream.SERIAL, StreamEvent.Type.RECEIVE)
.withData("portName", portName)
.withData("output", output);
PrintSocketClient.sendStream(session, event, closeHandler);
}
}
Loading